amicus 4.6.3 → 4.7.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/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +283 -0
- package/README.md +19 -7
- package/bin/amicus.js +31 -0
- package/docs/ROADMAP.md +143 -36
- package/docs/configuration.md +51 -3
- package/docs/council.md +63 -10
- package/docs/doc-system.md +8 -7
- package/docs/schemas.md +10 -1
- package/docs/troubleshooting.md +27 -1
- package/docs/usage.md +59 -13
- 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 +5 -2
- 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/skills/second-opinion/MANUAL-ORCHESTRATION.md +12 -0
- package/skills/second-opinion/MODEL-NOTES.md +5 -4
- package/skills/sidecar/SKILL.md +7 -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-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 +9 -2
- package/src/council/run-retry.js +4 -1
- 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/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/fanout-budget.js +1 -2
- package/src/sidecar/fanout-leg-fallback.js +7 -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/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/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
|
|
|
@@ -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);
|
|
@@ -72,4 +72,16 @@ async function finishWave({ wave, waveDir, waveId, project, exitCode, completedA
|
|
|
72
72
|
return { wave, exitCode };
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
|
|
75
|
+
/**
|
|
76
|
+
* v4.3 §7.2 (moved here v4.7 PR3 Task 1): stamp council attribution onto every
|
|
77
|
+
* leg — fanout-leg's appendSpend reads it; no-op for every non-council caller.
|
|
78
|
+
* v4.7 F8 (Task 7) adds tag stamping in the same pass.
|
|
79
|
+
*/
|
|
80
|
+
function stampLegAttribution(legs, options) {
|
|
81
|
+
if (options.councilRunId || options.councilName) {
|
|
82
|
+
legs.forEach(l => { l.councilRunId = options.councilRunId; l.councilName = options.councilName; });
|
|
83
|
+
}
|
|
84
|
+
if (options.tag) { legs.forEach(l => { l.tag = options.tag; }); }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
module.exports = { writeWaveMetadata, writeWaveDoc, finishWave, stampLegAttribution };
|
package/src/sidecar/fanout.js
CHANGED
|
@@ -19,7 +19,7 @@ const { ERROR_CODES } = require('../utils/error-doc');
|
|
|
19
19
|
// Wave-document persistence lives in ./fanout-wave-io (size-gate split, v4.4.1
|
|
20
20
|
// Task 0.5). writeWaveMetadata is re-exported below — fanout-retry.js and the
|
|
21
21
|
// fanout tests import it from here.
|
|
22
|
-
const { writeWaveMetadata, writeWaveDoc, finishWave } = require('./fanout-wave-io');
|
|
22
|
+
const { writeWaveMetadata, writeWaveDoc, finishWave, stampLegAttribution } = require('./fanout-wave-io');
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
25
|
* Derive leg task IDs: <waveId>-1 .. <waveId>-N (matches TASK_ID_PATTERN).
|
|
@@ -86,7 +86,11 @@ async function runFanout(options) {
|
|
|
86
86
|
// `reason` in metadata.json, no wave.json, and stage1 recorded 'complete'.
|
|
87
87
|
// waveDir is optional (only the post-creation caller has one).
|
|
88
88
|
const errorWave = (waveId, message, waveDir) => {
|
|
89
|
-
|
|
89
|
+
// v4.7 F8 (D13, T3 review): tag: options.tag || metaTag, TDZ-safe (sole call site runs after `const metaTag` below).
|
|
90
|
+
// v4.7 PR3 rider: pack got the same `|| metaPack` inherit the other two
|
|
91
|
+
// buildWaveResult sites have — it was the lone holdout, so an MCP-spawned
|
|
92
|
+
// wave whose server died dropped its pre-seeded pack while keeping its tag.
|
|
93
|
+
const doc = buildWaveResult({ waveId: waveId || null, legs: [], promptMeta: options.promptMeta || null, pack: options.pack || metaPack, tag: options.tag || metaTag, createdAt, completedAt: new Date().toISOString(), status: 'error' });
|
|
90
94
|
doc.error = message;
|
|
91
95
|
doc.reason = message; // classifier alias, same as fanout-leg.js's run docs
|
|
92
96
|
// best-effort: an unwritable wave dir must not mask the real error
|
|
@@ -119,11 +123,7 @@ async function runFanout(options) {
|
|
|
119
123
|
});
|
|
120
124
|
if (validated.error) { return failPre(validated.code || 'BAD_ARGS', validated.error); }
|
|
121
125
|
const legs = validated.legs;
|
|
122
|
-
|
|
123
|
-
// existing appendSpend reads it); no-op for every non-council caller.
|
|
124
|
-
if (options.councilRunId || options.councilName) {
|
|
125
|
-
legs.forEach(l => { l.councilRunId = options.councilRunId; l.councilName = options.councilName; });
|
|
126
|
-
}
|
|
126
|
+
stampLegAttribution(legs, options);
|
|
127
127
|
const okLegs = legs.filter(l => l.ok);
|
|
128
128
|
// FIX 2 (#61 whole-branch review): a leg's migration notice has no CLI
|
|
129
129
|
// stderr to land on — surface it on the wave doc instead, deduped in case
|
|
@@ -149,6 +149,7 @@ async function runFanout(options) {
|
|
|
149
149
|
briefing: String(options.prompt).slice(0, 200),
|
|
150
150
|
promptMeta: options.promptMeta || null,
|
|
151
151
|
...(options.pack ? { pack: options.pack } : {}), // v4.5 Task 13: absent-not-null.
|
|
152
|
+
...(options.tag ? { tag: options.tag } : {}), // v4.7 F8 (D13): absent-not-null, same idiom as pack above.
|
|
152
153
|
pid: process.pid, project, createdAt,
|
|
153
154
|
});
|
|
154
155
|
// v4.5 final-review F2: an MCP-spawned child never gets --pack (single-
|
|
@@ -161,6 +162,7 @@ async function runFanout(options) {
|
|
|
161
162
|
// result-schema-rebuild.js:93, which reads meta.pack off a metadata.json
|
|
162
163
|
// it loaded for an unrelated reason).
|
|
163
164
|
const metaPack = waveMeta.pack;
|
|
165
|
+
const metaTag = waveMeta.tag; // v4.7 F8 (D13): same pre-seed inherit mechanism as metaPack above.
|
|
164
166
|
emitWaveStarted(waveDir, waveId, legs.map(l => (l.ok ? l.model : l.modelInput)), legIds, follow);
|
|
165
167
|
|
|
166
168
|
// 2b. All legs failed to route (#61 perf): no leg will ever touch the
|
|
@@ -171,7 +173,7 @@ async function runFanout(options) {
|
|
|
171
173
|
const legDocs = legs.map((leg, i) => buildRoutingFailureLeg({ leg, legId: legIds[i], waveId, quiet: options.quiet }));
|
|
172
174
|
const completedAt = new Date().toISOString();
|
|
173
175
|
const wave = buildWaveResult({
|
|
174
|
-
waveId, legs: legDocs, promptMeta: options.promptMeta || null, pack: options.pack || metaPack, createdAt, completedAt, notices,
|
|
176
|
+
waveId, legs: legDocs, promptMeta: options.promptMeta || null, pack: options.pack || metaPack, tag: options.tag || metaTag, createdAt, completedAt, notices,
|
|
175
177
|
});
|
|
176
178
|
return finishWave({ wave, waveDir, waveId, project, completedAt, follow, emit,
|
|
177
179
|
exitCode: waveExitCode(wave.status),
|
|
@@ -282,7 +284,7 @@ async function runFanout(options) {
|
|
|
282
284
|
const completedAt = new Date().toISOString();
|
|
283
285
|
const signalled = waveAbort.signal();
|
|
284
286
|
const wave = buildWaveResult({
|
|
285
|
-
waveId, legs: legDocs, promptMeta: options.promptMeta || null, pack: options.pack || metaPack, createdAt, completedAt,
|
|
287
|
+
waveId, legs: legDocs, promptMeta: options.promptMeta || null, pack: options.pack || metaPack, tag: options.tag || metaTag, createdAt, completedAt,
|
|
286
288
|
status: signalled ? 'aborted' : null, notices,
|
|
287
289
|
});
|
|
288
290
|
const exitCode = signalled
|