amicus 4.4.0 → 4.5.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 +162 -0
- package/README.md +17 -2
- package/bin/amicus.js +10 -0
- package/docs/DISTRIBUTION.md +234 -0
- package/docs/ROADMAP.md +226 -0
- package/docs/SHIMS.md +62 -0
- package/docs/architecture.md +104 -0
- package/docs/configuration.md +395 -0
- package/docs/council.md +970 -0
- package/docs/doc-system.md +92 -0
- package/docs/electron-testing.md +471 -0
- package/docs/jsdoc-setup.md +75 -0
- package/docs/opencode-integration.md +114 -0
- package/docs/publishing.md +60 -0
- package/docs/schemas.md +56 -0
- package/docs/testing.md +589 -0
- package/docs/troubleshooting.md +298 -0
- package/docs/usage.md +849 -0
- package/electron/fold.js +1 -1
- package/electron/main.js +4 -1
- package/electron/setup-ui-aliases.js +6 -6
- package/electron/workspace-ui/live-model.js +12 -1
- package/electron/workspace-ui/md-lite.js +52 -8
- package/electron/workspace-ui/workspace-app.js +39 -17
- package/electron/workspace-ui/workspace-matrix.js +46 -9
- package/electron/workspace-ui/workspace-panels.js +88 -19
- package/electron/workspace-ui/workspace-render.js +17 -1
- package/electron/workspace-ui/workspace-verbs.js +48 -2
- package/package.json +8 -3
- package/schemas/council-run-live.schema.json +1 -1
- package/schemas/council-run.schema.json +34 -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 +13 -1
- package/schemas/run-live.schema.json +1 -1
- package/schemas/run.schema.json +2 -1
- package/schemas/spend.schema.json +52 -4
- 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-pack.js +238 -0
- package/src/cli-handlers-run.js +36 -8
- package/src/cli-handlers-spend.js +20 -2
- package/src/cli-handlers-template.js +53 -0
- package/src/cli-handlers-watch.js +11 -0
- package/src/cli.js +68 -5
- package/src/council/briefings-debate.js +27 -7
- package/src/council/briefings-stage2.js +155 -25
- package/src/council/briefings.js +24 -1
- package/src/council/findings.js +199 -9
- package/src/council/parse-stage2.js +10 -2
- package/src/council/presets-cli.js +23 -11
- package/src/council/report.js +19 -8
- package/src/council/run-assemble.js +42 -1
- package/src/council/run-budget.js +64 -11
- package/src/council/run-chair.js +4 -1
- package/src/council/run-debate.js +4 -2
- package/src/council/run-finalize.js +102 -0
- package/src/council/run-launch.js +29 -1
- package/src/council/run-server.js +248 -0
- package/src/council/run-stage2.js +118 -0
- package/src/council/run-stages.js +134 -110
- package/src/council/run-state.js +40 -1
- package/src/council/run.js +45 -47
- package/src/council/tally.js +10 -0
- package/src/headless.js +180 -7
- package/src/mcp-council-run.js +108 -4
- package/src/mcp-server.js +203 -7
- package/src/mcp-tools.js +15 -5
- package/src/observe/council-legs.js +60 -3
- package/src/observe/live-doc.js +18 -1
- package/src/observe/watch-render.js +4 -1
- 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/child-sessions.js +1 -2
- package/src/sidecar/fanout-leg-fallback.js +69 -21
- package/src/sidecar/fanout-leg.js +6 -0
- package/src/sidecar/fanout-signals.js +61 -0
- package/src/sidecar/fanout-wave-io.js +75 -0
- package/src/sidecar/fanout.js +82 -74
- package/src/sidecar/progress-fields.js +26 -4
- package/src/sidecar/progress.js +42 -1
- package/src/sidecar/session-utils.js +23 -14
- package/src/sidecar/start.js +5 -4
- package/src/sidecar/workspace-auto-open.js +69 -0
- package/src/sidecar/workspace-window.js +46 -1
- package/src/spend-query.js +17 -5
- 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/error-doc.js +5 -0
- package/src/utils/lifecycle.js +37 -1
- package/src/utils/path-fence.js +39 -1
- package/src/utils/pricing.js +26 -10
- package/src/utils/result-schema-rebuild.js +1 -0
- package/src/utils/result-schema.js +8 -2
- package/src/utils/server-setup.js +79 -1
- package/src/utils/spend-ledger.js +24 -3
- package/src/workspace/artifact-guard.js +66 -7
- package/src/workspace/fold-format.js +33 -4
- package/src/workspace/live-normalize.js +28 -15
- package/src/workspace/run-detail.js +13 -1
|
@@ -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
|
}
|
|
@@ -50,15 +50,22 @@ function parseSinceDays(since) {
|
|
|
50
50
|
* the arithmetic actually saw. It exists so the renderers can say "$X plus N
|
|
51
51
|
* unknown" instead of coercing null→0 and printing a measured-looking $0.0000
|
|
52
52
|
* (diagnosis §8, final paragraph).
|
|
53
|
+
*
|
|
54
|
+
* v4.4.1 CA-2: `unattributedSubtreeRows` is its sibling for the OTHER way the
|
|
55
|
+
* figure understates. Such a row is fully priced — it is in `amount`, it is in
|
|
56
|
+
* the `r` bucket, `unpricedRows` never sees it — but the leg spawned a child
|
|
57
|
+
* session whose spend the walk could not determine, which is exactly what makes
|
|
58
|
+
* `council run` report `costExact: false` about the same dollars. Without this
|
|
59
|
+
* counter `amicus spend` disagreed with `council run` and looked complete.
|
|
53
60
|
* @param {Array<object>} rows
|
|
54
61
|
*/
|
|
55
62
|
function aggregateSpend(rows) {
|
|
56
|
-
const total = { amount: 0, tokens: emptyTokens(), runs: rows.length, unpricedRows: 0, sourceMix: { reported: 0, estimated: 0, unknown: 0 } };
|
|
63
|
+
const total = { amount: 0, tokens: emptyTokens(), runs: rows.length, unpricedRows: 0, unattributedSubtreeRows: 0, sourceMix: { reported: 0, estimated: 0, unknown: 0 } };
|
|
57
64
|
const byModelMap = new Map();
|
|
58
65
|
for (const r of rows) {
|
|
59
66
|
const model = r.model || 'unknown';
|
|
60
67
|
if (!byModelMap.has(model)) {
|
|
61
|
-
byModelMap.set(model, { model, amount: 0, tokens: emptyTokens(), runs: 0, unpricedRows: 0, sourceMix: { reported: 0, estimated: 0, unknown: 0 } });
|
|
68
|
+
byModelMap.set(model, { model, amount: 0, tokens: emptyTokens(), runs: 0, unpricedRows: 0, unattributedSubtreeRows: 0, sourceMix: { reported: 0, estimated: 0, unknown: 0 } });
|
|
62
69
|
}
|
|
63
70
|
const bucket = byModelMap.get(model);
|
|
64
71
|
bucket.runs += 1;
|
|
@@ -68,6 +75,8 @@ function aggregateSpend(rows) {
|
|
|
68
75
|
const priced = typeof cost.amount === 'number';
|
|
69
76
|
const amount = priced ? cost.amount : 0;
|
|
70
77
|
if (!priced) { bucket.unpricedRows += 1; total.unpricedRows += 1; }
|
|
78
|
+
// Beside, never instead of: a row can be both unpriced and subtree-unknown.
|
|
79
|
+
if (r.subtreeUnknown) { bucket.unattributedSubtreeRows += 1; total.unattributedSubtreeRows += 1; }
|
|
71
80
|
bucket.amount += amount;
|
|
72
81
|
total.amount += amount;
|
|
73
82
|
// Any source string outside {reported,estimated} buckets as unknown —
|
|
@@ -178,6 +187,15 @@ function renderHuman({ total, byModel, windowDays, credit, wasted }) {
|
|
|
178
187
|
out += `${total.unpricedRows} unpriced row(s) — cost unknown and NOT in the total; `
|
|
179
188
|
+ 'real spend is at least this much.\n';
|
|
180
189
|
}
|
|
190
|
+
// v4.4.1 CA-2: a DIFFERENT statement from the line above, and deliberately
|
|
191
|
+
// worded as one. "We could not see this leg at all" (unpriced) vs "we saw this
|
|
192
|
+
// leg, and its own cost IS in the total, but not what it spawned" — the second
|
|
193
|
+
// is why `council run` reports costExact:false on a row that looks complete
|
|
194
|
+
// here. Both lines can appear at once; a row can be in both counts.
|
|
195
|
+
if (total.unattributedSubtreeRows > 0) {
|
|
196
|
+
out += `${total.unattributedSubtreeRows} row(s) spawned a subagent whose CHILD session `
|
|
197
|
+
+ 'spend could NOT be determined — real spend is HIGHER than this total.\n';
|
|
198
|
+
}
|
|
181
199
|
if (wasted && wasted.runs > 0) {
|
|
182
200
|
out += `Wasted (failed runs): ${formatCost({ amount: wasted.amount, source: 'mixed' })} across ${wasted.runs} rows — see amicus spend --failed\n`;
|
|
183
201
|
}
|
|
@@ -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 };
|
|
@@ -84,6 +84,17 @@ async function handleWatch(args) {
|
|
|
84
84
|
// at the wrong directory when a run was launched elsewhere.
|
|
85
85
|
const project = args.project || args.cwd || process.cwd();
|
|
86
86
|
const runId = args._[1] ? String(args._[1]) : '';
|
|
87
|
+
// ⚠️ v4.4.1 DOC-3: validate HERE, not by moving the branch. The branch's
|
|
88
|
+
// POSITION above the `id is required` gate is load-bearing (bare `--ui`
|
|
89
|
+
// opens the run-list landing and must keep working), so the runId check is
|
|
90
|
+
// pushed inside it and made conditional on a runId actually being supplied.
|
|
91
|
+
// Without this, `amicus watch <typo> --ui` skipped validateTaskId entirely
|
|
92
|
+
// and surfaced whatever getRunDetail produced — a vaguer error than the
|
|
93
|
+
// identical typo gets on the terminal path.
|
|
94
|
+
if (runId) {
|
|
95
|
+
const uiCheck = validateTaskId(runId);
|
|
96
|
+
if (!uiCheck.valid) { process.stderr.write(`${uiCheck.error}\n`); return 1; }
|
|
97
|
+
}
|
|
87
98
|
const { launchWorkspaceWindow } = require('./sidecar/workspace-window');
|
|
88
99
|
const res = await launchWorkspaceWindow({ project, runId });
|
|
89
100
|
if (res.error) { process.stderr.write(`${res.error}\n`); }
|
package/src/cli.js
CHANGED
|
@@ -44,6 +44,7 @@ function parseArgs(argv) {
|
|
|
44
44
|
_: [],
|
|
45
45
|
...DEFAULTS
|
|
46
46
|
};
|
|
47
|
+
result.__explicit = new Set();
|
|
47
48
|
|
|
48
49
|
for (let i = 0; i < argv.length; i++) {
|
|
49
50
|
const arg = argv[i];
|
|
@@ -63,19 +64,22 @@ function parseArgs(argv) {
|
|
|
63
64
|
// Boolean flags (no value expected)
|
|
64
65
|
if (isBooleanFlag(key)) {
|
|
65
66
|
result[key] = true;
|
|
67
|
+
result.__explicit.add(key);
|
|
66
68
|
continue;
|
|
67
69
|
}
|
|
68
70
|
|
|
69
71
|
// If --key=value was used, use the inline value directly
|
|
70
72
|
if (inlineValue !== undefined) {
|
|
71
73
|
result[key] = parseValue(key, inlineValue);
|
|
74
|
+
result.__explicit.add(key);
|
|
72
75
|
continue;
|
|
73
76
|
}
|
|
74
77
|
|
|
75
78
|
// Array accumulation flags
|
|
76
|
-
if (key === 'exclude-mcp' && next && !next.startsWith('--')) {
|
|
77
|
-
result[
|
|
78
|
-
result[
|
|
79
|
+
if ((key === 'exclude-mcp' || key === 'var') && next && !next.startsWith('--')) {
|
|
80
|
+
result[key] = result[key] || [];
|
|
81
|
+
result[key].push(next);
|
|
82
|
+
result.__explicit.add(key);
|
|
79
83
|
i++;
|
|
80
84
|
continue;
|
|
81
85
|
}
|
|
@@ -85,6 +89,7 @@ function parseArgs(argv) {
|
|
|
85
89
|
// boolean so it can never swallow the following positional as a value.
|
|
86
90
|
if (key.startsWith('no-')) {
|
|
87
91
|
result[key] = true;
|
|
92
|
+
result.__explicit.add(key);
|
|
88
93
|
continue;
|
|
89
94
|
}
|
|
90
95
|
|
|
@@ -95,6 +100,7 @@ function parseArgs(argv) {
|
|
|
95
100
|
} else {
|
|
96
101
|
result[key] = true;
|
|
97
102
|
}
|
|
103
|
+
result.__explicit.add(key);
|
|
98
104
|
} else if (arg === '-o') {
|
|
99
105
|
// Single short-flag alias, scoped to exactly '-o' (council verdict's
|
|
100
106
|
// --out shorthand). No general short-flag support is implemented —
|
|
@@ -106,6 +112,7 @@ function parseArgs(argv) {
|
|
|
106
112
|
} else {
|
|
107
113
|
result.out = true;
|
|
108
114
|
}
|
|
115
|
+
result.__explicit.add('out');
|
|
109
116
|
} else {
|
|
110
117
|
result._.push(arg);
|
|
111
118
|
}
|
|
@@ -428,6 +435,10 @@ Options for 'start':
|
|
|
428
435
|
--no-validate-model Skip model-catalog validation before launch
|
|
429
436
|
--gateway <mode> Routing: auto (direct-first), direct, or openrouter
|
|
430
437
|
--position <pos> Window position: right (default), left, center
|
|
438
|
+
--template <name|path> Render a briefing template ({{prompt}}, {{artifact}}, {{var.*}})
|
|
439
|
+
--artifact <file> File whose content fills {{artifact}} (256 KB cap; needs --template)
|
|
440
|
+
--var <k=v> Template variable, repeatable (needs --template)
|
|
441
|
+
--pack <name|path> Load a saved pack (model/options/template); explicit flags override it
|
|
431
442
|
`,
|
|
432
443
|
fanout: `
|
|
433
444
|
Options for 'fanout':
|
|
@@ -459,6 +470,10 @@ Options for 'fanout':
|
|
|
459
470
|
RESULT_FILE/EVENTS_FILE/COST/PROJECT), never model
|
|
460
471
|
text. Child stdout/stderr go to amicus stderr.
|
|
461
472
|
Never changes the wave's exit code, docs, or events.
|
|
473
|
+
--template <name|path> Render a briefing template ({{prompt}}, {{artifact}}, {{var.*}})
|
|
474
|
+
--artifact <file> File whose content fills {{artifact}} (256 KB cap; needs --template)
|
|
475
|
+
--var <k=v> Template variable, repeatable (needs --template)
|
|
476
|
+
--pack <name|path> Load a saved pack (bench/options/template); explicit flags override it
|
|
462
477
|
Shared per-leg knobs: --agent, --thinking, --timeout, --summary-length,
|
|
463
478
|
--no-context, --context-*, --mcp*, --no-validate-model, --cwd
|
|
464
479
|
Exit codes: 0 all legs complete, 2 partial, 1 none complete / hard failure
|
|
@@ -545,6 +560,8 @@ Subcommands for 'council':
|
|
|
545
560
|
[--gateway auto|direct|openrouter] [--no-validate-model]
|
|
546
561
|
[--debate] [--claude-review <file>] [--no-cost-gate] [--follow]
|
|
547
562
|
[--fallback] [--no-fallback] [--on-complete <cmd>]
|
|
563
|
+
[--template <name|path>] [--artifact <file>] [--var <k=v>]
|
|
564
|
+
[--pack <name|path>]
|
|
548
565
|
Run the full headless council engine (v4.0).
|
|
549
566
|
Chair default: deepseek (must NOT be a bench seat).
|
|
550
567
|
--critic and --lenses are mutually exclusive.
|
|
@@ -567,6 +584,13 @@ Subcommands for 'council':
|
|
|
567
584
|
EVENTS_FILE/COST/PROJECT), never model text. Child
|
|
568
585
|
stdout/stderr go to amicus stderr. Never changes
|
|
569
586
|
the run's exit code, docs, or events.
|
|
587
|
+
--template <name|path> renders a briefing from
|
|
588
|
+
{{prompt}}, {{artifact}}, {{var.*}}; --artifact
|
|
589
|
+
fills {{artifact}} (256 KB cap); --var sets
|
|
590
|
+
{{var.*}} (repeatable). Both require --template.
|
|
591
|
+
--pack <name|path> loads a saved pack (bench,
|
|
592
|
+
chair, critic/lenses, options, template);
|
|
593
|
+
explicit flags always override the pack's values.
|
|
570
594
|
Exit: 0 full run, 2 degraded, 1 quorum/cost/validation.
|
|
571
595
|
save <name> --models a,b,c Save a named council preset (>=2 resolvable members)
|
|
572
596
|
--json Machine-readable output
|
|
@@ -598,12 +622,14 @@ Options for 'spend':
|
|
|
598
622
|
`,
|
|
599
623
|
watch: `
|
|
600
624
|
Options for 'watch':
|
|
601
|
-
<id> A fan-out wave id, council run id, or session id
|
|
625
|
+
<id> A fan-out wave id, council run id, or session id.
|
|
626
|
+
Optional with --ui: bare 'amicus watch --ui' opens the
|
|
627
|
+
project's Council Workspace run list
|
|
602
628
|
--project <path> Project the run was launched in (default cwd)
|
|
603
629
|
--interval <sec> Refresh interval (default 2, floor 0.5)
|
|
604
630
|
--plain Milestone log lines instead of the refresh table
|
|
605
631
|
--json NDJSON: tailed events + composed doc on change + final doc
|
|
606
|
-
--ui Open the Council Workspace window (
|
|
632
|
+
--ui Open the Council Workspace window (interactive-only)
|
|
607
633
|
`,
|
|
608
634
|
setup: `
|
|
609
635
|
Options for 'setup':
|
|
@@ -637,6 +663,43 @@ Options for 'init':
|
|
|
637
663
|
Runs skill install + MCP registration on demand (for plugin-channel /
|
|
638
664
|
--ignore-scripts installs, a failed postinstall, or repairing deleted
|
|
639
665
|
~/.claude state). No flags registers both Claude Code and Claude Desktop.
|
|
666
|
+
`,
|
|
667
|
+
template: `
|
|
668
|
+
Options for 'template':
|
|
669
|
+
amicus template list [--json] List templates (built-ins marked)
|
|
670
|
+
amicus template show <name|path> [--json] Print a template
|
|
671
|
+
`,
|
|
672
|
+
pack: `
|
|
673
|
+
Options for 'pack':
|
|
674
|
+
amicus pack save <name> --kind council|fanout|solo [flags]
|
|
675
|
+
Save a pack built from flags:
|
|
676
|
+
--bench <a,b,c|name> council/fanout: comma-
|
|
677
|
+
separated members, or a
|
|
678
|
+
saved council name
|
|
679
|
+
--model <model> solo kind only
|
|
680
|
+
--chair/--critic/--lenses council kind only
|
|
681
|
+
--timeout/--max-cost/--gateway shared run
|
|
682
|
+
options
|
|
683
|
+
--agent/--thinking/--summary-length fanout/
|
|
684
|
+
solo kind only
|
|
685
|
+
--debate / --no-debate council kind only
|
|
686
|
+
--template <name|path> briefing template
|
|
687
|
+
reference (not rendered)
|
|
688
|
+
--version <semver> default 1.0.0 (an
|
|
689
|
+
unchanged re-save is a
|
|
690
|
+
no-op; a changed one
|
|
691
|
+
auto-bumps the patch)
|
|
692
|
+
--description <text>
|
|
693
|
+
amicus pack save <name> --from-run <id>
|
|
694
|
+
Build a pack from an existing council run /
|
|
695
|
+
fanout wave / solo session instead of flags
|
|
696
|
+
(models, options, and a template REFERENCE only
|
|
697
|
+
— briefing text is never captured)
|
|
698
|
+
amicus pack list [--json] List saved packs
|
|
699
|
+
amicus pack show <name|path> [--json]
|
|
700
|
+
Print a pack plus its validation report (never
|
|
701
|
+
fails on an invalid pack — see 'validation')
|
|
702
|
+
amicus pack rm <name> [--json] Remove a saved pack
|
|
640
703
|
`
|
|
641
704
|
};
|
|
642
705
|
|
|
@@ -110,20 +110,40 @@ function buildRevoteBundle({ findings, date }) {
|
|
|
110
110
|
return parts.join('\n\n');
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
-
|
|
113
|
+
/**
|
|
114
|
+
* ⚠️ LC-12: same omission as LC-6 one stage earlier — a repair solo is a FRESH
|
|
115
|
+
* session and cannot see the response it is repairing unless we hand it over.
|
|
116
|
+
* `prior` is embedded verbatim and uncapped; the absent case is stated rather
|
|
117
|
+
* than papered over with an empty block, so a model with nothing to correct is
|
|
118
|
+
* told to say so instead of left to invent a position.
|
|
119
|
+
* @param {string} kind 'defense' | 're-vote'
|
|
120
|
+
* @param {string} contract the trailing-JSON contract for that kind
|
|
121
|
+
* @param {Array<{code:string,detail:string}>} errors
|
|
122
|
+
* @param {string} [prior] the text that ACTUALLY failed validation
|
|
123
|
+
*/
|
|
124
|
+
function repair(kind, contract, errors, prior) {
|
|
114
125
|
const lines = (errors || []).map(e => `- ${e.code}: ${e.detail}`).join('\n');
|
|
126
|
+
const text = typeof prior === 'string' ? prior.trim() : '';
|
|
127
|
+
const block = text
|
|
128
|
+
? [`--- YOUR PREVIOUS ${kind.toUpperCase()} (verbatim — this is the text to correct) ---`,
|
|
129
|
+
text,
|
|
130
|
+
`--- END OF YOUR PREVIOUS ${kind.toUpperCase()} ---`].join('\n')
|
|
131
|
+
: `Your previous ${kind} response was empty — there is no prior text to correct. `
|
|
132
|
+
+ 'Do not invent a position to satisfy the schema: say so in your output.';
|
|
115
133
|
return [
|
|
116
|
-
'Do NOT use any tools or read any files; everything is in this message; begin '
|
|
117
|
-
'immediately with the JSON block.',
|
|
118
|
-
|
|
134
|
+
'Do NOT use any tools or read any files; everything is in this message; begin '
|
|
135
|
+
+ 'immediately with the JSON block.',
|
|
136
|
+
block,
|
|
137
|
+
`That ${kind} response's trailing JSON failed validation with these errors:`,
|
|
119
138
|
lines,
|
|
120
|
-
'Re-emit ONLY the corrected JSON block
|
|
139
|
+
'Re-emit ONLY the corrected JSON block (the same position, fixed — do not change '
|
|
140
|
+
+ 'your votes), as a single fenced ```json block:',
|
|
121
141
|
contract,
|
|
122
142
|
].join('\n\n');
|
|
123
143
|
}
|
|
124
144
|
|
|
125
|
-
function buildDefenseRepairPrompt({ errors }) { return repair('defense', DEFENSE_CONTRACT, errors); }
|
|
126
|
-
function buildRevoteRepairPrompt({ errors }) { return repair('re-vote', REVOTE_CONTRACT, errors); }
|
|
145
|
+
function buildDefenseRepairPrompt({ errors, defense }) { return repair('defense', DEFENSE_CONTRACT, errors, defense); }
|
|
146
|
+
function buildRevoteRepairPrompt({ errors, revote }) { return repair('re-vote', REVOTE_CONTRACT, errors, revote); }
|
|
127
147
|
|
|
128
148
|
/**
|
|
129
149
|
* Chair-packet "Debate round outcomes" section (spec §5.3c). De-anonymized —
|