amicus 4.8.1 → 4.9.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 +249 -0
- package/README.md +8 -3
- package/docs/ROADMAP.md +101 -10
- package/docs/configuration.md +54 -4
- package/docs/council.md +102 -14
- package/docs/troubleshooting.md +9 -2
- package/docs/usage.md +128 -12
- package/electron/ipc-setup.js +36 -15
- package/electron/offer-session.js +51 -0
- package/electron/setup-ui.js +18 -8
- package/electron/workspace-ui/live-dead-seats.js +163 -91
- package/electron/workspace-ui/workspace-banners.js +30 -7
- package/electron/workspace-ui/workspace-matrix.js +23 -3
- package/electron/workspace-ui/workspace-seats.js +95 -79
- package/package.json +1 -1
- package/schemas/council-run.schema.json +2 -2
- package/schemas/council-tally.schema.json +17 -1
- package/schemas/council-verdict.schema.json +12 -4
- package/schemas/run.schema.json +6 -1
- package/skills/second-opinion/COUNCIL-DESIGN.md +1 -1
- package/skills/second-opinion/MANUAL-ORCHESTRATION.md +1 -1
- package/skills/second-opinion/MODEL-NOTES.md +88 -9
- package/skills/second-opinion/SEAT-BRIEFS.md +36 -4
- package/skills/second-opinion/SKILL.md +151 -36
- package/src/cli-council-run-bench.js +98 -6
- package/src/cli-handlers-council-run.js +18 -6
- package/src/cli-handlers-council.js +57 -7
- package/src/cli-handlers-doctor.js +1 -1
- package/src/cli.js +3 -1
- package/src/council/anonymize.js +2 -1
- package/src/council/briefings-chair-task.js +161 -0
- package/src/council/briefings-chair.js +33 -8
- package/src/council/briefings-debate.js +79 -13
- package/src/council/briefings-stage2-task.js +236 -0
- package/src/council/briefings-stage2.js +103 -26
- package/src/council/briefings-task.js +167 -0
- package/src/council/briefings.js +41 -4
- package/src/council/chair-fallback.js +95 -0
- package/src/council/debate.js +38 -21
- package/src/council/findings.js +3 -2
- package/src/council/ledger.js +2 -2
- package/src/council/parse-stage2.js +63 -15
- package/src/council/report-cost.js +61 -0
- package/src/council/report-html.js +26 -4
- package/src/council/report-md.js +30 -2
- package/src/council/report.js +40 -37
- package/src/council/run-assemble.js +21 -6
- package/src/council/run-chair.js +44 -95
- package/src/council/run-debate-revote.js +81 -49
- package/src/council/run-debate.js +51 -34
- package/src/council/run-finish.js +5 -3
- package/src/council/run-retry-keys.js +4 -4
- package/src/council/run-retry-launch.js +4 -4
- package/src/council/run-retry-notes.js +72 -15
- package/src/council/run-stage1-launch.js +4 -4
- package/src/council/run-stage1-rows.js +9 -6
- package/src/council/run-stage2.js +81 -47
- package/src/council/run-stages.js +9 -21
- package/src/council/run-stats-entry.js +46 -1
- package/src/council/run.js +28 -13
- package/src/council/seats.js +2 -2
- package/src/council/stage1-bind.js +3 -2
- package/src/council/verdict-seat-loss.js +124 -0
- package/src/council/verdict.js +108 -99
- package/src/headless.js +256 -49
- package/src/mcp-council-bench.js +64 -3
- package/src/mcp-council-run.js +10 -3
- package/src/mcp-server.js +52 -12
- package/src/mcp-tools.js +41 -5
- package/src/observe/council-legs.js +2 -2
- package/src/opencode-client.js +19 -1
- package/src/pack/pack-forward.js +15 -12
- package/src/pack/pack-resolve.js +1 -1
- package/src/prompt-builder.js +17 -1
- package/src/sidecar/fanout-leg.js +26 -0
- package/src/sidecar/fanout.js +1 -1
- package/src/sidecar/list-council.js +178 -0
- package/src/sidecar/list-limit.js +3 -1
- package/src/sidecar/list-search.js +2 -1
- package/src/sidecar/models.js +8 -1
- package/src/sidecar/read.js +34 -10
- package/src/template/render.js +16 -7
- package/src/utils/alias-shadow-writer.js +220 -0
- package/src/utils/alias-shadow.js +294 -0
- package/src/utils/config.js +1 -1
- package/src/utils/curated-models.js +11 -3
- package/src/utils/degrade.js +12 -5
- package/src/utils/doctor-alias-check.js +2 -5
- package/src/utils/engine-log-parse.js +289 -0
- package/src/utils/engine-log-tail.js +114 -0
- package/src/utils/engine-log.js +250 -0
- package/src/utils/engine-skew-records.js +146 -0
- package/src/utils/engine-skew.js +300 -0
- package/src/utils/gateway-router.js +10 -2
- package/src/utils/model-catalog.js +1 -1
- package/src/utils/result-schema.js +10 -0
- package/src/utils/text-sanitize.js +81 -0
- package/src/utils/ttft.js +57 -0
- package/src/utils/untrusted-fence.js +111 -1
- package/src/workspace/fold-format.js +28 -7
- package/src/workspace/live-normalize.js +2 -1
- package/src/workspace/matrix-model.js +6 -2
- package/src/workspace/run-detail.js +33 -7
- package/src/workspace/seat-space.js +10 -6
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// src/council/verdict-seat-loss.js
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @module council/verdict-seat-loss
|
|
6
|
+
* The verdict's seat-loss surface: `summarizeSeatLoss` (v4.5.2) and
|
|
7
|
+
* `deriveSeatLoss` (v4.6 Plan 2, spec D3) — lifted VERBATIM out of verdict.js
|
|
8
|
+
* for the 300-line gate (v4.9 PR #200 fix round 3), on the W4 chair-fallback
|
|
9
|
+
* precedent. The extraction itself was a pure MOVE: same bodies, same
|
|
10
|
+
* docblocks, same exports, and verdict.js re-exports both so no caller changes.
|
|
11
|
+
* (v4.9 W9 has since edited `deriveSeatLoss`'s body here — the identity pins
|
|
12
|
+
* below cover the MOVE, not a freeze.) Their behaviour stays pinned where it
|
|
13
|
+
* already was (tests/council/verdict-seat-loss.test.js,
|
|
14
|
+
* tests/council/verdict-degrades.test.js), both of which still import through
|
|
15
|
+
* verdict.js — if the move changed anything, those go red.
|
|
16
|
+
*
|
|
17
|
+
* Pure: no IO, no verdict-document knowledge. Nothing in this file requires
|
|
18
|
+
* verdict.js, so the dependency runs one way only.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Describe which requested seats actually reviewed, for the verdict's own face.
|
|
23
|
+
*
|
|
24
|
+
* ⚠️ ADDED v4.5.2 from a field report. The critic is a SOLO wave with one leg,
|
|
25
|
+
* so losing it loses 100% of the adversarial role — and unlike a dead bench wave
|
|
26
|
+
* (which trips the quorum gate and fails the run loudly) a dead critic is
|
|
27
|
+
* survivable, so the run continues to a full verdict, tally and chair synthesis
|
|
28
|
+
* that never saw the critic's findings. Run `dfb6a692` did exactly that and the
|
|
29
|
+
* only record was `deadWaves` in run.json, a file nobody opens when the verdict
|
|
30
|
+
* reads clean. A user who typed `--critic` asked for adversarial review; a
|
|
31
|
+
* verdict produced without it must say so where the verdict is read.
|
|
32
|
+
*
|
|
33
|
+
* Returns null when no critic was requested — there is nothing to report, and an
|
|
34
|
+
* always-present block would train readers to ignore it.
|
|
35
|
+
*
|
|
36
|
+
* @param {{runId: string, critic: ?string,
|
|
37
|
+
* deadWaves: Array<{waveId: string, models: string[], reason: string}>}} o
|
|
38
|
+
* @returns {?{criticRequested: string, criticSeated: boolean, reason: ?string,
|
|
39
|
+
* deadBenchSeats: string[]}}
|
|
40
|
+
*/
|
|
41
|
+
function summarizeSeatLoss({ runId, critic, deadWaves = [] } = {}) {
|
|
42
|
+
if (!critic) { return null; }
|
|
43
|
+
// Match on EITHER carrier. The `-c1` suffix is the convention run-stages.js
|
|
44
|
+
// uses, but a wave that names the critic model is the critic wave whatever it
|
|
45
|
+
// is called — and relying on the id alone would silently under-report if that
|
|
46
|
+
// convention ever changes.
|
|
47
|
+
const isCriticWave = w =>
|
|
48
|
+
w.waveId === `${runId}-c1` || (w.models || []).includes(critic);
|
|
49
|
+
const dead = deadWaves.find(isCriticWave) || null;
|
|
50
|
+
return {
|
|
51
|
+
criticRequested: critic,
|
|
52
|
+
criticSeated: !dead,
|
|
53
|
+
reason: dead ? dead.reason : null,
|
|
54
|
+
deadBenchSeats: deadWaves.filter(w => !isCriticWave(w))
|
|
55
|
+
.flatMap(w => w.models || []),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* seatLoss, derived from the sink's records (v4.6 Plan 2, spec D3 — closes #84).
|
|
61
|
+
*
|
|
62
|
+
* WHY A DERIVATION: two fields reporting lost seats can disagree; deriving one
|
|
63
|
+
* from the other makes contradiction inexpressible. `summarizeSeatLoss` stays
|
|
64
|
+
* exactly as v4.5.2 shipped it (its tests pass unedited — that is the proof the
|
|
65
|
+
* shape survived); this function rebuilds its wave input from `dead-wave`
|
|
66
|
+
* records and then adds the losses waves can never show: `dead-leg` records —
|
|
67
|
+
* a solo critic wave that STARTED but whose one leg died is invisible to
|
|
68
|
+
* deadWaves (#84's second half) — plus v4.9 W9's gated `seat-unbound` family.
|
|
69
|
+
*
|
|
70
|
+
* Reads ONLY record.data (Task 1's machine surface) — never the prose fields.
|
|
71
|
+
* @param {{runId: string, critic: ?string, degrades: Array<object>}} o
|
|
72
|
+
* @returns {?object} the summarizeSeatLoss shape, or null when no critic was requested
|
|
73
|
+
*/
|
|
74
|
+
function deriveSeatLoss({ runId, critic, degrades = [] } = {}) {
|
|
75
|
+
if (!critic) { return null; }
|
|
76
|
+
// v4.9 W9 (SI-02). ⚠️ THIRD spelling of one admission rule; its renderer twins are
|
|
77
|
+
// `live-dead-seats.js :: isSeatLoss` and `workspace-seats.js :: retriedSeats`, which cannot
|
|
78
|
+
// require src/ — all three move together, enforced only by workspace-seats.test.js's drift
|
|
79
|
+
// pin. `seat-unbound` is GATED: orphan-leg, re-vote and Stage-2 judge notes share it, are NOT
|
|
80
|
+
// seat losses, and carry no retry-family field. (R-W9a is CLOSED at the producer in the W9 fix
|
|
81
|
+
// round: `run-retry-notes.js :: skippedWaveNote` emits the `firstFailure` fact its record
|
|
82
|
+
// already carried, so this unchanged gate admits that real loss and still excludes the three.)
|
|
83
|
+
// ⚠️ The kind test admits a kind-LESS record as a degrade — W9 fix round, council C4. The
|
|
84
|
+
// POSITIVE-only spelling rested on an ASSERTED caller inventory (everything here comes from
|
|
85
|
+
// `makeDegrade`, which stamps the default), which is convention, not structure: one new
|
|
86
|
+
// caller, or one hand-written record, and a real seat loss vanishes silently. `report.js`
|
|
87
|
+
// learned it as mutant LEGACYDROP; the two renderers now spell it exactly as this line does.
|
|
88
|
+
// What all four agree on is that an ABSENT kind is a loss — their kind LISTS still differ,
|
|
89
|
+
// deliberately. `heal`/`info` name themselves here and are still excluded.
|
|
90
|
+
const gatedUnbound = d => d.channel === 'seat-unbound'
|
|
91
|
+
&& (d.data.retryWaveId || d.data.firstFailure) && (d.data.seatId || d.data.seat);
|
|
92
|
+
const real = degrades.filter(d => (d.kind === undefined || d.kind === 'degrade') && d.data
|
|
93
|
+
&& (d.channel === 'dead-leg' || d.channel === 'dead-wave' || gatedUnbound(d)));
|
|
94
|
+
const waves = real.filter(d => d.channel === 'dead-wave')
|
|
95
|
+
.map(d => ({ waveId: d.data.waveId, models: d.data.models || [], reason: d.data.reason }));
|
|
96
|
+
const base = summarizeSeatLoss({ runId, critic, deadWaves: waves });
|
|
97
|
+
const legs = real.filter(d => d.channel !== 'dead-wave');
|
|
98
|
+
// ⚠️ STAYS ALIAS-KEYED — decided and measured in v4.9 W9 (R4), not mirrored from the
|
|
99
|
+
// renderers' seat-key fix. `seats.js :: preflightSeats` REFUSES a critic alias occupying
|
|
100
|
+
// more than one bench seat, zero-spend, before any leg launches, so on every run this can see
|
|
101
|
+
// `data.seat === critic` names exactly one seat and alias equality IS seat equality; its only
|
|
102
|
+
// production caller, `run-verdict-files.js :: writeVerdictFiles`, is fed in-process records
|
|
103
|
+
// from that run's own sink. The renderers differ: they read run.json/verdict.json off DISK,
|
|
104
|
+
// any version or hand edit, where that refusal is not in force.
|
|
105
|
+
const criticLeg = legs.find(l => l.data.seat === critic) || null;
|
|
106
|
+
return {
|
|
107
|
+
...base,
|
|
108
|
+
criticSeated: base.criticSeated && !criticLeg,
|
|
109
|
+
// SL-2 handoff: a reconciliation note (run-retry-notes.js's
|
|
110
|
+
// missingLegStillDeadNote) carries data.status: null when the retry
|
|
111
|
+
// produced no leg for the seat at all — there is no status to name, so
|
|
112
|
+
// the old `ended '${status}'` template rendered the literal string
|
|
113
|
+
// "ended 'null'". A status-carrying record keeps the original text.
|
|
114
|
+
reason: base.reason || (criticLeg
|
|
115
|
+
? (criticLeg.data.reason || (criticLeg.data.status
|
|
116
|
+
? `the critic leg ended '${criticLeg.data.status}' with no usable output`
|
|
117
|
+
: 'the critic leg produced no usable output'))
|
|
118
|
+
: null),
|
|
119
|
+
deadBenchSeats: [...base.deadBenchSeats,
|
|
120
|
+
...legs.filter(l => l.data.seat !== critic).map(l => l.data.seat)],
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
module.exports = { summarizeSeatLoss, deriveSeatLoss };
|
package/src/council/verdict.js
CHANGED
|
@@ -2,100 +2,28 @@
|
|
|
2
2
|
'use strict';
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
|
-
const {
|
|
5
|
+
const { parseChairTerminal, CHAIR_VERDICTS, CHAIR_ANSWERS } = require('./parse-stage2');
|
|
6
|
+
// v4.9 PR #200 fix round 3: the seat-loss pair lives in its own leaf module (the
|
|
7
|
+
// 300-line gate), re-exported below so every existing caller and pin is unchanged.
|
|
8
|
+
const { summarizeSeatLoss, deriveSeatLoss } = require('./verdict-seat-loss');
|
|
6
9
|
|
|
7
10
|
// v4.0 §7: council family v2 — verdict docs carry {schemaVersion, type} and a
|
|
8
|
-
// nullable overallVerdict (the chair's
|
|
9
|
-
//
|
|
11
|
+
// nullable overallVerdict (the chair's terminal line — `VERDICT:` on a review
|
|
12
|
+
// run, `ANSWER:` on a task one; populated by the headless engine in Plan B via
|
|
13
|
+
// opts.overallVerdict, null in every Stage-4 manual path).
|
|
10
14
|
const VERDICT_SCHEMA_VERSION = 2;
|
|
11
15
|
|
|
12
|
-
/**
|
|
13
|
-
* Describe which requested seats actually reviewed, for the verdict's own face.
|
|
14
|
-
*
|
|
15
|
-
* ⚠️ ADDED v4.5.2 from a field report. The critic is a SOLO wave with one leg,
|
|
16
|
-
* so losing it loses 100% of the adversarial role — and unlike a dead bench wave
|
|
17
|
-
* (which trips the quorum gate and fails the run loudly) a dead critic is
|
|
18
|
-
* survivable, so the run continues to a full verdict, tally and chair synthesis
|
|
19
|
-
* that never saw the critic's findings. Run `dfb6a692` did exactly that and the
|
|
20
|
-
* only record was `deadWaves` in run.json, a file nobody opens when the verdict
|
|
21
|
-
* reads clean. A user who typed `--critic` asked for adversarial review; a
|
|
22
|
-
* verdict produced without it must say so where the verdict is read.
|
|
23
|
-
*
|
|
24
|
-
* Returns null when no critic was requested — there is nothing to report, and an
|
|
25
|
-
* always-present block would train readers to ignore it.
|
|
26
|
-
*
|
|
27
|
-
* @param {{runId: string, critic: ?string,
|
|
28
|
-
* deadWaves: Array<{waveId: string, models: string[], reason: string}>}} o
|
|
29
|
-
* @returns {?{criticRequested: string, criticSeated: boolean, reason: ?string,
|
|
30
|
-
* deadBenchSeats: string[]}}
|
|
31
|
-
*/
|
|
32
|
-
function summarizeSeatLoss({ runId, critic, deadWaves = [] } = {}) {
|
|
33
|
-
if (!critic) { return null; }
|
|
34
|
-
// Match on EITHER carrier. The `-c1` suffix is the convention run-stages.js
|
|
35
|
-
// uses, but a wave that names the critic model is the critic wave whatever it
|
|
36
|
-
// is called — and relying on the id alone would silently under-report if that
|
|
37
|
-
// convention ever changes.
|
|
38
|
-
const isCriticWave = w =>
|
|
39
|
-
w.waveId === `${runId}-c1` || (w.models || []).includes(critic);
|
|
40
|
-
const dead = deadWaves.find(isCriticWave) || null;
|
|
41
|
-
return {
|
|
42
|
-
criticRequested: critic,
|
|
43
|
-
criticSeated: !dead,
|
|
44
|
-
reason: dead ? dead.reason : null,
|
|
45
|
-
deadBenchSeats: deadWaves.filter(w => !isCriticWave(w))
|
|
46
|
-
.flatMap(w => w.models || []),
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* seatLoss, derived from the sink's records (v4.6 Plan 2, spec D3 — closes #84).
|
|
52
|
-
*
|
|
53
|
-
* WHY A DERIVATION: two fields reporting lost seats can disagree; deriving one
|
|
54
|
-
* from the other makes contradiction inexpressible. `summarizeSeatLoss` stays
|
|
55
|
-
* exactly as v4.5.2 shipped it (its tests pass unedited — that is the proof the
|
|
56
|
-
* shape survived); this function rebuilds its wave input from `dead-wave`
|
|
57
|
-
* records and then adds the losses waves can never show: `dead-leg` records —
|
|
58
|
-
* a solo critic wave that STARTED but whose one leg died is invisible to
|
|
59
|
-
* deadWaves, which is #84's second half.
|
|
60
|
-
*
|
|
61
|
-
* Reads ONLY record.data (Task 1's machine surface) — never the prose fields.
|
|
62
|
-
* @param {{runId: string, critic: ?string, degrades: Array<object>}} o
|
|
63
|
-
* @returns {?object} the summarizeSeatLoss shape, or null when no critic was requested
|
|
64
|
-
*/
|
|
65
|
-
function deriveSeatLoss({ runId, critic, degrades = [] } = {}) {
|
|
66
|
-
if (!critic) { return null; }
|
|
67
|
-
const real = degrades.filter(d => d.kind !== 'heal' && d.data);
|
|
68
|
-
const waves = real.filter(d => d.channel === 'dead-wave')
|
|
69
|
-
.map(d => ({ waveId: d.data.waveId, models: d.data.models || [], reason: d.data.reason }));
|
|
70
|
-
const base = summarizeSeatLoss({ runId, critic, deadWaves: waves });
|
|
71
|
-
const legs = real.filter(d => d.channel === 'dead-leg');
|
|
72
|
-
const criticLeg = legs.find(l => l.data.seat === critic) || null;
|
|
73
|
-
return {
|
|
74
|
-
...base,
|
|
75
|
-
criticSeated: base.criticSeated && !criticLeg,
|
|
76
|
-
// SL-2 handoff: a reconciliation note (run-retry-notes.js's
|
|
77
|
-
// missingLegStillDeadNote) carries data.status: null when the retry
|
|
78
|
-
// produced no leg for the seat at all — there is no status to name, so
|
|
79
|
-
// the old `ended '${status}'` template rendered the literal string
|
|
80
|
-
// "ended 'null'". A status-carrying record keeps the original text.
|
|
81
|
-
reason: base.reason || (criticLeg
|
|
82
|
-
? (criticLeg.data.reason || (criticLeg.data.status
|
|
83
|
-
? `the critic leg ended '${criticLeg.data.status}' with no usable output`
|
|
84
|
-
: 'the critic leg produced no usable output'))
|
|
85
|
-
: null),
|
|
86
|
-
deadBenchSeats: [...base.deadBenchSeats,
|
|
87
|
-
...legs.filter(l => l.data.seat !== critic).map(l => l.data.seat)],
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
|
|
91
16
|
/**
|
|
92
17
|
* Merge a tally record with Claude's Stage-4 decisions into the verdict record.
|
|
93
18
|
* @param {object} record tally() output
|
|
94
19
|
* @param {Array<{id,decision,applied,duplicateOf,tierOverride}>} decisions
|
|
95
|
-
* @param {{overallVerdict?: (string|null),
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
20
|
+
* @param {{overallVerdict?: (string|null), intent?: (string|null),
|
|
21
|
+
* seatLoss?: object, degrades?: Array<object>}} [opts]
|
|
22
|
+
* `overallVerdict` is the engine hook (Plan B): the parsed chair terminal line
|
|
23
|
+
* (`VERDICT:` on a review run, `ANSWER:` on a task run — v4.9 W5/W7);
|
|
24
|
+
* omitted/undefined → null. `intent` is the Stage-5 rebuild's carrier (v4.9 fix
|
|
25
|
+
* round 2 — see the emit-when-'task' line below). `seatLoss` (v4.5.2) and
|
|
26
|
+
* `degrades` (v4.6 Plan 2) are additive and OPTIONAL — each lands only when
|
|
99
27
|
* truthy/non-empty, absent otherwise (never fabricated).
|
|
100
28
|
*/
|
|
101
29
|
function buildVerdict(record, decisions = [], opts = {}) {
|
|
@@ -116,6 +44,22 @@ function buildVerdict(record, decisions = [], opts = {}) {
|
|
|
116
44
|
// matching the `seatLoss` sibling below; PR5 codes against that name.
|
|
117
45
|
...(record.meta.seats ? { seats: record.meta.seats } : {}),
|
|
118
46
|
claudeInCouncil: record.meta.claudeInCouncil,
|
|
47
|
+
// v4.9 W5.3: task-mode marker — emit-when-'task' (the W4/W5 plan's §7.5
|
|
48
|
+
// byte-identity ruling): a review record never materializes the key, so a
|
|
49
|
+
// review verdict.json is unchanged byte for byte, and an explicit
|
|
50
|
+
// meta.intent 'review' (hand-assembled input) is NOT forwarded either.
|
|
51
|
+
// ⚠️ v4.9 fix round 2 (council C1): `opts.intent` is a SECOND carrier, not a
|
|
52
|
+
// redundant one. MEASURED: the run dir's tally.json copies `meta` verbatim
|
|
53
|
+
// (tally.js), so the canonical Stage-5 rebuild already came through the first
|
|
54
|
+
// guard — but a hand-assembled or MCP-supplied record has none (mcp-tools.js
|
|
55
|
+
// :: amicus_verdict types `record` as `z.record(z.any())`), and on THAT leg
|
|
56
|
+
// the rebuild dropped the key, regressing the fold line and Workspace chip to
|
|
57
|
+
// review scale. Passed through `opts` rather than assigned after the call, so
|
|
58
|
+
// the key keeps its SLOT here and a rebuilt document's key order still
|
|
59
|
+
// matches the engine's (pinned, cli-council-verdict-chair-carry.test.js).
|
|
60
|
+
// PR #200 round-5 B3: parens on the whole disjunction — behaviour-identical.
|
|
61
|
+
...(((record.meta && record.meta.intent === 'task') || opts.intent === 'task')
|
|
62
|
+
? { intent: 'task' } : {}),
|
|
119
63
|
overallVerdict: opts.overallVerdict === undefined ? null : opts.overallVerdict,
|
|
120
64
|
findings: record.findings.map(f => {
|
|
121
65
|
const d = byId.get(f.id) || {};
|
|
@@ -148,9 +92,9 @@ function buildVerdict(record, decisions = [], opts = {}) {
|
|
|
148
92
|
// row's flat {model, seat} shape — model is the alias, seat is the seat
|
|
149
93
|
// id, so on a unique-alias bench they are byte-equal and nothing is
|
|
150
94
|
// emitted (same semantics as `seat.id !== seat.alias` one layer up,
|
|
151
|
-
// run-stats-entry.js
|
|
152
|
-
// above
|
|
153
|
-
// {id, alias} seat OBJECT at its own decision point (run.js:
|
|
95
|
+
// run-stats-entry.js :: buildRunStatsEntry). NOT a plain pass-through like `raiserSeat`
|
|
96
|
+
// in the findings literal above — that field's upstream producer already holds a real
|
|
97
|
+
// {id, alias} seat OBJECT at its own decision point (run.js:212:
|
|
154
98
|
// `r.seat && r.seat.id !== r.seat.alias`), so passing its verdict
|
|
155
99
|
// through here is safe. The street-cred producer never has such an object
|
|
156
100
|
// at this point, only a flat row, so a pass-through here would leak `seat`
|
|
@@ -200,26 +144,62 @@ function buildVerdict(record, decisions = [], opts = {}) {
|
|
|
200
144
|
* verdict belongs to the run, not to wherever the caller writes the result):
|
|
201
145
|
* 1. `<runDir>/verdict.json` `overallVerdict` — the value the engine already
|
|
202
146
|
* parsed. Guarded by `runId`: a stale or foreign verdict.json sitting in
|
|
203
|
-
* the folder must never inject another run's chair line.
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
-
*
|
|
147
|
+
* the folder must never inject another run's chair line. Guarded by SCALE
|
|
148
|
+
* too (v4.9 fix round 3, council A1/B1 — see below).
|
|
149
|
+
* 2. `<runDir>/chair-output.md`, re-parsed with the engine's OWN parser, so
|
|
150
|
+
* there is no second parser to drift. This also recovers runs whose
|
|
151
|
+
* verdict.json was already nulled by the defect.
|
|
152
|
+
*
|
|
153
|
+
* ⚠️ ONE scale, chosen by the run's INTENT (v4.9 fix round 2, council B1/C2 —
|
|
154
|
+
* CORRECTING W7's MEDIUM F2, recorded because it was wrong instructively). W7 was
|
|
155
|
+
* right that a VERDICT-only fallback recovered null for every task run; its fix
|
|
156
|
+
* tried BOTH parsers unconditionally, justified thus — the two scales are
|
|
157
|
+
* DISJOINT by pinned construction (tests/council/chair-scale-drift.test.js), so
|
|
158
|
+
* neither can read the other's phrase and the order of the two calls cannot
|
|
159
|
+
* change an outcome. The premise is true; the conclusion does not follow.
|
|
160
|
+
* Disjointness is a property of the PHRASE SETS, not of the DOCUMENT: chair prose
|
|
161
|
+
* that quotes, contrasts or merely mentions the other scale carries BOTH keyword
|
|
162
|
+
* lines, and then order decides everything — measured red in both directions, now
|
|
163
|
+
* pinned in both (cli-council-verdict-chair-carry.test.js, SCALEFREEFALLBACK).
|
|
164
|
+
*
|
|
165
|
+
* So `intent` dispatches through `parseChairTerminal` — one parser, never both.
|
|
166
|
+
* W7 rejected that call believing a Stage-5 rebuild has no intent to pass; it has
|
|
167
|
+
* THREE, and the caller reads all of them (cli-handlers-council.js :: runVerdict):
|
|
168
|
+
* the record's `meta.intent`, `run.json`'s `intent` from the run folder this
|
|
169
|
+
* function already anchors on, and — since fix round 3, council C3 — the prior
|
|
170
|
+
* verdict.json's own `intent` (readPriorVerdictIntent below), which is the last
|
|
171
|
+
* carrier standing when the other two are absent. Absent — and anything but 'task' — is review,
|
|
172
|
+
* restoring pre-W7 review behaviour exactly. Never invents: an absent, skipped or
|
|
173
|
+
* unstructured chair yields null.
|
|
207
174
|
*
|
|
208
|
-
*
|
|
175
|
+
* ⚠️ AND THE CARRY OBEYS THE SAME SCALE (v4.9 fix round 3, council A1+B1 — one
|
|
176
|
+
* mechanism, one fix). Round 2 dispatched branch 2 and left branch 1 with no
|
|
177
|
+
* scale check at all, so the whole guard was inert whenever a prior verdict.json
|
|
178
|
+
* existed: `overallVerdict: 'Ship it'` was carried onto a task rebuild verbatim
|
|
179
|
+
* and every downstream surface then labelled a CHAIR_VERDICTS phrase `ANSWER:`
|
|
180
|
+
* — the defect round 2 measured on the prose leg, one branch earlier. So the
|
|
181
|
+
* carried phrase must be a member of the scale `intent` selects; off-scale is
|
|
182
|
+
* treated as NO CARRY and falls through to branch 2, which is intent-dispatched
|
|
183
|
+
* and therefore already right. This also subsumes the old string/non-empty
|
|
184
|
+
* check: every member of both scales is a non-empty string, and `includes`
|
|
185
|
+
* refuses a number, null or object without a separate typeof.
|
|
209
186
|
* @param {string} runDir
|
|
210
187
|
* @param {string} [runId] record.meta.runId — the run being rebuilt
|
|
211
|
-
* @
|
|
188
|
+
* @param {?string} [intent] 'task' selects the ANSWER scale; anything else review
|
|
189
|
+
* @returns {string|null} a canonical chair verdict OR answer phrase, or null
|
|
212
190
|
*/
|
|
213
|
-
function readOverallVerdict(runDir, runId) {
|
|
191
|
+
function readOverallVerdict(runDir, runId, intent) {
|
|
192
|
+
const scale = intent === 'task' ? CHAIR_ANSWERS : CHAIR_VERDICTS;
|
|
214
193
|
try {
|
|
215
194
|
const prior = JSON.parse(fs.readFileSync(path.join(runDir, 'verdict.json'), 'utf-8'));
|
|
216
|
-
if (
|
|
195
|
+
if (scale.includes(prior.overallVerdict)
|
|
217
196
|
&& (!runId || prior.runId === runId)) {
|
|
218
197
|
return prior.overallVerdict;
|
|
219
198
|
}
|
|
220
199
|
} catch { /* no prior verdict.json, or unreadable — try the chair prose */ }
|
|
221
200
|
try {
|
|
222
|
-
|
|
201
|
+
const text = fs.readFileSync(path.join(runDir, 'chair-output.md'), 'utf-8');
|
|
202
|
+
return parseChairTerminal(text, intent);
|
|
223
203
|
} catch { /* no chair-output.md — the chair genuinely produced nothing */ }
|
|
224
204
|
return null;
|
|
225
205
|
}
|
|
@@ -247,6 +227,35 @@ function readPriorVerdictSurfaces(runDir, runId) {
|
|
|
247
227
|
return { seatLoss: null, degrades: null };
|
|
248
228
|
}
|
|
249
229
|
|
|
230
|
+
/**
|
|
231
|
+
* The run's intent as the prior verdict.json records it (v4.9 fix round 3,
|
|
232
|
+
* council C3) — the THIRD carrier for a Stage-5 rebuild, after the record's
|
|
233
|
+
* `meta.intent` and run.json's checkpoint.
|
|
234
|
+
*
|
|
235
|
+
* A SEPARATE function rather than a fourth key on readPriorVerdictSurfaces
|
|
236
|
+
* directly above: intent has to be resolved BEFORE readOverallVerdict runs
|
|
237
|
+
* (it selects that call's scale), while the loss surfaces are recovered after
|
|
238
|
+
* the parse; and #87's pins assert that function's return shape exactly
|
|
239
|
+
* (verdict-degrades.test.js), so widening it would edit a pin to fit a change
|
|
240
|
+
* rather than the other way round.
|
|
241
|
+
*
|
|
242
|
+
* Same contract as both siblings: the run folder's own verdict.json, the same
|
|
243
|
+
* `!runId || prior.runId === runId` guard — waived only when the RECORD names
|
|
244
|
+
* no run, never when the DOCUMENT does not — and absence yields null.
|
|
245
|
+
* Emit-when-'task' at the source (buildVerdict), so ONLY 'task' is reported;
|
|
246
|
+
* anything else, including a hand-written `intent: 'review'`, is no vote.
|
|
247
|
+
* @param {string} runDir
|
|
248
|
+
* @param {string} [runId]
|
|
249
|
+
* @returns {'task'|null}
|
|
250
|
+
*/
|
|
251
|
+
function readPriorVerdictIntent(runDir, runId) {
|
|
252
|
+
try {
|
|
253
|
+
const prior = JSON.parse(fs.readFileSync(path.join(runDir, 'verdict.json'), 'utf-8'));
|
|
254
|
+
if ((!runId || prior.runId === runId) && prior.intent === 'task') { return 'task'; }
|
|
255
|
+
} catch { /* no prior verdict.json, or unreadable — no vote */ }
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
|
|
250
259
|
/** Atomic write: tmp + rename (matches the repo's wave.json convention). */
|
|
251
260
|
function writeVerdictAtomic(filePath, verdict) {
|
|
252
261
|
const tmp = `${filePath}.tmp-${process.pid}`;
|
|
@@ -256,5 +265,5 @@ function writeVerdictAtomic(filePath, verdict) {
|
|
|
256
265
|
|
|
257
266
|
module.exports = {
|
|
258
267
|
buildVerdict, summarizeSeatLoss, deriveSeatLoss, readOverallVerdict, readPriorVerdictSurfaces,
|
|
259
|
-
writeVerdictAtomic, VERDICT_SCHEMA_VERSION,
|
|
268
|
+
readPriorVerdictIntent, writeVerdictAtomic, VERDICT_SCHEMA_VERSION,
|
|
260
269
|
};
|