| 380 | |
| 381 | |
| 382 | def validate_opencode(report: Report) -> None: |
| 383 | root = WORKTREE / ".opencode" |
| 384 | if not root.is_dir(): |
| 385 | return |
| 386 | |
| 387 | # 1. opencode.json |
| 388 | cfg = WORKTREE / "opencode.json" |
| 389 | if cfg.is_file(): |
| 390 | try: |
| 391 | data = json.loads(cfg.read_text()) |
| 392 | except json.JSONDecodeError as e: |
| 393 | report.add( |
| 394 | severity="error", |
| 395 | harness="opencode", |
| 396 | path=cfg, |
| 397 | message=f"JSON parse error: {e}", |
| 398 | remediation="Regenerate via `make generate HARNESS=opencode`.", |
| 399 | ) |
| 400 | else: |
| 401 | if "$schema" not in data: |
| 402 | report.add( |
| 403 | severity="info", |
| 404 | harness="opencode", |
| 405 | path=cfg, |
| 406 | message="missing $schema reference", |
| 407 | remediation='Add "$schema": "https://opencode.ai/config.json" for editor tooling.', |
| 408 | ) |
| 409 | |
| 410 | # 2. Every agent .md has required frontmatter |
| 411 | agents_dir = root / "agents" |
| 412 | if agents_dir.is_dir(): |
| 413 | for agent_md in agents_dir.glob("*.md"): |
| 414 | content = agent_md.read_text() |
| 415 | fm, _ = parse_frontmatter(content) |
| 416 | if not fm: |
| 417 | report.add( |
| 418 | severity="error", |
| 419 | harness="opencode", |
| 420 | path=agent_md, |
| 421 | message="missing or invalid frontmatter", |
| 422 | remediation="Regenerate via `make generate HARNESS=opencode`.", |
| 423 | ) |
| 424 | continue |
| 425 | if fm.get("mode") not in _OPENCODE_MODES: |
| 426 | report.add( |
| 427 | severity="error", |
| 428 | harness="opencode", |
| 429 | path=agent_md, |
| 430 | message=f"mode {fm.get('mode')!r} not in {sorted(_OPENCODE_MODES)}", |
| 431 | remediation="Set mode: subagent on transpiled agents.", |
| 432 | ) |
| 433 | model = fm.get("model", "") |
| 434 | if model and "/" not in model: |
| 435 | report.add( |
| 436 | severity="warning", |
| 437 | harness="opencode", |
| 438 | path=agent_md, |
| 439 | message=f"model {model!r} is not provider-prefixed (e.g. 'anthropic/claude-...')", |