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/cli-handlers-run.js
CHANGED
|
@@ -13,11 +13,12 @@
|
|
|
13
13
|
'use strict';
|
|
14
14
|
|
|
15
15
|
const { validateStartArgs } = require('./cli');
|
|
16
|
-
const { validateTaskId } = require('./utils/validators');
|
|
16
|
+
const { validateTaskId, validateTag } = require('./utils/validators');
|
|
17
17
|
const { resolveLaunchModel, maybeOfferProviderDefaults } = require('./utils/start-helpers');
|
|
18
18
|
const { failJson, ERROR_CODES } = require('./utils/error-doc');
|
|
19
19
|
const { requireNoUiForJson } = require('./utils/cli-preflight');
|
|
20
|
-
const {
|
|
20
|
+
const { handleFanout } = require('./cli-handlers-fanout');
|
|
21
|
+
const { applyTemplateForArgs } = require('./cli-template-args');
|
|
21
22
|
|
|
22
23
|
/**
|
|
23
24
|
* Handle 'sidecar start' command
|
|
@@ -37,17 +38,9 @@ async function handleStart(args) {
|
|
|
37
38
|
// Drop --prompt-file post-resolve or validateStartArgs re-trips its XOR guard.
|
|
38
39
|
delete args['prompt-file'];
|
|
39
40
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
artifactFile: args.artifact, varList: args.var, project: args.cwd || process.cwd() });
|
|
44
|
-
if (t.error) { process.exit(failJson(useJson, t.error)); }
|
|
45
|
-
for (const n of t.notices) { process.stderr.write(n + '\n'); }
|
|
46
|
-
args.prompt = t.prompt;
|
|
47
|
-
templateMeta = t.promptMeta.template;
|
|
48
|
-
} else if (args.artifact !== undefined || args.var !== undefined) {
|
|
49
|
-
process.exit(failJson(useJson, { code: ERROR_CODES.BAD_ARGS, message: 'Error: --artifact/--var require --template (expansion happens only in template files)' }));
|
|
50
|
-
}
|
|
41
|
+
const tpl = applyTemplateForArgs(args, args.prompt, useJson);
|
|
42
|
+
if (tpl.fail !== undefined) { process.exit(tpl.fail); }
|
|
43
|
+
if (tpl.applied) { args.prompt = tpl.prompt; templateMeta = tpl.templateMeta; }
|
|
51
44
|
requireNoUiForJson(args, useJson);
|
|
52
45
|
|
|
53
46
|
const mc = args['max-cost'];
|
|
@@ -55,6 +48,18 @@ async function handleStart(args) {
|
|
|
55
48
|
process.exit(failJson(useJson, { code: ERROR_CODES.BAD_ARGS, message: 'Error: --max-cost must be a positive number' }));
|
|
56
49
|
}
|
|
57
50
|
|
|
51
|
+
// v4.7 F8 (D13): --tag rejects (unlike sanitizeCouncilName, which cleans) —
|
|
52
|
+
// a stored tag is a user-chosen search key, so silent truncation/stripping
|
|
53
|
+
// would make --search/--group-by tag miss it. Checked here, beside the other
|
|
54
|
+
// pre-flight arg checks, so a bad tag exits before model resolution / the
|
|
55
|
+
// one-time onboarding tip (T2-m1: was previously after resolveLaunchModel).
|
|
56
|
+
if (args.tag !== undefined) {
|
|
57
|
+
const tagCheck = validateTag(args.tag);
|
|
58
|
+
if (!tagCheck.ok) {
|
|
59
|
+
process.exit(failJson(useJson, { code: ERROR_CODES.BAD_ARGS, message: tagCheck.error }));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
58
63
|
const { model, alias } = await resolveLaunchModel(args);
|
|
59
64
|
args.model = model;
|
|
60
65
|
|
|
@@ -118,157 +123,10 @@ async function handleStart(args) {
|
|
|
118
123
|
modelInput: alias || null,
|
|
119
124
|
template: templateMeta, // F9 (v4.5): startSidecar ignores unknown keys; inert until a future task reads it.
|
|
120
125
|
pack: packRecord, // v4.5 Task 13: null when no --pack; additively recorded on solo session metadata.
|
|
126
|
+
tag: args.tag, // v4.7 F8: undefined when no --tag; Task 3 stores it on session metadata.
|
|
121
127
|
});
|
|
122
128
|
}
|
|
123
129
|
|
|
124
|
-
/**
|
|
125
|
-
* Handle 'amicus fanout' command (F4).
|
|
126
|
-
* Returns the wave exit code: 0 all complete, 2 partial, 1 none/hard failure,
|
|
127
|
-
* 130/143 when the wave was signal-aborted.
|
|
128
|
-
*/
|
|
129
|
-
async function handleFanout(args) {
|
|
130
|
-
const useJson = !!args.json;
|
|
131
|
-
|
|
132
|
-
// --retry-failed <waveId> (v4.3 Task 19, spec 6.1): a completely different
|
|
133
|
-
// path from the --prompt/--models launch below (no briefing, no required
|
|
134
|
-
// --models — the original wave's failed legs supply their own saved
|
|
135
|
-
// context) — dispatch BEFORE any of that validation runs. --models here is
|
|
136
|
-
// optional and, when present, filters which failed legs get retried.
|
|
137
|
-
if (args['retry-failed']) {
|
|
138
|
-
const { retryFailedWave } = require('./sidecar/fanout-retry');
|
|
139
|
-
const { parseModelsList } = require('./sidecar/fanout-validate');
|
|
140
|
-
const { exitCode, errorDoc } = await retryFailedWave(String(args['retry-failed']), args.cwd || process.cwd(), {
|
|
141
|
-
models: parseModelsList(args.models), json: useJson,
|
|
142
|
-
});
|
|
143
|
-
if (errorDoc && useJson) { process.stdout.write(JSON.stringify(errorDoc) + '\n'); }
|
|
144
|
-
return exitCode;
|
|
145
|
-
}
|
|
146
|
-
const packRecord = require('./pack/pack-cli').applyPackOrExit(args, 'fanout', useJson);
|
|
147
|
-
|
|
148
|
-
// FIX 4 (#61 whole-branch review, cheap parity): handleStart validates
|
|
149
|
-
// --gateway via validateStartArgs (cli.js) — fanout never did, so a typo'd
|
|
150
|
-
// value silently fell through to resolveGatewayMode's pass-through instead
|
|
151
|
-
// of failing fast with a clear error.
|
|
152
|
-
if (args.gateway !== undefined && !GATEWAY_MODES.includes(args.gateway)) {
|
|
153
|
-
process.exit(failJson(useJson, { code: ERROR_CODES.BAD_ARGS, message: `Error: --gateway must be one of: ${GATEWAY_MODES.join(', ')}` }));
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
const { resolvePromptSource } = require('./utils/prompt-source');
|
|
157
|
-
let promptRes;
|
|
158
|
-
if (args.prompt !== undefined || args['prompt-file'] !== undefined || args.template === undefined) {
|
|
159
|
-
promptRes = resolvePromptSource(args);
|
|
160
|
-
if (promptRes.error) { process.exit(failJson(useJson, { code: ERROR_CODES.MISSING_PROMPT, message: promptRes.error })); }
|
|
161
|
-
} else {
|
|
162
|
-
promptRes = { prompt: undefined, promptMeta: null };
|
|
163
|
-
}
|
|
164
|
-
if (args.template !== undefined) {
|
|
165
|
-
const { applyTemplate } = require('./template/apply');
|
|
166
|
-
const t = applyTemplate({ templateRef: args.template, prompt: promptRes.prompt,
|
|
167
|
-
artifactFile: args.artifact, varList: args.var, project: args.cwd || process.cwd() });
|
|
168
|
-
if (t.error) { process.exit(failJson(useJson, t.error)); }
|
|
169
|
-
for (const n of t.notices) { process.stderr.write(n + '\n'); }
|
|
170
|
-
promptRes = { prompt: t.prompt, promptMeta: t.promptMeta };
|
|
171
|
-
} else if (args.artifact !== undefined || args.var !== undefined) {
|
|
172
|
-
process.exit(failJson(useJson, { code: ERROR_CODES.BAD_ARGS, message: 'Error: --artifact/--var require --template (expansion happens only in template files)' }));
|
|
173
|
-
}
|
|
174
|
-
// Council preset: expand a saved council into args.models (mutually exclusive with --models).
|
|
175
|
-
const hasModels = typeof args.models === 'string' && args.models.trim();
|
|
176
|
-
const hasCouncil = args.council !== undefined && args.council !== false;
|
|
177
|
-
if (hasModels && hasCouncil) {
|
|
178
|
-
process.exit(failJson(useJson, { code: ERROR_CODES.BAD_ARGS, message: 'Error: pass exactly one of --models / --council, not both' }));
|
|
179
|
-
}
|
|
180
|
-
if (!hasModels && !hasCouncil) {
|
|
181
|
-
process.exit(failJson(useJson, { code: ERROR_CODES.BAD_ARGS, message: 'Error: --models is required (comma-separated aliases or provider/model IDs), or use --council <name>' }));
|
|
182
|
-
}
|
|
183
|
-
if (hasCouncil) {
|
|
184
|
-
if (typeof args.council !== 'string' || !args.council.trim()) {
|
|
185
|
-
process.exit(failJson(useJson, { code: ERROR_CODES.BAD_ARGS, message: 'Error: --council requires a council name (e.g. --council free)' }));
|
|
186
|
-
}
|
|
187
|
-
const { resolveCouncilMembers } = require('./utils/config');
|
|
188
|
-
const { readCache } = require('./utils/model-catalog');
|
|
189
|
-
const catalog = (readCache() || {}).models || [];
|
|
190
|
-
const expanded = resolveCouncilMembers(args.council.trim(), catalog);
|
|
191
|
-
if (expanded.error) {
|
|
192
|
-
process.exit(failJson(useJson, { code: ERROR_CODES.BAD_ARGS, message: `Error: ${expanded.error}` }));
|
|
193
|
-
}
|
|
194
|
-
if (expanded.dropped && expanded.dropped.length && !useJson) {
|
|
195
|
-
process.stderr.write(`Notice: dropped unavailable council member(s): ${expanded.dropped.join(', ')}\n`);
|
|
196
|
-
}
|
|
197
|
-
args.models = expanded.models.join(',');
|
|
198
|
-
}
|
|
199
|
-
if (args['wave-id']) {
|
|
200
|
-
const check = validateTaskId(String(args['wave-id']));
|
|
201
|
-
if (!check.valid) {
|
|
202
|
-
process.exit(failJson(useJson, { code: ERROR_CODES.BAD_SESSION, message: check.error }));
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
if (args.agent && String(args.agent).toLowerCase() === 'chat') {
|
|
206
|
-
process.exit(failJson(useJson, { code: ERROR_CODES.BAD_ARGS, message: 'Error: --agent chat is interactive-only; fanout is headless' }));
|
|
207
|
-
}
|
|
208
|
-
if (args.timeout !== undefined && args.timeout <= 0) {
|
|
209
|
-
process.exit(failJson(useJson, { code: ERROR_CODES.BAD_ARGS, message: 'Error: --timeout must be a positive number' }));
|
|
210
|
-
}
|
|
211
|
-
const mc = args['max-cost'];
|
|
212
|
-
if (mc !== undefined && (typeof mc !== 'number' || !Number.isFinite(mc) || mc <= 0)) {
|
|
213
|
-
process.exit(failJson(useJson, { code: ERROR_CODES.BAD_ARGS, message: 'Error: --max-cost must be a positive number' }));
|
|
214
|
-
}
|
|
215
|
-
const { parseModelsList } = require('./sidecar/fanout');
|
|
216
|
-
if (parseModelsList(args.models).length === 0) {
|
|
217
|
-
process.exit(failJson(useJson, { code: ERROR_CODES.BAD_ARGS, message: 'Error: --models must contain at least one non-empty entry' }));
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// Direct require (fanout stays internal — no src/index.js public re-export).
|
|
221
|
-
const { runFanout } = require('./sidecar/fanout');
|
|
222
|
-
const { loadConfig, resolveGatewayMode } = require('./utils/config');
|
|
223
|
-
const { resolveFallbackConfig } = require('./sidecar/fallback-chains');
|
|
224
|
-
const { readCache } = require('./utils/model-catalog');
|
|
225
|
-
const cfg = loadConfig() || {};
|
|
226
|
-
const { exitCode } = await runFanout({
|
|
227
|
-
models: args.models,
|
|
228
|
-
prompt: promptRes.prompt,
|
|
229
|
-
promptMeta: promptRes.promptMeta,
|
|
230
|
-
waveId: args['wave-id'],
|
|
231
|
-
project: args.cwd || process.cwd(),
|
|
232
|
-
agent: args.agent || args.mode,
|
|
233
|
-
thinking: args.thinking,
|
|
234
|
-
timeout: args.timeout,
|
|
235
|
-
summaryLength: args['summary-length'],
|
|
236
|
-
includeContext: !args['no-context'],
|
|
237
|
-
sessionId: args['session-id'],
|
|
238
|
-
contextTurns: args['context-turns'],
|
|
239
|
-
contextSince: args['context-since'],
|
|
240
|
-
contextMaxTokens: args['context-max-tokens'],
|
|
241
|
-
// #10: forward the Cowork parent so MCP-spawned fanout legs pin the right
|
|
242
|
-
// session (mirrors handleStart's coworkProcess plumbing). Without this the
|
|
243
|
-
// spawned `--cowork-process` flag is dropped and buildContext gets null.
|
|
244
|
-
coworkProcess: args['cowork-process'],
|
|
245
|
-
mcp: args.mcp,
|
|
246
|
-
mcpConfig: args['mcp-config'],
|
|
247
|
-
noMcp: args['no-mcp'],
|
|
248
|
-
excludeMcp: args['exclude-mcp'],
|
|
249
|
-
noValidateModel: args['no-validate-model'],
|
|
250
|
-
// #61 Task 7.3: --gateway merged with routing.prefer, applied per leg
|
|
251
|
-
// by validateFanoutModels' router call.
|
|
252
|
-
gatewayMode: resolveGatewayMode(args.gateway),
|
|
253
|
-
json: !!args.json,
|
|
254
|
-
client: args.client,
|
|
255
|
-
maxCost: args['max-cost'] !== null && args['max-cost'] !== undefined ? args['max-cost'] : cfg.maxCost,
|
|
256
|
-
noCostGate: !!args['no-cost-gate'],
|
|
257
|
-
maxCostPerMtok: cfg.maxCostPerMtok,
|
|
258
|
-
follow: !!args.follow,
|
|
259
|
-
onComplete: args['on-complete'],
|
|
260
|
-
// v4.3 Task 18 (spec §6.2): opt-in cheaper-model substitution. --fallback
|
|
261
|
-
// forces on, --no-fallback forces off; unset defers to config `fallbacks.enabled`.
|
|
262
|
-
fallback: resolveFallbackConfig({
|
|
263
|
-
flagFallback: args.fallback === true ? true : (args['no-fallback'] ? false : undefined),
|
|
264
|
-
config: cfg,
|
|
265
|
-
}),
|
|
266
|
-
catalog: (readCache() || {}).models || [],
|
|
267
|
-
pack: packRecord, // v4.5 Task 13: null when no --pack; additive on wave metadata.json + wave.json.
|
|
268
|
-
});
|
|
269
|
-
return exitCode;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
130
|
/**
|
|
273
131
|
* Handle 'sidecar read' command
|
|
274
132
|
* Spec Reference: §4.5
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @module cli-template-args
|
|
5
|
+
* v4.7 PR6 sweep: the single application point for --template/--artifact/--var,
|
|
6
|
+
* shared by the three CLI handlers (handleStart, handleFanout, handleCouncilRun)
|
|
7
|
+
* that used to carry this block verbatim-triplicated.
|
|
8
|
+
*
|
|
9
|
+
* NEVER calls process.exit: handleStart and handleFanout exit on failure, but
|
|
10
|
+
* handleCouncilRun RETURNS its exit code (its whole handler contract is
|
|
11
|
+
* return-the-code) — so the decision belongs to the caller, not this helper.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const { failJson, ERROR_CODES } = require('./utils/error-doc');
|
|
15
|
+
|
|
16
|
+
const NEEDS_TEMPLATE_MSG =
|
|
17
|
+
'Error: --artifact/--var require --template (expansion happens only in template files)';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @param {object} args parsed argv
|
|
21
|
+
* @param {string|undefined} prompt pre-template prompt text
|
|
22
|
+
* @param {boolean} useJson
|
|
23
|
+
* @returns {{applied:false}
|
|
24
|
+
* | {applied:true, prompt:string, promptMeta:object, templateMeta:object}
|
|
25
|
+
* | {fail:number}}
|
|
26
|
+
*/
|
|
27
|
+
function applyTemplateForArgs(args, prompt, useJson) {
|
|
28
|
+
if (args.template !== undefined) {
|
|
29
|
+
const { applyTemplate } = require('./template/apply');
|
|
30
|
+
const t = applyTemplate({
|
|
31
|
+
templateRef: args.template, prompt,
|
|
32
|
+
artifactFile: args.artifact, varList: args.var,
|
|
33
|
+
project: args.cwd || process.cwd(),
|
|
34
|
+
});
|
|
35
|
+
if (t.error) { return { fail: failJson(useJson, t.error) }; }
|
|
36
|
+
for (const n of t.notices) { process.stderr.write(n + '\n'); }
|
|
37
|
+
return {
|
|
38
|
+
applied: true, prompt: t.prompt, promptMeta: t.promptMeta,
|
|
39
|
+
templateMeta: t.promptMeta.template,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
if (args.artifact !== undefined || args.var !== undefined) {
|
|
43
|
+
return { fail: failJson(useJson, { code: ERROR_CODES.BAD_ARGS, message: NEEDS_TEMPLATE_MSG }) };
|
|
44
|
+
}
|
|
45
|
+
return { applied: false };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
module.exports = { applyTemplateForArgs, NEEDS_TEMPLATE_MSG };
|
package/src/cli.js
CHANGED
|
@@ -105,8 +105,12 @@ function parseArgs(argv) {
|
|
|
105
105
|
// Single short-flag alias, scoped to exactly '-o' (council verdict's
|
|
106
106
|
// --out shorthand). No general short-flag support is implemented —
|
|
107
107
|
// any other leading-dash token still falls through to positionals.
|
|
108
|
+
// R5: deliberately lockstep with the long-option branch above
|
|
109
|
+
// (`!next.startsWith('--')`) — '-o' and '--out' must consume values
|
|
110
|
+
// identically. A dash-leading value is still accepted here (parsing
|
|
111
|
+
// only normalizes); cli-handlers-council.js's R1 guard rejects it.
|
|
108
112
|
const next = argv[i + 1];
|
|
109
|
-
if (next && !next.startsWith('
|
|
113
|
+
if (next && !next.startsWith('--')) {
|
|
110
114
|
result.out = next;
|
|
111
115
|
i++;
|
|
112
116
|
} else {
|
|
@@ -455,6 +459,7 @@ Options for 'start':
|
|
|
455
459
|
--artifact <file> File whose content fills {{artifact}} (256 KB cap; needs --template)
|
|
456
460
|
--var <k=v> Template variable, repeatable (needs --template)
|
|
457
461
|
--pack <name|path> Load a saved pack (model/options/template); explicit flags override it
|
|
462
|
+
--tag <t> Label this session for list/search/spend grouping (1-64 chars, [A-Za-z0-9_-])
|
|
458
463
|
`,
|
|
459
464
|
fanout: `
|
|
460
465
|
Options for 'fanout':
|
|
@@ -465,31 +470,34 @@ Options for 'fanout':
|
|
|
465
470
|
~32KB Windows argument cap). Mutually exclusive
|
|
466
471
|
with --prompt. Also works with 'start'.
|
|
467
472
|
--wave-id <id> Explicit wave ID (leg IDs become <id>-1..N)
|
|
468
|
-
--retry-failed <waveId>
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
+
--retry-failed <waveId> Relaunch ONLY that wave's failed/timed-out/crashed/
|
|
474
|
+
aborted legs as a NEW linked wave, using each leg's
|
|
475
|
+
own saved context (byte-identical retry). Skips
|
|
476
|
+
--prompt/--models; --models filters which failed
|
|
477
|
+
legs to retry. wave.json is never modified.
|
|
473
478
|
--json Emit the wave result as stable JSON on stdout
|
|
479
|
+
--quiet Suppress per-leg progress ticks, the wave-result summary, and
|
|
480
|
+
preflight errors
|
|
474
481
|
--max-cost <$> Refuse the wave if the estimated total exceeds $ (soft ceiling)
|
|
475
482
|
--no-cost-gate Disable the budget gate (per-$/Mtok threshold + ceiling) for this run
|
|
476
483
|
--fallback / --no-fallback Opt-in cheaper-model substitution on a classified
|
|
477
484
|
rate-limit/overload leg failure (spec 6.2). Overrides
|
|
478
485
|
config fallbacks.enabled when passed; default: config, else off.
|
|
479
|
-
--gateway <mode>
|
|
480
|
-
--follow
|
|
481
|
-
--on-complete <cmd>
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
486
|
+
--gateway <mode> Routing: auto (direct-first), direct, or openrouter
|
|
487
|
+
--follow Stream this run's events to stderr as they happen (--json -> NDJSON)
|
|
488
|
+
--on-complete <cmd> Run a shell command once, at terminal state, after
|
|
489
|
+
wave.json is durable. The command is user-authored
|
|
490
|
+
on THIS command line (CLI-only — never sourced from
|
|
491
|
+
config/briefings/model output); payload rides via
|
|
492
|
+
env only (AMICUS_TASK_ID/TYPE/STATUS/EXIT_CODE/
|
|
493
|
+
RESULT_FILE/EVENTS_FILE/COST/PROJECT), never model
|
|
494
|
+
text. Child stdout/stderr go to amicus stderr.
|
|
495
|
+
Never changes the wave's exit code, docs, or events.
|
|
489
496
|
--template <name|path> Render a briefing template ({{prompt}}, {{artifact}}, {{var.*}})
|
|
490
497
|
--artifact <file> File whose content fills {{artifact}} (256 KB cap; needs --template)
|
|
491
498
|
--var <k=v> Template variable, repeatable (needs --template)
|
|
492
499
|
--pack <name|path> Load a saved pack (bench/options/template); explicit flags override it
|
|
500
|
+
--tag <t> Label this wave for list/search/spend grouping (1-64 chars, [A-Za-z0-9_-])
|
|
493
501
|
Shared per-leg knobs: --agent, --thinking, --timeout, --summary-length,
|
|
494
502
|
--no-context, --context-*, --mcp*, --no-validate-model, --cwd
|
|
495
503
|
Exit codes: 0 all legs complete, 2 partial, 1 none complete / hard failure
|
|
@@ -511,6 +519,8 @@ Options for 'models':
|
|
|
511
519
|
Options for 'list':
|
|
512
520
|
--status <filter> Filter by status (running, complete)
|
|
513
521
|
--all Show all projects
|
|
522
|
+
--search <q> Case-insensitive substring filter over id, tag, and briefing
|
|
523
|
+
--limit <n> Show only the n newest rows (0 = unlimited)
|
|
514
524
|
--json Output as JSON
|
|
515
525
|
`,
|
|
516
526
|
status: `
|
|
@@ -580,7 +590,7 @@ Subcommands for 'council':
|
|
|
580
590
|
[--debate] [--claude-review <file>] [--no-cost-gate] [--follow]
|
|
581
591
|
[--fallback] [--no-fallback] [--on-complete <cmd>]
|
|
582
592
|
[--template <name|path>] [--artifact <file>] [--var <k=v>]
|
|
583
|
-
[--pack <name|path>]
|
|
593
|
+
[--pack <name|path>] [--tag <t>]
|
|
584
594
|
Run the full headless council engine (v4.0).
|
|
585
595
|
Chair default: deepseek (must NOT be a bench seat).
|
|
586
596
|
--critic and --lenses are mutually exclusive.
|
|
@@ -633,7 +643,7 @@ Options for 'spend':
|
|
|
633
643
|
--model <id-or-prefix> Only rows whose model starts with this
|
|
634
644
|
--op <start|continue|resume|leg> Only rows with this operation
|
|
635
645
|
--failed Only non-complete (wasted) rows
|
|
636
|
-
--group-by <model|wave|council|project|op|day> Rollup dimension (default model)
|
|
646
|
+
--group-by <model|wave|council|project|op|day|tag> Rollup dimension (default model)
|
|
637
647
|
--rows Include matching raw rows (capped at 1000)
|
|
638
648
|
--json Machine-readable output (versioned spend doc)
|
|
639
649
|
Reads ~/.config/amicus/spend-ledger.jsonl (one row per completed run/leg).
|
|
@@ -691,34 +701,17 @@ Options for 'template':
|
|
|
691
701
|
pack: `
|
|
692
702
|
Options for 'pack':
|
|
693
703
|
amicus pack save <name> --kind council|fanout|solo [flags]
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
--template <name|path> briefing template
|
|
706
|
-
reference (not rendered)
|
|
707
|
-
--version <semver> default 1.0.0 (an
|
|
708
|
-
unchanged re-save is a
|
|
709
|
-
no-op; a changed one
|
|
710
|
-
auto-bumps the patch)
|
|
711
|
-
--description <text>
|
|
712
|
-
amicus pack save <name> --from-run <id>
|
|
713
|
-
Build a pack from an existing council run /
|
|
714
|
-
fanout wave / solo session instead of flags
|
|
715
|
-
(models, options, and a template REFERENCE only
|
|
716
|
-
— briefing text is never captured)
|
|
717
|
-
amicus pack list [--json] List saved packs
|
|
718
|
-
amicus pack show <name|path> [--json]
|
|
719
|
-
Print a pack plus its validation report (never
|
|
720
|
-
fails on an invalid pack — see 'validation')
|
|
721
|
-
amicus pack rm <name> [--json] Remove a saved pack
|
|
704
|
+
amicus pack save <name> --from-run <id> Build from an existing run (models/
|
|
705
|
+
options/template reference only — briefing never captured)
|
|
706
|
+
amicus pack list [--json] List saved packs
|
|
707
|
+
amicus pack show <name|path> [--json] Print a pack + validation report
|
|
708
|
+
amicus pack rm <name> [--json] Remove a saved pack
|
|
709
|
+
Flags: --bench <a,b,c|name> (council/fanout) · --model (solo) · --chair/--critic/--lenses
|
|
710
|
+
(council) · --debate/--no-debate (council) · --timeout/--max-cost/--gateway (shared) ·
|
|
711
|
+
--agent/--thinking/--summary-length (fanout/solo) · --template <name|path> (reference only,
|
|
712
|
+
not rendered) · --pack-version <semver> (the SAVED PACK's version, default 1.0.0; unchanged
|
|
713
|
+
re-save is a no-op, changed auto-bumps patch — NOT --version, which is amicus's own global
|
|
714
|
+
flag) · --description <text>
|
|
722
715
|
`
|
|
723
716
|
};
|
|
724
717
|
|
package/src/council/debate.js
CHANGED
|
@@ -8,12 +8,17 @@
|
|
|
8
8
|
* new tally input plus the debate.json findings rows. run.js then re-runs
|
|
9
9
|
* tally() on the returned input and decorates the record. Keeps run.js under the
|
|
10
10
|
* line gate.
|
|
11
|
+
* Also holds the pure pre-debate helpers (target selection, dispute detection, re-vote bundling) moved from run-debate.js (v4.7 PR0).
|
|
11
12
|
*/
|
|
12
13
|
|
|
13
14
|
const PAST_TENSE = { defend: 'defended', amend: 'amended', withdraw: 'withdrawn', 'no-response': 'no-response' };
|
|
14
15
|
|
|
15
|
-
//
|
|
16
|
-
//
|
|
16
|
+
// The debate-role vocabulary: a debate leg is an extra leg by an already-benched model,
|
|
17
|
+
// never an extra ledger row and never that model's ledger identity. Through v4.6 this Set
|
|
18
|
+
// ALSO drove ledger.js's join skip-set directly; Task 7 (v4.7 D4) replaced that with
|
|
19
|
+
// ledger.js's own LEDGER_JOIN_ROLES allowlist (fail-closed: everything not named there is
|
|
20
|
+
// excluded, not just DEBATE_ROLES), so this Set no longer has any runtime consumer outside
|
|
21
|
+
// this module — kept exported because debate.test.js pins its exact contents.
|
|
17
22
|
const DEBATE_ROLES = new Set(['rebuttal', 'revote']);
|
|
18
23
|
|
|
19
24
|
/**
|
|
@@ -78,21 +83,95 @@ function decorateRecord(record, debateFindings) {
|
|
|
78
83
|
}
|
|
79
84
|
|
|
80
85
|
/**
|
|
81
|
-
* runStats rows for the debate legs (spec §5.5)
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
+
* runStats rows for the debate legs (spec §5.5), plus v4.7 D2/E4's row-per-launch
|
|
87
|
+
* extras: role is 'rebuttal' | 'revote' for the primary defense/re-vote legs,
|
|
88
|
+
* 'superseded' for an original leg a successful repair replaced, and 'repair' for
|
|
89
|
+
* a repair attempt that itself never became usable (error status rides naturally
|
|
90
|
+
* off the raw leg). The rebuttal/revote legs never enter meta.models, so the
|
|
91
|
+
* ledger stays one row per (run×model). DEBATE_ROLES remains the debate-role
|
|
92
|
+
* vocabulary (rebuttal/revote); the ledger's overwrite protection for ALL FOUR
|
|
93
|
+
* of these row-per-launch roles — rebuttal, revote, superseded AND repair —
|
|
94
|
+
* lives in ledger.js's own LEDGER_JOIN_ROLES allowlist (v4.7 D4, Task 7):
|
|
95
|
+
* a role not named there never joins, full stop, regardless of which module
|
|
96
|
+
* produced the row or whether it is even in DEBATE_ROLES.
|
|
97
|
+
* @param {{defenseLegs: Array, revoteLegs: Array, supersededLegs?: Array,
|
|
98
|
+
* repairLegs?: Array}} args leg metadata
|
|
86
99
|
* @returns {Array<object>}
|
|
87
100
|
*/
|
|
88
|
-
function debateRunStatsRows({ defenseLegs, revoteLegs }) {
|
|
101
|
+
function debateRunStatsRows({ defenseLegs, revoteLegs, supersededLegs, repairLegs }) {
|
|
89
102
|
const mk = (role) => (l) => ({
|
|
90
103
|
model: l.model, role, wasChair: false, conformance: l.conformance || 'clean',
|
|
91
104
|
status: l.status || 'unknown',
|
|
92
105
|
durationMs: typeof l.durationMs === 'number' ? l.durationMs : null,
|
|
93
106
|
usage: l.usage || null,
|
|
107
|
+
...(l.waveId ? { waveId: l.waveId } : {}),
|
|
108
|
+
...(l.resolvedModel ? { resolvedModel: l.resolvedModel } : {}),
|
|
94
109
|
});
|
|
95
|
-
return [
|
|
110
|
+
return [
|
|
111
|
+
...(defenseLegs || []).map(mk('rebuttal')),
|
|
112
|
+
...(revoteLegs || []).map(mk('revote')),
|
|
113
|
+
...(supersededLegs || []).map(mk('superseded')),
|
|
114
|
+
...(repairLegs || []).map(mk('repair')),
|
|
115
|
+
];
|
|
96
116
|
}
|
|
97
117
|
|
|
98
|
-
|
|
118
|
+
/** Spec §5.7 fallback: a dead/unparseable defense means every bundled id's original stands. */
|
|
119
|
+
function allNoResponse(ids) {
|
|
120
|
+
const byId = {};
|
|
121
|
+
for (const id of ids) { byId[id] = { action: 'no-response' }; }
|
|
122
|
+
return byId;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** True when there is nothing to challenge (spec §5.1). */
|
|
126
|
+
function nothingToDebate(provisionalRecord) {
|
|
127
|
+
if (!provisionalRecord || provisionalRecord.judged === false) { return true; }
|
|
128
|
+
const n = provisionalRecord.findings.filter(f => f.tier === 'Contested' || f.tier === 'Disputed').length;
|
|
129
|
+
return n === 0;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Judges whose provisional adjudications dispute at least one bundled id. */
|
|
133
|
+
function disputingJudges(provisionalRecord, bundledIds) {
|
|
134
|
+
const ids = new Set(bundledIds);
|
|
135
|
+
const judges = new Set();
|
|
136
|
+
for (const f of provisionalRecord.findings) {
|
|
137
|
+
if (!ids.has(f.id)) { continue; }
|
|
138
|
+
for (const adj of f.adjudications || []) {
|
|
139
|
+
if (adj.verdict === 'dispute') { judges.add(adj.judge); }
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return [...judges];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Group Contested+Disputed findings by raiser (defense targets). */
|
|
146
|
+
function debateTargets(provisionalRecord, tallyInput) {
|
|
147
|
+
const claimById = new Map(tallyInput.findings.map(f => [f.id, f]));
|
|
148
|
+
const byRaiser = {};
|
|
149
|
+
const previousTier = {};
|
|
150
|
+
for (const f of provisionalRecord.findings) {
|
|
151
|
+
if (f.tier !== 'Contested' && f.tier !== 'Disputed') { continue; }
|
|
152
|
+
previousTier[f.id] = f.tier;
|
|
153
|
+
const src = claimById.get(f.id) || {};
|
|
154
|
+
const peerVerdicts = (f.adjudications || []).filter(a => a.judge !== f.raiser).map(a => a.verdict);
|
|
155
|
+
(byRaiser[f.raiser] = byRaiser[f.raiser] || []).push({ id: f.id, claim: src.claim,
|
|
156
|
+
severity: f.severity, location: src.location, peerVerdicts, disputeReasons: [] });
|
|
157
|
+
}
|
|
158
|
+
return { byRaiser, previousTier };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** The re-vote bundle: defended-or-amended findings ONLY (spec §5.1 — withdrawn never appear). */
|
|
162
|
+
function bundleFor(defenseResults, tallyInput) {
|
|
163
|
+
const out = [];
|
|
164
|
+
for (const dr of defenseResults) {
|
|
165
|
+
for (const [id, resp] of Object.entries(dr.byId)) {
|
|
166
|
+
if (resp.action !== 'defend' && resp.action !== 'amend') { continue; }
|
|
167
|
+
const src = tallyInput.findings.find(f => f.id === id) || {};
|
|
168
|
+
out.push({ id, severity: src.severity, amended: resp.action === 'amend',
|
|
169
|
+
claim: resp.action === 'amend' ? resp.claim : src.claim,
|
|
170
|
+
argument: resp.argument || 'defended without extra argument' });
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return out;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
module.exports = { applyDebate, decorateRecord, debateRunStatsRows, PAST_TENSE, DEBATE_ROLES,
|
|
177
|
+
allNoResponse, nothingToDebate, disputingJudges, debateTargets, bundleFor };
|