From 5823af6ff9254a2d163809c92a05a06201aba759 Mon Sep 17 00:00:00 2001 From: Ian Gulliver Date: Tue, 20 Oct 2026 21:08:31 -0700 Subject: [PATCH] Add background audits of the default branch's newest commit --- TODO.md | 3 - api/openapi.yaml | 33 +- docs/operations.md | 12 +- internal/agents/audit.go | 146 ++++ internal/agents/lead.go | 27 + internal/agents/leadtools.go | 4 +- internal/agents/manager.go | 3 + internal/agents/ready.go | 4 + internal/agents/tasks.go | 2 +- internal/agents/workflows.go | 47 +- internal/api/api.gen.go | 663 ++++++++++--------- internal/git/git.go | 4 + internal/server/server.go | 2 +- internal/server/workflows.go | 15 +- internal/store/migrations/077-add-audits.sql | 2 + internal/store/store.go | 35 +- internal/store/workflow.go | 27 +- mac/Sources/Byre/Notifications.swift | 7 +- mac/Sources/Byre/Project.swift | 58 +- mac/Sources/Byre/Store.swift | 5 + mac/Sources/Byre/Switcher.swift | 3 + 21 files changed, 745 insertions(+), 357 deletions(-) create mode 100644 internal/agents/audit.go create mode 100644 internal/store/migrations/077-add-audits.sql diff --git a/TODO.md b/TODO.md index 3a6a8ca..b582388 100644 --- a/TODO.md +++ b/TODO.md @@ -6,9 +6,6 @@ # M3 — Second vendor and review -- Continuous background review - Enforce the codebase's standards and expectations regularly over the whole codebase, not in the light of a particular change. - # M4 — Merging and history # M5 — Workflows diff --git a/api/openapi.yaml b/api/openapi.yaml index 7963223..afd78d6 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -249,7 +249,27 @@ paths: required: true content: application/json: - schema: { $ref: "#/components/schemas/ReviewWorkflow" } + schema: { $ref: "#/components/schemas/ProjectWorkflow" } + responses: + "200": + description: The updated project. + content: + application/json: + schema: { $ref: "#/components/schemas/Project" } + "400": { $ref: "#/components/responses/Error" } + "404": { $ref: "#/components/responses/Error" } + + /api/projects/{projectId}/audit-workflow: + parameters: + - { name: projectId, in: path, required: true, schema: { type: string } } + put: + operationId: setAuditWorkflow + description: Sets the audit workflow that byre runs in the background on the default branch's newest commit; empty turns audits off. Records project.updated, and starts an audit when the newest commit hasn't had one. Refused when the project's creator, whom audits run as, has no git identity. + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/ProjectWorkflow" } responses: "200": description: The updated project. @@ -1315,7 +1335,7 @@ components: Project: type: object - required: [id, name, repo_path, default_branch, remote, created_by, created_at, review_workflow] + required: [id, name, repo_path, default_branch, remote, created_by, created_at, review_workflow, audit_workflow] properties: id: { type: string } name: { type: string } @@ -1325,12 +1345,13 @@ components: created_by: { type: string } created_at: { type: string, format: date-time } review_workflow: { type: string, description: "The review workflow whose stages review every snapshot of the project's tasks; empty for no reviews." } + audit_workflow: { type: string, description: "The workflow byre runs in the background, one run at a time, on the default branch's newest commit; empty for no audits." } - ReviewWorkflow: + ProjectWorkflow: type: object required: [workflow] properties: - workflow: { type: string, description: "A saved workflow, or empty for no reviews." } + workflow: { type: string, description: "A saved workflow, or empty for none." } ProjectCreate: type: object @@ -1341,7 +1362,7 @@ components: Task: type: object - required: [id, project_id, title, merge_title, branch, worktree_path, base_ref, port_base, color, status, created_by, created_at, state, merging, discarding, merging_into_parent, merge_after_review, summary_seq] + required: [id, project_id, title, merge_title, branch, worktree_path, base_ref, port_base, color, status, created_by, created_at, state, merging, discarding, merging_into_parent, merge_after_review, summary_seq, background] properties: id: { type: string } project_id: { type: string } @@ -1367,6 +1388,8 @@ components: workflow: { type: string, description: "The name of the workflow the task froze at creation (getTaskWorkflow has its copy); absent for a task without one." } workflow_run: { type: string, description: "With workflow: the id every task of one run of the workflow shares." } workflow_stage: { type: string, description: "With workflow: the name of the stage this task runs." } + audit: { type: string, description: "The default branch commit this task audits, for a task of a run of its project's audit workflow; absent for other tasks." } + background: { type: boolean, description: "Whether the task is an audit that hasn't asked for attention yet: no question, ready_for_merge or error so far. The app keeps it out of the task switcher and shows it muted on the Project tab, and the lead gets no signals about it." } state: type: string description: "The one state the task is in, kept by the store in the same transaction as every change to it (a task.state event carries each change). The first that holds: merged or discarded once closed; discarding; asking while it has an open question; merging while a Merge or a merge into its parent is under way; merge_held while its Merge waits for another merge in the project; merge_after_review while a Merge with after_review waits for its review; running while one of its agents is mid-turn or has subagents running; reviewing while a review runs; review_pending between a turn's end and its snapshot's review starting or being skipped; ready_to_merge or discard_suggested when the agent's latest ready_for_merge or suggest_discard still stands; idle; no_agent when it has no live agent." diff --git a/docs/operations.md b/docs/operations.md index 0e4d500..8a1bc94 100644 --- a/docs/operations.md +++ b/docs/operations.md @@ -297,7 +297,7 @@ Each user has one long-running lead agent, shown in the left pane. It is not tie - **Messages.** `POST /api/me/lead/messages` sends the caller's message to their lead, starting a lead session when there is none. While the lead is mid-turn the message is held in `lead_inputs`; the lead's PostToolUse hook (Codex: `turn/steer` after each completed tool item; Antigravity: the `PreInvocation` hook) hands held messages over after its next tool call, like a task's queued messages (see "Agent sessions"), and any still held become its next turn. Signals are never handed over mid-turn. The composer stays enabled. `GET /api/me/lead/sessions` lists the caller's lead sessions; interrupt and compact use the usual session endpoints. - **Scrollback.** `GET /api/me/lead/events` returns a page of the caller's lead events in whole turns, `turns` of them (default 50): those after the `turns`+1'th `turn.ended` before `before` (or before the latest event, so the page also holds a turn still running), with `more` saying whether earlier events exist. The lead pane loads only the last page (see "Events"); when its top scrolls into view (an empty row kept first in the transcript), it fetches the page before its first event (`loadOlderLead` in `mac/Sources/Byre/Store.swift`), puts its rows in front (`TranscriptBuilder.prepend`) and scrolls back to the row that was first. A page starts just after a `turn.ended`, so rendering it from scratch matches how it drew as part of the whole history. Live events still append without limit until the app is relaunched. - **Signals.** Every published event goes through `Manager.Observe`. These become signals in `lead_inputs` for every user who has a lead session: `inbox.created` (a question with its choices and recommendation), `session.error` and `session.rate_limited` on a task, `inbox.updated` when a human answered a question (the signal also carries its text and the agent's recommendation, so the lead sees where the human departed from it), `task.created` (naming the TODO item it was started from, or a subtask's parent, if any), `task.merge_queued` ("'s merge was requested by and is held until the merge in progress in its project is done."), `task.merge_after_review` (" was set by to merge once the reviews of snapshot finish with no findings."), `task.merge_after_review_cancelled` ("'s merge after review was cancelled by ."), `task.merge_after_review_dropped` (" didn't merge after the review: "), `task.merge_started` (" is merging, started by ."), `task.merged`, `task.discarded`, `turn.ended` with `summary: true` (" finished a turn:" with its `result` cut to 1500 characters), `task.workflow_stage` (" started stage "" of workflow "".") and `task.workflow_finished` (" finished workflow "":" with its output cut to 1500 characters), `review.started` (" started the "" review of snapshot ."), `review.finished` (" "" review of snapshot :" with the review's summary, `change` and findings, or why it failed or was skipped; `reviewLine` in `internal/agents/review.go`), `user.message` on a task from a human (see "Pane lock"), and `task.state` (" state: → .", or " state: ." for a new task; see "Task state in the store"). The store hands `Observe` all of one transaction's events together, and they become one signal, its `task.state` lines first, so a turn's summary always arrives with the state its turn end left the task in. A batch whose lines are all quiet is dropped instead of held (`quietSignal` in `internal/agents/lead.go`): `task.state`, `review.started`, and `task.created`, `task.merge_started`, `task.merged` and `task.discarded` by a human, unless the line carries "Your right pane is free" (see "Pane lock"). Quiet lines still ride along in a batch with anything else, and the "Tasks now" list that follows every batch shows each task's current state. The human switching the right pane is not a signal. A signal caused by the lead itself (actor ` (lead)`) is skipped, as is the update from an answer it gave. A task's `task.created` event carries the actor that created it, separately from its `created_by` login: tasks the lead creates with `create_task` or `start_todo` keep the human's login as `created_by` but have actor ` (lead)`. Whenever the lead is idle, byre sends the oldest held message from the human as its own turn, or else every held signal as one `byre` message headed "Signals since your last turn (n)", followed by "Tasks now:" with the `list_tasks` text and a line saying what the lead's right pane shows (task or project, and selected tab) and whether the human is working in it (`leadStateText` in `internal/agents/lead.go`). If collecting that fails, for example because a project's repository is gone, the batch is still sent, ending instead with "The task list and right pane state are unavailable:" and the error, which is also logged as `lead state`. Its guidance tells it not to respond to the human's own actions, and to make its final message exactly `NO_REPLY` when a batch needs nothing from it. A transcript (`quietReply` in `mac/Sources/Byre/TranscriptBuilder.swift`) never draws an `agent.message` whose last line is `NO_REPLY` (the model sometimes writes a sentence before it), and hides the live partial message while its text is still a prefix of it, so an empty one never takes up space. A lead turn that byre starts without a handoff (a signal batch or a handoff continuation) is held (`ctx.held`): its `byre` message is never drawn, the turn isn't opened, so the scanner stays off, its partial message stays hidden, and its tool calls, subagents and thinking are built into a held row list instead of the transcript. The turn is released, inserting those rows where the turn belongs, when it draws anything else: a real message, a note, or its own `turn.ended` when it ends without `NO_REPLY`. When it ends in `NO_REPLY` while still held, the held rows are dropped, so nothing of the turn ever appears; a turn that draws a real message anywhere in it keeps all of it, even when a later message is `NO_REPLY`. -- **Tools.** `list_tasks` (each project's active tasks with their state, each subtask noting its parent, a held merge (`merge_queued_at`) and a merge after review (`merge_after_review`) each noted, then a line listing the titles of its `TODO.md` items on the default branch that no active task was started from), `read_transcript` (a task's events in brief, the last `limit` or those after `after_seq`; closed tasks too), `search_closed_tasks` (see "Closing a task"), `read_inbox`, `read_status` (the `GET /api/admin/status` body as JSON, built by the same `Server.status`, which the server hands `agents.New`), `answer_question` (an `option` or `other` response recorded with the lead's actor as its `user`; `inbox_responses.actor` and `inbox_items.resolved_by` hold actors rather than logins, so they have no foreign key to `users`; through the same checks as `POST /api/inbox/responses`), `send_message` (starts the task's agent if it has none; while it is mid-turn the message is queued through `Manager.Message` like the human's, see "Agent sessions", and the tool result says it was queued; refused while the agent is stopped after an interrupt or error, see "Agent sessions"; on a team task it posts to the board, see "Teams"), `create_task` and `start_todo` (each with an optional `workflow` to freeze and run; `create_task` also takes `agents` to start a team), `create_project` (`name` and `repo_path`, created as the user by the same `Server.createProject` as `POST /api/projects`, which the server hands `agents.New`, so it publishes the same `project.created` event, and returned as JSON), `interrupt`, `compact_task`, `take_snapshot` (see "Snapshots"), `merge_task` (optional `target`: `main`, or `parent` for a subtask, see "Subtasks"), `discard_task`, `show`, `compact_self`, `read_knowledge` (see "Knowledge base"), the note tools (see "Notes") and the workflow tools (see "Workflows"). The lead also has Claude Code's own tools and reads worktrees by path. Messages it sends task agents and its answers have actor ` (lead)`, so they don't make the user a participant and nobody takes them for the user's; tasks, merges and discards are done as the user. Wherever that actor is shown, it reads `'s lead agent` (`actorName` in `internal/agents/lead.go` and `mac/Sources/Byre/Format.swift`): in the agent's mid-turn and board headings, in answers delivered from the inbox, in decisions the notes are built from, in `read_transcript`, and on transcript bubbles, board posts and question cards in the app. A message from the lead that starts a turn reaches the agent headed "This message is from 's lead agent, not from :" (`agentText`), and a queued one mid-turn as "'s lead agent (not ) sent you this message while you were working"; the recorded `user.message` keeps the text as the lead wrote it. Its guidance says to answer a question only when the answer is clear or the human gave it, and to merge or discard only when the human asks. +- **Tools.** `list_tasks` (each project's active tasks with their state, each subtask noting its parent, a held merge (`merge_queued_at`) and a merge after review (`merge_after_review`) each noted, then a line listing the titles of its `TODO.md` items on the default branch that no active task was started from), `read_transcript` (a task's events in brief, the last `limit` or those after `after_seq`; closed tasks too), `search_closed_tasks` (see "Closing a task"), `read_inbox`, `read_status` (the `GET /api/admin/status` body as JSON, built by the same `Server.status`, which the server hands `agents.New`), `answer_question` (an `option` or `other` response recorded with the lead's actor as its `user`; `inbox_responses.actor` and `inbox_items.resolved_by` hold actors rather than logins, so they have no foreign key to `users`; through the same checks as `POST /api/inbox/responses`), `send_message` (starts the task's agent if it has none; while it is mid-turn the message is queued through `Manager.Message` like the human's, see "Agent sessions", and the tool result says it was queued; refused while the agent is stopped after an interrupt or error, see "Agent sessions"; on a team task it posts to the board, see "Teams"), `create_task` and `start_todo` (each with an optional `workflow` to freeze and run; `create_task` also takes `agents` to start a team), `create_project` (`name` and `repo_path`, created as the user by the same `Server.createProject` as `POST /api/projects`, which the server hands `agents.New`, so it publishes the same `project.created` event, and returned as JSON), `interrupt`, `compact_task`, `take_snapshot` (see "Snapshots"), `set_audit_workflow` (see "Audits"), `merge_task` (optional `target`: `main`, or `parent` for a subtask, see "Subtasks"), `discard_task`, `show`, `compact_self`, `read_knowledge` (see "Knowledge base"), the note tools (see "Notes") and the workflow tools (see "Workflows"). The lead also has Claude Code's own tools and reads worktrees by path. Messages it sends task agents and its answers have actor ` (lead)`, so they don't make the user a participant and nobody takes them for the user's; tasks, merges and discards are done as the user. Wherever that actor is shown, it reads `'s lead agent` (`actorName` in `internal/agents/lead.go` and `mac/Sources/Byre/Format.swift`): in the agent's mid-turn and board headings, in answers delivered from the inbox, in decisions the notes are built from, in `read_transcript`, and on transcript bubbles, board posts and question cards in the app. A message from the lead that starts a turn reaches the agent headed "This message is from 's lead agent, not from :" (`agentText`), and a queued one mid-turn as "'s lead agent (not ) sent you this message while you were working"; the recorded `user.message` keeps the text as the lead wrote it. Its guidance says to answer a question only when the answer is clear or the human gave it, and to merge or discard only when the human asks. - **Focus.** `users.focus` holds what the user's right pane shows: `project_id`, `task_id` and `tab` (the selected one). The tabs' order is fixed; neither the lead nor the human can change it. With a `task_id`, `project_id` is always that task's project, filled in by the server; with only a `project_id`, the pane shows that project with no task, which only the Project tab has anything for. `GET /api/me/focus` reads it; `PUT /api/me/focus` and the lead's `show` set it, refusing an unknown tab, a task that isn't active or an unknown project, and record a `focus.updated` event owned by the user. The app applies the focus at startup and on every `focus.updated` it didn't cause itself; the human's own changes (⌘K, the right pane's task picker, clicking a tab, a task link, a notification) are sent with `PUT /api/me/focus`. - **Pane lock.** A `show` of what the pane already shows (the same task, or the same project with no task, on the same selected tab; `sameView`) tells the lead so and changes nothing. Otherwise, while the human is working in the right pane (the app reports `focused` or `drafting`, below), the lead's `show` is refused with an error telling it to say in chat what it would have shown, unless its selected tab is `project` (the fallback the lead shows when nothing needs the human, so the human being there doesn't hold it) or the pane is finished (`Manager.paneFinished`): its task is merged, discarded or merging (`merging_by` or `finalizing` set), or the human is waiting on the task's agent (`Manager.paneWaiting`): the selected tab is `transcript`, the task's implementer session is running, the app reports no unsent text in the pane, and either the `user.message` that started the running turn came from the user after the pane last moved, or the pane last moved 10 seconds ago or more (`watchHold`). The pane's last move is the last `PUT /api/me/focus` or `show` that set it (`Manager.paneMoved`, in memory, so a restart counts as long ago). For the user's own message, unsent text reported before the turn started doesn't count, since the app's report of the composer emptying on send can arrive after the message does. A `user.message` on a task from a human (not `byre`, an agent or a lead) signals every lead " sent a message; its agent is working on it:" followed by the message's full text, and the sender's own lead, when its pane shows that task and is waiting that way, also gets "Your right pane is free: the human is waiting on 's agent." Each `PUT /api/me/focus` from the user starts a 10-second timer (`endWatchHold`); when it fires, if the pane hasn't moved since, the app still reports the human in it, the user hasn't messaged the task since the move (that already freed it, above), and the pane is waiting that way, the lead gets the signal "Your right pane is free: the human has watched 's agent work for 10 seconds without typing." The lead's own `show` starts no timer. When a `task.merge_started`, `task.merged` or `task.discarded` event leaves a user's focused task finished that way, that user's signal for it gets a line "Your right pane is free: is merging." (or "is merged."/"is discarded."). The lead's guidance tells it to show the next thing that needs the human on that line, or, when nothing does, the project's Project tab with a short status in chat: one line per active task, then up to three TODO items it would start next, each with a reason, starting one only when the human picks it. It writes each item as a link to `#/todo//`; Markdown views draw that (`todo` in `mac/Sources/Byre/MarkdownView.swift`) as the title followed by `start` and `discuss` actions, as accent link text so the line stays selectable, that call `POST /api/projects/{id}/todo/tasks` with the user's default model, as the Project tab's do, or, once an active task was started from that item, the task's colour, dot and state, linking to it. When the lead suggests new work that isn't in TODO.md, its guidance tells it to write a link to `#/suggest///`, drawn by the same code with `start` and `discuss` buttons: `start` calls `POST /api/projects/{id}/tasks` with the title, the brief as `prompt` and the user's default model, which checks the user's git identity, creates the task and starts its agent from one request, with no lead turn in between; `discuss` sends the lead `POST /api/me/lead/messages` asking to discuss the suggestion, quoting its title and brief (with the user's default model only when the user has no live lead session). Once an active task in that project has the suggestion's title, it shows that task's state the same way. A chat view refills them whenever its transcript grows or tasks or sessions change. When that status says nothing new since its last one, it replies `NO_REPLY` instead. The app sends `PUT /api/me/pane-focus` with `focused` and `drafting` whenever either changes (`PaneFocus` in `mac/Sources/Byre/PaneFocus.swift`): `focused` is true while the window is key, the app is active and the keyboard focus or the last click is inside the right pane, so a click anywhere in it counts, not only on inputs and buttons; `drafting` while any text field in the right pane other than the terminal holds non-blank text. The window reports to the server it shows. It sends false on deactivating, switching to another server, Forget Server, closing the window and quit, and resends true when its event stream reconnects. byre keeps both flags and the time of the last report in memory per user (`Manager.paneFocus`, `Manager.paneReported`), so they start false after a restart until the app resends them. - **Move hold.** Each `PUT /api/me/focus` from the user (switching task or tab, by any means: the switcher, a tab, a link or a notification) records the time in `Manager.humanMoved`, and for 10 seconds after it (`moveHold`) every `show` that would change the view is refused, whatever the pane lock above would allow, with an error giving how long ago the move was. A `show` of what the pane already shows changes nothing and says so. A refusal also marks the user in `Manager.moveRefused`, which each move by the user clears. When the 10 seconds pass without another move by the user (`endMoveHold`, on a timer), byre clears both and, only if a `show` was refused since that move, sends the lead the signal "The move hold is over: …"; its guidance tells it to show what it held back if the pane's other rules allow, or reply `NO_REPLY`. Because only a refusal arranges the signal, the lead's system prompt and the `show` tool's description tell it to call `show` at once whenever the user asks to see something or it wants to show something, never holding back in expectation of a refusal. It is in memory, so a restart clears it. @@ -316,7 +316,7 @@ A project's `TODO.md` is read from the root of its default branch in the project The title is the item's identity. `POST /api/projects/{id}/todo/tasks` with a `title` re-reads `TODO.md`, refuses an unknown title (404) or one an active task already has (409), creates a task from the default branch with that title and `todo_item` (the `tasks.todo_item` column, empty for other tasks), and starts its session as the requesting user with `todoPrompt` (`internal/agents/tasks.go`): the item as written, and an instruction to remove it from `TODO.md` in the worktree when the work is done. With `discuss: true` the prompt adds that the human wants to discuss the item first: the agent zooms out first, reads what it needs and gives its view on whether the item should be built at all and in what shape (the problem it solves, alternatives including a smaller change or nothing, its approach, trade-offs and doubts), talks it over in its final messages and in as many `ask_human` questions as help, each about those broad choices rather than build details and free to build on earlier answers, and changes nothing until told to build it. The agent's guidance names the item too (`taskGuidance`, see "Agent sessions"), so the instruction survives compaction and resumes. The agent removes the item on its own branch, so the removal lands on the default branch in the same squash commit as the work. -The **Project** tab is the dashboard of the right pane's project (the focused task's, or the project focused with no task). Under **Tasks** it lists the project's active tasks in the server's order (newest first), each with its task state (see "Task state") and open inbox item count, as links that point the right pane at the task's transcript; the focused task is highlighted. Under **TODO** it lists the `TODO.md` items, grouped by section, with each body rendered as Markdown. An item with an active task whose `todo_item` equals its title shows that task's state as a link to the task instead of the **Start** and **Discuss** buttons. **Start** posts the item with the composer's default model; **Discuss** posts it the same way with `discuss: true`. Neither moves the right pane (see "Start hold"). The task list follows byre's live state; the tab fetches the project's `TODO.md` when it opens, when the right pane moves to another project, when the project list changes, when the SSE stream reconnects, and on `task.merged`; a change pushed to the default branch outside byre shows the next time one of those happens. +The **Project** tab is the dashboard of the right pane's project (the focused task's, or the project focused with no task). Under **Tasks** it lists the project's active tasks in the server's order (newest first), each with its task state (see "Task state") and open inbox item count, as links that point the right pane at the task's transcript; the focused task is highlighted. Background audit tasks follow as muted rows (see "Audits"). The header carries the audits and reviews menus. Under **TODO** it lists the `TODO.md` items, grouped by section, with each body rendered as Markdown. An item with an active task whose `todo_item` equals its title shows that task's state as a link to the task instead of the **Start** and **Discuss** buttons. **Start** posts the item with the composer's default model; **Discuss** posts it the same way with `discuss: true`. Neither moves the right pane (see "Start hold"). The task list follows byre's live state; the tab fetches the project's `TODO.md` when it opens, when the right pane moves to another project, when the project list changes, when the SSE stream reconnects, and on `task.merged`; a change pushed to the default branch outside byre shows the next time one of those happens. ## Inbox @@ -484,6 +484,14 @@ Every new snapshot of a task is reviewed by its project's review workflow: each - **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. +## Audits + +Reviews look at one task's change; an audit checks the whole codebase as it stands on the default branch, against its standards and expectations (`internal/agents/audit.go`). A project's audit workflow is any saved workflow, named by `projects.audit_workflow` (`audit_workflow` on the Project, empty for none), set with `PUT /api/projects/{projectId}/audit-workflow` (`{workflow}`, the same `ProjectWorkflow` body as the review workflow's; a missing workflow is refused, and so is any workflow while the project's creator, whom audits run as, has no git identity), from the Project tab's audits menu (beside the reviews menu, one shared `ProjectWorkflowMenu` in `mac/Sources/Byre/Project.swift`) or the lead's `set_audit_workflow`, recording `project.updated`. Renaming or deleting a workflow repoints or clears `audit_workflow` as it does `review_workflow` (`repointProjects` in `internal/store/workflow.go`). + +- **Starting.** An audit is an ordinary run of the workflow (see "Workflows"), titled `Audit of `, run as the project's creator with actor `byre`, from the default branch's commit in the project's checkout (`git.BranchHead`), with a brief (`auditBrief`) saying it audits that commit rather than a change, that it runs in the background, to finish each stage with `finish_stage` as usual, and to answer byre's ready prompt with `suggest_discard` when nothing needed changing. Each of its stage tasks records that commit in `tasks.audit` (`audit` on the Task; a fork of one doesn't), and a stage that would start from the default branch (one after several stages) starts from that commit instead, so every stage audits the same tree. A project has at most one audit at a time and audits only the newest commit, so it doesn't run on every revision but the newest one always gets audited eventually: `startAudit`, under `Manager.auditMu`, starts one when the project has an audit workflow, no active task with `audit` set, and the branch's commit differs from the newest audit task's `audit` (`Store.LastAudit`). It runs at startup for every project, when the audit workflow is set, and on every `task.merged`, `task.merged_to_main` and `task.discarded` in the project (`Manager.Observe`), which covers the default branch moving through byre and an audit ending. A workflow that fails to start is logged as `start audit`, and each of its tasks, the one whose agent failed to start included, gets a `session.error` saying so, which brings it out of the background for the human and the lead. Those tasks stay active, so no other audit starts until someone discards them, and they keep `audit`, so the discard doesn't retry that commit; the branch's next commit does. byre never fetches for an audit, so a commit pushed to the default branch outside byre is audited only once a byre Merge or `merge_to_main` has fetched it into the checkout. `sqlite3 byre.db "SELECT id, status, audit FROM tasks WHERE audit != '' ORDER BY created_at DESC LIMIT 10"` shows recent audits. +- **Background.** A Task's `background` is true while it has `audit` set and no `inbox.created`, `task.ready`, `session.error` or `session.rate_limited` event, that is until it asks for attention (`backgroundExpr` in `internal/store/store.go`). While it is: the ⌘K switcher leaves it out; the Project tab draws it after the task list as one muted row, its title and state in grey (`BackgroundRow`), which opens it; its turn summaries, discard suggestions and finished workflow don't notify; the lead gets no signals about it (`dropBackground` in `internal/agents/lead.go`) and `list_tasks` leaves it out. After that it is an ordinary task everywhere, for good. A failed review of its latest snapshot, which keeps byre from asking whether it is ready, also brings it out: when the ready check (`tryPromptReady`) finds one on a background task, byre records a `session.error` on the task naming the review (`auditError`), so it doesn't sit hidden and block later audits. +- **Dismissing itself.** When a background task calls `suggest_discard` once its run has a `task.workflow_finished` event (byre's ready prompt offers it, and `readyPromptUnchanged` when the audit changed nothing), byre discards it at once as `byre` (`dismissAudit`), so it never shows **Ready to discard**. A suggestion before the run finishes isn't acted on, so a discarded stage can't stall the run's other stages. An audit that asked for attention first is left for the human like any task. At startup, before starting audits, byre retries the dismissal for every active background task left in `discard_suggested` by a restart (`auditAll`). + ## Workflows 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). diff --git a/internal/agents/audit.go b/internal/agents/audit.go new file mode 100644 index 0000000..2ce731d --- /dev/null +++ b/internal/agents/audit.go @@ -0,0 +1,146 @@ +package agents + +import ( + "context" + "fmt" + "log/slog" + "strings" + + "g.fc.run/ai/byre/internal/api" + "g.fc.run/ai/byre/internal/git" +) + +func (m *Manager) SetAuditWorkflow(ctx context.Context, projectID, name, actor string) (api.Project, error) { + name = strings.TrimSpace(name) + if name != "" { + _, err := m.store.GetWorkflow(ctx, name) + if err != nil { + return api.Project{}, err + } + project, err := m.store.GetProject(ctx, projectID) + if err != nil { + return api.Project{}, err + } + creator, err := m.store.GetUser(ctx, project.CreatedBy) + if err != nil { + return api.Project{}, err + } + if creator.GitName == "" || creator.GitEmail == "" { + return api.Project{}, fmt.Errorf("%w: audits run as the project's creator %s, who has no git identity", ErrInvalid, creator.Login) + } + } + p, err := m.store.SetAuditWorkflow(ctx, projectID, name, actor) + if err != nil { + return api.Project{}, err + } + go m.audit(p.Id) + return p, nil +} + +func (m *Manager) auditAll(ctx context.Context) { + projects, err := m.store.ListProjects(ctx) + if err != nil { + slog.Error("start audits", "err", err) + return + } + for _, p := range projects { + tasks, err := m.store.ListTasks(ctx, p.Id) + if err != nil { + slog.Error("start audits", "project", p.Id, "err", err) + continue + } + for _, t := range tasks { + if t.Status == api.TaskStatusActive && t.Background && t.State == api.TaskStateDiscardSuggested { + m.dismissAudit(t.Id) + } + } + m.audit(p.Id) + } +} + +func (m *Manager) audit(projectID string) { + err := m.startAudit(context.Background(), projectID) + if err != nil { + slog.Error("start audit", "project", projectID, "err", err) + } +} + +func (m *Manager) startAudit(ctx context.Context, projectID string) error { + m.auditMu.Lock() + defer m.auditMu.Unlock() + + project, err := m.store.GetProject(ctx, projectID) + if err != nil || project.AuditWorkflow == "" { + return err + } + last, active, err := m.store.LastAudit(ctx, projectID) + if err != nil || active { + return err + } + head, err := git.BranchHead(project.RepoPath, project.DefaultBranch) + if err != nil { + return err + } + if head == last { + return nil + } + creator, err := m.store.GetUser(ctx, project.CreatedBy) + if err != nil { + return err + } + tasks, err := m.StartWorkflow(ctx, project, creator, "Audit of "+short(head), head, "byre", project.AuditWorkflow, auditBrief(project.DefaultBranch, head), nil, head) + if err == nil { + slog.Info("audit started", "project", projectID, "commit", head, "workflow", project.AuditWorkflow) + return nil + } + for _, t := range tasks { + m.auditError(ctx, t, fmt.Sprintf("The audit of %s didn't start: %v. Discard this task to let audits continue; the next commit on %s starts one.", short(head), err, project.DefaultBranch)) + } + return err +} + +func (m *Manager) auditError(ctx context.Context, task api.Task, msg string) { + err := m.store.AppendEvent(ctx, api.Event{ + Kind: "session.error", + ProjectId: &task.ProjectId, + TaskId: &task.Id, + Actor: "byre", + Payload: map[string]any{"error": msg}, + }) + if err != nil { + slog.Error("record audit error", "task", task.Id, "err", err) + } +} + +func auditBrief(branch, commit string) string { + return fmt.Sprintf("Audit of %s at %s. This isn't a review of a change: check the whole codebase as it stands against the project's standards and expectations, and fix what falls short. "+ + "byre runs this task in the background: the human doesn't see it until you ask a question, call ready_for_merge or hit an error. "+ + "Finish your stage with finish_stage as usual, even when you found nothing. Once the workflow has finished, byre asks whether the task is ready to merge; "+ + "if nothing needed changing, answer with suggest_discard, and byre discards the task without bothering the human.", branch, commit) +} + +func (m *Manager) dismissAudit(taskID string) { + ctx := context.Background() + task, err := m.store.GetTask(ctx, taskID) + if err != nil { + slog.Error("dismiss audit", "task", taskID, "err", err) + return + } + if !task.Background || task.Status != api.TaskStatusActive { + return + } + finished, err := m.store.WorkflowFinished(ctx, *task.WorkflowRun) + if err != nil { + slog.Error("dismiss audit", "task", taskID, "err", err) + return + } + if !finished { + return + } + _, err = m.Discard(ctx, task, "byre") + if err != nil { + slog.Error("dismiss audit", "task", taskID, "err", err) + return + } + slog.Info("audit dismissed itself", "task", taskID) +} diff --git a/internal/agents/lead.go b/internal/agents/lead.go index 4a7499f..773fd05 100644 --- a/internal/agents/lead.go +++ b/internal/agents/lead.go @@ -368,6 +368,12 @@ func (m *Manager) Observe(events []api.Event) { go m.promptReady(id, nil) } } + switch { + case (ev.Kind == "task.merged" || ev.Kind == "task.merged_to_main" || ev.Kind == "task.discarded") && ev.ProjectId != nil: + go m.audit(*ev.ProjectId) + case ev.Kind == "task.discard_suggested" && ev.TaskId != nil: + go m.dismissAudit(*ev.TaskId) + } 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": case "task.ready_cleared": @@ -416,6 +422,10 @@ func (m *Manager) signal(ctx context.Context, batch []api.Event) error { if err != nil { return err } + batch, err = m.dropBackground(ctx, batch) + if err != nil || len(batch) == 0 { + return err + } texts := []string{} for _, ev := range batch { @@ -464,6 +474,23 @@ func (m *Manager) signal(ctx context.Context, batch []api.Event) error { return nil } +func (m *Manager) dropBackground(ctx context.Context, batch []api.Event) ([]api.Event, error) { + kept := []api.Event{} + for _, ev := range batch { + if ev.TaskId != nil { + task, err := m.store.GetTask(ctx, *ev.TaskId) + if err != nil { + return nil, err + } + if task.Background { + continue + } + } + kept = append(kept, ev) + } + return kept, nil +} + func quietSignal(ev api.Event) bool { switch ev.Kind { case "task.state", "review.started": diff --git a/internal/agents/leadtools.go b/internal/agents/leadtools.go index fa3044c..f1b53c1 100644 --- a/internal/agents/leadtools.go +++ b/internal/agents/leadtools.go @@ -284,7 +284,7 @@ func (m *Manager) leadTools(l *live) []mcp.Tool { } } if args.Workflow != "" { - tasks, err := m.StartWorkflow(ctx, project, user, args.Title, base, leadActor(user.Login), args.Workflow, args.Prompt, nil) + tasks, err := m.StartWorkflow(ctx, project, user, args.Title, base, leadActor(user.Login), args.Workflow, args.Prompt, nil, "") if err != nil { return "", err } @@ -897,7 +897,7 @@ func (m *Manager) listTasksText(ctx context.Context) (string, error) { listed := false started := map[string]bool{} for _, t := range tasks { - if t.Status != api.TaskStatusActive { + if t.Status != api.TaskStatusActive || t.Background { continue } listed = true diff --git a/internal/agents/manager.go b/internal/agents/manager.go index 67badb3..f39f5de 100644 --- a/internal/agents/manager.go +++ b/internal/agents/manager.go @@ -84,6 +84,8 @@ type Manager struct { workflowMu sync.Mutex + auditMu sync.Mutex + previewMu sync.Mutex previewListeners map[int]*previewListener @@ -475,6 +477,7 @@ func (m *Manager) Recover(ctx context.Context) error { go m.deliverAllQueued(context.Background()) go m.startHeldMerges(context.Background()) go m.recoverMergeAfterReviews(context.Background()) + go m.auditAll(context.Background()) return nil } diff --git a/internal/agents/ready.go b/internal/agents/ready.go index 648fb18..5f5d2af 100644 --- a/internal/agents/ready.go +++ b/internal/agents/ready.go @@ -189,6 +189,10 @@ func (m *Manager) tryPromptReady(ctx context.Context, taskID string, fromReview return err } for _, r := range reviews { + if r.State == api.ReviewStateFailed && task.Background { + m.auditError(ctx, task, fmt.Sprintf("Review %q of snapshot %d failed: %s. The audit can't go on without you.", r.Stage, latest.N, r.Error)) + return nil + } if r.State != api.ReviewStateDone && r.State != api.ReviewStateSkipped { return nil } diff --git a/internal/agents/tasks.go b/internal/agents/tasks.go index 162a4b3..b7cf87e 100644 --- a/internal/agents/tasks.go +++ b/internal/agents/tasks.go @@ -75,7 +75,7 @@ func (m *Manager) StartTodo(ctx context.Context, project api.Project, creator ap started := []api.Task{} if workflowName != "" { - started, err = m.StartWorkflow(ctx, project, creator, item.Title, project.DefaultBranch, actor, workflowName, todoPrompt(item, discuss), &item.Title) + started, err = m.StartWorkflow(ctx, project, creator, item.Title, project.DefaultBranch, actor, workflowName, todoPrompt(item, discuss), &item.Title, "") if err != nil { return nil, err } diff --git a/internal/agents/workflows.go b/internal/agents/workflows.go index 13904bb..cf8a437 100644 --- a/internal/agents/workflows.go +++ b/internal/agents/workflows.go @@ -231,7 +231,7 @@ type parentStage struct { commit string } -func (m *Manager) StartWorkflow(ctx context.Context, project api.Project, creator api.User, title, base, actor, name, brief string, todoItem *string) ([]api.Task, error) { +func (m *Manager) StartWorkflow(ctx context.Context, project api.Project, creator api.User, title, base, actor, name, brief string, todoItem *string, audit string) ([]api.Task, error) { if creator.GitName == "" || creator.GitEmail == "" { return nil, ErrNoGitIdentity } @@ -241,6 +241,7 @@ func (m *Manager) StartWorkflow(ctx context.Context, project api.Project, creato } frozen.Brief = brief frozen.Title = title + frozen.Audit = audit run := store.NewID() tasks := []api.Task{} @@ -250,23 +251,29 @@ func (m *Manager) StartWorkflow(ctx context.Context, project api.Project, creato from = project.DefaultBranch } task, err := m.startStage(ctx, project, creator, frozen, run, stage, from, todoItem, nil, actor) + if task.Id != "" { + tasks = append(tasks, task) + } if err != nil { return tasks, fmt.Errorf("workflow %q: stage %q did not start: %w", name, stage, err) } - tasks = append(tasks, task) } return tasks, nil } func (m *Manager) startStage(ctx context.Context, project api.Project, creator api.User, frozen store.FrozenWorkflow, run, stage, base string, todoItem *string, parents []parentStage, actor string) (api.Task, error) { - task, err := m.createTask(ctx, project, api.Task{ + t := api.Task{ Title: fmt.Sprintf("%s: %s", frozen.Title, stage), MergeTitle: frozen.Title, CreatedBy: creator.Login, TodoItem: todoItem, WorkflowRun: &run, WorkflowStage: &stage, - }, &frozen, base, actor) + } + if frozen.Audit != "" { + t.Audit = &frozen.Audit + } + task, err := m.createTask(ctx, project, t, &frozen, base, actor) if err != nil { return api.Task{}, err } @@ -489,7 +496,11 @@ func (m *Manager) advanceTo(ctx context.Context, from api.Task, frozen store.Fro } base := parents[0].commit - if workflow.FromMain(frozen.Definition, stage) { + switch { + case !workflow.FromMain(frozen.Definition, stage): + case frozen.Audit != "": + base = frozen.Audit + default: base = project.DefaultBranch } task, err := m.startStage(ctx, project, creator, frozen, *from.WorkflowRun, stage, base, from.TodoItem, parents, "byre") @@ -811,6 +822,32 @@ func (m *Manager) workflowTools(tool func(name, description, schema string, run return fmt.Sprintf("Project %s reviews with workflow %q from its next snapshot on.", p.Name, p.ReviewWorkflow), nil }), + tool("set_audit_workflow", + "Set the audit workflow (any saved workflow) that byre runs in the background on a project's default branch: one run at a time, started whenever the branch's newest commit hasn't had one, to check the whole codebase against its standards rather than one change. "+ + "An audit's tasks stay out of list_tasks and your signals until one asks a question, calls ready_for_merge or hits an error; one that suggests discarding first is discarded by byre. An empty workflow turns audits off. Only when the human asks.", + `{"type": "object", "properties": { + "project_id": {"type": "string"}, + "workflow": {"type": "string"} + }, "required": ["project_id", "workflow"]}`, + func(ctx context.Context, user api.User, raw json.RawMessage) (string, error) { + args := struct { + ProjectID string `json:"project_id"` + Workflow string `json:"workflow"` + }{} + err := json.Unmarshal(raw, &args) + if err != nil { + return "", err + } + p, err := m.SetAuditWorkflow(ctx, args.ProjectID, args.Workflow, leadActor(user.Login)) + if err != nil { + return "", err + } + if p.AuditWorkflow == "" { + return fmt.Sprintf("Project %s has no audit workflow now: it isn't audited.", p.Name), nil + } + return fmt.Sprintf("Project %s is audited with workflow %q.", p.Name, p.AuditWorkflow), nil + }), + tool("delete_workflow", "Delete a workflow from the server. Tasks that froze it keep their copy. Only when the human asks.", `{"type": "object", "properties": {"name": {"type": "string"}}, "required": ["name"]}`, diff --git a/internal/api/api.gen.go b/internal/api/api.gen.go index 4ad6e9c..809e0f1 100644 --- a/internal/api/api.gen.go +++ b/internal/api/api.gen.go @@ -1078,6 +1078,8 @@ type Preview struct { // Project defines model for Project. type Project struct { + // AuditWorkflow The workflow byre runs in the background, one run at a time, on the default branch's newest commit; empty for no audits. + AuditWorkflow string `json:"audit_workflow"` CreatedAt time.Time `json:"created_at"` CreatedBy string `json:"created_by"` DefaultBranch string `json:"default_branch"` @@ -1096,6 +1098,12 @@ type ProjectCreate struct { RepoPath string `json:"repo_path"` } +// ProjectWorkflow defines model for ProjectWorkflow. +type ProjectWorkflow struct { + // Workflow A saved workflow, or empty for none. + Workflow string `json:"workflow"` +} + // ProtocolError defines model for ProtocolError. type ProtocolError struct { At time.Time `json:"at"` @@ -1157,12 +1165,6 @@ type Review struct { // ReviewState skipped: the snapshot's step changed no files, so no reviewer ran. type ReviewState string -// ReviewWorkflow defines model for ReviewWorkflow. -type ReviewWorkflow struct { - // Workflow A saved workflow, or empty for no reviews. - Workflow string `json:"workflow"` -} - // SessionCreate defines model for SessionCreate. type SessionCreate struct { // Images Blob ids from uploadBlob, sent to the agent as images alongside the prompt. @@ -1288,8 +1290,13 @@ type Tailnet struct { // Task defines model for Task. type Task struct { - BaseRef string `json:"base_ref"` - Branch string `json:"branch"` + // Audit The default branch commit this task audits, for a task of a run of its project's audit workflow; absent for other tasks. + Audit *string `json:"audit,omitempty"` + + // Background Whether the task is an audit that hasn't asked for attention yet: no question, ready_for_merge or error so far. The app keeps it out of the task switcher and shows it muted on the Project tab, and the lead gets no signals about it. + Background bool `json:"background"` + BaseRef string `json:"base_ref"` + Branch string `json:"branch"` // ClosedAt When the task was merged or discarded. ClosedAt *time.Time `json:"closed_at,omitempty"` @@ -1742,8 +1749,11 @@ type SetPaneFocusJSONRequestBody = PaneFocus // CreateProjectJSONRequestBody defines body for CreateProject for application/json ContentType. type CreateProjectJSONRequestBody = ProjectCreate +// SetAuditWorkflowJSONRequestBody defines body for SetAuditWorkflow for application/json ContentType. +type SetAuditWorkflowJSONRequestBody = ProjectWorkflow + // SetReviewWorkflowJSONRequestBody defines body for SetReviewWorkflow for application/json ContentType. -type SetReviewWorkflowJSONRequestBody = ReviewWorkflow +type SetReviewWorkflowJSONRequestBody = ProjectWorkflow // CreateTaskJSONRequestBody defines body for CreateTask for application/json ContentType. type CreateTaskJSONRequestBody = TaskCreate @@ -1877,6 +1887,9 @@ type ServerInterface interface { // (GET /api/projects/{projectId}) GetProject(w http.ResponseWriter, r *http.Request, projectId string) + // (PUT /api/projects/{projectId}/audit-workflow) + SetAuditWorkflow(w http.ResponseWriter, r *http.Request, projectId string) + // (PUT /api/projects/{projectId}/review-workflow) SetReviewWorkflow(w http.ResponseWriter, r *http.Request, projectId string) @@ -2699,6 +2712,32 @@ func (siw *ServerInterfaceWrapper) GetProject(w http.ResponseWriter, r *http.Req handler.ServeHTTP(w, r) } +// SetAuditWorkflow operation middleware +func (siw *ServerInterfaceWrapper) SetAuditWorkflow(w http.ResponseWriter, r *http.Request) { + + var err error + _ = err + + // ------------- Path parameter "projectId" ------------- + var projectId string + + err = runtime.BindStyledParameterWithOptions("simple", "projectId", r.PathValue("projectId"), &projectId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: "", ValueIsUnescaped: true}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "projectId", Err: err}) + return + } + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.SetAuditWorkflow(w, r, projectId) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + // SetReviewWorkflow operation middleware func (siw *ServerInterfaceWrapper) SetReviewWorkflow(w http.ResponseWriter, r *http.Request) { @@ -4125,6 +4164,7 @@ func HandlerWithOptions(si ServerInterface, options StdHTTPServerOptions) http.H m.HandleFunc(http.MethodPost+" "+options.BaseURL+"/api/projects", wrapper.CreateProject) m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/api/projects/{projectId}", wrapper.GetProject) m.HandleFunc(http.MethodPut+" "+options.BaseURL+"/api/projects/{projectId}/review-workflow", wrapper.SetReviewWorkflow) + m.HandleFunc(http.MethodPut+" "+options.BaseURL+"/api/projects/{projectId}/audit-workflow", wrapper.SetAuditWorkflow) m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/api/projects/{projectId}/tasks", wrapper.ListTasks) m.HandleFunc(http.MethodPost+" "+options.BaseURL+"/api/projects/{projectId}/tasks", wrapper.CreateTask) m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/api/projects/{projectId}/todo", wrapper.GetTodo) @@ -4190,306 +4230,311 @@ 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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", + "8KkO4Oerw7FD3CcU5AEDq0aM1xejovvw4X8NAA==", } // decodeSpec returns the embedded OpenAPI spec as raw JSON bytes, diff --git a/internal/git/git.go b/internal/git/git.go index 56d9c84..611bc77 100644 --- a/internal/git/git.go +++ b/internal/git/git.go @@ -326,6 +326,10 @@ func Head(dir string) (string, error) { return run(dir, "rev-parse", "HEAD") } +func BranchHead(repo, branch string) (string, error) { + return run(repo, "rev-parse", "--verify", "refs/heads/"+branch+"^{commit}") +} + func IsAncestor(dir, ancestor, descendant string) (bool, error) { _, stderr, code, err := runCode(dir, nil, "merge-base", "--is-ancestor", ancestor, descendant) if err != nil { diff --git a/internal/server/server.go b/internal/server/server.go index c2d1c03..3d09943 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -361,7 +361,7 @@ func (s *Server) CreateTask(w http.ResponseWriter, r *http.Request, projectID st writeStoreError(w, err) return } - tasks, err := s.agents.StartWorkflow(r.Context(), p, u, title, p.DefaultBranch, u.Login, *req.Workflow, title, nil) + tasks, err := s.agents.StartWorkflow(r.Context(), p, u, title, p.DefaultBranch, u.Login, *req.Workflow, title, nil, "") if err != nil { writeAgentError(w, err) return diff --git a/internal/server/workflows.go b/internal/server/workflows.go index b4d65c2..c418185 100644 --- a/internal/server/workflows.go +++ b/internal/server/workflows.go @@ -63,7 +63,7 @@ func (s *Server) PutWorkflow(w http.ResponseWriter, r *http.Request, name string } func (s *Server) SetReviewWorkflow(w http.ResponseWriter, r *http.Request, projectID string) { - req := api.ReviewWorkflow{} + req := api.ProjectWorkflow{} if !readJSON(w, r, &req) { return } @@ -75,6 +75,19 @@ func (s *Server) SetReviewWorkflow(w http.ResponseWriter, r *http.Request, proje writeJSON(w, http.StatusOK, p) } +func (s *Server) SetAuditWorkflow(w http.ResponseWriter, r *http.Request, projectID string) { + req := api.ProjectWorkflow{} + if !readJSON(w, r, &req) { + return + } + p, err := s.agents.SetAuditWorkflow(r.Context(), projectID, req.Workflow, actor(r)) + if err != nil { + writeAgentError(w, err) + return + } + writeJSON(w, http.StatusOK, p) +} + func (s *Server) DeleteWorkflow(w http.ResponseWriter, r *http.Request, name string) { err := s.store.DeleteWorkflow(r.Context(), name, actor(r)) if err != nil { diff --git a/internal/store/migrations/077-add-audits.sql b/internal/store/migrations/077-add-audits.sql new file mode 100644 index 0000000..41d902b --- /dev/null +++ b/internal/store/migrations/077-add-audits.sql @@ -0,0 +1,2 @@ +ALTER TABLE projects ADD COLUMN audit_workflow TEXT NOT NULL DEFAULT ''; +ALTER TABLE tasks ADD COLUMN audit TEXT NOT NULL DEFAULT ''; diff --git a/internal/store/store.go b/internal/store/store.go index 7787752..cd9e6bc 100644 --- a/internal/store/store.go +++ b/internal/store/store.go @@ -295,8 +295,16 @@ func (s *Store) ListProjects(ctx context.Context) ([]api.Project, error) { } func (s *Store) SetReviewWorkflow(ctx context.Context, id, workflow, actor string) (api.Project, error) { + return s.setProjectWorkflow(ctx, "review_workflow", id, workflow, actor) +} + +func (s *Store) SetAuditWorkflow(ctx context.Context, id, workflow, actor string) (api.Project, error) { + return s.setProjectWorkflow(ctx, "audit_workflow", id, workflow, actor) +} + +func (s *Store) setProjectWorkflow(ctx context.Context, column, id, workflow, actor string) (api.Project, error) { err := s.write(ctx, func(tx *sql.Tx, emit emitFunc) error { - res, err := tx.ExecContext(ctx, `UPDATE projects SET review_workflow = ? WHERE id = ?`, workflow, id) + res, err := tx.ExecContext(ctx, `UPDATE projects SET `+column+` = ? WHERE id = ?`, workflow, id) if err != nil { return err } @@ -367,6 +375,11 @@ func (s *Store) CreateTask(ctx context.Context, t api.Task, frozen *FrozenWorkfl if t.ParentTaskId != nil { parentTaskID = *t.ParentTaskId } + audit := "" + if t.Audit != nil { + audit = *t.Audit + t.Background = true + } return s.write(ctx, func(tx *sql.Tx, emit emitFunc) error { if parentTaskID != "" { open := false @@ -398,9 +411,9 @@ func (s *Store) CreateTask(ctx context.Context, t api.Task, frozen *FrozenWorkfl return err } _, err = tx.ExecContext(ctx, ` - INSERT INTO tasks (id, project_id, title, merge_title, branch, worktree_path, base_ref, port_base, color, status, created_by, created_at, todo_item, forked_from, forked_at, workflow_name, workflow_definition, workflow_brief, workflow_title, workflow_run, workflow_stage, parent_task_id) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - `, t.Id, t.ProjectId, t.Title, mergeTitle, t.Branch, t.WorktreePath, t.BaseRef, t.PortBase, t.Color, string(t.Status), t.CreatedBy, formatTime(t.CreatedAt), todoItem, forkedFrom, forkedAt, workflowName, workflowDefinition, workflowBrief, workflowTitle, workflowRun, workflowStage, parentTaskID) + INSERT INTO tasks (id, project_id, title, merge_title, branch, worktree_path, base_ref, port_base, color, status, created_by, created_at, todo_item, forked_from, forked_at, workflow_name, workflow_definition, workflow_brief, workflow_title, workflow_run, workflow_stage, parent_task_id, audit) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `, t.Id, t.ProjectId, t.Title, mergeTitle, t.Branch, t.WorktreePath, t.BaseRef, t.PortBase, t.Color, string(t.Status), t.CreatedBy, formatTime(t.CreatedAt), todoItem, forkedFrom, forkedAt, workflowName, workflowDefinition, workflowBrief, workflowTitle, workflowRun, workflowStage, parentTaskID, audit) if err != nil { return err } @@ -1682,9 +1695,11 @@ func (s *Store) ClearReviewRequests(ctx context.Context) error { }) } -const projectColumns = `id, name, repo_path, default_branch, remote, created_by, created_at, review_workflow` +const projectColumns = `id, name, repo_path, default_branch, remote, created_by, created_at, review_workflow, audit_workflow` -const taskColumns = `id, project_id, title, merge_title, branch, worktree_path, base_ref, port_base, color, status, created_by, created_at, closed_at, merge_commit, todo_item, forked_from, forked_at, merge_queued_at, workflow_name, workflow_run, workflow_stage, parent_task_id, parent_merge_by, merge_after_review_by != '', state, merging_by != '' OR finalizing != '', discard_state != '' AND status = 'active', (SELECT coalesce(max(seq), 0) FROM events WHERE events.task_id = tasks.id AND kind = 'turn.ended' AND json_extract(payload, '$.summary') = 1)` +const taskColumns = `id, project_id, title, merge_title, branch, worktree_path, base_ref, port_base, color, status, created_by, created_at, closed_at, merge_commit, todo_item, forked_from, forked_at, merge_queued_at, workflow_name, workflow_run, workflow_stage, parent_task_id, parent_merge_by, merge_after_review_by != '', state, merging_by != '' OR finalizing != '', discard_state != '' AND status = 'active', (SELECT coalesce(max(seq), 0) FROM events WHERE events.task_id = tasks.id AND kind = 'turn.ended' AND json_extract(payload, '$.summary') = 1), audit, ` + backgroundExpr + +const backgroundExpr = `audit != '' AND NOT EXISTS (SELECT 1 FROM events WHERE events.task_id = tasks.id AND kind IN ('inbox.created', 'task.ready', 'session.error', 'session.rate_limited'))` const sessionColumns = `id, coalesce(task_id, ''), owner, vendor, model, vendor_session_id, state, role, member, created_by, created_at, forked_from, forked_at, subagents` @@ -1695,7 +1710,7 @@ type scanner interface { func scanProject(row scanner) (api.Project, error) { p := api.Project{} createdAt := "" - err := row.Scan(&p.Id, &p.Name, &p.RepoPath, &p.DefaultBranch, &p.Remote, &p.CreatedBy, &createdAt, &p.ReviewWorkflow) + err := row.Scan(&p.Id, &p.Name, &p.RepoPath, &p.DefaultBranch, &p.Remote, &p.CreatedBy, &createdAt, &p.ReviewWorkflow, &p.AuditWorkflow) if err != nil { return p, err } @@ -1719,11 +1734,15 @@ func scanTask(row scanner) (api.Task, error) { parentTaskID := "" parentMergeBy := "" taskState := "" - err := row.Scan(&t.Id, &t.ProjectId, &t.Title, &t.MergeTitle, &t.Branch, &t.WorktreePath, &t.BaseRef, &t.PortBase, &t.Color, &status, &t.CreatedBy, &createdAt, &closedAt, &mergeCommit, &todoItem, &forkedFrom, &forkedAt, &mergeQueuedAt, &workflowName, &workflowRun, &workflowStage, &parentTaskID, &parentMergeBy, &t.MergeAfterReview, &taskState, &t.Merging, &t.Discarding, &t.SummarySeq) + audit := "" + err := row.Scan(&t.Id, &t.ProjectId, &t.Title, &t.MergeTitle, &t.Branch, &t.WorktreePath, &t.BaseRef, &t.PortBase, &t.Color, &status, &t.CreatedBy, &createdAt, &closedAt, &mergeCommit, &todoItem, &forkedFrom, &forkedAt, &mergeQueuedAt, &workflowName, &workflowRun, &workflowStage, &parentTaskID, &parentMergeBy, &t.MergeAfterReview, &taskState, &t.Merging, &t.Discarding, &t.SummarySeq, &audit, &t.Background) if err != nil { return t, err } t.State = api.TaskState(taskState) + if audit != "" { + t.Audit = &audit + } if parentTaskID != "" { t.ParentTaskId = &parentTaskID } diff --git a/internal/store/workflow.go b/internal/store/workflow.go index a522e7f..723a2b2 100644 --- a/internal/store/workflow.go +++ b/internal/store/workflow.go @@ -15,6 +15,7 @@ type FrozenWorkflow struct { Definition api.WorkflowDefinition Brief string Title string + Audit string } func (s *Store) ListWorkflows(ctx context.Context) ([]api.WorkflowSummary, error) { @@ -81,7 +82,7 @@ func (s *Store) PutWorkflow(ctx context.Context, name, from string, def api.Work if err != nil { return err } - err = repointReviews(ctx, tx, emit, from, name, actor) + err = repointProjects(ctx, tx, emit, from, name, actor) if err != nil { return err } @@ -99,8 +100,13 @@ func (s *Store) PutWorkflow(ctx context.Context, name, from string, def api.Work }) } -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) +func repointProjects(ctx context.Context, tx *sql.Tx, emit emitFunc, from, to, actor string) error { + rows, err := tx.QueryContext(ctx, ` + UPDATE projects SET + review_workflow = CASE WHEN review_workflow = ?1 THEN ?2 ELSE review_workflow END, + audit_workflow = CASE WHEN audit_workflow = ?1 THEN ?2 ELSE audit_workflow END + WHERE review_workflow = ?1 OR audit_workflow = ?1 + RETURNING `+projectColumns, from, to) if err != nil { return err } @@ -139,7 +145,7 @@ 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) + err = repointProjects(ctx, tx, emit, name, "", actor) if err != nil { return err } @@ -158,7 +164,7 @@ func (s *Store) FreezeWorkflow(ctx context.Context, name string) (FrozenWorkflow func (s *Store) TaskWorkflow(ctx context.Context, taskID string) (FrozenWorkflow, error) { f := FrozenWorkflow{} def := "" - err := s.db.QueryRowContext(ctx, `SELECT workflow_name, workflow_definition, workflow_brief, workflow_title FROM tasks WHERE id = ?`, taskID).Scan(&f.Name, &def, &f.Brief, &f.Title) + err := s.db.QueryRowContext(ctx, `SELECT workflow_name, workflow_definition, workflow_brief, workflow_title, audit FROM tasks WHERE id = ?`, taskID).Scan(&f.Name, &def, &f.Brief, &f.Title, &f.Audit) if errors.Is(err, sql.ErrNoRows) { return FrozenWorkflow{}, fmt.Errorf("%w: no task %s", ErrNotFound, taskID) } @@ -172,6 +178,17 @@ func (s *Store) TaskWorkflow(ctx context.Context, taskID string) (FrozenWorkflow return f, err } +func (s *Store) LastAudit(ctx context.Context, projectID string) (string, bool, error) { + commit := "" + active := false + err := s.db.QueryRowContext(ctx, ` + SELECT + coalesce((SELECT audit FROM tasks WHERE project_id = ?1 AND audit != '' ORDER BY rowid DESC LIMIT 1), ''), + EXISTS (SELECT 1 FROM tasks WHERE project_id = ?1 AND audit != '' AND status = ?2) + `, projectID, string(api.TaskStatusActive)).Scan(&commit, &active) + return commit, active, err +} + func (s *Store) RunTasks(ctx context.Context, run string) ([]api.Task, error) { return s.queryTasks(ctx, `SELECT `+taskColumns+` FROM tasks WHERE workflow_run = ? ORDER BY created_at`, run) } diff --git a/mac/Sources/Byre/Notifications.swift b/mac/Sources/Byre/Notifications.swift index 59d31de..b44d0d8 100644 --- a/mac/Sources/Byre/Notifications.swift +++ b/mac/Sources/Byre/Notifications.swift @@ -125,16 +125,17 @@ func notifyTask(_ store: Store, _ ev: ByreEvent) { } let prefs = me.notificationPrefs let origin = store.api.origin.absoluteString + let background = store.findTask(taskId)?.background == true switch ev.kind { case "task.ready" where prefs.ready: show(store, taskId, "task-\(taskId)@\(origin)", "Ready to merge: \(str(ev.payload.value, "overview") ?? "")") - case "task.discard_suggested" where prefs.discard: + case "task.discard_suggested" where prefs.discard && !background: show(store, taskId, "task-\(taskId)@\(origin)", "Suggests discard: \(str(ev.payload.value, "reason") ?? "")") case "task.merge_stopped" where prefs.mergeProblems: show(store, taskId, "task-\(taskId)@\(origin)", "Merge stopped: \(str(ev.payload.value, "reason") ?? "")") case "task.merge_after_review_dropped" where prefs.mergeProblems: show(store, taskId, "task-\(taskId)@\(origin)", "Didn't merge after the review: \(str(ev.payload.value, "reason") ?? "")") - case "task.workflow_finished" where prefs.workflows: + case "task.workflow_finished" where prefs.workflows && !background: show(store, taskId, "workflow-\(ev.seq)@\(origin)", "Finished \(str(ev.payload.value, "workflow") ?? ""): \(str(ev.payload.value, "output") ?? "")") default: break @@ -151,7 +152,7 @@ func clearTask(_ store: Store, _ taskId: String) { @MainActor func notifyTurn(_ store: Store, _ ev: ByreEvent) { - guard let me = store.me, me.notificationPrefs.reports, ev.payload.value["summary"] as? Bool == true, let taskId = ev.taskId, !taskId.isEmpty else { + guard let me = store.me, me.notificationPrefs.reports, ev.payload.value["summary"] as? Bool == true, let taskId = ev.taskId, !taskId.isEmpty, store.findTask(taskId)?.background != true else { return } show(store, taskId, "turn-\(ev.seq)@\(store.api.origin.absoluteString)", str(ev.payload.value, "result") ?? "") diff --git a/mac/Sources/Byre/Project.swift b/mac/Sources/Byre/Project.swift index d17e985..50f835f 100644 --- a/mac/Sources/Byre/Project.swift +++ b/mac/Sources/Byre/Project.swift @@ -80,7 +80,7 @@ struct ProjectView: View { guard let p = store.focusedProject() else { return } - let rows = nested(store, (store.tasks[p.id] ?? []).filter { $0.status == .active }).map(\.task.id) + let rows = nested(store, (store.tasks[p.id] ?? []).filter { $0.status == .active && !$0.background }).map(\.task.id) if rows.isEmpty { return } @@ -110,21 +110,24 @@ struct ProjectView: View { } } -private struct ReviewWorkflowMenu: View { +private struct ProjectWorkflowMenu: View { let project: Project + let audit: Bool @Environment(Store.self) private var store @Environment(Overlays.self) private var overlays - @ViewState private var reviewWorkflows: [String] = [] + @ViewState private var workflows: [String] = [] var body: some View { + let current = audit ? project.auditWorkflow : project.reviewWorkflow + let kind = audit ? "audits" : "reviews" Menu { - Button("No reviews") { set("") } - ForEach(reviewWorkflows, id: \.self) { name in + Button("No \(kind)") { set("") } + ForEach(workflows, id: \.self) { name in Button(name) { set(name) } } } label: { HStack(spacing: 6) { - Text(project.reviewWorkflow.isEmpty ? "no reviews" : "reviews: \(project.reviewWorkflow)") + Text(current.isEmpty ? "no \(kind)" : "\(kind): \(current)") Image(systemName: "chevron.up.chevron.down") .font(.system(size: 8, weight: .semibold)) } @@ -136,12 +139,12 @@ private struct ReviewWorkflowMenu: View { .buttonStyle(.plain) .menuIndicator(.hidden) .fixedSize() - .help("The review workflow whose reviewers review every snapshot of this project's tasks") + .help(audit ? "The workflow byre runs in the background on \(project.defaultBranch)'s newest commit to check the whole codebase" : "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.map(\.name) + workflows = try await store.api.client.listWorkflows().ok.body.json.map(\.name) } catch { - reviewWorkflows = [] + workflows = [] } } } @@ -149,7 +152,11 @@ private struct ReviewWorkflowMenu: View { private func set(_ name: String) { Task { do { - _ = try await store.api.client.setReviewWorkflow(path: .init(projectId: project.id), body: .json(.init(workflow: name))).ok.body.json + if audit { + _ = try await store.api.client.setAuditWorkflow(path: .init(projectId: project.id), body: .json(.init(workflow: name))).ok.body.json + } else { + _ = try await store.api.client.setReviewWorkflow(path: .init(projectId: project.id), body: .json(.init(workflow: name))).ok.body.json + } } catch { overlays.showAlert(errorText(error)) } @@ -169,12 +176,17 @@ private struct ProjectBlock: View { Text(project.name).font(Fonts.font(13, weight: 700)) Text(project.defaultBranch).font(Fonts.font(11)).foregroundStyle(Palette.muted) Spacer() - ReviewWorkflowMenu(project: project) + ProjectWorkflowMenu(project: project, audit: true) + ProjectWorkflowMenu(project: project, audit: false) } .padding(.top, 16) .padding(.bottom, 6) heading("Tasks") - TaskRows(active: active) + TaskRows(active: active.filter { !$0.background }) + ForEach(active.filter(\.background), id: \.id) { task in + BackgroundRow(task: task) + .id(task.id) + } heading("TODO") TodoBlock(project: project, todo: todo, active: active) } @@ -293,6 +305,28 @@ private struct TaskRow: View { } } +private struct BackgroundRow: View { + let task: ByreTask + @Environment(Store.self) private var store + + var body: some View { + let ts = store.taskState(task) + HoverRow(selected: task.id == store.selectedTaskId) { + store.focusTask(task.id) + } content: { + Text(task.name) + .lineLimit(1) + .truncationMode(.tail) + .frame(maxWidth: .infinity, alignment: .leading) + Text(ts.label) + .help(ts.title) + } + .font(Fonts.font(11)) + .foregroundStyle(Palette.muted) + .help("Runs in the background until it asks for your attention") + } +} + private struct TodoBlock: View { let project: Project let todo: Result? diff --git a/mac/Sources/Byre/Store.swift b/mac/Sources/Byre/Store.swift index 0e6312e..97eeddf 100644 --- a/mac/Sources/Byre/Store.swift +++ b/mac/Sources/Byre/Store.swift @@ -688,6 +688,11 @@ final class Store { } case "session.error", "session.rate_limited": notifyError(self, ev) + if let projectId = ev.projectId { + spawn("load tasks") { + try await self.loadTasks(projectId) + } + } case "task.merge_stopped", "task.merge_after_review_dropped", "task.workflow_finished": notifyTask(self, ev) case "turn.ended": diff --git a/mac/Sources/Byre/Switcher.swift b/mac/Sources/Byre/Switcher.swift index 6b8b7b8..5904a7d 100644 --- a/mac/Sources/Byre/Switcher.swift +++ b/mac/Sources/Byre/Switcher.swift @@ -196,6 +196,9 @@ final class SwitcherModel { let visits = store.lastVisits() let matches = store.activeTasks() .filter { row in + if row.task.background { + return false + } if let targets { return targets.contains { $0.id == row.project.id } && includes(row.task.title.lowercased(), q) }