amicus 4.6.3 → 4.7.1
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/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +349 -0
- package/README.md +19 -7
- package/bin/amicus.js +31 -0
- package/docs/ROADMAP.md +143 -36
- package/docs/configuration.md +52 -4
- package/docs/council.md +65 -12
- package/docs/doc-system.md +8 -7
- package/docs/schemas.md +10 -1
- package/docs/testing.md +1 -1
- package/docs/troubleshooting.md +33 -5
- package/docs/usage.md +64 -14
- package/electron/workspace-ui/index.html +3 -0
- package/electron/workspace-ui/live-model.js +52 -14
- package/electron/workspace-ui/workspace-app.js +14 -3
- package/electron/workspace-ui/workspace-lazy.js +233 -0
- package/electron/workspace-ui/workspace-matrix.js +12 -1
- package/electron/workspace-ui/workspace-panels.js +24 -171
- package/electron/workspace-ui/workspace-render.js +6 -2
- package/electron/workspace-ui/workspace-seats.js +68 -0
- package/electron/workspace-ui/workspace.css +6 -0
- package/package.json +8 -4
- package/schemas/council-run.schema.json +1 -0
- package/schemas/council-stats.schema.json +9 -1
- package/schemas/run.schema.json +2 -1
- package/schemas/spend.schema.json +1 -1
- package/schemas/wave.schema.json +2 -1
- package/scripts/postinstall.js +6 -3
- package/scripts/setup-hooks.js +49 -3
- package/skills/second-opinion/MANUAL-ORCHESTRATION.md +12 -0
- package/skills/second-opinion/MODEL-NOTES.md +5 -4
- package/skills/sidecar/SKILL.md +9 -2
- package/src/cli-council-run-bench.js +86 -0
- package/src/cli-handlers-council-run.js +65 -81
- package/src/cli-handlers-council.js +17 -5
- package/src/cli-handlers-fanout.js +179 -0
- package/src/cli-handlers-pack.js +24 -10
- package/src/cli-handlers-resume-continue.js +20 -0
- package/src/cli-handlers-run.js +19 -161
- package/src/cli-template-args.js +48 -0
- package/src/cli.js +39 -46
- package/src/council/debate.js +89 -10
- package/src/council/ledger.js +72 -11
- package/src/council/report.js +17 -6
- package/src/council/run-assemble.js +15 -3
- package/src/council/run-budget.js +2 -2
- package/src/council/run-chair.js +61 -5
- package/src/council/run-debate.js +51 -67
- package/src/council/run-launch.js +20 -2
- package/src/council/run-retry.js +17 -2
- package/src/council/run-stage1-launch.js +94 -0
- package/src/council/run-stage2.js +25 -4
- package/src/council/run-stages.js +79 -86
- package/src/council/run-state.js +10 -2
- package/src/council/run.js +26 -2
- package/src/council/tally.js +6 -2
- package/src/headless.js +69 -6
- package/src/mcp-council-awareness.js +1 -0
- package/src/mcp-council-bench.js +4 -0
- package/src/mcp-council-run.js +10 -0
- package/src/mcp-server.js +114 -54
- package/src/mcp-tools.js +12 -5
- package/src/pack/pack-cli.js +1 -1
- package/src/pack/pack-forward.js +12 -4
- package/src/pack/pack-resolve.js +3 -0
- package/src/pack/pack-store.js +20 -3
- package/src/pack/pack-validate.js +5 -1
- package/src/sidecar/budget.js +38 -4
- package/src/sidecar/continue.js +8 -23
- package/src/sidecar/fanout-budget.js +1 -2
- package/src/sidecar/fanout-leg-fallback.js +7 -3
- package/src/sidecar/fanout-retry.js +15 -3
- package/src/sidecar/fanout-wave-io.js +13 -1
- package/src/sidecar/fanout.js +11 -9
- package/src/sidecar/list-limit.js +50 -0
- package/src/sidecar/list-search.js +69 -0
- package/src/sidecar/read.js +90 -5
- package/src/sidecar/reopen-spend.js +32 -0
- package/src/sidecar/resume.js +1 -1
- package/src/sidecar/start-metadata.js +58 -0
- package/src/sidecar/start.js +8 -43
- package/src/sidecar/workspace-auto-open.js +2 -2
- package/src/spend-query.js +2 -1
- package/src/template/apply.js +7 -4
- package/src/template/render.js +6 -2
- package/src/template/store.js +1 -1
- package/src/utils/cli-preflight.js +27 -1
- package/src/utils/config.js +15 -0
- package/src/utils/doctor-engine-check.js +32 -0
- package/src/utils/engine-install-scan.js +98 -15
- package/src/utils/engine-repair.js +96 -2
- package/src/utils/remediation-hints.js +29 -0
- package/src/utils/result-schema-rebuild.js +1 -0
- package/src/utils/result-schema.js +6 -1
- package/src/utils/session-index-tmp-sweep.js +18 -3
- package/src/utils/session-index.js +1 -0
- package/src/utils/session-metadata-tmp-sweep.js +24 -4
- package/src/utils/spend-ledger.js +11 -4
- package/src/utils/validators.js +16 -0
package/src/mcp-server.js
CHANGED
|
@@ -423,6 +423,10 @@ const handlers = {
|
|
|
423
423
|
// gate / template pre-render) for parity between the two paths.
|
|
424
424
|
if (packForward.maxCost !== undefined) { args.push('--max-cost', String(packForward.maxCost)); }
|
|
425
425
|
if (packForward.template !== undefined) { args.push('--template', packForward.template); }
|
|
426
|
+
// v4.7 F8 (D13, errata E-PR3-2): forwarded for the spawn-fallback path
|
|
427
|
+
// below — DEAD on the shared-server branch (args is never read there);
|
|
428
|
+
// that branch stamps input.tag directly into its own metadata write instead.
|
|
429
|
+
if (input.tag) { args.push('--tag', input.tag); }
|
|
426
430
|
args.push('--cwd', cwd);
|
|
427
431
|
|
|
428
432
|
if (sharedServer.enabled && input.noUi) {
|
|
@@ -435,28 +439,30 @@ const handlers = {
|
|
|
435
439
|
// a fall-through to the spawn-fallback catch must never leak one.
|
|
436
440
|
const inProcessNotices = [...fwd.notices];
|
|
437
441
|
const renderedPrompt = fwd.renderedPrompt !== undefined ? fwd.renderedPrompt : input.prompt;
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
442
|
+
// v4.7 PR6: the gate used to hang off `packForward.maxCost !== undefined`,
|
|
443
|
+
// so a no-pack MCP start skipped it while the CLI (cli-handlers-run.js:90)
|
|
444
|
+
// gated unconditionally with a cfg.maxCost fallback. Same guard, both doors.
|
|
445
|
+
// fwd.maxCost is already validated (I2); the ceiling gate itself
|
|
446
|
+
// stays here (needs resolvedModel/pricing, unavailable to pack-forward.js).
|
|
447
|
+
const { lookupPricing } = require('./utils/pricing');
|
|
448
|
+
const { checkBudget, formatBudgetError } = require('./sidecar/budget');
|
|
449
|
+
const { loadConfig } = require('./utils/config');
|
|
450
|
+
const cfg = loadConfig() || {};
|
|
451
|
+
const soloLeg = { modelInput: input.model || resolvedModel, model: resolvedModel, pricing: lookupPricing(resolvedModel) };
|
|
452
|
+
const budget = checkBudget([soloLeg], {
|
|
453
|
+
maxCostPerMtok: cfg.maxCostPerMtok,
|
|
454
|
+
maxCost: fwd.maxCost !== undefined ? fwd.maxCost : cfg.maxCost,
|
|
455
|
+
promptChars: (renderedPrompt && renderedPrompt.length) || 0,
|
|
456
|
+
});
|
|
457
|
+
if (!budget.ok) {
|
|
458
|
+
const { buildErrorDoc, ERROR_CODES } = require('./utils/error-doc');
|
|
459
|
+
return {
|
|
460
|
+
isError: true,
|
|
461
|
+
content: [{ type: 'text', text: JSON.stringify(buildErrorDoc({
|
|
462
|
+
code: ERROR_CODES.BUDGET_EXCEEDED, message: 'Error: budget gate refused the run',
|
|
463
|
+
hint: formatBudgetError(budget, { kind: 'mcp' }),
|
|
464
|
+
})) }],
|
|
465
|
+
};
|
|
460
466
|
}
|
|
461
467
|
|
|
462
468
|
const { server, client } = await sharedServer.ensureServer();
|
|
@@ -500,6 +506,10 @@ const handlers = {
|
|
|
500
506
|
briefing: renderedPrompt,
|
|
501
507
|
// v4.5 Task 15: additive-only — absent (not null) without a pack.
|
|
502
508
|
...(packRecord ? { pack: packRecord } : {}),
|
|
509
|
+
// v4.7 F8 (D13, errata E-PR3-2): THE critical site — this shared-server
|
|
510
|
+
// branch never spawns a CLI child, so the --tag argv forward above
|
|
511
|
+
// (dead here) can never reach it. Same additive-only idiom as pack.
|
|
512
|
+
...(input.tag ? { tag: input.tag } : {}),
|
|
503
513
|
}, null, 2), { mode: 0o600 });
|
|
504
514
|
|
|
505
515
|
// Build context from parent conversation (unless --no-context)
|
|
@@ -693,6 +703,19 @@ const handlers = {
|
|
|
693
703
|
// resolved the route in-process above, so surface its notice here.
|
|
694
704
|
const spawnContent = [{ type: 'text', text: body }];
|
|
695
705
|
if (routeResult.notice) { spawnContent.push({ type: 'text', text: routeResult.notice }); }
|
|
706
|
+
// W1-M6/M7: NOT dead code. This is the same idiom applyPackToMcpInput's
|
|
707
|
+
// orphan-knob notices always use — amicus_fanout genuinely reaches it today
|
|
708
|
+
// (FANOUT_PACK_PARAM_MAP has no contextTurns/contextMaxTokens destination;
|
|
709
|
+
// the fanout handler's own push into waveContent below is covered by
|
|
710
|
+
// mcp-pack-params.test.js's "notice wording (decision 1b)" test — named,
|
|
711
|
+
// not line-numbered, because a line citation goes stale the moment anything
|
|
712
|
+
// is inserted above it, including by its own commit). On THIS solo surface it is
|
|
713
|
+
// unreachable only while KIND_OPTIONS.solo stays fully covered by
|
|
714
|
+
// SOLO_PACK_PARAM_MAP destinations + pack-resolve.js's FORWARDABLE_ARG_KEYS
|
|
715
|
+
// — the invariant tests/pack/mcp-pack-params.test.js guards (the
|
|
716
|
+
// KIND_OPTIONS.solo round-trip test, mutation-proven against a synthetic
|
|
717
|
+
// orphaned knob). If that invariant ever breaks, this loop is what turns a
|
|
718
|
+
// silent drop into a visible notice — never delete it as unreachable.
|
|
696
719
|
for (const n of packNotices) { spawnContent.push({ type: 'text', text: n }); }
|
|
697
720
|
if (isHeadless) {
|
|
698
721
|
spawnContent.push({ type: 'text', text: HEADLESS_START_REMINDER });
|
|
@@ -984,37 +1007,11 @@ const handlers = {
|
|
|
984
1007
|
const root = path.join(cwd, '.claude', SESSIONS_DIR);
|
|
985
1008
|
if (!fs.existsSync(root)) { return textResult('No amicus sessions found.'); }
|
|
986
1009
|
|
|
1010
|
+
// v4.7 F8 (D14): one enumeration behind both the CLI and MCP list surfaces
|
|
1011
|
+
// — enumerateSessions is the shared core (src/sidecar/read.js).
|
|
1012
|
+
const { enumerateSessions, searchSessions } = require('./sidecar/read');
|
|
987
1013
|
const byId = new Map();
|
|
988
|
-
for (const
|
|
989
|
-
if (!/^[a-zA-Z0-9_-]{1,64}$/.test(d)) { continue; }
|
|
990
|
-
if (byId.has(d)) { continue; }
|
|
991
|
-
const metaPath = path.join(root, d, 'metadata.json');
|
|
992
|
-
if (!fs.existsSync(metaPath)) { continue; }
|
|
993
|
-
try {
|
|
994
|
-
const meta = JSON.parse(fs.readFileSync(metaPath, 'utf-8'));
|
|
995
|
-
const entry = {
|
|
996
|
-
id: d, model: meta.model, status: meta.status, agent: meta.agent,
|
|
997
|
-
briefing: sanitizePreview(String(meta.briefing || ''), 80),
|
|
998
|
-
createdAt: meta.createdAt,
|
|
999
|
-
mode: meta.mode
|
|
1000
|
-
|| (meta.headless === undefined ? undefined : (meta.headless ? 'headless' : 'interactive')),
|
|
1001
|
-
};
|
|
1002
|
-
// Live-progress enrichment for RUNNING sessions only — readProgress
|
|
1003
|
-
// parses conversation.jsonl, so terminal rows stay cheap.
|
|
1004
|
-
if (meta.status === 'running') {
|
|
1005
|
-
try {
|
|
1006
|
-
const p = readProgress(path.join(root, d));
|
|
1007
|
-
entry.phase = deriveStage(meta.status, p.stage);
|
|
1008
|
-
entry.messageCount = p.messages;
|
|
1009
|
-
entry.lastActivityAt = p.lastActivityAt;
|
|
1010
|
-
entry.latestPreview = p.latestPreview;
|
|
1011
|
-
} catch { /* progress optional */ }
|
|
1012
|
-
}
|
|
1013
|
-
byId.set(d, entry);
|
|
1014
|
-
} catch {
|
|
1015
|
-
// Skip unreadable metadata
|
|
1016
|
-
}
|
|
1017
|
-
}
|
|
1014
|
+
for (const row of enumerateSessions(cwd, {})) { byId.set(row.id, row); }
|
|
1018
1015
|
|
|
1019
1016
|
// v4.0 §8: council runs are pointer files in the same sessions root — merge
|
|
1020
1017
|
// them as first-class rows (type 'council-run') before sorting/filtering.
|
|
@@ -1025,8 +1022,34 @@ const handlers = {
|
|
|
1025
1022
|
if (input.status && input.status !== 'all') {
|
|
1026
1023
|
sessions = sessions.filter(s => s.status === input.status);
|
|
1027
1024
|
}
|
|
1025
|
+
// v4.7 F8 (D15, errata E-PR3-5): search runs on RAW briefing material —
|
|
1026
|
+
// BEFORE the sanitize/enrich pass below truncates row.briefing to an
|
|
1027
|
+
// 80-char preview. Council rows' material is read straight off disk
|
|
1028
|
+
// inside searchSessions; the `briefing` merged onto them here is already
|
|
1029
|
+
// listCouncilRuns' own sanitized preview and is never used for matching.
|
|
1030
|
+
if (input.search) { sessions = searchSessions(sessions, input.search, { project: cwd }); }
|
|
1028
1031
|
if (sessions.length === 0) { return textResult('No amicus sessions found.'); }
|
|
1029
1032
|
|
|
1033
|
+
// Rows are re-grafted here with the EXACT decorations this handler always
|
|
1034
|
+
// applied: sanitized (not raw) briefing, and running-only live-progress
|
|
1035
|
+
// enrichment. Council rows already carry a sanitized briefing from
|
|
1036
|
+
// listCouncilRuns, so they're left alone.
|
|
1037
|
+
for (const row of sessions) {
|
|
1038
|
+
if (row.type === 'council-run') { continue; }
|
|
1039
|
+
row.briefing = sanitizePreview(String(row.briefing || ''), 80);
|
|
1040
|
+
// Live-progress enrichment for RUNNING sessions only — readProgress
|
|
1041
|
+
// parses conversation.jsonl, so terminal rows stay cheap.
|
|
1042
|
+
if (row.status === 'running') {
|
|
1043
|
+
try {
|
|
1044
|
+
const p = readProgress(path.join(root, row.id));
|
|
1045
|
+
row.phase = deriveStage(row.status, p.stage);
|
|
1046
|
+
row.messageCount = p.messages;
|
|
1047
|
+
row.lastActivityAt = p.lastActivityAt;
|
|
1048
|
+
row.latestPreview = p.latestPreview;
|
|
1049
|
+
} catch { /* progress optional */ }
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1030
1053
|
return textResult(JSON.stringify(sessions, null, 2));
|
|
1031
1054
|
},
|
|
1032
1055
|
|
|
@@ -1237,6 +1260,17 @@ const handlers = {
|
|
|
1237
1260
|
if (fwd.error) { return textResult(fwd.error.message + (fwd.error.hint ? `\n${fwd.error.hint}` : ''), true); }
|
|
1238
1261
|
packNotices.push(...fwd.notices);
|
|
1239
1262
|
|
|
1263
|
+
// ⚠️ v4.7 PR7: the zod schema closes the TYPED door; a pack can push the same values through
|
|
1264
|
+
// the other one (validatePack checks option KEY names, never value types). Both entrances
|
|
1265
|
+
// reach the same spawn, so the check lives here, after pack merge, before any wave dir.
|
|
1266
|
+
if (typeof input.prompt !== 'string' || !input.prompt.trim()) {
|
|
1267
|
+
return textResult('Error: prompt must not be empty.', true);
|
|
1268
|
+
}
|
|
1269
|
+
if (input.timeout !== undefined
|
|
1270
|
+
&& (typeof input.timeout !== 'number' || !Number.isFinite(input.timeout) || input.timeout <= 0)) {
|
|
1271
|
+
return textResult('Error: timeout must be a positive number of minutes.', true);
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1240
1274
|
// Resolve a single effective models list (council OR models), validated
|
|
1241
1275
|
// BEFORE any wave dir / metadata is written so a bad request never strands
|
|
1242
1276
|
// a pid-less 'running' orphan wave.
|
|
@@ -1270,15 +1304,34 @@ const handlers = {
|
|
|
1270
1304
|
const waveDir = getSessionDir(cwd, waveId);
|
|
1271
1305
|
|
|
1272
1306
|
let briefingPath;
|
|
1307
|
+
let childPromptPath;
|
|
1273
1308
|
try {
|
|
1274
1309
|
fs.mkdirSync(waveDir, { recursive: true, mode: 0o700 });
|
|
1275
1310
|
briefingPath = path.join(waveDir, 'briefing.md');
|
|
1311
|
+
// ⚠️ W1-M4 (v4.7 PR7): this default is LOAD-BEARING. Omit it and every non-template wave
|
|
1312
|
+
// spawns with `--prompt-file undefined`.
|
|
1313
|
+
childPromptPath = briefingPath;
|
|
1276
1314
|
// The prompt goes via file: the spawned command line must NOT carry it,
|
|
1277
1315
|
// or it re-hits the ~32KB Windows argument cap (F4 spec §4.2).
|
|
1278
|
-
|
|
1316
|
+
// ⚠️ W1-M4: briefing.md is the SEARCH CORPUS — src/sidecar/list-search.js reads it verbatim
|
|
1317
|
+
// — and a child that aborts before fanout.js:145 never re-renders it, leaving the wave
|
|
1318
|
+
// permanently unfindable by the text the user actually sees. Write the RENDERED text here
|
|
1319
|
+
// (parity with the amicus_start path at :441/:506) and hand the child the raw input in a
|
|
1320
|
+
// sibling file, so its own later re-render still produces byte-identical output and
|
|
1321
|
+
// promptMeta.template provenance survives.
|
|
1322
|
+
const briefingText = fwd.renderedPrompt !== undefined ? fwd.renderedPrompt : input.prompt;
|
|
1323
|
+
fs.writeFileSync(briefingPath, briefingText, { mode: 0o600 });
|
|
1324
|
+
if (fwd.renderedPrompt !== undefined) {
|
|
1325
|
+
childPromptPath = path.join(waveDir, 'briefing-input.md');
|
|
1326
|
+
fs.writeFileSync(childPromptPath, input.prompt, { mode: 0o600 });
|
|
1327
|
+
}
|
|
1328
|
+
// ⚠️ W1-M4 rider (owner-approved fold): the `briefing` key below must stay — without it the
|
|
1329
|
+
// `list` BRIEFING column renders EMPTY for an aborted MCP wave, since only fanout.js:147
|
|
1330
|
+
// ever wrote a briefing key and an aborted child never reaches it.
|
|
1279
1331
|
writeFileAtomic(path.join(waveDir, 'metadata.json'), JSON.stringify({
|
|
1280
1332
|
taskId: waveId, type: 'wave', status: 'running', legs: legIds,
|
|
1281
1333
|
models: effectiveModels, headless: true, createdAt: new Date().toISOString(),
|
|
1334
|
+
briefing: briefingText.slice(0, 200),
|
|
1282
1335
|
// v4.5 Task 15: additive-only — absent (not null) without a pack.
|
|
1283
1336
|
...(packRecord ? { pack: packRecord } : {}),
|
|
1284
1337
|
}, null, 2), { mode: 0o600 });
|
|
@@ -1292,7 +1345,7 @@ const handlers = {
|
|
|
1292
1345
|
|
|
1293
1346
|
const args = [
|
|
1294
1347
|
'fanout', '--models', effectiveModels.join(','),
|
|
1295
|
-
'--prompt-file',
|
|
1348
|
+
'--prompt-file', childPromptPath, '--wave-id', waveId,
|
|
1296
1349
|
'--json', '--client', detectClient(mcpServer), '--cwd', cwd,
|
|
1297
1350
|
];
|
|
1298
1351
|
const agent = input.agent || 'Build';
|
|
@@ -1317,6 +1370,12 @@ const handlers = {
|
|
|
1317
1370
|
// itself is never forwarded, only the two knobs it resolved to).
|
|
1318
1371
|
if (packForward.maxCost !== undefined) { args.push('--max-cost', String(packForward.maxCost)); }
|
|
1319
1372
|
if (packForward.template !== undefined) { args.push('--template', packForward.template); }
|
|
1373
|
+
// v4.7 F8 (D13, errata E-PR3-2): argv-only forward — unlike pack, this does
|
|
1374
|
+
// NOT pre-seed the wave metadata (no single-resolution rule forces it here);
|
|
1375
|
+
// the spawned CLI child's own cli-handlers-fanout.js stores the tag on wave
|
|
1376
|
+
// metadata itself (Task 3), so fanout.js's metaTag inherit arm has no MCP
|
|
1377
|
+
// producer and is defense-in-depth only.
|
|
1378
|
+
if (input.tag) { args.push('--tag', input.tag); }
|
|
1320
1379
|
|
|
1321
1380
|
let child;
|
|
1322
1381
|
try { child = spawnSidecarProcess(args, waveDir); } catch (err) {
|
|
@@ -1506,4 +1565,5 @@ async function startMcpServer() {
|
|
|
1506
1565
|
|
|
1507
1566
|
module.exports = {
|
|
1508
1567
|
handlers, startMcpServer, getProjectDir, resolveProjectDir, getClientRoot,
|
|
1568
|
+
FANOUT_PACK_PARAM_MAP, SOLO_PACK_PARAM_MAP,
|
|
1509
1569
|
};
|
package/src/mcp-tools.js
CHANGED
|
@@ -121,6 +121,7 @@ function getTools() {
|
|
|
121
121
|
'left: flush against the left edge. center: centered.'
|
|
122
122
|
),
|
|
123
123
|
pack: z.string().optional().describe('Policy pack name or path — bench/chair/options/template defaults for this run; explicit params override pack values (recorded either way).'),
|
|
124
|
+
tag: z.string().regex(/^[a-zA-Z0-9_-]{1,64}$/, '1-64 chars, letters/digits/_/- only').optional().describe('Label this session for list/search/spend grouping'),
|
|
124
125
|
project: z.string().optional().describe(
|
|
125
126
|
'Optional project directory path. Auto-detected from working directory if omitted.'
|
|
126
127
|
),
|
|
@@ -211,10 +212,14 @@ function getTools() {
|
|
|
211
212
|
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
212
213
|
description:
|
|
213
214
|
'List all Amicus sessions for the current project. Shows task ID, ' +
|
|
214
|
-
'model, status,
|
|
215
|
+
'model, status, tag, createdAt, and briefing excerpt. Use `search` to ' +
|
|
216
|
+
'filter by id/tag/briefing substring.',
|
|
215
217
|
inputSchema: {
|
|
216
|
-
status: z.
|
|
217
|
-
|
|
218
|
+
status: z.string().optional().describe(
|
|
219
|
+
"Filter by status ('all', 'running', 'complete', 'error', 'aborted', …). Default: show all."
|
|
220
|
+
),
|
|
221
|
+
search: z.string().optional().describe(
|
|
222
|
+
'Case-insensitive substring filter over id, tag, and briefing material (F8 D15).'
|
|
218
223
|
),
|
|
219
224
|
project: z.string().optional().describe(
|
|
220
225
|
'Optional project directory path. Auto-detected from working directory if omitted.'
|
|
@@ -336,7 +341,7 @@ function getTools() {
|
|
|
336
341
|
'Routing preference: auto (direct-first, default), direct (require a ' +
|
|
337
342
|
'direct provider key), or openrouter (force OpenRouter).'
|
|
338
343
|
),
|
|
339
|
-
prompt: z.string().describe(
|
|
344
|
+
prompt: z.string().min(1, 'prompt must not be empty').describe(
|
|
340
345
|
'The briefing sent to every model. Self-contained briefings work best (set includeContext false).'
|
|
341
346
|
),
|
|
342
347
|
agent: z.enum(['Plan', 'Build']).optional().describe(
|
|
@@ -345,7 +350,7 @@ function getTools() {
|
|
|
345
350
|
thinking: z.enum(['none', 'minimal', 'low', 'medium', 'high', 'xhigh']).optional().describe(
|
|
346
351
|
'Reasoning effort for every leg. Default: medium.'
|
|
347
352
|
),
|
|
348
|
-
timeout: z.number().optional().describe(
|
|
353
|
+
timeout: z.number().positive('timeout must be a positive number of minutes').optional().describe(
|
|
349
354
|
'Per-leg timeout in minutes (wall-clock ≈ slowest leg). Default: 15.'
|
|
350
355
|
),
|
|
351
356
|
summaryLength: z.enum(['brief', 'normal', 'verbose']).optional().describe(
|
|
@@ -369,6 +374,7 @@ function getTools() {
|
|
|
369
374
|
'commands are NOT accepted over MCP.'
|
|
370
375
|
),
|
|
371
376
|
pack: z.string().optional().describe('Policy pack name or path — bench/chair/options/template defaults for this run; explicit params override pack values (recorded either way).'),
|
|
377
|
+
tag: z.string().regex(/^[a-zA-Z0-9_-]{1,64}$/, '1-64 chars, letters/digits/_/- only').optional().describe('Label this session for list/search/spend grouping'),
|
|
372
378
|
project: z.string().optional().describe(
|
|
373
379
|
'Optional project directory path. Auto-detected from working directory if omitted.'
|
|
374
380
|
),
|
|
@@ -509,6 +515,7 @@ function getTools() {
|
|
|
509
515
|
'commands are NOT accepted over MCP.'
|
|
510
516
|
),
|
|
511
517
|
pack: z.string().optional().describe('Policy pack name or path — bench/chair/options/template defaults for this run; explicit params override pack values (recorded either way).'),
|
|
518
|
+
tag: z.string().regex(/^[a-zA-Z0-9_-]{1,64}$/, '1-64 chars, letters/digits/_/- only').optional().describe('Label this session for list/search/spend grouping'),
|
|
512
519
|
ui: z.boolean().optional().describe(
|
|
513
520
|
'Auto-open the Council Workspace window on this run. Default: opens when the client is ' +
|
|
514
521
|
'Claude Code (local), Electron is installed, a display exists, and config workspace.autoOpen is ' +
|
package/src/pack/pack-cli.js
CHANGED
|
@@ -30,7 +30,7 @@ function applyPackOrExit(args, expectedKind, useJson) {
|
|
|
30
30
|
if (args.pack === undefined) { return null; }
|
|
31
31
|
const explicit = args.__explicit || new Set();
|
|
32
32
|
const pr = applyPackToArgs({ packRef: args.pack, expectedKind, args, explicit, useJson });
|
|
33
|
-
if (pr.error) { process.exit(failJson(useJson, pr.error)); }
|
|
33
|
+
if (pr.error) { return process.exit(failJson(useJson, pr.error)); }
|
|
34
34
|
for (const n of pr.notices) { process.stderr.write(n + '\n'); }
|
|
35
35
|
return pr.packRecord;
|
|
36
36
|
}
|
package/src/pack/pack-forward.js
CHANGED
|
@@ -36,10 +36,18 @@
|
|
|
36
36
|
* render, so template provenance (`promptMeta.template`) is still
|
|
37
37
|
* recorded exactly as Wave 1 built it.
|
|
38
38
|
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
39
|
+
* `renderedPrompt` has two different consumers now (W1-M4, v4.7 PR7):
|
|
40
|
+
* amicus_start's in-process path (which never spawns a child, so nothing
|
|
41
|
+
* else would render it) reuses it as the actual prompt instead of rendering
|
|
42
|
+
* a second time; amicus_fanout's spawn path writes it to the wave's
|
|
43
|
+
* on-disk `briefing.md` — the `--search` corpus (src/sidecar/list-search.js)
|
|
44
|
+
* — instead of the raw prompt, so a wave whose spawned child aborts before
|
|
45
|
+
* its own render (src/sidecar/fanout.js) stays findable by the text the
|
|
46
|
+
* user actually sees. The spawned child itself still gets the RAW prompt
|
|
47
|
+
* (via a sibling `briefing-input.md`), so its own later render remains the
|
|
48
|
+
* provenance source for `promptMeta.template`. amicus_start's
|
|
49
|
+
* spawn-fallback path is the one remaining caller that still only needs the
|
|
50
|
+
* pre-spend validation and ignores `renderedPrompt`.
|
|
43
51
|
*/
|
|
44
52
|
|
|
45
53
|
const { ERROR_CODES } = require('../utils/error-doc');
|
package/src/pack/pack-resolve.js
CHANGED
|
@@ -54,6 +54,9 @@ function resolveBenchKnob(pack, args, explicit) {
|
|
|
54
54
|
else if (Array.isArray(bench)) { args.models = bench.join(','); }
|
|
55
55
|
return null;
|
|
56
56
|
}
|
|
57
|
+
// Both-typed names --models here, but every consuming surface — fanout (cli-handlers-fanout.js:77),
|
|
58
|
+
// council run (cli-council-run-bench.js:45), MCP (mcp-council-bench.js:29) — rejects
|
|
59
|
+
// --models+--council with BAD_ARGS after pack apply, so a both-typed notice is never actionable.
|
|
57
60
|
const flag = modelsExplicit ? '--models' : '--council';
|
|
58
61
|
return `Notice: ${flag} overrides the bench from pack '${pack.name}'`;
|
|
59
62
|
}
|
package/src/pack/pack-store.js
CHANGED
|
@@ -52,7 +52,21 @@ function resolvePackRef(ref) {
|
|
|
52
52
|
|
|
53
53
|
function stripBom(text) { return text.charCodeAt(0) === 0xFEFF ? text.slice(1) : text; }
|
|
54
54
|
|
|
55
|
-
/**
|
|
55
|
+
/**
|
|
56
|
+
* @returns {{pack, path, source:'dir'|'path', hash}|{error}}
|
|
57
|
+
* All SIX `{error}` returns below — malformed name, name-form unreadable,
|
|
58
|
+
* path-form unreadable, invalid JSON, non-object body, name/filename mismatch —
|
|
59
|
+
* are mapped to PACK_NOT_FOUND by both callers (pack-resolve.js:76,
|
|
60
|
+
* cli-handlers-pack.js:225). A new `{error}` added here inherits that code
|
|
61
|
+
* silently: re-code it deliberately or it lands as "not found".
|
|
62
|
+
* PACK_NOT_FOUND has a THIRD emit site that never calls readPack — `pack rm`
|
|
63
|
+
* (cli-handlers-pack.js:242, via rmPack) — unified there by the v4.5 HOLD-gate
|
|
64
|
+
* decision 3 and pinned by the "rm nonexistent pack -> PACK_NOT_FOUND" describe
|
|
65
|
+
* in tests/pack/cli-pack-cmd.test.js.
|
|
66
|
+
* PACK_INVALID is NOT readPack's: it belongs to validatePack
|
|
67
|
+
* (pack-resolve.js:94, cli-handlers-pack.js:199) and to prepareForward's
|
|
68
|
+
* maxCost guard (pack-forward.js:68-76), which is not a validatePack call.
|
|
69
|
+
*/
|
|
56
70
|
function readPack(ref) {
|
|
57
71
|
const r = resolvePackRef(ref);
|
|
58
72
|
if (r.error) { return r; }
|
|
@@ -66,8 +80,11 @@ function readPack(ref) {
|
|
|
66
80
|
let pack;
|
|
67
81
|
try { pack = JSON.parse(raw); }
|
|
68
82
|
catch (err) { return { error: `Error: pack ${file} is not valid JSON: ${err.message}` }; }
|
|
69
|
-
if (
|
|
70
|
-
return { error: `Error: pack
|
|
83
|
+
if (pack === null || typeof pack !== 'object' || Array.isArray(pack)) {
|
|
84
|
+
return { error: `Error: pack ${file} is not a pack object (found ${pack === null ? 'null' : Array.isArray(pack) ? 'an array' : typeof pack})` };
|
|
85
|
+
}
|
|
86
|
+
if (r.kind === 'name' && pack.name !== r.name) {
|
|
87
|
+
return { error: `Error: pack file ${file} declares name '${pack.name}' which does not match its filename — rename one of them` };
|
|
71
88
|
}
|
|
72
89
|
return { pack, path: file, source: r.kind === 'name' ? 'dir' : 'path', hash: canonicalHash(pack) };
|
|
73
90
|
}
|
|
@@ -74,6 +74,11 @@ function validatePack(pack, { mode } = { mode: 'run' }) {
|
|
|
74
74
|
if (typeof pack.model !== 'string' || !pack.model.trim()) { errors.push('solo pack requires model'); }
|
|
75
75
|
else if (!seatOk(pack.model)) { errors.push(`unresolvable model '${pack.model}'`); }
|
|
76
76
|
} else {
|
|
77
|
+
// T11-d: bench-independent — a by-name bench used to skip this entirely, so a pack
|
|
78
|
+
// could carry both and only fail (mis-attributed) at handler time.
|
|
79
|
+
if (pack.kind === 'council' && pack.critic && pack.lenses) {
|
|
80
|
+
errors.push('critic and lenses are mutually exclusive');
|
|
81
|
+
}
|
|
77
82
|
const bench = pack.bench;
|
|
78
83
|
if (typeof bench === 'string') {
|
|
79
84
|
const { members } = getCouncilWithSource(bench, []);
|
|
@@ -87,7 +92,6 @@ function validatePack(pack, { mode } = { mode: 'run' }) {
|
|
|
87
92
|
if (pack.kind === 'council') {
|
|
88
93
|
if (pack.chair && bench.includes(pack.chair)) { errors.push(`chair '${pack.chair}' is a bench seat — the chair must not review`); }
|
|
89
94
|
if (pack.critic && !bench.includes(pack.critic)) { errors.push(`critic '${pack.critic}' must be one of the bench seats`); }
|
|
90
|
-
if (pack.critic && pack.lenses) { errors.push('critic and lenses are mutually exclusive'); }
|
|
91
95
|
if (Array.isArray(pack.lenses) && pack.lenses.length !== bench.length) {
|
|
92
96
|
errors.push(`lenses needs exactly one lens per seat (${bench.length} seats, got ${pack.lenses.length})`);
|
|
93
97
|
}
|
package/src/sidecar/budget.js
CHANGED
|
@@ -63,20 +63,54 @@ function checkBudget(legs, opts = {}) {
|
|
|
63
63
|
return { ok, offending, overCeiling, breakdown: { legs: breakdownLegs, totalEstCost, unpricedCount, maxCostPerMtok: cap, maxCost: opts.maxCost || null } };
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
/**
|
|
67
|
-
|
|
66
|
+
/**
|
|
67
|
+
* Human-readable refusal text (also used as the error envelope `hint`).
|
|
68
|
+
* @param {object} result checkBudget's return value
|
|
69
|
+
* @param {{kind:'cli'|'mcp'}} [surface] where the text will be read. Remedies are
|
|
70
|
+
* surface-specific: the MCP tool surface has no --flags, and (since v4.7 PR6's
|
|
71
|
+
* gate hoist) no per-call override at all. Defaults to 'cli' so the two CLI
|
|
72
|
+
* callers stay byte-identical.
|
|
73
|
+
*/
|
|
74
|
+
function formatBudgetError(result, surface = { kind: 'cli' }) {
|
|
68
75
|
const lines = [];
|
|
76
|
+
const isMcp = surface && surface.kind === 'mcp';
|
|
69
77
|
if (result.offending.length > 0) {
|
|
70
78
|
lines.push('Budget gate: model(s) over the per-$/Mtok threshold:');
|
|
71
79
|
for (const o of result.offending) { lines.push(` - ${o.modelInput} (${o.model}): ${o.reason}`); }
|
|
72
80
|
}
|
|
73
81
|
if (result.overCeiling) {
|
|
74
|
-
|
|
82
|
+
// On MCP the ceiling can come from EITHER a pack's `maxCost` option or the
|
|
83
|
+
// config, and the pack wins (mcp-server.js's fwd.maxCost ?? cfg.maxCost).
|
|
84
|
+
// Naming only one of them would send the caller to edit the loser — the very
|
|
85
|
+
// "remedy that cannot work" class this surface split exists to end.
|
|
86
|
+
lines.push(`Budget gate: estimated total $${result.breakdown.totalEstCost.toFixed(4)} exceeds ${isMcp ? 'the effective maxCost' : '--max-cost'} $${result.breakdown.maxCost.toFixed(4)} (estimate, not guaranteed).`);
|
|
75
87
|
}
|
|
76
88
|
if (result.breakdown.unpricedCount > 0) {
|
|
77
89
|
lines.push(`(${result.breakdown.unpricedCount} unpriced leg(s) — direct provider; cost unknown, not included in the estimate.)`);
|
|
78
90
|
}
|
|
79
|
-
|
|
91
|
+
// offending and overCeiling are independent (unrelated inputs — see module
|
|
92
|
+
// header), and either or both may have fired since formatBudgetError is only
|
|
93
|
+
// called when !ok. Only name a remedy that actually clears every branch that
|
|
94
|
+
// fired; when both fired, raising just one lever will not clear the run.
|
|
95
|
+
const hasOffending = result.offending.length > 0;
|
|
96
|
+
const hasCeiling = result.overCeiling;
|
|
97
|
+
if (isMcp) {
|
|
98
|
+
if (hasOffending && hasCeiling) {
|
|
99
|
+
lines.push("Override: raise maxCostPerMtok in the amicus config AND the effective maxCost (the pack's `maxCost` option if this run used a pack, otherwise the config's), or choose a cheaper model — raising just one will not clear this run.");
|
|
100
|
+
} else if (hasOffending) {
|
|
101
|
+
lines.push('Override: raise maxCostPerMtok in the amicus config, or choose a cheaper model.');
|
|
102
|
+
} else {
|
|
103
|
+
lines.push("Override: raise the effective maxCost — the pack's `maxCost` option if this run used a pack, otherwise the config's — or choose a cheaper model.");
|
|
104
|
+
}
|
|
105
|
+
} else {
|
|
106
|
+
if (hasOffending && hasCeiling) {
|
|
107
|
+
lines.push('Override: --no-cost-gate to disable both guards (e.g. an intentional o3 run) — raising just one of maxCostPerMtok or --max-cost will not clear this run.');
|
|
108
|
+
} else if (hasOffending) {
|
|
109
|
+
lines.push('Override: --no-cost-gate to disable both guards (e.g. an intentional o3 run), or raise maxCostPerMtok in config.');
|
|
110
|
+
} else {
|
|
111
|
+
lines.push('Override: --max-cost <$> to raise the ceiling, or --no-cost-gate to disable both guards (e.g. an intentional o3 run).');
|
|
112
|
+
}
|
|
113
|
+
}
|
|
80
114
|
return lines.join('\n');
|
|
81
115
|
}
|
|
82
116
|
|
package/src/sidecar/continue.js
CHANGED
|
@@ -88,7 +88,7 @@ Build on the previous sidecar's findings. The user wants to continue or extend t
|
|
|
88
88
|
|
|
89
89
|
/** Create session metadata for continuation */
|
|
90
90
|
function createContinueSessionMetadata(taskId, project, options, oldTaskId) {
|
|
91
|
-
const { model, briefing, headless, agent, gateway, resolutionVersion } = options;
|
|
91
|
+
const { model, briefing, headless, agent, gateway, resolutionVersion, tag } = options;
|
|
92
92
|
|
|
93
93
|
const sessionDir = SessionPaths.sessionDir(project, taskId);
|
|
94
94
|
fs.mkdirSync(sessionDir, { recursive: true });
|
|
@@ -102,7 +102,11 @@ function createContinueSessionMetadata(taskId, project, options, oldTaskId) {
|
|
|
102
102
|
agent: agent || (headless ? 'build' : 'chat'),
|
|
103
103
|
status: 'running',
|
|
104
104
|
createdAt: new Date().toISOString(),
|
|
105
|
-
continuesFrom: oldTaskId
|
|
105
|
+
continuesFrom: oldTaskId,
|
|
106
|
+
// v4.7.1 Task 7 (D13): absent-not-null, same idiom as start-metadata.js:50
|
|
107
|
+
// — a continuation inherits the parent's tag so a continue chain never
|
|
108
|
+
// scatters into `(unattributed)`.
|
|
109
|
+
...(tag ? { tag } : {}),
|
|
106
110
|
};
|
|
107
111
|
// #61 Task 5.2 (best-effort provenance): only present when THIS continue
|
|
108
112
|
// call freshly routed an explicit --model through the gateway router — the
|
|
@@ -116,26 +120,6 @@ function createContinueSessionMetadata(taskId, project, options, oldTaskId) {
|
|
|
116
120
|
return sessionDir;
|
|
117
121
|
}
|
|
118
122
|
|
|
119
|
-
/**
|
|
120
|
-
* Resolve a reopened session's usage, write it onto metadata, and append one
|
|
121
|
-
* attributed ledger row. Mirrors start.js's finalize (the only sites that
|
|
122
|
-
* dropped usage - BACKLOG.md:280). Best-effort ledger append; never throws.
|
|
123
|
-
* @returns {{usage: object|null}}
|
|
124
|
-
*/
|
|
125
|
-
function finalizeSpendForReopen({ taskId, model, mode, op, result, status, project, metadata }, ctx = {}) {
|
|
126
|
-
const { resolveUsage } = require('../utils/pricing');
|
|
127
|
-
const usage = result && result.usage ? resolveUsage({ model, usageTotals: result.usage }) : null;
|
|
128
|
-
if (usage) {
|
|
129
|
-
metadata.usage = usage; // buildRunResult surfaces metadata.usage into the --json doc for free
|
|
130
|
-
try {
|
|
131
|
-
const { appendSpend } = require('../utils/spend-ledger');
|
|
132
|
-
const gateway = metadata.gateway || (String(model).startsWith('openrouter/') ? 'openrouter' : 'direct');
|
|
133
|
-
appendSpend({ taskId, model, mode, usage, op, status, project, gateway }, ctx);
|
|
134
|
-
} catch { /* best-effort */ }
|
|
135
|
-
}
|
|
136
|
-
return { usage };
|
|
137
|
-
}
|
|
138
|
-
|
|
139
123
|
/**
|
|
140
124
|
* Continue from a previous sidecar session - Spec Reference: §4.4, §8.5
|
|
141
125
|
* @returns {Promise<number>} process exit code
|
|
@@ -196,6 +180,7 @@ async function continueSidecar(options) {
|
|
|
196
180
|
|
|
197
181
|
const sessionDir = createContinueSessionMetadata(newTaskId, project, {
|
|
198
182
|
model, briefing, headless, agent: effectiveAgent, gateway, resolutionVersion,
|
|
183
|
+
tag: oldMetadata.tag, // v4.7.1 Task 7: inherit the parent's tag (absent if the parent had none).
|
|
199
184
|
}, oldTaskId);
|
|
200
185
|
|
|
201
186
|
// Lock the NEW continuation session dir too — not just the previous one — so a
|
|
@@ -270,6 +255,7 @@ async function continueSidecar(options) {
|
|
|
270
255
|
// ledger row (status: statusFromResult, matching start.js — not terminal.status).
|
|
271
256
|
{
|
|
272
257
|
const { statusFromResult } = require('../utils/result-schema');
|
|
258
|
+
const { finalizeSpendForReopen } = require('./reopen-spend');
|
|
273
259
|
const reloaded = JSON.parse(fs.readFileSync(metaPath, 'utf-8'));
|
|
274
260
|
const { usage } = finalizeSpendForReopen({
|
|
275
261
|
taskId: newTaskId, model, mode: headless ? 'headless' : 'interactive',
|
|
@@ -292,6 +278,5 @@ module.exports = {
|
|
|
292
278
|
loadPreviousSession,
|
|
293
279
|
buildContinuationContext,
|
|
294
280
|
createContinueSessionMetadata,
|
|
295
|
-
finalizeSpendForReopen,
|
|
296
281
|
continueSidecar
|
|
297
282
|
};
|
|
@@ -61,8 +61,7 @@ function preflightBudget(okLegs, options) {
|
|
|
61
61
|
message: 'Error: budget gate refused the wave',
|
|
62
62
|
hint: `Budget gate: estimated total $${estimate.toFixed(4)} does not fit the --max-cost `
|
|
63
63
|
+ 'allowance still unclaimed by concurrently launching waves (estimate, not guaranteed).\n'
|
|
64
|
-
+ 'The run continues with the waves that did launch.
|
|
65
|
-
+ 'ceiling, or --no-cost-gate to disable both guards.',
|
|
64
|
+
+ 'The run continues with the waves that did launch.',
|
|
66
65
|
};
|
|
67
66
|
}
|
|
68
67
|
return { ok: true, estimate };
|
|
@@ -18,9 +18,12 @@ const { deriveChain } = require('./fallback-chains');
|
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* Append ONE attributed ledger row for a single attempt (spec 6.2/7.1). At
|
|
21
|
-
* `attempt:0` this
|
|
22
|
-
*
|
|
23
|
-
*
|
|
21
|
+
* `attempt:0` this row's LINKAGE fields (attempt/substitutedFor/retryOfWaveId,
|
|
22
|
+
* all undefined -> omitted for a plain leg) are byte-identical to today's
|
|
23
|
+
* pre-fallback appendSpend row. v4.7 F8 D16 adds `tag` — a NULLABLE dim, not a
|
|
24
|
+
* linkage field, so every row now carries it (null-default when the leg was
|
|
25
|
+
* never tagged) rather than omitting it; the byte-identity claim above applies
|
|
26
|
+
* only to the linkage-field convention, not to the full row shape. A
|
|
24
27
|
* substitution (`attempt` param > 0, the LOOP's attempt index) overrides
|
|
25
28
|
* `row.attempt`/`row.substitutedFor` with the substitution's own values.
|
|
26
29
|
* Best-effort — never throws, never affects the leg. `deps.spendDir` (tests)
|
|
@@ -42,6 +45,7 @@ function recordAttemptSpend({ doc, leg, currentModel, legId, waveId, project, at
|
|
|
42
45
|
councilRunId: leg && leg.councilRunId, councilName: leg && leg.councilName,
|
|
43
46
|
project, attempt: leg && leg.attempt, substitutedFor: leg && leg.substitutedFor,
|
|
44
47
|
retryOfWaveId: leg && leg.retryOfWaveId,
|
|
48
|
+
tag: (leg && leg.tag) || null,
|
|
45
49
|
};
|
|
46
50
|
if (attempt > 0) { row.attempt = attempt; row.substitutedFor = originalModel; }
|
|
47
51
|
appendSpend(row, deps.spendDir ? { dir: deps.spendDir } : undefined);
|
|
@@ -73,7 +73,10 @@ function buildRetryPlan(origWaveId, project, { models } = {}) {
|
|
|
73
73
|
} catch { /* legacy leg — fall back to briefing.md at launch time */ }
|
|
74
74
|
eligible.push({ legId, model, systemPrompt, userMessage, hadSavedContext });
|
|
75
75
|
}
|
|
76
|
-
|
|
76
|
+
// waveMeta has been read since :51 but was discarded; retryFailedWave needs
|
|
77
|
+
// the tag BEFORE it builds fanoutOpts, and its own origMeta re-read at :176
|
|
78
|
+
// runs after runFanoutImpl at :153 — too late to influence the launch.
|
|
79
|
+
return { eligible, tag: waveMeta.tag };
|
|
77
80
|
}
|
|
78
81
|
|
|
79
82
|
/**
|
|
@@ -121,7 +124,7 @@ async function retryFailedWave(origWaveId, project, opts = {}) {
|
|
|
121
124
|
// --json caller's stdout stays machine-parseable either way.
|
|
122
125
|
const noopDoc = {
|
|
123
126
|
...buildWaveResult({ waveId: origWaveId, legs: [], status: 'complete' }),
|
|
124
|
-
retryOf: origWaveId, effective: [], note: 'no failed legs',
|
|
127
|
+
retryOf: origWaveId, effective: [], note: 'no failed legs', tag: plan.tag,
|
|
125
128
|
};
|
|
126
129
|
process.stdout.write(JSON.stringify(noopDoc, null, 2) + '\n');
|
|
127
130
|
} else {
|
|
@@ -148,7 +151,16 @@ async function retryFailedWave(origWaveId, project, opts = {}) {
|
|
|
148
151
|
// expects (parseModelsList/validateFanoutModels) — an array silently fails
|
|
149
152
|
// every leg pre-flight (BAD_ARGS), matching run-launch.js:41's precedent.
|
|
150
153
|
// Strip our own injection key so it is never forwarded.
|
|
151
|
-
const fanoutOpts = {
|
|
154
|
+
const fanoutOpts = {
|
|
155
|
+
...opts, models: models.join(','), prompt: briefing, project, waveId: newWaveId, retryContexts, retryOfWaveId: origWaveId,
|
|
156
|
+
// v4.7.1 Task 8: inherit the ORIGINAL wave's tag — sourced from disk
|
|
157
|
+
// (plan.tag, off waveMeta) never from opts.tag/args.tag. This is placed
|
|
158
|
+
// AFTER the `...opts` spread so it wins even if a caller's opts somehow
|
|
159
|
+
// carried a tag (the CLI itself already rejects --tag + --retry-failed
|
|
160
|
+
// upstream in cli-handlers-fanout.js). Absent-not-null idiom, matching
|
|
161
|
+
// fanout.js:152's `...(options.pack ? {...} : {})`.
|
|
162
|
+
...(plan.tag ? { tag: plan.tag } : {}),
|
|
163
|
+
};
|
|
152
164
|
delete fanoutOpts.runFanout;
|
|
153
165
|
const { wave, exitCode } = await runFanoutImpl(fanoutOpts);
|
|
154
166
|
|