bmad-plus 0.17.1 → 0.19.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 +30 -1
- package/README.md +16 -18
- package/package.json +3 -1
- package/readme-international/README.de.md +16 -19
- package/readme-international/README.es.md +16 -19
- package/readme-international/README.fr.md +16 -19
- package/src/bmad-plus/agents/agent-quality/SKILL.md +3 -2
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/code-review-steps/step-03-triage.md +15 -0
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/code-review.md +25 -4
- package/src/bmad-plus/skills/bmad-plus-uat/SKILL.md +61 -1
- package/src/bmad-plus/skills/bmad-plus-uat/ref/uat-results.schema.json +11 -0
- package/src/bmad-plus/skills/bmad-plus-uat/template/page.html +455 -137
- package/src/bmad-plus/skills/bmad-plus-uat/template/strings.json +230 -20
- package/tools/cli/bmad-plus-cli.js +1 -0
- package/tools/cli/commands/review.js +152 -0
- package/tools/cli/commands/uat.js +50 -5
- package/tools/cli/commands/update-check.js +5 -1
- package/tools/cli/commands/update.js +9 -3
- package/tools/cli/lib/install-manifest.js +19 -1
- package/tools/cli/lib/packs.js +1 -1
- package/tools/cli/lib/review.js +522 -0
- package/tools/cli/lib/uat.js +140 -2
|
@@ -89,6 +89,13 @@ function serve(spec, paths, options) {
|
|
|
89
89
|
response.writeHead(code, { 'content-type': type, 'cache-control': 'no-store' });
|
|
90
90
|
response.end(body);
|
|
91
91
|
};
|
|
92
|
+
// Loopback is not enough against the tester's own browser: a name rebound to 127.0.0.1
|
|
93
|
+
// is same-origin for it. Only the two names this server answers to are served.
|
|
94
|
+
const host = String(request.headers.host || '').toLowerCase();
|
|
95
|
+
const bound = server.address() ? server.address().port : port;
|
|
96
|
+
if (host !== `127.0.0.1:${bound}` && host !== `localhost:${bound}`) {
|
|
97
|
+
return send(403, JSON.stringify({ error: 'unexpected host' }));
|
|
98
|
+
}
|
|
92
99
|
if (request.method === 'GET' && (url.pathname === '/' || url.pathname === '/index.html')) {
|
|
93
100
|
return send(200, html, 'text/html; charset=utf-8');
|
|
94
101
|
}
|
|
@@ -116,7 +123,29 @@ function serve(spec, paths, options) {
|
|
|
116
123
|
const run = uat.normalizeRun(JSON.parse(Buffer.concat(chunks).toString('utf8')), spec);
|
|
117
124
|
if (run.runId !== runId || run.specId !== spec.id)
|
|
118
125
|
fail('run identity does not match the request');
|
|
119
|
-
|
|
126
|
+
// The page's rule holds on disk too: a copy older than the file never replaces it.
|
|
127
|
+
const file = path.join(resultsDir, `${runId}.json`);
|
|
128
|
+
if (fs.existsSync(file)) {
|
|
129
|
+
let existing = null;
|
|
130
|
+
try {
|
|
131
|
+
existing = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
132
|
+
} catch {
|
|
133
|
+
existing = null;
|
|
134
|
+
}
|
|
135
|
+
const was = Date.parse(existing && existing.updatedAt);
|
|
136
|
+
const now = Date.parse(run.updatedAt);
|
|
137
|
+
if (!Number.isNaN(was) && (Number.isNaN(now) || was > now)) {
|
|
138
|
+
send(
|
|
139
|
+
409,
|
|
140
|
+
JSON.stringify({
|
|
141
|
+
error: 'a newer copy of this run is already stored',
|
|
142
|
+
updatedAt: existing.updatedAt,
|
|
143
|
+
})
|
|
144
|
+
);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
fs.writeFileSync(file, JSON.stringify(run, null, 2));
|
|
120
149
|
const s = run.summary;
|
|
121
150
|
console.log(
|
|
122
151
|
`${new Date().toISOString().slice(11, 19)} ${runId} — ${s.passed} passed, ${s.failed} failed, ${s.blocked} blocked, ${s.unanswered} to do${run.finishedAt ? ' — finished' : ''}`
|
|
@@ -133,7 +162,9 @@ function serve(spec, paths, options) {
|
|
|
133
162
|
|
|
134
163
|
// Loopback only: a tester on another machine uses the artifact or the file (decision D5).
|
|
135
164
|
server.listen(port, '127.0.0.1', () => {
|
|
136
|
-
console.log(
|
|
165
|
+
console.log(
|
|
166
|
+
`uat serve: http://127.0.0.1:${server.address().port} (${spec.id}, ${spec.steps.length} steps)`
|
|
167
|
+
);
|
|
137
168
|
console.log(`results → ${resultsDir}`);
|
|
138
169
|
console.log('Ctrl+C to stop.');
|
|
139
170
|
});
|
|
@@ -188,9 +219,21 @@ module.exports = {
|
|
|
188
219
|
process.exitCode = 1;
|
|
189
220
|
return;
|
|
190
221
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
222
|
+
let page;
|
|
223
|
+
try {
|
|
224
|
+
page = uat.buildPage(spec, {
|
|
225
|
+
language: options.language || projectLanguage(projectDir),
|
|
226
|
+
});
|
|
227
|
+
} catch (error) {
|
|
228
|
+
// A refused page reads like a refused lint: same shape, so a machine reader sees a refusal, not a crash.
|
|
229
|
+
print(
|
|
230
|
+
json,
|
|
231
|
+
{ action, specId: spec.id, status: 'error', errors: [error.message], guarantees: [] },
|
|
232
|
+
[`${spec.id}: not built`, ` error ${error.message}`]
|
|
233
|
+
);
|
|
234
|
+
process.exitCode = 1;
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
194
237
|
fs.mkdirSync(paths.pages, { recursive: true });
|
|
195
238
|
const file = path.join(paths.pages, `uat-${spec.id}.html`);
|
|
196
239
|
fs.writeFileSync(file, page.html, 'utf8');
|
|
@@ -203,11 +246,13 @@ module.exports = {
|
|
|
203
246
|
specSha256: page.sha256,
|
|
204
247
|
language: page.language,
|
|
205
248
|
languages: Object.keys(uat.STRINGS),
|
|
249
|
+
guarantees: page.guarantees,
|
|
206
250
|
warnings: report.warnings,
|
|
207
251
|
},
|
|
208
252
|
[
|
|
209
253
|
`${file} — ${spec.steps.length} steps, ${(page.html.length / 1024).toFixed(0)} KiB, spec ${page.sha256.slice(0, 12)}`,
|
|
210
254
|
`opens in ${uat.STRINGS[page.language].name}; the tester can switch to any of the ${Object.keys(uat.STRINGS).length} languages on the page`,
|
|
255
|
+
`carries the ${page.guarantees.length} page guarantees: ${page.guarantees.join(', ')}`,
|
|
211
256
|
...report.warnings.map((line) => ` warning ${line}`),
|
|
212
257
|
]
|
|
213
258
|
);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/** Report release status without applying updates. */
|
|
2
2
|
const path = require('node:path');
|
|
3
3
|
const { checkForUpdate } = require('../lib/update-check');
|
|
4
|
+
const { notInstalledMessage, hasInstallManifest } = require('../lib/install-manifest');
|
|
4
5
|
|
|
5
6
|
module.exports = {
|
|
6
7
|
command: 'update-check',
|
|
@@ -12,8 +13,9 @@ module.exports = {
|
|
|
12
13
|
['--json', 'Print machine-readable JSON'],
|
|
13
14
|
],
|
|
14
15
|
action: async (options = {}) => {
|
|
16
|
+
const projectDir = path.resolve(options.directory || process.cwd());
|
|
15
17
|
const result = await checkForUpdate({
|
|
16
|
-
projectDir
|
|
18
|
+
projectDir,
|
|
17
19
|
runningVersion: require('../../../package.json').version,
|
|
18
20
|
refresh: Boolean(options.refresh),
|
|
19
21
|
offline: Boolean(options.offline),
|
|
@@ -23,6 +25,8 @@ module.exports = {
|
|
|
23
25
|
},
|
|
24
26
|
});
|
|
25
27
|
if (result.status.startsWith('invalid-')) process.exitCode = 1;
|
|
28
|
+
// stderr keeps --json output parseable while still telling a person where to run it.
|
|
29
|
+
if (!hasInstallManifest(projectDir)) console.error(notInstalledMessage(projectDir));
|
|
26
30
|
if (options.json) console.log(JSON.stringify(result));
|
|
27
31
|
else {
|
|
28
32
|
console.log(
|
|
@@ -9,7 +9,11 @@ const clack = require('@clack/prompts');
|
|
|
9
9
|
const pc = require('picocolors');
|
|
10
10
|
const { t } = require('../i18n');
|
|
11
11
|
const { safeTarget } = require('../../build/generate-adapters');
|
|
12
|
-
const {
|
|
12
|
+
const {
|
|
13
|
+
readInstallManifest,
|
|
14
|
+
notInstalledMessage,
|
|
15
|
+
hasInstallManifest,
|
|
16
|
+
} = require('../lib/install-manifest');
|
|
13
17
|
const { isExactVersion } = require('../lib/update-policy');
|
|
14
18
|
const { planUpdate, applyPlan, restoreUpdate, MANIFEST } = require('../lib/update-transaction');
|
|
15
19
|
|
|
@@ -51,6 +55,9 @@ module.exports = {
|
|
|
51
55
|
if (options.restore && (options.latest || options.expectedVersion)) {
|
|
52
56
|
throw new Error('--restore cannot be combined with --latest or --expected-version.');
|
|
53
57
|
}
|
|
58
|
+
if (!options.restore && !hasInstallManifest(projectDir)) {
|
|
59
|
+
throw new Error(notInstalledMessage(projectDir));
|
|
60
|
+
}
|
|
54
61
|
// The dispatcher resolves the target package; this local package may be older.
|
|
55
62
|
if (options.latest) {
|
|
56
63
|
const result = await require('../lib/update-dispatch').runLatestUpdate({
|
|
@@ -96,8 +103,7 @@ module.exports = {
|
|
|
96
103
|
);
|
|
97
104
|
}
|
|
98
105
|
const manifestPath = safeTarget(projectDir, MANIFEST);
|
|
99
|
-
if (!fs.existsSync(manifestPath))
|
|
100
|
-
throw new Error('BMAD+ is not installed in this directory. Run bmad-plus install first.');
|
|
106
|
+
if (!fs.existsSync(manifestPath)) throw new Error(notInstalledMessage(projectDir));
|
|
101
107
|
let manifest;
|
|
102
108
|
try {
|
|
103
109
|
manifest = readInstallManifest(manifestPath);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** Read the manifest fields used by maintenance commands without unsafe coercion. */
|
|
2
2
|
const fs = require('node:fs');
|
|
3
|
+
const path = require('node:path');
|
|
3
4
|
|
|
4
5
|
function readInstallManifest(manifestPath) {
|
|
5
6
|
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
@@ -21,4 +22,21 @@ function readInstallManifest(manifestPath) {
|
|
|
21
22
|
return manifest;
|
|
22
23
|
}
|
|
23
24
|
|
|
24
|
-
|
|
25
|
+
/**
|
|
26
|
+
* Guidance for maintenance commands run outside an installed project. A person who
|
|
27
|
+
* opens a terminal in a drive root or a parent folder must learn where to run them.
|
|
28
|
+
*/
|
|
29
|
+
function notInstalledMessage(projectDir) {
|
|
30
|
+
return [
|
|
31
|
+
`No BMAD+ installation found in ${projectDir} (missing _bmad/.bmad-plus-install.json).`,
|
|
32
|
+
'Run this command from your project folder, for example: cd "path/to/your-project"',
|
|
33
|
+
'or pass the folder explicitly: --directory "path/to/your-project".',
|
|
34
|
+
'To install BMAD+ in this folder instead: npx bmad-plus@latest install',
|
|
35
|
+
].join('\n');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function hasInstallManifest(projectDir) {
|
|
39
|
+
return fs.existsSync(path.join(projectDir, '_bmad', '.bmad-plus-install.json'));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
module.exports = { readInstallManifest, notInstalledMessage, hasInstallManifest };
|