diff --git a/--check b/--check deleted file mode 100644 index 3a3d321c..00000000 --- a/--check +++ /dev/null @@ -1,15 +0,0 @@ -# BEGIN Agent-Ops managed gitignore -!agent-task/ -!agent-task/**/ -!agent-task/**/*.md -!agent-task/**/*.log -agent-roadmap/current.md -# END Agent-Ops managed gitignore - -# BEGIN Agent-Ops managed gitignore -!agent-task/ -!agent-task/**/ -!agent-task/**/*.md -!agent-task/**/*.log -agent-roadmap/current.md -# END Agent-Ops managed gitignore diff --git a/TestRefreshApplyConcurrentRuntimeReaders2961420242/001/base.yaml b/TestRefreshApplyConcurrentRuntimeReaders2961420242/001/base.yaml deleted file mode 100644 index 1986f7d3..00000000 --- a/TestRefreshApplyConcurrentRuntimeReaders2961420242/001/base.yaml +++ /dev/null @@ -1,46 +0,0 @@ - -server: - listen: "127.0.0.1:41091" -bootstrap: - listen: "0.0.0.0:18080" - artifact_dir: "artifacts" -logging: - level: "error" -refresh: - enabled: false - listen: "127.0.0.1:19093" -openai: - enabled: true - listen: "127.0.0.1:41355" - provider_id: "test-provider" - adapter: "openai_compat" - target: "" -a2a: - listen: "0.0.0.0:8081" -metrics: - port: 0 -models: - - id: "qwen3.6:35b" - display_name: "Qwen Base" - providers: - prov-a: "served-qwen" -nodes: - - id: "node-1" - alias: "n1" - token: "tok-1" - adapters: - openai_compat_instances: - - name: "vllm-gpu" - enabled: true - provider: "vllm" - endpoint: "http://127.0.0.1:8000/v1" - providers: - - id: "prov-a" - type: "vllm" - category: "api" - adapter: "vllm-gpu" - models: ["served-qwen"] - health: "available" - capacity: 2 - max_queue: 4 - queue_timeout_ms: 5000 diff --git a/TestRefreshApplyConcurrentRuntimeReaders2961420242/001/candidate.yaml b/TestRefreshApplyConcurrentRuntimeReaders2961420242/001/candidate.yaml deleted file mode 100644 index 936be6a5..00000000 --- a/TestRefreshApplyConcurrentRuntimeReaders2961420242/001/candidate.yaml +++ /dev/null @@ -1,46 +0,0 @@ - -server: - listen: "127.0.0.1:41091" -bootstrap: - listen: "0.0.0.0:18080" - artifact_dir: "artifacts" -logging: - level: "error" -refresh: - enabled: false - listen: "127.0.0.1:19093" -openai: - enabled: true - listen: "127.0.0.1:41355" - provider_id: "test-provider" - adapter: "openai_compat" - target: "" -a2a: - listen: "0.0.0.0:8081" -metrics: - port: 0 -models: - - id: "qwen3.6:35b" - display_name: "Qwen Candidate" - providers: - prov-a: "served-qwen" -nodes: - - id: "node-1" - alias: "n1" - token: "tok-1" - adapters: - openai_compat_instances: - - name: "vllm-gpu" - enabled: true - provider: "vllm" - endpoint: "http://127.0.0.1:8000/v1" - providers: - - id: "prov-a" - type: "vllm" - category: "api" - adapter: "vllm-gpu" - models: ["served-qwen"] - health: "available" - capacity: 8 - max_queue: 4 - queue_timeout_ms: 5000 diff --git a/agent b/agent deleted file mode 100755 index 042e04e2..00000000 Binary files a/agent and /dev/null differ diff --git a/agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_select_execution_target.py.orig b/agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_select_execution_target.py.orig deleted file mode 100644 index d7660e2d..00000000 --- a/agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_select_execution_target.py.orig +++ /dev/null @@ -1,1077 +0,0 @@ -import copy -import importlib.util -import json -import subprocess -import sys -import unittest -from datetime import datetime -from pathlib import Path -from tempfile import TemporaryDirectory -from zoneinfo import ZoneInfo - - -SCRIPT = ( - Path(__file__).resolve().parents[1] - / "scripts" - / "select_execution_target.py" -) -SPEC = importlib.util.spec_from_file_location("select_execution_target", SCRIPT) -selector = importlib.util.module_from_spec(SPEC) -assert SPEC.loader is not None -sys.modules[SPEC.name] = selector -SPEC.loader.exec_module(selector) - -KST = ZoneInfo("Asia/Seoul") - - -def kst(hour: int, minute: int = 0, second: int = 0) -> datetime: - return datetime(2026, 7, 25, hour, minute, second, tzinfo=KST) - - -def write_task_file( - directory: Path, - kind: str, - lane: str, - grade: int, - *, - task: str = "grp/01_unit", - plan: int = 0, - tag: str = "API", - body: str = "body\n", -) -> Path: - path = Path(directory) / f"{kind}-{lane}-G{grade:02d}.md" - path.write_text( - f"\n\n# title\n\n{body}", - encoding="utf-8", - ) - return path - - -_DELETE = object() - - -def _apply_path(prior: dict, path: tuple, value) -> None: - *parents, last = path - node = prior - for key in parents: - node = node[key] - if value is _DELETE: - del node[last] - else: - node[last] = value - - -# (name, path into a valid initial decision, replacement or _DELETE) triples that -# each leave the top-level containers well-typed but break one nested -# field/type/enum the resume path reuses verbatim. -MALFORMED_NESTED_VARIANTS = [ - ("empty_candidate", ("candidates", 0), {}), - ("candidate_missing_quota_mode", ("candidates", 0, "quota_mode"), _DELETE), - ("candidate_bad_eligibility_enum", ("candidates", 0, "eligibility"), "maybe"), - ("candidate_bad_selfcheck_type", ("candidates", 0, "selfcheck_required"), "yes"), - ("candidate_rank_not_consecutive", ("candidates", 0, "candidate_rank"), 5), - ("candidates_empty_list", ("candidates",), []), - ("decision_missing_rule_id", ("decision", "rule_id"), _DELETE), - ("decision_bad_time_window_enum", ("decision", "time_window"), "bogus"), - ("decision_wrong_timezone", ("decision", "timezone"), "UTC"), - ("decision_bad_pinned_type", ("decision", "pinned"), "yes"), - ("decision_reason_codes_scalar", ("decision", "reason_codes"), "kst_day_window"), - ("quota_missing_mode", ("quota", "mode"), _DELETE), - ("quota_bad_mode_enum", ("quota", "mode"), "bogus"), - ("quota_bad_status_enum", ("quota", "status"), "maybe"), - ("quota_bad_snapshot_id_type", ("quota", "snapshot_id"), 5), -] - - -class SelectorContractTests(unittest.TestCase): - def test_worker_contract_shape_and_types(self): - with TemporaryDirectory() as tmp: - task_file = write_task_file(Path(tmp), "PLAN", "cloud", 7) - result = selector.select_execution_target( - task_file, evaluated_at=kst(12) - ) - self.assertEqual(result["schema_version"], "1.0") - self.assertEqual( - result["work_unit_id"], "grp/01_unit::plan-0::tag-API" - ) - self.assertEqual(result["stage"], "worker") - self.assertEqual(result["lane"], "cloud") - self.assertEqual(result["grade"], 7) - self.assertIsInstance(result["grade"], int) - self.assertEqual( - result["selected"], - { - "adapter": "claude", - "target": "claude-opus-4-8", - "execution_class": "cloud_model", - "selfcheck_required": False, - }, - ) - for key in ("rule_id", "policy_priority", "reason_codes", "pinned"): - self.assertIn(key, result["decision"]) - self.assertIs(result["decision"]["pinned"], False) - self.assertEqual(result["decision"]["timezone"], "Asia/Seoul") - self.assertEqual( - set(result["quota"]), - {"snapshot_id", "mode", "status", "source", "checked_at"}, - ) - self.assertEqual(result["transition"]["trigger"], "initial") - self.assertEqual(result["transition"]["context_transfer"], "none") - - def test_stage_inference_and_mismatch(self): - with TemporaryDirectory() as tmp: - plan_file = write_task_file(Path(tmp), "PLAN", "local", 5) - review_file = write_task_file(Path(tmp), "CODE_REVIEW", "local", 5) - self.assertEqual( - selector.select_execution_target( - plan_file, evaluated_at=kst(12) - )["stage"], - "worker", - ) - self.assertEqual( - selector.select_execution_target( - review_file, evaluated_at=kst(12) - )["stage"], - "review", - ) - with self.assertRaises(selector.SelectorInputError) as ctx: - selector.select_execution_target( - plan_file, stage="review", evaluated_at=kst(12) - ) - self.assertEqual(ctx.exception.code, "stage_mismatch") - with self.assertRaises(selector.SelectorInputError): - selector.select_execution_target( - review_file, stage="worker", evaluated_at=kst(12) - ) - - def test_invalid_filenames_and_grades_rejected(self): - with TemporaryDirectory() as tmp: - root = Path(tmp) - bad_names = [ - "NOTE-cloud-G07.md", - "PLAN-hybrid-G07.md", - "PLAN-cloud-G7.md", - "PLAN-cloud-G07.txt", - "PLAN-cloud-G00.md", - "PLAN-cloud-G11.md", - ] - for name in bad_names: - path = root / name - path.write_text( - "\n", encoding="utf-8" - ) - with self.subTest(name=name): - with self.assertRaises(selector.SelectorInputError): - selector.select_execution_target( - path, evaluated_at=kst(12) - ) - - def test_malformed_header_rejected(self): - with TemporaryDirectory() as tmp: - path = Path(tmp) / "PLAN-cloud-G05.md" - path.write_text("# no generation header\n", encoding="utf-8") - with self.assertRaises(selector.SelectorInputError) as ctx: - selector.select_execution_target(path, evaluated_at=kst(12)) - self.assertEqual(ctx.exception.code, "malformed_header") - - def test_work_unit_id_stable_across_body_changes(self): - with TemporaryDirectory() as tmp: - task_file = write_task_file( - Path(tmp), "PLAN", "cloud", 7, body="first body\n" - ) - first = selector.select_execution_target( - task_file, evaluated_at=kst(12) - )["work_unit_id"] - task_file.write_text( - "\n\n# title\n\n" - "a much longer body with different content\n", - encoding="utf-8", - ) - second = selector.select_execution_target( - task_file, evaluated_at=kst(12) - )["work_unit_id"] - self.assertEqual(first, second) - # A new plan/tag generation must yield a new identity. - changed = write_task_file(Path(tmp), "PLAN", "cloud", 7, plan=1) - self.assertNotEqual( - first, - selector.select_execution_target( - changed, evaluated_at=kst(12) - )["work_unit_id"], - ) - - def test_deterministic_output_for_fixed_clock(self): - with TemporaryDirectory() as tmp: - task_file = write_task_file(Path(tmp), "PLAN", "local", 8) - first = selector.to_json( - selector.select_execution_target(task_file, evaluated_at=kst(12)) - ) - second = selector.to_json( - selector.select_execution_target(task_file, evaluated_at=kst(12)) - ) - self.assertEqual(first, second) - - def test_repeated_input_is_byte_stable(self): - with TemporaryDirectory() as tmp: - task_file = write_task_file(Path(tmp), "CODE_REVIEW", "cloud", 9) - runs = [ - subprocess.run( - [ - sys.executable, - str(SCRIPT), - str(task_file), - "--evaluated-at", - "2026-07-25T12:00:00+09:00", - ], - capture_output=True, - check=True, - ) - for _ in range(2) - ] - self.assertEqual(runs[0].stdout, runs[1].stdout) - self.assertTrue(runs[0].stdout.strip()) - - def test_resume_pins_prior_target_across_time(self): - with TemporaryDirectory() as tmp: - task_file = write_task_file(Path(tmp), "PLAN", "local", 7) - daytime = selector.select_execution_target( - task_file, evaluated_at=kst(12) - ) - self.assertEqual(daytime["selected"]["adapter"], "agy") - # A fresh night initial would flip to Laguna; resume must not. - night_initial = selector.select_execution_target( - task_file, evaluated_at=kst(2) - ) - self.assertEqual(night_initial["selected"]["adapter"], "pi") - resumed = selector.select_execution_target( - task_file, - evaluated_at=kst(2), - transition="resume", - prior_decision=daytime, - ) - self.assertEqual(resumed["selected"], daytime["selected"]) - self.assertIs(resumed["decision"]["pinned"], True) - self.assertEqual(resumed["transition"]["trigger"], "resume") - self.assertEqual( - resumed["transition"]["previous_target"], - {"adapter": "agy", "target": "Gemini 3.6 Flash Medium"}, - ) - - def test_resume_requires_matching_prior_decision(self): - with TemporaryDirectory() as tmp: - task_file = write_task_file(Path(tmp), "PLAN", "local", 7) - with self.assertRaises(selector.SelectorInputError) as ctx: - selector.select_execution_target( - task_file, evaluated_at=kst(12), transition="resume" - ) - self.assertEqual( - ctx.exception.code, "resume_requires_prior_decision" - ) - other = selector.select_execution_target( - task_file, evaluated_at=kst(12) - ) - other["work_unit_id"] = "grp/other::plan-0::tag-API" - with self.assertRaises(selector.SelectorInputError) as ctx: - selector.select_execution_target( - task_file, - evaluated_at=kst(12), - transition="resume", - prior_decision=other, - ) - self.assertEqual(ctx.exception.code, "resume_work_unit_mismatch") - - def test_failover_transition_is_unsupported(self): - with TemporaryDirectory() as tmp: - task_file = write_task_file(Path(tmp), "PLAN", "local", 7) - with self.assertRaises(selector.SelectorInputError) as ctx: - selector.select_execution_target( - task_file, evaluated_at=kst(12), transition="failover" - ) - self.assertEqual(ctx.exception.code, "unsupported_transition") - - def test_cli_input_error_is_stderr_json_without_stdout(self): - with TemporaryDirectory() as tmp: - task_file = write_task_file(Path(tmp), "PLAN", "local", 7) - proc = subprocess.run( - [ - sys.executable, - str(SCRIPT), - str(task_file), - "--evaluated-at", - "2026-07-25T12:00:00+09:00", - "--transition", - "failover", - ], - capture_output=True, - text=True, - ) - self.assertEqual(proc.returncode, 2) - self.assertEqual(proc.stdout, "") - self.assertEqual( - json.loads(proc.stderr)["error"], "unsupported_transition" - ) - - -class SelectorRouteMatrixTests(unittest.TestCase): - def test_kst_boundary_routes_through_selector(self): - cases = [ - (kst(6, 59, 59), "pi", "iop/laguna-s:2.1"), - (kst(7, 0, 0), "agy", "Gemini 3.6 Flash Medium"), - (kst(22, 59, 59), "agy", "Gemini 3.6 Flash Medium"), - (kst(23, 0, 0), "pi", "iop/laguna-s:2.1"), - ] - with TemporaryDirectory() as tmp: - for grade in (7, 8): - task_file = write_task_file(Path(tmp), "PLAN", "local", grade) - for evaluated_at, adapter, target in cases: - with self.subTest(grade=grade, evaluated_at=evaluated_at): - result = selector.select_execution_target( - task_file, evaluated_at=evaluated_at - ) - self.assertEqual(result["selected"]["adapter"], adapter) - self.assertEqual(result["selected"]["target"], target) - - def test_worker_route_matrix_through_selector(self): - expected = { - "local": { - **{g: ("pi", "iop/ornith-fast", "local_model", True) - for g in range(1, 5)}, - **{g: ("pi", "iop/laguna-s:2.1", "local_model", True) - for g in range(5, 9)}, - }, - "cloud": { - **{g: ("claude", "sonnet", "cloud_model", False) - for g in range(1, 7)}, - 7: ("claude", "claude-opus-4-8", "cloud_model", False), - 8: ("claude", "claude-opus-4-8", "cloud_model", False), - 9: ("codex", "gpt-5.6-sol", "cloud_model", False), - 10: ("codex", "gpt-5.6-sol", "cloud_model", False), - }, - } - with TemporaryDirectory() as tmp: - for lane, grades in expected.items(): - for grade, route in grades.items(): - task_file = write_task_file( - Path(tmp), "PLAN", lane, grade - ) - with self.subTest(lane=lane, grade=grade): - sel = selector.select_execution_target( - task_file, evaluated_at=kst(12) - )["selected"] - self.assertEqual( - ( - sel["adapter"], - sel["target"], - sel["execution_class"], - sel["selfcheck_required"], - ), - route, - ) - - def test_local_g09_g10_require_cloud_lane_through_selector(self): - with TemporaryDirectory() as tmp: - for grade in (9, 10): - task_file = write_task_file(Path(tmp), "PLAN", "local", grade) - with self.subTest(kind="PLAN", grade=grade): - with self.assertRaisesRegex( - selector.SelectorInputError, - "route G09..G10 through cloud", - ): - selector.select_execution_target(task_file, evaluated_at=kst(12)) - - def test_review_route_matrix_through_selector(self): - bands = [("local", range(1, 11)), ("cloud", range(1, 11))] - with TemporaryDirectory() as tmp: - for lane, grades in bands: - for grade in grades: - task_file = write_task_file( - Path(tmp), "CODE_REVIEW", lane, grade - ) - with self.subTest(lane=lane, grade=grade): - sel = selector.select_execution_target( - task_file, evaluated_at=kst(12) - )["selected"] - self.assertEqual( - ( - sel["adapter"], - sel["target"], - sel["execution_class"], - ), - ("codex", "gpt-5.6-sol", "cloud_model"), - ) - self.assertFalse(sel["selfcheck_required"]) - - def test_each_route_has_one_ranked_candidate(self): - with TemporaryDirectory() as tmp: - cases = [ - ("PLAN", "local", 8), - ("PLAN", "cloud", 5), - ("CODE_REVIEW", "local", 8), - ("CODE_REVIEW", "cloud", 9), - ] - for kind, lane, grade in cases: - task_file = write_task_file(Path(tmp), kind, lane, grade) - candidates = selector.select_execution_target( - task_file, evaluated_at=kst(12) - )["candidates"] - self.assertEqual([c["candidate_rank"] for c in candidates], [1]) - - -class SelectorQuotaRepresentationTests(unittest.TestCase): - def test_quota_probe_tri_state(self): - snapshots = { - "exhausted": "exhausted", - "available": "available", - "unknown": "unknown", - } - with TemporaryDirectory() as tmp: - cloud = write_task_file(Path(tmp), "PLAN", "cloud", 7) - for name, status in snapshots.items(): - with self.subTest(status=name): - if status == "exhausted": - with self.assertRaises(selector.SelectorInputError) as ctx: - selector.select_execution_target( - cloud, - evaluated_at=kst(12), - quota_snapshot={ - "snapshot_id": f"probe-{name}", - "source": "iop-node quota-probe", - "checked_at": "2026-07-25T03:00:00+09:00", - "targets": [ - { - "adapter": "claude", - "target": "claude-opus-4-8", - "status": status, - } - ], - }, - ) - self.assertEqual(ctx.exception.code, "no_eligible_target") - continue - result = selector.select_execution_target( - cloud, - evaluated_at=kst(12), - quota_snapshot={ - "snapshot_id": f"probe-{name}", - "source": "iop-node quota-probe", - "checked_at": "2026-07-25T03:00:00+09:00", - "targets": [ - { - "adapter": "claude", - "target": "claude-opus-4-8", - "status": status, - } - ], - }, - ) - candidate = result["candidates"][0] - self.assertEqual(candidate["quota_status"], status) - self.assertEqual( - candidate["eligibility"], - "eligible", - ) - - def test_unrelated_cloud_snapshot_does_not_change_local_route(self): - snapshot = { - "snapshot_id": "unrelated-exhausted", - "source": "iop-node quota-probe", - "checked_at": "2026-07-25T03:00:00+09:00", - "targets": [ - { - "adapter": "agy", - "target": "unrelated-cloud-target", - "status": "exhausted", - } - ], - } - with TemporaryDirectory() as tmp: - task_file = write_task_file(Path(tmp), "PLAN", "local", 7) - result = selector.select_execution_target( - task_file, evaluated_at=kst(12), quota_snapshot=snapshot - ) - self.assertEqual(len(result["candidates"]), 1) - self.assertEqual(result["candidates"][0]["eligibility"], "eligible") - self.assertEqual( - result["selected"], - { - "adapter": "pi", - "target": "iop/laguna-s:2.1", - "execution_class": "local_model", - "selfcheck_required": True, - }, - ) - self.assertEqual(result["quota"]["status"], "not_applicable") - - def test_all_candidates_exhausted_returns_no_eligible_target(self): - snapshot = { - "snapshot_id": "opus-exhausted", - "source": "iop-node quota-probe", - "checked_at": "2026-07-25T03:00:00+09:00", - "targets": [ - { - "adapter": "claude", - "target": "claude-opus-4-8", - "status": "exhausted", - } - ], - } - with TemporaryDirectory() as tmp: - root = Path(tmp) - task_file = write_task_file(root, "PLAN", "cloud", 7) - with self.assertRaises(selector.SelectorInputError) as ctx: - selector.select_execution_target( - task_file, evaluated_at=kst(12), quota_snapshot=snapshot - ) - self.assertEqual(ctx.exception.code, "no_eligible_target") - - snapshot_path = root / "quota.json" - snapshot_path.write_text(json.dumps(snapshot), encoding="utf-8") - proc = subprocess.run( - [ - sys.executable, - str(SCRIPT), - str(task_file), - "--evaluated-at", - "2026-07-25T12:00:00+09:00", - "--quota-snapshot", - str(snapshot_path), - ], - capture_output=True, - text=True, - ) - self.assertEqual(proc.returncode, 2) - self.assertEqual(proc.stdout, "") - self.assertEqual(json.loads(proc.stderr)["error"], "no_eligible_target") - - def test_unknown_is_admitted_once_per_work_unit(self): - snapshot = { - "snapshot_id": "unknown-1", - "source": "iop-node quota-probe", - "checked_at": "2026-07-25T03:00:00+09:00", - "targets": [ - { - "adapter": "claude", - "target": "claude-opus-4-8", - "status": "unknown", - } - ], - } - with TemporaryDirectory() as tmp: - cloud = write_task_file(Path(tmp), "PLAN", "cloud", 7) - initial = selector.select_execution_target( - cloud, evaluated_at=kst(12), quota_snapshot=snapshot - ) - self.assertEqual(initial["candidates"][0]["eligibility"], "eligible") - # Resume consumes the persisted decision instead of evaluating a - # second unknown admission for the same task/plan/tag generation. - resumed = selector.select_execution_target( - cloud, - evaluated_at=kst(12), - transition="resume", - prior_decision=initial, - quota_snapshot={ - **snapshot, - "snapshot_id": "later-exhausted", - "targets": [ - { - "adapter": "claude", - "target": "claude-opus-4-8", - "status": "exhausted", - } - ], - }, - ) - self.assertEqual(resumed["quota"], initial["quota"]) - self.assertEqual(resumed["candidates"], initial["candidates"]) - - def test_local_route_does_not_call_probe(self): - with TemporaryDirectory() as tmp: - local = write_task_file(Path(tmp), "PLAN", "local", 3) - result = selector.select_execution_target( - local, - evaluated_at=kst(12), - quota_probe_command="probe must not be used for local", - ) - self.assertEqual(result["quota"]["mode"], "unbounded") - self.assertEqual(result["quota"]["status"], "not_applicable") - self.assertEqual(result["quota"]["source"], "local_unbounded") - - def test_generic_stderr_is_not_quota_evidence(self): - with TemporaryDirectory() as tmp: - cloud = write_task_file(Path(tmp), "PLAN", "cloud", 7) - result = selector.select_execution_target( - cloud, - evaluated_at=kst(12), - quota_probe_command="generic stderr: quota might be exhausted", - ) - self.assertEqual(result["quota"]["status"], "unknown") - self.assertEqual(result["candidates"][0]["eligibility"], "eligible") - - def test_quota_representation_without_snapshot(self): - with TemporaryDirectory() as tmp: - cloud = write_task_file(Path(tmp), "PLAN", "cloud", 7) - cloud_result = selector.select_execution_target( - cloud, evaluated_at=kst(12) - ) - self.assertEqual(cloud_result["quota"]["mode"], "bounded") - self.assertEqual(cloud_result["quota"]["status"], "unknown") - self.assertEqual( - cloud_result["quota"]["source"], - selector.DEFAULT_QUOTA_PROBE_COMMAND, - ) - - local = write_task_file(Path(tmp), "PLAN", "local", 3) - local_result = selector.select_execution_target( - local, evaluated_at=kst(12) - ) - self.assertEqual(local_result["quota"]["mode"], "unbounded") - self.assertEqual(local_result["quota"]["status"], "not_applicable") - - local_high = write_task_file(Path(tmp), "PLAN", "local", 7) - candidates = selector.select_execution_target( - local_high, evaluated_at=kst(12) - )["candidates"] - self.assertEqual(len(candidates), 1) - self.assertEqual(candidates[0]["adapter"], "pi") - self.assertEqual(candidates[0]["quota_status"], "not_applicable") - - def test_injected_snapshot_is_reflected(self): - snapshot = { - "snapshot_id": "snap-1", - "source": "usage-checker", - "checked_at": "2026-07-25T03:00:00+09:00", - "targets": [ - {"adapter": "claude", "target": "sonnet", "status": "available"} - ], - } - with TemporaryDirectory() as tmp: - cloud = write_task_file(Path(tmp), "PLAN", "cloud", 5) - result = selector.select_execution_target( - cloud, evaluated_at=kst(12), quota_snapshot=snapshot - ) - self.assertEqual(result["quota"]["status"], "available") - self.assertEqual(result["quota"]["snapshot_id"], "snap-1") - self.assertEqual(result["quota"]["source"], "usage-checker") - self.assertEqual( - result["candidates"][0]["quota_status"], "available" - ) - - -class SelectorNestedInputContractTests(unittest.TestCase): - def test_resume_rejects_incomplete_selected_schema(self): - with TemporaryDirectory() as tmp: - task_file = write_task_file(Path(tmp), "PLAN", "local", 7) - prior = selector.select_execution_target( - task_file, evaluated_at=kst(12) - ) - # Reproduce the prior loop: a selected with only adapter/target must - # no longer flow through as a "successful" resume schema. - prior["selected"] = { - "adapter": prior["selected"]["adapter"], - "target": prior["selected"]["target"], - } - with self.assertRaises(selector.SelectorInputError) as ctx: - selector.select_execution_target( - task_file, - evaluated_at=kst(12), - transition="resume", - prior_decision=prior, - ) - self.assertEqual(ctx.exception.code, "malformed_prior_decision") - - def test_resume_rejects_selected_candidate_integrity_mismatches(self): - with TemporaryDirectory() as tmp: - task_file = write_task_file(Path(tmp), "PLAN", "local", 7) - base = selector.select_execution_target( - task_file, evaluated_at=kst(12) - ) - variants = { - "supported_but_different_target": ( - "target", - "iop/ornith-fast", - ), - "different_adapter": ("adapter", "claude"), - "different_execution_class": ( - "execution_class", - "cloud_model", - ), - "different_selfcheck": ("selfcheck_required", False), - } - for name, (field, value) in variants.items(): - with self.subTest(variant=name): - prior = copy.deepcopy(base) - prior["selected"][field] = value - with self.assertRaises(selector.SelectorInputError) as ctx: - selector.select_execution_target( - task_file, - evaluated_at=kst(12), - transition="resume", - prior_decision=prior, - ) - self.assertEqual( - ctx.exception.code, "malformed_prior_decision" - ) - - def test_resume_requires_exactly_one_eligible_selected_candidate(self): - with TemporaryDirectory() as tmp: - task_file = write_task_file(Path(tmp), "PLAN", "local", 7) - base = selector.select_execution_target( - task_file, evaluated_at=kst(12) - ) - - ineligible = copy.deepcopy(base) - ineligible["candidates"][0].update( - { - "quota_status": "exhausted", - "eligibility": "ineligible", - "rejection_reason": "quota_exhausted", - } - ) - duplicate = copy.deepcopy(base) - duplicate_candidate = copy.deepcopy(duplicate["candidates"][0]) - duplicate_candidate["candidate_rank"] = 2 - duplicate["candidates"].append(duplicate_candidate) - - for name, prior in ( - ("matching_candidate_ineligible", ineligible), - ("duplicate_eligible_match", duplicate), - ): - with self.subTest(variant=name): - with self.assertRaises(selector.SelectorInputError) as ctx: - selector.select_execution_target( - task_file, - evaluated_at=kst(12), - transition="resume", - prior_decision=prior, - ) - self.assertEqual( - ctx.exception.code, "malformed_prior_decision" - ) - - def test_resume_accepts_legacy_time_window(self): - with TemporaryDirectory() as tmp: - task_file = write_task_file(Path(tmp), "PLAN", "local", 7) - prior = selector.select_execution_target( - task_file, evaluated_at=kst(12) - ) - prior["decision"]["time_window"] = "kst-day-[07:00,23:00)" - resumed = selector.select_execution_target( - task_file, - evaluated_at=kst(2), - transition="resume", - prior_decision=prior, - ) - self.assertEqual(resumed["selected"], prior["selected"]) - self.assertEqual( - resumed["decision"]["time_window"], - "kst-day-[07:00,23:00)", - ) - self.assertIs(resumed["decision"]["pinned"], True) - - def test_resume_rejects_malformed_nested_prior_schema_variants(self): - with TemporaryDirectory() as tmp: - task_file = write_task_file(Path(tmp), "PLAN", "local", 7) - base = selector.select_execution_target( - task_file, evaluated_at=kst(12) - ) - # Sanity: the untouched decision resumes cleanly. - self.assertEqual( - selector.select_execution_target( - task_file, - evaluated_at=kst(12), - transition="resume", - prior_decision=copy.deepcopy(base), - )["selected"], - base["selected"], - ) - for name, path, value in MALFORMED_NESTED_VARIANTS: - with self.subTest(variant=name): - prior = copy.deepcopy(base) - _apply_path(prior, path, value) - with self.assertRaises(selector.SelectorInputError) as ctx: - selector.select_execution_target( - task_file, - evaluated_at=kst(12), - transition="resume", - prior_decision=prior, - ) - self.assertEqual( - ctx.exception.code, "malformed_prior_decision" - ) - - def test_cli_deeply_malformed_prior_uses_json_error_envelope(self): - with TemporaryDirectory() as tmp: - root = Path(tmp) - task_file = write_task_file(root, "PLAN", "local", 7) - prior = selector.select_execution_target( - task_file, evaluated_at=kst(12) - ) - # Containers stay well-typed object/list; only a nested enum is bad. - prior["quota"]["mode"] = "bogus" - prior_path = root / "prior.json" - prior_path.write_text(json.dumps(prior), encoding="utf-8") - proc = subprocess.run( - [ - sys.executable, - str(SCRIPT), - str(task_file), - "--evaluated-at", - "2026-07-25T12:00:00+09:00", - "--transition", - "resume", - "--prior-decision", - str(prior_path), - ], - capture_output=True, - text=True, - ) - self.assertEqual(proc.returncode, 2) - self.assertEqual(proc.stdout, "") - self.assertEqual( - json.loads(proc.stderr)["error"], "malformed_prior_decision" - ) - - def test_cli_malformed_prior_uses_json_error_envelope(self): - with TemporaryDirectory() as tmp: - root = Path(tmp) - task_file = write_task_file(root, "PLAN", "local", 7) - prior = selector.select_execution_target( - task_file, evaluated_at=kst(12) - ) - # A scalar where a nested object is required must not reach a raw - # TypeError/AttributeError traceback. - prior["decision"] = 1 - prior_path = root / "prior.json" - prior_path.write_text(json.dumps(prior), encoding="utf-8") - proc = subprocess.run( - [ - sys.executable, - str(SCRIPT), - str(task_file), - "--evaluated-at", - "2026-07-25T12:00:00+09:00", - "--transition", - "resume", - "--prior-decision", - str(prior_path), - ], - capture_output=True, - text=True, - ) - self.assertEqual(proc.returncode, 2) - self.assertEqual(proc.stdout, "") - self.assertEqual( - json.loads(proc.stderr)["error"], "malformed_prior_decision" - ) - - def test_cli_malformed_quota_uses_json_error_envelope(self): - with TemporaryDirectory() as tmp: - root = Path(tmp) - task_file = write_task_file(root, "PLAN", "cloud", 5) - cases = { - # A bare array instead of the snapshot object. - "array_snapshot": [ - { - "adapter": "claude", - "target": "sonnet", - "status": "available", - } - ], - # A target entry missing the required status field. - "invalid_target_entry": { - "targets": [{"adapter": "claude", "target": "sonnet"}] - }, - } - for name, snapshot in cases.items(): - quota_path = root / f"quota_{name}.json" - quota_path.write_text(json.dumps(snapshot), encoding="utf-8") - with self.subTest(case=name): - proc = subprocess.run( - [ - sys.executable, - str(SCRIPT), - str(task_file), - "--evaluated-at", - "2026-07-25T12:00:00+09:00", - "--quota-snapshot", - str(quota_path), - ], - capture_output=True, - text=True, - ) - self.assertEqual(proc.returncode, 2) - self.assertEqual(proc.stdout, "") - self.assertEqual( - json.loads(proc.stderr)["error"], - "malformed_quota_snapshot", - ) - - -class SelectorIdentityAndQuotaRoundtripTests(unittest.TestCase): - _VALID_TARGETS = [ - {"adapter": "claude", "target": "sonnet", "status": "available"} - ] - - def test_resume_rejects_unhashable_stage_and_lane_types(self): - with TemporaryDirectory() as tmp: - task_file = write_task_file(Path(tmp), "PLAN", "local", 7) - base = selector.select_execution_target( - task_file, evaluated_at=kst(12) - ) - # list/dict identity values must be normalized to a stable selector - # error instead of leaking a raw unhashable-type TypeError/exit 1. - for field, unhashable in (("stage", []), ("lane", {})): - with self.subTest(field=field): - prior = copy.deepcopy(base) - prior[field] = unhashable - with self.assertRaises(selector.SelectorInputError) as ctx: - selector.select_execution_target( - task_file, - evaluated_at=kst(12), - transition="resume", - prior_decision=prior, - ) - self.assertEqual( - ctx.exception.code, "malformed_prior_decision" - ) - - def test_quota_metadata_is_validated_before_initial_output(self): - with TemporaryDirectory() as tmp: - cloud = write_task_file(Path(tmp), "PLAN", "cloud", 5) - snapshot_cases = { - "numeric_snapshot_id": { - "snapshot_id": 5, - "targets": self._VALID_TARGETS, - }, - "numeric_checked_at": { - "checked_at": 1690000000, - "targets": self._VALID_TARGETS, - }, - "array_source": { - "source": ["usage-checker"], - "targets": self._VALID_TARGETS, - }, - "empty_source": { - "source": "", - "targets": self._VALID_TARGETS, - }, - } - for name, snapshot in snapshot_cases.items(): - with self.subTest(case=name): - with self.assertRaises(selector.SelectorInputError) as ctx: - selector.select_execution_target( - cloud, - evaluated_at=kst(12), - quota_snapshot=snapshot, - ) - self.assertEqual( - ctx.exception.code, "malformed_quota_snapshot" - ) - # An empty probe command would emit an empty quota.source that the - # resume validator rejects, so it must fail before any success JSON. - with self.assertRaises(selector.SelectorInputError) as ctx: - selector.select_execution_target( - cloud, evaluated_at=kst(12), quota_probe_command="" - ) - self.assertEqual( - ctx.exception.code, "invalid_quota_probe_command" - ) - - def test_valid_quota_initial_output_resumes(self): - snapshots = { - "no_snapshot": None, - "targets_only": {"targets": copy.deepcopy(self._VALID_TARGETS)}, - "full_metadata": { - "snapshot_id": "snap-1", - "source": "usage-checker", - "checked_at": "2026-07-25T03:00:00+09:00", - "targets": copy.deepcopy(self._VALID_TARGETS), - }, - } - with TemporaryDirectory() as tmp: - cloud = write_task_file(Path(tmp), "PLAN", "cloud", 5) - for name, snapshot in snapshots.items(): - with self.subTest(case=name): - initial = selector.select_execution_target( - cloud, evaluated_at=kst(12), quota_snapshot=snapshot - ) - # A daytime initial must resume verbatim at night without - # being rejected by its own prior-decision validator. - resumed = selector.select_execution_target( - cloud, - evaluated_at=kst(2), - transition="resume", - prior_decision=copy.deepcopy(initial), - ) - self.assertEqual(resumed["selected"], initial["selected"]) - self.assertEqual(resumed["quota"], initial["quota"]) - self.assertIs(resumed["decision"]["pinned"], True) - - def test_cli_malformed_identity_and_quota_metadata_use_json_error_envelope( - self, - ): - with TemporaryDirectory() as tmp: - root = Path(tmp) - task_file = write_task_file(root, "PLAN", "cloud", 5) - prior = selector.select_execution_target( - task_file, evaluated_at=kst(12) - ) - prior["stage"] = [] # unhashable identity type - prior_path = root / "prior.json" - prior_path.write_text(json.dumps(prior), encoding="utf-8") - snapshot_path = root / "quota.json" - snapshot_path.write_text( - json.dumps( - { - "snapshot_id": 5, - "targets": [ - { - "adapter": "claude", - "target": "sonnet", - "status": "available", - } - ], - } - ), - encoding="utf-8", - ) - cases = [ - ( - [ - "--transition", - "resume", - "--prior-decision", - str(prior_path), - ], - "malformed_prior_decision", - ), - ( - ["--quota-snapshot", str(snapshot_path)], - "malformed_quota_snapshot", - ), - ( - ["--quota-probe-command", ""], - "invalid_quota_probe_command", - ), - ] - for extra, code in cases: - with self.subTest(error=code): - proc = subprocess.run( - [ - sys.executable, - str(SCRIPT), - str(task_file), - "--evaluated-at", - "2026-07-25T12:00:00+09:00", - *extra, - ], - capture_output=True, - text=True, - ) - self.assertEqual(proc.returncode, 2) - self.assertEqual(proc.stdout, "") - self.assertEqual(json.loads(proc.stderr)["error"], code) - - -if __name__ == "__main__": - unittest.main() diff --git a/apps/edge/cmd/edge/edge.yaml b/apps/edge/cmd/edge/edge.yaml deleted file mode 100644 index 14b134b4..00000000 --- a/apps/edge/cmd/edge/edge.yaml +++ /dev/null @@ -1,30 +0,0 @@ -edge: - id: "edge-local" - name: "Local Edge" - -server: - listen: "0.0.0.0:9090" - advertise_host: "" - -bootstrap: - listen: "0.0.0.0:18080" - artifact_base_url: "" - artifact_dir: "artifacts" - -tls: - enabled: false - -logging: - level: "info" - pretty: false - path: "" - -metrics: - port: 19092 - -control_plane: - enabled: false - wire_addr: "" - reconnect_interval_sec: 5 - -nodes: [] diff --git a/apps/node/internal/bootstrap/iop.db b/apps/node/internal/bootstrap/iop.db deleted file mode 100644 index a0e97f5a..00000000 Binary files a/apps/node/internal/bootstrap/iop.db and /dev/null differ diff --git a/debug_trace.py b/debug_trace.py deleted file mode 100644 index cf831743..00000000 --- a/debug_trace.py +++ /dev/null @@ -1,110 +0,0 @@ -import sys, json, tempfile, asyncio -from pathlib import Path -from datetime import datetime, timezone, timedelta -from unittest import mock - -sys.path.insert(0, 'agent-ops/skills/project/orchestrate-agent-loop/scripts') -sys.path.insert(0, 'agent-ops/skills/project/orchestrate-agent-loop/tests') -import dispatch - - -async def main(): - with tempfile.TemporaryDirectory() as temporary: - workspace = Path(temporary) - (workspace / '.git').mkdir() - directory = workspace / 'agent-task' / 'route' / '01_blocked' - directory.mkdir(parents=True) - header = '\n' - (directory / 'PLAN-local-G07.md').write_text(header, encoding='utf-8') - (directory / 'CODE_REVIEW-local-G07.md').write_text(header, encoding='utf-8') - t_blocked = dispatch.scan_tasks(workspace, None)[0] - store = dispatch.StateStore(workspace) - - nighttime = datetime(2026, 7, 26, 23, 0, 0, tzinfo=timezone(timedelta(hours=9))) - selector = dispatch._selector_module() - - d_blocked, spec_blocked = dispatch.persisted_execution_decision(store, t_blocked, stage='worker', evaluated_at=nighttime) - - attempt_dir = workspace / 'attempt-loc' - attempt_dir.mkdir(parents=True, exist_ok=True) - loc_path = attempt_dir / 'locator.json' - stream_log = attempt_dir / 'stream.log' - stream_log.write_text('sample stream log', encoding='utf-8') - norm_log = attempt_dir / 'normalized-output.log' - norm_log.write_text('sample normalized output', encoding='utf-8') - loc_path.write_text(json.dumps({ - 'workspace': str(workspace.resolve()), - 'task': t_blocked.name, - 'plan_path': str(t_blocked.plan.resolve()), - 'stream_log': str(stream_log.resolve()), - 'normalized_output_log': str(norm_log.resolve()), - }), encoding='utf-8') - - store.update_task(t_blocked, blocked=f'worker failure provider-quota locator={loc_path}', blocker_evidence={ - 'role': 'worker', 'failure_class': 'provider-quota', 'locator': str(loc_path), - 'selected': d_blocked['selected'], 'work_unit_id': d_blocked['work_unit_id'], - }) - - store.mark_retry_quota_refresh('route/01_blocked', workspace) - - invoke_calls = [] - async def fake_invoke(ws, st, task, role, spec, prompt, resume_locator=None): - attempt_dir = ws / 'attempt-fake' - attempt_dir.mkdir(parents=True, exist_ok=True) - locator = attempt_dir / 'locator.json' - record = {'status': 'succeeded', 'task': task.name, 'role': role} - retry_ctx = st.task_state(task).get('retry_quota_refresh_context') if isinstance(st, dispatch.StateStore) else None - print(f' [fake_invoke] retry_ctx is None: {retry_ctx is None}') - if retry_ctx is not None: - print(f' [fake_invoke] retry_ctx keys: {list(retry_ctx.keys())}') - print(f' [fake_invoke] has locator: {bool(retry_ctx.get("locator"))}') - print(f' [fake_invoke] has handoff_id: {bool(retry_ctx.get("handoff_id"))}') - if isinstance(retry_ctx, dict) and retry_ctx.get('locator'): - record['handoff_id'] = retry_ctx.get('handoff_id') or retry_ctx.get('locator') - record['source_locator'] = retry_ctx.get('locator') - record['source_context'] = { - 'role': retry_ctx.get('role'), - 'failure_class': retry_ctx.get('failure_class'), - 'selected': retry_ctx.get('selected'), - 'work_unit_id': retry_ctx.get('work_unit_id'), - } - locator.write_text(json.dumps(record), encoding='utf-8') - invoke_calls.append((task.name, role, spec, prompt, resume_locator)) - return 0, None, locator - - async def fake_run_review(ws, st, task, **kwargs): - archive = ws / 'agent-task' / 'archive' / '2026' / '07' / task.name - archive.parent.mkdir(parents=True, exist_ok=True) - (task.directory / 'complete.log').write_text('simulation complete\n', encoding='utf-8') - task.directory.rename(archive) - return str(archive) - - args = dispatch.argparse.Namespace( - workspace=str(workspace), task_group='route', retry_blocked=True, dry_run=False, - ) - - with mock.patch.object(selector, 'probe_candidate_quota', return_value={'schema_version': '1.0', 'snapshot_id': 'snap', 'source': 'fake', 'checked_at': nighttime.isoformat(), 'targets': [{'adapter': 'codex', 'target': 'gpt-5.6-sol', 'status': 'available'}], 'required_caps': [], 'reason_codes': []}), \ - mock.patch.object(dispatch, 'run_review', side_effect=fake_run_review), \ - mock.patch.object(dispatch, 'ensure_review_shared_state'), \ - mock.patch.object(dispatch, 'invoke', side_effect=fake_invoke), \ - mock.patch.object(dispatch, 'datetime') as datetime_mock, \ - mock.patch.object(selector.subprocess, 'run', side_effect=AssertionError('unexpected')): - datetime_mock.now.return_value = nighttime - res = await dispatch.dispatch_with_store(args, workspace, store) - - print(f'Result: {res}') - print(f'Invoke calls: {len(invoke_calls)}') - for call in invoke_calls: - print(f' task={call[0]} role={call[1]}') - - attempt_locators = list(workspace.rglob('locator.json')) - attempt_locators = [p for p in attempt_locators if p != loc_path] - print(f'Attempt locators: {len(attempt_locators)}') - for p in attempt_locators: - record = json.loads(p.read_text(encoding='utf-8')) - print(f' {p}: handoff_id={record.get("handoff_id")}') - - store.close() - - -asyncio.run(main()) diff --git a/model_catalog b/model_catalog deleted file mode 100644 index 8728e91a..00000000 --- a/model_catalog +++ /dev/null @@ -1,23 +0,0 @@ -models: - - id: qwen3.6:35b - display_name: Qwen 3.6 35B - providers: - ollama-m1: qwen35b - vllm-dgx: qwen35b-awq - -nodes: - - id: node-m1 - providers: - - id: ollama-m1 - type: ollama - models: - - qwen35b - - llama3.1-8b - - - id: node-dgx - providers: - - id: vllm-dgx - type: vllm - models: - - qwen35b-awq - - qwen35b-fp16 diff --git a/scripts/readability_baseline.json b/scripts/readability_baseline.json index a1954aa1..b096c351 100644 --- a/scripts/readability_baseline.json +++ b/scripts/readability_baseline.json @@ -3034,14 +3034,6 @@ "function": "newSession", "reason": "function newSession exceeds warning threshold (111 > 80)" }, - { - "path": "debug_trace.py", - "metric": "function_loc", - "level": "warning", - "value": 97, - "function": "main", - "reason": "function main exceeds warning threshold (97 > 80)" - }, { "path": "packages/flutter/iop_console/test/iop_console_shell_test.dart", "metric": "function_loc", diff --git a/streamgate.test b/streamgate.test deleted file mode 100755 index d076039b..00000000 Binary files a/streamgate.test and /dev/null differ diff --git a/tmp/iop-review-followup.MreBOU/CODE_REVIEW-cloud-G08.md b/tmp/iop-review-followup.MreBOU/CODE_REVIEW-cloud-G08.md deleted file mode 100644 index d63a5f74..00000000 --- a/tmp/iop-review-followup.MreBOU/CODE_REVIEW-cloud-G08.md +++ /dev/null @@ -1,182 +0,0 @@ - - -# Code Review Reference - REVIEW_REVIEW_API - -> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.** -> The task is NOT complete until every implementation-owned section below is filled in. -> Complete the `구현 체크리스트`; the final checklist item is mandatory before saving. -> Fill implementation-owned sections, then stop with active files in place and report ready for review. -> If implementation is blocked, record the exact blocker, attempted commands/output, and resume condition only in implementation-owned evidence fields. -> Do not ask the user directly, present choices, call user-input tools, create control-plane stop files, or classify the next state. -> Finalization (`코드리뷰 결과`, log rename, `complete.log`, archive moves, `코드리뷰 전용 체크리스트`) is review-agent-only, even after compaction/resume. -> Follow the ownership table at the bottom of this file for which sections you own. - -## 개요 - -date=2026-07-26 -task=m-agent-task-runtime-target-selector/04+03_failover_budget, plan=13, tag=REVIEW_REVIEW_API - -## Roadmap Targets - -- Milestone: `agent-roadmap/phase/automation-runtime-bridge/milestones/agent-task-runtime-target-selector.md` -- Milestone link: [Milestone 문서](agent-roadmap/phase/automation-runtime-bridge/milestones/agent-task-runtime-target-selector.md) -- Task ids: - - `time-route`: local-G07~G08 KST 주야간 최초 target - - `context-failover`: Gemini↔Laguna 단방향 logical context failover - - `failure-budget`: target 전환 전후 동일 stage 10회 실패 예산 - - `selfcheck-policy`: 실제 worker 완료 target 기반 selfcheck -- Completion mode: check-on-pass - -## Archive Evidence Snapshot - -- 선행 완료: `agent-task/archive/2026/07/m-agent-task-runtime-target-selector/03+01,02_route_pin_state/complete.log` — route pin/state predecessor PASS. -- 직전 계획: `agent-task/m-agent-task-runtime-target-selector/04+03_failover_budget/plan_local_G08_12.log`. -- 직전 리뷰: `agent-task/m-agent-task-runtime-target-selector/04+03_failover_budget/code_review_cloud_G08_12.log` — FAIL, Required 7 / Suggested 0 / Nit 0. -- 영향 파일: `execution_target_policy.py`, `select_execution_target.py`, `dispatch.py`와 세 대응 테스트 파일. -- 검증 evidence: policy 6 tests PASS, selector 집중 7 tests PASS, dispatcher 집중 22 tests는 2 errors, 전체 181 tests는 1 error, `py_compile`/`git diff --check` PASS, SDD 후보 순서 재현 FAIL. -- 로드맵 carryover: S02/S06/S07/S10과 `time-route`, `context-failover`, `failure-budget`, `selfcheck-policy`가 미완료다. - -## 이 파일을 읽는 리뷰 에이전트에게 - -> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다. - -각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요. -리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다. - -1. 판정을 append한다. -2. `CODE_REVIEW-cloud-G08.md` → `code_review_cloud_G08_13.log`, `PLAN-local-G08.md` → `plan_local_G08_13.log`로 아카이브한다. -3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-agent-task-runtime-target-selector/04+03_failover_budget/`로 이동한다. WARN/FAIL이면 code-review skill이 요구하는 다음 filesystem state를 완전히 작성한다. -4. PASS이고 task group이 `m-`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다. -5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다. - ---- - -## 구현 항목별 완료 여부 - -| 항목 | 완료 여부 | -|------|---------| -| REVIEW_REVIEW_API-1 Canonical KST 후보 순서 | [ ] | -| REVIEW_REVIEW_API-2 Dispatcher failover와 logical context 연결 | [ ] | -| REVIEW_REVIEW_API-3 실제 invocation budget과 completing-target selfcheck | [ ] | -| REVIEW_REVIEW_API-4 회귀 기대와 전체 evidence 정합성 | [ ] | - -## 구현 체크리스트 - -- [ ] REVIEW_REVIEW_API-1 KST 네 경계의 canonical Gemini/Laguna 후보 순서와 selector 회귀 테스트를 구현한다. -- [ ] REVIEW_REVIEW_API-2 qualified failure의 단방향 selector failover, persisted transition, logical context 다음 invocation을 구현한다. -- [ ] REVIEW_REVIEW_API-3 실제 invocation target/transition 기준 stage budget과 completing-target selfcheck lifecycle을 구현한다. -- [ ] REVIEW_REVIEW_API-4 시간 명시 route matrix와 manual override resume schema를 일치시키고 집중·전체 검증을 통과한다. -- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. - -## 코드리뷰 전용 체크리스트 - -> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다. -> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다. - -- [ ] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다. -- [ ] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다. -- [ ] active `CODE_REVIEW-*-G??.md`를 `code_review_cloud_G08_13.log`로 아카이브한다. -- [ ] active `PLAN-*-G??.md`를 `plan_local_G08_13.log`로 아카이브한다. -- [ ] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md`와 `agent-task/**/*.log`를 unignore하고 `agent-roadmap/current.md`를 ignore하는지 확인한다. -- [ ] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다. -- [ ] PASS이면 active task 디렉터리 `agent-task/m-agent-task-runtime-target-selector/04+03_failover_budget/`를 `agent-task/archive/YYYY/MM/m-agent-task-runtime-target-selector/04+03_failover_budget/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다. -- [ ] PASS이고 task group이 `m-`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다. -- [ ] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-agent-task-runtime-target-selector/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다. -- [ ] WARN/FAIL이면 code-review skill의 판정에 맞는 다음 filesystem state를 작성하고 `complete.log`를 작성하지 않는다. - -## 계획 대비 변경 사항 - -_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._ - -## 주요 설계 결정 - -_구현 에이전트가 주요 설계 결정 사항을 기록한다._ - -## 리뷰어를 위한 체크포인트 - -- policy가 주간 Gemini→Laguna, 야간 Laguna→Gemini의 두 canonical 후보를 반환하는지 확인한다. -- qualified failure만 selector failover를 만들고 logical context가 실제 다음 invocation prompt에 전달되는지 확인한다. -- failure budget의 `last_target`/`last_transition`이 실제 invocation이며 reopen과 target 전환 뒤에도 worker stage 10회를 공유하는지 확인한다. -- Gemini→Laguna 완료만 pinned Laguna selfcheck를 실행하고 다른 completing target은 생략하는지 확인한다. -- 전체 181개 이상 suite와 SDD 집중 시나리오가 함께 PASS하고 수동 alternate fixture가 제거됐는지 확인한다. -- `IOP_FORCE_GEMINI_TODAY` initial→resume decision이 schema 오류 없이 roundtrip하고 canonical matrix 테스트는 ambient env와 독립적인지 확인한다. - -## 검증 결과 - -각 명령을 정확히 실행하고 actual stdout/stderr와 exit code를 아래에 기록한다. 명령을 바꾸면 `계획 대비 변경 사항`에 대체 명령과 이유를 먼저 기록한다. - -### Policy 경계 - -```bash -python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_execution_target_policy.py -``` - -결과: -_미실행_ - -### Selector 경계와 failover - -```bash -python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_select_execution_target.py SelectorRouteMatrixTests SelectorFailoverContractTests -v -``` - -결과: -_미실행_ - -### Dispatcher 집중 lifecycle - -```bash -python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py TaskStageTest RouteDecisionPersistenceTest DynamicFailoverBudgetTest DispatcherCanonicalFailoverIntegrationTest -v -``` - -결과: -_미실행_ - -### 전체 suite - -```bash -python3 -m unittest discover -s agent-ops/skills/project/orchestrate-agent-task-loop/tests -p 'test_*.py' -``` - -결과: -_미실행_ - -### Python compile - -```bash -python3 -m py_compile agent-ops/skills/project/orchestrate-agent-task-loop/scripts/execution_target_policy.py agent-ops/skills/project/orchestrate-agent-task-loop/scripts/select_execution_target.py agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_execution_target_policy.py agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_select_execution_target.py agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py -``` - -결과: -_미실행_ - -### Diff check - -```bash -git diff --check -``` - -결과: -_미실행_ - ---- - -> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every implementation-owned section?** -> If anything is blank, go back and fill it in before saving this file. -> Leave review-agent-only sections unchanged. - -## 섹션 소유권 - -| Section | Owner | Note | -|---------|-------|------| -| Header comment, 개요, 리뷰 에이전트 지시 | Fixed at stub creation | Implementing agent must not modify or execute these (archive, complete.log, and task-directory archive move are review-agent only) | -| Roadmap Targets | Fixed at stub creation from plan when present | Implementing agent must not modify; code-review copies it into complete.log as Roadmap Completion only on PASS | -| Archive Evidence Snapshot | Fixed at stub creation from plan when present | Implementing agent uses it as default prior-loop context; read only the specific archive files cited there when more detail is required | -| Agent UI Completion | Mixed | Present only for plan-required agent-ui code work; implementing agent fills actual evidence, review agent applies 구현됨 status/evidence update on PASS and copies the section into complete.log | -| 구현 항목별 완료 여부 (item names) | Fixed at stub creation | Implementing agent checks [ ] to [x] only | -| 구현 체크리스트 (item text/order) | Fixed at stub creation from plan | Implementing agent checks [ ] to [x] only | -| 코드리뷰 전용 체크리스트 | Review agent only | Implementing agent must not modify or check this section | -| 계획 대비 변경 사항, 주요 설계 결정 | Implementing agent | Replace placeholder text with actual content | -| 리뷰어를 위한 체크포인트 | Fixed at stub creation | Pre-filled from plan | -| 검증 결과 (section headings + commands) | Fixed at stub creation | Implementing agent fills in command output only; command changes require a 계획 대비 변경 사항 entry | -| 코드리뷰 결과 | Review agent appends | Not included in stub | diff --git a/tmp/iop-review-followup.MreBOU/PLAN-local-G08.md b/tmp/iop-review-followup.MreBOU/PLAN-local-G08.md deleted file mode 100644 index 39f3026b..00000000 --- a/tmp/iop-review-followup.MreBOU/PLAN-local-G08.md +++ /dev/null @@ -1,366 +0,0 @@ - - -# KST canonical failover, completing-target selfcheck와 resume schema 보완 - -## 이 파일을 읽는 구현 에이전트에게 - -구현과 테스트를 완료한 뒤 모든 검증 명령을 실행하고 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션에 실제 변경 내용과 stdout/stderr, exit code를 채운다. active PLAN/CODE_REVIEW 파일은 그대로 두고 리뷰 준비 완료만 보고한다. 최종 판정, 로그 아카이브, `complete.log`, 다음 상태 분류는 code-review skill 소유다. 차단되면 구현 소유 evidence 필드에 정확한 blocker, 시도한 명령과 출력, 재개 조건만 기록하며 사용자에게 질문하거나 user-input 도구·control-plane stop 파일을 만들지 않는다. - -## 배경 - -KST 주야간 1차 target 선택은 반영됐지만 `local-G07~G08` 정책은 시간대마다 후보가 하나뿐이어서 정규 Gemini↔Laguna failover를 실행할 수 없다. dispatcher는 selector의 failover 전이를 호출하지 않고 실제 invocation이 아닌 초기 persisted decision으로 failure budget을 기록하며, selfcheck도 실제 완료 target이 아니라 정적 lane/grade로 판별한다. 현재 구현은 active review evidence가 전부 비어 있고, 계획한 dispatcher 통합 테스트 클래스도 없다. reviewer 재실행에서 dispatcher 집중 검증은 2 errors, 전체 181개 suite는 `manual-gemini-today` prior decision schema 불일치로 1 error가 발생한다. - -## Archive Evidence Snapshot - -- 선행 완료: `agent-task/archive/2026/07/m-agent-task-runtime-target-selector/03+01,02_route_pin_state/complete.log` — route pin/state predecessor PASS. -- 직전 계획: `agent-task/m-agent-task-runtime-target-selector/04+03_failover_budget/plan_local_G08_12.log`. -- 직전 리뷰: `agent-task/m-agent-task-runtime-target-selector/04+03_failover_budget/code_review_cloud_G08_12.log` — FAIL, Required 7 / Suggested 0 / Nit 0. -- 영향 파일: `execution_target_policy.py`, `select_execution_target.py`, `dispatch.py`와 세 대응 테스트 파일. -- 검증 evidence: policy 6 tests PASS, selector 집중 7 tests PASS, dispatcher 집중 22 tests는 2 errors, 전체 181 tests는 1 error, `py_compile`/`git diff --check` PASS, SDD 후보 순서 재현 FAIL. -- 로드맵 carryover: S02/S06/S07/S10과 `time-route`, `context-failover`, `failure-budget`, `selfcheck-policy`가 미완료다. - -## Roadmap Targets - -- Milestone: `agent-roadmap/phase/automation-runtime-bridge/milestones/agent-task-runtime-target-selector.md` -- Milestone link: [Milestone 문서](agent-roadmap/phase/automation-runtime-bridge/milestones/agent-task-runtime-target-selector.md) -- Task ids: - - `time-route`: local-G07~G08 KST 주야간 최초 target - - `context-failover`: Gemini↔Laguna 단방향 logical context failover - - `failure-budget`: target 전환 전후 동일 stage 10회 실패 예산 - - `selfcheck-policy`: 실제 worker 완료 target 기반 selfcheck -- Completion mode: check-on-pass - -## 분석 결과 - -### 읽은 파일 - -- `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/execution_target_policy.py` -- `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/select_execution_target.py` -- `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py` -- `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_execution_target_policy.py` -- `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_select_execution_target.py` -- `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py` -- `agent-test/local/rules.md` -- `agent-roadmap/phase/automation-runtime-bridge/milestones/agent-task-runtime-target-selector.md` -- `agent-roadmap/sdd/automation-runtime-bridge/agent-task-runtime-target-selector/SDD.md` -- `agent-contract/index.md`, `agent-spec/index.md` — 이 runtime 범위에 매칭되는 별도 계약/spec 문서 없음. - -### SDD 기준 - -- SDD: `agent-roadmap/sdd/automation-runtime-bridge/agent-task-runtime-target-selector/SDD.md`, 상태 `[승인됨]`, 잠금 해제. -- S02 → `time-route`: 06:59:59, 07:00:00, 22:59:59, 23:00:00 KST의 initial target과 후보 순서를 REVIEW_REVIEW_API-1 및 최종 검증에 반영한다. -- S06 → `context-failover`: 주간 Gemini→Laguna, 야간 Laguna→quota-available Gemini, qualified failure만 전환, logical context, no bounce를 REVIEW_REVIEW_API-2에 반영한다. -- S07 → `failure-budget`: primary/alternate가 동일 stage 10회 예산을 공유하고 성공 때만 초기화되는 lifecycle을 REVIEW_REVIEW_API-3에 반영한다. -- S10 → `selfcheck-policy`: Gemini→Laguna 완료만 pinned Laguna selfcheck를 실행하고 Laguna→Gemini 및 cloud 완료는 생략하는 lifecycle을 REVIEW_REVIEW_API-3에 반영한다. -- Evidence Map의 경계 matrix, transition evidence, stage counter evidence, stage evidence를 각 집중 테스트와 전체 suite의 PASS 조건으로 고정한다. - -### 테스트 환경 규칙 - -- `test_env=local`. -- `agent-test/local/rules.md`가 존재해 전체를 읽었다. 이 agent-ops Python runtime 범위에 매칭되는 별도 profile route는 없어 profile 문서를 적용하지 않는다. -- fallback verification source는 repository Python unittest layout, `py_compile`, `git diff --check`, 승인 SDD Acceptance/Evidence Map이다. -- 외부 runner, provider 호출, 장기 실행 환경을 사용하지 않으므로 비-local preflight는 해당 없음이다. -- test-rule 유지보수 작업이 아니며 현재 local rule이 구조적으로 유효하므로 create-test/update-test는 필요하지 않다. - -### 테스트 커버리지 공백 - -- KST 경계 1차 target: policy/selector 테스트가 부분 커버하지만 후보가 하나인 상태를 정답으로 둔다. -- canonical failover: selector 테스트가 cloud decision에 Codex 후보를 수동 삽입하므로 Gemini/Laguna 정책을 실행하지 않는다. -- dispatcher failover/context: `build_context_package` helper만 직접 호출하며 실제 failure→다음 invocation 경로가 없다. -- failure budget: helper가 임의 target을 직접 기록하고 generic Pi 반복만 실행해 실제 primary→alternate audit를 검증하지 않는다. -- selfcheck: completing Laguna decision을 만들고 재사용하는 dispatcher 통합 테스트가 없다. -- resume schema: `IOP_FORCE_GEMINI_TODAY`가 `manual-gemini-today`를 persisted decision에 쓰지만 prior validator가 거부해 실제 resume과 전체 suite가 실패한다. -- 전체 회귀: `DispatcherCanonicalFailoverIntegrationTest`가 없고 전체 181개 suite가 resume schema 오류 1건으로 실패한다. - -### 심볼 참조 - -- rename/remove 없음. -- 변경 call sites: `select_policy`는 selector와 두 policy/selector 테스트가 소비한다. `_failover`는 `select_execution_target`이 호출한다. `select_execution_decision`/`persisted_execution_decision`은 `route_agent`, worker/selfcheck/review entry와 route persistence 테스트가 소비한다. `task_requires_selfcheck`는 `task_stage`가 호출한다. `StageFailureBudget`은 `run_escalating`과 budget 테스트가 소비한다. - -### 분할 판단 - -- split decision policy를 파일 선택 전에 평가했다. 이 디렉터리는 `04+03_failover_budget`이므로 predecessor `03`은 `agent-task/archive/2026/07/m-agent-task-runtime-target-selector/03+01,02_route_pin_state/complete.log`로 충족됐다. -- 정책 후보 순서, selector 전이, persisted decision, 실제 invocation 예산, completing-target selfcheck는 하나의 work-unit lifecycle과 동일 상태 schema를 함께 바꾼다. API와 call-site를 분리하면 중간 pair가 실행 불가능하고 같은 테스트 fixture를 중복 소유하므로 기존 dependent subtask 안의 단일 plan이 안전하다. -- 외부 소유권·독립 배포·별도 위험 프로필 경계는 없고, 테스트만 별도 sibling으로 떼어도 production slice를 독립 검증할 수 없다. - -### 범위 결정 근거 - -- official review route, cloud lane grade matrix, G01~G06/G09~G10 worker 정책은 변경하지 않는다. -- quota probe를 새로 실행하거나 외부 quota API를 추가하지 않고 기존 `quota_snapshot` tri-state 입력만 사용한다. -- legacy recovery 재분류와 process liveness/work-log archive 경로는 canonical Gemini/Laguna 전환에 필요한 최소 call site 외에는 수정하지 않는다. -- `agent-ops/rules/common/**`, `agent-ops/skills/common/**`, roadmap/SDD 문서는 구현 범위에서 제외한다. - -### 최종 라우팅 - -- `evaluation_mode=isolated-reassessment`, `finalizer=finalize-task-policy.sh`, `finalizer_mode=pair`. -- Build closures: scope/context/verification/evidence/ownership/decision 모두 `true`. 근거는 승인 SDD S02/S06/S07/S10, 전체 source/test/diff, local 재현 명령, 단일 dispatcher 소유 상태다. -- Build scores: scope_coupling=2, state_concurrency=2, blast_irreversibility=1, evidence_diagnosis=2, verification_complexity=1. `route_basis=local-fit`, capability gap=none, lane=`local`, grade=`G08`, filename=`PLAN-local-G08.md`. -- Build loop-risk: temporal_state=true(초기·resume·failover·성공·terminal), concurrent_consistency=true(dispatcher/state persistence의 atomic snapshot), boundary_contract=true(policy/selector/dispatcher와 두 consumer 이상), structured_interpretation=false, variant_product=true(시간대×failure×quota×completing target). `triggered=true`; unknown 없음. -- Review closures: scope/context/verification/evidence/ownership/decision 모두 `true`. -- Review scores: scope_coupling=2, state_concurrency=2, blast_irreversibility=1, evidence_diagnosis=2, verification_complexity=1. `route_basis=official-review`, lane=`cloud`, grade=`G08`, filename=`CODE_REVIEW-cloud-G08.md`, target=`codex/gpt-5.6-sol xhigh`. -- grade floor 및 capability-gap 승격: none. 반복 횟수와 직전 route는 평가 입력이나 점수에 사용하지 않았다. - -## 구현 체크리스트 - -- [ ] REVIEW_REVIEW_API-1 KST 네 경계의 canonical Gemini/Laguna 후보 순서와 selector 회귀 테스트를 구현한다. -- [ ] REVIEW_REVIEW_API-2 qualified failure의 단방향 selector failover, persisted transition, logical context 다음 invocation을 구현한다. -- [ ] REVIEW_REVIEW_API-3 실제 invocation target/transition 기준 stage budget과 completing-target selfcheck lifecycle을 구현한다. -- [ ] REVIEW_REVIEW_API-4 시간 명시 route matrix와 manual override resume schema를 일치시키고 집중·전체 검증을 통과한다. -- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. - -### [REVIEW_REVIEW_API-1] Canonical KST 후보 순서 - -#### 문제 - -`execution_target_policy.py:93-109`는 시간대별 1차 target을 고른 뒤 `candidates=(target,)`만 반환한다. 따라서 주간 Gemini 실패 시 Laguna, 야간 Laguna 실패 시 quota-available Gemini라는 S06 전이를 selector가 수행할 후보가 없다. - -```python -# Before: execution_target_policy.py:93-109 -if grade <= 8: - time_window = _kst_time_window(evaluated_at) - # ... target 하나 선택 ... - return PolicyDecision( - # ... - candidates=(target,), - ) -``` - -#### 해결 방법 - -시간대에 따라 같은 두 canonical target의 우선순위만 바꾸고, initial은 첫 eligible target을 선택하도록 유지한다. 두 후보가 모두 canonical set에 남으므로 KST 경계를 넘은 resume/failover decision 검증도 현재 시각에 의해 거부되지 않는다. - -```python -# After -if time_window == "kst-day-[07:00,23:00)": - candidates = (AGY_GEMINI_MEDIUM, PI_LAGUNA) -else: - candidates = (PI_LAGUNA, AGY_GEMINI_MEDIUM) -return PolicyDecision(..., candidates=candidates) -``` - -#### 수정 파일 및 체크리스트 - -- [ ] `scripts/execution_target_policy.py`: 두 target 후보 순서와 reason/time window를 고정한다. -- [ ] `tests/test_execution_target_policy.py`: G07/G08 네 경계에서 두 후보 전체 순서를 검증한다. -- [ ] `tests/test_select_execution_target.py`: initial selected, rank 1/2, quota eligibility와 canonical failover를 실제 local plan으로 검증한다. - -#### 테스트 작성 - -작성한다. `ExecutionTargetPolicyTests.test_local_g07_g08_candidate_order_uses_kst_boundaries`와 `SelectorRouteMatrixTests.test_local_g07_g08_use_kst_boundary_candidate_order`가 네 경계에서 adapter+target 순서를 검증한다. `SelectorFailoverContractTests`는 수동 Codex 후보 fixture를 제거하고 주간/야간 local-G08 decision을 사용한다. - -#### 중간 검증 - -```bash -python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_execution_target_policy.py -python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_select_execution_target.py SelectorRouteMatrixTests SelectorFailoverContractTests -v -``` - -예상 결과: 모든 테스트 PASS, exit 0. 주간 후보는 `agy Gemini Medium → pi Laguna`, 야간 후보는 역순이다. - -### [REVIEW_REVIEW_API-2] Dispatcher failover와 logical context 연결 - -#### 문제 - -`dispatch.py:1099-1118`은 prior decision 유무로 `initial|resume`만 선택한다. `run_escalating`의 qualified cloud failure는 `dispatch.py:3187-3214`에서 legacy `promoted_spec`으로 전환되며 selector `failover`, decision history, `build_context_package`가 실제 다음 invocation에 연결되지 않는다. - -```python -# Before: dispatch.py:1112-1118 -return selector.select_execution_target( - _decision_file(task, stage), - transition="resume" if prior_decision is not None else "initial", - prior_decision=prior_decision, - quota_snapshot=quota_snapshot, -) -``` - -#### 해결 방법 - -selector bridge와 persistence helper가 명시적 `transition` 및 `failure_class`를 받고 decision/history를 원자적으로 갱신하게 한다. worker의 qualified failure에서 현재 decision을 failover하고 다음 `AgentSpec`을 만든 뒤, `build_context_package`의 PLAN/locator/normalized output/raw log/workspace 경로를 다음 adapter의 continuation prompt에 넣는다. cross-adapter 전환은 native session을 전달하지 않고, generic failure·반복 횟수만으로는 failover하지 않으며 used candidate로 bounce를 막는다. 야간 Gemini quota가 exhausted이면 `no_failover_candidate`로 해당 task만 차단한다. - -```python -# After -next_decision = persisted_execution_decision( - store, - task, - stage="worker", - transition="failover", - failure_class=failure, -) -context = build_context_package( - workspace, task, locator, - previous_spec=spec, - next_spec=agent_spec_from_decision(next_decision), -) -``` - -#### 수정 파일 및 체크리스트 - -- [ ] `scripts/select_execution_target.py`: canonical candidate quota 상태를 보존하고 failed cloud target만 quota evidence에 따라 exhausted 처리한다. -- [ ] `scripts/dispatch.py`: selector bridge/persistence에 failover 인자를 연결하고 transition history를 갱신한다. -- [ ] `scripts/dispatch.py`: logical context package를 다음 invocation prompt에 연결하고 cross-adapter native resume을 차단한다. -- [ ] `tests/test_select_execution_target.py`: qualified/unqualified, quota unavailable, unknown-once, no-bounce를 실제 Gemini/Laguna 후보로 검증한다. -- [ ] `tests/test_dispatch.py`: 주간·야간 failure→alternate invocation의 spec, prompt context, persisted transition을 통합 검증한다. - -#### 테스트 작성 - -작성한다. `DispatcherCanonicalFailoverIntegrationTest.test_day_gemini_failure_continues_on_laguna_with_logical_context`, `test_night_laguna_failure_continues_on_available_gemini`, `test_night_gemini_quota_exhaustion_blocks_without_bounce`, `test_generic_failure_stays_on_same_target`를 추가한다. 실제 locator fixture는 PLAN, normalized-output.log, stream.log, workspace identity를 포함한다. - -#### 중간 검증 - -```bash -python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_select_execution_target.py SelectorFailoverContractTests -v -python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py DispatcherCanonicalFailoverIntegrationTest -v -``` - -예상 결과: 모든 전환 테스트 PASS, exit 0. cross-adapter continuation은 logical context만 사용하고 이전 target으로 bounce하지 않는다. - -### [REVIEW_REVIEW_API-3] 실제 invocation budget과 completing-target selfcheck - -#### 문제 - -`dispatch.py:3062-3066`은 실패한 `spec`이 아니라 persisted decision의 기존 `selected`와 transition을 budget audit에 기록한다. `dispatch.py:1157-1158`과 `task_stage:1239-1242`는 selfcheck를 정적 lane/grade로 판별해 Gemini→Laguna 완료와 Laguna→Gemini 완료를 구분하지 못한다. - -```python -# Before: dispatch.py:3062-3066 -recovery_failures += 1 -selected = state["execution_decisions"][role]["selected"] -transition = state["execution_decisions"][role]["transition"]["trigger"] -recovery_failures = stage_budget.record_failure( - target=selected, transition=transition -) -``` - -#### 해결 방법 - -각 invoke 직전에 active decision/spec/transition을 일치시켜 보관하고 그 snapshot으로 실패를 기록한다. primary 1회와 alternate 9회가 reopen 뒤에도 같은 `work_unit_id|worker` key를 공유하며 10번째에 terminal block하고, 성공 때만 reset한다. worker 성공 시 실제 completing spec의 local/cloud 성격과 pinned worker decision을 state에 저장한다. `task_stage`는 이 완료 evidence로 selfcheck를 예약하고, `run_selfcheck`는 저장된 Laguna decision을 resume하여 새 initial route를 평가하지 않는다. - -```python -# After -failure_target = current_decision["selected"] -failure_transition = current_decision["transition"]["trigger"] -count = stage_budget.record_failure( - target=failure_target, - transition=failure_transition, -) -store.update_task( - task, - worker_selfcheck_required=completed_spec.local_pi, -) -``` - -#### 수정 파일 및 체크리스트 - -- [ ] `scripts/dispatch.py`: invocation snapshot의 실제 target/transition으로 `StageFailureBudget`을 기록한다. -- [ ] `scripts/dispatch.py`: worker 완료 target의 `local_pi`와 pinned decision을 persisted state에 남긴다. -- [ ] `scripts/dispatch.py`: `task_stage`/`run_selfcheck`가 completing-target evidence와 resume decision을 사용하게 한다. -- [ ] `tests/test_dispatch.py`: primary 1 + alternate 9, stage 분리, success reset, last_target/last_transition audit를 검증한다. -- [ ] `tests/test_dispatch.py`: Gemini→Laguna만 Laguna selfcheck, Laguna→Gemini와 cloud 완료는 selfcheck 생략을 검증한다. - -#### 테스트 작성 - -작성한다. `DynamicFailoverBudgetTest.test_primary_then_alternate_share_ten_failure_budget_across_reopen`이 실제 decision lifecycle과 audit 필드를 검증한다. `DispatcherCanonicalFailoverIntegrationTest.test_completing_target_controls_selfcheck_and_reuses_pin`이 세 completing-target case와 transition history의 no-new-initial을 검증한다. - -#### 중간 검증 - -```bash -python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py DynamicFailoverBudgetTest DispatcherCanonicalFailoverIntegrationTest -v -``` - -예상 결과: 모든 테스트 PASS, exit 0. worker stage counter만 10에 도달하며 `last_target`은 alternate이고 Laguna 완료 case만 selfcheck로 전이한다. - -### [REVIEW_REVIEW_API-4] 회귀 기대와 전체 evidence 정합성 - -#### 문제 - -`execution_target_policy.py:96-103`은 `IOP_FORCE_GEMINI_TODAY`에서 `time_window=manual-gemini-today`를 저장하지만 `select_execution_target.py:257`의 prior validator는 이 값을 허용하지 않는다. 현재 환경에서 `DynamicFailoverBudgetTest.test_runtime_budget_resets_on_success_and_blocks_tenth_failure_after_reopen`과 전체 suite가 resume 중 `malformed_prior_decision`으로 실패하며, 시간 의존 G07/G08 matrix도 정적 기대와 분리되어야 한다. - -```python -# Before: test_dispatch.py:261-281 -expected = { - 7: ("agy", "Gemini 3.6 Flash (Medium)", False), - 8: ("agy", "Gemini 3.6 Flash (Medium)", False), -} -spec = dispatch.route_agent(task) -``` - -#### 해결 방법 - -정적 grade matrix는 시간 독립 grade만 유지하고, G07/G08은 명시적 KST day/night `evaluated_at`을 selector bridge에 주는 별도 matrix로 검증한다. 수동 override를 유지한다면 `manual-gemini-today`를 selector schema와 prior validator에 일관되게 포함하고 override initial→resume 회귀를 추가한다. canonical SDD 경로 테스트는 ambient env를 격리한다. 집중 테스트 후 전체 unittest discovery, py_compile, diff check를 실행해 SDD와 회귀 suite가 동시에 통과하는지 확인한다. - -```python -# After -for evaluated_at, expected in kst_cases: - decision = dispatch.select_execution_decision( - task, stage="worker", evaluated_at=evaluated_at - ) - assert decision["selected"] == expected -``` - -#### 수정 파일 및 체크리스트 - -- [ ] `scripts/execution_target_policy.py`, `scripts/select_execution_target.py`: manual override decision과 prior validator schema를 일치시키거나 canonical 정책 밖 override를 제거한다. -- [ ] `tests/test_dispatch.py`: 정적 grade matrix에서 시간 의존 G07/G08 기대를 분리한다. -- [ ] `tests/test_dispatch.py`: 명시적 KST day/night decision과 completing-target lifecycle을 검증한다. -- [ ] `tests/test_execution_target_policy.py`: canonical 후보 순서 경계 기대를 유지한다. -- [ ] `tests/test_select_execution_target.py`: selector initial/failover 기대를 canonical 후보와 일치시킨다. - -#### 테스트 작성 - -작성한다. `TaskStageTest.test_local_route_grade_boundaries`는 시간 독립 grade만 검증하고, G07/G08은 `test_local_g07_g08_route_uses_explicit_kst_boundaries`에서 고정 `evaluated_at`별 initial target을 검증한다. `test_manual_gemini_override_resume_roundtrip`으로 override initial→resume을 검증하고 기존 집중 테스트와 새 integration test를 전체 discovery에 포함한다. - -#### 중간 검증 - -```bash -python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py TaskStageTest -v -python3 -m unittest discover -s agent-ops/skills/project/orchestrate-agent-task-loop/tests -p 'test_*.py' -``` - -예상 결과: 전체 suite PASS, exit 0. 현재 181개를 줄이지 않으며 새 회귀 테스트만 증가한다. - -## 의존 관계 및 구현 순서 - -1. REVIEW_REVIEW_API-1에서 canonical 후보 순서와 selector matrix를 먼저 고정한다. -2. REVIEW_REVIEW_API-2가 그 decision 계약을 dispatcher failover와 logical context에 연결한다. -3. REVIEW_REVIEW_API-3이 실제 invocation audit와 completing-target selfcheck를 연결한다. -4. REVIEW_REVIEW_API-4가 시간 matrix와 manual override resume schema를 일치시키고 전체 회귀를 닫는다. - -선행 subtask `03+01,02_route_pin_state`는 archive `complete.log`로 충족됐으며, 이 active subtask 안에 추가 runtime dependency는 없다. - -## 수정 파일 요약 - -| 파일 | 구현 항목 | -|---|---| -| `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/execution_target_policy.py` | REVIEW_REVIEW_API-1, REVIEW_REVIEW_API-4 | -| `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/select_execution_target.py` | REVIEW_REVIEW_API-1, REVIEW_REVIEW_API-2, REVIEW_REVIEW_API-4 | -| `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py` | REVIEW_REVIEW_API-2, REVIEW_REVIEW_API-3 | -| `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_execution_target_policy.py` | REVIEW_REVIEW_API-1, REVIEW_REVIEW_API-4 | -| `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_select_execution_target.py` | REVIEW_REVIEW_API-1, REVIEW_REVIEW_API-2, REVIEW_REVIEW_API-4 | -| `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py` | REVIEW_REVIEW_API-2, REVIEW_REVIEW_API-3, REVIEW_REVIEW_API-4 | - -## 최종 검증 - -```bash -python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_execution_target_policy.py -``` - -예상 결과: PASS, exit 0. - -```bash -python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_select_execution_target.py SelectorRouteMatrixTests SelectorFailoverContractTests -v -``` - -예상 결과: PASS, exit 0. - -```bash -python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py TaskStageTest RouteDecisionPersistenceTest DynamicFailoverBudgetTest DispatcherCanonicalFailoverIntegrationTest -v -``` - -예상 결과: PASS, exit 0. - -```bash -python3 -m unittest discover -s agent-ops/skills/project/orchestrate-agent-task-loop/tests -p 'test_*.py' -python3 -m py_compile agent-ops/skills/project/orchestrate-agent-task-loop/scripts/execution_target_policy.py agent-ops/skills/project/orchestrate-agent-task-loop/scripts/select_execution_target.py agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_execution_target_policy.py agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_select_execution_target.py agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py -git diff --check -``` - -예상 결과: 전체 unittest PASS, compile/diff check exit 0. 모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다.