fix(agent-ops): Epic 준비 재시도 복구 정보를 보강한다
This commit is contained in:
parent
ba9c0717cf
commit
26bd023c86
2 changed files with 80 additions and 0 deletions
|
|
@ -350,6 +350,15 @@ def validate_pairs(
|
|||
or "<milestone-slug>" in unresolved_contents
|
||||
):
|
||||
raise CycleError(f"unresolved template token: {plan.parent}")
|
||||
for path in (plan, review):
|
||||
whitespace = run(
|
||||
["git", "diff", "--no-index", "--check", "--", "/dev/null", str(path)],
|
||||
cwd=workspace,
|
||||
check=False,
|
||||
)
|
||||
if whitespace.returncode not in {0, 1}:
|
||||
detail = (whitespace.stderr or whitespace.stdout or "").strip()
|
||||
raise CycleError(f"plan whitespace validation failed: {path}: {detail}")
|
||||
if dispatcher.is_file():
|
||||
run(
|
||||
[sys.executable, str(dispatcher), "--workspace", str(workspace), "--validate-plan", str(plan)],
|
||||
|
|
@ -379,6 +388,16 @@ def active_task_user_reviews(workspace: Path, task_group: str) -> list[Path]:
|
|||
return sorted(root.glob("USER_REVIEW.md")) + sorted(root.glob("*/USER_REVIEW.md"))
|
||||
|
||||
|
||||
def active_task_finalization_artifacts(workspace: Path, task_group: str) -> list[Path]:
|
||||
root = workspace / "agent-task" / task_group
|
||||
if not root.exists():
|
||||
return []
|
||||
values: set[Path] = set()
|
||||
for pattern in ("complete.log", "plan_*.log", "code_review_*.log"):
|
||||
values.update(root.rglob(pattern))
|
||||
return sorted(values)
|
||||
|
||||
|
||||
def sdd_user_review(workspace: Path, phase_slug: str, milestone_slug: str) -> Path:
|
||||
return workspace / "agent-roadmap" / "sdd" / phase_slug / milestone_slug / "USER_REVIEW.md"
|
||||
|
||||
|
|
@ -392,6 +411,7 @@ def stage_prompt(
|
|||
task_group: str,
|
||||
base_head: str,
|
||||
checkpoint_head: str | None,
|
||||
recovery_reason: str | None = None,
|
||||
) -> str:
|
||||
common = f"""You are a fresh child agent launched for one bounded Epic preparation stage, not the caller or monitor.
|
||||
Work only in {workspace}.
|
||||
|
|
@ -403,6 +423,13 @@ Target Epic: [{epic.epic_id}] {epic.title}
|
|||
Allowed Milestone Task ids: {','.join(epic.task_ids)}
|
||||
Active task group: agent-task/{task_group}
|
||||
Keep every change inside this Epic and preserve user changes. Final in Korean.
|
||||
"""
|
||||
if recovery_reason:
|
||||
common += f"""
|
||||
This is an explicit retry after the parent runtime rejected the previous stage output.
|
||||
Resolve the exact recovery condition below before finishing, while preserving valid work:
|
||||
{recovery_reason}
|
||||
Do not repeat a forbidden finalization action merely because its artifact is already present.
|
||||
"""
|
||||
if stage == "materialize":
|
||||
return common + f"""
|
||||
|
|
@ -760,6 +787,15 @@ def cycle(args: argparse.Namespace) -> int:
|
|||
if stage in {"materialize", "refine"}
|
||||
else reviewer_target
|
||||
)
|
||||
recovery_details: list[str] = []
|
||||
if args.retry and prior_cycle_status == "failed":
|
||||
recovery_details.append(str(state.get("reason", "unknown parent failure")))
|
||||
finalized = active_task_finalization_artifacts(workspace, task_group)
|
||||
if finalized:
|
||||
recovery_details.append(
|
||||
"Remove forbidden preparation-time finalization artifacts and restore valid active pairs: "
|
||||
+ ",".join(str(path.relative_to(workspace)) for path in finalized)
|
||||
)
|
||||
prompt = stage_prompt(
|
||||
stage=stage,
|
||||
workspace=workspace,
|
||||
|
|
@ -768,6 +804,7 @@ def cycle(args: argparse.Namespace) -> int:
|
|||
task_group=task_group,
|
||||
base_head=str(state["base_head"]),
|
||||
checkpoint_head=state.get("checkpoint_head"),
|
||||
recovery_reason="\n".join(recovery_details) or None,
|
||||
)
|
||||
result_path = state_root / "attempts" / f"{stage}.json"
|
||||
state.update(
|
||||
|
|
@ -824,6 +861,12 @@ def cycle(args: argparse.Namespace) -> int:
|
|||
)
|
||||
if refreshed_scope.task_ids != epic.task_ids:
|
||||
raise CycleError(f"target Epic Task ids changed unexpectedly: stage={stage}")
|
||||
finalized = active_task_finalization_artifacts(workspace, task_group)
|
||||
if finalized:
|
||||
raise CycleError(
|
||||
"preparation agent created forbidden finalization artifacts: "
|
||||
+ ",".join(str(path.relative_to(workspace)) for path in finalized)
|
||||
)
|
||||
pairs, task_union = validate_pairs(
|
||||
workspace,
|
||||
task_group,
|
||||
|
|
|
|||
|
|
@ -308,6 +308,43 @@ class EpicCycleContractTest(unittest.TestCase):
|
|||
with self.assertRaisesRegex(MODULE.CycleError, "unresolved template token"):
|
||||
MODULE.validate_pairs(workspace, "m-sample", {"inside"})
|
||||
|
||||
def test_validate_pair_rejects_untracked_whitespace_error(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 = (
|
||||
"<!-- task=m-sample/01_work plan=0 tag=TEST "
|
||||
"milestone-task=inside -->\n"
|
||||
)
|
||||
(task / "PLAN-local-G01.md").write_text(
|
||||
header + "# Plan\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
(task / "CODE_REVIEW-local-G01.md").write_text(
|
||||
header + "# Review\n\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
with self.assertRaisesRegex(MODULE.CycleError, "whitespace validation failed"):
|
||||
MODULE.validate_pairs(workspace, "m-sample", {"inside"})
|
||||
|
||||
def test_retry_prompt_includes_parent_failure(self) -> None:
|
||||
prompt = MODULE.stage_prompt(
|
||||
stage="initial-review",
|
||||
workspace=Path("/workspace"),
|
||||
milestone=Path("/workspace/milestone.md"),
|
||||
epic=MODULE.Epic("sample", "Sample", ("inside",), ("inside",), ""),
|
||||
task_group="m-sample",
|
||||
base_head="abc123",
|
||||
checkpoint_head=None,
|
||||
recovery_reason="git diff --cached --check failed",
|
||||
)
|
||||
|
||||
self.assertIn("explicit retry", prompt)
|
||||
self.assertIn("git diff --cached --check failed", prompt)
|
||||
|
||||
def test_validate_pair_rejects_foreign_pair_outside_selected_batch(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as raw:
|
||||
workspace = Path(raw)
|
||||
|
|
|
|||
Loading…
Reference in a new issue