# Incident-Dispatch Brief System — design

audience: AI coding agents first. PLAN_SLUG: `incident-brief`.

## Problem

An incident-dispatched agent today receives ONLY the owner's free text plus whatever
generic CLAUDE.md its CLI loads. No incident discipline is injected: no
reproduce-before-fix, no evidence-or-open rule, no write-back obligation, no knowledge
of similar past incidents, no per-type playbook. Verified 2026-08-08:
`collector/src/incidents/incident-service.ts` holds the dispatch state machine and
metadata (cli/model/effort/account) but assembles no prompt anywhere.

## Goal

Every incident dispatch carries a server-assembled **brief**: owner text = the
*situation*; the brief supplies identity, discipline, environment map, type playbook,
and similar-incident memory. Resolution is artifact-gated and feeds a knowledge base
future briefs draw from.

## Non-goals

- No embeddings/vector search (corpus is ~10² entries; type+recency selection wins on
  simplicity and is reversible).
- No collector-owned KB database — knowledge lives in the repo, versioned, converged.
- No change to collector incident ingestion/state machine semantics.

## Architecture

Knowledge is repo-versioned; assembly is server-side at dispatch time.

```
modules/workstation/claude/incidents/     (deployed by packaging/deploy-local.sh)
  taxonomy.json          type registry (id, title, absorbs[], skill, doctrine line)
  dispatch-template.md   brief template with {{placeholders}}
  placement-map.md       where things live (~/.overdeck, ~/Projects/overdeck, deploy clone, state dirs)
  never-touch.md         hard prohibitions (sshd config, ~/stupid.user, main-checkout writes, kill-by-name, owner sessions)
  kb/<incident-id>.md    one resolved incident per file (phase 2)
```

Collector reads the DEPLOYED copy (same resolution as other deploy-clone reads —
follow `paths.ts` conventions), never the dev checkout.

### Components

**1. Taxonomy (`taxonomy.json`)** — closed list, seeded from the 217-record
`COMPLAINT_REGISTRY.jsonl` category distribution:

| type id | absorbs (registry categories) | skill |
|---|---|---|
| resource-overload | cpu-load, memory-load, disk-io-load, runaway-process, disk-cache-fatigue | od-overload |
| machine-down | system-crash, machine-lockout, graphics-crash | od-recovery |
| cluster-offload | tool-reliability (buildbox/k3s/offload) | od-cluster |
| hooks-harness | hook-timeout, context-spam, polling-anti-pattern | od-hooks |
| auth-credentials | credential-session-loss | od-auth (exists) |
| agent-discipline | agent-ignores-instructions, git-decision-dumping, delivery-approval-fatigue | od-agent-discipline |
| notification-spam | notification-spam, notification-fatigue | od-notifications |
| data-safety | lost-work, data-retention, file-sprawl | od-data-safety |
| guard-tooling | (new class — shims/gates/wrappers/hooks misfiring; no clean legacy category; tool-reliability-fatigue records split case-by-case at authoring) | od-guards |

Coverage (EXACT, computed 2026-08-08 from `COMPLAINT_REGISTRY.jsonl`, recorded in
taxonomy.json `_meta`): 9 types absorb 205 of 217 records across 21 of 22 named
categories. Per type: resource-overload 59, agent-discipline 61, hooks-harness 24,
notification-spam 22, machine-down 16, data-safety 9, guard-tooling 7,
auth-credentials 5, cluster-offload 2. The 18 `tool-reliability-fatigue` records
split case-by-case (ids in taxonomy `_meta`): 9 → hooks-harness (harness
reliability), 7 → guard-tooling (shim/wrapper/lock/throttle), 2 → cluster-offload
(offload/failover). Unmapped: `ci-ritual-fatigue` (11) BY DESIGN — process-ceremony
complaints, not machine incidents — plus 1 record (C217) with no `category` field.

Shape per entry: `{ id, title, absorbs: string[], skill: string, doctrine: string }`
(doctrine = one line, e.g. "measure first; kill by cgroup, never by name").
`other` is NOT a type — unknown category maps to no type: brief omits the
type/skill/similar sections rather than injecting a wrong playbook.

**2. Incident type on the incident record.** New optional metadata key
`overdeck.incident_type` (taxonomy id). Filing form gains a Type dropdown fed from
`GET /incidents/options` (extend the existing options endpoint pattern the dropdowns
change introduced). Absent type ⇒ brief assembles without type-specific sections.

**Suggested type (deterministic, owner-overridable).** Untyped incidents make the
flagship sections inert, and the owner files terse mid-fire. So: keyword match of
title+description against the absorbs[] category lexicon (each taxonomy entry carries
`keywords: string[]`, seeded from its absorbed categories' vocabulary). Best match is
PRE-SELECTED in the filing dropdown; owner can override or clear. NEVER silently
applied to API/CLI filings — suggestion is a UI affordance only; an untyped API filing
stays untyped. No embeddings, no LLM classification.

**3. Brief assembly (`collector/src/incidents/dispatch-brief.ts`)**

```ts
assembleDispatchBrief(incident: Incident, deps: BriefDeps): DispatchBrief
// BriefDeps: { readAsset(name: string): string | null; listKb(): KbSummary[] }  — fs injected, pure logic testable
// DispatchBrief: { text: string; sections: string[] }  — sections = which template blocks resolved (for tests/UI)
```

Template placeholders (all resolved server-side): `{{incident_id}}`, `{{priority}}`,
`{{type}}`, `{{doctrine}}`, `{{skill}}` (invocation line for the per-type skill),
`{{situation}}` (owner title+description, verbatim, clearly fenced as the situation),
`{{similar}}` (phase 2: up to 3 same-type KB summaries, each: one line what happened +
one line how solved + KB id), `{{placement_map}}`, `{{never_touch}}`.

Template content (authored per `agent-doc-authoring`, caveman register) pins the
discipline: reproduce before fixing; **resolved requires a CHECKABLE artifact** —
a sha reachable on origin/main, a named passing test, or a live-state command +
expected output the reviewer can run; free prose is NOT an artifact — no artifact
⇒ stays open. Template MUST pin the exact `od-incidents` invocation lines verbatim
(list/show/resolve syntax) so the agent never guesses the CLI shape;
worktree isolation + install-before-landing (point at repo CLAUDE.md, do not copy);
write the outcome back via `od-incidents resolve` — transcript-only findings do not
exist; `/learn-from-incident` at resolution (phase 2).

Missing asset file ⇒ assembly THROWS (fail-closed: a dispatch without discipline is
the bug this system exists to kill). Missing type ⇒ sections omitted, no throw.

**4. Brief endpoint + persistence.** `GET /incidents/:id/brief` returns the assembled
brief (auth: same as other incident routes). At dispatch transition
(`filed → dispatching`) the assembled brief is stored to task metadata
`overdeck.dispatch_brief` so the UI shows EXACTLY what the agent was told (honest
data; no post-hoc reconstruction). Incident page renders it read-only.

**Provenance:** alongside the brief, store `overdeck.dispatch_brief_provenance` =
the deployed assets' commit sha + deploy stamp (deploy-local.sh already stamps the
deploy clone; read that stamp). A stored brief is only auditable if you know which
asset version produced it.

**Dispatch transition dependency (discovery 2026-08-08, outcome B):** main has NO
`filed → dispatching` transition and NO launcher anywhere — branch
`codex/incidents-file-dispatch` (b792a383, unmerged, 380 insertions with tests) adds
web-UI dispatch that flips metadata only (no CLI spawn). Landing that branch is
phase-1 scope: it provides the transition the brief-persistence hook needs. The
launcher itself (component 5) is the FIRST phase-2 item, not phase-1.

**5. Launcher integration.** DISCOVERY RESOLVED (2026-08-08): no launcher exists —
outcome B. The brief is wired so the FIRST launcher gets it for free (metadata
persistence, #4). Building the launcher is the FIRST phase-2 item: it MUST pass the
brief text as the prompt instead of raw owner text (read `overdeck.dispatch_brief`
or fetch `GET /incidents/:id/brief`). Phase-1 exit criterion is accordingly:
brief assembled + persisted at dispatch + visible in UI + `od-incidents resolve`
works — plumbing an agent CAN receive, not yet auto-received.

**6. `od-incidents` CLI** (`modules/workstation/claude/bin/od-incidents`, bun,
deployed to PATH like sibling bin/ tools). Talks to collector HTTP (base URL + auth:
same source the web/status tooling uses; read it, do not invent).

```
od-incidents list [--type T] [--state S]     table: id, state, type, priority, title
od-incidents show <id>                        full record incl. brief + metadata
od-incidents search <query>                   title/description substring match server-side
od-incidents resolve <id> --artifact <ref> [--summary <line>]
```

`resolve` REQUIRES `--artifact` — refuses otherwise (exit 2, message names the rule).
Artifact = free-form ref but non-empty and recorded to metadata
`overdeck.resolution_artifact` + `overdeck.resolution_summary`; state → resolved via
the existing state transition path. Works identically for claude/codex/cursor-agent
dispatches (CLI on PATH beats per-CLI MCP config).

**7. Phase 2 (pre-approved to start when phase 1 is usable):**
- The 7 new per-type skills (od-overload, od-recovery, od-cluster, od-hooks,
  od-agent-discipline, od-notifications, od-data-safety) — repo-backed under
  `modules/workstation/claude/skills/`, converged/deployed like existing ones; each
  authored via `agent-doc-authoring`; content seeded from the corresponding incident
  post-mortems, memories, and existing guard code (read, then write).
- KB corpus: `incidents/kb/<id>.md` — frontmatter `{id, type, date, artifact}`, body
  = 1-line what-happened + 1-line how-solved + evidence links. `ft index` wiring for
  interactive search; collector `listKb()` reads the deployed dir.
- `learn-from-incident` skill (agent-variant LFM): at resolve time the dispatched
  agent writes the KB entry AND proposes a diff to its per-type skill; both land
  through the normal gate (review catches rubbish edits). Distinct from
  `learn-from-mistakes` (which edits the misbehaving skill's Learned Rules from the
  operator side); this one is invoked by the incident agent itself and its target is
  the incident KB + od-* skills.
- Recurrence surfacing: at filing, collector lists same-type incidents from last 30
  days (Kanboard query) in UI + `{{similar}}`.

## Error handling

- Assembly fail-closed on missing template/assets; type-less incidents degrade
  gracefully (sections omitted).
- **Deploy-time canary:** `packaging/deploy-local.sh` (post-deploy check) assembles a
  synthetic brief against the freshly deployed assets and FAILS the deploy if
  assembly throws. Catches a broken asset at deploy time, not at the next fire.
- **Dispatch-time throw → named refusal:** UI shows "dispatch blocked: brief assembly
  failed — <reason>", never a bare 500. One explicit owner-click "dispatch without
  brief" degraded path exists; taking it is logged to the event store.
- CLI: non-2xx from collector → print status + body, nonzero exit; never silent.
- Brief persistence failure at dispatch transition aborts the transition (a dispatch
  that can't record what it told the agent must not proceed).

## Testing

- `dispatch-brief.test.ts`: placeholder resolution; type-less omission; missing-asset
  throw; brief persisted at transition; endpoint auth + 404.
- CLI: unit-test argument gate (`resolve` without `--artifact` exits 2) + one
  mocked-HTTP happy path per subcommand (follow collector test style; bun test).
- Live validation (phase 1 exit criterion, outcome-B shape): file a test incident
  with a type via the UI (suggested type pre-selected), fetch its brief, dispatch
  (state flips, brief + provenance persisted, visible on incident page),
  `od-incidents resolve` it with an artifact, see state + artifact on the page.
  Deploy canary green.

## Observability (feeds #84)

- Dispatch lifecycle transitions → event store, brief attached.
- Incidents page: evidence column (resolution artifact), brief viewer, same-type
  recurrence links.
- Weekly per-type digest generated from the event store (phase 2+, part of #84 design,
  not this plan).

## Execution constraints (owner-set, 2026-08-08)

- Launcher discovery ran FIRST (advisor-ordered): outcome B, recorded in components
  4/5. Phase-1 exit criterion adjusted accordingly; launcher = first phase-2 item.
- Phase 1 now; NO run-plan (harness mid-refactor per owner). Execute via direct
  subagent/ca.sh lanes; cursor ca.sh quota usable freely. Lanes dispatch through the
  normal admission queue — never bypass. Main thread is sole verifier before landing.
- Phase 2 starts immediately once phase 1 is usable (a brief demonstrably persisted +
  resolvable via CLI on a real incident) — no re-ask.

## Architecture Decisions

- Repo-versioned knowledge + server-side assembly over client-side assembly: one
  source of truth, UI parity (brief visible), works for every CLI.
- CLI over MCP for agent incident access: identical across claude/codex/cursor-agent.
- Deterministic type+recency similar-selection over embeddings: reversible, zero new
  infra at this scale.
- Collapsed candidate module "brief renderer service": assembly is one pure function +
  fs deps inside the existing incidents module — a separate service failed the
  deletion test.
