diff --git a/agent-ops/skills/common/orchestrate-agent-task-loop/SKILL.md b/agent-ops/skills/common/orchestrate-agent-task-loop/SKILL.md index 07857700..9d3ab558 100644 --- a/agent-ops/skills/common/orchestrate-agent-task-loop/SKILL.md +++ b/agent-ops/skills/common/orchestrate-agent-task-loop/SKILL.md @@ -112,7 +112,7 @@ Accept self-check completion only when `## Implementation Checklist` or its supp ## Work log - Keep one dispatcher-owned `WORK_LOG.md` per task group. -- Append chronological `START` and `FINISH` rows with KST time, task artifact, plan loop, role, attempt, selected agent/model display, result, and locator. +- Append chronological `START` and `FINISH` rows with UTC time, task artifact, plan loop, role, attempt, selected agent/model display, result, and locator. - Archive the group log as the next `work_log_N.log` only after every observed task in the group is verified complete and idle. - Work-log write or archive failure is a retryable control-plane failure and prevents exit `0`. diff --git a/agent-ops/skills/common/orchestrate-agent-task-loop/scripts/dispatch.py b/agent-ops/skills/common/orchestrate-agent-task-loop/scripts/dispatch.py index 2e9699cd..1a921fb5 100644 --- a/agent-ops/skills/common/orchestrate-agent-task-loop/scripts/dispatch.py +++ b/agent-ops/skills/common/orchestrate-agent-task-loop/scripts/dispatch.py @@ -17,7 +17,7 @@ import subprocess import sys import uuid from dataclasses import dataclass, field -from datetime import datetime, timedelta, timezone +from datetime import datetime, timezone from pathlib import Path from typing import Any @@ -153,7 +153,6 @@ DISPATCHER_CHILD_BOUNDARY_PROMPT = ( REPOSITORY_LANGUAGE_PROMPT = "Follow the repository's language and output rules." SELF_CHECK_PROMPT_PREFIX = REPOSITORY_LANGUAGE_PROMPT UTC = timezone.utc -KST = timezone(timedelta(hours=9)) DEFAULT_MAX_PARALLEL = 3 @@ -249,8 +248,8 @@ def now_iso() -> str: return datetime.now(timezone.utc).isoformat() -def work_log_now_kst() -> str: - return datetime.now(KST).strftime("%y-%m-%d %H:%M:%S") +def work_log_now_utc() -> str: + return datetime.now(UTC).strftime("%y-%m-%d %H:%M:%SZ") def sha256_file(path: Path | None) -> str: @@ -453,7 +452,7 @@ def append_work_log_event( return str(value).replace("|", r"\|").replace("\n", " ") stream.write( - f"| {sequence} | {work_log_now_kst()} | {cell(event)} | " + f"| {sequence} | {work_log_now_utc()} | {cell(event)} | " f"{cell(task_name)} | " f"{loop} | {cell(role)} | {attempt} | {cell(model)} | {cell(result)} | " f"{cell(locator.resolve())} |\n" diff --git a/agent-ops/skills/common/orchestrate-agent-task-loop/tests/test_dispatch.py b/agent-ops/skills/common/orchestrate-agent-task-loop/tests/test_dispatch.py index 8311eac6..e2cd102b 100644 --- a/agent-ops/skills/common/orchestrate-agent-task-loop/tests/test_dispatch.py +++ b/agent-ops/skills/common/orchestrate-agent-task-loop/tests/test_dispatch.py @@ -261,13 +261,6 @@ class RuntimeCatalogDispatcherTests(unittest.TestCase): class GenericDispatcherContractTests(unittest.TestCase): - def test_work_log_timestamp_uses_compact_kst_format(self): - fixed_kst = datetime(2026, 7, 26, 7, 40, 15, tzinfo=dispatch.KST) - with mock.patch.object(dispatch, "datetime") as datetime_mock: - datetime_mock.now.return_value = fixed_kst - self.assertEqual(dispatch.work_log_now_kst(), "26-07-26 07:40:15") - datetime_mock.now.assert_called_once_with(dispatch.KST) - def test_parallel_limit_contract(self): self.assertEqual(dispatch.validated_max_parallel(0), 0) self.assertEqual(dispatch.validated_max_parallel(3), 3) 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 ff38a375..753b968e 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,14 +426,17 @@ 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 = ( - workspace - / "agent-ops" - / "skills" - / "common" - / "orchestrate-agent-task-loop" - / "scripts" - / "dispatch.py" + 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}") @@ -455,7 +458,17 @@ def dispatcher_command( "--task-group", task_group, ] - command.extend(["--execution-catalog", execution_catalog]) + 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 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 ff6655a4..e31a2fa9 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,7 +51,7 @@ class PrepareWorkspaceTest(unittest.TestCase): Path("/tmp/example/sample-feature-worktree"), ) - def test_dispatcher_uses_common_runtime_only(self) -> None: + def test_dispatcher_prefers_project_override_and_private_pair(self) -> None: with tempfile.TemporaryDirectory() as raw: workspace = Path(raw) common = ( @@ -70,15 +70,24 @@ class PrepareWorkspaceTest(unittest.TestCase): 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_always_injects_catalog(self) -> None: + 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, @@ -86,7 +95,15 @@ class PrepareWorkspaceTest(unittest.TestCase): 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(