sync: agent-ops from agentic-framework v1.1.190

This commit is contained in:
toki 2026-08-08 11:54:00 +09:00
parent be6d9d6bce
commit 7e73d89af2
10 changed files with 136 additions and 45 deletions

View file

@ -1 +1 @@
1.1.189 1.1.190

View file

@ -1,6 +1,6 @@
--- ---
name: orchestrate-agent-task-loop name: orchestrate-agent-task-loop
description: Execute dependency-ready PLAN and CODE_REVIEW task loops with workspace write claims, a runtime-injected agent/model catalog, deterministic target failover, and persistent recovery state. description: Execute dependency-ready PLAN and CODE_REVIEW task loops with workspace write claims, a bundled default agent/model catalog, runtime catalog overrides, deterministic target failover, and persistent recovery state.
--- ---
# Orchestrate Agent Task Loop # Orchestrate Agent Task Loop
@ -21,7 +21,7 @@ Monitor the file-backed workflow under `agent-task/` and converge ready PLAN imp
## Inputs ## Inputs
- `workspace`: trusted repository root containing `agent-task/`; defaults to the current directory. - `workspace`: trusted repository root containing `agent-task/`; defaults to the current directory.
- `execution_catalog`: required runtime agent/model catalog path, supplied with `--execution-catalog` or `AGENT_TASK_EXECUTION_CATALOG`. - `execution_catalog`: optional runtime agent/model catalog override, supplied with `--execution-catalog` or `AGENT_TASK_EXECUTION_CATALOG`; otherwise use `assets/default-execution-catalog.json`.
- `task_group`: optional `agent-task/<task_group>` scope. - `task_group`: optional `agent-task/<task_group>` scope.
- `dry_run`: inspect routes, dependencies, claims, and catalog validity without launching an agent. - `dry_run`: inspect routes, dependencies, claims, and catalog validity without launching an agent.
- `max_parallel`: workspace-wide active task-stage limit; defaults to `3`; `0` means unlimited. - `max_parallel`: workspace-wide active task-stage limit; defaults to `3`; `0` means unlimited.
@ -32,14 +32,14 @@ Monitor the file-backed workflow under `agent-task/` and converge ready PLAN imp
## Preconditions ## Preconditions
- Read the current plan and code-review contracts routed by `agent-ops/skills/common/router.md`. - Read the current plan and code-review contracts routed by `agent-ops/skills/common/router.md`.
- Obtain the execution catalog from the runtime or project layer. Common owns no default agent, model, provider, or route catalog. - Use the bundled default catalog unless the runtime or project layer supplies an override.
- Run `--dry-run` before the first live execution. - Run `--dry-run` before the first live execution.
- Never bypass the physical-workspace dispatcher lock. - Never bypass the physical-workspace dispatcher lock.
- Keep automatic approval inside the current workspace and the PLAN's declared write set. - Keep automatic approval inside the current workspace and the PLAN's declared write set.
## Runtime catalog contract ## Runtime catalog contract
The catalog root contains exactly `schema_version`, `targets`, and `routes`. It must cover `worker` and `review`, and each stage must define every `local-G01` through `local-G10` and `cloud-G01` through `cloud-G10` route. The bundled catalog is `assets/default-execution-catalog.json`. Catalog resolution order is explicit argument, `AGENT_TASK_EXECUTION_CATALOG`, then the bundled default. Every catalog root contains exactly `schema_version`, `targets`, and `routes`. It must cover `worker` and `review`, and each stage must define every `local-G01` through `local-G10` and `cloud-G01` through `cloud-G10` route.
Each target has: Each target has:
@ -57,17 +57,17 @@ Each route owns its ordered `candidates` plus optional `rule_id`, `policy_priori
Before work starts, the dispatcher: Before work starts, the dispatcher:
1. loads and validates the entire catalog; 1. resolves and validates the entire catalog;
2. verifies exact route coverage and every target reference; 2. verifies exact route coverage and every target reference;
3. verifies each target command is executable; 3. verifies each target command is executable;
4. runs an optional target `preflight_command` for live execution; 4. runs an optional target `preflight_command` for live execution;
5. records the catalog source and SHA-256 revision in the decision. 5. records the catalog source and SHA-256 revision in the decision.
A persisted decision is valid only while the injected catalog revision and selected target snapshot still match. Catalog changes fail closed instead of silently changing an active work unit. A persisted decision is valid only while the resolved catalog revision and selected target snapshot still match. Catalog changes fail closed instead of silently changing an active work unit.
## Selection and failover ## Selection and failover
- Initial execution selects the first candidate in the injected route. - Initial execution selects the first candidate in the resolved route.
- Resume pins the persisted target and route revision. - Resume pins the persisted target and route revision.
- The dispatcher never queries quota before admission and never accepts a quota snapshot as selector input. - The dispatcher never queries quota before admission and never accepts a quota snapshot as selector input.
- Classify actual terminal output after an attempt. `provider-quota`, `context-limit`, `model-unavailable`, `provider-stream-disconnect`, and `provider-connection` may advance to the next unused route candidate. - Classify actual terminal output after an attempt. `provider-quota`, `context-limit`, `model-unavailable`, `provider-stream-disconnect`, and `provider-connection` may advance to the next unused route candidate.
@ -121,18 +121,17 @@ Accept self-check completion only when `## Implementation Checklist` or its supp
```bash ```bash
python3 agent-ops/skills/common/orchestrate-agent-task-loop/scripts/dispatch.py \ python3 agent-ops/skills/common/orchestrate-agent-task-loop/scripts/dispatch.py \
--workspace /absolute/repository \ --workspace /absolute/repository \
--execution-catalog /runtime/config/execution-catalog.json \
--dry-run --dry-run
``` ```
Remove `--dry-run` to start execution. Add `--task-group <name>`, `--max-parallel <n>`, or `--retry-blocked` only when requested by the workflow. Remove `--dry-run` to start execution. Add `--execution-catalog <path>` only to override the bundled default. Add `--task-group <name>`, `--max-parallel <n>`, or `--retry-blocked` only when requested by the workflow.
Launch the live dispatcher as one persistent foreground process. Do not wrap it in an arbitrary timeout and do not start a second dispatcher after a normal tool yield. Wait on the same execution handle until an attention event or terminal exit. Launch the live dispatcher as one persistent foreground process. Do not wrap it in an arbitrary timeout and do not start a second dispatcher after a normal tool yield. Wait on the same execution handle until an attention event or terminal exit.
## Completion checklist ## Completion checklist
- [ ] Catalog was injected, fully validated, preflighted, and revision-pinned. - [ ] The resolved catalog was fully validated, preflighted, and revision-pinned.
- [ ] No fixed common agent/model/provider route or quota probe was used. - [ ] No hidden route outside the resolved catalog or quota probe was used.
- [ ] Runtime quota errors moved only to the next catalog candidate. - [ ] Runtime quota errors moved only to the next catalog candidate.
- [ ] Dependencies, write claims, and workspace concurrency were enforced. - [ ] Dependencies, write claims, and workspace concurrency were enforced.
- [ ] Required self-check and official review stages completed. - [ ] Required self-check and official review stages completed.

View file

@ -1,4 +1,4 @@
interface: interface:
display_name: "Agent Task Loop Orchestrator" display_name: "Agent Task Loop Orchestrator"
short_description: "Orchestrate PLAN and review loops with an injected runtime catalog" short_description: "Orchestrate PLAN and review loops with a default runtime catalog"
default_prompt: "Use $orchestrate-agent-task-loop to execute the active agent-task workflow." default_prompt: "Use $orchestrate-agent-task-loop to execute the active agent-task workflow with the bundled catalog or a runtime override."

View file

@ -0,0 +1,73 @@
{
"schema_version": "1.0",
"targets": {
"codex-sol-xhigh": {
"agent": "codex",
"model": "gpt-5.6-sol",
"execution_class": "cloud_model",
"selfcheck_required": false,
"runtime": {
"command": [
"codex",
"exec",
"--json",
"-C",
"{workspace}",
"-m",
"{model}",
"-c",
"model_reasoning_effort=\"xhigh\"",
"--dangerously-bypass-approvals-and-sandbox",
"{prompt}"
],
"output_format": "jsonl"
}
}
},
"routes": {
"worker": {
"local-G01": {"candidates": ["codex-sol-xhigh"]},
"local-G02": {"candidates": ["codex-sol-xhigh"]},
"local-G03": {"candidates": ["codex-sol-xhigh"]},
"local-G04": {"candidates": ["codex-sol-xhigh"]},
"local-G05": {"candidates": ["codex-sol-xhigh"]},
"local-G06": {"candidates": ["codex-sol-xhigh"]},
"local-G07": {"candidates": ["codex-sol-xhigh"]},
"local-G08": {"candidates": ["codex-sol-xhigh"]},
"local-G09": {"candidates": ["codex-sol-xhigh"]},
"local-G10": {"candidates": ["codex-sol-xhigh"]},
"cloud-G01": {"candidates": ["codex-sol-xhigh"]},
"cloud-G02": {"candidates": ["codex-sol-xhigh"]},
"cloud-G03": {"candidates": ["codex-sol-xhigh"]},
"cloud-G04": {"candidates": ["codex-sol-xhigh"]},
"cloud-G05": {"candidates": ["codex-sol-xhigh"]},
"cloud-G06": {"candidates": ["codex-sol-xhigh"]},
"cloud-G07": {"candidates": ["codex-sol-xhigh"]},
"cloud-G08": {"candidates": ["codex-sol-xhigh"]},
"cloud-G09": {"candidates": ["codex-sol-xhigh"]},
"cloud-G10": {"candidates": ["codex-sol-xhigh"]}
},
"review": {
"local-G01": {"candidates": ["codex-sol-xhigh"]},
"local-G02": {"candidates": ["codex-sol-xhigh"]},
"local-G03": {"candidates": ["codex-sol-xhigh"]},
"local-G04": {"candidates": ["codex-sol-xhigh"]},
"local-G05": {"candidates": ["codex-sol-xhigh"]},
"local-G06": {"candidates": ["codex-sol-xhigh"]},
"local-G07": {"candidates": ["codex-sol-xhigh"]},
"local-G08": {"candidates": ["codex-sol-xhigh"]},
"local-G09": {"candidates": ["codex-sol-xhigh"]},
"local-G10": {"candidates": ["codex-sol-xhigh"]},
"cloud-G01": {"candidates": ["codex-sol-xhigh"]},
"cloud-G02": {"candidates": ["codex-sol-xhigh"]},
"cloud-G03": {"candidates": ["codex-sol-xhigh"]},
"cloud-G04": {"candidates": ["codex-sol-xhigh"]},
"cloud-G05": {"candidates": ["codex-sol-xhigh"]},
"cloud-G06": {"candidates": ["codex-sol-xhigh"]},
"cloud-G07": {"candidates": ["codex-sol-xhigh"]},
"cloud-G08": {"candidates": ["codex-sol-xhigh"]},
"cloud-G09": {"candidates": ["codex-sol-xhigh"]},
"cloud-G10": {"candidates": ["codex-sol-xhigh"]}
}
}
}

View file

@ -6175,8 +6175,8 @@ def parse_args() -> argparse.Namespace:
parser.add_argument( parser.add_argument(
"--execution-catalog", "--execution-catalog",
help=( help=(
"runtime agent/model catalog JSON; alternatively set " "runtime agent/model catalog JSON override; alternatively set "
"AGENT_TASK_EXECUTION_CATALOG" "AGENT_TASK_EXECUTION_CATALOG; defaults to the bundled catalog"
), ),
) )
parser.add_argument("--dry-run", action="store_true", help="classify and print without launching CLIs") parser.add_argument("--dry-run", action="store_true", help="classify and print without launching CLIs")

View file

@ -1,9 +1,9 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Runtime-injected execution-target catalog and route policy. """Execution-target catalog and route policy.
This common module intentionally owns no agent or model catalog. A caller The selector supplies either the bundled default catalog or a runtime override.
supplies a JSON catalog at runtime; this module validates it and resolves one This module validates that catalog and resolves one ordered route without
ordered route without interpreting provider-specific identities. interpreting provider-specific identities.
""" """
from __future__ import annotations from __future__ import annotations
@ -35,7 +35,7 @@ ALLOWED_TEMPLATE_FIELDS = {
class CatalogError(ValueError): class CatalogError(ValueError):
"""The injected execution catalog is missing or malformed.""" """The resolved execution catalog is unreadable or malformed."""
@dataclass(frozen=True) @dataclass(frozen=True)

View file

@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Select an execution target from a runtime-injected catalog. """Select an execution target from a bundled or runtime-overridden catalog.
The selector performs no quota lookup. Every initial candidate is eligible; The selector performs no quota lookup. Every initial candidate is eligible;
runtime failures such as ``provider-quota`` advance to the next catalog entry. runtime failures such as ``provider-quota`` advance to the next catalog entry.
@ -19,6 +19,11 @@ from pathlib import Path
SCHEMA_VERSION = "2.0" SCHEMA_VERSION = "2.0"
CATALOG_ENV = "AGENT_TASK_EXECUTION_CATALOG" CATALOG_ENV = "AGENT_TASK_EXECUTION_CATALOG"
DEFAULT_CATALOG_PATH = (
Path(__file__).resolve().parents[1]
/ "assets"
/ "default-execution-catalog.json"
)
TIMEZONE_NAME = "UTC" TIMEZONE_NAME = "UTC"
_FILENAME_RE = re.compile(r"^(PLAN|CODE_REVIEW)-(local|cloud)-G(\d{2})\.md$") _FILENAME_RE = re.compile(r"^(PLAN|CODE_REVIEW)-(local|cloud)-G(\d{2})\.md$")
_MILESTONE_TASK_ID_PATTERN = r"[A-Za-z0-9]+(?:[-_+=][A-Za-z0-9]+){0,3}" _MILESTONE_TASK_ID_PATTERN = r"[A-Za-z0-9]+(?:[-_+=][A-Za-z0-9]+){0,3}"
@ -63,12 +68,11 @@ class SelectorInputError(Exception):
def resolve_catalog_path(value: str | Path | None = None) -> Path: def resolve_catalog_path(value: str | Path | None = None) -> Path:
raw = str(value) if value is not None else os.environ.get(CATALOG_ENV, "") raw = str(value) if value is not None else os.environ.get(CATALOG_ENV, "")
if not raw: return (
raise SelectorInputError( Path(raw).expanduser().resolve()
"missing_execution_catalog", if raw
f"inject the execution catalog with --catalog or {CATALOG_ENV}", else DEFAULT_CATALOG_PATH.resolve()
) )
return Path(raw).expanduser().resolve()
def load_runtime_catalog(value: str | Path | None = None): def load_runtime_catalog(value: str | Path | None = None):
@ -261,7 +265,7 @@ def _catalog_matches_prior(catalog, prior: dict, decision) -> None:
if evidence["revision"] != catalog.revision: if evidence["revision"] != catalog.revision:
raise SelectorInputError( raise SelectorInputError(
code, code,
"the injected execution catalog changed after this work unit was selected", "the resolved execution catalog changed after this work unit was selected",
) )
if evidence["route_id"] != decision.route_id: if evidence["route_id"] != decision.route_id:
raise SelectorInputError(code, "persisted catalog route does not match the task route") raise SelectorInputError(code, "persisted catalog route does not match the task route")
@ -271,10 +275,10 @@ def _validate_prior_candidate_identity(prior: dict, *, catalog, decision) -> Non
code = "malformed_prior_decision" code = "malformed_prior_decision"
expected = [_candidate_snapshot(item, rank) for rank, item in enumerate(decision.candidates, 1)] expected = [_candidate_snapshot(item, rank) for rank, item in enumerate(decision.candidates, 1)]
if prior["candidates"] != expected: if prior["candidates"] != expected:
raise SelectorInputError(code, "prior_decision candidates do not match the injected catalog route") raise SelectorInputError(code, "prior_decision candidates do not match the resolved catalog route")
selected = prior["selected"] selected = prior["selected"]
if selected not in [{key: value for key, value in item.items() if key != "candidate_rank"} for item in expected]: if selected not in [{key: value for key, value in item.items() if key != "candidate_rank"} for item in expected]:
raise SelectorInputError(code, "prior_decision selected target is not in the injected route") raise SelectorInputError(code, "prior_decision selected target is not in the resolved route")
def _base_decision( def _base_decision(
@ -406,7 +410,7 @@ def select_execution_target_for_route(
) )
next_index = selected_index + 1 next_index = selected_index + 1
if next_index >= len(route.candidates): if next_index >= len(route.candidates):
raise SelectorInputError("no_failover_candidate", "the injected route has no unused next target") raise SelectorInputError("no_failover_candidate", "the resolved route has no unused next target")
return _base_decision( return _base_decision(
catalog=catalog, catalog=catalog,
route=route, route=route,

View file

@ -313,17 +313,24 @@ class GenericDispatcherContractTests(unittest.TestCase):
) )
self.assertEqual(completed.returncode, 0, completed.stderr) self.assertEqual(completed.returncode, 0, completed.stderr)
def test_dry_run_requires_catalog(self): def test_dry_run_uses_bundled_catalog_by_default(self):
with TemporaryDirectory() as tmp: with TemporaryDirectory() as tmp:
(Path(tmp) / "agent-task").mkdir()
env = {
key: value
for key, value in os.environ.items()
if key != "AGENT_TASK_EXECUTION_CATALOG"
}
env["XDG_STATE_HOME"] = str(Path(tmp) / "state")
completed = subprocess.run( completed = subprocess.run(
[sys.executable, str(SCRIPT), "--workspace", tmp, "--dry-run"], [sys.executable, str(SCRIPT), "--workspace", tmp, "--dry-run"],
capture_output=True, capture_output=True,
text=True, text=True,
env={key: value for key, value in os.environ.items() if key != "AGENT_TASK_EXECUTION_CATALOG"}, env=env,
check=False, check=False,
) )
self.assertEqual(completed.returncode, 2) self.assertEqual(completed.returncode, 0, completed.stderr)
self.assertIn("missing_execution_catalog", completed.stderr) self.assertNotIn("missing_execution_catalog", completed.stderr)
if __name__ == "__main__": if __name__ == "__main__":

View file

@ -212,7 +212,7 @@ class SkillObservationContractTest(unittest.TestCase):
self.assertIn("PID/start-token/process-marker evidence", skill) self.assertIn("PID/start-token/process-marker evidence", skill)
self.assertIn("never queries quota before admission", skill) self.assertIn("never queries quota before admission", skill)
self.assertIn("confirmed quota/rate-limit error advances directly", skill) self.assertIn("confirmed quota/rate-limit error advances directly", skill)
self.assertIn("Common owns no default agent, model, provider, or route catalog", skill) self.assertIn("Use the bundled default catalog", skill)
if __name__ == "__main__": if __name__ == "__main__":

View file

@ -71,12 +71,17 @@ def write_task(
class SelectorTests(unittest.TestCase): class SelectorTests(unittest.TestCase):
def test_catalog_must_be_injected(self): def test_bundled_catalog_is_used_by_default(self):
with TemporaryDirectory() as tmp, mock.patch.dict(os.environ, {}, clear=True): with TemporaryDirectory() as tmp, mock.patch.dict(os.environ, {}, clear=True):
task = write_task(Path(tmp)) task = write_task(Path(tmp))
with self.assertRaises(selector.SelectorInputError) as ctx: result = selector.select_execution_target(task)
selector.select_execution_target(task) self.assertEqual(result["selected"]["target_id"], "codex-sol-xhigh")
self.assertEqual(ctx.exception.code, "missing_execution_catalog") self.assertEqual(result["selected"]["agent"], "codex")
self.assertEqual(result["selected"]["model"], "gpt-5.6-sol")
self.assertEqual(
result["catalog"]["source"],
str(selector.DEFAULT_CATALOG_PATH.resolve()),
)
def test_initial_decision_contains_catalog_evidence_and_no_quota(self): def test_initial_decision_contains_catalog_evidence_and_no_quota(self):
with TemporaryDirectory() as tmp: with TemporaryDirectory() as tmp:
@ -195,7 +200,7 @@ class SelectorTests(unittest.TestCase):
selector.select_execution_target(missing, catalog_path=catalog) selector.select_execution_target(missing, catalog_path=catalog)
self.assertEqual(ctx.exception.code, "missing_milestone_task") self.assertEqual(ctx.exception.code, "missing_milestone_task")
def test_cli_returns_structured_catalog_error(self): def test_cli_uses_bundled_catalog_without_override(self):
with TemporaryDirectory() as tmp: with TemporaryDirectory() as tmp:
task = write_task(Path(tmp)) task = write_task(Path(tmp))
completed = subprocess.run( completed = subprocess.run(
@ -205,9 +210,12 @@ class SelectorTests(unittest.TestCase):
env={key: value for key, value in os.environ.items() if key != selector.CATALOG_ENV}, env={key: value for key, value in os.environ.items() if key != selector.CATALOG_ENV},
check=False, check=False,
) )
self.assertEqual(completed.returncode, 2) self.assertEqual(completed.returncode, 0, completed.stderr)
self.assertEqual(completed.stdout, "") self.assertEqual(completed.stderr, "")
self.assertEqual(json.loads(completed.stderr)["error"]["code"], "missing_execution_catalog") self.assertEqual(
json.loads(completed.stdout)["selected"]["target_id"],
"codex-sol-xhigh",
)
if __name__ == "__main__": if __name__ == "__main__":