← Ledger


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

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:

  1. 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.ts emitCouncilStart/emitCouncilCompleted. Shared outcome + duration when the underlying invocation cannot split them per-identity is acceptable.
  2. 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.
  3. The <subsystem>/agent.ts + re-exported emitAgentHatch pattern is now established at 3 producers. Factor to src/lib/hatch/agent-emit.ts when 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

Sources