diff --git a/agent-ops/skills/project/orchestrate-agent-task-loop/SKILL.md b/agent-ops/skills/project/orchestrate-agent-task-loop/SKILL.md index 1f13fb6c..d0916a8a 100644 --- a/agent-ops/skills/project/orchestrate-agent-task-loop/SKILL.md +++ b/agent-ops/skills/project/orchestrate-agent-task-loop/SKILL.md @@ -91,7 +91,7 @@ reinterpret a target from task prose or environment variables. Concurrency limits: - Global physical-workspace limit: omitting `max_parallel` caps execution at `3`; explicit `max_parallel=0` is unlimited. A positive value caps unique active task-stage attempts and is not narrowed by `task_group`. The cap applies across worker, self-check, review, and verified external-active attempts in the same physical workspace. -- Pi `ornith:35b`: 3. +- Pi execution has no model-specific dispatcher limit; the global physical-workspace cap applies. - agy: 1. - Official review: no separate review-only limit; subject to the global cap. @@ -131,7 +131,7 @@ After an AGY/Gemini worker exits `0`, apply the same `CODE_REVIEW_PATH` implemen For Pi worker recovery attempts, pass only `Read {PLAN_PATH}. Continue.` without a locator explanation. Pi self-check recovery must preserve the current full-review or checklist-review role and use its concise prompt. For other CLI escalation attempts, pass `Continue from {LOCATOR_PATH}. Check the saved context and current workspace. Keep artifact content in English. Final in Korean.` Preserve the collaboration prohibition and next-state-materialization sentence in official-review escalation and recovery prompts. Do not ask the model to write a separate handoff summary. -When recovering a KST-night `local-G07`–`local-G08` Laguna locator or a terminal `session-stall` locator left by an earlier dispatcher, first require the locator and native session to belong to the current physical workspace. Do not create a fresh session ID for an owned locator. Resume its native session file with `pi --session` and the existing `--session-dir`. For worker recovery pass `Think in English. Keep artifact content in English. Final in Korean. Continue this session and complete the current task.` For interrupted full-review recovery pass `Think in English. Final in Korean. Continue. Keep files in English.` For a checklist-review retry, pass its normal concise prompt while resuming the existing native session. After a dispatcher restart, find the owned locator and resume the same session. Count this same-session restart toward the same stage's 10-consecutive-failure limit. +When recovering a Pi locator whose catalog target enables `runtime.native_session_resume`, first require the locator and native session to belong to the current physical workspace. Do not create a fresh session ID for an owned locator. Resume its native session file with `pi --session` and the existing `--session-dir`. For worker recovery pass `Think in English. Keep artifact content in English. Final in Korean. Continue this session and complete the current task.` For interrupted full-review recovery pass `Think in English. Final in Korean. Continue. Keep files in English.` For a checklist-review retry, pass its normal concise prompt while resuming the existing native session. After a dispatcher restart, find the owned locator and resume the same session. Count this same-session restart toward the same stage's 10-consecutive-failure limit. ## Work-Log Contract @@ -172,7 +172,7 @@ When recovering a KST-night `local-G07`–`local-G08` Laguna locator or a termin - Record an explicit terminal blocker when a checklist-review initial pass plus 10 retries leaves the implementation checklist incomplete, or official review makes no change 10 consecutive times. - While one task recovers or becomes blocked, continue every ready/running task that neither requires it as a predecessor nor collides with its retained workspace claim. Internal recovery or blocking must not trigger an arbitrary complete-candidate rescan. - If review shared-state preflight fails, block only ready review tasks and still start every worker/self-check with a disjoint claim in the same pass. The complete scan after `complete.log` must preserve the existing snapshot rather than reread already running task directories, avoiding races with parallel archive moves that could stop another process. -- For KST-night `local-G07`–`local-G08` Laguna locator `context-limit`/`session-stall`, prefer the Prompt Contract's same-session resume and display `Pi세션연속재시작`. Use a fresh session and `세션응답복구재시도` only for other legacy Pi `session-stall` recovery. +- For a Pi locator whose target enables `runtime.native_session_resume`, prefer the Prompt Contract's same-session resume for `context-limit`/`session-stall` and display `Pi세션연속재시작`. Use a fresh session and `세션응답복구재시도` for Pi targets without that capability. - Do not stop for user review based on filename alone. Recognize a `user-review` terminal blocker only when the active task's `USER_REVIEW.md` contains `상태: USER_REVIEW`, exactly one supported type, a concrete target, non-`없음`/`미정` blocker rationale, unresolved user actions or decisions, and resume conditions that prevent the next safe implementation step. For `milestone-lock`, require a real `agent-roadmap/**/milestones/*.md` target. For `external-execution`, require an exact runner/device/service/access target and evidence that no authorized automatic executor can perform the required verification. If the form is incomplete or conflicts with active PLAN/CODE_REVIEW, block it as a task-state contract error instead. - Recognize `## Code Review Result` (with `Overall Verdict: PASS|WARN|FAIL`) or legacy `## 코드리뷰 결과` (with `종합 판정: PASS|WARN|FAIL`) as the review verdict. If both canonical and legacy headings are present in the same file, fail closed. Never parse the same string in implementation evidence, command output, or example text as the runtime verdict. - Locator/raw logs under `.git/agent-task-dispatcher/runs/` are internal recovery state and may not appear in the normal project tree. Include the `locator=` path emitted when the dispatcher starts an attempt and the task-group `WORK_LOG.md` path in status updates. @@ -282,14 +282,14 @@ When recovering a KST-night `local-G07`–`local-G08` Laguna locator or a termin ------------------------------------------ 작업시작: 03+01_event_contract_unit_tests ------------------------------------------ -model=pi/iop/ornith:35b +model=/ plan=/absolute/path/PLAN-local-G05.md work_log=/absolute/path/WORK_LOG.md ------------------------------------------ 리뷰시작: 03+01_event_contract_unit_tests ------------------------------------------ -model=codex/gpt-5.6-sol xhigh +model=/ review=/absolute/path/CODE_REVIEW-local-G05.md ``` diff --git a/agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py b/agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py index 3aabd90a..e414b759 100644 --- a/agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py +++ b/agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py @@ -78,8 +78,6 @@ AgentSpec = target_specs.AgentSpec effective_reasoning_effort = target_specs.effective_reasoning_effort effective_pi_thinking_level = target_specs.effective_pi_thinking_level pi_display = target_specs.pi_display -agent_spec_from_record = target_specs.agent_spec_from_record -agent_spec_from_locator = target_specs.agent_spec_from_locator PLAN_RE = re.compile(r"^PLAN-(local|cloud)-G(0[1-9]|10)\.md$") REVIEW_RE = re.compile(r"^CODE_REVIEW-(local|cloud)-G(0[1-9]|10)\.md$") @@ -1725,6 +1723,46 @@ def _selector_module(): return module +def _catalog_target_from_runtime_identity( + adapter: str, + model: str, + thinking_level: str | None = None, + reasoning_effort: str | None = None, +): + target = model + if adapter == "pi" and not target.startswith("iop/"): + target = f"iop/{target}" + return _selector_module().policy.canonical_target( + adapter, + target, + thinking_level, + reasoning_effort, + ) + + +def _catalog_target_from_spec(spec: AgentSpec): + return _catalog_target_from_runtime_identity( + spec.cli, + spec.model, + spec.thinking_level, + spec.reasoning_effort, + ) + + +def agent_spec_from_record(record: dict[str, Any]) -> AgentSpec | None: + return target_specs.agent_spec_from_record( + record, + _catalog_target_from_runtime_identity, + ) + + +def agent_spec_from_locator(locator: Path | None) -> AgentSpec | None: + return target_specs.agent_spec_from_locator( + locator, + _catalog_target_from_runtime_identity, + ) + + def _decision_file(task: Task, stage: str) -> Path: path = task.plan if stage == "worker" else task.review if path is None or not path.is_file(): @@ -1740,7 +1778,9 @@ def agent_spec_from_decision(decision: dict[str, Any]) -> AgentSpec: def _spec_from_completing_decision(decision: dict[str, Any]) -> AgentSpec: return target_specs.spec_from_snapshot( - decision, ExecutionDecisionError + decision, + ExecutionDecisionError, + _catalog_target_from_runtime_identity, ) @@ -1932,7 +1972,7 @@ def read_or_preview_stage_decision( try: agent_spec_from_decision(prior) except ExecutionDecisionError: - # Before catalog-routed review selection, the fixed Codex + # Before catalog-routed review selection, the fixed reviewer # policy persisted a different rule/source pair. Re-select # only that known legacy snapshot against the current # catalog; keep fail-closed behavior for all other invalid @@ -2316,11 +2356,6 @@ def completing_decision_selfcheck_stages( selected = completing.get("selected") if not isinstance(selected, dict): return SelfcheckStages(False, False) - if ( - selected.get("adapter") == "pi" - and selected.get("target") == "iop/glm-5.2" - ): - return SelfcheckStages(False, False) try: policy = _selector_module().policy target_id = selected.get("target_id") @@ -2766,7 +2801,7 @@ def terminal_diagnostic(cli: str, channel: str, line: str) -> str | None: ): # Preserve typed terminal fields such as api_error_status=429 and # error=rate_limit. The human-readable result alone is not the - # failure contract and may change between Claude CLI releases. + # failure contract and may change between provider CLI releases. return json.dumps(value, ensure_ascii=False) if event_type == "system" and subtype.startswith("error"): return json.dumps(value, ensure_ascii=False) @@ -3118,18 +3153,18 @@ def attempt_terminal_diagnostics( def promoted_spec(spec: AgentSpec, recovery_count: int) -> AgentSpec | None: - if spec.cli == "agy": - return AgentSpec("claude", "claude-opus-4-8", "claude/claude-opus-4-8 xhigh") - if spec.cli in {"claude", "claude-glm", "opencode"}: - return AgentSpec( - "codex", - "gpt-5.6-terra", - "codex/gpt-5.6-terra high", - reasoning_effort="high", - ) - if spec.cli == "codex" and recovery_count < 1: + current = _catalog_target_from_spec(spec) + if current is None: + return None + if recovery_count < current.same_target_retry_limit: return spec - return None + promoted = _selector_module().policy.promotion_target(current) + if promoted is None: + return None + return target_specs.spec_from_route_target( + promoted, + ExecutionDecisionError, + ) def render_json_line(cli: str, line: str) -> tuple[list[str], str | None]: @@ -3640,7 +3675,7 @@ def external_active_is_live( return False, f"active 증거 없음: {raw_locator}" -def laguna_resume_locator( +def native_session_resume_locator( state: dict[str, Any], *, expected_workspace: Path | None = None, @@ -3676,9 +3711,13 @@ def laguna_resume_locator( ) if not owned: return None + spec = agent_spec_from_record(record) + target = _catalog_target_from_spec(spec) if spec is not None else None if ( - record.get("cli") != "pi" - or not str(record.get("model", "")).startswith("laguna-s") + spec is None + or not spec.local_pi + or target is None + or not target.native_session_resume or record.get("failure_class") not in {"context-limit", "session-stall"} or record.get("status") != "failed" ): @@ -3801,7 +3840,7 @@ def build_command( if spec.cli == "agy": return [ # `--print` consumes its immediately following argument as the prompt. - # Keeping the timeout first makes Gemini answer the literal flag instead. + # Keeping the timeout first makes the selected model answer the flag instead. "agy", "--print", prompt, "--print-timeout", "8h", "--model", spec.model, "--dangerously-skip-permissions", "--log-file", str(attempt_dir / "agy-cli.log"), ] @@ -4670,7 +4709,7 @@ async def run_escalating( spec = initial recovery_key = recovery_state_key or role previous_locator = initial_resume_locator - codex_recovery_count = 0 + same_target_recovery_count = 0 codex_session_stall_retries = 0 review_control_retries = 0 pi_recovery_retries = 0 @@ -4738,7 +4777,7 @@ async def run_escalating( ) if legacy_recovery is not None and legacy_recovery.role == role: failed_spec = failed_spec_from_recovery(legacy_recovery) - next_spec = promoted_spec(failed_spec, codex_recovery_count) + next_spec = promoted_spec(failed_spec, same_target_recovery_count) if next_spec is not None: banner( "모델승격", @@ -4949,8 +4988,10 @@ async def run_escalating( ) return False, locator if spec.local_pi: + runtime_target = _catalog_target_from_spec(spec) if ( - spec.model.startswith("laguna-s") + runtime_target is not None + and runtime_target.native_session_resume and failure in {"context-limit", "session-stall"} ): pi_recovery_retries += 1 @@ -5139,7 +5180,7 @@ async def run_escalating( spec = next_spec previous_locator = locator continue - next_spec = promoted_spec(spec, codex_recovery_count) + next_spec = promoted_spec(spec, same_target_recovery_count) if next_spec is None: terminal_recovery_retries += 1 banner( @@ -5154,8 +5195,8 @@ async def run_escalating( previous_locator = locator await asyncio.sleep(min(30, 2 ** min(terminal_recovery_retries, 5))) continue - if spec.cli == "codex": - codex_recovery_count += 1 + if next_spec == spec: + same_target_recovery_count += 1 banner( "모델승격", task.name, @@ -6900,7 +6941,7 @@ async def dispatch_with_store( last_wait[task.name] = active_key continue if not args.dry_run: - resume_locator = laguna_resume_locator( + resume_locator = native_session_resume_locator( state, expected_workspace=store.workspace, expected_workspace_id=store.workspace_id, diff --git a/agent-ops/skills/project/orchestrate-agent-task-loop/scripts/execution_target_catalog.json b/agent-ops/skills/project/orchestrate-agent-task-loop/scripts/execution_target_catalog.json index e83b8557..1e31ebf3 100644 --- a/agent-ops/skills/project/orchestrate-agent-task-loop/scripts/execution_target_catalog.json +++ b/agent-ops/skills/project/orchestrate-agent-task-loop/scripts/execution_target_catalog.json @@ -19,6 +19,19 @@ "full_review": true, "checklist_review": true }, + "thinking_level": "high", + "runtime": { + "native_session_resume": true + } + }, + "legacy-pi-selfcheck-disabled": { + "adapter": "pi", + "target": "iop/glm-5.2", + "execution_class": "local_model", + "selfcheck": { + "full_review": false, + "checklist_review": false + }, "thinking_level": "high" }, "agy-gemini-low": { @@ -94,7 +107,7 @@ }, "claude-opus-xhigh": { "adapter": "claude", - "target": "claude-opus-4-8", + "target": "claude-opus-5", "execution_class": "cloud_model", "selfcheck": { "full_review": false, @@ -120,7 +133,10 @@ "full_review": false, "checklist_review": false }, - "reasoning_effort": "xhigh" + "reasoning_effort": "xhigh", + "runtime": { + "same_target_retry_limit": 1 + } }, "codex-sol-xhigh": { "adapter": "codex", @@ -130,7 +146,10 @@ "full_review": false, "checklist_review": false }, - "reasoning_effort": "xhigh" + "reasoning_effort": "xhigh", + "runtime": { + "same_target_retry_limit": 1 + } }, "codex-terra-high": { "adapter": "codex", @@ -140,7 +159,10 @@ "full_review": false, "checklist_review": false }, - "reasoning_effort": "high" + "reasoning_effort": "high", + "runtime": { + "same_target_retry_limit": 1 + } } }, "lanes": { @@ -592,6 +614,14 @@ } }, "promotions": { - "claude-opus-xhigh": "codex-terra-high" + "agy-gemini-low": "claude-opus-xhigh", + "agy-gemini-medium": "claude-opus-xhigh", + "agy-gemini-high": "claude-opus-xhigh", + "opencode-glm-medium": "codex-terra-high", + "opencode-glm-high": "codex-terra-high", + "opencode-glm-max": "codex-terra-high", + "legacy-claude-glm": "codex-terra-high", + "claude-opus-xhigh": "codex-terra-high", + "claude-haiku-xhigh": "codex-terra-high" } } diff --git a/agent-ops/skills/project/orchestrate-agent-task-loop/scripts/execution_target_contract.py b/agent-ops/skills/project/orchestrate-agent-task-loop/scripts/execution_target_contract.py index 52360807..e53cde5e 100644 --- a/agent-ops/skills/project/orchestrate-agent-task-loop/scripts/execution_target_contract.py +++ b/agent-ops/skills/project/orchestrate-agent-task-loop/scripts/execution_target_contract.py @@ -39,6 +39,10 @@ def validate_target_contract(target, path: str, error_type) -> None: raise error_type( f"{path}: pi target cannot set reasoning_effort or command_model" ) + elif target.native_session_resume: + raise error_type( + f"{path}: native_session_resume is only valid for pi" + ) elif target.thinking_level is not None: raise error_type(f"{path}: thinking_level is only valid for pi") if target.adapter == "agy" and ( diff --git a/agent-ops/skills/project/orchestrate-agent-task-loop/scripts/execution_target_policy.py b/agent-ops/skills/project/orchestrate-agent-task-loop/scripts/execution_target_policy.py index 62817dca..64559f89 100644 --- a/agent-ops/skills/project/orchestrate-agent-task-loop/scripts/execution_target_policy.py +++ b/agent-ops/skills/project/orchestrate-agent-task-loop/scripts/execution_target_policy.py @@ -40,6 +40,8 @@ class RouteTarget: thinking_level: str | None = None reasoning_effort: str | None = None command_model: str | None = None + native_session_resume: bool = False + same_target_retry_limit: int = 0 catalog_id: str | None = None @@ -129,6 +131,7 @@ def _target_from_config(target_id: str, value: object) -> RouteTarget: "thinking_level", "reasoning_effort", "command_model", + "runtime", } unknown = sorted(set(item) - allowed) if unknown: @@ -177,6 +180,27 @@ def _target_from_config(target_id: str, value: object) -> RouteTarget: command_model = item.get("command_model") if command_model is not None: command_model = _non_empty_string(command_model, f"{path}.command_model") + runtime = _object(item.get("runtime", {}), f"{path}.runtime") + runtime_fields = {"native_session_resume", "same_target_retry_limit"} + unknown_runtime = sorted(set(runtime) - runtime_fields) + if unknown_runtime: + raise CatalogError( + f"{path}.runtime has unknown fields: {unknown_runtime}" + ) + native_session_resume = runtime.get("native_session_resume", False) + if not isinstance(native_session_resume, bool): + raise CatalogError( + f"{path}.runtime.native_session_resume must be a boolean" + ) + same_target_retry_limit = runtime.get("same_target_retry_limit", 0) + if ( + isinstance(same_target_retry_limit, bool) + or not isinstance(same_target_retry_limit, int) + or same_target_retry_limit < 0 + ): + raise CatalogError( + f"{path}.runtime.same_target_retry_limit must be a non-negative integer" + ) target = RouteTarget( adapter=_non_empty_string(item.get("adapter"), f"{path}.adapter"), target=_non_empty_string(item.get("target"), f"{path}.target"), @@ -195,6 +219,8 @@ def _target_from_config(target_id: str, value: object) -> RouteTarget: f"{path}.reasoning_effort", ), command_model=command_model, + native_session_resume=native_session_resume, + same_target_retry_limit=same_target_retry_limit, catalog_id=target_id, ) target_contract.validate_target_contract(target, path, CatalogError) @@ -412,58 +438,11 @@ def reload_catalog(path: Path = CATALOG_PATH) -> ExecutionTargetCatalog: pinned decision; the next stage observes the newest self-check switches. """ catalog = load_catalog(path) - compatibility_ids = ( - "pi-ornith-high", - "pi-laguna-high", - "agy-gemini-low", - "agy-gemini-medium", - "agy-gemini-high", - "opencode-glm-medium", - "opencode-glm-high", - "opencode-glm-max", - "legacy-claude-glm", - "claude-opus-xhigh", - "claude-haiku-xhigh", - "codex-spark-xhigh", - "codex-sol-xhigh", - "codex-terra-high", - ) - missing = [ - target_id - for target_id in compatibility_ids - if target_id not in catalog.targets - ] - if missing: - raise CatalogError( - "catalog is missing compatibility targets required by the " - f"dispatcher: {missing}" - ) - compatibility_targets = { - target_id: catalog.targets[target_id] for target_id in compatibility_ids - } global CATALOG, CATALOG_REVISION, CATALOG_TARGETS_BY_ID, CANONICAL_TARGETS - global PI_ORNITH, PI_LAGUNA, AGY_GEMINI_LOW, AGY_GEMINI_MEDIUM - global AGY_GEMINI_HIGH, OPENCODE_GLM_MEDIUM, OPENCODE_GLM_HIGH - global OPENCODE_GLM_MAX, CLAUDE_GLM, CLAUDE_OPUS, CLAUDE_HAIKU_XHIGH - global CODEX_SPARK_XHIGH, CODEX_SOL_XHIGH, CODEX_TERRA_HIGH CATALOG = catalog CATALOG_REVISION = catalog.revision CATALOG_TARGETS_BY_ID = catalog.targets CANONICAL_TARGETS = tuple(catalog.targets.values()) - PI_ORNITH = compatibility_targets["pi-ornith-high"] - PI_LAGUNA = compatibility_targets["pi-laguna-high"] - AGY_GEMINI_LOW = compatibility_targets["agy-gemini-low"] - AGY_GEMINI_MEDIUM = compatibility_targets["agy-gemini-medium"] - AGY_GEMINI_HIGH = compatibility_targets["agy-gemini-high"] - OPENCODE_GLM_MEDIUM = compatibility_targets["opencode-glm-medium"] - OPENCODE_GLM_HIGH = compatibility_targets["opencode-glm-high"] - OPENCODE_GLM_MAX = compatibility_targets["opencode-glm-max"] - CLAUDE_GLM = compatibility_targets["legacy-claude-glm"] - CLAUDE_OPUS = compatibility_targets["claude-opus-xhigh"] - CLAUDE_HAIKU_XHIGH = compatibility_targets["claude-haiku-xhigh"] - CODEX_SPARK_XHIGH = compatibility_targets["codex-spark-xhigh"] - CODEX_SOL_XHIGH = compatibility_targets["codex-sol-xhigh"] - CODEX_TERRA_HIGH = compatibility_targets["codex-terra-high"] return catalog @@ -474,48 +453,30 @@ def catalog_target(target_id: str) -> RouteTarget: raise CatalogError(f"unknown catalog target: {target_id}") from exc -# Compatibility names remain for persisted-state recovery and focused driver tests. -# Lane membership and order live only in execution_target_catalog.json. -PI_ORNITH = catalog_target("pi-ornith-high") -PI_LAGUNA = catalog_target("pi-laguna-high") -AGY_GEMINI_LOW = catalog_target("agy-gemini-low") -AGY_GEMINI_MEDIUM = catalog_target("agy-gemini-medium") -AGY_GEMINI_HIGH = catalog_target("agy-gemini-high") -OPENCODE_GLM_MEDIUM = catalog_target("opencode-glm-medium") -OPENCODE_GLM_HIGH = catalog_target("opencode-glm-high") -OPENCODE_GLM_MAX = catalog_target("opencode-glm-max") -CLAUDE_GLM = catalog_target("legacy-claude-glm") -CLAUDE_OPUS = catalog_target("claude-opus-xhigh") -CLAUDE_HAIKU_XHIGH = catalog_target("claude-haiku-xhigh") -CODEX_SPARK_XHIGH = catalog_target("codex-spark-xhigh") -CODEX_SOL_XHIGH = catalog_target("codex-sol-xhigh") -CODEX_TERRA_HIGH = catalog_target("codex-terra-high") - - def canonical_target( adapter: str, target: str, thinking_level: str | None = None, reasoning_effort: str | None = None, ) -> RouteTarget | None: - """Resolve one catalog target, accepting pre-catalog implicit defaults.""" - if adapter == "pi" and thinking_level is None: - thinking_level = "high" - if adapter in {"claude", "claude-glm", "codex"} and reasoning_effort is None: - reasoning_effort = "xhigh" - return next( - ( - candidate - for candidate in CANONICAL_TARGETS - if ( - candidate.adapter == adapter - and candidate.target == target - and candidate.thinking_level == thinking_level - and candidate.reasoning_effort == reasoning_effort + """Resolve one unambiguous catalog target from its runtime identity.""" + matches = tuple( + candidate + for candidate in CANONICAL_TARGETS + if ( + candidate.adapter == adapter + and candidate.target == target + and ( + thinking_level is None + or candidate.thinking_level == thinking_level ) - ), - None, + and ( + reasoning_effort is None + or candidate.reasoning_effort == reasoning_effort + ) + ) ) + return matches[0] if len(matches) == 1 else None def promotion_target(current: RouteTarget) -> RouteTarget | None: diff --git a/agent-ops/skills/project/orchestrate-agent-task-loop/scripts/execution_target_specs.py b/agent-ops/skills/project/orchestrate-agent-task-loop/scripts/execution_target_specs.py index f27fe096..0dda2bfc 100644 --- a/agent-ops/skills/project/orchestrate-agent-task-loop/scripts/execution_target_specs.py +++ b/agent-ops/skills/project/orchestrate-agent-task-loop/scripts/execution_target_specs.py @@ -37,23 +37,43 @@ def pi_display(model: str, thinking_level: str | None) -> str: return f"pi/iop/{model}{suffix}" -def agent_spec_from_record(record: dict[str, Any]) -> AgentSpec | None: +def agent_spec_from_record( + record: dict[str, Any], + target_resolver=None, +) -> AgentSpec | None: cli = str(record.get("cli") or "") model = str(record.get("model") or "") if not cli or not model: return None reasoning_effort = record.get("reasoning_effort") thinking_level = record.get("thinking_level") - command_model = record.get("command_model") + selected = record.get("selected") + selected = selected if isinstance(selected, dict) else {} + command_model = record.get("command_model") or selected.get("command_model") reasoning_effort = ( str(reasoning_effort) if reasoning_effort is not None else None ) thinking_level = str(thinking_level) if thinking_level is not None else None command_model = str(command_model) if command_model is not None else None - if cli == "claude-glm" and model == "glm-5.2" and not command_model: - command_model = "sonnet" - if cli == "opencode" and model == "glm-5.2" and not command_model: - command_model = "iop-glm/glm-5.2" + canonical = None + if target_resolver is not None: + resolver_reasoning = reasoning_effort or ( + "max" if cli == "opencode" else None + ) + try: + canonical = target_resolver( + cli, + model, + thinking_level, + resolver_reasoning, + ) + except (AttributeError, TypeError, ValueError): + canonical = None + if canonical is not None: + if command_model is None: + command_model = canonical.command_model + if reasoning_effort is None: + reasoning_effort = canonical.reasoning_effort if cli in {"codex", "claude", "claude-glm", "opencode"}: effort = reasoning_effort or ("max" if cli == "opencode" else "xhigh") display = f"{cli}/{model} {effort}" @@ -72,14 +92,18 @@ def agent_spec_from_record(record: dict[str, Any]) -> AgentSpec | None: ) -def agent_spec_from_locator(locator: Path | None) -> AgentSpec | None: +def agent_spec_from_locator(locator: Path | None, target_resolver=None) -> AgentSpec | None: if locator is None: return None try: record = json.loads(locator.read_text(encoding="utf-8")) except (OSError, json.JSONDecodeError): return None - return agent_spec_from_record(record) if isinstance(record, dict) else None + return ( + agent_spec_from_record(record, target_resolver) + if isinstance(record, dict) + else None + ) def _selected_schema(decision: dict[str, Any], error_type): @@ -133,7 +157,7 @@ def _validate_promotion_path( raise error_type("selector promotion path tail이 selected와 다르다") -def _spec_from_canonical(canonical, error_type) -> AgentSpec: +def spec_from_route_target(canonical, error_type) -> AgentSpec: adapter = canonical.adapter target = canonical.target if adapter == "pi": @@ -190,7 +214,11 @@ def agent_spec_from_decision( isinstance(catalog, dict) and catalog.get("revision") != selector.policy.CATALOG.revision ): - return spec_from_snapshot(decision, error_type) + return spec_from_snapshot( + decision, + error_type, + selector.policy.canonical_target, + ) evaluated_at = selector.datetime.fromisoformat( decision["decision"]["evaluated_at"] ) @@ -215,7 +243,7 @@ def agent_spec_from_decision( _validate_promotion_path( decision, canonical, initial_keys, selector, error_type ) - return _spec_from_canonical(canonical, error_type) + return spec_from_route_target(canonical, error_type) def _validate_snapshot_contract( @@ -230,11 +258,7 @@ def _validate_snapshot_contract( raise error_type( f"Pi completing decision target이 iop/ prefix가 아니다: {target}" ) - model = target.removeprefix("iop/") - glm_cloud = model == "glm-5.2" - legacy_glm = glm_cloud and execution_class == "local_model" and selfcheck - expected = ("cloud_model", False) if glm_cloud else ("local_model", True) - if not legacy_glm and (execution_class, selfcheck) != expected: + if (execution_class, selfcheck) != ("local_model", True): raise error_type( "Pi completing decision execution/selfcheck 계약이 유효하지 않다: " f"target={target} execution_class={execution_class} " @@ -252,7 +276,11 @@ def _validate_snapshot_contract( ) -def spec_from_snapshot(decision: dict[str, Any], error_type) -> AgentSpec: +def spec_from_snapshot( + decision: dict[str, Any], + error_type, + target_resolver=None, +) -> AgentSpec: """Build a spec from the target snapshot pinned in a persisted decision.""" selected, adapter, target, execution_class, selfcheck = _selected_schema( decision, error_type @@ -260,6 +288,25 @@ def spec_from_snapshot(decision: dict[str, Any], error_type) -> AgentSpec: thinking = selected.get("thinking_level") reasoning = selected.get("reasoning_effort") command_model = selected.get("command_model") + canonical = None + if target_resolver is not None: + resolver_reasoning = reasoning or ( + "max" if adapter == "opencode" else None + ) + try: + canonical = target_resolver( + adapter, + target, + thinking, + resolver_reasoning, + ) + except (AttributeError, TypeError, ValueError): + canonical = None + if canonical is not None: + if command_model is None: + command_model = canonical.command_model + if reasoning is None: + reasoning = canonical.reasoning_effort _validate_snapshot_contract( adapter, target, execution_class, selfcheck, error_type ) @@ -277,11 +324,15 @@ def spec_from_snapshot(decision: dict[str, Any], error_type) -> AgentSpec: thinking_level=thinking, ) if adapter == "claude-glm": + if command_model is None: + raise error_type( + "claude-glm completing decision에 command_model이 없다" + ) return AgentSpec( adapter, target, f"{adapter}/{target} xhigh", - command_model=str(command_model or "sonnet"), + command_model=str(command_model), ) if adapter == "opencode": if reasoning is not None and reasoning not in {"medium", "high", "max"}: @@ -289,13 +340,17 @@ def spec_from_snapshot(decision: dict[str, Any], error_type) -> AgentSpec: "opencode completing decision reasoning_effort가 유효하지 않다: " f"{reasoning!r}" ) + if command_model is None: + raise error_type( + "opencode completing decision에 command_model이 없다" + ) effort = str(reasoning or "max") return AgentSpec( adapter, target, f"{adapter}/{target} {effort}", reasoning_effort=effort, - command_model=str(command_model or target), + command_model=str(command_model), ) effort = reasoning or ("xhigh" if adapter in {"claude", "codex"} else None) suffix = f" {effort}" if effort else "" diff --git a/agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py b/agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py index 7ce34f27..9ea28d17 100644 --- a/agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py +++ b/agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py @@ -52,7 +52,7 @@ def write_legacy_quota_attempts( task: dispatch.Task, *, cli: str = "claude", - model: str = "claude-opus-4-8", + model: str = "claude-opus-5", reasoning_effort: str | None = "xhigh", dispatcher_sha256: str = "older-dispatcher", ) -> list[Path]: @@ -106,6 +106,19 @@ def write_legacy_quota_attempts( class CommandConstructionTest(unittest.TestCase): + def test_legacy_opencode_record_resolves_command_model_from_catalog(self): + target = dispatch._selector_module().policy.catalog_target( + "opencode-glm-max" + ) + spec = dispatch.agent_spec_from_record( + {"cli": target.adapter, "model": target.target} + ) + + self.assertIsNotNone(spec) + assert spec is not None + self.assertEqual(spec.reasoning_effort, "max") + self.assertEqual(spec.command_model, target.command_model) + def test_agy_print_receives_prompt_before_timeout_option(self): with tempfile.TemporaryDirectory() as temporary: workspace = Path(temporary) @@ -456,7 +469,7 @@ class TaskStageTest(unittest.TestCase): "stage": "worker", "selected": { "adapter": "claude", - "target": "claude-opus-4-8", + "target": "claude-opus-5", "execution_class": "cloud_model", "selfcheck_required": False, }, @@ -489,8 +502,8 @@ class TaskStageTest(unittest.TestCase): expected = { 5: ("pi", "ornith:35b", True), 6: ("pi", "ornith:35b", True), - 9: ("claude", "claude-opus-4-8", False), - 10: ("claude", "claude-opus-4-8", False), + 9: ("claude", "claude-opus-5", False), + 10: ("claude", "claude-opus-5", False), } for grade, (cli, model, local_pi) in expected.items(): with self.subTest(grade=grade): @@ -569,7 +582,7 @@ class CompletingTargetSelfcheckTest(unittest.IsolatedAsyncioTestCase): _CLOUD_CASES = ( ("agy", "Gemini 3.6 Flash (Medium)"), - ("claude", "claude-opus-4-8"), + ("claude", "claude-opus-5"), ("codex", "gpt-5.6-sol"), ) @@ -1080,7 +1093,7 @@ class CompletingTargetSelfcheckTest(unittest.IsolatedAsyncioTestCase): "stage": "worker", "selected": { "adapter": "claude", - "target": "claude-opus-4-8", + "target": "claude-opus-5", "execution_class": "cloud_model", "selfcheck_required": False, }, @@ -1089,7 +1102,7 @@ class CompletingTargetSelfcheckTest(unittest.IsolatedAsyncioTestCase): task, worker_done=True, worker_cli="claude", - worker_model="claude-opus-4-8", + worker_model="claude-opus-5", completing_decision=cloud_decision, execution_class="cloud_model", selfcheck_done=True, @@ -1624,7 +1637,7 @@ class CompletingTargetSelfcheckTest(unittest.IsolatedAsyncioTestCase): "stage": "worker", "selected": { "adapter": "claude", - "target": "claude-opus-4-8", + "target": "claude-opus-5", "execution_class": "cloud_model", "selfcheck_required": True, }, @@ -1688,7 +1701,7 @@ class CompletingTargetSelfcheckTest(unittest.IsolatedAsyncioTestCase): "stage": "worker", "selected": { "adapter": 123, - "target": "claude-opus-4-8", + "target": "claude-opus-5", "execution_class": "cloud_model", "selfcheck_required": False, }, @@ -1714,7 +1727,7 @@ class CompletingTargetSelfcheckTest(unittest.IsolatedAsyncioTestCase): "stage": "worker", "selected": { "adapter": "claude", - "target": "claude-opus-4-8", + "target": "claude-opus-5", "execution_class": None, "selfcheck_required": False, }, @@ -1728,7 +1741,7 @@ class CompletingTargetSelfcheckTest(unittest.IsolatedAsyncioTestCase): "stage": "worker", "selected": { "adapter": "", - "target": "claude-opus-4-8", + "target": "claude-opus-5", "execution_class": "cloud_model", "selfcheck_required": False, }, @@ -1779,7 +1792,7 @@ class CompletingTargetSelfcheckTest(unittest.IsolatedAsyncioTestCase): decision = { "selected": { "adapter": "claude", - "target": "claude-opus-4-8", + "target": "claude-opus-5", "execution_class": "cloud_model", "selfcheck_required": True, }, @@ -1835,7 +1848,7 @@ class CompletingTargetSelfcheckTest(unittest.IsolatedAsyncioTestCase): "stage": "worker", "selected": { "adapter": "claude", - "target": "claude-opus-4-8", + "target": "claude-opus-5", "execution_class": "cloud_model", "selfcheck_required": False, }, @@ -3272,8 +3285,8 @@ class WorkLogInvokeIntegrationTest(unittest.IsolatedAsyncioTestCase): ] spec = dispatch.AgentSpec( "claude", - "claude-opus-4-8", - "claude/claude-opus-4-8 xhigh", + "claude-opus-5", + "claude/claude-opus-5 xhigh", ) try: with mock.patch.object( @@ -3342,8 +3355,8 @@ class WorkLogInvokeIntegrationTest(unittest.IsolatedAsyncioTestCase): ] spec = dispatch.AgentSpec( "claude", - "claude-opus-4-8", - "claude/claude-opus-4-8 xhigh", + "claude-opus-5", + "claude/claude-opus-5 xhigh", ) try: with mock.patch.object( @@ -3730,8 +3743,8 @@ class ReviewControlTest(unittest.TestCase): def test_claude_promotion_targets_terra_high(self): claude = dispatch.AgentSpec( "claude", - "claude-opus-4-8", - "claude/claude-opus-4-8 xhigh", + "claude-opus-5", + "claude/claude-opus-5 xhigh", ) promoted = dispatch.promoted_spec(claude, recovery_count=0) @@ -3776,8 +3789,8 @@ class ReviewControlTest(unittest.TestCase): ) claude = dispatch.AgentSpec( "claude", - "claude-opus-4-8", - "claude/claude-opus-4-8 xhigh", + "claude-opus-5", + "claude/claude-opus-5 xhigh", ) haiku = dispatch.AgentSpec( "claude", @@ -4371,8 +4384,9 @@ class ReviewRetryTest(unittest.IsolatedAsyncioTestCase): task = self.make_task(root) claude = dispatch.AgentSpec( "claude", - "claude-opus-4-8", - "claude/claude-opus-4-8 xhigh", + "claude-opus-5", + "claude/claude-opus-5 xhigh", + reasoning_effort="xhigh", ) terra = dispatch.AgentSpec( "codex", @@ -4415,8 +4429,9 @@ class ReviewRetryTest(unittest.IsolatedAsyncioTestCase): ) claude = dispatch.AgentSpec( "claude", - "claude-opus-4-8", - "claude/claude-opus-4-8 xhigh", + "claude-opus-5", + "claude/claude-opus-5 xhigh", + reasoning_effort="xhigh", ) terra = dispatch.AgentSpec( "codex", @@ -4461,8 +4476,9 @@ class ReviewRetryTest(unittest.IsolatedAsyncioTestCase): task = self.make_task(root) claude = dispatch.AgentSpec( "claude", - "claude-opus-4-8", - "claude/claude-opus-4-8 xhigh", + "claude-opus-5", + "claude/claude-opus-5 xhigh", + reasoning_effort="xhigh", ) locators = [root / "locator-0.json", root / "locator-1.json"] with ( @@ -4606,8 +4622,9 @@ class ReviewRetryTest(unittest.IsolatedAsyncioTestCase): ) claude = dispatch.AgentSpec( "claude", - "claude-opus-4-8", - "claude/claude-opus-4-8 xhigh", + "claude-opus-5", + "claude/claude-opus-5 xhigh", + reasoning_effort="xhigh", ) terra = dispatch.AgentSpec( "codex", @@ -4848,7 +4865,7 @@ class ReviewRetryTest(unittest.IsolatedAsyncioTestCase): encoding="utf-8", ) self.assertEqual( - dispatch.laguna_resume_locator( + dispatch.native_session_resume_locator( {"active_locator": str(locator)} ), locator, @@ -7176,7 +7193,7 @@ class WorkLogArchiveTest(unittest.TestCase): encoding="utf-8", ) self.assertIsNone( - dispatch.laguna_resume_locator( + dispatch.native_session_resume_locator( {"active_locator": str(foreign_locator)}, expected_workspace=store.workspace, expected_workspace_id=store.workspace_id, @@ -7203,7 +7220,7 @@ class WorkLogArchiveTest(unittest.TestCase): encoding="utf-8", ) self.assertEqual( - dispatch.laguna_resume_locator( + dispatch.native_session_resume_locator( {"active_locator": str(current_locator)}, expected_workspace=store.workspace, expected_workspace_id=store.workspace_id, @@ -7218,7 +7235,7 @@ class WorkLogArchiveTest(unittest.TestCase): encoding="utf-8", ) self.assertIsNone( - dispatch.laguna_resume_locator( + dispatch.native_session_resume_locator( {"active_locator": str(current_locator)}, expected_workspace=store.workspace, expected_workspace_id=store.workspace_id, @@ -8564,7 +8581,7 @@ class RouteDecisionPersistenceTest(unittest.TestCase): "source": "test", "targets": [{ "adapter": "claude", - "target": "claude-opus-4-8", + "target": "claude-opus-5", "status": "exhausted", }, { "adapter": "codex", @@ -9478,7 +9495,7 @@ class DispatcherCanonicalFailoverIntegrationTest(unittest.IsolatedAsyncioTestCas task = self.make_task(workspace, lane="cloud", grade=7) store = dispatch.StateStore(workspace) try: - claude_spec = dispatch.AgentSpec("claude", "claude-opus-4-8", "claude/claude-opus-4-8 xhigh") + claude_spec = dispatch.AgentSpec("claude", "claude-opus-5", "claude/claude-opus-5 xhigh") terra_spec = dispatch.AgentSpec( "codex", "gpt-5.6-terra", @@ -10015,7 +10032,7 @@ class DispatcherCanonicalFailoverIntegrationTest(unittest.IsolatedAsyncioTestCas "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" + "claude", "claude-opus-5", "claude/claude-opus-5 xhigh" ) locator = self.make_attempt_locator(workspace, task, agy_spec) invoked_specs = [] @@ -10947,7 +10964,7 @@ class SelectorDispatcherIntegrationTest(unittest.IsolatedAsyncioTestCase): task = self.make_task(workspace, lane="cloud", grade=7) store = dispatch.StateStore(workspace) try: - claude_spec = dispatch.AgentSpec("claude", "claude-opus-4-8", "claude/claude-opus-4-8 xhigh") + claude_spec = dispatch.AgentSpec("claude", "claude-opus-5", "claude/claude-opus-5 xhigh") loc_claude = self.make_attempt_locator(workspace, task, claude_spec) invoked_specs = [] diff --git a/agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatcher_observation.py b/agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatcher_observation.py index 066c9087..62e0cfe3 100644 --- a/agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatcher_observation.py +++ b/agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatcher_observation.py @@ -281,11 +281,11 @@ class SkillObservationContractTest(unittest.TestCase): self.assertIn("dispatcher_source_sha256", skill) self.assertIn("`dispatcher_source_matches_loaded=false`", skill) self.assertIn( - "KST-night `local-G07`–`local-G08` Laguna locator `context-limit`/`session-stall`", + "Pi locator whose target enables `runtime.native_session_resume`", skill, ) self.assertIn( - "fresh session and `세션응답복구재시도` only for other legacy Pi `session-stall` recovery", + "fresh session and `세션응답복구재시도` for Pi targets without that capability", skill, ) diff --git a/agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_execution_target_policy.py b/agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_execution_target_policy.py index 07a80f24..65dc93da 100644 --- a/agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_execution_target_policy.py +++ b/agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_execution_target_policy.py @@ -82,7 +82,7 @@ class ExecutionTargetPolicyTests(unittest.TestCase): path.write_text(json.dumps(data), encoding="utf-8") with self.assertRaisesRegex( policy.CatalogError, - "missing compatibility targets", + "references an unknown target", ): policy.reload_catalog(path) self.assertIs(policy.CATALOG, published) @@ -99,18 +99,62 @@ class ExecutionTargetPolicyTests(unittest.TestCase): policy.load_catalog(path) def test_catalog_owns_explicit_driver_options(self): - self.assertEqual(policy.PI_ORNITH.thinking_level, "high") - self.assertEqual(policy.PI_LAGUNA.thinking_level, "high") + self.assertEqual(policy.catalog_target("pi-ornith-high").thinking_level, "high") + self.assertEqual(policy.catalog_target("pi-laguna-high").thinking_level, "high") for target in ( - policy.CLAUDE_GLM, - policy.CLAUDE_OPUS, - policy.CLAUDE_HAIKU_XHIGH, - policy.CODEX_SPARK_XHIGH, - policy.CODEX_SOL_XHIGH, + policy.catalog_target("legacy-claude-glm"), + policy.catalog_target("claude-opus-xhigh"), + policy.catalog_target("claude-haiku-xhigh"), + policy.catalog_target("codex-spark-xhigh"), + policy.catalog_target("codex-sol-xhigh"), ): with self.subTest(target=target.catalog_id): self.assertEqual(target.reasoning_effort, "xhigh") + def test_catalog_owns_model_specific_runtime_capabilities(self): + self.assertTrue( + policy.catalog_target("pi-laguna-high").native_session_resume + ) + self.assertFalse( + policy.catalog_target("pi-ornith-high").native_session_resume + ) + for target_id in ( + "codex-spark-xhigh", + "codex-sol-xhigh", + "codex-terra-high", + ): + with self.subTest(target_id=target_id): + self.assertEqual( + policy.catalog_target(target_id).same_target_retry_limit, + 1, + ) + + def test_canonical_target_uses_unique_catalog_identity_when_options_are_absent(self): + target = policy.catalog_target("codex-terra-high") + self.assertEqual( + policy.canonical_target(target.adapter, target.target), + target, + ) + ambiguous = policy.catalog_target("opencode-glm-medium") + self.assertIsNone( + policy.canonical_target(ambiguous.adapter, ambiguous.target) + ) + + def test_dispatcher_python_does_not_embed_catalog_model_identities(self): + data = json.loads(policy.CATALOG_PATH.read_text(encoding="utf-8")) + model_identities = set(data["targets"]) + for target in data["targets"].values(): + model_identities.add(target["target"]) + command_model = target.get("command_model") + if command_model: + model_identities.add(command_model) + + for runtime_path in sorted(policy.CATALOG_PATH.parent.glob("*.py")): + source = runtime_path.read_text(encoding="utf-8") + for identity in sorted(model_identities): + with self.subTest(path=runtime_path.name, identity=identity): + self.assertNotIn(identity, source) + def test_catalog_rejects_unsupported_runtime_combinations(self): cases = ( ( @@ -148,6 +192,20 @@ class ExecutionTargetPolicyTests(unittest.TestCase): ].pop("checklist_review"), "must contain exactly", ), + ( + "invalid native resume capability", + lambda data: data["targets"]["pi-laguna-high"][ + "runtime" + ].update(native_session_resume="yes"), + "native_session_resume must be a boolean", + ), + ( + "invalid same-target retry limit", + lambda data: data["targets"]["codex-sol-xhigh"][ + "runtime" + ].update(same_target_retry_limit=True), + "same_target_retry_limit must be a non-negative integer", + ), ) for name, mutate, message in cases: with self.subTest(name=name), TemporaryDirectory() as tmp: @@ -187,9 +245,9 @@ class ExecutionTargetPolicyTests(unittest.TestCase): self.assertEqual( decision.candidates, ( - policy.AGY_GEMINI_HIGH, - policy.OPENCODE_GLM_MAX, - policy.CODEX_TERRA_HIGH, + policy.catalog_target("agy-gemini-high"), + policy.catalog_target("opencode-glm-max"), + policy.catalog_target("codex-terra-high"), ), ) self.assertEqual(decision.time_window, "kst-night-[23:00,07:00)") @@ -205,8 +263,8 @@ class ExecutionTargetPolicyTests(unittest.TestCase): }, 7: ("agy", "Gemini 3.6 Flash (High)", False), 8: ("agy", "Gemini 3.6 Flash (High)", False), - 9: ("claude", "claude-opus-4-8", False), - 10: ("claude", "claude-opus-4-8", False), + 9: ("claude", "claude-opus-5", False), + 10: ("claude", "claude-opus-5", False), }, "cloud": { **{ @@ -221,8 +279,8 @@ class ExecutionTargetPolicyTests(unittest.TestCase): grade: ("agy", "Gemini 3.6 Flash (High)", False) for grade in range(5, 7) }, - 7: ("claude", "claude-opus-4-8", False), - 8: ("claude", "claude-opus-4-8", False), + 7: ("claude", "claude-opus-5", False), + 8: ("claude", "claude-opus-5", False), 9: ("codex", "gpt-5.6-sol", False), 10: ("codex", "gpt-5.6-sol", False), }, @@ -257,10 +315,10 @@ class ExecutionTargetPolicyTests(unittest.TestCase): self.assertEqual( decision.candidates, ( - policy.CODEX_SPARK_XHIGH, - policy.AGY_GEMINI_LOW, - policy.OPENCODE_GLM_MEDIUM, - policy.CODEX_TERRA_HIGH, + policy.catalog_target("codex-spark-xhigh"), + policy.catalog_target("agy-gemini-low"), + policy.catalog_target("opencode-glm-medium"), + policy.catalog_target("codex-terra-high"), ), ) self.assertEqual( @@ -286,7 +344,7 @@ class ExecutionTargetPolicyTests(unittest.TestCase): decision.reason_codes, ("review_catalog_lane",), ) - self.assertEqual(decision.candidates, (policy.CODEX_SOL_XHIGH,)) + self.assertEqual(decision.candidates, (policy.catalog_target("codex-sol-xhigh"),)) def test_local_g07_g08_candidate_order_uses_gemini_high_then_glm_max(self): daytime = policy.select_policy( @@ -302,18 +360,18 @@ class ExecutionTargetPolicyTests(unittest.TestCase): evaluated_at=at_utc(15), ) expected = ( - policy.AGY_GEMINI_HIGH, - policy.OPENCODE_GLM_MAX, - policy.CODEX_TERRA_HIGH, + policy.catalog_target("agy-gemini-high"), + policy.catalog_target("opencode-glm-max"), + policy.catalog_target("codex-terra-high"), ) self.assertEqual(daytime.candidates, expected) self.assertEqual(nighttime.candidates, expected) def test_opencode_glm_effort_is_one_step_above_gemini(self): cases = ( - (policy.AGY_GEMINI_LOW, policy.OPENCODE_GLM_MEDIUM, "medium"), - (policy.AGY_GEMINI_MEDIUM, policy.OPENCODE_GLM_HIGH, "high"), - (policy.AGY_GEMINI_HIGH, policy.OPENCODE_GLM_MAX, "max"), + (policy.catalog_target("agy-gemini-low"), policy.catalog_target("opencode-glm-medium"), "medium"), + (policy.catalog_target("agy-gemini-medium"), policy.catalog_target("opencode-glm-high"), "high"), + (policy.catalog_target("agy-gemini-high"), policy.catalog_target("opencode-glm-max"), "max"), ) for gemini, target, effort in cases: with self.subTest(gemini=gemini.target): @@ -350,17 +408,30 @@ class ExecutionTargetPolicyTests(unittest.TestCase): def test_cloud_promotion_matrix(self): cases = [ - (policy.CLAUDE_OPUS, policy.CODEX_TERRA_HIGH), - (policy.CLAUDE_HAIKU_XHIGH, None), - (policy.CODEX_SPARK_XHIGH, None), - (policy.CODEX_SOL_XHIGH, None), - (policy.CODEX_TERRA_HIGH, None), - (policy.PI_ORNITH, None), - (policy.PI_LAGUNA, None), - (policy.OPENCODE_GLM_MEDIUM, None), - (policy.OPENCODE_GLM_HIGH, None), - (policy.OPENCODE_GLM_MAX, None), - (policy.CLAUDE_GLM, None), + ( + policy.catalog_target("agy-gemini-low"), + policy.catalog_target("claude-opus-xhigh"), + ), + ( + policy.catalog_target("agy-gemini-medium"), + policy.catalog_target("claude-opus-xhigh"), + ), + ( + policy.catalog_target("agy-gemini-high"), + policy.catalog_target("claude-opus-xhigh"), + ), + (policy.catalog_target("claude-opus-xhigh"), policy.catalog_target("codex-terra-high")), + (policy.catalog_target("claude-haiku-xhigh"), policy.catalog_target("codex-terra-high")), + (policy.catalog_target("codex-spark-xhigh"), None), + (policy.catalog_target("codex-sol-xhigh"), None), + (policy.catalog_target("codex-terra-high"), None), + (policy.catalog_target("pi-ornith-high"), None), + (policy.catalog_target("pi-laguna-high"), None), + (policy.catalog_target("legacy-pi-selfcheck-disabled"), None), + (policy.catalog_target("opencode-glm-medium"), policy.catalog_target("codex-terra-high")), + (policy.catalog_target("opencode-glm-high"), policy.catalog_target("codex-terra-high")), + (policy.catalog_target("opencode-glm-max"), policy.catalog_target("codex-terra-high")), + (policy.catalog_target("legacy-claude-glm"), policy.catalog_target("codex-terra-high")), ] for current, expected in cases: with self.subTest(current=current): @@ -381,38 +452,38 @@ class ExecutionTargetPolicyTests(unittest.TestCase): def test_quota_probe_spec_matrix(self): cases = [ - (policy.PI_ORNITH, None), - (policy.PI_LAGUNA, None), - (policy.OPENCODE_GLM_MEDIUM, None), - (policy.OPENCODE_GLM_HIGH, None), - (policy.OPENCODE_GLM_MAX, None), - (policy.CLAUDE_GLM, None), + (policy.catalog_target("pi-ornith-high"), None), + (policy.catalog_target("pi-laguna-high"), None), + (policy.catalog_target("opencode-glm-medium"), None), + (policy.catalog_target("opencode-glm-high"), None), + (policy.catalog_target("opencode-glm-max"), None), + (policy.catalog_target("legacy-claude-glm"), None), ( - policy.AGY_GEMINI_LOW, + policy.catalog_target("agy-gemini-low"), policy.QuotaProbeSpec("agy", "Gemini 3.6 Flash (Low)", ("overall", "model:Gemini 3.6 Flash (Low)")), ), ( - policy.AGY_GEMINI_MEDIUM, + policy.catalog_target("agy-gemini-medium"), policy.QuotaProbeSpec("agy", "Gemini 3.6 Flash (Medium)", ("overall", "model:Gemini 3.6 Flash (Medium)")), ), ( - policy.AGY_GEMINI_HIGH, + policy.catalog_target("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.catalog_target("claude-opus-xhigh"), + policy.QuotaProbeSpec("claude", "claude-opus-5", ("overall",)), ), ( - policy.CLAUDE_HAIKU_XHIGH, + policy.catalog_target("claude-haiku-xhigh"), policy.QuotaProbeSpec("claude", "claude-haiku-4-5", ("overall",)), ), ( - policy.CODEX_SPARK_XHIGH, + policy.catalog_target("codex-spark-xhigh"), policy.QuotaProbeSpec("codex", "gpt-5.3-codex-spark", ("overall",)), ), ( - policy.CODEX_SOL_XHIGH, + policy.catalog_target("codex-sol-xhigh"), policy.QuotaProbeSpec("codex", "gpt-5.6-sol", ("overall",)), ), ] diff --git a/agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_select_execution_target.py b/agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_select_execution_target.py index b100af4b..a549bafb 100644 --- a/agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_select_execution_target.py +++ b/agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_select_execution_target.py @@ -173,7 +173,7 @@ class SelectorContractTests(unittest.TestCase): result["selected"], { "adapter": "claude", - "target": "claude-opus-4-8", + "target": "claude-opus-5", "execution_class": "cloud_model", "selfcheck_required": False, "target_id": "claude-opus-xhigh", @@ -497,8 +497,8 @@ class SelectorRouteMatrixTests(unittest.TestCase): for g in range(1, 7)}, 7: ("agy", "Gemini 3.6 Flash (High)", "cloud_model", False), 8: ("agy", "Gemini 3.6 Flash (High)", "cloud_model", False), - 9: ("claude", "claude-opus-4-8", "cloud_model", False), - 10: ("claude", "claude-opus-4-8", "cloud_model", False), + 9: ("claude", "claude-opus-5", "cloud_model", False), + 10: ("claude", "claude-opus-5", "cloud_model", False), }, "cloud": { 1: ("codex", "gpt-5.3-codex-spark", "cloud_model", False), @@ -507,8 +507,8 @@ class SelectorRouteMatrixTests(unittest.TestCase): 4: ("agy", "Gemini 3.6 Flash (Medium)", "cloud_model", False), 5: ("agy", "Gemini 3.6 Flash (High)", "cloud_model", False), 6: ("agy", "Gemini 3.6 Flash (High)", "cloud_model", False), - 7: ("claude", "claude-opus-4-8", "cloud_model", False), - 8: ("claude", "claude-opus-4-8", "cloud_model", False), + 7: ("claude", "claude-opus-5", "cloud_model", False), + 8: ("claude", "claude-opus-5", "cloud_model", False), 9: ("codex", "gpt-5.6-sol", "cloud_model", False), 10: ("codex", "gpt-5.6-sol", "cloud_model", False), }, @@ -629,7 +629,7 @@ class SelectorQuotaRepresentationTests(unittest.TestCase): "targets": [ { "adapter": "claude", - "target": "claude-opus-4-8", + "target": "claude-opus-5", "status": status, } ], @@ -650,7 +650,7 @@ class SelectorQuotaRepresentationTests(unittest.TestCase): "targets": [ { "adapter": "claude", - "target": "claude-opus-4-8", + "target": "claude-opus-5", "status": status, } ], @@ -670,7 +670,7 @@ class SelectorQuotaRepresentationTests(unittest.TestCase): with self.subTest(status=status): snapshot = go_quota_snapshot( "claude", - "claude-opus-4-8", + "claude-opus-5", status, snapshot_id=f"quota-{status}", ) @@ -739,7 +739,7 @@ class SelectorQuotaRepresentationTests(unittest.TestCase): "targets": [ { "adapter": "claude", - "target": "claude-opus-4-8", + "target": "claude-opus-5", "status": "exhausted", }, { @@ -785,7 +785,7 @@ class SelectorQuotaRepresentationTests(unittest.TestCase): "targets": [ { "adapter": "claude", - "target": "claude-opus-4-8", + "target": "claude-opus-5", "status": "unknown", } ], @@ -809,7 +809,7 @@ class SelectorQuotaRepresentationTests(unittest.TestCase): "targets": [ { "adapter": "claude", - "target": "claude-opus-4-8", + "target": "claude-opus-5", "status": "exhausted", } ], @@ -1619,7 +1619,7 @@ class SelectorFailoverContractTests(unittest.TestCase): self.assertEqual( (initial["selected"]["adapter"], initial["selected"]["target"]), - ("claude", "claude-opus-4-8"), + ("claude", "claude-opus-5"), ) self.assertEqual(terra["transition"]["trigger"], "provider-quota") self.assertEqual( @@ -1709,7 +1709,7 @@ class SelectorFailoverContractTests(unittest.TestCase): else: run_mock.return_value = side_effect result = selector.probe_candidate_quota( - target="claude-opus-4-8", + target="claude-opus-5", adapter="claude", required_caps=("overall",), checked_at=eval_time,