Skip to main content
On this page

verify_repo.py

verify_repo.py — validate the adk v3 repo layout.

Source

scripts/verify_repo.py

Contents

Python
#!/usr/bin/env python3"""verify_repo.py — validate the adk v3 repo layout.Checks:- every skill in skills/ has SKILL.md with required frontmatter- every adk-mcp-*.json is valid JSON with required fields- shared/* required files exist- decision-log schema is well-formed JSON examples- no references to deleted v2 plugin paths anywhereExit 0 on clean. Exit 1 on errors. Warnings don't fail.Usage:  python3 scripts/verify_repo.py [--verbose]"""from __future__ import annotationsimport argparseimport jsonimport reimport sysfrom pathlib import PathREPO = Path(__file__).resolve().parent.parentSKILLS_DIR = REPO / "skills"MCP_DIR = REPO / "mcp"SHARED_DIR = REPO / "shared"SHARED_REQUIRED = [    "constitution.md", "advisor.md", "question-first.md", "decision-log-schema.md",    "seed-decisions.jsonl",]# v2 was deleted entirely; any reference to the old plugin layout in v3 docs is stale.errors: list[str] = []warnings: list[str] = []def err(msg: str) -> None:    errors.append(msg)def warn(msg: str) -> None:    warnings.append(msg)def check_skills(verbose: bool) -> None:    if not SKILLS_DIR.exists():        err(f"missing {SKILLS_DIR}")        return    expected = {        "adk-implement", "adk-review", "adk-investigate", "adk-document",        "adk-sync", "adk-setup", "adk-improve", "adk-explain",    }    found = {p.name for p in SKILLS_DIR.iterdir() if p.is_dir()}    missing = expected - found    if missing:        err(f"missing skills: {sorted(missing)}")    extra = found - expected    if extra:        warn(f"unexpected skill directories: {sorted(extra)}")    for skill_dir in sorted(SKILLS_DIR.iterdir()):        if not skill_dir.is_dir():            continue        skill_md = skill_dir / "SKILL.md"        if not skill_md.exists():            err(f"{skill_dir.name}: missing SKILL.md")            continue        content = skill_md.read_text(encoding="utf-8")        m = re.match(r"^---\s*\n(.*?)\n---\s*\n", content, re.DOTALL)        if not m:            err(f"{skill_dir.name}: SKILL.md missing YAML frontmatter")            continue        fm = m.group(1)        for field in ("name:", "description:"):            if field not in fm:                err(f"{skill_dir.name}: SKILL.md frontmatter missing `{field}`")        if verbose:            print(f"[skill] {skill_dir.name}: ok", file=sys.stderr)def check_mcps(verbose: bool) -> None:    if not MCP_DIR.exists():        err(f"missing {MCP_DIR}")        return    for mcp_file in sorted(MCP_DIR.glob("adk-mcp-*.json")):        try:            data = json.loads(mcp_file.read_text(encoding="utf-8"))        except json.JSONDecodeError as e:            err(f"{mcp_file.name}: invalid JSON ({e})")            continue        if "name" not in data:            err(f"{mcp_file.name}: missing `name`")        if "description" not in data:            warn(f"{mcp_file.name}: missing `description`")        # Must be either http+url or command-based        if "url" not in data and "command" not in data:            err(f"{mcp_file.name}: must declare either `url` or `command`")        if verbose:            print(f"[mcp] {mcp_file.name}: ok", file=sys.stderr)def check_shared(verbose: bool) -> None:    if not SHARED_DIR.exists():        err(f"missing {SHARED_DIR}")        return    for required in SHARED_REQUIRED:        if not (SHARED_DIR / required).exists():            err(f"missing shared/{required}")    seed = SHARED_DIR / "seed-decisions.jsonl"    if seed.exists():        for i, line in enumerate(seed.read_text(encoding="utf-8").splitlines(), 1):            line = line.strip()            if not line:                continue            try:                json.loads(line)            except json.JSONDecodeError as e:                err(f"seed-decisions.jsonl:{i}: invalid JSON ({e})")    if verbose:        print("[shared] checked", file=sys.stderr)def check_stale_v2_in_v3() -> None:    # v2 was deleted; any reference to plugins/adk-* in v3 source is now stale.    # Skip docs/reference/ (auto-generated; legitimately embeds script source containing the patterns)    # and node_modules/ (third-party).    for p in REPO.rglob("*.md"):        s = str(p)        if ".temp/" in s or ".git/" in s or "/docs/reference/" in s or "/node_modules/" in s:            continue        rel = p.relative_to(REPO)        text = p.read_text(encoding="utf-8", errors="ignore")        for bad in ("plugins/adk-core/", "plugins/adk-code/", "plugins/adk-review/",                    "plugins/adk-docs/", "plugins/adk-investigate/"):            if bad in text:                warn(f"{rel}: references deleted v2 path `{bad}`")def main() -> int:    ap = argparse.ArgumentParser()    ap.add_argument("--verbose", "-v", action="store_true")    args = ap.parse_args()    check_shared(args.verbose)    check_skills(args.verbose)    check_mcps(args.verbose)    check_stale_v2_in_v3()    print(f"verify_repo: errors={len(errors)} warnings={len(warnings)}")    for w in warnings:        print(f"  warn: {w}")    for e in errors:        print(f"  err: {e}")    return 1 if errors else 0if __name__ == "__main__":    raise SystemExit(main())