Repository navigation
Description
Problem Description
The stage planning job omits existing test files when a plan changes perimeter network policies. For example, adding an egress rule to k8s/logging/networkpolicy.yaml breaks tests/commands/k8s/test_k8s_logging_stack.py::test_logging_stack_security_and_scoping, which asserts len(egress_rules) == 2. The stage job did not include that test in the Changes: list of the plan. During the implement job, the runner cannot modify unlisted test files because the diff_scope constraint forbids it. The runner repeatedly runs tests and attempts workarounds until it exhausts its token budget of 8,000,000 tokens. The job then fails with LimitReached and marks the item status/blocked.
Reproduction Steps
- Stage an item that adds an egress rule to
k8s/logging/networkpolicy.yaml. - Inspect
tasks.mdand observe thattests/commands/k8s/test_k8s_logging_stack.pyis absent fromChanges:. - Run the implement job on the item.
- Observe the runner fail the egress rule count assertion and enter an edit loop until it exhausts the token budget.
Expected vs Actual Behavior
Expected: The stage job identifies existing tests that inspect the modified manifests and adds them to Changes: in tasks.md.
Actual: The stage job omits the test files from Changes:, creating a deadlock between diff_scope and test assertions during implementation.
Environment Diagnostics
devops-cli 0.79.0, Linux x86_64, 2026-11-07.
Proposed Solution & Architectural Design
- Extend the stage probe or stage paths analyzer to find existing tests that reference the target manifest paths.
- Ensure tests with explicit rule counts or structural assertions on target manifests are included in task group changes.
- Add a check or early-stop guard in the implement runner when an invariant test fails on a file outside
Changes:.
Existing Tool Considered
The existing stage_probe and stage_paths modules in src/devops_cli/roadmap/stage/.
Proposed design
Problem statement
The stage planning job leaves out existing tests that assert on a changed perimeter network policy. The implement job cannot edit a test outside its Changes: list because diff_scope forbids it. The runner then loops until it spends its token budget and marks the item blocked. This change makes the stage path check require those tests in the plan.
Acceptance criteria
Requirement: Perimeter test required
The stage path check SHALL refuse a plan that changes a YAML manifest under k8s/ and omits a test file that references the manifest.
Scenario: Logging egress plan omits the logging test
- WHEN The plan lists
k8s/logging/networkpolicy.yamlinChanges:and omitstests/commands/k8s/test_k8s_logging_stack.py, which assertslen(egress_rules) == 2and contains the literalsloggingandnetworkpolicy.yaml. - THEN
path_problemsreturns a problem that namesk8s/logging/networkpolicy.yamlandtests/commands/k8s/test_k8s_logging_stack.py.
Requirement: Included perimeter test accepted
The stage path check SHALL accept a plan that lists every test file that references a changed manifest.
Scenario: Plan lists the logging perimeter test
- WHEN The plan lists
k8s/logging/networkpolicy.yamlandtests/commands/k8s/test_k8s_logging_stack.pyinChanges:. - THEN
path_problemsreturns no problem that namestests/commands/k8s/test_k8s_logging_stack.py.
Requirement: Path components identify a reference
The stage path check MUST recognize a test that builds a manifest path from separate string literals.
Scenario: Test builds the logging manifest path from components
- WHEN A test file contains the string literals
loggingandnetworkpolicy.yaml. - THEN The check classifies that test as a reference to
k8s/logging/networkpolicy.yaml.
Requirement: Unrelated test not required
The stage path check SHALL NOT require a test file that lacks the manifest basename or the parent directory name.
Scenario: A logging test without the network policy basename
- WHEN A test file contains
loggingbut notnetworkpolicy.yaml. - THEN The check does not name that test file as a missing perimeter test for
k8s/logging/networkpolicy.yaml.
Requirement: Stage eval score task
The change's tasks.md MUST hold a task that runs uv run devops roadmap eval stage --replay with the words score, before and after.
Scenario: Stage path change plan
- WHEN The plan changes
src/devops_cli/roadmap/stage/stage_paths.py. - THEN
check_proposalaccepts the plan only when one group task holds that command and those three words.
Existing Tool Considered
No alternatives check has run. No open, maintained tool covers the need: this is an internal plan check in devops-cli. The change extends src/devops_cli/roadmap/stage/stage_paths.py and src/devops_cli/roadmap/stage/stage_change.py. The written cost is about 150,000 tokens to build, keep and fix the code, and about 2 engineer-days of elapsed time.
Key questions
- Where should the stage job enforce inclusion of perimeter tests?: Add the check to
path_problemsinsrc/devops_cli/roadmap/stage/stage_paths.py. (best practice) [sources: src/devops_cli/roadmap/stage/stage_paths.py, openspec/changes/3307-the-plan-check-names-a-caller/design.md:5, docs/roadmap/package-layout.md:11]- Rejected:
stage_probe. It reads the commit for the prompt and does not hold the plan-path refusal rule. - Chosen:
stage_paths.path_problems. It already refuses a plan that leaves out a caller file, andcheck_proposalcalls it. - Rejected: new module. It splits one plan-path rule across two modules.
- Rejected:
- How should the check find a test that inspects a changed manifest?: Collect string literals from each test file under
tests/withastand match the manifest basename and its parent directory name. (best practice) [sources: tests/commands/k8s/test_k8s_logging_stack.py, k8s/logging/networkpolicy.yaml]- Rejected: exact full-path string. The logging test builds the path from
REPO_ROOT,k8s,loggingandnetworkpolicy.yaml, so it holds no full-path string. - Rejected: raw text search. It matches comments and imports and gives false references.
- Chosen: AST string literals with parent and basename. It finds the logging test, ignores comments, and needs no live tree.
- Rejected: exact full-path string. The logging test builds the path from
- Which changed paths trigger the perimeter test check?: Every changed YAML file under
k8s/. (best practice) [sources: k8s/logging/networkpolicy.yaml, tests/commands/k8s/test_k8s_logging_stack.py]- Rejected: only
k8s/**/networkpolicy.yaml. Tests also assert on other manifests, so the same deadlock can return. - Chosen: every YAML file under
k8s/. It covers the logging case and other manifests without touching source files. - Rejected: every changed file. It overlaps the caller check and can require tests for source files.
- Rejected: only
- Should the stage path check refuse the plan or only warn?: Refuse the plan in
path_problems. (best practice) [sources: docs/ROADMAP_WORKFLOW.md:387]- Rejected: warning only. The implement job still cannot edit the test because
diff_scopeforbids it. - Chosen: hard refusal. The plan must list the test before staging can finish.
- Rejected: warning only. The implement job still cannot edit the test because
- Does a change to
stage_paths.pyneed a stage eval score task?: Yes. One group task must runuv run devops roadmap eval stage --replayand state the score before and after. [sources: docs/ROADMAP_WORKFLOW.md:314, src/devops_cli/roadmap/stage/stage_change.py] - Should this change also add an implement runner early-stop guard?: No. Defer it to a separate issue. The stage gate removes the reported deadlock, and the guard is separate runtime behavior. (best practice) [sources: src/devops_cli/roadmap/implement/implement_step.py, src/devops_cli/roadmap/implement/implement_limits.py, docs/ROADMAP_WORKFLOW.md:387]
- Rejected: include in this change. It adds a second runtime path and is not needed to meet the expected stage behavior.
- Chosen: defer to a separate issue. The stage gate fixes the reported deadlock, and the guard needs its own tests.
Fit
Fits in one pull request.
Suspected block
None.
Dependencies
None.
Activity
dan-petty commentedon Nov 7, 2026
Intake placed this item to the backlog: intake puts every new item in the backlog. A person plans the milestones.
fixprefix of the title gives the type.(roadmap)scope of the title gives the scope.The omission causes implement jobs to exhaust 8M tokens and mark items blocked, blocking all network-policy changes.Every plan touching a manifest with structural assertions is blocked, wasting token budgets and halting work.Extending the stage probe to scan for tests referencing target manifest paths is scoped to existing modules.Both address the stage job omitting existing tests from Changes, but #3414 refuses the plan while the candidate extends the probe to find and include the tests.devops-cli-service commentedon Nov 7, 2026
Status set to Ready at dc276b0 (see proposed design in issue body).
Moved to new items: #3983.
devops-cli-service commentedon Nov 8, 2026
Staged: branch
fix/3972-stage-includes-perimeter-tests-for-network, draft pull request #4138. The pull request plans the item and closes no issue.The task groups, with the tokens of the inputs of each step, for a budget of 64000 tokens:
The default branch changed these paths that the plan names, after the commit that the planner read. Read the commits before you judge the plan:
src/devops_cli/lang/en/messages.py: 682d206 feat(k8s): add the searxng-check command (feat(k8s): add the searxng-check command #4136)The runs of the planner and the review made 65 model request(s), and used 5089580 input tokens and 98051 output tokens. The tool calls, in order:
list_files({"directory":"src/devops_cli/roadmap/stage"})list_files({"directory":"tests/roadmap/stage"})search_code({"text":"caller_problems"})search_code({"text":"roadmap eval stage --replay"})search_code({"text":"STAGE_EVAL_TRIGGER_PATHS"})search_code({"text":"ProposalGate"})read_file({"path":"src/devops_cli/roadmap/stage/stage_change.py","start_line":180,"end_line":240})read_file({"path":"src/devops_cli/roadmap/stage/stage_change.py","start_line":1000,"end_line":1200})read_file({"path":"src/devops_cli/roadmap/stage/stage_change.py","start_line":1168,"end_line":1300})search_code({"text":"_self_acceptance_problems"})read_file({"path":"src/devops_cli/roadmap/stage/stage_change.py","start_line":790,"end_line":900})search_code({"text":"_PLAN_CHECK_FILE"})search_code({"text":"stage_problem_missing_caller"})read_file({"path":"tests/roadmap/stage/test_roadmap_stage_paths.py"})read_file({"path":"tests/roadmap/stage/test_roadmap_stage_paths.py","start_line":189,"end_line":420})read_file({"end_line":600,"path":"tests/roadmap/stage/test_roadmap_stage_paths.py","start_line":420})read_file({"end_line":720,"path":"tests/roadmap/stage/test_roadmap_stage_paths.py","start_line":600})read_spec({"capability":"roadmap-stage"})search_code({"text":"search_evidence"})read_file({"end_line":180,"path":"src/devops_cli/roadmap/stage/stage_change.py","start_line":100})search_code({"text":"_MESSAGE_NAME"})search_code({"text":"_stale_test_path_problems"})read_file({"end_line":60,"path":"src/devops_cli/roadmap/stage/stage_eval_gates.py","start_line":1})read_file({"end_line":1360,"path":"src/devops_cli/roadmap/stage/stage_change.py","start_line":1300})read_file({"end_line":160,"path":"src/devops_cli/roadmap/stage/stage_eval_gates.py","start_line":60})search_code({"text":"gate_arguments"})search_code({"text":"record_search_evidence"})read_file({"path":"src/devops_cli/roadmap/stage/stage_request.py"})read_file({"end_line":1200,"path":"src/devops_cli/roadmap/stage/stage.py","start_line":1100})read_file({"end_line":230,"path":"src/devops_cli/roadmap/stage/stage_form.py","start_line":140})search_code({"text":"self._grep"})search_code({"text":"def metric_senders"})read_file({"end_line":920,"path":"src/devops_cli/roadmap/stage/stage.py","start_line":830})callers({"symbol":"check_gates"})list_files({"directory":"src/devops_cli/roadmap"})list_files({"directory":"tests/roadmap/stage"})read_file({"path":"src/devops_cli/roadmap/plan_recheck.py","start_line":250,"end_line":330})read_file({"path":"src/devops_cli/roadmap/stage/stage_change.py","start_line":1120,"end_line":1270})file_symbols({"path":"src/devops_cli/roadmap/plan_recheck.py"})file_symbols({"path":"src/devops_cli/roadmap/stage/stage_eval_gates.py"})file_symbols({"path":"src/devops_cli/roadmap/stage/stage_eval_cases.py"})file_symbols({"path":"src/devops_cli/roadmap/stage/stage_request.py"})read_file({"path":"src/devops_cli/roadmap/stage/stage_eval_cases.py","start_line":155,"end_line":215})read_file({"path":"src/devops_cli/roadmap/stage/stage_eval_gates.py","start_line":60,"end_line":135})read_file({"path":"src/devops_cli/roadmap/stage/stage_request.py","start_line":58,"end_line":120})read_file({"path":"src/devops_cli/roadmap/plan_recheck.py","start_line":229,"end_line":250})search_code({"text":"_TOKEN_COST"})search_code({"text":"def eval_replay_problems"})file_symbols({"path":"src/devops_cli/roadmap/stage/stage_form.py"})file_symbols({"path":"src/devops_cli/roadmap/stage/stage.py"})read_file({"path":"src/devops_cli/roadmap/stage/stage.py","start_line":1154,"end_line":1200})read_file({"path":"src/devops_cli/roadmap/stage/stage_form.py","start_line":161,"end_line":205})read_file({"path":"src/devops_cli/roadmap/stage/stage_change.py","start_line":1029,"end_line":1060})read_file({"path":"src/devops_cli/roadmap/stage/stage_change.py","start_line":105,"end_line":118})callers({"symbol":"gate_arguments"})callers({"symbol":"check_proposal"})find_issues({"terms":"stage_paths perimeter test"})read_file({"path":"src/devops_cli/roadmap/stage/stage_change.py","start_line":155,"end_line":225})search_code({"text":"def path_problems"})search_code({"text":"def check_gates"})search_code({"text":"def record_search_evidence"})search_code({"text":"def _record"})search_code({"text":"class CaseFacts"})search_code({"text":"def _problems"})read_file({"path":"src/devops_cli/roadmap/stage/stage_paths.py","start_line":240,"end_line":420})read_file({"path":"src/devops_cli/roadmap/stage/stage_change.py","start_line":1100,"end_line":1300})read_file({"path":"src/devops_cli/roadmap/stage/stage_form.py","start_line":120,"end_line":240})read_file({"path":"src/devops_cli/roadmap/plan_recheck.py","start_line":150,"end_line":360})read_file({"end_line":1260,"path":"src/devops_cli/roadmap/stage/stage.py","start_line":1080})read_file({"end_line":240,"path":"src/devops_cli/roadmap/stage/stage_eval_cases.py","start_line":140})search_code({"text":"_grep"})search_code({"text":"def stage_problem_missing_caller"})search_code({"text":"stage_problem_missing_caller"})read_file({"end_line":920,"path":"src/devops_cli/roadmap/stage/stage.py","start_line":820})search_code({"text":"grep ="})search_code({"text":"grep="})search_code({"text":"def metric_senders"})search_code({"text":"def grep_at"})read_file({"end_line":230,"path":"src/devops_cli/roadmap/stage/stage_overlap.py","start_line":170})list_files({"directory":"tests/roadmap/stage"})search_code({"text":"_self_acceptance_problems"})search_code({"text":"self_acceptance"})read_file({"end_line":195,"path":"src/devops_cli/roadmap/stage/stage_change.py","start_line":150})read_file({"end_line":850,"path":"src/devops_cli/roadmap/stage/stage_change.py","start_line":800})list_files({"directory":"tests/roadmap"})search_code({"text":"plan_recheck"})read_spec({"capability":"roadmap-stage"})search_code({"text":"\"docs\""})search_code({"text":"ci docs"})search_code({"text":"networkpolicy.yaml"})search_code({"text":"egress_rules"})read_file({"end_line":340,"path":"tests/commands/k8s/test_k8s_logging_stack.py","start_line":280})read_file({"end_line":60,"path":"tests/commands/k8s/test_k8s_logging_stack.py","start_line":1})search_code({"text":"\"logging\""})search_code({"text":"k8s/logging"})callers({"symbol":"gate_arguments"})search_code({"text":"check_gates("})search_code({"text":"check_proposal("})search_code({"text":"check_proposal"})read_file({"end_line":440,"path":"src/devops_cli/roadmap/implement/implement.py","start_line":370})read_file({"end_line":410,"path":"src/devops_cli/roadmap/plan_check.py","start_line":350})search_code({"text":"def eval_replay_problems"})read_file({"end_line":180,"path":"src/devops_cli/roadmap/stage/stage_change.py","start_line":196})read_file({"end_line":1060,"path":"src/devops_cli/roadmap/stage/stage_change.py","start_line":1020})search_code({"text":"def _stale_test_path_problems"})read_file({"end_line":1000,"path":"src/devops_cli/roadmap/stage/stage_change.py","start_line":900})read_file({"end_line":1360,"path":"src/devops_cli/roadmap/stage/stage_change.py","start_line":1300})search_code({"text":"def _delta_problems"})read_file({"end_line":560,"path":"src/devops_cli/roadmap/stage/stage_change.py","start_line":430})read_file({"end_line":760,"path":"src/devops_cli/roadmap/stage/stage_change.py","start_line":660})read_file({"end_line":660,"path":"src/devops_cli/roadmap/stage/stage_change.py","start_line":590})search_code({"text":"networkpolicy.yaml"})references({"name":"networkpolicy.yaml"})read_file({"path":"src/devops_cli/roadmap/stage/stage_references.py"})read_file({"end_line":240,"path":"src/devops_cli/roadmap/stage/stage_eval_gates.py","start_line":150})read_file({"end_line":400,"path":"docs/ROADMAP_WORKFLOW.md","start_line":300})read_file({"end_line":395,"path":"docs/ROADMAP_WORKFLOW.md","start_line":375})read_file({"end_line":340,"path":"tests/roadmap/stage/test_roadmap_stage_request.py","start_line":240})read_file({"end_line":140,"path":"docs/roadmap/plan-recheck.md","start_line":95})Tool calls: callers=4 (empty=0), file_symbols=6 (empty=0), find_issues=1 (empty=0), list_files=6 (empty=0), read_file=54 (empty=0), read_spec=2 (empty=0), references=1 (empty=0), search_code=45 (empty=1)
The stage job read the open pull requests and their files in 1 GraphQL request(s).