diff --git a/api/openapi.yaml b/api/openapi.yaml index 51865f4..66e2036 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -1226,7 +1226,10 @@ paths: operationId: putWorkflow description: > Creates or replaces the workflow from its file, a WorkflowDefinition as YAML or JSON. The name is one line of - 1 to 100 characters. Tasks that already froze the workflow keep their copy. + 1 to 100 characters. Tasks that already froze the workflow keep their copy. With from, it renames workflow + from to name, which must not be taken, and projects that review with from review with name instead. + parameters: + - { name: from, in: query, required: false, schema: { type: string } } requestBody: required: true content: @@ -1241,6 +1244,8 @@ paths: application/json: schema: { $ref: "#/components/schemas/WorkflowSummary" } "400": { $ref: "#/components/responses/Error" } + "404": { $ref: "#/components/responses/Error" } + "409": { $ref: "#/components/responses/Error" } delete: operationId: deleteWorkflow responses: @@ -1325,7 +1330,7 @@ components: type: object required: [workflow] properties: - workflow: { type: string, description: "A saved workflow of kind review, or empty for no reviews." } + workflow: { type: string, description: "A saved workflow, or empty for no reviews." } ProjectCreate: type: object @@ -1443,28 +1448,20 @@ components: WorkflowSummary: type: object - required: [name, description, kind] + required: [name, description] properties: name: { type: string } description: { type: string } - kind: { type: string, enum: [task, review] } WorkflowDefinition: type: object description: > - A workflow's file, which holds everything but its name: its steps, run in order. Each stage runs as its own - task once the steps before it have finished. Sent and returned as YAML. + A workflow's file, which holds everything but its name: its steps, run in order. Run on a brief, each stage + runs as its own task once the steps before it have finished; as a project's review workflow, each stage is a + reviewer of a snapshot and gets the earlier stages' reviews. Sent and returned as YAML. required: [steps] properties: description: { type: string, x-go-type-skip-optional-pointer: true } - kind: - type: string - enum: [task, review] - x-enum-varnames: [WorkflowKindTask, WorkflowKindReview] - x-go-type-skip-optional-pointer: true - description: > - task (the default): each stage runs as its own task. review: each stage is a reviewer of a snapshot, run - when a project names the workflow as its review workflow; a stage gets the earlier stages' reviews. steps: type: array minItems: 1 diff --git a/docs/operations.md b/docs/operations.md index 7359f71..dcfbe95 100644 --- a/docs/operations.md +++ b/docs/operations.md @@ -308,7 +308,7 @@ Each user has one long-running lead agent, shown in the left pane. It is not tie Task agents get notes too. An implementer session's instructions end with `taskNotesGuidance` and `notesText` for the task's creator, limited to the every-project notes and the task's project's (`startLive` in `internal/agents/manager.go`), fixed when the agent process starts like the lead's. Reviewers get none. Implementers have a `suggest_note` tool (`text`, `evidence`) that queues a lead signal for the task's creator naming the task, the suggested text, the evidence and the project id, and refuses when the creator has no live lead session. The lead's guidance tells it to save a suggestion with `remember`, choosing its project, only when it holds and isn't already covered, and to tell the human nothing either way. A suggestion leaves no record of its own beyond the signal; a saved one shows as the lead's `lead.note_saved`. - **Restarts.** On shutdown, byre marks each running lead session idle with "byre stopped mid-turn" and holds a restart notice for it, so a lead is never left `running` for a build without lead support to find. After a crash, startup resumes a running lead like a task session. Startup then delivers any held input. -The UI is two panes filling the window, with no navbar. The left has six tabs: Lead, the lead's transcript and composer (the same view as a task transcript, with only the Compact button in its header), Notes, the lead's notes (see "Notes" under the lead), Workflows, Status, Server and Settings, and at the end of its tab strip the connection dot and the user's name. Workflows (`mac/Sources/Byre/Workflows.swift`) lists `GET /api/workflows` by name with each description, refetching on `workflow.updated` and `workflow.deleted`; a workflow's run button doesn't start anything itself, since a run needs a project and a brief: it appends `Run the "" workflow: ` to the lead's composer draft, selects the Lead tab and focuses the composer, for the human to finish and send. A workflow's edit button, and the tab's new button, replace the list with the editor (`mac/Sources/Byre/WorkflowEditor.swift`): it loads the definition with `GET /api/workflows/{name}` accepting only JSON, holds it as a tree of `StepNode`s, and draws one row per step with box-drawing guides, a stage as its name, model menu and an ellipsis menu (add a stage after, run in parallel with a new stage, group with a new stage in order, move up or down, remove), an `order` or `parallel` as "in order" or "in parallel" with add inside, switch kind and ungroup. Rows drag: dropping one on a stage moves it before that stage, on an order or parallel to its end. A stage's disclosure shows its prompt editor, and for a team, each agent's name, model and prompt. Save sends the definition as JSON to `PUT /api/workflows/{name}`, and shows the server's validation error under the description when it refuses; leaving with unsaved changes asks first. A new workflow's name is typed in the editor; an existing one's isn't editable there. A choice under the description sets its `kind`: runs on a brief, or reviews snapshots. Server is about the byre service on the Mac the app runs on, whatever server the app is connected to (`mac/Sources/Byre/Server.swift`), so its headings name the Mac (`Host.current().localizedName`, as in `Byre-Host`): Server on is On when launchd has the service loaded (`launchctl print gui//run.fc.byre` succeeds), showing Turn off (`launchctl bootout`) and, from `GET /api/tailnet` at the `loopback` address `byre check` prints, the service's `origin` once its node is `Running`, or else the node's hostname and state, polling every 2 s until the node runs (a node that needs signing in takes the window to the sign-in screen); Off otherwise, showing Turn on, which runs the bundle's `byre install` as the connection screen's Run byre on this Mac does. Below that, Tools on lists every check from the bundle's `byre check` (see "Running as a service"), each Found or Missing, with how to install a missing one: its command in a box with Copy and Run in Terminal, which writes the command to a `.command` file in the temporary directory, run by the user's login shell, and opens it in Terminal, so prompts like sudo's work and the list updates when the app becomes active again; Missing is in the danger colour only when it blocks the server. Run byre on this Mac runs `byre check` first, and when something blocks it lists the missing checks in the same form instead of installing. Both Turn on and Run byre on this Mac leave the service running instead of reinstalling when it is already on, since a reinstall restarts it and the old service could answer the first status check. The app runs both helpers through the user's login shell, so they see the `PATH` the service is given. It reads the state and checks again each time it shows and when the app becomes active. While the service is on, Agents' permissions has Check permissions, which calls `POST /api/admin/permissions` on the service at that address (see "Permissions" under "Screen and app control") and lists each grant as Granted, Missing (with Open System Settings, which opens the grant's pane) or Not checked. Below it, Usage commands has one field per vendor, empty meaning the vendor's own reports, loaded from and saved to that service's `/api/admin/usage-commands` (see "Usage" under "Status"). Settings holds the user's and this client's preferences; its This Mac's permissions has the same Check permissions for the app's own grants, the ones `client_hands` uses, running the bundle's `byre-hands check` from a copy in the temporary directory (`checkLocalPermissions` in `Server.swift`). Which left tab is selected is kept only in memory (`Overlays`); the Lead view stays mounted while another tab shows, and Notes, Workflows, Status, Server or Settings mounts each time its tab is selected. The right shows the focused task, or project with no task: a top row holding the task picker, centred, and the task's preview links at its right end (see "Previews"), then the tab strip, then the selected tab's view with its own header. The tabs are Transcript, Board, Changes, Files, Terminal, Workflow and Project, always in that order. Board shows only on a team task (see "Teams") and Workflow only on a task with a `workflow_run` (see "Workflows"); the selected tab always shows. Tabs that don't fit in the strip move, from the end, into a **More ▾** menu (`MoreButton` in `mac/Sources/Byre/Panes.swift`); the selected tab never moves there. Questions and reviews have no tabs: both live in the transcript (see "Inbox" and "Reviews"), and the status in the transcript's composer bar points at what needs the user there (see "Attention" under "Inbox"). The **Go** menu holds the shortcuts, each also on its Ctrl key outside the terminal. ⌘K opens the task switcher. ⌘P selects the Project tab, keeping the right pane's task. ⌘L selects the Lead tab and puts the cursor in the lead's composer. ⌘S opens the right pane's task's Merge confirmation, from any tab, when the Merge button would be enabled; while the task is `discard_suggested` it opens the Discard confirmation instead. ⌘/ opens a dialog listing every keyboard shortcut; the list is `shortcuts` in `mac/Sources/Byre/Switcher.swift`, kept in step with the menu by hand. The shortcuts do nothing while a dialog is open. +The UI is two panes filling the window, with no navbar. The left has six tabs: Lead, the lead's transcript and composer (the same view as a task transcript, with only the Compact button in its header), Notes, the lead's notes (see "Notes" under the lead), Workflows, Status, Server and Settings, and at the end of its tab strip the connection dot and the user's name. Workflows (`mac/Sources/Byre/Workflows.swift`) lists `GET /api/workflows` by name with each description, refetching on `workflow.updated` and `workflow.deleted`; a workflow's run button doesn't start anything itself, since a run needs a project and a brief: it appends `Run the "" workflow: ` to the lead's composer draft, selects the Lead tab and focuses the composer, for the human to finish and send. A workflow's edit button, and the tab's new button, replace the list with the editor (`mac/Sources/Byre/WorkflowEditor.swift`): it loads the definition with `GET /api/workflows/{name}` accepting only JSON, holds it as a tree of `StepNode`s, and draws one row per step with box-drawing guides, a stage as its name, model menu and an ellipsis menu (add a stage after, run in parallel with a new stage, group with a new stage in order, move up or down, remove), an `order` or `parallel` as "in order" or "in parallel" with add inside, switch kind and ungroup. Rows drag: dropping one on a stage moves it before that stage, on an order or parallel to its end. A stage's disclosure shows its prompt editor, and for a team, each agent's name, model and prompt. Save sends the definition as JSON to `PUT /api/workflows/{name}`, with `from` set to the name it was opened under for an existing workflow, and returns to the list; it shows the server's validation error under the description when it refuses, and leaving with unsaved changes asks first. The name is a field at the top of the editor; changing an existing workflow's name and saving renames it. `PUT` with `from` refuses with 404 when `from` doesn't exist, even when it equals the name, so an editor opened before another client renamed the workflow can't recreate the old name. When `from` differs from the name, it renames in the same transaction as the save (`Store.PutWorkflow`): it refuses with 409 when the new name is taken, deletes `from`, points every project whose `review_workflow` is `from` at the new name (a `project.updated` event each), and emits `workflow.deleted` for `from` before `workflow.updated` for the new name. Tasks that froze the workflow keep its old name in their copy. Under the description the editor draws the stages as a flowchart (`stepGraph`, laid out and drawn with the run view's `layoutFlowchart` and `FlowchartCanvas`), redrawn as the tree is edited. Each workflow in the list also has run (above) and delete, which asks first and calls `DELETE /api/workflows/{name}`. Server is about the byre service on the Mac the app runs on, whatever server the app is connected to (`mac/Sources/Byre/Server.swift`), so its headings name the Mac (`Host.current().localizedName`, as in `Byre-Host`): Server on is On when launchd has the service loaded (`launchctl print gui//run.fc.byre` succeeds), showing Turn off (`launchctl bootout`) and, from `GET /api/tailnet` at the `loopback` address `byre check` prints, the service's `origin` once its node is `Running`, or else the node's hostname and state, polling every 2 s until the node runs (a node that needs signing in takes the window to the sign-in screen); Off otherwise, showing Turn on, which runs the bundle's `byre install` as the connection screen's Run byre on this Mac does. Below that, Tools on lists every check from the bundle's `byre check` (see "Running as a service"), each Found or Missing, with how to install a missing one: its command in a box with Copy and Run in Terminal, which writes the command to a `.command` file in the temporary directory, run by the user's login shell, and opens it in Terminal, so prompts like sudo's work and the list updates when the app becomes active again; Missing is in the danger colour only when it blocks the server. Run byre on this Mac runs `byre check` first, and when something blocks it lists the missing checks in the same form instead of installing. Both Turn on and Run byre on this Mac leave the service running instead of reinstalling when it is already on, since a reinstall restarts it and the old service could answer the first status check. The app runs both helpers through the user's login shell, so they see the `PATH` the service is given. It reads the state and checks again each time it shows and when the app becomes active. While the service is on, Agents' permissions has Check permissions, which calls `POST /api/admin/permissions` on the service at that address (see "Permissions" under "Screen and app control") and lists each grant as Granted, Missing (with Open System Settings, which opens the grant's pane) or Not checked. Below it, Usage commands has one field per vendor, empty meaning the vendor's own reports, loaded from and saved to that service's `/api/admin/usage-commands` (see "Usage" under "Status"). Settings holds the user's and this client's preferences; its This Mac's permissions has the same Check permissions for the app's own grants, the ones `client_hands` uses, running the bundle's `byre-hands check` from a copy in the temporary directory (`checkLocalPermissions` in `Server.swift`). Which left tab is selected is kept only in memory (`Overlays`); the Lead view stays mounted while another tab shows, and Notes, Workflows, Status, Server or Settings mounts each time its tab is selected. The right shows the focused task, or project with no task: a top row holding the task picker, centred, and the task's preview links at its right end (see "Previews"), then the tab strip, then the selected tab's view with its own header. The tabs are Transcript, Board, Changes, Files, Terminal, Workflow and Project, always in that order. Board shows only on a team task (see "Teams") and Workflow only on a task with a `workflow_run` (see "Workflows"); the selected tab always shows. Tabs that don't fit in the strip move, from the end, into a **More ▾** menu (`MoreButton` in `mac/Sources/Byre/Panes.swift`); the selected tab never moves there. Questions and reviews have no tabs: both live in the transcript (see "Inbox" and "Reviews"), and the status in the transcript's composer bar points at what needs the user there (see "Attention" under "Inbox"). The **Go** menu holds the shortcuts, each also on its Ctrl key outside the terminal. ⌘K opens the task switcher. ⌘P selects the Project tab, keeping the right pane's task. ⌘L selects the Lead tab and puts the cursor in the lead's composer. ⌘S opens the right pane's task's Merge confirmation, from any tab, when the Merge button would be enabled; while the task is `discard_suggested` it opens the Discard confirmation instead. ⌘/ opens a dialog listing every keyboard shortcut; the list is `shortcuts` in `mac/Sources/Byre/Switcher.swift`, kept in step with the menu by hand. The shortcuts do nothing while a dialog is open. ## TODO @@ -462,14 +462,14 @@ An implementer on Claude Code forks its own task with the `fork` MCP tool (`fork Every new snapshot of a task is reviewed by its project's review workflow: each stage of it is a separate reviewer agent session that reads the change and reports on it (`internal/agents/review.go`, `internal/store/review.go`). A project with no review workflow isn't reviewed. A turn that changes no files makes no snapshot and so no review, an interrupted turn makes no snapshot (see "Snapshots"), and a snapshot whose change (the diff below) has no files runs no reviewer: it gets a `reviews` row in state `skipped`, with no session, vendor or model. The snapshot at the end of a Merge turn that finishes into the squash (the turn ended cleanly with `merging_by` still set, read by `turnEnded` before it asks for the snapshot) gets no review and no `reviews` row at all, since the task closes a moment later; a Merge turn that errors or calls `stop_merge` is reviewed as usual. Besides `take_snapshot` (see "Snapshots"), nothing else starts a review, and nothing waits for one: reviews ride along for the human to look at when they want. -- **Review workflows.** A project's review workflow is a saved workflow with `kind: review` (see "Workflows"), named by `projects.review_workflow` (`review_workflow` on the Project, empty for none). It is set with `PUT /api/projects/{projectId}/review-workflow` (`{workflow}`; empty turns reviews off, a task workflow or a missing one is refused), from the Project tab's reviews menu or the lead's `set_review_workflow`, and records `project.updated` with the project. Each stage of the workflow is one reviewer on the stage's `model` (fixed: it doesn't change with the model the task's agent runs), told the stage's `prompt`; a stage that comes after others starts once all of them are `done` and gets their summaries and findings under `## From reviewer ""`. A project's snapshots are reviewed with the workflow as it is when each review stage starts: each `reviews` row keeps the definition it ran from (`reviews.definition`), and later stages of that snapshot follow it. -- **Starting.** After `takeSnapshot` creates a snapshot, `afterSnapshot` runs `startReviews`: with no review workflow it does nothing; a snapshot with no changes against the default branch gets one `skipped` row; a review workflow that is missing or a task workflow gets one `failed` row naming it; otherwise it runs `startReviewer` for each stage the workflow starts with (`workflow.Starts`). When a review finishes `done`, `advanceReviews` starts each stage after it whose earlier stages are all `done` for that snapshot, unless the task closed or a later snapshot has reviews; a failed stage stops the stages after it. A stage that can't start (a model whose vendor's CLI isn't on the service's `PATH`, a checkout that fails) gets a `failed` row with the start error, so a snapshot's reviews never look complete with a stage missing; a failed row with no session shows in the transcript as an error note. `startReviewer` checks the snapshot's commit out detached at `worktrees//review-`, creates an `agent_sessions` row with `role` `reviewer` on the task (created by the task's creator), a `reviews` row (`running`, one per stage per snapshot, its `stage` the stage's name), and sends `reviewPrompt` as its first turn (actor `byre`): the stage's prompt, all of the task's work as `git diff` from the merge-base of the snapshot and the default branch, with its changed files, the task's first message to its agent, and earlier stages' reviews. Every review covers the whole task against the default branch, not only what changed since the previous snapshot. +- **Review workflows.** A project's review workflow is any saved workflow (see "Workflows") whose stages are single agents, named by `projects.review_workflow` (`review_workflow` on the Project, empty for none). It is set with `PUT /api/projects/{projectId}/review-workflow` (`{workflow}`; empty turns reviews off, a missing workflow or one with a team stage is refused, `reviewProblem` in `internal/agents/workflows.go`), from the Project tab's reviews menu or the lead's `set_review_workflow`, and records `project.updated` with the project. Each stage of the workflow is one reviewer on the stage's `model` (fixed: it doesn't change with the model the task's agent runs), told the stage's `prompt`; a stage that comes after others starts once all of them are `done` and gets their summaries and findings under `## From reviewer ""`. A project's snapshots are reviewed with the workflow as it is when each review stage starts: each `reviews` row keeps the definition it ran from (`reviews.definition`), and later stages of that snapshot follow it. +- **Starting.** After `takeSnapshot` creates a snapshot, `afterSnapshot` runs `startReviews`: with no review workflow it does nothing; a snapshot with no changes against the default branch gets one `skipped` row; a review workflow that is missing or has a team stage gets one `failed` row naming it; otherwise it runs `startReviewer` for each stage the workflow starts with (`workflow.Starts`). When a review finishes `done`, `advanceReviews` starts each stage after it whose earlier stages are all `done` for that snapshot, unless the task closed or a later snapshot has reviews; a failed stage stops the stages after it. A stage that can't start (a model whose vendor's CLI isn't on the service's `PATH`, a checkout that fails) gets a `failed` row with the start error, so a snapshot's reviews never look complete with a stage missing; a failed row with no session shows in the transcript as an error note. `startReviewer` checks the snapshot's commit out detached at `worktrees//review-`, creates an `agent_sessions` row with `role` `reviewer` on the task (created by the task's creator), a `reviews` row (`running`, one per stage per snapshot, its `stage` the stage's name), and sends `reviewPrompt` as its first turn (actor `byre`): the stage's prompt, all of the task's work as `git diff` from the merge-base of the snapshot and the default branch, with its changed files, the task's first message to its agent, and earlier stages' reviews. Every review covers the whole task against the default branch, not only what changed since the previous snapshot. - **Running.** Reviewer sessions get `reviewGuidance` and only the `submit_review` tool, whose arguments (`reviewSchema`: `change`, a few sentences of Markdown on what the change does, required and non-empty; a Markdown `summary`, which the guidance and schema require to be exactly "No significant findings." when there are no findings and otherwise a bulleted list with one line per finding; `findings`, each with `severity`, `category`, `title`, `detail`, `path`, `line`, `end_line` and `images`; and top-level `images`) become the review's `result` and mark it `done`. The guidance encourages, without requiring, Mermaid blocks for a data flow, call graph, state machine or schema the change alters, and screenshots when the change alters something visible. -- **Forwarding.** When a submitted review has findings, byre sends them to the task's live agent as a `byre` message (`forwardFindings` and `findingsMessage` in `internal/agents/review.go`): the reviewer's stage name, and each finding's number, severity, title, location and detail, under a preamble asking the agent to check each one against the code and its brief and, before changing anything, answer each with `respond_to_finding` (see "Agent responses"). Each reviewer's findings come in their own message. It goes through `Manager.Message`, so it is queued like any message while the agent is mid-turn. A task with no live agent session gets nothing, and the log says `review findings not forwarded`. Forwarding doesn't set any finding's state; **fix** and **ignore** stay available until the agent or the user answers. +- **Delivery.** A snapshot's review is delivered once its workflow is done: every stage has a finished row and no stage is left to start (`reviewsSettled`), so the agent never starts fixing while a reviewer is still at work, which would supersede it. `reviewFinished` then sends every stage's findings to the task's live agent in one `byre` message (`deliverFindings` and `findingsMessage` in `internal/agents/review.go`): under a `## Reviewer ""` heading per stage with findings, each finding's number, severity, title, location and detail, after a preamble asking the agent to check each one against the code and its brief and, before changing anything, answer each with `respond_to_finding` (see "Agent responses"). Only the finish that settles the review sends it: of the snapshot's reviews, the one with the latest `finished_at` (then the highest id), so two stages finishing at once send one message; the startup passes that rerun `reviewFinished` send nothing. Nothing is sent when no stage found anything, or when a newer snapshot has been taken. It goes through `Manager.Message`, so it is queued like any message while the agent is mid-turn. A task with no live agent session gets nothing, and the log says `review findings not delivered`. Forwarding doesn't set any finding's state; **fix** and **ignore** stay available until the agent or the user answers. - **Screenshots.** Reviewer sessions also get the Playwright MCP server (`playwrightMCP` in `internal/agents/review.go`: `npx @playwright/mcp` at a pinned version, headless Chrome, isolated profile, absolute file paths), passed to Claude as a `stdio` entry in `--mcp-config` and to Codex as `mcp_servers` in the thread's `config`. Its automatically named files go to `worktrees//review--media`, removed with the checkout. Each `images` entry is `{path, caption}`, the path absolute or relative to the checkout; at `submit_review` byre reads every one, refuses the whole submission (nothing recorded) for a missing file or one that isn't one of `store.ImageTypes` within `store.MaxBlobBytes`, then stores each as a blob and replaces its `path` with `blob`, since the files are deleted with the checkout. They run without the Stop-hook bounce, mid-turn delivery, end-of-turn commit, snapshots or turn summaries, and their `user.message` events carry no `head`. When the reviewer's turn ends byre completes its session, kills its process and removes the worktree; a review still `running` then fails with why (no submission, with the final message; an error; an interrupt). A process that exits mid-turn fails the review the same way. `StopReview` ends the reviewer the same way and fails the review; `Watch` calls it for a reviewer whose process the health check finds `stuck` (no stream event for `StuckAfter`), which is how a reviewer whose connection died while the machine slept gets cleared. Reviews are of all the task's work, so a newer one makes older ones moot: once a snapshot's review starts, or is skipped for having no diff, byre fails the task's running reviews of earlier snapshots ("superseded by snapshot N") and ends their reviewers (`failRunningReviews`). Each snapshot's review starts on its own goroutine under `reviewMu`, so they can run out of order; `afterSnapshot` starts no review for a snapshot when the task already has a review of a later one. Merging or discarding the task fails its running reviews ("the task was closed") and ends their reviewers. On startup, a running reviewer resumes like any session, and a running review whose reviewer isn't `running` fails; then every `done` review of an active task's latest reviewed snapshot goes through `reviewFinished` again (`LatestDoneStages`), so a stage whose successors a restart cut off before they started still starts them. - **Events.** `review.started`, `review.finished` (state `done`, `failed` or `skipped`; a skipped review emits only `review.finished`) and `review.finding` (a finding's state set, with the user as actor) carry the whole review, with the task's ids and no `agent_session_id`. A reviewer's own events carry its session id and the task's ids: the transcript draws them inside that reviewer's row (below), not as the task's agent's; the lead's `read_transcript` leaves the reviewer's events out and shows each `review.finished` with its summary and findings (`reviewLine`). `GET /api/tasks/{taskId}/sessions` and everything that looks for the task's agent (`ListSessions`) see only implementer sessions. - **UI.** Reviews live in the task's transcript (`reviewerEvent`, `reviewerState` and `renderReview` in `mac/Sources/Byre/TranscriptBuilder.swift`, rows in `mac/Sources/Byre/Reviews.swift`). Each reviewer session is a folded row at its `session.created` (role `reviewer`), keyed by session id so a snapshot can have several: " · ", then while it runs its latest step (its last tool call, or the first line of its last message) and "running" in the accent colour; once its review finishes, "superseded" or the severity counts ("no findings") muted, or "failed" in the danger colour with the error. Expanding it shows the reviewer's prompt, messages and tool calls read-only, drawn as a subagent's are (runs of tool calls fold into one row); its streamed text isn't shown. A skipped review has no session and no row. A finished review with findings is also a note at its `review.finished` event, " review of snapshot #n:" with its severity counts; the note carries the summary and screenshots, and each finding follows as its own row, numbered from 1 as in the message to the agent. The `change` field is not shown; it goes to the lead and into notes. A finding with `path` and `line` shows, under its title, the lines `line` to `end_line` plus 3 either side from that file's diff for the snapshot against the default branch (`changes?snapshot=`, the diff the reviewer was given), with new-file line numbers and the finding's own lines marked; it shows nothing when those lines aren't in the diff, and "diff not available" with the error once the task is closed. Clicking a screenshot opens it full size. A finding with no answer from the user or the agent has **fix** and **ignore** buttons. **fix** sends the finding's number, severity, title, location and detail to the task's agent as a message from the user: through `POST /api/sessions/{id}/messages` to its latest live session (queued like any message while the agent is mid-turn, see "Agent sessions"), or starting a session when it has none; the view stays where it is, so several findings can be sent in a row. Then it, or **ignore** on its own, records the finding's state (`fixed` or `ignored`) with `PUT /api/reviews/{reviewId}/findings/{index}`, the index being the finding's position in `result.findings`; the review's `finding_states` (the `reviews.finding_states` column, a JSON object from index to state) holds them, and the buttons give way to "fix sent" or "ignored". Only a `done` review's findings take a state; the user can't clear one, and only the agent's answer clears a `fixed` one (below). -- **Agent responses.** The message forwarding findings to the agent numbers them and tells it to answer each with the task agent's `respond_to_finding` MCP tool (`respondToFindingTool` in `internal/agents/review.go`) before changing anything: `snapshot`, `reviewer` (the stage name), `finding` (from 1 in that reviewer's review), `response` (`fixing` or `ignoring`) and a one-line `reason`. byre records it in the review's `finding_responses` (the `reviews.finding_responses` column, a JSON object from index to `{response, reason}`; a later call for the same finding replaces it) and emits `review.finding` with actor `agent`. The answer also clears the finding's `fixed` state, since it answers that fix request: an ignore after the user's **fix** brings **fix** back so the user can overrule it again. A user's `ignored` stays. A call naming a snapshot and reviewer with no `done` review, or a finding it doesn't have, is refused. On the finding, an answer shows as "agent fixing: …" or "agent ignoring: …" and dims the finding's diff, detail and screenshots. Fixing takes away both buttons; ignoring keeps **fix**, so the user can overrule it, and drops **ignore**. The tool call itself is its own transcript row (not folded into a group of tool calls): what the agent answered for which finding, its title and reason, a **show** button that scrolls to the finding, and, for an ignore, the same **fix** button while the user hasn't answered the finding. A call that fails is hidden. The Merge confirmation's review warning counts findings the user hasn't ignored, whatever the agent answered. +- **Agent responses.** The message delivering findings to the agent numbers them and tells it to answer each with the task agent's `respond_to_finding` MCP tool (`respondToFindingTool` in `internal/agents/review.go`) before changing anything: `snapshot`, `reviewer` (the stage name), `finding` (from 1 in that reviewer's review), `response` (`fixing` or `ignoring`) and a one-line `reason`. byre records it in the review's `finding_responses` (the `reviews.finding_responses` column, a JSON object from index to `{response, reason}`; a later call for the same finding replaces it) and emits `review.finding` with actor `agent`. The answer also clears the finding's `fixed` state, since it answers that fix request: an ignore after the user's **fix** brings **fix** back so the user can overrule it again. A user's `ignored` stays. A call naming a snapshot and reviewer with no `done` review, or a finding it doesn't have, is refused. On the finding, an answer shows as "agent fixing: …" or "agent ignoring: …" and dims the finding's diff, detail and screenshots. Fixing takes away both buttons; ignoring keeps **fix**, so the user can overrule it, and drops **ignore**. The tool call itself is its own transcript row (not folded into a group of tool calls): what the agent answered for which finding, its title and reason, a **show** button that scrolls to the finding, and, for an ignore, the same **fix** button while the user hasn't answered the finding. A call that fails is hidden. The Merge confirmation's review warning counts findings the user hasn't ignored, whatever the agent answered. - **State.** Snapshot rows carry no review mark; the reviewer rows show each review. While any of a task's reviews is `running` and its agent is idle, the task's state is `reviewing` (see "Task state in the store"), which the task switcher and the Project tab show as **Reviewing** (see "Task state"). `GET /api/tasks/{taskId}/reviews` lists a task's reviews oldest first. `sqlite3 byre.db "SELECT id, task_id, snapshot_n, stage, vendor, state, error FROM reviews ORDER BY created_at DESC LIMIT 20"` shows recent ones. ## Workflows @@ -477,14 +477,14 @@ Every new snapshot of a task is reviewed by its project's review workflow: each A workflow is a named tree of steps: stages, each a stage name, a model and the stage agent's prompt written out in full, nested in `order` and `parallel` blocks. Every stage runs as its own task, once the step before it has finished, so a stage's output can go to several branches of a `parallel` (fan-out) and the stage after a `parallel` waits for all its branches (fan-in). Workflows live in byre's database, in the `workflows` table (`name`, and the definition's JSON as `definition`), shared by every project and never in a repo. byre stores, validates, exports, imports and freezes them, and runs each run's frozen copy stage by stage (`internal/agents/workflows.go`; the graph helpers `After`, `Next`, `Starts` and `FromMain` are in `internal/workflow/workflow.go`). The human starts one from the task switcher (see "Starting"), or from the left pane's Workflows tab, which lists them and hands a run to the lead (see "UI"); the human edits them in that tab (see "UI"), and the lead with its tools, drawing a workflow for the human as a Mermaid flowchart of its stage names, which its chat renders (see "Inbox"). A run's progress is shown read-only in the stage tasks' Workflow tab (see "Progress" below). - **Names.** The name is the workflow's key and is shown to the human on buttons and pickers, so it is free text: one line of 1 to 100 characters, trimmed (`workflow.CleanName`). It isn't part of the definition, so the file doesn't carry it. In URLs it is a path-escaped segment. -- **File.** One workflow per file: the `WorkflowDefinition` (`api/openapi.yaml`) as YAML, an optional `description`, an optional `kind` and its `steps`, run in order. `kind` is `task` (the default: run on a brief, each stage its own task) or `review` (a project's review workflow, each stage a reviewer of every snapshot, see "Reviews"); a review workflow's stages can't have `agents`, it may end with several stages, and starting it as a run is refused. `GET /api/workflows` gives each workflow's `kind`; the task switcher offers only task workflows, and the Workflows tab marks review ones "reviews snapshots" with no run button. A step (`WorkflowStep`) is exactly one of a `stage` (its name, with `model` or `agents`, and `prompt`), an `order` (steps one after another) or a `parallel` (branches at the same time). A stage gets the outputs of the stages that end the step before it (the last stage of an `order`, the last stage of each branch of a `parallel`; `workflow.After` derives them by walking the tree), and a stage with nothing before it starts from the brief. Where its task branches from follows from that: from the commit its one earlier stage finished at, from the default branch when it comes after several, and for a stage with none, from where the run was started. In place of `model`, a stage may list `agents` (each `name`, `model` and `prompt`) to run as a team in its one task (see "Teams"): each agent gets the stage's message and then its own prompt, and the first agent calls `finish_stage`. `workflow.Format` writes it with multi-line prompts as literal blocks; `workflow.Parse` reads YAML or JSON and refuses unknown fields. The download is named after the workflow with `/\:*?"<>|` replaced by `_` (`workflow.Filename`). -- **Validation.** `workflow.Validate` refuses a definition (400, everything wrong listed, each located by its path such as `steps[0].parallel[1]`) with no steps, a step that isn't exactly one of `stage`, `order` and `parallel`, `model`, `prompt` or `agents` on an `order` or `parallel`, an empty `order` or `parallel`, a stage name that isn't a valid one-line name or repeats another stage's, an empty model without `agents` or a model with them, `agents` that fail `workflow.TeamProblems`, an empty prompt, or a tree that doesn't end with exactly one stage (the final stage, which ends the run). The tree can't express a cycle. A stage's vendor follows from its model (`workflow.VendorOf`, the same rule sessions use: `gpt-*` is Codex, `gemini-*` Antigravity, everything else Claude Code); a file with a `vendor` key is refused as an unknown field. -- **API.** `GET /api/workflows` lists every workflow's name and description. `GET /api/workflows/{name}` downloads its file, or returns the definition as JSON when the request's `Accept` names `application/json` and no YAML type; `PUT` takes a file (YAML or JSON) as the body and creates or replaces the workflow; `DELETE` removes it. Each save or delete records a `workflow.updated` or `workflow.deleted` event carrying the name. `sqlite3 byre.db 'SELECT name FROM workflows'` lists them. Saves and reads go through the Manager (`internal/agents/workflows.go`) so the HTTP API and the lead's tools share them. +- **File.** One workflow per file: the `WorkflowDefinition` (`api/openapi.yaml`) as YAML, an optional `description` and its `steps`, run in order. Any workflow can run on a brief, each stage its own task, and any whose stages are single agents can be a project's review workflow, each stage a reviewer of every snapshot (see "Reviews"). A step (`WorkflowStep`) is exactly one of a `stage` (its name, with `model` or `agents`, and `prompt`), an `order` (steps one after another) or a `parallel` (branches at the same time). A stage gets the outputs of the stages that end the step before it (the last stage of an `order`, the last stage of each branch of a `parallel`; `workflow.After` derives them by walking the tree), and a stage with nothing before it starts from the brief. Where its task branches from follows from that: from the commit its one earlier stage finished at, from the default branch when it comes after several, and for a stage with none, from where the run was started. In place of `model`, a stage may list `agents` (each `name`, `model` and `prompt`) to run as a team in its one task (see "Teams"): each agent gets the stage's message and then its own prompt, and the first agent calls `finish_stage`. `workflow.Format` writes it with multi-line prompts as literal blocks; `workflow.Parse` reads YAML or JSON and refuses unknown fields. The download is named after the workflow with `/\:*?"<>|` replaced by `_` (`workflow.Filename`). +- **Validation.** `workflow.Validate` refuses a definition (400, everything wrong listed, each located by its path such as `steps[0].parallel[1]`) with no steps, a step that isn't exactly one of `stage`, `order` and `parallel`, `model`, `prompt` or `agents` on an `order` or `parallel`, an empty `order` or `parallel`, a stage name that isn't a valid one-line name or repeats another stage's, an empty model without `agents` or a model with them, `agents` that fail `workflow.TeamProblems`, or an empty prompt. A workflow may end with several stages (a last `parallel`). The tree can't express a cycle. A stage's vendor follows from its model (`workflow.VendorOf`, the same rule sessions use: `gpt-*` is Codex, `gemini-*` Antigravity, everything else Claude Code); a file with a `vendor` key is refused as an unknown field. +- **API.** `GET /api/workflows` lists every workflow's name and description. `GET /api/workflows/{name}` downloads its file, or returns the definition as JSON when the request's `Accept` names `application/json` and no YAML type; `PUT` takes a file (YAML or JSON) as the body and creates or replaces the workflow; `DELETE` removes it and turns reviews off (a `project.updated` event each) for the projects that reviewed with it. Each save or delete records a `workflow.updated` or `workflow.deleted` event carrying the name. `sqlite3 byre.db 'SELECT name FROM workflows'` lists them. Saves and reads go through the Manager (`internal/agents/workflows.go`) so the HTTP API and the lead's tools share them. - **Lead tools.** `list_workflows`, `read_workflow` (`name`, or `task_id` for a task's frozen copy), `save_workflow` (`name` and `definition`, the file as YAML) and `delete_workflow`, with the same checks and errors as the API. Task agents don't get them. - **Starting.** The lead's `create_task` and `start_todo` take an optional `workflow` (404 when there is none of that name) and then start a run of it instead of an agent on the prompt (`Manager.StartWorkflow`); `model` together with `workflow` is refused, since each stage names its own. `POST /api/projects/{id}/tasks` takes the same optional `workflow`, with the title, required then, as the run's brief, and returns the run's first stage task. The task switcher (`mac/Sources/Byre/Switcher.swift`) loads `GET /api/workflows` when it opens, and when the message (after any project prefix) starts with a workflow's name, ignoring case, followed by a space or a colon and more text (the longest such name when several match), it offers a "Start in …" row before each "Start in …" row: `deploy: byre` or `deploy byre` starts a run of `deploy` with `byre` as its title and brief, and opens its first stage task. Text that starts with no workflow's name gets no workflow rows. The switcher's box grows with what is typed (Shift+Enter adds a line) and takes pasted or dropped images; with images attached it lists only the "Start in …" rows, and "Start in …" uploads them as blobs and sends their ids as `POST /api/projects/{id}/tasks`'s `images`, which start the agent with or without a `prompt` (refused with `workflow`). Closing the switcher drops its images. A run gets a new id and creates a task for every stage with nothing before it (`workflow.Starts`), branched from the lead's `from_task_id` HEAD when given, else from the default branch. Each stage task copies the workflow's name and definition as they are when the run starts into `tasks.workflow_name` and `tasks.workflow_definition`, so later saves or deletes don't change it; its brief into `tasks.workflow_brief` (the lead's `create_task` `prompt`, optional with a workflow and may be empty, or for `start_todo` the TODO item as `todoPrompt` words it); the run's title into `tasks.workflow_title`; the run id into `tasks.workflow_run`; and its stage's name into `tasks.workflow_stage`. Stage tasks are titled `: <stage>`, with the run's title as their merge title, and carry the run's TODO item. The Task's `workflow`, `workflow_run` and `workflow_stage` are those; `GET /api/tasks/{id}/workflow` returns the copy as `{name, definition}` with the definition as YAML (404 for a task without one). A task agent's `create_subtask`, `POST /api/projects/{id}/tasks` without `workflow` and the Project tab's Start create tasks without one. A fork of a stage task copies its frozen workflow, brief, title, run and stage, so it is another attempt at the same stage of the same run. `sqlite3 byre.db "SELECT id, status, workflow_stage, stage_output != '' FROM tasks WHERE workflow_run = '<run>'"` lists a run's tasks and which have finished. - **Stages.** A stage task's agent is an implementer session on the stage's model (for a team stage, one per agent, see "Teams"), created by the user who started the run; any later session of the task (a Compact handoff's, one started by a message) runs the same stage, since the stage belongs to the task. Starting a stage records a `task.workflow_stage` event on its task (`workflow`, `run`, `stage` the name, `after` the ids of the tasks whose outputs it gets; actor the lead for the stages a run starts with, `byre` after) and sends the session its first message, actor `byre`, so the transcript shows it collapsed under its first line: `Workflow "<name>", stage "<stage>"`, then the stage's prompt, a `## Brief` section when the brief isn't empty, a `## From stage "<stage>" (task <id>, branch <branch>)` section with each `after` stage's output, and a line saying where the task starts: from the default branch, with the branches above to `git merge` or read as it needs, or from the commit its one `after` stage finished at. Nothing else of earlier stages is passed on; their branches carry their work. -- **Advancing.** A stage session gets the `finish_stage` tool (`output`, required) and a guidance line naming the workflow, its stage and the stages after it. The call stores the output in `agent_sessions.stage_output`, replacing any earlier one; it is refused once the task's stage has finished. When a turn of that session ends normally (not an error or interrupt, not a handoff, not a Merge turn that squashes) with an output stored, byre sets `agent_sessions.stage_finished` before marking the session idle, and then advances (`nextStage`): it records the output and the worktree's HEAD in `tasks.stage_output` and `tasks.stage_commit`, then, holding `workflowMu` so two stages finishing at once can't both start the one after them, goes through each stage after this one. A stage starts once every stage it comes after has a finished task in the run (the latest one, when a fork made several); its task branches from that task's `stage_commit` when it comes after one stage, else from the default branch. Once every stage after it has been dealt with, byre clears `stage_output` and `stage_finished` on the session. The session stays idle in its task for more messages, and queued messages and held answers are delivered to it once advancing is done. When the final stage finishes, byre starts discarding every other active task of the run (as `byre`, through the same `Discard` as the button, so each gets its summary turn first and closes when that turn ends; one already merging or being discarded is skipped and logged as `stage task not discarded`), then records `task.workflow_finished` (`workflow`, `output`) on the final task and clears the flags in the same transaction, leaving the final task for the human to merge. A turn that ends without the call leaves the task in its stage. The output belongs to the turn that stored it: when that turn ends any other way (an error, an interrupt, a handoff, a Merge turn that squashes, an agent process that exits mid-turn, or a session that can't be resumed after a restart), byre clears `stage_output` before marking the session idle, so a later turn can't hand off a stale output; the agent calls `finish_stage` again. A turn cut off by a byre restart and resumed keeps it. If a stage can't start, byre logs `advance workflow stage` and records the error in the finishing task's transcript, and the flag stays set. On startup, before resuming running sessions, `recoverStages` advances every session with `stage_finished` set, skipping the steps already done: the recorded output isn't overwritten; a stage that already has a task in the run isn't created again, but when that task is active and has no session, its sessions are started, and each of its sessions that never got a message (`SessionStarted`) is sent the stage's first message (a team member's with its own prompt), and the restart notice isn't sent to them. `sqlite3 byre.db 'SELECT id, task_id FROM agent_sessions WHERE stage_finished = 1'` lists stages waiting to advance. `list_tasks` shows each stage task's workflow and `stage "<name>"`, with `(finished)` once its stage has finished and `workflow finished` once any task of the run has a `task.workflow_finished` event. Nothing reads or writes `agent_sessions.workflow_stage`; it stays in the schema so builds from older branches keep working against the same database. -- **Progress.** `GET /api/tasks/{id}/workflow/run` (`Manager.WorkflowRun`) returns the run of a stage task (404 for a task without one): the definition that task froze (typed, not YAML), name, title, brief, `finished` (any task of the run has `task.workflow_finished`), and per stage in the tree's order its `after` (the stages whose outputs it gets, from `workflow.After`), `waiting_on` (the `after` stages with no finished attempt), its `attempts` and a `state`. An attempt is every task of the run at that stage, closed ones included, oldest first (the first and its forks), with its `output` and `commit` once `tasks.stage_output` is set, `forked_from`, `discarded_by` (the `task.discarded` actor) and `inputs`: the tasks named in the `after` of its latest `task.workflow_stage` event, each with its stage, so it says which attempt fed it; a fork has no such event and takes its source's. An attempt's state is `merged`, `cleaned_up` (discarded after finishing; `byre` as `discarded_by` is the final stage's cleanup), `discarded` (before finishing), `finished`, `running` (a session is running) or `idle`. A stage is `waiting` with no attempts, else it takes the state of its latest finished attempt, or its latest attempt when none finished. The Workflow tab (`mac/Sources/Byre/Workflow.swift`) draws the graph natively with the flowchart layout from `Diagram.swift`, each stage's node coloured by its state (`stateColors`, from the palette's tint and accent pairs: waiting grey, running accent, idle warn, finished, merged and cleaned up ok, discarded danger) and redrawn whenever a refetch changes a state; then a card per stage with its state, its attempts linking to their tasks, each attempt's inputs ("started from") and the attempts it fed, and its output collapsed as Markdown. It refetches on any stored `task.*` or `session.*` event of a task in the run, and on a `task.created` whose payload carries the run, so a new stage or fork shows up. +- **Advancing.** A stage session gets the `finish_stage` tool (`output`, required) and a guidance line naming the workflow, its stage and the stages after it. The call stores the output in `agent_sessions.stage_output`, replacing any earlier one; it is refused once the task's stage has finished. When a turn of that session ends normally (not an error or interrupt, not a handoff, not a Merge turn that squashes) with an output stored, byre sets `agent_sessions.stage_finished` before marking the session idle, and then advances (`nextStage`): it records the output and the worktree's HEAD in `tasks.stage_output` and `tasks.stage_commit`, then, holding `workflowMu` so two stages finishing at once can't both start the one after them, goes through each stage after this one. A stage starts once every stage it comes after has a finished task in the run (the latest one, when a fork made several); its task branches from that task's `stage_commit` when it comes after one stage, else from the default branch. Once every stage after it has been dealt with, byre clears `stage_output` and `stage_finished` on the session. The session stays idle in its task for more messages, and queued messages and held answers are delivered to it once advancing is done. The workflow is finished once every stage has: when a last stage (one with nothing after it, `workflow.Ends`) finishes, byre checks that every last stage has a finished task in the run, and until they all do it only clears the flags. Since each stage records its result before taking `workflowMu`, two last stages finishing at once could both see the run complete, so under the lock a run that already has `task.workflow_finished` only clears the flags too. When the last of them finishes, byre starts discarding every other active task of the run, keeping each last stage's finished task (as `byre`, through the same `Discard` as the button, so each gets its summary turn first and closes when that turn ends; one already merging or being discarded is skipped and logged as `stage task not discarded`), then records `task.workflow_finished` (`workflow`, `output`, and `kept`, the kept tasks' ids) on the task that finished last and clears the flags in the same transaction, leaving the kept tasks for the human to merge; each of them is then asked whether it is ready to merge. With one last stage `output` is its output; with several, each one's under a `## <stage>` heading. A turn that ends without the call leaves the task in its stage. The output belongs to the turn that stored it: when that turn ends any other way (an error, an interrupt, a handoff, a Merge turn that squashes, an agent process that exits mid-turn, or a session that can't be resumed after a restart), byre clears `stage_output` before marking the session idle, so a later turn can't hand off a stale output; the agent calls `finish_stage` again. A turn cut off by a byre restart and resumed keeps it. If a stage can't start, byre logs `advance workflow stage` and records the error in the finishing task's transcript, and the flag stays set. On startup, before resuming running sessions, `recoverStages` advances every session with `stage_finished` set, skipping the steps already done: the recorded output isn't overwritten; a stage that already has a task in the run isn't created again, but when that task is active and has no session, its sessions are started, and each of its sessions that never got a message (`SessionStarted`) is sent the stage's first message (a team member's with its own prompt), and the restart notice isn't sent to them. `sqlite3 byre.db 'SELECT id, task_id FROM agent_sessions WHERE stage_finished = 1'` lists stages waiting to advance. `list_tasks` shows each stage task's workflow and `stage "<name>"`, with `(finished)` once its stage has finished and `workflow finished` once any task of the run has a `task.workflow_finished` event. Nothing reads or writes `agent_sessions.workflow_stage`; it stays in the schema so builds from older branches keep working against the same database. +- **Progress.** `GET /api/tasks/{id}/workflow/run` (`Manager.WorkflowRun`) returns the run of a stage task (404 for a task without one): the definition that task froze (typed, not YAML), name, title, brief, `finished` (any task of the run has `task.workflow_finished`), and per stage in the tree's order its `after` (the stages whose outputs it gets, from `workflow.After`), `waiting_on` (the `after` stages with no finished attempt), its `attempts` and a `state`. An attempt is every task of the run at that stage, closed ones included, oldest first (the first and its forks), with its `output` and `commit` once `tasks.stage_output` is set, `forked_from`, `discarded_by` (the `task.discarded` actor) and `inputs`: the tasks named in the `after` of its latest `task.workflow_stage` event, each with its stage, so it says which attempt fed it; a fork has no such event and takes its source's. An attempt's state is `merged`, `cleaned_up` (discarded after finishing; `byre` as `discarded_by` is the run's cleanup when it finished), `discarded` (before finishing), `finished`, `running` (a session is running) or `idle`. A stage is `waiting` with no attempts, else it takes the state of its latest finished attempt, or its latest attempt when none finished. The Workflow tab (`mac/Sources/Byre/Workflow.swift`) draws the graph natively with the flowchart layout from `Diagram.swift`, each stage's node coloured by its state (`stateColors`, from the palette's tint and accent pairs: waiting grey, running accent, idle warn, finished, merged and cleaned up ok, discarded danger) and redrawn whenever a refetch changes a state; then a card per stage with its state, its attempts linking to their tasks, each attempt's inputs ("started from") and the attempts it fed, and its output collapsed as Markdown. It refetches on any stored `task.*` or `session.*` event of a task in the run, and on a `task.created` whose payload carries the run, so a new stage or fork shows up. ## Events diff --git a/internal/agents/afterreview.go b/internal/agents/afterreview.go index aa354a8..76a976a 100644 --- a/internal/agents/afterreview.go +++ b/internal/agents/afterreview.go @@ -85,11 +85,11 @@ func (m *Manager) recoverMergeAfterReviews(ctx context.Context) { return } for _, id := range ids { - m.reviewFinished(id) + m.reviewFinished(id, false) } } -func (m *Manager) reviewFinished(reviewID string) { +func (m *Manager) reviewFinished(reviewID string, deliver bool) { ctx := context.Background() r, err := m.store.GetReview(ctx, reviewID) if err != nil { @@ -112,6 +112,9 @@ func (m *Manager) reviewFinished(reviewID string) { if !settled { return } + if deliver { + m.deliverFindings(ctx, r, reviews) + } ids := []string{} for _, sr := range reviews { ids = append(ids, sr.Id) diff --git a/internal/agents/kb/workflow-files.md b/internal/agents/kb/workflow-files.md index 13875e2..66eb538 100644 --- a/internal/agents/kb/workflow-files.md +++ b/internal/agents/kb/workflow-files.md @@ -1,8 +1,8 @@ # Workflow files -A workflow's file is YAML: an optional `description`, an optional `kind`, then `steps`, which run in order. The workflow's name is not in the file. Unknown fields are refused. +A workflow's file is YAML: an optional `description`, then `steps`, which run in order. The workflow's name is not in the file. Unknown fields are refused. -`kind` is `task` (the default) or `review`. A task workflow runs on a brief, each stage as its own task. A review workflow is what a project names (with `set_review_workflow`) to review its tasks' snapshots: each stage is one reviewer agent on its `model`, given the task's change and its stage's `prompt` (what to look for); a stage after others also gets their reviews. Its stages can't use `agents`, and it may end with several stages. +Any workflow can run on a brief, each stage as its own task, and any workflow whose stages are single agents can be a project's review workflow (set with `set_review_workflow`), which reviews its tasks' snapshots: each stage is then one reviewer agent on its `model`, given the task's change and its stage's `prompt` (what to look for), and a stage after others also gets their reviews. A project can't review with a workflow that has a team stage (`agents`). Each step is exactly one of: @@ -20,7 +20,7 @@ A stage gets the outputs of the stages that end the step before it: the last sta | `prompt` | The stage agent's instructions. | | `agents` | In place of `model`: a list of at least two, each with `name`, `model` and `prompt`, that runs the stage as a team. They work in the stage's one task and worktree and talk through a shared board; each gets the stage's `prompt` and then its own, and the first agent finishes the stage. | -Where a stage's task branches from follows from the shape. A stage after exactly one stage starts from the commit that stage finished at. A stage after a `parallel` starts from the default branch and is told the branches it can merge. A stage with nothing before it starts where the workflow was started. The workflow must end with one stage, which finishes it. An `order` or `parallel` can't be empty. +Where a stage's task branches from follows from the shape. A stage after exactly one stage starts from the commit that stage finished at. A stage after a `parallel` starts from the default branch and is told the branches it can merge. A stage with nothing before it starts where the workflow was started. A workflow is finished once every stage has finished; when it ends with a `parallel`, each branch's last stage keeps its task to merge, and the run's other stage tasks are discarded. A review is delivered to the task's agent, every stage's findings in one message, once every stage has reviewed. An `order` or `parallel` can't be empty. ```yaml description: Build a change, review and test it in parallel, then fix what they found. diff --git a/internal/agents/lead.go b/internal/agents/lead.go index adcde0b..4a7499f 100644 --- a/internal/agents/lead.go +++ b/internal/agents/lead.go @@ -29,11 +29,11 @@ const leadGuidance = `You are the human's lead agent in byre, shown in the left - Questions from task agents: answer one yourself with answer_question only when the answer is clear, for example the agent's recommendation is plainly right given what the human has said, the human already decided it, or one of your notes settles it outright. Otherwise show it to the human with your recommendation and wait. When the human tells you an answer, give it with answer_question. Say which questions you answered and how, naming the note by its id when one decided it. - Notes: you learn how the human works and keep it with remember, forget and list_notes. byre adds every note to your instructions at the start of each lead session, so they outlast compaction; the human can read and delete them in the Settings tab. Write a note when the human shows a priority, a preference or the reasoning behind a choice that will recur: an answer that departs from the agent's recommendation (the answered signal shows both), a correction or overrule of you or a task agent, or a statement of how they want things done. Record the choice and the why, in their terms, general enough to apply to the next case and specific enough to act on, for example "Prefers replacing a state label over adding a tag beside it: keeps the picker uncluttered." File each note under the project it is about with project_id when it concerns that project's code, UI, docs or process, and with no project_id only when it holds for how the human works everywhere; apply a project's notes only to that project's tasks. Task agents start with their creator's every-project notes and their project's, and suggest new ones with suggest_note, which reaches you as a signal: save a suggestion only when it holds and your notes don't already cover it, choosing its project yourself, and otherwise drop it; neither needs a word to the human. Don't note one-off facts about a single task, and don't duplicate: update an existing note with remember and its id instead. When the human contradicts a note, update it or forget it. Before recommending anything, check your notes, and when one shaped the recommendation, cite its id. - Finished turns: read them and show the human only what matters: work finished and ready to merge, problems, decisions made. Stay quiet about routine progress. -- Reviews: after each new snapshot of a task, the stages of its project's review workflow (a workflow of kind review, set per project with set_review_workflow) review the change, each a reviewer agent on its stage's model; a stage after others gets their reviews. A project with no review workflow isn't reviewed. The results are in the task's transcript, where its agent answers each finding with respond_to_finding (fixing, or ignoring with a reason), and read_transcript shows each review's findings. They are for the human to look at when they want; mention one only when its findings matter to what the human is deciding. +- Reviews: after each new snapshot of a task, the stages of its project's review workflow (any saved workflow, set per project with set_review_workflow) review the change, each a reviewer agent on its stage's model; a stage after others gets their reviews. A project with no review workflow isn't reviewed. Once every stage has reviewed the snapshot, the task's agent gets all their findings in one message. The results are in the task's transcript, where its agent answers each finding with respond_to_finding (fixing, or ignoring with a reason), and read_transcript shows each review's findings. They are for the human to look at when they want; mention one only when its findings matter to what the human is deciding. - Dig into context with list_tasks, read_transcript, read_inbox and read_status (vendor rate limits and agent health), and with your own Read, Grep and Bash on a task's worktree path (from list_tasks). Don't edit files in task worktrees; ask the task's agent with send_message. - You can create tasks, start TODO items, message, interrupt and compact task agents, and merge or discard tasks. Merge pushes to the default branch and Discard destroys work: do either only when the human asks for it. - Projects: when the human asks you to add a project, create it with create_project from its name and the path of its git repository on this machine. -- Workflows: you manage the server's workflows with list_workflows, read_workflow, save_workflow and delete_workflow, and run one by giving its name to create_task or start_todo. A workflow is a tree of steps run in order, each a stage, an order or a parallel; every stage runs as its own task on the stage's model once the step before it has finished, so stages fan out (a parallel after a stage) and fan in (a stage after a parallel); a stage's agent calls finish_stage to hand its output to the stages after it, signals tell you when a stage starts and when the workflow finishes, and when the final stage finishes byre discards the run's other stage tasks, leaving the final one to merge. Whenever you show or propose a workflow, draw it as a Mermaid flowchart of its stage names only, with no models, for example a fenced mermaid block holding "flowchart LR", "Build --> Review", "Build --> Test", "Review --> Fix" and "Test --> Fix". +- Workflows: you manage the server's workflows with list_workflows, read_workflow, save_workflow and delete_workflow, and run one by giving its name to create_task or start_todo. A workflow is a tree of steps run in order, each a stage, an order or a parallel; every stage runs as its own task on the stage's model once the step before it has finished, so stages fan out (a parallel after a stage) and fan in (a stage after a parallel); a stage's agent calls finish_stage to hand its output to the stages after it, signals tell you when a stage starts and when the workflow finishes, and the workflow finishes once every stage has; byre then discards the run's other stage tasks, leaving the task of each last stage (one per branch when it ends in a parallel) to merge. Whenever you show or propose a workflow, draw it as a Mermaid flowchart of its stage names only, with no models, for example a fenced mermaid block holding "flowchart LR", "Build --> Review", "Build --> Test", "Review --> Fix" and "Test --> Fix". - Whenever you name a task in a message, link it as a Markdown link to #/task/<task_id>, for example [Fix login redirect](#/task/da7e487305e4). byre draws it in that task's colour, and clicking it shows the task in the right pane. To open a specific tab, append it: #/task/<task_id>/<tab>. When you point the human at a question to answer or a finished turn, link to #/task/<task_id>/transcript. - Keep messages short; the human reads them in half the screen. Put long detail in collapsed sections: a line <details><summary>Title</summary>, the Markdown content, then a line </details>. - You are long-running. Your context window is large. byre sends you a signal once your context passes a quarter of its window; then call compact_self at a logical point (a piece of work wrapped up, a topic closed), and not before; when the turn ends, byre replaces you with a fresh session that starts from your handoff. @@ -363,7 +363,10 @@ func (m *Manager) Observe(events []api.Event) { go m.promptReady(*ev.TaskId, ev.Payload["from"]) } if ev.Kind == "task.workflow_finished" && ev.TaskId != nil { - go m.promptReady(*ev.TaskId, nil) + kept, _ := ev.Payload["kept"].([]string) + for _, id := range kept { + go m.promptReady(id, nil) + } } switch ev.Kind { case "inbox.created", "inbox.updated", "task.created", "task.merge_queued", "task.merge_after_review", "task.merge_after_review_cancelled", "task.merge_after_review_dropped", "task.merge_started", "task.merged", "task.discarded", "turn.ended", "task.workflow_stage", "task.workflow_finished", "review.started", "review.finished", "task.state", "task.ready", "task.discard_suggested": diff --git a/internal/agents/review.go b/internal/agents/review.go index b5449cd..70ab8d5 100644 --- a/internal/agents/review.go +++ b/internal/agents/review.go @@ -196,8 +196,8 @@ func (m *Manager) startReviews(ctx context.Context, task api.Task, snap api.Snap } def, err := m.store.GetWorkflow(ctx, project.ReviewWorkflow) - if err == nil && def.Kind != api.WorkflowKindReview { - err = fmt.Errorf("it is a task workflow, not a review workflow") + if err == nil { + err = reviewProblem(def) } if err != nil { return m.store.CreateReview(ctx, api.Review{ @@ -310,7 +310,7 @@ func (m *Manager) startReviewer(ctx context.Context, task api.Task, project api. slog.Error("record review that didn't start", "task", task.Id, "stage", stage, "err", rerr) return } - go m.reviewFinished(reviewID) + go m.reviewFinished(reviewID, true) }() prompt, err := m.reviewPrompt(ctx, task, project, snap, def, stage, inputs) @@ -472,66 +472,81 @@ func (m *Manager) submitReview(ctx context.Context, l *live, raw json.RawMessage if err != nil { return "", err } - if len(findings) > 0 { - go m.forwardFindings(l.taskID, l.reviewID) - } - go m.reviewFinished(l.reviewID) + go m.reviewFinished(l.reviewID, true) return "Review recorded. End your turn now with one line.", nil } -func (m *Manager) forwardFindings(taskID, reviewID string) { - ctx := context.Background() - r, err := m.store.GetReview(ctx, reviewID) - if err != nil { - slog.Error("load review to forward", "review", reviewID, "err", err) +func (m *Manager) deliverFindings(ctx context.Context, r api.Review, reviews []api.Review) { + last := r + for _, sr := range reviews { + if sr.FinishedAt != nil && last.FinishedAt != nil && (sr.FinishedAt.After(*last.FinishedAt) || sr.FinishedAt.Equal(*last.FinishedAt) && sr.Id > last.Id) { + last = sr + } + } + if last.Id != r.Id { return } - s, found, err := m.liveSession(ctx, taskID) + latest, err := m.store.LastReviewable(ctx, r.TaskId) + if err != nil || latest.N != r.SnapshotN { + return + } + text := findingsMessage(r.SnapshotN, reviews) + if text == "" { + return + } + s, found, err := m.liveSession(ctx, r.TaskId) if err != nil { - slog.Error("list sessions to forward review", "review", reviewID, "err", err) + slog.Error("list sessions to deliver findings", "task", r.TaskId, "err", err) return } if !found { - slog.Info("review findings not forwarded, task has no live agent", "review", reviewID, "task", taskID) + slog.Info("review findings not delivered, task has no live agent", "task", r.TaskId, "snapshot", r.SnapshotN) return } - _, _, err = m.Message(ctx, s.Id, "byre", findingsMessage(r), "", "") + _, _, err = m.Message(ctx, s.Id, "byre", text, "", "") if err != nil { - slog.Error("forward review findings", "review", reviewID, "session", s.Id, "err", err) + slog.Error("deliver review findings", "task", r.TaskId, "snapshot", r.SnapshotN, "session", s.Id, "err", err) return } - slog.Info("review findings forwarded", "review", reviewID, "session", s.Id) + slog.Info("review findings delivered", "task", r.TaskId, "snapshot", r.SnapshotN, "session", s.Id) } -const findingsPreamble = "A reviewer looked at snapshot #" +const findingsPreamble = "The review of snapshot #" func isFindingsMessage(actor, text string) bool { return actor == "byre" && strings.HasPrefix(text, findingsPreamble) } -func findingsMessage(r api.Review) string { - res := map[string]any{} - if r.Result != nil { - res = *r.Result - } +func findingsMessage(n int, reviews []api.Review) string { b := &strings.Builder{} - fmt.Fprintf(b, findingsPreamble+"%d of your work as reviewer %q and raised the findings below. Reviewers can be wrong or miss context: check each one against the code and your brief. "+ - "Before you change anything, answer each finding with respond_to_finding (snapshot %d, reviewer %q and the finding's number): fixing if it holds up and you'll act on it, ignoring with your reason if you're leaving it. "+ - "The human sees your answers on the findings and can overrule an ignore.", r.SnapshotN, r.Stage, r.SnapshotN, r.Stage) - findings, _ := res["findings"].([]any) - for i, f := range findings { - fm, _ := f.(map[string]any) - where := "" - if p, ok := fm["path"].(string); ok && p != "" { - where = p - if line, ok := fm["line"].(float64); ok && line > 0 { - where += fmt.Sprintf(":%d", int(line)) - } - where = " (`" + where + "`)" + for _, r := range reviews { + if r.State != api.ReviewStateDone || r.Result == nil { + continue + } + findings, _ := (*r.Result)["findings"].([]any) + if len(findings) == 0 { + continue + } + fmt.Fprintf(b, "\n\n## Reviewer %q", r.Stage) + for i, f := range findings { + fm, _ := f.(map[string]any) + where := "" + if p, ok := fm["path"].(string); ok && p != "" { + where = p + if line, ok := fm["line"].(float64); ok && line > 0 { + where += fmt.Sprintf(":%d", int(line)) + } + where = " (`" + where + "`)" + } + fmt.Fprintf(b, "\n\n**Finding %d: [%v] %v**%s\n\n%s", i+1, fm["severity"], fm["title"], where, strings.TrimSpace(fmt.Sprint(fm["detail"]))) } - fmt.Fprintf(b, "\n\n**Finding %d: [%v] %v**%s\n\n%s", i+1, fm["severity"], fm["title"], where, strings.TrimSpace(fmt.Sprint(fm["detail"]))) } - return b.String() + if b.Len() == 0 { + return "" + } + return fmt.Sprintf(findingsPreamble+"%d of your work has finished, and its reviewers raised the findings below. Reviewers can be wrong or miss context: check each one against the code and your brief. "+ + "Before you change anything, answer each finding with respond_to_finding (snapshot %d, the reviewer's name and the finding's number): fixing if it holds up and you'll act on it, ignoring with your reason if you're leaving it. "+ + "The human sees your answers on the findings and can overrule an ignore.", n, n) + b.String() } type findingResponseArgs struct { @@ -722,7 +737,7 @@ func (m *Manager) failReview(ctx context.Context, reviewID, reason string) { return } slog.Info("review failed", "review", reviewID, "reason", reason) - go m.reviewFinished(reviewID) + go m.reviewFinished(reviewID, true) } func (m *Manager) cancelReviews(ctx context.Context, task api.Task, project api.Project) { @@ -824,7 +839,7 @@ func (m *Manager) recoverReviews(ctx context.Context) error { return err } for _, id := range done { - go m.reviewFinished(id) + go m.reviewFinished(id, false) } return nil } diff --git a/internal/agents/workflows.go b/internal/agents/workflows.go index 5943399..13904bb 100644 --- a/internal/agents/workflows.go +++ b/internal/agents/workflows.go @@ -15,7 +15,7 @@ import ( "g.fc.run/ai/byre/internal/workflow" ) -func (m *Manager) SaveWorkflow(ctx context.Context, name string, file []byte, actor string) (api.WorkflowSummary, error) { +func (m *Manager) SaveWorkflow(ctx context.Context, name, from string, file []byte, actor string) (api.WorkflowSummary, error) { name, err := workflow.CleanName(name) if err != nil { return api.WorkflowSummary{}, err @@ -24,15 +24,14 @@ func (m *Manager) SaveWorkflow(ctx context.Context, name string, file []byte, ac if err != nil { return api.WorkflowSummary{}, err } - err = m.store.PutWorkflow(ctx, name, def, actor) + err = m.store.PutWorkflow(ctx, name, from, def, actor) + if errors.Is(err, store.ErrWorkflowExists) { + return api.WorkflowSummary{}, fmt.Errorf("%w: %w", ErrConflict, err) + } if err != nil { return api.WorkflowSummary{}, err } - kind := api.WorkflowSummaryKindTask - if def.Kind == api.WorkflowKindReview { - kind = api.WorkflowSummaryKindReview - } - return api.WorkflowSummary{Name: name, Description: def.Description, Kind: kind}, nil + return api.WorkflowSummary{Name: name, Description: def.Description}, nil } func (m *Manager) SetReviewWorkflow(ctx context.Context, projectID, name, actor string) (api.Project, error) { @@ -42,13 +41,23 @@ func (m *Manager) SetReviewWorkflow(ctx context.Context, projectID, name, actor if err != nil { return api.Project{}, err } - if def.Kind != api.WorkflowKindReview { - return api.Project{}, fmt.Errorf("%w: workflow %q is a task workflow; a project reviews with a workflow of kind review", ErrInvalid, name) + err = reviewProblem(def) + if err != nil { + return api.Project{}, fmt.Errorf("%w: workflow %q can't review: %w", ErrInvalid, name, err) } } return m.store.SetReviewWorkflow(ctx, projectID, name, actor) } +func reviewProblem(def api.WorkflowDefinition) error { + for _, s := range workflow.Stages(def) { + if s.Agents != nil { + return fmt.Errorf("stage %q is a team, and each review stage is one reviewer", s.Stage) + } + } + return nil +} + func (m *Manager) WorkflowFile(ctx context.Context, name string) ([]byte, error) { def, err := m.store.GetWorkflow(ctx, name) if err != nil { @@ -230,9 +239,6 @@ func (m *Manager) StartWorkflow(ctx context.Context, project api.Project, creato if err != nil { return nil, err } - if frozen.Definition.Kind == api.WorkflowKindReview { - return nil, fmt.Errorf("%w: workflow %q is a review workflow, which a project names to review its snapshots; it doesn't run on a brief", ErrInvalid, name) - } frozen.Brief = brief frozen.Title = title @@ -405,11 +411,40 @@ func (m *Manager) nextStage(ctx context.Context, sessionID string) ([]string, er next := workflow.Next(frozen.Definition, *task.WorkflowStage) if len(next) == 0 { - err = m.discardStages(ctx, task) + finished, err := m.store.WorkflowFinished(ctx, *task.WorkflowRun) if err != nil { return nil, err } - return nil, m.store.FinishWorkflow(ctx, task, sess.Id, frozen.Name, output) + if finished { + return nil, m.store.ClearStage(ctx, sess.Id) + } + tasks, err := m.store.RunTasks(ctx, *task.WorkflowRun) + if err != nil { + return nil, err + } + keep := map[string]bool{} + kept := []string{} + outputs := []string{} + for _, end := range workflow.Ends(frozen.Definition) { + p, found, err := m.finishedStage(ctx, tasks, end) + if err != nil { + return nil, err + } + if !found { + return nil, m.store.ClearStage(ctx, sess.Id) + } + keep[p.task.Id] = true + kept = append(kept, p.task.Id) + outputs = append(outputs, fmt.Sprintf("## %s\n\n%s", end, strings.TrimSpace(p.output))) + } + if len(outputs) > 1 { + output = strings.Join(outputs, "\n\n") + } + err = m.discardStages(ctx, tasks, keep) + if err != nil { + return nil, err + } + return nil, m.store.FinishWorkflow(ctx, task, sess.Id, frozen.Name, output, kept) } started := []string{} @@ -555,16 +590,12 @@ func teamMember(members []api.TeamMember, name string) (api.TeamMember, error) { return api.TeamMember{}, fmt.Errorf("the stage's frozen workflow has no agent %q", name) } -func (m *Manager) discardStages(ctx context.Context, final api.Task) error { - tasks, err := m.store.RunTasks(ctx, *final.WorkflowRun) - if err != nil { - return err - } +func (m *Manager) discardStages(ctx context.Context, tasks []api.Task, keep map[string]bool) error { for _, t := range tasks { - if t.Id == final.Id || t.Status != api.TaskStatusActive { + if keep[t.Id] || t.Status != api.TaskStatusActive { continue } - _, err = m.Discard(ctx, t, "byre") + _, err := m.Discard(ctx, t, "byre") if errors.Is(err, ErrConflict) { slog.Info("stage task not discarded", "task", t.Id, "err", err) continue @@ -599,12 +630,12 @@ func (m *Manager) finishStageTool(l *live) mcp.Tool { return mcp.Tool{ Name: "finish_stage", Description: "Finish this task's stage of its workflow. Call it once the stage's work is done; when this turn ends, byre hands the output to the stages that come after this one, " + - "each of which starts in its own task once every stage it comes after has finished (after the final stage, the workflow is finished). Calling it again in the same turn replaces the output. " + + "each of which starts in its own task once every stage it comes after has finished (once every stage has finished, the workflow is finished). Calling it again in the same turn replaces the output. " + "The call counts only for the turn that makes it: if that turn ends in an error or is interrupted, it is discarded, and you call it again once the stage's work is done.", InputSchema: json.RawMessage(`{ "type": "object", "properties": { - "output": {"type": "string", "description": "What the stages after this one start from: everything they need from this stage, since they see nothing else of your context. For the final stage, what the workflow produced."} + "output": {"type": "string", "description": "What the stages after this one start from: everything they need from this stage, since they see nothing else of your context. For a last stage, what this branch of the workflow produced."} }, "required": ["output"] }`), @@ -636,7 +667,7 @@ func (m *Manager) finishStageTool(l *live) mcp.Tool { } next := workflow.Next(frozen.Definition, l.stage) if len(next) == 0 { - return "This is the workflow's final stage: it finishes when this turn ends, and byre discards the workflow's other stage tasks. End your turn now with your summary.", nil + 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 }, @@ -659,9 +690,17 @@ func quoteList(names []string) string { return strings.Join(quoted, ", ") } +func lastStageText(def api.WorkflowDefinition) string { + ends := workflow.Ends(def) + if len(ends) == 1 { + return "This is the workflow's last stage: the workflow finishes when this turn ends, and byre discards its other stage tasks." + } + return fmt.Sprintf("This is one of the workflow's last stages (%s): the workflow finishes once all of them have, and byre then discards its other stage tasks, keeping each last stage's task.", quoteList(ends)) +} + func stageGuidance(frozen store.FrozenWorkflow, stage string) string { next := workflow.Next(frozen.Definition, stage) - then := "it is the workflow's final stage, so the workflow then finishes and byre discards its other stage tasks" + then := strings.TrimSuffix(lastStageText(frozen.Definition), ".") if len(next) > 0 { then = fmt.Sprintf("byre then hands the output to %s, each in its own task, which gets the output and nothing else of your context", quoteList(next)) } @@ -740,7 +779,7 @@ func (m *Manager) workflowTools(tool func(name, description, schema string, run if err != nil { return "", err } - w, err := m.SaveWorkflow(ctx, args.Name, []byte(args.Definition), leadActor(user.Login)) + w, err := m.SaveWorkflow(ctx, args.Name, "", []byte(args.Definition), leadActor(user.Login)) if err != nil { return "", err } @@ -748,7 +787,7 @@ func (m *Manager) workflowTools(tool func(name, description, schema string, run }), tool("set_review_workflow", - "Set the review workflow (a saved workflow of kind review) 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.", + "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"} diff --git a/internal/api/api.gen.go b/internal/api/api.gen.go index b13e5d6..9f136a0 100644 --- a/internal/api/api.gen.go +++ b/internal/api/api.gen.go @@ -516,24 +516,6 @@ func (e TaskTab) Valid() bool { } } -// Defines values for WorkflowDefinitionKind. -const ( - WorkflowKindReview WorkflowDefinitionKind = "review" - WorkflowKindTask WorkflowDefinitionKind = "task" -) - -// Valid indicates whether the value is a known member of the WorkflowDefinitionKind enum. -func (e WorkflowDefinitionKind) Valid() bool { - switch e { - case WorkflowKindReview: - return true - case WorkflowKindTask: - return true - default: - return false - } -} - // Defines values for WorkflowRunStageState. const ( StageStateCleanedUp WorkflowRunStageState = "cleaned_up" @@ -567,24 +549,6 @@ func (e WorkflowRunStageState) Valid() bool { } } -// Defines values for WorkflowSummaryKind. -const ( - WorkflowSummaryKindReview WorkflowSummaryKind = "review" - WorkflowSummaryKindTask WorkflowSummaryKind = "task" -) - -// Valid indicates whether the value is a known member of the WorkflowSummaryKind enum. -func (e WorkflowSummaryKind) Valid() bool { - switch e { - case WorkflowSummaryKindReview: - return true - case WorkflowSummaryKindTask: - return true - default: - return false - } -} - // Defines values for ZombieKind. const ( ProcessWithoutSession ZombieKind = "process_without_session" @@ -1196,7 +1160,7 @@ type ReviewState string // ReviewWorkflow defines model for ReviewWorkflow. type ReviewWorkflow struct { - // Workflow A saved workflow of kind review, or empty for no reviews. + // Workflow A saved workflow, or empty for no reviews. Workflow string `json:"workflow"` } @@ -1569,23 +1533,17 @@ type UserUpdate struct { GitName string `json:"git_name"` } -// WorkflowDefinition A workflow's file, which holds everything but its name: its steps, run in order. Each stage runs as its own task once the steps before it have finished. Sent and returned as YAML. +// WorkflowDefinition A workflow's file, which holds everything but its name: its steps, run in order. Run on a brief, each stage runs as its own task once the steps before it have finished; as a project's review workflow, each stage is a reviewer of a snapshot and gets the earlier stages' reviews. Sent and returned as YAML. type WorkflowDefinition struct { - Description string `json:"description,omitempty"` - - // Kind task (the default): each stage runs as its own task. review: each stage is a reviewer of a snapshot, run when a project names the workflow as its review workflow; a stage gets the earlier stages' reviews. - Kind WorkflowDefinitionKind `json:"kind,omitempty"` - Steps []WorkflowStep `json:"steps"` + Description string `json:"description,omitempty"` + Steps []WorkflowStep `json:"steps"` } -// WorkflowDefinitionKind task (the default): each stage runs as its own task. review: each stage is a reviewer of a snapshot, run when a project names the workflow as its review workflow; a stage gets the earlier stages' reviews. -type WorkflowDefinitionKind string - // WorkflowRun defines model for WorkflowRun. type WorkflowRun struct { Brief string `json:"brief"` - // Definition A workflow's file, which holds everything but its name: its steps, run in order. Each stage runs as its own task once the steps before it have finished. Sent and returned as YAML. + // Definition A workflow's file, which holds everything but its name: its steps, run in order. Run on a brief, each stage runs as its own task once the steps before it have finished; as a project's review workflow, each stage is a reviewer of a snapshot and gets the earlier stages' reviews. Sent and returned as YAML. Definition WorkflowDefinition `json:"definition"` // Finished Whether the final stage finished, which ends the run. @@ -1640,14 +1598,10 @@ type WorkflowStep struct { // WorkflowSummary defines model for WorkflowSummary. type WorkflowSummary struct { - Description string `json:"description"` - Kind WorkflowSummaryKind `json:"kind"` - Name string `json:"name"` + Description string `json:"description"` + Name string `json:"name"` } -// WorkflowSummaryKind defines model for WorkflowSummary.Kind. -type WorkflowSummaryKind string - // Zombie defines model for Zombie. type Zombie struct { Detail string `json:"detail"` @@ -1748,6 +1702,11 @@ type GetTaskTerminalParams struct { Rows int `form:"rows" json:"rows"` } +// PutWorkflowParams defines parameters for PutWorkflow. +type PutWorkflowParams struct { + From *string `form:"from,omitempty" json:"from,omitempty"` +} + // UpdateUsageCommandsJSONRequestBody defines body for UpdateUsageCommands for application/json ContentType. type UpdateUsageCommandsJSONRequestBody = UsageCommands @@ -2040,7 +1999,7 @@ type ServerInterface interface { GetWorkflow(w http.ResponseWriter, r *http.Request, name string) // (PUT /api/workflows/{name}) - PutWorkflow(w http.ResponseWriter, r *http.Request, name string) + PutWorkflow(w http.ResponseWriter, r *http.Request, name string, params PutWorkflowParams) } // ServerInterfaceWrapper converts contexts to parameters. @@ -4002,8 +3961,24 @@ func (siw *ServerInterfaceWrapper) PutWorkflow(w http.ResponseWriter, r *http.Re return } + // Parameter object where we will unmarshal all parameters from the context + var params PutWorkflowParams + + // ------------- Optional query parameter "from" ------------- + + err = runtime.BindQueryParameterWithOptions("form", true, false, "from", r.URL.Query(), ¶ms.From, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) + if err != nil { + var requiredError *runtime.RequiredParameterError + if errors.As(err, &requiredError) { + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "from"}) + } else { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "from", Err: err}) + } + return + } + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - siw.Handler.PutWorkflow(w, r, name) + siw.Handler.PutWorkflow(w, r, name, params) })) for _, middleware := range siw.HandlerMiddlewares { @@ -4216,306 +4191,306 @@ 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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", } // decodeSpec returns the embedded OpenAPI spec as raw JSON bytes, diff --git a/internal/server/workflows.go b/internal/server/workflows.go index a589221..b4d65c2 100644 --- a/internal/server/workflows.go +++ b/internal/server/workflows.go @@ -44,13 +44,17 @@ func (s *Server) GetWorkflow(w http.ResponseWriter, r *http.Request, name string w.Write(file) } -func (s *Server) PutWorkflow(w http.ResponseWriter, r *http.Request, name string) { +func (s *Server) PutWorkflow(w http.ResponseWriter, r *http.Request, name string, params api.PutWorkflowParams) { file, err := io.ReadAll(http.MaxBytesReader(w, r.Body, maxWorkflowFileBytes)) if err != nil { writeError(w, http.StatusBadRequest, fmt.Errorf("read workflow file: %w", err)) return } - summary, err := s.agents.SaveWorkflow(r.Context(), name, file, actor(r)) + from := "" + if params.From != nil { + from = *params.From + } + summary, err := s.agents.SaveWorkflow(r.Context(), name, from, file, actor(r)) if err != nil { writeAgentError(w, err) return diff --git a/internal/store/migrations/073-drop-workflow-kind.sql b/internal/store/migrations/073-drop-workflow-kind.sql new file mode 100644 index 0000000..9fcce50 --- /dev/null +++ b/internal/store/migrations/073-drop-workflow-kind.sql @@ -0,0 +1,11 @@ +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 new file mode 100644 index 0000000..c26d0e0 --- /dev/null +++ b/internal/store/migrations/074-one-review-per-stage.sql @@ -0,0 +1,25 @@ +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/workflow.go b/internal/store/workflow.go index 6ea949d..a522e7f 100644 --- a/internal/store/workflow.go +++ b/internal/store/workflow.go @@ -18,7 +18,7 @@ type FrozenWorkflow struct { } func (s *Store) ListWorkflows(ctx context.Context) ([]api.WorkflowSummary, error) { - rows, err := s.db.QueryContext(ctx, `SELECT name, coalesce(json_extract(definition, '$.description'), ''), coalesce(nullif(json_extract(definition, '$.kind'), ''), 'task') FROM workflows ORDER BY name`) + rows, err := s.db.QueryContext(ctx, `SELECT name, coalesce(json_extract(definition, '$.description'), '') FROM workflows ORDER BY name`) if err != nil { return nil, err } @@ -27,7 +27,7 @@ func (s *Store) ListWorkflows(ctx context.Context) ([]api.WorkflowSummary, error workflows := []api.WorkflowSummary{} for rows.Next() { w := api.WorkflowSummary{} - err = rows.Scan(&w.Name, &w.Description, &w.Kind) + err = rows.Scan(&w.Name, &w.Description) if err != nil { return nil, err } @@ -50,12 +50,46 @@ func (s *Store) GetWorkflow(ctx context.Context, name string) (api.WorkflowDefin return w, err } -func (s *Store) PutWorkflow(ctx context.Context, name string, def api.WorkflowDefinition, actor string) error { +var ErrWorkflowExists = errors.New("workflow exists") + +func (s *Store) PutWorkflow(ctx context.Context, name, from string, def api.WorkflowDefinition, actor string) error { b, err := json.Marshal(def) if err != nil { return err } return s.write(ctx, func(tx *sql.Tx, emit emitFunc) error { + if from != "" { + found := false + err := tx.QueryRowContext(ctx, `SELECT EXISTS (SELECT 1 FROM workflows WHERE name = ?)`, from).Scan(&found) + if err != nil { + return err + } + if !found { + return fmt.Errorf("%w: no workflow %q", ErrNotFound, from) + } + } + if from != "" && from != name { + taken := false + err := tx.QueryRowContext(ctx, `SELECT EXISTS (SELECT 1 FROM workflows WHERE name = ?)`, name).Scan(&taken) + if err != nil { + return err + } + if taken { + return fmt.Errorf("%w: a workflow named %q already exists", ErrWorkflowExists, name) + } + _, err = tx.ExecContext(ctx, `DELETE FROM workflows WHERE name = ?`, from) + if err != nil { + return err + } + err = repointReviews(ctx, tx, emit, from, name, actor) + if err != nil { + return err + } + err = emit(api.Event{Kind: "workflow.deleted", Actor: actor, Payload: map[string]any{"name": from}}) + if err != nil { + return err + } + } _, err := tx.ExecContext(ctx, `INSERT INTO workflows (name, definition) VALUES (?, ?) ON CONFLICT (name) DO UPDATE SET definition = excluded.definition`, name, string(b)) if err != nil { @@ -65,6 +99,33 @@ func (s *Store) PutWorkflow(ctx context.Context, name string, def api.WorkflowDe }) } +func repointReviews(ctx context.Context, tx *sql.Tx, emit emitFunc, from, to, actor string) error { + rows, err := tx.QueryContext(ctx, `UPDATE projects SET review_workflow = ? WHERE review_workflow = ? RETURNING `+projectColumns, to, from) + if err != nil { + return err + } + projects := []api.Project{} + for rows.Next() { + p, err := scanProject(rows) + if err != nil { + rows.Close() + return err + } + projects = append(projects, p) + } + err = errors.Join(rows.Err(), rows.Close()) + if err != nil { + return err + } + for _, p := range projects { + err = emit(api.Event{Kind: "project.updated", ProjectId: &p.Id, Actor: actor, Payload: ToPayload(p)}) + if err != nil { + return err + } + } + return nil +} + func (s *Store) DeleteWorkflow(ctx context.Context, name, actor string) error { return s.write(ctx, func(tx *sql.Tx, emit emitFunc) error { res, err := tx.ExecContext(ctx, `DELETE FROM workflows WHERE name = ?`, name) @@ -78,6 +139,10 @@ func (s *Store) DeleteWorkflow(ctx context.Context, name, actor string) error { if n == 0 { return fmt.Errorf("%w: no workflow %q", ErrNotFound, name) } + err = repointReviews(ctx, tx, emit, name, "", actor) + if err != nil { + return err + } return emit(api.Event{Kind: "workflow.deleted", Actor: actor, Payload: map[string]any{"name": name}}) }) } @@ -201,7 +266,7 @@ func clearStage(ctx context.Context, tx *sql.Tx, sessionID string) error { return err } -func (s *Store) FinishWorkflow(ctx context.Context, task api.Task, sessionID, workflowName, output string) error { +func (s *Store) FinishWorkflow(ctx context.Context, task api.Task, sessionID, workflowName, output string, kept []string) error { return s.write(ctx, func(tx *sql.Tx, emit emitFunc) error { err := clearStage(ctx, tx, sessionID) if err != nil { @@ -212,7 +277,7 @@ func (s *Store) FinishWorkflow(ctx context.Context, task api.Task, sessionID, wo ProjectId: &task.ProjectId, TaskId: &task.Id, Actor: "byre", - Payload: map[string]any{"id": task.Id, "workflow": workflowName, "output": output}, + Payload: map[string]any{"id": task.Id, "workflow": workflowName, "output": output, "kept": kept}, }) }) } diff --git a/internal/workflow/workflow.go b/internal/workflow/workflow.go index cd01f94..d44bb2d 100644 --- a/internal/workflow/workflow.go +++ b/internal/workflow/workflow.go @@ -53,26 +53,10 @@ func Validate(def api.WorkflowDefinition) error { if len(def.Steps) == 0 { problems = append(problems, "it has no steps") } - if def.Kind != "" && !def.Kind.Valid() { - problems = append(problems, fmt.Sprintf("kind must be task or review, not %q", def.Kind)) - } seen := map[string]bool{} for i, s := range def.Steps { problems = append(problems, stepProblems(s, fmt.Sprintf("steps[%d]", i), seen)...) } - if def.Kind == api.WorkflowKindReview { - for _, s := range Stages(def) { - if s.Agents != nil { - problems = append(problems, fmt.Sprintf("stage %q: a review workflow's stages are single reviewers, so agents can't be given", s.Stage)) - } - } - } - if len(problems) == 0 && def.Kind != api.WorkflowKindReview { - finals := ends(api.WorkflowStep{Order: &def.Steps}) - if len(finals) != 1 { - problems = append(problems, fmt.Sprintf("it must end with one stage, which finishes the workflow, but ends with %d: %s", len(finals), quoteList(finals))) - } - } if len(problems) > 0 { return fmt.Errorf("%w: %s", ErrInvalid, strings.Join(problems, "; ")) } @@ -161,6 +145,10 @@ func TeamProblems(members []api.TeamMember) []string { return problems } +func Ends(def api.WorkflowDefinition) []string { + return ends(api.WorkflowStep{Order: &def.Steps}) +} + func ends(s api.WorkflowStep) []string { switch { case s.Order != nil: @@ -246,11 +234,3 @@ func Starts(def api.WorkflowDefinition) []string { func FromMain(def api.WorkflowDefinition, name string) bool { return len(After(def, name)) > 1 } - -func quoteList(names []string) string { - quoted := []string{} - for _, n := range names { - quoted = append(quoted, fmt.Sprintf("%q", n)) - } - return strings.Join(quoted, ", ") -} diff --git a/mac/Sources/Byre/Project.swift b/mac/Sources/Byre/Project.swift index a49b239..d17e985 100644 --- a/mac/Sources/Byre/Project.swift +++ b/mac/Sources/Byre/Project.swift @@ -139,7 +139,7 @@ private struct ReviewWorkflowMenu: View { .help("The review workflow whose reviewers review every snapshot of this project's tasks") .task { do { - reviewWorkflows = try await store.api.client.listWorkflows().ok.body.json.filter { $0.kind == .review }.map(\.name) + reviewWorkflows = try await store.api.client.listWorkflows().ok.body.json.map(\.name) } catch { reviewWorkflows = [] } diff --git a/mac/Sources/Byre/Switcher.swift b/mac/Sources/Byre/Switcher.swift index d723cc4..6b8b7b8 100644 --- a/mac/Sources/Byre/Switcher.swift +++ b/mac/Sources/Byre/Switcher.swift @@ -159,7 +159,7 @@ final class SwitcherModel { self.anchor = anchor Task { do { - workflows = try await store.api.client.listWorkflows().ok.body.json.filter { $0.kind == .task } + workflows = try await store.api.client.listWorkflows().ok.body.json } catch { overlays.showAlert(errorText(error)) } diff --git a/mac/Sources/Byre/WorkflowEditor.swift b/mac/Sources/Byre/WorkflowEditor.swift index 3e9a4c0..5ec85fb 100644 --- a/mac/Sources/Byre/WorkflowEditor.swift +++ b/mac/Sources/Byre/WorkflowEditor.swift @@ -92,6 +92,28 @@ private func newStage(_ root: StepNode, _ model: String) -> StepNode { return s } +@MainActor +private func stepGraph(_ root: StepNode) -> Flowchart { + var chart = Flowchart(direction: .td) + func walk(_ node: StepNode, _ before: [String]) -> [String] { + switch node.kind { + case .order: + return node.children.reduce(before) { walk($1, $0) } + case .parallel: + return node.children.flatMap { walk($0, before) } + case .stage: + let id = "n\(chart.order.count)" + chart.node(id, label: node.name.isEmpty ? " " : node.name, shape: .box) + for b in before { + chart.edges.append(FlowEdge(from: b, to: id, label: nil, style: .solid, arrow: true)) + } + return [id] + } + } + _ = walk(root, []) + return chart +} + private struct StepLine: Identifiable { let node: StepNode let guide: String @@ -118,7 +140,6 @@ struct WorkflowEditor: View { @ViewState private var loaded: WorkflowDefinition? @ViewState private var name = "" @ViewState private var description = "" - @ViewState private var review = false @ViewState private var failure = "" @ViewState private var saving = false @@ -139,30 +160,21 @@ struct WorkflowEditor: View { } } .buttonStyle(WebButtonStyle(primary: true)) - .disabled(saving || !changed || name.trimmingCharacters(in: .whitespaces).isEmpty) + .disabled(saving || !changed || target.isEmpty) } .padding(.top, 16) if let root { - if original == nil { - TextField("Name", text: $name) - .modifier(FieldStyle()) - .padding(.top, 12) - } else { - Text(name) - .font(Fonts.font(13, weight: 700)) - .textSelection(.enabled) - .padding(.top, 12) - } + TextField("Name", text: $name) + .modifier(FieldStyle()) + .padding(.top, 12) TextField("Description", text: $description, axis: .vertical) .modifier(FieldStyle()) .padding(.top, 8) - Picker("", selection: $review) { - Text("Runs on a brief, each stage its own task").tag(false) - Text("Reviews snapshots, each stage a reviewer").tag(true) + if !root.stageNames().isEmpty { + FlowchartCanvas(layout: layoutFlowchart(stepGraph(root))) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.top, 12) } - .pickerStyle(.radioGroup) - .labelsHidden() - .padding(.top, 8) if !failure.isEmpty { Text(failure) .foregroundStyle(Palette.danger) @@ -197,11 +209,15 @@ struct WorkflowEditor: View { } private var current: WorkflowDefinition? { - root.map { WorkflowDefinition(description: description.isEmpty ? nil : description, kind: review ? .review : nil, steps: $0.children.map(\.step)) } + root.map { WorkflowDefinition(description: description.isEmpty ? nil : description, steps: $0.children.map(\.step)) } + } + + private var target: String { + name.trimmingCharacters(in: .whitespaces) } private var changed: Bool { - original == nil || current != loaded + original == nil || current != loaded || target != original } private func load() async { @@ -215,7 +231,6 @@ struct WorkflowEditor: View { let def = try await store.api.client.getWorkflow(path: .init(name: original), headers: .init(accept: [.init(contentType: .json)])).ok.body.json name = original description = def.description ?? "" - review = def.kind == .review root = StepNode(kind: .order, children: def.steps.map { StepNode($0) }) loaded = current } catch { @@ -229,14 +244,9 @@ struct WorkflowEditor: View { } saving = true defer { saving = false } - let target = name.trimmingCharacters(in: .whitespaces) do { - _ = try await store.api.client.putWorkflow(path: .init(name: target), body: .json(def)).ok.body.json - failure = "" - loaded = def - if original == nil { - close() - } + _ = try await store.api.client.putWorkflow(path: .init(name: target), query: .init(from: original), body: .json(def)).ok.body.json + close() } catch { failure = errorText(error) } @@ -338,10 +348,12 @@ private struct StepRow: View { Button("Ungroup") { ungroup() } } Divider() + let siblings = node.parent?.children ?? [] + let i = siblings.firstIndex { $0 === node } Button("Move up") { move(-1) } - .disabled(node.index == 0) + .disabled(i == nil || i == 0) Button("Move down") { move(1) } - .disabled(node.index == node.parent!.children.count - 1) + .disabled(i == nil || i == siblings.count - 1) Divider() Button("Remove") { node.detach() } } label: { diff --git a/mac/Sources/Byre/Workflows.swift b/mac/Sources/Byre/Workflows.swift index 474b780..33408cc 100644 --- a/mac/Sources/Byre/Workflows.swift +++ b/mac/Sources/Byre/Workflows.swift @@ -115,13 +115,8 @@ private struct WorkflowRow: View { var body: some View { HStack(alignment: .firstTextBaseline, spacing: 12) { VStack(alignment: .leading, spacing: 2) { - HStack(alignment: .firstTextBaseline, spacing: 6) { - Text(workflow.name) - .font(Fonts.font(13, weight: 700)) - if workflow.kind == .review { - Text("reviews snapshots").font(Fonts.font(11)).foregroundStyle(Palette.muted) - } - } + Text(workflow.name) + .font(Fonts.font(13, weight: 700)) if !workflow.description.isEmpty { Text(workflow.description) .foregroundStyle(Palette.muted) @@ -135,13 +130,18 @@ private struct WorkflowRow: View { } .buttonStyle(SmallLinkButtonStyle()) .help("See and change this workflow's steps and prompts") - if workflow.kind == .task { - Button("run") { - run() - } - .buttonStyle(SmallLinkButtonStyle()) - .help("Open the Lead tab with a request to run this workflow in its composer") + Button("run") { + run() } + .buttonStyle(SmallLinkButtonStyle()) + .help("Open the Lead tab with a request to run this workflow in its composer") + Button("delete") { + Task { + await delete() + } + } + .buttonStyle(SmallLinkButtonStyle()) + .help("Delete this workflow; tasks already running it keep their copy") } .padding(.vertical, 6) .overlay(alignment: .top) { @@ -149,6 +149,17 @@ private struct WorkflowRow: View { } } + private func delete() async { + guard await overlays.showConfirm(["Delete the workflow \"\(workflow.name)\"?"], action: "Delete", danger: true) else { + return + } + do { + _ = try await store.api.client.deleteWorkflow(path: .init(name: workflow.name)).noContent + } catch { + overlays.showAlert(errorText(error)) + } + } + private func run() { let key = "message:lead" let ask = "Run the \"\(workflow.name)\" workflow: "