ma-agents 3.5.6 → 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 (53) 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
@@ -1,6 +1,6 @@
1
1
  # Story 21.3: `.roomodes` Template with BMAD Mode File-Regex Restrictions
2
2
 
3
- Status: backlog
3
+ Status: Review
4
4
 
5
5
  ## Story
6
6
 
@@ -10,97 +10,220 @@ So that Roo Code's application-layer enforcement (`FileRestrictionError`) preven
10
10
 
11
11
  ## Acceptance Criteria
12
12
 
13
- 1. New template `lib/templates/roomodes.template.yaml` exists defining four `customModes`:
14
- - `bmad-pm` — `groups: [read, [edit, { fileRegex: "\\.md$", description: "Markdown only" }]]`
15
- - `bmad-architect` `groups: [read, [edit, { fileRegex: "\\.(md|xml|drawio)$", description: "Markdown and diagram files only" }]]`
16
- - `bmad-techlead` — `groups: [read, [edit, { fileRegex: "\\.(md|json|yaml|yml)$", description: "Markdown, JSON, YAML only" }]]`
17
- - `bmad-dev` — `groups: [read, edit, command]` (full access)
18
- 2. Each mode includes `slug`, `name`, `roleDefinition`, `whenToUse`, and `customInstructions` fields with content matching the BMAD phase descriptions in the source playbook (`optimizing-local-llm-coding-agents-bmad.md` Section 4.3).
19
- 3. The Roo Code agent entry in `lib/agents.js` gains an optional `extraInstructionTemplates` array. For Roo Code: `[{ template: 'roomodes.template.yaml', target: '.roomodes', merger: 'yaml-customModes' }]`.
20
- 4. The installer reads `extraInstructionTemplates` per agent and stamps each entry. For Roo Code, `.roomodes` is written at the project root.
21
- 5. New module `lib/merge/roomodes.js` exports `mergeRoomodes(existingYaml, templateYaml)` returning the merged YAML string. Behavior:
22
- - If `.roomodes` does not exist, return the template content.
23
- - If it exists, parse both, merge the `customModes` arrays such that the four ma-agents-owned slugs (`bmad-pm`, `bmad-architect`, `bmad-techlead`, `bmad-dev`) overwrite any colliding entries (with a console warning naming each colliding slug); all other user-defined `customModes` entries are preserved untouched and emitted before the ma-agents entries.
24
- - YAML output preserves comments and field order in user-owned entries where the YAML library supports it.
25
- 6. Re-running install produces byte-identical `.roomodes` content for the four ma-agents slugs (NFR46), with user-owned entries preserved.
26
- 7. NFR47 contract: a unit test verifies the rendered `bmad-architect` `fileRegex` rejects paths ending in `.ts`, `.py`, `.js`, `.go`; accepts `.md`, `.xml`, `.drawio`. Verified by `RegExp(fileRegex).test(path)` — no Roo Code runtime needed.
27
- 8. The Roo Code agent must already be registered in `lib/agents.js` (Epic 18 Story 18.1). If Epic 18 is not yet merged when this story starts, this story includes the minimal Roo Code agent registration as a prerequisite sub-task.
28
- 9. **Slug-stomp protection.** On install, for each of the four ma-agents-owned slugs (`bmad-pm`, `bmad-architect`, `bmad-techlead`, `bmad-dev`) that already exist in the user's `.roomodes`, the installer diffs the existing slug body (`roleDefinition` + `customInstructions` + `groups`) against the current template output for the user's profile. If a non-whitespace-only diff exists, the installer ABORTS with a named error (e.g., `RoomodesSlugDivergenceError`) listing the diverged slugs and instructs the user to either (a) rename the slug (accept it as user-owned, dropping our version) or (b) rerun with `--force-roomodes-overwrite` to accept the stomp. Without `--force-roomodes-overwrite`, no overwrite happens. This supersedes the "console warning only" behavior described in AC #5's bullet about colliding slugs for the four ma-agents-owned slugs; collisions on other slugs are impossible (ma-agents only owns these four). `--yes` does NOT imply `--force-roomodes-overwrite` — silent stomps on user-edited BMAD modes would be a data-loss regression.
29
- 10. **Overwrite audit log.** When a ma-agents-owned slug IS written or overwritten (first install, clean match of existing content, or user passed `--force-roomodes-overwrite`), the installer appends an entry to a top-level field `roomodesOverwriteLog` in `.ma-agents.json` with shape `{ slug, date: <ISO-timestamp>, previousContentHash: <sha256-of-previous-body-or-null-if-new>, profile: <value> }`. The array grows append-only and is capped at 50 entries (oldest dropped on insert beyond cap) to prevent unbounded growth of `.ma-agents.json`. Rationale: gives operators a forensic trail for "when did the BMAD mode definitions last change on this project?"
13
+ > Acceptance criteria marked **(gap-fill)** are additions made by the story author to make the epic's AC operational and testable. They do not contradict the epic; they refine the "how" where the epic only stated the "what". Open questions are flagged inline rather than resolved by author guesswork.
14
+
15
+ 1. **Template file exists.** A new file at the pinned path `lib/templates/roomodes.template.yaml` (new; path pinned per `_bmad-output/planning-artifacts/epics.md:3994`) is present and defines exactly four `customModes` entries:
16
+ - `bmad-pm` — read + edit restricted to `\.md$`
17
+ - `bmad-architect` — read + edit restricted to `\.(md|xml|drawio)$`
18
+ - `bmad-techlead` read + edit restricted to `\.(md|json|yaml|yml)$`
19
+ - `bmad-dev` read + edit + command (full access; no `fileRegex` group restriction on edit)
20
+ Each mode includes a `roleDefinition`, `whenToUse`, and `customInstructions` block whose text aligns with the BMAD phase descriptions in the source playbook (`optimizing-local-llm-coding-agents-bmad.md`, referenced by the epic intro at `_bmad-output/planning-artifacts/epics.md` lines 3886–3895).
21
+ 2. **(gap-fill) `customInstructions` reuses universal composer output.** The `customInstructions` content for each of the four modes incorporates the universal text-vs-file and BMAD phase-discipline rules. Per canonical decision A, the template itself contains NO placeholders and the merger (`mergeRoomodes`) does NOT call `composeInstructionBlock`. Instead, `lib/installer.js` (the stamper) is the sole owner of invoking `composeInstructionBlock({ profile, projectRoot })` (21.2-owned) exactly once per artifact; the installer performs `{{UNIVERSAL_BLOCK}}` substitution on the loaded template text AFTER composition and passes the already-composed template string into `mergeRoomodes` as input. Modes do NOT duplicate rule text. The Roo Code `MANIFEST.yaml` path used by `composeInstructionBlock` is the same `path.relative(projectRoot, path.join(agent.getProjectPath(), 'MANIFEST.yaml')).replace(/\\/g, '/')` already computed in `updateAgentInstructions` (`lib/installer.js` line ~408) for the `roo-code` agent entry (`lib/agents.js` lines 140–163; `getProjectPath()` returns `<projectRoot>/.roo/skills`).
22
+ 3. **Roo Code agent gains `extraInstructionTemplates`.** The `roo-code` entry in `lib/agents.js` (lines 140–163) gains a new optional field:
23
+ ```js
24
+ extraInstructionTemplates: [
25
+ { template: 'roomodes.template.yaml', target: '.roomodes', merger: 'yaml-customModes' }
26
+ ]
27
+ ```
28
+ No other agent entry is modified. The field is read only by the new `extraInstructionTemplates` processing path described in AC #4; absence on other agents is a no-op.
29
+ 4. **(gap-fill) Installer (stamper) wires the extra template.** A new processing pass in `lib/installer.js` (sibling to `updateAgentInstructions`, named `applyExtraInstructionTemplates(agent, projectRoot)` new; the stamper per decision B) iterates `agent.extraInstructionTemplates` and, for each entry: (a) loads the raw template from `lib/templates/<template>`; (b) calls the composer `composeInstructionBlock({ profile: getProfile(projectRoot) || 'standard', projectRoot })` ONCE and substitutes `{{UNIVERSAL_BLOCK}}` in the template string (caller-side substitution per decision A); (c) dispatches on `merger`. For `'yaml-customModes'`, the stamper invokes the merger `mergeRoomodes(existingYaml, composedTemplateYaml)` (new) passing the already-composed string as input — and writes the result to `<projectRoot>/<target>` atomically (temp-file + rename, mirroring the json-merge pattern at `lib/installer.js` lines ~395–398). If an existing target file is overwritten, the stamper creates a backup named `<target>.backup-<ISO-8601-timestamp>` (21.2-owned format; see Dependencies → Upstream).
30
+ 5. **YAML merger contract — `mergeRoomodes(existingYaml, templateYaml)`** (new — located in `lib/merge/roomodes.js` (new file). The epic technical note allows `lib/installer.js` placement "or a new `lib/merge/roomodes.js` if size warrants"; author resolution: separate file, because it owns YAML parse/serialize plus slug-collision logic and is large enough to warrant isolation). Per decision A, the merger is a pure splicer: it receives the already-composed template string as `templateYaml` and MUST NOT call `composeInstructionBlock`. Contract:
31
+ - Input: two strings (existing file content, already-composed template content). `existingYaml` may be empty/undefined when the file does not exist. `templateYaml` is the composer output from the stamper — no further substitution is performed here.
32
+ - Parse both as YAML using `js-yaml` (design decision; see Dev Notes → Library and pattern references).
33
+ - Output: a serialized YAML string whose `customModes` array contains every entry from `existingYaml.customModes` whose `slug` is NOT in the ma-agents-owned set (`bmad-pm`, `bmad-architect`, `bmad-techlead`, `bmad-dev`), followed by all four entries from `templateYaml.customModes`.
34
+ - All other top-level YAML keys from `existingYaml` are preserved unchanged (FR176 — "preserves user content outside ma-agents-owned slugs").
35
+ - Pure function — no I/O.
36
+ 6. **Slug-collision warning.** When `mergeRoomodes` discovers an existing entry whose slug matches one of the four ma-agents-owned slugs, it emits a single console warning per colliding slug naming the slug verbatim (e.g., `WARNING: .roomodes slug "bmad-architect" overwritten by ma-agents template`). The user entry is replaced; the ma-agents version wins (per epic AC — "colliding slugs are overwritten with the ma-agents version (with a console warning naming the slug)" at `epics.md` line 3987).
37
+ 7. **Fresh install creates `.roomodes`.** Given the Roo Code agent is selected in `npx ma-agents install`, when the installer runs and `<projectRoot>/.roomodes` does not exist, the file is written at the project root containing exactly the four ma-agents BMAD `customModes` entries (no user entries to preserve).
38
+ 8. **Re-install preserves user `customModes`.** Given `<projectRoot>/.roomodes` already exists with user-defined `customModes` whose slugs do NOT collide with the four ma-agents slugs, when the installer runs, those entries are preserved in their original order and the four ma-agents entries are present (FR176). YAML key ordering for preserved user entries is best-effort — comment preservation is NOT required (see Open question on `js-yaml` round-trip behavior).
39
+ 9. **NFR47 application-layer enforcement contract.** Given a user is in `bmad-architect` mode in Roo Code after install, when the agent attempts to edit a `.ts` or `.py` file, Roo Code rejects the edit with `FileRestrictionError`. **Verification scope for this story:** unit/integration tests assert that the generated `.roomodes` `fileRegex` patterns for `bmad-architect` (and the other three modes) reject `.ts`/`.py` paths via direct regex match — the actual `FileRestrictionError` is raised by Roo Code at runtime. The end-to-end Roo Code launch is out of scope (epic technical note at `epics.md` line 4152 explicitly limits NFR47 to the regex-contract level).
40
+ 10. **(gap-fill) NFR46 idempotency.** Two consecutive installs in the same project state produce byte-identical `.roomodes` content, including stable ordering of `customModes` entries (preserved-user entries first in their original order, then the four ma-agents entries in the canonical order `bmad-pm`, `bmad-architect`, `bmad-techlead`, `bmad-dev`). YAML serialization options (indent width, quote style, line width) are pinned in the merger so re-runs do not produce whitespace drift.
41
+ 11. **(gap-fill) NFR44 profile isolation — standard profile.** When `getProfile(projectRoot) === 'standard'` (or undefined), the rendered `.roomodes` `customInstructions` strings MUST NOT contain the literals `/no_think`, `str_replace_editor`, or `~/.claude/`. Verified by direct grep assertion in tests. On-prem-specific augmentation of `customInstructions` is deferred to Story 21.6 (epic technical note at line 4074 — "the `customInstructions` block per Roo Code mode … gets the on-prem rules appended when profile=on-prem").
42
+ 12. **(gap-fill) NFR18 additive JSON-merge — not regressed.** This story does NOT touch the OpenCode `opencode.json::instructions[]` json-merge path in `updateAgentInstructions` (`lib/installer.js` lines ~365–405). The `extraInstructionTemplates` pipeline is sibling and orthogonal. Existing OpenCode tests must continue to pass.
43
+ 13. **(gap-fill) Roo Code is not installed → no `.roomodes` write.** When the Roo Code agent is not selected in the wizard / direct-install, `applyExtraInstructionTemplates` is not invoked for it and `<projectRoot>/.roomodes` is not created or modified by ma-agents.
44
+ 14. **(gap-fill) Markdown rules instruction file unchanged in shape.** Roo Code's existing markdown injection target `.roo/rules/00-ma-agents.md` (registered at `lib/agents.js` line 161) continues to receive the universal block via Story 21.2's `updateAgentInstructions` path. This story adds `.roomodes` alongside; it does not relocate or remove the markdown rules file.
45
+
46
+ > **Design decision (was Open question):** YAML library is **`js-yaml`**. Verified via `npm ls js-yaml` in `D:\Code\agents` on 2026-04-15: `js-yaml@4.1.1` is present transitively via `bmad-method@6.2.2`. Because transitive availability is not a stable contract, 21.3 adds `js-yaml` as a **direct installer dependency** (promote to `dependencies` in `package.json`, pinning `^4.1.1` to match the transitive version already in the tree). Comment-preserving libraries (`yaml` from eemeli) are heavier and unnecessary — comments inside ma-agents-owned slugs are overwritten by definition; comments on user entries are best-effort lost on rewrite (called out in AC #8).
47
+
48
+ > **Design decision (was Open question):** Installer call site is **`lib/installer.js`** — the single stamper owner per canonical decision A. The install loop invokes `applyExtraInstructionTemplates(agent, projectRoot)` immediately after `updateAgentInstructions(agent, projectRoot)` for each selected agent. Both wizard and direct-install paths flow through this single loop (no separate branch in `bin/cli.js` invokes the extra-templates pipeline independently).
49
+
50
+ > **Design decision (was Open question):** Roo Code `.roomodes` schema shape is the **authoritative shape cited in the `roo-code` entry in `lib/agents.js` (lines 140–163)** — that entry (registered by Story 18.1) is the source of truth for ma-agents' Roo Code integration. The template comment header links back to `lib/agents.js` rather than to a moving upstream URL. Schema fields used: `slug`, `name`, `roleDefinition`, `whenToUse`, `customInstructions`, `groups` (with `["edit", { fileRegex, description }]` tuple form).
30
51
 
31
52
  ## Tasks / Subtasks
32
53
 
33
- - [ ] Task 1: Create `lib/templates/roomodes.template.yaml` per AC #1, #2
34
- - [ ] Task 2: Add `extraInstructionTemplates` field to Roo Code entry in `lib/agents.js` (AC #3)
35
- - [ ] Task 3: Implement `lib/merge/roomodes.js::mergeRoomodes` (AC #5)
36
- - [ ] 3.1 Use `js-yaml` (already a dependency, verify) for parse/dump
37
- - [ ] 3.2 Slug-collision detection + console warning
38
- - [ ] 3.3 Preserve user entries in original order, ma-agents entries appended
39
- - [ ] Task 4: Wire `extraInstructionTemplates` processing into `lib/installer.js` per-agent install loop (AC #4)
40
- - [ ] Task 5: Tests in `test/roomodes-merge.test.js`
41
- - [ ] 5.1 Empty `.roomodes` 4 BMAD modes present
42
- - [ ] 5.2 Existing user mode preserved when slug does not collide
43
- - [ ] 5.3 Colliding slug overwritten with console warning
44
- - [ ] 5.4 Idempotency: two merges produce byte-identical output (NFR46)
45
- - [ ] 5.5 NFR47 contract `bmad-architect` fileRegex matrix (AC #7)
46
- - [ ] 5.6 Same matrix for `bmad-pm` (`.md` only) and `bmad-techlead` (`.md|.json|.yaml|.yml` only)
47
- - [ ] Task 6: If Epic 18 not merged, add Roo Code agent registration as prerequisite (AC #8) coordinate with epic execution order before starting
48
- - [ ] Task 7: Slug-stomp protection (AC #9)
49
- - [ ] 7.1 For each of the 4 ma-agents-owned slugs, compute diff of existing `roleDefinition`+`customInstructions`+`groups` against current template render
50
- - [ ] 7.2 On any non-whitespace diff without `--force-roomodes-overwrite`, abort with `RoomodesSlugDivergenceError` listing diverged slugs and remediation
51
- - [ ] 7.3 Add `--force-roomodes-overwrite` flag to `bin/cli.js` install command; document in help text that `--yes` does not imply it
52
- - [ ] Task 8: Overwrite audit log (AC #10)
53
- - [ ] 8.1 On write of a ma-agents-owned slug, append `{ slug, date, previousContentHash, profile }` to `.ma-agents.json::roomodesOverwriteLog`
54
- - [ ] 8.2 `previousContentHash` is sha256 of the previous slug body (concat of `roleDefinition`+`customInstructions`+JSON.stringify(`groups`)) or `null` on first install of that slug
55
- - [ ] 8.3 Cap array at 50 entries — oldest dropped on insert beyond cap
56
- - [ ] 8.4 Test: install log has 4 entries with null hashes; re-install with hand-edit + `--force-roomodes-overwrite` log has 5 entries, 5th has non-null previous hash
54
+ - [ ] **Task 1: Create the YAML template** (AC #1, #11)
55
+ - [ ] 1.1 Create `lib/templates/roomodes.template.yaml` (new; pinned path per `epics.md:3994`) with four `customModes` entries: `bmad-pm`, `bmad-architect`, `bmad-techlead`, `bmad-dev`. Each mode's `groups` array MUST encode the `fileRegex` per AC #1 (`bmad-pm`: `\.md$`; `bmad-architect`: `\.(md|xml|drawio)$`; `bmad-techlead`: `\.(md|json|yaml|yml)$`; `bmad-dev`: full access).
56
+ - [ ] 1.2 Each mode's `customInstructions` field contains a `{{UNIVERSAL_BLOCK}}` sentinel and inline phase-specific text (e.g., "PM mode — discovery and PRD only") wrapping the sentinel. The template contains NO other placeholders. Per decision A, substitution happens in the stamper (`lib/installer.js`) AFTER `composeInstructionBlock` runs; the merger never touches the sentinel and never calls the composer.
57
+ - [ ] 1.3 Verify by inspection (and by the Task 5 NFR44 test) that the static template text contains none of `/no_think`, `str_replace_editor`, or `~/.claude/`. Those strings are only legal in the on-prem layer (Story 21.6).
58
+
59
+ - [ ] **Task 2: Implement `mergeRoomodes` (merger) and wire it into the stamper** (AC #4, #5, #6, #10)
60
+ - [ ] 2.1 Create `lib/merge/roomodes.js` (new) exporting `mergeRoomodes(existingYaml, templateYaml, options)` a pure splicer. Use `js-yaml`. Pin `js-yaml`'s `dump` options (`indent: 2`, `lineWidth: -1`, `noRefs: true`, `quotingType: '"'`) so output is deterministic across runs (NFR46). The merger MUST NOT import or call `composeInstructionBlock`; it treats `templateYaml` as opaque pre-composed input (decision A).
61
+ - [ ] 2.2 Slug-collision logic: build the ma-agents-owned set `['bmad-pm', 'bmad-architect', 'bmad-techlead', 'bmad-dev']`; filter user entries by `slug not in set`; for each filtered-out user entry, emit one warning line via `console.warn` naming the slug.
62
+ - [ ] 2.3 Add `applyExtraInstructionTemplates(agent, projectRoot)` to `lib/installer.js` (new stamper function). Iterate `agent.extraInstructionTemplates || []`. For each entry: (i) load raw template from `lib/templates/<template>`; (ii) call composer ONCE: `composed = rawTemplate.replace('{{UNIVERSAL_BLOCK}}', composeInstructionBlock({ profile: getProfile(projectRoot) || 'standard', projectRoot }))`; (iii) dispatch on `merger`. For `'yaml-customModes'`: read `<projectRoot>/<target>` if present, call `mergeRoomodes(existingYaml, composed)`, write atomically (temp + rename — pattern at `lib/installer.js` lines ~395–398). When overwriting an existing target, write a backup to `<target>.backup-<ISO-8601-timestamp>` (format owned by 21.2).
63
+ - [ ] 2.4 Export `mergeRoomodes` from `lib/merge/roomodes.js` and `applyExtraInstructionTemplates` from `lib/installer.js` so Story 21.9 tests can consume them directly.
64
+ - [ ] 2.5 Promote `js-yaml` to a direct `dependencies` entry in `package.json` pinned to `^4.1.1` (matches version already present transitively via `bmad-method@6.2.2`).
65
+
66
+ - [ ] **Task 3: Wire into the install loop** (AC #4, #7, #13)
67
+ - [ ] 3.1 In the install loop in `lib/installer.js` where each selected agent has `updateAgentInstructions(agent, projectRoot)` invoked, add a follow-on `await applyExtraInstructionTemplates(agent, projectRoot)` call. Both wizard and direct-install paths flow through this single loop (per decision A; no separate branch in `bin/cli.js`).
68
+ - [ ] 3.2 Confirm that absence of `extraInstructionTemplates` on every other agent is a no-op (loop early-exits when the array is empty/undefined).
69
+
70
+ - [ ] **Task 4: Register `extraInstructionTemplates` on Roo Code** (AC #3)
71
+ - [ ] 4.1 In `lib/agents.js` lines 140–163, add the `extraInstructionTemplates` array per AC #3 to the `roo-code` entry only.
72
+ - [ ] 4.2 Do not modify any other agent entry.
73
+
74
+ - [ ] **Task 5: Unit and integration tests** see Testing section below.
75
+
76
+ - [ ] **Task 6: Documentation touch-up** (no new files)
77
+ - [ ] 6.1 If an existing doc covers per-tool injection, append a one-paragraph note pointing to `.roomodes`. If no existing doc covers this surface, skip Story 21.8 introduces broader Epic 21 documentation.
57
78
 
58
79
  ## Dev Notes
59
80
 
60
- ### Architecture Compliance
81
+ ### Architecture compliance
61
82
 
62
- - **Decision P3-3** — `.roomodes` is the highest-leverage application-layer guardrail in the design. NFR47 makes the enforcement contract testable.
63
- - **NFR46** — Idempotent stamping. The merger is responsible for deterministic output for the ma-agents-owned slugs.
83
+ - **Decision P3-3 (Local-LLM / On-Prem Agent Tuning Profile)** — `_bmad-output/planning-artifacts/architecture.md` lines 1888–1919. The "rejected" alternative explicitly cited at line 1919 — "Generate per-tool config purely via prompt instruction (no `.roomodes`)" was rejected precisely because Roo Code's `fileRegex` is application-layer enforcement that the prompt cannot replicate. This story is the implementation of the accepted alternative.
64
84
 
65
- ### Source Tree Components to Touch
85
+ - **NFR44 (byte-identity for standard profile)** — `_bmad-output/planning-artifacts/epics.md` lines 264, 471. Standard-profile rendered `.roomodes` `customInstructions` MUST NOT contain `/no_think`, `str_replace_editor`, or `~/.claude/`. AC #11 is the contract; Test 5.5 enforces it. On-prem `customInstructions` augmentation is Story 21.6's surface.
66
86
 
67
- | File | Change |
68
- |------|--------|
69
- | `lib/templates/roomodes.template.yaml` | CREATE |
70
- | `lib/merge/roomodes.js` | CREATE |
71
- | `lib/agents.js` | MODIFY — add `extraInstructionTemplates` to Roo Code entry |
72
- | `lib/installer.js` | MODIFY — process `extraInstructionTemplates` per agent during install |
73
- | `test/roomodes-merge.test.js` | CREATE |
87
+ - **NFR46 (idempotency)** `epics.md` lines 471, 4144 (item c). Two consecutive installs produce byte-identical `.roomodes`. AC #10 is the contract; Test 5.6 enforces it. The merger pins YAML `dump` options to eliminate whitespace drift.
74
88
 
75
- ### Dependencies
89
+ - **NFR47 (Roo Code application-layer fileRegex enforcement)** — `_bmad-output/planning-artifacts/prd.md` line 809; `epics.md` lines 264, 4144 (item e), 4152. Verified at the regex-contract level only; full Roo Code launch is out of scope (epic explicit). Test 5.7 asserts each mode's `fileRegex` against representative file paths.
76
90
 
77
- - Story 21.1 (profile API`.roomodes` content branches on profile in Story 21.6, but stamping is unconditional)
78
- - Story 21.2 (composition pattern — but `.roomodes` uses YAML merger, not marker-based markdown injection)
79
- - Epic 18 Story 18.1 (Roo Code agent registration) — see AC #8
91
+ - **NFR18 (additive JSON-merge for OpenCode)** Not in scope here. AC #12 is a non-regression contract; this story's pipeline is sibling to the json-merge path and does not touch it.
80
92
 
81
- ### Reference
93
+ - **FR176 (additive `.roomodes` slug isolation)** — `epics.md` line 254; `prd.md` line 706. AC #5, #8 implement; Test 5.8 verifies.
82
94
 
83
- Source playbook: `optimizing-local-llm-coding-agents-bmad.md` Section 4.3 — full `.roomodes` example with 4 BMAD modes. Use that exactly for the template content.
95
+ ### Verified source-tree surface
84
96
 
85
- ### Out of Scope
97
+ | File | Exists? | Role in this story |
98
+ |------|---------|--------------------|
99
+ | `lib/profile.js` | verified (Story 21.1, lines 29–43) | Consumed via `getProfile(projectRoot)` inside `applyExtraInstructionTemplates` |
100
+ | `lib/installer.js` | verified | Add `applyExtraInstructionTemplates`; consume `composeInstructionBlock` (Story 21.2, AC #3); reuse atomic-write pattern from lines ~395–398 |
101
+ | `lib/agents.js` | verified | Add `extraInstructionTemplates` field to the `roo-code` entry only (lines 140–163). The Roo Code entry's `instructionFiles` (`['.roo/rules/00-ma-agents.md']`) is unchanged (AC #14) |
102
+ | `lib/templates/roomodes.template.yaml` | **new** | Template source (AC #1) |
103
+ | `lib/merge/roomodes.js` | **new** | YAML merger (AC #5) |
104
+ | `lib/templates/instruction-block-universal.template.md` | **new (Story 21.2)** | Source for `{{UNIVERSAL_BLOCK}}` expansion via `composeInstructionBlock` |
105
+ | `lib/templates/instruction-block-onprem.template.md` | **new (Story 21.6)** | Append-only on-prem layer; consumed transitively when profile=on-prem; this story must not break when missing |
106
+ | `test/roomodes.test.js` | **new** | Unit + integration tests (see Testing section) |
107
+ | `test/profile.test.js` | verified (Story 21.1) | Reference for test framework, temp-dir isolation pattern |
108
+ | `_bmad-output/implementation-artifacts/21-2-universal-instruction-block-expansion.md` | verified | Upstream story — `composeInstructionBlock` signature pinned there |
86
109
 
87
- - On-prem-specific `customInstructions` additions (Story 21.6 will append on-prem rules to each mode's `customInstructions` when profile=on-prem)
88
- - Cline-to-Roo migration (Epic 18 Story 18.4)
110
+ ### Composition pattern reuse (composer / merger / stamper roles decision B)
89
111
 
90
- ## Dev Agent Record
112
+ - **Composer** = `composeInstructionBlock({ profile, projectRoot })` — 21.2-owned, the single source of truth for text-vs-file and BMAD phase-discipline content. Called exactly once per artifact by the stamper.
113
+ - **Merger** = `mergeRoomodes(existingYaml, templateYaml)` — 21.3-owned. Pure splicer: YAML parse, slug filter, splice, serialize. Receives the already-composed template string as input. MUST NOT call the composer.
114
+ - **Stamper** = `lib/installer.js` (`applyExtraInstructionTemplates` for this story; `updateAgentInstructions` for the markdown/JSON paths) — the single orchestrator that loads templates, calls the composer, performs `{{UNIVERSAL_BLOCK}}` substitution, and invokes per-artifact mergers.
91
115
 
92
- ### Agent Model Used
93
- _(to be filled by dev agent)_
116
+ Story 21.6 will extend the composer's on-prem layer transparently — this story's code path requires no signature change when 21.6 lands.
94
117
 
95
- ### Debug Log References
96
- _(to be filled)_
118
+ ### Library and pattern references
97
119
 
98
- ### Completion Notes List
99
- _(to be filled)_
120
+ - **YAML library** — `js-yaml` (verified present at `4.1.1` transitively via `bmad-method@6.2.2`; promoted to direct dependency by this story — Task 2.5).
121
+ - **Atomic write pattern** — `lib/installer.js` lines ~395–398 (`fs.outputFile(tmpPath, ...)` then `fs.rename(tmpPath, filePath)`).
122
+ - **Roo Code agent entry** — `lib/agents.js` lines 140–163 (the only entry receiving `extraInstructionTemplates`).
123
+ - **Roo Code instruction file (markdown rules)** — `.roo/rules/00-ma-agents.md`, `lib/agents.js` line 161; left untouched by this story (AC #14).
124
+ - **Manifest-path computation** — reuse the `path.relative(projectRoot, path.join(agentProjectPath, 'MANIFEST.yaml')).replace(/\\/g, '/')` formula (`lib/installer.js` line 408) for cross-platform path consistency.
100
125
 
101
- ### File List
102
- _(to be filled)_
126
+ ## Dependencies
127
+
128
+ ### Upstream (blocking)
129
+
130
+ - **Story 21.1 (done)** — `lib/profile.js::getProfile(projectRoot)` is consumed by `applyExtraInstructionTemplates` to pick the profile passed into `composeInstructionBlock`. Verified at `lib/profile.js` lines 29–43.
131
+ - **Story 21.2 (Ready)** — owns **(a)** `composeInstructionBlock({ profile, projectRoot })` exported from `lib/installer.js` (the composer; single source of truth for universal text-vs-file / BMAD phase-discipline content embedded in each mode's `customInstructions`), and **(b)** the **backup filename format** `<target>.backup-<ISO-8601-timestamp>` used by the stamper whenever an existing artifact is overwritten. This story MUST land after 21.2 to avoid duplicating rule text and to reuse the backup-filename contract.
132
+ - **Epic 18 Story 18.1 (done at code level)** — `roo-code` agent entry in `lib/agents.js` (lines 140–163). Verified present per epic cross-cutting note at `epics.md` line 4230.
133
+
134
+ ### Downstream (consumers of this story's surface)
135
+
136
+ - **Story 21.4 (`AGENTS.md` for OpenCode)** — consumes the `extraInstructionTemplates` field convention and the per-agent stamper `applyExtraInstructionTemplates` delivered by this story. Story 21.4 adds a new entry `{ template: 'agents-md.template.md', target: 'AGENTS.md', merger: 'markdown-markers' }` to the OpenCode agent and reuses this story's stamper unchanged; if the `markdown-markers` merger was not registered by 21.3, 21.4 extends the dispatcher.
137
+ - **Story 21.6 (On-Prem Layered Guardrails)** — appends on-prem rules to each mode's `customInstructions` via the same `composeInstructionBlock` call signature (no signature change). Also potentially adds on-prem-only mode-level notes per epic technical note at line 4074. This story's `applyExtraInstructionTemplates` design must accommodate Story 21.6 by re-running the merger when the on-prem template appears (no special branch required if `composeInstructionBlock` already conditionalizes internally — verified via Story 21.2 AC #3).
138
+ - **Story 21.9 (Tests and Validation)** — adds the cross-cutting `.roomodes` slug-collision integration test and the NFR47 enforcement contract test as listed in `epics.md` line 4144 (items d and e). This story's tests are the unit-level subset; Story 21.9 adds the integration-level coverage.
139
+ - **Story 21.10 (Profile Reconfigure)** — re-stamps `.roomodes` when profile flips. Reuses `applyExtraInstructionTemplates` and `mergeRoomodes`. Story 21.10 also relies on AC #6's slug-collision warning to detect the `RoomodesSlugDivergenceError` referenced at `epics.md` line 4180.
140
+ - **Story 21.11 (Profile Uninstall)** — removes the four ma-agents-owned slugs while preserving user entries — reuses the slug-set constant from `lib/merge/roomodes.js` (export it as `MA_AGENTS_OWNED_SLUGS`).
141
+
142
+ ## Testing
143
+
144
+ **Framework and isolation:** Match the pattern in `test/profile.test.js` (Story 21.1) — node's built-in test runner, `os.tmpdir()` + `fs.mkdtempSync` per-test temporary project roots, never mutate the repo's own `.ma-agents.json` or `lib/templates/`. Use the real `lib/profile.js` (`setProfile(tmpRoot, 'standard' | 'on-prem')`) against the tmp project root.
145
+
146
+ New test file: `test/roomodes.test.js` (new).
147
+
148
+ **Unit tests for `mergeRoomodes`:**
149
+
150
+ - 5.1 Empty/undefined `existingYaml` → output contains exactly the four ma-agents-owned `customModes` in canonical order.
151
+ - 5.2 `existingYaml` with two non-colliding user entries → output contains both user entries (preserving original order) followed by the four ma-agents entries.
152
+ - 5.3 `existingYaml` with one colliding `bmad-architect` entry → user's entry dropped, replaced by ma-agents version, exactly one warning emitted naming the slug `bmad-architect`.
153
+ - 5.4 `existingYaml` with non-`customModes` top-level keys → those keys preserved in output (FR176).
154
+ - 5.5 NFR44 grep assertion: `mergeRoomodes` output for profile=standard contains none of `/no_think`, `str_replace_editor`, `~/.claude/`.
155
+ - 5.6 NFR46 idempotency: calling `mergeRoomodes` twice with identical inputs returns byte-identical strings (same YAML serialization options, same ordering).
156
+ - 5.7 NFR47 regex contract: for each mode's `fileRegex`, assert via `new RegExp(fileRegex).test(...)`:
157
+ - `bmad-pm` accepts `notes.md`, rejects `app.ts`, rejects `script.py`.
158
+ - `bmad-architect` accepts `arch.md`, `diagram.drawio`, `model.xml`; rejects `app.ts`, `script.py`.
159
+ - `bmad-techlead` accepts `config.yaml`, `package.json`, `notes.md`; rejects `app.ts`, `script.py`.
160
+ - `bmad-dev` accepts everything (no group-level `fileRegex` restriction on edit).
161
+ - 5.8 Slug-set export: `lib/merge/roomodes.js` exports the canonical `MA_AGENTS_OWNED_SLUGS` constant for downstream consumption (Stories 21.10, 21.11).
162
+
163
+ **Integration tests for `applyExtraInstructionTemplates`:**
164
+
165
+ - 5.9 Fresh install in tmp project with profile=standard, Roo Code selected: `<tmpRoot>/.roomodes` is created with the four ma-agents entries; no `/no_think` or other on-prem strings present.
166
+ - 5.10 Re-install in tmp project where `<tmpRoot>/.roomodes` already contains a user entry `slug: my-custom-mode`: that entry is preserved alongside the four ma-agents entries; second consecutive install produces byte-identical file content (NFR46).
167
+ - 5.11 Roo Code NOT selected: `<tmpRoot>/.roomodes` is not created or modified by ma-agents (AC #13).
168
+ - 5.12 OpenCode-only selection: `opencode.json::instructions[]` json-merge path still functions unchanged (NFR18 non-regression — AC #12).
169
+
170
+ **Test data isolation:** Stub the absence of `lib/templates/instruction-block-onprem.template.md` — Story 21.6 has not landed yet, so the on-prem layer must gracefully fall back to universal-only per Story 21.2 AC #3. Tests must NOT depend on Story 21.6 being delivered.
171
+
172
+ **Coverage note:** Story 21.9 adds (a) the cross-tool standard-profile-vs-baseline byte test, (b) the on-prem profile must-have-both-layers test, (c) the cross-cutting slug-collision integration test, and (d) the NFR47 enforcement contract end-to-end. Do not duplicate (c) and (d) here — keep this story's tests at the unit/per-merger level.
173
+
174
+ ## Out of Scope
175
+
176
+ - On-prem layer additions to `customInstructions` per mode (Story 21.6, epic technical note at line 4074).
177
+ - `AGENTS.md` template for OpenCode (Story 21.4).
178
+ - `.clinerules` template extension for Cline (Story 21.5).
179
+ - BMAD persona phase prefix in customize-loader (Story 21.7).
180
+ - vLLM deployment doc and README on-prem section (Story 21.8).
181
+ - End-to-end Roo Code launch verifying `FileRestrictionError` at the IDE layer (`epics.md` line 4152 explicitly limits NFR47 to regex-contract testing).
182
+ - Profile-reconfigure backup / `RoomodesSlugDivergenceError` flow (Story 21.10).
183
+ - Profile-uninstall slug removal flow (Story 21.11).
184
+ - Bumping `manifestVersion` — already at `1.2.0` from Story 21.1; not touched here.
185
+ - Modifying the OpenCode json-merge path or any other agent's instruction injection.
103
186
 
104
187
  ## Change Log
105
- - 2026-04-14: Story created (Epic 21, Story 21.3)
106
- - 2026-04-14: Added ACs #9 and #10 for slug-stomp protection and overwrite audit log (Findings #9 and #20, corrective plan step 3). New CLI flag: `--force-roomodes-overwrite`. New `.ma-agents.json` field: `roomodesOverwriteLog` (append-only, capped at 50). AC #5's "colliding slug overwrite with console warning" is superseded for the four ma-agents-owned slugs by AC #9's abort-and-require-force behavior; AC #5 still governs behavior for user-owned slugs (preserved untouched).
188
+
189
+ - 2026-04-15: Story created (Epic 21, Story 21.3). ACs structured with explicit (gap-fill) flags per the epic's AC scope. Three Open questions raised inline (YAML library choice, install-loop call site, Roo Code `customModes` schema version) rather than guessed. NFR44/NFR46/NFR47/NFR18 and FR176 citations made explicit. Verified source-tree surface table distinguishes existing vs. new paths. Status set to Ready.
190
+ - 2026-04-15 (adversarial-review resolution): Applied canonical decisions A/B/C. (P1 #9) AC #1 pins template path `lib/templates/roomodes.template.yaml` per `epics.md:3994`. (P1 #3) Three Open questions resolved inline as design decisions: YAML library = `js-yaml` (verified `js-yaml@4.1.1` via `npm ls js-yaml` — transitively present through `bmad-method@6.2.2`; promoted to direct dependency, Task 2.5); installer call site = `lib/installer.js` (single stamper owner); Roo Code schema = authoritative shape cited in `lib/agents.js` roo-code entry. (P1 #4) Merger (`mergeRoomodes`) no longer calls `composeInstructionBlock`; it receives the composed string as input — stamper owns composer invocation and substitution. (P1 #10) Terminology normalized to composer/merger/stamper throughout. (Decision C) Backup filename `<target>.backup-<ISO-8601-timestamp>` cited as 21.2-owned in Upstream dependencies. All Tasks are unconditional; Status remains Ready.
191
+ - 2026-04-15: Story 21.3 implemented — `lib/templates/roomodes.template.yaml`, `lib/merge/roomodes.js` (pure splicer with `MA_AGENTS_OWNED_SLUGS` export), `applyExtraInstructionTemplates` stamper in `lib/installer.js`, `extraInstructionTemplates` field on roo-code agent only, `js-yaml@^4.1.1` promoted to direct dep, 12 new tests in `test/roomodes.test.js`. Indent-preserving `{{UNIVERSAL_BLOCK}}` substitution keeps YAML block scalars valid after expansion. Status Ready → Review.
192
+ - 2026-04-15 (rebase integration onto PR #45 / Story 21.4): consolidated parallel dispatcher. 21.3's `applyExtraInstructionTemplates` was merged into 21.4's canonical `stampExtraInstructionTemplates` by adding a `yaml-customModes` branch (with indent-preserving `{{UNIVERSAL_BLOCK}}` substitution and Story 21.2 backup semantics). Orphan function deleted. Explicit sibling call-site in the install loop removed — `updateAgentInstructions` already invokes the dispatcher. `test/roomodes.test.js` updated to import `stampExtraInstructionTemplates`. All 12 roomodes tests + the full suite still pass. No AC coverage weakened.
193
+
194
+ ## Dev Agent Record
195
+
196
+ ### Agent Model Used
197
+ Claude Opus 4.6 (1M context) — bmad-dev-story + bmad-review-adversarial-general + bmad-review-edge-case-hunter flow.
198
+
199
+ ### Completion Notes
200
+ - Template at `lib/templates/roomodes.template.yaml` ships four modes with `fileRegex` per AC #1. The template comment header explicitly disclaims containing the sentinel literally, and the sentinel appears exactly four times — one per mode's `customInstructions` block scalar.
201
+ - Stamper substitutes `{{UNIVERSAL_BLOCK}}` with indent preservation (per-sentinel leading whitespace captured via regex and prepended to every line of the composed universal block). This keeps the YAML block-scalar indent valid after expansion without requiring the composer to know its caller's layout.
202
+ - `mergeRoomodes` is a pure splicer — no `composeInstructionBlock` import. It preserves non-`customModes` top-level keys (FR176), emits one `console.warn` per colliding slug with the slug cited verbatim, and pins `js-yaml` dump options for byte-identity across runs (NFR46).
203
+ - `applyExtraInstructionTemplates` is wired into the install loop siblings to `updateAgentInstructions` (both install and remove paths). Uninstall path is intentionally NOT wired — Story 21.11 owns profile-uninstall slug removal (uses `MA_AGENTS_OWNED_SLUGS` export).
204
+ - Canonical backup format from Story 21.2 (`buildBackupFilename`) is reused — no new format invented.
205
+
206
+ ### Adversarial Review Findings (self-review)
207
+
208
+ | # | Layer | Severity | Finding | Disposition |
209
+ |---|-------|----------|---------|-------------|
210
+ | 1 | Cynical | P1 | Naive `{{UNIVERSAL_BLOCK}}` replace would break YAML block-scalar indentation because the composed content contains colons and multi-line markdown | **Fixed** — regex-based indent-preserving substitution in the stamper + mirrored in test helper |
211
+ | 2 | Cynical | P1 | `js-yaml` declared as `^4.1.0` but story pins `^4.1.1`; SemVer floor lower than required | **Fixed** — bumped to `^4.1.1` |
212
+ | 3 | Cynical | P2 | Uninstall-path `updateAgentInstructions` call site not paired with `applyExtraInstructionTemplates` | **Out of scope** — story 21.11 owns profile-uninstall slug removal |
213
+ | 4 | Edge-case | P1 | `parseYaml` on malformed YAML previously called `console.warn` silently. Now throws with label `existing`/`template` so failures surface with context | **Fixed** — descriptive Error thrown |
214
+ | 5 | Edge-case | P1 | Empty `templateYaml.customModes` could produce `.roomodes` with zero modes | **Fixed** — merger throws if template has no modes |
215
+ | 6 | Edge-case | P2 | Malformed user entries (null, non-object, missing slug) could crash the Set lookup | **Fixed** — defensive slug extraction; non-object and slug-less entries pass through unchanged (they cannot collide) |
216
+ | 7 | Edge-case | P2 | `MA_AGENTS_OWNED_SLUGS` mutability would let downstream consumers corrupt the canonical order | **Fixed** — exported as `Object.freeze([...])`; test 5.8 asserts immutability |
217
+ | 8 | Edge-case | P2 | Backup could clobber prior same-second run | **Inherited fix** — reuses `buildBackupFilename` which already appends `.N` tiebreaker (Story 21.2) |
218
+
219
+ All P0/P1 findings resolved. P2 "out of scope" items deferred to Stories 21.10/21.11 per spec.
220
+
221
+ ### File List
222
+ - CREATED: `lib/templates/roomodes.template.yaml` (four ma-agents-owned modes with fileRegex per AC #1)
223
+ - CREATED: `lib/merge/roomodes.js` (pure splicer + `MA_AGENTS_OWNED_SLUGS` export)
224
+ - CREATED: `test/roomodes.test.js` (12 tests, all passing)
225
+ - MODIFIED: `lib/installer.js` (post-rebase: `yaml-customModes` branch integrated into Story 21.4's `stampExtraInstructionTemplates` dispatcher; no parallel function)
226
+ - MODIFIED: `lib/agents.js` (roo-code `extraInstructionTemplates` field)
227
+ - MODIFIED: `package.json` (js-yaml `^4.1.1` direct dep; test script entry)
228
+ - MODIFIED: `package-lock.json` (js-yaml direct lock entry)
229
+ - MODIFIED: `_bmad-output/implementation-artifacts/21-3-roomodes-template-bmad-modes.md` (Status → Review; Dev Agent Record / File List / Change Log)