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
|
@@ -3,12 +3,19 @@ const fs = require('node:fs');
|
|
|
3
3
|
|
|
4
4
|
function readInstallManifest(manifestPath) {
|
|
5
5
|
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
6
|
-
if (
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
6
|
+
if (
|
|
7
|
+
!manifest ||
|
|
8
|
+
typeof manifest !== 'object' ||
|
|
9
|
+
Array.isArray(manifest) ||
|
|
10
|
+
typeof manifest.version !== 'string' ||
|
|
11
|
+
!manifest.version.trim() ||
|
|
12
|
+
typeof manifest.installed !== 'string' ||
|
|
13
|
+
!Number.isFinite(Date.parse(manifest.installed)) ||
|
|
14
|
+
!Array.isArray(manifest.packs) ||
|
|
15
|
+
manifest.packs.length === 0 ||
|
|
16
|
+
manifest.packs.some((pack) => typeof pack !== 'string' || !/^[a-z0-9-]+$/.test(pack)) ||
|
|
17
|
+
(manifest.uiLanguage !== undefined && typeof manifest.uiLanguage !== 'string')
|
|
18
|
+
) {
|
|
12
19
|
throw new Error('Expected version, installed date, and a nonempty array of pack IDs.');
|
|
13
20
|
}
|
|
14
21
|
return manifest;
|
|
@@ -4,7 +4,13 @@ const path = require('node:path');
|
|
|
4
4
|
const semver = require('semver');
|
|
5
5
|
const { DERIVED } = require('./packs');
|
|
6
6
|
const { readInstallManifest } = require('./install-manifest');
|
|
7
|
-
const {
|
|
7
|
+
const {
|
|
8
|
+
inventoryOf,
|
|
9
|
+
collectManagedFiles,
|
|
10
|
+
inspectProjectLock,
|
|
11
|
+
MANIFEST,
|
|
12
|
+
LOCK,
|
|
13
|
+
} = require('./update-transaction');
|
|
8
14
|
const { fileHash } = require('./pack-copy');
|
|
9
15
|
const { contentHash } = require('./installed-adapters');
|
|
10
16
|
const {
|
|
@@ -86,6 +92,32 @@ function collectInstallationHealth({
|
|
|
86
92
|
}
|
|
87
93
|
report.versions.installed = manifest.version;
|
|
88
94
|
check('manifest.valid', 'pass', 'Manifest found: v' + manifest.version);
|
|
95
|
+
let lock = null;
|
|
96
|
+
try {
|
|
97
|
+
lock = inspectProjectLock(projectDir);
|
|
98
|
+
} catch (error) {
|
|
99
|
+
check('lock.unreadable', 'warning', 'Project lock cannot be inspected: ' + error.message, {
|
|
100
|
+
path: LOCK,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
if (lock) {
|
|
104
|
+
check(
|
|
105
|
+
lock.stale ? 'lock.stale' : 'lock.active',
|
|
106
|
+
lock.stale ? 'error' : 'warning',
|
|
107
|
+
lock.stale
|
|
108
|
+
? 'Stale project lock blocks install and update: ' +
|
|
109
|
+
lock.reason +
|
|
110
|
+
' If no BMAD+ install or update is running, delete ' +
|
|
111
|
+
LOCK +
|
|
112
|
+
' and retry.'
|
|
113
|
+
: 'An install or update holds the project lock (pid ' +
|
|
114
|
+
(lock.pid ?? 'unknown') +
|
|
115
|
+
', since ' +
|
|
116
|
+
lock.createdAt +
|
|
117
|
+
').',
|
|
118
|
+
{ path: LOCK }
|
|
119
|
+
);
|
|
120
|
+
}
|
|
89
121
|
check(
|
|
90
122
|
'version.cli',
|
|
91
123
|
manifest.version === cliVersion ? 'pass' : 'warning',
|
|
@@ -6,7 +6,8 @@ const clack = require('@clack/prompts');
|
|
|
6
6
|
const { GENERATED_MARKER, USER_CONFIG_MARKER } = require('../../build/generate-adapters');
|
|
7
7
|
|
|
8
8
|
const INSTALLER_MARKER = 'BMAD+ \u2014 AI Agent Configuration';
|
|
9
|
-
const contentHash =
|
|
9
|
+
const contentHash = (content) =>
|
|
10
|
+
crypto.createHash('sha256').update(String(content).replace(/\r\n/g, '\n')).digest('hex');
|
|
10
11
|
|
|
11
12
|
function classifyAdapter({ existing, content, previousHash, update = false, yes = false }) {
|
|
12
13
|
if (existing === undefined) return { action: 'write', backup: false };
|
|
@@ -16,7 +17,13 @@ function classifyAdapter({ existing, content, previousHash, update = false, yes
|
|
|
16
17
|
if (installed && existing === content) return { action: 'same', backup: false };
|
|
17
18
|
const unchanged = installed && previousHash === contentHash(existing);
|
|
18
19
|
if (adopted || (installed && !unchanged) || (!installed && !legacy && (update || !yes))) {
|
|
19
|
-
return {
|
|
20
|
+
return {
|
|
21
|
+
action: 'preserve',
|
|
22
|
+
backup: false,
|
|
23
|
+
adopted,
|
|
24
|
+
installed,
|
|
25
|
+
reason: adopted ? 'repository-adopted' : 'local-instructions',
|
|
26
|
+
};
|
|
20
27
|
}
|
|
21
28
|
return { action: 'write', backup: legacy || (!installed && yes), legacy };
|
|
22
29
|
}
|
|
@@ -26,14 +33,20 @@ function safeAdapterPath(projectDir, file) {
|
|
|
26
33
|
const root = path.resolve(projectDir);
|
|
27
34
|
const target = path.resolve(root, file);
|
|
28
35
|
const relative = path.relative(root, target);
|
|
29
|
-
if (
|
|
36
|
+
if (
|
|
37
|
+
!relative ||
|
|
38
|
+
relative === '..' ||
|
|
39
|
+
relative.startsWith('..' + path.sep) ||
|
|
40
|
+
path.isAbsolute(relative)
|
|
41
|
+
) {
|
|
30
42
|
throw new Error('Adapter path outside project: ' + file);
|
|
31
43
|
}
|
|
32
44
|
let current = root;
|
|
33
45
|
for (const part of relative.split(path.sep)) {
|
|
34
46
|
current = path.join(current, part);
|
|
35
47
|
try {
|
|
36
|
-
if (fs.lstatSync(current).isSymbolicLink())
|
|
48
|
+
if (fs.lstatSync(current).isSymbolicLink())
|
|
49
|
+
throw new Error('Adapter path is a symbolic link or junction: ' + current);
|
|
37
50
|
} catch (err) {
|
|
38
51
|
if (err.code !== 'ENOENT') throw err;
|
|
39
52
|
}
|
|
@@ -73,7 +86,11 @@ function writeIDEConfigs({ projectDir, files, yes = false, update = false, adapt
|
|
|
73
86
|
let suffix = '.bak';
|
|
74
87
|
let index = 1;
|
|
75
88
|
while (fs.existsSync(safeAdapterPath(projectDir, file + suffix))) suffix = `.bak.${index++}`;
|
|
76
|
-
fs.copyFileSync(
|
|
89
|
+
fs.copyFileSync(
|
|
90
|
+
target,
|
|
91
|
+
safeAdapterPath(projectDir, file + suffix),
|
|
92
|
+
fs.constants.COPYFILE_EXCL
|
|
93
|
+
);
|
|
77
94
|
outcome.backedUp.push(file);
|
|
78
95
|
clack.log.warn(`${file}: backed up to ${file + suffix} before refreshing.`);
|
|
79
96
|
}
|
|
@@ -86,4 +103,10 @@ function writeIDEConfigs({ projectDir, files, yes = false, update = false, adapt
|
|
|
86
103
|
return outcome;
|
|
87
104
|
}
|
|
88
105
|
|
|
89
|
-
module.exports = {
|
|
106
|
+
module.exports = {
|
|
107
|
+
INSTALLER_MARKER,
|
|
108
|
+
contentHash,
|
|
109
|
+
safeAdapterPath,
|
|
110
|
+
writeIDEConfigs,
|
|
111
|
+
classifyAdapter,
|
|
112
|
+
};
|
|
@@ -45,7 +45,9 @@ function resolveBrain(projectDir, homeDir = os.homedir()) {
|
|
|
45
45
|
const envBrain = process.env.BMAD_PLUS_BRAIN;
|
|
46
46
|
if (envBrain) {
|
|
47
47
|
if (isBrainCandidateDir(envBrain)) return { dir: envBrain, type: 'portfolio' };
|
|
48
|
-
clack.log.warn(
|
|
48
|
+
clack.log.warn(
|
|
49
|
+
`BMAD_PLUS_BRAIN is set but is not a directory: ${envBrain} — falling back to auto-detection`
|
|
50
|
+
);
|
|
49
51
|
}
|
|
50
52
|
|
|
51
53
|
let dir = path.resolve(projectDir);
|
|
@@ -129,7 +131,14 @@ function ensureGlobalBrain(brainRoot, templateDir, userName, commLang) {
|
|
|
129
131
|
* @param {string} [opts.homeDir] - Home directory (injectable for tests; defaults to os.homedir())
|
|
130
132
|
* @returns {void}
|
|
131
133
|
*/
|
|
132
|
-
function initMemory({
|
|
134
|
+
function initMemory({
|
|
135
|
+
projectDir,
|
|
136
|
+
bmadSrc,
|
|
137
|
+
userName,
|
|
138
|
+
commLang,
|
|
139
|
+
selectedPacks,
|
|
140
|
+
homeDir = os.homedir(),
|
|
141
|
+
}) {
|
|
133
142
|
const memoryDir = path.join(projectDir, '.agents', 'memory');
|
|
134
143
|
const sessionsDir = path.join(memoryDir, 'sessions');
|
|
135
144
|
const globalBrainRoot = path.join(homeDir, '.bmad-plus', 'brain');
|
|
@@ -170,7 +179,11 @@ function initMemory({ projectDir, bmadSrc, userName, commLang, selectedPacks, ho
|
|
|
170
179
|
fs.writeFileSync(
|
|
171
180
|
path.join(memoryDir, '.brain-link'),
|
|
172
181
|
JSON.stringify(
|
|
173
|
-
{
|
|
182
|
+
{
|
|
183
|
+
linked_brain: linkedBrain.dir,
|
|
184
|
+
brain_type: linkedBrain.type,
|
|
185
|
+
linked_at: new Date().toISOString(),
|
|
186
|
+
},
|
|
174
187
|
null,
|
|
175
188
|
2
|
|
176
189
|
),
|
|
@@ -195,7 +208,9 @@ function initMemory({ projectDir, bmadSrc, userName, commLang, selectedPacks, ho
|
|
|
195
208
|
};
|
|
196
209
|
fs.writeFileSync(
|
|
197
210
|
path.join(globalBrainRoot, 'projects', `${projHash}.yaml`),
|
|
198
|
-
Object.entries(projMeta)
|
|
211
|
+
Object.entries(projMeta)
|
|
212
|
+
.map(([k, v]) => `${k}: ${JSON.stringify(v)}`)
|
|
213
|
+
.join('\n'),
|
|
199
214
|
'utf8'
|
|
200
215
|
);
|
|
201
216
|
}
|
package/tools/cli/lib/packs.js
CHANGED
|
@@ -25,8 +25,6 @@ const PACKS = {
|
|
|
25
25
|
"data": [
|
|
26
26
|
"role-triggers.yaml"
|
|
27
27
|
],
|
|
28
|
-
"packDir": "pack-core",
|
|
29
|
-
"packSrcDir": "packs",
|
|
30
28
|
"required": true,
|
|
31
29
|
"desc": "Core agents & skills"
|
|
32
30
|
},
|
|
@@ -38,8 +36,6 @@ const PACKS = {
|
|
|
38
36
|
],
|
|
39
37
|
"skills": [],
|
|
40
38
|
"externalPackage": "osint-agent-package",
|
|
41
|
-
"packDir": "pack-osint",
|
|
42
|
-
"packSrcDir": "packs",
|
|
43
39
|
"desc": "OSINT & investigation"
|
|
44
40
|
},
|
|
45
41
|
"maker": {
|
|
@@ -50,16 +46,12 @@ const PACKS = {
|
|
|
50
46
|
],
|
|
51
47
|
"skills": [],
|
|
52
48
|
"data": [],
|
|
53
|
-
"packDir": "pack-maker",
|
|
54
|
-
"packSrcDir": "packs",
|
|
55
49
|
"desc": "Agent creation toolkit"
|
|
56
50
|
},
|
|
57
51
|
"shield": {
|
|
58
52
|
"name": "Shield",
|
|
59
53
|
"icon": "m",
|
|
60
|
-
"agents": [
|
|
61
|
-
"shield-orchestrator"
|
|
62
|
-
],
|
|
54
|
+
"agents": [],
|
|
63
55
|
"skills": [],
|
|
64
56
|
"packDir": "pack-shield",
|
|
65
57
|
"packSrcDir": "packs",
|
|
@@ -68,11 +60,7 @@ const PACKS = {
|
|
|
68
60
|
"seo": {
|
|
69
61
|
"name": "SEO",
|
|
70
62
|
"icon": "k",
|
|
71
|
-
"agents": [
|
|
72
|
-
"seo-scout",
|
|
73
|
-
"seo-chief",
|
|
74
|
-
"seo-judge"
|
|
75
|
-
],
|
|
63
|
+
"agents": [],
|
|
76
64
|
"skills": [],
|
|
77
65
|
"packDir": "pack-seo",
|
|
78
66
|
"packSrcDir": "packs",
|
|
@@ -81,9 +69,7 @@ const PACKS = {
|
|
|
81
69
|
"memory": {
|
|
82
70
|
"name": "Memory",
|
|
83
71
|
"icon": "x",
|
|
84
|
-
"agents": [
|
|
85
|
-
"zecher"
|
|
86
|
-
],
|
|
72
|
+
"agents": [],
|
|
87
73
|
"skills": [],
|
|
88
74
|
"packDir": "pack-memory",
|
|
89
75
|
"packSrcDir": "packs",
|
|
@@ -92,9 +78,7 @@ const PACKS = {
|
|
|
92
78
|
"dev-studio": {
|
|
93
79
|
"name": "Dev Studio",
|
|
94
80
|
"icon": "v",
|
|
95
|
-
"agents": [
|
|
96
|
-
"dev-studio-orchestrator"
|
|
97
|
-
],
|
|
81
|
+
"agents": [],
|
|
98
82
|
"skills": [
|
|
99
83
|
"dev-studio"
|
|
100
84
|
],
|
|
@@ -105,9 +89,7 @@ const PACKS = {
|
|
|
105
89
|
"backup": {
|
|
106
90
|
"name": "Backup",
|
|
107
91
|
"icon": "y",
|
|
108
|
-
"agents": [
|
|
109
|
-
"backup-agent"
|
|
110
|
-
],
|
|
92
|
+
"agents": [],
|
|
111
93
|
"skills": [],
|
|
112
94
|
"packDir": "pack-backup",
|
|
113
95
|
"packSrcDir": "packs",
|
|
@@ -116,9 +98,7 @@ const PACKS = {
|
|
|
116
98
|
"animated": {
|
|
117
99
|
"name": "Animated",
|
|
118
100
|
"icon": "z",
|
|
119
|
-
"agents": [
|
|
120
|
-
"animated-website-agent"
|
|
121
|
-
],
|
|
101
|
+
"agents": [],
|
|
122
102
|
"skills": [],
|
|
123
103
|
"packDir": "pack-animated",
|
|
124
104
|
"packSrcDir": "packs",
|
|
@@ -211,7 +191,7 @@ const DERIVED = {
|
|
|
211
191
|
"product": {
|
|
212
192
|
"code": "bmad-plus",
|
|
213
193
|
"displayName": "BMAD+",
|
|
214
|
-
"version": "0.
|
|
194
|
+
"version": "0.17.1",
|
|
215
195
|
"derivedFrom": "BMAD-METHOD v6.6.0"
|
|
216
196
|
},
|
|
217
197
|
"packOrder": [
|
|
@@ -246,6 +226,7 @@ const DERIVED = {
|
|
|
246
226
|
"order": 0,
|
|
247
227
|
"name": "Core",
|
|
248
228
|
"displayName": "Core Development",
|
|
229
|
+
"iconEmoji": "⚙️",
|
|
249
230
|
"required": true,
|
|
250
231
|
"installerAgentCount": 4,
|
|
251
232
|
"categoryAgentCount": 0,
|
|
@@ -306,6 +287,7 @@ const DERIVED = {
|
|
|
306
287
|
"order": 1,
|
|
307
288
|
"name": "OSINT",
|
|
308
289
|
"displayName": "OSINT Intelligence",
|
|
290
|
+
"iconEmoji": "🔍",
|
|
309
291
|
"required": false,
|
|
310
292
|
"installerAgentCount": 1,
|
|
311
293
|
"categoryAgentCount": 0,
|
|
@@ -328,7 +310,7 @@ const DERIVED = {
|
|
|
328
310
|
"id": "agent-shadow",
|
|
329
311
|
"name": "Shadow",
|
|
330
312
|
"role": "OSINT",
|
|
331
|
-
"description": "Investigation +
|
|
313
|
+
"description": "Investigation + Public-source research (governed, legal basis required)"
|
|
332
314
|
}
|
|
333
315
|
],
|
|
334
316
|
"summary": "Investigation & scraping (55+ Apify actors, 7 APIs).",
|
|
@@ -340,6 +322,7 @@ const DERIVED = {
|
|
|
340
322
|
"order": 2,
|
|
341
323
|
"name": "Maker",
|
|
342
324
|
"displayName": "Agent Creator",
|
|
325
|
+
"iconEmoji": "🧬",
|
|
343
326
|
"required": false,
|
|
344
327
|
"installerAgentCount": 1,
|
|
345
328
|
"categoryAgentCount": 0,
|
|
@@ -374,6 +357,7 @@ const DERIVED = {
|
|
|
374
357
|
"order": 3,
|
|
375
358
|
"name": "Shield",
|
|
376
359
|
"displayName": "Shield — GRC Compliance",
|
|
360
|
+
"iconEmoji": "🛡️",
|
|
377
361
|
"required": false,
|
|
378
362
|
"installerAgentCount": 1,
|
|
379
363
|
"categoryAgentCount": 27,
|
|
@@ -416,6 +400,7 @@ const DERIVED = {
|
|
|
416
400
|
"order": 4,
|
|
417
401
|
"name": "SEO",
|
|
418
402
|
"displayName": "SEO Audit 360",
|
|
403
|
+
"iconEmoji": "🔍",
|
|
419
404
|
"required": false,
|
|
420
405
|
"installerAgentCount": 3,
|
|
421
406
|
"categoryAgentCount": 0,
|
|
@@ -467,6 +452,7 @@ const DERIVED = {
|
|
|
467
452
|
"order": 5,
|
|
468
453
|
"name": "Memory",
|
|
469
454
|
"displayName": "Memory — Persistent Brain",
|
|
455
|
+
"iconEmoji": "🧠",
|
|
470
456
|
"required": false,
|
|
471
457
|
"installerAgentCount": 1,
|
|
472
458
|
"categoryAgentCount": 0,
|
|
@@ -503,6 +489,7 @@ const DERIVED = {
|
|
|
503
489
|
"order": 6,
|
|
504
490
|
"name": "Dev Studio",
|
|
505
491
|
"displayName": "Dev Studio — Full SDLC",
|
|
492
|
+
"iconEmoji": "🏗️",
|
|
506
493
|
"required": false,
|
|
507
494
|
"installerAgentCount": 1,
|
|
508
495
|
"categoryAgentCount": 0,
|
|
@@ -585,6 +572,7 @@ const DERIVED = {
|
|
|
585
572
|
"order": 7,
|
|
586
573
|
"name": "Backup",
|
|
587
574
|
"displayName": "Universal Backup",
|
|
575
|
+
"iconEmoji": "🗂️",
|
|
588
576
|
"required": false,
|
|
589
577
|
"installerAgentCount": 1,
|
|
590
578
|
"categoryAgentCount": 0,
|
|
@@ -619,6 +607,7 @@ const DERIVED = {
|
|
|
619
607
|
"order": 8,
|
|
620
608
|
"name": "Animated",
|
|
621
609
|
"displayName": "Animated Website",
|
|
610
|
+
"iconEmoji": "🎬",
|
|
622
611
|
"required": false,
|
|
623
612
|
"installerAgentCount": 1,
|
|
624
613
|
"categoryAgentCount": 0,
|
|
@@ -33,7 +33,11 @@ function normalizePathForHash(p) {
|
|
|
33
33
|
* @returns {string} 8-character hex hash
|
|
34
34
|
*/
|
|
35
35
|
function projectHash(projectDir) {
|
|
36
|
-
return crypto
|
|
36
|
+
return crypto
|
|
37
|
+
.createHash('sha256')
|
|
38
|
+
.update(normalizePathForHash(projectDir))
|
|
39
|
+
.digest('hex')
|
|
40
|
+
.slice(0, 8);
|
|
37
41
|
}
|
|
38
42
|
|
|
39
43
|
module.exports = { normalizePathForHash, projectHash };
|
|
@@ -156,7 +156,13 @@ function venvPython(envDir, platform = process.platform) {
|
|
|
156
156
|
* @returns {{ create: Array<{cmd:string,args:string[],label:string}>,
|
|
157
157
|
* install: Array<{cmd:string,args:string[],label:string}> }}
|
|
158
158
|
*/
|
|
159
|
-
function buildEnvCommands({
|
|
159
|
+
function buildEnvCommands({
|
|
160
|
+
tool,
|
|
161
|
+
python,
|
|
162
|
+
envDir,
|
|
163
|
+
requirementsPath = null,
|
|
164
|
+
platform = process.platform,
|
|
165
|
+
}) {
|
|
160
166
|
const envPy = venvPython(envDir, platform);
|
|
161
167
|
const create = [];
|
|
162
168
|
const install = [];
|
|
@@ -223,13 +229,21 @@ function missingPythonGuidance(platform = process.platform) {
|
|
|
223
229
|
const min = `${MIN_PYTHON.major}.${MIN_PYTHON.minor}`;
|
|
224
230
|
const lines = [`Python >= ${min} was not found on PATH.`];
|
|
225
231
|
if (platform === 'win32') {
|
|
226
|
-
lines.push(
|
|
232
|
+
lines.push(
|
|
233
|
+
`Install it with: winget install Python.Python.3.12 (or from https://www.python.org/downloads/)`
|
|
234
|
+
);
|
|
227
235
|
} else if (platform === 'darwin') {
|
|
228
|
-
lines.push(
|
|
236
|
+
lines.push(
|
|
237
|
+
`Install it with: brew install python@3.12 (or from https://www.python.org/downloads/)`
|
|
238
|
+
);
|
|
229
239
|
} else {
|
|
230
|
-
lines.push(
|
|
240
|
+
lines.push(
|
|
241
|
+
`Install it with your package manager (e.g. apt install python3.12) or from https://www.python.org/downloads/`
|
|
242
|
+
);
|
|
231
243
|
}
|
|
232
|
-
lines.push(
|
|
244
|
+
lines.push(
|
|
245
|
+
`Alternatively install uv (https://docs.astral.sh/uv/) — BMAD+ will use it to fetch Python ${min} automatically.`
|
|
246
|
+
);
|
|
233
247
|
return lines;
|
|
234
248
|
}
|
|
235
249
|
|
|
@@ -248,7 +262,11 @@ function missingPythonGuidance(platform = process.platform) {
|
|
|
248
262
|
* @returns {{ ok: boolean, tool: 'python'|null, command: {cmd:string,baseArgs:string[]}|null,
|
|
249
263
|
* version: object|null, messages: string[] }}
|
|
250
264
|
*/
|
|
251
|
-
function detectPython({
|
|
265
|
+
function detectPython({
|
|
266
|
+
runner = defaultRunner,
|
|
267
|
+
platform = process.platform,
|
|
268
|
+
minVersion = MIN_PYTHON,
|
|
269
|
+
} = {}) {
|
|
252
270
|
const messages = [];
|
|
253
271
|
let tooOld = null;
|
|
254
272
|
|
|
@@ -259,7 +277,9 @@ function detectPython({ runner = defaultRunner, platform = process.platform, min
|
|
|
259
277
|
const version = parsePythonVersion(`${res.stdout}\n${res.stderr}`);
|
|
260
278
|
if (!version) continue;
|
|
261
279
|
if (meetsMinVersion(version, minVersion)) {
|
|
262
|
-
messages.push(
|
|
280
|
+
messages.push(
|
|
281
|
+
`Found ${version.raw} via \`${[candidate.cmd, ...candidate.baseArgs].join(' ')}\`.`
|
|
282
|
+
);
|
|
263
283
|
return { ok: true, tool: 'python', command: candidate, version, messages };
|
|
264
284
|
}
|
|
265
285
|
tooOld = { candidate, version };
|
|
@@ -370,14 +390,22 @@ function verifyImports({ envDir, modules, runner = defaultRunner, platform = pro
|
|
|
370
390
|
* Verify that the pack's entry script runs inside the env (default: --help).
|
|
371
391
|
* @returns {{ ok: boolean, messages: string[] }}
|
|
372
392
|
*/
|
|
373
|
-
function verifyEntry({
|
|
393
|
+
function verifyEntry({
|
|
394
|
+
envDir,
|
|
395
|
+
entryPath,
|
|
396
|
+
entryArgs = ['--help'],
|
|
397
|
+
runner = defaultRunner,
|
|
398
|
+
platform = process.platform,
|
|
399
|
+
}) {
|
|
374
400
|
if (!entryPath) return { ok: true, messages: [] };
|
|
375
401
|
const envPy = venvPython(envDir, platform);
|
|
376
402
|
const res = runner(envPy, [entryPath, ...entryArgs]);
|
|
377
403
|
if (!ranOk(res)) {
|
|
378
404
|
return {
|
|
379
405
|
ok: false,
|
|
380
|
-
messages: [
|
|
406
|
+
messages: [
|
|
407
|
+
`Entry check failed (${path.basename(entryPath)} ${entryArgs.join(' ')}): ${firstErrorLine(res)}`,
|
|
408
|
+
],
|
|
381
409
|
};
|
|
382
410
|
}
|
|
383
411
|
return { ok: true, messages: [`Verified entry: ${path.basename(entryPath)} runs.`] };
|
|
@@ -418,7 +446,13 @@ function provisionPack({
|
|
|
418
446
|
const messages = [];
|
|
419
447
|
|
|
420
448
|
if (!envDir) {
|
|
421
|
-
return {
|
|
449
|
+
return {
|
|
450
|
+
ok: false,
|
|
451
|
+
tool: null,
|
|
452
|
+
envDir: null,
|
|
453
|
+
python: null,
|
|
454
|
+
messages: ['provisionPack: envDir is required.'],
|
|
455
|
+
};
|
|
422
456
|
}
|
|
423
457
|
|
|
424
458
|
// 1) provisioner first — uv can operate without a system Python.
|
|
@@ -432,7 +466,9 @@ function provisionPack({
|
|
|
432
466
|
return { ok: false, tool: prov.tool, envDir, python: null, messages };
|
|
433
467
|
}
|
|
434
468
|
if (!py.ok && prov.tool === 'uv') {
|
|
435
|
-
messages.push(
|
|
469
|
+
messages.push(
|
|
470
|
+
`No system Python >= ${minVersion.major}.${minVersion.minor} — uv will download a managed interpreter.`
|
|
471
|
+
);
|
|
436
472
|
}
|
|
437
473
|
|
|
438
474
|
// 3) create isolated env
|
|
@@ -482,7 +518,10 @@ function provisionPack({
|
|
|
482
518
|
function firstErrorLine(res) {
|
|
483
519
|
if (!res) return 'no result';
|
|
484
520
|
if (res.error) return res.error;
|
|
485
|
-
const text = `${res.stderr}\n${res.stdout}
|
|
521
|
+
const text = `${res.stderr}\n${res.stdout}`
|
|
522
|
+
.split(/\r?\n/)
|
|
523
|
+
.map((l) => l.trim())
|
|
524
|
+
.filter(Boolean);
|
|
486
525
|
return text.length > 0 ? text[text.length - 1] : `exit code ${res.status}`;
|
|
487
526
|
}
|
|
488
527
|
|
|
@@ -10,24 +10,28 @@ const fs = require('node:fs');
|
|
|
10
10
|
// ── Project Detection Markers (for scan.js) ──
|
|
11
11
|
|
|
12
12
|
const PROJECT_MARKERS = [
|
|
13
|
-
{
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
13
|
+
{
|
|
14
|
+
file: 'package.json',
|
|
15
|
+
stack: 'Node.js',
|
|
16
|
+
detect: (dir) => {
|
|
17
|
+
try {
|
|
18
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8'));
|
|
19
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
20
|
+
if (deps['next']) return 'Next.js';
|
|
21
|
+
if (deps['nuxt']) return 'Nuxt';
|
|
22
|
+
if (deps['react']) return 'React';
|
|
23
|
+
if (deps['vue']) return 'Vue.js';
|
|
24
|
+
if (deps['svelte']) return 'Svelte';
|
|
25
|
+
if (deps['express']) return 'Express';
|
|
26
|
+
if (deps['fastify']) return 'Fastify';
|
|
27
|
+
if (deps['electron']) return 'Electron';
|
|
28
|
+
if (deps['tauri']) return 'Tauri';
|
|
29
|
+
return 'Node.js';
|
|
30
|
+
} catch {
|
|
31
|
+
return 'Node.js';
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
},
|
|
31
35
|
{ file: 'Cargo.toml', stack: 'Rust' },
|
|
32
36
|
{ file: 'pyproject.toml', stack: 'Python' },
|
|
33
37
|
{ file: 'requirements.txt', stack: 'Python' },
|
|
@@ -56,7 +60,10 @@ function detectStack(dir) {
|
|
|
56
60
|
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
57
61
|
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
58
62
|
result.runtime = 'Node.js';
|
|
59
|
-
result.language =
|
|
63
|
+
result.language =
|
|
64
|
+
deps['typescript'] || fs.existsSync(path.join(dir, 'tsconfig.json'))
|
|
65
|
+
? 'TypeScript'
|
|
66
|
+
: 'JavaScript';
|
|
60
67
|
result.hasTypeScript = result.language === 'TypeScript';
|
|
61
68
|
|
|
62
69
|
// Framework detection
|
|
@@ -85,12 +92,31 @@ function detectStack(dir) {
|
|
|
85
92
|
|
|
86
93
|
// Other languages
|
|
87
94
|
if (!result.runtime) {
|
|
88
|
-
if (fs.existsSync(path.join(dir, 'Cargo.toml'))) {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
else if (
|
|
92
|
-
|
|
93
|
-
|
|
95
|
+
if (fs.existsSync(path.join(dir, 'Cargo.toml'))) {
|
|
96
|
+
result.language = 'Rust';
|
|
97
|
+
result.runtime = 'Rust';
|
|
98
|
+
} else if (
|
|
99
|
+
fs.existsSync(path.join(dir, 'pyproject.toml')) ||
|
|
100
|
+
fs.existsSync(path.join(dir, 'requirements.txt'))
|
|
101
|
+
) {
|
|
102
|
+
result.language = 'Python';
|
|
103
|
+
result.runtime = 'Python';
|
|
104
|
+
} else if (fs.existsSync(path.join(dir, 'go.mod'))) {
|
|
105
|
+
result.language = 'Go';
|
|
106
|
+
result.runtime = 'Go';
|
|
107
|
+
} else if (fs.existsSync(path.join(dir, 'composer.json'))) {
|
|
108
|
+
result.language = 'PHP';
|
|
109
|
+
result.runtime = 'PHP';
|
|
110
|
+
} else if (fs.existsSync(path.join(dir, 'Gemfile'))) {
|
|
111
|
+
result.language = 'Ruby';
|
|
112
|
+
result.runtime = 'Ruby';
|
|
113
|
+
} else if (
|
|
114
|
+
fs.existsSync(path.join(dir, 'pom.xml')) ||
|
|
115
|
+
fs.existsSync(path.join(dir, 'build.gradle'))
|
|
116
|
+
) {
|
|
117
|
+
result.language = 'Java';
|
|
118
|
+
result.runtime = 'JVM';
|
|
119
|
+
}
|
|
94
120
|
}
|
|
95
121
|
|
|
96
122
|
return result;
|
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
|
}
|