bmad-method 6.8.1-next.10 → 6.8.1-next.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "bmad-method",
4
- "version": "6.8.1-next.10",
4
+ "version": "6.8.1-next.12",
5
5
  "description": "Breakthrough Method of Agile AI-driven Development",
6
6
  "keywords": [
7
7
  "agile",
@@ -6,7 +6,7 @@ description: 'Produce the architecture: a lean spine of invariants that keeps ev
6
6
 
7
7
  ## Overview
8
8
 
9
- 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.
9
+ 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.
10
10
 
11
11
  One test decides what belongs:
12
12
 
@@ -18,17 +18,17 @@ Record decisions, not rationale (rationale lives in the memlog). Carry shape in
18
18
 
19
19
  ## How you work
20
20
 
21
- 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.
21
+ 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.
22
22
 
23
- 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.
23
+ 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.
24
24
 
25
- 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.
25
+ 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.
26
26
 
27
27
  ## Read the input to know the job
28
28
 
29
- 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.
29
+ 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.
30
30
 
31
- **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.
31
+ **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.
32
32
 
33
33
  ## How a run works
34
34
 
@@ -38,7 +38,6 @@ Writes go through the shared script (don't read the file back except on resume):
38
38
 
39
39
  - `python3 {project-root}/_bmad/scripts/memlog.py init --workspace {doc_workspace} --field scope="…" --field purpose="…" --field altitude="…"`
40
40
  - `python3 {project-root}/_bmad/scripts/memlog.py append --workspace {doc_workspace} --type <decision|constraint|version|assumption|question|direction|event> --text "…"`
41
- - A terminal moment (spine finalized, a validation verdict) is an `append --type event` entry — there is no status field to set.
42
41
 
43
42
  ## Resolution rules
44
43
 
@@ -49,7 +48,7 @@ Writes go through the shared script (don't read the file back except on resume):
49
48
 
50
49
  ## On Activation
51
50
 
52
- **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.
51
+ **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`.
53
52
 
54
53
  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.
55
54
  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
62
61
 
63
62
  ## Reviewer Gate
64
63
 
65
- 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.
64
+ 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.
66
65
 
67
66
  ## Finalize
68
67
 
69
68
  Walk the sequence; reviewer fixes land before polish.
70
69
 
71
- 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).
70
+ 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.
72
71
  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.
73
72
  3. **Reviewer pass.** Run the Reviewer Gate (`references/reviewer-gate.md`). Resolve before polish.
74
73
  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.
79
78
 
80
79
  ## Update
81
80
 
82
- 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.
81
+ 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.
83
82
 
84
83
  ## Validate
85
84
 
@@ -8,106 +8,58 @@ scope: '{what this spine governs}'
8
8
  status: draft # draft · final
9
9
  created: '{date}'
10
10
  updated: '{date}'
11
- stack: # SEED verified current at authoring; the code owns this once it exists
12
- languages: []
13
- frameworks: []
14
- key_deps: [] # name@version
15
- binds: [] # capability / unit IDs governed (from the driving spec; at epic altitude, also the parent AD IDs inherited)
11
+ binds: [] # capability / unit IDs governed (from the driving spec; at epic altitude, also the inherited parent AD ids)
16
12
  sources: []
17
13
  companions: []
18
14
  ---
19
15
 
20
16
  # Architecture Spine — {name}
21
17
 
22
- > A consistency contract, not a design document. It fixes the **invariants** that keep the
23
- > independently-built level below ({features | epics | stories}) coherent — the durable rules a
24
- > clean codebase can't reveal. Structure is **seed**: the code owns the detail, the spine keeps the shape.
25
- > Decisions, not rationale (that lives in the memlog). Diagrams over prose.
26
- >
27
- > **Scale to the job — drop any section a project doesn't need.** A small intent may be just a
28
- > paradigm + a few `AD`s + conventions, seed omitted; a platform earns the full set. An inherited
29
- > epic spine is usually mostly Inherited Invariants + a thin Deferred. Empty sections are cut, not left as headers.
18
+ <!-- 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. -->
30
19
 
31
20
  ## Design Paradigm
32
21
 
33
- Name the pattern a known one loads a whole model for free and map its layers to namespaces /
34
- directories. The smallest, most durable thing in the file.
22
+ <!-- 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. -->
35
23
 
36
24
  ## Inherited Invariants
37
25
 
38
- Present only when this spine inherits a parent at a higher altitude (e.g. an epic spine under a
39
- feature/initiative spine). The parent's `AD`s, conventions, and paradigm that bind here, listed by
40
- their original parent IDs — **read-only, never renumbered, not re-derived**. This spine adds only
41
- what the parent left open; anything here that a local decision would contradict is a conflict to
42
- surface, not override.
26
+ <!-- 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. -->
43
27
 
44
28
  | Inherited | From parent | Binds here |
45
29
  | --- | --- | --- |
46
- | {AD-n / convention} | {parent spine} | {what it constrains in this scope} |
30
+ | {AD-id / convention} | {parent spine} | {what it constrains in this scope} |
47
31
 
48
32
  ## Invariants & Rules
49
33
 
50
- The durable heart: the calls a future builder can't read from compliant code. Each `AD-n` has a
51
- stable ID (never reused), a binding scope, the divergence it prevents, and an enforceable rule.
52
- Cover the boundary/dependency rules (who may depend on whom) and how state is mutated — a
53
- dependency-direction diagram says these better than prose. An `AD-n` the user asserted as
54
- already-settled (or one verified from existing reality) carries an `[ADOPTED]` tag after its
55
- title, so its provenance is legible versus decisions made here.
56
-
57
- ```mermaid
58
- flowchart LR
59
- %% arrows = allowed dependency direction (a rule, not just structure)
60
- ```
34
+ <!-- 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. -->
61
35
 
62
36
  ### AD-1 — {decision}
63
37
 
64
- - **Binds:** {capability / unit IDs, areas, or `all`}
38
+ - **Binds:** {capability / unit ids / fr/nfr's, areas, or `all`}
65
39
  - **Prevents:** {the divergence this stops}
66
40
  - **Rule:** {the constraint downstream must follow}
67
41
 
68
42
  ## Consistency Conventions
69
43
 
70
- The defaults that bind everything where independent builders would otherwise drift. Cut rows that
71
- don't apply.
44
+ <!-- Defaults that bind where independent builders would drift. Cut rows that don't apply; add rows the project needs. -->
72
45
 
73
46
  | Concern | Convention |
74
47
  | --- | --- |
75
48
  | Naming (entities, files, interfaces, events) | |
76
- | Data & formats (IDs, dates, error shapes, envelopes) | |
49
+ | Data & formats (ids, dates, error shapes, envelopes) | |
77
50
  | State & cross-cutting (mutation, errors, logging, config, auth) | |
78
51
 
79
- ## Structural Seed
52
+ ## Stack
80
53
 
81
- Cold-start scaffolding, kept minimal include an item only where its shape is non-obvious at this
82
- altitude (at epic altitude the parent usually already fixed it, so the seed is often empty). The code
83
- owns the **detail** (every file, every column); once code exists it becomes the source of truth for
84
- detail, and this seed is a starting scaffold, not a mirror to maintain against it. Evolve a seed item
85
- only when the **shape** itself changes — a new container, a new core entity, a stack bump — and let
86
- the memlog keep the history.
87
-
88
- - **Stack & Versions** — the substrate (mirrors frontmatter `stack`).
89
- - **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.
90
- - **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).
91
- - **Project Structure** — a minimal source tree, only as deep as consistency needs.
92
-
93
- ```mermaid
94
- flowchart TD
95
- user(["{actor}"])
96
- subgraph sys["{system boundary}"]
97
- a["{container}<br/>{tech} — {role}"]
98
- end
99
- db[("{datastore}")]
100
- ext["{external system}"]
101
- user --> a
102
- a --> db
103
- a -->|{via port}| ext
104
- ```
54
+ <!-- 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. -->
105
55
 
106
- ```mermaid
107
- erDiagram
108
- ENTITY_A ||--o{ ENTITY_B : "{relationship}"
109
- ENTITY_B ||--o| ENTITY_C : "{relationship}"
110
- ```
56
+ | Name | Version |
57
+ | --- | --- |
58
+ | {language / framework / key dep / platform / chain} | {pinned version} |
59
+
60
+ ## Structural Seed
61
+
62
+ <!-- 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. -->
111
63
 
112
64
  ```text
113
65
  {root}/
@@ -116,14 +68,12 @@ erDiagram
116
68
 
117
69
  ## Capability → Architecture Map
118
70
 
119
- Bridges the spec's capabilities to the architecture (and is the consistency auditor's checklist).
120
- Present when a spec drove this run.
71
+ <!-- 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. -->
121
72
 
122
73
  | Capability / Area | Lives in | Governed by |
123
74
  | --- | --- | --- |
124
- | {CAP-n / area} | {component / module} | {AD-n, convention, paradigm} |
75
+ | {CAP-id / area} | {component / module} | {AD-id, convention, paradigm} |
125
76
 
126
77
  ## Deferred
127
78
 
128
- Decisions intentionally pushed down, each with the reason it can wait. The half of the contract
129
- that keeps the spine lean.
79
+ <!-- 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. -->
@@ -2,12 +2,12 @@
2
2
 
3
3
  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.
4
4
 
5
- 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.
5
+ 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.
6
6
 
7
7
  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.)
8
8
 
9
- 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.
9
+ 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.
10
10
 
11
- **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.
11
+ **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.
12
12
 
13
13
  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
13
13
  - placeholder literal TBD / TODO / "similar to AD-n" / unfilled {template-token}
14
14
  - ad_id duplicate or non-monotonic AD-n identifiers
15
15
  - ad_fields an AD-n block missing Binds / Prevents / Rule
16
- - version_pin a frontmatter key_deps entry with no @version
16
+ - version_pin a ## Stack table row with no version
17
17
 
18
18
  Fenced code blocks are blanked (replaced with equal-count blank lines) before scanning, so
19
19
  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]:
150
150
  return findings
151
151
 
152
152
 
153
- def find_unpinned_deps(frontmatter: str) -> list[dict]:
153
+ def find_unpinned_stack(body: str, offset: int) -> list[dict]:
154
+ """Flag a `## Stack` table row that names something but leaves its version blank or a
155
+ placeholder. Pinning lives in the body table now, not frontmatter. A row whose name is
156
+ still a `{token}` skeleton is left to the placeholder pass, not double-reported here.
157
+
158
+ Fences are blanked first (like find_placeholders / find_ad_issues), so a pipe-row or
159
+ heading inside a code block is never read as live Stack content. The heading match is
160
+ `## Stack` with a word boundary, so a renamed heading (`## Stack & Versions`) still
161
+ counts. Name and Version columns are located from the header row, so a reordered table
162
+ pairs name to version correctly; both default to the canonical positions (0, 1)."""
154
163
  findings: list[dict] = []
155
- lines = frontmatter.splitlines()
156
- in_key_deps = False
157
- key_indent = 0
158
- for raw in lines:
159
- stripped = raw.strip()
160
- if not stripped or stripped.startswith("#"):
164
+ in_stack = False
165
+ header_seen = False
166
+ name_idx, ver_idx = 0, 1
167
+ scan = blank_fences(body)
168
+ for i, raw in enumerate(scan.splitlines()):
169
+ if HEADING.match(raw):
170
+ in_stack = re.match(r"^##\s+Stack\b", raw) is not None
171
+ header_seen = False
172
+ name_idx, ver_idx = 0, 1
173
+ continue
174
+ if not in_stack or not raw.lstrip().startswith("|"):
175
+ continue
176
+ if set(raw.strip()) <= set("|-: "):
177
+ continue # separator row
178
+ cells = _table_cells(raw)
179
+ if not header_seen:
180
+ header_seen = True
181
+ for j, c in enumerate(cells):
182
+ if c.lower() == "name":
183
+ name_idx = j
184
+ elif c.lower() == "version":
185
+ ver_idx = j
161
186
  continue
162
- indent = len(raw) - len(raw.lstrip())
163
- m = re.match(r"key_deps:\s*(.*)$", stripped)
164
- if m:
165
- in_key_deps = True
166
- key_indent = indent
167
- inline = _strip_comment(m.group(1)).strip()
168
- if inline and inline not in ("[]", "[ ]"):
169
- # inline list form: key_deps: [a@1, b] — consumed here, no block follows
170
- for item in re.findall(r"[^\[\],]+", inline.strip("[]")):
171
- _check_dep(item.strip().strip("'\""), findings)
172
- in_key_deps = False
187
+ name = cells[name_idx] if len(cells) > name_idx else ""
188
+ version = cells[ver_idx] if len(cells) > ver_idx else ""
189
+ if not name or TEMPLATE_TOKEN.search(name):
173
190
  continue
174
- if in_key_deps:
175
- if indent <= key_indent and not stripped.startswith("-"):
176
- in_key_deps = False
177
- continue
178
- if stripped.startswith("-"):
179
- # block-sequence form: `- name@version`
180
- _check_dep(_strip_comment(stripped[1:]).strip().strip("'\""), findings)
181
- else:
182
- # map form: `name: version` — pinned iff a non-empty value is present
183
- mm = re.match(r"([^:]+):\s*(.*)$", stripped)
184
- if mm:
185
- name = mm.group(1).strip().strip("'\"")
186
- val = _strip_comment(mm.group(2)).strip().strip("'\"")
187
- if name and not val:
188
- findings.append({
189
- "category": "version_pin",
190
- "severity": "medium",
191
- "detail": f"key_deps entry {name!r} has no version pin",
192
- "location": f"{SPINE} frontmatter stack.key_deps",
193
- })
191
+ if not version or TEMPLATE_TOKEN.search(version):
192
+ findings.append({
193
+ "category": "version_pin",
194
+ "severity": "medium",
195
+ "detail": f"Stack entry {name!r} has no version",
196
+ "location": f"{SPINE} (line {offset + i + 1})",
197
+ })
194
198
  return findings
195
199
 
196
200
 
197
- def _strip_comment(s: str) -> str:
198
- """Drop a trailing YAML ` # comment`, leaving an inline `name@1.2` intact."""
199
- return re.sub(r"(^|\s)#.*$", "", s)
200
-
201
-
202
- def _check_dep(item: str, findings: list[dict]) -> None:
203
- if not item or item.startswith("#"):
204
- return
205
- if "@" not in item:
206
- findings.append({
207
- "category": "version_pin",
208
- "severity": "medium",
209
- "detail": f"key_deps entry {item!r} has no @version pin",
210
- "location": f"{SPINE} frontmatter stack.key_deps",
211
- })
201
+ def _table_cells(row: str) -> list[str]:
202
+ """Split a markdown table row into trimmed cells, dropping the leading/trailing pipe."""
203
+ s = row.strip()
204
+ if s.startswith("|"):
205
+ s = s[1:]
206
+ if s.endswith("|"):
207
+ s = s[:-1]
208
+ return [c.strip() for c in s.split("|")]
212
209
 
213
210
 
214
211
  def lint(text: str) -> dict:
@@ -217,7 +214,7 @@ def lint(text: str) -> dict:
217
214
  findings += find_frontmatter_placeholders(frontmatter)
218
215
  findings += find_placeholders(body, offset)
219
216
  findings += find_ad_issues(body, offset)
220
- findings += find_unpinned_deps(frontmatter)
217
+ findings += find_unpinned_stack(body, offset)
221
218
  counts: dict[str, int] = {}
222
219
  for f in findings:
223
220
  counts[f["severity"]] = counts.get(f["severity"], 0) + 1
@@ -6,7 +6,7 @@
6
6
 
7
7
  The spine under test: a clean spine lints empty; the linter catches exactly the
8
8
  mechanical defects a prompt is unreliable at — literal placeholders, AD-n id breakage,
9
- AD-n blocks missing required fields, and unpinned dependency versions.
9
+ AD-n blocks missing required fields, and unpinned Stack versions.
10
10
  """
11
11
  import importlib.util
12
12
  import json
@@ -26,10 +26,6 @@ _SPEC.loader.exec_module(lint_spine)
26
26
 
27
27
  CLEAN = """---
28
28
  name: 'Demo'
29
- stack:
30
- key_deps:
31
- - fastapi@0.115
32
- - pydantic@2.9
33
29
  ---
34
30
 
35
31
  ## Invariants & Rules
@@ -50,6 +46,13 @@ stack:
50
46
  flowchart LR
51
47
  A --> B{decision}
52
48
  ```
49
+
50
+ ## Stack
51
+
52
+ | Name | Version |
53
+ | --- | --- |
54
+ | fastapi | 0.115 |
55
+ | pydantic | 2.9 |
53
56
  """
54
57
 
55
58
 
@@ -108,30 +111,32 @@ def test_missing_field_caught():
108
111
 
109
112
 
110
113
  def test_unpinned_dep_caught():
111
- text = CLEAN.replace("- fastapi@0.115", "- fastapi")
114
+ text = CLEAN.replace("| fastapi | 0.115 |", "| fastapi | |")
112
115
  result = lint_spine.lint(text)
113
116
  assert "version_pin" in cats(result)
114
117
 
115
118
 
116
- def test_inline_key_deps_unpinned():
117
- text = CLEAN.replace(" key_deps:\n - fastapi@0.115\n - pydantic@2.9", " key_deps: [fastapi, redis@7]")
119
+ def test_placeholder_version_caught():
120
+ text = CLEAN.replace("| fastapi | 0.115 |", "| fastapi | {pin} |")
118
121
  result = lint_spine.lint(text)
119
- pins = [f for f in result["findings"] if f["category"] == "version_pin"]
120
- assert len(pins) == 1 and "fastapi" in pins[0]["detail"]
122
+ assert any(f["category"] == "version_pin" and "fastapi" in f["detail"] for f in result["findings"])
121
123
 
122
124
 
123
- def test_empty_key_deps_ok():
124
- text = CLEAN.replace(" key_deps:\n - fastapi@0.115\n - pydantic@2.9", " key_deps: []")
125
+ def test_no_stack_section_ok():
126
+ text = CLEAN.split("## Stack")[0]
125
127
  result = lint_spine.lint(text)
126
128
  assert "version_pin" not in cats(result)
127
129
 
128
130
 
129
- def test_yaml_comments_not_parsed_as_deps():
130
- # a SEED comment on the key_deps line must not read as an unpinned dependency
131
- text = CLEAN.replace(
132
- " key_deps:\n - fastapi@0.115\n - pydantic@2.9",
133
- " key_deps: # SEED verified current 2026-06\n - fastapi@0.115 # web framework",
134
- )
131
+ def test_stack_skeleton_row_not_version_pinned():
132
+ # a leftover {token} name is the placeholder pass's job, not a double-reported version_pin
133
+ text = CLEAN.replace("| fastapi | 0.115 |", "| {language / framework} | {pinned version} |")
134
+ result = lint_spine.lint(text)
135
+ assert "version_pin" not in cats(result)
136
+
137
+
138
+ def test_stack_html_comment_not_parsed_as_row():
139
+ text = CLEAN.replace("## Stack\n", "## Stack\n\n<!-- SEED — verified current 2026-06 -->\n")
135
140
  result = lint_spine.lint(text)
136
141
  assert "version_pin" not in cats(result)
137
142
 
@@ -153,7 +158,8 @@ def test_no_frontmatter_body_still_scanned():
153
158
 
154
159
  def test_frontmatter_value_with_dashes_not_truncated():
155
160
  # a value containing '---' must not be read as the closing fence (line-exact close)
156
- text = "---\nscope: 'phase 1 --- phase 2'\nstack:\n key_deps:\n - fastapi\n---\n\n## Invariants\n"
161
+ text = ("---\nname: 'x'\nscope: 'phase 1 --- phase 2'\n---\n\n"
162
+ "## Stack\n\n| Name | Version |\n| --- | --- |\n| fastapi | |\n")
157
163
  result = lint_spine.lint(text)
158
164
  assert any(f["category"] == "version_pin" for f in result["findings"]) # read past the inline ---
159
165
 
@@ -168,19 +174,55 @@ def test_ad_heading_in_fence_not_counted():
168
174
  assert result["ok"] is True # the fenced AD-2 is not a live AD → no ad_fields/ad_id finding
169
175
 
170
176
 
171
- def test_map_form_key_deps_unpinned_caught():
172
- text = "---\nstack:\n key_deps:\n fastapi: '0.115'\n redis:\n---\n\n## Invariants\n"
177
+ def test_stack_table_flags_only_the_unpinned_row():
178
+ text = ("---\nname: 'x'\n---\n\n## Stack\n\n| Name | Version |\n| --- | --- |\n"
179
+ "| fastapi | 0.115 |\n| redis | |\n")
173
180
  result = lint_spine.lint(text)
174
181
  pins = [f for f in result["findings"] if f["category"] == "version_pin"]
175
182
  assert len(pins) == 1 and "redis" in pins[0]["detail"]
176
183
 
177
184
 
178
- def test_map_form_key_deps_pinned_ok():
179
- text = "---\nstack:\n key_deps:\n fastapi: '0.115'\n---\n\n## Invariants\n"
185
+ def test_stack_table_all_pinned_ok():
186
+ text = ("---\nname: 'x'\n---\n\n## Stack\n\n| Name | Version |\n| --- | --- |\n"
187
+ "| fastapi | 0.115 |\n")
188
+ result = lint_spine.lint(text)
189
+ assert "version_pin" not in cats(result)
190
+
191
+
192
+ def test_fenced_stack_rows_not_parsed():
193
+ # an illustrative fenced table under ## Stack must not be read as live rows (fences are
194
+ # blanked first, like every other pass) — a blank-version row inside a fence is not a finding
195
+ text = ("---\nname: 'x'\n---\n\n## Stack\n\n| Name | Version |\n| --- | --- |\n"
196
+ "| fastapi | 0.115 |\n\n```text\n| example | |\n```\n")
197
+ result = lint_spine.lint(text)
198
+ assert "version_pin" not in cats(result)
199
+
200
+
201
+ def test_fenced_stack_heading_not_live():
202
+ # a `## Stack` heading shown inside a code fence is not the live Stack section
203
+ text = ("---\nname: 'x'\n---\n\n## Docs\n\n```md\n## Stack\n\n| foo | |\n```\n")
180
204
  result = lint_spine.lint(text)
181
205
  assert "version_pin" not in cats(result)
182
206
 
183
207
 
208
+ def test_renamed_stack_heading_still_scanned():
209
+ # the heading match is word-boundary, so a varied `## Stack` heading still counts
210
+ text = ("---\nname: 'x'\n---\n\n## Stack & Versions\n\n| Name | Version |\n| --- | --- |\n"
211
+ "| redis | |\n")
212
+ result = lint_spine.lint(text)
213
+ pins = [f for f in result["findings"] if f["category"] == "version_pin"]
214
+ assert len(pins) == 1 and "redis" in pins[0]["detail"]
215
+
216
+
217
+ def test_reordered_columns_pair_name_to_version():
218
+ # Version-then-Name header: the unpinned row must still be flagged by its real name
219
+ text = ("---\nname: 'x'\n---\n\n## Stack\n\n| Version | Name |\n| --- | --- |\n"
220
+ "| 0.115 | fastapi |\n| | redis |\n")
221
+ result = lint_spine.lint(text)
222
+ pins = [f for f in result["findings"] if f["category"] == "version_pin"]
223
+ assert len(pins) == 1 and "redis" in pins[0]["detail"]
224
+
225
+
184
226
  def test_placeholder_line_number_is_absolute():
185
227
  # a TBD after a multi-line fence reports its real file line (fence blanked, not collapsed)
186
228
  text = (
@@ -75,6 +75,9 @@ module.exports = {
75
75
  return;
76
76
  }
77
77
 
78
+ const { checkWindowsNodeFromWsl } = require('../core/wsl-node-check');
79
+ await checkWindowsNodeFromWsl();
80
+
78
81
  // Set debug flag as environment variable for all components
79
82
  if (options.debug) {
80
83
  process.env.BMAD_DEBUG_MANIFEST = 'true';
@@ -0,0 +1,109 @@
1
+ const prompts = require('../prompts');
2
+
3
+ const WSL_UNC_PATTERN = /^\\\\wsl(?:\.localhost|\$)?\\/i;
4
+
5
+ function normalizePath(value) {
6
+ return typeof value === 'string' ? value.replaceAll('/', '\\').toLowerCase() : '';
7
+ }
8
+
9
+ function isLinuxStylePath(value) {
10
+ return (
11
+ typeof value === 'string' &&
12
+ value.startsWith('/') &&
13
+ !value.startsWith('//') &&
14
+ !/^\/[a-z](?:\/|$)/i.test(value) &&
15
+ !/^\/cygdrive\/[a-z](?:\/|$)/i.test(value)
16
+ );
17
+ }
18
+
19
+ function isWslUncPath(value) {
20
+ return WSL_UNC_PATTERN.test(value || '');
21
+ }
22
+
23
+ /**
24
+ * Detect the broken interop case where WSL resolved node/npx to Windows.
25
+ * @param {Object} [runtime]
26
+ * @param {string} [runtime.platform]
27
+ * @param {Object} [runtime.env]
28
+ * @param {string} [runtime.cwd]
29
+ * @param {string} [runtime.execPath]
30
+ * @returns {{isMismatch: boolean, reason: string|null, execPath: string}}
31
+ */
32
+ function detectWindowsNodeFromWsl(runtime = {}) {
33
+ const platform = runtime.platform || process.platform;
34
+ const env = runtime.env || process.env;
35
+ const cwd = runtime.cwd || safeCwd();
36
+ const execPath = runtime.execPath || process.execPath || '';
37
+
38
+ if (platform !== 'win32') {
39
+ return { isMismatch: false, reason: null, execPath };
40
+ }
41
+
42
+ if (env.WSL_DISTRO_NAME) {
43
+ return { isMismatch: true, reason: 'WSL_DISTRO_NAME is set', execPath };
44
+ }
45
+
46
+ if (env.WSL_INTEROP) {
47
+ return { isMismatch: true, reason: 'WSL_INTEROP is set', execPath };
48
+ }
49
+
50
+ if (isLinuxStylePath(env.PWD)) {
51
+ return { isMismatch: true, reason: 'PWD is a Linux path', execPath };
52
+ }
53
+
54
+ if (isWslUncPath(cwd)) {
55
+ return { isMismatch: true, reason: 'current directory is a WSL UNC path', execPath };
56
+ }
57
+
58
+ const normalizedExecPath = normalizePath(execPath);
59
+ if (normalizedExecPath.includes('\\wsl$\\') || normalizedExecPath.includes('\\wsl.localhost\\')) {
60
+ return { isMismatch: true, reason: 'Node executable path is under a WSL UNC path', execPath };
61
+ }
62
+
63
+ return { isMismatch: false, reason: null, execPath };
64
+ }
65
+
66
+ function safeCwd() {
67
+ try {
68
+ return process.cwd();
69
+ } catch {
70
+ return '';
71
+ }
72
+ }
73
+
74
+ function formatWindowsNodeFromWslMessage(detection) {
75
+ const lines = [
76
+ 'Windows Node.js was launched from a WSL shell.',
77
+ '',
78
+ 'This usually means Node.js is not installed inside the WSL distro, so WSL resolved `node`/`npx` to Windows.',
79
+ 'The installer cannot safely continue because Linux paths may be interpreted as Windows paths.',
80
+ '',
81
+ 'Install Node.js inside WSL, then rerun the same command from the WSL terminal.',
82
+ ];
83
+
84
+ if (detection.execPath) {
85
+ lines.push('', `Detected Node executable: ${detection.execPath}`);
86
+ }
87
+
88
+ if (detection.reason) {
89
+ lines.push(`Detection signal: ${detection.reason}`);
90
+ }
91
+
92
+ return lines.join('\n');
93
+ }
94
+
95
+ async function checkWindowsNodeFromWsl() {
96
+ const detection = module.exports.detectWindowsNodeFromWsl();
97
+ if (!detection.isMismatch) {
98
+ return detection;
99
+ }
100
+
101
+ await prompts.log.error(formatWindowsNodeFromWslMessage(detection));
102
+ process.exit(1);
103
+ }
104
+
105
+ module.exports = {
106
+ checkWindowsNodeFromWsl,
107
+ detectWindowsNodeFromWsl,
108
+ formatWindowsNodeFromWslMessage,
109
+ };