diff --git a/api/openapi.yaml b/api/openapi.yaml
index 3f18782..430f418 100644
--- a/api/openapi.yaml
+++ b/api/openapi.yaml
@@ -1932,9 +1932,9 @@ components:
event_seq: { type: integer, format: int64, description: "The task's latest event when the snapshot was started: the tree holds at least the work up to it." }
trigger:
type: string
- enum: [message, turn_end, tool_call, ask_human, timer, requested, answer, fork]
- x-enum-varnames: [SnapshotTriggerMessage, SnapshotTriggerTurnEnd, SnapshotTriggerToolCall, SnapshotTriggerAskHuman, SnapshotTriggerTimer, SnapshotTriggerRequested, SnapshotTriggerAnswer, SnapshotTriggerFork]
- description: "turn_end: after byre's end-of-turn commit. requested: a task's agent or the lead asked for it with take_snapshot. answer: just before answers from the human were handed to the agent. fork: when the task's agent forked the task with its fork tool. ask_human: after byre committed the worktree inside an ask_human call whose task is behind the default branch, just before suggesting the agent merge it in. answer, fork and ask_human snapshots are never reviewed. message (the agent wrote text after one or more tool calls), tool_call and timer appear only on snapshots taken by earlier builds."
+ enum: [turn_end, ask_human, requested, answer, fork]
+ x-enum-varnames: [SnapshotTriggerTurnEnd, SnapshotTriggerAskHuman, SnapshotTriggerRequested, SnapshotTriggerAnswer, SnapshotTriggerFork]
+ description: "turn_end: after byre's end-of-turn commit. requested: a task's agent or the lead asked for it with take_snapshot. answer: just before answers from the human were handed to the agent. fork: when the task's agent forked the task with its fork tool. ask_human: after byre committed the worktree inside an ask_human call whose task is behind the default branch, just before suggesting the agent merge it in. answer, fork and ask_human snapshots are never reviewed."
created_at: { type: string, format: date-time }
Preview:
diff --git a/docs/operations.md b/docs/operations.md
index 914e1ae..d84dc54 100644
--- a/docs/operations.md
+++ b/docs/operations.md
@@ -8,6 +8,8 @@ The data directory must remain private to the service's OS user (0700): it conta
Add SQL migrations in `internal/store/migrations/` as `NNN-name.sql`. The number only orders; a migration is identified by its name. A migration's SQL is fixed once applied anywhere: write it out in full rather than sharing Go constants that later change. Concurrent task branches can each add migrations, so after a merge a migration can land before others the database has already applied, and it still runs. Give a new migration a name no other branch will use, and make any data migration safe to run twice. `sqlite3 byre.db 'SELECT * FROM migrations'` lists what has been applied.
+Migrations up to a threshold are folded into `100-baseline.sql`, the schema as they left it, and no data conversion survives below it. A database that hasn't applied the migration `baselineThrough` names (in `internal/store/store.go`) is refused at startup as too old; one that has is marked as on the baseline. To move the threshold up, apply the migrations to be folded to an empty database, dump its schema into the baseline file in place of the old one, delete those migrations' files, and set `baselineThrough` to the last one's name.
+
## Worktrees
byre runs nothing in a new worktree. Installing dependencies or copying untracked files such as `.env` is the project's own instruction in its AGENTS.md, carried out by the agent with its own tools.
diff --git a/internal/agents/bash.go b/internal/agents/bash.go
index ccf372a..09838f2 100644
--- a/internal/agents/bash.go
+++ b/internal/agents/bash.go
@@ -41,19 +41,14 @@ func (m *Manager) bashTool(l *live) mcp.Tool {
},
"required": ["command"]
}`),
- Handler: func(ctx context.Context, raw json.RawMessage) (string, error) {
- return m.bash(ctx, l, raw)
- },
+ Handler: mcp.Typed(func(ctx context.Context, args bashArgs) (string, error) {
+ return m.bash(ctx, l, args)
+ }),
}
}
-func (m *Manager) bash(ctx context.Context, l *live, raw json.RawMessage) (string, error) {
- args := bashArgs{}
- err := json.Unmarshal(raw, &args)
- if err != nil {
- return "", fmt.Errorf("malformed bash call: %w", err)
- }
- err = args.check()
+func (m *Manager) bash(ctx context.Context, l *live, args bashArgs) (string, error) {
+ err := args.check()
if err != nil {
return "", err
}
diff --git a/internal/agents/clients.go b/internal/agents/clients.go
index 0b0265f..04c2bd6 100644
--- a/internal/agents/clients.go
+++ b/internal/agents/clients.go
@@ -52,9 +52,7 @@ func (m *Manager) clientShellTools() []mcp.Tool {
},
"required": ["client", "command"]
}`),
- Handler: func(ctx context.Context, raw json.RawMessage) (string, error) {
- return m.clientBash(ctx, raw)
- },
+ Handler: mcp.Typed(m.clientBash),
},
}
}
@@ -74,9 +72,9 @@ func (m *Manager) clientTools(l *live) []mcp.Tool {
},
"required": ["client", "path", "dest"]
}`),
- Handler: func(ctx context.Context, raw json.RawMessage) (string, error) {
- return m.clientUpload(ctx, l, raw)
- },
+ Handler: mcp.Typed(func(ctx context.Context, args clientTransferArgs) (string, error) {
+ return m.clientUpload(ctx, l, args)
+ }),
},
{
Name: "client_open",
@@ -91,9 +89,9 @@ func (m *Manager) clientTools(l *live) []mcp.Tool {
},
"required": ["client", "path"]
}`),
- Handler: func(ctx context.Context, raw json.RawMessage) (string, error) {
- return m.clientOpen(ctx, l, raw)
- },
+ Handler: mcp.Typed(func(ctx context.Context, args clientOpenArgs) (string, error) {
+ return m.clientOpen(ctx, l, args)
+ }),
},
{
Name: "show_page",
@@ -111,9 +109,9 @@ func (m *Manager) clientTools(l *live) []mcp.Tool {
},
"required": ["client"]
}`),
- Handler: func(ctx context.Context, raw json.RawMessage) (string, error) {
- return m.showPage(ctx, l, raw)
- },
+ Handler: mcp.Typed(func(ctx context.Context, args showPageArgs) (string, error) {
+ return m.showPage(ctx, l, args)
+ }),
},
{
Name: "drive_page",
@@ -135,9 +133,9 @@ func (m *Manager) clientTools(l *live) []mcp.Tool {
},
"required": ["client", "op"]
}`),
- Rich: func(ctx context.Context, raw json.RawMessage) mcp.Result {
- return m.drivePage(ctx, l, raw)
- },
+ Rich: mcp.TypedRich(func(ctx context.Context, args drivePageArgs) mcp.Result {
+ return m.drivePage(ctx, l, args)
+ }),
},
{
Name: "show_file",
@@ -152,9 +150,9 @@ func (m *Manager) clientTools(l *live) []mcp.Tool {
},
"required": ["client", "path"]
}`),
- Handler: func(ctx context.Context, raw json.RawMessage) (string, error) {
- return m.show(ctx, l, raw, "show_file", api.OpenFile)
- },
+ Handler: mcp.Typed(func(ctx context.Context, args clientOpenArgs) (string, error) {
+ return m.show(ctx, l, args, api.OpenFile)
+ }),
},
{
Name: "client_download",
@@ -169,9 +167,9 @@ func (m *Manager) clientTools(l *live) []mcp.Tool {
},
"required": ["client", "path", "dest"]
}`),
- Handler: func(ctx context.Context, raw json.RawMessage) (string, error) {
- return m.clientDownload(ctx, l, raw)
- },
+ Handler: mcp.Typed(func(ctx context.Context, args clientTransferArgs) (string, error) {
+ return m.clientDownload(ctx, l, args)
+ }),
},
{
Name: "client_read",
@@ -187,9 +185,7 @@ func (m *Manager) clientTools(l *live) []mcp.Tool {
},
"required": ["client", "path"]
}`),
- Rich: func(ctx context.Context, raw json.RawMessage) mcp.Result {
- return m.clientRead(ctx, raw)
- },
+ Rich: mcp.TypedRich(m.clientRead),
},
{
Name: "client_write",
@@ -203,9 +199,7 @@ func (m *Manager) clientTools(l *live) []mcp.Tool {
},
"required": ["client", "path", "content"]
}`),
- Handler: func(ctx context.Context, raw json.RawMessage) (string, error) {
- return m.clientWrite(ctx, raw)
- },
+ Handler: mcp.Typed(m.clientWrite),
},
{
Name: "client_edit",
@@ -221,9 +215,7 @@ func (m *Manager) clientTools(l *live) []mcp.Tool {
},
"required": ["client", "path", "old_string", "new_string"]
}`),
- Handler: func(ctx context.Context, raw json.RawMessage) (string, error) {
- return m.clientEdit(ctx, raw)
- },
+ Handler: mcp.Typed(m.clientEdit),
},
{
Name: "client_grep",
@@ -240,9 +232,7 @@ func (m *Manager) clientTools(l *live) []mcp.Tool {
},
"required": ["client", "pattern", "path"]
}`),
- Handler: func(ctx context.Context, raw json.RawMessage) (string, error) {
- return m.clientGrep(ctx, raw)
- },
+ Handler: mcp.Typed(m.clientGrep),
},
{
Name: "client_list",
@@ -255,9 +245,7 @@ func (m *Manager) clientTools(l *live) []mcp.Tool {
},
"required": ["client", "path"]
}`),
- Handler: func(ctx context.Context, raw json.RawMessage) (string, error) {
- return m.clientList(ctx, raw)
- },
+ Handler: mcp.Typed(m.clientList),
},
{
Name: "client_hands",
@@ -275,9 +263,7 @@ func (m *Manager) clientTools(l *live) []mcp.Tool {
},
"required": ["client", "tool"]
}`),
- Rich: func(ctx context.Context, raw json.RawMessage) mcp.Result {
- return m.clientHands(ctx, raw)
- },
+ Rich: mcp.TypedRich(m.clientHands),
},
}...)
}
@@ -314,13 +300,8 @@ type clientBashArgs struct {
bashArgs
}
-func (m *Manager) clientBash(ctx context.Context, raw json.RawMessage) (string, error) {
- args := clientBashArgs{}
- err := json.Unmarshal(raw, &args)
- if err != nil {
- return "", fmt.Errorf("malformed client_bash call: %w", err)
- }
- err = args.check()
+func (m *Manager) clientBash(ctx context.Context, args clientBashArgs) (string, error) {
+ err := args.check()
if err != nil {
return "", err
}
@@ -349,13 +330,8 @@ func clientPath(name, p string) error {
return nil
}
-func (m *Manager) clientUpload(ctx context.Context, l *live, raw json.RawMessage) (string, error) {
- args := clientTransferArgs{}
- err := json.Unmarshal(raw, &args)
- if err != nil {
- return "", fmt.Errorf("malformed client_upload call: %w", err)
- }
- err = clientPath("dest", args.Dest)
+func (m *Manager) clientUpload(ctx context.Context, l *live, args clientTransferArgs) (string, error) {
+ err := clientPath("dest", args.Dest)
if err != nil {
return "", err
}
@@ -388,12 +364,7 @@ type clientOpenArgs struct {
Path string `json:"path"`
}
-func (m *Manager) clientOpen(ctx context.Context, l *live, raw json.RawMessage) (string, error) {
- args := clientOpenArgs{}
- err := json.Unmarshal(raw, &args)
- if err != nil {
- return "", fmt.Errorf("malformed client_open call: %w", err)
- }
+func (m *Manager) clientOpen(ctx context.Context, l *live, args clientOpenArgs) (string, error) {
full, err := l.resolve(args.Path)
if err != nil {
return "", err
@@ -425,17 +396,12 @@ type showPageArgs struct {
URL string `json:"url"`
}
-func (m *Manager) showPage(ctx context.Context, l *live, raw json.RawMessage) (string, error) {
- args := showPageArgs{}
- err := json.Unmarshal(raw, &args)
- if err != nil {
- return "", fmt.Errorf("malformed show_page call: %w", err)
- }
+func (m *Manager) showPage(ctx context.Context, l *live, args showPageArgs) (string, error) {
if (args.Path == "") == (args.URL == "") {
return "", fmt.Errorf("show_page takes path or url, not both or neither")
}
if args.Path != "" {
- return m.show(ctx, l, raw, "show_page", api.OpenPage)
+ return m.show(ctx, l, clientOpenArgs{Client: args.Client, Path: args.Path}, api.OpenPage)
}
u, err := url.Parse(args.URL)
if err != nil {
@@ -462,12 +428,7 @@ type drivePageArgs struct {
Code string `json:"code"`
}
-func (m *Manager) drivePage(ctx context.Context, l *live, raw json.RawMessage) mcp.Result {
- args := drivePageArgs{}
- err := json.Unmarshal(raw, &args)
- if err != nil {
- return mcp.TextResult(fmt.Sprintf("malformed drive_page call: %s", err), true)
- }
+func (m *Manager) drivePage(ctx context.Context, l *live, args drivePageArgs) mcp.Result {
req := api.ClientRequest{Task: &l.taskID, Tab: args.Tab}
switch args.Op {
case "tabs":
@@ -505,12 +466,7 @@ func (m *Manager) drivePage(ctx context.Context, l *live, raw json.RawMessage) m
return mcp.TextResult(*reply.Output, false)
}
-func (m *Manager) show(ctx context.Context, l *live, raw json.RawMessage, tool string, op api.ClientRequestOp) (string, error) {
- args := clientOpenArgs{}
- err := json.Unmarshal(raw, &args)
- if err != nil {
- return "", fmt.Errorf("malformed %s call: %w", tool, err)
- }
+func (m *Manager) show(ctx context.Context, l *live, args clientOpenArgs, op api.ClientRequestOp) (string, error) {
full, err := l.resolve(args.Path)
if err != nil {
return "", err
@@ -542,13 +498,8 @@ func (m *Manager) show(ctx context.Context, l *live, raw json.RawMessage, tool s
return fmt.Sprintf("Showing %s on %s.", shown, args.Client), nil
}
-func (m *Manager) clientDownload(ctx context.Context, l *live, raw json.RawMessage) (string, error) {
- args := clientTransferArgs{}
- err := json.Unmarshal(raw, &args)
- if err != nil {
- return "", fmt.Errorf("malformed client_download call: %w", err)
- }
- err = clientPath("path", args.Path)
+func (m *Manager) clientDownload(ctx context.Context, l *live, args clientTransferArgs) (string, error) {
+ err := clientPath("path", args.Path)
if err != nil {
return "", err
}
@@ -638,12 +589,7 @@ type clientReadArgs struct {
Limit *int `json:"limit"`
}
-func (m *Manager) clientRead(ctx context.Context, raw json.RawMessage) mcp.Result {
- args := clientReadArgs{}
- err := json.Unmarshal(raw, &args)
- if err != nil {
- return mcp.TextResult(fmt.Sprintf("malformed client_read call: %s", err), true)
- }
+func (m *Manager) clientRead(ctx context.Context, args clientReadArgs) mcp.Result {
data, err := m.readClientFile(ctx, args.Client, args.Path)
if err != nil {
return mcp.TextResult(err.Error(), true)
@@ -657,13 +603,8 @@ type clientWriteArgs struct {
Content string `json:"content"`
}
-func (m *Manager) clientWrite(ctx context.Context, raw json.RawMessage) (string, error) {
- args := clientWriteArgs{}
- err := json.Unmarshal(raw, &args)
- if err != nil {
- return "", fmt.Errorf("malformed client_write call: %w", err)
- }
- err = m.writeClientFile(ctx, args.Client, args.Path, []byte(args.Content))
+func (m *Manager) clientWrite(ctx context.Context, args clientWriteArgs) (string, error) {
+ err := m.writeClientFile(ctx, args.Client, args.Path, []byte(args.Content))
if err != nil {
return "", err
}
@@ -677,12 +618,7 @@ type clientEditArgs struct {
NewString string `json:"new_string"`
}
-func (m *Manager) clientEdit(ctx context.Context, raw json.RawMessage) (string, error) {
- args := clientEditArgs{}
- err := json.Unmarshal(raw, &args)
- if err != nil {
- return "", fmt.Errorf("malformed client_edit call: %w", err)
- }
+func (m *Manager) clientEdit(ctx context.Context, args clientEditArgs) (string, error) {
data, err := m.readClientFile(ctx, args.Client, args.Path)
if err != nil {
return "", err
@@ -703,12 +639,7 @@ type clientGrepArgs struct {
grepArgs
}
-func (m *Manager) clientGrep(ctx context.Context, raw json.RawMessage) (string, error) {
- args := clientGrepArgs{}
- err := json.Unmarshal(raw, &args)
- if err != nil {
- return "", fmt.Errorf("malformed client_grep call: %w", err)
- }
+func (m *Manager) clientGrep(ctx context.Context, args clientGrepArgs) (string, error) {
path, err := clientShellPath(args.Path)
if err != nil {
return "", err
@@ -731,12 +662,7 @@ type clientListArgs struct {
Path string `json:"path"`
}
-func (m *Manager) clientList(ctx context.Context, raw json.RawMessage) (string, error) {
- args := clientListArgs{}
- err := json.Unmarshal(raw, &args)
- if err != nil {
- return "", fmt.Errorf("malformed client_list call: %w", err)
- }
+func (m *Manager) clientList(ctx context.Context, args clientListArgs) (string, error) {
path, err := clientShellPath(args.Path)
if err != nil {
return "", err
@@ -792,12 +718,7 @@ type clientHandsArgs struct {
Arguments *map[string]any `json:"arguments"`
}
-func (m *Manager) clientHands(ctx context.Context, raw json.RawMessage) mcp.Result {
- args := clientHandsArgs{}
- err := json.Unmarshal(raw, &args)
- if err != nil {
- return mcp.TextResult(fmt.Sprintf("malformed client_hands call: %s", err), true)
- }
+func (m *Manager) clientHands(ctx context.Context, args clientHandsArgs) mcp.Result {
if args.Arguments == nil {
args.Arguments = &map[string]any{}
}
diff --git a/internal/agents/diagram.go b/internal/agents/diagram.go
index cda5a25..cc33efc 100644
--- a/internal/agents/diagram.go
+++ b/internal/agents/diagram.go
@@ -27,20 +27,15 @@ func (m *Manager) renderDiagramTool() mcp.Tool {
},
"required": ["source"]
}`),
- Rich: func(ctx context.Context, raw json.RawMessage) mcp.Result {
- args := struct {
- Source string `json:"source"`
- }{}
- err := json.Unmarshal(raw, &args)
- if err != nil {
- return mcp.TextResult(fmt.Sprintf("malformed render_diagram call: %v", err), true)
- }
+ Rich: mcp.TypedRich(func(ctx context.Context, args struct {
+ Source string `json:"source"`
+ }) mcp.Result {
png, err := renderDiagram(ctx, args.Source)
if err != nil {
return mcp.TextResult(err.Error(), true)
}
return mcp.Result{Content: []mcp.Content{{Type: "image", Data: base64.StdEncoding.EncodeToString(png), MimeType: "image/png"}}}
- },
+ }),
}
}
@@ -57,15 +52,10 @@ func (m *Manager) exportDiagramTool(l *live) mcp.Tool {
},
"required": ["source", "path"]
}`),
- Handler: func(ctx context.Context, raw json.RawMessage) (string, error) {
- args := struct {
- Source string `json:"source"`
- Path string `json:"path"`
- }{}
- err := json.Unmarshal(raw, &args)
- if err != nil {
- return "", fmt.Errorf("malformed export_diagram call: %w", err)
- }
+ Handler: mcp.Typed(func(ctx context.Context, args struct {
+ Source string `json:"source"`
+ Path string `json:"path"`
+ }) (string, error) {
full, err := l.resolve(args.Path)
if err != nil {
return "", err
@@ -83,7 +73,7 @@ func (m *Manager) exportDiagramTool(l *live) mcp.Tool {
return "", err
}
return fmt.Sprintf("Wrote the diagram to %s.", args.Path), nil
- },
+ }),
}
}
diff --git a/internal/agents/files.go b/internal/agents/files.go
index 879a34d..20d92b2 100644
--- a/internal/agents/files.go
+++ b/internal/agents/files.go
@@ -38,9 +38,9 @@ func (m *Manager) fileTools(l *live) []mcp.Tool {
},
"required": ["path"]
}`),
- Rich: func(ctx context.Context, raw json.RawMessage) mcp.Result {
- return m.read(l, raw)
- },
+ Rich: mcp.TypedRich(func(ctx context.Context, args readArgs) mcp.Result {
+ return m.read(l, args)
+ }),
},
{
Name: "write",
@@ -53,9 +53,9 @@ func (m *Manager) fileTools(l *live) []mcp.Tool {
},
"required": ["path", "content"]
}`),
- Handler: func(ctx context.Context, raw json.RawMessage) (string, error) {
- return m.write(l, raw)
- },
+ Handler: mcp.Typed(func(ctx context.Context, args writeArgs) (string, error) {
+ return m.write(l, args)
+ }),
},
{
Name: "edit",
@@ -70,9 +70,9 @@ func (m *Manager) fileTools(l *live) []mcp.Tool {
},
"required": ["path", "old_string", "new_string"]
}`),
- Handler: func(ctx context.Context, raw json.RawMessage) (string, error) {
- return m.edit(l, raw)
- },
+ Handler: mcp.Typed(func(ctx context.Context, args editArgs) (string, error) {
+ return m.edit(l, args)
+ }),
},
{
Name: "grep",
@@ -88,9 +88,9 @@ func (m *Manager) fileTools(l *live) []mcp.Tool {
},
"required": ["pattern"]
}`),
- Handler: func(ctx context.Context, raw json.RawMessage) (string, error) {
- return m.grep(ctx, l, raw)
- },
+ Handler: mcp.Typed(func(ctx context.Context, args grepArgs) (string, error) {
+ return m.grep(ctx, l, args)
+ }),
},
{
Name: "list",
@@ -101,9 +101,9 @@ func (m *Manager) fileTools(l *live) []mcp.Tool {
"path": {"type": "string", "description": "The directory, relative to your worktree, absolute, or starting with ~/ (default your worktree)."}
}
}`),
- Handler: func(ctx context.Context, raw json.RawMessage) (string, error) {
- return m.list(l, raw)
- },
+ Handler: mcp.Typed(func(ctx context.Context, args listArgs) (string, error) {
+ return m.list(l, args)
+ }),
},
}
}
@@ -156,12 +156,7 @@ type readArgs struct {
Limit *int `json:"limit"`
}
-func (m *Manager) read(l *live, raw json.RawMessage) mcp.Result {
- args := readArgs{}
- err := json.Unmarshal(raw, &args)
- if err != nil {
- return mcp.TextResult(fmt.Sprintf("malformed read call: %s", err), true)
- }
+func (m *Manager) read(l *live, args readArgs) mcp.Result {
full, err := l.resolve(args.Path)
if err != nil {
return mcp.TextResult(err.Error(), true)
@@ -178,12 +173,7 @@ type writeArgs struct {
Content string `json:"content"`
}
-func (m *Manager) write(l *live, raw json.RawMessage) (string, error) {
- args := writeArgs{}
- err := json.Unmarshal(raw, &args)
- if err != nil {
- return "", fmt.Errorf("malformed write call: %w", err)
- }
+func (m *Manager) write(l *live, args writeArgs) (string, error) {
full, err := l.resolve(args.Path)
if err != nil {
return "", err
@@ -209,12 +199,7 @@ type editArgs struct {
NewString string `json:"new_string"`
}
-func (m *Manager) edit(l *live, raw json.RawMessage) (string, error) {
- args := editArgs{}
- err := json.Unmarshal(raw, &args)
- if err != nil {
- return "", fmt.Errorf("malformed edit call: %w", err)
- }
+func (m *Manager) edit(l *live, args editArgs) (string, error) {
full, err := l.resolve(args.Path)
if err != nil {
return "", err
@@ -246,17 +231,13 @@ func (a grepArgs) options() grep.Options {
return grep.Options{Glob: a.Glob, IgnoreCase: a.IgnoreCase, FilesOnly: a.FilesOnly}
}
-func (m *Manager) grep(ctx context.Context, l *live, raw json.RawMessage) (string, error) {
- args := grepArgs{}
- err := json.Unmarshal(raw, &args)
- if err != nil {
- return "", fmt.Errorf("malformed grep call: %w", err)
- }
+func (m *Manager) grep(ctx context.Context, l *live, args grepArgs) (string, error) {
path := args.Path
if path == "" {
path = "."
}
if strings.HasPrefix(path, "~/") {
+ var err error
path, err = l.resolve(path)
if err != nil {
return "", err
@@ -269,12 +250,7 @@ type listArgs struct {
Path string `json:"path"`
}
-func (m *Manager) list(l *live, raw json.RawMessage) (string, error) {
- args := listArgs{}
- err := json.Unmarshal(raw, &args)
- if err != nil {
- return "", fmt.Errorf("malformed list call: %w", err)
- }
+func (m *Manager) list(l *live, args listArgs) (string, error) {
if args.Path == "" {
args.Path = "."
}
diff --git a/internal/agents/fork.go b/internal/agents/fork.go
index 3eca841..1e3997e 100644
--- a/internal/agents/fork.go
+++ b/internal/agents/fork.go
@@ -42,15 +42,10 @@ func (m *Manager) forkTool(l *live) mcp.Tool {
"title": {"type": "string", "description": "The new task's title, naming the path it takes."},
"prompt": {"type": "string", "description": "The forked agent's next message: tell it that it is the other side of a fork and which path to take."}
}, "required": ["title", "prompt"]}`),
- Handler: func(ctx context.Context, raw json.RawMessage) (string, error) {
- args := struct {
- Title string `json:"title"`
- Prompt string `json:"prompt"`
- }{}
- err := json.Unmarshal(raw, &args)
- if err != nil {
- return "", fmt.Errorf("malformed fork call: %w", err)
- }
+ Handler: mcp.Typed(func(ctx context.Context, args struct {
+ Title string `json:"title"`
+ Prompt string `json:"prompt"`
+ }) (string, error) {
if strings.TrimSpace(args.Title) == "" || strings.TrimSpace(args.Prompt) == "" {
return "", errors.New("title and prompt are required")
}
@@ -88,7 +83,7 @@ func (m *Manager) forkTool(l *live) mcp.Tool {
slog.Info("task forked by its agent", "task", src.Id, "fork", t.Id, "snapshot", snap.N)
return fmt.Sprintf("Forked at snapshot #%d as task %s %q, branch %s, worktree %s; its agent continues from just before this call with your prompt. Carry on here with your own path.",
snap.N, t.Id, t.Title, t.Branch, t.WorktreePath), nil
- },
+ }),
}
}
diff --git a/internal/agents/imagegen.go b/internal/agents/imagegen.go
index e0cff5e..a95491f 100644
--- a/internal/agents/imagegen.go
+++ b/internal/agents/imagegen.go
@@ -27,26 +27,21 @@ func (m *Manager) generateImageTool(l *live) mcp.Tool {
},
"required": ["prompt"]
}`),
- Handler: func(ctx context.Context, raw json.RawMessage) (string, error) {
- return m.generateImage(ctx, l, raw)
- },
+ Handler: mcp.Typed(func(ctx context.Context, args struct {
+ Prompt string `json:"prompt"`
+ }) (string, error) {
+ return m.generateImage(ctx, l, args.Prompt)
+ }),
}
}
-func (m *Manager) generateImage(ctx context.Context, l *live, raw json.RawMessage) (string, error) {
- args := struct {
- Prompt string `json:"prompt"`
- }{}
- err := json.Unmarshal(raw, &args)
- if err != nil {
- return "", fmt.Errorf("malformed generate_image call: %w", err)
- }
- if strings.TrimSpace(args.Prompt) == "" {
+func (m *Manager) generateImage(ctx context.Context, l *live, prompt string) (string, error) {
+ if strings.TrimSpace(prompt) == "" {
return "", fmt.Errorf("generate_image needs a prompt")
}
media := MediaDir(l.dir)
- err = os.MkdirAll(m.codexHome, 0o700)
+ err := os.MkdirAll(m.codexHome, 0o700)
if err != nil {
return "", err
}
@@ -60,7 +55,7 @@ func (m *Manager) generateImage(ctx context.Context, l *live, raw json.RawMessag
}
defer s.Close()
- _, err = s.Send(ctx, args.Prompt)
+ _, err = s.Send(ctx, prompt)
if err != nil {
return "", fmt.Errorf("start codex turn: %w", err)
}
diff --git a/internal/agents/inbox.go b/internal/agents/inbox.go
index b920f03..a497322 100644
--- a/internal/agents/inbox.go
+++ b/internal/agents/inbox.go
@@ -74,20 +74,14 @@ func (m *Manager) askTool(l *live) mcp.Tool {
},
"required": ["context", "question"]
}`),
- Handler: func(ctx context.Context, raw json.RawMessage) (string, error) {
- return m.ask(ctx, l, raw)
- },
+ Handler: mcp.Typed(func(ctx context.Context, args askArgs) (string, error) {
+ return m.ask(ctx, l, args)
+ }),
}
}
-func (m *Manager) ask(ctx context.Context, l *live, raw json.RawMessage) (string, error) {
- args := askArgs{}
- err := json.Unmarshal(raw, &args)
- if err != nil {
- return "", fmt.Errorf("malformed ask_human call: %w", err)
- }
-
- err = validateAsk(args)
+func (m *Manager) ask(ctx context.Context, l *live, args askArgs) (string, error) {
+ err := validateAsk(args)
if err != nil {
return "", err
}
@@ -205,19 +199,13 @@ func (m *Manager) cancelQuestionTool(l *live) mcp.Tool {
},
"required": ["question_id"]
}`),
- Handler: func(ctx context.Context, raw json.RawMessage) (string, error) {
- return m.cancelQuestion(ctx, l, raw)
- },
+ Handler: mcp.Typed(func(ctx context.Context, args cancelArgs) (string, error) {
+ return m.cancelQuestion(ctx, l, args)
+ }),
}
}
-func (m *Manager) cancelQuestion(ctx context.Context, l *live, raw json.RawMessage) (string, error) {
- args := cancelArgs{}
- err := json.Unmarshal(raw, &args)
- if err != nil {
- return "", fmt.Errorf("malformed cancel_question call: %w", err)
- }
-
+func (m *Manager) cancelQuestion(ctx context.Context, l *live, args cancelArgs) (string, error) {
item, err := m.store.GetInboxItem(ctx, args.QuestionID)
if errors.Is(err, store.ErrNotFound) {
return "", fmt.Errorf("question_id %s does not exist", args.QuestionID)
diff --git a/internal/agents/kb.go b/internal/agents/kb.go
index 94ba065..99d8027 100644
--- a/internal/agents/kb.go
+++ b/internal/agents/kb.go
@@ -48,19 +48,14 @@ func (m *Manager) readKnowledgeTool() mcp.Tool {
},
"required": ["topic"]
}`),
- Handler: func(ctx context.Context, raw json.RawMessage) (string, error) {
- args := struct {
- Topic string `json:"topic"`
- }{}
- err := json.Unmarshal(raw, &args)
- if err != nil {
- return "", fmt.Errorf("malformed read_knowledge call: %w", err)
- }
+ Handler: mcp.Typed(func(ctx context.Context, args struct {
+ Topic string `json:"topic"`
+ }) (string, error) {
body, ok := kb[args.Topic]
if !ok {
return "", fmt.Errorf("no article %q; the topics are:%s", args.Topic, kbIndex)
}
return body, nil
- },
+ }),
}
}
diff --git a/internal/agents/leadnotes.go b/internal/agents/leadnotes.go
index bda79d3..552d312 100644
--- a/internal/agents/leadnotes.go
+++ b/internal/agents/leadnotes.go
@@ -12,9 +12,9 @@ import (
"g.fc.run/ai/byre/internal/store"
)
-func (m *Manager) noteTools(tool func(name, description, schema string, run func(ctx context.Context, user api.User, raw json.RawMessage) (string, error)) mcp.Tool) []mcp.Tool {
+func (m *Manager) noteTools(l *live) []mcp.Tool {
return []mcp.Tool{
- tool("remember",
+ leadTool(m, l, "remember",
"Save a note about how the human works: a priority, a preference, or reasoning behind a choice, worded so you can apply it later. "+
"Give project_id when the note is about one project (its code, UI, docs or process); omit it when it holds for the human in every project. "+
"Without id it adds a note; with id it replaces that note's text and project. byre gives you every note at the start of each new lead session.",
@@ -23,22 +23,17 @@ func (m *Manager) noteTools(tool func(name, description, schema string, run func
"project_id": {"type": "string", "description": "The project the note applies to; omit for every project."},
"id": {"type": "string", "description": "The note to replace."}
}, "required": ["text"]}`,
- func(ctx context.Context, user api.User, raw json.RawMessage) (string, error) {
- args := struct {
- Text string `json:"text"`
- ProjectID string `json:"project_id"`
- ID string `json:"id"`
- }{}
- err := json.Unmarshal(raw, &args)
- if err != nil {
- return "", err
- }
+ func(ctx context.Context, user api.User, args struct {
+ Text string `json:"text"`
+ ProjectID string `json:"project_id"`
+ ID string `json:"id"`
+ }) (string, error) {
text := strings.TrimSpace(args.Text)
if text == "" {
return "", errors.New("text is empty")
}
if args.ProjectID != "" {
- _, err = m.store.GetProject(ctx, args.ProjectID)
+ _, err := m.store.GetProject(ctx, args.ProjectID)
if err != nil {
return "", err
}
@@ -53,28 +48,23 @@ func (m *Manager) noteTools(tool func(name, description, schema string, run func
return fmt.Sprintf("Saved as note %s.", n.Id), nil
}),
- tool("forget",
+ leadTool(m, l, "forget",
"Delete a note that no longer holds, for example because the human contradicted it.",
`{"type": "object", "properties": {"id": {"type": "string"}}, "required": ["id"]}`,
- func(ctx context.Context, user api.User, raw json.RawMessage) (string, error) {
- args := struct {
- ID string `json:"id"`
- }{}
- err := json.Unmarshal(raw, &args)
- if err != nil {
- return "", err
- }
- _, err = m.store.DeleteLeadNote(ctx, user.Login, args.ID, leadActor(user.Login))
+ func(ctx context.Context, user api.User, args struct {
+ ID string `json:"id"`
+ }) (string, error) {
+ _, err := m.store.DeleteLeadNote(ctx, user.Login, args.ID, leadActor(user.Login))
if err != nil {
return "", err
}
return fmt.Sprintf("Note %s deleted.", args.ID), nil
}),
- tool("list_notes",
+ leadTool(m, l, "list_notes",
"List your notes about how the human works, with their ids, as they are now. The human can delete notes in byre's Notes tab.",
`{"type": "object", "properties": {}}`,
- func(ctx context.Context, user api.User, raw json.RawMessage) (string, error) {
+ func(ctx context.Context, user api.User, _ struct{}) (string, error) {
return m.notesText(ctx, user.Login, "")
}),
}
@@ -96,15 +86,10 @@ func (m *Manager) suggestNoteTool(l *live) mcp.Tool {
},
"required": ["text", "evidence"]
}`),
- Handler: func(ctx context.Context, raw json.RawMessage) (string, error) {
- args := struct {
- Text string `json:"text"`
- Evidence string `json:"evidence"`
- }{}
- err := json.Unmarshal(raw, &args)
- if err != nil {
- return "", fmt.Errorf("malformed suggest_note call: %w", err)
- }
+ Handler: mcp.Typed(func(ctx context.Context, args struct {
+ Text string `json:"text"`
+ Evidence string `json:"evidence"`
+ }) (string, error) {
text := strings.TrimSpace(args.Text)
if text == "" {
return "", fmt.Errorf("%w: text must not be empty", ErrInvalid)
@@ -127,7 +112,7 @@ func (m *Manager) suggestNoteTool(l *live) mcp.Tool {
return "", err
}
return "Suggestion sent to the lead.", nil
- },
+ }),
}
}
diff --git a/internal/agents/leadtools.go b/internal/agents/leadtools.go
index a8dbc5d..179d02d 100644
--- a/internal/agents/leadtools.go
+++ b/internal/agents/leadtools.go
@@ -16,33 +16,38 @@ import (
"g.fc.run/ai/byre/internal/todo"
)
-func (m *Manager) leadTools(l *live) []mcp.Tool {
- tool := func(name, description, schema string, run func(ctx context.Context, user api.User, raw json.RawMessage) (string, error)) mcp.Tool {
- return mcp.Tool{
- Name: name,
- Description: description,
- InputSchema: json.RawMessage(schema),
- Handler: func(ctx context.Context, raw json.RawMessage) (string, error) {
- user, err := m.store.GetUser(ctx, l.owner)
- if err != nil {
- return "", err
- }
- return run(ctx, user, raw)
- },
- }
+type taskIDArgs struct {
+ TaskID string `json:"task_id"`
+}
+
+func leadTool[A any](m *Manager, l *live, name, description, schema string, run func(ctx context.Context, user api.User, args A) (string, error)) mcp.Tool {
+ return mcp.Tool{
+ Name: name,
+ Description: description,
+ InputSchema: json.RawMessage(schema),
+ Handler: mcp.Typed(func(ctx context.Context, args A) (string, error) {
+ user, err := m.store.GetUser(ctx, l.owner)
+ if err != nil {
+ return "", err
+ }
+ return run(ctx, user, args)
+ }),
}
+}
+
+func (m *Manager) leadTools(l *live) []mcp.Tool {
taskOnly := `{"type": "object", "properties": {"task_id": {"type": "string"}}, "required": ["task_id"]}`
return append([]mcp.Tool{
- tool("list_tasks",
+ leadTool(m, l, "list_tasks",
"List every project and its active tasks (id, title, worktree path, the agent's state and model, and open questions), "+
"and the titles of its TODO.md items on the default branch that no active task was started from.",
`{"type": "object", "properties": {}}`,
- func(ctx context.Context, user api.User, raw json.RawMessage) (string, error) {
+ func(ctx context.Context, user api.User, _ struct{}) (string, error) {
return m.listTasksText(ctx)
}),
- tool("read_transcript",
+ leadTool(m, l, "read_transcript",
"Read a task's transcript: messages, tool calls (briefly), tool results (cut short), turn ends, questions asked, errors and rate limits, each with its event seq. "+
"Without after_seq it returns the last limit events; with it, the events after that seq.",
`{"type": "object", "properties": {
@@ -50,42 +55,25 @@ func (m *Manager) leadTools(l *live) []mcp.Tool {
"after_seq": {"type": "integer"},
"limit": {"type": "integer", "description": "At most this many events, default 100, at most 500."}
}, "required": ["task_id"]}`,
- func(ctx context.Context, user api.User, raw json.RawMessage) (string, error) {
- args := struct {
- TaskID string `json:"task_id"`
- AfterSeq *int64 `json:"after_seq"`
- Limit int `json:"limit"`
- }{}
- err := json.Unmarshal(raw, &args)
- if err != nil {
- return "", err
- }
+ func(ctx context.Context, user api.User, args struct {
+ TaskID string `json:"task_id"`
+ AfterSeq *int64 `json:"after_seq"`
+ Limit int `json:"limit"`
+ }) (string, error) {
return m.transcriptText(ctx, args.TaskID, args.AfterSeq, args.Limit)
}),
- tool("search_closed_tasks",
+ leadTool(m, l, "search_closed_tasks",
searchClosedDescription+" Give project_id for one project's tasks; without it, every project's. read_transcript reads a closed task's transcript.",
`{"type": "object", "properties": {"project_id": {"type": "string"},`+searchClosedProperties+`}}`,
- func(ctx context.Context, user api.User, raw json.RawMessage) (string, error) {
- args := searchClosedArgs{}
- err := json.Unmarshal(raw, &args)
- if err != nil {
- return "", err
- }
+ func(ctx context.Context, user api.User, args searchClosedArgs) (string, error) {
return m.searchClosedText(ctx, args.ProjectID, args.Query, args.Limit)
}),
- tool("read_inbox",
+ leadTool(m, l, "read_inbox",
"Read open questions as JSON, with each one's full context, choices and responses. Give task_id for one task's questions.",
`{"type": "object", "properties": {"task_id": {"type": "string"}}}`,
- func(ctx context.Context, user api.User, raw json.RawMessage) (string, error) {
- args := struct {
- TaskID string `json:"task_id"`
- }{}
- err := json.Unmarshal(raw, &args)
- if err != nil {
- return "", err
- }
+ func(ctx context.Context, user api.User, args taskIDArgs) (string, error) {
items, err := m.store.ListInbox(ctx, store.InboxFilter{State: api.Open, TaskID: args.TaskID})
if err != nil {
return "", err
@@ -93,12 +81,12 @@ func (m *Manager) leadTools(l *live) []mcp.Tool {
return prettyJSON(items)
}),
- tool("read_status",
+ leadTool(m, l, "read_status",
"Read byre's health as JSON, the same as the Status tab: rate_limits (each vendor's latest usage: per window, how much is used of its limit and in what unit, and when it resets), "+
"agent_processes with each one's health, zombies, protocol_errors and running_turns. "+
"Check it before starting a batch of work or when agents misbehave: warn the human when a vendor nears its limit, and put new work on the vendor with headroom.",
`{"type": "object", "properties": {}}`,
- func(ctx context.Context, user api.User, raw json.RawMessage) (string, error) {
+ func(ctx context.Context, user api.User, _ struct{}) (string, error) {
status, err := m.status(ctx)
if err != nil {
return "", err
@@ -106,7 +94,7 @@ func (m *Manager) leadTools(l *live) []mcp.Tool {
return prettyJSON(status)
}),
- tool("answer_question",
+ leadTool(m, l, "answer_question",
"Answer an open question on the human's behalf. Only when the answer is clear or the human told you it. "+
"type option: options holds the chosen choices' exact text (one for pick-one), and text may add notes. "+
"type other: free text."+stoppedRefusal,
@@ -116,12 +104,7 @@ func (m *Manager) leadTools(l *live) []mcp.Tool {
"options": {"type": "array", "items": {"type": "string"}},
"text": {"type": "string"}
}, "required": ["item_id", "type"]}`,
- func(ctx context.Context, user api.User, raw json.RawMessage) (string, error) {
- c := api.InboxResponseCreate{}
- err := json.Unmarshal(raw, &c)
- if err != nil {
- return "", err
- }
+ func(ctx context.Context, user api.User, c api.InboxResponseCreate) (string, error) {
item, err := m.store.GetInboxItem(ctx, c.ItemId)
if err != nil {
return "", err
@@ -141,7 +124,7 @@ func (m *Manager) leadTools(l *live) []mcp.Tool {
return fmt.Sprintf("Responded to %s. The agent gets it once it is idle and has no other question waiting.", c.ItemId), nil
}),
- tool("send_message",
+ leadTool(m, l, "send_message",
"Send a message to a task's agent; it starts a turn. Starts the agent when the task has none. "+
"If the agent is mid-turn, the message is queued like the human's: the agent reads it with its next tool call's result, "+
"or as its next turn if it makes no further tool call, and gets it only once. "+
@@ -153,17 +136,12 @@ func (m *Manager) leadTools(l *live) []mcp.Tool {
"model": {"type": "string", "description": "Switch the agent to this model from this turn on."},
`+effortProperty+`
}, "required": ["task_id", "text"]}`,
- func(ctx context.Context, user api.User, raw json.RawMessage) (string, error) {
- args := struct {
- TaskID string `json:"task_id"`
- Text string `json:"text"`
- Model string `json:"model"`
- Effort string `json:"effort"`
- }{}
- err := json.Unmarshal(raw, &args)
- if err != nil {
- return "", err
- }
+ func(ctx context.Context, user api.User, args struct {
+ TaskID string `json:"task_id"`
+ Text string `json:"text"`
+ Model string `json:"model"`
+ Effort string `json:"effort"`
+ }) (string, error) {
if strings.TrimSpace(args.Text) == "" {
return "", errors.New("text is empty")
}
@@ -206,7 +184,7 @@ func (m *Manager) leadTools(l *live) []mcp.Tool {
return fmt.Sprintf("Sent to %s's agent.", task.Id), nil
}),
- tool("create_task",
+ leadTool(m, l, "create_task",
"Create a task with its own branch and worktree, and start an agent on it with prompt as its first message. "+
"The new agent sees nothing else, so the prompt must be a complete brief. It starts from the project's default branch, "+
"or from from_task_id's committed HEAD. With workflow, it runs that workflow instead: each stage becomes its own task, titled \"
: \", on the stage's model, "+
@@ -229,21 +207,16 @@ func (m *Manager) leadTools(l *live) []mcp.Tool {
"prompt": {"type": "string", "description": "This agent's own instructions: its role in the team."}
}, "required": ["name", "model", "prompt"]}}
}, "required": ["project_id", "title"]}`,
- func(ctx context.Context, user api.User, raw json.RawMessage) (string, error) {
- args := struct {
- ProjectID string `json:"project_id"`
- Title string `json:"title"`
- Prompt string `json:"prompt"`
- FromTaskID string `json:"from_task_id"`
- Model string `json:"model"`
- Effort string `json:"effort"`
- Workflow string `json:"workflow"`
- Agents []api.TeamMember `json:"agents"`
- }{}
- err := json.Unmarshal(raw, &args)
- if err != nil {
- return "", err
- }
+ func(ctx context.Context, user api.User, args struct {
+ ProjectID string `json:"project_id"`
+ Title string `json:"title"`
+ Prompt string `json:"prompt"`
+ FromTaskID string `json:"from_task_id"`
+ Model string `json:"model"`
+ Effort string `json:"effort"`
+ Workflow string `json:"workflow"`
+ Agents []api.TeamMember `json:"agents"`
+ }) (string, error) {
if strings.TrimSpace(args.Title) == "" {
return "", errors.New("title is required")
}
@@ -257,7 +230,7 @@ func (m *Manager) leadTools(l *live) []mcp.Tool {
return "", fmt.Errorf("%w: agents can't be combined with model or workflow", ErrInvalid)
}
if args.Agents != nil {
- err = checkTeam(args.Agents)
+ err := checkTeam(args.Agents)
if err != nil {
return "", err
}
@@ -306,19 +279,14 @@ func (m *Manager) leadTools(l *live) []mcp.Tool {
return fmt.Sprintf("Task %s %q created from %s and its agent started.", task.Id, task.Title, task.BaseRef), nil
}),
- tool("create_project",
+ leadTool(m, l, "create_project",
"Add a project to byre for the human: name it, and give the path of a git repository on this Mac. "+
"byre records the repository's root and default branch. Returns the project as JSON.",
`{"type": "object", "properties": {
"name": {"type": "string"},
"repo_path": {"type": "string"}
}, "required": ["name", "repo_path"]}`,
- func(ctx context.Context, user api.User, raw json.RawMessage) (string, error) {
- args := api.ProjectCreate{}
- err := json.Unmarshal(raw, &args)
- if err != nil {
- return "", err
- }
+ func(ctx context.Context, user api.User, args api.ProjectCreate) (string, error) {
p, err := m.addProject(ctx, args.Name, args.RepoPath, user.Login)
if err != nil {
return "", err
@@ -326,7 +294,7 @@ func (m *Manager) leadTools(l *live) []mcp.Tool {
return prettyJSON(p)
}),
- tool("start_todo",
+ leadTool(m, l, "start_todo",
"Start a task on an item of a project's TODO.md (its title, exactly as list_tasks' project or the file on the default branch has it). "+
"With workflow, it runs that workflow with the item as its brief, each stage as its own task, and model can't be given.",
`{"type": "object", "properties": {
@@ -336,18 +304,13 @@ func (m *Manager) leadTools(l *live) []mcp.Tool {
`+effortProperty+`,
"workflow": {"type": "string", "description": "The name of a workflow for the task to freeze a copy of and run."}
}, "required": ["project_id", "title"]}`,
- func(ctx context.Context, user api.User, raw json.RawMessage) (string, error) {
- args := struct {
- ProjectID string `json:"project_id"`
- Title string `json:"title"`
- Model string `json:"model"`
- Effort string `json:"effort"`
- Workflow string `json:"workflow"`
- }{}
- err := json.Unmarshal(raw, &args)
- if err != nil {
- return "", err
- }
+ func(ctx context.Context, user api.User, args struct {
+ ProjectID string `json:"project_id"`
+ Title string `json:"title"`
+ Model string `json:"model"`
+ Effort string `json:"effort"`
+ Workflow string `json:"workflow"`
+ }) (string, error) {
if args.Workflow != "" && args.Model != "" {
return "", errModelWithWorkflow
}
@@ -363,9 +326,9 @@ func (m *Manager) leadTools(l *live) []mcp.Tool {
return fmt.Sprintf("Started %s.", describeTasks(tasks)), nil
}),
- tool("interrupt", "Interrupt the turn a task's agent is running.", taskOnly,
- func(ctx context.Context, user api.User, raw json.RawMessage) (string, error) {
- sess, err := m.taskSessionArg(ctx, raw)
+ leadTool(m, l, "interrupt", "Interrupt the turn a task's agent is running.", taskOnly,
+ func(ctx context.Context, user api.User, args taskIDArgs) (string, error) {
+ sess, err := m.taskSession(ctx, args.TaskID)
if err != nil {
return "", err
}
@@ -376,22 +339,18 @@ func (m *Manager) leadTools(l *live) []mcp.Tool {
return "Interrupted.", nil
}),
- tool("compact_task", "Have a task's idle agent write a handoff and continue in a fresh session."+stoppedRefusal,
+ leadTool(m, l, "compact_task", "Have a task's idle agent write a handoff and continue in a fresh session."+stoppedRefusal,
`{"type": "object", "properties": {
"task_id": {"type": "string"},
"model": {"type": "string", "description": "Start the fresh session on this model, on either vendor; the current agent still writes the handoff. Defaults to the current model and effort."},
`+effortProperty+`
}, "required": ["task_id"]}`,
- func(ctx context.Context, user api.User, raw json.RawMessage) (string, error) {
- args := struct {
- Model string `json:"model"`
- Effort string `json:"effort"`
- }{}
- err := json.Unmarshal(raw, &args)
- if err != nil {
- return "", err
- }
- sess, err := m.taskSessionArg(ctx, raw)
+ func(ctx context.Context, user api.User, args struct {
+ TaskID string `json:"task_id"`
+ Model string `json:"model"`
+ Effort string `json:"effort"`
+ }) (string, error) {
+ sess, err := m.taskSession(ctx, args.TaskID)
if err != nil {
return "", err
}
@@ -406,7 +365,7 @@ func (m *Manager) leadTools(l *live) []mcp.Tool {
return "Compaction started.", nil
}),
- tool("merge_task",
+ leadTool(m, l, "merge_task",
"Merge a task as the human: its agent merges the default branch in, then byre squashes it into one commit, pushes it to origin and closes the task. "+
"While another task in the project is merging, byre holds the merge and starts it when that one is done. "+
"For a subtask, target parent instead asks its parent task's agent to merge it into the parent's branch, and the subtask closes once the parent has. "+
@@ -415,15 +374,11 @@ func (m *Manager) leadTools(l *live) []mcp.Tool {
"task_id": {"type": "string"},
"target": {"type": "string", "enum": ["main", "parent"], "description": "Default main."}
}, "required": ["task_id"]}`,
- func(ctx context.Context, user api.User, raw json.RawMessage) (string, error) {
- args := struct {
- Target string `json:"target"`
- }{}
- err := json.Unmarshal(raw, &args)
- if err != nil {
- return "", err
- }
- task, project, err := m.taskArg(ctx, raw)
+ func(ctx context.Context, user api.User, args struct {
+ TaskID string `json:"task_id"`
+ Target string `json:"target"`
+ }) (string, error) {
+ task, project, err := m.activeTask(ctx, args.TaskID)
if err != nil {
return "", err
}
@@ -458,9 +413,9 @@ func (m *Manager) leadTools(l *live) []mcp.Tool {
return fmt.Sprintf("Merge of %s started.", task.Id), nil
}),
- tool("take_snapshot", takeSnapshotDescription+" Refused while the task is merging or being discarded.", taskOnly,
- func(ctx context.Context, user api.User, raw json.RawMessage) (string, error) {
- task, _, err := m.taskArg(ctx, raw)
+ leadTool(m, l, "take_snapshot", takeSnapshotDescription+" Refused while the task is merging or being discarded.", taskOnly,
+ func(ctx context.Context, user api.User, args taskIDArgs) (string, error) {
+ task, _, err := m.activeTask(ctx, args.TaskID)
if err != nil {
return "", err
}
@@ -471,12 +426,12 @@ func (m *Manager) leadTools(l *live) []mcp.Tool {
return fmt.Sprintf("%s's worktree committed to its branch, and snapshot requested; its review shows in the task's transcript, and findings go to its agent.", task.Id), nil
}),
- tool("discard_task",
+ leadTool(m, l, "discard_task",
"Discard a task: its agent gets one turn to write the task summary, then byre stops it and deletes its worktree and branch without merging. This destroys its unmerged work. Refused for a task with active subtasks. Only when the human asks. "+
"If the agent is mid-turn, the summary turn starts when that turn ends; no other turn runs in between."+stoppedRefusal,
taskOnly,
- func(ctx context.Context, user api.User, raw json.RawMessage) (string, error) {
- task, _, err := m.taskArg(ctx, raw)
+ func(ctx context.Context, user api.User, args taskIDArgs) (string, error) {
+ task, _, err := m.activeTask(ctx, args.TaskID)
if err != nil {
return "", err
}
@@ -491,7 +446,7 @@ func (m *Manager) leadTools(l *live) []mcp.Tool {
return fmt.Sprintf("Discarding %s: its agent gets a turn to write the task summary, and byre discards the task when that turn ends.", task.Id), nil
}),
- tool("show",
+ leadTool(m, l, "show",
"Point the human's right pane at a task, which shows its transcript (a workflow stage task's run: its graph and each stage's state, attempts and outputs), or at a project with no task, which shows the project's dashboard: its active tasks and its TODO.md items. "+
"The task's other views (changes, files, terminal, board) open over the human's left pane only when the human opens them; you can't open them. "+
"Whenever the human asks you to show something (\"show me that when you can\") or you want to show something, call show right away; never hold back because you expect a refusal, since only a refused show arranges byre's signal to try again. "+
@@ -505,12 +460,7 @@ func (m *Manager) leadTools(l *live) []mcp.Tool {
"task_id": {"type": "string"},
"project_id": {"type": "string", "description": "Ignored when task_id is given."}
}}`,
- func(ctx context.Context, user api.User, raw json.RawMessage) (string, error) {
- f := api.Focus{}
- err := json.Unmarshal(raw, &f)
- if err != nil {
- return "", err
- }
+ func(ctx context.Context, user api.User, f api.Focus) (string, error) {
current, err := m.store.GetFocus(ctx, user.Login)
if err != nil {
return "", err
@@ -563,16 +513,16 @@ func (m *Manager) leadTools(l *live) []mcp.Tool {
return "Shown.", nil
}),
- tool("compact_self",
+ leadTool(m, l, "compact_self",
"Ask byre to compact you when this turn ends: you write a handoff, and a fresh lead session continues from it. Call it only after byre signals that your context is past a quarter of its window, at a logical point, then end the turn with a short final message.",
`{"type": "object", "properties": {}}`,
- func(ctx context.Context, user api.User, raw json.RawMessage) (string, error) {
+ func(ctx context.Context, user api.User, _ struct{}) (string, error) {
m.mu.Lock()
l.wantCompact = true
m.mu.Unlock()
return "byre will ask for your handoff once this turn ends.", nil
}),
- }, append(append(m.workflowTools(tool), m.scheduleTools(tool)...), m.noteTools(tool)...)...)
+ }, append(append(m.workflowTools(l), m.scheduleTools(l)...), m.noteTools(l)...)...)
}
func (m *Manager) SetFocus(ctx context.Context, login string, f api.Focus, actor string) error {
@@ -849,19 +799,8 @@ func (m *Manager) checkNotStopped(ctx context.Context, sess api.AgentSession) er
return fmt.Errorf("%w: the human interrupted task %s's agent (or its last turn ended in an error) and hasn't messaged it since, so byre won't start a turn on it for you; put what you wanted to send or do to the human in chat instead", ErrConflict, sess.TaskId)
}
-func (m *Manager) taskArg(ctx context.Context, raw json.RawMessage) (api.Task, api.Project, error) {
- args := struct {
- TaskID string `json:"task_id"`
- }{}
- err := json.Unmarshal(raw, &args)
- if err != nil {
- return api.Task{}, api.Project{}, err
- }
- return m.activeTask(ctx, args.TaskID)
-}
-
-func (m *Manager) taskSessionArg(ctx context.Context, raw json.RawMessage) (api.AgentSession, error) {
- task, _, err := m.taskArg(ctx, raw)
+func (m *Manager) taskSession(ctx context.Context, taskID string) (api.AgentSession, error) {
+ task, _, err := m.activeTask(ctx, taskID)
if err != nil {
return api.AgentSession{}, err
}
diff --git a/internal/agents/mergein.go b/internal/agents/mergein.go
index fb35596..74ca202 100644
--- a/internal/agents/mergein.go
+++ b/internal/agents/mergein.go
@@ -433,19 +433,13 @@ func (m *Manager) setTitlesTool(l *live) mcp.Tool {
},
"required": ["title", "merge_title"]
}`),
- Handler: func(ctx context.Context, raw json.RawMessage) (string, error) {
- return m.setTitles(ctx, l, raw)
- },
+ Handler: mcp.Typed(func(ctx context.Context, args setTitlesArgs) (string, error) {
+ return m.setTitles(ctx, l, args)
+ }),
}
}
-func (m *Manager) setTitles(ctx context.Context, l *live, raw json.RawMessage) (string, error) {
- args := setTitlesArgs{}
- err := json.Unmarshal(raw, &args)
- if err != nil {
- return "", fmt.Errorf("malformed set_titles call: %w", err)
- }
-
+func (m *Manager) setTitles(ctx context.Context, l *live, args setTitlesArgs) (string, error) {
title := strings.TrimSpace(args.Title)
if title == "" || strings.ContainsAny(title, "\r\n") {
return "", fmt.Errorf("%w: title must be one non-empty line", ErrInvalid)
@@ -481,18 +475,13 @@ func (m *Manager) stopMergeTool(l *live) mcp.Tool {
},
"required": ["reason"]
}`),
- Handler: func(ctx context.Context, raw json.RawMessage) (string, error) {
- return m.stopMerge(ctx, l, raw)
- },
+ Handler: mcp.Typed(func(ctx context.Context, args stopMergeArgs) (string, error) {
+ return m.stopMerge(ctx, l, args)
+ }),
}
}
-func (m *Manager) stopMerge(ctx context.Context, l *live, raw json.RawMessage) (string, error) {
- args := stopMergeArgs{}
- err := json.Unmarshal(raw, &args)
- if err != nil {
- return "", fmt.Errorf("malformed stop_merge call: %w", err)
- }
+func (m *Manager) stopMerge(ctx context.Context, l *live, args stopMergeArgs) (string, error) {
reason := strings.TrimSpace(args.Reason)
if reason == "" {
return "", fmt.Errorf("%w: reason must say what the resolution changed", ErrInvalid)
diff --git a/internal/agents/mergemain.go b/internal/agents/mergemain.go
index 29f2be0..0cecd6a 100644
--- a/internal/agents/mergemain.go
+++ b/internal/agents/mergemain.go
@@ -30,9 +30,9 @@ func (m *Manager) mergeToMainTool(l *live) mcp.Tool {
},
"required": ["title"]
}`),
- Handler: func(ctx context.Context, raw json.RawMessage) (string, error) {
- return m.mergeToMain(ctx, l, raw)
- },
+ Handler: mcp.Typed(func(ctx context.Context, args mergeToMainArgs) (string, error) {
+ return m.mergeToMain(ctx, l, args)
+ }),
}
}
@@ -43,12 +43,7 @@ func checkLandable(task api.Task) error {
return nil
}
-func (m *Manager) mergeToMain(ctx context.Context, l *live, raw json.RawMessage) (string, error) {
- args := mergeToMainArgs{}
- err := json.Unmarshal(raw, &args)
- if err != nil {
- return "", fmt.Errorf("malformed merge_to_main call: %w", err)
- }
+func (m *Manager) mergeToMain(ctx context.Context, l *live, args mergeToMainArgs) (string, error) {
title, err := cleanMergeTitle(args.Title)
if err != nil {
return "", err
diff --git a/internal/agents/preview.go b/internal/agents/preview.go
index 676f27a..9ed3c7f 100644
--- a/internal/agents/preview.go
+++ b/internal/agents/preview.go
@@ -44,19 +44,13 @@ func (m *Manager) exposePortTool(l *live) mcp.Tool {
},
"required": ["name", "port"]
}`),
- Handler: func(ctx context.Context, raw json.RawMessage) (string, error) {
- return m.exposePort(ctx, l, raw)
- },
+ Handler: mcp.Typed(func(ctx context.Context, args exposePortArgs) (string, error) {
+ return m.exposePort(ctx, l, args)
+ }),
}
}
-func (m *Manager) exposePort(ctx context.Context, l *live, raw json.RawMessage) (string, error) {
- args := exposePortArgs{}
- err := json.Unmarshal(raw, &args)
- if err != nil {
- return "", fmt.Errorf("malformed expose_port call: %w", err)
- }
-
+func (m *Manager) exposePort(ctx context.Context, l *live, args exposePortArgs) (string, error) {
name := strings.TrimSpace(args.Name)
if name == "" || strings.ContainsAny(name, "\r\n") || utf8.RuneCountInString(name) > maxPreviewName {
return "", fmt.Errorf("name must be one non-empty line of at most %d characters", maxPreviewName)
diff --git a/internal/agents/random.go b/internal/agents/random.go
index b8c8b44..d034b1f 100644
--- a/internal/agents/random.go
+++ b/internal/agents/random.go
@@ -3,7 +3,6 @@ package agents
import (
"context"
"encoding/json"
- "fmt"
"g.fc.run/ai/byre/internal/mcp"
"g.fc.run/ai/byre/internal/workflow"
@@ -33,17 +32,12 @@ func (m *Manager) randomChoiceTool() mcp.Tool {
},
"required": ["choices"]
}`),
- Rich: func(ctx context.Context, raw json.RawMessage) mcp.Result {
- args := struct {
- Choices []struct {
- Choice string `json:"choice"`
- Weight float64 `json:"weight"`
- } `json:"choices"`
- }{}
- err := json.Unmarshal(raw, &args)
- if err != nil {
- return mcp.TextResult(fmt.Sprintf("malformed random_choice call: %v", err), true)
- }
+ Rich: mcp.TypedRich(func(ctx context.Context, args struct {
+ Choices []struct {
+ Choice string `json:"choice"`
+ Weight float64 `json:"weight"`
+ } `json:"choices"`
+ }) mcp.Result {
if len(args.Choices) == 0 {
return mcp.TextResult("choices is empty", true)
}
@@ -60,6 +54,6 @@ func (m *Manager) randomChoiceTool() mcp.Tool {
return mcp.TextResult("No match: none of the choices was picked.", false)
}
return mcp.TextResult(args.Choices[i].Choice, false)
- },
+ }),
}
}
diff --git a/internal/agents/ready.go b/internal/agents/ready.go
index 5d6f095..6601fc1 100644
--- a/internal/agents/ready.go
+++ b/internal/agents/ready.go
@@ -57,12 +57,7 @@ func (m *Manager) readyForMergeTool(l *live) mcp.Tool {
},
"required": ["overview"]
}`),
- Handler: func(ctx context.Context, raw json.RawMessage) (string, error) {
- args := readyArgs{}
- err := json.Unmarshal(raw, &args)
- if err != nil {
- return "", fmt.Errorf("malformed ready_for_merge call: %w", err)
- }
+ Handler: mcp.Typed(func(ctx context.Context, args readyArgs) (string, error) {
overview := strings.TrimSpace(args.Overview)
if overview == "" {
return "", fmt.Errorf("%w: overview must not be empty", ErrInvalid)
@@ -73,7 +68,7 @@ func (m *Manager) readyForMergeTool(l *live) mcp.Tool {
if args.Merge != "complete" && args.Merge != "continue" {
return "", fmt.Errorf("%w: merge must be complete or continue", ErrInvalid)
}
- err = m.store.MarkReady(ctx, l.taskID, overview, args.Merge, agentActor)
+ err := m.store.MarkReady(ctx, l.taskID, overview, args.Merge, agentActor)
if errors.Is(err, store.ErrNotFound) {
return "", fmt.Errorf("%w: this task is closed", ErrConflict)
}
@@ -81,7 +76,7 @@ func (m *Manager) readyForMergeTool(l *live) mcp.Tool {
return "", err
}
return "Recorded: the human sees this task as ready to merge, with your overview.", nil
- },
+ }),
}
}
@@ -102,17 +97,12 @@ func (m *Manager) suggestDiscardTool(l *live) mcp.Tool {
},
"required": ["reason"]
}`),
- Handler: func(ctx context.Context, raw json.RawMessage) (string, error) {
- args := discardArgs{}
- err := json.Unmarshal(raw, &args)
- if err != nil {
- return "", fmt.Errorf("malformed suggest_discard call: %w", err)
- }
+ Handler: mcp.Typed(func(ctx context.Context, args discardArgs) (string, error) {
reason := strings.TrimSpace(args.Reason)
if reason == "" {
return "", fmt.Errorf("%w: reason must not be empty", ErrInvalid)
}
- err = m.store.SuggestDiscard(ctx, l.taskID, reason, agentActor)
+ err := m.store.SuggestDiscard(ctx, l.taskID, reason, agentActor)
if errors.Is(err, store.ErrNotFound) {
return "", fmt.Errorf("%w: this task is closed", ErrConflict)
}
@@ -120,7 +110,7 @@ func (m *Manager) suggestDiscardTool(l *live) mcp.Tool {
return "", err
}
return "Recorded: the human sees your suggestion to discard this task, with your reasoning.", nil
- },
+ }),
}
}
diff --git a/internal/agents/review.go b/internal/agents/review.go
index b2f1d68..9f2ae92 100644
--- a/internal/agents/review.go
+++ b/internal/agents/review.go
@@ -275,18 +275,13 @@ func (m *Manager) submitReviewTool(l *live) mcp.Tool {
Name: "submit_review",
Description: "Submit your review. Call it exactly once, when you're done; its arguments are the review the human reads.",
InputSchema: reviewSchema(),
- Handler: func(ctx context.Context, args json.RawMessage) (string, error) {
- return m.submitReview(ctx, l, args)
- },
+ Handler: mcp.Typed(func(ctx context.Context, result map[string]any) (string, error) {
+ return m.submitReview(ctx, l, result)
+ }),
}
}
-func (m *Manager) submitReview(ctx context.Context, l *live, raw json.RawMessage) (string, error) {
- result := map[string]any{}
- err := json.Unmarshal(raw, &result)
- if err != nil {
- return "", fmt.Errorf("submit_review takes a JSON object: %w", err)
- }
+func (m *Manager) submitReview(ctx context.Context, l *live, result map[string]any) (string, error) {
if c, ok := result["change"].(string); !ok || strings.TrimSpace(c) == "" {
return "", errors.New("submit_review rejected, nothing was recorded: change must say what the change does")
}
@@ -323,7 +318,7 @@ func (m *Manager) submitReview(ctx context.Context, l *live, raw json.RawMessage
delete(img.entry, "path")
}
- err = m.store.FinishReview(ctx, l.reviewID, api.ReviewStateDone, result, "")
+ err := m.store.FinishReview(ctx, l.reviewID, api.ReviewStateDone, result, "")
if errors.Is(err, store.ErrNotFound) {
return "", errors.New("this review was already submitted or has ended")
}
@@ -449,12 +444,7 @@ func (m *Manager) respondToFindingTool(l *live) mcp.Tool {
},
"required": ["snapshot", "reviewer", "finding", "response", "reason"]
}`),
- Handler: func(ctx context.Context, raw json.RawMessage) (string, error) {
- args := findingResponseArgs{}
- err := json.Unmarshal(raw, &args)
- if err != nil {
- return "", fmt.Errorf("malformed respond_to_finding call: %w", err)
- }
+ Handler: mcp.Typed(func(ctx context.Context, args findingResponseArgs) (string, error) {
if !args.Response.Valid() {
return "", fmt.Errorf("%w: response must be fixing or ignoring", ErrInvalid)
}
@@ -486,7 +476,7 @@ func (m *Manager) respondToFindingTool(l *live) mcp.Tool {
return "", err
}
return fmt.Sprintf("Recorded: %s finding %d of reviewer %q's review of snapshot #%d.", args.Response, args.Finding, args.Reviewer, args.Snapshot), nil
- },
+ }),
}
}
diff --git a/internal/agents/schedule.go b/internal/agents/schedule.go
index 0bec280..b0529df 100644
--- a/internal/agents/schedule.go
+++ b/internal/agents/schedule.go
@@ -2,7 +2,6 @@ package agents
import (
"context"
- "encoding/json"
"fmt"
"log/slog"
"strings"
@@ -136,19 +135,14 @@ func scheduleText(sc api.Schedule) string {
return b.String()
}
-func (m *Manager) scheduleTools(tool func(name, description, schema string, run func(ctx context.Context, user api.User, raw json.RawMessage) (string, error)) mcp.Tool) []mcp.Tool {
+func (m *Manager) scheduleTools(l *live) []mcp.Tool {
return []mcp.Tool{
- tool("list_schedules",
+ leadTool(m, l, "list_schedules",
"List the schedules: each one's id, project, owner, title, timing, workflow, prompt, and next run in the server's local time. Give project_id for one project's.",
`{"type": "object", "properties": {"project_id": {"type": "string"}}}`,
- func(ctx context.Context, user api.User, raw json.RawMessage) (string, error) {
- args := struct {
- ProjectID string `json:"project_id"`
- }{}
- err := json.Unmarshal(raw, &args)
- if err != nil {
- return "", err
- }
+ func(ctx context.Context, user api.User, args struct {
+ ProjectID string `json:"project_id"`
+ }) (string, error) {
list, err := m.store.ListSchedules(ctx, args.ProjectID)
if err != nil {
return "", err
@@ -163,7 +157,7 @@ func (m *Manager) scheduleTools(tool func(name, description, schema string, run
return strings.Join(lines, "\n"), nil
}),
- tool("save_schedule",
+ leadTool(m, l, "save_schedule",
"Create a schedule, or with schedule_id replace one's settings (project_id is then ignored). On each run byre starts the workflow in the project from its default branch, as create_task with workflow does, "+
"as the schedule's owner (you, for one you create). Times are the server's local time. "+
"every hour runs at minute past each hour (hour 0); day at hour:minute; week at hour:minute on each of weekdays; month at hour:minute on day_of_month, or the month's last day when shorter. "+
@@ -182,39 +176,25 @@ func (m *Manager) scheduleTools(tool func(name, description, schema string, run
"prompt": {"type": "string"},
"workflow": {"type": "string", "description": "The name of a workflow for each run to freeze a copy of and run."}
}, "required": ["title", "every", "hour", "minute", "workflow"]}`,
- func(ctx context.Context, user api.User, raw json.RawMessage) (string, error) {
- args := struct {
- ProjectID string `json:"project_id"`
- ScheduleID string `json:"schedule_id"`
- }{}
- err := json.Unmarshal(raw, &args)
- if err != nil {
- return "", err
- }
- save := api.ScheduleSave{}
- err = json.Unmarshal(raw, &save)
- if err != nil {
- return "", err
- }
- sc, err := m.SaveSchedule(ctx, args.ProjectID, args.ScheduleID, save, user.Login, leadActor(user.Login))
+ func(ctx context.Context, user api.User, args struct {
+ ProjectID string `json:"project_id"`
+ ScheduleID string `json:"schedule_id"`
+ api.ScheduleSave
+ }) (string, error) {
+ sc, err := m.SaveSchedule(ctx, args.ProjectID, args.ScheduleID, args.ScheduleSave, user.Login, leadActor(user.Login))
if err != nil {
return "", err
}
return "Saved:\n" + scheduleText(sc), nil
}),
- tool("delete_schedule",
+ leadTool(m, l, "delete_schedule",
"Delete a schedule. Tasks it already created stay. Only when the human asks.",
`{"type": "object", "properties": {"schedule_id": {"type": "string"}}, "required": ["schedule_id"]}`,
- func(ctx context.Context, user api.User, raw json.RawMessage) (string, error) {
- args := struct {
- ScheduleID string `json:"schedule_id"`
- }{}
- err := json.Unmarshal(raw, &args)
- if err != nil {
- return "", err
- }
- err = m.DeleteSchedule(ctx, args.ScheduleID, leadActor(user.Login))
+ func(ctx context.Context, user api.User, args struct {
+ ScheduleID string `json:"schedule_id"`
+ }) (string, error) {
+ err := m.DeleteSchedule(ctx, args.ScheduleID, leadActor(user.Login))
if err != nil {
return "", err
}
diff --git a/internal/agents/subtasks.go b/internal/agents/subtasks.go
index ab8024e..4083f1a 100644
--- a/internal/agents/subtasks.go
+++ b/internal/agents/subtasks.go
@@ -14,29 +14,30 @@ import (
"g.fc.run/ai/byre/internal/store"
)
-func (m *Manager) subtaskTools(l *live) []mcp.Tool {
- tool := func(name, description, schema string, run func(ctx context.Context, creator api.User, raw json.RawMessage) (string, error)) mcp.Tool {
- return mcp.Tool{
- Name: name,
- Description: description,
- InputSchema: json.RawMessage(schema),
- Handler: func(ctx context.Context, raw json.RawMessage) (string, error) {
- sess, err := m.store.GetSession(ctx, l.id)
- if err != nil {
- return "", err
- }
- creator, err := m.store.GetUser(ctx, sess.CreatedBy)
- if err != nil {
- return "", err
- }
- return run(ctx, creator, raw)
- },
- }
+func subtaskTool[A any](m *Manager, l *live, name, description, schema string, run func(ctx context.Context, creator api.User, args A) (string, error)) mcp.Tool {
+ return mcp.Tool{
+ Name: name,
+ Description: description,
+ InputSchema: json.RawMessage(schema),
+ Handler: mcp.Typed(func(ctx context.Context, args A) (string, error) {
+ sess, err := m.store.GetSession(ctx, l.id)
+ if err != nil {
+ return "", err
+ }
+ creator, err := m.store.GetUser(ctx, sess.CreatedBy)
+ if err != nil {
+ return "", err
+ }
+ return run(ctx, creator, args)
+ }),
}
+}
+
+func (m *Manager) subtaskTools(l *live) []mcp.Tool {
taskOnly := `{"type": "object", "properties": {"task_id": {"type": "string"}}, "required": ["task_id"]}`
return []mcp.Tool{
- tool("create_subtask",
+ subtaskTool(m, l, "create_subtask",
"Create a subtask of this task: a byre task with its own branch and worktree, started from this task's HEAD (your committed work; byre commits at the end of each turn), "+
"with an agent on it that gets prompt as its first message. Use it to run a piece of work in parallel or to get another model's view. "+
"The new agent sees nothing of your context, so the prompt must be a complete brief. "+
@@ -49,17 +50,12 @@ func (m *Manager) subtaskTools(l *live) []mcp.Tool {
"model": {"type": "string", "description": "Model for the new agent: gpt-* models run on Codex, gemini-* models on Antigravity, every other model on Claude Code. Defaults to this session's model and effort."},
`+effortProperty+`
}, "required": ["title", "prompt"]}`,
- func(ctx context.Context, creator api.User, raw json.RawMessage) (string, error) {
- args := struct {
- Title string `json:"title"`
- Prompt string `json:"prompt"`
- Model string `json:"model"`
- Effort string `json:"effort"`
- }{}
- err := json.Unmarshal(raw, &args)
- if err != nil {
- return "", fmt.Errorf("malformed create_subtask call: %w", err)
- }
+ func(ctx context.Context, creator api.User, args struct {
+ Title string `json:"title"`
+ Prompt string `json:"prompt"`
+ Model string `json:"model"`
+ Effort string `json:"effort"`
+ }) (string, error) {
if strings.TrimSpace(args.Title) == "" || strings.TrimSpace(args.Prompt) == "" {
return "", errors.New("title and prompt are required")
}
@@ -83,10 +79,10 @@ func (m *Manager) subtaskTools(l *live) []mcp.Tool {
return fmt.Sprintf("Subtask %s %q created from %s, branch %s, worktree %s, and its agent started.", task.Id, task.Title, task.BaseRef, task.Branch, task.WorktreePath), nil
}),
- tool("list_subtasks",
+ subtaskTool(m, l, "list_subtasks",
"List this task's active subtasks: id, title, the agent's state and model, branch and worktree.",
`{"type": "object", "properties": {}}`,
- func(ctx context.Context, creator api.User, raw json.RawMessage) (string, error) {
+ func(ctx context.Context, creator api.User, _ struct{}) (string, error) {
subs, err := m.store.ActiveSubtasks(ctx, l.taskID)
if err != nil {
return "", err
@@ -113,7 +109,7 @@ func (m *Manager) subtaskTools(l *live) []mcp.Tool {
return b.String(), nil
}),
- tool("read_subtask",
+ subtaskTool(m, l, "read_subtask",
"Read a subtask's transcript: messages, tool calls (briefly), tool results (cut short), turn ends, questions asked, errors and rate limits, each with its event seq. "+
"Without after_seq it returns the last limit events; with it, the events after that seq.",
`{"type": "object", "properties": {
@@ -121,16 +117,11 @@ func (m *Manager) subtaskTools(l *live) []mcp.Tool {
"after_seq": {"type": "integer"},
"limit": {"type": "integer", "description": "At most this many events, default 100, at most 500."}
}, "required": ["task_id"]}`,
- func(ctx context.Context, creator api.User, raw json.RawMessage) (string, error) {
- args := struct {
- TaskID string `json:"task_id"`
- AfterSeq *int64 `json:"after_seq"`
- Limit int `json:"limit"`
- }{}
- err := json.Unmarshal(raw, &args)
- if err != nil {
- return "", err
- }
+ func(ctx context.Context, creator api.User, args struct {
+ TaskID string `json:"task_id"`
+ AfterSeq *int64 `json:"after_seq"`
+ Limit int `json:"limit"`
+ }) (string, error) {
task, err := m.subtask(ctx, l, args.TaskID, false)
if err != nil {
return "", err
@@ -138,22 +129,17 @@ func (m *Manager) subtaskTools(l *live) []mcp.Tool {
return m.transcriptText(ctx, task.Id, args.AfterSeq, args.Limit)
}),
- tool("message_subtask",
+ subtaskTool(m, l, "message_subtask",
"Send a message to a subtask's agent. It starts a turn when the agent is idle, starts the agent when the subtask has none, "+
"and reaches it mid-turn when it is working.",
`{"type": "object", "properties": {
"task_id": {"type": "string"},
"text": {"type": "string"}
}, "required": ["task_id", "text"]}`,
- func(ctx context.Context, creator api.User, raw json.RawMessage) (string, error) {
- args := struct {
- TaskID string `json:"task_id"`
- Text string `json:"text"`
- }{}
- err := json.Unmarshal(raw, &args)
- if err != nil {
- return "", err
- }
+ func(ctx context.Context, creator api.User, args struct {
+ TaskID string `json:"task_id"`
+ Text string `json:"text"`
+ }) (string, error) {
if strings.TrimSpace(args.Text) == "" {
return "", errors.New("text is empty")
}
@@ -168,9 +154,9 @@ func (m *Manager) subtaskTools(l *live) []mcp.Tool {
return fmt.Sprintf("Sent to %s's agent.", task.Id), nil
}),
- tool("interrupt_subtask", "Interrupt the turn a subtask's agent is running.", taskOnly,
- func(ctx context.Context, creator api.User, raw json.RawMessage) (string, error) {
- task, err := m.subtaskArg(ctx, l, raw)
+ subtaskTool(m, l, "interrupt_subtask", "Interrupt the turn a subtask's agent is running.", taskOnly,
+ func(ctx context.Context, creator api.User, args taskIDArgs) (string, error) {
+ task, err := m.subtask(ctx, l, args.TaskID, true)
if err != nil {
return "", err
}
@@ -188,7 +174,7 @@ func (m *Manager) subtaskTools(l *live) []mcp.Tool {
return "Interrupted.", nil
}),
- tool("merge_subtask",
+ subtaskTool(m, l, "merge_subtask",
"Merge a subtask. target parent: byre sends you a message asking you to merge the subtask's branch into this task's branch, "+
"and closes the subtask once its branch is in this branch's history at the end of one of your turns. "+
"target main: the subtask's agent merges the default branch in, then byre squashes it onto the default branch, pushes it and closes it, as the human's Merge does; it starts once the subtask's agent finishes any turn it is in. "+
@@ -197,15 +183,10 @@ func (m *Manager) subtaskTools(l *live) []mcp.Tool {
"task_id": {"type": "string"},
"target": {"type": "string", "enum": ["parent", "main"]}
}, "required": ["task_id", "target"]}`,
- func(ctx context.Context, creator api.User, raw json.RawMessage) (string, error) {
- args := struct {
- TaskID string `json:"task_id"`
- Target string `json:"target"`
- }{}
- err := json.Unmarshal(raw, &args)
- if err != nil {
- return "", err
- }
+ func(ctx context.Context, creator api.User, args struct {
+ TaskID string `json:"task_id"`
+ Target string `json:"target"`
+ }) (string, error) {
task, err := m.subtask(ctx, l, args.TaskID, true)
if err != nil {
return "", err
@@ -234,11 +215,11 @@ func (m *Manager) subtaskTools(l *live) []mcp.Tool {
return "", fmt.Errorf("%w: target must be parent or main, not %q", ErrInvalid, args.Target)
}),
- tool("discard_subtask",
+ subtaskTool(m, l, "discard_subtask",
"Discard a subtask: its agent gets one turn to write the task summary, then byre stops it and deletes its worktree and branch without merging. This destroys its unmerged work.",
taskOnly,
- func(ctx context.Context, creator api.User, raw json.RawMessage) (string, error) {
- task, err := m.subtaskArg(ctx, l, raw)
+ func(ctx context.Context, creator api.User, args taskIDArgs) (string, error) {
+ task, err := m.subtask(ctx, l, args.TaskID, true)
if err != nil {
return "", err
}
@@ -251,17 +232,6 @@ func (m *Manager) subtaskTools(l *live) []mcp.Tool {
}
}
-func (m *Manager) subtaskArg(ctx context.Context, l *live, raw json.RawMessage) (api.Task, error) {
- args := struct {
- TaskID string `json:"task_id"`
- }{}
- err := json.Unmarshal(raw, &args)
- if err != nil {
- return api.Task{}, err
- }
- return m.subtask(ctx, l, args.TaskID, true)
-}
-
func (m *Manager) subtask(ctx context.Context, l *live, taskID string, active bool) (api.Task, error) {
task, err := m.store.GetTask(ctx, taskID)
if err != nil {
diff --git a/internal/agents/summary.go b/internal/agents/summary.go
index f54516e..b13295d 100644
--- a/internal/agents/summary.go
+++ b/internal/agents/summary.go
@@ -37,17 +37,12 @@ func (m *Manager) setTaskSummaryTool(l *live) mcp.Tool {
},
"required": ["summary"]
}`),
- Handler: func(ctx context.Context, raw json.RawMessage) (string, error) {
- args := setTaskSummaryArgs{}
- err := json.Unmarshal(raw, &args)
- if err != nil {
- return "", fmt.Errorf("malformed set_task_summary call: %w", err)
- }
+ Handler: mcp.Typed(func(ctx context.Context, args setTaskSummaryArgs) (string, error) {
summary := strings.TrimSpace(args.Summary)
if summary == "" {
return "", fmt.Errorf("%w: summary must not be empty", ErrInvalid)
}
- err = m.store.SetTaskSummary(ctx, l.taskID, summary, agentActor)
+ err := m.store.SetTaskSummary(ctx, l.taskID, summary, agentActor)
if errors.Is(err, store.ErrNotFound) {
return "", fmt.Errorf("%w: this task is closed", ErrConflict)
}
@@ -55,7 +50,7 @@ func (m *Manager) setTaskSummaryTool(l *live) mcp.Tool {
return "", err
}
return "Task summary saved.", nil
- },
+ }),
}
}
@@ -78,14 +73,9 @@ func (m *Manager) searchClosedTool(l *live) mcp.Tool {
Name: "search_closed_tasks",
Description: searchClosedDescription + " Searches this task's project.",
InputSchema: json.RawMessage(`{"type": "object", "properties": {` + searchClosedProperties + `}}`),
- Handler: func(ctx context.Context, raw json.RawMessage) (string, error) {
- args := searchClosedArgs{}
- err := json.Unmarshal(raw, &args)
- if err != nil {
- return "", fmt.Errorf("malformed search_closed_tasks call: %w", err)
- }
+ Handler: mcp.Typed(func(ctx context.Context, args searchClosedArgs) (string, error) {
return m.searchClosedText(ctx, l.projectID, args.Query, args.Limit)
- },
+ }),
}
}
diff --git a/internal/agents/team.go b/internal/agents/team.go
index e8acb9e..7d96ab4 100644
--- a/internal/agents/team.go
+++ b/internal/agents/team.go
@@ -229,14 +229,9 @@ func (m *Manager) boardTools(l *live) []mcp.Tool {
Name: "post_to_board",
Description: "Post to this task's team board. Every other agent of the team gets the post: a working agent after its next tool call, an idle one as a new turn. The human sees the board in the task's Board tab.",
InputSchema: json.RawMessage(`{"type": "object", "properties": {"text": {"type": "string", "description": "Markdown."}}, "required": ["text"]}`),
- Handler: func(ctx context.Context, raw json.RawMessage) (string, error) {
- args := struct {
- Text string `json:"text"`
- }{}
- err := json.Unmarshal(raw, &args)
- if err != nil {
- return "", fmt.Errorf("malformed post_to_board call: %w", err)
- }
+ Handler: mcp.Typed(func(ctx context.Context, args struct {
+ Text string `json:"text"`
+ }) (string, error) {
task, err := m.store.GetTask(ctx, l.taskID)
if err != nil {
return "", err
@@ -246,20 +241,15 @@ func (m *Manager) boardTools(l *live) []mcp.Tool {
return "", err
}
return "Posted to the board.", nil
- },
+ }),
},
{
Name: "read_board",
Description: "Read this task's team board: every post, oldest first, each with its seq and who posted it. With after_seq, only the posts after it.",
InputSchema: json.RawMessage(`{"type": "object", "properties": {"after_seq": {"type": "integer"}}}`),
- Handler: func(ctx context.Context, raw json.RawMessage) (string, error) {
- args := struct {
- AfterSeq int64 `json:"after_seq"`
- }{}
- err := json.Unmarshal(raw, &args)
- if err != nil {
- return "", fmt.Errorf("malformed read_board call: %w", err)
- }
+ Handler: mcp.Typed(func(ctx context.Context, args struct {
+ AfterSeq int64 `json:"after_seq"`
+ }) (string, error) {
posts, err := m.store.BoardPosts(ctx, l.taskID, args.AfterSeq)
if err != nil {
return "", err
@@ -272,7 +262,7 @@ func (m *Manager) boardTools(l *live) []mcp.Tool {
fmt.Fprintf(b, "#%d %s:\n%v\n\n", ev.Seq, postAuthor(ev), ev.Payload["text"])
}
return strings.TrimSpace(b.String()), nil
- },
+ }),
},
}
}
diff --git a/internal/agents/terminal.go b/internal/agents/terminal.go
index 2fcdf5f..90fd635 100644
--- a/internal/agents/terminal.go
+++ b/internal/agents/terminal.go
@@ -29,12 +29,7 @@ func (m *Manager) terminalReadTool(l *live) mcp.Tool {
"Returns its recent output as plain text, escape sequences removed, at most the last 64 KiB, then the task's terminals by id and name. " +
"Full-screen programs (vim, top, less) come out garbled, since this is the output stream, not a picture of the screen.",
InputSchema: json.RawMessage(`{"type": "object", "properties": {` + terminalArg + `}}`),
- Handler: func(ctx context.Context, raw json.RawMessage) (string, error) {
- args := terminalReadArgs{}
- err := json.Unmarshal(raw, &args)
- if err != nil {
- return "", fmt.Errorf("malformed terminal_read call: %w", err)
- }
+ Handler: mcp.Typed(func(ctx context.Context, args terminalReadArgs) (string, error) {
term := m.terminals.First(l.taskID)
if args.Terminal != "" {
term = m.terminals.Get(l.taskID, args.Terminal)
@@ -51,7 +46,7 @@ func (m *Manager) terminalReadTool(l *live) mcp.Tool {
out += "\n[shell ended: " + exit + "]"
}
return out + inputLine(term) + m.terminalList(l.taskID), nil
- },
+ }),
}
}
@@ -122,12 +117,7 @@ func (m *Manager) terminalTypeTool(l *live) mcp.Tool {
"append": {"type": "boolean", "description": "Type text even though the input line already holds unsent text, adding to it."}
}
}`),
- Handler: func(ctx context.Context, raw json.RawMessage) (string, error) {
- args := terminalTypeArgs{}
- err := json.Unmarshal(raw, &args)
- if err != nil {
- return "", fmt.Errorf("malformed terminal_type call: %w", err)
- }
+ Handler: mcp.Typed(func(ctx context.Context, args terminalTypeArgs) (string, error) {
task, err := m.store.GetTask(ctx, l.taskID)
if err != nil {
return "", err
@@ -192,7 +182,7 @@ func (m *Manager) terminalTypeTool(l *live) mcp.Tool {
out += "\n[byre messages you when the shell is next back at its prompt]"
}
return out, nil
- },
+ }),
}
}
diff --git a/internal/agents/workflows.go b/internal/agents/workflows.go
index ecc9c6f..543d6e5 100644
--- a/internal/agents/workflows.go
+++ b/internal/agents/workflows.go
@@ -665,14 +665,9 @@ func (m *Manager) finishStageTool(l *live) mcp.Tool {
},
"required": ["output"]
}`),
- Handler: func(ctx context.Context, raw json.RawMessage) (string, error) {
- args := struct {
- Output string `json:"output"`
- }{}
- err := json.Unmarshal(raw, &args)
- if err != nil {
- return "", fmt.Errorf("malformed finish_stage call: %w", err)
- }
+ Handler: mcp.Typed(func(ctx context.Context, args struct {
+ Output string `json:"output"`
+ }) (string, error) {
if strings.TrimSpace(args.Output) == "" {
return "", fmt.Errorf("%w: output is required", ErrInvalid)
}
@@ -696,7 +691,7 @@ func (m *Manager) finishStageTool(l *live) mcp.Tool {
return lastStageText(frozen.Definition) + " End your turn now with your summary.", nil
}
return fmt.Sprintf("When this turn ends, byre hands the output to %s. End your turn now with your summary.", quoteList(next)), nil
- },
+ }),
}
}
@@ -787,12 +782,17 @@ func prettyJSON(v any) (string, error) {
return string(b), err
}
-func (m *Manager) workflowTools(tool func(name, description, schema string, run func(ctx context.Context, user api.User, raw json.RawMessage) (string, error)) mcp.Tool) []mcp.Tool {
+type projectWorkflowArgs struct {
+ ProjectID string `json:"project_id"`
+ Workflow string `json:"workflow"`
+}
+
+func (m *Manager) workflowTools(l *live) []mcp.Tool {
return []mcp.Tool{
- tool("list_workflows",
+ leadTool(m, l, "list_workflows",
"List the workflows on the server: each one's name and description.",
`{"type": "object", "properties": {}}`,
- func(ctx context.Context, user api.User, raw json.RawMessage) (string, error) {
+ func(ctx context.Context, user api.User, _ struct{}) (string, error) {
workflows, err := m.store.ListWorkflows(ctx)
if err != nil {
return "", err
@@ -811,21 +811,16 @@ func (m *Manager) workflowTools(tool func(name, description, schema string, run
return b.String(), nil
}),
- tool("read_workflow",
+ leadTool(m, l, "read_workflow",
"Read a workflow's definition: with name, the workflow on the server; with task_id, the copy that task froze when it was created. "+workflowHelp,
`{"type": "object", "properties": {
"name": {"type": "string"},
"task_id": {"type": "string"}
}}`,
- func(ctx context.Context, user api.User, raw json.RawMessage) (string, error) {
- args := struct {
- Name string `json:"name"`
- TaskID string `json:"task_id"`
- }{}
- err := json.Unmarshal(raw, &args)
- if err != nil {
- return "", err
- }
+ func(ctx context.Context, user api.User, args struct {
+ Name string `json:"name"`
+ TaskID string `json:"task_id"`
+ }) (string, error) {
if (args.Name == "") == (args.TaskID == "") {
return "", fmt.Errorf("%w: give exactly one of name and task_id", ErrInvalid)
}
@@ -847,22 +842,17 @@ func (m *Manager) workflowTools(tool func(name, description, schema string, run
return prettyJSON(def)
}),
- tool("save_workflow",
+ leadTool(m, l, "save_workflow",
"Create or replace a workflow on the server from its definition. Tasks that already froze it keep their copy. "+
"The name is one line of 1 to 100 characters, shown to the human on buttons and pickers. "+workflowHelp,
`{"type": "object", "properties": {
"name": {"type": "string"},
"definition": `+workflowDefinitionSchema+`
}, "required": ["name", "definition"], `+workflowSchemaDefs+`}`,
- func(ctx context.Context, user api.User, raw json.RawMessage) (string, error) {
- args := struct {
- Name string `json:"name"`
- Definition json.RawMessage `json:"definition"`
- }{}
- err := json.Unmarshal(raw, &args)
- if err != nil {
- return "", err
- }
+ func(ctx context.Context, user api.User, args struct {
+ Name string `json:"name"`
+ Definition json.RawMessage `json:"definition"`
+ }) (string, error) {
w, err := m.SaveWorkflow(ctx, args.Name, "", args.Definition, leadActor(user.Login))
if err != nil {
return "", err
@@ -870,21 +860,13 @@ func (m *Manager) workflowTools(tool func(name, description, schema string, run
return fmt.Sprintf("Saved workflow %q.", w.Name), nil
}),
- tool("set_review_workflow",
+ leadTool(m, l, "set_review_workflow",
"Set the review workflow (any saved workflow whose stages are single agents, not teams) whose stages review every snapshot of a project's tasks, from the next snapshot on; an empty workflow turns reviews off for the project. Only when the human asks.",
`{"type": "object", "properties": {
"project_id": {"type": "string"},
"workflow": {"type": "string"}
}, "required": ["project_id", "workflow"]}`,
- func(ctx context.Context, user api.User, raw json.RawMessage) (string, error) {
- args := struct {
- ProjectID string `json:"project_id"`
- Workflow string `json:"workflow"`
- }{}
- err := json.Unmarshal(raw, &args)
- if err != nil {
- return "", err
- }
+ func(ctx context.Context, user api.User, args projectWorkflowArgs) (string, error) {
p, err := m.SetReviewWorkflow(ctx, args.ProjectID, args.Workflow, leadActor(user.Login))
if err != nil {
return "", err
@@ -895,22 +877,14 @@ func (m *Manager) workflowTools(tool func(name, description, schema string, run
return fmt.Sprintf("Project %s reviews with workflow %q from its next snapshot on.", p.Name, p.ReviewWorkflow), nil
}),
- tool("set_audit_workflow",
+ leadTool(m, l, "set_audit_workflow",
"Set the audit workflow (any saved workflow) that byre runs in the background on a project's default branch: one run at a time, started whenever the branch's newest commit hasn't had one, to check the whole codebase against its standards rather than one change. "+
"An audit's tasks stay out of list_tasks and your signals until one asks a question, calls ready_for_merge, hits an error, or suggests discarding in a workflow without auto_discard. An empty workflow turns audits off. Only when the human asks.",
`{"type": "object", "properties": {
"project_id": {"type": "string"},
"workflow": {"type": "string"}
}, "required": ["project_id", "workflow"]}`,
- func(ctx context.Context, user api.User, raw json.RawMessage) (string, error) {
- args := struct {
- ProjectID string `json:"project_id"`
- Workflow string `json:"workflow"`
- }{}
- err := json.Unmarshal(raw, &args)
- if err != nil {
- return "", err
- }
+ func(ctx context.Context, user api.User, args projectWorkflowArgs) (string, error) {
p, err := m.SetAuditWorkflow(ctx, args.ProjectID, args.Workflow, leadActor(user.Login))
if err != nil {
return "", err
@@ -921,19 +895,14 @@ func (m *Manager) workflowTools(tool func(name, description, schema string, run
return fmt.Sprintf("Project %s is audited with workflow %q.", p.Name, p.AuditWorkflow), nil
}),
- tool("set_action_button",
+ leadTool(m, l, "set_action_button",
"Set the workflow the human's iPhone Action button starts, and the project it runs in: pressing the button opens byre for a prompt and starts that workflow with it as the brief. "+
"Empty project_id and workflow turn it off. Only when the human asks.",
`{"type": "object", "properties": {
"project_id": {"type": "string"},
"workflow": {"type": "string"}
}, "required": ["project_id", "workflow"]}`,
- func(ctx context.Context, user api.User, raw json.RawMessage) (string, error) {
- q := api.QuickRun{}
- err := json.Unmarshal(raw, &q)
- if err != nil {
- return "", err
- }
+ func(ctx context.Context, user api.User, q api.QuickRun) (string, error) {
u, err := m.SetQuickRun(ctx, user.Login, q)
if err != nil {
return "", err
@@ -944,18 +913,13 @@ func (m *Manager) workflowTools(tool func(name, description, schema string, run
return fmt.Sprintf("The Action button starts workflow %q in project %s.", u.QuickRun.Workflow, u.QuickRun.ProjectId), nil
}),
- tool("delete_workflow",
+ leadTool(m, l, "delete_workflow",
"Delete a workflow from the server. Tasks that froze it keep their copy. Only when the human asks.",
`{"type": "object", "properties": {"name": {"type": "string"}}, "required": ["name"]}`,
- func(ctx context.Context, user api.User, raw json.RawMessage) (string, error) {
- args := struct {
- Name string `json:"name"`
- }{}
- err := json.Unmarshal(raw, &args)
- if err != nil {
- return "", err
- }
- err = m.store.DeleteWorkflow(ctx, args.Name, leadActor(user.Login))
+ func(ctx context.Context, user api.User, args struct {
+ Name string `json:"name"`
+ }) (string, error) {
+ err := m.store.DeleteWorkflow(ctx, args.Name, leadActor(user.Login))
if err != nil {
return "", err
}
diff --git a/internal/api/api.gen.go b/internal/api/api.gen.go
index d677763..30bb17a 100644
--- a/internal/api/api.gen.go
+++ b/internal/api/api.gen.go
@@ -392,10 +392,7 @@ const (
SnapshotTriggerAnswer SnapshotTrigger = "answer"
SnapshotTriggerAskHuman SnapshotTrigger = "ask_human"
SnapshotTriggerFork SnapshotTrigger = "fork"
- SnapshotTriggerMessage SnapshotTrigger = "message"
SnapshotTriggerRequested SnapshotTrigger = "requested"
- SnapshotTriggerTimer SnapshotTrigger = "timer"
- SnapshotTriggerToolCall SnapshotTrigger = "tool_call"
SnapshotTriggerTurnEnd SnapshotTrigger = "turn_end"
)
@@ -408,14 +405,8 @@ func (e SnapshotTrigger) Valid() bool {
return true
case SnapshotTriggerFork:
return true
- case SnapshotTriggerMessage:
- return true
case SnapshotTriggerRequested:
return true
- case SnapshotTriggerTimer:
- return true
- case SnapshotTriggerToolCall:
- return true
case SnapshotTriggerTurnEnd:
return true
default:
@@ -1423,11 +1414,11 @@ type Snapshot struct {
TaskId string `json:"task_id"`
TreeSha string `json:"tree_sha"`
- // Trigger turn_end: after byre's end-of-turn commit. requested: a task's agent or the lead asked for it with take_snapshot. answer: just before answers from the human were handed to the agent. fork: when the task's agent forked the task with its fork tool. ask_human: after byre committed the worktree inside an ask_human call whose task is behind the default branch, just before suggesting the agent merge it in. answer, fork and ask_human snapshots are never reviewed. message (the agent wrote text after one or more tool calls), tool_call and timer appear only on snapshots taken by earlier builds.
+ // Trigger turn_end: after byre's end-of-turn commit. requested: a task's agent or the lead asked for it with take_snapshot. answer: just before answers from the human were handed to the agent. fork: when the task's agent forked the task with its fork tool. ask_human: after byre committed the worktree inside an ask_human call whose task is behind the default branch, just before suggesting the agent merge it in. answer, fork and ask_human snapshots are never reviewed.
Trigger SnapshotTrigger `json:"trigger"`
}
-// SnapshotTrigger turn_end: after byre's end-of-turn commit. requested: a task's agent or the lead asked for it with take_snapshot. answer: just before answers from the human were handed to the agent. fork: when the task's agent forked the task with its fork tool. ask_human: after byre committed the worktree inside an ask_human call whose task is behind the default branch, just before suggesting the agent merge it in. answer, fork and ask_human snapshots are never reviewed. message (the agent wrote text after one or more tool calls), tool_call and timer appear only on snapshots taken by earlier builds.
+// SnapshotTrigger turn_end: after byre's end-of-turn commit. requested: a task's agent or the lead asked for it with take_snapshot. answer: just before answers from the human were handed to the agent. fork: when the task's agent forked the task with its fork tool. ask_human: after byre committed the worktree inside an ask_human call whose task is behind the default branch, just before suggesting the agent merge it in. answer, fork and ask_human snapshots are never reviewed.
type SnapshotTrigger string
// StageAttempt defines model for StageAttempt.
@@ -4921,371 +4912,369 @@ func HandlerWithOptions(si ServerInterface, options StdHTTPServerOptions) http.H
// const string: with thousands of chunks the chained `+` fold is several
// times slower for the Go compiler than parsing a slice literal.
var swaggerSpec = []string{
- "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",
- "MHgr7uu3pnz8+PH/HwA=",
+ "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",
}
// decodeSpec returns the embedded OpenAPI spec as raw JSON bytes,
diff --git a/internal/git/rename.go b/internal/git/rename.go
deleted file mode 100644
index 470cfd9..0000000
--- a/internal/git/rename.go
+++ /dev/null
@@ -1,184 +0,0 @@
-package git
-
-import (
- "bufio"
- "context"
- "fmt"
- "io"
- "os"
- "path/filepath"
- "strconv"
- "strings"
-)
-
-func RenameRefs(ctx context.Context, repo string, rename func(string) string) error {
- branches, err := run(ctx, repo, "for-each-ref", "--format=%(refname:short)", "refs/heads/byre/")
- if err != nil {
- return err
- }
- for _, b := range splitLines(branches) {
- if rename(b) == b {
- continue
- }
- _, err = run(ctx, repo, "branch", "-m", b, rename(b))
- if err != nil {
- return err
- }
- }
-
- snaps, err := run(ctx, repo, "for-each-ref", "--format=%(objectname) %(refname)", "refs/byre/tasks/")
- if err != nil {
- return err
- }
- b := &strings.Builder{}
- for _, l := range splitLines(snaps) {
- sha, ref, _ := strings.Cut(l, " ")
- if rename(ref) == ref {
- continue
- }
- fmt.Fprintf(b, "create %s %s\ndelete %s %s\n", rename(ref), sha, ref, sha)
- }
- if b.Len() == 0 {
- return nil
- }
- _, err = runInput(ctx, repo, nil, b.String(), "update-ref", "--stdin")
- return err
-}
-
-func RepairWorktrees(ctx context.Context, repo string, paths []string) error {
- if len(paths) == 0 {
- return nil
- }
- _, err := run(ctx, repo, append([]string{"worktree", "repair"}, paths...)...)
- return err
-}
-
-func RewriteNotes(ctx context.Context, repo string, rewrite func(string) string) error {
- parent, stderr, code, err := runCode(ctx, repo, "rev-parse", "--verify", "--quiet", NotesRef)
- if err != nil {
- return err
- }
- if code == 1 {
- return nil
- }
- if code != 0 {
- return fmt.Errorf("git rev-parse %s: exit status %d: %s", NotesRef, code, stderr)
- }
-
- listing, err := run(ctx, repo, "ls-tree", "-r", parent)
- if err != nil {
- return err
- }
- paths := map[string]string{}
- shas := []string{}
- for _, l := range splitLines(listing) {
- meta, path, _ := strings.Cut(l, "\t")
- fields := strings.Fields(meta)
- paths[fields[2]] = path
- shas = append(shas, fields[2])
- }
- if len(shas) == 0 {
- return nil
- }
-
- contents, err := catBlobs(ctx, repo, shas)
- if err != nil {
- return err
- }
-
- dir, err := os.MkdirTemp("", "byre-notes-*")
- if err != nil {
- return err
- }
- defer os.RemoveAll(dir)
-
- changed := []string{}
- files := []string{}
- for _, sha := range shas {
- text := rewrite(contents[sha])
- if text == contents[sha] {
- continue
- }
- file := filepath.Join(dir, sha)
- err = os.WriteFile(file, []byte(text), 0o644)
- if err != nil {
- return err
- }
- changed = append(changed, sha)
- files = append(files, file)
- }
- if len(changed) == 0 {
- return nil
- }
-
- blobs, err := runInput(ctx, repo, nil, strings.Join(files, "\n")+"\n", "hash-object", "-w", "--stdin-paths")
- if err != nil {
- return err
- }
- index := []string{"GIT_INDEX_FILE=" + filepath.Join(dir, "index")}
- _, err = runEnv(ctx, repo, index, "read-tree", parent)
- if err != nil {
- return err
- }
- b := &strings.Builder{}
- for i, blob := range splitLines(blobs) {
- fmt.Fprintf(b, "100644 %s\t%s\n", blob, paths[changed[i]])
- }
- _, err = runInput(ctx, repo, index, b.String(), "update-index", "--index-info")
- if err != nil {
- return err
- }
- tree, err := runEnv(ctx, repo, index, "write-tree")
- if err != nil {
- return err
- }
- who, err := run(ctx, repo, "log", "-1", "--format=%cn%n%ce", parent)
- if err != nil {
- return err
- }
- name, email, _ := strings.Cut(who, "\n")
- env := IdentityEnv(name, email)
- commit, err := runEnv(ctx, repo, env, "commit-tree", "-p", parent, "-m", "Rewrite byre ids in notes", tree)
- if err != nil {
- return err
- }
- _, err = run(ctx, repo, "update-ref", NotesRef, commit, parent)
- return err
-}
-
-func catBlobs(ctx context.Context, repo string, shas []string) (map[string]string, error) {
- out, err := runRaw(ctx, repo, nil, strings.Join(shas, "\n")+"\n", "cat-file", "--batch")
- if err != nil {
- return nil, err
- }
- r := bufio.NewReader(strings.NewReader(out))
- contents := map[string]string{}
- for range shas {
- header, err := r.ReadString('\n')
- if err != nil {
- return nil, err
- }
- fields := strings.Fields(header)
- if len(fields) != 3 {
- return nil, fmt.Errorf("git cat-file --batch: unexpected header %q", header)
- }
- size, err := strconv.Atoi(fields[2])
- if err != nil {
- return nil, err
- }
- body := make([]byte, size+1)
- _, err = io.ReadFull(r, body)
- if err != nil {
- return nil, err
- }
- contents[fields[0]] = string(body[:size])
- }
- return contents, nil
-}
-
-func splitLines(out string) []string {
- if out == "" {
- return []string{}
- }
- return strings.Split(out, "\n")
-}
diff --git a/internal/ids/ids.go b/internal/ids/ids.go
index 1a7628c..a27f7e2 100644
--- a/internal/ids/ids.go
+++ b/internal/ids/ids.go
@@ -2,9 +2,6 @@ package ids
import (
"crypto/rand"
- "crypto/sha256"
- "encoding/binary"
- "strings"
)
const (
@@ -25,7 +22,6 @@ const (
Blob Prefix = "blb"
LeadNote Prefix = "not"
WorkflowDefinition Prefix = "wfd"
- WorkflowRun Prefix = "run"
Send Prefix = "snd"
Client Prefix = "cli"
ClientRequest Prefix = "req"
@@ -48,14 +44,3 @@ func New(prefix Prefix) string {
}
return string(prefix) + string(out)
}
-
-func Derive(prefix Prefix, parts ...string) string {
- sum := sha256.Sum256([]byte(strings.Join(parts, "\x00")))
- n := binary.BigEndian.Uint64(sum[:8])
- out := make([]byte, randomLength)
- for i := range out {
- out[i] = alphabet[n%uint64(len(alphabet))]
- n /= uint64(len(alphabet))
- }
- return string(prefix) + string(out)
-}
diff --git a/internal/mcp/server.go b/internal/mcp/server.go
index eebe72f..940d460 100644
--- a/internal/mcp/server.go
+++ b/internal/mcp/server.go
@@ -17,6 +17,35 @@ type Tool struct {
Rich func(ctx context.Context, args json.RawMessage) Result
}
+func decode[A any](raw json.RawMessage) (A, error) {
+ args := *new(A)
+ err := json.Unmarshal(raw, &args)
+ if err != nil {
+ return args, fmt.Errorf("malformed call: %w", err)
+ }
+ return args, nil
+}
+
+func Typed[A any](run func(ctx context.Context, args A) (string, error)) func(ctx context.Context, raw json.RawMessage) (string, error) {
+ return func(ctx context.Context, raw json.RawMessage) (string, error) {
+ args, err := decode[A](raw)
+ if err != nil {
+ return "", err
+ }
+ return run(ctx, args)
+ }
+}
+
+func TypedRich[A any](run func(ctx context.Context, args A) Result) func(ctx context.Context, raw json.RawMessage) Result {
+ return func(ctx context.Context, raw json.RawMessage) Result {
+ args, err := decode[A](raw)
+ if err != nil {
+ return TextResult(err.Error(), true)
+ }
+ return run(ctx, args)
+ }
+}
+
type Content struct {
Type string `json:"type"`
Text string `json:"text"`
diff --git a/internal/store/migratedefinitions.go b/internal/store/migratedefinitions.go
deleted file mode 100644
index c8a76f1..0000000
--- a/internal/store/migratedefinitions.go
+++ /dev/null
@@ -1,135 +0,0 @@
-package store
-
-import (
- "bytes"
- "context"
- "database/sql"
- "encoding/json"
- "fmt"
-
- "g.fc.run/ai/byre/internal/api"
-)
-
-func migrateWorkflowDefinitions(tx *sql.Tx, _ string) error {
- _, err := tx.Exec(`
- CREATE TABLE workflow_definitions (
- id TEXT PRIMARY KEY,
- description TEXT NOT NULL
- );
- CREATE TABLE workflow_steps (
- definition_id TEXT NOT NULL REFERENCES workflow_definitions(id),
- id INTEGER NOT NULL,
- parent INTEGER,
- position INTEGER NOT NULL,
- kind TEXT NOT NULL CHECK (kind IN ('stage', 'order', 'parallel')),
- stage TEXT NOT NULL,
- model TEXT NOT NULL,
- prompt TEXT NOT NULL,
- PRIMARY KEY (definition_id, id),
- UNIQUE (definition_id, parent, position)
- );
- CREATE TABLE workflow_agents (
- definition_id TEXT NOT NULL REFERENCES workflow_definitions(id),
- step_id INTEGER NOT NULL,
- position INTEGER NOT NULL,
- name TEXT NOT NULL,
- model TEXT NOT NULL,
- prompt TEXT NOT NULL,
- PRIMARY KEY (definition_id, step_id, position),
- FOREIGN KEY (definition_id, step_id) REFERENCES workflow_steps(definition_id, id)
- );
- ALTER TABLE tasks ADD COLUMN workflow_definition_id TEXT REFERENCES workflow_definitions(id);
- ALTER TABLE reviews ADD COLUMN definition_id TEXT REFERENCES workflow_definitions(id);
- ALTER TABLE workflows ADD COLUMN definition_id TEXT REFERENCES workflow_definitions(id);
- `)
- if err != nil {
- return err
- }
-
- ids := map[string]string{}
- for _, c := range []struct{ table, key, from, to string }{
- {"workflows", "name", "definition", "definition_id"},
- {"tasks", "id", "workflow_definition", "workflow_definition_id"},
- {"reviews", "id", "definition", "definition_id"},
- } {
- err = moveDefinitions(tx, ids, c.table, c.key, c.from, c.to)
- if err != nil {
- return err
- }
- }
-
- _, err = tx.Exec(`
- CREATE TABLE workflows_next (
- name TEXT PRIMARY KEY,
- definition_id TEXT NOT NULL REFERENCES workflow_definitions(id)
- );
- INSERT INTO workflows_next (name, definition_id) SELECT name, definition_id FROM workflows;
- DROP TABLE workflows;
- ALTER TABLE workflows_next RENAME TO workflows;
- ALTER TABLE tasks DROP COLUMN workflow_definition;
- ALTER TABLE reviews DROP COLUMN definition;
- `)
- return err
-}
-
-func moveDefinitions(tx *sql.Tx, ids map[string]string, table, key, from, to string) error {
- rows, err := tx.Query(fmt.Sprintf(`SELECT %s, %s FROM %s WHERE %s != ''`, key, from, table, from))
- if err != nil {
- return err
- }
- type row struct{ key, def string }
- all := []row{}
- for rows.Next() {
- r := row{}
- err = rows.Scan(&r.key, &r.def)
- if err != nil {
- rows.Close()
- return err
- }
- all = append(all, r)
- }
- rows.Close()
- err = rows.Err()
- if err != nil {
- return err
- }
-
- ctx := context.Background()
- for _, r := range all {
- id, found := ids[r.def]
- if !found {
- def := api.WorkflowDefinition{}
- dec := json.NewDecoder(bytes.NewReader([]byte(r.def)))
- dec.DisallowUnknownFields()
- err = dec.Decode(&def)
- if err != nil {
- return fmt.Errorf("%s %s: %w", table, r.key, err)
- }
- id, err = insertDefinition(ctx, tx, def)
- if err != nil {
- return fmt.Errorf("%s %s: %w", table, r.key, err)
- }
- back, err := loadDefinition(ctx, tx, id)
- if err != nil {
- return fmt.Errorf("%s %s: %w", table, r.key, err)
- }
- want, err := json.Marshal(def)
- if err != nil {
- return err
- }
- got, err := json.Marshal(back)
- if err != nil {
- return err
- }
- if !bytes.Equal(want, got) {
- return fmt.Errorf("%s %s: definition reads back as %s, not %s", table, r.key, got, want)
- }
- ids[r.def] = id
- }
- _, err = tx.Exec(fmt.Sprintf(`UPDATE %s SET %s = ? WHERE %s = ?`, table, to, key), id, r.key)
- if err != nil {
- return err
- }
- }
- return nil
-}
diff --git a/internal/store/migrateids.go b/internal/store/migrateids.go
deleted file mode 100644
index 3ea20d5..0000000
--- a/internal/store/migrateids.go
+++ /dev/null
@@ -1,256 +0,0 @@
-package store
-
-import (
- "context"
- "database/sql"
- "errors"
- "fmt"
- "io/fs"
- "os"
- "path/filepath"
- "regexp"
- "strings"
-
- "g.fc.run/ai/byre/internal/git"
- "g.fc.run/ai/byre/internal/ids"
-)
-
-var idToken = regexp.MustCompile(`[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}|queued-[0-9]+|[0-9A-Za-z]+`)
-
-var keptColumns = map[string]bool{
- "agent_sessions.vendor_session_id": true,
- "agent_sessions.forked_from": true,
-}
-
-func migratePrefixIDs(tx *sql.Tx, dataDir string) error {
- renames, err := idRenames(tx)
- if err != nil {
- return err
- }
- rewrite := func(s string) string {
- return idToken.ReplaceAllStringFunc(s, func(tok string) string {
- if to, ok := renames[tok]; ok {
- return to
- }
- return tok
- })
- }
-
- tables, err := textColumns(tx)
- if err != nil {
- return err
- }
- for table, columns := range tables {
- err = rewriteTable(tx, table, columns, rewrite)
- if err != nil {
- return fmt.Errorf("%s: %w", table, err)
- }
- }
-
- repos := map[string]string{}
- rows, err := tx.Query(`SELECT id, repo_path FROM projects`)
- if err != nil {
- return err
- }
- for rows.Next() {
- id, repo := "", ""
- err = rows.Scan(&id, &repo)
- if err != nil {
- rows.Close()
- return err
- }
- repos[id] = repo
- }
- rows.Close()
- err = rows.Err()
- if err != nil {
- return err
- }
-
- for _, sub := range []string{"blobs", "hands", "antigravity", "worktrees"} {
- err = renameEntries(filepath.Join(dataDir, sub), rewrite)
- if err != nil {
- return err
- }
- }
- for project, repo := range repos {
- dir := filepath.Join(dataDir, "worktrees", project)
- err = renameEntries(dir, rewrite)
- if err != nil {
- return err
- }
- worktrees, err := filepath.Glob(filepath.Join(dir, "*", ".git"))
- if err != nil {
- return err
- }
- for i, w := range worktrees {
- worktrees[i] = filepath.Dir(w)
- }
- err = git.RepairWorktrees(context.Background(), repo, worktrees)
- if err != nil {
- return err
- }
- err = git.RenameRefs(context.Background(), repo, rewrite)
- if err != nil {
- return err
- }
- err = git.RewriteNotes(context.Background(), repo, rewrite)
- if err != nil {
- return err
- }
- }
- return nil
-}
-
-func idRenames(tx *sql.Tx) (map[string]string, error) {
- sources := []struct {
- prefix ids.Prefix
- query string
- }{
- {ids.Project, `SELECT id FROM projects`},
- {ids.Task, `SELECT id FROM tasks`},
- {ids.WorkflowRun, `SELECT DISTINCT workflow_run FROM tasks`},
- {ids.Session, `SELECT id FROM agent_sessions`},
- {ids.Snapshot, `SELECT id FROM snapshots`},
- {ids.Review, `SELECT id FROM reviews`},
- {ids.InboxItem, `SELECT id FROM inbox_items`},
- {ids.InboxResponse, `SELECT id FROM inbox_responses`},
- {ids.Blob, `SELECT id FROM blobs`},
- {ids.LeadNote, `SELECT id FROM lead_notes`},
- {ids.WorkflowDefinition, `SELECT id FROM workflow_definitions`},
- {ids.Send, `SELECT DISTINCT send_id FROM task_inputs`},
- {ids.Send, `SELECT DISTINCT send_id FROM lead_inputs`},
- {ids.Send, `SELECT DISTINCT json_extract(payload, '$.send_id') FROM events WHERE kind IN ('user.queued', 'user.message') AND json_extract(payload, '$.send_id') IS NOT NULL`},
- }
-
- renames := map[string]string{}
- for _, src := range sources {
- rows, err := tx.Query(src.query)
- if err != nil {
- return nil, err
- }
- for rows.Next() {
- old := ""
- err = rows.Scan(&old)
- if err != nil {
- rows.Close()
- return nil, err
- }
- _, mapped := renames[old]
- if old == "" || mapped || strings.HasPrefix(old, string(src.prefix)) {
- continue
- }
- renames[old] = ids.Derive(src.prefix, old)
- }
- rows.Close()
- err = rows.Err()
- if err != nil {
- return nil, err
- }
- }
- return renames, nil
-}
-
-func textColumns(tx *sql.Tx) (map[string][]string, error) {
- rows, err := tx.Query(`SELECT m.name, p.name FROM sqlite_master m JOIN pragma_table_info(m.name) p WHERE m.type = 'table' AND m.name NOT IN ('migrations', 'sqlite_sequence') AND upper(p.type) = 'TEXT'`)
- if err != nil {
- return nil, err
- }
- defer rows.Close()
-
- tables := map[string][]string{}
- for rows.Next() {
- table, column := "", ""
- err = rows.Scan(&table, &column)
- if err != nil {
- return nil, err
- }
- if keptColumns[table+"."+column] {
- continue
- }
- tables[table] = append(tables[table], column)
- }
- return tables, rows.Err()
-}
-
-func rewriteTable(tx *sql.Tx, table string, columns []string, rewrite func(string) string) error {
- quoted := []string{}
- for _, c := range columns {
- quoted = append(quoted, `"`+c+`"`)
- }
- query := fmt.Sprintf(`SELECT rowid, %s FROM "%s" WHERE rowid > ? ORDER BY rowid LIMIT 2000`, strings.Join(quoted, ", "), table)
-
- after := int64(0)
- for {
- type change struct {
- rowid int64
- column string
- value string
- }
- changes := []change{}
- n := 0
-
- rows, err := tx.Query(query, after)
- if err != nil {
- return err
- }
- for rows.Next() {
- values := make([]sql.NullString, len(columns))
- dest := []any{&after}
- for i := range values {
- dest = append(dest, &values[i])
- }
- err = rows.Scan(dest...)
- if err != nil {
- rows.Close()
- return err
- }
- n++
- for i, v := range values {
- if !v.Valid {
- continue
- }
- to := rewrite(v.String)
- if to != v.String {
- changes = append(changes, change{after, columns[i], to})
- }
- }
- }
- rows.Close()
- err = rows.Err()
- if err != nil {
- return err
- }
-
- for _, c := range changes {
- _, err = tx.Exec(fmt.Sprintf(`UPDATE "%s" SET "%s" = ? WHERE rowid = ?`, table, c.column), c.value, c.rowid)
- if err != nil {
- return err
- }
- }
- if n == 0 {
- return nil
- }
- }
-}
-
-func renameEntries(dir string, rewrite func(string) string) error {
- entries, err := os.ReadDir(dir)
- if errors.Is(err, fs.ErrNotExist) {
- return nil
- }
- if err != nil {
- return err
- }
- for _, e := range entries {
- to := rewrite(e.Name())
- if to == e.Name() {
- continue
- }
- err = os.Rename(filepath.Join(dir, e.Name()), filepath.Join(dir, to))
- if err != nil {
- return err
- }
- }
- return nil
-}
diff --git a/internal/store/migrateruns.go b/internal/store/migrateruns.go
deleted file mode 100644
index 41c813b..0000000
--- a/internal/store/migrateruns.go
+++ /dev/null
@@ -1,70 +0,0 @@
-package store
-
-import (
- "database/sql"
- "fmt"
-
- "g.fc.run/ai/byre/internal/ids"
-)
-
-func migrateRunTasks(tx *sql.Tx, _ string) error {
- rows, err := tx.Query(`SELECT workflow_run FROM tasks WHERE workflow_run != '' GROUP BY workflow_run ORDER BY min(rowid)`)
- if err != nil {
- return err
- }
- runs := []string{}
- for rows.Next() {
- run := ""
- err = rows.Scan(&run)
- if err != nil {
- rows.Close()
- return err
- }
- runs = append(runs, run)
- }
- rows.Close()
- err = rows.Err()
- if err != nil {
- return err
- }
-
- for _, run := range runs {
- id := ids.Derive(ids.Task, run)
- _, err = tx.Exec(`
- INSERT INTO tasks (id, project_id, title, branch, worktree_path, base_ref, status, created_by, created_at, closed_at, todo_item, color,
- workflow_name, workflow_definition_id, workflow_brief, workflow_title, audit)
- SELECT ?1, f.project_id, f.workflow_title, '', '', '',
- CASE
- WHEN EXISTS (SELECT 1 FROM tasks s WHERE s.workflow_run = ?2 AND s.status = 'active') THEN 'active'
- WHEN EXISTS (SELECT 1 FROM tasks s WHERE s.workflow_run = ?2 AND s.status = 'merged') THEN 'merged'
- ELSE 'discarded'
- END,
- f.created_by, f.created_at,
- CASE WHEN EXISTS (SELECT 1 FROM tasks s WHERE s.workflow_run = ?2 AND s.status = 'active') THEN NULL
- ELSE (SELECT max(s.closed_at) FROM tasks s WHERE s.workflow_run = ?2) END,
- f.todo_item, f.color, f.workflow_name, f.workflow_definition_id, f.workflow_brief, f.workflow_title, f.audit
- FROM tasks f WHERE f.workflow_run = ?2 ORDER BY f.created_at LIMIT 1`, id, run)
- if err != nil {
- return fmt.Errorf("run %s: %w", run, err)
- }
- _, err = tx.Exec(`
- UPDATE events SET task_id = ?1, payload = json_set(payload, '$.id', ?1)
- WHERE kind = 'task.workflow_finished' AND task_id IN (SELECT id FROM tasks WHERE workflow_run = ?2)`, id, run)
- if err != nil {
- return fmt.Errorf("run %s: %w", run, err)
- }
- _, err = tx.Exec(`
- UPDATE tasks SET parent_task_id = ?1, workflow_name = '', workflow_definition_id = NULL, workflow_brief = '', workflow_title = '', audit = '', todo_item = ''
- WHERE workflow_run = ?2`, id, run)
- if err != nil {
- return fmt.Errorf("run %s: %w", run, err)
- }
- _, err = tx.Exec(`UPDATE tasks SET state = `+taskStateExpr+` WHERE id = ?`, id)
- if err != nil {
- return fmt.Errorf("run %s: %w", run, err)
- }
- }
-
- _, err = tx.Exec(`ALTER TABLE tasks DROP COLUMN workflow_run`)
- return err
-}
diff --git a/internal/store/migrateschedules.go b/internal/store/migrateschedules.go
deleted file mode 100644
index 72f36b1..0000000
--- a/internal/store/migrateschedules.go
+++ /dev/null
@@ -1,55 +0,0 @@
-package store
-
-import (
- "context"
- "database/sql"
- "fmt"
-
- "g.fc.run/ai/byre/internal/api"
-)
-
-func migrateScheduleWorkflows(tx *sql.Tx, _ string) error {
- rows, err := tx.Query(`SELECT id, title, model, prompt FROM schedules WHERE workflow = ''`)
- if err != nil {
- return err
- }
- type row struct{ id, title, model, prompt string }
- all := []row{}
- for rows.Next() {
- r := row{}
- err = rows.Scan(&r.id, &r.title, &r.model, &r.prompt)
- if err != nil {
- rows.Close()
- return err
- }
- all = append(all, r)
- }
- rows.Close()
- err = rows.Err()
- if err != nil {
- return err
- }
-
- ctx := context.Background()
- for _, r := range all {
- def := api.WorkflowDefinition{
- Description: r.title,
- Steps: []api.WorkflowStep{{Stage: r.title, Model: r.model, Prompt: r.prompt}},
- }
- id, err := insertDefinition(ctx, tx, def)
- if err != nil {
- return fmt.Errorf("schedule %s: %w", r.id, err)
- }
- _, err = tx.Exec(`INSERT INTO workflows (name, definition_id) VALUES (?, ?)`, r.title, id)
- if err != nil {
- return fmt.Errorf("schedule %s: workflow %q: %w", r.id, r.title, err)
- }
- _, err = tx.Exec(`UPDATE schedules SET workflow = ?, prompt = '' WHERE id = ?`, r.title, r.id)
- if err != nil {
- return err
- }
- }
-
- _, err = tx.Exec(`ALTER TABLE schedules DROP COLUMN model`)
- return err
-}
diff --git a/internal/store/migrateworkflows.go b/internal/store/migrateworkflows.go
deleted file mode 100644
index 16f3b2f..0000000
--- a/internal/store/migrateworkflows.go
+++ /dev/null
@@ -1,258 +0,0 @@
-package store
-
-import (
- "database/sql"
- "encoding/json"
- "fmt"
- "maps"
- "slices"
-
- "g.fc.run/ai/byre/internal/api"
- "g.fc.run/ai/byre/internal/workflow"
-)
-
-type oldStage struct {
- Name string `json:"name"`
- After *[]string `json:"after"`
- Base *string `json:"base"`
- Model string `json:"model"`
- Prompt string `json:"prompt"`
- Agents *[]api.TeamMember `json:"agents"`
-}
-
-type oldDefinition struct {
- Description string `json:"description"`
- Stages []oldStage `json:"stages"`
- Steps *json.RawMessage `json:"steps"`
-}
-
-func migrateWorkflowSteps(tx *sql.Tx, _ string) error {
- err := convertColumn(tx, "workflows", "name", "definition")
- if err != nil {
- return err
- }
- return convertColumn(tx, "tasks", "id", "workflow_definition")
-}
-
-func convertColumn(tx *sql.Tx, table, key, column string) error {
- rows, err := tx.Query(fmt.Sprintf(`SELECT %s, %s FROM %s WHERE %s != ''`, key, column, table, column))
- if err != nil {
- return err
- }
- type row struct{ key, def string }
- all := []row{}
- for rows.Next() {
- r := row{}
- err = rows.Scan(&r.key, &r.def)
- if err != nil {
- rows.Close()
- return err
- }
- all = append(all, r)
- }
- rows.Close()
- err = rows.Err()
- if err != nil {
- return err
- }
-
- for _, r := range all {
- old := oldDefinition{}
- err = json.Unmarshal([]byte(r.def), &old)
- if err != nil {
- return fmt.Errorf("%s %s: %w", table, r.key, err)
- }
- if old.Steps != nil {
- continue
- }
- def, err := toSteps(old)
- if err != nil {
- return fmt.Errorf("%s %s: %w", table, r.key, err)
- }
- j, err := json.Marshal(def)
- if err != nil {
- return err
- }
- _, err = tx.Exec(fmt.Sprintf(`UPDATE %s SET %s = ? WHERE %s = ?`, table, column, key), string(j), r.key)
- if err != nil {
- return err
- }
- }
- return nil
-}
-
-func toSteps(old oldDefinition) (api.WorkflowDefinition, error) {
- after := map[string][]string{}
- byName := map[string]oldStage{}
- for i, s := range old.Stages {
- if s.Base != nil && *s.Base != "parent" {
- return api.WorkflowDefinition{}, fmt.Errorf("stage %q has base %q, which steps can't express", s.Name, *s.Base)
- }
- byName[s.Name] = s
- switch {
- case s.After != nil:
- after[s.Name] = *s.After
- case i == 0:
- after[s.Name] = []string{}
- default:
- after[s.Name] = []string{old.Stages[i-1].Name}
- }
- }
-
- pending := func(names []string, done map[string]bool) []string {
- out := []string{}
- for _, n := range names {
- if !done[n] {
- out = append(out, n)
- }
- }
- return out
- }
- var closure func(name string, done map[string]bool) map[string]bool
- closure = func(name string, done map[string]bool) map[string]bool {
- c := map[string]bool{name: true}
- for _, a := range pending(after[name], done) {
- for n := range closure(a, done) {
- c[n] = true
- }
- }
- return c
- }
- sequence := func(steps ...api.WorkflowStep) api.WorkflowStep {
- order := []api.WorkflowStep{}
- for _, s := range steps {
- if s.Order != nil {
- order = append(order, *s.Order...)
- continue
- }
- order = append(order, s)
- }
- return api.WorkflowStep{Order: &order}
- }
-
- var tree func(ends []string, done map[string]bool) (api.WorkflowStep, error)
- tree = func(ends []string, done map[string]bool) (api.WorkflowStep, error) {
- groups := [][]string{}
- for _, e := range ends {
- merged := []string{e}
- kept := [][]string{}
- for _, g := range groups {
- if slices.ContainsFunc(g, func(other string) bool { return shareAny(closure(e, done), closure(other, done)) }) {
- merged = append(g, merged...)
- continue
- }
- kept = append(kept, g)
- }
- groups = append(kept, merged)
- }
- if len(groups) > 1 {
- branches := []api.WorkflowStep{}
- for _, g := range groups {
- b, err := tree(g, done)
- if err != nil {
- return api.WorkflowStep{}, err
- }
- branches = append(branches, b)
- }
- return api.WorkflowStep{Parallel: &branches}, nil
- }
-
- if len(ends) > 1 {
- common := closure(ends[0], done)
- for _, e := range ends[1:] {
- c := closure(e, done)
- for n := range common {
- if !c[n] {
- delete(common, n)
- }
- }
- }
- heads := []string{}
- for n := range common {
- later := false
- for m := range common {
- later = later || (m != n && closure(m, done)[n])
- }
- if !later {
- heads = append(heads, n)
- }
- }
- if len(heads) == 0 {
- return api.WorkflowStep{}, fmt.Errorf("stages %v overlap in earlier stages without sharing all of them, which steps can't express", ends)
- }
- slices.Sort(heads)
- prefix, err := tree(heads, done)
- if err != nil {
- return api.WorkflowStep{}, err
- }
- next := maps.Clone(done)
- for n := range common {
- next[n] = true
- }
- rest, err := tree(ends, next)
- if err != nil {
- return api.WorkflowStep{}, err
- }
- return sequence(prefix, rest), nil
- }
-
- s := byName[ends[0]]
- stage := api.WorkflowStep{Stage: s.Name, Model: s.Model, Prompt: s.Prompt, Agents: s.Agents}
- preds := pending(after[s.Name], done)
- if len(preds) == 0 {
- return stage, nil
- }
- before, err := tree(preds, done)
- if err != nil {
- return api.WorkflowStep{}, err
- }
- return sequence(before, stage), nil
- }
-
- finals := []string{}
- for _, s := range old.Stages {
- next := false
- for _, a := range after {
- next = next || slices.Contains(a, s.Name)
- }
- if !next {
- finals = append(finals, s.Name)
- }
- }
- top, err := tree(finals, map[string]bool{})
- if err != nil {
- return api.WorkflowDefinition{}, err
- }
- steps := []api.WorkflowStep{top}
- if top.Order != nil {
- steps = *top.Order
- }
- def := api.WorkflowDefinition{Description: old.Description, Steps: steps}
-
- if len(workflow.Stages(def)) != len(old.Stages) {
- return api.WorkflowDefinition{}, fmt.Errorf("converted to %d stages from %d", len(workflow.Stages(def)), len(old.Stages))
- }
- for _, s := range old.Stages {
- got := workflow.After(def, s.Name)
- want := after[s.Name]
- if !slices.Equal(sorted(got), sorted(want)) {
- return api.WorkflowDefinition{}, fmt.Errorf("stage %q converted to come after %v, not %v", s.Name, got, want)
- }
- }
- return def, nil
-}
-
-func shareAny(a, b map[string]bool) bool {
- for n := range a {
- if b[n] {
- return true
- }
- }
- return false
-}
-
-func sorted(names []string) []string {
- out := slices.Clone(names)
- slices.Sort(out)
- return out
-}
diff --git a/internal/store/migrations/001-create-core-tables.sql b/internal/store/migrations/001-create-core-tables.sql
deleted file mode 100644
index 2254ec2..0000000
--- a/internal/store/migrations/001-create-core-tables.sql
+++ /dev/null
@@ -1,42 +0,0 @@
-CREATE TABLE IF NOT EXISTS users (
- login TEXT PRIMARY KEY,
- display_name TEXT NOT NULL,
- git_name TEXT NOT NULL DEFAULT '',
- git_email TEXT NOT NULL DEFAULT '',
- created_at TEXT NOT NULL
-);
-
-CREATE TABLE IF NOT EXISTS projects (
- id TEXT PRIMARY KEY,
- name TEXT NOT NULL UNIQUE,
- repo_path TEXT NOT NULL UNIQUE,
- default_branch TEXT NOT NULL,
- remote TEXT NOT NULL,
- created_by TEXT NOT NULL REFERENCES users(login),
- created_at TEXT NOT NULL
-);
-
-CREATE TABLE IF NOT EXISTS tasks (
- id TEXT PRIMARY KEY,
- project_id TEXT NOT NULL REFERENCES projects(id),
- title TEXT NOT NULL,
- branch TEXT NOT NULL,
- worktree_path TEXT NOT NULL,
- base_ref TEXT NOT NULL,
- status TEXT NOT NULL,
- created_by TEXT NOT NULL REFERENCES users(login),
- created_at TEXT NOT NULL,
- completed_at TEXT
-);
-
-CREATE INDEX IF NOT EXISTS tasks_project ON tasks(project_id, created_at);
-
-CREATE TABLE IF NOT EXISTS events (
- seq INTEGER PRIMARY KEY AUTOINCREMENT,
- kind TEXT NOT NULL,
- project_id TEXT,
- task_id TEXT,
- actor TEXT NOT NULL,
- payload TEXT NOT NULL,
- created_at TEXT NOT NULL
-);
diff --git a/internal/store/migrations/002-add-agent-sessions.sql b/internal/store/migrations/002-add-agent-sessions.sql
deleted file mode 100644
index fcb1708..0000000
--- a/internal/store/migrations/002-add-agent-sessions.sql
+++ /dev/null
@@ -1,16 +0,0 @@
-ALTER TABLE events ADD COLUMN agent_session_id TEXT;
-
-CREATE TABLE agent_sessions (
- id TEXT PRIMARY KEY,
- task_id TEXT NOT NULL REFERENCES tasks(id),
- vendor TEXT NOT NULL,
- model TEXT NOT NULL,
- vendor_session_id TEXT NOT NULL,
- state TEXT NOT NULL,
- role TEXT NOT NULL,
- created_by TEXT NOT NULL REFERENCES users(login),
- created_at TEXT NOT NULL
-);
-
-CREATE INDEX agent_sessions_task ON agent_sessions(task_id, created_at);
-CREATE INDEX agent_sessions_state ON agent_sessions(state);
diff --git a/internal/store/migrations/003-add-inbox.sql b/internal/store/migrations/003-add-inbox.sql
deleted file mode 100644
index 2cb0997..0000000
--- a/internal/store/migrations/003-add-inbox.sql
+++ /dev/null
@@ -1,34 +0,0 @@
-CREATE TABLE inbox_items (
- id TEXT PRIMARY KEY,
- project_id TEXT NOT NULL REFERENCES projects(id),
- task_id TEXT NOT NULL REFERENCES tasks(id),
- agent_session_id TEXT NOT NULL REFERENCES agent_sessions(id),
- thread_id TEXT NOT NULL,
- kind TEXT NOT NULL,
- priority INTEGER NOT NULL,
- blocking INTEGER NOT NULL,
- payload TEXT NOT NULL,
- state TEXT NOT NULL,
- snoozed_until TEXT,
- created_at TEXT NOT NULL,
- resolved_by TEXT REFERENCES users(login),
- resolved_at TEXT
-);
-
-CREATE INDEX inbox_items_open ON inbox_items(state, priority, created_at);
-CREATE INDEX inbox_items_session ON inbox_items(agent_session_id, state);
-
-CREATE TABLE inbox_responses (
- id TEXT PRIMARY KEY,
- item_id TEXT NOT NULL REFERENCES inbox_items(id),
- user_login TEXT NOT NULL REFERENCES users(login),
- type TEXT NOT NULL,
- option TEXT,
- text TEXT,
- attachments TEXT NOT NULL,
- snooze_until TEXT,
- created_at TEXT NOT NULL,
- delivered_at TEXT
-);
-
-CREATE INDEX inbox_responses_item ON inbox_responses(item_id, created_at);
diff --git a/internal/store/migrations/004-index-events-by-task.sql b/internal/store/migrations/004-index-events-by-task.sql
deleted file mode 100644
index 73a1c9f..0000000
--- a/internal/store/migrations/004-index-events-by-task.sql
+++ /dev/null
@@ -1 +0,0 @@
-CREATE INDEX events_task ON events(task_id, seq);
diff --git a/internal/store/migrations/005-add-turn-queue.sql b/internal/store/migrations/005-add-turn-queue.sql
deleted file mode 100644
index 3de0b16..0000000
--- a/internal/store/migrations/005-add-turn-queue.sql
+++ /dev/null
@@ -1,6 +0,0 @@
-CREATE TABLE turn_queue (
- session_id TEXT PRIMARY KEY REFERENCES agent_sessions(id),
- actor TEXT NOT NULL,
- text TEXT NOT NULL,
- created_at TEXT NOT NULL
-);
diff --git a/internal/store/migrations/006-merge-response-types-into-other.sql b/internal/store/migrations/006-merge-response-types-into-other.sql
deleted file mode 100644
index 28894da..0000000
--- a/internal/store/migrations/006-merge-response-types-into-other.sql
+++ /dev/null
@@ -1,2 +0,0 @@
-UPDATE inbox_responses SET type = 'other' WHERE type IN ('free_form', 'redirect', 'question');
-UPDATE inbox_items SET state = 'answered' WHERE state = 'redirected';
diff --git a/internal/store/migrations/007-flatten-report-payloads-to-text.sql b/internal/store/migrations/007-flatten-report-payloads-to-text.sql
deleted file mode 100644
index 179c990..0000000
--- a/internal/store/migrations/007-flatten-report-payloads-to-text.sql
+++ /dev/null
@@ -1,2 +0,0 @@
-UPDATE inbox_items SET payload = json_object('text', coalesce((SELECT group_concat('- ' || value, char(10)) FROM json_each(inbox_items.payload, '$.done')), ''))
-WHERE kind = 'report' AND json_type(payload, '$.done') IS NOT NULL;
diff --git a/internal/store/migrations/008-rename-completed-tasks-to-discarded.sql b/internal/store/migrations/008-rename-completed-tasks-to-discarded.sql
deleted file mode 100644
index 067b8c3..0000000
--- a/internal/store/migrations/008-rename-completed-tasks-to-discarded.sql
+++ /dev/null
@@ -1,3 +0,0 @@
-ALTER TABLE tasks RENAME COLUMN completed_at TO closed_at;
-ALTER TABLE tasks ADD COLUMN merge_commit TEXT NOT NULL DEFAULT '';
-UPDATE tasks SET status = 'discarded' WHERE status = 'completed';
diff --git a/internal/store/migrations/009-dismiss-inbox-of-closed-tasks.sql b/internal/store/migrations/009-dismiss-inbox-of-closed-tasks.sql
deleted file mode 100644
index f565e2e..0000000
--- a/internal/store/migrations/009-dismiss-inbox-of-closed-tasks.sql
+++ /dev/null
@@ -1,2 +0,0 @@
-UPDATE inbox_items SET state = 'dismissed', resolved_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now'), snoozed_until = NULL
-WHERE state = 'open' AND task_id IN (SELECT id FROM tasks WHERE status != 'active');
diff --git a/internal/store/migrations/010-add-blobs-and-queued-images.sql b/internal/store/migrations/010-add-blobs-and-queued-images.sql
deleted file mode 100644
index 54b615c..0000000
--- a/internal/store/migrations/010-add-blobs-and-queued-images.sql
+++ /dev/null
@@ -1,18 +0,0 @@
-CREATE TABLE IF NOT EXISTS blobs (
- id TEXT PRIMARY KEY,
- content_type TEXT NOT NULL,
- path TEXT NOT NULL,
- created_by TEXT NOT NULL REFERENCES users(login),
- created_at TEXT NOT NULL
-);
-
-CREATE TABLE turn_queue_next (
- session_id TEXT PRIMARY KEY REFERENCES agent_sessions(id),
- actor TEXT NOT NULL,
- text TEXT NOT NULL,
- images TEXT NOT NULL DEFAULT '[]',
- created_at TEXT NOT NULL
-);
-INSERT INTO turn_queue_next (session_id, actor, text, created_at) SELECT session_id, actor, text, created_at FROM turn_queue;
-DROP TABLE turn_queue;
-ALTER TABLE turn_queue_next RENAME TO turn_queue;
diff --git a/internal/store/migrations/011-add-task-merge-title.sql b/internal/store/migrations/011-add-task-merge-title.sql
deleted file mode 100644
index 1bee9cd..0000000
--- a/internal/store/migrations/011-add-task-merge-title.sql
+++ /dev/null
@@ -1 +0,0 @@
-ALTER TABLE tasks ADD COLUMN merge_title TEXT NOT NULL DEFAULT '';
diff --git a/internal/store/migrations/012-add-web-push.sql b/internal/store/migrations/012-add-web-push.sql
deleted file mode 100644
index e638f18..0000000
--- a/internal/store/migrations/012-add-web-push.sql
+++ /dev/null
@@ -1,17 +0,0 @@
-ALTER TABLE users ADD COLUMN notification_prefs TEXT NOT NULL
- DEFAULT '{"blocking_questions":true,"questions":false,"reports":false,"errors":false,"rate_limits":false}';
-
-CREATE TABLE vapid_keys (
- id INTEGER PRIMARY KEY CHECK (id = 1),
- public_key TEXT NOT NULL,
- private_key TEXT NOT NULL
-);
-
-CREATE TABLE push_subscriptions (
- endpoint TEXT PRIMARY KEY,
- user_login TEXT NOT NULL REFERENCES users(login),
- p256dh TEXT NOT NULL,
- auth TEXT NOT NULL,
- subscriber TEXT NOT NULL,
- created_at TEXT NOT NULL
-);
diff --git a/internal/store/migrations/013-add-snapshots.sql b/internal/store/migrations/013-add-snapshots.sql
deleted file mode 100644
index c2f8532..0000000
--- a/internal/store/migrations/013-add-snapshots.sql
+++ /dev/null
@@ -1,12 +0,0 @@
-CREATE TABLE snapshots (
- id TEXT PRIMARY KEY,
- task_id TEXT NOT NULL REFERENCES tasks(id),
- agent_session_id TEXT NOT NULL REFERENCES agent_sessions(id),
- n INTEGER NOT NULL,
- commit_sha TEXT NOT NULL,
- tree_sha TEXT NOT NULL,
- event_seq INTEGER NOT NULL,
- trigger TEXT NOT NULL,
- created_at TEXT NOT NULL,
- UNIQUE (task_id, n)
-);
diff --git a/internal/store/migrations/014-inline-question-choices.sql b/internal/store/migrations/014-inline-question-choices.sql
deleted file mode 100644
index 30ebf1a..0000000
--- a/internal/store/migrations/014-inline-question-choices.sql
+++ /dev/null
@@ -1,11 +0,0 @@
-ALTER TABLE inbox_responses ADD COLUMN options TEXT;
-UPDATE inbox_responses SET options = json_array(option) WHERE option IS NOT NULL;
-ALTER TABLE inbox_responses DROP COLUMN option;
-
-UPDATE inbox_items SET payload = json_set(json_remove(payload, '$.recommended_option'),
- '$.context', json_extract(payload, '$.context') || char(10) || char(10) || (
- SELECT group_concat(CASE WHEN o.value = json_extract(inbox_items.payload, '$.recommended_option') THEN '- (*) ' ELSE '- ( ) ' END || o.value, char(10))
- FROM json_each(inbox_items.payload, '$.options') o),
- '$.multiple', json('false'),
- '$.recommended', json_array(json_extract(payload, '$.recommended_option')))
-WHERE kind = 'question' AND json_type(payload, '$.recommended_option') IS NOT NULL;
diff --git a/internal/store/migrations/015-unsnooze-inbox-items.sql b/internal/store/migrations/015-unsnooze-inbox-items.sql
deleted file mode 100644
index a8d10e3..0000000
--- a/internal/store/migrations/015-unsnooze-inbox-items.sql
+++ /dev/null
@@ -1 +0,0 @@
-UPDATE inbox_items SET snoozed_until = NULL WHERE snoozed_until IS NOT NULL;
diff --git a/internal/store/migrations/016-add-task-todo-item.sql b/internal/store/migrations/016-add-task-todo-item.sql
deleted file mode 100644
index 3c02865..0000000
--- a/internal/store/migrations/016-add-task-todo-item.sql
+++ /dev/null
@@ -1 +0,0 @@
-ALTER TABLE tasks ADD COLUMN todo_item TEXT NOT NULL DEFAULT '';
diff --git a/internal/store/migrations/017-add-lead-agents.sql b/internal/store/migrations/017-add-lead-agents.sql
deleted file mode 100644
index 63b9396..0000000
--- a/internal/store/migrations/017-add-lead-agents.sql
+++ /dev/null
@@ -1,35 +0,0 @@
-CREATE TABLE agent_sessions_next (
- id TEXT PRIMARY KEY,
- task_id TEXT REFERENCES tasks(id),
- owner TEXT NOT NULL DEFAULT '',
- vendor TEXT NOT NULL,
- model TEXT NOT NULL,
- vendor_session_id TEXT NOT NULL,
- state TEXT NOT NULL,
- role TEXT NOT NULL,
- created_by TEXT NOT NULL REFERENCES users(login),
- created_at TEXT NOT NULL
-);
-INSERT INTO agent_sessions_next (id, task_id, vendor, model, vendor_session_id, state, role, created_by, created_at)
- SELECT id, task_id, vendor, model, vendor_session_id, state, role, created_by, created_at FROM agent_sessions;
-DROP TABLE agent_sessions;
-ALTER TABLE agent_sessions_next RENAME TO agent_sessions;
-CREATE INDEX agent_sessions_task ON agent_sessions(task_id, created_at);
-CREATE INDEX agent_sessions_state ON agent_sessions(state);
-CREATE INDEX agent_sessions_owner ON agent_sessions(owner, created_at);
-
-ALTER TABLE events ADD COLUMN owner TEXT;
-CREATE INDEX events_owner ON events(owner, seq) WHERE owner IS NOT NULL;
-
-ALTER TABLE users ADD COLUMN focus TEXT NOT NULL DEFAULT '{"tabs":["transcript"],"tab":"transcript"}';
-
-CREATE TABLE lead_inputs (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- owner TEXT NOT NULL REFERENCES users(login),
- kind TEXT NOT NULL,
- actor TEXT NOT NULL,
- text TEXT NOT NULL,
- images TEXT NOT NULL DEFAULT '[]',
- created_at TEXT NOT NULL
-);
-CREATE INDEX lead_inputs_owner ON lead_inputs(owner, id);
diff --git a/internal/store/migrations/018-add-focus-project.sql b/internal/store/migrations/018-add-focus-project.sql
deleted file mode 100644
index 0583d64..0000000
--- a/internal/store/migrations/018-add-focus-project.sql
+++ /dev/null
@@ -1,3 +0,0 @@
-UPDATE users SET focus = json_set(focus, '$.project_id', (SELECT project_id FROM tasks WHERE id = json_extract(users.focus, '$.task_id')))
-WHERE json_extract(focus, '$.task_id') IS NOT NULL;
-DELETE FROM lead_inputs WHERE text LIKE 'The human switched the right pane %' OR text = 'The human cleared the right pane.';
diff --git a/internal/store/migrations/019-add-task-merging-by.sql b/internal/store/migrations/019-add-task-merging-by.sql
deleted file mode 100644
index 0db1fc2..0000000
--- a/internal/store/migrations/019-add-task-merging-by.sql
+++ /dev/null
@@ -1 +0,0 @@
-ALTER TABLE tasks ADD COLUMN merging_by TEXT NOT NULL DEFAULT '';
diff --git a/internal/store/migrations/020-flag-handoff-prompts.sql b/internal/store/migrations/020-flag-handoff-prompts.sql
deleted file mode 100644
index 1b14a06..0000000
--- a/internal/store/migrations/020-flag-handoff-prompts.sql
+++ /dev/null
@@ -1,2 +0,0 @@
-UPDATE events SET payload = json_set(payload, '$.handoff', json('true'))
-WHERE kind = 'user.message' AND actor = 'byre' AND json_extract(payload, '$.text') LIKE 'Write a handoff for a fresh %';
diff --git a/internal/store/migrations/021-rename-todo-tab-to-project.sql b/internal/store/migrations/021-rename-todo-tab-to-project.sql
deleted file mode 100644
index 984ff13..0000000
--- a/internal/store/migrations/021-rename-todo-tab-to-project.sql
+++ /dev/null
@@ -1,3 +0,0 @@
-UPDATE users SET focus = json_set(focus,
- '$.tab', CASE json_extract(focus, '$.tab') WHEN 'todo' THEN 'project' ELSE json_extract(focus, '$.tab') END,
- '$.tabs', json((SELECT json_group_array(CASE value WHEN 'todo' THEN 'project' ELSE value END) FROM json_each(users.focus, '$.tabs'))));
diff --git a/internal/store/migrations/022-add-snapshot-forks.sql b/internal/store/migrations/022-add-snapshot-forks.sql
deleted file mode 100644
index 7bf250b..0000000
--- a/internal/store/migrations/022-add-snapshot-forks.sql
+++ /dev/null
@@ -1,4 +0,0 @@
-ALTER TABLE tasks ADD COLUMN forked_from TEXT NOT NULL DEFAULT '';
-ALTER TABLE tasks ADD COLUMN forked_at INTEGER NOT NULL DEFAULT 0;
-ALTER TABLE agent_sessions ADD COLUMN forked_from TEXT NOT NULL DEFAULT '';
-ALTER TABLE agent_sessions ADD COLUMN forked_at TEXT NOT NULL DEFAULT '';
diff --git a/internal/store/migrations/023-add-task-port-base.sql b/internal/store/migrations/023-add-task-port-base.sql
deleted file mode 100644
index 7205592..0000000
--- a/internal/store/migrations/023-add-task-port-base.sql
+++ /dev/null
@@ -1,5 +0,0 @@
-ALTER TABLE tasks ADD COLUMN port_base INTEGER NOT NULL DEFAULT 0;
-UPDATE tasks SET port_base = (
- SELECT 41000 + 10 * (count(*) - 1) FROM tasks AS earlier
- WHERE earlier.status = 'active' AND (earlier.created_at < tasks.created_at OR (earlier.created_at = tasks.created_at AND earlier.id <= tasks.id))
-) WHERE status = 'active' AND port_base = 0;
diff --git a/internal/store/migrations/024-add-task-merge-queue.sql b/internal/store/migrations/024-add-task-merge-queue.sql
deleted file mode 100644
index 683e085..0000000
--- a/internal/store/migrations/024-add-task-merge-queue.sql
+++ /dev/null
@@ -1,2 +0,0 @@
-ALTER TABLE tasks ADD COLUMN merge_queued_by TEXT NOT NULL DEFAULT '';
-ALTER TABLE tasks ADD COLUMN merge_queued_at TEXT NOT NULL DEFAULT '';
diff --git a/internal/store/migrations/025-add-task-previews.sql b/internal/store/migrations/025-add-task-previews.sql
deleted file mode 100644
index 3b8aa7b..0000000
--- a/internal/store/migrations/025-add-task-previews.sql
+++ /dev/null
@@ -1,8 +0,0 @@
-CREATE TABLE previews (
- task_id TEXT NOT NULL REFERENCES tasks(id),
- name TEXT NOT NULL,
- port INTEGER NOT NULL,
- created_at TEXT NOT NULL,
- PRIMARY KEY (task_id, name),
- UNIQUE (task_id, port)
-);
diff --git a/internal/store/migrations/026-add-task-color.sql b/internal/store/migrations/026-add-task-color.sql
deleted file mode 100644
index 3bae440..0000000
--- a/internal/store/migrations/026-add-task-color.sql
+++ /dev/null
@@ -1,5 +0,0 @@
-ALTER TABLE tasks ADD COLUMN color INTEGER NOT NULL DEFAULT 0;
-UPDATE tasks SET color = (SELECT count(*) FROM tasks AS earlier WHERE earlier.rowid < tasks.rowid) % 11;
-UPDATE tasks SET color = (
- SELECT count(*) FROM tasks AS earlier WHERE earlier.status = 'active' AND earlier.rowid < tasks.rowid
-) % 11 WHERE status = 'active';
diff --git a/internal/store/migrations/027-drop-turn-queue.sql b/internal/store/migrations/027-drop-turn-queue.sql
deleted file mode 100644
index ca9bb39..0000000
--- a/internal/store/migrations/027-drop-turn-queue.sql
+++ /dev/null
@@ -1,2 +0,0 @@
-UPDATE agent_sessions SET state = 'idle' WHERE state = 'queued';
-DROP TABLE turn_queue;
diff --git a/internal/store/migrations/028-add-snapshot-reviews.sql b/internal/store/migrations/028-add-snapshot-reviews.sql
deleted file mode 100644
index fa195e8..0000000
--- a/internal/store/migrations/028-add-snapshot-reviews.sql
+++ /dev/null
@@ -1,15 +0,0 @@
-CREATE TABLE reviews (
- id TEXT PRIMARY KEY,
- task_id TEXT NOT NULL REFERENCES tasks(id),
- snapshot_n INTEGER NOT NULL,
- agent_session_id TEXT NOT NULL REFERENCES agent_sessions(id),
- vendor TEXT NOT NULL,
- model TEXT NOT NULL,
- state TEXT NOT NULL,
- result TEXT NOT NULL DEFAULT '',
- error TEXT NOT NULL DEFAULT '',
- created_at TEXT NOT NULL,
- finished_at TEXT NOT NULL DEFAULT '',
- UNIQUE (task_id, snapshot_n)
-);
-CREATE UNIQUE INDEX reviews_session ON reviews(agent_session_id);
diff --git a/internal/store/migrations/029-add-task-inputs.sql b/internal/store/migrations/029-add-task-inputs.sql
deleted file mode 100644
index f64d2d0..0000000
--- a/internal/store/migrations/029-add-task-inputs.sql
+++ /dev/null
@@ -1,9 +0,0 @@
-CREATE TABLE task_inputs (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- task_id TEXT NOT NULL REFERENCES tasks(id),
- actor TEXT NOT NULL,
- text TEXT NOT NULL,
- images TEXT NOT NULL DEFAULT '[]',
- created_at TEXT NOT NULL
-);
-CREATE INDEX task_inputs_task ON task_inputs(task_id, id);
diff --git a/internal/store/migrations/030-drop-web-push.sql b/internal/store/migrations/030-drop-web-push.sql
deleted file mode 100644
index a56d3ce..0000000
--- a/internal/store/migrations/030-drop-web-push.sql
+++ /dev/null
@@ -1,2 +0,0 @@
-DROP TABLE push_subscriptions;
-DROP TABLE vapid_keys;
diff --git a/internal/store/migrations/031-delete-report-inbox-items.sql b/internal/store/migrations/031-delete-report-inbox-items.sql
deleted file mode 100644
index 33ad328..0000000
--- a/internal/store/migrations/031-delete-report-inbox-items.sql
+++ /dev/null
@@ -1,2 +0,0 @@
-DELETE FROM inbox_responses WHERE item_id IN (SELECT id FROM inbox_items WHERE kind = 'report');
-DELETE FROM inbox_items WHERE kind = 'report';
diff --git a/internal/store/migrations/032-allow-sessionless-reviews.sql b/internal/store/migrations/032-allow-sessionless-reviews.sql
deleted file mode 100644
index 2fba338..0000000
--- a/internal/store/migrations/032-allow-sessionless-reviews.sql
+++ /dev/null
@@ -1,18 +0,0 @@
-CREATE TABLE reviews_new (
- id TEXT PRIMARY KEY,
- task_id TEXT NOT NULL REFERENCES tasks(id),
- snapshot_n INTEGER NOT NULL,
- agent_session_id TEXT REFERENCES agent_sessions(id),
- vendor TEXT NOT NULL,
- model TEXT NOT NULL,
- state TEXT NOT NULL,
- result TEXT NOT NULL DEFAULT '',
- error TEXT NOT NULL DEFAULT '',
- created_at TEXT NOT NULL,
- finished_at TEXT NOT NULL DEFAULT '',
- UNIQUE (task_id, snapshot_n)
-);
-INSERT INTO reviews_new SELECT * FROM reviews;
-DROP TABLE reviews;
-ALTER TABLE reviews_new RENAME TO reviews;
-CREATE UNIQUE INDEX reviews_session ON reviews(agent_session_id);
diff --git a/internal/store/migrations/033-add-workflows.sql b/internal/store/migrations/033-add-workflows.sql
deleted file mode 100644
index a40d4cf..0000000
--- a/internal/store/migrations/033-add-workflows.sql
+++ /dev/null
@@ -1,6 +0,0 @@
-CREATE TABLE workflows (
- name TEXT PRIMARY KEY,
- definition TEXT NOT NULL
-);
-ALTER TABLE tasks ADD COLUMN workflow_name TEXT NOT NULL DEFAULT '';
-ALTER TABLE tasks ADD COLUMN workflow_definition TEXT NOT NULL DEFAULT '';
diff --git a/internal/store/migrations/034-run-workflow-stages.sql b/internal/store/migrations/034-run-workflow-stages.sql
deleted file mode 100644
index 8e4de86..0000000
--- a/internal/store/migrations/034-run-workflow-stages.sql
+++ /dev/null
@@ -1,9 +0,0 @@
-ALTER TABLE tasks ADD COLUMN workflow_brief TEXT NOT NULL DEFAULT '';
-ALTER TABLE agent_sessions ADD COLUMN workflow_stage INTEGER NOT NULL DEFAULT 0;
-ALTER TABLE agent_sessions ADD COLUMN stage_output TEXT NOT NULL DEFAULT '';
-ALTER TABLE agent_sessions ADD COLUMN stage_finished INTEGER NOT NULL DEFAULT 0;
-UPDATE workflows SET definition = json_set(definition, '$.stages',
- (SELECT json_group_array(json_remove(value, '$.vendor')) FROM (SELECT value FROM json_each(definition, '$.stages') ORDER BY key)));
-UPDATE tasks SET workflow_definition = json_set(workflow_definition, '$.stages',
- (SELECT json_group_array(json_remove(value, '$.vendor')) FROM (SELECT value FROM json_each(workflow_definition, '$.stages') ORDER BY key)))
- WHERE workflow_definition != '';
diff --git a/internal/store/migrations/035-workflow-stage-tasks.sql b/internal/store/migrations/035-workflow-stage-tasks.sql
deleted file mode 100644
index 111f6a0..0000000
--- a/internal/store/migrations/035-workflow-stage-tasks.sql
+++ /dev/null
@@ -1,11 +0,0 @@
-ALTER TABLE tasks ADD COLUMN workflow_run TEXT NOT NULL DEFAULT '';
-ALTER TABLE tasks ADD COLUMN workflow_stage TEXT NOT NULL DEFAULT '';
-ALTER TABLE tasks ADD COLUMN workflow_title TEXT NOT NULL DEFAULT '';
-ALTER TABLE tasks ADD COLUMN stage_output TEXT NOT NULL DEFAULT '';
-ALTER TABLE tasks ADD COLUMN stage_commit TEXT NOT NULL DEFAULT '';
-UPDATE tasks SET workflow_run = id, workflow_title = title,
- workflow_stage = coalesce(
- (SELECT json_extract(tasks.workflow_definition, '$.stages[' || (s.workflow_stage - 1) || '].name') FROM agent_sessions s
- WHERE s.task_id = tasks.id AND s.workflow_stage > 0 ORDER BY s.created_at DESC LIMIT 1),
- json_extract(workflow_definition, '$.stages[0].name'))
- WHERE workflow_name != '' AND workflow_run = '';
diff --git a/internal/store/migrations/036-drop-preview-tab.sql b/internal/store/migrations/036-drop-preview-tab.sql
deleted file mode 100644
index ba05afd..0000000
--- a/internal/store/migrations/036-drop-preview-tab.sql
+++ /dev/null
@@ -1,4 +0,0 @@
-UPDATE users SET focus = json_set(focus,
- '$.tab', CASE json_extract(focus, '$.tab') WHEN 'preview' THEN 'transcript' ELSE json_extract(focus, '$.tab') END,
- '$.tabs', json((SELECT json_group_array(value) FROM json_each(users.focus, '$.tabs') WHERE value != 'preview')));
-UPDATE users SET focus = json_set(focus, '$.tabs', json('["transcript"]')) WHERE json_array_length(focus, '$.tabs') = 0;
diff --git a/internal/store/migrations/037-drop-inbox-priority.sql b/internal/store/migrations/037-drop-inbox-priority.sql
deleted file mode 100644
index c8e16c5..0000000
--- a/internal/store/migrations/037-drop-inbox-priority.sql
+++ /dev/null
@@ -1,38 +0,0 @@
-CREATE TABLE inbox_items_next (
- id TEXT PRIMARY KEY,
- project_id TEXT NOT NULL REFERENCES projects(id),
- task_id TEXT NOT NULL REFERENCES tasks(id),
- agent_session_id TEXT NOT NULL REFERENCES agent_sessions(id),
- thread_id TEXT NOT NULL,
- kind TEXT NOT NULL,
- priority INTEGER NOT NULL DEFAULT 0,
- blocking INTEGER NOT NULL DEFAULT 1,
- payload TEXT NOT NULL,
- state TEXT NOT NULL,
- snoozed_until TEXT,
- created_at TEXT NOT NULL,
- resolved_by TEXT REFERENCES users(login),
- resolved_at TEXT
-);
-INSERT INTO inbox_items_next SELECT id, project_id, task_id, agent_session_id, thread_id, kind, priority, blocking, payload, state, snoozed_until, created_at, resolved_by, resolved_at FROM inbox_items;
-DROP TABLE inbox_items;
-ALTER TABLE inbox_items_next RENAME TO inbox_items;
-CREATE INDEX inbox_items_open ON inbox_items(state, created_at);
-CREATE INDEX inbox_items_session ON inbox_items(agent_session_id, state);
-
-CREATE TABLE users_next (
- login TEXT PRIMARY KEY,
- display_name TEXT NOT NULL,
- git_name TEXT NOT NULL DEFAULT '',
- git_email TEXT NOT NULL DEFAULT '',
- created_at TEXT NOT NULL,
- notification_prefs TEXT NOT NULL DEFAULT '{"questions":true,"reports":false,"errors":false,"rate_limits":false}',
- focus TEXT NOT NULL DEFAULT '{"tabs":["transcript"],"tab":"transcript"}'
-);
-INSERT INTO users_next (login, display_name, git_name, git_email, created_at, notification_prefs, focus)
- SELECT login, display_name, git_name, git_email, created_at,
- json_remove(json_set(notification_prefs, '$.questions', json(CASE WHEN json_extract(notification_prefs, '$.blocking_questions') THEN 'true' ELSE 'false' END)), '$.blocking_questions'),
- focus
- FROM users;
-DROP TABLE users;
-ALTER TABLE users_next RENAME TO users;
diff --git a/internal/store/migrations/038-add-task-summary-and-discarding-by.sql b/internal/store/migrations/038-add-task-summary-and-discarding-by.sql
deleted file mode 100644
index f980cc6..0000000
--- a/internal/store/migrations/038-add-task-summary-and-discarding-by.sql
+++ /dev/null
@@ -1,2 +0,0 @@
-ALTER TABLE tasks ADD COLUMN summary TEXT NOT NULL DEFAULT '';
-ALTER TABLE tasks ADD COLUMN discarding_by TEXT NOT NULL DEFAULT '';
diff --git a/internal/store/migrations/039-add-task-discard-state.sql b/internal/store/migrations/039-add-task-discard-state.sql
deleted file mode 100644
index e7c939e..0000000
--- a/internal/store/migrations/039-add-task-discard-state.sql
+++ /dev/null
@@ -1,3 +0,0 @@
-ALTER TABLE tasks ADD COLUMN discard_state TEXT NOT NULL DEFAULT '';
-ALTER TABLE tasks ADD COLUMN discard_session TEXT NOT NULL DEFAULT '';
-UPDATE tasks SET discard_state = 'held' WHERE discarding_by != '' AND status = 'active';
diff --git a/internal/store/migrations/040-add-subtasks.sql b/internal/store/migrations/040-add-subtasks.sql
deleted file mode 100644
index 2c40e52..0000000
--- a/internal/store/migrations/040-add-subtasks.sql
+++ /dev/null
@@ -1,3 +0,0 @@
-ALTER TABLE tasks ADD COLUMN parent_task_id TEXT NOT NULL DEFAULT '';
-ALTER TABLE tasks ADD COLUMN parent_merge_by TEXT NOT NULL DEFAULT '';
-CREATE INDEX tasks_parent ON tasks(parent_task_id) WHERE parent_task_id != '';
diff --git a/internal/store/migrations/041-add-review-finding-states.sql b/internal/store/migrations/041-add-review-finding-states.sql
deleted file mode 100644
index 62a91ee..0000000
--- a/internal/store/migrations/041-add-review-finding-states.sql
+++ /dev/null
@@ -1,3 +0,0 @@
-ALTER TABLE reviews ADD COLUMN finding_states TEXT NOT NULL DEFAULT '{}';
-UPDATE events SET payload = json_set(payload, '$.finding_states', json('{}'))
-WHERE kind IN ('review.started', 'review.finished') AND json_type(payload, '$.finding_states') IS NULL;
diff --git a/internal/store/migrations/042-add-lead-notes.sql b/internal/store/migrations/042-add-lead-notes.sql
deleted file mode 100644
index 0df4f04..0000000
--- a/internal/store/migrations/042-add-lead-notes.sql
+++ /dev/null
@@ -1,8 +0,0 @@
-CREATE TABLE lead_notes (
- id TEXT PRIMARY KEY,
- owner TEXT NOT NULL REFERENCES users(login),
- text TEXT NOT NULL,
- created_at TEXT NOT NULL,
- updated_at TEXT NOT NULL
-);
-CREATE INDEX lead_notes_owner ON lead_notes(owner, created_at);
diff --git a/internal/store/migrations/043-add-summary-reads.sql b/internal/store/migrations/043-add-summary-reads.sql
deleted file mode 100644
index d170706..0000000
--- a/internal/store/migrations/043-add-summary-reads.sql
+++ /dev/null
@@ -1,7 +0,0 @@
-CREATE INDEX events_summary ON events(task_id, seq) WHERE kind = 'turn.ended' AND json_extract(payload, '$.summary') = 1;
-CREATE TABLE summary_reads (
- login TEXT NOT NULL REFERENCES users(login),
- task_id TEXT NOT NULL REFERENCES tasks(id),
- seq INTEGER NOT NULL,
- PRIMARY KEY (login, task_id)
-);
diff --git a/internal/store/migrations/044-add-task-finalizing.sql b/internal/store/migrations/044-add-task-finalizing.sql
deleted file mode 100644
index 8806546..0000000
--- a/internal/store/migrations/044-add-task-finalizing.sql
+++ /dev/null
@@ -1 +0,0 @@
-ALTER TABLE tasks ADD COLUMN finalizing TEXT NOT NULL DEFAULT '';
diff --git a/internal/store/migrations/045-add-session-subagents.sql b/internal/store/migrations/045-add-session-subagents.sql
deleted file mode 100644
index 67038a4..0000000
--- a/internal/store/migrations/045-add-session-subagents.sql
+++ /dev/null
@@ -1 +0,0 @@
-ALTER TABLE agent_sessions ADD COLUMN subagents INTEGER NOT NULL DEFAULT 0;
diff --git a/internal/store/migrations/046-add-task-state.sql b/internal/store/migrations/046-add-task-state.sql
deleted file mode 100644
index 1b90070..0000000
--- a/internal/store/migrations/046-add-task-state.sql
+++ /dev/null
@@ -1,19 +0,0 @@
-ALTER TABLE tasks ADD COLUMN state TEXT NOT NULL DEFAULT '';
-ALTER TABLE tasks ADD COLUMN review_requests INTEGER NOT NULL DEFAULT 0;
-UPDATE tasks SET state = CASE
- WHEN tasks.status != 'active' THEN tasks.status
- WHEN tasks.discard_state != '' THEN 'discarding'
- WHEN tasks.merging_by != '' OR tasks.finalizing != '' THEN 'merging'
- ELSE coalesce((
- SELECT CASE
- WHEN s.state != 'idle' THEN s.state
- WHEN s.subagents > 0 THEN 'running'
- WHEN EXISTS (SELECT 1 FROM reviews r WHERE r.task_id = tasks.id AND r.state = 'running') THEN 'reviewing'
- WHEN tasks.review_requests > 0 THEN 'review_pending'
- ELSE 'idle'
- END
- FROM agent_sessions s
- WHERE s.task_id = tasks.id AND s.role != 'reviewer' AND s.state != 'completed'
- ORDER BY s.state = 'running' DESC, s.subagents > 0 DESC, s.created_at DESC LIMIT 1
- ), 'no_agent')
-END;
diff --git a/internal/store/migrations/047-add-task-finalizing-commit.sql b/internal/store/migrations/047-add-task-finalizing-commit.sql
deleted file mode 100644
index 9d1a793..0000000
--- a/internal/store/migrations/047-add-task-finalizing-commit.sql
+++ /dev/null
@@ -1,3 +0,0 @@
-ALTER TABLE tasks ADD COLUMN finalizing_commit TEXT NOT NULL DEFAULT '';
-ALTER TABLE tasks ADD COLUMN finalizing_notes TEXT NOT NULL DEFAULT '';
-ALTER TABLE tasks ADD COLUMN finalizing_unchanged INTEGER NOT NULL DEFAULT 0;
diff --git a/internal/store/migrations/048-add-task-merge-after-review.sql b/internal/store/migrations/048-add-task-merge-after-review.sql
deleted file mode 100644
index ac7242b..0000000
--- a/internal/store/migrations/048-add-task-merge-after-review.sql
+++ /dev/null
@@ -1,3 +0,0 @@
-ALTER TABLE tasks ADD COLUMN merge_after_review_by TEXT NOT NULL DEFAULT '';
-ALTER TABLE tasks ADD COLUMN merge_after_review_id TEXT NOT NULL DEFAULT '';
-ALTER TABLE tasks ADD COLUMN merge_after_review_target TEXT NOT NULL DEFAULT '';
diff --git a/internal/store/migrations/049-add-inbox-response-images.sql b/internal/store/migrations/049-add-inbox-response-images.sql
deleted file mode 100644
index f5d075f..0000000
--- a/internal/store/migrations/049-add-inbox-response-images.sql
+++ /dev/null
@@ -1 +0,0 @@
-ALTER TABLE inbox_responses ADD COLUMN images TEXT NOT NULL DEFAULT '[]';
diff --git a/internal/store/migrations/050-add-lead-note-projects.sql b/internal/store/migrations/050-add-lead-note-projects.sql
deleted file mode 100644
index 0c93cf0..0000000
--- a/internal/store/migrations/050-add-lead-note-projects.sql
+++ /dev/null
@@ -1 +0,0 @@
-ALTER TABLE lead_notes ADD COLUMN project_id TEXT NOT NULL DEFAULT '';
diff --git a/internal/store/migrations/051-add-inbox-run-responses.sql b/internal/store/migrations/051-add-inbox-run-responses.sql
deleted file mode 100644
index acaf438..0000000
--- a/internal/store/migrations/051-add-inbox-run-responses.sql
+++ /dev/null
@@ -1,2 +0,0 @@
-ALTER TABLE inbox_responses ADD COLUMN client TEXT NOT NULL DEFAULT '';
-ALTER TABLE inbox_responses ADD COLUMN exit_code INTEGER;
diff --git a/internal/store/migrations/052-drop-client-runs.sql b/internal/store/migrations/052-drop-client-runs.sql
deleted file mode 100644
index cd396af..0000000
--- a/internal/store/migrations/052-drop-client-runs.sql
+++ /dev/null
@@ -1,4 +0,0 @@
-DELETE FROM inbox_responses WHERE item_id IN (SELECT id FROM inbox_items WHERE kind = 'client_run');
-DELETE FROM inbox_items WHERE kind = 'client_run';
-ALTER TABLE inbox_responses DROP COLUMN client;
-ALTER TABLE inbox_responses DROP COLUMN exit_code;
diff --git a/internal/store/migrations/053-add-user-default-model.sql b/internal/store/migrations/053-add-user-default-model.sql
deleted file mode 100644
index 94e2765..0000000
--- a/internal/store/migrations/053-add-user-default-model.sql
+++ /dev/null
@@ -1 +0,0 @@
-ALTER TABLE users ADD COLUMN default_model TEXT NOT NULL DEFAULT 'claude-opus-5-5';
diff --git a/internal/store/migrations/054-add-team-members.sql b/internal/store/migrations/054-add-team-members.sql
deleted file mode 100644
index 27ddd32..0000000
--- a/internal/store/migrations/054-add-team-members.sql
+++ /dev/null
@@ -1,2 +0,0 @@
-ALTER TABLE agent_sessions ADD COLUMN member TEXT NOT NULL DEFAULT '';
-ALTER TABLE task_inputs ADD COLUMN member TEXT NOT NULL DEFAULT '';
diff --git a/internal/store/migrations/055-rename-inbox-tab.sql b/internal/store/migrations/055-rename-inbox-tab.sql
deleted file mode 100644
index 58b455b..0000000
--- a/internal/store/migrations/055-rename-inbox-tab.sql
+++ /dev/null
@@ -1 +0,0 @@
-UPDATE users SET focus = replace(focus, '"inbox"', '"questions"');
diff --git a/internal/store/migrations/056-add-inbox-response-snapshots.sql b/internal/store/migrations/056-add-inbox-response-snapshots.sql
deleted file mode 100644
index 9d0ec10..0000000
--- a/internal/store/migrations/056-add-inbox-response-snapshots.sql
+++ /dev/null
@@ -1 +0,0 @@
-ALTER TABLE inbox_responses ADD COLUMN snapshot_n INTEGER;
diff --git a/internal/store/migrations/057-errors-to-transcript.sql b/internal/store/migrations/057-errors-to-transcript.sql
deleted file mode 100644
index d2689e9..0000000
--- a/internal/store/migrations/057-errors-to-transcript.sql
+++ /dev/null
@@ -1,12 +0,0 @@
-UPDATE events SET kind = 'session.error', payload = json_object('error', json_extract(payload, '$.payload.error'))
- WHERE kind = 'inbox.created' AND json_extract(payload, '$.kind') = 'session_error';
-UPDATE events SET kind = 'session.rate_limited', payload = json_object(
- 'vendor', json_extract(payload, '$.payload.vendor'),
- 'rate_limit', json(json_extract(payload, '$.payload.rate_limit')),
- 'error', json_extract(payload, '$.payload.error'))
- WHERE kind = 'inbox.created' AND json_extract(payload, '$.kind') = 'rate_limited';
-DELETE FROM events WHERE kind IN ('inbox.created', 'inbox.updated') AND json_extract(payload, '$.kind') != 'question';
-UPDATE events SET payload = json_remove(payload, '$.kind') WHERE kind IN ('inbox.created', 'inbox.updated');
-DELETE FROM inbox_responses WHERE item_id IN (SELECT id FROM inbox_items WHERE kind != 'question');
-DELETE FROM inbox_items WHERE kind != 'question';
-ALTER TABLE inbox_items DROP COLUMN kind;
diff --git a/internal/store/migrations/058-add-user-review-models.sql b/internal/store/migrations/058-add-user-review-models.sql
deleted file mode 100644
index 31ac0fa..0000000
--- a/internal/store/migrations/058-add-user-review-models.sql
+++ /dev/null
@@ -1,2 +0,0 @@
-ALTER TABLE users ADD COLUMN review_model TEXT NOT NULL DEFAULT 'gpt-6.1-sol';
-ALTER TABLE users ADD COLUMN second_review_model TEXT NOT NULL DEFAULT 'claude-opus-5-5';
diff --git a/internal/store/migrations/059-add-task-ready-prompt.sql b/internal/store/migrations/059-add-task-ready-prompt.sql
deleted file mode 100644
index 7760f4e..0000000
--- a/internal/store/migrations/059-add-task-ready-prompt.sql
+++ /dev/null
@@ -1 +0,0 @@
-ALTER TABLE tasks ADD COLUMN ready_prompt_n INTEGER NOT NULL DEFAULT 0;
diff --git a/internal/store/migrations/060-drop-focus-tabs.sql b/internal/store/migrations/060-drop-focus-tabs.sql
deleted file mode 100644
index 4774542..0000000
--- a/internal/store/migrations/060-drop-focus-tabs.sql
+++ /dev/null
@@ -1,17 +0,0 @@
-CREATE TABLE users_next (
- login TEXT PRIMARY KEY,
- display_name TEXT NOT NULL,
- git_name TEXT NOT NULL DEFAULT '',
- git_email TEXT NOT NULL DEFAULT '',
- created_at TEXT NOT NULL,
- notification_prefs TEXT NOT NULL DEFAULT '{"questions":true,"reports":false,"errors":false,"rate_limits":false}',
- focus TEXT NOT NULL DEFAULT '{"tab":"transcript"}',
- default_model TEXT NOT NULL DEFAULT 'claude-opus-5-5',
- review_model TEXT NOT NULL DEFAULT 'gpt-6.1-sol',
- second_review_model TEXT NOT NULL DEFAULT 'claude-opus-5-5'
-);
-INSERT INTO users_next (login, display_name, git_name, git_email, created_at, notification_prefs, focus, default_model, review_model, second_review_model)
- SELECT login, display_name, git_name, git_email, created_at, notification_prefs, json_remove(focus, '$.tabs'), default_model, review_model, second_review_model
- FROM users;
-DROP TABLE users;
-ALTER TABLE users_next RENAME TO users;
diff --git a/internal/store/migrations/061-add-finding-responses-and-drop-questions-reviews-tabs.sql b/internal/store/migrations/061-add-finding-responses-and-drop-questions-reviews-tabs.sql
deleted file mode 100644
index ce5c595..0000000
--- a/internal/store/migrations/061-add-finding-responses-and-drop-questions-reviews-tabs.sql
+++ /dev/null
@@ -1,4 +0,0 @@
-ALTER TABLE reviews ADD COLUMN finding_responses TEXT NOT NULL DEFAULT '{}';
-UPDATE events SET payload = json_set(payload, '$.finding_responses', json('{}'))
-WHERE kind IN ('review.started', 'review.finished', 'review.finding') AND json_type(payload, '$.finding_responses') IS NULL;
-UPDATE users SET focus = json_set(focus, '$.tab', 'transcript') WHERE json_extract(focus, '$.tab') IN ('questions', 'reviews');
diff --git a/internal/store/migrations/062-add-usage-commands.sql b/internal/store/migrations/062-add-usage-commands.sql
deleted file mode 100644
index c31c6a9..0000000
--- a/internal/store/migrations/062-add-usage-commands.sql
+++ /dev/null
@@ -1,4 +0,0 @@
-CREATE TABLE usage_commands (
- vendor TEXT PRIMARY KEY,
- command TEXT NOT NULL
-);
diff --git a/internal/store/migrations/063-add-task-ready.sql b/internal/store/migrations/063-add-task-ready.sql
deleted file mode 100644
index 7ef1539..0000000
--- a/internal/store/migrations/063-add-task-ready.sql
+++ /dev/null
@@ -1,10 +0,0 @@
-ALTER TABLE tasks ADD COLUMN ready TEXT NOT NULL DEFAULT '';
-UPDATE tasks SET ready = coalesce((
- SELECT CASE e.kind WHEN 'task.ready' THEN 'merge' ELSE 'discard' END FROM events e
- WHERE e.task_id = tasks.id AND e.kind IN ('task.ready', 'task.discard_suggested')
- AND NOT EXISTS (
- SELECT 1 FROM events l WHERE l.task_id = tasks.id AND l.seq > e.seq
- AND (l.kind = 'inbox.created' OR (l.kind = 'snapshot.created' AND json_extract(l.payload, '$.trigger') != 'answer'))
- )
- ORDER BY e.seq DESC LIMIT 1
-), '') WHERE status = 'active';
diff --git a/internal/store/migrations/064-ready-prompt-by-event-seq.sql b/internal/store/migrations/064-ready-prompt-by-event-seq.sql
deleted file mode 100644
index 168a274..0000000
--- a/internal/store/migrations/064-ready-prompt-by-event-seq.sql
+++ /dev/null
@@ -1,9 +0,0 @@
-ALTER TABLE tasks ADD COLUMN ready_prompt_seq INTEGER NOT NULL DEFAULT 0;
-
-UPDATE tasks SET ready_prompt_seq = COALESCE((
- SELECT MAX(e.seq) FROM events e
- WHERE e.task_id = tasks.id AND e.kind = 'user.message' AND e.actor = 'byre'
- AND json_extract(e.payload, '$.text') LIKE 'Your turn has ended and the review of your latest work has finished.%'
-), 0);
-
-ALTER TABLE tasks DROP COLUMN ready_prompt_n;
diff --git a/internal/store/migrations/065-add-user-away.sql b/internal/store/migrations/065-add-user-away.sql
deleted file mode 100644
index c3f4def..0000000
--- a/internal/store/migrations/065-add-user-away.sql
+++ /dev/null
@@ -1 +0,0 @@
-ALTER TABLE users ADD COLUMN away INTEGER NOT NULL DEFAULT 0;
diff --git a/internal/store/migrations/066-fold-asking-held-and-ready-into-task-state.sql b/internal/store/migrations/066-fold-asking-held-and-ready-into-task-state.sql
deleted file mode 100644
index 28936b4..0000000
--- a/internal/store/migrations/066-fold-asking-held-and-ready-into-task-state.sql
+++ /dev/null
@@ -1,22 +0,0 @@
-UPDATE tasks SET state = CASE
- WHEN tasks.status != 'active' THEN tasks.status
- WHEN tasks.discard_state != '' THEN 'discarding'
- WHEN EXISTS (SELECT 1 FROM inbox_items i WHERE i.task_id = tasks.id AND i.state = 'open') THEN 'asking'
- WHEN tasks.merging_by != '' OR tasks.finalizing != '' OR tasks.parent_merge_by != '' THEN 'merging'
- WHEN tasks.merge_queued_by != '' THEN 'merge_held'
- WHEN tasks.merge_after_review_by != '' THEN 'merge_after_review'
- ELSE coalesce((
- SELECT CASE
- WHEN s.state != 'idle' THEN s.state
- WHEN s.subagents > 0 THEN 'running'
- WHEN EXISTS (SELECT 1 FROM reviews r WHERE r.task_id = tasks.id AND r.state = 'running') THEN 'reviewing'
- WHEN tasks.review_requests > 0 THEN 'review_pending'
- WHEN tasks.ready = 'merge' THEN 'ready_to_merge'
- WHEN tasks.ready = 'discard' THEN 'discard_suggested'
- ELSE 'idle'
- END
- FROM agent_sessions s
- WHERE s.task_id = tasks.id AND s.role != 'reviewer' AND s.state != 'completed'
- ORDER BY s.state = 'running' DESC, s.subagents > 0 DESC, s.created_at DESC LIMIT 1
- ), 'no_agent')
-END;
diff --git a/internal/store/migrations/067-add-session-started.sql b/internal/store/migrations/067-add-session-started.sql
deleted file mode 100644
index 9dcaa31..0000000
--- a/internal/store/migrations/067-add-session-started.sql
+++ /dev/null
@@ -1,3 +0,0 @@
-ALTER TABLE agent_sessions ADD COLUMN started INTEGER NOT NULL DEFAULT 0;
-ALTER TABLE agent_sessions ADD COLUMN pending INTEGER NOT NULL DEFAULT 0;
-UPDATE agent_sessions SET started = 1 WHERE id IN (SELECT agent_session_id FROM events WHERE kind = 'user.message' AND agent_session_id IS NOT NULL);
diff --git a/internal/store/migrations/068-inbox-actors.sql b/internal/store/migrations/068-inbox-actors.sql
deleted file mode 100644
index 20ba6e4..0000000
--- a/internal/store/migrations/068-inbox-actors.sql
+++ /dev/null
@@ -1,39 +0,0 @@
-CREATE TABLE inbox_items_next (
- id TEXT PRIMARY KEY,
- project_id TEXT NOT NULL REFERENCES projects(id),
- task_id TEXT NOT NULL REFERENCES tasks(id),
- agent_session_id TEXT NOT NULL REFERENCES agent_sessions(id),
- thread_id TEXT NOT NULL,
- priority INTEGER NOT NULL DEFAULT 0,
- blocking INTEGER NOT NULL DEFAULT 1,
- payload TEXT NOT NULL,
- state TEXT NOT NULL,
- snoozed_until TEXT,
- created_at TEXT NOT NULL,
- resolved_by TEXT,
- resolved_at TEXT
-);
-INSERT INTO inbox_items_next SELECT id, project_id, task_id, agent_session_id, thread_id, priority, blocking, payload, state, snoozed_until, created_at, resolved_by, resolved_at FROM inbox_items;
-DROP TABLE inbox_items;
-ALTER TABLE inbox_items_next RENAME TO inbox_items;
-CREATE INDEX inbox_items_open ON inbox_items(state, created_at);
-CREATE INDEX inbox_items_session ON inbox_items(agent_session_id, state);
-
-CREATE TABLE inbox_responses_next (
- id TEXT PRIMARY KEY,
- item_id TEXT NOT NULL REFERENCES inbox_items(id),
- actor TEXT NOT NULL,
- type TEXT NOT NULL,
- text TEXT,
- attachments TEXT NOT NULL,
- snooze_until TEXT,
- created_at TEXT NOT NULL,
- delivered_at TEXT,
- options TEXT,
- images TEXT NOT NULL DEFAULT '[]',
- snapshot_n INTEGER
-);
-INSERT INTO inbox_responses_next SELECT id, item_id, user_login, type, text, attachments, snooze_until, created_at, delivered_at, options, images, snapshot_n FROM inbox_responses;
-DROP TABLE inbox_responses;
-ALTER TABLE inbox_responses_next RENAME TO inbox_responses;
-CREATE INDEX inbox_responses_item ON inbox_responses(item_id, created_at);
diff --git a/internal/store/migrations/069-add-input-send-ids.sql b/internal/store/migrations/069-add-input-send-ids.sql
deleted file mode 100644
index eeef8ca..0000000
--- a/internal/store/migrations/069-add-input-send-ids.sql
+++ /dev/null
@@ -1,33 +0,0 @@
-ALTER TABLE task_inputs ADD COLUMN send_id TEXT NOT NULL DEFAULT '';
-ALTER TABLE lead_inputs ADD COLUMN send_id TEXT NOT NULL DEFAULT '';
-
-UPDATE task_inputs SET send_id = 'queued-' || id;
-
-UPDATE events SET payload = json_set(payload, '$.send_id', 'queued-' || json_extract(payload, '$.id'))
-WHERE kind = 'user.queued';
-
--- The n-th delivered queue entry of a task, sender and text pairs with the n-th later user.message of the same.
-WITH q AS (
- SELECT e.seq, e.task_id, e.actor, json_extract(e.payload, '$.text') AS text, json_extract(e.payload, '$.send_id') AS send_id,
- row_number() OVER (PARTITION BY e.task_id, e.actor, json_extract(e.payload, '$.text') ORDER BY e.seq) AS n
- FROM events AS e
- WHERE e.kind = 'user.queued'
- AND json_extract(e.payload, '$.id') NOT IN (SELECT id FROM task_inputs)
- AND NOT EXISTS (
- SELECT 1 FROM events AS u
- WHERE u.kind = 'user.unqueued' AND u.task_id = e.task_id AND json_extract(u.payload, '$.id') = json_extract(e.payload, '$.id')
- )
-), first AS (
- SELECT task_id, actor, text, min(seq) AS seq FROM q GROUP BY task_id, actor, text
-), m AS (
- SELECT e.seq, e.task_id, e.actor, json_extract(e.payload, '$.text') AS text,
- row_number() OVER (PARTITION BY e.task_id, e.actor, json_extract(e.payload, '$.text') ORDER BY e.seq) AS n
- FROM events AS e
- JOIN first AS f ON f.task_id = e.task_id AND f.actor = e.actor AND f.text = json_extract(e.payload, '$.text') AND e.seq > f.seq
- WHERE e.kind = 'user.message'
-), pairs AS (
- SELECT m.seq, q.send_id FROM m
- JOIN q ON q.task_id = m.task_id AND q.actor = m.actor AND q.text = m.text AND q.n = m.n AND q.seq < m.seq
-)
-UPDATE events SET payload = json_set(payload, '$.send_id', (SELECT send_id FROM pairs WHERE pairs.seq = events.seq))
-WHERE seq IN (SELECT seq FROM pairs);
diff --git a/internal/store/migrations/070-index-event-send-ids.sql b/internal/store/migrations/070-index-event-send-ids.sql
deleted file mode 100644
index 41570cc..0000000
--- a/internal/store/migrations/070-index-event-send-ids.sql
+++ /dev/null
@@ -1 +0,0 @@
-CREATE INDEX events_send_id ON events(json_extract(payload, '$.send_id')) WHERE kind IN ('user.queued', 'user.message');
diff --git a/internal/store/migrations/072-add-review-workflows.sql b/internal/store/migrations/072-add-review-workflows.sql
deleted file mode 100644
index a6d3f42..0000000
--- a/internal/store/migrations/072-add-review-workflows.sql
+++ /dev/null
@@ -1,33 +0,0 @@
-ALTER TABLE projects ADD COLUMN review_workflow TEXT NOT NULL DEFAULT '';
-
-ALTER TABLE reviews ADD COLUMN stage TEXT NOT NULL DEFAULT '';
-ALTER TABLE reviews ADD COLUMN definition TEXT NOT NULL DEFAULT '';
-
-UPDATE reviews SET stage = 'Review' WHERE state != 'skipped' AND stage = '';
-
-UPDATE events SET payload = json_set(payload, '$.stage', CASE json_extract(payload, '$.state') WHEN 'skipped' THEN '' ELSE 'Review' END)
-WHERE kind IN ('review.started', 'review.finished', 'review.finding') AND json_extract(payload, '$.stage') IS NULL;
-
-UPDATE events SET payload = json_set(payload, '$.input.reviewer', 'Review')
-WHERE kind = 'agent.tool_call' AND json_extract(payload, '$.name') LIKE '%respond_to_finding'
-AND json_extract(payload, '$.input.reviewer') IS NULL;
-
-INSERT OR IGNORE INTO workflows (name, definition)
-SELECT 'Review', json_object(
- 'description', 'One reviewer looks over all of the task''s work.',
- 'kind', 'review',
- 'steps', json_array(json_object(
- 'stage', 'Review',
- 'model', u.review_model,
- 'prompt', 'Look for bugs, security problems, missing or wrong tests, and anything that doesn''t do what the task asked.'
- ))
-)
-FROM projects p JOIN users u ON u.login = p.created_by
-ORDER BY p.created_at
-LIMIT 1;
-
-UPDATE projects SET review_workflow = 'Review'
-WHERE review_workflow = '' AND EXISTS (SELECT 1 FROM workflows WHERE name = 'Review');
-
-ALTER TABLE users DROP COLUMN review_model;
-ALTER TABLE users DROP COLUMN second_review_model;
diff --git a/internal/store/migrations/073-drop-workflow-kind.sql b/internal/store/migrations/073-drop-workflow-kind.sql
deleted file mode 100644
index 9fcce50..0000000
--- a/internal/store/migrations/073-drop-workflow-kind.sql
+++ /dev/null
@@ -1,11 +0,0 @@
-UPDATE workflows SET definition = json_remove(definition, '$.kind')
-WHERE json_type(definition, '$.kind') IS NOT NULL;
-
-UPDATE tasks SET workflow_definition = json_remove(workflow_definition, '$.kind')
-WHERE workflow_definition != '' AND json_type(workflow_definition, '$.kind') IS NOT NULL;
-
-UPDATE reviews SET definition = json_remove(definition, '$.kind')
-WHERE definition != '' AND json_type(definition, '$.kind') IS NOT NULL;
-
-UPDATE events SET payload = json_set(payload, '$.kept', json_array(task_id))
-WHERE kind = 'task.workflow_finished' AND json_type(payload, '$.kept') IS NULL;
diff --git a/internal/store/migrations/074-one-review-per-stage.sql b/internal/store/migrations/074-one-review-per-stage.sql
deleted file mode 100644
index c26d0e0..0000000
--- a/internal/store/migrations/074-one-review-per-stage.sql
+++ /dev/null
@@ -1,25 +0,0 @@
-CREATE TABLE reviews_next (
- id TEXT PRIMARY KEY,
- task_id TEXT NOT NULL REFERENCES tasks(id),
- snapshot_n INTEGER NOT NULL,
- agent_session_id TEXT REFERENCES agent_sessions(id),
- vendor TEXT NOT NULL,
- model TEXT NOT NULL,
- state TEXT NOT NULL,
- result TEXT NOT NULL DEFAULT '',
- error TEXT NOT NULL DEFAULT '',
- created_at TEXT NOT NULL,
- finished_at TEXT NOT NULL DEFAULT '',
- finding_states TEXT NOT NULL DEFAULT '{}',
- finding_responses TEXT NOT NULL DEFAULT '{}',
- stage TEXT NOT NULL DEFAULT '',
- definition TEXT NOT NULL DEFAULT '',
- UNIQUE (task_id, snapshot_n, stage)
-);
-
-INSERT INTO reviews_next (id, task_id, snapshot_n, agent_session_id, vendor, model, state, result, error, created_at, finished_at, finding_states, finding_responses, stage, definition)
-SELECT id, task_id, snapshot_n, agent_session_id, vendor, model, state, result, error, created_at, finished_at, finding_states, finding_responses, stage, definition FROM reviews;
-
-DROP TABLE reviews;
-ALTER TABLE reviews_next RENAME TO reviews;
-CREATE UNIQUE INDEX reviews_session ON reviews(agent_session_id);
diff --git a/internal/store/migrations/075-drop-inbox-thread.sql b/internal/store/migrations/075-drop-inbox-thread.sql
deleted file mode 100644
index e057486..0000000
--- a/internal/store/migrations/075-drop-inbox-thread.sql
+++ /dev/null
@@ -1,4 +0,0 @@
-ALTER TABLE inbox_items DROP COLUMN thread_id;
-
-UPDATE events SET payload = json_remove(payload, '$.thread_id')
-WHERE kind IN ('inbox.created', 'inbox.updated') AND json_type(payload, '$.thread_id') IS NOT NULL;
diff --git a/internal/store/migrations/076-normalize-tool-call-names.sql b/internal/store/migrations/076-normalize-tool-call-names.sql
deleted file mode 100644
index e53db22..0000000
--- a/internal/store/migrations/076-normalize-tool-call-names.sql
+++ /dev/null
@@ -1,5 +0,0 @@
-UPDATE events SET payload = json_set(payload, '$.name', 'Read', '$.input', json_object('file_path', coalesce(json_extract(payload, '$.input.Path'), json_extract(payload, '$.input.AbsolutePath'))))
-WHERE kind = 'agent.tool_call' AND json_extract(payload, '$.name') IN ('read_file', 'view_file');
-
-UPDATE events SET payload = json_set(payload, '$.name', 'mcp__byre__' || json_extract(payload, '$.name'))
-WHERE kind = 'agent.tool_call' AND json_extract(payload, '$.name') IN ('submit_review', 'set_task_summary', 'finish_stage', 'take_snapshot');
diff --git a/internal/store/migrations/077-add-audits.sql b/internal/store/migrations/077-add-audits.sql
deleted file mode 100644
index 41d902b..0000000
--- a/internal/store/migrations/077-add-audits.sql
+++ /dev/null
@@ -1,2 +0,0 @@
-ALTER TABLE projects ADD COLUMN audit_workflow TEXT NOT NULL DEFAULT '';
-ALTER TABLE tasks ADD COLUMN audit TEXT NOT NULL DEFAULT '';
diff --git a/internal/store/migrations/079-drop-obsolete-inbox-and-session-columns.sql b/internal/store/migrations/079-drop-obsolete-inbox-and-session-columns.sql
deleted file mode 100644
index 2f42971..0000000
--- a/internal/store/migrations/079-drop-obsolete-inbox-and-session-columns.sql
+++ /dev/null
@@ -1,5 +0,0 @@
-ALTER TABLE inbox_items DROP COLUMN priority;
-ALTER TABLE inbox_items DROP COLUMN blocking;
-ALTER TABLE inbox_items DROP COLUMN snoozed_until;
-ALTER TABLE inbox_responses DROP COLUMN snooze_until;
-ALTER TABLE agent_sessions DROP COLUMN workflow_stage;
diff --git a/internal/store/migrations/080-normalize-notification-preferences.sql b/internal/store/migrations/080-normalize-notification-preferences.sql
deleted file mode 100644
index 1b09246..0000000
--- a/internal/store/migrations/080-normalize-notification-preferences.sql
+++ /dev/null
@@ -1,20 +0,0 @@
-ALTER TABLE users ADD COLUMN notify_questions INTEGER NOT NULL DEFAULT 1;
-ALTER TABLE users ADD COLUMN notify_reports INTEGER NOT NULL DEFAULT 0;
-ALTER TABLE users ADD COLUMN notify_errors INTEGER NOT NULL DEFAULT 0;
-ALTER TABLE users ADD COLUMN notify_rate_limits INTEGER NOT NULL DEFAULT 0;
-ALTER TABLE users ADD COLUMN notify_ready INTEGER NOT NULL DEFAULT 0;
-ALTER TABLE users ADD COLUMN notify_discard INTEGER NOT NULL DEFAULT 0;
-ALTER TABLE users ADD COLUMN notify_merge_problems INTEGER NOT NULL DEFAULT 0;
-ALTER TABLE users ADD COLUMN notify_workflows INTEGER NOT NULL DEFAULT 0;
-
-UPDATE users SET
- notify_questions = coalesce(json_extract(notification_prefs, '$.questions'), 0),
- notify_reports = coalesce(json_extract(notification_prefs, '$.reports'), 0),
- notify_errors = coalesce(json_extract(notification_prefs, '$.errors'), 0),
- notify_rate_limits = coalesce(json_extract(notification_prefs, '$.rate_limits'), 0),
- notify_ready = coalesce(json_extract(notification_prefs, '$.ready'), 0),
- notify_discard = coalesce(json_extract(notification_prefs, '$.discard'), 0),
- notify_merge_problems = coalesce(json_extract(notification_prefs, '$.merge_problems'), 0),
- notify_workflows = coalesce(json_extract(notification_prefs, '$.workflows'), 0);
-
-ALTER TABLE users DROP COLUMN notification_prefs;
diff --git a/internal/store/migrations/081-drop-derived-blob-path.sql b/internal/store/migrations/081-drop-derived-blob-path.sql
deleted file mode 100644
index 1bec386..0000000
--- a/internal/store/migrations/081-drop-derived-blob-path.sql
+++ /dev/null
@@ -1 +0,0 @@
-ALTER TABLE blobs DROP COLUMN path;
diff --git a/internal/store/migrations/082-index-audit-attention.sql b/internal/store/migrations/082-index-audit-attention.sql
deleted file mode 100644
index b614ced..0000000
--- a/internal/store/migrations/082-index-audit-attention.sql
+++ /dev/null
@@ -1 +0,0 @@
-CREATE INDEX events_attention ON events(task_id) WHERE kind IN ('inbox.created', 'task.ready', 'session.error', 'session.rate_limited');
diff --git a/internal/store/migrations/083-add-finished-task-state.sql b/internal/store/migrations/083-add-finished-task-state.sql
deleted file mode 100644
index 604cfcc..0000000
--- a/internal/store/migrations/083-add-finished-task-state.sql
+++ /dev/null
@@ -1 +0,0 @@
-UPDATE tasks SET state = 'finished' WHERE state = 'idle' AND stage_output != '';
diff --git a/internal/store/migrations/084-index-session-messages.sql b/internal/store/migrations/084-index-session-messages.sql
deleted file mode 100644
index 7d319bd..0000000
--- a/internal/store/migrations/084-index-session-messages.sql
+++ /dev/null
@@ -1 +0,0 @@
-CREATE INDEX events_message_session ON events(agent_session_id, seq) WHERE kind = 'user.message';
diff --git a/internal/store/migrations/086-add-input-clients.sql b/internal/store/migrations/086-add-input-clients.sql
deleted file mode 100644
index b6de745..0000000
--- a/internal/store/migrations/086-add-input-clients.sql
+++ /dev/null
@@ -1,2 +0,0 @@
-ALTER TABLE task_inputs ADD COLUMN client_id TEXT NOT NULL DEFAULT '';
-ALTER TABLE lead_inputs ADD COLUMN client_id TEXT NOT NULL DEFAULT '';
diff --git a/internal/store/migrations/087-add-saved-commands.sql b/internal/store/migrations/087-add-saved-commands.sql
deleted file mode 100644
index fee1d87..0000000
--- a/internal/store/migrations/087-add-saved-commands.sql
+++ /dev/null
@@ -1,7 +0,0 @@
-CREATE TABLE saved_commands (
- id TEXT PRIMARY KEY,
- owner TEXT NOT NULL REFERENCES users(login),
- name TEXT NOT NULL,
- command TEXT NOT NULL
-);
-CREATE INDEX saved_commands_owner ON saved_commands(owner, name);
diff --git a/internal/store/migrations/089-add-task-merge-after-turn.sql b/internal/store/migrations/089-add-task-merge-after-turn.sql
deleted file mode 100644
index 5660995..0000000
--- a/internal/store/migrations/089-add-task-merge-after-turn.sql
+++ /dev/null
@@ -1 +0,0 @@
-ALTER TABLE tasks ADD COLUMN merge_after_turn INTEGER NOT NULL DEFAULT 0;
diff --git a/internal/store/migrations/090-add-task-pushing-to-main.sql b/internal/store/migrations/090-add-task-pushing-to-main.sql
deleted file mode 100644
index 6a51789..0000000
--- a/internal/store/migrations/090-add-task-pushing-to-main.sql
+++ /dev/null
@@ -1,2 +0,0 @@
-ALTER TABLE tasks ADD COLUMN pushing_to_main INTEGER NOT NULL DEFAULT 0;
-CREATE INDEX tasks_status ON tasks (status);
diff --git a/internal/store/migrations/091-add-task-merge-facts.sql b/internal/store/migrations/091-add-task-merge-facts.sql
deleted file mode 100644
index 6ff0068..0000000
--- a/internal/store/migrations/091-add-task-merge-facts.sql
+++ /dev/null
@@ -1,17 +0,0 @@
-ALTER TABLE tasks ADD COLUMN unmerged_commits INTEGER NOT NULL DEFAULT 0;
-ALTER TABLE tasks ADD COLUMN unmerged_files INTEGER NOT NULL DEFAULT 0;
-ALTER TABLE tasks ADD COLUMN unreviewed_changes INTEGER NOT NULL DEFAULT 0;
-ALTER TABLE tasks ADD COLUMN review_snapshot INTEGER NOT NULL DEFAULT 0;
-ALTER TABLE tasks ADD COLUMN review_count INTEGER NOT NULL DEFAULT 0;
-ALTER TABLE tasks ADD COLUMN review_running INTEGER NOT NULL DEFAULT 0;
-ALTER TABLE tasks ADD COLUMN review_failed INTEGER NOT NULL DEFAULT 0;
-ALTER TABLE tasks ADD COLUMN review_findings INTEGER NOT NULL DEFAULT 0;
-CREATE TABLE task_participants (
- task_id TEXT NOT NULL REFERENCES tasks (id),
- position INTEGER NOT NULL,
- login TEXT NOT NULL,
- display_name TEXT NOT NULL,
- git_name TEXT NOT NULL,
- git_email TEXT NOT NULL,
- PRIMARY KEY (task_id, position)
-);
diff --git a/internal/store/migrations/092-store-ready-merge-choice.sql b/internal/store/migrations/092-store-ready-merge-choice.sql
deleted file mode 100644
index 5c8b07a..0000000
--- a/internal/store/migrations/092-store-ready-merge-choice.sql
+++ /dev/null
@@ -1,6 +0,0 @@
-UPDATE tasks SET ready = coalesce((
- SELECT json_extract(e.payload, '$.merge') FROM events e
- WHERE e.task_id = tasks.id AND e.kind = 'task.ready'
- ORDER BY e.seq DESC LIMIT 1
-), 'complete')
-WHERE ready = 'merge';
diff --git a/internal/store/migrations/093-add-lead-wakes.sql b/internal/store/migrations/093-add-lead-wakes.sql
deleted file mode 100644
index 640610b..0000000
--- a/internal/store/migrations/093-add-lead-wakes.sql
+++ /dev/null
@@ -1,6 +0,0 @@
-CREATE TABLE lead_wakes (
- owner TEXT NOT NULL REFERENCES users(login),
- task_id TEXT NOT NULL REFERENCES tasks(id),
- created_at TEXT NOT NULL,
- PRIMARY KEY (owner, task_id)
-);
diff --git a/internal/store/migrations/094-add-snapshot-review-progress.sql b/internal/store/migrations/094-add-snapshot-review-progress.sql
deleted file mode 100644
index ff83fef..0000000
--- a/internal/store/migrations/094-add-snapshot-review-progress.sql
+++ /dev/null
@@ -1,14 +0,0 @@
-ALTER TABLE snapshots ADD COLUMN review INTEGER NOT NULL DEFAULT 0;
-ALTER TABLE snapshots ADD COLUMN settle_state TEXT NOT NULL DEFAULT '';
-
-UPDATE snapshots SET review = 1
-WHERE EXISTS (SELECT 1 FROM reviews r WHERE r.task_id = snapshots.task_id AND r.snapshot_n = snapshots.n);
-
-UPDATE snapshots SET settle_state = 'done'
-WHERE NOT EXISTS (SELECT 1 FROM reviews r WHERE r.task_id = snapshots.task_id AND r.snapshot_n = snapshots.n AND r.state = 'running')
- AND NOT EXISTS (
- SELECT 1 FROM tasks t JOIN reviews r ON r.id = t.merge_after_review_id
- WHERE t.id = snapshots.task_id AND t.merge_after_review_by != '' AND r.snapshot_n = snapshots.n
- );
-
-CREATE INDEX reviews_running ON reviews(state) WHERE state = 'running';
diff --git a/internal/store/migrations/095-add-task-landing.sql b/internal/store/migrations/095-add-task-landing.sql
deleted file mode 100644
index 5341056..0000000
--- a/internal/store/migrations/095-add-task-landing.sql
+++ /dev/null
@@ -1,2 +0,0 @@
-ALTER TABLE tasks ADD COLUMN landing_session TEXT NOT NULL DEFAULT '';
-ALTER TABLE tasks ADD COLUMN landing_by TEXT NOT NULL DEFAULT '';
diff --git a/internal/store/migrations/096-add-schedules.sql b/internal/store/migrations/096-add-schedules.sql
deleted file mode 100644
index ebda4ec..0000000
--- a/internal/store/migrations/096-add-schedules.sql
+++ /dev/null
@@ -1,23 +0,0 @@
-CREATE TABLE schedules (
- id TEXT PRIMARY KEY,
- project_id TEXT NOT NULL REFERENCES projects(id),
- owner TEXT NOT NULL REFERENCES users(login),
- title TEXT NOT NULL,
- every TEXT NOT NULL CHECK (every IN ('hour', 'day', 'week', 'month')),
- hour INTEGER NOT NULL CHECK (hour BETWEEN 0 AND 23),
- minute INTEGER NOT NULL CHECK (minute BETWEEN 0 AND 59),
- day_of_month INTEGER NOT NULL CHECK (day_of_month BETWEEN 0 AND 31),
- prompt TEXT NOT NULL,
- model TEXT NOT NULL,
- workflow TEXT NOT NULL,
- next_run_at TEXT NOT NULL,
- created_at TEXT NOT NULL
-);
-
-CREATE INDEX schedules_by_next_run ON schedules (next_run_at);
-
-CREATE TABLE schedule_weekdays (
- schedule_id TEXT NOT NULL REFERENCES schedules(id),
- weekday TEXT NOT NULL CHECK (weekday IN ('mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun')),
- PRIMARY KEY (schedule_id, weekday)
-);
diff --git a/internal/store/migrations/097-normalize-focus.sql b/internal/store/migrations/097-normalize-focus.sql
deleted file mode 100644
index 66c5c53..0000000
--- a/internal/store/migrations/097-normalize-focus.sql
+++ /dev/null
@@ -1,4 +0,0 @@
-ALTER TABLE users ADD COLUMN focus_task_id TEXT;
-ALTER TABLE users ADD COLUMN focus_project_id TEXT;
-UPDATE users SET focus_task_id = json_extract(focus, '$.task_id'), focus_project_id = json_extract(focus, '$.project_id');
-ALTER TABLE users DROP COLUMN focus;
diff --git a/internal/store/migrations/098-drop-task-port-base.sql b/internal/store/migrations/098-drop-task-port-base.sql
deleted file mode 100644
index 830800c..0000000
--- a/internal/store/migrations/098-drop-task-port-base.sql
+++ /dev/null
@@ -1 +0,0 @@
-ALTER TABLE tasks DROP COLUMN port_base;
diff --git a/internal/store/migrations/099-drop-lead-wakes.sql b/internal/store/migrations/099-drop-lead-wakes.sql
deleted file mode 100644
index 9c454b7..0000000
--- a/internal/store/migrations/099-drop-lead-wakes.sql
+++ /dev/null
@@ -1 +0,0 @@
-DROP TABLE lead_wakes;
diff --git a/internal/store/migrations/100-baseline.sql b/internal/store/migrations/100-baseline.sql
new file mode 100644
index 0000000..c0b9e1a
--- /dev/null
+++ b/internal/store/migrations/100-baseline.sql
@@ -0,0 +1,238 @@
+CREATE TABLE projects (
+ id TEXT PRIMARY KEY,
+ name TEXT NOT NULL UNIQUE,
+ repo_path TEXT NOT NULL UNIQUE,
+ default_branch TEXT NOT NULL,
+ remote TEXT NOT NULL,
+ created_by TEXT NOT NULL REFERENCES users(login),
+ created_at TEXT NOT NULL
+, review_workflow TEXT NOT NULL DEFAULT '', audit_workflow TEXT NOT NULL DEFAULT '');
+CREATE TABLE tasks (
+ id TEXT PRIMARY KEY,
+ project_id TEXT NOT NULL REFERENCES projects(id),
+ title TEXT NOT NULL,
+ branch TEXT NOT NULL,
+ worktree_path TEXT NOT NULL,
+ base_ref TEXT NOT NULL,
+ status TEXT NOT NULL,
+ created_by TEXT NOT NULL REFERENCES users(login),
+ created_at TEXT NOT NULL,
+ closed_at TEXT
+, merge_commit TEXT NOT NULL DEFAULT '', merge_title TEXT NOT NULL DEFAULT '', todo_item TEXT NOT NULL DEFAULT '', merging_by TEXT NOT NULL DEFAULT '', forked_from TEXT NOT NULL DEFAULT '', forked_at INTEGER NOT NULL DEFAULT 0, merge_queued_by TEXT NOT NULL DEFAULT '', merge_queued_at TEXT NOT NULL DEFAULT '', color INTEGER NOT NULL DEFAULT 0, workflow_name TEXT NOT NULL DEFAULT '', workflow_brief TEXT NOT NULL DEFAULT '', workflow_stage TEXT NOT NULL DEFAULT '', workflow_title TEXT NOT NULL DEFAULT '', stage_output TEXT NOT NULL DEFAULT '', stage_commit TEXT NOT NULL DEFAULT '', summary TEXT NOT NULL DEFAULT '', discarding_by TEXT NOT NULL DEFAULT '', discard_state TEXT NOT NULL DEFAULT '', discard_session TEXT NOT NULL DEFAULT '', parent_task_id TEXT NOT NULL DEFAULT '', parent_merge_by TEXT NOT NULL DEFAULT '', finalizing TEXT NOT NULL DEFAULT '', state TEXT NOT NULL DEFAULT '', review_requests INTEGER NOT NULL DEFAULT 0, finalizing_commit TEXT NOT NULL DEFAULT '', finalizing_notes TEXT NOT NULL DEFAULT '', finalizing_unchanged INTEGER NOT NULL DEFAULT 0, merge_after_review_by TEXT NOT NULL DEFAULT '', merge_after_review_id TEXT NOT NULL DEFAULT '', merge_after_review_target TEXT NOT NULL DEFAULT '', ready TEXT NOT NULL DEFAULT '', ready_prompt_seq INTEGER NOT NULL DEFAULT 0, audit TEXT NOT NULL DEFAULT '', workflow_definition_id TEXT REFERENCES workflow_definitions(id), merge_after_turn INTEGER NOT NULL DEFAULT 0, pushing_to_main INTEGER NOT NULL DEFAULT 0, unmerged_commits INTEGER NOT NULL DEFAULT 0, unmerged_files INTEGER NOT NULL DEFAULT 0, unreviewed_changes INTEGER NOT NULL DEFAULT 0, review_snapshot INTEGER NOT NULL DEFAULT 0, review_count INTEGER NOT NULL DEFAULT 0, review_running INTEGER NOT NULL DEFAULT 0, review_failed INTEGER NOT NULL DEFAULT 0, review_findings INTEGER NOT NULL DEFAULT 0, landing_session TEXT NOT NULL DEFAULT '', landing_by TEXT NOT NULL DEFAULT '');
+CREATE TABLE events (
+ seq INTEGER PRIMARY KEY AUTOINCREMENT,
+ kind TEXT NOT NULL,
+ project_id TEXT,
+ task_id TEXT,
+ actor TEXT NOT NULL,
+ payload TEXT NOT NULL,
+ created_at TEXT NOT NULL
+, agent_session_id TEXT, owner TEXT);
+CREATE TABLE blobs (
+ id TEXT PRIMARY KEY,
+ content_type TEXT NOT NULL,
+ created_by TEXT NOT NULL REFERENCES users(login),
+ created_at TEXT NOT NULL
+);
+CREATE TABLE snapshots (
+ id TEXT PRIMARY KEY,
+ task_id TEXT NOT NULL REFERENCES tasks(id),
+ agent_session_id TEXT NOT NULL REFERENCES agent_sessions(id),
+ n INTEGER NOT NULL,
+ commit_sha TEXT NOT NULL,
+ tree_sha TEXT NOT NULL,
+ event_seq INTEGER NOT NULL,
+ trigger TEXT NOT NULL,
+ created_at TEXT NOT NULL, review INTEGER NOT NULL DEFAULT 0, settle_state TEXT NOT NULL DEFAULT '',
+ UNIQUE (task_id, n)
+);
+CREATE TABLE "agent_sessions" (
+ id TEXT PRIMARY KEY,
+ task_id TEXT REFERENCES tasks(id),
+ owner TEXT NOT NULL DEFAULT '',
+ vendor TEXT NOT NULL,
+ model TEXT NOT NULL,
+ vendor_session_id TEXT NOT NULL,
+ state TEXT NOT NULL,
+ role TEXT NOT NULL,
+ created_by TEXT NOT NULL REFERENCES users(login),
+ created_at TEXT NOT NULL
+, forked_from TEXT NOT NULL DEFAULT '', forked_at TEXT NOT NULL DEFAULT '', stage_output TEXT NOT NULL DEFAULT '', stage_finished INTEGER NOT NULL DEFAULT 0, subagents INTEGER NOT NULL DEFAULT 0, member TEXT NOT NULL DEFAULT '', started INTEGER NOT NULL DEFAULT 0, pending INTEGER NOT NULL DEFAULT 0);
+CREATE TABLE lead_inputs (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ owner TEXT NOT NULL REFERENCES users(login),
+ kind TEXT NOT NULL,
+ actor TEXT NOT NULL,
+ text TEXT NOT NULL,
+ images TEXT NOT NULL DEFAULT '[]',
+ created_at TEXT NOT NULL
+, send_id TEXT NOT NULL DEFAULT '', client_id TEXT NOT NULL DEFAULT '');
+CREATE TABLE previews (
+ task_id TEXT NOT NULL REFERENCES tasks(id),
+ name TEXT NOT NULL,
+ port INTEGER NOT NULL,
+ created_at TEXT NOT NULL,
+ PRIMARY KEY (task_id, name),
+ UNIQUE (task_id, port)
+);
+CREATE TABLE task_inputs (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ task_id TEXT NOT NULL REFERENCES tasks(id),
+ actor TEXT NOT NULL,
+ text TEXT NOT NULL,
+ images TEXT NOT NULL DEFAULT '[]',
+ created_at TEXT NOT NULL
+, member TEXT NOT NULL DEFAULT '', send_id TEXT NOT NULL DEFAULT '', client_id TEXT NOT NULL DEFAULT '');
+CREATE TABLE lead_notes (
+ id TEXT PRIMARY KEY,
+ owner TEXT NOT NULL REFERENCES users(login),
+ text TEXT NOT NULL,
+ created_at TEXT NOT NULL,
+ updated_at TEXT NOT NULL
+, project_id TEXT NOT NULL DEFAULT '');
+CREATE TABLE summary_reads (
+ login TEXT NOT NULL REFERENCES users(login),
+ task_id TEXT NOT NULL REFERENCES tasks(id),
+ seq INTEGER NOT NULL,
+ PRIMARY KEY (login, task_id)
+);
+CREATE TABLE "users" (
+ login TEXT PRIMARY KEY,
+ display_name TEXT NOT NULL,
+ git_name TEXT NOT NULL DEFAULT '',
+ git_email TEXT NOT NULL DEFAULT '',
+ created_at TEXT NOT NULL,
+ default_model TEXT NOT NULL DEFAULT 'claude-opus-5-5',
+ away INTEGER NOT NULL DEFAULT 0, notify_questions INTEGER NOT NULL DEFAULT 1, notify_reports INTEGER NOT NULL DEFAULT 0, notify_errors INTEGER NOT NULL DEFAULT 0, notify_rate_limits INTEGER NOT NULL DEFAULT 0, notify_ready INTEGER NOT NULL DEFAULT 0, notify_discard INTEGER NOT NULL DEFAULT 0, notify_merge_problems INTEGER NOT NULL DEFAULT 0, notify_workflows INTEGER NOT NULL DEFAULT 0, focus_task_id TEXT, focus_project_id TEXT);
+CREATE TABLE usage_commands (
+ vendor TEXT PRIMARY KEY,
+ command TEXT NOT NULL
+);
+CREATE TABLE "inbox_items" (
+ id TEXT PRIMARY KEY,
+ project_id TEXT NOT NULL REFERENCES projects(id),
+ task_id TEXT NOT NULL REFERENCES tasks(id),
+ agent_session_id TEXT NOT NULL REFERENCES agent_sessions(id),
+ payload TEXT NOT NULL,
+ state TEXT NOT NULL,
+ created_at TEXT NOT NULL,
+ resolved_by TEXT,
+ resolved_at TEXT
+);
+CREATE TABLE "inbox_responses" (
+ id TEXT PRIMARY KEY,
+ item_id TEXT NOT NULL REFERENCES inbox_items(id),
+ actor TEXT NOT NULL,
+ type TEXT NOT NULL,
+ text TEXT,
+ attachments TEXT NOT NULL,
+ created_at TEXT NOT NULL,
+ delivered_at TEXT,
+ options TEXT,
+ images TEXT NOT NULL DEFAULT '[]',
+ snapshot_n INTEGER
+);
+CREATE TABLE "reviews" (
+ id TEXT PRIMARY KEY,
+ task_id TEXT NOT NULL REFERENCES tasks(id),
+ snapshot_n INTEGER NOT NULL,
+ agent_session_id TEXT REFERENCES agent_sessions(id),
+ vendor TEXT NOT NULL,
+ model TEXT NOT NULL,
+ state TEXT NOT NULL,
+ result TEXT NOT NULL DEFAULT '',
+ error TEXT NOT NULL DEFAULT '',
+ created_at TEXT NOT NULL,
+ finished_at TEXT NOT NULL DEFAULT '',
+ finding_states TEXT NOT NULL DEFAULT '{}',
+ finding_responses TEXT NOT NULL DEFAULT '{}',
+ stage TEXT NOT NULL DEFAULT '',
+ definition_id TEXT REFERENCES workflow_definitions(id),
+ UNIQUE (task_id, snapshot_n, stage)
+);
+CREATE TABLE workflow_definitions (
+ id TEXT PRIMARY KEY,
+ description TEXT NOT NULL
+);
+CREATE TABLE workflow_steps (
+ definition_id TEXT NOT NULL REFERENCES workflow_definitions(id),
+ id INTEGER NOT NULL,
+ parent INTEGER,
+ position INTEGER NOT NULL,
+ kind TEXT NOT NULL CHECK (kind IN ('stage', 'order', 'parallel')),
+ stage TEXT NOT NULL,
+ model TEXT NOT NULL,
+ prompt TEXT NOT NULL,
+ PRIMARY KEY (definition_id, id),
+ UNIQUE (definition_id, parent, position)
+);
+CREATE TABLE workflow_agents (
+ definition_id TEXT NOT NULL REFERENCES workflow_definitions(id),
+ step_id INTEGER NOT NULL,
+ position INTEGER NOT NULL,
+ name TEXT NOT NULL,
+ model TEXT NOT NULL,
+ prompt TEXT NOT NULL,
+ PRIMARY KEY (definition_id, step_id, position),
+ FOREIGN KEY (definition_id, step_id) REFERENCES workflow_steps(definition_id, id)
+);
+CREATE TABLE "workflows" (
+ name TEXT PRIMARY KEY,
+ definition_id TEXT NOT NULL REFERENCES workflow_definitions(id)
+);
+CREATE TABLE saved_commands (
+ id TEXT PRIMARY KEY,
+ owner TEXT NOT NULL REFERENCES users(login),
+ name TEXT NOT NULL,
+ command TEXT NOT NULL
+);
+CREATE TABLE task_participants (
+ task_id TEXT NOT NULL REFERENCES tasks (id),
+ position INTEGER NOT NULL,
+ login TEXT NOT NULL,
+ display_name TEXT NOT NULL,
+ git_name TEXT NOT NULL,
+ git_email TEXT NOT NULL,
+ PRIMARY KEY (task_id, position)
+);
+CREATE TABLE schedules (
+ id TEXT PRIMARY KEY,
+ project_id TEXT NOT NULL REFERENCES projects(id),
+ owner TEXT NOT NULL REFERENCES users(login),
+ title TEXT NOT NULL,
+ every TEXT NOT NULL CHECK (every IN ('hour', 'day', 'week', 'month')),
+ hour INTEGER NOT NULL CHECK (hour BETWEEN 0 AND 23),
+ minute INTEGER NOT NULL CHECK (minute BETWEEN 0 AND 59),
+ day_of_month INTEGER NOT NULL CHECK (day_of_month BETWEEN 0 AND 31),
+ prompt TEXT NOT NULL,
+ workflow TEXT NOT NULL,
+ next_run_at TEXT NOT NULL,
+ created_at TEXT NOT NULL
+);
+CREATE TABLE schedule_weekdays (
+ schedule_id TEXT NOT NULL REFERENCES schedules(id),
+ weekday TEXT NOT NULL CHECK (weekday IN ('mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun')),
+ PRIMARY KEY (schedule_id, weekday)
+);
+CREATE INDEX tasks_project ON tasks(project_id, created_at);
+CREATE INDEX events_task ON events(task_id, seq);
+CREATE INDEX agent_sessions_task ON agent_sessions(task_id, created_at);
+CREATE INDEX agent_sessions_state ON agent_sessions(state);
+CREATE INDEX agent_sessions_owner ON agent_sessions(owner, created_at);
+CREATE INDEX events_owner ON events(owner, seq) WHERE owner IS NOT NULL;
+CREATE INDEX lead_inputs_owner ON lead_inputs(owner, id);
+CREATE INDEX task_inputs_task ON task_inputs(task_id, id);
+CREATE INDEX tasks_parent ON tasks(parent_task_id) WHERE parent_task_id != '';
+CREATE INDEX lead_notes_owner ON lead_notes(owner, created_at);
+CREATE INDEX events_summary ON events(task_id, seq) WHERE kind = 'turn.ended' AND json_extract(payload, '$.summary') = 1;
+CREATE INDEX inbox_items_open ON inbox_items(state, created_at);
+CREATE INDEX inbox_items_session ON inbox_items(agent_session_id, state);
+CREATE INDEX inbox_responses_item ON inbox_responses(item_id, created_at);
+CREATE INDEX events_send_id ON events(json_extract(payload, '$.send_id')) WHERE kind IN ('user.queued', 'user.message');
+CREATE UNIQUE INDEX reviews_session ON reviews(agent_session_id);
+CREATE INDEX events_attention ON events(task_id) WHERE kind IN ('inbox.created', 'task.ready', 'session.error', 'session.rate_limited');
+CREATE INDEX events_message_session ON events(agent_session_id, seq) WHERE kind = 'user.message';
+CREATE INDEX saved_commands_owner ON saved_commands(owner, name);
+CREATE INDEX tasks_status ON tasks (status);
+CREATE INDEX reviews_running ON reviews(state) WHERE state = 'running';
+CREATE INDEX schedules_by_next_run ON schedules (next_run_at);
diff --git a/internal/store/migrations/111-fold-legacy-snapshot-triggers.sql b/internal/store/migrations/111-fold-legacy-snapshot-triggers.sql
new file mode 100644
index 0000000..71ea9ee
--- /dev/null
+++ b/internal/store/migrations/111-fold-legacy-snapshot-triggers.sql
@@ -0,0 +1,3 @@
+UPDATE snapshots SET trigger = 'turn_end' WHERE trigger IN ('message', 'tool_call', 'timer');
+UPDATE events SET payload = json_set(payload, '$.trigger', 'turn_end')
+ WHERE kind = 'snapshot.created' AND json_extract(payload, '$.trigger') IN ('message', 'tool_call', 'timer');
diff --git a/internal/store/store.go b/internal/store/store.go
index 1ab39c0..3011434 100644
--- a/internal/store/store.go
+++ b/internal/store/store.go
@@ -9,7 +9,6 @@ import (
"encoding/json"
"errors"
"fmt"
- "path/filepath"
"slices"
"strconv"
"strings"
@@ -25,19 +24,15 @@ var ErrNotFound = errors.New("not found")
//go:embed migrations/*.sql
var migrationFiles embed.FS
+const (
+ baselineName = "baseline"
+ baselineThrough = "schedule-workflows"
+)
+
type migration struct {
key string
name string
sql string
- run func(tx *sql.Tx, dataDir string) error
-}
-
-var goMigrations = map[string]func(tx *sql.Tx, dataDir string) error{
- "071-workflow-steps": migrateWorkflowSteps,
- "078-normalize-workflow-definitions": migrateWorkflowDefinitions,
- "085-prefix-ids": migratePrefixIDs,
- "088-workflow-run-tasks": migrateRunTasks,
- "100-schedule-workflows": migrateScheduleWorkflows,
}
func loadMigrations() ([]migration, error) {
@@ -55,9 +50,6 @@ func loadMigrations() ([]migration, error) {
}
migrations = append(migrations, migration{key: key, sql: string(sql)})
}
- for key, run := range goMigrations {
- migrations = append(migrations, migration{key: key, run: run})
- }
slices.SortFunc(migrations, func(a, b migration) int { return strings.Compare(a.key, b.key) })
for i, m := range migrations {
@@ -89,7 +81,7 @@ func Open(path string, onEvent func([]api.Event)) (*Store, error) {
}
db := sql.OpenDB(connector)
- err = migrate(db, filepath.Dir(path))
+ err = migrate(db)
if err != nil {
db.Close()
return nil, err
@@ -127,7 +119,7 @@ func (s *Store) Schema() string {
return s.schema
}
-func migrate(db *sql.DB, dataDir string) error {
+func migrate(db *sql.DB) error {
migrations, err := loadMigrations()
if err != nil {
return err
@@ -143,12 +135,23 @@ func migrate(db *sql.DB, dataDir string) error {
return err
}
+ if len(applied) > 0 && !applied[baselineName] {
+ if !applied[baselineThrough] {
+ return fmt.Errorf("database is too old: it hasn't applied migration %s; open it with an earlier byre build first to bring it that far", baselineThrough)
+ }
+ _, err = db.Exec(`INSERT INTO migrations (name, applied_at) VALUES (?, ?)`, baselineName, now())
+ if err != nil {
+ return err
+ }
+ applied[baselineName] = true
+ }
+
for _, m := range migrations {
if applied[m.name] {
continue
}
- err = applyMigration(db, m, dataDir)
+ err = applyMigration(db, m)
if err != nil {
return fmt.Errorf("migration %s: %w", m.name, err)
}
@@ -176,7 +179,7 @@ func appliedMigrations(db *sql.DB) (map[string]bool, error) {
return applied, rows.Err()
}
-func applyMigration(db *sql.DB, m migration, dataDir string) error {
+func applyMigration(db *sql.DB, m migration) error {
ctx := context.Background()
conn, err := db.Conn(ctx)
if err != nil {
@@ -196,11 +199,7 @@ func applyMigration(db *sql.DB, m migration, dataDir string) error {
}
defer tx.Rollback()
- if m.run != nil {
- err = m.run(tx, dataDir)
- } else {
- _, err = tx.Exec(m.sql)
- }
+ _, err = tx.Exec(m.sql)
if err != nil {
return err
}
diff --git a/mac/Sources/Byre/Format.swift b/mac/Sources/Byre/Format.swift
index b01c1ee..e31dd54 100644
--- a/mac/Sources/Byre/Format.swift
+++ b/mac/Sources/Byre/Format.swift
@@ -168,16 +168,17 @@ func isoText(_ date: Date) -> String {
extension Snapshot {
var reason: String {
switch trigger {
- case .message: "agent message"
case .turnEnd: "turn ended"
- case .toolCall: "tool call"
case .askHuman: "question asked"
- case .timer: "periodic snapshot"
case .requested: "snapshot requested"
case .answer: "before delivering an answer"
case .fork: "before forking"
}
}
+
+ var isCutPoint: Bool {
+ [.answer, .fork, .askHuman].contains(trigger)
+ }
}
func clockText(_ d: Date) -> String {
diff --git a/mac/Sources/Byre/Store.swift b/mac/Sources/Byre/Store.swift
index cc80184..e17c884 100644
--- a/mac/Sources/Byre/Store.swift
+++ b/mac/Sources/Byre/Store.swift
@@ -851,7 +851,7 @@ final class Store {
case "schedule.saved", "schedule.deleted":
applySchedule(ev)
case "task.created", "task.title", "task.merge_title", "task.merge_queued", "task.merge_unqueued", "task.merge_after_review", "task.merge_after_review_ended", "task.merge_after_review_canceled", "task.parent_merge_started", "task.parent_merge_stopped", "task.merge_started", "task.merge_ended", "task.discard_started", "task.merged", "task.discarded", "task.state", "task.ready", "task.discard_suggested", "task.ready_cleared", "snapshot.created":
- if let taskId = ev.taskId, ["task.merged", "task.discarded"].contains(ev.kind) || (ev.kind == "snapshot.created" && !["answer", "fork", "ask_human"].contains(str(ev.payload.value, "trigger"))) {
+ if let taskId = ev.taskId, ["task.merged", "task.discarded"].contains(ev.kind) || (ev.kind == "snapshot.created" && (try? decodePayload(ev.payload, as: Snapshot.self))?.isCutPoint != true) {
clearTask(self, taskId)
}
if let taskId = ev.taskId, ["task.merged", "task.discarded"].contains(ev.kind) {
diff --git a/mac/Sources/Byre/TranscriptBuilder.swift b/mac/Sources/Byre/TranscriptBuilder.swift
index 7349dff..9d9414a 100644
--- a/mac/Sources/Byre/TranscriptBuilder.swift
+++ b/mac/Sources/Byre/TranscriptBuilder.swift
@@ -528,7 +528,7 @@ final class TranscriptBuilder {
if ev.kind == "snapshot.created" {
ctx.run = nil
let snap = try! decodePayload(ev.payload, as: Snapshot.self)
- if ![.answer, .fork, .askHuman].contains(snap.trigger) && sink == nil && own {
+ if !snap.isCutPoint && sink == nil && own {
lastReady = nil
}
release()