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 c01b0ff7..293caf65 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 @@ -621,7 +621,7 @@ def cycle(args: argparse.Namespace) -> int: emit( "EPIC_BATCH_VALIDATED", identity=identity, - terminal="EPIC_COMPLETED" if not epic.incomplete_ids else "EPIC_WORK_ITEMS_READY", + event="EPIC_COMPLETED" if not epic.incomplete_ids else "EPIC_WORK_ITEMS_READY", plans=len(pairs), ) return 0 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 f8ae86a1..5362b45f 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 @@ -1,15 +1,12 @@ from __future__ import annotations import importlib.util -import io -import json import os from pathlib import Path import subprocess import sys import tempfile import unittest -from contextlib import redirect_stdout from unittest import mock @@ -290,29 +287,6 @@ class EpicCycleContractTest(unittest.TestCase): state = MODULE.read_state(state_path) self.assertEqual(state["event"], "EPIC_WORK_ITEMS_READY") - validate_output = io.StringIO() - with redirect_stdout(validate_output): - validated = MODULE.main( - [ - "--workspace", - str(workspace), - "--milestone", - str(milestone.relative_to(workspace)), - "--epic", - "sample-epic", - "--execution-catalog", - "/runtime/catalog.json", - "--planner-target", - "planner-primary", - "--validate-only", - ] - ) - self.assertEqual(validated, 0) - validation_event = json.loads(validate_output.getvalue().strip()) - self.assertEqual(validation_event["event"], "EPIC_BATCH_VALIDATED") - self.assertEqual(validation_event["terminal"], "EPIC_WORK_ITEMS_READY") - self.assertEqual(validation_event["plans"], 1) - reused_in_larger_batch = MODULE.main( [ "--workspace", diff --git a/agent-ops/skills/common/prepare-milestone-workspace/scripts/prepare_workspace.py b/agent-ops/skills/common/prepare-milestone-workspace/scripts/prepare_workspace.py index 25f9c90d..6736e679 100755 --- a/agent-ops/skills/common/prepare-milestone-workspace/scripts/prepare_workspace.py +++ b/agent-ops/skills/common/prepare-milestone-workspace/scripts/prepare_workspace.py @@ -426,50 +426,11 @@ def epic_cycle_script(workspace: Path) -> Path: def dispatcher_script(workspace: Path) -> Path: - skills_root = workspace / "agent-ops" / "skills" - project_root = skills_root / "project" / "orchestrate-agent-task-loop" - project_dispatcher = project_root / "scripts" / "dispatch.py" - if project_dispatcher.is_file(): - private_root = skills_root / "private" / "orchestrate-agent-task-loop" - private_dispatcher = private_root / "scripts" / "dispatch.py" - if (private_root / "SKILL.md").is_file() and private_dispatcher.is_file(): - return private_dispatcher - return project_dispatcher - common_dispatcher = ( - skills_root / "common" / "orchestrate-agent-task-loop" / "scripts" / "dispatch.py" - ) - if not common_dispatcher.is_file(): - raise PreparationError(f"dispatcher script not found: {common_dispatcher}") - return common_dispatcher - - -def dispatcher_command( - *, - workspace: Path, - dispatcher: Path, - task_group: str, - execution_catalog: str, -) -> list[str]: - command = [ - sys.executable, - str(dispatcher), - "--workspace", - str(workspace), - "--task-group", - task_group, - ] - common_dispatcher = ( - workspace - / "agent-ops" - / "skills" - / "common" - / "orchestrate-agent-task-loop" - / "scripts" - / "dispatch.py" - ) - if dispatcher.resolve() == common_dispatcher.resolve(): - command.extend(["--execution-catalog", execution_catalog]) - return command + root = workspace / "agent-ops" / "skills" / "common" / "orchestrate-agent-task-loop" + path = root / "scripts" / "dispatch.py" + if not path.is_file(): + raise PreparationError(f"dispatcher script not found: {path}") + return path def epic_cycle_command( @@ -718,31 +679,6 @@ def coordinate_batch( } atomic_json(state_path, state) else: - runtime_identity_keys = ( - "execution_catalog", - "planner_target", - "review_target", - ) - stable_identity_keys = ( - "milestone", - "workspace", - "selected_epics", - "batch_task_ids", - ) - missing_runtime_identity = [ - key for key in runtime_identity_keys if key not in state - ] - if missing_runtime_identity and all( - state.get(key) == identity[key] for key in stable_identity_keys - ): - for key in missing_runtime_identity: - state[key] = identity[key] - atomic_json(state_path, state) - emit( - "BATCH_IDENTITY_MIGRATED", - fields=missing_runtime_identity, - milestone=milestone_slug, - ) mismatched = [key for key, expected in identity.items() if state.get(key) != expected] if mismatched and state.get("status") == "completed": state = { @@ -947,15 +883,18 @@ def coordinate_batch( dispatcher = dispatcher_script(workspace) task_group = f"m-{milestone_slug}" if not state.get("dispatcher_dry_run_done"): - dry_run_command = dispatcher_command( - workspace=workspace, - dispatcher=dispatcher, - task_group=task_group, - execution_catalog=args.execution_catalog, - ) - dry_run_command.append("--dry-run") dry_run = run( - dry_run_command, + [ + sys.executable, + str(dispatcher), + "--workspace", + str(workspace), + "--task-group", + task_group, + "--execution-catalog", + args.execution_catalog, + "--dry-run", + ], cwd=workspace, check=False, capture=False, @@ -968,12 +907,16 @@ def coordinate_batch( atomic_json(state_path, state) emit("DISPATCHER_DRY_RUN_FINISHED", task_group=task_group) - command = dispatcher_command( - workspace=workspace, - dispatcher=dispatcher, - task_group=task_group, - execution_catalog=args.execution_catalog, - ) + command = [ + sys.executable, + str(dispatcher), + "--workspace", + str(workspace), + "--task-group", + task_group, + "--execution-catalog", + args.execution_catalog, + ] if resume_blocked_dispatcher and args.retry: command.append("--retry-blocked") try: diff --git a/agent-ops/skills/common/prepare-milestone-workspace/tests/test_prepare_workspace.py b/agent-ops/skills/common/prepare-milestone-workspace/tests/test_prepare_workspace.py index 01f08b7e..1a799f62 100644 --- a/agent-ops/skills/common/prepare-milestone-workspace/tests/test_prepare_workspace.py +++ b/agent-ops/skills/common/prepare-milestone-workspace/tests/test_prepare_workspace.py @@ -51,60 +51,6 @@ class PrepareWorkspaceTest(unittest.TestCase): Path("/tmp/example/sample-feature-worktree"), ) - def test_dispatcher_prefers_project_override_and_private_pair(self) -> None: - with tempfile.TemporaryDirectory() as raw: - workspace = Path(raw) - common = ( - workspace - / "agent-ops/skills/common/orchestrate-agent-task-loop/scripts/dispatch.py" - ) - project = ( - workspace - / "agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py" - ) - private_root = ( - workspace / "agent-ops/skills/private/orchestrate-agent-task-loop" - ) - private = private_root / "scripts/dispatch.py" - for path in (common, project, private): - path.parent.mkdir(parents=True, exist_ok=True) - path.touch() - - self.assertEqual(MODULE.dispatcher_script(workspace), project) - - (private_root / "SKILL.md").touch() - self.assertEqual(MODULE.dispatcher_script(workspace), private) - - project.unlink() - self.assertEqual(MODULE.dispatcher_script(workspace), common) - - def test_dispatcher_command_injects_catalog_only_for_common_runtime(self) -> None: - workspace = Path("/repo") - common = ( - workspace - / "agent-ops/skills/common/orchestrate-agent-task-loop/scripts/dispatch.py" - ) - project = ( - workspace - / "agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py" - ) - - common_command = MODULE.dispatcher_command( - workspace=workspace, - dispatcher=common, - task_group="m-sample", - execution_catalog="/runtime/catalog.json", - ) - project_command = MODULE.dispatcher_command( - workspace=workspace, - dispatcher=project, - task_group="m-sample", - execution_catalog="/runtime/catalog.json", - ) - - self.assertIn("--execution-catalog", common_command) - self.assertNotIn("--execution-catalog", project_command) - def test_epic_document_range_is_one_based_and_inclusive(self) -> None: epics = MODULE.parse_epics( """## 기능 @@ -622,68 +568,6 @@ class PrepareWorkspaceTest(unittest.TestCase): self.assertEqual(result, 3) popen.assert_not_called() - def test_legacy_batch_adopts_missing_runtime_identity_on_resume(self) -> None: - with tempfile.TemporaryDirectory() as raw: - root = Path(raw) - workspace = root / "workspace" - common = root / "git-common" - milestone = workspace / "agent-roadmap/phase/phase-one/milestones/sample.md" - milestone.parent.mkdir(parents=True) - milestone.write_text( - "# Milestone: Sample\n\n## 기능\n\n" - "### Epic: [first] First\n\n- [ ] [first-task] first\n", - encoding="utf-8", - ) - args = MODULE.apply_defaults( - MODULE.parser().parse_args( - [ - "--repo", - str(workspace), - "--milestone", - str(milestone), - "--workspace", - str(workspace), - "--epics", - "1..1", - ] - ) - ) - state_path = common / "milestone-work-preparation" / "sample" / "batch-state.json" - MODULE.atomic_json( - state_path, - { - "milestone": str(milestone), - "workspace": str(workspace), - "selected_epics": ["first"], - "batch_task_ids": ["first-task"], - "status": "dispatching", - "epic_events": {"first": "EPIC_WORK_ITEMS_READY"}, - "dispatcher_pid": os.getpid(), - "dispatcher_process_start_token": MODULE.process_start_token(os.getpid()), - }, - ) - - output = io.StringIO() - with contextlib.redirect_stdout(output), mock.patch.object( - MODULE.subprocess, "Popen" - ) as popen: - result = MODULE.coordinate_batch( - args=args, - workspace=workspace, - milestone=milestone, - milestone_slug="sample", - phase_slug="phase-one", - common=common, - ) - - state = MODULE.read_json(state_path) - self.assertEqual(result, 3) - self.assertEqual(state["execution_catalog"], "/runtime/catalog.json") - self.assertEqual(state["planner_target"], "planner-primary") - self.assertEqual(state["review_target"], "planner-primary") - self.assertIn('"event": "BATCH_IDENTITY_MIGRATED"', output.getvalue()) - popen.assert_not_called() - def test_completed_batch_can_start_a_different_epic_selection(self) -> None: with tempfile.TemporaryDirectory() as raw: root = Path(raw)