| 524 | |
| 525 | |
| 526 | def validate_gemini(report: Report) -> None: |
| 527 | skills_dir = WORKTREE / "skills" |
| 528 | agents_dir = WORKTREE / "agents" |
| 529 | commands_dir = WORKTREE / "commands" |
| 530 | |
| 531 | # 1. Every TOML command parses + has description + prompt |
| 532 | if commands_dir.is_dir(): |
| 533 | for toml_path in commands_dir.rglob("*.toml"): |
| 534 | try: |
| 535 | data = tomllib.loads(toml_path.read_text(encoding="utf-8")) |
| 536 | except tomllib.TOMLDecodeError as e: |
| 537 | report.add( |
| 538 | severity="error", |
| 539 | harness="gemini", |
| 540 | path=toml_path, |
| 541 | message=f"TOML parse error: {e}", |
| 542 | remediation="Likely a quoting issue in the command body. Regenerate or escape triple-quotes.", |
| 543 | ) |
| 544 | continue |
| 545 | if "description" not in data or "prompt" not in data: |
| 546 | report.add( |
| 547 | severity="error", |
| 548 | harness="gemini", |
| 549 | path=toml_path, |
| 550 | message=f"missing keys (have: {sorted(data.keys())})", |
| 551 | remediation="Gemini TOML requires both `description` and `prompt`.", |
| 552 | ) |
| 553 | if "prompt" in data and "{{args}}" not in data["prompt"]: |
| 554 | report.add( |
| 555 | severity="warning", |
| 556 | harness="gemini", |
| 557 | path=toml_path, |
| 558 | message="prompt does not include {{args}} placeholder", |
| 559 | remediation="Append {{args}} so user input is appended to the prompt.", |
| 560 | ) |
| 561 | |
| 562 | # 2. Every native skill has frontmatter name matching directory |
| 563 | if skills_dir.is_dir(): |
| 564 | for skill_md in skills_dir.glob("*/SKILL.md"): |
| 565 | content = skill_md.read_text() |
| 566 | fm, _ = parse_frontmatter(content) |
| 567 | if fm.get("name") != skill_md.parent.name: |
| 568 | report.add( |
| 569 | severity="error", |
| 570 | harness="gemini", |
| 571 | path=skill_md, |
| 572 | message=f"frontmatter name {fm.get('name')!r} != directory {skill_md.parent.name!r}", |
| 573 | remediation="Gemini auto-discovers by directory; name must match.", |
| 574 | ) |
| 575 | |
| 576 | # 3. Subagents have a Gemini-compatible model |
| 577 | if agents_dir.is_dir(): |
| 578 | valid_model_prefixes = ("gemini-",) |
| 579 | for agent_md in agents_dir.glob("*.md"): |
| 580 | fm, _ = parse_frontmatter(agent_md.read_text()) |
| 581 | model = fm.get("model", "") |
| 582 | if model and not model.startswith(valid_model_prefixes): |
| 583 | report.add( |