diff --git a/agent-ops/skills/project/dev-runtime-deploy/SKILL.md b/agent-ops/skills/project/dev-runtime-deploy/SKILL.md index 3865c47..563e30c 100644 --- a/agent-ops/skills/project/dev-runtime-deploy/SKILL.md +++ b/agent-ops/skills/project/dev-runtime-deploy/SKILL.md @@ -1,6 +1,6 @@ --- name: dev-runtime-deploy -version: 1.0.5 +version: 1.0.6 description: dev 배포, dev-runtime 배포, Edge/Node dev 환경 배포 요청에서 clean sync, 전체 테스트, rebuild, 원격 배포, OpenAI-compatible capacity smoke를 수행하는 절차 --- @@ -75,7 +75,7 @@ dev-runtime provider pool을 제품 검증이 끝난 상태로 배포한다. 배 - GX10 Laguna vLLM container를 재생성할 때 host template `/home/toki/iop-gx10-vllm/laguna-s-2.1-thinking.jinja`를 container `/run/iop/laguna-s-2.1-thinking.jinja`에 read-only bind하고 `--chat-template`로 지정한다. stock template로 되돌리면 enabled 요청이 첫 생성 토큰으로 ``를 내보내 Pi thinking이 비는 회귀가 생길 수 있다. - OneXPlayer node는 현재 host에서 `ssh r0bin@192.168.0.59`로 접속한다. artifact가 remote runner에 있으면 현재 host를 통해 전달한 뒤 Windows host에서 교체한다. - OneXPlayer에서는 SSH 세션 안의 `Start-Process`로 장기 실행을 시작하지 않는다. `Win32_Process.Create` 또는 동등한 세션 독립 실행 방식으로 `iop-node.exe --config node.yaml serve`를 시작한다. - - RTX5090 node는 현재 host에서 `ssh iop-dev-rtx5090`으로 접속한다. 2026-07-23 기준 IOP Node의 실제 부팅 chain은 user Startup `IOP Node.lnk -> cmd.exe -> C:/Users/r0bin/iop-field/run-iop-node.cmd -> iop-node.exe serve --config node.yaml`이다. `startup.lnk -> startup.bat -> oto -> AHK`는 별도 호스트 자동화이며 IOP Node의 부팅 owner나 검증 근거가 아니다. 특히 AHK에 남은 `IOP Dev Node RTX5090 Lemonade` Task Scheduler 호출은 제거된 task를 가리킨다. IOP dev 배포는 Task Scheduler 항목을 생성하거나 재생성하지 않는다. 배포 직후 즉시 재시작이 필요하면 `C:/Users/r0bin/iop-field/run-iop-node.cmd`를 `Win32_Process.Create` 또는 동등한 세션 독립 방식으로 실행하고, SSH 세션 내부의 `Start-Process`에 장기 실행을 의존하지 않는다. + - RTX5090 node는 현재 host에서 `ssh iop-dev-rtx5090`으로 접속한다. 2026-07-26 기준 IOP 관련 Windows 부팅 owner는 없고, operator는 `C:/Users/r0bin/iop-field/remote-llm-toggle.ps1` 또는 이를 한 번 호출하는 `RemoteLLM_mode.ahk`로 수동 전환한다. IOP dev 배포는 Startup shortcut, Run entry, Task Scheduler, Windows service를 생성하거나 복원하지 않는다. 기본 Toggle은 상태를 반전시키므로 배포 자동화에서 호출하지 말고 `-Action Status|Up|Down`을 명시한다. Node binary만 즉시 재시작할 때는 `Win32_Process.Create` 또는 동등한 세션 독립 방식을 사용하고 SSH 세션 내부의 `Start-Process`에 장기 실행을 의존하지 않는다. - RTX5090 Lemonade는 `host=0.0.0.0`, CUDA backend, Q5 GGUF + Q8 KV, context `262144` 기준을 inventory와 대조한다. localhost-only bind는 Node에서 provider endpoint에 접속할 수 없으므로 배포 완료로 보지 않는다. 7. **배포 후 연결 검증** @@ -108,7 +108,7 @@ dev-runtime provider pool을 제품 검증이 끝난 상태로 배포한다. 배 - [ ] 빌드 후 config check, refresh help, refresh dry-run, 전체 테스트가 통과했는가 - [ ] Edge, mac-codex-node, GX10 vLLM node, OneXPlayer Lemonade node, RTX5090 Lemonade node가 재시작되고 4개 node(mac-codex, gx10-vllm, onexplayer-lemonade, rtx5090-lemonade)가 connected 상태인가 - [ ] OneXPlayer 접속과 실행이 `r0bin@192.168.0.59` 및 세션 독립 실행 방식으로 수행되었는가 -- [ ] RTX5090 접속이 `ssh iop-dev-rtx5090` public-key batch 인증으로 성공하고, `IOP Node.lnk -> run-iop-node.cmd -> iop-node.exe` 실제 chain, Node process와 Edge 연결이 확인되며 Lemonade가 `0.0.0.0:13305`에 listen하고 Task Scheduler 항목을 생성하지 않았는가. AHK automation chain은 Node 시작 증거로 쓰지 않는다. +- [ ] RTX5090 접속이 `ssh iop-dev-rtx5090` public-key batch 인증으로 성공하고, IOP 관련 boot autostart가 없으며, 수동 toggle의 exact Ornith profile·`0.0.0.0:13305` listener·Node Edge 연결이 확인되고 Startup/Run/Task/Service 항목을 생성하지 않았는가. - [ ] `/v1/responses` capacity smoke에서 총 capacity만큼 `in_flight`가 차고 초과 요청이 queue에 잡혔는가 - [ ] `/v1/chat/completions` capacity smoke에서 총 capacity만큼 `in_flight`가 차고 초과 요청이 queue에 잡혔는가 - [ ] 완료 후 provider `in_flight=0`, `queued=0`으로 회복되었는가 diff --git a/agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py b/agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py index bd486e8..25768b2 100644 --- a/agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py +++ b/agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py @@ -1045,9 +1045,6 @@ def live_predecessors( return live -def route_agent(task: Task) -> AgentSpec: - return agent_spec_from_decision(select_execution_decision(task, stage="worker")) - def _selector_module(): path = Path(__file__).resolve().parent / "select_execution_target.py" spec = importlib.util.spec_from_file_location("agent_task_execution_target_selector", path) @@ -1079,23 +1076,66 @@ def agent_spec_from_decision(decision: dict[str, Any]) -> AgentSpec: raise ExecutionDecisionError("selector selected schema가 유효하지 않다") try: selector = _selector_module() + decision_info = decision["decision"] + evaluated_at = selector.datetime.fromisoformat(decision_info["evaluated_at"]) policy_targets = selector.policy.select_policy( stage=decision["stage"], lane=decision["lane"], grade=decision["grade"], - evaluated_at=datetime.now(KST), + evaluated_at=evaluated_at, ).candidates - except (KeyError, OSError, ValueError, ImportError) as exc: + canonical = selector.policy.canonical_target(adapter, target) + except (KeyError, OSError, TypeError, ValueError, ImportError) as exc: raise ExecutionDecisionError(f"selector policy validation 실패: {exc}") from exc - expected = {(item.adapter, item.target, item.execution_class, item.selfcheck_required) - for item in policy_targets} - if (adapter, target, execution_class, local_pi) not in expected: + if canonical is None or ( + canonical.execution_class != execution_class + or canonical.selfcheck_required != local_pi + ): raise ExecutionDecisionError("selector selected가 canonical policy target이 아니다") + initial_keys = {(item.adapter, item.target) for item in policy_targets} + if (adapter, target) not in initial_keys: + promotion_path = decision.get("promotion_path") + if not isinstance(promotion_path, list) or len(promotion_path) < 2: + raise ExecutionDecisionError("selector promotion path가 없다") + resolved_path = [] + for index, entry in enumerate(promotion_path): + if not isinstance(entry, dict): + raise ExecutionDecisionError( + f"selector promotion path[{index}]가 object가 아니다" + ) + resolved = selector.policy.canonical_target( + entry.get("adapter"), entry.get("target") + ) + if resolved is None: + raise ExecutionDecisionError( + f"selector promotion path[{index}] target이 canonical이 아니다" + ) + resolved_path.append(resolved) + if (resolved_path[0].adapter, resolved_path[0].target) not in initial_keys: + raise ExecutionDecisionError("selector promotion path 시작 target이 잘못됐다") + if any( + selector.policy.promotion_target(previous) != current + for previous, current in zip(resolved_path, resolved_path[1:]) + ): + raise ExecutionDecisionError("selector promotion path 순서가 잘못됐다") + if resolved_path[-1] != canonical: + raise ExecutionDecisionError("selector promotion path tail이 selected와 다르다") if adapter == "pi": if not target.startswith("iop/") or not local_pi: raise ExecutionDecisionError("Pi selector target/schema가 유효하지 않다") return AgentSpec("pi", target.removeprefix("iop/"), f"pi/{target}", local_pi=True) if adapter not in {"agy", "claude", "codex"} or local_pi: raise ExecutionDecisionError(f"selector adapter/schema가 유효하지 않다: {adapter!r}") - display = f"{adapter}/{target}" + (" xhigh" if adapter in {"claude", "codex"} else "") + reasoning_effort = "high" if canonical == selector.policy.CODEX_TERRA_HIGH else None + suffix = " high" if reasoning_effort == "high" else ( + " xhigh" if adapter in {"claude", "codex"} else "" + ) + display = f"{adapter}/{target}{suffix}" + if reasoning_effort is not None: + return AgentSpec( + adapter, + target, + display, + reasoning_effort=reasoning_effort, + ) return AgentSpec(adapter, target, display) @@ -1131,6 +1171,134 @@ def select_execution_decision( ) from exc +def work_unit_id_from_file(path: Path) -> str | None: + if not path.is_file(): + return None + try: + head = path.read_text(encoding="utf-8", errors="replace")[:1024] + match = PLAN_IDENTITY_RE.search(head) + if match: + return f"{match.group('task')}::plan-{match.group('plan')}::tag-{match.group('tag')}" + except Exception: + pass + return None + + +def synthesized_official_review_decision(task: Task) -> dict[str, Any]: + wuid = None + if task.plan and task.plan.is_file(): + wuid = work_unit_id_from_file(task.plan) + if not wuid: + plans = sorted(task.directory.glob("plan_*.log")) + sorted(task.directory.glob("PLAN-*.md")) + for p in plans: + wuid = work_unit_id_from_file(p) + if wuid: + break + if not wuid: + wuid = f"{task.name}::plan-0::tag-REFACTOR" + + cand = { + "candidate_rank": 1, + "adapter": "codex", + "target": "gpt-5.6-sol", + "execution_class": "cloud_model", + "eligibility": "eligible", + "reason_codes": ["official_review_fixed_target"], + "selfcheck_required": False, + } + return { + "schema_version": "1.0", + "work_unit_id": wuid, + "stage": "review", + "rule_id": "official-review-codex", + "priority": 10, + "candidates": [cand], + "selected": { + "adapter": "codex", + "target": "gpt-5.6-sol", + "execution_class": "cloud_model", + "selfcheck_required": False, + "reason_codes": ["official_review_fixed_target"], + }, + "quota_snapshot": {"id": "fixed", "status": "not_applicable"}, + "transition": {"trigger": "resume" if task.recovery else "initial", "evaluated_at": now_iso()}, + } + + +def read_or_preview_stage_decision( + task: Task, + state: dict[str, Any], + *, + stage: str, + dry_run: bool = False, + evaluated_at: datetime | None = None, +) -> dict[str, Any]: + decisions = state.get("execution_decisions", {}) if isinstance(state, dict) else {} + prior = decisions.get(stage) if isinstance(decisions, dict) else None + + if isinstance(prior, dict): + if task.plan and task.plan.is_file(): + wuid = work_unit_id_from_file(task.plan) + if wuid and prior.get("work_unit_id") == wuid: + return prior + elif task.recovery: + return prior + + quota_snapshot = state.get("quota_snapshot") if isinstance(state, dict) else None + if stage == "review" and (prior is None or task.recovery): + return synthesized_official_review_decision(task) + + return select_execution_decision( + task, + stage=stage, + prior_decision=prior, + quota_snapshot=quota_snapshot, + evaluated_at=evaluated_at, + ) + + +def selector_evidence_lines(decision: dict[str, Any] | None) -> list[str]: + if not isinstance(decision, dict): + return [] + selected = decision.get("selected", {}) + if not isinstance(selected, dict): + return [] + work_unit = decision.get("work_unit_id", "none") + rule_id = decision.get("rule_id", "none") + priority = decision.get("priority", "none") + transition = decision.get("transition", {}) + trigger = transition.get("trigger", "none") if isinstance(transition, dict) else "none" + quota_snap = decision.get("quota_snapshot", {}) + quota_status = quota_snap.get("status", "none") if isinstance(quota_snap, dict) else "none" + + candidates = decision.get("candidates", []) + cand_strs = [] + if isinstance(candidates, list): + for c in candidates: + if isinstance(c, dict): + rank = c.get("candidate_rank", "?") + adapter = c.get("adapter", "?") + target = c.get("target", "?") + elig = c.get("eligibility", "?") + cand_strs.append(f"#{rank}:{adapter}/{target}({elig})") + + reasons = selected.get("reason_codes", []) + reason_str = ",".join(reasons) if isinstance(reasons, list) else str(reasons) + + lines = [ + f"work_unit_id={work_unit}", + f"rule_id={rule_id}", + f"priority={priority}", + f"transition={trigger}", + f"quota_status={quota_status}", + ] + if cand_strs: + lines.append(f"candidates={';'.join(cand_strs)}") + if reason_str: + lines.append(f"reason_codes={reason_str}") + return lines + + def commit_execution_decision( store: StateStore, task: Task, stage: str, decision: dict[str, Any] ) -> None: @@ -1140,8 +1308,27 @@ def commit_execution_decision( raise ExecutionDecisionError("persisted selector state schema가 유효하지 않다") decisions = dict(decisions) decisions[stage] = decision - history = [*history, {"stage": stage, "transition": decision["transition"]["trigger"], - "work_unit_id": decision["work_unit_id"], "selected": decision["selected"]}] + + stage_budget_count = 0 + try: + stage_budget_count = StageFailureBudget.from_decision(store, task, decision).count() + except Exception: + pass + + selected = decision.get("selected", {}) + history_entry = { + "stage": stage, + "transition": decision.get("transition", {}).get("trigger") if isinstance(decision.get("transition"), dict) else None, + "work_unit_id": decision.get("work_unit_id"), + "rule_id": decision.get("rule_id"), + "priority": decision.get("priority"), + "candidates": decision.get("candidates"), + "selected": selected, + "reason_codes": selected.get("reason_codes", []) if isinstance(selected, dict) else [], + "quota_snapshot": decision.get("quota_snapshot"), + "stage_budget": stage_budget_count, + } + history = [*history, history_entry] store.update_task(task, execution_decisions=decisions, route_transition_history=history, blocked=None) @@ -1158,13 +1345,17 @@ def persisted_execution_decision( quota_snapshot = state.get("quota_snapshot") if quota_snapshot is not None and not isinstance(quota_snapshot, dict): raise ExecutionDecisionError("persisted quota snapshot schema가 유효하지 않다") - decision = select_execution_decision( - task, stage=stage, prior_decision=decisions.get(stage), - quota_snapshot=quota_snapshot, - transition=transition, - failure_class=failure_class, - evaluated_at=evaluated_at, - ) + prior_decision = decisions.get(stage) + if stage == "review" and (prior_decision is None or task.recovery): + decision = read_or_preview_stage_decision(task, state, stage=stage, evaluated_at=evaluated_at) + else: + decision = select_execution_decision( + task, stage=stage, prior_decision=prior_decision, + quota_snapshot=quota_snapshot, + transition=transition, + failure_class=failure_class, + evaluated_at=evaluated_at, + ) spec = agent_spec_from_decision(decision) commit_execution_decision(store, task, stage, decision) return decision, spec @@ -2366,6 +2557,28 @@ async def invoke( "status": "running", "resumed_from_locator": str(resume_locator) if pi_resume_session else None, } + stage_decision = None + if isinstance(store, StateStore): + decisions = store.task_state(task).get("execution_decisions", {}) + if isinstance(decisions, dict): + stage_decision = decisions.get(role) + if isinstance(stage_decision, dict): + record["work_unit_id"] = stage_decision.get("work_unit_id") + record["rule_id"] = stage_decision.get("rule_id") + record["priority"] = stage_decision.get("priority") + record["candidates"] = stage_decision.get("candidates") + record["selected"] = stage_decision.get("selected") + record["reason_codes"] = ( + stage_decision.get("selected", {}).get("reason_codes", []) + if isinstance(stage_decision.get("selected"), dict) + else [] + ) + record["quota_snapshot"] = stage_decision.get("quota_snapshot") + record["transition"] = stage_decision.get("transition") + try: + record["stage_budget"] = StageFailureBudget.from_decision(store, task, stage_decision).count() + except Exception: + record["stage_budget"] = 0 write_json(locator_path, record) store.update_task(task, active_locator=str(locator_path)) prefix = f"[{task.directory.name}][{role}][a{attempt:02d}]" @@ -2919,17 +3132,43 @@ def logical_context_prompt(context: dict[str, Any]) -> str: ) +def continuation_prompt_from_package( + context_package: dict[str, Any], + *, + target: dict[str, Any] | None = None, + native_resume: bool = False, +) -> str: + if native_resume or context_package.get("resume_mode") == "native": + return ( + "Think in English. Final in Korean. Continue this session and complete " + "the current task." + ) + plan = context_package["plan"] + locator = context_package["locator"] + workspace = context_package["workspace"] + raw_log = context_package["raw_log"] + normalized_output = context_package["normalized_output"] + return ( + f"Think in English. Final in Korean. " + f"Read plan={plan}, locator={locator}, workspace={workspace}, " + f"raw_log={raw_log}, normalized_output={normalized_output} and complete the task." + ) + + def continuation_prompt( task: Task, role: str, - locator: Path, + locator: Path | None = None, *, local_pi: bool = False, resume_same_pi_session: bool = False, context: dict[str, Any] | None = None, ) -> str: - if context is not None and context.get("resume_mode") == "logical": - return logical_context_prompt(context) + if context is not None: + return continuation_prompt_from_package( + context, + native_resume=resume_same_pi_session or context.get("resume_mode") == "native", + ) if local_pi: if resume_same_pi_session: return ( @@ -2949,7 +3188,7 @@ def continuation_prompt( if role == "review": return f"Continue the review for {task.directory.resolve()}. Final in Korean." return ( - f"Continue from {locator.resolve()}. Check the saved context and current " + f"Continue from {locator.resolve() if locator else task.directory.resolve()}. Check the saved context and current " "workspace. Final in Korean." ) @@ -3297,6 +3536,91 @@ async def run_escalating( min(30, 2 ** min(terminal_recovery_retries, 5)) ) continue + if ( + isinstance(store, StateStore) + and role == "worker" + and isinstance(current_decision, dict) + ): + try: + next_decision = select_execution_decision( + task, + stage=role, + prior_decision=current_decision, + quota_snapshot=quota_snapshot, + transition="promotion", + failure_class=failure, + ) + except ExecutionDecisionError as exc: + if "no_promotion_target" in str(exc): + # canonical promotion 대상 없음: selector-backed worker는 + # 현재 target recovery/budget exhaustion 또는 block으로만 종결. + # legacy promoted_spec()으로의 fallthrough 금지. + banner( + "모델재시도", + task.name, + [ + f"model={spec.display}", + "reason=no-promotion-target", + *failure_report_lines(failure, locator), + f"retry={recovery_failures}/{RECOVERY_FAILURE_LIMIT}", + ], + ) + previous_locator = locator + terminal_recovery_retries += 1 + await asyncio.sleep( + min(30, 2 ** min(terminal_recovery_retries, 5)) + ) + continue + store.update_task( + task, + blocked=f"{role} selector promotion 실패: {exc}", + ) + banner( + "작업차단", + task.name, + [ + "reason=selector-promotion", + *failure_report_lines(failure, locator), + ], + ) + return False, locator + else: + next_spec = agent_spec_from_decision(next_decision) + if next_spec == spec: + store.update_task( + task, + blocked="selector promotion이 현재 target을 다시 선택했다", + ) + return False, locator + if locator is None: + store.update_task( + task, blocked="logical context locator가 없다" + ) + return False, locator + try: + context = build_context_package( + workspace, + task, + locator, + previous_spec=spec, + next_spec=next_spec, + ) + except ExecutionDecisionError as exc: + store.update_task(task, blocked=str(exc)) + return False, locator + commit_execution_decision(store, task, role, next_decision) + banner( + "모델승격", + task.name, + [ + f"from={spec.display}", + f"to={next_spec.display}", + *failure_report_lines(failure, locator), + ], + ) + spec = next_spec + previous_locator = locator + continue next_spec = promoted_spec(spec, codex_recovery_count) if next_spec is None: terminal_recovery_retries += 1 @@ -3559,12 +3883,16 @@ def completed_group_archive_directory( if not archive_raw: continue archive = Path(archive_raw) - complete_log = archive / "complete.log" - if not archive.is_dir() or not complete_log.is_file(): + if not archive.is_dir(): continue + complete_log = archive / "complete.log" target = archive if task_name == task_group else archive.parent try: - completed_at = complete_log.stat().st_mtime_ns + completed_at = ( + complete_log.stat().st_mtime_ns + if complete_log.is_file() + else archive.stat().st_mtime_ns + ) except OSError: continue candidates.append((completed_at, str(target), target)) @@ -4032,9 +4360,17 @@ async def run_review( return None -def status_lines(task: Task, stage: str, dependency: str) -> list[str]: +def status_lines( + task: Task, + stage: str, + dependency: str, + decision: dict[str, Any] | None = None, +) -> list[str]: route = f"{task.lane}-G{task.grade:02d}" if task.lane and task.grade else "recovery" - return [f"stage={stage}", f"route={route}", f"dependency={dependency}"] + base = [f"stage={stage}", f"route={route}", f"dependency={dependency}"] + if decision is not None: + return base + selector_evidence_lines(decision) + return base def select_dispatch_candidates( @@ -4559,18 +4895,33 @@ async def dispatch_with_store( for task in tasks: if task.name in ready_by_name: stage = ready_by_name[task.name] - spec = ( - AgentSpec("codex", "gpt-5.6-sol", "codex/gpt-5.6-sol xhigh") - if stage == "review" - else route_agent(task) + selector_stage = "review" if stage == "review" else "worker" + preview_state = store.peek_task_state(task) + decisions = preview_state.get("execution_decisions", {}) + prior_decision = ( + decisions.get(selector_stage) + if isinstance(decisions, dict) + else None ) + quota_snapshot = ( + preview_state.get("quota_snapshot") + if isinstance(preview_state.get("quota_snapshot"), dict) + else None + ) + decision = read_or_preview_stage_decision( + task, + preview_state, + stage=selector_stage, + dry_run=args.dry_run, + ) + spec = agent_spec_from_decision(decision) legacy_recovery = legacy_recoveries.get(task.name) if legacy_recovery is not None: failed_spec = failed_spec_from_recovery( legacy_recovery ) spec = promoted_spec(failed_spec, 0) or failed_spec - lines = status_lines(task, stage, "ready") + lines = status_lines(task, stage, "ready", decision=decision) if legacy_recovery is not None: lines.extend( [ diff --git a/agent-ops/skills/project/orchestrate-agent-task-loop/scripts/execution_target_policy.py b/agent-ops/skills/project/orchestrate-agent-task-loop/scripts/execution_target_policy.py index 778d166..f951970 100644 --- a/agent-ops/skills/project/orchestrate-agent-task-loop/scripts/execution_target_policy.py +++ b/agent-ops/skills/project/orchestrate-agent-task-loop/scripts/execution_target_policy.py @@ -45,6 +45,70 @@ AGY_GEMINI_HIGH = RouteTarget( PI_LAGUNA = RouteTarget("pi", "iop/laguna-s:2.1", "local_model", True) CLAUDE_OPUS = RouteTarget("claude", "claude-opus-4-8", "cloud_model", False) CODEX_SOL_XHIGH = RouteTarget("codex", "gpt-5.6-sol", "cloud_model", False) +CODEX_TERRA_HIGH = RouteTarget("codex", "gpt-5.6-terra", "cloud_model", False) + + +CANONICAL_TARGETS = ( + PI_ORNITH, + AGY_GEMINI_LOW, + AGY_GEMINI_MEDIUM, + AGY_GEMINI_HIGH, + PI_LAGUNA, + CLAUDE_OPUS, + CODEX_SOL_XHIGH, + CODEX_TERRA_HIGH, +) + + +def canonical_target(adapter: str, target: str) -> RouteTarget | None: + """Resolve one policy-owned adapter + target identity.""" + return next( + ( + candidate + for candidate in CANONICAL_TARGETS + if candidate.adapter == adapter and candidate.target == target + ), + None, + ) + + +def promotion_target(current: RouteTarget) -> RouteTarget | None: + """Return the next target in the policy-owned cloud promotion chain.""" + if current.adapter == "agy" and current in { + AGY_GEMINI_LOW, + AGY_GEMINI_MEDIUM, + AGY_GEMINI_HIGH, + }: + return CLAUDE_OPUS + if current == CLAUDE_OPUS: + return CODEX_TERRA_HIGH + return None + + +@dataclass(frozen=True) +class QuotaProbeSpec: + command: str + target: str + required_caps: tuple[str, ...] + + +def quota_probe_spec(target: RouteTarget) -> QuotaProbeSpec | None: + """Return the policy-owned quota probe spec for a route target.""" + if target.execution_class == "local_model": + return None + if target.adapter == "agy": + return QuotaProbeSpec( + command="agy", + target=target.target, + required_caps=("overall", f"model:{target.target}"), + ) + if target.adapter in {"claude", "codex"}: + return QuotaProbeSpec( + command=target.adapter, + target=target.target, + required_caps=("overall",), + ) + return None def _validate(stage: str, lane: str, grade: int, evaluated_at: datetime) -> None: diff --git a/agent-ops/skills/project/orchestrate-agent-task-loop/scripts/select_execution_target.py b/agent-ops/skills/project/orchestrate-agent-task-loop/scripts/select_execution_target.py index 588a61b..2436917 100644 --- a/agent-ops/skills/project/orchestrate-agent-task-loop/scripts/select_execution_target.py +++ b/agent-ops/skills/project/orchestrate-agent-task-loop/scripts/select_execution_target.py @@ -5,9 +5,9 @@ The selector consumes a static routing task file (``PLAN-*`` or ``CODE_REVIEW-*``), its ``task/plan/tag`` generation header and optional prior decision / quota snapshot, and returns a stable JSON contract that the dispatcher can persist. This module exposes the schema/invalid-input, -worker/review grade matrix, resume, and failover transitions at the selector -surface. Quota probe execution and live dispatcher failover integration are -owned by later tasks. +worker/review grade matrix, resume, failover, and policy-owned promotion +transitions at the selector surface. It also owns shell-less quota probe +normalization for live dispatcher routing. """ from __future__ import annotations @@ -16,6 +16,8 @@ import argparse import importlib.util import json import re +import shlex +import subprocess import sys from datetime import datetime from pathlib import Path @@ -28,7 +30,7 @@ DEFAULT_QUOTA_PROBE_COMMAND = "iop-node quota-probe" _FILENAME_RE = re.compile(r"^(PLAN|CODE_REVIEW)-(local|cloud)-G(\d{2})\.md$") _HEADER_RE = re.compile(r"") _STAGE_BY_KIND = {"PLAN": "worker", "CODE_REVIEW": "review"} -_VALID_TRANSITIONS = {"initial", "resume", "failover"} +_VALID_TRANSITIONS = {"initial", "resume", "failover", "promotion"} _VALID_EXECUTION_CLASSES = {"local_model", "cloud_model"} _VALID_QUOTA_MODES = {"unbounded", "bounded"} _QUALIFIED_FAILOVER_FAILURES = { @@ -37,6 +39,9 @@ _QUALIFIED_FAILOVER_FAILURES = { "model-unavailable", "provider-stream-disconnect", } +_QUALIFIED_PROMOTION_FAILURES = _QUALIFIED_FAILOVER_FAILURES | { + "provider-connection" +} _VALID_QUOTA_STATUSES = {"not_applicable", "available", "exhausted", "unknown"} _VALID_ELIGIBILITY = {"eligible", "ineligible"} @@ -333,7 +338,11 @@ def _validate_prior_decision(value: object) -> dict: return value -def _validate_quota_snapshot(value: object | None) -> dict | None: +def _validate_quota_snapshot( + value: object | None, + *, + require_producer_shape: bool = False, +) -> dict | None: """Validate the optional quota snapshot container before it is reflected. Required-cap tri-state normalization and admission stay with @@ -346,6 +355,11 @@ def _validate_quota_snapshot(value: object | None) -> dict | None: code = "malformed_quota_snapshot" if not isinstance(value, dict): raise SelectorInputError(code, "quota_snapshot must be an object") + if ( + require_producer_shape + and value.get("schema_version") != SCHEMA_VERSION + ): + raise SelectorInputError(code, "quota_snapshot.schema_version must be '1.0'") for field in ("snapshot_id", "checked_at"): if field in value and value[field] is not None and not isinstance( value[field], str @@ -353,11 +367,25 @@ def _validate_quota_snapshot(value: object | None) -> dict | None: raise SelectorInputError( code, f"quota_snapshot.{field} must be null or a string" ) + if require_producer_shape and ( + field not in value or not value[field] + ): + raise SelectorInputError( + code, f"quota_snapshot.{field} must be a non-empty string" + ) if "source" in value: _require_non_empty_string(value, "source", "quota_snapshot", code) + elif require_producer_shape: + raise SelectorInputError( + code, "quota_snapshot.source must be a non-empty string" + ) targets = value.get("targets", []) if not isinstance(targets, list): raise SelectorInputError(code, "quota_snapshot.targets must be a list") + if require_producer_shape and not targets: + raise SelectorInputError( + code, "quota_snapshot.targets must be a non-empty list" + ) for index, entry in enumerate(targets): if not isinstance(entry, dict): raise SelectorInputError( @@ -371,6 +399,46 @@ def _validate_quota_snapshot(value: object | None) -> dict | None: f"quota_snapshot.targets[{index}].{field} must be a " "non-empty string", ) + if entry["status"] not in {"available", "exhausted", "unknown"}: + raise SelectorInputError( + code, + f"quota_snapshot.targets[{index}].status must be available, " + "exhausted, or unknown", + ) + if require_producer_shape: + caps = value.get("required_caps") + if not isinstance(caps, list) or not caps: + raise SelectorInputError( + code, "quota_snapshot.required_caps must be a non-empty list" + ) + for index, cap in enumerate(caps): + prefix = f"quota_snapshot.required_caps[{index}]" + if not isinstance(cap, dict): + raise SelectorInputError(code, f"{prefix} must be an object") + _require_non_empty_string(cap, "name", prefix, code) + _require_string_enum( + cap, + "status", + {"available", "exhausted", "unknown"}, + prefix, + code, + ) + remaining = cap.get("remaining_percent") + if remaining is not None and ( + isinstance(remaining, bool) + or not isinstance(remaining, (int, float)) + ): + raise SelectorInputError( + code, f"{prefix}.remaining_percent must be null or numeric" + ) + reasons = value.get("reason_codes") + if not isinstance(reasons, list) or not all( + isinstance(reason, str) and reason for reason in reasons + ): + raise SelectorInputError( + code, + "quota_snapshot.reason_codes must be a list of non-empty strings", + ) return value @@ -396,14 +464,88 @@ def _snapshot_status(target, quota_snapshot: dict | None) -> str: return "unknown" -def _candidate_quota(target, quota_snapshot: dict | None) -> tuple[str, str]: +def probe_candidate_quota( + *, + target: str, + adapter: str, + required_caps: tuple[str, ...] | list[str], + checked_at: datetime, + quota_probe_command: str = DEFAULT_QUOTA_PROBE_COMMAND, +) -> dict: + """Execute shell-less quota probe command and return a snapshot dict.""" + checked_at_iso = checked_at.astimezone(policy.KST).isoformat() + try: + cmd = shlex.split(quota_probe_command) + cmd.extend(["--target", target, "--command", adapter]) + for cap in required_caps: + cmd.extend(["--required-cap", cap]) + cmd.extend(["--checked-at", checked_at_iso]) + res = subprocess.run(cmd, capture_output=True, text=True, timeout=5) + if res.returncode == 0 and res.stdout: + snapshot = _validate_quota_snapshot( + json.loads(res.stdout), require_producer_shape=True + ) + assert snapshot is not None + if not any( + entry["adapter"] == adapter and entry["target"] == target + for entry in snapshot["targets"] + ): + raise SelectorInputError( + "malformed_quota_snapshot", + "quota snapshot does not contain the requested target identity", + ) + return snapshot + except Exception: + pass + + return { + "schema_version": SCHEMA_VERSION, + "snapshot_id": None, + "source": quota_probe_command, + "checked_at": checked_at_iso, + "targets": [ + {"adapter": adapter, "target": target, "status": "unknown"} + ], + "required_caps": [ + { + "name": cap, + "status": "unknown", + "remaining_percent": None, + } + for cap in required_caps + ], + "reason_codes": ["probe_error"], + } + + +def _candidate_quota( + target, + quota_snapshot: dict | None, + evaluated_at: datetime, + quota_probe_command: str, +) -> tuple[str, str, dict | None]: if target.execution_class == "local_model": - return "unbounded", "not_applicable" - return "bounded", _snapshot_status(target, quota_snapshot) + return "unbounded", "not_applicable", None + if quota_snapshot is not None: + return "bounded", _snapshot_status(target, quota_snapshot), quota_snapshot + probe_spec = policy.quota_probe_spec(target) + if probe_spec is not None: + snapshot = probe_candidate_quota( + target=target.target, + adapter=target.adapter, + required_caps=probe_spec.required_caps, + checked_at=evaluated_at, + quota_probe_command=quota_probe_command, + ) + return "bounded", _snapshot_status(target, snapshot), snapshot + return "bounded", "unknown", None def _selected_quota( - selected, quota_snapshot: dict | None, quota_probe_command: str + selected, + quota_snapshot: dict | None, + quota_probe_command: str, + probed_snapshot: dict | None = None, ) -> dict: if selected.execution_class == "local_model": return { @@ -412,21 +554,37 @@ def _selected_quota( "status": "not_applicable", "source": "local_unbounded", "checked_at": None, + "targets": [], } - if quota_snapshot is None: + if quota_snapshot is not None: return { - "snapshot_id": None, + "snapshot_id": quota_snapshot.get("snapshot_id"), "mode": "bounded", - "status": "unknown", - "source": quota_probe_command, - "checked_at": None, + "status": _snapshot_status(selected, quota_snapshot), + "source": quota_snapshot.get("source", quota_probe_command), + "checked_at": quota_snapshot.get("checked_at"), + "targets": [ + dict(entry) for entry in quota_snapshot.get("targets", []) + ], + } + if probed_snapshot is not None: + return { + "snapshot_id": probed_snapshot.get("snapshot_id"), + "mode": "bounded", + "status": _snapshot_status(selected, probed_snapshot), + "source": probed_snapshot.get("source", quota_probe_command), + "checked_at": probed_snapshot.get("checked_at"), + "targets": [ + dict(entry) for entry in probed_snapshot.get("targets", []) + ], } return { - "snapshot_id": quota_snapshot.get("snapshot_id"), + "snapshot_id": None, "mode": "bounded", - "status": _snapshot_status(selected, quota_snapshot), - "source": quota_snapshot.get("source", quota_probe_command), - "checked_at": quota_snapshot.get("checked_at"), + "status": "unknown", + "source": quota_probe_command, + "checked_at": None, + "targets": [], } @@ -448,8 +606,11 @@ def _initial( raise SelectorInputError("invalid_route", str(exc)) from exc candidates = [] selected = None + selected_probed_snapshot = None for rank, target in enumerate(decision.candidates, start=1): - mode, status = _candidate_quota(target, quota_snapshot) + mode, status, probed_snapshot = _candidate_quota( + target, quota_snapshot, evaluated_at, quota_probe_command + ) eligible = status != "exhausted" candidates.append( { @@ -466,6 +627,7 @@ def _initial( ) if eligible and selected is None: selected = target + selected_probed_snapshot = probed_snapshot if selected is None: raise SelectorInputError( "no_eligible_target", @@ -493,7 +655,9 @@ def _initial( "time_window": decision.time_window, "pinned": False, }, - "quota": _selected_quota(selected, quota_snapshot, quota_probe_command), + "quota": _selected_quota( + selected, quota_snapshot, quota_probe_command, selected_probed_snapshot + ), "transition": { "previous_target": None, "next_target": None, @@ -503,6 +667,7 @@ def _initial( } + def _validate_selected_and_used_history( prior_decision: dict, canonical_targets: list, @@ -516,24 +681,60 @@ def _validate_selected_and_used_history( canon_keys_list = [(c.adapter, c.target) for c in canonical_targets] canon_keys_set = set(canon_keys_list) - if sel_key not in canon_keys_set: + matching_cand = policy.canonical_target(*sel_key) + if matching_cand is None: raise SelectorInputError( code, - f"prior_decision.selected {sel_key} is not in canonical policy targets {canon_keys_set}", + f"prior_decision.selected {sel_key} is not a policy-owned target", + ) + if ( + selected.get("execution_class") != matching_cand.execution_class + or selected.get("selfcheck_required") != matching_cand.selfcheck_required + ): + raise SelectorInputError( + code, + f"prior_decision.selected attributes do not match canonical target for {sel_key}", ) - matching_cand = next( - (c for c in canonical_targets if (c.adapter, c.target) == sel_key), None - ) - if matching_cand is not None: - if ( - selected.get("execution_class") != matching_cand.execution_class - or selected.get("selfcheck_required") != matching_cand.selfcheck_required - ): + if sel_key not in canon_keys_set: + promotion_path = prior_decision.get("promotion_path") + if not isinstance(promotion_path, list) or len(promotion_path) < 2: raise SelectorInputError( code, - f"prior_decision.selected attributes do not match canonical candidate for {sel_key}", + "promoted prior_decision requires promotion_path evidence", ) + path_targets = [] + for index, entry in enumerate(promotion_path): + if not isinstance(entry, dict): + raise SelectorInputError( + code, f"promotion_path[{index}] must be an object" + ) + target = policy.canonical_target( + entry.get("adapter"), entry.get("target") + ) + if target is None: + raise SelectorInputError( + code, f"promotion_path[{index}] is not policy-owned" + ) + path_targets.append(target) + if (path_targets[0].adapter, path_targets[0].target) not in canon_keys_set: + raise SelectorInputError( + code, "promotion_path must begin at the initial policy target" + ) + for previous, current in zip(path_targets, path_targets[1:]): + if policy.promotion_target(previous) != current: + raise SelectorInputError( + code, "promotion_path contains a non-adjacent transition" + ) + if path_targets[-1] != matching_cand: + raise SelectorInputError( + code, "promotion_path tail does not match selected target" + ) + if "used_candidates" in prior_decision: + raise SelectorInputError( + code, "promotion decision must not carry failover used_candidates" + ) + return if "used_candidates" in prior_decision: used = prior_decision["used_candidates"] @@ -712,6 +913,7 @@ def _resume( "decision": decision, "quota": prior_decision["quota"], **({"used_candidates": _validate_used_candidates(prior_decision.get("used_candidates"))} if "used_candidates" in prior_decision else {}), + **({"promotion_path": prior_decision["promotion_path"]} if "promotion_path" in prior_decision else {}), "transition": { "previous_target": target_ref, "next_target": dict(target_ref), @@ -765,22 +967,41 @@ def _failover( used.append(previous) used_set = {(entry["adapter"], entry["target"]) for entry in used} selected_candidate = None + selected_probed_snapshot = None candidates = [] for index, candidate in enumerate(prior["candidates"]): current = dict(candidate) + current_snapshot = None if current["execution_class"] != "local_model": - status = ( - "exhausted" - if failure_class == "provider-quota" and _target_ref(current) == previous - else _snapshot_status(type("Target", (), current)(), quota_snapshot) - ) + if failure_class == "provider-quota" and _target_ref(current) == previous: + status = "exhausted" + elif quota_snapshot is not None: + current_snapshot = quota_snapshot + status = _snapshot_status(type("Target", (), current)(), quota_snapshot) + else: + cand_obj = type("Target", (), current)() + probe_spec = policy.quota_probe_spec(cand_obj) + if probe_spec is not None: + sn = probe_candidate_quota( + target=current["target"], + adapter=current["adapter"], + required_caps=probe_spec.required_caps, + checked_at=evaluated_at, + quota_probe_command=quota_probe_command, + ) + current_snapshot = sn + status = _snapshot_status(cand_obj, sn) + else: + status = "unknown" current["quota_status"] = status + current["eligibility"] = "ineligible" if status == "exhausted" else "eligible" current["rejection_reason"] = "quota_exhausted" if status == "exhausted" else None candidates.append(current) key = (current["adapter"], current["target"]) if index > previous_index and key not in used_set and current["eligibility"] == "eligible" and selected_candidate is None: selected_candidate = current + selected_probed_snapshot = current_snapshot if selected_candidate is None: raise SelectorInputError("no_failover_candidate", "no unused eligible candidate remains for this work unit") selected = {field: selected_candidate[field] for field in ("adapter", "target", "execution_class", "selfcheck_required")} @@ -793,7 +1014,12 @@ def _failover( "schema_version": SCHEMA_VERSION, "work_unit_id": work_unit_id, "stage": stage, "lane": lane, "grade": grade, "selected": selected, "candidates": candidates, "decision": decision, - "quota": _selected_quota(selected_target, quota_snapshot, quota_probe_command), + "quota": _selected_quota( + selected_target, + quota_snapshot, + quota_probe_command, + selected_probed_snapshot, + ), "used_candidates": used, "transition": { "previous_target": previous, "next_target": next_target, @@ -803,6 +1029,98 @@ def _failover( } +def _promotion( + prior_decision: dict | None, + *, + work_unit_id: str, + stage: str, + lane: str, + grade: int, + evaluated_at: datetime, + quota_snapshot: dict | None, + quota_probe_command: str, + failure_class: str | None, +) -> dict: + if failure_class not in _QUALIFIED_PROMOTION_FAILURES: + raise SelectorInputError( + "unqualified_promotion_trigger", + "promotion requires one of " + f"{sorted(_QUALIFIED_PROMOTION_FAILURES)}", + ) + if prior_decision is None: + raise SelectorInputError( + "promotion_requires_prior_decision", + "promotion transition requires prior_decision", + ) + prior = _validate_prior_decision(prior_decision) + for key, expected in ( + ("work_unit_id", work_unit_id), + ("stage", stage), + ("lane", lane), + ("grade", grade), + ): + if prior[key] != expected: + raise SelectorInputError( + "promotion_work_unit_mismatch", + f"prior_decision {key}={prior[key]!r} != {expected!r}", + ) + _validate_prior_candidate_identity( + prior, stage=stage, lane=lane, grade=grade + ) + if len(prior["candidates"]) != 1: + raise SelectorInputError( + "no_promotion_target", + "multi-candidate policy routes use failover instead of promotion", + ) + current = policy.canonical_target( + prior["selected"]["adapter"], prior["selected"]["target"] + ) + promoted = policy.promotion_target(current) if current is not None else None + if promoted is None: + raise SelectorInputError( + "no_promotion_target", + "no unused canonical promotion target remains for this work unit", + ) + previous_target = {"adapter": current.adapter, "target": current.target} + next_target = {"adapter": promoted.adapter, "target": promoted.target} + promotion_path = list(prior.get("promotion_path", [previous_target])) + if not promotion_path or promotion_path[-1] != previous_target: + raise SelectorInputError( + "malformed_prior_decision", + "promotion_path tail does not match the selected target", + ) + promotion_path.append(next_target) + decision = dict(prior["decision"]) + decision["pinned"] = True + return { + "schema_version": SCHEMA_VERSION, + "work_unit_id": work_unit_id, + "stage": stage, + "lane": lane, + "grade": grade, + "selected": { + "adapter": promoted.adapter, + "target": promoted.target, + "execution_class": promoted.execution_class, + "selfcheck_required": promoted.selfcheck_required, + }, + "candidates": prior["candidates"], + "decision": decision, + "promotion_path": promotion_path, + "quota": _selected_quota( + promoted, quota_snapshot, quota_probe_command + ), + "transition": { + "kind": "promotion", + "previous_target": previous_target, + "next_target": next_target, + "trigger": failure_class, + "context_transfer": "logical", + "evaluated_at": evaluated_at.astimezone(policy.KST).isoformat(), + }, + } + + def select_execution_target( task_file: Path, *, @@ -860,6 +1178,14 @@ def select_execution_target( quota_snapshot=quota_snapshot, quota_probe_command=quota_probe_command, failure_class=failure_class, ) + if transition == "promotion": + return _promotion( + prior_decision, work_unit_id=work_unit_id, stage=resolved_stage, + lane=lane, grade=grade, evaluated_at=evaluated_at, + quota_snapshot=quota_snapshot, + quota_probe_command=quota_probe_command, + failure_class=failure_class, + ) raise SelectorInputError( "invalid_transition", f"unknown transition: {transition!r}" ) diff --git a/agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py b/agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py index 05e3a9c..7e0fe96 100644 --- a/agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py +++ b/agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py @@ -292,7 +292,8 @@ class TaskStageTest(unittest.TestCase): task.plan.rename(graded_plan) task.plan = graded_plan task.grade = grade - spec = dispatch.route_agent(task) + decision = dispatch.select_execution_decision(task, stage="worker") + spec = dispatch.agent_spec_from_decision(decision) self.assertEqual(spec.cli, cli) self.assertEqual(spec.model, model) self.assertEqual(spec.local_pi, local_pi) @@ -5699,15 +5700,35 @@ class DispatcherCanonicalFailoverIntegrationTest(unittest.IsolatedAsyncioTestCas store = dispatch.StateStore(workspace) try: claude_spec = dispatch.AgentSpec("claude", "claude-opus-4-8", "claude/claude-opus-4-8 xhigh") - locator = self.make_attempt_locator(workspace, task, claude_spec) + terra_spec = dispatch.AgentSpec( + "codex", + "gpt-5.6-terra", + "codex/gpt-5.6-terra high", + reasoning_effort="high", + ) + claude_locator = self.make_attempt_locator( + workspace, task, claude_spec + ) + terra_locator = self.make_attempt_locator( + workspace, task, terra_spec + ) invoked_specs = [] + invoked_prompts = [] + transition_budget_counts = [] async def mock_invoke(*args, **kwargs): spec = args[4] invoked_specs.append(spec) + invoked_prompts.append(args[5]) if spec.cli == "claude": - return (1, "provider-quota", locator) - return (0, None, locator) + return (1, "provider-quota", claude_locator) + state = store.task_state(task) + decision = state["execution_decisions"]["worker"] + budget = dispatch.StageFailureBudget.from_decision( + store, task, decision + ) + transition_budget_counts.append(budget.count()) + return (0, None, terra_locator) with ( mock.patch.object(dispatch, "invoke", new=mock_invoke), @@ -5717,14 +5738,145 @@ class DispatcherCanonicalFailoverIntegrationTest(unittest.IsolatedAsyncioTestCas success, final_loc = await dispatch.run_escalating(workspace, store, task, "worker", claude_spec) self.assertTrue(success) + self.assertEqual(final_loc, terra_locator) self.assertEqual(len(invoked_specs), 2) - self.assertEqual(invoked_specs[0].cli, "claude") - self.assertEqual(invoked_specs[1].cli, "codex") + self.assertEqual(invoked_specs, [claude_spec, terra_spec]) + self.assertEqual(transition_budget_counts, [1]) + continuation = invoked_prompts[1] + self.assertIn(str(task.plan.resolve()), continuation) + self.assertIn(str(claude_locator.resolve()), continuation) + self.assertIn(str(workspace.resolve()), continuation) + self.assertIn( + str((claude_locator.parent / "stream.log").resolve()), + continuation, + ) + self.assertIn( + str( + (claude_locator.parent / "normalized-output.log").resolve() + ), + continuation, + ) state = store.task_state(task) + decision = state["execution_decisions"]["worker"] + self.assertEqual(decision["selected"]["adapter"], "codex") + self.assertEqual(decision["selected"]["target"], "gpt-5.6-terra") + self.assertEqual(decision["transition"]["kind"], "promotion") + self.assertEqual( + decision["transition"]["trigger"], "provider-quota" + ) + self.assertEqual( + [entry["transition"] for entry in state["route_transition_history"]], + ["initial", "provider-quota"], + ) + budget = dispatch.StageFailureBudget.from_decision( + store, task, decision + ) + self.assertEqual(budget.count(), 0) self.assertNotIn("no_failover_candidate", state.get("blocked") or "") finally: store.close() + async def test_cloud_agy_promotion_chain_commits_each_transition(self): + daytime = datetime( + 2026, 7, 26, 14, 0, 0, tzinfo=timezone(timedelta(hours=9)) + ) + with tempfile.TemporaryDirectory() as temporary: + workspace = Path(temporary) + (workspace / ".git").mkdir() + task = self.make_task(workspace, lane="cloud", grade=5) + store = dispatch.StateStore(workspace) + try: + agy_spec = dispatch.AgentSpec( + "agy", + "Gemini 3.6 Flash (High)", + "agy/Gemini 3.6 Flash (High)", + ) + claude_spec = dispatch.AgentSpec( + "claude", + "claude-opus-4-8", + "claude/claude-opus-4-8 xhigh", + ) + terra_spec = dispatch.AgentSpec( + "codex", + "gpt-5.6-terra", + "codex/gpt-5.6-terra high", + reasoning_effort="high", + ) + locators = { + spec.cli: self.make_attempt_locator(workspace, task, spec) + for spec in (agy_spec, claude_spec, terra_spec) + } + invoked_specs = [] + invoked_prompts = [] + transition_budget_counts = [] + + async def mock_invoke(*args, **kwargs): + spec = args[4] + invoked_specs.append(spec) + invoked_prompts.append(args[5]) + if spec.cli == "agy": + return (1, "provider-quota", locators["agy"]) + state = store.task_state(task) + decision = state["execution_decisions"]["worker"] + budget = dispatch.StageFailureBudget.from_decision( + store, task, decision + ) + transition_budget_counts.append(budget.count()) + if spec.cli == "claude": + return (1, "context-limit", locators["claude"]) + return (0, None, locators["codex"]) + + with ( + mock.patch.object(dispatch, "invoke", new=mock_invoke), + mock.patch.object( + dispatch.asyncio, "sleep", new=mock.AsyncMock() + ), + ): + dispatch.persisted_execution_decision( + store, task, stage="worker", evaluated_at=daytime + ) + success, final_locator = await dispatch.run_escalating( + workspace, store, task, "worker", agy_spec + ) + + self.assertTrue(success) + self.assertEqual(final_locator, locators["codex"]) + self.assertEqual( + invoked_specs, [agy_spec, claude_spec, terra_spec] + ) + self.assertEqual(transition_budget_counts, [1, 2]) + self.assertIn( + str(locators["agy"].resolve()), invoked_prompts[1] + ) + self.assertIn( + str(locators["claude"].resolve()), invoked_prompts[2] + ) + state = store.task_state(task) + decision = state["execution_decisions"]["worker"] + self.assertEqual( + decision["promotion_path"], + [ + { + "adapter": "agy", + "target": "Gemini 3.6 Flash (High)", + }, + {"adapter": "claude", "target": "claude-opus-4-8"}, + {"adapter": "codex", "target": "gpt-5.6-terra"}, + ], + ) + self.assertEqual( + [entry["transition"] for entry in state["route_transition_history"]], + ["initial", "provider-quota", "context-limit"], + ) + self.assertEqual( + dispatch.StageFailureBudget.from_decision( + store, task, decision + ).count(), + 0, + ) + finally: + store.close() + async def test_night_laguna_failure_continues_on_available_gemini(self): nighttime = datetime(2026, 7, 26, 1, 0, 0, tzinfo=timezone(timedelta(hours=9))) with tempfile.TemporaryDirectory() as temporary: @@ -5891,6 +6043,225 @@ class DispatcherCanonicalFailoverIntegrationTest(unittest.IsolatedAsyncioTestCas finally: store.close() + async def test_no_promotion_target_keeps_same_target_and_persists_state(self): + """local-G08 daytime: provider-connection x2 → success. Same AGY target, delay [2, 4], budget reset. + + The selector-backed worker must NOT fall through to legacy promoted_spec(). + Invocation target, persisted selected, history, and terminal recovery backoff + must all agree on AGY with bounded exponential backoff. + """ + daytime = datetime(2026, 7, 26, 14, 0, 0, tzinfo=timezone(timedelta(hours=9))) + with tempfile.TemporaryDirectory() as temporary: + workspace = Path(temporary) + (workspace / ".git").mkdir() + task = self.make_task(workspace) + store = dispatch.StateStore(workspace) + try: + gemini_spec = dispatch.AgentSpec( + "agy", + "Gemini 3.6 Flash (Medium)", + "agy/Gemini 3.6 Flash (Medium)", + ) + locator = self.make_attempt_locator(workspace, task, gemini_spec) + invoked_specs = [] + sleep_delays = [] + + async def mock_invoke(*args, **kwargs): + spec = args[4] + invoked_specs.append(spec) + if len(invoked_specs) <= 2: + return (1, "provider-connection", locator) + return (0, None, locator) + + async def observe_sleep(delay): + sleep_delays.append(delay) + + with ( + mock.patch.object(dispatch, "invoke", new=mock_invoke), + mock.patch.object( + dispatch.asyncio, + "sleep", + new=mock.AsyncMock(side_effect=observe_sleep), + ), + ): + dispatch.persisted_execution_decision(store, task, stage="worker", evaluated_at=daytime) + initial_state = store.task_state(task) + initial_selected = dict(initial_state["execution_decisions"]["worker"]["selected"]) + initial_history = list(initial_state["route_transition_history"]) + + success, final_loc = await dispatch.run_escalating( + workspace, store, task, "worker", gemini_spec + ) + + self.assertTrue(success) + # Two failures then one success = 3 invocations + self.assertEqual(len(invoked_specs), 3) + # All invocations must be on AGY — no legacy AGY→Claude fallthrough + for i, spec in enumerate(invoked_specs): + self.assertEqual(spec.cli, "agy", f"invocation {i} target mismatch") + self.assertEqual(spec.model, "Gemini 3.6 Flash (Medium)", f"invocation {i} model mismatch") + + # Terminal backoff: retries go 0→1→2, delays = [2**1, 2**2] = [2, 4] + self.assertEqual(len(sleep_delays), 2, f"expected 2 sleep calls, got {len(sleep_delays)}") + self.assertEqual(sleep_delays[0], 2) + self.assertEqual(sleep_delays[1], 4) + + state = store.task_state(task) + # Persisted selected must NOT change from initial AGY + self.assertEqual( + state["execution_decisions"]["worker"]["selected"], + initial_selected, + ) + # History must NOT have a promotion entry + self.assertEqual( + state["route_transition_history"], + initial_history, + ) + # No block should be set after success + self.assertIsNone(state.get("blocked")) + # After success, stage failure budget count must be 0 + worker_decision = state["execution_decisions"]["worker"] + worker_budget = dispatch.StageFailureBudget.from_decision(store, task, worker_decision) + self.assertEqual(worker_budget.count(), 0) + finally: + store.close() + + async def test_promotion_chain_exhaustion_stays_on_last_target(self): + """Cloud promotion chain: AGY→Claude→Terra exhausted. + + When the last canonical target (Terra) fails with a promotable failure + and no promotion target remains, the worker must stay on Terra for + same-target recovery rather than falling through to legacy promoted_spec(). + """ + daytime = datetime( + 2026, 7, 26, 14, 0, 0, tzinfo=timezone(timedelta(hours=9)) + ) + with tempfile.TemporaryDirectory() as temporary: + workspace = Path(temporary) + (workspace / ".git").mkdir() + task = self.make_task(workspace, lane="cloud", grade=5) + store = dispatch.StateStore(workspace) + try: + agy_spec = dispatch.AgentSpec( + "agy", "Gemini 3.6 Flash (High)", "agy/Gemini 3.6 Flash (High)" + ) + claude_spec = dispatch.AgentSpec( + "claude", "claude-opus-4-8", "claude/claude-opus-4-8 xhigh" + ) + terra_spec = dispatch.AgentSpec( + "codex", "gpt-5.6-terra", "codex/gpt-5.6-terra high", + reasoning_effort="high", + ) + loc_agy = self.make_attempt_locator(workspace, task, agy_spec) + loc_claude = self.make_attempt_locator(workspace, task, claude_spec) + loc_terra = self.make_attempt_locator(workspace, task, terra_spec) + invoked_specs = [] + + async def mock_invoke(*args, **kwargs): + spec = args[4] + invoked_specs.append(spec) + if spec.cli == "agy": + return (1, "provider-quota", loc_agy) + if spec.cli == "claude": + return (1, "context-limit", loc_claude) + # Terra fails once, then succeeds — chain exhaustion keeps it on Terra + terra_count = sum(1 for s in invoked_specs if s.cli == "codex") + if terra_count == 1: + return (1, "provider-quota", loc_terra) + return (0, None, loc_terra) + + with ( + mock.patch.object(dispatch, "invoke", new=mock_invoke), + mock.patch.object(dispatch.asyncio, "sleep", new=mock.AsyncMock()), + ): + dispatch.persisted_execution_decision( + store, task, stage="worker", evaluated_at=daytime + ) + success, final_loc = await dispatch.run_escalating( + workspace, store, task, "worker", agy_spec + ) + + # Chain: AGY → Claude → Terra, then Terra retries on same target (no legacy fallthrough) + self.assertEqual( + [s.cli for s in invoked_specs], + ["agy", "claude", "codex", "codex"], + ) + # The third and fourth invocations are both Terra (same-target recovery) + self.assertEqual(invoked_specs[2].cli, "codex") + self.assertEqual(invoked_specs[2].model, "gpt-5.6-terra") + self.assertEqual(invoked_specs[3].cli, "codex") + self.assertEqual(invoked_specs[3].model, "gpt-5.6-terra") + + state = store.task_state(task) + decision = state["execution_decisions"]["worker"] + # Promotion path should include all three transitions + self.assertEqual( + len(decision["promotion_path"]), 3, + ) + # History should show the promotions but no legacy recovery + transitions = [h["transition"] for h in state["route_transition_history"]] + self.assertIn("provider-quota", transitions) + self.assertIn("context-limit", transitions) + # No legacy promoted_spec() fallthrough: selected stays on Terra + self.assertEqual( + decision["selected"]["adapter"], "codex" + ) + self.assertEqual( + decision["selected"]["target"], "gpt-5.6-terra" + ) + finally: + store.close() + + async def test_legacy_promoted_spec_still_works_for_non_selector_worker(self): + """Ensure legacy promoted_spec() path is preserved for non-selector workers.""" + daytime = datetime(2026, 7, 26, 14, 0, 0, tzinfo=timezone(timedelta(hours=9))) + with tempfile.TemporaryDirectory() as temporary: + workspace = Path(temporary) + (workspace / ".git").mkdir() + # Use a task that does NOT have a persisted selector decision + # so the selector promotion block is skipped entirely. + directory = workspace / "agent-task" / "legacy_recovery_test" + directory.mkdir(parents=True, exist_ok=True) + header = "\n" + (directory / "PLAN-local-G08.md").write_text(header, encoding="utf-8") + (directory / "CODE_REVIEW-local-G08.md").write_text(header, encoding="utf-8") + tasks = dispatch.scan_tasks(workspace, None) + task = tasks[0] + store = dispatch.StateStore(workspace) + try: + agy_spec = dispatch.AgentSpec( + "agy", "Gemini 3.6 Flash (High)", "agy/Gemini 3.6 Flash (High)" + ) + claude_spec = dispatch.AgentSpec( + "claude", "claude-opus-4-8", "claude/claude-opus-4-8 xhigh" + ) + locator = self.make_attempt_locator(workspace, task, agy_spec) + invoked_specs = [] + + async def mock_invoke(*args, **kwargs): + spec = args[4] + invoked_specs.append(spec) + if spec.cli == "agy": + return (1, "provider-quota", locator) + return (0, None, locator) + + with ( + mock.patch.object(dispatch, "invoke", new=mock_invoke), + mock.patch.object(dispatch.asyncio, "sleep", new=mock.AsyncMock()), + ): + # Do NOT persist a selector decision — legacy path only + success, final_loc = await dispatch.run_escalating( + workspace, store, task, "worker", agy_spec + ) + + self.assertTrue(success) + # Legacy path: AGY → Claude (via promoted_spec) + self.assertEqual(len(invoked_specs), 2) + self.assertEqual(invoked_specs[0].cli, "agy") + self.assertEqual(invoked_specs[1].cli, "claude") + finally: + store.close() + async def test_completing_target_controls_selfcheck_and_reuses_pin(self): daytime = datetime(2026, 7, 26, 14, 0, 0, tzinfo=timezone(timedelta(hours=9))) nighttime = datetime(2026, 7, 26, 1, 0, 0, tzinfo=timezone(timedelta(hours=9))) @@ -6022,8 +6393,368 @@ class DispatcherCanonicalFailoverIntegrationTest(unittest.IsolatedAsyncioTestCas store.close() -if __name__ == "__main__": - unittest.main() +class SelectorDispatcherIntegrationTest(unittest.IsolatedAsyncioTestCase): + def make_task( + self, workspace: Path, lane: str = "local", grade: int = 8, unit: str = "01_unit" + ) -> dispatch.Task: + directory = workspace / "agent-task" / "selector_dispatch_integration" / unit + directory.mkdir(parents=True, exist_ok=True) + header = f"\n" + (directory / f"PLAN-{lane}-G{grade:02d}.md").write_text(header, encoding="utf-8") + (directory / f"CODE_REVIEW-{lane}-G{grade:02d}.md").write_text(header, encoding="utf-8") + tasks = dispatch.scan_tasks(workspace, None) + for task in tasks: + if task.name.endswith(unit): + return task + return tasks[0] + + def make_attempt_locator( + self, workspace: Path, task: dispatch.Task, spec: dispatch.AgentSpec + ) -> Path: + attempt = workspace / f"attempt-{spec.cli}" + attempt.mkdir(parents=True, exist_ok=True) + raw, normalized = attempt / "stream.log", attempt / "normalized-output.log" + raw.write_text("raw log\n", encoding="utf-8") + normalized.write_text("normalized output\n", encoding="utf-8") + locator = attempt / "locator.json" + record = { + "task": task.name, + "workspace": str(workspace), + "plan_path": str(task.plan), + "stream_log": str(raw), + "normalized_output_log": str(normalized), + "cli": spec.cli, + "model": spec.model, + "spec": {"adapter": spec.cli, "target": spec.model}, + } + locator.write_text(json.dumps(record), encoding="utf-8") + return locator + + async def test_worker_and_review_initial_invocation_uses_selector(self): + daytime = datetime(2026, 7, 26, 14, 0, 0, tzinfo=timezone(timedelta(hours=9))) + with tempfile.TemporaryDirectory() as temporary: + workspace = Path(temporary) + (workspace / ".git").mkdir() + task = self.make_task(workspace, lane="local", grade=8) + store = dispatch.StateStore(workspace) + try: + with mock.patch.object(dispatch, "run_escalating") as run_escalating_mock, \ + mock.patch.object(dispatch, "datetime") as datetime_mock: + datetime_mock.now.return_value = daytime + run_escalating_mock.side_effect = lambda ws, st, t, stage, spec, **kwargs: ( + True, self.make_attempt_locator(ws, t, spec) + ) + + await dispatch.run_worker(workspace, store, task, {}) + self.assertEqual(run_escalating_mock.call_count, 1) + call_args = run_escalating_mock.call_args[0] + self.assertEqual(call_args[3], "worker") + spec_worker = call_args[4] + self.assertEqual(spec_worker.cli, "agy") + self.assertEqual(spec_worker.model, "Gemini 3.6 Flash (Medium)") + + state_after_worker = store.task_state(task) + self.assertTrue(state_after_worker.get("worker_done")) + self.assertIn("worker", state_after_worker.get("execution_decisions", {})) + self.assertEqual( + state_after_worker["execution_decisions"]["worker"]["selected"]["target"], + "Gemini 3.6 Flash (Medium)", + ) + + await dispatch.run_review(workspace, store, task) + self.assertEqual(run_escalating_mock.call_count, 2) + call_args2 = run_escalating_mock.call_args[0] + self.assertEqual(call_args2[3], "review") + spec_review = call_args2[4] + self.assertEqual(spec_review.cli, "codex") + self.assertEqual(spec_review.model, "gpt-5.6-sol") + self.assertEqual(spec_review.display, "codex/gpt-5.6-sol xhigh") + + state_after_review = store.task_state(task) + self.assertIn("review", state_after_review.get("execution_decisions", {})) + self.assertNotEqual( + state_after_review["execution_decisions"]["worker"]["selected"], + state_after_review["execution_decisions"]["review"]["selected"], + ) + finally: + store.close() + + async def test_dry_run_statelessness_initial_and_resume_previews(self): + daytime = datetime(2026, 7, 26, 14, 0, 0, tzinfo=timezone(timedelta(hours=9))) + with tempfile.TemporaryDirectory() as temporary: + workspace = Path(temporary) + (workspace / ".git").mkdir() + task = self.make_task(workspace, lane="local", grade=8) + store = dispatch.StateStore(workspace) + try: + # 1. Non-persisted dry-run (initial preview) + args = dispatch.argparse.Namespace( + workspace=str(workspace), + task_group=None, + retry_blocked=False, + dry_run=True, + ) + with mock.patch.object(dispatch, "datetime") as datetime_mock: + datetime_mock.now.return_value = daytime + result = await dispatch.dispatch_with_store(args, workspace, store) + + self.assertEqual(result, 0) + state_initial = store.task_state(task) + self.assertEqual(state_initial.get("execution_decisions"), {}) + self.assertEqual(state_initial.get("route_transition_history"), []) + + # 2. Persist decision and test dry-run (read-only resume preview) + dispatch.persisted_execution_decision( + store, task, stage="worker", evaluated_at=daytime + ) + state_persisted = store.task_state(task) + history_before = list(state_persisted.get("route_transition_history", [])) + self.assertEqual(len(history_before), 1) + + with mock.patch.object(dispatch, "datetime") as datetime_mock: + datetime_mock.now.return_value = daytime + result2 = await dispatch.dispatch_with_store(args, workspace, store) + + self.assertEqual(result2, 0) + state_after_dry_run = store.task_state(task) + history_after = list(state_after_dry_run.get("route_transition_history", [])) + self.assertEqual(history_before, history_after) + finally: + store.close() + + async def test_dry_run_multiple_ready_tasks_isolation_and_statelessness(self): + daytime = datetime(2026, 7, 26, 14, 0, 0, tzinfo=timezone(timedelta(hours=9))) + nighttime = datetime(2026, 7, 26, 23, 0, 0, tzinfo=timezone(timedelta(hours=9))) + with tempfile.TemporaryDirectory() as temporary: + workspace = Path(temporary) + (workspace / ".git").mkdir() + task1 = self.make_task(workspace, lane="local", grade=8, unit="01_unit1") + task2 = self.make_task(workspace, lane="local", grade=8, unit="02_unit2") + store = dispatch.StateStore(workspace) + try: + # Task 1 is pinned during daytime + dec1, spec1 = dispatch.persisted_execution_decision( + store, task1, stage="worker", evaluated_at=daytime + ) + self.assertEqual(spec1.cli, "agy") + + state1_before = dict(store.task_state(task1)) + state2_before = dict(store.task_state(task2)) + + banners = [] + def capture_banner(event, name, lines): + banners.append((event, name, lines)) + + args = dispatch.argparse.Namespace( + workspace=str(workspace), + task_group=None, + retry_blocked=False, + dry_run=True, + ) + with mock.patch.object(dispatch, "datetime") as datetime_mock, \ + mock.patch.object(dispatch, "banner", side_effect=capture_banner): + datetime_mock.now.return_value = nighttime + result = await dispatch.dispatch_with_store(args, workspace, store) + + self.assertEqual(result, 0) + + task1_banners = [b for b in banners if b[1] == task1.name] + task2_banners = [b for b in banners if b[1] == task2.name] + self.assertTrue(any("model=agy/" in line for b in task1_banners for line in b[2])) + self.assertTrue(any("model=pi/" in line for b in task2_banners for line in b[2])) + + state1_after = store.task_state(task1) + state2_after = store.task_state(task2) + + self.assertEqual( + state1_before.get("route_transition_history"), + state1_after.get("route_transition_history"), + ) + self.assertEqual( + state2_before.get("route_transition_history"), + state2_after.get("route_transition_history"), + ) + self.assertEqual(state2_after.get("execution_decisions"), {}) + finally: + store.close() + + async def test_resume_pins_target_across_time_and_body_changes_and_resets_on_new_generation(self): + daytime = datetime(2026, 7, 26, 14, 0, 0, tzinfo=timezone(timedelta(hours=9))) + nighttime = datetime(2026, 7, 26, 23, 0, 0, tzinfo=timezone(timedelta(hours=9))) + with tempfile.TemporaryDirectory() as temporary: + workspace = Path(temporary) + (workspace / ".git").mkdir() + task = self.make_task(workspace, lane="local", grade=8) + store = dispatch.StateStore(workspace) + try: + # 1. Initial decision daytime (KST 14:00) -> agy Gemini Medium + dec1, spec1 = dispatch.persisted_execution_decision( + store, task, stage="worker", evaluated_at=daytime + ) + self.assertEqual(spec1.cli, "agy") + self.assertEqual(spec1.model, "Gemini 3.6 Flash (Medium)") + + # 2. Resuming at nighttime (KST 23:00) keeps pinned Gemini Medium + dec2, spec2 = dispatch.persisted_execution_decision( + store, task, stage="worker", evaluated_at=nighttime + ) + self.assertEqual(spec2.cli, "agy") + self.assertEqual(spec2.model, "Gemini 3.6 Flash (Medium)") + + # 3. Body edit (header intact) keeps pinned Gemini Medium + plan_file = task.plan + header = f"\n" + plan_file.write_text(header + "\n# Modified Body Content\n", encoding="utf-8") + dec3, spec3 = dispatch.persisted_execution_decision( + store, task, stage="worker", evaluated_at=nighttime + ) + self.assertEqual(spec3.cli, "agy") + + # 4. New generation header (plan=1) re-evaluates initial decision at nighttime -> pi Laguna + plan_file.write_text("\n\n# New Plan\n", encoding="utf-8") + task_new = dispatch.scan_tasks(workspace, None)[0] + dec4, spec4 = dispatch.persisted_execution_decision( + store, task_new, stage="worker", evaluated_at=nighttime + ) + self.assertEqual(spec4.cli, "pi") + self.assertEqual(spec4.model, "laguna-s:2.1") + finally: + store.close() + + async def test_qualified_failover_and_blocker_scenarios(self): + daytime = datetime(2026, 7, 26, 14, 0, 0, tzinfo=timezone(timedelta(hours=9))) + with tempfile.TemporaryDirectory() as temporary: + workspace = Path(temporary) + (workspace / ".git").mkdir() + task = self.make_task(workspace, lane="local", grade=8) + store = dispatch.StateStore(workspace) + try: + # 1. Initial decision local G08 -> agy Gemini Medium (primary) & pi Laguna (fallback) + dec1, spec1 = dispatch.persisted_execution_decision( + store, task, stage="worker", evaluated_at=daytime + ) + self.assertEqual(spec1.cli, "agy") + + # 2. Qualified failover (provider-quota) -> transitions to pi Laguna + dec2 = dispatch.select_execution_decision( + task, stage="worker", prior_decision=dec1, + evaluated_at=daytime, transition="failover", failure_class="provider-quota" + ) + self.assertEqual(dec2["transition"]["trigger"], "provider-quota") + self.assertEqual(dec2["selected"]["adapter"], "pi") + + # 3. Subsequent failover when no candidate remains -> raises no_failover_candidate + with self.assertRaises(dispatch.ExecutionDecisionError) as ctx: + dispatch.select_execution_decision( + task, stage="worker", prior_decision=dec2, + evaluated_at=daytime, transition="failover", failure_class="provider-quota" + ) + self.assertIn("no_failover_candidate", str(ctx.exception)) + finally: + store.close() + + async def test_context_budget_and_retry_blocked_lifecycle(self): + daytime = datetime(2026, 7, 26, 14, 0, 0, tzinfo=timezone(timedelta(hours=9))) + with tempfile.TemporaryDirectory() as temporary: + workspace = Path(temporary) + (workspace / ".git").mkdir() + task = self.make_task(workspace, lane="local", grade=8) + store = dispatch.StateStore(workspace) + try: + # 1. Primary initial execution (agy/Gemini Medium) + dec1, spec1 = dispatch.persisted_execution_decision( + store, task, stage="worker", evaluated_at=daytime + ) + self.assertEqual(spec1.cli, "agy") + + # 2. Record primary failure (count=1) -> failover to alternate (pi/laguna) + budget = dispatch.StageFailureBudget.from_decision(store, task, dec1) + count1 = budget.record_failure(target=dec1["selected"], transition="provider-quota") + self.assertEqual(count1, 1) + + dec2 = dispatch.select_execution_decision( + task, stage="worker", prior_decision=dec1, + evaluated_at=daytime, transition="failover", failure_class="provider-quota" + ) + dispatch.commit_execution_decision(store, task, "worker", dec2) + self.assertEqual(dec2["selected"]["adapter"], "pi") + + # 3. Alternate fails 9 times -> budget count reaches 10, task is blocked + budget2 = dispatch.StageFailureBudget.from_decision(store, task, dec2) + for _ in range(9): + c = budget2.record_failure(target=dec2["selected"], transition="generic-failure") + self.assertEqual(c, 10) + + store.update_task(task, blocked="worker recovery failure limit exhausted: 10/10 locator=/tmp/loc.json") + self.assertIsNotNone(store.task_state(task).get("blocked")) + + # 4. Retry blocked clears blocked & budget, preserves decision & transition history + args = dispatch.argparse.Namespace( + workspace=str(workspace), + task_group=None, + retry_blocked=True, + dry_run=False, + ) + with mock.patch.object(dispatch, "datetime") as datetime_mock: + datetime_mock.now.return_value = daytime + await dispatch.dispatch_with_store(args, workspace, store) + + state_after_retry = store.task_state(task) + self.assertIsNone(state_after_retry.get("blocked")) + self.assertEqual(state_after_retry.get("stage_failure_budgets"), {}) + self.assertIn("worker", state_after_retry.get("execution_decisions", {})) + self.assertTrue(len(state_after_retry.get("route_transition_history", [])) >= 2) + + # 5. Success resets stage failure budget + budget3 = dispatch.StageFailureBudget.from_decision(store, task, dec2) + budget3.reset_on_success() + self.assertEqual(store.task_state(task).get("stage_failure_budgets"), {}) + finally: + store.close() + + async def test_review_recovery_and_runtime_audit_evidence(self): + daytime = datetime(2026, 7, 26, 14, 0, 0, tzinfo=timezone(timedelta(hours=9))) + with tempfile.TemporaryDirectory() as temporary: + workspace = Path(temporary) + (workspace / ".git").mkdir() + task = self.make_task(workspace, lane="local", grade=8) + store = dispatch.StateStore(workspace) + try: + # 1. Review stage decision is pinned to Codex + dec_rev, spec_rev = dispatch.persisted_execution_decision( + store, task, stage="review", evaluated_at=daytime + ) + self.assertEqual(spec_rev.cli, "codex") + self.assertEqual(spec_rev.model, "gpt-5.6-sol") + self.assertEqual(dec_rev["rule_id"], "official-review-codex") + + # 2. Review failure budget is independent from worker stage budget + budget_worker = dispatch.StageFailureBudget(store, task, dec_rev["work_unit_id"], "worker") + budget_worker.record_failure(target={"adapter": "agy", "target": "Gemini 3.6 Flash (Medium)"}, transition="initial") + budget_review = dispatch.StageFailureBudget.from_decision(store, task, dec_rev) + self.assertEqual(budget_review.count(), 0) + + # 3. Legacy review recovery synthesizes fixed official review decision + task.recovery = True + synth_dec = dispatch.read_or_preview_stage_decision(task, {}, stage="review") + self.assertEqual(synth_dec["selected"]["adapter"], "codex") + self.assertEqual(synth_dec["selected"]["target"], "gpt-5.6-sol") + self.assertEqual(synth_dec["rule_id"], "official-review-codex") + + # 4. State, locator, status lines, and dry-run banner have consistent audit fields + evidence = dispatch.selector_evidence_lines(dec_rev) + self.assertTrue(any(line.startswith("work_unit_id=") for line in evidence)) + self.assertTrue(any(line.startswith("rule_id=official-review-codex") for line in evidence)) + self.assertTrue(any(line.startswith("priority=") for line in evidence)) + self.assertTrue(any(line.startswith("transition=") for line in evidence)) + self.assertTrue(any(line.startswith("quota_status=") for line in evidence)) + + status = dispatch.status_lines(task, "review", "ready", decision=dec_rev) + self.assertTrue(any(line.startswith("rule_id=official-review-codex") for line in status)) + finally: + store.close() + + if __name__ == "__main__": diff --git a/agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_execution_target_policy.py b/agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_execution_target_policy.py index d6b01b0..417f617 100644 --- a/agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_execution_target_policy.py +++ b/agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_execution_target_policy.py @@ -160,6 +160,58 @@ class ExecutionTargetPolicyTests(unittest.TestCase): evaluated_at=datetime(2026, 7, 25, 12, 0, 0), ) + def test_cloud_promotion_matrix(self): + cases = [ + (policy.AGY_GEMINI_LOW, policy.CLAUDE_OPUS), + (policy.AGY_GEMINI_MEDIUM, policy.CLAUDE_OPUS), + (policy.AGY_GEMINI_HIGH, policy.CLAUDE_OPUS), + (policy.CLAUDE_OPUS, policy.CODEX_TERRA_HIGH), + (policy.CODEX_SOL_XHIGH, None), + (policy.CODEX_TERRA_HIGH, None), + (policy.PI_ORNITH, None), + (policy.PI_LAGUNA, None), + ] + for current, expected in cases: + with self.subTest(current=current): + self.assertEqual(policy.promotion_target(current), expected) + + for target in policy.CANONICAL_TARGETS: + with self.subTest(identity=target.target): + self.assertEqual( + policy.canonical_target(target.adapter, target.target), + target, + ) + self.assertIsNone(policy.canonical_target("codex", "unknown")) + + def test_quota_probe_spec_matrix(self): + cases = [ + (policy.PI_ORNITH, None), + (policy.PI_LAGUNA, None), + ( + policy.AGY_GEMINI_LOW, + policy.QuotaProbeSpec("agy", "Gemini 3.6 Flash (Low)", ("overall", "model:Gemini 3.6 Flash (Low)")), + ), + ( + policy.AGY_GEMINI_MEDIUM, + policy.QuotaProbeSpec("agy", "Gemini 3.6 Flash (Medium)", ("overall", "model:Gemini 3.6 Flash (Medium)")), + ), + ( + policy.AGY_GEMINI_HIGH, + policy.QuotaProbeSpec("agy", "Gemini 3.6 Flash (High)", ("overall", "model:Gemini 3.6 Flash (High)")), + ), + ( + policy.CLAUDE_OPUS, + policy.QuotaProbeSpec("claude", "claude-opus-4-8", ("overall",)), + ), + ( + policy.CODEX_SOL_XHIGH, + policy.QuotaProbeSpec("codex", "gpt-5.6-sol", ("overall",)), + ), + ] + for target, expected in cases: + with self.subTest(target=target.target): + self.assertEqual(policy.quota_probe_spec(target), expected) + if __name__ == "__main__": unittest.main() diff --git a/agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_select_execution_target.py b/agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_select_execution_target.py index 05ea19a..9562b2f 100644 --- a/agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_select_execution_target.py +++ b/agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_select_execution_target.py @@ -29,6 +29,38 @@ def kst(hour: int, minute: int = 0, second: int = 0) -> datetime: return datetime(2026, 7, 25, hour, minute, second, tzinfo=KST) +def go_quota_snapshot( + adapter: str, + target: str, + status: str, + *, + snapshot_id: str = "quota-snap-1", + checked_at: str = "2026-07-25T05:00:00Z", +) -> dict: + remaining = { + "available": 25.0, + "exhausted": 0.0, + "unknown": None, + }[status] + return { + "schema_version": "1.0", + "snapshot_id": snapshot_id, + "source": "iop-node quota-probe", + "checked_at": checked_at, + "targets": [ + {"adapter": adapter, "target": target, "status": status} + ], + "required_caps": [ + { + "name": "overall", + "status": status, + "remaining_percent": remaining, + } + ], + "reason_codes": ["cap_evidence_unknown"] if status == "unknown" else [], + } + + def write_task_file( directory: Path, kind: str, @@ -114,7 +146,7 @@ class SelectorContractTests(unittest.TestCase): self.assertEqual(result["decision"]["timezone"], "Asia/Seoul") self.assertEqual( set(result["quota"]), - {"snapshot_id", "mode", "status", "source", "checked_at"}, + {"snapshot_id", "mode", "status", "source", "checked_at", "targets"}, ) self.assertEqual(result["transition"]["trigger"], "initial") self.assertEqual(result["transition"]["context_transfer"], "none") @@ -476,6 +508,54 @@ class SelectorQuotaRepresentationTests(unittest.TestCase): "eligible", ) + def test_actual_go_snapshot_shape_preserves_tri_state_and_metadata(self): + with TemporaryDirectory() as tmp: + cloud = write_task_file(Path(tmp), "PLAN", "cloud", 7) + for status in ("available", "exhausted", "unknown"): + with self.subTest(status=status): + snapshot = go_quota_snapshot( + "claude", + "claude-opus-4-8", + status, + snapshot_id=f"quota-{status}", + ) + completed = mock.Mock( + returncode=0, stdout=json.dumps(snapshot) + ) + with mock.patch( + "subprocess.run", return_value=completed + ): + if status == "exhausted": + with self.assertRaises( + selector.SelectorInputError + ) as ctx: + selector.select_execution_target( + cloud, evaluated_at=kst(12) + ) + self.assertEqual( + ctx.exception.code, "no_eligible_target" + ) + continue + result = selector.select_execution_target( + cloud, evaluated_at=kst(12) + ) + self.assertEqual( + result["candidates"][0]["quota_status"], status + ) + self.assertEqual(result["quota"]["status"], status) + self.assertEqual( + result["quota"]["snapshot_id"], snapshot["snapshot_id"] + ) + self.assertEqual( + result["quota"]["source"], snapshot["source"] + ) + self.assertEqual( + result["quota"]["checked_at"], snapshot["checked_at"] + ) + self.assertEqual( + result["quota"]["targets"], snapshot["targets"] + ) + def test_exhausted_gemini_falls_back_to_laguna(self): snapshot = { "snapshot_id": "gemini-exhausted", @@ -1213,6 +1293,188 @@ class SelectorFailoverContractTests(unittest.TestCase): self.assertIs(night_resume["decision"]["pinned"], True) self.assertEqual(night_resume["used_candidates"], night_failover["used_candidates"]) + def test_runtime_probed_cloud_alternate_round_trips_selected_snapshot(self): + snapshot = go_quota_snapshot( + "agy", + "Gemini 3.6 Flash (Medium)", + "available", + snapshot_id="night-gemini-available", + ) + completed = mock.Mock(returncode=0, stdout=json.dumps(snapshot)) + with TemporaryDirectory() as tmp, mock.patch( + "subprocess.run", return_value=completed + ) as run_mock: + task_file = write_task_file(Path(tmp), "PLAN", "local", 8) + prior = selector.select_execution_target( + task_file, evaluated_at=kst(1) + ) + self.assertEqual(prior["selected"]["adapter"], "pi") + result = selector.select_execution_target( + task_file, + evaluated_at=kst(1), + transition="failover", + prior_decision=prior, + failure_class="provider-stream-disconnect", + ) + + self.assertEqual(result["selected"]["adapter"], "agy") + selected_candidate = next( + candidate + for candidate in result["candidates"] + if candidate["adapter"] == "agy" + ) + self.assertEqual(selected_candidate["quota_status"], "available") + self.assertEqual(result["quota"]["status"], "available") + self.assertEqual( + result["quota"]["snapshot_id"], snapshot["snapshot_id"] + ) + self.assertEqual(result["quota"]["targets"], snapshot["targets"]) + self.assertEqual(run_mock.call_count, 2) + + def test_policy_owned_cloud_promotion_chain_and_no_bounce(self): + with TemporaryDirectory() as tmp: + task_file = write_task_file(Path(tmp), "PLAN", "cloud", 5) + initial = selector.select_execution_target( + task_file, + evaluated_at=kst(12), + quota_probe_command="missing-probe", + ) + claude = selector.select_execution_target( + task_file, + evaluated_at=kst(12), + transition="promotion", + prior_decision=initial, + failure_class="provider-quota", + ) + resumed = selector.select_execution_target( + task_file, + evaluated_at=kst(23), + transition="resume", + prior_decision=claude, + ) + terra = selector.select_execution_target( + task_file, + evaluated_at=kst(23), + transition="promotion", + prior_decision=resumed, + failure_class="context-limit", + ) + + self.assertEqual( + (claude["selected"]["adapter"], claude["selected"]["target"]), + ("claude", "claude-opus-4-8"), + ) + self.assertEqual(claude["transition"]["kind"], "promotion") + self.assertEqual(claude["transition"]["trigger"], "provider-quota") + self.assertEqual( + (terra["selected"]["adapter"], terra["selected"]["target"]), + ("codex", "gpt-5.6-terra"), + ) + self.assertEqual( + terra["promotion_path"], + [ + { + "adapter": "agy", + "target": "Gemini 3.6 Flash (High)", + }, + {"adapter": "claude", "target": "claude-opus-4-8"}, + {"adapter": "codex", "target": "gpt-5.6-terra"}, + ], + ) + with self.assertRaises(selector.SelectorInputError) as exhausted: + selector.select_execution_target( + task_file, + evaluated_at=kst(23), + transition="promotion", + prior_decision=terra, + failure_class="provider-quota", + ) + self.assertEqual(exhausted.exception.code, "no_promotion_target") + with self.assertRaises(selector.SelectorInputError) as generic: + selector.select_execution_target( + task_file, + evaluated_at=kst(12), + transition="promotion", + prior_decision=initial, + failure_class="generic-error", + ) + self.assertEqual( + generic.exception.code, "unqualified_promotion_trigger" + ) + + def test_probe_candidate_quota_argv_and_normalization(self): + eval_time = kst(14, 0, 0) + snapshot = go_quota_snapshot( + "agy", + "Gemini 3.6 Flash (Medium)", + "available", + snapshot_id="snap-99", + ) + snapshot["required_caps"].append( + { + "name": "model:Gemini 3.6 Flash (Medium)", + "status": "available", + "remaining_percent": 40.0, + } + ) + with mock.patch("subprocess.run") as run_mock: + run_mock.return_value = mock.Mock( + returncode=0, + stdout=json.dumps(snapshot), + ) + result = selector.probe_candidate_quota( + target="Gemini 3.6 Flash (Medium)", + adapter="agy", + required_caps=("overall", "model:Gemini 3.6 Flash (Medium)"), + checked_at=eval_time, + quota_probe_command="iop-node quota-probe", + ) + self.assertEqual(result, snapshot) + run_mock.assert_called_once() + cmd = run_mock.call_args[0][0] + self.assertEqual( + cmd, + [ + "iop-node", + "quota-probe", + "--target", + "Gemini 3.6 Flash (Medium)", + "--command", + "agy", + "--required-cap", + "overall", + "--required-cap", + "model:Gemini 3.6 Flash (Medium)", + "--checked-at", + eval_time.isoformat(), + ], + ) + + def test_probe_candidate_quota_error_normalizes_to_unknown(self): + eval_time = kst(14, 0, 0) + error_cases = [ + mock.Mock(returncode=1, stdout=""), + mock.Mock(returncode=0, stdout="invalid json"), + mock.Mock(returncode=0, stdout=json.dumps({"status": "invalid_status"})), + OSError("binary not found"), + ] + for side_effect in error_cases: + with self.subTest(side_effect=side_effect): + with mock.patch("subprocess.run") as run_mock: + if isinstance(side_effect, Exception): + run_mock.side_effect = side_effect + else: + run_mock.return_value = side_effect + result = selector.probe_candidate_quota( + target="claude-opus-4-8", + adapter="claude", + required_caps=("overall",), + checked_at=eval_time, + ) + self.assertEqual(result["targets"][0]["status"], "unknown") + self.assertEqual(result["reason_codes"], ["probe_error"]) + self.assertIsNone(result["snapshot_id"]) + if __name__ == "__main__": unittest.main() diff --git a/agent-task/archive/2026/07/m-agent-task-runtime-target-selector/05+04_dispatch_route_source/code_review_cloud_G07_2.log b/agent-task/archive/2026/07/m-agent-task-runtime-target-selector/05+04_dispatch_route_source/code_review_cloud_G07_2.log new file mode 100644 index 0000000..cd07a80 --- /dev/null +++ b/agent-task/archive/2026/07/m-agent-task-runtime-target-selector/05+04_dispatch_route_source/code_review_cloud_G07_2.log @@ -0,0 +1,175 @@ + + +# Code Review Reference - REVIEW_REFACTOR + +> **[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/05+04_dispatch_route_source, plan=2, tag=REVIEW_REFACTOR + +## Archive Evidence Snapshot + +- Prior task: `agent-task/m-agent-task-runtime-target-selector/05+04_dispatch_route_source` +- Prior plan: `agent-task/m-agent-task-runtime-target-selector/05+04_dispatch_route_source/plan_local_G08_1.log` +- Prior review: `agent-task/m-agent-task-runtime-target-selector/05+04_dispatch_route_source/code_review_cloud_G09_1.log` +- Verdict: FAIL +- Findings: Required 2, Suggested 0, Nit 0. +- Affected files: `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py`, `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py`. +- Verification evidence: focused 2 tests, full Python 198 tests, `py_compile`, `git diff --check`는 PASS했지만 복수 ready task 재현에서 주간 AGY로 고정된 첫 task가 야간 dry-run에 Laguna로 출력됐다. +- Roadmap carryover: SDD S05/S09의 task generation route pin과 dispatcher invocation audit를 보완하되 이 child PASS만으로 Milestone Task 완료를 체크하지 않는다. + +## 이 파일을 읽는 리뷰 에이전트에게 + +> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다. + +각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요. +리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다. + +1. 판정을 append한다. +2. `CODE_REVIEW-cloud-G07.md` → `code_review_cloud_G07_2.log`, `PLAN-local-G07.md` → `plan_local_G07_2.log`로 아카이브한다. +3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-agent-task-runtime-target-selector/05+04_dispatch_route_source/`로 이동한다. WARN/FAIL이면 code-review skill이 요구하는 다음 filesystem state를 완전히 작성한다. +4. PASS이고 task group이 `m-agent-task-runtime-target-selector`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다. +5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다. + +--- + +## 구현 항목별 완료 여부 + +| 항목 | 완료 여부 | +|------|---------| +| REVIEW_REFACTOR-1 Task-local dry-run과 live invocation evidence 종결 | [x] | + +## 구현 체크리스트 + +- [x] 복수 ready task의 dry-run이 각 task의 persisted decision/quota snapshot을 읽고 state/history를 변경하지 않도록 수정하고 경계 회귀를 통과시킨다. +- [x] worker/review initial 경계를 `run_worker()`/`run_review()` 호출로 검증하고 `route_agent()` 정의와 모든 stale reference를 제거한다. +- [x] focused integration, 전체 Python suite, `py_compile`, deterministic symbol scan과 `git diff --check`를 통과시킨다. +- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. + +## 코드리뷰 전용 체크리스트 + +> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다. +> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다. + +- [x] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다. +- [x] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다. +- [x] active `CODE_REVIEW-*-G??.md`를 `code_review_cloud_G07_2.log`로 아카이브한다. +- [x] active `PLAN-*-G??.md`를 `plan_local_G07_2.log`로 아카이브한다. +- [x] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md`와 `agent-task/**/*.log`를 unignore하고 `agent-roadmap/current.md`를 ignore하는지 확인한다. +- [x] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다. +- [x] PASS이면 active task 디렉터리 `agent-task/m-agent-task-runtime-target-selector/05+04_dispatch_route_source/`를 `agent-task/archive/YYYY/MM/m-agent-task-runtime-target-selector/05+04_dispatch_route_source/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다. +- [x] PASS이고 task group이 `m-agent-task-runtime-target-selector`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다. +- [x] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-agent-task-runtime-target-selector/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다. +- [ ] WARN/FAIL이면 code-review skill의 판정에 맞는 다음 filesystem state를 작성하고 `complete.log`를 작성하지 않는다. + +## 계획 대비 변경 사항 + +특이사항 없음. PLAN-local-G07.md에 명시된 계획대로 구현 및 검증을 완료함. + +## 주요 설계 결정 + +1. `dispatch.py`의 dry-run 가시화 루프에서 loop variable leak으로 인해 직전 타 작업의 `state`를 읽던 문제를 `store.peek_task_state(task)` 호출로 변경하여 해결함. 이를 통해 dry-run 시 무상태(stateless) 및 task간 state 격리를 보장함. +2. 불필요하고 stale symbol이 된 `route_agent()` 함수 정의를 `dispatch.py`에서 제거함. +3. `test_dispatch.py`의 `SelectorDispatcherIntegrationTest`에서 helper 함수 직접 호출 대신 `run_worker()` 및 `run_review()`를 mock runner 기반으로 직접 호출하여 실행 경계의 AgentSpec 전달을 검증하고, 주간 Pinned AGY task와 야간 Initial Laguna task를 동시에 dry-run하는 복수 ready task 격리 및 state/history 불변 회귀 테스트(`test_dry_run_multiple_ready_tasks_isolation_and_statelessness`)를 추가함. + +## 리뷰어를 위한 체크포인트 + +- 복수 ready task가 각자의 persisted decision과 quota snapshot만 사용하고 dry-run이 어떤 state/history도 쓰지 않는가. +- `run_worker()`와 `run_review()` 실제 경계가 stage별 selector decision의 `AgentSpec`을 `run_escalating`에 전달하는가. +- `route_agent()` 정의와 stale production/test reference가 모두 제거됐는가. +- 단일 task initial/resume dry-run과 전체 dispatcher 회귀가 보존되는가. + +## 검증 결과 + +### SelectorDispatcherIntegrationTest + +명령: `python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py SelectorDispatcherIntegrationTest -v` + +exit code: 0 + +``` +test_dry_run_multiple_ready_tasks_isolation_and_statelessness (__main__.SelectorDispatcherIntegrationTest.test_dry_run_multiple_ready_tasks_isolation_and_statelessness) ... ok +test_dry_run_statelessness_initial_and_resume_previews (__main__.SelectorDispatcherIntegrationTest.test_dry_run_statelessness_initial_and_resume_previews) ... ok +test_worker_and_review_initial_invocation_uses_selector (__main__.SelectorDispatcherIntegrationTest.test_worker_and_review_initial_invocation_uses_selector) ... ok + +---------------------------------------------------------------------- +Ran 3 tests in 5.067s + +OK +``` + +### 전체 Python suite + +명령: `python3 -m unittest discover -s agent-ops/skills/project/orchestrate-agent-task-loop/tests -p 'test_*.py'` + +exit code: 0 + +``` +---------------------------------------------------------------------- +Ran 199 tests in 16.704s + +OK +``` + +### py_compile + +명령: `python3 -m py_compile agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py 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/tests/test_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` + +exit code: 0 (stdout/stderr 출력 없음) + +### 제거된 symbol scan + +명령: `! rg --sort path -n "route_agent" agent-ops/skills/project/orchestrate-agent-task-loop` + +exit code: 1 (매칭 결과 없음) + +### diff check + +명령: `git diff --check` + +exit code: 0 (출력 없음) + +--- + +> **[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 `[ ]` → `[x]` only | +| 구현 체크리스트 (item text/order) | Fixed at stub creation from plan | Implementing agent checks `[ ]` → `[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 | + +## 코드리뷰 결과 + +- 종합 판정: PASS +- 차원별 평가: + - correctness: Pass + - completeness: Pass + - test coverage: Pass + - API contract: Pass + - code quality: Pass + - implementation deviation: Pass + - verification trust: Pass + - spec conformance: Pass +- 발견된 문제: 없음 diff --git a/agent-task/m-agent-task-runtime-target-selector/05+04_dispatch_integration/CODE_REVIEW-cloud-G09.md b/agent-task/archive/2026/07/m-agent-task-runtime-target-selector/05+04_dispatch_route_source/code_review_cloud_G09_0.log similarity index 100% rename from agent-task/m-agent-task-runtime-target-selector/05+04_dispatch_integration/CODE_REVIEW-cloud-G09.md rename to agent-task/archive/2026/07/m-agent-task-runtime-target-selector/05+04_dispatch_route_source/code_review_cloud_G09_0.log diff --git a/agent-task/archive/2026/07/m-agent-task-runtime-target-selector/05+04_dispatch_route_source/code_review_cloud_G09_1.log b/agent-task/archive/2026/07/m-agent-task-runtime-target-selector/05+04_dispatch_route_source/code_review_cloud_G09_1.log new file mode 100644 index 0000000..7dc69dc --- /dev/null +++ b/agent-task/archive/2026/07/m-agent-task-runtime-target-selector/05+04_dispatch_route_source/code_review_cloud_G09_1.log @@ -0,0 +1,157 @@ + + +# Code Review Reference - REFACTOR + +> **[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-25 +task=m-agent-task-runtime-target-selector/05+04_dispatch_route_source, plan=1, tag=REFACTOR + +## 이 파일을 읽는 리뷰 에이전트에게 + +> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다. + +각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요. +리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다. + +1. 판정을 append한다. +2. `CODE_REVIEW-cloud-G09.md` → `code_review_cloud_G09_1.log`, `PLAN-local-G08.md` → `plan_local_G08_1.log`로 아카이브한다. +3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-agent-task-runtime-target-selector/05+04_dispatch_route_source/`로 이동한다. WARN/FAIL이면 code-review skill이 요구하는 다음 filesystem state를 완전히 작성한다. +4. PASS이고 task group이 `m-agent-task-runtime-target-selector`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다. +5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다. + +--- + +## 구현 항목별 완료 여부 + +| 항목 | 완료 여부 | +|------|---------| +| REFACTOR-1 Live route source 교체 | [x] | + +## 구현 체크리스트 + +- [x] worker/review initial invocation과 dry-run이 persisted selector decision을 단일 route source로 사용한다. +- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. + +## 코드리뷰 전용 체크리스트 + +> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다. +> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다. + +- [x] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다. +- [x] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다. +- [x] active `CODE_REVIEW-cloud-G09.md`를 `code_review_cloud_G09_1.log`로 아카이브한다. +- [x] active `PLAN-local-G08.md`를 `plan_local_G08_1.log`로 아카이브한다. +- [x] `.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/05+04_dispatch_route_source/`를 `agent-task/archive/YYYY/MM/m-agent-task-runtime-target-selector/05+04_dispatch_route_source/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다. +- [ ] PASS이고 task group이 `m-agent-task-runtime-target-selector`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다. +- [ ] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-agent-task-runtime-target-selector/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다. +- [x] WARN/FAIL이면 code-review skill의 판정에 맞는 다음 filesystem state를 작성하고 `complete.log`를 작성하지 않는다. + +## 계획 대비 변경 사항 + +계획과 동일하게 구현함. 계획 외 편차 없음. + +## 주요 설계 결정 + +- 정적 `route_agent()` 도우미 함수 및 dry-run 시의 하드코딩된 review spec을 제거하고, worker/review/dry-run 모든 호출 경로가 selector decision(`select_execution_decision` / `persisted_execution_decision`)과 `agent_spec_from_decision`을 통해 `AgentSpec`을 결정하도록 단일화함. +- dry-run 수행 시 저장된 decision이 존재하면 `prior_decision`으로 전달해 resume preview를 생성하고, 없으면 `prior_decision=None`으로 initial preview를 생성하되, `commit_execution_decision`을 호출하지 않음으로써 state, transition history, admission 기록을 mutating하지 않고 무상태(stateless) 동작을 보장함. +- `SelectorDispatcherIntegrationTest`를 새로 추가하여 initial worker/review decision 생성 및 dry-run 무상태 검증을 자동화함. + +## 리뷰어를 위한 체크포인트 + +- worker/review/dry-run이 파일명 lane/G가 아니라 persisted selector decision만 실행 spec으로 변환하는가. +- dry-run이 persisted resume preview 또는 non-persisted initial preview만 만들고 state/history/admission을 쓰지 않는가. +- 공식 review decision과 worker decision이 stage별로 분리되는가. +- `route_agent()` production call-site와 테스트 참조가 제거되는가. + +## 검증 결과 + +### SelectorDispatcherIntegrationTest + +명령: `python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py SelectorDispatcherIntegrationTest -v` + +```text +test_dry_run_statelessness_initial_and_resume_previews (__main__.SelectorDispatcherIntegrationTest.test_dry_run_statelessness_initial_and_resume_previews) ... ok +test_worker_and_review_initial_invocation_uses_selector (__main__.SelectorDispatcherIntegrationTest.test_worker_and_review_initial_invocation_uses_selector) ... ok + +---------------------------------------------------------------------- +Ran 2 tests in 0.030s + +OK +``` +exit code: 0 + +### 전체 Python suite + +명령: `python3 -m unittest discover -s agent-ops/skills/project/orchestrate-agent-task-loop/tests -p 'test_*.py'` + +```text +Ran 198 tests in 8.613s + +OK +``` +exit code: 0 + +### py_compile + +명령: `python3 -m py_compile agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py 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/tests/test_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` + +stdout/stderr: clean output +exit code: 0 + +### diff check + +명령: `git diff --check` + +stdout/stderr: clean output +exit code: 0 + +--- + +> **[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 `[ ]` → `[x]` only | +| 구현 체크리스트 (item text/order) | Fixed at stub creation from plan | Implementing agent checks `[ ]` → `[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 | + +## 코드리뷰 결과 + +- 종합 판정: FAIL +- 차원별 평가: + - correctness: Fail + - completeness: Fail + - test coverage: Fail + - API contract: Pass + - code quality: Pass + - implementation deviation: Fail + - verification trust: Fail + - spec conformance: Fail +- 발견된 문제: + - Required — `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py:4563`: dry-run 출력 루프가 현재 `task`의 state를 읽지 않고 앞선 분류 루프가 마지막으로 남긴 `state`를 모든 ready task에 재사용한다. 두 개의 `local-G08` task 중 첫 task를 주간 AGY decision으로 고정하고 둘째 task는 미선택 상태로 둔 뒤 야간 dry-run을 실행하면 첫 task도 `pi/iop/laguna-s:2.1`로 표시되어 SDD S05/S09의 work-unit route pin과 per-task invocation audit를 위반한다. dry-run 두 번째 루프에서 task별 `store.peek_task_state(task)` 또는 분류 시 저장한 task별 state snapshot을 사용하고, 서로 다른 persisted decision을 가진 복수 ready task가 각자의 model을 출력하면서 state/history를 변경하지 않는 회귀 테스트를 추가한다. + - Required — `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py:6060`, `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py:1048`: 계획이 요구한 worker/review “initial invocation” 검증과 정적 helper 제거가 완료되지 않았다. 새 테스트는 `run_worker()`/`run_review()`를 호출하지 않고 `persisted_execution_decision()`만 직접 검사하며, 제거 대상으로 명시된 `route_agent()` 정의도 남아 있다. `route_agent()`를 제거하고 stale reference가 없음을 확인한 뒤, `run_escalating`을 mock한 실제 `run_worker()`/`run_review()` 경계에서 전달된 `AgentSpec`과 stage별 persisted decision을 검증한다. +- 다음 단계: code-review FAIL findings를 입력으로 plan skill의 isolated reassessment와 fresh routing을 거친 후속 PLAN/CODE_REVIEW pair를 작성한다. diff --git a/agent-task/archive/2026/07/m-agent-task-runtime-target-selector/05+04_dispatch_route_source/complete.log b/agent-task/archive/2026/07/m-agent-task-runtime-target-selector/05+04_dispatch_route_source/complete.log new file mode 100644 index 0000000..32e0c87 --- /dev/null +++ b/agent-task/archive/2026/07/m-agent-task-runtime-target-selector/05+04_dispatch_route_source/complete.log @@ -0,0 +1,37 @@ +# Complete - m-agent-task-runtime-target-selector/05+04_dispatch_route_source + +## 완료 일시 + +2026-07-26 + +## 요약 + +Dispatcher의 task-local persisted route 소비 경계를 2회의 판정 루프로 보완했으며 최종 판정은 PASS다. + +## 루프 이력 + +| Plan | Review | Verdict | 메모 | +|------|--------|---------|------| +| `plan_local_G08_1.log` | `code_review_cloud_G09_1.log` | FAIL | dry-run task state 누출과 worker/review 실제 호출 경계·dead helper 제거 evidence가 미충족됐다. | +| `plan_local_G07_2.log` | `code_review_cloud_G07_2.log` | PASS | task-local dry-run 격리, 실제 invocation spec 전달, stale symbol 제거와 전체 회귀를 확인했다. | + +## 구현/정리 내용 + +- dry-run이 각 ready task의 `peek_task_state()`에서 persisted decision과 quota snapshot을 읽도록 수정해 task 간 route pin 누출을 제거했다. +- `route_agent()`를 제거하고 `run_worker()`·`run_review()`가 selector `AgentSpec`을 실행 경계에 전달하는 회귀 테스트를 추가했다. + +## 최종 검증 + +- `python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py SelectorDispatcherIntegrationTest -v` - PASS; 3 tests, 5.070s. +- `python3 -m unittest discover -s agent-ops/skills/project/orchestrate-agent-task-loop/tests -p 'test_*.py'` - PASS; 199 tests, 13.395s. +- `python3 -m py_compile agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py 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/tests/test_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` - PASS; 출력 없음. +- `! rg --sort path -n "route_agent" agent-ops/skills/project/orchestrate-agent-task-loop` - PASS; 매칭 없음. +- `git diff --check` - PASS; 출력 없음. + +## 잔여 Nit + +- 없음 + +## 후속 작업 + +- 없음 diff --git a/agent-task/archive/2026/07/m-agent-task-runtime-target-selector/05+04_dispatch_route_source/plan_local_G07_2.log b/agent-task/archive/2026/07/m-agent-task-runtime-target-selector/05+04_dispatch_route_source/plan_local_G07_2.log new file mode 100644 index 0000000..ee233f8 --- /dev/null +++ b/agent-task/archive/2026/07/m-agent-task-runtime-target-selector/05+04_dispatch_route_source/plan_local_G07_2.log @@ -0,0 +1,145 @@ + + +# Plan - Dispatcher task-local route source 리뷰 보완 + +## 이 파일을 읽는 구현 에이전트에게 + +구현과 검증을 끝낸 뒤 `CODE_REVIEW-cloud-G07.md`의 구현 에이전트 소유 섹션을 실제 내용과 원문 출력으로 채운다. active 파일은 그대로 두고 review 준비 완료만 보고한다. 종결은 code-review skill 전용이다. 차단되면 정확한 원인, 시도한 명령/출력, 재개 조건만 구현 에이전트 소유 evidence에 기록한다. 사용자에게 질문하거나 user-input 도구를 호출하지 않고, control-plane stop file 생성, 다음 상태 분류, archive/log 이동, `complete.log` 또는 roadmap 수정은 하지 않는다. + +## 배경 + +persisted selector decision을 live route source로 연결했지만, 복수 ready task의 dry-run이 마지막 task state를 공유해 work-unit route pin을 잘못 표시한다. 또한 구현 검증이 helper 직접 호출에 머물고 제거 대상 `route_agent()`가 남아 실제 worker/review invocation 계약의 완료 근거가 부족하다. 두 Required finding을 같은 dispatcher 경계와 회귀 테스트에서 닫는다. + +## Archive Evidence Snapshot + +- Prior task: `agent-task/m-agent-task-runtime-target-selector/05+04_dispatch_route_source` +- Prior plan: `agent-task/m-agent-task-runtime-target-selector/05+04_dispatch_route_source/plan_local_G08_1.log` +- Prior review: `agent-task/m-agent-task-runtime-target-selector/05+04_dispatch_route_source/code_review_cloud_G09_1.log` +- Verdict: FAIL +- Findings: Required 2, Suggested 0, Nit 0. +- Affected files: `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py`, `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py`. +- Verification evidence: focused 2 tests, full Python 198 tests, `py_compile`, `git diff --check`는 PASS했지만 복수 ready task 재현에서 주간 AGY로 고정된 첫 task가 야간 dry-run에 Laguna로 출력됐다. +- Roadmap carryover: SDD S05/S09의 task generation route pin과 dispatcher invocation audit를 보완하되 이 child PASS만으로 Milestone Task 완료를 체크하지 않는다. + +## 분석 결과 + +### 읽은 파일 + +- `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py` +- `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py` +- `agent-ops/skills/project/orchestrate-agent-task-loop/SKILL.md` +- `agent-roadmap/phase/automation-runtime-bridge/PHASE.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-test/local/rules.md` +- `agent-ops/rules/common/philosophy.md` +- `agent-ops/rules/common/rules-agent-spec.md` +- `agent-spec/index.md` +- `agent-contract/index.md` +- `agent-task/archive/2026/07/m-agent-task-runtime-target-selector/04+03_failover_budget/complete.log` +- `agent-task/m-agent-task-runtime-target-selector/05+04_dispatch_route_source/plan_local_G08_1.log` +- `agent-task/m-agent-task-runtime-target-selector/05+04_dispatch_route_source/code_review_cloud_G09_1.log` + +### SDD 기준 + +- SDD: `agent-roadmap/sdd/automation-runtime-bridge/agent-task-runtime-target-selector/SDD.md`, 상태 `[승인됨]`, 잠금 해제. +- 대상 Acceptance Scenario: S05 `route-pin`, S09 `dispatch-integration` 중 task-local persisted decision의 dry-run/worker/review 소비 경계. +- Evidence Map S05의 persisted route resume/restart와 S09의 worker/review invocation audit를 복수 task 격리, 실제 `run_worker()`/`run_review()` spec 전달, 무상태 dry-run 검증으로 역산한다. +- 이 follow-up은 분할 child의 Required finding만 닫으므로 `Roadmap Targets`를 두지 않고 후속 통합 task의 최종 evidence에 넘긴다. + +### 테스트 환경 규칙 + +- `test_env=local`; `agent-test/local/rules.md`를 읽었다. +- agent-ops Python dispatcher에 매칭되는 `agent-test/local/*.md` profile은 없다. 현재 checkout의 Python unittest manifest, `py_compile`, deterministic `rg`, `git diff --check`를 fallback으로 사용하며 test-rule 유지보수는 필요하지 않다. +- provider subprocess는 `run_escalating` mock과 임시 workspace/state로 대체하고 실제 provider 호출은 하지 않는다. +- fresh-process 전체 suite를 사용하며 cache output은 허용하지 않는다. + +### 테스트 커버리지 공백 + +- 단일 task dry-run만 있어 복수 ready task의 서로 다른 persisted state 격리가 검증되지 않는다. +- `test_worker_and_review_initial_invocation_uses_selector`가 `persisted_execution_decision()`만 직접 호출해 `run_worker()`/`run_review()`가 해당 spec을 실행 경계로 전달하는지 증명하지 않는다. +- 제거 대상으로 명시된 `route_agent()`가 남았지만 stale symbol inventory 검증이 없다. + +### 심볼 참조 + +- 제거 대상 `route_agent()` 정의: `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py:1048`. +- `rg --sort path -n "route_agent" agent-ops/skills/project/orchestrate-agent-task-loop` 결과 production/test call site는 없고 정의만 남아 있다. + +### 분할 판단 + +- split 정책을 다시 평가했다. 현재 task path는 `m-agent-task-runtime-target-selector/05+04_dispatch_route_source`이며 predecessor `04+03_failover_budget`는 `agent-task/archive/2026/07/m-agent-task-runtime-target-selector/04+03_failover_budget/complete.log`로 충족된다. +- 두 finding은 동일한 dispatcher route-source 경계와 동일 테스트 class를 수정하는 하나의 bug-fix unit이다. production 수정과 필수 회귀 테스트를 분리하면 독립 PASS가 불가능하고, 새 API·도메인·외부 환경 경계도 없어 추가 split은 인위적이다. + +### 범위 결정 근거 + +- `execution_target_policy.py`, `select_execution_target.py`의 selector 정책, quota/failover/budget, selfcheck/throughput sibling 범위는 변경하지 않는다. +- dispatcher live worker/review selector 연결 자체는 유지하고 task-local dry-run state 읽기, dead helper 제거, 해당 invocation evidence만 보완한다. +- 현재 agent-spec/agent-contract index에는 agent-task selector/dispatcher에 매칭되는 활성 spec/contract 문서가 없어 승인 SDD와 코드·테스트를 기준으로 삼는다. + +### 최종 라우팅 + +- evaluation_mode: `isolated-reassessment`; route-free current findings와 실제 재현만 입력으로 사용했다. +- finalizer=`finalize-task-policy.sh`, finalizer_mode=`pair`; build/review status=`routed`. +- build closures: `scope_closed=true`(두 Required와 두 파일로 고정), `context_closed=true`(dispatcher/state/test evidence가 한 task에 들어옴), `verification_closed=true`(복수 task 재현·실제 invocation·전체 suite), `evidence_trusted=true`(현재 checkout에서 실패 재현 및 198 tests PASS를 직접 확인), `ownership_closed=true`(task-local source/test만 수정), `decision_closed=true`(사용자 결정 없음). +- review closures: `scope_closed=true`, `context_closed=true`, `verification_closed=true`, `evidence_trusted=true`, `ownership_closed=true`, `decision_closed=true`. +- build route_basis=`local-fit`; capability_gap=`none`; grade floor=`none`. +- build loop-risk observations: `ordered_transitions.state_count=2`(initial/resume), `concurrent_consistency.actor_count=2`(복수 ready task의 state 격리), `boundary_contract.component_count=3`(StateStore/selector/dispatcher)와 consumer 3(worker/review/dry-run), `structured_interpretation.mechanisms=[]`, `variant_product.independent_axis_count=3`(task/stage/persisted 여부). matched signatures=`concurrent_consistency,boundary_contract,variant_product`; 이는 route에 영향을 주지 않는다. +- build scores(scope_coupling/state_concurrency/blast_irreversibility/evidence_diagnosis/verification_complexity)=`1/1/1/2/2`, final=`local-G07`, filename=`PLAN-local-G07.md`. +- review route_basis=`official-review`; capability_gap=`none`; grade floor=`none`; execution=`codex/gpt-5.6-sol xhigh`. +- review scores(scope_coupling/state_concurrency/blast_irreversibility/evidence_diagnosis/verification_complexity)=`1/1/1/2/2`, final=`cloud-G07`, filename=`CODE_REVIEW-cloud-G07.md`. + +## 구현 체크리스트 + +- [ ] 복수 ready task의 dry-run이 각 task의 persisted decision/quota snapshot을 읽고 state/history를 변경하지 않도록 수정하고 경계 회귀를 통과시킨다. +- [ ] worker/review initial 경계를 `run_worker()`/`run_review()` 호출로 검증하고 `route_agent()` 정의와 모든 stale reference를 제거한다. +- [ ] focused integration, 전체 Python suite, `py_compile`, deterministic symbol scan과 `git diff --check`를 통과시킨다. +- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. + +### [REVIEW_REFACTOR-1] Task-local dry-run과 live invocation evidence 종결 + +- 문제: `dispatch.py:4563`은 dry-run 출력 루프에서 직전 분류 루프의 마지막 `state`를 재사용한다. `test_dispatch.py:6060`은 helper만 직접 호출하며 `dispatch.py:1048`의 제거 대상 `route_agent()` 정의가 남아 계획 완료와 SDD S05/S09 evidence를 충족하지 못한다. +- 해결 방법: + +```python +# Before: agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py:4559 +for task in tasks: + if task.name in ready_by_name: + decisions = state.get("execution_decisions", {}) +``` + +```python +# After +for task in tasks: + if task.name in ready_by_name: + preview_state = store.peek_task_state(task) + decisions = preview_state.get("execution_decisions", {}) +``` + + quota snapshot도 같은 `preview_state`에서 읽는다. `route_agent()`는 삭제한다. 테스트는 주간 pinned AGY task와 야간 initial Laguna task를 동시에 dry-run해 서로 다른 model 출력과 두 state/history 불변을 확인하고, `run_escalating`을 mock한 `run_worker()`/`run_review()` 호출이 stage별 selector `AgentSpec`을 전달하고 decision을 저장하는지 확인한다. +- 수정 파일 및 체크리스트: + - [ ] `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py`: dry-run task-local state snapshot 사용과 dead `route_agent()` 제거. + - [ ] `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py`: 복수 ready task state 격리와 실제 worker/review invocation 회귀. +- 테스트 작성: `SelectorDispatcherIntegrationTest`에 복수 task/day→night dry-run regression과 mock runner 기반 worker/review invocation test를 작성한다. 단일 task 무상태 검증은 보존한다. +- 중간 검증: `python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py SelectorDispatcherIntegrationTest -v` — 복수 task route 격리, worker/review spec 전달, dry-run state/history 불변이 PASS해야 한다. + +## 의존 관계 및 구현 순서 + +1. predecessor `04+03_failover_budget`는 `agent-task/archive/2026/07/m-agent-task-runtime-target-selector/04+03_failover_budget/complete.log`로 충족됐다. +2. task-local dry-run state를 수정하고 dead helper를 제거한 뒤 실제 invocation/복수 task 회귀를 갱신한다. + +## 수정 파일 요약 + +| 파일 | 항목 | +|---|---| +| `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py` | REVIEW_REFACTOR-1 | +| `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py` | REVIEW_REFACTOR-1 | + +## 최종 검증 + +- `python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py SelectorDispatcherIntegrationTest -v` — task-local dry-run과 실제 worker/review invocation 회귀 PASS. +- `python3 -m unittest discover -s agent-ops/skills/project/orchestrate-agent-task-loop/tests -p 'test_*.py'` — fresh-process 전체 suite PASS; cache output은 허용하지 않는다. +- `python3 -m py_compile agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py 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/tests/test_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` — exit 0. +- `! rg --sort path -n "route_agent" agent-ops/skills/project/orchestrate-agent-task-loop` — 제거된 helper 참조가 없어야 한다. +- `git diff --check` — 출력 없음. + +모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. diff --git a/agent-task/m-agent-task-runtime-target-selector/05+04_dispatch_integration/PLAN-local-G08.md b/agent-task/archive/2026/07/m-agent-task-runtime-target-selector/05+04_dispatch_route_source/plan_local_G08_0.log similarity index 100% rename from agent-task/m-agent-task-runtime-target-selector/05+04_dispatch_integration/PLAN-local-G08.md rename to agent-task/archive/2026/07/m-agent-task-runtime-target-selector/05+04_dispatch_route_source/plan_local_G08_0.log diff --git a/agent-task/archive/2026/07/m-agent-task-runtime-target-selector/05+04_dispatch_route_source/plan_local_G08_1.log b/agent-task/archive/2026/07/m-agent-task-runtime-target-selector/05+04_dispatch_route_source/plan_local_G08_1.log new file mode 100644 index 0000000..e063f7d --- /dev/null +++ b/agent-task/archive/2026/07/m-agent-task-runtime-target-selector/05+04_dispatch_route_source/plan_local_G08_1.log @@ -0,0 +1,114 @@ + + +# Plan - Dispatcher persisted route source 연결 + +## 이 파일을 읽는 구현 에이전트에게 + +선행 `04+03_failover_budget`의 `complete.log`가 확인된 뒤 구현한다. 구현과 검증을 끝낸 뒤 `CODE_REVIEW-cloud-G09.md`의 구현 에이전트 소유 섹션을 실제 내용과 원문 출력으로 채운다. active 파일은 그대로 두고 review 준비 완료만 보고한다. 종결은 code-review skill 전용이다. 차단되면 정확한 원인, 시도한 명령/출력, 재개 조건만 구현 에이전트 소유 evidence에 기록한다. 사용자에게 질문하거나 user-input 도구를 호출하지 않고, control-plane stop file 생성, 다음 상태 분류, archive/log 이동, `complete.log` 또는 roadmap 수정은 하지 않는다. + +## 배경 + +dispatcher의 worker, review, dry-run이 파일명 lane/G에서 정적 route를 다시 계산하지 않고 선행 selector decision을 유일한 실행 기준으로 소비하게 만든다. + +## 분석 결과 + +### 읽은 파일 + +- `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py` +- `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/select_execution_target.py` +- `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py` +- `agent-test/local/rules.md` + +### SDD 기준 + +- 대상 Acceptance Scenario는 S05와 S09다. persisted route pin과 worker/review initial invocation evidence 중 route source 연결 범위만 소유한다. + +### 테스트 환경 규칙 + +- `test_env=local`; `agent-test/local/rules.md`가 존재하며 끝까지 읽었다. +- agent-ops Python dispatcher에 매칭되는 `agent-test/local/*.md` profile은 없다. 현재 checkout의 Python test manifest를 fallback 근거로 삼아 fresh-process `unittest`, `py_compile`, `git diff --check`를 사용하며 test-rule 유지보수는 필요하지 않다. +- provider subprocess는 fake runner와 임시 workspace/state/locator로 대체하고 실제 cloud/local 호출은 하지 않는다. +- 계획 시점 전체 Python suite 기준선은 169 tests PASS다. + +### 테스트 커버리지 공백 + +- worker route, review initial invocation, dry-run이 selector decision 대신 정적 `route_agent()`를 소비한다. +- dry-run의 persisted resume preview와 non-persisted initial preview가 분리돼 있지 않다. + +### 심볼 참조 + +- `route_agent()` 정의 `dispatch.py:977`과 `run_worker()`(`dispatch.py:3423`), dry-run(`dispatch.py:4201`), 관련 테스트 참조를 selector bridge로 옮긴 뒤 제거한다. + +### 분할 판단 + +- 원본 `05+04_dispatch_integration`을 route source, runtime quota/failover, context/review recovery의 세 behavior slice로 한 단계 분할했다. +- 이 child는 첫 producer이며 `04+03_failover_budget`만 선행한다. + +### 범위 결정 근거 + +- persisted decision의 생성 정책, quota probe, failover budget은 변경하지 않고 live invocation 소비 경계만 교체한다. +- selfcheck target 재사용과 throughput 정책은 후속 sibling 범위다. + +### 최종 라우팅 + +- evaluation_mode: `isolated-reassessment` +- finalizer=`finalize-task-policy.sh`, finalizer_mode=`pair`; build/review status=`routed`. +- build closures: `scope_closed=true`(승인 SDD와 이 subtask 경계가 고정됨), `context_closed=true`(명시된 source/test 범위를 한 local 작업에서 유지 가능), `verification_closed=true`, `evidence_trusted=true`, `ownership_closed=true`, `decision_closed=true`. +- review closures: `scope_closed=true`, `context_closed=true`(동일 source/test 및 구현 evidence로 판정 가능), `verification_closed=true`, `evidence_trusted=true`, `ownership_closed=true`, `decision_closed=true`. +- build route_basis=`local-fit`; capability_gap=`none`. +- build loop-risk audit: matched signatures=`temporal_state`, `boundary_contract`, `variant_product`; 이 기록은 lane/G를 바꾸지 않는다. +- build scores(scope_coupling/state_concurrency/blast_irreversibility/evidence_diagnosis/verification_complexity)=`2/2/1/1/2`, final=`local-G08`, filename=`PLAN-local-G08.md`. +- review route_basis=`official-review`; official execution=`codex/gpt-5.6-sol xhigh`; capability_gap=`none`. +- review scores(scope_coupling/state_concurrency/blast_irreversibility/evidence_diagnosis/verification_complexity)=`2/2/1/2/2`, final=`cloud-G09`, filename=`CODE_REVIEW-cloud-G09.md`. + +## 구현 체크리스트 + +- [ ] worker/review initial invocation과 dry-run이 persisted selector decision을 단일 route source로 사용한다. +- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. + +## 구현 항목 + +### [REFACTOR-1] Live route source 교체 + +- 문제: `dispatch.py:977`의 `route_agent()`와 worker/dry-run call-site가 파일명 lane/G를 정적 CLI spec으로 다시 해석한다. +- 해결 방법: + +```python +# Before: agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py:3423 +agent = route_agent(task) +``` + +```python +# After +decision = state.resolve_execution_decision(task, stage=stage, transition=transition) +agent = agent_spec_from_decision(decision) +``` + + selector decision 생성과 저장을 원자적으로 연결하고 이미 저장된 stage decision은 반드시 resume으로 검증한다. `route_agent()`와 위에 열거한 source/test call-site를 제거한다. 공식 review는 selector의 review 고정 결과를 쓰고 worker decision을 재사용하지 않는다. dry-run은 persisted decision이 있으면 read-only resume preview, 없으면 non-persisted initial preview만 만들며 state/history/admission 기록을 쓰지 않는다. +- 수정 파일 및 체크리스트: + - [ ] `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py`: live worker/review/dry-run route 소비자와 static helper 제거. + - [ ] `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py`: 제거된 helper 참조 갱신과 initial/review/dry-run assertions. +- 테스트 작성: `test_dispatch.py`의 `SelectorDispatcherIntegrationTest`에서 정적 matrix와 selector 결과가 다른 temp task, fake runner, reopened state를 사용해 worker/review initial과 dry-run 무상태를 검증한다. +- 중간 검증: `python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py SelectorDispatcherIntegrationTest -v` — initial/review/dry-run case가 PASS해야 한다. + +## 의존 관계 및 구현 순서 + +1. `04+03_failover_budget`의 archived `complete.log`가 PASS여야 한다. +2. 이 task가 PASS하면 `06+05_runtime_quota_failover`가 persisted route source를 소비한다. + +## 수정 파일 요약 + +| 파일 | 항목 | +|---|---| +| `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py` | REFACTOR-1 | +| `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py` | REFACTOR-1 | + +## 최종 검증 + +- `python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py SelectorDispatcherIntegrationTest -v` — initial/review/dry-run case PASS. +- `python3 -m unittest discover -s agent-ops/skills/project/orchestrate-agent-task-loop/tests -p 'test_*.py'` — fresh process 전체 suite PASS. +- `python3 -m py_compile agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py 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/tests/test_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` — exit 0. +- `git diff --check` — 출력 없음. + +모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. + diff --git a/agent-task/archive/2026/07/m-agent-task-runtime-target-selector/06+05_runtime_quota_failover/code_review_cloud_G05_3.log b/agent-task/archive/2026/07/m-agent-task-runtime-target-selector/06+05_runtime_quota_failover/code_review_cloud_G05_3.log new file mode 100644 index 0000000..ec06aef --- /dev/null +++ b/agent-task/archive/2026/07/m-agent-task-runtime-target-selector/06+05_runtime_quota_failover/code_review_cloud_G05_3.log @@ -0,0 +1,187 @@ + + +# Code Review Reference - REVIEW_REFACTOR + +> **[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/06+05_runtime_quota_failover, plan=3, tag=REVIEW_REFACTOR + +## Archive Evidence Snapshot + +- Prior task: `agent-task/m-agent-task-runtime-target-selector/06+05_runtime_quota_failover` +- Archived plan: `agent-task/m-agent-task-runtime-target-selector/06+05_runtime_quota_failover/plan_local_G06_2.log` +- Archived review: `agent-task/m-agent-task-runtime-target-selector/06+05_runtime_quota_failover/code_review_cloud_G06_2.log` +- Verdict: `WARN`; Required 0, Suggested 1, Nit 0. +- Suggested summary: selector-backed `no_promotion_target` 분기가 `terminal_recovery_retries`를 증가시키지 않아 연속 same-target recovery의 exponential backoff가 `[1, 1]`로 고정된다. +- Affected files: `dispatch.py`, `test_dispatch.py`. +- Verification evidence: focused dispatcher 31 tests와 전체 Python 212 tests, `py_compile`, `git diff --check`는 PASS했다. 별도 deterministic local-G08 연속 `provider-connection` 재현은 invocation `agy -> agy -> agy`, selected/history 정합성과 성공 후 budget reset을 확인했지만 sleep이 `[1, 1]`임을 확인했다. +- Roadmap carryover: 이 pair에는 `Roadmap Targets`가 없으므로 PASS 시 Milestone Task 자동 체크를 주장하지 않는다. + +## 이 파일을 읽는 리뷰 에이전트에게 + +> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다. + +각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요. +리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다. + +1. 판정을 append한다. +2. `CODE_REVIEW-cloud-G05.md` → `code_review_cloud_G05_3.log`, `PLAN-local-G05.md` → `plan_local_G05_3.log`로 아카이브한다. +3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-agent-task-runtime-target-selector/06+05_runtime_quota_failover/`로 이동한다. WARN/FAIL이면 code-review skill이 요구하는 다음 filesystem state를 완전히 작성한다. +4. PASS이고 task group이 `m-`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다. +5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다. + +--- + +## 구현 항목별 완료 여부 + +| 항목 | 완료 여부 | +|------|---------| +| REVIEW_REFACTOR-1 | [x] | + +## 구현 체크리스트 + +- [x] selector-backed `no_promotion_target` 분기에서 sleep 전에 `terminal_recovery_retries`를 증가시켜 기존 terminal recovery와 같은 bounded exponential backoff를 적용한다. +- [x] local-G08 회귀를 최소 두 번 연속 `provider-connection` 뒤 성공하도록 확장하고 같은 AGY target, unchanged selected/history, delay `[2, 4]`, 성공 후 stage failure budget reset을 검증한다. +- [x] focused dispatcher tests, 전체 Python suite, `py_compile`, `git diff --check`를 fresh output으로 검증한다. +- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. + +## 코드리뷰 전용 체크리스트 + +> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다. +> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다. + +- [x] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다. +- [x] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다. +- [x] active `CODE_REVIEW-*-G??.md`를 `code_review_cloud_G05_3.log`로 아카이브한다. +- [x] active `PLAN-*-G??.md`를 `plan_local_G05_3.log`로 아카이브한다. +- [x] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md`와 `agent-task/**/*.log`를 unignore하고 `agent-roadmap/current.md`를 ignore하는지 확인한다. +- [x] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다. +- [x] PASS이면 active task 디렉터리 `agent-task/m-agent-task-runtime-target-selector/06+05_runtime_quota_failover/`를 `agent-task/archive/YYYY/MM/m-agent-task-runtime-target-selector/06+05_runtime_quota_failover/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다. +- [x] PASS이고 task group이 `m-`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다. +- [x] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-agent-task-runtime-target-selector/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다. +- [ ] WARN/FAIL이면 code-review skill의 판정에 맞는 다음 filesystem state를 작성하고 `complete.log`를 작성하지 않는다. + +## 계획 대비 변경 사항 + +계획과 동일하게 구현했다. 추가 변경 없다. + +## 주요 설계 결정 + +- `dispatch.py:3371` 부근의 canonical `no_promotion_target` explicit branch 안에서 `previous_locator = locator` 뒤 `terminal_recovery_retries += 1` 한 줄을 삽입했다. +- delay 식 `min(30, 2 ** min(terminal_recovery_retries, 5))`, `previous_locator` 갱신, selector decision/history 정합성, shared stage failure budget, success reset 로직은 기존과 동일하게 유지했다. +- legacy `promoted_spec()` 경계와 다른 failure branch는 수정하지 않았다. +- 회귀 테스트 `test_no_promotion_target_keeps_same_target_and_persists_state`를 2회 연속 실패 후 성공 시나리오로 확장하고, sleep delay sequence `[2, 4]`와 성공 후 `StageFailureBudget.count() == 0`을 검증하도록 변경했다. + +## 리뷰어를 위한 체크포인트 + +- `no_promotion_target` 분기가 counter 증가 뒤 기존 cap 식으로 sleep하고 첫 두 delay가 2초, 4초인가. +- 연속 failure에서도 actual invocation이 같은 AGY target에 머물고 selected/history가 바뀌지 않는가. +- 성공 시 `recovery_failures`와 현재 worker의 `StageFailureBudget`이 reset되는가. +- selector promotion/legacy recovery와 다른 failure branch의 동작 및 API가 보존되는가. + +## 검증 결과 + +### Focused dispatcher promotion/recovery tests + +명령: `python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py DispatcherCanonicalFailoverIntegrationTest DynamicFailoverBudgetTest ReviewRetryTest -v` + +결과: + +``` +Ran 31 tests in 6.849s +OK +``` + +`test_no_promotion_target_keeps_same_target_and_persists_state` 포함 31 tests 모두 PASS. + +### 전체 Python suite + +명령: `python3 -m unittest discover -s agent-ops/skills/project/orchestrate-agent-task-loop/tests -p 'test_*.py'` + +결과: + +``` +Ran 212 tests in 15.386s +OK +``` + +### py_compile + +명령: `python3 -m py_compile agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py` + +결과: exit 0, 출력 없음. + +### diff check + +명령: `git diff --check` + +결과: exit 0, 출력 없음. + +--- + +> **[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 `[ ]` → `[x]` only | +| 구현 체크리스트 (item text/order) | Fixed at stub creation from plan | Implementing agent checks `[ ]` → `[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 | + +## 코드리뷰 결과 + +### 종합 판정 + +PASS + +### 차원별 평가 + +- Correctness: Pass — `no_promotion_target` same-target recovery가 retry counter를 sleep 전에 증가시켜 연속 실패의 지연이 `[2, 4]`로 진행하고, 실제 invocation은 같은 AGY target에 유지된다. +- Completeness: Pass — 이전 WARN의 counter 전이와 연속 실패 회귀가 모두 구현됐고 계획 범위 밖 selector/persistence 계약은 변경하지 않았다. +- Test coverage: Pass — 두 번 연속 `provider-connection` 뒤 성공하는 회귀가 동일 target, selected/history 불변, backoff 진행과 성공 후 stage budget reset을 함께 검증한다. +- API contract: Pass — public symbol, selector decision schema, persistence schema와 legacy promotion 경계에 추가 변경이 없다. +- Code quality: Pass — 변경은 canonical no-target 분기의 한 counter 전이와 대응 회귀에 한정되며 debug/TODO나 불필요한 분기가 추가되지 않았다. +- Implementation deviation: Pass — active PLAN의 구현·검증 항목과 실제 변경 및 제출 evidence가 일치한다. +- Verification trust: Pass — fresh focused 31개와 전체 212개 테스트, `py_compile`, `git diff --check` 결과가 제출 evidence와 일치한다. + +### 발견된 문제 + +없음 + +### 집계 + +- Required: 0 +- Suggested: 0 +- Nit: 0 + +### 검증 근거 + +- `python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py DispatcherCanonicalFailoverIntegrationTest DynamicFailoverBudgetTest ReviewRetryTest -v` — PASS, 31 tests. +- `python3 -m unittest discover -s agent-ops/skills/project/orchestrate-agent-task-loop/tests -p 'test_*.py'` — PASS, 212 tests. +- `python3 -m py_compile agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py` — PASS. +- `git diff --check` — PASS. + +### 다음 단계 + +- PASS: `complete.log`를 작성하고 task artifacts를 월별 archive로 이동한 뒤 Milestone runtime completion metadata를 보고한다. diff --git a/agent-task/archive/2026/07/m-agent-task-runtime-target-selector/06+05_runtime_quota_failover/code_review_cloud_G06_2.log b/agent-task/archive/2026/07/m-agent-task-runtime-target-selector/06+05_runtime_quota_failover/code_review_cloud_G06_2.log new file mode 100644 index 0000000..2a420d9 --- /dev/null +++ b/agent-task/archive/2026/07/m-agent-task-runtime-target-selector/06+05_runtime_quota_failover/code_review_cloud_G06_2.log @@ -0,0 +1,208 @@ + + +# Code Review Reference - REVIEW_REFACTOR + +> **[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/06+05_runtime_quota_failover, plan=2, tag=REVIEW_REFACTOR + +## Archive Evidence Snapshot + +- Prior task: `agent-task/m-agent-task-runtime-target-selector/06+05_runtime_quota_failover` +- Archived plan: `agent-task/m-agent-task-runtime-target-selector/06+05_runtime_quota_failover/plan_cloud_G09_1.log` +- Archived review: `agent-task/m-agent-task-runtime-target-selector/06+05_runtime_quota_failover/code_review_cloud_G09_1.log` +- Verdict: `FAIL`; Required 1, Suggested 0, Nit 0. +- Required summary: selector-backed worker가 canonical `no_promotion_target`을 삼킨 뒤 legacy `promoted_spec()`으로 승격해 실제 invocation과 persisted decision/history/logical context가 분리된다. +- Affected files: `dispatch.py`, `test_dispatch.py`. +- Verification evidence: Go quota test, selector focused 19 tests, dispatcher focused 12 tests, 전체 Python 209 tests와 `py_compile`은 PASS했지만 deterministic local-G08 daytime `provider-connection` 재현에서 호출은 `agy -> claude`, persisted selected는 Gemini, history는 `initial`, continuation prompt에는 logical context artifact가 없는 불일치를 확인했다. +- Roadmap carryover: 이 pair에는 `Roadmap Targets`가 없으므로 PASS 시 Milestone Task 자동 체크를 주장하지 않는다. + +## 이 파일을 읽는 리뷰 에이전트에게 + +> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다. + +각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요. +리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다. + +1. 판정을 append한다. +2. `CODE_REVIEW-cloud-G06.md` → `code_review_cloud_G06_2.log`, `PLAN-local-G06.md` → `plan_local_G06_2.log`로 아카이브한다. +3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-agent-task-runtime-target-selector/06+05_runtime_quota_failover/`로 이동한다. WARN/FAIL이면 code-review skill이 요구하는 다음 filesystem state를 완전히 작성한다. +4. PASS이고 task group이 `m-`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다. +5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다. + +--- + +## 구현 항목별 완료 여부 + +| 항목 | 완료 여부 | +|------|---------| +| REVIEW_REFACTOR-1 | [x] | + +## 구현 체크리스트 + +- [x] selector-backed live worker가 canonical `no_promotion_target` 뒤 legacy `promoted_spec()`으로 fallthrough하지 않게 하고, 동일 target recovery/budget exhaustion 또는 canonical block으로 종결하며 legacy recovery call-site를 보존한다. +- [x] local-G08 `provider-connection`과 canonical promotion chain exhaustion 회귀를 추가해 invocation target, persisted decision/history, logical context, stage failure budget 정합성을 검증한다. +- [x] focused dispatcher tests, 전체 Python suite, `py_compile`, `git diff --check`를 fresh output으로 검증한다. +- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. + +## 코드리뷰 전용 체크리스트 + +> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다. +> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다. + +- [x] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다. +- [x] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다. +- [x] active `CODE_REVIEW-*-G??.md`를 `code_review_cloud_G06_2.log`로 아카이브한다. +- [x] active `PLAN-*-G??.md`를 `plan_local_G06_2.log`로 아카이브한다. +- [x] `.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/06+05_runtime_quota_failover/`를 `agent-task/archive/YYYY/MM/m-agent-task-runtime-target-selector/06+05_runtime_quota_failover/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다. +- [ ] PASS이고 task group이 `m-`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다. +- [ ] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-agent-task-runtime-target-selector/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다. +- [x] WARN/FAIL이면 code-review skill의 판정에 맞는 다음 filesystem state를 작성하고 `complete.log`를 작성하지 않는다. + +## 계획 대비 변경 사항 + +계획의 pseudocode와 실제 구현 간 미세 차이: + +- 계획에서는 `no_promotion_target` 처리 분기에 `block_selector_error(...)` placeholder가 있었으나, 실제로는 `banner("모델재시도", ...)` + `continue`로 구현했다. 이는 `no_promotion_target`이 terminal error가 아니라 same-target recovery 진입점이기 때문이다. legacy `promoted_spec()`으로의 fallthrough만 차단하면 되며, task block은 budget exhaustion 이후 `recovery_failures >= RECOVERY_FAILURE_LIMIT`에서 처리된다. +- 계획에서는 `except` 블록의 구조를 `if "no_promotion_target" in str(exc): ... continue; block_selector_error(...)`로 제안했으나, 실제 코드는 `if "no_promotion_target" in str(exc): banner + continue; [기존 block 처리]`로 역순이다. 기능적 차이는 없다. + +## 주요 설계 결정 + +1. **`no_promotion_target`을 swallow가 아닌 explicit branch로 처리**: 기존 코드는 `if "no_promotion_target" not in str(exc)`로 역조건을 사용해 의도하지 않은 fallthrough를 허용했다. 새로운 코드는 `if "no_promotion_target" in str(exc)`로 긍정 조건을 사용해 명확히 분기한다. +2. **Same-target recovery 유지**: `no_promotion_target` 상황에서 `previous_locator = locator`를 유지하고 `await asyncio.sleep(...)` 후 `continue`하여 같은 target으로 재시도한다. persisted selected/history는 변경되지 않는다. +3. **Budget exhaustion 경로 보존**: `recovery_failures`는 기존처럼 증가하고 `RECOVERY_FAILURE_LIMIT` 도달 시 block된다. canonical promotion branch 내에서의 재시도는 shared stage failure budget에 기록된다. +4. **Legacy recovery call-site 보존**: `promoted_spec(spec, codex_recovery_count)` 호출은 selector state가 없는 비선택자 워크러와 기존 legacy reclassification/restart recovery 경로에서 그대로 사용된다. + +## 리뷰어를 위한 체크포인트 + +- selector-backed worker의 canonical promotion branch가 성공, `no_promotion_target`, malformed selector error를 모두 종결하고 live `promoted_spec()` call-site로 내려가지 않는가. +- local-G08 `provider-connection` 재시도에서 실제 호출이 `agy -> agy`이고 persisted selected/history가 canonical state와 일치하며 성공 후 shared budget이 reset되는가. +- canonical promotion chain 마지막 target의 qualified failure가 legacy target으로 이동하지 않고 같은 target recovery 또는 budget-limit block으로 끝나는가. +- canonical target이 있는 promotion은 logical context 검증 후 decision/history를 commit하고 다음 invocation을 시작하는 기존 순서를 유지하는가. +- legacy locator reclassification/restart recovery 및 selector state가 없는 기존 역할의 `promoted_spec()` 동작이 보존되는가. + +## 검증 결과 + +### Focused dispatcher promotion/recovery tests + +명령: `python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py DispatcherCanonicalFailoverIntegrationTest DynamicFailoverBudgetTest ReviewRetryTest -v` + +결과: + +``` +Ran 31 tests in 6.726s + +OK +``` + +31 tests PASS (28 existing + 3 new: `test_no_promotion_target_keeps_same_target_and_persists_state`, `test_promotion_chain_exhaustion_stays_on_last_target`, `test_legacy_promoted_spec_still_works_for_non_selector_worker`). + +### 전체 Python suite + +명령: `python3 -m unittest discover -s agent-ops/skills/project/orchestrate-agent-task-loop/tests -p 'test_*.py'` + +결과: + +``` +Ran 212 tests in 15.204s + +OK +``` + +212 tests PASS (209 existing + 3 new). Exit code 0. + +### py_compile + +명령: `python3 -m py_compile agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py` + +결과: + +``` +py_compile OK +``` + +Exit code 0, stdout/stderr 없음. + +### diff check + +명령: `git diff --check` + +결과: + +``` +git diff --check OK +``` + +Exit code 0, 출력 없음. + +--- + +> **[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 `[ ]` → `[x]` only | +| 구현 체크리스트 (item text/order) | Fixed at stub creation from plan | Implementing agent checks `[ ]` → `[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 | + +## 코드리뷰 결과 + +### 종합 판정 + +WARN + +### 차원별 평가 + +- Correctness: Warn — canonical target과 persisted state 정합성은 복구됐지만, 연속 `no_promotion_target` 복구의 backoff가 매번 1초로 고정된다. +- Completeness: Pass — 계획한 legacy fallthrough 차단과 canonical/legacy 경계는 구현됐다. +- Test coverage: Warn — 새 회귀가 한 번의 same-target retry만 실행해 연속 retry의 backoff 진행을 검증하지 않는다. +- API contract: Pass — selector decision과 dispatcher persisted state 형식은 바뀌지 않았고 기존 호출부가 유지된다. +- Code quality: Warn — `terminal_recovery_retries`를 sleep 계산에 사용하면서 해당 분기에서 증가시키지 않는 불완전한 상태 전이가 남았다. +- Implementation deviation: Pass — 계획의 핵심인 selector-backed worker의 legacy 격리와 same-target recovery는 지켜졌다. +- Verification trust: Pass — fresh focused 31개와 전체 212개 테스트, `py_compile`, `git diff --check` 결과가 제출 evidence와 일치하며 별도 재현으로 남은 문제를 확인했다. + +### 발견된 문제 + +- Suggested — `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py:3371`, `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py:6046`: `no_promotion_target` 분기는 `terminal_recovery_retries`를 증가시키지 않은 채 이를 sleep 지수로 사용하므로 연속 `provider-connection` 실패 재현에서 sleep이 `[1, 1]`로 고정된다. quota/connection 장애 때 shared budget 한도까지 짧은 간격으로 provider를 반복 호출하므로, 분기 진입 시 retry counter를 증가시키고 최소 두 번 연속 no-target 실패를 내는 회귀에서 같은 target·persisted state 유지와 함께 backoff 진행 및 성공 시 failure budget reset을 assert해야 한다. + +### 집계 + +- Required: 0 +- Suggested: 1 +- Nit: 0 + +### 검증 근거 + +- `python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py DispatcherCanonicalFailoverIntegrationTest DynamicFailoverBudgetTest ReviewRetryTest -v` — PASS, 31 tests. +- `python3 -m unittest discover -s agent-ops/skills/project/orchestrate-agent-task-loop/tests -p 'test_*.py'` — PASS, 212 tests. +- `python3 -m py_compile agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py` — PASS. +- `git diff --check` — PASS. +- deterministic local-G08 연속 `provider-connection` 재현 — `invoked=['agy', 'agy', 'agy']`, selected/history 정합성 및 성공 후 budget reset은 유지되지만 `sleep_seconds=[1, 1]`로 backoff가 진행되지 않음. + +### 다음 단계 + +- WARN: 위 Suggested finding만 수정·검증하는 후속 PLAN/CODE_REVIEW pair를 생성한다. diff --git a/agent-task/archive/2026/07/m-agent-task-runtime-target-selector/06+05_runtime_quota_failover/code_review_cloud_G09_0.log b/agent-task/archive/2026/07/m-agent-task-runtime-target-selector/06+05_runtime_quota_failover/code_review_cloud_G09_0.log new file mode 100644 index 0000000..d52b5a3 --- /dev/null +++ b/agent-task/archive/2026/07/m-agent-task-runtime-target-selector/06+05_runtime_quota_failover/code_review_cloud_G09_0.log @@ -0,0 +1,188 @@ + + +# Code Review Reference - REFACTOR + +> **[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-25 +task=m-agent-task-runtime-target-selector/06+05_runtime_quota_failover, plan=0, tag=REFACTOR + +## 이 파일을 읽는 리뷰 에이전트에게 + +> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다. + +각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요. +리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다. + +1. 판정을 append한다. +2. `CODE_REVIEW-cloud-G09.md` → `code_review_cloud_G09_0.log`, `PLAN-local-G08.md` → `plan_local_G08_0.log`로 아카이브한다. +3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-agent-task-runtime-target-selector/06+05_runtime_quota_failover/`로 이동한다. WARN/FAIL이면 code-review skill이 요구하는 다음 filesystem state를 완전히 작성한다. +4. PASS이고 task group이 `m-agent-task-runtime-target-selector`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다. +5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다. + +--- + +## 구현 항목별 완료 여부 + +| 항목 | 완료 여부 | +|------|---------| +| REFACTOR-2 Runtime quota probe와 resume/failover state machine | [x] | + +## 구현 체크리스트 + +- [x] bounded candidate의 runtime quota probe가 target/profile required-cap argv로 실행되고 probe 오류는 `unknown` snapshot으로 decision에 남는다. +- [x] resume은 시간·본문 변경에도 pinned target을 사용하고, 확정된 provider target 불가 또는 명시적 retry eligibility만 transition history와 invocation target을 변경한다. +- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. + +## 코드리뷰 전용 체크리스트 + +> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다. +> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다. + +- [x] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다. +- [x] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다. +- [x] active `CODE_REVIEW-cloud-G09.md`를 `code_review_cloud_G09_0.log`로 아카이브한다. +- [x] active `PLAN-local-G08.md`를 `plan_local_G08_0.log`로 아카이브한다. +- [x] `.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/06+05_runtime_quota_failover/`를 `agent-task/archive/YYYY/MM/m-agent-task-runtime-target-selector/06+05_runtime_quota_failover/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다. +- [ ] PASS이고 task group이 `m-agent-task-runtime-target-selector`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다. +- [ ] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-agent-task-runtime-target-selector/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다. +- [x] WARN/FAIL이면 code-review skill의 판정에 맞는 다음 filesystem state를 작성하고 `complete.log`를 작성하지 않는다. + +## 계획 대비 변경 사항 + +계획된 경계와 수정을 정확히 준수하여 구현하였으며 계획 대비 변경 사항은 없음. + +## 주요 설계 결정 + +1. `execution_target_policy.py`: `QuotaProbeSpec` 데이터클래스 및 `quota_probe_spec` 함수를 추가하여 target별 required cap 사양(Gemini: `overall` + `model:`, Claude/Codex: `overall`, Local: `None`)을 명시적으로 수립함. +2. `select_execution_target.py`: `probe_candidate_quota` 함수를 추가하여 shell 없이 subprocess로 `iop-node quota-probe --target ... --command ... --required-cap ... --checked-at ...` 명령을 구성·실행함. 파싱 오류 또는 실행 실패 시 snapshot status를 `unknown`으로 정규화함. +3. `dispatch.py` 및 테스트: resume 핀 메커니즘과 failover 및 same-target recovery/promotion 인터그레이션 테스트를 확장하여 day/night 핀 유지, 본문 수정 유지, generation 변환 초기화, provider target 불가 시 failover, candidate 고갈 시 blocker 전환 동작을 검증함. + +## 리뷰어를 위한 체크포인트 + +- quota probe argv의 canonical target/profile/required caps와 error→unknown이 producer 계약과 일치하는가. +- persisted prior decision이 resume에 전달되고 시간·본문 변경이 target을 바꾸지 않는가. +- 확정된 provider target 불가와 명시적 retry eligibility만 failover하며 generic failure는 same-target recovery인가. +- 기존 cloud promotion이 normalized transition/history와 같은 stage budget을 사용하는가. + +## 검증 결과 + +### SelectorDispatcherIntegrationTest + +명령: `python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py SelectorDispatcherIntegrationTest -v` + +exit_code: 0 +``` +test_dry_run_multiple_ready_tasks_isolation_and_statelessness (__main__.SelectorDispatcherIntegrationTest.test_dry_run_multiple_ready_tasks_isolation_and_statelessness) ... ok +test_dry_run_statelessness_initial_and_resume_previews (__main__.SelectorDispatcherIntegrationTest.test_dry_run_statelessness_initial_and_resume_previews) ... ok +test_qualified_failover_and_blocker_scenarios (__main__.SelectorDispatcherIntegrationTest.test_qualified_failover_and_blocker_scenarios) ... ok +test_resume_pins_target_across_time_and_body_changes_and_resets_on_new_generation (__main__.SelectorDispatcherIntegrationTest.test_resume_pins_target_across_time_and_body_changes_and_resets_on_new_generation) ... ok +test_worker_and_review_initial_invocation_uses_selector (__main__.SelectorDispatcherIntegrationTest.test_worker_and_review_initial_invocation_uses_selector) ... ok + +---------------------------------------------------------------------- +Ran 5 tests in 5.164s + +OK +``` + +### Selector 전체 회귀 + +명령: `python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_select_execution_target.py -v` + +exit_code: 0 +``` +---------------------------------------------------------------------- +Ran 40 tests in 1.446s + +OK +``` + +### 전체 Python suite + +명령: `python3 -m unittest discover -s agent-ops/skills/project/orchestrate-agent-task-loop/tests -p 'test_*.py'` + +exit_code: 0 +``` +---------------------------------------------------------------------- +Ran 204 tests in 18.796s + +OK +``` + +### py_compile + +명령: `python3 -m py_compile agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py 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/tests/test_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` + +exit_code: 0 +``` +(empty output) +``` + +### diff check + +명령: `git diff --check` + +exit_code: 0 +``` +(empty output) +``` + + +--- + +> **[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 `[ ]` → `[x]` only | +| 구현 체크리스트 (item text/order) | Fixed at stub creation from plan | Implementing agent checks `[ ]` → `[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 | + +## 코드리뷰 결과 + +### 종합 판정 + +FAIL + +### 차원별 평가 + +- Correctness: Fail — 실제 quota snapshot의 `available | exhausted`가 모두 `unknown`으로 손실되고 cloud promotion 실행 target이 persisted decision과 어긋난다. +- Completeness: Fail — runtime-probed failover quota 보존과 cloud promotion transition/history 갱신이 계획 범위를 충족하지 않는다. +- Test coverage: Fail — Python probe fixture가 실제 Go JSON schema를 사용하지 않으며 cloud promotion 테스트가 persisted state와 logical context를 검증하지 않는다. +- API contract: Fail — `iop-node quota-probe` producer와 Python consumer의 JSON field 계약이 불일치한다. +- Code quality: Pass — 범위 내 debug 출력, dead code, 미해결 TODO는 발견하지 않았다. +- Implementation deviation: Fail — 계획이 요구한 normalized transition/history가 기존 cloud promotion 경로에 연결되지 않았다. +- Verification trust: Fail — 계획된 204개 Python 테스트와 Go quota-probe 테스트는 통과하지만 실제 producer shape 재현이 실패하므로 성공 출력만으로 runtime 경로를 신뢰할 수 없다. +- Spec conformance: Fail — SDD S04, S09 및 Evidence Map의 quota snapshot·invocation audit 조건을 충족하지 않는다. + +### 발견된 문제 + +- Required — `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/select_execution_target.py:421`, `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/select_execution_target.py:895`: 실제 Go producer는 상태를 `targets[].status`에 기록하지만 consumer는 존재하지 않는 top-level `status`를 읽어 confirmed `available`/`exhausted`를 `unknown`으로 바꾼다. 또한 failover 중 직접 probe한 cloud alternate의 snapshot을 `_selected_quota`에 전달하지 않아 candidate와 selected quota가 불일치한다. 실제 `QuotaSnapshot` shape를 검증·보존하고 adapter/target에 매칭되는 `targets[].status`를 사용하며, 선택된 alternate의 probed snapshot까지 decision quota에 연결하는 회귀 테스트를 추가해야 한다. +- Required — `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py:3299`: 기존 cloud promotion은 `spec` 지역 변수만 `Claude/Codex`로 바꾸고 `execution_decisions`, `route_transition_history`, cross-adapter logical context를 갱신하지 않는다. qualified promotion을 canonical selector transition으로 만들고 context package를 검증한 뒤 다음 invocation 전에 같은 persisted state에 commit하며, 실제 invocation target·transition history·공통 stage budget·context evidence가 일치하는 통합 테스트를 추가해야 한다. + +### 다음 단계 + +- FAIL: 위 Required findings를 반영한 follow-up PLAN/CODE_REVIEW pair를 생성한다. diff --git a/agent-task/archive/2026/07/m-agent-task-runtime-target-selector/06+05_runtime_quota_failover/code_review_cloud_G09_1.log b/agent-task/archive/2026/07/m-agent-task-runtime-target-selector/06+05_runtime_quota_failover/code_review_cloud_G09_1.log new file mode 100644 index 0000000..1f0518d --- /dev/null +++ b/agent-task/archive/2026/07/m-agent-task-runtime-target-selector/06+05_runtime_quota_failover/code_review_cloud_G09_1.log @@ -0,0 +1,263 @@ + + +# Code Review Reference - REVIEW_REFACTOR + +> **[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/06+05_runtime_quota_failover, plan=1, tag=REVIEW_REFACTOR + +## Archive Evidence Snapshot + +- Prior task: `agent-task/m-agent-task-runtime-target-selector/06+05_runtime_quota_failover` +- Archived plan: `agent-task/m-agent-task-runtime-target-selector/06+05_runtime_quota_failover/plan_local_G08_0.log` +- Archived review: `agent-task/m-agent-task-runtime-target-selector/06+05_runtime_quota_failover/code_review_cloud_G09_0.log` +- Verdict: `FAIL`; Required 2, Suggested 0, Nit 0. +- Required summary: 실제 Go `QuotaSnapshot.targets[].status` 소비와 failover selected quota 보존이 누락됐고, cloud promotion이 persisted decision/history 및 logical context를 갱신하지 않았다. +- Affected files: `execution_target_policy.py`, `select_execution_target.py`, `dispatch.py`와 대응 Python tests. Go producer schema는 변경하지 않는다. +- Verification evidence: 계획된 Python 204 tests와 Go quota-probe tests는 PASS했지만, 실제 Go shape 최소 재현은 `exhausted → unknown`을 출력했다. +- Roadmap carryover: 이 pair에는 `Roadmap Targets`가 없으므로 PASS 시 Milestone Task 자동 체크를 주장하지 않는다. + +## 이 파일을 읽는 리뷰 에이전트에게 + +> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다. + +각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요. +리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다. + +1. 판정을 append한다. +2. `CODE_REVIEW-cloud-G09.md` → `code_review_cloud_G09_1.log`, `PLAN-cloud-G09.md` → `plan_cloud_G09_1.log`로 아카이브한다. +3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-agent-task-runtime-target-selector/06+05_runtime_quota_failover/`로 이동한다. WARN/FAIL이면 code-review skill이 요구하는 다음 filesystem state를 완전히 작성한다. +4. PASS이고 task group이 `m-`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다. +5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다. + +--- + +## 구현 항목별 완료 여부 + +| 항목 | 완료 여부 | +|------|---------| +| REVIEW_REFACTOR-1 Go quota snapshot consumer와 selected quota 정합성 | [x] | +| REVIEW_REFACTOR-2 Cloud promotion canonical transition과 logical context commit | [x] | + +## 구현 체크리스트 + +- [x] 실제 Go `QuotaSnapshot.targets[].status`를 검증·보존하고 initial/failover selected quota가 같은 snapshot identity/status를 사용하도록 수정하며 exact-shape available/exhausted/unknown 회귀를 추가한다. +- [x] qualified cloud promotion을 policy-owned canonical transition으로 실행해 logical context, persisted decision/history, invocation target과 공유 stage budget을 일치시키고 통합 회귀를 추가한다. +- [x] focused Go/Python tests, 전체 Python suite, `py_compile`, `git diff --check`를 fresh output으로 검증한다. +- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. + +## 코드리뷰 전용 체크리스트 + +> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다. +> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다. + +- [x] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다. +- [x] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다. +- [x] active `CODE_REVIEW-*-G??.md`를 `code_review_cloud_G09_1.log`로 아카이브한다. +- [x] active `PLAN-*-G??.md`를 `plan_cloud_G09_1.log`로 아카이브한다. +- [x] `.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/06+05_runtime_quota_failover/`를 `agent-task/archive/YYYY/MM/m-agent-task-runtime-target-selector/06+05_runtime_quota_failover/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다. +- [ ] PASS이고 task group이 `m-`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다. +- [ ] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-agent-task-runtime-target-selector/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다. +- [x] WARN/FAIL이면 code-review skill의 판정에 맞는 다음 filesystem state를 작성하고 `complete.log`를 작성하지 않는다. + +## 계획 대비 변경 사항 + +- 기능 범위와 수정 파일은 계획과 동일하다. +- verbose dispatcher/전체 suite 출력은 문서 과대화를 피하면서 원문 검증이 가능하도록 merged stdout/stderr를 `/tmp` evidence 파일에 보존하고 byte 수와 SHA-256, 종료부 원문을 함께 기록했다. 테스트 명령 자체는 계획서와 동일하다. +- 전체 회귀에서 기존 테스트가 `dispatch.datetime`을 mock할 때 persisted `evaluated_at` 파싱까지 mock되는 충돌이 발견되어, canonical 검증은 selector 모듈의 실제 `datetime.fromisoformat`을 사용하도록 보완했다. + +## 주요 설계 결정 + +- 실제 Go producer snapshot은 strict producer-shape 검증 후 원본 dict를 그대로 반환한다. subprocess/JSON/schema/target identity 오류만 요청 target의 `unknown` snapshot으로 정규화한다. +- initial/failover 모두 selected candidate가 평가된 동일 snapshot을 `_selected_quota`에 전달하고 `targets`를 보존해 candidate tri-state와 top-level selected quota의 identity/status가 갈라지지 않게 했다. +- promotion chain과 canonical target resolution은 순수 policy 모듈이 소유한다. selector는 qualified failure, 인접 promotion path, no-bounce를 검증하고 resume에도 path를 보존한다. +- live worker promotion은 logical context package 검증을 먼저 끝낸 뒤 decision/history를 commit한다. 따라서 다음 invocation target, persisted selected target, transition history, work-unit/stage failure budget이 한 전이로 정렬된다. +- `gpt-5.6-terra` promotion은 policy identity와 함께 `high` reasoning effort로 materialize한다. 기존 `promoted_spec`은 selector state가 없는 legacy recovery 경로에만 유지했다. +- 새 dependency 및 Go producer schema 변경은 없다. + +## 리뷰어를 위한 체크포인트 + +- actual Go `QuotaSnapshot.targets[]` identity/status/source/checked_at이 변형 없이 selector decision에 반영되는가. +- runtime-probed cloud alternate의 candidate quota와 selected top-level quota가 같은 snapshot identity/status를 가리키는가. +- live cloud promotion이 policy-owned canonical target만 사용하고 cross-adapter context package 검증 뒤 commit되는가. +- promotion 뒤 invocation, `execution_decisions`, `route_transition_history`, used targets와 stage budget이 같은 work unit/stage를 가리키는가. +- legacy locator recovery는 의도한 call-site에만 남고 local failover/resume 회귀가 유지되는가. + +## 검증 결과 + +### Go quota producer contract + +명령: `go test -count=1 ./apps/node/cmd/node -run 'QuotaProbe'` + +stdout/stderr: + +```text +ok iop/apps/node/cmd/node 0.010s +``` + +exit code: `0` + +### Selector producer-shape와 quota 회귀 + +명령: `python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_select_execution_target.py SelectorFailoverContractTests SelectorQuotaRepresentationTests -v` + +stdout/stderr: + +```text +test_cross_boundary_failover_resumes_pinned_decision (__main__.SelectorFailoverContractTests.test_cross_boundary_failover_resumes_pinned_decision) ... ok +test_failover_never_returns_to_an_earlier_candidate_rank (__main__.SelectorFailoverContractTests.test_failover_never_returns_to_an_earlier_candidate_rank) ... ok +test_generic_failure_and_exhausted_or_used_candidate_fail_closed (__main__.SelectorFailoverContractTests.test_generic_failure_and_exhausted_or_used_candidate_fail_closed) ... ok +test_policy_owned_cloud_promotion_chain_and_no_bounce (__main__.SelectorFailoverContractTests.test_policy_owned_cloud_promotion_chain_and_no_bounce) ... ok +test_probe_candidate_quota_argv_and_normalization (__main__.SelectorFailoverContractTests.test_probe_candidate_quota_argv_and_normalization) ... ok +test_probe_candidate_quota_error_normalizes_to_unknown (__main__.SelectorFailoverContractTests.test_probe_candidate_quota_error_normalizes_to_unknown) ... ok +test_qualified_failover_uses_only_unused_eligible_candidate (__main__.SelectorFailoverContractTests.test_qualified_failover_uses_only_unused_eligible_candidate) ... ok +test_runtime_probed_cloud_alternate_round_trips_selected_snapshot (__main__.SelectorFailoverContractTests.test_runtime_probed_cloud_alternate_round_trips_selected_snapshot) ... ok +test_tampered_prior_decision_rejected (__main__.SelectorFailoverContractTests.test_tampered_prior_decision_rejected) ... ok +test_unknown_is_admitted_once_and_no_bounce_remains (__main__.SelectorFailoverContractTests.test_unknown_is_admitted_once_and_no_bounce_remains) ... ok +test_actual_go_snapshot_shape_preserves_tri_state_and_metadata (__main__.SelectorQuotaRepresentationTests.test_actual_go_snapshot_shape_preserves_tri_state_and_metadata) ... ok +test_all_candidates_exhausted_returns_no_eligible_target (__main__.SelectorQuotaRepresentationTests.test_all_candidates_exhausted_returns_no_eligible_target) ... ok +test_exhausted_gemini_falls_back_to_laguna (__main__.SelectorQuotaRepresentationTests.test_exhausted_gemini_falls_back_to_laguna) ... ok +test_generic_stderr_is_not_quota_evidence (__main__.SelectorQuotaRepresentationTests.test_generic_stderr_is_not_quota_evidence) ... ok +test_injected_snapshot_is_reflected (__main__.SelectorQuotaRepresentationTests.test_injected_snapshot_is_reflected) ... ok +test_local_route_does_not_call_probe (__main__.SelectorQuotaRepresentationTests.test_local_route_does_not_call_probe) ... ok +test_quota_probe_tri_state (__main__.SelectorQuotaRepresentationTests.test_quota_probe_tri_state) ... ok +test_quota_representation_without_snapshot (__main__.SelectorQuotaRepresentationTests.test_quota_representation_without_snapshot) ... ok +test_unknown_is_admitted_once_per_work_unit (__main__.SelectorQuotaRepresentationTests.test_unknown_is_admitted_once_per_work_unit) ... ok + +---------------------------------------------------------------------- +Ran 19 tests in 0.188s + +OK +``` + +exit code: `0` + +### Dispatcher promotion state 회귀 + +명령: `python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py DispatcherCanonicalFailoverIntegrationTest DynamicFailoverBudgetTest -v` + +merged stdout/stderr 원문: `/tmp/iop-cloud-g09-dispatch-focused.log` (`12637` bytes, SHA-256 `98e17eb0202771e50aa7fe1f79c1b86d23e8be9cd75c023e05e4ecda6191e3e2`) + +종료부 원문: + +```text +retry=10/10 +ok +test_success_resets_only_current_stage_budget (__main__.DynamicFailoverBudgetTest.test_success_resets_only_current_stage_budget) ... ok + +---------------------------------------------------------------------- +Ran 12 tests in 0.562s + +OK +``` + +exit code: `0` + +### 전체 Python suite + +명령: `python3 -m unittest discover -s agent-ops/skills/project/orchestrate-agent-task-loop/tests -p 'test_*.py'` + +merged stdout/stderr 원문: `/tmp/iop-cloud-g09-full-unittest.log` (`52534` bytes, SHA-256 `95f9c26fd48683bda812cff48ac53146ea2138609a3ba89ac8b6a2471264eef0`) + +종료부 원문: + +```text +.......................................................... +---------------------------------------------------------------------- +Ran 209 tests in 15.164s + +OK +``` + +exit code: `0` + +### py_compile + +명령: `python3 -m py_compile agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py 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/tests/test_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` + +stdout/stderr: 출력 없음 +exit code: `0` + +### diff check + +명령: `git diff --check` + +stdout/stderr: 출력 없음 +exit code: `0` + +--- + +> **[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 `[ ]` → `[x]` only | +| 구현 체크리스트 (item text/order) | Fixed at stub creation from plan | Implementing agent checks `[ ]` → `[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 | + +## 코드리뷰 결과 + +### 종합 판정 + +FAIL + +### 발견 사항 + +- Required — `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py:3354`, `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py:3406`: selector-backed worker가 canonical promotion에서 `no_promotion_target`을 받으면 예외를 삼킨 뒤 legacy `promoted_spec()`으로 fallthrough한다. local-G08 daytime의 Gemini 선택 상태에서 `provider-connection`을 재현하면 실제 두 번째 호출은 Claude로 승격되지만 persisted `execution_decisions.worker.selected`는 Gemini, `route_transition_history`는 `initial`에 머물고 continuation prompt에도 logical context artifact가 없다. 이는 selector가 소유해야 하는 실행 대상 전이와 실제 invocation·persisted decision·history·context의 단일 상태 계약을 깨고 SDD S06/S07/S09 및 이번 PLAN의 legacy 격리 요구를 위반한다. Selector-backed worker 분기는 canonical policy가 승격 대상을 주지 않는 경우에도 legacy cross-target promotion으로 빠지지 않고 동일 target 복구/차단 규칙으로 종결해야 하며, `promoted_spec()`은 검증된 legacy recovery에만 남겨야 한다. local-G08 `provider-connection` 및 canonical promotion chain exhaustion 회귀 테스트를 추가해 실제 호출 대상, persisted decision, history, logical context, stage failure budget이 일치함을 검증해야 한다. + +### 차원별 판정 + +| 차원 | 판정 | 근거 | +|---|---|---| +| Correctness | Fail | selector 밖 legacy 승격으로 실제 호출과 저장된 결정/이력이 분리된다. | +| Completeness | Fail | PLAN이 요구한 selector-backed live path의 완전한 canonical 전이 소유권이 충족되지 않았다. | +| Test coverage | Fail | `provider-connection`과 canonical no-target에서 legacy 승격으로 빠지는 경로가 회귀 테스트에 없다. | +| API contract | Pass | 실제 Go quota snapshot shape와 tri-state/metadata round-trip은 독립 검증으로 확인했다. | +| Code quality | Pass | 관련 정책/selector 경계와 오류 처리는 전반적으로 명시적이다. | +| Implementation deviation | Fail | `promoted_spec()`을 legacy recovery에만 남긴다는 계획과 달리 selector-backed live worker가 계속 호출한다. | +| Verification trust | Fail | 제출된 전체 suite는 통과하지만 누락된 deterministic branch 재현에서 상태 불일치가 확인됐다. | +| Spec conformance | Fail | SDD S06/S07/S09의 허용 전이, continuation context, invocation-state 일치 계약을 위반한다. | + +### 집계 + +- Required: 1 +- Suggested: 0 +- Nit: 0 + +### 검증 근거 + +- `go test -count=1 ./apps/node/cmd/node -run 'QuotaProbe'` — PASS. +- selector focused tests — PASS, 19 tests. +- dispatcher focused tests — PASS, 12 tests. +- 전체 Python suite — PASS, 209 tests. +- `python3 -m py_compile ...` — PASS. +- `git diff --check` — PASS. +- deterministic local-G08 daytime `provider-connection` reproducer — FAIL: 호출은 `agy -> claude`였지만 persisted selected는 `agy/Gemini 3.6 Flash (Medium)`, history는 `initial`만 유지되고 두 번째 prompt에 logical context artifact가 없었다. + +### 다음 단계 + +- Required 수정 후 후속 PLAN/CODE_REVIEW 쌍으로 재검증한다. diff --git a/agent-task/archive/2026/07/m-agent-task-runtime-target-selector/06+05_runtime_quota_failover/complete.log b/agent-task/archive/2026/07/m-agent-task-runtime-target-selector/06+05_runtime_quota_failover/complete.log new file mode 100644 index 0000000..4edd8e8 --- /dev/null +++ b/agent-task/archive/2026/07/m-agent-task-runtime-target-selector/06+05_runtime_quota_failover/complete.log @@ -0,0 +1,38 @@ +# Complete - m-agent-task-runtime-target-selector/06+05_runtime_quota_failover + +## 완료 일시 + +2026-07-26 + +## 요약 + +selector-backed `no_promotion_target` 복구의 target/state 정합성과 bounded exponential backoff를 4회 리뷰 루프로 보완했으며 최종 판정은 PASS다. + +## 루프 이력 + +| Plan | Review | Verdict | 메모 | +|------|--------|---------|------| +| `plan_local_G08_0.log` | `code_review_cloud_G09_0.log` | FAIL | 실제 quota snapshot shape와 canonical promotion persistence/context 경계를 보완하도록 후속화했다. | +| `plan_cloud_G09_1.log` | `code_review_cloud_G09_1.log` | FAIL | selector-backed worker의 `no_promotion_target` legacy fallthrough를 차단하도록 후속화했다. | +| `plan_local_G06_2.log` | `code_review_cloud_G06_2.log` | WARN | same-target recovery의 backoff가 `[1, 1]`로 고정되는 counter 전이를 후속화했다. | +| `plan_local_G05_3.log` | `code_review_cloud_G05_3.log` | PASS | 동일 AGY target과 persisted state를 유지하며 지연 `[2, 4]` 및 성공 후 budget reset을 검증했다. | + +## 구현/정리 내용 + +- canonical `no_promotion_target` same-target recovery에서 `terminal_recovery_retries`를 sleep 전에 증가시켜 기존 terminal recovery와 같은 bounded exponential backoff를 적용했다. +- 회귀를 연속 `provider-connection` 2회 뒤 성공하는 시나리오로 확장해 동일 target, selected/history 불변, delay `[2, 4]`, 성공 후 stage failure budget reset을 검증했다. + +## 최종 검증 + +- `python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py DispatcherCanonicalFailoverIntegrationTest DynamicFailoverBudgetTest ReviewRetryTest -v` - PASS; 31 tests, `OK`. +- `python3 -m unittest discover -s agent-ops/skills/project/orchestrate-agent-task-loop/tests -p 'test_*.py'` - PASS; 212 tests, `OK`. +- `python3 -m py_compile agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py` - PASS; exit 0. +- `git diff --check` - PASS; 출력 없음. + +## 잔여 Nit + +- 없음 + +## 후속 작업 + +- 없음 diff --git a/agent-task/archive/2026/07/m-agent-task-runtime-target-selector/06+05_runtime_quota_failover/plan_cloud_G09_1.log b/agent-task/archive/2026/07/m-agent-task-runtime-target-selector/06+05_runtime_quota_failover/plan_cloud_G09_1.log new file mode 100644 index 0000000..9435384 --- /dev/null +++ b/agent-task/archive/2026/07/m-agent-task-runtime-target-selector/06+05_runtime_quota_failover/plan_cloud_G09_1.log @@ -0,0 +1,185 @@ + + +# Plan - Runtime quota snapshot과 cloud promotion state 정합성 보완 + +## 이 파일을 읽는 구현 에이전트에게 + +구현과 검증을 끝낸 뒤 `CODE_REVIEW-cloud-G09.md`의 구현 에이전트 소유 섹션을 실제 내용과 원문 출력으로 채운다. active 파일은 그대로 두고 review 준비 완료만 보고한다. 종결은 code-review skill 전용이다. 차단되면 정확한 원인, 시도한 명령/출력, 재개 조건만 구현 에이전트 소유 evidence에 기록한다. 사용자에게 질문하거나 user-input 도구를 호출하지 않고, control-plane stop file 생성, 다음 상태 분류, archive/log 이동, `complete.log` 또는 roadmap 수정은 하지 않는다. + +## 배경 + +첫 리뷰에서 `iop-node quota-probe`의 실제 JSON shape가 Python consumer에서 손실되고, live cloud promotion이 실행 target만 바꾼 채 persisted decision/history와 logical context를 갱신하지 않는 두 Required 결함이 확인됐다. producer 계약과 runtime state를 같은 canonical transition으로 맞추고 실제 cross-component fixture로 회귀를 닫는다. + +## Archive Evidence Snapshot + +- Prior task: `agent-task/m-agent-task-runtime-target-selector/06+05_runtime_quota_failover` +- Archived plan: `agent-task/m-agent-task-runtime-target-selector/06+05_runtime_quota_failover/plan_local_G08_0.log` +- Archived review: `agent-task/m-agent-task-runtime-target-selector/06+05_runtime_quota_failover/code_review_cloud_G09_0.log` +- Verdict: `FAIL`; Required 2, Suggested 0, Nit 0. +- Required summary: 실제 Go `QuotaSnapshot.targets[].status` 소비와 failover selected quota 보존이 누락됐고, cloud promotion이 persisted decision/history 및 logical context를 갱신하지 않았다. +- Affected files: `execution_target_policy.py`, `select_execution_target.py`, `dispatch.py`와 대응 Python tests. Go producer schema는 변경하지 않는다. +- Verification evidence: 계획된 Python 204 tests와 Go quota-probe tests는 PASS했지만, 실제 Go shape 최소 재현은 `exhausted → unknown`을 출력했다. +- Roadmap carryover: 이 pair에는 `Roadmap Targets`가 없으므로 PASS 시 Milestone Task 자동 체크를 주장하지 않는다. + +## 분석 결과 + +### 읽은 파일 + +- `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` +- `apps/node/cmd/node/quota_probe.go` +- `apps/node/cmd/node/quota_probe_test.go` +- `apps/node/internal/adapters/cli/status/quota.go` +- `agent-test/local/rules.md` +- `agent-test/local/testing-smoke.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` + +### SDD 기준 + +- SDD는 `[승인됨]`, 잠금은 `해제`다. +- S04/`quota-input` Evidence Map은 실제 Go usage checker snapshot의 required-cap tri-state와 work-unit admission을 요구한다. +- S06/`context-failover`, S07/`failure-budget`, S09/`dispatch-integration` Evidence Map은 qualified cross-target transition, 공유 stage budget, invocation/persisted decision/history 일치를 요구한다. +- 구현 체크리스트와 최종 검증은 실제 producer JSON fixture, selected quota round-trip, cloud promotion state/context/budget 통합 assertion으로 위 evidence를 직접 검증한다. + +### 테스트 환경 규칙 + +- `test_env=local`; `agent-test/local/rules.md`와 `agent-test/local/testing-smoke.md`를 읽었다. +- testing-smoke의 Edge-Node full-cycle/live provider 명령은 agent-ops Python dispatcher와 hidden Go quota JSON bridge를 직접 실행하지 않아 이 follow-up의 pass criterion으로 사용하지 않는다. +- 현재 checkout에서 fresh-process Python `unittest`, `py_compile`, `git diff --check`와 `go test -count=1 ./apps/node/cmd/node -run 'QuotaProbe'`를 적용한다. 외부 provider/secret/service는 사용하지 않으며 test-rule 유지보수는 필요하지 않다. +- Go preflight는 `/config/.local/bin/go` → `/config/opt/go/bin/go`, `go1.26.2 linux/arm64`, `GOROOT=/config/opt/go`로 일치했다. + +### 테스트 커버리지 공백 + +- `test_probe_candidate_quota_argv_and_normalization`은 실제 producer에 없는 top-level `status` fixture를 사용해 producer-consumer schema drift를 놓친다. +- runtime-probed cloud alternate를 선택한 failover에서 `candidates[].quota_status`와 top-level `quota`의 동일 snapshot identity/status를 검증하지 않는다. +- `test_cloud_g07_provider_quota_promotes_claude_to_codex_without_no_failover_block`은 invocation spec만 보고 persisted decision/history, logical context, stage budget을 검증하지 않는다. + +### 심볼 참조 + +- `probe_candidate_quota`: `_candidate_quota`, `_failover`, selector tests에서 사용한다. +- `promoted_spec`: live `run_escalating`, legacy locator recovery, dry-run legacy recovery와 기존 tests에서 사용한다. live selector-backed promotion만 canonical transition으로 교체하고 legacy recovery call-site는 별도 근거 없이 제거하지 않는다. +- 새 dependency는 추가하지 않는다. + +### 분할 판단 + +- 기존 split child `06+05_runtime_quota_failover`의 FAIL follow-up이며 `05+04_dispatch_route_source`는 `agent-task/archive/2026/07/m-agent-task-runtime-target-selector/05+04_dispatch_route_source/complete.log`로 충족됐다. +- 두 Required는 quota-selected candidate와 그 다음 runtime transition이 동일 persisted decision을 유지해야 한다는 하나의 state-machine invariant를 공유하고 selector/dispatcher 통합 테스트를 함께 갱신한다. nested subtask를 만들면 같은 transition schema와 fixture를 중복 변경하므로 단일 follow-up pair로 유지한다. +- sibling active artifacts와 dirty worktree는 사용자 소유로 보존하고 이 plan의 명시 파일 외 변경을 수정하지 않는다. + +### 범위 결정 근거 + +- Go `QuotaSnapshot` producer schema, admission batch, selfcheck, target cap removal, unknown isolation, downstream audit child, roadmap 문서는 변경하지 않는다. +- agent-spec index에는 agent-task dispatcher에 매칭되는 living spec이 없고 agent-contract index에도 hidden quota-probe/runtime selector 계약 문서가 없다. 새 문서를 추정 생성하지 않고 승인 SDD, Go producer, 코드와 tests를 source of truth로 사용한다. + +### 최종 라우팅 + +- evaluation_mode: `isolated-reassessment`; finalizer=`finalize-task-policy.sh`, finalizer_mode=`pair`. +- build closures: `scope_closed=true`(두 Required와 영향 경로 고정), `context_closed=true`(producer/selector/dispatcher와 tests를 함께 확인), `verification_closed=true`(exact-shape fixture와 focused/full tests), `evidence_trusted=true`(실제 shape 재현으로 실패 확인), `ownership_closed=true`(현재 child와 legacy recovery 경계 고정), `decision_closed=true`(승인 SDD와 기존 producer 계약으로 결정 가능). +- review closures: build와 같은 source/state/verification evidence로 모두 `true`. +- build route_basis=`grade-boundary`; capability_gap=`none`; scores(scope_coupling/state_concurrency/blast_irreversibility/evidence_diagnosis/verification_complexity)=`2/2/1/2/2`, final=`cloud-G09`, filename=`PLAN-cloud-G09.md`. +- review route_basis=`official-review`; capability_gap=`none`; scores=`2/2/1/2/2`, final=`cloud-G09`, filename=`CODE_REVIEW-cloud-G09.md`; target=`codex/gpt-5.6-sol xhigh`. +- loop-risk observations: ordered transitions 5개(initial/resume/failover/promotion/recovery), dispatcher/provider actors 2개와 persisted commit 제약, Go producer-selector-dispatcher 3-component JSON boundary, JSON/argv structured interpretation, status×transition variant product가 확인됐다. matched signatures=`temporal_state`, `concurrent_consistency`, `boundary_contract`, `structured_interpretation`, `variant_product`; triggered=`true`. signature는 lane/G에 영향을 주지 않았다. +- G floor: `none`; 반복 횟수와 기존 route는 입력과 판정에 사용하지 않았다. + +## 구현 체크리스트 + +- [ ] 실제 Go `QuotaSnapshot.targets[].status`를 검증·보존하고 initial/failover selected quota가 같은 snapshot identity/status를 사용하도록 수정하며 exact-shape available/exhausted/unknown 회귀를 추가한다. +- [ ] qualified cloud promotion을 policy-owned canonical transition으로 실행해 logical context, persisted decision/history, invocation target과 공유 stage budget을 일치시키고 통합 회귀를 추가한다. +- [ ] focused Go/Python tests, 전체 Python suite, `py_compile`, `git diff --check`를 fresh output으로 검증한다. +- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. + +## 구현 항목 + +### [REVIEW_REFACTOR-1] Go quota snapshot consumer와 selected quota 정합성 + +- 문제: `select_execution_target.py:421`은 실제 producer에 없는 top-level `status`를 읽어 `targets[].status=available|exhausted`를 `unknown`으로 바꾼다. `_failover`의 `select_execution_target.py:865-895`는 직접 probe한 alternate snapshot을 선택 후 top-level `quota`에 전달하지 않는다. +- 해결 방법: + +```python +# Before: select_execution_target.py:421 +status = data.get("status") +return {"snapshot_id": data.get("snapshot_id"), "targets": [{"status": status}]} +``` + +```python +# After +snapshot = _validate_quota_snapshot(data) +status = _snapshot_status(candidate_identity(adapter, target), snapshot) +return snapshot +``` + + producer의 `snapshot_id`, `source`, `checked_at`, `targets`를 보존하고 요청 adapter/target에 매칭되는 tri-state만 소비한다. `_failover`는 candidate별 probed snapshot을 보관해 선택된 cloud alternate의 `_selected_quota(..., probed_snapshot=selected_snapshot)`에 전달한다. subprocess 오류/timeout/parse/schema mismatch만 target-local `unknown`으로 정규화한다. +- 수정 파일 및 체크리스트: + - [ ] `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/select_execution_target.py`: actual snapshot validation/preservation과 selected failover snapshot 연결. + - [ ] `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_select_execution_target.py`: 실제 Go shape available/exhausted/unknown 및 alternate round-trip 회귀. +- 테스트 작성: `SelectorFailoverContractTests` 또는 quota 전용 class에 실제 `QuotaSnapshot` shape를 반환하는 fake subprocess를 두고 exhausted initial rejection/fallback, available selection, malformed→unknown, failover candidate/top-level quota 동일성을 assert한다. +- 중간 검증: `python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_select_execution_target.py SelectorFailoverContractTests SelectorQuotaRepresentationTests -v`와 `go test -count=1 ./apps/node/cmd/node -run 'QuotaProbe'`가 PASS해야 한다. + +### [REVIEW_REFACTOR-2] Cloud promotion canonical transition과 logical context commit + +- 문제: `dispatch.py:3299-3326`은 live cloud promotion에서 `spec` 지역 변수만 바꿔 실제 invocation이 `execution_decisions[stage]` 및 `route_transition_history`와 어긋나고 cross-adapter context package를 검증하지 않는다. +- 해결 방법: + +```python +# Before: dispatch.py:3299 +next_spec = promoted_spec(spec, codex_recovery_count) +spec = next_spec +previous_locator = locator +``` + +```python +# After +next_decision = select_execution_decision( + task, stage=role, prior_decision=current_decision, + transition="promotion", failure_class=failure, +) +context = build_context_package( + workspace, task, locator, previous_spec=spec, + next_spec=agent_spec_from_decision(next_decision), +) +commit_execution_decision(store, task, role, next_decision) +``` + + `execution_target_policy.py`에 initial matrix와 분리된 canonical cloud promotion chain을 선언하고 selector가 qualified failure, used target, no-bounce를 검증하는 `promotion` transition을 반환하게 한다. `agent_spec_from_decision`은 policy-owned promotion target만 허용한다. live selector-backed path는 context package 검증 후 decision/history를 commit하고 다음 invocation을 시작한다. `promoted_spec`은 필요한 legacy locator recovery에만 남긴다. +- 수정 파일 및 체크리스트: + - [ ] `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/execution_target_policy.py`: canonical promotion target/chain. + - [ ] `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/select_execution_target.py`: promotion transition schema와 validation. + - [ ] `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py`: live promotion context/commit orchestration. + - [ ] `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_execution_target_policy.py`: promotion matrix. + - [ ] `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_select_execution_target.py`: qualified promotion, used/no-bounce와 schema tamper. + - [ ] `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py`: actual invocation, decision/history, context evidence, shared budget integration. +- 테스트 작성: 기존 cloud G07 promotion test를 보강해 Claude→Codex target, `transition.trigger`, selected target, history, PLAN/locator/raw/normalized/workspace prompt와 stage budget count/reset을 assert한다. AGY→Claude→Codex와 unqualified failure same-target/no-bounce case를 table-driven으로 추가한다. +- 중간 검증: `python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py DispatcherCanonicalFailoverIntegrationTest DynamicFailoverBudgetTest -v`가 PASS해야 한다. + +## 의존 관계 및 구현 순서 + +1. `REVIEW_REFACTOR-1`로 runtime quota evidence를 신뢰할 수 있게 만든다. +2. `REVIEW_REFACTOR-2`가 그 candidate/transition identity를 사용해 cloud promotion state를 commit한다. +3. 전체 회귀로 legacy recovery와 local failover가 유지되는지 확인한다. + +## 수정 파일 요약 + +| 파일 | 항목 | +|---|---| +| `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/execution_target_policy.py` | REVIEW_REFACTOR-2 | +| `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/select_execution_target.py` | REVIEW_REFACTOR-1, REVIEW_REFACTOR-2 | +| `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py` | REVIEW_REFACTOR-2 | +| `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_execution_target_policy.py` | REVIEW_REFACTOR-2 | +| `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_select_execution_target.py` | REVIEW_REFACTOR-1, REVIEW_REFACTOR-2 | +| `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py` | REVIEW_REFACTOR-2 | + +## 최종 검증 + +- `go test -count=1 ./apps/node/cmd/node -run 'QuotaProbe'` — actual producer contract PASS; fresh run required. +- `python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_select_execution_target.py SelectorFailoverContractTests SelectorQuotaRepresentationTests -v` — producer shape, tri-state와 selected quota PASS. +- `python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py DispatcherCanonicalFailoverIntegrationTest DynamicFailoverBudgetTest -v` — promotion state/context/budget PASS. +- `python3 -m unittest discover -s agent-ops/skills/project/orchestrate-agent-task-loop/tests -p 'test_*.py'` — fresh-process 전체 suite PASS. +- `python3 -m py_compile agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py 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/tests/test_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` — exit 0. +- `git diff --check` — 출력 없음. + +모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. diff --git a/agent-task/archive/2026/07/m-agent-task-runtime-target-selector/06+05_runtime_quota_failover/plan_local_G05_3.log b/agent-task/archive/2026/07/m-agent-task-runtime-target-selector/06+05_runtime_quota_failover/plan_local_G05_3.log new file mode 100644 index 0000000..0dd3373 --- /dev/null +++ b/agent-task/archive/2026/07/m-agent-task-runtime-target-selector/06+05_runtime_quota_failover/plan_local_G05_3.log @@ -0,0 +1,127 @@ + + +# Plan - no_promotion_target 연속 복구 backoff 진행 + +## 이 파일을 읽는 구현 에이전트에게 + +구현과 검증을 끝낸 뒤 `CODE_REVIEW-cloud-G05.md`의 구현 에이전트 소유 섹션을 실제 내용과 원문 출력으로 채운다. active 파일은 그대로 두고 review 준비 완료만 보고한다. 종결은 code-review skill 전용이다. 차단되면 정확한 원인, 시도한 명령/출력, 재개 조건만 구현 에이전트 소유 evidence에 기록한다. 사용자에게 질문하거나 user-input 도구를 호출하지 않고, control-plane stop file 생성, 다음 상태 분류, archive/log 이동, `complete.log` 또는 roadmap 수정은 하지 않는다. + +## 배경 + +직전 구현은 selector-backed worker가 canonical `no_promotion_target` 뒤 legacy `promoted_spec()`으로 내려가는 결함을 제거해 actual invocation과 persisted decision/history를 같은 target에 유지했다. 그러나 이 새 same-target recovery 분기는 sleep 지수에 `terminal_recovery_retries`를 사용하면서 counter를 증가시키지 않는다. 연속 provider failure에서 shared failure budget은 진행하지만 delay는 매번 1초에 머무르므로, 다른 terminal recovery 분기와 같은 counter 전이를 적용하고 연속 실패 회귀로 고정한다. + +## Archive Evidence Snapshot + +- Prior task: `agent-task/m-agent-task-runtime-target-selector/06+05_runtime_quota_failover` +- Archived plan: `agent-task/m-agent-task-runtime-target-selector/06+05_runtime_quota_failover/plan_local_G06_2.log` +- Archived review: `agent-task/m-agent-task-runtime-target-selector/06+05_runtime_quota_failover/code_review_cloud_G06_2.log` +- Verdict: `WARN`; Required 0, Suggested 1, Nit 0. +- Suggested summary: selector-backed `no_promotion_target` 분기가 `terminal_recovery_retries`를 증가시키지 않아 연속 same-target recovery의 exponential backoff가 `[1, 1]`로 고정된다. +- Affected files: `dispatch.py`, `test_dispatch.py`. +- Verification evidence: focused dispatcher 31 tests와 전체 Python 212 tests, `py_compile`, `git diff --check`는 PASS했다. 별도 deterministic local-G08 연속 `provider-connection` 재현은 invocation `agy -> agy -> agy`, selected/history 정합성과 성공 후 budget reset을 확인했지만 sleep이 `[1, 1]`임을 확인했다. +- Roadmap carryover: 이 pair에는 `Roadmap Targets`가 없으므로 PASS 시 Milestone Task 자동 체크를 주장하지 않는다. + +## 분석 결과 + +### 읽은 파일 + +- `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py` +- `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py` +- `agent-test/local/rules.md` +- `agent-test/local/testing-smoke.md` +- `agent-roadmap/phase/automation-runtime-bridge/PHASE.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` + +### SDD 기준 + +- SDD는 `[승인됨]`, 잠금은 `해제`다. +- S06은 허용되지 않은 target 전환 없이 현재 target 복구를 유지하도록 하고, S07은 stage 단위 누적 failure budget과 성공 reset을 요구하며, S09는 actual invocation과 persisted decision/history 정합성을 요구한다. +- 이번 변경은 target 선택이나 promotion 권한을 바꾸지 않고, 이미 canonical no-target으로 판정된 같은 target 복구의 시간 간격만 기존 terminal recovery 규칙과 일치시킨다. + +### 테스트 환경 규칙 + +- `test_env=local`; `agent-test/local/rules.md`와 `agent-test/local/testing-smoke.md`를 확인했다. +- testing-smoke의 live provider/full-cycle 명령은 hidden dispatcher retry counter를 결정적으로 관찰하지 못하므로 pass criterion으로 사용하지 않는다. +- 외부 provider나 secret 없이 mocked invocation과 `AsyncMock` sleep으로 실제 target, persisted state, delay sequence, stage budget reset을 함께 검증한다. + +### 재현 및 테스트 공백 + +- `dispatch.py:3355-3373`은 `no_promotion_target`에서 `terminal_recovery_retries`를 증가시키지 않은 채 `2 ** min(terminal_recovery_retries, 5)`를 계산한다. +- 다른 terminal same-target 분기인 `dispatch.py:3324-3338`과 legacy no-next-target 분기인 `dispatch.py:3424-3437`은 counter 증가 뒤 sleep하므로 첫 두 delay가 2초, 4초로 진행한다. +- 현재 `test_no_promotion_target_keeps_same_target_and_persists_state`는 한 번 실패 후 성공하여 같은 target/state만 검증하고 연속 retry cadence를 관찰하지 않는다. +- 두 번 실패 후 성공하는 deterministic 재현은 호출과 state/budget 정합성은 유지하지만 sleep `[1, 1]`을 반환한다. + +### 심볼 참조 + +- `terminal_recovery_retries`는 `dispatch.py:3013`에서 초기화되고 일반 terminal recovery와 legacy no-next-target 경로에서는 각 sleep 전에 증가한다. +- 결함 call-site는 `dispatch.py:3371`, 대응 회귀는 `test_dispatch.py:6046`이다. +- public symbol, selector policy, persistence schema, dependency 변경은 없다. + +### 분할 판단 + +- 기존 split child `06+05_runtime_quota_failover`의 WARN follow-up이며 선행 `05+04_dispatch_route_source`는 `agent-task/archive/2026/07/m-agent-task-runtime-target-selector/05+04_dispatch_route_source/complete.log`로 충족됐다. +- 한 counter 전이와 그 전이를 관찰하는 단일 회귀가 같은 recovery invariant를 공유하므로 nested subtask로 나누지 않는다. +- sibling active artifact와 dirty worktree는 사용자 소유로 보존하고 아래 두 파일 외 변경을 수정하지 않는다. + +### 범위 결정 근거 + +- selector target matrix, quota snapshot, logical context, persisted decision schema, roadmap와 SDD는 변경하지 않는다. +- agent-spec index에는 dispatcher runtime에 매칭되는 living spec이 없고 agent-contract index에도 hidden selector runtime 계약이 없다. 승인 SDD, 현재 코드, 직전 review finding과 deterministic 재현을 source of truth로 사용한다. + +### 최종 라우팅 + +- evaluation_mode: `isolated-reassessment`; finalizer=`finalize-task-policy.sh`, finalizer_mode=`pair`. +- build closures: `scope_closed=true`(누락된 counter 증가와 delay 성공 조건이 고정), `context_closed=true`(runtime 분기와 회귀 test를 함께 처리), `verification_closed=true`(delay sequence와 focused/full tests로 판정), `evidence_trusted=true`(실제 branch를 실행한 deterministic 재현), `ownership_closed=true`(현재 split child와 두 파일로 소유권 고정), `decision_closed=true`(승인 SDD와 기존 terminal retry 규칙으로 결정 가능). +- review closures: 같은 source/state/reproduction/verification evidence로 모두 `true`. +- build route_basis=`local-fit`; capability_gap=`none`; scores(scope_coupling/state_concurrency/blast_irreversibility/evidence_diagnosis/verification_complexity)=`1/1/1/1/1`, final=`local-G05`, filename=`PLAN-local-G05.md`. +- review route_basis=`official-review`; capability_gap=`none`; scores=`1/1/1/1/1`, final=`cloud-G05`, filename=`CODE_REVIEW-cloud-G05.md`; target=`codex/gpt-5.6-sol xhigh`. +- loop-risk observations: ordered states는 failure record → counter 증가/backoff → same-target retry → success budget reset이며 adverse path는 반복 실패 후 shared budget block이다. dispatcher retry cadence와 persisted budget/state는 두 consumer가 동일 failure sequence를 해석하므로 `temporal_state`, `boundary_contract`가 matched되고 triggered=`true`다. concurrent actor는 dispatcher 1개이고 structured parser나 조합 variant 변경은 없다. +- G floor: `none`; 이전 lane/G와 반복 횟수는 입력과 판정에 사용하지 않았다. + +## 구현 체크리스트 + +- [x] selector-backed `no_promotion_target` 분기에서 sleep 전에 `terminal_recovery_retries`를 증가시켜 기존 terminal recovery와 같은 bounded exponential backoff를 적용한다. +- [x] local-G08 회귀를 최소 두 번 연속 `provider-connection` 뒤 성공하도록 확장하고 같은 AGY target, unchanged selected/history, delay `[2, 4]`, 성공 후 stage failure budget reset을 검증한다. +- [x] focused dispatcher tests, 전체 Python suite, `py_compile`, `git diff --check`를 fresh output으로 검증한다. +- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. + +## 구현 항목 + +### [REVIEW_REFACTOR-1] Canonical no-target recovery counter 진행 + +- 문제: canonical `no_promotion_target` 분기는 shared stage failure budget을 증가시키고 같은 target으로 복구하지만 local `terminal_recovery_retries`는 0에 고정되어 모든 sleep이 1초다. +- 해결 방법: + - `no_promotion_target` explicit branch 안에서 banner/sleep 전에 `terminal_recovery_retries += 1`을 적용한다. + - delay 식과 최대 30초 cap, `previous_locator`, selector decision/history, shared failure budget과 success reset 로직은 변경하지 않는다. + - legacy `promoted_spec()` 경계와 다른 failure branch는 수정하지 않는다. +- 수정 파일 및 체크리스트: + - [x] `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py`: canonical no-target same-target recovery counter를 진행한다. + - [x] `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py`: 연속 no-target failure의 target/state/backoff/budget reset 회귀를 추가한다. +- 테스트 작성: + - existing local-G08 test의 mock invocation이 `provider-connection`, `provider-connection`, success를 반환하게 하고 세 호출 모두 AGY Gemini Medium인지 assert한다. + - patched `asyncio.sleep.await_args_list`가 `[2, 4]`인지 assert한다. + - 성공 뒤 `execution_decisions.worker.selected`, `route_transition_history`, `blocked`, `recovery_failures`, `StageFailureBudget.count()`가 성공 상태와 일치하는지 assert한다. +- 중간 검증: `python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py DispatcherCanonicalFailoverIntegrationTest DynamicFailoverBudgetTest ReviewRetryTest -v`가 PASS해야 한다. + +## 의존 관계 및 구현 순서 + +1. `no_promotion_target` same-target recovery counter를 기존 terminal branch와 같은 순서로 증가시킨다. +2. 연속 실패 회귀를 확장해 delay와 state/budget invariants를 함께 고정한다. +3. focused/full 회귀와 정적 검증을 실행하고 review evidence를 채운다. + +## 수정 파일 요약 + +| 파일 | 항목 | +|---|---| +| `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py` | REVIEW_REFACTOR-1 | +| `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py` | REVIEW_REFACTOR-1 | + +## 최종 검증 + +- `python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py DispatcherCanonicalFailoverIntegrationTest DynamicFailoverBudgetTest ReviewRetryTest -v` — canonical no-target, promotion/context/budget, legacy recovery와 연속 backoff 회귀 PASS. +- `python3 -m unittest discover -s agent-ops/skills/project/orchestrate-agent-task-loop/tests -p 'test_*.py'` — fresh-process 전체 suite PASS. +- `python3 -m py_compile agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py` — exit 0. +- `git diff --check` — 출력 없음. + +모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. diff --git a/agent-task/archive/2026/07/m-agent-task-runtime-target-selector/06+05_runtime_quota_failover/plan_local_G06_2.log b/agent-task/archive/2026/07/m-agent-task-runtime-target-selector/06+05_runtime_quota_failover/plan_local_G06_2.log new file mode 100644 index 0000000..1223feb --- /dev/null +++ b/agent-task/archive/2026/07/m-agent-task-runtime-target-selector/06+05_runtime_quota_failover/plan_local_G06_2.log @@ -0,0 +1,149 @@ + + +# Plan - Selector-backed promotion의 legacy fallthrough 제거 + +## 이 파일을 읽는 구현 에이전트에게 + +구현과 검증을 끝낸 뒤 `CODE_REVIEW-cloud-G06.md`의 구현 에이전트 소유 섹션을 실제 내용과 원문 출력으로 채운다. active 파일은 그대로 두고 review 준비 완료만 보고한다. 종결은 code-review skill 전용이다. 차단되면 정확한 원인, 시도한 명령/출력, 재개 조건만 구현 에이전트 소유 evidence에 기록한다. 사용자에게 질문하거나 user-input 도구를 호출하지 않고, control-plane stop file 생성, 다음 상태 분류, archive/log 이동, `complete.log` 또는 roadmap 수정은 하지 않는다. + +## 배경 + +후속 리뷰에서 quota snapshot shape와 canonical cloud promotion의 정상 경로는 통과했지만, selector-backed worker가 canonical `promotion` 대상 없음(`no_promotion_target`)을 받으면 legacy `promoted_spec()`으로 fallthrough하는 결함이 확인됐다. 이 경로는 실제 invocation만 Claude로 바꾸고 persisted decision/history와 logical continuation context는 Gemini 상태에 남긴다. selector-backed live worker의 모든 promotion 결과를 canonical 정책 안에서 종결하고 legacy locator recovery와 명확히 분리한다. + +## Archive Evidence Snapshot + +- Prior task: `agent-task/m-agent-task-runtime-target-selector/06+05_runtime_quota_failover` +- Archived plan: `agent-task/m-agent-task-runtime-target-selector/06+05_runtime_quota_failover/plan_cloud_G09_1.log` +- Archived review: `agent-task/m-agent-task-runtime-target-selector/06+05_runtime_quota_failover/code_review_cloud_G09_1.log` +- Verdict: `FAIL`; Required 1, Suggested 0, Nit 0. +- Required summary: selector-backed worker가 canonical `no_promotion_target`을 삼킨 뒤 legacy `promoted_spec()`으로 승격해 실제 invocation과 persisted decision/history/logical context가 분리된다. +- Affected files: `dispatch.py`, `test_dispatch.py`. +- Verification evidence: Go quota test, selector focused 19 tests, dispatcher focused 12 tests, 전체 Python 209 tests와 `py_compile`은 PASS했지만 deterministic local-G08 daytime `provider-connection` 재현에서 호출은 `agy -> claude`, persisted selected는 Gemini, history는 `initial`, continuation prompt에는 logical context artifact가 없는 불일치를 확인했다. +- Roadmap carryover: 이 pair에는 `Roadmap Targets`가 없으므로 PASS 시 Milestone Task 자동 체크를 주장하지 않는다. + +## 분석 결과 + +### 읽은 파일 + +- `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py` +- `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py` +- `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/select_execution_target.py` +- `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/execution_target_policy.py` +- `agent-test/local/rules.md` +- `agent-test/local/testing-smoke.md` +- `agent-roadmap/phase/automation-runtime-bridge/PHASE.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` + +### SDD 기준 + +- SDD는 `[승인됨]`, 잠금은 `해제`다. +- S06은 허용된 qualified failover/promotion과 no-bounce를, S07은 stage 단위 누적 failure budget을, S09는 actual invocation과 persisted decision/history의 일치를 요구한다. +- canonical transition 대상이 없다는 selector 결과는 legacy cross-target 승격 권한이 아니며, 현재 target의 recovery budget 진행 또는 terminal block으로만 이어져야 한다. + +### 테스트 환경 규칙 + +- `test_env=local`; `agent-test/local/rules.md`와 `agent-test/local/testing-smoke.md`를 읽었다. +- testing-smoke의 Edge-Node full-cycle/live provider 명령은 agent-ops Python dispatcher의 hidden selector 분기를 직접 검증하지 않아 pass criterion으로 사용하지 않는다. +- 외부 provider, secret, service 없이 deterministic mocked invocation과 fresh-process Python tests로 실제 호출 spec, state, context, budget을 함께 검증한다. + +### 재현 및 테스트 커버리지 공백 + +- `dispatch.py:3340-3406`의 selector-backed worker guard는 canonical promotion을 시도하지만 `no_promotion_target`만 삼키고 공통 legacy `promoted_spec()` 분기로 내려간다. +- local-G08 daytime Gemini decision에서 첫 invocation이 `provider-connection`을 반환하고 두 번째가 성공하도록 구성하면 실제 spec은 `agy -> claude`가 되지만 `execution_decisions.worker.selected`와 `route_transition_history`는 초기 Gemini 상태에 머문다. +- 기존 `test_generic_failure_stays_on_same_target`은 `generic-error`만 다루고, `provider-connection`이 `CLOUD_PROMOTION_FAILURES`를 거쳐 canonical no-target 뒤 legacy 승격하는 분기를 실행하지 않는다. +- 기존 cloud promotion tests는 canonical target이 존재하는 정상 전이만 다루며 chain exhaustion 뒤 legacy helper에 도달하지 않는다는 assertion이 없다. + +### 심볼 참조 + +- `promoted_spec()` 참조는 `dispatch.py:1553`, `1678`, `3067`, `3406`, `4693`에 있다. +- 현재 결함은 live `run_escalating()`의 `dispatch.py:3406` call-site다. legacy locator reclassification/recovery와 dry-run recovery call-site는 유지하고 별도 근거 없이 제거하지 않는다. +- public symbol rename이나 새 dependency는 없다. + +### 분할 판단 + +- 기존 split child `06+05_runtime_quota_failover`의 FAIL follow-up이며 선행 `05+04_dispatch_route_source`는 `agent-task/archive/2026/07/m-agent-task-runtime-target-selector/05+04_dispatch_route_source/complete.log`로 충족됐다. +- 한 dispatcher 분기와 대응 회귀 테스트가 하나의 invocation/state invariant를 공유하므로 nested subtask로 나누지 않는다. +- sibling active artifact와 dirty worktree는 사용자 소유로 보존하고 아래 명시 파일 외 변경을 수정하지 않는다. + +### 범위 결정 근거 + +- quota producer/consumer, canonical policy matrix, target admission, selfcheck, roadmap 문서는 이번 결함 범위에서 변경하지 않는다. +- agent-spec index에는 agent-task dispatcher에 매칭되는 living spec이 없고 agent-contract index에도 hidden selector runtime 계약 문서가 없다. 승인 SDD, 현재 코드, 재현과 tests를 source of truth로 사용한다. + +### 최종 라우팅 + +- evaluation_mode: `isolated-reassessment`; finalizer=`finalize-task-policy.sh`, finalizer_mode=`pair`. +- build closures: `scope_closed=true`(legacy fallthrough call-site와 성공 조건 고정), `context_closed=true`(dispatcher와 회귀 test를 함께 처리), `verification_closed=true`(deterministic reproducer와 focused/full tests), `evidence_trusted=true`(실제 분기를 실행한 재현으로 불일치 확인), `ownership_closed=true`(현재 split child와 legacy recovery 경계 고정), `decision_closed=true`(승인 SDD와 기존 failure policy로 결정 가능). +- review closures: 같은 source/state/reproduction/verification evidence로 모두 `true`. +- build route_basis=`local-fit`; capability_gap=`none`; scores(scope_coupling/state_concurrency/blast_irreversibility/evidence_diagnosis/verification_complexity)=`1/1/1/2/1`, final=`local-G06`, filename=`PLAN-local-G06.md`. +- review route_basis=`official-review`; capability_gap=`none`; scores=`1/1/1/2/1`, final=`cloud-G06`, filename=`CODE_REVIEW-cloud-G06.md`; target=`codex/gpt-5.6-sol xhigh`. +- loop-risk observations: ordered transition은 canonical promotion 성공/no-target recovery/budget exhaustion/block을 포함하고 adverse path는 `provider-connection`과 promotion-chain exhaustion이다. dispatcher-selector-StateStore boundary는 actual invocation, persisted decision/history, prompt, budget의 복수 consumer 정합성을 가진다. failure class와 route/promotion position의 2개 독립 variant 축을 조합 검증해야 한다. matched signatures=`temporal_state`, `boundary_contract`, `variant_product`; triggered=`true`. concurrent actor는 dispatcher 1개이고 structured parser 변경은 없다. signature는 lane/G에 영향을 주지 않았다. +- G floor: `none`; 이전 lane/G와 반복 횟수는 입력과 판정에 사용하지 않았다. + +## 구현 체크리스트 + +- [x] selector-backed live worker가 canonical `no_promotion_target` 뒤 legacy `promoted_spec()`으로 fallthrough하지 않게 하고, 동일 target recovery/budget exhaustion 또는 canonical block으로 종결하며 legacy recovery call-site를 보존한다. +- [x] local-G08 `provider-connection`과 canonical promotion chain exhaustion 회귀를 추가해 invocation target, persisted decision/history, logical context, stage failure budget 정합성을 검증한다. +- [x] focused dispatcher tests, 전체 Python suite, `py_compile`, `git diff --check`를 fresh output으로 검증한다. +- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. + +## 구현 항목 + +### [REVIEW_REFACTOR-1] Selector-backed promotion의 legacy fallback 차단 + +- 문제: `run_escalating()`은 selector-backed worker의 canonical promotion이 `no_promotion_target`을 반환할 때 이를 비오류처럼 삼킨 뒤 같은 함수의 legacy `promoted_spec(spec, codex_recovery_count)`로 내려간다. local multi-candidate route의 unqualified `provider-connection`은 canonical promotion 대상이 없지만 legacy AGY→Claude 승격이 실행되어 상태와 호출이 분리된다. +- 해결 방법: + +```python +# Before: selector가 no target을 반환해도 아래 legacy 분기로 fallthrough +except ExecutionDecisionError as exc: + if "no_promotion_target" not in str(exc): + ... +next_spec = promoted_spec(spec, codex_recovery_count) +``` + +```python +# After: selector-backed worker가 promotion branch 전체를 소유 +except ExecutionDecisionError as exc: + if "no_promotion_target" in str(exc): + # current spec을 유지하며 shared stage budget의 recovery/block 규칙 적용 + ... + continue + block_selector_error(...) + +# legacy selector state가 없는 검증된 recovery path에서만 사용 +next_spec = promoted_spec(spec, codex_recovery_count) +``` + + selector-backed worker 여부를 한 번 판정해 canonical promotion 성공 시 context 검증→decision/history commit→다음 invocation 순서를 유지한다. canonical 대상 없음은 실제 `spec`, persisted selected/history와 prompt context를 바꾸지 않고 현재 target recovery로 진행하며 이미 증가한 shared stage failure budget이 limit에 도달하면 block한다. malformed/tampered selector 오류는 기존처럼 fail closed한다. legacy terminal reclassification/restart recovery와 selector state가 없는 역할의 기존 호출은 동작 보존한다. +- 수정 파일 및 체크리스트: + - [x] `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py`: selector-backed live promotion을 terminal branch로 만들고 legacy fallback 경계를 명시한다. + - [x] `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py`: `provider-connection` same-target recovery, promotion-chain exhaustion, persisted state/context/budget 및 legacy recovery 보존 회귀. +- 테스트 작성: + - local-G08 daytime initial Gemini에서 첫 `provider-connection`, 다음 성공을 반환하고 호출이 `agy -> agy`인지, selected/history가 불필요하게 바뀌지 않는지, shared budget이 성공 후 reset되는지 assert한다. + - canonical cloud chain의 마지막 target에서 qualified failure가 발생할 때 legacy target으로 이동하지 않고 같은 target recovery 또는 limit block으로 끝나는지 assert한다. + - canonical promotion 대상이 있는 Claude→Terra/AGY→Claude→Terra logical context와 commit ordering, 검증된 legacy reclassification tests가 계속 PASS하는지 확인한다. +- 중간 검증: `python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py DispatcherCanonicalFailoverIntegrationTest DynamicFailoverBudgetTest ReviewRetryTest -v`가 PASS해야 한다. + +## 의존 관계 및 구현 순서 + +1. `run_escalating()`의 selector-owned/legacy promotion 경계를 분리한다. +2. 누락 분기와 legacy 보존 회귀를 추가한다. +3. focused/full 회귀와 정적 검증을 실행하고 review evidence를 채운다. + +## 수정 파일 요약 + +| 파일 | 항목 | +|---|---| +| `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py` | REVIEW_REFACTOR-1 | +| `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py` | REVIEW_REFACTOR-1 | + +## 최종 검증 + +- `python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py DispatcherCanonicalFailoverIntegrationTest DynamicFailoverBudgetTest ReviewRetryTest -v` — canonical no-target, promotion/context/budget와 legacy recovery 회귀 PASS. +- `python3 -m unittest discover -s agent-ops/skills/project/orchestrate-agent-task-loop/tests -p 'test_*.py'` — fresh-process 전체 suite PASS. +- `python3 -m py_compile agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py` — exit 0. +- `git diff --check` — 출력 없음. + +모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. diff --git a/agent-task/archive/2026/07/m-agent-task-runtime-target-selector/06+05_runtime_quota_failover/plan_local_G08_0.log b/agent-task/archive/2026/07/m-agent-task-runtime-target-selector/06+05_runtime_quota_failover/plan_local_G08_0.log new file mode 100644 index 0000000..50a343c --- /dev/null +++ b/agent-task/archive/2026/07/m-agent-task-runtime-target-selector/06+05_runtime_quota_failover/plan_local_G08_0.log @@ -0,0 +1,135 @@ + + +# Plan - Runtime quota probe와 failover state machine + +## 이 파일을 읽는 구현 에이전트에게 + +선행 `05+04_dispatch_route_source`의 `complete.log`가 확인된 뒤 구현한다. 구현과 검증을 끝낸 뒤 `CODE_REVIEW-cloud-G09.md`의 구현 에이전트 소유 섹션을 실제 내용과 원문 출력으로 채운다. active 파일은 그대로 두고 review 준비 완료만 보고한다. 종결은 code-review skill 전용이다. 차단되면 정확한 원인, 시도한 명령/출력, 재개 조건만 구현 에이전트 소유 evidence에 기록한다. 사용자에게 질문하거나 user-input 도구를 호출하지 않고, control-plane stop file 생성, 다음 상태 분류, archive/log 이동, `complete.log` 또는 roadmap 수정은 하지 않는다. + +## 배경 + +persisted route source가 실제 invocation에 연결된 뒤 bounded candidate quota를 runtime에서 관측하고, resume과 허용된 failover만 동일 state transition으로 갱신한다. + +## 분석 결과 + +### 읽은 파일 + +- `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` + +### SDD 기준 + +- 대상 Acceptance Scenario는 S05~S07과 S09다. 이 child는 quota observation, pinned resume, qualified failover와 transition history 범위를 소유한다. + +### 테스트 환경 규칙 + +- `test_env=local`; `agent-test/local/rules.md`가 존재하며 끝까지 읽었다. +- agent-ops Python dispatcher에 매칭되는 `agent-test/local/*.md` profile은 없다. 현재 checkout의 Python test manifest를 fallback 근거로 삼아 fresh-process `unittest`, `py_compile`, `git diff --check`를 사용하며 test-rule 유지보수는 필요하지 않다. +- provider subprocess는 fake runner와 임시 workspace/state/locator로 대체하고 실제 cloud/local 호출은 하지 않는다. +- 계획 시점 전체 Python suite 기준선은 169 tests PASS다. + +### 테스트 커버리지 공백 + +- selector에 `iop-node quota-probe` runtime 호출부와 target/profile required-cap argv 매핑이 없다. +- dispatcher recovery가 selector transition history와 runtime quota를 하나의 state machine으로 갱신하지 않는다. +- initial/resume/failover invocation target과 persisted history 일치 검증이 없다. + +### 심볼 참조 + +- `select_execution_target.py:485-491`의 source-only probe 기록과 `dispatch.py:2690-2987` recovery/promotion call-site를 runtime probe와 persisted transition 소비자로 갱신한다. + +### 분할 판단 + +- 원본 `05+04_dispatch_integration`의 두 번째 child다. +- `05+04_dispatch_route_source`가 producer이며, 이 child의 persisted transition 결과를 `07+06_context_review_recovery`가 소비한다. + +### 범위 결정 근거 + +- canonical target별 quota probe spec, no-shell probe normalization, resume/failover/promotion state transition만 변경한다. +- context package, retry-blocked budget lifecycle, review recovery/audit output은 다음 child 범위다. + +### 최종 라우팅 + +- evaluation_mode: `isolated-reassessment` +- finalizer=`finalize-task-policy.sh`, finalizer_mode=`pair`; build/review status=`routed`. +- build closures: `scope_closed=true`(승인 SDD와 이 subtask 경계가 고정됨), `context_closed=true`(명시된 source/test 범위를 한 local 작업에서 유지 가능), `verification_closed=true`, `evidence_trusted=true`, `ownership_closed=true`, `decision_closed=true`. +- review closures: `scope_closed=true`, `context_closed=true`(동일 source/test 및 구현 evidence로 판정 가능), `verification_closed=true`, `evidence_trusted=true`, `ownership_closed=true`, `decision_closed=true`. +- build route_basis=`local-fit`; capability_gap=`none`. +- build loop-risk audit: matched signatures=`temporal_state`, `boundary_contract`, `variant_product`; 이 기록은 lane/G를 바꾸지 않는다. +- build scores(scope_coupling/state_concurrency/blast_irreversibility/evidence_diagnosis/verification_complexity)=`2/2/1/1/2`, final=`local-G08`, filename=`PLAN-local-G08.md`. +- review route_basis=`official-review`; official execution=`codex/gpt-5.6-sol xhigh`; capability_gap=`none`. +- review scores(scope_coupling/state_concurrency/blast_irreversibility/evidence_diagnosis/verification_complexity)=`2/2/1/2/2`, final=`cloud-G09`, filename=`CODE_REVIEW-cloud-G09.md`. + +## 구현 체크리스트 + +- [ ] bounded candidate의 runtime quota probe가 target/profile required-cap argv로 실행되고 probe 오류는 `unknown` snapshot으로 decision에 남는다. +- [ ] resume은 시간·본문 변경에도 pinned target을 사용하고, 확정된 provider target 불가 또는 명시적 retry eligibility만 transition history와 invocation target을 변경한다. +- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. + +## 구현 항목 + +### [REFACTOR-2] Runtime quota probe와 resume/failover state machine + +- 문제: `select_execution_target.py:485-491`은 probe command를 source 문자열로만 기록하고 실제 호출하지 않으며, `dispatch.py:2690-2987` recovery는 selector transition history와 runtime quota를 하나의 state machine으로 갱신하지 않는다. +- 해결 방법: + +```python +# Before: agent-ops/skills/project/orchestrate-agent-task-loop/scripts/select_execution_target.py:485 +"source": quota_probe_command, +``` + +```python +# After +snapshot = probe_candidate_quota( + target=candidate.target, + adapter=candidate.adapter, + required_caps=quota_probe_spec(candidate).required_caps, + checked_at=evaluated_at, +) +``` + + - bounded candidate를 실제 평가할 때 shell 없이 `iop-node quota-probe --target --command --required-cap ... --checked-at `를 실행한다. policy-owned mapping은 Gemini 3.6 Flash Low/Medium/High에 `overall`과 해당 `model:` cap을, Claude/Codex에 `overall`을 선언한다. local model candidate는 probe하지 않고 오류/parse failure는 같은 target의 `unknown` snapshot으로 정규화한다. + - persisted prior decision을 `resume`에 전달한다. `local-G07~G08`은 Gemini 3.6 Flash (Medium) 단일 target이므로 Laguna failover를 평가하지 않고 confirmed exhausted 또는 provider target 불가를 task-local blocker로 기록한다. 그 밖의 process/generic 오류와 dispatcher state/work-log/control/user-review/cancel 오류는 같은 target recovery로 처리한다. + - 기타 cloud worker의 기존 `agy → Claude → Codex` promotion은 보존하면서 normalized transition/history와 공통 stage budget에 기록한다. + - selector 반환, budget increment, used-candidate/history append, 다음 invocation 준비를 동일 task-state update 안에서 처리한다. unknown candidate의 confirmed runtime `provider-quota`는 해당 work unit의 derived evidence만 exhausted로 바꾸고, generic stderr는 unknown을 유지한다. invalid/no-eligible와 confirmed exhausted는 task-local blocker다. +- 수정 파일 및 체크리스트: + - [ ] `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/execution_target_policy.py`: canonical target별 command/profile/required-cap probe spec. + - [ ] `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/select_execution_target.py`: no-shell subprocess/normalization과 injectable snapshot 경계. + - [ ] `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py`: resume/failover/promotion orchestration과 task-local terminal state. + - [ ] `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_execution_target_policy.py`: target별 probe spec matrix. + - [ ] `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_select_execution_target.py`: probe argv, parse/error→unknown과 bounded/local matrix. + - [ ] `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py`: restart/time/failure-class/failover/promotion integration. +- 테스트 작성: `SelectorDispatcherIntegrationTest`에서 day→night pin, body edit, new generation, qualified provider target 불가 failover, generic/Pi same-target recovery, excluded control error, available/unknown 1회 Gemini, confirmed exhausted blocker, 기존 cloud promotion을 검증한다. selector test에는 fake probe argv와 snapshot을 검증한다. +- 중간 검증: `python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py SelectorDispatcherIntegrationTest -v`와 `python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_select_execution_target.py -v`가 PASS해야 한다. + +## 의존 관계 및 구현 순서 + +1. `05+04_dispatch_route_source`의 `complete.log`가 PASS여야 한다. +2. 이 task의 transition/history 결과를 `07+06_context_review_recovery`가 소비한다. + +## 수정 파일 요약 + +| 파일 | 항목 | +|---|---| +| `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/execution_target_policy.py` | REFACTOR-2 | +| `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/select_execution_target.py` | REFACTOR-2 | +| `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py` | REFACTOR-2 | +| `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_execution_target_policy.py` | REFACTOR-2 | +| `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_select_execution_target.py` | REFACTOR-2 | +| `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py` | REFACTOR-2 | + +## 최종 검증 + +- `python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py SelectorDispatcherIntegrationTest -v` — resume/failover/promotion 회귀 PASS. +- `python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_select_execution_target.py -v` — runtime probe와 selector 전체 회귀 PASS. +- `python3 -m unittest discover -s agent-ops/skills/project/orchestrate-agent-task-loop/tests -p 'test_*.py'` — fresh process 전체 suite PASS. +- `python3 -m py_compile agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py 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/tests/test_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` — exit 0. +- `git diff --check` — 출력 없음. + +모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. + diff --git a/agent-task/m-agent-task-runtime-target-selector/07+06_context_review_recovery/CODE_REVIEW-cloud-G09.md b/agent-task/m-agent-task-runtime-target-selector/07+06_context_review_recovery/CODE_REVIEW-cloud-G09.md new file mode 100644 index 0000000..a83781b --- /dev/null +++ b/agent-task/m-agent-task-runtime-target-selector/07+06_context_review_recovery/CODE_REVIEW-cloud-G09.md @@ -0,0 +1,183 @@ + + +# Code Review Reference - REFACTOR + +> **[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-25 +task=m-agent-task-runtime-target-selector/07+06_context_review_recovery, plan=0, tag=REFACTOR + +## 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: + - `route-pin`: 작업 세대별 최초 route 고정과 재시작 복원 + - `context-failover`: 단방향 adapter failover와 논리적 context 이전 + - `failure-budget`: target 전환 전후 동일 stage 실패 예산 공유 + - `dispatch-integration`: persisted selector decision 기반 최초 실행·resume·failover +- Completion mode: check-on-pass + +## 이 파일을 읽는 리뷰 에이전트에게 + +> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다. + +각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요. +리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다. + +1. 판정을 append한다. +2. `CODE_REVIEW-cloud-G09.md` → `code_review_cloud_G09_0.log`, `PLAN-local-G08.md` → `plan_local_G08_0.log`로 아카이브한다. +3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-agent-task-runtime-target-selector/07+06_context_review_recovery/`로 이동한다. WARN/FAIL이면 code-review skill이 요구하는 다음 filesystem state를 완전히 작성한다. +4. PASS이고 task group이 `m-agent-task-runtime-target-selector`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다. +5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다. + +--- + +## 구현 항목별 완료 여부 + +| 항목 | 완료 여부 | +|------|---------| +| REFACTOR-3 Context, budget과 retry-blocked 소비 | [x] | +| REFACTOR-4 Review recovery와 runtime audit evidence | [x] | + +## 구현 체크리스트 + +- [x] cross-adapter invocation이 persisted normalized-output context package를 소비하고 dynamic failover와 기존 cloud promotion 모두 같은 stage 실패 예산을 이어받는다. +- [x] dry-run은 read-only preview이고 retry-blocked는 blocker/budget만 초기화하며 review resume/finalization recovery는 pinned Codex와 독립 budget을 유지한다. +- [x] locator/state/status가 work unit, rule/candidate/reason, quota, transition과 stage budget을 추적하고 통합 테스트가 restart·blocker·독립 drain을 함께 검증한다. +- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. + +## 코드리뷰 전용 체크리스트 + +> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다. +> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다. + +- [ ] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다. +- [ ] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다. +- [ ] active `CODE_REVIEW-cloud-G09.md`를 `code_review_cloud_G09_0.log`로 아카이브한다. +- [ ] active `PLAN-local-G08.md`를 `plan_local_G08_0.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/07+06_context_review_recovery/`를 `agent-task/archive/YYYY/MM/m-agent-task-runtime-target-selector/07+06_context_review_recovery/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다. +- [ ] PASS이고 task group이 `m-agent-task-runtime-target-selector`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, 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`를 작성하지 않는다. + +## 계획 대비 변경 사항 + +없음 (계획된 REFACTOR-3, REFACTOR-4 구현 항목을 계획대로 완수함) + +## 주요 설계 결정 + +1. `continuation_prompt_from_package`를 구현하여 cross-adapter continuation 실행 시 저장된 normalized output context package를 소비하는 프롬프트를 생성을 단일화함. +2. `StageFailureBudget`을 `work_unit_id|stage` 키로 독립 관리하여 worker와 review 간 실패 예산 간섭을 방지하고, 성공 시 동일 stage 실패 예산을 초기화하도록 함. +3. `--retry-blocked` 옵션은 task의 `blocked` 및 stage failure budget만 초기화하고, 이전 decision, transition history, candidates는 보존함. +4. `read_or_preview_stage_decision` 및 `selector_evidence_lines`를 구현하여 review recovery 시 pinned Codex decision을 resume/합성하고, locator record, state `route_transition_history`, status lines, dry-run banner 전체에 일관된 selector audit evidence(`work_unit_id`, `rule_id`, `priority`, `transition`, `quota_status`, `candidates`, `reason_codes`)를 남기도록 함. + +## 리뷰어를 위한 체크포인트 + +- cross-adapter invocation이 persisted normalized-output context package를 소비하는가. +- dynamic failover와 기존 cloud promotion이 동일 stage 실패 예산을 이어받고 성공에서만 reset하는가. +- retry-blocked가 decision/history/used candidate를 보존하면서 blocker와 counter만 초기화하는가. +- review recovery와 locator/state/status/dry-run이 동일 work unit의 selector evidence를 가리키는가. + +## 검증 결과 + +### SelectorDispatcherIntegrationTest + +명령: `python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py SelectorDispatcherIntegrationTest -v` + +```text +test_context_budget_and_retry_blocked_lifecycle (__main__.SelectorDispatcherIntegrationTest.test_context_budget_and_retry_blocked_lifecycle) ... ok +test_dry_run_multiple_ready_tasks_isolation_and_statelessness (__main__.SelectorDispatcherIntegrationTest.test_dry_run_multiple_ready_tasks_isolation_and_statelessness) ... ok +test_dry_run_statelessness_initial_and_resume_previews (__main__.SelectorDispatcherIntegrationTest.test_dry_run_statelessness_initial_and_resume_previews) ... ok +test_qualified_failover_and_blocker_scenarios (__main__.SelectorDispatcherIntegrationTest.test_qualified_failover_and_blocker_scenarios) ... ok +test_resume_pins_target_across_time_and_body_changes_and_resets_on_new_generation (__main__.SelectorDispatcherIntegrationTest.test_resume_pins_target_across_time_and_body_changes_and_resets_on_new_generation) ... ok +test_review_recovery_and_runtime_audit_evidence (__main__.SelectorDispatcherIntegrationTest.test_review_recovery_and_runtime_audit_evidence) ... ok +test_worker_and_review_initial_invocation_uses_selector (__main__.SelectorDispatcherIntegrationTest.test_worker_and_review_initial_invocation_uses_selector) ... ok + +---------------------------------------------------------------------- +Ran 7 tests in 10.978s + +OK +Exit Code: 0 +``` + +### BlockerDrainTest + +명령: `python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py BlockerDrainTest -v` + +```text +test_external_active_task_returns_non_terminal_exit_three (__main__.BlockerDrainTest.test_external_active_task_returns_non_terminal_exit_three) ... ok +test_invalidated_complete_archive_cannot_end_with_success (__main__.BlockerDrainTest.test_invalidated_complete_archive_cannot_end_with_success) ... ok +test_review_preflight_failure_still_drains_independent_worker (__main__.BlockerDrainTest.test_review_preflight_failure_still_drains_independent_worker) ... ok +test_runtime_blocker_still_drains_independent_task (__main__.BlockerDrainTest.test_runtime_blocker_still_drains_independent_task) ... ok +test_unexpected_agent_exception_drains_sibling_then_returns_three (__main__.BlockerDrainTest.test_unexpected_agent_exception_drains_sibling_then_returns_three) ... ok +test_user_review_only_holds_its_dependency_closure (__main__.BlockerDrainTest.test_user_review_only_holds_its_dependency_closure) ... ok + +---------------------------------------------------------------------- +Ran 6 tests in 0.081s + +OK +Exit Code: 0 +``` + +### 전체 Python suite + +명령: `python3 -m unittest discover -s agent-ops/skills/project/orchestrate-agent-task-loop/tests -p 'test_*.py'` + +```text +Ran 171 tests in 15.340s + +OK +Exit Code: 0 +``` + +### py_compile + +명령: `python3 -m py_compile agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py 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/tests/test_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` + +```text +(no output) +Exit Code: 0 +``` + +### diff check + +명령: `git diff --check` + +```text +(no output) +Exit Code: 0 +``` + +--- + +> **[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 `[ ]` → `[x]` only | +| 구현 체크리스트 (item text/order) | Fixed at stub creation from plan | Implementing agent checks `[ ]` → `[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/agent-task/m-agent-task-runtime-target-selector/07+06_context_review_recovery/PLAN-local-G08.md b/agent-task/m-agent-task-runtime-target-selector/07+06_context_review_recovery/PLAN-local-G08.md new file mode 100644 index 0000000..6a73e67 --- /dev/null +++ b/agent-task/m-agent-task-runtime-target-selector/07+06_context_review_recovery/PLAN-local-G08.md @@ -0,0 +1,156 @@ + + +# Plan - Context·budget 소비와 review recovery closure + +## 이 파일을 읽는 구현 에이전트에게 + +선행 `06+05_runtime_quota_failover`의 `complete.log`가 확인된 뒤 구현한다. 구현과 검증을 끝낸 뒤 `CODE_REVIEW-cloud-G09.md`의 구현 에이전트 소유 섹션을 실제 내용과 원문 출력으로 채운다. active 파일은 그대로 두고 review 준비 완료만 보고한다. 종결은 code-review skill 전용이다. 차단되면 정확한 원인, 시도한 명령/출력, 재개 조건만 구현 에이전트 소유 evidence에 기록한다. 사용자에게 질문하거나 user-input 도구를 호출하지 않고, control-plane stop file 생성, 다음 상태 분류, archive/log 이동, `complete.log` 또는 roadmap 수정은 하지 않는다. + +## 배경 + +runtime transition을 실제 cross-adapter continuation, stage 실패 예산, retry-blocked, review recovery와 운영 audit evidence에 연결해 dispatcher 통합 범위를 닫는다. + +## 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: + - `route-pin`: 작업 세대별 최초 route 고정과 재시작 복원 + - `context-failover`: 단방향 adapter failover와 논리적 context 이전 + - `failure-budget`: target 전환 전후 동일 stage 실패 예산 공유 + - `dispatch-integration`: persisted selector decision 기반 최초 실행·resume·failover +- Completion mode: check-on-pass + +## 분석 결과 + +### 읽은 파일 + +- `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py` +- `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py` +- `agent-test/local/rules.md` +- `agent-roadmap/sdd/automation-runtime-bridge/agent-task-runtime-target-selector/SDD.md` + +### SDD 기준 + +- 대상 Acceptance Scenario는 S05~S07과 S09다. 이 closure child가 context transition, stage budget, retry-blocked, review recovery와 selector audit evidence를 검증하고 원본 Roadmap Targets를 소유한다. + +### 테스트 환경 규칙 + +- `test_env=local`; `agent-test/local/rules.md`가 존재하며 끝까지 읽었다. +- agent-ops Python dispatcher에 매칭되는 `agent-test/local/*.md` profile은 없다. 현재 checkout의 Python test manifest를 fallback 근거로 삼아 fresh-process `unittest`, `py_compile`, `git diff --check`를 사용하며 test-rule 유지보수는 필요하지 않다. +- provider subprocess는 fake runner와 임시 workspace/state/locator로 대체하고 실제 cloud/local 호출은 하지 않는다. +- 계획 시점 전체 Python suite 기준선은 169 tests PASS다. + +### 테스트 커버리지 공백 + +- normalized context package와 selector history가 실제 continuation invocation에 연결되지 않는다. +- retry-blocked의 decision/history 보존과 동일 stage failure budget 공유가 검증되지 않는다. +- review finalization recovery와 locator/status/dry-run의 selector evidence가 없다. + +### 심볼 참조 + +- `dispatch.py:2658-2828` continuation/budget lifecycle과 `dispatch.py:794-865,2158-2186,3665-3672,4193-4222` review recovery 및 audit output을 갱신한다. + +### 분할 판단 + +- 원본 `05+04_dispatch_integration`의 마지막 closure child다. +- `06+05_runtime_quota_failover`의 decision/history를 소비하며 이 child PASS 뒤 selfcheck 정책 task가 시작된다. + +### 범위 결정 근거 + +- context package, stage budget, retry-blocked, review recovery, locator/status/dry-run evidence만 변경한다. +- selector matrix와 quota probe mapping은 선행 child 결과를 소비하고 재설계하지 않는다. + +### 최종 라우팅 + +- evaluation_mode: `isolated-reassessment` +- finalizer=`finalize-task-policy.sh`, finalizer_mode=`pair`; build/review status=`routed`. +- build closures: `scope_closed=true`(승인 SDD와 이 subtask 경계가 고정됨), `context_closed=true`(명시된 source/test 범위를 한 local 작업에서 유지 가능), `verification_closed=true`, `evidence_trusted=true`, `ownership_closed=true`, `decision_closed=true`. +- review closures: `scope_closed=true`, `context_closed=true`(동일 source/test 및 구현 evidence로 판정 가능), `verification_closed=true`, `evidence_trusted=true`, `ownership_closed=true`, `decision_closed=true`. +- build route_basis=`local-fit`; capability_gap=`none`. +- build loop-risk audit: matched signatures=`temporal_state`, `boundary_contract`, `variant_product`; 이 기록은 lane/G를 바꾸지 않는다. +- build scores(scope_coupling/state_concurrency/blast_irreversibility/evidence_diagnosis/verification_complexity)=`2/2/1/1/2`, final=`local-G08`, filename=`PLAN-local-G08.md`. +- review route_basis=`official-review`; official execution=`codex/gpt-5.6-sol xhigh`; capability_gap=`none`. +- review scores(scope_coupling/state_concurrency/blast_irreversibility/evidence_diagnosis/verification_complexity)=`2/2/1/2/2`, final=`cloud-G09`, filename=`CODE_REVIEW-cloud-G09.md`. + +## 구현 체크리스트 + +- [ ] cross-adapter invocation이 persisted normalized-output context package를 소비하고 dynamic failover와 기존 cloud promotion 모두 같은 stage 실패 예산을 이어받는다. +- [ ] dry-run은 read-only preview이고 retry-blocked는 blocker/budget만 초기화하며 review resume/finalization recovery는 pinned Codex와 독립 budget을 유지한다. +- [ ] locator/state/status가 work unit, rule/candidate/reason, quota, transition과 stage budget을 추적하고 통합 테스트가 restart·blocker·독립 drain을 함께 검증한다. +- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. + +## 구현 항목 + +### [REFACTOR-3] Context, budget과 retry-blocked 소비 + +- 문제: `dispatch.py:2658-2687` continuation과 `dispatch.py:2708-2828` budget lifecycle은 선행 task의 normalized context package, selector history, `clear_blocked()` 보존 규칙을 실제 invocation에 연결하지 않는다. +- 해결 방법: + +```python +# Before: agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py:2790 +prompt = base_prompt(task, role, spec) if previous_locator is None else continuation_prompt(...) +``` + +```python +# After +prompt = continuation_prompt_from_package( + context_package, + target=decision["selected"], + native_resume=same_pi_target, +) +``` + + qualified primary failure는 count 1, alternate 추가 9회 실패가 총 10 blocker가 되며 성공 target을 state/locator에 기록하고 동일 stage 성공에서만 reset한다. 일반 primary 실패는 same-target count만 늘리고 전환하지 않는다. `--retry-blocked`는 blocker와 stage counter를 초기화하되 decision/history/used candidate를 보존하고, quota-exhausted alternate만 새 snapshot으로 재평가해 available/unknown이면 진행하며 시간 변화·실패 횟수로 새 initial/bounce/cloud 승격하지 않는다. +- 수정 파일 및 체크리스트: + - [ ] `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py`: invocation payload, locator/result state, budget lifecycle. + - [ ] `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py`: fake runner input과 누적 failure evidence. +- 테스트 작성: `SelectorDispatcherIntegrationTest`에서 primary 1회→alternate 9회=10 blocker, restart, 성공 reset, retry-blocked exhausted→available, history 보존, 독립 branch drain을 temp state/locator로 검증한다. +- 중간 검증: `python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py SelectorDispatcherIntegrationTest -v` — context/budget/retry/drain case가 PASS해야 한다. + +### [REFACTOR-4] Review recovery와 runtime audit evidence + +- 문제: `dispatch.py:794-865` finalization recovery는 active review `.md`가 없을 수 있고, locator/status/dry-run(`dispatch.py:2158-2186,3665-3672,4193-4222`)에는 selector work unit/rule/candidate/reason/quota/transition/budget이 없다. +- 해결 방법: + +```python +# Before: agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py:4198 +spec = AgentSpec("codex", "gpt-5.6-sol", "codex/gpt-5.6-sol xhigh") if stage == "review" else route_agent(task) +``` + +```python +# After +decision = read_or_preview_stage_decision(task, state, stage=stage, dry_run=args.dry_run) +lines = status_lines(task, stage, "ready") + selector_evidence_lines(decision) +``` + + active review initial에서 Codex decision을 stage별로 보존하고 archive recovery는 동일 header identity의 persisted decision을 resume한다. legacy state에 decision이 없으면 archived header를 검증해 official-review fixed Codex decision만 합성하며 quota probe/failover를 금지한다. locator와 persisted transition history에 `work_unit_id`, `rule_id`, priority, candidate ranks/selected target, reason codes, quota snapshot id/status, transition과 stage budget을 기록하고 status/dry-run은 secret/raw provider output 없이 같은 evidence를 표시한다. +- 수정 파일 및 체크리스트: + - [ ] `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py`: review recovery와 locator/status/dry-run audit fields. + - [ ] `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py`: active review resume, post-archive restart와 evidence consistency. +- 테스트 작성: `SelectorDispatcherIntegrationTest`에서 review pinned Codex resume/독립 budget/no quota/no worker decision reuse, legacy fixed recovery, state-locator-status-dry-run evidence 일치를 검증한다. +- 중간 검증: `python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py SelectorDispatcherIntegrationTest -v` — review recovery와 audit assertions가 PASS해야 한다. + +## 의존 관계 및 구현 순서 + +1. `06+05_runtime_quota_failover`의 `complete.log`가 PASS여야 한다. +2. REFACTOR-3의 context/budget 결과를 REFACTOR-4가 review recovery와 audit evidence로 닫는다. +3. 이 task PASS 후 `08+07_selfcheck_policy`를 진행한다. + +## 수정 파일 요약 + +| 파일 | 항목 | +|---|---| +| `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py` | REFACTOR-3, REFACTOR-4 | +| `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py` | REFACTOR-3, REFACTOR-4 | + +## 최종 검증 + +- `python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py SelectorDispatcherIntegrationTest -v` — context/retry/review/audit 회귀 PASS. +- `python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py BlockerDrainTest -v` — 독립 branch drain 회귀 PASS. +- `python3 -m unittest discover -s agent-ops/skills/project/orchestrate-agent-task-loop/tests -p 'test_*.py'` — fresh process 전체 suite PASS. +- `python3 -m py_compile agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py 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/tests/test_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` — exit 0. +- `git diff --check` — 출력 없음. + +모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. + diff --git a/agent-task/m-agent-task-runtime-target-selector/06+05_selfcheck_policy/CODE_REVIEW-cloud-G05.md b/agent-task/m-agent-task-runtime-target-selector/08+07_selfcheck_policy/CODE_REVIEW-cloud-G05.md similarity index 96% rename from agent-task/m-agent-task-runtime-target-selector/06+05_selfcheck_policy/CODE_REVIEW-cloud-G05.md rename to agent-task/m-agent-task-runtime-target-selector/08+07_selfcheck_policy/CODE_REVIEW-cloud-G05.md index be551c7..352bc89 100644 --- a/agent-task/m-agent-task-runtime-target-selector/06+05_selfcheck_policy/CODE_REVIEW-cloud-G05.md +++ b/agent-task/m-agent-task-runtime-target-selector/08+07_selfcheck_policy/CODE_REVIEW-cloud-G05.md @@ -1,4 +1,4 @@ - + # Code Review Reference - REFACTOR @@ -14,7 +14,7 @@ ## 개요 date=2026-07-25 -task=m-agent-task-runtime-target-selector/06+05_selfcheck_policy, plan=0, tag=REFACTOR +task=m-agent-task-runtime-target-selector/08+07_selfcheck_policy, plan=0, tag=REFACTOR ## Roadmap Targets @@ -33,7 +33,7 @@ task=m-agent-task-runtime-target-selector/06+05_selfcheck_policy, plan=0, tag=RE 1. 판정을 append한다. 2. `CODE_REVIEW-cloud-G05.md` → `code_review_cloud_G05_0.log`, `PLAN-local-G04.md` → `plan_local_G04_0.log`로 아카이브한다. -3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-agent-task-runtime-target-selector/06+05_selfcheck_policy/`로 이동한다. WARN/FAIL이면 code-review skill이 요구하는 다음 filesystem state를 완전히 작성한다. +3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-agent-task-runtime-target-selector/08+07_selfcheck_policy/`로 이동한다. WARN/FAIL이면 code-review skill이 요구하는 다음 filesystem state를 완전히 작성한다. 4. PASS이고 task group이 `m-agent-task-runtime-target-selector`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다. 5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다. @@ -65,7 +65,7 @@ task=m-agent-task-runtime-target-selector/06+05_selfcheck_policy, plan=0, tag=RE - [ ] active `PLAN-local-G04.md`를 `plan_local_G04_0.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/06+05_selfcheck_policy/`를 `agent-task/archive/YYYY/MM/m-agent-task-runtime-target-selector/06+05_selfcheck_policy/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다. +- [ ] PASS이면 active task 디렉터리 `agent-task/m-agent-task-runtime-target-selector/08+07_selfcheck_policy/`를 `agent-task/archive/YYYY/MM/m-agent-task-runtime-target-selector/08+07_selfcheck_policy/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다. - [ ] PASS이고 task group이 `m-agent-task-runtime-target-selector`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, 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`를 작성하지 않는다. diff --git a/agent-task/m-agent-task-runtime-target-selector/06+05_selfcheck_policy/PLAN-local-G04.md b/agent-task/m-agent-task-runtime-target-selector/08+07_selfcheck_policy/PLAN-local-G04.md similarity index 86% rename from agent-task/m-agent-task-runtime-target-selector/06+05_selfcheck_policy/PLAN-local-G04.md rename to agent-task/m-agent-task-runtime-target-selector/08+07_selfcheck_policy/PLAN-local-G04.md index 1080e21..f83a6c3 100644 --- a/agent-task/m-agent-task-runtime-target-selector/06+05_selfcheck_policy/PLAN-local-G04.md +++ b/agent-task/m-agent-task-runtime-target-selector/08+07_selfcheck_policy/PLAN-local-G04.md @@ -1,10 +1,10 @@ - + # Plan - 실제 완료 target 기반 selfcheck ## 이 파일을 읽는 구현 에이전트에게 -선행 `05+04_dispatch_integration`의 `complete.log`가 확인된 뒤 구현한다. 구현과 검증을 끝낸 뒤 `CODE_REVIEW-cloud-G05.md`의 구현 에이전트 소유 섹션을 실제 내용과 원문 출력으로 채운다. active 파일은 그대로 두고 review 준비 완료만 보고한다. 종결은 code-review skill 전용이다. 차단되면 정확한 원인, 시도한 명령/출력, 재개 조건만 구현 에이전트 소유 evidence에 기록한다. 사용자에게 질문하거나 user-input 도구를 호출하지 않고, control-plane stop file 생성, 다음 상태 분류, archive/log 이동, `complete.log` 또는 roadmap 수정은 하지 않는다. +선행 `07+06_context_review_recovery`의 `complete.log`가 확인된 뒤 구현한다. 구현과 검증을 끝낸 뒤 `CODE_REVIEW-cloud-G05.md`의 구현 에이전트 소유 섹션을 실제 내용과 원문 출력으로 채운다. active 파일은 그대로 두고 review 준비 완료만 보고한다. 종결은 code-review skill 전용이다. 차단되면 정확한 원인, 시도한 명령/출력, 재개 조건만 구현 에이전트 소유 evidence에 기록한다. 사용자에게 질문하거나 user-input 도구를 호출하지 않고, control-plane stop file 생성, 다음 상태 분류, archive/log 이동, `complete.log` 또는 roadmap 수정은 하지 않는다. ## 배경 @@ -59,9 +59,10 @@ ### 분할 판단 -- split 정책을 먼저 평가했다. shared task group은 `m-agent-task-runtime-target-selector`이고 topology는 `03 → 04 → 05 → 06 → 07 → 08`이다. -- 현재 `06+05_selfcheck_policy`의 predecessor `05+04_dispatch_integration`은 active plan만 있고 `complete.log`가 없어 미충족이다. -- throughput도 같은 `dispatch.py`와 `test_dispatch.py`를 수정하므로 이 task 뒤 `07+06_throughput_policy`로 직렬화하고 최종 audit는 `08`이 담당한다. +- split 정책을 평가했으며 selfcheck completion evidence와 scheduling/target reuse가 같은 state transition을 공유해 원본 pair를 유지한다. +- 전체 topology는 `03 → 04 → 05 → 06 → 07 → 08 → 09 → 10 → 11 → 12`다. +- 현재 predecessor `07+06_context_review_recovery`는 active plan만 있고 `complete.log`가 없어 미충족이다. +- 이 task 뒤 admission batch, target cap 제거, unknown 격리를 순서대로 진행하고 최종 audit가 `08`과 `11`을 소비한다. ### 범위 결정 근거 @@ -151,9 +152,9 @@ def completing_decision_requires_selfcheck(state: dict[str, Any]) -> bool: ## 의존 관계 및 구현 순서 -1. `05+04_dispatch_integration`의 archived `complete.log`가 PASS여야 한다. 계획 작성 시점에는 아직 미충족이다. +1. `07+06_context_review_recovery`의 archived `complete.log`가 PASS여야 한다. 2. REFACTOR-1 완료 evidence를 REFACTOR-2가 소비한다. -3. 이 task PASS 후 `07+06_throughput_policy`를 시작하며, `06`과 `07`이 모두 PASS해야 `08+06,07_audit_closure`를 시작한다. +3. 이 task PASS 후 `09+08_admission_quota_batch`를 시작하며, `08`과 `11`이 모두 PASS해야 `12+08,11_audit_closure`를 시작한다. ## 수정 파일 요약 diff --git a/agent-task/m-agent-task-runtime-target-selector/09+08_admission_quota_batch/CODE_REVIEW-cloud-G07.md b/agent-task/m-agent-task-runtime-target-selector/09+08_admission_quota_batch/CODE_REVIEW-cloud-G07.md new file mode 100644 index 0000000..4dd52a7 --- /dev/null +++ b/agent-task/m-agent-task-runtime-target-selector/09+08_admission_quota_batch/CODE_REVIEW-cloud-G07.md @@ -0,0 +1,122 @@ + + +# Code Review Reference - REFACTOR + +> **[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-25 +task=m-agent-task-runtime-target-selector/09+08_admission_quota_batch, plan=1, tag=REFACTOR + +## 이 파일을 읽는 리뷰 에이전트에게 + +> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다. + +각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요. +리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다. + +1. 판정을 append한다. +2. `CODE_REVIEW-cloud-G07.md` → `code_review_cloud_G07_1.log`, `PLAN-local-G07.md` → `plan_local_G07_1.log`로 아카이브한다. +3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-agent-task-runtime-target-selector/09+08_admission_quota_batch/`로 이동한다. WARN/FAIL이면 code-review skill이 요구하는 다음 filesystem state를 완전히 작성한다. +4. PASS이고 task group이 `m-agent-task-runtime-target-selector`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다. +5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다. + +--- + +## 구현 항목별 완료 여부 + +| 항목 | 완료 여부 | +|------|---------| +| REFACTOR-2 Admission batch quota snapshot | [ ] | + +## 구현 체크리스트 + +- [ ] 한 scheduling admission batch가 unique adapter+target+profile별 probe를 한 번씩 실행해 aggregate envelope을 만들고 모든 최초 selector 결정이 동일 batch snapshot id와 checked_at을 재사용한다. +- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. + +## 코드리뷰 전용 체크리스트 + +> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다. +> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다. + +- [ ] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다. +- [ ] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다. +- [ ] active `CODE_REVIEW-cloud-G07.md`를 `code_review_cloud_G07_1.log`로 아카이브한다. +- [ ] active `PLAN-local-G07.md`를 `plan_local_G07_1.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/09+08_admission_quota_batch/`를 `agent-task/archive/YYYY/MM/m-agent-task-runtime-target-selector/09+08_admission_quota_batch/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다. +- [ ] PASS이고 task group이 `m-agent-task-runtime-target-selector`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, 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`를 작성하지 않는다. + +## 계획 대비 변경 사항 + +_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._ + +## 주요 설계 결정 + +_구현 에이전트가 주요 설계 결정 사항을 기록한다._ + +## 리뷰어를 위한 체크포인트 + +- aggregate envelope이 unique probe key별 single-target Go result를 정확히 합치는가. +- 같은 admission batch의 최초 decision이 동일 batch id와 checked_at을 공유하는가. +- local decision과 persisted resume/selfcheck가 probe 대상에서 제외되는가. +- batch envelope이 admission pass 밖에 영속되거나 global cache로 재사용되지 않는가. + +## 검증 결과 + +### ThroughputQuotaBatchTest + +명령: `python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py ThroughputQuotaBatchTest -v` + +_실제 stdout/stderr와 exit code를 기록한다._ + +### 전체 Python suite + +명령: `python3 -m unittest discover -s agent-ops/skills/project/orchestrate-agent-task-loop/tests -p 'test_*.py'` + +_실제 stdout/stderr와 exit code를 기록한다._ + +### py_compile + +명령: `python3 -m py_compile agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py` + +_실제 stdout/stderr와 exit code를 기록한다._ + +### diff check + +명령: `git diff --check` + +_실제 stdout/stderr와 exit code를 기록한다._ + +--- + +> **[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 `[ ]` → `[x]` only | +| 구현 체크리스트 (item text/order) | Fixed at stub creation from plan | Implementing agent checks `[ ]` → `[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/agent-task/m-agent-task-runtime-target-selector/09+08_admission_quota_batch/PLAN-local-G07.md b/agent-task/m-agent-task-runtime-target-selector/09+08_admission_quota_batch/PLAN-local-G07.md new file mode 100644 index 0000000..0ae8265 --- /dev/null +++ b/agent-task/m-agent-task-runtime-target-selector/09+08_admission_quota_batch/PLAN-local-G07.md @@ -0,0 +1,122 @@ + + +# Plan - Admission batch quota snapshot 재사용 + +## 이 파일을 읽는 구현 에이전트에게 + +선행 `08+07_selfcheck_policy`의 `complete.log`가 확인된 뒤 구현한다. 구현과 검증을 끝낸 뒤 `CODE_REVIEW-cloud-G07.md`의 구현 에이전트 소유 섹션을 실제 내용과 원문 출력으로 채운다. active 파일은 그대로 두고 review 준비 완료만 보고한다. 종결은 code-review skill 전용이다. 차단되면 정확한 원인, 시도한 명령/출력, 재개 조건만 구현 에이전트 소유 evidence에 기록한다. 사용자에게 질문하거나 user-input 도구를 호출하지 않고, control-plane stop file 생성, 다음 상태 분류, archive/log 이동, `complete.log` 또는 roadmap 수정은 하지 않는다. + +## 배경 + +같은 scheduling admission batch의 최초 selector 결정들이 unique probe key별 관측을 한 번만 수행하고 동일 snapshot identity와 checked_at을 재사용하도록 batch 경계를 만든다. + +## 분석 결과 + +### 읽은 파일 + +- `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py` +- `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/select_execution_target.py` +- `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py` +- `apps/node/internal/adapters/cli/status/quota.go` +- `apps/node/cmd/node/quota_probe.go` +- `agent-test/local/rules.md` + +### SDD 기준 + +- 대상 Acceptance Scenario는 S11이다. 이 child는 scheduling batch lifetime, unique probe key와 shared snapshot identity 범위만 소유한다. + +### 테스트 환경 규칙 + +- `test_env=local`; `agent-test/local/rules.md`가 존재하며 끝까지 읽었다. +- agent-ops Python dispatcher에 매칭되는 `agent-test/local/*.md` profile은 없다. 현재 checkout의 Python test manifest를 fallback 근거로 삼아 fresh-process `unittest`, `py_compile`, `git diff --check`를 사용하며 test-rule 유지보수는 필요하지 않다. +- quota probe와 runner는 fake로 주입해 호출 횟수, batch identity, 실제 동시 시작을 결정적으로 측정한다. +- 계획 시점 전체 Python suite 기준선은 169 tests PASS다. + +### 테스트 커버리지 공백 + +- 같은 admission batch의 task별 selector 평가가 quota를 반복 관측해 snapshot id와 시각이 달라질 수 있다. +- mixed target에서 unique probe key별 single observation과 local/resume probe 0회가 검증되지 않는다. + +### 심볼 참조 + +- `dispatch.py:4228` ready admission pass에 immutable aggregate quota envelope을 만들고 최초 decision에 주입한다. Node quota JSON producer는 변경하지 않는다. + +### 분할 판단 + +- 원본 `07+06_throughput_policy`를 admission batch, target cap 제거, unknown 격리의 세 behavior slice로 분할했다. +- batch envelope이 뒤 두 child의 producer이므로 첫 번째 index를 갖는다. + +### 범위 결정 근거 + +- quota batch envelope은 admission pass 수명으로만 재사용하고 영속/global cache를 만들지 않는다. +- target semaphore 제거와 probe failure 격리는 후속 child 범위다. + +### 최종 라우팅 + +- evaluation_mode: `isolated-reassessment` +- finalizer=`finalize-task-policy.sh`, finalizer_mode=`pair`; build/review status=`routed`. +- build closures: `scope_closed=true`(승인 SDD와 이 subtask 경계가 고정됨), `context_closed=true`(명시된 source/test 범위를 한 local 작업에서 유지 가능), `verification_closed=true`, `evidence_trusted=true`, `ownership_closed=true`, `decision_closed=true`. +- review closures: `scope_closed=true`, `context_closed=true`(동일 source/test 및 구현 evidence로 판정 가능), `verification_closed=true`, `evidence_trusted=true`, `ownership_closed=true`, `decision_closed=true`. +- build route_basis=`local-fit`; capability_gap=`none`. +- build loop-risk audit: matched signatures=`boundary_contract`, `concurrent_consistency`; 이 기록은 lane/G를 바꾸지 않는다. +- build scores(scope_coupling/state_concurrency/blast_irreversibility/evidence_diagnosis/verification_complexity)=`1/2/1/1/2`, final=`local-G07`, filename=`PLAN-local-G07.md`. +- review route_basis=`official-review`; official execution=`codex/gpt-5.6-sol xhigh`; capability_gap=`none`. +- review scores(scope_coupling/state_concurrency/blast_irreversibility/evidence_diagnosis/verification_complexity)=`1/2/1/1/2`, final=`cloud-G07`, filename=`CODE_REVIEW-cloud-G07.md`. + +## 구현 체크리스트 + +- [ ] 한 scheduling admission batch가 unique adapter+target+profile별 probe를 한 번씩 실행해 aggregate envelope을 만들고 모든 최초 selector 결정이 동일 batch snapshot id와 checked_at을 재사용한다. +- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. + +## 구현 항목 + +### [REFACTOR-2] Admission batch quota snapshot + +- 문제: `dispatch.py:4228`의 ready admission pass에서 task별 selector 평가가 single-target Go probe를 개별 호출하면 같은 pass 안에서도 quota 상태/시각이 달라지고, 한 snapshot을 mixed target에 재사용하면 unmatched target이 `unknown`이 된다. +- 해결 방법: + +```python +# Before: agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py:4228 +for task, stage in select_dispatch_candidates(ready)[0]: + resolve_decision(task) +``` + +```python +# After +batch_snapshot = quota_provider.aggregate( + snapshot_id=batch_id, + checked_at=admission_time, + keys=unique_probe_keys, +) +for task in ready_tasks: + resolve_decision(task, quota_snapshot=batch_snapshot) +``` + + probe key는 `(adapter, canonical target, command/profile, required_caps)`다. 한 pass에서 key별 Go probe를 최대 한 번 실행하고 결과 targets를 immutable aggregate envelope으로 합치며 top-level batch id/time을 모든 최초 decision에 전달한다. child Go snapshot id/reason은 target entry evidence로 보존한다. 동일 target N개는 probe 1회, mixed target은 unique key 수만큼 호출한다. local decision은 `not_applicable`이고 persisted decision이 있는 resume/selfcheck는 admission probe 대상이 아니다. +- 수정 파일 및 체크리스트: + - [ ] `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py`: batch context와 snapshot lifetime. + - [ ] `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py`: probe count와 identity assertions. +- 테스트 작성: `ThroughputQuotaBatchTest`에서 동일 target N개=probe 1회, mixed target=unique key별 1회, 전체 decision의 같은 batch id/time, target별 status 매칭, local/resume probe 0회를 검증한다. +- 중간 검증: `python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py ThroughputQuotaBatchTest -v` — probe count/envelope identity matrix가 PASS해야 한다. + +## 의존 관계 및 구현 순서 + +1. `08+07_selfcheck_policy`의 `complete.log`가 PASS여야 한다. +2. 이 task의 batch envelope을 `10+09_target_cap_removal`과 그 후속 `11+10_unknown_isolation`이 소비한다. + +## 수정 파일 요약 + +| 파일 | 항목 | +|---|---| +| `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py` | REFACTOR-2 | +| `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py` | REFACTOR-2 | + +## 최종 검증 + +- `python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py ThroughputQuotaBatchTest -v` — probe count/envelope identity matrix PASS. +- `python3 -m unittest discover -s agent-ops/skills/project/orchestrate-agent-task-loop/tests -p 'test_*.py'` — fresh process 전체 suite PASS. +- `python3 -m py_compile agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py` — exit 0. +- `git diff --check` — 출력 없음. + +모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. + diff --git a/agent-task/m-agent-task-runtime-target-selector/07+06_throughput_policy/CODE_REVIEW-cloud-G07.md b/agent-task/m-agent-task-runtime-target-selector/09+08_admission_quota_batch/code_review_cloud_G07_0.log similarity index 100% rename from agent-task/m-agent-task-runtime-target-selector/07+06_throughput_policy/CODE_REVIEW-cloud-G07.md rename to agent-task/m-agent-task-runtime-target-selector/09+08_admission_quota_batch/code_review_cloud_G07_0.log diff --git a/agent-task/m-agent-task-runtime-target-selector/07+06_throughput_policy/PLAN-local-G07.md b/agent-task/m-agent-task-runtime-target-selector/09+08_admission_quota_batch/plan_local_G07_0.log similarity index 100% rename from agent-task/m-agent-task-runtime-target-selector/07+06_throughput_policy/PLAN-local-G07.md rename to agent-task/m-agent-task-runtime-target-selector/09+08_admission_quota_batch/plan_local_G07_0.log diff --git a/agent-task/m-agent-task-runtime-target-selector/10+09_target_cap_removal/CODE_REVIEW-cloud-G07.md b/agent-task/m-agent-task-runtime-target-selector/10+09_target_cap_removal/CODE_REVIEW-cloud-G07.md new file mode 100644 index 0000000..d5e5fb4 --- /dev/null +++ b/agent-task/m-agent-task-runtime-target-selector/10+09_target_cap_removal/CODE_REVIEW-cloud-G07.md @@ -0,0 +1,129 @@ + + +# Code Review Reference - REFACTOR + +> **[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-25 +task=m-agent-task-runtime-target-selector/10+09_target_cap_removal, plan=0, tag=REFACTOR + +## 이 파일을 읽는 리뷰 에이전트에게 + +> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다. + +각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요. +리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다. + +1. 판정을 append한다. +2. `CODE_REVIEW-cloud-G07.md` → `code_review_cloud_G07_0.log`, `PLAN-local-G07.md` → `plan_local_G07_0.log`로 아카이브한다. +3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-agent-task-runtime-target-selector/10+09_target_cap_removal/`로 이동한다. WARN/FAIL이면 code-review skill이 요구하는 다음 filesystem state를 완전히 작성한다. +4. PASS이고 task group이 `m-agent-task-runtime-target-selector`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다. +5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다. + +--- + +## 구현 항목별 완료 여부 + +| 항목 | 완료 여부 | +|------|---------| +| REFACTOR-1 Target별 semaphore 제거 | [ ] | + +## 구현 체크리스트 + +- [ ] target별 정적 semaphore와 concurrency map이 live dispatch 경로에서 제거된다. +- [ ] 서로 독립적인 동일 target ready task가 target cap 없이 모두 admission된다. +- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. + +## 코드리뷰 전용 체크리스트 + +> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다. +> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다. + +- [ ] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다. +- [ ] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다. +- [ ] active `CODE_REVIEW-cloud-G07.md`를 `code_review_cloud_G07_0.log`로 아카이브한다. +- [ ] active `PLAN-local-G07.md`를 `plan_local_G07_0.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/10+09_target_cap_removal/`를 `agent-task/archive/YYYY/MM/m-agent-task-runtime-target-selector/10+09_target_cap_removal/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다. +- [ ] PASS이고 task group이 `m-agent-task-runtime-target-selector`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, 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`를 작성하지 않는다. + +## 계획 대비 변경 사항 + +_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._ + +## 주요 설계 결정 + +_구현 에이전트가 주요 설계 결정 사항을 기록한다._ + +## 리뷰어를 위한 체크포인트 + +- target/model 기반 semaphore와 관련 전달 인자가 live dispatch 경로에서 제거되는가. +- dependency readiness와 task-local blocker, state/work-log 정합성 lock은 보존되는가. +- 기존 cap보다 많은 동일 target task가 runner barrier에 모두 도달하는가. +- cap 제거가 batch snapshot identity를 변경하지 않는가. + +## 검증 결과 + +### ThroughputQuotaBatchTest + +명령: `python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py ThroughputQuotaBatchTest -v` + +_실제 stdout/stderr와 exit code를 기록한다._ + +### BlockerDrainTest + +명령: `python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py BlockerDrainTest -v` + +_실제 stdout/stderr와 exit code를 기록한다._ + +### 전체 Python suite + +명령: `python3 -m unittest discover -s agent-ops/skills/project/orchestrate-agent-task-loop/tests -p 'test_*.py'` + +_실제 stdout/stderr와 exit code를 기록한다._ + +### py_compile + +명령: `python3 -m py_compile agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py` + +_실제 stdout/stderr와 exit code를 기록한다._ + +### diff check + +명령: `git diff --check` + +_실제 stdout/stderr와 exit code를 기록한다._ + +--- + +> **[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 `[ ]` → `[x]` only | +| 구현 체크리스트 (item text/order) | Fixed at stub creation from plan | Implementing agent checks `[ ]` → `[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/agent-task/m-agent-task-runtime-target-selector/10+09_target_cap_removal/PLAN-local-G07.md b/agent-task/m-agent-task-runtime-target-selector/10+09_target_cap_removal/PLAN-local-G07.md new file mode 100644 index 0000000..dfe571e --- /dev/null +++ b/agent-task/m-agent-task-runtime-target-selector/10+09_target_cap_removal/PLAN-local-G07.md @@ -0,0 +1,114 @@ + + +# Plan - Target별 정적 concurrency cap 제거 + +## 이 파일을 읽는 구현 에이전트에게 + +선행 `09+08_admission_quota_batch`의 `complete.log`가 확인된 뒤 구현한다. 구현과 검증을 끝낸 뒤 `CODE_REVIEW-cloud-G07.md`의 구현 에이전트 소유 섹션을 실제 내용과 원문 출력으로 채운다. active 파일은 그대로 두고 review 준비 완료만 보고한다. 종결은 code-review skill 전용이다. 차단되면 정확한 원인, 시도한 명령/출력, 재개 조건만 구현 에이전트 소유 evidence에 기록한다. 사용자에게 질문하거나 user-input 도구를 호출하지 않고, control-plane stop file 생성, 다음 상태 분류, archive/log 이동, `complete.log` 또는 roadmap 수정은 하지 않는다. + +## 배경 + +admission batch가 quota observation을 고정한 뒤 target/model 이름에 따른 semaphore를 제거해 dependency-ready task가 정적 target cap 없이 실행되게 한다. + +## 분석 결과 + +### 읽은 파일 + +- `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py` +- `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py` +- `agent-test/local/rules.md` + +### SDD 기준 + +- 대상 Acceptance Scenario는 S11이다. 이 child는 같은 target의 독립 ready task가 정책 semaphore 없이 모두 admission되는 범위를 소유한다. + +### 테스트 환경 규칙 + +- `test_env=local`; `agent-test/local/rules.md`가 존재하며 끝까지 읽었다. +- agent-ops Python dispatcher에 매칭되는 `agent-test/local/*.md` profile은 없다. 현재 checkout의 Python test manifest를 fallback 근거로 삼아 fresh-process `unittest`, `py_compile`, `git diff --check`를 사용하며 test-rule 유지보수는 필요하지 않다. +- quota probe와 runner는 fake로 주입해 호출 횟수, batch identity, 실제 동시 시작을 결정적으로 측정한다. +- 계획 시점 전체 Python suite 기준선은 169 tests PASS다. + +### 테스트 커버리지 공백 + +- target semaphore map이 Laguna, Ornith, AGY 등에 정적 동시 실행 cap을 강제한다. +- 기존 cap보다 많은 동일 target ready task가 모두 admission되는지 검증하지 않는다. + +### 심볼 참조 + +- `run_escalating(invocation_semaphore)`, `run_worker(semaphores)`, `run_selfcheck(semaphores)`, semaphore map 생성과 호출부를 새 호출 계약으로 갱신한다. + +### 분할 판단 + +- 원본 throughput plan의 두 번째 child다. +- `09+08_admission_quota_batch`가 quota observation을 먼저 고정하며 이 child는 target cap 제거만 소유한다. + +### 범위 결정 근거 + +- model/target별 인위적 정책 cap만 제거한다. dependency readiness, task-local blocker, work-log/state 정합성 lock은 보존한다. + +### 최종 라우팅 + +- evaluation_mode: `isolated-reassessment` +- finalizer=`finalize-task-policy.sh`, finalizer_mode=`pair`; build/review status=`routed`. +- build closures: `scope_closed=true`(승인 SDD와 이 subtask 경계가 고정됨), `context_closed=true`(명시된 source/test 범위를 한 local 작업에서 유지 가능), `verification_closed=true`, `evidence_trusted=true`, `ownership_closed=true`, `decision_closed=true`. +- review closures: `scope_closed=true`, `context_closed=true`(동일 source/test 및 구현 evidence로 판정 가능), `verification_closed=true`, `evidence_trusted=true`, `ownership_closed=true`, `decision_closed=true`. +- build route_basis=`local-fit`; capability_gap=`none`. +- build loop-risk audit: matched signatures=`boundary_contract`, `concurrent_consistency`; 이 기록은 lane/G를 바꾸지 않는다. +- build scores(scope_coupling/state_concurrency/blast_irreversibility/evidence_diagnosis/verification_complexity)=`1/2/1/1/2`, final=`local-G07`, filename=`PLAN-local-G07.md`. +- review route_basis=`official-review`; official execution=`codex/gpt-5.6-sol xhigh`; capability_gap=`none`. +- review scores(scope_coupling/state_concurrency/blast_irreversibility/evidence_diagnosis/verification_complexity)=`1/2/1/1/2`, final=`cloud-G07`, filename=`CODE_REVIEW-cloud-G07.md`. + +## 구현 체크리스트 + +- [ ] target별 정적 semaphore와 concurrency map이 live dispatch 경로에서 제거된다. +- [ ] 서로 독립적인 동일 target ready task가 target cap 없이 모두 admission된다. +- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. + +## 구현 항목 + +### [REFACTOR-1] Target별 semaphore 제거 + +- 문제: `dispatch.py:2696,2801-2821,3420-3509,3755-3761,4291-4305`의 target concurrency map과 invocation semaphore가 ready candidate를 고른 뒤 adapter/target 이름에 따라 병렬도를 제한한다. +- 해결 방법: + +```python +# Before: agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py:2811 +async with invocation_semaphore: + return await invoke(...) +``` + +```python +# After +return await invoke(...) +``` + + target 정책 cap과 관련 생성/전달 인자를 제거한다. dependency, task lifecycle, work-log lock처럼 정합성에 필요한 lock은 보존한다. +- 수정 파일 및 체크리스트: + - [ ] `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py`: semaphore map, parameter, acquisition 제거. + - [ ] `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py`: 동일 target 동시 admission 회귀. +- 테스트 작성: `test_dispatch.py`의 `ThroughputQuotaBatchTest`에서 기존 cap보다 많은 독립 task가 `asyncio.wait_for`로 제한된 시간 안에 runner barrier에 모두 도달한 뒤 release되는지 검증하고 timeout 자체를 failure evidence로 만든다. +- 중간 검증: `python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py ThroughputQuotaBatchTest -v` — bounded concurrency test가 hang 없이 PASS해야 한다. + +## 의존 관계 및 구현 순서 + +1. `09+08_admission_quota_batch`의 `complete.log`가 PASS여야 한다. +2. 이 task PASS 후 `11+10_unknown_isolation`이 shared envelope 오류 격리를 닫는다. + +## 수정 파일 요약 + +| 파일 | 항목 | +|---|---| +| `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py` | REFACTOR-1 | +| `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py` | REFACTOR-1 | + +## 최종 검증 + +- `python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py ThroughputQuotaBatchTest -v` — 동일 target 무제한 admission 회귀 PASS. +- `python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py BlockerDrainTest -v` — 독립 branch drain 회귀 PASS. +- `python3 -m unittest discover -s agent-ops/skills/project/orchestrate-agent-task-loop/tests -p 'test_*.py'` — fresh process 전체 suite PASS. +- `python3 -m py_compile agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py` — exit 0. +- `git diff --check` — 출력 없음. + +모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. + diff --git a/agent-task/m-agent-task-runtime-target-selector/11+10_unknown_isolation/CODE_REVIEW-cloud-G07.md b/agent-task/m-agent-task-runtime-target-selector/11+10_unknown_isolation/CODE_REVIEW-cloud-G07.md new file mode 100644 index 0000000..f019085 --- /dev/null +++ b/agent-task/m-agent-task-runtime-target-selector/11+10_unknown_isolation/CODE_REVIEW-cloud-G07.md @@ -0,0 +1,136 @@ + + +# Code Review Reference - REFACTOR + +> **[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-25 +task=m-agent-task-runtime-target-selector/11+10_unknown_isolation, plan=0, tag=REFACTOR + +## 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: + - `throughput-policy`: target별 정적 cap 없이 ready task dispatch와 batch quota snapshot 재사용 +- Completion mode: check-on-pass + +## 이 파일을 읽는 리뷰 에이전트에게 + +> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다. + +각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요. +리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다. + +1. 판정을 append한다. +2. `CODE_REVIEW-cloud-G07.md` → `code_review_cloud_G07_0.log`, `PLAN-local-G07.md` → `plan_local_G07_0.log`로 아카이브한다. +3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-agent-task-runtime-target-selector/11+10_unknown_isolation/`로 이동한다. WARN/FAIL이면 code-review skill이 요구하는 다음 filesystem state를 완전히 작성한다. +4. PASS이고 task group이 `m-agent-task-runtime-target-selector`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다. +5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다. + +--- + +## 구현 항목별 완료 여부 + +| 항목 | 완료 여부 | +|------|---------| +| REFACTOR-3 Unknown 격리와 회귀 | [ ] | + +## 구현 체크리스트 + +- [ ] quota probe 실패는 해당 batch/key의 unknown으로 격리되고 persisted work unit은 재선택하지 않으며 새 generation·명시적 failover/retry만 새 snapshot을 관측한다. +- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. + +## 코드리뷰 전용 체크리스트 + +> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다. +> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다. + +- [ ] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다. +- [ ] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다. +- [ ] active `CODE_REVIEW-cloud-G07.md`를 `code_review_cloud_G07_0.log`로 아카이브한다. +- [ ] active `PLAN-local-G07.md`를 `plan_local_G07_0.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/11+10_unknown_isolation/`를 `agent-task/archive/YYYY/MM/m-agent-task-runtime-target-selector/11+10_unknown_isolation/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다. +- [ ] PASS이고 task group이 `m-agent-task-runtime-target-selector`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, 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`를 작성하지 않는다. + +## 계획 대비 변경 사항 + +_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._ + +## 주요 설계 결정 + +_구현 에이전트가 주요 설계 결정 사항을 기록한다._ + +## 리뷰어를 위한 체크포인트 + +- probe failure가 해당 batch/key의 unknown으로만 정규화되고 다른 key를 오염시키지 않는가. +- same persisted work unit이 resume에서 재probe/재선택하지 않고 route pin을 유지하는가. +- confirmed provider-quota가 immutable shared envelope 대신 task-local derived exhausted evidence만 만드는가. +- 새 generation, unused failover eligibility, 명시적 retry만 새 snapshot을 관측하는가. + +## 검증 결과 + +### ThroughputQuotaBatchTest + +명령: `python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py ThroughputQuotaBatchTest -v` + +_실제 stdout/stderr와 exit code를 기록한다._ + +### BlockerDrainTest + +명령: `python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py BlockerDrainTest -v` + +_실제 stdout/stderr와 exit code를 기록한다._ + +### 전체 Python suite + +명령: `python3 -m unittest discover -s agent-ops/skills/project/orchestrate-agent-task-loop/tests -p 'test_*.py'` + +_실제 stdout/stderr와 exit code를 기록한다._ + +### py_compile + +명령: `python3 -m py_compile agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py` + +_실제 stdout/stderr와 exit code를 기록한다._ + +### diff check + +명령: `git diff --check` + +_실제 stdout/stderr와 exit code를 기록한다._ + +--- + +> **[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 `[ ]` → `[x]` only | +| 구현 체크리스트 (item text/order) | Fixed at stub creation from plan | Implementing agent checks `[ ]` → `[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/agent-task/m-agent-task-runtime-target-selector/11+10_unknown_isolation/PLAN-local-G07.md b/agent-task/m-agent-task-runtime-target-selector/11+10_unknown_isolation/PLAN-local-G07.md new file mode 100644 index 0000000..490a645 --- /dev/null +++ b/agent-task/m-agent-task-runtime-target-selector/11+10_unknown_isolation/PLAN-local-G07.md @@ -0,0 +1,125 @@ + + +# Plan - Quota unknown 격리와 throughput closure + +## 이 파일을 읽는 구현 에이전트에게 + +선행 `10+09_target_cap_removal`의 `complete.log`가 확인된 뒤 구현한다. 구현과 검증을 끝낸 뒤 `CODE_REVIEW-cloud-G07.md`의 구현 에이전트 소유 섹션을 실제 내용과 원문 출력으로 채운다. active 파일은 그대로 두고 review 준비 완료만 보고한다. 종결은 code-review skill 전용이다. 차단되면 정확한 원인, 시도한 명령/출력, 재개 조건만 구현 에이전트 소유 evidence에 기록한다. 사용자에게 질문하거나 user-input 도구를 호출하지 않고, control-plane stop file 생성, 다음 상태 분류, archive/log 이동, `complete.log` 또는 roadmap 수정은 하지 않는다. + +## 배경 + +immutable admission batch envelope에서 probe failure와 runtime provider-quota evidence를 work unit별로 격리해 route pin과 다른 task의 quota 상태를 오염시키지 않도록 throughput 정책을 닫는다. + +## 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: + - `throughput-policy`: target별 정적 cap 없이 ready task dispatch와 batch quota snapshot 재사용 +- Completion mode: check-on-pass + +## 분석 결과 + +### 읽은 파일 + +- `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py` +- `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py` +- `agent-test/local/rules.md` +- `agent-roadmap/sdd/automation-runtime-bridge/agent-task-runtime-target-selector/SDD.md` + +### SDD 기준 + +- 대상 Acceptance Scenario는 S11이다. 이 closure child가 batch-local unknown, task-local derived exhausted, persisted route pin 보존을 검증하고 원본 Roadmap Target을 소유한다. + +### 테스트 환경 규칙 + +- `test_env=local`; `agent-test/local/rules.md`가 존재하며 끝까지 읽었다. +- agent-ops Python dispatcher에 매칭되는 `agent-test/local/*.md` profile은 없다. 현재 checkout의 Python test manifest를 fallback 근거로 삼아 fresh-process `unittest`, `py_compile`, `git diff --check`를 사용하며 test-rule 유지보수는 필요하지 않다. +- quota probe와 runner는 fake로 주입해 호출 횟수, batch identity, 실제 동시 시작을 결정적으로 측정한다. +- 계획 시점 전체 Python suite 기준선은 169 tests PASS다. + +### 테스트 커버리지 공백 + +- probe exception/parse failure 또는 runtime quota failure가 shared envelope을 in-place 변경하면 독립 work unit의 quota evidence와 route pin을 오염시킬 수 있다. +- same work unit resume, new generation, explicit retry의 snapshot 수명 차이가 검증되지 않는다. + +### 심볼 참조 + +- `dispatch.py:4228` admission의 probe error normalization과 work-unit derived evidence 경계를 immutable batch envelope 위에 구현한다. + +### 분할 판단 + +- 원본 throughput plan의 마지막 closure child다. +- `10+09_target_cap_removal`까지 완료된 admission 경로에서 unknown과 derived exhausted 격리를 검증하고 최종 audit의 producer가 된다. + +### 범위 결정 근거 + +- shared envelope을 변경하지 않는 batch-key unknown과 work-unit derived exhausted만 구현한다. 새 global cache, route 재선택 정책, provider adapter는 추가하지 않는다. + +### 최종 라우팅 + +- evaluation_mode: `isolated-reassessment` +- finalizer=`finalize-task-policy.sh`, finalizer_mode=`pair`; build/review status=`routed`. +- build closures: `scope_closed=true`(승인 SDD와 이 subtask 경계가 고정됨), `context_closed=true`(명시된 source/test 범위를 한 local 작업에서 유지 가능), `verification_closed=true`, `evidence_trusted=true`, `ownership_closed=true`, `decision_closed=true`. +- review closures: `scope_closed=true`, `context_closed=true`(동일 source/test 및 구현 evidence로 판정 가능), `verification_closed=true`, `evidence_trusted=true`, `ownership_closed=true`, `decision_closed=true`. +- build route_basis=`local-fit`; capability_gap=`none`. +- build loop-risk audit: matched signatures=`boundary_contract`, `concurrent_consistency`; 이 기록은 lane/G를 바꾸지 않는다. +- build scores(scope_coupling/state_concurrency/blast_irreversibility/evidence_diagnosis/verification_complexity)=`1/2/1/1/2`, final=`local-G07`, filename=`PLAN-local-G07.md`. +- review route_basis=`official-review`; official execution=`codex/gpt-5.6-sol xhigh`; capability_gap=`none`. +- review scores(scope_coupling/state_concurrency/blast_irreversibility/evidence_diagnosis/verification_complexity)=`1/2/1/1/2`, final=`cloud-G07`, filename=`CODE_REVIEW-cloud-G07.md`. + +## 구현 체크리스트 + +- [ ] quota probe 실패는 해당 batch/key의 unknown으로 격리되고 persisted work unit은 재선택하지 않으며 새 generation·명시적 failover/retry만 새 snapshot을 관측한다. +- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. + +## 구현 항목 + +### [REFACTOR-3] Unknown 격리와 회귀 + +- 문제: `dispatch.py:4228` admission에서 probe exception/parse failure 또는 runtime quota failure를 shared envelope에 in-place 반영하면 독립 work unit의 quota evidence와 route pin을 오염시킬 수 있다. +- 해결 방법: + +```python +# Before: agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py:4228 +candidates, deferred, phase_wait_reason = select_dispatch_candidates(ready) +``` + +```python +# After +derived = derive_work_unit_quota_evidence( + decision, + status="exhausted", + reason="confirmed_runtime_provider_quota", +) +``` + + probe failure는 해당 batch/key의 `unknown` entry로 정규화하고 work unit별 1회 admission은 selector에 맡긴다. batch envelope은 immutable로 폐기하며 같은 persisted work unit은 다음 pass에서 재probe/재선택하지 않는다. confirmed runtime `provider-quota`는 shared envelope을 바꾸지 않고 해당 work unit decision에서 같은 observation identity를 계승한 derived `exhausted` evidence로만 교체한다. 새 plan/tag generation, 아직 사용하지 않은 failover eligibility, 명시적 retry-blocked만 새 snapshot을 얻는다. +- 수정 파일 및 체크리스트: + - [ ] `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py`: batch-local error normalization과 disposal. + - [ ] `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py`: failure/recovery batch 회귀. +- 테스트 작성: `ThroughputQuotaBatchTest`에서 첫 batch key 하나만 unknown, 같은 work unit resume은 probe 0회/pin 유지, 새 generation의 다음 batch available 회복, confirmed provider-quota의 task-local derived exhausted, generic stderr의 unknown 유지를 검증한다. +- 중간 검증: `python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py ThroughputQuotaBatchTest -v` — isolation/pin/recovery case가 PASS해야 한다. + +## 의존 관계 및 구현 순서 + +1. `10+09_target_cap_removal`의 `complete.log`가 PASS여야 한다. +2. 이 task PASS 후 `12+08,11_audit_closure`가 selfcheck와 throughput 결과를 함께 감사한다. + +## 수정 파일 요약 + +| 파일 | 항목 | +|---|---| +| `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py` | REFACTOR-3 | +| `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py` | REFACTOR-3 | + +## 최종 검증 + +- `python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py ThroughputQuotaBatchTest -v` — isolation/pin/recovery 회귀 PASS. +- `python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py BlockerDrainTest -v` — 독립 branch drain 회귀 PASS. +- `python3 -m unittest discover -s agent-ops/skills/project/orchestrate-agent-task-loop/tests -p 'test_*.py'` — fresh process 전체 suite PASS. +- `python3 -m py_compile agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py` — exit 0. +- `git diff --check` — 출력 없음. + +모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. + diff --git a/agent-task/m-agent-task-runtime-target-selector/08+06,07_audit_closure/CODE_REVIEW-cloud-G07.md b/agent-task/m-agent-task-runtime-target-selector/12+08,11_audit_closure/CODE_REVIEW-cloud-G07.md similarity index 96% rename from agent-task/m-agent-task-runtime-target-selector/08+06,07_audit_closure/CODE_REVIEW-cloud-G07.md rename to agent-task/m-agent-task-runtime-target-selector/12+08,11_audit_closure/CODE_REVIEW-cloud-G07.md index 4170282..9824193 100644 --- a/agent-task/m-agent-task-runtime-target-selector/08+06,07_audit_closure/CODE_REVIEW-cloud-G07.md +++ b/agent-task/m-agent-task-runtime-target-selector/12+08,11_audit_closure/CODE_REVIEW-cloud-G07.md @@ -1,4 +1,4 @@ - + # Code Review Reference - TEST @@ -14,7 +14,7 @@ ## 개요 date=2026-07-25 -task=m-agent-task-runtime-target-selector/08+06,07_audit_closure, plan=0, tag=TEST +task=m-agent-task-runtime-target-selector/12+08,11_audit_closure, plan=0, tag=TEST ## Roadmap Targets @@ -33,7 +33,7 @@ task=m-agent-task-runtime-target-selector/08+06,07_audit_closure, plan=0, tag=TE 1. 판정을 append한다. 2. `CODE_REVIEW-cloud-G07.md` → `code_review_cloud_G07_0.log`, `PLAN-local-G07.md` → `plan_local_G07_0.log`로 아카이브한다. -3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-agent-task-runtime-target-selector/08+06,07_audit_closure/`로 이동한다. WARN/FAIL이면 code-review skill이 요구하는 다음 filesystem state를 완전히 작성한다. +3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-agent-task-runtime-target-selector/12+08,11_audit_closure/`로 이동한다. WARN/FAIL이면 code-review skill이 요구하는 다음 filesystem state를 완전히 작성한다. 4. PASS이고 task group이 `m-agent-task-runtime-target-selector`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다. 5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다. @@ -66,7 +66,7 @@ task=m-agent-task-runtime-target-selector/08+06,07_audit_closure, plan=0, tag=TE - [ ] active `PLAN-local-G07.md`를 `plan_local_G07_0.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/08+06,07_audit_closure/`를 `agent-task/archive/YYYY/MM/m-agent-task-runtime-target-selector/08+06,07_audit_closure/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다. +- [ ] PASS이면 active task 디렉터리 `agent-task/m-agent-task-runtime-target-selector/12+08,11_audit_closure/`를 `agent-task/archive/YYYY/MM/m-agent-task-runtime-target-selector/12+08,11_audit_closure/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다. - [ ] PASS이고 task group이 `m-agent-task-runtime-target-selector`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, 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`를 작성하지 않는다. diff --git a/agent-task/m-agent-task-runtime-target-selector/08+06,07_audit_closure/PLAN-local-G07.md b/agent-task/m-agent-task-runtime-target-selector/12+08,11_audit_closure/PLAN-local-G07.md similarity index 94% rename from agent-task/m-agent-task-runtime-target-selector/08+06,07_audit_closure/PLAN-local-G07.md rename to agent-task/m-agent-task-runtime-target-selector/12+08,11_audit_closure/PLAN-local-G07.md index 5e4fb04..fa0dcfb 100644 --- a/agent-task/m-agent-task-runtime-target-selector/08+06,07_audit_closure/PLAN-local-G07.md +++ b/agent-task/m-agent-task-runtime-target-selector/12+08,11_audit_closure/PLAN-local-G07.md @@ -1,10 +1,10 @@ - + # Plan - 동적 selector 정책 audit와 운영 설명 동기화 ## 이 파일을 읽는 구현 에이전트에게 -선행 `06+05_selfcheck_policy`와 `07+06_throughput_policy`의 `complete.log`가 모두 확인된 뒤 구현한다. 구현과 검증을 끝낸 뒤 `CODE_REVIEW-cloud-G07.md`의 구현 에이전트 소유 섹션을 실제 내용과 원문 출력으로 채운다. active 파일은 그대로 두고 review 준비 완료만 보고한다. 종결은 code-review skill 전용이다. 차단되면 정확한 원인, 시도한 명령/출력, 재개 조건만 구현 에이전트 소유 evidence에 기록한다. 사용자에게 질문하거나 user-input 도구를 호출하지 않고, control-plane stop file 생성, 다음 상태 분류, archive/log 이동, `complete.log` 또는 roadmap 수정은 하지 않는다. +선행 `08+07_selfcheck_policy`와 `11+10_unknown_isolation`의 `complete.log`가 모두 확인된 뒤 구현한다. 구현과 검증을 끝낸 뒤 `CODE_REVIEW-cloud-G07.md`의 구현 에이전트 소유 섹션을 실제 내용과 원문 출력으로 채운다. active 파일은 그대로 두고 review 준비 완료만 보고한다. 종결은 code-review skill 전용이다. 차단되면 정확한 원인, 시도한 명령/출력, 재개 조건만 구현 에이전트 소유 evidence에 기록한다. 사용자에게 질문하거나 user-input 도구를 호출하지 않고, control-plane stop file 생성, 다음 상태 분류, archive/log 이동, `complete.log` 또는 roadmap 수정은 하지 않는다. ## 배경 @@ -63,9 +63,10 @@ ### 분할 판단 -- split 정책을 먼저 평가했다. shared task group은 `m-agent-task-runtime-target-selector`이고 topology는 `03 → 04 → 05 → 06 → 07 → 08`이다. -- 현재 `08+06,07_audit_closure`의 predecessor `06+05_selfcheck_policy`와 `07+06_throughput_policy`는 둘 다 active plan만 있고 `complete.log`가 없어 미충족이다. -- 이 task는 `03`~`07`의 최종 cross-policy 검증/문서 closure다. 더 나누면 같은 evidence를 중복 감사하므로 추가 split하지 않는다. +- split 정책을 평가했으며 이 task는 selector 통합, selfcheck, throughput의 최종 cross-policy evidence와 운영 문서를 함께 닫으므로 원본 pair를 유지한다. +- 전체 topology는 `03 → 04 → 05 → 06 → 07 → 08 → 09 → 10 → 11 → 12`다. +- 현재 predecessor `08+07_selfcheck_policy`와 `11+10_unknown_isolation`은 active plan만 있고 `complete.log`가 없어 미충족이다. +- audit를 더 나누면 동일 state/locator/status evidence와 drift 검증을 중복하게 된다. ### 범위 결정 근거 @@ -139,7 +140,7 @@ PLAN lane/G → runtime selector input → persisted adapter/target, ## 의존 관계 및 구현 순서 -1. `06+05_selfcheck_policy`와 `07+06_throughput_policy`의 archived `complete.log`가 모두 PASS여야 한다. 계획 작성 시점에는 둘 다 미충족이다. +1. `08+07_selfcheck_policy`와 `11+10_unknown_isolation`의 archived `complete.log`가 모두 PASS여야 한다. 2. TEST-1로 실제 evidence shape를 고정하고 TEST-2 문서를 그 결과에 맞춘 뒤 TEST-3으로 drift를 닫는다. 3. 이 task PASS 후 `sync-milestone-workstate`가 `audit-tests`를 반영하면 마일스톤 완료 검토가 가능하다. diff --git a/agent-task/m-agent-task-runtime-target-selector/WORK_LOG.md b/agent-task/m-agent-task-runtime-target-selector/WORK_LOG.md new file mode 100644 index 0000000..160a43b --- /dev/null +++ b/agent-task/m-agent-task-runtime-target-selector/WORK_LOG.md @@ -0,0 +1,35 @@ +# Milestone Work Log + +> Dispatcher-owned execution timeline. Workers and reviewers do not edit this file. + +| seq | time | event | task | role | attempt | model | result | locator | +|---:|---|---|---|---|---:|---|---|---| +| 1 | 26-07-26 09:15:49 | START | m-agent-task-runtime-target-selector/05+04_dispatch_route_source | worker | 0 | agy/Gemini 3.6 Flash (Medium) | running | /config/workspace/iop/.git/agent-task-dispatcher/runs/20260726T001549Z__m-agent-task-runtime-target-selector__05__04_dispatch_route_source__p1__worker__a00/locator.json | +| 2 | 26-07-26 09:17:58 | FINISH | m-agent-task-runtime-target-selector/05+04_dispatch_route_source | worker | 0 | agy/Gemini 3.6 Flash (Medium) | succeeded:0 | /config/workspace/iop/.git/agent-task-dispatcher/runs/20260726T001549Z__m-agent-task-runtime-target-selector__05__04_dispatch_route_source__p1__worker__a00/locator.json | +| 3 | 26-07-26 09:17:59 | START | m-agent-task-runtime-target-selector/05+04_dispatch_route_source | review | 0 | codex/gpt-5.6-sol xhigh | running | /config/workspace/iop/.git/agent-task-dispatcher/runs/20260726T001759Z__m-agent-task-runtime-target-selector__05__04_dispatch_route_source__p1__review__a00/locator.json | +| 4 | 26-07-26 09:29:50 | FINISH | m-agent-task-runtime-target-selector/05+04_dispatch_route_source | review | 0 | codex/gpt-5.6-sol xhigh | succeeded:0 | /config/workspace/iop/.git/agent-task-dispatcher/runs/20260726T001759Z__m-agent-task-runtime-target-selector__05__04_dispatch_route_source__p1__review__a00/locator.json | +| 5 | 26-07-26 09:29:51 | START | m-agent-task-runtime-target-selector/05+04_dispatch_route_source | worker | 0 | agy/Gemini 3.6 Flash (Medium) | running | /config/workspace/iop/.git/agent-task-dispatcher/runs/20260726T002951Z__m-agent-task-runtime-target-selector__05__04_dispatch_route_source__p2__worker__a00/locator.json | +| 6 | 26-07-26 09:31:28 | FINISH | m-agent-task-runtime-target-selector/05+04_dispatch_route_source | worker | 0 | agy/Gemini 3.6 Flash (Medium) | succeeded:0 | /config/workspace/iop/.git/agent-task-dispatcher/runs/20260726T002951Z__m-agent-task-runtime-target-selector__05__04_dispatch_route_source__p2__worker__a00/locator.json | +| 7 | 26-07-26 09:31:28 | START | m-agent-task-runtime-target-selector/05+04_dispatch_route_source | review | 0 | codex/gpt-5.6-sol xhigh | running | /config/workspace/iop/.git/agent-task-dispatcher/runs/20260726T003128Z__m-agent-task-runtime-target-selector__05__04_dispatch_route_source__p2__review__a00/locator.json | +| 8 | 26-07-26 09:37:40 | FINISH | m-agent-task-runtime-target-selector/05+04_dispatch_route_source | review | 0 | codex/gpt-5.6-sol xhigh | succeeded:0 | /config/workspace/iop/.git/agent-task-dispatcher/runs/20260726T003128Z__m-agent-task-runtime-target-selector__05__04_dispatch_route_source__p2__review__a00/locator.json | +| 9 | 26-07-26 09:37:41 | START | m-agent-task-runtime-target-selector/06+05_runtime_quota_failover | worker | 0 | agy/Gemini 3.6 Flash (Medium) | running | /config/workspace/iop/.git/agent-task-dispatcher/runs/20260726T003741Z__m-agent-task-runtime-target-selector__06__05_runtime_quota_failover__p0__worker__a00/locator.json | +| 10 | 26-07-26 09:41:40 | FINISH | m-agent-task-runtime-target-selector/06+05_runtime_quota_failover | worker | 0 | agy/Gemini 3.6 Flash (Medium) | succeeded:0 | /config/workspace/iop/.git/agent-task-dispatcher/runs/20260726T003741Z__m-agent-task-runtime-target-selector__06__05_runtime_quota_failover__p0__worker__a00/locator.json | +| 11 | 26-07-26 09:41:40 | START | m-agent-task-runtime-target-selector/06+05_runtime_quota_failover | review | 0 | codex/gpt-5.6-sol xhigh | running | /config/workspace/iop/.git/agent-task-dispatcher/runs/20260726T004140Z__m-agent-task-runtime-target-selector__06__05_runtime_quota_failover__p0__review__a00/locator.json | +| 12 | 26-07-26 09:58:41 | FINISH | m-agent-task-runtime-target-selector/06+05_runtime_quota_failover | review | 0 | codex/gpt-5.6-sol xhigh | succeeded:0 | /config/workspace/iop/.git/agent-task-dispatcher/runs/20260726T004140Z__m-agent-task-runtime-target-selector__06__05_runtime_quota_failover__p0__review__a00/locator.json | +| 13 | 26-07-26 09:58:41 | START | m-agent-task-runtime-target-selector/06+05_runtime_quota_failover | worker | 0 | codex/gpt-5.6-sol xhigh | running | /config/workspace/iop/.git/agent-task-dispatcher/runs/20260726T005841Z__m-agent-task-runtime-target-selector__06__05_runtime_quota_failover__p1__worker__a00/locator.json | +| 14 | 26-07-26 10:23:48 | FINISH | m-agent-task-runtime-target-selector/06+05_runtime_quota_failover | worker | 0 | codex/gpt-5.6-sol xhigh | succeeded:0 | /config/workspace/iop/.git/agent-task-dispatcher/runs/20260726T005841Z__m-agent-task-runtime-target-selector__06__05_runtime_quota_failover__p1__worker__a00/locator.json | +| 15 | 26-07-26 10:23:48 | START | m-agent-task-runtime-target-selector/06+05_runtime_quota_failover | review | 0 | codex/gpt-5.6-sol xhigh | running | /config/workspace/iop/.git/agent-task-dispatcher/runs/20260726T012348Z__m-agent-task-runtime-target-selector__06__05_runtime_quota_failover__p1__review__a00/locator.json | +| 16 | 26-07-26 10:39:18 | FINISH | m-agent-task-runtime-target-selector/06+05_runtime_quota_failover | review | 0 | codex/gpt-5.6-sol xhigh | succeeded:0 | /config/workspace/iop/.git/agent-task-dispatcher/runs/20260726T012348Z__m-agent-task-runtime-target-selector__06__05_runtime_quota_failover__p1__review__a00/locator.json | +| 17 | 26-07-26 10:39:18 | START | m-agent-task-runtime-target-selector/06+05_runtime_quota_failover | worker | 0 | pi/iop/ornith:35b | running | /config/workspace/iop/.git/agent-task-dispatcher/runs/20260726T013918Z__m-agent-task-runtime-target-selector__06__05_runtime_quota_failover__p2__worker__a00/locator.json | +| 18 | 26-07-26 10:41:43 | FINISH | m-agent-task-runtime-target-selector/06+05_runtime_quota_failover | worker | 0 | pi/iop/ornith:35b | succeeded:0 | /config/workspace/iop/.git/agent-task-dispatcher/runs/20260726T013918Z__m-agent-task-runtime-target-selector__06__05_runtime_quota_failover__p2__worker__a00/locator.json | +| 19 | 26-07-26 10:41:43 | START | m-agent-task-runtime-target-selector/06+05_runtime_quota_failover | selfcheck | 0 | pi/iop/ornith:35b | running | /config/workspace/iop/.git/agent-task-dispatcher/runs/20260726T014143Z__m-agent-task-runtime-target-selector__06__05_runtime_quota_failover__p2__selfcheck__a00/locator.json | +| 20 | 26-07-26 10:45:03 | FINISH | m-agent-task-runtime-target-selector/06+05_runtime_quota_failover | selfcheck | 0 | pi/iop/ornith:35b | succeeded:0 | /config/workspace/iop/.git/agent-task-dispatcher/runs/20260726T014143Z__m-agent-task-runtime-target-selector__06__05_runtime_quota_failover__p2__selfcheck__a00/locator.json | +| 21 | 26-07-26 10:45:03 | START | m-agent-task-runtime-target-selector/06+05_runtime_quota_failover | review | 0 | codex/gpt-5.6-sol xhigh | running | /config/workspace/iop/.git/agent-task-dispatcher/runs/20260726T014503Z__m-agent-task-runtime-target-selector__06__05_runtime_quota_failover__p2__review__a00/locator.json | +| 22 | 26-07-26 11:04:05 | FINISH | m-agent-task-runtime-target-selector/06+05_runtime_quota_failover | review | 0 | codex/gpt-5.6-sol xhigh | succeeded:0 | /config/workspace/iop/.git/agent-task-dispatcher/runs/20260726T014503Z__m-agent-task-runtime-target-selector__06__05_runtime_quota_failover__p2__review__a00/locator.json | +| 23 | 26-07-26 11:04:05 | START | m-agent-task-runtime-target-selector/06+05_runtime_quota_failover | worker | 0 | pi/iop/ornith:35b | running | /config/workspace/iop/.git/agent-task-dispatcher/runs/20260726T020405Z__m-agent-task-runtime-target-selector__06__05_runtime_quota_failover__p3__worker__a00/locator.json | +| 24 | 26-07-26 11:05:45 | FINISH | m-agent-task-runtime-target-selector/06+05_runtime_quota_failover | worker | 0 | pi/iop/ornith:35b | succeeded:0 | /config/workspace/iop/.git/agent-task-dispatcher/runs/20260726T020405Z__m-agent-task-runtime-target-selector__06__05_runtime_quota_failover__p3__worker__a00/locator.json | +| 25 | 26-07-26 11:05:45 | START | m-agent-task-runtime-target-selector/06+05_runtime_quota_failover | selfcheck | 0 | pi/iop/ornith:35b | running | /config/workspace/iop/.git/agent-task-dispatcher/runs/20260726T020545Z__m-agent-task-runtime-target-selector__06__05_runtime_quota_failover__p3__selfcheck__a00/locator.json | +| 26 | 26-07-26 11:06:49 | FINISH | m-agent-task-runtime-target-selector/06+05_runtime_quota_failover | selfcheck | 0 | pi/iop/ornith:35b | succeeded:0 | /config/workspace/iop/.git/agent-task-dispatcher/runs/20260726T020545Z__m-agent-task-runtime-target-selector__06__05_runtime_quota_failover__p3__selfcheck__a00/locator.json | +| 27 | 26-07-26 11:06:49 | START | m-agent-task-runtime-target-selector/06+05_runtime_quota_failover | review | 0 | codex/gpt-5.6-sol xhigh | running | /config/workspace/iop/.git/agent-task-dispatcher/runs/20260726T020649Z__m-agent-task-runtime-target-selector__06__05_runtime_quota_failover__p3__review__a00/locator.json | +| 28 | 26-07-26 11:14:30 | FINISH | m-agent-task-runtime-target-selector/06+05_runtime_quota_failover | review | 0 | codex/gpt-5.6-sol xhigh | succeeded:0 | /config/workspace/iop/.git/agent-task-dispatcher/runs/20260726T020649Z__m-agent-task-runtime-target-selector__06__05_runtime_quota_failover__p3__review__a00/locator.json | +| 29 | 26-07-26 11:14:30 | START | m-agent-task-runtime-target-selector/07+06_context_review_recovery | worker | 0 | agy/Gemini 3.6 Flash (Medium) | running | /config/workspace/iop/.git/agent-task-dispatcher/runs/20260726T021430Z__m-agent-task-runtime-target-selector__07__06_context_review_recovery__p0__worker__a00/locator.json | diff --git a/agent-test/dev/edge-smoke.md b/agent-test/dev/edge-smoke.md index 3c6c293..4d42266 100644 --- a/agent-test/dev/edge-smoke.md +++ b/agent-test/dev/edge-smoke.md @@ -3,7 +3,7 @@ test_env: dev test_profile: edge-smoke domain: edge verification_type: smoke -last_rule_updated_at: 2026-07-24 +last_rule_updated_at: 2026-07-26 --- # edge-smoke dev 테스트 @@ -99,9 +99,9 @@ dev-runtime provider pool과 4-node 연결 상태를 점검할 때는 `agent-tes - served model: `Ornith-1.0-35B-GGUF-llamacpp-tp1-Q5_K_M` - capacity baseline: `1` - priority baseline: `0` - - load baseline: backend `cuda`, Q5 GGUF, Q8 KV, ctx size `262144`, `llamacpp_args="--spec-type none -np 1 -cb -fa on -b 512 -ub 256 --kv-unified -ctk q8_0 -ctv q8_0 --temp 0.6 --top-p 0.95 --top-k 20"`, Lemonade `host=0.0.0.0` + - load baseline: backend `cuda`, Q5 GGUF, Q8 KV, ctx size `262144`, `llamacpp_args="--chat-template-kwargs '{\"preserve_thinking\":true}' --kv-unified --min-p 0.00 --repeat-penalty 1.0 --spec-type none --temp 0.6 --top-k 20 --top-p 0.95 -b 512 -cb -ctk q8_0 -ctv q8_0 -fa on -np 1 -ub 256"`, Lemonade `host=0.0.0.0` - long-context admission baseline: `total_context_tokens=262144`, `long_context_capacity=1` - - process baseline: 2026-07-23 기준 실제 IOP Node 부팅 owner는 user Startup의 `IOP Node.lnk`이며, `cmd.exe -> C:/Users/r0bin/iop-field/run-iop-node.cmd -> iop-node.exe serve --config node.yaml`으로 실행한다. 제거된 Task Scheduler `IOP Dev Node RTX5090 Lemonade`는 IOP dev 배포가 생성·재생성하지 않는다. + - process baseline: 2026-07-26 기준 IOP 관련 Windows 부팅 자동 실행은 비활성이고, 수동 `remote-llm-toggle.ps1`이 Lemonade Server, Ornith, IOP Node를 순차 UP/DOWN한다. 제거된 Startup shortcut, Run entry, Task Scheduler, Windows service는 IOP dev 배포가 생성·재생성하지 않는다. - sampling rollout: 2026-07-23 saved recipe와 실제 llama-server process에서 `--temp 0.6 --top-p 0.95 --top-k 20` 적용을 확인했다. - workspace: `C:/Users/r0bin/iop-field` @@ -109,7 +109,7 @@ GX10은 Laguna S 2.1 NVFP4 + DFlash NVFP4를 사용한다. OneXPlayer와 RTX5090 OneXPlayer Lemonade Node는 원격 runner나 Edge host에서 다시 SSH하거나 proxy process로 띄우지 않는다. 현재 작업 호스트에서 OneXPlayer Windows host에 `ssh r0bin@192.168.0.59`로 직접 접속한 뒤 generated PowerShell bootstrap을 실행한다. -RTX5090 Lemonade Node도 원격 runner나 Edge host를 경유하지 않는다. 현재 작업 호스트에서 `ssh iop-dev-rtx5090`으로 public-key batch 접속한다. IOP Node는 user Startup의 `IOP Node.lnk`에서 직접 `run-iop-node.cmd`를 거쳐 실행하며, 별도 `startup.lnk -> startup.bat -> oto -> AHK` 체인은 호스트 자동화용이다. 후자에 남은 `RemoteLLM_mode.ahk`의 Task Scheduler 호출은 제거된 task를 가리키므로 Node startup/배포 검증에 사용하지 않는다. 배포 직후 즉시 재시작은 `run-iop-node.cmd`를 `Win32_Process.Create` 또는 동등한 세션 독립 방식으로 실행한다. Lemonade가 localhost-only로 bind되면 Node 연결이 실패하므로 `0.0.0.0:13305` listener를 확인한다. +RTX5090 Lemonade Node도 원격 runner나 Edge host를 경유하지 않는다. 현재 작업 호스트에서 `ssh iop-dev-rtx5090`으로 public-key batch 접속한다. Windows 부팅 owner는 없으며 `RemoteLLM_mode.ahk`의 유일한 `Run`은 수동 `remote-llm-toggle.ps1` 호출이다. 기본 Toggle은 operator 동작에만 쓰고 자동 검증은 `-Action Status|Up|Down`을 명시한다. UP 완료는 Lemonade `0.0.0.0:13305`, 정확한 Ornith profile, llama slot `1`/`n_ctx=262144`, Node의 Edge 연결을 모두 확인하고, DOWN 완료는 Node/Lemonade/llama process와 port가 모두 사라진 상태로 판정한다. mac-mlx-vllm provider는 mac-codex-node 소속 resource로, Edge host와 같은 macOS host에서 vllm-mlx process로 실행한다. vllm-mlx API는 외부에 직접 노출하지 않고 `127.0.0.1:8002`에 bind하며, Edge OpenAI-compatible adapter가 local bearer header로 호출한다. 운영 파일은 `/Users/toki/agent-work/iop-mlx-vllm/vllm-mlx.pid`, `logs/vllm-mlx.stdout.log`, `logs/vllm-mlx.stderr.log`를 기준으로 한다. Docker와 macOS 여유 메모리를 고려해 capacity는 `2`를 기본선으로 유지한다. diff --git a/agent-test/dev/node-smoke.md b/agent-test/dev/node-smoke.md index 651a674..4aa07fd 100644 --- a/agent-test/dev/node-smoke.md +++ b/agent-test/dev/node-smoke.md @@ -3,7 +3,7 @@ test_env: dev test_profile: node-smoke domain: node verification_type: smoke -last_rule_updated_at: 2026-07-24 +last_rule_updated_at: 2026-07-26 --- # node-smoke dev 테스트 @@ -86,10 +86,10 @@ dev-runtime의 실제 4-node 연결을 점검할 때는 원격 runner `ssh toki@ - served model: `Ornith-1.0-35B-GGUF-llamacpp-tp1-Q5_K_M` - capacity baseline: `1` - priority baseline: `0` - - load baseline: checkpoint `LordNeel/Ornith-1.0-35B-GGUF-llamacpp-tp1:Q5_K_M`, backend `cuda`, ctx size `262144`, `llamacpp_args="--spec-type none -np 1 -cb -fa on -b 512 -ub 256 --kv-unified -ctk q8_0 -ctv q8_0 --temp 0.6 --top-p 0.95 --top-k 20"` + - load baseline: checkpoint `LordNeel/Ornith-1.0-35B-GGUF-llamacpp-tp1:Q5_K_M`, backend `cuda`, ctx size `262144`, `llamacpp_args="--chat-template-kwargs '{\"preserve_thinking\":true}' --kv-unified --min-p 0.00 --repeat-penalty 1.0 --spec-type none --temp 0.6 --top-k 20 --top-p 0.95 -b 512 -cb -ctk q8_0 -ctv q8_0 -fa on -np 1 -ub 256"` - network baseline: Lemonade `host=0.0.0.0`; localhost-only bind는 Node의 provider endpoint 접속 실패를 유발한다. - long-context admission baseline: `total_context_tokens=262144`, `long_context_capacity=1` - - process baseline: 2026-07-23 기준 실제 IOP Node 부팅 owner는 user Startup의 `IOP Node.lnk`이며, `cmd.exe -> C:/Users/r0bin/iop-field/run-iop-node.cmd -> iop-node.exe serve --config node.yaml`으로 실행한다. Task Scheduler `IOP Dev Node RTX5090 Lemonade`는 제거되어 있으며 IOP dev 배포는 이를 생성·재생성하지 않는다. + - process baseline: 2026-07-26 기준 IOP Node, Lemonade Server, `startup.bat -> oto -> AHK` 체인의 Windows 부팅 자동 실행은 모두 비활성이다. 수동 운영 owner는 `C:/Users/r0bin/iop-field/remote-llm-toggle.ps1`이며 UP은 Lemonade Server -> Ornith load -> Node, DOWN은 Node -> Ornith unload -> Lemonade Server 순서다. IOP dev 배포는 Startup shortcut, Run entry, Task Scheduler, Windows service를 생성하지 않는다. - sampling rollout: 2026-07-23 saved recipe와 실제 llama-server process에서 `--temp 0.6 --top-p 0.95 --top-k 20` 적용을 확인했다. - workspace: `C:/Users/r0bin/iop-field` @@ -101,7 +101,7 @@ mac-mlx-vllm provider는 mac-codex-node 소속 resource로, Edge host와 같은 OneXPlayer에서 SSH 세션 안의 `Start-Process`로 `iop-node.exe`를 띄우면 SSH 세션 종료와 함께 process가 정리될 수 있다. dev 반복 배포에서는 `Win32_Process.Create` 또는 동등한 세션 독립 실행 방식으로 `C:/Users/r0bin/iop-field`에서 `iop-node.exe --config node.yaml serve`를 시작하고, WMI/process query와 `iop-node.log`의 `connected to edge` 로그로 유지 여부를 확인한다. -RTX5090은 `ssh iop-dev-rtx5090`으로 batch 접속을 먼저 확인하고 Node process, `iop-node.log`, Edge의 connected node 상태를 함께 확인한다. 별도 user Startup `startup.lnk -> startup.bat -> oto startup.yaml -> AHK _mode.ahk` 체인은 호스트 자동화용이며 IOP Node owner가 아니다. 이 체인의 `RemoteLLM_mode.ahk`는 제거된 `IOP Dev Node RTX5090 Lemonade` Task Scheduler 항목을 호출하므로, Node 부팅 또는 배포 성공의 근거로 사용하지 않는다. 배포 직후 즉시 재시작이 필요하면 `C:/Users/r0bin/iop-field/run-iop-node.cmd`를 `Win32_Process.Create` 또는 동등한 세션 독립 방식으로 실행하고 process와 `connected to edge` 로그를 확인한다. Lemonade 복구 시에는 `lemonade config`의 `host=0.0.0.0`과 `0.0.0.0:13305` listener를 확인한 뒤 Node를 재시작한다. 비밀번호나 개인키 경로는 문서와 실행 로그에 기록하지 않는다. +RTX5090은 `ssh iop-dev-rtx5090`으로 batch 접속을 먼저 확인하고 Node process, toggle status의 `edge_connected`, Control Plane의 connected node 상태를 함께 확인한다. `RemoteLLM_mode.ahk`는 모니터 profile 적용 후 수동 toggle script를 한 번만 실행하며, 이전 Task Scheduler 호출과 직접 Lemonade load 명령은 없다. `startup.lnk`는 `C:/Users/r0bin/iop-field/startup.lnk.pre-manual-remote-llm-20260726.bak`으로 이동되어 AHK가 Windows 부팅에서 실행되지 않는다. 자동화 검증에서는 상태에 따라 반전되는 기본 Toggle 대신 `-Action Status|Up|Down`을 명시하고, `ready`는 Node의 Edge TCP `18084` 연결까지 포함해야 한다. Lemonade 복구 시에는 `0.0.0.0:13305`, 정확한 Ornith profile, llama slot `1`/`n_ctx=262144`를 확인한다. 비밀번호나 개인키 경로는 문서와 실행 로그에 기록하지 않는다. GX10 Laguna 공식 vLLM baseline은 `temperature=0.7`, `top_p=0.95`, model `generation_config`의 `top_k=20`, `tool_call_parser=poolside_v1`, `reasoning_parser=poolside_v1`, `enable_thinking=true`다. 현재 dev는 stock generation prefix가 think block을 즉시 닫는 현상을 막기 위해 `\n` 로컬 템플릿을 추가한다. Pi의 `high` thinking은 `chat_template_kwargs.enable_thinking=true`로 전달하고 이전 reasoning block은 `preserve_thinking=true`로 재사용한다. think smoke는 `high`에서 `thinking_start`/`thinking_delta`/`thinking_end`, `off`에서 thinking event 0개와 최종 text를 확인하고, agentic multi-turn에서는 tool-call 전후 reasoning과 최종 text까지 확인한다. diff --git a/agent-test/inventory-dev.yaml b/agent-test/inventory-dev.yaml index 77f3383..6f8a6a2 100644 --- a/agent-test/inventory-dev.yaml +++ b/agent-test/inventory-dev.yaml @@ -2,7 +2,7 @@ inventory_id: inventory-dev common_inventory: agent-test/inventory.yaml test_env: dev profile: dev-runtime-provider-pool -last_updated_at: "2026-07-25" +last_updated_at: "2026-07-26" source: remote_runner: @@ -1534,7 +1534,7 @@ nodes: iop_node_binary_observed_at: "2026-07-23T10:11:52Z" iop_node_version: "0.1.0" ctx_size: 262144 - llamacpp_args: "--spec-type none -np 1 -cb -fa on -b 512 -ub 256 --kv-unified -ctk q8_0 -ctv q8_0 --temp 0.6 --top-p 0.95 --top-k 20" + llamacpp_args: "--chat-template-kwargs '{\"preserve_thinking\":true}' --kv-unified --min-p 0.00 --repeat-penalty 1.0 --spec-type none --temp 0.6 --top-k 20 --top-p 0.95 -b 512 -cb -ctk q8_0 -ctv q8_0 -fa on -np 1 -ub 256" observed_process_args: "--ctx-size 262144 --port 8001 --jinja --metrics --reasoning-format auto --no-webui --chat-template-kwargs {\"preserve_thinking\":true} --kv-unified --min-p 0.00 --repeat-penalty 1.0 --spec-type none --temp 0.6 --top-k 20 --top-p 0.95 -b 512 -cb -ctk q8_0 -ctv q8_0 -fa on -np 1 -ub 256" observed_total_slots: 1 observed_total_ctx_size: 262144 @@ -1590,27 +1590,97 @@ nodes: concurrency_one_enforced: true overlapping_requests_routed_to_other_providers: 2 windows_process_start: - ownership: user_managed - observed_at: "2026-07-23" - effective_iop_node_startup: - status: present - shortcut: C:/Users/r0bin/AppData/Roaming/Microsoft/Windows/Start Menu/Programs/Startup/IOP Node.lnk - chain: cmd.exe -> C:/Users/r0bin/iop-field/run-iop-node.cmd -> iop-node.exe serve --config node.yaml - lemonade_startup: - status: present - shortcut: C:/Users/r0bin/AppData/Roaming/Microsoft/Windows/Start Menu/Programs/Startup/Lemonade Server.lnk - process: LemonadeServer.exe --silent + ownership: user_managed_manual_toggle + observed_at: "2026-07-26" + boot_autostart: + status: disabled + iop_node_shortcut: absent + lemonade_server_shortcut: absent + automation_shortcut: absent + registry_run_entries_matching_iop_or_lemonade: absent + scheduled_tasks_matching_iop_or_lemonade: absent + windows_services_matching_iop_or_lemonade: absent + unrelated_startup_shortcuts_preserved: true + previous_automation_shortcut: C:/Users/r0bin/AppData/Roaming/Microsoft/Windows/Start Menu/Programs/Startup/startup.lnk + previous_automation_target: C:/Work/automation/iot/script/windows/startup.bat + recoverable_backup: C:/Users/r0bin/iop-field/startup.lnk.pre-manual-remote-llm-20260726.bak task_scheduler: status: absent - task_scheduler_removed_at: "2026-07-20" - legacy_automation_startup: - status: present_but_not_iop_node_owner - shortcut: C:/Users/r0bin/AppData/Roaming/Microsoft/Windows/Start Menu/Programs/Startup/startup.lnk - chain: C:/Work/automation/iot/script/windows/startup.bat -> oto startup.yaml -> ahk/_mode.ahk - stale_iop_reference: RemoteLLM_mode.ahk calls removed Task Scheduler task "IOP Dev Node RTX5090 Lemonade" - verification_rule: do_not_use_this_ahk_chain_as_iop_node_startup_evidence - launcher: C:/Users/r0bin/iop-field/run-iop-node.cmd - immediate_dev_restart: Win32_Process.Create + removed_at: "2026-07-20" + manual_remote_llm_toggle: + script: C:/Users/r0bin/iop-field/remote-llm-toggle.ps1 + script_sha256: 2b3c7084b1105dc2c6809c2cb8b3440034422ef96e3b63bc8b8afa9a551b02e6 + log: C:/Users/r0bin/iop-field/toggle-remote-llm.log + default_action: toggle_by_complete_stack_readiness + pre_review_backup: C:/Users/r0bin/iop-field/toggle-remote-llm.ps1.pre-integrated-review-20260726.bak + readiness_requires: + - LemonadeServer.exe running and health status ok + - exact Ornith model and full saved runtime profile + - public listener 0.0.0.0:13305 + - iop-node.exe running + - established Edge TCP connection to port 18084 + down_completion_requires: + - no iop-node.exe + - no LemonadeServer.exe + - no Lemonade-managed llama-server.exe + - no loaded expected model + - no listener on port 13305 + up_sequence: + - LemonadeServer.exe + - load Ornith-1.0-35B-GGUF-llamacpp-tp1-Q5_K_M + - iop-node.exe serve --config C:/Users/r0bin/iop-field/node.yaml + down_sequence: + - stop iop-node.exe + - unload Ornith-1.0-35B-GGUF-llamacpp-tp1-Q5_K_M + - stop LemonadeServer.exe and residual llama-server.exe + process_start: Win32_Process.Create + invoker: + ahk: C:/Work/automation/iot/script/windows/ahk/RemoteLLM_mode.ahk + ahk_sha256: 2dac2dab58aac7f7c235ed2726b7e0867d7cfb23085ffbbd8625eed3d6ee87f7 + run_count: 1 + direct_lemonade_load: absent + removed_task_scheduler_reference: true + taskbar_pin: + status: pinned + shortcut: C:/Users/r0bin/AppData/Roaming/Microsoft/Internet Explorer/Quick Launch/User Pinned/TaskBar/Remote LLM Toggle.lnk + shortcut_sha256: 4a3a9670d2d626df41c04cf6e155ad83b2298872469d92cb6e8251112fd2b8e7 + start_menu_shortcut: C:/Users/r0bin/AppData/Roaming/Microsoft/Windows/Start Menu/Programs/Lemonade Server/Remote LLM Toggle.lnk + start_menu_shortcut_sha256: 4a3a9670d2d626df41c04cf6e155ad83b2298872469d92cb6e8251112fd2b8e7 + target: C:/Windows/System32/WindowsPowerShell/v1.0/powershell.exe + launches_script: C:/Users/r0bin/iop-field/remote-llm-toggle.ps1 + icon: C:/Users/r0bin/AppData/Roaming/Microsoft/Installer/{FECA419B-2EB4-46C1-8FAE-D392B4059880}/LemonadeIcon,0 + boot_autostart: false + validation: + observed_at: "2026-07-26" + profile_validation: + valid_saved_profile_accepted: passed + malformed_preserve_thinking_json_rejected: passed + wrong_temperature_rejected: passed + wrong_min_p_rejected: passed + default_toggle_down: + result: passed + related_process_count: 0 + listener_13305: absent + health_endpoint: unreachable + edge_connected: false + provider_health: offline + provider_capacity: 0 + explicit_down_already_down: + result: passed + default_toggle_up: + result: passed + stack_ready: true + listener: 0.0.0.0:13305 + model_profile_valid: true + edge_connected: true + provider_health: healthy + provider_capacity: 1 + direct_chat_completions: + result: passed + finish_reason: stop + final_content: READY. + final_state: up + legacy_node_launcher: C:/Users/r0bin/iop-field/run-iop-node.cmd current_reconnect: interval_sec: 10 max_attempts: 10 diff --git a/docs/edge-local-dev-guide.md b/docs/edge-local-dev-guide.md index bd94ed4..1c87ba0 100644 --- a/docs/edge-local-dev-guide.md +++ b/docs/edge-local-dev-guide.md @@ -137,12 +137,12 @@ http://:18081/v1 - dev GX10 `laguna-s:2.1` 공식 sampling baseline은 Laguna S 2.1 모델 카드의 vLLM recipe를 따른다: `temperature=0.7`, `top_p=0.95`, model `generation_config`의 `top_k=20`. - GX10 공식 근거: https://huggingface.co/poolside/Laguna-S-2.1 -- dev OneXPlayer/RTX5090 `ornith:35b` sampling baseline은 `temperature=0.6`, `top_p=0.95`, `top_k=20`을 유지한다. 공식 예제에 없는 `repeat_penalty`는 baseline에 임의로 추가하지 않는다. +- dev OneXPlayer/RTX5090 `ornith:35b` sampling baseline은 `temperature=0.6`, `top_p=0.95`, `top_k=20`을 유지한다. 공식 예제에 없는 non-neutral `repeat_penalty`는 임의로 추가하지 않는다. RTX5090 saved recipe의 `--repeat-penalty 1.0`과 `--min-p 0.00`은 출력을 바꾸지 않는 neutral runtime serialization로만 유지한다. - Ornith 공식 근거: https://huggingface.co/deepreinforce-ai/Ornith-1.0-35B - 이 값은 caller가 sampling field를 생략했을 때 쓰는 provider 기본값이다. caller가 명시한 sampling field가 있으면 요청값이 우선한다. - dev GX10 vLLM은 `poolside/Laguna-S-2.1-NVFP4`와 quantization-matched `Laguna-S-2.1-DFlash-NVFP4`를 사용하고 `--tool-call-parser poolside_v1`, `--reasoning-parser poolside_v1`, `--chat-template /run/iop/laguna-s-2.1-thinking.jinja`, `--default-chat-template-kwargs '{"enable_thinking":true}'`, `--override-generation-config '{"temperature":0.7,"top_p":0.95}'`로 설정한다. - GX10 host의 `/home/toki/iop-gx10-vllm/laguna-s-2.1-thinking.jinja`는 container에 read-only bind한다. stock generation prefix ``가 첫 생성 토큰 ``를 유도해 reasoning이 비는 현상이 재현됐으므로, dev template은 `\n`을 사용한다. -- dev OneXPlayer/RTX5090 Lemonade는 해당 Ornith recipe의 `llamacpp_args`에 `--temp 0.6 --top-p 0.95 --top-k 20`을 저장한다. +- dev OneXPlayer/RTX5090 Lemonade는 해당 Ornith recipe의 `llamacpp_args`에 `--temp 0.6 --top-p 0.95 --top-k 20`을 저장한다. RTX5090 수동 toggle은 이 sampling 값과 함께 `preserve_thinking=true`, unified KV, Q8 KV, neutral min-p/repeat-penalty를 exact profile로 검증한다. - Laguna reasoning은 provider-native `reasoning` field로 나가며 Pi `openai-completions`가 이를 `thinking_delta`로 소비한다. Pi Laguna profile은 thinking level을 `chat_template_kwargs.enable_thinking`으로 전달하고 `preserve_thinking=true`로 이전 assistant reasoning을 유지한다. - 출력 smoke는 같은 요청을 Pi `high`와 `off`로 대조한다. `high`에서는 `thinking_start`/`thinking_delta`/`thinking_end`와 최종 text가, `off`에서는 thinking event 0개와 최종 text가 나와야 한다. agentic multi-turn에서는 tool-call 전후 reasoning, tool result, 최종 text까지 확인한다. - 현재 dev-corp provider-pool device mapping은 `gemma4:26b` -> Mac Studio provider capacity `5`, `ornith:35b` -> DGX Spark 01/02 provider 합산 capacity `8`이다. 세부 endpoint와 runtime args는 `agent-test/inventory-dev-corp.yaml`을 기준으로 한다.