# Normalized external-agent status (#10, scoped) **Status:** in progress (started 2026-06-01) **Source:** `boocode_code_review_v2.md` §1 #10, §5j (superset, Elastic License 2.0 — PATTERN-ONLY, clean-room; `/opt/forks/superset/.../map-event-type.ts`, `notify-hook.template.sh`, `agent-setup/*`). **Decision (Sam, 2026-06-01):** scoped status-publish now; config-injection notify-hook as a follow-on. ## Why (corrected premise) BooCoder already *observes* agent lifecycle (warm-acp/opencode/SDK backends know active/idle/crashed; the permission-waiter knows blocked) but never **publishes a normalized per-`(chat,agent)` status** to the UI — so blocked-on-permission is invisible and crash/idle aren't pushed proactively. The `AgentComposerBar` dot only shows WS liveness. This batch publishes the status BooCoder already knows; the heavier config-injection notify-hook (for out-of-band signals) is the documented follow-on. ## State model (clean-room from superset's `mapEventType`) Superset collapses ~30 vendor event names → 3 signals: **Start** (working), **PermissionRequest** (blocked), **Stop** (done). BooCoder adds idle (after done) + error (crash/fail). Normalized status: `working | blocked | idle | error`. ## Pinned frame contract (server + web, byte-identical, parity-tested) ```ts { type: 'agent_status_updated', chat_id: Uuid, agent: string, status: 'working' | 'blocked' | 'idle' | 'error', reason?: string, at: IsoTimestamp } ``` Added to `apps/server/src/types/ws-frames.ts` AND `apps/web/src/api/ws-frames.ts` (the `ws-frames` parity test), plus the web `WsFrame` union in `apps/web/src/api/types.ts`. Published via the coder's `broker.publishFrame` (validated against the server `WsFrameSchema`). ## Clean-room normalize helper (built now, reused by the injection follow-on) `apps/coder/src/services/normalize-agent-status.ts`: `normalizeAgentEvent(raw: string): 'working' | 'blocked' | 'done' | null` — a clean-room reimplementation of the vendor-event-name → bucket mapping (the event names are facts about each agent's hooks: `SessionStart`/`UserPromptSubmit`/`PostToolUse`→working; `PreToolUse`/`Notification`/`PermissionRequest`/ `exec_approval_request`→blocked; `Stop`/`session_end`/`task_complete`→done). The scoped publish points use BooCoder's own already-normalized turn boundaries; this helper exists so the config-injection follow-on (which receives raw vendor event names POSTed from agent hooks) reuses it. Unit-tested. ## Publish points (BooCoder's existing observation — no per-backend change) - Dispatcher (`dispatcher.ts`) turn boundaries, for every external-agent path (warm-acp/opencode/sdk/pty): `working` at turn start, `idle` on clean completion, `error` on failure. - Permission-waiter (`permission-waiter.ts` / the `setPermissionHooks` publish in `index.ts`): `blocked` when a permission is requested, back to `working` when resolved. A small `publishAgentStatus(broker, chatId, agent, status, reason?)` helper centralizes the frame. ## Frontend - `CoderPane.tsx` tracks the latest `agent_status_updated` per `(chat, agent)` (a small live map; reset on chat switch). - `AgentComposerBar.tsx` renders a normalized status dot beside the existing session chip (reuse the `StatusDot` visual language: working=spinner/green, blocked=amber, idle=gray, error=red), distinct from the WS-liveness `connected` dot. ## Follow-on (documented, not built): config-injection notify-hook Clean-room re-derive superset's `agent-setup`: inject a notify hook into each agent's native config (claude `~/.claude/settings.json`, opencode plugin, codex/gemini templates) that POSTs `{agent, chat_id, eventType}` to a new `POST /api/coder/agent-status` endpoint, which runs `normalizeAgentEvent` → publishes the SAME `agent_status_updated` frame. Reuses everything this batch builds. Catches out-of-band signals BooCoder's dispatch can't see. ## Verify - `pnpm -C apps/coder test` (+ normalize-agent-status tests) + `pnpm -C apps/server test` (ws-frames parity) - `pnpm -C apps/server build && pnpm -C apps/coder build`; `npx tsc -p apps/web/tsconfig.app.json --noEmit`