ma-agents 2.20.2 → 2.21.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 +84 -6
- 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/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
package/lib/bmad.js
CHANGED
|
@@ -40,6 +40,7 @@ async function installBmad(modules = ['bmm', 'bmb'], tools = [], projectRoot = p
|
|
|
40
40
|
console.log(chalk.gray(` Running: ${command}`));
|
|
41
41
|
try {
|
|
42
42
|
execSync(command, { stdio: 'inherit', cwd: projectRoot, env: { ...process.env, GIT_TERMINAL_PROMPT: '0' } });
|
|
43
|
+
await deployMethodology(projectRoot, force);
|
|
43
44
|
return true;
|
|
44
45
|
} catch (error) {
|
|
45
46
|
console.error(chalk.red(` BMAD installation failed: ${error.message}`));
|
|
@@ -65,6 +66,7 @@ async function updateBmad(modules = ['bmm', 'bmb'], tools = [], projectRoot = pr
|
|
|
65
66
|
console.log(chalk.gray(` Running: ${command}`));
|
|
66
67
|
try {
|
|
67
68
|
execSync(command, { stdio: 'inherit', cwd: projectRoot, env: { ...process.env, GIT_TERMINAL_PROMPT: '0' } });
|
|
69
|
+
await deployMethodology(projectRoot, force);
|
|
68
70
|
return true;
|
|
69
71
|
} catch (error) {
|
|
70
72
|
console.error(chalk.red(` BMAD update failed: ${error.message}`));
|
|
@@ -122,7 +124,7 @@ async function applyCustomizations(projectRoot = process.cwd(), modules = ['bmm'
|
|
|
122
124
|
}
|
|
123
125
|
}
|
|
124
126
|
|
|
125
|
-
// Recompile agents to apply YAML customizations
|
|
127
|
+
// STAGE:RECOMPILE — Recompile agents to apply YAML customizations
|
|
126
128
|
// IMPORTANT: Steps 3-5 must run AFTER recompile because the recompile regenerates
|
|
127
129
|
// the _bmad/bmm/ tree and would wipe any files placed there beforehand.
|
|
128
130
|
let command = getBmadCommand(`install --yes --directory "${projectRoot}"`);
|
|
@@ -143,21 +145,37 @@ async function applyCustomizations(projectRoot = process.cwd(), modules = ['bmm'
|
|
|
143
145
|
console.error(chalk.red(` BMAD recompile failed: ${error.message}`));
|
|
144
146
|
}
|
|
145
147
|
|
|
146
|
-
//
|
|
148
|
+
// STAGE:EXTENSION — Deploy BMAD extension module (AFTER recompile — extensions/ survives recompile)
|
|
149
|
+
// NOTE: Stage 1 deploys persona/menu customize.yaml to _bmad/_config/agents/ (consumed
|
|
150
|
+
// by BMAD's agent compilation). This stage deploys the extension module to
|
|
151
|
+
// _bmad/extensions/ma-agents-skills/ which adds critical_actions for skill loading.
|
|
152
|
+
// These are complementary paths: _config/agents/ drives agent recompilation (persona/menu),
|
|
153
|
+
// while extensions/ adds runtime behavior (critical_actions). BMAD does NOT merge them —
|
|
154
|
+
// they are consumed by different mechanisms. The 4 custom agents have persona/menu in both
|
|
155
|
+
// locations; this is intentional for backward compatibility during transition (Story 8.3).
|
|
156
|
+
const extensionSource = path.join(__dirname, 'bmad-extension');
|
|
157
|
+
const extensionTarget = path.join(projectRoot, BMAD_DIR, 'extensions', 'ma-agents-skills');
|
|
158
|
+
if (fs.existsSync(extensionSource)) {
|
|
159
|
+
await fs.ensureDir(extensionTarget);
|
|
160
|
+
await fs.copy(extensionSource, extensionTarget);
|
|
161
|
+
console.log(chalk.cyan(' + Deployed BMAD extension module: ma-agents-skills'));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// STAGE:WORKFLOWS — Apply workflows (AFTER recompile so they persist)
|
|
147
165
|
if (fs.existsSync(workflowSourceDir)) {
|
|
148
166
|
await fs.ensureDir(workflowTargetDir);
|
|
149
167
|
await fs.copy(workflowSourceDir, workflowTargetDir);
|
|
150
168
|
console.log(chalk.cyan(` + Applied BMAD workflows`));
|
|
151
169
|
}
|
|
152
170
|
|
|
153
|
-
//
|
|
171
|
+
// 5. Apply MIL-STD-498 templates (AFTER recompile so they persist)
|
|
154
172
|
if (fs.existsSync(templateSourceDir)) {
|
|
155
173
|
await fs.ensureDir(templateTargetDir);
|
|
156
174
|
await fs.copy(templateSourceDir, templateTargetDir);
|
|
157
175
|
console.log(chalk.cyan(` + Applied MIL-STD-498 templates`));
|
|
158
176
|
}
|
|
159
177
|
|
|
160
|
-
//
|
|
178
|
+
// 6. Copy agent templates to compiled location (AFTER recompile)
|
|
161
179
|
// BMAD recompile doesn't generate custom agents (mil498, cyber) in _bmad/bmm/agents/.
|
|
162
180
|
// Without this, updateAgentInstructions() creates the file with only the MA-AGENTS block.
|
|
163
181
|
if (fs.existsSync(sourceDir)) {
|
|
@@ -175,7 +193,7 @@ async function applyCustomizations(projectRoot = process.cwd(), modules = ['bmm'
|
|
|
175
193
|
}
|
|
176
194
|
}
|
|
177
195
|
|
|
178
|
-
//
|
|
196
|
+
// 7. Register custom workflows in BMAD CSV registries (after recompile)
|
|
179
197
|
if (selectedAgentIds.length === 0 || selectedAgentIds.includes('bmm-mil498')) {
|
|
180
198
|
await registerMilWorkflows(projectRoot);
|
|
181
199
|
}
|
|
@@ -260,6 +278,65 @@ async function registerMilWorkflows(projectRoot) {
|
|
|
260
278
|
}
|
|
261
279
|
}
|
|
262
280
|
|
|
281
|
+
/**
|
|
282
|
+
* Deploy the methodology presentation to the target project's _bmad-output/methodology/ directory.
|
|
283
|
+
* Version-aware: skips if target is same or newer; overwrites only with --force.
|
|
284
|
+
* AC: #1, #2, #3, #5 (Story 6.1)
|
|
285
|
+
*/
|
|
286
|
+
async function deployMethodology(projectRoot = process.cwd(), force = false) {
|
|
287
|
+
const methodologySource = path.join(__dirname, 'methodology');
|
|
288
|
+
if (!(await fs.pathExists(methodologySource))) {
|
|
289
|
+
console.log(chalk.gray(' No methodology content found — skipping'));
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const methodologyTarget = path.join(projectRoot, '_bmad-output', 'methodology');
|
|
294
|
+
await fs.ensureDir(methodologyTarget);
|
|
295
|
+
|
|
296
|
+
// Version comparison
|
|
297
|
+
const sourceVersionFile = path.join(methodologySource, 'version.json');
|
|
298
|
+
const targetVersionFile = path.join(methodologyTarget, 'version.json');
|
|
299
|
+
|
|
300
|
+
let sourceVersion = '0.0.0';
|
|
301
|
+
let targetVersion = null;
|
|
302
|
+
|
|
303
|
+
if (await fs.pathExists(sourceVersionFile)) {
|
|
304
|
+
try {
|
|
305
|
+
const v = JSON.parse(await fs.readFile(sourceVersionFile, 'utf-8'));
|
|
306
|
+
sourceVersion = v.version || '0.0.0';
|
|
307
|
+
} catch { /* ignore */ }
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (await fs.pathExists(targetVersionFile)) {
|
|
311
|
+
try {
|
|
312
|
+
const v = JSON.parse(await fs.readFile(targetVersionFile, 'utf-8'));
|
|
313
|
+
targetVersion = v.version || null;
|
|
314
|
+
} catch { /* ignore */ }
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (targetVersion !== null && !force) {
|
|
318
|
+
// Simple semver comparison: skip if target is same or newer
|
|
319
|
+
const semverGte = (a, b) => {
|
|
320
|
+
const pa = a.split('.').map(Number);
|
|
321
|
+
const pb = b.split('.').map(Number);
|
|
322
|
+
for (let i = 0; i < 3; i++) {
|
|
323
|
+
if ((pa[i] || 0) > (pb[i] || 0)) return true;
|
|
324
|
+
if ((pa[i] || 0) < (pb[i] || 0)) return false;
|
|
325
|
+
}
|
|
326
|
+
return true;
|
|
327
|
+
};
|
|
328
|
+
if (semverGte(targetVersion, sourceVersion)) {
|
|
329
|
+
console.log(chalk.gray(` ✓ Methodology presentation is up to date (v${targetVersion})`));
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
await fs.copy(methodologySource, methodologyTarget, { overwrite: true });
|
|
335
|
+
console.log(chalk.green(' ✓ Methodology presentation installed to _bmad-output/methodology/'));
|
|
336
|
+
console.log(chalk.gray(' Hint: Share this with your team for BMAD-METHOD onboarding'));
|
|
337
|
+
console.log(chalk.gray(' Tip: Use /open-presentation to open the slides (install via npx ma-agents install open-presentation)'));
|
|
338
|
+
}
|
|
339
|
+
|
|
263
340
|
async function prePopulateBmadCache(force = false) {
|
|
264
341
|
const cacheSource = path.join(__dirname, 'bmad-cache');
|
|
265
342
|
if (!(await fs.pathExists(cacheSource))) {
|
|
@@ -345,5 +422,6 @@ module.exports = {
|
|
|
345
422
|
installBmad,
|
|
346
423
|
updateBmad,
|
|
347
424
|
applyCustomizations,
|
|
348
|
-
prePopulateBmadCache
|
|
425
|
+
prePopulateBmadCache,
|
|
426
|
+
deployMethodology,
|
|
349
427
|
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Claude Code SessionStart Hook — MANIFEST.yaml Skill Enforcement
|
|
5
|
+
*
|
|
6
|
+
* Injects a context reminder at session startup to reinforce skill loading
|
|
7
|
+
* from the project's MANIFEST.yaml. Runs as a standalone Node.js script
|
|
8
|
+
* with no external dependencies.
|
|
9
|
+
*
|
|
10
|
+
* Hook event: SessionStart (matcher: "startup")
|
|
11
|
+
* Exit 0 + stdout text → injected into Claude's session context
|
|
12
|
+
*
|
|
13
|
+
* Installed by: ma-agents installer (lib/installer.js)
|
|
14
|
+
* Removed by: ma-agents uninstaller when all skills are removed
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
'use strict';
|
|
18
|
+
|
|
19
|
+
const fs = require('fs');
|
|
20
|
+
const path = require('path');
|
|
21
|
+
|
|
22
|
+
// Read hook input from stdin (JSON)
|
|
23
|
+
let input = '';
|
|
24
|
+
process.stdin.setEncoding('utf8');
|
|
25
|
+
process.stdin.on('data', (chunk) => { input += chunk; });
|
|
26
|
+
process.stdin.on('end', () => {
|
|
27
|
+
try {
|
|
28
|
+
const data = JSON.parse(input);
|
|
29
|
+
const projectDir = data.cwd || process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
30
|
+
|
|
31
|
+
// Look for skills MANIFEST.yaml in the project
|
|
32
|
+
const manifestPath = path.join(projectDir, '.claude', 'skills', 'MANIFEST.yaml');
|
|
33
|
+
|
|
34
|
+
if (!fs.existsSync(manifestPath)) {
|
|
35
|
+
// No manifest — this project doesn't use ma-agents skills. Exit silently.
|
|
36
|
+
process.exit(0);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// MANIFEST exists — inject context reminder into session
|
|
40
|
+
const reminder = [
|
|
41
|
+
'SKILL ENFORCEMENT ACTIVE: This project uses ma-agents skills.',
|
|
42
|
+
`Read the skill manifest at ${manifestPath} before starting any task.`,
|
|
43
|
+
'Load all skills marked always_load: true, then select relevant skills for the current task.',
|
|
44
|
+
].join(' ');
|
|
45
|
+
|
|
46
|
+
process.stdout.write(reminder);
|
|
47
|
+
process.exit(0);
|
|
48
|
+
} catch (err) {
|
|
49
|
+
// On any error, allow session to proceed (don't block startup)
|
|
50
|
+
process.stderr.write(`verify-manifest hook warning: ${err.message}\n`);
|
|
51
|
+
process.exit(0);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Handle stdin close without data (defensive)
|
|
56
|
+
process.stdin.on('error', () => { process.exit(0); });
|
package/lib/installer.js
CHANGED
|
@@ -6,6 +6,12 @@ const { getAgent, getAllAgents } = require('./agents');
|
|
|
6
6
|
|
|
7
7
|
const MANIFEST_FILE = '.ma-agents.json';
|
|
8
8
|
const MANIFEST_VERSION = '1.1.0';
|
|
9
|
+
const MA_AGENTS_SOURCE = 'ma-agents';
|
|
10
|
+
const TEMPLATE_PATH = path.join(__dirname, 'templates', 'project-context.template.md');
|
|
11
|
+
|
|
12
|
+
// Claude Code hook configuration for MANIFEST verification
|
|
13
|
+
const CLAUDE_CODE_HOOK_ID = 'ma-agents-verify-manifest';
|
|
14
|
+
const CLAUDE_CODE_SETTINGS_FILE = '.claude/settings.json';
|
|
9
15
|
|
|
10
16
|
function getPackageVersion() {
|
|
11
17
|
const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf-8'));
|
|
@@ -31,6 +37,27 @@ function writeManifest(installPath, manifest) {
|
|
|
31
37
|
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n', 'utf-8');
|
|
32
38
|
}
|
|
33
39
|
|
|
40
|
+
const BMAD_OUTPUT_PATTERNS = ['_bmad-output', '_bmad-output/', '/_bmad-output', '/_bmad-output/'];
|
|
41
|
+
|
|
42
|
+
function ensureBmadOutputTracked(projectRoot) {
|
|
43
|
+
const gitignorePath = path.join(projectRoot, '.gitignore');
|
|
44
|
+
let content;
|
|
45
|
+
try {
|
|
46
|
+
content = fs.readFileSync(gitignorePath, 'utf-8');
|
|
47
|
+
} catch (err) {
|
|
48
|
+
if (err.code === 'ENOENT') return; // no .gitignore — nothing to do
|
|
49
|
+
throw err;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const lines = content.split(/\r?\n/);
|
|
53
|
+
const filtered = lines.filter(line => !BMAD_OUTPUT_PATTERNS.includes(line.trim()));
|
|
54
|
+
|
|
55
|
+
if (filtered.length === lines.length) return; // nothing removed — do not write
|
|
56
|
+
|
|
57
|
+
fs.writeFileSync(gitignorePath, filtered.join('\n'), 'utf-8');
|
|
58
|
+
console.log(chalk.green('_bmad-output is now tracked as project knowledge (not gitignored)'));
|
|
59
|
+
}
|
|
60
|
+
|
|
34
61
|
function ensureManifest(installPath, agentId, scope) {
|
|
35
62
|
let manifest = readManifest(installPath);
|
|
36
63
|
if (!manifest) {
|
|
@@ -107,6 +134,72 @@ async function generateSkillsManifest(installPath) {
|
|
|
107
134
|
console.log(chalk.cyan(` + Generated ${manifestYamlPath}`));
|
|
108
135
|
}
|
|
109
136
|
|
|
137
|
+
/**
|
|
138
|
+
* Generate project-context.md content by stamping the template with installed agent MANIFEST paths.
|
|
139
|
+
* @param {string} projectRoot - Absolute path to project root (unused for path generation but part of API)
|
|
140
|
+
* @param {Array<{skillsDir: string}>} installedAgents - Agent objects with skillsDir property
|
|
141
|
+
* @returns {Promise<string>} Stamped template content string (does NOT write any file)
|
|
142
|
+
*/
|
|
143
|
+
async function generateProjectContext(projectRoot, installedAgents) {
|
|
144
|
+
let template;
|
|
145
|
+
try {
|
|
146
|
+
template = await fs.readFile(TEMPLATE_PATH, 'utf8');
|
|
147
|
+
} catch (err) {
|
|
148
|
+
throw new Error(`project-context template not found at ${TEMPLATE_PATH} — ma-agents installation may be corrupted: ${err.message}`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const validAgents = installedAgents ? installedAgents.filter(a => a && a.skillsDir) : [];
|
|
152
|
+
let manifestList;
|
|
153
|
+
if (validAgents.length === 0) {
|
|
154
|
+
manifestList = ' - (no agents installed — run ma-agents to install skills)';
|
|
155
|
+
} else {
|
|
156
|
+
manifestList = validAgents
|
|
157
|
+
.map(a => ` - \`${a.skillsDir}/MANIFEST.yaml\``)
|
|
158
|
+
.join('\n');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return template.replace('{{MANIFEST_PATHS_LIST}}', manifestList);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Update the MANIFEST paths section in an existing project-context.md.
|
|
166
|
+
* Locates content between <!-- ma-agents:manifest-paths-start --> and
|
|
167
|
+
* <!-- ma-agents:manifest-paths-end --> markers and replaces it with the
|
|
168
|
+
* current agent list. Returns true if the file was updated, false if
|
|
169
|
+
* markers were not found (old-format file — backward compatible) or if
|
|
170
|
+
* the content was already up to date.
|
|
171
|
+
* @param {string} outputPath - Absolute path to the existing project-context.md
|
|
172
|
+
* @param {Array<{skillsDir: string}>} installedAgents - Agent objects with skillsDir property
|
|
173
|
+
* @returns {Promise<boolean>} true if file was written, false otherwise
|
|
174
|
+
*/
|
|
175
|
+
async function updateProjectContextManifestPaths(outputPath, installedAgents) {
|
|
176
|
+
const content = await fs.readFile(outputPath, 'utf8');
|
|
177
|
+
const validAgents = installedAgents ? installedAgents.filter(a => a && a.skillsDir) : [];
|
|
178
|
+
const newList = validAgents.length === 0
|
|
179
|
+
? ' - (no agents installed — run ma-agents to install skills)'
|
|
180
|
+
: validAgents.map(a => ` - \`${a.skillsDir}/MANIFEST.yaml\``).join('\n');
|
|
181
|
+
|
|
182
|
+
const START = '<!-- ma-agents:manifest-paths-start -->';
|
|
183
|
+
const END = '<!-- ma-agents:manifest-paths-end -->';
|
|
184
|
+
const startIdx = content.indexOf(START);
|
|
185
|
+
const endIdx = content.indexOf(END);
|
|
186
|
+
|
|
187
|
+
if (startIdx === -1 || endIdx === -1 || endIdx <= startIdx) {
|
|
188
|
+
return false; // no markers — old-format file, skip silently (backward compatible)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const before = content.slice(0, startIdx + START.length);
|
|
192
|
+
const after = content.slice(endIdx);
|
|
193
|
+
const newContent = `${before}\n${newList}\n${after}`;
|
|
194
|
+
|
|
195
|
+
if (newContent === content) {
|
|
196
|
+
return false; // already up to date, no write needed
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
await fs.writeFile(outputPath, newContent, 'utf8');
|
|
200
|
+
return true;
|
|
201
|
+
}
|
|
202
|
+
|
|
110
203
|
/**
|
|
111
204
|
* Find the insertion point in content after all skipped headers.
|
|
112
205
|
* For '---' pattern, skips YAML frontmatter block (opening --- on first line through closing ---).
|
|
@@ -160,6 +253,48 @@ function findInsertionPoint(content, skipPatterns) {
|
|
|
160
253
|
async function updateAgentInstructions(agent, projectRoot) {
|
|
161
254
|
if (!agent.instructionFiles || agent.instructionFiles.length === 0) return;
|
|
162
255
|
|
|
256
|
+
// JSON merge strategy (e.g., OpenCode)
|
|
257
|
+
if (agent.injectionStrategy?.position === 'json-merge') {
|
|
258
|
+
const targetKey = agent.injectionStrategy.targetKey || 'instructions';
|
|
259
|
+
const filePath = path.join(projectRoot, agent.instructionFiles[0]);
|
|
260
|
+
const agentProjectPath = agent.getProjectPath();
|
|
261
|
+
const relManifestPath = path.relative(projectRoot, path.join(agentProjectPath, 'MANIFEST.yaml')).replace(/\\/g, '/');
|
|
262
|
+
const instructionText = `# AI Agent Skills - Planning Instruction\n\nYou have access to a library of skills in your skills directory. Before starting any task:\n\n1. Read the skill manifest at ${relManifestPath}\n2. Based on the task description, select which skills are relevant\n3. Read only the selected skill files\n4. Then proceed with the task\n\nAlways load skills marked with always_load: true.\nDo not load skills that are not relevant to the current task.`;
|
|
263
|
+
|
|
264
|
+
if (!fs.existsSync(filePath)) {
|
|
265
|
+
// File absent: create fresh (atomic write)
|
|
266
|
+
const data = { [targetKey]: [] };
|
|
267
|
+
data[targetKey].push({ _source: MA_AGENTS_SOURCE, text: instructionText });
|
|
268
|
+
const tmpPath = filePath + '.tmp';
|
|
269
|
+
await fs.outputFile(tmpPath, JSON.stringify(data, null, 2), 'utf-8');
|
|
270
|
+
await fs.rename(tmpPath, filePath);
|
|
271
|
+
console.log(chalk.cyan(` + Created ${agent.instructionFiles[0]}`));
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
// File present: read and merge
|
|
275
|
+
try {
|
|
276
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
277
|
+
const data = JSON.parse(content);
|
|
278
|
+
if (!Array.isArray(data[targetKey])) {
|
|
279
|
+
data[targetKey] = [];
|
|
280
|
+
}
|
|
281
|
+
// Filter out stale ma-agents entries, keep user entries (null-safe)
|
|
282
|
+
const userEntries = data[targetKey].filter(entry => entry != null && entry._source !== MA_AGENTS_SOURCE);
|
|
283
|
+
// Append fresh ma-agents entries
|
|
284
|
+
const freshEntries = [{ _source: MA_AGENTS_SOURCE, text: instructionText }];
|
|
285
|
+
data[targetKey] = [...userEntries, ...freshEntries];
|
|
286
|
+
// Atomic write: temp file then rename
|
|
287
|
+
const tmpPath = filePath + '.tmp';
|
|
288
|
+
await fs.outputFile(tmpPath, JSON.stringify(data, null, 2), 'utf-8');
|
|
289
|
+
await fs.rename(tmpPath, filePath);
|
|
290
|
+
console.log(chalk.cyan(` + Updated ${agent.instructionFiles[0]}`));
|
|
291
|
+
} catch (err) {
|
|
292
|
+
console.error(`[${MA_AGENTS_SOURCE}] ERROR: Could not parse ${filePath} — ${err.message}. File not modified.`);
|
|
293
|
+
return; // non-fatal: do NOT re-throw
|
|
294
|
+
}
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
|
|
163
298
|
const agentProjectPath = agent.getProjectPath();
|
|
164
299
|
const relManifestPath = path.relative(projectRoot, path.join(agentProjectPath, 'MANIFEST.yaml')).replace(/\\/g, '/');
|
|
165
300
|
|
|
@@ -362,6 +497,11 @@ async function installSkill(skillId, agentIds, customPath = '', scope = 'project
|
|
|
362
497
|
pathGroups.get(installPath).push({ agentId, agent });
|
|
363
498
|
}
|
|
364
499
|
|
|
500
|
+
// Guard: project-context generation runs at most once per installSkill() call.
|
|
501
|
+
// pathGroups can have multiple entries when agents share paths; without this flag
|
|
502
|
+
// the generation would fire once per path group, producing redundant file I/O.
|
|
503
|
+
let projectContextHandled = false;
|
|
504
|
+
|
|
365
505
|
for (const [installPath, agentEntries] of pathGroups) {
|
|
366
506
|
const primaryAgent = agentEntries[0].agent;
|
|
367
507
|
const agentNames = agentEntries.map(e => e.agent.name).join(', ');
|
|
@@ -516,6 +656,7 @@ async function installSkill(skillId, agentIds, customPath = '', scope = 'project
|
|
|
516
656
|
agentVersion: primaryAgent.version
|
|
517
657
|
};
|
|
518
658
|
writeManifest(installPath, manifest);
|
|
659
|
+
ensureBmadOutputTracked(installPath);
|
|
519
660
|
|
|
520
661
|
// Generate MANIFEST.yaml and update instruction files for ALL agents
|
|
521
662
|
await generateSkillsManifest(installPath);
|
|
@@ -523,6 +664,36 @@ async function installSkill(skillId, agentIds, customPath = '', scope = 'project
|
|
|
523
664
|
for (const entry of agentEntries) {
|
|
524
665
|
await updateAgentInstructions(entry.agent, process.cwd());
|
|
525
666
|
}
|
|
667
|
+
// Deploy Claude Code hook when skills are installed for claude-code
|
|
668
|
+
if (includesClaudeCode(agentEntries)) {
|
|
669
|
+
await deployClaudeCodeHook(process.cwd());
|
|
670
|
+
}
|
|
671
|
+
// Generate project-context.md on first install; update manifest paths on subsequent installs.
|
|
672
|
+
// Guard: runs at most once per installSkill() call — pathGroups may have multiple entries
|
|
673
|
+
// when agents share install paths, so without this flag generation would fire redundantly.
|
|
674
|
+
if (!projectContextHandled) {
|
|
675
|
+
projectContextHandled = true;
|
|
676
|
+
const projectRoot = process.cwd();
|
|
677
|
+
const outputPath = path.join(projectRoot, '_bmad-output', 'project-context.md');
|
|
678
|
+
const allAgents = getManifestAgents(manifest).map(id => getAgent(id)).filter(Boolean);
|
|
679
|
+
try {
|
|
680
|
+
await fs.ensureDir(path.join(projectRoot, '_bmad-output'));
|
|
681
|
+
if (await fs.pathExists(outputPath)) {
|
|
682
|
+
const updated = await updateProjectContextManifestPaths(outputPath, allAgents);
|
|
683
|
+
if (updated) {
|
|
684
|
+
console.log(chalk.green('✓ project-context.md manifest paths updated'));
|
|
685
|
+
} else {
|
|
686
|
+
console.log(chalk.blue('ℹ project-context.md already up to date'));
|
|
687
|
+
}
|
|
688
|
+
} else {
|
|
689
|
+
const content = await generateProjectContext(projectRoot, allAgents);
|
|
690
|
+
await fs.writeFile(outputPath, content, 'utf8');
|
|
691
|
+
console.log(chalk.green('✓ project-context.md generated at _bmad-output/project-context.md'));
|
|
692
|
+
}
|
|
693
|
+
} catch (err) {
|
|
694
|
+
console.log(chalk.yellow(`⚠ project-context generation failed: ${err.message} — continuing install`));
|
|
695
|
+
}
|
|
696
|
+
}
|
|
526
697
|
}
|
|
527
698
|
}
|
|
528
699
|
} catch (error) {
|
|
@@ -602,6 +773,14 @@ async function uninstallSkill(skillId, agentIds, customPath = '', scope = 'proje
|
|
|
602
773
|
for (const entry of agentEntries) {
|
|
603
774
|
await updateAgentInstructions(entry.agent, process.cwd());
|
|
604
775
|
}
|
|
776
|
+
// Remove Claude Code hook when no skills remain for claude-code
|
|
777
|
+
if (includesClaudeCode(agentEntries)) {
|
|
778
|
+
const currentManifest = readManifest(installPath);
|
|
779
|
+
const hasRemainingSkills = currentManifest && currentManifest.skills && Object.keys(currentManifest.skills).length > 0;
|
|
780
|
+
if (!hasRemainingSkills) {
|
|
781
|
+
await removeClaudeCodeHook(process.cwd());
|
|
782
|
+
}
|
|
783
|
+
}
|
|
605
784
|
}
|
|
606
785
|
} catch (error) {
|
|
607
786
|
console.log(chalk.red(` x Failed: ${error.message}`));
|
|
@@ -655,6 +834,101 @@ function getStatus(agentIds, customPath = '', scope = 'project') {
|
|
|
655
834
|
return results;
|
|
656
835
|
}
|
|
657
836
|
|
|
837
|
+
// --- Claude Code Hook Management ---
|
|
838
|
+
|
|
839
|
+
/**
|
|
840
|
+
* Deploy the verify-manifest SessionStart hook into .claude/settings.json.
|
|
841
|
+
* Performs a JSON merge — preserves existing settings and hooks.
|
|
842
|
+
*/
|
|
843
|
+
async function deployClaudeCodeHook(projectRoot) {
|
|
844
|
+
const settingsPath = path.join(projectRoot, CLAUDE_CODE_SETTINGS_FILE);
|
|
845
|
+
let settings = {};
|
|
846
|
+
|
|
847
|
+
if (fs.existsSync(settingsPath)) {
|
|
848
|
+
try {
|
|
849
|
+
settings = JSON.parse(await fs.readFile(settingsPath, 'utf-8'));
|
|
850
|
+
} catch {
|
|
851
|
+
console.log(chalk.yellow(' Warning: Could not parse .claude/settings.json, skipping hook deployment'));
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
if (!settings.hooks) {
|
|
857
|
+
settings.hooks = {};
|
|
858
|
+
}
|
|
859
|
+
if (!settings.hooks.SessionStart) {
|
|
860
|
+
settings.hooks.SessionStart = [];
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
// Check if our hook is already present
|
|
864
|
+
const hookCommand = `node "$CLAUDE_PROJECT_DIR/lib/hooks/claude-code/verify-manifest.js"`;
|
|
865
|
+
const alreadyInstalled = settings.hooks.SessionStart.some(group =>
|
|
866
|
+
group.hooks && group.hooks.some(h => h.command === hookCommand)
|
|
867
|
+
);
|
|
868
|
+
|
|
869
|
+
if (alreadyInstalled) {
|
|
870
|
+
return; // Already deployed
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
settings.hooks.SessionStart.push({
|
|
874
|
+
matcher: 'startup',
|
|
875
|
+
hooks: [
|
|
876
|
+
{
|
|
877
|
+
type: 'command',
|
|
878
|
+
command: hookCommand,
|
|
879
|
+
_id: CLAUDE_CODE_HOOK_ID
|
|
880
|
+
}
|
|
881
|
+
]
|
|
882
|
+
});
|
|
883
|
+
|
|
884
|
+
await fs.ensureDir(path.dirname(settingsPath));
|
|
885
|
+
await fs.writeFile(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
|
|
886
|
+
console.log(chalk.cyan(' + Deployed Claude Code verify-manifest hook'));
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
/**
|
|
890
|
+
* Remove the verify-manifest hook from .claude/settings.json.
|
|
891
|
+
*/
|
|
892
|
+
async function removeClaudeCodeHook(projectRoot) {
|
|
893
|
+
const settingsPath = path.join(projectRoot, CLAUDE_CODE_SETTINGS_FILE);
|
|
894
|
+
|
|
895
|
+
if (!fs.existsSync(settingsPath)) return;
|
|
896
|
+
|
|
897
|
+
let settings;
|
|
898
|
+
try {
|
|
899
|
+
settings = JSON.parse(await fs.readFile(settingsPath, 'utf-8'));
|
|
900
|
+
} catch {
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
if (!settings.hooks || !settings.hooks.SessionStart) return;
|
|
905
|
+
|
|
906
|
+
const hookCommand = `node "$CLAUDE_PROJECT_DIR/lib/hooks/claude-code/verify-manifest.js"`;
|
|
907
|
+
settings.hooks.SessionStart = settings.hooks.SessionStart.filter(group => {
|
|
908
|
+
if (!group.hooks) return true;
|
|
909
|
+
group.hooks = group.hooks.filter(h => h.command !== hookCommand && h._id !== CLAUDE_CODE_HOOK_ID);
|
|
910
|
+
return group.hooks.length > 0;
|
|
911
|
+
});
|
|
912
|
+
|
|
913
|
+
// Clean up empty arrays
|
|
914
|
+
if (settings.hooks.SessionStart.length === 0) {
|
|
915
|
+
delete settings.hooks.SessionStart;
|
|
916
|
+
}
|
|
917
|
+
if (Object.keys(settings.hooks).length === 0) {
|
|
918
|
+
delete settings.hooks;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
await fs.writeFile(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
|
|
922
|
+
console.log(chalk.cyan(' - Removed Claude Code verify-manifest hook'));
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
/**
|
|
926
|
+
* Check if any agents in the list include claude-code.
|
|
927
|
+
*/
|
|
928
|
+
function includesClaudeCode(agentEntries) {
|
|
929
|
+
return agentEntries.some(e => e.agentId === 'claude-code');
|
|
930
|
+
}
|
|
931
|
+
|
|
658
932
|
module.exports = {
|
|
659
933
|
listSkills,
|
|
660
934
|
listAgents,
|
|
@@ -666,5 +940,12 @@ module.exports = {
|
|
|
666
940
|
getManifestAgents,
|
|
667
941
|
compareSemver,
|
|
668
942
|
findInsertionPoint,
|
|
669
|
-
|
|
943
|
+
deployClaudeCodeHook,
|
|
944
|
+
removeClaudeCodeHook,
|
|
945
|
+
ensureBmadOutputTracked,
|
|
946
|
+
generateSkillsManifest,
|
|
947
|
+
generateProjectContext,
|
|
948
|
+
_updateProjectContextManifestPaths: updateProjectContextManifestPaths,
|
|
949
|
+
_testUpdateAgentInstructions: updateAgentInstructions,
|
|
950
|
+
_MA_AGENTS_SOURCE: MA_AGENTS_SOURCE
|
|
670
951
|
};
|
|
Binary file
|