amicus 4.8.1 → 4.9.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 +316 -0
- package/README.md +8 -3
- package/docs/ROADMAP.md +101 -10
- package/docs/configuration.md +54 -4
- package/docs/council.md +102 -14
- package/docs/troubleshooting.md +9 -2
- package/docs/usage.md +128 -12
- package/electron/ipc-setup.js +41 -18
- package/electron/main.js +19 -5
- package/electron/offer-session.js +51 -0
- package/electron/setup-ui.js +46 -39
- package/electron/workspace-ui/live-dead-seats.js +163 -91
- package/electron/workspace-ui/workspace-banners.js +30 -7
- package/electron/workspace-ui/workspace-matrix.js +23 -3
- package/electron/workspace-ui/workspace-seats.js +95 -79
- package/package.json +1 -1
- package/schemas/council-run.schema.json +2 -2
- package/schemas/council-tally.schema.json +17 -1
- package/schemas/council-verdict.schema.json +12 -4
- package/schemas/run.schema.json +6 -1
- package/skills/second-opinion/COUNCIL-DESIGN.md +1 -1
- package/skills/second-opinion/MANUAL-ORCHESTRATION.md +1 -1
- package/skills/second-opinion/MODEL-NOTES.md +88 -9
- package/skills/second-opinion/SEAT-BRIEFS.md +36 -4
- package/skills/second-opinion/SKILL.md +151 -36
- package/src/cli-council-run-bench.js +98 -6
- package/src/cli-handlers-council-run.js +18 -6
- package/src/cli-handlers-council.js +57 -7
- package/src/cli-handlers-doctor.js +1 -1
- package/src/cli.js +3 -1
- package/src/council/anonymize.js +2 -1
- package/src/council/briefings-chair-task.js +161 -0
- package/src/council/briefings-chair.js +33 -8
- package/src/council/briefings-debate.js +79 -13
- package/src/council/briefings-stage2-task.js +236 -0
- package/src/council/briefings-stage2.js +103 -26
- package/src/council/briefings-task.js +167 -0
- package/src/council/briefings.js +41 -4
- package/src/council/chair-fallback.js +95 -0
- package/src/council/debate.js +38 -21
- package/src/council/findings.js +3 -2
- package/src/council/ledger.js +2 -2
- package/src/council/parse-stage2.js +63 -15
- package/src/council/report-cost.js +61 -0
- package/src/council/report-html.js +26 -4
- package/src/council/report-md.js +30 -2
- package/src/council/report.js +40 -37
- package/src/council/run-assemble.js +21 -6
- package/src/council/run-chair.js +44 -95
- package/src/council/run-debate-revote.js +81 -49
- package/src/council/run-debate.js +51 -34
- package/src/council/run-finish.js +5 -3
- package/src/council/run-retry-keys.js +4 -4
- package/src/council/run-retry-launch.js +4 -4
- package/src/council/run-retry-notes.js +72 -15
- package/src/council/run-stage1-launch.js +4 -4
- package/src/council/run-stage1-rows.js +9 -6
- package/src/council/run-stage2.js +81 -47
- package/src/council/run-stages.js +9 -21
- package/src/council/run-stats-entry.js +46 -1
- package/src/council/run.js +28 -13
- package/src/council/seats.js +2 -2
- package/src/council/stage1-bind.js +3 -2
- package/src/council/verdict-seat-loss.js +124 -0
- package/src/council/verdict.js +108 -99
- package/src/headless.js +256 -49
- package/src/mcp-council-bench.js +64 -3
- package/src/mcp-council-run.js +10 -3
- package/src/mcp-server.js +52 -12
- package/src/mcp-tools.js +41 -5
- package/src/observe/council-legs.js +2 -2
- package/src/opencode-client.js +19 -1
- package/src/pack/pack-forward.js +15 -12
- package/src/pack/pack-resolve.js +1 -1
- package/src/prompt-builder.js +17 -1
- package/src/sidecar/fanout-leg-fallback.js +2 -1
- package/src/sidecar/fanout-leg.js +26 -0
- package/src/sidecar/fanout.js +1 -1
- package/src/sidecar/list-council.js +178 -0
- package/src/sidecar/list-limit.js +3 -1
- package/src/sidecar/list-search.js +2 -1
- package/src/sidecar/models-render.js +71 -0
- package/src/sidecar/models.js +19 -45
- package/src/sidecar/read.js +34 -10
- package/src/sidecar/reopen-spend.js +2 -1
- package/src/sidecar/setup.js +13 -4
- package/src/sidecar/start.js +2 -1
- package/src/template/render.js +16 -7
- package/src/utils/alias-audit.js +10 -3
- package/src/utils/alias-shadow-writer.js +220 -0
- package/src/utils/alias-shadow.js +294 -0
- package/src/utils/config.js +1 -1
- package/src/utils/curated-models.js +19 -9
- package/src/utils/degrade.js +12 -5
- package/src/utils/doctor-alias-check.js +2 -5
- package/src/utils/engine-log-parse.js +289 -0
- package/src/utils/engine-log-tail.js +114 -0
- package/src/utils/engine-log.js +250 -0
- package/src/utils/engine-skew-records.js +146 -0
- package/src/utils/engine-skew.js +300 -0
- package/src/utils/gateway-router.js +21 -3
- package/src/utils/model-canonicalization.js +55 -6
- package/src/utils/model-catalog.js +27 -9
- package/src/utils/model-fetcher.js +69 -16
- package/src/utils/model-shortlist.js +5 -2
- package/src/utils/provider-default-picker.js +6 -3
- package/src/utils/quick-picks.js +45 -7
- package/src/utils/result-schema.js +17 -1
- package/src/utils/text-sanitize.js +81 -0
- package/src/utils/ttft.js +57 -0
- package/src/utils/untrusted-fence.js +111 -1
- package/src/workspace/fold-format.js +28 -7
- package/src/workspace/live-normalize.js +2 -1
- package/src/workspace/matrix-model.js +6 -2
- package/src/workspace/run-detail.js +33 -7
- package/src/workspace/seat-space.js +10 -6
|
@@ -36,23 +36,31 @@ function waveStillDeadNote(w, unit) {
|
|
|
36
36
|
why: `${w.reason || 'no reason recorded'}; the once-only retry wave also produced no legs`,
|
|
37
37
|
effect: 'Those seats are NOT in this council. The run continues with the bench that did '
|
|
38
38
|
+ 'launch and will exit degraded (2)',
|
|
39
|
-
// `seat`
|
|
40
|
-
// degrade-channels.test.js's exact toEqual on a real dead wave.
|
|
41
|
-
// ALIAS because verdict.js
|
|
42
|
-
// and because it is the same key the dead-leg shape uses — one vocabulary
|
|
43
|
-
// for one field. ⚠️ It
|
|
44
|
-
// live-dead-seats.js
|
|
45
|
-
//
|
|
46
|
-
//
|
|
39
|
+
// `seat`/`seatId` ride ONLY on the partial shape: adding either unconditionally breaks
|
|
40
|
+
// degrade-channels.test.js's exact toEqual on a real dead wave. `seat` stays the
|
|
41
|
+
// ALIAS because verdict-seat-loss.js :: deriveSeatLoss compares data.seat against o.critic,
|
|
42
|
+
// an alias, and because it is the same key the dead-leg shape uses — one vocabulary
|
|
43
|
+
// for one field. ⚠️ It IS read by the Workspace as of v4.9 W9: all three consumers
|
|
44
|
+
// (live-dead-seats.js :: deadSeats, workspace-seats.js :: retriedSeats,
|
|
45
|
+
// verdict-seat-loss.js :: deriveSeatLoss) now admit `seat-unbound` — GATED on the retry-family
|
|
46
|
+
// fields below, because orphan-leg and re-vote notes share this channel and are not
|
|
47
|
+
// seat losses. Cited by SYMBOL, not line: these three references rotted twice during
|
|
48
|
+
// v4.8 PR5c alone.
|
|
47
49
|
data: { waveId: w.waveId, models: w.models, reason: w.reason, retryWaveId: unit.waveId,
|
|
48
50
|
// v4.8 PR5c: seat identity, index-parallel with `models`, on the dead-wave arm only
|
|
49
|
-
// (the partial arm names ONE seat
|
|
50
|
-
//
|
|
51
|
+
// (the partial arm names ONE seat, so it carries the SCALAR `seatId` below instead —
|
|
52
|
+
// the reason given here for emitting nothing at all, that `seat-unbound` had no
|
|
53
|
+
// consumer, stopped being true in v4.9 W9, which gave it three).
|
|
51
54
|
// ⚠️ An unidentified slot emits `null`, NEVER the alias. Collapsing it onto the alias
|
|
52
55
|
// makes it indistinguishable from a second reference to that alias, and no consumer
|
|
53
56
|
// can recover the difference — deadSeats has no per-alias seat count. That collapse
|
|
54
57
|
// is what made two distinct dead twins render as a single row.
|
|
55
|
-
|
|
58
|
+
// v4.9 W9 P1: the fifth arm's seat identity. A SCALAR `seatId`, matching the two leg
|
|
59
|
+
// arms' vocabulary — this arm names exactly ONE seat, so the dead-wave arm's parallel
|
|
60
|
+
// ARRAY would be a second spelling of the same fact. Same null discipline as that
|
|
61
|
+
// array: an unidentified slot emits `null`, never the alias.
|
|
62
|
+
...(partial ? { seat: (w.models || [])[0],
|
|
63
|
+
seatId: ((w.seats || [])[0] && (w.seats || [])[0].id) || null } : {
|
|
56
64
|
seats: (w.models || []).map((m, i) => {
|
|
57
65
|
const so = (w.seats || [])[i];
|
|
58
66
|
return so ? so.id : null;
|
|
@@ -60,13 +68,61 @@ function waveStillDeadNote(w, unit) {
|
|
|
60
68
|
}) } };
|
|
61
69
|
}
|
|
62
70
|
|
|
71
|
+
/**
|
|
72
|
+
* The retry pass never ATTEMPTED this wave (run-retry.js's two `skipped` arms: an unmappable
|
|
73
|
+
* or zero-model unit, or `ctx.overBudget()`). Lifted out of run-stages.js's emit loop in the
|
|
74
|
+
* v4.9 W9 fix round so it sits beside `waveStillDeadNote`, whose partial arm it mirrors: two
|
|
75
|
+
* spellings of ONE record shape in two files is what let them drift apart in the first place.
|
|
76
|
+
*
|
|
77
|
+
* ⚠️ Carries NO `retryWaveId`, and must not: no retry wave was ever launched, so naming one
|
|
78
|
+
* would be a false statement about spend, and the two Workspace renderers read exactly that
|
|
79
|
+
* field to decide the 'retried once' phrasing.
|
|
80
|
+
*
|
|
81
|
+
* v4.9 W9 fix round 1 (council A1/C1). The partial arm previously carried `{waveId, models,
|
|
82
|
+
* reason, seat}` and no retry-family field at all, so all three W9 consumers
|
|
83
|
+
* (`live-dead-seats.js :: isSeatLoss`, `workspace-seats.js :: retriedSeats`,
|
|
84
|
+
* `verdict-seat-loss.js :: deriveSeatLoss`) dropped a genuinely dead seat — residual R-W9a,
|
|
85
|
+
* pinned known-wrong and escalated. It now emits the two facts it has ALREADY:
|
|
86
|
+
* `seatId` — from the record's own `seats[0]` (stage1-bind.js :: missingSeatDeadWave carries
|
|
87
|
+
* it), same null discipline as the arms above: an unidentified slot emits `null`,
|
|
88
|
+
* NEVER the alias.
|
|
89
|
+
* `firstFailure` — the canonical `run-retry-group.js :: recordFailure` shape for a partial
|
|
90
|
+
* wave (`class: 'missing'`, the record's own waveId/reason). It restates this
|
|
91
|
+
* record's first-pass loss and claims nothing about a retry, which is what opens
|
|
92
|
+
* the consumers' retry-family gate WITHOUT loosening it: orphan-leg, re-vote and
|
|
93
|
+
* Stage-2 judge notes still carry neither field and stay excluded.
|
|
94
|
+
*/
|
|
95
|
+
function skippedWaveNote(d) {
|
|
96
|
+
const partial = !!d.partial;
|
|
97
|
+
const alias = (d.models || [])[0];
|
|
98
|
+
return {
|
|
99
|
+
channel: partial ? 'seat-unbound' : 'dead-wave',
|
|
100
|
+
// A `partial` record is one seat of a wave that DID produce legs, so the plain dead-wave
|
|
101
|
+
// sentence would be false. Everything below `models` rides on that shape ONLY: adding any
|
|
102
|
+
// of it unconditionally breaks an exact toEqual on a real dead wave.
|
|
103
|
+
what: partial
|
|
104
|
+
? `seat ${alias} did not review`
|
|
105
|
+
: `Stage-1 wave ${d.waveId} (${d.models.join(', ') || 'no models'}) produced NO legs`,
|
|
106
|
+
why: d.reason,
|
|
107
|
+
effect: 'Those seats are NOT in this council. The run continues with the bench that did '
|
|
108
|
+
+ 'launch and will exit degraded (2)',
|
|
109
|
+
data: { waveId: d.waveId, models: d.models, reason: d.reason,
|
|
110
|
+
...(partial ? { seat: alias,
|
|
111
|
+
seatId: ((d.seats || [])[0] && (d.seats || [])[0].id) || null,
|
|
112
|
+
firstFailure: { seat: alias, class: 'missing', waveId: d.waveId, reason: d.reason },
|
|
113
|
+
} : {}) },
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
63
117
|
/**
|
|
64
118
|
* Leg-origin, retry wave died wholesale (bench-batch case).
|
|
65
119
|
*
|
|
66
120
|
* v4.8 PR5c: `seatId` is the caller's Stage-1 leg->seat binding, or null when the leg was
|
|
67
|
-
* never bound. It is a SEPARATE key from `seat`, which stays the ALIAS —
|
|
68
|
-
*
|
|
69
|
-
*
|
|
121
|
+
* never bound. It is a SEPARATE key from `seat`, which stays the ALIAS —
|
|
122
|
+
* `verdict-seat-loss.js :: deriveSeatLoss`'s `criticLeg` lookup compares `data.seat` against
|
|
123
|
+
* `o.critic`, an alias, so re-pointing it breaks critic-loss detection. Cited by SYMBOL, not
|
|
124
|
+
* line: the old `verdict.js:72` had already slid one line off that comparison, and the
|
|
125
|
+
* function has since left verdict.js entirely. Add a key; never repurpose that one.
|
|
70
126
|
*/
|
|
71
127
|
function srcLegStillDeadNote(leg, unit, counts, seatId = null) {
|
|
72
128
|
const seat = leg.modelInput || leg.model;
|
|
@@ -123,4 +179,5 @@ function missingLegStillDeadNote(seat, ff, unit, counts) {
|
|
|
123
179
|
data: { seat, status: null, reason: null, firstFailure: ff, retryWaveId: unit.waveId } };
|
|
124
180
|
}
|
|
125
181
|
|
|
126
|
-
module.exports = { waveStillDeadNote,
|
|
182
|
+
module.exports = { waveStillDeadNote, skippedWaveNote, srcLegStillDeadNote,
|
|
183
|
+
retryLegStillDeadNote, missingLegStillDeadNote };
|
|
@@ -14,7 +14,7 @@ const { buildSeats } = require('./seats');
|
|
|
14
14
|
/** Launch all Stage-1 legs (wave + critic/lens solos), collect run docs. */
|
|
15
15
|
async function launchStage1(ctx) {
|
|
16
16
|
const { o, launchers } = ctx;
|
|
17
|
-
// Seat identity for THIS launch. run.js:
|
|
17
|
+
// Seat identity for THIS launch. run.js:142 sets o.seats from asm.preflightSeats;
|
|
18
18
|
// buildSeats is pure and total, so a direct require() caller or a legacy run dir
|
|
19
19
|
// reconstructs the same table rather than binding nothing (spec §4.3).
|
|
20
20
|
const seats = Array.isArray(o.seats) && o.seats.length > 0
|
|
@@ -47,7 +47,7 @@ async function launchStage1(ctx) {
|
|
|
47
47
|
seated.push({ waveId, models: [m], roster: seats.slice(i, i + 1) });
|
|
48
48
|
launches.push(launchers.launchSolo({
|
|
49
49
|
...common, model: m, waveId, seats: seated[seated.length - 1].roster,
|
|
50
|
-
prompt: briefings.
|
|
50
|
+
prompt: briefings.stage1LensBriefing(o.intent, { lens: o.lenses[i], briefing: o.briefing, date: o.date }),
|
|
51
51
|
}));
|
|
52
52
|
});
|
|
53
53
|
} else {
|
|
@@ -61,7 +61,7 @@ async function launchStage1(ctx) {
|
|
|
61
61
|
roster: seats.filter(s => s.alias !== o.critic) });
|
|
62
62
|
launches.push(launchers.launchWave({
|
|
63
63
|
...common, models: seats1, waveId: `${o.runId}-s1`, seats: seated[seated.length - 1].roster,
|
|
64
|
-
prompt: briefings.
|
|
64
|
+
prompt: briefings.stage1SeatBriefing(o.intent, { briefing: o.briefing, date: o.date }),
|
|
65
65
|
}));
|
|
66
66
|
}
|
|
67
67
|
if (o.critic) {
|
|
@@ -70,7 +70,7 @@ async function launchStage1(ctx) {
|
|
|
70
70
|
roster: seats.filter(s => s.alias === o.critic).slice(0, 1) });
|
|
71
71
|
launches.push(launchers.launchSolo({
|
|
72
72
|
...common, model: o.critic, waveId: `${o.runId}-c1`, seats: seated[seated.length - 1].roster,
|
|
73
|
-
prompt: briefings.
|
|
73
|
+
prompt: briefings.stage1CriticBriefing(o.intent, { briefing: o.briefing, date: o.date }),
|
|
74
74
|
}));
|
|
75
75
|
}
|
|
76
76
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// src/council/run-stage1-rows.js
|
|
2
2
|
'use strict';
|
|
3
3
|
// Superseded-seat rows + primary-error dead-seat rows (v4.7 D2/E4), moved
|
|
4
|
-
// verbatim from run-stages.js:
|
|
4
|
+
// verbatim from run-stages.js:218-267 (v4.8 PR0 size-gate split, zero
|
|
5
5
|
// behavior). The code ran inline in runStage1; it is now a function.
|
|
6
6
|
// roleFor and seatOf are PARAMETERS, not requires — requiring them back from
|
|
7
7
|
// run-stages would recreate the parent-child cycle that file's tail comment
|
|
@@ -10,8 +10,8 @@ const { buildRunStatsEntry } = require('./run-assemble');
|
|
|
10
10
|
// The one keyspace. It lives in ./run-retry-keys (v4.8 Phase 2 T-A1), which IS require-free;
|
|
11
11
|
// run-retry-group.js re-exports it and requires nothing else, so this import's closure
|
|
12
12
|
// terminates at a leaf and cannot re-create the parent-child cycle the header above
|
|
13
|
-
// documents eliminating.
|
|
14
|
-
const { twinAliases, legLossKey } = require('./run-retry-group');
|
|
13
|
+
// documents eliminating. (v4.9 W3, SI-DUP b: `seatKey` joined this destructure — see keyOf.)
|
|
14
|
+
const { seatKey, twinAliases, legLossKey } = require('./run-retry-group');
|
|
15
15
|
// v4.8 T-A6 size-gate split: the SUPERSEDED half of these rows moved to
|
|
16
16
|
// ./run-stage1-superseded, taking `../utils/degrade` and the default stderr sink with it.
|
|
17
17
|
// `degrade` is still a parameter HERE, forwarded unchanged, so a caller that omits it still
|
|
@@ -63,8 +63,11 @@ const { supersededRows } = require('./run-stage1-superseded');
|
|
|
63
63
|
function pushDeadSeatRows({ o, retry, deadLegs0, stillDeadLegs, stillDeadWaves, extraRows,
|
|
64
64
|
roleFor, seatOf, degrade, twins = twinAliases(o.seats) }) {
|
|
65
65
|
// A leg's join key: its bound seat's id, else its alias — the same fallback
|
|
66
|
-
// `run-retry-keys.js :: seatKey` (imported by run-retry.js since v4.8 T-A1
|
|
67
|
-
//
|
|
66
|
+
// `run-retry-keys.js :: seatKey` (imported by run-retry.js since v4.8 T-A1, and by
|
|
67
|
+
// this file since v4.9 W3 for the dead-wave slot join below) uses for a roster slot
|
|
68
|
+
// it could not identify, so both sides of every lookup agree. keyOf itself stays
|
|
69
|
+
// hand-written on purpose: its else branch is `l.modelInput || l.model`, a LEG read
|
|
70
|
+
// with no bare alias in scope — a sibling form, not a re-spelling of that rule.
|
|
68
71
|
const keyOf = (leg) => { const s = seatOf.get(leg); return s ? s.id : (leg.modelInput || leg.model); };
|
|
69
72
|
// v4.8 T2.2 ruling R2, the LEG arms' MINT branch: on an alias the roster repeats, an
|
|
70
73
|
// unbound leg's `keyOf` names BOTH twins, so N dead legs collapsed into ONE row.
|
|
@@ -139,7 +142,7 @@ function pushDeadSeatRows({ o, retry, deadLegs0, stillDeadLegs, stillDeadWaves,
|
|
|
139
142
|
// reconcile, so index-zipping them is safe here and nowhere else.
|
|
140
143
|
(w.models || []).forEach((alias, i) => {
|
|
141
144
|
const s = (w.seats || [])[i] || null;
|
|
142
|
-
const join = s
|
|
145
|
+
const join = seatKey(s, alias);
|
|
143
146
|
// R2's MARK branch. This arm has no leg to mint from, and `(waveId, i)` is
|
|
144
147
|
// measurably NOT unique — missingSeatDeadWave emits several records under one
|
|
145
148
|
// waveId each with i === 0, and run-retry.js re-indexes when it narrows a
|
|
@@ -30,6 +30,63 @@ const { buildRunStatsEntry } = require('./run-assemble');
|
|
|
30
30
|
const { artifactName } = require('./seats');
|
|
31
31
|
const { orphanLegNote, bindPaddedWave } = require('./stage1-bind');
|
|
32
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Bind the -s2 wave's legs to the seats `reviews[]` holds and note the two
|
|
35
|
+
* unbindable shapes (orphan legs, unbound seats) on ctx.degrade. Lifted
|
|
36
|
+
* verbatim out of `runStage2` below (v4.9 W2, the SI-16 function-length
|
|
37
|
+
* split); the caller reads back only `judgeSeatOf` — `s2WaveId` rides along
|
|
38
|
+
* in the return for the seam's shape, nothing downstream consumes it.
|
|
39
|
+
* @param {object} ctx
|
|
40
|
+
* @param {{reviews: Array, judges: Array<string>, s2Legs: Array<object>,
|
|
41
|
+
* runId: string}} args — `s2Legs` arrives pre-guarded
|
|
42
|
+
* (`(wave && wave.legs) || []`): the wave-null guard stays at the call
|
|
43
|
+
* site, beside the leg loop that shares it.
|
|
44
|
+
* @returns {{s2WaveId: string, judgeSeatOf: Map<object, object>}}
|
|
45
|
+
*/
|
|
46
|
+
function bindStage2Seats(ctx, { reviews, judges, s2Legs, runId }) {
|
|
47
|
+
// v4.8 PR3 Task 4: bind the -s2 wave's legs to the SAME seats reviews[] holds,
|
|
48
|
+
// in `reviews` order — judges is built from that same array by the same
|
|
49
|
+
// `.map`, and real fanout stamps `${waveId}-${i+1}` off that same index
|
|
50
|
+
// (sidecar/leg-ids.js), so this holds even after an SL-2 heal (recovered legs
|
|
51
|
+
// are appended, run-stages.js:141, so `reviews` order is no longer seat order
|
|
52
|
+
// — both arrays still derive from the same `reviews`).
|
|
53
|
+
// §3.4's padding pattern now lives in `stage1-bind.js :: bindPaddedWave`
|
|
54
|
+
// (v4.8 SI-27) — why the roster is padded rather than filtered, and why
|
|
55
|
+
// placeholders are tracked by IDENTITY rather than an id-name prefix test,
|
|
56
|
+
// are in that function's docblock. Only this site's TAIL stays here.
|
|
57
|
+
const s2WaveId = `${runId}-s2`;
|
|
58
|
+
const { seatOf: judgeSeatOf, bindRes, placeholders } =
|
|
59
|
+
bindPaddedWave(s2WaveId, reviews.map(r => r.seat), i => judges[i], s2Legs);
|
|
60
|
+
// An orphan leg (a judge DID land, but no roster slot claims it) gets the
|
|
61
|
+
// same shape as Stage 1's orphanLegNote — `data.legId` present.
|
|
62
|
+
for (const leg of bindRes.orphanLegs) { ctx.degrade.note(orphanLegNote(s2WaveId, leg)); }
|
|
63
|
+
// review F(important): mirrors stage1-bind.js:40's suppression rule
|
|
64
|
+
// verbatim. A wave that returned ZERO legs is already announced on a louder
|
|
65
|
+
// channel (thin-cross-review, or this refusal itself) — there is no
|
|
66
|
+
// "missing seat" fact this adds. An orphan leg means a judge DID land for
|
|
67
|
+
// SOME seat we could not name — reporting the roster's other unbound seats
|
|
68
|
+
// as "missing" would double-count that one failure as two, on a channel
|
|
69
|
+
// whose whole contract is "nothing was guessed": the stray leg might BE the
|
|
70
|
+
// seat this loop would otherwise call missing.
|
|
71
|
+
if (s2Legs.length > 0 && bindRes.orphanLegs.length === 0) {
|
|
72
|
+
for (const seat of bindRes.unbound) {
|
|
73
|
+
if (placeholders.has(seat)) { continue; }
|
|
74
|
+
ctx.degrade.note({
|
|
75
|
+
channel: 'seat-unbound',
|
|
76
|
+
what: `leg for seat ${seat.alias} in wave ${s2WaveId} never returned`,
|
|
77
|
+
// R27-5: `reviews.length` IS the padded roster's length — bindPaddedWave
|
|
78
|
+
// maps 1:1 over the source array, and `Array.prototype.map` preserves
|
|
79
|
+
// length. Pinned (`ROSTERLEN`) in run-stages.test.js, because a
|
|
80
|
+
// substitution into unpinned prose is how a true sentence goes quietly false.
|
|
81
|
+
why: `the wave returned fewer judge legs than its roster of ${reviews.length}`,
|
|
82
|
+
effect: 'That seat did not judge; nothing was guessed and nothing was dropped',
|
|
83
|
+
data: { waveId: s2WaveId, seat: seat.alias },
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return { s2WaveId, judgeSeatOf };
|
|
88
|
+
}
|
|
89
|
+
|
|
33
90
|
/**
|
|
34
91
|
* Stage 2: shared anonymized bundle → judge wave in _scratch → parse + repair.
|
|
35
92
|
* @param {object} ctx
|
|
@@ -54,7 +111,14 @@ async function runStage2(ctx, { reviews, labels, globalFindings, extraLabeled =
|
|
|
54
111
|
const labeled = reviews
|
|
55
112
|
.map((r, i) => ({ label: labels.entries[i].label, text: r.text }))
|
|
56
113
|
.concat(extraLabeled);
|
|
57
|
-
|
|
114
|
+
// v4.9 W7: one dispatch, both intents (briefings-stage2.js :: judgeBundleFor —
|
|
115
|
+
// 'task' | absent, fail-closed). `briefing` is the EXTRA argument the task
|
|
116
|
+
// bundle needs and the review bundle ignores: task judges rank "which response
|
|
117
|
+
// best does the work the briefing asked for", which is unanswerable without the
|
|
118
|
+
// ask (spec §5.4). It rides `o.briefing` — the same field the Stage-1
|
|
119
|
+
// dispatchers compose from — so the text is never re-read off disk.
|
|
120
|
+
const bundle = stage2.judgeBundleFor(o.intent,
|
|
121
|
+
{ reviews: labeled, findings: globalFindings, date: o.date, briefing: o.briefing });
|
|
58
122
|
fs.writeFileSync(path.join(o.runDir, 'bundle-stage2.md'), bundle, { mode: 0o600 });
|
|
59
123
|
|
|
60
124
|
// ROSTER, not bundle: derived ONLY from legs that actually ran, so a file-sourced
|
|
@@ -76,58 +140,23 @@ async function runStage2(ctx, { reviews, labels, globalFindings, extraLabeled =
|
|
|
76
140
|
ctx.addWave(wave);
|
|
77
141
|
if (isAbortExit(exitCode)) { return { aborted: exitCode, judgeResults: [], extraRows: [] }; }
|
|
78
142
|
|
|
79
|
-
// v4.8 PR3 Task 4
|
|
80
|
-
// in `reviews` order — judges is built from that same array by the same
|
|
81
|
-
// `.map`, and real fanout stamps `${waveId}-${i+1}` off that same index
|
|
82
|
-
// (sidecar/leg-ids.js), so this holds even after an SL-2 heal (recovered legs
|
|
83
|
-
// are appended, run-stages.js:153, so `reviews` order is no longer seat order
|
|
84
|
-
// — both arrays still derive from the same `reviews`).
|
|
85
|
-
// §3.4's padding pattern now lives in `stage1-bind.js :: bindPaddedWave`
|
|
86
|
-
// (v4.8 SI-27) — why the roster is padded rather than filtered, and why
|
|
87
|
-
// placeholders are tracked by IDENTITY rather than an id-name prefix test,
|
|
88
|
-
// are in that function's docblock. Only this site's TAIL stays here.
|
|
89
|
-
const s2WaveId = `${o.runId}-s2`;
|
|
143
|
+
// Seat binding (v4.8 PR3 Task 4) lives in bindStage2Seats above (v4.9 W2).
|
|
90
144
|
// review F(critical): `wave` is legitimately null — a budget/args refusal
|
|
91
145
|
// (run-budget.js failPre) returns `{wave: null, exitCode: 1}`, which
|
|
92
146
|
// isAbortExit does NOT catch (only 130/143), so execution reaches here.
|
|
93
147
|
// Guarded exactly like the leg loop below (`(wave && wave.legs) || []`) —
|
|
94
148
|
// this is the only OTHER dereference of wave.legs in the file.
|
|
95
149
|
const s2Legs = (wave && wave.legs) || [];
|
|
96
|
-
const {
|
|
97
|
-
bindPaddedWave(s2WaveId, reviews.map(r => r.seat), i => judges[i], s2Legs);
|
|
98
|
-
// An orphan leg (a judge DID land, but no roster slot claims it) gets the
|
|
99
|
-
// same shape as Stage 1's orphanLegNote — `data.legId` present.
|
|
100
|
-
for (const leg of bindRes.orphanLegs) { ctx.degrade.note(orphanLegNote(s2WaveId, leg)); }
|
|
101
|
-
// review F(important): mirrors stage1-bind.js:40's suppression rule
|
|
102
|
-
// verbatim. A wave that returned ZERO legs is already announced on a louder
|
|
103
|
-
// channel (thin-cross-review, or this refusal itself) — there is no
|
|
104
|
-
// "missing seat" fact this adds. An orphan leg means a judge DID land for
|
|
105
|
-
// SOME seat we could not name — reporting the roster's other unbound seats
|
|
106
|
-
// as "missing" would double-count that one failure as two, on a channel
|
|
107
|
-
// whose whole contract is "nothing was guessed": the stray leg might BE the
|
|
108
|
-
// seat this loop would otherwise call missing.
|
|
109
|
-
if (s2Legs.length > 0 && bindRes.orphanLegs.length === 0) {
|
|
110
|
-
for (const seat of bindRes.unbound) {
|
|
111
|
-
if (placeholders.has(seat)) { continue; }
|
|
112
|
-
ctx.degrade.note({
|
|
113
|
-
channel: 'seat-unbound',
|
|
114
|
-
what: `leg for seat ${seat.alias} in wave ${s2WaveId} never returned`,
|
|
115
|
-
// R27-5: `reviews.length` IS the padded roster's length — bindPaddedWave
|
|
116
|
-
// maps 1:1 over the source array, and `Array.prototype.map` preserves
|
|
117
|
-
// length. Pinned (`ROSTERLEN`) in run-stages.test.js, because a
|
|
118
|
-
// substitution into unpinned prose is how a true sentence goes quietly false.
|
|
119
|
-
why: `the wave returned fewer judge legs than its roster of ${reviews.length}`,
|
|
120
|
-
effect: 'That seat did not judge; nothing was guessed and nothing was dropped',
|
|
121
|
-
data: { waveId: s2WaveId, seat: seat.alias },
|
|
122
|
-
});
|
|
123
|
-
}
|
|
124
|
-
}
|
|
150
|
+
const { judgeSeatOf } = bindStage2Seats(ctx, { reviews, judges, s2Legs, runId: o.runId });
|
|
125
151
|
|
|
126
152
|
const judgeResults = [];
|
|
127
153
|
// v4.7 D2: every judge-repair launch is a billed leg of its own, distinct from
|
|
128
154
|
// the judge's original Stage-2 wave leg it is trying to fix — it gets its own
|
|
129
155
|
// row so its cost is never folded into, or lost from, the judge's row (mirrors
|
|
130
|
-
// runStage1's -p<N> extraRows,
|
|
156
|
+
// runStage1's -p<N> extraRows, run-stages.js :: runStage1). ⚠️ Cited by LINE until the v4.9 W9
|
|
157
|
+
// fix round re-opened it: run-stages.js@5830ece3:117-120 was the skipped-wave note's `what`
|
|
158
|
+
// arm, not extraRows at all, and it has since moved to run-retry-notes.js :: skippedWaveNote.
|
|
159
|
+
// Anchored by symbol now, per the anti-rot rule.
|
|
131
160
|
const extraRows = [];
|
|
132
161
|
let repairSeq = 0;
|
|
133
162
|
for (const leg of (wave && wave.legs) || []) {
|
|
@@ -159,7 +188,12 @@ async function runStage2(ctx, { reviews, labels, globalFindings, extraLabeled =
|
|
|
159
188
|
runState.appendStageWave(o.runDir, 'stage2', waveId);
|
|
160
189
|
const solo = await ctx.launchers.launchSolo({
|
|
161
190
|
model: judge,
|
|
162
|
-
|
|
191
|
+
// v4.9 W7 fix round (F1): the SAME intent channel the bundle dispatch
|
|
192
|
+
// above rides. A judge briefed on the task contract must be repaired
|
|
193
|
+
// against the task contract — a repair solo is a fresh session, so the
|
|
194
|
+
// contract embedded here is the only output shape it ever sees.
|
|
195
|
+
prompt: stage2.judgeRepairPromptFor(o.intent,
|
|
196
|
+
{ errors: parsed.errors, judgement: judging }),
|
|
163
197
|
project: ctx.scratchDir, waveId, timeout: o.timeout,
|
|
164
198
|
gateway: o.gateway, noValidateModel: o.noValidateModel, noCostGate: o.noCostGate,
|
|
165
199
|
councilRunId: o.runId, councilName: o.councilName,
|
|
@@ -172,13 +206,13 @@ async function runStage2(ctx, { reviews, labels, globalFindings, extraLabeled =
|
|
|
172
206
|
// is returned only for shape consistency, never read past this point.
|
|
173
207
|
return { aborted: solo.exitCode, judgeResults, extraRows };
|
|
174
208
|
}
|
|
175
|
-
// Every -q<N> launch gets a row — INCLUDING a repair that failed: the
|
|
176
|
-
// error status rides naturally off solo.leg (null/'error'-status leg ⇒
|
|
177
|
-
// buildRunStatsEntry's own never-invent defaults), no special-casing needed.
|
|
178
|
-
extraRows.push(buildRunStatsEntry({ leg: solo.leg, model: judge, role: 'repair', wasChair: false }));
|
|
179
209
|
const out = (solo.leg && solo.leg.summary) || '';
|
|
180
210
|
if (out.trim()) { judging = out; }
|
|
181
211
|
parsed = parseJudgeOutput(out, parseCtx);
|
|
212
|
+
// Every -q<N> launch gets a row — INCLUDING a failed repair (null/'error' leg ⇒ never-invent
|
|
213
|
+
// defaults); pushed AFTER the re-parse to stamp the repair LEG's own measured outcome (PR 199 D1, v4.9 V18 refined).
|
|
214
|
+
extraRows.push(buildRunStatsEntry({ leg: solo.leg, model: judge, role: 'repair',
|
|
215
|
+
wasChair: false, conformance: parsed.ok ? 'clean' : 'unstructured' }));
|
|
182
216
|
if (parsed.ok) { conformance = 'repaired'; }
|
|
183
217
|
}
|
|
184
218
|
if (!parsed.ok) {
|
|
@@ -27,6 +27,7 @@ const { launchStage1 } = require('./run-stage1-launch');
|
|
|
27
27
|
const { buildRunStatsEntry } = require('./run-assemble');
|
|
28
28
|
const { pushDeadSeatRows } = require('./run-stage1-rows');
|
|
29
29
|
const { bindStage1Waves, orphanLegNote, missingSeatDeadWave } = require('./stage1-bind');
|
|
30
|
+
const { skippedWaveNote } = require('./run-retry-notes');
|
|
30
31
|
// slug lives in ./seats (v4.8 PR1) so that module can stay require-free;
|
|
31
32
|
// re-exported below — run-stages.test.js imports it from here.
|
|
32
33
|
const { slug } = require('./seats');
|
|
@@ -108,22 +109,9 @@ async function runStage1(ctx) {
|
|
|
108
109
|
}).filter(w => w.models.length > 0) };
|
|
109
110
|
}
|
|
110
111
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
// adding it unconditionally breaks an exact toEqual on a real dead wave.
|
|
115
|
-
ctx.degrade.note({
|
|
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`,
|
|
120
|
-
why: d.reason,
|
|
121
|
-
effect: 'Those seats are NOT in this council. The run continues with the bench that did '
|
|
122
|
-
+ 'launch and will exit degraded (2)',
|
|
123
|
-
data: { waveId: d.waveId, models: d.models, reason: d.reason,
|
|
124
|
-
...(d.partial ? { seat: (d.models || [])[0] } : {}) },
|
|
125
|
-
});
|
|
126
|
-
}
|
|
112
|
+
// Same shape as `stillDeadNotes` below: the builder lives in run-retry-notes.js (v4.9 W9
|
|
113
|
+
// fix round), beside `waveStillDeadNote` whose partial arm it mirrors; this file EMITS.
|
|
114
|
+
for (const d of retry.skippedDeadWaves) { ctx.degrade.note(skippedWaveNote(d)); }
|
|
127
115
|
for (const leg of retry.skippedDeadLegs) {
|
|
128
116
|
ctx.degrade.note({
|
|
129
117
|
channel: 'dead-leg',
|
|
@@ -197,7 +185,7 @@ async function runStage1(ctx) {
|
|
|
197
185
|
runState.appendStageWave(o.runDir, 'stage1', waveId);
|
|
198
186
|
const solo = await ctx.launchers.launchSolo({
|
|
199
187
|
model: m.modelInput,
|
|
200
|
-
prompt: briefings.
|
|
188
|
+
prompt: briefings.stage1RepairPrompt(o.intent, { errors: res.errors, review: repairing }),
|
|
201
189
|
project: o.runDir, waveId, timeout: o.timeout,
|
|
202
190
|
gateway: o.gateway, noValidateModel: o.noValidateModel, noCostGate: o.noCostGate,
|
|
203
191
|
councilRunId: o.runId, councilName: o.councilName,
|
|
@@ -216,13 +204,13 @@ async function runStage1(ctx) {
|
|
|
216
204
|
return { aborted: solo.exitCode, reviews, deadLegs: stillDeadLegs, deadWaves: stillDeadWaves,
|
|
217
205
|
degraded: false, extraRows };
|
|
218
206
|
}
|
|
219
|
-
// Every -p<N> launch gets a row — INCLUDING a repair that failed: the
|
|
220
|
-
// error status rides naturally off solo.leg (null/'error'-status leg ⇒
|
|
221
|
-
// buildRunStatsEntry's own never-invent defaults), no special-casing needed.
|
|
222
|
-
extraRows.push(buildRunStatsEntry({ leg: solo.leg, model: m.modelInput, role: 'repair', wasChair: false }));
|
|
223
207
|
const repaired = (solo.leg && solo.leg.summary) || '';
|
|
224
208
|
if (repaired.trim()) { repairing = repaired; }
|
|
225
209
|
res = validateFindings(repaired);
|
|
210
|
+
// Every -p<N> launch gets a row — INCLUDING a failed repair (null/'error' leg ⇒ never-invent
|
|
211
|
+
// defaults); pushed AFTER re-validation to stamp the repair LEG's own measured outcome (PR 199 D1, v4.9 V18 refined).
|
|
212
|
+
extraRows.push(buildRunStatsEntry({ leg: solo.leg, model: m.modelInput, role: 'repair',
|
|
213
|
+
wasChair: false, conformance: res.ok ? 'clean' : 'unstructured' }));
|
|
226
214
|
if (res.ok) { conformance = 'repaired'; }
|
|
227
215
|
}
|
|
228
216
|
if (!res.ok) { conformance = 'unstructured'; }
|
|
@@ -40,9 +40,37 @@
|
|
|
40
40
|
* `seat` (v4.8 PR4c §3.1) is the seat OBJECT — {id, alias, role, lens, position}
|
|
41
41
|
* or null — never an id string. Callers pass `r.seat` / the dead-seat loop's own
|
|
42
42
|
* `seat` verbatim, so the object IS the contract instead of a prose one.
|
|
43
|
+
*
|
|
44
|
+
* `summary` (v4.9 W11 / PR1F-2) is the one field the folded
|
|
45
|
+
* `run-debate-revote.js :: legRow` needed that this entry did not emit. It is
|
|
46
|
+
* EXPLICIT-ONLY and emit-when-set: it is deliberately NOT sourced from
|
|
47
|
+
* `leg.summary`, because that is the model's raw review prose and no runStats
|
|
48
|
+
* row has ever carried it — a leg-sourced default would push review text into
|
|
49
|
+
* tally-input.json for every existing caller. It stops there: `tally.js ::
|
|
50
|
+
* tally`'s re-projection allowlist does not name `summary`, so it reaches
|
|
51
|
+
* neither tally.json/verdict.json nor the ledger (MEASURED). Passing nothing
|
|
52
|
+
* leaves every row byte-for-byte unchanged (pins G4a/G4b/G4c,
|
|
53
|
+
* tests/council/runstats-byte-order.test.js).
|
|
54
|
+
*
|
|
55
|
+
* `ttftMs` (v4.9 W13 Task A) is time-to-first-token for this row's leg, read off
|
|
56
|
+
* the leg document and emitted only when it is a NON-NEGATIVE INTEGER — the
|
|
57
|
+
* shape council-tally.schema.json declares, and (PR #207 round 3, B3) a stricter
|
|
58
|
+
* test than the bare type check this used to spell. PROBE ONLY — nothing
|
|
59
|
+
* derives a backstop, threshold, or routing decision from it yet (ruling R12:
|
|
60
|
+
* probe first, derive later). Absent means no substantive tick was observed —
|
|
61
|
+
* or that the only reading taken was not an honest measurement — which is
|
|
62
|
+
* neither `0` (a real measurement) nor `null`.
|
|
43
63
|
*/
|
|
44
64
|
function buildRunStatsEntry({ leg, model, role, wasChair, conformance, findingsUnverified,
|
|
45
|
-
repairRefused, seat }) {
|
|
65
|
+
repairRefused, seat, summary }) {
|
|
66
|
+
// v4.9 W13 Task A: the TTFT probe's last hop, read off the LEG document the
|
|
67
|
+
// same way `waveId` and `resolvedModel` below are. DEVIATION from the plan's
|
|
68
|
+
// literal "thread from the leg at the callers": ten call sites across seven
|
|
69
|
+
// council modules hold a leg, and threading a parameter through each would
|
|
70
|
+
// (a) reach outside this task's file scope and (b) make a forgotten caller a
|
|
71
|
+
// SILENT gap. Sourcing it here covers every caller that holds a leg by
|
|
72
|
+
// construction, and a dead seat (`leg: null`) still carries no key.
|
|
73
|
+
const ttftMs = leg ? leg.ttftMs : undefined;
|
|
46
74
|
return {
|
|
47
75
|
model: model !== undefined ? model : (leg ? leg.model : null),
|
|
48
76
|
role,
|
|
@@ -50,6 +78,7 @@ function buildRunStatsEntry({ leg, model, role, wasChair, conformance, findingsU
|
|
|
50
78
|
conformance: conformance || 'clean',
|
|
51
79
|
...(findingsUnverified ? { findingsUnverified: true } : {}),
|
|
52
80
|
...(repairRefused ? { repairRefused } : {}),
|
|
81
|
+
...(summary ? { summary } : {}),
|
|
53
82
|
...(leg && leg.waveId ? { waveId: leg.waveId } : {}),
|
|
54
83
|
...(leg && leg.model ? { resolvedModel: leg.model } : {}),
|
|
55
84
|
// v4.8 PR4c §3.1 / R4c-9: emit-when-DIFFERENT, compared against the seat's
|
|
@@ -64,6 +93,22 @@ function buildRunStatsEntry({ leg, model, role, wasChair, conformance, findingsU
|
|
|
64
93
|
...(seat && seat.id !== seat.alias ? { seat: seat.id } : {}),
|
|
65
94
|
status: leg ? leg.status : 'error',
|
|
66
95
|
durationMs: leg && typeof leg.durationMs === 'number' ? leg.durationMs : null,
|
|
96
|
+
// Emit-when-set, NOT `durationMs`'s null-coercion one line above: this row
|
|
97
|
+
// is the C2 derivation's future input, and a null there would be read as a
|
|
98
|
+
// measurement. Absent means "never observed" and must stay absent.
|
|
99
|
+
//
|
|
100
|
+
// PR #207 round 3 (B3): emit-when-VALID too. The shared predicate is
|
|
101
|
+
// `src/utils/ttft.js :: isMeasuredTtft`, and it is spelled out by HAND here
|
|
102
|
+
// for one reason only — the module invariant at the top of this file forbids
|
|
103
|
+
// importing anything, and the pin that enforces it fires on the character
|
|
104
|
+
// sequence anywhere in the file, comments included. The structural pins that
|
|
105
|
+
// keep this copy in step with the shared one are in this module's own test
|
|
106
|
+
// file. Do not "simplify" it back to a bare type test: that admits NaN and
|
|
107
|
+
// ±Infinity (both of which serialize to `null`), negatives from a backward
|
|
108
|
+
// wall-clock jump at the probe, and fractions from a hand-edited artifact —
|
|
109
|
+
// every one of them forbidden by council-tally.schema.json's
|
|
110
|
+
// `integer, minimum 0`.
|
|
111
|
+
...(Number.isInteger(ttftMs) && ttftMs >= 0 ? { ttftMs } : {}),
|
|
67
112
|
usage: (leg && leg.usage) || null,
|
|
68
113
|
};
|
|
69
114
|
}
|
package/src/council/run.js
CHANGED
|
@@ -119,6 +119,15 @@ async function runCouncil(options, deps = {}) {
|
|
|
119
119
|
const ctx = { o, launchers, addWave, overBudget, degrade, scratchDir: path.join(o.runDir, '_scratch') };
|
|
120
120
|
|
|
121
121
|
try {
|
|
122
|
+
// v4.9 W5.3: o.intent is 'task' or ABSENT. ⚠️ PR #200 A3 (round-3 ruling): 'review' — the default spelled out loud, which both transports already strip (cli-handlers-council-run.js, mcp-council-run.js) — NORMALIZES to absent here rather than being refused, because runCouncil is a public door too and one input must not mean two things at two doors. `delete`, never `= undefined` (a present-but-undefined key still changes JSON output paths — the T10 lesson). Only a genuinely unknown value is BAD_ARGS. Pinned: tests/council/run-intent.test.js. ⚠️ Round-4 C2 (that this normalization runs AFTER the seed and so materializes intent:'review' on run.json) is REFUTED BY MEASUREMENT, not by argument: the ordering is real — `runState.initCouncilRun` runs ~50 lines above — but that seed literal (`run-state.js :: initCouncilRun`) has no `intent` key to copy, and BOTH run.json writers of it (the checkpoint below, and `cli-handlers-council-run.js`'s) are `=== 'task'` spreads that 'review' cannot pass. Mutant SEEDCOPY (a `...(o.intent ? {intent:o.intent} : {})` added to that seed) reddens exactly the pin, so no reordering is needed here.
|
|
123
|
+
if (o.intent === 'review') { delete o.intent; } else if (o.intent !== undefined && o.intent !== 'task') {
|
|
124
|
+
return finalize(1, { code: 'BAD_ARGS',
|
|
125
|
+
message: `Error: intent must be 'task' or 'review' (omitted means review); got '${o.intent}'` });
|
|
126
|
+
}
|
|
127
|
+
if (o.intent === 'task' && o.claudeReviewFile) { // V12: a file review is REVIEW machinery
|
|
128
|
+
return finalize(1, { code: 'BAD_ARGS', message:
|
|
129
|
+
'--claude-review enters a REVIEW as review N+1 and has no task-mode meaning; drop it for a task run' });
|
|
130
|
+
}
|
|
122
131
|
// v4.1 §4.4: Claude-in-council is a FILE input — validated after initRun (so the
|
|
123
132
|
// error doc lands in a run dir that exists) and before any launch (zero spend).
|
|
124
133
|
const pre = asm.preflightClaudeReview(o);
|
|
@@ -132,11 +141,12 @@ async function runCouncil(options, deps = {}) {
|
|
|
132
141
|
if (seatPre.error) { return finalize(1, seatPre.error); }
|
|
133
142
|
o.seats = seatPre.seats;
|
|
134
143
|
o.criticSeat = seatPre.criticSeat;
|
|
135
|
-
runState.checkpoint(o.runDir, { seats: o.seats, criticSeat: o.criticSeat
|
|
144
|
+
runState.checkpoint(o.runDir, { seats: o.seats, criticSeat: o.criticSeat,
|
|
145
|
+
...(o.intent === 'task' ? { intent: 'task' } : {}) }); // v4.9 W5.3: emit-when-'task', never 'review'
|
|
136
146
|
|
|
137
147
|
// Composed Stage-1 seat briefing persisted for auditability (spec §4 layout).
|
|
138
148
|
fs.writeFileSync(path.join(o.runDir, 'briefing-stage1.md'),
|
|
139
|
-
briefings.
|
|
149
|
+
briefings.stage1SeatBriefing(o.intent, { briefing: o.briefing, date: o.date }), { mode: 0o600 });
|
|
140
150
|
|
|
141
151
|
// ---- Stage 1: independent reviews ----
|
|
142
152
|
// Lens mode launches one solo per seat instead of a `-s1` seat wave, so it
|
|
@@ -229,7 +239,7 @@ async function runCouncil(options, deps = {}) {
|
|
|
229
239
|
// falls back to r.model on a miss rather than assuming the two arrays stay
|
|
230
240
|
// symmetric — a naive seatKey on both sides would make that orphan's
|
|
231
241
|
// conformance unreachable instead of merged, a silent total loss.
|
|
232
|
-
const seatKey = (
|
|
242
|
+
const { seatKey } = require('./run-retry-keys'); // v4.9 W3 (SI-DUP b): the one rule, one home
|
|
233
243
|
const byJudge = new Map(s2.judgeResults.map(j => [seatKey(j.seat, j.judge), j]));
|
|
234
244
|
for (const r of s1.reviews) {
|
|
235
245
|
const j = byJudge.get(r.seat ? r.seat.id : r.model) || byJudge.get(r.model);
|
|
@@ -242,16 +252,21 @@ async function runCouncil(options, deps = {}) {
|
|
|
242
252
|
// buildTallyInput appends after the primary review rows (run-assemble.js
|
|
243
253
|
// docblock). Neither stage invents a second mechanism for the other's kind
|
|
244
254
|
// of row.
|
|
245
|
-
const mkInput = (chairStats, chairModel) =>
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
+
const mkInput = (chairStats, chairModel) => {
|
|
256
|
+
const input = asm.buildTallyInput({
|
|
257
|
+
runId: o.runId, date: o.date, bench: o.models.slice(), chair: chairModel,
|
|
258
|
+
reviews: s1.reviews, judgeResults: s2.judgeResults, chairStats, claudeReview,
|
|
259
|
+
extraRows: [...s1.extraRows, ...s2.extraRows],
|
|
260
|
+
// v4.8 PR4c §3.2: the ONLY production caller of buildTallyInput, so this is
|
|
261
|
+
// the single seam meta.seats enters through. The final input is derived by
|
|
262
|
+
// SPREAD (run-finish.js:36), not rebuilt, so both the provisional and the
|
|
263
|
+
// debated input inherit it from here.
|
|
264
|
+
seats: o.seats,
|
|
265
|
+
});
|
|
266
|
+
// v4.9 W5.3: emit-when-'task' as a pure meta TAIL; review meta stays untouched.
|
|
267
|
+
if (o.intent === 'task') { input.meta = { ...input.meta, intent: 'task' }; }
|
|
268
|
+
return input;
|
|
269
|
+
};
|
|
255
270
|
const provisionalInput = mkInput(null, o.chair);
|
|
256
271
|
const provisional = tally(provisionalInput);
|
|
257
272
|
|
package/src/council/seats.js
CHANGED
|
@@ -33,7 +33,7 @@ function sanitizeName(model) {
|
|
|
33
33
|
* Roles follow run-stages.js roleFor's PRECEDENCE — under lenses every seat is
|
|
34
34
|
* a lens seat and 'critic' is unreachable — and match it for every bench the
|
|
35
35
|
* CLI can produce (no repeated alias; lenses.length === bench.length, enforced
|
|
36
|
-
* at cli-handlers-council-run.js:
|
|
36
|
+
* at cli-handlers-council-run.js:164). They DELIBERATELY diverge where roleFor
|
|
37
37
|
* is wrong: roles here are POSITIONAL, so twins under lenses get their own
|
|
38
38
|
* lens instead of both getting the first twin's (roleFor uses
|
|
39
39
|
* o.models.indexOf). An empty lenses array counts as no lenses (roleFor treats
|
|
@@ -73,7 +73,7 @@ function buildSeats(bench, critic, lenses) {
|
|
|
73
73
|
/**
|
|
74
74
|
* Role of a seat by its id. Returns 'seat' for an unknown id, matching
|
|
75
75
|
* roleFor's fallthrough for any alias that is not the critic. (roleFor returns
|
|
76
|
-
* 'critic' for an OFF-BENCH critic because run-stages.js:
|
|
76
|
+
* 'critic' for an OFF-BENCH critic because run-stages.js:41 is not gated on
|
|
77
77
|
* bench membership; buildSeats never mints a seat for one and preflightSeats
|
|
78
78
|
* rejects that state pre-spend, so 'seat' is right for every run v4.8 creates.)
|
|
79
79
|
* @param {?Array<object>} seats
|
|
@@ -35,7 +35,7 @@ function bindStage1Waves(waves) {
|
|
|
35
35
|
// either: an orphan leg is a review that LANDED — materializeReviews writes it
|
|
36
36
|
// under its alias name — for a seat we cannot name. Retrying that seat would
|
|
37
37
|
// buy a SECOND paid leg and put two reviews on one seat, breaking the
|
|
38
|
-
// invariant run-stages.js:
|
|
38
|
+
// invariant run-stages.js:130-139 states. The orphan is already announced on
|
|
39
39
|
// `seat-unbound` at bind time (R-B: orphans are not a loss and not retryable).
|
|
40
40
|
if (legs.length === 0 || strays.length > 0) { continue; }
|
|
41
41
|
for (const seat of unbound) {
|
|
@@ -90,7 +90,8 @@ function missingSeatDeadWave(m) {
|
|
|
90
90
|
* and the alias lookup are parameterised — site 2's callback was literally
|
|
91
91
|
* `(r, i) => { if (r.seat) … }`, reading the seat off a review, not off a roster
|
|
92
92
|
* slot. The three sites: `run-retry-launch.js :: bindRetryWave`,
|
|
93
|
-
* `run-stage2.js :: runStage2`
|
|
93
|
+
* `run-stage2.js :: bindStage2Seats` (in `runStage2` itself until the v4.9 W2
|
|
94
|
+
* function-length split) and `run-debate-revote.js :: runRevoteWave`.
|
|
94
95
|
*
|
|
95
96
|
* How this differs from `bindStage1Waves` above: that one takes MANY waves, each
|
|
96
97
|
* with a REAL roster — no padding, no placeholders, and it owns its own missing/
|