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.
Files changed (127) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/README.md +96 -461
  3. package/package.json +12 -5
  4. package/readme-international/README.de.md +42 -9
  5. package/readme-international/README.es.md +43 -10
  6. package/readme-international/README.fr.md +42 -9
  7. package/src/bmad-plus/agents/agent-orchestrator/SKILL.md +5 -3
  8. package/src/bmad-plus/module.yaml +270 -220
  9. package/src/bmad-plus/packs/pack-dev-studio/README.md +133 -141
  10. package/src/bmad-plus/packs/pack-dev-studio/SKILL.md +49 -0
  11. package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/analyst-agent.md +35 -60
  12. package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/document-project.md +59 -59
  13. package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/domain-research.md +55 -93
  14. package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/market-research.md +58 -93
  15. package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/prfaq.md +55 -132
  16. package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/product-brief.md +63 -78
  17. package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/tech-writer-agent.md +54 -69
  18. package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/technical-research.md +54 -93
  19. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/architect-agent.md +32 -60
  20. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/create-architecture.md +67 -71
  21. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/create-epics-stories.md +61 -90
  22. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/generate-project-context.md +56 -78
  23. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/implementation-readiness.md +55 -88
  24. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-01-init.md +20 -153
  25. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-01b-continue.md +20 -173
  26. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-02-context.md +14 -220
  27. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-03-starter.md +20 -329
  28. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-04-decisions.md +15 -314
  29. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-05-patterns.md +15 -355
  30. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-06-structure.md +15 -375
  31. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-07-validation.md +14 -357
  32. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-08-complete.md +13 -78
  33. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/checkpoint-preview.md +52 -65
  34. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/code-review-steps/step-01-gather-context.md +14 -81
  35. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/code-review-steps/step-02-review.md +14 -31
  36. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/code-review-steps/step-03-triage.md +14 -45
  37. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/code-review-steps/step-04-present.md +13 -128
  38. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/code-review.md +61 -87
  39. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/correct-course.md +55 -298
  40. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/create-story.md +52 -426
  41. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/dev-agent.md +48 -69
  42. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/dev-story-checklist.md +22 -80
  43. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/dev-story.md +60 -482
  44. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/investigate.md +50 -184
  45. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/qa-e2e-tests.md +57 -173
  46. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/quick-dev.md +56 -108
  47. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/retrospective.md +54 -1509
  48. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/sprint-planning.md +54 -296
  49. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/sprint-status.md +41 -283
  50. package/src/bmad-plus/packs/pack-dev-studio/categories/planning/create-prd.md +58 -18
  51. package/src/bmad-plus/packs/pack-dev-studio/categories/planning/create-ux-design.md +103 -72
  52. package/src/bmad-plus/packs/pack-dev-studio/categories/planning/edit-prd.md +55 -27
  53. package/src/bmad-plus/packs/pack-dev-studio/categories/planning/pm-agent.md +34 -60
  54. package/src/bmad-plus/packs/pack-dev-studio/categories/planning/prd.md +46 -87
  55. package/src/bmad-plus/packs/pack-dev-studio/categories/planning/steps/step-01-init.md +10 -0
  56. package/src/bmad-plus/packs/pack-dev-studio/categories/planning/ux-designer-agent.md +30 -60
  57. package/src/bmad-plus/packs/pack-dev-studio/categories/planning/validate-prd.md +57 -27
  58. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/advanced-elicitation.md +47 -138
  59. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/adversarial-review.md +48 -34
  60. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/bmad-help.md +51 -68
  61. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/brainstorming.md +46 -3
  62. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/customize.md +68 -109
  63. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/distillator.md +53 -174
  64. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/edge-case-hunter.md +39 -53
  65. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/editorial-review-prose.md +45 -83
  66. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/editorial-review-structure.md +45 -176
  67. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/index-docs.md +45 -63
  68. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/party-mode.md +53 -124
  69. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/shard-doc.md +44 -100
  70. package/src/bmad-plus/packs/pack-dev-studio/dev-studio-orchestrator.md +56 -116
  71. package/src/bmad-plus/packs/pack-dev-studio/shared/architecture-decision-template.md +49 -12
  72. package/src/bmad-plus/packs/pack-dev-studio/shared/bwml-spec.md +51 -328
  73. package/src/bmad-plus/packs/pack-dev-studio/shared/catalog.json +489 -0
  74. package/src/bmad-plus/packs/pack-dev-studio/shared/execution.md +69 -0
  75. package/src/bmad-plus/packs/pack-dev-studio/shared/module-help.csv +39 -32
  76. package/src/bmad-plus/packs/pack-dev-studio/upstream-sync.yaml +85 -14
  77. package/src/bmad-plus/packs/pack-memory/README.md +6 -0
  78. package/src/bmad-plus/packs/pack-memory/memory-orchestrator.md +12 -5
  79. package/src/bmad-plus/packs/pack-seo/SKILL.md +27 -1
  80. package/src/bmad-plus/packs/pack-seo/scripts/seo_apis.py +8 -8
  81. package/src/bmad-plus/packs/pack-seo/scripts/seo_fetch.py +1 -2
  82. package/src/bmad-plus/packs/pack-seo/scripts/seo_report.py +0 -1
  83. package/src/bmad-plus/packs/pack-seo/seo-chief.md +16 -1
  84. package/src/bmad-plus/packs/pack-seo/seo-judge.md +12 -0
  85. package/src/bmad-plus/packs/pack-seo/seo-scout.md +12 -0
  86. package/src/bmad-plus/skills/bmad-plus-autopilot/SKILL.md +4 -4
  87. package/src/bmad-plus/skills/bmad-plus-sync/SKILL.md +5 -3
  88. package/src/bmad-plus/skills/dev-studio/SKILL.md +19 -0
  89. package/tools/bmad-plus-npx.js +4 -2
  90. package/tools/build/adapters.config.js +60 -51
  91. package/tools/build/check-counts.js +52 -54
  92. package/tools/build/check-install-contract.js +450 -0
  93. package/tools/build/generate-adapters.js +252 -56
  94. package/tools/build/generate.js +364 -14
  95. package/tools/build/generated-adapters/.codex/AGENTS.md +20 -7
  96. package/tools/build/generated-adapters/.cursor/rules/bmad-plus.mdc +20 -7
  97. package/tools/build/generated-adapters/.opencode/AGENTS.md +20 -7
  98. package/tools/build/generated-adapters/AGENTS.md +20 -7
  99. package/tools/build/generated-adapters/CLAUDE.md +20 -7
  100. package/tools/build/generated-adapters/CONVENTIONS.md +20 -7
  101. package/tools/build/generated-adapters/GEMINI.md +20 -7
  102. package/tools/build/module.template.yaml +82 -0
  103. package/tools/build/pack-delivery.js +78 -0
  104. package/tools/cli/bmad-plus-cli.js +16 -1
  105. package/tools/cli/commands/doctor.js +49 -217
  106. package/tools/cli/commands/install.js +109 -164
  107. package/tools/cli/commands/studio.js +68 -0
  108. package/tools/cli/commands/uninstall.js +173 -65
  109. package/tools/cli/commands/update-check.js +31 -0
  110. package/tools/cli/commands/update-policy.js +39 -0
  111. package/tools/cli/commands/update.js +102 -113
  112. package/tools/cli/i18n.js +60 -0
  113. package/tools/cli/lib/ide-config.js +4 -261
  114. package/tools/cli/lib/install-manifest.js +17 -0
  115. package/tools/cli/lib/installation-health.js +360 -0
  116. package/tools/cli/lib/installed-adapters.js +89 -0
  117. package/tools/cli/lib/npm-runner.js +177 -0
  118. package/tools/cli/lib/pack-copy.js +62 -66
  119. package/tools/cli/lib/packs.js +546 -3
  120. package/tools/cli/lib/python-health.js +233 -0
  121. package/tools/cli/lib/python-provision.js +2 -2
  122. package/tools/cli/lib/studio.js +310 -0
  123. package/tools/cli/lib/update-check.js +153 -0
  124. package/tools/cli/lib/update-dispatch.js +182 -0
  125. package/tools/cli/lib/update-policy.js +90 -0
  126. package/tools/cli/lib/update-transaction.js +334 -0
  127. 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]` → seo, memory). One code path for every pack:
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" (seo, memory, …).
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 };