# SPEC-P2 — Observability: every attempt fully inspectable (LIVE engine)

audience: AI coding agents first.
Target: `modules/harness/v2`, `collector/src/adapters/harness.ts`, `apps/web/src/components/plans/`. DO NOT touch `modules/harness/src` (dead WIP). Reference semantics: sssf `tracer.py` + `references/observability.md` (vendored).

Goal: pick ANY run (live or historical-after-this-lands) in /plans and answer per attempt, without a terminal: who ran, which model/wrapper, what it was told, what it returned, what gates/review said, what it cost, whether it is looping. The babysitting questions ("stuck? looping? burning?") answered by UI.

Verified live state: journal = NDJSON per run at `<repo>/runstate/v2/<slug>/<runId>/events.ndjson` (`v2/journal.js runDir`); registry `~/.harness/history/v2/runs` (`v2/registry.js historyDir`); control API serves `/runs/:id/{events,status,log,timeline,decisions,config,plan}` + `/tasks/:taskId/{prompt,activity,transcript}` — prompt route returns task DESCRIPTION only, no compiled prompt, no per-attempt projection, no `/runs/:id/attempts`. Usage capture wrapper-dependent.

## O1 — Attempt record completeness (journal kinds; declare all in journal-events.schema.json)

Emission point: `v2/dispatch.js runDispatch` (+ `quality.js` for review attempts). Every provider invocation gets `attemptId`; artifacts under `<runDir>/attempts/<attemptId>.{prompt.md,reply.json}` capped 256KB tail-truncated.

- `attempt.prompt` `{ attemptId, task, phase, seat, model, wrapper, timeoutSecs, promptPath, promptSha, causalInput }` — compiled prompt as sent; `causalInput` = hash of what changed vs prior attempt (gate error / review findings / base head) — the "is it redoing the same thing" answer.
- `attempt.reply` `{ attemptId, valid, verdict?, replyPath }` — parsed reply or raw tail on parse failure.
- `attempt.heartbeat` every 60s while a dispatch runs `{ attemptId, pid, elapsedSecs, timeoutSecs, eventCount, lastActivity, usageDelta? }` — live burn/stall evidence; end-only reporting leaves an active loop invisible until it's over.
- `attempt.usage` — ALWAYS per attempt: wrapper usage when present, else `{ unknown: true, model }`. UI renders unknown as "unmetered", never blank, never invented.
- `attempt.activity` end-rollup `{ attemptId, toolCalls, filesTouched, repeatedCommands, failureFingerprint }` — `repeatedCommands` = identical argv ≥3× (loop signature).

## O2 — Control API (`v2/control-api.js`)

- `/runs/:id/attempts` → per-attempt projection joining O1 + retry ledger: `[{ attemptId, task, phase, seat, model, wrapper, timeoutSecs, startedTs, endedTs, rc, failureClass, usage, activity, heartbeatAge, taskDispatchCount, taskDispatchBudget, promptPath, verdict }]`.
- Serve attempt artifacts (path-validated under `<runDir>/attempts/` only), same pattern as existing `/tasks/:taskId/transcript`.

## O3 — /plans UI: attempt drawer + autopsy + burn

- Per task: attempt timeline; click attempt → drawer: prompt (collapsed), reply, verdict/gate evidence, usage (or "unmetered"), activity counters, failureClass.
- Autopsy strip for failed/blocked runs: attempt chain (attempt → failureClass → next causal input) as one visible sequence — "why did this run die".
- Burn panel: cumulative usage by seat/model, attempts + `taskDispatchCount/Budget` per task, repeated-fingerprint counter. Warning ≥3 attempts; alarmed at identical fingerprint ×2 or budget ≥75%. Must answer "did anything loop overnight" in one glance.
- Reusable components ONLY (`plans-ui-componentization-mandate`), design-system gallery registration, both themes, honest data ("not captured" over blank), slopgate green.

## O4 — Live liveness (collector `harness.ts`)

- Per-run `staleness` = now − max(newest journal ts, newest attempt heartbeat ts). Attempt elapsed > 2× `timeoutSecs` or heartbeat gap > 3 intervals → alarmed state on run card + task row.

## Tests

Engine: attempt kinds emitted for every dispatch incl. failed ones (`v2/test/`, suite `node modules/harness/v2/test/index.js`). Collector: `bun run --cwd collector typecheck && bun test` adapter fields. Web: `pnpm --filter web typecheck && pnpm --filter web build && pnpm --filter web test` incl. "not captured" rendering states.

## Exit criteria

- Run recorded after this lands: failing attempt + its compiled prompt + its reply + its cost identified in UI alone. (Older runs: existing kinds only, rendered honestly as "not captured".)
- Overnight loop drill: task forced to repeat-fail → morning burn panel shows loop, fingerprint, spend, budget state without opening logs.
- Stuck attempt visibly alarmed within one collector poll of breaching timeout/heartbeat gap.
- Every new kind declared in `spec/journal-events.schema.json`; no UI field silently defaults.
