bmad-plus 0.14.0 → 0.17.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 +76 -0
- package/README.md +55 -18
- package/SECURITY.md +71 -0
- package/THIRD-PARTY-LICENSES.md +349 -0
- package/osint-agent-package/README.md +1 -1
- package/package.json +11 -3
- package/readme-international/README.de.md +20 -9
- package/readme-international/README.es.md +21 -10
- package/readme-international/README.fr.md +20 -9
- package/src/bmad-plus/agents/agent-architect-dev/SKILL.md +11 -13
- package/src/bmad-plus/agents/agent-orchestrator/SKILL.md +147 -8
- package/src/bmad-plus/agents/agent-quality/SKILL.md +41 -11
- package/src/bmad-plus/data/role-triggers.yaml +19 -0
- package/src/bmad-plus/module-help.csv +1 -0
- package/src/bmad-plus/module.yaml +1 -0
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/create-story.md +3 -1
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/dev-story-checklist.md +2 -0
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/dev-story.md +5 -1
- package/src/bmad-plus/packs/pack-memory/README.md +29 -4
- package/src/bmad-plus/packs/pack-memory/memory-orchestrator.md +21 -1
- package/src/bmad-plus/packs/pack-memory/shared/karpathy-guardrails.md +3 -3
- package/src/bmad-plus/packs/pack-memory/shared/memory-protocol.md +27 -3
- package/src/bmad-plus/packs/pack-memory/zecher-agent.md +18 -2
- package/src/bmad-plus/skills/bmad-plus-autopilot/SKILL.md +47 -10
- package/src/bmad-plus/skills/bmad-plus-parallel/SKILL.md +17 -3
- package/src/bmad-plus/skills/bmad-plus-sync/SKILL.md +76 -67
- package/src/bmad-plus/skills/bmad-plus-uat/SKILL.md +158 -0
- package/src/bmad-plus/skills/bmad-plus-uat/ref/uat-results.schema.json +60 -0
- package/src/bmad-plus/skills/bmad-plus-uat/ref/uat-spec.schema.json +121 -0
- package/src/bmad-plus/skills/bmad-plus-uat/ref/uat-triage.schema.json +60 -0
- package/src/bmad-plus/skills/bmad-plus-uat/template/page.html +628 -0
- package/src/bmad-plus/skills/bmad-plus-uat/template/strings.json +442 -0
- package/src/bmad-plus/skills/bmad-plus-uat/templates/README.md +63 -0
- package/src/bmad-plus/skills/bmad-plus-uat/templates/example-uat-spec.json +95 -0
- package/src/bmad-plus/skills/bmad-plus-uat/templates/uat-release-gate.mjs +127 -0
- package/src/bmad-plus/skills/bmad-plus-uat/templates/uat-spec-present.mjs +77 -0
- package/tools/build/check-install-contract.js +202 -4
- package/tools/build/generate.js +16 -0
- package/tools/build/generated-adapters/.codex/AGENTS.md +1 -1
- package/tools/build/generated-adapters/.cursor/rules/bmad-plus.mdc +1 -1
- package/tools/build/generated-adapters/.opencode/AGENTS.md +1 -1
- package/tools/build/generated-adapters/AGENTS.md +1 -1
- package/tools/build/generated-adapters/CLAUDE.md +1 -1
- package/tools/build/generated-adapters/CONVENTIONS.md +1 -1
- package/tools/build/generated-adapters/GEMINI.md +1 -1
- package/tools/cli/bmad-plus-cli.js +15 -12
- package/tools/cli/commands/autoconfig.js +4 -2
- package/tools/cli/commands/doctor.js +1 -0
- package/tools/cli/commands/install.js +21 -2
- package/tools/cli/commands/memory-journal-cmd.js +119 -19
- package/tools/cli/commands/nexus.js +111 -0
- package/tools/cli/commands/uat.js +405 -0
- package/tools/cli/i18n.js +10 -0
- package/tools/cli/lib/README-memory-journal.md +19 -8
- package/tools/cli/lib/installation-health.js +6 -0
- package/tools/cli/lib/memory-journal.js +0 -0
- package/tools/cli/lib/memory-outcomes.js +293 -0
- package/tools/cli/lib/memory-store.js +139 -0
- package/tools/cli/lib/nexus-process.js +377 -0
- package/tools/cli/lib/nexus.js +1532 -0
- package/tools/cli/lib/pack-copy.js +39 -11
- package/tools/cli/lib/packs.js +17 -3
- package/tools/cli/lib/uat.js +887 -0
- package/tools/maintain/upstream-candidate.js +456 -0
- package/tools/release/publication-content.js +4 -1
- package/tools/release/supply-chain.js +282 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Gate G3 — production waits for a human run that was read and classified.
|
|
4
|
+
*
|
|
5
|
+
* Resolves the recipe covering the version in package.json, then runs
|
|
6
|
+
* `bmad-plus uat gate` on it. Exit 0 only when the gate passes: a finished run,
|
|
7
|
+
* answering the current revision of the recipe, with every failure classified and
|
|
8
|
+
* every writing step confirmed read-only.
|
|
9
|
+
*
|
|
10
|
+
* node scripts/uat-release-gate.mjs
|
|
11
|
+
* node scripts/uat-release-gate.mjs --id myproduct-1.4.0
|
|
12
|
+
* node scripts/uat-release-gate.mjs --cli node_modules/bmad-plus/tools/cli/bmad-plus-cli.js
|
|
13
|
+
* BMAD_PLUS_CLI=/path/to/bmad-plus-cli.js node scripts/uat-release-gate.mjs
|
|
14
|
+
*
|
|
15
|
+
* Exit codes mirror the command: 0 passed · 1 failed · 2 awaiting a run ·
|
|
16
|
+
* 3 the run answered another revision · 4 the gate could not be run at all.
|
|
17
|
+
*
|
|
18
|
+
* The gate establishes that the run is complete, current and classified. It never
|
|
19
|
+
* establishes that the tester looked at the right place: that stays human-observed.
|
|
20
|
+
*/
|
|
21
|
+
import fs from 'node:fs';
|
|
22
|
+
import path from 'node:path';
|
|
23
|
+
import { spawnSync } from 'node:child_process';
|
|
24
|
+
|
|
25
|
+
const args = process.argv.slice(2);
|
|
26
|
+
const option = (name, fallback) => {
|
|
27
|
+
const index = args.indexOf(`--${name}`);
|
|
28
|
+
return index >= 0 && args[index + 1] ? args[index + 1] : fallback;
|
|
29
|
+
};
|
|
30
|
+
const root = path.resolve(option('project', process.cwd()));
|
|
31
|
+
const directory = option('dir', '_bmad-output/uat');
|
|
32
|
+
const specs = path.join(root, directory, 'specs');
|
|
33
|
+
|
|
34
|
+
function resolveId() {
|
|
35
|
+
const explicit = option('id', null);
|
|
36
|
+
if (explicit) return explicit;
|
|
37
|
+
const file = path.join(root, 'package.json');
|
|
38
|
+
if (!fs.existsSync(file)) throw new Error(`no package.json in ${root}; pass --id <recipe>`);
|
|
39
|
+
const version = JSON.parse(fs.readFileSync(file, 'utf8')).version;
|
|
40
|
+
if (!fs.existsSync(specs)) throw new Error(`no recipe folder at ${specs}`);
|
|
41
|
+
const covering = fs
|
|
42
|
+
.readdirSync(specs)
|
|
43
|
+
.filter((name) => name.endsWith('.json'))
|
|
44
|
+
.filter((name) => {
|
|
45
|
+
try {
|
|
46
|
+
const spec = JSON.parse(fs.readFileSync(path.join(specs, name), 'utf8'));
|
|
47
|
+
return Array.isArray(spec.versions) && spec.versions.includes(version);
|
|
48
|
+
} catch {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
if (covering.length !== 1) {
|
|
53
|
+
throw new Error(
|
|
54
|
+
covering.length
|
|
55
|
+
? `${covering.length} recipes name ${version}; pass --id to choose one`
|
|
56
|
+
: `no recipe names ${version}; write it before deploying`
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
return covering[0].replace(/\.json$/, '');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The CLI's own entry point, run by this Node: an explicit one, or the copy
|
|
64
|
+
* installed in the project. Never a shell, never `npx` — a `.cmd` shim cannot be
|
|
65
|
+
* spawned without a shell on Windows, and a gate must not depend on a download.
|
|
66
|
+
*/
|
|
67
|
+
function command() {
|
|
68
|
+
const candidates = [
|
|
69
|
+
option('cli', null),
|
|
70
|
+
process.env.BMAD_PLUS_CLI,
|
|
71
|
+
path.join(root, 'node_modules', 'bmad-plus', 'tools', 'cli', 'bmad-plus-cli.js'),
|
|
72
|
+
].filter(Boolean);
|
|
73
|
+
for (const candidate of candidates) {
|
|
74
|
+
const resolved = path.resolve(root, candidate);
|
|
75
|
+
if (fs.existsSync(resolved)) return resolved;
|
|
76
|
+
}
|
|
77
|
+
throw new Error(
|
|
78
|
+
'no BMAD+ CLI to run the gate with. Install bmad-plus in this project ' +
|
|
79
|
+
'(npm install --save-dev bmad-plus) or set BMAD_PLUS_CLI to its bmad-plus-cli.js. ' +
|
|
80
|
+
'A missing gate is not a passing gate.'
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
const id = resolveId();
|
|
86
|
+
const cli = command();
|
|
87
|
+
const child = spawnSync(
|
|
88
|
+
process.execPath,
|
|
89
|
+
[cli, 'uat', 'gate', id, '--directory', root, '--dir', directory, '--json'],
|
|
90
|
+
{ encoding: 'utf8', timeout: 120000, windowsHide: true }
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
// The verdict is read from the gate's own answer, not guessed from an exit code:
|
|
94
|
+
// a CLI that never ran also exits non-zero, and that is a different problem.
|
|
95
|
+
let verdict = null;
|
|
96
|
+
try {
|
|
97
|
+
const parsed = JSON.parse(child.stdout);
|
|
98
|
+
if (parsed && typeof parsed.status === 'string') verdict = parsed;
|
|
99
|
+
} catch {
|
|
100
|
+
verdict = null;
|
|
101
|
+
}
|
|
102
|
+
if (!verdict) {
|
|
103
|
+
console.error(
|
|
104
|
+
`uat: the gate could not be run for ${id}` +
|
|
105
|
+
`${child.error ? ` (${child.error.message})` : ''}.\n` +
|
|
106
|
+
`${(child.stdout || '').trim()}${(child.stderr || '').trim()}\n` +
|
|
107
|
+
'Install the CLI in this project, or set BMAD_PLUS_CLI to its path. ' +
|
|
108
|
+
'A missing gate is not a passing gate.'
|
|
109
|
+
);
|
|
110
|
+
process.exit(4);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const explanation = {
|
|
114
|
+
passed: `${id}: human-observed, complete, current and triaged.`,
|
|
115
|
+
failed: `${id} did not pass its gate — production stays where it is.`,
|
|
116
|
+
awaiting: `${id} has no finished run yet — someone still has to play it.`,
|
|
117
|
+
stale: `${id} was answered on another revision of the recipe — replay the affected steps.`,
|
|
118
|
+
};
|
|
119
|
+
const code = { passed: 0, failed: 1, awaiting: 2, stale: 3 }[verdict.status] ?? 4;
|
|
120
|
+
const report = code === 0 ? console.log : console.error;
|
|
121
|
+
report(explanation[verdict.status] || `${id}: unexpected gate status "${verdict.status}".`);
|
|
122
|
+
for (const reason of verdict.reasons || []) report(` - ${reason}`);
|
|
123
|
+
process.exit(code);
|
|
124
|
+
} catch (error) {
|
|
125
|
+
console.error(`uat: ${error.message}`);
|
|
126
|
+
process.exit(4);
|
|
127
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Gate G1 — a version does not leave without saying what a person must check.
|
|
4
|
+
*
|
|
5
|
+
* Fails when no acceptance recipe covers the version in package.json. Copy this
|
|
6
|
+
* file into your project and run it wherever your other checks run: a pre-push
|
|
7
|
+
* hook, a test script, a CI step. It needs nothing but Node.
|
|
8
|
+
*
|
|
9
|
+
* node scripts/uat-spec-present.mjs
|
|
10
|
+
* node scripts/uat-spec-present.mjs --version 1.4.0 --dir _bmad-output/uat --json
|
|
11
|
+
*
|
|
12
|
+
* Exit 0 when a recipe covers the version, 1 otherwise. A reminder printed after
|
|
13
|
+
* a deployment protects nothing; this one runs before.
|
|
14
|
+
*/
|
|
15
|
+
import fs from 'node:fs';
|
|
16
|
+
import path from 'node:path';
|
|
17
|
+
|
|
18
|
+
const args = process.argv.slice(2);
|
|
19
|
+
const option = (name, fallback) => {
|
|
20
|
+
const index = args.indexOf(`--${name}`);
|
|
21
|
+
return index >= 0 && args[index + 1] ? args[index + 1] : fallback;
|
|
22
|
+
};
|
|
23
|
+
const json = args.includes('--json');
|
|
24
|
+
const root = path.resolve(option('project', process.cwd()));
|
|
25
|
+
const directory = path.resolve(root, option('dir', '_bmad-output/uat'));
|
|
26
|
+
|
|
27
|
+
function projectVersion() {
|
|
28
|
+
const explicit = option('version', null);
|
|
29
|
+
if (explicit) return explicit;
|
|
30
|
+
const file = path.join(root, 'package.json');
|
|
31
|
+
if (!fs.existsSync(file)) {
|
|
32
|
+
throw new Error(`no package.json in ${root}; pass --version <x.y.z>`);
|
|
33
|
+
}
|
|
34
|
+
const version = JSON.parse(fs.readFileSync(file, 'utf8')).version;
|
|
35
|
+
if (!version) throw new Error('package.json declares no version');
|
|
36
|
+
return version;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Recipes that name this version, whether they cover one version or a campaign. */
|
|
40
|
+
function covering(version) {
|
|
41
|
+
const specs = path.join(directory, 'specs');
|
|
42
|
+
if (!fs.existsSync(specs)) return [];
|
|
43
|
+
return fs
|
|
44
|
+
.readdirSync(specs)
|
|
45
|
+
.filter((name) => name.endsWith('.json'))
|
|
46
|
+
.filter((name) => {
|
|
47
|
+
try {
|
|
48
|
+
const spec = JSON.parse(fs.readFileSync(path.join(specs, name), 'utf8'));
|
|
49
|
+
// A grouped recipe covers a version because it NAMES it, never because its
|
|
50
|
+
// file name suggests a range.
|
|
51
|
+
return Array.isArray(spec.versions) && spec.versions.includes(version);
|
|
52
|
+
} catch {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
.map((name) => name.replace(/\.json$/, ''));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
const version = projectVersion();
|
|
61
|
+
const found = covering(version);
|
|
62
|
+
if (json) {
|
|
63
|
+
console.log(JSON.stringify({ version, directory, covering: found, ok: found.length > 0 }, null, 2));
|
|
64
|
+
} else if (found.length) {
|
|
65
|
+
console.log(`uat: ${version} is covered by ${found.join(', ')}`);
|
|
66
|
+
} else {
|
|
67
|
+
console.error(
|
|
68
|
+
`uat: no acceptance recipe covers ${version}.\n` +
|
|
69
|
+
`Write ${path.relative(root, path.join(directory, 'specs'))}/<product>-${version}.json before shipping,\n` +
|
|
70
|
+
`or add ${version} to the versions of a grouped recipe. Ask your agent: "write the recette for this delivery".`
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
process.exit(found.length ? 0 : 1);
|
|
74
|
+
} catch (error) {
|
|
75
|
+
console.error(`uat: ${error.message}`);
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
@@ -238,24 +238,221 @@ function packAndExtract(tempRoot, env) {
|
|
|
238
238
|
],
|
|
239
239
|
{ cwd: REPO_ROOT, env, timeout: 120000 }
|
|
240
240
|
);
|
|
241
|
-
|
|
242
|
-
|
|
241
|
+
// npm 11 reports a list, npm 12 an object keyed by package name.
|
|
242
|
+
const reported = JSON.parse(output);
|
|
243
|
+
const packed = Array.isArray(reported) ? reported : Object.values(reported);
|
|
244
|
+
assert(
|
|
245
|
+
packed.length === 1 && typeof packed[0]?.filename === 'string',
|
|
246
|
+
'npm pack must produce exactly one package'
|
|
247
|
+
);
|
|
243
248
|
const archive = resolveWithin(destination, packed[0].filename);
|
|
244
|
-
|
|
249
|
+
// The archive already sits in cwd, and it is named relatively on purpose: GNU tar
|
|
250
|
+
// reads a Windows "C:\..." argument as a remote host and refuses to open it.
|
|
251
|
+
const archiveName = path.basename(archive);
|
|
252
|
+
const entries = run('tar', ['-tzf', archiveName], { cwd: destination, env })
|
|
253
|
+
.trim()
|
|
254
|
+
.split(/\r?\n/);
|
|
245
255
|
for (const entry of entries) {
|
|
246
256
|
assert(entry.startsWith('package/'), `Unexpected npm archive entry: ${entry}`);
|
|
247
257
|
resolveWithin(destination, entry);
|
|
248
258
|
}
|
|
249
|
-
run('tar', ['-xzf',
|
|
259
|
+
run('tar', ['-xzf', archiveName, '-C', '.'], { cwd: destination, env });
|
|
250
260
|
const packageRoot = path.join(destination, 'package');
|
|
251
261
|
assert(
|
|
252
262
|
!fs.existsSync(path.join(packageRoot, 'registry.yaml')),
|
|
253
263
|
'npm contract must work without registry.yaml'
|
|
254
264
|
);
|
|
255
265
|
assert(fs.statSync(packageRoot).isDirectory(), 'npm package directory is missing');
|
|
266
|
+
for (const file of ['LICENSE', 'THIRD-PARTY-LICENSES.md', 'SECURITY.md']) {
|
|
267
|
+
assert(fs.statSync(path.join(packageRoot, file)).isFile(), `npm archive is missing ${file}`);
|
|
268
|
+
}
|
|
269
|
+
assert(
|
|
270
|
+
!fs.existsSync(path.join(packageRoot, 'src/bmad-plus/packs/pack-seo/scripts/__pycache__')),
|
|
271
|
+
'npm archive must exclude local Python caches'
|
|
272
|
+
);
|
|
256
273
|
return packageRoot;
|
|
257
274
|
}
|
|
258
275
|
|
|
276
|
+
function verifyNexusConsumer(packageRoot, projectDir, env) {
|
|
277
|
+
const invoke = (args) =>
|
|
278
|
+
JSON.parse(
|
|
279
|
+
run(
|
|
280
|
+
process.execPath,
|
|
281
|
+
[
|
|
282
|
+
path.join(packageRoot, 'tools/cli/bmad-plus-cli.js'),
|
|
283
|
+
'nexus',
|
|
284
|
+
...args,
|
|
285
|
+
'--directory',
|
|
286
|
+
projectDir,
|
|
287
|
+
'--json',
|
|
288
|
+
],
|
|
289
|
+
{ cwd: projectDir, env }
|
|
290
|
+
)
|
|
291
|
+
);
|
|
292
|
+
const input = (name, value) => {
|
|
293
|
+
const file = path.join(projectDir, name);
|
|
294
|
+
fs.writeFileSync(file, JSON.stringify(value));
|
|
295
|
+
return file;
|
|
296
|
+
};
|
|
297
|
+
const instruction = '.agents/skills/agent-orchestrator/SKILL.md';
|
|
298
|
+
assert(
|
|
299
|
+
fs
|
|
300
|
+
.readFileSync(path.join(projectDir, instruction), 'utf8')
|
|
301
|
+
.includes('Durable host-managed attempts')
|
|
302
|
+
);
|
|
303
|
+
fs.mkdirSync(path.join(projectDir, 'checks'));
|
|
304
|
+
fs.mkdirSync(path.join(projectDir, 'result'));
|
|
305
|
+
fs.writeFileSync(
|
|
306
|
+
path.join(projectDir, 'checks/accepted.cjs'),
|
|
307
|
+
"const fs=require('node:fs');const assert=require('node:assert/strict');assert.equal(fs.readFileSync('result/value.txt','utf8'),'accepted');\n"
|
|
308
|
+
);
|
|
309
|
+
const plan = input('nexus-plan.json', {
|
|
310
|
+
id: 'consumer',
|
|
311
|
+
tasks: [
|
|
312
|
+
{
|
|
313
|
+
id: 'work',
|
|
314
|
+
objective: 'Verify a real relocated npm/source consumer',
|
|
315
|
+
scope: ['result'],
|
|
316
|
+
artifacts: ['result/value.txt'],
|
|
317
|
+
resources: [instruction, 'checks/accepted.cjs'],
|
|
318
|
+
checks: [{ id: 'actual-output', command: 'node', args: ['checks/accepted.cjs'] }],
|
|
319
|
+
},
|
|
320
|
+
],
|
|
321
|
+
});
|
|
322
|
+
invoke(['create', '--plan', plan]);
|
|
323
|
+
const backend = { kind: 'host', id: 'consumer-check', sessionId: 'observed-attempt' };
|
|
324
|
+
const started = invoke([
|
|
325
|
+
'start',
|
|
326
|
+
'consumer',
|
|
327
|
+
'work',
|
|
328
|
+
'--input',
|
|
329
|
+
input('nexus-host.json', { backend }),
|
|
330
|
+
]);
|
|
331
|
+
const attemptId = started.tasks[0].attempts[0].id;
|
|
332
|
+
fs.writeFileSync(path.join(projectDir, 'result/value.txt'), 'accepted');
|
|
333
|
+
// A separate CLI process reconciles an interrupted host without repeating its work.
|
|
334
|
+
invoke([
|
|
335
|
+
'reconcile',
|
|
336
|
+
'consumer',
|
|
337
|
+
'work',
|
|
338
|
+
'--input',
|
|
339
|
+
input('nexus-observation.json', {
|
|
340
|
+
attemptId,
|
|
341
|
+
backend,
|
|
342
|
+
ownerStopped: true,
|
|
343
|
+
outcome: 'completed',
|
|
344
|
+
summary: 'Test host stopped after writing its artifact; independent checker follows.',
|
|
345
|
+
}),
|
|
346
|
+
]);
|
|
347
|
+
const checked = invoke(['verify', 'consumer', 'work']);
|
|
348
|
+
assert.equal(checked.tasks[0].attempts[0].verification.status, 'passed');
|
|
349
|
+
const accepted = invoke(['accept', 'consumer', 'work']);
|
|
350
|
+
assert.equal(accepted.tasks[0].integration, 'accepted');
|
|
351
|
+
assert.equal(accepted.tasks[0].integrationReceipt.gitOperation, null);
|
|
352
|
+
const observed = invoke(['inspect', 'consumer']);
|
|
353
|
+
assert.equal(observed.observations[0].evidenceEligible, true);
|
|
354
|
+
assert.equal(observed.observations[0].stale, false);
|
|
355
|
+
|
|
356
|
+
fs.mkdirSync(path.join(projectDir, 'processed'));
|
|
357
|
+
fs.writeFileSync(path.join(projectDir, 'checks/task.txt'), 'Write the supervised result.\n');
|
|
358
|
+
fs.writeFileSync(
|
|
359
|
+
path.join(projectDir, 'checks/worker.cjs'),
|
|
360
|
+
"require('node:fs').writeFileSync('processed/value.txt','supervised');\n"
|
|
361
|
+
);
|
|
362
|
+
fs.writeFileSync(
|
|
363
|
+
path.join(projectDir, 'checks/process.cjs'),
|
|
364
|
+
"require('node:assert/strict').equal(require('node:fs').readFileSync('processed/value.txt','utf8'),'supervised');\n"
|
|
365
|
+
);
|
|
366
|
+
invoke([
|
|
367
|
+
'create',
|
|
368
|
+
'--plan',
|
|
369
|
+
input('process-plan.json', {
|
|
370
|
+
id: 'process-consumer',
|
|
371
|
+
tasks: [
|
|
372
|
+
{
|
|
373
|
+
id: 'work',
|
|
374
|
+
objective: 'Verify the packaged foreground process backend',
|
|
375
|
+
scope: ['processed'],
|
|
376
|
+
artifacts: ['processed/value.txt'],
|
|
377
|
+
resources: [instruction, 'checks/process.cjs'],
|
|
378
|
+
checks: [{ id: 'process-output', command: 'node', args: ['checks/process.cjs'] }],
|
|
379
|
+
execution: {
|
|
380
|
+
adapter: 'command',
|
|
381
|
+
command: 'node',
|
|
382
|
+
args: ['checks/worker.cjs'],
|
|
383
|
+
resources: ['checks/worker.cjs'],
|
|
384
|
+
input: 'checks/task.txt',
|
|
385
|
+
},
|
|
386
|
+
},
|
|
387
|
+
],
|
|
388
|
+
}),
|
|
389
|
+
]);
|
|
390
|
+
const launched = invoke(['launch', 'process-consumer', 'work']);
|
|
391
|
+
const processAttempt = launched.tasks[0].attempts[0];
|
|
392
|
+
assert.equal(processAttempt.process.receipt.outcome, 'completed');
|
|
393
|
+
invoke([
|
|
394
|
+
'collect',
|
|
395
|
+
'process-consumer',
|
|
396
|
+
'work',
|
|
397
|
+
'--input',
|
|
398
|
+
input('collect.json', {
|
|
399
|
+
attemptId: processAttempt.id,
|
|
400
|
+
backend: processAttempt.backend,
|
|
401
|
+
}),
|
|
402
|
+
]);
|
|
403
|
+
assert.equal(
|
|
404
|
+
invoke(['verify', 'process-consumer', 'work']).tasks[0].attempts[0].verification.status,
|
|
405
|
+
'passed'
|
|
406
|
+
);
|
|
407
|
+
assert.equal(invoke(['accept', 'process-consumer', 'work']).tasks[0].integration, 'accepted');
|
|
408
|
+
|
|
409
|
+
const mem = (args) =>
|
|
410
|
+
JSON.parse(
|
|
411
|
+
run(
|
|
412
|
+
process.execPath,
|
|
413
|
+
[
|
|
414
|
+
path.join(packageRoot, 'tools/cli/bmad-plus-cli.js'),
|
|
415
|
+
'mem',
|
|
416
|
+
...args,
|
|
417
|
+
'--directory',
|
|
418
|
+
projectDir,
|
|
419
|
+
'--json',
|
|
420
|
+
],
|
|
421
|
+
{ cwd: projectDir, env }
|
|
422
|
+
)
|
|
423
|
+
);
|
|
424
|
+
const memoryFile = '.agents/memory/patterns.md';
|
|
425
|
+
fs.mkdirSync(path.dirname(path.join(projectDir, memoryFile)), { recursive: true });
|
|
426
|
+
fs.appendFileSync(
|
|
427
|
+
path.join(projectDir, memoryFile),
|
|
428
|
+
'\n### Consumer supervision evidence\nVerify the supervised artifact before accepting process work.\n'
|
|
429
|
+
);
|
|
430
|
+
input('memory-observation.json', {
|
|
431
|
+
runId: 'process-consumer',
|
|
432
|
+
taskId: 'work',
|
|
433
|
+
memory: { file: memoryFile, heading: 'Consumer supervision evidence' },
|
|
434
|
+
scope: ['processed'],
|
|
435
|
+
interpretation:
|
|
436
|
+
'Consumer fixture links this note to the accepted result; causality is unmeasured.',
|
|
437
|
+
});
|
|
438
|
+
mem(['observe', '--input', 'memory-observation.json']);
|
|
439
|
+
const recall = () =>
|
|
440
|
+
mem([
|
|
441
|
+
'recall',
|
|
442
|
+
'Consumer supervision evidence',
|
|
443
|
+
'--ranking',
|
|
444
|
+
'evidence',
|
|
445
|
+
'--context-scope',
|
|
446
|
+
'processed',
|
|
447
|
+
]);
|
|
448
|
+
assert(recall().results.some((entry) => entry.ref === 'Consumer supervision evidence'));
|
|
449
|
+
fs.writeFileSync(path.join(projectDir, 'processed/value.txt'), 'changed after acceptance');
|
|
450
|
+
assert(
|
|
451
|
+
!recall().results.some((entry) => entry.ref === 'Consumer supervision evidence'),
|
|
452
|
+
'Packaged memory must reject evidence after its accepted artifact changes'
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
|
|
259
456
|
function runMatrix(packageRoot, variant, tempRoot, env, reference) {
|
|
260
457
|
const allPacks = Object.keys(reference.packDefinitions).filter(
|
|
261
458
|
(id) => !reference.packDefinitions[id].disabled
|
|
@@ -313,6 +510,7 @@ function runMatrix(packageRoot, variant, tempRoot, env, reference) {
|
|
|
313
510
|
derived: reference.derived,
|
|
314
511
|
variant,
|
|
315
512
|
});
|
|
513
|
+
if (installations === 1) verifyNexusConsumer(packageRoot, projectDir, env);
|
|
316
514
|
if (selection.packs.includes('dev-studio')) {
|
|
317
515
|
const list = JSON.parse(
|
|
318
516
|
run(
|
package/tools/build/generate.js
CHANGED
|
@@ -189,6 +189,21 @@ function validateRegistry(registry) {
|
|
|
189
189
|
'registry.yaml: targets.integration must declare the delivered instruction integration'
|
|
190
190
|
);
|
|
191
191
|
}
|
|
192
|
+
const processBackend = registry.targets?.optional_process_backend;
|
|
193
|
+
if (
|
|
194
|
+
processBackend &&
|
|
195
|
+
(!isDeepStrictEqual(processBackend.adapters, ['command', 'codex-exec']) ||
|
|
196
|
+
processBackend.supervisor !== 'foreground' ||
|
|
197
|
+
processBackend.launch !== 'explicit-plan' ||
|
|
198
|
+
processBackend.collect !== 'exact-attempt-receipt' ||
|
|
199
|
+
processBackend.cancellation !== 'original-owner-direct-child' ||
|
|
200
|
+
processBackend.verification !== 'independent-required' ||
|
|
201
|
+
processBackend.scheduling !== 'none')
|
|
202
|
+
) {
|
|
203
|
+
throw new Error(
|
|
204
|
+
'registry.yaml: optional process backend differs from the delivered Nexus contract'
|
|
205
|
+
);
|
|
206
|
+
}
|
|
192
207
|
for (const [runtime, config] of Object.entries(registry.runtimes || {})) {
|
|
193
208
|
if (
|
|
194
209
|
typeof config?.min_version !== 'string' ||
|
|
@@ -359,6 +374,7 @@ function buildDerived(registry, { sourceRoot = path.join(REPO_ROOT, 'src', 'bmad
|
|
|
359
374
|
pythonPacks,
|
|
360
375
|
diagnostics: {
|
|
361
376
|
schemaVersion: 1,
|
|
377
|
+
processExecution: registry.targets.optional_process_backend || null,
|
|
362
378
|
runtimeMinimums: Object.fromEntries(
|
|
363
379
|
Object.entries(registry.runtimes).map(([id, runtime]) => [id, runtime.min_version])
|
|
364
380
|
),
|
|
@@ -113,7 +113,7 @@ After a successful update, reread the project spine and relevant installed skill
|
|
|
113
113
|
|
|
114
114
|
## Registry facts (computed from registry.yaml — never hand-typed)
|
|
115
115
|
|
|
116
|
-
- Product: BMAD+ v0.
|
|
116
|
+
- Product: BMAD+ v0.17.0 (derived from BMAD-METHOD v6.6.0)
|
|
117
117
|
- Models supported: claude, gpt, gemini, local (model-agnostic by contract)
|
|
118
118
|
- Packs (9): Core, OSINT, Maker, Shield, SEO, Memory, Dev Studio, Backup, Animated
|
|
119
119
|
- Installer agents (14 across all packs):
|
|
@@ -118,7 +118,7 @@ After a successful update, reread the project spine and relevant installed skill
|
|
|
118
118
|
|
|
119
119
|
## Registry facts (computed from registry.yaml — never hand-typed)
|
|
120
120
|
|
|
121
|
-
- Product: BMAD+ v0.
|
|
121
|
+
- Product: BMAD+ v0.17.0 (derived from BMAD-METHOD v6.6.0)
|
|
122
122
|
- Models supported: claude, gpt, gemini, local (model-agnostic by contract)
|
|
123
123
|
- Packs (9): Core, OSINT, Maker, Shield, SEO, Memory, Dev Studio, Backup, Animated
|
|
124
124
|
- Installer agents (14 across all packs):
|
|
@@ -113,7 +113,7 @@ After a successful update, reread the project spine and relevant installed skill
|
|
|
113
113
|
|
|
114
114
|
## Registry facts (computed from registry.yaml — never hand-typed)
|
|
115
115
|
|
|
116
|
-
- Product: BMAD+ v0.
|
|
116
|
+
- Product: BMAD+ v0.17.0 (derived from BMAD-METHOD v6.6.0)
|
|
117
117
|
- Models supported: claude, gpt, gemini, local (model-agnostic by contract)
|
|
118
118
|
- Packs (9): Core, OSINT, Maker, Shield, SEO, Memory, Dev Studio, Backup, Animated
|
|
119
119
|
- Installer agents (14 across all packs):
|
|
@@ -111,7 +111,7 @@ After a successful update, reread the project spine and relevant installed skill
|
|
|
111
111
|
|
|
112
112
|
## Registry facts (computed from registry.yaml — never hand-typed)
|
|
113
113
|
|
|
114
|
-
- Product: BMAD+ v0.
|
|
114
|
+
- Product: BMAD+ v0.17.0 (derived from BMAD-METHOD v6.6.0)
|
|
115
115
|
- Models supported: claude, gpt, gemini, local (model-agnostic by contract)
|
|
116
116
|
- Packs (9): Core, OSINT, Maker, Shield, SEO, Memory, Dev Studio, Backup, Animated
|
|
117
117
|
- Installer agents (14 across all packs):
|
|
@@ -114,7 +114,7 @@ After a successful update, reread the project spine and relevant installed skill
|
|
|
114
114
|
|
|
115
115
|
## Registry facts (computed from registry.yaml — never hand-typed)
|
|
116
116
|
|
|
117
|
-
- Product: BMAD+ v0.
|
|
117
|
+
- Product: BMAD+ v0.17.0 (derived from BMAD-METHOD v6.6.0)
|
|
118
118
|
- Models supported: claude, gpt, gemini, local (model-agnostic by contract)
|
|
119
119
|
- Packs (9): Core, OSINT, Maker, Shield, SEO, Memory, Dev Studio, Backup, Animated
|
|
120
120
|
- Installer agents (14 across all packs):
|
|
@@ -113,7 +113,7 @@ After a successful update, reread the project spine and relevant installed skill
|
|
|
113
113
|
|
|
114
114
|
## Registry facts (computed from registry.yaml — never hand-typed)
|
|
115
115
|
|
|
116
|
-
- Product: BMAD+ v0.
|
|
116
|
+
- Product: BMAD+ v0.17.0 (derived from BMAD-METHOD v6.6.0)
|
|
117
117
|
- Models supported: claude, gpt, gemini, local (model-agnostic by contract)
|
|
118
118
|
- Packs (9): Core, OSINT, Maker, Shield, SEO, Memory, Dev Studio, Backup, Animated
|
|
119
119
|
- Installer agents (14 across all packs):
|
|
@@ -118,7 +118,7 @@ After a successful update, reread the project spine and relevant installed skill
|
|
|
118
118
|
|
|
119
119
|
## Registry facts (computed from registry.yaml — never hand-typed)
|
|
120
120
|
|
|
121
|
-
- Product: BMAD+ v0.
|
|
121
|
+
- Product: BMAD+ v0.17.0 (derived from BMAD-METHOD v6.6.0)
|
|
122
122
|
- Models supported: claude, gpt, gemini, local (model-agnostic by contract)
|
|
123
123
|
- Packs (9): Core, OSINT, Maker, Shield, SEO, Memory, Dev Studio, Backup, Animated
|
|
124
124
|
- Installer agents (14 across all packs):
|
|
@@ -45,9 +45,7 @@ for (const option of install.options || []) {
|
|
|
45
45
|
installCmd.action(install.action);
|
|
46
46
|
|
|
47
47
|
// Uninstall command
|
|
48
|
-
const uninstallCmd = program
|
|
49
|
-
.command('uninstall')
|
|
50
|
-
.description('Remove BMAD+ from your project');
|
|
48
|
+
const uninstallCmd = program.command('uninstall').description('Remove BMAD+ from your project');
|
|
51
49
|
for (const option of uninstall.options || []) {
|
|
52
50
|
uninstallCmd.option(...option);
|
|
53
51
|
}
|
|
@@ -64,9 +62,7 @@ for (const option of update.options || []) {
|
|
|
64
62
|
updateCmd.action(update.action);
|
|
65
63
|
|
|
66
64
|
// Doctor command
|
|
67
|
-
const doctorCmd = program
|
|
68
|
-
.command('doctor')
|
|
69
|
-
.description('Check BMAD+ installation integrity');
|
|
65
|
+
const doctorCmd = program.command('doctor').description('Check BMAD+ installation integrity');
|
|
70
66
|
|
|
71
67
|
for (const option of doctor.options || []) {
|
|
72
68
|
doctorCmd.option(...option);
|
|
@@ -81,7 +77,9 @@ const scanCmd = program
|
|
|
81
77
|
for (const option of scan.options || []) {
|
|
82
78
|
scanCmd.option(...option);
|
|
83
79
|
}
|
|
84
|
-
scanCmd.action((scanPath, options) =>
|
|
80
|
+
scanCmd.action((scanPath, options) =>
|
|
81
|
+
scan.action({ ...options, directory: scanPath || options.directory })
|
|
82
|
+
);
|
|
85
83
|
|
|
86
84
|
// Memory command
|
|
87
85
|
const memoryCmd = program
|
|
@@ -95,9 +93,7 @@ memoryCmd.action(memory.action);
|
|
|
95
93
|
|
|
96
94
|
// Mem command (Karpathy memory loop — journal recall/write/reinforce)
|
|
97
95
|
const memJournal = require('./commands/memory-journal-cmd');
|
|
98
|
-
const memJournalCmd = program
|
|
99
|
-
.command(memJournal.command)
|
|
100
|
-
.description(memJournal.description);
|
|
96
|
+
const memJournalCmd = program.command(memJournal.command).description(memJournal.description);
|
|
101
97
|
|
|
102
98
|
for (const option of memJournal.options || []) {
|
|
103
99
|
memJournalCmd.option(...option);
|
|
@@ -116,15 +112,22 @@ for (const option of autoconfig.options || []) {
|
|
|
116
112
|
autoconfigCmd.action(autoconfig.action);
|
|
117
113
|
|
|
118
114
|
// Version discovery and explicit per-project update policy.
|
|
119
|
-
for (const modulePath of [
|
|
115
|
+
for (const modulePath of [
|
|
116
|
+
'./commands/update-check',
|
|
117
|
+
'./commands/update-policy',
|
|
118
|
+
'./commands/studio',
|
|
119
|
+
'./commands/nexus',
|
|
120
|
+
'./commands/uat',
|
|
121
|
+
]) {
|
|
120
122
|
const command = require(modulePath);
|
|
121
123
|
const configured = program.command(command.command).description(command.description);
|
|
124
|
+
if (command.aliases) configured.aliases(command.aliases);
|
|
122
125
|
for (const option of command.options || []) configured.option(...option);
|
|
123
126
|
configured.action(command.action);
|
|
124
127
|
}
|
|
125
128
|
|
|
126
129
|
// Await asynchronous registry checks and updates before completing the CLI.
|
|
127
|
-
program.parseAsync(process.argv).catch(error => {
|
|
130
|
+
program.parseAsync(process.argv).catch((error) => {
|
|
128
131
|
console.error(`BMAD+: ${error.message}`);
|
|
129
132
|
process.exitCode = 1;
|
|
130
133
|
});
|
|
@@ -87,7 +87,9 @@ function calculateHealth(structure) {
|
|
|
87
87
|
return { pct, checks };
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
|
|
90
|
+
// `health` is measured elsewhere and does not change the recommendation: a project
|
|
91
|
+
// with failing checks needs the same packs as a healthy one.
|
|
92
|
+
function recommendPacks(stack, structure) {
|
|
91
93
|
const packs = ['core', 'memory']; // Always
|
|
92
94
|
const reasons = {
|
|
93
95
|
core: 'Essential multi-role agents (Atlas, Forge, Sentinel, Nexus)',
|
|
@@ -215,7 +217,7 @@ module.exports = {
|
|
|
215
217
|
const stack = detectStack(projectDir);
|
|
216
218
|
const structure = analyzeStructure(projectDir);
|
|
217
219
|
const health = calculateHealth(structure);
|
|
218
|
-
const { packs, reasons } = recommendPacks(stack, structure
|
|
220
|
+
const { packs, reasons } = recommendPacks(stack, structure);
|
|
219
221
|
const recs = generateRecommendations(stack, structure, health);
|
|
220
222
|
|
|
221
223
|
spinner.stop('Analysis complete');
|
|
@@ -45,6 +45,7 @@ module.exports = {
|
|
|
45
45
|
'Installed: v' + report.versions.installed + ' | Executing CLI: v' + report.versions.cli,
|
|
46
46
|
'Ownership inventory: ' + report.inventory.status,
|
|
47
47
|
'Integrations use host-managed execution; host capabilities were not probed.',
|
|
48
|
+
'Optional Nexus process execution requires an explicit plan; executable availability was not probed.',
|
|
48
49
|
].join('\n'),
|
|
49
50
|
'BMAD+ Health Report'
|
|
50
51
|
);
|