Repository navigation
fix(runner): decide scan completion from persisted report state, not final_output (#29) - #31
Conversation
…final_output (#29) After a successful claude-cli scan (finish_scan called, report written, scan_completed=true), the runner still logged an ERROR — "Scan ... ended without calling finish_scan ... Final output: 'None'" — because it inferred completion from result.final_output, which the claude-cli lane leaves None even on a clean finish. A success looked like a text-only end. Decide completion from the authoritative source instead: the global report state, which finish_scan populates (scan_results.scan_completed=True, run record marked completed) and a fresh/resumed run clears first. final_output is kept only as a fallback for lanes that populate it, so their behaviour is unchanged and a run that genuinely never finishes still logs the ERROR. Closes #29. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
|
jarvisallmen-qa
left a comment
There was a problem hiding this comment.
QA Verdict: APPROVE
Milestone: bugfix/29-runner-authoritative-completion PR: #31/fix/29-runner-authoritative-completion
CI
- Lint & Format: pass
- Tests & Coverage: pass (419 passed / 0 skipped, coverage 73.48%)
- Security Checks: pass (gitleaks, actionlint, osv-scanner, semgrep all green)
Acceptance criteria
| Criterion (from Brief/FRD) | Implemented | Test verifying it | Status |
|---|---|---|---|
AC-1: After successful scan with report written (scan_results.scan_completed=true), runner does not log ERROR |
yes (strix/core/runner.py:143-145) |
tests/test_runner_completion_detection.py::test_complete_when_finish_scan_persisted_results |
met |
| AC-2: After scan genuinely ending without finish_scan, runner still logs ERROR | yes (strix/core/runner.py:139-146,638-644) |
tests/test_runner_completion_detection.py::test_incomplete_when_never_finished, test_incomplete_when_scan_results_not_completed |
met |
AC-3: Runner determines scan success from authoritative source (scan_results.scan_completed, report state/run_record), robust to final_output=None |
yes (strix/core/runner.py:129-146) |
tests/test_runner_completion_detection.py::test_complete_when_finish_scan_persisted_results, test_complete_when_run_record_marked_completed |
met |
AC-4: Lanes populating final_output show unchanged behavior (fallback preserved) |
yes (strix/core/runner.py:638-644) |
tests/test_runner_completion_detection.py::test_incomplete_without_report_state + full runner suite |
met |
Blocking issues (REJECT only)
None.
Non-blocking notes
- Change is clean, minimal (+20/-3 in runner.py), and specifically addresses the symptom on the claude-cli lane without altering other execution lanes or Vera reporting contracts.
|
|
{
"stage": "security",
"agent": "vera-agent",
"status": "done",
"attempt": 1,
"heartbeat_at": "2026-09-14T13:36:00Z",
"checkpoints": [
"PR #31 head 356bca3 confirmed = QA-approved head (jarvisallmen-qa, 2026-09-14T13:30:59Z)",
"security.yml: 4/4 jobs pass (actionlint, semgrep, gitleaks full-history, osv-scanner); 6 Semgrep audit-level hits all pre-existing/out of scope, triaged in review",
"diff vs issue #29 brief: no undesigned surface (no new auth/listener/egress/subprocess/file-write/dependency)",
"verdict APPROVE submitted as native review; sec-approved applied; stage:security set",
"coverage note: this repo's security.yml has 4 jobs (no repo-hygiene/detect-languages/Tier-2 boundary jobs) — recorded not-run, not passed"
]
} |
jarvisallmen
left a comment
There was a problem hiding this comment.
Security Verdict: APPROVE
Milestone: bugfix #29 (no vault FRD folder; issue brief is the authorization of record) · PR: #31 fix/29-runner-authoritative-completion @ 356bca3 · QA: approved by @jarvisallmen-qa at 2026-09-14T13:30:59Z on the same head SHA.
Static analysis (security.yml)
| Check | Result | Findings | Disposition |
|---|---|---|---|
| actionlint (workflows) | pass | 0 | — |
| semgrep (SAST) | pass | 6 (all audit/informational) | 0 confirmed, 0 false positive, 6 out of scope (below) |
| gitleaks (secrets) | pass | 0 (full history) | — |
| osv-scanner (dependencies) | pass | 0 | — |
| detect languages / repo hygiene / client boundary / python boundary | not present in this repo's security.yml (4-job variant) |
— | recorded as not-run, not passed — see Coverage limits |
Undesigned surface
None. The diff adds one pure helper (_scan_reported_complete(), strix/core/runner.py:129) that reads in-process report state, and reorders the completion check to prefer it. No new auth/session path, no listener or bind change, no egress, no subprocess, no file write, no deserialization change (json.loads(final_output) fallback is pre-existing and unchanged), no new dependency, no widened permission.
Open questions a pentest would settle
None. The change is internal-state logic with no attacker-facing input path; reachability is fully traceable by reading. No pentest has run (normal for this gate; not needed here).
Dismissed findings — Semgrep audit-level hits, all pre-existing and outside this diff
| Finding | Where | Why it does not apply |
|---|---|---|
| direct-use-of-jinja2 | strix/agents/prompt.py:89 |
Untouched by this milestone; prompt templating, not in the diff's code path. File separately. |
| dangerous-os-exec-tainted-env-args | strix/interface/update_check.py:295 |
Untouched; update-check subprocess, pre-existing. File separately. |
| detect-non-literal-regexp | TerminalRenderer.tsx:74 |
Untouched; viewer frontend, pre-existing. File separately. |
| react-dangerouslysetinnerhtml | PocBlock.tsx:72 |
Untouched; renders scan-report HTML — worth its own follow-up, but outside this diff. |
| non-literal-import | strix/llm/warmup.py:38 |
Untouched; warmup module loading, pre-existing. File separately. |
| dynamic-urllib-use | strix/tools/proxy/caido_api.py:79 |
Untouched; proxy tool egress, pre-existing. File separately. |
Coverage limits
- This repo's
security.ymlcarries only four jobs.detect languages,repo hygiene, and both Tier 2 boundary checks do not exist in the workflow here — I record them as not-run, not passed. The most relevant absence for a Python repo is thepython boundarycheck; I compensated by reading the diff for default-open binds and debug flags — none added. - No Strix pentest has run against this head (on-demand only; absence is not a blocker).
- The vault has no
strix-29folder — this is atype:bugfixchange authorized by the issue #29 brief, which I used in place of an FRD. Scope in the brief (scan-completion detection inrunner.py) matches the diff exactly.
Non-blocking notes
- The six pre-existing Semgrep audit hits above should be triaged in a separate pass; none is in code this milestone touched.
|
|
What
After a successful claude-cli scan (finish_scan called, executive report written,
scan_completed=true, findings persisted) the runner still logged an ERROR:Harmless downstream (Vera reads
scan_results/findings, not this log), but it's misleading and misdirects debugging.Why
The completion check inferred success from
result.final_output. On the claude-cli lanefinal_outputisNoneeven on a clean finish (the settle-interrupt path — see #28), so a real success read as a text-only end.Change
Decide completion from the authoritative source — the global report state that
finish_scanpopulates (scan_results.scan_completed=True, run record markedcompleted) viaupdate_scan_final_fields, and that a fresh/resumed run clears first.final_outputis kept only as a fallback for lanes that populate it.scan_results.scan_completed/ run-record status, robust tofinal_output=None;final_outputare unchanged (fallback branch preserved).Test
tests/test_runner_completion_detection.pycovers the authoritative helper across all four states (persisted results, run-record completed, never-finished, no report state).Full
runnersuite green;ruff+mypyclean.Closes #29. Independent of #28/#30 (that fix may repopulate
final_output, but this check is correct regardless).🤖 Generated with Claude Code