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
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema": "bmad-plus/uat-spec/2",
|
|
3
|
+
"id": "example-1.0.0",
|
|
4
|
+
"product": "Example",
|
|
5
|
+
"versions": ["1.0.0"],
|
|
6
|
+
"date": "2026-09-16",
|
|
7
|
+
"language": "en",
|
|
8
|
+
"title": "What this delivery changes, in one sentence — this becomes the page title",
|
|
9
|
+
"subtitle": "Example · TEST environment · version 1.0.0 · 2026-09-16",
|
|
10
|
+
"environment": { "name": "TEST", "url": "https://test.example.com" },
|
|
11
|
+
"estimate": "5 min",
|
|
12
|
+
"intro": [
|
|
13
|
+
"<b>What the measurement showed.</b> The numbers taken before the delivery: how many cases change, where, production included. \"0\" is a number and gets said."
|
|
14
|
+
],
|
|
15
|
+
"warnings": [
|
|
16
|
+
"<b>Step 2 writes for real on TEST</b> (a button that persists). It is intended."
|
|
17
|
+
],
|
|
18
|
+
"witnesses": [
|
|
19
|
+
{
|
|
20
|
+
"id": "TEST-001",
|
|
21
|
+
"proof": "select updated_at, funder from folder where reference = 'TEST-001'",
|
|
22
|
+
"reads": ["1-nominal"],
|
|
23
|
+
"writes": ["2-the-gesture"]
|
|
24
|
+
}
|
|
25
|
+
],
|
|
26
|
+
"steps": [
|
|
27
|
+
{
|
|
28
|
+
"id": "0-setup",
|
|
29
|
+
"title": "Setup",
|
|
30
|
+
"duration": "1 min",
|
|
31
|
+
"where": [
|
|
32
|
+
"Open <a href=\"https://test.example.com\" target=\"_blank\" rel=\"noopener\">test.example.com</a> and sign in."
|
|
33
|
+
],
|
|
34
|
+
"do": ["Find the <span class=\"ecran\">Folders</span> menu."],
|
|
35
|
+
"expect": [{ "id": "a", "text": "I am signed in and I can see the list." }]
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"id": "1-nominal",
|
|
39
|
+
"title": "The nominal case (TEST-001)",
|
|
40
|
+
"duration": "2 min",
|
|
41
|
+
"stories": [{ "ref": "story-12", "criteria": ["AC-3"] }],
|
|
42
|
+
"where": [
|
|
43
|
+
"Folder <code>TEST-001</code> → tab <span class=\"ecran\">Technical</span> → the card named exactly as on screen."
|
|
44
|
+
],
|
|
45
|
+
"do": [
|
|
46
|
+
"One click per line: click <span class=\"ecran\">Analyse</span>.",
|
|
47
|
+
"Wait until the button reads <span class=\"ecran\">Analyse</span> again."
|
|
48
|
+
],
|
|
49
|
+
"expect": [
|
|
50
|
+
{
|
|
51
|
+
"id": "a",
|
|
52
|
+
"text": "The line reads exactly: <span class=\"ecran\">Label copied from the code, accents included or not</span>."
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"id": "b",
|
|
56
|
+
"text": "One fact per line: the <span class=\"ecran\">My box</span> checkbox is <b>ticked</b>."
|
|
57
|
+
}
|
|
58
|
+
]
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"id": "2-the-gesture",
|
|
62
|
+
"title": "The gesture that lifts the red",
|
|
63
|
+
"duration": "2 min",
|
|
64
|
+
"writes": true,
|
|
65
|
+
"verify": {
|
|
66
|
+
"kind": "sql",
|
|
67
|
+
"text": "select updated_at, funder from folder where reference = 'TEST-001' — updated_at must be later than the run"
|
|
68
|
+
},
|
|
69
|
+
"where": ["Same folder, same card."],
|
|
70
|
+
"do": [
|
|
71
|
+
"Click <span class=\"ecran\">Confirm</span>. <b>This writes for real.</b>",
|
|
72
|
+
"Come back to the folder with the browser's back button."
|
|
73
|
+
],
|
|
74
|
+
"expect": [
|
|
75
|
+
{
|
|
76
|
+
"id": "a",
|
|
77
|
+
"text": "The <span class=\"ecran\">My box</span> checkbox is now <b>ticked</b>."
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
"id": "b",
|
|
81
|
+
"text": "Positive control: <code>TEST-002</code> is <b>unchanged</b> — its box was already ticked and no link appeared."
|
|
82
|
+
}
|
|
83
|
+
]
|
|
84
|
+
}
|
|
85
|
+
],
|
|
86
|
+
"closing": {
|
|
87
|
+
"text": [
|
|
88
|
+
"<b>What did not change</b>: no migration, and the deployment touched no data.",
|
|
89
|
+
"<b>What waits for a decision</b>: name it here, or say there is none."
|
|
90
|
+
]
|
|
91
|
+
},
|
|
92
|
+
"authorNotes": [
|
|
93
|
+
"Free field for the agent who wrote this: doubts, witnesses not yet measured, labels to confirm. The page ignores it; the reviewer reads it."
|
|
94
|
+
]
|
|
95
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -105,7 +105,7 @@ const PROJECT_INSTRUCTIONS = [
|
|
|
105
105
|
'',
|
|
106
106
|
'## Repository Maintenance Rule',
|
|
107
107
|
'',
|
|
108
|
-
'
|
|
108
|
+
'The public README is README-DIST.md, generated by tools/release/publication-content.js. When it changes, you MUST synchronously update all translations in the readme-international/ directory (fr, es, de), which mirror it section by section.',
|
|
109
109
|
'',
|
|
110
110
|
...MEMORY_INSTRUCTIONS,
|
|
111
111
|
'',
|