amicus 4.7.1 → 4.8.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 +882 -1
- package/README.md +3 -2
- package/docs/CITATIONS.md +114 -0
- package/docs/ROADMAP.md +33 -5
- package/docs/SHIMS.md +1 -1
- package/docs/configuration.md +2 -2
- package/docs/council.md +179 -32
- package/docs/doc-system.md +1 -1
- package/docs/publishing.md +2 -0
- package/docs/troubleshooting.md +3 -3
- package/docs/usage.md +2 -2
- package/electron/setup-ui-aliases.js +2 -2
- package/electron/workspace-ui/index.html +9 -0
- package/electron/workspace-ui/live-dead-seats.js +228 -0
- package/electron/workspace-ui/live-model.js +10 -236
- package/electron/workspace-ui/live-seats.js +126 -0
- package/electron/workspace-ui/workspace-app.js +6 -41
- package/electron/workspace-ui/workspace-banners.js +95 -0
- package/electron/workspace-ui/workspace-lazy.js +55 -12
- package/electron/workspace-ui/workspace-matrix.js +2 -2
- package/electron/workspace-ui/workspace-panels.js +42 -10
- package/electron/workspace-ui/workspace-render.js +2 -2
- package/electron/workspace-ui/workspace-seats.js +101 -17
- package/package.json +3 -1
- package/schemas/council-run-live.schema.json +1 -0
- package/schemas/council-run.schema.json +19 -0
- package/schemas/council-tally.schema.json +34 -2
- package/schemas/council-verdict.schema.json +15 -0
- package/skills/second-opinion/COUNCIL-DESIGN.md +9 -4
- package/skills/second-opinion/MANUAL-ORCHESTRATION.md +1 -1
- package/skills/second-opinion/SKILL.md +25 -11
- package/src/cli-handlers-doctor.js +7 -0
- package/src/council/anonymize.js +80 -11
- package/src/council/briefings-chair.js +272 -0
- package/src/council/briefings-stage2.js +12 -140
- package/src/council/debate.js +120 -15
- package/src/council/ledger-join.js +284 -0
- package/src/council/ledger-stats.js +100 -0
- package/src/council/ledger.js +177 -88
- package/src/council/peer-split.js +196 -0
- package/src/council/report-html.js +12 -5
- package/src/council/report-md.js +146 -0
- package/src/council/report.js +188 -112
- package/src/council/run-assemble.js +100 -79
- package/src/council/run-chair.js +17 -1
- package/src/council/run-debate-revote.js +268 -0
- package/src/council/run-debate.js +92 -102
- package/src/council/run-finish.js +70 -0
- package/src/council/run-launch.js +45 -18
- package/src/council/run-retry-group.js +266 -0
- package/src/council/run-retry-keys.js +74 -0
- package/src/council/run-retry-launch.js +55 -0
- package/src/council/run-retry-notes.js +65 -13
- package/src/council/run-retry.js +166 -161
- package/src/council/run-stage1-launch.js +27 -10
- package/src/council/run-stage1-rows.js +220 -0
- package/src/council/run-stage1-superseded.js +156 -0
- package/src/council/run-stage2.js +65 -5
- package/src/council/run-stages.js +72 -69
- package/src/council/run-state.js +1 -1
- package/src/council/run-stats-entry.js +71 -0
- package/src/council/run-verdict-files.js +52 -0
- package/src/council/run.js +42 -52
- package/src/council/seats.js +262 -0
- package/src/council/stage1-bind.js +142 -0
- package/src/council/street-cred.js +258 -0
- package/src/council/tally.js +100 -49
- package/src/council/verdict.js +49 -1
- package/src/headless.js +20 -6
- package/src/mcp-tools.js +71 -1
- package/src/observe/council-legs.js +7 -1
- package/src/observe/live-doc.js +3 -3
- package/src/sidecar/fanout-leg.js +22 -1
- package/src/sidecar/fanout-wave-io.js +26 -1
- package/src/sidecar/fanout.js +4 -10
- package/src/sidecar/leg-ids.js +19 -0
- package/src/sidecar/models-probe.js +7 -4
- package/src/sidecar/reopen-spend.js +1 -1
- package/src/sidecar/setup.js +13 -2
- package/src/utils/config.js +109 -11
- package/src/utils/curated-models.js +17 -2
- package/src/utils/degrade.js +5 -0
- package/src/utils/no-output-backstop.js +1 -1
- package/src/utils/remediation-hints.js +13 -0
- package/src/utils/session-index-prune.js +297 -0
- package/src/utils/session-metadata-tmp-sweep.js +1 -1
- package/src/workspace/artifact-guard.js +8 -114
- package/src/workspace/artifact-names.js +222 -0
- package/src/workspace/fold-format.js +9 -6
- package/src/workspace/live-normalize.js +6 -2
- package/src/workspace/matrix-model.js +141 -19
- package/src/workspace/run-detail.js +27 -1
- package/src/workspace/seat-space.js +143 -0
package/src/council/tally.js
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
// src/council/tally.js
|
|
2
2
|
'use strict';
|
|
3
|
+
const { peersOf, unattributedPeerDrops } = require('./peer-split');
|
|
4
|
+
// The street-cred half (rankPositions / credSeats / computeStreetCred) lives in
|
|
5
|
+
// ./street-cred (v4.8 T3.3 size-gate split; this file stood at 301 of 300 with
|
|
6
|
+
// the seat-keying in it — release Constraint 6 is EXTRACT, never shave).
|
|
7
|
+
// computeStreetCred is re-exported below, so no existing import path moved.
|
|
8
|
+
const { computeStreetCred } = require('./street-cred');
|
|
3
9
|
|
|
4
10
|
/**
|
|
5
11
|
* Peers-only tier cascade. a/d are agree/dispute counts among PEER judges
|
|
@@ -26,49 +32,9 @@ function assignTier(a, d) {
|
|
|
26
32
|
return { tier, confidence };
|
|
27
33
|
}
|
|
28
34
|
|
|
29
|
-
function mean(arr) { return arr.reduce((s, x) => s + x, 0) / arr.length; }
|
|
30
|
-
|
|
31
|
-
/** Map each model to its (possibly fractional) rank position in one judge's order. */
|
|
32
|
-
function rankPositions(order) {
|
|
33
|
-
const pos = new Map();
|
|
34
|
-
let p = 1;
|
|
35
|
-
for (const slot of order) {
|
|
36
|
-
const group = Array.isArray(slot) ? slot : [slot];
|
|
37
|
-
const meanPos = p + (group.length - 1) / 2;
|
|
38
|
-
for (const m of group) { pos.set(m, meanPos); }
|
|
39
|
-
p += group.length;
|
|
40
|
-
}
|
|
41
|
-
return pos;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Both-numbers street-cred. Lower mean rank = better.
|
|
46
|
-
* @param {Array<{judge:string, order:Array<string|string[]>}>} rankings
|
|
47
|
-
* @param {string[]} models all reviewed models (incl. claude when in-council)
|
|
48
|
-
*/
|
|
49
|
-
function computeStreetCred(rankings, models) {
|
|
50
|
-
const judgePos = rankings.map(r => ({ judge: r.judge, pos: rankPositions(r.order) }));
|
|
51
|
-
return models.map(m => {
|
|
52
|
-
const all = [], peers = [], perJudgeRank = {};
|
|
53
|
-
for (const { judge, pos } of judgePos) {
|
|
54
|
-
if (!pos.has(m)) { continue; } // absent from this judge's ranking → skip
|
|
55
|
-
const rank = pos.get(m);
|
|
56
|
-
perJudgeRank[judge] = rank;
|
|
57
|
-
all.push(rank);
|
|
58
|
-
if (judge !== m) { peers.push(rank); }
|
|
59
|
-
}
|
|
60
|
-
return {
|
|
61
|
-
model: m,
|
|
62
|
-
withSelf: all.length ? mean(all) : null,
|
|
63
|
-
peersOnly: peers.length ? mean(peers) : null,
|
|
64
|
-
perJudgeRank,
|
|
65
|
-
};
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
|
|
69
35
|
// v4.0 §7: council family v2 — every council doc carries {schemaVersion, type}.
|
|
70
36
|
const COUNCIL_SCHEMA_VERSION = 2;
|
|
71
|
-
const VERDICTS = { agree: 'a', dispute: 'd', neutral: 'n' };
|
|
37
|
+
const VERDICTS = { __proto__: null, agree: 'a', dispute: 'd', neutral: 'n' };
|
|
72
38
|
|
|
73
39
|
function countTiers(findings) {
|
|
74
40
|
const counts = { Confirmed: 0, Contested: 0, Singleton: 0, Disputed: 0 };
|
|
@@ -86,31 +52,111 @@ function tally(input) {
|
|
|
86
52
|
const byFinding = new Map();
|
|
87
53
|
for (const adj of adjudications) {
|
|
88
54
|
if (!byFinding.has(adj.findingId)) { byFinding.set(adj.findingId, []); }
|
|
89
|
-
byFinding.get(adj.findingId).push({ judge: adj.judge, verdict: adj.verdict });
|
|
55
|
+
byFinding.get(adj.findingId).push({ judge: adj.judge, verdict: adj.verdict, ...(adj.seat ? { seat: adj.seat } : {}) });
|
|
90
56
|
}
|
|
91
57
|
const outFindings = findings.map(f => {
|
|
92
58
|
const votes = byFinding.get(f.id) || [];
|
|
93
|
-
|
|
94
|
-
//
|
|
95
|
-
//
|
|
96
|
-
|
|
59
|
+
const peers = peersOf(f, votes);
|
|
60
|
+
// v4.8 T-B2: how many votes `peersOf` excluded without being able to
|
|
61
|
+
// attribute them — the one-sided alias fallback, plus (T-B4) the falsy
|
|
62
|
+
// judges of a falsy raiser. ⚠️ NOT the votes the SEAT ids attributed: when
|
|
63
|
+
// both sides carry a seat id the engine knows whose vote it is, so that
|
|
64
|
+
// exclusion is announced by nothing. Same function and same emit rule
|
|
65
|
+
// (> 0 only) as debate.js :: debateTargets, so this document and the
|
|
66
|
+
// defense brief can never announce different numbers.
|
|
67
|
+
// ⚠️ On the SEATED shapes — tally.test.js T1 and T2, the one-sided twin
|
|
68
|
+
// pair — `basis` deliberately does NOT move: counting the ambiguous vote
|
|
69
|
+
// reproduces the naive filter's outcome, measured Confirmed on both, which
|
|
70
|
+
// re-arms #137. Announced, not counted. ⚠️ T-B4 is the deliberate exception
|
|
71
|
+
// and the scope of that sentence narrowed with it: on a finding whose
|
|
72
|
+
// raiser is FALSY, `basis` DOES move, because there the ambiguous vote is
|
|
73
|
+
// the raiser's own inflating its own basis rather than a twin's signal
|
|
74
|
+
// going uncounted (council C1 — see peer-split.js :: peersOf).
|
|
75
|
+
const drops = unattributedPeerDrops(f, votes);
|
|
97
76
|
const basis = { a: 0, d: 0, n: 0 };
|
|
98
77
|
// Skip unknown verdict strings so a stray value can't corrupt the basis via
|
|
99
|
-
// basis[undefined] = NaN (L9).
|
|
78
|
+
// basis[undefined] = NaN (L9). Guaranteed by VERDICTS's `__proto__: null`,
|
|
79
|
+
// not `!== undefined` alone — measured, all four Object.prototype keys
|
|
80
|
+
// (toString/__proto__/constructor/valueOf) read undefined on it. VERDICTS
|
|
81
|
+
// is module-local, absent from module.exports, so this isn't observable
|
|
82
|
+
// outside this file.
|
|
100
83
|
for (const v of peers) {
|
|
101
84
|
const key = VERDICTS[v.verdict];
|
|
102
85
|
if (key !== undefined) { basis[key] += 1; }
|
|
103
86
|
}
|
|
104
87
|
const { tier, confidence } = assignTier(basis.a, basis.d);
|
|
88
|
+
// v4.8 PR4c §3.3 (R8): `peers` has already excluded the raiser BY SEAT, so a
|
|
89
|
+
// surviving peer whose ALIAS equals the raiser's is a different seat of the
|
|
90
|
+
// same model — corroboration that is not independent. Emitted only when TRUE
|
|
91
|
+
// (an unconditional `false` would change every document's shape).
|
|
92
|
+
// ⚠️ The leading `f.raiser &&` is LOAD-BEARING, not decoration — and the
|
|
93
|
+
// interesting part is that this sentence was FALSE for one commit inside
|
|
94
|
+
// v4.8 T-B4, so it is written with its measurement rather than its
|
|
95
|
+
// adjective. It is load-bearing because `peersOf` can hand back a
|
|
96
|
+
// seat-carrying vote whose `judge` is falsy, and then `v.judge === f.raiser`
|
|
97
|
+
// reads `undefined === undefined` on the CLI path (cli-handlers-council.js
|
|
98
|
+
// is a raw JSON.parse with no schema) and `'' === ''` on the MCP path
|
|
99
|
+
// (mcp-tools.js's z.string() accepts the empty string) — so without the
|
|
100
|
+
// guard this stamp fires on documents that name no models at all.
|
|
101
|
+
// MEASURED at each step over the 768-shape cross-product of (f.raiser,
|
|
102
|
+
// f.raiserSeat, v.judge, v.seat, verdict): deleting the guard flipped 8
|
|
103
|
+
// shapes at 64b835b8, ZERO after T-B4 round 1 — which had made `peersOf`
|
|
104
|
+
// drop every falsy-judge vote of a falsy raiser, briefly disarming the very
|
|
105
|
+
// pins that guarded this — and 4 after round 2, whose P0 rule counts the
|
|
106
|
+
// ones the SEAT ids prove are real peers. The 4 are the seat-DIFFER shapes
|
|
107
|
+
// of the T7b and T7d families, i.e. exactly those two tests. Re-run this
|
|
108
|
+
// after any edit to peer-split.js :: peersOf; do not infer it.
|
|
109
|
+
// ⚠️ Alias-only in BOTH directions, and the CHANGELOG says so: it misses
|
|
110
|
+
// `gpt-5,openai/gpt-5` (one model, two aliases — votes carry no
|
|
111
|
+
// resolvedModel) and it fires falsely on a SPLIT alias, whose two seats
|
|
112
|
+
// resolved to different executables. PR4b's ledger treats
|
|
113
|
+
// (alias, resolvedModel) as identity; this stamp is same-ALIAS only.
|
|
114
|
+
// SI-23 (R10): `location` is declared on the MCP schema now
|
|
115
|
+
// (mcp-tools.js :: getTools) so it survives zod validation, but
|
|
116
|
+
// surviving validation and reaching THIS document are two different
|
|
117
|
+
// properties — the round-trip pin below only holds if this map also
|
|
118
|
+
// forwards it, emit-when-present, the same convention as `raiserSeat`
|
|
119
|
+
// two lines up. Scoped to `location` alone AT R10: `claim` was already
|
|
120
|
+
// reaching this function on both the CLI and MCP paths (declared on the
|
|
121
|
+
// MCP schema since before R10) and was ALREADY not forwarded here — R10
|
|
122
|
+
// named the gap and left it for a separate PR rather than widening its
|
|
123
|
+
// own mandate to close it. SI-23 fix round 1 (paid council on PR #183,
|
|
124
|
+
// findings A1/B1, two independent raisers) ruled that indefensible one
|
|
125
|
+
// line from the fix it sits beside: `claim` is forwarded below too,
|
|
126
|
+
// same round.
|
|
127
|
+
// A2 (nit) / C1 (major, contested a0/d1/n2, thin): the truthiness checks
|
|
128
|
+
// below drop an empty string that zod would accept. Deliberate, not a
|
|
129
|
+
// bug — `raiserSeat` above uses the identical pattern, and diverging
|
|
130
|
+
// `location`/`claim` from their own immediate sibling is worse than the
|
|
131
|
+
// edge case, so "" is treated as absent on all three fields.
|
|
105
132
|
return { id: f.id, raiser: f.raiser, severity: f.severity, tier, basis, confidence,
|
|
106
|
-
tierOverride: null, adjudications: votes }
|
|
133
|
+
tierOverride: null, adjudications: votes, ...(f.raiserSeat ? { raiserSeat: f.raiserSeat } : {}),
|
|
134
|
+
...(f.location ? { location: f.location } : {}),
|
|
135
|
+
...(f.claim ? { claim: f.claim } : {}),
|
|
136
|
+
...(f.raiser
|
|
137
|
+
&& peers.some(v => v.seat && f.raiserSeat && VERDICTS[v.verdict] === 'a' && v.judge === f.raiser)
|
|
138
|
+
? { sameModelCorroboration: true } : {}),
|
|
139
|
+
...(drops > 0 ? { unattributedPeerDrops: drops } : {}) };
|
|
107
140
|
});
|
|
108
141
|
return {
|
|
109
142
|
schemaVersion: COUNCIL_SCHEMA_VERSION,
|
|
110
143
|
type: 'council-tally',
|
|
111
144
|
meta,
|
|
112
145
|
judged: Array.isArray(rankings) && rankings.length >= 2,
|
|
113
|
-
|
|
146
|
+
// v4.8 T3.3: `meta.seats` joins BY VALUE inside computeStreetCred (never
|
|
147
|
+
// positionally against meta.models — run-assemble.js :: buildTallyInput
|
|
148
|
+
// forbids that). The ENGINE emits it only when the bench repeats an alias,
|
|
149
|
+
// so a unique-alias run leaves the rows alias-driven and byte-identical.
|
|
150
|
+
// ⚠️ DO NOT READ THAT AS "hand-assembled input is always alias-driven" —
|
|
151
|
+
// this sentence said so until fix round 1 and it was false, in the way that
|
|
152
|
+
// hides a defect rather than merely misinforming. `meta` is copied verbatim
|
|
153
|
+
// from user JSON on both hand-assembled appendRun paths, and
|
|
154
|
+
// mcp-tools.js :: amicus_council_tally DECLARES `meta.seats`, so such a
|
|
155
|
+
// record produces SEAT-driven street-cred rows here while its `runStats`
|
|
156
|
+
// rows — declared `z.array(z.record(z.any()))`, so never asked for a seat —
|
|
157
|
+
// carry none. That asymmetry is a live quadrant, not a hypothetical; it is
|
|
158
|
+
// what ledger-join.js :: credFor's second lookup exists for.
|
|
159
|
+
streetCred: computeStreetCred(rankings || [], meta.models, meta.seats),
|
|
114
160
|
findings: outFindings,
|
|
115
161
|
runStats: (runStats || []).map(r => ({
|
|
116
162
|
model: r.model, role: r.role, wasChair: !!r.wasChair, conformance: r.conformance || 'clean',
|
|
@@ -128,6 +174,11 @@ function tally(input) {
|
|
|
128
174
|
...(r.repairRefused ? { repairRefused: r.repairRefused } : {}),
|
|
129
175
|
...(r.waveId ? { waveId: r.waveId } : {}),
|
|
130
176
|
...(r.resolvedModel ? { resolvedModel: r.resolvedModel } : {}),
|
|
177
|
+
// v4.8 PR4c §3.1: `seat` rides the same slot. It is emitted upstream only
|
|
178
|
+
// when the bench repeats that alias (run-assemble.js's buildRunStatsEntry),
|
|
179
|
+
// so a unique-alias run is byte-for-byte unchanged here and in verdict.json,
|
|
180
|
+
// which copies this array verbatim (verdict.js :: buildVerdict).
|
|
181
|
+
...(r.seat ? { seat: r.seat } : {}),
|
|
131
182
|
status: r.status || 'unknown',
|
|
132
183
|
durationMs: typeof r.durationMs === 'number' ? r.durationMs : null,
|
|
133
184
|
usage: r.usage || null,
|
package/src/council/verdict.js
CHANGED
|
@@ -108,6 +108,13 @@ function buildVerdict(record, decisions = [], opts = {}) {
|
|
|
108
108
|
date: record.meta.date,
|
|
109
109
|
chair: record.meta.chair,
|
|
110
110
|
council: record.meta.models,
|
|
111
|
+
// v4.8 PR4c §3.2: this projection RENAMES meta.models to `council`, so
|
|
112
|
+
// nothing from meta reaches verdict.json unless it is named here — which is
|
|
113
|
+
// why the seat table needs its own line rather than riding tally's verbatim
|
|
114
|
+
// meta copy. Emitted only when the record carries one (a twin bench), so a
|
|
115
|
+
// unique-alias verdict is byte-for-byte unchanged. The key is `seats`,
|
|
116
|
+
// matching the `seatLoss` sibling below; PR5 codes against that name.
|
|
117
|
+
...(record.meta.seats ? { seats: record.meta.seats } : {}),
|
|
111
118
|
claudeInCouncil: record.meta.claudeInCouncil,
|
|
112
119
|
overallVerdict: opts.overallVerdict === undefined ? null : opts.overallVerdict,
|
|
113
120
|
findings: record.findings.map(f => {
|
|
@@ -121,11 +128,52 @@ function buildVerdict(record, decisions = [], opts = {}) {
|
|
|
121
128
|
adjudications: f.adjudications,
|
|
122
129
|
decision: d.decision || null,
|
|
123
130
|
applied: d.applied === true,
|
|
131
|
+
// v4.8 PR4c §3.4: this literal is CLOSED — it names every key and copies
|
|
132
|
+
// nothing else off `f` — so the two fields tally() stamps need their own
|
|
133
|
+
// lines or verdict.json names seats it cannot resolve (§1.2). Appended as
|
|
134
|
+
// a pure TAIL, leaving the shipped eleven-key order untouched.
|
|
135
|
+
// ⚠️ NOT `|| null`, even though `duplicateOf` and `decision` above are:
|
|
136
|
+
// `JSON.stringify({raiserSeat: null})` still WRITES `"raiserSeat":`, so
|
|
137
|
+
// that idiom changes the shape of every unique-alias verdict.json and
|
|
138
|
+
// fails seat-parity-ondisk's needles. `applied` is the sibling to copy —
|
|
139
|
+
// it computes a value rather than defaulting one. Emit-when-set matches
|
|
140
|
+
// both producers (tally.js) and keeps a non-twin verdict byte-identical.
|
|
141
|
+
...(f.raiserSeat ? { raiserSeat: f.raiserSeat } : {}),
|
|
142
|
+
...(f.sameModelCorroboration ? { sameModelCorroboration: true } : {}),
|
|
124
143
|
};
|
|
125
144
|
if (f.debate) { out.debate = f.debate; } // v4.1: additive debate decoration carry-through (spec §5.6)
|
|
126
145
|
return out;
|
|
127
146
|
}),
|
|
128
|
-
|
|
147
|
+
// v4.8 fix round 1 (review finding): emit-when-DIFFERENT, adapted to this
|
|
148
|
+
// row's flat {model, seat} shape — model is the alias, seat is the seat
|
|
149
|
+
// id, so on a unique-alias bench they are byte-equal and nothing is
|
|
150
|
+
// emitted (same semantics as `seat.id !== seat.alias` one layer up,
|
|
151
|
+
// run-stats-entry.js:64). NOT a plain pass-through like `raiserSeat`
|
|
152
|
+
// above (:141) — that field's upstream producer already holds a real
|
|
153
|
+
// {id, alias} seat OBJECT at its own decision point (run.js:202:
|
|
154
|
+
// `r.seat && r.seat.id !== r.seat.alias`), so passing its verdict
|
|
155
|
+
// through here is safe. The street-cred producer never has such an object
|
|
156
|
+
// at this point, only a flat row, so a pass-through here would leak `seat`
|
|
157
|
+
// onto every unique-alias verdict.json the moment that producer's own
|
|
158
|
+
// guard slipped — silently, since nothing else guards this closed literal.
|
|
159
|
+
// This check is deliberate defense in depth: buildVerdict is also reachable
|
|
160
|
+
// on externally-supplied records that never touched computeStreetCred
|
|
161
|
+
// in-process at all — the MCP `record` param of mcp-tools.js ::
|
|
162
|
+
// amicus_verdict is `z.record(z.any())`, fully permissive — so this
|
|
163
|
+
// literal's own byte-identity cannot be contingent on that producer alone;
|
|
164
|
+
// this file's own tests exercise that exact shape (hand-built rec objects,
|
|
165
|
+
// never calling tally()). tally.json keeps its own pin regardless — a
|
|
166
|
+
// genuine producer bug still reds at seat-parity-ondisk.test.js — this
|
|
167
|
+
// check exists so verdict.json is never the ONE document such a bug (or an
|
|
168
|
+
// externally-supplied record) masks.
|
|
169
|
+
// ⚠️ NO LONGER INERT. This comment said "computeStreetCred emits no `seat`
|
|
170
|
+
// at all yet" until v4.8 T3.3 shipped that producer — street-cred.js ::
|
|
171
|
+
// computeStreetCred, one row per SEAT with the id emitted when it differs
|
|
172
|
+
// from the alias. Both guards now fire on the same real documents, and the
|
|
173
|
+
// pin that proves this literal carries the field through lives at
|
|
174
|
+
// seat-parity-ondisk.test.js on a real runCouncil twin bench.
|
|
175
|
+
streetCred: record.streetCred.map(s => ({ model: s.model, withSelf: s.withSelf, peersOnly: s.peersOnly,
|
|
176
|
+
...(s.seat && s.seat !== s.model ? { seat: s.seat } : {}) })),
|
|
129
177
|
runStats: record.runStats,
|
|
130
178
|
tierCounts: record.tierCounts,
|
|
131
179
|
// Additive and OPTIONAL (schemaVersion stays 2): present only when a critic
|
package/src/headless.js
CHANGED
|
@@ -174,13 +174,13 @@ function withTimeout(promise, ms, label) {
|
|
|
174
174
|
* because raising the env var changes exactly this window.
|
|
175
175
|
* - fromEnv=false: `ms` arrived as a direct, caller-set numeric option.
|
|
176
176
|
* Task 6 review (Important finding): this is NOT synonymous with "the
|
|
177
|
-
* env var doesn't apply" —
|
|
178
|
-
* Stage-1 retry's
|
|
177
|
+
* env var doesn't apply" — `run-retry.js :: retryStage1Losses`'s
|
|
178
|
+
* `escalatedBackstopMs` computes a Stage-1 retry's window as
|
|
179
179
|
* `2 * (Number.isFinite(o.noOutputBackstopMs) ? o.noOutputBackstopMs :
|
|
180
|
-
* resolveNoOutputBackstopMs())` and forwards
|
|
181
|
-
* `noOutputBackstopMs`
|
|
182
|
-
* "caller-set" by this predicate while still being
|
|
183
|
-
* env default
|
|
180
|
+
* resolveNoOutputBackstopMs())` and forwards it on `common` as a direct
|
|
181
|
+
* `noOutputBackstopMs` — so a 600s retry-fired backstop (the 300s env
|
|
182
|
+
* default, doubled) is "caller-set" by this predicate while still being
|
|
183
|
+
* *derived from* the env default. Only src/sidecar/models-probe.js:79's hardcoded,
|
|
184
184
|
* non-tunable 30s (PROBE_WINDOW_MS; docs/usage.md:406 promises it's "not
|
|
185
185
|
* tunable") is truly independent of the env var. Because a real
|
|
186
186
|
* `fromEnv` flag distinguishing those two cases would have to ride the
|
|
@@ -1306,6 +1306,9 @@ async function runHeadless(model, systemPrompt, userMessage, taskId, project, ti
|
|
|
1306
1306
|
...settleResult,
|
|
1307
1307
|
...subtreeFlags,
|
|
1308
1308
|
...subtreeResult,
|
|
1309
|
+
// #133 P1: sessionId was assigned at :413/:417, well before this
|
|
1310
|
+
// return — guaranteed set here, same as `taskId` above.
|
|
1311
|
+
opencodeSessionId: sessionId,
|
|
1309
1312
|
error: sessionError
|
|
1310
1313
|
};
|
|
1311
1314
|
}
|
|
@@ -1321,6 +1324,8 @@ async function runHeadless(model, systemPrompt, userMessage, taskId, project, ti
|
|
|
1321
1324
|
...settleResult,
|
|
1322
1325
|
...subtreeFlags,
|
|
1323
1326
|
...subtreeResult,
|
|
1327
|
+
// #133 P1: see the comment on the sibling return above — guaranteed set.
|
|
1328
|
+
opencodeSessionId: sessionId,
|
|
1324
1329
|
exitCode: 0
|
|
1325
1330
|
};
|
|
1326
1331
|
|
|
@@ -1396,6 +1401,15 @@ async function runHeadless(model, systemPrompt, userMessage, taskId, project, ti
|
|
|
1396
1401
|
aborted: false,
|
|
1397
1402
|
taskId,
|
|
1398
1403
|
usage: emptyUsageTotals(),
|
|
1404
|
+
// #133 P1: measured, not assumed — `sessionId` (:365) is in scope
|
|
1405
|
+
// through this whole catch (it is already read at the `if (sessionId)`
|
|
1406
|
+
// abort-on-error above) but, unlike the two returns in the try body,
|
|
1407
|
+
// is NOT guaranteed assigned here: an exception thrown before session
|
|
1408
|
+
// creation (e.g. during the pre-session server-readiness wait) reaches
|
|
1409
|
+
// this same catch with sessionId still unset. `|| null` makes both
|
|
1410
|
+
// outcomes explicit and already schema-shaped (run.schema.json:23
|
|
1411
|
+
// wants string|null, never undefined).
|
|
1412
|
+
opencodeSessionId: sessionId || null,
|
|
1399
1413
|
error: error.message
|
|
1400
1414
|
};
|
|
1401
1415
|
}
|
package/src/mcp-tools.js
CHANGED
|
@@ -390,19 +390,89 @@ function getTools() {
|
|
|
390
390
|
'returns the tally record immediately. No subprocess, no polling. Claude ' +
|
|
391
391
|
'assembles the input and may override margin tiers afterward.',
|
|
392
392
|
inputSchema: {
|
|
393
|
+
// v4.8 PR4c R4c-5: the three seat keys are declared here or zod SILENTLY
|
|
394
|
+
// STRIPS them, and `amicus_council_tally` stays permanently on the #137
|
|
395
|
+
// behaviour while `amicus council tally` (a raw JSON.parse with no schema)
|
|
396
|
+
// gets the fix. Deliberately PERMISSIVE — validate the envelope, let
|
|
397
|
+
// tally() be the single arbiter of shape on both paths, matching
|
|
398
|
+
// amicus_verdict's own `record: z.record(z.any())`. `z.array(z.record())`
|
|
399
|
+
// would reject a string seat table the CLI accepts, and a bare
|
|
400
|
+
// `.optional()` would reject the `|| null` a hand-assembling caller
|
|
401
|
+
// writes, which serializes byte-identically to omitting the key.
|
|
402
|
+
// ⚠️ ALL THREE keys carry `.nullable()`, not two of three (council C1).
|
|
403
|
+
// Bare `.optional()` on `seats` failed the WHOLE call with `seats:
|
|
404
|
+
// Expected array, received null` where `amicus council tally` — the raw
|
|
405
|
+
// JSON.parse at cli-handlers-council.js:24 — accepts it and every
|
|
406
|
+
// seat-space reader treats it as absent (`Array.isArray(null)` is
|
|
407
|
+
// false). That is the same silent fork in the same schema, one key
|
|
408
|
+
// later. `amicus_verdict` below already spells its array/record
|
|
409
|
+
// envelope keys this way (`seatLoss` :461, `degrades` :465).
|
|
393
410
|
meta: z.object({
|
|
394
411
|
runId: z.string(), runType: z.string().optional(), date: z.string().optional(),
|
|
395
412
|
models: z.array(z.string()).min(1), chair: z.string().optional(),
|
|
396
|
-
claudeInCouncil: z.boolean().optional(),
|
|
413
|
+
claudeInCouncil: z.boolean().optional(), seats: z.array(z.any()).nullable().optional(),
|
|
397
414
|
}).describe('Run metadata; meta.models lists every reviewed model.'),
|
|
398
415
|
findings: z.array(z.object({
|
|
399
416
|
id: z.string(), raiser: z.string(), severity: z.string(), claim: z.string().optional(),
|
|
417
|
+
raiserSeat: z.string().nullable().optional(),
|
|
418
|
+
// SI-23 (R10): `location` was silently stripped by this closed
|
|
419
|
+
// z.object too — the same #137-shaped fork as the seat keys above,
|
|
420
|
+
// one key later. R10 (as relayed) also named `evidence`/`file`/`line`
|
|
421
|
+
// as siblings to fix here; MEASURED against findings.js :: REQUIRED
|
|
422
|
+
// (Stage-1's required-field list), briefings.js ::
|
|
423
|
+
// FINDINGS_JSON_SHAPE (the JSON contract every reviewer is briefed
|
|
424
|
+
// on), and every `f.<key>` read under src/ — none of those three
|
|
425
|
+
// exist anywhere in this codebase's finding shape. Only `location`
|
|
426
|
+
// does: anonymize.js :: toGlobalFindings forwards it alongside
|
|
427
|
+
// `claim` into the exact "run-global findings" shape this tool
|
|
428
|
+
// already documents below, and briefings-debate.js ::
|
|
429
|
+
// findingBlockDefense / debate.js :: debateTargets read it on the
|
|
430
|
+
// engine's native debate stage — unreachable from this MCP tool
|
|
431
|
+
// today (no MCP debate step exists), so this is a document-shape /
|
|
432
|
+
// round-trip fix here, not an unlocked renderer. `rationale` is also
|
|
433
|
+
// Stage-1-required but toGlobalFindings does not forward it past
|
|
434
|
+
// Stage-1 either; adding it here would invent a shape the engine's
|
|
435
|
+
// own path never produces, so it stays out.
|
|
436
|
+
// Plain `.optional()`, matching `claim` immediately above — a
|
|
437
|
+
// same-shape string sibling, not a seat field, so the `.nullable()`
|
|
438
|
+
// seat idiom explained above `meta` doesn't apply here.
|
|
439
|
+
// tally.js :: tally's `outFindings` map was ALSO dropping
|
|
440
|
+
// `claim`/`location` from every finding regardless of what survived
|
|
441
|
+
// validation, on the CLI and MCP paths alike — declaring the field
|
|
442
|
+
// here alone would validate but never reach the document, so that
|
|
443
|
+
// map forwards both, the same emit-when-present way it already
|
|
444
|
+
// forwards `raiserSeat`. R10 shipped only `location`'s forward and
|
|
445
|
+
// left `claim`'s for later (`claim` was already declared above,
|
|
446
|
+
// pre-R10); SI-23 fix round 1 (PR #183 council, A1/B1) closed that
|
|
447
|
+
// gap in the same map, same convention.
|
|
448
|
+
location: z.string().optional(),
|
|
400
449
|
})).describe('Run-global findings (ids already A1/B2/C3-prefixed by Claude).'),
|
|
401
450
|
adjudications: z.array(z.object({
|
|
402
451
|
judge: z.string(), findingId: z.string(), verdict: z.enum(['agree', 'dispute', 'neutral']),
|
|
452
|
+
seat: z.string().nullable().optional(),
|
|
403
453
|
})).describe('One row per (judge × finding).'),
|
|
454
|
+
// v4.8 T3.2: `seat` added here to match `adjudications.seat` immediately
|
|
455
|
+
// above — the channel run-assemble.js's buildTallyInput now puts on
|
|
456
|
+
// rankings[] (the judge's own seat, emit-when-DIFFERENT) is otherwise
|
|
457
|
+
// unreachable from a hand-assembled MCP call: zod strips unknown keys
|
|
458
|
+
// by default. NOT R10's territory — R10 covers SI-23 (findings[]
|
|
459
|
+
// losing evidence/file/line) as its own document-shape PR; this is a
|
|
460
|
+
// different field on a different array, and the adjudications.seat
|
|
461
|
+
// precedent immediately above is this repo's own pattern of adding a
|
|
462
|
+
// seat field in the PR that introduces it, not deferring to a later
|
|
463
|
+
// schema PR.
|
|
464
|
+
// v4.8 T3.3 adds `orderSeats` for the same reason and by the same rule.
|
|
465
|
+
// It is the seat-valued PARALLEL of `order` (street-cred.js ::
|
|
466
|
+
// rankPositions keys on it), so without the declaration an MCP caller
|
|
467
|
+
// that sends it gets the alias collapse SI-20 exists to fix, silently.
|
|
468
|
+
// `nullable()` inside the arrays is load-bearing, not defensive:
|
|
469
|
+
// anonymize.js :: rankingToOrder emits `null` for every slot with no
|
|
470
|
+
// distinguishing seat, so a mixed bench legitimately sends nulls.
|
|
404
471
|
rankings: z.array(z.object({
|
|
405
472
|
judge: z.string(), order: z.array(z.union([z.string(), z.array(z.string())])),
|
|
473
|
+
seat: z.string().nullable().optional(),
|
|
474
|
+
orderSeats: z.array(z.union([z.string().nullable(),
|
|
475
|
+
z.array(z.string().nullable())])).nullable().optional(),
|
|
406
476
|
})).describe("Each judge's preference order over the reviews (ties = nested array)."),
|
|
407
477
|
runStats: z.array(z.record(z.any())).optional().describe('Optional per-model run stats (status/duration/usage).'),
|
|
408
478
|
project: z.string().optional().describe('Optional project directory path.'),
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* em-dash) and blind mode's labelOf(alias) lookup never matches (real model
|
|
23
23
|
* id leaks). The alias IS on disk per-leg, though: every council leg goes
|
|
24
24
|
* through src/sidecar/fanout-leg.js's runSingleAttempt, which calls
|
|
25
|
-
* `writeLegPatch(legDir, { parentWave, modelInput })` synchronously,
|
|
25
|
+
* `writeLegPatch(legDir, { parentWave, modelInput, seat })` synchronously,
|
|
26
26
|
* immediately after leg creation (fanout-leg.js:101) — well before any
|
|
27
27
|
* status poll could reasonably observe it missing. So this module reads
|
|
28
28
|
* `modelInput` straight off the leg's own metadata.json, no run.json join
|
|
@@ -127,6 +127,12 @@ function buildLegRow(project, legId, runCtx) {
|
|
|
127
127
|
const row = {
|
|
128
128
|
taskId: legId, model: meta.model || null, status: meta.status || 'unknown',
|
|
129
129
|
modelInput, role,
|
|
130
|
+
// v4.8 R5: the leg's seat id (`alias#N`), written at launch by
|
|
131
|
+
// fanout-leg.js :: runSingleAttempt. Null on a unique-alias bench, where
|
|
132
|
+
// buildSeats sets id === alias and the shared emit-when-DIFFERENT predicate
|
|
133
|
+
// therefore stamps nothing — the same rule run-stats-entry.js ::
|
|
134
|
+
// buildRunStatsEntry applies on the terminal path.
|
|
135
|
+
seat: meta.seat || null,
|
|
130
136
|
};
|
|
131
137
|
let stalledMs = null;
|
|
132
138
|
let p = null;
|
package/src/observe/live-doc.js
CHANGED
|
@@ -26,9 +26,9 @@ const { resolveUsage, sumWaveUsage } = require('../utils/pricing');
|
|
|
26
26
|
// a timed-out single session never exited (watch-render.js:138 polls until
|
|
27
27
|
// TERMINAL.has(doc.status), and amicus_status stamps metadata.status straight onto
|
|
28
28
|
// the doc — mcp-server.js:687); a timed-out leg skipped the "prefer metadata.usage
|
|
29
|
-
// over the stale progress.json snapshot" branch in council-legs.js
|
|
30
|
-
// an under-counted cost; and markLive kept stamping view:'live' on a
|
|
31
|
-
// single-session doc.
|
|
29
|
+
// over the stale progress.json snapshot" branch in council-legs.js :: buildLegRow
|
|
30
|
+
// and reported an under-counted cost; and markLive kept stamping view:'live' on a
|
|
31
|
+
// finished single-session doc.
|
|
32
32
|
// NOTE this is deliberately NOT the same list as src/utils/result-schema.js:13 TERMINAL_STATUSES
|
|
33
33
|
// (the leg set, no 'partial'). Two mirrors of THIS list exist — src/workspace/run-detail.js:26 and
|
|
34
34
|
// electron/workspace-ui/live-model.js:14 — byte-identical, held by drift pins. Edit all three.
|
|
@@ -98,7 +98,7 @@ async function runSingleAttempt({ leg, legId, waveId, project, directory, follow
|
|
|
98
98
|
});
|
|
99
99
|
waveDir = getSessionDir(project, waveId);
|
|
100
100
|
emitLegStarted(waveDir, waveId, legId, leg.model, leg.modelInput, follow);
|
|
101
|
-
writeLegPatch(legDir, { parentWave: waveId, modelInput: leg.modelInput });
|
|
101
|
+
writeLegPatch(legDir, { parentWave: waveId, modelInput: leg.modelInput, seat: leg.seat });
|
|
102
102
|
saveInitialContext(legDir, systemPrompt, userMessage);
|
|
103
103
|
|
|
104
104
|
// Per-leg watchdog: a BACKSTOP strictly behind runHeadless's own deadline
|
|
@@ -158,6 +158,27 @@ async function runSingleAttempt({ leg, legId, waveId, project, directory, follow
|
|
|
158
158
|
reason: result.error || undefined,
|
|
159
159
|
completedAt: new Date().toISOString(),
|
|
160
160
|
usage: usage || undefined,
|
|
161
|
+
// #133 P1: `|| undefined`, matching status/reason/usage above — NOT the
|
|
162
|
+
// `toolSettleAborted` carve-out below, because a session id has no
|
|
163
|
+
// meaningful-falsy value the way `false` does there. A clean/failed
|
|
164
|
+
// attempt (no session ever created) therefore carries neither key on
|
|
165
|
+
// disk, rather than writing an explicit `null` that could clobber a
|
|
166
|
+
// PRIOR attempt's real session id: fallback substitution reruns
|
|
167
|
+
// runSingleAttempt against the SAME legId/legDir (fanout-leg-fallback.js
|
|
168
|
+
// "SAME leg dir under the SAME legId"), and writeLegPatch's read-merge-
|
|
169
|
+
// write only drops keys that are `undefined`, not ones explicitly set to
|
|
170
|
+
// `null`. This does not disagree with result-schema.js:72's
|
|
171
|
+
// `metadata.opencodeSessionId || null` — that coercion runs downstream,
|
|
172
|
+
// on the per-request OUTPUT run document, where run.schema.json requires
|
|
173
|
+
// the field always present as string|null. The two layers do different
|
|
174
|
+
// jobs (omit-if-absent on disk here vs. always-present in the emitted
|
|
175
|
+
// doc there) and compose without conflict.
|
|
176
|
+
//
|
|
177
|
+
// Raised again in the PR #185 council round (findings A3/D2) as a
|
|
178
|
+
// possible conflation of "no session" with "empty session id"; kept
|
|
179
|
+
// as-is per the reasoning above — this is `legPatch`'s existing
|
|
180
|
+
// convention (matching status/reason/usage), not a gap opened by #133 P1.
|
|
181
|
+
opencodeSessionId: result.opencodeSessionId || undefined,
|
|
161
182
|
// v4.4 B4 part 1: the leg completed with tool calls still live, so its
|
|
162
183
|
// OpenCode session may have kept working (and billing) afterwards. Travels
|
|
163
184
|
// with the leg so it is readable long after the run's stderr is gone.
|
|
@@ -75,13 +75,38 @@ async function finishWave({ wave, waveDir, waveId, project, exitCode, completedA
|
|
|
75
75
|
/**
|
|
76
76
|
* v4.3 §7.2 (moved here v4.7 PR3 Task 1): stamp council attribution onto every
|
|
77
77
|
* leg — fanout-leg's appendSpend reads it; no-op for every non-council caller.
|
|
78
|
-
* v4.7 F8 (Task 7) adds tag stamping in the same pass.
|
|
78
|
+
* v4.7 F8 (Task 7) adds tag stamping in the same pass. v4.8 R5 (T4.2) adds
|
|
79
|
+
* seat stamping in the same pass.
|
|
79
80
|
*/
|
|
80
81
|
function stampLegAttribution(legs, options) {
|
|
81
82
|
if (options.councilRunId || options.councilName) {
|
|
82
83
|
legs.forEach(l => { l.councilRunId = options.councilRunId; l.councilName = options.councilName; });
|
|
83
84
|
}
|
|
84
85
|
if (options.tag) { legs.forEach(l => { l.tag = options.tag; }); }
|
|
86
|
+
// v4.8 R5: seat identity for the LIVE path. `options.seats` is the launching
|
|
87
|
+
// wave's roster, index-parallel with `options.models` by construction
|
|
88
|
+
// (run-stage1-launch.js builds `roster` and `models` from the same filter, and
|
|
89
|
+
// fanout-validate.js:66-87 pushes exactly one leg per model on BOTH its ok and
|
|
90
|
+
// its unroutable branch, so a leg that never routed still holds its slot).
|
|
91
|
+
//
|
|
92
|
+
// ⚠️ emit-when-DIFFERENT, against the seat's OWN alias — the shared predicate
|
|
93
|
+
// stated at run-stats-entry.js :: buildRunStatsEntry, which the three sites in
|
|
94
|
+
// run-assemble.js also spell. buildSeats mints `alias#N` ONLY when an alias
|
|
95
|
+
// repeats (seats.js:67), so `id !== alias` IS "the bench repeats this alias",
|
|
96
|
+
// and on a unique bench `id` IS the alias — a bare `if (s.id)` would stamp an
|
|
97
|
+
// alias-valued seat onto every leg of every run: the same wrong output shape
|
|
98
|
+
// run-assemble.js:165-169 records having already fixed once there (a
|
|
99
|
+
// wrong-comparator bug, `!== j.judge` rather than `!== alias`, with a
|
|
100
|
+
// narrower trigger — only a leg/seat alias drift, not every leg).
|
|
101
|
+
// Comparing against the seat's own alias and never against `model` also makes
|
|
102
|
+
// this immune to the two cases where `model` is NOT the alias (a leg reporting
|
|
103
|
+
// no modelInput; a padded --council member).
|
|
104
|
+
if (Array.isArray(options.seats)) {
|
|
105
|
+
legs.forEach((l, i) => {
|
|
106
|
+
const s = options.seats[i];
|
|
107
|
+
if (s && s.id !== s.alias) { l.seat = s.id; }
|
|
108
|
+
});
|
|
109
|
+
}
|
|
85
110
|
}
|
|
86
111
|
|
|
87
112
|
module.exports = { writeWaveMetadata, writeWaveDoc, finishWave, stampLegAttribution };
|
package/src/sidecar/fanout.js
CHANGED
|
@@ -20,16 +20,10 @@ const { ERROR_CODES } = require('../utils/error-doc');
|
|
|
20
20
|
// Task 0.5). writeWaveMetadata is re-exported below — fanout-retry.js and the
|
|
21
21
|
// fanout tests import it from here.
|
|
22
22
|
const { writeWaveMetadata, writeWaveDoc, finishWave, stampLegAttribution } = require('./fanout-wave-io');
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
* @param {number} count
|
|
28
|
-
* @returns {string[]}
|
|
29
|
-
*/
|
|
30
|
-
function deriveLegIds(waveId, count) {
|
|
31
|
-
return Array.from({ length: count }, (_, i) => `${waveId}-${i + 1}`);
|
|
32
|
-
}
|
|
23
|
+
// Leg-id derivation lives in ./leg-ids (v4.8 PR0 size-gate split).
|
|
24
|
+
// deriveLegIds is re-exported below — mcp-server.js, fanout-retry.js and
|
|
25
|
+
// the fanout tests import it from here.
|
|
26
|
+
const { deriveLegIds } = require('./leg-ids');
|
|
33
27
|
|
|
34
28
|
/**
|
|
35
29
|
* Run a fan-out wave. Spec §4.3.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// deriveLegIds — the <waveId>-<i+1> leg-id convention. Moved verbatim from
|
|
3
|
+
// fanout.js:24-32 (v4.8 PR0 size-gate split, zero behavior; fanout.js was
|
|
4
|
+
// 300/300). The shape is load-bearing: council stage-1 composes
|
|
5
|
+
// `${runId}-s1` waves onto it, and ~10 suites plus a replay fixture
|
|
6
|
+
// hard-code the composite. Pinned by tests/sidecar/fanout.test.js:84-93
|
|
7
|
+
// through fanout.js's re-export.
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Derive leg task IDs: <waveId>-1 .. <waveId>-N (matches TASK_ID_PATTERN).
|
|
11
|
+
* @param {string} waveId
|
|
12
|
+
* @param {number} count
|
|
13
|
+
* @returns {string[]}
|
|
14
|
+
*/
|
|
15
|
+
function deriveLegIds(waveId, count) {
|
|
16
|
+
return Array.from({ length: count }, (_, i) => `${waveId}-${i + 1}`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
module.exports = { deriveLegIds };
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
19
|
/** Probe backstop override (spec D5) — a fixed constant, NOT env-configurable;
|
|
20
|
-
* the env knob (AMICUS_NO_OUTPUT_BACKSTOP_MS) stays the ordinary
|
|
20
|
+
* the env knob (AMICUS_NO_OUTPUT_BACKSTOP_MS) stays the ordinary 300s leg default. */
|
|
21
21
|
const PROBE_WINDOW_MS = 30000;
|
|
22
22
|
|
|
23
23
|
/** Fixed tiny prompt — a probe leg only needs to prove the model answers at all. */
|
|
@@ -28,9 +28,12 @@ const PROBE_PROMPT = 'Reply with exactly: OK';
|
|
|
28
28
|
* schema.js) per the plan's Global Constraints classification contract.
|
|
29
29
|
* Precedence matters: 'complete' wins outright; otherwise a NO_OUTPUT_
|
|
30
30
|
* BACKSTOP error (PR2's silent-leg detector, armed here at PROBE_WINDOW_MS
|
|
31
|
-
* instead of its
|
|
32
|
-
*
|
|
33
|
-
* ordinary routing/auth/timeout failure.
|
|
31
|
+
* instead of its 300s default) is the one specific error shape that means
|
|
32
|
+
* NOTHING arrived in the window — no output, no reasoning, no tool call —
|
|
33
|
+
* rather than an ordinary routing/auth/timeout failure. ⚠️ It does NOT prove
|
|
34
|
+
* the model "accepted the request": a stalled gateway or a dropped connection
|
|
35
|
+
* fires the same backstop. `accepted-but-silent` names the classification, not
|
|
36
|
+
* a fact about the endpoint (council B1 on PR #182).
|
|
34
37
|
* @param {{status?:string, error?:string|null}} leg
|
|
35
38
|
* @returns {'served'|'accepted-but-silent'|'error'}
|
|
36
39
|
*/
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
/**
|
|
11
11
|
* Resolve a reopened session's usage, write it onto metadata, and append one
|
|
12
12
|
* attributed ledger row. Mirrors start.js's finalize (the only sites that
|
|
13
|
-
* dropped usage - BACKLOG.md:280). Best-effort ledger append; never throws.
|
|
13
|
+
* dropped usage - BACKLOG.md :: "continue/resume never compute per-run usage" — ⚠️ cited `BACKLOG.md:280` until T-A8 re-opened it 2026-08-17: `:280` is a docs/usage.md line, the entry is `:289`). Best-effort ledger append; never throws.
|
|
14
14
|
* @returns {{usage: object|null}}
|
|
15
15
|
*/
|
|
16
16
|
function finalizeSpendForReopen({ taskId, model, mode, op, result, status, project, metadata }, ctx = {}) {
|
package/src/sidecar/setup.js
CHANGED
|
@@ -51,7 +51,11 @@ function createDefaultConfig(defaultModel) {
|
|
|
51
51
|
const cfg = {
|
|
52
52
|
...existing,
|
|
53
53
|
default: existing.default || defaultModel,
|
|
54
|
-
|
|
54
|
+
// Same restatement as the readline gate below (fix round 3, G-2): a spread
|
|
55
|
+
// into `{}` re-materialises Object.prototype. `saveConfig` rebuilds this
|
|
56
|
+
// into its own literal anyway, so this one is defense in depth rather than
|
|
57
|
+
// a measured hole — recorded as such rather than claimed as a fix.
|
|
58
|
+
aliases: { __proto__: null, ...getDefaultAliases(), ...(existing.aliases || {}) },
|
|
55
59
|
};
|
|
56
60
|
saveConfig(cfg);
|
|
57
61
|
logger.info('Default config ensured', {
|
|
@@ -128,7 +132,14 @@ function resolveChoice(input, picks, catalog) {
|
|
|
128
132
|
return { modelId: input };
|
|
129
133
|
}
|
|
130
134
|
const cfg = loadConfig();
|
|
131
|
-
|
|
135
|
+
// ⚠️ `__proto__: null` — v4.8 SI-22.4 fix round 3 (council G-2). `input` here
|
|
136
|
+
// is FREE-FORM readline text, and the gate below is `aliases[input] !==
|
|
137
|
+
// undefined`, so on a plain literal `toString` / `valueOf` / `constructor` /
|
|
138
|
+
// `hasOwnProperty` all measured TRUE and returned `{alias: input, noUpgrade:
|
|
139
|
+
// true}` — setup accepted them as existing aliases. Spreading a
|
|
140
|
+
// null-prototype object into a bare `{}` produces a PLAIN object again, so the
|
|
141
|
+
// curated-models fix does not reach this literal; the seed has to be restated.
|
|
142
|
+
const aliases = { __proto__: null, ...getDefaultAliases(), ...((cfg && cfg.aliases) || {}) };
|
|
132
143
|
if (aliases[input] !== undefined) {
|
|
133
144
|
return { alias: input, noUpgrade: true };
|
|
134
145
|
}
|