chore(sync): dev 변경을 병합한다

This commit is contained in:
toki 2026-08-07 14:54:16 +09:00
commit 7c390db9d8
10 changed files with 424 additions and 245 deletions

View file

@ -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=<adapter>/<target> <options>
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=<adapter>/<target> <options>
review=/absolute/path/CODE_REVIEW-local-G05.md
```

View file

@ -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$")
@ -1736,6 +1734,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():
@ -1751,7 +1789,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,
)
@ -1943,7 +1983,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
@ -2327,11 +2367,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")
@ -2777,7 +2812,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)
@ -3129,18 +3164,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]:
@ -3651,7 +3686,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,
@ -3687,9 +3722,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"
):
@ -3812,7 +3851,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"),
]
@ -4681,7 +4720,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
@ -4749,7 +4788,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(
"모델승격",
@ -4960,8 +4999,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
@ -5150,7 +5191,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(
@ -5165,8 +5206,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,
@ -6911,7 +6952,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,

View file

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

View file

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

View file

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

View file

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

View file

@ -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",
@ -4381,8 +4394,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",
@ -4425,8 +4439,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",
@ -4471,8 +4486,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 (
@ -4616,8 +4632,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",
@ -4858,7 +4875,7 @@ class ReviewRetryTest(unittest.IsolatedAsyncioTestCase):
encoding="utf-8",
)
self.assertEqual(
dispatch.laguna_resume_locator(
dispatch.native_session_resume_locator(
{"active_locator": str(locator)}
),
locator,
@ -7186,7 +7203,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,
@ -7213,7 +7230,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,
@ -7228,7 +7245,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,
@ -8574,7 +8591,7 @@ class RouteDecisionPersistenceTest(unittest.TestCase):
"source": "test",
"targets": [{
"adapter": "claude",
"target": "claude-opus-4-8",
"target": "claude-opus-5",
"status": "exhausted",
}, {
"adapter": "codex",
@ -9488,7 +9505,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",
@ -10025,7 +10042,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 = []
@ -10957,7 +10974,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 = []

View file

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

View file

@ -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",)),
),
]

View file

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