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,260 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Tests for Story 9.4: JSON Injection — Handle Invalid opencode.json Gracefully
4
+ *
5
+ * Validates that updateAgentInstructions() recovers non-fatally when
6
+ * opencode.json contains invalid JSON: logs an error, leaves the file
7
+ * byte-identical, and continues processing subsequent agents.
8
+ */
9
+ 'use strict';
10
+
11
+ const assert = require('assert');
12
+ const fs = require('fs-extra');
13
+ const path = require('path');
14
+ const os = require('os');
15
+
16
+ let passed = 0;
17
+ let failed = 0;
18
+ const errors = [];
19
+
20
+ async function asyncTest(name, fn) {
21
+ try {
22
+ await fn();
23
+ console.log(` ✓ ${name}`);
24
+ passed++;
25
+ } catch (err) {
26
+ console.error(` ✗ ${name}: ${err.message}`);
27
+ failed++;
28
+ errors.push({ name, error: err.message });
29
+ }
30
+ }
31
+
32
+ // Load functions under test
33
+ const {
34
+ _testUpdateAgentInstructions: updateFn,
35
+ _MA_AGENTS_SOURCE: MA_AGENTS_SOURCE
36
+ } = require('../lib/installer');
37
+
38
+ // Helper: create a temporary directory, run fn, then clean up
39
+ async function withTempDir(fn) {
40
+ const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'ma-agents-json-error-test-'));
41
+ try {
42
+ await fn(tmpDir);
43
+ } finally {
44
+ await fs.remove(tmpDir);
45
+ }
46
+ }
47
+
48
+ // Helper: build a mock OpenCode agent pointing to a given projectRoot
49
+ function createOpenCodeAgent(projectRoot) {
50
+ return {
51
+ id: 'opencode',
52
+ name: 'OpenCode',
53
+ category: 'ide',
54
+ instructionFiles: ['opencode.json'],
55
+ injectionStrategy: { position: 'json-merge', targetKey: 'instructions' },
56
+ getProjectPath: () => path.join(projectRoot, '.opencode', 'skills'),
57
+ getGlobalPath: () => path.join(os.homedir(), '.opencode', 'skills')
58
+ };
59
+ }
60
+
61
+ // Helper: build a generic markdown agent (simulates "someOtherAgent")
62
+ function createMarkdownAgent(projectRoot, fileName) {
63
+ return {
64
+ id: 'claude-code',
65
+ name: 'Claude Code',
66
+ category: 'ide',
67
+ instructionFiles: [fileName],
68
+ injectionStrategy: { position: 'top', skipPatterns: [] },
69
+ getProjectPath: () => path.join(projectRoot, '.claude', 'skills'),
70
+ getGlobalPath: () => path.join(os.homedir(), '.claude', 'skills')
71
+ };
72
+ }
73
+
74
+ // The corrupt JSON fixture content
75
+ const CORRUPT_JSON = 'not valid json {{{';
76
+
77
+ // ─── Fixture A: corrupt opencode.json ─────────────────────────────────────────
78
+
79
+ console.log('\nStory 9.4 — JSON error handling: corrupt opencode.json');
80
+
81
+ (async () => {
82
+ await asyncTest('9.4.1: file is byte-identical after run with corrupt JSON (not modified)', async () => {
83
+ await withTempDir(async (tmpDir) => {
84
+ const agent = createOpenCodeAgent(tmpDir);
85
+ const filePath = path.join(tmpDir, 'opencode.json');
86
+
87
+ // Write the corrupt fixture
88
+ await fs.writeFile(filePath, CORRUPT_JSON, 'utf-8');
89
+ const beforeBytes = fs.readFileSync(filePath);
90
+
91
+ await updateFn(agent, tmpDir);
92
+
93
+ const afterBytes = fs.readFileSync(filePath);
94
+ assert.ok(
95
+ beforeBytes.equals(afterBytes),
96
+ `File content changed after corrupt-JSON run.\nBefore: ${beforeBytes.toString()}\nAfter: ${afterBytes.toString()}`
97
+ );
98
+ });
99
+ });
100
+
101
+ await asyncTest('9.4.2: console.error is called with the file path on parse failure', async () => {
102
+ await withTempDir(async (tmpDir) => {
103
+ const agent = createOpenCodeAgent(tmpDir);
104
+ const filePath = path.join(tmpDir, 'opencode.json');
105
+ await fs.writeFile(filePath, CORRUPT_JSON, 'utf-8');
106
+
107
+ const errorMessages = [];
108
+ const origError = console.error;
109
+ console.error = (...args) => errorMessages.push(args.join(' '));
110
+ try {
111
+ await updateFn(agent, tmpDir);
112
+ } finally {
113
+ console.error = origError;
114
+ }
115
+
116
+ assert.ok(errorMessages.length > 0, 'console.error should have been called');
117
+ const combined = errorMessages.join('\n');
118
+ assert.ok(
119
+ combined.includes(filePath),
120
+ `Error message should contain the file path "${filePath}".\nActual messages: ${combined}`
121
+ );
122
+ });
123
+ });
124
+
125
+ await asyncTest('9.4.3: console.error message describes the parse failure', async () => {
126
+ await withTempDir(async (tmpDir) => {
127
+ const agent = createOpenCodeAgent(tmpDir);
128
+ const filePath = path.join(tmpDir, 'opencode.json');
129
+ await fs.writeFile(filePath, CORRUPT_JSON, 'utf-8');
130
+
131
+ const errorMessages = [];
132
+ const origError = console.error;
133
+ console.error = (...args) => errorMessages.push(args.join(' '));
134
+ try {
135
+ await updateFn(agent, tmpDir);
136
+ } finally {
137
+ console.error = origError;
138
+ }
139
+
140
+ const combined = errorMessages.join('\n');
141
+ // The message must mention that file was not modified
142
+ assert.ok(
143
+ combined.toLowerCase().includes('not modified') || combined.toLowerCase().includes('file not modified'),
144
+ `Error message should state the file was not modified.\nActual messages: ${combined}`
145
+ );
146
+ // The message must include a parse/error description (err.message from JSON.parse)
147
+ assert.ok(
148
+ combined.toLowerCase().includes('parse') ||
149
+ combined.toLowerCase().includes('token') ||
150
+ combined.toLowerCase().includes('json') ||
151
+ combined.toLowerCase().includes('unexpected'),
152
+ `Error message should describe the parse failure.\nActual messages: ${combined}`
153
+ );
154
+ });
155
+ });
156
+
157
+ await asyncTest('9.4.4: no .tmp file is left on disk after parse failure', async () => {
158
+ await withTempDir(async (tmpDir) => {
159
+ const agent = createOpenCodeAgent(tmpDir);
160
+ const filePath = path.join(tmpDir, 'opencode.json');
161
+ await fs.writeFile(filePath, CORRUPT_JSON, 'utf-8');
162
+
163
+ const origError = console.error;
164
+ console.error = () => {};
165
+ try {
166
+ await updateFn(agent, tmpDir);
167
+ } finally {
168
+ console.error = origError;
169
+ }
170
+
171
+ const tmpPath = filePath + '.tmp';
172
+ assert.strictEqual(
173
+ fs.existsSync(tmpPath),
174
+ false,
175
+ '.tmp file should not exist after a parse-failure run'
176
+ );
177
+ });
178
+ });
179
+
180
+ // ─── Fixture B: continuation test (CRITICAL) ────────────────────────────────
181
+
182
+ console.log('\nStory 9.4 — JSON error handling: continuation after error (Fixture B)');
183
+
184
+ await asyncTest('9.4.5: subsequent agent instruction file IS updated after OpenCode parse failure', async () => {
185
+ await withTempDir(async (tmpDir) => {
186
+ // 1. Set up OpenCode with corrupt JSON
187
+ const opencodeAgent = createOpenCodeAgent(tmpDir);
188
+ const opencodeFilePath = path.join(tmpDir, 'opencode.json');
189
+ await fs.writeFile(opencodeFilePath, CORRUPT_JSON, 'utf-8');
190
+
191
+ // 2. Set up a second markdown agent whose file does not yet exist
192
+ const otherFileName = '.claude/CLAUDE.md';
193
+ const otherAgent = createMarkdownAgent(tmpDir, otherFileName);
194
+ const otherFilePath = path.join(tmpDir, otherFileName);
195
+
196
+ // Suppress console.error during this test
197
+ const origError = console.error;
198
+ console.error = () => {};
199
+ try {
200
+ // 3. Run both agents in sequence — simulating what the install loop does
201
+ await updateFn(opencodeAgent, tmpDir);
202
+ await updateFn(otherAgent, tmpDir);
203
+ } finally {
204
+ console.error = origError;
205
+ }
206
+
207
+ // 4. OpenCode file must be unchanged (still corrupt)
208
+ const opencodeBytes = fs.readFileSync(opencodeFilePath, 'utf-8');
209
+ assert.strictEqual(
210
+ opencodeBytes,
211
+ CORRUPT_JSON,
212
+ 'opencode.json should remain byte-identical (corrupt) after error'
213
+ );
214
+
215
+ // 5. The other agent's instruction file MUST have been created/updated
216
+ assert.ok(
217
+ fs.existsSync(otherFilePath),
218
+ `Second agent's instruction file "${otherFileName}" should have been created after OpenCode error`
219
+ );
220
+ const otherContent = fs.readFileSync(otherFilePath, 'utf-8');
221
+ assert.ok(
222
+ otherContent.includes('MA-AGENTS-START') || otherContent.includes('MANIFEST.yaml'),
223
+ `Second agent's instruction file should contain injected content.\nActual: ${otherContent}`
224
+ );
225
+ });
226
+ });
227
+
228
+ await asyncTest('9.4.6: updateAgentInstructions does not throw on corrupt JSON (non-fatal)', async () => {
229
+ await withTempDir(async (tmpDir) => {
230
+ const agent = createOpenCodeAgent(tmpDir);
231
+ const filePath = path.join(tmpDir, 'opencode.json');
232
+ await fs.writeFile(filePath, CORRUPT_JSON, 'utf-8');
233
+
234
+ const origError = console.error;
235
+ console.error = () => {};
236
+ let threw = false;
237
+ try {
238
+ await updateFn(agent, tmpDir);
239
+ } catch (err) {
240
+ threw = true;
241
+ } finally {
242
+ console.error = origError;
243
+ }
244
+
245
+ assert.strictEqual(
246
+ threw,
247
+ false,
248
+ 'updateAgentInstructions() must NOT throw when JSON.parse fails — error must be non-fatal'
249
+ );
250
+ });
251
+ });
252
+
253
+ // Print summary
254
+ console.log(`\n${passed} passed, ${failed} failed`);
255
+ if (errors.length > 0) {
256
+ console.log('\nFailed tests:');
257
+ errors.forEach(e => console.log(` - ${e.name}: ${e.error}`));
258
+ }
259
+ if (failed > 0) process.exit(1);
260
+ })();
@@ -0,0 +1,256 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Tests for Story 9.2: JSON Injection — Create opencode.json When Absent
4
+ *
5
+ * Validates that updateAgentInstructions() creates a valid opencode.json
6
+ * from scratch when the file does not yet exist, using the json-merge strategy.
7
+ */
8
+ 'use strict';
9
+
10
+ const assert = require('assert');
11
+ const fs = require('fs-extra');
12
+ const path = require('path');
13
+ const os = require('os');
14
+
15
+ let passed = 0;
16
+ let failed = 0;
17
+ const errors = [];
18
+
19
+ function test(name, fn) {
20
+ try {
21
+ fn();
22
+ console.log(` ✓ ${name}`);
23
+ passed++;
24
+ } catch (err) {
25
+ console.error(` ✗ ${name}: ${err.message}`);
26
+ failed++;
27
+ errors.push({ name, error: err.message });
28
+ }
29
+ }
30
+
31
+ async function asyncTest(name, fn) {
32
+ try {
33
+ await fn();
34
+ console.log(` ✓ ${name}`);
35
+ passed++;
36
+ } catch (err) {
37
+ console.error(` ✗ ${name}: ${err.message}`);
38
+ failed++;
39
+ errors.push({ name, error: err.message });
40
+ }
41
+ }
42
+
43
+ // Load functions under test
44
+ const {
45
+ _testUpdateAgentInstructions: updateFn,
46
+ _MA_AGENTS_SOURCE: MA_AGENTS_SOURCE
47
+ } = require('../lib/installer');
48
+
49
+ // Helper: create a temporary directory, run fn, then clean up
50
+ async function withTempDir(fn) {
51
+ const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'ma-agents-json-test-'));
52
+ try {
53
+ await fn(tmpDir);
54
+ } finally {
55
+ await fs.remove(tmpDir);
56
+ }
57
+ }
58
+
59
+ // Helper: build a mock OpenCode agent pointing to a given projectRoot
60
+ function createOpenCodeAgent(projectRoot) {
61
+ return {
62
+ id: 'opencode',
63
+ name: 'OpenCode',
64
+ category: 'ide',
65
+ instructionFiles: ['opencode.json'],
66
+ injectionStrategy: { position: 'json-merge', targetKey: 'instructions' },
67
+ getProjectPath: () => path.join(projectRoot, '.opencode', 'skills'),
68
+ getGlobalPath: () => path.join(os.homedir(), '.opencode', 'skills')
69
+ };
70
+ }
71
+
72
+ // ─── Creation path tests (file absent) ───────────────────────────────────────
73
+
74
+ console.log('\nStory 9.2 — JSON injection: file-absent creation path');
75
+
76
+ (async () => {
77
+ await asyncTest('9.2.1: creates opencode.json when file does not exist', async () => {
78
+ await withTempDir(async (tmpDir) => {
79
+ const agent = createOpenCodeAgent(tmpDir);
80
+ const filePath = path.join(tmpDir, 'opencode.json');
81
+
82
+ assert.strictEqual(fs.existsSync(filePath), false, 'Precondition: file must not exist');
83
+ await updateFn(agent, tmpDir);
84
+ assert.ok(fs.existsSync(filePath), 'opencode.json should have been created');
85
+ });
86
+ });
87
+
88
+ await asyncTest('9.2.2: created file is valid JSON', async () => {
89
+ await withTempDir(async (tmpDir) => {
90
+ const agent = createOpenCodeAgent(tmpDir);
91
+ const filePath = path.join(tmpDir, 'opencode.json');
92
+
93
+ await updateFn(agent, tmpDir);
94
+ const raw = fs.readFileSync(filePath, 'utf-8');
95
+ let parsed;
96
+ try {
97
+ parsed = JSON.parse(raw);
98
+ } catch (e) {
99
+ throw new Error(`File content is not valid JSON: ${e.message}\nContent: ${raw}`);
100
+ }
101
+ assert.ok(parsed !== null && typeof parsed === 'object', 'Parsed result should be an object');
102
+ });
103
+ });
104
+
105
+ await asyncTest('9.2.3: created file contains an "instructions" array', async () => {
106
+ await withTempDir(async (tmpDir) => {
107
+ const agent = createOpenCodeAgent(tmpDir);
108
+ const filePath = path.join(tmpDir, 'opencode.json');
109
+
110
+ await updateFn(agent, tmpDir);
111
+ const parsed = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
112
+ assert.ok(Array.isArray(parsed.instructions),
113
+ `Expected "instructions" to be an array, got: ${typeof parsed.instructions}`);
114
+ });
115
+ });
116
+
117
+ await asyncTest('9.2.4: instructions array is non-empty', async () => {
118
+ await withTempDir(async (tmpDir) => {
119
+ const agent = createOpenCodeAgent(tmpDir);
120
+ const filePath = path.join(tmpDir, 'opencode.json');
121
+
122
+ await updateFn(agent, tmpDir);
123
+ const parsed = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
124
+ assert.ok(parsed.instructions.length > 0,
125
+ 'instructions array should have at least one entry');
126
+ });
127
+ });
128
+
129
+ await asyncTest('9.2.5: every entry has _source === MA_AGENTS_SOURCE', async () => {
130
+ await withTempDir(async (tmpDir) => {
131
+ const agent = createOpenCodeAgent(tmpDir);
132
+ const filePath = path.join(tmpDir, 'opencode.json');
133
+
134
+ await updateFn(agent, tmpDir);
135
+ const parsed = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
136
+ for (const entry of parsed.instructions) {
137
+ assert.strictEqual(entry._source, MA_AGENTS_SOURCE,
138
+ `Entry _source should be "${MA_AGENTS_SOURCE}", got: "${entry._source}"`);
139
+ }
140
+ });
141
+ });
142
+
143
+ await asyncTest('9.2.6: every entry has a non-empty "text" string', async () => {
144
+ await withTempDir(async (tmpDir) => {
145
+ const agent = createOpenCodeAgent(tmpDir);
146
+ const filePath = path.join(tmpDir, 'opencode.json');
147
+
148
+ await updateFn(agent, tmpDir);
149
+ const parsed = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
150
+ for (const entry of parsed.instructions) {
151
+ assert.strictEqual(typeof entry.text, 'string',
152
+ `Entry "text" should be a string, got: ${typeof entry.text}`);
153
+ assert.ok(entry.text.length > 0,
154
+ 'Entry "text" should not be empty');
155
+ }
156
+ });
157
+ });
158
+
159
+ await asyncTest('9.2.7: no extra top-level keys in the written JSON', async () => {
160
+ await withTempDir(async (tmpDir) => {
161
+ const agent = createOpenCodeAgent(tmpDir);
162
+ const filePath = path.join(tmpDir, 'opencode.json');
163
+
164
+ await updateFn(agent, tmpDir);
165
+ const parsed = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
166
+ const keys = Object.keys(parsed);
167
+ assert.deepStrictEqual(keys, ['instructions'],
168
+ `Expected only ["instructions"] as top-level keys, got: ${JSON.stringify(keys)}`);
169
+ });
170
+ });
171
+
172
+ await asyncTest('9.2.8: file uses 2-space indentation', async () => {
173
+ await withTempDir(async (tmpDir) => {
174
+ const agent = createOpenCodeAgent(tmpDir);
175
+ const filePath = path.join(tmpDir, 'opencode.json');
176
+
177
+ await updateFn(agent, tmpDir);
178
+ const raw = fs.readFileSync(filePath, 'utf-8');
179
+ // JSON.stringify(data, null, 2) produces lines starting with " " (2 spaces) for first level
180
+ assert.ok(raw.includes(' "instructions"'),
181
+ 'File should use 2-space indentation (line should start with " \\"instructions\\"")\nActual content:\n' + raw);
182
+ });
183
+ });
184
+
185
+ await asyncTest('9.2.9: instruction text contains skill manifest reference', async () => {
186
+ await withTempDir(async (tmpDir) => {
187
+ const agent = createOpenCodeAgent(tmpDir);
188
+ const filePath = path.join(tmpDir, 'opencode.json');
189
+
190
+ await updateFn(agent, tmpDir);
191
+ const parsed = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
192
+ const allText = parsed.instructions.map(e => e.text).join('\n');
193
+ assert.ok(allText.includes('MANIFEST.yaml'),
194
+ 'Instruction text should reference MANIFEST.yaml');
195
+ });
196
+ });
197
+
198
+ await asyncTest('9.2.10: MA_AGENTS_SOURCE constant is "ma-agents"', () => {
199
+ assert.strictEqual(MA_AGENTS_SOURCE, 'ma-agents',
200
+ `MA_AGENTS_SOURCE should equal "ma-agents", got: "${MA_AGENTS_SOURCE}"`);
201
+ });
202
+
203
+ await asyncTest('9.2.11: non-json-merge agents are not affected (falls through to markdown logic)', async () => {
204
+ await withTempDir(async (tmpDir) => {
205
+ // A regular markdown agent — should not produce a JSON file
206
+ const markdownAgent = {
207
+ id: 'claude-code',
208
+ name: 'Claude Code',
209
+ category: 'ide',
210
+ instructionFiles: ['.claude/CLAUDE.md'],
211
+ injectionStrategy: { position: 'top', skipPatterns: [] },
212
+ getProjectPath: () => path.join(tmpDir, '.claude', 'skills'),
213
+ getGlobalPath: () => path.join(os.homedir(), '.claude', 'skills')
214
+ };
215
+ await updateFn(markdownAgent, tmpDir);
216
+ // Should NOT create opencode.json
217
+ assert.strictEqual(fs.existsSync(path.join(tmpDir, 'opencode.json')), false,
218
+ 'Should not create opencode.json for non-json-merge agents');
219
+ // Should create the markdown file instead
220
+ assert.ok(fs.existsSync(path.join(tmpDir, '.claude', 'CLAUDE.md')),
221
+ 'Markdown agent file should be created');
222
+ });
223
+ });
224
+
225
+ await asyncTest('9.2.12: file present — user entries preserved, fresh ma-agents entries appended (Story 9.3)', async () => {
226
+ await withTempDir(async (tmpDir) => {
227
+ const agent = createOpenCodeAgent(tmpDir);
228
+ const filePath = path.join(tmpDir, 'opencode.json');
229
+
230
+ // Pre-create a file with a single user entry
231
+ const existingContent = { instructions: [{ _source: 'user', text: 'user instruction' }] };
232
+ await fs.writeFile(filePath, JSON.stringify(existingContent, null, 2), 'utf-8');
233
+
234
+ await updateFn(agent, tmpDir);
235
+
236
+ // Story 9.3 merge: user entry preserved, fresh ma-agents entries appended
237
+ const parsed = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
238
+ const userEntries = parsed.instructions.filter(e => e._source !== MA_AGENTS_SOURCE);
239
+ const maEntries = parsed.instructions.filter(e => e._source === MA_AGENTS_SOURCE);
240
+ assert.strictEqual(userEntries.length, 1,
241
+ 'Existing user entry should be preserved');
242
+ assert.strictEqual(userEntries[0]._source, 'user',
243
+ 'User entry _source should remain "user"');
244
+ assert.ok(maEntries.length > 0,
245
+ 'Fresh ma-agents entries should be appended');
246
+ });
247
+ });
248
+
249
+ // Print summary
250
+ console.log(`\n${passed} passed, ${failed} failed`);
251
+ if (errors.length > 0) {
252
+ console.log('\nFailed tests:');
253
+ errors.forEach(e => console.log(` - ${e.name}: ${e.error}`));
254
+ }
255
+ if (failed > 0) process.exit(1);
256
+ })();