amicus 4.6.3 → 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 +283 -0
- package/README.md +19 -7
- package/bin/amicus.js +31 -0
- package/docs/ROADMAP.md +143 -36
- package/docs/configuration.md +51 -3
- 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 +59 -13
- package/electron/workspace-ui/index.html +3 -0
- package/electron/workspace-ui/live-model.js +52 -14
- package/electron/workspace-ui/workspace-app.js +14 -3
- 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 +6 -2
- package/electron/workspace-ui/workspace-seats.js +68 -0
- 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 +17 -5
- 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/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 +61 -5
- 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/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/cli-preflight.js +27 -1
- package/src/utils/config.js +15 -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 +24 -4
- package/src/utils/spend-ledger.js +11 -4
- package/src/utils/validators.js +16 -0
|
@@ -1,14 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Council Workspace —
|
|
3
|
-
* (v4.4 §5, ⚠️ DE-ROT F05 split of workspace-app.js).
|
|
2
|
+
* Council Workspace — name resolution + the matrix/verdict/seats panel adapters
|
|
3
|
+
* (v4.4 §5, ⚠️ DE-ROT F05 split of workspace-app.js). v4.7 PR7 (Task 1) moved the lazy
|
|
4
|
+
* prose-panel loading machinery (loaders/loading/lastWiredRunId state, loadPanel,
|
|
5
|
+
* proseLoader, wireLazyPanels) out to workspace-lazy.js — this file was pressed up against
|
|
6
|
+
* the 300-line size gate with the T19 stale-paint fixes still to land, the same treatment
|
|
7
|
+
* workspace-seats.js got in v4.6.2 PR4 (D8).
|
|
8
|
+
*
|
|
9
|
+
* Split line: this file owns NAME RESOLUTION (sanitizeName / resolveArtifactName — the RN-1
|
|
10
|
+
* disambiguation pair, pinned by tests/electron/workspace-ui-static.test.js) and the panel
|
|
11
|
+
* adapters; workspace-lazy.js owns WHEN and WHETHER an artifact read is issued and which
|
|
12
|
+
* reply is allowed to paint.
|
|
4
13
|
*
|
|
5
14
|
* Loads BEFORE workspace-app.js (md-lite → live-model → workspace-render →
|
|
6
|
-
* workspace-matrix → workspace-seats → workspace-
|
|
7
|
-
* workspace-app), so every function here reads `window.AmicusApp` /
|
|
8
|
-
* `window.AmicusVerbs` at CALL time (never captured at this file's
|
|
9
|
-
* time —
|
|
10
|
-
* publishes its namespace at the top of its own boot, before calling into this
|
|
11
|
-
*
|
|
15
|
+
* workspace-matrix → workspace-seats → workspace-lazy → workspace-panels →
|
|
16
|
+
* workspace-verbs → workspace-app), so every function here reads `window.AmicusApp` /
|
|
17
|
+
* `window.AmicusVerbs` / `window.AmicusLazy` at CALL time (never captured at this file's
|
|
18
|
+
* own load time — none of those namespaces exist yet when this IIFE runs). window.AmicusApp
|
|
19
|
+
* publishes its namespace at the top of its own boot, before calling into this file, so by
|
|
20
|
+
* the time any function below actually executes, all three are present.
|
|
12
21
|
*/
|
|
13
22
|
(function () {
|
|
14
23
|
'use strict';
|
|
@@ -67,167 +76,10 @@
|
|
|
67
76
|
}
|
|
68
77
|
}
|
|
69
78
|
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
// dispatch through this module-level `loaders` map, which renderDetail (via wireLazyPanels)
|
|
75
|
-
// overwrites per run.
|
|
76
|
-
//
|
|
77
|
-
// ⚠️ PRE-FLIGHT (P4): the load is AWAITABLE — drillIntoJudge needs to know when it has
|
|
78
|
-
// settled (the old code guessed with setTimeout(render, 300), which could fire before an
|
|
79
|
-
// unbounded N-artifact IPC round trip finished and silently render nothing). loadPanel()
|
|
80
|
-
// is idempotent per panel id and returns its in-flight promise; the promise cache
|
|
81
|
-
// (`loading`) and the per-run spec (`loaders`) are both keyed by panel id, but only
|
|
82
|
-
// `loading` is cleared by wireLazyPanels() — on a run CHANGE only (Task 19, RN-5) — and
|
|
83
|
-
// that clearing is what stops F09's stale-run artifact requests.
|
|
84
|
-
var loaders = {}; // panelId -> {bodyId, files} (rewritten per run by wireLazyPanels)
|
|
85
|
-
var loading = {}; // panelId -> Promise (cleared per run by wireLazyPanels)
|
|
86
|
-
// Task 19 (RN-5): the run wireLazyPanels() last reset panels/loading for — gates the reset
|
|
87
|
-
// below to run CHANGES only. A same-run call (renderDetail() runs this on every blind toggle
|
|
88
|
-
// too, and on the live loop's terminal refresh) instead refreshes any open panel (Fix 1).
|
|
89
|
-
var lastWiredRunId = null;
|
|
90
|
-
|
|
91
|
-
function loadPanel(panelId, bodyId, files) {
|
|
92
|
-
var A = window.AmicusApp;
|
|
93
|
-
if (loading[panelId]) { return loading[panelId]; }
|
|
94
|
-
// ⚠️ R4 COUNCIL REVIEW (fourth live paid council, major, unanimous): this is the third
|
|
95
|
-
// instance of the F09 class of bug (a stale async response overwriting shared DOM after
|
|
96
|
-
// the user has navigated away) — already fixed once for the toggle-listener stack (F09
|
|
97
|
-
// itself) and once for the fire-and-forget debate.json fetch in workspace-app.js (guards
|
|
98
|
-
// with `if (state.runId !== runId) return;`). wireLazyPanels() clearing `loading[panelId]`
|
|
99
|
-
// on every run switch permits a NEW request to be issued, but never fenced the PRIOR
|
|
100
|
-
// request's eventual resolution — open reviews-panel on run A, switch to run B (which
|
|
101
|
-
// issues its own request), and A's response — however late — used to overwrite whatever
|
|
102
|
-
// B had just rendered. Capture the runId this request was issued for, and guard as the
|
|
103
|
-
// FIRST statement of the completion handler, exactly like the debate.json fix.
|
|
104
|
-
var runId = A.state.runId;
|
|
105
|
-
loading[panelId] = Promise.all(files().map(function (f) {
|
|
106
|
-
return A.invoke('workspace:read-artifact', runId, f.name).then(function (res) {
|
|
107
|
-
return { name: f.name, title: f.title, text: res.text || '', truncated: res.truncated, error: res.error };
|
|
108
|
-
});
|
|
109
|
-
})).then(function (sections) {
|
|
110
|
-
if (A.state.runId !== runId) { return; } // stale: superseded by a later run switch
|
|
111
|
-
window.AmicusRender.renderProseSections(A.$(bodyId), sections.map(function (s) {
|
|
112
|
-
return s.error ? { name: s.name, title: s.title, error: s.name + ' — ' + s.error } : s;
|
|
113
|
-
}));
|
|
114
|
-
A.$(panelId).dataset.loaded = '1'; // display/debug marker only — `loading` is the real gate
|
|
115
|
-
});
|
|
116
|
-
return loading[panelId];
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/** Registered ONCE at boot (per panel id); reads the current run's spec off `loaders`. */
|
|
120
|
-
function proseLoader(panelId) {
|
|
121
|
-
var A = window.AmicusApp;
|
|
122
|
-
var panel = A.$(panelId);
|
|
123
|
-
panel.addEventListener('toggle', function () {
|
|
124
|
-
if (!panel.open) { return; }
|
|
125
|
-
var spec = loaders[panelId];
|
|
126
|
-
if (spec) { loadPanel(panelId, spec.bodyId, spec.files); }
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* Rewrites the per-run spec map on every call. On a run CHANGE (tracked via the module-level
|
|
132
|
-
* `lastWiredRunId`, above), resets panel open/loaded state and drops the previous run's
|
|
133
|
-
* cached load promises — exactly what F09's stale-run protection needs. On a SAME-run call
|
|
134
|
-
* (Task 19, RN-5: renderDetail() calls this on every blind toggle too, and the live loop's
|
|
135
|
-
* terminal refresh) any panel the user already has open is instead refreshed in place — see
|
|
136
|
-
* Fix 1 below — never left showing stale-blind content, never collapsed. Registers no
|
|
137
|
-
* listeners itself.
|
|
138
|
-
*/
|
|
139
|
-
function wireLazyPanels() {
|
|
140
|
-
var A = window.AmicusApp;
|
|
141
|
-
// ⚠️ Fix-wave (Fix 4): keyed off `A.state.detail.runId`, not `A.state.runId` — the latter is
|
|
142
|
-
// set synchronously at the top of openRun(), before its workspace:get-run reply lands, so an
|
|
143
|
-
// out-of-order reply could make the two diverge. workspace-app.js's own run-change gate
|
|
144
|
-
// (renderDetail(), above `d.runId`) reads off the SAME `state.detail.runId`, so the two
|
|
145
|
-
// provably agree on whether this is a run change.
|
|
146
|
-
var sameRun = A.state.detail.runId === lastWiredRunId;
|
|
147
|
-
if (!sameRun) {
|
|
148
|
-
['reviews-panel', 'bundle-panel', 'judges-panel'].forEach(function (id) {
|
|
149
|
-
var p = A.$(id);
|
|
150
|
-
p.dataset.loaded = '0';
|
|
151
|
-
p.open = false;
|
|
152
|
-
delete loading[id];
|
|
153
|
-
});
|
|
154
|
-
lastWiredRunId = A.state.detail.runId;
|
|
155
|
-
}
|
|
156
|
-
var bench = A.state.detail.run.bench || [];
|
|
157
|
-
var debated = !!A.state.detail.run.debate;
|
|
158
|
-
// ⚠️ CODE REVIEW (round 2, finding 2): readRunArtifact's error for a genuinely-missing
|
|
159
|
-
// artifact is NOT translated into a friendly "not written yet" note anywhere in this
|
|
160
|
-
// read path — it lands in the panel verbatim, absolute host path and all. `run.debate` is
|
|
161
|
-
// seeded on run.json's FIRST write, so it's truthy on every --debate run, including ones
|
|
162
|
-
// where the re-vote wave never actually ran (no contested findings, cost ceiling, abort) —
|
|
163
|
-
// requesting revote-<model>.md speculatively in that (near-certain) case means one ugly
|
|
164
|
-
// error row per bench model for a condition that isn't an error at all. run-detail.js
|
|
165
|
-
// already computes a presence manifest (state.detail.artifacts) for exactly these
|
|
166
|
-
// allowlisted names via fs.statSync — filter on it instead of requesting known-absent
|
|
167
|
-
// files. Applies to review-/judge- too (the same latent gap, just plan-mandated rather
|
|
168
|
-
// than new).
|
|
169
|
-
var artifacts = A.state.detail.artifacts || {};
|
|
170
|
-
function present(name) { return !!(artifacts[name] && artifacts[name].present); }
|
|
171
|
-
// ⚠️ v4.4.1 RN-9: these two titles used to hand-roll `A.state.blind && label ? label : m`
|
|
172
|
-
// inline. Both now go through AmicusRender.display() — the single blind-flip definition the
|
|
173
|
-
// re-vote title below already used — so the next blind-mode ruling lands in one place instead
|
|
174
|
-
// of being re-applied by hand in every file that happens to render an identity.
|
|
175
|
-
loaders['reviews-panel'] = { bodyId: 'reviews-body', files: function () {
|
|
176
|
-
return bench.map(function (m) {
|
|
177
|
-
var label = A.state.labelByModel[m];
|
|
178
|
-
return { name: resolveArtifactName(m, 'review'), title: window.AmicusRender.display({ model: m, label: label }, A.state.blind) };
|
|
179
|
-
}).filter(function (f) { return present(f.name); });
|
|
180
|
-
} };
|
|
181
|
-
loaders['bundle-panel'] = { bodyId: 'bundle-body', files: function () {
|
|
182
|
-
// ⚠️ v4.4.1 RN-4: the presence filter is NOT optional here either. Without it, a run whose
|
|
183
|
-
// Stage 2 never ran (a one-seat bench, an abort before the cross-review, a cost ceiling)
|
|
184
|
-
// requested a file the manifest already knows is absent and rendered readRunArtifact's raw
|
|
185
|
-
// error string in the panel — "absolute host path and all", per this file's own round-2
|
|
186
|
-
// note above `present()`. reviews-panel and judges-panel have always filtered; this was the
|
|
187
|
-
// odd one out.
|
|
188
|
-
return [{ name: 'bundle-stage2.md', title: 'bundle-stage2.md (verbatim)' }]
|
|
189
|
-
.filter(function (f) { return present(f.name); });
|
|
190
|
-
} };
|
|
191
|
-
loaders['judges-panel'] = { bodyId: 'judges-body', files: function () {
|
|
192
|
-
var files = bench.map(function (m) {
|
|
193
|
-
var label = A.state.labelByModel[m];
|
|
194
|
-
return { name: resolveArtifactName(m, 'judge'), title: 'Judge ' + window.AmicusRender.display({ model: m, label: label }, A.state.blind) };
|
|
195
|
-
});
|
|
196
|
-
if (debated) {
|
|
197
|
-
// ⚠️ DE-ROT (F38): on a --debate run, a matrix dispute cell can be a RE-VOTE whose
|
|
198
|
-
// prose lives in revote-<model>.md (not judge-<model>.md). Included per bench model
|
|
199
|
-
// like judge-*.md above, but — per the presence filter — only when the manifest
|
|
200
|
-
// confirms the file actually exists (see the code-review note above `present()`).
|
|
201
|
-
// ⚠️ CODE REVIEW (round 2, finding 3): this title is new code (unlike the review-/
|
|
202
|
-
// judge- titles above, which mirror the brief verbatim), so it goes through
|
|
203
|
-
// AmicusRender.display() — the single blind-flip definition — rather than adding a
|
|
204
|
-
// fourth hand-rolled copy of the same ternary.
|
|
205
|
-
// ⚠️ Task 18 fix-wave (RN-1, review finding 1): this name used to be recomputed via a
|
|
206
|
-
// bare sanitizeName(m) call, ignoring the disambiguation map entirely — for a colliding
|
|
207
|
-
// pair BOTH models resolved to the same bare revote-<sanitized>.md name, reintroducing
|
|
208
|
-
// for re-votes the exact cross-match bug Task 18 fixed for review-/judge-. Routed
|
|
209
|
-
// through resolveArtifactName(m, 'revote') like the other three sites; its built-in
|
|
210
|
-
// legacy fallback keeps older detail payloads (no artifactsByModel map) correct too.
|
|
211
|
-
files = files.concat(bench.map(function (m) {
|
|
212
|
-
var label = A.state.labelByModel[m];
|
|
213
|
-
return { name: resolveArtifactName(m, 'revote'), title: 'Re-vote ' + window.AmicusRender.display({ model: m, label: label }, A.state.blind) };
|
|
214
|
-
}));
|
|
215
|
-
}
|
|
216
|
-
return files.filter(function (f) { return present(f.name); });
|
|
217
|
-
} };
|
|
218
|
-
// ⚠️ Fix-wave (Fix 1, RN-9): a same-run call (the blind toggle, or the live loop's
|
|
219
|
-
// running -> terminal refresh) must re-render any panel the user already has open, or it
|
|
220
|
-
// keeps showing content painted under the PREVIOUS blind state. renderProseSections()
|
|
221
|
-
// (workspace-render.js) clears its container before repainting, so this replaces sections
|
|
222
|
-
// in place rather than appending duplicates. Drop the cached promise first so loadPanel()
|
|
223
|
-
// actually re-fetches instead of returning its already-settled one.
|
|
224
|
-
if (sameRun) {
|
|
225
|
-
['reviews-panel', 'bundle-panel', 'judges-panel'].forEach(function (id) {
|
|
226
|
-
var p = A.$(id);
|
|
227
|
-
if (p.open) { delete loading[id]; loadPanel(id, loaders[id].bodyId, loaders[id].files); }
|
|
228
|
-
});
|
|
229
|
-
}
|
|
230
|
-
}
|
|
79
|
+
// ⚠️ v4.7 PR7 extraction: bodies moved verbatim to workspace-lazy.js
|
|
80
|
+
// (window.AmicusLazy), which loads immediately before this file.
|
|
81
|
+
function proseLoader(panelId) { window.AmicusLazy.proseLoader(panelId); }
|
|
82
|
+
function wireLazyPanels() { window.AmicusLazy.wireLazyPanels(); }
|
|
231
83
|
|
|
232
84
|
// ⚠️ DE-ROT (F38): on a --debate run the FINAL tally.json is rebuilt from the debate's
|
|
233
85
|
// replaced adjudications, so a matrix `dispute` cell can be a re-vote — gate per
|
|
@@ -240,9 +92,9 @@
|
|
|
240
92
|
var A = window.AmicusApp;
|
|
241
93
|
var panel = A.$('judges-panel');
|
|
242
94
|
panel.open = true;
|
|
243
|
-
var spec =
|
|
95
|
+
var spec = window.AmicusLazy.panelSpec('judges-panel');
|
|
244
96
|
if (!spec) { return Promise.resolve(); }
|
|
245
|
-
return loadPanel('judges-panel', spec.bodyId, spec.files).then(function () {
|
|
97
|
+
return window.AmicusLazy.loadPanel('judges-panel', spec.bodyId, spec.files).then(function () {
|
|
246
98
|
var rv = ((A.state.debate && A.state.debate.revotes) || []).find(function (r) {
|
|
247
99
|
return r.judge === judgePair.model && r.id === findingId;
|
|
248
100
|
});
|
|
@@ -290,5 +142,6 @@
|
|
|
290
142
|
proseLoader: proseLoader,
|
|
291
143
|
drillIntoJudge: drillIntoJudge,
|
|
292
144
|
sanitizeName: sanitizeName,
|
|
145
|
+
resolveArtifactName: resolveArtifactName,
|
|
293
146
|
};
|
|
294
147
|
})();
|
|
@@ -183,11 +183,11 @@
|
|
|
183
183
|
// did) while the stored `dataset.key` stays raw means a key containing `"` or `\` can
|
|
184
184
|
// never match its own row — the lookup misses every tick and the row is re-appended
|
|
185
185
|
// forever. A plain object lookup sidesteps the escaping problem entirely.
|
|
186
|
-
var existing =
|
|
186
|
+
var existing = Object.create(null);
|
|
187
187
|
Array.prototype.slice.call(tbody.children).forEach(function (row) {
|
|
188
188
|
existing[row.dataset.key] = row;
|
|
189
189
|
});
|
|
190
|
-
var seen =
|
|
190
|
+
var seen = Object.create(null);
|
|
191
191
|
seats.forEach(function (seat) {
|
|
192
192
|
// ⚠️ DE-ROT (F37): `seat.id` is now always set by seatsFromRunStats (`model:role`), so
|
|
193
193
|
// debate rebuttal/revote rows no longer collide with the seat row. The `|| seat.model`
|
|
@@ -217,6 +217,10 @@
|
|
|
217
217
|
// RN-11 (v4.5): the keyed update added and removed rows but never MOVED
|
|
218
218
|
// them, so table order was frozen at first render — wrong the moment a
|
|
219
219
|
// repair solo or new wave changes the composed doc's leg order mid-run.
|
|
220
|
+
// The O(n²) `find()` per seat is deliberate: bench is capped at 26 (anonymize.js:21),
|
|
221
|
+
// so this is ~100 rows worst case on a debate run, every 1.5s. `existing` above is
|
|
222
|
+
// already the map an O(n) rewrite would need — reach for it only if this table ever
|
|
223
|
+
// renders unbounded rows.
|
|
220
224
|
seats.forEach(function (seat, i) {
|
|
221
225
|
var key = String(seat.id || seat.model);
|
|
222
226
|
var current = tbody.children[i];
|
|
@@ -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;
|
|
@@ -55,6 +91,38 @@
|
|
|
55
91
|
// records for the SAME run, and the persisted run.json copy is authoritative when present.
|
|
56
92
|
var deg = (d.run && d.run.degrades && d.run.degrades.length) ? d.run.degrades
|
|
57
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
|
+
});
|
|
58
126
|
var dead = window.AmicusLive.deadSeats(deg, seatLoss, seats, runMeta);
|
|
59
127
|
renderDeadSeatRows(tbody, dead, A.state.blind, A.labelOf);
|
|
60
128
|
}
|
|
@@ -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 };
|