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
@@ -111,7 +111,7 @@ After a successful update, reread the project spine and relevant installed skill
111
111
 
112
112
  ## Registry facts (computed from registry.yaml — never hand-typed)
113
113
 
114
- - Product: BMAD+ v0.14.0 (derived from BMAD-METHOD v6.6.0)
114
+ - Product: BMAD+ v0.16.0 (derived from BMAD-METHOD v6.6.0)
115
115
  - Models supported: claude, gpt, gemini, local (model-agnostic by contract)
116
116
  - Packs (9): Core, OSINT, Maker, Shield, SEO, Memory, Dev Studio, Backup, Animated
117
117
  - Installer agents (14 across all packs):
@@ -114,7 +114,7 @@ After a successful update, reread the project spine and relevant installed skill
114
114
 
115
115
  ## Registry facts (computed from registry.yaml — never hand-typed)
116
116
 
117
- - Product: BMAD+ v0.14.0 (derived from BMAD-METHOD v6.6.0)
117
+ - Product: BMAD+ v0.16.0 (derived from BMAD-METHOD v6.6.0)
118
118
  - Models supported: claude, gpt, gemini, local (model-agnostic by contract)
119
119
  - Packs (9): Core, OSINT, Maker, Shield, SEO, Memory, Dev Studio, Backup, Animated
120
120
  - Installer agents (14 across all packs):
@@ -113,7 +113,7 @@ After a successful update, reread the project spine and relevant installed skill
113
113
 
114
114
  ## Registry facts (computed from registry.yaml — never hand-typed)
115
115
 
116
- - Product: BMAD+ v0.14.0 (derived from BMAD-METHOD v6.6.0)
116
+ - Product: BMAD+ v0.16.0 (derived from BMAD-METHOD v6.6.0)
117
117
  - Models supported: claude, gpt, gemini, local (model-agnostic by contract)
118
118
  - Packs (9): Core, OSINT, Maker, Shield, SEO, Memory, Dev Studio, Backup, Animated
119
119
  - Installer agents (14 across all packs):
@@ -118,7 +118,7 @@ After a successful update, reread the project spine and relevant installed skill
118
118
 
119
119
  ## Registry facts (computed from registry.yaml — never hand-typed)
120
120
 
121
- - Product: BMAD+ v0.14.0 (derived from BMAD-METHOD v6.6.0)
121
+ - Product: BMAD+ v0.16.0 (derived from BMAD-METHOD v6.6.0)
122
122
  - Models supported: claude, gpt, gemini, local (model-agnostic by contract)
123
123
  - Packs (9): Core, OSINT, Maker, Shield, SEO, Memory, Dev Studio, Backup, Animated
124
124
  - Installer agents (14 across all packs):
@@ -45,9 +45,7 @@ for (const option of install.options || []) {
45
45
  installCmd.action(install.action);
46
46
 
47
47
  // Uninstall command
48
- const uninstallCmd = program
49
- .command('uninstall')
50
- .description('Remove BMAD+ from your project');
48
+ const uninstallCmd = program.command('uninstall').description('Remove BMAD+ from your project');
51
49
  for (const option of uninstall.options || []) {
52
50
  uninstallCmd.option(...option);
53
51
  }
@@ -64,9 +62,7 @@ for (const option of update.options || []) {
64
62
  updateCmd.action(update.action);
65
63
 
66
64
  // Doctor command
67
- const doctorCmd = program
68
- .command('doctor')
69
- .description('Check BMAD+ installation integrity');
65
+ const doctorCmd = program.command('doctor').description('Check BMAD+ installation integrity');
70
66
 
71
67
  for (const option of doctor.options || []) {
72
68
  doctorCmd.option(...option);
@@ -81,7 +77,9 @@ const scanCmd = program
81
77
  for (const option of scan.options || []) {
82
78
  scanCmd.option(...option);
83
79
  }
84
- scanCmd.action((scanPath, options) => scan.action({ ...options, directory: scanPath || options.directory }));
80
+ scanCmd.action((scanPath, options) =>
81
+ scan.action({ ...options, directory: scanPath || options.directory })
82
+ );
85
83
 
86
84
  // Memory command
87
85
  const memoryCmd = program
@@ -95,9 +93,7 @@ memoryCmd.action(memory.action);
95
93
 
96
94
  // Mem command (Karpathy memory loop — journal recall/write/reinforce)
97
95
  const memJournal = require('./commands/memory-journal-cmd');
98
- const memJournalCmd = program
99
- .command(memJournal.command)
100
- .description(memJournal.description);
96
+ const memJournalCmd = program.command(memJournal.command).description(memJournal.description);
101
97
 
102
98
  for (const option of memJournal.options || []) {
103
99
  memJournalCmd.option(...option);
@@ -116,15 +112,22 @@ for (const option of autoconfig.options || []) {
116
112
  autoconfigCmd.action(autoconfig.action);
117
113
 
118
114
  // Version discovery and explicit per-project update policy.
119
- for (const modulePath of ['./commands/update-check', './commands/update-policy', './commands/studio']) {
115
+ for (const modulePath of [
116
+ './commands/update-check',
117
+ './commands/update-policy',
118
+ './commands/studio',
119
+ './commands/nexus',
120
+ './commands/uat',
121
+ ]) {
120
122
  const command = require(modulePath);
121
123
  const configured = program.command(command.command).description(command.description);
124
+ if (command.aliases) configured.aliases(command.aliases);
122
125
  for (const option of command.options || []) configured.option(...option);
123
126
  configured.action(command.action);
124
127
  }
125
128
 
126
129
  // Await asynchronous registry checks and updates before completing the CLI.
127
- program.parseAsync(process.argv).catch(error => {
130
+ program.parseAsync(process.argv).catch((error) => {
128
131
  console.error(`BMAD+: ${error.message}`);
129
132
  process.exitCode = 1;
130
133
  });
@@ -45,6 +45,7 @@ module.exports = {
45
45
  'Installed: v' + report.versions.installed + ' | Executing CLI: v' + report.versions.cli,
46
46
  'Ownership inventory: ' + report.inventory.status,
47
47
  'Integrations use host-managed execution; host capabilities were not probed.',
48
+ 'Optional Nexus process execution requires an explicit plan; executable availability was not probed.',
48
49
  ].join('\n'),
49
50
  'BMAD+ Health Report'
50
51
  );
@@ -180,6 +180,7 @@ module.exports = {
180
180
  let userName = process.env.USER || process.env.USERNAME || 'Developer';
181
181
  let commLang = 'French';
182
182
  let execMode = options.mode || 'manual';
183
+ let uatEnvironment = '';
183
184
 
184
185
  if (!options.yes) {
185
186
  const userConfig = await clack.group({
@@ -192,6 +193,13 @@ module.exports = {
192
193
  message: i.comm_language,
193
194
  options: getCommLanguageOptions(),
194
195
  }),
196
+ uatEnvironment: () => clack.text({
197
+ message:
198
+ i.uat_environment ||
199
+ 'Test environment for human acceptance recipes (URL, blank if none)',
200
+ placeholder: 'https://demo.example.com',
201
+ defaultValue: '',
202
+ }),
195
203
  execMode: () => clack.select({
196
204
  message: i.exec_mode,
197
205
  options: [
@@ -215,6 +223,7 @@ module.exports = {
215
223
  userName = validatedName;
216
224
  commLang = userConfig.commLang;
217
225
  execMode = options.mode || userConfig.execMode;
226
+ uatEnvironment = String(userConfig.uatEnvironment || '').trim();
218
227
  }
219
228
 
220
229
  const releaseLock = acquireProjectLock(projectDir);
@@ -366,7 +375,7 @@ module.exports = {
366
375
  }
367
376
 
368
377
  // ── Step 6: Create config.yaml ──
369
- const configYaml = generateConfigYaml(userName, commLang, projectDir, execMode);
378
+ const configYaml = generateConfigYaml(userName, commLang, projectDir, execMode, uatEnvironment);
370
379
  const configPath = path.join(targetBmadDir, 'config.yaml');
371
380
  fs.writeFileSync(configPath, configYaml, 'utf8');
372
381
 
@@ -522,7 +531,7 @@ module.exports = {
522
531
  _internal: { writeIDEConfigs, generateConfigYaml },
523
532
  };
524
533
 
525
- function generateConfigYaml(userName, language, projectDir, execMode = 'manual') {
534
+ function generateConfigYaml(userName, language, projectDir, execMode = 'manual', uatEnvironment = '') {
526
535
  if (!['manual', 'autopilot', 'hybrid'].includes(execMode)) throw new Error('Unknown execution mode: ' + execMode);
527
536
  return '# BMAD+ Project Configuration\n# Generated by bmad-plus install\n\n' + yaml.dump({
528
537
  user_name: userName,
@@ -539,5 +548,15 @@ function generateConfigYaml(userName, language, projectDir, execMode = 'manual')
539
548
  story: execMode === 'autopilot' ? 'notify_only' : 'require_approval',
540
549
  delivery: 'require_approval',
541
550
  },
551
+ // Human acceptance recipes. advisory: every delivery produces and offers its page and
552
+ // nothing is ever blocked. gate: the delivery waits for a passing "bmad-plus uat gate".
553
+ // off: no recipe, and the delivery report says so.
554
+ uat: {
555
+ mode: 'advisory',
556
+ granularity: 'delivery',
557
+ environment: { name: uatEnvironment ? 'TEST' : '', url: uatEnvironment || '' },
558
+ page_budget: { max_steps: 15, max_minutes: 30 },
559
+ dir: '_bmad-output/uat',
560
+ },
542
561
  }, { lineWidth: -1, forceQuotes: true, quotingType: '"' });
543
562
  }
@@ -32,6 +32,8 @@
32
32
  const path = require('node:path');
33
33
  const fs = require('node:fs');
34
34
  const mj = require('../lib/memory-journal');
35
+ const store = require('../lib/memory-store');
36
+ const outcomes = require('../lib/memory-outcomes');
35
37
 
36
38
  // ── Pattern score store ──────────────────────────────────────────────────────
37
39
  // Lives next to the journal in the north-star scope (.bmad/memory/). Keyed by
@@ -63,9 +65,7 @@ function readScores(baseDir) {
63
65
  }
64
66
 
65
67
  function writeScores(baseDir, scores) {
66
- const file = scoresPath(baseDir);
67
- fs.mkdirSync(path.dirname(file), { recursive: true });
68
- fs.writeFileSync(file, JSON.stringify(scores, null, 2) + '\n', 'utf8');
68
+ store.writeJson(baseDir, SCORES_RELPATH, scores);
69
69
  }
70
70
 
71
71
  // ── Flag parsing helpers ─────────────────────────────────────────────────────
@@ -104,12 +104,30 @@ function truncate(text, max = 120) {
104
104
  * @returns {object[]} ranked entries (same shape as memory-journal.recall)
105
105
  */
106
106
  function runRecall(query, opts = {}) {
107
- const { baseDir = process.cwd(), scope = 'project', portfolioDir = null, limit = 8, now = null, json = false, log = console.log } = opts;
107
+ const {
108
+ baseDir = process.cwd(),
109
+ scope = 'project',
110
+ portfolioDir = null,
111
+ limit = 8,
112
+ now = null,
113
+ json = false,
114
+ log = console.log,
115
+ ranking = 'lexical',
116
+ contextScope,
117
+ } = opts;
108
118
 
109
- const results = mj.recall(query, { baseDir, scope, portfolioDir, limit, now });
119
+ const results = mj.recall(query, {
120
+ baseDir,
121
+ scope,
122
+ portfolioDir,
123
+ limit,
124
+ now,
125
+ ranking,
126
+ contextScope,
127
+ });
110
128
 
111
129
  if (json) {
112
- log(JSON.stringify({ query, scope, count: results.length, results }, null, 2));
130
+ log(JSON.stringify({ query, scope, ranking, count: results.length, results }, null, 2));
113
131
  return results;
114
132
  }
115
133
 
@@ -119,7 +137,8 @@ function runRecall(query, opts = {}) {
119
137
  }
120
138
  log(`mem recall: ${results.length} match(es) for "${query}" (scope: ${scope})`);
121
139
  results.forEach((r, i) => {
122
- const origin = r.kind === 'event' ? `event ${r.source}#${r.ref}` : `note ${r.source} § ${r.ref}`;
140
+ const origin =
141
+ r.kind === 'event' ? `event ${r.source}#${r.ref}` : `note ${r.source} § ${r.ref}`;
123
142
  log(` ${i + 1}. [${r.score.toFixed(3)}] ${origin}`);
124
143
  log(` ${truncate(r.text)}`);
125
144
  });
@@ -134,7 +153,18 @@ function runRecall(query, opts = {}) {
134
153
  * @returns {object} the persisted event
135
154
  */
136
155
  function runWrite(opts = {}) {
137
- const { baseDir = process.cwd(), agent, cli = null, model = null, task = '', outcome = undefined, signals = {}, now, json = false, log = console.log } = opts;
156
+ const {
157
+ baseDir = process.cwd(),
158
+ agent,
159
+ cli = null,
160
+ model = null,
161
+ task = '',
162
+ outcome = undefined,
163
+ signals = {},
164
+ now,
165
+ json = false,
166
+ log = console.log,
167
+ } = opts;
138
168
 
139
169
  const event = mj.appendEvent(baseDir, {
140
170
  ts: now,
@@ -150,7 +180,9 @@ function runWrite(opts = {}) {
150
180
  log(JSON.stringify({ written: event }, null, 2));
151
181
  } else {
152
182
  log(`mem write: event ${event.id} appended (${event.ts})`);
153
- log(` agent=${event.agent} cli=${event.cli || '-'} model=${event.model || '-'} outcome=${event.outcome || '-'}`);
183
+ log(
184
+ ` agent=${event.agent} cli=${event.cli || '-'} model=${event.model || '-'} outcome=${event.outcome || '-'}`
185
+ );
154
186
  if (event.task) log(` task: ${truncate(event.task)}`);
155
187
  }
156
188
  return event;
@@ -168,10 +200,24 @@ function runWrite(opts = {}) {
168
200
  * @returns {{patternId:string, reward:number, previous:object, next:object, promotion:object|null}}
169
201
  */
170
202
  function runReinforce(opts = {}) {
171
- const { baseDir = process.cwd(), patternId, signals = {}, evidence = [], now, json = false, log = console.log } = opts;
203
+ return store.withMemoryLock(opts.baseDir || process.cwd(), () => reinforceUnderLock(opts));
204
+ }
205
+
206
+ function reinforceUnderLock(opts = {}) {
207
+ const {
208
+ baseDir = process.cwd(),
209
+ patternId,
210
+ signals = {},
211
+ evidence = [],
212
+ now,
213
+ json = false,
214
+ log = console.log,
215
+ } = opts;
172
216
 
173
217
  if (typeof patternId !== 'string' || patternId.trim().length === 0) {
174
- throw new TypeError('mem reinforce: --pattern <id> is required (the pattern heading in patterns.md)');
218
+ throw new TypeError(
219
+ 'mem reinforce: --pattern <id> is required (the pattern heading in patterns.md)'
220
+ );
175
221
  }
176
222
 
177
223
  const reward = mj.computeReward(signals);
@@ -198,7 +244,15 @@ function runReinforce(opts = {}) {
198
244
  );
199
245
  }
200
246
 
201
- const result = { patternId, reward, previous, next, promotion };
247
+ const result = {
248
+ patternId,
249
+ reward,
250
+ previous,
251
+ next,
252
+ promotion,
253
+ evidenceStatus: 'self-reported',
254
+ rankingEligible: false,
255
+ };
202
256
 
203
257
  if (json) {
204
258
  log(JSON.stringify(result, null, 2));
@@ -209,12 +263,16 @@ function runReinforce(opts = {}) {
209
263
  log(`mem reinforce: pattern "${patternId}"`);
210
264
  log(` reward ${reward.toFixed(3)} (from ${JSON.stringify(signals)})`);
211
265
  log(` elo ${prevElo.toFixed(1)} -> ${next.elo.toFixed(1)}`);
212
- log(` mean ${next.mean.toFixed(3)} (alpha=${next.alpha.toFixed(2)}, beta=${next.beta.toFixed(2)}, updates=${next.updates})`);
266
+ log(
267
+ ` mean ${next.mean.toFixed(3)} (alpha=${next.alpha.toFixed(2)}, beta=${next.beta.toFixed(2)}, updates=${next.updates})`
268
+ );
213
269
  if (promotion) {
214
270
  log(` promotion PROPOSED (${promotion.id}): ${promotion.fromStatus} -> ${promotion.toStatus}`);
215
271
  log(` awaiting human/Shield approval — never auto-applied (governance guard)`);
216
272
  } else {
217
- log(` promotion: none proposed (needs mean >= ${PROMOTION_MEAN_THRESHOLD} and >= ${PROMOTION_MIN_UPDATES} updates)`);
273
+ log(
274
+ ` promotion: none proposed (needs mean >= ${PROMOTION_MEAN_THRESHOLD} and >= ${PROMOTION_MIN_UPDATES} updates)`
275
+ );
218
276
  }
219
277
  return result;
220
278
  }
@@ -223,14 +281,20 @@ function runReinforce(opts = {}) {
223
281
 
224
282
  module.exports = {
225
283
  command: 'mem <action> [query...]',
226
- description: 'Karpathy memory loop — recall | write | reinforce (portable across agent CLIs)',
284
+ description: 'Project memory — recall | write | reinforce | observe | outcomes',
227
285
  options: [
228
286
  ['-d, --directory <path>', 'Project directory (default: current directory)'],
229
287
  ['--scope <scope>', 'Recall scope: project | portfolio', 'project'],
230
288
  ['--portfolio <path>', 'Portfolio brain directory (used with --scope portfolio)'],
231
289
  ['--limit <n>', 'Max recall results', '8'],
290
+ ['--ranking <mode>', 'Recall ranking: lexical | evidence (project only)', 'lexical'],
291
+ ['--context-scope <paths>', 'Comma-separated project paths for evidence recall'],
292
+ ['--input <file>', 'Project-relative JSON outcome observation input (observe)'],
232
293
  ['--agent <name>', 'Agent that produced the event (write)'],
233
- ['--cli <name>', 'Driving CLI: claude-code, gemini-cli, codex-cli, cursor, opencode, aider, antigravity'],
294
+ [
295
+ '--cli <name>',
296
+ 'Driving CLI: claude-code, gemini-cli, codex-cli, cursor, opencode, aider, antigravity',
297
+ ],
234
298
  ['--model <id>', 'Model id used (model-agnostic: claude/gpt/gemini/local)'],
235
299
  ['--task <text>', 'What was attempted (write)'],
236
300
  ['--outcome <outcome>', 'success | failure | partial | abandoned (write)'],
@@ -245,6 +309,8 @@ module.exports = {
245
309
  recall: 'Ranked retrieval over the journal + memory files',
246
310
  write: 'Append a structured event to .bmad/memory/journal.ndjson',
247
311
  reinforce: 'Apply a reward to a pattern score (promotions PROPOSED only)',
312
+ observe: 'Record accepted current Nexus evidence for an exact memory section',
313
+ outcomes: 'Inspect stored outcomes and their current eligibility',
248
314
  },
249
315
 
250
316
  action: async (action, query, options = {}) => {
@@ -264,6 +330,12 @@ module.exports = {
264
330
  limit: Number(options.limit || 8),
265
331
  now,
266
332
  json,
333
+ ranking: options.ranking || 'lexical',
334
+ contextScope: options.contextScope
335
+ ? String(options.contextScope)
336
+ .split(',')
337
+ .map((s) => s.trim())
338
+ : undefined,
267
339
  });
268
340
  } else if (action === 'write') {
269
341
  runWrite({
@@ -282,16 +354,44 @@ module.exports = {
282
354
  baseDir,
283
355
  patternId: options.pattern,
284
356
  signals: parseSignals(options),
285
- evidence: options.evidence ? String(options.evidence).split(',').map(s => s.trim()).filter(Boolean) : [],
357
+ evidence: options.evidence
358
+ ? String(options.evidence)
359
+ .split(',')
360
+ .map((s) => s.trim())
361
+ .filter(Boolean)
362
+ : [],
286
363
  now,
287
364
  json,
288
365
  });
366
+ } else if (action === 'observe') {
367
+ const input = JSON.parse(store.readText(baseDir, options.input));
368
+ const written = outcomes.observeOutcome(baseDir, input, { now });
369
+ console.log(
370
+ json
371
+ ? JSON.stringify({ written }, null, 2)
372
+ : `mem observe: ${written.id} recorded; lesson causality remains unmeasured.`
373
+ );
374
+ } else if (action === 'outcomes') {
375
+ const results = outcomes.inspectOutcomes(baseDir);
376
+ console.log(
377
+ json
378
+ ? JSON.stringify({ count: results.length, results }, null, 2)
379
+ : results
380
+ .map(
381
+ (record) =>
382
+ `${record.id}: ${record.eligible ? 'accepted-current' : record.reason}`
383
+ )
384
+ .join('\n') || 'mem outcomes: no observations.'
385
+ );
289
386
  } else {
290
- console.error(`mem: unknown action '${action}' — expected recall | write | reinforce`);
387
+ console.error(
388
+ `mem: unknown action '${action}' — expected recall | write | reinforce | observe | outcomes`
389
+ );
291
390
  process.exitCode = 1;
292
391
  }
293
392
  } catch (err) {
294
- console.error(`mem ${action}: ${err.message}`);
393
+ if (json) console.log(JSON.stringify({ error: err.message }));
394
+ else console.error(`mem ${action}: ${err.message}`);
295
395
  process.exitCode = 1;
296
396
  }
297
397
  },
@@ -0,0 +1,111 @@
1
+ /** Persist host-owned work, reconcile interruptions and verify current artifacts. */
2
+ const fs = require('node:fs');
3
+ const path = require('node:path');
4
+ const nexus = require('../lib/nexus');
5
+
6
+ function inputFile(file, option) {
7
+ if (!file) throw new Error(option + ' requires a JSON file.');
8
+ const absolute = path.resolve(file);
9
+ const stat = fs.lstatSync(absolute);
10
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 1024 * 1024) {
11
+ throw new Error(option + ' must name a regular JSON file no larger than 1 MiB.');
12
+ }
13
+ const value = JSON.parse(fs.readFileSync(absolute, 'utf8'));
14
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
15
+ throw new Error(option + ' must contain a JSON object.');
16
+ }
17
+ return value;
18
+ }
19
+
20
+ function summary(result) {
21
+ if (!result.tasks) return JSON.stringify(result, null, 2);
22
+ return [
23
+ `Nexus run ${result.id} — durable task coordination`,
24
+ ...result.tasks.map((task) => {
25
+ const attempt = task.attempts.at(-1);
26
+ const observation = result.observations?.find((item) => item.taskId === task.id);
27
+ return (
28
+ `${task.id}: ${attempt?.execution || 'not started'}; verification=${attempt?.verification.status || 'pending'}; acceptance=${task.integration}` +
29
+ (attempt
30
+ ? `\n Attempt: ${attempt.id}; ${attempt.backend.kind}: ${attempt.backend.id}/${attempt.backend.sessionId}`
31
+ : '') +
32
+ (observation
33
+ ? `\n ${observation.hint}${observation.stale ? ' STALE: ' + observation.staleReason : ''}`
34
+ : '')
35
+ );
36
+ }),
37
+ 'Use inspect --json for recorded evidence and reconciliation observations.',
38
+ ].join('\n');
39
+ }
40
+
41
+ module.exports = {
42
+ command: 'nexus <action> [run] [task]',
43
+ description:
44
+ 'Launch or coordinate attempts, recover interrupted work and verify current artifacts',
45
+ options: [
46
+ ['-d, --directory <path>', 'Project directory'],
47
+ ['--plan <file>', 'JSON task plan for create'],
48
+ ['--input <file>', 'JSON observation or host identity for a mutation'],
49
+ ['--json', 'Print durable state and evidence as JSON'],
50
+ ],
51
+ action: async (action, runId, taskId, options = {}) => {
52
+ try {
53
+ const directory = path.resolve(options.directory || process.cwd());
54
+ let result;
55
+ if (action === 'create' && !runId && !taskId) {
56
+ if (options.input) throw new Error('create uses --plan.');
57
+ result = nexus.createRun(directory, inputFile(options.plan, '--plan'));
58
+ } else if (action === 'recover-lock' && !runId && !taskId) {
59
+ if (options.plan) throw new Error('recover-lock uses --input.');
60
+ result = nexus.recoverLock(directory, inputFile(options.input, '--input'));
61
+ } else if (action === 'inspect' && !taskId) {
62
+ if (options.input || options.plan)
63
+ throw new Error('inspect is read-only and takes no input file.');
64
+ result = nexus.inspectRun(directory, runId);
65
+ } else {
66
+ const mutations = {
67
+ start: nexus.startTask,
68
+ launch: nexus.launchTask,
69
+ collect: nexus.collectTask,
70
+ record: nexus.recordAttempt,
71
+ reconcile: nexus.reconcileAttempt,
72
+ verify: nexus.verifyTask,
73
+ retry: nexus.retryTask,
74
+ cancel: nexus.cancelTask,
75
+ accept: nexus.acceptTask,
76
+ };
77
+ if (!Object.hasOwn(mutations, action) || !runId || !taskId || options.plan) {
78
+ throw new Error(
79
+ 'Use nexus create --plan FILE, inspect RUN, or start|launch|collect|record|reconcile|verify|retry|cancel|accept RUN TASK.'
80
+ );
81
+ }
82
+ const needsInput = !['verify', 'accept'].includes(action);
83
+ if (!needsInput && options.input)
84
+ throw new Error(action + ' executes its recorded contract and takes no input file.');
85
+ result = await mutations[action](
86
+ directory,
87
+ runId,
88
+ taskId,
89
+ action === 'launch' && !options.input
90
+ ? {}
91
+ : needsInput
92
+ ? inputFile(options.input, '--input')
93
+ : undefined
94
+ );
95
+ }
96
+ const latest = result.tasks?.find((task) => task.id === taskId)?.attempts.at(-1);
97
+ process.exitCode =
98
+ (action === 'verify' && latest?.verification.status !== 'passed') ||
99
+ (action === 'launch' && latest?.process?.receipt?.outcome !== 'completed') ||
100
+ (action === 'collect' && latest?.execution !== 'completed')
101
+ ? 1
102
+ : 0;
103
+ console.log(options.json ? JSON.stringify(result, null, 2) : summary(result));
104
+ } catch (error) {
105
+ process.exitCode = 1;
106
+ if (options.json)
107
+ console.log(JSON.stringify({ schemaVersion: 1, status: 'error', error: error.message }));
108
+ else console.error('Nexus: ' + error.message);
109
+ }
110
+ },
111
+ };