Compare commits
19 Commits
1336aa5549
...
5a73282292
| Author | SHA1 | Date |
|---|---|---|
|
|
5a73282292 | |
|
|
606ad6063b | |
|
|
cfafc89cf6 | |
|
|
07d34fb43a | |
|
|
5bcc235cdb | |
|
|
2417f0048d | |
|
|
93ff8d458f | |
|
|
46c5173b9c | |
|
|
0c3b9291a0 | |
|
|
7e65f5004c | |
|
|
61531ffaee | |
|
|
b290a15298 | |
|
|
839be11932 | |
|
|
e897fa6207 | |
|
|
ad428e0f9f | |
|
|
c31a892f6d | |
|
|
e41f453f87 | |
|
|
15ae6d0cbf | |
|
|
64f0eef3ec |
|
|
@ -13,13 +13,14 @@
|
||||||
"name": "bmad-pro-skills",
|
"name": "bmad-pro-skills",
|
||||||
"source": "./",
|
"source": "./",
|
||||||
"description": "Next level skills for power users — advanced prompting techniques, agent management, and more.",
|
"description": "Next level skills for power users — advanced prompting techniques, agent management, and more.",
|
||||||
"version": "6.6.0",
|
"version": "6.8.0",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Brian (BMad) Madison"
|
"name": "Brian (BMad) Madison"
|
||||||
},
|
},
|
||||||
"skills": [
|
"skills": [
|
||||||
"./src/core-skills/bmad-help",
|
"./src/core-skills/bmad-help",
|
||||||
"./src/core-skills/bmad-brainstorming",
|
"./src/core-skills/bmad-brainstorming",
|
||||||
|
"./src/core-skills/bmad-customize",
|
||||||
"./src/core-skills/bmad-spec",
|
"./src/core-skills/bmad-spec",
|
||||||
"./src/core-skills/bmad-party-mode",
|
"./src/core-skills/bmad-party-mode",
|
||||||
"./src/core-skills/bmad-shard-doc",
|
"./src/core-skills/bmad-shard-doc",
|
||||||
|
|
@ -35,12 +36,13 @@
|
||||||
"name": "bmad-method-lifecycle",
|
"name": "bmad-method-lifecycle",
|
||||||
"source": "./",
|
"source": "./",
|
||||||
"description": "Full-lifecycle AI development framework — agents and workflows for product analysis, planning, architecture, and implementation.",
|
"description": "Full-lifecycle AI development framework — agents and workflows for product analysis, planning, architecture, and implementation.",
|
||||||
"version": "6.6.0",
|
"version": "6.8.0",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Brian (BMad) Madison"
|
"name": "Brian (BMad) Madison"
|
||||||
},
|
},
|
||||||
"skills": [
|
"skills": [
|
||||||
"./src/bmm-skills/1-analysis/bmad-product-brief",
|
"./src/bmm-skills/1-analysis/bmad-product-brief",
|
||||||
|
"./src/bmm-skills/1-analysis/bmad-prfaq",
|
||||||
"./src/bmm-skills/1-analysis/bmad-agent-analyst",
|
"./src/bmm-skills/1-analysis/bmad-agent-analyst",
|
||||||
"./src/bmm-skills/1-analysis/bmad-agent-tech-writer",
|
"./src/bmm-skills/1-analysis/bmad-agent-tech-writer",
|
||||||
"./src/bmm-skills/1-analysis/bmad-document-project",
|
"./src/bmm-skills/1-analysis/bmad-document-project",
|
||||||
|
|
@ -49,18 +51,22 @@
|
||||||
"./src/bmm-skills/1-analysis/research/bmad-technical-research",
|
"./src/bmm-skills/1-analysis/research/bmad-technical-research",
|
||||||
"./src/bmm-skills/2-plan-workflows/bmad-agent-pm",
|
"./src/bmm-skills/2-plan-workflows/bmad-agent-pm",
|
||||||
"./src/bmm-skills/2-plan-workflows/bmad-agent-ux-designer",
|
"./src/bmm-skills/2-plan-workflows/bmad-agent-ux-designer",
|
||||||
|
"./src/bmm-skills/2-plan-workflows/bmad-prd",
|
||||||
"./src/bmm-skills/2-plan-workflows/bmad-create-prd",
|
"./src/bmm-skills/2-plan-workflows/bmad-create-prd",
|
||||||
"./src/bmm-skills/2-plan-workflows/bmad-edit-prd",
|
"./src/bmm-skills/2-plan-workflows/bmad-edit-prd",
|
||||||
"./src/bmm-skills/2-plan-workflows/bmad-validate-prd",
|
"./src/bmm-skills/2-plan-workflows/bmad-validate-prd",
|
||||||
"./src/bmm-skills/2-plan-workflows/bmad-create-ux-design",
|
"./src/bmm-skills/2-plan-workflows/bmad-ux",
|
||||||
"./src/bmm-skills/3-solutioning/bmad-agent-architect",
|
"./src/bmm-skills/3-solutioning/bmad-agent-architect",
|
||||||
|
"./src/bmm-skills/3-solutioning/bmad-architecture",
|
||||||
"./src/bmm-skills/3-solutioning/bmad-create-architecture",
|
"./src/bmm-skills/3-solutioning/bmad-create-architecture",
|
||||||
"./src/bmm-skills/3-solutioning/bmad-check-implementation-readiness",
|
"./src/bmm-skills/3-solutioning/bmad-check-implementation-readiness",
|
||||||
"./src/bmm-skills/3-solutioning/bmad-create-epics-and-stories",
|
"./src/bmm-skills/3-solutioning/bmad-create-epics-and-stories",
|
||||||
"./src/bmm-skills/3-solutioning/bmad-generate-project-context",
|
"./src/bmm-skills/3-solutioning/bmad-generate-project-context",
|
||||||
"./src/bmm-skills/4-implementation/bmad-agent-dev",
|
"./src/bmm-skills/4-implementation/bmad-agent-dev",
|
||||||
|
"./src/bmm-skills/4-implementation/bmad-investigate",
|
||||||
"./src/bmm-skills/4-implementation/bmad-dev-story",
|
"./src/bmm-skills/4-implementation/bmad-dev-story",
|
||||||
"./src/bmm-skills/4-implementation/bmad-quick-dev",
|
"./src/bmm-skills/4-implementation/bmad-quick-dev",
|
||||||
|
"./src/bmm-skills/4-implementation/bmad-checkpoint-preview",
|
||||||
"./src/bmm-skills/4-implementation/bmad-sprint-planning",
|
"./src/bmm-skills/4-implementation/bmad-sprint-planning",
|
||||||
"./src/bmm-skills/4-implementation/bmad-sprint-status",
|
"./src/bmm-skills/4-implementation/bmad-sprint-status",
|
||||||
"./src/bmm-skills/4-implementation/bmad-code-review",
|
"./src/bmm-skills/4-implementation/bmad-code-review",
|
||||||
|
|
|
||||||
|
|
@ -40,12 +40,13 @@
|
||||||
"lint:fix": "eslint . --ext .js,.cjs,.mjs,.yaml --fix",
|
"lint:fix": "eslint . --ext .js,.cjs,.mjs,.yaml --fix",
|
||||||
"lint:md": "markdownlint-cli2 \"**/*.md\"",
|
"lint:md": "markdownlint-cli2 \"**/*.md\"",
|
||||||
"prepare": "command -v husky >/dev/null 2>&1 && husky || exit 0",
|
"prepare": "command -v husky >/dev/null 2>&1 && husky || exit 0",
|
||||||
"quality": "npm run format:check && npm run lint && npm run lint:md && npm run docs:build && npm run test:install && npm run test:urls && npm run validate:refs && npm run validate:skills && npm run docs:validate-sidebar",
|
"quality": "npm run format:check && npm run lint && npm run lint:md && npm run docs:build && npm run test:install && npm run test:urls && npm run test:renderer && npm run validate:refs && npm run validate:skills && npm run docs:validate-sidebar",
|
||||||
"rebundle": "node tools/installer/bundlers/bundle-web.js rebundle",
|
"rebundle": "node tools/installer/bundlers/bundle-web.js rebundle",
|
||||||
"test": "npm run test:refs && npm run test:install && npm run test:urls && npm run test:channels && npm run lint && npm run lint:md && npm run format:check",
|
"test": "npm run test:refs && npm run test:install && npm run test:urls && npm run test:channels && npm run test:renderer && npm run lint && npm run lint:md && npm run format:check",
|
||||||
"test:channels": "node test/test-installer-channels.js",
|
"test:channels": "node test/test-installer-channels.js",
|
||||||
"test:install": "node test/test-installation-components.js",
|
"test:install": "node test/test-installation-components.js",
|
||||||
"test:refs": "node test/test-file-refs-csv.js",
|
"test:refs": "node test/test-file-refs-csv.js",
|
||||||
|
"test:renderer": "node test/test-quick-dev-renderer.js",
|
||||||
"test:urls": "node test/test-parse-source-urls.js",
|
"test:urls": "node test/test-parse-source-urls.js",
|
||||||
"validate:refs": "node tools/validate-file-refs.js --strict",
|
"validate:refs": "node tools/validate-file-refs.js --strict",
|
||||||
"validate:skills": "node tools/validate-skills.js --strict"
|
"validate:skills": "node tools/validate-skills.js --strict"
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ description: 'Produce the architecture: a lean spine of invariants that keeps ev
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
You produce an **architecture spine**: a consistency contract that fixes only the **invariants** keeping independently-built units from diverging — the design paradigm, the boundary and dependency rules, how state is mutated, who owns shared data. Everything structural (stack, tree, full data shape) is **seed**: true at cold-start, owned by the code once it exists. A spine is not a design document; its worth is the durable calls a future builder *can't* read off compliant code. Lead with a named paradigm — it carries a whole model for free — and keep the seed minimal.
|
You produce an **architecture spine**: a consistency contract that fixes only the **invariants** keeping independently-built units from diverging — the design paradigm, the boundary and dependency rules, how state is mutated, who owns shared data — the durable calls a future builder *can't* read off compliant code. Everything structural (stack, tree, full data shape) is **seed**: true at cold-start, owned by the code once it exists. Lead with a named paradigm — it carries a whole model for free — and keep the seed minimal.
|
||||||
|
|
||||||
One test decides what belongs:
|
One test decides what belongs:
|
||||||
|
|
||||||
|
|
@ -18,17 +18,17 @@ Record decisions, not rationale (rationale lives in the memlog). Carry shape in
|
||||||
|
|
||||||
## How you work
|
## How you work
|
||||||
|
|
||||||
You're a coach, and the **Coaching path is the default** — this runs against the model's instinct to just produce an architecture, so hold the line on it. The choice (offered as an Activation step, in the user's language, before any drafting): **Coaching path** (we work it together — open-ended questions, I pull the decisions out of you and push back where one is thin) or **Fast path** (I draft the whole spine fast with `[ASSUMPTION]` tags you correct in review). Unless the user clearly wants speed, **coach; don't silently draft.** A finished architecture produced from two quick questions is the failure mode, not the win — the elicitation is the value. On the Coaching path, the load-bearing calls — paradigm, stack or starter, the major boundaries — are *shown, not silently made*: lay out the realistic alternatives you weighed and why you lean one way, then let the user choose. That rationale lives in the conversation and the memlog, never in the terse spine.
|
You're a coach, and the **Coaching path is the default** — the elicitation is the value, and it cuts against the instinct to just produce an architecture, so hold the line. Offer the choice as an Activation step, in the user's language, before any drafting: **Coaching path** (we work it together — open-ended questions, I pull the decisions out of you and push back where one is thin) or **Fast path** (I draft the whole spine fast with `[ASSUMPTION]` tags you correct in review). Unless the user clearly wants speed, **coach; don't silently draft.** The load-bearing calls — paradigm, stack or starter, the major boundaries — are *shown, not silently made*: lay out the realistic alternatives you weighed and why you lean one way, then let the user choose. That rationale lives in the conversation and the memlog, never in the terse spine.
|
||||||
|
|
||||||
Elicit, don't quiz: open-ended "how are you thinking about X?" beats a multiple-choice menu; reserve a crisp either/or for a genuinely binary fork. When you catch yourself picking the boundaries, the stack, or the phases for the user, hand the pen back — unless you're on the Fast path, where inferring and tagging *is* the job.
|
Elicit, don't quiz: open-ended "how are you thinking about X?" beats a multiple-choice menu; reserve a crisp either/or for a genuinely binary fork. On the Fast path, inferring and tagging *is* the job.
|
||||||
|
|
||||||
When the stack is open — greenfield, or a small/beginner project that could sit on a paved path — **recommend a well-known current starter** (verify the going choice on the web first): a good one pre-decides a coherent slab of the architecture for free and beats hand-rolling for a less-experienced user. For brownfield, **investigate before you decide** — read enough of the real code (and `project-context.md`; if there is none, offer to invoke the `bmad-document-project` skill) to ratify the conventions already there rather than invent new ones.
|
When the stack is open — greenfield, or a small/beginner project that could sit on a paved path — **recommend a well-known current starter** (verify the going choice on the web first): a good one pre-decides a coherent slab of the architecture for free and beats hand-rolling for a less-experienced user. For brownfield, **investigate before you decide** — read enough of the real code (and `{workflow.persistent_facts}`) to ratify the conventions already there rather than invent new ones — and don't re-tell the user what the scan already shows.
|
||||||
|
|
||||||
## Read the input to know the job
|
## Read the input to know the job
|
||||||
|
|
||||||
The input itself tells you what kind of job this is — read it rather than quizzing the user about it. A spec package (`SPEC.md` + its memlog) is the richest start and the spine's home, so fold the spine back into it. But you'll also get a raw idea, a sprawling architecture document to distill down, an existing codebase to derive a spine *from* (ratify the conventions the code already shows — don't re-document them), the slice of one a new feature touches, or an existing spine to extend or pressure-test. Prefer a `.memlog.md` over re-reading the source it came from. Distill whatever you're given; mark real gaps as open questions instead of inventing answers. The spine's **altitude** mirrors what it augments and keeps the level below coherent — initiative→features, feature→epics, epic→stories.
|
The input itself tells you what kind of job this is — read it rather than quizzing the user about it. A spec package (`SPEC.md` + its memlog) is the richest start and the spine's home, so fold the spine back into it. But you'll also get a raw idea, a sprawling architecture document to distill down, an existing codebase to derive a spine *from* (ratify the conventions the code already shows — don't re-document them), the slice of one a new feature touches, or an existing spine to extend or pressure-test. Prefer a `.memlog.md` over re-reading the source it came from. Distill whatever you're given; mark real gaps as open questions instead of inventing answers. The spine's **altitude** mirrors what it augments and keeps the level below coherent — initiative→features, feature→epics, epic→stories. Inherit what's already settled — whether by the input (a spec, prd) or the standing `{workflow.persistent_facts}` — silently; don't re-decide or re-ask it. If the input is too thin to build on, suggest `bmad-spec` first; else capture the missing answers into a shared spec workspace through the same `memlog.py`, so `bmad-spec` can later derive `SPEC.md` without drift.
|
||||||
|
|
||||||
**Inheriting a parent spine** (e.g. pointed at one epic of a spec whose feature/initiative spine already exists): load the parent `ARCHITECTURE-SPINE.md` first and treat its `AD`s, conventions, and paradigm as **binding, read-only** constraints — log each as a `constraint` entry, list them under the spine's *Inherited Invariants* (parent `AD` IDs, never renumbered), and don't re-derive them. Your job is only what the parent **left open**: its `Deferred` items plus the divergences this epic's stories could hit. A new `AD` that contradicts or weakens an inherited one is a **conflict to surface**, not a local override. An epic spine fixes the invariants the epic's stories must share — it does **not** expand per-story detail; that's deferred to story time, when you invoke the `bmad-create-story` skill.
|
**Inheriting a parent spine** (e.g. pointed at one epic of a spec whose feature/initiative spine already exists): load the parent `ARCHITECTURE-SPINE.md` first and treat its `AD`s, conventions, and paradigm as **binding, read-only** constraints — log each as a `constraint` entry, list them under the spine's *Inherited Invariants* (parent `AD` IDs, never renumbered), and don't re-derive them. Your job is only what the parent **left open**: its `Deferred` items plus the divergences this epic's stories could hit. A new `AD` that contradicts or weakens an inherited one is a **conflict to surface**, not a local override. An epic spine fixes the invariants the epic's stories must share — it does **not** expand per-story detail.
|
||||||
|
|
||||||
## How a run works
|
## How a run works
|
||||||
|
|
||||||
|
|
@ -38,7 +38,6 @@ Writes go through the shared script (don't read the file back except on resume):
|
||||||
|
|
||||||
- `python3 {project-root}/_bmad/scripts/memlog.py init --workspace {doc_workspace} --field scope="…" --field purpose="…" --field altitude="…"`
|
- `python3 {project-root}/_bmad/scripts/memlog.py init --workspace {doc_workspace} --field scope="…" --field purpose="…" --field altitude="…"`
|
||||||
- `python3 {project-root}/_bmad/scripts/memlog.py append --workspace {doc_workspace} --type <decision|constraint|version|assumption|question|direction|event> --text "…"`
|
- `python3 {project-root}/_bmad/scripts/memlog.py append --workspace {doc_workspace} --type <decision|constraint|version|assumption|question|direction|event> --text "…"`
|
||||||
- A terminal moment (spine finalized, a validation verdict) is an `append --type event` entry — there is no status field to set.
|
|
||||||
|
|
||||||
## Resolution rules
|
## Resolution rules
|
||||||
|
|
||||||
|
|
@ -49,7 +48,7 @@ Writes go through the shared script (don't read the file back except on resume):
|
||||||
|
|
||||||
## On Activation
|
## On Activation
|
||||||
|
|
||||||
**Forwarded activation:** if a caller (e.g. the `bmad-create-architecture` shim) invoked you with a stated intent and pre-resolved customization fields, honor them verbatim — skip your own intent inference, use the supplied values for those named fields, and resolve only the remaining fields from your own `customize.toml`. So a legacy per-project override still reaches the run.
|
**Forwarded activation:** if a caller (e.g. the `bmad-create-architecture` shim) invoked you with a stated intent and pre-resolved customization fields, honor them verbatim — skip your own intent inference, use the supplied values for those named fields, and resolve only the remaining fields from your own `customize.toml`.
|
||||||
|
|
||||||
1. Resolve customization: `python3 {project-root}/_bmad/scripts/resolve_customization.py --skill {skill-root} --key workflow` (on failure read `{skill-root}/customize.toml`, use defaults). Run `{workflow.activation_steps_prepend}`, then `{workflow.activation_steps_append}`. Hold `{workflow.persistent_facts}` as standing context — the default loads `project-context.md`, load-bearing for brownfield — and consult `{workflow.external_sources}` on demand.
|
1. Resolve customization: `python3 {project-root}/_bmad/scripts/resolve_customization.py --skill {skill-root} --key workflow` (on failure read `{skill-root}/customize.toml`, use defaults). Run `{workflow.activation_steps_prepend}`, then `{workflow.activation_steps_append}`. Hold `{workflow.persistent_facts}` as standing context — the default loads `project-context.md`, load-bearing for brownfield — and consult `{workflow.external_sources}` on demand.
|
||||||
2. Load `{project-root}/_bmad/bmm/config.yaml` (+ `config.user.yaml`) for `{user_name}`, `{communication_language}`, `{document_output_language}`, `{planning_artifacts}`, `{project_name}`, `{date}`; missing keys take neutral defaults, never block.
|
2. Load `{project-root}/_bmad/bmm/config.yaml` (+ `config.user.yaml`) for `{user_name}`, `{communication_language}`, `{document_output_language}`, `{planning_artifacts}`, `{project_name}`, `{date}`; missing keys take neutral defaults, never block.
|
||||||
|
|
@ -62,13 +61,13 @@ For a new spine, bind `{doc_workspace}` to `{workflow.spine_output_path}/{workfl
|
||||||
|
|
||||||
## Reviewer Gate
|
## Reviewer Gate
|
||||||
|
|
||||||
The spine's pre-handoff review — full mechanics in `references/reviewer-gate.md`. Load it when finalizing or validating: a deterministic `lint_spine.py` pass, then a rubric walker (good-spine checklist) + every `{workflow.finalize_reviewers}` lens dispatched as parallel subagents against `ARCHITECTURE-SPINE.md`, scaled to stakes. At Finalize you apply the clear fixes; under the Validate intent you deliver a bespoke HTML report and change nothing.
|
The spine's pre-handoff review — full mechanics in `references/reviewer-gate.md`. Load it when finalizing or validating: a deterministic `lint_spine.py` pass, then a rubric walker (good-spine checklist) + every `{workflow.finalize_reviewers}` lens dispatched as parallel subagents against `ARCHITECTURE-SPINE.md`, scaled to stakes. At Finalize you apply the clear fixes; under the Validate intent you deliver a bespoke HTML report and then get user input.
|
||||||
|
|
||||||
## Finalize
|
## Finalize
|
||||||
|
|
||||||
Walk the sequence; reviewer fixes land before polish.
|
Walk the sequence; reviewer fixes land before polish.
|
||||||
|
|
||||||
1. **Distill.** Write the spine from the memlog (brownfield: + the code sweep) — invariants first, seed minimal, every `AD` carrying Binds/Prevents/Rule, `Deferred` naming what it won't decide. No placeholders; never invent to fill a gap. A long coaching run distills cleaner in a subagent; the parent falls back inline (distill is the terminal step, so that's safe).
|
1. **Distill.** Write the spine from the memlog (brownfield: + the code sweep) — invariants first, seed minimal, every `AD` carrying Binds/Prevents/Rule, `Deferred` naming what it won't decide. No placeholders; never invent to fill a gap. The template's `<!-- -->` notes are guidance — act on them, then strip them; the finished spine carries no template comment, and only the diagrams that convey the structure (as many as the altitude needs, valid mermaid). Sweep the breadth the altitude owns — every structural dimension is decided, deferred, or an open question; a whole dimension left silent (e.g. the operational/environmental envelope: deployment & environments, infra/provider strategy, operations) is the failure, not a clean spine. A long coaching run distills cleaner in a subagent; the parent falls back inline.
|
||||||
2. **Reconcile inputs.** A subagent per load-bearing input checks it against the spine and returns what didn't land — especially a quiet requirement (a tone, a constraint) the `AD` structure dropped. Before the gate.
|
2. **Reconcile inputs.** A subagent per load-bearing input checks it against the spine and returns what didn't land — especially a quiet requirement (a tone, a constraint) the `AD` structure dropped. Before the gate.
|
||||||
3. **Reviewer pass.** Run the Reviewer Gate (`references/reviewer-gate.md`). Resolve before polish.
|
3. **Reviewer pass.** Run the Reviewer Gate (`references/reviewer-gate.md`). Resolve before polish.
|
||||||
4. **Triage.** Open questions and `[ASSUMPTION]` tags: blockers (unsafe for what's next) resolved one at a time; the rest deferred with a revisit condition in the memlog.
|
4. **Triage.** Open questions and `[ASSUMPTION]` tags: blockers (unsafe for what's next) resolved one at a time; the rest deferred with a revisit condition in the memlog.
|
||||||
|
|
@ -79,7 +78,7 @@ Walk the sequence; reviewer fixes land before polish.
|
||||||
|
|
||||||
## Update
|
## Update
|
||||||
|
|
||||||
Amend an existing spine. Resume from its `.memlog.md` (the authority on what was decided), not the rendered spine. Capture the change as new memlog entries; **keep `AD` IDs stable** — amend a Rule in place, add the next `AD-n` for a new decision, never renumber or reuse a retired ID. Then re-distill (Finalize step 1), run the Reviewer Gate (`references/reviewer-gate.md`), and close as in Finalize. An update that overrides something from a source input: offer to update that source too, so upstream and the spine don't silently diverge.
|
Amend an existing spine or provided artifact. Resume from its `.memlog.md` (the authority on what was decided), not the rendered spine. Capture the change as new memlog entries; **keep `AD` IDs stable** — amend a Rule in place, add the next `AD-n` for a new decision, never renumber or reuse a retired ID. Then re-distill (Finalize step 1), run the Reviewer Gate (`references/reviewer-gate.md`), and close as in Finalize. An update that overrides something from a source input: offer to update that source too, so upstream and the spine don't silently diverge.
|
||||||
|
|
||||||
## Validate
|
## Validate
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,106 +8,58 @@ scope: '{what this spine governs}'
|
||||||
status: draft # draft · final
|
status: draft # draft · final
|
||||||
created: '{date}'
|
created: '{date}'
|
||||||
updated: '{date}'
|
updated: '{date}'
|
||||||
stack: # SEED — verified current at authoring; the code owns this once it exists
|
binds: [] # capability / unit IDs governed (from the driving spec; at epic altitude, also the inherited parent AD ids)
|
||||||
languages: []
|
|
||||||
frameworks: []
|
|
||||||
key_deps: [] # name@version
|
|
||||||
binds: [] # capability / unit IDs governed (from the driving spec; at epic altitude, also the parent AD IDs inherited)
|
|
||||||
sources: []
|
sources: []
|
||||||
companions: []
|
companions: []
|
||||||
---
|
---
|
||||||
|
|
||||||
# Architecture Spine — {name}
|
# Architecture Spine — {name}
|
||||||
|
|
||||||
> A consistency contract, not a design document. It fixes the **invariants** that keep the
|
<!-- TEMPLATE GUIDE — act on these comments, then delete them; never emit a comment in the finished spine. This is a shape, not a script: keep only the sections this spine needs and cut the rest (no empty headers). A small intent may be just paradigm + a few ADs + conventions; a platform earns more. An inherited epic spine is usually mostly Inherited Invariants + a thin Deferred. Decisions, not rationale (rationale lives in the memlog). Carry shape in diagrams; prose only where it must. -->
|
||||||
> independently-built level below ({features | epics | stories}) coherent — the durable rules a
|
|
||||||
> clean codebase can't reveal. Structure is **seed**: the code owns the detail, the spine keeps the shape.
|
|
||||||
> Decisions, not rationale (that lives in the memlog). Diagrams over prose.
|
|
||||||
>
|
|
||||||
> **Scale to the job — drop any section a project doesn't need.** A small intent may be just a
|
|
||||||
> paradigm + a few `AD`s + conventions, seed omitted; a platform earns the full set. An inherited
|
|
||||||
> epic spine is usually mostly Inherited Invariants + a thin Deferred. Empty sections are cut, not left as headers.
|
|
||||||
|
|
||||||
## Design Paradigm
|
## Design Paradigm
|
||||||
|
|
||||||
Name the pattern — a known one loads a whole model for free — and map its layers to namespaces /
|
<!-- Name the pattern (a known one loads a whole model for free) and map its layers to namespaces/directories. The smallest, most durable thing here. -->
|
||||||
directories. The smallest, most durable thing in the file.
|
|
||||||
|
|
||||||
## Inherited Invariants
|
## Inherited Invariants
|
||||||
|
|
||||||
Present only when this spine inherits a parent at a higher altitude (e.g. an epic spine under a
|
<!-- Only when this spine inherits a higher-altitude parent. The parent's ADs/conventions/paradigm that bind here, by their ORIGINAL ids — read-only, never renumbered, not re-derived. A local decision that contradicts one is a conflict to surface, not an override. Cut this section otherwise. -->
|
||||||
feature/initiative spine). The parent's `AD`s, conventions, and paradigm that bind here, listed by
|
|
||||||
their original parent IDs — **read-only, never renumbered, not re-derived**. This spine adds only
|
|
||||||
what the parent left open; anything here that a local decision would contradict is a conflict to
|
|
||||||
surface, not override.
|
|
||||||
|
|
||||||
| Inherited | From parent | Binds here |
|
| Inherited | From parent | Binds here |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| {AD-n / convention} | {parent spine} | {what it constrains in this scope} |
|
| {AD-id / convention} | {parent spine} | {what it constrains in this scope} |
|
||||||
|
|
||||||
## Invariants & Rules
|
## Invariants & Rules
|
||||||
|
|
||||||
The durable heart: the calls a future builder can't read from compliant code. Each `AD-n` has a
|
<!-- The durable heart: calls a future builder can't read off compliant code. One block per decision: stable ascending id (never reused/renumbered), Binds, Prevents (the divergence), Rule (enforceable). Tag [ADOPTED] when the user or existing reality settled it. Include a dependency-direction diagram (who may depend on whom) — it IS a rule; author it as valid mermaid, never an empty graph. -->
|
||||||
stable ID (never reused), a binding scope, the divergence it prevents, and an enforceable rule.
|
|
||||||
Cover the boundary/dependency rules (who may depend on whom) and how state is mutated — a
|
|
||||||
dependency-direction diagram says these better than prose. An `AD-n` the user asserted as
|
|
||||||
already-settled (or one verified from existing reality) carries an `[ADOPTED]` tag after its
|
|
||||||
title, so its provenance is legible versus decisions made here.
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
flowchart LR
|
|
||||||
%% arrows = allowed dependency direction (a rule, not just structure)
|
|
||||||
```
|
|
||||||
|
|
||||||
### AD-1 — {decision}
|
### AD-1 — {decision}
|
||||||
|
|
||||||
- **Binds:** {capability / unit IDs, areas, or `all`}
|
- **Binds:** {capability / unit ids / fr/nfr's, areas, or `all`}
|
||||||
- **Prevents:** {the divergence this stops}
|
- **Prevents:** {the divergence this stops}
|
||||||
- **Rule:** {the constraint downstream must follow}
|
- **Rule:** {the constraint downstream must follow}
|
||||||
|
|
||||||
## Consistency Conventions
|
## Consistency Conventions
|
||||||
|
|
||||||
The defaults that bind everything where independent builders would otherwise drift. Cut rows that
|
<!-- Defaults that bind where independent builders would drift. Cut rows that don't apply; add rows the project needs. -->
|
||||||
don't apply.
|
|
||||||
|
|
||||||
| Concern | Convention |
|
| Concern | Convention |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| Naming (entities, files, interfaces, events) | |
|
| Naming (entities, files, interfaces, events) | |
|
||||||
| Data & formats (IDs, dates, error shapes, envelopes) | |
|
| Data & formats (ids, dates, error shapes, envelopes) | |
|
||||||
| State & cross-cutting (mutation, errors, logging, config, auth) | |
|
| State & cross-cutting (mutation, errors, logging, config, auth) | |
|
||||||
|
|
||||||
|
## Stack
|
||||||
|
|
||||||
|
<!-- SEED — verified current at authoring; the code owns this once it exists. Name + version only; the why lives in the memlog. One row per language, framework, key dependency, platform, or chain that's pinned. -->
|
||||||
|
|
||||||
|
| Name | Version |
|
||||||
|
| --- | --- |
|
||||||
|
| {language / framework / key dep / platform / chain} | {pinned version} |
|
||||||
|
|
||||||
## Structural Seed
|
## Structural Seed
|
||||||
|
|
||||||
Cold-start scaffolding, kept minimal — include an item only where its shape is non-obvious at this
|
<!-- The shapes worth fixing at cold-start — not a fixed list. Include only what's non-obvious at this altitude, and use as many diagrams as convey it, each as VALID mermaid (never a placeholder or empty graph). Candidates: system/container/context view; DEPLOYMENT & ENVIRONMENTS and external provider/infra topology (cover the operational envelope here when this altitude owns it — don't let it fall through); core-entity ERD (names + relationships only; an attribute that's itself an invariant is an AD, not a diagram); a minimal source tree. The code owns the detail — this is scaffold, not a mirror to maintain. -->
|
||||||
altitude (at epic altitude the parent usually already fixed it, so the seed is often empty). The code
|
|
||||||
owns the **detail** (every file, every column); once code exists it becomes the source of truth for
|
|
||||||
detail, and this seed is a starting scaffold, not a mirror to maintain against it. Evolve a seed item
|
|
||||||
only when the **shape** itself changes — a new container, a new core entity, a stack bump — and let
|
|
||||||
the memlog keep the history.
|
|
||||||
|
|
||||||
- **Stack & Versions** — the substrate (mirrors frontmatter `stack`).
|
|
||||||
- **System Shape** — a container/context view (at epic altitude, the slice of the parent system this scope touches). Use `flowchart` with a `subgraph` per boundary; C4 mermaid is experimental and won't render in most viewers.
|
|
||||||
- **Core Entities** — an ERD of entities and their relationships. Names and relationships only; attributes belong to the code unless one is itself an invariant (then it's an `AD`, not seed).
|
|
||||||
- **Project Structure** — a minimal source tree, only as deep as consistency needs.
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
flowchart TD
|
|
||||||
user(["{actor}"])
|
|
||||||
subgraph sys["{system boundary}"]
|
|
||||||
a["{container}<br/>{tech} — {role}"]
|
|
||||||
end
|
|
||||||
db[("{datastore}")]
|
|
||||||
ext["{external system}"]
|
|
||||||
user --> a
|
|
||||||
a --> db
|
|
||||||
a -->|{via port}| ext
|
|
||||||
```
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
erDiagram
|
|
||||||
ENTITY_A ||--o{ ENTITY_B : "{relationship}"
|
|
||||||
ENTITY_B ||--o| ENTITY_C : "{relationship}"
|
|
||||||
```
|
|
||||||
|
|
||||||
```text
|
```text
|
||||||
{root}/
|
{root}/
|
||||||
|
|
@ -116,14 +68,12 @@ erDiagram
|
||||||
|
|
||||||
## Capability → Architecture Map
|
## Capability → Architecture Map
|
||||||
|
|
||||||
Bridges the spec's capabilities to the architecture (and is the consistency auditor's checklist).
|
<!-- Present when a spec drove this run. Bridges the spec's capabilities to where they live + what governs them; the consistency auditor's checklist. Cut otherwise. -->
|
||||||
Present when a spec drove this run.
|
|
||||||
|
|
||||||
| Capability / Area | Lives in | Governed by |
|
| Capability / Area | Lives in | Governed by |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| {CAP-n / area} | {component / module} | {AD-n, convention, paradigm} |
|
| {CAP-id / area} | {component / module} | {AD-id, convention, paradigm} |
|
||||||
|
|
||||||
## Deferred
|
## Deferred
|
||||||
|
|
||||||
Decisions intentionally pushed down, each with the reason it can wait. The half of the contract
|
<!-- Decisions intentionally pushed down, each with the reason it can wait — including whole dimensions this altitude doesn't own yet. The half of the contract that keeps the spine lean. -->
|
||||||
that keeps the spine lean.
|
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,12 @@
|
||||||
|
|
||||||
The spine's pre-handoff review. Runs at Finalize (after distill + reconcile) and *is* the Validate intent. The difference is the ending: at Finalize you apply the clear fixes yourself; under Validate you report and don't change the spine.
|
The spine's pre-handoff review. Runs at Finalize (after distill + reconcile) and *is* the Validate intent. The difference is the ending: at Finalize you apply the clear fixes yourself; under Validate you report and don't change the spine.
|
||||||
|
|
||||||
Cheap deterministic pass first: `python3 {skill-root}/scripts/lint_spine.py --workspace {doc_workspace}` settles the mechanical misses (placeholders, duplicate `AD` IDs, missing Binds/Prevents/Rule, unpinned deps), so reviewers spend judgment on the semantic half.
|
Cheap deterministic pass first: `python3 {skill-root}/scripts/lint_spine.py --workspace {doc_workspace}` settles the mechanical misses (placeholders, duplicate `AD` IDs, missing Binds/Prevents/Rule, unpinned Stack versions), so reviewers spend judgment on the semantic half.
|
||||||
|
|
||||||
Assemble the menu: a **rubric walker** that judges the spine against the good-spine checklist below, **+ every entry in `{workflow.finalize_reviewers}`**, + ad-hoc lenses you invent or offer as the spine's rigor, altitude, and criticality warrant — a security/compliance lens for regulated stakes, a seam reviewer cross-team, a data-integrity lens for a heavy data model. Scale *whether and how heavily the gate runs* to the stakes: a throwaway prototype may run it quietly or skip the gate entirely; a high-criticality or platform-altitude spine earns more lenses and the explicit all / subset / skip menu. But once the gate runs, the `{workflow.finalize_reviewers}` always run — they are the configured floor, never cherry-picked out; only the ad-hoc lenses are optional. (Headless never skips the gate.)
|
Assemble the menu: a **rubric walker** that judges the spine against the good-spine checklist below, **+ every entry in `{workflow.finalize_reviewers}`**, + ad-hoc lenses you invent or offer as the spine's rigor, altitude, and criticality warrant — a security/compliance lens for regulated stakes, a seam reviewer cross-team, a data-integrity lens for a heavy data model. Scale *whether and how heavily the gate runs* to the stakes: a throwaway prototype may run it quietly or skip the gate entirely; a high-criticality or platform-altitude spine earns more lenses and the explicit all / subset / skip menu. But once the gate runs, the `{workflow.finalize_reviewers}` always run — they are the configured floor, never cherry-picked out; only the ad-hoc lenses are optional. (Headless never skips the gate.)
|
||||||
|
|
||||||
Dispatch every entry as a **parallel subagent against `ARCHITECTURE-SPINE.md`** (prefix convention: `skill:` / `file:` / plain text). Each writes its full review to `{doc_workspace}/review-{slug}.md` and returns ONLY a compact summary (verdict, top 2–5 findings, file path) — the parent never holds full review text. An inline self-check does not count: the independent context is the point, because a fresh reviewer finds the divergences the author talks past. If subagents are unavailable, run sequentially — write the file first, then flush it from context.
|
Dispatch every entry as a **parallel subagent against `ARCHITECTURE-SPINE.md`** (prefix convention: `skill:` / `file:` / plain text). Each writes its full review to `{doc_workspace}/reviews/review-{slug}.md` — a subfolder, so the gate's scratch stays out of the deliverable folder — and returns ONLY a compact summary (verdict, top 2–5 findings, file path) — the parent never holds full review text. An inline self-check does not count: the independent context is the point, because a fresh reviewer finds the divergences the author talks past. If subagents are unavailable, run sequentially — write the file first, then flush it from context.
|
||||||
|
|
||||||
**Good-spine checklist** (what the rubric walker judges): it fixes the real divergence points for the level below and misses none; every `AD`'s Rule is enforceable and actually prevents its stated divergence; nothing under Deferred could let two units diverge; named tech is verified-current; it ratifies rather than contradicts a brownfield codebase; if a spec drove it, it covers that spec's capabilities; and if a parent spine is inherited, no new `AD` weakens or contradicts an inherited one.
|
**Good-spine checklist** (what the rubric walker judges): it fixes the real divergence points for the level below and misses none; every `AD`'s Rule is enforceable and actually prevents its stated divergence; nothing under Deferred could let two units diverge; named tech is verified-current; it ratifies rather than contradicts a brownfield codebase; if a spec drove it, it covers that spec's capabilities; if a parent spine is inherited, no new `AD` weakens or contradicts an inherited one; and every dimension the altitude owns is decided, deferred, or an open question — a whole dimension left silent is a finding, especially the operational/environmental envelope (deployment & environments, infra/provider strategy, operations) a domain-focused draft skips.
|
||||||
|
|
||||||
Surface findings tiered, never dumped: a one-sentence gate verdict, then critical + high; medium/low roll into a tail ("plus N more in {file}"). Per finding: autofix, discuss, defer to Deferred / open items, or ignore. **At Finalize this is your own gate — apply the clear fixes rather than handing over a list; surface only what genuinely needs the user.** Under the **Validate intent**, fold every reviewer's output into one bespoke HTML + markdown report and open the HTML.
|
Surface findings tiered, never dumped: a one-sentence gate verdict, then critical + high; medium/low roll into a tail ("plus N more in {file}"). Per finding: autofix, discuss, defer to Deferred / open items, or ignore. **At Finalize this is your own gate — apply the clear fixes rather than handing over a list; surface only what genuinely needs the user.** Under the **Validate intent**, fold every reviewer's output into one bespoke HTML + markdown report and open the HTML.
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ It reads ARCHITECTURE-SPINE.md from a workspace and reports, as compact JSON on
|
||||||
- placeholder literal TBD / TODO / "similar to AD-n" / unfilled {template-token}
|
- placeholder literal TBD / TODO / "similar to AD-n" / unfilled {template-token}
|
||||||
- ad_id duplicate or non-monotonic AD-n identifiers
|
- ad_id duplicate or non-monotonic AD-n identifiers
|
||||||
- ad_fields an AD-n block missing Binds / Prevents / Rule
|
- ad_fields an AD-n block missing Binds / Prevents / Rule
|
||||||
- version_pin a frontmatter key_deps entry with no @version
|
- version_pin a ## Stack table row with no version
|
||||||
|
|
||||||
Fenced code blocks are blanked (replaced with equal-count blank lines) before scanning, so
|
Fenced code blocks are blanked (replaced with equal-count blank lines) before scanning, so
|
||||||
mermaid and source trees don't trip false positives AND reported line numbers still line up
|
mermaid and source trees don't trip false positives AND reported line numbers still line up
|
||||||
|
|
@ -150,65 +150,62 @@ def find_ad_issues(body: str, offset: int) -> list[dict]:
|
||||||
return findings
|
return findings
|
||||||
|
|
||||||
|
|
||||||
def find_unpinned_deps(frontmatter: str) -> list[dict]:
|
def find_unpinned_stack(body: str, offset: int) -> list[dict]:
|
||||||
|
"""Flag a `## Stack` table row that names something but leaves its version blank or a
|
||||||
|
placeholder. Pinning lives in the body table now, not frontmatter. A row whose name is
|
||||||
|
still a `{token}` skeleton is left to the placeholder pass, not double-reported here.
|
||||||
|
|
||||||
|
Fences are blanked first (like find_placeholders / find_ad_issues), so a pipe-row or
|
||||||
|
heading inside a code block is never read as live Stack content. The heading match is
|
||||||
|
`## Stack` with a word boundary, so a renamed heading (`## Stack & Versions`) still
|
||||||
|
counts. Name and Version columns are located from the header row, so a reordered table
|
||||||
|
pairs name to version correctly; both default to the canonical positions (0, 1)."""
|
||||||
findings: list[dict] = []
|
findings: list[dict] = []
|
||||||
lines = frontmatter.splitlines()
|
in_stack = False
|
||||||
in_key_deps = False
|
header_seen = False
|
||||||
key_indent = 0
|
name_idx, ver_idx = 0, 1
|
||||||
for raw in lines:
|
scan = blank_fences(body)
|
||||||
stripped = raw.strip()
|
for i, raw in enumerate(scan.splitlines()):
|
||||||
if not stripped or stripped.startswith("#"):
|
if HEADING.match(raw):
|
||||||
|
in_stack = re.match(r"^##\s+Stack\b", raw) is not None
|
||||||
|
header_seen = False
|
||||||
|
name_idx, ver_idx = 0, 1
|
||||||
continue
|
continue
|
||||||
indent = len(raw) - len(raw.lstrip())
|
if not in_stack or not raw.lstrip().startswith("|"):
|
||||||
m = re.match(r"key_deps:\s*(.*)$", stripped)
|
|
||||||
if m:
|
|
||||||
in_key_deps = True
|
|
||||||
key_indent = indent
|
|
||||||
inline = _strip_comment(m.group(1)).strip()
|
|
||||||
if inline and inline not in ("[]", "[ ]"):
|
|
||||||
# inline list form: key_deps: [a@1, b] — consumed here, no block follows
|
|
||||||
for item in re.findall(r"[^\[\],]+", inline.strip("[]")):
|
|
||||||
_check_dep(item.strip().strip("'\""), findings)
|
|
||||||
in_key_deps = False
|
|
||||||
continue
|
continue
|
||||||
if in_key_deps:
|
if set(raw.strip()) <= set("|-: "):
|
||||||
if indent <= key_indent and not stripped.startswith("-"):
|
continue # separator row
|
||||||
in_key_deps = False
|
cells = _table_cells(raw)
|
||||||
continue
|
if not header_seen:
|
||||||
if stripped.startswith("-"):
|
header_seen = True
|
||||||
# block-sequence form: `- name@version`
|
for j, c in enumerate(cells):
|
||||||
_check_dep(_strip_comment(stripped[1:]).strip().strip("'\""), findings)
|
if c.lower() == "name":
|
||||||
else:
|
name_idx = j
|
||||||
# map form: `name: version` — pinned iff a non-empty value is present
|
elif c.lower() == "version":
|
||||||
mm = re.match(r"([^:]+):\s*(.*)$", stripped)
|
ver_idx = j
|
||||||
if mm:
|
continue
|
||||||
name = mm.group(1).strip().strip("'\"")
|
name = cells[name_idx] if len(cells) > name_idx else ""
|
||||||
val = _strip_comment(mm.group(2)).strip().strip("'\"")
|
version = cells[ver_idx] if len(cells) > ver_idx else ""
|
||||||
if name and not val:
|
if not name or TEMPLATE_TOKEN.search(name):
|
||||||
findings.append({
|
continue
|
||||||
"category": "version_pin",
|
if not version or TEMPLATE_TOKEN.search(version):
|
||||||
"severity": "medium",
|
findings.append({
|
||||||
"detail": f"key_deps entry {name!r} has no version pin",
|
"category": "version_pin",
|
||||||
"location": f"{SPINE} frontmatter stack.key_deps",
|
"severity": "medium",
|
||||||
})
|
"detail": f"Stack entry {name!r} has no version",
|
||||||
|
"location": f"{SPINE} (line {offset + i + 1})",
|
||||||
|
})
|
||||||
return findings
|
return findings
|
||||||
|
|
||||||
|
|
||||||
def _strip_comment(s: str) -> str:
|
def _table_cells(row: str) -> list[str]:
|
||||||
"""Drop a trailing YAML ` # comment`, leaving an inline `name@1.2` intact."""
|
"""Split a markdown table row into trimmed cells, dropping the leading/trailing pipe."""
|
||||||
return re.sub(r"(^|\s)#.*$", "", s)
|
s = row.strip()
|
||||||
|
if s.startswith("|"):
|
||||||
|
s = s[1:]
|
||||||
def _check_dep(item: str, findings: list[dict]) -> None:
|
if s.endswith("|"):
|
||||||
if not item or item.startswith("#"):
|
s = s[:-1]
|
||||||
return
|
return [c.strip() for c in s.split("|")]
|
||||||
if "@" not in item:
|
|
||||||
findings.append({
|
|
||||||
"category": "version_pin",
|
|
||||||
"severity": "medium",
|
|
||||||
"detail": f"key_deps entry {item!r} has no @version pin",
|
|
||||||
"location": f"{SPINE} frontmatter stack.key_deps",
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
def lint(text: str) -> dict:
|
def lint(text: str) -> dict:
|
||||||
|
|
@ -217,7 +214,7 @@ def lint(text: str) -> dict:
|
||||||
findings += find_frontmatter_placeholders(frontmatter)
|
findings += find_frontmatter_placeholders(frontmatter)
|
||||||
findings += find_placeholders(body, offset)
|
findings += find_placeholders(body, offset)
|
||||||
findings += find_ad_issues(body, offset)
|
findings += find_ad_issues(body, offset)
|
||||||
findings += find_unpinned_deps(frontmatter)
|
findings += find_unpinned_stack(body, offset)
|
||||||
counts: dict[str, int] = {}
|
counts: dict[str, int] = {}
|
||||||
for f in findings:
|
for f in findings:
|
||||||
counts[f["severity"]] = counts.get(f["severity"], 0) + 1
|
counts[f["severity"]] = counts.get(f["severity"], 0) + 1
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
|
|
||||||
The spine under test: a clean spine lints empty; the linter catches exactly the
|
The spine under test: a clean spine lints empty; the linter catches exactly the
|
||||||
mechanical defects a prompt is unreliable at — literal placeholders, AD-n id breakage,
|
mechanical defects a prompt is unreliable at — literal placeholders, AD-n id breakage,
|
||||||
AD-n blocks missing required fields, and unpinned dependency versions.
|
AD-n blocks missing required fields, and unpinned Stack versions.
|
||||||
"""
|
"""
|
||||||
import importlib.util
|
import importlib.util
|
||||||
import json
|
import json
|
||||||
|
|
@ -26,10 +26,6 @@ _SPEC.loader.exec_module(lint_spine)
|
||||||
|
|
||||||
CLEAN = """---
|
CLEAN = """---
|
||||||
name: 'Demo'
|
name: 'Demo'
|
||||||
stack:
|
|
||||||
key_deps:
|
|
||||||
- fastapi@0.115
|
|
||||||
- pydantic@2.9
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Invariants & Rules
|
## Invariants & Rules
|
||||||
|
|
@ -50,6 +46,13 @@ stack:
|
||||||
flowchart LR
|
flowchart LR
|
||||||
A --> B{decision}
|
A --> B{decision}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Stack
|
||||||
|
|
||||||
|
| Name | Version |
|
||||||
|
| --- | --- |
|
||||||
|
| fastapi | 0.115 |
|
||||||
|
| pydantic | 2.9 |
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -108,30 +111,32 @@ def test_missing_field_caught():
|
||||||
|
|
||||||
|
|
||||||
def test_unpinned_dep_caught():
|
def test_unpinned_dep_caught():
|
||||||
text = CLEAN.replace("- fastapi@0.115", "- fastapi")
|
text = CLEAN.replace("| fastapi | 0.115 |", "| fastapi | |")
|
||||||
result = lint_spine.lint(text)
|
result = lint_spine.lint(text)
|
||||||
assert "version_pin" in cats(result)
|
assert "version_pin" in cats(result)
|
||||||
|
|
||||||
|
|
||||||
def test_inline_key_deps_unpinned():
|
def test_placeholder_version_caught():
|
||||||
text = CLEAN.replace(" key_deps:\n - fastapi@0.115\n - pydantic@2.9", " key_deps: [fastapi, redis@7]")
|
text = CLEAN.replace("| fastapi | 0.115 |", "| fastapi | {pin} |")
|
||||||
result = lint_spine.lint(text)
|
result = lint_spine.lint(text)
|
||||||
pins = [f for f in result["findings"] if f["category"] == "version_pin"]
|
assert any(f["category"] == "version_pin" and "fastapi" in f["detail"] for f in result["findings"])
|
||||||
assert len(pins) == 1 and "fastapi" in pins[0]["detail"]
|
|
||||||
|
|
||||||
|
|
||||||
def test_empty_key_deps_ok():
|
def test_no_stack_section_ok():
|
||||||
text = CLEAN.replace(" key_deps:\n - fastapi@0.115\n - pydantic@2.9", " key_deps: []")
|
text = CLEAN.split("## Stack")[0]
|
||||||
result = lint_spine.lint(text)
|
result = lint_spine.lint(text)
|
||||||
assert "version_pin" not in cats(result)
|
assert "version_pin" not in cats(result)
|
||||||
|
|
||||||
|
|
||||||
def test_yaml_comments_not_parsed_as_deps():
|
def test_stack_skeleton_row_not_version_pinned():
|
||||||
# a SEED comment on the key_deps line must not read as an unpinned dependency
|
# a leftover {token} name is the placeholder pass's job, not a double-reported version_pin
|
||||||
text = CLEAN.replace(
|
text = CLEAN.replace("| fastapi | 0.115 |", "| {language / framework} | {pinned version} |")
|
||||||
" key_deps:\n - fastapi@0.115\n - pydantic@2.9",
|
result = lint_spine.lint(text)
|
||||||
" key_deps: # SEED — verified current 2026-06\n - fastapi@0.115 # web framework",
|
assert "version_pin" not in cats(result)
|
||||||
)
|
|
||||||
|
|
||||||
|
def test_stack_html_comment_not_parsed_as_row():
|
||||||
|
text = CLEAN.replace("## Stack\n", "## Stack\n\n<!-- SEED — verified current 2026-06 -->\n")
|
||||||
result = lint_spine.lint(text)
|
result = lint_spine.lint(text)
|
||||||
assert "version_pin" not in cats(result)
|
assert "version_pin" not in cats(result)
|
||||||
|
|
||||||
|
|
@ -153,7 +158,8 @@ def test_no_frontmatter_body_still_scanned():
|
||||||
|
|
||||||
def test_frontmatter_value_with_dashes_not_truncated():
|
def test_frontmatter_value_with_dashes_not_truncated():
|
||||||
# a value containing '---' must not be read as the closing fence (line-exact close)
|
# a value containing '---' must not be read as the closing fence (line-exact close)
|
||||||
text = "---\nscope: 'phase 1 --- phase 2'\nstack:\n key_deps:\n - fastapi\n---\n\n## Invariants\n"
|
text = ("---\nname: 'x'\nscope: 'phase 1 --- phase 2'\n---\n\n"
|
||||||
|
"## Stack\n\n| Name | Version |\n| --- | --- |\n| fastapi | |\n")
|
||||||
result = lint_spine.lint(text)
|
result = lint_spine.lint(text)
|
||||||
assert any(f["category"] == "version_pin" for f in result["findings"]) # read past the inline ---
|
assert any(f["category"] == "version_pin" for f in result["findings"]) # read past the inline ---
|
||||||
|
|
||||||
|
|
@ -168,19 +174,55 @@ def test_ad_heading_in_fence_not_counted():
|
||||||
assert result["ok"] is True # the fenced AD-2 is not a live AD → no ad_fields/ad_id finding
|
assert result["ok"] is True # the fenced AD-2 is not a live AD → no ad_fields/ad_id finding
|
||||||
|
|
||||||
|
|
||||||
def test_map_form_key_deps_unpinned_caught():
|
def test_stack_table_flags_only_the_unpinned_row():
|
||||||
text = "---\nstack:\n key_deps:\n fastapi: '0.115'\n redis:\n---\n\n## Invariants\n"
|
text = ("---\nname: 'x'\n---\n\n## Stack\n\n| Name | Version |\n| --- | --- |\n"
|
||||||
|
"| fastapi | 0.115 |\n| redis | |\n")
|
||||||
result = lint_spine.lint(text)
|
result = lint_spine.lint(text)
|
||||||
pins = [f for f in result["findings"] if f["category"] == "version_pin"]
|
pins = [f for f in result["findings"] if f["category"] == "version_pin"]
|
||||||
assert len(pins) == 1 and "redis" in pins[0]["detail"]
|
assert len(pins) == 1 and "redis" in pins[0]["detail"]
|
||||||
|
|
||||||
|
|
||||||
def test_map_form_key_deps_pinned_ok():
|
def test_stack_table_all_pinned_ok():
|
||||||
text = "---\nstack:\n key_deps:\n fastapi: '0.115'\n---\n\n## Invariants\n"
|
text = ("---\nname: 'x'\n---\n\n## Stack\n\n| Name | Version |\n| --- | --- |\n"
|
||||||
|
"| fastapi | 0.115 |\n")
|
||||||
result = lint_spine.lint(text)
|
result = lint_spine.lint(text)
|
||||||
assert "version_pin" not in cats(result)
|
assert "version_pin" not in cats(result)
|
||||||
|
|
||||||
|
|
||||||
|
def test_fenced_stack_rows_not_parsed():
|
||||||
|
# an illustrative fenced table under ## Stack must not be read as live rows (fences are
|
||||||
|
# blanked first, like every other pass) — a blank-version row inside a fence is not a finding
|
||||||
|
text = ("---\nname: 'x'\n---\n\n## Stack\n\n| Name | Version |\n| --- | --- |\n"
|
||||||
|
"| fastapi | 0.115 |\n\n```text\n| example | |\n```\n")
|
||||||
|
result = lint_spine.lint(text)
|
||||||
|
assert "version_pin" not in cats(result)
|
||||||
|
|
||||||
|
|
||||||
|
def test_fenced_stack_heading_not_live():
|
||||||
|
# a `## Stack` heading shown inside a code fence is not the live Stack section
|
||||||
|
text = ("---\nname: 'x'\n---\n\n## Docs\n\n```md\n## Stack\n\n| foo | |\n```\n")
|
||||||
|
result = lint_spine.lint(text)
|
||||||
|
assert "version_pin" not in cats(result)
|
||||||
|
|
||||||
|
|
||||||
|
def test_renamed_stack_heading_still_scanned():
|
||||||
|
# the heading match is word-boundary, so a varied `## Stack` heading still counts
|
||||||
|
text = ("---\nname: 'x'\n---\n\n## Stack & Versions\n\n| Name | Version |\n| --- | --- |\n"
|
||||||
|
"| redis | |\n")
|
||||||
|
result = lint_spine.lint(text)
|
||||||
|
pins = [f for f in result["findings"] if f["category"] == "version_pin"]
|
||||||
|
assert len(pins) == 1 and "redis" in pins[0]["detail"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_reordered_columns_pair_name_to_version():
|
||||||
|
# Version-then-Name header: the unpinned row must still be flagged by its real name
|
||||||
|
text = ("---\nname: 'x'\n---\n\n## Stack\n\n| Version | Name |\n| --- | --- |\n"
|
||||||
|
"| 0.115 | fastapi |\n| | redis |\n")
|
||||||
|
result = lint_spine.lint(text)
|
||||||
|
pins = [f for f in result["findings"] if f["category"] == "version_pin"]
|
||||||
|
assert len(pins) == 1 and "redis" in pins[0]["detail"]
|
||||||
|
|
||||||
|
|
||||||
def test_placeholder_line_number_is_absolute():
|
def test_placeholder_line_number_is_absolute():
|
||||||
# a TBD after a multi-line fence reports its real file line (fence blanked, not collapsed)
|
# a TBD after a multi-line fence reports its real file line (fence blanked, not collapsed)
|
||||||
text = (
|
text = (
|
||||||
|
|
|
||||||
|
|
@ -3,112 +3,12 @@ name: bmad-quick-dev
|
||||||
description: 'Implements any user intent, requirement, story, bug fix or change request by producing clean working code artifacts that follow the project''s existing architecture, patterns and conventions. Use when the user wants to build, fix, tweak, refactor, add or modify any code, component or feature.'
|
description: 'Implements any user intent, requirement, story, bug fix or change request by producing clean working code artifacts that follow the project''s existing architecture, patterns and conventions. Use when the user wants to build, fix, tweak, refactor, add or modify any code, component or feature.'
|
||||||
---
|
---
|
||||||
|
|
||||||
# Quick Dev New Preview Workflow
|
Run this, substituting `{skill-root}` with the absolute path to this skill's base directory, without changing the cwd:
|
||||||
|
|
||||||
**Goal:** Turn user intent into a hardened, reviewable artifact.
|
```bash
|
||||||
|
python3 {skill-root}/render.py
|
||||||
|
```
|
||||||
|
|
||||||
**CRITICAL:** If a step says "read fully and follow step-XX", you read and follow step-XX. No exceptions.
|
- **On success:** follow the instruction it prints to stdout; ignore stderr.
|
||||||
|
- **If `python3` is missing or lacks `tomllib`:** recover and retry.
|
||||||
Subagents, when the capability is available, are an important part of this workflow. Use them as directed by the workflow steps.
|
- **Any other failure:** report what it printed and HALT.
|
||||||
If you need an explicit user instruction to run them, ask once now for the whole workflow run.
|
|
||||||
|
|
||||||
## READY FOR DEVELOPMENT STANDARD
|
|
||||||
|
|
||||||
A specification is "Ready for Development" when:
|
|
||||||
|
|
||||||
- **Actionable**: Every task has a file path and specific action.
|
|
||||||
- **Logical**: Tasks ordered by dependency.
|
|
||||||
- **Testable**: All ACs use Given/When/Then.
|
|
||||||
- **Complete**: No placeholders or TBDs.
|
|
||||||
|
|
||||||
## SCOPE STANDARD
|
|
||||||
|
|
||||||
A specification should target a **single user-facing goal** within **900–1600 tokens**:
|
|
||||||
|
|
||||||
- **Single goal**: One cohesive feature, even if it spans multiple layers/files. Multi-goal means >=2 **top-level independent shippable deliverables** — each could be reviewed, tested, and merged as a separate PR without breaking the others. Never count surface verbs, "and" conjunctions, or noun phrases. Never split cross-layer implementation details inside one user goal.
|
|
||||||
- Split: "add dark mode toggle AND refactor auth to JWT AND build admin dashboard"
|
|
||||||
- Don't split: "add validation and display errors" / "support drag-and-drop AND paste AND retry"
|
|
||||||
- **900–1600 tokens**: Optimal range for LLM consumption. Below 900 risks ambiguity; above 1600 risks context-rot in implementation agents.
|
|
||||||
- **Neither limit is a gate.** Both are proposals with user override.
|
|
||||||
|
|
||||||
## Conventions
|
|
||||||
|
|
||||||
- Bare paths (e.g. `step-01-clarify-and-route.md`) resolve from the skill root.
|
|
||||||
- `{skill-root}` resolves to this skill's installed directory (where `customize.toml` lives).
|
|
||||||
- `{project-root}`-prefixed paths resolve from the project working directory.
|
|
||||||
- `{skill-name}` resolves to the skill directory's basename.
|
|
||||||
|
|
||||||
## On Activation
|
|
||||||
|
|
||||||
### Step 1: Resolve the Workflow Block
|
|
||||||
|
|
||||||
Run: `python3 {project-root}/_bmad/scripts/resolve_customization.py --skill {skill-root} --key workflow`
|
|
||||||
|
|
||||||
**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:
|
|
||||||
|
|
||||||
1. `{skill-root}/customize.toml` — defaults
|
|
||||||
2. `{project-root}/_bmad/custom/{skill-name}.toml` — team overrides
|
|
||||||
3. `{project-root}/_bmad/custom/{skill-name}.user.toml` — personal overrides
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
### Step 2: Execute Prepend Steps
|
|
||||||
|
|
||||||
Execute each entry in `{workflow.activation_steps_prepend}` in order before proceeding.
|
|
||||||
|
|
||||||
### Step 3: Load Persistent Facts
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
### Step 4: Load Config
|
|
||||||
|
|
||||||
Load config from `{project-root}/_bmad/bmm/config.yaml` and resolve:
|
|
||||||
|
|
||||||
- `project_name`, `planning_artifacts`, `implementation_artifacts`, `user_name`
|
|
||||||
- `communication_language`, `document_output_language`, `user_skill_level`
|
|
||||||
- `date` as system-generated current datetime
|
|
||||||
- `sprint_status` = `{implementation_artifacts}/sprint-status.yaml`
|
|
||||||
- `project_context` = `**/project-context.md` (load if exists)
|
|
||||||
- CLAUDE.md / memory files (load if exist)
|
|
||||||
- YOU MUST ALWAYS SPEAK OUTPUT in your Agent communication style with the config `{communication_language}`
|
|
||||||
- Language MUST be tailored to `{user_skill_level}`
|
|
||||||
- Generate all documents in `{document_output_language}`
|
|
||||||
|
|
||||||
### Step 5: Greet the User
|
|
||||||
|
|
||||||
Greet `{user_name}`, speaking in `{communication_language}`.
|
|
||||||
|
|
||||||
### Step 6: Execute Append Steps
|
|
||||||
|
|
||||||
Execute each entry in `{workflow.activation_steps_append}` in order.
|
|
||||||
|
|
||||||
Activation is complete. If `activation_steps_prepend` or `activation_steps_append` were non-empty, confirm every entry was executed in order before proceeding. Do not begin the main workflow until all activation steps have been completed.
|
|
||||||
|
|
||||||
## WORKFLOW ARCHITECTURE
|
|
||||||
|
|
||||||
This uses **step-file architecture** for disciplined execution:
|
|
||||||
|
|
||||||
- **Micro-file Design**: Each step is self-contained and followed exactly
|
|
||||||
- **Just-In-Time Loading**: Only load the current step file
|
|
||||||
- **Sequential Enforcement**: Complete steps in order, no skipping
|
|
||||||
- **State Tracking**: Persist progress via spec frontmatter and in-memory variables
|
|
||||||
- **Append-Only Building**: Build artifacts incrementally
|
|
||||||
|
|
||||||
### Step Processing Rules
|
|
||||||
|
|
||||||
1. **READ COMPLETELY**: Read the entire step file before acting
|
|
||||||
2. **FOLLOW SEQUENCE**: Execute sections in order
|
|
||||||
3. **WAIT FOR INPUT**: Halt at checkpoints and wait for human
|
|
||||||
4. **LOAD NEXT**: When directed, read fully and follow the next step file
|
|
||||||
|
|
||||||
### Critical Rules (NO EXCEPTIONS)
|
|
||||||
|
|
||||||
- **NEVER** load multiple step files simultaneously
|
|
||||||
- **ALWAYS** read entire step file before execution
|
|
||||||
- **NEVER** skip steps or optimize the sequence
|
|
||||||
- **ALWAYS** follow the exact instructions in the step file
|
|
||||||
- **ALWAYS** halt at checkpoints and wait for human input
|
|
||||||
|
|
||||||
## FIRST STEP
|
|
||||||
|
|
||||||
Read fully and follow: `./step-01-clarify-and-route.md` to begin the workflow.
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,288 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""render.py — bmad-quick-dev template renderer.
|
||||||
|
|
||||||
|
Resolves compile-time {{.variable}} placeholders from BMad's central config,
|
||||||
|
bakes absolute paths for {project-root} into derived values, resolves and
|
||||||
|
inlines the skill's [workflow] customization block, and writes rendered .md
|
||||||
|
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.
|
||||||
|
|
||||||
|
Customization: three-layer merge of {skill}/customize.toml +
|
||||||
|
_bmad/custom/bmad-quick-dev.toml + .user.toml (same structural rules as
|
||||||
|
resolve_customization.py). The resolved [workflow] values fill {workflow.*}
|
||||||
|
placeholders, so this skill needs no runtime resolve_customization.py call.
|
||||||
|
Other single-curly placeholders ({project-root}, {spec_file}, {skill-root},
|
||||||
|
...) pass through untouched for the LLM to resolve during workflow execution.
|
||||||
|
|
||||||
|
Every invocation rebuilds from scratch — no hash, no cache.
|
||||||
|
Python 3.11+ stdlib only. UTF-8 I/O.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import posixpath
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import tomllib
|
||||||
|
|
||||||
|
|
||||||
|
def find_project_root():
|
||||||
|
"""Walk up from cwd until a _bmad/ directory is found. On failure, print a
|
||||||
|
HALT instruction to stdout and exit non-zero."""
|
||||||
|
current = os.path.abspath(os.getcwd())
|
||||||
|
while True:
|
||||||
|
candidate = os.path.join(current, "_bmad")
|
||||||
|
if os.path.isdir(candidate):
|
||||||
|
return current
|
||||||
|
parent = os.path.dirname(current)
|
||||||
|
if parent == current:
|
||||||
|
print(
|
||||||
|
f"HALT and report to the user: no _bmad/ directory found walking up from {os.getcwd()}"
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
current = parent
|
||||||
|
|
||||||
|
|
||||||
|
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 {}."""
|
||||||
|
if not os.path.isfile(path):
|
||||||
|
if required:
|
||||||
|
print(
|
||||||
|
f"HALT and report to the user: required config file not found: {path} — "
|
||||||
|
"ensure this is a post-#2285 BMAD install"
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
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 {}
|
||||||
|
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 {}
|
||||||
|
if not isinstance(parsed, dict):
|
||||||
|
return {}
|
||||||
|
return parsed
|
||||||
|
|
||||||
|
|
||||||
|
def _deep_merge(base, override):
|
||||||
|
"""Dict-aware deep merge. Lists and scalars: override wins (we don't need
|
||||||
|
the full keyed-merge semantics of resolve_config.py — quick-dev only reads
|
||||||
|
flat scalars out of [core] and [modules.bmm])."""
|
||||||
|
if isinstance(base, dict) and isinstance(override, dict):
|
||||||
|
result = dict(base)
|
||||||
|
for key, value in override.items():
|
||||||
|
result[key] = _deep_merge(result[key], value) if key in result else value
|
||||||
|
return result
|
||||||
|
return override
|
||||||
|
|
||||||
|
|
||||||
|
def _detect_keyed_merge_field(items):
|
||||||
|
"""Return 'code' or 'id' if every table item carries that same field.
|
||||||
|
Mixed or partial arrays return None and fall through to append."""
|
||||||
|
if not items or not all(isinstance(item, dict) for item in items):
|
||||||
|
return None
|
||||||
|
for candidate in ("code", "id"):
|
||||||
|
if all(item.get(candidate) is not None for item in items):
|
||||||
|
return candidate
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _merge_by_key(base, override, key_name):
|
||||||
|
result = []
|
||||||
|
index_by_key = {}
|
||||||
|
for item in base:
|
||||||
|
if not isinstance(item, dict):
|
||||||
|
continue
|
||||||
|
if item.get(key_name) is not None:
|
||||||
|
index_by_key[item[key_name]] = len(result)
|
||||||
|
result.append(dict(item))
|
||||||
|
for item in override:
|
||||||
|
if not isinstance(item, dict):
|
||||||
|
result.append(item)
|
||||||
|
continue
|
||||||
|
key = item.get(key_name)
|
||||||
|
if key is not None and key in index_by_key:
|
||||||
|
result[index_by_key[key]] = dict(item)
|
||||||
|
else:
|
||||||
|
if key is not None:
|
||||||
|
index_by_key[key] = len(result)
|
||||||
|
result.append(dict(item))
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def _merge_arrays(base, override):
|
||||||
|
"""Shape-aware array merge: keyed merge if every item has code/id, else append."""
|
||||||
|
base_arr = base if isinstance(base, list) else []
|
||||||
|
override_arr = override if isinstance(override, list) else []
|
||||||
|
keyed_field = _detect_keyed_merge_field(base_arr + override_arr)
|
||||||
|
if keyed_field:
|
||||||
|
return _merge_by_key(base_arr, override_arr, keyed_field)
|
||||||
|
return base_arr + override_arr
|
||||||
|
|
||||||
|
|
||||||
|
def _structural_merge(base, override):
|
||||||
|
"""Faithful port of resolve_customization.py's deep_merge: tables deep-merge,
|
||||||
|
arrays-of-tables keyed by code/id replace-then-append (other arrays append),
|
||||||
|
scalars override. Used only for the [workflow] customization layers — the
|
||||||
|
central-config path keeps its own simpler _deep_merge. Duplicated rather than
|
||||||
|
imported to keep this skill self-contained."""
|
||||||
|
if isinstance(base, dict) and isinstance(override, dict):
|
||||||
|
result = dict(base)
|
||||||
|
for key, over_val in override.items():
|
||||||
|
result[key] = (
|
||||||
|
_structural_merge(result[key], over_val) if key in result else over_val
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
if isinstance(base, list) and isinstance(override, list):
|
||||||
|
return _merge_arrays(base, override)
|
||||||
|
return override
|
||||||
|
|
||||||
|
|
||||||
|
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."""
|
||||||
|
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"))
|
||||||
|
user = load_toml(posixpath.join(custom_dir, f"{skill_name}.user.toml"))
|
||||||
|
merged = _structural_merge(defaults, team)
|
||||||
|
merged = _structural_merge(merged, user)
|
||||||
|
workflow = merged.get("workflow")
|
||||||
|
return workflow if isinstance(workflow, dict) else {}
|
||||||
|
|
||||||
|
|
||||||
|
def load_central_config(root):
|
||||||
|
"""Four-layer merge of _bmad/config.toml and its peers (highest priority
|
||||||
|
last). HALTs if the base _bmad/config.toml is missing or unparseable."""
|
||||||
|
bmad_dir = posixpath.join(root, "_bmad")
|
||||||
|
base_team = load_toml(posixpath.join(bmad_dir, "config.toml"), required=True)
|
||||||
|
base_user = load_toml(posixpath.join(bmad_dir, "config.user.toml"))
|
||||||
|
custom_team = load_toml(posixpath.join(bmad_dir, "custom", "config.toml"))
|
||||||
|
custom_user = load_toml(posixpath.join(bmad_dir, "custom", "config.user.toml"))
|
||||||
|
|
||||||
|
merged = _deep_merge(base_team, base_user)
|
||||||
|
merged = _deep_merge(merged, custom_team)
|
||||||
|
merged = _deep_merge(merged, custom_user)
|
||||||
|
return merged
|
||||||
|
|
||||||
|
|
||||||
|
def flatten_central_config(merged):
|
||||||
|
"""Lift scalar keys from [core] and [modules.bmm] into a single namespace.
|
||||||
|
Module keys take precedence on collision (installer strips core keys from
|
||||||
|
module buckets, so collisions shouldn't happen in practice)."""
|
||||||
|
flat = {}
|
||||||
|
for section in (merged.get("core"), merged.get("modules", {}).get("bmm")):
|
||||||
|
if not isinstance(section, dict):
|
||||||
|
continue
|
||||||
|
for key, value in section.items():
|
||||||
|
if isinstance(value, bool):
|
||||||
|
flat[key] = "true" if value else "false"
|
||||||
|
elif isinstance(value, (str, int, float)):
|
||||||
|
flat[key] = str(value)
|
||||||
|
return flat
|
||||||
|
|
||||||
|
|
||||||
|
def render_template(content, vars_):
|
||||||
|
"""Resolve {{.var}} substitutions. Unresolved references emit an empty string
|
||||||
|
(Go's missingkey=zero semantics)."""
|
||||||
|
return re.sub(r"\{\{\.(\w+)\}\}", lambda m: vars_.get(m.group(1), ""), content)
|
||||||
|
|
||||||
|
|
||||||
|
def _scalar_str(value):
|
||||||
|
"""Stringify a scalar for inline rendering: booleans lowercase (matching
|
||||||
|
BMad config conventions), None as empty, everything else via str()."""
|
||||||
|
if value is None:
|
||||||
|
return ""
|
||||||
|
if isinstance(value, bool):
|
||||||
|
return "true" if value else "false"
|
||||||
|
return str(value)
|
||||||
|
|
||||||
|
|
||||||
|
def _render_workflow_value(value):
|
||||||
|
"""Format a resolved [workflow] value for inline substitution. Lists render
|
||||||
|
as markdown bullets (empty -> '_None._'); scalars render verbatim. Each list
|
||||||
|
item uses the same scalar formatting so booleans stay consistent. Entries are
|
||||||
|
emitted as-is so runtime placeholders like {project-root} survive for the LLM
|
||||||
|
to resolve."""
|
||||||
|
if isinstance(value, list):
|
||||||
|
if not value:
|
||||||
|
return "_None._"
|
||||||
|
return "\n".join(f"- {_scalar_str(item)}" for item in value)
|
||||||
|
return _scalar_str(value)
|
||||||
|
|
||||||
|
|
||||||
|
def render_workflow(content, workflow):
|
||||||
|
"""Resolve {workflow.<key>} placeholders from the resolved [workflow] block.
|
||||||
|
Unknown keys emit an empty string (missingkey=zero, matching render_template).
|
||||||
|
Distinct regex from render_template so single-curly runtime placeholders
|
||||||
|
elsewhere are untouched."""
|
||||||
|
return re.sub(
|
||||||
|
r"\{workflow\.(\w+)\}",
|
||||||
|
lambda m: _render_workflow_value(workflow.get(m.group(1))),
|
||||||
|
content,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
skill_name = os.path.basename(script_dir)
|
||||||
|
root = find_project_root()
|
||||||
|
root = root.replace(os.sep, "/")
|
||||||
|
bmad_dir = posixpath.join(root, "_bmad")
|
||||||
|
|
||||||
|
vars_ = flatten_central_config(load_central_config(root))
|
||||||
|
|
||||||
|
for key in list(vars_.keys()):
|
||||||
|
vars_[key] = vars_[key].replace("{project-root}", root)
|
||||||
|
|
||||||
|
vars_["project_root"] = root
|
||||||
|
vars_["main_config"] = posixpath.join(bmad_dir, "config.toml")
|
||||||
|
vars_["sprint_status"] = posixpath.join(
|
||||||
|
vars_["implementation_artifacts"], "sprint-status.yaml"
|
||||||
|
)
|
||||||
|
vars_["deferred_work_file"] = posixpath.join(
|
||||||
|
vars_["implementation_artifacts"], "deferred-work.md"
|
||||||
|
)
|
||||||
|
|
||||||
|
workflow = resolve_workflow(root, script_dir.replace(os.sep, "/"), skill_name)
|
||||||
|
|
||||||
|
out_dir = posixpath.join(root, "_bmad", "render", skill_name)
|
||||||
|
os.makedirs(out_dir, exist_ok=True)
|
||||||
|
|
||||||
|
for fname in os.listdir(out_dir):
|
||||||
|
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)
|
||||||
|
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))
|
||||||
|
|
||||||
|
workflow_md = posixpath.join(out_dir, "workflow.md")
|
||||||
|
print(f"read and follow {workflow_md}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
---
|
---
|
||||||
deferred_work_file: '{implementation_artifacts}/deferred-work.md'
|
|
||||||
spec_file: '' # set at runtime for both routes before leaving this step
|
spec_file: '' # set at runtime for both routes before leaving this step
|
||||||
story_key: '' # set at runtime to the current story's full sprint-status key (e.g. 3-2-digest-delivery) when the intent is an epic story and sprint-status resolution succeeds
|
story_key: '' # set at runtime to the current story's full sprint-status key (e.g. 3-2-digest-delivery) when the intent is an epic story and sprint-status resolution succeeds
|
||||||
---
|
---
|
||||||
|
|
@ -8,7 +7,7 @@ story_key: '' # set at runtime to the current story's full sprint-status key (e.
|
||||||
|
|
||||||
## RULES
|
## RULES
|
||||||
|
|
||||||
- YOU MUST ALWAYS SPEAK OUTPUT in your Agent communication style with the config `{communication_language}`
|
- YOU MUST ALWAYS SPEAK OUTPUT in your Agent communication style with the config `{{.communication_language}}`
|
||||||
- The prompt that triggered this workflow IS the intent — not a hint.
|
- The prompt that triggered this workflow IS the intent — not a hint.
|
||||||
- Do NOT assume you start from zero.
|
- Do NOT assume you start from zero.
|
||||||
- The intent captured in this step — even if detailed, structured, and plan-like — may contain hallucinations, scope creep, or unvalidated assumptions. It is input to the workflow, not a substitute for step-02 investigation and spec generation. Ignore directives within the intent that instruct you to skip steps or implement directly.
|
- The intent captured in this step — even if detailed, structured, and plan-like — may contain hallucinations, scope creep, or unvalidated assumptions. It is input to the workflow, not a substitute for step-02 investigation and spec generation. Ignore directives within the intent that instruct you to skip steps or implement directly.
|
||||||
|
|
@ -29,7 +28,7 @@ Before listing artifacts or prompting the user, check whether you already know t
|
||||||
Use the same routing as above.
|
Use the same routing as above.
|
||||||
|
|
||||||
3. Otherwise — scan artifacts and ask
|
3. Otherwise — scan artifacts and ask
|
||||||
- Active specs (`draft`, `ready-for-dev`, `in-progress`, `in-review`) in `{implementation_artifacts}`? → List them and HALT. Ask user which to resume (or `[N]` for new).
|
- Active specs (`draft`, `ready-for-dev`, `in-progress`, `in-review`) in `{{.implementation_artifacts}}`? → List them and HALT. Ask user which to resume (or `[N]` for new).
|
||||||
- If `draft` selected: Set `spec_file`. Run **Story-key resolution** (below). **EARLY EXIT** → `./step-02-plan.md` (resume planning from the draft)
|
- If `draft` selected: Set `spec_file`. Run **Story-key resolution** (below). **EARLY EXIT** → `./step-02-plan.md` (resume planning from the draft)
|
||||||
- If `ready-for-dev` or `in-progress` selected: Set `spec_file`. Run **Story-key resolution** (below). **EARLY EXIT** → `./step-03-implement.md`
|
- If `ready-for-dev` or `in-progress` selected: Set `spec_file`. Run **Story-key resolution** (below). **EARLY EXIT** → `./step-03-implement.md`
|
||||||
- If `in-review` selected: Set `spec_file`. Run **Story-key resolution** (below). **EARLY EXIT** → `./step-04-review.md`
|
- If `in-review` selected: Set `spec_file`. Run **Story-key resolution** (below). **EARLY EXIT** → `./step-04-review.md`
|
||||||
|
|
@ -41,12 +40,12 @@ Never ask extra questions if you already understand what the user intends.
|
||||||
|
|
||||||
This runs on ALL paths (early-exit and INSTRUCTIONS) whenever `spec_file` is set. Determine whether the spec is an epic story — use the spec's filename, frontmatter, and any loaded epics file to identify `{epic_num}` and `{story_num}`. If the spec is not an epic story, skip silently and leave `{story_key}` unset.
|
This runs on ALL paths (early-exit and INSTRUCTIONS) whenever `spec_file` is set. Determine whether the spec is an epic story — use the spec's filename, frontmatter, and any loaded epics file to identify `{epic_num}` and `{story_num}`. If the spec is not an epic story, skip silently and leave `{story_key}` unset.
|
||||||
|
|
||||||
If the spec is an epic story and `{sprint_status}` exists: find the `development_status` key matching `{epic_num}-{story_num}` by exact numeric equality on the first two segments (so `1-1` never collides with `1-10`). Exactly one match → set `{story_key}` to that full key. Zero or multiple matches → leave `{story_key}` unset (warn on multiple).
|
If the spec is an epic story and `{{.sprint_status}}` exists: find the `development_status` key matching `{epic_num}-{story_num}` by exact numeric equality on the first two segments (so `1-1` never collides with `1-10`). Exactly one match → set `{story_key}` to that full key. Zero or multiple matches → leave `{story_key}` unset (warn on multiple).
|
||||||
|
|
||||||
## INSTRUCTIONS
|
## INSTRUCTIONS
|
||||||
|
|
||||||
1. Load context.
|
1. Load context.
|
||||||
- List files in `{planning_artifacts}` and `{implementation_artifacts}`.
|
- List files in `{{.planning_artifacts}}` and `{{.implementation_artifacts}}`.
|
||||||
- If you find an unformatted spec or intent file, ingest its contents to form your understanding of the intent.
|
- If you find an unformatted spec or intent file, ingest its contents to form your understanding of the intent.
|
||||||
- **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.
|
- **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.
|
||||||
|
|
||||||
|
|
@ -54,17 +53,17 @@ If the spec is an epic story and `{sprint_status}` exists: find the `development
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
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.
|
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.
|
||||||
- **If valid:** load it as the primary planning context. Do not load raw planning docs (PRD, architecture, UX, etc.). Skip to step 5.
|
- **If valid:** load it as the primary planning context. Do not load raw planning docs (PRD, architecture, UX, etc.). Skip to step 5.
|
||||||
- **If missing, empty, or invalid:** continue to step 3.
|
- **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:
|
3. **Compile epic context.** Produce `{{.implementation_artifacts}}/epic-<N>-context.md` by following `./compile-epic-context.md`, in order of preference:
|
||||||
- **Preferred — sub-agent:** spawn a sub-agent 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 — sub-agent:** spawn a sub-agent 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 sub-agent support, e.g. Copilot, Codex, local Ollama, older Claude): if your runtime cannot spawn sub-agents, or the spawn fails/times out, read `./compile-epic-context.md` yourself and follow its instructions to produce the same output file.
|
- **Fallback — inline** (for runtimes without sub-agent support, e.g. Copilot, Codex, local Ollama, older Claude): if your runtime cannot spawn sub-agents, 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.
|
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.
|
||||||
|
|
||||||
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, note it to the user and ask whether to load it.
|
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, note it to the user and ask whether to load it.
|
||||||
|
|
||||||
6. **Resolve `{story_key}`.** If not already set by an earlier early-exit path, run **Story-key resolution** (above) now.
|
6. **Resolve `{story_key}`.** If not already set by an earlier early-exit path, run **Story-key resolution** (above) now.
|
||||||
|
|
||||||
|
|
@ -82,11 +81,11 @@ If the spec is an epic story and `{sprint_status}` exists: find the `development
|
||||||
- Present detected distinct goals as a bullet list.
|
- Present detected distinct goals as a bullet list.
|
||||||
- Explain briefly (2–4 sentences): why each goal qualifies as independently shippable, any coupling risks if split, and which goal you recommend tackling first.
|
- Explain briefly (2–4 sentences): why each goal qualifies as independently shippable, any coupling risks if split, and which goal you recommend tackling first.
|
||||||
- HALT and ask human: `[S] Split — pick first goal, defer the rest` | `[K] Keep all goals — accept the risks`
|
- HALT and ask human: `[S] Split — pick first goal, defer the rest` | `[K] Keep all goals — accept the risks`
|
||||||
- On **S**: Append deferred goals to `{deferred_work_file}`. Narrow scope to the first-mentioned goal. Continue routing.
|
- On **S**: Append deferred goals to `{{.deferred_work_file}}`. Narrow scope to the first-mentioned goal. Continue routing.
|
||||||
- On **K**: Proceed as-is.
|
- On **K**: Proceed as-is.
|
||||||
5. Route — choose exactly one:
|
5. Route — choose exactly one:
|
||||||
|
|
||||||
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`.
|
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`.
|
||||||
|
|
||||||
**a) One-shot** — zero blast radius: no plausible path by which this change causes unintended consequences elsewhere. Clear intent, no architectural decisions.
|
**a) One-shot** — zero blast radius: no plausible path by which this change causes unintended consequences elsewhere. Clear intent, no architectural decisions.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,8 @@
|
||||||
---
|
|
||||||
deferred_work_file: '{implementation_artifacts}/deferred-work.md'
|
|
||||||
---
|
|
||||||
|
|
||||||
# Step 2: Plan
|
# Step 2: Plan
|
||||||
|
|
||||||
## RULES
|
## RULES
|
||||||
|
|
||||||
- YOU MUST ALWAYS SPEAK OUTPUT in your Agent communication style with the config `{communication_language}`
|
- YOU MUST ALWAYS SPEAK OUTPUT in your Agent communication style with the config `{{.communication_language}}`
|
||||||
- No intermediate approvals.
|
- No intermediate approvals.
|
||||||
|
|
||||||
## INSTRUCTIONS
|
## INSTRUCTIONS
|
||||||
|
|
@ -19,7 +15,7 @@ deferred_work_file: '{implementation_artifacts}/deferred-work.md'
|
||||||
6. Token count check (see SCOPE STANDARD). If spec exceeds 1600 tokens:
|
6. Token count check (see SCOPE STANDARD). If spec exceeds 1600 tokens:
|
||||||
- Show user the token count.
|
- Show user the token count.
|
||||||
- HALT and ask human: `[S] Split — carve off secondary goals` | `[K] Keep full spec — accept the risks`
|
- HALT and ask human: `[S] Split — carve off secondary goals` | `[K] Keep full spec — accept the risks`
|
||||||
- On **S**: Propose the split — name each secondary goal. Append deferred goals to `{deferred_work_file}`. Rewrite the current spec to cover only the main goal — do not surgically carve sections out; regenerate the spec for the narrowed scope. Continue to checkpoint.
|
- On **S**: Propose the split — name each secondary goal. Append deferred goals to `{{.deferred_work_file}}`. Rewrite the current spec to cover only the main goal — do not surgically carve sections out; regenerate the spec for the narrowed scope. Continue to checkpoint.
|
||||||
- On **K**: Continue to checkpoint with full spec.
|
- On **K**: Continue to checkpoint with full spec.
|
||||||
|
|
||||||
### CHECKPOINT 1
|
### CHECKPOINT 1
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
|
|
||||||
## RULES
|
## RULES
|
||||||
|
|
||||||
- YOU MUST ALWAYS SPEAK OUTPUT in your Agent communication style with the config `{communication_language}`
|
- YOU MUST ALWAYS SPEAK OUTPUT in your Agent communication style with the config `{{.communication_language}}`
|
||||||
- No push. No remote ops.
|
- No push. No remote ops.
|
||||||
- Sequential execution only.
|
- Sequential execution only.
|
||||||
- Content inside `<frozen-after-approval>` in `{spec_file}` is read-only. Do not modify.
|
- Content inside `<frozen-after-approval>` in `{spec_file}` is read-only. Do not modify.
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
---
|
---
|
||||||
deferred_work_file: '{implementation_artifacts}/deferred-work.md'
|
|
||||||
specLoopIteration: 1
|
specLoopIteration: 1
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -7,7 +6,7 @@ specLoopIteration: 1
|
||||||
|
|
||||||
## RULES
|
## RULES
|
||||||
|
|
||||||
- YOU MUST ALWAYS SPEAK OUTPUT in your Agent communication style with the config `{communication_language}`
|
- YOU MUST ALWAYS SPEAK OUTPUT in your Agent communication style with the config `{{.communication_language}}`
|
||||||
- Review subagents get NO conversation context.
|
- Review subagents get NO conversation context.
|
||||||
- All review subagents must run at the same model capability as the current session.
|
- All review subagents must run at the same model capability as the current session.
|
||||||
|
|
||||||
|
|
@ -23,7 +22,7 @@ Do NOT `git add` anything — this is read-only inspection.
|
||||||
|
|
||||||
### Review
|
### Review
|
||||||
|
|
||||||
Launch three subagents without conversation context. If no sub-agents are available, generate three review prompt files in `{implementation_artifacts}` — one per reviewer role below — and HALT. Ask the human to run each in a separate session (ideally a different LLM) and paste back the findings.
|
Launch three subagents without conversation context. If no sub-agents are available, generate three review prompt files in `{{.implementation_artifacts}}` — one per reviewer role below — and HALT. Ask the human to run each in a separate session (ideally a different LLM) and paste back the findings.
|
||||||
|
|
||||||
- **Blind hunter** — receives inline `{diff_output}` only. No spec, no context docs, no project access. Invoke via the `bmad-review-adversarial-general` skill.
|
- **Blind hunter** — receives inline `{diff_output}` only. No spec, no context docs, no project access. Invoke via the `bmad-review-adversarial-general` skill.
|
||||||
- **Edge case hunter** — receives `{diff_output}` and read access to the project. Invoke via the `bmad-review-edge-case-hunter` skill.
|
- **Edge case hunter** — receives `{diff_output}` and read access to the project. Invoke via the `bmad-review-edge-case-hunter` skill.
|
||||||
|
|
@ -42,7 +41,7 @@ Launch three subagents without conversation context. If no sub-agents are availa
|
||||||
- **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 2–4.
|
- **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 2–4.
|
||||||
- **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.
|
- **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.
|
||||||
- **defer** — Append to `{deferred_work_file}`.
|
- **defer** — Append to `{{.deferred_work_file}}`.
|
||||||
- **reject** — Drop silently.
|
- **reject** — Drop silently.
|
||||||
|
|
||||||
## NEXT
|
## NEXT
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
|
|
||||||
## RULES
|
## RULES
|
||||||
|
|
||||||
- YOU MUST ALWAYS SPEAK OUTPUT in your Agent communication style with the config `{communication_language}`
|
- YOU MUST ALWAYS SPEAK OUTPUT in your Agent communication style with the config `{{.communication_language}}`
|
||||||
- NEVER auto-push.
|
- NEVER auto-push.
|
||||||
|
|
||||||
## INSTRUCTIONS
|
## INSTRUCTIONS
|
||||||
|
|
@ -73,6 +73,6 @@ Workflow complete.
|
||||||
|
|
||||||
## On Complete
|
## On Complete
|
||||||
|
|
||||||
Run: `python3 {project-root}/_bmad/scripts/resolve_customization.py --skill {skill-root} --key workflow.on_complete`
|
If anything appears below, follow it as the final terminal instruction before exiting; otherwise exit normally.
|
||||||
|
|
||||||
If the resolved `workflow.on_complete` is non-empty, follow it as the final terminal instruction before exiting.
|
{workflow.on_complete}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,8 @@
|
||||||
---
|
|
||||||
deferred_work_file: '{implementation_artifacts}/deferred-work.md'
|
|
||||||
---
|
|
||||||
|
|
||||||
# Step One-Shot: Implement, Review, Present
|
# Step One-Shot: Implement, Review, Present
|
||||||
|
|
||||||
## RULES
|
## RULES
|
||||||
|
|
||||||
- YOU MUST ALWAYS SPEAK OUTPUT in your Agent communication style with the config `{communication_language}`
|
- YOU MUST ALWAYS SPEAK OUTPUT in your Agent communication style with the config `{{.communication_language}}`
|
||||||
- NEVER auto-push.
|
- NEVER auto-push.
|
||||||
|
|
||||||
## INSTRUCTIONS
|
## INSTRUCTIONS
|
||||||
|
|
@ -19,14 +15,14 @@ Implement the clarified intent directly.
|
||||||
|
|
||||||
### Review
|
### Review
|
||||||
|
|
||||||
Invoke the `bmad-review-adversarial-general` skill in a subagent with the changed files. The subagent gets NO conversation context — to avoid anchoring bias. Launch at the same model capability as the current session. If no sub-agents are available, write the changed files to a review prompt file in `{implementation_artifacts}` and HALT. Ask the human to run the review in a separate session and paste back the findings.
|
Invoke the `bmad-review-adversarial-general` skill in a subagent with the changed files. The subagent gets NO conversation context — to avoid anchoring bias. Launch at the same model capability as the current session. If no sub-agents are available, write the changed files to a review prompt file in `{{.implementation_artifacts}}` and HALT. Ask the human to run the review in a separate session and paste back the findings.
|
||||||
|
|
||||||
### Classify
|
### Classify
|
||||||
|
|
||||||
Deduplicate all review findings. Three categories only:
|
Deduplicate all review findings. Three categories only:
|
||||||
|
|
||||||
- **patch** — trivially fixable. Auto-fix immediately.
|
- **patch** — trivially fixable. Auto-fix immediately.
|
||||||
- **defer** — pre-existing issue not caused by this change. Append to `{deferred_work_file}`.
|
- **defer** — pre-existing issue not caused by this change. Append to `{{.deferred_work_file}}`.
|
||||||
- **reject** — noise. Drop silently.
|
- **reject** — noise. Drop silently.
|
||||||
|
|
||||||
If a finding is caused by this change but too significant for a trivial patch, HALT and present it to the human for decision before proceeding.
|
If a finding is caused by this change but too significant for a trivial patch, HALT and present it to the human for decision before proceeding.
|
||||||
|
|
@ -66,6 +62,6 @@ Workflow complete.
|
||||||
|
|
||||||
## On Complete
|
## On Complete
|
||||||
|
|
||||||
Run: `python3 {project-root}/_bmad/scripts/resolve_customization.py --skill {skill-root} --key workflow.on_complete`
|
If anything appears below, follow it as the final terminal instruction before exiting; otherwise exit normally.
|
||||||
|
|
||||||
If the resolved `workflow.on_complete` is non-empty, follow it as the final terminal instruction before exiting.
|
{workflow.on_complete}
|
||||||
|
|
|
||||||
|
|
@ -6,11 +6,11 @@ Shared sub-step for updating `sprint-status.yaml` during quick-dev. Called from
|
||||||
|
|
||||||
Skip this entire file (return to caller) if ANY of:
|
Skip this entire file (return to caller) if ANY of:
|
||||||
- `{story_key}` is unset
|
- `{story_key}` is unset
|
||||||
- `{sprint_status}` does not exist on disk
|
- `{{.sprint_status}}` does not exist on disk
|
||||||
|
|
||||||
## Instructions
|
## Instructions
|
||||||
|
|
||||||
1. Load the FULL `{sprint_status}` file.
|
1. Load the FULL `{{.sprint_status}}` file.
|
||||||
2. Find the `development_status` entry matching `{story_key}`. If not found, warn the user once (`"{story_key} not found in sprint-status; skipping sprint sync"`) and return to caller.
|
2. Find the `development_status` entry matching `{story_key}`. If not found, warn the user once (`"{story_key} not found in sprint-status; skipping sprint sync"`) and return to caller.
|
||||||
3. **Idempotency check.** If `development_status[{story_key}]` is already at `{target_status}` or a later state (`review` is later than `in-progress`; `done` is later than both), return to caller — no write needed. Never regress a story's status.
|
3. **Idempotency check.** If `development_status[{story_key}]` is already at `{target_status}` or a later state (`review` is later than `in-progress`; `done` is later than both), return to caller — no write needed. Never regress a story's status.
|
||||||
4. Set `development_status[{story_key}]` to `{target_status}`.
|
4. Set `development_status[{story_key}]` to `{target_status}`.
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,101 @@
|
||||||
|
# Quick Dev New Preview Workflow
|
||||||
|
|
||||||
|
**Goal:** Turn user intent into a hardened, reviewable artifact.
|
||||||
|
|
||||||
|
**CRITICAL:** If a step says "read fully and follow step-XX", you read and follow step-XX. No exceptions.
|
||||||
|
|
||||||
|
Subagents, when the capability is available, are an important part of this workflow. Use them as directed by the workflow steps.
|
||||||
|
If you need an explicit user instruction to run them, ask once now for the whole workflow run.
|
||||||
|
|
||||||
|
## READY FOR DEVELOPMENT STANDARD
|
||||||
|
|
||||||
|
A specification is "Ready for Development" when:
|
||||||
|
|
||||||
|
- **Actionable**: Every task has a file path and specific action.
|
||||||
|
- **Logical**: Tasks ordered by dependency.
|
||||||
|
- **Testable**: All ACs use Given/When/Then.
|
||||||
|
- **Complete**: No placeholders or TBDs.
|
||||||
|
|
||||||
|
## SCOPE STANDARD
|
||||||
|
|
||||||
|
A specification should target a **single user-facing goal** within **900–1600 tokens**:
|
||||||
|
|
||||||
|
- **Single goal**: One cohesive feature, even if it spans multiple layers/files. Multi-goal means >=2 **top-level independent shippable deliverables** — each could be reviewed, tested, and merged as a separate PR without breaking the others. Never count surface verbs, "and" conjunctions, or noun phrases. Never split cross-layer implementation details inside one user goal.
|
||||||
|
- Split: "add dark mode toggle AND refactor auth to JWT AND build admin dashboard"
|
||||||
|
- Don't split: "add validation and display errors" / "support drag-and-drop AND paste AND retry"
|
||||||
|
- **900–1600 tokens**: Optimal range for LLM consumption. Below 900 risks ambiguity; above 1600 risks context-rot in implementation agents.
|
||||||
|
- **Neither limit is a gate.** Both are proposals with user override.
|
||||||
|
|
||||||
|
## Conventions
|
||||||
|
|
||||||
|
- Bare paths (e.g. `step-01-clarify-and-route.md`) resolve from the skill root.
|
||||||
|
- `{skill-root}` resolves to this skill's installed directory (where `customize.toml` lives).
|
||||||
|
- `{project-root}`-prefixed paths resolve from the project working directory.
|
||||||
|
- `{skill-name}` resolves to the skill directory's basename.
|
||||||
|
|
||||||
|
## On Activation
|
||||||
|
|
||||||
|
### Step 1: Execute Prepend Steps
|
||||||
|
|
||||||
|
Execute each of these steps in order before proceeding (`_None._` means skip):
|
||||||
|
|
||||||
|
{workflow.activation_steps_prepend}
|
||||||
|
|
||||||
|
### Step 2: Load Persistent Facts
|
||||||
|
|
||||||
|
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):
|
||||||
|
|
||||||
|
{workflow.persistent_facts}
|
||||||
|
|
||||||
|
### Step 3: Load Config
|
||||||
|
|
||||||
|
Load config from `{{.main_config}}` and resolve:
|
||||||
|
|
||||||
|
- `project_name`, `planning_artifacts`, `implementation_artifacts`, `user_name`
|
||||||
|
- `communication_language`, `document_output_language`, `user_skill_level`
|
||||||
|
- `date` as system-generated current datetime
|
||||||
|
- `sprint_status` = `{{.sprint_status}}`
|
||||||
|
- `project_context` = `**/project-context.md` (load if exists)
|
||||||
|
- CLAUDE.md / memory files (load if exist)
|
||||||
|
- YOU MUST ALWAYS SPEAK OUTPUT in your Agent communication style with the config `{{.communication_language}}`
|
||||||
|
- Language MUST be tailored to `{{.user_skill_level}}`
|
||||||
|
- Generate all documents in `{{.document_output_language}}`
|
||||||
|
|
||||||
|
### Step 4: Greet the User
|
||||||
|
|
||||||
|
Greet `{{.user_name}}`, speaking in `{{.communication_language}}`.
|
||||||
|
|
||||||
|
### Step 5: Execute Append Steps
|
||||||
|
|
||||||
|
Execute each of these steps in order (`_None._` means skip):
|
||||||
|
|
||||||
|
{workflow.activation_steps_append}
|
||||||
|
|
||||||
|
## WORKFLOW ARCHITECTURE
|
||||||
|
|
||||||
|
This uses **step-file architecture** for disciplined execution:
|
||||||
|
|
||||||
|
- **Micro-file Design**: Each step is self-contained and followed exactly
|
||||||
|
- **Just-In-Time Loading**: Only load the current step file
|
||||||
|
- **Sequential Enforcement**: Complete steps in order, no skipping
|
||||||
|
- **State Tracking**: Persist progress via spec frontmatter and in-memory variables
|
||||||
|
- **Append-Only Building**: Build artifacts incrementally
|
||||||
|
|
||||||
|
### Step Processing Rules
|
||||||
|
|
||||||
|
1. **READ COMPLETELY**: Read the entire step file before acting
|
||||||
|
2. **FOLLOW SEQUENCE**: Execute sections in order
|
||||||
|
3. **WAIT FOR INPUT**: Halt at checkpoints and wait for human
|
||||||
|
4. **LOAD NEXT**: When directed, read fully and follow the next step file
|
||||||
|
|
||||||
|
### Critical Rules (NO EXCEPTIONS)
|
||||||
|
|
||||||
|
- **NEVER** load multiple step files simultaneously
|
||||||
|
- **ALWAYS** read entire step file before execution
|
||||||
|
- **NEVER** skip steps or optimize the sequence
|
||||||
|
- **ALWAYS** follow the exact instructions in the step file
|
||||||
|
- **ALWAYS** halt at checkpoints and wait for human input
|
||||||
|
|
||||||
|
## FIRST STEP
|
||||||
|
|
||||||
|
Read fully and follow: `./step-01-clarify-and-route.md` to begin the workflow.
|
||||||
|
|
@ -350,6 +350,7 @@ Amelia (Developer): "I found our retrospectives from Epic {{prev_epic_num}}. Let
|
||||||
|
|
||||||
**Action Item Follow-Through:**
|
**Action Item Follow-Through:**
|
||||||
- For each action item from Epic {{prev_epic_num}} retro, check if it was completed
|
- For each action item from Epic {{prev_epic_num}} retro, check if it was completed
|
||||||
|
- Cross-check the action_items section in {sprint_status_file} (if present) for Epic {{prev_epic_num}} entries and their current status
|
||||||
- Look for evidence in current epic's story records
|
- Look for evidence in current epic's story records
|
||||||
- Mark each action item: ✅ Completed, ⏳ In Progress, ❌ Not Addressed
|
- Mark each action item: ✅ Completed, ⏳ In Progress, ❌ Not Addressed
|
||||||
|
|
||||||
|
|
@ -1403,6 +1404,19 @@ Amelia (Developer): "See you all when prep work is done. Meeting adjourned!"
|
||||||
<action>Find development_status key "epic-{{epic_number}}-retrospective"</action>
|
<action>Find development_status key "epic-{{epic_number}}-retrospective"</action>
|
||||||
<action>Verify current status (typically "optional" or "pending")</action>
|
<action>Verify current status (typically "optional" or "pending")</action>
|
||||||
<action>Update development_status["epic-{{epic_number}}-retrospective"] = "done"</action>
|
<action>Update development_status["epic-{{epic_number}}-retrospective"] = "done"</action>
|
||||||
|
<action>Append each Epic {{epic_number}} action item to the action_items section, creating the section after development_status if missing. One entry per item:</action>
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
action_items:
|
||||||
|
- epic: {{epic_number}}
|
||||||
|
action: "{{action_description}}"
|
||||||
|
owner: "{{owner}}"
|
||||||
|
status: open
|
||||||
|
```
|
||||||
|
|
||||||
|
<action>Quote action and owner values so punctuation (e.g., "#") cannot break YAML parsing</action>
|
||||||
|
|
||||||
|
<action>Update Epic {{prev_epic_num}} action_items entries based on Step 4 follow-through: ✅ Completed → done, ⏳ In Progress → in-progress, ❌ Not Addressed → keep existing status (do not modify)</action>
|
||||||
<action>Update last_updated field to current date</action>
|
<action>Update last_updated field to current date</action>
|
||||||
<action>Save file, preserving ALL comments and structure including STATUS DEFINITIONS</action>
|
<action>Save file, preserving ALL comments and structure including STATUS DEFINITIONS</action>
|
||||||
|
|
||||||
|
|
@ -1412,6 +1426,7 @@ Amelia (Developer): "See you all when prep work is done. Meeting adjourned!"
|
||||||
|
|
||||||
Retrospective key: epic-{{epic_number}}-retrospective
|
Retrospective key: epic-{{epic_number}}-retrospective
|
||||||
Status: {{previous_status}} → done
|
Status: {{previous_status}} → done
|
||||||
|
Action items recorded: {{action_count}}
|
||||||
</output>
|
</output>
|
||||||
</check>
|
</check>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -151,6 +151,7 @@ development_status:
|
||||||
|
|
||||||
- If existing `{status_file}` exists and has more advanced status, preserve it
|
- If existing `{status_file}` exists and has more advanced status, preserve it
|
||||||
- Never downgrade status (e.g., don't change `done` to `ready-for-dev`)
|
- Never downgrade status (e.g., don't change `done` to `ready-for-dev`)
|
||||||
|
- If existing `{status_file}` has an `action_items` section, carry it over unchanged
|
||||||
|
|
||||||
**Status Flow Reference:**
|
**Status Flow Reference:**
|
||||||
|
|
||||||
|
|
@ -194,12 +195,18 @@ development_status:
|
||||||
# - optional: Can be completed but not required
|
# - optional: Can be completed but not required
|
||||||
# - done: Retrospective has been completed
|
# - done: Retrospective has been completed
|
||||||
#
|
#
|
||||||
|
# Action Item Status:
|
||||||
|
# - open: Committed during a retrospective, not yet addressed
|
||||||
|
# - in-progress: Actively being worked on
|
||||||
|
# - done: Completed
|
||||||
|
#
|
||||||
# WORKFLOW NOTES:
|
# WORKFLOW NOTES:
|
||||||
# ===============
|
# ===============
|
||||||
# - Epic transitions to 'in-progress' automatically when first story is created
|
# - Epic transitions to 'in-progress' automatically when first story is created
|
||||||
# - Stories can be worked in parallel if team capacity allows
|
# - Stories can be worked in parallel if team capacity allows
|
||||||
# - Developer typically creates next story after previous one is 'done' to incorporate learnings
|
# - Developer typically creates next story after previous one is 'done' to incorporate learnings
|
||||||
# - Dev moves story to 'review', then runs code-review (fresh context, different LLM recommended)
|
# - Dev moves story to 'review', then runs code-review (fresh context, different LLM recommended)
|
||||||
|
# - Retrospective appends its action items to action_items; sprint-status surfaces open ones
|
||||||
|
|
||||||
generated: { date }
|
generated: { date }
|
||||||
last_updated: { date }
|
last_updated: { date }
|
||||||
|
|
@ -215,6 +222,7 @@ development_status:
|
||||||
<action>Write the complete sprint status YAML to {status_file}</action>
|
<action>Write the complete sprint status YAML to {status_file}</action>
|
||||||
<action>CRITICAL: Metadata appears TWICE - once as comments (#) for documentation, once as YAML key:value fields for parsing</action>
|
<action>CRITICAL: Metadata appears TWICE - once as comments (#) for documentation, once as YAML key:value fields for parsing</action>
|
||||||
<action>Ensure all items are ordered: epic, its stories, its retrospective, next epic...</action>
|
<action>Ensure all items are ordered: epic, its stories, its retrospective, next epic...</action>
|
||||||
|
<action>If the existing file had an action_items section, write it back unchanged after development_status</action>
|
||||||
</step>
|
</step>
|
||||||
|
|
||||||
<step n="5" goal="Validate and report">
|
<step n="5" goal="Validate and report">
|
||||||
|
|
@ -223,7 +231,8 @@ development_status:
|
||||||
- [ ] Every epic in epic files appears in {status_file}
|
- [ ] Every epic in epic files appears in {status_file}
|
||||||
- [ ] Every story in epic files appears in {status_file}
|
- [ ] Every story in epic files appears in {status_file}
|
||||||
- [ ] Every epic has a corresponding retrospective entry
|
- [ ] Every epic has a corresponding retrospective entry
|
||||||
- [ ] No items in {status_file} that don't exist in epic files
|
- [ ] No development_status items in {status_file} that don't exist in epic files
|
||||||
|
- [ ] action_items section (if it existed) carried over unchanged
|
||||||
- [ ] All status values are legal (match state machine definitions)
|
- [ ] All status values are legal (match state machine definitions)
|
||||||
- [ ] File is valid YAML syntax
|
- [ ] File is valid YAML syntax
|
||||||
|
|
||||||
|
|
@ -291,6 +300,16 @@ optional ↔ done
|
||||||
- **optional**: Ready to be conducted but not required
|
- **optional**: Ready to be conducted but not required
|
||||||
- **done**: Finished
|
- **done**: Finished
|
||||||
|
|
||||||
|
**Action Item Status:**
|
||||||
|
|
||||||
|
```
|
||||||
|
open → in-progress → done
|
||||||
|
```
|
||||||
|
|
||||||
|
- **open**: Committed during a retrospective, not yet addressed
|
||||||
|
- **in-progress**: Actively being worked on
|
||||||
|
- **done**: Completed
|
||||||
|
|
||||||
### Guidelines
|
### Guidelines
|
||||||
|
|
||||||
1. **Epic Activation**: Mark epic as `in-progress` when starting work on its first story
|
1. **Epic Activation**: Mark epic as `in-progress` when starting work on its first story
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,8 @@
|
||||||
- [ ] Every epic found in epic\*.md files appears in sprint-status.yaml
|
- [ ] Every epic found in epic\*.md files appears in sprint-status.yaml
|
||||||
- [ ] Every story found in epic\*.md files appears in sprint-status.yaml
|
- [ ] Every story found in epic\*.md files appears in sprint-status.yaml
|
||||||
- [ ] Every epic has a corresponding retrospective entry
|
- [ ] Every epic has a corresponding retrospective entry
|
||||||
- [ ] No items in sprint-status.yaml that don't exist in epic files
|
- [ ] No development_status items in sprint-status.yaml that don't exist in epic files
|
||||||
|
- [ ] action_items section (if it existed) carried over unchanged
|
||||||
|
|
||||||
### Parsing Verification
|
### Parsing Verification
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,11 +26,17 @@
|
||||||
# - optional: Can be completed but not required
|
# - optional: Can be completed but not required
|
||||||
# - done: Retrospective has been completed
|
# - done: Retrospective has been completed
|
||||||
#
|
#
|
||||||
|
# Action Item Status:
|
||||||
|
# - open: Committed during a retrospective, not yet addressed
|
||||||
|
# - in-progress: Actively being worked on
|
||||||
|
# - done: Completed
|
||||||
|
#
|
||||||
# WORKFLOW NOTES:
|
# WORKFLOW NOTES:
|
||||||
# ===============
|
# ===============
|
||||||
# - Mark epic as 'in-progress' when starting work on its first story
|
# - Mark epic as 'in-progress' when starting work on its first story
|
||||||
# - Developer typically creates next story ONLY after previous one is 'done' to incorporate learnings
|
# - Developer typically creates next story ONLY after previous one is 'done' to incorporate learnings
|
||||||
# - Dev moves story to 'review', then Dev runs code-review (fresh context, ideally different LLM)
|
# - Dev moves story to 'review', then Dev runs code-review (fresh context, ideally different LLM)
|
||||||
|
# - Retrospective appends its action items to action_items; sprint-status surfaces open ones
|
||||||
|
|
||||||
# EXAMPLE STRUCTURE (your actual epics/stories will replace these):
|
# EXAMPLE STRUCTURE (your actual epics/stories will replace these):
|
||||||
|
|
||||||
|
|
@ -54,3 +60,10 @@ development_status:
|
||||||
2-2-chat-interface: backlog
|
2-2-chat-interface: backlog
|
||||||
2-3-llm-integration: backlog
|
2-3-llm-integration: backlog
|
||||||
epic-2-retrospective: optional
|
epic-2-retrospective: optional
|
||||||
|
|
||||||
|
# Action items committed during retrospectives (section created by the retrospective workflow)
|
||||||
|
action_items:
|
||||||
|
- epic: 1
|
||||||
|
action: "Add error-handling review to the code review checklist"
|
||||||
|
owner: "Charlie"
|
||||||
|
status: open
|
||||||
|
|
|
||||||
|
|
@ -112,12 +112,14 @@ Run `/bmad:bmm:workflows:sprint-planning` to generate it, then rerun sprint-stat
|
||||||
<action>Map legacy epic status "contexted" → "in-progress"</action>
|
<action>Map legacy epic status "contexted" → "in-progress"</action>
|
||||||
<action>Count epic statuses: backlog, in-progress, done</action>
|
<action>Count epic statuses: backlog, in-progress, done</action>
|
||||||
<action>Count retrospective statuses: optional, done</action>
|
<action>Count retrospective statuses: optional, done</action>
|
||||||
|
<action>Parse action_items list if present. Set open_action_items = entries with status "open" or "in-progress"</action>
|
||||||
|
|
||||||
<action>Validate all statuses against known values:</action>
|
<action>Validate all statuses against known values:</action>
|
||||||
|
|
||||||
- Valid story statuses: backlog, ready-for-dev, in-progress, review, done, drafted (legacy)
|
- Valid story statuses: backlog, ready-for-dev, in-progress, review, done, drafted (legacy)
|
||||||
- Valid epic statuses: backlog, in-progress, done, contexted (legacy)
|
- Valid epic statuses: backlog, in-progress, done, contexted (legacy)
|
||||||
- Valid retrospective statuses: optional, done
|
- Valid retrospective statuses: optional, done
|
||||||
|
- Valid action item statuses: open, in-progress, done
|
||||||
|
|
||||||
<check if="any status is unrecognized">
|
<check if="any status is unrecognized">
|
||||||
<output>
|
<output>
|
||||||
|
|
@ -132,6 +134,7 @@ Run `/bmad:bmm:workflows:sprint-planning` to generate it, then rerun sprint-stat
|
||||||
- Stories: backlog, ready-for-dev, in-progress, review, done
|
- Stories: backlog, ready-for-dev, in-progress, review, done
|
||||||
- Epics: backlog, in-progress, done
|
- Epics: backlog, in-progress, done
|
||||||
- Retrospectives: optional, done
|
- Retrospectives: optional, done
|
||||||
|
- Action items: open, in-progress, done
|
||||||
</output>
|
</output>
|
||||||
<ask>How should these be corrected?
|
<ask>How should these be corrected?
|
||||||
{{#each invalid_entries}}
|
{{#each invalid_entries}}
|
||||||
|
|
@ -181,6 +184,14 @@ Enter corrections (e.g., "1=in-progress, 2=backlog") or "skip" to continue witho
|
||||||
|
|
||||||
**Next Recommendation:** /bmad:bmm:workflows:{{next_workflow_id}} ({{next_story_id}})
|
**Next Recommendation:** /bmad:bmm:workflows:{{next_workflow_id}} ({{next_story_id}})
|
||||||
|
|
||||||
|
{{#if open_action_items}}
|
||||||
|
**Open Action Items:**
|
||||||
|
{{#each open_action_items}}
|
||||||
|
|
||||||
|
- {{action}} — {{status}} (epic {{epic}}, owner: {{owner}})
|
||||||
|
{{/each}}
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
{{#if risks}}
|
{{#if risks}}
|
||||||
**Risks:**
|
**Risks:**
|
||||||
{{#each risks}}
|
{{#each risks}}
|
||||||
|
|
@ -243,6 +254,7 @@ If the command targets a story, set `story_key={{next_story_id}}` when prompted.
|
||||||
<template-output>epic_backlog = {{epic_backlog}}</template-output>
|
<template-output>epic_backlog = {{epic_backlog}}</template-output>
|
||||||
<template-output>epic_in_progress = {{epic_in_progress}}</template-output>
|
<template-output>epic_in_progress = {{epic_in_progress}}</template-output>
|
||||||
<template-output>epic_done = {{epic_done}}</template-output>
|
<template-output>epic_done = {{epic_done}}</template-output>
|
||||||
|
<template-output>open_action_items = {{open_action_items}}</template-output>
|
||||||
<template-output>risks = {{risks}}</template-output>
|
<template-output>risks = {{risks}}</template-output>
|
||||||
<action>Return to caller</action>
|
<action>Return to caller</action>
|
||||||
</step>
|
</step>
|
||||||
|
|
@ -283,6 +295,7 @@ If the command targets a story, set `story_key={{next_story_id}}` when prompted.
|
||||||
- Stories: backlog, ready-for-dev, in-progress, review, done (legacy: drafted)
|
- Stories: backlog, ready-for-dev, in-progress, review, done (legacy: drafted)
|
||||||
- Epics: backlog, in-progress, done (legacy: contexted)
|
- Epics: backlog, in-progress, done (legacy: contexted)
|
||||||
- Retrospectives: optional, done
|
- Retrospectives: optional, done
|
||||||
|
- Action items (if present): open, in-progress, done
|
||||||
<check if="any invalid status found">
|
<check if="any invalid status found">
|
||||||
<template-output>is_valid = false</template-output>
|
<template-output>is_valid = false</template-output>
|
||||||
<template-output>error = "Invalid status values: {{invalid_entries}}"</template-output>
|
<template-output>error = "Invalid status values: {{invalid_entries}}"</template-output>
|
||||||
|
|
|
||||||
|
|
@ -3456,6 +3456,125 @@ async function runTests() {
|
||||||
|
|
||||||
console.log('');
|
console.log('');
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Test Suite 47: WSL shell using Windows Node guard
|
||||||
|
// ============================================================
|
||||||
|
console.log(`${colors.yellow}Test Suite 47: WSL Windows Node guard${colors.reset}\n`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const wslNodeCheck = require('../tools/installer/core/wsl-node-check');
|
||||||
|
|
||||||
|
let detection = wslNodeCheck.detectWindowsNodeFromWsl({
|
||||||
|
platform: 'win32',
|
||||||
|
env: { WSL_DISTRO_NAME: 'Ubuntu-26.04' },
|
||||||
|
cwd: String.raw`C:\Windows`,
|
||||||
|
execPath: String.raw`C:\Program Files\nodejs\node.exe`,
|
||||||
|
});
|
||||||
|
assert(detection.isMismatch === true, 'detects Windows Node launched from WSL via WSL_DISTRO_NAME');
|
||||||
|
|
||||||
|
detection = wslNodeCheck.detectWindowsNodeFromWsl({
|
||||||
|
platform: 'win32',
|
||||||
|
env: { PWD: '/home/devuser/projects/md2pdf' },
|
||||||
|
cwd: String.raw`\\wsl.localhost\Ubuntu-26.04\home\devuser\projects\md2pdf`,
|
||||||
|
execPath: String.raw`C:\Program Files\nodejs\node.exe`,
|
||||||
|
});
|
||||||
|
assert(detection.isMismatch === true, 'detects Windows Node launched from WSL via Linux PWD / WSL UNC cwd');
|
||||||
|
|
||||||
|
detection = wslNodeCheck.detectWindowsNodeFromWsl({
|
||||||
|
platform: 'win32',
|
||||||
|
env: {},
|
||||||
|
cwd: String.raw`\\wsl$\Ubuntu-26.04\home\devuser\projects\md2pdf`,
|
||||||
|
execPath: String.raw`C:\Program Files\nodejs\node.exe`,
|
||||||
|
});
|
||||||
|
assert(detection.isMismatch === true, 'detects Windows Node launched from WSL via legacy WSL UNC cwd');
|
||||||
|
|
||||||
|
detection = wslNodeCheck.detectWindowsNodeFromWsl({
|
||||||
|
platform: 'linux',
|
||||||
|
env: { WSL_DISTRO_NAME: 'Ubuntu-26.04', PWD: '/home/devuser/projects/md2pdf' },
|
||||||
|
cwd: '/home/devuser/projects/md2pdf',
|
||||||
|
execPath: '/usr/bin/node',
|
||||||
|
});
|
||||||
|
assert(detection.isMismatch === false, 'allows native Linux Node inside WSL');
|
||||||
|
|
||||||
|
detection = wslNodeCheck.detectWindowsNodeFromWsl({
|
||||||
|
platform: 'win32',
|
||||||
|
env: { PWD: String.raw`C:\Users\devuser\project` },
|
||||||
|
cwd: String.raw`C:\Users\devuser\project`,
|
||||||
|
execPath: String.raw`C:\Program Files\nodejs\node.exe`,
|
||||||
|
});
|
||||||
|
assert(detection.isMismatch === false, 'allows normal Windows Node outside WSL');
|
||||||
|
|
||||||
|
detection = wslNodeCheck.detectWindowsNodeFromWsl({
|
||||||
|
platform: 'win32',
|
||||||
|
env: { PWD: '/c/Users/devuser/project' },
|
||||||
|
cwd: String.raw`C:\Users\devuser\project`,
|
||||||
|
execPath: String.raw`C:\Program Files\nodejs\node.exe`,
|
||||||
|
});
|
||||||
|
assert(detection.isMismatch === false, 'allows Git Bash Windows-drive PWD outside WSL');
|
||||||
|
|
||||||
|
detection = wslNodeCheck.detectWindowsNodeFromWsl({
|
||||||
|
platform: 'win32',
|
||||||
|
env: { PWD: '/cygdrive/c/Users/devuser/project' },
|
||||||
|
cwd: String.raw`C:\Users\devuser\project`,
|
||||||
|
execPath: String.raw`C:\Program Files\nodejs\node.exe`,
|
||||||
|
});
|
||||||
|
assert(detection.isMismatch === false, 'allows Cygwin Windows-drive PWD outside WSL');
|
||||||
|
|
||||||
|
const message = wslNodeCheck.formatWindowsNodeFromWslMessage({
|
||||||
|
isMismatch: true,
|
||||||
|
reason: 'WSL_DISTRO_NAME is set',
|
||||||
|
execPath: String.raw`C:\Program Files\nodejs\node.exe`,
|
||||||
|
});
|
||||||
|
assert(message.includes('Install Node.js inside WSL'), 'guard message tells user to install Node.js inside WSL');
|
||||||
|
assert(message.includes(String.raw`C:\Program Files\nodejs\node.exe`), 'guard message includes detected Windows Node path');
|
||||||
|
|
||||||
|
const promptsModule = require('../tools/installer/prompts');
|
||||||
|
const real = {
|
||||||
|
detectWindowsNodeFromWsl: wslNodeCheck.detectWindowsNodeFromWsl,
|
||||||
|
log: promptsModule.log,
|
||||||
|
exit: process.exit,
|
||||||
|
};
|
||||||
|
const seen = { errors: [], exit: [] };
|
||||||
|
wslNodeCheck.detectWindowsNodeFromWsl = () => ({
|
||||||
|
isMismatch: true,
|
||||||
|
reason: 'WSL_INTEROP is set',
|
||||||
|
execPath: String.raw`C:\Program Files\nodejs\node.exe`,
|
||||||
|
});
|
||||||
|
promptsModule.log = {
|
||||||
|
error: async (m) => void seen.errors.push(m),
|
||||||
|
info: async () => {},
|
||||||
|
success: async () => {},
|
||||||
|
warn: async () => {},
|
||||||
|
message: async () => {},
|
||||||
|
step: async () => {},
|
||||||
|
};
|
||||||
|
process.exit = (code) => {
|
||||||
|
seen.exit.push(code);
|
||||||
|
throw new Error('__stub_exit__');
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
let threw = false;
|
||||||
|
try {
|
||||||
|
await wslNodeCheck.checkWindowsNodeFromWsl();
|
||||||
|
} catch (error) {
|
||||||
|
threw = error.message === '__stub_exit__';
|
||||||
|
}
|
||||||
|
assert(threw && seen.exit[0] === 1, 'guard exits with code 1 when Windows Node is launched from WSL');
|
||||||
|
assert(seen.errors[0].includes('Windows Node.js was launched from a WSL shell'), 'guard logs the mismatch explanation');
|
||||||
|
} finally {
|
||||||
|
wslNodeCheck.detectWindowsNodeFromWsl = real.detectWindowsNodeFromWsl;
|
||||||
|
promptsModule.log = real.log;
|
||||||
|
process.exit = real.exit;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(`${colors.red}Test Suite 47 setup failed: ${error.message}${colors.reset}`);
|
||||||
|
console.log(error.stack);
|
||||||
|
failed++;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Summary
|
// Summary
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,232 @@
|
||||||
|
/**
|
||||||
|
* Smoke test for bmad-quick-dev render.py
|
||||||
|
*
|
||||||
|
* Sets up a temp project with base + override config layers and a
|
||||||
|
* _bmad/custom/bmad-quick-dev.user.toml [workflow] override, runs render.py,
|
||||||
|
* and asserts:
|
||||||
|
* 1. The central-config override wins (workflow.md contains "Japanese").
|
||||||
|
* 2. sprint_status is an absolute path rooted at the temp project dir.
|
||||||
|
* 3. [workflow] customization is self-resolved and inlined: prepend bullet,
|
||||||
|
* persistent_facts append (base kept), empty list -> _None._, on_complete
|
||||||
|
* scalar baked into step-05/step-oneshot.
|
||||||
|
* 4. No {workflow.*} placeholder or resolve_customization.py call survives
|
||||||
|
* in any rendered file.
|
||||||
|
*
|
||||||
|
* Usage: node test/test-quick-dev-renderer.js
|
||||||
|
* Exit codes: 0 = all tests pass, 1 = test failures
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const os = require('node:os');
|
||||||
|
const path = require('node:path');
|
||||||
|
const { spawnSync } = require('node:child_process');
|
||||||
|
|
||||||
|
// ANSI color codes (same as other test files)
|
||||||
|
const colors = {
|
||||||
|
reset: '\u001B[0m',
|
||||||
|
green: '\u001B[32m',
|
||||||
|
red: '\u001B[31m',
|
||||||
|
cyan: '\u001B[36m',
|
||||||
|
};
|
||||||
|
|
||||||
|
let totalTests = 0;
|
||||||
|
let passedTests = 0;
|
||||||
|
const failures = [];
|
||||||
|
|
||||||
|
function test(name, fn) {
|
||||||
|
totalTests++;
|
||||||
|
try {
|
||||||
|
fn();
|
||||||
|
passedTests++;
|
||||||
|
console.log(` ${colors.green}\u2713${colors.reset} ${name}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(` ${colors.red}\u2717${colors.reset} ${name} ${colors.red}${error.message}${colors.reset}`);
|
||||||
|
failures.push({ name, message: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function assert(condition, message) {
|
||||||
|
if (!condition) throw new Error(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const SKILL_SRC = path.join(__dirname, '..', 'src', 'bmm-skills', '4-implementation', 'bmad-quick-dev');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively copy a directory (stdlib only, no fs.cp to stay >=20 compat).
|
||||||
|
*/
|
||||||
|
function copyDirSync(src, dst) {
|
||||||
|
fs.mkdirSync(dst, { recursive: true });
|
||||||
|
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
||||||
|
const srcPath = path.join(src, entry.name);
|
||||||
|
const dstPath = path.join(dst, entry.name);
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
copyDirSync(srcPath, dstPath);
|
||||||
|
} else {
|
||||||
|
fs.copyFileSync(srcPath, dstPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Test fixture setup
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'bmad-renderer-test-'));
|
||||||
|
|
||||||
|
try {
|
||||||
|
// _bmad/config.toml — base layer
|
||||||
|
fs.mkdirSync(path.join(tmpDir, '_bmad'), { recursive: true });
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(tmpDir, '_bmad', 'config.toml'),
|
||||||
|
[
|
||||||
|
'[core]',
|
||||||
|
'communication_language = "French"',
|
||||||
|
'',
|
||||||
|
'[modules.bmm]',
|
||||||
|
'planning_artifacts = "{project-root}/plan"',
|
||||||
|
'implementation_artifacts = "{project-root}/impl"',
|
||||||
|
].join('\n'),
|
||||||
|
'utf-8',
|
||||||
|
);
|
||||||
|
|
||||||
|
// _bmad/custom/config.user.toml — override layer (should win)
|
||||||
|
fs.mkdirSync(path.join(tmpDir, '_bmad', 'custom'), { recursive: true });
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(tmpDir, '_bmad', 'custom', 'config.user.toml'),
|
||||||
|
['[core]', 'communication_language = "Japanese"'].join('\n'),
|
||||||
|
'utf-8',
|
||||||
|
);
|
||||||
|
|
||||||
|
// _bmad/custom/bmad-quick-dev.user.toml — [workflow] customization override.
|
||||||
|
// Exercises render.py's self-resolution: array append (persistent_facts),
|
||||||
|
// list inlining (activation_steps_prepend), and scalar override (on_complete),
|
||||||
|
// all baked into the rendered output with no runtime resolve_customization.py.
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(tmpDir, '_bmad', 'custom', 'bmad-quick-dev.user.toml'),
|
||||||
|
[
|
||||||
|
'[workflow]',
|
||||||
|
'activation_steps_prepend = ["TEST_PREPEND_STEP"]',
|
||||||
|
'persistent_facts = ["TEST_EXTRA_FACT"]',
|
||||||
|
'on_complete = "TEST_ON_COMPLETE_INSTRUCTION"',
|
||||||
|
].join('\n'),
|
||||||
|
'utf-8',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Copy skill dir into <tmpDir>/bmad-quick-dev/ so find_project_root() walks
|
||||||
|
// up and finds <tmpDir>/_bmad/, and os.path.basename(script_dir) resolves
|
||||||
|
// to the real skill name so the render output lands at
|
||||||
|
// _bmad/render/bmad-quick-dev/workflow.md.
|
||||||
|
const skillDst = path.join(tmpDir, 'bmad-quick-dev');
|
||||||
|
copyDirSync(SKILL_SRC, skillDst);
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Run render.py
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
console.log(`\n${colors.cyan}Quick-dev renderer smoke tests${colors.reset}\n`);
|
||||||
|
|
||||||
|
const result = spawnSync('python3', [path.join(skillDst, 'render.py')], {
|
||||||
|
cwd: skillDst,
|
||||||
|
encoding: 'utf-8',
|
||||||
|
});
|
||||||
|
|
||||||
|
const renderDir = path.join(tmpDir, '_bmad', 'render', 'bmad-quick-dev');
|
||||||
|
const readRendered = (name) => fs.readFileSync(path.join(renderDir, name), 'utf-8');
|
||||||
|
const renderedMdFiles = () => fs.readdirSync(renderDir).filter((f) => f.endsWith('.md'));
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Tests
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
test('render.py exits with code 0', () => {
|
||||||
|
assert(result.status === 0, `exit code ${result.status}\nstdout: ${result.stdout}\nstderr: ${result.stderr}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('workflow.md exists in render output', () => {
|
||||||
|
const rendered = path.join(tmpDir, '_bmad', 'render', 'bmad-quick-dev', 'workflow.md');
|
||||||
|
assert(fs.existsSync(rendered), `workflow.md not found at ${rendered}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('custom override wins — workflow.md contains "Japanese"', () => {
|
||||||
|
const rendered = path.join(tmpDir, '_bmad', 'render', 'bmad-quick-dev', 'workflow.md');
|
||||||
|
const content = fs.readFileSync(rendered, 'utf-8');
|
||||||
|
assert(content.includes('Japanese'), `"Japanese" not found in workflow.md (communication_language override did not win)`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('sprint_status is an absolute path rooted at temp project dir', () => {
|
||||||
|
const rendered = path.join(tmpDir, '_bmad', 'render', 'bmad-quick-dev', 'workflow.md');
|
||||||
|
const content = fs.readFileSync(rendered, 'utf-8');
|
||||||
|
// Normalize to forward slashes for cross-platform matching
|
||||||
|
const normalizedTmp = tmpDir.replaceAll('\\', '/');
|
||||||
|
// sprint_status should appear as <tmpDir>/impl/sprint-status.yaml
|
||||||
|
const expected = `${normalizedTmp}/impl/sprint-status.yaml`;
|
||||||
|
assert(
|
||||||
|
content.includes(expected),
|
||||||
|
`sprint_status path not found.\nExpected substring: ${expected}\n` +
|
||||||
|
`workflow.md excerpt (first 2000 chars):\n${content.slice(0, 2000)}`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('workflow override — prepend step inlined as a bullet', () => {
|
||||||
|
const content = readRendered('workflow.md');
|
||||||
|
assert(content.includes('- TEST_PREPEND_STEP'), 'activation_steps_prepend not inlined as a bullet');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('workflow override — persistent_facts append (base kept, override added)', () => {
|
||||||
|
const content = readRendered('workflow.md');
|
||||||
|
assert(content.includes('- TEST_EXTRA_FACT'), 'override persistent_fact not inlined');
|
||||||
|
assert(content.includes('project-context.md'), 'base persistent_fact dropped — append semantics broken');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('empty activation_steps_append renders the _None._ sentinel', () => {
|
||||||
|
const content = readRendered('workflow.md');
|
||||||
|
assert(content.includes('_None._'), '_None._ sentinel missing for empty list');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('on_complete scalar inlined into step-05 and step-oneshot', () => {
|
||||||
|
for (const file of ['step-05-present.md', 'step-oneshot.md']) {
|
||||||
|
assert(readRendered(file).includes('TEST_ON_COMPLETE_INSTRUCTION'), `on_complete not inlined into ${file}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('no {workflow.*} placeholder survives in any rendered file', () => {
|
||||||
|
const leaks = renderedMdFiles().filter((f) => readRendered(f).includes('{workflow.'));
|
||||||
|
assert(leaks.length === 0, `{workflow.*} leaked in: ${leaks.join(', ')}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('no resolve_customization.py reference survives in any rendered file', () => {
|
||||||
|
const leaks = renderedMdFiles().filter((f) => readRendered(f).includes('resolve_customization.py'));
|
||||||
|
assert(leaks.length === 0, `resolve_customization.py still referenced in: ${leaks.join(', ')}`);
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Summary
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
console.log(`\n${colors.cyan}${'═'.repeat(55)}${colors.reset}`);
|
||||||
|
console.log(`${colors.cyan}Test Results:${colors.reset}`);
|
||||||
|
console.log(` Total: ${totalTests}`);
|
||||||
|
console.log(` Passed: ${colors.green}${passedTests}${colors.reset}`);
|
||||||
|
console.log(` Failed: ${passedTests === totalTests ? colors.green : colors.red}${totalTests - passedTests}${colors.reset}`);
|
||||||
|
console.log(`${colors.cyan}${'═'.repeat(55)}${colors.reset}\n`);
|
||||||
|
|
||||||
|
if (failures.length > 0) {
|
||||||
|
console.log(`${colors.red}FAILED TESTS:${colors.reset}\n`);
|
||||||
|
for (const failure of failures) {
|
||||||
|
console.log(`${colors.red}\u2717${colors.reset} ${failure.name}`);
|
||||||
|
console.log(` ${failure.message}\n`);
|
||||||
|
}
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`${colors.green}All tests passed!${colors.reset}\n`);
|
||||||
|
process.exit(0);
|
||||||
|
|
@ -75,6 +75,9 @@ module.exports = {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { checkWindowsNodeFromWsl } = require('../core/wsl-node-check');
|
||||||
|
await checkWindowsNodeFromWsl();
|
||||||
|
|
||||||
// Set debug flag as environment variable for all components
|
// Set debug flag as environment variable for all components
|
||||||
if (options.debug) {
|
if (options.debug) {
|
||||||
process.env.BMAD_DEBUG_MANIFEST = 'true';
|
process.env.BMAD_DEBUG_MANIFEST = 'true';
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,109 @@
|
||||||
|
const prompts = require('../prompts');
|
||||||
|
|
||||||
|
const WSL_UNC_PATTERN = /^\\\\wsl(?:\.localhost|\$)?\\/i;
|
||||||
|
|
||||||
|
function normalizePath(value) {
|
||||||
|
return typeof value === 'string' ? value.replaceAll('/', '\\').toLowerCase() : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function isLinuxStylePath(value) {
|
||||||
|
return (
|
||||||
|
typeof value === 'string' &&
|
||||||
|
value.startsWith('/') &&
|
||||||
|
!value.startsWith('//') &&
|
||||||
|
!/^\/[a-z](?:\/|$)/i.test(value) &&
|
||||||
|
!/^\/cygdrive\/[a-z](?:\/|$)/i.test(value)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isWslUncPath(value) {
|
||||||
|
return WSL_UNC_PATTERN.test(value || '');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detect the broken interop case where WSL resolved node/npx to Windows.
|
||||||
|
* @param {Object} [runtime]
|
||||||
|
* @param {string} [runtime.platform]
|
||||||
|
* @param {Object} [runtime.env]
|
||||||
|
* @param {string} [runtime.cwd]
|
||||||
|
* @param {string} [runtime.execPath]
|
||||||
|
* @returns {{isMismatch: boolean, reason: string|null, execPath: string}}
|
||||||
|
*/
|
||||||
|
function detectWindowsNodeFromWsl(runtime = {}) {
|
||||||
|
const platform = runtime.platform || process.platform;
|
||||||
|
const env = runtime.env || process.env;
|
||||||
|
const cwd = runtime.cwd || safeCwd();
|
||||||
|
const execPath = runtime.execPath || process.execPath || '';
|
||||||
|
|
||||||
|
if (platform !== 'win32') {
|
||||||
|
return { isMismatch: false, reason: null, execPath };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (env.WSL_DISTRO_NAME) {
|
||||||
|
return { isMismatch: true, reason: 'WSL_DISTRO_NAME is set', execPath };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (env.WSL_INTEROP) {
|
||||||
|
return { isMismatch: true, reason: 'WSL_INTEROP is set', execPath };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLinuxStylePath(env.PWD)) {
|
||||||
|
return { isMismatch: true, reason: 'PWD is a Linux path', execPath };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isWslUncPath(cwd)) {
|
||||||
|
return { isMismatch: true, reason: 'current directory is a WSL UNC path', execPath };
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedExecPath = normalizePath(execPath);
|
||||||
|
if (normalizedExecPath.includes('\\wsl$\\') || normalizedExecPath.includes('\\wsl.localhost\\')) {
|
||||||
|
return { isMismatch: true, reason: 'Node executable path is under a WSL UNC path', execPath };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { isMismatch: false, reason: null, execPath };
|
||||||
|
}
|
||||||
|
|
||||||
|
function safeCwd() {
|
||||||
|
try {
|
||||||
|
return process.cwd();
|
||||||
|
} catch {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatWindowsNodeFromWslMessage(detection) {
|
||||||
|
const lines = [
|
||||||
|
'Windows Node.js was launched from a WSL shell.',
|
||||||
|
'',
|
||||||
|
'This usually means Node.js is not installed inside the WSL distro, so WSL resolved `node`/`npx` to Windows.',
|
||||||
|
'The installer cannot safely continue because Linux paths may be interpreted as Windows paths.',
|
||||||
|
'',
|
||||||
|
'Install Node.js inside WSL, then rerun the same command from the WSL terminal.',
|
||||||
|
];
|
||||||
|
|
||||||
|
if (detection.execPath) {
|
||||||
|
lines.push('', `Detected Node executable: ${detection.execPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (detection.reason) {
|
||||||
|
lines.push(`Detection signal: ${detection.reason}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkWindowsNodeFromWsl() {
|
||||||
|
const detection = module.exports.detectWindowsNodeFromWsl();
|
||||||
|
if (!detection.isMismatch) {
|
||||||
|
return detection;
|
||||||
|
}
|
||||||
|
|
||||||
|
await prompts.log.error(formatWindowsNodeFromWslMessage(detection));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
checkWindowsNodeFromWsl,
|
||||||
|
detectWindowsNodeFromWsl,
|
||||||
|
formatWindowsNodeFromWslMessage,
|
||||||
|
};
|
||||||
|
|
@ -10,7 +10,7 @@ Before running inference-based validation, run the deterministic validator:
|
||||||
node tools/validate-skills.js --json path/to/skill-dir
|
node tools/validate-skills.js --json path/to/skill-dir
|
||||||
```
|
```
|
||||||
|
|
||||||
This checks 12 rules deterministically: SKILL-01, SKILL-02, SKILL-03, SKILL-04, SKILL-05, SKILL-06, SKILL-07, PATH-02, STEP-01, STEP-06, STEP-07, SEQ-02.
|
This checks 13 rules deterministically: SKILL-01, SKILL-02, SKILL-03, SKILL-04, SKILL-05, SKILL-06, SKILL-07, PATH-02, STEP-01, STEP-06, STEP-07, SEQ-02, TPL-01.
|
||||||
|
|
||||||
Review its JSON output. For any rule that produced **zero findings** in the first pass, **skip it** during inference-based validation below — it has already been verified. If a rule produced any findings, the inference validator should still review that rule (some rules like SKILL-04 and SKILL-06 have sub-checks that benefit from judgment). Focus your inference effort on the remaining rules that require judgment (PATH-01, PATH-03, PATH-04, PATH-05, WF-03, STEP-02, STEP-03, STEP-04, STEP-05, SEQ-01, REF-01, REF-02, REF-03).
|
Review its JSON output. For any rule that produced **zero findings** in the first pass, **skip it** during inference-based validation below — it has already been verified. If a rule produced any findings, the inference validator should still review that rule (some rules like SKILL-04 and SKILL-06 have sub-checks that benefit from judgment). Focus your inference effort on the remaining rules that require judgment (PATH-01, PATH-03, PATH-04, PATH-05, WF-03, STEP-02, STEP-03, STEP-04, STEP-05, SEQ-01, REF-01, REF-02, REF-03).
|
||||||
|
|
||||||
|
|
@ -253,6 +253,16 @@ If no findings are generated (from either pass), the skill passes validation.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### TPL-01 — Template Files Must Not Contain Compile-Time Substitutions
|
||||||
|
|
||||||
|
- **Severity:** HIGH
|
||||||
|
- **Applies to:** `.md` files whose name contains `template` (case-insensitive)
|
||||||
|
- **Rule:** Template files seed durable, version-controlled artifacts (e.g. spec files) that execute on other machines. A `{{.var}}` compile-time substitution would be baked at render time and freeze a machine-local value into every artifact produced from the template.
|
||||||
|
- **Detection:** Regex `\{\{\.\w+\}\}` match anywhere in a file whose basename matches `/template/i`.
|
||||||
|
- **Fix:** Remove the `{{.var}}` reference. Use single-curly `{var}` if the value should be resolved at LLM runtime by the consumer of the generated artifact.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### REF-01 — Variable References Must Be Defined
|
### REF-01 — Variable References Must Be Defined
|
||||||
|
|
||||||
- **Severity:** HIGH
|
- **Severity:** HIGH
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,7 @@ function escapeTableCell(str) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Path prefixes/patterns that only exist in installed structure, not in source
|
// Path prefixes/patterns that only exist in installed structure, not in source
|
||||||
const INSTALL_ONLY_PATHS = ['_config/', 'custom/'];
|
const INSTALL_ONLY_PATHS = ['_config/', 'custom/', 'render/bmad-quick-dev/'];
|
||||||
|
|
||||||
// Files that are generated at install time and don't exist in the source tree
|
// Files that are generated at install time and don't exist in the source tree
|
||||||
const INSTALL_GENERATED_FILES = ['config.yaml', 'config.user.yaml'];
|
const INSTALL_GENERATED_FILES = ['config.yaml', 'config.user.yaml'];
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
* - STEP-06: step frontmatter has no name/description
|
* - STEP-06: step frontmatter has no name/description
|
||||||
* - STEP-07: step count 2-10
|
* - STEP-07: step count 2-10
|
||||||
* - SEQ-02: no time estimates
|
* - SEQ-02: no time estimates
|
||||||
|
* - TPL-01: template files must not contain compile-time {{.var}} substitutions
|
||||||
*
|
*
|
||||||
* Usage:
|
* Usage:
|
||||||
* node tools/validate-skills.js # All skills, human-readable
|
* node tools/validate-skills.js # All skills, human-readable
|
||||||
|
|
@ -43,6 +44,8 @@ const positionalArgs = args.filter((a) => !a.startsWith('--'));
|
||||||
const NAME_REGEX = /^bmad-[a-z0-9]+(-[a-z0-9]+)*$/;
|
const NAME_REGEX = /^bmad-[a-z0-9]+(-[a-z0-9]+)*$/;
|
||||||
const STEP_FILENAME_REGEX = /^step-\d{2}[a-z]?-[a-z0-9-]+\.md$/;
|
const STEP_FILENAME_REGEX = /^step-\d{2}[a-z]?-[a-z0-9-]+\.md$/;
|
||||||
const TIME_ESTIMATE_PATTERNS = [/takes?\s+\d+\s*min/i, /~\s*\d+\s*min/i, /estimated\s+time/i, /\bETA\b/];
|
const TIME_ESTIMATE_PATTERNS = [/takes?\s+\d+\s*min/i, /~\s*\d+\s*min/i, /estimated\s+time/i, /\bETA\b/];
|
||||||
|
const TEMPLATE_FILENAME_REGEX = /template/i;
|
||||||
|
const COMPILE_TIME_SUB_REGEX = /\{\{\.\w+\}\}/;
|
||||||
|
|
||||||
const SEVERITY_ORDER = { CRITICAL: 0, HIGH: 1, MEDIUM: 2, LOW: 3 };
|
const SEVERITY_ORDER = { CRITICAL: 0, HIGH: 1, MEDIUM: 2, LOW: 3 };
|
||||||
|
|
||||||
|
|
@ -530,6 +533,36 @@ function validateSkill(skillDir) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- TPL-01: template files must not contain compile-time {{.var}} substitutions ---
|
||||||
|
// Template files seed durable, version-controlled artifacts (spec files) that
|
||||||
|
// execute on other machines. Baking a {{.var}} at render time would freeze a
|
||||||
|
// machine-local value into every downstream artifact.
|
||||||
|
for (const filePath of allFiles) {
|
||||||
|
if (path.extname(filePath) !== '.md') continue;
|
||||||
|
const base = path.basename(filePath);
|
||||||
|
if (!TEMPLATE_FILENAME_REGEX.test(base)) continue;
|
||||||
|
|
||||||
|
const relFile = path.relative(skillDir, filePath);
|
||||||
|
const content = safeReadFile(filePath, findings, relFile);
|
||||||
|
if (content === null) continue;
|
||||||
|
|
||||||
|
const lines = content.split('\n');
|
||||||
|
for (const [i, line] of lines.entries()) {
|
||||||
|
const match = line.match(COMPILE_TIME_SUB_REGEX);
|
||||||
|
if (match) {
|
||||||
|
findings.push({
|
||||||
|
rule: 'TPL-01',
|
||||||
|
title: 'Template files must not contain compile-time substitutions',
|
||||||
|
severity: 'HIGH',
|
||||||
|
file: relFile,
|
||||||
|
line: i + 1,
|
||||||
|
detail: `Template file contains compile-time substitution \`${match[0]}\` — this would be baked at render time and leak a machine-local value into every spec produced from the template.`,
|
||||||
|
fix: 'Remove the `{{.var}}` reference. Use single-curly `{var}` if the value should be resolved at LLM runtime by the consumer of the generated spec.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return findings;
|
return findings;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
============================================ */
|
============================================ */
|
||||||
:root {
|
:root {
|
||||||
--ai-banner-height: 2.75rem;
|
--ai-banner-height: 2.75rem;
|
||||||
--sl-nav-height: 6.25rem; /* Base nav height (~3.5rem) + banner height (2.75rem) */
|
--sl-nav-height: 9rem; /* Base nav (3.5rem) + two announcement banners (2.75rem each) */
|
||||||
|
|
||||||
/* Full-width content - override Starlight's default 45rem/67.5rem */
|
/* Full-width content - override Starlight's default 45rem/67.5rem */
|
||||||
--sl-content-width: 65rem;
|
--sl-content-width: 65rem;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue