bmad-plus 0.14.0 → 0.16.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 (60) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/README.md +34 -15
  3. package/SECURITY.md +71 -0
  4. package/THIRD-PARTY-LICENSES.md +349 -0
  5. package/osint-agent-package/README.md +1 -1
  6. package/package.json +9 -3
  7. package/readme-international/README.de.md +14 -8
  8. package/readme-international/README.es.md +15 -9
  9. package/readme-international/README.fr.md +14 -8
  10. package/src/bmad-plus/agents/agent-architect-dev/SKILL.md +11 -13
  11. package/src/bmad-plus/agents/agent-orchestrator/SKILL.md +147 -8
  12. package/src/bmad-plus/agents/agent-quality/SKILL.md +41 -11
  13. package/src/bmad-plus/data/role-triggers.yaml +19 -0
  14. package/src/bmad-plus/module-help.csv +1 -0
  15. package/src/bmad-plus/module.yaml +1 -0
  16. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/create-story.md +3 -1
  17. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/dev-story-checklist.md +2 -0
  18. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/dev-story.md +5 -1
  19. package/src/bmad-plus/packs/pack-memory/README.md +29 -4
  20. package/src/bmad-plus/packs/pack-memory/memory-orchestrator.md +21 -1
  21. package/src/bmad-plus/packs/pack-memory/shared/karpathy-guardrails.md +3 -3
  22. package/src/bmad-plus/packs/pack-memory/shared/memory-protocol.md +27 -3
  23. package/src/bmad-plus/packs/pack-memory/zecher-agent.md +18 -2
  24. package/src/bmad-plus/skills/bmad-plus-autopilot/SKILL.md +47 -10
  25. package/src/bmad-plus/skills/bmad-plus-parallel/SKILL.md +17 -3
  26. package/src/bmad-plus/skills/bmad-plus-sync/SKILL.md +76 -67
  27. package/src/bmad-plus/skills/bmad-plus-uat/SKILL.md +144 -0
  28. package/src/bmad-plus/skills/bmad-plus-uat/ref/uat-results.schema.json +60 -0
  29. package/src/bmad-plus/skills/bmad-plus-uat/ref/uat-spec.schema.json +121 -0
  30. package/src/bmad-plus/skills/bmad-plus-uat/ref/uat-triage.schema.json +60 -0
  31. package/src/bmad-plus/skills/bmad-plus-uat/template/page.html +552 -0
  32. package/src/bmad-plus/skills/bmad-plus-uat/template/strings.json +362 -0
  33. package/tools/build/check-install-contract.js +202 -4
  34. package/tools/build/generate.js +16 -0
  35. package/tools/build/generated-adapters/.codex/AGENTS.md +1 -1
  36. package/tools/build/generated-adapters/.cursor/rules/bmad-plus.mdc +1 -1
  37. package/tools/build/generated-adapters/.opencode/AGENTS.md +1 -1
  38. package/tools/build/generated-adapters/AGENTS.md +1 -1
  39. package/tools/build/generated-adapters/CLAUDE.md +1 -1
  40. package/tools/build/generated-adapters/CONVENTIONS.md +1 -1
  41. package/tools/build/generated-adapters/GEMINI.md +1 -1
  42. package/tools/cli/bmad-plus-cli.js +15 -12
  43. package/tools/cli/commands/doctor.js +1 -0
  44. package/tools/cli/commands/install.js +21 -2
  45. package/tools/cli/commands/memory-journal-cmd.js +119 -19
  46. package/tools/cli/commands/nexus.js +111 -0
  47. package/tools/cli/commands/uat.js +389 -0
  48. package/tools/cli/lib/README-memory-journal.md +19 -8
  49. package/tools/cli/lib/installation-health.js +6 -0
  50. package/tools/cli/lib/memory-journal.js +0 -0
  51. package/tools/cli/lib/memory-outcomes.js +293 -0
  52. package/tools/cli/lib/memory-store.js +139 -0
  53. package/tools/cli/lib/nexus-process.js +377 -0
  54. package/tools/cli/lib/nexus.js +1532 -0
  55. package/tools/cli/lib/pack-copy.js +39 -11
  56. package/tools/cli/lib/packs.js +17 -3
  57. package/tools/cli/lib/uat.js +869 -0
  58. package/tools/maintain/upstream-candidate.js +456 -0
  59. package/tools/release/publication-content.js +3 -1
  60. package/tools/release/supply-chain.js +282 -0
@@ -0,0 +1,377 @@
1
+ /** One foreground owner, one direct child. Never take ownership from a recorded PID. */
2
+ const fs = require('node:fs');
3
+ const path = require('node:path');
4
+ const crypto = require('node:crypto');
5
+ const { spawn } = require('node:child_process');
6
+
7
+ const sha256 = (value) => crypto.createHash('sha256').update(value).digest('hex');
8
+ const AUTH_PATH_KEYS = ['HOME', 'USERPROFILE', 'HOMEDRIVE', 'HOMEPATH', 'CODEX_HOME'];
9
+
10
+ function normalizeExecution(root, value, helpers) {
11
+ if (value === undefined) return undefined;
12
+ const { fail, relative, directoryAt, executable, stringList } = helpers;
13
+ if (!value || typeof value !== 'object' || Array.isArray(value))
14
+ fail('execution must be a command or codex-exec object.');
15
+ if (!['command', 'codex-exec'].includes(value.adapter)) fail('Unsupported execution adapter.');
16
+ const allowed = [
17
+ 'adapter',
18
+ 'command',
19
+ 'input',
20
+ 'instructions',
21
+ 'resources',
22
+ 'cwd',
23
+ 'outputLimitBytes',
24
+ ...(value.adapter === 'command' ? ['args', 'blockedExitCodes'] : ['sandbox', 'model']),
25
+ ];
26
+ if (Object.keys(value).some((key) => !allowed.includes(key)))
27
+ fail('Unknown execution field; shell, environment and arbitrary Codex flags are unsupported.');
28
+ const input = relative(value.input);
29
+ const instructions = stringList(value.instructions ?? [], 'execution instructions', {
30
+ paths: true,
31
+ });
32
+ const resources = [
33
+ ...new Set([
34
+ input,
35
+ ...instructions,
36
+ ...stringList(value.resources ?? [], 'execution resources', { paths: true }),
37
+ ]),
38
+ ];
39
+ const cwd = value.cwd ?? '.';
40
+ directoryAt(root, cwd);
41
+ const outputLimitBytes = value.outputLimitBytes ?? 256 * 1024;
42
+ if (
43
+ !Number.isInteger(outputLimitBytes) ||
44
+ outputLimitBytes < 1024 ||
45
+ outputLimitBytes > 2 * 1024 * 1024
46
+ )
47
+ fail('execution outputLimitBytes must be between 1024 and 2097152.');
48
+ const resolved = executable(value.command);
49
+ let args;
50
+ let blockedExitCodes = [];
51
+ let sandbox;
52
+ let model;
53
+ if (value.adapter === 'command') {
54
+ args = value.args;
55
+ if (
56
+ !Array.isArray(args) ||
57
+ args.length > 100 ||
58
+ args.some((arg) => typeof arg !== 'string' || arg.length > 8000 || arg.includes('\0'))
59
+ )
60
+ fail('execution args must be a bounded literal argument array.');
61
+ blockedExitCodes = value.blockedExitCodes ?? [];
62
+ if (
63
+ !Array.isArray(blockedExitCodes) ||
64
+ blockedExitCodes.length > 10 ||
65
+ blockedExitCodes.some((code) => !Number.isInteger(code) || code < 1 || code > 255)
66
+ )
67
+ fail('blockedExitCodes must contain explicit nonzero exit codes between 1 and 255.');
68
+ } else {
69
+ sandbox = value.sandbox ?? 'read-only';
70
+ if (!['read-only', 'workspace-write'].includes(sandbox))
71
+ fail('codex-exec supports read-only or workspace-write only.');
72
+ model = value.model ?? null;
73
+ if (
74
+ model !== null &&
75
+ (typeof model !== 'string' || !/^[a-zA-Z0-9][a-zA-Z0-9._/-]{0,119}$/.test(model))
76
+ )
77
+ fail('Codex model must be an explicit bounded model identifier.');
78
+ // stdin carries task text; it is never parsed as flags or a shell command.
79
+ // Auth stays with the installed CLI. No user-config or bypass flag is imported.
80
+ // Project and host rules still apply; this is not a Nexus sandbox.
81
+ args = [
82
+ '-a',
83
+ 'never',
84
+ 'exec',
85
+ '--json',
86
+ '--color',
87
+ 'never',
88
+ '--sandbox',
89
+ sandbox,
90
+ '--skip-git-repo-check',
91
+ '--ignore-user-config',
92
+ ];
93
+ if (model) args.push('--model', model);
94
+ args.push('-');
95
+ }
96
+ return {
97
+ adapter: value.adapter,
98
+ ...resolved,
99
+ args: [...args],
100
+ input,
101
+ instructions,
102
+ resources,
103
+ cwd,
104
+ outputLimitBytes,
105
+ blockedExitCodes: [...new Set(blockedExitCodes)],
106
+ ...(value.adapter === 'codex-exec' ? { sandbox, model } : {}),
107
+ };
108
+ }
109
+
110
+ function inputBytes(root, execution, safeTarget) {
111
+ const parts = [];
112
+ let size = 0;
113
+ for (const file of [...execution.instructions, execution.input]) {
114
+ const target = safeTarget(root, file);
115
+ const stat = fs.statSync(target);
116
+ if (!stat.isFile() || stat.size > 256 * 1024)
117
+ throw new Error('Nexus: execution input must be a regular file no larger than 256 KiB.');
118
+ const bytes = fs.readFileSync(target);
119
+ // Round-trip catches invalid UTF-8; NUL has no place in an agent instruction packet.
120
+ if (!Buffer.from(bytes.toString('utf8')).equals(bytes) || bytes.includes(0))
121
+ throw new Error('Nexus: execution input must contain UTF-8 text without NUL bytes.');
122
+ const prefix = execution.instructions.length
123
+ ? Buffer.from('\n--- ' + file + ' ---\n')
124
+ : Buffer.alloc(0);
125
+ size += prefix.length + bytes.length;
126
+ if (size > 1024 * 1024) throw new Error('Nexus: combined execution input exceeds 1 MiB.');
127
+ parts.push(prefix, bytes);
128
+ }
129
+ return Buffer.concat(parts);
130
+ }
131
+
132
+ function executionEnvironment(execution, minimal) {
133
+ const values = { ...minimal.values };
134
+ if (execution.adapter === 'codex-exec') {
135
+ for (const key of AUTH_PATH_KEYS) {
136
+ const source = Object.keys(process.env).find((name) => name.toUpperCase() === key);
137
+ if (source !== undefined) values[key] = process.env[source];
138
+ }
139
+ }
140
+ // No values or credential hashes enter execution receipts. CLI-owned auth files
141
+ // are not read by Nexus. Verifier environments remain the separate stricter policy.
142
+ return {
143
+ values,
144
+ receipt: {
145
+ policy: execution.adapter === 'codex-exec' ? 'local-codex-auth-paths-v1' : minimal.policy,
146
+ inheritedKeys: Object.keys(values).sort(),
147
+ credentialValuesRecorded: false,
148
+ },
149
+ };
150
+ }
151
+
152
+ function interpretOutput(execution, stdout, code) {
153
+ if (execution.blockedExitCodes.includes(code)) return { outcome: 'blocked', provider: null };
154
+ if (execution.adapter !== 'codex-exec')
155
+ return { outcome: code === 0 ? 'completed' : 'failed', provider: null };
156
+ let threadId = null;
157
+ let completed = false;
158
+ let failed = false;
159
+ let approval = false;
160
+ let usage = null;
161
+ for (const line of stdout.split(/\r?\n/).filter(Boolean)) {
162
+ let event;
163
+ try {
164
+ event = JSON.parse(line);
165
+ } catch {
166
+ return { outcome: 'failed', provider: { problem: 'Invalid Codex JSONL.' } };
167
+ }
168
+ if (!event || typeof event !== 'object' || Array.isArray(event))
169
+ return { outcome: 'failed', provider: { problem: 'Invalid Codex event object.' } };
170
+ if (event.type === 'thread.started' && typeof event.thread_id === 'string')
171
+ threadId = event.thread_id;
172
+ if (event.type === 'turn.completed') {
173
+ completed = true;
174
+ if (event.usage && typeof event.usage === 'object') {
175
+ usage = Object.fromEntries(
176
+ Object.entries(event.usage).filter(
177
+ ([key, value]) =>
178
+ ['input_tokens', 'cached_input_tokens', 'output_tokens'].includes(key) &&
179
+ Number.isSafeInteger(value) &&
180
+ value >= 0
181
+ )
182
+ );
183
+ }
184
+ }
185
+ if (event.type === 'turn.failed' || event.type === 'error') {
186
+ failed = true;
187
+ if (event.error?.code === 'approval_required' || event.code === 'approval_required')
188
+ approval = true;
189
+ }
190
+ }
191
+ return {
192
+ outcome: approval
193
+ ? 'blocked'
194
+ : code === 0 && completed && threadId && !failed
195
+ ? 'completed'
196
+ : 'failed',
197
+ provider: { threadId, completed, failed, usage, concreteModel: null, monetaryCost: null },
198
+ };
199
+ }
200
+
201
+ function runOwnedProcess({ root, execution, input, environment, deadline, callbacks }) {
202
+ return new Promise((resolve) => {
203
+ const startedAt = new Date().toISOString();
204
+ const buffers = { stdout: [], stderr: [] };
205
+ const observed = { stdout: 0, stderr: 0 };
206
+ let captured = 0;
207
+ let child;
208
+ let exitCode = null;
209
+ let signal = null;
210
+ let error = null;
211
+ let reason = null;
212
+ let childExited = false;
213
+ let closed = false;
214
+ let done = false;
215
+ let stopTimer;
216
+ let closeTimer;
217
+ let pollTimer;
218
+ let heartbeatAt = 0;
219
+ let signalled = false;
220
+
221
+ function finish() {
222
+ if (done) return;
223
+ done = true;
224
+ clearTimeout(stopTimer);
225
+ clearTimeout(closeTimer);
226
+ clearInterval(pollTimer);
227
+ process.removeListener('SIGINT', interrupt);
228
+ process.removeListener('SIGTERM', interrupt);
229
+ child?.stdin?.destroy();
230
+ child?.stdout?.destroy();
231
+ child?.stderr?.destroy();
232
+ if (!childExited) child?.unref();
233
+ const stdout = Buffer.concat(buffers.stdout);
234
+ const stderr = Buffer.concat(buffers.stderr);
235
+ const interpreted = interpretOutput(execution, stdout.toString('utf8'), exitCode);
236
+ resolve({
237
+ startedAt,
238
+ endedAt: new Date().toISOString(),
239
+ exitCode,
240
+ signal,
241
+ error,
242
+ reason,
243
+ spawned: Boolean(child?.pid),
244
+ directChildExited: Boolean(child?.pid) && childExited,
245
+ streamsClosed: closed,
246
+ directChildSignalled: signalled,
247
+ descendantState: 'unobserved',
248
+ outcome:
249
+ reason || !childExited || !closed
250
+ ? 'ambiguous'
251
+ : error || signal
252
+ ? 'failed'
253
+ : interpreted.outcome,
254
+ provider: interpreted.provider,
255
+ output: Object.fromEntries(
256
+ [
257
+ ['stdout', stdout],
258
+ ['stderr', stderr],
259
+ ].map(([name, bytes]) => [
260
+ name,
261
+ {
262
+ text: bytes.toString('utf8'),
263
+ base64: bytes.toString('base64'),
264
+ sha256: sha256(bytes),
265
+ capturedBytes: bytes.length,
266
+ observedBytes: observed[name],
267
+ complete: closed && !reason && observed[name] === bytes.length,
268
+ hashScope: 'captured-bytes',
269
+ },
270
+ ])
271
+ ),
272
+ });
273
+ }
274
+ function stop(why) {
275
+ reason ||= why;
276
+ if (child && !childExited && !signalled) {
277
+ // This object was returned by this spawn call. Never signal a saved PID.
278
+ try {
279
+ signalled = child.kill('SIGTERM');
280
+ } catch (caught) {
281
+ error ||= caught.message;
282
+ }
283
+ }
284
+ if (!stopTimer) stopTimer = setTimeout(finish, 1000);
285
+ }
286
+ function interrupt() {
287
+ stop('supervisor-interrupted');
288
+ }
289
+ function capture(name, bytes) {
290
+ observed[name] += bytes.length;
291
+ const keep = Math.min(bytes.length, execution.outputLimitBytes - captured);
292
+ if (keep > 0) {
293
+ buffers[name].push(bytes.subarray(0, keep));
294
+ captured += keep;
295
+ }
296
+ if (keep < bytes.length) stop('output-limit');
297
+ }
298
+ process.on('SIGINT', interrupt);
299
+ process.on('SIGTERM', interrupt);
300
+ try {
301
+ const pending = callbacks.poll();
302
+ if (pending || Date.now() >= deadline) {
303
+ reason = pending || 'deadline';
304
+ childExited = true;
305
+ closed = true;
306
+ finish();
307
+ return;
308
+ }
309
+ child = spawn(execution.command, execution.args, {
310
+ cwd: path.join(root, execution.cwd),
311
+ env: environment.values,
312
+ shell: false,
313
+ windowsHide: true,
314
+ stdio: ['pipe', 'pipe', 'pipe'],
315
+ });
316
+ child.stdout.on('data', (bytes) => capture('stdout', bytes));
317
+ child.stderr.on('data', (bytes) => capture('stderr', bytes));
318
+ child.stdin.on('error', (caught) => {
319
+ if (caught.code !== 'EPIPE') error ||= caught.message;
320
+ });
321
+ child.on('error', (caught) => {
322
+ error = caught.message;
323
+ // Spawn failure creates no child; close follows on supported Node versions.
324
+ if (!child.pid) {
325
+ childExited = true;
326
+ exitCode = null;
327
+ }
328
+ });
329
+ child.on('exit', (code, receivedSignal) => {
330
+ childExited = true;
331
+ exitCode = code;
332
+ signal = receivedSignal;
333
+ closeTimer = setTimeout(() => {
334
+ reason ||= 'retained-pipes';
335
+ finish();
336
+ }, 250);
337
+ });
338
+ child.on('close', (code, receivedSignal) => {
339
+ closed = true;
340
+ exitCode = code;
341
+ signal = receivedSignal;
342
+ finish();
343
+ });
344
+ callbacks.spawned(child.pid ?? null);
345
+ child.stdin.end(input);
346
+ pollTimer = setInterval(() => {
347
+ try {
348
+ const requested = callbacks.poll();
349
+ if (requested) stop(requested);
350
+ if (Date.now() >= deadline) stop('deadline');
351
+ if (Date.now() - heartbeatAt >= 1000) {
352
+ callbacks.heartbeat();
353
+ heartbeatAt = Date.now();
354
+ }
355
+ } catch (caught) {
356
+ error ||= caught.message;
357
+ stop('owner-state-unavailable');
358
+ }
359
+ }, 100);
360
+ } catch (caught) {
361
+ error = caught.message;
362
+ if (!child) {
363
+ childExited = true;
364
+ closed = true;
365
+ finish();
366
+ } else stop('launch-observation-failed');
367
+ }
368
+ });
369
+ }
370
+
371
+ module.exports = {
372
+ normalizeExecution,
373
+ inputBytes,
374
+ executionEnvironment,
375
+ interpretOutput,
376
+ runOwnedProcess,
377
+ };