amicus 3.2.3 → 4.0.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 +106 -0
- package/README.md +15 -3
- package/electron/main.js +4 -1
- package/package.json +3 -1
- package/schemas/abort.schema.json +17 -0
- package/schemas/alias-audit.schema.json +17 -0
- package/schemas/council-run.schema.json +38 -0
- package/schemas/council-stats.schema.json +28 -0
- package/schemas/council-tally.schema.json +70 -0
- package/schemas/council-validate.schema.json +22 -0
- package/schemas/council-verdict.schema.json +47 -0
- package/schemas/doctor.schema.json +29 -0
- package/schemas/error.schema.json +23 -0
- package/schemas/model-catalog.schema.json +19 -0
- package/schemas/run.schema.json +26 -0
- package/schemas/spend.schema.json +16 -0
- package/schemas/wave.schema.json +33 -0
- package/skills/second-opinion/SEAT-BRIEFS.md +5 -3
- package/skills/second-opinion/SKILL.md +8 -0
- package/src/cli-handlers-abort.js +29 -0
- package/src/cli-handlers-council-run.js +168 -0
- package/src/cli-handlers-council.js +8 -5
- package/src/cli-handlers-status.js +35 -4
- package/src/cli.js +9 -0
- package/src/council/anonymize.js +76 -0
- package/src/council/briefings-stage2.js +150 -0
- package/src/council/briefings.js +141 -0
- package/src/council/findings.js +13 -1
- package/src/council/ledger.js +13 -1
- package/src/council/parse-stage2.js +103 -0
- package/src/council/run-assemble.js +100 -0
- package/src/council/run-launch.js +99 -0
- package/src/council/run-stages.js +203 -0
- package/src/council/run-state.js +161 -0
- package/src/council/run.js +277 -0
- package/src/council/tally.js +3 -1
- package/src/council/verdict.js +9 -2
- package/src/headless.js +24 -25
- package/src/mcp-council-awareness.js +187 -0
- package/src/mcp-council-run.js +161 -0
- package/src/mcp-server.js +87 -28
- package/src/mcp-tools.js +50 -0
- package/src/prompt-builder.js +36 -19
- package/src/sidecar/fanout-leg.js +2 -2
- package/src/sidecar/fanout.js +1 -1
- package/src/sidecar/resume.js +7 -2
- package/src/utils/abort-result.js +1 -1
- package/src/utils/error-doc.js +2 -0
- package/src/utils/fold-marker.js +21 -0
- package/src/utils/route-error.js +26 -0
- package/src/utils/start-helpers.js +19 -10
- package/src/utils/untrusted-fence.js +8 -7
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://raw.githubusercontent.com/BourbonDog/amicus/main/schemas/wave.schema.json",
|
|
4
|
+
"title": "amicus wave result document",
|
|
5
|
+
"description": "Fan-out wave result (`fanout --json`, wave.json). counts named buckets may not sum to total (see result-schema.js COUNTS REMAINDER RULE).",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": ["schemaVersion", "type", "waveId", "status", "counts", "legs"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"schemaVersion": { "const": 2 },
|
|
10
|
+
"type": { "const": "wave" },
|
|
11
|
+
"waveId": { "type": "string" },
|
|
12
|
+
"status": { "type": "string" },
|
|
13
|
+
"error": { "type": ["string", "null"] },
|
|
14
|
+
"counts": {
|
|
15
|
+
"type": "object",
|
|
16
|
+
"required": ["total", "complete", "error", "timeout", "aborted"],
|
|
17
|
+
"properties": {
|
|
18
|
+
"total": { "type": "number" },
|
|
19
|
+
"complete": { "type": "number" },
|
|
20
|
+
"error": { "type": "number" },
|
|
21
|
+
"timeout": { "type": "number" },
|
|
22
|
+
"aborted": { "type": "number" }
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"legs": { "type": "array", "items": { "type": "object" } },
|
|
26
|
+
"prompt": { "type": ["object", "null"] },
|
|
27
|
+
"createdAt": { "type": ["string", "null"] },
|
|
28
|
+
"completedAt": { "type": ["string", "null"] },
|
|
29
|
+
"durationMs": { "type": ["number", "null"] },
|
|
30
|
+
"usage": { "type": "object" },
|
|
31
|
+
"notices": { "type": "array", "items": { "type": "string" } }
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -178,13 +178,15 @@ Append to the chair packet (`_tmp-chair-packet.md`) when the element is toggled
|
|
|
178
178
|
> 1. **HARD QUESTIONS** — three to five questions the artifact's author has probably not
|
|
179
179
|
> asked themselves, chosen so that an unanswerable question reveals a structural gap in
|
|
180
180
|
> the artifact (not gotchas — questions whose answers should exist).
|
|
181
|
-
> 2. A final line, alone on the last line,
|
|
181
|
+
> 2. A final line, alone on the last line, containing ONLY the phrase — no rationale, no
|
|
182
|
+
> dash, no trailing text of any kind — exactly one of:
|
|
182
183
|
>
|
|
183
184
|
> `VERDICT: Ship it` | `VERDICT: Fix these first` | `VERDICT: Fundamental rethink`
|
|
184
185
|
>
|
|
185
186
|
> Pick one. "Ship it" = solid, nothing blocking. "Fix these first" = specific gaps must
|
|
186
|
-
> be resolved
|
|
187
|
-
>
|
|
187
|
+
> be resolved first. "Fundamental rethink" = structural problems that cannot be patched.
|
|
188
|
+
> Name the gaps or the structural problems in the synthesis ABOVE, not on the VERDICT
|
|
189
|
+
> line itself — that line carries the phrase and nothing else.
|
|
188
190
|
|
|
189
191
|
**Orchestration note:** surface the chair's `VERDICT:` line verbatim at the top of
|
|
190
192
|
`report.md` and in the inline chat presentation of the results.
|
|
@@ -33,6 +33,13 @@ Operating lessons from each run fold back into `MODEL-NOTES.md` (with approval),
|
|
|
33
33
|
|
|
34
34
|
**Transport rule — CLI not on PATH:** every command below assumes the `amicus` CLI. If `amicus` is not on PATH (typical for **plugin-only installs**), run the identical commands as `npx -y amicus@latest <args>` (e.g. `npx -y amicus@latest fanout --models "m1,m2,m3" --prompt-file <path> --json`), or use the equivalent MCP tools (`amicus_fanout`, `amicus_start`, `amicus_wait`, `amicus_status`, `amicus_read`, `amicus_council_tally`, `amicus_council_stats`, `amicus_verdict`) — council briefings are always self-contained (`--no-context`), so MCP transport is equivalent.
|
|
35
35
|
|
|
36
|
+
**Headless contexts (v4.0):** CI and scripted environments with no Claude runtime can run the
|
|
37
|
+
whole mechanical pipeline (Stages 1–3 plus the deterministic Stage-5 artifacts) as one command —
|
|
38
|
+
`amicus council run --prompt-file <briefing.md> --models "a,b,c" --chair <model> --json` — see
|
|
39
|
+
[docs/council.md](../../docs/council.md#amicus-council-run). This skill's staged, human-in-the-loop
|
|
40
|
+
orchestration remains the interactive path: Stage 0 intake, Stage 4 decisions, and Stage 6 lessons
|
|
41
|
+
are human stages the engine never automates.
|
|
42
|
+
|
|
36
43
|
## When to use
|
|
37
44
|
|
|
38
45
|
- The user provides documents, artifacts, or links **and** an analysis request **and** criteria, and wants other models to weigh in independently.
|
|
@@ -169,6 +176,7 @@ file) returns `{waveId, taskIds[]}` immediately. Preferred: call `amicus_wait` w
|
|
|
169
176
|
one blocking call per wave; re-call it while it returns `timedOut: true`. Fallback: poll
|
|
170
177
|
`amicus_status`. Either way, `amicus_read` each leg when done. The council's briefings are always
|
|
171
178
|
self-contained (`--no-context`), so MCP transport is equivalent.
|
|
179
|
+
Council JSON returned by the MCP tools (`amicus_council_tally`, `amicus_council_stats`, `amicus_verdict`) arrives wrapped in the `<untrusted_sidecar_output>` fence since v4.0 — parse the JSON from inside the fence; CLI `--json` output remains unfenced.
|
|
172
180
|
|
|
173
181
|
**Required structured output from every model.** Instruct each council model to produce:
|
|
174
182
|
|
|
@@ -14,6 +14,31 @@ const { validateTaskId, safeSessionDir } = require('./utils/validators');
|
|
|
14
14
|
const { failJson, ERROR_CODES } = require('./utils/error-doc');
|
|
15
15
|
const { buildAbortResult } = require('./utils/result-schema');
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Council-run abort via the sessions-dir pointer (v4.0 §8). Returns the exit
|
|
19
|
+
* code when the taskId was a council run, or null to fall through to the
|
|
20
|
+
* ordinary session/wave paths.
|
|
21
|
+
*/
|
|
22
|
+
function tryCouncilAbort(project, taskId, useJson) {
|
|
23
|
+
const { abortCouncilRun } = require('./mcp-council-run');
|
|
24
|
+
const res = abortCouncilRun(project, taskId);
|
|
25
|
+
if (!res) { return null; }
|
|
26
|
+
if (res.alreadyTerminal) {
|
|
27
|
+
if (useJson) {
|
|
28
|
+
console.log(JSON.stringify(buildAbortResult({ scope: 'council-run', taskId, aborted: [] }), null, 2));
|
|
29
|
+
} else {
|
|
30
|
+
console.log(`Council run ${taskId} is not running (status: ${res.status}).`);
|
|
31
|
+
}
|
|
32
|
+
return 0;
|
|
33
|
+
}
|
|
34
|
+
if (useJson) {
|
|
35
|
+
console.log(JSON.stringify(buildAbortResult({ scope: 'council-run', taskId, aborted: [taskId] }), null, 2));
|
|
36
|
+
} else {
|
|
37
|
+
console.log(`Council run ${taskId} marked as aborted (${res.cascaded} running leg(s) aborted).`);
|
|
38
|
+
}
|
|
39
|
+
return 0;
|
|
40
|
+
}
|
|
41
|
+
|
|
17
42
|
/**
|
|
18
43
|
* Handle 'amicus abort --all --json': mark every running session aborted.
|
|
19
44
|
* @returns {number} exit code (always 0 — even a no-op --all is a success)
|
|
@@ -45,6 +70,8 @@ async function handleAbortTaskJson(args, taskId) {
|
|
|
45
70
|
const metaPath = path.join(sessionDir, 'metadata.json');
|
|
46
71
|
|
|
47
72
|
if (!fs.existsSync(metaPath)) {
|
|
73
|
+
const council = tryCouncilAbort(project, taskId, true);
|
|
74
|
+
if (council !== null) { return council; }
|
|
48
75
|
process.exit(failJson(true, { code: ERROR_CODES.BAD_SESSION, message: `Session ${taskId} not found` }));
|
|
49
76
|
}
|
|
50
77
|
|
|
@@ -171,6 +198,8 @@ async function handleAbort(args) {
|
|
|
171
198
|
const metaPath = path.join(sessionDir, 'metadata.json');
|
|
172
199
|
|
|
173
200
|
if (!fs.existsSync(metaPath)) {
|
|
201
|
+
const council = tryCouncilAbort(project, taskId, false);
|
|
202
|
+
if (council !== null) { return council; }
|
|
174
203
|
console.error(`Session ${taskId} not found`);
|
|
175
204
|
process.exit(1);
|
|
176
205
|
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
// src/cli-handlers-council-run.js
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* CLI `amicus council run` (v4.0 spec §4): flag validation, headless engine
|
|
6
|
+
* invocation, document emission, exit codes. Lives in its own file because
|
|
7
|
+
* cli-handlers-council.js is near the 300-line gate; dispatched from
|
|
8
|
+
* handleCouncil. Pre-flight failures go through the error envelope (exit 1)
|
|
9
|
+
* BEFORE any spend.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const { failJson, buildErrorDoc, ERROR_CODES } = require('./utils/error-doc');
|
|
14
|
+
const { validateTaskId } = require('./utils/validators');
|
|
15
|
+
const { GATEWAY_MODES } = require('./utils/model-descriptor');
|
|
16
|
+
|
|
17
|
+
const CHAIR_DEFAULT = 'deepseek';
|
|
18
|
+
|
|
19
|
+
function parseList(value) {
|
|
20
|
+
return String(value).split(',').map(s => s.trim()).filter(Boolean);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Resolve bench models from --models XOR --council (mirrors handleFanout). */
|
|
24
|
+
function resolveBench(args, useJson) {
|
|
25
|
+
const hasModels = typeof args.models === 'string' && args.models.trim();
|
|
26
|
+
const hasCouncil = args.council !== undefined && args.council !== false;
|
|
27
|
+
if (hasModels && hasCouncil) {
|
|
28
|
+
return { fail: failJson(useJson, { code: ERROR_CODES.BAD_ARGS,
|
|
29
|
+
message: 'Error: pass exactly one of --models / --council, not both' }) };
|
|
30
|
+
}
|
|
31
|
+
if (!hasModels && !hasCouncil) {
|
|
32
|
+
return { fail: failJson(useJson, { code: ERROR_CODES.BAD_ARGS,
|
|
33
|
+
message: 'Error: council run needs --models a,b,c or --council <preset> (at least 2 seats)' }) };
|
|
34
|
+
}
|
|
35
|
+
if (hasCouncil) {
|
|
36
|
+
if (typeof args.council !== 'string' || !args.council.trim()) {
|
|
37
|
+
return { fail: failJson(useJson, { code: ERROR_CODES.BAD_ARGS,
|
|
38
|
+
message: 'Error: --council requires a council name (e.g. --council budget)' }) };
|
|
39
|
+
}
|
|
40
|
+
const { resolveCouncilMembers } = require('./utils/config');
|
|
41
|
+
const { readCache } = require('./utils/model-catalog');
|
|
42
|
+
const catalog = (readCache() || {}).models || [];
|
|
43
|
+
const expanded = resolveCouncilMembers(args.council.trim(), catalog);
|
|
44
|
+
if (expanded.error) {
|
|
45
|
+
return { fail: failJson(useJson, { code: ERROR_CODES.BAD_ARGS, message: `Error: ${expanded.error}` }) };
|
|
46
|
+
}
|
|
47
|
+
if (expanded.dropped && expanded.dropped.length && !useJson) {
|
|
48
|
+
process.stderr.write(`Notice: dropped unavailable council member(s): ${expanded.dropped.join(', ')}\n`);
|
|
49
|
+
}
|
|
50
|
+
return { bench: expanded.models };
|
|
51
|
+
}
|
|
52
|
+
return { bench: parseList(args.models) };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function renderRunHuman(run) {
|
|
56
|
+
const lines = [
|
|
57
|
+
`Council run ${run.runId}: ${run.status} (exit ${run.exitCode})`,
|
|
58
|
+
` bench: ${(run.bench || []).join(', ')} chair: ${run.chair}`,
|
|
59
|
+
` dir: ${run.options && run.options.outDir}`,
|
|
60
|
+
];
|
|
61
|
+
if (run.usage && run.usage.cost && typeof run.usage.cost.amount === 'number') {
|
|
62
|
+
lines.push(` cost: $${run.usage.cost.amount.toFixed(4)} (${run.usage.cost.source})`);
|
|
63
|
+
}
|
|
64
|
+
if (run.error) { lines.push(` error: ${run.error.code}: ${run.error.message}`); }
|
|
65
|
+
return lines.join('\n') + '\n';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** @param {object} args parsed CLI args @returns {Promise<number>} exit code */
|
|
69
|
+
async function handleCouncilRun(args) {
|
|
70
|
+
const useJson = !!args.json;
|
|
71
|
+
|
|
72
|
+
// --prompt-file required; inline --prompt rejected (councils always have
|
|
73
|
+
// real briefings — same rationale as MCP fanout's briefing-via-file).
|
|
74
|
+
if (args.prompt !== undefined) {
|
|
75
|
+
return failJson(useJson, { code: ERROR_CODES.BAD_ARGS,
|
|
76
|
+
message: 'Error: council run takes --prompt-file only (no inline --prompt)',
|
|
77
|
+
hint: 'write the briefing to a file and pass --prompt-file <path>' });
|
|
78
|
+
}
|
|
79
|
+
const { resolvePromptSource } = require('./utils/prompt-source');
|
|
80
|
+
const promptRes = resolvePromptSource(args);
|
|
81
|
+
if (promptRes.error) {
|
|
82
|
+
return failJson(useJson, { code: ERROR_CODES.MISSING_PROMPT, message: promptRes.error });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const benchRes = resolveBench(args, useJson);
|
|
86
|
+
if (benchRes.fail !== undefined) { return benchRes.fail; }
|
|
87
|
+
const bench = benchRes.bench;
|
|
88
|
+
if (bench.length < 2) {
|
|
89
|
+
return failJson(useJson, { code: ERROR_CODES.BAD_ARGS,
|
|
90
|
+
message: 'Error: a council needs at least 2 seats (fanout semantics)' });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const chair = (typeof args.chair === 'string' && args.chair.trim())
|
|
94
|
+
? args.chair.trim() : CHAIR_DEFAULT;
|
|
95
|
+
if (bench.includes(chair)) {
|
|
96
|
+
return failJson(useJson, { code: ERROR_CODES.BAD_ARGS,
|
|
97
|
+
message: `Error: chair '${chair}' is a bench seat — the chair must not review`,
|
|
98
|
+
hint: `pick a chair outside --models (default: ${CHAIR_DEFAULT}), or remove '${chair}' from the bench` });
|
|
99
|
+
}
|
|
100
|
+
const critic = (typeof args.critic === 'string' && args.critic.trim()) ? args.critic.trim() : null;
|
|
101
|
+
if (critic && !bench.includes(critic)) {
|
|
102
|
+
return failJson(useJson, { code: ERROR_CODES.BAD_ARGS,
|
|
103
|
+
message: `Error: critic '${critic}' must be one of the bench seats`,
|
|
104
|
+
hint: `--critic swaps one seat's brief; pass one of: ${bench.join(', ')}` });
|
|
105
|
+
}
|
|
106
|
+
const lenses = (typeof args.lenses === 'string' && args.lenses.trim()) ? parseList(args.lenses) : null;
|
|
107
|
+
if (critic && lenses) {
|
|
108
|
+
return failJson(useJson, { code: ERROR_CODES.BAD_ARGS,
|
|
109
|
+
message: 'Error: --critic and --lenses are mutually exclusive in v4.0' });
|
|
110
|
+
}
|
|
111
|
+
if (lenses && lenses.length !== bench.length) {
|
|
112
|
+
return failJson(useJson, { code: ERROR_CODES.BAD_ARGS,
|
|
113
|
+
message: `Error: --lenses needs exactly one lens per seat (${bench.length} seats, got ${lenses.length})` });
|
|
114
|
+
}
|
|
115
|
+
if (args.timeout !== undefined && args.timeout <= 0) {
|
|
116
|
+
return failJson(useJson, { code: ERROR_CODES.BAD_ARGS, message: 'Error: --timeout must be a positive number' });
|
|
117
|
+
}
|
|
118
|
+
const mc = args['max-cost'];
|
|
119
|
+
if (mc !== undefined && (typeof mc !== 'number' || !Number.isFinite(mc) || mc <= 0)) {
|
|
120
|
+
return failJson(useJson, { code: ERROR_CODES.BAD_ARGS, message: 'Error: --max-cost must be a positive number' });
|
|
121
|
+
}
|
|
122
|
+
if (args.gateway !== undefined && !GATEWAY_MODES.includes(args.gateway)) {
|
|
123
|
+
return failJson(useJson, { code: ERROR_CODES.BAD_ARGS,
|
|
124
|
+
message: `Error: --gateway must be one of: ${GATEWAY_MODES.join(', ')}` });
|
|
125
|
+
}
|
|
126
|
+
let runId;
|
|
127
|
+
if (args['run-id']) {
|
|
128
|
+
const check = validateTaskId(String(args['run-id']));
|
|
129
|
+
if (!check.valid) {
|
|
130
|
+
return failJson(useJson, { code: ERROR_CODES.BAD_SESSION, message: check.error });
|
|
131
|
+
}
|
|
132
|
+
runId = String(args['run-id']);
|
|
133
|
+
} else {
|
|
134
|
+
runId = require('./sidecar/start').generateTaskId();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const project = args.cwd || process.cwd();
|
|
138
|
+
const runDir = args['out-dir']
|
|
139
|
+
? path.resolve(project, String(args['out-dir']))
|
|
140
|
+
: path.resolve(project, `council-${runId}`);
|
|
141
|
+
|
|
142
|
+
const { resolveGatewayMode } = require('./utils/config');
|
|
143
|
+
const { runCouncil } = require('./council/run');
|
|
144
|
+
const { exitCode, run } = await runCouncil({
|
|
145
|
+
briefing: promptRes.prompt, models: bench, chair, critic, lenses,
|
|
146
|
+
project, runId, runDir,
|
|
147
|
+
timeout: args.timeout, maxCost: mc !== undefined ? mc : null,
|
|
148
|
+
gateway: resolveGatewayMode(args.gateway),
|
|
149
|
+
noValidateModel: !!args['no-validate-model'],
|
|
150
|
+
date: new Date().toISOString().slice(0, 10),
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
if (useJson) {
|
|
154
|
+
// Spec §4: exit-1 rows fail through the error envelope; 0/2 emit the manifest.
|
|
155
|
+
if (exitCode === 1 && run && run.error) {
|
|
156
|
+
process.stdout.write(JSON.stringify(buildErrorDoc({
|
|
157
|
+
code: run.error.code, message: run.error.message, command: 'council run',
|
|
158
|
+
}), null, 2) + '\n');
|
|
159
|
+
} else {
|
|
160
|
+
process.stdout.write(JSON.stringify(run, null, 2) + '\n');
|
|
161
|
+
}
|
|
162
|
+
} else {
|
|
163
|
+
process.stdout.write(renderRunHuman(run));
|
|
164
|
+
}
|
|
165
|
+
return exitCode;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
module.exports = { handleCouncilRun, CHAIR_DEFAULT };
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
'use strict';
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const { tally } = require('./council/tally');
|
|
5
|
-
const { deriveReliability, appendRun } = require('./council/ledger');
|
|
5
|
+
const { deriveReliability, appendRun, buildStatsDoc } = require('./council/ledger');
|
|
6
6
|
const { sumWaveUsage, formatCost } = require('./utils/pricing');
|
|
7
7
|
const { failJson, ERROR_CODES } = require('./utils/error-doc');
|
|
8
8
|
const { buildReport } = require('./council/report');
|
|
9
|
-
const { validateFindings } = require('./council/findings');
|
|
9
|
+
const { validateFindings, buildValidateDoc } = require('./council/findings');
|
|
10
10
|
const { buildVerdict, writeVerdictAtomic } = require('./council/verdict');
|
|
11
11
|
const {
|
|
12
12
|
runSave: runCouncilSave,
|
|
@@ -44,7 +44,9 @@ function runTally(inputPath, useJson, opts = {}) {
|
|
|
44
44
|
|
|
45
45
|
function runStats(useJson) {
|
|
46
46
|
const agg = deriveReliability();
|
|
47
|
-
|
|
47
|
+
// v4.0 §7: --json emits the enveloped doc (breaking: was a bare array);
|
|
48
|
+
// human output is unchanged (renderStats still takes the rows).
|
|
49
|
+
process.stdout.write(useJson ? JSON.stringify(buildStatsDoc(agg), null, 2) + '\n' : renderStats(agg));
|
|
48
50
|
return 0;
|
|
49
51
|
}
|
|
50
52
|
|
|
@@ -117,7 +119,7 @@ function runValidate(filePath, useJson) {
|
|
|
117
119
|
hint: 'pass a Stage-1 reviewer output file (prose + trailing ```json findings block)' });
|
|
118
120
|
}
|
|
119
121
|
const result = validateFindings(text);
|
|
120
|
-
process.stdout.write(useJson ? JSON.stringify(result, null, 2) + '\n' : renderValidate(result));
|
|
122
|
+
process.stdout.write(useJson ? JSON.stringify(buildValidateDoc(result), null, 2) + '\n' : renderValidate(result));
|
|
121
123
|
return result.ok ? 0 : 2;
|
|
122
124
|
}
|
|
123
125
|
|
|
@@ -185,6 +187,7 @@ function renderVerdict(v, outPath) {
|
|
|
185
187
|
async function handleCouncil(args) {
|
|
186
188
|
const sub = args._[1];
|
|
187
189
|
const useJson = !!args.json;
|
|
190
|
+
if (sub === 'run') { return require('./cli-handlers-council-run').handleCouncilRun(args); }
|
|
188
191
|
if (sub === 'tally') { return runTally(args._[2], useJson, { append: !args['no-ledger'] }); }
|
|
189
192
|
if (sub === 'stats') { return runStats(useJson); }
|
|
190
193
|
if (sub === 'report') { return runReport(args, useJson); }
|
|
@@ -195,7 +198,7 @@ async function handleCouncil(args) {
|
|
|
195
198
|
if (sub === 'show') { return runCouncilShow(args._[2], useJson); }
|
|
196
199
|
return failJson(useJson, { code: ERROR_CODES.BAD_ARGS,
|
|
197
200
|
message: `unknown council subcommand '${sub || ''}'`,
|
|
198
|
-
hint: 'amicus council tally|stats|report|validate|verdict|save|list|show' });
|
|
201
|
+
hint: 'amicus council run|tally|stats|report|validate|verdict|save|list|show' });
|
|
199
202
|
}
|
|
200
203
|
|
|
201
204
|
module.exports = { handleCouncil };
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
'use strict';
|
|
10
10
|
|
|
11
11
|
const { validateTaskId } = require('./utils/validators');
|
|
12
|
+
const { failJson, ERROR_CODES } = require('./utils/error-doc');
|
|
12
13
|
|
|
13
14
|
/** Render a key-value block for a single-session status payload. */
|
|
14
15
|
function formatRunHuman(d) {
|
|
@@ -42,6 +43,18 @@ function formatWaveHumanStatus(d) {
|
|
|
42
43
|
return [head, ...legLines].join('\n');
|
|
43
44
|
}
|
|
44
45
|
|
|
46
|
+
/** Render a council-run payload: header + one line per stage. */
|
|
47
|
+
function formatCouncilHuman(d) {
|
|
48
|
+
const head = `Council run ${d.runId}: ${d.status}` +
|
|
49
|
+
(d.currentStage ? ` — ${d.currentStage}` : '') +
|
|
50
|
+
(d.legsTotal !== null && d.legsTotal !== undefined ? ` (${d.legsComplete}/${d.legsTotal} legs)` : '') +
|
|
51
|
+
` (${d.elapsed})`;
|
|
52
|
+
const stageLines = (d.stages || []).map(s =>
|
|
53
|
+
` ${String(s.name).padEnd(10)} ${String(s.status).padEnd(10)}${s.waveId ? ` wave ${s.waveId}` : ''}`);
|
|
54
|
+
const tail = d.reason ? [` Reason: ${d.reason}`] : [];
|
|
55
|
+
return [head, ...stageLines, ...tail].join('\n');
|
|
56
|
+
}
|
|
57
|
+
|
|
45
58
|
/**
|
|
46
59
|
* Handle 'amicus status'. Exit code 0 = status retrieved (any run state, even
|
|
47
60
|
* a failed/crashed run — the QUERY succeeded); 1 = missing/invalid/unknown id.
|
|
@@ -49,28 +62,46 @@ function formatWaveHumanStatus(d) {
|
|
|
49
62
|
* @returns {Promise<number>}
|
|
50
63
|
*/
|
|
51
64
|
async function handleStatus(args) {
|
|
65
|
+
const useJson = !!args.json;
|
|
52
66
|
const taskId = args.wave || args._[1];
|
|
53
67
|
if (!taskId || taskId === true) {
|
|
68
|
+
// v4.0 §7: --json failures land on stdout as the error doc; human stderr
|
|
69
|
+
// is byte-identical to pre-4.0.
|
|
70
|
+
if (useJson) {
|
|
71
|
+
return failJson(true, { code: ERROR_CODES.BAD_SESSION, message: 'task_id is required for status',
|
|
72
|
+
hint: 'amicus status <task_id> [--json] (or: amicus status --wave <wave_id>)' });
|
|
73
|
+
}
|
|
54
74
|
process.stderr.write('Error: task_id is required for status\n');
|
|
55
75
|
process.stderr.write('Usage: amicus status <task_id> [--json] (or: amicus status --wave <wave_id>)\n');
|
|
56
76
|
return 1;
|
|
57
77
|
}
|
|
58
78
|
const check = validateTaskId(String(taskId));
|
|
59
|
-
if (!check.valid) {
|
|
79
|
+
if (!check.valid) {
|
|
80
|
+
if (useJson) { return failJson(true, { code: ERROR_CODES.BAD_SESSION, message: check.error }); }
|
|
81
|
+
process.stderr.write(`${check.error}\n`);
|
|
82
|
+
return 1;
|
|
83
|
+
}
|
|
60
84
|
|
|
61
85
|
const project = args.cwd || process.cwd();
|
|
62
86
|
const { handlers } = require('./mcp-server');
|
|
63
87
|
const result = await handlers.amicus_status({ taskId: String(taskId) }, project);
|
|
64
88
|
const text = result.content[0].text;
|
|
65
|
-
if (result.isError) {
|
|
89
|
+
if (result.isError) {
|
|
90
|
+
if (useJson) { return failJson(true, { code: ERROR_CODES.BAD_SESSION, message: text }); }
|
|
91
|
+
process.stderr.write(`${text}\n`);
|
|
92
|
+
return 1;
|
|
93
|
+
}
|
|
66
94
|
|
|
67
95
|
let data;
|
|
68
96
|
try { data = JSON.parse(text); } catch { process.stdout.write(`${text}\n`); return 0; }
|
|
69
97
|
delete data.next_poll; // MCP-agent polling guidance, not CLI output
|
|
70
98
|
|
|
71
99
|
if (args.json) { process.stdout.write(`${JSON.stringify(data, null, 2)}\n`); return 0; }
|
|
72
|
-
|
|
100
|
+
const rendered = data.type === 'wave' ? formatWaveHumanStatus(data)
|
|
101
|
+
: data.type === 'council-run' ? formatCouncilHuman(data)
|
|
102
|
+
: formatRunHuman(data);
|
|
103
|
+
process.stdout.write(`${rendered}\n`);
|
|
73
104
|
return 0;
|
|
74
105
|
}
|
|
75
106
|
|
|
76
|
-
module.exports = { handleStatus, formatRunHuman, formatWaveHumanStatus };
|
|
107
|
+
module.exports = { handleStatus, formatRunHuman, formatWaveHumanStatus, formatCouncilHuman };
|
package/src/cli.js
CHANGED
|
@@ -357,6 +357,7 @@ Commands:
|
|
|
357
357
|
continue New session building on previous
|
|
358
358
|
read Output session summary/conversation
|
|
359
359
|
models List/search the model catalog, refresh it, audit aliases
|
|
360
|
+
council run <briefing.md> (--models a,b,c | --council <name>) Headless council: reviews → cross-review → tally → chair → verdict
|
|
360
361
|
council tally <input.json> [--json] Tally council findings → tiers/street-cred
|
|
361
362
|
council stats [--json] Reviewer-reliability from the ledger
|
|
362
363
|
council report <verdict.json> [--wave <wave.json>] [--md|--html] Disagreement+verdict report
|
|
@@ -507,6 +508,14 @@ Subcommands for 'council':
|
|
|
507
508
|
--decisions <d.json> Optional. Stage-4 decisions array (default [])
|
|
508
509
|
-o, --out <out.json> Output path (default ./verdict.json)
|
|
509
510
|
--json Print the full verdict document
|
|
511
|
+
run --prompt-file <briefing.md> (--models a,b,c | --council <name>)
|
|
512
|
+
[--chair <model>] [--critic <model>] [--lenses s1,s2,...]
|
|
513
|
+
[--out-dir <dir>] [--json] [--max-cost <usd>] [--timeout <min>]
|
|
514
|
+
[--gateway auto|direct|openrouter] [--no-validate-model]
|
|
515
|
+
Run the full headless council engine (v4.0).
|
|
516
|
+
Chair default: deepseek (must NOT be a bench seat).
|
|
517
|
+
--critic and --lenses are mutually exclusive.
|
|
518
|
+
Exit: 0 full run, 2 degraded, 1 quorum/cost/validation.
|
|
510
519
|
save <name> --models a,b,c Save a named council preset (>=2 resolvable members)
|
|
511
520
|
--json Machine-readable output
|
|
512
521
|
list List saved councils plus the built-in benches
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// src/council/anonymize.js
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @module council/anonymize
|
|
6
|
+
* Pure label-map helpers for the headless council engine (spec §6).
|
|
7
|
+
* Each surviving Stage-1 review gets a stable letter label ('Review A',
|
|
8
|
+
* 'Review B', …) in bench order. The label↔model map lives ONLY in
|
|
9
|
+
* orchestrator memory and run.json — never in any judge-visible file.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const LETTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Assign stable labels in the given model order.
|
|
16
|
+
* @param {string[]} models reviewed model ids (bench order)
|
|
17
|
+
* @returns {{entries: Array<{label: string, letter: string, model: string}>,
|
|
18
|
+
* labelMap: Object<string, string>, letterByModel: Object<string, string>}}
|
|
19
|
+
*/
|
|
20
|
+
function assignLabels(models) {
|
|
21
|
+
if (!Array.isArray(models) || models.length === 0 || models.length > LETTERS.length) {
|
|
22
|
+
throw new Error(`assignLabels needs 1-${LETTERS.length} models`);
|
|
23
|
+
}
|
|
24
|
+
const entries = models.map((model, i) => ({
|
|
25
|
+
label: `Review ${LETTERS[i]}`, letter: LETTERS[i], model,
|
|
26
|
+
}));
|
|
27
|
+
const labelMap = {};
|
|
28
|
+
const letterByModel = {};
|
|
29
|
+
for (const e of entries) {
|
|
30
|
+
labelMap[e.label] = e.model;
|
|
31
|
+
letterByModel[e.model] = e.letter;
|
|
32
|
+
}
|
|
33
|
+
return { entries, labelMap, letterByModel };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Rewrite a review's local integer finding id to its run-global label id. */
|
|
37
|
+
function toGlobalId(letter, localId) {
|
|
38
|
+
return `${letter}${localId}`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Rewrite a validated review's findings to run-global tally-input entries.
|
|
43
|
+
* @param {string} letter review letter (e.g. 'A')
|
|
44
|
+
* @param {string} raiser de-anonymized model id
|
|
45
|
+
* @param {Array<{id: number, severity: string, claim: string, location: string}>} findings
|
|
46
|
+
* @returns {Array<{id: string, raiser: string, severity: string, claim: string, location: string}>}
|
|
47
|
+
*/
|
|
48
|
+
function toGlobalFindings(letter, raiser, findings) {
|
|
49
|
+
// claim + location must reach tally-input.json — Action v2 (Plan C) joins on
|
|
50
|
+
// them for file:line annotations; tally() reads only id/raiser/severity.
|
|
51
|
+
return findings.map(f => ({
|
|
52
|
+
id: toGlobalId(letter, f.id), raiser, severity: f.severity, claim: f.claim,
|
|
53
|
+
location: f.location,
|
|
54
|
+
}));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Translate a judge's anonymized ranking (labels; ties as nested arrays) into
|
|
59
|
+
* a tally rankings[].order array of model ids via the private label map.
|
|
60
|
+
* @param {Array<string|string[]>} ranking
|
|
61
|
+
* @param {Object<string, string>} labelMap
|
|
62
|
+
* @returns {{order: Array<string|string[]>, errors: string[]}}
|
|
63
|
+
*/
|
|
64
|
+
function rankingToOrder(ranking, labelMap) {
|
|
65
|
+
const errors = [];
|
|
66
|
+
const mapOne = (label) => {
|
|
67
|
+
const model = labelMap[label];
|
|
68
|
+
if (!model) { errors.push(`unknown label '${label}'`); }
|
|
69
|
+
return model;
|
|
70
|
+
};
|
|
71
|
+
const order = (Array.isArray(ranking) ? ranking : [])
|
|
72
|
+
.map(slot => (Array.isArray(slot) ? slot.map(mapOne) : mapOne(slot)));
|
|
73
|
+
return { order, errors };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
module.exports = { assignLabels, toGlobalId, toGlobalFindings, rankingToOrder, LETTERS };
|