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.
- package/.opencode/skills/.ma-agents.json +241 -0
- package/.opencode/skills/MANIFEST.yaml +254 -0
- package/.opencode/skills/ai-audit-trail/SKILL.md +23 -0
- package/.opencode/skills/auto-bug-detection/SKILL.md +169 -0
- package/.opencode/skills/cmake-best-practices/SKILL.md +64 -0
- package/.opencode/skills/cmake-best-practices/examples/cmake.md +59 -0
- package/.opencode/skills/code-documentation/SKILL.md +57 -0
- package/.opencode/skills/code-documentation/examples/cpp.md +29 -0
- package/.opencode/skills/code-documentation/examples/csharp.md +28 -0
- package/.opencode/skills/code-documentation/examples/javascript_typescript.md +28 -0
- package/.opencode/skills/code-documentation/examples/python.md +57 -0
- package/.opencode/skills/code-review/SKILL.md +43 -0
- package/.opencode/skills/commit-message/SKILL.md +79 -0
- package/.opencode/skills/cpp-best-practices/SKILL.md +234 -0
- package/.opencode/skills/cpp-best-practices/examples/modern-idioms.md +189 -0
- package/.opencode/skills/cpp-best-practices/examples/naming-and-organization.md +102 -0
- package/.opencode/skills/cpp-concurrency-safety/SKILL.md +60 -0
- package/.opencode/skills/cpp-concurrency-safety/examples/concurrency.md +73 -0
- package/.opencode/skills/cpp-const-correctness/SKILL.md +63 -0
- package/.opencode/skills/cpp-const-correctness/examples/const_correctness.md +54 -0
- package/.opencode/skills/cpp-memory-handling/SKILL.md +42 -0
- package/.opencode/skills/cpp-memory-handling/examples/modern-cpp.md +49 -0
- package/.opencode/skills/cpp-memory-handling/examples/smart-pointers.md +46 -0
- package/.opencode/skills/cpp-modern-composition/SKILL.md +64 -0
- package/.opencode/skills/cpp-modern-composition/examples/composition.md +51 -0
- package/.opencode/skills/cpp-robust-interfaces/SKILL.md +55 -0
- package/.opencode/skills/cpp-robust-interfaces/examples/interfaces.md +56 -0
- package/.opencode/skills/create-hardened-docker-skill/SKILL.md +637 -0
- package/.opencode/skills/create-hardened-docker-skill/scripts/create-all.sh +489 -0
- package/.opencode/skills/csharp-best-practices/SKILL.md +278 -0
- package/.opencode/skills/docker-hardening-verification/SKILL.md +28 -0
- package/.opencode/skills/docker-hardening-verification/scripts/verify-hardening.sh +39 -0
- package/.opencode/skills/docker-image-signing/SKILL.md +28 -0
- package/.opencode/skills/docker-image-signing/scripts/sign-image.sh +33 -0
- package/.opencode/skills/document-revision-history/SKILL.md +104 -0
- package/.opencode/skills/git-workflow-skill/SKILL.md +194 -0
- package/.opencode/skills/git-workflow-skill/hooks/commit-msg +61 -0
- package/.opencode/skills/git-workflow-skill/hooks/pre-commit +38 -0
- package/.opencode/skills/git-workflow-skill/hooks/prepare-commit-msg +56 -0
- package/.opencode/skills/git-workflow-skill/scripts/finish-feature.sh +192 -0
- package/.opencode/skills/git-workflow-skill/scripts/install-hooks.sh +55 -0
- package/.opencode/skills/git-workflow-skill/scripts/start-feature.sh +110 -0
- package/.opencode/skills/git-workflow-skill/scripts/validate-workflow.sh +229 -0
- package/.opencode/skills/js-ts-dependency-mgmt/SKILL.md +49 -0
- package/.opencode/skills/js-ts-dependency-mgmt/examples/dependency_mgmt.md +60 -0
- package/.opencode/skills/js-ts-security-skill/SKILL.md +64 -0
- package/.opencode/skills/js-ts-security-skill/scripts/verify-security.sh +136 -0
- package/.opencode/skills/logging-best-practices/SKILL.md +50 -0
- package/.opencode/skills/logging-best-practices/examples/cpp.md +36 -0
- package/.opencode/skills/logging-best-practices/examples/csharp.md +49 -0
- package/.opencode/skills/logging-best-practices/examples/javascript.md +77 -0
- package/.opencode/skills/logging-best-practices/examples/python.md +57 -0
- package/.opencode/skills/logging-best-practices/references/logging-standards.md +29 -0
- package/.opencode/skills/open-presentation/SKILL.md +35 -0
- package/.opencode/skills/opentelemetry-best-practices/SKILL.md +34 -0
- package/.opencode/skills/opentelemetry-best-practices/examples/go.md +32 -0
- package/.opencode/skills/opentelemetry-best-practices/examples/javascript.md +58 -0
- package/.opencode/skills/opentelemetry-best-practices/examples/python.md +37 -0
- package/.opencode/skills/opentelemetry-best-practices/references/otel-standards.md +37 -0
- package/.opencode/skills/python-best-practices/SKILL.md +385 -0
- package/.opencode/skills/python-dependency-mgmt/SKILL.md +42 -0
- package/.opencode/skills/python-dependency-mgmt/examples/dependency_mgmt.md +67 -0
- package/.opencode/skills/python-security-skill/SKILL.md +56 -0
- package/.opencode/skills/python-security-skill/examples/security.md +56 -0
- package/.opencode/skills/self-signed-cert/SKILL.md +42 -0
- package/.opencode/skills/self-signed-cert/scripts/generate-cert.ps1 +45 -0
- package/.opencode/skills/self-signed-cert/scripts/generate-cert.sh +43 -0
- package/.opencode/skills/skill-creator/SKILL.md +196 -0
- package/.opencode/skills/skill-creator/references/output-patterns.md +82 -0
- package/.opencode/skills/skill-creator/references/workflows.md +28 -0
- package/.opencode/skills/skill-creator/scripts/init_skill.py +208 -0
- package/.opencode/skills/skill-creator/scripts/package_skill.py +99 -0
- package/.opencode/skills/skill-creator/scripts/quick_validate.py +113 -0
- package/.opencode/skills/story-status-lookup/SKILL.md +78 -0
- package/.opencode/skills/test-accompanied-development/SKILL.md +50 -0
- package/.opencode/skills/test-generator/SKILL.md +65 -0
- package/.opencode/skills/vercel-react-best-practices/SKILL.md +109 -0
- package/.opencode/skills/verify-hardened-docker-skill/SKILL.md +442 -0
- package/.opencode/skills/verify-hardened-docker-skill/scripts/verify-docker-hardening.sh +439 -0
- package/AiAudit.md +5 -0
- package/QUICK_START.md +11 -5
- package/README.md +52 -1
- package/bin/cli.js +31 -4
- package/docs/BMAD_AI_Development_Training.pptx +0 -0
- package/docs/technical-notes/context-persistence-research.md +434 -0
- package/docs/technical-notes/enforcement-hooks-research.md +415 -0
- package/lib/agents.js +34 -0
- package/lib/bmad-extension/agents/bmm-architect.customize.yaml +5 -0
- package/lib/bmad-extension/agents/bmm-bmad-master.customize.yaml +5 -0
- package/lib/bmad-extension/agents/bmm-cyber.customize.yaml +30 -0
- package/lib/bmad-extension/agents/bmm-dev.customize.yaml +5 -0
- package/lib/bmad-extension/agents/bmm-devops.customize.yaml +30 -0
- package/lib/bmad-extension/agents/bmm-mil498.customize.yaml +42 -0
- package/lib/bmad-extension/agents/bmm-pm.customize.yaml +5 -0
- package/lib/bmad-extension/agents/bmm-qa.customize.yaml +5 -0
- package/lib/bmad-extension/agents/bmm-sm.customize.yaml +5 -0
- package/lib/bmad-extension/agents/bmm-sre.customize.yaml +30 -0
- package/lib/bmad-extension/agents/bmm-tech-writer.customize.yaml +5 -0
- package/lib/bmad-extension/agents/bmm-ux-designer.customize.yaml +5 -0
- package/lib/bmad-extension/module-help.csv +7 -0
- package/lib/bmad-extension/module.yaml +3 -0
- package/lib/bmad-extension/workflows/add-sprint/workflow.md +112 -0
- package/lib/bmad-extension/workflows/add-to-sprint/workflow.md +206 -0
- package/lib/bmad-extension/workflows/create-bug-story/workflow.md +186 -0
- package/lib/bmad-extension/workflows/modify-sprint/workflow.md +250 -0
- package/lib/bmad-extension/workflows/project-context-expansion/workflow.md +229 -0
- package/lib/bmad-extension/workflows/sprint-status-view/workflow.md +193 -0
- package/lib/bmad.js +168 -36
- package/lib/hooks/claude-code/verify-manifest.js +56 -0
- package/lib/installer.js +282 -1
- package/lib/methodology/BMAD_AI_Development_Training.pptx +0 -0
- package/lib/methodology/version.json +7 -0
- package/lib/skill-authoring.js +732 -0
- package/lib/templates/project-context.template.md +47 -0
- package/opencode.json +8 -0
- package/package.json +2 -2
- package/skills/auto-bug-detection/SKILL.md +165 -0
- package/skills/auto-bug-detection/skill.json +8 -0
- package/skills/code-review/SKILL.md +40 -0
- package/skills/cpp-best-practices/SKILL.md +230 -0
- package/skills/cpp-best-practices/examples/modern-idioms.md +189 -0
- package/skills/cpp-best-practices/examples/naming-and-organization.md +102 -0
- package/skills/cpp-best-practices/skill.json +25 -0
- package/skills/csharp-best-practices/SKILL.md +274 -0
- package/skills/csharp-best-practices/skill.json +23 -0
- package/skills/git-workflow-skill/skill.json +1 -1
- package/skills/open-presentation/SKILL.md +31 -0
- package/skills/open-presentation/skill.json +11 -0
- package/skills/python-best-practices/SKILL.md +381 -0
- package/skills/python-best-practices/skill.json +26 -0
- package/skills/story-status-lookup/SKILL.md +74 -0
- package/skills/story-status-lookup/skill.json +8 -0
- package/test/agent-injection-strategy.test.js +13 -7
- package/test/bmad-extension.test.js +237 -0
- package/test/bmad-output-policy.test.js +119 -0
- package/test/build-bmad-args.test.js +361 -0
- package/test/create-agent.test.js +232 -0
- package/test/enforcement-hooks.test.js +324 -0
- package/test/generate-project-context.test.js +337 -0
- package/test/integration-verification.test.js +402 -0
- package/test/opencode-agent.test.js +150 -0
- package/test/opencode-json-error.test.js +260 -0
- package/test/opencode-json-injection.test.js +256 -0
- package/test/opencode-json-merge.test.js +299 -0
- package/test/skill-authoring.test.js +272 -0
- package/test/skill-customize-agent.test.js +253 -0
- package/test/skill-mandatory.test.js +235 -0
- package/test/skill-validation.test.js +378 -0
- 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
|
+
};
|