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
|
@@ -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,
|
package/src/mcp-server.js
CHANGED
|
@@ -423,6 +423,10 @@ const handlers = {
|
|
|
423
423
|
// gate / template pre-render) for parity between the two paths.
|
|
424
424
|
if (packForward.maxCost !== undefined) { args.push('--max-cost', String(packForward.maxCost)); }
|
|
425
425
|
if (packForward.template !== undefined) { args.push('--template', packForward.template); }
|
|
426
|
+
// v4.7 F8 (D13, errata E-PR3-2): forwarded for the spawn-fallback path
|
|
427
|
+
// below — DEAD on the shared-server branch (args is never read there);
|
|
428
|
+
// that branch stamps input.tag directly into its own metadata write instead.
|
|
429
|
+
if (input.tag) { args.push('--tag', input.tag); }
|
|
426
430
|
args.push('--cwd', cwd);
|
|
427
431
|
|
|
428
432
|
if (sharedServer.enabled && input.noUi) {
|
|
@@ -435,28 +439,30 @@ const handlers = {
|
|
|
435
439
|
// a fall-through to the spawn-fallback catch must never leak one.
|
|
436
440
|
const inProcessNotices = [...fwd.notices];
|
|
437
441
|
const renderedPrompt = fwd.renderedPrompt !== undefined ? fwd.renderedPrompt : input.prompt;
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
442
|
+
// v4.7 PR6: the gate used to hang off `packForward.maxCost !== undefined`,
|
|
443
|
+
// so a no-pack MCP start skipped it while the CLI (cli-handlers-run.js:90)
|
|
444
|
+
// gated unconditionally with a cfg.maxCost fallback. Same guard, both doors.
|
|
445
|
+
// fwd.maxCost is already validated (I2); the ceiling gate itself
|
|
446
|
+
// stays here (needs resolvedModel/pricing, unavailable to pack-forward.js).
|
|
447
|
+
const { lookupPricing } = require('./utils/pricing');
|
|
448
|
+
const { checkBudget, formatBudgetError } = require('./sidecar/budget');
|
|
449
|
+
const { loadConfig } = require('./utils/config');
|
|
450
|
+
const cfg = loadConfig() || {};
|
|
451
|
+
const soloLeg = { modelInput: input.model || resolvedModel, model: resolvedModel, pricing: lookupPricing(resolvedModel) };
|
|
452
|
+
const budget = checkBudget([soloLeg], {
|
|
453
|
+
maxCostPerMtok: cfg.maxCostPerMtok,
|
|
454
|
+
maxCost: fwd.maxCost !== undefined ? fwd.maxCost : cfg.maxCost,
|
|
455
|
+
promptChars: (renderedPrompt && renderedPrompt.length) || 0,
|
|
456
|
+
});
|
|
457
|
+
if (!budget.ok) {
|
|
458
|
+
const { buildErrorDoc, ERROR_CODES } = require('./utils/error-doc');
|
|
459
|
+
return {
|
|
460
|
+
isError: true,
|
|
461
|
+
content: [{ type: 'text', text: JSON.stringify(buildErrorDoc({
|
|
462
|
+
code: ERROR_CODES.BUDGET_EXCEEDED, message: 'Error: budget gate refused the run',
|
|
463
|
+
hint: formatBudgetError(budget, { kind: 'mcp' }),
|
|
464
|
+
})) }],
|
|
465
|
+
};
|
|
460
466
|
}
|
|
461
467
|
|
|
462
468
|
const { server, client } = await sharedServer.ensureServer();
|
|
@@ -500,6 +506,10 @@ const handlers = {
|
|
|
500
506
|
briefing: renderedPrompt,
|
|
501
507
|
// v4.5 Task 15: additive-only — absent (not null) without a pack.
|
|
502
508
|
...(packRecord ? { pack: packRecord } : {}),
|
|
509
|
+
// v4.7 F8 (D13, errata E-PR3-2): THE critical site — this shared-server
|
|
510
|
+
// branch never spawns a CLI child, so the --tag argv forward above
|
|
511
|
+
// (dead here) can never reach it. Same additive-only idiom as pack.
|
|
512
|
+
...(input.tag ? { tag: input.tag } : {}),
|
|
503
513
|
}, null, 2), { mode: 0o600 });
|
|
504
514
|
|
|
505
515
|
// Build context from parent conversation (unless --no-context)
|
|
@@ -693,6 +703,19 @@ const handlers = {
|
|
|
693
703
|
// resolved the route in-process above, so surface its notice here.
|
|
694
704
|
const spawnContent = [{ type: 'text', text: body }];
|
|
695
705
|
if (routeResult.notice) { spawnContent.push({ type: 'text', text: routeResult.notice }); }
|
|
706
|
+
// W1-M6/M7: NOT dead code. This is the same idiom applyPackToMcpInput's
|
|
707
|
+
// orphan-knob notices always use — amicus_fanout genuinely reaches it today
|
|
708
|
+
// (FANOUT_PACK_PARAM_MAP has no contextTurns/contextMaxTokens destination;
|
|
709
|
+
// the fanout handler's own push into waveContent below is covered by
|
|
710
|
+
// mcp-pack-params.test.js's "notice wording (decision 1b)" test — named,
|
|
711
|
+
// not line-numbered, because a line citation goes stale the moment anything
|
|
712
|
+
// is inserted above it, including by its own commit). On THIS solo surface it is
|
|
713
|
+
// unreachable only while KIND_OPTIONS.solo stays fully covered by
|
|
714
|
+
// SOLO_PACK_PARAM_MAP destinations + pack-resolve.js's FORWARDABLE_ARG_KEYS
|
|
715
|
+
// — the invariant tests/pack/mcp-pack-params.test.js guards (the
|
|
716
|
+
// KIND_OPTIONS.solo round-trip test, mutation-proven against a synthetic
|
|
717
|
+
// orphaned knob). If that invariant ever breaks, this loop is what turns a
|
|
718
|
+
// silent drop into a visible notice — never delete it as unreachable.
|
|
696
719
|
for (const n of packNotices) { spawnContent.push({ type: 'text', text: n }); }
|
|
697
720
|
if (isHeadless) {
|
|
698
721
|
spawnContent.push({ type: 'text', text: HEADLESS_START_REMINDER });
|
|
@@ -984,37 +1007,11 @@ const handlers = {
|
|
|
984
1007
|
const root = path.join(cwd, '.claude', SESSIONS_DIR);
|
|
985
1008
|
if (!fs.existsSync(root)) { return textResult('No amicus sessions found.'); }
|
|
986
1009
|
|
|
1010
|
+
// v4.7 F8 (D14): one enumeration behind both the CLI and MCP list surfaces
|
|
1011
|
+
// — enumerateSessions is the shared core (src/sidecar/read.js).
|
|
1012
|
+
const { enumerateSessions, searchSessions } = require('./sidecar/read');
|
|
987
1013
|
const byId = new Map();
|
|
988
|
-
for (const
|
|
989
|
-
if (!/^[a-zA-Z0-9_-]{1,64}$/.test(d)) { continue; }
|
|
990
|
-
if (byId.has(d)) { continue; }
|
|
991
|
-
const metaPath = path.join(root, d, 'metadata.json');
|
|
992
|
-
if (!fs.existsSync(metaPath)) { continue; }
|
|
993
|
-
try {
|
|
994
|
-
const meta = JSON.parse(fs.readFileSync(metaPath, 'utf-8'));
|
|
995
|
-
const entry = {
|
|
996
|
-
id: d, model: meta.model, status: meta.status, agent: meta.agent,
|
|
997
|
-
briefing: sanitizePreview(String(meta.briefing || ''), 80),
|
|
998
|
-
createdAt: meta.createdAt,
|
|
999
|
-
mode: meta.mode
|
|
1000
|
-
|| (meta.headless === undefined ? undefined : (meta.headless ? 'headless' : 'interactive')),
|
|
1001
|
-
};
|
|
1002
|
-
// Live-progress enrichment for RUNNING sessions only — readProgress
|
|
1003
|
-
// parses conversation.jsonl, so terminal rows stay cheap.
|
|
1004
|
-
if (meta.status === 'running') {
|
|
1005
|
-
try {
|
|
1006
|
-
const p = readProgress(path.join(root, d));
|
|
1007
|
-
entry.phase = deriveStage(meta.status, p.stage);
|
|
1008
|
-
entry.messageCount = p.messages;
|
|
1009
|
-
entry.lastActivityAt = p.lastActivityAt;
|
|
1010
|
-
entry.latestPreview = p.latestPreview;
|
|
1011
|
-
} catch { /* progress optional */ }
|
|
1012
|
-
}
|
|
1013
|
-
byId.set(d, entry);
|
|
1014
|
-
} catch {
|
|
1015
|
-
// Skip unreadable metadata
|
|
1016
|
-
}
|
|
1017
|
-
}
|
|
1014
|
+
for (const row of enumerateSessions(cwd, {})) { byId.set(row.id, row); }
|
|
1018
1015
|
|
|
1019
1016
|
// v4.0 §8: council runs are pointer files in the same sessions root — merge
|
|
1020
1017
|
// them as first-class rows (type 'council-run') before sorting/filtering.
|
|
@@ -1025,8 +1022,34 @@ const handlers = {
|
|
|
1025
1022
|
if (input.status && input.status !== 'all') {
|
|
1026
1023
|
sessions = sessions.filter(s => s.status === input.status);
|
|
1027
1024
|
}
|
|
1025
|
+
// v4.7 F8 (D15, errata E-PR3-5): search runs on RAW briefing material —
|
|
1026
|
+
// BEFORE the sanitize/enrich pass below truncates row.briefing to an
|
|
1027
|
+
// 80-char preview. Council rows' material is read straight off disk
|
|
1028
|
+
// inside searchSessions; the `briefing` merged onto them here is already
|
|
1029
|
+
// listCouncilRuns' own sanitized preview and is never used for matching.
|
|
1030
|
+
if (input.search) { sessions = searchSessions(sessions, input.search, { project: cwd }); }
|
|
1028
1031
|
if (sessions.length === 0) { return textResult('No amicus sessions found.'); }
|
|
1029
1032
|
|
|
1033
|
+
// Rows are re-grafted here with the EXACT decorations this handler always
|
|
1034
|
+
// applied: sanitized (not raw) briefing, and running-only live-progress
|
|
1035
|
+
// enrichment. Council rows already carry a sanitized briefing from
|
|
1036
|
+
// listCouncilRuns, so they're left alone.
|
|
1037
|
+
for (const row of sessions) {
|
|
1038
|
+
if (row.type === 'council-run') { continue; }
|
|
1039
|
+
row.briefing = sanitizePreview(String(row.briefing || ''), 80);
|
|
1040
|
+
// Live-progress enrichment for RUNNING sessions only — readProgress
|
|
1041
|
+
// parses conversation.jsonl, so terminal rows stay cheap.
|
|
1042
|
+
if (row.status === 'running') {
|
|
1043
|
+
try {
|
|
1044
|
+
const p = readProgress(path.join(root, row.id));
|
|
1045
|
+
row.phase = deriveStage(row.status, p.stage);
|
|
1046
|
+
row.messageCount = p.messages;
|
|
1047
|
+
row.lastActivityAt = p.lastActivityAt;
|
|
1048
|
+
row.latestPreview = p.latestPreview;
|
|
1049
|
+
} catch { /* progress optional */ }
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1030
1053
|
return textResult(JSON.stringify(sessions, null, 2));
|
|
1031
1054
|
},
|
|
1032
1055
|
|
|
@@ -1237,6 +1260,17 @@ const handlers = {
|
|
|
1237
1260
|
if (fwd.error) { return textResult(fwd.error.message + (fwd.error.hint ? `\n${fwd.error.hint}` : ''), true); }
|
|
1238
1261
|
packNotices.push(...fwd.notices);
|
|
1239
1262
|
|
|
1263
|
+
// ⚠️ v4.7 PR7: the zod schema closes the TYPED door; a pack can push the same values through
|
|
1264
|
+
// the other one (validatePack checks option KEY names, never value types). Both entrances
|
|
1265
|
+
// reach the same spawn, so the check lives here, after pack merge, before any wave dir.
|
|
1266
|
+
if (typeof input.prompt !== 'string' || !input.prompt.trim()) {
|
|
1267
|
+
return textResult('Error: prompt must not be empty.', true);
|
|
1268
|
+
}
|
|
1269
|
+
if (input.timeout !== undefined
|
|
1270
|
+
&& (typeof input.timeout !== 'number' || !Number.isFinite(input.timeout) || input.timeout <= 0)) {
|
|
1271
|
+
return textResult('Error: timeout must be a positive number of minutes.', true);
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1240
1274
|
// Resolve a single effective models list (council OR models), validated
|
|
1241
1275
|
// BEFORE any wave dir / metadata is written so a bad request never strands
|
|
1242
1276
|
// a pid-less 'running' orphan wave.
|
|
@@ -1270,15 +1304,34 @@ const handlers = {
|
|
|
1270
1304
|
const waveDir = getSessionDir(cwd, waveId);
|
|
1271
1305
|
|
|
1272
1306
|
let briefingPath;
|
|
1307
|
+
let childPromptPath;
|
|
1273
1308
|
try {
|
|
1274
1309
|
fs.mkdirSync(waveDir, { recursive: true, mode: 0o700 });
|
|
1275
1310
|
briefingPath = path.join(waveDir, 'briefing.md');
|
|
1311
|
+
// ⚠️ W1-M4 (v4.7 PR7): this default is LOAD-BEARING. Omit it and every non-template wave
|
|
1312
|
+
// spawns with `--prompt-file undefined`.
|
|
1313
|
+
childPromptPath = briefingPath;
|
|
1276
1314
|
// The prompt goes via file: the spawned command line must NOT carry it,
|
|
1277
1315
|
// or it re-hits the ~32KB Windows argument cap (F4 spec §4.2).
|
|
1278
|
-
|
|
1316
|
+
// ⚠️ W1-M4: briefing.md is the SEARCH CORPUS — src/sidecar/list-search.js reads it verbatim
|
|
1317
|
+
// — and a child that aborts before fanout.js:145 never re-renders it, leaving the wave
|
|
1318
|
+
// permanently unfindable by the text the user actually sees. Write the RENDERED text here
|
|
1319
|
+
// (parity with the amicus_start path at :441/:506) and hand the child the raw input in a
|
|
1320
|
+
// sibling file, so its own later re-render still produces byte-identical output and
|
|
1321
|
+
// promptMeta.template provenance survives.
|
|
1322
|
+
const briefingText = fwd.renderedPrompt !== undefined ? fwd.renderedPrompt : input.prompt;
|
|
1323
|
+
fs.writeFileSync(briefingPath, briefingText, { mode: 0o600 });
|
|
1324
|
+
if (fwd.renderedPrompt !== undefined) {
|
|
1325
|
+
childPromptPath = path.join(waveDir, 'briefing-input.md');
|
|
1326
|
+
fs.writeFileSync(childPromptPath, input.prompt, { mode: 0o600 });
|
|
1327
|
+
}
|
|
1328
|
+
// ⚠️ W1-M4 rider (owner-approved fold): the `briefing` key below must stay — without it the
|
|
1329
|
+
// `list` BRIEFING column renders EMPTY for an aborted MCP wave, since only fanout.js:147
|
|
1330
|
+
// ever wrote a briefing key and an aborted child never reaches it.
|
|
1279
1331
|
writeFileAtomic(path.join(waveDir, 'metadata.json'), JSON.stringify({
|
|
1280
1332
|
taskId: waveId, type: 'wave', status: 'running', legs: legIds,
|
|
1281
1333
|
models: effectiveModels, headless: true, createdAt: new Date().toISOString(),
|
|
1334
|
+
briefing: briefingText.slice(0, 200),
|
|
1282
1335
|
// v4.5 Task 15: additive-only — absent (not null) without a pack.
|
|
1283
1336
|
...(packRecord ? { pack: packRecord } : {}),
|
|
1284
1337
|
}, null, 2), { mode: 0o600 });
|
|
@@ -1292,7 +1345,7 @@ const handlers = {
|
|
|
1292
1345
|
|
|
1293
1346
|
const args = [
|
|
1294
1347
|
'fanout', '--models', effectiveModels.join(','),
|
|
1295
|
-
'--prompt-file',
|
|
1348
|
+
'--prompt-file', childPromptPath, '--wave-id', waveId,
|
|
1296
1349
|
'--json', '--client', detectClient(mcpServer), '--cwd', cwd,
|
|
1297
1350
|
];
|
|
1298
1351
|
const agent = input.agent || 'Build';
|
|
@@ -1317,6 +1370,12 @@ const handlers = {
|
|
|
1317
1370
|
// itself is never forwarded, only the two knobs it resolved to).
|
|
1318
1371
|
if (packForward.maxCost !== undefined) { args.push('--max-cost', String(packForward.maxCost)); }
|
|
1319
1372
|
if (packForward.template !== undefined) { args.push('--template', packForward.template); }
|
|
1373
|
+
// v4.7 F8 (D13, errata E-PR3-2): argv-only forward — unlike pack, this does
|
|
1374
|
+
// NOT pre-seed the wave metadata (no single-resolution rule forces it here);
|
|
1375
|
+
// the spawned CLI child's own cli-handlers-fanout.js stores the tag on wave
|
|
1376
|
+
// metadata itself (Task 3), so fanout.js's metaTag inherit arm has no MCP
|
|
1377
|
+
// producer and is defense-in-depth only.
|
|
1378
|
+
if (input.tag) { args.push('--tag', input.tag); }
|
|
1320
1379
|
|
|
1321
1380
|
let child;
|
|
1322
1381
|
try { child = spawnSidecarProcess(args, waveDir); } catch (err) {
|
|
@@ -1506,4 +1565,5 @@ async function startMcpServer() {
|
|
|
1506
1565
|
|
|
1507
1566
|
module.exports = {
|
|
1508
1567
|
handlers, startMcpServer, getProjectDir, resolveProjectDir, getClientRoot,
|
|
1568
|
+
FANOUT_PACK_PARAM_MAP, SOLO_PACK_PARAM_MAP,
|
|
1509
1569
|
};
|