Grooming decision
Filed as a separate issue, not folded into #195 or #197. #195 is about components that complete with a non-success result under-reporting the counts their own logs hold (ci/evidence.py::machine_output_claim, inside create_report); this is about a component that never completes preventing create_report from being reached at all — a different file, a different layer, and #195's non-goals pin its verdict/report semantics tight. #197 is origin attribution for local evidence, an unrelated mechanism. The three issues are adjacent faces of one principle — the harness must always produce a truthful machine-readable aggregate — and each keeps a small attributable diff.
Problem
When a verification component command hangs, make verification-run never emits a final verification-report.json. Per-component evidence accumulates, but there is no aggregate artifact, so the handoff cannot carry the report path/digest _docs/PROCESS.md requires, and the engineer must hand-assemble the account.
Root cause (verified): ci/runner.py executes each component via subprocess.run(command, ..., check=False, stdout=PIPE, stderr=STDOUT) with no timeout — a hung child blocks the runner indefinitely. The Makefile chain only reaches verification-report-check (which invokes ci.verification report to write verification-report.json) after the runner exits. Other CI modules already bound their waits (ci/screenshot_runtime.py uses process.wait(timeout=...) with a graceful terminate; ci/container_check.py and ci/history.py use HTTP timeouts) — the runner's component execution is the unbounded outlier.
This violates the principle #193's verdict-precedence fix established and #195 carries forward: reports must stay emittable on failure. A hang is a failure mode; today it produces no aggregate at all.
Observed incidents (from #198 engineering, #198 (comment))
- Attempt 1: disk-full failures plus a wedged playwright teardown, manually terminated — "No final aggregate report file was produced (
verification-report-check not reached)."
- Attempt 2: six components green with envelopes retained, but
playwright hangs forever in a driver-level deadlock (test_accessibility.py mobile-viewport core smoke; faulthandler shows the main thread inside the Playwright driver event loop), reproduced identically at clean base b487b1f via stash A/B, and the owner's main-checkout run of the same module stayed wedged for 12 hours. "Because the runner hangs inside this component, make verification-run cannot emit a final verification-report.json."
Scope
- Bound every runner component execution with an explicit wall-clock timeout: a documented default generous enough for the longest legitimate suite, plus an override (CLI flag and/or plan field — engineering's choice, recorded in the doc).
- On expiry: terminate and then kill the child, retain whatever partial output exists under the component's output artifact, and record the component's result envelope as non-success with an explicit timeout indication and the exact command.
- Continue to the remaining components and finish the run so
verification-report-check emits verification-report.json with a failure verdict; the timed-out component is an executed rerun with a failure result, never a skip.
- Document the default and override in
_docs/ci/change-selective-ci.md.
Non-goals
Acceptance criteria
Dependencies: none. Coordination note: after #195 lands, a timed-out component's zero-count fallback falls out naturally from its unparseable-log path; before it, the existing non-success envelope behavior applies — neither ordering blocks this issue.
Normative references: _docs/PROCESS.md (evidence origins, report requirements), _docs/ci/change-selective-ci.md.
Grooming decision
Filed as a separate issue, not folded into #195 or #197. #195 is about components that complete with a non-success result under-reporting the counts their own logs hold (
ci/evidence.py::machine_output_claim, insidecreate_report); this is about a component that never completes preventingcreate_reportfrom being reached at all — a different file, a different layer, and #195's non-goals pin its verdict/report semantics tight. #197 is origin attribution for local evidence, an unrelated mechanism. The three issues are adjacent faces of one principle — the harness must always produce a truthful machine-readable aggregate — and each keeps a small attributable diff.Problem
When a verification component command hangs,
make verification-runnever emits a finalverification-report.json. Per-component evidence accumulates, but there is no aggregate artifact, so the handoff cannot carry the report path/digest_docs/PROCESS.mdrequires, and the engineer must hand-assemble the account.Root cause (verified):
ci/runner.pyexecutes each component viasubprocess.run(command, ..., check=False, stdout=PIPE, stderr=STDOUT)with no timeout — a hung child blocks the runner indefinitely. The Makefile chain only reachesverification-report-check(which invokesci.verification reportto writeverification-report.json) after the runner exits. Other CI modules already bound their waits (ci/screenshot_runtime.pyusesprocess.wait(timeout=...)with a graceful terminate;ci/container_check.pyandci/history.pyuse HTTP timeouts) — the runner's component execution is the unbounded outlier.This violates the principle #193's verdict-precedence fix established and #195 carries forward: reports must stay emittable on failure. A hang is a failure mode; today it produces no aggregate at all.
Observed incidents (from #198 engineering, #198 (comment))
verification-report-checknot reached)."playwrighthangs forever in a driver-level deadlock (test_accessibility.pymobile-viewport core smoke; faulthandler shows the main thread inside the Playwright driver event loop), reproduced identically at clean baseb487b1fvia stash A/B, and the owner's main-checkout run of the same module stayed wedged for 12 hours. "Because the runner hangs inside this component,make verification-runcannot emit a finalverification-report.json."Scope
verification-report-checkemitsverification-report.jsonwith afailureverdict; the timed-out component is an executed rerun with a failure result, never a skip._docs/ci/change-selective-ci.md.Non-goals
validate_report, or reuse policy — that is Failed test components' evidence envelopes must carry the counts their logs hold #195's territory; this issue only guarantees the aggregate is reached.Acceptance criteria
verification-report-check: a finalverification-report.jsonis emitted with afailureverdict and no silently skipped component.tests_ci/: a never-terminating component command under a small configured timeout yields the timeout result envelope and an aggregate report with afailureverdict; a normal green run is unaffected by the timeout plumbing._docs/ci/change-selective-ci.mdrecords the default and override;make test-ci, lint, format-check, and typecheck green; verification plan per_docs/ci/change-selective-ci.mdwith all component dispositions recorded._docs/PROCESS.md.Dependencies: none. Coordination note: after #195 lands, a timed-out component's zero-count fallback falls out naturally from its unparseable-log path; before it, the existing non-success envelope behavior applies — neither ordering blocks this issue.
Normative references:
_docs/PROCESS.md(evidence origins, report requirements),_docs/ci/change-selective-ci.md.