amicus 4.8.0 → 4.9.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 +355 -0
- package/README.md +10 -5
- package/docs/CITATIONS.md +13 -5
- package/docs/ROADMAP.md +101 -10
- package/docs/configuration.md +55 -5
- package/docs/council.md +102 -14
- package/docs/troubleshooting.md +9 -2
- package/docs/usage.md +128 -12
- package/electron/ipc-setup.js +39 -2
- package/electron/main.js +46 -3
- package/electron/offer-session.js +51 -0
- package/electron/setup-ui-model.js +99 -9
- package/electron/setup-ui-styles.js +22 -0
- package/electron/setup-ui.js +244 -32
- package/electron/workspace-ui/live-dead-seats.js +163 -91
- package/electron/workspace-ui/live-seats.js +4 -4
- 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 +2 -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 +12 -15
- 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 +64 -16
- 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.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.js +8 -1
- package/src/sidecar/read.js +34 -10
- package/src/sidecar/setup.js +124 -0
- package/src/template/render.js +16 -7
- package/src/utils/alias-audit.js +81 -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 +16 -8
- package/src/utils/degrade.js +12 -5
- package/src/utils/doctor-alias-check.js +149 -0
- 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 +10 -2
- package/src/utils/model-canonicalization.js +64 -0
- package/src/utils/model-catalog.js +1 -1
- package/src/utils/model-shortlist.js +100 -0
- 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 +24 -0
- package/src/utils/result-schema.js +10 -0
- 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 +35 -9
- package/src/workspace/seat-space.js +10 -6
|
@@ -102,10 +102,17 @@ function labelClaudeReview(claudeReview, labels) {
|
|
|
102
102
|
/**
|
|
103
103
|
* The synthesized runStats row for a review that never ran a leg (v4.1 §4.4).
|
|
104
104
|
* durationMs/usage are null per the never-invent rule — nothing was launched.
|
|
105
|
+
*
|
|
106
|
+
* v4.9 W11 (PR1F-2): folded onto buildRunStatsEntry, byte-for-byte (pin G3,
|
|
107
|
+
* tests/council/runstats-byte-order.test.js). The one-key stand-in leg is the
|
|
108
|
+
* whole trick: this review COMPLETED (a file the orchestrator authored), so
|
|
109
|
+
* `status: 'complete'` is a fact, not an invention — and a leg carrying only a
|
|
110
|
+
* status leaves every other entry field on its never-invent default
|
|
111
|
+
* (durationMs/usage null, no waveId, no resolvedModel). Passing `leg: null`
|
|
112
|
+
* instead would emit `status: 'error'` and call a valid review dead.
|
|
105
113
|
*/
|
|
106
114
|
function claudeRunStatsRow() {
|
|
107
|
-
return {
|
|
108
|
-
status: 'complete', durationMs: null, usage: null };
|
|
115
|
+
return buildRunStatsEntry({ leg: { status: 'complete' }, model: CLAUDE_SEAT, role: CLAUDE_SEAT });
|
|
109
116
|
}
|
|
110
117
|
|
|
111
118
|
/**
|
|
@@ -136,7 +143,7 @@ function buildTallyInput({ runId, date, bench, chair, reviews, judgeResults, cha
|
|
|
136
143
|
// ONLY when the bench repeats an alias — the one case where the `alias#N`
|
|
137
144
|
// seat ids on findings[].raiserSeat, adjudications[].seat and runStats[].seat
|
|
138
145
|
// resolve to nothing else in the document (meta.models is the ALIAS list).
|
|
139
|
-
// ⚠️ `seats ?` alone would be VACUOUS: run.js:
|
|
146
|
+
// ⚠️ `seats ?` alone would be VACUOUS: run.js:142 sets o.seats unconditionally
|
|
140
147
|
// past the preflight and buildSeats always returns an ARRAY ([] is still
|
|
141
148
|
// truthy), so that spelling writes a full table into tally-input.json,
|
|
142
149
|
// tally.json AND verdict.json on every unique-alias bench.
|
|
@@ -152,7 +159,7 @@ function buildTallyInput({ runId, date, bench, chair, reviews, judgeResults, cha
|
|
|
152
159
|
// joined positionally to meta.models (`claude` is pushed onto that inside
|
|
153
160
|
// run-assemble.js :: buildTallyInput) or to streetCred[].
|
|
154
161
|
// .slice() is defence-in-depth only: the array is shared with
|
|
155
|
-
// runState.checkpoint (run.js:
|
|
162
|
+
// runState.checkpoint (run.js:144) and with both tally inputs. Nothing
|
|
156
163
|
// mutates meta.seats — unlike models, which meta.models.push mutates below —
|
|
157
164
|
// so no test can distinguish the copy from the reference.
|
|
158
165
|
...(Array.isArray(seats) && seats.some(s => s.id !== s.alias) ? { seats: seats.slice() } : {}),
|
|
@@ -207,8 +214,10 @@ function buildTallyInput({ runId, date, bench, chair, reviews, judgeResults, cha
|
|
|
207
214
|
// a repair solo's — run-stage2.js mirrors Stage-1's convention there); a judge
|
|
208
215
|
// whose wave leg died still gets an honest error row.
|
|
209
216
|
// v4.8 PR5a T4 (R5-8): the judge row carries its SEAT. PR4c withheld it because
|
|
210
|
-
// `joinsLedger` has no 'judge' member, so nothing consumed it;
|
|
211
|
-
//
|
|
217
|
+
// `joinsLedger` has no 'judge' member, so nothing consumed it;
|
|
218
|
+
// `report-cost.js :: buildCostModel` (the report's cost table, extracted from
|
|
219
|
+
// report.js in v4.9 W8) does now, and without it a twin bench's two judge rows
|
|
220
|
+
// are identical.
|
|
212
221
|
// `buildRunStatsEntry` (run-stats-entry.js :: buildRunStatsEntry) applies the
|
|
213
222
|
// shared emit-when-DIFFERENT predicate, so a unique bench stays byte-identical
|
|
214
223
|
// and no new predicate enters the tree.
|
|
@@ -261,6 +270,12 @@ function buildChairPacketFile({ runDir, reviews, claudeReview, tallyInput, recor
|
|
|
261
270
|
// line here and keeps every rendering decision in briefings-chair.js.
|
|
262
271
|
findings: record.findings,
|
|
263
272
|
tierCounts: record.tierCounts, date,
|
|
273
|
+
// v4.9 W7: the chair's intent is READ OFF the tallied record's meta rather than
|
|
274
|
+
// added to this signature — run.js already stamps meta.intent emit-when-'task'
|
|
275
|
+
// (run.js :: runCouncil's mkInput), and both the provisional and the debated
|
|
276
|
+
// record inherit it by spread. A record with no meta at all (hand-assembled
|
|
277
|
+
// input, and every pre-W7 caller) composes the review packet.
|
|
278
|
+
...(record.meta && record.meta.intent === 'task' ? { intent: 'task' } : {}),
|
|
264
279
|
}) + (debateOutcomes ? '\n\n' + buildDebateAddendum({ outcomes: debateOutcomes }) : '');
|
|
265
280
|
fs.writeFileSync(path.join(runDir, 'chair-packet.md'), packet, { mode: 0o600 });
|
|
266
281
|
return packet;
|
package/src/council/run-chair.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* @module council/run-chair
|
|
6
|
-
* Chair synthesis +
|
|
6
|
+
* Chair synthesis + terminal-line repair (VERDICT | task ANSWER) for the driver,
|
|
7
7
|
* lifted VERBATIM out of run.js for the 300-line gate (v4.1 Task 0.5). Pure
|
|
8
8
|
* refactor: same launches, same waveIds, same run.json checkpoints, same
|
|
9
9
|
* degradation rules. Launchers come in through `ctx` exactly as run-stages.js
|
|
@@ -16,96 +16,18 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
const stage2 = require('./briefings-stage2');
|
|
19
|
-
const {
|
|
19
|
+
const { parseChairTerminal } = require('./parse-stage2');
|
|
20
20
|
const runState = require('./run-state');
|
|
21
21
|
const { isAbortExit } = require('./run-stages');
|
|
22
22
|
const { emitStageStarted, emitStageTerminal } = require('../observe/events');
|
|
23
23
|
const { buildRunStatsEntry } = require('./run-assemble');
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
* model from `council stats` that is not a bench seat and not the failed
|
|
28
|
-
* chair. "Highest street-cred" = BEST = numerically LOWEST mean rank
|
|
29
|
-
* (deriveReliability's avgStreetCredPeersOnly; lower is better).
|
|
30
|
-
*
|
|
31
|
-
* The reserved seat name 'claude' is never eligible (v4.1 §4.4 "never chairs"):
|
|
32
|
-
* a --claude-review run puts a real 'claude' row in the ledger, so without this
|
|
33
|
-
* filter a LATER run could promote it and walk straight past the pre-flight
|
|
34
|
-
* --chair claude guard — with no Claude leg to launch.
|
|
35
|
-
*
|
|
36
|
-
* v4.7 GOA-7 D11: exclusions test the group key AND aliases[]; the promoted
|
|
37
|
-
* name is aliases[0] (most-recent alias) so the launch string stays routable
|
|
38
|
-
* through the same alias policy both call sites (run.js mid-walk, run-server.js
|
|
39
|
-
* pre-seed) already resolve.
|
|
40
|
-
* @returns {string|null}
|
|
41
|
-
*/
|
|
42
|
-
function pickFallbackChair(statsRows, bench, failedChair) {
|
|
43
|
-
const benchSet = new Set(bench);
|
|
44
|
-
// v4.7 GOA-7 D11: an aggregate's identity is its key PLUS every alias it was
|
|
45
|
-
// observed under — post-D10 keys may be executable ids while bench/o.chair
|
|
46
|
-
// stay alias-space, so every exclusion tests the whole name set (a bench
|
|
47
|
-
// seat's resolved-keyed group must never be promoted as its own chair).
|
|
48
|
-
// The LAUNCHED name is aliases[0] (most-recent alias): alias-space names
|
|
49
|
-
// re-enter the router's alias bridge and current key/gateway policy; a raw
|
|
50
|
-
// executable id would dodge them (divergent-vendor forms, openrouter-
|
|
51
|
-
// literals under --gateway direct, dropped aliases). aliases[] is non-empty
|
|
52
|
-
// for every ledger-derived group; the bare-model fallback covers pre-D10
|
|
53
|
-
// aggregate shapes only.
|
|
54
|
-
const names = (r) => [r.model, ...(Array.isArray(r.aliases) ? r.aliases : [])];
|
|
55
|
-
const excluded = (r) => names(r).some(n => n === 'claude' || benchSet.has(n) || n === failedChair);
|
|
56
|
-
// v4.8 PR4a: every sort term is read off the row, so CANDIDATE SELECTION is
|
|
57
|
-
// independent of the order `statsRows` arrives in — previously that order
|
|
58
|
-
// (deriveReliability's Map insertion order = council-ledger.jsonl row order)
|
|
59
|
-
// silently decided every exact street-cred tie, and such ties are an ordinary
|
|
60
|
-
// arithmetic outcome. Terms: street cred (lower mean rank = better), then
|
|
61
|
-
// council appearances, then model id for a guaranteed total order. ⚠️ `runs`
|
|
62
|
-
// is the count of DISTINCT runIds across the group's ledger rows (v4.8 PR4b
|
|
63
|
-
// R4b-1, ledger-stats.js's countRuns) — one council run contributes 1 however many
|
|
64
|
-
// seats that executable filled, and rows from `judged:false` runs that
|
|
65
|
-
// contributed no street cred still count. It is a tie-break, never a ranking
|
|
66
|
-
// signal. Always present on deriveReliability output; the default serves
|
|
67
|
-
// fixtures.
|
|
68
|
-
// Full rationale + the tie arithmetic: tests/council/run-chair.test.js.
|
|
69
|
-
const runsOf = (r) => (typeof r.runs === 'number' ? r.runs : 0);
|
|
70
|
-
const candidates = (statsRows || [])
|
|
71
|
-
.filter(r => !excluded(r) && typeof r.avgStreetCredPeersOnly === 'number')
|
|
72
|
-
.sort((a, b) => (a.avgStreetCredPeersOnly - b.avgStreetCredPeersOnly)
|
|
73
|
-
|| (runsOf(b) - runsOf(a))
|
|
74
|
-
|| (a.model < b.model ? -1 : a.model > b.model ? 1 : 0));
|
|
75
|
-
if (!candidates.length) { return null; }
|
|
76
|
-
const top = candidates[0];
|
|
77
|
-
return (Array.isArray(top.aliases) && top.aliases.length) ? top.aliases[0] : top.model;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Outcome taxonomy for one fallback-walk attempt (spec §8, LC-5). The ch4
|
|
82
|
-
* VERDICT repair is deliberately NOT an attempt: its chair leg already
|
|
83
|
-
* completed — only the verdict line is being re-prompted — and the outcome
|
|
84
|
-
* enum has no honest value for it.
|
|
85
|
-
* @param {object|null} rawLeg the UNFILTERED leg (attemptChair nulls `leg` on
|
|
86
|
-
* failure; this is the one before that narrowing, so a failed leg document
|
|
87
|
-
* is still visible here)
|
|
88
|
-
* @param {object|null} [errorDoc] set when the launch never produced a wave
|
|
89
|
-
* at all (pre-flight refusal) — the only source of a reason in that case
|
|
90
|
-
* @returns {{outcome: 'completed'|'error'|'timeout'|'no-output', reason: string|null}}
|
|
91
|
-
*/
|
|
92
|
-
function classifyChairAttempt(rawLeg, errorDoc) {
|
|
93
|
-
if (!rawLeg) {
|
|
94
|
-
const reason = (errorDoc && (errorDoc.message || errorDoc.reason)) || 'no leg document';
|
|
95
|
-
return { outcome: 'error', reason };
|
|
96
|
-
}
|
|
97
|
-
if (rawLeg.status === 'timeout') { return { outcome: 'timeout', reason: rawLeg.reason || null }; }
|
|
98
|
-
if (rawLeg.status === 'complete') {
|
|
99
|
-
const hasOutput = rawLeg.summary && String(rawLeg.summary).trim();
|
|
100
|
-
return hasOutput ? { outcome: 'completed', reason: null }
|
|
101
|
-
: { outcome: 'no-output', reason: rawLeg.reason || null };
|
|
102
|
-
}
|
|
103
|
-
return { outcome: 'error', reason: rawLeg.reason || rawLeg.error || String(rawLeg.status) };
|
|
104
|
-
}
|
|
24
|
+
// v4.9 W4: pickFallbackChair + classifyChairAttempt moved to chair-fallback.js
|
|
25
|
+
// (size-gate split); re-exported below so no caller changes.
|
|
26
|
+
const { pickFallbackChair, classifyChairAttempt } = require('./chair-fallback');
|
|
105
27
|
|
|
106
28
|
/**
|
|
107
29
|
* Chair chain (attempt → retry → ledger-promoted fallback → give up) plus the
|
|
108
|
-
* single
|
|
30
|
+
* single terminal-line repair re-prompt (VERDICT, or ANSWER when o.intent is 'task').
|
|
109
31
|
* @param {object} ctx run.js's {o, launchers, addWave, overBudget, scratchDir}
|
|
110
32
|
* @param {{packet: string, degrade: {note: Function}, statsFn: Function,
|
|
111
33
|
* isSignalled: function(): (number|null)}} args
|
|
@@ -126,7 +48,7 @@ async function runChair(ctx, { packet, degrade, statsFn, isSignalled }) {
|
|
|
126
48
|
const solo = await launchers.launchSolo({
|
|
127
49
|
model, prompt: packet, project: o.runDir, waveId,
|
|
128
50
|
timeout: o.timeout, gateway: o.gateway, noValidateModel: o.noValidateModel,
|
|
129
|
-
// v4.1 §4.5d: the chair chain (ch1/ch2/ch3) and the ch4
|
|
51
|
+
// v4.1 §4.5d: the chair chain (ch1/ch2/ch3) and the ch4 terminal repair
|
|
130
52
|
// below are the launches a re-armed price gate would refuse LAST, after
|
|
131
53
|
// the whole bench has already been paid for.
|
|
132
54
|
noCostGate: o.noCostGate,
|
|
@@ -203,6 +125,27 @@ async function runChair(ctx, { packet, degrade, statsFn, isSignalled }) {
|
|
|
203
125
|
}
|
|
204
126
|
if (attempt.leg) { actualChair = o.chair; }
|
|
205
127
|
else if (!overBudget()) {
|
|
128
|
+
// v4.9 W5.4 (V5): the promotion below draws on the reliability ledger,
|
|
129
|
+
// which task runs never feed (run-finish.js gate 1) — announced once,
|
|
130
|
+
// kind 'info' (speech, not loss: the sink never flips `degraded` on it).
|
|
131
|
+
// ⚠️ PR #200 round-3 finding A2 read "announced once" as a promise a flag
|
|
132
|
+
// had to keep, and reported the note firing once per FAILED ATTEMPT.
|
|
133
|
+
// MEASURED and refuted: "once" here is STRUCTURAL. This is the
|
|
134
|
+
// else-arm of the whole fallback walk — ch1 and ch2 have already resolved
|
|
135
|
+
// above — so it is reached at most once per runChair, which is called
|
|
136
|
+
// exactly once per run (run.js :: runCouncil). A walk with ch1, ch2 AND
|
|
137
|
+
// ch3 all failing emits exactly ONE note; pinned in run-chair.test.js
|
|
138
|
+
// ("task run reaching the promotion step emits EXACTLY ONE info note").
|
|
139
|
+
// No flag: a flag would only be able to make a once-reached branch fire
|
|
140
|
+
// less than once.
|
|
141
|
+
if (o.intent === 'task') {
|
|
142
|
+
degrade.note({
|
|
143
|
+
kind: 'info', channel: 'ledger-skipped',
|
|
144
|
+
what: 'task runs write no reliability rows',
|
|
145
|
+
why: 'ledger-driven chair promotion draws only on review-run history — task rankings measure concurrence, never defect confirmation',
|
|
146
|
+
effect: 'fallback candidates come from review runs only; a task-only install has none',
|
|
147
|
+
});
|
|
148
|
+
}
|
|
206
149
|
let statsRows = [];
|
|
207
150
|
try { statsRows = statsFn(); } catch { /* no ledger yet */ }
|
|
208
151
|
const fallback = pickFallbackChair(statsRows, o.models, o.chair);
|
|
@@ -228,16 +171,16 @@ async function runChair(ctx, { packet, degrade, statsFn, isSignalled }) {
|
|
|
228
171
|
const chairText = chairLeg ? chairLeg.summary : null;
|
|
229
172
|
let chairConformance = 'clean';
|
|
230
173
|
|
|
231
|
-
// ---- Chair
|
|
232
|
-
let overallVerdict = chairText ?
|
|
174
|
+
// ---- Chair terminal line — VERDICT, or ANSWER on a task run (v4.9 W7) ----
|
|
175
|
+
let overallVerdict = chairText ? parseChairTerminal(chairText, o.intent) : null;
|
|
233
176
|
if (chairText && !overallVerdict && !overBudget()) {
|
|
234
177
|
const waveId4 = `${o.runId}-ch4`;
|
|
235
178
|
runState.appendStageWave(o.runDir, 'chair', waveId4);
|
|
236
179
|
const repair = await launchers.launchSolo({
|
|
237
180
|
// ⚠️ LC-12: the synthesis rides along. The chair leg SUCCEEDED — only the
|
|
238
|
-
//
|
|
181
|
+
// terminal line is missing — so a fresh repair session that cannot see the
|
|
239
182
|
// synthesis is picking a verdict on an artifact it has never read.
|
|
240
|
-
model: actualChair, prompt: stage2.
|
|
183
|
+
model: actualChair, prompt: stage2.chairRepairPromptFor(o.intent, { synthesis: chairText }),
|
|
241
184
|
project: o.runDir, waveId: waveId4,
|
|
242
185
|
timeout: o.timeout, gateway: o.gateway, noValidateModel: o.noValidateModel,
|
|
243
186
|
noCostGate: o.noCostGate,
|
|
@@ -255,15 +198,17 @@ async function runChair(ctx, { packet, degrade, statsFn, isSignalled }) {
|
|
|
255
198
|
// (repair.leg)` guard below is therefore load-bearing on that exact
|
|
256
199
|
// distinction: a launched ch4 (a leg document exists, whatever its
|
|
257
200
|
// status) gets its own row so the repair's spend is attributed even when
|
|
258
|
-
// it never supplies a
|
|
259
|
-
// row at all, because there is nothing billed to attribute.
|
|
201
|
+
// it never supplies a terminal line; a ch4 that never launched gets no
|
|
202
|
+
// row at all, because there is nothing billed to attribute. The push sits
|
|
203
|
+
// AFTER the verdict parse so it stamps the ch4 leg's own measured outcome (PR 199 D1).
|
|
204
|
+
overallVerdict = parseChairTerminal((repair.leg && repair.leg.summary) || '', o.intent);
|
|
205
|
+
chairConformance = overallVerdict ? 'repaired' : 'unstructured';
|
|
260
206
|
if (repair.leg) {
|
|
261
207
|
chairRows.push(buildRunStatsEntry({
|
|
262
208
|
leg: repair.leg, model: actualChair, role: 'repair', wasChair: false,
|
|
209
|
+
conformance: overallVerdict ? 'clean' : 'unstructured',
|
|
263
210
|
}));
|
|
264
211
|
}
|
|
265
|
-
overallVerdict = parseChairVerdict((repair.leg && repair.leg.summary) || '');
|
|
266
|
-
chairConformance = overallVerdict ? 'repaired' : 'unstructured';
|
|
267
212
|
}
|
|
268
213
|
// A completed chair whose verdict never parsed is 'unstructured' even when
|
|
269
214
|
// the repair was skipped (e.g. the chair leg itself tripped --max-cost).
|
|
@@ -273,10 +218,14 @@ async function runChair(ctx, { packet, degrade, statsFn, isSignalled }) {
|
|
|
273
218
|
channel: 'chair-failed',
|
|
274
219
|
what: 'the council has no chair synthesis',
|
|
275
220
|
why: chairLeg
|
|
276
|
-
?
|
|
221
|
+
? `the chair ran but its output carried no parseable ${o.intent === 'task' ? 'ANSWER' : 'VERDICT'}: line`
|
|
277
222
|
: `no chair leg completed after the fallback walk — ${chairAttempts.map(a =>
|
|
278
223
|
`${a.waveId.split('-').pop()} ${a.model}: ${a.reason || a.outcome}`).join(' · ')}`,
|
|
279
|
-
|
|
224
|
+
// v4.9 W12 (PR #200 tail C4): the noun forks the way `why` above already
|
|
225
|
+
// does — a task run is told its ANSWER was lost, not its verdict. The
|
|
226
|
+
// `overallVerdict` KEY does not fork (verdict.js emits it on both scales),
|
|
227
|
+
// and `what` above is intent-neutral, so this is the only word that moves.
|
|
228
|
+
effect: `the ${o.intent === 'task' ? 'answer' : 'verdict'} is written with overallVerdict null; will exit degraded (2)`,
|
|
280
229
|
});
|
|
281
230
|
}
|
|
282
231
|
|
|
@@ -3,19 +3,22 @@
|
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* @module council/run-debate-revote
|
|
6
|
-
* v4.8 PR3 Task 1: `legOpts`, `legRow`, and `runRevoteWave` extracted verbatim
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
6
|
+
* v4.8 PR3 Task 1: `legOpts`, `legRow`, and `runRevoteWave` extracted verbatim out of
|
|
7
|
+
* run-debate.js (283/300 — the 300-line-per-file gate) with NO behaviour change.
|
|
8
|
+
* run-debate.js requires all three back; runDebate still calls runRevoteWave for the
|
|
9
|
+
* re-vote mini-wave (spec §5.1).
|
|
10
10
|
*
|
|
11
|
-
* That "verbatim" claim held for all three only through Task 1
|
|
12
|
-
* `
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
11
|
+
* That "verbatim" claim held for all three only through Task 1 and now holds for
|
|
12
|
+
* `legOpts` ALONE: v4.9 W11 (PR1F-2) folded `legRow`'s body onto `buildRunStatsEntry`
|
|
13
|
+
* (its own docblock points at the diff), so it is no longer byte-identical either.
|
|
14
|
+
* `runRevoteWave` diverged first — PR3 Task 6 gave it real seat-binding behaviour:
|
|
15
|
+
* the padded roster + bind (`stage1-bind.js :: bindPaddedWave` since v4.8 SI-27;
|
|
16
|
+
* an inline `bindSeats` call before it), the seat-keyed `byJudge`, the
|
|
17
|
+
* `sanitizeName`'d per-seat repair id, and the `seat` field on the pushed legs
|
|
18
|
+
* (see the function's own docblock below). Do not treat `runRevoteWave` as a
|
|
19
|
+
* behaviour-neutral mirror of the old run-debate.js code. Since v4.9 W2 (SI-16)
|
|
20
|
+
* the one-bounded-repair block lives in the in-file `repairRevoteLeg`, called from
|
|
21
|
+
* runRevoteWave's leg loop — a structural split only, no behaviour change.
|
|
19
22
|
*
|
|
20
23
|
* `isAbortExit` comes from ./run-launch, NEVER from ./run-stages: run-stage2.js:12
|
|
21
24
|
* records that taking it from run-launch.js "is what dissolved the old cycle
|
|
@@ -38,6 +41,15 @@ const { sanitizeName } = require('./seats');
|
|
|
38
41
|
// ./seats, so this leaf stays cycle-free (see the module docblock's cycle-class
|
|
39
42
|
// paragraph above — named, not line-numbered, so it cannot rot).
|
|
40
43
|
const { bindPaddedWave } = require('./stage1-bind');
|
|
44
|
+
// v4.9 W3 (SI-DUP disposition b): the wave's join key for one leg — the bound
|
|
45
|
+
// seat's id, else the bare alias. Was a local `function seatKey` here; now the
|
|
46
|
+
// run-retry-keys.js export (same rule, one home). ./run-retry-keys is
|
|
47
|
+
// REQUIRE-FREE by design (its own docblock), so this leaf stays cycle-free.
|
|
48
|
+
const { seatKey } = require('./run-retry-keys');
|
|
49
|
+
// v4.9 W11 (PR1F-2): the ONE runStats row builder. ./run-stats-entry is REQUIRE-FREE
|
|
50
|
+
// by design, so this leaf stays cycle-free — taking the same function off
|
|
51
|
+
// ./run-assemble would drag that module's whole graph in.
|
|
52
|
+
const { buildRunStatsEntry } = require('./run-stats-entry');
|
|
41
53
|
|
|
42
54
|
/** Common launch options for every debate leg (judge-isolated `_scratch` cwd). */
|
|
43
55
|
function legOpts(ctx, waveId) {
|
|
@@ -50,24 +62,21 @@ function legOpts(ctx, waveId) {
|
|
|
50
62
|
|
|
51
63
|
/**
|
|
52
64
|
* v4.7 D2/E4: normalize a raw (possibly leg-absent) leg into the shape
|
|
53
|
-
* debateRunStatsRows' superseded/repair lists expect
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
65
|
+
* debateRunStatsRows' superseded/repair lists expect — keyed on an explicit `model` (the
|
|
66
|
+
* raiser or judge identity), since a leg-absent attempt has no `.model` of its own to
|
|
67
|
+
* read, and threading resolvedModel emit-only-when-set (v4.7 GOA-7 D8).
|
|
68
|
+
*
|
|
69
|
+
* v4.9 W11 (PR1F-2): the never-invent-a-waveId / never-invent-a-duration discipline
|
|
70
|
+
* this restated is now INHERITED — this is a call to buildRunStatsEntry, whose `leg`
|
|
71
|
+
* contract this argument already satisfies exactly (a raw leg doc: `.model` IS the
|
|
72
|
+
* resolved id, `model` here IS the alias). `role` is deliberately not passed: which
|
|
73
|
+
* role this is depends on which list the caller pushes it onto, and `debate.js :: mk`
|
|
74
|
+
* stamps it. Its byte diff + the pin that all of it is invisible to `mk`: "FOLD DIFF #2" and G6, tests/council/runstats-byte-order.test.js.
|
|
58
75
|
*/
|
|
59
76
|
function legRow(model, leg, conformance) {
|
|
60
|
-
return leg
|
|
61
|
-
? { model, status: leg.status, durationMs: typeof leg.durationMs === 'number' ? leg.durationMs : null,
|
|
62
|
-
usage: leg.usage || null, conformance, summary: leg.summary || '',
|
|
63
|
-
...(leg.waveId ? { waveId: leg.waveId } : {}),
|
|
64
|
-
...(leg.model ? { resolvedModel: leg.model } : {}) }
|
|
65
|
-
: { model, status: 'error', durationMs: null, usage: null, conformance, summary: '' };
|
|
77
|
+
return buildRunStatsEntry({ leg, model, conformance, summary: leg && leg.summary });
|
|
66
78
|
}
|
|
67
79
|
|
|
68
|
-
/** The wave's join key for one leg: the bound seat's id, else the bare alias. */
|
|
69
|
-
function seatKey(seat, alias) { return seat ? seat.id : alias; }
|
|
70
|
-
|
|
71
80
|
/**
|
|
72
81
|
* T5.1 (owner ruling R8), narrowed by T5.5: announce a re-vote leg whose key
|
|
73
82
|
* this wave cannot account for — its key names none of the judges this wave
|
|
@@ -132,6 +141,46 @@ function reVoteUnboundNote(waveId, judge, key, leg) {
|
|
|
132
141
|
};
|
|
133
142
|
}
|
|
134
143
|
|
|
144
|
+
/**
|
|
145
|
+
* v4.9 W2 (SI-16): the one bounded repair for an ALIVE-but-unparseable re-vote
|
|
146
|
+
* leg (spec §5.7 — only ONE repair is spent), split out of runRevoteWave's leg
|
|
147
|
+
* loop. The CALLER owns the `alive && !parsed.ok` gate; this function always
|
|
148
|
+
* launches exactly one repair solo. It returns the post-repair view the caller
|
|
149
|
+
* records from there on — `parsed`, `conformance`, `outLeg` (the repaired leg
|
|
150
|
+
* when the repair completed, else the original) — plus exactly one non-null
|
|
151
|
+
* row: `supersededRow` (completed repair — the pre-repair leg's row) or
|
|
152
|
+
* `repairRow` (dead repair — the failed attempt's own row), for the caller to
|
|
153
|
+
* push. A user abort mid-repair returns `{ aborted: <exitCode> }` alone, which
|
|
154
|
+
* the caller propagates as its own return.
|
|
155
|
+
*/
|
|
156
|
+
async function repairRevoteLeg(ctx, { waveId, key, judge, leg, parsed, expectedIds }) {
|
|
157
|
+
// One repair, solo, to that judge. The id is built from the SEAT key so
|
|
158
|
+
// two twins never share one repair id (and one never overwrites the
|
|
159
|
+
// other's run-state entry). ⚠️ The trailing `r` is load-bearing: it is what
|
|
160
|
+
// stops bindSeats' `/^(.*)-(\d+)$/` matching a repair id whose judge alias
|
|
161
|
+
// is a bare number (`r1-rv-2r` does not match; `r1-rv-2` would). Dropping
|
|
162
|
+
// it re-arms a collision. sanitizeName also fixes the pre-existing slash
|
|
163
|
+
// bug (D4) — `r1-rv-openrouter/deepseek/deepseek-chatr` stops nesting
|
|
164
|
+
// three directory levels — and is a no-op for every plain alias.
|
|
165
|
+
const repairId = `${waveId}-${sanitizeName(key)}r`;
|
|
166
|
+
runState.appendStageWave(ctx.o.runDir, 'debate-revote', repairId);
|
|
167
|
+
const r2 = await ctx.launchers.launchSolo({ ...legOpts(ctx, repairId), model: judge,
|
|
168
|
+
// ⚠️ LC-12: ditto — the re-vote output being repaired rides with its errors.
|
|
169
|
+
prompt: dbrief.buildRevoteRepairPrompt({ errors: parsed.errors, revote: leg.summary }) });
|
|
170
|
+
ctx.addWave(r2.wave);
|
|
171
|
+
if (isAbortExit(r2.exitCode)) { return { aborted: r2.exitCode }; }
|
|
172
|
+
const leg2 = r2.leg && r2.leg.status === 'complete' ? r2.leg : null;
|
|
173
|
+
parsed = leg2 ? parseRevote(leg2.summary, expectedIds) : parsed;
|
|
174
|
+
const conformance = parsed.ok ? 'repaired' : 'unstructured';
|
|
175
|
+
// Symmetric with runDefenseSolo's `if (leg2) { leg = leg2; }` — otherwise
|
|
176
|
+
// revote-<model>.md and the runStats row keep the PRE-repair output.
|
|
177
|
+
return leg2
|
|
178
|
+
? { aborted: null, parsed, conformance, outLeg: leg2,
|
|
179
|
+
supersededRow: legRow(judge, leg, 'unstructured'), repairRow: null }
|
|
180
|
+
: { aborted: null, parsed, conformance, outLeg: leg,
|
|
181
|
+
supersededRow: null, repairRow: legRow(judge, r2.leg, 'unstructured') };
|
|
182
|
+
}
|
|
183
|
+
|
|
135
184
|
/**
|
|
136
185
|
* The re-vote mini-wave (spec §5.1).
|
|
137
186
|
*
|
|
@@ -151,7 +200,7 @@ function reVoteUnboundNote(waveId, judge, key, leg) {
|
|
|
151
200
|
* @param {function(string): string} aliasOf seat id → bench alias
|
|
152
201
|
*/
|
|
153
202
|
async function runRevoteWave(ctx, judgeKeys, bundleFindings, judgeSeats, aliasOf) {
|
|
154
|
-
const bundle = dbrief.buildRevoteBundle({ findings: bundleFindings, date: ctx.o.date });
|
|
203
|
+
const bundle = dbrief.buildRevoteBundle({ findings: bundleFindings, date: ctx.o.date, intent: ctx.o.intent });
|
|
155
204
|
// spec §5.1 names `revote-bundle.md` a run-dir artifact: the shared re-vote prompt goes to
|
|
156
205
|
// disk exactly like Stage 2's bundle-stage2.md, so the round's model-facing input is
|
|
157
206
|
// auditable alongside briefing-stage1.md and chair-packet.md.
|
|
@@ -194,28 +243,11 @@ async function runRevoteWave(ctx, judgeKeys, bundleFindings, judgeSeats, aliasOf
|
|
|
194
243
|
: { ok: false, byId: {}, errors: [{ code: 'DEAD_LEG', detail: 'no summary' }] };
|
|
195
244
|
let conformance = alive ? 'clean' : 'unstructured';
|
|
196
245
|
if (alive && !parsed.ok) {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
// it re-arms a collision. sanitizeName also fixes the pre-existing slash
|
|
203
|
-
// bug (D4) — `r1-rv-openrouter/deepseek/deepseek-chatr` stops nesting
|
|
204
|
-
// three directory levels — and is a no-op for every plain alias.
|
|
205
|
-
const repairId = `${waveId}-${sanitizeName(key)}r`;
|
|
206
|
-
runState.appendStageWave(ctx.o.runDir, 'debate-revote', repairId);
|
|
207
|
-
const r2 = await ctx.launchers.launchSolo({ ...legOpts(ctx, repairId), model: judge,
|
|
208
|
-
// ⚠️ LC-12: ditto — the re-vote output being repaired rides with its errors.
|
|
209
|
-
prompt: dbrief.buildRevoteRepairPrompt({ errors: parsed.errors, revote: leg.summary }) });
|
|
210
|
-
ctx.addWave(r2.wave);
|
|
211
|
-
if (isAbortExit(r2.exitCode)) { return { aborted: r2.exitCode }; }
|
|
212
|
-
const leg2 = r2.leg && r2.leg.status === 'complete' ? r2.leg : null;
|
|
213
|
-
parsed = leg2 ? parseRevote(leg2.summary, expectedIds) : parsed;
|
|
214
|
-
conformance = parsed.ok ? 'repaired' : 'unstructured';
|
|
215
|
-
// Symmetric with runDefenseSolo's `if (leg2) { leg = leg2; }` — otherwise
|
|
216
|
-
// revote-<model>.md and the runStats row keep the PRE-repair output.
|
|
217
|
-
if (leg2) { supersededLegs.push(legRow(judge, leg, 'unstructured')); outLeg = leg2; }
|
|
218
|
-
else { repairLegs.push(legRow(judge, r2.leg, 'unstructured')); }
|
|
246
|
+
const rep = await repairRevoteLeg(ctx, { waveId, key, judge, leg, parsed, expectedIds });
|
|
247
|
+
if (rep.aborted) { return { aborted: rep.aborted }; }
|
|
248
|
+
({ parsed, conformance, outLeg } = rep);
|
|
249
|
+
if (rep.supersededRow) { supersededLegs.push(rep.supersededRow); }
|
|
250
|
+
if (rep.repairRow) { repairLegs.push(rep.repairRow); }
|
|
219
251
|
}
|
|
220
252
|
// ⚠️ Two DIFFERENT values on the same iteration: `byJudge`'s key is the SEAT
|
|
221
253
|
// (applyDebate joins it against `(a.seat || a.judge)`), while the leg's
|
|
@@ -35,7 +35,7 @@ const { legOpts, legRow, runRevoteWave } = require('./run-debate-revote');
|
|
|
35
35
|
* `raiserKey` survives in exactly two places here: the returned `{raiser}` (which
|
|
36
36
|
* becomes defenseByRaiser's key — §3.3: that MUST stay seat-keyed, because
|
|
37
37
|
* re-keying it to the alias is last-wins and silently drops one twin's entire
|
|
38
|
-
* debate row plus its amended claim) and the `seat`
|
|
38
|
+
* debate row plus its amended claim) and the `seat` runDefenseWave derives from it.
|
|
39
39
|
* Everything else — both launch sites, both legRow calls, the stub and the
|
|
40
40
|
* returned leg's `model` — is `raiserAlias`.
|
|
41
41
|
*
|
|
@@ -47,7 +47,7 @@ const { legOpts, legRow, runRevoteWave } = require('./run-debate-revote');
|
|
|
47
47
|
*/
|
|
48
48
|
async function runDefenseSolo(ctx, raiserKey, findings, idx, aliasOf) {
|
|
49
49
|
const raiserAlias = aliasOf(raiserKey);
|
|
50
|
-
const brief = dbrief.buildDefenseBrief({ findings, date: ctx.o.date });
|
|
50
|
+
const brief = dbrief.buildDefenseBrief({ findings, date: ctx.o.date, intent: ctx.o.intent });
|
|
51
51
|
const waveId = `${ctx.o.runId}-d${idx + 1}`;
|
|
52
52
|
const expectedIds = findings.map(f => f.id);
|
|
53
53
|
// Record the sub-wave BEFORE launching: `amicus abort` cascades over stages[].waveIds
|
|
@@ -97,6 +97,51 @@ async function runDefenseSolo(ctx, raiserKey, findings, idx, aliasOf) {
|
|
|
97
97
|
supersededLeg, repairLeg };
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
/**
|
|
101
|
+
* Defense mini-wave: ONE CONCURRENT solo per raiser (spec §5.1).
|
|
102
|
+
*
|
|
103
|
+
* Concurrent, not sequential: every raiser gets its OWN briefing, so this is N independent
|
|
104
|
+
* solos rather than one fanout wave. No per-leg budget check interleaves between them — the
|
|
105
|
+
* cost ceiling is a WHOLE-ROUND gate run.js applies BEFORE calling runDebate
|
|
106
|
+
* ('skipped-cost-ceiling' is a round-level outcome in spec §5.1's enum, not a per-leg one).
|
|
107
|
+
* `appendStageWave` is sync fs and each solo registers its waveId before its first await,
|
|
108
|
+
* so concurrency cannot interleave a read-modify-write of run.json.
|
|
109
|
+
* v4.1 §4.4: the reserved seat 'claude' is a FILE-sourced review with no leg to
|
|
110
|
+
* launch, so it is never asked to defend — its contested findings simply stand
|
|
111
|
+
* (the same "originals stand" outcome as a no-response).
|
|
112
|
+
*
|
|
113
|
+
* @param {object} ctx run.js's {o, launchers, addWave, overBudget, scratchDir}
|
|
114
|
+
* @param {{byRaiser: object, aliasOf: function(string): string, seatById: Map<string, object>}} args
|
|
115
|
+
* @returns {Promise<{aborted: (number|null), defenseResults: Array<object>, defenseByRaiser: (object|null)}>}
|
|
116
|
+
* `aborted` is an abort exit code (defenseByRaiser is null then) or null on a full wave.
|
|
117
|
+
*/
|
|
118
|
+
async function runDefenseWave(ctx, { byRaiser, aliasOf, seatById }) {
|
|
119
|
+
const raisers = Object.keys(byRaiser).filter(m => m !== 'claude');
|
|
120
|
+
const defenseResults = await Promise.all(
|
|
121
|
+
raisers.map((raiserKey, i) => runDefenseSolo(ctx, raiserKey, byRaiser[raiserKey], i, aliasOf)));
|
|
122
|
+
// A signal during the defense wave aborts the whole finalization (spec §5.7): return the
|
|
123
|
+
// abort code — runDebate maps it onto its own abort return — so run.js finalizes
|
|
124
|
+
// 'aborted' with NO tally-final / NO ledger.
|
|
125
|
+
const abortedDefense = defenseResults.find(d => d.aborted);
|
|
126
|
+
if (abortedDefense) { return { aborted: abortedDefense.aborted, defenseResults, defenseByRaiser: null }; }
|
|
127
|
+
// ⚠️ This literal lives in runDefenseWave, where `raiserAlias` does not exist — the
|
|
128
|
+
// projection is `aliasOf(d.raiser)`. `model` must stay ALIAS-valued (R3-1);
|
|
129
|
+
// `seat` is what gives two twins two files instead of one clobbered one (R3-3).
|
|
130
|
+
materializeDebate(ctx.o.runDir, defenseResults.map(d => ({ model: aliasOf(d.raiser),
|
|
131
|
+
summary: d.leg.summary, seat: seatById.get(d.raiser) || null })), 'rebuttal');
|
|
132
|
+
|
|
133
|
+
const defenseByRaiser = {};
|
|
134
|
+
for (const dr of defenseResults) { defenseByRaiser[dr.raiser] = { ...dr.byId }; }
|
|
135
|
+
// v4.1 §4.4: claude never gets a defense leg (raisers filter above), but its
|
|
136
|
+
// contested/disputed findings still need an audit trail — the SAME spec §5.7
|
|
137
|
+
// "originals stand" fallback a dead/unrepaired defense leg gets. Seeded into
|
|
138
|
+
// defenseByRaiser ONLY (never defenseResults, which feeds runDebate's `bad(l)`
|
|
139
|
+
// degraded check — a claude entry there would wrongly flip a clean run to
|
|
140
|
+
// degraded/exit 2).
|
|
141
|
+
if (byRaiser.claude) { defenseByRaiser.claude = allNoResponse(byRaiser.claude.map(f => f.id)); }
|
|
142
|
+
return { aborted: null, defenseResults, defenseByRaiser };
|
|
143
|
+
}
|
|
144
|
+
|
|
100
145
|
/**
|
|
101
146
|
* Full Stage-2.5 sequence (spec §5.1). Returns everything run.js needs. Cost gate: run.js
|
|
102
147
|
* checks overBudget before invoking; this checks again before the re-vote wave (spec §5.7).
|
|
@@ -128,38 +173,10 @@ async function runDebate(ctx, { provisionalRecord, tallyInput }) {
|
|
|
128
173
|
const seatById = new Map(seatTable.map(s => [s.id, s]));
|
|
129
174
|
const aliasOf = (key) => { const s = seatById.get(key); return s ? s.alias : key; };
|
|
130
175
|
|
|
131
|
-
// ---- Defense mini-wave:
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
// ('skipped-cost-ceiling' is a round-level outcome in spec §5.1's enum, not a per-leg one).
|
|
136
|
-
// `appendStageWave` is sync fs and each solo registers its waveId before its first await,
|
|
137
|
-
// so concurrency cannot interleave a read-modify-write of run.json.
|
|
138
|
-
// v4.1 §4.4: the reserved seat 'claude' is a FILE-sourced review with no leg to
|
|
139
|
-
// launch, so it is never asked to defend — its contested findings simply stand
|
|
140
|
-
// (the same "originals stand" outcome as a no-response).
|
|
141
|
-
const raisers = Object.keys(byRaiser).filter(m => m !== 'claude');
|
|
142
|
-
const defenseResults = await Promise.all(
|
|
143
|
-
raisers.map((raiserKey, i) => runDefenseSolo(ctx, raiserKey, byRaiser[raiserKey], i, aliasOf)));
|
|
144
|
-
// A signal during the defense wave aborts the whole finalization (spec §5.7):
|
|
145
|
-
// return the abort code so run.js finalizes 'aborted' with NO tally-final / NO ledger.
|
|
146
|
-
const abortedDefense = defenseResults.find(d => d.aborted);
|
|
147
|
-
if (abortedDefense) { return { aborted: abortedDefense.aborted, contested, disputed }; }
|
|
148
|
-
// ⚠️ This literal lives in runDebate, where `raiserAlias` does not exist — the
|
|
149
|
-
// projection is `aliasOf(d.raiser)`. `model` must stay ALIAS-valued (R3-1);
|
|
150
|
-
// `seat` is what gives two twins two files instead of one clobbered one (R3-3).
|
|
151
|
-
materializeDebate(ctx.o.runDir, defenseResults.map(d => ({ model: aliasOf(d.raiser),
|
|
152
|
-
summary: d.leg.summary, seat: seatById.get(d.raiser) || null })), 'rebuttal');
|
|
153
|
-
|
|
154
|
-
const defenseByRaiser = {};
|
|
155
|
-
for (const dr of defenseResults) { defenseByRaiser[dr.raiser] = { ...dr.byId }; }
|
|
156
|
-
// v4.1 §4.4: claude never gets a defense leg (raisers filter above), but its
|
|
157
|
-
// contested/disputed findings still need an audit trail — the SAME spec §5.7
|
|
158
|
-
// "originals stand" fallback a dead/unrepaired defense leg gets. Seeded into
|
|
159
|
-
// defenseByRaiser ONLY (never defenseResults, which feeds the `bad(l)`
|
|
160
|
-
// degraded check below — a claude entry there would wrongly flip a clean run
|
|
161
|
-
// to degraded/exit 2).
|
|
162
|
-
if (byRaiser.claude) { defenseByRaiser.claude = allNoResponse(byRaiser.claude.map(f => f.id)); }
|
|
176
|
+
// ---- Defense mini-wave (runDefenseWave): abort short-circuits the round (spec §5.7) ----
|
|
177
|
+
const dw = await runDefenseWave(ctx, { byRaiser, aliasOf, seatById });
|
|
178
|
+
if (dw.aborted) { return { aborted: dw.aborted, contested, disputed }; }
|
|
179
|
+
const { defenseResults, defenseByRaiser } = dw;
|
|
163
180
|
// Stamp previousTier onto the tally input: applyDebate reads it off tallyInput.findings[]
|
|
164
181
|
// (it ignores the provisional record), so without this every row's previousTier is null.
|
|
165
182
|
const stampedInput = { ...tallyInput, findings: tallyInput.findings.map(f => ({ ...f, previousTier: previousTier[f.id] })) };
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// Moved verbatim from run.js@6b0c3b6b:242-288 (v4.8 PR0 size-gate split, zero
|
|
5
5
|
// behavior). NOT run-finalize.js — that sibling owns exit codes and the
|
|
6
6
|
// terminal write; this module builds the final tally, appends the run
|
|
7
|
-
// record to the ledger (skipped for lens runs), and writes the
|
|
7
|
+
// record to the ledger (skipped for lens runs and task runs), and writes the
|
|
8
8
|
// tally/verdict artifact files + their stage events.
|
|
9
9
|
const { tally } = require('./tally');
|
|
10
10
|
const { decorateRecord } = require('./debate');
|
|
@@ -48,8 +48,10 @@ function finishRun({ o, chairRes, debatedInput, debateFindings, appendRunFn, deg
|
|
|
48
48
|
];
|
|
49
49
|
const record = tally(finalInput);
|
|
50
50
|
if (debateFindings) { decorateRecord(record, debateFindings); }
|
|
51
|
-
if (!o.lenses) {
|
|
52
|
-
// Lens runs never feed cross-run reliability stats (spec §4 / skill rule)
|
|
51
|
+
if (!o.lenses && o.intent !== 'task') {
|
|
52
|
+
// Lens runs never feed cross-run reliability stats (spec §4 / skill rule);
|
|
53
|
+
// task runs neither (v4.9 W5.4 gate 1 — task rankings measure concurrence,
|
|
54
|
+
// never defect confirmation, so a task row would poison chair promotion).
|
|
53
55
|
try { appendRunFn(record); }
|
|
54
56
|
catch (e) { process.stderr.write(`Notice: council ledger append failed: ${e.message}\n`); }
|
|
55
57
|
}
|
|
@@ -7,10 +7,10 @@
|
|
|
7
7
|
// run-retry-group.js re-exports all four, so no import path in the tree moved.
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
|
-
* The one seat-key rule: a seat's id when it was identified, its alias otherwise.
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
10
|
+
* The one seat-key rule: a seat's id when it was identified, its alias otherwise. Exported
|
|
11
|
+
* so `run-retry-group.js :: recordFailure`, run-retry.js and — since v4.9 W3 (SI-DUP
|
|
12
|
+
* disposition b) — run.js, run-debate-revote.js and run-stage1-rows.js all consume it
|
|
13
|
+
* rather than re-spelling it: readers of one rule that drift apart is how it splits.
|
|
14
14
|
*/
|
|
15
15
|
const seatKey = (s, alias) => (s ? s.id : alias);
|
|
16
16
|
|
|
@@ -15,13 +15,13 @@
|
|
|
15
15
|
const briefings = require('./briefings');
|
|
16
16
|
const { bindPaddedWave } = require('./stage1-bind');
|
|
17
17
|
|
|
18
|
-
/** The briefing a retry unit re-issues — same
|
|
18
|
+
/** The briefing a retry unit re-issues — same intent-aware dispatchers Stage 1 used (v4.9 W6). */
|
|
19
19
|
function briefingFor(o, unit) {
|
|
20
|
-
if (unit.unit === 'critic') { return briefings.
|
|
20
|
+
if (unit.unit === 'critic') { return briefings.stage1CriticBriefing(o.intent, { briefing: o.briefing, date: o.date }); }
|
|
21
21
|
if (unit.unit === 'lens') {
|
|
22
|
-
return briefings.
|
|
22
|
+
return briefings.stage1LensBriefing(o.intent, { lens: o.lenses[unit.lensIndex - 1], briefing: o.briefing, date: o.date });
|
|
23
23
|
}
|
|
24
|
-
return briefings.
|
|
24
|
+
return briefings.stage1SeatBriefing(o.intent, { briefing: o.briefing, date: o.date });
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
/**
|