bmad-method 6.10.1-next.13 → 6.10.1-next.15
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/package.json +2 -2
- package/src/bmm-skills/4-implementation/bmad-dev-auto/SKILL.md +6 -116
- package/src/bmm-skills/4-implementation/bmad-dev-auto/render.py +381 -0
- package/src/bmm-skills/4-implementation/bmad-dev-auto/step-01-clarify-and-route.md +7 -8
- package/src/bmm-skills/4-implementation/bmad-dev-auto/step-02-plan.md +3 -7
- package/src/bmm-skills/4-implementation/bmad-dev-auto/step-03-implement.md +5 -3
- package/src/bmm-skills/4-implementation/bmad-dev-auto/step-04-review.md +7 -13
- package/src/bmm-skills/4-implementation/bmad-dev-auto/workflow.md +104 -0
- package/src/bmm-skills/4-implementation/bmad-quick-dev/render.py +58 -24
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
3
|
"name": "bmad-method",
|
|
4
|
-
"version": "6.10.1-next.
|
|
4
|
+
"version": "6.10.1-next.15",
|
|
5
5
|
"description": "Breakthrough Method of Agile AI-driven Development",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"agile",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"test:channels": "node test/test-installer-channels.js",
|
|
47
47
|
"test:install": "node test/test-installation-components.js",
|
|
48
48
|
"test:refs": "node test/test-file-refs-csv.js",
|
|
49
|
-
"test:renderer": "node test/test-quick-dev-renderer.js",
|
|
49
|
+
"test:renderer": "node test/test-quick-dev-renderer.js && node test/test-dev-auto-renderer.js",
|
|
50
50
|
"test:skills": "node test/test-validate-skills.js",
|
|
51
51
|
"test:urls": "node test/test-parse-source-urls.js",
|
|
52
52
|
"validate:refs": "node tools/validate-file-refs.js --strict",
|
|
@@ -3,121 +3,11 @@ name: bmad-dev-auto
|
|
|
3
3
|
description: 'One iteration of an unattended development loop. Use when invoked by name.'
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
Run this, substituting `{skill-root}` with the absolute path to this skill's base directory, without changing the cwd:
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
```bash
|
|
9
|
+
uv run {skill-root}/render.py
|
|
10
|
+
```
|
|
9
11
|
|
|
10
|
-
**
|
|
11
|
-
|
|
12
|
-
## HALT
|
|
13
|
-
|
|
14
|
-
To HALT with a final status and optional blocking condition:
|
|
15
|
-
|
|
16
|
-
1. **Folder+id dispatch** (`{spec_folder}` and `{story_id}` are set): the write-back always lands at the id-keyed story spec. The `{implementation_artifacts}` fallback in step 2 below is never used in this mode, even for halts before planning starts.
|
|
17
|
-
- If `{spec_file}` is still empty, resolve it now:
|
|
18
|
-
- **Entry not resolved** (`stories.yaml` is missing/unparseable, or `{story_id}` has no matching entry): use the fixed slug segment `unresolved`: `{spec_file}` = `{spec_folder}/stories/{story_id}-unresolved.md`.
|
|
19
|
-
- **Ambiguous on-disk match** (the halt is `ambiguous story file match` — more than one file already matches `{spec_folder}/stories/{story_id}-*.md`): use the fixed slug segment `ambiguous` instead of deriving from the title, so the write-back neither creates a third title-derived candidate nor risks silently landing on one of the existing ambiguous files: `{spec_file}` = `{spec_folder}/stories/{story_id}-ambiguous.md`.
|
|
20
|
-
- **Otherwise** (the entry was resolved and no ambiguous on-disk match exists): derive `{spec_file}` = `{spec_folder}/stories/{story_id}-{slug}.md`, where `{slug}` is a kebab-case slug from `title` (and `description` if needed) with no `{story_id}` prefix — the same derivation step-01's Route uses.
|
|
21
|
-
- If `{spec_file}` exists on disk, update `status` in frontmatter and append missing result details under `## Auto Run Result`.
|
|
22
|
-
- If it does not exist, create it as a skeletal story spec:
|
|
23
|
-
```markdown
|
|
24
|
-
---
|
|
25
|
-
status: <final status>
|
|
26
|
-
---
|
|
27
|
-
|
|
28
|
-
# <entry title, or "Story {story_id}" if the entry could not be resolved or the on-disk match was ambiguous>
|
|
29
|
-
|
|
30
|
-
## Auto Run Result
|
|
31
|
-
|
|
32
|
-
Status: <final status>
|
|
33
|
-
Blocking condition: <blocking condition, if any>
|
|
34
|
-
```
|
|
35
|
-
2. **Otherwise:**
|
|
36
|
-
- If `{spec_file}` is known and exists, update `status` in frontmatter and append missing result details under `## Auto Run Result`.
|
|
37
|
-
- If `{spec_file}` is unknown or missing, create `{implementation_artifacts}/bmad-dev-auto-result-<slug-or-timestamp>.md` with:
|
|
38
|
-
```markdown
|
|
39
|
-
---
|
|
40
|
-
status: <final status>
|
|
41
|
-
---
|
|
42
|
-
|
|
43
|
-
# BMad Dev Auto Result
|
|
44
|
-
|
|
45
|
-
Status: <final status>
|
|
46
|
-
Blocking condition: <blocking condition, if any>
|
|
47
|
-
```
|
|
48
|
-
3. Run: `python3 {project-root}/_bmad/scripts/resolve_customization.py --skill {skill-root} --key workflow.on_complete`
|
|
49
|
-
4. If the resolved `workflow.on_complete` is non-empty, follow it as the final instruction before exiting.
|
|
50
|
-
5. Stop the workflow.
|
|
51
|
-
|
|
52
|
-
## Subagents
|
|
53
|
-
|
|
54
|
-
Using subagents when instructed is mandatory. If you cannot, HALT with status `blocked` and blocking condition `no subagents`.
|
|
55
|
-
|
|
56
|
-
Invoke every subagent **synchronously**: launch it, wait for it to return within the same turn, then continue with its result. When a step says to run subagents "in parallel" (e.g. the reviewers), that means several **blocking** calls awaited together in one turn — not detached execution. Never run a subagent in the background / detached / async (e.g. `run_in_background: true`), and never end your turn to "await a completion notification." This workflow runs unattended: there is no event loop to resume a yielded turn, so a backgrounded subagent never hands control back and the run stalls. The only sanctioned way to end a turn is the HALT protocol above with an explicit terminal `status`.
|
|
57
|
-
|
|
58
|
-
## READY FOR DEVELOPMENT STANDARD
|
|
59
|
-
|
|
60
|
-
A specification is "Ready for Development" when:
|
|
61
|
-
|
|
62
|
-
- **Actionable**: Every task has a file path and specific action.
|
|
63
|
-
- **Logical**: Tasks ordered by dependency.
|
|
64
|
-
- **Testable**: All ACs use Given/When/Then.
|
|
65
|
-
- **Surface-anchored**: ACs observe the outermost surface the intent references — never a more internal proxy for it (e.g. the API response, not the database row behind it).
|
|
66
|
-
- **Complete**: No placeholders or TBDs.
|
|
67
|
-
- **Sufficient**: No known requirement, acceptance, dependency, or implementation gaps remain unresolved.
|
|
68
|
-
- **Coherent**: No unresolved ambiguities or internal contradictions.
|
|
69
|
-
|
|
70
|
-
## Conventions
|
|
71
|
-
|
|
72
|
-
- Bare paths (e.g. `step-01-clarify-and-route.md`) resolve from the skill root.
|
|
73
|
-
- `{skill-root}` resolves to this skill's installed directory (where `customize.toml` lives).
|
|
74
|
-
- `{project-root}`-prefixed paths resolve from the project working directory.
|
|
75
|
-
- `{skill-name}` resolves to the skill directory's basename.
|
|
76
|
-
|
|
77
|
-
## On Activation
|
|
78
|
-
|
|
79
|
-
### Step 1: Resolve the Workflow Block
|
|
80
|
-
|
|
81
|
-
Run: `python3 {project-root}/_bmad/scripts/resolve_customization.py --skill {skill-root} --key workflow`
|
|
82
|
-
|
|
83
|
-
**If the script fails**, resolve the `workflow` block yourself by reading these three files in base → team → user order and applying the same structural merge rules as the resolver:
|
|
84
|
-
|
|
85
|
-
1. `{skill-root}/customize.toml` — defaults
|
|
86
|
-
2. `{project-root}/_bmad/custom/{skill-name}.toml` — team overrides
|
|
87
|
-
3. `{project-root}/_bmad/custom/{skill-name}.user.toml` — personal overrides
|
|
88
|
-
|
|
89
|
-
Any missing file is skipped. Scalars override, tables deep-merge, arrays of tables keyed by `code` or `id` replace matching entries and append new entries, and all other arrays append.
|
|
90
|
-
|
|
91
|
-
### Step 2: Execute Prepend Steps
|
|
92
|
-
|
|
93
|
-
Execute each entry in `{workflow.activation_steps_prepend}` in order before proceeding.
|
|
94
|
-
|
|
95
|
-
### Step 3: Load Persistent Facts
|
|
96
|
-
|
|
97
|
-
Treat every entry in `{workflow.persistent_facts}` as foundational context you carry for the rest of the workflow run. Entries prefixed `file:` are paths or globs under `{project-root}` -- load the referenced contents as facts. All other entries are facts verbatim.
|
|
98
|
-
|
|
99
|
-
### Step 4: Load Config
|
|
100
|
-
|
|
101
|
-
Load config from `{project-root}/_bmad/bmm/config.yaml` and resolve:
|
|
102
|
-
|
|
103
|
-
- `project_name`, `planning_artifacts`, `implementation_artifacts`, `user_name`
|
|
104
|
-
- `communication_language`, `document_output_language`, `user_skill_level`
|
|
105
|
-
- `date` as system-generated current datetime
|
|
106
|
-
- `project_context` = `**/project-context.md` (load if exists)
|
|
107
|
-
- YOU MUST ALWAYS SPEAK OUTPUT in your Agent communication style with the config `{communication_language}`
|
|
108
|
-
- Language MUST be tailored to `{user_skill_level}`
|
|
109
|
-
- Generate all documents in `{document_output_language}`
|
|
110
|
-
|
|
111
|
-
### Step 5: Execute Append Steps
|
|
112
|
-
|
|
113
|
-
Execute each entry in `{workflow.activation_steps_append}` in order.
|
|
114
|
-
|
|
115
|
-
Activation is complete after all activation steps have run.
|
|
116
|
-
|
|
117
|
-
## Workflow Execution
|
|
118
|
-
|
|
119
|
-
Follow the step files in order. Read one step fully, execute it, then load the next step only when directed. Do not skip, reorder, or pre-load steps.
|
|
120
|
-
|
|
121
|
-
## First workflow step
|
|
122
|
-
|
|
123
|
-
Read fully and follow: `./step-01-clarify-and-route.md` to begin the workflow.
|
|
12
|
+
- **On success:** follow the instruction it prints to stdout; ignore stderr.
|
|
13
|
+
- **On any failure** (including `uv` not being installed): report what it printed and HALT.
|
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# /// script
|
|
3
|
+
# requires-python = ">=3.11"
|
|
4
|
+
# ///
|
|
5
|
+
"""render.py — bmad-dev-auto template renderer.
|
|
6
|
+
|
|
7
|
+
Resolves compile-time {{.variable}} placeholders from BMad's central config,
|
|
8
|
+
bakes absolute paths for {project-root} into derived values, resolves and
|
|
9
|
+
inlines the skill's [workflow] customization block, and writes rendered .md
|
|
10
|
+
files to {project-root}/_bmad/render/bmad-dev-auto/.
|
|
11
|
+
|
|
12
|
+
Config: four-layer merge of _bmad/config.toml + config.user.toml +
|
|
13
|
+
custom/config.toml + custom/config.user.toml (post-#2285 installs).
|
|
14
|
+
Keys surface from [core] and [modules.bmm]. Missing or unparseable
|
|
15
|
+
config.toml → HALT. A {{.var}} referenced by this skill's .md sources but
|
|
16
|
+
absent from the merged config → HALT (never a silent empty substitution).
|
|
17
|
+
Optional layers may be missing, but one that exists and cannot be parsed
|
|
18
|
+
or read → HALT.
|
|
19
|
+
|
|
20
|
+
Customization: three-layer merge of {skill}/customize.toml +
|
|
21
|
+
_bmad/custom/bmad-dev-auto.toml + .user.toml (same structural rules as
|
|
22
|
+
resolve_customization.py). The resolved [workflow] values fill {workflow.*}
|
|
23
|
+
placeholders, so this skill needs no runtime resolve_customization.py call.
|
|
24
|
+
Other single-curly placeholders ({project-root}, {spec_file}, {skill-root},
|
|
25
|
+
...) pass through untouched for the LLM to resolve during workflow execution.
|
|
26
|
+
|
|
27
|
+
Every invocation rebuilds from scratch — no hash, no cache.
|
|
28
|
+
Python 3.11+ stdlib only. UTF-8 I/O.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
import os
|
|
32
|
+
import posixpath
|
|
33
|
+
import re
|
|
34
|
+
import sys
|
|
35
|
+
import tomllib
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def find_project_root():
|
|
39
|
+
"""Walk up from cwd until a _bmad/ directory is found. On failure, print a
|
|
40
|
+
HALT instruction to stdout and exit non-zero."""
|
|
41
|
+
current = os.path.abspath(os.getcwd())
|
|
42
|
+
while True:
|
|
43
|
+
candidate = os.path.join(current, "_bmad")
|
|
44
|
+
if os.path.isdir(candidate):
|
|
45
|
+
return current
|
|
46
|
+
parent = os.path.dirname(current)
|
|
47
|
+
if parent == current:
|
|
48
|
+
print(
|
|
49
|
+
f"HALT and report to the user: no _bmad/ directory found walking up from {os.getcwd()}"
|
|
50
|
+
)
|
|
51
|
+
sys.exit(1)
|
|
52
|
+
current = parent
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def load_toml(path, required=False):
|
|
56
|
+
"""Load a TOML file. Only absence is negotiable: a missing optional file
|
|
57
|
+
returns {} (customization layers are optional), a missing required file
|
|
58
|
+
HALTs. A file that exists but cannot be parsed or read always HALTs —
|
|
59
|
+
stdout is how this script signals workflow halts to its LLM caller — the
|
|
60
|
+
user wrote it to be honored, and silently continuing with {} would discard
|
|
61
|
+
their customizations with no failure signal."""
|
|
62
|
+
if not os.path.isfile(path):
|
|
63
|
+
if required:
|
|
64
|
+
print(
|
|
65
|
+
f"HALT and report to the user: required config file not found: {path} — "
|
|
66
|
+
"ensure this is a post-#2285 BMAD install"
|
|
67
|
+
)
|
|
68
|
+
sys.exit(1)
|
|
69
|
+
return {}
|
|
70
|
+
try:
|
|
71
|
+
with open(path, "rb") as fh:
|
|
72
|
+
parsed = tomllib.load(fh)
|
|
73
|
+
except tomllib.TOMLDecodeError as error:
|
|
74
|
+
print(f"HALT and report to the user: failed to parse {path}: {error}")
|
|
75
|
+
sys.exit(1)
|
|
76
|
+
except OSError as error:
|
|
77
|
+
print(f"HALT and report to the user: failed to read {path}: {error}")
|
|
78
|
+
sys.exit(1)
|
|
79
|
+
if not isinstance(parsed, dict):
|
|
80
|
+
return {}
|
|
81
|
+
return parsed
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _deep_merge(base, override):
|
|
85
|
+
"""Dict-aware deep merge. Lists and scalars: override wins (we don't need
|
|
86
|
+
the full keyed-merge semantics of resolve_config.py — dev-auto only reads
|
|
87
|
+
flat scalars out of [core] and [modules.bmm])."""
|
|
88
|
+
if isinstance(base, dict) and isinstance(override, dict):
|
|
89
|
+
result = dict(base)
|
|
90
|
+
for key, value in override.items():
|
|
91
|
+
result[key] = _deep_merge(result[key], value) if key in result else value
|
|
92
|
+
return result
|
|
93
|
+
return override
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _detect_keyed_merge_field(items):
|
|
97
|
+
"""Return 'code' or 'id' if every table item carries that same field.
|
|
98
|
+
Mixed or partial arrays return None and fall through to append."""
|
|
99
|
+
if not items or not all(isinstance(item, dict) for item in items):
|
|
100
|
+
return None
|
|
101
|
+
for candidate in ("code", "id"):
|
|
102
|
+
if all(item.get(candidate) is not None for item in items):
|
|
103
|
+
return candidate
|
|
104
|
+
return None
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _merge_by_key(base, override, key_name):
|
|
108
|
+
result = []
|
|
109
|
+
index_by_key = {}
|
|
110
|
+
for item in base:
|
|
111
|
+
if not isinstance(item, dict):
|
|
112
|
+
continue
|
|
113
|
+
if item.get(key_name) is not None:
|
|
114
|
+
index_by_key[item[key_name]] = len(result)
|
|
115
|
+
result.append(dict(item))
|
|
116
|
+
for item in override:
|
|
117
|
+
if not isinstance(item, dict):
|
|
118
|
+
result.append(item)
|
|
119
|
+
continue
|
|
120
|
+
key = item.get(key_name)
|
|
121
|
+
if key is not None and key in index_by_key:
|
|
122
|
+
result[index_by_key[key]] = dict(item)
|
|
123
|
+
else:
|
|
124
|
+
if key is not None:
|
|
125
|
+
index_by_key[key] = len(result)
|
|
126
|
+
result.append(dict(item))
|
|
127
|
+
return result
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _merge_arrays(base, override):
|
|
131
|
+
"""Shape-aware array merge: keyed merge if every item has code/id, else append."""
|
|
132
|
+
base_arr = base if isinstance(base, list) else []
|
|
133
|
+
override_arr = override if isinstance(override, list) else []
|
|
134
|
+
keyed_field = _detect_keyed_merge_field(base_arr + override_arr)
|
|
135
|
+
if keyed_field:
|
|
136
|
+
return _merge_by_key(base_arr, override_arr, keyed_field)
|
|
137
|
+
return base_arr + override_arr
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _structural_merge(base, override):
|
|
141
|
+
"""Faithful port of resolve_customization.py's deep_merge: tables deep-merge,
|
|
142
|
+
arrays-of-tables keyed by code/id replace-then-append (other arrays append),
|
|
143
|
+
scalars override. Used only for the [workflow] customization layers — the
|
|
144
|
+
central-config path keeps its own simpler _deep_merge. Duplicated rather than
|
|
145
|
+
imported to keep this skill self-contained."""
|
|
146
|
+
if isinstance(base, dict) and isinstance(override, dict):
|
|
147
|
+
result = dict(base)
|
|
148
|
+
for key, over_val in override.items():
|
|
149
|
+
result[key] = (
|
|
150
|
+
_structural_merge(result[key], over_val) if key in result else over_val
|
|
151
|
+
)
|
|
152
|
+
return result
|
|
153
|
+
if isinstance(base, list) and isinstance(override, list):
|
|
154
|
+
return _merge_arrays(base, override)
|
|
155
|
+
return override
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def resolve_workflow(root, skill_dir, skill_name):
|
|
159
|
+
"""Resolve the [workflow] customization block via the three-layer merge
|
|
160
|
+
(skill defaults -> team -> user), highest priority last. Same structural
|
|
161
|
+
rules as resolve_customization.py. All three layers are optional: a missing
|
|
162
|
+
file is skipped, but an unparseable one HALTs (via load_toml)."""
|
|
163
|
+
defaults = load_toml(posixpath.join(skill_dir, "customize.toml"))
|
|
164
|
+
custom_dir = posixpath.join(root, "_bmad", "custom")
|
|
165
|
+
team = load_toml(posixpath.join(custom_dir, f"{skill_name}.toml"))
|
|
166
|
+
user = load_toml(posixpath.join(custom_dir, f"{skill_name}.user.toml"))
|
|
167
|
+
merged = _structural_merge(defaults, team)
|
|
168
|
+
merged = _structural_merge(merged, user)
|
|
169
|
+
workflow = merged.get("workflow")
|
|
170
|
+
return workflow if isinstance(workflow, dict) else {}
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def load_central_config(root):
|
|
174
|
+
"""Four-layer merge of _bmad/config.toml and its peers (highest priority
|
|
175
|
+
last). HALTs if the base _bmad/config.toml is missing or unparseable."""
|
|
176
|
+
bmad_dir = posixpath.join(root, "_bmad")
|
|
177
|
+
base_team = load_toml(posixpath.join(bmad_dir, "config.toml"), required=True)
|
|
178
|
+
base_user = load_toml(posixpath.join(bmad_dir, "config.user.toml"))
|
|
179
|
+
custom_team = load_toml(posixpath.join(bmad_dir, "custom", "config.toml"))
|
|
180
|
+
custom_user = load_toml(posixpath.join(bmad_dir, "custom", "config.user.toml"))
|
|
181
|
+
|
|
182
|
+
merged = _deep_merge(base_team, base_user)
|
|
183
|
+
merged = _deep_merge(merged, custom_team)
|
|
184
|
+
merged = _deep_merge(merged, custom_user)
|
|
185
|
+
return merged
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def flatten_central_config(merged):
|
|
189
|
+
"""Lift scalar keys from [core] and [modules.bmm] into a single namespace.
|
|
190
|
+
Module keys take precedence on collision (installer strips core keys from
|
|
191
|
+
module buckets, so collisions shouldn't happen in practice)."""
|
|
192
|
+
flat = {}
|
|
193
|
+
modules = merged.get("modules")
|
|
194
|
+
modules = modules if isinstance(modules, dict) else {}
|
|
195
|
+
for section in (merged.get("core"), modules.get("bmm")):
|
|
196
|
+
if not isinstance(section, dict):
|
|
197
|
+
continue
|
|
198
|
+
for key, value in section.items():
|
|
199
|
+
if isinstance(value, bool):
|
|
200
|
+
flat[key] = "true" if value else "false"
|
|
201
|
+
elif isinstance(value, (str, int, float)):
|
|
202
|
+
flat[key] = str(value)
|
|
203
|
+
return flat
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def render_template(content, vars_):
|
|
207
|
+
"""Resolve {{.var}} substitutions. Unresolved references emit an empty string,
|
|
208
|
+
but main() HALTs on any missing reference before rendering starts, so this
|
|
209
|
+
fallback never fires in practice."""
|
|
210
|
+
return re.sub(r"\{\{\.(\w+)\}\}", lambda m: vars_.get(m.group(1), ""), content)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def collect_missing_vars(sources, vars_):
|
|
214
|
+
"""Map each {{.var}} name referenced by the source .md files but absent from
|
|
215
|
+
the merged config to the files that reference it. A missing key must HALT:
|
|
216
|
+
missingkey=zero rendering would bake a corrupted workflow (empty paths,
|
|
217
|
+
blank language lines) with no failure signal."""
|
|
218
|
+
missing = {}
|
|
219
|
+
for fname, content in sources:
|
|
220
|
+
for name in re.findall(r"\{\{\.(\w+)\}\}", content):
|
|
221
|
+
if name not in vars_:
|
|
222
|
+
files = missing.setdefault(name, [])
|
|
223
|
+
if fname not in files:
|
|
224
|
+
files.append(fname)
|
|
225
|
+
return missing
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _scalar_str(value):
|
|
229
|
+
"""Stringify a scalar for inline rendering: booleans lowercase (matching
|
|
230
|
+
BMad config conventions), None as empty, everything else via str()."""
|
|
231
|
+
if value is None:
|
|
232
|
+
return ""
|
|
233
|
+
if isinstance(value, bool):
|
|
234
|
+
return "true" if value else "false"
|
|
235
|
+
return str(value)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
# [workflow] keys holding review layers ([[workflow.review_layers]] tables with
|
|
239
|
+
# id/name/instruction/when fields). This renderer knows this skill's
|
|
240
|
+
# customization schema outright — layer semantics are materialized here, not
|
|
241
|
+
# interpreted by the LLM at run time.
|
|
242
|
+
_REVIEW_LAYER_KEYS = ("review_layers", "oneshot_review_layers")
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _render_review_layers(layers):
|
|
246
|
+
"""Materialize review layers into direct invocation blocks. A layer with an
|
|
247
|
+
empty or missing instruction is disabled (that is how an override turns off
|
|
248
|
+
a default layer) and drops out entirely. A `when` condition is the one part
|
|
249
|
+
that stays with the LLM: it renders as a run-time guard line. No active
|
|
250
|
+
layers renders as the HALT instruction the workflow would otherwise have to
|
|
251
|
+
derive from an empty list."""
|
|
252
|
+
active = [
|
|
253
|
+
layer
|
|
254
|
+
for layer in layers
|
|
255
|
+
if isinstance(layer, dict) and _scalar_str(layer.get("instruction")).strip()
|
|
256
|
+
]
|
|
257
|
+
if not active:
|
|
258
|
+
return (
|
|
259
|
+
"No review layers are active. HALT with status `blocked` and "
|
|
260
|
+
"blocking condition `no active review layers`."
|
|
261
|
+
)
|
|
262
|
+
blocks = []
|
|
263
|
+
for layer in active:
|
|
264
|
+
title = (
|
|
265
|
+
_scalar_str(layer.get("name")).strip()
|
|
266
|
+
or _scalar_str(layer.get("id")).strip()
|
|
267
|
+
or "Review layer"
|
|
268
|
+
)
|
|
269
|
+
lines = [f"#### {title}", ""]
|
|
270
|
+
when = _scalar_str(layer.get("when")).strip()
|
|
271
|
+
if when:
|
|
272
|
+
lines.append(
|
|
273
|
+
"Run this layer only if the following holds in the "
|
|
274
|
+
f"current context: `{when}`"
|
|
275
|
+
)
|
|
276
|
+
lines.append("")
|
|
277
|
+
lines.append(_scalar_str(layer.get("instruction")).strip("\n"))
|
|
278
|
+
blocks.append("\n".join(lines))
|
|
279
|
+
return "\n\n".join(blocks)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def _render_workflow_value(key, value):
|
|
283
|
+
"""Format a resolved [workflow] value for inline substitution. Review-layer
|
|
284
|
+
keys materialize as invocation blocks; other lists render as markdown
|
|
285
|
+
bullets (empty -> '_None._'); scalars render verbatim. Each list item uses
|
|
286
|
+
the same scalar formatting so booleans stay consistent. Entries are emitted
|
|
287
|
+
as-is so runtime placeholders like {project-root} or {diff_output} survive
|
|
288
|
+
for the LLM to resolve."""
|
|
289
|
+
if key in _REVIEW_LAYER_KEYS and isinstance(value, list):
|
|
290
|
+
return _render_review_layers(value)
|
|
291
|
+
if isinstance(value, list):
|
|
292
|
+
if not value:
|
|
293
|
+
return "_None._"
|
|
294
|
+
return "\n".join(f"- {_scalar_str(item)}" for item in value)
|
|
295
|
+
return _scalar_str(value)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def render_workflow(content, workflow):
|
|
299
|
+
"""Resolve {workflow.<key>} placeholders from the resolved [workflow] block.
|
|
300
|
+
Unknown keys emit an empty string (missingkey=zero, matching render_template).
|
|
301
|
+
Distinct regex from render_template so single-curly runtime placeholders
|
|
302
|
+
elsewhere are untouched."""
|
|
303
|
+
return re.sub(
|
|
304
|
+
r"\{workflow\.(\w+)\}",
|
|
305
|
+
lambda m: _render_workflow_value(m.group(1), workflow.get(m.group(1))),
|
|
306
|
+
content,
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def main():
|
|
311
|
+
script_dir = os.path.dirname(os.path.abspath(__file__))
|
|
312
|
+
skill_name = os.path.basename(script_dir)
|
|
313
|
+
root = find_project_root()
|
|
314
|
+
root = root.replace(os.sep, "/")
|
|
315
|
+
|
|
316
|
+
vars_ = flatten_central_config(load_central_config(root))
|
|
317
|
+
|
|
318
|
+
for key in list(vars_.keys()):
|
|
319
|
+
vars_[key] = vars_[key].replace("{project-root}", root)
|
|
320
|
+
|
|
321
|
+
vars_["project_root"] = root
|
|
322
|
+
|
|
323
|
+
# Guarded ahead of the general missing-vars scan: sprint_status and
|
|
324
|
+
# deferred_work_file derive from it below, and unlike the scan (absent
|
|
325
|
+
# keys only) this also HALTs on a present-but-empty value.
|
|
326
|
+
implementation_artifacts = vars_.get("implementation_artifacts", "").strip()
|
|
327
|
+
if not implementation_artifacts:
|
|
328
|
+
print(
|
|
329
|
+
"HALT and report to the user: config is missing `implementation_artifacts` "
|
|
330
|
+
"(expected under [core] or [modules.bmm] in _bmad/config.toml)"
|
|
331
|
+
)
|
|
332
|
+
sys.exit(1)
|
|
333
|
+
|
|
334
|
+
vars_["sprint_status"] = posixpath.join(
|
|
335
|
+
implementation_artifacts, "sprint-status.yaml"
|
|
336
|
+
)
|
|
337
|
+
vars_["deferred_work_file"] = posixpath.join(
|
|
338
|
+
implementation_artifacts, "deferred-work.md"
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
sources = []
|
|
342
|
+
for fname in sorted(os.listdir(script_dir)):
|
|
343
|
+
if not fname.endswith(".md") or fname == "SKILL.md":
|
|
344
|
+
continue
|
|
345
|
+
with open(
|
|
346
|
+
posixpath.join(script_dir, fname), "r", encoding="utf-8", newline=""
|
|
347
|
+
) as fh:
|
|
348
|
+
sources.append((fname, fh.read()))
|
|
349
|
+
|
|
350
|
+
missing = collect_missing_vars(sources, vars_)
|
|
351
|
+
if missing:
|
|
352
|
+
details = "; ".join(
|
|
353
|
+
f"`{name}` (referenced by {', '.join(files)})"
|
|
354
|
+
for name, files in sorted(missing.items())
|
|
355
|
+
)
|
|
356
|
+
print(
|
|
357
|
+
f"HALT and report to the user: config is missing {details} "
|
|
358
|
+
"(expected under [core] or [modules.bmm] in _bmad/config.toml)"
|
|
359
|
+
)
|
|
360
|
+
sys.exit(1)
|
|
361
|
+
|
|
362
|
+
workflow = resolve_workflow(root, script_dir.replace(os.sep, "/"), skill_name)
|
|
363
|
+
|
|
364
|
+
out_dir = posixpath.join(root, "_bmad", "render", skill_name)
|
|
365
|
+
os.makedirs(out_dir, exist_ok=True)
|
|
366
|
+
|
|
367
|
+
for fname in os.listdir(out_dir):
|
|
368
|
+
if fname.endswith(".md"):
|
|
369
|
+
os.remove(posixpath.join(out_dir, fname))
|
|
370
|
+
|
|
371
|
+
for fname, content in sources:
|
|
372
|
+
dst = posixpath.join(out_dir, fname)
|
|
373
|
+
with open(dst, "w", encoding="utf-8", newline="") as fh:
|
|
374
|
+
fh.write(render_workflow(render_template(content, vars_), workflow))
|
|
375
|
+
|
|
376
|
+
workflow_md = posixpath.join(out_dir, "workflow.md")
|
|
377
|
+
print(f"read and follow {workflow_md}")
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
if __name__ == "__main__":
|
|
381
|
+
main()
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
---
|
|
2
|
-
deferred_work_file: '{implementation_artifacts}/deferred-work.md'
|
|
3
2
|
spec_file: '' # set at runtime once a route resolves it; some HALT branches exit before it is set
|
|
4
3
|
spec_folder: '' # set at runtime under folder+id dispatch only
|
|
5
4
|
story_id: '' # set at runtime under folder+id dispatch only
|
|
@@ -9,7 +8,7 @@ story_id: '' # set at runtime under folder+id dispatch only
|
|
|
9
8
|
|
|
10
9
|
## RULES
|
|
11
10
|
|
|
12
|
-
-
|
|
11
|
+
- **Language** — Speak in `{{.communication_language}}`. Write any file output in `{{.document_output_language}}`.
|
|
13
12
|
- Treat the invocation intent as workflow input, not as a substitute for step-02 investigation and spec generation.
|
|
14
13
|
- **EARLY EXIT** means: stop this step immediately, then read and follow the target file. Return here only if a later step explicitly says to loop back.
|
|
15
14
|
|
|
@@ -44,7 +43,7 @@ If the invocation prompt does not contain enough intent to identify what to impl
|
|
|
44
43
|
## INSTRUCTIONS
|
|
45
44
|
|
|
46
45
|
1. Load context.
|
|
47
|
-
- List files in `{planning_artifacts}` and `{implementation_artifacts}`.
|
|
46
|
+
- List files in `{{.planning_artifacts}}` and `{{.implementation_artifacts}}`.
|
|
48
47
|
- If the invocation prompt points to an unformatted spec or intent file, ingest that file. Do not scan for unrelated intent files.
|
|
49
48
|
- **Determine context strategy.** Using the intent and the artifact listing, infer whether the current work is a story from an epic. Do not rely on filename patterns or regex — reason about the intent, the listing, and any epics file content together.
|
|
50
49
|
|
|
@@ -52,15 +51,15 @@ If the invocation prompt does not contain enough intent to identify what to impl
|
|
|
52
51
|
|
|
53
52
|
1. Identify the epic number `{epic_num}` and (if present) the story number `{story_num}`. If you can't identify an epic number, use path B.
|
|
54
53
|
|
|
55
|
-
2. **Check for a valid cached epic context.** Look for `{implementation_artifacts}/epic-<N>-context.md` (where `<N>` is the epic number). A file is **valid** when it exists, is non-empty, starts with `# Epic <N> Context:` (with the correct epic number), and no file in `{planning_artifacts}` is newer.
|
|
54
|
+
2. **Check for a valid cached epic context.** Look for `{{.implementation_artifacts}}/epic-<N>-context.md` (where `<N>` is the epic number). A file is **valid** when it exists, is non-empty, starts with `# Epic <N> Context:` (with the correct epic number), and no file in `{{.planning_artifacts}}` is newer.
|
|
56
55
|
- **If valid:** load it as the primary planning context. Do not load raw planning docs (PRD, architecture, UX, etc.).
|
|
57
56
|
- **If missing, empty, or invalid:** compile it in the next bullet.
|
|
58
57
|
|
|
59
|
-
3. **Compile epic context if needed.** If no valid cached epic context was loaded, produce `{implementation_artifacts}/epic-<N>-context.md` by spawning a subagent synchronously (wait for it to return in this turn) with `./compile-epic-context.md` as its prompt. Pass it the epic number, the epics file path, the `{planning_artifacts}` directory, and the output path `{implementation_artifacts}/epic-<N>-context.md`.
|
|
58
|
+
3. **Compile epic context if needed.** If no valid cached epic context was loaded, produce `{{.implementation_artifacts}}/epic-<N>-context.md` by spawning a subagent synchronously (wait for it to return in this turn) with `./compile-epic-context.md` as its prompt. Pass it the epic number, the epics file path, the `{{.planning_artifacts}}` directory, and the output path `{{.implementation_artifacts}}/epic-<N>-context.md`.
|
|
60
59
|
|
|
61
60
|
4. **Verify if compiled.** If epic context was compiled, verify the output file exists, is non-empty, and starts with `# Epic <N> Context:`. If valid, load it. If verification fails, HALT with status `blocked` and blocking condition `context compilation verification failed`.
|
|
62
61
|
|
|
63
|
-
5. **Previous story continuity.** Regardless of which context source succeeded above, scan `{implementation_artifacts}` for specs from the same epic with `status: done` and a lower story number. Load the most recent one (highest story number below current). Extract its **Code Map**, **Design Notes**, **Spec Change Log**, and **task list** as continuity context for step-02 planning. If no `done` spec is found but an `in-review` spec exists for the same epic with a lower story number, HALT with status `blocked` and blocking condition `missing previous-story continuity decision`.
|
|
62
|
+
5. **Previous story continuity.** Regardless of which context source succeeded above, scan `{{.implementation_artifacts}}` for specs from the same epic with `status: done` and a lower story number. Load the most recent one (highest story number below current). Extract its **Code Map**, **Design Notes**, **Spec Change Log**, and **task list** as continuity context for step-02 planning. If no `done` spec is found but an `in-review` spec exists for the same epic with a lower story number, HALT with status `blocked` and blocking condition `missing previous-story continuity decision`.
|
|
64
63
|
|
|
65
64
|
**B) Freeform path** — if the intent is not an epic story:
|
|
66
65
|
- Planning artifacts are the output of BMAD phases 1-3. Typical files include:
|
|
@@ -75,9 +74,9 @@ If the invocation prompt does not contain enough intent to identify what to impl
|
|
|
75
74
|
4. Multi-goal warning. If the intent appears to contain multiple independently shippable goals, carry `multiple-goals` forward so step-02 can add it to `{spec_file}` frontmatter `warnings`. Do not split or block.
|
|
76
75
|
5. Route:
|
|
77
76
|
|
|
78
|
-
**Folder+id dispatch:** derive a valid kebab-case slug from the entry's `title` (and `description` if needed) — the same kebab-casing convention as below, but never prefixed with `{story_id}`, since the id is already the filename's separate leading segment. Set `spec_file` = `{spec_folder}/stories/{story_id}-{slug}.md`. The id already disambiguates: no `{implementation_artifacts}` fallback, no `-2`/`-3` suffixing.
|
|
77
|
+
**Folder+id dispatch:** derive a valid kebab-case slug from the entry's `title` (and `description` if needed) — the same kebab-casing convention as below, but never prefixed with `{story_id}`, since the id is already the filename's separate leading segment. Set `spec_file` = `{spec_folder}/stories/{story_id}-{slug}.md`. The id already disambiguates: no `{{.implementation_artifacts}}` fallback, no `-2`/`-3` suffixing.
|
|
79
78
|
|
|
80
|
-
**Otherwise:** derive a valid kebab-case slug from the clarified intent. If the intent references a tracking identifier (story number, issue number, ticket ID), lead the slug with it (e.g. `3-2-digest-delivery`, `gh-47-fix-auth`). If `{implementation_artifacts}/spec-{slug}.md` already exists: if its status is `draft`, treat it as the same work and resume it (set `spec_file` to that path, **EARLY EXIT** → `./step-02-plan.md`); otherwise append `-2`, `-3`, etc. Set `spec_file` = `{implementation_artifacts}/spec-{slug}.md`.
|
|
79
|
+
**Otherwise:** derive a valid kebab-case slug from the clarified intent. If the intent references a tracking identifier (story number, issue number, ticket ID), lead the slug with it (e.g. `3-2-digest-delivery`, `gh-47-fix-auth`). If `{{.implementation_artifacts}}/spec-{slug}.md` already exists: if its status is `draft`, treat it as the same work and resume it (set `spec_file` to that path, **EARLY EXIT** → `./step-02-plan.md`); otherwise append `-2`, `-3`, etc. Set `spec_file` = `{{.implementation_artifacts}}/spec-{slug}.md`.
|
|
81
80
|
|
|
82
81
|
## NEXT
|
|
83
82
|
|
|
@@ -1,26 +1,22 @@
|
|
|
1
|
-
---
|
|
2
|
-
deferred_work_file: '{implementation_artifacts}/deferred-work.md'
|
|
3
|
-
---
|
|
4
|
-
|
|
5
1
|
# Step 2: Plan
|
|
6
2
|
|
|
7
3
|
## RULES
|
|
8
4
|
|
|
9
|
-
-
|
|
5
|
+
- **Language** — Speak in `{{.communication_language}}`. Write any file output in `{{.document_output_language}}`.
|
|
10
6
|
- No human interaction: do not ask questions or wait for approval in this step.
|
|
11
7
|
|
|
12
8
|
## INSTRUCTIONS
|
|
13
9
|
|
|
14
10
|
1. Draft resume check. If `{spec_file}` exists with `status: draft`, read it and capture the verbatim `<intent-contract>...</intent-contract>` block as `preserved_intent_contract`. Otherwise `preserved_intent_contract` is empty.
|
|
15
11
|
2. Investigate codebase. _Read the code yourself for narrow, localized tasks. Isolate deep exploration in synchronous subagents: instruct them to give you distilled summaries only, and plan from those summaries._
|
|
16
|
-
3. Read `./spec-template.md` fully. Fill it out based on the intent and investigation. If `{preserved_intent_contract}` is non-empty, substitute it for the `<intent-contract>` block in your filled spec before writing. Write the result to `{spec_file}`.
|
|
12
|
+
3. Read `./spec-template.md` fully. Fill it out based on the intent and investigation, resolving the template's `date` field to the current system date. If `{preserved_intent_contract}` is non-empty, substitute it for the `<intent-contract>` block in your filled spec before writing. Write the result to `{spec_file}`.
|
|
17
13
|
4. Self-review against READY FOR DEVELOPMENT standard.
|
|
18
14
|
5. If intent gaps exist, do not fantasize and do not leave open questions. Multiple defensible readings of the intent that lead to observably different outcomes, with nothing in the intent to select between them, are an intent gap — do not resolve one by picking a reading. HALT with status `blocked`, blocking condition `intent gap`, and include the unanswered questions and evidence gathered.
|
|
19
15
|
6. Warning check. If step-01 carried `multiple-goals`, add it to `{spec_file}` frontmatter `warnings`. If `{spec_file}` exceeds 1600 tokens, add `oversized` to frontmatter `warnings`. Continue either way.
|
|
20
16
|
|
|
21
17
|
### READY-FOR-DEVELOPMENT GATE
|
|
22
18
|
|
|
23
|
-
Re-read `./
|
|
19
|
+
Re-read `./workflow.md`, then re-read `{spec_file}` from disk and verify the spec meets the READY FOR DEVELOPMENT standard.
|
|
24
20
|
|
|
25
21
|
- **If the file is missing:** HALT with status `blocked` and blocking condition `planned spec file disappeared before implementation`.
|
|
26
22
|
- **If the spec meets the standard:** set `{spec_file}` frontmatter status to `ready-for-dev`. If the invocation prompt directs a halt after planning (standard phrasing: `Halt after planning.` — accept any clear equivalent), HALT with status `ready-for-dev`; otherwise continue to step 3.
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
## RULES
|
|
7
7
|
|
|
8
|
-
-
|
|
8
|
+
- **Language** — Speak in `{{.communication_language}}`. Write any file output in `{{.document_output_language}}`.
|
|
9
9
|
- No human interaction: do not ask questions or wait for approval in this step.
|
|
10
10
|
- Content inside `<intent-contract>` in `{spec_file}` is read-only. Do not modify.
|
|
11
11
|
|
|
@@ -23,9 +23,11 @@ Capture `baseline_revision` (current HEAD, or `NO_VCS` if version control is una
|
|
|
23
23
|
|
|
24
24
|
Change `{spec_file}` status to `in-progress` in the frontmatter before starting implementation.
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
Substitute the runtime placeholders (e.g. `{spec_file}`) into the implementation handoff below, then follow it verbatim. Do not add parent-authored goal restatements, file lists, ownership boundaries, or acceptance criteria to the handoff — the spec is the subagent's sole source of truth. If the handoff conflicts with the spec, HALT with status `blocked` and blocking condition `handoff conflicts with spec`, and include both conflicting passages.
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
{workflow.implementation_handoff}
|
|
29
|
+
|
|
30
|
+
Invoke the subagent **synchronously** and wait for it to return in this same turn — do not background/detach it (`run_in_background`) or end your turn to await a notification (see workflow.md → Subagents). Resume at "Verify" only after it returns. If the platform allows, keep the subagent available for re-engagement after it returns — step-04 may send it review fixes.
|
|
29
31
|
|
|
30
32
|
**Path formatting rule:** Any markdown links written into `{spec_file}` must use paths relative to `{spec_file}`'s directory so they are clickable in VS Code. Any file paths displayed in terminal/conversation output must use CWD-relative format with `:line` notation (e.g., `src/path/file.ts:42`) for terminal clickability. No leading `/` in either case.
|
|
31
33
|
|
|
@@ -1,12 +1,8 @@
|
|
|
1
|
-
---
|
|
2
|
-
deferred_work_file: '{implementation_artifacts}/deferred-work.md'
|
|
3
|
-
---
|
|
4
|
-
|
|
5
1
|
# Step 4: Review
|
|
6
2
|
|
|
7
3
|
## RULES
|
|
8
4
|
|
|
9
|
-
-
|
|
5
|
+
- **Language** — Speak in `{{.communication_language}}`. Write any file output in `{{.document_output_language}}`.
|
|
10
6
|
- No human interaction: do not ask questions or wait for approval in this step.
|
|
11
7
|
- All review subagents must run at the same model capability as the current session.
|
|
12
8
|
|
|
@@ -22,13 +18,11 @@ Do NOT `git add` anything — this is read-only inspection.
|
|
|
22
18
|
|
|
23
19
|
### Review
|
|
24
20
|
|
|
25
|
-
The review layers are `{workflow.review_layers}`, resolved during activation.
|
|
26
|
-
|
|
27
|
-
Skip every layer whose `instruction` is empty or missing — that is how an override disables a default layer — and every layer whose `when` condition (if present) does not hold in the current context. If no layers remain, HALT with status `blocked` and blocking condition `no active review layers`.
|
|
28
|
-
|
|
29
21
|
Runtime placeholders: `{diff_output}` is the diff constructed above. `{verbatim_intent}` is the invocation intent exactly as this run received it at step-01; if the run started from an existing spec file rather than a fresh intent, it is the spec's `<intent-contract>` block instead.
|
|
30
22
|
|
|
31
|
-
Execute
|
|
23
|
+
Execute these review layers in parallel wherever their execution methods allow: substitute the runtime placeholders (e.g. `{diff_output}`) into each layer's instruction, then follow it verbatim. Parallel means several blocking calls awaited together in this turn — never backgrounded or detached, never ending the turn to await results (see workflow.md → Subagents). Spawn every reviewer subagent before reading or reacting to any of their output; begin collection and triage only once all are launched.
|
|
24
|
+
|
|
25
|
+
{workflow.review_layers}
|
|
32
26
|
|
|
33
27
|
### Classify
|
|
34
28
|
|
|
@@ -56,17 +50,17 @@ Execute all remaining layers in parallel wherever their execution methods allow:
|
|
|
56
50
|
- addressed_findings:
|
|
57
51
|
- `[high|medium|low]` `[patch|bad_spec]` <finding summary and action taken in this pass>
|
|
58
52
|
```
|
|
59
|
-
Where `count` is either just `0`, or total with breakdown by severity `N: (high Nhigh, medium Nmedium, low Nlow)`.
|
|
53
|
+
Where `{date}` is the current system date and `count` is either just `0`, or total with breakdown by severity `N: (high Nhigh, medium Nmedium, low Nlow)`.
|
|
60
54
|
If no patch was fixed and no bad_spec repair loopback was triggered in this pass, write:
|
|
61
55
|
```markdown
|
|
62
56
|
- addressed_findings:
|
|
63
57
|
- none
|
|
64
58
|
```
|
|
65
59
|
5. Process findings in cascading order. If intent_gap exists, lower findings are moot; follow the intent_gap branch below. If bad_spec exists, lower findings are moot since code will be re-derived. If neither exists, process patch and defer normally. Before each bad_spec loopback, read `{spec_file}` frontmatter `review_loop_iteration` (missing means `0`), increment it by 1, and write it back. If it exceeds 5, append the triage-log entry for this pass with `addressed_findings: none`, then HALT with status `blocked` and blocking condition `review repair loop exceeded 5 iterations (non-convergence)`.
|
|
66
|
-
- **intent_gap** — Root cause is inside `<intent-contract>`. Save the attempted change as a patch file in `{implementation_artifacts}` and reference it from the triage-log entry, then revert code changes. Append the triage-log entry for this pass with `addressed_findings: none`, then HALT with status `blocked`, blocking condition `intent gap`, and include the unresolved questions and the saved patch path.
|
|
60
|
+
- **intent_gap** — Root cause is inside `<intent-contract>`. Save the attempted change as a patch file in `{{.implementation_artifacts}}` and reference it from the triage-log entry, then revert code changes. Append the triage-log entry for this pass with `addressed_findings: none`, then HALT with status `blocked`, blocking condition `intent gap`, and include the unresolved questions and the saved patch path.
|
|
67
61
|
- **bad_spec** — Root cause is outside `<intent-contract>`. Do not modify content inside `<intent-contract>`. Before reverting code: extract KEEP instructions for positive preservation (what worked well and must survive re-derivation). Revert code changes. Read the `## Spec Change Log` in `{spec_file}` and strictly respect all logged constraints when amending the sections outside `<intent-contract>` that contain the root cause. Append a new change-log entry recording: the triggering finding, what was amended, the known-bad state avoided, and the KEEP instructions. Append the triage-log entry for this pass, listing every bad_spec finding that triggered the spec amendment and implementation loopback under `addressed_findings`. Read fully and follow `./step-03-implement.md` to re-derive the code, then this step will run again.
|
|
68
62
|
- **patch** — Auto-fix. These are the only findings that survive loopbacks. If the step-03 implementation subagent can be re-engaged with its context intact, send it all patch findings in one synchronous message — for each: the file, what is wrong, and what the fix must do. If it cannot be re-engaged, apply the patches yourself. Then re-run the commands in `{spec_file}`'s `## Verification` section (or perform its manual checks); if verification fails and the failure cannot be fixed, HALT with status `blocked` and blocking condition `patch verification failed`. Append the triage-log entry for this pass, listing every patch fixed in this pass under `addressed_findings`.
|
|
69
|
-
- **defer** — Append one new entry to `{deferred_work_file}` using this format. Do not modify existing entries or look for duplicates.
|
|
63
|
+
- **defer** — Append one new entry to `{{.deferred_work_file}}` using this format. Do not modify existing entries or look for duplicates.
|
|
70
64
|
```markdown
|
|
71
65
|
- source_spec: `{spec_file}`
|
|
72
66
|
summary: <one sentence>
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# Dev Auto Workflow
|
|
2
|
+
|
|
3
|
+
**Goal:** Turn intent into a hardened, reviewable artifact, without human interaction.
|
|
4
|
+
|
|
5
|
+
**CRITICAL:** If a step says "read fully and follow step-XX", you read and follow step-XX. No exceptions.
|
|
6
|
+
|
|
7
|
+
## HALT
|
|
8
|
+
|
|
9
|
+
To HALT with a final status and optional blocking condition:
|
|
10
|
+
|
|
11
|
+
1. **Folder+id dispatch** (`{spec_folder}` and `{story_id}` are set): the write-back always lands at the id-keyed story spec. The `{{.implementation_artifacts}}` fallback in step 2 below is never used in this mode, even for halts before planning starts.
|
|
12
|
+
- If `{spec_file}` is still empty, resolve it now:
|
|
13
|
+
- **Entry not resolved** (`stories.yaml` is missing/unparseable, or `{story_id}` has no matching entry): use the fixed slug segment `unresolved`: `{spec_file}` = `{spec_folder}/stories/{story_id}-unresolved.md`.
|
|
14
|
+
- **Ambiguous on-disk match** (the halt is `ambiguous story file match` — more than one file already matches `{spec_folder}/stories/{story_id}-*.md`): use the fixed slug segment `ambiguous` instead of deriving from the title, so the write-back neither creates a third title-derived candidate nor risks silently landing on one of the existing ambiguous files: `{spec_file}` = `{spec_folder}/stories/{story_id}-ambiguous.md`.
|
|
15
|
+
- **Otherwise** (the entry was resolved and no ambiguous on-disk match exists): derive `{spec_file}` = `{spec_folder}/stories/{story_id}-{slug}.md`, where `{slug}` is a kebab-case slug from `title` (and `description` if needed) with no `{story_id}` prefix — the same derivation step-01's Route uses.
|
|
16
|
+
- If `{spec_file}` exists on disk, update `status` in frontmatter and append missing result details under `## Auto Run Result`.
|
|
17
|
+
- If it does not exist, create it as a skeletal story spec:
|
|
18
|
+
```markdown
|
|
19
|
+
---
|
|
20
|
+
status: <final status>
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
# <entry title, or "Story {story_id}" if the entry could not be resolved or the on-disk match was ambiguous>
|
|
24
|
+
|
|
25
|
+
## Auto Run Result
|
|
26
|
+
|
|
27
|
+
Status: <final status>
|
|
28
|
+
Blocking condition: <blocking condition, if any>
|
|
29
|
+
```
|
|
30
|
+
2. **Otherwise:**
|
|
31
|
+
- If `{spec_file}` is known and exists, update `status` in frontmatter and append missing result details under `## Auto Run Result`.
|
|
32
|
+
- If `{spec_file}` is unknown or missing, create `{{.implementation_artifacts}}/bmad-dev-auto-result-<slug-or-timestamp>.md` with:
|
|
33
|
+
```markdown
|
|
34
|
+
---
|
|
35
|
+
status: <final status>
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
# BMad Dev Auto Result
|
|
39
|
+
|
|
40
|
+
Status: <final status>
|
|
41
|
+
Blocking condition: <blocking condition, if any>
|
|
42
|
+
```
|
|
43
|
+
3. Follow **On Complete** below, then stop the workflow.
|
|
44
|
+
|
|
45
|
+
### On Complete
|
|
46
|
+
|
|
47
|
+
If anything appears below, follow it as the final terminal instruction before exiting; otherwise exit normally.
|
|
48
|
+
|
|
49
|
+
{workflow.on_complete}
|
|
50
|
+
|
|
51
|
+
## Subagents
|
|
52
|
+
|
|
53
|
+
Using subagents when instructed is mandatory. If you cannot, HALT with status `blocked` and blocking condition `no subagents`.
|
|
54
|
+
|
|
55
|
+
Invoke every subagent **synchronously**: launch it, wait for it to return within the same turn, then continue with its result. When a step says to run subagents "in parallel" (e.g. the reviewers), that means several **blocking** calls awaited together in one turn — not detached execution. Never run a subagent in the background / detached / async (e.g. `run_in_background: true`), and never end your turn to "await a completion notification." This workflow runs unattended: there is no event loop to resume a yielded turn, so a backgrounded subagent never hands control back and the run stalls. The only sanctioned way to end a turn is the HALT protocol above with an explicit terminal `status`.
|
|
56
|
+
|
|
57
|
+
## READY FOR DEVELOPMENT STANDARD
|
|
58
|
+
|
|
59
|
+
A specification is "Ready for Development" when:
|
|
60
|
+
|
|
61
|
+
- **Actionable**: Every task has a file path and specific action.
|
|
62
|
+
- **Logical**: Tasks ordered by dependency.
|
|
63
|
+
- **Testable**: All ACs use Given/When/Then.
|
|
64
|
+
- **Surface-anchored**: ACs observe the outermost surface the intent references — never a more internal proxy for it (e.g. the API response, not the database row behind it).
|
|
65
|
+
- **Complete**: No placeholders or TBDs.
|
|
66
|
+
- **Sufficient**: No known requirement, acceptance, dependency, or implementation gaps remain unresolved.
|
|
67
|
+
- **Coherent**: No unresolved ambiguities or internal contradictions.
|
|
68
|
+
|
|
69
|
+
## Conventions
|
|
70
|
+
|
|
71
|
+
- Bare paths (e.g. `step-01-clarify-and-route.md`) resolve from the skill root.
|
|
72
|
+
- `{skill-root}` resolves to this skill's installed directory (where `customize.toml` lives).
|
|
73
|
+
- `{project-root}`-prefixed paths resolve from the project working directory.
|
|
74
|
+
- `{skill-name}` resolves to the skill directory's basename.
|
|
75
|
+
|
|
76
|
+
## On Activation
|
|
77
|
+
|
|
78
|
+
### Step 1: Execute Prepend Steps
|
|
79
|
+
|
|
80
|
+
Execute each of these steps in order before proceeding (`_None._` means skip):
|
|
81
|
+
|
|
82
|
+
{workflow.activation_steps_prepend}
|
|
83
|
+
|
|
84
|
+
### Step 2: Load Persistent Facts
|
|
85
|
+
|
|
86
|
+
Treat every entry below as foundational context you carry for the rest of the workflow run. Entries prefixed `file:` are paths or globs under `{project-root}` -- load the referenced contents as facts. All other entries are facts verbatim (`_None._` means none):
|
|
87
|
+
|
|
88
|
+
{workflow.persistent_facts}
|
|
89
|
+
|
|
90
|
+
### Step 3: Execute Append Steps
|
|
91
|
+
|
|
92
|
+
Execute each of these steps in order (`_None._` means skip):
|
|
93
|
+
|
|
94
|
+
{workflow.activation_steps_append}
|
|
95
|
+
|
|
96
|
+
Activation is complete after all activation steps have run.
|
|
97
|
+
|
|
98
|
+
## Workflow Execution
|
|
99
|
+
|
|
100
|
+
Follow the step files in order. Read one step fully, execute it, then load the next step only when directed. Do not skip, reorder, or pre-load steps.
|
|
101
|
+
|
|
102
|
+
## First workflow step
|
|
103
|
+
|
|
104
|
+
Read fully and follow: `./step-01-clarify-and-route.md` to begin the workflow.
|
|
@@ -12,7 +12,10 @@ files to {project-root}/_bmad/render/bmad-quick-dev/.
|
|
|
12
12
|
Config: four-layer merge of _bmad/config.toml + config.user.toml +
|
|
13
13
|
custom/config.toml + custom/config.user.toml (post-#2285 installs).
|
|
14
14
|
Keys surface from [core] and [modules.bmm]. Missing or unparseable
|
|
15
|
-
config.toml → HALT.
|
|
15
|
+
config.toml → HALT. A {{.var}} referenced by this skill's .md sources but
|
|
16
|
+
absent from the merged config → HALT (never a silent empty substitution).
|
|
17
|
+
Optional layers may be missing, but one that exists and cannot be parsed
|
|
18
|
+
or read → HALT.
|
|
16
19
|
|
|
17
20
|
Customization: three-layer merge of {skill}/customize.toml +
|
|
18
21
|
_bmad/custom/bmad-quick-dev.toml + .user.toml (same structural rules as
|
|
@@ -50,10 +53,12 @@ def find_project_root():
|
|
|
50
53
|
|
|
51
54
|
|
|
52
55
|
def load_toml(path, required=False):
|
|
53
|
-
"""Load a TOML file.
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
56
|
+
"""Load a TOML file. Only absence is negotiable: a missing optional file
|
|
57
|
+
returns {} (customization layers are optional), a missing required file
|
|
58
|
+
HALTs. A file that exists but cannot be parsed or read always HALTs —
|
|
59
|
+
stdout is how this script signals workflow halts to its LLM caller — the
|
|
60
|
+
user wrote it to be honored, and silently continuing with {} would discard
|
|
61
|
+
their customizations with no failure signal."""
|
|
57
62
|
if not os.path.isfile(path):
|
|
58
63
|
if required:
|
|
59
64
|
print(
|
|
@@ -66,17 +71,11 @@ def load_toml(path, required=False):
|
|
|
66
71
|
with open(path, "rb") as fh:
|
|
67
72
|
parsed = tomllib.load(fh)
|
|
68
73
|
except tomllib.TOMLDecodeError as error:
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
sys.exit(1)
|
|
72
|
-
print(f"render.py: warning: failed to parse {path}: {error}", file=sys.stderr)
|
|
73
|
-
return {}
|
|
74
|
+
print(f"HALT and report to the user: failed to parse {path}: {error}")
|
|
75
|
+
sys.exit(1)
|
|
74
76
|
except OSError as error:
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
sys.exit(1)
|
|
78
|
-
print(f"render.py: warning: failed to read {path}: {error}", file=sys.stderr)
|
|
79
|
-
return {}
|
|
77
|
+
print(f"HALT and report to the user: failed to read {path}: {error}")
|
|
78
|
+
sys.exit(1)
|
|
80
79
|
if not isinstance(parsed, dict):
|
|
81
80
|
return {}
|
|
82
81
|
return parsed
|
|
@@ -160,7 +159,7 @@ def resolve_workflow(root, skill_dir, skill_name):
|
|
|
160
159
|
"""Resolve the [workflow] customization block via the three-layer merge
|
|
161
160
|
(skill defaults -> team -> user), highest priority last. Same structural
|
|
162
161
|
rules as resolve_customization.py. All three layers are optional: a missing
|
|
163
|
-
|
|
162
|
+
file is skipped, but an unparseable one HALTs (via load_toml)."""
|
|
164
163
|
defaults = load_toml(posixpath.join(skill_dir, "customize.toml"))
|
|
165
164
|
custom_dir = posixpath.join(root, "_bmad", "custom")
|
|
166
165
|
team = load_toml(posixpath.join(custom_dir, f"{skill_name}.toml"))
|
|
@@ -205,11 +204,27 @@ def flatten_central_config(merged):
|
|
|
205
204
|
|
|
206
205
|
|
|
207
206
|
def render_template(content, vars_):
|
|
208
|
-
"""Resolve {{.var}} substitutions. Unresolved references emit an empty string
|
|
209
|
-
(
|
|
207
|
+
"""Resolve {{.var}} substitutions. Unresolved references emit an empty string,
|
|
208
|
+
but main() HALTs on any missing reference before rendering starts, so this
|
|
209
|
+
fallback never fires in practice."""
|
|
210
210
|
return re.sub(r"\{\{\.(\w+)\}\}", lambda m: vars_.get(m.group(1), ""), content)
|
|
211
211
|
|
|
212
212
|
|
|
213
|
+
def collect_missing_vars(sources, vars_):
|
|
214
|
+
"""Map each {{.var}} name referenced by the source .md files but absent from
|
|
215
|
+
the merged config to the files that reference it. A missing key must HALT:
|
|
216
|
+
missingkey=zero rendering would bake a corrupted workflow (empty paths,
|
|
217
|
+
blank language lines) with no failure signal."""
|
|
218
|
+
missing = {}
|
|
219
|
+
for fname, content in sources:
|
|
220
|
+
for name in re.findall(r"\{\{\.(\w+)\}\}", content):
|
|
221
|
+
if name not in vars_:
|
|
222
|
+
files = missing.setdefault(name, [])
|
|
223
|
+
if fname not in files:
|
|
224
|
+
files.append(fname)
|
|
225
|
+
return missing
|
|
226
|
+
|
|
227
|
+
|
|
213
228
|
def _scalar_str(value):
|
|
214
229
|
"""Stringify a scalar for inline rendering: booleans lowercase (matching
|
|
215
230
|
BMad config conventions), None as empty, everything else via str()."""
|
|
@@ -305,6 +320,9 @@ def main():
|
|
|
305
320
|
|
|
306
321
|
vars_["project_root"] = root
|
|
307
322
|
|
|
323
|
+
# Guarded ahead of the general missing-vars scan: sprint_status and
|
|
324
|
+
# deferred_work_file derive from it below, and unlike the scan (absent
|
|
325
|
+
# keys only) this also HALTs on a present-but-empty value.
|
|
308
326
|
implementation_artifacts = vars_.get("implementation_artifacts", "").strip()
|
|
309
327
|
if not implementation_artifacts:
|
|
310
328
|
print(
|
|
@@ -320,6 +338,27 @@ def main():
|
|
|
320
338
|
implementation_artifacts, "deferred-work.md"
|
|
321
339
|
)
|
|
322
340
|
|
|
341
|
+
sources = []
|
|
342
|
+
for fname in sorted(os.listdir(script_dir)):
|
|
343
|
+
if not fname.endswith(".md") or fname == "SKILL.md":
|
|
344
|
+
continue
|
|
345
|
+
with open(
|
|
346
|
+
posixpath.join(script_dir, fname), "r", encoding="utf-8", newline=""
|
|
347
|
+
) as fh:
|
|
348
|
+
sources.append((fname, fh.read()))
|
|
349
|
+
|
|
350
|
+
missing = collect_missing_vars(sources, vars_)
|
|
351
|
+
if missing:
|
|
352
|
+
details = "; ".join(
|
|
353
|
+
f"`{name}` (referenced by {', '.join(files)})"
|
|
354
|
+
for name, files in sorted(missing.items())
|
|
355
|
+
)
|
|
356
|
+
print(
|
|
357
|
+
f"HALT and report to the user: config is missing {details} "
|
|
358
|
+
"(expected under [core] or [modules.bmm] in _bmad/config.toml)"
|
|
359
|
+
)
|
|
360
|
+
sys.exit(1)
|
|
361
|
+
|
|
323
362
|
workflow = resolve_workflow(root, script_dir.replace(os.sep, "/"), skill_name)
|
|
324
363
|
|
|
325
364
|
out_dir = posixpath.join(root, "_bmad", "render", skill_name)
|
|
@@ -329,13 +368,8 @@ def main():
|
|
|
329
368
|
if fname.endswith(".md"):
|
|
330
369
|
os.remove(posixpath.join(out_dir, fname))
|
|
331
370
|
|
|
332
|
-
for fname in
|
|
333
|
-
if not fname.endswith(".md") or fname == "SKILL.md":
|
|
334
|
-
continue
|
|
335
|
-
src = posixpath.join(script_dir, fname)
|
|
371
|
+
for fname, content in sources:
|
|
336
372
|
dst = posixpath.join(out_dir, fname)
|
|
337
|
-
with open(src, "r", encoding="utf-8", newline="") as fh:
|
|
338
|
-
content = fh.read()
|
|
339
373
|
with open(dst, "w", encoding="utf-8", newline="") as fh:
|
|
340
374
|
fh.write(render_workflow(render_template(content, vars_), workflow))
|
|
341
375
|
|