basic BMAD setup
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
#!/usr/bin/env python3
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = ["tiktoken"]
|
||||
# ///
|
||||
"""prepass: the deterministic pre-pass the review lenses read before opening any file.
|
||||
|
||||
One skill folder in, one JSON object out. Token counts come from count_tokens, path findings from
|
||||
scan_paths and script findings from scan_scripts, all imported from this folder, so the numbers match
|
||||
what each script prints on its own.
|
||||
|
||||
shape_hint is inferred, first match wins:
|
||||
memory-agent `_bmad/memory` in SKILL.md, or scripts/wake.py or scripts/init-sanctum.py present
|
||||
agent customize.toml has an [agent] table
|
||||
rendered-skill SKILL.md calls render_skill.py
|
||||
single-skill-module bmod.toml has both [bmod] and [skill]
|
||||
multi-skill-module bmod.toml has [bmod] only (a module record)
|
||||
script-utility scripts/ exists and SKILL.md is under 60 lines
|
||||
plain-skill everything else; "unknown" when there is no SKILL.md
|
||||
|
||||
Usage:
|
||||
prepass.py <skill-folder>
|
||||
|
||||
Output, one JSON object on stdout:
|
||||
{"skill", "shape_hint", "files": [{"path", "tokens", "kind"}], "skill_md_tokens", "total_tokens",
|
||||
"has_customize", "has_scripts", "scripts": [{"path", "has_pep723", "has_test"}],
|
||||
"description_chars", "has_use_when", "frontmatter_ok", "bmod_kind", "path_findings": [...], "script_findings": [...]}
|
||||
kind is one of entry, prompt, script, test, asset, config, other.
|
||||
bmod_kind is none (no bmod.toml), skill (a module member), record (a record folder), record+skill (a
|
||||
single-skill module) or invalid (a file with neither table, or one that does not parse).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
import tomllib
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
|
||||
from count_tokens import count_tokens, iter_text_files, read_text # noqa: E402
|
||||
from init_skill import USE_WHEN_RE, parse_frontmatter # noqa: E402
|
||||
from scan_paths import scan_skill # noqa: E402
|
||||
from scan_scripts import scan_scripts # noqa: E402
|
||||
|
||||
MEMORY_RE = re.compile(r"_bmad/memory")
|
||||
SCRIPT_UTILITY_MAX_LINES = 60
|
||||
|
||||
|
||||
def kind_of(rel: Path) -> str:
|
||||
parts = rel.parts
|
||||
if "assets" in parts[:-1]:
|
||||
return "asset"
|
||||
if parts[0] == "scripts":
|
||||
return "test" if "tests" in parts[1:-1] else "script"
|
||||
if rel.suffix == ".md":
|
||||
return "entry" if rel.name == "SKILL.md" and len(parts) == 1 else "prompt"
|
||||
if rel.suffix == ".toml":
|
||||
return "config"
|
||||
return "other"
|
||||
|
||||
|
||||
def load_toml(path: Path) -> dict:
|
||||
try:
|
||||
return tomllib.loads(path.read_text(encoding="utf-8"))
|
||||
except (OSError, tomllib.TOMLDecodeError, UnicodeDecodeError):
|
||||
return {}
|
||||
|
||||
|
||||
def shape_hint(root: Path, skill_text: str | None) -> str:
|
||||
if skill_text is None:
|
||||
return "unknown"
|
||||
scripts = root / "scripts"
|
||||
if MEMORY_RE.search(skill_text) or (scripts / "wake.py").is_file() or (scripts / "init-sanctum.py").is_file():
|
||||
return "memory-agent"
|
||||
if "agent" in load_toml(root / "customize.toml"):
|
||||
return "agent"
|
||||
if "render_skill.py" in skill_text:
|
||||
return "rendered-skill"
|
||||
bmod = load_toml(root / "bmod.toml")
|
||||
if "bmod" in bmod:
|
||||
return "single-skill-module" if "skill" in bmod else "multi-skill-module"
|
||||
if scripts.is_dir() and len(skill_text.splitlines()) < SCRIPT_UTILITY_MAX_LINES:
|
||||
return "script-utility"
|
||||
return "plain-skill"
|
||||
|
||||
|
||||
def bmod_kind(root: Path) -> str:
|
||||
manifest = root / "bmod.toml"
|
||||
if not manifest.is_file():
|
||||
return "none"
|
||||
data = load_toml(manifest)
|
||||
has_record = isinstance(data.get("bmod"), dict)
|
||||
has_skill = isinstance(data.get("skill"), dict)
|
||||
if has_record and has_skill:
|
||||
return "record+skill"
|
||||
if has_record:
|
||||
return "record"
|
||||
if has_skill:
|
||||
return "skill"
|
||||
return "invalid"
|
||||
|
||||
|
||||
def build(root: Path) -> dict:
|
||||
skill_path = root / "SKILL.md"
|
||||
skill_text = read_text(skill_path) if skill_path.is_file() else None
|
||||
|
||||
files = []
|
||||
skill_md_tokens = 0
|
||||
for path in iter_text_files(root):
|
||||
rel = path.relative_to(root)
|
||||
tokens, _ = count_tokens(read_text(path))
|
||||
files.append({"path": rel.as_posix(), "tokens": tokens, "kind": kind_of(rel)})
|
||||
if path == skill_path:
|
||||
skill_md_tokens = tokens
|
||||
|
||||
meta = parse_frontmatter(skill_text)[0] if skill_text is not None else None
|
||||
description = (meta or {}).get("description", "")
|
||||
frontmatter_ok = bool(
|
||||
meta and meta.get("name") == root.name and description and set(meta) <= {"name", "description"}
|
||||
)
|
||||
|
||||
scripts_result = scan_scripts(root)
|
||||
return {
|
||||
"skill": root.name,
|
||||
"shape_hint": shape_hint(root, skill_text),
|
||||
"files": files,
|
||||
"skill_md_tokens": skill_md_tokens,
|
||||
"total_tokens": sum(f["tokens"] for f in files),
|
||||
"has_customize": (root / "customize.toml").is_file(),
|
||||
"has_scripts": (root / "scripts").is_dir(),
|
||||
"scripts": [
|
||||
{"path": s["path"], "has_pep723": s["has_pep723"], "has_test": s["has_test"]}
|
||||
for s in scripts_result["scripts"]
|
||||
],
|
||||
"description_chars": len(description),
|
||||
"has_use_when": bool(USE_WHEN_RE.search(description)),
|
||||
"frontmatter_ok": frontmatter_ok,
|
||||
"bmod_kind": bmod_kind(root),
|
||||
"path_findings": scan_skill(root)["findings"],
|
||||
"script_findings": scripts_result["findings"],
|
||||
}
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
p.add_argument("skill", type=Path, help="skill folder to analyze")
|
||||
args = p.parse_args(argv)
|
||||
root = args.skill.expanduser().resolve()
|
||||
if not root.is_dir():
|
||||
p.error(f"not a directory: {args.skill}")
|
||||
print(json.dumps(build(root), indent=2))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user