bmad-plus 0.14.0 → 0.17.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 +76 -0
- package/README.md +55 -18
- package/SECURITY.md +71 -0
- package/THIRD-PARTY-LICENSES.md +349 -0
- package/osint-agent-package/README.md +1 -1
- package/package.json +11 -3
- package/readme-international/README.de.md +20 -9
- package/readme-international/README.es.md +21 -10
- package/readme-international/README.fr.md +20 -9
- package/src/bmad-plus/agents/agent-architect-dev/SKILL.md +11 -13
- package/src/bmad-plus/agents/agent-orchestrator/SKILL.md +147 -8
- package/src/bmad-plus/agents/agent-quality/SKILL.md +41 -11
- package/src/bmad-plus/data/role-triggers.yaml +19 -0
- package/src/bmad-plus/module-help.csv +1 -0
- package/src/bmad-plus/module.yaml +1 -0
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/create-story.md +3 -1
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/dev-story-checklist.md +2 -0
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/dev-story.md +5 -1
- package/src/bmad-plus/packs/pack-memory/README.md +29 -4
- package/src/bmad-plus/packs/pack-memory/memory-orchestrator.md +21 -1
- package/src/bmad-plus/packs/pack-memory/shared/karpathy-guardrails.md +3 -3
- package/src/bmad-plus/packs/pack-memory/shared/memory-protocol.md +27 -3
- package/src/bmad-plus/packs/pack-memory/zecher-agent.md +18 -2
- package/src/bmad-plus/skills/bmad-plus-autopilot/SKILL.md +47 -10
- package/src/bmad-plus/skills/bmad-plus-parallel/SKILL.md +17 -3
- package/src/bmad-plus/skills/bmad-plus-sync/SKILL.md +76 -67
- package/src/bmad-plus/skills/bmad-plus-uat/SKILL.md +158 -0
- package/src/bmad-plus/skills/bmad-plus-uat/ref/uat-results.schema.json +60 -0
- package/src/bmad-plus/skills/bmad-plus-uat/ref/uat-spec.schema.json +121 -0
- package/src/bmad-plus/skills/bmad-plus-uat/ref/uat-triage.schema.json +60 -0
- package/src/bmad-plus/skills/bmad-plus-uat/template/page.html +628 -0
- package/src/bmad-plus/skills/bmad-plus-uat/template/strings.json +442 -0
- package/src/bmad-plus/skills/bmad-plus-uat/templates/README.md +63 -0
- package/src/bmad-plus/skills/bmad-plus-uat/templates/example-uat-spec.json +95 -0
- package/src/bmad-plus/skills/bmad-plus-uat/templates/uat-release-gate.mjs +127 -0
- package/src/bmad-plus/skills/bmad-plus-uat/templates/uat-spec-present.mjs +77 -0
- package/tools/build/check-install-contract.js +202 -4
- package/tools/build/generate.js +16 -0
- package/tools/build/generated-adapters/.codex/AGENTS.md +1 -1
- package/tools/build/generated-adapters/.cursor/rules/bmad-plus.mdc +1 -1
- package/tools/build/generated-adapters/.opencode/AGENTS.md +1 -1
- package/tools/build/generated-adapters/AGENTS.md +1 -1
- package/tools/build/generated-adapters/CLAUDE.md +1 -1
- package/tools/build/generated-adapters/CONVENTIONS.md +1 -1
- package/tools/build/generated-adapters/GEMINI.md +1 -1
- package/tools/cli/bmad-plus-cli.js +15 -12
- package/tools/cli/commands/autoconfig.js +4 -2
- package/tools/cli/commands/doctor.js +1 -0
- package/tools/cli/commands/install.js +21 -2
- package/tools/cli/commands/memory-journal-cmd.js +119 -19
- package/tools/cli/commands/nexus.js +111 -0
- package/tools/cli/commands/uat.js +405 -0
- package/tools/cli/i18n.js +10 -0
- package/tools/cli/lib/README-memory-journal.md +19 -8
- package/tools/cli/lib/installation-health.js +6 -0
- package/tools/cli/lib/memory-journal.js +0 -0
- package/tools/cli/lib/memory-outcomes.js +293 -0
- package/tools/cli/lib/memory-store.js +139 -0
- package/tools/cli/lib/nexus-process.js +377 -0
- package/tools/cli/lib/nexus.js +1532 -0
- package/tools/cli/lib/pack-copy.js +39 -11
- package/tools/cli/lib/packs.js +17 -3
- package/tools/cli/lib/uat.js +887 -0
- package/tools/maintain/upstream-candidate.js +456 -0
- package/tools/release/publication-content.js +4 -1
- package/tools/release/supply-chain.js +282 -0
|
@@ -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
|
-
|
|
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 {
|
|
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, {
|
|
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 =
|
|
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 {
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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 = {
|
|
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(
|
|
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(
|
|
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: '
|
|
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
|
-
[
|
|
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
|
|
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(
|
|
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.
|
|
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
|
+
};
|