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
package/tools/build/generate.js
CHANGED
|
@@ -26,6 +26,7 @@ const fs = require('node:fs');
|
|
|
26
26
|
const path = require('node:path');
|
|
27
27
|
const { isDeepStrictEqual } = require('node:util');
|
|
28
28
|
const yaml = require('js-yaml');
|
|
29
|
+
const { validateIntake } = require('./intake-contract');
|
|
29
30
|
|
|
30
31
|
const REPO_ROOT = path.join(__dirname, '..', '..');
|
|
31
32
|
const DEFAULT_REGISTRY_PATH = path.join(REPO_ROOT, 'registry.yaml');
|
|
@@ -39,6 +40,10 @@ const OWNED_COUNT_RE =
|
|
|
39
40
|
/(?<![\w.-])\d+\+?[\s-]+(?:[A-Za-zÀ-ÿ-]+\s+){0,6}(?:agents?|skills?|workflows?|packs?|frameworks?|languages?|reference files)\b/gi;
|
|
40
41
|
const STANDARD_PREFIX_RE =
|
|
41
42
|
/\b(?:ISO(?:\/IEC)?|IEC|SOC|NIST(?: SP)?|PCI(?: DSS)?|WCAG|Section)\s*$/i;
|
|
43
|
+
const SKILL_ID_RE = /^[a-z][a-z0-9-]*$/;
|
|
44
|
+
const PACKAGE_NAME_RE = /^(?:@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/;
|
|
45
|
+
const EXACT_VERSION_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
|
|
46
|
+
const PIPELINE_GATES = ['require_approval', 'notify_only', 'auto'];
|
|
42
47
|
|
|
43
48
|
function hasOwnedCount(value) {
|
|
44
49
|
return [...value.matchAll(OWNED_COUNT_RE)].some(
|
|
@@ -60,13 +65,16 @@ function assertResourcePath(file, where) {
|
|
|
60
65
|
/** Installed entry points, derived from the same layout used by pack-copy.js. */
|
|
61
66
|
function buildRequiredResources(pack, id = pack.pack_dir) {
|
|
62
67
|
const where = `registry.yaml: pack "${id}"`;
|
|
68
|
+
const externalSkills = (pack.external_module || {}).skills || [];
|
|
63
69
|
for (const file of [
|
|
64
70
|
...pack.agents,
|
|
65
71
|
...(pack.skills || []),
|
|
72
|
+
...externalSkills,
|
|
66
73
|
...(pack.data || []),
|
|
67
74
|
...((pack.doctor || {}).pack_agents || []),
|
|
68
75
|
...((pack.doctor || {}).required_resources || []),
|
|
69
76
|
...(pack.python_entry ? [pack.python_entry] : []),
|
|
77
|
+
...(pack.intake ? [pack.intake] : []),
|
|
70
78
|
]) {
|
|
71
79
|
assertResourcePath(file, where);
|
|
72
80
|
}
|
|
@@ -79,6 +87,7 @@ function buildRequiredResources(pack, id = pack.pack_dir) {
|
|
|
79
87
|
);
|
|
80
88
|
}
|
|
81
89
|
resources.push(...(pack.skills || []).map((id) => `.agents/skills/${id}/SKILL.md`));
|
|
90
|
+
resources.push(...externalSkills.map((id) => `.agents/skills/${id}/SKILL.md`));
|
|
82
91
|
resources.push(...(pack.data || []).map((file) => `.agents/data/${file}`));
|
|
83
92
|
resources.push(
|
|
84
93
|
...((pack.doctor || {}).required_resources || []).map(
|
|
@@ -86,10 +95,137 @@ function buildRequiredResources(pack, id = pack.pack_dir) {
|
|
|
86
95
|
)
|
|
87
96
|
);
|
|
88
97
|
if (pack.python_entry) resources.push(`.agents/skills/${pack.pack_dir}/${pack.python_entry}`);
|
|
98
|
+
if (pack.intake) resources.push(`.agents/skills/${pack.pack_dir}/${pack.intake}`);
|
|
89
99
|
for (const file of resources) assertResourcePath(file, where);
|
|
90
100
|
return [...new Set(resources)];
|
|
91
101
|
}
|
|
92
102
|
|
|
103
|
+
function assertSkillIds(list, where) {
|
|
104
|
+
if (
|
|
105
|
+
!Array.isArray(list) ||
|
|
106
|
+
list.length === 0 ||
|
|
107
|
+
list.some((id) => typeof id !== 'string' || !SKILL_ID_RE.test(id)) ||
|
|
108
|
+
new Set(list).size !== list.length
|
|
109
|
+
) {
|
|
110
|
+
throw new Error(`${where}: must be a non-empty list of distinct skill ids`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Structural contract of `external_module` (§5.2); the package itself is checked by --check. */
|
|
115
|
+
function validateExternalModule(pack, where) {
|
|
116
|
+
const module = pack.external_module;
|
|
117
|
+
if (module === undefined) return;
|
|
118
|
+
if (
|
|
119
|
+
!module ||
|
|
120
|
+
typeof module !== 'object' ||
|
|
121
|
+
Object.keys(module).some((key) => !['package', 'version', 'skills'].includes(key))
|
|
122
|
+
) {
|
|
123
|
+
throw new Error(`${where}: external_module takes exactly package, version and skills`);
|
|
124
|
+
}
|
|
125
|
+
if (typeof module.package !== 'string' || !PACKAGE_NAME_RE.test(module.package)) {
|
|
126
|
+
throw new Error(`${where}: external_module.package must be an npm package name`);
|
|
127
|
+
}
|
|
128
|
+
if (typeof module.version !== 'string' || !EXACT_VERSION_RE.test(module.version)) {
|
|
129
|
+
throw new Error(`${where}: external_module.version must be an exact version`);
|
|
130
|
+
}
|
|
131
|
+
assertSkillIds(module.skills, `${where}: external_module.skills`);
|
|
132
|
+
for (const id of module.skills) {
|
|
133
|
+
if ((pack.skills || []).includes(id)) {
|
|
134
|
+
throw new Error(`${where}: skill ${id} is both shipped in-tree and taken from the module`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Structural contract of `pipelines` (§5.3): every step names one real thing of the pack. */
|
|
140
|
+
function validatePipelines(pack, where) {
|
|
141
|
+
if (pack.pipelines === undefined) return;
|
|
142
|
+
if (!Array.isArray(pack.pipelines) || pack.pipelines.length === 0) {
|
|
143
|
+
throw new Error(`${where}: pipelines must be a non-empty list`);
|
|
144
|
+
}
|
|
145
|
+
const workflows = new Set([
|
|
146
|
+
...(pack.workflows || []),
|
|
147
|
+
...(pack.categories || []).flatMap((category) => category.workflows || []),
|
|
148
|
+
]);
|
|
149
|
+
const agents = new Set([...pack.agents, ...(pack.sub_agents || [])]);
|
|
150
|
+
const ids = new Set();
|
|
151
|
+
for (const pipeline of pack.pipelines) {
|
|
152
|
+
if (
|
|
153
|
+
!pipeline ||
|
|
154
|
+
typeof pipeline !== 'object' ||
|
|
155
|
+
Object.keys(pipeline).some((key) => !['id', 'title', 'steps'].includes(key))
|
|
156
|
+
) {
|
|
157
|
+
throw new Error(`${where}: each pipeline takes exactly id, title and steps`);
|
|
158
|
+
}
|
|
159
|
+
if (typeof pipeline.id !== 'string' || !SKILL_ID_RE.test(pipeline.id) || ids.has(pipeline.id)) {
|
|
160
|
+
throw new Error(`${where}: pipeline ids must be distinct lowercase slugs`);
|
|
161
|
+
}
|
|
162
|
+
ids.add(pipeline.id);
|
|
163
|
+
const label = `${where}: pipeline ${pipeline.id}`;
|
|
164
|
+
if (typeof pipeline.title !== 'string' || !pipeline.title.trim()) {
|
|
165
|
+
throw new Error(`${label}: needs a title`);
|
|
166
|
+
}
|
|
167
|
+
if (!Array.isArray(pipeline.steps) || pipeline.steps.length === 0) {
|
|
168
|
+
throw new Error(`${label}: needs at least one step`);
|
|
169
|
+
}
|
|
170
|
+
pipeline.steps.forEach((step, index) => {
|
|
171
|
+
const at = `${label}: step ${index + 1}`;
|
|
172
|
+
if (
|
|
173
|
+
!step ||
|
|
174
|
+
typeof step !== 'object' ||
|
|
175
|
+
Object.keys(step).some((key) => !['workflow', 'agent', 'gate'].includes(key))
|
|
176
|
+
) {
|
|
177
|
+
throw new Error(`${at}: takes workflow or agent, and gate`);
|
|
178
|
+
}
|
|
179
|
+
if ((step.workflow === undefined) === (step.agent === undefined)) {
|
|
180
|
+
throw new Error(`${at}: names exactly one of workflow or agent`);
|
|
181
|
+
}
|
|
182
|
+
if (step.workflow !== undefined && !workflows.has(step.workflow)) {
|
|
183
|
+
throw new Error(`${at}: workflow ${step.workflow} is not declared by the pack`);
|
|
184
|
+
}
|
|
185
|
+
if (step.agent !== undefined && !agents.has(step.agent)) {
|
|
186
|
+
throw new Error(`${at}: agent ${step.agent} is not declared by the pack`);
|
|
187
|
+
}
|
|
188
|
+
if (!PIPELINE_GATES.includes(step.gate)) {
|
|
189
|
+
throw new Error(`${at}: gate must be one of ${PIPELINE_GATES.join('|')}`);
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** `intake` names a file inside the pack directory (§5.4); its content is checked by --check. */
|
|
196
|
+
function validateIntakeField(pack, where) {
|
|
197
|
+
if (pack.intake === undefined) return;
|
|
198
|
+
if (typeof pack.intake !== 'string' || !pack.intake) {
|
|
199
|
+
throw new Error(`${where}: intake must be a file path relative to pack_dir`);
|
|
200
|
+
}
|
|
201
|
+
if (!pack.pack_dir) throw new Error(`${where}: intake needs a pack_dir to live in`);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Every `uses_skills` id (§5.1) is a skill some pack ships in-tree or through its module. */
|
|
205
|
+
function validateUsesSkills(registry) {
|
|
206
|
+
const known = new Set();
|
|
207
|
+
for (const pack of Object.values(registry.packs)) {
|
|
208
|
+
for (const id of pack.skills || []) known.add(id);
|
|
209
|
+
for (const id of (pack.external_module || {}).skills || []) known.add(id);
|
|
210
|
+
}
|
|
211
|
+
const resolve = (list, where) => {
|
|
212
|
+
if (list === undefined) return;
|
|
213
|
+
assertSkillIds(list, where);
|
|
214
|
+
for (const id of list) {
|
|
215
|
+
if (!known.has(id)) throw new Error(`${where}: skill ${id} is shipped by no pack`);
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
for (const [id, pack] of Object.entries(registry.packs)) {
|
|
219
|
+
resolve(pack.uses_skills, `registry.yaml: pack "${id}": uses_skills`);
|
|
220
|
+
for (const persona of pack.personas || []) {
|
|
221
|
+
resolve(
|
|
222
|
+
persona.uses_skills,
|
|
223
|
+
`registry.yaml: pack "${id}": persona ${persona.id}: uses_skills`
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
93
229
|
/**
|
|
94
230
|
* Validate the minimal contract the generator relies on.
|
|
95
231
|
* Throws with a precise message on the first violation.
|
|
@@ -160,6 +296,15 @@ function validateRegistry(registry) {
|
|
|
160
296
|
) {
|
|
161
297
|
throw new Error(`${where}: runtime must declare known runtimes`);
|
|
162
298
|
}
|
|
299
|
+
validateExternalModule(pack, where);
|
|
300
|
+
validatePipelines(pack, where);
|
|
301
|
+
validateIntakeField(pack, where);
|
|
302
|
+
if (
|
|
303
|
+
pack.cohabitation_warning !== undefined &&
|
|
304
|
+
(typeof pack.cohabitation_warning !== 'string' || !pack.cohabitation_warning.trim())
|
|
305
|
+
) {
|
|
306
|
+
throw new Error(`${where}: cohabitation_warning must be a non-empty sentence`);
|
|
307
|
+
}
|
|
163
308
|
buildRequiredResources(pack, id);
|
|
164
309
|
if (pack.personas !== undefined) {
|
|
165
310
|
if (!Array.isArray(pack.personas)) throw new Error(`${where}: personas must be a list`);
|
|
@@ -183,6 +328,7 @@ function validateRegistry(registry) {
|
|
|
183
328
|
}
|
|
184
329
|
}
|
|
185
330
|
}
|
|
331
|
+
validateUsesSkills(registry);
|
|
186
332
|
const integration = registry.targets?.integration;
|
|
187
333
|
if (
|
|
188
334
|
integration?.execution !== 'host-managed' ||
|
|
@@ -242,6 +388,21 @@ function buildPackOrder(registry) {
|
|
|
242
388
|
return sortedPackEntries(registry).map(([id]) => id);
|
|
243
389
|
}
|
|
244
390
|
|
|
391
|
+
function externalModuleFacts({ package: name, version, skills }) {
|
|
392
|
+
return { package: name, version, skills: [...skills] };
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function pipelineFacts(pipelines) {
|
|
396
|
+
return pipelines.map(({ id, title, steps }) => ({
|
|
397
|
+
id,
|
|
398
|
+
title,
|
|
399
|
+
steps: steps.map((step) => ({
|
|
400
|
+
...(step.workflow !== undefined ? { workflow: step.workflow } : { agent: step.agent }),
|
|
401
|
+
gate: step.gate,
|
|
402
|
+
})),
|
|
403
|
+
}));
|
|
404
|
+
}
|
|
405
|
+
|
|
245
406
|
/**
|
|
246
407
|
* Generate the PACKS object with the exact shape of tools/cli/lib/packs.js.
|
|
247
408
|
* Key-presence rules mirror the hand-written module:
|
|
@@ -262,10 +423,14 @@ function buildPacks(registry) {
|
|
|
262
423
|
};
|
|
263
424
|
if ('data' in p) entry.data = [...p.data];
|
|
264
425
|
if (p.external_package) entry.externalPackage = p.external_package;
|
|
426
|
+
if (p.external_module) entry.externalModule = externalModuleFacts(p.external_module);
|
|
265
427
|
if (p.pack_dir) {
|
|
266
428
|
entry.packDir = p.pack_dir;
|
|
267
429
|
entry.packSrcDir = p.pack_src_dir || 'packs';
|
|
268
430
|
}
|
|
431
|
+
if (p.uses_skills) entry.usesSkills = [...p.uses_skills];
|
|
432
|
+
if (p.pipelines) entry.pipelines = pipelineFacts(p.pipelines);
|
|
433
|
+
if (p.intake) entry.intake = p.intake;
|
|
269
434
|
if (p.required === true) entry.required = true;
|
|
270
435
|
entry.desc = derived.packs[id].desc;
|
|
271
436
|
packs[id] = entry;
|
|
@@ -302,6 +467,19 @@ function renderSummary(pack, derived, field = 'summary') {
|
|
|
302
467
|
});
|
|
303
468
|
}
|
|
304
469
|
|
|
470
|
+
/**
|
|
471
|
+
* The package flows of the data-handling doctrine that can leave the machine, with their
|
|
472
|
+
* consent basis; maintainer services are not installed and stay out. verify-egress.js owns
|
|
473
|
+
* the doctrine's schema and its agreement with the code.
|
|
474
|
+
*/
|
|
475
|
+
function buildDataHandling(registry) {
|
|
476
|
+
const doctrine = registry.data_handling || {};
|
|
477
|
+
const services = new Set((doctrine.services || []).map((service) => service.id));
|
|
478
|
+
return (doctrine.flows || [])
|
|
479
|
+
.filter((flow) => flow.egress !== 'none' && !services.has(flow.component))
|
|
480
|
+
.map((flow) => ({ id: flow.id, component: flow.component, consent: flow.consent?.basis }));
|
|
481
|
+
}
|
|
482
|
+
|
|
305
483
|
/** The one derivation engine; its JSON result is shipped for runtime consumers. */
|
|
306
484
|
function buildDerived(registry, { sourceRoot = path.join(REPO_ROOT, 'src', 'bmad-plus') } = {}) {
|
|
307
485
|
const packs = {};
|
|
@@ -339,13 +517,23 @@ function buildDerived(registry, { sourceRoot = path.join(REPO_ROOT, 'src', 'bmad
|
|
|
339
517
|
categoryAgentCounts,
|
|
340
518
|
runtimes: [...pack.runtime],
|
|
341
519
|
requiredResources: buildRequiredResources(pack, id),
|
|
342
|
-
personas: (pack.personas || []).map(
|
|
343
|
-
id,
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
520
|
+
personas: (pack.personas || []).map(
|
|
521
|
+
({ id, name, role, description, alias, uses_skills }) => ({
|
|
522
|
+
id,
|
|
523
|
+
name,
|
|
524
|
+
role,
|
|
525
|
+
description,
|
|
526
|
+
...(alias ? { alias } : {}),
|
|
527
|
+
...(uses_skills ? { usesSkills: [...uses_skills] } : {}),
|
|
528
|
+
})
|
|
529
|
+
),
|
|
530
|
+
...(pack.uses_skills ? { usesSkills: [...pack.uses_skills] } : {}),
|
|
531
|
+
...(pack.external_module
|
|
532
|
+
? { externalModule: externalModuleFacts(pack.external_module) }
|
|
533
|
+
: {}),
|
|
534
|
+
...(pack.pipelines ? { pipelines: pipelineFacts(pack.pipelines) } : {}),
|
|
535
|
+
...(pack.intake ? { intake: pack.intake } : {}),
|
|
536
|
+
...(pack.cohabitation_warning ? { cohabitationWarning: pack.cohabitation_warning } : {}),
|
|
349
537
|
};
|
|
350
538
|
facts.summary = renderSummary(pack, facts);
|
|
351
539
|
facts.desc = renderSummary(pack, facts, 'description');
|
|
@@ -381,6 +569,7 @@ function buildDerived(registry, { sourceRoot = path.join(REPO_ROOT, 'src', 'bmad
|
|
|
381
569
|
languages: Object.keys(LANGUAGES),
|
|
382
570
|
packs,
|
|
383
571
|
pythonPacks,
|
|
572
|
+
dataHandling: buildDataHandling(registry),
|
|
384
573
|
diagnostics: {
|
|
385
574
|
schemaVersion: 1,
|
|
386
575
|
processExecution: registry.targets.optional_process_backend || null,
|
|
@@ -562,6 +751,88 @@ function checkSourcePaths(packs, packageRoot = REPO_ROOT) {
|
|
|
562
751
|
return missing;
|
|
563
752
|
}
|
|
564
753
|
|
|
754
|
+
function resolvePackageManifest(name, packageRoot) {
|
|
755
|
+
return require.resolve(name + '/package.json', { paths: [packageRoot] });
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
/**
|
|
759
|
+
* Every `external_module` is an exact pinned dependency, resolvable from the
|
|
760
|
+
* repository root at the declared version, and ships each listed skill.
|
|
761
|
+
*/
|
|
762
|
+
function checkExternalModules(
|
|
763
|
+
registry,
|
|
764
|
+
{ packageRoot = REPO_ROOT, packageJson, resolve = resolvePackageManifest } = {}
|
|
765
|
+
) {
|
|
766
|
+
const problems = [];
|
|
767
|
+
const dependencies = (packageJson && packageJson.dependencies) || {};
|
|
768
|
+
const declared = new Set((registry.data_handling || {}).runtime_dependencies || []);
|
|
769
|
+
for (const [id, pack] of Object.entries(registry.packs)) {
|
|
770
|
+
const module = pack.external_module;
|
|
771
|
+
if (!module) continue;
|
|
772
|
+
const where = `PACKS.${id}: external_module ${module.package}`;
|
|
773
|
+
if (dependencies[module.package] !== module.version) {
|
|
774
|
+
problems.push(
|
|
775
|
+
`${where}: package.json must pin dependencies["${module.package}"] to exactly "${module.version}" (found ${JSON.stringify(dependencies[module.package])})`
|
|
776
|
+
);
|
|
777
|
+
}
|
|
778
|
+
if (!declared.has(module.package)) {
|
|
779
|
+
problems.push(`${where}: missing from data_handling.runtime_dependencies`);
|
|
780
|
+
}
|
|
781
|
+
let manifestPath;
|
|
782
|
+
try {
|
|
783
|
+
manifestPath = resolve(module.package, packageRoot);
|
|
784
|
+
} catch (error) {
|
|
785
|
+
problems.push(`${where}: cannot be resolved from the repository root (${error.message})`);
|
|
786
|
+
continue;
|
|
787
|
+
}
|
|
788
|
+
let manifest;
|
|
789
|
+
try {
|
|
790
|
+
manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
791
|
+
} catch (error) {
|
|
792
|
+
problems.push(`${where}: unreadable package.json (${error.message})`);
|
|
793
|
+
continue;
|
|
794
|
+
}
|
|
795
|
+
if (!manifest || manifest.name !== module.package || manifest.version !== module.version) {
|
|
796
|
+
problems.push(
|
|
797
|
+
`${where}: installed ${manifest && manifest.name}@${manifest && manifest.version}, declared ${module.version}`
|
|
798
|
+
);
|
|
799
|
+
}
|
|
800
|
+
for (const skill of module.skills) {
|
|
801
|
+
const file = path.join(path.dirname(manifestPath), 'skills', skill, 'SKILL.md');
|
|
802
|
+
if (!fs.existsSync(file)) problems.push(`${where}: skills/${skill}/SKILL.md is missing`);
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
return problems;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
/** Every declared intake file exists in its pack directory and passes the contract. */
|
|
809
|
+
function checkIntakes(registry, { packageRoot = REPO_ROOT } = {}) {
|
|
810
|
+
const problems = [];
|
|
811
|
+
for (const [id, pack] of Object.entries(registry.packs)) {
|
|
812
|
+
if (!pack.intake) continue;
|
|
813
|
+
const relative = `src/bmad-plus/${pack.pack_src_dir || 'packs'}/${pack.pack_dir}/${pack.intake}`;
|
|
814
|
+
const file = path.join(packageRoot, ...relative.split('/'));
|
|
815
|
+
const where = `PACKS.${id}: intake ${relative}`;
|
|
816
|
+
if (!fs.existsSync(file)) {
|
|
817
|
+
problems.push(`${where}: file does not exist`);
|
|
818
|
+
continue;
|
|
819
|
+
}
|
|
820
|
+
let parsed;
|
|
821
|
+
try {
|
|
822
|
+
parsed = yaml.load(fs.readFileSync(file, 'utf8'), { schema: yaml.JSON_SCHEMA });
|
|
823
|
+
} catch (error) {
|
|
824
|
+
problems.push(`${where}: ${error.message}`);
|
|
825
|
+
continue;
|
|
826
|
+
}
|
|
827
|
+
const result = validateIntake(parsed);
|
|
828
|
+
for (const problem of result.problems) problems.push(`${where}: ${problem}`);
|
|
829
|
+
if (result.ok && parsed.pack !== id) {
|
|
830
|
+
problems.push(`${where}: declares pack "${parsed.pack}" instead of "${id}"`);
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
return problems;
|
|
834
|
+
}
|
|
835
|
+
|
|
565
836
|
/**
|
|
566
837
|
* `--check` mode: prove registry.yaml reproduces the live packs.js exactly,
|
|
567
838
|
* and that product.version matches package.json.
|
|
@@ -581,6 +852,8 @@ function check({
|
|
|
581
852
|
|
|
582
853
|
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
583
854
|
mismatches.push(...checkSourcePaths(generated.PACKS));
|
|
855
|
+
mismatches.push(...checkExternalModules(registry, { packageJson: pkg }));
|
|
856
|
+
mismatches.push(...checkIntakes(registry));
|
|
584
857
|
const { checkPythonDelivery } = require('./pack-delivery');
|
|
585
858
|
mismatches.push(...checkPythonDelivery(registry, { packageRoot: REPO_ROOT, packageJson: pkg }));
|
|
586
859
|
try {
|
|
@@ -666,6 +939,7 @@ module.exports = {
|
|
|
666
939
|
DEFAULT_PACKS_MODULE_PATH,
|
|
667
940
|
DEFAULT_MODULE_PATH,
|
|
668
941
|
MODULE_TEMPLATE_PATH,
|
|
942
|
+
PACKAGE_NAME_RE,
|
|
669
943
|
loadRegistry,
|
|
670
944
|
validateRegistry,
|
|
671
945
|
buildPacks,
|
|
@@ -678,6 +952,8 @@ module.exports = {
|
|
|
678
952
|
generatePacksModuleSource,
|
|
679
953
|
check,
|
|
680
954
|
checkSourcePaths,
|
|
955
|
+
checkExternalModules,
|
|
956
|
+
checkIntakes,
|
|
681
957
|
main,
|
|
682
958
|
};
|
|
683
959
|
|