amicus 4.6.3 → 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 +283 -0
- package/README.md +19 -7
- package/bin/amicus.js +31 -0
- package/docs/ROADMAP.md +143 -36
- package/docs/configuration.md +51 -3
- 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 +59 -13
- package/electron/workspace-ui/index.html +3 -0
- package/electron/workspace-ui/live-model.js +52 -14
- package/electron/workspace-ui/workspace-app.js +14 -3
- 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 +6 -2
- package/electron/workspace-ui/workspace-seats.js +68 -0
- 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 +17 -5
- 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/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 +61 -5
- 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/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/cli-preflight.js +27 -1
- package/src/utils/config.js +15 -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 +24 -4
- package/src/utils/spend-ledger.js +11 -4
- package/src/utils/validators.js +16 -0
|
@@ -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 };
|
|
@@ -23,91 +23,13 @@ const { materializeReviews, isAbortExit } = require('./run-launch');
|
|
|
23
23
|
const { retryStage1Losses } = require('./run-retry');
|
|
24
24
|
const runState = require('./run-state');
|
|
25
25
|
const { runStage2 } = require('./run-stage2');
|
|
26
|
+
const { launchStage1 } = require('./run-stage1-launch');
|
|
27
|
+
const { buildRunStatsEntry } = require('./run-assemble');
|
|
26
28
|
|
|
27
29
|
function slug(text) {
|
|
28
30
|
return String(text).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|
|
29
31
|
}
|
|
30
32
|
|
|
31
|
-
/** Launch all Stage-1 legs (wave + critic/lens solos), collect run docs. */
|
|
32
|
-
async function launchStage1(ctx) {
|
|
33
|
-
const { o, launchers } = ctx;
|
|
34
|
-
// `noCostGate` rides EVERY launch object in this file (here, the findings
|
|
35
|
-
// repair, the judge wave, the judge repair) — see run-launch.js's fanout call.
|
|
36
|
-
const common = {
|
|
37
|
-
project: o.runDir, timeout: o.timeout, gateway: o.gateway,
|
|
38
|
-
noValidateModel: o.noValidateModel, noCostGate: o.noCostGate,
|
|
39
|
-
// v4.3 Task 3 (spec §7.2): attribution ids, forwarded verbatim to runFanout
|
|
40
|
-
// via run-launch.js so every Stage-1 leg's ledger row carries them.
|
|
41
|
-
councilRunId: o.runId, councilName: o.councilName,
|
|
42
|
-
// v4.3 Task 18 (spec §6.2): fallback chains apply to STAGE legs only —
|
|
43
|
-
// the chair (run-chair.js) and debate legs (run-debate.js) never receive
|
|
44
|
-
// this, so they never substitute via chains.
|
|
45
|
-
fallback: o.fallback, catalog: o.catalog,
|
|
46
|
-
};
|
|
47
|
-
const launches = [];
|
|
48
|
-
const seated = []; // parallel to `launches`: what each one was SUPPOSED to seat
|
|
49
|
-
// Record every sub-wave BEFORE it launches: `amicus abort` cascades over
|
|
50
|
-
// stages[].waveIds, so an id written after the launch leaves that leg
|
|
51
|
-
// reachable only by the pid kill (no per-leg abort marker).
|
|
52
|
-
const record = (waveId) => runState.appendStageWave(o.runDir, 'stage1', waveId);
|
|
53
|
-
if (o.lenses) {
|
|
54
|
-
o.models.forEach((m, i) => {
|
|
55
|
-
const waveId = `${o.runId}-l${i + 1}`;
|
|
56
|
-
record(waveId);
|
|
57
|
-
seated.push({ waveId, models: [m] });
|
|
58
|
-
launches.push(launchers.launchSolo({
|
|
59
|
-
...common, model: m, waveId,
|
|
60
|
-
prompt: briefings.buildLensBriefing({ lens: o.lenses[i], briefing: o.briefing, date: o.date }),
|
|
61
|
-
}));
|
|
62
|
-
});
|
|
63
|
-
} else {
|
|
64
|
-
const seats = o.models.filter(m => m !== o.critic);
|
|
65
|
-
if (seats.length > 0) {
|
|
66
|
-
record(`${o.runId}-s1`);
|
|
67
|
-
seated.push({ waveId: `${o.runId}-s1`, models: seats.slice() });
|
|
68
|
-
launches.push(launchers.launchWave({
|
|
69
|
-
...common, models: seats, waveId: `${o.runId}-s1`,
|
|
70
|
-
prompt: briefings.buildSeatBriefing({ briefing: o.briefing, date: o.date }),
|
|
71
|
-
}));
|
|
72
|
-
}
|
|
73
|
-
if (o.critic) {
|
|
74
|
-
record(`${o.runId}-c1`);
|
|
75
|
-
seated.push({ waveId: `${o.runId}-c1`, models: [o.critic] });
|
|
76
|
-
launches.push(launchers.launchSolo({
|
|
77
|
-
...common, model: o.critic, waveId: `${o.runId}-c1`,
|
|
78
|
-
prompt: briefings.buildCriticBriefing({ briefing: o.briefing, date: o.date }),
|
|
79
|
-
}));
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
const results = await Promise.all(launches);
|
|
83
|
-
let aborted = null;
|
|
84
|
-
const legs = [];
|
|
85
|
-
const deadWaves = [];
|
|
86
|
-
results.forEach((r, i) => {
|
|
87
|
-
ctx.addWave(r.wave);
|
|
88
|
-
const abort = isAbortExit(r.exitCode);
|
|
89
|
-
if (abort) { aborted = r.exitCode; }
|
|
90
|
-
const got = (r.wave && Array.isArray(r.wave.legs)) ? r.wave.legs : [];
|
|
91
|
-
legs.push(...got);
|
|
92
|
-
// ⚠️ Step 10's uncovered half. A wave that died BEFORE its legs (the server
|
|
93
|
-
// never started; `database is locked`) contributes NOTHING to `legs`, so
|
|
94
|
-
// deadLegs cannot see it either — which is how run v441plan01 recorded
|
|
95
|
-
// stage1 'complete' with four seats missing and no trace of them. In lens
|
|
96
|
-
// mode every seat is its own wave, so a run could lose seats and still exit
|
|
97
|
-
// 0; the quorum gate only catches the non-lens seat wave. A budget refusal
|
|
98
|
-
// has its own louder channel already (run-budget.noteBudgetRefusal) and
|
|
99
|
-
// must not be double-counted here.
|
|
100
|
-
if (got.length > 0 || abort) { return; }
|
|
101
|
-
if (r.errorDoc && r.errorDoc.code === 'BUDGET_EXCEEDED') { return; }
|
|
102
|
-
deadWaves.push({
|
|
103
|
-
waveId: seated[i].waveId, models: seated[i].models,
|
|
104
|
-
reason: (r.wave && (r.wave.reason || r.wave.error))
|
|
105
|
-
|| (r.errorDoc && r.errorDoc.message) || 'the wave produced no legs',
|
|
106
|
-
});
|
|
107
|
-
});
|
|
108
|
-
return { aborted, legs, deadWaves };
|
|
109
|
-
}
|
|
110
|
-
|
|
111
33
|
/** Role of a seat by its input alias. */
|
|
112
34
|
function roleFor(o, alias) {
|
|
113
35
|
if (o.lenses) {
|
|
@@ -120,8 +42,15 @@ function roleFor(o, alias) {
|
|
|
120
42
|
/**
|
|
121
43
|
* Stage 1: independent reviews + findings validation + bounded repair.
|
|
122
44
|
* @returns {Promise<{aborted: number|null, reviews: Array, deadLegs: Array,
|
|
123
|
-
* deadWaves: Array, degraded: boolean}>} `
|
|
124
|
-
*
|
|
45
|
+
* deadWaves: Array, degraded: boolean, extraRows: Array}>} `extraRows` (v4.7
|
|
46
|
+
* D2/E4) is the row-per-launch channel for legs that never became — or
|
|
47
|
+
* stopped being — a seat's primary review: one `role:'repair'` row per
|
|
48
|
+
* findings-repair solo (error status when the repair itself failed), one
|
|
49
|
+
* `role:'superseded'` row per first leg a later attempt replaced (healed OR
|
|
50
|
+
* still-dead — either way the first leg stopped being primary), and one
|
|
51
|
+
* PRIMARY error row per seat with no surviving review at all.
|
|
52
|
+
* `degraded` covers BOTH ways a seat can go missing: a leg that ran and died
|
|
53
|
+
* (deadLegs) and a whole sub-wave that
|
|
125
54
|
* died before its legs existed (deadWaves). A pushed review carries
|
|
126
55
|
* `findingsUnverified: true` (LC-11) when its findings came from a repair whose
|
|
127
56
|
* contract could not be checked — the original block was absent or unparseable,
|
|
@@ -132,7 +61,7 @@ function roleFor(o, alias) {
|
|
|
132
61
|
async function runStage1(ctx) {
|
|
133
62
|
const { o } = ctx;
|
|
134
63
|
const { aborted, legs, deadWaves } = await launchStage1(ctx);
|
|
135
|
-
if (aborted) { return { aborted, reviews: [], deadLegs: [], deadWaves: [], degraded: false }; }
|
|
64
|
+
if (aborted) { return { aborted, reviews: [], deadLegs: [], deadWaves: [], degraded: false, extraRows: [] }; }
|
|
136
65
|
|
|
137
66
|
const firstPass = materializeReviews(o.runDir, legs);
|
|
138
67
|
const alive0 = new Set(firstPass.map(m => m.leg));
|
|
@@ -147,7 +76,7 @@ async function runStage1(ctx) {
|
|
|
147
76
|
// abort fixed ~87 lines below ("Must be the post-retry set") — subtract
|
|
148
77
|
// whatever retry.recoveredLegs already healed before this abort landed.
|
|
149
78
|
const healed = new Set(retry.recoveredLegs.map(l => l.modelInput || l.model));
|
|
150
|
-
return { aborted: retry.aborted, reviews: [], degraded: false,
|
|
79
|
+
return { aborted: retry.aborted, reviews: [], degraded: false, extraRows: [],
|
|
151
80
|
deadLegs: deadLegs0.filter(l => !healed.has(l.modelInput || l.model)),
|
|
152
81
|
deadWaves: deadWaves.map(w => ({ ...w, models: (w.models || []).filter(m => !healed.has(m)) })).filter(w => w.models.length > 0) };
|
|
153
82
|
}
|
|
@@ -185,6 +114,10 @@ async function runStage1(ctx) {
|
|
|
185
114
|
const stillDeadWaves = [...retry.skippedDeadWaves, ...retry.stillDeadWaves];
|
|
186
115
|
|
|
187
116
|
const reviews = [];
|
|
117
|
+
// v4.7 D2/E4: every repair launch is a billed leg of its own, distinct from
|
|
118
|
+
// the seat's own review leg (m.leg) it is trying to fix — it gets its own
|
|
119
|
+
// row so its cost is never folded into, or lost from, the review's row.
|
|
120
|
+
const extraRows = [];
|
|
188
121
|
let repairSeq = 0;
|
|
189
122
|
for (const m of materialized) {
|
|
190
123
|
let conformance = 'clean';
|
|
@@ -227,6 +160,7 @@ async function runStage1(ctx) {
|
|
|
227
160
|
project: o.runDir, waveId, timeout: o.timeout,
|
|
228
161
|
gateway: o.gateway, noValidateModel: o.noValidateModel, noCostGate: o.noCostGate,
|
|
229
162
|
councilRunId: o.runId, councilName: o.councilName,
|
|
163
|
+
tag: o.tag, // v4.7 F8 D16: rides the same forward as councilRunId/councilName.
|
|
230
164
|
fallback: o.fallback, catalog: o.catalog,
|
|
231
165
|
});
|
|
232
166
|
ctx.addWave(solo.wave);
|
|
@@ -236,8 +170,15 @@ async function runStage1(ctx) {
|
|
|
236
170
|
// short-circuit, so a heal-then-abort run was recording seats as dead
|
|
237
171
|
// that had actually reviewed on retry. Must be the post-retry set,
|
|
238
172
|
// same as the normal-completion return below.
|
|
239
|
-
|
|
173
|
+
// Abort paths add no rows (aborted runs never reach tally) — extraRows
|
|
174
|
+
// is returned only for shape consistency, never read past this point.
|
|
175
|
+
return { aborted: solo.exitCode, reviews, deadLegs: stillDeadLegs, deadWaves: stillDeadWaves,
|
|
176
|
+
degraded: false, extraRows };
|
|
240
177
|
}
|
|
178
|
+
// Every -p<N> launch gets a row — INCLUDING a repair that failed: the
|
|
179
|
+
// error status rides naturally off solo.leg (null/'error'-status leg ⇒
|
|
180
|
+
// buildRunStatsEntry's own never-invent defaults), no special-casing needed.
|
|
181
|
+
extraRows.push(buildRunStatsEntry({ leg: solo.leg, model: m.modelInput, role: 'repair', wasChair: false }));
|
|
241
182
|
const repaired = (solo.leg && solo.leg.summary) || '';
|
|
242
183
|
if (repaired.trim()) { repairing = repaired; }
|
|
243
184
|
res = validateFindings(repaired);
|
|
@@ -285,8 +226,60 @@ async function runStage1(ctx) {
|
|
|
285
226
|
...(repairRefused ? { repairRefused } : {}),
|
|
286
227
|
});
|
|
287
228
|
}
|
|
229
|
+
|
|
230
|
+
// v4.7 D2/E4 — superseded rows: a leg-origin seat's FIRST leg stops being
|
|
231
|
+
// primary the moment a retry was actually attempted for it, healed or not
|
|
232
|
+
// (deadLegs0 × recovered-or-still-dead seats — mirrors the healed-set idiom
|
|
233
|
+
// above, extended to the still-dead half E4 also requires). A skipped seat
|
|
234
|
+
// (cost ceiling / unmappable — never got a second leg) keeps NO superseded
|
|
235
|
+
// row: nothing replaced it. Wave-origin seats never had a first leg at all,
|
|
236
|
+
// so they can never appear here regardless of healed/dead outcome (E4).
|
|
237
|
+
const supersededAliases = new Set([
|
|
238
|
+
...retry.recoveredLegs.map(l => l.modelInput || l.model),
|
|
239
|
+
...retry.stillDeadLegs.map(l => l.modelInput || l.model),
|
|
240
|
+
]);
|
|
241
|
+
for (const dead of deadLegs0) {
|
|
242
|
+
const alias = dead.modelInput || dead.model;
|
|
243
|
+
if (supersededAliases.has(alias)) {
|
|
244
|
+
extraRows.push(buildRunStatsEntry({ leg: dead, model: alias, role: 'superseded', wasChair: false }));
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// v4.7 D2/E4 — primary error rows: one per seat with NO surviving review
|
|
249
|
+
// (every alias still in stillDeadLegs/stillDeadWaves after retry). E5
|
|
250
|
+
// amended (Task-4 review, owner-ruled): run-retry.js now surfaces the real
|
|
251
|
+
// retry leg (stillDeadRetryLegs), from the ONE branch it exists in —
|
|
252
|
+
// retryLegStillDeadNote, a retry leg that came back unusable. Prefer that
|
|
253
|
+
// REAL leg: status/waveId/usage/duration all real, all from the SAME
|
|
254
|
+
// attempt (no more pairing a retry's waveId with a different attempt's
|
|
255
|
+
// status). The other two dead-leg note classes — srcLegStillDeadNote (retry
|
|
256
|
+
// wave died wholesale, zero legs) and missingLegStillDeadNote (partial
|
|
257
|
+
// return never named this seat) — never get a real leg, so `leg: null`
|
|
258
|
+
// (no phantom waveId: one must never appear without a real billed leg).
|
|
259
|
+
// No 'dead-leg' note at all ⇒ never retried (skipped) ⇒ the original dead
|
|
260
|
+
// leg is this seat's only, and therefore final, leg.
|
|
261
|
+
const retryLegByAlias = new Map();
|
|
262
|
+
for (const leg of retry.stillDeadRetryLegs) { retryLegByAlias.set(leg.modelInput || leg.model, leg); }
|
|
263
|
+
const attemptedAliases = new Set();
|
|
264
|
+
for (const n of retry.stillDeadNotes) {
|
|
265
|
+
if (n.channel === 'dead-leg' && n.data && n.data.seat) { attemptedAliases.add(n.data.seat); }
|
|
266
|
+
}
|
|
267
|
+
const deadAliases = new Set([
|
|
268
|
+
...stillDeadLegs.map(l => l.modelInput || l.model),
|
|
269
|
+
...stillDeadWaves.flatMap(w => w.models || []),
|
|
270
|
+
]);
|
|
271
|
+
for (const alias of deadAliases) {
|
|
272
|
+
let finalLeg = retryLegByAlias.get(alias);
|
|
273
|
+
if (!finalLeg) {
|
|
274
|
+
finalLeg = attemptedAliases.has(alias)
|
|
275
|
+
? null // retried; no leg at all for this seat
|
|
276
|
+
: (deadLegs0.find(l => (l.modelInput || l.model) === alias) || null); // never retried
|
|
277
|
+
}
|
|
278
|
+
extraRows.push(buildRunStatsEntry({ leg: finalLeg, model: alias, role: roleFor(o, alias), wasChair: false }));
|
|
279
|
+
}
|
|
280
|
+
|
|
288
281
|
return { aborted: null, reviews, deadLegs: stillDeadLegs, deadWaves: stillDeadWaves,
|
|
289
|
-
degraded: stillDeadLegs.length > 0 || stillDeadWaves.length > 0 };
|
|
282
|
+
degraded: stillDeadLegs.length > 0 || stillDeadWaves.length > 0, extraRows };
|
|
290
283
|
}
|
|
291
284
|
|
|
292
285
|
// runStage2 lives in ./run-stage2.js (300-line gate) but is re-exported here so
|
package/src/council/run-state.js
CHANGED
|
@@ -101,13 +101,21 @@ function initCouncilRun(o) {
|
|
|
101
101
|
// "no debate key" contract and fail the object-typed schema), and with a VALID
|
|
102
102
|
// outcome from the first write so a run killed mid-debate stays schema-valid.
|
|
103
103
|
...(o.debate ? { debate: { enabled: true, outcome: 'nothing-to-debate' } } : {}),
|
|
104
|
-
// F9 (v4.5): additive-only — absent (not null) without --template
|
|
105
|
-
//
|
|
104
|
+
// F9 (v4.5): additive-only — absent (not null) without --template. T15-m2
|
|
105
|
+
// (v4.7): the MCP handler (mcp-council-run.js) now pre-seeds this directly
|
|
106
|
+
// via its own initRun call when a pack forwards briefing.template; the
|
|
107
|
+
// spawned CLI child's own seed here still passes o.template: null (no
|
|
108
|
+
// --template flag crosses that spawn boundary), so this spread omits the
|
|
109
|
+
// key and mergeRun's shallow merge preserves the parent's pre-seeded value.
|
|
106
110
|
...(o.template ? { template: o.template } : {}),
|
|
107
111
|
// v4.5 Task 12 (B7/F5): additive-only — absent (not null) without --pack.
|
|
108
112
|
// Preserved across a later MCP-child initRun whose own seed omits it —
|
|
109
113
|
// mergeRun's plain shallow merge never drops a key patch doesn't mention.
|
|
110
114
|
...(o.pack ? { pack: o.pack } : {}),
|
|
115
|
+
// v4.7 F8 (D13): additive-only — absent (not null) without --tag. Same
|
|
116
|
+
// merge-preserve precedent as pack above (mergeRun's shallow merge never
|
|
117
|
+
// drops a key a later patch doesn't mention).
|
|
118
|
+
...(o.tag ? { tag: o.tag } : {}),
|
|
111
119
|
// v4.5 Wave 2 (post-HOLD chip, task-23-report.md Anomaly 1): additive-only
|
|
112
120
|
// — absent (never an empty array) when nothing was dropped. Handler-computed
|
|
113
121
|
// (cli-handlers-council-run.js's resolveBench, via resolveCouncilMembers) so
|
package/src/council/run.js
CHANGED
|
@@ -207,9 +207,15 @@ async function runCouncil(options, deps = {}) {
|
|
|
207
207
|
}
|
|
208
208
|
|
|
209
209
|
// ---- Chair synthesis (provisional tally feeds the packet) ----
|
|
210
|
+
// v4.7 D2: two independent extraRows channels — Stage 1's findings-repair
|
|
211
|
+
// rows and Stage 2's judge-repair rows — concatenate into the ONE array
|
|
212
|
+
// buildTallyInput appends after the primary review rows (run-assemble.js
|
|
213
|
+
// docblock). Neither stage invents a second mechanism for the other's kind
|
|
214
|
+
// of row.
|
|
210
215
|
const mkInput = (chairStats, chairModel) => asm.buildTallyInput({
|
|
211
216
|
runId: o.runId, date: o.date, bench: o.models.slice(), chair: chairModel,
|
|
212
217
|
reviews: s1.reviews, judgeResults: s2.judgeResults, chairStats, claudeReview,
|
|
218
|
+
extraRows: [...s1.extraRows, ...s2.extraRows],
|
|
213
219
|
});
|
|
214
220
|
const provisionalInput = mkInput(null, o.chair);
|
|
215
221
|
const provisional = tally(provisionalInput);
|
|
@@ -231,17 +237,35 @@ async function runCouncil(options, deps = {}) {
|
|
|
231
237
|
packet, degrade, statsFn, isSignalled: () => signalled,
|
|
232
238
|
});
|
|
233
239
|
if (chairRes.aborted !== null) { return finalize(chairRes.aborted); }
|
|
234
|
-
const { chairLeg, actualChair, chairText, chairConformance, overallVerdict } = chairRes;
|
|
240
|
+
const { chairLeg, actualChair, chairText, chairConformance, overallVerdict, chairRows, chairAttempts } = chairRes;
|
|
235
241
|
|
|
236
242
|
// ---- Final tally (chair row included) + ledger + artifacts ----
|
|
237
243
|
const chairStats = chairLeg ? asm.buildRunStatsEntry({
|
|
238
244
|
leg: chairLeg, model: actualChair, role: 'chair', wasChair: true,
|
|
239
245
|
conformance: chairConformance,
|
|
240
246
|
}) : null;
|
|
247
|
+
// v4.7 D2: a give-up (no chairLeg) with at least one recorded attempt gets
|
|
248
|
+
// an explicit error row so the walk's outcome isn't silently absorbed.
|
|
249
|
+
// Keyed on chairAttempts, NOT chairRows — attempts that die pre-wave (no
|
|
250
|
+
// money spent) record an outcome but yield no row (errata E3).
|
|
251
|
+
const giveUpRow = (!chairLeg && chairAttempts && chairAttempts.length)
|
|
252
|
+
? asm.buildRunStatsEntry({ leg: null, model: o.chair, role: 'chair', wasChair: false })
|
|
253
|
+
: null;
|
|
241
254
|
// Built on the (possibly debated) input so the debate's amended claims, replaced
|
|
242
255
|
// adjudications and rebuttal/revote runStats rows all reach the final record.
|
|
243
256
|
const finalInput = { ...debatedInput, meta: { ...debatedInput.meta, chair: actualChair || o.chair } };
|
|
244
|
-
|
|
257
|
+
// Item 8, final-review consolidated wave: was three sequential
|
|
258
|
+
// reassignments (chairStats, then chairRows, then giveUpRow), each
|
|
259
|
+
// rebuilding finalInput.runStats from scratch — collapsed into the one
|
|
260
|
+
// spread that was always the net effect. The `|| []` fallbacks were
|
|
261
|
+
// dead: `runStats` is a real array on every debatedInput
|
|
262
|
+
// (asm.buildTallyInput always returns one via .map()), never undefined.
|
|
263
|
+
finalInput.runStats = [
|
|
264
|
+
...finalInput.runStats,
|
|
265
|
+
...(chairStats ? [chairStats] : []),
|
|
266
|
+
...chairRows,
|
|
267
|
+
...(giveUpRow ? [giveUpRow] : []),
|
|
268
|
+
];
|
|
245
269
|
const record = tally(finalInput);
|
|
246
270
|
if (debateFindings) { decorateRecord(record, debateFindings); }
|
|
247
271
|
if (!o.lenses) {
|
package/src/council/tally.js
CHANGED
|
@@ -120,10 +120,14 @@ function tally(input) {
|
|
|
120
120
|
// travel with it: LC-11's `findingsUnverified` (contract uncheckable) and
|
|
121
121
|
// F1's `repairRefused` (contract checked and broken). Additive, emitted only
|
|
122
122
|
// when set, and the runStats schema declares no additionalProperties, so a
|
|
123
|
-
// run without either is byte-for-byte unchanged.
|
|
124
|
-
//
|
|
123
|
+
// run without either is byte-for-byte unchanged. v4.7 GOA-7 exercised the
|
|
124
|
+
// ledger's schema-versioned extension slot: `resolvedModel` rides this
|
|
125
|
+
// allowlist and reaches ledger rows via the ledger's model-keyed join of
|
|
126
|
+
// primary rows (ledger.js, LEDGER_SCHEMA_VERSION 2).
|
|
125
127
|
...(r.findingsUnverified ? { findingsUnverified: true } : {}),
|
|
126
128
|
...(r.repairRefused ? { repairRefused: r.repairRefused } : {}),
|
|
129
|
+
...(r.waveId ? { waveId: r.waveId } : {}),
|
|
130
|
+
...(r.resolvedModel ? { resolvedModel: r.resolvedModel } : {}),
|
|
127
131
|
status: r.status || 'unknown',
|
|
128
132
|
durationMs: typeof r.durationMs === 'number' ? r.durationMs : null,
|
|
129
133
|
usage: r.usage || null,
|
package/src/mcp-council-bench.js
CHANGED
|
@@ -21,6 +21,10 @@
|
|
|
21
21
|
* spawns the CLI child with an already-expanded `--models` list (never
|
|
22
22
|
* `--council`), so the preset name would otherwise be lost; the caller
|
|
23
23
|
* forwards it via the internal `--council-name` passthrough instead.
|
|
24
|
+
* Parallel twin: cli-council-run-bench.js's `resolveBench` wraps the same
|
|
25
|
+
* `resolveCouncilMembers` core with its own XOR rules (CLI failJson docs there,
|
|
26
|
+
* plain `{error}` strings here) and carries one guard this side lacks. The two
|
|
27
|
+
* wrappers evolve independently — change a validation rule on one, change both.
|
|
24
28
|
*/
|
|
25
29
|
function resolveBenchInput(input) {
|
|
26
30
|
const inputModels = Array.isArray(input.models) ? input.models : [];
|
package/src/mcp-council-run.js
CHANGED
|
@@ -94,11 +94,13 @@ async function handleCouncilRunTool(input, project, helpers) {
|
|
|
94
94
|
// onto input.template above) is the only way one reaches this handler.
|
|
95
95
|
// {{prompt}} = the briefingFile content; the RENDERED text is what lands in
|
|
96
96
|
// briefing.md below (mirrors the CLI's single template-application point).
|
|
97
|
+
let templateMeta = null;
|
|
97
98
|
if (input.template !== undefined) {
|
|
98
99
|
const { applyTemplate } = require('./template/apply');
|
|
99
100
|
const t = applyTemplate({ templateRef: input.template, prompt: briefing, project });
|
|
100
101
|
if (t.error) { return textResult(t.error.message, true); }
|
|
101
102
|
briefing = t.prompt;
|
|
103
|
+
templateMeta = t.promptMeta && t.promptMeta.template;
|
|
102
104
|
notices.push(...t.notices);
|
|
103
105
|
}
|
|
104
106
|
|
|
@@ -158,6 +160,10 @@ async function handleCouncilRunTool(input, project, helpers) {
|
|
|
158
160
|
// v4.5 Wave 2: additive, same preserved-across-the-child's-own-initRun
|
|
159
161
|
// precedent as `pack` above — absent (never []) when nothing dropped.
|
|
160
162
|
...(droppedMembers.length ? { droppedMembers } : {}),
|
|
163
|
+
// T15-m2 (v4.7): additive-only — absent (not null) without a pack-forwarded
|
|
164
|
+
// template. Same preserved-across-the-child's-own-initRun precedent as
|
|
165
|
+
// `pack`/`droppedMembers` above (run-state.js:104-110).
|
|
166
|
+
...(templateMeta ? { template: templateMeta } : {}),
|
|
161
167
|
usage: null, createdAt: new Date().toISOString(),
|
|
162
168
|
});
|
|
163
169
|
runState.writePointer(project, runId, runDir);
|
|
@@ -195,6 +201,9 @@ async function handleCouncilRunTool(input, project, helpers) {
|
|
|
195
201
|
if (input.debate) { args.push('--debate'); }
|
|
196
202
|
if (input.claudeReviewFile) { args.push('--claude-review', path.resolve(project, String(input.claudeReviewFile))); }
|
|
197
203
|
if (input.noCostGate) { args.push('--no-cost-gate'); }
|
|
204
|
+
// v4.7 F8 (D13): the spawned CLI child's own cli-handlers-council-run.js
|
|
205
|
+
// stores the tag on the run.json seed (Task 3) — this handler only forwards.
|
|
206
|
+
if (input.tag) { args.push('--tag', input.tag); }
|
|
198
207
|
|
|
199
208
|
let child;
|
|
200
209
|
try { child = helpers.spawnFn(args, runDir); } catch (err) {
|
|
@@ -275,6 +284,7 @@ const awareness = require('./mcp-council-awareness');
|
|
|
275
284
|
|
|
276
285
|
module.exports = {
|
|
277
286
|
handleCouncilRunTool,
|
|
287
|
+
COUNCIL_PACK_PARAM_MAP,
|
|
278
288
|
buildCouncilStatusPayload: awareness.buildCouncilStatusPayload,
|
|
279
289
|
listCouncilRuns: awareness.listCouncilRuns,
|
|
280
290
|
abortCouncilRun: awareness.abortCouncilRun,
|