amicus 4.4.1 → 4.5.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 +154 -0
- package/README.md +15 -2
- package/bin/amicus.js +10 -0
- package/docs/ROADMAP.md +38 -14
- package/docs/configuration.md +24 -0
- package/docs/council.md +62 -0
- package/docs/schemas.md +1 -0
- package/docs/usage.md +151 -1
- package/electron/workspace-ui/workspace-app.js +39 -17
- package/electron/workspace-ui/workspace-panels.js +76 -18
- package/electron/workspace-ui/workspace-render.js +10 -0
- package/package.json +1 -1
- package/schemas/council-run-live.schema.json +1 -1
- package/schemas/council-run.schema.json +14 -0
- package/schemas/error.schema.json +1 -1
- package/schemas/event.schema.json +1 -1
- package/schemas/pack.schema.json +30 -0
- package/schemas/progress.schema.json +1 -1
- package/schemas/run-live.schema.json +1 -1
- package/schemas/run.schema.json +2 -1
- package/schemas/wave-live.schema.json +1 -1
- package/schemas/wave.schema.json +2 -1
- package/skills/second-opinion/SKILL.md +5 -0
- package/src/cli-handlers-council-run.js +51 -8
- package/src/cli-handlers-doctor.js +10 -0
- package/src/cli-handlers-pack.js +238 -0
- package/src/cli-handlers-run.js +36 -8
- package/src/cli-handlers-template.js +53 -0
- package/src/cli.js +64 -3
- package/src/council/findings.js +4 -41
- package/src/council/presets-cli.js +23 -11
- package/src/council/run-stages.js +12 -9
- package/src/council/run-state.js +17 -0
- package/src/council/run.js +1 -1
- package/src/headless.js +18 -14
- package/src/mcp-council-run.js +110 -4
- package/src/mcp-server.js +203 -7
- package/src/mcp-tools.js +15 -5
- package/src/pack/pack-cli.js +38 -0
- package/src/pack/pack-forward.js +96 -0
- package/src/pack/pack-resolve.js +297 -0
- package/src/pack/pack-store.js +130 -0
- package/src/pack/pack-validate.js +113 -0
- package/src/sidecar/electron-state.js +61 -0
- package/src/sidecar/fanout.js +21 -4
- package/src/sidecar/progress.js +34 -0
- package/src/sidecar/start.js +5 -4
- package/src/sidecar/workspace-auto-open.js +83 -0
- package/src/sidecar/workspace-window.js +46 -1
- package/src/template/apply.js +88 -0
- package/src/template/render.js +86 -0
- package/src/template/store.js +106 -0
- package/src/utils/config.js +65 -25
- package/src/utils/doctor-electron-mcp-check.js +150 -0
- package/src/utils/error-doc.js +5 -0
- package/src/utils/result-schema-rebuild.js +1 -0
- package/src/utils/result-schema.js +8 -2
- package/src/workspace/artifact-guard.js +44 -6
- package/src/workspace/run-detail.js +6 -0
package/src/mcp-server.js
CHANGED
|
@@ -225,6 +225,27 @@ function computeNextPoll() {
|
|
|
225
225
|
const HEADLESS_START_REMINDER = '<system-reminder>Preferred: call amicus_wait with this task ID instead — one blocking call replaces the sleep+status loop; re-call it while it returns timedOut: true. Fallback (no amicus_wait tool available): before calling amicus_status, you MUST run `sleep 25` in your shell first. This enforces the polling interval and prevents token waste. Do other useful work while waiting, or run `sleep 25` to block until the next poll window.</system-reminder>';
|
|
226
226
|
const HEADLESS_STATUS_REMINDER = '<system-reminder>Preferred: call amicus_wait with this task ID instead — one blocking call replaces the sleep+status loop; re-call it while it returns timedOut: true. Fallback (no amicus_wait tool available): this session is still running. Before calling amicus_status again, you MUST run `sleep 25` in your shell first. Each premature poll wastes context tokens for zero benefit. Run `sleep 25` now, then check again.</system-reminder>';
|
|
227
227
|
|
|
228
|
+
/**
|
|
229
|
+
* v4.5 Task 15 (B7/F5): map amicus_fanout / amicus_start's MCP input keys to
|
|
230
|
+
* the CLI arg-key names applyPackToArgs's knob tables use (pack-resolve.js),
|
|
231
|
+
* so applyPackToMcpInput can reuse those tables unchanged — see
|
|
232
|
+
* src/mcp-council-run.js's COUNCIL_PACK_PARAM_MAP for the sibling map and its
|
|
233
|
+
* fuller docblock. `includeContext` is the one inverted-polarity knob: the
|
|
234
|
+
* pack/CLI side is `no-context` (true = drop context), the MCP side is
|
|
235
|
+
* `includeContext` (true = keep context, default true).
|
|
236
|
+
*/
|
|
237
|
+
const FANOUT_PACK_PARAM_MAP = {
|
|
238
|
+
models: 'models', council: 'council', gateway: 'gateway', agent: 'agent', thinking: 'thinking',
|
|
239
|
+
timeout: 'timeout', summaryLength: 'summary-length',
|
|
240
|
+
includeContext: { argKey: 'no-context', invert: true },
|
|
241
|
+
};
|
|
242
|
+
const SOLO_PACK_PARAM_MAP = {
|
|
243
|
+
model: 'model', gateway: 'gateway', agent: 'agent', noUi: 'no-ui', thinking: 'thinking',
|
|
244
|
+
timeout: 'timeout', contextTurns: 'context-turns', contextMaxTokens: 'context-max-tokens',
|
|
245
|
+
summaryLength: 'summary-length',
|
|
246
|
+
includeContext: { argKey: 'no-context', invert: true },
|
|
247
|
+
};
|
|
248
|
+
|
|
228
249
|
/** Spawn an Amicus CLI process (fire-and-forget) */
|
|
229
250
|
function spawnSidecarProcess(args, sessionDir) {
|
|
230
251
|
const sidecarBin = path.join(__dirname, '..', 'bin', 'amicus.js');
|
|
@@ -250,9 +271,49 @@ function spawnSidecarProcess(args, sessionDir) {
|
|
|
250
271
|
return child;
|
|
251
272
|
}
|
|
252
273
|
|
|
274
|
+
/** v4.5 Wave-1 review fix (I1/I2): validates/dry-runs a pack-forwarded
|
|
275
|
+
* maxCost/template (see src/pack/pack-forward.js) — a no-op when the pack
|
|
276
|
+
* forwarded neither knob. Shared by amicus_fanout and amicus_start (both
|
|
277
|
+
* build their OWN error response below; the two tools use different shapes). */
|
|
278
|
+
function checkPackForward(packForward, packRecord, prompt, project) {
|
|
279
|
+
if (packForward.maxCost === undefined && packForward.template === undefined) {
|
|
280
|
+
return { notices: [] };
|
|
281
|
+
}
|
|
282
|
+
return require('./pack/pack-forward').prepareForward({
|
|
283
|
+
forward: packForward, packRef: packRecord.name, prompt, project,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
253
287
|
/** Tool handler implementations */
|
|
254
288
|
const handlers = {
|
|
255
289
|
async amicus_start(input, project, mcpServer) {
|
|
290
|
+
// v4.5 Task 15 (B7/F5): resolve `pack` IN-PROCESS before any validation —
|
|
291
|
+
// mirrors handleStart's own ordering (cli-handlers-run.js resolves --pack
|
|
292
|
+
// before validateStartArgs), so a pack-filled value gets the SAME
|
|
293
|
+
// validation pass a typed one does. Single-resolution rule: never spawn
|
|
294
|
+
// --pack — this is the only place the pack is resolved.
|
|
295
|
+
let packRecord = null;
|
|
296
|
+
const packNotices = [];
|
|
297
|
+
// v4.5 HOLD-gate decision 1: pack-filled maxCost/template have no MCP
|
|
298
|
+
// schema param of their own (see SOLO_PACK_PARAM_MAP above) but must still
|
|
299
|
+
// apply for CLI parity, on BOTH of this handler's paths (spawn-fallback
|
|
300
|
+
// below; the in-process shared-server branch further down) — a pack must
|
|
301
|
+
// not behave differently depending on which path fired.
|
|
302
|
+
let packForward = {};
|
|
303
|
+
if (input.pack !== undefined) {
|
|
304
|
+
const { applyPackToMcpInput } = require('./pack/pack-resolve');
|
|
305
|
+
const pr = applyPackToMcpInput({
|
|
306
|
+
packRef: input.pack, expectedKind: 'solo', input, paramMap: SOLO_PACK_PARAM_MAP,
|
|
307
|
+
});
|
|
308
|
+
if (pr.error) {
|
|
309
|
+
const { buildErrorDoc } = require('./utils/error-doc');
|
|
310
|
+
return { isError: true, content: [{ type: 'text', text: JSON.stringify(buildErrorDoc(pr.error)) }] };
|
|
311
|
+
}
|
|
312
|
+
packRecord = pr.packRecord;
|
|
313
|
+
packNotices.push(...pr.notices);
|
|
314
|
+
packForward = pr.forward;
|
|
315
|
+
}
|
|
316
|
+
|
|
256
317
|
// Validate non-model inputs (prompt/timeout/agent) before any session creation.
|
|
257
318
|
const { validateStartInputs } = require('./utils/input-validators');
|
|
258
319
|
const validation = validateStartInputs(input);
|
|
@@ -307,6 +368,15 @@ const handlers = {
|
|
|
307
368
|
const resolvedModel = routeResult.executableId;
|
|
308
369
|
|
|
309
370
|
const cwd = project || getProjectDir(input.project);
|
|
371
|
+
|
|
372
|
+
// v4.5 Wave-1 review fix (I1/I2): validated ONCE, before EITHER downstream
|
|
373
|
+
// path does anything spend-adjacent — parity holds BY CONSTRUCTION.
|
|
374
|
+
const fwd = checkPackForward(packForward, packRecord, input.prompt, cwd);
|
|
375
|
+
if (fwd.error) {
|
|
376
|
+
const { buildErrorDoc } = require('./utils/error-doc');
|
|
377
|
+
return { isError: true, content: [{ type: 'text', text: JSON.stringify(buildErrorDoc(fwd.error)) }] };
|
|
378
|
+
}
|
|
379
|
+
|
|
310
380
|
const { generateTaskId } = require('./sidecar/start');
|
|
311
381
|
const taskId = generateTaskId();
|
|
312
382
|
|
|
@@ -325,8 +395,14 @@ const handlers = {
|
|
|
325
395
|
// resolvedModel is always defined here — a routing failure already
|
|
326
396
|
// returned above — and is the router's executableId, not the raw alias.
|
|
327
397
|
args.push('--model', resolvedModel);
|
|
328
|
-
|
|
329
|
-
|
|
398
|
+
// v4.5 Task 15 fix wave 2 (Finding 1): agent/noUi no longer carry a Zod
|
|
399
|
+
// .default() (see mcp-tools.js) — an omitted key is now genuinely absent
|
|
400
|
+
// here, so a pack can fill it. Preserve today's effective default ('Chat')
|
|
401
|
+
// at this READ site instead: the resolved value is identical whether the
|
|
402
|
+
// key was never set, or a caller/pack explicitly wrote 'Chat'.
|
|
403
|
+
const agentInput = input.agent || 'Chat';
|
|
404
|
+
const agent = (input.noUi && agentInput.toLowerCase() === 'chat')
|
|
405
|
+
? 'build' : agentInput;
|
|
330
406
|
if (agent) { args.push('--agent', agent); }
|
|
331
407
|
if (input.noUi) { args.push('--no-ui'); }
|
|
332
408
|
if (input.thinking) { args.push('--thinking', input.thinking); }
|
|
@@ -339,12 +415,50 @@ const handlers = {
|
|
|
339
415
|
if (input.coworkProcess) { args.push('--cowork-process', input.coworkProcess); }
|
|
340
416
|
if (input.parentSession) { args.push('--session-id', input.parentSession); }
|
|
341
417
|
if (input.windowPosition) { args.push('--position', input.windowPosition); }
|
|
418
|
+
// v4.5 HOLD-gate decision 1: forward a pack-filled maxCost/template as
|
|
419
|
+
// plain CLI flags on the spawn-fallback path — neither has an MCP schema
|
|
420
|
+
// param of its own, but CLI parity requires them to apply anyway. Never
|
|
421
|
+
// pushed when the pack didn't fill them. The in-process shared-server
|
|
422
|
+
// branch below applies the SAME two knobs via its own mechanism (budget
|
|
423
|
+
// gate / template pre-render) for parity between the two paths.
|
|
424
|
+
if (packForward.maxCost !== undefined) { args.push('--max-cost', String(packForward.maxCost)); }
|
|
425
|
+
if (packForward.template !== undefined) { args.push('--template', packForward.template); }
|
|
342
426
|
args.push('--cwd', cwd);
|
|
343
427
|
|
|
344
428
|
if (sharedServer.enabled && input.noUi) {
|
|
345
429
|
// Shared server path: headless only, delegates to runHeadless()
|
|
346
430
|
let sessionId;
|
|
347
431
|
try {
|
|
432
|
+
// v4.5 decision 1 + Wave-1 fix (I1/I2): the template was already
|
|
433
|
+
// rendered by the shared prepareForward call above — reuse its text
|
|
434
|
+
// instead of rendering again. Notices stay LOCAL (not packNotices):
|
|
435
|
+
// a fall-through to the spawn-fallback catch must never leak one.
|
|
436
|
+
const inProcessNotices = [...fwd.notices];
|
|
437
|
+
const renderedPrompt = fwd.renderedPrompt !== undefined ? fwd.renderedPrompt : input.prompt;
|
|
438
|
+
if (packForward.maxCost !== undefined) {
|
|
439
|
+
// fwd.maxCost is already validated (I2); the ceiling gate itself
|
|
440
|
+
// stays here (needs resolvedModel/pricing, unavailable to pack-forward.js).
|
|
441
|
+
const { lookupPricing } = require('./utils/pricing');
|
|
442
|
+
const { checkBudget, formatBudgetError } = require('./sidecar/budget');
|
|
443
|
+
const { loadConfig } = require('./utils/config');
|
|
444
|
+
const cfg = loadConfig() || {};
|
|
445
|
+
const soloLeg = { modelInput: input.model || resolvedModel, model: resolvedModel, pricing: lookupPricing(resolvedModel) };
|
|
446
|
+
const budget = checkBudget([soloLeg], {
|
|
447
|
+
maxCostPerMtok: cfg.maxCostPerMtok, maxCost: fwd.maxCost,
|
|
448
|
+
promptChars: (renderedPrompt && renderedPrompt.length) || 0,
|
|
449
|
+
});
|
|
450
|
+
if (!budget.ok) {
|
|
451
|
+
const { buildErrorDoc, ERROR_CODES } = require('./utils/error-doc');
|
|
452
|
+
return {
|
|
453
|
+
isError: true,
|
|
454
|
+
content: [{ type: 'text', text: JSON.stringify(buildErrorDoc({
|
|
455
|
+
code: ERROR_CODES.BUDGET_EXCEEDED, message: 'Error: budget gate refused the run',
|
|
456
|
+
hint: formatBudgetError(budget),
|
|
457
|
+
})) }],
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
348
462
|
const { server, client } = await sharedServer.ensureServer();
|
|
349
463
|
const { createSession } = require('./opencode-client');
|
|
350
464
|
const { buildContext } = require('./sidecar/context-builder');
|
|
@@ -380,7 +494,12 @@ const handlers = {
|
|
|
380
494
|
// so without them status/list/read show a briefing-less, mode-less run.
|
|
381
495
|
mode: 'headless',
|
|
382
496
|
agent: agent || 'build',
|
|
383
|
-
|
|
497
|
+
// v4.5 HOLD-gate decision 1: the RENDERED prompt (byte-identical to
|
|
498
|
+
// input.prompt when no pack template applied) — parity with the CLI,
|
|
499
|
+
// whose briefing.md on disk is always the rendered text (spec §4).
|
|
500
|
+
briefing: renderedPrompt,
|
|
501
|
+
// v4.5 Task 15: additive-only — absent (not null) without a pack.
|
|
502
|
+
...(packRecord ? { pack: packRecord } : {}),
|
|
384
503
|
}, null, 2), { mode: 0o600 });
|
|
385
504
|
|
|
386
505
|
// Build context from parent conversation (unless --no-context)
|
|
@@ -402,9 +521,13 @@ const handlers = {
|
|
|
402
521
|
// 15b.3: one nonce per run, generated before prompt construction.
|
|
403
522
|
const foldNonce = generateFoldNonce();
|
|
404
523
|
|
|
405
|
-
// Build prompts (same as CLI path in start.js)
|
|
524
|
+
// Build prompts (same as CLI path in start.js). renderedPrompt is the
|
|
525
|
+
// pack-template-rendered text when packForward.template was set above
|
|
526
|
+
// (byte-identical to input.prompt otherwise) — this is the ONE place
|
|
527
|
+
// the built prompt reaches the model, so rendering has no effect
|
|
528
|
+
// unless it is threaded in here.
|
|
406
529
|
const { system: systemPrompt, userMessage } = buildPrompts(
|
|
407
|
-
|
|
530
|
+
renderedPrompt, context, cwd, true, agent, input.summaryLength, undefined, foldNonce
|
|
408
531
|
);
|
|
409
532
|
|
|
410
533
|
// Register session with idle eviction
|
|
@@ -505,6 +628,14 @@ const handlers = {
|
|
|
505
628
|
// shared-server path (no CLI stderr exists for an MCP caller).
|
|
506
629
|
const sharedServerContent = [{ type: 'text', text: body }];
|
|
507
630
|
if (routeResult.notice) { sharedServerContent.push({ type: 'text', text: routeResult.notice }); }
|
|
631
|
+
// v4.5 Task 15: pack notices (e.g. a bench-override) are non-fatal —
|
|
632
|
+
// surfaced as extra content blocks, same precedent as routeResult.notice above.
|
|
633
|
+
for (const n of packNotices) { sharedServerContent.push({ type: 'text', text: n }); }
|
|
634
|
+
// v4.5 HOLD-gate decision 1: template-render notices (e.g. an unused
|
|
635
|
+
// --var-equivalent), merged only now that the in-process path has
|
|
636
|
+
// actually succeeded — see the try block's opening comment for why
|
|
637
|
+
// these are not in the shared packNotices array.
|
|
638
|
+
for (const n of inProcessNotices) { sharedServerContent.push({ type: 'text', text: n }); }
|
|
508
639
|
sharedServerContent.push({ type: 'text', text: HEADLESS_START_REMINDER });
|
|
509
640
|
return { content: sharedServerContent };
|
|
510
641
|
} catch (err) {
|
|
@@ -542,6 +673,8 @@ const handlers = {
|
|
|
542
673
|
// CLI child's createSessionMetadata overwrite (or if it crashes first).
|
|
543
674
|
mode: input.noUi ? 'headless' : 'interactive',
|
|
544
675
|
briefing: input.prompt,
|
|
676
|
+
// v4.5 Task 15: additive-only — absent (not null) without a pack.
|
|
677
|
+
...(packRecord ? { pack: packRecord } : {}),
|
|
545
678
|
}, null, 2), { mode: 0o600 });
|
|
546
679
|
}
|
|
547
680
|
}
|
|
@@ -560,6 +693,7 @@ const handlers = {
|
|
|
560
693
|
// resolved the route in-process above, so surface its notice here.
|
|
561
694
|
const spawnContent = [{ type: 'text', text: body }];
|
|
562
695
|
if (routeResult.notice) { spawnContent.push({ type: 'text', text: routeResult.notice }); }
|
|
696
|
+
for (const n of packNotices) { spawnContent.push({ type: 'text', text: n }); }
|
|
563
697
|
if (isHeadless) {
|
|
564
698
|
spawnContent.push({ type: 'text', text: HEADLESS_START_REMINDER });
|
|
565
699
|
return { content: spawnContent };
|
|
@@ -1068,6 +1202,41 @@ const handlers = {
|
|
|
1068
1202
|
const { generateTaskId } = require('./sidecar/start');
|
|
1069
1203
|
const { deriveLegIds, DEFAULT_MAX_LEGS } = require('./sidecar/fanout');
|
|
1070
1204
|
|
|
1205
|
+
// v4.5 Task 15 (B7/F5): resolve `pack` IN-PROCESS before the models/council
|
|
1206
|
+
// validation below, same single-resolution rule as amicus_start/
|
|
1207
|
+
// amicus_council_run — never spawn --pack; this is the only place the
|
|
1208
|
+
// pack is resolved.
|
|
1209
|
+
let packRecord = null;
|
|
1210
|
+
const packNotices = [];
|
|
1211
|
+
// v4.5 HOLD-gate decision 1: pack-filled maxCost/template have no MCP
|
|
1212
|
+
// schema param of their own (see FANOUT_PACK_PARAM_MAP above) but must
|
|
1213
|
+
// still apply for CLI parity — applyPackToMcpInput hands them back here
|
|
1214
|
+
// instead of turning them into an ignore-notice; this handler forwards
|
|
1215
|
+
// them to the spawned CLI child's argv below.
|
|
1216
|
+
let packForward = {};
|
|
1217
|
+
if (input.pack !== undefined) {
|
|
1218
|
+
const { applyPackToMcpInput } = require('./pack/pack-resolve');
|
|
1219
|
+
const pr = applyPackToMcpInput({
|
|
1220
|
+
packRef: input.pack, expectedKind: 'fanout', input, paramMap: FANOUT_PACK_PARAM_MAP,
|
|
1221
|
+
});
|
|
1222
|
+
// v4.5 final-review T15-m1: amicus_start's own pack-error branch above
|
|
1223
|
+
// keeps code+hint via buildErrorDoc's JSON envelope; amicus_fanout's
|
|
1224
|
+
// error surface is plain text, so the hint (e.g. PACK_NOT_FOUND's
|
|
1225
|
+
// 'amicus pack list') is appended to the message instead of being
|
|
1226
|
+
// converted into a JSON envelope, which would change this tool's
|
|
1227
|
+
// established response shape.
|
|
1228
|
+
if (pr.error) { return textResult(pr.error.message + (pr.error.hint ? `\n${pr.error.hint}` : ''), true); }
|
|
1229
|
+
packRecord = pr.packRecord;
|
|
1230
|
+
packNotices.push(...pr.notices);
|
|
1231
|
+
packForward = pr.forward;
|
|
1232
|
+
}
|
|
1233
|
+
// v4.5 Wave-1 review fix (I1/I2): validated HERE, before any wave dir /
|
|
1234
|
+
// metadata write — never blind. Same posture as the pack-error branch
|
|
1235
|
+
// above (plain text + appended hint; this handler is born-fenced, not JSON).
|
|
1236
|
+
const fwd = checkPackForward(packForward, packRecord, input.prompt, cwd);
|
|
1237
|
+
if (fwd.error) { return textResult(fwd.error.message + (fwd.error.hint ? `\n${fwd.error.hint}` : ''), true); }
|
|
1238
|
+
packNotices.push(...fwd.notices);
|
|
1239
|
+
|
|
1071
1240
|
// Resolve a single effective models list (council OR models), validated
|
|
1072
1241
|
// BEFORE any wave dir / metadata is written so a bad request never strands
|
|
1073
1242
|
// a pid-less 'running' orphan wave.
|
|
@@ -1110,6 +1279,8 @@ const handlers = {
|
|
|
1110
1279
|
writeFileAtomic(path.join(waveDir, 'metadata.json'), JSON.stringify({
|
|
1111
1280
|
taskId: waveId, type: 'wave', status: 'running', legs: legIds,
|
|
1112
1281
|
models: effectiveModels, headless: true, createdAt: new Date().toISOString(),
|
|
1282
|
+
// v4.5 Task 15: additive-only — absent (not null) without a pack.
|
|
1283
|
+
...(packRecord ? { pack: packRecord } : {}),
|
|
1113
1284
|
}, null, 2), { mode: 0o600 });
|
|
1114
1285
|
// #40: index the wave AND each leg so status/read of any leg resolves the
|
|
1115
1286
|
// project even when the default later defaults to a different one.
|
|
@@ -1138,8 +1309,17 @@ const handlers = {
|
|
|
1138
1309
|
// so context-inheriting fanout launched from Cowork resolves the right parent.
|
|
1139
1310
|
if (input.coworkProcess) { args.push('--cowork-process', input.coworkProcess); }
|
|
1140
1311
|
if (input.parentSession) { args.push('--session-id', input.parentSession); }
|
|
1312
|
+
// v4.5 HOLD-gate decision 1: forward a pack-filled maxCost/template as
|
|
1313
|
+
// plain CLI flags — neither has an MCP schema param of its own, but CLI
|
|
1314
|
+
// parity requires them to apply anyway (a shared pack's spend cap and
|
|
1315
|
+
// briefing template must not silently vanish over MCP). Never pushed when
|
|
1316
|
+
// the pack didn't fill them; single-resolution rule unaffected (--pack
|
|
1317
|
+
// itself is never forwarded, only the two knobs it resolved to).
|
|
1318
|
+
if (packForward.maxCost !== undefined) { args.push('--max-cost', String(packForward.maxCost)); }
|
|
1319
|
+
if (packForward.template !== undefined) { args.push('--template', packForward.template); }
|
|
1141
1320
|
|
|
1142
|
-
|
|
1321
|
+
let child;
|
|
1322
|
+
try { child = spawnSidecarProcess(args, waveDir); } catch (err) {
|
|
1143
1323
|
// Best-effort: never leave a pid-less wave record claiming 'running'
|
|
1144
1324
|
// forever (crash detection only probes records WITH a pid).
|
|
1145
1325
|
try {
|
|
@@ -1149,6 +1329,16 @@ const handlers = {
|
|
|
1149
1329
|
} catch { /* best-effort */ }
|
|
1150
1330
|
return textResult(`Failed to start fan-out: ${err.message}`, true);
|
|
1151
1331
|
}
|
|
1332
|
+
|
|
1333
|
+
// v4.5 Wave-1 fix (I4): merge the child pid into the pre-seeded wave metadata
|
|
1334
|
+
// so crash detection can probe pid-bearing records (absent pid = unreapable phantom).
|
|
1335
|
+
if (child && child.pid) {
|
|
1336
|
+
try {
|
|
1337
|
+
const m = JSON.parse(fs.readFileSync(path.join(waveDir, 'metadata.json'), 'utf-8'));
|
|
1338
|
+
Object.assign(m, { pid: child.pid });
|
|
1339
|
+
writeFileAtomic(path.join(waveDir, 'metadata.json'), JSON.stringify(m, null, 2), { mode: 0o600 });
|
|
1340
|
+
} catch { /* best-effort: metadata already has no pid */ }
|
|
1341
|
+
}
|
|
1152
1342
|
// Task 15 (spec §5.3): the run is now known-launched under waveId — mark
|
|
1153
1343
|
// it for a best-effort terminal notify. runWait's poll loop (mcp-wait.js)
|
|
1154
1344
|
// is the only code that later sees this wave reach terminal state.
|
|
@@ -1160,7 +1350,13 @@ const handlers = {
|
|
|
1160
1350
|
'replaces polling; re-call it while it returns timedOut: true. Fallback: poll amicus_status ' +
|
|
1161
1351
|
'with the waveId. Either way, amicus_read the waveId when complete.',
|
|
1162
1352
|
}));
|
|
1163
|
-
|
|
1353
|
+
const waveContent = [{ type: 'text', text: body }];
|
|
1354
|
+
// v4.5 Task 15: pack notices (e.g. a bench-override) are non-fatal —
|
|
1355
|
+
// surfaced as extra content blocks, same precedent as amicus_start's
|
|
1356
|
+
// routeResult.notice handling above.
|
|
1357
|
+
for (const n of packNotices) { waveContent.push({ type: 'text', text: n }); }
|
|
1358
|
+
waveContent.push({ type: 'text', text: HEADLESS_START_REMINDER });
|
|
1359
|
+
return { content: waveContent };
|
|
1164
1360
|
},
|
|
1165
1361
|
|
|
1166
1362
|
async amicus_council_tally(input) {
|
package/src/mcp-tools.js
CHANGED
|
@@ -71,12 +71,12 @@ function getTools() {
|
|
|
71
71
|
'files of interest, success criteria.'
|
|
72
72
|
),
|
|
73
73
|
agent: z.enum(['Chat', 'Plan', 'Build']).optional()
|
|
74
|
-
.
|
|
74
|
+
.describe(
|
|
75
75
|
'Agent mode. Chat (interactive default; headless runs auto-convert ' +
|
|
76
76
|
'to Build): reads auto, writes ask permission. Plan: read-only ' +
|
|
77
77
|
'analysis. Build: full auto (all operations approved).'
|
|
78
78
|
),
|
|
79
|
-
noUi: z.boolean().optional().
|
|
79
|
+
noUi: z.boolean().optional().describe(
|
|
80
80
|
'Run headless without GUI. Default false (opens Electron window).'
|
|
81
81
|
),
|
|
82
82
|
thinking: z.enum([
|
|
@@ -101,7 +101,7 @@ function getTools() {
|
|
|
101
101
|
'Fold summary verbosity. brief: key findings only. normal (default): full ' +
|
|
102
102
|
'structured output. verbose: maximum detail.'
|
|
103
103
|
),
|
|
104
|
-
includeContext: z.boolean().optional().
|
|
104
|
+
includeContext: z.boolean().optional().describe(
|
|
105
105
|
'Whether to include parent conversation history as context. '
|
|
106
106
|
+ 'Default: true. Set to false when the briefing is self-contained '
|
|
107
107
|
+ 'and does not depend on prior conversation. See amicus_guide for guidance.'
|
|
@@ -120,6 +120,7 @@ function getTools() {
|
|
|
120
120
|
'right (default): flush against the right edge. ' +
|
|
121
121
|
'left: flush against the left edge. center: centered.'
|
|
122
122
|
),
|
|
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).'),
|
|
123
124
|
project: z.string().optional().describe(
|
|
124
125
|
'Optional project directory path. Auto-detected from working directory if omitted.'
|
|
125
126
|
),
|
|
@@ -350,8 +351,9 @@ function getTools() {
|
|
|
350
351
|
summaryLength: z.enum(['brief', 'normal', 'verbose']).optional().describe(
|
|
351
352
|
'Summary verbosity for every leg.'
|
|
352
353
|
),
|
|
353
|
-
includeContext: z.boolean().optional().
|
|
354
|
-
'Include parent conversation context (built once, shared by all legs).
|
|
354
|
+
includeContext: z.boolean().optional().describe(
|
|
355
|
+
'Include parent conversation context (built once, shared by all legs). ' +
|
|
356
|
+
'Default: true. Set false for self-contained briefings.'
|
|
355
357
|
),
|
|
356
358
|
coworkProcess: z.string().optional().describe(
|
|
357
359
|
'Cowork VM process name (e.g., "modest-laughing-goodall"). ' +
|
|
@@ -366,6 +368,7 @@ function getTools() {
|
|
|
366
368
|
'finishes (best-effort; amicus_wait remains the reliable completion mechanism). Exec ' +
|
|
367
369
|
'commands are NOT accepted over MCP.'
|
|
368
370
|
),
|
|
371
|
+
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).'),
|
|
369
372
|
project: z.string().optional().describe(
|
|
370
373
|
'Optional project directory path. Auto-detected from working directory if omitted.'
|
|
371
374
|
),
|
|
@@ -497,6 +500,13 @@ function getTools() {
|
|
|
497
500
|
'finishes (best-effort; amicus_wait remains the reliable completion mechanism). Exec ' +
|
|
498
501
|
'commands are NOT accepted over MCP.'
|
|
499
502
|
),
|
|
503
|
+
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).'),
|
|
504
|
+
ui: z.boolean().optional().describe(
|
|
505
|
+
'Auto-open the Council Workspace window on this run. Default: opens when the client is ' +
|
|
506
|
+
'Claude Code (local), Electron is installed, a display exists, and config workspace.autoOpen is ' +
|
|
507
|
+
'not false. false: never open. true: open even for other clients (still requires Electron + ' +
|
|
508
|
+
'a display; never installs).'
|
|
509
|
+
),
|
|
500
510
|
project: z.string().optional().describe(
|
|
501
511
|
'Optional project directory path. Auto-detected from working directory if omitted.'
|
|
502
512
|
),
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// src/pack/pack-cli.js
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @module pack/pack-cli
|
|
6
|
+
* v4.5 Task 13: shared --pack CLI wiring for handleFanout and handleStart
|
|
7
|
+
* (src/cli-handlers-run.js). Task 12 wired handleCouncilRun's pack block
|
|
8
|
+
* inline; fanout/start need the identical resolve-or-exit shape at TWO call
|
|
9
|
+
* sites in the SAME file, which would push cli-handlers-run.js over the
|
|
10
|
+
* 300-line size gate — extracted here instead. Deliberately excludes
|
|
11
|
+
* council's packSuffix helper: neither fanout nor start has a
|
|
12
|
+
* chair/critic/lenses pre-flight to attribute a failure to (Task-12 reviewer
|
|
13
|
+
* confirmed no analog is needed here).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const { applyPackToArgs } = require('./pack-resolve');
|
|
17
|
+
const { failJson } = require('../utils/error-doc');
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Resolve `args.pack` (when present) into `args`, in place, using
|
|
21
|
+
* cli-handlers-run.js's own exit-on-error style (`process.exit(failJson(...))`)
|
|
22
|
+
* rather than handleCouncilRun's `return failJson(...)`. A no-op — returns
|
|
23
|
+
* null without touching `args` or exiting — when --pack was not passed.
|
|
24
|
+
* @param {object} args - parsed CLI args; explicit-flag keys read from args.__explicit
|
|
25
|
+
* @param {'fanout'|'solo'} expectedKind
|
|
26
|
+
* @param {boolean} useJson
|
|
27
|
+
* @returns {{name: string, version: string, hash: string, source: string}|null}
|
|
28
|
+
*/
|
|
29
|
+
function applyPackOrExit(args, expectedKind, useJson) {
|
|
30
|
+
if (args.pack === undefined) { return null; }
|
|
31
|
+
const explicit = args.__explicit || new Set();
|
|
32
|
+
const pr = applyPackToArgs({ packRef: args.pack, expectedKind, args, explicit, useJson });
|
|
33
|
+
if (pr.error) { process.exit(failJson(useJson, pr.error)); }
|
|
34
|
+
for (const n of pr.notices) { process.stderr.write(n + '\n'); }
|
|
35
|
+
return pr.packRecord;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
module.exports = { applyPackOrExit };
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// src/pack/pack-forward.js
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @module pack/pack-forward
|
|
6
|
+
* Wave-1 review fix wave (I1/I2/I3): validates a pack-forwarded `maxCost`/
|
|
7
|
+
* `template` (pack-resolve.js's `applyPackToMcpInput(...).forward` — see that
|
|
8
|
+
* module's own docblock; only ever `maxCost`/`template`, both optional)
|
|
9
|
+
* BEFORE any spawn or state write, on every MCP path that forwards them
|
|
10
|
+
* (amicus_fanout; amicus_start's spawn-fallback AND in-process paths).
|
|
11
|
+
* Extracted out of mcp-server.js (grandfathered/size-exempt) because this is
|
|
12
|
+
* genuine new logic — pack-content validation and template rendering, not
|
|
13
|
+
* wiring — and needs direct unit tests a source-text grep can't provide.
|
|
14
|
+
*
|
|
15
|
+
* Two independent defects, one call:
|
|
16
|
+
* - I2 (maxCost): `checkBudget` (sidecar/budget.js) only enforces
|
|
17
|
+
* `typeof === 'number' && > 0`; a hand-edited pack with a string/zero/
|
|
18
|
+
* negative maxCost silently runs UNCAPPED on the in-process path (the
|
|
19
|
+
* type check just falls through, so no ceiling is ever applied), while a
|
|
20
|
+
* spawned CLI child re-validates its own --max-cost and refuses it
|
|
21
|
+
* (BAD_ARGS) — but only AFTER the tool already returned success and (on
|
|
22
|
+
* amicus_fanout) already wrote a 'running' wave record. Caught here,
|
|
23
|
+
* pre-spend, on every path, instead.
|
|
24
|
+
* - I1 (template): the built-in `review` template needs {{artifact}}/
|
|
25
|
+
* {{artifact_path}}, which fanout/start can never supply (neither passes
|
|
26
|
+
* an --artifact-equivalent through pack forwarding). A naive blind forward
|
|
27
|
+
* lets the CHILD's render fail via process.exit AFTER the tool already
|
|
28
|
+
* returned success — on amicus_fanout the pre-seeded wave record has no
|
|
29
|
+
* `pid`, so crash-detection (which only probes pid-bearing records) never
|
|
30
|
+
* notices, and the wave is stuck 'running' forever. Fixed by dry-running
|
|
31
|
+
* applyTemplate here, in-process, with EXACTLY the inputs the child will
|
|
32
|
+
* use (no artifact, no --var — a pack's forwarded template carries only
|
|
33
|
+
* its name). applyTemplate is a pure function of those inputs ({{date}}
|
|
34
|
+
* is day-granular), so a success here guarantees the child's own later
|
|
35
|
+
* (real) re-render succeeds identically — the child still does its own
|
|
36
|
+
* render, so template provenance (`promptMeta.template`) is still
|
|
37
|
+
* recorded exactly as Wave 1 built it.
|
|
38
|
+
*
|
|
39
|
+
* Callers that only need the pre-spend validation (the two spawn paths)
|
|
40
|
+
* ignore the returned `renderedPrompt`; amicus_start's in-process path
|
|
41
|
+
* (which never spawns a child, so nothing else would render it) reuses it
|
|
42
|
+
* as the actual prompt instead of rendering a second time.
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
const { ERROR_CODES } = require('../utils/error-doc');
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @param {{forward: {maxCost?: number, template?: string}, packRef: string,
|
|
49
|
+
* prompt: string, project: string}} opts `forward` is pack-resolve.js's
|
|
50
|
+
* `applyPackToMcpInput(...).forward`; `packRef` names the pack in error
|
|
51
|
+
* text — pass the RESOLVED pack's name (`packRecord.name`), not necessarily
|
|
52
|
+
* the caller's raw `--pack`/`pack` ref (which may be a path).
|
|
53
|
+
* @returns {{maxCost?: number, templateName?: string, renderedPrompt?: string,
|
|
54
|
+
* notices: string[]} | {error: {code, message, hint}}}
|
|
55
|
+
*/
|
|
56
|
+
function prepareForward({ forward, packRef, prompt, project }) {
|
|
57
|
+
const notices = [];
|
|
58
|
+
const result = { notices };
|
|
59
|
+
|
|
60
|
+
if (forward.maxCost !== undefined) {
|
|
61
|
+
const mc = forward.maxCost;
|
|
62
|
+
// Same validity rule cli-handlers-run.js applies to a typed --max-cost
|
|
63
|
+
// (handleStart/handleFanout): only a positive finite JS number passes. A
|
|
64
|
+
// pack is JSON, so a hand-edited "maxCost": "2.00" (string) is exactly
|
|
65
|
+
// the kind of mistake this must catch — it would otherwise reach
|
|
66
|
+
// checkBudget's `typeof === 'number'` check, fail it silently, and run
|
|
67
|
+
// with no ceiling at all.
|
|
68
|
+
if (typeof mc !== 'number' || !Number.isFinite(mc) || mc <= 0) {
|
|
69
|
+
return {
|
|
70
|
+
error: {
|
|
71
|
+
code: ERROR_CODES.PACK_INVALID,
|
|
72
|
+
message: `Error: pack '${packRef}' sets an invalid max-cost (${JSON.stringify(mc)}) — must be a positive number`,
|
|
73
|
+
hint: null,
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
result.maxCost = mc;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (forward.template !== undefined) {
|
|
81
|
+
// Same call shape the child process makes (cli-handlers-run.js's
|
|
82
|
+
// handleStart :40-43 / handleFanout :164-167): no artifactFile, no
|
|
83
|
+
// varList — a pack's forwarded template carries only its name, never
|
|
84
|
+
// --artifact/--var (pack-resolve.js only ever forwards `template`).
|
|
85
|
+
const { applyTemplate } = require('../template/apply');
|
|
86
|
+
const t = applyTemplate({ templateRef: forward.template, prompt, project });
|
|
87
|
+
if (t.error) { return { error: t.error }; }
|
|
88
|
+
result.templateName = forward.template;
|
|
89
|
+
result.renderedPrompt = t.prompt;
|
|
90
|
+
notices.push(...t.notices);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return result;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
module.exports = { prepareForward };
|