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

117 lines
6.8 KiB
Markdown

# 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/server``docker 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.ts``Flow`, `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:46``createDispatcher`. `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:97``AgentBackend` (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.
## Precedents / related
- **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.