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>
117 lines
6.8 KiB
Markdown
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.
|