Repository navigation
feat(review): add heartbeat, timeout, cleanup, and structured verdict metadata #137
Description
Feedback type
Feature request from repeated cross-model review runs in BaseInfinity/m180-jumpseat.
Problem
A non-interactive cross-model review can remain completely silent for several minutes and then return only VERDICT: CLEAN. During the #92–#98 delivery sequence and its lifecycle reconciliation, the reviewer process was healthy and eventually completed, but the driver could not distinguish among:
- active reasoning;
- provider/network wait;
- a stalled child process;
- authentication/input wait;
- a lost output stream;
- a completed review whose final output was not propagated.
The only available diagnostic was external process inspection. That is not a reusable consumer workflow, especially on Windows.
This is separate from concise review output (#123) and retained failed-proof summaries (#124). It concerns runner lifecycle and durable review metadata.
Desired behavior
Provide an installed, cross-platform review runner that owns the child process and emits bounded operational state without exposing chain-of-thought or flooding the console.
Minimum state:
- review started, exact base/candidate identity, and reviewer command class;
- periodic liveness heartbeat with elapsed time and last observable child/output activity;
- explicit distinction among running, input/auth blocked, timed out, failed, findings, and clean;
- bounded stall and wall-clock timeouts;
- process-tree cleanup on timeout/cancel;
- one policy-controlled retry only for transport/provider failure before a valid verdict, never as a way to reroll findings;
- structured terminal metadata containing candidate SHA/tree, start/end time, exit code, timeout/retry state, verdict, and sanitized log location.
Acceptance criteria
- A synthetic silent-but-healthy child produces heartbeats and completes successfully.
- A hung child times out and its complete process tree is reaped on Windows and POSIX.
- A child waiting for interactive input/auth is classified instead of hanging indefinitely where detectable.
- A valid CLEAN or FINDINGS verdict is bound to the exact reviewed candidate.
- Retry occurs only for a configured infrastructure failure and records the original attempt; findings are never retried away.
- Full output is retained in ignored/local metadata while the terminal remains concise.
- Structured metadata is machine-readable and cannot be authored by the implementation under review.
- No chain-of-thought, credentials, raw private prompt data, or repository secrets are added to heartbeat or metadata output.
- Cancellation and host shutdown leave no stale reviewer processes.
- Consumer guidance explains the difference between liveness, successful process exit, and a valid review verdict.
Related
- Does the output-style lever port here? Tighten Fable's cross-model review output for code review #123 — concise Fable review finding shape
- bug: retain failed-test summaries when broad proof output is truncated #124 — retain failed broad-proof summaries
- Audit and bind final review approval to the exact integrated candidate #111 — bind final review to the integrated candidate
BaseInfinity/claude-sdlc-harness#594— ship the sibling review-leg launcherBaseInfinity/claude-sdlc-harness#491— phantom watchdog and stale-process evidence
Activity
BaseInfinity commentedon Aug 15, 2026
Sibling-repo evidence: we built this twice and deleted it both times
claude-sdlc-harnesshit the identical symptom — a non-interactive review leg silent for minutes, with no way to tell reasoning from stall. Filed as #590 after two legs were waited on for 51 and 28 minutes. It shipped fixed in v1.98.0 (2026-08-15).The root cause was not lifecycle observability. It was stdin.
The reviewer CLI reads stdin to EOF and appends it to the argv prompt — they concatenate, they are not alternatives. Handed a pipe nobody closes, it blocks on that read before contacting the provider, forever. Both of our "silent reviewer" incidents were processes that had never started. Verified against the real 0.147.0 binary with the launcher's own stdin on an unclosed fifo: with
< /dev/nullon the child, the leg completes normally.That matters for this issue's framing: a heartbeat on those runs would have reported healthy the entire time. The child was alive, the output stream was intact, nothing had timed out. Liveness was never the missing signal.
We built the runner this issue asks for. Twice. Both were falsified with running code.
Attempt 1 — watch the output file and the process table. Every signal was refuted:
-ooutput carries no completion markertokens usedappears in the CLI's echoed prompt, so a crashed leg reads as completetail -fflips the verdictAttempt 2 — record child pid and exit status in sidecar files. Also refuted:
Same mistake in two costumes: a second observer reconstructing what the first process already knows. The launcher's own exit status is that knowledge, delivered by the process that has it.
What we shipped instead
scripts/run-review-leg.sh— about 40 lines of actual logic:/dev/nullon stdin regardless of what the parent inherited (the whole prevention)execs the reviewer, so the caller sees its exit status with nothing in between that can die separatelyPreToolUsehook (#610) refuses a review leg typed outside the launcher, so a leg cannot be started without an ownerWhere our experience does back this issue
Three of your acceptance criteria we independently proved necessary:
Process-tree cleanup is real and non-optional. Our CI preflight probe backgrounds
gh. Withoutset -mthe subshell shares the parent's process group, so killing by pid leavesghalive, reparented to PID 1. Review ran the fixture and observed exactly that. Fix:set -mto make the subshell a group leader, thenkill -TERM -- "-$PID". Alsodisownafter, or bash's job-termination notice leaks into the reviewer's own output file.Do not use
kill -0for liveness. A background job of the shell stays a zombie until waited on, sokill -0reports it alive after it finished.wait -nsolves it but needs bash 4 and macOS ships 3.2. We use a done-marker file plus a bounded poll."Bounded wall-clock timeout" cannot lean on the child. We wrote "gh carries its own network timeouts" and it is false — gh 2.92 configures no overall HTTP timeout (
cli/cli api/http_client.go,go-gh pkg/api/client_options.go). A stall would block before the reviewer ever launched. The deadline has to be the launcher's own and depend on nothing.One more that bit us:
set -ein the probe subshell killed it before it could write its marker, so every failing child burned the full deadline and reported "abandoned" — the one case it most needed to report correctly.set +einside the subshell.Suggestion
Split #137. The cleanup and bounded-timeout half is well-evidenced and we'd copy your version. The heartbeat and liveness-classification half is the part we spent two attempts on and could not make sound — every completion signal was forgeable, and the actual bug produced a perfectly healthy-looking process. Before building it, check whether your silent runs are the same stdin concatenation; if so,
< /dev/nullon the child is the entire fix and the runner is unnecessary.Cross-references:
claude-sdlc-harness#590 (incident), #610 (the gate requiring the launcher), #613 (the preflight that nearly reintroduced the hang), #594 (the launcher is still repo-internal and does not ship — already linked here).BaseInfinity commentedon Aug 15, 2026
Official host guidance checked (2026-08-15)
This issue should supervise each host through its structured non-interactive interface rather than infer liveness from terminal text.
Codex leg
Current Codex CLI exposes:
codex exec review ... --jsonfor JSONL lifecycle events;--output-schemafor a constrained final verdict;--output-last-messagefor a durable final-response fallback;Official references:
Implementation implication: heartbeat metadata should report elapsed time plus the last structured event timestamp/type. It must not claim the model is "thinking" merely because the process is alive.
Claude/Fable leg
Current Claude Code documents and exposes:
claude -p --output-format stream-jsonfor real-time newline-delimited events;--json-schemafor structured final output;Official reference:
Do not blindly use
--barefor our subscription-backed Fable lane: the official docs say bare mode does not use OAuth/keychain credentials and requires an API key for the Anthropic API. Prefer the narrowest safe non-interactive configuration that preserves the intended subscription authentication.Runner design confirmed
This validates the existing acceptance criteria and adds two important details: parse Claude failures from both stdout and exit status, and do not let continuing output defeat the hard wall-clock timeout.
BaseInfinity commentedon Aug 15, 2026
Conceded — the structured-stream citation defeats my objection, and I want to be precise about why
My previous comment argued the heartbeat half was unsound. That argument does not survive the docs you cited, and the distinction is one I drew myself without following it through.
What I actually falsified was inference over unstructured output: watching an
-otext file and the process table, and trying to reconstruct the child's state from byte counts, fd states, and file holders. Every one of those signals was forgeable, and I stand by each individual falsification. The conclusion I drew from them — "a runner cannot know" — was too wide.The line my own comment drew was between a second observer reconstructing what the first process knows, and the first process reporting it.
codex exec review --jsonand--output-schema, and Claude'sstream-json, are the host reporting. That is first-party knowledge on a documented contract, not reconstruction. It falls on the sound side of the line I was arguing for, and I put it on the wrong side.Two specifics where the citation is strictly better than what we shipped:
tokens usedin the echoed prompt — my sharpest falsification — is an artifact of scraping human-readable output. A typed event stream has no such collision.Your item 7, "bind the verdict to the exact reviewed tree," is the same primitive we are landing right now on our side for
claude-sdlc-harness#540:git write-treeascandidate_tree, verified at the commit boundary and again at the merge boundary. We read #111'sdual-review.cjs(requireFrozenIndexthencandidateTree) rather than deriving it, as your ruling said to. Two notes from doing it that may save you a round:base_treeis not observable without knowing whether the commit is an amend (base isHEADvsHEAD^), and determining that means parsing git's option grammar. We deferredbase_treeto the merge boundary, where the base ref is well-defined, and rely oncandidate_treealone at commit — a rebase changes the index tree anyway, so nothing is lost.requireFrozenIndex. That single precondition is what let us avoid flag parsing entirely: with a clean tracked tree,git commitandgit commit -acommit the same tree.Agreed on your sequencing: #111, then #137, before 1.0.
Retracting my "split #137" suggestion. It rested on the heartbeat half being unsound and it is not.
For our part, adopting the structured-stream contract in
scripts/run-review-leg.shis now a real gap on our side rather than a design we rejected, and I am filing it against ourselves. Our launcher's exit-status-only design is the correct floor — it needs no cooperation from the child — but it is a floor, not a ceiling, and "nothing by your deadline" is a worse answer than "zero events in 90 seconds" when the host will tell us which.BaseInfinity commentedon Aug 16, 2026
Actual Fable-high field evidence
Feedback type: pattern confirmation / provenance requirement
In a five-milestone overnight Codex Desktop run, every risk tier that required Fable used the real subscription-backed command class claude -p --model fable --effort high. A native Codex subagent was not treated as a substitute. The reviews completed and supplied valid clean verdicts against frozen candidates.
What worked
What remains weak
Suggested receipt fields
Record executable identity/version, sanitized exact argv class, model=fable, effort=high, authentication class (subscription-backed, never credential material), candidate SHA/tree, start/end timestamps, structured-output mode, exit status, retry reason, and verdict. Fail closed as AUTH_OR_INPUT_BLOCKED or PROVIDER_OR_TRANSPORT_FAILURE when the real Fable leg cannot launch; never silently substitute another model. This fits #137’s structured metadata and process supervision contract.
BaseInfinity commentedon Aug 16, 2026
Dogfood reproduction from a bundled 0.7.35 consumer on 2026-08-16:
dual-review.cjshard-coded a 10-minute production timeout becauseconfiguredDuration()ignoredCODEX_SDLC_REVIEW_TIMEOUT_MSunlessCODEX_SDLC_TEST_MODE=1. A real Opus 4.8 xhigh review timed out without a receipt.Fable review failed.finallyblock removed the temporary Sol/Claude outputs, leaving no phase/candidate diagnostic.claude -p --model claude-opus-4-8 --effort xhighprobe succeeded under first-party subscription auth, confirming this was adapter behavior rather than missing subscription/model access.Local recovery for the blocked consumer release: 30-minute default with a one-hour bounded override, parse nonzero Claude stdout diagnostics, write a local error receipt with phase/base/tree, pin
claude-opus-4-8+xhighdirectly, and record the actual reviewer identity. Please retain production-timeout, stdout-error-envelope, error-receipt, and truthful-identity regressions in the new review-gate lifecycle suite.BaseInfinity commentedon Aug 16, 2026
#143 receipt-schema follow-up
PR #152 shipped truthful requested-versus-actual cross-model reviewer identity and receipt schema v2. The bounded final review found no blocking consumer defect, but #137 should explicitly audit every receipt reader before extending the lifecycle runner:
Keep the audit narrow: prove each consumer either understands schema v2 or fails closed. Do not rerun #143 reviewer-lane design.
BaseInfinity commentedon Aug 17, 2026
Post-merge cadence evidence for the #109 pilot (captured from the exact #137 review receipt and PR #156):
e1a635d78d7e97747324dbc7e312e023cb01b91f26d7faebc711590efa5c30f1646c53e8fc75bb49sha256:94c5b0e3931343605033ef1239b5b708d7e8a7021d33f5956b0d1c5f5d52d55fwindows-npmcheck succeededThis observation is deliberately ineligible for the ten-delivery decision because subscription quota/token costs and total delivery minutes were not captured. Unknown measurements must not be treated as zero.