Bold questions
This commit is contained in:
@@ -8,6 +8,7 @@ Transcripts (`Options.Transcript`) record every line in both directions as `{"di
|
||||
|
||||
```
|
||||
claude --output-format stream-json --input-format stream-json --verbose \
|
||||
--include-partial-messages \
|
||||
--permission-mode bypassPermissions --allow-dangerously-skip-permissions \
|
||||
--settings '{"attribution":{"commit":"","pr":""}}' \
|
||||
--mcp-config '{"mcpServers":{"byre":{"type":"sdk","name":"byre"}}}' \
|
||||
@@ -26,6 +27,7 @@ NDJSON both ways: one JSON object per line on stdin and stdout. Non-JSON stdout
|
||||
|
||||
- User turn, written to stdin: `{"type":"user","message":{"role":"user","content":"..."},"parent_tool_use_id":null,"session_id":"..."}`.
|
||||
- Stdout events: `system` (subtype `init` carries `session_id`, `model`, tools, MCP server status), `assistant` and `user` (Anthropic message content blocks: `text`, `tool_use`, `tool_result`), `result` (ends a turn; `subtype` is `success` or `error_during_execution` after an interrupt, with cost, duration, turn count).
|
||||
- `--include-partial-messages` adds `stream_event` lines wrapping the raw API stream (`message_start`, `content_block_start`, `content_block_delta` with `text_delta`, `content_block_stop`, `message_delta`, `message_stop`), each with `parent_tool_use_id` (set inside subagents). The complete `assistant` event for a block still arrives, before that block's `content_block_stop`. Transcripts leave `stream_event` lines out.
|
||||
- The `init` event arrives with the first turn, not at launch, so the session ID is unknown until the first message is sent.
|
||||
|
||||
## Control channel
|
||||
@@ -36,7 +38,8 @@ Host → CLI:
|
||||
|
||||
- `initialize` must be first. It carries hook registrations: `{"hooks":{"Stop":[{"matcher":null,"hookCallbackIds":["hook_0"]}],...}}`.
|
||||
- `interrupt` ends the current turn; the turn's `result` follows almost immediately and a running Bash tool is killed. The session stays usable.
|
||||
- `set_model`, `set_permission_mode`, `mcp_status`, `get_context_usage`, `rewind_files`, `stop_task` exist and are not used yet.
|
||||
- `set_model` (`{"subtype":"set_model","model":M}`) switches the running process's model for later turns.
|
||||
- `set_permission_mode`, `mcp_status`, `get_context_usage`, `rewind_files`, `stop_task` exist and are not used yet.
|
||||
|
||||
CLI → host:
|
||||
|
||||
|
||||
+32
-9
@@ -6,16 +6,21 @@
|
||||
- `api/openapi.yaml` is the API source of truth. `go generate ./internal/api` regenerates `internal/api/api.gen.go`, whose types are also the store's model types.
|
||||
- `internal/store`: SQLite (WAL). Every mutation and its event commit in one transaction; events are published to SSE subscribers in `seq` order.
|
||||
- `internal/server`: HTTP handlers, identity, SSE.
|
||||
- `internal/git`: worktree create and remove.
|
||||
- `internal/git`: worktrees, the task changes view (see "Changes"), unmerged-change summaries and squash merges.
|
||||
- `internal/agents`: the agent session manager. It owns the running Claude processes and records their activity as events.
|
||||
- `internal/claude`: the Claude Code stream-json client (see `docs/claude-protocol.md`).
|
||||
- `internal/report`: the `report` MCP tool and the Stop hook that requires it each turn.
|
||||
- `web/`: the SPA, frameworkless TypeScript compiled file-by-file to ES modules (no bundler). `web/src/*.ts` is the source; `web/src/api.gen.ts` holds types generated from `api/openapi.yaml`, and `web/src/api.ts` is the typed client over them. `go generate ./web` runs `npm ci`, regenerates the types and compiles `web/dist/`, which is committed so `go install` needs no Node. The binary embeds `index.html`, `style.css`, `dist/` and `src/` (for source maps). `byre serve --dev` serves `./web` from disk instead (run from the repo root); pair it with `npm --prefix web exec tsc -- --watch`.
|
||||
|
||||
## Data
|
||||
|
||||
State lives in the `byre` directory under Go's `os.UserConfigDir()` (on macOS, `~/Library/Application Support/byre`): `byre.db` and `worktrees/<project-id>/<task-id>`. Task branches are `byre/<task-id>` in the project's repo.
|
||||
|
||||
## Closing a task
|
||||
|
||||
A task ends `merged` or `discarded`; either way its sessions are stopped, its worktree and branch are removed, and its record and events remain. `GET /api/tasks/{id}/unmerged` reports what would be lost or merged: commits on the branch that aren't on the default branch, and files, lines and uncommitted paths that differ from the merge base. The UI shows it in the confirm dialog of both buttons.
|
||||
|
||||
`POST /api/tasks/{id}/merge` squashes the worktree as it stands, uncommitted and untracked files included (captured through a temporary index, so the worktree isn't touched), onto the default branch with `git merge-tree --merge-base`, makes one commit with `git commit-tree` authored and committed by the merging user, and runs `git merge --ff-only` in the project's checkout, which must have the default branch checked out. The commit message is the task title. Nothing is pushed. It is refused (409) while a session is `running` or `queued`, on merge conflicts (the message lists the files; the task stays active), and if the default branch moved between the merge and the fast-forward. When the merge changes nothing, no commit is made. `POST /api/tasks/{id}/discard` removes everything without merging.
|
||||
|
||||
## Running as a service
|
||||
|
||||
The listen address is `listenAddr` in `cmd/byre/serve.go`; it binds loopback only.
|
||||
@@ -45,21 +50,33 @@ curl -H 'Tailscale-User-Login: you@example.com' -H 'Tailscale-User-Name: You' ht
|
||||
|
||||
## Agent sessions
|
||||
|
||||
`POST /api/tasks/{id}/sessions` starts a Claude session in the task's worktree and sends the prompt as its first turn; `POST /api/sessions/{id}/messages` starts later turns and `POST /api/sessions/{id}/interrupt` ends the current one. A task has at most one session that isn't `completed`. The session's commits use the git identity of the user who created it, so that user must have set one with `PUT /api/me`.
|
||||
`POST /api/tasks/{id}/sessions` starts a Claude session in the task's worktree and sends the prompt as its first turn; `POST /api/sessions/{id}/messages` starts later turns and `POST /api/sessions/{id}/interrupt` ends the current one. A task has at most one session that isn't `completed`. A message may carry `model`; when it differs from the session's, byre writes it to `agent_sessions.model`, records a `session.model` event (shown as a note), and sends the live CLI a `set_model` control request before the message, so the switch applies from that turn and survives a resume (`--model`) or compaction. The composer's model menu sends `model` only when the choice differs from the session's model, or from the `session.context` model when the session has none. The session's commits use the git identity of the user who created it, so that user must have set one with `PUT /api/me`.
|
||||
|
||||
byre chooses the Claude session ID (`vendor_session_id`) up front, so every session can be resumed. Processes start on demand: the first message after a restart resumes the session with `--resume`. Sessions are `running` from the moment a turn is sent until its `result` arrives; on startup, every session still marked `running` was cut off mid-turn, and byre resumes it and sends a message telling the agent to re-check the working tree. Stopping the server kills all agent processes. Completing a task kills its sessions and marks them `completed`.
|
||||
Compaction is vendor-agnostic: `POST /api/sessions/{id}/compact` (the transcript's **Compact** button, enabled while the session is idle) sends the agent `handoffPrompt` (`internal/agents/compact.go`) as a `byre` message, asking for a handoff covering the goal, decisions and why, current state, dead ends, open questions and files. That turn files no inbox summary and isn't bounced for questions. When it ends normally, its `turn.ended` carries `handoff: true`, the session is completed, and a new session on the same task and model starts with `handoffPreamble` plus the handoff as its first message (actor `byre`). The context meter tracks the new session. Once the handoff turn ends, the transcript folds the request and everything the agent did in that turn into one collapsed note. If byre restarts during the handoff turn, the continuation is lost and the old session simply goes idle.
|
||||
|
||||
Running turns are capped globally and per vendor (`DefaultLimits` in `internal/agents/manager.go`). A turn that would exceed a cap leaves its session `queued`, with the message held in the `turn_queue` table; whenever a turn ends, a process exits, or a task completes, the oldest queued turns whose vendor has room start. Interrupting a `queued` session cancels its queued turn. Idle sessions with a live process don't count against the caps. Whether a turn resumes the Claude conversation is decided by whether the session has ever sent one.
|
||||
Agents spawn parallel work with the `create_task` MCP tool (`internal/agents/tasks.go`): `title`, `prompt`, `base` and optional `model` (default: the caller's session model). It creates a task in the caller's project exactly as `POST /api/projects/{id}/tasks` does, and starts a session there with `prompt` as the first message (actor `agent`), created by the caller's session creator so commits use that user's git identity. `base` is the agent's choice: `default_branch` starts from the project's default branch, `this_task` from the caller's worktree `HEAD`, which leaves out uncommitted edits from the current turn. The tool returns the new task's id at once; the new session is independent of the caller.
|
||||
|
||||
Agents don't commit; byre does. Each `user.message` event records `head`, the worktree's `HEAD` when the turn started. When a turn ends, byre runs `git add --all` and commits if anything changed (`git.CommitAll`), as the session's creator, with the first sentence of the turn's final message as the message (`commitMessage` in `internal/agents/manager.go`); `turn.ended` then carries the new sha as `commit`. A failed commit is logged and the turn still ends.
|
||||
|
||||
byre chooses the Claude session ID (`vendor_session_id`) up front, so every session can be resumed. Processes start on demand: the first message after a restart resumes the session with `--resume`. Sessions are `running` from the moment a turn is sent until its `result` arrives; on startup, every session still marked `running` was cut off mid-turn, and byre resumes it and sends a message telling the agent to re-check the working tree. Stopping the server kills all agent processes. Merging or discarding a task kills its sessions and marks them `completed`.
|
||||
|
||||
Every session gets byre's guidance (`guidance` in `internal/agents/guidance.go`): ask questions with `ask_human`, end each turn with a final message that summarizes it (including every departure from the plan), don't commit (byre commits each turn), and don't push, touch other branches or run git maintenance. It is passed as `--append-system-prompt`, which Claude Code records on a session's first request and reuses on resume, so a running session keeps the text it started with; a `UserPromptSubmit` hook also attaches it as `additionalContext` to every user turn, which reaches existing sessions too.
|
||||
|
||||
A Stop hook (`bounceProseQuestion` in `internal/agents/guidance.go`) bounces a turn whose final message asks the human something in prose: if `ask_human` wasn't called during the turn and the final message (`last_assistant_message`) has a sentence ending in `?` outside code spans, code blocks, quotes and blockquotes, the stop is blocked once with an instruction to ask through `ask_human`. The per-turn flags reset when the next user message is sent. Claude Code reports the block as a "Stop hook error" notification; that is expected. An agent told explicitly to ask in plain text will refuse the bounce.
|
||||
|
||||
A turn's final message is its summary. When a turn ends normally (not an error, not interrupted) with non-empty `result` text, byre files that text as a `report` inbox item. There is no report tool: models write a closing recap whatever they are told, so byre uses it rather than asking for a second one.
|
||||
|
||||
Running turns are capped globally and per vendor (`DefaultLimits` in `internal/agents/manager.go`). A turn that would exceed a cap leaves its session `queued`, with the message held in the `turn_queue` table; whenever a turn ends, a process exits, or a task is merged or discarded, the oldest queued turns whose vendor has room start. Interrupting a `queued` session cancels its queued turn. Idle sessions with a live process don't count against the caps. Whether a turn resumes the Claude conversation is decided by whether the session has ever sent one.
|
||||
|
||||
When Claude Code reports a rate-limit status of `rejected` and the turn then fails, the inbox gets a `rate_limited` item carrying the vendor's status (including when the limit resets) instead of a generic error. `GET /api/admin/status` reports running and queued turns and the latest rate-limit status each vendor sent.
|
||||
|
||||
Agent activity becomes events with `agent_session_id` set: `user.message`, `agent.message`, `agent.thinking`, `agent.tool_call`, `agent.tool_result`, `agent.report`, `turn.ended`, `session.state`, and `session.context`. Anything else Claude Code emits (rate-limit status, task progress, state changes) is recorded unchanged as `agent.vendor`. An `agent.report` event carries `inbox_item_id`, the inbox item filed for the same report. `session.context` records the context size whenever it changes: `used` is the last assistant message's input, cache-read, cache-creation and output tokens, and `window` is the model's context window from the turn's `result` (`modelUsage[model].contextWindow`), so it is 0 until the session's first turn after a server start ends. The task header shows the latest one as a percentage of the window, coloured green, amber from 70%, red from 90%.
|
||||
Agent activity becomes events with `agent_session_id` set: `user.message`, `agent.message`, `agent.thinking`, `agent.tool_call`, `agent.tool_result`, `turn.ended`, `session.state`, `session.model`, and `session.context`. Anything else Claude Code emits (rate-limit status, task progress, state changes) is recorded unchanged as `agent.vendor`. The one exception is partial output: each top-level `text_delta` is broadcast to SSE clients as an `agent.delta` event (`payload.text`, `seq` 0) that is never stored, and the transcript grows a live message from them until the finished `agent.message` replaces it. A `turn.ended` event carries `inbox_item_id` when its final message was filed as a `report` item. `session.context` records the context size whenever it changes: `used` is the last assistant message's input, cache-read, cache-creation and output tokens, and `window` is the model's context window from the turn's `result` (`modelUsage[model].contextWindow`), so it is 0 until the session's first turn after a server start ends. The task header shows the latest one as a percentage of the window, coloured green, amber from 70%, red from 90%.
|
||||
|
||||
Messages byre itself sends an agent (actor `byre`, such as the restart notice) show as a muted note, not a user bubble. The transcript view has an accent border while the session is `running`, and a braille spinner on its last line. The spinner is two GIFs in `web/img/`, one per colour scheme, rendered by `web/img/spinner.go` during `go generate ./web`; its colours are copied from the palette in `web/style.css`, so a palette change means updating them there too. Consecutive tool calls (with any thinking between them) collapse into one group whose summary counts calls per tool and file, and file paths show relative to the worktree. A report card that is at least half visible in the transcript while the page is visible dismisses its inbox item, as the viewing user. In the composer, Enter sends and Shift-Enter adds a newline; unsent text is kept per task in memory, so it survives switching tabs or tasks but not a page reload.
|
||||
Messages byre itself sends an agent (actor `byre`, such as the restart notice) show as a muted note, not a user bubble. The transcript view has an accent border while the session is `running`, and a braille spinner on its last line. The spinner is pure CSS: six dots laid out as a braille cell, each switched on and off by its own `step-end` keyframes (`braille-1` to `braille-6` in `web/style.css`) so together they cycle ⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏ every 0.8s. Browsers that freeze animated GIFs (Orion) still run it. Consecutive tool calls (with any thinking between them) collapse into one group whose summary counts calls per tool and file, and file paths show relative to the worktree. A turn's final message that is at least half visible in the transcript while the page is visible dismisses its `report` inbox item, as the viewing user. In the composer, Enter sends and Shift-Enter adds a newline; unsent text is kept per task in memory, so it survives switching tabs or tasks but not a page reload.
|
||||
|
||||
## Inbox
|
||||
|
||||
Agents ask questions with the `ask_human` MCP tool. Each question carries a `context` with the background the human needs to answer without reading the transcript; the UI renders it, the question, and the options as Markdown. A malformed call (no `context` or `why_blocked`, fewer than two options, a recommendation that isn't one of the options, an unknown attachment kind, a foreign `thread_id`) is rejected with a message saying what to fix, and nothing is filed. Each `report` call and each agent error (a turn that ends in error without a human interrupt, a process that dies mid-turn) also files an item.
|
||||
Agents ask questions with the `ask_human` MCP tool. Each question carries a `context` with the background the human needs to answer without reading the transcript; the UI renders it, the question, and the options as Markdown. A malformed call (no `context` or `why_blocked`, fewer than two options, a recommendation that isn't one of the options, an unknown attachment kind, a foreign `thread_id`) is rejected with a message saying what to fix, and nothing is filed. Each turn's final message (as a `report`) and each agent error (a turn that ends in error without a human interrupt, a process that dies mid-turn) also files an item.
|
||||
|
||||
`GET /api/inbox` is one query over open, unsnoozed items, oldest first; `project_id` and `task_id` narrow it to a project or task view, and `state` and `include_snoozed` widen it. Blocking questions and errors are `blocked`; reports and non-blocking questions are `fyi`. The inbox view groups items by task, each group headed by project and task: tasks with a blocked item come first, then tasks sort by project name and by their oldest item. Within a task, items are in time order.
|
||||
|
||||
@@ -76,6 +93,12 @@ In the UI a question offers its options plus Other, whose text box is the only t
|
||||
|
||||
Answers reach the agent as one message, listing each answer under the question or error it responds to, once the session is idle and none of its blocking questions are still waiting for the human. An `other` answer to a question tells the agent that if it asks something back, it should answer with `ask_human` using the item's `thread_id`. Every item change is an `inbox.created` or `inbox.updated` event carrying the whole item, including who resolved it.
|
||||
|
||||
## Changes
|
||||
|
||||
`GET /api/tasks/{taskId}/changes` snapshots the task's worktree as if everything were staged: it copies the worktree's index to a temporary file, runs `git add --all` against that copy (`GIT_INDEX_FILE`), and `git write-tree`s it, so committed, staged, unstaged and untracked work is included, `.gitignore` is respected, and the worktree's real index is never touched. Starting from the real index rather than `read-tree HEAD` keeps git's stat cache, so unchanged files aren't rehashed. The tree is compared with `git merge-base HEAD <default branch>`, or with `since` when given, using rename detection, giving per-file status and line counts. Work already merged into the default branch therefore drops out. The Changes view toggles between **Since main** and **This turn**, which passes the latest `user.message` event's `head` as `since`; the summary above the inbox always shows since main. `GET /api/tasks/{taskId}/diff` returns one file's patch between a `base` and `tree` from that response. Each snapshot writes the changed blobs and a tree into the repo's object store as loose objects.
|
||||
|
||||
The Changes view lists the files with their counts, opens a file's diff on click (open files stay open across refreshes), and refreshes when it opens, when the selected task changes, and 1.5s after the selected task's last tool result or turn end. The Inbox view shows just its summary line (file count, +/− totals, base) for the selected task at the top.
|
||||
|
||||
## Events
|
||||
|
||||
`GET /api/events` is server-sent events: `id` is the event's `seq`, `data` the Event JSON. Reconnecting with `Last-Event-ID` (EventSource does this itself) or `?after=` replays everything after that seq before going live. A client that falls further behind than its subscriber buffer (`subscribe` in `internal/server/hub.go`) is disconnected and catches up by reconnecting.
|
||||
`GET /api/events` is server-sent events: `id` is the event's `seq`, `data` the Event JSON. Reconnecting with `Last-Event-ID` (EventSource does this itself) or `?after=` replays everything after that seq before going live. Unstored events (`seq` 0, currently only `agent.delta`) go out live only, without an `id` line, so they don't move `Last-Event-ID`. A client that falls further behind than its subscriber buffer (`subscribe` in `internal/server/hub.go`) is disconnected and catches up by reconnecting.
|
||||
|
||||
+24
-22
@@ -83,10 +83,10 @@ This plan reflects decisions already agreed. Do not deviate from it without aski
|
||||
|
||||
Vendor specifics live behind adapters. The event model is a **shared core plus vendor-specific extensions**:
|
||||
|
||||
- Core event kinds: message, tool call, tool result, file change, question, report, turn end, error.
|
||||
- Core event kinds: message, tool call, tool result, file change, question, turn end, error.
|
||||
- Vendor-specific event types for features that don't map cleanly, rendered by vendor-specific UI components.
|
||||
|
||||
**Full interface requirement.** The SPA must offer the full capability of each vendor's native interface, not a reduced view with a drop-out escape hatch. This includes slash commands, plan mode, model switching, compaction, todo lists, subagent views, image input, `@`-file mentions, and interrupts. Tracking vendor feature releases is an ongoing cost.
|
||||
**Interface coverage.** The SPA covers the vendor capabilities this workflow actually uses, each added as a specific feature when it is needed, rather than mirroring the native interface wholesale: interrupts, model switching, compaction, and image input. Compaction is byre's own and vendor-agnostic, not the vendor's: the agent writes a handoff (the same contents as an `export_context` bundle) and the work continues in a new session that starts from it. Subagent views and Mermaid rendering come later, once agents use them. Not planned: generic slash-command passthrough (a command worth having gets its own UI) and plan mode (planning is a workflow stage).
|
||||
|
||||
**Claude.** Implement the Claude Code CLI's stream-json control protocol natively in Go, reverse-engineered from the open-source Agent SDKs (TypeScript and Python). These SDKs spawn the `claude` CLI and exchange stream-json over stdio with a bidirectional control channel. Required capabilities: start session, stream events, send messages, interrupt, resume by session ID, fork session, register hooks (at minimum Stop and PostToolUse), attach MCP servers.
|
||||
|
||||
@@ -94,14 +94,14 @@ Vendor specifics live behind adapters. The event model is a **shared core plus v
|
||||
|
||||
**Protocol stability.** Neither protocol is a guaranteed public contract.
|
||||
|
||||
- Disable CLI auto-updates and pin CLI versions in config.
|
||||
- Maintain a contract test suite built from recorded transcripts; run it before bumping a CLI version.
|
||||
- Disable CLI auto-updates, so the version changes only when the owner updates the CLI. The version is not pinned.
|
||||
- Maintain a contract test suite built from recorded transcripts; run it after updating the CLI.
|
||||
- Fallback if Claude protocol drift becomes painful: a thin Node sidecar wrapping the official SDK, speaking our protocol to Go. Do not adopt the fallback without asking.
|
||||
|
||||
### Session configuration
|
||||
|
||||
- Permission posture: fully permissive ("yolo"). Agents act freely inside their worktree; humans review afterwards.
|
||||
- Agents may commit inside their task branch. Instruct them not to attribute themselves in commits. For Claude, also disable co-author attribution via Claude Code's settings for the session, since attribution is a default behavior.
|
||||
- Agents don't commit. byre commits everything in the worktree to the task branch at the end of every turn that changed files, authored as the session's creator, with the first sentence of the turn's summary as the message. Turns are the unit the human reviews, and the merge cycle squashes the branch anyway, so these commits only mark turn boundaries for diffing and restore. For Claude, also disable co-author attribution via Claude Code's settings for the session, since attribution is a default behavior.
|
||||
- Instruct agents not to push, not to touch other branches, and not to run git maintenance commands (see "Known hazards").
|
||||
- Every session is launched with our MCP server attached.
|
||||
|
||||
@@ -120,8 +120,6 @@ The Go server hosts an MCP server attached to every agent session.
|
||||
- `blocking: true` means the agent should end its turn; `false` means it continues with unblocked work.
|
||||
- The human's answer arrives as a structured next turn.
|
||||
- `thread_id` continues an existing inbox thread.
|
||||
- `report(done[], next_steps[], risks[], deviations[])`
|
||||
- `deviations[]` lists every departure from the task's plan.
|
||||
- `request_review(scope, concerns[], profile?)`
|
||||
- `export_context(purpose)` → a handoff bundle (see "Handoff bundles").
|
||||
- `spawn_stages(stages[])` and `collect(stage_ids[])` for just-in-time fan-out and fan-in (see "Workflows").
|
||||
@@ -130,13 +128,9 @@ The Go server hosts an MCP server attached to every agent session.
|
||||
|
||||
Validation: reject malformed calls with a corrective message (for example, an `ask_human` with no options or no `why_blocked`).
|
||||
|
||||
**Enforcement when an agent ends a turn without reporting:**
|
||||
**Turn summaries.** There is no report tool. Each turn's final message is its summary: session guidance asks agents to end every turn with one covering what they did, every departure from the plan, risks, and what's next, and byre files it to the inbox as a report. A dedicated report tool was tried and dropped: models wrote the closing recap anyway, even when the tool's result, the system prompt and a per-turn reminder all said not to, so the human read everything twice.
|
||||
|
||||
1. Claude: a Stop hook blocks stopping until `report` has been called this turn, injecting an instruction to call it.
|
||||
2. Codex (or any vendor without a stop gate): on turn completion with no report, the server automatically sends a follow-up turn asking for one.
|
||||
3. After the nudge limit (`maxNudges` in `internal/report/gate.go`), the review agent synthesizes a report from the transcript and diff. The UI marks it as synthesized, not self-reported.
|
||||
|
||||
The same mechanism bounces questions asked in plain prose (a final message that asks something without an `ask_human` call). Expect to tune strictness; overly strict validation produces nudge loops, overly lenient produces filler reports.
|
||||
Questions asked in plain prose (a final message that asks something without an `ask_human` call) are bounced by a Stop hook that sends the agent back to ask properly, once per turn. Detection is a sentence ending in `?` outside code, quotes and blockquotes; expect to tune it, since overly strict detection produces nudge loops and overly lenient lets prose questions through.
|
||||
|
||||
---
|
||||
|
||||
@@ -150,7 +144,7 @@ Project (id, name, repo_path, default_branch, remote, setup_config,
|
||||
Workflow (id, name, version, definition) -- server-side, exportable
|
||||
Task (id, project_id, title, branch, worktree_path, base_ref,
|
||||
status, workflow_id, workflow_snapshot, created_by,
|
||||
created_at, completed_at)
|
||||
created_at, closed_at, merge_commit)
|
||||
TaskIssueLink (task_id, tracker, issue_ref)
|
||||
Stage (id, task_id, parent_stage_id, def_id, role, vendor, model,
|
||||
state, origin) -- origin: workflow | jit
|
||||
@@ -267,7 +261,7 @@ Rules:
|
||||
|
||||
- Versioned. Humans edit through the inbox; agents propose edits, which go to the inbox for approval.
|
||||
- Stored on the server and attached to merges via git notes. Not committed to the repo.
|
||||
- Build agents report departures in `report.deviations[]`.
|
||||
- Build agents state every departure from the plan in their turn summaries.
|
||||
- Eval stages report pass or fail per acceptance criterion.
|
||||
- The plan-conformance review profile compares the diff against the plan.
|
||||
|
||||
@@ -343,15 +337,15 @@ A task can go through many merge cycles before completion. Merges happen only at
|
||||
- Author: the human who confirmed the merge.
|
||||
- Co-author trailers: other humans who participated in the task since the last merge cycle (resolved inbox items, messaged agents, triggered forks).
|
||||
- No agent attribution.
|
||||
- Message: drafted by an agent from the task's `report` calls since the last merge; the human edits before confirming.
|
||||
- Message: drafted by an agent from the task's turn summaries since the last merge; the human edits before confirming.
|
||||
4. Attach a git note to the squash commit (see "History").
|
||||
5. Push the default branch and the notes ref. If rejected because the remote moved, fetch and repeat from step 2.
|
||||
6. Reset the task branch to the new default-branch head. Required because squash commits make agent commits non-ancestors of the default branch; without the reset, later cycles accumulate spurious conflicts. Pre-squash history remains available via snapshot refs.
|
||||
6. Reset the task branch to the new default-branch head. Required because squash commits make agent commits non-ancestors of the default branch; without the reset, later cycles accumulate spurious conflicts. Pre-squash history (the per-turn commits) remains available via snapshot refs.
|
||||
7. Tell the agent explicitly that its branch history was squashed and reset, so it doesn't try to repair history. Then resume it. Files on disk are unchanged by the reset.
|
||||
|
||||
### Task completion
|
||||
|
||||
An explicit human action. Removes the worktree, the task branch, and the task's snapshot refs. Transcripts, artifacts, reviews, and inbox history remain archived in SQLite.
|
||||
An explicit human action, as one of two buttons: **Merge** squashes the task onto the default branch and then removes it; **Discard** removes it without merging, after a confirm that states the commits and changed files that will be lost. Either way the worktree, the task branch, and the task's snapshot refs are removed. Transcripts, artifacts, reviews, and inbox history remain archived in SQLite. Nothing deletes a task branch without one of these two buttons.
|
||||
|
||||
---
|
||||
|
||||
@@ -456,6 +450,7 @@ Issues identified in review that implementation must account for:
|
||||
- **Visual review reliability.** Capturing meaningful screenshots requires building, seeding data, and navigating to the affected screen. Expect flakiness; noisy reviews train users to ignore them.
|
||||
- **Squash-and-reset under a running agent.** Agents may notice their commits disappearing and try to repair history; step 7 of the merge cycle addresses this and needs testing per vendor.
|
||||
- **Event log growth.** Stream-json is verbose and tool outputs are large. SSE resume and scrubbing need efficient queries; consider separating large payloads into blobs.
|
||||
- **Migration numbering across branches.** Migrations are tracked by position (`PRAGMA user_version`). Each task branch appends its own, and byre is deployed from task branches, so two branches can claim the same number: the database records it as applied and the other branch's migration silently never runs. Fix: track applied migrations by name in a table. Before switching, make every existing data migration safe to run twice, since one recorded only by number would run again under its name.
|
||||
|
||||
---
|
||||
|
||||
@@ -479,12 +474,18 @@ Record transcripts from the spike as the seed of the contract test suite.
|
||||
|
||||
- **M0 — Skeleton.** launchd service, tailscale serve auth, SQLite schema, OpenAPI spec, projects, tasks, worktree create/delete, event log, SSE stream with resume, embedded SPA shell, basic server observability.
|
||||
- **Status: done**, and running as a service on the Mac Studio. Observability is exposed through `GET /api/admin/status` only; the SPA has no admin view yet.
|
||||
- **M1 — One agent end to end (Claude).** Sessions, streaming, messaging, full-interface rendering for Claude, MCP tools (`ask_human`, `report`), report enforcement, inbox with all views and response types, web push notifications, restart-and-resume, concurrency caps.
|
||||
- **Done:** sessions, streaming, messaging, interrupt; `ask_human` and `report` with validation; Stop-hook report enforcement; inbox with global, project, and task views and every response type, answered as one batch and delivered to the agent as one message; restart-and-resume; global and per-vendor caps with queueing; rate limits as their own inbox state; the web UI (two panes, task switcher, transcript, inbox) with Markdown rendering.
|
||||
- **Not done:** full-interface rendering beyond messages, tool calls, and interrupt (slash commands, plan mode, model switching, compaction, todo lists, subagent views, image input, `@`-file mentions, Mermaid rendering); web push notifications; bouncing questions asked in plain prose; the synthesized-report fallback (needs the M3 review agent); the session instructions from "Session configuration" (no pushing, no other branches, no git maintenance, no self-attribution; only the attribution setting is applied); pinning the CLI version (auto-update is disabled, the version isn't pinned).
|
||||
- **M1 — One agent end to end (Claude).** Sessions, streaming, messaging, the interface coverage for Claude, the `ask_human` MCP tool, turn summaries, inbox with all views and response types, web push notifications, restart-and-resume, concurrency caps.
|
||||
- **Done:** sessions, streaming, messaging, interrupt; `ask_human` with validation; each turn's final message filed to the inbox as its summary; inbox with global, project, and task views and every response type, answered as one batch and delivered to the agent as one message; restart-and-resume; global and per-vendor caps with queueing; rate limits as their own inbox state; the web UI (two panes, task switcher, transcript, inbox) with Markdown rendering; the session instructions from "Session configuration", given as an appended system prompt and a per-turn reminder; per-turn commits by byre; bouncing questions asked in plain prose.
|
||||
- **Not done:** image input (see "Interface coverage"); web push notifications. Compaction is done, as a handoff into a fresh session; model switching is done, per message.
|
||||
- **Later:** subagent views and Mermaid rendering.
|
||||
- **M2 — Seeing the work.** Temp-index diffs and stats, snapshots, timeline scrubbing, terminals, file browser, previews.
|
||||
- **M3 — Second vendor and review.** Codex adapter with full-interface rendering, review profiles, review triggers, visual review output, cross-vendor review.
|
||||
- **Done:** temp-index diffs and stats, with a Changes view.
|
||||
- **Not done:** snapshots, timeline scrubbing, terminals, file browser, previews.
|
||||
- **M3 — Second vendor and review.** Codex adapter with the same interface coverage, review profiles, review triggers, visual review output, cross-vendor review.
|
||||
- **M4 — Merging and history.** Full merge cycle with squash, authorship and co-author trailers, drafted commit messages, merge-resolution stage, git notes, task completion and archive, archive search, GitHub Issues integration.
|
||||
- **Done:** Merge and Discard replace "Complete task". Merge squashes the worktree, uncommitted files included, into one commit on the default branch, authored by the merging user, and fast-forwards the project checkout; it doesn't push, and it refuses on conflicts or while a turn is running.
|
||||
- **Next:** the squash commit message, which is the task title for now. Decided: a one-shot `claude -p` run in the task's worktree writes a one-line summary from the branch's log and diff stat, so Merge becomes a background job that reports back with a `task.merged` event or an error.
|
||||
- **Not done:** co-author trailers, merge-resolution stage, git notes, push, repeated merge cycles, archive search, GitHub Issues.
|
||||
- **M5 — Workflows.** Workflow definitions and storage, stages, gates, bounded loops, plan artifact, eval stages, task intake, mid-task swapping.
|
||||
- **M6 — Fan-out.** `export_context` and handoff bundles, same-vendor forks, cross-vendor handoffs, fork-and-compare, just-in-time fan-out with limits.
|
||||
- **M7 — Inbox triage.**
|
||||
@@ -501,3 +502,4 @@ Record transcripts from the spike as the seed of the contract test suite.
|
||||
- Periodic review thresholds (defaults for time and diff size).
|
||||
- Default limits for just-in-time fan-out (depth, concurrency, budget).
|
||||
- Task navigation: keep both the ⌘K switcher and the Tasks view, or only one.
|
||||
- Interface coverage still undecided: todo-list rendering and `@`-file mentions.
|
||||
|
||||
Reference in New Issue
Block a user