amicus 4.6.2 → 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 +349 -0
- package/README.md +24 -13
- package/bin/amicus.js +31 -0
- package/docs/ROADMAP.md +172 -36
- package/docs/configuration.md +56 -6
- 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 +68 -15
- package/electron/workspace-ui/index.html +3 -0
- package/electron/workspace-ui/live-model.js +132 -21
- package/electron/workspace-ui/workspace-app.js +20 -4
- 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 +15 -5
- package/electron/workspace-ui/workspace-seats.js +88 -5
- package/electron/workspace-ui/workspace-verbs.js +1 -1
- 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 +24 -3
- package/src/cli-handlers-doctor.js +9 -3
- 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/presets-cli.js +6 -2
- 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 +70 -11
- 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/session-manager.js +6 -2
- 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/alias-audit.js +19 -0
- package/src/utils/cli-preflight.js +27 -1
- package/src/utils/config.js +15 -0
- package/src/utils/curated-models.js +43 -7
- package/src/utils/gateway-route-audit.js +16 -3
- package/src/utils/model-fetcher.js +8 -6
- package/src/utils/remediation-hints.js +14 -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 +156 -0
- package/src/utils/spend-ledger.js +11 -4
- package/src/utils/validators.js +16 -0
|
@@ -80,10 +80,14 @@ function realDeps() {
|
|
|
80
80
|
now: () => Date.now(),
|
|
81
81
|
listSessionIndexTmpFiles: () => tmpSweep.listSessionIndexTmpFiles(), // B15
|
|
82
82
|
unlinkSessionIndexTmp: (n) => tmpSweep.unlinkSessionIndexTmp(n),
|
|
83
|
+
listSessionMetadataTmpFiles: () => metaSweep.listSessionMetadataTmpFiles(), // D8
|
|
84
|
+
unlinkSessionMetadataTmp: (n) => metaSweep.unlinkSessionMetadataTmp(n),
|
|
83
85
|
};
|
|
84
86
|
}
|
|
85
87
|
// B15: sweep logic in utils/session-index-tmp-sweep.js (mirrors mcp-legacy's split).
|
|
86
88
|
const tmpSweep = require('./utils/session-index-tmp-sweep');
|
|
89
|
+
// D8: per-session metadata.json sibling sweep — utils/session-metadata-tmp-sweep.js.
|
|
90
|
+
const metaSweep = require('./utils/session-metadata-tmp-sweep');
|
|
87
91
|
|
|
88
92
|
/** Run one guarded check; a thrown fn becomes an error line. */
|
|
89
93
|
function guard(id, name, fn) {
|
|
@@ -108,10 +112,10 @@ async function runDoctorChecks(depsOverride = {}) {
|
|
|
108
112
|
const checks = [];
|
|
109
113
|
|
|
110
114
|
checks.push(guard('node', 'Node.js', () => {
|
|
111
|
-
const
|
|
112
|
-
return
|
|
115
|
+
const [maj, min] = String(d.nodeVersion).replace(/^v/, '').split('.').map(n => parseInt(n, 10));
|
|
116
|
+
return (maj > 22 || (maj === 22 && min >= 12))
|
|
113
117
|
? { id: 'node', name: 'Node.js', status: 'ok', message: d.nodeVersion, hint: null }
|
|
114
|
-
: { id: 'node', name: 'Node.js', status: 'error', message: `${d.nodeVersion} (need >=
|
|
118
|
+
: { id: 'node', name: 'Node.js', status: 'error', message: `${d.nodeVersion} (need >=22.12)`, hint: 'Install Node 22.12 or newer from https://nodejs.org' };
|
|
115
119
|
}));
|
|
116
120
|
|
|
117
121
|
checks.push(guard('config-dir', 'Config directory', () => (
|
|
@@ -201,6 +205,8 @@ async function runDoctorChecks(depsOverride = {}) {
|
|
|
201
205
|
|
|
202
206
|
checks.push(guard('sessions-index-tmp', 'Session index tmp files', () => tmpSweep.evaluateSessionIndexTmpSweep(d)));
|
|
203
207
|
|
|
208
|
+
checks.push(guard('session-metadata-tmp', 'Session metadata tmp files', () => metaSweep.evaluateSessionMetadataTmpSweep(d)));
|
|
209
|
+
|
|
204
210
|
// #43: OpenRouter credit/free-tier — warns (never errors); skipped when no key.
|
|
205
211
|
checks.push(await guardAsync('openrouter-credit', 'OpenRouter credit', async () => {
|
|
206
212
|
const values = d.readApiKeyValues() || {};
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI handler for the fanout command (multi-model parallel runs).
|
|
3
|
+
*
|
|
4
|
+
* Extracted verbatim from cli-handlers-run.js (v4.7 PR0) to keep that file
|
|
5
|
+
* under the 300-line gate before F8 adds --tag forwarding. Whole-handler
|
|
6
|
+
* split precedent: cli-handlers-resume-continue.js.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
const { validateTaskId, validateTag } = require('./utils/validators');
|
|
12
|
+
const { failJson, ERROR_CODES } = require('./utils/error-doc');
|
|
13
|
+
const { GATEWAY_MODES } = require('./utils/model-descriptor');
|
|
14
|
+
const { applyTemplateForArgs } = require('./cli-template-args');
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Handle 'amicus fanout' command (F4).
|
|
18
|
+
* Returns the wave exit code: 0 all complete, 2 partial, 1 none/hard failure,
|
|
19
|
+
* 130/143 when the wave was signal-aborted.
|
|
20
|
+
*/
|
|
21
|
+
async function handleFanout(args) {
|
|
22
|
+
const useJson = !!args.json;
|
|
23
|
+
|
|
24
|
+
// v4.7 F8 (D13): a retried wave replays each leg's own saved context
|
|
25
|
+
// byte-identical — there is no fresh session to attach a new --tag to, so
|
|
26
|
+
// reject the combination before the retry-failed dispatch below.
|
|
27
|
+
if (args.tag !== undefined && args['retry-failed']) {
|
|
28
|
+
process.exit(failJson(useJson, { code: ERROR_CODES.BAD_ARGS, message: 'Error: --tag cannot be combined with --retry-failed' }));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// --retry-failed <waveId> (v4.3 Task 19, spec 6.1): a completely different
|
|
32
|
+
// path from the --prompt/--models launch below (no briefing, no required
|
|
33
|
+
// --models — the original wave's failed legs supply their own saved
|
|
34
|
+
// context) — dispatch BEFORE any of that validation runs. --models here is
|
|
35
|
+
// optional and, when present, filters which failed legs get retried.
|
|
36
|
+
// --pack is likewise ignored on this path (deliberate, same precedent: retry
|
|
37
|
+
// replays the wave's recorded per-leg config; flags that reshape a wave don't apply).
|
|
38
|
+
if (args['retry-failed']) {
|
|
39
|
+
const { retryFailedWave } = require('./sidecar/fanout-retry');
|
|
40
|
+
const { parseModelsList } = require('./sidecar/fanout-validate');
|
|
41
|
+
const { exitCode, errorDoc } = await retryFailedWave(String(args['retry-failed']), args.cwd || process.cwd(), {
|
|
42
|
+
models: parseModelsList(args.models), json: useJson,
|
|
43
|
+
});
|
|
44
|
+
if (errorDoc && useJson) { process.stdout.write(JSON.stringify(errorDoc) + '\n'); }
|
|
45
|
+
return exitCode;
|
|
46
|
+
}
|
|
47
|
+
const packRecord = require('./pack/pack-cli').applyPackOrExit(args, 'fanout', useJson);
|
|
48
|
+
|
|
49
|
+
// FIX 4 (#61 whole-branch review, cheap parity): handleStart validates
|
|
50
|
+
// --gateway via validateStartArgs (cli.js) — fanout never did, so a typo'd
|
|
51
|
+
// value silently fell through to resolveGatewayMode's pass-through instead
|
|
52
|
+
// of failing fast with a clear error.
|
|
53
|
+
if (args.gateway !== undefined && !GATEWAY_MODES.includes(args.gateway)) {
|
|
54
|
+
process.exit(failJson(useJson, { code: ERROR_CODES.BAD_ARGS, message: `Error: --gateway must be one of: ${GATEWAY_MODES.join(', ')}` }));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const { resolvePromptSource } = require('./utils/prompt-source');
|
|
58
|
+
let promptRes;
|
|
59
|
+
if (args.prompt !== undefined || args['prompt-file'] !== undefined || args.template === undefined) {
|
|
60
|
+
promptRes = resolvePromptSource(args);
|
|
61
|
+
if (promptRes.error) { process.exit(failJson(useJson, { code: ERROR_CODES.MISSING_PROMPT, message: promptRes.error })); }
|
|
62
|
+
} else {
|
|
63
|
+
promptRes = { prompt: undefined, promptMeta: null };
|
|
64
|
+
}
|
|
65
|
+
const tpl = applyTemplateForArgs(args, promptRes.prompt, useJson);
|
|
66
|
+
if (tpl.fail !== undefined) { process.exit(tpl.fail); }
|
|
67
|
+
if (tpl.applied) { promptRes = { prompt: tpl.prompt, promptMeta: tpl.promptMeta }; }
|
|
68
|
+
// Council preset: expand a saved council into args.models (mutually exclusive with --models).
|
|
69
|
+
const hasModels = typeof args.models === 'string' && args.models.trim();
|
|
70
|
+
const hasCouncil = args.council !== undefined && args.council !== false;
|
|
71
|
+
if (hasModels && hasCouncil) {
|
|
72
|
+
process.exit(failJson(useJson, { code: ERROR_CODES.BAD_ARGS, message: 'Error: pass exactly one of --models / --council, not both' }));
|
|
73
|
+
}
|
|
74
|
+
if (!hasModels && !hasCouncil) {
|
|
75
|
+
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>' }));
|
|
76
|
+
}
|
|
77
|
+
if (hasCouncil) {
|
|
78
|
+
if (typeof args.council !== 'string' || !args.council.trim()) {
|
|
79
|
+
process.exit(failJson(useJson, { code: ERROR_CODES.BAD_ARGS, message: 'Error: --council requires a council name (e.g. --council free)' }));
|
|
80
|
+
}
|
|
81
|
+
const { resolveCouncilMembers } = require('./utils/config');
|
|
82
|
+
const { readCache } = require('./utils/model-catalog');
|
|
83
|
+
const catalog = (readCache() || {}).models || [];
|
|
84
|
+
const expanded = resolveCouncilMembers(args.council.trim(), catalog);
|
|
85
|
+
if (expanded.error) {
|
|
86
|
+
process.exit(failJson(useJson, { code: ERROR_CODES.BAD_ARGS, message: `Error: ${expanded.error}` }));
|
|
87
|
+
}
|
|
88
|
+
if (expanded.dropped && expanded.dropped.length && !useJson) {
|
|
89
|
+
process.stderr.write(`Notice: dropped unavailable council member(s): ${expanded.dropped.join(', ')}\n`);
|
|
90
|
+
}
|
|
91
|
+
args.models = expanded.models.join(',');
|
|
92
|
+
}
|
|
93
|
+
if (args['wave-id']) {
|
|
94
|
+
const check = validateTaskId(String(args['wave-id']));
|
|
95
|
+
if (!check.valid) {
|
|
96
|
+
process.exit(failJson(useJson, { code: ERROR_CODES.BAD_SESSION, message: check.error }));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (args.agent && String(args.agent).toLowerCase() === 'chat') {
|
|
100
|
+
process.exit(failJson(useJson, { code: ERROR_CODES.BAD_ARGS, message: 'Error: --agent chat is interactive-only; fanout is headless' }));
|
|
101
|
+
}
|
|
102
|
+
if (args.timeout !== undefined && args.timeout <= 0) {
|
|
103
|
+
process.exit(failJson(useJson, { code: ERROR_CODES.BAD_ARGS, message: 'Error: --timeout must be a positive number' }));
|
|
104
|
+
}
|
|
105
|
+
const mc = args['max-cost'];
|
|
106
|
+
if (mc !== undefined && (typeof mc !== 'number' || !Number.isFinite(mc) || mc <= 0)) {
|
|
107
|
+
process.exit(failJson(useJson, { code: ERROR_CODES.BAD_ARGS, message: 'Error: --max-cost must be a positive number' }));
|
|
108
|
+
}
|
|
109
|
+
// v4.7 F8 (D13): reject-style (unlike sanitizeCouncilName, which cleans) —
|
|
110
|
+
// a stored tag is a user-chosen search key, so silent truncation/stripping
|
|
111
|
+
// would make --search/--group-by tag miss it.
|
|
112
|
+
if (args.tag !== undefined) {
|
|
113
|
+
const tagCheck = validateTag(args.tag);
|
|
114
|
+
if (!tagCheck.ok) {
|
|
115
|
+
process.exit(failJson(useJson, { code: ERROR_CODES.BAD_ARGS, message: tagCheck.error }));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const { parseModelsList } = require('./sidecar/fanout');
|
|
119
|
+
if (parseModelsList(args.models).length === 0) {
|
|
120
|
+
process.exit(failJson(useJson, { code: ERROR_CODES.BAD_ARGS, message: 'Error: --models must contain at least one non-empty entry' }));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Direct require (fanout stays internal — no src/index.js public re-export).
|
|
124
|
+
const { runFanout } = require('./sidecar/fanout');
|
|
125
|
+
const { loadConfig, resolveGatewayMode } = require('./utils/config');
|
|
126
|
+
const { resolveFallbackConfig } = require('./sidecar/fallback-chains');
|
|
127
|
+
const { readCache } = require('./utils/model-catalog');
|
|
128
|
+
const cfg = loadConfig() || {};
|
|
129
|
+
const { exitCode } = await runFanout({
|
|
130
|
+
models: args.models,
|
|
131
|
+
prompt: promptRes.prompt,
|
|
132
|
+
promptMeta: promptRes.promptMeta,
|
|
133
|
+
waveId: args['wave-id'],
|
|
134
|
+
project: args.cwd || process.cwd(),
|
|
135
|
+
agent: args.agent || args.mode,
|
|
136
|
+
thinking: args.thinking,
|
|
137
|
+
timeout: args.timeout,
|
|
138
|
+
summaryLength: args['summary-length'],
|
|
139
|
+
includeContext: !args['no-context'],
|
|
140
|
+
sessionId: args['session-id'],
|
|
141
|
+
contextTurns: args['context-turns'],
|
|
142
|
+
contextSince: args['context-since'],
|
|
143
|
+
contextMaxTokens: args['context-max-tokens'],
|
|
144
|
+
// #10: forward the Cowork parent so MCP-spawned fanout legs pin the right
|
|
145
|
+
// session (mirrors handleStart's coworkProcess plumbing). Without this the
|
|
146
|
+
// spawned `--cowork-process` flag is dropped and buildContext gets null.
|
|
147
|
+
coworkProcess: args['cowork-process'],
|
|
148
|
+
mcp: args.mcp,
|
|
149
|
+
mcpConfig: args['mcp-config'],
|
|
150
|
+
noMcp: args['no-mcp'],
|
|
151
|
+
excludeMcp: args['exclude-mcp'],
|
|
152
|
+
noValidateModel: args['no-validate-model'],
|
|
153
|
+
// #61 Task 7.3: --gateway merged with routing.prefer, applied per leg
|
|
154
|
+
// by validateFanoutModels' router call.
|
|
155
|
+
gatewayMode: resolveGatewayMode(args.gateway),
|
|
156
|
+
json: !!args.json,
|
|
157
|
+
// v4.7 PR3 rider: `quiet` is a repo-wide known flag, so `fanout --quiet`
|
|
158
|
+
// parsed and exited 0 while runFanout still printed — forward it.
|
|
159
|
+
quiet: !!args.quiet,
|
|
160
|
+
client: args.client,
|
|
161
|
+
maxCost: args['max-cost'] !== null && args['max-cost'] !== undefined ? args['max-cost'] : cfg.maxCost,
|
|
162
|
+
noCostGate: !!args['no-cost-gate'],
|
|
163
|
+
maxCostPerMtok: cfg.maxCostPerMtok,
|
|
164
|
+
follow: !!args.follow,
|
|
165
|
+
onComplete: args['on-complete'],
|
|
166
|
+
// v4.3 Task 18 (spec §6.2): opt-in cheaper-model substitution. --fallback
|
|
167
|
+
// forces on, --no-fallback forces off; unset defers to config `fallbacks.enabled`.
|
|
168
|
+
fallback: resolveFallbackConfig({
|
|
169
|
+
flagFallback: args.fallback === true ? true : (args['no-fallback'] ? false : undefined),
|
|
170
|
+
config: cfg,
|
|
171
|
+
}),
|
|
172
|
+
catalog: (readCache() || {}).models || [],
|
|
173
|
+
pack: packRecord, // v4.5 Task 13: null when no --pack; additive on wave metadata.json + wave.json.
|
|
174
|
+
tag: args.tag, // v4.7 F8: undefined when no --tag; Task 3 stores it on wave metadata.
|
|
175
|
+
});
|
|
176
|
+
return exitCode;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
module.exports = { handleFanout };
|
package/src/cli-handlers-pack.js
CHANGED
|
@@ -23,7 +23,9 @@ const { ERROR_CODES, failJson } = require('./utils/error-doc');
|
|
|
23
23
|
/** Build a pack object from `pack save <name> --kind ... <flags>`. */
|
|
24
24
|
function buildPackFromFlags(name, args) {
|
|
25
25
|
const kind = args.kind;
|
|
26
|
-
|
|
26
|
+
// `--pack-version`, NOT `--version`: the latter is a global BOOLEAN_FLAG that
|
|
27
|
+
// bin/amicus.js intercepts before dispatch, so it could never arrive here.
|
|
28
|
+
const pack = { schemaVersion: 1, type: 'pack', name, version: args['pack-version'] || '1.0.0', kind };
|
|
27
29
|
if (args.description) { pack.description = String(args.description); }
|
|
28
30
|
if (kind === 'solo') { pack.model = args.model; }
|
|
29
31
|
else if (typeof args.bench === 'string') {
|
|
@@ -72,12 +74,18 @@ function packFromCouncilRun(name, version, run) {
|
|
|
72
74
|
};
|
|
73
75
|
}
|
|
74
76
|
|
|
75
|
-
/**
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
77
|
+
/**
|
|
78
|
+
* wave branch: a fanout wave's metadata.json (type:'wave'), plus its first leg.
|
|
79
|
+
* `{fs, path, getSessionDir}` is threaded in from buildPackFromRun's own lazy
|
|
80
|
+
* require (below) rather than required again here — T14-m7: this file's other
|
|
81
|
+
* lazy requires (session-manager per cli-handlers-abort.js/cli-handlers-watch.js/
|
|
82
|
+
* mcp-council-awareness.js; pack-store/pack-validate in handlePack below) are
|
|
83
|
+
* all function-scoped, deferred until the `--from-run` path actually needs
|
|
84
|
+
* them, so a second copy of the same require triple here would just be
|
|
85
|
+
* duplication without buying back any additional laziness — buildPackFromRun
|
|
86
|
+
* already pays that cost once, on the only branch that reaches this function.
|
|
87
|
+
*/
|
|
88
|
+
function packFromWave(name, version, project, meta, { fs, path, getSessionDir }) {
|
|
81
89
|
const opts = {};
|
|
82
90
|
const firstLeg = Array.isArray(meta.legs) ? meta.legs[0] : null;
|
|
83
91
|
if (firstLeg) {
|
|
@@ -114,7 +122,7 @@ function packFromSolo(name, version, meta) {
|
|
|
114
122
|
*/
|
|
115
123
|
function buildPackFromRun(name, id, project, args) {
|
|
116
124
|
const runState = require('./council/run-state');
|
|
117
|
-
const version = args
|
|
125
|
+
const version = args['pack-version'] || '1.0.0'; // see buildPackFromFlags — never args.version
|
|
118
126
|
|
|
119
127
|
const ptr = runState.readPointer(project, id); // {runId, runDir}|null (run-state.js:156)
|
|
120
128
|
const run = ptr ? runState.readRun(ptr.runDir) : null;
|
|
@@ -128,7 +136,9 @@ function buildPackFromRun(name, id, project, args) {
|
|
|
128
136
|
let meta = null;
|
|
129
137
|
try { meta = JSON.parse(fs.readFileSync(path.join(getSessionDir(project, id), 'metadata.json'), 'utf-8')); }
|
|
130
138
|
catch { return { error: `Session ${id} not found` }; }
|
|
131
|
-
pack = meta.type === 'wave'
|
|
139
|
+
pack = meta.type === 'wave'
|
|
140
|
+
? packFromWave(name, version, project, meta, { fs, path, getSessionDir })
|
|
141
|
+
: packFromSolo(name, version, meta);
|
|
132
142
|
}
|
|
133
143
|
// T14-m4 (final-review): buildPackFromFlags honors --description (line ~27
|
|
134
144
|
// above); this --from-run path threaded `version` the same way but silently
|
|
@@ -152,7 +162,6 @@ function renderPackList(doc) {
|
|
|
152
162
|
});
|
|
153
163
|
text = 'Packs:\n' + lines.join('\n') + '\n';
|
|
154
164
|
}
|
|
155
|
-
for (const w of doc.warnings) { text += `Warning: ${w}\n`; }
|
|
156
165
|
return text;
|
|
157
166
|
}
|
|
158
167
|
|
|
@@ -201,6 +210,11 @@ async function handlePack(args) {
|
|
|
201
210
|
const { packs, warnings } = listPacks();
|
|
202
211
|
const doc = { schemaVersion: SCHEMA_VERSION, type: 'pack-list', dir: packsDir(), packs, warnings };
|
|
203
212
|
process.stdout.write(useJson ? JSON.stringify(doc, null, 2) + '\n' : renderPackList(doc));
|
|
213
|
+
// T14-m1: warnings are diagnostics, not data — keep them off stdout so
|
|
214
|
+
// `amicus pack list | grep`/`--json` consumers never see them mixed into
|
|
215
|
+
// the list itself. `pack save` already writes this identical string to
|
|
216
|
+
// stderr (above); `--json` is untouched: `warnings` stays a field on doc.
|
|
217
|
+
if (!useJson) { for (const w of warnings) { process.stderr.write(`Warning: ${w}\n`); } }
|
|
204
218
|
return 0;
|
|
205
219
|
}
|
|
206
220
|
|
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 };
|