v2.0 proposal: BooCoder — write tools, pending changes, ACP dispatch, MCP server

Comprehensive roadmap for the v2.0 major version bump. Covers:
- Schema: pending_changes, tasks, available_agents tables + human_inbox view
- Path A: native write tools (edit_file, create_file, delete_file) queuing
  through pending_changes before /apply flushes to disk
- Path B: external agent dispatch via ACP (opencode, goose) or PTY fallback
  (claude, pi) with per-task git worktrees and automatic diff-on-completion
- BooCoder MCP server: 6 tools exposing task primitives over stdio
- Code lifts: agent-hub (Apache-2.0, task DAG), plandex (MIT, diff UX),
  ACP SDK (Apache-2.0, subprocess protocol), Paseo (AGPL, design-only)
- Sub-versions: v2.0.0 (Path A), v2.0.1 (Path B), v2.0.2 (MCP server),
  v2.0.3 (CLI + polish)
- Estimate: ~2200 LoC total

All v1.x dependencies shipped (v1.13 parts, v1.14 outer loop, v1.15 MCP
client, v1.16 codesight). v2.0 is unblocked.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-24 15:11:16 +00:00
parent 29c7d051b6
commit f2974d6887
2 changed files with 404 additions and 0 deletions

View File

@@ -0,0 +1,274 @@
# v2.0 — BooCoder
Major version bump. New app `apps/coder/` inside the existing monorepo. Lands together with the `boocode_db``boochat_db` DB rename and the per-app subdomain split (`code.indifferentketchup.com` → BooChat, `coder.indifferentketchup.com` → BooCoder).
## What BooCoder is
A write-capable coding agent surface. Two execution paths, same UI:
- **Path A (native):** BooCode's own inference loop with write tools (`edit_file`, `create_file`, `delete_file`). Edits queue in `pending_changes` — nothing touches disk until user approves via `/apply`.
- **Path B (dispatch):** Shells out to external CLI agents (`opencode`, `goose`, `claude`, `pi`) via ACP (preferred) or raw PTY (fallback). One git worktree per dispatch. Captures events into the same parts taxonomy.
Both paths feed the same task DAG, same project registry, same pending-changes queue, same UI.
## Why now
v1.x proved the read-only loop works end-to-end: inference, tool dispatch, streaming, compaction, MCP client, outer loop, step caps, artifact rendering. The infrastructure is stable. The jump from "read-only chat" to "write-capable agent orchestrator" is the remaining gap between BooCode and having a real development environment.
## Architecture
### Three protocol roles (locked 2026-05-22)
1. **MCP client (write-capable allowed).** Inherits v1.15 client. Write-capable MCP servers (e.g. `@modelcontextprotocol/server-filesystem`) route writes through `pending_changes`. Per-task allow/deny means dispatched tasks can have a different MCP roster.
2. **MCP server (BooCoder's own primitives).** Exposes `boocoder.create_task`, `boocoder.list_pending_changes`, `boocoder.apply`, `boocoder.reject`, `boocoder.dispatch_external_agent`, `boocoder.list_worktrees` as MCP tools. Stdio transport for local consumers (Sam's `opencode` in Termius); HTTP deferred until OAuth + secret storage.
3. **ACP client (host).** Spawns `opencode acp` and `goose acp` as JSON-RPC stdio subprocesses. Maps ACP events (file operations, tool calls, terminal output) to BooCode's parts taxonomy. MCP servers configured in BooCoder are auto-forwarded to the dispatched agent (per goose docs — `context_servers` is the field).
### Container layout (post-v2.0)
| Container | Port | Mount | Purpose |
|---|---|---|---|
| `boochat` (was `boocode`) | `100.114.205.53:9500` | `/opt:/opt:ro` | Read-only chat + MCP client |
| `booterm` | `100.114.205.53:9501` | `/opt:/opt:rw` | PTY/tmux terminal |
| `boocoder` | `100.114.205.53:9502` | `/opt:/opt:rw` (policy-gated) | Write tools + ACP host + MCP client + MCP server |
| `boochat_db` (was `boocode_db`) | `127.0.0.1:5500` | `boocode_pgdata` | Shared Postgres 16 |
| `codecontext` | internal `:8080` | `/opt:/opt:ro` | Analysis sidecar (shared) |
### Caddy routing
```
code.indifferentketchup.com → boochat:9500
coder.indifferentketchup.com → boocoder:9502
term.indifferentketchup.com → booterm:9501 (or routed under code.*/term/)
```
## Schema (new tables)
```sql
-- Pending changes: queued writes before /apply
CREATE TABLE IF NOT EXISTS pending_changes (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
session_id UUID NOT NULL REFERENCES sessions(id),
task_id UUID REFERENCES tasks(id),
file_path TEXT NOT NULL,
operation TEXT NOT NULL CHECK (operation IN ('create', 'edit', 'delete')),
diff TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'applied', 'rejected', 'reverted')),
created_at TIMESTAMPTZ NOT NULL DEFAULT clock_timestamp()
);
-- Tasks: the dispatch DAG
CREATE TABLE IF NOT EXISTS tasks (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
project_id UUID NOT NULL REFERENCES projects(id),
parent_task_id UUID REFERENCES tasks(id),
state TEXT NOT NULL DEFAULT 'pending'
CHECK (state IN ('pending', 'running', 'completed', 'failed', 'blocked', 'cancelled')),
input TEXT NOT NULL,
output_summary TEXT,
agent TEXT,
model TEXT,
execution_path TEXT CHECK (execution_path IN ('native', 'acp', 'pty')),
worktree_path TEXT,
cost_tokens INTEGER,
started_at TIMESTAMPTZ,
ended_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT clock_timestamp()
);
-- Available agents: probed at startup
CREATE TABLE IF NOT EXISTS available_agents (
name TEXT PRIMARY KEY,
install_path TEXT,
version TEXT,
supports_acp BOOLEAN NOT NULL DEFAULT false,
supports_mcp_client BOOLEAN NOT NULL DEFAULT false,
last_probed_at TIMESTAMPTZ
);
-- Human inbox: tasks needing attention
CREATE VIEW human_inbox AS
SELECT * FROM tasks WHERE state IN ('blocked', 'failed');
```
`task_templates` and `pipelines` deferred to v2.1 — overhead for single-user. The core is `tasks` + `pending_changes` + `available_agents`.
## Path A — Native write tools
### Tools
| Tool | Description |
|---|---|
| `edit_file` | Apply a diff to an existing file. Input: `{file_path, old_string, new_string}`. Queues in `pending_changes` with `operation='edit'`. |
| `create_file` | Create a new file. Input: `{file_path, content}`. Queues as `operation='create'`. |
| `delete_file` | Delete a file. Input: `{file_path}`. Queues as `operation='delete'`. |
| `apply_pending` | Flush all pending changes for the current session to disk. Path-guarded. |
| `rewind` | Revert a specific applied change or all changes since a checkpoint. |
### Path guard for writes
Same `pathGuard()` function from BooChat, but with a write-path variant:
- `resolveWritePath(projectRoot, requested)` — uses `resolve()` (not `realpath()`, since the file may not exist yet for creates), then verifies the result starts with `projectRoot + sep`.
- Deny list: everything in `secret_guard.ts` (`.env`, `*.pem`, etc.) — can't write to those either.
- Defense-in-depth: the `pending_changes` queue means even a path-guard bypass only queues; it doesn't hit disk until `/apply` (which re-validates).
### Diff format
Standard unified diff (what `git diff` produces). The `edit_file` tool takes `old_string` / `new_string` (same as Claude Code's edit tool — the model is trained on this shape). Server computes the unified diff for storage in `pending_changes.diff`.
### UI: per-pane diff viewer
Frontend pane type `pending_changes` in BooCoder's workspace. Shows:
- List of queued changes with file path + operation
- Per-change diff view (syntax-highlighted, side-by-side or unified)
- Approve / Reject per change, or Approve All / Reject All
## Path B — External agent dispatch
### dispatch_external_agent tool
```typescript
{
agent: 'opencode' | 'claude' | 'goose' | 'pi',
model: string, // e.g. 'claude-opus-4-7'
task: string, // natural-language task description
worktree?: string, // optional — auto-creates if not specified
}
```
### Transport selection
Dispatcher checks `available_agents.supports_acp` at runtime:
- **ACP** (preferred): `opencode acp`, `goose acp` — JSON-RPC stdio. Native session lifecycle, file-operation events, terminal events, permission prompts.
- **PTY** (fallback): `claude`, `pi`, `smallcode` — raw terminal capture via `node-pty`. Captures stdout/stderr/exit-code into PostgreSQL. Less structured than ACP.
### Worktree management
Each dispatched task gets its own git worktree:
```bash
git worktree add /tmp/booworktrees/<task-id> -b task-<task-id> HEAD
```
On completion: diff the worktree against HEAD, queue the diff into `pending_changes` for the same task, clean up the worktree. User approves/rejects the diff the same way as Path A.
### ACP event mapping
ACP events → BooCode parts taxonomy:
- `file_operation``tool_call` part (name: `acp_edit_file`) + `tool_result` part
- `tool_call``tool_call` part (preserves name)
- `terminal_output` → routes into BooTerm pane
- `permission_request` → pause inference (same mechanism as `ask_user_input`)
- `session_end` → task state → `completed` or `failed`
### MCP server auto-forward
Per goose docs, `context_servers` field in the ACP session config auto-forwards BooCoder's configured MCP servers to the dispatched agent. One MCP config drives every agent.
## Dispatcher worker
Background process (or in-process `setInterval` for v2.0 simplicity) that:
1. Queries `tasks` WHERE `state = 'pending'` ORDER BY `created_at`
2. For each ready task (no unmet dependencies):
- Mark `state = 'running'`
- Resolve execution path (Path A if no agent specified, Path B if agent specified)
- Path A: run the inference loop with write tools enabled
- Path B: spawn ACP/PTY subprocess, stream events into parts
- On completion: mark `state = 'completed'` or `'failed'`
- Queue output diff into `pending_changes`
3. On failure: mark `state = 'failed'`, surface in `human_inbox` view
## BooCoder MCP server
Exposes BooCoder's primitives as MCP tools so external agents (Sam's opencode in Termius) can drive the task queue:
| MCP Tool | Description |
|---|---|
| `boocoder.create_task` | Create a new task in the queue |
| `boocoder.list_pending_changes` | List queued changes awaiting approval |
| `boocoder.apply` | Apply a specific pending change |
| `boocoder.reject` | Reject a pending change |
| `boocoder.dispatch_external_agent` | Dispatch a task to an external agent |
| `boocoder.list_worktrees` | List active git worktrees |
Stdio transport for local consumers. HTTP transport deferred until OAuth + secret storage.
**Eval requirement:** run through `anthropics/skills mcp-builder` 10-question evaluation framework before shipping.
## Code lifts
### Primary architectural template
**`Dominic789654/agent-hub`** (Apache-2.0) — task DAG schema, dispatcher worker, project registry, human inbox. Three-process model (board server + dispatcher + assistant terminal). BooCode adapts this into a single-process Fastify app (v2.0.0) with the dispatcher as an in-process worker.
### Pending-changes UX
**`plandex-ai/plandex`** (MIT) — diff/apply/rewind vocabulary. The `pending_changes` queue concept, per-file diff view, approve/reject UI pattern. No code lifted — schema and UX design only.
### ACP client
**`agentclientprotocol.com` spec + `@zed-industries/agent-client-protocol` SDK** (Apache-2.0) — local-subprocess ACP via stdio JSON-RPC. The SDK handles framing; BooCode maps events to its parts taxonomy.
**`goose` docs** (`goose-docs.ai/docs/guides/acp-clients/`) — `context_servers` auto-forward pattern. Critical: one MCP config drives every dispatched agent.
### MCP server
**`anthropics/skills/mcp-builder`** (MIT) — 4-phase build workflow + 10-question evaluation framework for validating the MCP server before shipping.
### Dispatcher pattern
**Paseo (`getpaseo/paseo`)** — AGPL-3.0, **design only, no code lift**. Daemon+clients architecture, `--worktree` flag, CLI verb shape (`run/ls/attach/send`). BooCode reproduces the architecture using only license-clean patterns.
**Roo Code Boomerang Tasks** — orchestrator with intentional capability restriction. Down-pass/up-pass context discipline (`new_task` message, `attempt_completion` result, no implicit inheritance). Explicit precedence override clause.
### Write-tool security
**opencode `permission/evaluate.ts`** — wildcard permission ruleset (already lifted in v1.15). Extended in v2.0 to gate write tools.
**`covibes/zeroshot`** — blind-validation invariant. Verify gate runs in a separate agent context that only sees the diff and acceptance criteria, not the producing conversation. v2.0+ optional batch.
## Sub-versions
| Version | Scope |
|---|---|
| **v2.0.0** | Schema + Path A (native write tools + pending-changes queue + diff UI) + basic dispatcher |
| **v2.0.1** | Path B (ACP client for opencode/goose + PTY fallback for claude/pi + worktree management) |
| **v2.0.2** | BooCoder MCP server (stdio transport, `boocoder.*` tools, eval framework) |
| **v2.0.3** | Polish: `boocode` CLI (`run/ls/attach/send`), human_inbox UI, cost tracking |
## Dependencies
- v1.13 ✅ (parts table — the event taxonomy for everything)
- v1.14 ✅ (outer loop + step boundaries for future revert snapshots)
- v1.14.x-mcp ✅ (MCP client PoC — proves the protocol)
- v1.15 ✅ (full MCP client + tool globs — write-capable MCP servers route through pending_changes)
- v1.16 ✅ (codesight merge — codecontext now has blast-radius for impact analysis)
All dependencies shipped. v2.0 is unblocked.
## Estimate
- v2.0.0: ~800 LoC (schema + write tools + pending-changes service + diff pane + dispatcher skeleton)
- v2.0.1: ~600 LoC (ACP client + PTY dispatch + worktree management + event mapping)
- v2.0.2: ~400 LoC (MCP server + 6 tool handlers + stdio transport + eval)
- v2.0.3: ~400 LoC (CLI client + inbox UI + cost aggregation)
- **Total: ~2200 LoC** across 4 sub-versions
## Hard rules
- BooChat stays read-only. BooCoder is the only surface with write tools.
- Path-guard correctness is the #1 test target. Fuzz against every traversal pattern.
- Pending-changes queue gates ALL writes (native + MCP). Nothing touches disk without user approval (or explicit auto-apply flag per task).
- One shared database. Cross-surface joins are valuable (task → chat → terminal debugging session).
- External CLI agents on the host, not in containers. BooCoder shells out via local-exec.
- No OAuth in v2.0. MCP server is stdio-only until secret storage lands.
- DB rename `boocode_db``boochat_db` lands with v2.0.0 (one-time migration).
## Repos to clone before starting
```bash
cd /opt/forks
git clone https://github.com/Dominic789654/agent-hub.git # Apache-2.0, task DAG reference
git clone https://github.com/plandex-ai/plandex.git # MIT, pending-changes UX
git clone https://github.com/anomalyco/opencode.git # MIT, permission evaluate.ts + MCP client reference
```
ACP SDK is an npm package (`@zed-industries/agent-client-protocol`), installed at implementation time. Paseo is design-only (AGPL, no code lift).

View File

@@ -0,0 +1,130 @@
# v2.0 — BooCoder task breakdown
## Phase 0 — Prep (before any code)
- [ ] Clone lift sources: `agent-hub`, `plandex`, `opencode` to `/opt/forks/`
- [ ] Read agent-hub's schema + dispatcher pattern (Apache-2.0)
- [ ] Read plandex's pending-changes + diff/apply/rewind flow (MIT)
- [ ] Read opencode's `permission/evaluate.ts` for write-gate patterns (MIT)
- [ ] Install ACP SDK: `pnpm add @zed-industries/agent-client-protocol`
- [ ] Verify `opencode acp` and `goose acp` are available on the host
- [ ] Write `openspec/changes/v2.0-boocoder/design.md` with finalized decisions
## v2.0.0 — Schema + Path A (native write tools + pending-changes + diff UI)
### Infra
- [ ] Create `apps/coder/` directory skeleton (Fastify server, mirroring `apps/server/` structure)
- [ ] Create `apps/coder/Dockerfile` (Node 20 bookworm-slim, `/opt:/opt:rw` mount)
- [ ] Add `boocoder` service to `docker-compose.yml` (port 9502, boocode_net)
- [ ] Add Caddy route: `coder.indifferentketchup.com` → boocoder:9502
- [ ] DB rename: `boocode_db``boochat_db` (one-time ALTER DATABASE + docker-compose volume rename)
- [ ] Schema migration: CREATE TABLE `pending_changes`, `tasks`, `available_agents`; CREATE VIEW `human_inbox`
- [ ] Container guidance: `BOOCODER.md` (bind-mounted at `/app/BOOCODER.md`)
### Write tools
- [ ] `apps/coder/src/services/write_guard.ts``resolveWritePath(projectRoot, filePath)` (resolve + prefix-check, no realpath since file may not exist)
- [ ] `apps/coder/src/services/pending_changes.ts` — queue, apply, reject, revert operations
- [ ] Tool: `edit_file` — takes `{file_path, old_string, new_string}`, computes unified diff, queues in `pending_changes`
- [ ] Tool: `create_file` — takes `{file_path, content}`, queues as `operation='create'`
- [ ] Tool: `delete_file` — takes `{file_path}`, queues as `operation='delete'`
- [ ] Tool: `apply_pending` — flushes pending changes to disk (re-validates write_guard before each write)
- [ ] Tool: `rewind` — reverts applied changes by inverse-diff
### Inference loop
- [ ] Port the v1.14 outer loop from `apps/server/` into `apps/coder/` (or share via workspace package)
- [ ] Register write tools in the coder's tool registry (alongside all read tools from BooChat)
- [ ] Permission gate: write tools require `pending_changes` queue (can't bypass to direct disk write)
### Frontend (diff pane)
- [ ] Create `apps/coder/web/` SPA (React + Vite, same stack as BooChat's `apps/web/`)
- [ ] Diff pane component: shows pending changes with syntax-highlighted diffs
- [ ] Approve / Reject per change, Approve All / Reject All buttons
- [ ] Workspace splitter integration (chat pane + diff pane side by side)
### Verification
- [ ] `pnpm -C apps/coder build` clean
- [ ] Write path-guard fuzz tests (traversal patterns, symlinks, non-existent paths, `.env` deny)
- [ ] `docker compose up --build -d` — boocoder container starts, healthcheck passes
- [ ] Smoke: send a chat requesting a file edit → see it queued in diff pane → approve → file written
## v2.0.1 — Path B (ACP dispatch + PTY fallback + worktrees)
### ACP client
- [ ] `apps/coder/src/services/acp-client.ts` — spawn `opencode acp` / `goose acp` via `@zed-industries/agent-client-protocol` StdioTransport
- [ ] Event mapping: ACP `file_operation``tool_call` part, `terminal_output` → BooTerm route, `permission_request` → pause
- [ ] Session lifecycle: start, mid-session model switch, end
- [ ] MCP auto-forward: pass BooCoder's `context_servers` config to the ACP session
### PTY fallback
- [ ] `apps/coder/src/services/pty-dispatch.ts` — spawn `claude` / `pi` / `smallcode` via `node-pty`
- [ ] Capture stdout/stderr/exit-code into parts (less structured than ACP)
- [ ] Worktree setup: `git worktree add /tmp/booworktrees/<task-id> -b task-<task-id> HEAD`
- [ ] On completion: diff worktree vs HEAD → queue into `pending_changes`
### Dispatcher
- [ ] `apps/coder/src/services/dispatcher.ts` — polls `tasks` WHERE `state='pending'`, picks by priority + creation order
- [ ] Transport selection: check `available_agents.supports_acp` at dispatch time
- [ ] On failure: mark `state='failed'`, surface in `human_inbox`
- [ ] On completion: mark `state='completed'`, queue diff if Path B
### Agent probing
- [ ] Startup probe: `which opencode && opencode --version`, `which goose`, `which claude`, `which pi`
- [ ] Populate `available_agents` table with version + ACP support
### Verification
- [ ] Smoke: dispatch a task to `opencode` via ACP → task completes → diff queued
- [ ] Smoke: dispatch to `claude` via PTY fallback → captures output → diff from worktree
- [ ] Worktree cleanup after task completion
## v2.0.2 — BooCoder MCP server
### Implementation
- [ ] `apps/coder/src/services/mcp-server.ts` — register 6 tools as MCP tool handlers
- [ ] Stdio transport (use `@modelcontextprotocol/sdk` server-side, same SDK as client)
- [ ] Tools: `boocoder.create_task`, `boocoder.list_pending_changes`, `boocoder.apply`, `boocoder.reject`, `boocoder.dispatch_external_agent`, `boocoder.list_worktrees`
- [ ] Each tool maps to a DB operation or service call
### Eval
- [ ] Write 10-question eval per `anthropics/skills/mcp-builder` framework
- [ ] Run eval against the MCP server — all 10 must pass before shipping
- [ ] Document eval results in openspec
### Verification
- [ ] From a terminal: `echo '{"jsonrpc":"2.0","method":"tools/list","id":1}' | boocoder --mcp` → returns 6 tools
- [ ] From opencode: configure BooCoder as an MCP server in `~/.opencode/config.json`, verify tool calls work
## v2.0.3 — Polish
### CLI client
- [ ] `apps/coder/src/cli.ts` — thin WebSocket/HTTP client against BooCoder API
- [ ] Verbs: `boocode run <task>`, `boocode ls`, `boocode attach <id>`, `boocode send <id> <message>`
- [ ] Mirrors Paseo's UX, license-clean implementation
### Human inbox UI
- [ ] Frontend route showing tasks in `blocked`/`failed` state
- [ ] Per-task: view output, retry, cancel, reassign to different agent
### Cost tracking
- [ ] `tasks.cost_tokens` populated from inference usage
- [ ] Summary view: per-project, per-agent, per-day token spend
### Verification
- [ ] `boocode run "add a health endpoint"` from terminal → task appears in UI → completes → diff in pane
- [ ] `boocode ls` shows running/completed/failed tasks