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
@@ -0,0 +1,10 @@
1
+ {
2
+ "manifestVersion": "1.2.0",
3
+ "agent": null,
4
+ "agents": [
5
+ null
6
+ ],
7
+ "scope": "project",
8
+ "skills": {},
9
+ "profile": "standard"
10
+ }
package/AGENTS.md ADDED
@@ -0,0 +1,97 @@
1
+ <!-- Generated by ma-agents. Edit outside the MA-AGENTS-START/END markers to preserve your changes. -->
2
+ # Project Agent Instructions
3
+
4
+ This file is auto-discovered by OpenCode and any agent that respects `AGENTS.md`.
5
+ It establishes universal safety rules, text-vs-file discipline, and BMAD phase
6
+ discipline for every agent operating in this project.
7
+
8
+ ## Universal Rules
9
+
10
+ The universal rules block below is stamped and maintained by ma-agents. Edit
11
+ outside the HTML-comment `MA-AGENTS` start and end markers to preserve your
12
+ own additions — content inside the markers is regenerated on every install.
13
+
14
+ <!-- MA-AGENTS-START -->
15
+ # AI Agent Skills - Planning Instruction
16
+
17
+ You have access to a library of skills in your skills directory. Before starting any task:
18
+
19
+ 1. Read the skill manifest at .opencode/skills/MANIFEST.yaml
20
+ 2. Based on the task description, select which skills are relevant
21
+ 3. Read only the selected skill files
22
+ 4. Then proceed with the task
23
+
24
+ Always load skills marked with always_load: true.
25
+ Do not load skills that are not relevant to the current task.
26
+
27
+ ## Respond in TEXT vs. create FILES
28
+
29
+ Choose your response medium deliberately. Defaulting to file creation when the user asked a question is a common failure mode — especially for coding agents running in web UIs.
30
+
31
+ - **Create or modify FILES when the user's request contains file-action keywords:** `create`, `write`, `generate`, `build`, `implement` (and obvious synonyms such as `add`, `produce`, `refactor`, `fix`, `update <file>`). These signal a concrete artifact is expected.
32
+ - **Respond in TEXT when the request contains text-response keywords:** `what do you think`, `how should we`, `discuss`, `opinion` (and obvious synonyms such as `explain`, `why`, `should I`, `compare`, `recommend`). These signal that a conversation is expected, not a deliverable.
33
+ - **If unsure, respond in TEXT.** A text answer can always be followed by file creation on confirmation; an unwanted file cannot be cleanly undone.
34
+ - **Never create `response.md`, `output.md`, or any similarly named scratch file as a reply.** A reply belongs in the chat transcript, not on disk.
35
+ - **Confirm file paths before writing.** When you are about to create or modify a file whose path the user has not explicitly named, state the intended path in text and wait for confirmation, unless the path is unambiguous from the task context.
36
+
37
+ ## BMAD phase discipline
38
+
39
+ BMAD-METHOD organizes work into declared phases (analysis, planning, architecture, story-creation, implementation, review). Respect the currently declared phase.
40
+
41
+ - **Do not skip ahead to implementation during planning.** If the project is in a planning phase — or the user has asked for requirements, architecture, or a story — produce planning artifacts, not code.
42
+ - **Do not retroactively plan after you have already coded.** If implementation has already started, flag the gap instead of fabricating back-dated planning documents.
43
+ - The declared phase is established by the active skill, the story status, or an explicit statement from the user. When none of these is available, ask before assuming.
44
+ <!-- MA-AGENTS-END -->
45
+
46
+ ## Critical Behavior Rules
47
+
48
+ These rules are non-negotiable across every profile and every agent.
49
+
50
+ - **Never create files in `~/.claude/` or any user home directory.** All
51
+ project artifacts must land inside the current working directory. Home-
52
+ directory writes cross-contaminate between projects and are a common source
53
+ of secret/config leakage.
54
+ - **Never write outside the project root without an explicit user request
55
+ naming the absolute path.** "Write to disk" means the project, not the
56
+ operator's machine.
57
+ - **Do not modify files you did not read first.** Read the current content
58
+ before proposing or performing an edit — blind writes silently destroy user
59
+ work.
60
+
61
+ ## BMAD Phase Declaration
62
+
63
+ BMAD-METHOD organizes work into four phases. Respect the currently declared
64
+ phase; do not skip ahead to the next phase without a phase transition signal
65
+ from the user, the active skill, or the story status.
66
+
67
+ - **Discovery / PM (analysis, planning).** Deliverables: product briefs,
68
+ PRDs, market and domain research, epics and stories lists. Do NOT produce
69
+ code, architecture diagrams, or implementation artifacts in this phase.
70
+ When asked "what do you think", respond in text.
71
+ - **Architecture.** Deliverables: solution design, component boundaries,
72
+ data-flow, interface contracts. Do NOT write application code or skill
73
+ implementations. Narrate decisions and capture them as documents.
74
+ - **Tech Lead / Stories.** Deliverables: individual story files with full
75
+ acceptance criteria, task breakdowns, and dev notes. Do NOT begin
76
+ implementation — stories are contracts the implementer consumes later.
77
+ - **Implementation.** Deliverables: code, tests, and the Dev Agent Record on
78
+ the story file. At this phase, write files. Do NOT retroactively fabricate
79
+ planning documents for code that already exists — flag the gap instead.
80
+
81
+ When no phase is declared (no active skill, no story in progress, no explicit
82
+ user statement), ask before assuming.
83
+
84
+ ## Project BMAD Output Structure
85
+
86
+ BMAD artifacts live under `_bmad-output/` (or the paths configured in
87
+ `_bmad/bmm/config.yaml` when present). The install-time resolver logs the
88
+ resolved paths on each run; consult that log output if in doubt.
89
+
90
+ - **Planning artifacts** — PRDs, product briefs, market and domain research.
91
+ - **Architecture artifacts** — solution design, component boundaries. May be
92
+ co-located with planning artifacts when no separate directory is configured.
93
+ - **Implementation artifacts (stories)** — individual story files and their
94
+ Dev Agent Records.
95
+
96
+ Always consult the `MANIFEST.yaml` referenced inside the universal block above
97
+ for the full list of installed skills and their locations.
package/MANIFEST.yaml ADDED
@@ -0,0 +1,3 @@
1
+ # MANIFEST.yaml
2
+
3
+ skills:
package/README.md CHANGED
@@ -103,6 +103,23 @@ The file is version-controlled as part of `_bmad-output/` project knowledge. Com
103
103
 
104
104
  ---
105
105
 
106
+ ## On-Prem / Air-Gapped Deployment
107
+
108
+ When running AI coding agents against a locally-hosted LLM (e.g., Nemotron Super 49B on vLLM), `ma-agents install` asks a one-time profile question at setup:
109
+
110
+ ```
111
+ ? Profile (standard / on-prem):
112
+ ```
113
+
114
+ Your answer is persisted in `.ma-agents.json` under the `profile` field and is read on every subsequent install, update, or reconfigure. The `standard` profile produces the same output as always; the `on-prem` profile additionally stamps on-prem-specific guardrails into every agent's instruction block — including `/no_think` directives for planning personas, `str_replace_editor` prohibition, and home-directory write restrictions.
115
+
116
+ To change your profile after initial setup: `npx ma-agents reconfigure`
117
+ To remove all on-prem profile artifacts: `npx ma-agents uninstall --profile-artifacts`
118
+
119
+ For vLLM server configuration, quantization tradeoffs, per-phase sampling parameters, and the `str_replace_editor` hallucination mitigation, see [`docs/deployment/vllm-nemotron.md`](docs/deployment/vllm-nemotron.md).
120
+
121
+ ---
122
+
106
123
  ## Supported Coding Tools
107
124
 
108
125
  Skills can be installed into any of these AI coding agents:
@@ -1,6 +1,6 @@
1
1
  # Story 21.10: Profile Reconfigure
2
2
 
3
- Status: ready-for-dev
3
+ Status: Review
4
4
 
5
5
  ## Story
6
6
 
@@ -117,21 +117,45 @@ Match the existing test layout (node:test or mocha-style — verify by reading o
117
117
 
118
118
  ### Epic 21 Cross-Story Context
119
119
 
120
- **Story 21.10 (this):** Reconfigure subcommand — escape hatch for a previously-persisted profile. Depends on Stories 21.1 (profile API), 21.2 (marker injection + backup convention), 21.3 (slug-stomp protection), 21.5 (dual-file drift detection), 21.6 (on-prem layer composition — the actual content being re-stamped). Runs after 21.6 in the execution order; must exist before 21.9's end-to-end tests so the round-trip test (21.9 AC #1 (f)) can exercise the real command instead of manual `.ma-agents.json` edits.
120
+ **Story 21.10 (this):** Reconfigure subcommand — escape hatch for a previously-persisted profile. Depends on Stories 21.1 (profile API), 21.2 (marker injection + backup convention), 21.3 (slug-stomp protection), 21.4 (`AGENTS.md` template + `markdown-markers` merger — re-stamped on profile flip), 21.5 (dual-file drift detection), 21.6 (on-prem layer composition — the actual content being re-stamped), 21.7 (BMAD persona `on_prem_phase_prefix` — re-composed when profile flips, per Task 3.3). Runs after 21.6 in the execution order; must exist before 21.9's end-to-end tests so the round-trip test (21.9 AC #1 (f)) can exercise the real command instead of manual `.ma-agents.json` edits.
121
121
 
122
122
  ## Dev Agent Record
123
123
 
124
124
  ### Agent Model Used
125
- _(to be filled by dev agent)_
125
+ Claude Opus 4.6 (1M context) bmad-dev-story flow.
126
126
 
127
127
  ### Debug Log References
128
- _(to be filled by dev agent)_
128
+ - `.roomodes` slug-divergence comparison deliberately strips `customInstructions` because installed files already carry the profile-composed universal block; a profile flip legitimately rewrites that field. `whenToUse`, `roleDefinition`, `groups`, `name` are the user-editable surfaces the check catches. Documented inline in `lib/reconfigure.js::checkRoomodesSlugDivergence`.
129
+ - `updateAgentInstructions` is invoked with `yesMode: true` from the reconfigure loop so per-file drift prompts do not re-ask after the global `Continue?` confirmation (AC #7). The installer's `handleMarkerBlockDrift` still emits the pinned WARNING line and writes the canonical `<target>.backup-<ISO>` sibling (AC #8).
130
+ - `appendProfileHistory` does a fresh read-modify-write of `.ma-agents.json` so it composes with `setProfile`'s write — no parallel JSON-IO path. Cap enforcement is `shift()`-based (oldest-first eviction).
129
131
 
130
132
  ### Completion Notes List
131
- _(to be filled by dev agent)_
133
+ - New `lib/reconfigure.js` orchestrator exports `reconfigure({ projectRoot, argv, promptsLib, now })` plus three named error classes (`RoomodesSlugDivergenceError`, `ManifestNotFoundError`, `ReconfigureYesRejectedError`) and the `PROFILE_HISTORY_CAP` constant (20).
134
+ - `bin/cli.js` registers the `reconfigure` verb and routes to `handleReconfigure`, which maps each error class to a user-facing exit(1) message. `--yes` on reconfigure exits nonzero with the pinned message verbatim (AC #6).
135
+ - Re-stamp reuses the canonical `updateAgentInstructions` path from `lib/installer.js`; zero forked logic. Backups are produced by the installer's existing drift handler using the canonical `buildBackupFilename` helper owned by Story 21.2.
136
+ - `checkRoomodesSlugDivergence` throws `RoomodesSlugDivergenceError` when any ma-agents-owned slug present in the existing `.roomodes` differs from the shipped template on non-customInstructions fields; `--force-roomodes-overwrite` bypasses the check (AC #9). `.clinerules` dual-file drift is delegated to Story 21.5's `checkClinerulesDualFileDrift` (no override, AC #10).
137
+ - `profileHistory` append uses a fresh read-modify-write that composes with `setProfile`'s write — capped at 20, oldest-first eviction (AC #11). Missing-field start is handled (first reconfigure creates the array).
138
+ - Prompt shape mirrors Story 21.1's wizard prompt (same two choices) but with `initial` set to the persisted value's row and the message `Current profile: <value>. Change to?` per AC #2.
139
+
140
+ ### Adversarial Review Findings
141
+
142
+ | # | Layer | Severity | Finding | Disposition |
143
+ |---|-------|----------|---------|-------------|
144
+ | 1 | Cynical | P1 | setProfile runs BEFORE re-stamp loop; mid-flight re-stamp failure leaves profile+artifacts out of sync | Accepted — story explicitly lists "Auto-rollback" as out of scope; `profileHistory` append is gated on full loop success so forensic log does not falsely record a partial reconfigure |
145
+ | 2 | Cynical | P2 | `--yes` detection is string-includes on argv; benign for reconfigure (no positional args) | Accepted — same pattern as installer; reconfigure AC #1 forbids positional args |
146
+ | 3 | Edge-case | P1 | Slug-divergence check drops `customInstructions` from the compare | Correct by design — installer-stamped `customInstructions` carries the composed block which legitimately changes per profile; other fields remain user-edit detectors. Test 8.7 exercises `whenToUse` drift |
147
+ | 4 | Cynical | P2 | `appendProfileHistory` is read-modify-write, not atomic rename | Accepted — matches `setProfile`'s write pattern; reconfigure is interactive-only so concurrency isn't a realistic vector |
148
+ | 5 | Edge-case | P2 | Same-value short-circuit when `persistedProfile === undefined` requires user to pick a value; cannot short-circuit to undefined | Accepted — prompt choices are `on-prem` | `standard` only; chosenProfile is always a concrete value |
149
+ | 6 | Edge-case | P2 | No help-text doc for `--force-roomodes-overwrite` | **Fixed** — added Reconfigure options block in `showHelp()` |
132
150
 
133
151
  ### File List
134
- _(to be filled by dev agent)_
152
+ - CREATED: `lib/reconfigure.js`
153
+ - CREATED: `test/reconfigure.test.js` (12 tests, all passing)
154
+ - MODIFIED: `bin/cli.js` (new `reconfigure` case + `handleReconfigure` + help text)
155
+ - MODIFIED: `package.json` (added `test/reconfigure.test.js` to npm test script)
156
+ - MODIFIED: `_bmad-output/implementation-artifacts/21-10-profile-reconfigure.md` (Dev Agent Record / File List / Change Log)
135
157
 
136
158
  ## Change Log
159
+ - 2026-04-15: Story 21.10 implemented — reconfigure CLI verb + orchestrator + slug/dual-file guards + profileHistory append. Status ready-for-dev → review.
160
+ - 2026-04-15: Upstream dependency list completed per adversarial-review finding — added Stories 21.4 (`AGENTS.md` re-stamping surface), 21.7 (BMAD persona phase prefix re-composition). Restores Dependencies bidirectionality with those stories' downstream lists.
137
161
  - 2026-04-14: Story created (Epic 21, Story 21.10). Closes adversarial-review Findings #5 and #7 (no escape hatch for persisted profile; CI-default silent-downgrade trap).
@@ -129,7 +129,7 @@ Story 21.1 AC #1 pinned `lib/profile.js` to exactly three exports: `getProfile`,
129
129
 
130
130
  ### Epic 21 Cross-Story Context
131
131
 
132
- **Story 21.11 (this):** Uninstall subcommand — rollback path for profile-dependent content. Depends on Stories 21.1 (profile API — extends it), 21.2 (marker injection + backup convention), 21.3 (slug list + audit log), 21.6 (on-prem layer composition — reverses it), 21.10 (`profileHistory` field — preserves + appends). Runs LAST in the Epic 21 execution order (after 21.9) because the 21.9 end-to-end test harness needs uninstall as part of round-trip coverage, and uninstall must not land before all upstream stamping work is committable.
132
+ **Story 21.11 (this):** Uninstall subcommand — rollback path for profile-dependent content. Depends on Stories 21.1 (profile API — extends it), 21.2 (marker injection + backup convention), 21.3 (slug list + audit log), 21.4 (`AGENTS.md` — removes the ma-agents-stamped file / marker block), 21.5 (`.clinerules` + `.cline/clinerules.md` — removes both Cline rule files' ma-agents marker blocks), 21.6 (on-prem layer composition — reverses it), 21.7 (BMAD persona `on_prem_phase_prefix` — strips prefix when uninstalling profile artifacts), 21.10 (`profileHistory` field — preserves + appends). Runs LAST in the Epic 21 execution order (after 21.9) because the 21.9 end-to-end test harness needs uninstall as part of round-trip coverage, and uninstall must not land before all upstream stamping work is committable.
133
133
 
134
134
  ## Dev Agent Record
135
135
 
@@ -146,4 +146,5 @@ _(to be filled by dev agent)_
146
146
  _(to be filled by dev agent)_
147
147
 
148
148
  ## Change Log
149
+ - 2026-04-15: Upstream dependency list completed per adversarial-review finding — added Stories 21.4 (`AGENTS.md` removal surface), 21.5 (`.clinerules` dual-file removal surface), 21.7 (BMAD persona phase-prefix strip on uninstall). Restores Dependencies bidirectionality with those stories' downstream lists.
149
150
  - 2026-04-14: Story created (Epic 21, Story 21.11). Closes adversarial-review Finding #17 (no uninstall / rollback path).
@@ -1,6 +1,6 @@
1
1
  # Story 21.2: Universal Per-Tool Instruction Block Expansion
2
2
 
3
- Status: backlog
3
+ Status: Review
4
4
 
5
5
  ## Story
6
6
 
@@ -10,89 +10,244 @@ So that all coding agents — even Claude on the web — stop dumping random fil
10
10
 
11
11
  ## Acceptance Criteria
12
12
 
13
- 1. New template file `lib/templates/instruction-block-universal.template.md` exists and contains, in addition to the current MANIFEST loading instruction:
14
- - A "Respond in TEXT vs. create FILES" rules section with concrete keyword triggers — file-action keywords (`create`, `write`, `generate`, `build`, `implement`) and text-response keywords (`what do you think`, `how should we`, `discuss`, `opinion`)
15
- - An "if unsure, respond in text" default rule
16
- - A "never create `response.md` or `output.md` as a reply" rule
17
- - A BMAD phase discipline rule: respect the declared phase; do not skip ahead to implementation during planning
18
- - A "confirm file paths before writing" rule
19
- 2. The template contains `{{MANIFEST_PATH}}` exactly once as a placeholder; no agent-specific paths are hardcoded in the template source.
20
- 3. A new function `composeInstructionBlock({ profile, manifestPath })` in `lib/installer.js` reads the universal template, stamps `{{MANIFEST_PATH}}`, and returns the content. When `profile === 'on-prem'`, it appends the on-prem template content (Story 21.6 wires that file; this story stubs the call site with a graceful fallback if the on-prem template is absent).
21
- 4. The injection function in `lib/installer.js` that writes per-tool instruction files (the existing marker-based path) calls `composeInstructionBlock` and emits the content within `<!-- MA-AGENTS-START -->` / `<!-- MA-AGENTS-END -->` markers.
22
- 5. For every existing markdown-injection agent (Claude Code, Cline, Roo Code rules, Cursor, Kilocode, Copilot, Gemini), a fresh install produces an instruction file containing the universal block. Existing user content outside the markers is preserved byte-for-byte (NFR5, NFR46).
23
- 6. Two consecutive installs with the same profile and project state produce byte-identical content within the marker block (NFR46).
24
- 7. The block must NOT mention `/no_think`, `str_replace_editor`, `~/.claude/`, or any local-LLM-specific concept those belong in the on-prem template (Story 21.6). Verified by grep on the rendered standard-profile output.
25
- 8. **Upgrade-safety (marker-block hand-edit detection).** When `npx ma-agents install` runs against an already-installed project, before overwriting the content inside `<!-- MA-AGENTS-START -->`/`<!-- MA-AGENTS-END -->` markers the installer compares the existing marker-block content against the ma-agents-generated content for the project's previously recorded profile+version. If the existing content differs from what ma-agents would have produced (i.e., the user hand-edited inside the markers):
26
- - **Interactive mode:** the installer surfaces a diff of the in-marker changes and requires explicit confirmation before proceeding to overwrite.
27
- - **`--yes` mode:** the prompt is skipped but the installer emits a visible migration warning line that CI will capture, in the exact format `WARNING: ma-agents marker-block content modified since last install — overwriting. Previous content backed up to <path>.backup-<timestamp>`.
28
- - **Backup:** before overwriting, the installer writes a backup sibling file alongside the target — same directory, same basename, suffix `.backup-<ISO-timestamp>` (example: `CLAUDE.md.backup-2026-04-14T12-34-56Z`). Only the portion between (and including) the markers is written to the backup; the rest of the target file is untouched by the installer and therefore needs no backup. Rationale: hand-edits inside markers represent work the user expects to keep; silent overwrite is a data-loss regression.
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".
14
+
15
+ 1. **Universal template file exists.** A new file `lib/templates/instruction-block-universal.template.md` (new) is present and contains, in addition to the current MANIFEST loading instruction already emitted by `updateAgentInstructions` in `lib/installer.js`:
16
+ - A "Respond in TEXT vs. create FILES" rules section listing concrete keyword triggers: file-action keywords (`create`, `write`, `generate`, `build`, `implement`) and text-response keywords (`what do you think`, `how should we`, `discuss`, `opinion`).
17
+ - An "if unsure, respond in text" default rule.
18
+ - A "never create `response.md` or `output.md` as a reply" rule.
19
+ - A BMAD phase discipline rule: respect the declared phase; do not skip ahead to implementation during planning.
20
+ - A "confirm file paths before writing" rule.
21
+ 2. **(gap-fill) Placeholder contract.** The template source contains `{{MANIFEST_PATH}}` exactly once as the only placeholder; no agent-specific paths are hardcoded. The existing per-agent manifest-path computation in `updateAgentInstructions` (`lib/installer.js` line ~369 / ~408 `path.relative(projectRoot, path.join(agentProjectPath, 'MANIFEST.yaml'))`) is the value substituted in.
22
+ 3. **(gap-fill) Composer function.** A new function `composeInstructionBlock({ profile, projectRoot })` (new) in `lib/installer.js`:
23
+ - Reads `lib/templates/instruction-block-universal.template.md` (always).
24
+ - When `profile === 'on-prem'`, reads `lib/templates/instruction-block-onprem.template.md` (new, ships in Story 21.6) and appends it to the universal content separated by a single blank line.
25
+ - When `profile === 'on-prem'` AND the on-prem template file is missing, **throws** `Error('on-prem profile selected but instruction-block-onprem.template.md is missing')`. NO silent fallback.
26
+ - When `profile !== 'on-prem'`, returns the universal content only.
27
+ - Templates contain NO `{{...}}` placeholders; any substitution is the caller's responsibility, done AFTER composition.
28
+ - Single-owner contract: `lib/installer.js` calls `composeInstructionBlock` exactly once per stamped artifact. Mergers receive the already-composed string; mergers do not call `composeInstructionBlock` themselves.
29
+ - Is the **single composer entry point** consumed by stories 21.3 (roomodes mode `customInstructions`), 21.4 (AGENTS.md), 21.5 (.clinerules), and 21.6 (on-prem content). Those stories extend behavior by adding inputs to `composeInstructionBlock` or sibling composers — they do not duplicate the text-vs-file / phase-discipline rules.
30
+ 4. **Per-tool merger wired.** The existing marker-based merger in `updateAgentInstructions` (`lib/installer.js`, markers `<!-- MA-AGENTS-START -->` / `<!-- MA-AGENTS-END -->`, see `lib/installer.js` lines ~424–459) calls `composeInstructionBlock` with the project's resolved profile (obtained via `getProfile(projectRoot)` from `lib/profile.js`) and emits the composed content between the markers.
31
+ 5. **All markdown-merger agents receive the block.** For every agent in `lib/agents.js` whose `instructionFiles[]` is a markdown file — Claude Code (`.claude/CLAUDE.md`), Cline (`.cline/clinerules.md` and `.clinerules`), Roo Code (`.roo/rules/00-ma-agents.md`), Cursor (`.cursor/cursor.md`), Kilocode (`.kilocode/kilocode.md`), Copilot (`.github/copilot/copilot.md`), Gemini (`.gemini/gemini.md`) — a fresh install produces an instruction file whose marker block contains the universal block content. Content outside the markers is preserved byte-for-byte (**NFR5 additive-only contract; NFR46 idempotency**).
32
+ 6. **Idempotency (NFR46).** Two consecutive installs with the same profile and project state produce byte-identical content inside the marker block. The per-install header/footer contains no timestamps, random IDs, or ordering-sensitive content.
33
+ 7. **Profile isolation (NFR44).** The universal block content MUST NOT mention `/no_think`, `str_replace_editor`, `~/.claude/`, or any local-LLM-specific concept. Those strings belong in the on-prem template (Story 21.6). Verified by a grep-style assertion on rendered standard-profile output: absence of each of those three strings.
34
+ 8. **(gap-fill) OpenCode json-merge coexistence (NFR18).** The OpenCode agent uses `injectionStrategy.position === 'json-merge'` (`lib/agents.js` lines ~244, `lib/installer.js` lines ~365–405) writing into `opencode.json::instructions[]` as a plain string. The json-merge path in `updateAgentInstructions` MUST also emit the universal block content (inline in the string, no file markers because the target is JSON). The ma-agents entry is identified by its existing `[ma-agents]` prefix marker; stale entries are replaced, user entries preserved (**NFR18 additive-only JSON merge**). No other keys in `opencode.json` are touched.
35
+ 9. **(gap-fill) BMAD agent instruction files unchanged in scope.** The BMAD-category branch in `updateAgentInstructions` (`lib/installer.js` line ~445) that skips missing BMAD instruction files ("BMAD agent file not yet deployed") remains unchanged. When a BMAD agent file does exist and has markers, the universal block is re-rendered inside its markers on re-install. This story does not modify `applyCustomizations` or BMAD persona prompt composition (those belong to Stories 21.6/21.7).
36
+ 10. **(gap-fill) Upgrade-safety: marker-block hand-edit detection.** Before overwriting content inside the markers, the installer compares existing in-marker content against the content `composeInstructionBlock` would produce for the project's current resolved profile. If they differ:
37
+ - **Interactive mode:** the installer prints a diff of the in-marker changes and requires explicit confirmation before proceeding.
38
+ - **`--yes` mode:** the prompt is skipped but the installer prints the warning line verbatim: `WARNING: ma-agents marker-block content modified since last install — overwriting. Previous content backed up to <path>.backup-<timestamp>`.
39
+ - **Backup:** before overwriting, the installer writes a sibling backup file at `<target>.backup-<ISO-timestamp>` (UTC, `YYYY-MM-DDTHH-mm-ssZ`, colons replaced with hyphens for Windows compatibility). The backup contains only the original marker-block region (including the marker lines themselves). The rest of the target file is untouched by the installer and therefore needs no backup.
40
+
41
+ > **Open question:** The epic does not specify what to do when the existing in-marker content matches ma-agents output for the *previous* profile but not the *current* profile (e.g., user ran `reconfigure`). Story 21.10 (Profile Reconfigure) introduces its own backup flow. Proposed resolution: AC #10's drift detection compares against the current profile's expected output only — reconfigure-driven rewrites go through Story 21.10's path, not Story 21.2's. To be confirmed during Story 21.10 dev.
42
+
43
+ > **Open question:** Epic AC requires byte-identical marker-block content across runs (NFR46). The current `updateAgentInstructions` trims the wrapped instruction with `.replace(regex, wrappedInstruction.trim())` on replace but uses the non-trimmed form on first insert (line ~443). This is a pre-existing asymmetry that must be normalized so both paths produce identical trailing whitespace. Flagged for the implementing dev to resolve; not a new AC because the epic simply mandates byte-identity and leaves the mechanism to the implementation.
29
44
 
30
45
  ## Tasks / Subtasks
31
46
 
32
- - [ ] Task 1: Create `lib/templates/instruction-block-universal.template.md` (AC #1, #2, #7)
33
- - [ ] Task 2: Implement `composeInstructionBlock({ profile, manifestPath })` in `lib/installer.js` (AC #3)
34
- - [ ] 2.1 Read universal template via existing `fs.readFile` pattern
35
- - [ ] 2.2 Stamp `{{MANIFEST_PATH}}`
36
- - [ ] 2.3 If `profile === 'on-prem'` and `lib/templates/instruction-block-onprem.template.md` exists, append its content; otherwise return universal only (Story 21.6 ships the on-prem template)
37
- - [ ] Task 3: Wire `composeInstructionBlock` into the existing per-tool injection function so the marker block is rewritten with composed content (AC #4, #5)
38
- - [ ] Task 4: Verify additive behavior content outside markers preserved across runs (AC #5, #6)
39
- - [ ] Task 5: Tests in `test/instruction-block.test.js`
40
- - [ ] 5.1 Universal template stamps manifest path correctly
41
- - [ ] 5.2 `composeInstructionBlock({ profile: 'standard' })` excludes on-prem content even when on-prem template file exists
42
- - [ ] 5.3 `composeInstructionBlock({ profile: 'on-prem' })` includes on-prem content when present, falls back gracefully when absent (this story may stub absence — Story 21.6 will ship the file)
43
- - [ ] 5.4 Idempotency: two installs produce byte-identical marker-block content
44
- - [ ] 5.5 No on-prem-specific strings (`/no_think`, `str_replace_editor`, `~/.claude/`) appear in standard-profile output
45
- - [ ] 5.6 User content outside markers preserved across runs
46
- - [ ] Task 6 (upgrade-safety): Marker-block hand-edit detection, backup, and warning (AC #8)
47
- - [ ] 6.1 Before overwrite, compare existing in-marker content against expected ma-agents-generated content for the persisted profile+version
48
- - [ ] 6.2 On drift: interactive prompt shows a diff and requires confirmation; `--yes` bypasses the prompt and emits the pinned WARNING line so CI captures it
49
- - [ ] 6.3 Write `<target>.backup-<ISO-timestamp>` alongside the target, containing only the original marker-block region (including the marker lines themselves)
50
- - [ ] 6.4 Test: clean marker block no warning, no backup file; hand-edited marker block + `--yes` WARNING emitted and backup file exists with original content
47
+ - [x] **Task 1: Create universal template** (AC #1, #2, #7)
48
+ - [x] 1.1 Create `lib/templates/instruction-block-universal.template.md` (new) containing the MANIFEST loading section (with `{{MANIFEST_PATH}}` placeholder) followed by the five rule sections listed in AC #1.
49
+ - [x] 1.2 Verify by inspection that no string in the template matches `/no_think`, `str_replace_editor`, or `~/.claude/` (AC #7).
50
+
51
+ - [x] **Task 2: Implement `composeInstructionBlock`** (AC #3)
52
+ - [x] 2.1 Add `composeInstructionBlock({ profile, projectRoot })` to `lib/installer.js` (new function).
53
+ - [x] 2.2 Load the universal template from disk (synchronous `fs.readFileSync` to keep parity with adjacent helpers; module-load-time caching optional). Templates contain NO `{{...}}` placeholders — do not perform substitution inside the composer.
54
+ - [x] 2.3 If `profile === 'on-prem'`, read `lib/templates/instruction-block-onprem.template.md` and append its content separated by a single blank line. If that file is absent, throw `Error('on-prem profile selected but instruction-block-onprem.template.md is missing')`. NO silent fallback.
55
+ - [x] 2.4 Export from `lib/installer.js`'s `module.exports` so tests and Stories 21.3–21.6 can consume it.
56
+
57
+ - [x] **Task 3: Wire composer into the marker-based merger** (AC #4, #5, #9)
58
+ - [x] 3.1 In `updateAgentInstructions` (`lib/installer.js` line ~358), replace the hardcoded `planningInstruction` string (lines ~410–422) with a call to `composeInstructionBlock({ profile: getProfile(projectRoot) || 'standard', projectRoot })`, performing any `{{MANIFEST_PATH}}` substitution on the returned string AFTER composition (caller-owned).
59
+ - [x] 3.2 Keep the marker-wrap (`markerStart`/`markerEnd`) and the first-insert / in-place-replace split unchanged.
60
+ - [x] 3.3 Normalize trailing whitespace so the first-insert and in-place-replace paths produce byte-identical content inside the markers (resolves the Open question under AC).
61
+ - [x] 3.4 Leave the BMAD-category "skip if file missing" branch (line ~445) untouched (AC #9).
62
+
63
+ - [x] **Task 4: Wire composer into the json-merge (OpenCode) merger** (AC #8)
64
+ - [x] 4.1 In the `injectionStrategy.position === 'json-merge'` branch (`lib/installer.js` lines ~365–405), replace the hardcoded `instructionText` template literal (line ~370) with the composed content, prefixed by the existing `[${MA_AGENTS_SOURCE}]` tag used for ma-agents-entry identification.
65
+ - [x] 4.2 Confirm the filter `isMaEntry` still correctly identifies the new entry (prefix unchangedfilter unchanged).
66
+ - [x] 4.3 Verify no other keys in `opencode.json` are modified (NFR18).
67
+
68
+ - [x] **Task 5: Upgrade-safety — hand-edit detection, backup, warning** (AC #10)
69
+ - [x] 5.1 Before overwriting the in-marker region, extract current in-marker content and compare against `composeInstructionBlock(...)` output for the resolved profile.
70
+ - [x] 5.2 On drift in interactive mode: show unified diff (reuse existing prompts library); require confirmation.
71
+ - [x] 5.3 On drift with `--yes`: emit the pinned WARNING line (verbatim format from AC #10).
72
+ - [x] 5.4 Before overwrite, write `<target>.backup-<ISO-timestamp>` containing only the marker-block region including marker lines. Timestamp format: `YYYY-MM-DDTHH-mm-ssZ` (hyphens not colons — Windows filename safety).
73
+ - [x] 5.5 No backup file created when existing in-marker content matches expected output.
74
+
75
+ - [x] **Task 6: Unit and integration tests** — see Testing section below.
76
+
77
+ - [ ] **Task 7: Documentation touch-up** (no new files)
78
+ - [ ] 7.1 Add a single paragraph to `docs/` only if an existing doc already covers instruction injection; do not create a new doc file. (If no existing doc covers this, skip — Story 21.8 introduces documentation for the broader Epic 21 surface.)
51
79
 
52
80
  ## Dev Notes
53
81
 
54
- ### Architecture Compliance
82
+ ### Canonical Composer Contract (decision A — verbatim)
83
+
84
+ > `composeInstructionBlock({ profile, projectRoot }) → string`
85
+ > - Reads `lib/templates/instruction-block-universal.template.md` (always).
86
+ > - If `profile === 'on-prem'`, reads `lib/templates/instruction-block-onprem.template.md` and appends it to the universal content separated by a single blank line.
87
+ > - If `profile === 'on-prem'` and the on-prem template file is missing, THROWS `Error('on-prem profile selected but instruction-block-onprem.template.md is missing')`. NO silent fallback.
88
+ > - Returns the composed string. Templates contain NO `{{...}}` placeholders; any substitution is the caller's responsibility, done AFTER composition.
89
+ > - Single owner: `lib/installer.js` calls `composeInstructionBlock` exactly once per stamped artifact. Mergers receive the already-composed string; mergers do not call `composeInstructionBlock` themselves.
90
+
91
+ ### Canonical Backup Filename Format (decision C — verbatim)
92
+
93
+ > Backup filename format: `<target>.backup-<ISO-8601-timestamp>` e.g. `.roomodes.backup-2026-04-15T12-30-00Z`. Story 21.2 OWNS this format.
94
+
95
+ ### Terminology (decision B)
96
+
97
+ - **composer** = `composeInstructionBlock` (owned by 21.2).
98
+ - **merger** = per-artifact splicer (marker-block merger for markdown files; json-merge merger for `opencode.json`).
99
+ - **stamper** = installer orchestration.
100
+ - "injection function" / "marker-injection" wording is retired.
101
+
102
+ ### Architecture compliance
103
+
104
+ - **Decision P3-3 (Local-LLM / On-Prem Agent Tuning Profile)** — this story implements the "Universal layer" of the two-layer injection model. The on-prem layer ships in Story 21.6 via `lib/templates/instruction-block-onprem.template.md`. The composition function lives in this story; Story 21.6 only adds the on-prem template file and (if needed) one additional conditional branch.
105
+
106
+ - **NFR44 (byte-identity for standard profile)** — Standard-profile rendered output must not contain `/no_think`, `str_replace_editor`, or `~/.claude/`. AC #7 asserts absence; Test 5.5 in the Testing section enforces it with a direct grep assertion. Story 21.9 will add a standard-profile-vs-pre-Epic-21-baseline byte-comparison integration test; this story stays focused on the composition unit.
107
+
108
+ - **NFR46 (idempotency)** — Two consecutive installs with identical inputs must produce byte-identical marker-block content. AC #6 is the contract; Task 3.3 resolves the pre-existing insert/replace asymmetry that otherwise would violate this.
109
+
110
+ - **NFR47 (Roo Code fileRegex enforcement)** — Not directly implemented here. Story 21.3 ships `.roomodes` with `fileRegex` restrictions per BMAD mode; NFR47 is verified by Story 21.9. Story 21.2 must only ensure that Roo Code's instruction file (`.roo/rules/00-ma-agents.md` — registered in `lib/agents.js` line 161) receives the universal block unchanged in shape, so Story 21.3 can cleanly add `.roomodes` alongside without conflicting with this story's markdown merger.
111
+
112
+ - **NFR18 (additive JSON-merge for OpenCode)** — The existing `injectionStrategy.position === 'json-merge'` path already implements additive merge for `opencode.json::instructions[]` (ma-agents entry identified by `[ma-agents]` prefix, stale entries replaced, user entries preserved). Task 4 reuses that machinery unchanged; only the instruction-text content source switches from the hardcoded literal to `composeInstructionBlock`.
113
+
114
+ ### Verified source-tree surface
115
+
116
+ | File | Exists? | Role in this story |
117
+ |------|---------|--------------------|
118
+ | `lib/profile.js` | verified (Story 21.1, lines 1–107) | Consumed via `getProfile(projectRoot)` in `updateAgentInstructions` |
119
+ | `lib/installer.js` | verified | Add `composeInstructionBlock`; modify `updateAgentInstructions` markdown branch (line ~358) and json-merge branch (line ~365) |
120
+ | `lib/agents.js` | verified | Read-only — enumerates the agents whose `instructionFiles[]` receive the universal block (Claude Code, Cline, Roo Code, Cursor, Kilocode, Copilot, Gemini, OpenCode) |
121
+ | `lib/templates/instruction-block-universal.template.md` | **new** | Template source (AC #1, #2) |
122
+ | `lib/templates/instruction-block-onprem.template.md` | **new (Story 21.6)** | Consumed conditionally by `composeInstructionBlock`; graceful absence handling required now |
123
+ | `lib/templates/project-context.template.md` | verified | Reference pattern for template stamping syntax (placeholder conventions) |
124
+ | `test/instruction-block.test.js` | **new** | Unit + snapshot tests (see Testing section) |
125
+ | `test/profile.test.js` | verified (Story 21.1) | Reference for test framework, temp-dir isolation pattern |
126
+
127
+ ### Composition pattern (template foundation for Stories 21.3–21.6)
55
128
 
56
- - **Decision P3-3**: This story implements the "Universal layer" half of the two-layer model. The on-prem layer ships in Story 21.6.
57
- - **NFR44**: Standard profile must produce no on-prem-specific output. AC #7 + Test 5.5 enforce this.
58
- - **NFR46**: Marker-block content is deterministic given profile + project state.
129
+ ```
130
+ composeInstructionBlock({ profile, projectRoot })
131
+ ├── universal template (this story always applied)
132
+ └── on-prem template (Story 21.6 — applied when profile === 'on-prem'; THROWS if missing)
133
+ ```
59
134
 
60
- ### Source Tree Components to Touch
135
+ Stories 21.3 (`.roomodes`), 21.4 (`AGENTS.md`), and 21.5 (`.clinerules`) consume this function where their templates reference the universal text-vs-file and phase-discipline rules — they do not duplicate the rule text. Story 21.6 adds the on-prem template file and, if its rules need structural interleaving with universal rules (rather than straight append), one additional branch in `composeInstructionBlock`. Story 21.7 (BMAD persona phase prefix) is a separate composition site (BMAD customize-loader) and does not call `composeInstructionBlock`.
61
136
 
62
- | File | Change |
63
- |------|--------|
64
- | `lib/templates/instruction-block-universal.template.md` | CREATE |
65
- | `lib/installer.js` | MODIFY — add `composeInstructionBlock`; wire into existing marker-based injection |
66
- | `test/instruction-block.test.js` | CREATE |
137
+ ### Merger-site map (for reviewers)
67
138
 
68
- ### Dependencies
139
+ - **Markdown marker merger** (`lib/installer.js` `updateAgentInstructions` lines ~407–459): Claude Code, Cline (both files), Roo Code rules, Cursor, Kilocode, Copilot, Gemini.
140
+ - **JSON merge merger** (`lib/installer.js` `updateAgentInstructions` lines ~365–405): OpenCode (`opencode.json::instructions[]`).
141
+ - **BMAD agent files** (`lib/installer.js` lines ~445–451): Skipped when file absent; receive markers via `applyCustomizations` (outside this story's scope).
142
+ - **Anti-gravity** (`lib/agents.js` line 221): Uses `.antigravity/antigravity.md` — treated as markdown marker merger, same path as Claude Code et al.
69
143
 
70
- - Story 21.1 must be merged first — `composeInstructionBlock` reads `profile` from the resolved value passed in; injection call sites obtain it via `getProfile(projectRoot)` from `lib/profile.js`.
144
+ ### Library and pattern references
71
145
 
72
- ### Out of Scope
146
+ - **Prompts library** (`prompts` npm) for the drift-confirmation prompt in Task 5.2 — reuse the existing `prompts({ type: 'confirm' })` style used elsewhere in `bin/cli.js`.
147
+ - **Backup filename format** — see the "Canonical Backup Filename Format" block above. Story 21.2 OWNS this format; Stories 21.10 (reconfigure) and 21.11 (uninstall) consume it so uninstall's backup-cleanup logic works uniformly.
148
+ - **Manifest-path computation** reuse `path.relative(projectRoot, path.join(agent.getProjectPath(), 'MANIFEST.yaml')).replace(/\\/g, '/')` (already present in both branches of `updateAgentInstructions`). Do not duplicate — pass it into `composeInstructionBlock` from the call site.
73
149
 
74
- - On-prem template content (Story 21.6 ships `lib/templates/instruction-block-onprem.template.md`)
75
- - `.roomodes`, `AGENTS.md`, `.clinerules` per-tool templates (Stories 21.3–21.5)
76
- - BMAD persona phase prefix (Story 21.7)
150
+ ### Out of scope
77
151
 
78
- ### Testing Standards
152
+ - On-prem template content and the conditional append wiring details beyond the graceful-fallback stub (Story 21.6).
153
+ - `.roomodes` template and Roo Code `fileRegex` mode restrictions (Story 21.3).
154
+ - `AGENTS.md` template for OpenCode (Story 21.4) — noting that Story 21.4 *reuses* `composeInstructionBlock` for the text-vs-file section; the `AGENTS.md` filename and layout are Story 21.4's concern.
155
+ - `.clinerules` template for Cline (Story 21.5) — Cline's existing `.clinerules` injection already passes through `updateAgentInstructions`; Story 21.5 ships a richer dedicated template and is out of scope here.
156
+ - BMAD persona phase prefix and customize-loader changes (Story 21.7).
157
+ - vLLM deployment documentation (Story 21.8).
158
+ - Integration test covering NFR44 byte-comparison against pre-Epic-21 baseline (Story 21.9).
159
+ - Profile-reconfigure and profile-uninstall flows (Stories 21.10, 21.11).
160
+ - Bumping `manifestVersion` — already bumped to `1.2.0` in Story 21.1 and not touched here.
79
161
 
80
- Match `test/profile.test.js` (created in Story 21.1) for framework and isolation patterns.
162
+ ## Dependencies
163
+
164
+ ### Upstream (blocking)
165
+
166
+ - **Story 21.1 (done)** — `lib/profile.js` exports `getProfile(projectRoot)` which `updateAgentInstructions` calls to pick the profile. Verified at `lib/profile.js` lines 29–43.
167
+ - **Epic 9 (done)** — `opencode.json` additive JSON-merge pattern in `updateAgentInstructions` (lines ~365–405) is reused unchanged. NFR18 already satisfied by that code; this story must not regress it.
168
+
169
+ ### Downstream (consumers of this story's surface)
170
+
171
+ - **Story 21.3** — `.roomodes` mode `customInstructions` text references the rules defined in the universal template. Adds `extraInstructionTemplates` field to `lib/agents.js` Roo Code entry.
172
+ - **Story 21.4** — `AGENTS.md` template for OpenCode reuses the universal text-vs-file section; also appends `AGENTS.md` to `opencode.json::instructions[]` via the same NFR18-compliant JSON-merge.
173
+ - **Story 21.5** — `.clinerules` template reuses the universal rules, formatted per Cline's convention. Cline's two instruction files (`.cline/clinerules.md`, `.clinerules`) both receive the universal block via this story; Story 21.5 extends with Cline-specific content.
174
+ - **Story 21.6** — Adds `lib/templates/instruction-block-onprem.template.md` and toggles on-prem content via the same `composeInstructionBlock` call signature from this story. Must not require a signature change.
175
+ - **Story 21.9** — Tests verifying NFR44 (standard-profile absence of on-prem strings), NFR46 (idempotency across profiles), and roomodes-level NFR47.
176
+ - **Story 21.10 (Profile Reconfigure)** — Consumes the canonical backup-filename format (`<target>.backup-<ISO-8601-timestamp>`) owned by this story, plus the drift-detection pattern introduced here.
177
+ - **Story 21.11 (Profile Uninstall)** — Consumes the canonical backup-filename format owned by this story so uninstall's backup-cleanup logic matches install-time backups uniformly.
178
+
179
+ ## Testing
180
+
181
+ **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/`.
182
+
183
+ New test file: `test/instruction-block.test.js` (new).
184
+
185
+ **Unit tests:**
186
+
187
+ - 5.1 Universal template reads from disk and substitutes `{{MANIFEST_PATH}}` exactly once with the supplied value.
188
+ - 5.2 `composeInstructionBlock({ profile: 'standard', projectRoot })` returns universal content only, even when the on-prem template file exists (test creates it in the tmp-copy of `lib/templates/` or uses a fs spy).
189
+ - 5.3 `composeInstructionBlock({ profile: 'on-prem', projectRoot })` appends on-prem content when the on-prem template file is present; **throws** `Error('on-prem profile selected but instruction-block-onprem.template.md is missing')` when the on-prem template file is absent (no silent fallback — decision A).
190
+ - 5.4 Idempotency: calling the composer twice with identical inputs returns byte-identical strings (trim/whitespace parity).
191
+ - 5.5 NFR44 assertion: standard-profile output does not contain the literals `/no_think`, `str_replace_editor`, or `~/.claude/`.
192
+ - 5.6 `composeInstructionBlock` throws a descriptive Error when the universal template is missing or has no `{{MANIFEST_PATH}}` placeholder (defensive).
193
+
194
+ **Integration tests (within this story — full end-to-end lives in Story 21.9):**
195
+
196
+ - 5.7 Marker-block replacement preserves content outside markers byte-for-byte across two installs (targets `.claude/CLAUDE.md` in a tmp project with pre-existing user content above and below the markers).
197
+ - 5.8 Fresh install writes the universal block to each markdown-injection agent's instruction file when the agent is selected.
198
+ - 5.9 OpenCode `opencode.json::instructions[]` receives the composed string with `[ma-agents]` prefix; other keys untouched; second install replaces the ma-agents entry (not duplicates it) and preserves user entries (NFR18).
199
+ - 5.10 Upgrade-safety (AC #10):
200
+ - Clean marker block (no hand-edit) → no warning, no backup file, overwrite is silent.
201
+ - Hand-edited marker block + `--yes` → WARNING line emitted verbatim, backup file exists at `<target>.backup-<timestamp>`, backup contains only the marker-block region including marker lines.
202
+ - Hand-edited marker block + interactive (no `--yes`) → prompt appears; simulated decline leaves file unchanged; simulated confirm proceeds with backup.
203
+
204
+ **Test data isolation:** All tests create a tmp project with a stub `.ma-agents.json` written via `setProfile(tmpRoot, 'standard' | 'on-prem')` (Story 21.1 API). Do not stub `lib/profile.js` — use the real module against the tmp project root to catch integration bugs.
205
+
206
+ **Coverage note:** Story 21.9 adds (a) a standard-profile vs. pre-Epic-21 baseline byte-comparison test, (b) the on-prem profile must-have-both-layers test, (c) roomodes slug-collision test, and (d) NFR47 fileRegex contract test. Do not duplicate those here.
207
+
208
+ ## Change Log
209
+
210
+ - 2026-04-14: Story created (Epic 21, Story 21.2).
211
+ - 2026-04-14: Added AC #8 for upgrade-safety — installer detects hand-edited marker content, backs it up, warns on --yes override (Finding #15, corrective plan step 3).
212
+ - 2026-04-15: Story rewritten as template foundation for Stories 21.3–21.6 — ACs reorganized and gap-fills flagged explicitly (AC #2, #3, #8, #9, #10); NFR44/NFR46/NFR47/NFR18 citations made explicit in Dev Notes; Dependencies section split into upstream/downstream; Testing section expanded; verified source-tree surface table added distinguishing existing vs. new paths; injection-site map added. Two open questions raised inline rather than invented as ACs. Status set to Ready.
213
+ - 2026-04-15: Adversarial-review findings resolved. AC #3 rewritten to canonical composer contract — signature is `composeInstructionBlock({ profile, projectRoot })`; on-prem template absence THROWS (no silent fallback); templates contain no placeholders (caller-owned substitution); single-owner contract (installer calls composer once; mergers consume string). Added Canonical Composer Contract and Canonical Backup Filename Format blocks to Dev Notes. Terminology normalized to composer/merger/stamper — "injection function" / "marker-injection" wording retired. Downstream deps explicitly list 21.10 and 21.11 as backup-filename-format consumers. Tasks 2–4 and tests 5.2–5.3 updated to match. Status remains Ready.
214
+ - 2026-04-15: Story 21.2 implemented — universal template, `composeInstructionBlock`, both mergers wired, drift detection + canonical backup format. Status Ready → Review.
81
215
 
82
216
  ## Dev Agent Record
83
217
 
84
218
  ### Agent Model Used
85
- _(to be filled by dev agent)_
219
+ Claude Opus 4.6 (1M context) bmad-dev-story + bmad-review-adversarial-general + bmad-review-edge-case-hunter flow.
86
220
 
87
221
  ### Debug Log References
88
- _(to be filled)_
222
+ - Initial implementation had `agent._yesMode` reading — a field never populated anywhere. Adversarial review (Finding #6) surfaced this: CLI `--yes` callers would still hit the interactive drift prompt. Resolved by extending `updateAgentInstructions` with an `opts = {}` argument carrying `yesMode` and wiring both install-path call sites (`action === 'remove'` and the main install branch) to pass `{ yesMode: yes }` from `installSkill`. Uninstall (`uninstallSkill`) left unchanged since it has no `yes` flag surface today; drift during uninstall is rare and the env-var fallback (`MA_AGENTS_YES=1`) still works for CI.
223
+ - Added test 5.10c to guard against regression of the opts-based yesMode path.
89
224
 
90
225
  ### Completion Notes List
91
- _(to be filled)_
226
+ - `lib/templates/instruction-block-universal.template.md` ships with `{{MANIFEST_PATH}}` as the ONLY placeholder (AC #1, #2). Contains the five rule sections from AC #1 verbatim and does NOT mention `/no_think`, `str_replace_editor`, or `~/.claude/` (AC #7, asserted by test 5.5).
227
+ - `composeInstructionBlock({ profile, projectRoot })` is exported from `lib/installer.js` (AC #3). On-prem template absence THROWS with the pinned error message — no silent fallback. Templates carry no placeholders substituted inside the composer; mergers substitute `{{MANIFEST_PATH}}` AFTER composition.
228
+ - Marker-based merger (`updateAgentInstructions` markdown path) calls `composeInstructionBlock` exactly once per agent + substitutes per-agent MANIFEST path after composition (AC #4). JSON-merge merger (OpenCode) prefixes the composed string with `[ma-agents]` tag — filter unchanged, NFR18 additive merge preserved (AC #8).
229
+ - First-insert vs in-place-replace normalized: both paths emit `<MARKER>\n<content>\n<MARKER>` identically (AC #6 / NFR46 — resolved the Open question under AC).
230
+ - BMAD agents branch untouched — still skips missing files via the "BMAD agent file not yet deployed" message (AC #9).
231
+ - AC #10 upgrade-safety: drift detection compares existing in-marker content against composer output. `--yes` path emits the pinned WARNING line verbatim and writes a sibling backup file. Interactive path shows the old/new block inline and requires confirmation. Backup filename is `<target>.backup-<ISO-timestamp>` with hyphens for Windows compatibility (`buildBackupFilename` / `formatBackupTimestamp` exported; Story 21.2 OWNS this format per Dev Notes decision C).
232
+ - Open question under AC (profile-reconfigure) deferred to Story 21.10 per spec.
233
+
234
+ ### Adversarial Review Findings
235
+
236
+ | # | Layer | Severity | Finding | Disposition |
237
+ |---|-------|----------|---------|-------------|
238
+ | 1 | Cynical | P0 | `agent._yesMode` read but never set anywhere — CLI `--yes` path would hit interactive prompt on drift, contradicting AC #10 | **Fixed** — extended `updateAgentInstructions(agent, projectRoot, opts)` with `opts.yesMode`; wired through `installSkill` → two call sites |
239
+ | 2 | Cynical | P1 | OpenCode instructionText trailing-whitespace strip could elide final content when body ends with blank line | **Fixed by design** — `composeInstructionBlock` already trims trailing whitespace and appends exactly one `\n`; strip is idempotent |
240
+ | 3 | Cynical | P1 | Insert path wrote `wrappedInstructionWithTrailingNewline + '\n'` (two newlines) while replace path wrote just the marker block — potential NFR46 violation | **Fixed by design** — NFR46 applies to content INSIDE markers; outside-markers formatting is preserved byte-for-byte by regex replace. Both paths produce byte-identical in-marker content |
241
+ | 4 | Cynical | P2 | `buildBackupFilename` has TOCTOU race between `existsSync` and `outputFile` | **Out of scope** — sub-second repeat runs would need multiple installs within the same second; filename has numeric tiebreaker; would need fs lock to fully fix |
242
+ | 5 | Edge-case | P1 | Missing test for `opts.yesMode` path (env-var path tested but opts path untested) | **Fixed** — added test 5.10c |
243
+ | 6 | Edge-case | P2 | `uninstallSkill` call-site to `updateAgentInstructions` doesn't pass yesMode | **Out of scope** — `uninstallSkill` signature doesn't carry `yes` today; env-var fallback covers CI; tracked implicitly for 21.11 |
244
+ | 7 | Edge-case | P2 | Interactive drift prompt could trigger on `reconfigure` flow (profile change) | **Out of scope per spec** — story's Open question under AC explicitly defers profile-reconfigure to Story 21.10 |
245
+
246
+ All P0/P1 findings resolved or accepted with documented rationale.
92
247
 
93
248
  ### File List
94
- _(to be filled)_
95
-
96
- ## Change Log
97
- - 2026-04-14: Story created (Epic 21, Story 21.2)
98
- - 2026-04-14: Added AC #8 for upgrade-safety — installer detects hand-edited marker content, backs it up, warns on --yes override (Finding #15, corrective plan step 3).
249
+ - CREATED: `lib/templates/instruction-block-universal.template.md`
250
+ - CREATED: `test/instruction-block.test.js` (14 tests, all passing)
251
+ - MODIFIED: `lib/installer.js` (composer, drift handler, canonical backup format, merger wiring, yesMode opts plumbing)
252
+ - MODIFIED: `package.json` (added `test/instruction-block.test.js` to npm test script)
253
+ - MODIFIED: `_bmad-output/implementation-artifacts/21-2-universal-instruction-block-expansion.md` (Status Review; Dev Agent Record / File List / Change Log updated)