amicus 4.7.1 → 4.8.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 +988 -1
- package/README.md +5 -4
- package/docs/CITATIONS.md +122 -0
- package/docs/ROADMAP.md +33 -5
- package/docs/SHIMS.md +1 -1
- package/docs/configuration.md +3 -3
- package/docs/council.md +179 -32
- package/docs/doc-system.md +1 -1
- package/docs/publishing.md +2 -0
- package/docs/troubleshooting.md +3 -3
- package/docs/usage.md +2 -2
- package/electron/ipc-setup.js +18 -2
- package/electron/main.js +46 -3
- package/electron/setup-ui-aliases.js +2 -2
- package/electron/setup-ui-model.js +99 -9
- package/electron/setup-ui-styles.js +22 -0
- package/electron/setup-ui.js +231 -29
- package/electron/workspace-ui/index.html +9 -0
- package/electron/workspace-ui/live-dead-seats.js +228 -0
- package/electron/workspace-ui/live-model.js +10 -236
- package/electron/workspace-ui/live-seats.js +126 -0
- package/electron/workspace-ui/workspace-app.js +6 -41
- package/electron/workspace-ui/workspace-banners.js +95 -0
- package/electron/workspace-ui/workspace-lazy.js +55 -12
- package/electron/workspace-ui/workspace-matrix.js +2 -2
- package/electron/workspace-ui/workspace-panels.js +42 -10
- package/electron/workspace-ui/workspace-render.js +2 -2
- package/electron/workspace-ui/workspace-seats.js +101 -17
- package/package.json +4 -1
- package/schemas/council-run-live.schema.json +1 -0
- package/schemas/council-run.schema.json +19 -0
- package/schemas/council-tally.schema.json +34 -2
- package/schemas/council-verdict.schema.json +15 -0
- package/skills/second-opinion/COUNCIL-DESIGN.md +9 -4
- package/skills/second-opinion/MANUAL-ORCHESTRATION.md +1 -1
- package/skills/second-opinion/SKILL.md +25 -11
- package/src/cli-handlers-doctor.js +18 -14
- package/src/council/anonymize.js +80 -11
- package/src/council/briefings-chair.js +272 -0
- package/src/council/briefings-stage2.js +12 -140
- package/src/council/debate.js +120 -15
- package/src/council/ledger-join.js +284 -0
- package/src/council/ledger-stats.js +100 -0
- package/src/council/ledger.js +177 -88
- package/src/council/parse-stage2.js +1 -1
- package/src/council/peer-split.js +196 -0
- package/src/council/report-html.js +12 -5
- package/src/council/report-md.js +146 -0
- package/src/council/report.js +188 -112
- package/src/council/run-assemble.js +100 -79
- package/src/council/run-chair.js +17 -1
- package/src/council/run-debate-revote.js +268 -0
- package/src/council/run-debate.js +92 -102
- package/src/council/run-finish.js +70 -0
- package/src/council/run-launch.js +45 -18
- package/src/council/run-retry-group.js +266 -0
- package/src/council/run-retry-keys.js +74 -0
- package/src/council/run-retry-launch.js +55 -0
- package/src/council/run-retry-notes.js +65 -13
- package/src/council/run-retry.js +166 -161
- package/src/council/run-stage1-launch.js +27 -10
- package/src/council/run-stage1-rows.js +220 -0
- package/src/council/run-stage1-superseded.js +156 -0
- package/src/council/run-stage2.js +65 -5
- package/src/council/run-stages.js +72 -69
- package/src/council/run-state.js +1 -1
- package/src/council/run-stats-entry.js +71 -0
- package/src/council/run-verdict-files.js +52 -0
- package/src/council/run.js +42 -52
- package/src/council/seats.js +262 -0
- package/src/council/stage1-bind.js +142 -0
- package/src/council/street-cred.js +258 -0
- package/src/council/tally.js +100 -49
- package/src/council/verdict.js +49 -1
- package/src/headless.js +20 -6
- package/src/mcp-tools.js +71 -1
- package/src/observe/council-legs.js +7 -1
- package/src/observe/live-doc.js +3 -3
- package/src/sidecar/fanout-leg.js +22 -1
- package/src/sidecar/fanout-wave-io.js +26 -1
- package/src/sidecar/fanout.js +4 -10
- package/src/sidecar/leg-ids.js +19 -0
- package/src/sidecar/models-probe.js +7 -4
- package/src/sidecar/reopen-spend.js +1 -1
- package/src/sidecar/setup.js +137 -2
- package/src/utils/alias-audit.js +81 -3
- package/src/utils/config.js +109 -11
- package/src/utils/curated-models.js +17 -2
- package/src/utils/degrade.js +5 -0
- package/src/utils/doctor-alias-check.js +152 -0
- package/src/utils/model-canonicalization.js +64 -0
- package/src/utils/model-shortlist.js +100 -0
- package/src/utils/no-output-backstop.js +1 -1
- package/src/utils/provider-default-picker.js +93 -45
- package/src/utils/provider-default-prompt.js +1 -1
- package/src/utils/quick-picks.js +2 -2
- package/src/utils/remediation-hints.js +37 -0
- package/src/utils/session-index-prune.js +297 -0
- package/src/utils/session-metadata-tmp-sweep.js +1 -1
- package/src/workspace/artifact-guard.js +8 -114
- package/src/workspace/artifact-names.js +222 -0
- package/src/workspace/fold-format.js +9 -6
- package/src/workspace/live-normalize.js +6 -2
- package/src/workspace/matrix-model.js +141 -19
- package/src/workspace/run-detail.js +30 -4
- package/src/workspace/seat-space.js +143 -0
|
@@ -167,47 +167,12 @@
|
|
|
167
167
|
V.startLiveLoop();
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
return;
|
|
177
|
-
}
|
|
178
|
-
// ⚠️ R4 COUNCIL REVIEW (fourth live paid council, major, unanimous): two distinct bench
|
|
179
|
-
// entries that sanitize to the same artifact name (src/workspace/artifact-guard.js's
|
|
180
|
-
// artifactAllowlist) mean this run directory cannot hold both models' review/judge files
|
|
181
|
-
// under distinct names — drillIntoJudge's artifact lookup would otherwise silently
|
|
182
|
-
// misattribute one model's prose to the other. This is a run-integrity defect the user
|
|
183
|
-
// must see, not a display quirk to smooth over — surfaced ahead of the run.error/reason
|
|
184
|
-
// banner below since it calls into question every judges-panel section's attribution.
|
|
185
|
-
if (d.derived.artifactCollisions && d.derived.artifactCollisions.length) {
|
|
186
|
-
var c = d.derived.artifactCollisions[0];
|
|
187
|
-
R.renderBanner($('banner'),
|
|
188
|
-
'Run integrity error: bench entries ' + c.models.join(' and ') + ' both sanitize to "' + c.sanitized +
|
|
189
|
-
'" — this run directory cannot distinguish their artifacts, so prose below may be misattributed.', '');
|
|
190
|
-
return;
|
|
191
|
-
}
|
|
192
|
-
// ⚠️ PRE-FLIGHT (P3), caught live by the CDP e2e (Task 18): gating on `d.run.error` ALONE
|
|
193
|
-
// never shows this banner for a `status:'partial'` run — `finalize()` only ever sets
|
|
194
|
-
// `run.error` on the exit-1 path (src/council/run.js:98-100); the exit-2 "degraded" path
|
|
195
|
-
// leaves it null (run-detail.js:83-91). `verdictPanel.reason` (degradedReason()) already
|
|
196
|
-
// covers BOTH cases — exit-1's {code, message} and the partial-run stage-failure/skip
|
|
197
|
-
// sentence — so check for either signal, not run.error alone.
|
|
198
|
-
if (d.run.error || d.derived.verdictPanel.reason) {
|
|
199
|
-
// ⚠️ Code review round 2, finding 2: `d.run.error` is a structured {code, message} object
|
|
200
|
-
// (never a string) — passing it straight to renderBanner set `textContent = <object>`,
|
|
201
|
-
// which coerces to the literal string "[object Object]". `d.derived.verdictPanel.reason`
|
|
202
|
-
// is already the correctly-formatted string (run-detail.js's degradedReason(): "CODE:
|
|
203
|
-
// message" on the exit-1 path, or a stage-failure/skip sentence otherwise) — reuse it
|
|
204
|
-
// rather than re-deriving the same formatting a second time here.
|
|
205
|
-
R.renderBanner($('banner'), d.derived.verdictPanel.reason || 'Run reported an error',
|
|
206
|
-
d.run.status === 'error' ? '' : 'warn');
|
|
207
|
-
return;
|
|
208
|
-
}
|
|
209
|
-
R.renderBanner($('banner'), null);
|
|
210
|
-
}
|
|
170
|
+
// ⚠️ v4.8 PR5a fix-wave 3: the banner ladder moved to workspace-banners.js — this file
|
|
171
|
+
// hit 310/300 when the orphan-collision and rejected-seat-table banners landed, the same
|
|
172
|
+
// gate that produced workspace-lazy.js out of workspace-panels.js (v4.7 PR7). Kept as a
|
|
173
|
+
// named delegate, not deleted: `window.AmicusApp.renderBanners` is a shipped entry point
|
|
174
|
+
// (workspace-verbs.js:216 calls it after an abort) and boundary tests assert it exists.
|
|
175
|
+
function renderBanners() { window.AmicusBanners.render(); }
|
|
211
176
|
|
|
212
177
|
// ---- blind toggle + keyboard ------------------------------------------
|
|
213
178
|
$('blind-toggle').addEventListener('change', function (e) {
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Council Workspace — the run-detail banner ladder (v4.4 §9, spec's "graceful when present").
|
|
3
|
+
*
|
|
4
|
+
* v4.8 PR5a fix-wave 3 extraction of renderBanners out of workspace-app.js, which hit
|
|
5
|
+
* 310/300 when the orphan-collision and rejected-seat-table banners landed. Exactly the
|
|
6
|
+
* treatment workspace-panels.js got in v4.7 PR7 (workspace-lazy.js) and v4.6.2 PR4
|
|
7
|
+
* (workspace-seats.js), on the same gate and the same kind of seam.
|
|
8
|
+
*
|
|
9
|
+
* Split line: this file decides WHICH single banner a run shows and what it says.
|
|
10
|
+
* workspace-app.js keeps `state`, the IPC calls and the render orchestration;
|
|
11
|
+
* workspace-render.js keeps renderBanner(), the DOM write itself.
|
|
12
|
+
*
|
|
13
|
+
* ⚠️ ONE banner, first match wins — every branch returns. That is not incidental: the
|
|
14
|
+
* element is a single node (workspace-render.js sets `textContent`), so a second call would
|
|
15
|
+
* overwrite the first. The ORDER is therefore the ranking, most-alarming first, and each
|
|
16
|
+
* branch below states why it sits where it does. Cross-calls resolve `window.Amicus*` at
|
|
17
|
+
* CALL time, never at this file's load time — the house discipline for every renderer script.
|
|
18
|
+
*/
|
|
19
|
+
(function () {
|
|
20
|
+
'use strict';
|
|
21
|
+
|
|
22
|
+
function render() {
|
|
23
|
+
var A = window.AmicusApp;
|
|
24
|
+
var d = A.state.detail;
|
|
25
|
+
var R = window.AmicusRender;
|
|
26
|
+
var el = A.$('banner');
|
|
27
|
+
if (!d.derived.schemaSupported) {
|
|
28
|
+
R.renderBanner(el, 'This run was written by a different amicus version (schemaVersion ' +
|
|
29
|
+
d.run.schemaVersion + ') — artifacts: ' + d.runDir, 'warn');
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
// ⚠️ R4 COUNCIL REVIEW (fourth live paid council, major, unanimous): two distinct bench
|
|
33
|
+
// entries that sanitize to the same artifact name (src/workspace/artifact-guard.js's
|
|
34
|
+
// artifactAllowlist) mean this run directory cannot hold both models' review/judge files
|
|
35
|
+
// under distinct names — drillIntoJudge's artifact lookup would otherwise silently
|
|
36
|
+
// misattribute one model's prose to the other. This is a run-integrity defect the user
|
|
37
|
+
// must see, not a display quirk to smooth over — surfaced ahead of the run.error/reason
|
|
38
|
+
// banner below since it calls into question every judges-panel section's attribution.
|
|
39
|
+
// ⚠️ Fix-wave 3 (council-3 A1): TWO DIFFERENT FACTS, two sentences. The orphan case is
|
|
40
|
+
// not a sanitize collision and must not borrow its wording — `a#1` and `a-1` do NOT
|
|
41
|
+
// sanitize to the same name; an orphaned leg wrote a file under its own alias whose stem
|
|
42
|
+
// happens to BE another seat's artifact name. PR5a added the `orphan` flag
|
|
43
|
+
// (artifact-names.js) and shipped no consumer, so every orphan collision rendered a
|
|
44
|
+
// sentence that was simply untrue about the run.
|
|
45
|
+
// ⚠️ And EVERY entry is rendered, not `[0]`. One stem can legitimately produce two
|
|
46
|
+
// entries — MEASURED on a legacy bench: ['vendor/a','vendor?a'] with a 'vendor?a' orphan
|
|
47
|
+
// emits a sanitize entry AND an orphan entry for stem 'vendor-a'. Different causes, same
|
|
48
|
+
// consequence, so both are stated rather than merged: a merge has to drop one cause.
|
|
49
|
+
if (d.derived.artifactCollisions && d.derived.artifactCollisions.length) {
|
|
50
|
+
var clauses = d.derived.artifactCollisions.map(function (c) {
|
|
51
|
+
// `models` on an orphan entry is [owning entity, orphan alias] — both CLAIMANTS of
|
|
52
|
+
// the stem (artifact-names.js states why the orphan half is not projected into
|
|
53
|
+
// entity space). On a sanitize entry it is the two raw bench values.
|
|
54
|
+
return c.orphan
|
|
55
|
+
? 'the artifact name "' + c.sanitized + '" is claimed by both ' + c.models.join(' and ') +
|
|
56
|
+
' — an orphaned leg wrote a file under its own alias and run.json cannot say which owns it'
|
|
57
|
+
: 'bench entries ' + c.models.join(' and ') + ' both sanitize to "' + c.sanitized + '"';
|
|
58
|
+
});
|
|
59
|
+
R.renderBanner(el, 'Run integrity error: ' + clauses.join('; ') +
|
|
60
|
+
'. This run directory cannot distinguish those artifacts, so prose below may be misattributed.', '');
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
// ⚠️ Fix-wave 3 (council-3 C2): seats[] was present but malformed, so the whole run fell
|
|
64
|
+
// back to alias space — two seats running one model are indistinguishable in every panel
|
|
65
|
+
// below. Fail-safe, but SILENT until now, which the product principle rejects as hard as
|
|
66
|
+
// a crash. Ranked AFTER the collision banner (that one says prose may be misattributed,
|
|
67
|
+
// strictly worse) and BEFORE run.error, since it qualifies how to read every panel.
|
|
68
|
+
if (d.derived.seatTableRejected) {
|
|
69
|
+
R.renderBanner(el, 'This run\'s seat table could not be read (a malformed entry), so ' +
|
|
70
|
+
'artifacts are shown by model alias instead of per seat — two seats running the same ' +
|
|
71
|
+
'model cannot be told apart below. Artifacts: ' + d.runDir, 'warn');
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
// ⚠️ PRE-FLIGHT (P3), caught live by the CDP e2e (Task 18): gating on `d.run.error` ALONE
|
|
75
|
+
// never shows this banner for a `status:'partial'` run — `finalize()` only ever sets
|
|
76
|
+
// `run.error` on the exit-1 path (src/council/run.js:98-100); the exit-2 "degraded" path
|
|
77
|
+
// leaves it null (run-detail.js:83-91). `verdictPanel.reason` (degradedReason()) already
|
|
78
|
+
// covers BOTH cases — exit-1's {code, message} and the partial-run stage-failure/skip
|
|
79
|
+
// sentence — so check for either signal, not run.error alone.
|
|
80
|
+
if (d.run.error || d.derived.verdictPanel.reason) {
|
|
81
|
+
// ⚠️ Code review round 2, finding 2: `d.run.error` is a structured {code, message} object
|
|
82
|
+
// (never a string) — passing it straight to renderBanner set `textContent = <object>`,
|
|
83
|
+
// which coerces to the literal string "[object Object]". `d.derived.verdictPanel.reason`
|
|
84
|
+
// is already the correctly-formatted string (run-detail.js's degradedReason(): "CODE:
|
|
85
|
+
// message" on the exit-1 path, or a stage-failure/skip sentence otherwise) — reuse it
|
|
86
|
+
// rather than re-deriving the same formatting a second time here.
|
|
87
|
+
R.renderBanner(el, d.derived.verdictPanel.reason || 'Run reported an error',
|
|
88
|
+
d.run.status === 'error' ? '' : 'warn');
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
R.renderBanner(el, null);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
window.AmicusBanners = { render: render };
|
|
95
|
+
})();
|
|
@@ -132,8 +132,34 @@
|
|
|
132
132
|
});
|
|
133
133
|
lastWiredRunId = A.state.detail.runId;
|
|
134
134
|
}
|
|
135
|
-
|
|
136
|
-
|
|
135
|
+
// ⚠️ Fix-wave (council A1): CAPTURED here, at wire time, exactly like `bench` and
|
|
136
|
+
// `debated` have always been — never re-read inside roster(). The `files` closures below
|
|
137
|
+
// are invoked LATER (on first panel open, and again on every same-run rewire), by which
|
|
138
|
+
// point `A.state.detail` may be a different payload entirely: an unreadable run, whose
|
|
139
|
+
// detail carries `error` and no `run` at all. Reading `A.state.detail.run.seats` at that
|
|
140
|
+
// moment threw — a crash path the bench-iteration code it replaced never had, because
|
|
141
|
+
// `bench` was already a local. Capturing also stops roster() from returning run B's
|
|
142
|
+
// seats while `debated` still describes run A.
|
|
143
|
+
var detail = A.state.detail;
|
|
144
|
+
var run = detail.run || {};
|
|
145
|
+
var bench = run.bench || [];
|
|
146
|
+
var debated = !!run.debate;
|
|
147
|
+
// ⚠️ Fix-wave (council B1): the seat-space decision is NOT re-derived here. It is
|
|
148
|
+
// isSeatTable(run.seats) — the same predicate artifactAllowlist gates on — computed in
|
|
149
|
+
// src/workspace/run-detail.js and shipped as its answer. `!seats || !seats.length`, the
|
|
150
|
+
// check this replaces, is strictly weaker: it accepts a seats[] with duplicate, empty or
|
|
151
|
+
// non-string ids that isSeatTable rejects, so a malformed table put derived.artifactsByModel
|
|
152
|
+
// in ALIAS space while this roster handed resolveArtifactName SEAT ids. Every lookup
|
|
153
|
+
// missed, resolveArtifactName returned null, present(null) dropped every row, and all
|
|
154
|
+
// three panels rendered empty with no banner and no error. Absent (a pre-fix detail
|
|
155
|
+
// payload) reads as alias space, which is the space such a payload's map is keyed in.
|
|
156
|
+
var seatSpace = !!(detail.derived && detail.derived.seatSpace);
|
|
157
|
+
// ⚠️ Fix-wave 2 (council-2 A2): captured for the same reason `run` is. renderDetail
|
|
158
|
+
// REBUILDS state.labelByModel per run (workspace-app.js:147-148), so a closure that
|
|
159
|
+
// read it live could pair THIS run's keys with the NEXT run's labels — and this map
|
|
160
|
+
// is what blind mode renders, so the failure is a wrong name shown confidently
|
|
161
|
+
// rather than a visibly empty panel.
|
|
162
|
+
var labels = A.state.labelByModel;
|
|
137
163
|
// ⚠️ CODE REVIEW (round 2, finding 2): readRunArtifact's error for a genuinely-missing
|
|
138
164
|
// artifact is NOT translated into a friendly "not written yet" note anywhere in this
|
|
139
165
|
// read path — it lands in the panel verbatim, absolute host path and all. `run.debate` is
|
|
@@ -146,15 +172,34 @@
|
|
|
146
172
|
// files. Applies to review-/judge- too (the same latent gap, just plan-mandated rather
|
|
147
173
|
// than new).
|
|
148
174
|
var artifacts = A.state.detail.artifacts || {};
|
|
149
|
-
function present(name) { return !!(artifacts[name] && artifacts[name].present); }
|
|
175
|
+
function present(name) { return !!(name && artifacts[name] && artifacts[name].present); }
|
|
176
|
+
// ⚠️ v4.8 PR5a T2: iterate SEATS, not the bench. A repeated alias appears twice in
|
|
177
|
+
// `bench`, so these loops used to build two identical rows that resolved to one
|
|
178
|
+
// alias-named file and were then both dropped by present() — the whole issue-137
|
|
179
|
+
// Workspace symptom. run.json carries `seats[]` unconditionally (run.js checkpoints it), and
|
|
180
|
+
// getRunDetail returns the parsed run wholesale, so it is already on state.
|
|
181
|
+
//
|
|
182
|
+
// ⚠️ The label is resolved from the seat's ALIAS while the seat's ID is carried as
|
|
183
|
+
// identity. `labelByModel` is alias-keyed (workspace-app.js, from run.json's labelMap,
|
|
184
|
+
// which PR5a does not touch — R5-2), so keying it with a seat id yields undefined and
|
|
185
|
+
// AmicusRender.display() falls through to `pair.model` — printing `gemini#1` with blind
|
|
186
|
+
// mode ON. A seat id contains its alias, so rendering one defeats blind mode. This is
|
|
187
|
+
// the same split matrix-model.js already makes for the adjudication columns.
|
|
188
|
+
function roster() {
|
|
189
|
+
if (!seatSpace) {
|
|
190
|
+
return bench.map(function (m) { return { key: m, label: labels[m] }; });
|
|
191
|
+
}
|
|
192
|
+
return run.seats.map(function (s) {
|
|
193
|
+
return { key: s.id, label: labels[s.alias] };
|
|
194
|
+
});
|
|
195
|
+
}
|
|
150
196
|
// ⚠️ v4.4.1 RN-9: these two titles used to hand-roll `A.state.blind && label ? label : m`
|
|
151
197
|
// inline. Both now go through AmicusRender.display() — the single blind-flip definition the
|
|
152
198
|
// re-vote title below already used — so the next blind-mode ruling lands in one place instead
|
|
153
199
|
// of being re-applied by hand in every file that happens to render an identity.
|
|
154
200
|
loaders['reviews-panel'] = { bodyId: 'reviews-body', files: function () {
|
|
155
|
-
return
|
|
156
|
-
|
|
157
|
-
return { name: window.AmicusPanels.resolveArtifactName(m, 'review'), title: window.AmicusRender.display({ model: m, label: label }, A.state.blind) };
|
|
201
|
+
return roster().map(function (r) {
|
|
202
|
+
return { name: window.AmicusPanels.resolveArtifactName(r.key, 'review'), title: window.AmicusRender.display({ model: r.key, label: r.label }, A.state.blind) };
|
|
158
203
|
}).filter(function (f) { return present(f.name); });
|
|
159
204
|
} };
|
|
160
205
|
loaders['bundle-panel'] = { bodyId: 'bundle-body', files: function () {
|
|
@@ -168,9 +213,8 @@
|
|
|
168
213
|
.filter(function (f) { return present(f.name); });
|
|
169
214
|
} };
|
|
170
215
|
loaders['judges-panel'] = { bodyId: 'judges-body', files: function () {
|
|
171
|
-
var files =
|
|
172
|
-
|
|
173
|
-
return { name: window.AmicusPanels.resolveArtifactName(m, 'judge'), title: 'Judge ' + window.AmicusRender.display({ model: m, label: label }, A.state.blind) };
|
|
216
|
+
var files = roster().map(function (r) {
|
|
217
|
+
return { name: window.AmicusPanels.resolveArtifactName(r.key, 'judge'), title: 'Judge ' + window.AmicusRender.display({ model: r.key, label: r.label }, A.state.blind) };
|
|
174
218
|
});
|
|
175
219
|
if (debated) {
|
|
176
220
|
// ⚠️ DE-ROT (F38): on a --debate run, a matrix dispute cell can be a RE-VOTE whose
|
|
@@ -187,9 +231,8 @@
|
|
|
187
231
|
// for re-votes the exact cross-match bug Task 18 fixed for review-/judge-. Routed
|
|
188
232
|
// through resolveArtifactName(m, 'revote') like the other three sites; its built-in
|
|
189
233
|
// legacy fallback keeps older detail payloads (no artifactsByModel map) correct too.
|
|
190
|
-
files = files.concat(
|
|
191
|
-
|
|
192
|
-
return { name: window.AmicusPanels.resolveArtifactName(m, 'revote'), title: 'Re-vote ' + window.AmicusRender.display({ model: m, label: label }, A.state.blind) };
|
|
234
|
+
files = files.concat(roster().map(function (r) {
|
|
235
|
+
return { name: window.AmicusPanels.resolveArtifactName(r.key, 'revote'), title: 'Re-vote ' + window.AmicusRender.display({ model: r.key, label: r.label }, A.state.blind) };
|
|
193
236
|
}));
|
|
194
237
|
}
|
|
195
238
|
return files.filter(function (f) { return present(f.name); });
|
|
@@ -114,8 +114,8 @@
|
|
|
114
114
|
]));
|
|
115
115
|
}
|
|
116
116
|
// ⚠️ DE-ROT (F38): on `--debate` runs the re-vote rationale IS structured — debate.json
|
|
117
|
-
// `revotes[] {judge, id, verdict, reason}` (run-debate.js
|
|
118
|
-
// not claim "no structured field". Wording below
|
|
117
|
+
// `revotes[] {judge, id, verdict, reason}` (run-debate.js :: runDebate's `revotesJson` push)
|
|
118
|
+
// — so the parenthetical must not claim "no structured field". Wording below covers both.
|
|
119
119
|
container.appendChild(R.el('p', { className: 'empty-note' }, [
|
|
120
120
|
'Legend: ✓ agree · ✗ dispute · – neutral · * raiser · click a dispute cell for the judge’s prose (rationale lives in prose; on --debate runs a re-voted cell also carries a structured reason from debate.json).',
|
|
121
121
|
]));
|
|
@@ -38,12 +38,23 @@
|
|
|
38
38
|
// computation only when the map itself is absent: older detail payloads (pre-v4.5 runs,
|
|
39
39
|
// live-doc consumers not yet updated to build the map) never carry it, and re-deriving via
|
|
40
40
|
// sanitizeName is exactly what those payloads always did, so it stays correct for them too.
|
|
41
|
+
// ⚠️ v4.8 PR5a T2: the map is now keyed by SEAT ID when the run carries a seat table,
|
|
42
|
+
// and callers pass seat ids to match (RULE OF ONE SPACE — roster, keys and values move
|
|
43
|
+
// together). The legacy re-derivation below is now REACHABLE in a way it never was: an
|
|
44
|
+
// alias-keyed caller used to always hit the map, so the fallback only ever served
|
|
45
|
+
// pre-v4.5 payloads. Under seat keys an alias-keyed caller MISSES — and because a
|
|
46
|
+
// fallback name can be a real file the guard admits (artifact-names.js), re-deriving
|
|
47
|
+
// would hand back another seat's prose instead of the honest empty state. So a miss
|
|
48
|
+
// now returns null and the caller renders "not written yet".
|
|
49
|
+
// Keep the map absent -> legacy path: pre-v4.5 payloads have no map at all and their
|
|
50
|
+
// callers still pass aliases, which is exactly what sanitizeName expects.
|
|
41
51
|
function resolveArtifactName(model, kind) {
|
|
42
52
|
var A = window.AmicusApp;
|
|
43
|
-
var
|
|
44
|
-
var
|
|
45
|
-
if (
|
|
46
|
-
|
|
53
|
+
var derived = A.state.detail.derived;
|
|
54
|
+
var map = derived && derived.artifactsByModel;
|
|
55
|
+
if (!map) { return kind + '-' + sanitizeName(model) + '.md'; }
|
|
56
|
+
var entry = map[model];
|
|
57
|
+
return (entry && entry[kind]) ? entry[kind] : null;
|
|
47
58
|
}
|
|
48
59
|
|
|
49
60
|
// ⚠️ D8 extraction (Task 1): body moved verbatim to workspace-seats.js
|
|
@@ -83,11 +94,23 @@
|
|
|
83
94
|
|
|
84
95
|
// ⚠️ DE-ROT (F38): on a --debate run the FINAL tally.json is rebuilt from the debate's
|
|
85
96
|
// replaced adjudications, so a matrix `dispute` cell can be a re-vote — gate per
|
|
86
|
-
// (judge, findingId), not per run: a
|
|
87
|
-
// actually re-voted; every other dispute cell still belongs to judge-*.md. debate.json's
|
|
88
|
-
// `revotes[]` is keyed on the bench ALIAS (
|
|
89
|
-
//
|
|
90
|
-
//
|
|
97
|
+
// (judge, findingId), not per run: a re-voting seat gets ONE re-vote leg covering only the ids
|
|
98
|
+
// it actually re-voted; every other dispute cell still belongs to judge-*.md. debate.json's
|
|
99
|
+
// `revotes[].judge` is still keyed on the bench ALIAS (src/council/run-debate.js :: runDebate keeps it
|
|
100
|
+
// alias-valued and puts the seat in a sibling `seat` field only when the two differ), which is
|
|
101
|
+
// the same key state.labelByModel uses, so this join needs no inversion.
|
|
102
|
+
// ⚠️ v4.8 PR5a T2 CLOSES the PR3 forecast that stood here. The allowlist is seat-space now
|
|
103
|
+
// (artifact-names.js) and the matrix has been seat-space since PR4c, so `judgePair.model` is
|
|
104
|
+
// a SEAT ID and the name resolves. That is exactly why the re-vote join below had to move in
|
|
105
|
+
// the SAME commit: `revotes[].judge` stays alias-valued by design (run-debate.js :: runDebate), so an
|
|
106
|
+
// alias-vs-seat comparison is permanently false. At HEAD that was harmless — the name missed
|
|
107
|
+
// too, so nothing rendered — but once the name resolves, a false join silently serves the
|
|
108
|
+
// ORIGINAL judge's prose highlighted as the re-vote. `revotes[]` carries a sibling `seat`
|
|
109
|
+
// emit-when-different, so `(r.seat || r.judge)` is ONE term that is correct in both spaces;
|
|
110
|
+
// an `alias AND seat` conjunction would be unsatisfiable on every unique-alias bench, i.e.
|
|
111
|
+
// on every bench that has ever run.
|
|
112
|
+
// Returns the settle promise so callers (and tests) can await the highlight instead of
|
|
113
|
+
// guessing when it lands.
|
|
91
114
|
function drillIntoJudge(judgePair, findingId) {
|
|
92
115
|
var A = window.AmicusApp;
|
|
93
116
|
var panel = A.$('judges-panel');
|
|
@@ -96,13 +119,22 @@
|
|
|
96
119
|
if (!spec) { return Promise.resolve(); }
|
|
97
120
|
return window.AmicusLazy.loadPanel('judges-panel', spec.bodyId, spec.files).then(function () {
|
|
98
121
|
var rv = ((A.state.debate && A.state.debate.revotes) || []).find(function (r) {
|
|
99
|
-
return r.judge === judgePair.model && r.id === findingId;
|
|
122
|
+
return (r.seat || r.judge) === judgePair.model && r.id === findingId;
|
|
100
123
|
});
|
|
101
124
|
// ⚠️ Task 18 fix-wave (RN-1, review finding 1): this branch used to recompute the name via
|
|
102
125
|
// bare sanitizeName(judgePair.model), independently of the (already-fixed) judge branch
|
|
103
126
|
// right below it — for a colliding pair, drilling a re-vote on the SECOND model resolved
|
|
104
127
|
// to the bare name and cross-matched the FIRST model's genuine revote section. Both arms
|
|
105
128
|
// of this ternary now go through the same disambiguation-aware helper.
|
|
129
|
+
// ⚠️ NO ALIAS FALLBACK — refused on purpose (council-3 B3, owner-ruled). The join above
|
|
130
|
+
// misses for debate.json written between 2026-08-12 (run.json began carrying seats[])
|
|
131
|
+
// and 2026-08-13 (revotes[] began carrying seat): those rows have only `judge`, an
|
|
132
|
+
// ALIAS, while judgePair.model is a seat id. Adding `|| r.judge === <this seat's alias>`
|
|
133
|
+
// would "fix" it by picking the wrong twin's re-vote roughly half the time — both twins
|
|
134
|
+
// write judge:'gemini' and nothing in that data distinguishes them, so it is RN-1 again,
|
|
135
|
+
// silent. Missing means rv stays undefined and the judge- artifact renders: the right
|
|
136
|
+
// seat, the wrong KIND, which is the fail-safe direction. That vintage never shipped in
|
|
137
|
+
// a release (`git tag --contains` on the first commit is empty).
|
|
106
138
|
var artifactName = rv
|
|
107
139
|
? resolveArtifactName(judgePair.model, 'revote')
|
|
108
140
|
: resolveArtifactName(judgePair.model, 'judge');
|
|
@@ -196,7 +196,7 @@
|
|
|
196
196
|
seen[key] = true;
|
|
197
197
|
var row = existing[key];
|
|
198
198
|
// ⚠️ DE-ROT (F35): seat.lastActivity is the ISO `leg.lastActivityAt`; format it HERE.
|
|
199
|
-
// seatCells lives in live-
|
|
199
|
+
// seatCells lives in live-seats.js, which loads first and has no access to relTime.
|
|
200
200
|
var view = Object.assign({}, seat, {
|
|
201
201
|
lastActivity: seat.lastActivity ? relTime(seat.lastActivity) : null,
|
|
202
202
|
});
|
|
@@ -217,7 +217,7 @@
|
|
|
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
|
|
220
|
+
// The O(n²) `find()` per seat is deliberate: bench is capped at 26 (anonymize.js :: assignLabels),
|
|
221
221
|
// so this is ~100 rows worst case on a debate run, every 1.5s. `existing` above is
|
|
222
222
|
// already the map an O(n) rewrite would need — reach for it only if this table ever
|
|
223
223
|
// renders unbounded rows.
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* `getRunDetail` returns them wholesale, unfiltered), so `run.degrades` and
|
|
13
13
|
* `verdict.seatLoss` are already on `state.detail` today; no data-layer
|
|
14
14
|
* threading was needed. Only the derivation (window.AmicusLive.deadSeats,
|
|
15
|
-
* live-
|
|
15
|
+
* live-seats.js) and this file's painting are new.
|
|
16
16
|
*
|
|
17
17
|
* NOTE (scope, matches the plan's file list): renderSeatsPanel() (below) is reached from
|
|
18
18
|
* renderDetail() — called both from openRun() (a fresh run open) and from the blind toggle
|
|
@@ -42,19 +42,35 @@
|
|
|
42
42
|
'use strict';
|
|
43
43
|
|
|
44
44
|
/**
|
|
45
|
-
*
|
|
45
|
+
* Keys — seat id where a record supplies one, alias otherwise — of seats whose degrade record
|
|
46
|
+
* says they were retried. PR1F-4 (v4.7 PR7); re-keyed in v4.8 PR5b, renamed from
|
|
47
|
+
* `retriedAliases` because the keys are no longer uniformly aliases.
|
|
46
48
|
*
|
|
47
|
-
* ⚠️
|
|
48
|
-
*
|
|
49
|
+
* ⚠️ THE MIRROR IS RESTORED (v4.8 PR5c). It went partial in PR5b, when this side was
|
|
50
|
+
* seat-keyed and deadSeats still dedup'd on the alias; PR5c seat-keyed the other side too,
|
|
51
|
+
* so both now key on "seat id where the record supplies one, alias otherwise". The
|
|
52
|
+
* kind/channel FILTER still mirrors window.AmicusLive.deadSeats — now at
|
|
53
|
+
* live-dead-seats.js, which PR5c split out of live-seats.js for the 300-line gate — and
|
|
54
|
+
* must keep doing so. Two spellings of one rule is PR5a council finding B1.
|
|
55
|
+
*
|
|
56
|
+
* ⚠️ The two sides are NOT identical, and the difference is deliberate: deadSeats decides
|
|
57
|
+
* whether a seat is RENDERED, so it fails toward showing a row; this decides whether a
|
|
58
|
+
* badge is PAINTED, so an unidentified record falls back to the alias and marks every seat
|
|
59
|
+
* sharing it. Over-badging is visible and self-correcting; a missing badge is silent.
|
|
60
|
+
*
|
|
61
|
+
* The kind/channel filter is load-bearing: run.degrades[] also carries
|
|
49
62
|
* kind:'heal' / channel:'stage1-retry' records with the SAME retryWaveId/firstFailure fields
|
|
50
63
|
* for seats that RECOVERED, and a field-only scan would tag a recovered seat "retried once".
|
|
51
64
|
*
|
|
52
|
-
* ⚠️ firstFailure is TRUTHINESS ONLY.
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
65
|
+
* ⚠️ firstFailure is TRUTHINESS ONLY. Two shapes, both built in
|
|
66
|
+
* run-retry-group.js :: groupStage1Losses — its deadLegs loop emits {seat, class:'leg',
|
|
67
|
+
* status, reason}; its deadWaves loop emits {seat, class: lossClass(w), waveId, reason}
|
|
68
|
+
* at THREE sites (lens/critic/bench), none carrying a status key, where
|
|
69
|
+
* `const lossClass = w => (w.partial ? 'missing' : 'wave')` — so firstFailure.status is
|
|
70
|
+
* undefined on every wave-origin seat. ⚠️ T-A8 DROPPED five line numbers here: re-opened
|
|
71
|
+
* 2026-08-17 they had all rotted a uniform +31 (T-A3 +15, T-A6 +16), and "now 235" is 266.
|
|
56
72
|
*/
|
|
57
|
-
function
|
|
73
|
+
function retriedSeats(degrades) {
|
|
58
74
|
var out = Object.create(null);
|
|
59
75
|
(degrades || []).forEach(function (d) {
|
|
60
76
|
if (!d || d.kind !== 'degrade') { return; }
|
|
@@ -62,15 +78,32 @@
|
|
|
62
78
|
var data = d.data || {};
|
|
63
79
|
if (!(data.retryWaveId || data.firstFailure)) { return; }
|
|
64
80
|
if (d.channel === 'dead-leg') {
|
|
65
|
-
|
|
81
|
+
// ⚠️ Prefer the SEAT ID when the record names one. TWO mechanisms now supply it, not
|
|
82
|
+
// one: retryLegStillDeadNote and missingLegStillDeadNote via `firstFailure.seatId`
|
|
83
|
+
// (pinned by tests/council/run-retry.test.js:628 on a twin bench), and — since
|
|
84
|
+
// v4.8 PR5c — srcLegStillDeadNote via its own `data.seatId`. Reading only the first
|
|
85
|
+
// meant a srcLeg record keyed by ALIAS and badged the live twin "retried once" while
|
|
86
|
+
// the seat that was actually retried showed nothing.
|
|
87
|
+
// `data.seat` remains the last fallback for pre-PR5c records (residual R6).
|
|
88
|
+
var key = (data.firstFailure && data.firstFailure.seatId) || data.seatId || data.seat;
|
|
89
|
+
if (key) { out[key] = true; }
|
|
66
90
|
} else {
|
|
67
|
-
|
|
91
|
+
// v4.8 PR5c: dead-wave now carries `seats[]`, index-parallel with `models[]`, whose
|
|
92
|
+
// elements are seat ids or `null` for a slot the producer could not identify. A null
|
|
93
|
+
// slot falls back to the alias and therefore badges every seat sharing it — the loud
|
|
94
|
+
// direction, disclosed. A pre-PR5c record has no `seats` at all and is alias-only.
|
|
95
|
+
var ws = data.seats;
|
|
96
|
+
(data.models || []).forEach(function (m, i) {
|
|
97
|
+
var k = (Array.isArray(ws) ? ws[i] : null) || m;
|
|
98
|
+
if (k) { out[k] = true; }
|
|
99
|
+
});
|
|
68
100
|
}
|
|
69
101
|
});
|
|
70
102
|
return out;
|
|
71
103
|
}
|
|
72
104
|
|
|
73
|
-
// Mirrors isReviewing at live-
|
|
105
|
+
// Mirrors isReviewing at live-dead-seats.js (moved there by PR5c's size-gate split) —
|
|
106
|
+
// a chair/judge/rebuttal/revote row must not
|
|
74
107
|
// carry a reviewer's retry marker.
|
|
75
108
|
function isReviewingRole(role) {
|
|
76
109
|
return role === 'seat' || role === 'critic' ||
|
|
@@ -91,7 +124,7 @@
|
|
|
91
124
|
// records for the SAME run, and the persisted run.json copy is authoritative when present.
|
|
92
125
|
var deg = (d.run && d.run.degrades && d.run.degrades.length) ? d.run.degrades
|
|
93
126
|
: ((d.verdict && d.verdict.degrades) || []);
|
|
94
|
-
var retried =
|
|
127
|
+
var retried = retriedSeats(deg);
|
|
95
128
|
// ⚠️ Look rows up by data-key, NEVER by position. renderSeats (workspace-render.js:179-216)
|
|
96
129
|
// keys every row on String(seat.id || seat.model) and RN-11 made it REORDER rows to match the
|
|
97
130
|
// composed doc's leg order — so tbody.children[i] is not seats[i]. Build the key exactly the
|
|
@@ -105,7 +138,7 @@
|
|
|
105
138
|
if (!row || !row.children[8]) { return; }
|
|
106
139
|
// Column 8 is the table's unlabeled trailing flag cell (index.html:51's final <th></th>).
|
|
107
140
|
// It carries '⏳ stalled' on the LIVE path; on this terminal path seatsFromRunStats
|
|
108
|
-
// hardcodes stalled:false (live-
|
|
141
|
+
// hardcodes stalled:false (live-seats.js, seatsFromRunStats), so it is always empty here and free to use.
|
|
109
142
|
// If that ever changes, this is the collision site.
|
|
110
143
|
// Fix wave (whole-branch review, finding 2): this pass must be SYMMETRIC. renderSeats
|
|
111
144
|
// reuses rows keyed on `model:role` across calls — including across two different
|
|
@@ -114,7 +147,47 @@
|
|
|
114
147
|
// of the SAME run and leaves a stale 'seat-retried' class on a row that belonged to a
|
|
115
148
|
// PREVIOUS run's non-retried seat. classList.add/remove (not string concatenation) so a
|
|
116
149
|
// repeat add never duplicates the token and a seat that is no longer retried gets cleared.
|
|
117
|
-
|
|
150
|
+
// ⚠️ v4.8 PR5b: DUAL lookup, and both arms are load-bearing. `s.seat` (PR5b Task 1, null
|
|
151
|
+
// on a unique bench) matches the one emitter arm that names a seat; `s.model` matches the
|
|
152
|
+
// four that carry only an alias — srcLegStillDeadNote and every dead-wave record. Pairing
|
|
153
|
+
// this map's dual key with a seat-id-ONLY lookup badges NOTHING for those four arms: a
|
|
154
|
+
// silent false negative replacing a visible false positive, which is strictly worse.
|
|
155
|
+
// On a twin bench an alias-only record still badges BOTH seats. That is not a defect — the
|
|
156
|
+
// record does not say which seat failed, so no consumer can attribute it. Over-badging is
|
|
157
|
+
// the deliberate direction: visible beats silent.
|
|
158
|
+
//
|
|
159
|
+
// ⚠️ NAMING HAZARD (council A1): `seat` means TWO different things three lines apart.
|
|
160
|
+
// `s.seat` is a SEAT ID (`alias#N`, from src/council/run-stats-entry.js :: buildRunStatsEntry
|
|
161
|
+
// via the cost row). The degrade records keyed into `retried` above use `data.seat`,
|
|
162
|
+
// which is an ALIAS and stays one
|
|
163
|
+
// deliberately (run-retry-notes.js:39-45 — verdict.js:72 compares it against `o.critic`).
|
|
164
|
+
// Reading one as the other is precisely how an earlier revision of this fix paired an
|
|
165
|
+
// alias-keyed map with a seat-id lookup and dropped every badge. When touching either
|
|
166
|
+
// side, say which space you are in.
|
|
167
|
+
//
|
|
168
|
+
// ⚠️ The pre-PR expression was `retried[s.modelInput || s.model]` (council B1). The
|
|
169
|
+
// `modelInput` arm is dropped on purpose: this loop only ever iterates
|
|
170
|
+
// `seatsFromRunStats(...)` output (assigned in workspace-seats.js :: renderSeatsPanel), and
|
|
171
|
+
// that projection emits no `modelInput` at all — live payload seats, which DO carry it,
|
|
172
|
+
// reach `deadSeats` in workspace-seats.js :: appendDeadRows and never reach here. The
|
|
173
|
+
// invariant is pinned by test (12) in workspace-seats.test.js;
|
|
174
|
+
// if that test ever fails, restore the `s.modelInput` arm rather than deleting the test.
|
|
175
|
+
// ⚠️ `s.seat &&` is LOAD-BEARING (council A1). `s.seat` is null on a unique bench, and a
|
|
176
|
+
// bare `retried[s.seat]` coerces null to the STRING key 'null' — so a seat with no seat id
|
|
177
|
+
// would match a degrade record whose alias is literally `null`. Measured: with
|
|
178
|
+
// `m = Object.create(null); m['null'] = true`, `m[null]` is `true`. Contrived, but this
|
|
179
|
+
// module already guards the same class elsewhere — live-dead-seats.js, deadSeats' Object.create(null) note, records a model
|
|
180
|
+
// named `toString` crashing the seats repaint, which is why Object.create(null) is used
|
|
181
|
+
// throughout. The guard costs nothing and closes it.
|
|
182
|
+
//
|
|
183
|
+
// ⚠️ KEYSPACE (council B1): `retried` deliberately mixes seat ids (`alias#N`) and bare
|
|
184
|
+
// aliases as keys, because only one of five emitter arms supplies a seat id. That is a
|
|
185
|
+
// real collision surface and a KNOWN one — src/council/seats.js:236 already records that
|
|
186
|
+
// a literal alias containing '#' collides with a minted #N id, and preflightSeats refuses
|
|
187
|
+
// exactly that shape. This map inherits that guarantee rather than re-deriving it; if
|
|
188
|
+
// preflightSeats ever stops refusing it, this lookup becomes ambiguous.
|
|
189
|
+
var isRetried = isReviewingRole(s.role)
|
|
190
|
+
&& !!((s.seat && retried[s.seat]) || retried[s.model]);
|
|
118
191
|
if (isRetried) {
|
|
119
192
|
row.classList.add('seat-retried');
|
|
120
193
|
row.children[8].textContent = '↻ retried once';
|
|
@@ -153,14 +226,25 @@
|
|
|
153
226
|
// Fix wave 2 (smoke-caught, GUI smoke on real degraded run 12c96b6b): dead seats never
|
|
154
227
|
// produce a review, so state.labelByModel (built from the run's names derivation — models
|
|
155
228
|
// that DID review) never carries them; seatCells' own `blindOn && label ? label : alias`
|
|
156
|
-
// fallback is LOAD-BEARING for LIVE rows (RN-9/F36, live-
|
|
229
|
+
// fallback is LOAD-BEARING for LIVE rows (RN-9/F36, live-seats.js) and stays untouched, but
|
|
157
230
|
// for a dead seat that fallback leaks the raw model name under blind — precisely the seat
|
|
158
231
|
// blind mode most needs to hide. Placeholder ONLY when blind is on AND no label resolved;
|
|
159
232
|
// a label that DOES resolve (possible in principle) still wins via seatCells' own cell.
|
|
160
233
|
if (blindOn && !(labelOf && labelOf(seat.model))) { cells[0] = '(masked)'; }
|
|
161
234
|
cells[6] = '';
|
|
235
|
+
// v4.8 PR5c: key on the SEAT, not the alias — two dead twins are two rows and must
|
|
236
|
+
// not share a dataset.key. ⚠️ Honest scope: unlike the live path, this collision has
|
|
237
|
+
// NO measured symptom today. renderSeats removes leavers per ROW
|
|
238
|
+
// (workspace-render.js:231 tests each child's own key), so colliding rows are both
|
|
239
|
+
// removed rather than one leaking, and dead rows are always appended fresh so the
|
|
240
|
+
// reuse path at :197 — where last-wins froze a live row in PR5b — is never reached.
|
|
241
|
+
// It is fixed because :188 still builds a last-wins `existing` map that any future
|
|
242
|
+
// reuse would hit, and because rows for different seats having one key is a landmine.
|
|
243
|
+
// Plain concatenation is injective here (ONE field, and 'dead:' cannot collide with a
|
|
244
|
+
// live key, which is a JSON array starting '['); the live path needs JSON.stringify
|
|
245
|
+
// only because it joins TWO fields.
|
|
162
246
|
var row = window.AmicusRender.el('tr',
|
|
163
|
-
{ className: 'seat-dead', dataset: { key: 'dead:' + seat.model } },
|
|
247
|
+
{ className: 'seat-dead', dataset: { key: 'dead:' + (seat.seat || seat.model) } },
|
|
164
248
|
cells.map(function (c, i) {
|
|
165
249
|
return window.AmicusRender.el('td',
|
|
166
250
|
{ className: window.AmicusRender.seatCellClass(i) }, [c]);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "amicus",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.8.1",
|
|
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": [
|
|
@@ -63,6 +63,7 @@
|
|
|
63
63
|
"models:info": "node bin/amicus.js models",
|
|
64
64
|
"models:check": "node bin/amicus.js models --check",
|
|
65
65
|
"models:check:strict": "node bin/amicus.js models --check --strict",
|
|
66
|
+
"check:ci-alias-pins": "node scripts/check-ci-alias-pins.js",
|
|
66
67
|
"generate-icon": "node scripts/generate-icon.js",
|
|
67
68
|
"generate-docs": "node scripts/generate-docs.js",
|
|
68
69
|
"generate-docs:check": "node scripts/generate-docs.js --check",
|
|
@@ -71,6 +72,7 @@
|
|
|
71
72
|
"graphify:index": "graphify extract . --backend claude-cli",
|
|
72
73
|
"check:secrets": "node scripts/check-secrets.js --all",
|
|
73
74
|
"check:sizes": "node scripts/check-file-sizes.js --all",
|
|
75
|
+
"check:citations": "node scripts/check-citations.js --all",
|
|
74
76
|
"check:tarball": "node scripts/check-tarball-lifecycle.js"
|
|
75
77
|
},
|
|
76
78
|
"dependencies": {
|
|
@@ -93,6 +95,7 @@
|
|
|
93
95
|
"eslint": "^8.0.0",
|
|
94
96
|
"espree": "^9.6.1",
|
|
95
97
|
"jest": "^29.0.0",
|
|
98
|
+
"js-yaml": "^4.3.0",
|
|
96
99
|
"lint-staged": "^16.3.2",
|
|
97
100
|
"puppeteer": "^24.36.0",
|
|
98
101
|
"sharp": "^0.35.3",
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
"model": { "type": ["string", "null"], "description": "Resolved executable id (metadata.model)." },
|
|
40
40
|
"modelInput": { "type": ["string", "null"], "description": "Council alias (run.json bench/chair/critic/lenses are keyed on this, not `model`); null when the leg's metadata.json has not been patched with it yet (F36)." },
|
|
41
41
|
"role": { "type": ["string", "null"], "description": "'seat' | 'critic' | 'lens:<slug>' | 'chair', derived via roleFor keyed on modelInput (chair via the owning stage's name instead, F34); null when modelInput is unknown." },
|
|
42
|
+
"seat": { "type": ["string", "null"], "description": "v4.8 R5, optional. This leg's seat id (`alias#N`) when the bench repeats an alias, else null. Written at launch by src/sidecar/fanout-leg.js :: runSingleAttempt from the per-wave roster and read back by src/observe/council-legs.js :: buildLegRow. Present on every leg row, including a truthful null — unlike metadata.json's own `seat` key, which is absent (not null) on a unique-alias bench: `buildLegRow`'s base row literal is unconditional and already emits nulls the same way for `model`/`modelInput`, so this field matches that literal rather than the absent-when-unset convention `usageError` above uses. The leg-row `items` object declares no `additionalProperties: false`, so an additive field was already accepted here — this documents the shape rather than changing what is accepted." },
|
|
42
43
|
"status": { "type": "string" },
|
|
43
44
|
"messages": { "type": "number" },
|
|
44
45
|
"stage": { "type": "string" },
|
|
@@ -43,6 +43,25 @@
|
|
|
43
43
|
}
|
|
44
44
|
},
|
|
45
45
|
"labelMap": { "type": ["object", "null"], "additionalProperties": { "type": "string" } },
|
|
46
|
+
"seats": {
|
|
47
|
+
"description": "v4.8: derived seat table, one entry per bench position. Bench-only — the synthesized 'claude' review is never a seat.",
|
|
48
|
+
"type": ["array", "null"],
|
|
49
|
+
"items": {
|
|
50
|
+
"type": "object",
|
|
51
|
+
"required": ["id", "alias", "role", "position"],
|
|
52
|
+
"properties": {
|
|
53
|
+
"id": { "type": "string" },
|
|
54
|
+
"alias": { "type": "string" },
|
|
55
|
+
"role": { "type": "string" },
|
|
56
|
+
"lens": { "type": ["string", "null"] },
|
|
57
|
+
"position": { "type": "integer" }
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
"criticSeat": {
|
|
62
|
+
"description": "v4.8: seat id of the critic, or null.",
|
|
63
|
+
"type": ["string", "null"]
|
|
64
|
+
},
|
|
46
65
|
"options": { "type": "object" },
|
|
47
66
|
"pack": { "type": "object" },
|
|
48
67
|
"tag": { "type": "string" },
|