bmad-plus 0.22.0 → 0.23.1
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 +59 -0
- package/README.md +46 -26
- package/SECURITY.md +1 -1
- package/osint-agent-package/agents/osint-investigator.md +3 -3
- package/osint-agent-package/skills/bmad-osint-investigate/osint/SKILL.md +51 -51
- package/osint-agent-package/skills/bmad-osint-investigate/osint/references/content-extraction.md +7 -7
- package/osint-agent-package/skills/bmad-osint-investigate/osint/references/platforms.md +2 -2
- package/osint-agent-package/skills/bmad-osint-investigate/osint/references/tools.md +31 -41
- package/package.json +24 -5
- package/readme-international/README.de.md +46 -26
- package/readme-international/README.es.md +47 -27
- package/readme-international/README.fr.md +46 -26
- package/src/bmad-plus/agents/agent-maker/SKILL.md +39 -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 +26 -4
- package/src/bmad-plus/packs/pack-animated/animated-website-agent.md +40 -14
- package/src/bmad-plus/packs/pack-animated/templates/animated-website-workflow.md +15 -12
- package/src/bmad-plus/packs/pack-backup/backup-agent.md +2 -1
- package/src/bmad-plus/packs/pack-backup/templates/backup-workflow.md +2 -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-memory/zecher-agent.md +1 -1
- package/src/bmad-plus/packs/pack-shield/README.md +1 -1
- package/src/bmad-plus/packs/pack-shield/SKILL.md +3 -3
- package/src/bmad-plus/packs/pack-shield/categories/industry-compliance/pci-dss-agent.md +1 -1
- package/src/bmad-plus/packs/pack-shield/shield-orchestrator.md +2 -2
- 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/tools/build/adapters.config.js +3 -1
- package/tools/build/generate-adapters.js +26 -2
- package/tools/build/generate.js +269 -7
- package/tools/build/intake-contract.js +522 -0
- package/tools/cli/bmad-plus-cli.js +2 -0
- package/tools/cli/commands/install.js +45 -11
- package/tools/cli/commands/intake.js +124 -0
- package/tools/cli/commands/nexus.js +2 -1
- package/tools/cli/commands/review.js +1 -1
- package/tools/cli/commands/swarm.js +119 -0
- package/tools/cli/commands/uninstall.js +17 -0
- package/tools/cli/commands/update.js +30 -1
- package/tools/cli/i18n.js +50 -30
- package/tools/cli/lib/intake.js +202 -0
- package/tools/cli/lib/pack-copy.js +95 -5
- package/tools/cli/lib/packs.js +102 -12
- package/tools/cli/lib/review.js +2 -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/update-dispatch.js +10 -2
- package/tools/cli/lib/update-transaction.js +9 -6
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/** Read, plan, record and render a pack intake inside one project; the contract decides. */
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
const crypto = require('node:crypto');
|
|
7
|
+
const yaml = require('js-yaml');
|
|
8
|
+
const { safeTarget } = require('../../build/generate-adapters');
|
|
9
|
+
const contract = require('../../build/intake-contract');
|
|
10
|
+
|
|
11
|
+
const MAX_FILE = 1024 * 1024;
|
|
12
|
+
// Trees the intake never writes into, wherever they sit in the project.
|
|
13
|
+
const RESERVED_DIRS = new Set(['.git', '_bmad', '.bmad-plus', 'node_modules']);
|
|
14
|
+
|
|
15
|
+
/** A user-supplied path, kept inside the project and free of symlinks. */
|
|
16
|
+
function projectFile(projectDir, file) {
|
|
17
|
+
if (typeof file !== 'string' || file.trim() === '') throw new Error('a file path is required');
|
|
18
|
+
const root = path.resolve(projectDir);
|
|
19
|
+
const relative = path.relative(root, path.resolve(root, file)).split(path.sep).join('/');
|
|
20
|
+
if (!relative || relative === '..' || relative.startsWith('../'))
|
|
21
|
+
throw new Error(`${file}: outside the project directory ${root}`);
|
|
22
|
+
return { relative, absolute: safeTarget(root, relative) };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** A path the intake may write: outside the reserved trees, and never through a link. */
|
|
26
|
+
function writableFile(projectDir, file) {
|
|
27
|
+
const target = projectFile(projectDir, file);
|
|
28
|
+
const reserved = target.relative.split('/').find((part) => RESERVED_DIRS.has(part));
|
|
29
|
+
if (reserved) throw new Error(`${target.relative}: the intake never writes under ${reserved}/`);
|
|
30
|
+
return target;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function readBounded(absolute, label) {
|
|
34
|
+
const stat = fs.lstatSync(absolute);
|
|
35
|
+
if (!stat.isFile()) throw new Error(`${label}: not a regular file`);
|
|
36
|
+
if (stat.size > MAX_FILE) throw new Error(`${label}: larger than ${MAX_FILE} bytes`);
|
|
37
|
+
return fs.readFileSync(absolute, 'utf8');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function describe(problems) {
|
|
41
|
+
return problems.map((problem) => ` - ${problem}`).join('\n');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Parse and validate an intake file; throws listing every problem. */
|
|
45
|
+
function loadIntake(projectDir, file) {
|
|
46
|
+
const target = projectFile(projectDir, file);
|
|
47
|
+
let parsed;
|
|
48
|
+
try {
|
|
49
|
+
parsed = yaml.load(readBounded(target.absolute, target.relative), { schema: yaml.JSON_SCHEMA });
|
|
50
|
+
} catch (error) {
|
|
51
|
+
throw new Error(`${target.relative}: ${error.message}`, { cause: error });
|
|
52
|
+
}
|
|
53
|
+
const result = contract.validateIntake(parsed);
|
|
54
|
+
if (!result.ok)
|
|
55
|
+
throw new Error(`${target.relative} is not a valid intake:\n${describe(result.problems)}`);
|
|
56
|
+
return { file: target.relative, intake: parsed };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** The parsed JSON of an existing answers file when it belongs to this intake; else throws. */
|
|
60
|
+
function ownedAnswers(target, intake) {
|
|
61
|
+
let parsed;
|
|
62
|
+
try {
|
|
63
|
+
parsed = JSON.parse(readBounded(target.absolute, target.relative));
|
|
64
|
+
} catch (error) {
|
|
65
|
+
throw new Error(
|
|
66
|
+
`${target.relative}: exists and is not an answers file of intake "${intake.id}" (${error.message}); move it away or pick another path`,
|
|
67
|
+
{ cause: error }
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
if (!parsed || typeof parsed !== 'object' || parsed.intake !== intake.id) {
|
|
71
|
+
throw new Error(
|
|
72
|
+
`${target.relative}: exists and belongs to ${JSON.stringify(parsed && parsed.intake)}, not to intake "${intake.id}"; move it away or pick another path`
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
return parsed;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Parse and validate an answers file; an absent file means no answer yet. */
|
|
79
|
+
function loadAnswers(projectDir, file, intake) {
|
|
80
|
+
const target = writableFile(projectDir, file);
|
|
81
|
+
if (!fs.existsSync(target.absolute)) {
|
|
82
|
+
return { file: target.relative, answers: contract.emptyAnswers(intake), exists: false };
|
|
83
|
+
}
|
|
84
|
+
const parsed = ownedAnswers(target, intake);
|
|
85
|
+
const result = contract.validateAnswers(parsed, intake);
|
|
86
|
+
if (!result.ok)
|
|
87
|
+
throw new Error(
|
|
88
|
+
`${target.relative} does not answer this intake:\n${describe(result.problems)}\nRecord again with --reset to start over.`
|
|
89
|
+
);
|
|
90
|
+
return { file: target.relative, answers: parsed, exists: true };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function atomicWrite(projectDir, relative, content) {
|
|
94
|
+
const target = safeTarget(projectDir, relative);
|
|
95
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
96
|
+
const temporaryFile = `${relative}.${crypto.randomUUID()}.tmp`;
|
|
97
|
+
const temporary = safeTarget(projectDir, temporaryFile);
|
|
98
|
+
let fd;
|
|
99
|
+
try {
|
|
100
|
+
fd = fs.openSync(temporary, 'wx');
|
|
101
|
+
fs.writeFileSync(fd, content);
|
|
102
|
+
fs.fsyncSync(fd);
|
|
103
|
+
fs.closeSync(fd);
|
|
104
|
+
fd = undefined;
|
|
105
|
+
safeTarget(projectDir, relative);
|
|
106
|
+
fs.renameSync(temporary, target);
|
|
107
|
+
} finally {
|
|
108
|
+
if (fd !== undefined) fs.closeSync(fd);
|
|
109
|
+
if (fs.existsSync(temporary)) fs.unlinkSync(temporary);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function check({ projectDir, file }) {
|
|
114
|
+
const target = projectFile(projectDir, file);
|
|
115
|
+
let parsed;
|
|
116
|
+
try {
|
|
117
|
+
parsed = yaml.load(readBounded(target.absolute, target.relative), { schema: yaml.JSON_SCHEMA });
|
|
118
|
+
} catch (error) {
|
|
119
|
+
return { file: target.relative, ok: false, problems: [error.message] };
|
|
120
|
+
}
|
|
121
|
+
const result = contract.validateIntake(parsed);
|
|
122
|
+
const summary = result.ok
|
|
123
|
+
? {
|
|
124
|
+
id: parsed.id,
|
|
125
|
+
pack: parsed.pack,
|
|
126
|
+
questions: contract.listQuestions(parsed).length,
|
|
127
|
+
mustHave: contract.listQuestions(parsed).filter((q) => q.must_have === true).length,
|
|
128
|
+
}
|
|
129
|
+
: {};
|
|
130
|
+
return { file: target.relative, ...result, ...summary };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function plan({ projectDir, intakeFile, answersFile }) {
|
|
134
|
+
const { intake } = loadIntake(projectDir, intakeFile);
|
|
135
|
+
const { answers, file } = loadAnswers(projectDir, answersFile, intake);
|
|
136
|
+
return { answersFile: file, ...contract.planIntake(intake, answers) };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* `sets` is a list of "id=VALUE" texts, `defaults` a list of question ids. With
|
|
141
|
+
* `reset`, the file starts again empty for this intake (an existing file must
|
|
142
|
+
* already belong to it), then takes the sets and defaults of the same call.
|
|
143
|
+
*/
|
|
144
|
+
function record({ projectDir, intakeFile, answersFile, sets = [], defaults = [], reset = false }) {
|
|
145
|
+
const { intake } = loadIntake(projectDir, intakeFile);
|
|
146
|
+
let loaded;
|
|
147
|
+
if (reset) {
|
|
148
|
+
const target = writableFile(projectDir, answersFile);
|
|
149
|
+
if (fs.existsSync(target.absolute)) ownedAnswers(target, intake);
|
|
150
|
+
loaded = { file: target.relative, answers: contract.emptyAnswers(intake) };
|
|
151
|
+
} else loaded = loadAnswers(projectDir, answersFile, intake);
|
|
152
|
+
const pairs = {};
|
|
153
|
+
for (const assignment of sets) {
|
|
154
|
+
const at = typeof assignment === 'string' ? assignment.indexOf('=') : -1;
|
|
155
|
+
if (at < 1) throw new Error(`--set expects id=VALUE, got "${assignment}"`);
|
|
156
|
+
const id = assignment.slice(0, at);
|
|
157
|
+
if (Object.hasOwn(pairs, id)) throw new Error(`question "${id}" is set twice in one call`);
|
|
158
|
+
pairs[id] = assignment.slice(at + 1);
|
|
159
|
+
}
|
|
160
|
+
if (!reset && Object.keys(pairs).length === 0 && defaults.length === 0)
|
|
161
|
+
throw new Error('nothing to record: pass --set id=VALUE, --default id or --reset');
|
|
162
|
+
const answers = contract.recordAnswers(intake, loaded.answers, { sets: pairs, defaults });
|
|
163
|
+
const result = contract.validateAnswers(answers, intake);
|
|
164
|
+
if (!result.ok) throw new Error(`recorded answers are invalid:\n${describe(result.problems)}`);
|
|
165
|
+
atomicWrite(projectDir, loaded.file, JSON.stringify(answers, null, 2) + '\n');
|
|
166
|
+
return {
|
|
167
|
+
answersFile: loaded.file,
|
|
168
|
+
reset,
|
|
169
|
+
recorded: [...Object.keys(pairs), ...defaults],
|
|
170
|
+
...contract.planIntake(intake, answers),
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** The brief overwrites only a file it wrote before for the same intake. */
|
|
175
|
+
function render({ projectDir, intakeFile, answersFile, out }) {
|
|
176
|
+
const { intake } = loadIntake(projectDir, intakeFile);
|
|
177
|
+
const { answers } = loadAnswers(projectDir, answersFile, intake);
|
|
178
|
+
const target = writableFile(projectDir, out);
|
|
179
|
+
if (fs.existsSync(target.absolute)) {
|
|
180
|
+
const marker = contract.briefMarker(intake);
|
|
181
|
+
const head = readBounded(target.absolute, target.relative).split(/\r?\n/, 2);
|
|
182
|
+
if (head[1] !== marker)
|
|
183
|
+
throw new Error(
|
|
184
|
+
`${target.relative}: exists and is not a brief of intake "${intake.id}" (its second line is not the ownership marker); move it away or pick another path`
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
const brief = contract.renderBrief(intake, answers);
|
|
188
|
+
atomicWrite(projectDir, target.relative, brief);
|
|
189
|
+
const planned = contract.planIntake(intake, answers);
|
|
190
|
+
return { out: target.relative, bytes: Buffer.byteLength(brief), ready: planned.ready };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
module.exports = {
|
|
194
|
+
projectFile,
|
|
195
|
+
writableFile,
|
|
196
|
+
loadIntake,
|
|
197
|
+
loadAnswers,
|
|
198
|
+
check,
|
|
199
|
+
plan,
|
|
200
|
+
record,
|
|
201
|
+
render,
|
|
202
|
+
};
|
|
@@ -8,11 +8,90 @@ const crypto = require('node:crypto');
|
|
|
8
8
|
const { safeTarget } = require('../../build/generate-adapters');
|
|
9
9
|
|
|
10
10
|
const fileHash = (content) => crypto.createHash('sha256').update(content).digest('hex');
|
|
11
|
+
const MAX_MANIFEST = 1024 * 1024;
|
|
11
12
|
|
|
12
|
-
|
|
13
|
+
/** The package name comes from the generated registry data, never from user input. */
|
|
14
|
+
function resolveModule(name, projectRoot) {
|
|
15
|
+
return require.resolve(name + '/package.json', { paths: [projectRoot] });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** The node_modules directories Node searches from the BMAD+ package root upwards. */
|
|
19
|
+
function moduleChain(projectRoot) {
|
|
20
|
+
const chain = [];
|
|
21
|
+
let current = fs.realpathSync(projectRoot);
|
|
22
|
+
for (;;) {
|
|
23
|
+
if (path.basename(current) !== 'node_modules') chain.push(path.join(current, 'node_modules'));
|
|
24
|
+
const parent = path.dirname(current);
|
|
25
|
+
if (parent === current) return chain;
|
|
26
|
+
current = parent;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The root of an `external_module` package installed next to BMAD+, once its
|
|
32
|
+
* version is the one the registry pinned. Node resolves through symlinks, so the
|
|
33
|
+
* resolved package must sit in the node_modules chain itself: a link or a global
|
|
34
|
+
* module path is refused.
|
|
35
|
+
*/
|
|
36
|
+
function externalModuleRoot({ externalModule, projectRoot, resolve = resolveModule }) {
|
|
37
|
+
const { package: name, version } = externalModule;
|
|
38
|
+
let manifestPath;
|
|
39
|
+
try {
|
|
40
|
+
manifestPath = resolve(name, projectRoot);
|
|
41
|
+
} catch (error) {
|
|
42
|
+
throw new Error(
|
|
43
|
+
'External module ' + name + ' is not installed next to BMAD+: ' + error.message,
|
|
44
|
+
{ cause: error }
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
const expected = moduleChain(projectRoot).map((dir) =>
|
|
48
|
+
path.join(dir, ...name.split('/'), 'package.json')
|
|
49
|
+
);
|
|
50
|
+
if (!expected.includes(manifestPath)) {
|
|
51
|
+
throw new Error(
|
|
52
|
+
'External module ' +
|
|
53
|
+
name +
|
|
54
|
+
' resolved to ' +
|
|
55
|
+
manifestPath +
|
|
56
|
+
', outside the node_modules chain of this BMAD+. A symlinked package, a global module path and the isolated layout of pnpm (node_modules/.pnpm) are refused: install bmad-plus with npm so its pinned dependency sits in that chain.'
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
const root = path.dirname(manifestPath);
|
|
60
|
+
safeTarget(root, 'package.json');
|
|
61
|
+
const stat = fs.lstatSync(manifestPath);
|
|
62
|
+
if (!stat.isFile() || stat.size > MAX_MANIFEST)
|
|
63
|
+
throw new Error('External module ' + name + ' has no readable package.json');
|
|
64
|
+
let manifest;
|
|
65
|
+
try {
|
|
66
|
+
manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
67
|
+
} catch (error) {
|
|
68
|
+
throw new Error('External module ' + name + ': ' + error.message, { cause: error });
|
|
69
|
+
}
|
|
70
|
+
if (!manifest || manifest.name !== name || manifest.version !== version) {
|
|
71
|
+
throw new Error(
|
|
72
|
+
'External module ' +
|
|
73
|
+
name +
|
|
74
|
+
' is ' +
|
|
75
|
+
String(manifest && manifest.version) +
|
|
76
|
+
' but this BMAD+ expects exactly ' +
|
|
77
|
+
version +
|
|
78
|
+
'; reinstall bmad-plus so its pinned dependency matches.'
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
return root;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function listPackFiles({
|
|
85
|
+
bmadSrc,
|
|
86
|
+
targetAgentsDir,
|
|
87
|
+
targetDataDir,
|
|
88
|
+
projectRoot,
|
|
89
|
+
pack,
|
|
90
|
+
resolveModule: resolve,
|
|
91
|
+
}) {
|
|
13
92
|
const result = { copiedAgents: 0, copiedSkills: 0, copiedFiles: 0, files: [] };
|
|
14
93
|
if (!pack) return result;
|
|
15
|
-
function walk(source, target) {
|
|
94
|
+
function walk(source, target, root) {
|
|
16
95
|
// Local Python execution must not turn interpreter caches into owned/shipped
|
|
17
96
|
// framework files. Keep this aligned with the explicit npm files exclusions.
|
|
18
97
|
if (path.basename(source) === '__pycache__' || /\.py[co]$/i.test(source)) return false;
|
|
@@ -26,9 +105,9 @@ function listPackFiles({ bmadSrc, targetAgentsDir, targetDataDir, projectRoot, p
|
|
|
26
105
|
if (stat.isSymbolicLink()) throw new Error('Pack source is a symbolic link: ' + source);
|
|
27
106
|
if (stat.isDirectory()) {
|
|
28
107
|
for (const name of fs.readdirSync(source).sort())
|
|
29
|
-
walk(path.join(source, name), path.join(target, name));
|
|
108
|
+
walk(path.join(source, name), path.join(target, name), root);
|
|
30
109
|
} else if (stat.isFile()) {
|
|
31
|
-
result.files.push({ source, target });
|
|
110
|
+
result.files.push(root ? { source, target, root } : { source, target });
|
|
32
111
|
} else throw new Error('Pack source is not a regular file: ' + source);
|
|
33
112
|
return true;
|
|
34
113
|
}
|
|
@@ -49,6 +128,17 @@ function listPackFiles({ bmadSrc, targetAgentsDir, targetDataDir, projectRoot, p
|
|
|
49
128
|
walk(path.join(projectRoot, pack.externalPackage, 'skills'), targetAgentsDir)
|
|
50
129
|
)
|
|
51
130
|
result.copiedSkills++;
|
|
131
|
+
if (pack.externalModule) {
|
|
132
|
+
const root = externalModuleRoot({ externalModule: pack.externalModule, projectRoot, resolve });
|
|
133
|
+
for (const name of pack.externalModule.skills) {
|
|
134
|
+
if (!walk(path.join(root, 'skills', name), path.join(targetAgentsDir, name), root)) {
|
|
135
|
+
throw new Error(
|
|
136
|
+
'External module ' + pack.externalModule.package + ' does not ship skill ' + name
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
result.copiedSkills++;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
52
142
|
if (
|
|
53
143
|
pack.packDir &&
|
|
54
144
|
walk(
|
|
@@ -105,4 +195,4 @@ function copyPackFiles(options) {
|
|
|
105
195
|
return counts;
|
|
106
196
|
}
|
|
107
197
|
|
|
108
|
-
module.exports = { copyPackFiles, listPackFiles, copyManagedFile, fileHash };
|
|
198
|
+
module.exports = { copyPackFiles, listPackFiles, copyManagedFile, externalModuleRoot, fileHash };
|
package/tools/cli/lib/packs.js
CHANGED
|
@@ -18,8 +18,10 @@ const PACKS = {
|
|
|
18
18
|
],
|
|
19
19
|
"skills": [
|
|
20
20
|
"bmad-plus-autopilot",
|
|
21
|
+
"bmad-plus-intake",
|
|
21
22
|
"bmad-plus-parallel",
|
|
22
23
|
"bmad-plus-sync",
|
|
24
|
+
"bmad-plus-swarm",
|
|
23
25
|
"bmad-plus-uat"
|
|
24
26
|
],
|
|
25
27
|
"data": [
|
|
@@ -55,7 +57,7 @@ const PACKS = {
|
|
|
55
57
|
"skills": [],
|
|
56
58
|
"packDir": "pack-shield",
|
|
57
59
|
"packSrcDir": "packs",
|
|
58
|
-
"desc": "GRC compliance (
|
|
60
|
+
"desc": "GRC compliance (27 frameworks)"
|
|
59
61
|
},
|
|
60
62
|
"seo": {
|
|
61
63
|
"name": "SEO",
|
|
@@ -103,6 +105,25 @@ const PACKS = {
|
|
|
103
105
|
"packDir": "pack-animated",
|
|
104
106
|
"packSrcDir": "packs",
|
|
105
107
|
"desc": "Animated website agents"
|
|
108
|
+
},
|
|
109
|
+
"design": {
|
|
110
|
+
"name": "Design",
|
|
111
|
+
"icon": "d",
|
|
112
|
+
"agents": [],
|
|
113
|
+
"skills": [],
|
|
114
|
+
"externalModule": {
|
|
115
|
+
"package": "design-skill",
|
|
116
|
+
"version": "0.2.1",
|
|
117
|
+
"skills": [
|
|
118
|
+
"design",
|
|
119
|
+
"design-review",
|
|
120
|
+
"redesign"
|
|
121
|
+
]
|
|
122
|
+
},
|
|
123
|
+
"packDir": "pack-design",
|
|
124
|
+
"packSrcDir": "packs",
|
|
125
|
+
"intake": "intake.yaml",
|
|
126
|
+
"desc": "Design engine: build, review and redesign interfaces"
|
|
106
127
|
}
|
|
107
128
|
};
|
|
108
129
|
|
|
@@ -115,7 +136,8 @@ const PACK_ORDER = [
|
|
|
115
136
|
"memory",
|
|
116
137
|
"dev-studio",
|
|
117
138
|
"backup",
|
|
118
|
-
"animated"
|
|
139
|
+
"animated",
|
|
140
|
+
"design"
|
|
119
141
|
];
|
|
120
142
|
|
|
121
143
|
const EXPECTED_AGENTS = {
|
|
@@ -184,6 +206,13 @@ const EXPECTED_AGENTS = {
|
|
|
184
206
|
"packAgents": [
|
|
185
207
|
"animated-website-agent.md"
|
|
186
208
|
]
|
|
209
|
+
},
|
|
210
|
+
"design": {
|
|
211
|
+
"agents": [],
|
|
212
|
+
"packDir": "pack-design",
|
|
213
|
+
"packAgents": [
|
|
214
|
+
"design-engine.md"
|
|
215
|
+
]
|
|
187
216
|
}
|
|
188
217
|
};
|
|
189
218
|
|
|
@@ -191,7 +220,7 @@ const DERIVED = {
|
|
|
191
220
|
"product": {
|
|
192
221
|
"code": "bmad-plus",
|
|
193
222
|
"displayName": "BMAD+",
|
|
194
|
-
"version": "0.
|
|
223
|
+
"version": "0.23.1",
|
|
195
224
|
"derivedFrom": "BMAD-METHOD v6.6.0"
|
|
196
225
|
},
|
|
197
226
|
"packOrder": [
|
|
@@ -203,11 +232,12 @@ const DERIVED = {
|
|
|
203
232
|
"memory",
|
|
204
233
|
"dev-studio",
|
|
205
234
|
"backup",
|
|
206
|
-
"animated"
|
|
235
|
+
"animated",
|
|
236
|
+
"design"
|
|
207
237
|
],
|
|
208
|
-
"packCount":
|
|
209
|
-
"installerAgents":
|
|
210
|
-
"totalAgents":
|
|
238
|
+
"packCount": 10,
|
|
239
|
+
"installerAgents": 15,
|
|
240
|
+
"totalAgents": 48,
|
|
211
241
|
"languages": [
|
|
212
242
|
"en",
|
|
213
243
|
"fr",
|
|
@@ -235,7 +265,7 @@ const DERIVED = {
|
|
|
235
265
|
"workflowCount": 0,
|
|
236
266
|
"frameworkCount": 0,
|
|
237
267
|
"categoryCount": 0,
|
|
238
|
-
"skillCount":
|
|
268
|
+
"skillCount": 6,
|
|
239
269
|
"referenceFiles": 0,
|
|
240
270
|
"categoryAgentCounts": [],
|
|
241
271
|
"runtimes": [
|
|
@@ -247,8 +277,10 @@ const DERIVED = {
|
|
|
247
277
|
".agents/skills/agent-quality/SKILL.md",
|
|
248
278
|
".agents/skills/agent-orchestrator/SKILL.md",
|
|
249
279
|
".agents/skills/bmad-plus-autopilot/SKILL.md",
|
|
280
|
+
".agents/skills/bmad-plus-intake/SKILL.md",
|
|
250
281
|
".agents/skills/bmad-plus-parallel/SKILL.md",
|
|
251
282
|
".agents/skills/bmad-plus-sync/SKILL.md",
|
|
283
|
+
".agents/skills/bmad-plus-swarm/SKILL.md",
|
|
252
284
|
".agents/skills/bmad-plus-uat/SKILL.md",
|
|
253
285
|
".agents/data/role-triggers.yaml"
|
|
254
286
|
],
|
|
@@ -364,7 +396,7 @@ const DERIVED = {
|
|
|
364
396
|
"agentCount": 27,
|
|
365
397
|
"subAgentCount": 0,
|
|
366
398
|
"workflowCount": 11,
|
|
367
|
-
"frameworkCount":
|
|
399
|
+
"frameworkCount": 27,
|
|
368
400
|
"categoryCount": 6,
|
|
369
401
|
"skillCount": 0,
|
|
370
402
|
"referenceFiles": 79,
|
|
@@ -392,9 +424,9 @@ const DERIVED = {
|
|
|
392
424
|
"description": "Compliance agents for GDPR, ISO 27001, SOC 2, HIPAA, EU AI Act, DORA, NIS2 and more"
|
|
393
425
|
}
|
|
394
426
|
],
|
|
395
|
-
"summary": "27 compliance agents + 11 workflows across
|
|
396
|
-
"desc": "GRC compliance (
|
|
397
|
-
"description": "GRC compliance (
|
|
427
|
+
"summary": "27 compliance agents + 11 workflows across 27+ frameworks.",
|
|
428
|
+
"desc": "GRC compliance (27 frameworks)",
|
|
429
|
+
"description": "GRC compliance (27 frameworks)"
|
|
398
430
|
},
|
|
399
431
|
"seo": {
|
|
400
432
|
"id": "seo",
|
|
@@ -481,6 +513,7 @@ const DERIVED = {
|
|
|
481
513
|
"alias": "זכר, Memory Guardian"
|
|
482
514
|
}
|
|
483
515
|
],
|
|
516
|
+
"cohabitationWarning": "Detects and links existing brains (_brain/, ~/.claude/memory/); NEVER overwrites memory files.",
|
|
484
517
|
"summary": "Cross-session memory + project scanner + Karpathy guardrails.",
|
|
485
518
|
"desc": "Persistent cross-session memory",
|
|
486
519
|
"description": "Persistent cross-session memory"
|
|
@@ -564,6 +597,7 @@ const DERIVED = {
|
|
|
564
597
|
"alias": "אהליאב"
|
|
565
598
|
}
|
|
566
599
|
],
|
|
600
|
+
"cohabitationWarning": "Dev Studio agents overlap with Core (Atlas/Forge/Sentinel/Nexus); both can coexist.",
|
|
567
601
|
"summary": "6 specialized agents + 38 workflows: Analysis → Planning → Architecture → Implementation → Review.",
|
|
568
602
|
"desc": "SDLC automation (6 agents, specialized workflows)",
|
|
569
603
|
"description": "SDLC automation (6 agents, specialized workflows)"
|
|
@@ -637,6 +671,62 @@ const DERIVED = {
|
|
|
637
671
|
"summary": "Luxury scroll-driven website from video.",
|
|
638
672
|
"desc": "Animated website agents",
|
|
639
673
|
"description": "Animated website agents"
|
|
674
|
+
},
|
|
675
|
+
"design": {
|
|
676
|
+
"id": "design",
|
|
677
|
+
"order": 9,
|
|
678
|
+
"name": "Design",
|
|
679
|
+
"displayName": "Design",
|
|
680
|
+
"iconEmoji": "🎨",
|
|
681
|
+
"required": false,
|
|
682
|
+
"installerAgentCount": 1,
|
|
683
|
+
"categoryAgentCount": 0,
|
|
684
|
+
"agentCount": 1,
|
|
685
|
+
"subAgentCount": 0,
|
|
686
|
+
"workflowCount": 0,
|
|
687
|
+
"frameworkCount": 0,
|
|
688
|
+
"categoryCount": 0,
|
|
689
|
+
"skillCount": 0,
|
|
690
|
+
"referenceFiles": 0,
|
|
691
|
+
"categoryAgentCounts": [],
|
|
692
|
+
"runtimes": [
|
|
693
|
+
"node"
|
|
694
|
+
],
|
|
695
|
+
"requiredResources": [
|
|
696
|
+
".agents/skills/pack-design/design-engine.md",
|
|
697
|
+
".agents/skills/design/SKILL.md",
|
|
698
|
+
".agents/skills/design-review/SKILL.md",
|
|
699
|
+
".agents/skills/redesign/SKILL.md",
|
|
700
|
+
".agents/skills/pack-design/SKILL.md",
|
|
701
|
+
".agents/skills/pack-design/intake.yaml"
|
|
702
|
+
],
|
|
703
|
+
"personas": [
|
|
704
|
+
{
|
|
705
|
+
"id": "design-engine",
|
|
706
|
+
"name": "Atelier",
|
|
707
|
+
"role": "Design Engine",
|
|
708
|
+
"description": "Senior product designer and design engineer: surfaces, tokens, accessibility, audits, redesigns",
|
|
709
|
+
"usesSkills": [
|
|
710
|
+
"design",
|
|
711
|
+
"design-review",
|
|
712
|
+
"redesign"
|
|
713
|
+
]
|
|
714
|
+
}
|
|
715
|
+
],
|
|
716
|
+
"externalModule": {
|
|
717
|
+
"package": "design-skill",
|
|
718
|
+
"version": "0.2.1",
|
|
719
|
+
"skills": [
|
|
720
|
+
"design",
|
|
721
|
+
"design-review",
|
|
722
|
+
"redesign"
|
|
723
|
+
]
|
|
724
|
+
},
|
|
725
|
+
"intake": "intake.yaml",
|
|
726
|
+
"cohabitationWarning": "Overlaps Sentinel's UX-review role and Dev Studio's Rachel (UX Designer); when this pack is installed its rules govern visual design and accessibility, and the others hand those over.",
|
|
727
|
+
"summary": "Atelier builds, audits and redesigns interfaces under checkable rules: intake first, semantic tokens, WCAG AA, every state covered, pre-flight before delivery.",
|
|
728
|
+
"desc": "Design engine: build, review and redesign interfaces",
|
|
729
|
+
"description": "Design engine: build, review and redesign interfaces"
|
|
640
730
|
}
|
|
641
731
|
},
|
|
642
732
|
"pythonPacks": {
|
package/tools/cli/lib/review.js
CHANGED
|
@@ -221,7 +221,8 @@ function contentDigest(projectDir, file) {
|
|
|
221
221
|
*/
|
|
222
222
|
function buildScope(projectDir, options = {}) {
|
|
223
223
|
const head = options.workspace ? null : resolveCommit(projectDir, options.head || 'HEAD');
|
|
224
|
-
|
|
224
|
+
// A workspace review covers the uncommitted work, so its default base is HEAD.
|
|
225
|
+
const base = resolveCommit(projectDir, options.base || (options.workspace ? 'HEAD' : 'HEAD~1'));
|
|
225
226
|
const mergeBase = git(projectDir, ['merge-base', base, head || 'HEAD']).trim();
|
|
226
227
|
const range = options.workspace ? [mergeBase] : [mergeBase, head];
|
|
227
228
|
const numstat = parseNumstat(
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/** Deterministic dealing: the same profile, n and seed give the same cards, on any platform. */
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const crypto = require('node:crypto');
|
|
5
|
+
|
|
6
|
+
const WORD = 2 ** 32;
|
|
7
|
+
|
|
8
|
+
/** A counter-mode generator over SHA-256: the stream depends on the seed alone. */
|
|
9
|
+
class Dealer {
|
|
10
|
+
constructor(seed) {
|
|
11
|
+
this.seed = String(seed);
|
|
12
|
+
this.block = 0;
|
|
13
|
+
this.words = [];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
next() {
|
|
17
|
+
if (!this.words.length) {
|
|
18
|
+
const digest = crypto
|
|
19
|
+
.createHash('sha256')
|
|
20
|
+
.update(this.seed + ':' + this.block)
|
|
21
|
+
.digest();
|
|
22
|
+
this.block += 1;
|
|
23
|
+
for (let offset = 0; offset < digest.length; offset += 4)
|
|
24
|
+
this.words.push(digest.readUInt32BE(offset));
|
|
25
|
+
}
|
|
26
|
+
return this.words.shift();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Uniform integer in [0, n): values above the last full cycle are rejected, never folded. */
|
|
30
|
+
below(n) {
|
|
31
|
+
const limit = WORD - (WORD % n);
|
|
32
|
+
let value = this.next();
|
|
33
|
+
while (value >= limit) value = this.next();
|
|
34
|
+
return value % n;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
between(low, high) {
|
|
38
|
+
if (Number.isInteger(low) && Number.isInteger(high)) return low + this.below(high - low + 1);
|
|
39
|
+
return Math.round((low + (this.next() / WORD) * (high - low)) * 100) / 100;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
shuffle(items) {
|
|
43
|
+
for (let index = items.length - 1; index > 0; index -= 1) {
|
|
44
|
+
const other = this.below(index + 1);
|
|
45
|
+
[items[index], items[other]] = [items[other], items[index]];
|
|
46
|
+
}
|
|
47
|
+
return items;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Largest-remainder quotas: the counts add up to n exactly and follow the shares. */
|
|
52
|
+
function quotas(shares, n) {
|
|
53
|
+
const exact = shares.map((share) => share * n);
|
|
54
|
+
const counts = exact.map((value) => Math.floor(value));
|
|
55
|
+
let left = n - counts.reduce((sum, count) => sum + count, 0);
|
|
56
|
+
const order = exact
|
|
57
|
+
.map((value, index) => ({ index, remainder: value - Math.floor(value) }))
|
|
58
|
+
.sort((a, b) => b.remainder - a.remainder || a.index - b.index);
|
|
59
|
+
for (const { index } of order) {
|
|
60
|
+
if (left === 0) break;
|
|
61
|
+
counts[index] += 1;
|
|
62
|
+
left -= 1;
|
|
63
|
+
}
|
|
64
|
+
return counts;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function unitId(prefix, position, n) {
|
|
68
|
+
return prefix + String(position).padStart(Math.max(3, String(n).length), '0');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function dealCards(profile, n, seed, idPrefix) {
|
|
72
|
+
const dealer = new Dealer(seed);
|
|
73
|
+
const slots = profile.dimensions.map((dimension) => {
|
|
74
|
+
const counts = quotas(
|
|
75
|
+
dimension.values.map((entry) => entry.share),
|
|
76
|
+
n
|
|
77
|
+
);
|
|
78
|
+
const column = [];
|
|
79
|
+
counts.forEach((count, index) => {
|
|
80
|
+
for (let k = 0; k < count; k += 1) column.push(index);
|
|
81
|
+
});
|
|
82
|
+
return dealer.shuffle(column);
|
|
83
|
+
});
|
|
84
|
+
const cards = [];
|
|
85
|
+
for (let position = 0; position < n; position += 1) {
|
|
86
|
+
const card = { id: unitId(idPrefix, position + 1, n) };
|
|
87
|
+
profile.dimensions.forEach((dimension, index) => {
|
|
88
|
+
const entry = dimension.values[slots[index][position]];
|
|
89
|
+
card[dimension.name] = entry.value;
|
|
90
|
+
// A note is a field of the whole dimension, so every card carries it, empty or not.
|
|
91
|
+
if (dimension.values.some((value) => value.note !== null))
|
|
92
|
+
card[dimension.name + '_note'] = entry.note ?? '';
|
|
93
|
+
for (const [name, [low, high]] of Object.entries(entry.ranges))
|
|
94
|
+
card[name] = dealer.between(low, high);
|
|
95
|
+
});
|
|
96
|
+
for (const [name, [low, high]] of Object.entries(profile.ranges))
|
|
97
|
+
card[name] = dealer.between(low, high);
|
|
98
|
+
for (const [name, pool] of Object.entries(profile.pools))
|
|
99
|
+
card[name] = pool[dealer.below(pool.length)];
|
|
100
|
+
cards.push(card);
|
|
101
|
+
}
|
|
102
|
+
return cards;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function listCards(units, idPrefix) {
|
|
106
|
+
const cards = units.map((unit, index) => ({
|
|
107
|
+
...unit,
|
|
108
|
+
id: unit.id ?? unitId(idPrefix, index + 1, units.length),
|
|
109
|
+
}));
|
|
110
|
+
// Answer files are named by id, and some file systems fold case.
|
|
111
|
+
if (new Set(cards.map((card) => card.id.toLowerCase())).size !== cards.length)
|
|
112
|
+
throw new Error('Swarm: jury unit ids must be distinct once dealt, whatever their case.');
|
|
113
|
+
return cards;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
module.exports = { Dealer, quotas, unitId, dealCards, listCards };
|