Skill Tracer
Skills › BMAD Method › bmad-toolsmith › init-sanctum-template.py

init-sanctum-template.py

155 lines · skills/bmad-toolsmith/shapes/memory-agent/assets/init-sanctum-template.py at bd70c64

1#!/usr/bin/env python3
2# /// script
3# requires-python = ">=3.11"
4# ///
5"""First Breath: deterministic sanctum scaffolding.
6
7Runs before the conversational awakening. Creates the sanctum folder and its
8memory layout, writes every sanctum template in the skill's assets/, copies the
9capability references and their scripts into the sanctum, and fills
10CAPABILITIES.md from the capability files' frontmatter.
12Safe to rerun. A file that already exists is never rewritten, so a run that was
13cut off partway is finished by the next one, and a finished sanctum is left
14alone. The owner's name and language are not read from anywhere: the agent
15learns them in the First Breath conversation and writes them to BOND.md.
17After this runs the sanctum is self-contained: the agent depends on the skill
18bundle only for First Breath, wake.py and this script.
20Usage:
21 uv run {skill-root}/scripts/init-sanctum.py <project-root> <skill-path>
23 project-root: the folder holding _bmad/
24 skill-path: the skill folder (SKILL.md, references/, assets/, scripts/)
26Exit 0 and a report on stdout; exit 2 on a usage error.
27"""
29import re
30import shutil
31import sys
32from datetime import date
33from pathlib import Path
35# The skill folder's name is the skill name; this script lives in its scripts/.
36SKILL_NAME = Path(__file__).resolve().parent.parent.name
38# References that stay in the skill bundle: used once, at First Breath.
39SKILL_ONLY_FILES = {"first-breath.md"}
41# Bootloader-side scripts; capability scripts are everything else in scripts/.
42BOOT_SCRIPTS = {"init-sanctum.py", "wake.py"}
44# The memory layout HOW-I-REMEMBER.md describes. The agent adds folders under memory/ as kinds of subject appear.
45MEMORY_DIRS = ("memory/sessions", "raw", "capabilities")
48def parse_frontmatter(file_path: Path) -> dict:
49 """Extract YAML frontmatter from a markdown file."""
50 meta = {}
51 content = file_path.read_text(encoding="utf-8")
52 match = re.match(r"^---\s*\n(.*?)\n---", content, re.DOTALL)
53 if not match:
54 return meta
55 for line in match.group(1).strip().split("\n"):
56 if ":" in line:
57 key, _, value = line.partition(":")
58 meta[key.strip()] = value.strip().strip("'\"")
59 return meta
62def copy_files(source_dir: Path, dest_dir: Path, skip: set[str]) -> list[str]:
63 """Copy every file in source_dir that the destination lacks, except the skipped names."""
64 if not source_dir.is_dir():
65 return []
66 files = [f for f in sorted(source_dir.iterdir()) if f.is_file() and f.name not in skip]
67 copied = []
68 if files:
69 dest_dir.mkdir(parents=True, exist_ok=True)
70 for source_file in files:
71 target = dest_dir / source_file.name
72 if target.exists():
73 continue
74 shutil.copy2(source_file, target)
75 copied.append(source_file.name)
76 return copied
79def capabilities_table(references_dir: Path) -> str:
80 """Rows for CAPABILITIES.md from references with `name` and `code` frontmatter."""
81 rows = []
82 for md_file in sorted(references_dir.glob("*.md")) if references_dir.is_dir() else []:
83 if md_file.name in SKILL_ONLY_FILES:
84 continue
85 meta = parse_frontmatter(md_file)
86 if meta.get("name") and meta.get("code"):
87 rows.append(
88 f"| [{meta['code']}] | {meta['name']} | {meta.get('description', '')} | `references/{md_file.name}` |"
89 )
90 return "\n".join(rows)
93def substitute_vars(content: str, variables: dict) -> str:
94 """Replace {var_name} placeholders with values from the variables dict."""
95 for key, value in variables.items():
96 content = content.replace(f"{{{key}}}", value)
97 return content
100def main() -> int:
101 if len(sys.argv) < 3:
102 print("Usage: init-sanctum.py <project-root> <skill-path>", file=sys.stderr)
103 return 2
105 project_root = Path(sys.argv[1]).resolve()
106 skill_path = Path(sys.argv[2]).resolve()
107 sanctum_path = project_root / "_bmad" / "memory" / SKILL_NAME
108 assets_dir = skill_path / "assets"
109 references_dir = skill_path / "references"
111 existed = sanctum_path.is_dir()
112 variables = {
113 "birth_date": date.today().isoformat(),
114 "project_root": str(project_root),
115 "sanctum_path": str(sanctum_path),
116 "capabilities-table": capabilities_table(references_dir),
117 }
119 sanctum_path.mkdir(parents=True, exist_ok=True)
120 for rel in MEMORY_DIRS:
121 (sanctum_path / rel).mkdir(parents=True, exist_ok=True)
123 copied_refs = copy_files(references_dir, sanctum_path / "references", SKILL_ONLY_FILES)
124 copied_scripts = copy_files(skill_path / "scripts", sanctum_path / "scripts", BOOT_SCRIPTS)
126 created = []
127 for template_path in sorted(assets_dir.glob("*-template.md")):
128 output_name = template_path.name.removesuffix("-template.md").upper() + ".md"
129 target = sanctum_path / output_name
130 if target.exists():
131 continue
132 content = substitute_vars(template_path.read_text(encoding="utf-8"), variables)
133 target.write_text(content, encoding="utf-8")
134 created.append(output_name)
136 if existed and not created and not copied_refs and not copied_scripts:
137 print(f"Sanctum already exists at {sanctum_path}")
138 print("This agent has already been born. Nothing to scaffold.")
139 return 0
141 print(f"{'Finished' if existed else 'Created'} sanctum at {sanctum_path}")
142 for name in created:
143 print(f" Created {name}")
144 if copied_refs:
145 print(f" Copied {len(copied_refs)} reference files to references/")
146 if copied_scripts:
147 print(f" Copied {len(copied_scripts)} scripts to scripts/")
148 print()
149 print("First Breath scaffolding complete. The conversational awakening can now begin.")
150 print(f"Sanctum: {sanctum_path}")
151 return 0
154if __name__ == "__main__":
155 raise SystemExit(main())