audience: AI coding agents first.

# Incident dispatch selector options — request

**Goal:** Make `/incidents` Model and Account selectors populate from authoritative deployed capabilities on an empty incident store; accept only a valid codex CLI/model/effort/account/permission combination.

**Context:** `apps/web/src/components/incidents/incident-view.ts` currently derives Model and Account options from prior incidents. `GET /incidents/options` in `collector/src/server.ts` returns taxonomy only. `FileIncidentRequestSchema` requires all dispatch strings, so the visible `None` values produce an invalid request. Canonical design: `docs/specs/2026-08-09-incident-dispatch-selector-options-design.md`; parent dispatch contract: `docs/specs/2026-08-08-incidents-page-design.md` §6.

Before modifying `apps/web/src/**`, read `.claude/skills/od-ui-dev/SKILL.md` and obey it. Reuse existing `@overdeck/deck-ui` `Select`; NEVER add a primitive.

## Files

- Create `modules/workstation/claude/incidents/dispatch-capabilities.json` — versioned codex-only incident capability manifest.
- Create `collector/src/incidents/dispatch-options.ts` — schemas, deployed-source join, serialization, and request validation.
- Create `collector/src/incidents/dispatch-options.test.ts` — source, join, serialization, and trust-boundary tests.
- Create `modules/harness/test/codex-permission-mode.test.sh` — fail-closed wrapper argv contract tests using a fake engine; NEVER launch real Codex.
- Modify `modules/harness/wrappers/codex.sh` — require and honor `--permission-mode safe|unsafe` for dispatch mode.
- Modify `modules/harness/wrappers/lib/remote-seat.sh`, `modules/harness/seat/remote-seat.mjs`, and `modules/harness/seat/seat-run.sh` — carry permission mode unchanged across remote-seat dispatch.
- Modify `modules/harness/seat/test/seat-contract.test.sh` — prove remote permission-mode forwarding and fail-closed parsing.
- Modify `collector/src/paths.ts` — deployed adapter-registry and dispatch-capability paths rooted at `deployDir()`.
- Modify `collector/src/routing-config.ts` — expose one account-registry reader for reuse; keep registry parsing single-source.
- Modify `collector/src/server.ts` — inject option authority, extend options response, validate POST before mutation, map named failures.
- Modify `collector/src/incidents/incident-service.ts` — accept dispatch-option authority dependency and revalidate persisted selection before changing dispatch state.
- Modify `collector/src/incidents/routes.test.ts` and `collector/src/incidents/incident-service.test.ts` — route, mutation-order, and stale-dispatch coverage.
- Modify `apps/web/src/lib/incident-types.ts` — complete response contract.
- Modify `apps/web/src/components/incidents/FileIncidentForm.tsx` — server-driven cascade and honest states.
- Modify `apps/web/src/components/incidents/IncidentsContent.tsx` — remove obsolete incident-option prop wiring.
- Modify `apps/web/src/components/incidents/incident-view.ts` — delete incident-derived dispatch option exports.
- Modify `apps/web/src/components/incidents/incident-view.test.ts` and `apps/web/src/components/incidents/IncidentsContent.test.tsx` — remove seeded-history workaround; cover cascade and unavailable states.

Use existing adjacent test files instead of creating a listed test file when repository convention clearly places that behavior there. Do not create parallel helpers or duplicate fixtures.

## Contracts

### Capability manifest

```ts
type IncidentDispatchCapabilityManifest = {
  version: "incident-dispatch-capabilities/v1";
  capabilities: Array<{
    cli: string;
    label: string;
    adapter: string;
    wrapperContract: "incident-wrapper/v1";
    models: Array<{
      id: string;
      efforts: Array<{ id: string; wrapperModel: string }>;
    }>;
    accountMode:
      | { kind: "profile"; provider: "codex" | "claude" }
      | { kind: "fixed"; account: string; label: string };
    permissionModes: Array<"safe" | "unsafe">;
  }>;
};
```

Manifest v1 MUST contain only:

- CLI `codex`, adapter `codex`, contract `incident-wrapper/v1`;
- logical models in this order: `gpt-5.6-sol`, `gpt-5.6-terra`, `gpt-5.6-luna`;
- efforts for every model in this order: `low`, `medium`, `high`;
- nine explicit `wrapperModel` references matching existing `modules/harness/presets/adapters.json` fused ids verbatim;
- account mode `{ "kind": "profile", "provider": "codex" }`;
- permission modes `safe`, `unsafe`.

NEVER generate wrapper ids by splitting or concatenating model strings. `xhigh`, `max`, Claude, Fable, and Cursor MUST remain absent.

### Collector seams

```text
loadIncidentOptions(): LoadedIncidentOptions
serializeIncidentOptions(options): IncidentOptionsResponse
validateIncidentDispatchSelection(request, options): ValidatedIncidentDispatchSelection
```

`LoadedIncidentOptions` retains explicit `wrapperModel` mappings and source provenance internally. HTTP serialization strips wrapper mappings.

```ts
type IncidentOptionsResponse = {
  types: IncidentTypeOption[];
  clis: Array<{
    id: string;
    label: string;
    models: Array<{ id: string; efforts: string[] }>;
    accounts: Array<{
      slug: string;
      label: string;
      ready: boolean;
      fixed: boolean;
    }>;
    permissionModes: Array<"safe" | "unsafe">;
  }>;
};
```

Preserve manifest model/effort order. Provider registry account order remains deterministic. V1 sets `ready:true` for every account present in the codex registry and `fixed:false`. Registration is the only current readiness authority; do not invent a health source.

Loader MUST fail closed on unknown version/shape, duplicate scoped identifiers, absent adapter, any wrapper model absent from bound adapter `models`, wrapper mismatch, empty joined models/efforts/accounts/permissions, empty fixed account, or unreadable/malformed provider registry.

`GET /incidents/options`:

- `200` only for complete taxonomy plus validated dispatch options;
- `503` body code `incident-assets-unavailable` for missing/invalid taxonomy, adapter registry, or capability manifest;
- `503` body code `incident-accounts-unavailable` for missing/invalid required provider registry;
- NEVER partial `200`.

Every POST `/incidents` MUST freshly load taxonomy, adapters, capabilities, and account registry. Interpret request `unsafe:false` as permission mode `safe` and `unsafe:true` as `unsafe`. It MUST reject unknown CLI/model/effort/account, unready account, or unsupported permission with `400` code `invalid-incident-dispatch` before Kanboard mutation or idempotency record creation. Resolve and persist `overdeck.wrapper_model` server-side from the registered triple. Accept no client-supplied wrapper-model mapping.

Immediately before `dispatchIncident` changes metadata/column state, freshly reload and validate persisted dispatch metadata. Refuse stale or removed selections with named code `incident-dispatch-options-stale`; leave incident filed and recoverable.

### Codex wrapper

Dispatch-mode signature adds required:

```text
--permission-mode <safe|unsafe>
```

- `safe`: Codex execution argv MUST omit `--dangerously-bypass-approvals-and-sandbox`.
- `unsafe`: argv MUST include that flag exactly once.
- missing, empty, duplicate, or unknown permission mode: exit `2` before spend-cap checks, account selection, remote dispatch, or engine launch.
- `--health` and `--list-models` remain non-dispatch probes and do not require permission mode.
- forward permission mode through `seat_remote_dispatch`, `remote-seat.mjs`, and `seat-run.sh` exactly once so remote execution preserves the selected contract; each parser rejects missing values and unknown flags with exit `2`.

Tests MUST inject a fake engine/remote seam and inspect argv. NEVER invoke a real model or consume quota.

### Web form

`FileIncidentForm` MUST use `useIncidentOptions()` as sole dispatch-option authority. Remove `incidents` as an option source and remove unused prop wiring.

State contract:

- loading: disable CLI, Model, Reasoning effort, Account, Unsafe, and submit; placeholders `Loading options…`;
- options error: disable same controls and submit; visible retry action; preserve title/description;
- no CLI: enable CLI only; dependent placeholders `Select CLI first`;
- selected CLI: show only its models/accounts; Unsafe control available only when declared;
- selected model: show only its efforts;
- upstream change: atomically clear incompatible model, effort, and account;
- complete registered dispatch selection plus valid title/description: filing enabled;
- required dispatch fields use placeholders, NEVER `None` as an option.

Delete orphaned `KNOWN_CLIS`, `REASONING_EFFORTS`, `cliFilingOptions`, `modelFilingOptions`, `reasoningEffortFilingOptions`, and `accountFilingOptions`.

## Error behavior

- Preserve narrative form input across options retry and filing failure.
- Empty capability result is invalid source, not a successful empty selector response.
- Registry changes after GET are caught by fresh POST validation without mutation.
- Registry/manifest changes after filing are caught by fresh dispatch validation without state transition.
- Server logs source path and parse reason; client receives safe named error detail only.

## Out of scope

- Claude, Fable, Cursor, `xhigh`, or `max` dispatch capabilities.
- Account health/readiness redesign.
- Incident table and taxonomy-suggestion behavior.
- New UI primitives or unrelated incident refactors.
- Factory phase, retry, gate, permission, or commit machinery under `modules/harness/factory/**`.

## Resumed browser verification — 2026-08-11

status: DONE
task IDs: #2
source request: Owner ordered immediate resumption after `e2e-remote` restoration. Run live `/incidents` selector acceptance now; do not stop at generic browser canary.

### Acceptance delta

- Use installed `~/.claude/bin/e2e-remote`; NEVER launch browser locally.
- Verify live `/incidents` loads without seeded incident dependence.
- Verify CLI, Model, Reasoning effort, Account, and Unsafe controls expose authoritative options and dependency clearing.
- Capture browser console/page errors; visually inspect a form-only screenshot, then delete it. NEVER retain broad live-page data.
- Preserve existing implementation work; no duplicate branch or feature rewrite.

### Preserved WIP

- Implementation already landed in main history from `wt/incident-selector-clean`; old worktree clean and 236 commits behind current main.
- Runtime fix landed at `f1028e745`; installed deploy-clone `e2e-remote` browser canary passed.

### Current receipt

- Task #2 completed by `main`.
- Existing authoritative request selected by its explicit live `/incidents` browser acceptance at line 197.
- Installed `e2e-remote` provisions and launches Playwright only on reachable buildboxes; current Chromium revision was installed and guarded on the selected host. No local browser launched.
- Collision-free temporary tunnel reaches the live workstation web/collector pair from remote browser localhost. Earlier temporary use of port 31339 collided with Kanboard; proxy stopped immediately, Kanboard restarted, and tunnel moved to 31439/31440.
- Live UI hydrates and exposes codex, `gpt-5.6-sol|terra|luna`, ordered `low|medium|high`, registered ready accounts, and Unsafe. Model change clears reasoning effort. CLI reset is not browser-actionable because v1 intentionally exposes one CLI and the disabled placeholder cannot be selected; component tests own that internal transition.
- Security review correctly rejected broad live-page capture. Probe now logs status/path only, captures no body or query strings, and screenshots only the blank filing form. Prior broad screenshots were deleted.
- Acceptance complete for selector scope: remote Playwright selected codex, verified all three logical models, exact ordered efforts, at least one ready account, Unsafe availability, and model-change effort clearing. The rendered blank form was visually inspected. V1 exposes only codex, so browser-level CLI-change clearing is not actionable; existing component coverage owns that state transition.
- No local browser launched. Temporary live tunnel stopped; broad and form screenshots, probe scripts, and temporary sync directory removed after inspection.
- Receipt landed on `main` at `16e07c6da`. Mandatory canonical deploy reused unchanged web release, then failed because sync backed up and removed live `~/.claude/buildbox-hosts.json` before reporting its source unreadable. Exact backup `20260811T164254Z` was immediately restored with mode 0600; installed registry now returns `debian1,debian2,debian3` in e2e order. No product/runtime bytes changed by this docs-only receipt.
- Separate follow-up evidence: `/api/collector/state` measured 81,022,061 bytes; `factory-runs` contributes 62,638,068 bytes. That load correlates with transient incident-query 503/timeouts despite successful retries. This signal is preserved, not accepted as selector behavior.

### Next executable action

Selector work has no remaining action. Future collector-state payload work must remain independent and must not reopen this completed selector implementation.

## Acceptance

Run from worktree; all output MUST be clean with no ignored warning, notice, or hint:

```text
cd collector && bun test
pnpm --filter web test
pnpm --filter web typecheck
pnpm --filter web build
bash modules/harness/test/codex-permission-mode.test.sh
```

Expected:

- collector tests prove options are independent of incident history;
- malformed/missing/duplicate/mismatched sources fail closed;
- tampered POST causes zero Kanboard writes;
- stale dispatch causes zero state writes;
- valid triple persists the explicit registered wrapper model;
- fake wrapper test proves safe/unsafe argv and all fail-closed argument branches;
- web test uses an empty incident response and still shows codex models/accounts;
- CLI/model changes clear incompatible dependent state;
- loading/error/retry states are visible and preserve narrative input;
- typecheck and build pass cleanly.

Install-before-landing is acceptance, not optional cleanup:

```text
bash packaging/deploy-local.sh
```

Expected after deploy:

- deployed `modules/workstation/claude/incidents/dispatch-capabilities.json` and `modules/harness/wrappers/codex.sh` match tested worktree bytes;
- real installed `GET /incidents/options` returns codex with three logical models, ordered low/medium/high efforts, and live registered account slugs;
- installed wrapper contract probes pass without invoking a real model;
- live `/incidents`, with no seeded incident dependency, lets the owner select codex Model and Account; changing upstream selection clears stale values;
- browser verification runs through `~/.claude/bin/e2e-remote`, never a local browser/dev-server pair.

Only after live acceptance, land through project-authorized shipping. Re-run affected checks if upstream movement changes touched seams.
