Files
byre/docs/plan.md
T

26 KiB

Agent Manager — Plan

Working name: agentmgr (placeholder; final name TBD).

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.

Goals

  • 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, which provides HTTPS with a valid *.ts.net certificate.
  • 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.
  • Config maps each Tailscale login to a git name and email, used for commit authorship and co-author trailers.

API

  • REST, OpenAPI spec-first. Generate the TypeScript client for the SPA from the spec; a Swift client can be generated later for the macOS app.
  • Real-time state: one multiplexed SSE stream per client. Every event carries a monotonic sequence number; clients resume with Last-Event-ID.
  • Terminals are the only WebSocket use: one WebSocket per open terminal.

Notifications

  • Web push via a service worker (possible because of the real ts.net certificate).
  • Notify on blocked inbox items first; other kinds are opt-in.
  • Per-user preferences; collapse bursts into one notification.

Server observability

  • Agent process liveness; detection of stuck and zombie processes.
  • Adapter and protocol-parse errors.
  • Snapshot counts and disk usage per task.
  • Concurrency queue state and vendor rate-limit status.
  • Exposed in an admin view in the SPA.

Concurrency and usage

  • Global and per-vendor caps on concurrently running agent sessions; excess work queues.
  • Usage visibility per task, workflow stage, and vendor.
  • Vendor rate-limit errors surface as their own inbox state rather than as generic failures.

Agent integration

Adapters and the event model

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.
  • 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.

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.

  • 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.
  • 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.
  • 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.

  • ask_human(question, why_blocked, options[], recommended_option, attachments[], blocking, thread_id?)
    • Non-blocking at the protocol level: returns immediately with "question filed."
    • 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[], open_questions[], 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").

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).

Enforcement when an agent ends a turn without reporting:

  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 N failed nudges, 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.


Data model

User            (tailscale_login, display_name, git_name, git_email,
                 notification_prefs)
Project         (id, name, repo_path, default_branch, remote, setup_config,
                 issue_tracker_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)
TaskIssueLink   (task_id, tracker, issue_ref)
Stage           (id, task_id, parent_stage_id, def_id, role, vendor, model,
                 state, origin)                          -- origin: workflow | jit
AgentSession    (id, task_id, stage_id, vendor, model, vendor_session_id,
                 parent_session_id, forked_from_snapshot_id, state, role)
Artifact        (id, task_id, stage_id, kind, version, payload, created_by)
                                                         -- plan, eval-report, handoff, ...
Event           (seq, agent_session_id, task_id, project_id, kind,
                 payload, created_at)                    -- append-only
Snapshot        (id, task_id, agent_session_id, n, commit_sha, tree_sha,
                 event_seq, trigger, created_at)
InboxItem       (id, project_id, task_id, agent_session_id, thread_id, kind,
                 priority, blocking, payload, snapshot_id, review_id, state,
                 snoozed_until)
InboxResponse   (id, inbox_item_id, user_id, type, text, attachments,
                 created_at)          -- type: option | free_form | redirect | question
Review          (id, task_id, snapshot_id, tree_sha, profile, reviewer_vendor,
                 trigger, state, summary, highlights, attachments, stale)
ReviewProfile   (id, name, prompt, vendor, model, output_schema, capture_config)
Merge           (id, task_id, cycle, commit_sha, merged_by, co_authors,
                 note_ref, pushed_at)
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:

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.
  • Build agents report departures in report.deviations[].
  • Eval stages report pass or fail per acceptance criterion.
  • The plan-conformance review profile compares the diff against the plan.

Task intake

  1. Pick a workflow.
  2. Fill its inputs: a brief, attachments, linked GitHub issues, handoff bundles from other tasks.
  3. Optionally override vendors, models, prompts, or review profiles per stage.

Handoff bundles

Produced by export_context(purpose):

  • goal
  • decisions made and why
  • current state
  • dead ends tried
  • open questions
  • relevant files

Used for:

  • cross-vendor forks (the forked agent receives exactly what it needs),
  • stage-to-stage handoff in workflows,
  • the durable record of a task.

Git mechanics

Worktrees

  • One worktree and one branch per task, created from the project's default branch.
  • Per-project setup config: setup script (dependency install, env file copying) and port allocation so parallel dev servers don't collide.
  • The repo's original checkout is used only by the server, as the integration checkout for merges.

Diffs and stats (never touching the real index)

Compute "as if everything were staged" without forcing staging:

GIT_INDEX_FILE=$tmp git read-tree HEAD
GIT_INDEX_FILE=$tmp git add -A        # respects .gitignore, includes untracked
GIT_INDEX_FILE=$tmp git write-tree    # → tree sha

Diff and stats are computed between the resulting tree and the merge-base with the default branch.

Snapshots

  • Built with the same temp-index technique, then git commit-tree on the resulting tree.
  • Stored under hidden refs: refs/agentmgr/tasks/<task-id>/snap/<n>. Git deduplicates blobs, so frequent snapshots are cheap.
  • 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).
    • No agent attribution.
    • Message: drafted by an agent from the task's report calls 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.
  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.


History

  • On each squash merge, attach a git note under refs/agentmgr/notes (or refs/notes/agentmgr) 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.
  • Priority: blocked (an agent is idle waiting on this) > gate approval > review ready > FYI/report; then by age.
  • Everyone sees every item. Resolutions record who resolved them and are broadcast to all clients over SSE.

Response types

  • Pick an option, with an optional note.
  • Free-form answer when none of the options fit.
  • Redirect: new instructions that change the agent's direction. The item resolves as "redirected," and the agent receives the instructions as its next turn.
  • Reply with a question: the item becomes a thread; the agent answers through a follow-up ask_human with the same thread_id, and the item stays open.
  • Snooze: hidden until a set time; the agent stays blocked.

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.

File browser

Read-only, scoped to project and worktree roots.

UI

  • Split-screen layout.
  • Fast project and task switching (command palette).
  • SPA framework not yet chosen.

Known hazards

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.

Build sequence

Spike (first)

A Go client for the Claude Code CLI's stream-json control protocol that can:

  • Start a session, stream events, send a message.
  • Interrupt and resume by session ID.
  • Register a Stop hook and a PostToolUse hook.
  • Launch with co-author attribution disabled and GIT_AUTHOR_* environment variables set.
  • Attach an MCP server.

Record transcripts from the spike as the seed of the contract test suite.

Milestones

  • 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.
  • 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.
  • M2 — Seeing the work. Temp-index diffs and stats, snapshots, timeline scrubbing, terminals, file browser, split screen, previews.
  • M3 — Second vendor and review. Codex adapter with full-interface rendering, 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.
  • 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.

Open items

  • Raised in critique, not decided: build the MCP server and inbox first against manually launched agents, to validate the structured-question loop before investing in orchestration.
  • Verify Codex app-server support for forking threads.
  • Confirm the exact Claude Code setting for disabling co-author attribution in the CLI version we pin.
  • Confirm tailscale serve behavior for HTTP/2 and for exposing per-task preview ports.
  • Terminal persistence across server restarts: accept loss, or back terminals with tmux?
  • Format of per-project setup config (setup script, port allocation, run/capture config for reviews).
  • Simulator allocation strategy for parallel tasks and review agents.
  • Periodic review thresholds (defaults for time and diff size).
  • Report-enforcement nudge limit N.
  • Default limits for just-in-time fan-out (depth, concurrency, budget).
  • SPA framework.
  • Final app name.