feat(qa): keep a failed multi-turn walk in the trace and in the denominator (refs #718) - #785
Conversation
…inator (refs #718) The synthetic walk drove the real consider route once and kept no conversation, so the failure the owner screenshotted -- product answers, user corrects it, product narrates its own repair work instead of re-answering -- left nothing behind to look at. A run that failed strict verification also left no counted attempt. The walk now invokes the real route twice, on the declared motive and then on the synthetic user's correction, and records the three visible turns around it. The engine's own challenge is what proves the correction landed: the second evaluation must quote the user's corrected words verbatim or the walk refuses. Each assistant surface is classified where it is recorded, so a turn that narrates diagnosis is stored as `process_error` rather than as the decision result it arrived in the slot for. Attempt accounting is pessimistic by construction. `campaigns_started` and `route_runs_started` reach disk before the first turn exists, and a started run reads `harness_incomplete` until a terminal verdict replaces it, so a crash mid-trajectory leaves a partial trace with a stop reason rather than no run at all. Exit codes separate the three outcomes: 0 passed, 1 the product surfaces failed, 2 never settled. Raw surface text and machine detail go to separate local artifacts; the combined receipt carries counts, labels and digests only. The committed mutation differs from the baseline in the third surface alone, so what it catches is a delivery failure and nothing else. Limits worth naming: the assistant surfaces are fictional, not a host capture, so `production_answer_capture` stays `unavailable`; the narration detector is English-only because `qa/` is held to English and the gate's allowlist is prefix-keyed; and the classification is a delivery check, not an answer-quality judgment. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Exit code 1 means "the product surfaces failed". An exception outside the named list -- JSONDecodeError from a malformed engine payload is the reachable one, since `_run` parses stdout after a zero return code -- reached the interpreter instead, which also exits 1. A harness fault would have been read as a product verdict. `main` now catches every Exception and reports `harness_error:<Type>` with exit 2. KeyboardInterrupt and SystemExit still propagate. The on-disk report was already flushed on this path, so the change is to what the caller is told, not to what is preserved. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…e (refs #718) Five findings from an independent review of the first commit, each with a scenario that reached a wrong outcome. The CLI's error path built a fresh ledger, so a run that died mid-trajectory was announced on stdout as `route_runs_started: 0` -- the exact disappearance this slice exists to prevent, on the surface most likely to be read. `run_walk` now hands the caller its last *durable* report through a sink, and stdout and disk agree. That sink is set only after both writes land. A verdict whose write failed would otherwise have been announced as recorded, since stopping a run incomplete changes the stop reason and not the counters. A surface script declared its own `terminal_options`, so a fixture could invent a resolution word, put it in its final surface and manufacture a passing decision. The vocabulary now comes from the product's own trade-evaluation schema, and a script may declare a subset of it. `str(exc)` carried raw engine stdout and stderr into the public report. Detail goes to stderr and the diagnostics artifact; the receipt keeps the exception type. Reports and traces are written through a temp file and renamed, so a death mid-write leaves the previous honest state rather than a truncated one. Also: the `--plan` invocation count was a constant that would have stayed green with the second route call removed, and no test drove the CLI error path. Both are now covered. `question` and `limitation` remain in the taxonomy without being derived; the docstring says so and names the cost. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Independent review — 8 findings, 5 real, all closedAn independent review (different model family, judged the diff before reading the PR body) ran against
Also fixed before the review landed, found while re-reading the exit-code contract: an exception outside the named list ( Evidence after the fixesMutation dance, second batch: 5 more mutations for the new guards, 4 caught immediately and 1 survived — setting the report sink before the writes land, which no test could see without a write-failure scenario. Rather than call it covered, a test was added that fails the write of the settled report and asserts the announced state is the last durable one. That mutation is now caught. 17/17 across both batches, restore verified clean each time. Two things worth stating plainly, since they change what the mutation numbers are worth: the first batch's readout was void (the probe grepped stdout case-insensitively for |
Smallest vertical slice of #718's promoted QA-repair front: make one
considerroute run observable enough that a failed multi-turn experience cannot disappear.Head SHA:
41d1aab(branched fromorigin/main@a33f315). Three commits: the slice, then two rounds of fixes — see the independent-review disposition comment for what changed and why.What changed
One route, driven twice, with the turns around it recorded. The walk invokes the real
considerCLI on the scenario's declared motive, then again on the synthetic user's correction. No second state machine, no product renderer, no new route or persona. The engine's own challenge is what proves the correction landed: the second evaluation must carry the user's corrected words inchallenge.quote_verbatimor the walk refuses.An ordered visible-turn trace.
qa/turn_trace.pyrecords each surface with role, #718's message-type taxonomy, route state and timestamp, and chains their digests intoordered_surface_digest. The message type is derived from the surface, never declared by the fixture — a turn that narrates diagnosis is stored asprocess_erroreven though it arrived in the slot where a decision result was due.Accounting that a failure can re-bucket but not erase.
campaigns_startedandroute_runs_startedreach disk before the first turn exists, and a started run readsharness_incompleteuntil a terminal verdict replaces it. A crash after the correction therefore leaves a partial trace, a turn count and a stop reason rather than no run. Exit codes separate the outcomes:0passed,1the product surfaces failed,2never settled.The controlled regression.
consider-ai-momentum-correction-mutated.jsondiffers from the baseline in the third surface alone — a test asserts turns one and two are byte-identical — so what it catches is a delivery failure and nothing else. It is classifiedproduct_failwith all three turns retained.Privacy split. Raw surface text goes to
turn-trace.local.jsonland machine detail toharness-diagnostics.local.json; the combined receipt carries counts, labels and digests only. A test asserts no raw surface reaches the public report, and that the visible route-failure surface carries none of the tool output behind it.Evidence
Mutation proof — 17 mutations across two batches, each restored by
cpwith__pycache__cleared, all 17 caught. The first batch:start_routestops assuming the run is incompleteTwo corrections made during that proof, both recorded because they change how much the numbers are worth. The first readout of this dance was void: the probe grepped stdout case-insensitively for
error, which matches a test name containing it, so all twelve REDs were unearned. Re-run on exit code alone. That run then left one survivor — mixing a position index into the digest chain is redundant, since a streaming hash already carries order — so the redundant field was removed rather than left untested, and the mutation replaced with one that sorts the turns.Limits and rollback boundary
production_answer_capturestaysunavailableandux_receipt_statusstaysincomplete_product_delivery, unchanged from before this PR.semantic_judgestaysskippedwith a stated reason.qa/is held to English bydocs/language-policy.md, and that gate's allowlist is keyed on a line prefix, which for a list of quoted needles would be"— wide enough to retire the gate for the whole module. The walk pins--language enfor the same reason; the lane is blind on the zh locales the product ships.owner_acceptanceremainsowner_unreviewed; nothing here maps a classification onto acceptance.qa/turn_trace.pyandqa/scenarios/are new; reverting this commit restores the previous single-invocation walk with no product-runtime change to undo. No engine, skill, schema or receipt-contract file is touched.Not in scope
No dashboard, no parallel scheduler, no second persona or route, no #735 fan-out.
🤖 Generated with Claude Code