Files
boocode/openspec/changes/orchestrator/artifacts/.discovery-notes.md
indifferentketchup 1937af8df9 feat: in-app Orchestrator (Phase 2) — multi-agent conductor
Brings the deterministic Han-flow conductor into BooCode: launch any read-only
flow from BooChat or BooCoder, watch each agent stream live in a Paseo-style
run pane, get an evidence-disciplined report — on local Qwen, persisted and
resumable. Read-only enforced hard via qwen --approval-mode plan (orchestrator
tasks fail closed if qwen is unavailable; never fall to write-capable native).

Backend (apps/coder): re-homed conductor defs, flow_runs/flow_steps schema,
flow-runner + dispatcher onTaskTerminal hook, restart-resume, runs routes
(launch/list/get/cancel), user-channel WS. Contracts: two flow_run_* frames.
Web: orchestrator pane kind + OrchestratorPane, Workflow button + slash flows
(BooChat/BooCoder parity), FlowLauncherDialog, "New Orchestrator" in the + and
split menus, runs history + export. Plan: openspec/changes/orchestrator.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 15:22:48 +00:00

6.8 KiB

Discovery notes — Orchestrator (Phase 2)

Single source of truth for project context. Specialists: read this first; do not re-grep for what's here. Search further only for what your domain needs that's missing.

Tech stack

  • pnpm monorepo. apps/server (BooChat: Fastify + Postgres), apps/coder (BooCoder: host systemd service, agent dispatch), apps/web (React + Vite), apps/booterm, packages/contracts (@boocode/contracts, cross-app wire SSOT).
  • DB boochat (Postgres 16). TypeScript strict, NodeNext (server/coder), .js import extensions. Tests: vitest (server + coder); no web test harness.
  • Deploy: apps/coder change → sudo systemctl restart boocoder; apps/web/ apps/serverdocker compose up --build -d boocode.

Phase 1 assets to reuse (/opt/boocode/conductor/)

  • src/spine.ts — the Spine→Flow factory (band gating, fold, synthesizer, validator, render), + contracts injection.
  • src/flows/*.ts + flows/index.ts — 22 flows (21 Spine configs + bespoke code-review.ts), registry (getFlow, FLOW_NAMES, describeFlows).
  • src/contracts.ts — Han evidence/yagni contracts (produce/review).
  • src/types.tsFlow, Step, Spine, Angle, Band, Contract.
  • src/flow.ts — the wave scheduler (dep-aware parallel). This is what Phase 2 must re-home/replace so steps dispatch through BooCoder backends + persist.
  • src/dispatch.ts — current opencode run subprocess dispatch. Replaced in Phase 2 by BooCoder backend dispatch.
  • agents/*.md — 23 Han personas (also live in ~/.config/opencode/agents/).

apps/coder — execution surfaces

  • src/services/dispatcher.ts:46createDispatcher. LISTEN 'tasks_new' fast path (pg trigger notify_tasks_new, schema line 279) + 2s poll. runTask routes a state='pending' task to a backend. inflight map keyed session_id ?? 'task:<id>' serializes per session.
  • src/services/agent-backend.ts:97AgentBackend (ensureSession / prompt / closeSession / dispose / health). Backends: backends/opencode-server.ts, warm-acp.ts, claude-sdk.ts; one-shot acp-dispatch.ts / pty-dispatch.ts.
  • AgentEvent (agent-backend.ts:28, union text|reasoning|tool_call|tool_update| commands) → mapped to WS frames by the dispatcher → broker.publishUserFrame.
  • Tasks are how work is dispatched. INSERT INTO tasks (project_id, input, agent, model, mode_id, thinking_option_id, session_id, chat_id) then the LISTEN/NOTIFY trigger picks it up. Precedents: routes/messages.ts:233, routes/skills.ts:94 (a skill IS already dispatched as a task), routes/arena.ts:49, tools/new_task.ts:54 (writes parent_task_id).

apps/coder — schema (src/schema.sql, coder-owned)

  • tasks (line 18): id, project_id, parent_task_id (FK self, written by new_task, NOT read by dispatcher), state CHECK(pending|running|completed|failed|blocked| cancelled), input, output_summary (≤500 char), agent, model, execution_path, cost_tokens, started_at, ended_at, session_id, arena_id, mode_id, thinking_option_id, chat_id.
  • agent_sessions (line 88): PK (chat_id, agent); backend, agent_session_id, server_port, status(idle|active|crashed|closed), config_hash, token/cost cols.
  • worktrees (line 142), available_agents (line 36), checkpoints (233), claude_session_entries (252). notify_tasks_new trigger (279).
  • Schema discipline (root CLAUDE.md): two schema files one DB; coder schema is applied by the host boocoder service. CHECK migrations: DROP IF EXISTS the system-named constraint → UPDATE → guarded ADD. CREATE OR REPLACE VIEW can't reorder cols. JSONB via sql.json(value as never). clock_timestamp() in txns.

packages/contracts — WS frames

  • src/ws-frames.ts — Zod frames in WsFrameSchema (SSOT). Existing: snapshot, message_started, delta, reasoning_delta, tool_call, tool_result, message_complete, usage, messages_deleted, chat_renamed, compacted, error.
  • Adding a frame (cross-app, root CLAUDE.md): add to WsFrameSchema here (rebuild pnpm -C packages/contracts build), AND the server's loose InferenceFrame union (services/inference/turn.ts), AND the web's strict WsFrame union (apps/web/src/api/types.ts) — the web type is the wire gate; missing it silently drops the frame at JSON-parse.

apps/web — panes + composer

  • Pane kinds (api/types.ts:386 WorkspacePaneKind): empty | chat | coder | terminal | settings | markdown_artifact | html_artifact. Extra non-chat pane kinds are already precedented — adding orchestrator follows markdown_artifact/html_artifact.
  • hooks/useWorkspacePanes.ts — pane state, addSplitPane(kind), server-persisted (+ legacy localStorage seed). Workspace.tsx, NewPaneMenu.tsx, ChatTabBar.tsx, PaneHeaderActions.tsx all take kind: 'chat'|'terminal'| 'coder' — adding a kind touches these.
  • ChatInput.tsx is the shared composer rendered by BOTH ChatPane.tsx and CoderPane.tsx (CoderPane also stacks AgentComposerBar above it). Its toolbar row (icons: Globe, ListPlus, Paperclip, Send/Stop, SquareSlash for slash) is where the Orchestrator button goes → parity for free. It takes slashGroups (ChatPane passes BooChat skills; CoderPane passes agent-commands+skills), onSlashCommand. SlashCommandPicker.tsx, hooks/useSkills.ts.
  • Mobile: per prior preference, crowded toolbars must fit one line (no scroll/wrap) and the new button shows icon-only on mobile.
  • Arena (routes/arena.ts): same task → N contestants (tasks sharing arena_id), parallel, [SELECTED] winner. Closest existing fan-out; stays separate but is a structural precedent for "one launch → many tasks grouped".
  • BooChat skills (apps/server routes/skills.ts + services/skills, getSkillBody): slash injects a skill body, the single chat model runs it inline. The coder also has routes/skills.ts that dispatches a skill as a task.
  • Event-dedup discipline (root CLAUDE.md): a mutation published via broker.publishUser must NOT also sessionEvents.emit locally; handlers idempotent.

Enumerated gaps (searched, not found)

  • No flow_runs / flow_steps / flows tables, no depends_on/step_index on tasks, no DAG/pipeline concept anywhere (confirmed Phase-1 research).
  • No orchestrator pane kind, component, or WS frame yet.
  • No coding-standards dir hits for orchestration; ADR dir not present under docs/adr/ (none found) — architectural decisions live in openspec/changes/.
  • No resume mechanism for a multi-step run after coder restart (single tasks resume via agent_sessions; a run spanning tasks does not).
  • The conductor's scheduler (conductor/src/flow.ts) is in-process/in-memory; it does not persist step state or survive restart.