ma-agents 3.5.5 → 3.6.0

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.
Files changed (56) hide show
  1. package/.ma-agents.json +10 -0
  2. package/AGENTS.md +97 -0
  3. package/MANIFEST.yaml +3 -0
  4. package/README.md +17 -0
  5. package/_bmad-output/implementation-artifacts/21-10-profile-reconfigure.md +30 -6
  6. package/_bmad-output/implementation-artifacts/21-11-profile-uninstall.md +2 -1
  7. package/_bmad-output/implementation-artifacts/21-2-universal-instruction-block-expansion.md +217 -62
  8. package/_bmad-output/implementation-artifacts/21-3-roomodes-template-bmad-modes.md +196 -73
  9. package/_bmad-output/implementation-artifacts/21-4-agents-md-template-opencode.md +242 -53
  10. package/_bmad-output/implementation-artifacts/21-5-clinerules-template-extension.md +180 -41
  11. package/_bmad-output/implementation-artifacts/21-6-onprem-layered-guardrails.md +250 -75
  12. package/_bmad-output/implementation-artifacts/21-7-bmad-persona-phase-prefix.md +221 -89
  13. package/_bmad-output/implementation-artifacts/21-8-vllm-reference-doc-readme.md +121 -63
  14. package/_bmad-output/implementation-artifacts/21-9-tests-validation.md +332 -61
  15. package/_bmad-output/implementation-artifacts/bug-bmad-recompile-fails-on-airgapped-network.md +112 -0
  16. package/_bmad-output/implementation-artifacts/sprint-status.yaml +3 -2
  17. package/bin/cli.js +59 -0
  18. package/docs/deployment/vllm-nemotron.md +130 -0
  19. package/lib/agents.js +17 -2
  20. package/lib/bmad-customize/bmm-analyst.customize.yaml +8 -0
  21. package/lib/bmad-customize/bmm-architect.customize.yaml +2 -0
  22. package/lib/bmad-customize/bmm-dev.customize.yaml +2 -0
  23. package/lib/bmad-customize/bmm-pm.customize.yaml +2 -0
  24. package/lib/bmad-customize/bmm-qa.customize.yaml +2 -0
  25. package/lib/bmad-customize/bmm-quick-flow-solo-dev.customize.yaml +8 -0
  26. package/lib/bmad-customize/bmm-sm.customize.yaml +2 -0
  27. package/lib/bmad-customize/bmm-tech-writer.customize.yaml +2 -0
  28. package/lib/bmad-customize/bmm-ux-designer.customize.yaml +2 -0
  29. package/lib/bmad.js +293 -1
  30. package/lib/installer.js +617 -43
  31. package/lib/merge/roomodes.js +125 -0
  32. package/lib/profile.js +25 -2
  33. package/lib/reconfigure.js +334 -0
  34. package/lib/templates/agents-md.template.md +67 -0
  35. package/lib/templates/clinerules.template.md +13 -0
  36. package/lib/templates/instruction-block-onprem.template.md +86 -0
  37. package/lib/templates/instruction-block-universal.template.md +29 -0
  38. package/lib/templates/roomodes.template.yaml +96 -0
  39. package/lib/uninstall.js +314 -0
  40. package/package.json +4 -3
  41. package/test/agents-md.test.js +398 -0
  42. package/test/bmad-extension.test.js +2 -2
  43. package/test/bmad-persona-phase-prefix.test.js +271 -0
  44. package/test/clinerules.test.js +339 -0
  45. package/test/instruction-block.test.js +388 -0
  46. package/test/integration-verification.test.js +2 -2
  47. package/test/migration-validation.test.js +2 -2
  48. package/test/offline-recompile.test.js +237 -0
  49. package/test/onprem-injection.test.js +425 -32
  50. package/test/onprem-layer.test.js +419 -0
  51. package/test/reconfigure.test.js +436 -0
  52. package/test/roomodes.test.js +343 -0
  53. package/test/uninstall.test.js +402 -0
  54. package/_bmad-output/methodology/BMAD_AI_Development_Training.pptx +0 -0
  55. package/_bmad-output/methodology/version.json +0 -7
  56. package/docs/BMAD_AI_Development_Training.pptx +0 -0
@@ -1,6 +1,15 @@
1
1
  # Story 21.9: Tests and Validation
2
2
 
3
- Status: backlog
3
+ Status: Draft
4
+
5
+ ### Blockers (why not Ready)
6
+
7
+ Tasks are NOT unconditional — they branch on upstream artifacts and surfaced gaps. Story is Draft until blockers below clear:
8
+
9
+ 1. **Upstream artifacts not on disk.** Stories 21.2, 21.3, 21.4, 21.5, 21.6, 21.7, 21.8 are all **status: backlog** per sprint-status.yaml. Task 1.2–1.8 explicitly HALT this story when prerequisites are missing. Ready requires at minimum 21.2, 21.3, 21.4, 21.5, 21.6, 21.7 merged (21.8 is doc-only and soft; 21.10 is soft).
10
+ 2. **AC #6 implementation-path conditional.** Round-trip test path branches on Story 21.10 availability (reconfigure subcommand vs. direct `.ma-agents.json` edit). Resolve once 21.10 status is known.
11
+ 3. **Open question on Story 21.7 loader branch (A vs B).** BF-O baseline capture shape differs between Branch A (post-deploy rewrite) and Branch B (dual-variant files). Cannot finalize Task 4.3 fixture layout until 21.7 lands its Branch decision in its Change Log.
12
+ 4. **Test Coverage Gaps requiring in-story absorption** (see Dev Notes → Test Coverage Gaps). The gaps tagged as in-story-closeable (21.4 AC #5 append branch, 21.4 AC #9 exception, 21.5 AC #2 cross-file identity, 21.5 AC #9 manifest-path substitution, 21.6 AC #12 cross-profile fileRegex equality, 21.7 AC #4/#8 deployed-customize coverage, 21.8 AC #8 README presence) must be folded into Tasks 2/3/4 or filed as follow-up bug stories before promoting to Ready.
4
13
 
5
14
  ## Story
6
15
 
@@ -10,88 +19,350 @@ So that future changes to installer or templates do not silently regress on-prem
10
19
 
11
20
  ## Acceptance Criteria
12
21
 
13
- 1. A consolidated integration test file `test/onprem-injection.test.js` exists covering the cross-story Epic 21 contracts that per-story tests do not own:
14
- - **(a) NFR44 — standard profile cleanliness:** A full standard-profile install on a fresh project produces zero occurrences of the strings `/no_think`, `str_replace_editor`, `~/.claude/`, or `Never create files in` (the on-prem rule prefix) across all generated instruction files (`CLAUDE.md`, `.clinerules`, `.cline/clinerules.md`, `.roo/rules/00-ma-agents.md`, `AGENTS.md`, `.roomodes`).
15
- - **(b) On-prem profile completeness:** A full on-prem-profile install produces all on-prem strings in the expected files (`.roomodes` `customInstructions`, `AGENTS.md`, `.clinerules`, `CLAUDE.md` injection block).
16
- - **(c) NFR46 idempotency:** Two consecutive installs with the same profile produce byte-identical content within ma-agents-owned regions (marker blocks for markdown files, owned slugs for `.roomodes`) across all per-tool files.
17
- - **(d) `.roomodes` slug-collision behavior:** Pre-existing user-defined `customModes` with non-conflicting slugs are preserved through reinstall; conflicting slugs are overwritten with a console warning naming the slug.
18
- - **(e) NFR47 enforcement contract:** The four ma-agents BMAD modes' `fileRegex` patterns reject the expected code-file extensions and accept the expected planning-file extensions. `bmad-architect` regex matrix tested against `.ts`, `.py`, `.js`, `.go` (rejected) and `.md`, `.xml`, `.drawio` (accepted). Same shape of test for `bmad-pm`, `bmad-techlead`.
19
- - **(f) Profile switch round-trip:** Install standard → switch to on-prem (via the Story 21.10 reconfigure command, or — until 21.10 lands — by editing `.ma-agents.json` directly and re-running install) switch back to standard. Final state must match the original standard install (byte-identical for ma-agents-owned regions). User content outside markers preserved across all switches.
20
- 2. The full test suite (`npm test`) passes after Story 21.9 no regressions in any pre-Epic-21 tests.
21
- 3. A short test-coverage table is added to the PR body listing each Epic 21 NFR (44, 45, 46, 47) and the test(s) that cover it. Any uncovered NFR is flagged as an open issue, not silently shipped.
22
- 4. The integration tests use temporary directories (`fs.mkdtempSync(os.tmpdir() + ...)` per test) to avoid touching the repo's own `.ma-agents.json` or instruction files.
23
- 5. **Standard-profile baseline fixture (byte-for-byte).** Commit `test/fixtures/standard-profile-baseline/` containing the expected byte-for-byte generated output for a canonical `npx ma-agents install --yes` run against a canonical empty-project fixture (also committed under `test/fixtures/empty-project/`). At a minimum the baseline fixture includes the rendered `CLAUDE.md`, `.clinerules`, `.roo/rules/00-ma-agents.md`, `AGENTS.md`, `.roomodes`, and `.ma-agents.json`. The test harness runs the installer against a scratch temp dir seeded with the empty-project fixture and diffs the result against this baseline byte-for-byte. Any drift in universal-block content, template rendering, or `.roomodes` rendering surfaces as a test failure pointing at the exact file and diff region. This replaces the vague "diff against pre-Epic-21 baseline" language in NFR44 with a concrete, version-controlled artifact.
24
- 6. **End-to-end installer harness (exercises the actual binary).** The test harness at `test/onprem-injection.test.js` (or a sibling file) exercises the actual installer entry point (`node bin/cli.js install --yes` via `child_process.spawnSync` or equivalent, OR a direct programmatic call to the exported top-level install function that matches what the CLI does — no internal-only helpers) against a scratch tmpdir for BOTH profiles (`standard` and `on-prem`), snapshots the generated filesystem tree, and diffs it against per-profile fixtures (`test/fixtures/standard-profile-baseline/` for standard, `test/fixtures/onprem-profile-baseline/` for on-prem). This is distinct from and additional to — unit-level template-rendering tests. Unit tests can pass while end-to-end install breaks (wiring bugs, file-path bugs, profile-resolution bugs); this AC closes that gap.
22
+ Numbered ACs derive from the Epic 21 Story 21.9 spec (`_bmad-output/planning-artifacts/epics.md` lines 4132–4152). ACs flagged **(gap-fill)** are additions this story introduces to make the epic spec concretely testable — they do not contradict the epic, they operationalize it.
23
+
24
+ 1. A test file `test/onprem-injection.test.js` exists and covers the five sub-cases called out in the epic (lines 4144):
25
+ - **(a)** Standard profile produces NO occurrences of the explicit literal strings `["/no_think", "str_replace_editor", "~/.claude/"]` (EXHAUSTIVE negative assertion set) anywhere in generated instruction files, per Story 21.6 AC #4 scope narrowing — covers NFR44. Reasoning-mode and sampling-parameter prose (e.g., `temperature`, `top_p`) are NOT in this negative set; they are verified positive-side only under the on-prem profile in sub-test (b).
26
+ - **(b)** On-prem profile produces BOTH the universal block content AND the on-prem block content in the expected target files (CLAUDE.md / `.roo/rules/00-ma-agents.md` / `.clinerules` / `.cline/clinerules.md` / `AGENTS.md` / `.roomodes` `customInstructions`).
27
+ - **(c)** Idempotency two consecutive installs with the same profile produce byte-identical marker-block content across all ma-agents-stamped files covers NFR46.
28
+ - **(d)** `.roomodes` slug-collision: the four ma-agents-owned slugs (`bmad-pm`, `bmad-architect`, `bmad-techlead`, `bmad-dev`) overwrite existing entries with the same slug; non-colliding user slugs are preserved byte-for-byte.
29
+ - **(e)** NFR47 enforcement contract: the generated `.roomodes` `fileRegex` patterns reject `.ts` and `.py` paths under `bmad-architect`. The test asserts the regex pattern, not a running Roo Code process (per epic technical note, line 4152).
30
+
31
+ 2. `test/profile.test.js` already exists (delivered by Story 21.1, verified at `D:\Code\agents\test\profile.test.js`); Story 21.9 confirms it covers: `getProfile`, `setProfile`, `resolveProfile` precedence (persisted > yes-default > null), persistence round-trip, and missing-file handling. If any of those are missing, this story adds them rather than creating a second file.
32
+
33
+ 3. After Story 21.9 merges, `npm test` passes with all new tests green AND all pre-Epic-21 tests still green (epic AC, lines 4146–4149) no regressions.
34
+
35
+ 4. **(gap-fill)** The integration tests isolate the repo's own `.ma-agents.json` by operating exclusively in per-test temp directories via `fs.mkdtempSync(path.join(os.tmpdir(), 'ma-agents-21-9-'))`. The test run must NOT mutate `D:\Code\agents\.ma-agents.json` or any file at the repo root. Tests run sequentially (`for...of` with `await`), never `Promise.all`, because the installer writes shared template-derived files (see existing race in `test/generate-project-context.test.js` — lessons learned).
36
+
37
+ 5. **(gap-fill)** `fileRegex` matrix (NFR47, AC (e) expansion). For each of the four BMAD modes the test asserts accept/reject behavior against a pinned path set so implementation drift in the regex is caught:
38
+ - `bmad-pm`: accepts `.md`; rejects `.ts`, `.py`, `.js`, `.go`, `.yaml`, `.json`, `.xml`, `.drawio`
39
+ - `bmad-architect`: accepts `.md`, `.xml`, `.drawio`; rejects `.ts`, `.py`, `.js`, `.go`, `.yaml`, `.json`
40
+ - `bmad-techlead`: accepts `.md`, `.json`, `.yaml`, `.yml`; rejects `.ts`, `.py`, `.js`, `.go`, `.xml`, `.drawio`
41
+ - `bmad-dev`: accepts all of the above (full access — no `fileRegex` restriction, or `.*` equivalent)
42
+
43
+ 6. **(gap-fill)** Profile switch round-trip. The test performs: install with `profile=standard` → switch to `on-prem` (by rewriting `.ma-agents.json` directly in the temp dir and re-running the installer entry point — Story 21.10's `reconfigure` subcommand is a dependency for a non-workaround path, but this story must not block on 21.10) → switch back to `standard`. The final state's ma-agents-owned regions must be byte-identical to the first standard-profile install. User content outside markers is preserved across all three installs.
44
+
45
+ 7. **(gap-fill)** The test harness exercises the installer via its actual programmatic entry point — the exported top-level install function reachable from `bin/cli.js` — NOT via internal-only helpers. Unit-level template rendering tests are allowed and encouraged but are ADDITIONAL to the end-to-end harness; they do not replace it. Rationale: unit tests pass while wiring bugs (profile resolution, file-path routing, merger dispatch) silently break end-to-end install.
46
+
47
+ 8. **(gap-fill)** A per-profile baseline fixture lives under `test/fixtures/`:
48
+ - `test/fixtures/empty-project/` — minimal canonical project seed (`README.md`, `package.json`, no other files that affect install output). Already present (placeholder — see Change Log 2026-04-14 entry); Story 21.9 implementation populates/refreshes it if empty.
49
+ - `test/fixtures/standard-profile-baseline/` — full rendered output for a canonical `install --yes` run against `empty-project` with standard profile.
50
+ - `test/fixtures/onprem-profile-baseline/` — same for on-prem profile (seeded via a pre-written `.ma-agents.json` with `"profile": "on-prem"`).
51
+ The harness diffs every generated file against its baseline byte-for-byte; any drift is a test failure naming the file and diff region. A regeneration procedure is documented in `test/fixtures/README.md` (already present; verify it covers both profiles and is accurate).
52
+
53
+ 9. **(gap-fill)** PR body coverage table (added to the PR, not the story file) tabulates each Epic 21 NFR (NFR44, NFR45, NFR46, NFR47) → the specific test name(s) asserting it. If any NFR is uncovered, the gap is filed as a bug story — not silently shipped.
54
+
55
+ > **Open question:** AC #6 (profile switch round-trip) assumes the installer entry point is re-runnable in-process and that writing a new `"profile"` value to `.ma-agents.json` between runs is sufficient to trigger the profile-dependent re-stamping. If Story 21.10's `reconfigure` is merged first, prefer calling it; otherwise document the direct-edit approach and flag Story 21.10 as the preferred path in the test comment.
56
+
57
+ > **Open question:** Does the `bmad-dev` mode spec permit `fileRegex: '.*'` or omit `fileRegex` entirely? Story 21.3 spec says "full access" but doesn't pin the YAML shape. Defer to the template produced by Story 21.3; if ambiguous at implementation time, prefer omission (more permissive, matches Roo Code default).
58
+
59
+ > **Open question:** NFR45 (CI/CD `--yes` default to `standard`; non-TTY silent default) is already covered by `test/profile.test.js` (Story 21.1). This story does not duplicate that coverage — it only cites it in the NFR coverage table (AC #9). Confirm with reviewer that this non-duplication is acceptable.
25
60
 
26
61
  ## Tasks / Subtasks
27
62
 
28
- - [ ] Task 1: Create `test/onprem-injection.test.js` per AC #1 (a)–(f)
29
- - [ ] 1.1 Helper: scaffold a fresh test project in a temp dir with all 7 markdown-injection agents + Roo Code + OpenCode selected
30
- - [ ] 1.2 Helper: run the installer programmatically (not via CLI subprocess if possible call exported `installSkill` with profile option)
31
- - [ ] 1.3 Helper: read all generated instruction files and return as `{ filename content }` map
32
- - [ ] 1.4 Test (a): standard install assert no on-prem strings anywhere
33
- - [ ] 1.5 Test (b): on-prem install assert on-prem strings present in expected files
34
- - [ ] 1.6 Test (c): idempotency two installs, byte-equal content
35
- - [ ] 1.7 Test (d): `.roomodes` slug-collision matrix
36
- - [ ] 1.8 Test (e): `fileRegex` accept/reject matrix for all 4 BMAD modes
37
- - [ ] 1.9 Test (f): standard on-prem standard round-trip
38
- - [ ] Task 2: Run full `npm test` — verify no regressions (AC #2)
39
- - [ ] Task 3: PR body coverage table (AC #3)
40
- - [ ] 3.1 Tabulate NFR44, NFR45, NFR46, NFR47 covering test name(s)
41
- - [ ] 3.2 Confirm NFR45 (CI/CD compatibility) is covered by `test/profile.test.js` (Story 21.1) tests for `--yes` defaulting and persisted-value precedence (no CLI flag exists)
42
- - [ ] Task 4: Commit baseline fixtures (AC #5)
43
- - [ ] 4.1 Create `test/fixtures/empty-project/` minimal canonical project seed (README.md, package.json, nothing else that affects install output)
44
- - [ ] 4.2 Generate and commit `test/fixtures/standard-profile-baseline/` by running the installer once against the empty-project fixture with `--yes` (standard profile) and capturing all generated files
45
- - [ ] 4.3 Generate and commit `test/fixtures/onprem-profile-baseline/` via the same pattern with profile=on-prem persisted in `.ma-agents.json`
46
- - [ ] 4.4 Document regeneration procedure in a `test/fixtures/README.md` so future dev knows how to refresh when templates intentionally change
47
- - [ ] Task 5: End-to-end installer harness (AC #6)
48
- - [ ] 5.1 Harness scaffolds a scratch tmpdir, copies the `empty-project` fixture into it, and invokes the installer entry point for each profile
49
- - [ ] 5.2 After each install, walk the scratch dir and diff every file against the corresponding baseline fixture; any diff is a test failure naming the file and showing the diff hunk
50
- - [ ] 5.3 Run for both `standard` and `on-prem` profiles sequentially (not in parallel — shared template files)
63
+ - [ ] **Task 1: Verify prerequisite artifacts exist on disk** (precondition for all subsequent tasks)
64
+ - [ ] 1.1 Verify `D:\Code\agents\lib\profile.js` exists and exports `getProfile`, `setProfile`, `resolveProfile` (Story 21.1)
65
+ - [ ] 1.2 Verify `D:\Code\agents\lib\templates\instruction-block-universal.template.md` exists (Story 21.2) — **(new)** as of epic plan; verify before writing tests that consume it
66
+ - [ ] 1.3 Verify `D:\Code\agents\lib\templates\roomodes.template.yaml` and `D:\Code\agents\lib\merge\roomodes.js` exist (Story 21.3) **(new)**
67
+ - [ ] 1.4 Verify `D:\Code\agents\lib\templates\agents-md.template.md` exists (Story 21.4) — **(new)**
68
+ - [ ] 1.5 Verify `D:\Code\agents\lib\templates\clinerules.template.md` exists (Story 21.5) — **(new)**
69
+ - [ ] 1.6 Verify `D:\Code\agents\lib\templates\instruction-block-onprem.template.md` exists (Story 21.6) — **(new)**
70
+ - [ ] 1.7 Verify the eight `D:\Code\agents\lib\bmad-customize\bmm-*.customize.yaml` files have the Story 21.7 fields (`phase:`, `on_prem_phase_prefix:`)
71
+ - [ ] 1.8 Verify `D:\Code\agents\docs\deployment\vllm-nemotron.md` exists and `README.md` has the on-prem section (Story 21.8) — **(new)** doc
72
+ - [ ] 1.9 If any prerequisite is missing, HALT this story and surface the gap to the dev lead rather than stubbing around it
73
+
74
+ - [ ] **Task 2: Author `D:\Code\agents\test\onprem-injection.test.js`** (AC #1 a–e, #4, #5)
75
+ - [ ] 2.1 Scaffold helper: create temp project in `os.tmpdir()` seeded from `test/fixtures/empty-project/` with all markdown-injection agents + Roo Code + OpenCode selected
76
+ - [ ] 2.2 Helper: run installer programmatically via the exported top-level install function (AC #7)
77
+ - [ ] 2.3 Helper: read generated instruction files into a `{ filename → content }` map
78
+ - [ ] 2.4 Sub-test (a): standard install assert zero occurrences of on-prem strings
79
+ - [ ] 2.5 Sub-test (b): on-prem install assert on-prem strings present in each expected file
80
+ - [ ] 2.6 Sub-test (c): idempotency run installer twice, diff marker-block content byte-for-byte
81
+ - [ ] 2.7 Sub-test (d): `.roomodes` slug-collision seed pre-existing `.roomodes` with one colliding and one non-colliding slug, run installer, assert overwrite + preserve semantics
82
+ - [ ] 2.8 Sub-test (e) expansion: `fileRegex` accept/reject matrix per AC #5 for all four BMAD modes
83
+ - [ ] 2.9 Assert no writes to the repo-root `.ma-agents.json` use tmp dirs only (AC #4)
84
+
85
+ - [ ] **Task 3: Profile switch round-trip** (AC #6)
86
+ - [ ] 3.1 Test: install standard → overwrite `.ma-agents.json` profile field to `on-prem` → re-run install → overwrite back to `standard` → re-run install
87
+ - [ ] 3.2 Assert final standard-profile output matches the first standard-profile output (ma-agents-owned regions only); assert user-content regions outside markers unchanged at every step
88
+
89
+ - [ ] **Task 4: Baseline fixtures** (AC #8)
90
+ - [ ] 4.1 If `test/fixtures/empty-project/` is empty or stale, populate with canonical minimal seed (`README.md`, `package.json` with no install-affecting scripts)
91
+ - [ ] 4.2 Generate `test/fixtures/standard-profile-baseline/` by running the installer against the empty-project seed with standard profile, capturing all generated files under the baseline dir
92
+ - [ ] 4.3 Generate `test/fixtures/onprem-profile-baseline/` via the same pattern after persisting `"profile": "on-prem"` to `.ma-agents.json` in the temp project
93
+ - [ ] 4.4 Verify `test/fixtures/README.md` documents the regeneration procedure for both profiles; update if outdated
94
+ - [ ] 4.5 Harness walks generated tmp dir and diffs every file against the corresponding baseline; any diff fails the test naming the file + diff hunk
95
+
96
+ - [ ] **Task 5: End-to-end harness** (AC #7)
97
+ - [ ] 5.1 Harness invokes the installer entry point once per profile (sequential, not parallel)
98
+ - [ ] 5.2 Snapshots the generated filesystem tree
99
+ - [ ] 5.3 Diffs against the committed per-profile baseline fixture
100
+
101
+ - [ ] **Task 6: Profile unit-test coverage audit** (AC #2)
102
+ - [ ] 6.1 Read `D:\Code\agents\test\profile.test.js` and confirm it covers: missing-file `getProfile`, round-trip `setProfile`/`getProfile`, `resolveProfile` precedence (three cases), non-I/O property of `resolveProfile`
103
+ - [ ] 6.2 If any coverage is missing, add the test(s) to the same file; do NOT create a parallel file
104
+
105
+ - [ ] **Task 7: Run full `npm test` and produce NFR coverage table** (AC #3, #9)
106
+ - [ ] 7.1 `npm test` — must be green end-to-end
107
+ - [ ] 7.2 Author the NFR44/NFR45/NFR46/NFR47 → test-name table for the PR body
108
+ - [ ] 7.3 Confirm NFR45 is satisfied by `test/profile.test.js` `--yes` + persisted-precedence tests (no separate test required)
109
+
110
+ - [ ] **Task 8: Documentation touch-up**
111
+ - [ ] 8.1 If `test/fixtures/README.md` does not already document "how to regenerate baselines when templates change intentionally," add that section
112
+ - [ ] 8.2 No other docs modified by this story (Story 21.8 owns the on-prem README/docs surface)
51
113
 
52
114
  ## Dev Notes
53
115
 
54
116
  ### Architecture Compliance
55
117
 
56
- - **Decision P3-3** — This story closes the testing gap for cross-story contracts. Per-story tests cover their own scope; this story covers integration-level NFRs.
57
- - **NFR44, NFR45, NFR46, NFR47** explicit coverage required.
118
+ - **Decision P3-3** (`_bmad-output/planning-artifacts/architecture.md` §P3-3) this story closes the testing gap for cross-story contracts. Per-story unit tests cover their own scope; Story 21.9 covers integration-level NFRs.
119
+ - **NFR44** (profile isolationstandard profile emits no on-prem-specific strings): verified by AC #1 sub-test (a) and the standard-profile baseline fixture diff (AC #8).
120
+ - **NFR46** (stamping idempotency — two installs produce byte-identical marker content): verified by AC #1 sub-test (c).
121
+ - **NFR47** (Roo Code `fileRegex` application-layer enforcement): verified by AC #1 sub-test (e) + the AC #5 matrix. Per epic technical note (line 4152), this story verifies the *generated regex*, not a running Roo Code `FileRestrictionError`.
122
+ - **NFR18** (OpenCode JSON-merge additive-only): this story does NOT add new NFR18 coverage — existing `test/opencode-json-merge.test.js` owns that NFR. Story 21.9 cites NFR18 in the PR coverage table (AC #9) and confirms the on-prem profile injection into `opencode.json::instructions[]` does not introduce a regression via the end-to-end harness diff against the baseline fixture (Tasks 4, 5).
123
+
124
+ ### Prior-Story Artifacts the Test Suite Depends On (must exist on disk)
125
+
126
+ This story's tests consume artifacts produced by every other Epic 21 story. Task 1 verifies each before any test is authored. If any is missing the story HALTs — the tests cannot be stubbed in isolation without producing false confidence.
127
+
128
+ | Source story | Artifact(s) | Expected path | Consumer in 21.9 |
129
+ |---|---|---|---|
130
+ | 21.1 | `lib/profile.js` (getProfile/setProfile/resolveProfile) | `D:\Code\agents\lib\profile.js` (verified present) | AC #2, all tmp-project setup |
131
+ | 21.1 | `test/profile.test.js` | `D:\Code\agents\test\profile.test.js` (verified present) | AC #2 |
132
+ | 21.2 | Universal instruction-block template | `lib/templates/instruction-block-universal.template.md` **(new — Story 21.2)** | AC #1 (a), (b); baseline fixtures |
133
+ | 21.2 | `composeInstructionBlock` in `lib/installer.js` | `D:\Code\agents\lib\installer.js` (verified present; function new) | Tasks 2, 5 |
134
+ | 21.3 | `.roomodes` template + YAML merger | `lib/templates/roomodes.template.yaml` **(new)**, `lib/merge/roomodes.js` **(new)** | AC #1 (d), (e); AC #5 matrix |
135
+ | 21.3 | Roo Code entry `extraInstructionTemplates` in agents.js | `D:\Code\agents\lib\agents.js` (verified present; field new) | Task 2.7 |
136
+ | 21.4 | `AGENTS.md` template | `lib/templates/agents-md.template.md` **(new)** | AC #1 (b); baseline |
137
+ | 21.5 | `.clinerules` template | `lib/templates/clinerules.template.md` **(new)** | AC #1 (b); baseline |
138
+ | 21.6 | On-prem instruction-block template | `lib/templates/instruction-block-onprem.template.md` **(new)** | AC #1 (a), (b); NFR44 |
139
+ | 21.7 | Per-persona `phase:` + `on_prem_phase_prefix:` in 8 customize YAMLs | `lib/bmad-customize/bmm-*.customize.yaml` (files verified present; fields new) | On-prem baseline fixture (customize output) |
140
+ | 21.8 | vLLM doc + README section | `docs/deployment/vllm-nemotron.md` **(new)**, `README.md` on-prem section (modify) | Not directly test-consumed — cited in PR coverage table |
141
+ | 21.10 | `ma-agents reconfigure` subcommand | `bin/cli.js` (verified; flag new), `lib/reconfigure.js` **(new)** | AC #6 preferred path; if unmerged, fall back to direct `.ma-agents.json` edit |
142
+ | 21.11 | (not a dependency of 21.9 tests) | — | — |
58
143
 
59
144
  ### Source Tree Components to Touch
60
145
 
61
- | File | Change |
62
- |------|--------|
63
- | `test/onprem-injection.test.js` | CREATE |
146
+ | File | Change | Verified? |
147
+ |---|---|---|
148
+ | `D:\Code\agents\test\onprem-injection.test.js` | MODIFY (placeholder harness exists — see Change Log 2026-04-14; Story 21.9 populates it) | verified present (placeholder, currently exits 0) |
149
+ | `D:\Code\agents\test\profile.test.js` | VERIFY / augment only if coverage gaps found | verified present |
150
+ | `D:\Code\agents\test\fixtures\empty-project\` | MODIFY / populate if empty | verified present (directory) |
151
+ | `D:\Code\agents\test\fixtures\standard-profile-baseline\` | POPULATE with generated files | verified present (directory) |
152
+ | `D:\Code\agents\test\fixtures\onprem-profile-baseline\` | POPULATE with generated files | verified present (directory) |
153
+ | `D:\Code\agents\test\fixtures\README.md` | VERIFY / update regeneration procedure | verified present |
154
+ | `D:\Code\agents\lib\profile.js` | READ-ONLY | verified present |
155
+ | `D:\Code\agents\lib\installer.js` | READ-ONLY (consumed programmatically) | verified present |
156
+ | `D:\Code\agents\lib\agents.js` | READ-ONLY | verified present |
157
+ | `D:\Code\agents\lib\templates\project-context.template.md` | READ-ONLY reference pattern | verified present |
158
+ | `D:\Code\agents\lib\bmad-customize\*.customize.yaml` | READ-ONLY (8 files) | verified present |
159
+ | `D:\Code\agents\bin\cli.js` | READ-ONLY (entry point located; not modified) | verified present |
160
+ | `D:\Code\agents\docs\deployment\vllm-nemotron.md` | NOT TOUCHED (Story 21.8 owns) | **(new — Story 21.8)** |
64
161
 
65
- ### Dependencies
162
+ Legend: **(new)** = file does not yet exist on disk; will be created by the owning story. Story 21.9 cannot proceed on any `(new)` artifact that is still missing when it runs.
66
163
 
67
- - Stories 21.1 through 21.8 all merged.
164
+ ### Library and Pattern References
68
165
 
69
- ### Out of Scope
166
+ - Test framework: use the same node-test / mocha-style harness already in use across `D:\Code\agents\test\`. Verify by reading one existing file first (`test/profile.test.js` is the closest peer) before picking a style.
167
+ - Temp-dir isolation: `fs.mkdtempSync(path.join(os.tmpdir(), 'ma-agents-21-9-'))` — mirror the pattern in `test/profile.test.js`.
168
+ - Never run tests in parallel (`Promise.all`) against the installer — pre-existing race documented in `test/generate-project-context.test.js` (Epic 13).
169
+ - Byte-for-byte diffing: use `fs.readFileSync(..., 'utf-8')` + strict equality, or `Buffer.compare` for binary-safe files. For directory diffs, walk recursively and sort entries for stable comparison.
70
170
 
71
- - Adding new per-story unit tests (those are owned by their respective stories)
72
- - Performance benchmarks
73
- - Manual QA scripts
171
+ ### NFR Coverage Map (for PR body AC #9)
172
+
173
+ | NFR | Covered by |
174
+ |---|---|
175
+ | NFR44 (profile isolation) | `test/onprem-injection.test.js` sub-test (a); `test/fixtures/standard-profile-baseline/` diff |
176
+ | NFR45 (CI/CD non-blocking) | `test/profile.test.js` (existing — Story 21.1) — `--yes` defaulting + persisted-precedence tests |
177
+ | NFR46 (stamping idempotency) | `test/onprem-injection.test.js` sub-test (c) |
178
+ | NFR47 (application-layer file restriction) | `test/onprem-injection.test.js` sub-test (e) + AC #5 matrix |
179
+ | NFR18 (OpenCode JSON-merge additive) | `test/opencode-json-merge.test.js` (existing) + end-to-end harness diff |
180
+
181
+ ### Test Isolation Notes
182
+
183
+ The existing `test/generate-project-context.test.js` (Epic 13) used `Promise.all` and hit a race because two tests temporarily renamed the template file. Run all tests in `test/onprem-injection.test.js` sequentially (`for...of` with `await`). Each temp dir is created fresh per test and is not shared.
184
+
185
+ ### Test Coverage Gaps (surfaced by 21.2–21.8 AC enumeration)
186
+
187
+ The per-upstream-story AC mapping in the Testing section identifies every AC from Stories 21.2 through 21.8. ACs flagged **GAP** below are NOT covered by a test authored in Story 21.9 and are either (a) owned by the upstream story's own unit tests (UNIT), (b) documentation content not amenable to automation, or (c) genuine coverage gaps that should be filed as follow-up items. Story 21.9's contract per AC #9 is to surface — not silently absorb — every such gap.
188
+
189
+ - **21.2 AC #2** (placeholder-shape contract) — UNIT (Story 21.2 author's unit test).
190
+ - **21.2 AC #3** (composer on-prem-missing-throws error path) — UNIT.
191
+ - **21.2 AC #9** (BMAD agent file missing-skip branch) — UNIT.
192
+ - **21.2 AC #10** (upgrade-safety hand-edit detection + backup format) — UNIT (21.2 owns the contract; 21.9 does not exercise the drift/backup path).
193
+ - **21.3 AC #3** (`extraInstructionTemplates` static-registry field) — UNIT.
194
+ - **21.3 AC #5** (pure-function `mergeRoomodes` contract) — UNIT.
195
+ - **21.3 AC #6** (console-warning text exact match) — partial in OIT sub-test (d); UNIT owns the warning-text assertion.
196
+ - **21.3 AC #13** (Roo Code not installed → no write) — UNIT.
197
+ - **21.4 AC #2** (AGENTS.md template static-text shape) — UNIT.
198
+ - **21.4 AC #4** (OpenCode `extraInstructionTemplates` registry field) — UNIT.
199
+ - **21.4 AC #5 (append-to-unmarkered-existing-file branch)** — GAP, potentially testable in OIT sub-test (b) variant; surface as follow-up if Story 21.4 UNIT does not cover.
200
+ - **21.4 AC #9 exception** (legitimate `~/.claude/` in AGENTS.md Critical Behavior Rules under on-prem) — GAP; add positive assertion in OIT sub-test (b) on-prem branch so NFR44 narrowing is provable.
201
+ - **21.4 AC #10** (path stamping precedence `_bmad/bmm/config.yaml` vs defaults) — UNIT.
202
+ - **21.4 AC #11** (upgrade-safety drift inheritance from 21.2) — UNIT.
203
+ - **21.4 AC #12** (`instructionFiles` unchanged) — UNIT.
204
+ - **21.5 AC #2** (cross-file byte-identity of Cline files by construction) — GAP; add `assert.strictEqual(fs.readFileSync('.cline/clinerules.md'), fs.readFileSync('.clinerules'))` to OIT sub-test (b) to close in-story.
205
+ - **21.5 AC #6** (`ClinerulesDualFileDriftError` error-path) — UNIT.
206
+ - **21.5 AC #9** (`{{MANIFEST_PATH}}` resolves to `.cline/skills/MANIFEST.yaml`) — GAP; add explicit grep assertion in OIT sub-test (b) to close in-story.
207
+ - **21.6 AC #3** (template placeholder-free shape) — UNIT.
208
+ - **21.6 AC #9** (persona-prefix scope-out assertion) — UNIT (implicit; 21.7 owns positive side).
209
+ - **21.6 AC #12** (fileRegex unchanged between standard and on-prem renders) — GAP; extend OIT sub-test (e) to run under both profiles and assert regex equality.
210
+ - **21.7 AC #4** (deployed-output presence of new YAML fields — OR stripped per Branch B) — verify during BF-O capture; if customize deployed output is not walked by the baseline diff, surface as gap.
211
+ - **21.7 AC #5** (warning on invalid `phase:` value) — UNIT.
212
+ - **21.7 AC #8** (idempotency of deployed customize files) — GAP; extend OIT sub-test (c) to include `_bmad/_config/agents/*.customize.yaml` in the byte-identity hash set, OR verify it is walked by BF-O diff.
213
+ - **21.7 AC #10** (untouched-persona back-compat) — UNIT.
214
+ - **21.8 AC #2–#7** (doc content) — DOC REVIEW, not test-automatable.
215
+ - **21.8 AC #8** (README On-Prem section) — GAP; add `fs.readFileSync('README.md').includes('On-Prem / Air-Gapped Deployment')` assertion to 21.9 harness preconditions (Task 1.8) to close in-story.
216
+
217
+ **Follow-up action:** Per AC #9, gaps that are neither UNIT-owned nor doc-review-only (21.4 AC #5 append branch; 21.4 AC #9 exception; 21.5 AC #2 cross-file identity; 21.5 AC #9 manifest-path substitution; 21.6 AC #12 cross-profile fileRegex equality; 21.7 AC #4 and #8 deployed-customize coverage; 21.8 AC #8 README presence) MUST be either (a) absorbed into this story's OIT/harness tasks before merge, or (b) filed as bug stories against the owning Epic-21 story. The PR body coverage table (AC #9) names each resolution explicitly.
218
+
219
+ ### Override-via-Extension Policy Reminder
220
+
221
+ Story 21.7's customize-loader field additions (`phase:`, `on_prem_phase_prefix:`) rely on the BMAD built-in override policy (the project policy is: "override BMAD built-ins via extension, never upstream PRs to bmad-method"). Story 21.9 tests must validate BOTH variants the loader may emit (per Epic Cross-Epic Notes line 4231): if Story 21.7's implementation produces `*.customize.on-prem.yaml` sibling files, the test harness diff against the on-prem baseline must include them. If it produces a single field-augmented file selected at install time, the diff is against the rendered output only. Confirm which branch Story 21.7 landed on before authoring the baseline fixture.
222
+
223
+ ## Testing
224
+
225
+ - **Framework:** match existing `D:\Code\agents\test\` style. Verify first by reading `test/profile.test.js` — that is the Story-21.1 peer and the authoritative pattern for Epic 21 tests.
226
+ - **Isolation:** `fs.mkdtempSync` per test; sequential execution; zero writes to the repo root.
227
+ - **Fixtures:** `test/fixtures/empty-project/`, `test/fixtures/standard-profile-baseline/`, `test/fixtures/onprem-profile-baseline/` — regenerable per `test/fixtures/README.md`.
228
+ - **Entry point:** exported top-level install function (not internal helpers) — AC #7 non-negotiable.
229
+ - **Success criteria:** `npm test` green; all new sub-tests pass; pre-Epic-21 tests pass unchanged; PR body includes the NFR coverage table from the Dev Notes section above.
230
+
231
+ ### Per-Upstream-Story AC → Test-File Mapping
74
232
 
75
- ### Notes on Test Isolation
233
+ Each AC from Stories 21.2 through 21.8 is enumerated below and mapped to the test file that asserts it. ACs not mapped to a test are surfaced as Test Coverage Gap entries under Dev Notes. Mapping key: **OIT** = `test/onprem-injection.test.js` (this story), **PT** = `test/profile.test.js` (Story 21.1, extended here), **BF-S** = `test/fixtures/standard-profile-baseline/` diff (AC #8), **BF-O** = `test/fixtures/onprem-profile-baseline/` diff (AC #8), **OJM** = `test/opencode-json-merge.test.js` (pre-existing), **UNIT** = per-story unit test under `test/` owned by the upstream story, **GAP** = no test in 21.9; see Test Coverage Gap list.
76
234
 
77
- The existing `test/generate-project-context.test.js` (from Epic 13) used `Promise.all` and hit a race because two tests temporarily renamed the template file. Run `test/onprem-injection.test.js` tests sequentially (`for...of` with `await`) — never parallel — because they share template files and depend on full-install state.
235
+ **Story 21.2 (Universal Instruction Block + composer):**
236
+ - 21.2 AC #1 (universal template file exists + content) → BF-S / BF-O (content appears in baseline); OIT sub-test (b).
237
+ - 21.2 AC #2 (`{{MANIFEST_PATH}}` single placeholder contract) → GAP (21.9 does not assert placeholder shape; owned by Story 21.2 UNIT).
238
+ - 21.2 AC #3 (composer function signature + on-prem-missing-throws) → GAP (error-path not exercised in 21.9; owned by Story 21.2 UNIT).
239
+ - 21.2 AC #4 (per-tool marker injection calls composer) → OIT sub-test (b); BF-S / BF-O.
240
+ - 21.2 AC #5 (all markdown-injection agents receive the block) → OIT sub-test (b); BF-S / BF-O.
241
+ - 21.2 AC #6 (idempotency — NFR46) → OIT sub-test (c).
242
+ - 21.2 AC #7 (profile isolation — NFR44) → OIT sub-test (a); BF-S.
243
+ - 21.2 AC #8 (OpenCode JSON-merge coexistence — NFR18) → OJM (existing) + BF-S / BF-O (instructions[] entry present).
244
+ - 21.2 AC #9 (BMAD agent instruction files unchanged in scope) → GAP (no negative assertion authored in 21.9; owned by Story 21.2 UNIT).
245
+ - 21.2 AC #10 (upgrade-safety hand-edit detection + backup) → GAP (drift/backup path not exercised in 21.9; owned by Story 21.2 UNIT).
78
246
 
79
- ## Dev Agent Record
247
+ **Story 21.3 (`.roomodes` template + YAML merger):**
248
+ - 21.3 AC #1 (four customModes entries present) → OIT sub-test (b); BF-S / BF-O.
249
+ - 21.3 AC #2 (customInstructions reuses composer output) → BF-O (on-prem content appears in customInstructions); OIT sub-test (b).
250
+ - 21.3 AC #3 (`roo-code` agent gains `extraInstructionTemplates`) → GAP (static agent-registry field; owned by Story 21.3 UNIT).
251
+ - 21.3 AC #4 (stamper wires extra template) → OIT sub-test (b) end-to-end; BF-S / BF-O.
252
+ - 21.3 AC #5 (mergeRoomodes contract — pure splicer) → GAP (pure-function unit test owned by Story 21.3 UNIT).
253
+ - 21.3 AC #6 (slug-collision warning emitted) → OIT sub-test (d) — asserts overwrite; GAP on the console-warning text match (surface if Story 21.3 UNIT does not cover).
254
+ - 21.3 AC #7 (fresh install creates `.roomodes`) → OIT sub-test (b); BF-S.
255
+ - 21.3 AC #8 (re-install preserves user customModes) → OIT sub-test (d).
256
+ - 21.3 AC #9 (NFR47 fileRegex contract) → OIT sub-test (e) + AC #5 matrix.
257
+ - 21.3 AC #10 (NFR46 idempotency for .roomodes) → OIT sub-test (c).
258
+ - 21.3 AC #11 (NFR44 standard profile — customInstructions has no on-prem strings) → OIT sub-test (a); BF-S.
259
+ - 21.3 AC #12 (NFR18 not regressed) → OJM (existing).
260
+ - 21.3 AC #13 (Roo Code not installed → no `.roomodes` write) → GAP (21.9 always selects Roo Code; owned by Story 21.3 UNIT).
261
+ - 21.3 AC #14 (markdown rules file `.roo/rules/00-ma-agents.md` unchanged in shape) → OIT sub-test (b); BF-S / BF-O.
80
262
 
81
- ### Agent Model Used
82
- _(to be filled by dev agent)_
263
+ **Story 21.4 (`AGENTS.md` template + OpenCode wiring):**
264
+ - 21.4 AC #1 (template file exists + content) → BF-S / BF-O.
265
+ - 21.4 AC #2 (static text, no placeholders) → GAP (source-file shape; owned by Story 21.4 UNIT).
266
+ - 21.4 AC #3 (composer output written via markdown-markers merger) → OIT sub-test (b).
267
+ - 21.4 AC #4 (OpenCode agent `extraInstructionTemplates` registered) → GAP (static registry field; owned by Story 21.4 UNIT).
268
+ - 21.4 AC #5 (markdown-markers merger behavior — create / replace / append) → OIT sub-test (b), (c) cover create + replace; GAP on the append-to-existing-unmarkered-file case.
269
+ - 21.4 AC #6 (`"AGENTS.md"` appended to `opencode.json::instructions[]`) → OJM (existing) + BF-S / BF-O.
270
+ - 21.4 AC #7 (`AGENTS.md` entry user-owned after first install — no re-append) → OIT sub-test (c) (idempotency catches double-append).
271
+ - 21.4 AC #8 (idempotency — NFR46) → OIT sub-test (c).
272
+ - 21.4 AC #9 (profile isolation — NFR44 with `~/.claude/` exception inside Critical Behavior Rules) → OIT sub-test (a) asserts the three literals absent in standard-profile render; GAP on the exception-carve-out assertion (the single legitimate `~/.claude/` occurrence under on-prem profile in AGENTS.md Critical Behavior Rules).
273
+ - 21.4 AC #10 (path stamping resolution precedence) → GAP (owned by Story 21.4 UNIT).
274
+ - 21.4 AC #11 (upgrade-safety hand-edit detection) → GAP (inherits Story 21.2 AC #10; owned by Story 21.2/21.4 UNIT).
275
+ - 21.4 AC #12 (OpenCode `instructionFiles` unchanged) → GAP (static registry; owned by Story 21.4 UNIT).
83
276
 
84
- ### Debug Log References
85
- _(to be filled)_
277
+ **Story 21.5 (`.clinerules` template extension):**
278
+ - 21.5 AC #1 (template file exists + content) → BF-S / BF-O.
279
+ - 21.5 AC #2 (universal text not hand-duplicated — single composer render) → OIT sub-test (b) + cross-file byte-identity check (GAP if not added).
280
+ - 21.5 AC #3 (both Cline files written via marker-based injection) → OIT sub-test (b); BF-S / BF-O.
281
+ - 21.5 AC #4 (user content outside markers preserved) → OIT sub-test (c), (d).
282
+ - 21.5 AC #5 (idempotency per file — NFR46) → OIT sub-test (c).
283
+ - 21.5 AC #6 (dual-file drift detection — `ClinerulesDualFileDriftError`) → GAP (error-path not exercised in 21.9; owned by Story 21.5 UNIT).
284
+ - 21.5 AC #7 (profile isolation — NFR44, both files) → OIT sub-test (a); BF-S.
285
+ - 21.5 AC #8 (on-prem profile appends on-prem content to both files) → OIT sub-test (b); BF-O.
286
+ - 21.5 AC #9 (`{{MANIFEST_PATH}}` resolves to `.cline/skills/MANIFEST.yaml`) → GAP (placeholder substitution result — add explicit assertion in OIT sub-test (b) or surface as gap; owned by Story 21.5 UNIT).
86
287
 
87
- ### Completion Notes List
88
- _(to be filled)_
288
+ **Story 21.6 (on-prem layered guardrails):**
289
+ - 21.6 AC #1 (on-prem template file exists + four categories) → BF-O.
290
+ - 21.6 AC #2 (composer append wiring — on-prem layer) → OIT sub-test (b); BF-O.
291
+ - 21.6 AC #3 (on-prem template has no placeholders) → GAP (source-file shape; owned by Story 21.6 UNIT).
292
+ - 21.6 AC #4 (profile isolation — standard — NFR44, exhaustive three-literal set) → OIT sub-test (a); BF-S. **This AC pins the canonical negative set referenced by 21.9 AC #1(a).**
293
+ - 21.6 AC #5 (profile merge — on-prem includes both blocks in every agent) → OIT sub-test (b); BF-O.
294
+ - 21.6 AC #6 (`.roomodes` customInstructions on-prem augmentation — `/no_think` in each of four modes) → OIT sub-test (b); BF-O.
295
+ - 21.6 AC #7 (`AGENTS.md` on-prem augmentation inside Critical Behavior Rules anchor) → OIT sub-test (b); BF-O. Shape-A vs Shape-B decision deferred to Story 21.6 dev.
296
+ - 21.6 AC #8 (`.clinerules` / `.cline/clinerules.md` on-prem augmentation) → OIT sub-test (b); BF-O.
297
+ - 21.6 AC #9 (BMAD persona phase prefix NOT in scope for 21.6) → GAP (negative scope assertion; implicit via Story 21.7 test ownership).
298
+ - 21.6 AC #10 (idempotency — NFR46) → OIT sub-test (c); BF-O.
299
+ - 21.6 AC #11 (additive JSON-merge not regressed — NFR18) → OJM (existing) + BF-O.
300
+ - 21.6 AC #12 (Roo Code fileRegex not regressed — NFR47) → OIT sub-test (e) run under both profiles (GAP if not added — surface).
301
+ - 21.6 AC #13 (upgrade-safety on profile flip — deferred to 21.10) → Out of scope for 21.9; covered indirectly by AC #6 profile switch round-trip.
89
302
 
90
- ### File List
91
- _(to be filled)_
303
+ **Story 21.7 (BMAD persona phase prefix):**
304
+ - 21.7 AC #1 (planning-persona prefix content when on-prem) → BF-O (deployed `_bmad/_config/agents/*.customize.yaml`).
305
+ - 21.7 AC #2 (implementation-persona prefix content when on-prem) → BF-O.
306
+ - 21.7 AC #3 (standard-profile isolation — no prefix, byte-identical to pre-Epic-21) → BF-S; NFR coverage table.
307
+ - 21.7 AC #4 (YAML schema additions — `phase:` + `on_prem_phase_prefix:`) → Task 1.7 verifies source files have fields; GAP on actual deployed-output assertion if BF-O does not include customize-output files (verify during baseline capture).
308
+ - 21.7 AC #5 (`phase` field values enumerated — warning on invalid) → GAP (owned by Story 21.7 UNIT).
309
+ - 21.7 AC #6 (loader integration contract — no upstream PRs) → Policy, not test-verifiable; cite only.
310
+ - 21.7 AC #7 (prefix composition rule — prepend not replace) → BF-O (deployed customize file shape).
311
+ - 21.7 AC #8 (idempotency — NFR46) → OIT sub-test (c) extended to hash deployed customize files, or BF-O diff on second run (GAP if not added — surface).
312
+ - 21.7 AC #9 (standard-profile byte-identity to pre-Epic-21 baseline) → BF-S; NFR coverage table.
313
+ - 21.7 AC #10 (authoring back-compat for untouched personas) → GAP (out-of-scope-persona assertion; owned by Story 21.7 UNIT).
314
+ - 21.7 AC #11 (NFR47 non-regression) → OIT sub-test (e).
315
+ - 21.7 AC #12 (NFR18 non-regression) → OJM (existing).
316
+
317
+ **Story 21.8 (vLLM reference doc + README):**
318
+ - 21.8 AC #1 (doc file created at `docs/deployment/vllm-nemotron.md`) → Task 1.8 filesystem presence check; cited in NFR coverage table.
319
+ - 21.8 AC #2 (doc covers vLLM flags with rationale) → GAP (content review, not test-automatable; owned by Story 21.8 doc review).
320
+ - 21.8 AC #3 (doc covers quantization tradeoffs) → GAP (doc content; not test-automatable).
321
+ - 21.8 AC #4 (doc covers reasoning-mode behavior / `/no_think`) → GAP (doc content; not test-automatable).
322
+ - 21.8 AC #5 (per-phase sampling-parameters table) → GAP (doc content; not test-automatable).
323
+ - 21.8 AC #6 (`str_replace_editor` hallucination warning) → GAP (doc content; not test-automatable).
324
+ - 21.8 AC #7 (copy-paste-runnable `vllm serve` launch command) → GAP (doc content; not test-automatable).
325
+ - 21.8 AC #8 (README gains On-Prem / Air-Gapped Deployment section) → Task 1.8 filesystem grep check; GAP on automated presence assertion if not added to harness.
326
+ - 21.8 AC #9 (deployment doc NOT stamped into target projects — FR179) → BF-S / BF-O (absence of doc from installer output tree).
327
+
328
+ ## Dependencies
329
+
330
+ ### Upstream (must be merged before Story 21.9 can start)
331
+
332
+ - **Story 21.1** (`lib/profile.js`, `test/profile.test.js`) — **status: done** per sprint-status.yaml
333
+ - **Story 21.2** (universal instruction-block template + `composeInstructionBlock`) — **status: backlog**
334
+ - **Story 21.3** (`.roomodes` template + YAML merger + agents.js `extraInstructionTemplates`) — **status: backlog**
335
+ - **Story 21.4** (`AGENTS.md` template + OpenCode wiring) — **status: backlog**
336
+ - **Story 21.5** (`.clinerules` template) — **status: backlog**
337
+ - **Story 21.6** (on-prem instruction-block template) — **status: backlog**
338
+ - **Story 21.7** (BMAD persona phase prefix — 8 `*.customize.yaml` files) — **status: backlog**
339
+ - **Story 21.8** (vLLM doc + README section) — **status: backlog** (soft dependency; cited in coverage table, not directly test-consumed)
340
+ - **Story 21.10** (Profile Reconfigure — `ma-agents reconfigure` subcommand) — **status: backlog** (soft dependency; the AC #6 profile-switch round-trip test prefers calling `reconfigure` over direct `.ma-agents.json` editing when available. Test suite depends on 21.10's artifact (`lib/reconfigure.js`, `bin/cli.js` flag) existing; if 21.10 ships before 21.9 the test uses the canonical path, otherwise it falls back to direct edit with a TODO pointing at 21.10 per the Open question above.)
341
+
342
+ ### Downstream (stories enabled by 21.9 completion)
343
+
344
+ - **Story 21.10** (Profile Reconfigure) — Story 21.9 is NOT a hard dependency, but 21.10 benefits from the baseline fixtures when authoring its own round-trip tests. Per sprint-status.yaml execution order (line 113), 21.9 runs before 21.10.
345
+ - **Story 21.11** (Profile Uninstall) — runs after 21.9 per execution order. Reuses isolation patterns established here.
346
+
347
+ ### Soft / informational
348
+
349
+ - `_bmad-output/implementation-artifacts/sprint-status.yaml` (READ-ONLY in this story — DO NOT modify per user instruction override)
350
+
351
+ ## Out of Scope
352
+
353
+ - Adding new per-story unit tests (those are owned by their respective stories — Stories 21.1–21.8, 21.10, 21.11)
354
+ - Performance benchmarks for installer speed
355
+ - Manual QA scripts
356
+ - Any test that requires a running Roo Code process (NFR47 regex contract is verified against the generated pattern, not runtime `FileRestrictionError` — per epic technical note line 4152)
357
+ - vLLM serving-stack tests (Story 21.8 ships vLLM as documentation only — FR179)
358
+ - BMAD upstream contributions (project policy: override via extension; never upstream PRs to bmad-method)
359
+ - Editing `.claude/skills/` — skill sources live in `lib/bmad-extension/skills/` or `skills/`; `.claude/skills/` is generated
92
360
 
93
361
  ## Change Log
362
+
94
363
  - 2026-04-14: Story created (Epic 21, Story 21.9)
95
- - 2026-04-14: Removed prescriptive `--profile=` flag references from test (f) description and Task 3.2 (flag retired; profile switch deferred to Story 21.10 reconfigure). Aligned with P0 spec-alignment PR #34.
96
- - 2026-04-14: Added ACs #5 and #6 for standard-profile byte-for-byte fixture baseline and end-to-end installer harness (Findings #14, #18, corrective plan step 3). Replaces the vague "diff against pre-Epic-21 baseline" language in NFR44 with concrete committed fixtures and a harness that exercises the actual installer entry point, not just template-rendering internals.
97
- - 2026-04-14: E2E fixture scaffolding committed (corrective-plan step 7): `test/fixtures/empty-project/` seed, placeholder baseline dirs, pending `test/onprem-injection.test.js` harness (exits 0 until Story 21.9 implementation), regeneration README. Baselines are captured by Story 21.9 implementation per `test/fixtures/README.md`.
364
+ - 2026-04-14: Removed prescriptive `--profile=` flag references (flag retired per P0 spec-alignment PR #34)
365
+ - 2026-04-14: Added ACs for standard-profile byte-for-byte fixture baseline and end-to-end installer harness (Findings #14, #18)
366
+ - 2026-04-14: E2E fixture scaffolding committed (`test/fixtures/empty-project/`, placeholder baseline dirs, pending `test/onprem-injection.test.js` harness exits 0 until Story 21.9 implementation)
367
+ - 2026-04-15: Story rewritten to spec — Status=Ready, verbatim epic Story paragraph, ACs flag `(gap-fill)` additions, Tasks/Subtasks with exact absolute paths, Dev Notes cite NFR44/46/47/18, prior-story artifact dependency table enumerated, override-via-extension policy reminder added. Open questions raised for (a) Story 21.10 availability at test-author time, (b) `bmad-dev` `fileRegex` shape ambiguity, (c) NFR45 non-duplication with `test/profile.test.js`.
368
+ - 2026-04-15: Adversarial-review resolution pass. (1) AC #1(a) now pins the EXHAUSTIVE NFR44 negative literal set `["/no_think", "str_replace_editor", "~/.claude/"]` per Story 21.6 AC #4 scope narrowing (closes P1 #7); reasoning-mode / sampling prose is explicitly tested positive-side only. (2) Testing section expanded with per-upstream-story AC → test-file mapping covering every AC from Stories 21.2–21.8. (3) Dev Notes gains a Test Coverage Gaps subsection surfacing each GAP with ownership (UNIT vs in-story absorb vs doc-review). (4) Upstream dependencies now cite Story 21.10 (soft). (5) Status changed to Draft with explicit blockers — Tasks are conditional on upstream merge status and surfaced gaps. Canonical decision B (composer/merger/stamper terminology) verified consistent throughout.
@@ -0,0 +1,112 @@
1
+ ---
2
+ type: bug
3
+ status: ready-for-dev
4
+ severity: high
5
+ bug_type: regression
6
+ version_found: 3.5.3
7
+ title: BMAD recompile failed on disconnected (air-gapped) network install
8
+ ---
9
+
10
+ # Bug: BMAD recompile failed on disconnected (air-gapped) network install
11
+
12
+ **Severity:** high
13
+ **Affected Component:** installer pipeline — `lib/bmad.js` recompile stage (wraps `bmad-method/tools/bmad-npx-wrapper.js`)
14
+
15
+ ## Reproduction Steps
16
+
17
+ 1. Provision a host with **no outbound internet access** (air-gapped / on-prem lab) but with `node_modules/bmad-method` already vendored (i.e. install performed from an offline `npm ci` or bundled tarball).
18
+ 2. Run `npx ma-agents install` (v3.5.3) targeting a fresh project directory.
19
+ 3. Pipeline reaches the "Running: node …/bmad-npx-wrapper.js install …" stage.
20
+ 4. Upstream `bmad-method` (6.2.2) installer invokes `git fetch origin --depth 1` / `git clone --depth 1 <url>` to refresh external modules (bmb, gds, tea, wds, cis) — see `node_modules/bmad-method/tools/cli/installers/lib/modules/manager.js` around lines 284–332 — and also shells out to `npm install --omit=dev` for those modules' deps (line 346).
21
+ 5. All three network operations fail (no DNS / no route / git prompts disabled by `GIT_TERMINAL_PROMPT=0`). `execSync` throws.
22
+ 6. `lib/bmad.js:366` catches and prints `BMAD recompile failed: <error.message>`. The install continues past this point (the try/catch swallows) and the user is left with a partially configured BMAD tree with a scary red error.
23
+
24
+ ## Expected Behavior
25
+
26
+ On an air-gapped host where the ma-agents bundled cache has been pre-populated into `~/.bmad/cache/external-modules/`, the recompile step should:
27
+
28
+ 1. Detect offline mode (either explicitly via a flag/env, or implicitly when upstream fetch fails but a valid cache is present).
29
+ 2. Skip network fetches and proceed with the cached modules.
30
+ 3. Either succeed silently, or, if it must fail, emit a **clear, actionable** error explaining (a) that the network is unreachable, (b) which cached modules are present, and (c) what the operator should do next (retry with `--offline`, populate the cache, etc.).
31
+
32
+ ## Actual Behavior
33
+
34
+ - Upstream `bmad-method` unconditionally attempts `git fetch`/`git clone`/`npm install` even when the cache directory already has valid, fully-populated modules.
35
+ - `lib/bmad.js` catches the resulting error and prints a one-line red banner: `BMAD recompile failed: Command failed: node "…/bmad-npx-wrapper.js" install …` — which surfaces in `bmad-npx-wrapper.js` because that is the entry point.
36
+ - The error gives no indication that (a) the problem is network-related, (b) the bundled cache *is* present and could be reused, or (c) how the operator should recover.
37
+ - Because the try/catch swallows the error, later stages (EXTENSION, WORKFLOWS, templates, MIL registries) run against a partially-compiled `_bmad/` tree and can produce additional confusing failures downstream.
38
+
39
+ ## Root Cause Hypothesis
40
+
41
+ Two contributing causes:
42
+
43
+ 1. **Upstream coupling** — `bmad-method@6.2.2` (`installers/lib/modules/manager.js`) has no `--offline` switch and always runs `git fetch` on existing cache dirs. We cannot fix this in-place, but we *can* detect the failure mode and stop bleeding into later stages.
44
+ 2. **ma-agents lossy error handling** — `lib/bmad.js:363-367` catches *any* recompile failure (network, config, assertion, etc.) and continues silently. No offline-specific diagnosis, no cache-presence check, no actionable remediation.
45
+
46
+ ## Affected Files
47
+
48
+ - `lib/bmad.js` (lines 28–37 `getBmadCommand`, 347–367 recompile try/catch, 629–683 `prePopulateBmadCache`)
49
+ - `test/` — new regression test required (`test/offline-recompile.test.js`)
50
+
51
+ ## Suggested Fix
52
+
53
+ Add an offline-safe code path in `lib/bmad.js`:
54
+
55
+ 1. Expose `isOfflineMode()` helper — true when `MA_AGENTS_OFFLINE=1` or when upstream recompile fails AND the vendored cache has been successfully pre-populated (`~/.bmad/cache/external-modules/<module>` exists for every module in `lib/bmad-cache/cache-manifest.json`).
56
+ 2. At the recompile catch site, classify the failure:
57
+ - If offline mode is active **and** cache is intact → downgrade to a `warn`-level diagnostic ("network unavailable — proceeded with vendored cache") and continue.
58
+ - If offline mode is active **but** cache is incomplete → emit an actionable error listing the missing modules and the remediation command (`npm run build:bmad-cache`).
59
+ - Otherwise → re-throw so the operator sees a loud failure instead of a swallowed one.
60
+ 3. Add a regression test that mocks `execSync` to throw an ENOTFOUND-style error and asserts (a) we do not crash, (b) we emit the offline-mode diagnostic, (c) we do not re-throw when the cache is intact.
61
+
62
+ ## Notes
63
+
64
+ - Created via `create-bug-story` workflow (non-interactive — bug B pipeline).
65
+ - Discoverable by sprint workflows via glob: `_bmad-output/implementation-artifacts/bug-*.md`
66
+ - Related skill: `devops-disconnected-deployment` — same target-environment assumptions.
67
+ - Related epic: 21 (On-Prem / Local-LLM Tuning) — on-prem is a documented supported deployment target.
68
+
69
+ ## Dev Agent Record
70
+
71
+ **Branch:** `worktree-agent-ab0035d4`
72
+ **Status:** in-progress → review (after CR)
73
+
74
+ ### Fix summary
75
+
76
+ - `lib/bmad.js` — added four exported helpers: `isOfflineModeDeclared()`, `looksLikeOfflineFailure(error)`, `inspectBmadCache(cacheRoot?)`, `classifyRecompileFailure(error, { cacheInspector? })`.
77
+ - `lib/bmad.js` — at the `applyCustomizations()` recompile catch site (~line 366), replaced the single red-banner `BMAD recompile failed` with a 3-way classified diagnosis:
78
+ - `warn` → yellow diagnostic, install continues (offline + cache intact)
79
+ - `error` → actionable red message listing missing cache modules and the `npm run build:bmad-cache` remediation (offline + cache incomplete)
80
+ - `rethrow` → prints red as before (non-network failures — no behaviour change)
81
+ - `test/offline-recompile.test.js` — new 13-test regression suite; mocks `process.env.MA_AGENTS_OFFLINE` and injects a fake cache inspector so the test runs deterministically without spawning subprocesses.
82
+ - `package.json` — appended new test to the `test` script.
83
+
84
+ ### Offline simulation approach
85
+
86
+ Rather than spawning a real air-gapped subprocess (not tractable in a Windows worktree), the test:
87
+
88
+ 1. Constructs synthetic `Error` objects with realistic messages (`ENOTFOUND`, `fatal: unable to access...`, `ECONNREFUSED`) — matching what `execSync` emits when `git fetch`/`git clone`/`npm install` hit DNS failures on an air-gapped host.
89
+ 2. Mocks the cache-inspector via dependency injection (`classifyRecompileFailure(err, { cacheInspector: () => ({ ... }) })`) to test both intact-cache and missing-module cases.
90
+ 3. Toggles `process.env.MA_AGENTS_OFFLINE` to exercise explicit vs inferred offline detection.
91
+
92
+ ### Limitations
93
+
94
+ - We do not patch bmad-method 6.2.2 itself; it still attempts `git fetch`/`git clone`/`npm install` unconditionally (its `cloneExternalModule` has no `--offline` switch). Our fix is post-hoc: we classify the resulting failure and give the operator clear recovery guidance. The pre-existing `restoreGitDir()` helper (lib/bmad.js:1305) already rewrites the `origin` URL to `file://` so `git fetch` becomes a local no-op for already-cached modules — this is the primary defence; our change is the safety net when that defence is defeated (e.g. a newly-added module not yet in the vendored cache).
95
+ - An upstream fix (or a wrapper shim under `lib/bmad-extension/`) that intercepts `cloneExternalModule` to short-circuit on cache-hit would be a cleaner long-term solution. Out of scope here — recommend filing a follow-up story under Epic 21.
96
+
97
+ ### Test verification
98
+
99
+ ```
100
+ $ node test/offline-recompile.test.js
101
+ ... 13 passed, 0 failed
102
+ ```
103
+
104
+ Other related tests (`build-bmad-args.test.js`, `migration.test.js`) still pass. One unrelated pre-existing failure in `bmad-version-bump.test.js` is caused by the worktree lacking a populated `node_modules/bmad-method` — not introduced by this change.
105
+
106
+ ### Code review (adversarial)
107
+
108
+ - **F1 (Low)** — `action: 'rethrow'` is advisory; the `applyCustomizations()` caller still prints-and-continues (historical behaviour preserved to avoid breaking mid-pipeline failure recovery). Clarified in JSDoc.
109
+ - **F2 (Low, fixed)** — removed `'proxy'`/`'ssl'`/`'certificate'` heuristics that risked false-positives on benign YAML errors referencing cert-generation skills. Final needle list targets DNS/git/connection errors only.
110
+ - **F5 (Low)** — cache "intact" check is directory-presence only; acceptable because `prePopulateBmadCache()` runs first and performs structural repair. Documented.
111
+ - **AC coverage** — "detect air-gapped condition": done. "Skip network ops when vendored": partial — upstream still attempts the calls; we recover gracefully. "Surface clearer actionable error": done. "Unit test simulating offline mode": done.
112
+ - Verdict: **APPROVED** — no High findings, Med/Low findings resolved or documented.
@@ -27,9 +27,10 @@ tracking_system: file-system
27
27
  story_location: _bmad-output/implementation-artifacts
28
28
 
29
29
  development_status:
30
- # ─── BUG FIXES (ACTIVE) ───────────────────────────────────────────────────────
31
- # Bug A (2026-04-14): ExperimentalWarning on installer startup. Severity: medium.
30
+ # ─── ACTIVE BUGS ──────────────────────────────────────────────────────────────
31
+ # Standalone bug stories discoverable via glob: _bmad-output/implementation-artifacts/bug-*.md
32
32
  bug-experimentalwarning-about-commonjs-loading-es-module-during-install: ready-for-dev
33
+ bug-bmad-recompile-fails-on-airgapped-network: review
33
34
 
34
35
  # ─── IN PROGRESS ──────────────────────────────────────────────────────────────
35
36