ma-agents 3.13.2 → 3.14.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/lib/agents.js +9 -2
- package/lib/bmad-cache/bmb/.claude-plugin/marketplace.json +3 -3
- package/lib/bmad-cache/bmb/_git_preserved/index +0 -0
- package/lib/bmad-cache/bmb/_git_preserved/objects/pack/pack-b388526edf307cf56f8e65673466cff074105abf.idx +0 -0
- package/lib/bmad-cache/bmb/_git_preserved/objects/pack/{pack-8f8b045fef5af6911495cf3b2a89f1ed75e120f7.pack → pack-b388526edf307cf56f8e65673466cff074105abf.pack} +0 -0
- package/lib/bmad-cache/bmb/_git_preserved/objects/pack/pack-b388526edf307cf56f8e65673466cff074105abf.rev +0 -0
- package/lib/bmad-cache/bmb/_git_preserved/packed-refs +1 -1
- package/lib/bmad-cache/bmb/_git_preserved/refs/heads/main +1 -1
- package/lib/bmad-cache/bmb/_git_preserved/shallow +1 -1
- package/lib/bmad-cache/bmb/package-lock.json +2 -2
- package/lib/bmad-cache/bmb/package.json +1 -1
- package/lib/bmad-cache/bmb/samples/bmad-agent-code-coach/scripts/wake.py +78 -0
- package/lib/bmad-cache/bmb/samples/bmad-agent-creative-muse/scripts/wake.py +78 -0
- package/lib/bmad-cache/bmb/samples/bmad-agent-sentinel/scripts/wake.py +78 -0
- package/lib/bmad-cache/bmb/skills/bmad-agent-builder/assets/customize-template.toml +3 -0
- package/lib/bmad-cache/bmb/skills/bmad-agent-builder/assets/init-sanctum-template.py +11 -5
- package/lib/bmad-cache/bmb/skills/bmad-agent-builder/assets/report-shell.html +1073 -0
- package/lib/bmad-cache/bmb/skills/bmad-agent-builder/assets/wake-template.py +78 -0
- package/lib/bmad-cache/bmb/skills/bmad-agent-builder/customize.toml +48 -0
- package/lib/bmad-cache/bmb/skills/bmad-agent-builder/scripts/count_tokens.py +78 -0
- package/lib/bmad-cache/bmb/skills/bmad-agent-builder/scripts/prepass.py +258 -0
- package/lib/bmad-cache/bmb/skills/bmad-agent-builder/scripts/process-template.py +20 -0
- package/lib/bmad-cache/bmb/skills/bmad-agent-builder/scripts/render_report.py +387 -0
- package/lib/bmad-cache/bmb/skills/bmad-eval-runner/assets/adapter-claude-code.json +9 -0
- package/lib/bmad-cache/bmb/skills/bmad-eval-runner/scripts/aggregate_benchmark.py +236 -0
- package/lib/bmad-cache/bmb/skills/bmad-eval-runner/scripts/memlog.py +197 -0
- package/lib/bmad-cache/bmb/skills/bmad-eval-runner/scripts/run_evals.py +513 -390
- package/lib/bmad-cache/bmb/skills/bmad-eval-runner/scripts/run_triggers.py +347 -251
- package/lib/bmad-cache/bmb/skills/bmad-eval-runner/scripts/tests/test_env_isolation.py +79 -0
- package/lib/bmad-cache/bmb/skills/bmad-eval-runner/scripts/tests/test_trigger_detection.py +123 -0
- package/lib/bmad-cache/bmb/skills/bmad-module-builder/scripts/tests/test-validate-module.py +82 -4
- package/lib/bmad-cache/bmb/skills/bmad-module-builder/scripts/validate-module.py +55 -11
- package/lib/bmad-cache/bmb/skills/bmad-workflow-builder/assets/customize-template.toml +3 -5
- package/lib/bmad-cache/bmb/skills/bmad-workflow-builder/assets/report-shell.html +860 -0
- package/lib/bmad-cache/bmb/skills/bmad-workflow-builder/customize.toml +59 -0
- package/lib/bmad-cache/bmb/skills/bmad-workflow-builder/scripts/count_tokens.py +77 -0
- package/lib/bmad-cache/bmb/skills/bmad-workflow-builder/scripts/init_skill.py +132 -0
- package/lib/bmad-cache/bmb/skills/bmad-workflow-builder/scripts/prepass-prompt-metrics.py +186 -221
- package/lib/bmad-cache/bmb/skills/bmad-workflow-builder/scripts/quick_validate.py +117 -0
- package/lib/bmad-cache/bmb/skills/bmad-workflow-builder/scripts/render_report.py +387 -0
- package/lib/bmad-cache/bmb/skills/bmad-workflow-builder/scripts/tests/test_canon_sync.py +46 -0
- package/lib/bmad-cache/bmb/skills/bmad-workflow-builder/scripts/tests/test_count_tokens.py +180 -0
- package/lib/bmad-cache/bmb/skills/bmad-workflow-builder/scripts/tests/test_render_report.py +171 -0
- package/lib/bmad-cache/cache-manifest.json +4 -4
- package/lib/bmad-customize/bmm-qa.customize.yaml +3 -3
- package/lib/bmad-extension/.claude-plugin/marketplace.json.template +3 -2
- package/lib/bmad-extension/skills/bmad-dev-epic/SKILL.md +101 -0
- package/lib/bmad-extension/skills/bmad-dev-epic/bmad-skill-manifest.yaml +3 -0
- package/lib/bmad-extension/skills/bmad-dev-epic/checklist.md +44 -0
- package/lib/bmad-extension/skills/bmad-dev-epic/customize.toml +54 -0
- package/lib/bmad-extension/skills/bmad-dev-epic/references/story-pipeline.md +112 -0
- package/lib/bmad-extension/skills/bmad-dev-epic/steps/step-01-intake.md +59 -0
- package/lib/bmad-extension/skills/bmad-dev-epic/steps/step-02-plan-waves.md +56 -0
- package/lib/bmad-extension/skills/bmad-dev-epic/steps/step-03-epic-branch.md +35 -0
- package/lib/bmad-extension/skills/bmad-dev-epic/steps/step-04-run-waves.md +49 -0
- package/lib/bmad-extension/skills/bmad-dev-epic/steps/step-05-epic-adversarial.md +31 -0
- package/lib/bmad-extension/skills/bmad-dev-epic/steps/step-06-release.md +57 -0
- package/lib/bmad-extension/skills/bmad-dev-epic/steps/step-07-report.md +40 -0
- package/lib/bmad-extension/skills/ma-agent-sqa/SKILL.md +3 -3
- package/lib/bmad-extension/skills/ma-agent-sqa/bmad-skill-manifest.yaml +1 -1
- package/lib/bmad-extension/skills/module-help.csv +2 -1
- package/lib/bmad-extension/skills/module.yaml +1 -1
- package/lib/bmad-extension/skills/sqa-audit/SKILL.md +2 -2
- package/lib/bmad-extension/skills/sqa-ieee12207/SKILL.md +3 -3
- package/lib/bmad-extension/skills/sqa-requirements-quality/SKILL.md +2 -2
- package/lib/bmad-extension-plugin/.claude-plugin/marketplace.json +4 -3
- package/lib/bmad-extension-plugin/skills/bmad-dev-epic/SKILL.md +101 -0
- package/lib/bmad-extension-plugin/skills/bmad-dev-epic/bmad-skill-manifest.yaml +3 -0
- package/lib/bmad-extension-plugin/skills/bmad-dev-epic/checklist.md +44 -0
- package/lib/bmad-extension-plugin/skills/bmad-dev-epic/customize.toml +54 -0
- package/lib/bmad-extension-plugin/skills/bmad-dev-epic/references/story-pipeline.md +112 -0
- package/lib/bmad-extension-plugin/skills/bmad-dev-epic/steps/step-01-intake.md +59 -0
- package/lib/bmad-extension-plugin/skills/bmad-dev-epic/steps/step-02-plan-waves.md +56 -0
- package/lib/bmad-extension-plugin/skills/bmad-dev-epic/steps/step-03-epic-branch.md +35 -0
- package/lib/bmad-extension-plugin/skills/bmad-dev-epic/steps/step-04-run-waves.md +49 -0
- package/lib/bmad-extension-plugin/skills/bmad-dev-epic/steps/step-05-epic-adversarial.md +31 -0
- package/lib/bmad-extension-plugin/skills/bmad-dev-epic/steps/step-06-release.md +57 -0
- package/lib/bmad-extension-plugin/skills/bmad-dev-epic/steps/step-07-report.md +40 -0
- package/lib/bmad-extension-plugin/skills/ma-agent-sqa/SKILL.md +3 -3
- package/lib/bmad-extension-plugin/skills/ma-agent-sqa/bmad-skill-manifest.yaml +1 -1
- package/lib/bmad-extension-plugin/skills/module-help.csv +2 -1
- package/lib/bmad-extension-plugin/skills/module.yaml +1 -1
- package/lib/bmad-extension-plugin/skills/sqa-audit/SKILL.md +2 -2
- package/lib/bmad-extension-plugin/skills/sqa-ieee12207/SKILL.md +3 -3
- package/lib/bmad-extension-plugin/skills/sqa-requirements-quality/SKILL.md +2 -2
- package/lib/bmad.js +6 -1
- package/package.json +3 -3
- package/lib/.bmad-extension-plugin.build-22612-1782454014750/.claude-plugin/marketplace.json +0 -109
- package/lib/.bmad-extension-plugin.build-22612-1782454014750/skills/module-help.csv +0 -62
- package/lib/.bmad-extension-plugin.build-22612-1782454014750/skills/module.yaml +0 -20
- package/lib/bmad-cache/bmb/_git_preserved/objects/pack/pack-8f8b045fef5af6911495cf3b2a89f1ed75e120f7.idx +0 -0
- package/lib/bmad-cache/bmb/_git_preserved/objects/pack/pack-8f8b045fef5af6911495cf3b2a89f1ed75e120f7.rev +0 -0
- package/lib/bmad-cache/bmb/skills/bmad-agent-builder/references/sample-init-sanctum.py +0 -274
- package/lib/bmad-cache/bmb/skills/bmad-agent-builder/scripts/generate-html-report.py +0 -534
- package/lib/bmad-cache/bmb/skills/bmad-agent-builder/scripts/prepass-execution-deps.py +0 -337
- package/lib/bmad-cache/bmb/skills/bmad-agent-builder/scripts/prepass-prompt-metrics.py +0 -425
- package/lib/bmad-cache/bmb/skills/bmad-agent-builder/scripts/prepass-sanctum-architecture.py +0 -385
- package/lib/bmad-cache/bmb/skills/bmad-agent-builder/scripts/prepass-structure-capabilities.py +0 -482
- package/lib/bmad-cache/bmb/skills/bmad-eval-runner/assets/Dockerfile +0 -29
- package/lib/bmad-cache/bmb/skills/bmad-eval-runner/scripts/docker_setup.py +0 -115
- package/lib/bmad-cache/bmb/skills/bmad-eval-runner/scripts/generate_report.py +0 -184
- package/lib/bmad-cache/bmb/skills/bmad-eval-runner/scripts/pty_runner.py +0 -171
- package/lib/bmad-cache/bmb/skills/bmad-eval-runner/scripts/utils.py +0 -260
- package/lib/bmad-cache/bmb/skills/bmad-workflow-builder/scripts/extract-report-json.py +0 -287
- package/lib/bmad-cache/bmb/skills/bmad-workflow-builder/scripts/generate-html-report.py +0 -588
- package/lib/bmad-cache/bmb/skills/bmad-workflow-builder/scripts/prepass-execution-deps.py +0 -288
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# /// script
|
|
3
|
+
# requires-python = ">=3.10"
|
|
4
|
+
# ///
|
|
5
|
+
"""
|
|
6
|
+
Waking — load the agent's sanctum in one pass, or route to First Breath.
|
|
7
|
+
|
|
8
|
+
Run on activation. Determines the mode from the filesystem (and the --pulse
|
|
9
|
+
flag) and, when the sanctum exists, prints the full identity in a single read
|
|
10
|
+
(INDEX, PERSONA, CREED, BOND, MEMORY, CAPABILITIES) so the agent becomes itself
|
|
11
|
+
in one shot instead of six. In --pulse mode it also appends PULSE.md. When no
|
|
12
|
+
sanctum exists, it prints a directive to run First Breath.
|
|
13
|
+
|
|
14
|
+
This loads runtime memory only. It never reads or writes config or customize.toml.
|
|
15
|
+
|
|
16
|
+
Usage:
|
|
17
|
+
uv run wake.py <project-root> [--pulse]
|
|
18
|
+
|
|
19
|
+
project-root: The root of the project (where _bmad/ lives)
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import sys
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
SKILL_NAME = "{skillName}"
|
|
26
|
+
|
|
27
|
+
# Load order — the "become yourself" set.
|
|
28
|
+
IDENTITY_FILES = [
|
|
29
|
+
"INDEX.md",
|
|
30
|
+
"PERSONA.md",
|
|
31
|
+
"CREED.md",
|
|
32
|
+
"BOND.md",
|
|
33
|
+
"MEMORY.md",
|
|
34
|
+
"CAPABILITIES.md",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def emit(path: Path) -> None:
|
|
39
|
+
print(f"\n===== {path.name} =====")
|
|
40
|
+
try:
|
|
41
|
+
print(path.read_text(encoding="utf-8").rstrip())
|
|
42
|
+
except FileNotFoundError:
|
|
43
|
+
print(f"(missing: {path.name})")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def main() -> int:
|
|
47
|
+
args = sys.argv[1:]
|
|
48
|
+
pulse = "--pulse" in args
|
|
49
|
+
positional = [a for a in args if not a.startswith("--")]
|
|
50
|
+
if not positional:
|
|
51
|
+
print("Usage: wake.py <project-root> [--pulse]", file=sys.stderr)
|
|
52
|
+
return 2
|
|
53
|
+
|
|
54
|
+
project_root = Path(positional[0]).resolve()
|
|
55
|
+
sanctum = project_root / "_bmad" / "memory" / SKILL_NAME
|
|
56
|
+
|
|
57
|
+
core_ok = (
|
|
58
|
+
sanctum.is_dir()
|
|
59
|
+
and (sanctum / "CREED.md").is_file()
|
|
60
|
+
and (sanctum / "MEMORY.md").is_file()
|
|
61
|
+
)
|
|
62
|
+
if not core_ok:
|
|
63
|
+
print("MODE: FIRST_BREATH")
|
|
64
|
+
print(f"NO SANCTUM at {sanctum}")
|
|
65
|
+
print("This is your one birth. Load references/first-breath.md and follow it.")
|
|
66
|
+
return 0
|
|
67
|
+
|
|
68
|
+
print("MODE: PULSE" if pulse else "MODE: WAKING")
|
|
69
|
+
print(f"Sanctum: {sanctum}")
|
|
70
|
+
for name in IDENTITY_FILES:
|
|
71
|
+
emit(sanctum / name)
|
|
72
|
+
if pulse:
|
|
73
|
+
emit(sanctum / "PULSE.md")
|
|
74
|
+
return 0
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
if __name__ == "__main__":
|
|
78
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# DO NOT EDIT -- overwritten on every update.
|
|
2
|
+
#
|
|
3
|
+
# Customization surface for bmad-agent-builder. This governs how the builder
|
|
4
|
+
# builds: the org-wide context, standards, and gates applied to every agent it
|
|
5
|
+
# produces. It is distinct from the per-built-agent customize.toml the builder
|
|
6
|
+
# emits during an individual build.
|
|
7
|
+
#
|
|
8
|
+
# Override files (not edited here):
|
|
9
|
+
# {project-root}/_bmad/custom/bmad-agent-builder.toml (team)
|
|
10
|
+
# {project-root}/_bmad/custom/bmad-agent-builder.user.toml (personal)
|
|
11
|
+
|
|
12
|
+
[agent]
|
|
13
|
+
|
|
14
|
+
# --- Configurable below. Overrides merge per BMad structural rules: ---
|
|
15
|
+
# scalars: override wins • arrays: append
|
|
16
|
+
|
|
17
|
+
# Steps to run before standard activation (config load, greet).
|
|
18
|
+
# Use for org pre-flight loads or compliance checks.
|
|
19
|
+
activation_steps_prepend = []
|
|
20
|
+
|
|
21
|
+
# Steps to run after intent routing, before the build/analyze loop begins.
|
|
22
|
+
activation_steps_append = []
|
|
23
|
+
|
|
24
|
+
# Standards the builder keeps in mind for the whole session, loaded as context
|
|
25
|
+
# into every build and analyze. Each entry is a literal sentence, a `skill:`
|
|
26
|
+
# skill, or a `file:` path/glob whose contents load as facts. Use for house
|
|
27
|
+
# conventions you want present but not hard-gated (for gates, see build_standards).
|
|
28
|
+
# "Every agent persona names its owner relationship explicitly."
|
|
29
|
+
# "file:{project-root}/_bmad/standards/agent-house-style.md"
|
|
30
|
+
persistent_facts = ["file:{project-root}/**/project-context.md"]
|
|
31
|
+
|
|
32
|
+
# Executed when a build or analyze run completes, after the user has been told
|
|
33
|
+
# the artifact is ready. String scalar (one instruction) or array (in order).
|
|
34
|
+
on_complete = ""
|
|
35
|
+
|
|
36
|
+
# --- Builder gates ---
|
|
37
|
+
|
|
38
|
+
# Hard standards every BUILT agent must satisfy. Unlike persistent_facts
|
|
39
|
+
# (context), these are enforced: applied as build criteria and checked again as
|
|
40
|
+
# a conformance pass during Analyze. Each entry is a `skill:`, `file:`, or
|
|
41
|
+
# plain-text directive. Append-only. Empty by default (no org gates).
|
|
42
|
+
build_standards = []
|
|
43
|
+
|
|
44
|
+
# Eval requirement for a build to be declared done. Empty (default) keeps evals
|
|
45
|
+
# opt-in, offered at the eval beat but never forced.
|
|
46
|
+
# "baseline" -- require a passing baseline run (agent beats the bare model)
|
|
47
|
+
# "any" -- require at least one eval case to exist and pass
|
|
48
|
+
evals_required = ""
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# vendored from bmad-workflow-builder/scripts; canonical source there
|
|
3
|
+
# /// script
|
|
4
|
+
# requires-python = ">=3.9"
|
|
5
|
+
# dependencies = ["tiktoken"]
|
|
6
|
+
# ///
|
|
7
|
+
"""count_tokens — the single length metric for skill authoring.
|
|
8
|
+
|
|
9
|
+
Token counts replace line counts everywhere in the builder and eval-runner.
|
|
10
|
+
This script reports the token length of a file or of text piped on stdin, using
|
|
11
|
+
the tiktoken cl100k_base encoding. When tiktoken is not installed it falls back
|
|
12
|
+
to a character-based estimate (len(text) // 4) and says so, so the script always
|
|
13
|
+
runs under a bare python3 even with no third-party packages present.
|
|
14
|
+
|
|
15
|
+
Usage:
|
|
16
|
+
count_tokens.py <file> count the tokens in a file
|
|
17
|
+
count_tokens.py --stdin count the tokens read from stdin
|
|
18
|
+
|
|
19
|
+
Output (one line of JSON on stdout):
|
|
20
|
+
{"tokens": <int>, "method": "tiktoken"} when tiktoken loaded
|
|
21
|
+
{"tokens": <int>, "method": "fallback"} when it fell back to chars // 4
|
|
22
|
+
|
|
23
|
+
Budgets this feeds: SKILL.md ~1500-2500, multi-branch reference ~4500,
|
|
24
|
+
single-purpose reference ~9000.
|
|
25
|
+
"""
|
|
26
|
+
import argparse
|
|
27
|
+
import json
|
|
28
|
+
import sys
|
|
29
|
+
|
|
30
|
+
ENCODING = "cl100k_base"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def count_tokens(text: str) -> tuple[int, str]:
|
|
34
|
+
"""Return (token_count, method).
|
|
35
|
+
|
|
36
|
+
Tries tiktoken's cl100k_base encoding first. If tiktoken cannot be imported
|
|
37
|
+
or initialized, estimates with len(text) // 4 and reports method "fallback".
|
|
38
|
+
"""
|
|
39
|
+
try:
|
|
40
|
+
import tiktoken
|
|
41
|
+
except Exception:
|
|
42
|
+
return len(text) // 4, "fallback"
|
|
43
|
+
try:
|
|
44
|
+
enc = tiktoken.get_encoding(ENCODING)
|
|
45
|
+
except Exception:
|
|
46
|
+
return len(text) // 4, "fallback"
|
|
47
|
+
return len(enc.encode(text)), "tiktoken"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def read_input(args) -> str:
|
|
51
|
+
if args.stdin:
|
|
52
|
+
return sys.stdin.read()
|
|
53
|
+
with open(args.file, encoding="utf-8") as f:
|
|
54
|
+
return f.read()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def main(argv: list[str] | None = None) -> int:
|
|
58
|
+
p = argparse.ArgumentParser(
|
|
59
|
+
description=__doc__,
|
|
60
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
61
|
+
)
|
|
62
|
+
p.add_argument("file", nargs="?", help="path to the file to count")
|
|
63
|
+
p.add_argument("--stdin", action="store_true", help="read text from stdin instead of a file")
|
|
64
|
+
args = p.parse_args(argv)
|
|
65
|
+
|
|
66
|
+
if not args.stdin and not args.file:
|
|
67
|
+
p.error("provide a file path or --stdin")
|
|
68
|
+
if args.stdin and args.file:
|
|
69
|
+
p.error("provide either a file path or --stdin, not both")
|
|
70
|
+
|
|
71
|
+
text = read_input(args)
|
|
72
|
+
tokens, method = count_tokens(text)
|
|
73
|
+
print(json.dumps({"tokens": tokens, "method": method}))
|
|
74
|
+
return 0
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
if __name__ == "__main__":
|
|
78
|
+
sys.exit(main())
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# /// script
|
|
3
|
+
# requires-python = ">=3.9"
|
|
4
|
+
# dependencies = ["tiktoken"]
|
|
5
|
+
# ///
|
|
6
|
+
"""prepass — the Analyze pre-pass for the agent builder.
|
|
7
|
+
|
|
8
|
+
Reads an agent skill directory and emits one compact JSON object that every
|
|
9
|
+
lens and the analyze orchestrator consume. The pre-pass does the one thing the
|
|
10
|
+
lenses should not each redo: it classifies the agent along the three-point
|
|
11
|
+
gradient (stateless, memory, autonomous), counts tokens for SKILL.md and every
|
|
12
|
+
in-tree file, and sets the gate that turns the conditional sanctum lens on.
|
|
13
|
+
|
|
14
|
+
Detection rests on the sanctum, the built agent's runtime memory at
|
|
15
|
+
`{project-root}/_bmad/memory/{skillName}/`. An agent that reloads a sanctum on
|
|
16
|
+
waking is a memory agent; one that also carries live wake behavior (a PULSE
|
|
17
|
+
file or a pulse/autonomous wake reference with named-task routing) is
|
|
18
|
+
autonomous; one with no sanctum at all is stateless. This is the BUILT agent's
|
|
19
|
+
memory, never the builder's process log (.memlog.md), and the two are kept
|
|
20
|
+
apart here.
|
|
21
|
+
|
|
22
|
+
Lengths come from tokens, never line counts. The count uses count_tokens.py
|
|
23
|
+
(imported as a sibling, then shelled out, then a chars // 4 fallback) so the
|
|
24
|
+
metric matches the rest of the builder and runs under a bare python3.
|
|
25
|
+
|
|
26
|
+
Output contract (one line of JSON on stdout, the pinned prepass shape):
|
|
27
|
+
{
|
|
28
|
+
"agent_type": "stateless" | "memory" | "autonomous",
|
|
29
|
+
"is_memory_agent": bool, # true for memory and autonomous
|
|
30
|
+
"skill_md_tokens": int,
|
|
31
|
+
"files": [{"path": str, "tokens": int}, ...]
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
Read-only over the target agent directory. It opens files to count and classify
|
|
35
|
+
and writes nothing inside the agent tree.
|
|
36
|
+
|
|
37
|
+
Usage:
|
|
38
|
+
prepass.py <agent-dir> classify and count the agent at this directory
|
|
39
|
+
"""
|
|
40
|
+
from __future__ import annotations
|
|
41
|
+
|
|
42
|
+
import argparse
|
|
43
|
+
import json
|
|
44
|
+
import re
|
|
45
|
+
import subprocess
|
|
46
|
+
import sys
|
|
47
|
+
from pathlib import Path
|
|
48
|
+
|
|
49
|
+
SCRIPT_DIR = Path(__file__).resolve().parent
|
|
50
|
+
|
|
51
|
+
# Directories we never descend into while counting agent files.
|
|
52
|
+
SKIP_DIRS = {".git", "__pycache__", ".pytest_cache", "node_modules", ".venv", "venv"}
|
|
53
|
+
|
|
54
|
+
# Extensions we treat as countable text. Binary or opaque assets are skipped.
|
|
55
|
+
TEXT_SUFFIXES = {
|
|
56
|
+
".md", ".py", ".toml", ".yaml", ".yml", ".json", ".txt",
|
|
57
|
+
".csv", ".html", ".sh", ".cfg", ".ini",
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# --- token counting ---------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
def _count_via_import(text: str):
|
|
64
|
+
"""Count tokens by importing the sibling count_tokens module."""
|
|
65
|
+
if str(SCRIPT_DIR) not in sys.path:
|
|
66
|
+
sys.path.insert(0, str(SCRIPT_DIR))
|
|
67
|
+
try:
|
|
68
|
+
import count_tokens # type: ignore
|
|
69
|
+
except Exception:
|
|
70
|
+
return None
|
|
71
|
+
try:
|
|
72
|
+
tokens, _method = count_tokens.count_tokens(text)
|
|
73
|
+
return int(tokens)
|
|
74
|
+
except Exception:
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _count_via_shell(text: str):
|
|
79
|
+
"""Count tokens by shelling out to count_tokens.py with text on stdin."""
|
|
80
|
+
script = SCRIPT_DIR / "count_tokens.py"
|
|
81
|
+
if not script.exists():
|
|
82
|
+
return None
|
|
83
|
+
try:
|
|
84
|
+
proc = subprocess.run(
|
|
85
|
+
[sys.executable, str(script), "--stdin"],
|
|
86
|
+
input=text,
|
|
87
|
+
capture_output=True,
|
|
88
|
+
text=True,
|
|
89
|
+
timeout=60,
|
|
90
|
+
)
|
|
91
|
+
except Exception:
|
|
92
|
+
return None
|
|
93
|
+
if proc.returncode != 0:
|
|
94
|
+
return None
|
|
95
|
+
try:
|
|
96
|
+
return int(json.loads(proc.stdout)["tokens"])
|
|
97
|
+
except Exception:
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def count_tokens(text: str) -> int:
|
|
102
|
+
"""Token length of text via count_tokens.py, falling back to chars // 4.
|
|
103
|
+
|
|
104
|
+
Prefers importing the vendored count_tokens module, then shelling out to it,
|
|
105
|
+
then a bare character estimate so the pre-pass always produces a number.
|
|
106
|
+
"""
|
|
107
|
+
for counter in (_count_via_import, _count_via_shell):
|
|
108
|
+
result = counter(text)
|
|
109
|
+
if result is not None:
|
|
110
|
+
return result
|
|
111
|
+
return len(text) // 4
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def read_text(path: Path) -> str:
|
|
115
|
+
try:
|
|
116
|
+
return path.read_text(encoding="utf-8")
|
|
117
|
+
except (OSError, UnicodeDecodeError):
|
|
118
|
+
return ""
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
# --- agent classification ---------------------------------------------------
|
|
122
|
+
|
|
123
|
+
def iter_files(root: Path):
|
|
124
|
+
"""Yield countable text files under root, skipping noise directories."""
|
|
125
|
+
for path in sorted(root.rglob("*")):
|
|
126
|
+
if not path.is_file():
|
|
127
|
+
continue
|
|
128
|
+
if any(part in SKIP_DIRS for part in path.relative_to(root).parts):
|
|
129
|
+
continue
|
|
130
|
+
if path.suffix.lower() in TEXT_SUFFIXES:
|
|
131
|
+
yield path
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def has_sanctum(root: Path, skill_text: str) -> bool:
|
|
135
|
+
"""True when the agent reloads a runtime sanctum on waking (a memory agent).
|
|
136
|
+
|
|
137
|
+
The sanctum is the built agent's memory at `_bmad/memory/{skillName}/`. We
|
|
138
|
+
treat any of these as a sanctum signal: the SKILL referencing that memory
|
|
139
|
+
path, the Sacred-Truth / waking bootloader language, a wake or init-sanctum
|
|
140
|
+
scaffolder, or the sanctum template assets (PERSONA / CREED / BOND / MEMORY
|
|
141
|
+
/ INDEX / CAPABILITIES). This is the built agent's memory, distinct from the
|
|
142
|
+
builder's .memlog.md, which is never a sanctum signal.
|
|
143
|
+
"""
|
|
144
|
+
if re.search(r"_bmad/memory/", skill_text):
|
|
145
|
+
return True
|
|
146
|
+
if re.search(r"\bsanctum\b", skill_text, re.IGNORECASE):
|
|
147
|
+
return True
|
|
148
|
+
if "Sacred Truth" in skill_text and re.search(r"\b(waking|wake)\b", skill_text, re.IGNORECASE):
|
|
149
|
+
return True
|
|
150
|
+
|
|
151
|
+
for pattern in ("scripts/wake*", "scripts/init-sanctum*"):
|
|
152
|
+
for script in root.glob(pattern):
|
|
153
|
+
if script.is_file():
|
|
154
|
+
return True
|
|
155
|
+
|
|
156
|
+
sanctum_seed = re.compile(
|
|
157
|
+
r"^(PERSONA|CREED|BOND|MEMORY|INDEX|CAPABILITIES)-template\.md$"
|
|
158
|
+
)
|
|
159
|
+
assets = root / "assets"
|
|
160
|
+
if assets.is_dir():
|
|
161
|
+
for asset in assets.iterdir():
|
|
162
|
+
if asset.is_file() and sanctum_seed.match(asset.name):
|
|
163
|
+
return True
|
|
164
|
+
return False
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def has_autonomous_wake(root: Path, skill_text: str) -> bool:
|
|
168
|
+
"""True when a memory agent also carries live autonomous wake behavior.
|
|
169
|
+
|
|
170
|
+
Autonomous is memory plus a PULSE-driven wake: a deployed PULSE.md, a
|
|
171
|
+
pulse/autonomous-wake reference, or SKILL wake routing (named-task pulse
|
|
172
|
+
routing, a default wake behavior, quiet hours, or a wake frequency).
|
|
173
|
+
|
|
174
|
+
The standard memory bootloader already names a Pulse Mode (`--pulse`) path
|
|
175
|
+
that loads PULSE.md, and ships a PULSE template asset, in every memory
|
|
176
|
+
agent. Those are seeds, not live wake behavior, so neither the bootloader's
|
|
177
|
+
Pulse-Mode line nor a PULSE template asset counts here. The wake behavior
|
|
178
|
+
must be deployed: a real PULSE.md, a wake reference file, or SKILL routing
|
|
179
|
+
that names tasks or schedules a recurring wake.
|
|
180
|
+
"""
|
|
181
|
+
if (root / "PULSE.md").is_file():
|
|
182
|
+
return True
|
|
183
|
+
|
|
184
|
+
refs = root / "references"
|
|
185
|
+
if refs.is_dir():
|
|
186
|
+
for ref in refs.iterdir():
|
|
187
|
+
name = ref.name.lower()
|
|
188
|
+
if ref.is_file() and ("pulse-wake" in name or "autonomous-wake" in name):
|
|
189
|
+
return True
|
|
190
|
+
|
|
191
|
+
wake_signals = [
|
|
192
|
+
r"--pulse:\{", # named-task pulse routing
|
|
193
|
+
r"-p:\{", # short-flag named-task routing
|
|
194
|
+
r"default pulse wake",
|
|
195
|
+
r"default wake behavior",
|
|
196
|
+
r"\bquiet hours\b",
|
|
197
|
+
r"wake frequency",
|
|
198
|
+
r"autonomous wake",
|
|
199
|
+
]
|
|
200
|
+
for pattern in wake_signals:
|
|
201
|
+
if re.search(pattern, skill_text, re.IGNORECASE):
|
|
202
|
+
return True
|
|
203
|
+
return False
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def classify(root: Path, skill_text: str) -> str:
|
|
207
|
+
"""Return the agent_type along the gradient."""
|
|
208
|
+
if not has_sanctum(root, skill_text):
|
|
209
|
+
return "stateless"
|
|
210
|
+
if has_autonomous_wake(root, skill_text):
|
|
211
|
+
return "autonomous"
|
|
212
|
+
return "memory"
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
# --- main -------------------------------------------------------------------
|
|
216
|
+
|
|
217
|
+
def build_payload(root: Path) -> dict:
|
|
218
|
+
skill_path = root / "SKILL.md"
|
|
219
|
+
skill_text = read_text(skill_path) if skill_path.is_file() else ""
|
|
220
|
+
|
|
221
|
+
agent_type = classify(root, skill_text)
|
|
222
|
+
is_memory_agent = agent_type in ("memory", "autonomous")
|
|
223
|
+
|
|
224
|
+
files = []
|
|
225
|
+
skill_md_tokens = 0
|
|
226
|
+
for path in iter_files(root):
|
|
227
|
+
tokens = count_tokens(read_text(path))
|
|
228
|
+
rel = path.relative_to(root).as_posix()
|
|
229
|
+
files.append({"path": rel, "tokens": tokens})
|
|
230
|
+
if path == skill_path:
|
|
231
|
+
skill_md_tokens = tokens
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
"agent_type": agent_type,
|
|
235
|
+
"is_memory_agent": is_memory_agent,
|
|
236
|
+
"skill_md_tokens": skill_md_tokens,
|
|
237
|
+
"files": files,
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def main(argv: list[str] | None = None) -> int:
|
|
242
|
+
p = argparse.ArgumentParser(
|
|
243
|
+
description=__doc__,
|
|
244
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
245
|
+
)
|
|
246
|
+
p.add_argument("agent_dir", help="path to the agent skill directory to analyze")
|
|
247
|
+
args = p.parse_args(argv)
|
|
248
|
+
|
|
249
|
+
root = Path(args.agent_dir).expanduser().resolve()
|
|
250
|
+
if not root.is_dir():
|
|
251
|
+
p.error(f"not a directory: {root}")
|
|
252
|
+
|
|
253
|
+
print(json.dumps(build_payload(root)))
|
|
254
|
+
return 0
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
if __name__ == "__main__":
|
|
258
|
+
sys.exit(main())
|
|
@@ -5,6 +5,13 @@ Performs deterministic variable substitution and conditional block processing
|
|
|
5
5
|
on template files from assets/. Replaces {varName} placeholders with provided
|
|
6
6
|
values and evaluates {if-X}...{/if-X} conditional blocks, keeping content
|
|
7
7
|
when the condition is in the --true list and removing the entire block otherwise.
|
|
8
|
+
|
|
9
|
+
Any {if-X} or {/if-X} marker still present after processing is a defect (a
|
|
10
|
+
malformed or mismatched block the emitted agent would ship verbatim): the
|
|
11
|
+
script exits 3 and names the markers. Remaining {token} placeholders are
|
|
12
|
+
reported in the --json metadata as tokens_remaining, not failed, because they
|
|
13
|
+
may be runtime-resolution tokens such as {project-root} or {agent.<name>} —
|
|
14
|
+
the builder judges that list against the build-time token set.
|
|
8
15
|
"""
|
|
9
16
|
|
|
10
17
|
# /// script
|
|
@@ -160,6 +167,18 @@ def main() -> int:
|
|
|
160
167
|
content, conds_true, conds_false = process_conditionals(content, true_conditions)
|
|
161
168
|
content, vars_substituted = process_variables(content, variables)
|
|
162
169
|
|
|
170
|
+
# Leftover conditional markers mean a malformed/mismatched block that
|
|
171
|
+
# would ship verbatim in the emitted agent.
|
|
172
|
+
leftover_markers = sorted(set(re.findall(r'\{/?if-[a-zA-Z0-9_-]+\}', content)))
|
|
173
|
+
if leftover_markers:
|
|
174
|
+
print(
|
|
175
|
+
f"Error: leftover conditional markers after processing: {', '.join(leftover_markers)}",
|
|
176
|
+
file=sys.stderr,
|
|
177
|
+
)
|
|
178
|
+
return 3
|
|
179
|
+
|
|
180
|
+
tokens_remaining = sorted(set(re.findall(r'\{[a-zA-Z][a-zA-Z0-9_.-]*\}', content)))
|
|
181
|
+
|
|
163
182
|
# Write output
|
|
164
183
|
output_file = args.output
|
|
165
184
|
try:
|
|
@@ -180,6 +199,7 @@ def main() -> int:
|
|
|
180
199
|
'vars_substituted': vars_substituted,
|
|
181
200
|
'conditions_true': conds_true,
|
|
182
201
|
'conditions_false': conds_false,
|
|
202
|
+
'tokens_remaining': tokens_remaining,
|
|
183
203
|
}
|
|
184
204
|
print(json.dumps(metadata, indent=2), file=sys.stderr)
|
|
185
205
|
|