ready to merge and ready to discard as states

This commit is contained in:
Ian Gulliver
2026-10-19 17:53:54 -07:00
parent fb79919ebb
commit 2bb527aca0
11 changed files with 394 additions and 293 deletions
+3 -1
View File
@@ -41,6 +41,8 @@ An implementer says its task is ready to merge with the `ready_for_merge` MCP to
An implementer that thinks its task shouldn't land at all (the approach failed, the task turned out unnecessary or already done) says so with the `suggest_discard` MCP tool (also `internal/agents/ready.go`), whose `reason` explains why; `readyPrompt` offers it as the alternative to `ready_for_merge`. Each call records a `task.discard_suggested` event, which the lead gets as a signal and the transcript draws with the same `ReadyBubble`, headed "suggests discarding". It takes part in "latest" with the ready-for-merge overviews, so whichever of the two came last is the current one, drawn and cleared under the same rules, and it carries only the Discard button. It only suggests: the task stays open until the human discards it.
A Task's `ready` (`tasks.ready`) is `merge` or `discard` after the latest of these two calls, set in the same transaction as its event, and is cleared (absent in the API) by the same things that clear the latest bubble: a snapshot other than an answer snapshot, or a new inbox item. The switcher's and the Project tab's task rows show it as a **Ready to merge** or **Ready to discard** state, in the warn colour with a warn dot like **Needs your answer**, when no question, merge or running work outranks it (`taskState` in `mac/Sources/Byre/Store.swift`). The app reloads a project's tasks on `task.ready`, `task.discard_suggested`, `snapshot.created` and `inbox.created`.
Agents find closed tasks with the `search_closed_tasks` MCP tool, which task agents get for their own project and the lead for one project (`project_id`) or all: a case-insensitive substring match of `query` on title, merge title, TODO item, summary and merge commit (`Store.SearchClosedTasks`), newest closed first, at most `limit` (default 20, at most 100), each with its id, outcome, merge commit, close time, creator, titles, TODO item, fork source and summary. The lead reads a closed task's transcript with `read_transcript`. There is no closed-task list in the UI; the human asks the lead.
**Knowledge base.** How byre features work that agents need only now and then lives in articles they fetch on demand instead of in their guidance: the Markdown files in `internal/agents/kb/`, embedded in the binary (`kb.go`). A file's name without `.md` is its topic, its first line a one-line summary and the rest the article. The `read_knowledge` MCP tool, which task agents and the lead get, lists every topic with its summary in its description and returns the article for `topic`. Adding an article is adding a file; the index follows. The articles are `signing` (turning on signed merge commits), `usage-commands` (the usage command's JSON format, see "Usage" under "Status"), `workflow-files` (a workflow's YAML, see "Workflows"; `read_workflow` and `save_workflow` point at it) and `todo-file` (the `TODO.md` format; the task guidance points at it).
@@ -276,7 +278,7 @@ Messages byre itself sends an agent (actor `byre`: the restart notice, Sync and
**Drafts.** Unsent composer text (task and lead) and unsent inbox answers (checked choices and Other text) are kept in memory by `Drafts` (`mac/Sources/Byre/Drafts.swift`) and saved to a SwiftData `DraftRecord` per server and login (`docs/macos.md`, "Drafts"), one JSON object with `composer` (keyed by `message:<task id>` or `message:lead`, each `{text, images}`), `checked` and `texts` (both keyed by inbox item id), and `comments`, the Changes view's pending line comments keyed by task id, each `{anchor: {path, line, old, quote}, text}`. A comment shows only under a diff line whose path, line number, side and text all still match, so after the agent moves lines a comment never attaches to a different line. Every pending comment not shown under a line (its file closed or not loaded, or its line gone from the diff) is listed at the top of the Changes view with its `path:line`, the quoted line, its editable text and **remove**, so nothing is sent unseen. Writes are debounced (400 ms) and flushed at once when a composer or answer box loses focus, on send and on quit. A composer draft is cleared only once its send succeeds, and an inbox answer only once its response is accepted; each send in flight is tracked on its own and the stored draft holds the text of every unconfirmed send (oldest first, then the composer's current text, separated by blank lines) and counts its images, so a relaunch mid-send brings back everything not yet confirmed. A failed send's text goes back into the composer ahead of whatever is there now (text typed since, or earlier failures), separated by a blank line, rather than replacing it. The composer follows its `Drafts` text as it changes, so text put back from outside it (a withdrawn queued message, a draft synced in from another Mac) shows at once. Drafts for tasks that are no longer active and for inbox items no longer open are pruned whenever the task list or inbox reloads. A record that doesn't decode is logged and ignored, as is a write that fails; drafts then live only in memory. Pending images are not stored, only their count: after a relaunch the composer shows how many need re-attaching, until dismissed or the next successful send.
The task switcher (⌘K, or the project ▸ task picker centred in the right pane's top row) lists active tasks with their task state and open inbox item count (the Project tab shows the same state), live while it is open. Tasks with inbox items come first; within each group, the task least recently visited comes first (never-visited tasks before all others, the right pane's own task after all others, ties in the server's order: projects by name, tasks newest first), so ⌘K Enter repeated cycles through the tasks. Visit times are SwiftData `VisitRecord`s synced through iCloud (`docs/macos.md`, "Persistence"), stamped when a task is selected and again when it is left. After the tasks come the projects, one row each; picking one points the right pane at the project with no task, on the Project tab. Typing filters both and adds a last row that creates a task with the typed text as its title, in the right pane's project (the first project when there is none); Enter picks the highlighted row. Text before a first colon that starts at least one project's name (ignoring case, no spaces) picks those projects instead: `by: fix` lists only their tasks whose titles contain "fix", their project rows that match, and one row per project creating the task titled "fix" there. When no project name starts with it, the whole text is the title, so "Fix: login" stays as typed. A create row shows the user's default model (see "Agent sessions"), the model the composer offers first for the new task's first message; the task itself starts with no session. Below each create row, a "Start in …" row creates the same task and starts its agent on the default model with the title as its first message, through `POST /api/projects/{id}/tasks` with `prompt` set to the title. Picking a task points the right pane at it on the Transcript tab, scrolled to its oldest open question when it has open inbox items, otherwise to its latest summary when that is unread (see below and "Lead agent").
The task switcher (⌘K, or the project ▸ task picker centred in the right pane's top row) lists active tasks with their task state and open inbox item count (the Project tab shows the same state), live while it is open. Tasks are grouped by state (`taskRank` in `mac/Sources/Byre/Store.swift`, which also orders the Project tab's task list, ties in the server's order, subtasks sorted among their siblings under their parent): with inbox items first, then **Ready to merge** or **Ready to discard**, then idle, then working or reviewing, and merging, held or discarding last; within each group, the task least recently visited comes first (never-visited tasks before all others, the right pane's own task after all others, ties in the server's order: projects by name, tasks newest first), so ⌘K Enter repeated cycles through the tasks. Visit times are SwiftData `VisitRecord`s synced through iCloud (`docs/macos.md`, "Persistence"), stamped when a task is selected and again when it is left. After the tasks come the projects, one row each; picking one points the right pane at the project with no task, on the Project tab. Typing filters both and adds a last row that creates a task with the typed text as its title, in the right pane's project (the first project when there is none); Enter picks the highlighted row. Text before a first colon that starts at least one project's name (ignoring case, no spaces) picks those projects instead: `by: fix` lists only their tasks whose titles contain "fix", their project rows that match, and one row per project creating the task titled "fix" there. When no project name starts with it, the whole text is the title, so "Fix: login" stays as typed. A create row shows the user's default model (see "Agent sessions"), the model the composer offers first for the new task's first message; the task itself starts with no session. Below each create row, a "Start in …" row creates the same task and starts its agent on the default model with the title as its first message, through `POST /api/projects/{id}/tasks` with `prompt` set to the title. Picking a task points the right pane at it on the Transcript tab, scrolled to its oldest open question when it has open inbox items, otherwise to its latest summary when that is unread (see below and "Lead agent").
**Task state.** The switcher and the Project tab show one label per task in place of its raw session state, the first that applies (`taskState` in `mac/Sources/Byre/Store.swift`), with a dot and a tooltip saying why: **Needs your answer** (an open `question` inbox item), **Merging** (`merging` or `merging_into_parent`), **Merge held** (`merge_queued_at`), **Merge after review** (`merge_after_review`), **Reviewing** (the task's `state` is `review_pending` or `reviewing`), **Working** (`running`), **Discarding** (`discarding`; see "Task state in the store" for both), **Idle** (anything else, including no session). The Project tab, in its task list and TODO items, draws a spinner in place of the dot for **Reviewing**, **Working**, **Discarding** and **Merging**. The label follows the tasks and inbox state the app already holds, so it needs no extra fetch.