Merge branch 'main' into core-streamline

This commit is contained in:
Brian
2026-07-18 19:06:15 -05:00
committed by GitHub
11 changed files with 125 additions and 38 deletions
@@ -4,7 +4,7 @@ type: 'feature' # feature | bugfix | refactor | chore
created: '{date}'
status: 'draft' # draft | ready-for-dev | in-progress | in-review | done | blocked
review_loop_iteration: 0 # incremented by step-04 before each review loopback
followup_review_recommended: false # set by step-04 on status: done from the final review pass significance judgment
followup_review_recommended: false # set by step-04 on status: done — true if the LLM decided another review pass is worthwhile
context: [] # optional: `{project-root}/`-prefixed paths to project-wide standards/docs the implementation agent should load. Keep short — only what isn't already distilled into the spec body.
warnings: [] # optional: machine-readable warnings for orchestration, e.g. oversized, multiple-goals
---
@@ -56,7 +56,7 @@ If the invocation prompt does not contain enough intent to identify what to impl
- **If valid:** load it as the primary planning context. Do not load raw planning docs (PRD, architecture, UX, etc.).
- **If missing, empty, or invalid:** compile it in the next bullet.
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 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`.
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`.
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`.
@@ -12,7 +12,7 @@ deferred_work_file: '{implementation_artifacts}/deferred-work.md'
## INSTRUCTIONS
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.
2. Investigate codebase. _Read the code yourself for narrow, localized tasks. Isolate deep exploration in subagents: instruct them to give you distilled summaries only, and plan from those summaries._
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._
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}`.
4. Self-review against READY FOR DEVELOPMENT standard.
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.
@@ -25,7 +25,7 @@ Change `{spec_file}` status to `in-progress` in the frontmatter before starting
The implementation handoff is `{workflow.implementation_handoff}`, resolved during activation. Substitute the runtime placeholders (e.g. `{spec_file}`) into it, 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 resolved handoff conflicts with the spec, HALT with status `blocked` and blocking condition `handoff conflicts with spec`, and include both conflicting passages.
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 SKILL.md → Subagents). Resume at "Verify" only after it returns.
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 SKILL.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.
**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.
@@ -28,7 +28,7 @@ Skip every layer whose `instruction` is empty or missing — that is how an over
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.
Execute all remaining 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. Spawn every reviewer subagent before reading or reacting to any of their output; begin collection and triage only once all are launched.
Execute all remaining 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 SKILL.md → Subagents). Spawn every reviewer subagent before reading or reacting to any of their output; begin collection and triage only once all are launched.
### Classify
@@ -65,7 +65,7 @@ Execute all remaining layers in parallel wherever their execution methods allow:
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)`.
- **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.
- **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.
- **patch** — Auto-fix. These are the only findings that survive loopbacks. After auto-fixing, append the triage-log entry for this pass, listing every patch fixed in this pass under `addressed_findings`.
- **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`.
- **defer** — Append one new entry to `{deferred_work_file}` using this format. Do not modify existing entries or look for duplicates.
```markdown
- source_spec: `{spec_file}`
@@ -80,11 +80,11 @@ Prepare `Auto Run Result` details:
- Summary of implemented change
- Files changed with one-line descriptions
- Review findings breakdown: patches applied, items deferred, items rejected
- Follow-up review recommendation: `true` when the final review pass made review-driven changes significant enough to benefit from an independent follow-up review; otherwise `false`. Use judgment, not a fixed numeric threshold. Base the judgment on the final pass's triage log and fixes, including patched-finding volume, consequence/severity, breadth, behavior/API/security/data impact, and implementation complexity. Many low-severity patched findings can be significant by volume. Do not recommend follow-up for only a few localized low-consequence fixes.
- Follow-up review recommendation: count only this pass's findings triaged `patch` — never defer or reject. `true` if any patched finding was `high` severity, or if `3 × medium count + 1 × low count` is 5 or more; otherwise `false`. Record the patched counts by severity and the score.
- Verification performed, including command outcomes or manual inspection notes
- Any residual risks
Set `{spec_file}` frontmatter `followup_review_recommended` from the judgment above.
Set `{spec_file}` frontmatter `followup_review_recommended` from the computation above.
If version control is available, commit every file in the reviewed diff — tracked and untracked. Do not push. After committing, verify the commit contains each file from the reviewed diff; if any is missing, add it and amend before proceeding. Anything still visible in `git status --porcelain` is by definition not part of the change: leave it in place — do not commit, delete, or gitignore it — and list it under `Auto Run Result` as residual artifacts.
@@ -12,7 +12,10 @@ files to {project-root}/_bmad/render/bmad-quick-dev/.
Config: four-layer merge of _bmad/config.toml + config.user.toml +
custom/config.toml + custom/config.user.toml (post-#2285 installs).
Keys surface from [core] and [modules.bmm]. Missing or unparseable
config.toml → HALT.
config.toml → HALT. A {{.var}} referenced by this skill's .md sources but
absent from the merged config → HALT (never a silent empty substitution).
Optional layers may be missing, but one that exists and cannot be parsed
or read → HALT.
Customization: three-layer merge of {skill}/customize.toml +
_bmad/custom/bmad-quick-dev.toml + .user.toml (same structural rules as
@@ -50,10 +53,12 @@ def find_project_root():
def load_toml(path, required=False):
"""Load a TOML file. For required files, HALT (stdout) on missing/parse
error so the LLM-driven workflow stops — stdout is how this script signals
workflow halts to its LLM caller. For optional files, write a stderr
warning and return {}."""
"""Load a TOML file. Only absence is negotiable: a missing optional file
returns {} (customization layers are optional), a missing required file
HALTs. A file that exists but cannot be parsed or read always HALTs —
stdout is how this script signals workflow halts to its LLM caller — the
user wrote it to be honored, and silently continuing with {} would discard
their customizations with no failure signal."""
if not os.path.isfile(path):
if required:
print(
@@ -66,17 +71,11 @@ def load_toml(path, required=False):
with open(path, "rb") as fh:
parsed = tomllib.load(fh)
except tomllib.TOMLDecodeError as error:
if required:
print(f"HALT and report to the user: failed to parse {path}: {error}")
sys.exit(1)
print(f"render.py: warning: failed to parse {path}: {error}", file=sys.stderr)
return {}
print(f"HALT and report to the user: failed to parse {path}: {error}")
sys.exit(1)
except OSError as error:
if required:
print(f"HALT and report to the user: failed to read {path}: {error}")
sys.exit(1)
print(f"render.py: warning: failed to read {path}: {error}", file=sys.stderr)
return {}
print(f"HALT and report to the user: failed to read {path}: {error}")
sys.exit(1)
if not isinstance(parsed, dict):
return {}
return parsed
@@ -160,7 +159,7 @@ def resolve_workflow(root, skill_dir, skill_name):
"""Resolve the [workflow] customization block via the three-layer merge
(skill defaults -> team -> user), highest priority last. Same structural
rules as resolve_customization.py. All three layers are optional: a missing
or unparseable file warns (via load_toml) and is skipped."""
file is skipped, but an unparseable one HALTs (via load_toml)."""
defaults = load_toml(posixpath.join(skill_dir, "customize.toml"))
custom_dir = posixpath.join(root, "_bmad", "custom")
team = load_toml(posixpath.join(custom_dir, f"{skill_name}.toml"))
@@ -205,11 +204,27 @@ def flatten_central_config(merged):
def render_template(content, vars_):
"""Resolve {{.var}} substitutions. Unresolved references emit an empty string
(Go's missingkey=zero semantics)."""
"""Resolve {{.var}} substitutions. Unresolved references emit an empty string,
but main() HALTs on any missing reference before rendering starts, so this
fallback never fires in practice."""
return re.sub(r"\{\{\.(\w+)\}\}", lambda m: vars_.get(m.group(1), ""), content)
def collect_missing_vars(sources, vars_):
"""Map each {{.var}} name referenced by the source .md files but absent from
the merged config to the files that reference it. A missing key must HALT:
missingkey=zero rendering would bake a corrupted workflow (empty paths,
blank language lines) with no failure signal."""
missing = {}
for fname, content in sources:
for name in re.findall(r"\{\{\.(\w+)\}\}", content):
if name not in vars_:
files = missing.setdefault(name, [])
if fname not in files:
files.append(fname)
return missing
def _scalar_str(value):
"""Stringify a scalar for inline rendering: booleans lowercase (matching
BMad config conventions), None as empty, everything else via str()."""
@@ -305,6 +320,9 @@ def main():
vars_["project_root"] = root
# Guarded ahead of the general missing-vars scan: sprint_status and
# deferred_work_file derive from it below, and unlike the scan (absent
# keys only) this also HALTs on a present-but-empty value.
implementation_artifacts = vars_.get("implementation_artifacts", "").strip()
if not implementation_artifacts:
print(
@@ -320,6 +338,27 @@ def main():
implementation_artifacts, "deferred-work.md"
)
sources = []
for fname in sorted(os.listdir(script_dir)):
if not fname.endswith(".md") or fname == "SKILL.md":
continue
with open(
posixpath.join(script_dir, fname), "r", encoding="utf-8", newline=""
) as fh:
sources.append((fname, fh.read()))
missing = collect_missing_vars(sources, vars_)
if missing:
details = "; ".join(
f"`{name}` (referenced by {', '.join(files)})"
for name, files in sorted(missing.items())
)
print(
f"HALT and report to the user: config is missing {details} "
"(expected under [core] or [modules.bmm] in _bmad/config.toml)"
)
sys.exit(1)
workflow = resolve_workflow(root, script_dir.replace(os.sep, "/"), skill_name)
out_dir = posixpath.join(root, "_bmad", "render", skill_name)
@@ -329,13 +368,8 @@ def main():
if fname.endswith(".md"):
os.remove(posixpath.join(out_dir, fname))
for fname in sorted(os.listdir(script_dir)):
if not fname.endswith(".md") or fname == "SKILL.md":
continue
src = posixpath.join(script_dir, fname)
for fname, content in sources:
dst = posixpath.join(out_dir, fname)
with open(src, "r", encoding="utf-8", newline="") as fh:
content = fh.read()
with open(dst, "w", encoding="utf-8", newline="") as fh:
fh.write(render_workflow(render_template(content, vars_), workflow))
@@ -58,7 +58,7 @@ If the spec is an epic story and `{{.sprint_status}}` exists: find the `developm
- **If missing, empty, or invalid:** continue to step 3.
3. **Compile epic context.** Produce `{{.implementation_artifacts}}/epic-<N>-context.md` by following `./compile-epic-context.md`, in order of preference:
- **Preferred — subagent:** spawn a subagent 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`.
- **Preferred — subagent:** spawn 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`.
- **Fallback — inline** (for runtimes without subagent support, e.g. Copilot, Codex, local Ollama, older Claude): if your runtime cannot spawn subagents, or the spawn fails/times out, read `./compile-epic-context.md` yourself and follow its instructions to produce the same output file.
4. **Verify.** After compilation, verify the output file exists, is non-empty, and starts with `# Epic <N> Context:`. If valid, load it. If verification fails, HALT and report the failure.
@@ -8,7 +8,7 @@
## INSTRUCTIONS
1. Draft resume check. If `{spec_file}` exists with `status: draft`, read it and capture the verbatim `<frozen-after-approval>...</frozen-after-approval>` block as `preserved_intent`. Otherwise `preserved_intent` is empty.
2. Investigate codebase. _Isolate deep exploration in subagents/tasks where available. To prevent context snowballing, instruct subagents to give you distilled summaries only._
2. Investigate codebase. _Isolate deep exploration in synchronous subagents/tasks where available. To prevent context snowballing, instruct subagents to give you distilled summaries only._
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` is non-empty, replace the `<frozen-after-approval>` block in the spec you just filled out with `preserved_intent`, before writing. Write the result to `{spec_file}`.
4. Self-review against READY FOR DEVELOPMENT standard.
5. If intent gaps exist, do not fantasize, do not leave open questions, HALT and ask the human.
@@ -28,7 +28,7 @@ Follow `./sync-sprint-status.md` with `target_status` = `in-progress`.
If `{spec_file}` has a non-empty `context:` list in its frontmatter, load those files before implementation begins. When handing to a subagent, include them in the subagent prompt so it has access to the referenced context.
Hand `{spec_file}` to a subagent/task and let it implement. If no subagents are available, implement directly.
Hand `{spec_file}` to a subagent/task and let it implement. If no subagents are available, implement directly. If the platform allows, keep the subagent available for re-engagement after it returns — step-04 may send it review fixes.
**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.
@@ -18,7 +18,7 @@ Do NOT `git add` anything — this is read-only inspection.
### Review
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.
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. When running layers as subagents, spawn every reviewer before reading or reacting to any of their output; begin collection and triage only once all are launched.
{workflow.review_layers}
@@ -41,7 +41,7 @@ If a layer's instruction requires subagents and none are available, generate one
4. Process findings in cascading order. If intent_gap or bad_spec findings exist, they trigger a loopback — lower findings are moot since code will be re-derived. If neither exists, process patch and defer normally. Before each loopback, read `{spec_file}` frontmatter `review_loop_iteration` (missing means `0`), increment it by 1, and write it back. If it exceeds 5, HALT and escalate to the human.
- **intent_gap** — Root cause is inside `<frozen-after-approval>`. Revert code changes. Loop back to the human to resolve. Once resolved, read fully and follow `./step-02-plan.md` to re-run steps 24.
- **bad_spec** — Root cause is outside `<frozen-after-approval>`. 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 non-frozen sections 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. Read fully and follow `./step-03-implement.md` to re-derive the code, then this step will run again.
- **patch** — Auto-fix. These are the only findings that survive loopbacks.
- **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 checks in `{spec_file}`'s `## Verification` section, if present; if verification fails and the failure cannot be fixed, HALT and escalate to the human.
- **defer** — Append one new entry to `{{.deferred_work_file}}` using this format. Do not modify existing entries or look for duplicates.
```markdown
- source_spec: `{spec_file}`
+54 -1
View File
@@ -328,9 +328,62 @@ try {
assert(!res.stderr.includes('Traceback'), `renderer crashed with a traceback instead of HALTing:\n${res.stderr}`);
});
test('missing planning_artifacts HALTs cleanly (no traceback)', () => {
// implementation_artifacts is present, so this exercises the general
// missing-vars scan rather than the dedicated guard.
const { skillDst: dst } = makeProject(
[
'[core]',
'communication_language = "French"',
'document_output_language = "Klingon"',
'implementation_artifacts = "{project-root}/impl"',
].join('\n'),
);
const res = spawnSync('python3', [path.join(dst, 'render.py')], { cwd: dst, encoding: 'utf-8' });
assert(res.status === 1, `expected exit 1, got ${res.status}\nstdout: ${res.stdout}\nstderr: ${res.stderr}`);
assert(
res.stdout.includes('HALT and report to the user: config is missing') && res.stdout.includes('`planning_artifacts`'),
`stdout missing the planning_artifacts HALT directive.\nstdout: ${res.stdout}`,
);
assert(
res.stdout.includes('step-01-clarify-and-route.md'),
`HALT directive does not name the referencing file.\nstdout: ${res.stdout}`,
);
assert(!res.stderr.includes('Traceback'), `renderer crashed with a traceback instead of HALTing:\n${res.stderr}`);
});
test('unparseable customization override HALTs cleanly (no traceback)', () => {
const { dir, skillDst: dst } = makeProject(
[
'[core]',
'communication_language = "French"',
'document_output_language = "Klingon"',
'planning_artifacts = "{project-root}/plan"',
'implementation_artifacts = "{project-root}/impl"',
].join('\n'),
);
fs.mkdirSync(path.join(dir, '_bmad', 'custom'), { recursive: true });
fs.writeFileSync(path.join(dir, '_bmad', 'custom', 'bmad-quick-dev.user.toml'), '[workflow\non_complete = broken', 'utf-8');
const res = spawnSync('python3', [path.join(dst, 'render.py')], { cwd: dst, encoding: 'utf-8' });
assert(res.status === 1, `expected exit 1, got ${res.status}\nstdout: ${res.stdout}\nstderr: ${res.stderr}`);
assert(
res.stdout.includes('HALT and report to the user: failed to parse') && res.stdout.includes('bmad-quick-dev.user.toml'),
`stdout missing the failed-to-parse HALT directive naming the override file.\nstdout: ${res.stdout}`,
);
assert(!res.stderr.includes('Traceback'), `renderer crashed with a traceback instead of HALTing:\n${res.stderr}`);
});
test('non-table [modules] does not crash the renderer', () => {
const { dir, skillDst: dst } = makeProject(
['modules = "oops-not-a-table"', '', '[core]', 'implementation_artifacts = "{project-root}/impl"'].join('\n'),
[
'modules = "oops-not-a-table"',
'',
'[core]',
'communication_language = "French"',
'document_output_language = "Klingon"',
'planning_artifacts = "{project-root}/plan"',
'implementation_artifacts = "{project-root}/impl"',
].join('\n'),
);
const res = spawnSync('python3', [path.join(dst, 'render.py')], { cwd: dst, encoding: 'utf-8' });
assert(res.status === 0, `expected exit 0, got ${res.status}\nstdout: ${res.stdout}\nstderr: ${res.stderr}`);