amicus 4.6.2 → 4.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +349 -0
- package/README.md +24 -13
- package/bin/amicus.js +31 -0
- package/docs/ROADMAP.md +172 -36
- package/docs/configuration.md +56 -6
- package/docs/council.md +63 -10
- package/docs/doc-system.md +8 -7
- package/docs/schemas.md +10 -1
- package/docs/troubleshooting.md +27 -1
- package/docs/usage.md +68 -15
- package/electron/workspace-ui/index.html +3 -0
- package/electron/workspace-ui/live-model.js +132 -21
- package/electron/workspace-ui/workspace-app.js +20 -4
- package/electron/workspace-ui/workspace-lazy.js +233 -0
- package/electron/workspace-ui/workspace-matrix.js +12 -1
- package/electron/workspace-ui/workspace-panels.js +24 -171
- package/electron/workspace-ui/workspace-render.js +15 -5
- package/electron/workspace-ui/workspace-seats.js +88 -5
- package/electron/workspace-ui/workspace-verbs.js +1 -1
- package/electron/workspace-ui/workspace.css +6 -0
- package/package.json +5 -2
- package/schemas/council-run.schema.json +1 -0
- package/schemas/council-stats.schema.json +9 -1
- package/schemas/run.schema.json +2 -1
- package/schemas/spend.schema.json +1 -1
- package/schemas/wave.schema.json +2 -1
- package/skills/second-opinion/MANUAL-ORCHESTRATION.md +12 -0
- package/skills/second-opinion/MODEL-NOTES.md +5 -4
- package/skills/sidecar/SKILL.md +7 -2
- package/src/cli-council-run-bench.js +86 -0
- package/src/cli-handlers-council-run.js +65 -81
- package/src/cli-handlers-council.js +24 -3
- package/src/cli-handlers-doctor.js +9 -3
- package/src/cli-handlers-fanout.js +179 -0
- package/src/cli-handlers-pack.js +24 -10
- package/src/cli-handlers-run.js +19 -161
- package/src/cli-template-args.js +48 -0
- package/src/cli.js +39 -46
- package/src/council/debate.js +89 -10
- package/src/council/ledger.js +72 -11
- package/src/council/presets-cli.js +6 -2
- package/src/council/report.js +17 -6
- package/src/council/run-assemble.js +15 -3
- package/src/council/run-budget.js +2 -2
- package/src/council/run-chair.js +70 -11
- package/src/council/run-debate.js +51 -67
- package/src/council/run-launch.js +9 -2
- package/src/council/run-retry.js +4 -1
- package/src/council/run-stage1-launch.js +94 -0
- package/src/council/run-stage2.js +25 -4
- package/src/council/run-stages.js +79 -86
- package/src/council/run-state.js +10 -2
- package/src/council/run.js +26 -2
- package/src/council/tally.js +6 -2
- package/src/mcp-council-awareness.js +1 -0
- package/src/mcp-council-bench.js +4 -0
- package/src/mcp-council-run.js +10 -0
- package/src/mcp-server.js +114 -54
- package/src/mcp-tools.js +12 -5
- package/src/pack/pack-cli.js +1 -1
- package/src/pack/pack-forward.js +12 -4
- package/src/pack/pack-resolve.js +3 -0
- package/src/pack/pack-store.js +20 -3
- package/src/pack/pack-validate.js +5 -1
- package/src/session-manager.js +6 -2
- package/src/sidecar/budget.js +38 -4
- package/src/sidecar/fanout-budget.js +1 -2
- package/src/sidecar/fanout-leg-fallback.js +7 -3
- package/src/sidecar/fanout-wave-io.js +13 -1
- package/src/sidecar/fanout.js +11 -9
- package/src/sidecar/list-limit.js +50 -0
- package/src/sidecar/list-search.js +69 -0
- package/src/sidecar/read.js +90 -5
- package/src/sidecar/start-metadata.js +58 -0
- package/src/sidecar/start.js +8 -43
- package/src/sidecar/workspace-auto-open.js +2 -2
- package/src/spend-query.js +2 -1
- package/src/template/apply.js +7 -4
- package/src/template/render.js +6 -2
- package/src/template/store.js +1 -1
- package/src/utils/alias-audit.js +19 -0
- package/src/utils/cli-preflight.js +27 -1
- package/src/utils/config.js +15 -0
- package/src/utils/curated-models.js +43 -7
- package/src/utils/gateway-route-audit.js +16 -3
- package/src/utils/model-fetcher.js +8 -6
- package/src/utils/remediation-hints.js +14 -0
- package/src/utils/result-schema-rebuild.js +1 -0
- package/src/utils/result-schema.js +6 -1
- package/src/utils/session-index-tmp-sweep.js +18 -3
- package/src/utils/session-index.js +1 -0
- package/src/utils/session-metadata-tmp-sweep.js +156 -0
- package/src/utils/spend-ledger.js +11 -4
- package/src/utils/validators.js +16 -0
|
@@ -41,6 +41,42 @@
|
|
|
41
41
|
(function () {
|
|
42
42
|
'use strict';
|
|
43
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Aliases of seats whose degrade record says they were retried. PR1F-4 (v4.7 PR7).
|
|
46
|
+
*
|
|
47
|
+
* ⚠️ Mirrors window.AmicusLive.deadSeats' own predicate (live-model.js:227-241) EXACTLY, and
|
|
48
|
+
* must keep mirroring it. The kind/channel filter is load-bearing: run.degrades[] also carries
|
|
49
|
+
* kind:'heal' / channel:'stage1-retry' records with the SAME retryWaveId/firstFailure fields
|
|
50
|
+
* for seats that RECOVERED, and a field-only scan would tag a recovered seat "retried once".
|
|
51
|
+
*
|
|
52
|
+
* ⚠️ firstFailure is TRUTHINESS ONLY. It has two shapes — run-retry.js:98 emits
|
|
53
|
+
* {seat, class:'leg', status, reason}; :86/:90/:93 emit {seat, class:'wave', waveId, reason}
|
|
54
|
+
* with NO status key — so any read of firstFailure.status is undefined on every wave-origin
|
|
55
|
+
* seat.
|
|
56
|
+
*/
|
|
57
|
+
function retriedAliases(degrades) {
|
|
58
|
+
var out = Object.create(null);
|
|
59
|
+
(degrades || []).forEach(function (d) {
|
|
60
|
+
if (!d || d.kind !== 'degrade') { return; }
|
|
61
|
+
if (d.channel !== 'dead-leg' && d.channel !== 'dead-wave') { return; }
|
|
62
|
+
var data = d.data || {};
|
|
63
|
+
if (!(data.retryWaveId || data.firstFailure)) { return; }
|
|
64
|
+
if (d.channel === 'dead-leg') {
|
|
65
|
+
if (data.seat) { out[data.seat] = true; }
|
|
66
|
+
} else {
|
|
67
|
+
(data.models || []).forEach(function (m) { if (m) { out[m] = true; } });
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Mirrors isReviewing at live-model.js:261-264 — a chair/judge/rebuttal/revote row must not
|
|
74
|
+
// carry a reviewer's retry marker.
|
|
75
|
+
function isReviewingRole(role) {
|
|
76
|
+
return role === 'seat' || role === 'critic' ||
|
|
77
|
+
(typeof role === 'string' && role.indexOf('lens:') === 0);
|
|
78
|
+
}
|
|
79
|
+
|
|
44
80
|
function renderSeatsPanel() {
|
|
45
81
|
var A = window.AmicusApp;
|
|
46
82
|
var d = A.state.detail;
|
|
@@ -48,13 +84,52 @@
|
|
|
48
84
|
var tbody = A.$('seats-body');
|
|
49
85
|
window.AmicusRender.renderSeats(tbody, seats, A.state.blind, A.labelOf);
|
|
50
86
|
var seatLoss = d.verdict && d.verdict.seatLoss;
|
|
51
|
-
var
|
|
87
|
+
var runMeta = { critic: (d.run && d.run.critic) || null };
|
|
88
|
+
// Source-selection (v4.6.3 PR2, spec D4): run-degrade.js swallows checkpoint failures, so
|
|
89
|
+
// verdict.json can carry degrade records run.json's own checkpoint lost — fall back to it
|
|
90
|
+
// ONLY when run.degrades is empty/absent. A fallback, never a union: both docs can carry
|
|
91
|
+
// records for the SAME run, and the persisted run.json copy is authoritative when present.
|
|
92
|
+
var deg = (d.run && d.run.degrades && d.run.degrades.length) ? d.run.degrades
|
|
93
|
+
: ((d.verdict && d.verdict.degrades) || []);
|
|
94
|
+
var retried = retriedAliases(deg);
|
|
95
|
+
// ⚠️ Look rows up by data-key, NEVER by position. renderSeats (workspace-render.js:179-216)
|
|
96
|
+
// keys every row on String(seat.id || seat.model) and RN-11 made it REORDER rows to match the
|
|
97
|
+
// composed doc's leg order — so tbody.children[i] is not seats[i]. Build the key exactly the
|
|
98
|
+
// way renderSeats does or the lookup silently misses.
|
|
99
|
+
var rowsByKey = Object.create(null);
|
|
100
|
+
Array.prototype.slice.call(tbody.children).forEach(function (row) {
|
|
101
|
+
rowsByKey[row.dataset.key] = row;
|
|
102
|
+
});
|
|
103
|
+
seats.forEach(function (s) {
|
|
104
|
+
var row = rowsByKey[String(s.id || s.model)];
|
|
105
|
+
if (!row || !row.children[8]) { return; }
|
|
106
|
+
// Column 8 is the table's unlabeled trailing flag cell (index.html:51's final <th></th>).
|
|
107
|
+
// It carries '⏳ stalled' on the LIVE path; on this terminal path seatsFromRunStats
|
|
108
|
+
// hardcodes stalled:false (live-model.js:128), so it is always empty here and free to use.
|
|
109
|
+
// If that ever changes, this is the collision site.
|
|
110
|
+
// Fix wave (whole-branch review, finding 2): this pass must be SYMMETRIC. renderSeats
|
|
111
|
+
// reuses rows keyed on `model:role` across calls — including across two different
|
|
112
|
+
// terminal runs opened in sequence that happen to share an alias+role — and never resets
|
|
113
|
+
// row.className itself. An add-only write here both duplicates the token on every repaint
|
|
114
|
+
// of the SAME run and leaves a stale 'seat-retried' class on a row that belonged to a
|
|
115
|
+
// PREVIOUS run's non-retried seat. classList.add/remove (not string concatenation) so a
|
|
116
|
+
// repeat add never duplicates the token and a seat that is no longer retried gets cleared.
|
|
117
|
+
var isRetried = isReviewingRole(s.role) && !!retried[s.modelInput || s.model];
|
|
118
|
+
if (isRetried) {
|
|
119
|
+
row.classList.add('seat-retried');
|
|
120
|
+
row.children[8].textContent = '↻ retried once';
|
|
121
|
+
} else {
|
|
122
|
+
row.classList.remove('seat-retried');
|
|
123
|
+
row.children[8].textContent = '';
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
var dead = window.AmicusLive.deadSeats(deg, seatLoss, seats, runMeta);
|
|
52
127
|
renderDeadSeatRows(tbody, dead, A.state.blind, A.labelOf);
|
|
53
128
|
}
|
|
54
129
|
|
|
55
130
|
/**
|
|
56
131
|
* Paints the dead-seat rows appended after live rows. Deliberately NOT
|
|
57
|
-
* folded into workspace-render.js's renderSeats (
|
|
132
|
+
* folded into workspace-render.js's renderSeats (293/300 — must not grow)
|
|
58
133
|
* and NOT run through its keyed diff: dead rows carry no per-tick-changing
|
|
59
134
|
* field, so a full rebuild every call is correct and cheap, and renderSeats
|
|
60
135
|
* just above already self-cleans any PRIOR dead row as an unrecognized
|
|
@@ -74,7 +149,7 @@
|
|
|
74
149
|
function renderDeadSeatRows(tbody, dead, blindOn, labelOf) {
|
|
75
150
|
(dead || []).forEach(function (seat) {
|
|
76
151
|
var cells = window.AmicusLive.seatCells(
|
|
77
|
-
{ model: seat.model, status: seat.statusText, stalled: false }, blindOn, labelOf);
|
|
152
|
+
{ model: seat.model, role: seat.role, status: seat.statusText, stalled: false }, blindOn, labelOf);
|
|
78
153
|
// Fix wave 2 (smoke-caught, GUI smoke on real degraded run 12c96b6b): dead seats never
|
|
79
154
|
// produce a review, so state.labelByModel (built from the run's names derivation — models
|
|
80
155
|
// that DID review) never carries them; seatCells' own `blindOn && label ? label : alias`
|
|
@@ -88,7 +163,7 @@
|
|
|
88
163
|
{ className: 'seat-dead', dataset: { key: 'dead:' + seat.model } },
|
|
89
164
|
cells.map(function (c, i) {
|
|
90
165
|
return window.AmicusRender.el('td',
|
|
91
|
-
{ className:
|
|
166
|
+
{ className: window.AmicusRender.seatCellClass(i) }, [c]);
|
|
92
167
|
}));
|
|
93
168
|
tbody.appendChild(row);
|
|
94
169
|
});
|
|
@@ -105,7 +180,15 @@
|
|
|
105
180
|
var A = window.AmicusApp;
|
|
106
181
|
var d = A.state.detail;
|
|
107
182
|
var seatLoss = d && d.verdict ? d.verdict.seatLoss : null;
|
|
108
|
-
var
|
|
183
|
+
var runMeta = { critic: (d && d.run && d.run.critic) || null };
|
|
184
|
+
// Source-selection (v4.6.3 PR2, spec D4), live-path twin of renderSeatsPanel's fallback
|
|
185
|
+
// above: the tick's own live.degrades wins when non-empty; state.detail.verdict.degrades is
|
|
186
|
+
// usually absent mid-run (verdict.json doesn't exist until the run finishes) — fine, this
|
|
187
|
+
// branch only matters for the rare same-run reopen where a prior terminal fetch already
|
|
188
|
+
// populated state.detail.verdict.
|
|
189
|
+
var deg = (live.degrades && live.degrades.length) ? live.degrades
|
|
190
|
+
: ((d && d.verdict && d.verdict.degrades) || []);
|
|
191
|
+
var dead = window.AmicusLive.deadSeats(deg, seatLoss, live.seats || [], runMeta);
|
|
109
192
|
renderDeadSeatRows(A.$('seats-body'), dead, A.state.blind, A.labelOf);
|
|
110
193
|
}
|
|
111
194
|
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
var A = window.AmicusApp;
|
|
67
67
|
stopLiveLoop();
|
|
68
68
|
var d = A.state.detail;
|
|
69
|
-
if (!d || !d.run || window.AmicusLive.
|
|
69
|
+
if (!d || !d.run || window.AmicusLive.isTerminal(d.run.status)) { return; }
|
|
70
70
|
var epoch = A.state.liveEpoch; // F42: stopLiveLoop() above just bumped it; this chain owns it
|
|
71
71
|
var tick = function () {
|
|
72
72
|
// F42: pin the id PER TICK and SEND the pinned id — matching the reply against
|
|
@@ -176,3 +176,9 @@ td.vote-cell.dispute { cursor: pointer; text-decoration: underline dotted; }
|
|
|
176
176
|
|
|
177
177
|
.empty-note { color: var(--text-3); font-size: var(--fs-12); }
|
|
178
178
|
.truncate-note { color: var(--warn); font-size: var(--fs-11); margin-top: var(--space-3); }
|
|
179
|
+
|
|
180
|
+
/* PR1F-4: a seat that was retried and still failed — the surviving errored row's own marker.
|
|
181
|
+
⚠️ Plan snippet said var(--muted), which does not exist in src/design/tokens.css (verified
|
|
182
|
+
repo-wide). --text-3 is this file's own established "muted" token — see tr.seat-dead td
|
|
183
|
+
above, the sibling dead-row treatment. */
|
|
184
|
+
.seat-retried td:last-child { color: var(--text-3); white-space: nowrap; }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "amicus",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.7.0",
|
|
4
4
|
"mcpName": "io.github.BourbonDog/amicus",
|
|
5
5
|
"description": "Multi-model LLM Council + parallel AI window for Claude Code. Run structured council reviews across Gemini, GPT, DeepSeek and more — or fork a conversation to any model and fold the results back.",
|
|
6
6
|
"keywords": [
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"test:all": "jest --testPathIgnorePatterns='/node_modules/' --testPathIgnorePatterns='worktrees' && node scripts/mark-test-passed.js",
|
|
56
56
|
"test:e2e:mcp": "jest tests/mcp-repomix-e2e.integration.test.js --testTimeout=180000 --forceExit",
|
|
57
57
|
"posttest": "node scripts/mark-test-passed.js",
|
|
58
|
-
"lint": "eslint src/ electron/",
|
|
58
|
+
"lint": "eslint src/ electron/ tests/helpers/",
|
|
59
59
|
"postinstall": "node scripts/postinstall.js",
|
|
60
60
|
"test:thinking": "node scripts/benchmark-thinking.js",
|
|
61
61
|
"test:thinking:quick": "MODELS=gemini node scripts/benchmark-thinking.js",
|
|
@@ -107,6 +107,9 @@
|
|
|
107
107
|
],
|
|
108
108
|
"electron/**/*.js": [
|
|
109
109
|
"eslint --fix"
|
|
110
|
+
],
|
|
111
|
+
"tests/helpers/**/*.js": [
|
|
112
|
+
"eslint --fix"
|
|
110
113
|
]
|
|
111
114
|
}
|
|
112
115
|
}
|
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
"labelMap": { "type": ["object", "null"], "additionalProperties": { "type": "string" } },
|
|
46
46
|
"options": { "type": "object" },
|
|
47
47
|
"pack": { "type": "object" },
|
|
48
|
+
"tag": { "type": "string" },
|
|
48
49
|
"template": { "type": "object" },
|
|
49
50
|
"usage": { "type": "object" },
|
|
50
51
|
"exitCode": { "type": ["number", "null"] },
|
|
@@ -20,7 +20,15 @@
|
|
|
20
20
|
"avgStreetCredPeersOnly": { "type": ["number", "null"] },
|
|
21
21
|
"lifetimeConfirmRate": { "type": ["number", "null"] },
|
|
22
22
|
"lifetimeFactErrorRate": { "type": ["number", "null"] },
|
|
23
|
-
"conformance": { "type": "object" }
|
|
23
|
+
"conformance": { "type": "object" },
|
|
24
|
+
"aliases": {
|
|
25
|
+
"type": "array", "items": { "type": "string" },
|
|
26
|
+
"description": "Every alias observed for this group, most recently observed first — aliases[0] is the launch-preferred name (v4.7 GOA-7)."
|
|
27
|
+
},
|
|
28
|
+
"legacy": {
|
|
29
|
+
"type": "boolean",
|
|
30
|
+
"description": "True when every row in the group lacks resolvedModel — alias-keyed history from before resolved-id segmentation, or leg-less rows whose resolution is unknowable (v4.7 GOA-7, spec R2)."
|
|
31
|
+
}
|
|
24
32
|
}
|
|
25
33
|
}
|
|
26
34
|
}
|
package/schemas/run.schema.json
CHANGED
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
},
|
|
34
34
|
"credit": { "type": ["object", "null"] },
|
|
35
35
|
"filters": { "type": "object" },
|
|
36
|
-
"groupBy": { "enum": ["model", "wave", "council", "project", "op", "day"] },
|
|
36
|
+
"groupBy": { "enum": ["model", "wave", "council", "project", "op", "day", "tag"] },
|
|
37
37
|
"groups": {
|
|
38
38
|
"type": "array",
|
|
39
39
|
"items": {
|
package/schemas/wave.schema.json
CHANGED
|
@@ -146,6 +146,18 @@ As each judge's ranking + adjudication response returns, collect it (the raw per
|
|
|
146
146
|
3. **Translate each judge's `FINAL RANKING:` block** — convert the label order (`1. Review C / 2. Review A / 3. Review B`) into a model `order` array via the same map (e.g. `{C→mistral, A→deepseek, B→gpt}` ⇒ `order: ["mistral","deepseek","gpt"]`). This is each entry in `rankings[]`.
|
|
147
147
|
4. **Populate `runStats`** from the per-leg run documents emitted by `fanout --json` (and any solo red-team/chair `start --json` docs): copy `model`, `status`, `durationMs`, `usage` verbatim. Any leg with no run doc gets `durationMs: null` and `usage: null` — never invent a value. Attach `role` (`council` | `redteam` | `claude`), `wasChair`, and `conformance` (`clean` | `repaired` | `unstructured`) as council-domain labels.
|
|
148
148
|
|
|
149
|
+
⚠️ **v4.7 CA-4 note:** the headless engine driver (`amicus council run`) now emits extra
|
|
150
|
+
non-primary `runStats` rows (`chair-attempt`/`repair`/`superseded` — failed chair launches,
|
|
151
|
+
repair solos, and superseded legs) alongside one seat-primary row per model. This manual
|
|
152
|
+
orchestration path is unaffected and still produces exactly one seat-primary row per model as
|
|
153
|
+
described above — no contract change here — but any code or report template reading `runStats`
|
|
154
|
+
should be a **tolerant reader** (filter by role rather than assume one row per model), since an
|
|
155
|
+
engine-produced tally.json can now carry rows this recipe never does. Hand-assembled `runStats`
|
|
156
|
+
rows carry no `resolvedModel`; their ledger rows therefore aggregate as alias-keyed `legacy`
|
|
157
|
+
groups in `council stats` (legacy-by-absence, by design) — expected, not an error. Add
|
|
158
|
+
`resolvedModel` (the executable id that served) to a row only if you know it; never copy the
|
|
159
|
+
alias into it.
|
|
160
|
+
|
|
149
161
|
**Five-keys checklist — verify `tally-input.json` has ALL of:** `meta` (with `meta.models`), `findings`, `adjudications`, `rankings`, `runStats` (`runStats` may be `[]`; the other four are required). Do not call `tally` until all five are present.
|
|
150
162
|
|
|
151
163
|
Then call, saving the printed `record` to `<run-folder>/tally.json` (Stage 5's `amicus council verdict` reads it back from disk):
|
|
@@ -101,10 +101,11 @@ the peer-consensus≠evidence rule upstreamed from the field ledger; see changel
|
|
|
101
101
|
OpenRouter balance — gemini/gpt/anthropic bill directly against their own keys, so inferring cost
|
|
102
102
|
from that balance under-reports it badly (observed: ~6x low). A 3-model bench + chair + debate is
|
|
103
103
|
roughly **$0.60-0.80 per run**, not cents; budget `--max-cost` accordingly or the chair gets
|
|
104
|
-
skipped mid-run (exit 2, degraded) when the debate legs push the total past the ceiling.
|
|
105
|
-
v4.
|
|
106
|
-
|
|
107
|
-
`role
|
|
104
|
+
skipped mid-run (exit 2, degraded) when the debate legs push the total past the ceiling. Totals
|
|
105
|
+
read higher again as of v4.7 — repairs, failed chair attempts, and superseded legs now get their
|
|
106
|
+
own `runStats` rows too — so anything keying `runStats` by model must use an **allowlist**, not a
|
|
107
|
+
judge exclusion; see `docs/council.md`'s `runStats[].role` roster (under `amicus council tally`)
|
|
108
|
+
for the exact set.
|
|
108
109
|
- **Expect agreement inflation in Stage-2 adjudication.** The judge contract defines `agree` by
|
|
109
110
|
worked example ("an 'I missed this — it's valid' counts as agree") but gives no example for
|
|
110
111
|
`dispute` and no positive definition of `neutral`, while requiring a verdict on EVERY finding —
|
package/skills/sidecar/SKILL.md
CHANGED
|
@@ -367,16 +367,21 @@ The catalog is cached at `~/.config/amicus/model-catalog.json` and refreshes aut
|
|
|
367
367
|
```bash
|
|
368
368
|
amicus list
|
|
369
369
|
amicus list --status complete
|
|
370
|
-
amicus list --all
|
|
371
|
-
amicus list --
|
|
370
|
+
amicus list --all # All projects
|
|
371
|
+
amicus list --search foo # Substring match: id, tag, briefing material
|
|
372
|
+
amicus list --json # Output as JSON
|
|
372
373
|
```
|
|
373
374
|
|
|
374
375
|
**Optional:**
|
|
375
376
|
- `--status <filter>`: Filter by status (`running`, `complete`)
|
|
376
377
|
- `--all`: Show sessions from all projects
|
|
378
|
+
- `--search <q>`: Case-insensitive substring filter over id/tag/briefing material
|
|
377
379
|
- `--json`: Output as JSON format (for programmatic use)
|
|
378
380
|
- `--cwd <path>`: Project directory (default: current directory)
|
|
379
381
|
|
|
382
|
+
Rows show the `--tag <t>` label set at launch time (`start`/`fanout`/`council run` —
|
|
383
|
+
not a `list` flag itself); untagged sessions show blank.
|
|
384
|
+
|
|
380
385
|
### Resume a Sidecar
|
|
381
386
|
|
|
382
387
|
```bash
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bench and input resolution for the council run command.
|
|
3
|
+
*
|
|
4
|
+
* Exports parseList, sanitizeCouncilName, resolveBench, extracted verbatim
|
|
5
|
+
* from cli-handlers-council-run.js (v4.7 PR0). ⚠️ The top-level cli*.js
|
|
6
|
+
* name is LOAD-BEARING: the known-flags source scan covers only src/cli*.js,
|
|
7
|
+
* and resolveBench reads args['dropped-members'].
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
'use strict';
|
|
11
|
+
|
|
12
|
+
const { failJson, ERROR_CODES } = require('./utils/error-doc');
|
|
13
|
+
|
|
14
|
+
function parseList(value) {
|
|
15
|
+
return String(value).split(',').map(s => s.trim()).filter(Boolean);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Sanitize the internal `--council-name` passthrough before it can reach the
|
|
20
|
+
* spend ledger's `councilName` column (v4.3 Task 4 review fix, spec §7.3:
|
|
21
|
+
* spend docs hold only ids/numbers/paths "by construction"). That value is
|
|
22
|
+
* user-supplied (via mcp-council-run.js, ultimately an MCP caller's `input`),
|
|
23
|
+
* unbounded, and unvalidated — unlike a real `--council <preset>`, which is
|
|
24
|
+
* catalog-validated upstream. Strips control/non-printable characters, trims,
|
|
25
|
+
* and caps length so a hostile/malformed passthrough can't land raw in a
|
|
26
|
+
* `--group-by council` rollup. Precedence is untouched by this: it's applied
|
|
27
|
+
* only to the passthrough branch, never to the catalog-validated preset name.
|
|
28
|
+
* @param {string} name @returns {string|null} sanitized name, or null if empty after cleanup
|
|
29
|
+
*/
|
|
30
|
+
function sanitizeCouncilName(name) {
|
|
31
|
+
// eslint-disable-next-line no-control-regex -- deliberately stripping C0/DEL control chars
|
|
32
|
+
const cleaned = String(name).replace(/[\x00-\x1F\x7F]/g, '').trim().slice(0, 64);
|
|
33
|
+
return cleaned || null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Resolve bench models from --models XOR --council (mirrors handleFanout).
|
|
38
|
+
* Also returns `presetName` (v4.3 Task 3, spec §7.1: trimmed --council name,
|
|
39
|
+
* else null) and `droppedMembers`: a preset's own drops, or — bare --models —
|
|
40
|
+
* the parsed `--dropped-members` MCP→child passthrough (v4.6 Plan 4 Task 4b).
|
|
41
|
+
* Parallel twin: mcp-council-bench.js's `resolveBenchInput` hand-rolls the same
|
|
42
|
+
* models-XOR-council wrapper around the shared `resolveCouncilMembers` core.
|
|
43
|
+
* They have already diverged (this side has a third guard for a valueless
|
|
44
|
+
* --council, and the min-seat rule lives in both callers, not here) — change
|
|
45
|
+
* a validation rule on one side, change the other.
|
|
46
|
+
*/
|
|
47
|
+
function resolveBench(args, useJson) {
|
|
48
|
+
const hasModels = typeof args.models === 'string' && args.models.trim();
|
|
49
|
+
const hasCouncil = args.council !== undefined && args.council !== false;
|
|
50
|
+
if (hasModels && hasCouncil) {
|
|
51
|
+
return { fail: failJson(useJson, { code: ERROR_CODES.BAD_ARGS,
|
|
52
|
+
message: 'Error: pass exactly one of --models / --council, not both' }) };
|
|
53
|
+
}
|
|
54
|
+
if (!hasModels && !hasCouncil) {
|
|
55
|
+
return { fail: failJson(useJson, { code: ERROR_CODES.BAD_ARGS,
|
|
56
|
+
message: 'Error: council run needs --models a,b,c or --council <preset> (at least 2 seats)' }) };
|
|
57
|
+
}
|
|
58
|
+
if (hasCouncil) {
|
|
59
|
+
if (typeof args.council !== 'string' || !args.council.trim()) {
|
|
60
|
+
return { fail: failJson(useJson, { code: ERROR_CODES.BAD_ARGS,
|
|
61
|
+
message: 'Error: --council requires a council name (e.g. --council budget)' }) };
|
|
62
|
+
}
|
|
63
|
+
const { resolveCouncilMembers } = require('./utils/config');
|
|
64
|
+
const { readCache } = require('./utils/model-catalog');
|
|
65
|
+
const catalog = (readCache() || {}).models || [];
|
|
66
|
+
const presetName = args.council.trim();
|
|
67
|
+
const expanded = resolveCouncilMembers(presetName, catalog);
|
|
68
|
+
if (expanded.error) {
|
|
69
|
+
return { fail: failJson(useJson, { code: ERROR_CODES.BAD_ARGS, message: `Error: ${expanded.error}` }) };
|
|
70
|
+
}
|
|
71
|
+
// v4.5 Wave 2 → Plan 4 Task 4: threaded into runCouncil's options — the
|
|
72
|
+
// sink now announces each dropped member, with reason, on every transport and surface.
|
|
73
|
+
return { bench: expanded.models, presetName, droppedMembers: expanded.droppedMembers || [] };
|
|
74
|
+
}
|
|
75
|
+
if (args['dropped-members'] === undefined) {
|
|
76
|
+
return { bench: parseList(args.models), presetName: null, droppedMembers: [] };
|
|
77
|
+
}
|
|
78
|
+
let dm; try { dm = JSON.parse(args['dropped-members']); } catch { dm = null; }
|
|
79
|
+
if (!Array.isArray(dm) || !dm.every(d => d && typeof d.member === 'string' && typeof d.reason === 'string')) {
|
|
80
|
+
return { fail: failJson(useJson, { code: ERROR_CODES.BAD_ARGS,
|
|
81
|
+
message: 'Error: --dropped-members must be a JSON array of {member, reason} entries' }) };
|
|
82
|
+
}
|
|
83
|
+
return { bench: parseList(args.models), presetName: null, droppedMembers: dm };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
module.exports = { parseList, sanitizeCouncilName, resolveBench };
|
|
@@ -11,82 +11,17 @@
|
|
|
11
11
|
|
|
12
12
|
const path = require('path');
|
|
13
13
|
const { failJson, buildErrorDoc, ERROR_CODES } = require('./utils/error-doc');
|
|
14
|
-
const { validateTaskId } = require('./utils/validators');
|
|
14
|
+
const { validateTaskId, validateTag } = require('./utils/validators');
|
|
15
15
|
const { GATEWAY_MODES } = require('./utils/model-descriptor');
|
|
16
16
|
// v4.6 Plan 4 Task 2: renderRunHuman moved to its own leaf (size gate); this
|
|
17
17
|
// file re-exports it below so every existing require() of this path still
|
|
18
18
|
// resolves it unchanged.
|
|
19
19
|
const { renderRunHuman } = require('./cli-council-run-render');
|
|
20
|
+
const { parseList, sanitizeCouncilName, resolveBench } = require('./cli-council-run-bench');
|
|
21
|
+
const { applyTemplateForArgs } = require('./cli-template-args');
|
|
20
22
|
|
|
21
23
|
const CHAIR_DEFAULT = 'deepseek';
|
|
22
24
|
|
|
23
|
-
function parseList(value) {
|
|
24
|
-
return String(value).split(',').map(s => s.trim()).filter(Boolean);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Sanitize the internal `--council-name` passthrough before it can reach the
|
|
29
|
-
* spend ledger's `councilName` column (v4.3 Task 4 review fix, spec §7.3:
|
|
30
|
-
* spend docs hold only ids/numbers/paths "by construction"). That value is
|
|
31
|
-
* user-supplied (via mcp-council-run.js, ultimately an MCP caller's `input`),
|
|
32
|
-
* unbounded, and unvalidated — unlike a real `--council <preset>`, which is
|
|
33
|
-
* catalog-validated upstream. Strips control/non-printable characters, trims,
|
|
34
|
-
* and caps length so a hostile/malformed passthrough can't land raw in a
|
|
35
|
-
* `--group-by council` rollup. Precedence is untouched by this: it's applied
|
|
36
|
-
* only to the passthrough branch, never to the catalog-validated preset name.
|
|
37
|
-
* @param {string} name @returns {string|null} sanitized name, or null if empty after cleanup
|
|
38
|
-
*/
|
|
39
|
-
function sanitizeCouncilName(name) {
|
|
40
|
-
// eslint-disable-next-line no-control-regex -- deliberately stripping C0/DEL control chars
|
|
41
|
-
const cleaned = String(name).replace(/[\x00-\x1F\x7F]/g, '').trim().slice(0, 64);
|
|
42
|
-
return cleaned || null;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Resolve bench models from --models XOR --council (mirrors handleFanout).
|
|
47
|
-
* Also returns `presetName` (v4.3 Task 3, spec §7.1: trimmed --council name,
|
|
48
|
-
* else null) and `droppedMembers`: a preset's own drops, or — bare --models —
|
|
49
|
-
* the parsed `--dropped-members` MCP→child passthrough (v4.6 Plan 4 Task 4b).
|
|
50
|
-
*/
|
|
51
|
-
function resolveBench(args, useJson) {
|
|
52
|
-
const hasModels = typeof args.models === 'string' && args.models.trim();
|
|
53
|
-
const hasCouncil = args.council !== undefined && args.council !== false;
|
|
54
|
-
if (hasModels && hasCouncil) {
|
|
55
|
-
return { fail: failJson(useJson, { code: ERROR_CODES.BAD_ARGS,
|
|
56
|
-
message: 'Error: pass exactly one of --models / --council, not both' }) };
|
|
57
|
-
}
|
|
58
|
-
if (!hasModels && !hasCouncil) {
|
|
59
|
-
return { fail: failJson(useJson, { code: ERROR_CODES.BAD_ARGS,
|
|
60
|
-
message: 'Error: council run needs --models a,b,c or --council <preset> (at least 2 seats)' }) };
|
|
61
|
-
}
|
|
62
|
-
if (hasCouncil) {
|
|
63
|
-
if (typeof args.council !== 'string' || !args.council.trim()) {
|
|
64
|
-
return { fail: failJson(useJson, { code: ERROR_CODES.BAD_ARGS,
|
|
65
|
-
message: 'Error: --council requires a council name (e.g. --council budget)' }) };
|
|
66
|
-
}
|
|
67
|
-
const { resolveCouncilMembers } = require('./utils/config');
|
|
68
|
-
const { readCache } = require('./utils/model-catalog');
|
|
69
|
-
const catalog = (readCache() || {}).models || [];
|
|
70
|
-
const presetName = args.council.trim();
|
|
71
|
-
const expanded = resolveCouncilMembers(presetName, catalog);
|
|
72
|
-
if (expanded.error) {
|
|
73
|
-
return { fail: failJson(useJson, { code: ERROR_CODES.BAD_ARGS, message: `Error: ${expanded.error}` }) };
|
|
74
|
-
}
|
|
75
|
-
// v4.5 Wave 2 → Plan 4 Task 4: threaded into runCouncil's options — the
|
|
76
|
-
// sink now announces each dropped member, with reason, on every transport and surface.
|
|
77
|
-
return { bench: expanded.models, presetName, droppedMembers: expanded.droppedMembers || [] };
|
|
78
|
-
}
|
|
79
|
-
if (args['dropped-members'] === undefined) {
|
|
80
|
-
return { bench: parseList(args.models), presetName: null, droppedMembers: [] };
|
|
81
|
-
}
|
|
82
|
-
let dm; try { dm = JSON.parse(args['dropped-members']); } catch { dm = null; }
|
|
83
|
-
if (!Array.isArray(dm) || !dm.every(d => d && typeof d.member === 'string' && typeof d.reason === 'string')) {
|
|
84
|
-
return { fail: failJson(useJson, { code: ERROR_CODES.BAD_ARGS,
|
|
85
|
-
message: 'Error: --dropped-members must be a JSON array of {member, reason} entries' }) };
|
|
86
|
-
}
|
|
87
|
-
return { bench: parseList(args.models), presetName: null, droppedMembers: dm };
|
|
88
|
-
}
|
|
89
|
-
|
|
90
25
|
/**
|
|
91
26
|
* Default real helpers; tests override via depsOverride (mirrors
|
|
92
27
|
* cli-handlers-spend.js's realDeps()/depsOverride convention).
|
|
@@ -112,6 +47,27 @@ async function handleCouncilRun(args, depsOverride = {}) {
|
|
|
112
47
|
// existing application point exactly like a typed --template.
|
|
113
48
|
let packRecord = null;
|
|
114
49
|
const explicitKeys = args.__explicit || new Set();
|
|
50
|
+
// v4.7 PR6: these all parse as boolean `true` when typed without a value
|
|
51
|
+
// (src/cli.js:101) and reached runCouncil as `true`, a NaN, or a bogus path.
|
|
52
|
+
// Voice matches the R5 -o/--out precedent (cli-handlers-council.js:183).
|
|
53
|
+
for (const flag of ['out-dir', 'claude-review', 'run-id']) {
|
|
54
|
+
if (!explicitKeys.has(flag)) { continue; }
|
|
55
|
+
const v = args[flag];
|
|
56
|
+
if (typeof v !== 'string' || v === '') {
|
|
57
|
+
return failJson(useJson, { code: ERROR_CODES.BAD_ARGS, message: `Error: --${flag} requires a value` });
|
|
58
|
+
}
|
|
59
|
+
if (v.startsWith('-')) {
|
|
60
|
+
return failJson(useJson, { code: ERROR_CODES.BAD_ARGS, message: `Error: --${flag} cannot start with '-': got '${v}'` });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// --timeout is DEFAULTS-seeded to 15 (src/cli.js:31), so `!== undefined` proves
|
|
64
|
+
// nothing; NaN is the real hole — it passes the `<= 0` guard below.
|
|
65
|
+
if (explicitKeys.has('timeout') && (typeof args.timeout !== 'number' || !Number.isFinite(args.timeout))) {
|
|
66
|
+
// Do NOT echo args.timeout: parseArgs already ran parseInt, so a typed
|
|
67
|
+
// `--timeout abc` reads back as NaN and quoting it shows the user a value
|
|
68
|
+
// they never typed. Boolean `true` (bare flag) has the same problem.
|
|
69
|
+
return failJson(useJson, { code: ERROR_CODES.BAD_ARGS, message: 'Error: --timeout requires a number' });
|
|
70
|
+
}
|
|
115
71
|
if (args.pack !== undefined) {
|
|
116
72
|
const { applyPackToArgs } = require('./pack/pack-resolve');
|
|
117
73
|
const pr = applyPackToArgs({
|
|
@@ -146,17 +102,14 @@ async function handleCouncilRun(args, depsOverride = {}) {
|
|
|
146
102
|
promptRes = { prompt: undefined, promptMeta: null };
|
|
147
103
|
}
|
|
148
104
|
let templateMeta = null;
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
} else if (args.artifact !== undefined || args.var !== undefined) {
|
|
158
|
-
return failJson(useJson, { code: ERROR_CODES.BAD_ARGS, message: 'Error: --artifact/--var require --template (expansion happens only in template files)' });
|
|
159
|
-
}
|
|
105
|
+
const tpl = applyTemplateForArgs(args, promptRes.prompt, useJson);
|
|
106
|
+
if (tpl.fail !== undefined) { return tpl.fail; }
|
|
107
|
+
// The trailing `templateMeta =` is NOT copy-paste drift against handleFanout's
|
|
108
|
+
// otherwise-identical call: it feeds `template: templateMeta` on the run.json
|
|
109
|
+
// seed below (the `template:` field of the runCouncil options object). Drop it
|
|
110
|
+
// and every --template council run silently records
|
|
111
|
+
// `template: null`. handleFanout has no such field, which is why its call is shorter.
|
|
112
|
+
if (tpl.applied) { promptRes = { prompt: tpl.prompt, promptMeta: tpl.promptMeta }; templateMeta = tpl.templateMeta; }
|
|
160
113
|
|
|
161
114
|
const benchRes = resolveBench(args, useJson);
|
|
162
115
|
if (benchRes.fail !== undefined) { return benchRes.fail; }
|
|
@@ -194,14 +147,29 @@ async function handleCouncilRun(args, depsOverride = {}) {
|
|
|
194
147
|
}
|
|
195
148
|
const lenses = (typeof args.lenses === 'string' && args.lenses.trim()) ? parseList(args.lenses) : null;
|
|
196
149
|
if (critic && lenses) {
|
|
150
|
+
// T11-d: no packSuffix() here (unlike the chair/critic-in-bench checks
|
|
151
|
+
// above) — it would only ever contribute ''. pack-validate.js now rejects
|
|
152
|
+
// a pack supplying both critic and lenses before this handler ever runs
|
|
153
|
+
// (PACK_INVALID, pre-spend, via pack-resolve.js's validatePack call), and
|
|
154
|
+
// pack-resolve.js:140/143 already suppress the mixed pack-field x
|
|
155
|
+
// explicit-flag crossings (a pack-filled critic is skipped when --lenses
|
|
156
|
+
// is explicit, and vice versa). So whenever this branch fires, both
|
|
157
|
+
// critic and lenses are always explicit flags, never pack-attributed.
|
|
197
158
|
return failJson(useJson, { code: ERROR_CODES.BAD_ARGS,
|
|
198
|
-
message:
|
|
159
|
+
message: 'Error: --critic and --lenses are mutually exclusive in v4.0' });
|
|
199
160
|
}
|
|
200
161
|
if (lenses && lenses.length !== bench.length) {
|
|
201
162
|
return failJson(useJson, { code: ERROR_CODES.BAD_ARGS,
|
|
202
163
|
message: `Error: --lenses needs exactly one lens per seat (${bench.length} seats, got ${lenses.length})` });
|
|
203
164
|
}
|
|
204
|
-
|
|
165
|
+
// v4.7 PR6: this check is POST-pack-merge and ungated, so it is the only one a
|
|
166
|
+
// pack-filled value passes through — `timeout` is a legal council pack option
|
|
167
|
+
// (pack-validate.js KIND_OPTIONS) and validatePack checks the key name, never
|
|
168
|
+
// the value type. The old `<= 0` test alone let `{timeout: true}` past (true
|
|
169
|
+
// coerces to 1) and `{timeout: "abc"}` past as NaN, reproducing the very bug
|
|
170
|
+
// the typed-flag guard above closes. Same shape as --max-cost's check below.
|
|
171
|
+
if (args.timeout !== undefined
|
|
172
|
+
&& (typeof args.timeout !== 'number' || !Number.isFinite(args.timeout) || args.timeout <= 0)) {
|
|
205
173
|
return failJson(useJson, { code: ERROR_CODES.BAD_ARGS, message: 'Error: --timeout must be a positive number' });
|
|
206
174
|
}
|
|
207
175
|
const mc = args['max-cost'];
|
|
@@ -212,6 +180,15 @@ async function handleCouncilRun(args, depsOverride = {}) {
|
|
|
212
180
|
return failJson(useJson, { code: ERROR_CODES.BAD_ARGS,
|
|
213
181
|
message: `Error: --gateway must be one of: ${GATEWAY_MODES.join(', ')}` });
|
|
214
182
|
}
|
|
183
|
+
// v4.7 F8 (D13): reject-style (unlike sanitizeCouncilName, which cleans) —
|
|
184
|
+
// a stored tag is a user-chosen search key, so silent truncation/stripping
|
|
185
|
+
// would make --search/--group-by tag miss it.
|
|
186
|
+
if (args.tag !== undefined) {
|
|
187
|
+
const tagCheck = validateTag(args.tag);
|
|
188
|
+
if (!tagCheck.ok) {
|
|
189
|
+
return failJson(useJson, { code: ERROR_CODES.BAD_ARGS, message: tagCheck.error });
|
|
190
|
+
}
|
|
191
|
+
}
|
|
215
192
|
let runId;
|
|
216
193
|
if (args['run-id']) {
|
|
217
194
|
const check = validateTaskId(String(args['run-id']));
|
|
@@ -227,6 +204,12 @@ async function handleCouncilRun(args, depsOverride = {}) {
|
|
|
227
204
|
const runDir = args['out-dir']
|
|
228
205
|
? path.resolve(project, String(args['out-dir']))
|
|
229
206
|
: path.resolve(project, `council-${runId}`);
|
|
207
|
+
// v4.7 PR6: MCP has fenced this since v4.5 (mcp-council-run.js:137-141); the CLI
|
|
208
|
+
// never did, so `--out-dir ../../x` wrote outside the project and exited 0.
|
|
209
|
+
const { isPathInside } = require('./project-root-allowlist');
|
|
210
|
+
if (!isPathInside(runDir, project)) {
|
|
211
|
+
return failJson(useJson, { code: ERROR_CODES.BAD_ARGS, message: `Error: --out-dir must stay inside the project: '${args['out-dir']}' resolves outside ${project}` });
|
|
212
|
+
}
|
|
230
213
|
|
|
231
214
|
const { resolveGatewayMode, loadConfig } = require('./utils/config');
|
|
232
215
|
const { resolveFallbackConfig } = require('./sidecar/fallback-chains');
|
|
@@ -254,6 +237,7 @@ async function handleCouncilRun(args, depsOverride = {}) {
|
|
|
254
237
|
councilName,
|
|
255
238
|
template: templateMeta, // F9 (v4.5): null when no --template; additive on the run.json seed (run-state.js).
|
|
256
239
|
pack: packRecord, // v4.5 Task 12 (B7/F5): null when no --pack; additive on the run.json seed (run-state.js).
|
|
240
|
+
tag: args.tag, // v4.7 F8: undefined when no --tag; Task 3 stores it on the run.json seed.
|
|
257
241
|
droppedMembers: benchRes.droppedMembers, // v4.5 Wave 2: [] when nothing dropped; additive on the run.json seed (run-state.js).
|
|
258
242
|
// v4.1 §4.5b/§4.5d. `--claude-review` is resolved here but VALIDATED by the
|
|
259
243
|
// engine's preflightClaudeReview (run-assemble.js): the reserved-seat and
|
|
@@ -60,10 +60,14 @@ function renderRecord(r) {
|
|
|
60
60
|
}
|
|
61
61
|
function renderStats(agg) {
|
|
62
62
|
if (!agg.length) { return 'No council runs recorded yet.\n'; }
|
|
63
|
-
|
|
64
|
-
|
|
63
|
+
// v4.7 GOA-7 D10: group keys may be executable ids (>16 chars) — size the
|
|
64
|
+
// model column to the longest key; legacy (alias-keyed) groups get a notes
|
|
65
|
+
// marker beside low-N.
|
|
66
|
+
const w = Math.max(16, ...agg.map(a => String(a.model).length));
|
|
67
|
+
return 'model'.padEnd(w) + ' runs avg-cred confirm fact-err notes\n' +
|
|
68
|
+
agg.map(a => `${String(a.model).padEnd(w)} ${String(a.runs).padStart(4)} ` +
|
|
65
69
|
`${fmt(a.avgStreetCredPeersOnly)} ${fmt(a.lifetimeConfirmRate)} ${fmt(a.lifetimeFactErrorRate)}` +
|
|
66
|
-
`${a.lowN ? ' low-N' : ''}`).join('\n') + '\n';
|
|
70
|
+
`${a.lowN ? ' low-N' : ''}${a.legacy ? ' legacy' : ''}`).join('\n') + '\n';
|
|
67
71
|
}
|
|
68
72
|
function fmt(v) { return (v === null || v === undefined) ? ' — ' : v.toFixed(2); }
|
|
69
73
|
|
|
@@ -162,6 +166,23 @@ function runVerdict(args, useJson) {
|
|
|
162
166
|
hint: 'pass a valid decisions.json array or omit --decisions' });
|
|
163
167
|
}
|
|
164
168
|
}
|
|
169
|
+
// R1 (v4.6.3): parseArgs records a valueless trailing -o/--out as boolean
|
|
170
|
+
// true (and --out= as ''). The boolean crashes writeVerdictAtomic mid-write
|
|
171
|
+
// (renameSync TypeError on a non-string path) leaving an orphaned
|
|
172
|
+
// true.tmp-<pid>; the empty string silently falls through to the default
|
|
173
|
+
// path. Name the flag and refuse both — the unknown-flag precedent.
|
|
174
|
+
// R5 (v4.7): a dash-leading value ('-x') is a well-formed string as far as
|
|
175
|
+
// parseArgs is concerned (it normalizes, it does not validate) — refuse it
|
|
176
|
+
// here too, or it resolves straight through to writeVerdictAtomic('-x', ...)
|
|
177
|
+
// and writes a file literally named '-x' in cwd. Same failure class as R1,
|
|
178
|
+
// one form short.
|
|
179
|
+
if (args.out !== undefined && (typeof args.out !== 'string' || args.out === '' || args.out.startsWith('-'))) {
|
|
180
|
+
return failJson(useJson, { code: ERROR_CODES.BAD_ARGS,
|
|
181
|
+
message: (typeof args.out !== 'string' || args.out === '')
|
|
182
|
+
? '-o/--out requires a value'
|
|
183
|
+
: `-o/--out cannot start with '-': got '${args.out}'`,
|
|
184
|
+
hint: 'amicus council verdict <tally.json> [--decisions <decisions.json>] [-o|--out <out.json>]' });
|
|
185
|
+
}
|
|
165
186
|
const outPath = args.out || './verdict.json';
|
|
166
187
|
let verdict;
|
|
167
188
|
try {
|