Files
boocode/apps/coder/CLAUDE.md
indifferentketchup bef6bef504 docs: update changelog, roadmap, current focus, and coder CLAUDE.md
- CHANGELOG: v2.8.0-fork-lifts entry covering all 8 integrations
- Roadmap: update shipped header through v2.8.0, bump last-updated date
- CURRENT.md: reflect fork-lifts as last-shipped batch
- apps/coder/CLAUDE.md: document edit-guards behavior and API
2026-06-07 18:05:55 +00:00

10 KiB
Raw Permalink Blame History

apps/coder — BooCoder (deep reference)

Per-app engineering notes for apps/coder/src/. BooCoder runs as a systemd service on the host (boocoder.service), NOT in Docker — Fastify at port 9502, postgres at 127.0.0.1:5500. Cross-cutting commands, database, environment, workflow, and cross-app contracts live in the root CLAUDE.md. This file auto-loads when you read/edit files under apps/coder/.

Probe & provider discovery

  • services/provider-registry.ts — Static registry of provider metadata (label, transport, model source). PROVIDERS array, PROVIDERS_BY_NAME map. 5 providers: boocode (native), opencode (acp), goose (pty), claude (pty), qwen (pty). PROBED_AGENT_NAMES derives from it — adding/removing providers means editing this file, not the frontend.
  • services/agent-probe.ts — Startup probe via direct exec() (not SSH): discovers installed agents, versions, ACP support, models. Qwen models from ~/.qwen/settings.json; Claude models static from the registry. Persisted to available_agents.
  • routes/providers.tsGET /api/providers returns installed providers with models. Transport reflects actual capability (checks supports_acp from DB, not just registry preference). The apps/server side is "Provider picker dispatch" (see apps/server/CLAUDE.md).
  • Provider snapshot lifecycle (services/): provider-config.ts (Zod config, never-throws) → provider-config-registry.ts (buildResolvedRegistry, singleton) → provider-snapshot.ts (two-tier probe: tier-1 fast presence, tier-2 cold ACP probe skipped unless force / stale PROVIDER_PROBE_TTL_MS 24h / dbEmpty; cached). Verify live: curl http://100.114.205.53:9502/api/providers/snapshot — returns providers + models + commands, the exact shape AgentComposerBar renders.
  • PATCH /api/providers/config replaces a provider id's override object wholesale (per-id shallow merge) — to flip one field send {...existing, enabled}, or a custom ACP entry's command/label is wiped and it drops out of the resolved registry. data/coder-providers.json is gitignored (live runtime config — the coder reads AND writes it on UI toggles); tracked reference is data/coder-providers.example.json. The loader falls back to {providers:{}} (built-ins only) when absent, so a fresh checkout needs no copy.

Build, deploy, dispatch

  • Workspace dependency on @boocode/server: imports createInferenceRunner, createBroker, ALL_TOOLS, appendMcpTools from the server's compiled dist/. apps/server's package.json has an exports map with types conditions for NodeNext resolution. apps/server must build FIRST.
  • Build + deploy: pnpm -C packages/contracts build && pnpm -C apps/server build && pnpm -C apps/coder build && sudo systemctl restart boocoder. Env file at apps/coder/.env.host. Service file at /etc/systemd/system/boocoder.service.
  • After pnpm -C apps/coder build the host service keeps running the OLD process until sudo systemctl restart boocoder — a stale process shows new routes 404 with {error:'not found'} while old routes still 200 (the /api not-found handler shape). Restart, don't re-debug.
  • :9502/api/health is down ~1520s after a boocoder restart while the startup agent-probe scan runs — retry; an early connection-refused is not a failed deploy.
  • Agent dispatch spawns binaries directly using install_path from available_agents — no spawn('sh', ['-c', ...]) (fails under systemd). Paseo's pattern: spawn(fullBinaryPath, argsArray, { cwd }).
  • systemd hardening: only NoNewPrivileges=true is safe. ProtectSystem, ProtectHome, PrivateTmp all break agent dispatch (agents need full filesystem access to read configs, write to worktrees).
  • apps/server/tsconfig.json has declaration: true so .d.ts files exist for workspace consumers. The provider's package.json needs exports with types + default conditions per subpath ("./inference": { "types": "./dist/.../index.d.ts", "default": "./dist/.../index.js" }) — without the types condition, NodeNext can't find .d.ts files and tsc fails "Cannot find module" here.
  • Write tools (edit_file, create_file, delete_file, apply_pending, rewind) queue in pending_changes. Nothing hits disk until apply_pending. write_guard.ts validates paths (resolve + prefix-check, no realpath since files may not exist for creates).

Backends

Behavioral overview + flows + data model: see /docs/coder-backends.md. The notes below are the deep per-fact reference.

  • opencode runs as a warm HTTP server (services/backends/opencode-server.tsopencode serve per BooCoder process, one opencode session per BooCode session, resumed via agent_sessions). goose/qwen/claude dispatch one-shot ACP/PTY with no ctx/token usage; only native boocode (llama-swap) tracks ctx.
  • opencode SSE (opencode-server.ts): live streaming is session.next.text.delta / .reasoning.delta / .tool.{called,success,failed} — NOT message.part.* (terminal/post-hoc). client.event.subscribe({ directory }) MUST pass the session's worktree dir; omit it and opencode scopes events to the server process.cwd() → zero session events (empty turns, 180s timeout). Each live session owns its own subscribe loop + AbortController (a sessionID demux guard drops cross-session events when two share a dir). Turn completes on session.idle; promptAsync is fire-and-forget (204).
  • opencode model strings must be provider-prefixed (llama-swap/<model>) AND exist in ~/.config/opencode/opencode.json provider.llama-swap.models — not merely loadable by llama-swap. parseModel infers llama-swap/ for a bare id; the dispatcher coalesces empty→DEFAULT_MODEL then prefixes. agent-probe populates opencode's available_agents.models via mergeLlamaSwap (fetches /v1/models); empty model list → frontend sends '' → no inference (empty turn).
  • agent_sessions resume: config_hash = sha256('opencode_server|<model>') — must NOT include the server port (random per boot; breaks cross-restart resume). Keyed (chat_id, agent) — the tab/chat is the context unit (two opencode tabs = two contexts sharing one worktree). chat_id CASCADEs from chats; session_id/worktree_id are informational SET NULL. The worktrees table (one-per-session, survives session delete) supersedes the defanged session_worktrees. tasks.chat_id threads the tab id to the dispatcher; runOpenCodeServerTask resolves-or-creates a chat when null. The @opencode-ai/sdk v2 client takes flattened params ({sessionID, directory, parts, model:{providerID,modelID}}), createOpencodeClient from @opencode-ai/sdk/v2/client.
  • Claude SDK backend tool RESULTS arrive as type:'user' SDK messages (tool_result content blocks): mapSdkMessage (claude-sdk-map.ts) MUST map the user case → a terminal tool_update (completed/failed + output), else the tool_call persists status:'running' and the UI spinner never stops. The dispatcher's tool_update path then publishes + persists it.
  • ACP command discovery is async: acp-probe.ts must poll after newSession for available_commands_update (commands arrive in a later notification; reading synchronously captures 0). PTY providers (claude) discover from disk via claude-command-discovery.ts (~/.claude/commands + enabledPlugins, bare names, deduped). AgentCommand.kind tags 'command' vs 'skill'; CoderPane's slashGroups splits them into icon'd groups. SlashCommandPicker's groups? prop is opt-in.
  • A new per-message coder field silently drops unless you update every mapper: the HTTP read SELECT + mapCoderMessageRow (apps/coder/src/routes/messages.ts), the WS snapshot SELECT (apps/coder/src/routes/ws.ts) — it has its OWN column list and the client's snapshot handler setMessages-overwrites the HTTP load, so a field present in the HTTP route but absent here shows live yet vanishes on refresh — CoderPane.tsx (RawCoderMessage/CoderMessage/mapCoderTimelineRow + the live message_complete WS reducer), CoderMessageWire (CoderMessageList.tsx), and api/types.ts. The client mapCoderTimelineRow whitelists fields — easiest to forget. This bit model twice: the client chain (v2.7.9) and then the WS snapshot SELECT (v2.7.11) — the chip showed live but vanished on coder refresh until both were fixed.

Orchestrator (v2.7.17)

  • In-app multi-agent conductor: services/flow-runner.ts runs a flow by inserting each step as a tasks row (the existing dispatcher runs it) and advancing on a new onTaskTerminal dispatcher-deps hook; persisted in flow_runs/flow_steps (resumed at startup via initResume). The 22 conductor flow defs + Spine factory are re-homed under src/conductor/. Pure scheduler/resume helpers in flow-runner-decisions.ts. Full design: openspec/changes/archived/orchestrator/.
  • Read-only is load-bearing — don't add a dispatch path that bypasses it. Every step dispatches agent='qwen', mode_id='plan'; dispatcher.ts force-routes qwen+plan to the PTY --approval-mode plan gate and HARD-FAILS the task (never falls to write-capable native inference) when qwen is unavailable (shouldFailOnMissingAgent). BOOCODE_TOOLS gates BooChat's NATIVE inference tools only — it does NOT govern an external CLI agent (qwen/opencode bring their own write tools); read-only for a dispatched agent is the agent-layer mode (PTY --approval-mode plan; ACP setSessionMode is fail-OPEN by default, fail-CLOSED for plan via READ_ONLY_MODE_IDS in acp-dispatch.ts).

Edit safety guards (v2.8)

  • services/edit-guards.tsvalidateEditResult(original, updated, filePath) runs in pending_changes.ts immediately before writeFileAtomic. Rejects catastrophic truncation (>60% char loss AND >50% line loss). Throws a formatGuardError message that percolates to the agent as a visible error.
  • services/edit-guards-imports.tscheckDroppedImports(original, updated, filePath) detects removed import/require lines. Called alongside the truncation guard.
  • Both guards run on the /apply path only (not on queue). Re-queued identical edits re-validate at apply time.
  • Guard functions are pure — no DB or filesystem access. Easy to unit-test.