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
|
@@ -211,6 +211,11 @@ mangled alias (instant arg-parse failure). For a free council, swap `--models "<
|
|
|
211
211
|
`--council free`. Run it in the background (`run_in_background: true`); you are notified on
|
|
212
212
|
completion — do not poll.
|
|
213
213
|
|
|
214
|
+
**Saved run configs (v4.5).** Every flag above this line can also come from a saved
|
|
215
|
+
[policy pack](../../docs/usage.md#policy-packs) — `--pack <name>` loads its bench/chair/critic/
|
|
216
|
+
lenses/options/template as defaults (an explicit flag still overrides it), the same on the
|
|
217
|
+
`amicus_council_run`/`amicus_fanout`/`amicus_start` MCP tools' `pack` param.
|
|
218
|
+
|
|
214
219
|
**Budget gate — one flag for the whole run.** By default the gate refuses any leg whose price
|
|
215
220
|
exceeds the per-$/Mtok threshold (the o3/o3-pro guard). To run an intentionally expensive model the
|
|
216
221
|
user asked for by name, pass `--no-cost-gate`; to raise only the total ceiling, pass
|
|
@@ -71,9 +71,11 @@ function resolveBench(args, useJson) {
|
|
|
71
71
|
if (expanded.dropped && expanded.dropped.length && !useJson) {
|
|
72
72
|
process.stderr.write(`Notice: dropped unavailable council member(s): ${expanded.dropped.join(', ')}\n`);
|
|
73
73
|
}
|
|
74
|
-
|
|
74
|
+
// v4.5 Wave 2: threaded into runCouncil's options — the ONLY prior signal
|
|
75
|
+
// was the stderr-only Notice above, which --json mode never even prints.
|
|
76
|
+
return { bench: expanded.models, presetName, droppedMembers: expanded.droppedMembers || [] };
|
|
75
77
|
}
|
|
76
|
-
return { bench: parseList(args.models), presetName: null };
|
|
78
|
+
return { bench: parseList(args.models), presetName: null, droppedMembers: [] };
|
|
77
79
|
}
|
|
78
80
|
|
|
79
81
|
function renderRunHuman(run) {
|
|
@@ -115,6 +117,27 @@ function renderRunHuman(run) {
|
|
|
115
117
|
async function handleCouncilRun(args) {
|
|
116
118
|
const useJson = !!args.json;
|
|
117
119
|
|
|
120
|
+
// v4.5 Task 12 (B7/F5): resolve --pack FIRST, above the Task-5 template
|
|
121
|
+
// block, so a pack-filled args.template renders through that single
|
|
122
|
+
// existing application point exactly like a typed --template.
|
|
123
|
+
let packRecord = null;
|
|
124
|
+
const explicitKeys = args.__explicit || new Set();
|
|
125
|
+
if (args.pack !== undefined) {
|
|
126
|
+
const { applyPackToArgs } = require('./pack/pack-resolve');
|
|
127
|
+
const pr = applyPackToArgs({
|
|
128
|
+
packRef: args.pack, expectedKind: 'council', args,
|
|
129
|
+
explicit: explicitKeys, useJson,
|
|
130
|
+
});
|
|
131
|
+
if (pr.error) { return failJson(useJson, pr.error); }
|
|
132
|
+
for (const n of pr.notices) { process.stderr.write(n + '\n'); }
|
|
133
|
+
packRecord = pr.packRecord;
|
|
134
|
+
}
|
|
135
|
+
// 2026-07-28 ruling (Task-11 review): attribute a pre-flight failure to the
|
|
136
|
+
// pack that supplied the failing value — ONLY when the pack filled it (an
|
|
137
|
+
// explicit flag always wins and is never "blamed" on the pack).
|
|
138
|
+
const packSuffix = (key) => (packRecord && args[key] !== undefined && !explicitKeys.has(key))
|
|
139
|
+
? ` (set by pack '${packRecord.name}')` : '';
|
|
140
|
+
|
|
118
141
|
// --prompt-file required; inline --prompt rejected (councils always have
|
|
119
142
|
// real briefings — same rationale as MCP fanout's briefing-via-file).
|
|
120
143
|
if (args.prompt !== undefined) {
|
|
@@ -123,9 +146,26 @@ async function handleCouncilRun(args) {
|
|
|
123
146
|
hint: 'write the briefing to a file and pass --prompt-file <path>' });
|
|
124
147
|
}
|
|
125
148
|
const { resolvePromptSource } = require('./utils/prompt-source');
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
149
|
+
// F9 (v4.5): with --template and no {{prompt}} slot, --prompt-file may be
|
|
150
|
+
// absent (mirrors handleFanout's guard); byte-identical without --template.
|
|
151
|
+
let promptRes;
|
|
152
|
+
if (args.prompt !== undefined || args['prompt-file'] !== undefined || args.template === undefined) {
|
|
153
|
+
promptRes = resolvePromptSource(args);
|
|
154
|
+
if (promptRes.error) { return failJson(useJson, { code: ERROR_CODES.MISSING_PROMPT, message: promptRes.error }); }
|
|
155
|
+
} else {
|
|
156
|
+
promptRes = { prompt: undefined, promptMeta: null };
|
|
157
|
+
}
|
|
158
|
+
let templateMeta = null;
|
|
159
|
+
if (args.template !== undefined) {
|
|
160
|
+
const { applyTemplate } = require('./template/apply');
|
|
161
|
+
const t = applyTemplate({ templateRef: args.template, prompt: promptRes.prompt,
|
|
162
|
+
artifactFile: args.artifact, varList: args.var, project: args.cwd || process.cwd() });
|
|
163
|
+
if (t.error) { return failJson(useJson, t.error); }
|
|
164
|
+
for (const n of t.notices) { process.stderr.write(n + '\n'); }
|
|
165
|
+
promptRes = { prompt: t.prompt, promptMeta: t.promptMeta };
|
|
166
|
+
templateMeta = t.promptMeta.template;
|
|
167
|
+
} else if (args.artifact !== undefined || args.var !== undefined) {
|
|
168
|
+
return failJson(useJson, { code: ERROR_CODES.BAD_ARGS, message: 'Error: --artifact/--var require --template (expansion happens only in template files)' });
|
|
129
169
|
}
|
|
130
170
|
|
|
131
171
|
const benchRes = resolveBench(args, useJson);
|
|
@@ -153,19 +193,19 @@ async function handleCouncilRun(args) {
|
|
|
153
193
|
? args.chair.trim() : CHAIR_DEFAULT;
|
|
154
194
|
if (bench.includes(chair)) {
|
|
155
195
|
return failJson(useJson, { code: ERROR_CODES.BAD_ARGS,
|
|
156
|
-
message: `Error: chair '${chair}' is a bench seat — the chair must not review`,
|
|
196
|
+
message: `Error: chair '${chair}' is a bench seat — the chair must not review${packSuffix('chair')}`,
|
|
157
197
|
hint: `pick a chair outside --models (default: ${CHAIR_DEFAULT}), or remove '${chair}' from the bench` });
|
|
158
198
|
}
|
|
159
199
|
const critic = (typeof args.critic === 'string' && args.critic.trim()) ? args.critic.trim() : null;
|
|
160
200
|
if (critic && !bench.includes(critic)) {
|
|
161
201
|
return failJson(useJson, { code: ERROR_CODES.BAD_ARGS,
|
|
162
|
-
message: `Error: critic '${critic}' must be one of the bench seats`,
|
|
202
|
+
message: `Error: critic '${critic}' must be one of the bench seats${packSuffix('critic')}`,
|
|
163
203
|
hint: `--critic swaps one seat's brief; pass one of: ${bench.join(', ')}` });
|
|
164
204
|
}
|
|
165
205
|
const lenses = (typeof args.lenses === 'string' && args.lenses.trim()) ? parseList(args.lenses) : null;
|
|
166
206
|
if (critic && lenses) {
|
|
167
207
|
return failJson(useJson, { code: ERROR_CODES.BAD_ARGS,
|
|
168
|
-
message:
|
|
208
|
+
message: `Error: --critic and --lenses are mutually exclusive in v4.0${packSuffix('critic') || packSuffix('lenses')}` });
|
|
169
209
|
}
|
|
170
210
|
if (lenses && lenses.length !== bench.length) {
|
|
171
211
|
return failJson(useJson, { code: ERROR_CODES.BAD_ARGS,
|
|
@@ -211,6 +251,9 @@ async function handleCouncilRun(args) {
|
|
|
211
251
|
noValidateModel: !!args['no-validate-model'],
|
|
212
252
|
date: new Date().toISOString().slice(0, 10),
|
|
213
253
|
councilName,
|
|
254
|
+
template: templateMeta, // F9 (v4.5): null when no --template; additive on the run.json seed (run-state.js).
|
|
255
|
+
pack: packRecord, // v4.5 Task 12 (B7/F5): null when no --pack; additive on the run.json seed (run-state.js).
|
|
256
|
+
droppedMembers: benchRes.droppedMembers, // v4.5 Wave 2: [] when nothing dropped; additive on the run.json seed (run-state.js).
|
|
214
257
|
// v4.1 §4.5b/§4.5d. `--claude-review` is resolved here but VALIDATED by the
|
|
215
258
|
// engine's preflightClaudeReview (run-assemble.js): the reserved-seat and
|
|
216
259
|
// 'claude may not chair' guards live there on purpose so MCP, the GitHub
|
|
@@ -7,6 +7,8 @@ const mcpChecks = require('./utils/doctor-mcp-checks');
|
|
|
7
7
|
// engine-mcp check body — verifies the engine in the npx-cache copies the MCP
|
|
8
8
|
// actually launches (bug report #1). Split out to keep this file under the gate.
|
|
9
9
|
const engineCheck = require('./utils/doctor-engine-check');
|
|
10
|
+
// electron-mcp check body (#76) — same blind spot, electron-flavored.
|
|
11
|
+
const electronMcpCheck = require('./utils/doctor-electron-mcp-check');
|
|
10
12
|
// local-providers check body (v4.2 §4.7 C8) — split out to keep this file
|
|
11
13
|
// under the gate (mirrors the engineCheck/mcpChecks split above).
|
|
12
14
|
const localProvidersCheck = require('./utils/doctor-local-providers-check');
|
|
@@ -47,6 +49,8 @@ function realDeps() {
|
|
|
47
49
|
scanEngineInstalls: () => require('./utils/engine-install-scan').scanEngineInstalls(),
|
|
48
50
|
// report #2: copy-from-sibling self-heal for `doctor --fix`.
|
|
49
51
|
repairEngine: (o) => require('./utils/engine-repair').repairEngine(o),
|
|
52
|
+
// electron-mcp check (#76): same enumeration, electron probed per copy.
|
|
53
|
+
scanElectronInstalls: () => electronMcpCheck.scanElectronInstalls(),
|
|
50
54
|
getElectronPath: () => require('./sidecar/interactive-process').getElectronPath(),
|
|
51
55
|
// #56: self-heal primitive for `doctor --fix`. Pure probe (getElectronPath)
|
|
52
56
|
// stays separate; repair only runs when fix is requested.
|
|
@@ -161,6 +165,12 @@ async function runDoctorChecks(depsOverride = {}) {
|
|
|
161
165
|
// install) can't hide a broken copy the MCP would spawn (bug report #1/#4).
|
|
162
166
|
checks.push(await guardAsync('engine-mcp', 'OpenCode engine (MCP launch path)', () => engineCheck.evaluateEngineMcp(d)));
|
|
163
167
|
|
|
168
|
+
// #76: same green-while-broken blind spot for Electron — probe the npx-cache
|
|
169
|
+
// copies `ui: true` actually depends on. fixTimeoutMs forwards the #56
|
|
170
|
+
// never-hang guard to the per-copy repairElectron calls.
|
|
171
|
+
checks.push(await guardAsync('electron-mcp', 'Electron (MCP launch path)',
|
|
172
|
+
() => electronMcpCheck.evaluateElectronMcp({ ...d, fixTimeoutMs: FIX_TIMEOUT_MS })));
|
|
173
|
+
|
|
164
174
|
checks.push(await guardAsync('electron', 'Electron (interactive GUI)', async () => {
|
|
165
175
|
if (d.getElectronPath()) {
|
|
166
176
|
return { id: 'electron', name: 'Electron (interactive GUI)', status: 'ok', message: 'installed', hint: null };
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
// src/cli-handlers-pack.js
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @module cli-handlers-pack
|
|
6
|
+
* F5/B7 (v4.5), Task 14: `amicus pack save|list|show|rm` + `--from-run`. The
|
|
7
|
+
* user-facing management CLI on top of the pack subsystem built by Tasks 7-13
|
|
8
|
+
* (pack-store, pack-validate, pack-resolve, --pack on the three run commands).
|
|
9
|
+
* Idiom donor: src/cli-handlers-template.js (doc object -> useJson ?
|
|
10
|
+
* JSON.stringify : render) and src/council/presets-cli.js (list/show render
|
|
11
|
+
* shape, `amicus council show`'s diagnostic-mirror style for `pack show`).
|
|
12
|
+
*
|
|
13
|
+
* `pack save <name> --from-run <id>` builds a pack from an existing council
|
|
14
|
+
* run / fanout wave / solo session instead of flags (buildPackFromRun below).
|
|
15
|
+
* Resolution order: council pointer (run-state.js) -> wave metadata.json ->
|
|
16
|
+
* solo metadata.json. Briefing TEXT is never captured, only a template
|
|
17
|
+
* reference when the source run recorded one.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const { SCHEMA_VERSION } = require('./utils/result-schema');
|
|
21
|
+
const { ERROR_CODES, failJson } = require('./utils/error-doc');
|
|
22
|
+
|
|
23
|
+
/** Build a pack object from `pack save <name> --kind ... <flags>`. */
|
|
24
|
+
function buildPackFromFlags(name, args) {
|
|
25
|
+
const kind = args.kind;
|
|
26
|
+
const pack = { schemaVersion: 1, type: 'pack', name, version: args.version || '1.0.0', kind };
|
|
27
|
+
if (args.description) { pack.description = String(args.description); }
|
|
28
|
+
if (kind === 'solo') { pack.model = args.model; }
|
|
29
|
+
else if (typeof args.bench === 'string') {
|
|
30
|
+
// A comma present means an explicit member list; a single comma-less
|
|
31
|
+
// value is a council/bench NAME string (validatePack {mode:'save'} is
|
|
32
|
+
// the net that catches an unresolvable one of either form).
|
|
33
|
+
pack.bench = args.bench.includes(',')
|
|
34
|
+
? args.bench.split(',').map((s) => s.trim()).filter(Boolean)
|
|
35
|
+
: args.bench.trim();
|
|
36
|
+
}
|
|
37
|
+
if (kind === 'council') {
|
|
38
|
+
if (args.chair) { pack.chair = args.chair; }
|
|
39
|
+
if (args.critic) { pack.critic = args.critic; }
|
|
40
|
+
if (typeof args.lenses === 'string') { pack.lenses = args.lenses.split(',').map((s) => s.trim()).filter(Boolean); }
|
|
41
|
+
}
|
|
42
|
+
const opts = {};
|
|
43
|
+
if (args.__explicit.has('timeout')) { opts.timeout = args.timeout; }
|
|
44
|
+
if (args['max-cost'] !== undefined) { opts.maxCost = args['max-cost']; }
|
|
45
|
+
if (args.gateway) { opts.gateway = args.gateway; }
|
|
46
|
+
if (args.agent) { opts.agent = args.agent; }
|
|
47
|
+
if (args.thinking) { opts.thinking = args.thinking; }
|
|
48
|
+
if (args.__explicit.has('summary-length')) { opts.summaryLength = args['summary-length']; }
|
|
49
|
+
// Task-10 ruling (pack-resolve.js:116): a negatable boolean's "was this
|
|
50
|
+
// explicit" check must test BOTH forms, since __explicit only records the
|
|
51
|
+
// literal typed key (--no-debate never sets args.debate to anything).
|
|
52
|
+
if (args.__explicit.has('debate') || args.__explicit.has('no-debate')) { opts.debate = !!args.debate; }
|
|
53
|
+
if (Object.keys(opts).length) { pack.options = opts; }
|
|
54
|
+
if (args.template) { pack.briefing = { template: String(args.template) }; }
|
|
55
|
+
return pack;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** council branch of buildPackFromRun: a run.json reached via the council pointer. */
|
|
59
|
+
function packFromCouncilRun(name, version, run) {
|
|
60
|
+
const opts = {};
|
|
61
|
+
if (run.options && run.options.timeout) { opts.timeout = run.options.timeout; }
|
|
62
|
+
if (run.options && run.options.maxCost !== undefined && run.options.maxCost !== null) { opts.maxCost = run.options.maxCost; }
|
|
63
|
+
if (run.options && run.options.gateway) { opts.gateway = run.options.gateway; }
|
|
64
|
+
if (run.debate && run.debate.enabled) { opts.debate = true; }
|
|
65
|
+
return {
|
|
66
|
+
schemaVersion: 1, type: 'pack', name, version, kind: 'council',
|
|
67
|
+
bench: run.bench.slice(), chair: run.chair,
|
|
68
|
+
...(run.critic ? { critic: run.critic } : {}),
|
|
69
|
+
...(run.lenses ? { lenses: run.lenses.slice() } : {}),
|
|
70
|
+
...(Object.keys(opts).length ? { options: opts } : {}),
|
|
71
|
+
...(run.template ? { briefing: { template: run.template.name } } : {}),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** wave branch: a fanout wave's metadata.json (type:'wave'), plus its first leg. */
|
|
76
|
+
function packFromWave(name, version, project, meta) {
|
|
77
|
+
const fs = require('fs');
|
|
78
|
+
const path = require('path');
|
|
79
|
+
const { getSessionDir } = require('./session-manager');
|
|
80
|
+
|
|
81
|
+
const opts = {};
|
|
82
|
+
const firstLeg = Array.isArray(meta.legs) ? meta.legs[0] : null;
|
|
83
|
+
if (firstLeg) {
|
|
84
|
+
try {
|
|
85
|
+
const legMeta = JSON.parse(fs.readFileSync(path.join(getSessionDir(project, firstLeg), 'metadata.json'), 'utf-8'));
|
|
86
|
+
if (legMeta.agent) { opts.agent = legMeta.agent; }
|
|
87
|
+
if (legMeta.thinking) { opts.thinking = legMeta.thinking; }
|
|
88
|
+
} catch { /* leg metadata unreadable/absent — omit, never null-stuff */ }
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
schemaVersion: 1, type: 'pack', name, version, kind: 'fanout',
|
|
92
|
+
bench: meta.models.slice(),
|
|
93
|
+
...(Object.keys(opts).length ? { options: opts } : {}),
|
|
94
|
+
...(meta.promptMeta && meta.promptMeta.template ? { briefing: { template: meta.promptMeta.template.name } } : {}),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** solo branch: an ordinary (non-wave) session's metadata.json. */
|
|
99
|
+
function packFromSolo(name, version, meta) {
|
|
100
|
+
const opts = {};
|
|
101
|
+
if (meta.agent) { opts.agent = meta.agent; }
|
|
102
|
+
if (meta.thinking) { opts.thinking = meta.thinking; }
|
|
103
|
+
return {
|
|
104
|
+
schemaVersion: 1, type: 'pack', name, version, kind: 'solo',
|
|
105
|
+
model: meta.model,
|
|
106
|
+
...(Object.keys(opts).length ? { options: opts } : {}),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Build a pack object from `pack save <name> --from-run <id>`. Resolution
|
|
112
|
+
* order: council pointer/run.json -> wave metadata.json -> solo metadata.json.
|
|
113
|
+
* @returns {object} a pack object, or `{error: string}` when `id` resolves to nothing.
|
|
114
|
+
*/
|
|
115
|
+
function buildPackFromRun(name, id, project, args) {
|
|
116
|
+
const runState = require('./council/run-state');
|
|
117
|
+
const version = args.version || '1.0.0';
|
|
118
|
+
|
|
119
|
+
const ptr = runState.readPointer(project, id); // {runId, runDir}|null (run-state.js:156)
|
|
120
|
+
const run = ptr ? runState.readRun(ptr.runDir) : null;
|
|
121
|
+
let pack;
|
|
122
|
+
if (run && run.type === 'council-run') {
|
|
123
|
+
pack = packFromCouncilRun(name, version, run);
|
|
124
|
+
} else {
|
|
125
|
+
const fs = require('fs');
|
|
126
|
+
const path = require('path');
|
|
127
|
+
const { getSessionDir } = require('./session-manager');
|
|
128
|
+
let meta = null;
|
|
129
|
+
try { meta = JSON.parse(fs.readFileSync(path.join(getSessionDir(project, id), 'metadata.json'), 'utf-8')); }
|
|
130
|
+
catch { return { error: `Session ${id} not found` }; }
|
|
131
|
+
pack = meta.type === 'wave' ? packFromWave(name, version, project, meta) : packFromSolo(name, version, meta);
|
|
132
|
+
}
|
|
133
|
+
// T14-m4 (final-review): buildPackFromFlags honors --description (line ~27
|
|
134
|
+
// above); this --from-run path threaded `version` the same way but silently
|
|
135
|
+
// dropped --description. Honor it here too, regardless of which branch
|
|
136
|
+
// (council/wave/solo) produced the pack.
|
|
137
|
+
if (args.description) { pack.description = String(args.description); }
|
|
138
|
+
return pack;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function renderPackList(doc) {
|
|
142
|
+
let text = '';
|
|
143
|
+
if (doc.packs.length === 0) {
|
|
144
|
+
text = 'No packs.\n';
|
|
145
|
+
} else {
|
|
146
|
+
const lines = doc.packs.map((p) => {
|
|
147
|
+
const name = String(p.name || '(unnamed)').padEnd(20);
|
|
148
|
+
const kind = String(p.kind || '(unknown)');
|
|
149
|
+
const version = String(p.version || '0.0.0');
|
|
150
|
+
const desc = p.description ? ` — ${p.description}` : '';
|
|
151
|
+
return ` ${name} [${kind}] v${version}${desc}`;
|
|
152
|
+
});
|
|
153
|
+
text = 'Packs:\n' + lines.join('\n') + '\n';
|
|
154
|
+
}
|
|
155
|
+
for (const w of doc.warnings) { text += `Warning: ${w}\n`; }
|
|
156
|
+
return text;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function renderPackShow(doc) {
|
|
160
|
+
const { pack, path: file, source, hash, validation } = doc;
|
|
161
|
+
const lines = [`Pack '${pack.name}' v${pack.version} [${pack.kind}] (${source}: ${file})`, ` hash: ${hash}`];
|
|
162
|
+
if (pack.description) { lines.push(` description: ${pack.description}`); }
|
|
163
|
+
if (pack.kind === 'solo') { lines.push(` model: ${pack.model}`); }
|
|
164
|
+
else { lines.push(` bench: ${Array.isArray(pack.bench) ? pack.bench.join(', ') : pack.bench}`); }
|
|
165
|
+
if (pack.chair) { lines.push(` chair: ${pack.chair}`); }
|
|
166
|
+
if (pack.critic) { lines.push(` critic: ${pack.critic}`); }
|
|
167
|
+
if (pack.lenses) { lines.push(` lenses: ${pack.lenses.join(', ')}`); }
|
|
168
|
+
if (pack.options && Object.keys(pack.options).length) { lines.push(` options: ${JSON.stringify(pack.options)}`); }
|
|
169
|
+
if (pack.briefing && pack.briefing.template) { lines.push(` briefing.template: ${pack.briefing.template}`); }
|
|
170
|
+
lines.push(validation.ok ? ' validation: ok' : ` validation: INVALID — ${validation.errors.join('; ')}`);
|
|
171
|
+
if (validation.ok && validation.warnings.length) { lines.push(` warnings: ${validation.warnings.join('; ')}`); }
|
|
172
|
+
return lines.join('\n') + '\n';
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** @param {object} args parsed CLI args @returns {Promise<number>} exit code */
|
|
176
|
+
async function handlePack(args) {
|
|
177
|
+
const useJson = !!args.json;
|
|
178
|
+
const sub = args._[1];
|
|
179
|
+
const { readPack, writePack, listPacks, rmPack, packsDir } = require('./pack/pack-store');
|
|
180
|
+
const { validatePack } = require('./pack/pack-validate');
|
|
181
|
+
|
|
182
|
+
if (sub === 'save') {
|
|
183
|
+
const name = args._[2];
|
|
184
|
+
if (!name) { return failJson(useJson, { code: ERROR_CODES.BAD_ARGS, message: 'Error: pack save needs a <name>' }); }
|
|
185
|
+
const pack = args['from-run']
|
|
186
|
+
? buildPackFromRun(name, String(args['from-run']), args.cwd || process.cwd(), args)
|
|
187
|
+
: buildPackFromFlags(name, args);
|
|
188
|
+
if (pack.error) { return failJson(useJson, { code: ERROR_CODES.BAD_SESSION, message: `Error: ${pack.error}` }); }
|
|
189
|
+
const v = validatePack(pack, { mode: 'save' });
|
|
190
|
+
if (!v.ok) { return failJson(useJson, { code: ERROR_CODES.PACK_INVALID, message: `Error: invalid pack: ${v.errors.join('; ')}` }); }
|
|
191
|
+
for (const w of v.warnings) { process.stderr.write(`Warning: ${w}\n`); }
|
|
192
|
+
const res = writePack(pack);
|
|
193
|
+
const doc = { schemaVersion: SCHEMA_VERSION, type: 'pack-save', name, ...res };
|
|
194
|
+
process.stdout.write(useJson ? JSON.stringify(doc, null, 2) + '\n'
|
|
195
|
+
: (res.noop ? `Pack '${name}' unchanged (no-op).\n`
|
|
196
|
+
: `Saved pack '${name}' v${res.version}${res.bumped ? ' (version auto-bumped)' : ''} → ${res.path}\n`));
|
|
197
|
+
return 0;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (sub === 'list') {
|
|
201
|
+
const { packs, warnings } = listPacks();
|
|
202
|
+
const doc = { schemaVersion: SCHEMA_VERSION, type: 'pack-list', dir: packsDir(), packs, warnings };
|
|
203
|
+
process.stdout.write(useJson ? JSON.stringify(doc, null, 2) + '\n' : renderPackList(doc));
|
|
204
|
+
return 0;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (sub === 'show') {
|
|
208
|
+
const ref = args._[2];
|
|
209
|
+
if (!ref) { return failJson(useJson, { code: ERROR_CODES.BAD_ARGS, message: 'Error: pack show needs a <name|path>' }); }
|
|
210
|
+
const r = readPack(ref);
|
|
211
|
+
if (r.error) { return failJson(useJson, { code: ERROR_CODES.PACK_NOT_FOUND, message: r.error, hint: 'amicus pack list' }); }
|
|
212
|
+
// {mode:'save'} semantics per spec §5.3: reports, never fails — the
|
|
213
|
+
// `council show` diagnostic mirror. Run-time-only checks (e.g. an
|
|
214
|
+
// unresolvable briefing.template) are demoted to a warning in this mode.
|
|
215
|
+
const validation = validatePack(r.pack, { mode: 'save' });
|
|
216
|
+
const doc = { schemaVersion: SCHEMA_VERSION, type: 'pack-show', ...r, validation };
|
|
217
|
+
process.stdout.write(useJson ? JSON.stringify(doc, null, 2) + '\n' : renderPackShow(doc));
|
|
218
|
+
return 0;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (sub === 'rm') {
|
|
222
|
+
const name = args._[2];
|
|
223
|
+
if (!name) { return failJson(useJson, { code: ERROR_CODES.BAD_ARGS, message: 'Error: pack rm needs a <name>' }); }
|
|
224
|
+
const res = rmPack(name);
|
|
225
|
+
// v4.5 HOLD-gate decision 3: unified with `pack show`'s missing-pack code
|
|
226
|
+
// (same hint idiom) — was BAD_ARGS, so a script switching on error.code got
|
|
227
|
+
// two different answers for the identical "no such pack" condition.
|
|
228
|
+
if (!res.removed) { return failJson(useJson, { code: ERROR_CODES.PACK_NOT_FOUND, message: `Error: pack '${name}' not found`, hint: 'amicus pack list' }); }
|
|
229
|
+
const doc = { schemaVersion: SCHEMA_VERSION, type: 'pack-rm', name, removed: true };
|
|
230
|
+
process.stdout.write(useJson ? JSON.stringify(doc, null, 2) + '\n' : `Removed pack '${name}'.\n`);
|
|
231
|
+
return 0;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return failJson(useJson, { code: ERROR_CODES.BAD_ARGS,
|
|
235
|
+
message: `Error: unknown pack subcommand '${sub || ''}'`, hint: 'amicus pack save|list|show|rm' });
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
module.exports = { handlePack };
|
package/src/cli-handlers-run.js
CHANGED
|
@@ -25,18 +25,29 @@ const { GATEWAY_MODES } = require('./utils/model-descriptor');
|
|
|
25
25
|
*/
|
|
26
26
|
async function handleStart(args) {
|
|
27
27
|
const useJson = !!args.json;
|
|
28
|
-
|
|
28
|
+
const packRecord = require('./pack/pack-cli').applyPackOrExit(args, 'solo', useJson);
|
|
29
29
|
// F4: --prompt-file support (XOR --prompt) and --json gating
|
|
30
|
+
// F9 (v4.5): --template renders {{prompt}}/{{artifact}}/{{var.*}} into the prompt; byte-identical without it.
|
|
31
|
+
let templateMeta = null;
|
|
30
32
|
if (args.prompt !== undefined || args['prompt-file'] !== undefined) {
|
|
31
33
|
const { resolvePromptSource } = require('./utils/prompt-source');
|
|
32
34
|
const promptRes = resolvePromptSource(args);
|
|
33
35
|
if (promptRes.error) { process.exit(failJson(useJson, { code: ERROR_CODES.MISSING_PROMPT, message: promptRes.error })); }
|
|
34
36
|
args.prompt = promptRes.prompt;
|
|
35
|
-
// Drop --prompt-file
|
|
36
|
-
// guard would otherwise re-run resolvePromptSource with both prompt and
|
|
37
|
-
// prompt-file set and trip its mutually-exclusive branch.
|
|
37
|
+
// Drop --prompt-file post-resolve or validateStartArgs re-trips its XOR guard.
|
|
38
38
|
delete args['prompt-file'];
|
|
39
39
|
}
|
|
40
|
+
if (args.template !== undefined) {
|
|
41
|
+
const { applyTemplate } = require('./template/apply');
|
|
42
|
+
const t = applyTemplate({ templateRef: args.template, prompt: args.prompt,
|
|
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
|
+
}
|
|
40
51
|
requireNoUiForJson(args, useJson);
|
|
41
52
|
|
|
42
53
|
const mc = args['max-cost'];
|
|
@@ -105,6 +116,8 @@ async function handleStart(args) {
|
|
|
105
116
|
position: args.position,
|
|
106
117
|
json: !!args.json,
|
|
107
118
|
modelInput: alias || null,
|
|
119
|
+
template: templateMeta, // F9 (v4.5): startSidecar ignores unknown keys; inert until a future task reads it.
|
|
120
|
+
pack: packRecord, // v4.5 Task 13: null when no --pack; additively recorded on solo session metadata.
|
|
108
121
|
});
|
|
109
122
|
}
|
|
110
123
|
|
|
@@ -130,6 +143,7 @@ async function handleFanout(args) {
|
|
|
130
143
|
if (errorDoc && useJson) { process.stdout.write(JSON.stringify(errorDoc) + '\n'); }
|
|
131
144
|
return exitCode;
|
|
132
145
|
}
|
|
146
|
+
const packRecord = require('./pack/pack-cli').applyPackOrExit(args, 'fanout', useJson);
|
|
133
147
|
|
|
134
148
|
// FIX 4 (#61 whole-branch review, cheap parity): handleStart validates
|
|
135
149
|
// --gateway via validateStartArgs (cli.js) — fanout never did, so a typo'd
|
|
@@ -140,9 +154,22 @@ async function handleFanout(args) {
|
|
|
140
154
|
}
|
|
141
155
|
|
|
142
156
|
const { resolvePromptSource } = require('./utils/prompt-source');
|
|
143
|
-
|
|
144
|
-
if (
|
|
145
|
-
|
|
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)' }));
|
|
146
173
|
}
|
|
147
174
|
// Council preset: expand a saved council into args.models (mutually exclusive with --models).
|
|
148
175
|
const hasModels = typeof args.models === 'string' && args.models.trim();
|
|
@@ -190,7 +217,7 @@ async function handleFanout(args) {
|
|
|
190
217
|
process.exit(failJson(useJson, { code: ERROR_CODES.BAD_ARGS, message: 'Error: --models must contain at least one non-empty entry' }));
|
|
191
218
|
}
|
|
192
219
|
|
|
193
|
-
// Direct require —
|
|
220
|
+
// Direct require (fanout stays internal — no src/index.js public re-export).
|
|
194
221
|
const { runFanout } = require('./sidecar/fanout');
|
|
195
222
|
const { loadConfig, resolveGatewayMode } = require('./utils/config');
|
|
196
223
|
const { resolveFallbackConfig } = require('./sidecar/fallback-chains');
|
|
@@ -237,6 +264,7 @@ async function handleFanout(args) {
|
|
|
237
264
|
config: cfg,
|
|
238
265
|
}),
|
|
239
266
|
catalog: (readCache() || {}).models || [],
|
|
267
|
+
pack: packRecord, // v4.5 Task 13: null when no --pack; additive on wave metadata.json + wave.json.
|
|
240
268
|
});
|
|
241
269
|
return exitCode;
|
|
242
270
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// src/cli-handlers-template.js
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @module cli-handlers-template
|
|
6
|
+
* F9 (v4.5): `amicus template list|show`. No save/rm — templates are a folder
|
|
7
|
+
* of Markdown files; your editor is the manager.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const { SCHEMA_VERSION } = require('./utils/result-schema');
|
|
11
|
+
const { ERROR_CODES, failJson } = require('./utils/error-doc');
|
|
12
|
+
const { listTemplates, resolveTemplate, templatesDir } = require('./template/store');
|
|
13
|
+
|
|
14
|
+
async function handleTemplate(args) {
|
|
15
|
+
const useJson = !!args.json;
|
|
16
|
+
const sub = args._[1];
|
|
17
|
+
|
|
18
|
+
if (sub === 'list') {
|
|
19
|
+
const templates = listTemplates();
|
|
20
|
+
if (useJson) {
|
|
21
|
+
process.stdout.write(JSON.stringify({ schemaVersion: SCHEMA_VERSION, type: 'template-list', dir: templatesDir(), templates }, null, 2) + '\n');
|
|
22
|
+
return 0;
|
|
23
|
+
}
|
|
24
|
+
if (templates.length === 0) { process.stdout.write('No templates.\n'); return 0; }
|
|
25
|
+
for (const t of templates) {
|
|
26
|
+
const marker = t.builtin ? ' [built-in]' : (t.shadowed ? ' [shadows built-in]' : '');
|
|
27
|
+
process.stdout.write(`${t.name}${marker}\n`);
|
|
28
|
+
}
|
|
29
|
+
return 0;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (sub === 'show') {
|
|
33
|
+
const ref = args._[2];
|
|
34
|
+
if (!ref) {
|
|
35
|
+
return failJson(useJson, { code: ERROR_CODES.BAD_ARGS, message: 'Error: template show requires a <name|path>' });
|
|
36
|
+
}
|
|
37
|
+
const t = resolveTemplate(ref);
|
|
38
|
+
if (t.error) {
|
|
39
|
+
return failJson(useJson, { code: ERROR_CODES.TEMPLATE_NOT_FOUND, message: t.error, hint: 'amicus template list' });
|
|
40
|
+
}
|
|
41
|
+
if (useJson) {
|
|
42
|
+
process.stdout.write(JSON.stringify({ schemaVersion: SCHEMA_VERSION, type: 'template', name: t.name, path: t.path, hash: t.hash, builtin: t.builtin, text: t.text }, null, 2) + '\n');
|
|
43
|
+
return 0;
|
|
44
|
+
}
|
|
45
|
+
process.stdout.write(t.text + (t.text.endsWith('\n') ? '' : '\n'));
|
|
46
|
+
return 0;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return failJson(useJson, { code: ERROR_CODES.BAD_ARGS,
|
|
50
|
+
message: `Error: unknown template subcommand '${sub || ''}'`, hint: 'amicus template list | amicus template show <name|path>' });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
module.exports = { handleTemplate };
|