basic BMAD setup

This commit is contained in:
Ebbe Baß
2026-10-15 08:57:23 +02:00
parent 44029127dc
commit 5e747654b9
438 changed files with 41818 additions and 0 deletions
@@ -0,0 +1,23 @@
---
name: {name}
description: '{description}'
---
# {title}
{purpose: one or two lines on what the script does and what the result is for.} The script does the work; this file says how to run it and what to do with what comes back.
## Run
```bash
uv run {skill-root}/scripts/{script}.py {arguments}
```
`--help` lists every option. {When to pass which option, one line each, only where the choice takes judgment.}
## Read the result
{The output shape: the JSON keys and what each means, or what the text report shows. What a good result looks like, and what each kind of finding asks you to do next.}
## When it fails
A non-zero exit prints one line on stderr saying why. Report that line to the user; do not do the job by hand instead. {Failures the user can fix and what to tell them: a missing input, a malformed file, a path outside the project.}
@@ -0,0 +1,47 @@
#!/usr/bin/env python3
# /// script
# requires-python = ">=3.11"
# ///
"""{script} — {purpose}.
{What the script reads, what it writes, and the shape of its output. This docstring is the
`--help` text, so it is the whole reference: say what each result means.}
Exit codes: 0 with the result as JSON on stdout; 1 with a one-line reason on stderr.
"""
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
class ScriptError(Exception):
"""A failure the user can act on. Its message is the one line printed on exit."""
def run(path: Path, *, strict: bool = False) -> dict:
"""{The work. Return the result as a dict so main() can print it as JSON.}"""
if not path.exists():
raise ScriptError(f"not found: {path}")
return {"path": str(path), "strict": strict}
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument("path", type=Path, help="{what the positional input is}")
parser.add_argument("--strict", action="store_true", help="{what strict changes}")
args = parser.parse_args(argv)
try:
result = run(args.path, strict=args.strict)
except ScriptError as error:
print(f"{parser.prog}: {error}", file=sys.stderr)
return 1
print(json.dumps(result, ensure_ascii=False, indent=2))
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,38 @@
import json
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
SCRIPT = Path(__file__).resolve().parents[1] / "{script}.py"
def run(*args: str) -> subprocess.CompletedProcess[str]:
"""Run the script as a subprocess, the way the agent does."""
return subprocess.run([sys.executable, str(SCRIPT), *args], capture_output=True, text=True, check=False)
class ScriptTest(unittest.TestCase):
def setUp(self) -> None:
tmp = tempfile.TemporaryDirectory()
self.addCleanup(tmp.cleanup)
self.root = Path(tmp.name)
def test_reports_the_result(self) -> None:
fixture = self.root / "input.txt"
fixture.write_text("{fixture contents}\n", encoding="utf-8")
result = run(str(fixture))
self.assertEqual(result.returncode, 0, result.stderr)
data = json.loads(result.stdout)
self.assertEqual(data["path"], str(fixture))
def test_fails_with_one_line(self) -> None:
result = run(str(self.root / "missing.txt"))
self.assertEqual(result.returncode, 1)
self.assertEqual(result.stderr.count("\n"), 1, result.stderr)
self.assertIn("not found", result.stderr)
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,20 @@
# Shape: script utility
A thin `SKILL.md` over a script that does the work. The conversation has reached it when the job is deterministic: the same input gives the same output, success is checkable, and a model doing it by hand would be slower and less reliable than code (file transforms, extraction, counting, validation, driving a fixed CLI). The prose only tells the agent how to run the script and what to do with the result.
## Files
| File | From |
|---|---|
| `SKILL.md` | `assets/SKILL-template.md` |
| `scripts/<name>.py` | `assets/script-template.py` |
| `scripts/tests/test_<name>.py` | `assets/test-template.py` |
| `bmod.toml` | the scaffold, `init_skill.py --bmod <record> --source <update_source>`, when the skill joins a module; `ecosystem.md` adds its dependency lists; a skill outside BMad has none. When other skills must call the script, add `scripts = ["scripts/<name>.py"]` under `[skill]`, which installs it to `_bmad/scripts/` |
## Rules
- `SKILL.md` says what the script does, the one command that runs it, how to read its output, and what to do when it fails. Nothing the script's `--help` already says.
- The call form is always `uv run {skill-root}/scripts/<name>.py ...`. A script installed for other skills is called as `uv run {project-root}/_bmad/scripts/<name>.py ...`.
- The script carries a PEP 723 header with `requires-python = ">=3.11"` and uses argparse, so `--help` is the reference. It prints one JSON object with stable keys when a model reads the result, plain text when a person does. On failure it exits non-zero with a one-line reason on stderr, so the agent reports instead of guessing. No network calls and no model ids baked in.
- Judgment stays out of the script. When a result needs interpreting, `SKILL.md` says what each outcome asks the agent to do next; the script does not decide for it.
- The test uses `unittest`, runs the script as a subprocess on a fixture built in a temp directory, and covers the success path and the one-line failure.