bmad-plus 0.16.0 → 0.17.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 +114 -29
- package/README.md +45 -27
- package/SECURITY.md +9 -1
- package/osint-agent-package/README.md +13 -0
- package/osint-agent-package/SETUP_KEYS.md +13 -0
- package/package.json +32 -8
- package/readme-international/README.de.md +134 -537
- package/readme-international/README.es.md +136 -556
- package/readme-international/README.fr.md +135 -553
- package/src/bmad-plus/agents/agent-shadow/SKILL.md +6 -0
- package/src/bmad-plus/packs/pack-memory/README.md +1 -1
- package/src/bmad-plus/packs/pack-memory/memory-orchestrator.md +1 -1
- package/src/bmad-plus/packs/pack-memory/shared/memory-protocol.md +1 -1
- package/src/bmad-plus/packs/pack-shield/README.md +2 -0
- package/src/bmad-plus/packs/pack-shield/SKILL.md +2 -0
- package/src/bmad-plus/packs/pack-shield/shared/audit-report-template.md +6 -0
- package/src/bmad-plus/packs/pack-shield/shield-orchestrator.md +4 -0
- package/src/bmad-plus/skills/bmad-plus-uat/SKILL.md +18 -4
- package/src/bmad-plus/skills/bmad-plus-uat/template/page.html +95 -19
- package/src/bmad-plus/skills/bmad-plus-uat/template/strings.json +90 -10
- 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/adapters.config.js +1 -1
- package/tools/build/generate-adapters.js +186 -60
- package/tools/build/generate.js +49 -13
- package/tools/cli/bmad-plus-cli.js +3 -6
- package/tools/cli/commands/autoconfig.js +66 -24
- package/tools/cli/commands/install.js +620 -356
- package/tools/cli/commands/memory.js +40 -13
- package/tools/cli/commands/scan.js +94 -30
- package/tools/cli/commands/uat.js +21 -5
- package/tools/cli/commands/uninstall.js +65 -16
- package/tools/cli/commands/update-check.js +8 -3
- package/tools/cli/commands/update-policy.js +14 -5
- package/tools/cli/commands/update.js +89 -20
- package/tools/cli/i18n.js +128 -66
- package/tools/cli/lib/ide-config.js +3 -1
- package/tools/cli/lib/install-manifest.js +13 -6
- package/tools/cli/lib/installation-health.js +33 -1
- package/tools/cli/lib/installed-adapters.js +29 -6
- package/tools/cli/lib/memory-init.js +19 -4
- package/tools/cli/lib/packs.js +17 -28
- package/tools/cli/lib/path-hash.js +5 -1
- package/tools/cli/lib/python-provision.js +51 -12
- package/tools/cli/lib/stack-detect.js +51 -25
- package/tools/cli/lib/uat.js +18 -0
- package/tools/cli/lib/update-check.js +132 -34
- package/tools/cli/lib/update-dispatch.js +2 -2
- package/tools/cli/lib/update-policy.js +57 -19
- package/tools/cli/lib/update-transaction.js +439 -85
- package/tools/build/README.md +0 -78
- package/tools/build/check-counts.js +0 -626
- package/tools/build/check-install-contract.js +0 -648
- package/tools/build/generated-adapters/.codex/AGENTS.md +0 -134
- package/tools/build/generated-adapters/.cursor/rules/bmad-plus.mdc +0 -139
- package/tools/build/generated-adapters/.opencode/AGENTS.md +0 -134
- package/tools/build/generated-adapters/AGENTS.md +0 -132
- package/tools/build/generated-adapters/CLAUDE.md +0 -135
- package/tools/build/generated-adapters/CONVENTIONS.md +0 -134
- package/tools/build/generated-adapters/GEMINI.md +0 -139
- package/tools/build/generated-adapters/README.md +0 -79
- package/tools/maintain/upstream-candidate.js +0 -456
- package/tools/release/publication-content.js +0 -903
- package/tools/release/supply-chain.js +0 -282
|
@@ -1,648 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Verify that real source and npm-package installs deliver the adapter contract.
|
|
4
|
-
* Uses existing dependencies, temporary projects and an isolated brain; no
|
|
5
|
-
* lifecycle scripts, dependency installation or Python provisioning are run.
|
|
6
|
-
*
|
|
7
|
-
* Run: node tools/build/check-install-contract.js
|
|
8
|
-
* Author: Laurent Rochetta
|
|
9
|
-
*/
|
|
10
|
-
'use strict';
|
|
11
|
-
|
|
12
|
-
const assert = require('node:assert/strict');
|
|
13
|
-
const fs = require('node:fs');
|
|
14
|
-
const os = require('node:os');
|
|
15
|
-
const path = require('node:path');
|
|
16
|
-
const crypto = require('node:crypto');
|
|
17
|
-
const { spawnSync } = require('node:child_process');
|
|
18
|
-
|
|
19
|
-
const REPO_ROOT = path.resolve(__dirname, '..', '..');
|
|
20
|
-
const TEMP_PREFIX = 'bmad-install-contract-';
|
|
21
|
-
|
|
22
|
-
function resolveWithin(root, relative) {
|
|
23
|
-
assert(typeof relative === 'string' && relative.length > 0, 'Expected a relative file path');
|
|
24
|
-
assert(
|
|
25
|
-
!path.win32.isAbsolute(relative) && !path.posix.isAbsolute(relative),
|
|
26
|
-
`Absolute path: ${relative}`
|
|
27
|
-
);
|
|
28
|
-
const target = path.resolve(root, relative);
|
|
29
|
-
const fromRoot = path.relative(root, target);
|
|
30
|
-
assert(
|
|
31
|
-
fromRoot &&
|
|
32
|
-
!path.isAbsolute(fromRoot) &&
|
|
33
|
-
!fromRoot.startsWith(`..${path.sep}`) &&
|
|
34
|
-
fromRoot !== '..',
|
|
35
|
-
`Path escapes project: ${relative}`
|
|
36
|
-
);
|
|
37
|
-
return target;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function sameSelection(actual, expected, label) {
|
|
41
|
-
assert(Array.isArray(actual), `Manifest ${label} must be an array`);
|
|
42
|
-
assert.deepEqual(
|
|
43
|
-
[...actual].sort(),
|
|
44
|
-
[...expected].sort(),
|
|
45
|
-
`Manifest ${label} differs from requested selection`
|
|
46
|
-
);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/** Compare the installed bytes, normalizing only Windows/Unix line endings. */
|
|
50
|
-
function verifyInstallation(
|
|
51
|
-
projectDir,
|
|
52
|
-
{ packs, tools, derived, ideConfigs, renderFiles, requireInventory = false }
|
|
53
|
-
) {
|
|
54
|
-
const manifest = JSON.parse(
|
|
55
|
-
fs.readFileSync(path.join(projectDir, '_bmad', '.bmad-plus-install.json'), 'utf8')
|
|
56
|
-
);
|
|
57
|
-
sameSelection(manifest.packs, packs, 'packs');
|
|
58
|
-
sameSelection(manifest.ides, tools, 'ides');
|
|
59
|
-
assert(typeof manifest.user === 'string' && manifest.user, 'Manifest user is missing');
|
|
60
|
-
assert(
|
|
61
|
-
typeof manifest.language === 'string' && manifest.language,
|
|
62
|
-
'Manifest language is missing'
|
|
63
|
-
);
|
|
64
|
-
|
|
65
|
-
const files = renderFiles(derived, {
|
|
66
|
-
packs,
|
|
67
|
-
userName: manifest.user,
|
|
68
|
-
language: manifest.language,
|
|
69
|
-
tools: manifest.ides,
|
|
70
|
-
});
|
|
71
|
-
const expectedPaths = new Set(files.map(({ file }) => file));
|
|
72
|
-
assert.equal(expectedPaths.size, files.length, 'Renderer returned duplicate output paths');
|
|
73
|
-
assert.equal(
|
|
74
|
-
derived.targets.spine,
|
|
75
|
-
'AGENTS.md',
|
|
76
|
-
'Install contract requires the root AGENTS.md spine'
|
|
77
|
-
);
|
|
78
|
-
assert(
|
|
79
|
-
expectedPaths.has(derived.targets.spine),
|
|
80
|
-
'Renderer must include the root AGENTS.md spine'
|
|
81
|
-
);
|
|
82
|
-
for (const tool of tools) {
|
|
83
|
-
assert(expectedPaths.has(ideConfigs[tool].configFile), `Renderer omitted the ${tool} adapter`);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
for (const { file, content } of files) {
|
|
87
|
-
const target = resolveWithin(projectDir, file);
|
|
88
|
-
assert(fs.existsSync(target), `Missing installed file: ${file}`);
|
|
89
|
-
const actual = fs.readFileSync(target, 'utf8').replace(/\r\n/g, '\n');
|
|
90
|
-
const expected = content.replace(/\r\n/g, '\n');
|
|
91
|
-
assert(actual === expected, `Installed adapter differs from renderer: ${file}`);
|
|
92
|
-
}
|
|
93
|
-
for (const { configFile } of Object.values(ideConfigs)) {
|
|
94
|
-
assert(
|
|
95
|
-
expectedPaths.has(configFile) || !fs.existsSync(resolveWithin(projectDir, configFile)),
|
|
96
|
-
`Unselected adapter was installed: ${configFile}`
|
|
97
|
-
);
|
|
98
|
-
}
|
|
99
|
-
if (requireInventory) {
|
|
100
|
-
assert.equal(manifest.fileInventory?.schemaVersion, 1, 'Missing file inventory');
|
|
101
|
-
const { collectManagedFiles } = require('../cli/lib/update-transaction');
|
|
102
|
-
const managedFiles = collectManagedFiles({ projectDir, packs });
|
|
103
|
-
assert.deepEqual(
|
|
104
|
-
Object.keys(manifest.fileInventory.files).sort(),
|
|
105
|
-
managedFiles.map((entry) => entry.file).sort(),
|
|
106
|
-
'Inventory does not cover exactly the selected framework files'
|
|
107
|
-
);
|
|
108
|
-
for (const { file } of managedFiles) {
|
|
109
|
-
const actualHash = crypto
|
|
110
|
-
.createHash('sha256')
|
|
111
|
-
.update(fs.readFileSync(resolveWithin(projectDir, file)))
|
|
112
|
-
.digest('hex');
|
|
113
|
-
assert.equal(
|
|
114
|
-
manifest.fileInventory.files[file],
|
|
115
|
-
actualHash,
|
|
116
|
-
`Inventory differs from installed bytes: ${file}`
|
|
117
|
-
);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
return files.length;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
function verifyDiagnostics(report, { packs, tools, derived }) {
|
|
124
|
-
assert.equal(report.schemaVersion, 1, 'Unsupported diagnostic report');
|
|
125
|
-
assert.equal(report.versions.installed, derived.product.version);
|
|
126
|
-
assert.equal(report.versions.cli, derived.product.version);
|
|
127
|
-
assert.equal(report.versions.published, null, 'Doctor must not invent npm freshness evidence');
|
|
128
|
-
sameSelection(
|
|
129
|
-
report.packs.map(({ id }) => id),
|
|
130
|
-
packs,
|
|
131
|
-
'diagnostic packs'
|
|
132
|
-
);
|
|
133
|
-
sameSelection(
|
|
134
|
-
report.integrations.map(({ tool }) => tool),
|
|
135
|
-
tools,
|
|
136
|
-
'diagnostic integrations'
|
|
137
|
-
);
|
|
138
|
-
for (const integration of report.integrations) {
|
|
139
|
-
assert.deepEqual(
|
|
140
|
-
integration,
|
|
141
|
-
{
|
|
142
|
-
tool: integration.tool,
|
|
143
|
-
...derived.diagnostics.integrations[integration.tool],
|
|
144
|
-
ownership: 'unchanged',
|
|
145
|
-
hostVerified: false,
|
|
146
|
-
},
|
|
147
|
-
'An installed adapter must not imply verified host execution'
|
|
148
|
-
);
|
|
149
|
-
}
|
|
150
|
-
const expectedErrors = [];
|
|
151
|
-
assert.deepEqual(
|
|
152
|
-
report.checks
|
|
153
|
-
.filter(({ status }) => status === 'error')
|
|
154
|
-
.map(({ code, path: file }) => `${code}:${file}`)
|
|
155
|
-
.sort(),
|
|
156
|
-
expectedErrors.sort(),
|
|
157
|
-
'Every advertised pack must install without delivery errors'
|
|
158
|
-
);
|
|
159
|
-
assert.equal(report.summary.errors, expectedErrors.length);
|
|
160
|
-
assert.equal(report.inventory.status, 'complete');
|
|
161
|
-
assert(report.inventory.trackedFiles > 0, 'Doctor did not inspect the ownership inventory');
|
|
162
|
-
assert.equal(
|
|
163
|
-
report.checks.filter(({ code }) => code === 'file.unchanged').length,
|
|
164
|
-
report.inventory.trackedFiles,
|
|
165
|
-
'Every freshly installed managed file must have matching ownership'
|
|
166
|
-
);
|
|
167
|
-
assert(!report.checks.some(({ code }) => ['file.modified', 'file.unowned'].includes(code)));
|
|
168
|
-
for (const runtime of report.runtimes.filter(({ runtime }) => runtime === 'python')) {
|
|
169
|
-
assert.equal(
|
|
170
|
-
runtime.imports,
|
|
171
|
-
'not-verified',
|
|
172
|
-
'Presence of requirements cannot prove Python imports'
|
|
173
|
-
);
|
|
174
|
-
assert.equal(runtime.environment, 'missing', 'Contract checks must not provision Python');
|
|
175
|
-
}
|
|
176
|
-
const expectedStatus = expectedErrors.length
|
|
177
|
-
? 'error'
|
|
178
|
-
: report.summary.warnings
|
|
179
|
-
? 'warning'
|
|
180
|
-
: 'ok';
|
|
181
|
-
assert.equal(report.status, expectedStatus);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
function run(command, args, { cwd, env, timeout = 60000, expectedExitCode = 0 }) {
|
|
185
|
-
const result = spawnSync(command, args, {
|
|
186
|
-
cwd,
|
|
187
|
-
env,
|
|
188
|
-
timeout,
|
|
189
|
-
encoding: 'utf8',
|
|
190
|
-
maxBuffer: 16 * 1024 * 1024,
|
|
191
|
-
windowsHide: true,
|
|
192
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
193
|
-
});
|
|
194
|
-
if (result.error || result.status !== expectedExitCode) {
|
|
195
|
-
const reason = result.error
|
|
196
|
-
? result.error.message
|
|
197
|
-
: `exit ${result.status}, signal ${result.signal || 'none'}`;
|
|
198
|
-
throw new Error(
|
|
199
|
-
`${path.basename(command)} ${args.join(' ')}: ${reason}\n${result.stdout || ''}${result.stderr || ''}`
|
|
200
|
-
);
|
|
201
|
-
}
|
|
202
|
-
return result.stdout;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/** Invoke npm without a shell, including Windows where npm.cmd is not executable. */
|
|
206
|
-
function npmCommand(env) {
|
|
207
|
-
const candidates = [
|
|
208
|
-
env.npm_execpath,
|
|
209
|
-
path.join(path.dirname(process.execPath), 'node_modules', 'npm', 'bin', 'npm-cli.js'),
|
|
210
|
-
...(env.PATH || env.Path || '')
|
|
211
|
-
.split(path.delimiter)
|
|
212
|
-
.map((dir) => path.join(dir, 'node_modules', 'npm', 'bin', 'npm-cli.js')),
|
|
213
|
-
];
|
|
214
|
-
const cli = candidates.find(
|
|
215
|
-
(candidate) => candidate && /npm-cli\.js$/i.test(candidate) && fs.existsSync(candidate)
|
|
216
|
-
);
|
|
217
|
-
if (cli) return { command: process.execPath, args: [cli] };
|
|
218
|
-
assert(process.platform !== 'win32', 'Could not locate npm-cli.js beside Node or on PATH');
|
|
219
|
-
return { command: 'npm', args: [] };
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
function packAndExtract(tempRoot, env) {
|
|
223
|
-
const destination = path.join(tempRoot, 'distribution');
|
|
224
|
-
fs.mkdirSync(destination);
|
|
225
|
-
const npm = npmCommand(env);
|
|
226
|
-
const output = run(
|
|
227
|
-
npm.command,
|
|
228
|
-
[
|
|
229
|
-
...npm.args,
|
|
230
|
-
'pack',
|
|
231
|
-
'--ignore-scripts',
|
|
232
|
-
'--offline',
|
|
233
|
-
'--json',
|
|
234
|
-
'--pack-destination',
|
|
235
|
-
destination,
|
|
236
|
-
'--cache',
|
|
237
|
-
path.join(tempRoot, 'npm-cache'),
|
|
238
|
-
],
|
|
239
|
-
{ cwd: REPO_ROOT, env, timeout: 120000 }
|
|
240
|
-
);
|
|
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
|
-
);
|
|
248
|
-
const archive = resolveWithin(destination, packed[0].filename);
|
|
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/);
|
|
255
|
-
for (const entry of entries) {
|
|
256
|
-
assert(entry.startsWith('package/'), `Unexpected npm archive entry: ${entry}`);
|
|
257
|
-
resolveWithin(destination, entry);
|
|
258
|
-
}
|
|
259
|
-
run('tar', ['-xzf', archiveName, '-C', '.'], { cwd: destination, env });
|
|
260
|
-
const packageRoot = path.join(destination, 'package');
|
|
261
|
-
assert(
|
|
262
|
-
!fs.existsSync(path.join(packageRoot, 'registry.yaml')),
|
|
263
|
-
'npm contract must work without registry.yaml'
|
|
264
|
-
);
|
|
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
|
-
);
|
|
273
|
-
return packageRoot;
|
|
274
|
-
}
|
|
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
|
-
|
|
456
|
-
function runMatrix(packageRoot, variant, tempRoot, env, reference) {
|
|
457
|
-
const allPacks = Object.keys(reference.packDefinitions).filter(
|
|
458
|
-
(id) => !reference.packDefinitions[id].disabled
|
|
459
|
-
);
|
|
460
|
-
const allTools = Object.keys(reference.ideConfigs);
|
|
461
|
-
assert(allTools.length > 0, 'IDE_CONFIGS must expose at least one target');
|
|
462
|
-
const selections = [
|
|
463
|
-
{ argument: 'core', packs: ['core'] },
|
|
464
|
-
{ argument: 'core,osint', packs: ['core', 'osint'] },
|
|
465
|
-
{ argument: 'core,memory', packs: ['core', 'memory'] },
|
|
466
|
-
{ argument: 'all', packs: allPacks },
|
|
467
|
-
];
|
|
468
|
-
const toolSelections = [...allTools.map((tool) => [tool]), allTools];
|
|
469
|
-
let installations = 0;
|
|
470
|
-
let files = 0;
|
|
471
|
-
for (const selection of selections) {
|
|
472
|
-
for (const tools of toolSelections) {
|
|
473
|
-
const label = `${variant}/${selection.argument}/${tools.join(',')}`;
|
|
474
|
-
const projectDir = path.join(tempRoot, `${variant}-project-${++installations}`);
|
|
475
|
-
fs.mkdirSync(projectDir);
|
|
476
|
-
try {
|
|
477
|
-
run(
|
|
478
|
-
process.execPath,
|
|
479
|
-
[
|
|
480
|
-
path.join(packageRoot, 'tools', 'cli', 'bmad-plus-cli.js'),
|
|
481
|
-
'install',
|
|
482
|
-
'--yes',
|
|
483
|
-
'--lang',
|
|
484
|
-
'en',
|
|
485
|
-
'--directory',
|
|
486
|
-
projectDir,
|
|
487
|
-
'--packs',
|
|
488
|
-
selection.argument,
|
|
489
|
-
'--tools',
|
|
490
|
-
tools.join(','),
|
|
491
|
-
],
|
|
492
|
-
{ cwd: projectDir, env }
|
|
493
|
-
);
|
|
494
|
-
files += verifyInstallation(projectDir, { ...reference, packs: selection.packs, tools });
|
|
495
|
-
const expectedExitCode = 0;
|
|
496
|
-
const diagnosis = run(
|
|
497
|
-
process.execPath,
|
|
498
|
-
[
|
|
499
|
-
path.join(packageRoot, 'tools', 'cli', 'bmad-plus-cli.js'),
|
|
500
|
-
'doctor',
|
|
501
|
-
'--json',
|
|
502
|
-
'--directory',
|
|
503
|
-
projectDir,
|
|
504
|
-
],
|
|
505
|
-
{ cwd: projectDir, env, expectedExitCode }
|
|
506
|
-
);
|
|
507
|
-
verifyDiagnostics(JSON.parse(diagnosis), {
|
|
508
|
-
packs: selection.packs,
|
|
509
|
-
tools,
|
|
510
|
-
derived: reference.derived,
|
|
511
|
-
variant,
|
|
512
|
-
});
|
|
513
|
-
if (installations === 1) verifyNexusConsumer(packageRoot, projectDir, env);
|
|
514
|
-
if (selection.packs.includes('dev-studio')) {
|
|
515
|
-
const list = JSON.parse(
|
|
516
|
-
run(
|
|
517
|
-
process.execPath,
|
|
518
|
-
[
|
|
519
|
-
path.join(packageRoot, 'tools/cli/bmad-plus-cli.js'),
|
|
520
|
-
'studio',
|
|
521
|
-
'list',
|
|
522
|
-
'--directory',
|
|
523
|
-
projectDir,
|
|
524
|
-
'--json',
|
|
525
|
-
],
|
|
526
|
-
{ cwd: projectDir, env }
|
|
527
|
-
)
|
|
528
|
-
);
|
|
529
|
-
assert.equal(list.workflows.length, reference.derived.packs['dev-studio'].workflowCount);
|
|
530
|
-
const prepared = JSON.parse(
|
|
531
|
-
run(
|
|
532
|
-
process.execPath,
|
|
533
|
-
[
|
|
534
|
-
path.join(packageRoot, 'tools/cli/bmad-plus-cli.js'),
|
|
535
|
-
'studio',
|
|
536
|
-
'prepare',
|
|
537
|
-
'create-ux-design',
|
|
538
|
-
'--request',
|
|
539
|
-
'Design the sign-in flow',
|
|
540
|
-
'--directory',
|
|
541
|
-
projectDir,
|
|
542
|
-
'--json',
|
|
543
|
-
],
|
|
544
|
-
{ cwd: projectDir, env }
|
|
545
|
-
)
|
|
546
|
-
);
|
|
547
|
-
assert.equal(prepared.status, 'ready');
|
|
548
|
-
assert.equal(prepared.executed, false);
|
|
549
|
-
assert(prepared.instructions.some((item) => item.path.endsWith('/create-ux-design.md')));
|
|
550
|
-
assert(!fs.existsSync(path.join(projectDir, prepared.outputPath)));
|
|
551
|
-
}
|
|
552
|
-
if (tools.length === allTools.length) {
|
|
553
|
-
const output = run(
|
|
554
|
-
process.execPath,
|
|
555
|
-
[
|
|
556
|
-
path.join(packageRoot, 'tools', 'cli', 'bmad-plus-cli.js'),
|
|
557
|
-
'update-check',
|
|
558
|
-
'--offline',
|
|
559
|
-
'--json',
|
|
560
|
-
'--directory',
|
|
561
|
-
projectDir,
|
|
562
|
-
],
|
|
563
|
-
{ cwd: projectDir, env }
|
|
564
|
-
);
|
|
565
|
-
const checked = JSON.parse(output);
|
|
566
|
-
assert.equal(
|
|
567
|
-
checked.status,
|
|
568
|
-
'unknown',
|
|
569
|
-
'Offline check must not claim the latest release'
|
|
570
|
-
);
|
|
571
|
-
assert.equal(checked.updateAvailable, null);
|
|
572
|
-
assert.equal(checked.canAutoApply, false);
|
|
573
|
-
}
|
|
574
|
-
} catch (error) {
|
|
575
|
-
throw new Error(`${label}: ${error.message}`, { cause: error });
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
console.log(
|
|
579
|
-
`OK ${variant}: ${selection.argument} across ${toolSelections.length} tool selections`
|
|
580
|
-
);
|
|
581
|
-
}
|
|
582
|
-
return { installations, files };
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
function main() {
|
|
586
|
-
const { PACKS, DERIVED } = require('../cli/lib/packs');
|
|
587
|
-
const { IDE_CONFIGS } = require('../cli/lib/ide-config');
|
|
588
|
-
const { generateUserFiles } = require('./generate-adapters');
|
|
589
|
-
assert(
|
|
590
|
-
DERIVED && typeof generateUserFiles === 'function',
|
|
591
|
-
'Generated install contract exports are missing'
|
|
592
|
-
);
|
|
593
|
-
const reference = {
|
|
594
|
-
packDefinitions: PACKS,
|
|
595
|
-
derived: DERIVED,
|
|
596
|
-
ideConfigs: IDE_CONFIGS,
|
|
597
|
-
renderFiles: generateUserFiles,
|
|
598
|
-
requireInventory: true,
|
|
599
|
-
};
|
|
600
|
-
const tempParent = fs.realpathSync(os.tmpdir());
|
|
601
|
-
const tempRoot = fs.mkdtempSync(path.join(tempParent, TEMP_PREFIX));
|
|
602
|
-
try {
|
|
603
|
-
const brain = path.join(tempRoot, 'brain');
|
|
604
|
-
fs.mkdirSync(brain);
|
|
605
|
-
const env = {
|
|
606
|
-
...process.env,
|
|
607
|
-
BMAD_PLUS_BRAIN: brain,
|
|
608
|
-
NODE_PATH: path.join(REPO_ROOT, 'node_modules'),
|
|
609
|
-
NO_COLOR: '1',
|
|
610
|
-
NPM_CONFIG_UPDATE_NOTIFIER: 'false',
|
|
611
|
-
};
|
|
612
|
-
const source = runMatrix(REPO_ROOT, 'source', tempRoot, env, reference);
|
|
613
|
-
const packageRoot = packAndExtract(tempRoot, env);
|
|
614
|
-
const { checkPythonDelivery } = require('./pack-delivery');
|
|
615
|
-
const { loadRegistry } = require('./generate');
|
|
616
|
-
assert.deepEqual(
|
|
617
|
-
checkPythonDelivery(loadRegistry(), {
|
|
618
|
-
packageRoot,
|
|
619
|
-
packageJson: JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8')),
|
|
620
|
-
}),
|
|
621
|
-
[],
|
|
622
|
-
'Declared Python resources must exist in the real npm archive'
|
|
623
|
-
);
|
|
624
|
-
const packaged = runMatrix(packageRoot, 'npm', tempRoot, env, reference);
|
|
625
|
-
console.log(
|
|
626
|
-
`Install contract passed: ${source.installations + packaged.installations} installs and doctor JSON reports, ${source.files + packaged.files} files compared (source + npm without registry.yaml).`
|
|
627
|
-
);
|
|
628
|
-
return 0;
|
|
629
|
-
} finally {
|
|
630
|
-
// Delete only this mkdtemp-owned direct child of the verified temp directory.
|
|
631
|
-
assert(
|
|
632
|
-
path.dirname(tempRoot) === tempParent && path.basename(tempRoot).startsWith(TEMP_PREFIX)
|
|
633
|
-
);
|
|
634
|
-
assert(fs.realpathSync(tempRoot) === tempRoot && !fs.lstatSync(tempRoot).isSymbolicLink());
|
|
635
|
-
fs.rmSync(tempRoot, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
if (require.main === module) {
|
|
640
|
-
try {
|
|
641
|
-
process.exitCode = main();
|
|
642
|
-
} catch (error) {
|
|
643
|
-
console.error(`Install contract failed: ${error.message}`);
|
|
644
|
-
process.exitCode = 1;
|
|
645
|
-
}
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
module.exports = { main, resolveWithin, verifyInstallation };
|