Skip to content

fix(runner): decide scan completion from persisted report state, not final_output (#29) - #31

Merged
jarvisallmen merged 1 commit into
mainfrom
fix/29-runner-authoritative-completion
Sep 14, 2026
Merged

jarvisallmen merged 1 commit into
mainfrom
fix/29-runner-authoritative-completion

Conversation

@Alex-vonAllmen

Copy link
Copy Markdown
Owner

What

After a successful claude-cli scan (finish_scan called, executive report written, scan_completed=true, findings persisted) the runner still logged an ERROR:

Scan ... ended without calling finish_scan ... Final output: 'None'

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 lane final_output is None even 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_scan populates (scan_results.scan_completed=True, run record marked completed) via update_scan_final_fields, and that a fresh/resumed run clears first. final_output is kept only as a fallback for lanes that populate it.

  • AC-1 a persisted completion no longer logs the ERROR;
  • AC-2 a genuine no-finish run still does (report state stays uncompleted);
  • AC-3 completion comes from scan_results.scan_completed / run-record status, robust to final_output=None;
  • AC-4 lanes that populate final_output are unchanged (fallback branch preserved).

Test

tests/test_runner_completion_detection.py covers the authoritative helper across all four states (persisted results, run-record completed, never-finished, no report state).

Full runner suite green; ruff + mypy clean.

Closes #29. Independent of #28/#30 (that fix may repopulate final_output, but this check is correct regardless).

🤖 Generated with Claude Code

…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

Copy link
Copy Markdown
Collaborator

oren-agent · qa · 2026-09-14T13:29:57.101Z · initial · corr=19848:31:qa

@jarvisallmen-qa jarvisallmen-qa left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@jarvisallmen

Copy link
Copy Markdown
Collaborator

vera-agent · security · 2026-09-14T13:31:02.634Z · initial · corr=19860:31:security

@jarvisallmen-qa jarvisallmen-qa added the qa-approved QA approved this PR label Sep 14, 2026
@jarvisallmen jarvisallmen added the stage:security PR under security review (last gate) label Sep 14, 2026
@jarvisallmen

jarvisallmen commented Sep 14, 2026 •

Copy link
Copy Markdown
Collaborator
{
  "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 jarvisallmen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.yml carries 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 the python boundary check; 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-29 folder — this is a type:bugfix change authorized by the issue #29 brief, which I used in place of an FRD. Scope in the brief (scan-completion detection in runner.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.

@jarvisallmen jarvisallmen added the sec-approved Security approved; this review merges the PR label Sep 14, 2026
@jarvisallmen
jarvisallmen merged commit 1d11332 into main Sep 14, 2026
6 checks passed
@jarvisallmen

Copy link
Copy Markdown
Collaborator

mara-agent · advance · 2026-09-14T13:34:27.382Z · initial · corr=19881:31:advance

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

qa-approved QA approved this PR sec-approved Security approved; this review merges the PR stage:security PR under security review (last gate)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

runner logs spurious 'ended without finish_scan' after a successful finish on the claude-cli lane

3 participants