amicus 4.6.2 → 4.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +349 -0
- package/README.md +24 -13
- package/bin/amicus.js +31 -0
- package/docs/ROADMAP.md +172 -36
- package/docs/configuration.md +56 -6
- package/docs/council.md +63 -10
- package/docs/doc-system.md +8 -7
- package/docs/schemas.md +10 -1
- package/docs/troubleshooting.md +27 -1
- package/docs/usage.md +68 -15
- package/electron/workspace-ui/index.html +3 -0
- package/electron/workspace-ui/live-model.js +132 -21
- package/electron/workspace-ui/workspace-app.js +20 -4
- package/electron/workspace-ui/workspace-lazy.js +233 -0
- package/electron/workspace-ui/workspace-matrix.js +12 -1
- package/electron/workspace-ui/workspace-panels.js +24 -171
- package/electron/workspace-ui/workspace-render.js +15 -5
- package/electron/workspace-ui/workspace-seats.js +88 -5
- package/electron/workspace-ui/workspace-verbs.js +1 -1
- package/electron/workspace-ui/workspace.css +6 -0
- package/package.json +5 -2
- package/schemas/council-run.schema.json +1 -0
- package/schemas/council-stats.schema.json +9 -1
- package/schemas/run.schema.json +2 -1
- package/schemas/spend.schema.json +1 -1
- package/schemas/wave.schema.json +2 -1
- package/skills/second-opinion/MANUAL-ORCHESTRATION.md +12 -0
- package/skills/second-opinion/MODEL-NOTES.md +5 -4
- package/skills/sidecar/SKILL.md +7 -2
- package/src/cli-council-run-bench.js +86 -0
- package/src/cli-handlers-council-run.js +65 -81
- package/src/cli-handlers-council.js +24 -3
- package/src/cli-handlers-doctor.js +9 -3
- package/src/cli-handlers-fanout.js +179 -0
- package/src/cli-handlers-pack.js +24 -10
- package/src/cli-handlers-run.js +19 -161
- package/src/cli-template-args.js +48 -0
- package/src/cli.js +39 -46
- package/src/council/debate.js +89 -10
- package/src/council/ledger.js +72 -11
- package/src/council/presets-cli.js +6 -2
- package/src/council/report.js +17 -6
- package/src/council/run-assemble.js +15 -3
- package/src/council/run-budget.js +2 -2
- package/src/council/run-chair.js +70 -11
- package/src/council/run-debate.js +51 -67
- package/src/council/run-launch.js +9 -2
- package/src/council/run-retry.js +4 -1
- package/src/council/run-stage1-launch.js +94 -0
- package/src/council/run-stage2.js +25 -4
- package/src/council/run-stages.js +79 -86
- package/src/council/run-state.js +10 -2
- package/src/council/run.js +26 -2
- package/src/council/tally.js +6 -2
- package/src/mcp-council-awareness.js +1 -0
- package/src/mcp-council-bench.js +4 -0
- package/src/mcp-council-run.js +10 -0
- package/src/mcp-server.js +114 -54
- package/src/mcp-tools.js +12 -5
- package/src/pack/pack-cli.js +1 -1
- package/src/pack/pack-forward.js +12 -4
- package/src/pack/pack-resolve.js +3 -0
- package/src/pack/pack-store.js +20 -3
- package/src/pack/pack-validate.js +5 -1
- package/src/session-manager.js +6 -2
- package/src/sidecar/budget.js +38 -4
- package/src/sidecar/fanout-budget.js +1 -2
- package/src/sidecar/fanout-leg-fallback.js +7 -3
- package/src/sidecar/fanout-wave-io.js +13 -1
- package/src/sidecar/fanout.js +11 -9
- package/src/sidecar/list-limit.js +50 -0
- package/src/sidecar/list-search.js +69 -0
- package/src/sidecar/read.js +90 -5
- package/src/sidecar/start-metadata.js +58 -0
- package/src/sidecar/start.js +8 -43
- package/src/sidecar/workspace-auto-open.js +2 -2
- package/src/spend-query.js +2 -1
- package/src/template/apply.js +7 -4
- package/src/template/render.js +6 -2
- package/src/template/store.js +1 -1
- package/src/utils/alias-audit.js +19 -0
- package/src/utils/cli-preflight.js +27 -1
- package/src/utils/config.js +15 -0
- package/src/utils/curated-models.js +43 -7
- package/src/utils/gateway-route-audit.js +16 -3
- package/src/utils/model-fetcher.js +8 -6
- package/src/utils/remediation-hints.js +14 -0
- package/src/utils/result-schema-rebuild.js +1 -0
- package/src/utils/result-schema.js +6 -1
- package/src/utils/session-index-tmp-sweep.js +18 -3
- package/src/utils/session-index.js +1 -0
- package/src/utils/session-metadata-tmp-sweep.js +156 -0
- package/src/utils/spend-ledger.js +11 -4
- package/src/utils/validators.js +16 -0
package/src/council/run-chair.js
CHANGED
|
@@ -20,6 +20,7 @@ const { parseChairVerdict } = 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
|
+
const { buildRunStatsEntry } = require('./run-assemble');
|
|
23
24
|
|
|
24
25
|
/**
|
|
25
26
|
* Chair fallback promotion (spec §4): the highest peers-only street-cred
|
|
@@ -31,15 +32,33 @@ const { emitStageStarted, emitStageTerminal } = require('../observe/events');
|
|
|
31
32
|
* a --claude-review run puts a real 'claude' row in the ledger, so without this
|
|
32
33
|
* filter a LATER run could promote it and walk straight past the pre-flight
|
|
33
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.
|
|
34
40
|
* @returns {string|null}
|
|
35
41
|
*/
|
|
36
42
|
function pickFallbackChair(statsRows, bench, failedChair) {
|
|
37
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);
|
|
38
56
|
const candidates = (statsRows || [])
|
|
39
|
-
.filter(r =>
|
|
40
|
-
&& typeof r.avgStreetCredPeersOnly === 'number')
|
|
57
|
+
.filter(r => !excluded(r) && typeof r.avgStreetCredPeersOnly === 'number')
|
|
41
58
|
.sort((a, b) => a.avgStreetCredPeersOnly - b.avgStreetCredPeersOnly);
|
|
42
|
-
|
|
59
|
+
if (!candidates.length) { return null; }
|
|
60
|
+
const top = candidates[0];
|
|
61
|
+
return (Array.isArray(top.aliases) && top.aliases.length) ? top.aliases[0] : top.model;
|
|
43
62
|
}
|
|
44
63
|
|
|
45
64
|
/**
|
|
@@ -98,6 +117,7 @@ async function runChair(ctx, { packet, degrade, statsFn, isSignalled }) {
|
|
|
98
117
|
// v4.3 Task 3 (spec §7.2 named defect): without this, chair spend is
|
|
99
118
|
// ledgered with councilRunId:null and is unattributable.
|
|
100
119
|
councilRunId: o.runId, councilName: o.councilName,
|
|
120
|
+
tag: o.tag, // v4.7 F8 D16: rides the same forward as councilRunId/councilName.
|
|
101
121
|
});
|
|
102
122
|
addWave(solo.wave);
|
|
103
123
|
const ok = solo.leg && solo.leg.status === 'complete'
|
|
@@ -117,12 +137,24 @@ async function runChair(ctx, { packet, degrade, statsFn, isSignalled }) {
|
|
|
117
137
|
// cost-skipped chair (the `if` branch) simply never calls recordAttempt —
|
|
118
138
|
// chairAttempts is never checkpointed and the key stays absent on run.json.
|
|
119
139
|
const chairAttempts = [];
|
|
140
|
+
// v4.7 D2 (spec "the count is the count"): a non-primary row per launch so
|
|
141
|
+
// spend is fully attributable — one 'chair-attempt' row per FAILED attempt
|
|
142
|
+
// that produced a rawLeg (null rawLeg = no wave = no money = no row), plus
|
|
143
|
+
// one 'repair' row for a launched ch4 (pushed below, after the ch4 block).
|
|
144
|
+
// The eventual SUCCESSFUL attempt's leg is never pushed here — it becomes
|
|
145
|
+
// the primary 'chair' row (wasChair:true) via run.js's own chairStats.
|
|
146
|
+
const chairRows = [];
|
|
120
147
|
const recordAttempt = (attempt, waveId, model) => {
|
|
121
148
|
const cls = classifyChairAttempt(attempt.rawLeg, attempt.errorDoc);
|
|
122
149
|
chairAttempts.push({ waveId, model, outcome: cls.outcome, reason: cls.reason });
|
|
123
150
|
// Checkpointed HERE, before the caller's own isAbortExit bail — a mid-walk
|
|
124
151
|
// kill must not lose the attempts already resolved (spec §8 kill-mid-walk).
|
|
125
152
|
runState.checkpoint(o.runDir, { chairAttempts });
|
|
153
|
+
if (!attempt.leg && attempt.rawLeg) {
|
|
154
|
+
chairRows.push(buildRunStatsEntry({
|
|
155
|
+
leg: attempt.rawLeg, model, role: 'chair-attempt', wasChair: false,
|
|
156
|
+
}));
|
|
157
|
+
}
|
|
126
158
|
};
|
|
127
159
|
if (overBudget()) {
|
|
128
160
|
// Ceiling hit after the tally is computable: skip the chair, write the
|
|
@@ -143,12 +175,14 @@ async function runChair(ctx, { packet, degrade, statsFn, isSignalled }) {
|
|
|
143
175
|
emitStageStarted(o.runDir, o.runId, 'chair', null, o.follow);
|
|
144
176
|
// Fallback chain (spec §4): retry same chair once → promote best
|
|
145
177
|
// non-bench model from the ledger → give up (no Claude fallback headless).
|
|
146
|
-
|
|
147
|
-
|
|
178
|
+
const waveId1 = `${o.runId}-ch1`;
|
|
179
|
+
let attempt = await attemptChair(o.chair, waveId1);
|
|
180
|
+
recordAttempt(attempt, waveId1, o.chair);
|
|
148
181
|
if (isAbortExit(attempt.exitCode) || isSignalled()) { return bail(attempt.exitCode || isSignalled()); }
|
|
149
182
|
if (!attempt.leg && !overBudget()) {
|
|
150
|
-
|
|
151
|
-
|
|
183
|
+
const waveId2 = `${o.runId}-ch2`;
|
|
184
|
+
attempt = await attemptChair(o.chair, waveId2);
|
|
185
|
+
recordAttempt(attempt, waveId2, o.chair);
|
|
152
186
|
if (isAbortExit(attempt.exitCode) || isSignalled()) { return bail(attempt.exitCode || isSignalled()); }
|
|
153
187
|
}
|
|
154
188
|
if (attempt.leg) { actualChair = o.chair; }
|
|
@@ -157,8 +191,9 @@ async function runChair(ctx, { packet, degrade, statsFn, isSignalled }) {
|
|
|
157
191
|
try { statsRows = statsFn(); } catch { /* no ledger yet */ }
|
|
158
192
|
const fallback = pickFallbackChair(statsRows, o.models, o.chair);
|
|
159
193
|
if (fallback) {
|
|
160
|
-
|
|
161
|
-
|
|
194
|
+
const waveId3 = `${o.runId}-ch3`;
|
|
195
|
+
attempt = await attemptChair(fallback, waveId3);
|
|
196
|
+
recordAttempt(attempt, waveId3, fallback);
|
|
162
197
|
if (isAbortExit(attempt.exitCode) || isSignalled()) { return bail(attempt.exitCode || isSignalled()); }
|
|
163
198
|
if (attempt.leg) { actualChair = fallback; }
|
|
164
199
|
}
|
|
@@ -180,19 +215,37 @@ async function runChair(ctx, { packet, degrade, statsFn, isSignalled }) {
|
|
|
180
215
|
// ---- Chair VERDICT line (one repair re-prompt, spec §5) ----
|
|
181
216
|
let overallVerdict = chairText ? parseChairVerdict(chairText) : null;
|
|
182
217
|
if (chairText && !overallVerdict && !overBudget()) {
|
|
183
|
-
|
|
218
|
+
const waveId4 = `${o.runId}-ch4`;
|
|
219
|
+
runState.appendStageWave(o.runDir, 'chair', waveId4);
|
|
184
220
|
const repair = await launchers.launchSolo({
|
|
185
221
|
// ⚠️ LC-12: the synthesis rides along. The chair leg SUCCEEDED — only the
|
|
186
222
|
// VERDICT line is missing — so a fresh repair session that cannot see the
|
|
187
223
|
// synthesis is picking a verdict on an artifact it has never read.
|
|
188
224
|
model: actualChair, prompt: stage2.buildChairRepairPrompt({ synthesis: chairText }),
|
|
189
|
-
project: o.runDir, waveId:
|
|
225
|
+
project: o.runDir, waveId: waveId4,
|
|
190
226
|
timeout: o.timeout, gateway: o.gateway, noValidateModel: o.noValidateModel,
|
|
191
227
|
noCostGate: o.noCostGate,
|
|
192
228
|
councilRunId: o.runId, councilName: o.councilName,
|
|
229
|
+
tag: o.tag, // v4.7 F8 D16: rides the same forward as councilRunId/councilName.
|
|
193
230
|
});
|
|
194
231
|
addWave(repair.wave);
|
|
195
232
|
if (isAbortExit(repair.exitCode) || isSignalled()) { return bail(repair.exitCode || isSignalled()); }
|
|
233
|
+
// repair.leg is the raw leg document — launchSolo DOES null it, but only
|
|
234
|
+
// on a wave-less failure (a pre-flight refusal with no wave launched at
|
|
235
|
+
// all: run-launch.js's launchSolo derives `leg` from `wave.legs[0]`, so
|
|
236
|
+
// no wave means no leg, no waveId, no money spent — the errata E3 "no
|
|
237
|
+
// leg = no wave = no money = no row" case). A wave that DID launch
|
|
238
|
+
// always yields a leg document, whatever its status. The `if
|
|
239
|
+
// (repair.leg)` guard below is therefore load-bearing on that exact
|
|
240
|
+
// distinction: a launched ch4 (a leg document exists, whatever its
|
|
241
|
+
// status) gets its own row so the repair's spend is attributed even when
|
|
242
|
+
// it never supplies a VERDICT; a ch4 that never even launched gets no
|
|
243
|
+
// row at all, because there is nothing billed to attribute.
|
|
244
|
+
if (repair.leg) {
|
|
245
|
+
chairRows.push(buildRunStatsEntry({
|
|
246
|
+
leg: repair.leg, model: actualChair, role: 'repair', wasChair: false,
|
|
247
|
+
}));
|
|
248
|
+
}
|
|
196
249
|
overallVerdict = parseChairVerdict((repair.leg && repair.leg.summary) || '');
|
|
197
250
|
chairConformance = overallVerdict ? 'repaired' : 'unstructured';
|
|
198
251
|
}
|
|
@@ -213,6 +266,12 @@ async function runChair(ctx, { packet, degrade, statsFn, isSignalled }) {
|
|
|
213
266
|
|
|
214
267
|
return {
|
|
215
268
|
aborted: null, chairLeg, actualChair, chairText, chairConformance, overallVerdict,
|
|
269
|
+
// Additive (v4.7 D2): chairRows holds the non-primary rows (attempts +
|
|
270
|
+
// repair); chairAttempts is handed back too so run.js can key the
|
|
271
|
+
// give-up row on "the walk actually happened" without re-reading disk —
|
|
272
|
+
// NOT on chairRows, since attempts that die pre-wave record an outcome
|
|
273
|
+
// but produce no row (errata E3: no wave = no money = no row).
|
|
274
|
+
chairRows, chairAttempts,
|
|
216
275
|
};
|
|
217
276
|
}
|
|
218
277
|
|
|
@@ -13,62 +13,38 @@ const fs = require('fs');
|
|
|
13
13
|
const path = require('path');
|
|
14
14
|
const dbrief = require('./briefings-debate');
|
|
15
15
|
const { parseDebateDefense, parseRevote } = require('./parse-stage2');
|
|
16
|
-
const { applyDebate, debateRunStatsRows, PAST_TENSE
|
|
16
|
+
const { applyDebate, debateRunStatsRows, PAST_TENSE,
|
|
17
|
+
allNoResponse, nothingToDebate, disputingJudges, debateTargets, bundleFor } = require('./debate');
|
|
17
18
|
const { materializeDebate } = require('./run-launch');
|
|
18
19
|
const { tally } = require('./tally');
|
|
19
20
|
const { isAbortExit } = require('./run-stages');
|
|
20
21
|
const runState = require('./run-state');
|
|
21
22
|
const { emitStageStarted } = require('../observe/events');
|
|
22
23
|
|
|
23
|
-
/** Spec §5.7 fallback: a dead/unparseable defense means every bundled id's original stands. */
|
|
24
|
-
function allNoResponse(ids) {
|
|
25
|
-
const byId = {};
|
|
26
|
-
for (const id of ids) { byId[id] = { action: 'no-response' }; }
|
|
27
|
-
return byId;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/** True when there is nothing to challenge (spec §5.1). */
|
|
31
|
-
function nothingToDebate(provisionalRecord) {
|
|
32
|
-
if (!provisionalRecord || provisionalRecord.judged === false) { return true; }
|
|
33
|
-
const n = provisionalRecord.findings.filter(f => f.tier === 'Contested' || f.tier === 'Disputed').length;
|
|
34
|
-
return n === 0;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/** Judges whose provisional adjudications dispute at least one bundled id. */
|
|
38
|
-
function disputingJudges(provisionalRecord, bundledIds) {
|
|
39
|
-
const ids = new Set(bundledIds);
|
|
40
|
-
const judges = new Set();
|
|
41
|
-
for (const f of provisionalRecord.findings) {
|
|
42
|
-
if (!ids.has(f.id)) { continue; }
|
|
43
|
-
for (const adj of f.adjudications || []) {
|
|
44
|
-
if (adj.verdict === 'dispute') { judges.add(adj.judge); }
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
return [...judges];
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/** Group Contested+Disputed findings by raiser (defense targets). */
|
|
51
|
-
function debateTargets(provisionalRecord, tallyInput) {
|
|
52
|
-
const claimById = new Map(tallyInput.findings.map(f => [f.id, f]));
|
|
53
|
-
const byRaiser = {};
|
|
54
|
-
const previousTier = {};
|
|
55
|
-
for (const f of provisionalRecord.findings) {
|
|
56
|
-
if (f.tier !== 'Contested' && f.tier !== 'Disputed') { continue; }
|
|
57
|
-
previousTier[f.id] = f.tier;
|
|
58
|
-
const src = claimById.get(f.id) || {};
|
|
59
|
-
const peerVerdicts = (f.adjudications || []).filter(a => a.judge !== f.raiser).map(a => a.verdict);
|
|
60
|
-
(byRaiser[f.raiser] = byRaiser[f.raiser] || []).push({ id: f.id, claim: src.claim,
|
|
61
|
-
severity: f.severity, location: src.location, peerVerdicts, disputeReasons: [] });
|
|
62
|
-
}
|
|
63
|
-
return { byRaiser, previousTier };
|
|
64
|
-
}
|
|
65
|
-
|
|
66
24
|
/** Common launch options for every debate leg (judge-isolated `_scratch` cwd). */
|
|
67
25
|
function legOpts(ctx, waveId) {
|
|
68
26
|
return { project: ctx.scratchDir, waveId, timeout: ctx.o.timeout, gateway: ctx.o.gateway,
|
|
69
27
|
noValidateModel: ctx.o.noValidateModel, noCostGate: ctx.o.noCostGate,
|
|
70
28
|
// v4.3 Task 3 (spec §7.2): attribution ids for every defense/re-vote leg.
|
|
71
|
-
councilRunId: ctx.o.runId, councilName: ctx.o.councilName
|
|
29
|
+
councilRunId: ctx.o.runId, councilName: ctx.o.councilName,
|
|
30
|
+
tag: ctx.o.tag }; // v4.7 F8 D16: rides the same forward as councilRunId/councilName.
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* v4.7 D2/E4: normalize a raw (possibly leg-absent) leg into the shape
|
|
35
|
+
* debateRunStatsRows' superseded/repair lists expect. Same never-invent-a-waveId
|
|
36
|
+
* discipline as buildRunStatsEntry (run-assemble.js) — only spread `waveId` when
|
|
37
|
+
* the leg genuinely carries one — but keyed on an explicit `model` (the raiser or
|
|
38
|
+
* judge identity), since a leg-absent attempt has no `.model` of its own to read.
|
|
39
|
+
* Threads resolvedModel (the raw leg's .model, the executable id) emit-only-when-set — v4.7 GOA-7 D8.
|
|
40
|
+
*/
|
|
41
|
+
function legRow(model, leg, conformance) {
|
|
42
|
+
return leg
|
|
43
|
+
? { model, status: leg.status, durationMs: typeof leg.durationMs === 'number' ? leg.durationMs : null,
|
|
44
|
+
usage: leg.usage || null, conformance, summary: leg.summary || '',
|
|
45
|
+
...(leg.waveId ? { waveId: leg.waveId } : {}),
|
|
46
|
+
...(leg.model ? { resolvedModel: leg.model } : {}) }
|
|
47
|
+
: { model, status: 'error', durationMs: null, usage: null, conformance, summary: '' };
|
|
72
48
|
}
|
|
73
49
|
|
|
74
50
|
async function runDefenseSolo(ctx, raiser, findings, idx) {
|
|
@@ -90,6 +66,12 @@ async function runDefenseSolo(ctx, raiser, findings, idx) {
|
|
|
90
66
|
let parsed = leg ? parseDebateDefense(leg.summary, expectedIds)
|
|
91
67
|
: { ok: false, byId: allNoResponse(expectedIds), errors: [{ code: 'DEAD_LEG', detail: 'no summary' }] };
|
|
92
68
|
let conformance = leg ? 'clean' : 'unstructured';
|
|
69
|
+
// v4.7 D2/E4: the repair's loser leg — the ORIGINAL when the repair produced a
|
|
70
|
+
// usable (complete) leg (today's leg-swap below is unchanged), or the failed
|
|
71
|
+
// repair attempt itself when it did not — retained so runDebate can turn it
|
|
72
|
+
// into an extra debate-defense runStats row. Both stay null when no repair is
|
|
73
|
+
// attempted at all (today's single-row shape, byte-identical).
|
|
74
|
+
let supersededLeg = null, repairLeg = null;
|
|
93
75
|
if (leg && !parsed.ok) {
|
|
94
76
|
const repairId = `${waveId}r`;
|
|
95
77
|
runState.appendStageWave(ctx.o.runDir, 'debate-defense', repairId);
|
|
@@ -103,13 +85,17 @@ async function runDefenseSolo(ctx, raiser, findings, idx) {
|
|
|
103
85
|
const leg2 = res2.leg && res2.leg.status === 'complete' ? res2.leg : null;
|
|
104
86
|
parsed = leg2 ? parseDebateDefense(leg2.summary, expectedIds) : parsed;
|
|
105
87
|
conformance = parsed.ok ? 'repaired' : 'unstructured';
|
|
106
|
-
if (leg2) { leg = leg2; }
|
|
88
|
+
if (leg2) { supersededLeg = legRow(raiser, leg, 'unstructured'); leg = leg2; }
|
|
89
|
+
else { repairLeg = legRow(raiser, res2.leg, 'unstructured'); }
|
|
107
90
|
}
|
|
108
91
|
// A dead leg (no complete summary) OR an 'unstructured' conformance after the one
|
|
109
92
|
// repair is a debate degradation (spec §5.7) — surfaced via the returned leg.
|
|
110
93
|
const stub = { model: raiser, status: 'error', durationMs: null, usage: null, conformance: 'unstructured', summary: '' };
|
|
111
94
|
return { raiser, byId: parsed.byId,
|
|
112
|
-
leg: leg ? { model: raiser, status: leg.status, durationMs: leg.durationMs, usage: leg.usage,
|
|
95
|
+
leg: leg ? { model: raiser, status: leg.status, durationMs: leg.durationMs, usage: leg.usage,
|
|
96
|
+
conformance, summary: leg.summary, waveId: leg.waveId,
|
|
97
|
+
...(leg.model ? { resolvedModel: leg.model } : {}) } : stub,
|
|
98
|
+
supersededLeg, repairLeg };
|
|
113
99
|
}
|
|
114
100
|
|
|
115
101
|
async function runRevoteWave(ctx, judges, bundleFindings) {
|
|
@@ -131,6 +117,9 @@ async function runRevoteWave(ctx, judges, bundleFindings) {
|
|
|
131
117
|
ctx.addWave(res.wave);
|
|
132
118
|
if (isAbortExit(res.exitCode)) { return { aborted: res.exitCode }; }
|
|
133
119
|
const byJudge = {}, legs = [];
|
|
120
|
+
// v4.7 D2/E4: mirrors runDefenseSolo's supersededLeg/repairLeg — one list each,
|
|
121
|
+
// accumulated across every judge in this wave (most judges contribute neither).
|
|
122
|
+
const supersededLegs = [], repairLegs = [];
|
|
134
123
|
for (const leg of ((res.wave && res.wave.legs) || [])) {
|
|
135
124
|
// The council ALIAS, not the resolved executable id — runStats rows join
|
|
136
125
|
// meta.models by exact string (run-assemble.js's buildRunStatsEntry).
|
|
@@ -154,27 +143,15 @@ async function runRevoteWave(ctx, judges, bundleFindings) {
|
|
|
154
143
|
conformance = parsed.ok ? 'repaired' : 'unstructured';
|
|
155
144
|
// Symmetric with runDefenseSolo's `if (leg2) { leg = leg2; }` — otherwise
|
|
156
145
|
// revote-<model>.md and the runStats row keep the PRE-repair output.
|
|
157
|
-
if (leg2) { outLeg = leg2; }
|
|
146
|
+
if (leg2) { supersededLegs.push(legRow(judge, leg, 'unstructured')); outLeg = leg2; }
|
|
147
|
+
else { repairLegs.push(legRow(judge, r2.leg, 'unstructured')); }
|
|
158
148
|
}
|
|
159
149
|
byJudge[judge] = parsed.byId;
|
|
160
|
-
legs.push({ model: judge, status: outLeg.status, durationMs: outLeg.durationMs, usage: outLeg.usage,
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/** The re-vote bundle: defended-or-amended findings ONLY (spec §5.1 — withdrawn never appear). */
|
|
166
|
-
function bundleFor(defenseResults, tallyInput) {
|
|
167
|
-
const out = [];
|
|
168
|
-
for (const dr of defenseResults) {
|
|
169
|
-
for (const [id, resp] of Object.entries(dr.byId)) {
|
|
170
|
-
if (resp.action !== 'defend' && resp.action !== 'amend') { continue; }
|
|
171
|
-
const src = tallyInput.findings.find(f => f.id === id) || {};
|
|
172
|
-
out.push({ id, severity: src.severity, amended: resp.action === 'amend',
|
|
173
|
-
claim: resp.action === 'amend' ? resp.claim : src.claim,
|
|
174
|
-
argument: resp.argument || 'defended without extra argument' });
|
|
175
|
-
}
|
|
150
|
+
legs.push({ model: judge, status: outLeg.status, durationMs: outLeg.durationMs, usage: outLeg.usage,
|
|
151
|
+
conformance, summary: outLeg.summary || '', waveId: outLeg.waveId,
|
|
152
|
+
...(outLeg.model ? { resolvedModel: outLeg.model } : {}) });
|
|
176
153
|
}
|
|
177
|
-
return
|
|
154
|
+
return { byJudge, legs, supersededLegs, repairLegs };
|
|
178
155
|
}
|
|
179
156
|
|
|
180
157
|
/**
|
|
@@ -221,7 +198,7 @@ async function runDebate(ctx, { provisionalRecord, tallyInput }) {
|
|
|
221
198
|
const stampedInput = { ...tallyInput, findings: tallyInput.findings.map(f => ({ ...f, previousTier: previousTier[f.id] })) };
|
|
222
199
|
|
|
223
200
|
// ---- Re-vote mini-wave (disputing judges only) ----
|
|
224
|
-
let revoteByJudge = {}, revoteLegs = [];
|
|
201
|
+
let revoteByJudge = {}, revoteLegs = [], revoteSuperseded = [], revoteRepairs = [];
|
|
225
202
|
const defendedOrAmended = bundleFor(defenseResults, tallyInput);
|
|
226
203
|
const judges = disputingJudges(provisionalRecord, defendedOrAmended.map(f => f.id));
|
|
227
204
|
// A re-vote is warranted only when something was defended/amended AND ≥1 judge disputed it.
|
|
@@ -238,6 +215,8 @@ async function runDebate(ctx, { provisionalRecord, tallyInput }) {
|
|
|
238
215
|
if (rv.aborted) { return { aborted: rv.aborted, contested, disputed }; }
|
|
239
216
|
revoteByJudge = rv.byJudge;
|
|
240
217
|
revoteLegs = rv.legs;
|
|
218
|
+
revoteSuperseded = rv.supersededLegs;
|
|
219
|
+
revoteRepairs = rv.repairLegs;
|
|
241
220
|
// revote-<model>.md per surviving judge leg, mirroring rebuttal-<model>.md
|
|
242
221
|
// (spec §5.1 'raw outputs revote-<model>.md').
|
|
243
222
|
materializeDebate(ctx.o.runDir, revoteLegs, 'revote');
|
|
@@ -247,7 +226,12 @@ async function runDebate(ctx, { provisionalRecord, tallyInput }) {
|
|
|
247
226
|
const { input: debatedInput, debateFindings } = applyDebate({
|
|
248
227
|
tallyInput: stampedInput, provisionalRecord, defenseByRaiser, revoteByJudge });
|
|
249
228
|
debatedInput.runStats = [...(debatedInput.runStats || []),
|
|
250
|
-
...debateRunStatsRows({ defenseLegs: defenseResults.map(d => d.leg), revoteLegs
|
|
229
|
+
...debateRunStatsRows({ defenseLegs: defenseResults.map(d => d.leg), revoteLegs,
|
|
230
|
+
// v4.7 D2/E4: the retained loser legs from every raiser's defense repair
|
|
231
|
+
// plus every judge's re-vote repair — same append, no new channel into
|
|
232
|
+
// buildTallyInput.
|
|
233
|
+
supersededLegs: [...defenseResults.map(d => d.supersededLeg).filter(Boolean), ...revoteSuperseded],
|
|
234
|
+
repairLegs: [...defenseResults.map(d => d.repairLeg).filter(Boolean), ...revoteRepairs] })];
|
|
251
235
|
|
|
252
236
|
// verdictChanges: findings whose tier moved from provisional to debated.
|
|
253
237
|
const provTierById = new Map(provisionalRecord.findings.map(f => [f.id, f.tier]));
|
|
@@ -63,10 +63,13 @@ function createLaunchers(deps = {}) {
|
|
|
63
63
|
/**
|
|
64
64
|
* @param {{models: string[], prompt: string, project: string, waveId: string,
|
|
65
65
|
* timeout?: number, gateway?: string, noValidateModel?: boolean, agent?: string,
|
|
66
|
-
* councilRunId?: string, councilName?: string,
|
|
66
|
+
* councilRunId?: string, councilName?: string, tag?: string, fallback?: object,
|
|
67
|
+
* catalog?: Array}} opts
|
|
67
68
|
* councilRunId/councilName (v4.3 Task 3, spec §7.2) are additive attribution
|
|
68
69
|
* ids forwarded verbatim into the runFanout call so it can stamp them onto
|
|
69
|
-
* every leg.
|
|
70
|
+
* every leg. tag (v4.7 F8 D16) rides the same forward — every call site
|
|
71
|
+
* below that sets councilRunId/councilName sets `tag: o.tag` alongside it.
|
|
72
|
+
* fallback/catalog (v4.3 Task 18, spec §6.2) are likewise
|
|
70
73
|
* additive/opt-in — omitted by callers that must never substitute (the
|
|
71
74
|
* chair, debate legs); run-stages.js's Stage-1/Stage-2 launches pass them.
|
|
72
75
|
* @returns {Promise<{wave: object|null, exitCode: number}>}
|
|
@@ -111,6 +114,10 @@ function createLaunchers(deps = {}) {
|
|
|
111
114
|
noValidateModel: opts.noValidateModel,
|
|
112
115
|
councilRunId: opts.councilRunId,
|
|
113
116
|
councilName: opts.councilName,
|
|
117
|
+
// v4.7 F8 D16: rides the SAME forward as councilRunId/councilName above —
|
|
118
|
+
// undefined when no --tag, so stampLegAttribution's `if (options.tag)`
|
|
119
|
+
// guard (fanout-wave-io.js) simply no-ops, byte-identical to today.
|
|
120
|
+
tag: opts.tag,
|
|
114
121
|
// v4.3 Task 18 (spec §6.2): additive/opt-in. Callers that must never
|
|
115
122
|
// substitute (run-chair.js, run-debate.js) simply omit these — runLeg's
|
|
116
123
|
// fallback path only activates when `fallback.enabled` is true.
|
package/src/council/run-retry.js
CHANGED
|
@@ -141,7 +141,8 @@ function briefingFor(o, unit) {
|
|
|
141
141
|
async function retryStage1Losses(ctx, { deadWaves = [], deadLegs = [], counts = { reviewed: 0, total: 0 } } = {}) {
|
|
142
142
|
const { o, launchers } = ctx;
|
|
143
143
|
const out = { aborted: null, recoveredLegs: [], stillDeadNotes: [],
|
|
144
|
-
stillDeadWaves: [], stillDeadLegs: [], skippedDeadWaves: [], skippedDeadLegs: []
|
|
144
|
+
stillDeadWaves: [], stillDeadLegs: [], skippedDeadWaves: [], skippedDeadLegs: [],
|
|
145
|
+
stillDeadRetryLegs: [] };
|
|
145
146
|
|
|
146
147
|
for (const unit of groupStage1Losses(o, deadWaves, deadLegs)) {
|
|
147
148
|
// Task-4 review hardening: a unit this pass cannot even ATTEMPT — an
|
|
@@ -168,6 +169,7 @@ async function retryStage1Losses(ctx, { deadWaves = [], deadLegs = [], counts =
|
|
|
168
169
|
const common = { project: o.runDir, timeout: o.timeout, gateway: o.gateway,
|
|
169
170
|
noValidateModel: o.noValidateModel, noCostGate: o.noCostGate,
|
|
170
171
|
councilRunId: o.runId, councilName: o.councilName,
|
|
172
|
+
tag: o.tag, // v4.7 F8 D16: rides the same forward as councilRunId/councilName.
|
|
171
173
|
fallback: o.fallback, catalog: o.catalog,
|
|
172
174
|
waveId: unit.waveId, retryOfWaveId: unit.retryOfWaveId, prompt: briefingFor(o, unit) };
|
|
173
175
|
// Dispatch by UNIT TYPE, not model count (spec §4: bench is always a wave —
|
|
@@ -234,6 +236,7 @@ async function retryStage1Losses(ctx, { deadWaves = [], deadLegs = [], counts =
|
|
|
234
236
|
data: { seat, retryWaveId: unit.waveId, retryOfWaveId: unit.retryOfWaveId, firstFailure: ff } });
|
|
235
237
|
} else {
|
|
236
238
|
out.stillDeadNotes.push(retryLegStillDeadNote(seat, ff, leg, unit, counts));
|
|
239
|
+
out.stillDeadRetryLegs.push(leg);
|
|
237
240
|
if (ff && ff.class === 'wave') {
|
|
238
241
|
if (!lostWaveSeats.has(ff.waveId)) { lostWaveSeats.set(ff.waveId, []); }
|
|
239
242
|
lostWaveSeats.get(ff.waveId).push(seat);
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stage-1 launch pass for the council engine.
|
|
3
|
+
*
|
|
4
|
+
* launchStage1 moved verbatim from run-stages.js (v4.7 PR1 Task 1) to free
|
|
5
|
+
* gate headroom before the row-per-launch edits land there.
|
|
6
|
+
*/
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
const briefings = require('./briefings');
|
|
10
|
+
const runState = require('./run-state');
|
|
11
|
+
const { isAbortExit } = require('./run-launch');
|
|
12
|
+
|
|
13
|
+
/** Launch all Stage-1 legs (wave + critic/lens solos), collect run docs. */
|
|
14
|
+
async function launchStage1(ctx) {
|
|
15
|
+
const { o, launchers } = ctx;
|
|
16
|
+
// `noCostGate` rides EVERY launch object in this file (here, the findings
|
|
17
|
+
// repair, the judge wave, the judge repair) — see run-launch.js's fanout call.
|
|
18
|
+
const common = {
|
|
19
|
+
project: o.runDir, timeout: o.timeout, gateway: o.gateway,
|
|
20
|
+
noValidateModel: o.noValidateModel, noCostGate: o.noCostGate,
|
|
21
|
+
// v4.3 Task 3 (spec §7.2): attribution ids, forwarded verbatim to runFanout
|
|
22
|
+
// via run-launch.js so every Stage-1 leg's ledger row carries them.
|
|
23
|
+
councilRunId: o.runId, councilName: o.councilName,
|
|
24
|
+
tag: o.tag, // v4.7 F8 D16: rides the same forward as councilRunId/councilName.
|
|
25
|
+
// v4.3 Task 18 (spec §6.2): fallback chains apply to STAGE legs only —
|
|
26
|
+
// the chair (run-chair.js) and debate legs (run-debate.js) never receive
|
|
27
|
+
// this, so they never substitute via chains.
|
|
28
|
+
fallback: o.fallback, catalog: o.catalog,
|
|
29
|
+
};
|
|
30
|
+
const launches = [];
|
|
31
|
+
const seated = []; // parallel to `launches`: what each one was SUPPOSED to seat
|
|
32
|
+
// Record every sub-wave BEFORE it launches: `amicus abort` cascades over
|
|
33
|
+
// stages[].waveIds, so an id written after the launch leaves that leg
|
|
34
|
+
// reachable only by the pid kill (no per-leg abort marker).
|
|
35
|
+
const record = (waveId) => runState.appendStageWave(o.runDir, 'stage1', waveId);
|
|
36
|
+
if (o.lenses) {
|
|
37
|
+
o.models.forEach((m, i) => {
|
|
38
|
+
const waveId = `${o.runId}-l${i + 1}`;
|
|
39
|
+
record(waveId);
|
|
40
|
+
seated.push({ waveId, models: [m] });
|
|
41
|
+
launches.push(launchers.launchSolo({
|
|
42
|
+
...common, model: m, waveId,
|
|
43
|
+
prompt: briefings.buildLensBriefing({ lens: o.lenses[i], briefing: o.briefing, date: o.date }),
|
|
44
|
+
}));
|
|
45
|
+
});
|
|
46
|
+
} else {
|
|
47
|
+
const seats = o.models.filter(m => m !== o.critic);
|
|
48
|
+
if (seats.length > 0) {
|
|
49
|
+
record(`${o.runId}-s1`);
|
|
50
|
+
seated.push({ waveId: `${o.runId}-s1`, models: seats.slice() });
|
|
51
|
+
launches.push(launchers.launchWave({
|
|
52
|
+
...common, models: seats, waveId: `${o.runId}-s1`,
|
|
53
|
+
prompt: briefings.buildSeatBriefing({ briefing: o.briefing, date: o.date }),
|
|
54
|
+
}));
|
|
55
|
+
}
|
|
56
|
+
if (o.critic) {
|
|
57
|
+
record(`${o.runId}-c1`);
|
|
58
|
+
seated.push({ waveId: `${o.runId}-c1`, models: [o.critic] });
|
|
59
|
+
launches.push(launchers.launchSolo({
|
|
60
|
+
...common, model: o.critic, waveId: `${o.runId}-c1`,
|
|
61
|
+
prompt: briefings.buildCriticBriefing({ briefing: o.briefing, date: o.date }),
|
|
62
|
+
}));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const results = await Promise.all(launches);
|
|
66
|
+
let aborted = null;
|
|
67
|
+
const legs = [];
|
|
68
|
+
const deadWaves = [];
|
|
69
|
+
results.forEach((r, i) => {
|
|
70
|
+
ctx.addWave(r.wave);
|
|
71
|
+
const abort = isAbortExit(r.exitCode);
|
|
72
|
+
if (abort) { aborted = r.exitCode; }
|
|
73
|
+
const got = (r.wave && Array.isArray(r.wave.legs)) ? r.wave.legs : [];
|
|
74
|
+
legs.push(...got);
|
|
75
|
+
// ⚠️ Step 10's uncovered half. A wave that died BEFORE its legs (the server
|
|
76
|
+
// never started; `database is locked`) contributes NOTHING to `legs`, so
|
|
77
|
+
// deadLegs cannot see it either — which is how run v441plan01 recorded
|
|
78
|
+
// stage1 'complete' with four seats missing and no trace of them. In lens
|
|
79
|
+
// mode every seat is its own wave, so a run could lose seats and still exit
|
|
80
|
+
// 0; the quorum gate only catches the non-lens seat wave. A budget refusal
|
|
81
|
+
// has its own louder channel already (run-budget.noteBudgetRefusal) and
|
|
82
|
+
// must not be double-counted here.
|
|
83
|
+
if (got.length > 0 || abort) { return; }
|
|
84
|
+
if (r.errorDoc && r.errorDoc.code === 'BUDGET_EXCEEDED') { return; }
|
|
85
|
+
deadWaves.push({
|
|
86
|
+
waveId: seated[i].waveId, models: seated[i].models,
|
|
87
|
+
reason: (r.wave && (r.wave.reason || r.wave.error))
|
|
88
|
+
|| (r.errorDoc && r.errorDoc.message) || 'the wave produced no legs',
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
return { aborted, legs, deadWaves };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
module.exports = { launchStage1 };
|
|
@@ -23,6 +23,7 @@ const stage2 = require('./briefings-stage2');
|
|
|
23
23
|
const { parseJudgeOutput } = require('./parse-stage2');
|
|
24
24
|
const { sanitizeName, isAbortExit } = require('./run-launch');
|
|
25
25
|
const runState = require('./run-state');
|
|
26
|
+
const { buildRunStatsEntry } = require('./run-assemble');
|
|
26
27
|
|
|
27
28
|
/**
|
|
28
29
|
* Stage 2: shared anonymized bundle → judge wave in _scratch → parse + repair.
|
|
@@ -31,7 +32,12 @@ const runState = require('./run-state');
|
|
|
31
32
|
* extraLabeled?: Array<{label: string, text: string}>}} args
|
|
32
33
|
* `extraLabeled` (v4.1 §4.4) are labeled reviews sourced from a FILE rather than
|
|
33
34
|
* a leg (the Claude review): they join the judged BUNDLE, never the judge ROSTER.
|
|
34
|
-
* @returns {Promise<{aborted: number|null, judgeResults: Array}>}
|
|
35
|
+
* @returns {Promise<{aborted: number|null, judgeResults: Array, extraRows: Array}>}
|
|
36
|
+
* `extraRows` (v4.7 D2, mirroring runStage1's channel) is one `role:'repair'`
|
|
37
|
+
* row per `-q<N>` judge-repair solo (error status when the repair itself
|
|
38
|
+
* failed) — the judge's own judgeResults entry keeps attributing its ORIGINAL
|
|
39
|
+
* Stage-2 wave leg throughout (the #83 comment below), so a repair never
|
|
40
|
+
* overwrites the primary judge row; it only adds this separate one.
|
|
35
41
|
*/
|
|
36
42
|
async function runStage2(ctx, { reviews, labels, globalFindings, extraLabeled = [] }) {
|
|
37
43
|
const { o } = ctx;
|
|
@@ -59,12 +65,18 @@ async function runStage2(ctx, { reviews, labels, globalFindings, extraLabeled =
|
|
|
59
65
|
timeout: o.timeout, gateway: o.gateway, noValidateModel: o.noValidateModel,
|
|
60
66
|
noCostGate: o.noCostGate,
|
|
61
67
|
councilRunId: o.runId, councilName: o.councilName,
|
|
68
|
+
tag: o.tag, // v4.7 F8 D16: rides the same forward as councilRunId/councilName.
|
|
62
69
|
fallback: o.fallback, catalog: o.catalog,
|
|
63
70
|
});
|
|
64
71
|
ctx.addWave(wave);
|
|
65
|
-
if (isAbortExit(exitCode)) { return { aborted: exitCode, judgeResults: [] }; }
|
|
72
|
+
if (isAbortExit(exitCode)) { return { aborted: exitCode, judgeResults: [], extraRows: [] }; }
|
|
66
73
|
|
|
67
74
|
const judgeResults = [];
|
|
75
|
+
// v4.7 D2: every judge-repair launch is a billed leg of its own, distinct from
|
|
76
|
+
// the judge's original Stage-2 wave leg it is trying to fix — it gets its own
|
|
77
|
+
// row so its cost is never folded into, or lost from, the judge's row (mirrors
|
|
78
|
+
// runStage1's -p<N> extraRows, ./run-stages.js:117-120).
|
|
79
|
+
const extraRows = [];
|
|
68
80
|
let repairSeq = 0;
|
|
69
81
|
for (const leg of (wave && wave.legs) || []) {
|
|
70
82
|
const judge = leg.modelInput || leg.model;
|
|
@@ -95,10 +107,19 @@ async function runStage2(ctx, { reviews, labels, globalFindings, extraLabeled =
|
|
|
95
107
|
project: ctx.scratchDir, waveId, timeout: o.timeout,
|
|
96
108
|
gateway: o.gateway, noValidateModel: o.noValidateModel, noCostGate: o.noCostGate,
|
|
97
109
|
councilRunId: o.runId, councilName: o.councilName,
|
|
110
|
+
tag: o.tag, // v4.7 F8 D16: rides the same forward as councilRunId/councilName.
|
|
98
111
|
fallback: o.fallback, catalog: o.catalog,
|
|
99
112
|
});
|
|
100
113
|
ctx.addWave(solo.wave);
|
|
101
|
-
if (isAbortExit(solo.exitCode)) {
|
|
114
|
+
if (isAbortExit(solo.exitCode)) {
|
|
115
|
+
// Abort paths add no rows (aborted runs never reach tally) — extraRows
|
|
116
|
+
// is returned only for shape consistency, never read past this point.
|
|
117
|
+
return { aborted: solo.exitCode, judgeResults, extraRows };
|
|
118
|
+
}
|
|
119
|
+
// Every -q<N> launch gets a row — INCLUDING a repair that failed: the
|
|
120
|
+
// error status rides naturally off solo.leg (null/'error'-status leg ⇒
|
|
121
|
+
// buildRunStatsEntry's own never-invent defaults), no special-casing needed.
|
|
122
|
+
extraRows.push(buildRunStatsEntry({ leg: solo.leg, model: judge, role: 'repair', wasChair: false }));
|
|
102
123
|
const out = (solo.leg && solo.leg.summary) || '';
|
|
103
124
|
if (out.trim()) { judging = out; }
|
|
104
125
|
parsed = parseJudgeOutput(out, parseCtx);
|
|
@@ -120,7 +141,7 @@ async function runStage2(ctx, { reviews, labels, globalFindings, extraLabeled =
|
|
|
120
141
|
judgeResults.push({ judge, ok: true, order, adjudications: parsed.adjudications, conformance,
|
|
121
142
|
leg: leg || null });
|
|
122
143
|
}
|
|
123
|
-
return { aborted: null, judgeResults };
|
|
144
|
+
return { aborted: null, judgeResults, extraRows };
|
|
124
145
|
}
|
|
125
146
|
|
|
126
147
|
module.exports = { runStage2 };
|