feat: runtime target selector milestones progress, archives, and skill updates

- Update orchestrate-agent-task-loop skill and dispatch scripts
- Add 8 new milestone directories (07+06 through 12+08,11)
- Archive completed tasks for 05+04 and 06+05
- Update test configs, docs, and edge-smoke/node-smoke
- Add WORK_LOG.md for runtime target selector
This commit is contained in:
toki 2026-07-26 12:01:14 +09:00
parent 01ca614ef3
commit 8d782e8fc3
42 changed files with 5221 additions and 133 deletions

View file

@ -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 요청이 첫 생성 토큰으로 `</think>`를 내보내 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`으로 회복되었는가

View file

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

View file

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

View file

@ -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"<!--\s*task=(\S+)\s+plan=(\d+)\s+tag=(\S+)\s*-->")
_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}"
)

View file

@ -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 = "<!-- task=legacy_recovery_test plan=0 tag=API -->\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"<!-- task=selector_dispatch_integration/{unit} plan=0 tag=API -->\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"<!-- task=selector_dispatch_integration/01_unit plan=0 tag=API -->\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("<!-- task=selector_dispatch_integration/01_unit plan=1 tag=API -->\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__":

View file

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

View file

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

View file

@ -0,0 +1,175 @@
<!-- task=m-agent-task-runtime-target-selector/05+04_dispatch_route_source plan=2 tag=REVIEW_REFACTOR -->
# 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
- 발견된 문제: 없음

View file

@ -0,0 +1,157 @@
<!-- task=m-agent-task-runtime-target-selector/05+04_dispatch_route_source plan=1 tag=REFACTOR -->
# 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를 작성한다.

View file

@ -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
- 없음
## 후속 작업
- 없음

View file

@ -0,0 +1,145 @@
<!-- task=m-agent-task-runtime-target-selector/05+04_dispatch_route_source plan=2 tag=REVIEW_REFACTOR -->
# 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`의 구현 에이전트 소유 섹션을 채운다.

View file

@ -0,0 +1,114 @@
<!-- task=m-agent-task-runtime-target-selector/05+04_dispatch_route_source plan=1 tag=REFACTOR -->
# 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`의 구현 에이전트 소유 섹션을 채운다.

View file

@ -0,0 +1,187 @@
<!-- task=m-agent-task-runtime-target-selector/06+05_runtime_quota_failover plan=3 tag=REVIEW_REFACTOR -->
# 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-<milestone-slug>`이면 완료 이벤트 메타데이터를 보고한다. 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-<milestone-slug>`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, 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를 보고한다.

View file

@ -0,0 +1,208 @@
<!-- task=m-agent-task-runtime-target-selector/06+05_runtime_quota_failover plan=2 tag=REVIEW_REFACTOR -->
# 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-<milestone-slug>`이면 완료 이벤트 메타데이터를 보고한다. 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-<milestone-slug>`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, 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를 생성한다.

View file

@ -0,0 +1,188 @@
<!-- task=m-agent-task-runtime-target-selector/06+05_runtime_quota_failover plan=0 tag=REFACTOR -->
# 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:<target>`, 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를 생성한다.

View file

@ -0,0 +1,263 @@
<!-- task=m-agent-task-runtime-target-selector/06+05_runtime_quota_failover plan=1 tag=REVIEW_REFACTOR -->
# 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-<milestone-slug>`이면 완료 이벤트 메타데이터를 보고한다. 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-<milestone-slug>`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, 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 쌍으로 재검증한다.

View file

@ -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
- 없음
## 후속 작업
- 없음

View file

@ -0,0 +1,185 @@
<!-- task=m-agent-task-runtime-target-selector/06+05_runtime_quota_failover plan=1 tag=REVIEW_REFACTOR -->
# 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`의 구현 에이전트 소유 섹션을 채운다.

View file

@ -0,0 +1,127 @@
<!-- task=m-agent-task-runtime-target-selector/06+05_runtime_quota_failover plan=3 tag=REVIEW_REFACTOR -->
# 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`의 구현 에이전트 소유 섹션을 채운다.

View file

@ -0,0 +1,149 @@
<!-- task=m-agent-task-runtime-target-selector/06+05_runtime_quota_failover plan=2 tag=REVIEW_REFACTOR -->
# 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`의 구현 에이전트 소유 섹션을 채운다.

View file

@ -0,0 +1,135 @@
<!-- task=m-agent-task-runtime-target-selector/06+05_runtime_quota_failover plan=0 tag=REFACTOR -->
# 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 <canonical> --command <adapter> --required-cap ... --checked-at <RFC3339>`를 실행한다. policy-owned mapping은 Gemini 3.6 Flash Low/Medium/High에 `overall`과 해당 `model:<canonical>` 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`의 구현 에이전트 소유 섹션을 채운다.

View file

@ -0,0 +1,183 @@
<!-- task=m-agent-task-runtime-target-selector/07+06_context_review_recovery plan=0 tag=REFACTOR -->
# 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 |

View file

@ -0,0 +1,156 @@
<!-- task=m-agent-task-runtime-target-selector/07+06_context_review_recovery plan=0 tag=REFACTOR -->
# 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`의 구현 에이전트 소유 섹션을 채운다.

View file

@ -1,4 +1,4 @@
<!-- 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 -->
# 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`를 작성하지 않는다.

View file

@ -1,10 +1,10 @@
<!-- 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 -->
# 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`를 시작한다.
## 수정 파일 요약

View file

@ -0,0 +1,122 @@
<!-- task=m-agent-task-runtime-target-selector/09+08_admission_quota_batch plan=1 tag=REFACTOR -->
# 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 |

View file

@ -0,0 +1,122 @@
<!-- task=m-agent-task-runtime-target-selector/09+08_admission_quota_batch plan=1 tag=REFACTOR -->
# 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`의 구현 에이전트 소유 섹션을 채운다.

View file

@ -0,0 +1,129 @@
<!-- task=m-agent-task-runtime-target-selector/10+09_target_cap_removal plan=0 tag=REFACTOR -->
# 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 |

View file

@ -0,0 +1,114 @@
<!-- task=m-agent-task-runtime-target-selector/10+09_target_cap_removal plan=0 tag=REFACTOR -->
# 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`의 구현 에이전트 소유 섹션을 채운다.

View file

@ -0,0 +1,136 @@
<!-- task=m-agent-task-runtime-target-selector/11+10_unknown_isolation plan=0 tag=REFACTOR -->
# 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 |

View file

@ -0,0 +1,125 @@
<!-- task=m-agent-task-runtime-target-selector/11+10_unknown_isolation plan=0 tag=REFACTOR -->
# 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`의 구현 에이전트 소유 섹션을 채운다.

View file

@ -1,4 +1,4 @@
<!-- 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 -->
# 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`를 작성하지 않는다.

View file

@ -1,10 +1,10 @@
<!-- 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 -->
# 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`를 반영하면 마일스톤 완료 검토가 가능하다.

View file

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

View file

@ -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`를 기본선으로 유지한다.

View file

@ -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>_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을 즉시 닫는 현상을 막기 위해 `<think>\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까지 확인한다.

View file

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

View file

@ -137,12 +137,12 @@ http://<edge-host>: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 `<think>`가 첫 생성 토큰 `</think>`를 유도해 reasoning이 비는 현상이 재현됐으므로, dev template은 `<think>\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`을 기준으로 한다.