ma-agents 2.20.3 → 2.22.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 (149) hide show
  1. package/.opencode/skills/.ma-agents.json +241 -0
  2. package/.opencode/skills/MANIFEST.yaml +254 -0
  3. package/.opencode/skills/ai-audit-trail/SKILL.md +23 -0
  4. package/.opencode/skills/auto-bug-detection/SKILL.md +169 -0
  5. package/.opencode/skills/cmake-best-practices/SKILL.md +64 -0
  6. package/.opencode/skills/cmake-best-practices/examples/cmake.md +59 -0
  7. package/.opencode/skills/code-documentation/SKILL.md +57 -0
  8. package/.opencode/skills/code-documentation/examples/cpp.md +29 -0
  9. package/.opencode/skills/code-documentation/examples/csharp.md +28 -0
  10. package/.opencode/skills/code-documentation/examples/javascript_typescript.md +28 -0
  11. package/.opencode/skills/code-documentation/examples/python.md +57 -0
  12. package/.opencode/skills/code-review/SKILL.md +43 -0
  13. package/.opencode/skills/commit-message/SKILL.md +79 -0
  14. package/.opencode/skills/cpp-best-practices/SKILL.md +234 -0
  15. package/.opencode/skills/cpp-best-practices/examples/modern-idioms.md +189 -0
  16. package/.opencode/skills/cpp-best-practices/examples/naming-and-organization.md +102 -0
  17. package/.opencode/skills/cpp-concurrency-safety/SKILL.md +60 -0
  18. package/.opencode/skills/cpp-concurrency-safety/examples/concurrency.md +73 -0
  19. package/.opencode/skills/cpp-const-correctness/SKILL.md +63 -0
  20. package/.opencode/skills/cpp-const-correctness/examples/const_correctness.md +54 -0
  21. package/.opencode/skills/cpp-memory-handling/SKILL.md +42 -0
  22. package/.opencode/skills/cpp-memory-handling/examples/modern-cpp.md +49 -0
  23. package/.opencode/skills/cpp-memory-handling/examples/smart-pointers.md +46 -0
  24. package/.opencode/skills/cpp-modern-composition/SKILL.md +64 -0
  25. package/.opencode/skills/cpp-modern-composition/examples/composition.md +51 -0
  26. package/.opencode/skills/cpp-robust-interfaces/SKILL.md +55 -0
  27. package/.opencode/skills/cpp-robust-interfaces/examples/interfaces.md +56 -0
  28. package/.opencode/skills/create-hardened-docker-skill/SKILL.md +637 -0
  29. package/.opencode/skills/create-hardened-docker-skill/scripts/create-all.sh +489 -0
  30. package/.opencode/skills/csharp-best-practices/SKILL.md +278 -0
  31. package/.opencode/skills/docker-hardening-verification/SKILL.md +28 -0
  32. package/.opencode/skills/docker-hardening-verification/scripts/verify-hardening.sh +39 -0
  33. package/.opencode/skills/docker-image-signing/SKILL.md +28 -0
  34. package/.opencode/skills/docker-image-signing/scripts/sign-image.sh +33 -0
  35. package/.opencode/skills/document-revision-history/SKILL.md +104 -0
  36. package/.opencode/skills/git-workflow-skill/SKILL.md +194 -0
  37. package/.opencode/skills/git-workflow-skill/hooks/commit-msg +61 -0
  38. package/.opencode/skills/git-workflow-skill/hooks/pre-commit +38 -0
  39. package/.opencode/skills/git-workflow-skill/hooks/prepare-commit-msg +56 -0
  40. package/.opencode/skills/git-workflow-skill/scripts/finish-feature.sh +192 -0
  41. package/.opencode/skills/git-workflow-skill/scripts/install-hooks.sh +55 -0
  42. package/.opencode/skills/git-workflow-skill/scripts/start-feature.sh +110 -0
  43. package/.opencode/skills/git-workflow-skill/scripts/validate-workflow.sh +229 -0
  44. package/.opencode/skills/js-ts-dependency-mgmt/SKILL.md +49 -0
  45. package/.opencode/skills/js-ts-dependency-mgmt/examples/dependency_mgmt.md +60 -0
  46. package/.opencode/skills/js-ts-security-skill/SKILL.md +64 -0
  47. package/.opencode/skills/js-ts-security-skill/scripts/verify-security.sh +136 -0
  48. package/.opencode/skills/logging-best-practices/SKILL.md +50 -0
  49. package/.opencode/skills/logging-best-practices/examples/cpp.md +36 -0
  50. package/.opencode/skills/logging-best-practices/examples/csharp.md +49 -0
  51. package/.opencode/skills/logging-best-practices/examples/javascript.md +77 -0
  52. package/.opencode/skills/logging-best-practices/examples/python.md +57 -0
  53. package/.opencode/skills/logging-best-practices/references/logging-standards.md +29 -0
  54. package/.opencode/skills/open-presentation/SKILL.md +35 -0
  55. package/.opencode/skills/opentelemetry-best-practices/SKILL.md +34 -0
  56. package/.opencode/skills/opentelemetry-best-practices/examples/go.md +32 -0
  57. package/.opencode/skills/opentelemetry-best-practices/examples/javascript.md +58 -0
  58. package/.opencode/skills/opentelemetry-best-practices/examples/python.md +37 -0
  59. package/.opencode/skills/opentelemetry-best-practices/references/otel-standards.md +37 -0
  60. package/.opencode/skills/python-best-practices/SKILL.md +385 -0
  61. package/.opencode/skills/python-dependency-mgmt/SKILL.md +42 -0
  62. package/.opencode/skills/python-dependency-mgmt/examples/dependency_mgmt.md +67 -0
  63. package/.opencode/skills/python-security-skill/SKILL.md +56 -0
  64. package/.opencode/skills/python-security-skill/examples/security.md +56 -0
  65. package/.opencode/skills/self-signed-cert/SKILL.md +42 -0
  66. package/.opencode/skills/self-signed-cert/scripts/generate-cert.ps1 +45 -0
  67. package/.opencode/skills/self-signed-cert/scripts/generate-cert.sh +43 -0
  68. package/.opencode/skills/skill-creator/SKILL.md +196 -0
  69. package/.opencode/skills/skill-creator/references/output-patterns.md +82 -0
  70. package/.opencode/skills/skill-creator/references/workflows.md +28 -0
  71. package/.opencode/skills/skill-creator/scripts/init_skill.py +208 -0
  72. package/.opencode/skills/skill-creator/scripts/package_skill.py +99 -0
  73. package/.opencode/skills/skill-creator/scripts/quick_validate.py +113 -0
  74. package/.opencode/skills/story-status-lookup/SKILL.md +78 -0
  75. package/.opencode/skills/test-accompanied-development/SKILL.md +50 -0
  76. package/.opencode/skills/test-generator/SKILL.md +65 -0
  77. package/.opencode/skills/vercel-react-best-practices/SKILL.md +109 -0
  78. package/.opencode/skills/verify-hardened-docker-skill/SKILL.md +442 -0
  79. package/.opencode/skills/verify-hardened-docker-skill/scripts/verify-docker-hardening.sh +439 -0
  80. package/AiAudit.md +5 -0
  81. package/QUICK_START.md +11 -5
  82. package/README.md +52 -1
  83. package/bin/cli.js +31 -4
  84. package/docs/BMAD_AI_Development_Training.pptx +0 -0
  85. package/docs/technical-notes/context-persistence-research.md +434 -0
  86. package/docs/technical-notes/enforcement-hooks-research.md +415 -0
  87. package/lib/agents.js +34 -0
  88. package/lib/bmad-extension/agents/bmm-architect.customize.yaml +5 -0
  89. package/lib/bmad-extension/agents/bmm-bmad-master.customize.yaml +5 -0
  90. package/lib/bmad-extension/agents/bmm-cyber.customize.yaml +30 -0
  91. package/lib/bmad-extension/agents/bmm-dev.customize.yaml +5 -0
  92. package/lib/bmad-extension/agents/bmm-devops.customize.yaml +30 -0
  93. package/lib/bmad-extension/agents/bmm-mil498.customize.yaml +42 -0
  94. package/lib/bmad-extension/agents/bmm-pm.customize.yaml +5 -0
  95. package/lib/bmad-extension/agents/bmm-qa.customize.yaml +5 -0
  96. package/lib/bmad-extension/agents/bmm-sm.customize.yaml +5 -0
  97. package/lib/bmad-extension/agents/bmm-sre.customize.yaml +30 -0
  98. package/lib/bmad-extension/agents/bmm-tech-writer.customize.yaml +5 -0
  99. package/lib/bmad-extension/agents/bmm-ux-designer.customize.yaml +5 -0
  100. package/lib/bmad-extension/module-help.csv +7 -0
  101. package/lib/bmad-extension/module.yaml +3 -0
  102. package/lib/bmad-extension/workflows/add-sprint/workflow.md +112 -0
  103. package/lib/bmad-extension/workflows/add-to-sprint/workflow.md +206 -0
  104. package/lib/bmad-extension/workflows/create-bug-story/workflow.md +186 -0
  105. package/lib/bmad-extension/workflows/modify-sprint/workflow.md +250 -0
  106. package/lib/bmad-extension/workflows/project-context-expansion/workflow.md +229 -0
  107. package/lib/bmad-extension/workflows/sprint-status-view/workflow.md +193 -0
  108. package/lib/bmad.js +168 -36
  109. package/lib/hooks/claude-code/verify-manifest.js +56 -0
  110. package/lib/installer.js +282 -1
  111. package/lib/methodology/BMAD_AI_Development_Training.pptx +0 -0
  112. package/lib/methodology/version.json +7 -0
  113. package/lib/skill-authoring.js +732 -0
  114. package/lib/templates/project-context.template.md +47 -0
  115. package/opencode.json +8 -0
  116. package/package.json +2 -2
  117. package/skills/auto-bug-detection/SKILL.md +165 -0
  118. package/skills/auto-bug-detection/skill.json +8 -0
  119. package/skills/code-review/SKILL.md +40 -0
  120. package/skills/cpp-best-practices/SKILL.md +230 -0
  121. package/skills/cpp-best-practices/examples/modern-idioms.md +189 -0
  122. package/skills/cpp-best-practices/examples/naming-and-organization.md +102 -0
  123. package/skills/cpp-best-practices/skill.json +25 -0
  124. package/skills/csharp-best-practices/SKILL.md +274 -0
  125. package/skills/csharp-best-practices/skill.json +23 -0
  126. package/skills/git-workflow-skill/skill.json +1 -1
  127. package/skills/open-presentation/SKILL.md +31 -0
  128. package/skills/open-presentation/skill.json +11 -0
  129. package/skills/python-best-practices/SKILL.md +381 -0
  130. package/skills/python-best-practices/skill.json +26 -0
  131. package/skills/story-status-lookup/SKILL.md +74 -0
  132. package/skills/story-status-lookup/skill.json +8 -0
  133. package/test/agent-injection-strategy.test.js +13 -7
  134. package/test/bmad-extension.test.js +237 -0
  135. package/test/bmad-output-policy.test.js +119 -0
  136. package/test/build-bmad-args.test.js +361 -0
  137. package/test/create-agent.test.js +232 -0
  138. package/test/enforcement-hooks.test.js +324 -0
  139. package/test/generate-project-context.test.js +337 -0
  140. package/test/integration-verification.test.js +402 -0
  141. package/test/opencode-agent.test.js +150 -0
  142. package/test/opencode-json-error.test.js +260 -0
  143. package/test/opencode-json-injection.test.js +256 -0
  144. package/test/opencode-json-merge.test.js +299 -0
  145. package/test/skill-authoring.test.js +272 -0
  146. package/test/skill-customize-agent.test.js +253 -0
  147. package/test/skill-mandatory.test.js +235 -0
  148. package/test/skill-validation.test.js +378 -0
  149. package/test/yes-flag.test.js +1 -1
@@ -0,0 +1,402 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Tests for Story 8.4: Epic 8 Integration Verification
4
+ *
5
+ * Validates that Stories 8.1, 8.2, and 8.3 work together end-to-end:
6
+ * - IDE agent install injects MA-AGENTS block at TOP (after frontmatter)
7
+ * - BMAD agent install deploys extension module
8
+ * - Re-install replaces blocks in-place (no duplicates)
9
+ * - Cross-story integration (agents.js → installer.js → bmad.js)
10
+ */
11
+ 'use strict';
12
+
13
+ const assert = require('assert');
14
+ const fs = require('fs-extra');
15
+ const path = require('path');
16
+ const os = require('os');
17
+
18
+ let passed = 0;
19
+ let failed = 0;
20
+ const errors = [];
21
+
22
+ function test(name, fn) {
23
+ try {
24
+ fn();
25
+ console.log(` \u2713 ${name}`);
26
+ passed++;
27
+ } catch (err) {
28
+ console.error(` \u2717 ${name}: ${err.message}`);
29
+ failed++;
30
+ errors.push({ name, error: err.message });
31
+ }
32
+ }
33
+
34
+ async function asyncTest(name, fn) {
35
+ try {
36
+ await fn();
37
+ console.log(` \u2713 ${name}`);
38
+ passed++;
39
+ } catch (err) {
40
+ console.error(` \u2717 ${name}: ${err.message}`);
41
+ failed++;
42
+ errors.push({ name, error: err.message });
43
+ }
44
+ }
45
+
46
+ // Load modules under test
47
+ const { findInsertionPoint, _testUpdateAgentInstructions: updateAgentInstructions } = require('../lib/installer');
48
+ const { getAgent, getAllAgents, getAgentsByCategory } = require('../lib/agents');
49
+
50
+ const MARKER_START = '<!-- MA-AGENTS-START -->';
51
+ const MARKER_END = '<!-- MA-AGENTS-END -->';
52
+
53
+ // ─── Task 1: End-to-end IDE agent test (AC #1, #3) ──────────────────────────
54
+
55
+ console.log('\nTask 1 — End-to-end IDE agent test');
56
+
57
+ // Task 1.1: Run updateAgentInstructions for Claude Code and verify injection
58
+ (async () => {
59
+ const tmpDir = path.join(os.tmpdir(), `ma-agents-test-8.4-${Date.now()}`);
60
+
61
+ try {
62
+ // ── Task 1.1 & 1.2: Install targeting Claude Code, verify TOP injection ──
63
+
64
+ await asyncTest('1.1/1.2: Claude Code install injects MA-AGENTS block at TOP after frontmatter', async () => {
65
+ const projectRoot = path.join(tmpDir, 'ide-test');
66
+ await fs.ensureDir(path.join(projectRoot, '.claude'));
67
+
68
+ // Create a CLAUDE.md with frontmatter and existing content
69
+ const existingContent = '---\ntitle: My Project\n---\n\n# Existing Instructions\n\nSome existing rules here.\n';
70
+ await fs.writeFile(path.join(projectRoot, '.claude', 'CLAUDE.md'), existingContent, 'utf-8');
71
+
72
+ // Create skills dir so MANIFEST path resolves
73
+ await fs.ensureDir(path.join(projectRoot, '.claude', 'skills'));
74
+
75
+ const agent = getAgent('claude-code');
76
+ assert.ok(agent, 'claude-code agent must exist');
77
+ assert.ok(agent.injectionStrategy, 'claude-code must have injectionStrategy');
78
+
79
+ await updateAgentInstructions(agent, projectRoot);
80
+
81
+ const result = await fs.readFile(path.join(projectRoot, '.claude', 'CLAUDE.md'), 'utf-8');
82
+
83
+ // Verify MA-AGENTS block exists
84
+ assert.ok(result.includes(MARKER_START), 'Must contain MA-AGENTS-START marker');
85
+ assert.ok(result.includes(MARKER_END), 'Must contain MA-AGENTS-END marker');
86
+
87
+ // Verify block is AFTER frontmatter
88
+ const frontmatterEnd = result.indexOf('---\n', result.indexOf('---\n') + 4) + 4;
89
+ const blockStart = result.indexOf(MARKER_START);
90
+ assert.ok(blockStart >= frontmatterEnd, `MA-AGENTS block (${blockStart}) must be after frontmatter (${frontmatterEnd})`);
91
+
92
+ // Verify existing content is preserved below the block
93
+ assert.ok(result.includes('# Existing Instructions'), 'Existing content must be preserved');
94
+ assert.ok(result.includes('Some existing rules here'), 'Existing content must be preserved');
95
+
96
+ // Verify block contains MANIFEST reference
97
+ assert.ok(result.includes('MANIFEST.yaml'), 'Block must reference MANIFEST.yaml');
98
+ });
99
+
100
+ // ── Task 1.3: Re-install, verify no duplicate blocks ──
101
+
102
+ await asyncTest('1.3: Re-install replaces block in-place (no duplicates)', async () => {
103
+ const projectRoot = path.join(tmpDir, 'ide-test');
104
+
105
+ const agent = getAgent('claude-code');
106
+ await updateAgentInstructions(agent, projectRoot);
107
+
108
+ const result = await fs.readFile(path.join(projectRoot, '.claude', 'CLAUDE.md'), 'utf-8');
109
+
110
+ // Count occurrences of markers
111
+ const startCount = (result.match(new RegExp(MARKER_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g')) || []).length;
112
+ const endCount = (result.match(new RegExp(MARKER_END.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g')) || []).length;
113
+
114
+ assert.strictEqual(startCount, 1, `Expected exactly 1 MA-AGENTS-START marker, found ${startCount}`);
115
+ assert.strictEqual(endCount, 1, `Expected exactly 1 MA-AGENTS-END marker, found ${endCount}`);
116
+
117
+ // Verify existing content is still preserved
118
+ assert.ok(result.includes('# Existing Instructions'), 'Existing content must survive re-install');
119
+ });
120
+
121
+ // ── Task 1.4: Cline agent has TWO instruction files, both get top-injection ──
122
+
123
+ await asyncTest('1.4: Cline install injects into BOTH instruction files at top', async () => {
124
+ const projectRoot = path.join(tmpDir, 'cline-test');
125
+ await fs.ensureDir(path.join(projectRoot, '.cline'));
126
+ await fs.ensureDir(path.join(projectRoot, '.cline', 'skills'));
127
+
128
+ // Create both Cline instruction files with frontmatter
129
+ const clineContent1 = '---\ntitle: Cline Rules\n---\n\n# Cline Project Rules\n\nRule 1: Be helpful.\n';
130
+ const clineContent2 = '---\nformat: clinerules\n---\n\n# Root Rules\n\nAnother rule set.\n';
131
+ await fs.writeFile(path.join(projectRoot, '.cline', 'clinerules.md'), clineContent1, 'utf-8');
132
+ await fs.writeFile(path.join(projectRoot, '.clinerules'), clineContent2, 'utf-8');
133
+
134
+ const agent = getAgent('cline');
135
+ assert.ok(agent, 'cline agent must exist');
136
+ assert.deepStrictEqual(agent.instructionFiles, ['.cline/clinerules.md', '.clinerules'],
137
+ 'Cline must have two instruction files');
138
+
139
+ await updateAgentInstructions(agent, projectRoot);
140
+
141
+ // Verify BOTH files have MA-AGENTS block
142
+ for (const file of agent.instructionFiles) {
143
+ const result = await fs.readFile(path.join(projectRoot, file), 'utf-8');
144
+ assert.ok(result.includes(MARKER_START), `${file} must contain MA-AGENTS-START marker`);
145
+ assert.ok(result.includes(MARKER_END), `${file} must contain MA-AGENTS-END marker`);
146
+
147
+ // Verify block is after frontmatter
148
+ const fmEnd = result.indexOf('---\n', result.indexOf('---\n') + 4) + 4;
149
+ const bStart = result.indexOf(MARKER_START);
150
+ assert.ok(bStart >= fmEnd, `${file}: MA-AGENTS block must be after frontmatter`);
151
+ }
152
+
153
+ // Re-run and verify no duplicates in either file
154
+ await updateAgentInstructions(agent, projectRoot);
155
+ for (const file of agent.instructionFiles) {
156
+ const result = await fs.readFile(path.join(projectRoot, file), 'utf-8');
157
+ const count = (result.match(new RegExp(MARKER_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g')) || []).length;
158
+ assert.strictEqual(count, 1, `${file}: Expected exactly 1 MA-AGENTS-START after re-install, found ${count}`);
159
+ }
160
+ });
161
+
162
+ // ─── Task 2: End-to-end BMAD agent test (AC #2, #3) ────────────────────────
163
+
164
+ console.log('\nTask 2 — End-to-end BMAD agent test');
165
+
166
+ // Task 2.1 & 2.2: Verify extension module structure exists in source
167
+
168
+ await asyncTest('2.1/2.2: BMAD extension module source has module.yaml + 11 customize.yaml files', async () => {
169
+ const extSource = path.join(__dirname, '..', 'lib', 'bmad-extension');
170
+ assert.ok(await fs.pathExists(extSource), 'lib/bmad-extension/ must exist');
171
+
172
+ // module.yaml
173
+ const moduleYaml = await fs.readFile(path.join(extSource, 'module.yaml'), 'utf-8');
174
+ assert.ok(moduleYaml.includes('extends-module: bmm'), 'module.yaml must extend bmm');
175
+ assert.ok(moduleYaml.includes('name: ma-agents-skills'), 'module.yaml must be named ma-agents-skills');
176
+
177
+ // 11 customize.yaml files in agents/
178
+ const agentsDir = path.join(extSource, 'agents');
179
+ assert.ok(await fs.pathExists(agentsDir), 'lib/bmad-extension/agents/ must exist');
180
+ const files = (await fs.readdir(agentsDir)).filter(f => f.endsWith('.customize.yaml'));
181
+ assert.strictEqual(files.length, 11, `Expected 11 .customize.yaml files, found ${files.length}`);
182
+ });
183
+
184
+ // Task 2.2 (continued): Verify deployment via fs.copy
185
+
186
+ await asyncTest('2.2: applyCustomizations deploys extension to _bmad/extensions/ma-agents-skills/', async () => {
187
+ const projectRoot = path.join(tmpDir, 'bmad-deploy-test');
188
+ const extSource = path.join(__dirname, '..', 'lib', 'bmad-extension');
189
+ const extTarget = path.join(projectRoot, '_bmad', 'extensions', 'ma-agents-skills');
190
+
191
+ // Simulate the deployment step from bmad.js applyCustomizations() STAGE:EXTENSION
192
+ await fs.ensureDir(extTarget);
193
+ await fs.copy(extSource, extTarget);
194
+
195
+ // Verify deployed structure
196
+ assert.ok(await fs.pathExists(path.join(extTarget, 'module.yaml')), 'Deployed module.yaml must exist');
197
+ assert.ok(await fs.pathExists(path.join(extTarget, 'module-help.csv')), 'Deployed module-help.csv must exist');
198
+
199
+ const deployedFiles = (await fs.readdir(path.join(extTarget, 'agents'))).filter(f => f.endsWith('.customize.yaml'));
200
+ assert.strictEqual(deployedFiles.length, 11, `Deployed agents/ must have 11 .customize.yaml files, found ${deployedFiles.length}`);
201
+ });
202
+
203
+ // Task 2.3: Verify bmm-sre.customize.yaml critical_actions reference
204
+
205
+ await asyncTest('2.3: bmm-sre.customize.yaml has critical_actions pointing to _bmad/skills/sre/MANIFEST.yaml', async () => {
206
+ const sreYaml = await fs.readFile(
207
+ path.join(__dirname, '..', 'lib', 'bmad-extension', 'agents', 'bmm-sre.customize.yaml'),
208
+ 'utf-8'
209
+ );
210
+ assert.ok(sreYaml.includes('critical_actions'), 'bmm-sre.customize.yaml must have critical_actions');
211
+ assert.ok(sreYaml.includes('_bmad/skills/sre/MANIFEST.yaml'),
212
+ 'critical_actions must reference _bmad/skills/sre/MANIFEST.yaml');
213
+ });
214
+
215
+ // Task 2.4: Verify BMAD agent .md file gets MA-AGENTS block at top after frontmatter
216
+
217
+ await asyncTest('2.4: BMAD agent instruction file gets MA-AGENTS block at top (when file exists)', async () => {
218
+ const projectRoot = path.join(tmpDir, 'bmad-agent-test');
219
+
220
+ // Create the BMAD agent .md file (simulating what BMAD compilation produces)
221
+ const agentMdDir = path.join(projectRoot, '_bmad', 'bmm', 'agents');
222
+ await fs.ensureDir(agentMdDir);
223
+ await fs.ensureDir(path.join(projectRoot, '_bmad', 'skills', 'sre'));
224
+
225
+ const agentContent = '---\nname: Alex\nrole: SRE Agent\n---\n\n# SRE Agent\n\nYou are Alex, the SRE agent.\n';
226
+ await fs.writeFile(path.join(agentMdDir, 'sre.md'), agentContent, 'utf-8');
227
+
228
+ const agent = getAgent('bmm-sre');
229
+ assert.ok(agent, 'bmm-sre agent must exist');
230
+ assert.ok(agent.category === 'bmad', 'bmm-sre must be a bmad agent');
231
+
232
+ await updateAgentInstructions(agent, projectRoot);
233
+
234
+ const result = await fs.readFile(path.join(agentMdDir, 'sre.md'), 'utf-8');
235
+
236
+ // Verify MA-AGENTS block exists
237
+ assert.ok(result.includes(MARKER_START), 'BMAD agent file must contain MA-AGENTS-START');
238
+ assert.ok(result.includes(MARKER_END), 'BMAD agent file must contain MA-AGENTS-END');
239
+
240
+ // Verify block is after frontmatter
241
+ const fmEnd = result.indexOf('---\n', result.indexOf('---\n') + 4) + 4;
242
+ const bStart = result.indexOf(MARKER_START);
243
+ assert.ok(bStart >= fmEnd, 'MA-AGENTS block must be after frontmatter in BMAD agent file');
244
+
245
+ // Verify existing agent content preserved
246
+ assert.ok(result.includes('# SRE Agent'), 'Agent content must be preserved');
247
+ assert.ok(result.includes('You are Alex'), 'Agent identity must be preserved');
248
+ });
249
+
250
+ // Task 2.5: Verify extension survives re-deployment (re-copy doesn't break it)
251
+
252
+ await asyncTest('2.5: Extension module survives re-deployment (update scenario)', async () => {
253
+ const projectRoot = path.join(tmpDir, 'bmad-deploy-test');
254
+ const extSource = path.join(__dirname, '..', 'lib', 'bmad-extension');
255
+ const extTarget = path.join(projectRoot, '_bmad', 'extensions', 'ma-agents-skills');
256
+
257
+ // Re-deploy (simulating bmad update → recompile → extension re-deploy)
258
+ await fs.copy(extSource, extTarget);
259
+
260
+ // Verify structure is intact after re-deployment
261
+ assert.ok(await fs.pathExists(path.join(extTarget, 'module.yaml')), 'module.yaml must survive re-deploy');
262
+ const moduleYaml = await fs.readFile(path.join(extTarget, 'module.yaml'), 'utf-8');
263
+ assert.ok(moduleYaml.includes('ma-agents-skills'), 'module.yaml content must be intact after re-deploy');
264
+
265
+ const deployedFiles = (await fs.readdir(path.join(extTarget, 'agents'))).filter(f => f.endsWith('.customize.yaml'));
266
+ assert.strictEqual(deployedFiles.length, 11, `Must still have 11 .customize.yaml files after re-deploy`);
267
+ });
268
+
269
+ // ─── Task 3: Cross-story integration check (AC #1, #2) ─────────────────────
270
+
271
+ console.log('\nTask 3 — Cross-story integration check');
272
+
273
+ // Task 3.1: Verify agents.js has injectionStrategy on all 11 agents (Story 8.2 output)
274
+
275
+ await asyncTest('3.1: All 11 agents have injectionStrategy property (Story 8.2)', async () => {
276
+ const allAgents = getAllAgents();
277
+ assert.strictEqual(allAgents.length, 11, `Expected 11 agents, found ${allAgents.length}`);
278
+
279
+ for (const agent of allAgents) {
280
+ assert.ok(agent.injectionStrategy,
281
+ `Agent '${agent.id}' must have injectionStrategy`);
282
+ assert.strictEqual(agent.injectionStrategy.position, 'top',
283
+ `Agent '${agent.id}' injectionStrategy.position must be 'top'`);
284
+ assert.ok(Array.isArray(agent.injectionStrategy.skipPatterns),
285
+ `Agent '${agent.id}' injectionStrategy.skipPatterns must be an array`);
286
+ }
287
+ });
288
+
289
+ // Task 3.2: Verify installer.js reads injectionStrategy with defaults (Story 8.1 output)
290
+
291
+ await asyncTest('3.2: installer.js reads injectionStrategy from agent with fallback default (Story 8.1)', async () => {
292
+ const projectRoot = path.join(tmpDir, 'default-strategy-test');
293
+ await fs.ensureDir(path.join(projectRoot, '.claude', 'skills'));
294
+
295
+ // Create a mock agent WITHOUT injectionStrategy to test the default fallback
296
+ const mockAgent = {
297
+ id: 'test-no-strategy',
298
+ name: 'Test Agent',
299
+ category: 'ide',
300
+ instructionFiles: ['.claude/CLAUDE.md'],
301
+ getProjectPath: () => path.join(projectRoot, '.claude', 'skills'),
302
+ injectionStrategy: undefined
303
+ };
304
+
305
+ const content = '# Test Content\nSome rules.\n';
306
+ await fs.writeFile(path.join(projectRoot, '.claude', 'CLAUDE.md'), content, 'utf-8');
307
+
308
+ await updateAgentInstructions(mockAgent, projectRoot);
309
+
310
+ const result = await fs.readFile(path.join(projectRoot, '.claude', 'CLAUDE.md'), 'utf-8');
311
+
312
+ // With no injectionStrategy, default is { position: 'top', skipPatterns: [] }
313
+ // So block should be at position 0 (no skip patterns)
314
+ assert.ok(result.startsWith(MARKER_START), 'Without injectionStrategy, block should be at very top (position 0)');
315
+ assert.ok(result.includes('# Test Content'), 'Original content must be preserved');
316
+ });
317
+
318
+ // Task 3.3: Verify bmad.js deploys extension module (Story 8.3 output)
319
+
320
+ await asyncTest('3.3: bmad.js has extension deployment stage with correct paths (Story 8.3)', async () => {
321
+ const bmadJs = await fs.readFile(path.join(__dirname, '..', 'lib', 'bmad.js'), 'utf-8');
322
+
323
+ // Verify STAGE:EXTENSION code exists
324
+ assert.ok(bmadJs.includes('STAGE:EXTENSION'), 'bmad.js must have STAGE:EXTENSION deployment');
325
+ assert.ok(bmadJs.includes('bmad-extension'), 'bmad.js must reference bmad-extension source');
326
+ assert.ok(bmadJs.includes("'extensions', 'ma-agents-skills'"),
327
+ 'bmad.js must deploy to extensions/ma-agents-skills');
328
+
329
+ // Verify extension deployment happens AFTER recompile
330
+ const recompileIdx = bmadJs.indexOf('STAGE:RECOMPILE');
331
+ const extensionIdx = bmadJs.indexOf('STAGE:EXTENSION');
332
+ assert.ok(recompileIdx > -1, 'STAGE:RECOMPILE must exist');
333
+ assert.ok(extensionIdx > recompileIdx,
334
+ 'STAGE:EXTENSION must come AFTER STAGE:RECOMPILE in bmad.js');
335
+ });
336
+
337
+ // Task 3.4: Document any issues found
338
+
339
+ await asyncTest('3.4: Cross-story data flow is consistent (agents → installer → bmad)', async () => {
340
+ // Verify the data flow chain:
341
+ // agents.js defines injectionStrategy → installer.js reads it → bmad.js deploys extension
342
+
343
+ // 1. IDE agents have instructionFiles that installer.js processes
344
+ const ideAgents = getAgentsByCategory('ide');
345
+ for (const agent of ideAgents) {
346
+ assert.ok(agent.instructionFiles && agent.instructionFiles.length > 0,
347
+ `IDE agent '${agent.id}' must have instructionFiles`);
348
+ assert.ok(agent.injectionStrategy,
349
+ `IDE agent '${agent.id}' must have injectionStrategy`);
350
+ }
351
+
352
+ // 2. BMAD agents have instructionFiles pointing to _bmad/bmm/agents/
353
+ const bmadAgents = getAgentsByCategory('bmad');
354
+ for (const agent of bmadAgents) {
355
+ assert.ok(agent.instructionFiles && agent.instructionFiles.length > 0,
356
+ `BMAD agent '${agent.id}' must have instructionFiles`);
357
+ for (const file of agent.instructionFiles) {
358
+ assert.ok(file.startsWith('_bmad/'),
359
+ `BMAD agent '${agent.id}' instruction file '${file}' must be under _bmad/`);
360
+ }
361
+ }
362
+
363
+ // 3. Extension module has a customize.yaml for each BMAD agent
364
+ const extAgentsDir = path.join(__dirname, '..', 'lib', 'bmad-extension', 'agents');
365
+ const extFiles = (await fs.readdir(extAgentsDir)).filter(f => f.endsWith('.customize.yaml'));
366
+
367
+ for (const agent of bmadAgents) {
368
+ const expectedFile = `${agent.id}.customize.yaml`;
369
+ assert.ok(extFiles.includes(expectedFile),
370
+ `Extension must include ${expectedFile} for BMAD agent '${agent.id}'`);
371
+ }
372
+
373
+ // 4. All 7 built-in BMM agents also have customize.yaml in extension
374
+ const builtInBmm = ['bmm-pm', 'bmm-architect', 'bmm-dev', 'bmm-qa', 'bmm-sm', 'bmm-tech-writer', 'bmm-ux-designer'];
375
+ for (const agentId of builtInBmm) {
376
+ const expectedFile = `${agentId}.customize.yaml`;
377
+ assert.ok(extFiles.includes(expectedFile),
378
+ `Extension must include ${expectedFile} for built-in BMM agent`);
379
+ }
380
+
381
+ // 5. Verify installer.js BMAD guard (category === 'bmad' skips file creation)
382
+ const installerJs = await fs.readFile(path.join(__dirname, '..', 'lib', 'installer.js'), 'utf-8');
383
+ assert.ok(installerJs.includes("agent.category === 'bmad'"),
384
+ 'installer.js must have BMAD agent guard to skip file creation');
385
+ });
386
+
387
+ } finally {
388
+ // Clean up temp directory
389
+ await fs.remove(tmpDir).catch(() => {});
390
+ }
391
+
392
+ // ─── Summary ────────────────────────────────────────────────────────────────
393
+
394
+ console.log(`\n${'─'.repeat(60)}`);
395
+ console.log(`Story 8.4 Integration Verification: ${passed} passed, ${failed} failed`);
396
+ if (errors.length > 0) {
397
+ console.log('\nFailures:');
398
+ errors.forEach(({ name, error }) => console.log(` ✗ ${name}: ${error}`));
399
+ }
400
+ console.log(`${'─'.repeat(60)}\n`);
401
+ process.exit(failed > 0 ? 1 : 0);
402
+ })();
@@ -0,0 +1,150 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Tests for Story 9.1: Register OpenCode Agent in Registry
4
+ *
5
+ * Validates that the opencode agent entry conforms to the required schema
6
+ * and resolves correctly through the existing config-driven registry pattern.
7
+ */
8
+ 'use strict';
9
+
10
+ const assert = require('assert');
11
+
12
+ let passed = 0;
13
+ let failed = 0;
14
+ const errors = [];
15
+
16
+ function test(name, fn) {
17
+ try {
18
+ fn();
19
+ console.log(` ✓ ${name}`);
20
+ passed++;
21
+ } catch (err) {
22
+ console.error(` ✗ ${name}: ${err.message}`);
23
+ failed++;
24
+ errors.push({ name, error: err.message });
25
+ }
26
+ }
27
+
28
+ const { getAllAgents, getAgent } = require('../lib/agents');
29
+ const allAgents = getAllAgents();
30
+ const opencode = getAgent('opencode');
31
+
32
+ // ─── Schema validation ────────────────────────────────────────────────────────
33
+
34
+ console.log('\nOpenCode agent schema validation');
35
+
36
+ test('opencode agent exists in registry', () => {
37
+ assert.ok(opencode, 'opencode agent should exist in the registry');
38
+ });
39
+
40
+ test('id is "opencode"', () => {
41
+ assert.strictEqual(opencode.id, 'opencode');
42
+ });
43
+
44
+ test('name is "OpenCode"', () => {
45
+ assert.strictEqual(opencode.name, 'OpenCode');
46
+ });
47
+
48
+ test('category is "ide"', () => {
49
+ assert.strictEqual(opencode.category, 'ide');
50
+ });
51
+
52
+ test('getProjectPath is a function', () => {
53
+ assert.strictEqual(typeof opencode.getProjectPath, 'function',
54
+ 'getProjectPath should be a function');
55
+ });
56
+
57
+ test('getGlobalPath is a function', () => {
58
+ assert.strictEqual(typeof opencode.getGlobalPath, 'function',
59
+ 'getGlobalPath should be a function');
60
+ });
61
+
62
+ test('getProjectPath returns path ending in .opencode/skills', () => {
63
+ const projectPath = opencode.getProjectPath();
64
+ assert.ok(
65
+ projectPath.endsWith('.opencode/skills') || projectPath.endsWith('.opencode\\skills'),
66
+ `Expected path ending in .opencode/skills, got: ${projectPath}`
67
+ );
68
+ });
69
+
70
+ test('getGlobalPath returns a non-empty string', () => {
71
+ const globalPath = opencode.getGlobalPath();
72
+ assert.strictEqual(typeof globalPath, 'string');
73
+ assert.ok(globalPath.length > 0, 'getGlobalPath should return a non-empty string');
74
+ });
75
+
76
+ test('getGlobalPath contains opencode (no leading dot — follows AppData/XDG convention)', () => {
77
+ const globalPath = opencode.getGlobalPath();
78
+ assert.ok(
79
+ globalPath.includes('opencode'),
80
+ `Expected global path to contain 'opencode', got: ${globalPath}`
81
+ );
82
+ });
83
+
84
+ test('fileExtension is ".md"', () => {
85
+ assert.strictEqual(opencode.fileExtension, '.md');
86
+ });
87
+
88
+ test('description is a non-empty string', () => {
89
+ assert.strictEqual(typeof opencode.description, 'string',
90
+ 'description should be a string');
91
+ assert.ok(opencode.description.length > 0, 'description should not be empty');
92
+ });
93
+
94
+ test('template is "generic"', () => {
95
+ assert.strictEqual(opencode.template, 'generic');
96
+ });
97
+
98
+ test('instructionFiles is a non-empty array', () => {
99
+ assert.ok(Array.isArray(opencode.instructionFiles),
100
+ 'instructionFiles should be an array');
101
+ assert.ok(opencode.instructionFiles.length > 0,
102
+ 'instructionFiles should not be empty');
103
+ });
104
+
105
+ test('instructionFiles contains "opencode.json"', () => {
106
+ assert.ok(opencode.instructionFiles.includes('opencode.json'),
107
+ 'instructionFiles should include "opencode.json"');
108
+ });
109
+
110
+ test('injectionStrategy is an object', () => {
111
+ assert.ok(opencode.injectionStrategy && typeof opencode.injectionStrategy === 'object',
112
+ 'injectionStrategy should be an object');
113
+ });
114
+
115
+ test('injectionStrategy.position is "json-merge"', () => {
116
+ assert.strictEqual(opencode.injectionStrategy.position, 'json-merge');
117
+ });
118
+
119
+ test('injectionStrategy.targetKey is "instructions"', () => {
120
+ assert.strictEqual(opencode.injectionStrategy.targetKey, 'instructions');
121
+ });
122
+
123
+ // ─── Registry resolution ──────────────────────────────────────────────────────
124
+
125
+ console.log('\nOpenCode agent resolution');
126
+
127
+ test('getAgent("opencode") returns the agent', () => {
128
+ const resolved = getAgent('opencode');
129
+ assert.ok(resolved, 'getAgent("opencode") should return an agent object');
130
+ assert.strictEqual(resolved.id, 'opencode');
131
+ });
132
+
133
+ test('opencode appears in getAllAgents()', () => {
134
+ const ids = allAgents.map(a => a.id);
135
+ assert.ok(ids.includes('opencode'), 'opencode should appear in getAllAgents()');
136
+ });
137
+
138
+ test('opencode is the agent with id "opencode" in the full list', () => {
139
+ const found = allAgents.find(a => a.id === 'opencode');
140
+ assert.ok(found, 'opencode entry should be findable in allAgents array');
141
+ assert.strictEqual(found.id, 'opencode');
142
+ });
143
+
144
+ // Print summary
145
+ console.log(`\n${passed} passed, ${failed} failed`);
146
+ if (errors.length > 0) {
147
+ console.log('\nFailed tests:');
148
+ errors.forEach(e => console.log(` - ${e.name}: ${e.error}`));
149
+ }
150
+ if (failed > 0) process.exit(1);