G1: remove the dead `model:` line from all 6 .openclaw/agents/*.md prompts — launcher never read it; config (agent_model_*) is the single source of truth. G2: add is_valid_model helper (format check ^claude-…$) applied inside resolve_agent_model's resolution cascade and at the inline --fallback-model read in _spawn. An invalid name is logged and skipped to the next valid level (in the limit: no --model flag), never passed to the CLI, never raises. Format check chosen over an allowlist for forward-compatibility (ADR-001). G3 (routing) and G4 (fallback) intentionally NOT enabled — all agents stay on claude-opus-4-8; agent_fallback_model stays "". Docs (golden source) updated in the same change: README model/effort table + validation, CLAUDE.md, .env.example (ORCH_AGENT_MODEL_*/EFFORT_*/FALLBACK_MODEL), CHANGELOG. Tests: test_agent_frontmatter_no_model.py (G1), extended test_resolve_agent_model.py (G2 never-break). Refs: ORCH-074 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
69 lines
2.7 KiB
Python
69 lines
2.7 KiB
Python
"""ORCH-074 (G1): the dead `model:` frontmatter is gone from all 6 agent prompts.
|
|
|
|
launcher.py never reads frontmatter `model:` — it was a lying/dead declaration
|
|
(claude-sonnet-4-6 / claude-opus-4-7) that contradicted the real model resolved
|
|
from config (ORCH-41). The mine: if someone "fixed" the launcher to read it, every
|
|
agent would silently fall back to a stale model. G1 removes the line entirely so
|
|
config (agent_model_*) stays the single source of truth.
|
|
|
|
TC-01: no .openclaw/agents/*.md contains a `^model:` line in its frontmatter.
|
|
TC-02: each frontmatter is still valid YAML and keeps name/description.
|
|
"""
|
|
import os
|
|
|
|
import pytest
|
|
|
|
try:
|
|
import yaml # PyYAML
|
|
_HAVE_YAML = True
|
|
except Exception: # pragma: no cover - yaml is a test/runtime dep
|
|
_HAVE_YAML = False
|
|
|
|
_AGENTS = ("analyst", "architect", "developer", "reviewer", "tester", "deployer")
|
|
|
|
# tests/ is one level under the repo root; .openclaw/agents lives at the root.
|
|
_AGENTS_DIR = os.path.join(
|
|
os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
|
|
".openclaw", "agents",
|
|
)
|
|
|
|
|
|
def _frontmatter_block(text: str) -> str:
|
|
"""Return the YAML between the first two '---' fences (the frontmatter)."""
|
|
lines = text.splitlines()
|
|
assert lines and lines[0].strip() == "---", "frontmatter must open with '---'"
|
|
end = None
|
|
for i in range(1, len(lines)):
|
|
if lines[i].strip() == "---":
|
|
end = i
|
|
break
|
|
assert end is not None, "frontmatter must close with a second '---'"
|
|
return "\n".join(lines[1:end])
|
|
|
|
|
|
@pytest.mark.parametrize("agent", _AGENTS)
|
|
def test_no_model_line_in_frontmatter(agent):
|
|
"""TC-01: no agent prompt declares a `model:` key in its frontmatter."""
|
|
path = os.path.join(_AGENTS_DIR, f"{agent}.md")
|
|
with open(path, encoding="utf-8") as f:
|
|
block = _frontmatter_block(f.read())
|
|
for line in block.splitlines():
|
|
assert not line.lstrip().startswith("model:"), (
|
|
f"{agent}.md still declares a frontmatter 'model:' line: {line!r}"
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize("agent", _AGENTS)
|
|
def test_frontmatter_still_valid_yaml_with_keys(agent):
|
|
"""TC-02: frontmatter parses as YAML and keeps name/description (no model)."""
|
|
path = os.path.join(_AGENTS_DIR, f"{agent}.md")
|
|
with open(path, encoding="utf-8") as f:
|
|
block = _frontmatter_block(f.read())
|
|
if not _HAVE_YAML:
|
|
pytest.skip("PyYAML not available")
|
|
data = yaml.safe_load(block)
|
|
assert isinstance(data, dict), f"{agent}.md frontmatter is not a YAML mapping"
|
|
assert data.get("name") == agent
|
|
assert data.get("description"), f"{agent}.md lost its description"
|
|
assert "model" not in data, f"{agent}.md frontmatter still has a model key"
|