A self-hosted Go server on a home Mac Studio that manages third-party coding agents (Claude Code, Codex) across projects and tasks, exposed to a SPA over Tailscale. Built for its author first; multi-user and multi-device from the start.
## Working agreement
This plan reflects decisions already agreed. Do not deviate from it without asking first. If an approach turns out not to work, a decision needs revisiting, or something requires reverting work or a destructive action, stop and ask rather than choosing an alternative. Items under "Open items" are not yet decided; raise them when they become blocking.
`AGENTS.md` holds the standing rules for working on byre (deploying before handoff, UI layout and look, web code). `docs/operations.md` describes how the built system works and runs. "Build sequence" below says what is built.
- Manage many concurrent agent sessions across projects (repos) and tasks (worktrees).
- Spend human attention efficiently: an inbox of structured questions and reports, reviews that explain changes visually, and diffs as the least-encouraged way to understand work.
- Support customizable workflows (for example plan → build → eval) with per-stage vendors, models, and review types.
- Snapshot everything so work can be scrubbed through, forked, and compared.
- Keep the history of why work was done attached to the merges it produced.
- API-first so a future native macOS app is a second client, not a rewrite.
## Non-goals (for now)
- Mobile UI.
- Pull requests. Merges are local, then pushed.
- Our own agent. We orchestrate third-party agents only.
- Public internet exposure (no Tailscale Funnel, no cloud relay).
- Generic agent-to-agent wiring. Agent collaboration happens through workflow stages and the specific patterns below.
- General DAG workflow engine.
## Future features
- Humans editing code directly in task worktrees (open-in-editor, rules for hand edits while agents run).
- Mobile UI.
- Claim/assign ownership of inbox items.
---
## Architecture
### Server
- Go, single binary, run under launchd on the Mac Studio.
- Binds to `127.0.0.1` only. Fronted by `tailscale serve` on its own HTTPS port of the machine's `*.ts.net` name (not a path prefix, which breaks the SPA's absolute paths), with a valid certificate. Clients get HTTP/2 and SSE passes through unbuffered.
- SPA served from `embed.FS` in release builds and `os.DirFS` behind a dev flag. Same handler either way.
- Storage: SQLite via `modernc.org/sqlite` (pure Go, no CGO), WAL mode.
### Auth and identity
- Identity comes from Tailscale's injected headers (`Tailscale-User-Login`, `Tailscale-User-Name`). Trust them only on requests arriving from loopback via the serve proxy.
- Multi-user is a real requirement. Every mutating action records the acting user.
- Users are created on first request. Each sets the git name and email used for commit authorship and co-author trailers through the API (`PUT /api/me`); agents can't start until it is set.
- REST, OpenAPI spec-first (`api/openapi.yaml`). Go server types and routing are generated with `oapi-codegen`; the SPA's TypeScript types are generated with `openapi-typescript` and used through a small typed client. A Swift client can be generated later for the macOS app.
- Global and per-vendor caps on concurrently running agent turns (idle sessions don't count); excess turns queue.
- Usage (cost, duration, turns) is recorded per turn in the event log and available through the API per task, workflow stage, and vendor. The UI never shows it.
**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.
**Codex.** Implement a JSON-RPC client for `codex app-server`. Generate Go types from the protocol schema the CLI can emit. Required capabilities: start thread, stream events, send turns, interrupt, resume. Fork support needs verification.
**Protocol stability.** Neither protocol is a guaranteed public contract.
- 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.
- 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.
### Lifecycle
- Agent processes are children of the server. On server restart they are killed.
- On restart, sessions that were active are resumed by vendor session ID. The server then injects a message: the agent was interrupted mid-turn and must re-check the working tree before continuing.
### MCP tools
The Go server hosts an MCP server attached to every agent session.
-`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").
`attachments` share one schema across tools, inbox replies, and reviews: file and line-range references, diff references, images, diagrams (Mermaid source), links to tasks or snapshots, and URLs.
Validation: reject malformed calls with a corrective message (for example, an `ask_human` with no options or no `why_blocked`).
**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.
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.
Blob (id, content_type, path) -- screenshots, recordings, etc.
Terminal (id, task_id, cwd, created_by, state)
Preview (id, task_id, port, url, process_state)
Participation (task_id, user_id, kind, since_merge_cycle) -- for co-author trailers
```
Notes:
-`AgentSession.role` distinguishes implementers from planners, reviewers, evaluators, merge resolvers, judges, and triage agents.
-`Event` is the source of truth for the SSE stream and for scrubbing; `seq` is global and monotonic.
-`Task.workflow_snapshot` freezes the workflow definition at intake, so later edits to a workflow don't change running tasks.
- Browser connections are ephemeral and are not part of the persistent model.
---
## Workflows
### Model
A workflow is a sequence of **stages**. Each stage has:
- a role prompt template,
- a vendor and model,
- typed artifact inputs and outputs,
- an optional gate: human approval (via the inbox), a review pass, or an eval pass,
- optional review profiles to run during the stage.
Engine scope: linear stages, gates, bounded loops (`on_fail: goto … max … then escalate`), and fan-out with fan-in. No general DAGs until a real workflow needs one.
Example:
```yaml
name:plan-build-eval
stages:
- id:plan
agent:{vendor:claude, model:opus }
prompt:templates/plan.md
outputs:[plan]
gate:human # approve / edit / redirect via inbox
- id:build
agent:{vendor:codex }
inputs:[plan]
reviews:[plan-conformance, security]
- id:eval
agent:{vendor:claude }
inputs:[plan, diff]
outputs:[eval-report]
on_fail:{goto:build, max:3, then:escalate }
```
### Storage
- Workflow definitions, prompt templates, and review profiles live on the server, not in repos.
- They are portable: export and import as files.
### Everything is swappable
Templates and roles are starting points. At intake and at any point mid-task, a human can swap a stage's vendor, model, prompt, or review profiles, and edit the task's frozen workflow (add, remove, or reorder stages).
### Just-in-time fan-out
Agents can construct workflows on the fly via `spawn_stages` and `collect`:
- Spawned stages use the same stage definition format.
- Limits: maximum depth, maximum concurrent spawned stages, and per-task usage budget, all configurable.
- An optional human gate on spawning, configured per workflow.
- Spawned stages are visible in the task's UI tree with `origin: jit`.
- A just-in-time structure can be saved as a reusable workflow template.
### Stage patterns
The earlier "agent features" are expressed as stage patterns:
1.**Cross-vendor review:** a review profile whose vendor differs from the implementer's.
2.**Merge resolution:** a stage triggered when merging the default branch into a task branch conflicts; its result goes to review.
3.**Fork-and-compare:** a fan-out stage (N forks with different vendors or prompts from one snapshot) followed by a judge stage that produces a comparison with visuals.
4.**Inbox triage:** an agent answers questions resolvable from the repo or docs and escalates the rest with its reasoning attached. Tune on real inbox data.
Build them in that order.
### Plan artifact
Standard schema:
- goal
- approach
- steps
- acceptance criteria
- risks
- open questions
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.
- A snapshot pairs the code commit with the conversation position (`event_seq`).
- Triggers: after every tool call (PostToolUse hook where available; shell commands can change files, so file-editing tools alone are not enough), at turn end, on `ask_human`, and on a timer as a backstop. Skip if the tree sha is unchanged.
- Retention: keep everything until task completion.
### Scrubbing and forking
- Timeline UI over a task's snapshots, showing the diff at each point alongside the transcript up to that event.
- Fork: a new worktree from a snapshot commit plus a new AgentSession.
- Same vendor: native session fork/resume.
- Different vendor: a handoff bundle from `export_context`. The UI labels it as a handoff, not a true fork.
### Merge cycle
A task can go through many merge cycles before completion. Merges happen only at turn boundaries; partially done work is not merged. Each cycle:
1. Wait for the task's implementer to reach a turn boundary, then pause it.
2. In the task worktree, merge the default branch into the task branch. The merge-resolution stage resolves conflicts; review runs on the result.
3. Human confirms. In the integration checkout, run `git merge --squash <task-branch>` and commit:
- 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).
- Message: the task's merge title, which starts as the task title and which the agent updates with `set_merge_title` as the work evolves; merging the default branch in asks it to check the title.
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.
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.
- On each squash merge, attach a git note under `refs/notes/byre` containing: the plan, the handoff bundle, review summaries, eval results, and resolved inbox decisions. Full transcripts are referenced by task ID.
- Notes don't push or fetch by default; the merge cycle pushes the notes ref explicitly, and setup configures fetching it.
- Archived tasks are searchable across transcripts, artifacts, and decisions.
### Issues
- Issue tracker integration is pluggable; GitHub Issues first.
- Tasks link to issues at intake.
- On merge, post or update a summary comment on linked issues.
---
## Reviews
- Reviews are performed by a separate agent session, never the implementer grading itself. By default the reviewer is a different vendor from the implementer.
- Every review targets a specific snapshot and is cached by tree sha. When the task's tree moves on, existing reviews are marked stale.
- Review types are named **review profiles** (security, performance, privacy, visual UX, plan conformance, and so on), each with a prompt, vendor, model, output schema, and capture config. Stages reference profiles by name.
- Triggers:
- On `ask_human`: the inbox item appears immediately; the review attaches when it completes. If the item is resolved first, the review still attaches to its history.
- Before merge.
- On demand.
- Periodic: on whichever comes first of elapsed time or diff size, with per-project overrides. Debounced to turn boundaries or a quiet period so half-applied edits are not reviewed.
### Review output
Structured, leaning visual rather than quoting code:
- Summary.
- Highlights, each with a category, severity, and anchors to files or lines so the UI can jump to them.
- Visual attachments:
- Mermaid diagrams for changed data flow, call graphs, state machines, and schemas.
- Screenshots or recordings of the running result. Per-project config declares how to run and capture: Playwright MCP for web projects; `xcrun simctl io` screenshot/recordVideo for iOS simulator projects.
- Before/after pairs from the base ref and the current snapshot where possible.
### Change view
In increasing order of how much the UI encourages their use:
1. File diffs
2. Diff stats
3. Agent-generated summary
4. Agent highlights with visual explanations
---
## Inbox
### Views and priority
- One underlying query with global, per-project, and per-task views.
- Sources: `ask_human` calls, reports, completed reviews, gate approvals, rate-limit states, sessions that went idle or errored.
- **Other**: the only text field, for an answer outside the options, a question back, or new instructions. If it asks the agent something, the agent answers through a follow-up `ask_human` with the same `thread_id`.
Responses can carry attachments (pasted images and sketches, file and line references, links to tasks or snapshots, URLs). When an agent asks several questions at once, they are answered as one form with one submit.
---
## Other features
### Previews
- Web: the server supervises a per-task dev server and exposes it over Tailscale, preferably as per-task ports through `tailscale serve` (path-prefix proxying breaks apps that use absolute paths).
- iOS: on-demand simulator screenshots and recordings attached to the task; Screen Sharing over Tailscale as the manual fallback.
### Terminals
PTYs via `github.com/creack/pty`, rendered with xterm.js, one WebSocket each. Scoped to a task's worktree by default.
- Two panes, always side by side at exactly half the width each, under one top navbar. Each pane has tabs and can show any view; new views (diff, files, terminal, preview) become tabs, never columns. `AGENTS.md` holds the binding layout and look rules.
- Fast task switching: a ⌘K switcher in the navbar, plus a Tasks view.
- Frameworkless: TypeScript compiled file-by-file to ES modules, no bundler.
- Atkinson Hyperlegible Mono throughout, self-hosted; high-contrast text on a quiet palette.
Issues identified in review that implementation must account for:
- **Shared `.git` across worktrees.** Permissive agents can delete snapshot refs, run `git gc` or `git worktree prune`, check out other branches, or push. Mitigate with instructions, a pre-push hook on the shared repo that blocks pushes not made by the server, and periodic verification that snapshot refs survive.
- **Snapshot cost.** `git add -A` on every tool call is slow in large repos, and non-ignored build output bloats the object store. Nothing is garbage-collectable until task completion.
- **Shared machine resources.** Parallel Xcode builds compete for CPU; per-worktree DerivedData costs disk and cold builds; iOS simulators are shared between tasks and review agents and need allocation or locking.
- **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.
**Status: done.**`internal/claude` is the client; `docs/claude-protocol.md` records the protocol; `internal/claude/session_test.go` exercises each capability against the real CLI and records transcripts into `internal/claude/testdata/transcripts/`, with the account email scrubbed. Fork is wired in but untested.
- **Status: done**, and running as a service on the Mac Studio. Observability covers queue state, vendor rate-limit status, agent process liveness, stuck and zombie detection, and adapter and protocol-parse errors, through `GET /api/admin/status` and the SPA's Admin view; per-task snapshot counts and disk usage are not done.
- **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; web push notifications through a service worker, with per-user preferences (blocking questions by default, other kinds opt-in) set in a Settings tab, bursts collapsed into one notification, and a click opening the task. Compaction is done, as a handoff into a fresh session; model switching is done, per message; image input is done, pasted or dropped into the composer and stored as blobs.
- **M3 — Second vendor and review.** Codex adapter with the same interface coverage, review profiles, review triggers, visual review output, cross-vendor review.
- **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, pushes it to the project's `origin`, and then fast-forwards the project checkout; it refuses while a turn is running. Merge first has the agent merge the default branch in (also available alone as "Merge main in"), then squashes when that turn ends. Merge fetches the remote's default branch into the checkout before the merge-in; a refused or failed push is filed as an error and the task stays active for another Merge. The squash commit's title is the task's merge title: the task title until the agent sets another with the `set_merge_title` MCP tool, which the merge-in prompt asks it to do when the current title no longer describes the branch.
- **Not done:** co-author trailers, human editing of the drafted message before confirming, merge-resolution stage, git notes (and pushing the notes ref), repeated merge cycles, archive search, GitHub Issues.