sync: agent-ops from agentic-framework v1.1.205
This commit is contained in:
parent
e3879b7ff9
commit
d5edc6fd8d
15 changed files with 358 additions and 13 deletions
|
|
@ -3,6 +3,7 @@
|
||||||
**현재 문서를 반드시 끝까지 정독하고 작업한다. 다 읽지 않고 즉각 작업은 금지한다.**
|
**현재 문서를 반드시 끝까지 정독하고 작업한다. 다 읽지 않고 즉각 작업은 금지한다.**
|
||||||
|
|
||||||
- 기존 구조를 우선한다. 새 파일 생성보다 기존 파일 수정을 우선한다.
|
- 기존 구조를 우선한다. 새 파일 생성보다 기존 파일 수정을 우선한다.
|
||||||
|
- **과설계를 금지한다.** 기능을 먼저 최소 구현하며, 구현 단계의 검증은 보안·데이터 손상 방지처럼 반드시 필요한 경우 외에는 두지 않는다. 추가 검증은 실제 문제가 확인된 뒤에만 보강한다.
|
||||||
- `agent-ops/rules/common/**`와 `agent-ops/skills/common/**`은 중앙 관리되는 공통 영역이므로 어떤 프로젝트 작업에서도 사용자가 직접 지시하지 않는 이상 절대 직접 수정하지 않는다. 프로젝트별 규칙과 스킬은 반드시 대응하는 `project/**` 영역에만 반영한다.
|
- `agent-ops/rules/common/**`와 `agent-ops/skills/common/**`은 중앙 관리되는 공통 영역이므로 어떤 프로젝트 작업에서도 사용자가 직접 지시하지 않는 이상 절대 직접 수정하지 않는다. 프로젝트별 규칙과 스킬은 반드시 대응하는 `project/**` 영역에만 반영한다.
|
||||||
- 최종 답변은 한국어로 한다.
|
- 최종 답변은 한국어로 한다.
|
||||||
- 코드 변경 전 관련 domain rule을 먼저 확인한다.
|
- 코드 변경 전 관련 domain rule을 먼저 확인한다.
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
**현재 문서를 반드시 끝까지 정독하고 작업한다. 다 읽지 않고 즉각 작업은 금지한다.**
|
**현재 문서를 반드시 끝까지 정독하고 작업한다. 다 읽지 않고 즉각 작업은 금지한다.**
|
||||||
|
|
||||||
- 기존 구조를 우선한다. 새 파일 생성보다 기존 파일 수정을 우선한다.
|
- 기존 구조를 우선한다. 새 파일 생성보다 기존 파일 수정을 우선한다.
|
||||||
|
- **과설계를 금지한다.** 기능을 먼저 최소 구현하며, 구현 단계의 검증은 보안·데이터 손상 방지처럼 반드시 필요한 경우 외에는 두지 않는다. 추가 검증은 실제 문제가 확인된 뒤에만 보강한다.
|
||||||
- `agent-ops/rules/common/**`와 `agent-ops/skills/common/**`은 중앙 관리되는 공통 영역이므로 어떤 프로젝트 작업에서도 사용자가 직접 지시하지 않는 이상 절대 직접 수정하지 않는다. 프로젝트별 규칙과 스킬은 반드시 대응하는 `project/**` 영역에만 반영한다.
|
- `agent-ops/rules/common/**`와 `agent-ops/skills/common/**`은 중앙 관리되는 공통 영역이므로 어떤 프로젝트 작업에서도 사용자가 직접 지시하지 않는 이상 절대 직접 수정하지 않는다. 프로젝트별 규칙과 스킬은 반드시 대응하는 `project/**` 영역에만 반영한다.
|
||||||
- 최종 답변은 한국어로 한다.
|
- 최종 답변은 한국어로 한다.
|
||||||
- 코드 변경 전 관련 domain rule을 먼저 확인한다.
|
- 코드 변경 전 관련 domain rule을 먼저 확인한다.
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
**현재 문서를 반드시 끝까지 정독하고 작업한다. 다 읽지 않고 즉각 작업은 금지한다.**
|
**현재 문서를 반드시 끝까지 정독하고 작업한다. 다 읽지 않고 즉각 작업은 금지한다.**
|
||||||
|
|
||||||
- 기존 구조를 우선한다. 새 파일 생성보다 기존 파일 수정을 우선한다.
|
- 기존 구조를 우선한다. 새 파일 생성보다 기존 파일 수정을 우선한다.
|
||||||
|
- **과설계를 금지한다.** 기능을 먼저 최소 구현하며, 구현 단계의 검증은 보안·데이터 손상 방지처럼 반드시 필요한 경우 외에는 두지 않는다. 추가 검증은 실제 문제가 확인된 뒤에만 보강한다.
|
||||||
- `agent-ops/rules/common/**`와 `agent-ops/skills/common/**`은 중앙 관리되는 공통 영역이므로 어떤 프로젝트 작업에서도 사용자가 직접 지시하지 않는 이상 절대 직접 수정하지 않는다. 프로젝트별 규칙과 스킬은 반드시 대응하는 `project/**` 영역에만 반영한다.
|
- `agent-ops/rules/common/**`와 `agent-ops/skills/common/**`은 중앙 관리되는 공통 영역이므로 어떤 프로젝트 작업에서도 사용자가 직접 지시하지 않는 이상 절대 직접 수정하지 않는다. 프로젝트별 규칙과 스킬은 반드시 대응하는 `project/**` 영역에만 반영한다.
|
||||||
- 최종 답변은 한국어로 한다.
|
- 최종 답변은 한국어로 한다.
|
||||||
- 코드 변경 전 관련 domain rule을 먼저 확인한다.
|
- 코드 변경 전 관련 domain rule을 먼저 확인한다.
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
**현재 문서를 반드시 끝까지 정독하고 작업한다. 다 읽지 않고 즉각 작업은 금지한다.**
|
**현재 문서를 반드시 끝까지 정독하고 작업한다. 다 읽지 않고 즉각 작업은 금지한다.**
|
||||||
|
|
||||||
- 기존 구조를 우선한다. 새 파일 생성보다 기존 파일 수정을 우선한다.
|
- 기존 구조를 우선한다. 새 파일 생성보다 기존 파일 수정을 우선한다.
|
||||||
|
- **과설계를 금지한다.** 기능을 먼저 최소 구현하며, 구현 단계의 검증은 보안·데이터 손상 방지처럼 반드시 필요한 경우 외에는 두지 않는다. 추가 검증은 실제 문제가 확인된 뒤에만 보강한다.
|
||||||
- `agent-ops/rules/common/**`와 `agent-ops/skills/common/**`은 중앙 관리되는 공통 영역이므로 어떤 프로젝트 작업에서도 사용자가 직접 지시하지 않는 이상 절대 직접 수정하지 않는다. 프로젝트별 규칙과 스킬은 반드시 대응하는 `project/**` 영역에만 반영한다.
|
- `agent-ops/rules/common/**`와 `agent-ops/skills/common/**`은 중앙 관리되는 공통 영역이므로 어떤 프로젝트 작업에서도 사용자가 직접 지시하지 않는 이상 절대 직접 수정하지 않는다. 프로젝트별 규칙과 스킬은 반드시 대응하는 `project/**` 영역에만 반영한다.
|
||||||
- 최종 답변은 한국어로 한다.
|
- 최종 답변은 한국어로 한다.
|
||||||
- 코드 변경 전 관련 domain rule을 먼저 확인한다.
|
- 코드 변경 전 관련 domain rule을 먼저 확인한다.
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
**현재 문서를 반드시 끝까지 정독하고 작업한다. 다 읽지 않고 즉각 작업은 금지한다.**
|
**현재 문서를 반드시 끝까지 정독하고 작업한다. 다 읽지 않고 즉각 작업은 금지한다.**
|
||||||
|
|
||||||
- 기존 구조를 우선한다. 새 파일 생성보다 기존 파일 수정을 우선한다.
|
- 기존 구조를 우선한다. 새 파일 생성보다 기존 파일 수정을 우선한다.
|
||||||
|
- **과설계를 금지한다.** 기능을 먼저 최소 구현하며, 구현 단계의 검증은 보안·데이터 손상 방지처럼 반드시 필요한 경우 외에는 두지 않는다. 추가 검증은 실제 문제가 확인된 뒤에만 보강한다.
|
||||||
- `agent-ops/rules/common/**`와 `agent-ops/skills/common/**`은 중앙 관리되는 공통 영역이므로 어떤 프로젝트 작업에서도 사용자가 직접 지시하지 않는 이상 절대 직접 수정하지 않는다. 프로젝트별 규칙과 스킬은 반드시 대응하는 `project/**` 영역에만 반영한다.
|
- `agent-ops/rules/common/**`와 `agent-ops/skills/common/**`은 중앙 관리되는 공통 영역이므로 어떤 프로젝트 작업에서도 사용자가 직접 지시하지 않는 이상 절대 직접 수정하지 않는다. 프로젝트별 규칙과 스킬은 반드시 대응하는 `project/**` 영역에만 반영한다.
|
||||||
- 최종 답변은 한국어로 한다.
|
- 최종 답변은 한국어로 한다.
|
||||||
- 코드 변경 전 관련 domain rule을 먼저 확인한다.
|
- 코드 변경 전 관련 domain rule을 먼저 확인한다.
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
1.1.204
|
1.1.205
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
**현재 문서를 반드시 끝까지 정독하고 작업한다. 다 읽지 않고 즉각 작업은 금지한다.**
|
**현재 문서를 반드시 끝까지 정독하고 작업한다. 다 읽지 않고 즉각 작업은 금지한다.**
|
||||||
|
|
||||||
- 기존 구조를 우선한다. 새 파일 생성보다 기존 파일 수정을 우선한다.
|
- 기존 구조를 우선한다. 새 파일 생성보다 기존 파일 수정을 우선한다.
|
||||||
|
- **과설계를 금지한다.** 기능을 먼저 최소 구현하며, 구현 단계의 검증은 보안·데이터 손상 방지처럼 반드시 필요한 경우 외에는 두지 않는다. 추가 검증은 실제 문제가 확인된 뒤에만 보강한다.
|
||||||
- `agent-ops/rules/common/**`와 `agent-ops/skills/common/**`은 중앙 관리되는 공통 영역이므로 어떤 프로젝트 작업에서도 사용자가 직접 지시하지 않는 이상 절대 직접 수정하지 않는다. 프로젝트별 규칙과 스킬은 반드시 대응하는 `project/**` 영역에만 반영한다.
|
- `agent-ops/rules/common/**`와 `agent-ops/skills/common/**`은 중앙 관리되는 공통 영역이므로 어떤 프로젝트 작업에서도 사용자가 직접 지시하지 않는 이상 절대 직접 수정하지 않는다. 프로젝트별 규칙과 스킬은 반드시 대응하는 `project/**` 영역에만 반영한다.
|
||||||
- 최종 답변은 한국어로 한다.
|
- 최종 답변은 한국어로 한다.
|
||||||
- 코드 변경 전 관련 domain rule을 먼저 확인한다.
|
- 코드 변경 전 관련 domain rule을 먼저 확인한다.
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,8 @@ Each target has:
|
||||||
- an opaque `model` identity;
|
- an opaque `model` identity;
|
||||||
- optional `reasoning_effort`, stored as a separate opaque catalog value rather than embedded in dispatcher code or a literal command argument;
|
- optional `reasoning_effort`, stored as a separate opaque catalog value rather than embedded in dispatcher code or a literal command argument;
|
||||||
- `execution_class`: `local_model` or `cloud_model`;
|
- `execution_class`: `local_model` or `cloud_model`;
|
||||||
- optional `selfcheck_required` boolean;
|
- optional `selfcheck_required` boolean for an unconditional post-worker self-check;
|
||||||
|
- optional `completion_selfcheck_on_incomplete` boolean that routes a changed, zero-exit worker with incomplete implementation checklist evidence to the self-check stage before charging a generic worker failure;
|
||||||
- `runtime.command`: a non-empty argv template executed without a shell;
|
- `runtime.command`: a non-empty argv template executed without a shell;
|
||||||
- optional `runtime.resume_command`, `preflight_command`, `environment`, `session_path`, `native_session_monitor`, `session_stall_resume`, `terminal_success`, and `auxiliary_logs`;
|
- optional `runtime.resume_command`, `preflight_command`, `environment`, `session_path`, `native_session_monitor`, `session_stall_resume`, `terminal_success`, and `auxiliary_logs`;
|
||||||
- optional `runtime.output_format`: `text` or `jsonl`.
|
- optional `runtime.output_format`: `text` or `jsonl`.
|
||||||
|
|
@ -101,7 +102,9 @@ Never ask a child to create, edit, or summarize `WORK_LOG.md`; that file is disp
|
||||||
|
|
||||||
Run self-check only when the selected catalog target declares `selfcheck_required=true`. The completing decision, not a fixed agent identity or execution class, determines the requirement.
|
Run self-check only when the selected catalog target declares `selfcheck_required=true`. The completing decision, not a fixed agent identity or execution class, determines the requirement.
|
||||||
|
|
||||||
Treat worker exit `0` as transport completion only. Before marking the worker done, require at least one claimed file or implementation-evidence change and a complete implementation-owned checklist (or concrete blocker evidence). Classify a no-op or incomplete-evidence exit as `generic-error`, apply the same per-target three-error budget, and return persisted `worker_done` state to the worker stage while that contract remains incomplete. Apply that bounded three-attempt target budget to `session-stall` as well, so a repeatedly silent candidate advances instead of consuming the ten-attempt stage recovery budget.
|
When `completion_selfcheck_on_incomplete=true`, a zero-exit worker that changed claimed files but left implementation checklist evidence incomplete advances to self-check instead of being counted immediately as `generic-error`. This conditional path does not waive the checklist gate, does not accept a no-op worker, and does not run after a complete handoff. The self-check must fill or concretely block the implementation-owned evidence before official review. If a conditional self-check on a target without native resume still leaves the checklist incomplete, fail over once to the next worker candidate with the current workspace instead of fresh-restarting the same target.
|
||||||
|
|
||||||
|
Treat worker exit `0` as transport completion only. Before advancing to official review, require at least one claimed file or implementation-evidence change and a complete implementation-owned checklist (or concrete blocker evidence). Always classify a no-op exit as `generic-error`. Classify incomplete evidence as `generic-error` unless the selected target declares `completion_selfcheck_on_incomplete=true`; that target enters self-check without consuming the worker generic-failure budget. Apply the bounded per-target three-attempt budget to ordinary worker retries and `session-stall`, so a repeatedly silent candidate advances instead of consuming the ten-failure stage recovery budget.
|
||||||
|
|
||||||
Accept self-check completion only when `## Implementation Checklist` or its supported legacy heading contains at least one checkbox and every checkbox has a non-empty value. Run one full pass, then resume the latest successful native context for at most 10 unchecked-item retries when the target supports native resume. Block instead of silently starting a new context when a required persisted context is unavailable.
|
Accept self-check completion only when `## Implementation Checklist` or its supported legacy heading contains at least one checkbox and every checkbox has a non-empty value. Run one full pass, then resume the latest successful native context for at most 10 unchecked-item retries when the target supports native resume. Block instead of silently starting a new context when a required persisted context is unavailable.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -50,12 +50,62 @@
|
||||||
"terminal_success": "agent_end"
|
"terminal_success": "agent_end"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"pi-ornith-fast-high": {
|
||||||
|
"agent": "pi",
|
||||||
|
"model": "ornith-fast",
|
||||||
|
"reasoning_effort": "high",
|
||||||
|
"execution_class": "local_model",
|
||||||
|
"selfcheck_required": true,
|
||||||
|
"runtime": {
|
||||||
|
"command": [
|
||||||
|
"pi",
|
||||||
|
"-p",
|
||||||
|
"--mode",
|
||||||
|
"json",
|
||||||
|
"--approve",
|
||||||
|
"--provider",
|
||||||
|
"iop",
|
||||||
|
"--model",
|
||||||
|
"{model}",
|
||||||
|
"--thinking",
|
||||||
|
"{reasoning_effort}",
|
||||||
|
"--session-id",
|
||||||
|
"{session_id}",
|
||||||
|
"--session-dir",
|
||||||
|
"{attempt_dir}/pi-sessions",
|
||||||
|
"{prompt}"
|
||||||
|
],
|
||||||
|
"resume_command": [
|
||||||
|
"pi",
|
||||||
|
"-p",
|
||||||
|
"--mode",
|
||||||
|
"json",
|
||||||
|
"--approve",
|
||||||
|
"--provider",
|
||||||
|
"iop",
|
||||||
|
"--model",
|
||||||
|
"{model}",
|
||||||
|
"--thinking",
|
||||||
|
"{reasoning_effort}",
|
||||||
|
"--session",
|
||||||
|
"{resume_session}",
|
||||||
|
"--session-dir",
|
||||||
|
"{resume_session_dir}",
|
||||||
|
"{prompt}"
|
||||||
|
],
|
||||||
|
"output_format": "jsonl",
|
||||||
|
"session_path": "{attempt_dir}/pi-sessions/*{session_id}*.jsonl",
|
||||||
|
"native_session_monitor": true,
|
||||||
|
"terminal_success": "agent_end"
|
||||||
|
}
|
||||||
|
},
|
||||||
"opencode-glm-medium": {
|
"opencode-glm-medium": {
|
||||||
"agent": "opencode",
|
"agent": "opencode",
|
||||||
"model": "glm-5.2",
|
"model": "glm-5.2",
|
||||||
"reasoning_effort": "medium",
|
"reasoning_effort": "medium",
|
||||||
"execution_class": "cloud_model",
|
"execution_class": "cloud_model",
|
||||||
"selfcheck_required": false,
|
"selfcheck_required": false,
|
||||||
|
"completion_selfcheck_on_incomplete": true,
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"command": [
|
"command": [
|
||||||
"opencode",
|
"opencode",
|
||||||
|
|
@ -107,6 +157,7 @@
|
||||||
"reasoning_effort": "high",
|
"reasoning_effort": "high",
|
||||||
"execution_class": "cloud_model",
|
"execution_class": "cloud_model",
|
||||||
"selfcheck_required": false,
|
"selfcheck_required": false,
|
||||||
|
"completion_selfcheck_on_incomplete": true,
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"command": [
|
"command": [
|
||||||
"opencode",
|
"opencode",
|
||||||
|
|
@ -158,6 +209,7 @@
|
||||||
"reasoning_effort": "high",
|
"reasoning_effort": "high",
|
||||||
"execution_class": "cloud_model",
|
"execution_class": "cloud_model",
|
||||||
"selfcheck_required": false,
|
"selfcheck_required": false,
|
||||||
|
"completion_selfcheck_on_incomplete": true,
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"command": [
|
"command": [
|
||||||
"opencode",
|
"opencode",
|
||||||
|
|
@ -345,7 +397,7 @@
|
||||||
"reason_codes": ["worker_catalog_lane"]
|
"reason_codes": ["worker_catalog_lane"]
|
||||||
},
|
},
|
||||||
"local-G04": {
|
"local-G04": {
|
||||||
"candidates": ["pi-ornith-high"],
|
"candidates": ["pi-ornith-fast-high"],
|
||||||
"rule_id": "worker-local-g04-catalog",
|
"rule_id": "worker-local-g04-catalog",
|
||||||
"policy_priority": 30,
|
"policy_priority": 30,
|
||||||
"reason_codes": ["worker_catalog_lane"]
|
"reason_codes": ["worker_catalog_lane"]
|
||||||
|
|
|
||||||
|
|
@ -512,6 +512,7 @@ class AgentSpec:
|
||||||
target_id: str | None = None
|
target_id: str | None = None
|
||||||
execution_class: str = "cloud_model"
|
execution_class: str = "cloud_model"
|
||||||
selfcheck_required: bool = False
|
selfcheck_required: bool = False
|
||||||
|
completion_selfcheck_on_incomplete: bool = False
|
||||||
reasoning_effort: str | None = None
|
reasoning_effort: str | None = None
|
||||||
runtime: dict[str, Any] = field(default_factory=dict)
|
runtime: dict[str, Any] = field(default_factory=dict)
|
||||||
|
|
||||||
|
|
@ -532,6 +533,11 @@ def agent_spec_from_record(record: dict[str, Any]) -> AgentSpec | None:
|
||||||
selfcheck_required = record.get("selfcheck_required", False)
|
selfcheck_required = record.get("selfcheck_required", False)
|
||||||
if not isinstance(selfcheck_required, bool):
|
if not isinstance(selfcheck_required, bool):
|
||||||
return None
|
return None
|
||||||
|
completion_selfcheck_on_incomplete = record.get(
|
||||||
|
"completion_selfcheck_on_incomplete", False
|
||||||
|
)
|
||||||
|
if not isinstance(completion_selfcheck_on_incomplete, bool):
|
||||||
|
return None
|
||||||
reasoning_effort = record.get("reasoning_effort")
|
reasoning_effort = record.get("reasoning_effort")
|
||||||
if reasoning_effort is not None and (
|
if reasoning_effort is not None and (
|
||||||
not isinstance(reasoning_effort, str) or not reasoning_effort
|
not isinstance(reasoning_effort, str) or not reasoning_effort
|
||||||
|
|
@ -549,6 +555,7 @@ def agent_spec_from_record(record: dict[str, Any]) -> AgentSpec | None:
|
||||||
target_id=target_id,
|
target_id=target_id,
|
||||||
execution_class=execution_class,
|
execution_class=execution_class,
|
||||||
selfcheck_required=selfcheck_required,
|
selfcheck_required=selfcheck_required,
|
||||||
|
completion_selfcheck_on_incomplete=completion_selfcheck_on_incomplete,
|
||||||
reasoning_effort=reasoning_effort,
|
reasoning_effort=reasoning_effort,
|
||||||
runtime=dict(runtime),
|
runtime=dict(runtime),
|
||||||
)
|
)
|
||||||
|
|
@ -842,6 +849,7 @@ class StateStore:
|
||||||
"worker_cli": None,
|
"worker_cli": None,
|
||||||
"worker_model": None,
|
"worker_model": None,
|
||||||
"selfcheck_done": False,
|
"selfcheck_done": False,
|
||||||
|
"completion_selfcheck_pending": False,
|
||||||
"blocked": None,
|
"blocked": None,
|
||||||
"active_stage": None,
|
"active_stage": None,
|
||||||
"active_locator": None,
|
"active_locator": None,
|
||||||
|
|
@ -871,6 +879,7 @@ class StateStore:
|
||||||
"worker_cli": None,
|
"worker_cli": None,
|
||||||
"worker_model": None,
|
"worker_model": None,
|
||||||
"selfcheck_done": False,
|
"selfcheck_done": False,
|
||||||
|
"completion_selfcheck_pending": False,
|
||||||
"blocked": None,
|
"blocked": None,
|
||||||
"active_stage": None,
|
"active_stage": None,
|
||||||
"active_locator": None,
|
"active_locator": None,
|
||||||
|
|
@ -1134,6 +1143,7 @@ class StateStore:
|
||||||
retry_failover_pending=False,
|
retry_failover_pending=False,
|
||||||
retry_failover_context=None,
|
retry_failover_context=None,
|
||||||
blocker_evidence=None,
|
blocker_evidence=None,
|
||||||
|
completion_selfcheck_pending=False,
|
||||||
blocked=None,
|
blocked=None,
|
||||||
active_stage=None,
|
active_stage=None,
|
||||||
active_locator=None,
|
active_locator=None,
|
||||||
|
|
@ -1188,6 +1198,7 @@ class StateStore:
|
||||||
value["review_no_progress"] = 0
|
value["review_no_progress"] = 0
|
||||||
value["selfcheck_incomplete"] = 0
|
value["selfcheck_incomplete"] = 0
|
||||||
value["selfcheck_context_locator"] = None
|
value["selfcheck_context_locator"] = None
|
||||||
|
value["completion_selfcheck_pending"] = False
|
||||||
value["recovery_failures"] = {}
|
value["recovery_failures"] = {}
|
||||||
value["stage_failure_budgets"] = {}
|
value["stage_failure_budgets"] = {}
|
||||||
value["generic_failure_budgets"] = {}
|
value["generic_failure_budgets"] = {}
|
||||||
|
|
@ -1959,6 +1970,9 @@ def agent_spec_from_decision(decision: dict[str, Any]) -> AgentSpec:
|
||||||
target_id=target.catalog_id,
|
target_id=target.catalog_id,
|
||||||
execution_class=target.execution_class,
|
execution_class=target.execution_class,
|
||||||
selfcheck_required=target.selfcheck_required,
|
selfcheck_required=target.selfcheck_required,
|
||||||
|
completion_selfcheck_on_incomplete=(
|
||||||
|
target.completion_selfcheck_on_incomplete
|
||||||
|
),
|
||||||
reasoning_effort=target.reasoning_effort,
|
reasoning_effort=target.reasoning_effort,
|
||||||
runtime=runtime,
|
runtime=runtime,
|
||||||
)
|
)
|
||||||
|
|
@ -2347,6 +2361,18 @@ def completing_decision_requires_selfcheck(state: dict[str, Any]) -> bool:
|
||||||
return selected.get("selfcheck_required") is True
|
return selected.get("selfcheck_required") is True
|
||||||
|
|
||||||
|
|
||||||
|
def completing_decision_allows_completion_selfcheck(
|
||||||
|
state: dict[str, Any],
|
||||||
|
) -> bool:
|
||||||
|
completing = state.get("completing_decision")
|
||||||
|
if not isinstance(completing, dict):
|
||||||
|
return False
|
||||||
|
selected = completing.get("selected")
|
||||||
|
if not isinstance(selected, dict):
|
||||||
|
return False
|
||||||
|
return selected.get("completion_selfcheck_on_incomplete") is True
|
||||||
|
|
||||||
|
|
||||||
def _validated_completing_decision(
|
def _validated_completing_decision(
|
||||||
task: Task, decision: dict[str, Any]
|
task: Task, decision: dict[str, Any]
|
||||||
) -> tuple[dict[str, Any], AgentSpec]:
|
) -> tuple[dict[str, Any], AgentSpec]:
|
||||||
|
|
@ -2506,6 +2532,13 @@ def task_stage(task: Task, state: dict[str, Any]) -> str:
|
||||||
# worker stage until the implementation-owned review contract is
|
# worker stage until the implementation-owned review contract is
|
||||||
# actually materialized (or contains complete blocker evidence).
|
# actually materialized (or contains complete blocker evidence).
|
||||||
if implementation_review_errors(task):
|
if implementation_review_errors(task):
|
||||||
|
if (
|
||||||
|
state.get("completion_selfcheck_pending")
|
||||||
|
and not state.get("selfcheck_done")
|
||||||
|
and completing_decision_allows_completion_selfcheck(state)
|
||||||
|
and _completing_decision_is_valid(task, state)
|
||||||
|
):
|
||||||
|
return "selfcheck"
|
||||||
return "worker"
|
return "worker"
|
||||||
if not _completing_decision_is_valid(task, state):
|
if not _completing_decision_is_valid(task, state):
|
||||||
return "blocked"
|
return "blocked"
|
||||||
|
|
@ -3677,6 +3710,9 @@ async def invoke(
|
||||||
"target_id": spec.target_id,
|
"target_id": spec.target_id,
|
||||||
"execution_class": spec.execution_class,
|
"execution_class": spec.execution_class,
|
||||||
"selfcheck_required": spec.selfcheck_required,
|
"selfcheck_required": spec.selfcheck_required,
|
||||||
|
"completion_selfcheck_on_incomplete": (
|
||||||
|
spec.completion_selfcheck_on_incomplete
|
||||||
|
),
|
||||||
"reasoning_effort": spec.reasoning_effort,
|
"reasoning_effort": spec.reasoning_effort,
|
||||||
"runtime": spec.runtime,
|
"runtime": spec.runtime,
|
||||||
"agent_process_marker": process_marker,
|
"agent_process_marker": process_marker,
|
||||||
|
|
@ -4257,13 +4293,18 @@ async def invoke(
|
||||||
):
|
):
|
||||||
worker_errors = implementation_review_errors(task)
|
worker_errors = implementation_review_errors(task)
|
||||||
worker_signature_after = task_signature(workspace, task)
|
worker_signature_after = task_signature(workspace, task)
|
||||||
if worker_signature_before == worker_signature_after or worker_errors:
|
unchanged = worker_signature_before == worker_signature_after
|
||||||
|
incomplete_without_selfcheck = (
|
||||||
|
bool(worker_errors) and not spec.completion_selfcheck_on_incomplete
|
||||||
|
)
|
||||||
|
if unchanged or incomplete_without_selfcheck:
|
||||||
failure_class = "generic-error"
|
failure_class = "generic-error"
|
||||||
failure_source = "dispatcher-worker-completion-contract"
|
failure_source = "dispatcher-worker-completion-contract"
|
||||||
details = []
|
details = []
|
||||||
if worker_signature_before == worker_signature_after:
|
if unchanged:
|
||||||
details.append("no claimed file or implementation evidence changed")
|
details.append("no claimed file or implementation evidence changed")
|
||||||
details.extend(worker_errors)
|
if incomplete_without_selfcheck:
|
||||||
|
details.extend(worker_errors)
|
||||||
failure_evidence = "; ".join(details)
|
failure_evidence = "; ".join(details)
|
||||||
failure_evidence_source = "dispatcher:worker-completion-contract"
|
failure_evidence_source = "dispatcher:worker-completion-contract"
|
||||||
try:
|
try:
|
||||||
|
|
@ -5655,6 +5696,13 @@ def _mark_worker_done(
|
||||||
_require_same_runtime_identity(expected_spec, worker_cli, worker_model)
|
_require_same_runtime_identity(expected_spec, worker_cli, worker_model)
|
||||||
selected = validated_decision["selected"]
|
selected = validated_decision["selected"]
|
||||||
execution_class = selected["execution_class"]
|
execution_class = selected["execution_class"]
|
||||||
|
completion_selfcheck_pending = bool(
|
||||||
|
selected.get("completion_selfcheck_on_incomplete")
|
||||||
|
and implementation_review_errors(task)
|
||||||
|
)
|
||||||
|
selfcheck_pending = bool(
|
||||||
|
selected["selfcheck_required"] or completion_selfcheck_pending
|
||||||
|
)
|
||||||
store.update_task(
|
store.update_task(
|
||||||
task,
|
task,
|
||||||
worker_done=True,
|
worker_done=True,
|
||||||
|
|
@ -5662,7 +5710,8 @@ def _mark_worker_done(
|
||||||
worker_model=worker_model,
|
worker_model=worker_model,
|
||||||
completing_decision=validated_decision,
|
completing_decision=validated_decision,
|
||||||
execution_class=execution_class,
|
execution_class=execution_class,
|
||||||
selfcheck_done=not selected["selfcheck_required"],
|
selfcheck_done=not selfcheck_pending,
|
||||||
|
completion_selfcheck_pending=completion_selfcheck_pending,
|
||||||
review_no_progress=0,
|
review_no_progress=0,
|
||||||
blocked=None,
|
blocked=None,
|
||||||
)
|
)
|
||||||
|
|
@ -5690,8 +5739,16 @@ async def run_selfcheck(
|
||||||
store.update_task(task, blocked=str(exc))
|
store.update_task(task, blocked=str(exc))
|
||||||
banner("작업차단", task.name, [f"reason={exc}"])
|
banner("작업차단", task.name, [f"reason={exc}"])
|
||||||
return
|
return
|
||||||
if not spec.selfcheck_required:
|
completion_selfcheck_pending = bool(
|
||||||
raise RuntimeError("selfcheck_required가 아닌 route에 selfcheck stage가 배정됐다")
|
store.task_state(task).get("completion_selfcheck_pending")
|
||||||
|
)
|
||||||
|
if not spec.selfcheck_required and not (
|
||||||
|
completion_selfcheck_pending
|
||||||
|
and spec.completion_selfcheck_on_incomplete
|
||||||
|
):
|
||||||
|
raise RuntimeError(
|
||||||
|
"selfcheck 계약이 없는 route에 selfcheck stage가 배정됐다"
|
||||||
|
)
|
||||||
work_log = milestone_work_log_path(task)
|
work_log = milestone_work_log_path(task)
|
||||||
banner(
|
banner(
|
||||||
"자가검증시작",
|
"자가검증시작",
|
||||||
|
|
@ -5756,7 +5813,7 @@ async def run_selfcheck(
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
while True:
|
while True:
|
||||||
unchecked_items = incomplete_results > 0
|
unchecked_items = completion_selfcheck_pending or incomplete_results > 0
|
||||||
success, locator = await run_escalating(
|
success, locator = await run_escalating(
|
||||||
workspace,
|
workspace,
|
||||||
store,
|
store,
|
||||||
|
|
@ -5776,6 +5833,45 @@ async def run_selfcheck(
|
||||||
if not errors:
|
if not errors:
|
||||||
break
|
break
|
||||||
if not spec.native_resume:
|
if not spec.native_resume:
|
||||||
|
if completion_selfcheck_pending:
|
||||||
|
try:
|
||||||
|
next_decision = select_execution_decision(
|
||||||
|
task,
|
||||||
|
stage="worker",
|
||||||
|
prior_decision=completing,
|
||||||
|
transition="failover",
|
||||||
|
failure_class="generic-error",
|
||||||
|
)
|
||||||
|
next_spec = agent_spec_from_decision(next_decision)
|
||||||
|
except (ExecutionDecisionError, OSError, ValueError):
|
||||||
|
next_decision = None
|
||||||
|
next_spec = spec
|
||||||
|
if next_decision is not None and next_spec != spec:
|
||||||
|
commit_execution_decision(store, task, "worker", next_decision)
|
||||||
|
store.update_task(
|
||||||
|
task,
|
||||||
|
worker_done=False,
|
||||||
|
worker_cli=None,
|
||||||
|
worker_model=None,
|
||||||
|
completing_decision=None,
|
||||||
|
execution_class=next_spec.execution_class,
|
||||||
|
selfcheck_done=False,
|
||||||
|
completion_selfcheck_pending=False,
|
||||||
|
selfcheck_incomplete=0,
|
||||||
|
selfcheck_context_locator=None,
|
||||||
|
blocked=None,
|
||||||
|
)
|
||||||
|
banner(
|
||||||
|
"자가검증실행대상전환",
|
||||||
|
task.name,
|
||||||
|
[
|
||||||
|
f"from={spec.display}",
|
||||||
|
f"to={next_spec.display}",
|
||||||
|
f"reason={'; '.join(errors)}",
|
||||||
|
f"locator={locator}",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
return
|
||||||
reason = "selfcheck checklist가 미완료지만 target에 native resume 계약이 없다"
|
reason = "selfcheck checklist가 미완료지만 target에 native resume 계약이 없다"
|
||||||
store.update_task(task, blocked=reason)
|
store.update_task(task, blocked=reason)
|
||||||
banner(
|
banner(
|
||||||
|
|
@ -5836,6 +5932,7 @@ async def run_selfcheck(
|
||||||
store.update_task(
|
store.update_task(
|
||||||
task,
|
task,
|
||||||
selfcheck_done=True,
|
selfcheck_done=True,
|
||||||
|
completion_selfcheck_pending=False,
|
||||||
selfcheck_incomplete=0,
|
selfcheck_incomplete=0,
|
||||||
selfcheck_context_locator=None,
|
selfcheck_context_locator=None,
|
||||||
blocked=None,
|
blocked=None,
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,7 @@ class RouteTarget:
|
||||||
reasoning_effort: str | None
|
reasoning_effort: str | None
|
||||||
execution_class: str
|
execution_class: str
|
||||||
selfcheck_required: bool
|
selfcheck_required: bool
|
||||||
|
completion_selfcheck_on_incomplete: bool
|
||||||
runtime: dict[str, Any]
|
runtime: dict[str, Any]
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
|
|
@ -211,6 +212,7 @@ def _validate_target(target_id: str, value: object) -> RouteTarget:
|
||||||
"reasoning_effort",
|
"reasoning_effort",
|
||||||
"execution_class",
|
"execution_class",
|
||||||
"selfcheck_required",
|
"selfcheck_required",
|
||||||
|
"completion_selfcheck_on_incomplete",
|
||||||
"runtime",
|
"runtime",
|
||||||
}
|
}
|
||||||
if unknown:
|
if unknown:
|
||||||
|
|
@ -224,6 +226,13 @@ def _validate_target(target_id: str, value: object) -> RouteTarget:
|
||||||
selfcheck_required = value.get("selfcheck_required", False)
|
selfcheck_required = value.get("selfcheck_required", False)
|
||||||
if not isinstance(selfcheck_required, bool):
|
if not isinstance(selfcheck_required, bool):
|
||||||
raise CatalogError(f"{label}.selfcheck_required must be a boolean")
|
raise CatalogError(f"{label}.selfcheck_required must be a boolean")
|
||||||
|
completion_selfcheck_on_incomplete = value.get(
|
||||||
|
"completion_selfcheck_on_incomplete", False
|
||||||
|
)
|
||||||
|
if not isinstance(completion_selfcheck_on_incomplete, bool):
|
||||||
|
raise CatalogError(
|
||||||
|
f"{label}.completion_selfcheck_on_incomplete must be a boolean"
|
||||||
|
)
|
||||||
reasoning_effort_value = value.get("reasoning_effort")
|
reasoning_effort_value = value.get("reasoning_effort")
|
||||||
reasoning_effort = (
|
reasoning_effort = (
|
||||||
None
|
None
|
||||||
|
|
@ -268,6 +277,7 @@ def _validate_target(target_id: str, value: object) -> RouteTarget:
|
||||||
reasoning_effort=reasoning_effort,
|
reasoning_effort=reasoning_effort,
|
||||||
execution_class=execution_class,
|
execution_class=execution_class,
|
||||||
selfcheck_required=selfcheck_required,
|
selfcheck_required=selfcheck_required,
|
||||||
|
completion_selfcheck_on_incomplete=completion_selfcheck_on_incomplete,
|
||||||
runtime=runtime,
|
runtime=runtime,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ from pathlib import Path
|
||||||
from zoneinfo import ZoneInfo
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
|
|
||||||
SCHEMA_VERSION = "2.0"
|
SCHEMA_VERSION = "3.0"
|
||||||
CATALOG_ENV = "AGENT_TASK_EXECUTION_CATALOG"
|
CATALOG_ENV = "AGENT_TASK_EXECUTION_CATALOG"
|
||||||
DEFAULT_CATALOG_PATH = (
|
DEFAULT_CATALOG_PATH = (
|
||||||
Path(__file__).resolve().parents[1]
|
Path(__file__).resolve().parents[1]
|
||||||
|
|
@ -160,6 +160,9 @@ def _target_snapshot(target) -> dict:
|
||||||
"model": target.model,
|
"model": target.model,
|
||||||
"execution_class": target.execution_class,
|
"execution_class": target.execution_class,
|
||||||
"selfcheck_required": target.selfcheck_required,
|
"selfcheck_required": target.selfcheck_required,
|
||||||
|
"completion_selfcheck_on_incomplete": (
|
||||||
|
target.completion_selfcheck_on_incomplete
|
||||||
|
),
|
||||||
}
|
}
|
||||||
if target.reasoning_effort is not None:
|
if target.reasoning_effort is not None:
|
||||||
snapshot["reasoning_effort"] = target.reasoning_effort
|
snapshot["reasoning_effort"] = target.reasoning_effort
|
||||||
|
|
@ -180,6 +183,7 @@ def _validate_target_snapshot(value: object, prefix: str) -> dict:
|
||||||
"model",
|
"model",
|
||||||
"execution_class",
|
"execution_class",
|
||||||
"selfcheck_required",
|
"selfcheck_required",
|
||||||
|
"completion_selfcheck_on_incomplete",
|
||||||
}
|
}
|
||||||
missing = required - set(value)
|
missing = required - set(value)
|
||||||
if missing:
|
if missing:
|
||||||
|
|
@ -194,6 +198,11 @@ def _validate_target_snapshot(value: object, prefix: str) -> dict:
|
||||||
)
|
)
|
||||||
if not isinstance(value["selfcheck_required"], bool):
|
if not isinstance(value["selfcheck_required"], bool):
|
||||||
raise SelectorInputError(code, f"{prefix}.selfcheck_required must be a boolean")
|
raise SelectorInputError(code, f"{prefix}.selfcheck_required must be a boolean")
|
||||||
|
if not isinstance(value["completion_selfcheck_on_incomplete"], bool):
|
||||||
|
raise SelectorInputError(
|
||||||
|
code,
|
||||||
|
f"{prefix}.completion_selfcheck_on_incomplete must be a boolean",
|
||||||
|
)
|
||||||
reasoning_effort = value.get("reasoning_effort")
|
reasoning_effort = value.get("reasoning_effort")
|
||||||
if reasoning_effort is not None and (
|
if reasoning_effort is not None and (
|
||||||
not isinstance(reasoning_effort, str) or not reasoning_effort
|
not isinstance(reasoning_effort, str) or not reasoning_effort
|
||||||
|
|
|
||||||
|
|
@ -729,6 +729,7 @@ class RuntimeCatalogDispatcherTests(unittest.TestCase):
|
||||||
"fake-model",
|
"fake-model",
|
||||||
"fake-json-runner/fake-model",
|
"fake-json-runner/fake-model",
|
||||||
target_id="fake-json-target",
|
target_id="fake-json-target",
|
||||||
|
completion_selfcheck_on_incomplete=True,
|
||||||
runtime={
|
runtime={
|
||||||
"command": [sys.executable, str(runner)],
|
"command": [sys.executable, str(runner)],
|
||||||
"output_format": "jsonl",
|
"output_format": "jsonl",
|
||||||
|
|
@ -762,6 +763,54 @@ class RuntimeCatalogDispatcherTests(unittest.TestCase):
|
||||||
self.assertEqual(record["status"], "failed")
|
self.assertEqual(record["status"], "failed")
|
||||||
self.assertNotIn("succeeded:0", work_log)
|
self.assertNotIn("succeeded:0", work_log)
|
||||||
|
|
||||||
|
def test_changed_worker_with_incomplete_evidence_can_advance_to_completion_selfcheck(self):
|
||||||
|
with TemporaryDirectory() as tmp:
|
||||||
|
root = Path(tmp)
|
||||||
|
plan = write_plan(root)
|
||||||
|
task = task_from_plan(root, plan)
|
||||||
|
review = plan.parent / "CODE_REVIEW-cloud-G05.md"
|
||||||
|
review.write_text(
|
||||||
|
"## Implementation Checklist\n\n- [ ] Implement the task.\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
task.review = review
|
||||||
|
claimed = root / "src" / "item.txt"
|
||||||
|
claimed.parent.mkdir(parents=True)
|
||||||
|
runner = root / "completion_selfcheck_runner.py"
|
||||||
|
runner.write_text(
|
||||||
|
"import json\n"
|
||||||
|
f"open({str(claimed)!r}, 'w', encoding='utf-8').write('changed')\n"
|
||||||
|
"print(json.dumps({'type': 'agent_end', 'willRetry': False, "
|
||||||
|
"'messages': [{'role': 'assistant', 'stopReason': 'stop'}]}))\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
agent = dispatch.AgentSpec(
|
||||||
|
"fake-json-runner",
|
||||||
|
"fake-model",
|
||||||
|
"fake-json-runner/fake-model",
|
||||||
|
target_id="fake-json-target",
|
||||||
|
completion_selfcheck_on_incomplete=True,
|
||||||
|
runtime={
|
||||||
|
"command": [sys.executable, str(runner)],
|
||||||
|
"output_format": "jsonl",
|
||||||
|
"terminal_success": "agent_end",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
with mock.patch.dict(os.environ, {"XDG_STATE_HOME": str(root / "state")}):
|
||||||
|
store = dispatch.StateStore(root)
|
||||||
|
try:
|
||||||
|
return_code, failure, locator = asyncio.run(
|
||||||
|
dispatch.invoke(root, store, task, "worker", agent, "fake prompt")
|
||||||
|
)
|
||||||
|
record = json.loads(locator.read_text(encoding="utf-8"))
|
||||||
|
finally:
|
||||||
|
store.close()
|
||||||
|
|
||||||
|
self.assertEqual(return_code, 0)
|
||||||
|
self.assertIsNone(failure)
|
||||||
|
self.assertEqual(record["status"], "succeeded")
|
||||||
|
self.assertTrue(record["completion_selfcheck_on_incomplete"])
|
||||||
|
|
||||||
def test_worker_done_with_incomplete_evidence_returns_to_worker_stage(self):
|
def test_worker_done_with_incomplete_evidence_returns_to_worker_stage(self):
|
||||||
with TemporaryDirectory() as tmp:
|
with TemporaryDirectory() as tmp:
|
||||||
root = Path(tmp)
|
root = Path(tmp)
|
||||||
|
|
@ -778,6 +827,98 @@ class RuntimeCatalogDispatcherTests(unittest.TestCase):
|
||||||
|
|
||||||
self.assertEqual(stage, "worker")
|
self.assertEqual(stage, "worker")
|
||||||
|
|
||||||
|
def test_worker_done_with_conditional_selfcheck_routes_incomplete_evidence_to_selfcheck(self):
|
||||||
|
with TemporaryDirectory() as tmp:
|
||||||
|
root = Path(tmp)
|
||||||
|
value = catalog_value()
|
||||||
|
value["targets"]["primary"][
|
||||||
|
"completion_selfcheck_on_incomplete"
|
||||||
|
] = True
|
||||||
|
catalog = write_catalog(root, value)
|
||||||
|
plan = write_plan(root)
|
||||||
|
task = task_from_plan(root, plan)
|
||||||
|
review = plan.parent / "CODE_REVIEW-cloud-G05.md"
|
||||||
|
review.write_text(
|
||||||
|
"## Implementation Checklist\n\n- [ ] Implement the task.\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
task.review = review
|
||||||
|
selector = dispatch._selector_module()
|
||||||
|
decision = selector.select_execution_target(plan, catalog_path=catalog)
|
||||||
|
|
||||||
|
stage = dispatch.task_stage(
|
||||||
|
task,
|
||||||
|
{
|
||||||
|
"worker_done": True,
|
||||||
|
"selfcheck_done": False,
|
||||||
|
"completion_selfcheck_pending": True,
|
||||||
|
"completing_decision": decision,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(stage, "selfcheck")
|
||||||
|
|
||||||
|
def test_incomplete_conditional_selfcheck_without_resume_fails_over_to_next_worker(self):
|
||||||
|
with TemporaryDirectory() as tmp:
|
||||||
|
root = Path(tmp)
|
||||||
|
value = catalog_value()
|
||||||
|
value["targets"]["primary"]["runtime"].pop(
|
||||||
|
"native_session_monitor"
|
||||||
|
)
|
||||||
|
value["targets"]["primary"]["runtime"].pop("resume_command")
|
||||||
|
value["targets"]["primary"][
|
||||||
|
"completion_selfcheck_on_incomplete"
|
||||||
|
] = True
|
||||||
|
catalog = write_catalog(root, value)
|
||||||
|
plan = write_plan(root)
|
||||||
|
task = task_from_plan(root, plan)
|
||||||
|
review = plan.parent / "CODE_REVIEW-cloud-G05.md"
|
||||||
|
review.write_text(
|
||||||
|
"## Implementation Checklist\n\n- [ ] Implement the task.\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
task.review = review
|
||||||
|
dispatch.EXECUTION_CATALOG_PATH = catalog
|
||||||
|
selector = dispatch._selector_module()
|
||||||
|
decision = selector.select_execution_target(
|
||||||
|
plan, catalog_path=catalog
|
||||||
|
)
|
||||||
|
with mock.patch.dict(
|
||||||
|
os.environ, {"XDG_STATE_HOME": str(root / "state")}
|
||||||
|
):
|
||||||
|
store = dispatch.StateStore(root)
|
||||||
|
try:
|
||||||
|
store.update_task(
|
||||||
|
task,
|
||||||
|
worker_done=True,
|
||||||
|
selfcheck_done=False,
|
||||||
|
completion_selfcheck_pending=True,
|
||||||
|
completing_decision=decision,
|
||||||
|
execution_decisions={"worker": decision},
|
||||||
|
)
|
||||||
|
with mock.patch.object(
|
||||||
|
dispatch,
|
||||||
|
"run_escalating",
|
||||||
|
new=mock.AsyncMock(
|
||||||
|
return_value=(
|
||||||
|
True,
|
||||||
|
root / "selfcheck-locator.json",
|
||||||
|
)
|
||||||
|
),
|
||||||
|
):
|
||||||
|
asyncio.run(dispatch.run_selfcheck(root, store, task))
|
||||||
|
state = store.task_state(task)
|
||||||
|
finally:
|
||||||
|
store.close()
|
||||||
|
|
||||||
|
self.assertFalse(state["worker_done"])
|
||||||
|
self.assertFalse(state["completion_selfcheck_pending"])
|
||||||
|
self.assertIsNone(state["blocked"])
|
||||||
|
self.assertEqual(
|
||||||
|
state["execution_decisions"]["worker"]["selected"]["target_id"],
|
||||||
|
"alternate",
|
||||||
|
)
|
||||||
|
|
||||||
def test_silent_native_session_is_terminated_and_classified_as_stall(self):
|
def test_silent_native_session_is_terminated_and_classified_as_stall(self):
|
||||||
with TemporaryDirectory() as tmp:
|
with TemporaryDirectory() as tmp:
|
||||||
root = Path(tmp)
|
root = Path(tmp)
|
||||||
|
|
|
||||||
|
|
@ -105,6 +105,30 @@ class ExecutionTargetPolicyTests(unittest.TestCase):
|
||||||
self.assertFalse(hasattr(policy, "quota_probe_spec"))
|
self.assertFalse(hasattr(policy, "quota_probe_spec"))
|
||||||
self.assertFalse(hasattr(policy, "promotion_target"))
|
self.assertFalse(hasattr(policy, "promotion_target"))
|
||||||
|
|
||||||
|
def test_completion_selfcheck_flag_is_optional_and_typed(self):
|
||||||
|
with TemporaryDirectory() as tmp:
|
||||||
|
root = Path(tmp)
|
||||||
|
value = catalog_value()
|
||||||
|
value["targets"]["target-a"][
|
||||||
|
"completion_selfcheck_on_incomplete"
|
||||||
|
] = True
|
||||||
|
catalog = policy.load_catalog(write_catalog(root, value))
|
||||||
|
self.assertTrue(
|
||||||
|
catalog.targets["target-a"].completion_selfcheck_on_incomplete
|
||||||
|
)
|
||||||
|
self.assertFalse(
|
||||||
|
catalog.targets["target-b"].completion_selfcheck_on_incomplete
|
||||||
|
)
|
||||||
|
|
||||||
|
value["targets"]["target-a"][
|
||||||
|
"completion_selfcheck_on_incomplete"
|
||||||
|
] = "yes"
|
||||||
|
with self.assertRaisesRegex(
|
||||||
|
policy.CatalogError,
|
||||||
|
"completion_selfcheck_on_incomplete must be a boolean",
|
||||||
|
):
|
||||||
|
policy.load_catalog(write_catalog(root, value))
|
||||||
|
|
||||||
def test_optional_windows_are_catalog_owned_and_timezone_generic(self):
|
def test_optional_windows_are_catalog_owned_and_timezone_generic(self):
|
||||||
with TemporaryDirectory() as tmp:
|
with TemporaryDirectory() as tmp:
|
||||||
catalog = policy.load_catalog(write_catalog(Path(tmp), catalog_value(windows=True)))
|
catalog = policy.load_catalog(write_catalog(Path(tmp), catalog_value(windows=True)))
|
||||||
|
|
|
||||||
|
|
@ -90,6 +90,7 @@ class SelectorTests(unittest.TestCase):
|
||||||
f"local-G{grade:02d}": ["pi-ornith-high"]
|
f"local-G{grade:02d}": ["pi-ornith-high"]
|
||||||
for grade in range(1, 7)
|
for grade in range(1, 7)
|
||||||
},
|
},
|
||||||
|
"local-G04": ["pi-ornith-fast-high"],
|
||||||
"local-G07": ["opencode-glm-max", "codex-terra-high"],
|
"local-G07": ["opencode-glm-max", "codex-terra-high"],
|
||||||
"local-G08": ["opencode-glm-max", "codex-terra-high"],
|
"local-G08": ["opencode-glm-max", "codex-terra-high"],
|
||||||
"local-G09": ["codex-sol-high", "codex-terra-high"],
|
"local-G09": ["codex-sol-high", "codex-terra-high"],
|
||||||
|
|
@ -107,6 +108,7 @@ class SelectorTests(unittest.TestCase):
|
||||||
}
|
}
|
||||||
expected_targets = {
|
expected_targets = {
|
||||||
"pi-ornith-high",
|
"pi-ornith-high",
|
||||||
|
"pi-ornith-fast-high",
|
||||||
"opencode-glm-medium",
|
"opencode-glm-medium",
|
||||||
"opencode-glm-high",
|
"opencode-glm-high",
|
||||||
"opencode-glm-max",
|
"opencode-glm-max",
|
||||||
|
|
@ -180,6 +182,7 @@ class SelectorTests(unittest.TestCase):
|
||||||
opencode = catalog.targets["opencode-glm-max"]
|
opencode = catalog.targets["opencode-glm-max"]
|
||||||
self.assertIn("iop-glm/glm-5.2", opencode.runtime["command"])
|
self.assertIn("iop-glm/glm-5.2", opencode.runtime["command"])
|
||||||
self.assertEqual(opencode.reasoning_effort, "high")
|
self.assertEqual(opencode.reasoning_effort, "high")
|
||||||
|
self.assertTrue(opencode.completion_selfcheck_on_incomplete)
|
||||||
self.assertIn("{reasoning_effort}", opencode.runtime["command"])
|
self.assertIn("{reasoning_effort}", opencode.runtime["command"])
|
||||||
for target_id in (
|
for target_id in (
|
||||||
"opencode-glm-medium",
|
"opencode-glm-medium",
|
||||||
|
|
@ -220,7 +223,7 @@ class SelectorTests(unittest.TestCase):
|
||||||
catalog_path=catalog,
|
catalog_path=catalog,
|
||||||
evaluated_at=datetime(2026, 1, 1, tzinfo=timezone.utc),
|
evaluated_at=datetime(2026, 1, 1, tzinfo=timezone.utc),
|
||||||
)
|
)
|
||||||
self.assertEqual(result["schema_version"], "2.0")
|
self.assertEqual(result["schema_version"], "3.0")
|
||||||
self.assertEqual(result["selected"]["target_id"], "first")
|
self.assertEqual(result["selected"]["target_id"], "first")
|
||||||
self.assertEqual(result["selected"]["agent"], "agent-one")
|
self.assertEqual(result["selected"]["agent"], "agent-one")
|
||||||
self.assertEqual(result["selected"]["model"], "model-one")
|
self.assertEqual(result["selected"]["model"], "model-one")
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue