title: Phase 7b (sniffer slice) close — console emits agent.* for SNIFFER date: 2026-06-29 status: Accepted phase: 7b (sniffer slice) predecessor: docs/decisions/2026-06-29-phase-7b-council-close.md spec: docs/specs/2026-06-29-agent-registration-v1.md plan: docs/plans/2026-06-29-phase-7b-sniffer.md
Phase 7b sniffer-slice close
Shipped
rocky-hq/consolePR #17 (merge commit5e14bc1):POST /api/sniffer/analyzeemits the fullagent.{registered,invoked,completed}triple per invocation peragent-registration.v1. Producer lives atsrc/lib/sniffer/agent.ts(builders only);emitAgentHatchre-exported fromsrc/lib/ralph/agent.tsso all four 7b producers (RALPH, RELAY, COUNCIL, SNIFFER) POST to the same/api/relay/agentingest endpoint.- One agent per workspace,
agent_id = <workspace_slug>-sniffer-analyzer, registered withcapabilities = ["sniffer.analyze_repo"]. Per-invocationcapability = "sniffer.analyze_repo";invocation_id = randomUUID()per request (sniffer has no native execution id). outcomemapping: success →ok;AnalysisErroror unexpected throw →error.duration_msis wall-clock aroundanalyzeRepository. Rate-limit 429 short-circuits before any emit (request never reached the analyzer).- Emission gated on
x-rocky-workspaceheader (no agent identity without owner). Existing role-gate (operator/admin), rate-limiter, andAnalysisError→ HTTP status map unchanged. - Contracts version unchanged from 7b-ralph (
^0.3.0).
Locked decisions
1. Subsystem assignment: SS-04 WORKSPACE
Sniffer is operator-scoped tooling adjacent to the workspace surface and not yet a discrete SS#. The spec's owner.subsystem enum requires SS-01..SS-08. SS-04 is the closest fit (mirrors how COUNCIL lives under SS-04). A future SS# split-out would be additive at the projection layer.
2. invocation_id = randomUUID() per request
Sniffer has no native execution id (unlike RELAY's result.executionId). Mint once at agent.registered emit time, reuse across agent.invoked + agent.completed. Deterministic per request; not threaded into the response body.
3. Single capability in v1
sniffer.analyze_repo is the only registered verb. Future per-language / per-mode analyzers register as additional verbs in the same capability set (additive; consumer dedupes).
4. Failure path emits the triple
Unlike RELAY (no 5xx emit because no executionId available), sniffer mints invocation_id up-front, so the triple stays consistent across success and failure. AnalysisError and unexpected throws both produce completed{outcome:"error"}. The HTTP status map (404/400/429/502/500) preserves the failure mode for operators reading the route response.
5. Rate-limit 429 short-circuits emission
The per-user 5/hour limit fires before any agent emission. Request never reached the analyzer; the agent projection records no invocation. Operator-side observability of rate-limit hits stays in the existing 429 response + server logs.
6. Header gate consistent with prior slices
x-rocky-workspace required for emission. No header → no agent identity → no triple. Existing sniffer audit (rate-limiter recordSuccess) continues independently.
Deferred
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. Sniffer is operator-curated; no per-invocation approval gate. First approval.required: true candidate is now hearth driver (per producer-order from 2026-06-29-agentic-layer-projection.md).
Per-stage sub-events (fetch → select → analyze → respond)
The sniffer pipeline has internal stages (GitHub fetch, file selection, Anthropic call, validation). Each is a logical sub-step but not a distinct agent invocation. Sub-event decomposition deferred until the LORE causality graph proves it adds value.
Carry-forward to remaining 7b producers
This slice extends the pattern from 2026-06-29-phase-7b-relay-close.md §Carry-forward with one new wrinkle: synchronous producers without a native execution id mint invocation_id via randomUUID() up-front so the failure path can emit a coherent triple. STRATT will inherit this when it lands (also one-shot, no native id at the strategy-entry surface). Hearth driver is async (Go process, RPC over Unix socket) and reverts to the RALPH split-emit shape.
Remaining producers: STRATT (next, per producer-order), hearth driver (cross-submodule; Go producer in hearth/). Then 7b-ralph-2 (terminal observer) and 7c (hatch consumer + Polar RATE column).
Acceptance verified
npm run typecheckclean.npm run test:run417/417 (delta vs 7b-council: +6 sniffer-agent builder suite, +5 sniffer route agent-projection suite; no regressions).POST /api/sniffer/analyzeround-trips emit the three events withx-rocky-workspacepresent; emits zero when absent; emits triple withoutcome:"error"onAnalysisErrorand unexpected throw; emits zero on rate-limit 429 short-circuit.
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-relay-close.md— sync-producer pattern.docs/decisions/2026-06-29-phase-7b-council-close.md— predecessor slice.docs/plans/2026-06-29-phase-7b-sniffer.md— this slice's plan (approved at PR open).