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,378 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Tests for validate-skill command implementation
4
+ * Story 3.2: Skill Validation Command
5
+ *
6
+ * Task 6.1: Valid skill reports VALID
7
+ * Task 6.2: Broken skill reports all errors
8
+ * Task 6.3: Unknown agent template triggers warning
9
+ */
10
+ 'use strict';
11
+
12
+ const assert = require('assert');
13
+ const { spawnSync } = require('child_process');
14
+ const path = require('path');
15
+ const fs = require('fs');
16
+ const os = require('os');
17
+
18
+ const CLI_PATH = path.join(__dirname, '..', 'bin', 'cli.js');
19
+ const PROJECT_ROOT = path.join(__dirname, '..');
20
+
21
+ let passed = 0;
22
+ let failed = 0;
23
+ const errors = [];
24
+
25
+ function test(name, fn) {
26
+ try {
27
+ fn();
28
+ console.log(` ✓ ${name}`);
29
+ passed++;
30
+ } catch (err) {
31
+ console.error(` ✗ ${name}: ${err.message}`);
32
+ failed++;
33
+ errors.push({ name, error: err.message });
34
+ }
35
+ }
36
+
37
+ // ─── Module imports ────────────────────────────────────────────────────────────
38
+ let validateSkill;
39
+ try {
40
+ ({ validateSkill } = require('../lib/skill-authoring'));
41
+ } catch (e) {
42
+ console.error('\n FATAL: Cannot load skill-authoring module');
43
+ console.error(' ', e.message);
44
+ process.exit(1);
45
+ }
46
+
47
+ // ─── Helper: build a temporary skill dir ─────────────────────────────────────
48
+ function makeTmpSkill(fields = {}) {
49
+ const tmpBase = fs.mkdtempSync(path.join(os.tmpdir(), 'ma-validate-test-'));
50
+ const skillName = fields.skillName || 'test-skill';
51
+ const skillDir = path.join(tmpBase, skillName);
52
+ fs.mkdirSync(skillDir, { recursive: true });
53
+
54
+ if (fields.skillJson !== undefined) {
55
+ fs.writeFileSync(path.join(skillDir, 'skill.json'), fields.skillJson, 'utf8');
56
+ } else if (fields.skillJson !== null) {
57
+ // Default valid skill.json
58
+ const json = Object.assign({
59
+ name: 'Test Skill',
60
+ description: 'A test skill',
61
+ version: '1.0.0',
62
+ category: 'workflow',
63
+ author: 'ma-agents',
64
+ tags: ['test'],
65
+ always_load: false
66
+ }, fields.skillJsonOverride || {});
67
+ fs.writeFileSync(path.join(skillDir, 'skill.json'), JSON.stringify(json, null, 2), 'utf8');
68
+ }
69
+
70
+ if (fields.skillMd !== false) {
71
+ fs.writeFileSync(path.join(skillDir, 'SKILL.md'), '# Test Skill\n', 'utf8');
72
+ }
73
+
74
+ if (fields.templates) {
75
+ const templatesDir = path.join(skillDir, 'templates');
76
+ fs.mkdirSync(templatesDir, { recursive: true });
77
+ for (const [filename, content] of Object.entries(fields.templates)) {
78
+ fs.writeFileSync(path.join(templatesDir, filename), content, 'utf8');
79
+ }
80
+ }
81
+
82
+ return { tmpBase, skillDir, skillName };
83
+ }
84
+
85
+ // ─── Unit tests: validateSkill() ─────────────────────────────────────────────
86
+ console.log('\n validateSkill unit tests\n');
87
+
88
+ // AC #1: Valid skill
89
+ test('valid skill returns valid=true with metadata', () => {
90
+ const { tmpBase, skillName } = makeTmpSkill({});
91
+ try {
92
+ const result = validateSkill(skillName, tmpBase);
93
+ assert.strictEqual(result.valid, true, `Expected valid=true, got: ${JSON.stringify(result)}`);
94
+ assert.ok(Array.isArray(result.errors), 'Expected errors array');
95
+ assert.strictEqual(result.errors.length, 0, `Expected no errors, got: ${JSON.stringify(result.errors)}`);
96
+ assert.ok(result.metadata, 'Expected metadata object');
97
+ assert.strictEqual(result.metadata.name, 'Test Skill');
98
+ assert.strictEqual(result.metadata.version, '1.0.0');
99
+ } finally {
100
+ fs.rmSync(tmpBase, { recursive: true, force: true });
101
+ }
102
+ });
103
+
104
+ // AC #2: Missing skill.json
105
+ test('missing skill.json produces error', () => {
106
+ const { tmpBase, skillName } = makeTmpSkill({ skillJson: undefined });
107
+ // Remove the default skill.json by passing null-ish — need to NOT write it
108
+ fs.rmSync(path.join(tmpBase, skillName, 'skill.json'), { force: true });
109
+ try {
110
+ const result = validateSkill(skillName, tmpBase);
111
+ assert.strictEqual(result.valid, false);
112
+ assert.ok(result.errors.some(e => e.includes('skill.json')), `Expected skill.json error, got: ${JSON.stringify(result.errors)}`);
113
+ } finally {
114
+ fs.rmSync(tmpBase, { recursive: true, force: true });
115
+ }
116
+ });
117
+
118
+ // AC #3: Missing SKILL.md
119
+ test('missing SKILL.md produces error', () => {
120
+ const { tmpBase, skillName } = makeTmpSkill({ skillMd: false });
121
+ try {
122
+ const result = validateSkill(skillName, tmpBase);
123
+ assert.strictEqual(result.valid, false);
124
+ assert.ok(result.errors.some(e => e.includes('SKILL.md')), `Expected SKILL.md error, got: ${JSON.stringify(result.errors)}`);
125
+ } finally {
126
+ fs.rmSync(tmpBase, { recursive: true, force: true });
127
+ }
128
+ });
129
+
130
+ // Skill not found
131
+ test('non-existent skill returns error (not found)', () => {
132
+ const tmpBase = fs.mkdtempSync(path.join(os.tmpdir(), 'ma-validate-empty-'));
133
+ try {
134
+ const result = validateSkill('no-such-skill', tmpBase);
135
+ assert.strictEqual(result.valid, false);
136
+ assert.ok(result.notFound, 'Expected notFound flag');
137
+ } finally {
138
+ fs.rmSync(tmpBase, { recursive: true, force: true });
139
+ }
140
+ });
141
+
142
+ // AC #4: Invalid JSON
143
+ test('corrupt skill.json produces error', () => {
144
+ const { tmpBase, skillName } = makeTmpSkill({ skillJson: 'not valid json {{{' });
145
+ try {
146
+ const result = validateSkill(skillName, tmpBase);
147
+ assert.strictEqual(result.valid, false);
148
+ assert.ok(result.errors.some(e => e.toLowerCase().includes('json')), `Expected JSON error, got: ${JSON.stringify(result.errors)}`);
149
+ } finally {
150
+ fs.rmSync(tmpBase, { recursive: true, force: true });
151
+ }
152
+ });
153
+
154
+ // AC #4: Missing required fields
155
+ test('missing name field produces field error', () => {
156
+ const { tmpBase, skillName } = makeTmpSkill({ skillJsonOverride: { name: '' } });
157
+ try {
158
+ const result = validateSkill(skillName, tmpBase);
159
+ assert.strictEqual(result.valid, false);
160
+ assert.ok(result.errors.some(e => e.includes('name')), `Expected name error, got: ${JSON.stringify(result.errors)}`);
161
+ } finally {
162
+ fs.rmSync(tmpBase, { recursive: true, force: true });
163
+ }
164
+ });
165
+
166
+ test('missing version field produces field error', () => {
167
+ const { tmpBase, skillName } = makeTmpSkill({ skillJsonOverride: { version: undefined } });
168
+ // Remove version from written JSON
169
+ const skillDir = path.join(tmpBase, skillName);
170
+ const json = JSON.parse(fs.readFileSync(path.join(skillDir, 'skill.json'), 'utf8'));
171
+ delete json.version;
172
+ fs.writeFileSync(path.join(skillDir, 'skill.json'), JSON.stringify(json), 'utf8');
173
+ try {
174
+ const result = validateSkill(skillName, tmpBase);
175
+ assert.strictEqual(result.valid, false);
176
+ assert.ok(result.errors.some(e => e.includes('version')), `Expected version error, got: ${JSON.stringify(result.errors)}`);
177
+ } finally {
178
+ fs.rmSync(tmpBase, { recursive: true, force: true });
179
+ }
180
+ });
181
+
182
+ test('invalid version format produces field error', () => {
183
+ const { tmpBase, skillName } = makeTmpSkill({ skillJsonOverride: { version: 'v1.0' } });
184
+ try {
185
+ const result = validateSkill(skillName, tmpBase);
186
+ assert.strictEqual(result.valid, false);
187
+ assert.ok(result.errors.some(e => e.includes('version')), `Expected version error, got: ${JSON.stringify(result.errors)}`);
188
+ } finally {
189
+ fs.rmSync(tmpBase, { recursive: true, force: true });
190
+ }
191
+ });
192
+
193
+ test('invalid category produces field error', () => {
194
+ const { tmpBase, skillName } = makeTmpSkill({ skillJsonOverride: { category: 'invalid-cat' } });
195
+ try {
196
+ const result = validateSkill(skillName, tmpBase);
197
+ assert.strictEqual(result.valid, false);
198
+ assert.ok(result.errors.some(e => e.includes('category')), `Expected category error, got: ${JSON.stringify(result.errors)}`);
199
+ } finally {
200
+ fs.rmSync(tmpBase, { recursive: true, force: true });
201
+ }
202
+ });
203
+
204
+ test('non-boolean always_load produces field error', () => {
205
+ const { tmpBase, skillName } = makeTmpSkill({ skillJsonOverride: { always_load: 'yes' } });
206
+ try {
207
+ const result = validateSkill(skillName, tmpBase);
208
+ assert.strictEqual(result.valid, false);
209
+ assert.ok(result.errors.some(e => e.includes('always_load')), `Expected always_load error, got: ${JSON.stringify(result.errors)}`);
210
+ } finally {
211
+ fs.rmSync(tmpBase, { recursive: true, force: true });
212
+ }
213
+ });
214
+
215
+ test('non-array tags produces field error', () => {
216
+ const { tmpBase, skillName } = makeTmpSkill({ skillJsonOverride: { tags: 'not-an-array' } });
217
+ try {
218
+ const result = validateSkill(skillName, tmpBase);
219
+ assert.strictEqual(result.valid, false);
220
+ assert.ok(result.errors.some(e => e.includes('tags')), `Expected tags error, got: ${JSON.stringify(result.errors)}`);
221
+ } finally {
222
+ fs.rmSync(tmpBase, { recursive: true, force: true });
223
+ }
224
+ });
225
+
226
+ // AC #5: Template validation
227
+ test('valid agent template produces no warning', () => {
228
+ const { tmpBase, skillName } = makeTmpSkill({
229
+ templates: { 'claude-code.md': '# Template' }
230
+ });
231
+ try {
232
+ const result = validateSkill(skillName, tmpBase);
233
+ assert.strictEqual(result.valid, true, `Expected valid, got errors: ${JSON.stringify(result.errors)}`);
234
+ assert.ok(!result.warnings.some(w => w.includes('claude-code')), 'Expected no warning for known agent');
235
+ } finally {
236
+ fs.rmSync(tmpBase, { recursive: true, force: true });
237
+ }
238
+ });
239
+
240
+ test('unknown agent template produces warning', () => {
241
+ const { tmpBase, skillName } = makeTmpSkill({
242
+ templates: { 'unknown-agent.md': '# Template' }
243
+ });
244
+ try {
245
+ const result = validateSkill(skillName, tmpBase);
246
+ assert.ok(Array.isArray(result.warnings), 'Expected warnings array');
247
+ assert.ok(result.warnings.some(w => w.includes('unknown-agent')), `Expected unknown-agent warning, got: ${JSON.stringify(result.warnings)}`);
248
+ } finally {
249
+ fs.rmSync(tmpBase, { recursive: true, force: true });
250
+ }
251
+ });
252
+
253
+ test('unknown agent template does not make skill invalid', () => {
254
+ const { tmpBase, skillName } = makeTmpSkill({
255
+ templates: { 'unknown-agent.md': '# Template' }
256
+ });
257
+ try {
258
+ const result = validateSkill(skillName, tmpBase);
259
+ assert.strictEqual(result.valid, true, 'Warnings should not make skill invalid');
260
+ } finally {
261
+ fs.rmSync(tmpBase, { recursive: true, force: true });
262
+ }
263
+ });
264
+
265
+ // ─── CLI integration tests ─────────────────────────────────────────────────────
266
+ console.log('\n validate-skill CLI integration tests\n');
267
+
268
+ // Task 6.1: Valid skill
269
+ test('validate-skill: git-workflow-skill reports VALID with exit 0', () => {
270
+ const result = spawnSync('node', [CLI_PATH, 'validate-skill', 'git-workflow-skill'], {
271
+ encoding: 'utf8', cwd: PROJECT_ROOT
272
+ });
273
+ assert.strictEqual(result.status, 0, `Expected exit 0. stderr: ${result.stderr}`);
274
+ assert.ok(result.stdout.includes('VALID'), `Expected VALID in output: ${result.stdout}`);
275
+ });
276
+
277
+ test('validate-skill: valid skill prints metadata summary', () => {
278
+ const result = spawnSync('node', [CLI_PATH, 'validate-skill', 'git-workflow-skill'], {
279
+ encoding: 'utf8', cwd: PROJECT_ROOT
280
+ });
281
+ assert.ok(result.stdout.includes('Version'), 'Expected Version in output');
282
+ assert.ok(result.stdout.includes('Category') || result.stdout.includes('Name'), 'Expected metadata fields');
283
+ });
284
+
285
+ // Non-existent skill
286
+ test('validate-skill: unknown skill exits 1 with error', () => {
287
+ const result = spawnSync('node', [CLI_PATH, 'validate-skill', 'no-such-skill'], {
288
+ encoding: 'utf8', cwd: PROJECT_ROOT
289
+ });
290
+ assert.strictEqual(result.status, 1, 'Expected exit 1 for missing skill');
291
+ const output = result.stdout + result.stderr;
292
+ assert.ok(output.includes('not found') || output.includes('Error'), 'Expected not-found error');
293
+ });
294
+
295
+ // Task 6.2: Broken skill — create deliberately invalid skill, validate, cleanup
296
+ test('validate-skill: INVALID skill exits 1', () => {
297
+ const brokenSkill = 'broken-test-skill';
298
+ const brokenDir = path.join(PROJECT_ROOT, 'skills', brokenSkill);
299
+ try {
300
+ fs.mkdirSync(brokenDir, { recursive: true });
301
+ // Deliberately invalid: missing SKILL.md, bad skill.json
302
+ fs.writeFileSync(path.join(brokenDir, 'skill.json'), JSON.stringify({
303
+ name: '',
304
+ version: 'bad-version',
305
+ category: 'invalid-cat'
306
+ }), 'utf8');
307
+
308
+ const result = spawnSync('node', [CLI_PATH, 'validate-skill', brokenSkill], {
309
+ encoding: 'utf8', cwd: PROJECT_ROOT
310
+ });
311
+ assert.strictEqual(result.status, 1, `Expected exit 1. stdout: ${result.stdout}`);
312
+ assert.ok(result.stdout.includes('INVALID'), `Expected INVALID in output: ${result.stdout}`);
313
+ } finally {
314
+ fs.rmSync(brokenDir, { recursive: true, force: true });
315
+ }
316
+ });
317
+
318
+ test('validate-skill: INVALID output lists individual field errors', () => {
319
+ const brokenSkill = 'broken-test-skill2';
320
+ const brokenDir = path.join(PROJECT_ROOT, 'skills', brokenSkill);
321
+ try {
322
+ fs.mkdirSync(brokenDir, { recursive: true });
323
+ fs.writeFileSync(path.join(brokenDir, 'skill.json'), JSON.stringify({
324
+ name: '',
325
+ version: 'bad',
326
+ category: 'invalid-cat'
327
+ }), 'utf8');
328
+
329
+ const result = spawnSync('node', [CLI_PATH, 'validate-skill', brokenSkill], {
330
+ encoding: 'utf8', cwd: PROJECT_ROOT
331
+ });
332
+ assert.ok(result.stdout.includes('name'), `Expected name error in output: ${result.stdout}`);
333
+ assert.ok(result.stdout.includes('version'), `Expected version error in output: ${result.stdout}`);
334
+ assert.ok(result.stdout.includes('category'), `Expected category error in output: ${result.stdout}`);
335
+ } finally {
336
+ fs.rmSync(brokenDir, { recursive: true, force: true });
337
+ }
338
+ });
339
+
340
+ // Task 6.3: Unknown agent template warning
341
+ test('validate-skill: warns about unknown agent template', () => {
342
+ const warnSkill = 'warn-template-skill';
343
+ const warnDir = path.join(PROJECT_ROOT, 'skills', warnSkill);
344
+ try {
345
+ fs.mkdirSync(path.join(warnDir, 'templates'), { recursive: true });
346
+ fs.writeFileSync(path.join(warnDir, 'skill.json'), JSON.stringify({
347
+ name: 'Warn Skill', description: 'test', version: '1.0.0'
348
+ }), 'utf8');
349
+ fs.writeFileSync(path.join(warnDir, 'SKILL.md'), '# Warn Skill\n', 'utf8');
350
+ fs.writeFileSync(path.join(warnDir, 'templates', 'unknown-agent.md'), '# Template\n', 'utf8');
351
+
352
+ const result = spawnSync('node', [CLI_PATH, 'validate-skill', warnSkill], {
353
+ encoding: 'utf8', cwd: PROJECT_ROOT
354
+ });
355
+ assert.strictEqual(result.status, 0, `Expected exit 0 (warnings don't fail). stderr: ${result.stderr}`);
356
+ assert.ok(
357
+ result.stdout.includes('unknown-agent') || result.stdout.includes('Warning'),
358
+ `Expected warning about unknown-agent: ${result.stdout}`
359
+ );
360
+ } finally {
361
+ fs.rmSync(warnDir, { recursive: true, force: true });
362
+ }
363
+ });
364
+
365
+ // No skill name provided
366
+ test('validate-skill: no skill name exits 1 with usage hint', () => {
367
+ const result = spawnSync('node', [CLI_PATH, 'validate-skill'], {
368
+ encoding: 'utf8', cwd: PROJECT_ROOT
369
+ });
370
+ assert.strictEqual(result.status, 1, 'Expected exit 1 for missing skill name');
371
+ });
372
+
373
+ // ─── Results ──────────────────────────────────────────────────────────────────
374
+ console.log(`\n ${passed} passed, ${failed} failed\n`);
375
+ if (failed > 0) {
376
+ errors.forEach(e => console.error(` ✗ ${e.name}: ${e.error}`));
377
+ process.exit(1);
378
+ }
@@ -175,7 +175,7 @@ test('--yes exits 0 with piped stdin (no interactive prompts)', () => {
175
175
  // Note: this performs actual skill installation in the current project directory.
176
176
  // It verifies that the process exits without hanging on interactive prompts.
177
177
  const result = spawnSync(process.execPath, [CLI_PATH, 'install', '--yes'], {
178
- timeout: 60000,
178
+ timeout: 300000, // 5 min — BMAD module installation copies hundreds of files
179
179
  encoding: 'utf-8',
180
180
  input: '',
181
181
  stdio: ['pipe', 'pipe', 'pipe'],