title: Phase 7b (stratt slice) close — console emits agent.registered for STRATT date: 2026-06-29 status: Accepted phase: 7b (stratt slice) predecessor: docs/decisions/2026-06-29-phase-7b-sniffer-close.md spec: docs/specs/2026-06-29-agent-registration-v1.md plan: docs/plans/2026-06-29-phase-7b-stratt.md
Phase 7b stratt-slice close
Shipped
rocky-hq/consolePR #18 (merge commitcbd5d2a):POST /api/stratt/doctrinesemitsagent.registeredafter each successfulsaveUploadedDoctrine, one event per uploaded doctrine, peragent-registration.v1. Producer lives atsrc/lib/stratt/agent.ts(builders only);emitAgentHatchre-exported fromsrc/lib/ralph/agent.tsso all five 7b producers (RALPH, RELAY, COUNCIL, SNIFFER, STRATT) POST to the same/api/relay/agentingest endpoint.- One agent per
(workspace, doctrine)pair.agent_id = <workspace>-stratt-<doctrine.id>with the doctrine slug normalized to[a-z0-9-](source-prefixed collision ids likestratt:counciland dot-containing filenames pass regex after normalization). capabilities = ["stratt.serve_doctrine"](fixed v1 verb).- No
agent.invoked/agent.completed. STRATT routes are registry CRUD — doctrine entries are registry entities, not per-request invocations. GET routes (list councils/doctrines/manifest) emit nothing. - Emission gated on
x-rocky-workspaceheader. Failure path (saveUploadedDoctrinethrows → 400) emits nothing (no agent identity existed). Emit failure is non-blocking (try/catch →console.error) so a hatch outage never faults a successful upload. - Contracts version unchanged from 7b-ralph (
^0.3.0).
Locked decisions
1. Lifecycle: agent.registered only
STRATT's routes are registry list/read + one upload. Only the upload maps to a lifecycle event: doctrine entries ARE the agents; upload = registration. Alternatives rejected at plan approval:
- Full triple on POST — treats upload as an invocation of a
stratt.ingestcapability. Weaker semantics (upload doesn't "do" anything except persist). - Full triple on every GET list — list ≠ invocation; would inflate the projection with noise events.
Consumer-side dedup is idempotent on agent_id body — re-upload of the same doctrine is a no-op at the projection layer.
2. agent_id = <workspace>-stratt-<doctrine.id>
One agent per doctrine entry. Doctrine id comes from the filename slug in console/src/lib/stratt/registry.ts:163; source-prefixed on collision (e.g. stratt:council). agent_id normalizes any [^a-z0-9-] char to - (dots from foo.bar, colons from stratt:council) to keep the spec regex ^[a-z][a-z0-9-]*-stratt-[a-z0-9-]+$ satisfied. Alternative (coarse <workspace>-stratt-doctrine id per workspace) rejected: hides doctrine identity from the hatch projection.
3. Capabilities: fixed ["stratt.serve_doctrine"]
Doctrine capabilities field in the parsed markdown is a string[] of nouns (frontend, theming, nextjs). The spec's AgentCapabilityVerbSchema regex ^[a-z][a-z0-9_]*\.[a-z][a-z0-9_]*$ requires <namespace>.<action> verb form; nouns like frontend fail. Registering them prefixed as stratt.frontend was rejected — the noun-per-doctrine mixing would poison the CAPABILITIES column with per-doctrine tags. Fixed verb keeps the registry projection scannable. Doctrine's parsed capability nouns still ride through additively via .passthrough() if future consumers want them.
4. Subsystem assignment: SS-04 WORKSPACE
STRATT registry lifts under SS-04 for the agent projection, mirroring sniffer/council placement. Not yet a discrete SS# in the CLAUDE.md subsystems table; a future SS# split-out would be additive at the projection layer.
5. Header gate consistent with prior slices
x-rocky-workspace required for emission. No header → no agent identity → no event. Existing role-gate (operator/admin) + upload semantics unchanged.
6. Failure paths emit nothing
- 400 (invalid doctrine,
saveUploadedDoctrinethrows): no agent identity ever existed. - 403 (non-operator): role-gate short-circuits before reaching the upload path.
- Emit-error (hatch outage,
emitAgentHatchthrows): caught and logged; 200 response preserved. A hatch outage must not turn a successful upload into a 500.
Deferred
Council-entry agent.registered
No POST surface exists for council upload; existing councils are read-only from vendor/stratt/ or the live STRATT_HQ_PATH checkout. Retroactive registration is explicitly forbidden by the predecessor decision's forward-only projection policy. Councils will land in a future slice if a write surface is added.
agent.revoked on doctrine delete
No delete route exists in /api/stratt/doctrines. When one lands, agent.revoked fires symmetrically.
Polar entitlement check at registration 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. Doctrine upload is operator-curated; no per-upload approval gate. First approval.required: true candidate remains hearth driver (per producer-order from 2026-06-29-agentic-layer-projection.md).
Doctrine-parsed capabilities into capabilities
Doctrine markdown carries a capabilities row (nouns like frontend, theming). Not projected as agent capabilities in v1 because they fail the verb regex. Future v1.x consumers can read them additively from the .passthrough() fields; no schema change required to expose.
Carry-forward to remaining 7b producers
Registered-only lifecycle establishes the projection shape for registry-CRUD producers: identity carriers (registry entries) emit agent.registered on write; no invoked/completed since there is no invocation surface. This is distinct from the sync-producer pattern (RELAY/SNIFFER: triple per request) and the async split-emit pattern (RALPH: submit/cancel/terminal).
Remaining 7b producer: hearth driver (Go producer in hearth/, cross-submodule). Async — reverts to the RALPH split-emit shape (submit + terminal). Then 7b-ralph-2 (terminal observer) and 7c (hatch consumer + Polar RATE column).
Acceptance verified
npm run typecheckclean.npm run test:run427/427 (delta vs 7b-sniffer: +5 stratt-agent builder suite, +5 stratt route agent-projection suite; no regressions).POST /api/stratt/doctrines(200) emitsagent.registeredwithx-rocky-workspacepresent; zero when absent; zero on 400 upload failure; zero on 403 non-operator; 200 preserved whenemitAgentHatchthrows.
Sources
docs/specs/2026-06-29-agent-registration-v1.md— wire format.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-sniffer-close.md— predecessor slice.docs/plans/2026-06-29-phase-7b-stratt.md— this slice's plan (approved at PR open).