bmad-plus 0.16.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 +24 -0
- package/README.md +38 -20
- package/package.json +3 -1
- package/readme-international/README.de.md +12 -7
- package/readme-international/README.es.md +12 -7
- package/readme-international/README.fr.md +12 -7
- 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/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/commands/autoconfig.js +4 -2
- package/tools/cli/commands/uat.js +21 -5
- package/tools/cli/i18n.js +10 -0
- package/tools/cli/lib/packs.js +1 -1
- package/tools/cli/lib/uat.js +18 -0
- package/tools/release/publication-content.js +1 -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
|
+
}
|
|
@@ -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):
|
|
@@ -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');
|
|
@@ -17,11 +17,27 @@ function fail(message) {
|
|
|
17
17
|
|
|
18
18
|
function resolveSpec(target, paths) {
|
|
19
19
|
if (!target) fail('an id or a spec file is required');
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
20
|
+
if (target.endsWith('.json')) {
|
|
21
|
+
const file = path.resolve(target);
|
|
22
|
+
if (!fs.existsSync(file)) fail(`no spec at ${file}`);
|
|
23
|
+
return uat.loadSpec(file);
|
|
24
|
+
}
|
|
25
|
+
const direct = path.join(paths.specs, `${target}.json`);
|
|
26
|
+
if (fs.existsSync(direct)) return uat.loadSpec(direct);
|
|
27
|
+
// A recipe is named by its id, not by its file: a copied template keeps its own name
|
|
28
|
+
// (the shipped example-uat-spec.json holds the id example-1.0.0).
|
|
29
|
+
if (fs.existsSync(paths.specs)) {
|
|
30
|
+
for (const name of fs.readdirSync(paths.specs).filter((entry) => entry.endsWith('.json'))) {
|
|
31
|
+
try {
|
|
32
|
+
const loaded = uat.loadSpec(path.join(paths.specs, name));
|
|
33
|
+
if (loaded.spec.id === target) return loaded;
|
|
34
|
+
} catch {
|
|
35
|
+
// an unreadable file cannot be the recipe asked for
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
fail(`no spec with the id "${target}" in ${paths.specs}`);
|
|
40
|
+
return null;
|
|
25
41
|
}
|
|
26
42
|
|
|
27
43
|
function readStdin() {
|
package/tools/cli/i18n.js
CHANGED
|
@@ -21,6 +21,7 @@ const LANGUAGES = {
|
|
|
21
21
|
selected_packs: 'Selected packs',
|
|
22
22
|
enter_name: 'Your first name (agents use it to greet you)',
|
|
23
23
|
comm_language: 'Communication language',
|
|
24
|
+
uat_environment: 'Test environment for human acceptance recipes (URL, blank if none)',
|
|
24
25
|
exec_mode: 'Execution mode',
|
|
25
26
|
exec_manual: 'Manual — You call agents yourself',
|
|
26
27
|
exec_autopilot: 'Autopilot — Nexus manages the full pipeline',
|
|
@@ -109,6 +110,7 @@ const LANGUAGES = {
|
|
|
109
110
|
selected_packs: 'Packs sélectionnés',
|
|
110
111
|
enter_name: 'Ton prénom (les agents l\'utilisent pour te saluer)',
|
|
111
112
|
comm_language: 'Langue de communication',
|
|
113
|
+
uat_environment: 'Environnement de test pour les recettes humaines (URL, vide si aucun)',
|
|
112
114
|
exec_mode: 'Mode d\'exécution',
|
|
113
115
|
exec_manual: 'Manuel — Tu appelles les agents toi-même',
|
|
114
116
|
exec_autopilot: 'Autopilot — Nexus gère tout le pipeline',
|
|
@@ -195,6 +197,7 @@ const LANGUAGES = {
|
|
|
195
197
|
selected_packs: 'Packs seleccionados',
|
|
196
198
|
enter_name: 'Tu nombre (los agentes lo usan para saludarte)',
|
|
197
199
|
comm_language: 'Idioma de comunicación',
|
|
200
|
+
uat_environment: 'Entorno de pruebas para las recetas de aceptación (URL, vacío si no hay)',
|
|
198
201
|
exec_mode: 'Modo de ejecución',
|
|
199
202
|
exec_manual: 'Manual — Tú llamas a los agentes',
|
|
200
203
|
exec_autopilot: 'Autopilot — Nexus gestiona todo el pipeline',
|
|
@@ -281,6 +284,7 @@ const LANGUAGES = {
|
|
|
281
284
|
selected_packs: 'Ausgewählte Packs',
|
|
282
285
|
enter_name: 'Dein Vorname (Agenten verwenden ihn zur Begrüßung)',
|
|
283
286
|
comm_language: 'Kommunikationssprache',
|
|
287
|
+
uat_environment: 'Testumgebung für menschliche Abnahmen (URL, leer wenn keine)',
|
|
284
288
|
exec_mode: 'Ausführungsmodus',
|
|
285
289
|
exec_manual: 'Manuell — Du rufst Agenten selbst auf',
|
|
286
290
|
exec_autopilot: 'Autopilot — Nexus verwaltet die gesamte Pipeline',
|
|
@@ -367,6 +371,7 @@ const LANGUAGES = {
|
|
|
367
371
|
selected_packs: 'Packs selecionados',
|
|
368
372
|
enter_name: 'Seu nome (os agentes usam para te cumprimentar)',
|
|
369
373
|
comm_language: 'Idioma de comunicação',
|
|
374
|
+
uat_environment: 'Ambiente de teste para receitas de aceitação humana (URL, vazio se não houver)',
|
|
370
375
|
exec_mode: 'Modo de execução',
|
|
371
376
|
exec_manual: 'Manual — Você chama os agentes',
|
|
372
377
|
exec_autopilot: 'Autopilot — Nexus gerencia todo o pipeline',
|
|
@@ -453,6 +458,7 @@ const LANGUAGES = {
|
|
|
453
458
|
selected_packs: 'Выбранные пакеты',
|
|
454
459
|
enter_name: 'Ваше имя (агенты используют его для приветствия)',
|
|
455
460
|
comm_language: 'Язык общения',
|
|
461
|
+
uat_environment: 'Тестовая среда для ручной приёмки (URL, пусто если нет)',
|
|
456
462
|
exec_mode: 'Режим выполнения',
|
|
457
463
|
exec_manual: 'Ручной — Вы вызываете агентов сами',
|
|
458
464
|
exec_autopilot: 'Автопилот — Nexus управляет всем конвейером',
|
|
@@ -539,6 +545,7 @@ const LANGUAGES = {
|
|
|
539
545
|
selected_packs: '已选择的包',
|
|
540
546
|
enter_name: '您的名字(代理会用它来问候您)',
|
|
541
547
|
comm_language: '沟通语言',
|
|
548
|
+
uat_environment: '人工验收的测试环境(URL,没有则留空)',
|
|
542
549
|
exec_mode: '执行模式',
|
|
543
550
|
exec_manual: '手动 — 您自己调用代理',
|
|
544
551
|
exec_autopilot: '自动驾驶 — Nexus 管理整个流程',
|
|
@@ -625,6 +632,7 @@ const LANGUAGES = {
|
|
|
625
632
|
selected_packs: 'חבילות שנבחרו',
|
|
626
633
|
enter_name: 'השם שלך (הסוכנים משתמשים בו לברכך)',
|
|
627
634
|
comm_language: 'שפת תקשורת',
|
|
635
|
+
uat_environment: 'סביבת בדיקה לבדיקות קבלה אנושיות (כתובת URL, ריק אם אין)',
|
|
628
636
|
exec_mode: 'מצב ביצוע',
|
|
629
637
|
exec_manual: 'ידני — אתה קורא לסוכנים בעצמך',
|
|
630
638
|
exec_autopilot: 'טייס אוטומטי — Nexus מנהל את כל הצינור',
|
|
@@ -711,6 +719,7 @@ const LANGUAGES = {
|
|
|
711
719
|
selected_packs: '選択されたパック',
|
|
712
720
|
enter_name: 'あなたの名前(エージェントが挨拶に使います)',
|
|
713
721
|
comm_language: 'コミュニケーション言語',
|
|
722
|
+
uat_environment: '人による受け入れ確認のテスト環境(URL、なければ空欄)',
|
|
714
723
|
exec_mode: '実行モード',
|
|
715
724
|
exec_manual: '手動 — 自分でエージェントを呼び出す',
|
|
716
725
|
exec_autopilot: 'オートパイロット — Nexusがパイプライン全体を管理',
|
|
@@ -797,6 +806,7 @@ const LANGUAGES = {
|
|
|
797
806
|
selected_packs: 'Pack selezionati',
|
|
798
807
|
enter_name: 'Il tuo nome (gli agenti lo usano per salutarti)',
|
|
799
808
|
comm_language: 'Lingua di comunicazione',
|
|
809
|
+
uat_environment: 'Ambiente di test per le verifiche di accettazione umana (URL, vuoto se nessuno)',
|
|
800
810
|
exec_mode: 'Modalità di esecuzione',
|
|
801
811
|
exec_manual: 'Manuale — Chiami tu gli agenti',
|
|
802
812
|
exec_autopilot: 'Autopilot — Nexus gestisce l\'intera pipeline',
|
package/tools/cli/lib/packs.js
CHANGED
package/tools/cli/lib/uat.js
CHANGED
|
@@ -247,6 +247,15 @@ function validateSpec(spec, options = {}) {
|
|
|
247
247
|
`${at}: writes: true requires verify {kind: ${VERIFY_KINDS.join('|')}, text} — the read-only check run after the tester`
|
|
248
248
|
);
|
|
249
249
|
}
|
|
250
|
+
for (const [line, text] of (step.do || []).entries()) {
|
|
251
|
+
// A tester copies "doctor." from "Run <code>…doctor</code>." into the terminal —
|
|
252
|
+
// it happened on the first human run of BMAD+'s own recipe.
|
|
253
|
+
if (/<\/code>\s*[.,;:!?](?:\s|$)/.test(String(text))) {
|
|
254
|
+
warnings.push(
|
|
255
|
+
`${at}: "do" line ${line + 1} puts punctuation right after a command — testers copy it into the terminal`
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
250
259
|
const letters = new Set();
|
|
251
260
|
for (const expect of step.expect || []) {
|
|
252
261
|
for (const key of unknownKeys(expect, EXPECT_KEYS))
|
|
@@ -706,6 +715,13 @@ function gate({ spec, runs, triage }) {
|
|
|
706
715
|
);
|
|
707
716
|
} else if (entry.decision === 'accept-risk' && !String(entry.decidedBy || '').trim()) {
|
|
708
717
|
reasons.push(`${failure.step}/${failure.expect}: accept-risk requires decidedBy`);
|
|
718
|
+
} else if (failure.state === 'blocked' && entry.decision !== 'accept-risk') {
|
|
719
|
+
// A blocked expectation was never observed. Classifying why does not establish
|
|
720
|
+
// anything about the product: replay it, or have someone accept the risk by name.
|
|
721
|
+
// (First run of BMAD+'s own recipe: 17 of 21 blocked would otherwise have passed.)
|
|
722
|
+
reasons.push(
|
|
723
|
+
`${failure.step}/${failure.expect} was blocked: nothing was observed — replay it, or record accept-risk with decidedBy`
|
|
724
|
+
);
|
|
709
725
|
}
|
|
710
726
|
}
|
|
711
727
|
// A tick on a step that writes is not a write: the read-only proof settles it.
|
|
@@ -815,6 +831,8 @@ for (const [stepId, step] of Object.entries(run.steps || {})) {
|
|
|
815
831
|
if (!entry) problems.push(stepId + '/' + letter + ' ' + expect.state + ', not triaged');
|
|
816
832
|
else if (!entry.class || entry.class === 'undecided' || !entry.decision) {
|
|
817
833
|
problems.push(stepId + '/' + letter + ' triaged without a class and a decision');
|
|
834
|
+
} else if (expect.state === 'blocked' && (entry.decision !== 'accept-risk' || !entry.decidedBy)) {
|
|
835
|
+
problems.push(stepId + '/' + letter + ' was blocked and never observed; replay it or accept the risk by name');
|
|
818
836
|
}
|
|
819
837
|
}
|
|
820
838
|
}
|