bmad-plus 0.21.0 → 0.23.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/CHANGELOG.md +39 -0
- package/README.md +24 -14
- package/SECURITY.md +63 -1
- package/osint-agent-package/skills/bmad-osint-investigate/osint/scripts/_http.py +68 -24
- package/package.json +4 -1
- package/readme-international/README.de.md +24 -14
- package/readme-international/README.es.md +24 -14
- package/readme-international/README.fr.md +24 -14
- package/src/bmad-plus/agents/agent-orchestrator/SKILL.md +5 -1
- package/src/bmad-plus/data/role-triggers.yaml +34 -0
- package/src/bmad-plus/module-help.csv +2 -0
- package/src/bmad-plus/module.yaml +21 -0
- package/src/bmad-plus/packs/pack-design/SKILL.md +72 -0
- package/src/bmad-plus/packs/pack-design/bmad-skill-manifest.yaml +13 -0
- package/src/bmad-plus/packs/pack-design/design-engine.md +126 -0
- package/src/bmad-plus/packs/pack-design/intake.yaml +105 -0
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/code-review.md +3 -1
- package/src/bmad-plus/packs/pack-seo/SKILL.md +3 -1
- package/src/bmad-plus/packs/pack-seo/ref/cwv-thresholds.md +2 -2
- package/src/bmad-plus/packs/pack-seo/requirements.txt +1 -1
- package/src/bmad-plus/packs/pack-seo/scripts/seo_apis.py +72 -30
- package/src/bmad-plus/packs/pack-seo/scripts/seo_report.py +5 -6
- package/src/bmad-plus/packs/pack-seo/scripts/seo_screenshot.py +176 -14
- package/src/bmad-plus/packs/pack-shield/README.md +12 -0
- package/src/bmad-plus/packs/pack-shield/SKILL.md +7 -1
- package/src/bmad-plus/packs/pack-shield/review-rules/access-control.md +10 -0
- package/src/bmad-plus/packs/pack-shield/review-rules/ai-integrations.md +10 -0
- package/src/bmad-plus/packs/pack-shield/review-rules/change-and-supply-chain.md +10 -0
- package/src/bmad-plus/packs/pack-shield/review-rules/cryptography.md +10 -0
- package/src/bmad-plus/packs/pack-shield/review-rules/index.yaml +134 -0
- package/src/bmad-plus/packs/pack-shield/review-rules/logging.md +10 -0
- package/src/bmad-plus/packs/pack-shield/review-rules/personal-data.md +10 -0
- package/src/bmad-plus/packs/pack-shield/shared/ai-processing-register-template.yaml +53 -0
- package/src/bmad-plus/packs/pack-shield/shared/ai-processing-register.md +32 -0
- package/src/bmad-plus/packs/pack-shield/shared/assurance-case-template.yaml +87 -0
- package/src/bmad-plus/packs/pack-shield/shared/assurance-case.md +50 -0
- package/src/bmad-plus/packs/pack-shield/shield-orchestrator.md +24 -1
- package/src/bmad-plus/skills/bmad-plus-autopilot/SKILL.md +7 -0
- package/src/bmad-plus/skills/bmad-plus-intake/SKILL.md +87 -0
- package/src/bmad-plus/skills/bmad-plus-intake/ref/intake-format.md +112 -0
- package/src/bmad-plus/skills/bmad-plus-parallel/SKILL.md +18 -0
- package/src/bmad-plus/skills/bmad-plus-swarm/SKILL.md +115 -0
- package/src/bmad-plus/skills/bmad-plus-swarm/ref/definition.md +188 -0
- package/src/bmad-plus/skills/bmad-plus-uat/SKILL.md +1 -0
- package/src/bmad-plus/skills/bmad-plus-uat/template/page.html +5 -4
- package/tools/build/adapters.config.js +2 -0
- package/tools/build/generate-adapters.js +32 -1
- package/tools/build/generate.js +283 -7
- package/tools/build/intake-contract.js +522 -0
- package/tools/cli/bmad-plus-cli.js +4 -0
- package/tools/cli/commands/ai-register.js +63 -0
- package/tools/cli/commands/assurance.js +162 -0
- package/tools/cli/commands/install.js +28 -1
- package/tools/cli/commands/intake.js +124 -0
- package/tools/cli/commands/review.js +10 -3
- package/tools/cli/commands/swarm.js +119 -0
- package/tools/cli/commands/uninstall.js +17 -0
- package/tools/cli/i18n.js +20 -0
- package/tools/cli/lib/ai-register.js +393 -0
- package/tools/cli/lib/assurance.js +822 -0
- package/tools/cli/lib/control-refs.js +132 -0
- package/tools/cli/lib/installation-health.js +17 -0
- package/tools/cli/lib/intake.js +202 -0
- package/tools/cli/lib/pack-copy.js +95 -5
- package/tools/cli/lib/packs.js +156 -8
- package/tools/cli/lib/page-origins.js +582 -0
- package/tools/cli/lib/review-rules.js +92 -24
- package/tools/cli/lib/review.js +28 -1
- package/tools/cli/lib/swarm-deal.js +116 -0
- package/tools/cli/lib/swarm-defaults.js +6 -0
- package/tools/cli/lib/swarm-tally.js +195 -0
- package/tools/cli/lib/swarm-validate.js +589 -0
- package/tools/cli/lib/swarm.js +712 -0
- package/tools/cli/lib/uat.js +22 -5
- package/tools/cli/lib/update-transaction.js +8 -4
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Security assurance cases: start one from the Shield template, run the checks that produce
|
|
3
|
+
* evidence, verify a case against them.
|
|
4
|
+
*/
|
|
5
|
+
'use strict';
|
|
6
|
+
|
|
7
|
+
const fs = require('node:fs');
|
|
8
|
+
const path = require('node:path');
|
|
9
|
+
const assurance = require('../lib/assurance');
|
|
10
|
+
|
|
11
|
+
const ACTIONS = ['init', 'run', 'verify'];
|
|
12
|
+
|
|
13
|
+
function print(json, payload, lines) {
|
|
14
|
+
if (json) console.log(JSON.stringify({ schemaVersion: 1, ...payload }, null, 2));
|
|
15
|
+
else for (const line of lines) console.log(line);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** `--emit-check` writes a JSON file atomically, never over the case or its ledger. */
|
|
19
|
+
function writeCheck(projectDir, value, kase, ledger, result) {
|
|
20
|
+
const file = path.resolve(projectDir, value);
|
|
21
|
+
if (!/\.json$/i.test(file)) throw new Error('--emit-check writes a .json file');
|
|
22
|
+
const protectedFiles = [path.resolve(projectDir, kase.file), ledger];
|
|
23
|
+
if (protectedFiles.some((taken) => path.relative(taken, file) === ''))
|
|
24
|
+
throw new Error('--emit-check must not overwrite the case or its ledger');
|
|
25
|
+
if (fs.existsSync(file) && !fs.statSync(file).isFile())
|
|
26
|
+
throw new Error(`--emit-check: ${file} is not a file`);
|
|
27
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
28
|
+
const temporary = `${file}.${process.pid}.tmp`;
|
|
29
|
+
fs.writeFileSync(temporary, `${JSON.stringify(result, null, 2)}\n`, { flag: 'wx' });
|
|
30
|
+
try {
|
|
31
|
+
fs.renameSync(temporary, file);
|
|
32
|
+
} catch (error) {
|
|
33
|
+
fs.rmSync(temporary, { force: true });
|
|
34
|
+
throw error;
|
|
35
|
+
}
|
|
36
|
+
return file;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function initAction(projectDir, caseFile, json) {
|
|
40
|
+
const { file, id } = assurance.initCase(projectDir, caseFile);
|
|
41
|
+
print(json, { action: 'init', case: id, file }, [
|
|
42
|
+
`${file}: case ${id} started from the Shield template.`,
|
|
43
|
+
'Replace every example claim and check with those of the project, commit it, then run it.',
|
|
44
|
+
]);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function runAction(projectDir, kase, options, json) {
|
|
48
|
+
const { file, results, head, authenticated } = await assurance.runChecks(projectDir, kase, {
|
|
49
|
+
dir: options.dir,
|
|
50
|
+
only: options.check,
|
|
51
|
+
});
|
|
52
|
+
const failed = results.filter((result) => result.failures.length);
|
|
53
|
+
print(
|
|
54
|
+
json,
|
|
55
|
+
{
|
|
56
|
+
action: 'run',
|
|
57
|
+
case: kase.id,
|
|
58
|
+
ledger: file,
|
|
59
|
+
head,
|
|
60
|
+
authenticated,
|
|
61
|
+
results: results.map(({ check, failures, record }) => ({
|
|
62
|
+
check,
|
|
63
|
+
status: failures.length ? 'failed' : 'passed',
|
|
64
|
+
failures,
|
|
65
|
+
sequence: record.sequence,
|
|
66
|
+
exitCode: record.exitCode,
|
|
67
|
+
revision: record.revision,
|
|
68
|
+
dirty: record.dirty,
|
|
69
|
+
sha256: record.sha256,
|
|
70
|
+
})),
|
|
71
|
+
},
|
|
72
|
+
[
|
|
73
|
+
`${file}`,
|
|
74
|
+
...results.map(
|
|
75
|
+
({ check, failures, record }) =>
|
|
76
|
+
` ${failures.length ? 'failed' : 'passed'} ${check.padEnd(24)} exit ${record.exitCode ?? '-'} in ${Math.round(record.durationMs / 100) / 10} s${failures.length ? ` — ${failures.join('; ')}` : ''}`
|
|
77
|
+
),
|
|
78
|
+
...(results.some(({ record }) => record.dirty !== false)
|
|
79
|
+
? [
|
|
80
|
+
' Ran on uncommitted changes, untracked files or outside git: a case bound to a commit will not accept these runs.',
|
|
81
|
+
]
|
|
82
|
+
: []),
|
|
83
|
+
`Recorded ${results.length} run(s)${authenticated ? ', authenticated by key' : ''}; ledger head ${head}.`,
|
|
84
|
+
`Keep the head outside the ledger and verify with bmad-plus assurance verify ${kase.file} --ledger-head ${head}.`,
|
|
85
|
+
]
|
|
86
|
+
);
|
|
87
|
+
process.exitCode = failed.length ? 1 : 0;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function verifyAction(projectDir, kase, options, json) {
|
|
91
|
+
const verdict = assurance.verifyCase(projectDir, kase, {
|
|
92
|
+
dir: options.dir,
|
|
93
|
+
head: options.ledgerHead ?? null,
|
|
94
|
+
});
|
|
95
|
+
let check = null;
|
|
96
|
+
if (options.emitCheck)
|
|
97
|
+
check = writeCheck(
|
|
98
|
+
projectDir,
|
|
99
|
+
options.emitCheck,
|
|
100
|
+
kase,
|
|
101
|
+
assurance.ledgerFile(projectDir, options.dir, kase.id),
|
|
102
|
+
assurance.checkResult(verdict)
|
|
103
|
+
);
|
|
104
|
+
print(
|
|
105
|
+
json,
|
|
106
|
+
{ action: 'verify', ...verdict, check },
|
|
107
|
+
[
|
|
108
|
+
`${kase.id}: ${verdict.status} — ${verdict.claims.filter((c) => c.status === 'supported').length}/${verdict.claims.length} claim(s) supported, ${verdict.ledger.runs} recorded run(s)`,
|
|
109
|
+
...verdict.reasons.map((reason) => ` - ${reason}`),
|
|
110
|
+
...Object.entries(verdict.checks)
|
|
111
|
+
.filter(([, judged]) => judged.status !== 'passed')
|
|
112
|
+
.map(([id, judged]) => ` ${judged.status.padEnd(7)} ${id}: ${judged.reasons.join('; ')}`),
|
|
113
|
+
...verdict.claims
|
|
114
|
+
.filter((claim) => claim.status !== 'supported')
|
|
115
|
+
.map((claim) => ` unsupported claim ${claim.id}: ${claim.reasons.join('; ')}`),
|
|
116
|
+
...(verdict.controls.supported.length
|
|
117
|
+
? [` controls with executed evidence: ${verdict.controls.supported.join(', ')}`]
|
|
118
|
+
: []),
|
|
119
|
+
check ? ` check: ${check}` : '',
|
|
120
|
+
].filter(Boolean)
|
|
121
|
+
);
|
|
122
|
+
process.exitCode = verdict.exitCode;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
module.exports = {
|
|
126
|
+
command: 'assurance <action> <case>',
|
|
127
|
+
description: 'Security assurance case bound to executed checks: init, run, verify',
|
|
128
|
+
options: [
|
|
129
|
+
['-d, --directory <path>', 'Project directory'],
|
|
130
|
+
['--dir <path>', 'Evidence folder inside the project', assurance.DEFAULT_DIR],
|
|
131
|
+
['--check <id>', 'run: only this check (repeatable)', (v, all) => [...all, v], []],
|
|
132
|
+
['--ledger-head <sha256>', 'verify: fail unless the ledger still holds this record'],
|
|
133
|
+
['--emit-check <file>', 'verify: also write the verdict as a JSON check result for CI'],
|
|
134
|
+
['--json', 'Machine-readable output'],
|
|
135
|
+
],
|
|
136
|
+
action: async (action, caseFile, options = {}) => {
|
|
137
|
+
const projectDir = path.resolve(options.directory || process.cwd());
|
|
138
|
+
const json = Boolean(options.json);
|
|
139
|
+
const settings = { dir: options.dir || assurance.DEFAULT_DIR, check: options.check || [] };
|
|
140
|
+
try {
|
|
141
|
+
if (!ACTIONS.includes(action))
|
|
142
|
+
throw new Error(`unknown action "${action}" (${ACTIONS.join(', ')})`);
|
|
143
|
+
if (action === 'init') return initAction(projectDir, caseFile, json);
|
|
144
|
+
const kase = assurance.loadCase(projectDir, caseFile);
|
|
145
|
+
if (action === 'run') await runAction(projectDir, kase, settings, json);
|
|
146
|
+
else
|
|
147
|
+
verifyAction(
|
|
148
|
+
projectDir,
|
|
149
|
+
kase,
|
|
150
|
+
{ ...settings, emitCheck: options.emitCheck, ledgerHead: options.ledgerHead },
|
|
151
|
+
json
|
|
152
|
+
);
|
|
153
|
+
} catch (error) {
|
|
154
|
+
if (json)
|
|
155
|
+
console.log(
|
|
156
|
+
JSON.stringify({ schemaVersion: 1, status: 'error', message: error.message }, null, 2)
|
|
157
|
+
);
|
|
158
|
+
else console.error(`assurance: ${error.message}`);
|
|
159
|
+
process.exitCode = 3;
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
};
|
|
@@ -15,7 +15,7 @@ const clack = require('@clack/prompts');
|
|
|
15
15
|
const pc = require('picocolors');
|
|
16
16
|
const { t, LANGUAGES, getLanguageOptions, getCommLanguageOptions } = require('../i18n');
|
|
17
17
|
const { PACKS, DERIVED } = require('../lib/packs');
|
|
18
|
-
const { copyPackFiles, copyManagedFile } = require('../lib/pack-copy');
|
|
18
|
+
const { copyPackFiles, copyManagedFile, listPackFiles } = require('../lib/pack-copy');
|
|
19
19
|
const {
|
|
20
20
|
acquireProjectLock,
|
|
21
21
|
atomicWrite,
|
|
@@ -30,6 +30,7 @@ const { readInstallManifest } = require('../lib/install-manifest');
|
|
|
30
30
|
const yaml = require('js-yaml');
|
|
31
31
|
const { validateUserName } = require('../lib/validate');
|
|
32
32
|
const { provisionPack } = require('../lib/python-provision');
|
|
33
|
+
const { SWARM_DEFAULTS } = require('../lib/swarm-defaults');
|
|
33
34
|
|
|
34
35
|
const PYTHON_PACKS = DERIVED.pythonPacks;
|
|
35
36
|
|
|
@@ -287,6 +288,19 @@ module.exports = {
|
|
|
287
288
|
const releaseLock = acquireProjectLock(projectDir);
|
|
288
289
|
try {
|
|
289
290
|
const { safeTarget } = require('../../build/generate-adapters');
|
|
291
|
+
// Every pack must enumerate before the first write: a missing or mismatched
|
|
292
|
+
// external module aborts here, not after Core files exist without a manifest.
|
|
293
|
+
for (const packId of selectedPacks) {
|
|
294
|
+
const pack = PACKS[packId];
|
|
295
|
+
if (!pack || pack.disabled) continue;
|
|
296
|
+
listPackFiles({
|
|
297
|
+
bmadSrc,
|
|
298
|
+
targetAgentsDir: path.join(projectDir, '.agents', 'skills'),
|
|
299
|
+
targetDataDir: path.join(projectDir, '.agents', 'data'),
|
|
300
|
+
projectRoot: path.join(bmadSrc, '..', '..'),
|
|
301
|
+
pack,
|
|
302
|
+
});
|
|
303
|
+
}
|
|
290
304
|
const plannedFiles = collectManagedFiles({ projectDir, packs: selectedPacks });
|
|
291
305
|
if (detectedIDEs.length)
|
|
292
306
|
plannedFiles.push(
|
|
@@ -558,6 +572,10 @@ module.exports = {
|
|
|
558
572
|
agentGuide.push(` ${i.guide_animated.padEnd(28)} → "/animated build <video>"`);
|
|
559
573
|
}
|
|
560
574
|
|
|
575
|
+
if (selectedPacks.includes('design')) {
|
|
576
|
+
agentGuide.push(` ${i.guide_design.padEnd(28)} → "/design read"`);
|
|
577
|
+
}
|
|
578
|
+
|
|
561
579
|
if (selectedPacks.includes('shield')) {
|
|
562
580
|
agentGuide.push(
|
|
563
581
|
` ${(i.guide_shield || '🛡️ GRC Compliance').padEnd(28)} → "Shield, audit my SaaS for GDPR"`
|
|
@@ -609,6 +627,11 @@ module.exports = {
|
|
|
609
627
|
if (selectedPacks.includes('animated')) {
|
|
610
628
|
examples.push(` ${i.guide_example_animated || '🎬 Animated: "/animated build hero.mp4"'}`);
|
|
611
629
|
}
|
|
630
|
+
if (selectedPacks.includes('design')) {
|
|
631
|
+
examples.push(
|
|
632
|
+
` ${i.guide_example_design || '🎨 Design: "/design read" → Atelier interviews you, then builds'}`
|
|
633
|
+
);
|
|
634
|
+
}
|
|
612
635
|
if (selectedPacks.includes('osint')) {
|
|
613
636
|
examples.push(` ${i.guide_example_osint || '🔍 OSINT: "Shadow, investigate John Doe"'}`);
|
|
614
637
|
}
|
|
@@ -819,6 +842,10 @@ function generateConfigYaml(
|
|
|
819
842
|
page_budget: { max_steps: 15, max_minutes: 30 },
|
|
820
843
|
dir: '_bmad-output/uat',
|
|
821
844
|
},
|
|
845
|
+
// Swarms of sub-agents. budget: how many one run may use before the host asks the
|
|
846
|
+
// user; wave: how many are launched together; media_credits: paid generation is
|
|
847
|
+
// never covered by a budget, the host always asks.
|
|
848
|
+
swarm: { ...SWARM_DEFAULTS },
|
|
822
849
|
},
|
|
823
850
|
{ lineWidth: -1, forceQuotes: true, quotingType: '"' }
|
|
824
851
|
)
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/** Validate, plan, record and render a pack intake; the host asks, the user answers. */
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const intake = require('../lib/intake');
|
|
6
|
+
|
|
7
|
+
const collect = (value, list) => [...list, value];
|
|
8
|
+
|
|
9
|
+
function describePlan(result) {
|
|
10
|
+
const lines = [
|
|
11
|
+
`${result.intake} (${result.pack}): ${result.ready ? 'ready' : 'not ready'} — ` +
|
|
12
|
+
`${result.counts.answered} answered, ${result.counts.defaulted} defaulted, ` +
|
|
13
|
+
`${result.counts.unknown} unknown, ${result.counts.pending} not asked yet.`,
|
|
14
|
+
];
|
|
15
|
+
if (result.missing.length) lines.push(`Must-haves still open: ${result.missing.join(', ')}`);
|
|
16
|
+
if (result.next.length) {
|
|
17
|
+
lines.push(`Ask next (at most ${result.maxPerMessage} per message):`);
|
|
18
|
+
for (const question of result.next) {
|
|
19
|
+
const hints = [];
|
|
20
|
+
if (question.mustHave) hints.push('must-have');
|
|
21
|
+
if (question.choices) hints.push(`choices: ${question.choices.join(', ')}`);
|
|
22
|
+
if (question.default !== undefined)
|
|
23
|
+
hints.push(`default: ${JSON.stringify(question.default)}`);
|
|
24
|
+
lines.push(
|
|
25
|
+
` - ${question.id} [${question.kind}] ${question.ask}${hints.length ? ` (${hints.join('; ')})` : ''}`
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
for (const unknown of result.unknowns) {
|
|
30
|
+
lines.push(`Unknown: ${unknown.id}${unknown.risk ? ` — risk: ${unknown.risk}` : ''}`);
|
|
31
|
+
}
|
|
32
|
+
return lines;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
module.exports = {
|
|
36
|
+
command: 'intake <action> [file]',
|
|
37
|
+
description:
|
|
38
|
+
'Pack intake: check a file, plan the next questions, record answers, render the brief',
|
|
39
|
+
options: [
|
|
40
|
+
['-d, --directory <path>', 'Project directory (default: current directory)'],
|
|
41
|
+
['--intake <file>', 'Intake file, relative to the project'],
|
|
42
|
+
['--answers <file>', 'Answers file, relative to the project (created by record)'],
|
|
43
|
+
[
|
|
44
|
+
'--set <id=VALUE>',
|
|
45
|
+
'Record an answer, or id=UNKNOWN to mark it unknown (repeatable)',
|
|
46
|
+
collect,
|
|
47
|
+
[],
|
|
48
|
+
],
|
|
49
|
+
['--default <id>', 'Record the proposed default of a question (repeatable)', collect, []],
|
|
50
|
+
['--reset', 'Start the answers file again, empty, for this intake (record)'],
|
|
51
|
+
['--out <file>', 'Brief to write (render)'],
|
|
52
|
+
['--json', 'Machine-readable output'],
|
|
53
|
+
],
|
|
54
|
+
action: (action, file, options = {}) => {
|
|
55
|
+
const projectDir = path.resolve(options.directory || process.cwd());
|
|
56
|
+
const needs = (name) => {
|
|
57
|
+
if (!options[name]) throw new Error(`--${name} is required for intake ${action}`);
|
|
58
|
+
return options[name];
|
|
59
|
+
};
|
|
60
|
+
try {
|
|
61
|
+
let result;
|
|
62
|
+
let lines;
|
|
63
|
+
if (action === 'check') {
|
|
64
|
+
if (!file) throw new Error('Use intake check FILE.');
|
|
65
|
+
result = intake.check({ projectDir, file });
|
|
66
|
+
process.exitCode = result.ok ? 0 : 1;
|
|
67
|
+
lines = result.ok
|
|
68
|
+
? [
|
|
69
|
+
`${result.file}: valid intake ${result.id} (${result.pack}), ${result.questions} questions, ${result.mustHave} must-have.`,
|
|
70
|
+
]
|
|
71
|
+
: [
|
|
72
|
+
`${result.file}: not a valid intake`,
|
|
73
|
+
...result.problems.map((problem) => ` - ${problem}`),
|
|
74
|
+
];
|
|
75
|
+
result = { status: result.ok ? 'valid' : 'invalid', ...result };
|
|
76
|
+
} else if (action === 'plan') {
|
|
77
|
+
result = intake.plan({
|
|
78
|
+
projectDir,
|
|
79
|
+
intakeFile: needs('intake'),
|
|
80
|
+
answersFile: needs('answers'),
|
|
81
|
+
});
|
|
82
|
+
process.exitCode = result.ready ? 0 : 2;
|
|
83
|
+
lines = describePlan(result);
|
|
84
|
+
result = { status: result.ready ? 'ready' : 'needs-answers', ...result };
|
|
85
|
+
} else if (action === 'record') {
|
|
86
|
+
result = intake.record({
|
|
87
|
+
projectDir,
|
|
88
|
+
intakeFile: needs('intake'),
|
|
89
|
+
answersFile: needs('answers'),
|
|
90
|
+
sets: options.set,
|
|
91
|
+
defaults: options.default,
|
|
92
|
+
reset: options.reset === true,
|
|
93
|
+
});
|
|
94
|
+
process.exitCode = 0;
|
|
95
|
+
lines = [
|
|
96
|
+
result.reset
|
|
97
|
+
? `Reset ${result.answersFile}${result.recorded.length ? `, recorded ${result.recorded.join(', ')}` : ''}.`
|
|
98
|
+
: `Recorded ${result.recorded.join(', ')} in ${result.answersFile}.`,
|
|
99
|
+
...describePlan(result),
|
|
100
|
+
];
|
|
101
|
+
result = { status: 'recorded', ...result };
|
|
102
|
+
} else if (action === 'render') {
|
|
103
|
+
result = intake.render({
|
|
104
|
+
projectDir,
|
|
105
|
+
intakeFile: needs('intake'),
|
|
106
|
+
answersFile: needs('answers'),
|
|
107
|
+
out: needs('out'),
|
|
108
|
+
});
|
|
109
|
+
process.exitCode = 0;
|
|
110
|
+
lines = [
|
|
111
|
+
`Brief written to ${result.out} (${result.bytes} bytes); ${result.ready ? 'the intake is ready' : 'a must-have is still open'}.`,
|
|
112
|
+
];
|
|
113
|
+
result = { status: 'rendered', ...result };
|
|
114
|
+
} else throw new Error('Use intake check FILE, plan, record or render.');
|
|
115
|
+
if (options.json) console.log(JSON.stringify({ schemaVersion: 1, ...result }, null, 2));
|
|
116
|
+
else for (const line of lines) console.log(line);
|
|
117
|
+
} catch (error) {
|
|
118
|
+
process.exitCode = 1;
|
|
119
|
+
if (options.json)
|
|
120
|
+
console.log(JSON.stringify({ schemaVersion: 1, status: 'error', error: error.message }));
|
|
121
|
+
else console.error('Intake: ' + error.message);
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
};
|
|
@@ -169,23 +169,30 @@ function rulesAction(projectDir, target, json) {
|
|
|
169
169
|
action: 'rules',
|
|
170
170
|
path: file,
|
|
171
171
|
sha256: ruleset.sha256,
|
|
172
|
+
packFiles: ruleset.packFiles,
|
|
172
173
|
projectFile: ruleset.projectFile,
|
|
173
174
|
disabled: ruleset.disabled,
|
|
174
|
-
rules: listed.map(({ id, title, group, layer, globs, source }) => ({
|
|
175
|
+
rules: listed.map(({ id, title, group, layer, pack, globs, controls, source }) => ({
|
|
175
176
|
id,
|
|
176
177
|
title,
|
|
177
178
|
group,
|
|
178
179
|
layer,
|
|
180
|
+
...(pack ? { pack } : {}),
|
|
179
181
|
globs,
|
|
182
|
+
controls,
|
|
180
183
|
source,
|
|
181
184
|
})),
|
|
185
|
+
controls: reviewRules.controlsOf(
|
|
186
|
+
ruleset,
|
|
187
|
+
listed.map((rule) => rule.id)
|
|
188
|
+
),
|
|
182
189
|
},
|
|
183
190
|
[
|
|
184
|
-
`rule set ${ruleset.sha256.slice(0, 12)}${ruleset.projectFile ?
|
|
191
|
+
`rule set ${ruleset.sha256.slice(0, 12)} (${['built-in', ...ruleset.packFiles, ...(ruleset.projectFile ? [ruleset.projectFile] : [])].join(' + ')})`,
|
|
185
192
|
...(file ? [`${file} gets ${listed.length} rule(s):`] : []),
|
|
186
193
|
...listed.map(
|
|
187
194
|
(rule) =>
|
|
188
|
-
` ${rule.id.padEnd(24)} ${rule.group.padEnd(12)} ${rule.layer.padEnd(8)} ${rule.globs.join(' ')}`
|
|
195
|
+
` ${rule.id.padEnd(24)} ${rule.group.padEnd(12)} ${rule.layer.padEnd(8)} ${rule.globs.join(' ')}${rule.controls.length ? `\n ${''.padEnd(24)} controls: ${rule.controls.join(' ')}` : ''}`
|
|
189
196
|
),
|
|
190
197
|
...ruleset.disabled.map((id) => ` ${id.padEnd(24)} disabled by the project`),
|
|
191
198
|
]
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/** Run a swarm of sub-agents under a budget: deal, brief, check, file, tally; the host launches. */
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const swarm = require('../lib/swarm');
|
|
6
|
+
|
|
7
|
+
const USAGE =
|
|
8
|
+
'Use swarm init --definition FILE [--profile FILE | --units FILE] [--context NAME=FILE] [--n N] [--seed N] [--approved], ' +
|
|
9
|
+
'briefs RUN [--wave N], check RUN, save RUN UNIT --input FILE, tally RUN [--out FILE], inspect RUN, or plan RUN [--out FILE].';
|
|
10
|
+
|
|
11
|
+
function lines(action, result) {
|
|
12
|
+
if (result.status === 'needs-approval') return [`${result.run}: needs approval. ${result.next}`];
|
|
13
|
+
switch (action) {
|
|
14
|
+
case 'init':
|
|
15
|
+
return [
|
|
16
|
+
`${result.run}: ${result.kind} of ${result.n} units, budget ${result.budget}, approval ${result.approval}.`,
|
|
17
|
+
result.seed === null
|
|
18
|
+
? 'Units listed, no seed.'
|
|
19
|
+
: `Seed ${result.seed} (same seed, same cards).`,
|
|
20
|
+
`Next: ${result.next}`,
|
|
21
|
+
];
|
|
22
|
+
case 'briefs':
|
|
23
|
+
return [
|
|
24
|
+
`${result.run}: ${result.pending} of ${result.units} units need a sub-agent, in waves of ${result.wave}.`,
|
|
25
|
+
...result.waves.flatMap((wave) => [
|
|
26
|
+
`Wave ${wave.index}:`,
|
|
27
|
+
...wave.units.map((unit) => ` ${unit.id} ${unit.brief}`),
|
|
28
|
+
]),
|
|
29
|
+
`Each sub-agent: ${result.instruction}`,
|
|
30
|
+
`Disclosure: ${result.disclosure}`,
|
|
31
|
+
];
|
|
32
|
+
case 'check':
|
|
33
|
+
return [
|
|
34
|
+
`${result.run}: ${result.valid} of ${result.units} valid answers.`,
|
|
35
|
+
...result.pending.map(
|
|
36
|
+
(unit) => ` ${unit.id} ${unit.state}${unit.reason ? `: ${unit.reason}` : ''}`
|
|
37
|
+
),
|
|
38
|
+
];
|
|
39
|
+
case 'save':
|
|
40
|
+
return [`${result.run}: answer of ${result.unit} filed at ${result.answer}`];
|
|
41
|
+
case 'tally': {
|
|
42
|
+
const { valid, total, skipped } = result.tally.units;
|
|
43
|
+
return [
|
|
44
|
+
result.files.markdown,
|
|
45
|
+
`${valid} of ${total} units counted; ${skipped.length} skipped.`,
|
|
46
|
+
`Disclosure: ${result.tally.disclosure}`,
|
|
47
|
+
];
|
|
48
|
+
}
|
|
49
|
+
case 'inspect': {
|
|
50
|
+
const { counts } = result;
|
|
51
|
+
return [
|
|
52
|
+
`${result.run.id}: ${result.status} (${result.run.kind}, ${result.budget.decision})`,
|
|
53
|
+
`units ${counts.units}, briefed ${counts.briefed}, valid ${counts.valid}, invalid ${counts.invalid}, missing ${counts.missing}`,
|
|
54
|
+
`Disclosure: ${result.disclosure}`,
|
|
55
|
+
];
|
|
56
|
+
}
|
|
57
|
+
case 'plan':
|
|
58
|
+
return [
|
|
59
|
+
`${result.plan}: ${result.tasks} tasks, ${result.maxParallel} in parallel.`,
|
|
60
|
+
`Next: ${result.next}`,
|
|
61
|
+
];
|
|
62
|
+
default:
|
|
63
|
+
return [JSON.stringify(result)];
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = {
|
|
68
|
+
command: 'swarm <action> [run] [unit]',
|
|
69
|
+
description:
|
|
70
|
+
'Deal, brief, check, file and tally a swarm of sub-agents within the configured budget',
|
|
71
|
+
options: [
|
|
72
|
+
['-d, --directory <path>', 'Project directory'],
|
|
73
|
+
['--definition <file>', 'Swarm definition (project-relative JSON) for init'],
|
|
74
|
+
['--profile <file>', 'Panel profile (project-relative JSON) for init'],
|
|
75
|
+
['--units <file>', 'Fanout units (project-relative JSON) for init'],
|
|
76
|
+
[
|
|
77
|
+
'--context <name=file>',
|
|
78
|
+
'Context file included verbatim in the briefs (repeatable)',
|
|
79
|
+
(value, all) => [...all, value],
|
|
80
|
+
[],
|
|
81
|
+
],
|
|
82
|
+
['--n <count>', 'Units to deal for a panel'],
|
|
83
|
+
['--seed <number>', 'Dealing seed; drawn once and recorded when absent'],
|
|
84
|
+
['--approved', 'Record that the user agreed to exceed the budget'],
|
|
85
|
+
['--wave <count>', 'Sub-agents launched together (default: the configured wave)'],
|
|
86
|
+
['--input <file>', 'Printed reply to file with save (project-relative)'],
|
|
87
|
+
['--out <file>', 'Output file for tally or plan, under .bmad-plus/swarm/'],
|
|
88
|
+
['--json', 'Machine-readable output'],
|
|
89
|
+
],
|
|
90
|
+
action: (action, runId, unitId, options = {}) => {
|
|
91
|
+
const directory = path.resolve(options.directory || process.cwd());
|
|
92
|
+
try {
|
|
93
|
+
let result;
|
|
94
|
+
if (action === 'init' && !runId) result = swarm.init(directory, options);
|
|
95
|
+
else if (action === 'briefs' && runId && !unitId)
|
|
96
|
+
result = swarm.briefs(directory, runId, options);
|
|
97
|
+
else if (action === 'check' && runId && !unitId) result = swarm.check(directory, runId);
|
|
98
|
+
else if (action === 'save' && runId && unitId)
|
|
99
|
+
result = swarm.save(directory, runId, unitId, options);
|
|
100
|
+
else if (action === 'tally' && runId && !unitId)
|
|
101
|
+
result = swarm.tally(directory, runId, options);
|
|
102
|
+
else if (action === 'inspect' && runId && !unitId) result = swarm.inspect(directory, runId);
|
|
103
|
+
else if (action === 'plan' && runId && !unitId)
|
|
104
|
+
result = swarm.plan(directory, runId, options);
|
|
105
|
+
else throw new Error(USAGE);
|
|
106
|
+
process.exitCode =
|
|
107
|
+
result.status === 'needs-approval' ? 2 : result.status === 'incomplete' ? 1 : 0;
|
|
108
|
+
if (options.json) console.log(JSON.stringify(result, null, 2));
|
|
109
|
+
else for (const line of lines(action, result)) console.log(line);
|
|
110
|
+
} catch (error) {
|
|
111
|
+
process.exitCode = 1;
|
|
112
|
+
if (options.json)
|
|
113
|
+
console.log(
|
|
114
|
+
JSON.stringify({ schemaVersion: 1, status: 'error', error: error.message }, null, 2)
|
|
115
|
+
);
|
|
116
|
+
else console.error('Swarm: ' + error.message.replace(/^Swarm: /, ''));
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
};
|
|
@@ -11,6 +11,7 @@ const pc = require('picocolors');
|
|
|
11
11
|
const { t } = require('../i18n');
|
|
12
12
|
const { PACKS, DERIVED } = require('../lib/packs');
|
|
13
13
|
const { readInstallManifest } = require('../lib/install-manifest');
|
|
14
|
+
const { externalModuleRoot } = require('../lib/pack-copy');
|
|
14
15
|
const { generateUserFiles, USER_CONFIG_MARKER } = require('../../build/generate-adapters');
|
|
15
16
|
const { contentHash } = require('../lib/installed-adapters');
|
|
16
17
|
|
|
@@ -44,6 +45,16 @@ function safeStat(projectDir, target) {
|
|
|
44
45
|
return stat;
|
|
45
46
|
}
|
|
46
47
|
|
|
48
|
+
// The skills copied from an npm package are matched against that package. When it is gone
|
|
49
|
+
// or no longer the pinned version, nothing can be compared and they stay.
|
|
50
|
+
function resolvedExternalModule(externalModule, packageRoot) {
|
|
51
|
+
try {
|
|
52
|
+
return externalModuleRoot({ externalModule, projectRoot: packageRoot });
|
|
53
|
+
} catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
47
58
|
function removalPlan(projectDir, manifest, manifestPath) {
|
|
48
59
|
const packageRoot = path.join(__dirname, '..', '..', '..');
|
|
49
60
|
const sourceRoot = path.join(packageRoot, 'src', 'bmad-plus');
|
|
@@ -91,6 +102,12 @@ function removalPlan(projectDir, manifest, manifestPath) {
|
|
|
91
102
|
path.join(packageRoot, pack.externalPackage, 'skills'),
|
|
92
103
|
targetPath('.agents', 'skills')
|
|
93
104
|
);
|
|
105
|
+
if (pack.externalModule) {
|
|
106
|
+
const moduleRoot = resolvedExternalModule(pack.externalModule, packageRoot);
|
|
107
|
+
if (moduleRoot)
|
|
108
|
+
for (const name of pack.externalModule.skills)
|
|
109
|
+
matchSource(path.join(moduleRoot, 'skills', name), targetPath('.agents', 'skills', name));
|
|
110
|
+
}
|
|
94
111
|
}
|
|
95
112
|
for (const name of ['module.yaml', 'module-help.csv'])
|
|
96
113
|
matchSource(path.join(sourceRoot, name), targetPath('_bmad', name));
|