type: meta

slug: harness-observability

base_branch: origin/main

gate0_mode: strict

preset: codex

land_mode: merge-to-main


type: goal

text: Make a v2 harness run explain itself while it is still running: which task failed, which failure class, the actual cause, what the agent did, and the log tail — streamed to the terminal and shown in /plans, never buffered to the end and never lost to a crash.


type: methodology

text: Producer-side only. The 2026-07-22-overdeck-observability-b3 consumer half is ALREADY on origin/main (all 14 files verified present) — it was written against the dead v1 producer. NEVER re-author collector/src/adapters/harness.ts beyond APPENDING the new upstream calls task t3 needs, nor collector/src/server.ts routes that already exist, apps/web collector-client, useSseStream, AgentStatusBar, AgentFeed, DistanceToDone, or RunCommandBar. Make the live v2 engine emit what the landed consumer already reads.


type: intent

text: Observability is the user's #1 standing requirement, asked across three harness versions and repeatedly lost to compaction. The bar is SSSF's: a failing run says why, live. Wave 1 alone answers all four of the user's questions ('why it failed? what failed? what did the agent do? anything?') — it must ship first and must ship whole.


type: direction

verbatim: i have no observability . why it failed? what failed? what did the agent do? anything? nothing. i want observability like in sssf.


verbatim: i want to /brainstorm the observability so we get it right once and for all. first, check the existing plans. i am PRETTY SURE we had the existing plan. To it, i want to add all SSSFs observability in addition


verbatim: needs full observability about what the resolver decided and why


note: Engine code runs from the bundle at ~/.harness/engine/versions/$(cat ~/.harness/engine/CURRENT). Landing to main does NOT deploy it. A bundle bump via bin/harness-release.sh is post-run main-thread work, NEVER a plan task. Any acceptance command shelling out to gates.sh or runplan MUST export HARNESS_ENGINE_DEV=1 scoped to that subprocess.


note: spec/journal-events.schema.json is fail-closed: journal.js:289 throws 'undeclared journal event kind'. Every new kind ships with its schema entry in the SAME commit. Never relax the validator.


note: v2/supervision.js projectHeartbeat and projectRestartHistory read four kinds nothing emits and the schema does not declare. Resolved at plan time: task t8 DELETES both; the process table is fed by attempt.heartbeat instead.


type: deferred

text: SSSF visualizer parity — waterfall lanes, per-agent context-window bars, compiled-prompt viewer (PhaseDetail.vue is 1265 lines of the 4621-line app)

reason: Follow-on plan against the same producer surface; waves 1-3 make the producer complete enough to build it with no further engine work. Not what unblocks a failing run.


type: anchor

what: the landed consumer half — read to confirm what NOT to rebuild; its v1 producer anchor is void


what: source of the A7/A8/A9/A10 gap IDs still rendering as placeholder strings in /plans; its src/*.js file targets are v1 and void


type: task

seat: coder

tier: medium

desc: Read docs/plans/2026-08-07-harness-observability.md Task 1a (The display policy module) for the full contract. Create modules/harness/v2/display-policy.js: an exhaustive kind-to-DisplayDecision table over spec/journal-events.schema.json, the verbatim three-line failure block formatter, and a pure edge-triggered heartbeat reducer. Pure addition — do NOT touch run.js or bin/runplan.js, Task 1b and 1c own them. Acceptance: HARNESS_ENGINE_DEV=1 bash modules/harness/test/display-policy.sh Commit only: modules/harness/v2/display-policy.js modules/harness/test/display-policy.sh

acceptance: Every event kind declared in modules/harness/spec/journal-events.schema.json resolves to a display decision or the documented one-line info default; no kind can be silent.

acceptance: formatFailure renders exactly the three lines: task <id> blocked: <failureClass> (<cause>) / log: <logPath> / | <logTail>, and renders the lines that exist when a field is missing rather than suppressing the block.

acceptance: heartbeatEdge returns print:true exactly once across ten consecutive attempt.heartbeat events past the stall threshold, and once more on the first heartbeat after activity resumes.

acceptance: The module never imports journal.js and never writes anything — it decides display only.


tier: high

desc: Read docs/plans/2026-08-07-harness-observability.md Task 1b (One emitter in run.js) for the full contract. Collapse run.js report()/journal() into a single emit(event) that records to the journal AND offers the event to the progress sink. Delete journal() as a separate exported path. Fix the :654 identity spread so caller fields win. Make the :669 quarantine event carry failureClass, cause, logPath and logTail on the event itself. Acceptance: HARNESS_ENGINE_DEV=1 bash modules/harness/test/emit-single-path.sh Commit only: modules/harness/v2/run.js modules/harness/test/emit-single-path.sh

acceptance: No journal write exists in run.js outside emit(); journal() is deleted, not deprecated.

acceptance: A fixture run offers exactly one progress event per journaled record — recording is never filtered.

acceptance: A synthetic quarantine event carries failureClass, cause, logPath and logTail, and retains phase:"quality" instead of having it overwritten by the identity spread.


desc: Read docs/plans/2026-08-07-harness-observability.md Task 1c (The CLI prints failures at block time) for the full contract. Make bin/runplan.js reportProgress the sole consumer of displayPolicy: every offered event routes through it and a non-null decision prints immediately, never buffered to run end. The three-line failure block prints at block time for every quarantined task. Hold the heartbeat-edge state and pass it through heartbeatEdge. Keep the end-of-run summary as a recap. Acceptance: HARNESS_ENGINE_DEV=1 bash modules/harness/test/runplan-live-failure.sh Commit only: modules/harness/v2/bin/runplan.js modules/harness/test/runplan-live-failure.sh

acceptance: A fixture feeding a quarantine event mid-stream produces the three-line failure block on stdout BEFORE the stream ends.

acceptance: Ten consecutive heartbeats past the stall threshold produce exactly one printed line.

acceptance: Every non-null display decision prints exactly once; the end-of-run summary is a recap of lines already printed, not the first appearance.

acceptance: A quarantine carrying no logPath still prints the lines that exist.


desc: Read docs/plans/2026-08-07-harness-observability.md Task 2 (taskId/attemptId correlation) for the full contract. Stamp taskId on every task-scoped journal event and attemptId on every attempt.* event, keeping task as a deprecated alias, with schema entries in the same commit.

acceptance: A fixture run journals taskId on every verify.passed|failed|retry, reply.unmarked, budget.exhausted and attempt.* record.

acceptance: Every attempt.* record additionally carries attemptId.

acceptance: A legacy fixture carrying only `task` still projects, via the taskId ?? task fallback.


desc: Read docs/plans/2026-08-07-harness-observability.md Task 3 (Attempt routes reachable from the browser) for the full contract. Add read-only collector routes and Astro allowlist regexes for exactly /runs/:id/attempts, /attempts/:id/prompt and /attempts/:id/reply, leaking no harness token and preserving upstream status.

acceptance: Each of the three attempt paths returns the stubbed upstream body and status through the collector.

acceptance: An upstream 404 surfaces as 404, never as an empty-array 200.

acceptance: A path outside those three is denied by the Astro allowlist default-deny.

acceptance: No response body or header contains the harness token.

acceptance: The harness adapter gains only appended calls; no existing adapter function is rewritten.

verify: cd collector && bun test test/attempts-routes.test.ts

verify: pnpm --filter web test -- collector-proxy


desc: Read docs/plans/2026-08-07-harness-observability.md Task 4 (Agent transcripts are reachable) for the full contract. Introduce v2/log-phases.js as the single LOG_PHASES source, derive the control-api route regex from it, and serve any declared phase so the coder transcript is retrievable.

acceptance: For a fixture run with a coder log on disk, /tasks/:id/transcript returns it.

acceptance: ?phase=review selects the review log; the default is the most recent log for that task.

acceptance: ?phase=bogus returns 404 whose body lists LOG_PHASES, never a silent fallback to another phase.

acceptance: A path-traversal attempt in :task or ?phase is rejected.

acceptance: The route regex is derived from LOG_PHASES — adding a phase requires editing one file only.


desc: Read docs/plans/2026-08-07-harness-observability.md Task 5 (Decision records + per-attempt duration) for the full contract. Emit a decision record for every escalation rung carrying seat binding, why it differs from the failed seat, inputs, verdict and action, and stamp durationMs on attempt-terminal events.

acceptance: A canary run driven into the fix ladder journals one decision record per rung with id, taskId, rung, seat, differsFrom, why, inputs, verdict and action all populated.

acceptance: differsFrom is set whenever the rung selected a binding different from the failed seat, and the record explains why.

acceptance: A rung that decides NOT to escalate still emits a decision record.

acceptance: Every attempt-terminal event carries a positive integer durationMs.


desc: Read docs/plans/2026-08-07-harness-observability.md Task 6 (Control-api projections stop lying) for the full contract. Add agentId, absolute startedAt, journalSeq, durationMs and note to timeline segments, project attemptId, compute CAPABILITIES per run, and fill the decisions and ratelimits routes; missing data projects null, never zero.

acceptance: Against an enriched fixture journal every timeline segment carries agentId, an absolute startedAt inside the run window, journalSeq, durationMs, and note on the failed segment; the relative t0 is retained alongside startedAt.

acceptance: eventsOf projects the stamped attemptId instead of a hardcoded null.

acceptance: CAPABILITIES is computed from what the run's journal actually contains.

acceptance: The decisions and ratelimits routes return real records for the enriched fixture and no synthesized quota numbers.

acceptance: Against a legacy fixture the same routes return null fields and empty decisions without throwing.


desc: Read docs/plans/2026-08-07-harness-observability.md Task 7 (Gate evidence + verbatim failure tails) for the full contract. Make gate0 return GateCheck/GateReport evidence instead of a boolean and attach a verbatim tail-biased 4000-char outputTail to gate, verify and dispatch failures.

acceptance: A passing fixture emits gate_pass carrying a non-empty checks array — a green gate says what it verified.

acceptance: A failing fixture emits gate_fail with the offending check ok:false, a populated violations list, and an outputTail whose last line matches the real command output.

acceptance: An output longer than 4000 chars truncates to its LAST 4000 chars and marks the truncation; the tail is never regex-summarized.

acceptance: The no-check-commands infra path still fails closed and now also emits the checks it looked for.


desc: Read docs/plans/2026-08-07-harness-observability.md Task 8 (Process table + usage projections) for the full contract. Project ProcessRecord from attempt.heartbeat and UsageBreakdown from attempt.usage, render silence past the threshold as stalled, and delete the unfed supervision.js projections.

acceptance: A fixture with heartbeats projects one ProcessRecord per attempt carrying the real pid.

acceptance: An attempt whose last heartbeat is older than the stall threshold projects stalled:true with lastActivity — never as running.

acceptance: Usage totals equal the sum of the fixture's attempt.usage records and merge attempt to task to run.

acceptance: A fixture with no usage records projects null, never zeros.

acceptance: grep -c projectHeartbeat modules/harness/v2/supervision.js returns 0.


desc: Read docs/plans/2026-08-07-harness-observability.md Task 9 (One-call run settlement) for the full contract. Derive run status, banner and exit code from one journal projection with failure-first defaults so a run that dies mid-flight settles failed.

acceptance: An all-green fixture settles success with exit code 0.

acceptance: A fixture truncated mid-run settles failed with a non-zero exit code — success is asserted from evidence, never assumed from the absence of an error.

acceptance: A fixture with one quarantined task settles partial with a non-zero exit code and a banner naming that task.

acceptance: Status, banner and exit code come from a single settleRun projection; no second site computes an exit code.


desc: Read docs/plans/2026-08-07-harness-observability.md Task 10 (UI defects) for the full contract. Key fixLoopShare on fix.rung, branch the decisions block on length, gate the run-control string on computed capabilities, drop the stale A8/A10 gap rows, feed IdleTimer from lastActivity, and render absolute startedAt in SegmentDetailDrawer. Compose existing deck-ui exports only; never invent a primitive.

acceptance: SegmentDetailDrawer renders the fixture's real start date from startedAt, never 1/1/1970, and renders the honest-gap label when startedAt is null.

acceptance: The decisions block and its gap notice branch on array length, not truthiness.

acceptance: The run-control unsupported string is conditional on the computed capabilities.control.

acceptance: The hardcoded A8/A10 coverage-gap rows are gone and IdleTimer is fed from the process record's lastActivity.

acceptance: No new deck-ui primitive is introduced; tokens only, both themes.

acceptance: All three typecheck/test commands pass clean.

verify: pnpm --filter @overdeck/deck-ui typecheck


desc: Read docs/plans/2026-08-07-harness-observability.md Task 11 (The decisive integration test) for the full contract. Drive an offline canary run engineered to quarantine and assert the failure class, cause and log tail appear in stdout WHILE the run is still going, the transcript is retrievable live, and the journal keeps everything the terminal printed. No fork/OOM/pid-exhaustion/disk-fill/stress behaviour of any kind.

acceptance: Stdout captured mid-run — before the process exits — contains 'task <id> blocked: <failureClass> (<cause>)', its log: line, and a non-empty tail line.

acceptance: The quarantined task's coder transcript is retrievable from the control-api while the run is still live.

acceptance: The journal contains every record the terminal printed and more; display filtering removed no record.

acceptance: The run settles with a non-zero exit code.

acceptance: HARNESS_ENGINE_DEV=1 is exported scoped to the subprocess only, and no existing canary-stub.sh arm is modified.