amicus 4.6.3 → 4.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +349 -0
- package/README.md +19 -7
- package/bin/amicus.js +31 -0
- package/docs/ROADMAP.md +143 -36
- package/docs/configuration.md +52 -4
- package/docs/council.md +65 -12
- package/docs/doc-system.md +8 -7
- package/docs/schemas.md +10 -1
- package/docs/testing.md +1 -1
- package/docs/troubleshooting.md +33 -5
- package/docs/usage.md +64 -14
- 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 +8 -4
- 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/scripts/postinstall.js +6 -3
- package/scripts/setup-hooks.js +49 -3
- package/skills/second-opinion/MANUAL-ORCHESTRATION.md +12 -0
- package/skills/second-opinion/MODEL-NOTES.md +5 -4
- package/skills/sidecar/SKILL.md +9 -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-resume-continue.js +20 -0
- 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 +20 -2
- package/src/council/run-retry.js +17 -2
- 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/headless.js +69 -6
- 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/continue.js +8 -23
- package/src/sidecar/fanout-budget.js +1 -2
- package/src/sidecar/fanout-leg-fallback.js +7 -3
- package/src/sidecar/fanout-retry.js +15 -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/reopen-spend.js +32 -0
- package/src/sidecar/resume.js +1 -1
- 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/doctor-engine-check.js +32 -0
- package/src/utils/engine-install-scan.js +98 -15
- package/src/utils/engine-repair.js +96 -2
- package/src/utils/remediation-hints.js +29 -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/headless.js
CHANGED
|
@@ -154,6 +154,58 @@ function withTimeout(promise, ms, label) {
|
|
|
154
154
|
]);
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
+
/**
|
|
158
|
+
* Task 6 (#129, #133): build the NO_OUTPUT_BACKSTOP reason string. Report
|
|
159
|
+
* ONLY what the mechanism observed — a deadline passed with no substantive
|
|
160
|
+
* activity (output/reasoning/tool calls) — never a cause. At the pre-send
|
|
161
|
+
* firing site (runHeadless, ~:506-518) the backstop can win the race against
|
|
162
|
+
* sendPromptAsync before the send ever resolves, so "the endpoint accepted
|
|
163
|
+
* the request" is not even something that site observed. The previous text
|
|
164
|
+
* asserted "likely a listed-but-not-serving model or a dead endpoint" — a
|
|
165
|
+
* canned guess with no evidence gate — which sent 30 minutes of #133's
|
|
166
|
+
* debugging at model ids and API keys while the real cause (an opencode
|
|
167
|
+
* engine version skew) sat in ~/.local/share/opencode/log/opencode.log the
|
|
168
|
+
* whole time.
|
|
169
|
+
*
|
|
170
|
+
* `fromEnv` distinguishes two ways `ms` was decided, NOT whether
|
|
171
|
+
* AMICUS_NO_OUTPUT_BACKSTOP_MS is relevant — it is relevant on both branches:
|
|
172
|
+
* - fromEnv=true: `ms` IS the live env-resolved value (or its documented
|
|
173
|
+
* default) — the message says so directly, "(0 disables)" included,
|
|
174
|
+
* because raising the env var changes exactly this window.
|
|
175
|
+
* - fromEnv=false: `ms` arrived as a direct, caller-set numeric option.
|
|
176
|
+
* Task 6 review (Important finding): this is NOT synonymous with "the
|
|
177
|
+
* env var doesn't apply" — src/council/run-retry.js:154 computes a
|
|
178
|
+
* Stage-1 retry's escalated window as
|
|
179
|
+
* `2 * (Number.isFinite(o.noOutputBackstopMs) ? o.noOutputBackstopMs :
|
|
180
|
+
* resolveNoOutputBackstopMs())` and forwards that as a direct
|
|
181
|
+
* `noOutputBackstopMs` (line 186) — so a 240s retry-fired backstop is
|
|
182
|
+
* "caller-set" by this predicate while still being *derived from* the
|
|
183
|
+
* env default doubled. Only src/sidecar/models-probe.js:79's hardcoded,
|
|
184
|
+
* non-tunable 30s (PROBE_WINDOW_MS; docs/usage.md:406 promises it's "not
|
|
185
|
+
* tunable") is truly independent of the env var. Because a real
|
|
186
|
+
* `fromEnv` flag distinguishing those two cases would have to ride the
|
|
187
|
+
* same value through src/sidecar/fanout.js, which is line-locked at
|
|
188
|
+
* EXACTLY 300/300 this release, the caller-set branch instead names the
|
|
189
|
+
* var as something this window *overrides* rather than either claiming
|
|
190
|
+
* it governs (false on the probe) or omitting it (false/unhelpful on the
|
|
191
|
+
* retry) — true on both, and still points a user at the remedy.
|
|
192
|
+
*
|
|
193
|
+
* Kept module-scope and pure (not a closure over runHeadless locals) so it
|
|
194
|
+
* can be asserted on directly in tests without driving the poll loop; the
|
|
195
|
+
* `noOutputBackstopReason` closure inside runHeadless just forwards to this
|
|
196
|
+
* with the per-run `noOutputBackstopMs`/`backstopFromEnv` values, so the two
|
|
197
|
+
* firing sites there stay identical to what's tested here.
|
|
198
|
+
* @param {{ms: number, fromEnv: boolean}} args
|
|
199
|
+
* @returns {string}
|
|
200
|
+
*/
|
|
201
|
+
function formatNoOutputBackstopReason({ ms, fromEnv }) {
|
|
202
|
+
return 'NO_OUTPUT_BACKSTOP: no output, reasoning, or tool calls in '
|
|
203
|
+
+ `${Math.round(ms / 1000)}s — `
|
|
204
|
+
+ (fromEnv
|
|
205
|
+
? 'the AMICUS_NO_OUTPUT_BACKSTOP_MS window (0 disables)'
|
|
206
|
+
: 'a caller-set window overriding the AMICUS_NO_OUTPUT_BACKSTOP_MS default');
|
|
207
|
+
}
|
|
208
|
+
|
|
157
209
|
/**
|
|
158
210
|
* Wait for the OpenCode server to be ready using SDK health check
|
|
159
211
|
*/
|
|
@@ -473,16 +525,26 @@ async function runHeadless(model, systemPrompt, userMessage, taskId, project, ti
|
|
|
473
525
|
// deadline `nowMs >= deadline` can never satisfy — the backstop would
|
|
474
526
|
// silently never fire. Finite zero (the documented explicit-disable
|
|
475
527
|
// value) still takes the direct branch: Number.isFinite(0) === true.
|
|
476
|
-
|
|
477
|
-
|
|
528
|
+
// Capture the source-of-truth predicate ONCE and reuse it (do not call
|
|
529
|
+
// Number.isFinite a second time below) — `backstopFromEnv` is just its
|
|
530
|
+
// complement: options.noOutputBackstopMs is a direct, caller-set value
|
|
531
|
+
// exactly when Number.isFinite is true, so the window came from
|
|
532
|
+
// resolveNoOutputBackstopMs's env-resolution seam exactly when it's false.
|
|
533
|
+
// Reused both to pick noOutputBackstopMs and to decide whether the reason
|
|
534
|
+
// string below may name the env var, so the two can never drift apart.
|
|
535
|
+
const backstopFromEnv = !Number.isFinite(options.noOutputBackstopMs);
|
|
536
|
+
const noOutputBackstopMs = backstopFromEnv
|
|
537
|
+
? resolveNoOutputBackstopMs(options._env) : options.noOutputBackstopMs;
|
|
478
538
|
const noOutputBackstop = createNoOutputBackstop({ ms: noOutputBackstopMs, startedAt: Date.now() });
|
|
479
539
|
let backstopFired = false;
|
|
480
540
|
// Single source for the reason string so the pre-send firing site below and
|
|
481
541
|
// the per-poll firing site further down (still ticking the SAME instance)
|
|
482
|
-
// can never drift apart.
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
542
|
+
// can never drift apart. Forwards to the module-scope, pure
|
|
543
|
+
// formatNoOutputBackstopReason (below/exported) so tests can assert on the
|
|
544
|
+
// string shape directly without driving the whole poll loop.
|
|
545
|
+
const noOutputBackstopReason = () => formatNoOutputBackstopReason({
|
|
546
|
+
ms: noOutputBackstopMs, fromEnv: backstopFromEnv,
|
|
547
|
+
});
|
|
486
548
|
|
|
487
549
|
// Send prompt asynchronously (returns immediately, we poll for results) —
|
|
488
550
|
// bounded by the backstop: an endpoint that accepts but never answers must
|
|
@@ -1413,6 +1475,7 @@ module.exports = {
|
|
|
1413
1475
|
extractSummary,
|
|
1414
1476
|
findTrailingFoldMarker,
|
|
1415
1477
|
formatFoldOutput,
|
|
1478
|
+
formatNoOutputBackstopReason,
|
|
1416
1479
|
DEFAULT_TIMEOUT,
|
|
1417
1480
|
FOLD_MARKER,
|
|
1418
1481
|
COMPLETE_MARKER,
|
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,
|