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

wake-template.py

154 lines · skills/bmad-toolsmith/shapes/memory-agent/assets/wake-template.py at bd70c64

1#!/usr/bin/env python3
2# /// script
3# requires-python = ">=3.11"
4# ///
5"""Waking: load the agent's self in one pass, or route to First Breath.
6
7Run on activation. Determines the mode from the filesystem (and the --pulse
8flag) and, when the sanctum is complete, prints the identity files in a single
9read (PERSONA, CREED, BOND, HOW-I-REMEMBER, CAPABILITIES) followed by a map of the
10memory: each folder under memory/ and raw/ with its file count, the newest
11dated files, how many raw files are still undistilled, when the memory was last
12tended (memory/.tended) with the session notes written since, and
13memory/pending.md when it has content. The memory files themselves are not printed; the agent
14reads the ones a conversation reaches. In --pulse mode it also appends
15PULSE.md. When the sanctum is missing or incomplete, it prints a directive to
16run First Breath, whose script finishes an incomplete sanctum.
18This loads runtime memory only. It never reads or writes config or customize.toml.
20Usage:
21 uv run {skill-root}/scripts/wake.py <project-root> [--pulse]
23 project-root: the folder holding _bmad/
24"""
26import re
27import sys
28from pathlib import Path
30# The skill folder's name is the skill name; this script lives in its scripts/.
31SKILL_NAME = Path(__file__).resolve().parent.parent.name
33# Load order: the "become yourself" set. All must exist for the sanctum to count as born.
34IDENTITY_FILES = [
35 "PERSONA.md",
36 "CREED.md",
37 "BOND.md",
38 "HOW-I-REMEMBER.md",
39 "CAPABILITIES.md",
42RECENT_COUNT = 8
43DATED_RE = re.compile(r"^(\d{4}-\d{2}-\d{2})")
44STATUS_RAW_RE = re.compile(r"^status:\s*raw\s*$", re.MULTILINE)
47def emit(path: Path) -> None:
48 print(f"\n===== {path.name} =====")
49 print(path.read_text(encoding="utf-8").rstrip())
52def folder_lines(root: Path, label: str) -> list[str]:
53 """One line per folder under root (root itself when it has files), with its file count."""
54 if not root.is_dir():
55 return []
56 lines = []
57 direct = [p for p in root.iterdir() if p.is_file() and not p.name.startswith(".")]
58 if direct:
59 lines.append(f"{label}/ ({len(direct)} files)")
60 for folder in sorted(p for p in root.rglob("*") if p.is_dir()):
61 count = sum(1 for p in folder.iterdir() if p.is_file() and not p.name.startswith("."))
62 rel = folder.relative_to(root.parent).as_posix()
63 lines.append(f"{rel}/ ({count} files)")
64 return lines
67def recent_dated(root: Path) -> list[str]:
68 """The newest dated files under root, by the date in their names."""
69 if not root.is_dir():
70 return []
71 dated = [p for p in root.rglob("*") if p.is_file() and DATED_RE.match(p.name)]
72 dated.sort(key=lambda p: (DATED_RE.match(p.name).group(1), p.name), reverse=True)
73 return [p.relative_to(root.parent).as_posix() for p in dated[:RECENT_COUNT]]
76def undistilled(raw: Path) -> list[str]:
77 if not raw.is_dir():
78 return []
79 out = []
80 for p in sorted(raw.iterdir()):
81 if not p.is_file():
82 continue
83 try:
84 head = p.read_text(encoding="utf-8", errors="replace")[:2000]
85 except OSError:
86 continue
87 if STATUS_RAW_RE.search(head):
88 out.append(p.name)
89 return out
92def tending_line(sanctum: Path) -> str:
93 """When memory was last tended and how many session notes came after it."""
94 stamp = sanctum / "memory" / ".tended"
95 tended = stamp.read_text(encoding="utf-8").strip()[:10] if stamp.is_file() else ""
96 sessions = sanctum / "memory" / "sessions"
97 notes = [p.name for p in sessions.iterdir() if p.is_file() and DATED_RE.match(p.name)] if sessions.is_dir() else []
98 since = [n for n in notes if DATED_RE.match(n).group(1) > tended] if tended else notes
99 if tended:
100 return f"Tended: {tended}; session notes since: {len(since)}"
101 return f"Never tended; session notes: {len(since)}"
104def emit_map(sanctum: Path) -> None:
105 print("\n===== memory map =====")
106 lines = folder_lines(sanctum / "memory", "memory") + folder_lines(sanctum / "raw", "raw")
107 print("\n".join(lines) if lines else "(empty: nothing remembered yet)")
108 recent = recent_dated(sanctum / "memory")
109 if recent:
110 print("\nNewest:")
111 print("\n".join(f" {r}" for r in recent))
112 print(f"\n{tending_line(sanctum)}")
113 raw = undistilled(sanctum / "raw")
114 if raw:
115 print(f"\nUndistilled raw ({len(raw)}):")
116 print("\n".join(f" raw/{r}" for r in raw))
117 pending = sanctum / "memory" / "pending.md"
118 if pending.is_file() and pending.read_text(encoding="utf-8").strip():
119 emit(pending)
122def main() -> int:
123 args = sys.argv[1:]
124 pulse = "--pulse" in args
125 positional = [a for a in args if not a.startswith("--")]
126 if not positional:
127 print("Usage: wake.py <project-root> [--pulse]", file=sys.stderr)
128 return 2
130 project_root = Path(positional[0]).resolve()
131 sanctum = project_root / "_bmad" / "memory" / SKILL_NAME
133 missing = [name for name in IDENTITY_FILES if not (sanctum / name).is_file()]
134 if missing:
135 print("MODE: FIRST_BREATH")
136 if sanctum.is_dir():
137 print(f"INCOMPLETE SANCTUM at {sanctum}: missing {', '.join(missing)}")
138 else:
139 print(f"NO SANCTUM at {sanctum}")
140 print("This is your one birth. Load references/first-breath.md and follow it.")
141 return 0
143 print("MODE: PULSE" if pulse else "MODE: WAKING")
144 print(f"Sanctum: {sanctum}")
145 for name in IDENTITY_FILES:
146 emit(sanctum / name)
147 if pulse and (sanctum / "PULSE.md").is_file():
148 emit(sanctum / "PULSE.md")
149 emit_map(sanctum)
150 return 0
153if __name__ == "__main__":
154 raise SystemExit(main())