title: Phase 7b (council slice) close — console emits agent.* per COUNCIL persona date: 2026-06-29 status: Accepted phase: 7b (council slice) predecessor: docs/decisions/2026-06-29-phase-7b-relay-close.md spec: docs/specs/2026-06-29-agent-registration-v1.md plan: docs/plans/2026-06-29-phase-7b-council.md
Phase 7b council-slice close
Shipped
rocky-hq/consolePR #16 (merge commitdc1cb3c):POST /api/workspaceconvene_councilnow emitsagent.{registered,invoked,completed}per COUNCIL persona peragent-registration.v1. Producer lives atsrc/lib/council/agent.ts(builders only);emitAgentHatchre-exported fromsrc/lib/ralph/agent.ts(same/api/relay/agentendpoint that landed in 7b-ralph).- Per-persona fan-out: N personas in a council emit N×(
agent.registered+agent.invoked) at convene-start, N×agent.completedat stream end.agent_id = <workspace_slug>-council-${designation.toLowerCase()};invocation_id = ${conveneUuid}-${designation_lower}(deterministic per(convene, persona)pair). capabilities = ["council.respond"](spec v1).approval.required: false.outcome: stream success →ok; stream error →error.duration_msshared per debate (wall-clock).- Emission gated on
x-rocky-workspaceheader; existing debate streaming + message persistence unchanged. - Contracts version unchanged from 7b-ralph (
^0.3.0).
Locked decisions
1. Per-persona fan-out (spec §COUNCIL personas resolved literally)
The predecessor decision (2026-06-29-agentic-layer-projection.md) flagged "do personas register as agents" as the question that defines what counts as "an agent" project-wide. Spec §COUNCIL personas resolved yes. This slice implements that literally: a 5-persona council produces 15 events per convene (5× registered + 5× invoked + 5× completed). The HATCH ledger absorbs the volume cheaply (best-effort appendAudit); the projection benefits from per-persona auditability.
2. invocation_id = ${conveneUuid}-${designation_lower}
conveneUuid minted once per convene call via randomUUID(). Deterministic per (convene, persona) pair, so completion correctly pairs with invocation across the route's emit-at-start / emit-at-end split. Does NOT co-key with the debate message id — that mapping stays via session/message ids in the existing audit path. If a future query wants persona-row ↔ debate-message join, it joins on session_id + actor + timestamp window, not on invocation_id.
3. outcome is per-debate, not per-persona
The current stream gives one success/error signal for the whole debate. All personas in a convene share the same outcome. Per-persona outcome (a persona refused to speak, a persona's block was malformed) would require a debate-parser pass over fullText — out of scope. The spec's outcome enum stays valid because "the debate failed → every persona's invocation failed" is a true statement at this altitude.
4. duration_ms is wall-clock, shared
Same reasoning as outcome: the stream has one wall-clock. Each persona's completed carries the same duration_ms. Per-persona timing is interesting future work; not blocking projection landing.
5. No promote-path agent emission
POST /api/workspace/council/promote is an operator gesture on a persisted debate — not a persona-side invocation. Recording it as an agent event would over-attribute work; the existing artifact record is the right surface.
6. No STRATT CRUD emission this slice
GET /api/stratt/councils is read-only. A future STRATT CRUD path would emit agent.registered (re-declaration) when council YAML changes; until that surface exists there is nothing to wire. Forward-only projection (per predecessor decision §consequences) means pre-existing personas register on their first post-merge convene call, not at boot.
7. Header gate carries forward
No x-rocky-workspace → skip agent emission. Same guard as RALPH cancel-path and RELAY route. Council debates in workspace-less contexts (smoke scripts, console-internal tests) stay invisible to the agent projection by design.
Deferred
Per-persona completion signal
agent.completed per persona currently carries shared outcome + duration. A debate-parser pass over the streamed text could attribute per-persona spans and emit individualized completions. Not blocking — the projection is forward-only and additive; per-persona attribution can replace shared attribution in a follow-up without a schema change.
agent.revoked on persona removal
Personas come and go via STRATT council YAML edits. No console-side gesture removes a persona today. Revocation surface waits for the STRATT CRUD slice (or a 7c sweep that synthesizes revocations from the gap between projected agents and current STRATT registry).
Polar entitlement check at invocation time
tier_floor: "solo" + seats_required: 0 → no-op for this slice. Real check waits for Phase 7c.
Airlock agent.approve verb
approval.required: false for council personas (operator-curated fixtures, not provisioned tools). First user remains SNIFFER or hearth driver.
Carry-forward to remaining 7b producers
The carry-forward pattern from 2026-06-29-phase-7b-relay-close.md §Carry-forward holds. Specific additions from this slice:
- Fan-out producers (one logical surface, multiple agent identities per invocation — like council personas) follow the per-identity loop pattern in
src/app/api/workspace/route.tsemitCouncilStart/emitCouncilCompleted. Shared outcome + duration when the underlying invocation cannot split them per-identity is acceptable. - Streaming surfaces split emission into a start-side helper (registered + invoked) and a finally-side helper (completed) wrapped in try/catch so an emit failure cannot break the user-visible stream.
- The
<subsystem>/agent.ts+ re-exportedemitAgentHatchpattern is now established at 3 producers. Factor tosrc/lib/hatch/agent-emit.tswhen the 4th producer lands (SNIFFER), not now.
Remaining producers: SNIFFER (analyzer runs; first approval.required: true candidate per spec carry-forward), STRATT (strategy entry; CRUD-shaped, may overlap council registration), hearth driver (cross-subsystem Go producer in hearth/).
Acceptance verified
npm run typecheckclean.npm run test:run406/406 (delta vs 7b-relay: +7 council-agent suite; no regressions elsewhere).- Convene path emits per-persona triple when
x-rocky-workspacepresent; emits zero events when absent; stream error path flips outcome to"error"for all personas; emit failures inside the finally block are logged and do not break the stream.
Sources
docs/specs/2026-06-29-agent-registration-v1.md— wire format; §COUNCIL personas resolution.docs/decisions/2026-06-29-agentic-layer-projection.md— predecessor; producer-order ralph→relay→council→sniffer→stratt→hearth driver.docs/decisions/2026-06-29-phase-7b-ralph-close.md+docs/decisions/2026-06-29-phase-7b-relay-close.md— patterns this slice builds on.docs/plans/2026-06-29-phase-7b-council.md— this slice's plan (approved at PR open).