bmad-method 6.8.1-next.2 → 6.8.1-next.20
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/.claude-plugin/marketplace.json +9 -3
- package/package.json +10 -4
- package/src/bmm-skills/1-analysis/bmad-prfaq/SKILL.md +2 -2
- package/src/bmm-skills/1-analysis/bmad-product-brief/SKILL.md +8 -8
- package/src/bmm-skills/2-plan-workflows/bmad-prd/SKILL.md +7 -7
- package/src/bmm-skills/2-plan-workflows/bmad-prd/assets/headless-schemas.md +2 -2
- package/src/bmm-skills/2-plan-workflows/bmad-prd/customize.toml +1 -1
- package/src/bmm-skills/2-plan-workflows/bmad-prd/references/headless.md +1 -1
- package/src/bmm-skills/2-plan-workflows/bmad-prd/references/validate.md +1 -1
- package/src/bmm-skills/2-plan-workflows/bmad-ux/SKILL.md +8 -8
- package/src/bmm-skills/2-plan-workflows/bmad-ux/assets/design-directions.md +1 -1
- package/src/bmm-skills/2-plan-workflows/bmad-ux/assets/headless-schemas.md +2 -2
- package/src/bmm-skills/2-plan-workflows/bmad-ux/assets/key-screens.md +4 -4
- package/src/bmm-skills/2-plan-workflows/bmad-ux/customize.toml +1 -1
- package/src/bmm-skills/2-plan-workflows/bmad-ux/references/creative-tools.md +1 -1
- package/src/bmm-skills/2-plan-workflows/bmad-ux/references/headless.md +1 -1
- package/src/bmm-skills/2-plan-workflows/bmad-ux/references/validate.md +2 -2
- package/src/bmm-skills/3-solutioning/bmad-agent-architect/customize.toml +2 -2
- package/src/bmm-skills/3-solutioning/bmad-architecture/SKILL.md +85 -0
- package/src/bmm-skills/3-solutioning/bmad-architecture/assets/spine-template.md +79 -0
- package/src/bmm-skills/3-solutioning/bmad-architecture/customize.toml +100 -0
- package/src/bmm-skills/3-solutioning/bmad-architecture/references/headless.md +26 -0
- package/src/bmm-skills/3-solutioning/bmad-architecture/references/reviewer-gate.md +13 -0
- package/src/bmm-skills/3-solutioning/bmad-architecture/scripts/lint_spine.py +257 -0
- package/src/bmm-skills/3-solutioning/bmad-architecture/scripts/tests/test_lint_spine.py +270 -0
- package/src/bmm-skills/3-solutioning/bmad-create-architecture/SKILL.md +16 -60
- package/src/bmm-skills/3-solutioning/bmad-create-epics-and-stories/steps/step-01-validate-prerequisites.md +12 -4
- package/src/bmm-skills/4-implementation/bmad-code-review/steps/step-02-review.md +1 -1
- package/src/bmm-skills/4-implementation/bmad-quick-dev/step-04-review.md +1 -1
- package/src/bmm-skills/4-implementation/bmad-retrospective/SKILL.md +29 -14
- package/src/bmm-skills/4-implementation/bmad-retrospective/customize.toml +1 -1
- package/src/bmm-skills/4-implementation/bmad-sprint-planning/SKILL.md +20 -1
- package/src/bmm-skills/4-implementation/bmad-sprint-planning/checklist.md +2 -1
- package/src/bmm-skills/4-implementation/bmad-sprint-planning/sprint-status-template.yaml +13 -0
- package/src/bmm-skills/4-implementation/bmad-sprint-status/SKILL.md +13 -0
- package/src/bmm-skills/module-help.csv +2 -2
- package/src/core-skills/bmad-brainstorming/SKILL.md +8 -10
- package/src/core-skills/bmad-brainstorming/references/converge.md +1 -1
- package/src/core-skills/bmad-brainstorming/references/finalize.md +1 -1
- package/src/core-skills/bmad-brainstorming/references/headless.md +4 -4
- package/src/core-skills/bmad-brainstorming/references/in-chat-techniques.md +1 -1
- package/src/core-skills/bmad-brainstorming/references/mode-autonomous.md +1 -1
- package/src/core-skills/bmad-brainstorming/scripts/tests/test_brain.py +2 -2
- package/src/core-skills/bmad-customize/scripts/tests/test_list_customizable_skills.py +1 -1
- package/src/core-skills/bmad-forge-idea/SKILL.md +79 -0
- package/src/core-skills/bmad-forge-idea/customize.toml +42 -0
- package/src/core-skills/bmad-forge-idea/scripts/resolve_personas.py +270 -0
- package/src/core-skills/bmad-forge-idea/scripts/tests/test_resolve_personas.py +138 -0
- package/src/core-skills/bmad-party-mode/SKILL.md +39 -56
- package/src/core-skills/bmad-party-mode/customize.toml +175 -0
- package/src/core-skills/bmad-party-mode/references/create-party.md +70 -0
- package/src/core-skills/bmad-party-mode/references/mode-agent-team.md +11 -0
- package/src/core-skills/bmad-party-mode/references/mode-auto.md +13 -0
- package/src/core-skills/bmad-party-mode/references/mode-subagent.md +19 -0
- package/src/core-skills/bmad-party-mode/references/party-memory.md +51 -0
- package/src/core-skills/bmad-party-mode/scripts/resolve_party.py +272 -0
- package/src/core-skills/bmad-party-mode/scripts/tests/test-resolve_party.py +146 -0
- package/src/core-skills/bmad-spec/SKILL.md +25 -9
- package/src/core-skills/bmad-spec/assets/headless-schemas.md +3 -3
- package/src/core-skills/bmad-spec/assets/spec-template.md +4 -4
- package/src/core-skills/module-help.csv +1 -0
- package/src/{core-skills/bmad-brainstorming/scripts → scripts}/memlog.py +56 -34
- package/src/scripts/resolve_config.py +8 -6
- package/src/scripts/resolve_customization.py +8 -6
- package/src/{core-skills/bmad-brainstorming/scripts → scripts}/tests/test_memlog.py +68 -27
- package/tools/installer/commands/install.js +3 -0
- package/tools/installer/core/installer.js +35 -1
- package/tools/installer/core/uv-check.js +97 -0
- package/tools/installer/core/wsl-node-check.js +109 -0
- package/tools/installer/ide/platform-codes.yaml +14 -0
- package/tools/installer/install-messages.yaml +4 -0
- package/tools/installer/ui.js +11 -0
- package/evals/bmm-skills/bmad-product-brief/evals.json +0 -237
- package/evals/bmm-skills/bmad-product-brief/files/branfield-memo.md +0 -46
- package/evals/bmm-skills/bmad-product-brief/files/forkbird-brief/addendum.md +0 -40
- package/evals/bmm-skills/bmad-product-brief/files/forkbird-brief/brief.md +0 -56
- package/evals/bmm-skills/bmad-product-brief/files/forkbird-brief/decision-log.md +0 -27
- package/evals/bmm-skills/bmad-product-brief/files/meridian-mobility-report.md +0 -116
- package/evals/bmm-skills/bmad-product-brief/files/mossridge-brief/addendum.md +0 -41
- package/evals/bmm-skills/bmad-product-brief/files/mossridge-brief/brief.md +0 -57
- package/evals/bmm-skills/bmad-product-brief/files/mossridge-brief/decision-log.md +0 -29
- package/evals/bmm-skills/bmad-product-brief/files/pantry-bridge-interviews.md +0 -90
- package/evals/bmm-skills/bmad-product-brief/files/q2-brainstorm.md +0 -101
- package/evals/bmm-skills/bmad-product-brief/triggers.json +0 -18
- package/src/bmm-skills/3-solutioning/bmad-create-architecture/architecture-decision-template.md +0 -12
- package/src/bmm-skills/3-solutioning/bmad-create-architecture/data/domain-complexity.csv +0 -13
- package/src/bmm-skills/3-solutioning/bmad-create-architecture/data/project-types.csv +0 -7
- package/src/bmm-skills/3-solutioning/bmad-create-architecture/steps/step-01-init.md +0 -153
- package/src/bmm-skills/3-solutioning/bmad-create-architecture/steps/step-01b-continue.md +0 -173
- package/src/bmm-skills/3-solutioning/bmad-create-architecture/steps/step-02-context.md +0 -224
- package/src/bmm-skills/3-solutioning/bmad-create-architecture/steps/step-03-starter.md +0 -329
- package/src/bmm-skills/3-solutioning/bmad-create-architecture/steps/step-04-decisions.md +0 -318
- package/src/bmm-skills/3-solutioning/bmad-create-architecture/steps/step-05-patterns.md +0 -359
- package/src/bmm-skills/3-solutioning/bmad-create-architecture/steps/step-06-structure.md +0 -379
- package/src/bmm-skills/3-solutioning/bmad-create-architecture/steps/step-07-validation.md +0 -361
- package/src/bmm-skills/3-solutioning/bmad-create-architecture/steps/step-08-complete.md +0 -82
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# /// script
|
|
3
|
+
# requires-python = ">=3.11"
|
|
4
|
+
# ///
|
|
5
|
+
"""Unit tests for resolve_party.py — merge, alias, override, group resolution."""
|
|
6
|
+
|
|
7
|
+
import sys
|
|
8
|
+
import unittest
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
12
|
+
import resolve_party as rp # noqa: E402
|
|
13
|
+
|
|
14
|
+
AGENTS = {
|
|
15
|
+
"bmad-agent-analyst": {"name": "Mary", "icon": "📊", "title": "Analyst"},
|
|
16
|
+
"bmad-agent-pm": {"name": "John", "icon": "📋", "title": "PM"},
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class TestAlias(unittest.TestCase):
|
|
21
|
+
def test_strips_known_prefixes(self):
|
|
22
|
+
self.assertEqual(rp._alias("bmad-agent-analyst"), "analyst")
|
|
23
|
+
self.assertEqual(rp._alias("bmad-foo"), "foo")
|
|
24
|
+
|
|
25
|
+
def test_passes_through_unprefixed(self):
|
|
26
|
+
self.assertEqual(rp._alias("morpheus"), "morpheus")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class TestBuildCollective(unittest.TestCase):
|
|
30
|
+
def test_installed_agents_indexed_by_code_alias_and_name(self):
|
|
31
|
+
col, idx, _ = rp.build_collective(AGENTS, [])
|
|
32
|
+
self.assertEqual(set(col), {"bmad-agent-analyst", "bmad-agent-pm"})
|
|
33
|
+
self.assertEqual(idx["analyst"], "bmad-agent-analyst") # alias
|
|
34
|
+
self.assertEqual(idx["mary"], "bmad-agent-analyst") # name (ci)
|
|
35
|
+
self.assertEqual(idx["bmad-agent-pm"], "bmad-agent-pm") # full code
|
|
36
|
+
self.assertEqual(col["bmad-agent-analyst"]["source"], "installed")
|
|
37
|
+
|
|
38
|
+
def test_custom_member_appends(self):
|
|
39
|
+
col, _, _ = rp.build_collective(AGENTS, [{"code": "morpheus", "name": "Morpheus", "persona": "riddles"}])
|
|
40
|
+
self.assertIn("morpheus", col)
|
|
41
|
+
self.assertEqual(col["morpheus"]["source"], "custom")
|
|
42
|
+
self.assertEqual(col["morpheus"]["persona"], "riddles")
|
|
43
|
+
|
|
44
|
+
def test_custom_overrides_installed_by_alias(self):
|
|
45
|
+
col, _, _ = rp.build_collective(AGENTS, [{"code": "analyst", "name": "Mary-Custom", "persona": "p"}])
|
|
46
|
+
# Override lands on the canonical installed code, not a new "analyst" entry.
|
|
47
|
+
self.assertNotIn("analyst", col)
|
|
48
|
+
self.assertEqual(col["bmad-agent-analyst"]["source"], "custom")
|
|
49
|
+
self.assertEqual(col["bmad-agent-analyst"]["name"], "Mary-Custom")
|
|
50
|
+
|
|
51
|
+
def test_member_without_code_skipped(self):
|
|
52
|
+
col, _, _ = rp.build_collective(AGENTS, [{"name": "Nameless"}])
|
|
53
|
+
self.assertEqual(set(col), {"bmad-agent-analyst", "bmad-agent-pm"})
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class TestResolveMembers(unittest.TestCase):
|
|
57
|
+
def setUp(self):
|
|
58
|
+
self.col, self.idx, _ = rp.build_collective(AGENTS, [{"code": "morpheus", "name": "Morpheus"}])
|
|
59
|
+
|
|
60
|
+
def test_resolves_in_listed_order_and_flags_unknowns(self):
|
|
61
|
+
resolved, unresolved = rp.resolve_members(["morpheus", "analyst", "ghost"], self.col, self.idx)
|
|
62
|
+
self.assertEqual([m["code"] for m in resolved], ["morpheus", "bmad-agent-analyst"])
|
|
63
|
+
self.assertEqual(unresolved, ["ghost"])
|
|
64
|
+
|
|
65
|
+
def test_empty(self):
|
|
66
|
+
self.assertEqual(rp.resolve_members([], self.col, self.idx), ([], []))
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class TestGroups(unittest.TestCase):
|
|
70
|
+
GROUPS = [
|
|
71
|
+
{"id": "wr", "name": "Writers", "members": ["analyst", "morpheus"]},
|
|
72
|
+
{"id": "bad"}, # no name -> falls back to id; no members -> count 0
|
|
73
|
+
{"name": "no-id"}, # dropped from menu
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
def test_menu_is_names_only_with_counts_and_open_cast_flag(self):
|
|
77
|
+
menu = rp.group_menu(self.GROUPS)
|
|
78
|
+
self.assertEqual(menu, [
|
|
79
|
+
{"id": "wr", "name": "Writers", "member_count": 2},
|
|
80
|
+
{"id": "bad", "name": "bad", "member_count": 0, "open_cast": True},
|
|
81
|
+
])
|
|
82
|
+
|
|
83
|
+
def test_find_group(self):
|
|
84
|
+
self.assertEqual(rp.find_group(self.GROUPS, "wr")["name"], "Writers")
|
|
85
|
+
self.assertIsNone(rp.find_group(self.GROUPS, "missing"))
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class TestGroupDetail(unittest.TestCase):
|
|
89
|
+
def setUp(self):
|
|
90
|
+
self.col, self.idx, _ = rp.build_collective(AGENTS, [{"code": "morpheus", "name": "Morpheus"}])
|
|
91
|
+
|
|
92
|
+
def test_scene_passes_through_when_present(self):
|
|
93
|
+
g = {"id": "tos-10-forward", "name": "Ten Forward", "members": ["morpheus"],
|
|
94
|
+
"scene": "Late evening, a few rounds in."}
|
|
95
|
+
d = rp.group_detail(g, self.col, self.idx)
|
|
96
|
+
self.assertEqual(d["scene"], "Late evening, a few rounds in.")
|
|
97
|
+
self.assertEqual([m["code"] for m in d["members"]], ["morpheus"])
|
|
98
|
+
|
|
99
|
+
def test_scene_omitted_when_absent_or_empty(self):
|
|
100
|
+
for g in ({"id": "g", "members": ["morpheus"]},
|
|
101
|
+
{"id": "g", "members": ["morpheus"], "scene": ""}):
|
|
102
|
+
self.assertNotIn("scene", rp.group_detail(g, self.col, self.idx))
|
|
103
|
+
|
|
104
|
+
def test_anchored_group_is_not_open_cast(self):
|
|
105
|
+
g = {"id": "g", "members": ["morpheus"]}
|
|
106
|
+
self.assertNotIn("open_cast", rp.group_detail(g, self.col, self.idx))
|
|
107
|
+
|
|
108
|
+
def test_open_cast_group_flagged_with_empty_members(self):
|
|
109
|
+
g = {"id": "rebels", "name": "Star Wars Rebels",
|
|
110
|
+
"scene": "Figures from the Rebels universe drop in as the topic calls for them."}
|
|
111
|
+
d = rp.group_detail(g, self.col, self.idx)
|
|
112
|
+
self.assertTrue(d["open_cast"])
|
|
113
|
+
self.assertEqual(d["members"], [])
|
|
114
|
+
self.assertEqual(d["scene"][:7], "Figures")
|
|
115
|
+
|
|
116
|
+
def test_memory_enabled_follows_group_flag_and_defaults_off(self):
|
|
117
|
+
on = rp.group_detail({"id": "g", "members": ["morpheus"], "memory": True}, self.col, self.idx)
|
|
118
|
+
self.assertTrue(on["memory_enabled"])
|
|
119
|
+
off = rp.group_detail({"id": "g", "members": ["morpheus"], "memory": False}, self.col, self.idx)
|
|
120
|
+
self.assertFalse(off["memory_enabled"])
|
|
121
|
+
absent = rp.group_detail({"id": "g", "members": ["morpheus"]}, self.col, self.idx)
|
|
122
|
+
self.assertFalse(absent["memory_enabled"]) # opt-in per named group
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class TestInstalledCodesIsDefaultRoom(unittest.TestCase):
|
|
126
|
+
"""The default room is installed agents only; pure customs stay in the pool."""
|
|
127
|
+
|
|
128
|
+
def test_pure_custom_excluded_override_kept_in_default_room(self):
|
|
129
|
+
col, _, installed = rp.build_collective(AGENTS, [
|
|
130
|
+
{"code": "morpheus", "name": "Morpheus"}, # pure custom
|
|
131
|
+
{"code": "analyst", "name": "Mary-Custom", "persona": "p"}, # override
|
|
132
|
+
{"code": "sec-hawk", "name": "Vex"}, # shipped crew member
|
|
133
|
+
])
|
|
134
|
+
# Pure customs are in the pool...
|
|
135
|
+
self.assertIn("morpheus", col)
|
|
136
|
+
self.assertIn("sec-hawk", col)
|
|
137
|
+
# ...but NOT in the default room.
|
|
138
|
+
self.assertEqual(installed, ["bmad-agent-analyst", "bmad-agent-pm"])
|
|
139
|
+
default_room = [col[c]["code"] for c in installed]
|
|
140
|
+
self.assertEqual(default_room, ["bmad-agent-analyst", "bmad-agent-pm"])
|
|
141
|
+
# An override keeps its installed slot (and its custom content).
|
|
142
|
+
self.assertEqual(col["bmad-agent-analyst"]["name"], "Mary-Custom")
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
if __name__ == "__main__":
|
|
146
|
+
unittest.main()
|
|
@@ -18,7 +18,7 @@ Multiple skills may call to update the same spec over time.
|
|
|
18
18
|
|
|
19
19
|
## On Activation
|
|
20
20
|
|
|
21
|
-
1. Resolve customization: `
|
|
21
|
+
1. Resolve customization: `uv run {project-root}/_bmad/scripts/resolve_customization.py --skill {skill-root} --key workflow`. On failure, read `{skill-root}/customize.toml` directly.
|
|
22
22
|
2. Run `{workflow.activation_steps_prepend}`. Treat `{workflow.persistent_facts}` as foundational context (`file:` entries are loaded).
|
|
23
23
|
3. Load `{project-root}/_bmad/core/config.yaml` (and `config.user.yaml` if present), root level and `bmm` section. Resolve `{user_name}`, `{communication_language}`, `{document_output_language}`, `{planning_artifacts}`, `{project_name}`, `{date}`.
|
|
24
24
|
4. Detect mode. **Headless** when any of: no TTY, programmatic caller (another skill or non-interactive runner), or the first message pre-supplies all inputs and asks for an artifact path back. **Interactive** otherwise. In interactive mode, greet by `{user_name}` in `{communication_language}`, stay in that language, and mention that `bmad-party-mode` and `bmad-advanced-elicitation` are available for deeper exploration on any field.
|
|
@@ -43,22 +43,38 @@ Inside the spec folder:
|
|
|
43
43
|
|
|
44
44
|
```
|
|
45
45
|
<spec-folder>/
|
|
46
|
-
SPEC.md ← uppercase, the kernel
|
|
47
|
-
<companion-1>.md ← optional, content-typed (e.g. glossary.md)
|
|
46
|
+
SPEC.md ← uppercase, the kernel — DERIVED from .memlog.md, never hand-edited
|
|
47
|
+
<companion-1>.md ← optional, content-typed (e.g. glossary.md); spec-authored ones are derived too
|
|
48
48
|
<companion-2>.md
|
|
49
|
-
.
|
|
49
|
+
.memlog.md ← canonical, append-only memory; what SPEC.md is distilled from
|
|
50
50
|
```
|
|
51
51
|
|
|
52
|
+
## Memory and derivation
|
|
53
|
+
|
|
54
|
+
`.memlog.md` is canonical — an append-only, chronological record of every decision, constraint, capability (with its stable `CAP-N`), assumption, open question, and bit of user direction, one line each in the order it happened, never edited or reordered. `SPEC.md` and every spec-authored companion are **derived on each run** from the memlog (the decision-of-record) plus the sources it cites for raw content — never hand-patched.
|
|
55
|
+
|
|
56
|
+
Deriving the contract from a living log instead of editing the contract in place is what lets the steps around the spec (PRD, UX, architecture, epics) run in any order and feed the same spec without merge drift: the log only accumulates, the artifact is re-rendered. So the spec is updated *only* by re-deriving it here — bmad-spec is its single writer; a hand-edit to `SPEC.md` from outside is unsupported and is overwritten on the next derive.
|
|
57
|
+
|
|
58
|
+
Writes go through the shared script — `{project-root}/_bmad/scripts/memlog.py`, the same location as `resolve_customization.py` (atomic; never read it back except to resume):
|
|
59
|
+
|
|
60
|
+
- `uv run {project-root}/_bmad/scripts/memlog.py init --workspace {spec-folder} --field topic="<what is being specced>"` — once, at create.
|
|
61
|
+
- `uv run {project-root}/_bmad/scripts/memlog.py append --workspace {spec-folder} --type <decision|constraint|capability|assumption|question|direction|note|event> --text "<one-line gist, reason included>"` — as each lands.
|
|
62
|
+
- Terminal moments (a validation verdict, "spec finalized") are `--type event` entries; the memlog carries no status field.
|
|
63
|
+
|
|
52
64
|
## The Operation
|
|
53
65
|
|
|
54
|
-
Read the input and its ancillary linked materials. If there is no input, follow the no-input branch in **Workspace** (ask or block). If a prior
|
|
66
|
+
Read the input and its ancillary linked materials. If there is no input, follow the no-input branch in **Workspace** (ask or block). If a prior `.memlog.md` exists at the target folder, read it — the operation becomes an update, and the memlog (not the rendered `SPEC.md`) is the authority on what was decided and on capability IDs. Preserve those IDs; new capabilities get the next unused `CAP-N`; never reuse retired IDs. Otherwise this is a create, and the first move is `memlog.py init`.
|
|
55
67
|
|
|
56
68
|
When the input is structured and pre-sorted (a PRD with an addendum, a GDD, a brief produced by an upstream BMad skill), trust the authored separation: lift kernel-fitting content into SPEC.md, lift overflow into appropriately-named companions. When the input is mixed (a brain dump, a transcript, an RFC, a customer email), do the sorting yourself: walk each claim, apply the three-lens load-bearing test (Spec Law rule 7), and route to the kernel field or a companion.
|
|
57
69
|
|
|
58
70
|
Distill the input into the five-field kernel using `{workflow.spec_template}` as the skeleton. When input is rich, extract directly — no elicitation. When input is sparse, choose: **express** (best-effort distill, every gap becomes an `open_questions[]` entry) or **guided** (walk the five fields with the user one at a time). Headless defaults to express and logs the choice. Interactive asks.
|
|
59
71
|
|
|
72
|
+
A recognized domain implication the input leaves unaddressed *is* such a gap — name it as an `open_questions[]` entry (healthcare input silent on PHI/HIPAA, payments silent on PCI, control systems silent on fail-safe) and move on. Flag it; never invent the answer or coach toward it. If these dominate, the input is too thin — suggest `bmad-prd`.
|
|
73
|
+
|
|
60
74
|
Write lean from the first pass: every sentence must earn its place. Decoration costs tokens and dilutes downstream readers.
|
|
61
75
|
|
|
76
|
+
Log each decision, capability, constraint, and accepted change to `.memlog.md` as it is made — that running record is what the render reads. Because the log is append-only, a later entry supersedes an earlier one on the same point while the history stays intact. When two currently-live sources or companions disagree on the same field, or an either/or never got resolved, surface it to the user rather than silently choosing — the resolution is itself a new memlog entry.
|
|
77
|
+
|
|
62
78
|
If the input is genuinely too thin to distill (e.g. "an app for hikers" with no surrounding context), stop and suggest `bmad-prd` (or sibling ceremony skill). This skill distills; it does not coach.
|
|
63
79
|
|
|
64
80
|
## Load-bearing
|
|
@@ -94,7 +110,7 @@ Every spec must satisfy these eight rules. The operation aims for them; the self
|
|
|
94
110
|
5. **Success signal is concrete enough to test or demonstrate against.** "Users love it" doesn't qualify.
|
|
95
111
|
6. **Capability IDs are stable and unique.** Never reused, never renumbered.
|
|
96
112
|
7. **Preservation.** Every load-bearing source claim lands in SPEC.md or a companion. Wrapper ceremony does not.
|
|
97
|
-
8. **Lean prose.** Every sentence carries load-bearing content. Cut decoration, hedges, backstory, throat-clearing. Applies to SPEC.md, companions, and `.
|
|
113
|
+
8. **Lean prose.** Every sentence carries load-bearing content. Cut decoration, hedges, backstory, throat-clearing. Applies to SPEC.md, companions, and `.memlog.md`.
|
|
98
114
|
|
|
99
115
|
## Self-Validate
|
|
100
116
|
|
|
@@ -104,7 +120,7 @@ After every create or update, sweep the resulting artifact in **two passes** bef
|
|
|
104
120
|
|
|
105
121
|
**Pass 2 — Preservation.** Walk the source claim by claim. Confirm each load-bearing claim landed in SPEC.md or a companion. Wrapper-ceremony drops are logged under "Wrapper-only content" so the drop is on the record, not silent.
|
|
106
122
|
|
|
107
|
-
|
|
123
|
+
Record the verdict for each pass to `.memlog.md` (`append --type event`). In interactive mode, review it with the user. In headless mode, `.memlog.md` is one of the files returned, so the caller (or its downstream LLM) reads the verdict there.
|
|
108
124
|
|
|
109
125
|
## Spec with no change signal
|
|
110
126
|
|
|
@@ -120,10 +136,10 @@ Run `{workflow.on_complete}` if set.
|
|
|
120
136
|
|
|
121
137
|
## After Spec is Output
|
|
122
138
|
|
|
123
|
-
Any update to spec
|
|
139
|
+
Any update to the spec — resolved assumptions, answered open questions, other changes — is appended to `.memlog.md` as it happens. When a change overrides something that came from a source input, offer to update that source too, so upstream and the spec don't silently diverge.
|
|
124
140
|
|
|
125
141
|
## Frontmatter conventions
|
|
126
142
|
|
|
127
143
|
- `companions:` array of `.md` files downstream MUST read alongside SPEC.md to have the full contract. Paths may point inside the spec folder (spec-authored companions like `glossary.md`) or outside it (adopted companions like `../planning-artifacts/ux-designs/ux-foo-bar-2026-05-23/DESIGN.md`). The split between spec-authored and adopted is implicit by path; downstream treats both the same.
|
|
128
144
|
- `sources:` array of paths to files that were **fully absorbed** into the SPEC, with no remaining downstream value (e.g., a PRD whose every load-bearing claim is now in the kernel). Listed for audit and for bmad-spec to re-read on update. Downstream does NOT read these. Files that downstream still needs to read belong in `companions:`, not here.
|
|
129
|
-
- **Do not list**
|
|
145
|
+
- **Do not list** the memlog, README files, organizational artifacts, or any operational record of how upstream skills produced their artifacts. Those are not source content; they are process metadata that downstream consumers don't need.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Headless JSON Response
|
|
2
2
|
|
|
3
|
-
The default invocation is headless: input goes in, JSON comes out. The contract is intentionally tiny — return the outcome and the files touched. Anything else a caller needs is inside those files (SPEC.md, companions, `.
|
|
3
|
+
The default invocation is headless: input goes in, JSON comes out. The contract is intentionally tiny — return the outcome and the files touched. Anything else a caller needs is inside those files (SPEC.md, companions, `.memlog.md`).
|
|
4
4
|
|
|
5
5
|
## Success
|
|
6
6
|
|
|
@@ -10,12 +10,12 @@ The default invocation is headless: input goes in, JSON comes out. The contract
|
|
|
10
10
|
"files": [
|
|
11
11
|
"_bmad-output/specs/spec-quarter-drop/SPEC.md",
|
|
12
12
|
"_bmad-output/specs/spec-quarter-drop/glossary.md",
|
|
13
|
-
"_bmad-output/specs/spec-quarter-drop/.
|
|
13
|
+
"_bmad-output/specs/spec-quarter-drop/.memlog.md"
|
|
14
14
|
]
|
|
15
15
|
}
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
-
`files` lists every file written or modified in this run, in any order. The spec folder, kernel filename,
|
|
18
|
+
`files` lists every file written or modified in this run, in any order. The spec folder, kernel filename, memlog location, capabilities, companions, and verdict are all readable from those files; no need to re-encode them in the response.
|
|
19
19
|
|
|
20
20
|
## Blocked
|
|
21
21
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
id: SPEC-{slug}
|
|
3
3
|
companions: [] # files downstream MUST read alongside SPEC.md. Paths may point inside the spec folder (spec-authored) or outside it (adopted from an upstream skill).
|
|
4
|
-
sources: [] # files fully absorbed into the SPEC (audit only; downstream does NOT read these). Never
|
|
4
|
+
sources: [] # files fully absorbed into the SPEC (audit only; downstream does NOT read these). Never the memlog.
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
> **Canonical contract.** This SPEC and the files in `companions:` are the complete, preservation-validated contract for what to build, test, and validate. Source documents listed in frontmatter are for traceability only — consult them only if you need narrative rationale or prose color this contract intentionally omits.
|
|
@@ -20,9 +20,9 @@ Name which (or which combination) applies, who is affected, and the backdrop tha
|
|
|
20
20
|
|
|
21
21
|
## Capabilities
|
|
22
22
|
|
|
23
|
-
-
|
|
24
|
-
intent
|
|
25
|
-
success
|
|
23
|
+
- **CAP-1**
|
|
24
|
+
- **intent:** {One sentence. "User or system can do X to achieve Y." WHAT, not HOW.}
|
|
25
|
+
- **success:** {Testable or demonstrable criterion. Something a test or a real demonstration can decide.}
|
|
26
26
|
|
|
27
27
|
## Constraints
|
|
28
28
|
|
|
@@ -11,3 +11,4 @@ Core,bmad-review-adversarial-general,Adversarial Review,AR,"Use for quality assu
|
|
|
11
11
|
Core,bmad-review-edge-case-hunter,Edge Case Hunter Review,ECH,Use alongside adversarial review for orthogonal coverage — method-driven not attitude-driven.,,[path],anytime,,,false,,
|
|
12
12
|
Core,bmad-spec,Spec,SP,"Use to distill any intent input (brief, PRD, transcript, brain dump, design folder, mixed multi-source) into a succinct, no-fluff SPEC.md contract + companions that downstream work derives from. Locks the WHAT before the HOW. Works for software, game design, research, editorial, policy, business, anything intent-bearing. Validation mode also available.",,[path],anytime,,,false,{output_folder}/specs/spec-{slug},SPEC.md + companion files
|
|
13
13
|
Core,bmad-customize,BMad Customize,BC,"Use when you want to change how an agent or workflow behaves — add persistent facts, swap templates, insert activation hooks, or customize menus. Scans what's customizable, picks the right scope (agent vs workflow), writes the override to _bmad/custom/, and verifies the merge. No TOML hand-authoring required.",,,anytime,,,false,{project-root}/_bmad/custom,TOML override files
|
|
14
|
+
Core,bmad-forge-idea,Forge Idea,FI,"Use to pressure-test and harden an idea — software, business, creative, research, or life — until it proves out, hardens into something buildable, or dies cheaply. Persona-driven interrogation; optional handoff to bmad-spec or bmad-quick-dev.",,,anytime,,,false,{output_folder}/forge,refined-idea brief (optional)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
# /// script
|
|
3
|
-
# requires-python = ">=3.
|
|
3
|
+
# requires-python = ">=3.8"
|
|
4
4
|
# ///
|
|
5
5
|
"""memlog — an append-only memory log: LLM-optimal working memory for a skill.
|
|
6
6
|
|
|
@@ -15,21 +15,31 @@ It is a FLAT log: there are no sections or grouping. Every entry is one line, re
|
|
|
15
15
|
at the END in the order it happened. The chronology itself is the structure — an event
|
|
16
16
|
like "started technique X" is just another entry, same as an idea or an insight.
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
Three invariants make it trustworthy:
|
|
19
19
|
|
|
20
20
|
1. Append-only, chronological. Entries land at the end, in the order they happen.
|
|
21
|
-
Nothing is ever inserted backward, reordered, or
|
|
21
|
+
Nothing is ever inserted backward, reordered, edited, or removed. There is no
|
|
22
|
+
edit or delete subcommand by design; history is never rewritten.
|
|
22
23
|
2. Write-only / blind. Every command is an atomic, context-free write and echoes the
|
|
23
|
-
new state as JSON, so the caller never re-reads the file mid-session.
|
|
24
|
-
the file is read is on resume — and the caller reads it itself, not
|
|
24
|
+
new state as one line of JSON, so the caller never re-reads the file mid-session.
|
|
25
|
+
The one time the file is read is on resume — and the caller reads it itself, not
|
|
26
|
+
via this script.
|
|
27
|
+
3. No lifecycle status. A memory log has no "complete" flag. Whether the work is done,
|
|
28
|
+
blocked, or paused is itself a fact that happened, so it is recorded as an entry
|
|
29
|
+
(e.g. `append --type event --text "session complete"`), never as frontmatter the
|
|
30
|
+
log would have to mutate. The chronology stays the single source of truth, and a
|
|
31
|
+
resume learns the state by reading the last entries — the same way it learns
|
|
32
|
+
everything else.
|
|
33
|
+
|
|
34
|
+
Atomicity: every write goes to a temp file, is flushed and fsync'd, then atomically
|
|
35
|
+
renamed over the target, so a crash never leaves a half-written entry.
|
|
25
36
|
|
|
26
37
|
The file shape (.memlog.md):
|
|
27
38
|
|
|
28
39
|
---
|
|
29
40
|
topic: Onboarding flow for a budgeting app
|
|
30
41
|
goal: lift week-1 retention
|
|
31
|
-
|
|
32
|
-
updated: 2026-05-30T14:22
|
|
42
|
+
updated: 2026-06-07T14:22
|
|
33
43
|
---
|
|
34
44
|
|
|
35
45
|
- (note) user picked techniques: SCAMPER, then Six Thinking Hats
|
|
@@ -37,25 +47,27 @@ The file shape (.memlog.md):
|
|
|
37
47
|
- (idea) skip the signup wall: let people try with sample data first
|
|
38
48
|
- (idea) auto-import one bank account so the first screen shows real numbers
|
|
39
49
|
- (question) is open-banking consent too heavy for step one?
|
|
40
|
-
- (technique) started Six Thinking Hats
|
|
41
|
-
- (idea) black-hat: imported transactions look scary before they're categorized
|
|
42
50
|
- (insight) the "scary numbers" risk and the "real numbers" idea are one lever: show real data, pre-categorized
|
|
43
|
-
- (direction)
|
|
51
|
+
- (direction) optimize for the anxious first-timer, not the power user
|
|
44
52
|
- (decision) lead with one pre-categorized account; defer multi-account import
|
|
53
|
+
- (event) session complete
|
|
45
54
|
|
|
46
55
|
Each entry may carry an optional `--type` — what KIND it is (idea, insight, question,
|
|
47
|
-
decision,
|
|
48
|
-
`coach`), for sessions where authorship matters. Both
|
|
49
|
-
`(idea)`, `(idea by user)`, `(by coach)`. Omit them
|
|
50
|
-
names the vocabulary; the script does not.
|
|
56
|
+
decision, direction, assumption, gap, note, event, …) — and an optional `--by` naming
|
|
57
|
+
who it came from (e.g. `user`, `coach`), for sessions where authorship matters. Both
|
|
58
|
+
render into one short inline tag: `(idea)`, `(idea by user)`, `(by coach)`. Omit them
|
|
59
|
+
for a plain note. The host skill names the vocabulary; the script does not enforce one.
|
|
51
60
|
|
|
52
61
|
Commands:
|
|
53
|
-
init --workspace DIR [--field k=v ...]
|
|
54
|
-
append --workspace DIR --text STR [--type T] [--by W] append one entry at the end
|
|
55
|
-
set --workspace DIR --key K --value V
|
|
62
|
+
init (--workspace DIR | --path FILE) [--field k=v ...] create the memlog (errors if it exists)
|
|
63
|
+
append (--workspace DIR | --path FILE) --text STR [--type T] [--by W] append one entry at the end
|
|
64
|
+
set (--workspace DIR | --path FILE) --key K --value V set/replace a descriptive frontmatter field
|
|
56
65
|
|
|
57
|
-
|
|
66
|
+
Addressing: `--workspace` is the run folder, and the memlog is always {workspace}/.memlog.md.
|
|
67
|
+
`--path` points straight at the memlog file instead, for callers that already hold the path.
|
|
58
68
|
"""
|
|
69
|
+
from __future__ import annotations # keep type-hint syntax lazy so the script runs on 3.8+
|
|
70
|
+
|
|
59
71
|
import argparse
|
|
60
72
|
import json
|
|
61
73
|
import os
|
|
@@ -70,8 +82,9 @@ def now() -> str:
|
|
|
70
82
|
return datetime.now().strftime("%Y-%m-%dT%H:%M")
|
|
71
83
|
|
|
72
84
|
|
|
73
|
-
def
|
|
74
|
-
|
|
85
|
+
def resolve(args) -> Path:
|
|
86
|
+
"""The memlog file, from either addressing mode: {workspace}/.memlog.md or an explicit --path."""
|
|
87
|
+
return Path(args.path) if args.path else Path(args.workspace) / MEMLOG
|
|
75
88
|
|
|
76
89
|
|
|
77
90
|
def split(text: str) -> tuple[dict, str]:
|
|
@@ -107,8 +120,12 @@ def touch(meta: dict) -> None:
|
|
|
107
120
|
|
|
108
121
|
|
|
109
122
|
def write_atomic(path: Path, text: str) -> None:
|
|
123
|
+
"""Temp + flush + fsync + atomic rename, so a crash never half-writes an entry."""
|
|
110
124
|
tmp = path.with_suffix(path.suffix + ".tmp")
|
|
111
|
-
tmp
|
|
125
|
+
with open(tmp, "w", encoding="utf-8") as f:
|
|
126
|
+
f.write(text)
|
|
127
|
+
f.flush()
|
|
128
|
+
os.fsync(f.fileno())
|
|
112
129
|
os.replace(tmp, path)
|
|
113
130
|
|
|
114
131
|
|
|
@@ -116,18 +133,17 @@ def entry_count(body: str) -> int:
|
|
|
116
133
|
return sum(1 for ln in body.splitlines() if ln.startswith("- "))
|
|
117
134
|
|
|
118
135
|
|
|
119
|
-
def ack(path: Path,
|
|
136
|
+
def ack(path: Path, body: str) -> None:
|
|
120
137
|
"""Echo new state so the caller never re-reads the file to know where it stands."""
|
|
121
138
|
print(json.dumps({
|
|
122
139
|
"ok": True,
|
|
123
140
|
"memlog": str(path),
|
|
124
|
-
"status": meta.get("status", ""),
|
|
125
141
|
"entries": entry_count(body),
|
|
126
142
|
}))
|
|
127
143
|
|
|
128
144
|
|
|
129
145
|
def cmd_init(args) -> int:
|
|
130
|
-
path =
|
|
146
|
+
path = resolve(args)
|
|
131
147
|
if path.exists():
|
|
132
148
|
print(f"error: {path} already exists; use append/set to update it", file=sys.stderr)
|
|
133
149
|
return 2
|
|
@@ -139,15 +155,14 @@ def cmd_init(args) -> int:
|
|
|
139
155
|
return 2
|
|
140
156
|
k, v = pair.split("=", 1)
|
|
141
157
|
meta[k.strip()] = v.strip()
|
|
142
|
-
meta.setdefault("status", "active")
|
|
143
158
|
touch(meta)
|
|
144
159
|
write_atomic(path, render(meta, ""))
|
|
145
|
-
ack(path,
|
|
160
|
+
ack(path, "")
|
|
146
161
|
return 0
|
|
147
162
|
|
|
148
163
|
|
|
149
164
|
def cmd_append(args) -> int:
|
|
150
|
-
path =
|
|
165
|
+
path = resolve(args)
|
|
151
166
|
meta, body = split(path.read_text(encoding="utf-8"))
|
|
152
167
|
text = " ".join(args.text.split()) # collapse newlines/runs → one-line entry, no prose bloat
|
|
153
168
|
label = args.type or ""
|
|
@@ -158,38 +173,45 @@ def cmd_append(args) -> int:
|
|
|
158
173
|
body = (body.rstrip("\n") + "\n" + entry) if body.strip() else entry # always at the end
|
|
159
174
|
touch(meta)
|
|
160
175
|
write_atomic(path, render(meta, body))
|
|
161
|
-
ack(path,
|
|
176
|
+
ack(path, body)
|
|
162
177
|
return 0
|
|
163
178
|
|
|
164
179
|
|
|
165
180
|
def cmd_set(args) -> int:
|
|
166
|
-
path =
|
|
181
|
+
path = resolve(args)
|
|
167
182
|
meta, body = split(path.read_text(encoding="utf-8"))
|
|
168
183
|
meta[args.key] = args.value
|
|
169
184
|
touch(meta)
|
|
170
185
|
write_atomic(path, render(meta, body))
|
|
171
|
-
ack(path,
|
|
186
|
+
ack(path, body)
|
|
172
187
|
return 0
|
|
173
188
|
|
|
174
189
|
|
|
190
|
+
def add_target(sp) -> None:
|
|
191
|
+
"""Every command addresses the memlog the same way: a run folder or an explicit path."""
|
|
192
|
+
g = sp.add_mutually_exclusive_group(required=True)
|
|
193
|
+
g.add_argument("--workspace", help="run folder; the memlog is {workspace}/.memlog.md")
|
|
194
|
+
g.add_argument("--path", help="explicit memlog file path (alternative to --workspace)")
|
|
195
|
+
|
|
196
|
+
|
|
175
197
|
def main(argv: list[str] | None = None) -> int:
|
|
176
198
|
p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
177
199
|
sub = p.add_subparsers(dest="cmd", required=True)
|
|
178
200
|
|
|
179
201
|
pi = sub.add_parser("init", help="create the memlog")
|
|
180
|
-
pi
|
|
202
|
+
add_target(pi)
|
|
181
203
|
pi.add_argument("--field", action="append", metavar="KEY=VALUE", help="frontmatter field (repeatable)")
|
|
182
204
|
pi.set_defaults(func=cmd_init)
|
|
183
205
|
|
|
184
206
|
pa = sub.add_parser("append", help="append one entry at the end")
|
|
185
|
-
pa
|
|
207
|
+
add_target(pa)
|
|
186
208
|
pa.add_argument("--text", required=True)
|
|
187
209
|
pa.add_argument("--type", help="entry kind, rendered as an inline tag")
|
|
188
210
|
pa.add_argument("--by", help="who the entry came from (e.g. user, coach); rendered into the tag")
|
|
189
211
|
pa.set_defaults(func=cmd_append)
|
|
190
212
|
|
|
191
|
-
pset = sub.add_parser("set", help="set a frontmatter field")
|
|
192
|
-
pset
|
|
213
|
+
pset = sub.add_parser("set", help="set a descriptive frontmatter field")
|
|
214
|
+
add_target(pset)
|
|
193
215
|
pset.add_argument("--key", required=True)
|
|
194
216
|
pset.add_argument("--value", required=True)
|
|
195
217
|
pset.set_defaults(func=cmd_set)
|
|
@@ -10,12 +10,14 @@ Reads from four layers (highest priority last):
|
|
|
10
10
|
|
|
11
11
|
Outputs merged JSON to stdout. Errors go to stderr.
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
13
|
+
Uses only the Python stdlib (`tomllib`) — no third-party dependencies.
|
|
14
|
+
BMad is standardizing on `uv run` to invoke scripts (uv provisions a suitable
|
|
15
|
+
interpreter for you); a plain `python3` on PATH still works during the
|
|
16
|
+
transition. Either runner needs Python 3.11+ for `tomllib`.
|
|
17
|
+
|
|
18
|
+
uv run resolve_config.py --project-root /abs/path/to/project
|
|
19
|
+
uv run resolve_config.py --project-root ... --key core
|
|
20
|
+
uv run resolve_config.py --project-root ... --key agents
|
|
19
21
|
|
|
20
22
|
Merge rules (same as resolve_customization.py):
|
|
21
23
|
- Scalars: override wins
|
|
@@ -11,12 +11,14 @@ Skill name is derived from the basename of the skill directory.
|
|
|
11
11
|
|
|
12
12
|
Outputs merged JSON to stdout. Errors go to stderr.
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
14
|
+
Uses only the Python stdlib (`tomllib`) — no third-party dependencies.
|
|
15
|
+
BMad is standardizing on `uv run` to invoke scripts (uv provisions a suitable
|
|
16
|
+
interpreter for you); a plain `python3` on PATH still works during the
|
|
17
|
+
transition. Either runner needs Python 3.11+ for `tomllib`.
|
|
18
|
+
|
|
19
|
+
uv run resolve_customization.py --skill /abs/path/to/skill-dir
|
|
20
|
+
uv run resolve_customization.py --skill ... --key agent
|
|
21
|
+
uv run resolve_customization.py --skill ... --key agent.menu
|
|
20
22
|
|
|
21
23
|
Merge rules (purely structural — no field-name special-casing):
|
|
22
24
|
- Scalars (string, int, bool, float): override wins
|