diff --git a/agent-ops/skills/common/prepare-epic-work-items/scripts/run_epic_cycle.py b/agent-ops/skills/common/prepare-epic-work-items/scripts/run_epic_cycle.py index cfb4dd01..bc581d1b 100755 --- a/agent-ops/skills/common/prepare-epic-work-items/scripts/run_epic_cycle.py +++ b/agent-ops/skills/common/prepare-epic-work-items/scripts/run_epic_cycle.py @@ -343,7 +343,12 @@ def validate_pairs( pairs.append((plan, review, header)) union.update(target_ids) contents = plan.read_text(encoding="utf-8") + "\n" + review.read_text(encoding="utf-8") - if "[TODO" in contents or "" in contents: + unresolved_contents = contents.replace("`m-`", "") + if ( + "[TODO" in unresolved_contents + or "" in unresolved_contents + or "" in unresolved_contents + ): raise CycleError(f"unresolved template token: {plan.parent}") if dispatcher.is_file(): run( diff --git a/agent-ops/skills/common/prepare-epic-work-items/tests/test_run_epic_cycle.py b/agent-ops/skills/common/prepare-epic-work-items/tests/test_run_epic_cycle.py index 8650d300..ac3c8ea0 100644 --- a/agent-ops/skills/common/prepare-epic-work-items/tests/test_run_epic_cycle.py +++ b/agent-ops/skills/common/prepare-epic-work-items/tests/test_run_epic_cycle.py @@ -286,6 +286,28 @@ class EpicCycleContractTest(unittest.TestCase): self.assertEqual(len(pairs), 1) self.assertEqual(union, {"inside"}) + def test_validate_pair_rejects_bare_milestone_slug_placeholder(self) -> None: + with tempfile.TemporaryDirectory() as raw: + workspace = Path(raw) + command(workspace, "git", "init") + task = workspace / "agent-task" / "m-sample" / "01_work" + task.mkdir(parents=True) + header = ( + "\n" + ) + (task / "PLAN-local-G01.md").write_text( + header + "# Plan\n\nPath: agent-task/m-/\n", + encoding="utf-8", + ) + (task / "CODE_REVIEW-local-G01.md").write_text( + header + "# Review\n", + encoding="utf-8", + ) + + with self.assertRaisesRegex(MODULE.CycleError, "unresolved template token"): + MODULE.validate_pairs(workspace, "m-sample", {"inside"}) + def test_validate_pair_rejects_foreign_pair_outside_selected_batch(self) -> None: with tempfile.TemporaryDirectory() as raw: workspace = Path(raw)