fix(cli): lift the diff-scope restriction in the resumed session itself

A resumed run replays its saved conversation (initial_input=[]), so the
instruction recorded for the original diff-scoped run never reaches the
agents on its own: only scan_config["resume_instruction"] is injected
as a fresh message. A widened --scope-mode full resume now uses that
channel to inject a note lifting the old restriction, so the replayed
diff-scope preamble stops suppressing findings outside the old diff.

Also drop a preamble-only instruction (run started without
--instruction) from the widened record instead of leaving it next to
scope_mode "full", which re-introduced the contradiction the change
fixes for that case.

Addresses the greptile review on #1509.
This commit is contained in:
root 2026-10-11 20:44:32 +08:00
parent 88b19267c2
commit f7324b4bfe
4 changed files with 69 additions and 9 deletions

View file

@ -97,7 +97,9 @@ async def run_cli(args: Any) -> None: # noqa: PLR0915
"workspace_files": getattr(args, "workspace_files", None) or [],
"scope_mode": getattr(args, "scope_mode", "auto"),
"diff_base": getattr(args, "diff_base", None),
"resume_instruction": getattr(args, "user_explicit_instruction", None) or "",
"resume_instruction": getattr(args, "resume_instruction", None)
or getattr(args, "user_explicit_instruction", None)
or "",
}
report_state = ReportState(args.run_name)

View file

@ -24,6 +24,15 @@ from strix.report.runs import list_run_summaries
# Severities ``--fail-on`` accepts, most severe first.
FAIL_ON_SEVERITIES = ("critical", "high", "medium", "low", "info")
# Injected into a resumed session that was widened past its recorded diff
# scope, so the diff-scope preamble replayed from the original conversation
# stops telling the agents to report findings only inside the old diff.
RESUME_SCOPE_WIDENED_INSTRUCTION = (
"The diff scope of the original run has been lifted (--scope-mode full): "
"analyze the full repository. Findings are no longer limited to the "
"changes listed in the earlier scope instructions."
)
def get_version() -> str:
try:
@ -548,14 +557,24 @@ def load_resume_state(args: argparse.Namespace) -> None:
args.scope_mode = persisted_scope_mode
if state.get("diff_base") and not args.diff_base:
args.diff_base = state.get("diff_base")
# What reaches the resumed session as a fresh message: the runner injects
# scan_config["resume_instruction"], because a replayed session would
# otherwise ignore any new instruction entirely.
args.resume_instruction = args.user_explicit_instruction or ""
if args.scope_mode == "full":
# An explicit --scope-mode full widens the resume to the full
# repository, as --help promises; the flag's "auto" default keeps the
# intent unambiguous. The persisted instruction carries the diff-scope
# preamble, so a widened resume rebuilds it from the clean user text.
# intent unambiguous.
widened = bool(isinstance(args.diff_scope, dict) and args.diff_scope.get("active"))
args.diff_scope = {"active": False, "mode": "full"}
if not args.user_explicit_instruction and args.user_instruction:
args.instruction = args.user_instruction
if widened and not args.user_explicit_instruction:
# The instruction recorded for a diff-scoped run carries the
# preamble prepare_run() prepended to it; the widened resume
# records the clean user text instead, and injects a note into
# the replayed session so the old restriction is lifted for
# the agents too, not just in the published record.
args.instruction = state.get("user_instruction") or None
args.resume_instruction = RESUME_SCOPE_WIDENED_INSTRUCTION
agents_path = runtime_state_dir(run_dir) / "agents.json"
if not agents_path.exists():
raise ResumeError(

View file

@ -99,7 +99,9 @@ class GoTuiRuntime:
"workspace_files": getattr(self.args, "workspace_files", None) or [],
"scope_mode": self.args.scope_mode,
"diff_base": self.args.diff_base,
"resume_instruction": self.args.user_explicit_instruction or "",
"resume_instruction": getattr(self.args, "resume_instruction", None)
or getattr(self.args, "user_explicit_instruction", None)
or "",
"workspace_mount": getattr(self.args, "workspace_mount", None) or "",
"workspace_subdir": getattr(self.args, "workspace_subdir", None) or "",
}

View file

@ -10,6 +10,8 @@ from typing import TYPE_CHECKING, Any
import pytest
from strix.interface.cli_args import RESUME_SCOPE_WIDENED_INSTRUCTION
if TYPE_CHECKING:
from pathlib import Path
@ -78,6 +80,7 @@ def test_resume_restores_persisted_scope_over_flag_defaults(
assert args.diff_base == "origin/main"
assert args.diff_scope == {"active": True, "mode": "diff", "repos": []}
assert args.instruction == PREAMBLE + "find auth bugs"
assert args.resume_instruction == ""
def test_resume_honors_scope_mode_full(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
@ -96,8 +99,10 @@ def test_resume_honors_scope_mode_full(tmp_path: Path, monkeypatch: pytest.Monke
assert args.scope_mode == "full"
assert args.diff_scope == {"active": False, "mode": "full"}
# The recorded instruction carries the diff-scope preamble; the widened
# resume keeps only the clean user text.
# resume keeps only the clean user text, and injects a note lifting the
# old restriction into the replayed session.
assert args.instruction == "find auth bugs"
assert args.resume_instruction == RESUME_SCOPE_WIDENED_INSTRUCTION
def test_resume_honors_scope_mode_full_with_new_instruction(
@ -118,12 +123,13 @@ def test_resume_honors_scope_mode_full_with_new_instruction(
assert args.scope_mode == "full"
assert args.diff_scope == {"active": False, "mode": "full"}
assert args.instruction == "check the API"
assert args.resume_instruction == "check the API"
def test_resume_honors_scope_mode_full_without_recorded_user_instruction(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""Widening still takes effect when the run predates user_instruction."""
"""A widened resume drops a preamble-only instruction and injects the note."""
work = tmp_path / "project"
work.mkdir()
monkeypatch.chdir(tmp_path)
@ -136,7 +142,38 @@ def test_resume_honors_scope_mode_full_without_recorded_user_instruction(
args = _resume(monkeypatch, "--scope-mode", "full")
assert args.diff_scope == {"active": False, "mode": "full"}
assert args.instruction == PREAMBLE
assert args.instruction is None
assert args.resume_instruction == RESUME_SCOPE_WIDENED_INSTRUCTION
def test_resume_with_scope_mode_full_on_a_full_run_injects_nothing(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""Re-asserting full on a run that was already full is a plain no-op."""
work = tmp_path / "project"
work.mkdir()
monkeypatch.chdir(tmp_path)
_write_run_record(
tmp_path / "strix_runs",
"pentest_abcd",
{
"run_name": "pentest_abcd",
"targets_info": [],
"local_sources": [],
"instruction": "find auth bugs",
"user_instruction": "find auth bugs",
"scan_mode": "deep",
"scope_mode": "full",
"diff_scope": {"active": False, "mode": "full"},
"workspace_mount": str(work),
},
)
args = _resume(monkeypatch, "--scope-mode", "full")
assert args.diff_scope == {"active": False, "mode": "full"}
assert args.instruction == "find auth bugs"
assert args.resume_instruction == ""
@pytest.mark.parametrize("extra", [["--scope-mode", "diff"], ["--diff-base", "origin/main"]])