bmad-plus 0.12.2 → 0.14.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 +47 -0
- package/README.md +96 -461
- package/package.json +12 -5
- package/readme-international/README.de.md +42 -9
- package/readme-international/README.es.md +43 -10
- package/readme-international/README.fr.md +42 -9
- package/src/bmad-plus/agents/agent-orchestrator/SKILL.md +5 -3
- package/src/bmad-plus/module.yaml +270 -220
- package/src/bmad-plus/packs/pack-dev-studio/README.md +133 -141
- package/src/bmad-plus/packs/pack-dev-studio/SKILL.md +49 -0
- package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/analyst-agent.md +35 -60
- package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/document-project.md +59 -59
- package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/domain-research.md +55 -93
- package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/market-research.md +58 -93
- package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/prfaq.md +55 -132
- package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/product-brief.md +63 -78
- package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/tech-writer-agent.md +54 -69
- package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/technical-research.md +54 -93
- package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/architect-agent.md +32 -60
- package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/create-architecture.md +67 -71
- package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/create-epics-stories.md +61 -90
- package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/generate-project-context.md +56 -78
- package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/implementation-readiness.md +55 -88
- package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-01-init.md +20 -153
- package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-01b-continue.md +20 -173
- package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-02-context.md +14 -220
- package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-03-starter.md +20 -329
- package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-04-decisions.md +15 -314
- package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-05-patterns.md +15 -355
- package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-06-structure.md +15 -375
- package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-07-validation.md +14 -357
- package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-08-complete.md +13 -78
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/checkpoint-preview.md +52 -65
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/code-review-steps/step-01-gather-context.md +14 -81
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/code-review-steps/step-02-review.md +14 -31
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/code-review-steps/step-03-triage.md +14 -45
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/code-review-steps/step-04-present.md +13 -128
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/code-review.md +61 -87
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/correct-course.md +55 -298
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/create-story.md +52 -426
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/dev-agent.md +48 -69
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/dev-story-checklist.md +22 -80
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/dev-story.md +60 -482
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/investigate.md +50 -184
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/qa-e2e-tests.md +57 -173
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/quick-dev.md +56 -108
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/retrospective.md +54 -1509
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/sprint-planning.md +54 -296
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/sprint-status.md +41 -283
- package/src/bmad-plus/packs/pack-dev-studio/categories/planning/create-prd.md +58 -18
- package/src/bmad-plus/packs/pack-dev-studio/categories/planning/create-ux-design.md +103 -72
- package/src/bmad-plus/packs/pack-dev-studio/categories/planning/edit-prd.md +55 -27
- package/src/bmad-plus/packs/pack-dev-studio/categories/planning/pm-agent.md +34 -60
- package/src/bmad-plus/packs/pack-dev-studio/categories/planning/prd.md +46 -87
- package/src/bmad-plus/packs/pack-dev-studio/categories/planning/steps/step-01-init.md +10 -0
- package/src/bmad-plus/packs/pack-dev-studio/categories/planning/ux-designer-agent.md +30 -60
- package/src/bmad-plus/packs/pack-dev-studio/categories/planning/validate-prd.md +57 -27
- package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/advanced-elicitation.md +47 -138
- package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/adversarial-review.md +48 -34
- package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/bmad-help.md +51 -68
- package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/brainstorming.md +46 -3
- package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/customize.md +68 -109
- package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/distillator.md +53 -174
- package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/edge-case-hunter.md +39 -53
- package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/editorial-review-prose.md +45 -83
- package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/editorial-review-structure.md +45 -176
- package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/index-docs.md +45 -63
- package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/party-mode.md +53 -124
- package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/shard-doc.md +44 -100
- package/src/bmad-plus/packs/pack-dev-studio/dev-studio-orchestrator.md +56 -116
- package/src/bmad-plus/packs/pack-dev-studio/shared/architecture-decision-template.md +49 -12
- package/src/bmad-plus/packs/pack-dev-studio/shared/bwml-spec.md +51 -328
- package/src/bmad-plus/packs/pack-dev-studio/shared/catalog.json +489 -0
- package/src/bmad-plus/packs/pack-dev-studio/shared/execution.md +69 -0
- package/src/bmad-plus/packs/pack-dev-studio/shared/module-help.csv +39 -32
- package/src/bmad-plus/packs/pack-dev-studio/upstream-sync.yaml +85 -14
- package/src/bmad-plus/packs/pack-memory/README.md +6 -0
- package/src/bmad-plus/packs/pack-memory/memory-orchestrator.md +12 -5
- package/src/bmad-plus/packs/pack-seo/SKILL.md +27 -1
- package/src/bmad-plus/packs/pack-seo/scripts/seo_apis.py +8 -8
- package/src/bmad-plus/packs/pack-seo/scripts/seo_fetch.py +1 -2
- package/src/bmad-plus/packs/pack-seo/scripts/seo_report.py +0 -1
- package/src/bmad-plus/packs/pack-seo/seo-chief.md +16 -1
- package/src/bmad-plus/packs/pack-seo/seo-judge.md +12 -0
- package/src/bmad-plus/packs/pack-seo/seo-scout.md +12 -0
- package/src/bmad-plus/skills/bmad-plus-autopilot/SKILL.md +4 -4
- package/src/bmad-plus/skills/bmad-plus-sync/SKILL.md +5 -3
- package/src/bmad-plus/skills/dev-studio/SKILL.md +19 -0
- package/tools/bmad-plus-npx.js +4 -2
- package/tools/build/adapters.config.js +60 -51
- package/tools/build/check-counts.js +52 -54
- package/tools/build/check-install-contract.js +450 -0
- package/tools/build/generate-adapters.js +252 -56
- package/tools/build/generate.js +364 -14
- package/tools/build/generated-adapters/.codex/AGENTS.md +20 -7
- package/tools/build/generated-adapters/.cursor/rules/bmad-plus.mdc +20 -7
- package/tools/build/generated-adapters/.opencode/AGENTS.md +20 -7
- package/tools/build/generated-adapters/AGENTS.md +20 -7
- package/tools/build/generated-adapters/CLAUDE.md +20 -7
- package/tools/build/generated-adapters/CONVENTIONS.md +20 -7
- package/tools/build/generated-adapters/GEMINI.md +20 -7
- package/tools/build/module.template.yaml +82 -0
- package/tools/build/pack-delivery.js +78 -0
- package/tools/cli/bmad-plus-cli.js +16 -1
- package/tools/cli/commands/doctor.js +49 -217
- package/tools/cli/commands/install.js +109 -164
- package/tools/cli/commands/studio.js +68 -0
- package/tools/cli/commands/uninstall.js +173 -65
- package/tools/cli/commands/update-check.js +31 -0
- package/tools/cli/commands/update-policy.js +39 -0
- package/tools/cli/commands/update.js +102 -113
- package/tools/cli/i18n.js +60 -0
- package/tools/cli/lib/ide-config.js +4 -261
- package/tools/cli/lib/install-manifest.js +17 -0
- package/tools/cli/lib/installation-health.js +360 -0
- package/tools/cli/lib/installed-adapters.js +89 -0
- package/tools/cli/lib/npm-runner.js +177 -0
- package/tools/cli/lib/pack-copy.js +62 -66
- package/tools/cli/lib/packs.js +546 -3
- package/tools/cli/lib/python-health.js +233 -0
- package/tools/cli/lib/python-provision.js +2 -2
- package/tools/cli/lib/studio.js +310 -0
- package/tools/cli/lib/update-check.js +153 -0
- package/tools/cli/lib/update-dispatch.js +182 -0
- package/tools/cli/lib/update-policy.js +90 -0
- package/tools/cli/lib/update-transaction.js +334 -0
- package/tools/release/publication-content.js +901 -0
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/** Opt-in verification of an existing pack environment. Never installs a runtime. */
|
|
2
|
+
const fs = require('node:fs');
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const { MIN_PYTHON, venvPython, defaultRunner } = require('./python-provision');
|
|
5
|
+
|
|
6
|
+
const TIMEOUT_MS = 15_000;
|
|
7
|
+
const MAX_BUFFER = 64 * 1024;
|
|
8
|
+
const MODULE_NAME = /^[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*$/;
|
|
9
|
+
|
|
10
|
+
// -I excludes project/PYTHONPATH imports; -B avoids bytecode writes. Importing
|
|
11
|
+
// installed dependencies still executes their code, so callers must opt in.
|
|
12
|
+
const PROBE = [
|
|
13
|
+
'import contextlib, importlib, json, os, sys',
|
|
14
|
+
'expected, minimum, names = sys.argv[1], json.loads(sys.argv[2]), json.loads(sys.argv[3])',
|
|
15
|
+
'normalize = lambda value: os.path.normcase(os.path.realpath(value))',
|
|
16
|
+
'prefix, base = normalize(sys.prefix), normalize(sys.base_prefix)',
|
|
17
|
+
'version = list(sys.version_info[:3])',
|
|
18
|
+
'checks = [{"name": name, "status": "not-verified"} for name in names]',
|
|
19
|
+
'if prefix == normalize(expected) and prefix != base and version >= minimum:',
|
|
20
|
+
' with open(os.devnull, "w", encoding="utf-8") as sink:',
|
|
21
|
+
' with contextlib.redirect_stdout(sink), contextlib.redirect_stderr(sink):',
|
|
22
|
+
' for check in checks:',
|
|
23
|
+
' try:',
|
|
24
|
+
' importlib.import_module(check["name"])',
|
|
25
|
+
' check["status"] = "verified"',
|
|
26
|
+
' except BaseException:',
|
|
27
|
+
' check["status"] = "failed"',
|
|
28
|
+
'print(json.dumps({"schemaVersion": 1, "prefix": prefix, "basePrefix": base, "version": version, "modules": checks}))',
|
|
29
|
+
].join('\n');
|
|
30
|
+
|
|
31
|
+
function minimumParts(minimum) {
|
|
32
|
+
if (typeof minimum !== 'string' || !/^\d+\.\d+(?:\.\d+)?$/.test(minimum)) return null;
|
|
33
|
+
const parts = minimum.split('.').map(Number);
|
|
34
|
+
if (parts.length === 2) parts.push(0);
|
|
35
|
+
return parts[0] > 0 && parts.every((part) => Number.isSafeInteger(part) && part >= 0)
|
|
36
|
+
? parts
|
|
37
|
+
: null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function normalizedPath(value) {
|
|
41
|
+
const resolved = path.resolve(value);
|
|
42
|
+
return process.platform === 'win32' ? resolved.toLowerCase() : resolved;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function environmentPaths(projectDir, packId) {
|
|
46
|
+
const directory = path.join(projectDir, '.bmad', 'venv', packId);
|
|
47
|
+
const executable = venvPython(directory);
|
|
48
|
+
const parent = path.dirname(executable);
|
|
49
|
+
let cursor = path.parse(parent).root;
|
|
50
|
+
for (const component of parent.slice(cursor.length).split(path.sep)) {
|
|
51
|
+
cursor = path.join(cursor, component);
|
|
52
|
+
const stat = fs.lstatSync(cursor);
|
|
53
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) return null;
|
|
54
|
+
}
|
|
55
|
+
const config = fs.lstatSync(path.join(directory, 'pyvenv.cfg'));
|
|
56
|
+
if (config.isSymbolicLink() || !config.isFile()) return null;
|
|
57
|
+
const binary = fs.lstatSync(executable);
|
|
58
|
+
if (binary.isSymbolicLink()) {
|
|
59
|
+
// A Unix venv normally links bin/python to its base interpreter. Keep that
|
|
60
|
+
// executable path: resolving it before invocation would lose the venv.
|
|
61
|
+
if (process.platform === 'win32' || !fs.statSync(executable).isFile()) return null;
|
|
62
|
+
} else if (!binary.isFile()) return null;
|
|
63
|
+
return { directory, executable };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Verify the actual prefix, version and declared imports in .bmad/venv/<packId>.
|
|
68
|
+
* One bounded interpreter invocation; no PATH fallback, repair or provisioning.
|
|
69
|
+
*/
|
|
70
|
+
function probePythonRuntime({
|
|
71
|
+
projectDir,
|
|
72
|
+
packId,
|
|
73
|
+
minimum = `${MIN_PYTHON.major}.${MIN_PYTHON.minor}`,
|
|
74
|
+
verifyModules = [],
|
|
75
|
+
runner = defaultRunner,
|
|
76
|
+
} = {}) {
|
|
77
|
+
const started = Date.now();
|
|
78
|
+
const floor = minimumParts(minimum);
|
|
79
|
+
const validNames =
|
|
80
|
+
Array.isArray(verifyModules) &&
|
|
81
|
+
verifyModules.length <= 64 &&
|
|
82
|
+
[...verifyModules].every(
|
|
83
|
+
(name) => typeof name === 'string' && name.length <= 200 && MODULE_NAME.test(name)
|
|
84
|
+
);
|
|
85
|
+
const names = validNames ? [...new Set(verifyModules)] : [];
|
|
86
|
+
const finish = (
|
|
87
|
+
status,
|
|
88
|
+
message,
|
|
89
|
+
version = null,
|
|
90
|
+
modules = names.map((name) => ({ name, status: 'not-verified' }))
|
|
91
|
+
) => ({
|
|
92
|
+
status,
|
|
93
|
+
version,
|
|
94
|
+
imports: modules.some((entry) => entry.status === 'failed')
|
|
95
|
+
? 'failed'
|
|
96
|
+
: modules.length > 0 && modules.every((entry) => entry.status === 'verified')
|
|
97
|
+
? 'verified'
|
|
98
|
+
: 'not-verified',
|
|
99
|
+
modules,
|
|
100
|
+
message,
|
|
101
|
+
});
|
|
102
|
+
if (
|
|
103
|
+
typeof projectDir !== 'string' ||
|
|
104
|
+
!projectDir ||
|
|
105
|
+
projectDir.includes('\0') ||
|
|
106
|
+
typeof packId !== 'string' ||
|
|
107
|
+
!/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/.test(packId) ||
|
|
108
|
+
!floor ||
|
|
109
|
+
!validNames ||
|
|
110
|
+
typeof runner !== 'function'
|
|
111
|
+
) {
|
|
112
|
+
return finish('invalid', 'Python verification options are invalid.');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
projectDir = path.resolve(projectDir);
|
|
116
|
+
let environment;
|
|
117
|
+
try {
|
|
118
|
+
environment = environmentPaths(projectDir, packId);
|
|
119
|
+
} catch (error) {
|
|
120
|
+
return finish(
|
|
121
|
+
error.code === 'ENOENT' ? 'missing' : 'unavailable',
|
|
122
|
+
error.code === 'ENOENT'
|
|
123
|
+
? 'The pack Python environment is missing or incomplete.'
|
|
124
|
+
: 'The pack Python environment cannot be inspected.'
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
if (!environment)
|
|
128
|
+
return finish(
|
|
129
|
+
'invalid',
|
|
130
|
+
'The pack Python environment contains an unsafe path or invalid file.'
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
const timeout = TIMEOUT_MS - (Date.now() - started);
|
|
134
|
+
if (timeout <= 0) return finish('unavailable', 'Python verification exceeded its time limit.');
|
|
135
|
+
let result;
|
|
136
|
+
try {
|
|
137
|
+
result = runner(
|
|
138
|
+
environment.executable,
|
|
139
|
+
[
|
|
140
|
+
'-I',
|
|
141
|
+
'-B',
|
|
142
|
+
'-c',
|
|
143
|
+
PROBE,
|
|
144
|
+
environment.directory,
|
|
145
|
+
JSON.stringify(floor),
|
|
146
|
+
JSON.stringify(names),
|
|
147
|
+
],
|
|
148
|
+
{
|
|
149
|
+
cwd: projectDir,
|
|
150
|
+
encoding: 'utf8',
|
|
151
|
+
shell: false,
|
|
152
|
+
windowsHide: true,
|
|
153
|
+
timeout,
|
|
154
|
+
killSignal: 'SIGKILL',
|
|
155
|
+
maxBuffer: MAX_BUFFER,
|
|
156
|
+
}
|
|
157
|
+
);
|
|
158
|
+
} catch {
|
|
159
|
+
return finish('unavailable', 'The pack Python interpreter could not complete verification.');
|
|
160
|
+
}
|
|
161
|
+
if (Date.now() - started >= TIMEOUT_MS || !result || result.error || result.status !== 0) {
|
|
162
|
+
return finish(
|
|
163
|
+
'unavailable',
|
|
164
|
+
'The pack Python interpreter could not complete verification within its limits.'
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
let evidence;
|
|
168
|
+
try {
|
|
169
|
+
if (typeof result.stdout !== 'string' || Buffer.byteLength(result.stdout, 'utf8') > MAX_BUFFER)
|
|
170
|
+
throw new Error();
|
|
171
|
+
evidence = JSON.parse(result.stdout);
|
|
172
|
+
} catch {
|
|
173
|
+
return finish('invalid', 'The Python interpreter returned invalid verification evidence.');
|
|
174
|
+
}
|
|
175
|
+
if (
|
|
176
|
+
!evidence ||
|
|
177
|
+
evidence.schemaVersion !== 1 ||
|
|
178
|
+
typeof evidence.prefix !== 'string' ||
|
|
179
|
+
!path.isAbsolute(evidence.prefix) ||
|
|
180
|
+
typeof evidence.basePrefix !== 'string' ||
|
|
181
|
+
!path.isAbsolute(evidence.basePrefix) ||
|
|
182
|
+
!Array.isArray(evidence.version) ||
|
|
183
|
+
evidence.version.length !== 3 ||
|
|
184
|
+
evidence.version.some((part) => !Number.isSafeInteger(part) || part < 0) ||
|
|
185
|
+
evidence.version[0] === 0 ||
|
|
186
|
+
!Array.isArray(evidence.modules) ||
|
|
187
|
+
evidence.modules.length !== names.length ||
|
|
188
|
+
evidence.modules.some(
|
|
189
|
+
(entry, index) =>
|
|
190
|
+
!entry ||
|
|
191
|
+
entry.name !== names[index] ||
|
|
192
|
+
!['verified', 'failed', 'not-verified'].includes(entry.status)
|
|
193
|
+
)
|
|
194
|
+
) {
|
|
195
|
+
return finish('invalid', 'The Python interpreter returned invalid verification evidence.');
|
|
196
|
+
}
|
|
197
|
+
if (
|
|
198
|
+
normalizedPath(evidence.prefix) !== normalizedPath(environment.directory) ||
|
|
199
|
+
normalizedPath(evidence.prefix) === normalizedPath(evidence.basePrefix)
|
|
200
|
+
) {
|
|
201
|
+
return finish(
|
|
202
|
+
'invalid',
|
|
203
|
+
'The interpreter is not running from the expected pack virtual environment.'
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
const version = evidence.version.join('.');
|
|
207
|
+
const difference = evidence.version.findIndex((part, index) => part !== floor[index]);
|
|
208
|
+
if (difference >= 0 && evidence.version[difference] < floor[difference]) {
|
|
209
|
+
return finish('unsupported', 'The pack Python version is below the required minimum.', version);
|
|
210
|
+
}
|
|
211
|
+
if (evidence.modules.some((entry) => entry.status === 'not-verified')) {
|
|
212
|
+
return finish('invalid', 'The interpreter did not verify every declared module.', version);
|
|
213
|
+
}
|
|
214
|
+
const modules = evidence.modules.map(({ name, status }) => ({ name, status }));
|
|
215
|
+
if (modules.some((entry) => entry.status === 'failed')) {
|
|
216
|
+
return finish(
|
|
217
|
+
'unavailable',
|
|
218
|
+
'One or more declared modules could not be imported in the pack environment.',
|
|
219
|
+
version,
|
|
220
|
+
modules
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
return finish(
|
|
224
|
+
'verified',
|
|
225
|
+
modules.length
|
|
226
|
+
? 'The pack Python version and declared imports are verified.'
|
|
227
|
+
: 'The pack Python version is verified; no import checks were requested.',
|
|
228
|
+
version,
|
|
229
|
+
modules
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
module.exports = { probePythonRuntime };
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* BMAD+ Unified Python Provisioning (Pillar 2 — kills split-brain / broken-on-arrival)
|
|
3
3
|
*
|
|
4
4
|
* Provisions the Python runtime a pack declares in the registry
|
|
5
|
-
* (`runtime: [node, python]`
|
|
5
|
+
* (`runtime: [node, python]`). One code path for every Python pack:
|
|
6
6
|
* 1. detect a Python >= 3.11 interpreter (or a uv that can fetch one)
|
|
7
7
|
* 2. choose a provisioner: uv > pipx > venv+pip
|
|
8
8
|
* 3. create an isolated env under a given dir (e.g. <project>/.bmad/venv)
|
|
@@ -386,7 +386,7 @@ function verifyEntry({ envDir, entryPath, entryArgs = ['--help'], runner = defau
|
|
|
386
386
|
/**
|
|
387
387
|
* Full pipeline: detect → choose provisioner → create env → install → verify.
|
|
388
388
|
* This is the single call site the installer uses for ANY pack whose registry
|
|
389
|
-
* runtime includes "python"
|
|
389
|
+
* runtime includes "python".
|
|
390
390
|
*
|
|
391
391
|
* Never throws for environmental problems — always returns
|
|
392
392
|
* { ok, tool, messages[] } so the installer can degrade gracefully and print
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
/** Read-only Dev Studio resource resolution; execution belongs to the host. */
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
const crypto = require('node:crypto');
|
|
7
|
+
const yaml = require('js-yaml');
|
|
8
|
+
const { safeTarget } = require('../../build/generate-adapters');
|
|
9
|
+
|
|
10
|
+
const PACK = '.agents/skills/pack-dev-studio';
|
|
11
|
+
const MAX_FILE = 256 * 1024;
|
|
12
|
+
const MAX_CONTEXT = 2 * 1024 * 1024;
|
|
13
|
+
const hash = (bytes) => crypto.createHash('sha256').update(bytes).digest('hex');
|
|
14
|
+
|
|
15
|
+
function relativePath(file) {
|
|
16
|
+
if (
|
|
17
|
+
typeof file !== 'string' ||
|
|
18
|
+
!file ||
|
|
19
|
+
/[\\:\0?#{}]/.test(file) ||
|
|
20
|
+
file.split('/').some((part) => !part || part === '.' || part === '..')
|
|
21
|
+
) {
|
|
22
|
+
throw new Error('Expected a portable relative file path: ' + String(file));
|
|
23
|
+
}
|
|
24
|
+
return file;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function target(root, file) {
|
|
28
|
+
relativePath(file);
|
|
29
|
+
const result = safeTarget(root, file);
|
|
30
|
+
let parent = path.resolve(root);
|
|
31
|
+
for (const part of file.split('/')) {
|
|
32
|
+
if (!fs.existsSync(parent)) break;
|
|
33
|
+
const names = fs.readdirSync(parent);
|
|
34
|
+
if (!names.includes(part) && names.some((name) => name.toLowerCase() === part.toLowerCase())) {
|
|
35
|
+
throw new Error('Path case mismatch: ' + file);
|
|
36
|
+
}
|
|
37
|
+
parent = path.join(parent, part);
|
|
38
|
+
}
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function readText(root, file, limit = MAX_FILE) {
|
|
43
|
+
const resolved = target(root, file);
|
|
44
|
+
const stat = fs.statSync(resolved);
|
|
45
|
+
if (!stat.isFile() || stat.size > limit) throw new Error('Resource is not bounded text: ' + file);
|
|
46
|
+
const bytes = fs.readFileSync(resolved);
|
|
47
|
+
if (bytes.includes(0) || bytes.length > limit)
|
|
48
|
+
throw new Error('Resource is not bounded text: ' + file);
|
|
49
|
+
const content = bytes.toString('utf8');
|
|
50
|
+
if (!Buffer.from(content).equals(bytes)) throw new Error('Resource is not UTF-8: ' + file);
|
|
51
|
+
return { path: file, sha256: hash(bytes), content };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function readJson(root, file) {
|
|
55
|
+
try {
|
|
56
|
+
return JSON.parse(readText(root, file).content);
|
|
57
|
+
} catch (error) {
|
|
58
|
+
throw new Error(file + ': ' + error.message, { cause: error });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function loadCatalog(packRoot) {
|
|
63
|
+
const catalog = readJson(packRoot, 'shared/catalog.json');
|
|
64
|
+
if (
|
|
65
|
+
catalog.schemaVersion !== 1 ||
|
|
66
|
+
!Array.isArray(catalog.agents) ||
|
|
67
|
+
!Array.isArray(catalog.workflows) ||
|
|
68
|
+
!Array.isArray(catalog.compatibility) ||
|
|
69
|
+
!catalog.agents.length ||
|
|
70
|
+
!catalog.workflows.length
|
|
71
|
+
) {
|
|
72
|
+
throw new Error('Unsupported Dev Studio catalog');
|
|
73
|
+
}
|
|
74
|
+
catalog.compatibility.forEach(relativePath);
|
|
75
|
+
const agents = new Set();
|
|
76
|
+
const routes = new Set();
|
|
77
|
+
const entries = new Set();
|
|
78
|
+
for (const agent of catalog.agents) {
|
|
79
|
+
if (
|
|
80
|
+
!agent ||
|
|
81
|
+
!/^[a-z][a-z0-9-]*$/.test(agent.id) ||
|
|
82
|
+
agents.has(agent.id) ||
|
|
83
|
+
typeof agent.persona !== 'string' ||
|
|
84
|
+
!agent.persona.trim()
|
|
85
|
+
)
|
|
86
|
+
throw new Error('Invalid or duplicate persona');
|
|
87
|
+
relativePath(agent.path);
|
|
88
|
+
if (entries.has(agent.path)) throw new Error('Duplicate catalog entry: ' + agent.path);
|
|
89
|
+
entries.add(agent.path);
|
|
90
|
+
agents.add(agent.id);
|
|
91
|
+
}
|
|
92
|
+
for (const workflow of catalog.workflows) {
|
|
93
|
+
if (
|
|
94
|
+
!workflow ||
|
|
95
|
+
!/^[a-z][a-z0-9-]*$/.test(workflow.id) ||
|
|
96
|
+
!agents.has(workflow.agent) ||
|
|
97
|
+
!['request', 'artifact', 'project', 'none'].includes(workflow.input) ||
|
|
98
|
+
!Array.isArray(workflow.aliases) ||
|
|
99
|
+
!Array.isArray(workflow.resources)
|
|
100
|
+
) {
|
|
101
|
+
throw new Error('Invalid workflow declaration');
|
|
102
|
+
}
|
|
103
|
+
relativePath(workflow.path);
|
|
104
|
+
if (entries.has(workflow.path)) throw new Error('Duplicate catalog entry: ' + workflow.path);
|
|
105
|
+
entries.add(workflow.path);
|
|
106
|
+
for (const resource of workflow.resources) relativePath(resource);
|
|
107
|
+
for (const route of [workflow.id, ...workflow.aliases]) {
|
|
108
|
+
if (typeof route !== 'string' || !/^[a-z][a-z0-9-]*$/.test(route) || routes.has(route)) {
|
|
109
|
+
throw new Error('Invalid or duplicate workflow route: ' + route);
|
|
110
|
+
}
|
|
111
|
+
routes.add(route);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return catalog;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Inline and reference-style Markdown links are packaged dependencies. */
|
|
118
|
+
function localLinks(content, file) {
|
|
119
|
+
const destinations = [
|
|
120
|
+
...Array.from(
|
|
121
|
+
content.matchAll(/!?\[[^\]\n]*\]\(<?([^\s)>]+)>?(?:\s+"[^"]*")?\)/g),
|
|
122
|
+
(m) => m[1]
|
|
123
|
+
),
|
|
124
|
+
...Array.from(content.matchAll(/^\s{0,3}\[[^\]\n]+\]:\s*<?([^\s>]+)>?/gm), (m) => m[1]),
|
|
125
|
+
];
|
|
126
|
+
return destinations
|
|
127
|
+
.filter((link) => !/^(?:https?:|mailto:|#)/i.test(link))
|
|
128
|
+
.map((link) => {
|
|
129
|
+
const clean = decodeURIComponent(link.split('#')[0]);
|
|
130
|
+
if (!clean || /[\\:\0?{}]/.test(clean) || path.posix.isAbsolute(clean)) {
|
|
131
|
+
throw new Error('Unsupported resource link in ' + file + ': ' + link);
|
|
132
|
+
}
|
|
133
|
+
return relativePath(path.posix.normalize(path.posix.join(path.posix.dirname(file), clean)));
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function validatePack(packRoot, expected) {
|
|
138
|
+
const catalog = loadCatalog(packRoot);
|
|
139
|
+
if (expected) {
|
|
140
|
+
const workflows = Object.values(expected.categories)
|
|
141
|
+
.flatMap((category) => category.workflows)
|
|
142
|
+
.sort();
|
|
143
|
+
if (
|
|
144
|
+
JSON.stringify(workflows) !== JSON.stringify(catalog.workflows.map((w) => w.id).sort()) ||
|
|
145
|
+
JSON.stringify([...expected.sub_agents].sort()) !==
|
|
146
|
+
JSON.stringify(catalog.agents.map((a) => a.id).sort())
|
|
147
|
+
) {
|
|
148
|
+
throw new Error('Dev Studio catalog disagrees with registry routes');
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
const queue = [
|
|
152
|
+
'SKILL.md',
|
|
153
|
+
'README.md',
|
|
154
|
+
'dev-studio-orchestrator.md',
|
|
155
|
+
'shared/execution.md',
|
|
156
|
+
...catalog.compatibility,
|
|
157
|
+
...catalog.agents.map((a) => a.path),
|
|
158
|
+
...catalog.workflows.flatMap((w) => [w.path, ...w.resources]),
|
|
159
|
+
];
|
|
160
|
+
const visited = new Set();
|
|
161
|
+
let bytes = 0;
|
|
162
|
+
while (queue.length) {
|
|
163
|
+
const file = queue.shift();
|
|
164
|
+
if (visited.has(file)) continue;
|
|
165
|
+
if (visited.size >= 256) throw new Error('Too many Dev Studio resources');
|
|
166
|
+
const resource = readText(packRoot, file);
|
|
167
|
+
bytes += Buffer.byteLength(resource.content);
|
|
168
|
+
if (bytes > MAX_CONTEXT) throw new Error('Dev Studio resource graph is too large');
|
|
169
|
+
visited.add(file);
|
|
170
|
+
if (file.endsWith('.md')) {
|
|
171
|
+
if (
|
|
172
|
+
/\{(?:workflow|agent)\.[^}]+\}|\{(?:project-root|planning_artifacts|communication_language)\}/.test(
|
|
173
|
+
resource.content
|
|
174
|
+
)
|
|
175
|
+
) {
|
|
176
|
+
throw new Error('Unresolved runtime declaration: ' + file);
|
|
177
|
+
}
|
|
178
|
+
queue.push(...localLinks(resource.content, file));
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return { catalog, resources: [...visited].sort() };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function resolveConfig(projectDir) {
|
|
185
|
+
const file = '_bmad/config.yaml';
|
|
186
|
+
const location = target(projectDir, file);
|
|
187
|
+
let supplied = {};
|
|
188
|
+
let source = null;
|
|
189
|
+
if (fs.existsSync(location)) {
|
|
190
|
+
source = readText(projectDir, file, 64 * 1024);
|
|
191
|
+
try {
|
|
192
|
+
supplied = yaml.load(source.content);
|
|
193
|
+
} catch (error) {
|
|
194
|
+
throw new Error(file + ': ' + error.message, { cause: error });
|
|
195
|
+
}
|
|
196
|
+
if (!supplied || typeof supplied !== 'object' || Array.isArray(supplied)) {
|
|
197
|
+
throw new Error(file + ' must be a YAML mapping');
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
const config = {
|
|
201
|
+
user_name: 'user',
|
|
202
|
+
communication_language: 'English',
|
|
203
|
+
document_output_language: 'English',
|
|
204
|
+
output_folder: '_bmad-output',
|
|
205
|
+
project_name: path.basename(projectDir),
|
|
206
|
+
};
|
|
207
|
+
const defaultsUsed = [];
|
|
208
|
+
for (const key of Object.keys(config)) {
|
|
209
|
+
if (Object.hasOwn(supplied, key)) {
|
|
210
|
+
if (typeof supplied[key] !== 'string' || !supplied[key].trim()) {
|
|
211
|
+
throw new Error(file + ': ' + key + ' must be a nonempty string');
|
|
212
|
+
}
|
|
213
|
+
config[key] = supplied[key];
|
|
214
|
+
} else defaultsUsed.push(key);
|
|
215
|
+
}
|
|
216
|
+
relativePath(config.output_folder);
|
|
217
|
+
return { values: config, defaultsUsed, source };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function installedPack(projectDir) {
|
|
221
|
+
// Validate ancestors through an entry path before using the pack as a root.
|
|
222
|
+
target(projectDir, PACK + '/SKILL.md');
|
|
223
|
+
const packRoot = path.join(projectDir, PACK);
|
|
224
|
+
if (!fs.existsSync(packRoot))
|
|
225
|
+
throw new Error('Dev Studio is not installed. Run bmad-plus install --packs core,dev-studio');
|
|
226
|
+
return packRoot;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function prepare({
|
|
230
|
+
projectDir = process.cwd(),
|
|
231
|
+
workflow: route,
|
|
232
|
+
request = '',
|
|
233
|
+
inputs = [],
|
|
234
|
+
packRoot,
|
|
235
|
+
} = {}) {
|
|
236
|
+
projectDir = path.resolve(projectDir);
|
|
237
|
+
if (!fs.statSync(projectDir).isDirectory()) throw new Error('Project must be a directory');
|
|
238
|
+
packRoot = packRoot || installedPack(projectDir);
|
|
239
|
+
const { catalog } = validatePack(packRoot);
|
|
240
|
+
const workflow = catalog.workflows.find((w) => w.id === route || w.aliases.includes(route));
|
|
241
|
+
if (!workflow) throw new Error('Unknown Dev Studio workflow: ' + route);
|
|
242
|
+
if (
|
|
243
|
+
typeof request !== 'string' ||
|
|
244
|
+
Buffer.byteLength(request) > MAX_FILE ||
|
|
245
|
+
!Array.isArray(inputs) ||
|
|
246
|
+
inputs.length > 32
|
|
247
|
+
) {
|
|
248
|
+
throw new Error('Invalid or oversized workflow inputs');
|
|
249
|
+
}
|
|
250
|
+
const config = resolveConfig(projectDir);
|
|
251
|
+
const loadedInputs = [...new Set(inputs)].map((file) => readText(projectDir, file));
|
|
252
|
+
const agent = catalog.agents.find((a) => a.id === workflow.agent);
|
|
253
|
+
const routes = new Set([
|
|
254
|
+
...catalog.agents.map((a) => a.path),
|
|
255
|
+
...catalog.workflows.map((w) => w.path),
|
|
256
|
+
'SKILL.md',
|
|
257
|
+
'README.md',
|
|
258
|
+
'dev-studio-orchestrator.md',
|
|
259
|
+
'shared/catalog.json',
|
|
260
|
+
]);
|
|
261
|
+
const queue = ['shared/execution.md', agent.path, workflow.path, ...workflow.resources];
|
|
262
|
+
const instructions = [];
|
|
263
|
+
const loaded = new Set();
|
|
264
|
+
while (queue.length) {
|
|
265
|
+
const file = queue.shift();
|
|
266
|
+
if (loaded.has(file)) continue;
|
|
267
|
+
loaded.add(file);
|
|
268
|
+
const resource = readText(packRoot, file);
|
|
269
|
+
instructions.push(resource);
|
|
270
|
+
if (file.endsWith('.md'))
|
|
271
|
+
queue.push(...localLinks(resource.content, file).filter((link) => !routes.has(link)));
|
|
272
|
+
}
|
|
273
|
+
const outputPath = `${config.values.output_folder}/dev-studio/${workflow.id}.md`;
|
|
274
|
+
const outputTarget = target(projectDir, outputPath);
|
|
275
|
+
const previousReport = fs.existsSync(outputTarget) ? readText(projectDir, outputPath) : null;
|
|
276
|
+
const size = [
|
|
277
|
+
...loadedInputs,
|
|
278
|
+
...instructions,
|
|
279
|
+
...(previousReport ? [previousReport] : []),
|
|
280
|
+
].reduce(
|
|
281
|
+
(total, item) => total + Buffer.byteLength(item.content),
|
|
282
|
+
Buffer.byteLength(request) + Buffer.byteLength(config.source?.content || '')
|
|
283
|
+
);
|
|
284
|
+
if (size > MAX_CONTEXT) throw new Error('Prepared context exceeds 2 MiB');
|
|
285
|
+
const missingInputs =
|
|
286
|
+
workflow.input === 'artifact' && !loadedInputs.length
|
|
287
|
+
? ['Supply a project artifact with --input RELATIVE_FILE.']
|
|
288
|
+
: ['request', 'project'].includes(workflow.input) && !request.trim() && !loadedInputs.length
|
|
289
|
+
? ['Supply the task scope with --request TEXT or --input RELATIVE_FILE.']
|
|
290
|
+
: [];
|
|
291
|
+
return {
|
|
292
|
+
schemaVersion: 1,
|
|
293
|
+
status: missingInputs.length ? 'needs-input' : 'ready',
|
|
294
|
+
execution: 'host-managed',
|
|
295
|
+
executed: false,
|
|
296
|
+
workflow: workflow.id,
|
|
297
|
+
agent: agent.persona,
|
|
298
|
+
catalogSha256: readText(packRoot, 'shared/catalog.json').sha256,
|
|
299
|
+
projectDir,
|
|
300
|
+
config,
|
|
301
|
+
request,
|
|
302
|
+
inputs: loadedInputs,
|
|
303
|
+
instructions,
|
|
304
|
+
outputPath,
|
|
305
|
+
previousReport,
|
|
306
|
+
missingInputs,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
module.exports = { loadCatalog, localLinks, validatePack, resolveConfig, installedPack, prepare };
|