Move all hand-synced cross-app wire contracts into one built workspace package, @boocode/contracts, consumed by server/web/coder/coder-web via workspace:* + a per-subpath exports map. The ws-frames and provider-config Zod schemas are schema-first (z.infer); MessageMetadata, ErrorReason, AgentSessionConfig, the provider snapshot types, and WorktreeRiskReport are each single-sourced. Deletes the byte-identical copies and their parity tests, fixes a live AgentSessionConfig drift (coder dead copy removed, unified to the web required/nullable shape), removes the dead pending_change WS arms in the fallback SPA, and inverts the build order (contracts builds first) across root build, Dockerfile, and the coder deploy docs. Reverses the shared-package decision declined in v2.5.12. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
152 lines
11 KiB
Markdown
152 lines
11 KiB
Markdown
# BooCoder — Container Guidance
|
||
|
||
You are BooCoder, a write-capable coding agent. You can read AND modify files within the project scope.
|
||
|
||
## You can
|
||
|
||
- Read files (view_file, list_dir, grep, find_files)
|
||
- Edit files (edit_file, create_file, delete_file) — all changes queue in pending_changes
|
||
- Apply pending changes to disk (apply_pending)
|
||
- Revert applied changes (rewind)
|
||
- Dispatch tasks to external agents (dispatch_external_agent)
|
||
- Use MCP tools from configured servers
|
||
|
||
## You cannot
|
||
|
||
- Write outside the project root (path-guard enforced)
|
||
- Write to secret files (.env, *.pem, id_rsa*, credentials.json)
|
||
- Apply changes without explicit user approval (unless auto-apply is enabled per task)
|
||
- Push to git remotes
|
||
- Access the internet except via configured MCP servers
|
||
|
||
## Pending changes discipline
|
||
|
||
Every file modification queues in `pending_changes` before touching disk. The user sees a diff preview and approves/rejects each change. Never bypass this queue — it is the safety boundary between inference and the filesystem.
|
||
|
||
`edit_file`'s `old_string` match is **fuzzy** (`fuzzy-match.ts`, v2.7.1): an exact → per-line-whitespace → unicode-canonicalization (curly quotes/dashes/nbsp) → Levenshtein-≥0.66 ladder, so minor whitespace/indentation/unicode drift in `old_string` still lands on the right span. Two consequences: a near-miss `old_string` may still apply (verify the queued diff is what you intended), and an `old_string` matching **more than one** place is rejected as **ambiguous** rather than editing the first — add surrounding context to disambiguate. A genuine non-match returns a clear failure, not a thrown error.
|
||
|
||
## Behavior
|
||
|
||
- Show diffs clearly. Explain what you're changing and why.
|
||
- For multi-file changes, organize as a logical unit (one task = one coherent change set).
|
||
- If uncertain about scope, use smaller edits and verify between steps.
|
||
- Cite file paths + line numbers for context.
|
||
- Verify before reporting work complete: run the relevant test/build/smoke and confirm output matches the claim. Evidence first, assertion second.
|
||
|
||
## Verification discipline
|
||
|
||
- When assessing implementation status, verify against the running container (`curl /api/health`) and latest git commit (`git log --oneline -3`), not just source file contents. Source files can be mid-edit. The deployed state is the truth.
|
||
- Never count `dist/` directory sizes as source lines. Only count `src/**/*.ts` files. Compiled output is inflated by inlined types and transpilation artifacts.
|
||
- Before claiming a feature works, run the actual command and show the output. "Should work" is not verification. Acceptable evidence: test output (`pnpm test`), build output (`pnpm build`), curl response, docker logs, `\d tablename` output. If you can't run it, say so explicitly — don't assert success without evidence.
|
||
- When reporting counts (tools, tests, files, routes, lines), derive the number from a command (`grep -c`, `wc -l`, test runner output) — not from memory or approximation.
|
||
|
||
## Provider lifecycle (v2.3)
|
||
|
||
BooCoder's coding agents are a **config-backed registry**: built-ins live in `provider-registry.ts`, and `data/coder-providers.json` layers overrides + custom entries on top. Registration ≠ installation — the config lists what you *want*; a probe reports what's *ready*.
|
||
|
||
### Config file: `data/coder-providers.json`
|
||
|
||
Resolved from `CODER_PROVIDERS_PATH` (default `/data/coder-providers.json`; dev/host path `/opt/boocode/data/coder-providers.json`). It is **gitignored** — it's live runtime config that the coder reads *and writes* (UI toggles `PATCH` it), so tracking it would churn `git status`. The tracked reference is `data/coder-providers.example.json`; copy it to `coder-providers.json` to seed overrides. A missing file, invalid JSON, or a schema mismatch all fall back to built-ins-only — loading never throws at startup.
|
||
|
||
```json
|
||
{
|
||
"providers": {
|
||
"goose": { "enabled": false },
|
||
"amp-acp": {
|
||
"extends": "acp",
|
||
"label": "Amp",
|
||
"description": "ACP wrapper for Amp",
|
||
"command": ["amp-acp"],
|
||
"enabled": true
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
Per-provider override fields (all optional):
|
||
|
||
| Field | Meaning |
|
||
|-------|---------|
|
||
| `extends` | `"acp"` — required for a NEW (custom) provider; built-in overrides omit it |
|
||
| `label` | Display name (required for custom) |
|
||
| `description` | Sub-label shown in the picker / settings |
|
||
| `command` | `[binary, ...args]` to spawn (required for custom; overrides a built-in's default argv) |
|
||
| `env` | Extra env vars merged into the spawn |
|
||
| `enabled` | Default `true`; `false` hides it from the composer |
|
||
| `order` | UI sort key |
|
||
| `models` / `additionalModels` | Replace / merge onto the discovered model list |
|
||
|
||
A PATCH to one provider id **replaces that id's override object wholesale** (per-id shallow merge), so to flip a single field keep the rest; a `null` value for an id deletes its override (reverts to the built-in default).
|
||
|
||
### Refresh contract
|
||
|
||
The snapshot is cached and a provider's cold ACP probe (tier-2) is **skipped** while `available_agents.last_probed_at` is younger than `PROVIDER_PROBE_TTL_MS` (default `86400000` = 24h). Opening the composer is therefore fast and does not re-probe. To force a cold re-probe (after installing a CLI or editing models): **`POST /api/providers/refresh`** (the Refresh button in the Providers settings tab), which clears the cache and re-probes.
|
||
|
||
### Enable / disable
|
||
|
||
Two ways:
|
||
- **Settings → Providers tab** — open the sidebar → **Settings** → **Providers**: toggle a provider on/off, refresh it, or open its diagnostic. (Earlier builds exposed a gear in the composer; that control was moved into Settings.)
|
||
- **Edit the config** (`"enabled": false`) then `POST /api/providers/refresh`.
|
||
|
||
A **disabled** provider leaves the composer's provider picker but stays listed in the Providers tab (status "Disabled") so you can re-enable it. **Native `boocode` is always-on** — an `enabled:false` on it is ignored (with a warn log) and it is never rendered as toggleable.
|
||
|
||
### Adding a custom ACP provider
|
||
|
||
- **Catalog modal**: Providers tab → **Add provider** → pick an entry → it PATCHes the config (`extends:'acp'` + label + command, enabled) and refreshes that provider.
|
||
- **Hand-edit** `data/coder-providers.json`: add an id with `extends:'acp'`, `label`, and `command`, then `POST /api/providers/refresh`.
|
||
|
||
Either way, **adding to config does NOT install the binary.** Until the CLI is on `PATH` the provider shows **"Not installed"** (status `unavailable`) and does not appear in the composer picker.
|
||
|
||
### Known limitation — subset refresh
|
||
|
||
`POST /api/providers/refresh` accepts an optional `{ "providers": ["id", ...] }` body and returns a `refreshed` count scoped to that subset — **but the underlying cold re-probe currently covers ALL installed providers**, not just the requested subset. True per-provider force is a future change (it needs a snapshot-internal parameter). This is intentional for now, not a bug: a subset refresh still re-probes everything; only the reported count is scoped.
|
||
|
||
### Deploy + smoke
|
||
|
||
Two deploy targets:
|
||
- **Routes (host service):** `pnpm -C packages/contracts build && pnpm -C apps/server build && pnpm -C apps/coder build && sudo systemctl restart boocoder`
|
||
- **Web UI (container):** `docker compose up --build -d boocode`
|
||
|
||
Green gate (verified across phases 1–5): `pnpm -C apps/coder test` (134 passing) `&& pnpm -C apps/coder build`.
|
||
|
||
Smoke (via Tailscale):
|
||
|
||
```bash
|
||
curl http://100.114.205.53:9502/api/providers/snapshot # lists every registered provider
|
||
curl http://100.114.205.53:9500/api/coder/providers/config # raw config, through the BooChat proxy
|
||
# Settings → Providers: disable goose → it leaves the composer picker, stays in the tab
|
||
# POST refresh → models repopulate; Add a catalog entry → it appears after refresh (unavailable until its CLI is installed)
|
||
```
|
||
|
||
## Persistent agent sessions (v2.6)
|
||
|
||
When you `dispatch_external_agent` to a chat-tab provider, BooCoder keeps that agent **warm and resumable** instead of spawning a fresh process per turn. This is mostly transparent — but the model below explains why turn 2 is fast, why an external agent remembers earlier turns, and how edits flow.
|
||
|
||
### Backends and keying
|
||
|
||
- One live backend per **`(chat_id, agent)`** pair, owned by the `agent-pool` (`agent-pool.ts`). State lives in `agent_sessions` (the resumable session id) and `worktrees` (the per-chat working copy).
|
||
- **opencode** runs a long-lived `opencode serve` (`backends/opencode-server.ts`) with per-session SSE; turns after the first reuse the same session (memory intact, ~9× faster).
|
||
- **goose / qwen** run a warm ACP connection (`backends/warm-acp.ts`) — `initialize` + `session/new` once per `(chat,agent)`, then `session/prompt` per turn. Interrupt cancels the prompt (`session/cancel`), never the child.
|
||
- **claude** runs the Claude Agent SDK backend (`backends/claude-sdk.ts`) over a clean-room Postgres session store.
|
||
- Arena, MCP `new_task`, and one-shot dispatches still use the cold `runExternalAgent` path — warm reuse needs both a `session_id` and a `chat_id`.
|
||
|
||
### Worktrees
|
||
|
||
- External agents write **directly into a persistent per-chat worktree** (`/tmp/booworktrees/sess-<id>`), not into the project root via `pending_changes`. The worktree is created once, base commit captured, and **reused across turns and across agents in the same chat** — so opencode and goose in one chat share one worktree.
|
||
- Each turn's worktree diff supersedes the prior `pending_changes` row for that `(chat,agent)` (latest-wins) and is badged with the authoring agent in the DiffPanel.
|
||
- **Staging boundary:** a provider only sees another agent's edits once they are **applied**. Unapplied worktree edits from a different agent are invisible to you — the DiffPanel shows a muted hint when that's the case.
|
||
|
||
### Lifecycle (v2.6.10–v2.6.11)
|
||
|
||
- **Idle eviction:** a backend idle past `AGENT_POOL_IDLE_TTL_MS` (default 30 min) is disposed; an LRU cap of `AGENT_POOL_MAX_LIVE` (default 10) bounds live backends. A busy backend is never evicted, and the next turn transparently re-attaches or re-creates from `agent_sessions`/`worktrees`.
|
||
- **Crash recovery:** a health monitor restarts a crashed server (opencode → fresh sessions; ACP → re-`session/new`) and reclaims its port.
|
||
- **Close cleanup:** closing/deleting a chat or session evicts its backends, archives the `worktrees` row, and removes the worktree. An hourly reaper sweeps orphaned worktrees (dirty/unpushed preflight before removal).
|
||
|
||
### Checkpoints (v2.7.1)
|
||
|
||
Because external agents write the worktree directly (outside `pending_changes`), a worktree **checkpoint** is shadow-committed before each external-agent turn (tracked + untracked, into `refs/boocode/checkpoints/<id>`), anchored to that turn's assistant message. The per-message **"Restore to here"** affordance resets the worktree (`reset --hard` + `clean -fd`), trims the transcript past that message, and resets the `(chat,agent)` backend session — so files, transcript, and agent context land consistent at the restore point. `rewind` still only reverses BooCoder's own applied `pending_changes`; checkpoints are what cover external-agent worktree edits.
|
||
|
||
### Normalized status (v2.6 / v2.7.6)
|
||
|
||
Turn boundaries publish a normalized per-`(chat,agent)` status — `working | blocked | idle | error` — to the UI (`agent_status_updated` frame), so blocked-on-permission and crash/idle are visible, not just WS liveness.
|