basic BMAD setup
This commit is contained in:
@@ -0,0 +1,155 @@
|
||||
#!/usr/bin/env python3
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# ///
|
||||
"""First Breath: deterministic sanctum scaffolding.
|
||||
|
||||
Runs before the conversational awakening. Creates the sanctum folder and its
|
||||
memory layout, writes every sanctum template in the skill's assets/, copies the
|
||||
capability references and their scripts into the sanctum, and fills
|
||||
CAPABILITIES.md from the capability files' frontmatter.
|
||||
|
||||
Safe to rerun. A file that already exists is never rewritten, so a run that was
|
||||
cut off partway is finished by the next one, and a finished sanctum is left
|
||||
alone. The owner's name and language are not read from anywhere: the agent
|
||||
learns them in the First Breath conversation and writes them to BOND.md.
|
||||
|
||||
After this runs the sanctum is self-contained: the agent depends on the skill
|
||||
bundle only for First Breath, wake.py and this script.
|
||||
|
||||
Usage:
|
||||
uv run {skill-root}/scripts/init-sanctum.py <project-root> <skill-path>
|
||||
|
||||
project-root: the folder holding _bmad/
|
||||
skill-path: the skill folder (SKILL.md, references/, assets/, scripts/)
|
||||
|
||||
Exit 0 and a report on stdout; exit 2 on a usage error.
|
||||
"""
|
||||
|
||||
import re
|
||||
import shutil
|
||||
import sys
|
||||
from datetime import date
|
||||
from pathlib import Path
|
||||
|
||||
# The skill folder's name is the skill name; this script lives in its scripts/.
|
||||
SKILL_NAME = Path(__file__).resolve().parent.parent.name
|
||||
|
||||
# References that stay in the skill bundle: used once, at First Breath.
|
||||
SKILL_ONLY_FILES = {"first-breath.md"}
|
||||
|
||||
# Bootloader-side scripts; capability scripts are everything else in scripts/.
|
||||
BOOT_SCRIPTS = {"init-sanctum.py", "wake.py"}
|
||||
|
||||
# The memory layout HOW-I-REMEMBER.md describes. The agent adds folders under memory/ as kinds of subject appear.
|
||||
MEMORY_DIRS = ("memory/sessions", "raw", "capabilities")
|
||||
|
||||
|
||||
def parse_frontmatter(file_path: Path) -> dict:
|
||||
"""Extract YAML frontmatter from a markdown file."""
|
||||
meta = {}
|
||||
content = file_path.read_text(encoding="utf-8")
|
||||
match = re.match(r"^---\s*\n(.*?)\n---", content, re.DOTALL)
|
||||
if not match:
|
||||
return meta
|
||||
for line in match.group(1).strip().split("\n"):
|
||||
if ":" in line:
|
||||
key, _, value = line.partition(":")
|
||||
meta[key.strip()] = value.strip().strip("'\"")
|
||||
return meta
|
||||
|
||||
|
||||
def copy_files(source_dir: Path, dest_dir: Path, skip: set[str]) -> list[str]:
|
||||
"""Copy every file in source_dir that the destination lacks, except the skipped names."""
|
||||
if not source_dir.is_dir():
|
||||
return []
|
||||
files = [f for f in sorted(source_dir.iterdir()) if f.is_file() and f.name not in skip]
|
||||
copied = []
|
||||
if files:
|
||||
dest_dir.mkdir(parents=True, exist_ok=True)
|
||||
for source_file in files:
|
||||
target = dest_dir / source_file.name
|
||||
if target.exists():
|
||||
continue
|
||||
shutil.copy2(source_file, target)
|
||||
copied.append(source_file.name)
|
||||
return copied
|
||||
|
||||
|
||||
def capabilities_table(references_dir: Path) -> str:
|
||||
"""Rows for CAPABILITIES.md from references with `name` and `code` frontmatter."""
|
||||
rows = []
|
||||
for md_file in sorted(references_dir.glob("*.md")) if references_dir.is_dir() else []:
|
||||
if md_file.name in SKILL_ONLY_FILES:
|
||||
continue
|
||||
meta = parse_frontmatter(md_file)
|
||||
if meta.get("name") and meta.get("code"):
|
||||
rows.append(
|
||||
f"| [{meta['code']}] | {meta['name']} | {meta.get('description', '')} | `references/{md_file.name}` |"
|
||||
)
|
||||
return "\n".join(rows)
|
||||
|
||||
|
||||
def substitute_vars(content: str, variables: dict) -> str:
|
||||
"""Replace {var_name} placeholders with values from the variables dict."""
|
||||
for key, value in variables.items():
|
||||
content = content.replace(f"{{{key}}}", value)
|
||||
return content
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if len(sys.argv) < 3:
|
||||
print("Usage: init-sanctum.py <project-root> <skill-path>", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
project_root = Path(sys.argv[1]).resolve()
|
||||
skill_path = Path(sys.argv[2]).resolve()
|
||||
sanctum_path = project_root / "_bmad" / "memory" / SKILL_NAME
|
||||
assets_dir = skill_path / "assets"
|
||||
references_dir = skill_path / "references"
|
||||
|
||||
existed = sanctum_path.is_dir()
|
||||
variables = {
|
||||
"birth_date": date.today().isoformat(),
|
||||
"project_root": str(project_root),
|
||||
"sanctum_path": str(sanctum_path),
|
||||
"capabilities-table": capabilities_table(references_dir),
|
||||
}
|
||||
|
||||
sanctum_path.mkdir(parents=True, exist_ok=True)
|
||||
for rel in MEMORY_DIRS:
|
||||
(sanctum_path / rel).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
copied_refs = copy_files(references_dir, sanctum_path / "references", SKILL_ONLY_FILES)
|
||||
copied_scripts = copy_files(skill_path / "scripts", sanctum_path / "scripts", BOOT_SCRIPTS)
|
||||
|
||||
created = []
|
||||
for template_path in sorted(assets_dir.glob("*-template.md")):
|
||||
output_name = template_path.name.removesuffix("-template.md").upper() + ".md"
|
||||
target = sanctum_path / output_name
|
||||
if target.exists():
|
||||
continue
|
||||
content = substitute_vars(template_path.read_text(encoding="utf-8"), variables)
|
||||
target.write_text(content, encoding="utf-8")
|
||||
created.append(output_name)
|
||||
|
||||
if existed and not created and not copied_refs and not copied_scripts:
|
||||
print(f"Sanctum already exists at {sanctum_path}")
|
||||
print("This agent has already been born. Nothing to scaffold.")
|
||||
return 0
|
||||
|
||||
print(f"{'Finished' if existed else 'Created'} sanctum at {sanctum_path}")
|
||||
for name in created:
|
||||
print(f" Created {name}")
|
||||
if copied_refs:
|
||||
print(f" Copied {len(copied_refs)} reference files to references/")
|
||||
if copied_scripts:
|
||||
print(f" Copied {len(copied_scripts)} scripts to scripts/")
|
||||
print()
|
||||
print("First Breath scaffolding complete. The conversational awakening can now begin.")
|
||||
print(f"Sanctum: {sanctum_path}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user