sync: agent-ops from agentic-framework v1.1.188

This commit is contained in:
toki 2026-08-06 13:25:08 +09:00
parent 3e4704e777
commit 43f2b1599b
4 changed files with 27 additions and 226 deletions

View file

@ -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

View file

@ -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",

View file

@ -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:

View file

@ -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)