Apply 7 proposed edits from guidance improver audit: - CLAUDE.md: refusal rails up front, version anchor, resolution order - BOOCHAT.md: resolution order section - BOOCODER.md: tool reliability callouts - data/AGENTS.md: tool list drift guard, failure modes preamble
12 KiB
BooCoder — Container Guidance — v2.7.x (last meaningful update: 2026-06)
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
Tool reliability
edit_file's fuzzy match can succeed on a near-miss or return ambiguous whenold_stringmatches multiple locations. Always verify the queued diff before callingapply_pending— the diff preview is authoritative, the tool's "success" return is not.- The external agent's worktree diff only shows changes since the last turn, not since the project baseline. The DiffPanel merges these, but if you call
git diffdirectly, you'll get incomplete results.
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 countsrc/**/*.tsfiles. 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 tablenameoutput. 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.
{
"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) thenPOST /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 withextends:'acp',label, andcommand, thenPOST /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):
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 theagent-pool(agent-pool.ts). State lives inagent_sessions(the resumable session id) andworktrees(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/newonce per(chat,agent), thensession/promptper 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 coldrunExternalAgentpath — warm reuse needs both asession_idand achat_id.
Worktrees
- External agents write directly into a persistent per-chat worktree (
/tmp/booworktrees/sess-<id>), not into the project root viapending_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_changesrow 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 ofAGENT_POOL_MAX_LIVE(default 10) bounds live backends. A busy backend is never evicted, and the next turn transparently re-attaches or re-creates fromagent_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
worktreesrow, 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.