amicus 4.7.0 → 4.8.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 +948 -1
- package/README.md +3 -2
- package/docs/CITATIONS.md +114 -0
- package/docs/ROADMAP.md +33 -5
- package/docs/SHIMS.md +1 -1
- package/docs/configuration.md +3 -3
- package/docs/council.md +181 -34
- package/docs/doc-system.md +1 -1
- package/docs/publishing.md +2 -0
- package/docs/testing.md +1 -1
- package/docs/troubleshooting.md +6 -4
- package/docs/usage.md +15 -11
- package/electron/setup-ui-aliases.js +2 -2
- 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 +6 -3
- 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/scripts/postinstall.js +6 -3
- package/scripts/setup-hooks.js +49 -3
- 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/skills/sidecar/SKILL.md +3 -1
- package/src/cli-handlers-doctor.js +7 -0
- package/src/cli-handlers-resume-continue.js +20 -0
- 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/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 +56 -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 +179 -162
- 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 +83 -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/continue.js +8 -23
- package/src/sidecar/fanout-leg.js +22 -1
- package/src/sidecar/fanout-retry.js +15 -3
- 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 +32 -0
- package/src/sidecar/resume.js +1 -1
- package/src/sidecar/setup.js +13 -2
- 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-engine-check.js +32 -0
- package/src/utils/engine-install-scan.js +98 -15
- package/src/utils/engine-repair.js +96 -2
- package/src/utils/no-output-backstop.js +1 -1
- package/src/utils/remediation-hints.js +42 -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 +27 -1
- package/src/workspace/seat-space.js +143 -0
|
@@ -25,10 +25,11 @@ const runState = require('./run-state');
|
|
|
25
25
|
const { runStage2 } = require('./run-stage2');
|
|
26
26
|
const { launchStage1 } = require('./run-stage1-launch');
|
|
27
27
|
const { buildRunStatsEntry } = require('./run-assemble');
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
const { pushDeadSeatRows } = require('./run-stage1-rows');
|
|
29
|
+
const { bindStage1Waves, orphanLegNote, missingSeatDeadWave } = require('./stage1-bind');
|
|
30
|
+
// slug lives in ./seats (v4.8 PR1) so that module can stay require-free;
|
|
31
|
+
// re-exported below — run-stages.test.js imports it from here.
|
|
32
|
+
const { slug } = require('./seats');
|
|
32
33
|
|
|
33
34
|
/** Role of a seat by its input alias. */
|
|
34
35
|
function roleFor(o, alias) {
|
|
@@ -60,35 +61,67 @@ function roleFor(o, alias) {
|
|
|
60
61
|
*/
|
|
61
62
|
async function runStage1(ctx) {
|
|
62
63
|
const { o } = ctx;
|
|
63
|
-
const { aborted, legs, deadWaves } = await launchStage1(ctx);
|
|
64
|
+
const { aborted, legs, deadWaves, waves } = await launchStage1(ctx);
|
|
64
65
|
if (aborted) { return { aborted, reviews: [], deadLegs: [], deadWaves: [], degraded: false, extraRows: [] }; }
|
|
65
66
|
|
|
66
|
-
|
|
67
|
+
// Per-wave binding, before anything reads a leg. Orphans are announced now —
|
|
68
|
+
// they are not a loss, so the "never degrade for a seat the retry saves" rule
|
|
69
|
+
// below does not apply to them.
|
|
70
|
+
const { seatOf, missingSeats, orphanLegs } = bindStage1Waves(waves);
|
|
71
|
+
for (const { waveId, leg } of orphanLegs) { ctx.degrade.note(orphanLegNote(waveId, leg)); }
|
|
72
|
+
// R-B: a launched seat whose leg never came back is a LOSS — it lands in
|
|
73
|
+
// neither deadLegs (no leg object) nor deadWaves (the wave DID produce legs).
|
|
74
|
+
// It reaches the retry as a single-seat dead wave flagged `partial` so its
|
|
75
|
+
// prose stays true, and is announced only if the retry cannot save it.
|
|
76
|
+
const allDeadWaves = [...deadWaves, ...missingSeats.map(missingSeatDeadWave)];
|
|
77
|
+
|
|
78
|
+
const firstPass = materializeReviews(o.runDir, legs, seatOf);
|
|
67
79
|
const alive0 = new Set(firstPass.map(m => m.leg));
|
|
68
80
|
const deadLegs0 = legs.filter(l => !alive0.has(l));
|
|
69
81
|
|
|
70
82
|
// SL-2: one retry BEFORE anything is recorded lost — the sink never
|
|
71
83
|
// un-flips, so a degrade for a seat the retry saves must never fire at all.
|
|
72
|
-
|
|
73
|
-
|
|
84
|
+
// `total` counts SEATS: legs.length alone renders "1 of 1 seats reviewed"
|
|
85
|
+
// beside a degrade when a seat's leg never returned.
|
|
86
|
+
const retry = await retryStage1Losses(ctx, { deadWaves: allDeadWaves, deadLegs: deadLegs0, seatOf,
|
|
87
|
+
counts: { reviewed: firstPass.length, total: legs.length + missingSeats.length } });
|
|
74
88
|
if (retry.aborted) {
|
|
75
89
|
// Final whole-branch review: same bug class as the post-retry-repair
|
|
76
90
|
// abort fixed ~87 lines below ("Must be the post-retry set") — subtract
|
|
77
91
|
// whatever retry.recoveredLegs already healed before this abort landed.
|
|
78
|
-
|
|
92
|
+
// SEAT-keyed since v4.8 H4: twin seats now retry INDEPENDENTLY, so an alias
|
|
93
|
+
// Set marks BOTH healed the moment one of them is — the still-dead twin
|
|
94
|
+
// silently disappears from deadLegs AND deadWaves[].models, and run.js
|
|
95
|
+
// persists that return into stage-1 state as if it had reviewed.
|
|
96
|
+
const keyOf = (l, bind) => { const s = bind.get(l); return s ? s.id : (l.modelInput || l.model); };
|
|
97
|
+
const healed = new Set(retry.recoveredLegs.map(l => keyOf(l, retry.seatOf)));
|
|
98
|
+
// `seats` must be narrowed in LOCKSTEP with `models`: a bare `...w` carries
|
|
99
|
+
// the FULL roster past a narrowed models list, and run.js persists this
|
|
100
|
+
// record before the abort short-circuit — so index i of each would name a
|
|
101
|
+
// different seat in run.json. Omitted entirely when the source had none.
|
|
79
102
|
return { aborted: retry.aborted, reviews: [], degraded: false, extraRows: [],
|
|
80
|
-
deadLegs: deadLegs0.filter(l => !healed.has(l
|
|
81
|
-
deadWaves:
|
|
103
|
+
deadLegs: deadLegs0.filter(l => !healed.has(keyOf(l, seatOf))),
|
|
104
|
+
deadWaves: allDeadWaves.map((w) => {
|
|
105
|
+
const keep = (w.models || []).map((m, i) => [m, (w.seats || [])[i] || null])
|
|
106
|
+
.filter(([m, s]) => !healed.has(s ? s.id : m));
|
|
107
|
+
return { ...w, models: keep.map(x => x[0]), ...(w.seats ? { seats: keep.map(x => x[1]) } : {}) };
|
|
108
|
+
}).filter(w => w.models.length > 0) };
|
|
82
109
|
}
|
|
83
110
|
|
|
84
111
|
for (const d of retry.skippedDeadWaves) {
|
|
112
|
+
// A `partial` record is one seat of a wave that DID produce legs, so the
|
|
113
|
+
// plain dead-wave sentence would be false. `seat` rides only on that shape:
|
|
114
|
+
// adding it unconditionally breaks an exact toEqual on a real dead wave.
|
|
85
115
|
ctx.degrade.note({
|
|
86
|
-
channel: 'dead-wave',
|
|
87
|
-
what:
|
|
116
|
+
channel: d.partial ? 'seat-unbound' : 'dead-wave',
|
|
117
|
+
what: d.partial
|
|
118
|
+
? `seat ${(d.models || [])[0]} did not review`
|
|
119
|
+
: `Stage-1 wave ${d.waveId} (${d.models.join(', ') || 'no models'}) produced NO legs`,
|
|
88
120
|
why: d.reason,
|
|
89
121
|
effect: 'Those seats are NOT in this council. The run continues with the bench that did '
|
|
90
122
|
+ 'launch and will exit degraded (2)',
|
|
91
|
-
data: { waveId: d.waveId, models: d.models, reason: d.reason
|
|
123
|
+
data: { waveId: d.waveId, models: d.models, reason: d.reason,
|
|
124
|
+
...(d.partial ? { seat: (d.models || [])[0] } : {}) },
|
|
92
125
|
});
|
|
93
126
|
}
|
|
94
127
|
for (const leg of retry.skippedDeadLegs) {
|
|
@@ -96,20 +129,28 @@ async function runStage1(ctx) {
|
|
|
96
129
|
channel: 'dead-leg',
|
|
97
130
|
what: `seat ${leg.modelInput || leg.model} did not review`,
|
|
98
131
|
why: `the leg ended '${leg.status}'${leg.error ? `: ${leg.error}` : ''} with no usable output`,
|
|
99
|
-
effect: `${firstPass.length} of ${legs.length} seats reviewed; `
|
|
132
|
+
effect: `${firstPass.length} of ${legs.length + missingSeats.length} seats reviewed; `
|
|
100
133
|
+ 'the run continues with the bench that did and will exit degraded (2)',
|
|
101
134
|
data: { seat: leg.modelInput || leg.model, status: leg.status, reason: leg.error || null },
|
|
102
135
|
});
|
|
103
136
|
}
|
|
104
137
|
for (const rec of retry.stillDeadNotes) { ctx.degrade.note(rec); }
|
|
138
|
+
// Same shape: the retry pass BUILDS its orphan records, the caller EMITS them
|
|
139
|
+
// (that module emits heals only and never notes a degrade, by construction).
|
|
140
|
+
for (const { waveId, leg } of retry.orphanLegs) { ctx.degrade.note(orphanLegNote(waveId, leg)); }
|
|
105
141
|
|
|
106
142
|
// Invariant this merge relies on: retry.recoveredLegs only ever names seats
|
|
107
143
|
// that actually lost their seat on the first pass (run-retry.js's recovery
|
|
108
144
|
// loop drops any leg for a seat with no firstFailures entry) — so `legs`
|
|
109
145
|
// and `recoveredLegs` can never both carry a leg for the same seat here.
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
|
|
146
|
+
// A recovered leg is a RETRY-wave object, absent from Stage-1's object-keyed
|
|
147
|
+
// seatOf, so this union is mandatory rather than tidiness: without it every
|
|
148
|
+
// healed seat re-materializes with seat:null and its role falls back to
|
|
149
|
+
// roleFor's alias shim. Re-writing an already-materialized recovered leg's review file
|
|
150
|
+
// is only an idempotent no-op while the name is the seat's — under the alias
|
|
151
|
+
// it is the twin clobber this PR removes (two healed twins, one file).
|
|
152
|
+
const allSeatOf = new Map([...seatOf, ...retry.seatOf]);
|
|
153
|
+
const materialized = materializeReviews(o.runDir, [...legs, ...retry.recoveredLegs], allSeatOf);
|
|
113
154
|
const stillDeadLegs = [...retry.skippedDeadLegs, ...retry.stillDeadLegs];
|
|
114
155
|
const stillDeadWaves = [...retry.skippedDeadWaves, ...retry.stillDeadWaves];
|
|
115
156
|
|
|
@@ -220,63 +261,25 @@ async function runStage1(ctx) {
|
|
|
220
261
|
}
|
|
221
262
|
}
|
|
222
263
|
reviews.push({
|
|
223
|
-
model: m.modelInput, modelInput: m.modelInput,
|
|
264
|
+
model: m.modelInput, modelInput: m.modelInput, seat: m.seat || null,
|
|
265
|
+
// Seat-space role (spec §4.5), read off the SEAT — NOT roleAt(o.seats):
|
|
266
|
+
// run-stage1-launch.js re-derives the table when o.seats is absent, so
|
|
267
|
+
// m.seat is truthy while o.seats is not, and roleAt's unknown-id 'seat'
|
|
268
|
+
// collapses every critic/lens role. Unbound legs keep the roleFor shim.
|
|
269
|
+
role: m.seat ? m.seat.role : roleFor(o, m.modelInput),
|
|
224
270
|
text: m.text, findings: res.ok ? res.findings : [], conformance, leg: m.leg,
|
|
225
271
|
...(unverified ? { findingsUnverified: true } : {}),
|
|
226
272
|
...(repairRefused ? { repairRefused } : {}),
|
|
227
273
|
});
|
|
228
274
|
}
|
|
229
275
|
|
|
230
|
-
//
|
|
231
|
-
//
|
|
232
|
-
//
|
|
233
|
-
//
|
|
234
|
-
//
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
const supersededAliases = new Set([
|
|
238
|
-
...retry.recoveredLegs.map(l => l.modelInput || l.model),
|
|
239
|
-
...retry.stillDeadLegs.map(l => l.modelInput || l.model),
|
|
240
|
-
]);
|
|
241
|
-
for (const dead of deadLegs0) {
|
|
242
|
-
const alias = dead.modelInput || dead.model;
|
|
243
|
-
if (supersededAliases.has(alias)) {
|
|
244
|
-
extraRows.push(buildRunStatsEntry({ leg: dead, model: alias, role: 'superseded', wasChair: false }));
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
// v4.7 D2/E4 — primary error rows: one per seat with NO surviving review
|
|
249
|
-
// (every alias still in stillDeadLegs/stillDeadWaves after retry). E5
|
|
250
|
-
// amended (Task-4 review, owner-ruled): run-retry.js now surfaces the real
|
|
251
|
-
// retry leg (stillDeadRetryLegs), from the ONE branch it exists in —
|
|
252
|
-
// retryLegStillDeadNote, a retry leg that came back unusable. Prefer that
|
|
253
|
-
// REAL leg: status/waveId/usage/duration all real, all from the SAME
|
|
254
|
-
// attempt (no more pairing a retry's waveId with a different attempt's
|
|
255
|
-
// status). The other two dead-leg note classes — srcLegStillDeadNote (retry
|
|
256
|
-
// wave died wholesale, zero legs) and missingLegStillDeadNote (partial
|
|
257
|
-
// return never named this seat) — never get a real leg, so `leg: null`
|
|
258
|
-
// (no phantom waveId: one must never appear without a real billed leg).
|
|
259
|
-
// No 'dead-leg' note at all ⇒ never retried (skipped) ⇒ the original dead
|
|
260
|
-
// leg is this seat's only, and therefore final, leg.
|
|
261
|
-
const retryLegByAlias = new Map();
|
|
262
|
-
for (const leg of retry.stillDeadRetryLegs) { retryLegByAlias.set(leg.modelInput || leg.model, leg); }
|
|
263
|
-
const attemptedAliases = new Set();
|
|
264
|
-
for (const n of retry.stillDeadNotes) {
|
|
265
|
-
if (n.channel === 'dead-leg' && n.data && n.data.seat) { attemptedAliases.add(n.data.seat); }
|
|
266
|
-
}
|
|
267
|
-
const deadAliases = new Set([
|
|
268
|
-
...stillDeadLegs.map(l => l.modelInput || l.model),
|
|
269
|
-
...stillDeadWaves.flatMap(w => w.models || []),
|
|
270
|
-
]);
|
|
271
|
-
for (const alias of deadAliases) {
|
|
272
|
-
let finalLeg = retryLegByAlias.get(alias);
|
|
273
|
-
if (!finalLeg) {
|
|
274
|
-
finalLeg = attemptedAliases.has(alias)
|
|
275
|
-
? null // retried; no leg at all for this seat
|
|
276
|
-
: (deadLegs0.find(l => (l.modelInput || l.model) === alias) || null); // never retried
|
|
277
|
-
}
|
|
278
|
-
extraRows.push(buildRunStatsEntry({ leg: finalLeg, model: alias, role: roleFor(o, alias), wasChair: false }));
|
|
279
|
-
}
|
|
276
|
+
// Superseded + dead-seat rows live in ./run-stage1-rows (v4.8 PR0 size-gate split).
|
|
277
|
+
// ⚠️ allSeatOf, never the Stage-1 `seatOf`: retry.recoveredLegs and
|
|
278
|
+
// retry.stillDeadRetryLegs are retry-wave objects that map has never seen.
|
|
279
|
+
// ⚠️ `retry.twins`, never a fresh `twinAliases(o.seats)`: the row keys asked for here must be
|
|
280
|
+
// minted from the SAME collection the retry pass filled `attemptedSeats` with (v4.8 T-A6).
|
|
281
|
+
pushDeadSeatRows({ o, retry, deadLegs0, stillDeadLegs, stillDeadWaves, extraRows,
|
|
282
|
+
roleFor, seatOf: allSeatOf, degrade: ctx.degrade, twins: retry.twins });
|
|
280
283
|
|
|
281
284
|
return { aborted: null, reviews, deadLegs: stillDeadLegs, deadWaves: stillDeadWaves,
|
|
282
285
|
degraded: stillDeadLegs.length > 0 || stillDeadWaves.length > 0, extraRows };
|
package/src/council/run-state.js
CHANGED
|
@@ -96,7 +96,7 @@ function initCouncilRun(o) {
|
|
|
96
96
|
initRun(o.runDir, {
|
|
97
97
|
schemaVersion: 2, type: 'council-run', runId: o.runId, status: 'running', stages: [],
|
|
98
98
|
bench: o.models.slice(), chair: o.chair, critic: o.critic, lenses: o.lenses,
|
|
99
|
-
labelMap: null,
|
|
99
|
+
labelMap: null, seats: null, criticSeat: null,
|
|
100
100
|
// Seeded ONLY under --debate (a `debate:null` seed would both break the v4.0
|
|
101
101
|
// "no debate key" contract and fail the object-typed schema), and with a VALID
|
|
102
102
|
// outcome from the first write so a run killed mid-debate stays schema-valid.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// src/council/run-stats-entry.js
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @module council/run-stats-entry
|
|
6
|
+
* One runStats row from a leg run document. Extracted verbatim from
|
|
7
|
+
* ./run-assemble (v4.8 Phase 1 T1.1) to give that file headroom under the
|
|
8
|
+
* 300-line gate, and re-exported there so every existing call spelling —
|
|
9
|
+
* `asm.buildRunStatsEntry(...)` and pulling `buildRunStatsEntry` off
|
|
10
|
+
* `./run-assemble`'s own exports — survives the move untouched.
|
|
11
|
+
*
|
|
12
|
+
* ⚠️ This module is REQUIRE-FREE by design, like ./seats. Consumers that cannot
|
|
13
|
+
* take run-assemble's graph (./debate.js is dependency-injection-free) must be
|
|
14
|
+
* able to import it. Do not add a `require` call here — the pin (P3,
|
|
15
|
+
* tests/council/run-stats-entry.test.js) scans this file's raw text for the
|
|
16
|
+
* word `require` immediately followed by an opening parenthesis, so it fires
|
|
17
|
+
* on that sequence ANYWHERE, including inside a comment, not only inside a
|
|
18
|
+
* real statement.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* One runStats row from a leg run document. Verbatim copies only — a missing
|
|
23
|
+
* leg doc yields durationMs/usage null (never invent a value). `model` (the
|
|
24
|
+
* council alias) overrides leg.model (the resolved executable id) so ledger
|
|
25
|
+
* rows join meta.models by exact string (ledger.js:20-24).
|
|
26
|
+
* `resolvedModel` (v4.7 GOA-7) preserves leg.model — the executable id that
|
|
27
|
+
* actually served, post-fallback-substitution — emit-only-when-set and never
|
|
28
|
+
* sourced from modelInput (an alias must never masquerade as a resolved id).
|
|
29
|
+
*
|
|
30
|
+
* ⚠️ LC-11 / review F1: `findingsUnverified` and `repairRefused` are the same
|
|
31
|
+
* class of fact as `conformance` and ride the same row. They are the two halves
|
|
32
|
+
* of the repair contract's outcome: `findingsUnverified` marks a 'repaired' seat
|
|
33
|
+
* whose contract could NOT be checked (the original block was absent or
|
|
34
|
+
* unparseable, so there was no finding count to compare), and `repairRefused`
|
|
35
|
+
* ({code, detail}) marks the stronger case — the contract WAS checked and broken,
|
|
36
|
+
* which is otherwise indistinguishable from a seat that never emitted JSON at
|
|
37
|
+
* all. Both are additive and present only when set, so a run without either is
|
|
38
|
+
* byte-for-byte unchanged.
|
|
39
|
+
*
|
|
40
|
+
* `seat` (v4.8 PR4c §3.1) is the seat OBJECT — {id, alias, role, lens, position}
|
|
41
|
+
* or null — never an id string. Callers pass `r.seat` / the dead-seat loop's own
|
|
42
|
+
* `seat` verbatim, so the object IS the contract instead of a prose one.
|
|
43
|
+
*/
|
|
44
|
+
function buildRunStatsEntry({ leg, model, role, wasChair, conformance, findingsUnverified,
|
|
45
|
+
repairRefused, seat }) {
|
|
46
|
+
return {
|
|
47
|
+
model: model !== undefined ? model : (leg ? leg.model : null),
|
|
48
|
+
role,
|
|
49
|
+
wasChair: !!wasChair,
|
|
50
|
+
conformance: conformance || 'clean',
|
|
51
|
+
...(findingsUnverified ? { findingsUnverified: true } : {}),
|
|
52
|
+
...(repairRefused ? { repairRefused } : {}),
|
|
53
|
+
...(leg && leg.waveId ? { waveId: leg.waveId } : {}),
|
|
54
|
+
...(leg && leg.model ? { resolvedModel: leg.model } : {}),
|
|
55
|
+
// v4.8 PR4c §3.1 / R4c-9: emit-when-DIFFERENT, compared against the seat's
|
|
56
|
+
// OWN alias — never against `model`. buildSeats mints `alias#N` only when
|
|
57
|
+
// an alias repeats (seats.js:67), so `id !== alias` IS "the bench repeats
|
|
58
|
+
// this alias": the single predicate all four seat-emit producers now share,
|
|
59
|
+
// which is what stops them disagreeing. `model` is the LEG's modelInput,
|
|
60
|
+
// which is NOT the alias when a leg reports none (it falls back to the
|
|
61
|
+
// RESOLVED id, the same fallback run-launch.js :: materializeReviews uses)
|
|
62
|
+
// or when a --council preset carries a padded member — either would ship a
|
|
63
|
+
// seat id with no seat table behind it, on a bench with no twin at all.
|
|
64
|
+
...(seat && seat.id !== seat.alias ? { seat: seat.id } : {}),
|
|
65
|
+
status: leg ? leg.status : 'error',
|
|
66
|
+
durationMs: leg && typeof leg.durationMs === 'number' ? leg.durationMs : null,
|
|
67
|
+
usage: (leg && leg.usage) || null,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
module.exports = { buildRunStatsEntry };
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// src/council/run-verdict-files.js
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @module council/run-verdict-files
|
|
6
|
+
* The verdict half of the run-dir artifact set: verdict.json (with the nullable
|
|
7
|
+
* overallVerdict and the derived seatLoss), report.html and chair-output.md.
|
|
8
|
+
*
|
|
9
|
+
* Lifted VERBATIM out of run-assemble.js for the 300-line gate (v4.8 PR4c
|
|
10
|
+
* Task 2), on the same precedent as seats.js/preflightSeats: the body lives
|
|
11
|
+
* here and run-assemble re-exports it, so `asm.writeVerdictFiles(...)` — the
|
|
12
|
+
* only production call spelling (run-finish.js:63) — is untouched and no test
|
|
13
|
+
* moved. This was the only consumer of run-assemble's `./verdict` and
|
|
14
|
+
* `./report` requires, so the two came with it.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
const { buildVerdict, summarizeSeatLoss, deriveSeatLoss, writeVerdictAtomic } = require('./verdict');
|
|
20
|
+
const { buildReport } = require('./report');
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Undecided verdict + deterministic report. Sets the nullable overallVerdict
|
|
24
|
+
* (council family v2, Plan A) on buildVerdict's output — independent of
|
|
25
|
+
* buildVerdict's own signature.
|
|
26
|
+
* @param {{runDir: string, record: object, overallVerdict?: (string|null),
|
|
27
|
+
* chairText?: string, critic?: string, deadWaves?: Array<object>,
|
|
28
|
+
* degrades?: Array<object>}} o `degrades` (v4.6 Plan 2), when present, is
|
|
29
|
+
* both carried onto the verdict and used to DERIVE `seatLoss` (deriveSeatLoss)
|
|
30
|
+
* in preference to summarizing it from `deadWaves` (summarizeSeatLoss).
|
|
31
|
+
* @returns {object} the verdict written to disk
|
|
32
|
+
*/
|
|
33
|
+
function writeVerdictFiles({ runDir, record, overallVerdict, chairText, critic, deadWaves, degrades }) {
|
|
34
|
+
// v4.6 Plan 2 (spec D3): when the sink's records are available they are the
|
|
35
|
+
// single source of truth — seatLoss derives from them so it can never
|
|
36
|
+
// disagree with degrades[]. deadWaves remains the fallback for direct
|
|
37
|
+
// callers that predate the sink (their tests pass unedited).
|
|
38
|
+
const seatLoss = degrades
|
|
39
|
+
? deriveSeatLoss({ runId: record.meta.runId, critic, degrades })
|
|
40
|
+
: summarizeSeatLoss({ runId: record.meta.runId, critic, deadWaves });
|
|
41
|
+
const verdict = buildVerdict(record, [], { seatLoss, degrades });
|
|
42
|
+
verdict.overallVerdict = (overallVerdict === undefined) ? null : overallVerdict;
|
|
43
|
+
writeVerdictAtomic(path.join(runDir, 'verdict.json'), verdict);
|
|
44
|
+
const html = buildReport({ verdict }, { format: 'html' });
|
|
45
|
+
fs.writeFileSync(path.join(runDir, 'report.html'), html, { mode: 0o600 });
|
|
46
|
+
if (chairText) {
|
|
47
|
+
fs.writeFileSync(path.join(runDir, 'chair-output.md'), chairText, { mode: 0o600 });
|
|
48
|
+
}
|
|
49
|
+
return verdict;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
module.exports = { writeVerdictFiles };
|
package/src/council/run.js
CHANGED
|
@@ -27,13 +27,13 @@ const { createLaunchers } = require('./run-launch');
|
|
|
27
27
|
const { runStage1, runStage2 } = require('./run-stages'); // stage 2 lives in ./run-stage2 (300-line gate), re-exported there
|
|
28
28
|
const { runChair, pickFallbackChair } = require('./run-chair');
|
|
29
29
|
const { runDebateStage } = require('./run-debate-stage'); // debate orchestration lives there (300-line gate), extracted from here (v4.6 Plan 1 Task 1)
|
|
30
|
-
const { decorateRecord } = require('./debate');
|
|
31
30
|
const asm = require('./run-assemble');
|
|
32
31
|
const { createBudget } = require('./run-budget');
|
|
33
32
|
const { emitRunStarted, emitStageStarted, emitStageTerminal } = require('../observe/events');
|
|
34
33
|
// v4.4.1 CA-6: the whole exit-code vocabulary (SIGNAL_EXIT, statusForExit and the
|
|
35
34
|
// degradation in resolveTerminalExit) lives in ./run-finalize — see its docblock.
|
|
36
35
|
const { writeRunTerminal, resolveTerminalExit, SIGNAL_EXIT } = require('./run-finalize');
|
|
36
|
+
const { finishRun } = require('./run-finish');
|
|
37
37
|
|
|
38
38
|
/**
|
|
39
39
|
* @param {object} options {briefing, models, chair, critic?, lenses?, project, runId,
|
|
@@ -125,6 +125,15 @@ async function runCouncil(options, deps = {}) {
|
|
|
125
125
|
if (pre.error) { return finalize(1, pre.error); }
|
|
126
126
|
const claudeReview = pre.claudeReview;
|
|
127
127
|
|
|
128
|
+
// v4.8 §4.3: seats are derived pre-spend from data run.json already holds,
|
|
129
|
+
// then checkpointed — initCouncilRun ran ~50 lines earlier, so they cannot
|
|
130
|
+
// ride the seed.
|
|
131
|
+
const seatPre = asm.preflightSeats(o);
|
|
132
|
+
if (seatPre.error) { return finalize(1, seatPre.error); }
|
|
133
|
+
o.seats = seatPre.seats;
|
|
134
|
+
o.criticSeat = seatPre.criticSeat;
|
|
135
|
+
runState.checkpoint(o.runDir, { seats: o.seats, criticSeat: o.criticSeat });
|
|
136
|
+
|
|
128
137
|
// Composed Stage-1 seat briefing persisted for auditability (spec §4 layout).
|
|
129
138
|
fs.writeFileSync(path.join(o.runDir, 'briefing-stage1.md'),
|
|
130
139
|
briefings.buildSeatBriefing({ briefing: o.briefing, date: o.date }), { mode: 0o600 });
|
|
@@ -172,12 +181,25 @@ async function runCouncil(options, deps = {}) {
|
|
|
172
181
|
|
|
173
182
|
// ---- Stage 2: anonymized cross-review ----
|
|
174
183
|
// A file-sourced Claude review is ALWAYS the last label — review N+1 (§4.4).
|
|
175
|
-
|
|
184
|
+
// v4.8 T3.2: seats travel in lockstep with models — both are s1.reviews in
|
|
185
|
+
// the same pass, not the positional join anonymize.js :: assignLabels's
|
|
186
|
+
// own docblock explains is forbidden elsewhere.
|
|
187
|
+
const labels = assignLabels(s1.reviews.map(r => r.model).concat(claudeReview ? ['claude'] : []),
|
|
188
|
+
s1.reviews.map(r => r.seat).concat(claudeReview ? [null] : []));
|
|
176
189
|
runState.checkpoint(o.runDir, { labelMap: labels.labelMap });
|
|
177
190
|
// Attach each review's run-global findings (buildTallyInput reads
|
|
178
191
|
// r.globalFindings per review, not a bare parallel array).
|
|
179
192
|
s1.reviews.forEach((r, i) => {
|
|
180
|
-
|
|
193
|
+
// v4.8 PR3 Task 5, re-based by PR4c R4c-9: pass the seat id ONLY when it
|
|
194
|
+
// differs from the seat's OWN alias — the naive `r.seat ? r.seat.id :
|
|
195
|
+
// null` form would emit raiserSeat on every finding of every run, a
|
|
196
|
+
// universal artifact-shape change. The operand was `r.model`, the leg's
|
|
197
|
+
// modelInput — not the alias when a leg reports none (it falls back to
|
|
198
|
+
// the RESOLVED id) or when a --council preset carries a padded member.
|
|
199
|
+
// There it emitted a seat id byte-equal to its own alias, on a bench with
|
|
200
|
+
// no twin, with no seat table able to resolve it.
|
|
201
|
+
r.globalFindings = toGlobalFindings(labels.entries[i].letter, r.model, r.findings,
|
|
202
|
+
r.seat && r.seat.id !== r.seat.alias ? r.seat.id : null);
|
|
181
203
|
});
|
|
182
204
|
const globalFindings = s1.reviews.flatMap(r => r.globalFindings)
|
|
183
205
|
.concat(claudeReview ? asm.labelClaudeReview(claudeReview, labels) : []);
|
|
@@ -200,9 +222,17 @@ async function runCouncil(options, deps = {}) {
|
|
|
200
222
|
}
|
|
201
223
|
|
|
202
224
|
// Merge Stage-2 judging conformance into each seat's row (worst wins).
|
|
203
|
-
|
|
225
|
+
// Seat-keyed (v4.8 PR3 Task 4): an alias-only key collapses a twin bench
|
|
226
|
+
// onto ONE Map entry, so the last-wins judge silently overwrote every
|
|
227
|
+
// twin's row (D7). j.seat can still be null (an orphaned -s2 leg,
|
|
228
|
+
// seats.js:130-146) even when r.seat is a real bound seat, so the lookup
|
|
229
|
+
// falls back to r.model on a miss rather than assuming the two arrays stay
|
|
230
|
+
// symmetric — a naive seatKey on both sides would make that orphan's
|
|
231
|
+
// conformance unreachable instead of merged, a silent total loss.
|
|
232
|
+
const seatKey = (s, alias) => (s ? s.id : alias);
|
|
233
|
+
const byJudge = new Map(s2.judgeResults.map(j => [seatKey(j.seat, j.judge), j]));
|
|
204
234
|
for (const r of s1.reviews) {
|
|
205
|
-
const j = byJudge.get(r.model);
|
|
235
|
+
const j = byJudge.get(r.seat ? r.seat.id : r.model) || byJudge.get(r.model);
|
|
206
236
|
if (j) { r.conformance = asm.worseConformance(r.conformance, j.conformance); }
|
|
207
237
|
}
|
|
208
238
|
|
|
@@ -216,6 +246,11 @@ async function runCouncil(options, deps = {}) {
|
|
|
216
246
|
runId: o.runId, date: o.date, bench: o.models.slice(), chair: chairModel,
|
|
217
247
|
reviews: s1.reviews, judgeResults: s2.judgeResults, chairStats, claudeReview,
|
|
218
248
|
extraRows: [...s1.extraRows, ...s2.extraRows],
|
|
249
|
+
// v4.8 PR4c §3.2: the ONLY production caller of buildTallyInput, so this is
|
|
250
|
+
// the single seam meta.seats enters through. The final input is derived by
|
|
251
|
+
// SPREAD (run-finish.js:36), not rebuilt, so both the provisional and the
|
|
252
|
+
// debated input inherit it from here.
|
|
253
|
+
seats: o.seats,
|
|
219
254
|
});
|
|
220
255
|
const provisionalInput = mkInput(null, o.chair);
|
|
221
256
|
const provisional = tally(provisionalInput);
|
|
@@ -237,54 +272,9 @@ async function runCouncil(options, deps = {}) {
|
|
|
237
272
|
packet, degrade, statsFn, isSignalled: () => signalled,
|
|
238
273
|
});
|
|
239
274
|
if (chairRes.aborted !== null) { return finalize(chairRes.aborted); }
|
|
240
|
-
const { chairLeg, actualChair, chairText, chairConformance, overallVerdict, chairRows, chairAttempts } = chairRes;
|
|
241
275
|
|
|
242
|
-
//
|
|
243
|
-
|
|
244
|
-
leg: chairLeg, model: actualChair, role: 'chair', wasChair: true,
|
|
245
|
-
conformance: chairConformance,
|
|
246
|
-
}) : null;
|
|
247
|
-
// v4.7 D2: a give-up (no chairLeg) with at least one recorded attempt gets
|
|
248
|
-
// an explicit error row so the walk's outcome isn't silently absorbed.
|
|
249
|
-
// Keyed on chairAttempts, NOT chairRows — attempts that die pre-wave (no
|
|
250
|
-
// money spent) record an outcome but yield no row (errata E3).
|
|
251
|
-
const giveUpRow = (!chairLeg && chairAttempts && chairAttempts.length)
|
|
252
|
-
? asm.buildRunStatsEntry({ leg: null, model: o.chair, role: 'chair', wasChair: false })
|
|
253
|
-
: null;
|
|
254
|
-
// Built on the (possibly debated) input so the debate's amended claims, replaced
|
|
255
|
-
// adjudications and rebuttal/revote runStats rows all reach the final record.
|
|
256
|
-
const finalInput = { ...debatedInput, meta: { ...debatedInput.meta, chair: actualChair || o.chair } };
|
|
257
|
-
// Item 8, final-review consolidated wave: was three sequential
|
|
258
|
-
// reassignments (chairStats, then chairRows, then giveUpRow), each
|
|
259
|
-
// rebuilding finalInput.runStats from scratch — collapsed into the one
|
|
260
|
-
// spread that was always the net effect. The `|| []` fallbacks were
|
|
261
|
-
// dead: `runStats` is a real array on every debatedInput
|
|
262
|
-
// (asm.buildTallyInput always returns one via .map()), never undefined.
|
|
263
|
-
finalInput.runStats = [
|
|
264
|
-
...finalInput.runStats,
|
|
265
|
-
...(chairStats ? [chairStats] : []),
|
|
266
|
-
...chairRows,
|
|
267
|
-
...(giveUpRow ? [giveUpRow] : []),
|
|
268
|
-
];
|
|
269
|
-
const record = tally(finalInput);
|
|
270
|
-
if (debateFindings) { decorateRecord(record, debateFindings); }
|
|
271
|
-
if (!o.lenses) {
|
|
272
|
-
// Lens runs never feed cross-run reliability stats (spec §4 / skill rule).
|
|
273
|
-
try { appendRunFn(record); }
|
|
274
|
-
catch (e) { process.stderr.write(`Notice: council ledger append failed: ${e.message}\n`); }
|
|
275
|
-
}
|
|
276
|
-
asm.writeTallyFiles({ runDir: o.runDir, tallyInput: finalInput, record });
|
|
277
|
-
const tallyStage = o.debate ? 'tally-final' : 'tally';
|
|
278
|
-
runState.updateStage(o.runDir, tallyStage, { status: 'complete', completedAt: now() });
|
|
279
|
-
emitStageStarted(o.runDir, o.runId, tallyStage, null, o.follow);
|
|
280
|
-
emitStageTerminal(o.runDir, o.runId, tallyStage, 'complete', null, o.follow);
|
|
281
|
-
// Verdict assembly is the degrade cut-off: anything noted after this line
|
|
282
|
-
// reaches stderr + run.json but not verdict.json (spec §6 rule 1).
|
|
283
|
-
asm.writeVerdictFiles({ runDir: o.runDir, record, overallVerdict, chairText,
|
|
284
|
-
critic: o.critic, deadWaves, degrades: degrade.all() });
|
|
285
|
-
runState.updateStage(o.runDir, 'verdict', { status: 'complete', completedAt: now() });
|
|
286
|
-
emitStageStarted(o.runDir, o.runId, 'verdict', null, o.follow);
|
|
287
|
-
emitStageTerminal(o.runDir, o.runId, 'verdict', 'complete', null, o.follow);
|
|
276
|
+
// Final tally + ledger + artifacts live in ./run-finish (v4.8 PR0 size-gate split).
|
|
277
|
+
finishRun({ o, chairRes, debatedInput, debateFindings, appendRunFn, degrade, deadWaves, now });
|
|
288
278
|
|
|
289
279
|
return finalize(degraded.value ? 2 : 0);
|
|
290
280
|
} catch (err) {
|