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,732 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const chalk = require('chalk');
6
+ const { getAllAgents } = require('./agents');
7
+
8
+ const KEBAB_CASE_PATTERN = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
9
+
10
+ /**
11
+ * Check that targetPath is within baseDir (prevents path traversal via path.join).
12
+ * @param {string} targetPath Constructed path to validate
13
+ * @param {string} baseDir Trusted base directory
14
+ * @returns {boolean}
15
+ */
16
+ function isWithinDir(targetPath, baseDir) {
17
+ const resolved = path.resolve(targetPath);
18
+ const base = path.resolve(baseDir);
19
+ return resolved === base || resolved.startsWith(base + path.sep);
20
+ }
21
+
22
+ /**
23
+ * Escape a user-supplied string for safe embedding in a YAML double-quoted scalar.
24
+ * @param {string} s
25
+ * @returns {string} Double-quoted, safely escaped YAML scalar
26
+ */
27
+ function yamlStr(s) {
28
+ return '"' + String(s || '').replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n') + '"';
29
+ }
30
+
31
+ /**
32
+ * Validate that a skill name is kebab-case.
33
+ * @param {string} name
34
+ * @returns {boolean}
35
+ */
36
+ function validateSkillName(name) {
37
+ return KEBAB_CASE_PATTERN.test(name);
38
+ }
39
+
40
+ /**
41
+ * Convert kebab-case string to Title Case.
42
+ * e.g. 'my-new-skill' -> 'My New Skill'
43
+ * @param {string} kebab
44
+ * @returns {string}
45
+ */
46
+ function kebabToTitleCase(kebab) {
47
+ return kebab.split('-').map(w => w.length > 0 ? w[0].toUpperCase() + w.slice(1) : '').join(' ');
48
+ }
49
+
50
+ /**
51
+ * Create a new skill directory with required files.
52
+ * @param {string} skillName kebab-case skill name
53
+ * @param {string} [skillsDir] base skills directory (defaults to <cwd>/skills)
54
+ * @returns {{ success: boolean, error?: string, hint?: string }}
55
+ */
56
+ function createSkill(skillName, skillsDir) {
57
+ const baseDir = skillsDir || path.join(process.cwd(), 'skills');
58
+ const skillDir = path.join(baseDir, skillName);
59
+
60
+ // Prevent path traversal (path.join normalizes '..' segments)
61
+ if (!isWithinDir(skillDir, baseDir)) {
62
+ return { success: false, error: `Invalid skill name '${skillName}'`, hint: 'Skill names must be lowercase kebab-case' };
63
+ }
64
+
65
+ // Duplicate detection
66
+ if (fs.existsSync(skillDir)) {
67
+ return {
68
+ success: false,
69
+ error: `Skill '${skillName}' already exists at ${path.join('skills', skillName)}`,
70
+ hint: `Choose a different name or remove the existing skill first`
71
+ };
72
+ }
73
+
74
+ // Create all directories and files with rollback on failure
75
+ try {
76
+ fs.mkdirSync(skillDir, { recursive: true });
77
+ fs.mkdirSync(path.join(skillDir, 'templates'), { recursive: true });
78
+ fs.mkdirSync(path.join(skillDir, 'references'), { recursive: true });
79
+ fs.mkdirSync(path.join(skillDir, 'assets'), { recursive: true });
80
+
81
+ const titleName = kebabToTitleCase(skillName);
82
+
83
+ // Generate skill.json
84
+ const skillJson = {
85
+ name: titleName,
86
+ description: '',
87
+ version: '1.0.0',
88
+ category: 'workflow',
89
+ author: 'ma-agents',
90
+ tags: [],
91
+ always_load: false
92
+ };
93
+ fs.writeFileSync(
94
+ path.join(skillDir, 'skill.json'),
95
+ JSON.stringify(skillJson, null, 2) + '\n',
96
+ 'utf8'
97
+ );
98
+
99
+ // Generate SKILL.md
100
+ const skillMd = `# ${titleName}
101
+
102
+ <Brief description of what this skill does and when it applies.>
103
+
104
+ ## Policies
105
+
106
+ ### 1. <First Policy>
107
+
108
+ **Rule:** <What the agent must do or not do>
109
+
110
+ **Action:**
111
+ - <Specific instruction>
112
+ - <Specific instruction>
113
+
114
+ ### 2. <Second Policy>
115
+
116
+ **Rule:** <What the agent must do or not do>
117
+
118
+ **Action:**
119
+ - <Specific instruction>
120
+
121
+ ## References
122
+
123
+ - <Link to any supporting documents in references/ directory>
124
+ `;
125
+ fs.writeFileSync(path.join(skillDir, 'SKILL.md'), skillMd, 'utf8');
126
+
127
+ } catch (err) {
128
+ // Rollback: remove partially-created directory so the name is not permanently locked
129
+ try {
130
+ fs.rmSync(skillDir, { recursive: true, force: true });
131
+ } catch (_) {
132
+ // Best-effort cleanup; ignore secondary errors
133
+ }
134
+ return {
135
+ success: false,
136
+ error: `Failed to create skill '${skillName}': ${err.message}`,
137
+ hint: `Check directory permissions and try again`
138
+ };
139
+ }
140
+
141
+ return { success: true, skillDir, titleName: kebabToTitleCase(skillName) };
142
+ }
143
+
144
+ /**
145
+ * CLI handler for the create-skill command.
146
+ * @param {string[]} args CLI arguments after 'create-skill'
147
+ */
148
+ async function handleCreateSkill(args) {
149
+ const skillName = args[0];
150
+
151
+ if (!skillName) {
152
+ console.error(chalk.red('Error: Skill name is required'));
153
+ console.error(chalk.gray(' Hint: Usage: npx ma-agents create-skill <skill-name>'));
154
+ process.exit(1);
155
+ }
156
+
157
+ // Validate name
158
+ if (!validateSkillName(skillName)) {
159
+ console.error(chalk.red('Error: Skill name must be kebab-case (lowercase letters, numbers, hyphens)'));
160
+ console.error(chalk.gray(' Hint: Example: my-new-skill, python-linting, api-security'));
161
+ process.exit(1);
162
+ }
163
+
164
+ // Attempt creation
165
+ const result = createSkill(skillName);
166
+
167
+ if (!result.success) {
168
+ console.error(chalk.red(`Error: ${result.error}`));
169
+ console.error(chalk.gray(` Hint: ${result.hint}`));
170
+ process.exit(1);
171
+ }
172
+
173
+ const displayPath = path.join('skills', skillName);
174
+ console.log(chalk.green(`Skill '${skillName}' created at ${displayPath}`));
175
+ console.log(chalk.gray(` Next: Edit skill.json and SKILL.md to add your skill content`));
176
+ console.log(chalk.gray(` Next: Register in .claude/skills/MANIFEST.yaml to enable auto-loading`));
177
+ }
178
+
179
+ const VALID_CATEGORIES = ['security', 'architecture', 'testing', 'documentation', 'workflow', 'language'];
180
+ const SEMVER_PATTERN = /^\d+\.\d+\.\d+$/;
181
+
182
+ /**
183
+ * Validate an existing skill directory against the schema.
184
+ * @param {string} skillName skill directory name
185
+ * @param {string} [skillsDir] base skills directory (defaults to <cwd>/skills)
186
+ * @returns {{ valid: boolean, notFound?: boolean, errors: string[], warnings: string[], metadata?: object }}
187
+ */
188
+ function validateSkill(skillName, skillsDir) {
189
+ const baseDir = skillsDir || path.join(process.cwd(), 'skills');
190
+ const skillDir = path.join(baseDir, skillName);
191
+ const errors = [];
192
+ const warnings = [];
193
+
194
+ // Prevent path traversal
195
+ if (!isWithinDir(skillDir, baseDir)) {
196
+ return { valid: false, notFound: true, errors: [`Invalid skill name '${skillName}'`], warnings };
197
+ }
198
+
199
+ // Check skill directory exists
200
+ if (!fs.existsSync(skillDir)) {
201
+ return { valid: false, notFound: true, errors: [`Skill '${skillName}' not found`], warnings };
202
+ }
203
+
204
+ // Task 2: File existence checks
205
+ const hasSkillJson = fs.existsSync(path.join(skillDir, 'skill.json'));
206
+ const hasSkillMd = fs.existsSync(path.join(skillDir, 'SKILL.md'));
207
+
208
+ if (!hasSkillJson) errors.push('missing required file skill.json');
209
+ if (!hasSkillMd) errors.push('missing required file SKILL.md');
210
+
211
+ // Task 3: skill.json field validation
212
+ let metadata = null;
213
+ if (hasSkillJson) {
214
+ let parsed;
215
+ try {
216
+ parsed = JSON.parse(fs.readFileSync(path.join(skillDir, 'skill.json'), 'utf8'));
217
+ } catch (_) {
218
+ errors.push('skill.json is not valid JSON');
219
+ }
220
+
221
+ if (parsed) {
222
+ metadata = parsed;
223
+
224
+ // Required fields
225
+ if (typeof parsed.name !== 'string' || parsed.name.trim() === '') {
226
+ errors.push('name: required, must be a non-empty string');
227
+ }
228
+ if (typeof parsed.version !== 'string' || !SEMVER_PATTERN.test(parsed.version)) {
229
+ errors.push('version: must match semver format (e.g., 1.0.0)');
230
+ }
231
+ if (!Object.prototype.hasOwnProperty.call(parsed, 'description') || typeof parsed.description !== 'string') {
232
+ errors.push('description: required, must be a string');
233
+ }
234
+
235
+ // Optional fields — validate only if present
236
+ if (parsed.category !== undefined && !VALID_CATEGORIES.includes(parsed.category)) {
237
+ errors.push(`category: must be one of: ${VALID_CATEGORIES.join(', ')}`);
238
+ }
239
+ if (parsed.tags !== undefined && !Array.isArray(parsed.tags)) {
240
+ errors.push('tags: must be an array of strings');
241
+ }
242
+ if (parsed.always_load !== undefined && typeof parsed.always_load !== 'boolean') {
243
+ errors.push('always_load: must be a boolean');
244
+ }
245
+ if (parsed.author !== undefined && (typeof parsed.author !== 'string' || parsed.author.trim() === '')) {
246
+ errors.push('author: must be a non-empty string if present');
247
+ }
248
+ if (parsed.applies_when !== undefined && !Array.isArray(parsed.applies_when)) {
249
+ errors.push('applies_when: must be an array of strings');
250
+ }
251
+ }
252
+ }
253
+
254
+ // Task 4: Template validation
255
+ const templatesDir = path.join(skillDir, 'templates');
256
+ if (fs.existsSync(templatesDir)) {
257
+ const knownAgentIds = new Set(getAllAgents().map(a => a.id));
258
+ const files = fs.readdirSync(templatesDir).filter(f =>
259
+ f.endsWith('.md') && fs.statSync(path.join(templatesDir, f)).isFile()
260
+ );
261
+ for (const file of files) {
262
+ const agentId = path.basename(file, '.md');
263
+ if (knownAgentIds.has(agentId)) {
264
+ // valid — no action needed (metadata shown in output)
265
+ } else {
266
+ warnings.push(`Template '${file}' targets unknown agent '${agentId}'`);
267
+ }
268
+ }
269
+ }
270
+
271
+ return { valid: errors.length === 0, errors, warnings, metadata };
272
+ }
273
+
274
+ /**
275
+ * CLI handler for the validate-skill command.
276
+ * @param {string[]} args CLI arguments after 'validate-skill'
277
+ */
278
+ async function handleValidateSkill(args) {
279
+ const skillName = args[0];
280
+
281
+ if (!skillName) {
282
+ console.error(chalk.red('Error: Skill name is required'));
283
+ console.error(chalk.gray(' Hint: Usage: npx ma-agents validate-skill <skill-name>'));
284
+ process.exit(1);
285
+ }
286
+
287
+ const result = validateSkill(skillName);
288
+
289
+ if (result.notFound) {
290
+ console.error(chalk.red(`Error: Skill '${skillName}' not found`));
291
+ console.error(chalk.gray(` Hint: Run "list" to see available skills`));
292
+ process.exit(1);
293
+ }
294
+
295
+ if (result.valid) {
296
+ const m = result.metadata || {};
297
+ console.log(chalk.bold.green(`VALID: ${skillName}`));
298
+ if (m.name) console.log(` Name: ${m.name}`);
299
+ if (m.version) console.log(` Version: ${m.version}`);
300
+ if (m.category) console.log(` Category: ${m.category}`);
301
+ if (Array.isArray(m.tags)) console.log(` Tags: ${m.tags.length} tag${m.tags.length !== 1 ? 's' : ''}`);
302
+ if (m.always_load !== undefined) console.log(` Always Load: ${m.always_load}`);
303
+ } else {
304
+ console.log(chalk.bold.red(`INVALID: ${skillName}`));
305
+ console.log(` Errors:`);
306
+ result.errors.forEach(e => console.log(` - ${e}`));
307
+ }
308
+
309
+ if (result.warnings && result.warnings.length > 0) {
310
+ if (result.valid) console.log(` Warnings:`);
311
+ result.warnings.forEach(w => console.log(chalk.yellow(` - ${w}`)));
312
+ }
313
+
314
+ process.exit(result.valid ? 0 : 1);
315
+ }
316
+
317
+ /**
318
+ * Toggle the always_load flag on a skill's skill.json.
319
+ * @param {string} skillName kebab-case skill name
320
+ * @param {boolean} enable true = mandatory, false = optional
321
+ * @param {string} [skillsDir] base skills directory (defaults to <cwd>/skills)
322
+ * @returns {{ success: boolean, error?: string, hint?: string }}
323
+ */
324
+ function setMandatory(skillName, enable, skillsDir) {
325
+ const baseDir = skillsDir || path.join(process.cwd(), 'skills');
326
+ const skillDir = path.join(baseDir, skillName);
327
+ const skillJsonPath = path.join(skillDir, 'skill.json');
328
+
329
+ // Prevent path traversal
330
+ if (!isWithinDir(skillDir, baseDir)) {
331
+ return { success: false, error: `Invalid skill name '${skillName}'`, hint: 'Skill names must be lowercase kebab-case' };
332
+ }
333
+
334
+ if (!fs.existsSync(skillJsonPath)) {
335
+ return {
336
+ success: false,
337
+ error: `Skill '${skillName}' not found`,
338
+ hint: `Run "list" to see available skills`
339
+ };
340
+ }
341
+
342
+ let data;
343
+ try {
344
+ data = JSON.parse(fs.readFileSync(skillJsonPath, 'utf8'));
345
+ } catch (_) {
346
+ return {
347
+ success: false,
348
+ error: `skill.json for '${skillName}' is not valid JSON`,
349
+ hint: `Fix skill.json before updating always_load`
350
+ };
351
+ }
352
+
353
+ data.always_load = enable;
354
+
355
+ try {
356
+ fs.writeFileSync(skillJsonPath, JSON.stringify(data, null, 2) + '\n', 'utf8');
357
+ } catch (err) {
358
+ return {
359
+ success: false,
360
+ error: `Failed to write skill.json: ${err.message}`,
361
+ hint: `Check file permissions and try again`
362
+ };
363
+ }
364
+
365
+ return { success: true, enable };
366
+ }
367
+
368
+ /**
369
+ * CLI handler for the set-mandatory command.
370
+ * @param {string[]} args CLI arguments after 'set-mandatory'
371
+ */
372
+ async function handleSetMandatory(args) {
373
+ const { generateSkillsManifest } = require('./installer');
374
+
375
+ const skillName = args[0];
376
+
377
+ if (!skillName) {
378
+ console.error(chalk.red('Error: Skill name is required'));
379
+ console.error(chalk.gray(' Hint: Usage: npx ma-agents set-mandatory <skill-name> [--off]'));
380
+ process.exit(1);
381
+ }
382
+
383
+ const enable = !args.includes('--off');
384
+ const result = setMandatory(skillName, enable);
385
+
386
+ if (!result.success) {
387
+ console.error(chalk.red(`Error: ${result.error}`));
388
+ console.error(chalk.gray(` Hint: ${result.hint}`));
389
+ process.exit(1);
390
+ }
391
+
392
+ if (enable) {
393
+ console.log(chalk.green(`Skill '${skillName}' is now mandatory (always_load: true)`));
394
+ } else {
395
+ console.log(chalk.yellow(`Skill '${skillName}' is no longer mandatory (always_load: false)`));
396
+ }
397
+
398
+ // Regenerate MANIFEST.yaml to reflect the change (best-effort — no-op if no install found)
399
+ try {
400
+ await generateSkillsManifest(process.cwd());
401
+ } catch (_) {
402
+ // Not fatal — MANIFEST.yaml will be updated on next install
403
+ }
404
+ }
405
+
406
+ // ─── Story 3.4: BMAD Persona Customization Tooling ───────────────────────────
407
+
408
+ const CUSTOM_AGENTS = ['bmm-sre', 'bmm-devops', 'bmm-cyber', 'bmm-mil498'];
409
+ const BUILTIN_AGENTS = ['bmm-pm', 'bmm-architect', 'bmm-dev', 'bmm-qa', 'bmm-sm', 'bmm-tech-writer', 'bmm-ux-designer'];
410
+
411
+ const MANDATORY_CRITICAL_ACTIONS = {
412
+ 1: 'Read the skills MANIFEST at skills/MANIFEST.yaml (relative to project root)',
413
+ 2: 'For each skill marked always_load: true, read the skill file completely',
414
+ 3: 'Follow all skill directives during this session'
415
+ };
416
+
417
+ /**
418
+ * Validate a BMAD agent name.
419
+ * @param {string} agentName
420
+ * @returns {{ valid: boolean, type?: 'custom'|'builtin', hint?: string }}
421
+ */
422
+ function validateBmadAgent(agentName) {
423
+ if (!agentName) {
424
+ const all = [...CUSTOM_AGENTS, ...BUILTIN_AGENTS].join(', ');
425
+ return { valid: false, hint: `Agent name is required. Available: ${all}` };
426
+ }
427
+ if (CUSTOM_AGENTS.includes(agentName)) {
428
+ return { valid: true, type: 'custom' };
429
+ }
430
+ if (BUILTIN_AGENTS.includes(agentName)) {
431
+ return { valid: true, type: 'builtin' };
432
+ }
433
+ const all = [...CUSTOM_AGENTS, ...BUILTIN_AGENTS].join(', ');
434
+ return { valid: false, hint: `Unknown agent '${agentName}'. Available: ${all}` };
435
+ }
436
+
437
+ /**
438
+ * Generate YAML content for a customize file.
439
+ * @param {string} agentName
440
+ * @param {object} data Optional: { persona, menu, extraActions }
441
+ * @param {'custom'|'builtin'} agentType
442
+ * @returns {string}
443
+ */
444
+ function generateCustomizeYaml(agentName, data, agentType) {
445
+ const lines = [];
446
+ lines.push(`# Customization file for ${agentName}`);
447
+ lines.push('# Generated by ma-agents customize-agent');
448
+ lines.push('');
449
+
450
+ if (agentType === 'custom') {
451
+ // Persona section (optional)
452
+ if (data.persona) {
453
+ lines.push('persona:');
454
+ lines.push(` role: ${yamlStr(data.persona.role)}`);
455
+ lines.push(` identity: ${yamlStr(data.persona.identity)}`);
456
+ lines.push(` communication_style: ${yamlStr(data.persona.communication_style)}`);
457
+ if (Array.isArray(data.persona.principles) && data.persona.principles.length > 0) {
458
+ lines.push(' principles:');
459
+ data.persona.principles.forEach(p => lines.push(` - ${yamlStr(p)}`));
460
+ }
461
+ lines.push('');
462
+ }
463
+
464
+ // Menu section (optional)
465
+ if (Array.isArray(data.menu) && data.menu.length > 0) {
466
+ lines.push('menu:');
467
+ data.menu.forEach(item => {
468
+ lines.push(` - trigger: ${yamlStr(item.trigger)}`);
469
+ lines.push(` workflow: ${yamlStr(item.workflow)}`);
470
+ lines.push(` description: ${yamlStr(item.description)}`);
471
+ });
472
+ lines.push('');
473
+ }
474
+ }
475
+
476
+ // Critical actions — mandatory 1-3 are immutable
477
+ lines.push('critical_actions:');
478
+ lines.push(` 1: "${MANDATORY_CRITICAL_ACTIONS[1]}"`);
479
+ lines.push(` 2: "${MANDATORY_CRITICAL_ACTIONS[2]}"`);
480
+ lines.push(` 3: "${MANDATORY_CRITICAL_ACTIONS[3]}"`);
481
+
482
+ // Extra actions appended starting at 4 (custom agents only)
483
+ if (agentType === 'custom' && data.extraActions) {
484
+ const keys = Object.keys(data.extraActions).map(Number).sort((a, b) => a - b);
485
+ for (const k of keys) {
486
+ if (k >= 4) {
487
+ lines.push(` ${k}: ${yamlStr(data.extraActions[k])}`);
488
+ }
489
+ }
490
+ }
491
+
492
+ lines.push('');
493
+ return lines.join('\n');
494
+ }
495
+
496
+ /**
497
+ * Write a customize YAML file to the agents directory.
498
+ * @param {string} agentName
499
+ * @param {string} yamlContent
500
+ * @param {string} agentsDir
501
+ */
502
+ function writeCustomizeFile(agentName, yamlContent, agentsDir) {
503
+ fs.mkdirSync(agentsDir, { recursive: true });
504
+ const filePath = path.join(agentsDir, `${agentName}.customize.yaml`);
505
+ fs.writeFileSync(filePath, yamlContent, 'utf8');
506
+ }
507
+
508
+ /**
509
+ * Load existing customization file content, or null if not found.
510
+ * @param {string} agentName
511
+ * @param {string} agentsDir
512
+ * @returns {string|null}
513
+ */
514
+ function loadExistingCustomization(agentName, agentsDir) {
515
+ const filePath = path.join(agentsDir, `${agentName}.customize.yaml`);
516
+ if (!fs.existsSync(filePath)) return null;
517
+ return fs.readFileSync(filePath, 'utf8');
518
+ }
519
+
520
+ /**
521
+ * CLI handler for the customize-agent command.
522
+ * @param {string[]} args CLI arguments after 'customize-agent'
523
+ */
524
+ async function handleCustomizeAgent(args) {
525
+ const agentName = args[0];
526
+ const yesFlag = args.includes('--yes');
527
+
528
+ if (!agentName) {
529
+ console.error(chalk.red('Error: Agent name is required'));
530
+ console.error(chalk.gray(' Hint: Usage: npx ma-agents customize-agent <agent-name>'));
531
+ process.exit(1);
532
+ }
533
+
534
+ const validation = validateBmadAgent(agentName);
535
+ if (!validation.valid) {
536
+ console.error(chalk.red(`Error: Unknown agent '${agentName}'`));
537
+ console.error(chalk.gray(` Hint: ${validation.hint}`));
538
+ process.exit(1);
539
+ }
540
+
541
+ const agentsDir = path.join(process.cwd(), 'lib', 'bmad-extension', 'agents');
542
+
543
+ if (yesFlag) {
544
+ const yaml = generateCustomizeYaml(agentName, {}, validation.type);
545
+ writeCustomizeFile(agentName, yaml, agentsDir);
546
+ console.log(chalk.green(`Customization file generated for ${agentName} (non-interactive mode)`));
547
+ return;
548
+ }
549
+
550
+ // Interactive: generate file with defaults
551
+ const yaml = generateCustomizeYaml(agentName, {}, validation.type);
552
+ writeCustomizeFile(agentName, yaml, agentsDir);
553
+ console.log(chalk.green(`Customization file generated for ${agentName}`));
554
+ }
555
+
556
+ // ─── Story 3.5: Specialized Agent Development Tooling ────────────────────────
557
+
558
+ const BMM_AGENT_NAME_PATTERN = /^bmm-[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
559
+ const ALL_KNOWN_AGENTS = new Set([...CUSTOM_AGENTS, ...BUILTIN_AGENTS]);
560
+
561
+ /**
562
+ * Validate and normalize a BMAD agent name for creation.
563
+ * Auto-prepends 'bmm-' if missing. Checks for conflicts.
564
+ * @param {string} name Raw agent name from user
565
+ * @returns {{ valid: boolean, name?: string, normalized?: boolean, error?: string, hint?: string }}
566
+ */
567
+ function validateAgentName(name) {
568
+ if (!name) {
569
+ return { valid: false, error: 'Agent name is required', hint: 'Usage: npx ma-agents create-agent <agent-name>' };
570
+ }
571
+
572
+ // Auto-prepend bmm- if missing
573
+ let normalized = false;
574
+ let agentName = name;
575
+ if (!agentName.startsWith('bmm-')) {
576
+ agentName = `bmm-${agentName}`;
577
+ normalized = true;
578
+ }
579
+
580
+ // Validate pattern
581
+ if (!BMM_AGENT_NAME_PATTERN.test(agentName)) {
582
+ return {
583
+ valid: false,
584
+ error: `Agent name '${agentName}' is not valid`,
585
+ hint: 'Use kebab-case with bmm- prefix, e.g. bmm-my-role'
586
+ };
587
+ }
588
+
589
+ // Check against known agents (builtin + custom)
590
+ if (ALL_KNOWN_AGENTS.has(agentName)) {
591
+ return {
592
+ valid: false,
593
+ error: `Agent '${agentName}' already exists`,
594
+ hint: 'Use customize-agent to modify it, or choose a different name'
595
+ };
596
+ }
597
+
598
+ return { valid: true, name: agentName, normalized };
599
+ }
600
+
601
+ /**
602
+ * Generate a template-filled YAML for a new custom agent.
603
+ * @param {string} agentName
604
+ * @returns {string}
605
+ */
606
+ function generateAgentTemplate(agentName) {
607
+ const lines = [];
608
+ lines.push(`# Customization file for ${agentName}`);
609
+ lines.push('# Generated by ma-agents create-agent');
610
+ lines.push('# Edit this file to define your agent persona, menu, and actions');
611
+ lines.push('');
612
+ lines.push('persona:');
613
+ lines.push(` role: "<Role title for ${agentName}>"`);
614
+ lines.push(' identity: "<Expert description of this agent\'s expertise and focus.>"');
615
+ lines.push(' communication_style: "<How this agent communicates — tone, style, priorities.>"');
616
+ lines.push(' principles:');
617
+ lines.push(' - "<Core principle 1>"');
618
+ lines.push(' - "<Core principle 2>"');
619
+ lines.push('');
620
+ lines.push('menu:');
621
+ lines.push(' - trigger: <command-trigger>');
622
+ lines.push(' workflow: workflows/<workflow-name>/workflow.md');
623
+ lines.push(' description: "<Description of this workflow>"');
624
+ lines.push('');
625
+ lines.push('critical_actions:');
626
+ lines.push(` 1: "${MANDATORY_CRITICAL_ACTIONS[1]}"`);
627
+ lines.push(` 2: "${MANDATORY_CRITICAL_ACTIONS[2]}"`);
628
+ lines.push(` 3: "${MANDATORY_CRITICAL_ACTIONS[3]}"`);
629
+ lines.push('');
630
+ return lines.join('\n');
631
+ }
632
+
633
+ /**
634
+ * Create a new specialized BMAD agent customize file.
635
+ * @param {string} agentName Validated (normalized) agent name
636
+ * @param {string} [agentsDir] Target directory (defaults to <cwd>/lib/bmad-extension/agents)
637
+ * @returns {{ success: boolean, filePath?: string, error?: string, hint?: string }}
638
+ */
639
+ function createBmadAgent(agentName, agentsDir) {
640
+ const baseDir = agentsDir || path.join(process.cwd(), 'lib', 'bmad-extension', 'agents');
641
+ const filePath = path.join(baseDir, `${agentName}.customize.yaml`);
642
+
643
+ // Prevent path traversal
644
+ if (!isWithinDir(filePath, baseDir)) {
645
+ return { success: false, error: `Invalid agent name '${agentName}'`, hint: 'Agent names must use bmm- prefix and lowercase kebab-case' };
646
+ }
647
+
648
+ if (fs.existsSync(filePath)) {
649
+ return {
650
+ success: false,
651
+ error: `Agent '${agentName}' already exists at ${filePath}`,
652
+ hint: 'Use customize-agent to modify it, or choose a different name'
653
+ };
654
+ }
655
+
656
+ try {
657
+ fs.mkdirSync(baseDir, { recursive: true });
658
+ const yaml = generateAgentTemplate(agentName);
659
+ fs.writeFileSync(filePath, yaml, 'utf8');
660
+ } catch (err) {
661
+ return {
662
+ success: false,
663
+ error: `Failed to create agent '${agentName}': ${err.message}`,
664
+ hint: 'Check directory permissions and try again'
665
+ };
666
+ }
667
+
668
+ return { success: true, filePath };
669
+ }
670
+
671
+ /**
672
+ * CLI handler for the create-agent command.
673
+ * @param {string[]} args CLI arguments after 'create-agent'
674
+ */
675
+ async function handleCreateAgent(args) {
676
+ const rawName = args[0];
677
+ const yesFlag = args.includes('--yes');
678
+
679
+ if (!rawName) {
680
+ console.error(chalk.red('Error: Agent name is required'));
681
+ console.error(chalk.gray(' Hint: Usage: npx ma-agents create-agent <agent-name>'));
682
+ process.exit(1);
683
+ }
684
+
685
+ const validation = validateAgentName(rawName);
686
+ if (!validation.valid) {
687
+ console.error(chalk.red(`Error: ${validation.error}`));
688
+ console.error(chalk.gray(` Hint: ${validation.hint}`));
689
+ process.exit(1);
690
+ }
691
+
692
+ const agentName = validation.name;
693
+
694
+ if (validation.normalized) {
695
+ console.log(chalk.gray(` Agent name normalized to '${agentName}'`));
696
+ }
697
+
698
+ const agentsDir = path.join(process.cwd(), 'lib', 'bmad-extension', 'agents');
699
+
700
+ // Check for file conflict (beyond known agent lists)
701
+ const filePath = path.join(agentsDir, `${agentName}.customize.yaml`);
702
+ if (fs.existsSync(filePath)) {
703
+ console.error(chalk.red(`Error: Agent '${agentName}' already exists`));
704
+ console.error(chalk.gray(' Hint: Use customize-agent to modify it, or choose a different name'));
705
+ process.exit(1);
706
+ }
707
+
708
+ const result = createBmadAgent(agentName, agentsDir);
709
+
710
+ if (!result.success) {
711
+ console.error(chalk.red(`Error: ${result.error}`));
712
+ console.error(chalk.gray(` Hint: ${result.hint}`));
713
+ process.exit(1);
714
+ }
715
+
716
+ const displayPath = path.join('lib', 'bmad-extension', 'agents', `${agentName}.customize.yaml`);
717
+ console.log(chalk.green(`Agent '${agentName}' created at ${displayPath}`));
718
+ console.log(chalk.gray(' Next steps:'));
719
+ console.log(chalk.gray(' 1. Review and refine the persona in the .customize.yaml file'));
720
+ console.log(chalk.gray(' 2. Run \'npx ma-agents install\' to deploy the extension module'));
721
+ console.log(chalk.gray(' 3. Run \'npx bmad-method install --action recompile\' to activate the agent'));
722
+ console.log(chalk.gray(' 4. The agent will appear in the BMAD agent menu'));
723
+ }
724
+
725
+ module.exports = {
726
+ validateSkillName, createSkill, handleCreateSkill,
727
+ validateSkill, handleValidateSkill,
728
+ setMandatory, handleSetMandatory,
729
+ validateBmadAgent, generateCustomizeYaml, writeCustomizeFile, loadExistingCustomization, handleCustomizeAgent,
730
+ validateAgentName, createBmadAgent, handleCreateAgent,
731
+ kebabToTitleCase
732
+ };