# Plan: complete Incidents end to end

## Outcome

Finish the checkpointed Incidents work as one coherent, deployable outcome:

- preserve every incident-related change already present at `58551673` (the six WIP commits after merge base `6e656142`);
- reconcile that WIP with the real, freshly fetched `origin/main` before changing behavior and again immediately before landing;
- make the historical complaint registry a complete, idempotently imported Kanboard corpus;
- give every incident an explicit, validated metadata schema without inventing unknown historical facts;
- make Model and Account selectors come from executable deployed authority and route the selected billed account for real;
- make `POST /incidents` perform the specified file-and-dispatch flow, including one isolated transient runner, rather than merely creating a task or changing a column;
- deploy only after all affected suites pass, then prove the installed endpoint, imported corpus, wrapper contract, runner, and browser flow.

The newer request supersedes the old “historical import is out of scope” and “launcher is phase 2” status notes in the two incident design documents. Kanboard remains the sole incident store; do not add a second runtime database or a browser-owned queue.

## Current checkpoint and reconciliation

At planning time the real GitHub `main` was `802ae80e`; it was 50 commits ahead of merge base `6e656142`, while the checkpoint had six WIP commits (`a9a1e627` through `58551673`). The upstream range had no effective file overlap with the WIP range then, but main is moving and this observation is not a substitute for a fresh fetch.

1. Create a named branch at the current detached `HEAD`; do not reset, squash away, or reconstruct the checkpoint. Record `git status`, the six commits, and `git diff <merge-base>..HEAD` before integration.
2. Fetch the actual `origin/main` and merge it into the WIP branch (do not rebase away checkpoint history). Preserve all current incident changes in `apps/web`, `collector`, `modules/harness`, and `modules/workstation`. Treat `.rb-origin` and `.factory/` as orchestration state, not product changes; do not clean or commit them.
3. Re-read `AGENTS.md`, `.claude/skills/od-ui-dev/SKILL.md`, and `modules/harness/CLAUDE.md` after the merge. Resolve conflicts by retaining both the upstream reliability/deploy changes and the incident behavior below; never weaken a gate.
4. Before landing, fetch and merge the then-current `origin/main` once more. Re-run the full affected verification if that merge changes any touched seam.

## 1. Pin the revised contracts and historical source

### Documentation and source asset

- Amend `docs/specs/2026-08-08-incidents-page-design.md`, `docs/specs/2026-08-08-incident-brief-design.md`, and the selector design/status notes so they describe the shipped behavior: historical registry import is now required, new filings require complete metadata, filing automatically dispatches, and the transient launcher/runner is in scope.
- Recover the current canonical `~/Projects/0 DOCS/COMPLAINT_REGISTRY.jsonl` without synthesizing rows. The known schema includes `id`, `date`, `date_precision`, `category` (absent on at least C217), `complaint_short`, `verbatim`, `source_doc`, `source_section`, `resolution_status`, and `repeat_of`; taxonomy evidence records 217 rows as of 2026-08-08, but implementation must compare against the actual recovered source rather than hard-code 217.
- Copy the exact validated JSONL into a versioned deployed incident asset such as `modules/workstation/claude/incidents/history/complaint-registry.jsonl`, with a small provenance manifest containing source path, row count, and SHA-256. Deployment must fail closed if the source snapshot is missing, malformed, has duplicate IDs, a broken `repeat_of`, or differs from its manifest. Do not deploy a partial reconstruction.
- Add a strict parser/schema in a collector incident module and tests with representative exact/approximate dates, null source fields, missing category, repeats, all observed resolution-status values, malformed rows, and duplicate IDs.

### Honest historical mapping

Create an idempotent importer (collector module plus `collector/scripts/import-historical-incidents.ts`) with this contract:

- One Kanboard task per source row, keyed by the immutable source ID/reference (`C001`, etc.) and metadata source key. Re-running imports creates zero duplicates and repairs only missing importer-owned fields on the same task.
- Preserve the complete short text, verbatim text, category/null, date precision, source document/section, resolution status, repeat link, source digest, and import schema in `overdeck.history_*` metadata. If Kanboard’s 160-character title requires truncation, preserve the full short text in the description/source metadata.
- Map categories to taxonomy IDs using `taxonomy.json`, including its explicit `tool-reliability-fatigue` split IDs. Import the `ci-ritual-fatigue` and category-less rows too; retain a null incident type plus an explicit unmapped reason instead of skipping them or inventing an `other` type.
- Enumerate every actual `resolution_status` in the recovered source and encode an exhaustive lookup: statuses explicitly meaning resolved go to Resolved and are closed; open/directive-issued statuses remain active in Filed. An unknown status aborts import. Do not infer resolution from age.
- Historical records have no authoritative CLI/model/account/permission selection. Store them under a separate `incident/history-v1` metadata schema with `dispatchable=0`; render dispatch fields as not applicable and disable dispatch with a named explanation. Never assign the current account/model retroactively.
- Kanboard still needs a valid native priority/swimlane. Use one documented import placement default, record that it is an importer default, and expose source priority as unknown/null in the domain so the UI never presents the placement default as historical fact. Add `occurredAt`/date-precision fields; do not rewrite Kanboard’s task creation timestamp to masquerade as occurrence time.
- On an existing conflicting task/reference or changed source digest, stop with a named conflict rather than overwrite human changes. Batch/chunk task and metadata calls so the complete corpus does not create an unbounded JSON-RPC request.

Wire the importer into `packaging/deploy-local.sh` after the new code/assets are installed and while Kanboard is ready, before the deploy reports success. Extend `packaging/test-deploy-local.sh`/dry-run expectations. The script must print created/repaired/unchanged/conflicted counts and assert that every snapshot ID exists exactly once.

## 2. Centralize and enforce complete incident metadata

Create a single metadata codec/validator (for example `collector/src/incidents/incident-metadata.ts`) used by filing, reads, dispatch, status updates, resolution, and historical import; do not continue scattering string-key parsing through `incident-service.ts`.

### New web-created incidents

- Keep schema `incident/v1` only if it can be made strict and backward-compatible; otherwise introduce and migrate to an explicit next version. On initial filing write all required identity, source, dispatch-selection, and lifecycle keys together: incident/request/dispatch IDs, request hash, CLI, logical model, authoritative wrapper model, effort, account, account mode, permission mode/unsafe, priority, incident type, source, status revision, started/heartbeat/completed, exit/failure/continuity/result, resolution artifact/summary, and brief/provenance fields. Empty lifecycle values are explicit empty strings until applicable, not omitted keys.
- Require `incidentType` for new UI/API filings and validate it against the freshly loaded taxonomy. The deterministic suggestion may preselect it, but the owner must still have a real valid selection. Historical rows are the only allowed untyped records, with the explicit source reason above.
- Preserve idempotency: canonical request hashing excludes server-derived wrapper/paths but includes every browser-owned field; the same request/body returns the same task/launch, a changed body returns 409, and crash-window repair completes the original task.
- Decode all metadata at reads. Missing/invalid required fields produce named coverage gaps and block dispatch; they must not silently become a healthy Filed record. Extend the public `Incident` type with explicit source/provenance, occurrence, permission mode/unsafe, continuity, resolution evidence, and metadata-schema fields while keeping raw metadata keys, paths, and credentials server-side.
- Complete state writes in `incident-service.ts`: metadata revision first, then column, then deduplicated comment. Resolution must call `closeTask`; repeated same-artifact resolution is a no-op, and a different artifact remains a conflict.

Update `collector/src/incidents/incident-service.ts`, `provider.ts`, Kanboard fixtures/client typing as needed, and all adjacent tests. Add migration/repair coverage for pre-checkpoint tasks that have the old partial `incident/v1` metadata; repair only facts that can be derived authoritatively and surface the rest as incomplete instead of fabricating them.

## 3. Finish executable selector and account authority

Preserve and finish the current `dispatch-options.ts`, manifest, Codex permission-mode, remote-seat forwarding, route, and web cascade work.

- Keep `modules/workstation/claude/incidents/dispatch-capabilities.json` as the incident-specific manifest and `modules/harness/presets/adapters.json` as wrapper/model authority. Add manifest-owned timeout/account-routing fields needed by the launcher. Continue to offer only the proven Codex matrix (`gpt-5.6-sol`, `terra`, `luna`; low/medium/high); do not add xhigh/max or another CLI opportunistically.
- Make the account reader use the same runtime directory variable and parser as `modules/systray/runtime_paths.py`/`AccountRegistry` (the WIP’s `OVERDECK_SYSTRAY_RUNTIME_DIR` must not drift from the systray’s actual `SYSTRAY_RUNTIME_DIR`). Reuse aliases and deterministic registry order, reject missing/malformed/duplicate/empty registries, and never derive accounts from old incidents.
- Ensure installation converges an existing authenticated Codex account into the canonical AccountRegistry when the registry is empty, using AccountRegistry’s credential/identity logic and an idempotent stable slug; do not parse or expose auth secrets in collector code. If no authenticated identity can be adopted, fail the options/deploy canary rather than return a dead selector.
- Prove selected-account routing, not just labeling. In incident mode the Codex wrapper must resolve the selected registered slug to the corresponding account home/credential and run with that identity; do not pass a slug as an unrelated Codex config profile and claim it controls billing. Carry the account and permission mode exactly once through `remote-seat.sh`, `remote-seat.mjs`, `seat-entrypoint.sh`, and `seat-run.sh`, with fail-closed duplicate/missing/unknown parsing.
- Keep safe/unsafe behavior: safe omits `--dangerously-bypass-approvals-and-sandbox`, unsafe includes it exactly once, and missing/duplicate/unknown permission mode exits 2 before account/spend/remote/engine work. Health/model probes remain non-dispatch probes.
- Extend `LoadedIncidentOptions` so the server retains wrapper path, wrapper model, account mode/resolution, permission mode, and timeout internally while the HTTP response exposes only safe selector data. Freshly reload and validate before any Kanboard write and immediately before launch. Named source failures stay 503; tampered/stale selections write nothing.
- Add tests using distinct fake account homes/identities and fake engines to prove the selected account is the one invoked locally and remotely. No test may call a real model.

Relevant files include `collector/src/paths.ts`, `routing-config.ts`, `incidents/dispatch-options.ts` and tests; `modules/systray/account_registry.py` and tests if adoption is needed; the existing harness wrapper/seat files and contract tests; and the capability manifest.

## 4. Implement the one-attempt launcher and runner

Add injected, testable collector seams (for example `incident-launcher.ts`, `incident-runner.ts`, and `incident-result.ts`) and wire them through `collector/src/index.ts`, `provider.ts`, `incident-service.ts`, and `server.ts`.

### File-and-dispatch orchestration

`POST /incidents` must perform the fixed sequence from the parent spec:

1. Strictly validate the 64 KiB-bounded body, required taxonomy type, and fresh dispatch selection before any write.
2. Idempotently create/repair the Filed task with complete metadata.
3. Provision a short incident-specific detached worktree below a configured incident workspace root, from the landed/deployed repository, without touching the shared main checkout. Serialize worktree creation and retain a needs-attention workspace; remove a resolved workspace only after its artifact/work is durable.
4. Assemble and persist the exact dispatch brief and provenance. Revalidate options, save `starting` revision 1, and move to Dispatching.
5. Admit exactly one `systemd-run --user` transient unit named `overdeck-incident-<safe-id>.service`, with `Restart=no`, a hard runtime deadline plus bounded teardown, one foreground Bun runner, and no secret in argv. Wait for exec admission and persist enough non-secret launch receipt data for an idempotent retry to return the original receipt without starting a second unit.
6. Return `201 { incident, launch: { state: "starting"|"running", dispatchId } }` only after admission. Admission/provision failures update the same task to Needs attention and return a named non-2xx response. No poll, page load, collector restart, importer, or deploy may relaunch it.

Keep the explicit `/incidents/:id/dispatch` route only as the repair path for a valid Filed web incident (including the existing “dispatch without brief” owner override). It must use the same launcher and one-attempt/idempotency rules; historical records are never eligible.

### Runner behavior

- The runner reloads the persisted record and fresh capability, invokes the registered wrapper with argv (never shell text): workspace, persisted brief, task slug, authoritative wrapper model, timeout, selected account only in the registered routing form, and permission mode.
- Write Running on wrapper admission, heartbeat every 15 seconds, and classify the terminal result exactly: valid `incident.result` + exit 0 resolves; needs-attention result stays open; exit 75 rate-limited; 124/deadline timed-out; wrapper precondition engine-down; malformed/missing result invalid-result; signal/interruption interrupted. Parse the final agent message strictly from the captured wrapper event stream with bounded output; trailing prose or an earlier result cannot forge success.
- Status revisions are monotonic/idempotent. For each revision save metadata first, move the column second, then create one comment whose first line is `overdeck:<dispatch-id>:<revision>` after dedupe scanning. Resolved closes the task. Bounded retries repeat only the same revision; exhaustion exits nonzero and leaves last confirmed truth.
- Do not infer success from exit 0, a dead unit, or column position. Stale heartbeat remains Needs attention coverage only and never triggers a relaunch.
- Build deterministic fake-wrapper/systemd/worktree tests for every lifecycle branch, duplicate POST/admission, status partial failure, stale authority, timeout, signal, result parsing, no automatic relaunch, argv secrecy, and fixed/profile account handling. Add a provider-free transient-unit canary for installed verification.

## 5. Make the filing UI match the API and specification

Continue to obey the UI skill: compose `DataTable` from ui-primitives and existing deck-ui exports only; add no primitive and no raw token values.

- Refactor `FileIncidentForm.tsx` out of the checkpoint’s compressed WIP form into clear state/derivation code. Its only dispatch authority is `useIncidentOptions()`; delete all incident-history option helpers and obsolete props.
- Present the specified `File incident` action and filing `DetailDrawer`, with fields in order: Title, Description, CLI, Model, Reasoning effort, Account, Type, Unsafe, Priority. Use existing `TextField`, `TextArea`, `Select`, `Checkbox`, `Button`, `SectionCard`, `DataCoveragePanel`, and `DetailDrawer`.
- Loading disables all dispatch controls and submit with honest loading placeholders. Failure keeps title/description/type/priority, shows a retry action, and never invents options. CLI changes atomically clear model/effort/account; model changes clear effort; refreshed options clear stale values. Required selectors never offer `None`.
- Unsafe starts unchecked every time. The unchecked form is submit-capable only when safe is registered; checking is possible only when unsafe is registered and includes visible explanatory copy. Priority defaults P2. Type is required; preserve the deterministic suggestion but permit choosing another valid type.
- Validate all fields, focus the first invalid control, link field errors with `aria-describedby`, and use an `aria-live` submit error. The primary action reads `File and dispatch`, changes to `Filing…`, and stays disabled until a complete registered combination and valid narrative are present.
- On success consume the authoritative `{incident, launch}` response, close the filing drawer, select the returned incident, and invalidate list/detail queries. Never fabricate Running. On failure retain every input and map named invalid/stale/assets/accounts/store/admission errors to useful copy.
- Update `incident-types.ts`, collector client/query/proxy contracts, `IncidentsContent.tsx`, and `IncidentDetailDrawer.tsx`. Detail must display schema/source, occurrence precision, priority provenance, type, CLI/model/effort/account, permission mode, continuity, lifecycle times, resolution artifact/summary, and historical source links through `KvPanel`/`SectionCard`; absent facts remain em dashes or named coverage, never fake values. Hide/disable Dispatch for imported history with its reason.
- Extend web tests for empty incident history with populated selectors, full cascade/reset, options retry preserving narrative, keyboard/a11y errors, required type, safe/unsafe, File-and-dispatch request/response, authoritative selected state, historical rows/details, and no duplicate manual dispatch.

## 6. Verification, install, landing, and deploy

Judge every command by exit status and require clean output where the project gate requires it. Tests must use fake engines/accounts; do not spend provider quota.

### Worktree verification after the first main merge

Run at minimum:

```bash
cd collector && bun test
bash collector/test-kanboard-live.sh
pnpm --filter @overdeck/deck-ui test
pnpm --filter @overdeck/deck-ui typecheck
pnpm --filter web test
pnpm --filter web typecheck
pnpm --filter web build
bash modules/harness/test/codex-permission-mode.test.sh
bash modules/harness/test/codex-sidecar-contract.test.sh
bash modules/harness/seat/test/seat-contract.test.sh
node modules/harness/presets/_validate.mjs
python3 -m pytest modules/systray/tests/ -q
bash modules/workstation/claude/tests/od-incidents.test.sh
bash packaging/test-deploy-local.sh
```

Also run the repository slopgate/gate checks for touched UI and `git diff --check`. Because this plan does not change `modules/harness/factory/**`, the factory suite is not automatically required; if reconciliation or implementation touches factory phase/retry/gate/permission/commit code, run `python3 -m pytest modules/harness/factory/tests/ -q` as mandated.

### Runtime-first requirement and final deploy

1. After all worktree tests pass, install the changed harness/workstation pieces into the paths that actually execute (including the current harness engine/remote-seat bundle and incident assets) using the repository’s supported release/sync path, then run the installed fake-engine permission/account/remote probes. Do not report source-tree tests as a live harness fix.
2. Fetch/merge current `origin/main` again and rerun all affected suites. Land only through the project-authorized shipping flow; do not hand-run a merge/push cascade.
3. Only after the tested code is landed, run `bash packaging/deploy-local.sh`. Its new canaries/importer must fail the deploy on broken assets, unavailable account authority, incomplete history, duplicate imports, or runner admission failure.
4. Verify the installed system through real entrypoints:
   - deployed history snapshot/manifest and wrapper bytes match the tested source;
   - authenticated `GET /incidents/options` returns Codex, the three ordered models/efforts, and at least one actually routable registered account, with no internal wrapper mapping;
   - the importer reports the source manifest count, every source ID exists exactly once, a second import is all unchanged, and imported metadata has no unexplained coverage gaps;
   - installed wrapper safe/unsafe/account and provider-free runner canaries pass without a real model;
   - `od-incidents list/show/search` sees imported history and complete metadata;
   - live `/incidents` via `~/.claude/bin/e2e-remote` (use `domcontentloaded`, never `networkidle`) shows historical records, populated cascading selectors, preserves input across retry, and renders both themes; use the deterministic test runner for file-and-dispatch acceptance rather than consuming Codex quota;
   - collector/web/Kanboard restart preserves the corpus and does not start another incident unit.
5. Record exact test commands, import counts/digest, deployed SHA, live endpoint assertions, and anything not exercised. Deployment is not complete if the laptop still serves old collector, assets, wrapper, or web bytes.
