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.
Files changed (105) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/CHANGELOG.md +249 -0
  3. package/README.md +8 -3
  4. package/docs/ROADMAP.md +101 -10
  5. package/docs/configuration.md +54 -4
  6. package/docs/council.md +102 -14
  7. package/docs/troubleshooting.md +9 -2
  8. package/docs/usage.md +128 -12
  9. package/electron/ipc-setup.js +36 -15
  10. package/electron/offer-session.js +51 -0
  11. package/electron/setup-ui.js +18 -8
  12. package/electron/workspace-ui/live-dead-seats.js +163 -91
  13. package/electron/workspace-ui/workspace-banners.js +30 -7
  14. package/electron/workspace-ui/workspace-matrix.js +23 -3
  15. package/electron/workspace-ui/workspace-seats.js +95 -79
  16. package/package.json +1 -1
  17. package/schemas/council-run.schema.json +2 -2
  18. package/schemas/council-tally.schema.json +17 -1
  19. package/schemas/council-verdict.schema.json +12 -4
  20. package/schemas/run.schema.json +6 -1
  21. package/skills/second-opinion/COUNCIL-DESIGN.md +1 -1
  22. package/skills/second-opinion/MANUAL-ORCHESTRATION.md +1 -1
  23. package/skills/second-opinion/MODEL-NOTES.md +88 -9
  24. package/skills/second-opinion/SEAT-BRIEFS.md +36 -4
  25. package/skills/second-opinion/SKILL.md +151 -36
  26. package/src/cli-council-run-bench.js +98 -6
  27. package/src/cli-handlers-council-run.js +18 -6
  28. package/src/cli-handlers-council.js +57 -7
  29. package/src/cli-handlers-doctor.js +1 -1
  30. package/src/cli.js +3 -1
  31. package/src/council/anonymize.js +2 -1
  32. package/src/council/briefings-chair-task.js +161 -0
  33. package/src/council/briefings-chair.js +33 -8
  34. package/src/council/briefings-debate.js +79 -13
  35. package/src/council/briefings-stage2-task.js +236 -0
  36. package/src/council/briefings-stage2.js +103 -26
  37. package/src/council/briefings-task.js +167 -0
  38. package/src/council/briefings.js +41 -4
  39. package/src/council/chair-fallback.js +95 -0
  40. package/src/council/debate.js +38 -21
  41. package/src/council/findings.js +3 -2
  42. package/src/council/ledger.js +2 -2
  43. package/src/council/parse-stage2.js +63 -15
  44. package/src/council/report-cost.js +61 -0
  45. package/src/council/report-html.js +26 -4
  46. package/src/council/report-md.js +30 -2
  47. package/src/council/report.js +40 -37
  48. package/src/council/run-assemble.js +21 -6
  49. package/src/council/run-chair.js +44 -95
  50. package/src/council/run-debate-revote.js +81 -49
  51. package/src/council/run-debate.js +51 -34
  52. package/src/council/run-finish.js +5 -3
  53. package/src/council/run-retry-keys.js +4 -4
  54. package/src/council/run-retry-launch.js +4 -4
  55. package/src/council/run-retry-notes.js +72 -15
  56. package/src/council/run-stage1-launch.js +4 -4
  57. package/src/council/run-stage1-rows.js +9 -6
  58. package/src/council/run-stage2.js +81 -47
  59. package/src/council/run-stages.js +9 -21
  60. package/src/council/run-stats-entry.js +46 -1
  61. package/src/council/run.js +28 -13
  62. package/src/council/seats.js +2 -2
  63. package/src/council/stage1-bind.js +3 -2
  64. package/src/council/verdict-seat-loss.js +124 -0
  65. package/src/council/verdict.js +108 -99
  66. package/src/headless.js +256 -49
  67. package/src/mcp-council-bench.js +64 -3
  68. package/src/mcp-council-run.js +10 -3
  69. package/src/mcp-server.js +52 -12
  70. package/src/mcp-tools.js +41 -5
  71. package/src/observe/council-legs.js +2 -2
  72. package/src/opencode-client.js +19 -1
  73. package/src/pack/pack-forward.js +15 -12
  74. package/src/pack/pack-resolve.js +1 -1
  75. package/src/prompt-builder.js +17 -1
  76. package/src/sidecar/fanout-leg.js +26 -0
  77. package/src/sidecar/fanout.js +1 -1
  78. package/src/sidecar/list-council.js +178 -0
  79. package/src/sidecar/list-limit.js +3 -1
  80. package/src/sidecar/list-search.js +2 -1
  81. package/src/sidecar/models.js +8 -1
  82. package/src/sidecar/read.js +34 -10
  83. package/src/template/render.js +16 -7
  84. package/src/utils/alias-shadow-writer.js +220 -0
  85. package/src/utils/alias-shadow.js +294 -0
  86. package/src/utils/config.js +1 -1
  87. package/src/utils/curated-models.js +11 -3
  88. package/src/utils/degrade.js +12 -5
  89. package/src/utils/doctor-alias-check.js +2 -5
  90. package/src/utils/engine-log-parse.js +289 -0
  91. package/src/utils/engine-log-tail.js +114 -0
  92. package/src/utils/engine-log.js +250 -0
  93. package/src/utils/engine-skew-records.js +146 -0
  94. package/src/utils/engine-skew.js +300 -0
  95. package/src/utils/gateway-router.js +10 -2
  96. package/src/utils/model-catalog.js +1 -1
  97. package/src/utils/result-schema.js +10 -0
  98. package/src/utils/text-sanitize.js +81 -0
  99. package/src/utils/ttft.js +57 -0
  100. package/src/utils/untrusted-fence.js +111 -1
  101. package/src/workspace/fold-format.js +28 -7
  102. package/src/workspace/live-normalize.js +2 -1
  103. package/src/workspace/matrix-model.js +6 -2
  104. package/src/workspace/run-detail.js +33 -7
  105. package/src/workspace/seat-space.js +10 -6
@@ -1,18 +1,15 @@
1
1
  /**
2
- * Council Workspace — seats panel painter (v4.4 §5). D8 extraction (Task 1,
3
- * v4.6.2 PR4): moved verbatim out of workspace-panels.js, which was pressed
4
- * up against the 300-line size gate — this file is where Task 2 adds
5
- * dead-seat rows. Loads immediately before workspace-panels.js (index.html),
6
- * which keeps a thin delegate; reads `window.AmicusApp` at CALL time, same
7
- * discipline as every sibling renderer file (workspace-app.js boots last and
8
- * owns `state`).
2
+ * Council Workspace — seats panel painter (v4.4 §5). D8 extraction (Task 1, v4.6.2 PR4): moved
3
+ * verbatim out of workspace-panels.js, which was pressed up against the 300-line size gate —
4
+ * this file is where Task 2 adds dead-seat rows. Loads immediately before workspace-panels.js
5
+ * (index.html), which keeps a thin delegate; reads `window.AmicusApp` at CALL time, same
6
+ * discipline as every sibling renderer file (workspace-app.js boots last and owns `state`).
9
7
  *
10
- * Task 2 ("dead-seat rows"): `state.detail.run` and `state.detail.verdict`
11
- * are the raw run.json/verdict.json docs (src/workspace/run-detail.js —
12
- * `getRunDetail` returns them wholesale, unfiltered), so `run.degrades` and
13
- * `verdict.seatLoss` are already on `state.detail` today; no data-layer
14
- * threading was needed. Only the derivation (window.AmicusLive.deadSeats,
15
- * live-seats.js) and this file's painting are new.
8
+ * Task 2 ("dead-seat rows"): `state.detail.run` and `state.detail.verdict` are the raw
9
+ * run.json/verdict.json docs (src/workspace/run-detail.js — `getRunDetail` returns them
10
+ * wholesale, unfiltered), so `run.degrades` and `verdict.seatLoss` are already on
11
+ * `state.detail` today; no data-layer threading was needed. Only the derivation
12
+ * (window.AmicusLive.deadSeats, live-seats.js) and this file's painting are new.
16
13
  *
17
14
  * NOTE (scope, matches the plan's file list): renderSeatsPanel() (below) is reached from
18
15
  * renderDetail() — called both from openRun() (a fresh run open) and from the blind toggle
@@ -50,41 +47,57 @@
50
47
  * seat-keyed and deadSeats still dedup'd on the alias; PR5c seat-keyed the other side too,
51
48
  * so both now key on "seat id where the record supplies one, alias otherwise". The
52
49
  * kind/channel FILTER still mirrors window.AmicusLive.deadSeats — now at
53
- * live-dead-seats.js, which PR5c split out of live-seats.js for the 300-line gate and
54
- * must keep doing so. Two spellings of one rule is PR5a council finding B1.
50
+ * live-dead-seats.js, where PR5c's size-gate split put it and v4.9 W9 named it `isSeatLoss`
51
+ * and must keep doing so; the two moved together in W9's one commit. Two spellings of one rule
52
+ * is PR5a council finding B1; only the behavioural drift pin in workspace-seats.test.js holds it.
55
53
  *
56
54
  * ⚠️ The two sides are NOT identical, and the difference is deliberate: deadSeats decides
57
55
  * whether a seat is RENDERED, so it fails toward showing a row; this decides whether a
58
56
  * badge is PAINTED, so an unidentified record falls back to the alias and marks every seat
59
57
  * sharing it. Over-badging is visible and self-correcting; a missing badge is silent.
60
58
  *
61
- * The kind/channel filter is load-bearing: run.degrades[] also carries
62
- * kind:'heal' / channel:'stage1-retry' records with the SAME retryWaveId/firstFailure fields
63
- * for seats that RECOVERED, and a field-only scan would tag a recovered seat "retried once".
59
+ * The kind/channel filter is load-bearing: run.degrades[] also carries kind:'heal' /
60
+ * channel:'stage1-retry' records with the SAME retryWaveId/firstFailure fields for seats that
61
+ * RECOVERED, and a field-only scan would tag a recovered seat "retried once".
64
62
  *
65
- * ⚠️ firstFailure is TRUTHINESS ONLY. Two shapes, both built in
66
- * run-retry-group.js :: groupStage1Lossesits deadLegs loop emits {seat, class:'leg',
67
- * status, reason}; its deadWaves loop emits {seat, class: lossClass(w), waveId, reason}
68
- * at THREE sites (lens/critic/bench), none carrying a status key, where
69
- * `const lossClass = w => (w.partial ? 'missing' : 'wave')` so firstFailure.status is
70
- * undefined on every wave-origin seat. ⚠️ T-A8 DROPPED five line numbers here: re-opened
71
- * 2026-08-17 they had all rotted a uniform +31 (T-A3 +15, T-A6 +16), and "now 235" is 266.
63
+ * ⚠️ `firstFailure` is no longer a retried MARKER here — the W9 fix round narrowed the gate to
64
+ * `retryWaveId` (see below)and the only thing left read off it is `.seatId`. THREE shapes
65
+ * carry that key today: `run-retry-group.js :: groupStage1Losses` builds two, its deadLegs
66
+ * loop's {seat, class:'leg', status, reason} and its deadWaves loop's {seat, class:
67
+ * lossClass(w), waveId, reason} at three sites (lens/critic/bench), both stamped with `seatId`
68
+ * by `recordFailure`; `run-retry-notes.js :: skippedWaveNote` builds a third, {seat, class:
69
+ * 'missing', waveId, reason}, which never passes through recordFailure and so carries NO
70
+ * seatId — it puts the seat id on `data.seatId` instead, which the key read below falls
71
+ * through to. ⚠️ T-A8 DROPPED five line numbers here: re-opened 2026-08-17 they had all rotted
72
+ * a uniform +31 (T-A3 +15, T-A6 +16), and "now 235" is 266.
72
73
  */
73
74
  function retriedSeats(degrades) {
74
75
  var out = Object.create(null);
75
76
  (degrades || []).forEach(function (d) {
76
- if (!d || d.kind !== 'degrade') { return; }
77
- if (d.channel !== 'dead-leg' && d.channel !== 'dead-wave') { return; }
77
+ // A kind-LESS record IS a degrade (W9 fix round, C4) this panel falls back to
78
+ // `verdict.degrades` below, documents that can predate kinds. Mirrors isSeatLoss.
79
+ if (!d || (d.kind !== undefined && d.kind !== 'degrade')) { return; }
78
80
  var data = d.data || {};
79
- if (!(data.retryWaveId || data.firstFailure)) { return; }
80
- if (d.channel === 'dead-leg') {
81
- // ⚠️ Prefer the SEAT ID when the record names one. TWO mechanisms now supply it, not
82
- // one: retryLegStillDeadNote and missingLegStillDeadNote via `firstFailure.seatId`
83
- // (pinned by tests/council/run-retry.test.js:628 on a twin bench), and since
84
- // v4.8 PR5c srcLegStillDeadNote via its own `data.seatId`. Reading only the first
85
- // meant a srcLeg record keyed by ALIAS and badged the live twin "retried once" while
86
- // the seat that was actually retried showed nothing.
87
- // `data.seat` remains the last fallback for pre-PR5c records (residual R6).
81
+ // Hoisted ABOVE the channel test (behaviour-preserving the two loss channels already
82
+ // required it) because it is ALSO the `seat-unbound` gate v4.9 W9 admits that shared channel
83
+ // through: orphan-leg, re-vote and Stage-2 judge notes ride it, are not retried seats, and
84
+ // none carries a retry-family field. ⚠️ NARROWED to `retryWaveId` alone in the W9 fix round
85
+ // (mutant SKIPRETRIED): this is a RETRIED set, and `run-retry-notes.js :: skippedWaveNote`
86
+ // carries a `firstFailure` for a seat the retry never ATTEMPTED, so badging it would be
87
+ // false. `isSeatLoss` still admits that record as a LOSS its gate keeps both fields; only
88
+ // the retried READ differs (the drift pin compares exactly that read). Every RETRIED seat's
89
+ // builder emits `retryWaveId` never-attempted skippedWaveNote alone lacks it so no badge is lost.
90
+ if (!data.retryWaveId) { return; }
91
+ if (d.channel !== 'dead-leg' && d.channel !== 'dead-wave'
92
+ && !(d.channel === 'seat-unbound' && (data.seatId || data.seat))) { return; }
93
+ if (d.channel !== 'dead-wave') {
94
+ // ⚠️ Prefer the SEAT ID when the record names one. TWO mechanisms supply it, not one:
95
+ // retryLegStillDeadNote and missingLegStillDeadNote via `firstFailure.seatId` (pinned by
96
+ // tests/council/run-retry.test.js:628 on a twin bench), and — since v4.8 PR5c —
97
+ // srcLegStillDeadNote via its own `data.seatId`, joined on that key by waveStillDeadNote's
98
+ // partial `seat-unbound` arm in v4.9 W9. Reading only the first meant a srcLeg record
99
+ // keyed by ALIAS and badged the live twin "retried once" while the seat that was actually
100
+ // retried showed nothing. `data.seat` is the last fallback, for pre-PR5c records (R6).
88
101
  var key = (data.firstFailure && data.firstFailure.seatId) || data.seatId || data.seat;
89
102
  if (key) { out[key] = true; }
90
103
  } else {
@@ -102,9 +115,8 @@
102
115
  return out;
103
116
  }
104
117
 
105
- // Mirrors isReviewing at live-dead-seats.js (moved there by PR5c's size-gate split) —
106
- // a chair/judge/rebuttal/revote row must not
107
- // carry a reviewer's retry marker.
118
+ // Mirrors isReviewing at live-dead-seats.js (moved there by PR5c's size-gate split) — a
119
+ // chair/judge/rebuttal/revote row must not carry a reviewer's retry marker.
108
120
  function isReviewingRole(role) {
109
121
  return role === 'seat' || role === 'critic' ||
110
122
  (typeof role === 'string' && role.indexOf('lens:') === 0);
@@ -117,11 +129,16 @@
117
129
  var tbody = A.$('seats-body');
118
130
  window.AmicusRender.renderSeats(tbody, seats, A.state.blind, A.labelOf);
119
131
  var seatLoss = d.verdict && d.verdict.seatLoss;
120
- var runMeta = { critic: (d.run && d.run.critic) || null };
132
+ // `criticSeat` (v4.9 W9 / R4): run.json's resolved critic SEAT id (run-state.js ::
133
+ // initCouncilRun seeds it, seats.js :: preflightSeats supplies it) — what lets deadSeats
134
+ // tag the critic by seat identity. Null with no critic, and on pre-field run.json.
135
+ var runMeta = { critic: (d.run && d.run.critic) || null,
136
+ criticSeat: (d.run && d.run.criticSeat) || null };
121
137
  // Source-selection (v4.6.3 PR2, spec D4): run-degrade.js swallows checkpoint failures, so
122
- // verdict.json can carry degrade records run.json's own checkpoint lost — fall back to it
123
- // ONLY when run.degrades is empty/absent. A fallback, never a union: both docs can carry
124
- // records for the SAME run, and the persisted run.json copy is authoritative when present.
138
+ // verdict.json can carry degrade records run.json's own checkpoint lost — fall back to it ONLY
139
+ // when run.degrades is empty/absent. A fallback, never a union: both docs can carry records for
140
+ // the SAME run, and the persisted run.json copy is authoritative when present. (This fallback
141
+ // is why the kind test above must admit a kind-LESS record: verdict.json can predate kinds.)
125
142
  var deg = (d.run && d.run.degrades && d.run.degrades.length) ? d.run.degrades
126
143
  : ((d.verdict && d.verdict.degrades) || []);
127
144
  var retried = retriedSeats(deg);
@@ -130,16 +147,14 @@
130
147
  // composed doc's leg order — so tbody.children[i] is not seats[i]. Build the key exactly the
131
148
  // way renderSeats does or the lookup silently misses.
132
149
  var rowsByKey = Object.create(null);
133
- Array.prototype.slice.call(tbody.children).forEach(function (row) {
134
- rowsByKey[row.dataset.key] = row;
135
- });
150
+ Array.prototype.slice.call(tbody.children).forEach(function (row) { rowsByKey[row.dataset.key] = row; });
136
151
  seats.forEach(function (s) {
137
152
  var row = rowsByKey[String(s.id || s.model)];
138
153
  if (!row || !row.children[8]) { return; }
139
- // Column 8 is the table's unlabeled trailing flag cell (index.html:51's final <th></th>).
140
- // It carries '⏳ stalled' on the LIVE path; on this terminal path seatsFromRunStats
141
- // hardcodes stalled:false (live-seats.js, seatsFromRunStats), so it is always empty here and free to use.
142
- // If that ever changes, this is the collision site.
154
+ // Column 8 is the table's unlabeled trailing flag cell (index.html:51's final <th></th>). It
155
+ // carries '⏳ stalled' on the LIVE path; on this terminal path seatsFromRunStats hardcodes
156
+ // stalled:false, so it is always empty here and free to use — the collision site if that
157
+ // ever changes.
143
158
  // Fix wave (whole-branch review, finding 2): this pass must be SYMMETRIC. renderSeats
144
159
  // reuses rows keyed on `model:role` across calls — including across two different
145
160
  // terminal runs opened in sequence that happen to share an alias+role — and never resets
@@ -158,34 +173,35 @@
158
173
  //
159
174
  // ⚠️ NAMING HAZARD (council A1): `seat` means TWO different things three lines apart.
160
175
  // `s.seat` is a SEAT ID (`alias#N`, from src/council/run-stats-entry.js :: buildRunStatsEntry
161
- // via the cost row). The degrade records keyed into `retried` above use `data.seat`,
162
- // which is an ALIAS and stays one
163
- // deliberately (run-retry-notes.js:39-45 verdict.js:72 compares it against `o.critic`).
164
- // Reading one as the other is precisely how an earlier revision of this fix paired an
165
- // alias-keyed map with a seat-id lookup and dropped every badge. When touching either
166
- // side, say which space you are in.
176
+ // via the cost row). The degrade records keyed into `retried` above use `data.seat`, which is
177
+ // an ALIAS and stays one deliberately (run-retry-notes.js :: waveStillDeadNote's `data`
178
+ // comment; `verdict-seat-loss.js :: deriveSeatLoss`'s `criticLeg` compares it to `o.critic`
179
+ // both by SYMBOL since W9). Reading one as the other is precisely how an earlier revision of
180
+ // this fix paired an alias-keyed map with a seat-id lookup and dropped every badge. When
181
+ // touching either side, say which space you are in.
167
182
  //
168
183
  // ⚠️ The pre-PR expression was `retried[s.modelInput || s.model]` (council B1). The
169
184
  // `modelInput` arm is dropped on purpose: this loop only ever iterates
170
185
  // `seatsFromRunStats(...)` output (assigned in workspace-seats.js :: renderSeatsPanel), and
171
186
  // that projection emits no `modelInput` at all — live payload seats, which DO carry it,
172
187
  // reach `deadSeats` in workspace-seats.js :: appendDeadRows and never reach here. The
173
- // invariant is pinned by test (12) in workspace-seats.test.js;
174
- // if that test ever fails, restore the `s.modelInput` arm rather than deleting the test.
175
- // ⚠️ `s.seat &&` is LOAD-BEARING (council A1). `s.seat` is null on a unique bench, and a
176
- // bare `retried[s.seat]` coerces null to the STRING key 'null' — so a seat with no seat id
177
- // would match a degrade record whose alias is literally `null`. Measured: with
178
- // `m = Object.create(null); m['null'] = true`, `m[null]` is `true`. Contrived, but this
179
- // module already guards the same class elsewhere — live-dead-seats.js, deadSeats' Object.create(null) note, records a model
180
- // named `toString` crashing the seats repaint, which is why Object.create(null) is used
181
- // throughout. The guard costs nothing and closes it.
188
+ // invariant is pinned by test (12) in workspace-seats.test.js; if that test ever fails,
189
+ // restore the `s.modelInput` arm rather than deleting the test.
190
+ // ⚠️ `s.seat &&` is LOAD-BEARING (council A1). `s.seat` is null on a unique bench, and a bare
191
+ // `retried[s.seat]` coerces null to the STRING key 'null' — so a seat with no seat id would
192
+ // match a degrade record whose alias is literally `null`. Measured: with
193
+ // `m = Object.create(null); m['null'] = true`, `m[null]` is `true`. Contrived, but this module
194
+ // already guards the same class elsewhere — live-dead-seats.js's Object.create(null) note in
195
+ // deadSeats records a model named `toString` crashing the seats repaint, which is why
196
+ // Object.create(null) is used throughout. The guard costs nothing and closes it.
182
197
  //
183
- // ⚠️ KEYSPACE (council B1): `retried` deliberately mixes seat ids (`alias#N`) and bare
184
- // aliases as keys, because only one of five emitter arms supplies a seat id. That is a
185
- // real collision surface and a KNOWN one src/council/seats.js:236 already records that
186
- // a literal alias containing '#' collides with a minted #N id, and preflightSeats refuses
187
- // exactly that shape. This map inherits that guarantee rather than re-deriving it; if
188
- // preflightSeats ever stops refusing it, this lookup becomes ambiguous.
198
+ // ⚠️ KEYSPACE (council B1): `retried` deliberately mixes seat ids (`alias#N`) and bare aliases
199
+ // as keys every emitter arm supplies a seat id since v4.9 W9 P1, but a slot the producer
200
+ // could NOT identify still emits null and falls back to the alias. That is a real collision
201
+ // surface and a KNOWN one src/council/seats.js:236 already records that a literal alias
202
+ // containing '#' collides with a minted #N id, and preflightSeats refuses exactly that shape.
203
+ // This map inherits that guarantee rather than re-deriving it; if preflightSeats ever stops
204
+ // refusing it, this lookup becomes ambiguous.
189
205
  var isRetried = isReviewingRole(s.role)
190
206
  && !!((s.seat && retried[s.seat]) || retried[s.model]);
191
207
  if (isRetried) {
@@ -201,13 +217,12 @@
201
217
  }
202
218
 
203
219
  /**
204
- * Paints the dead-seat rows appended after live rows. Deliberately NOT
205
- * folded into workspace-render.js's renderSeats (293/300 — must not grow)
206
- * and NOT run through its keyed diff: dead rows carry no per-tick-changing
207
- * field, so a full rebuild every call is correct and cheap, and renderSeats
208
- * just above already self-cleans any PRIOR dead row as an unrecognized
209
- * `data-key` (its own seen-set only knows about the live `seats` it was
210
- * just given), so nothing here needs to track dead rows across calls.
220
+ * Paints the dead-seat rows appended after live rows. Deliberately NOT folded into
221
+ * workspace-render.js's renderSeats (293/300 — must not grow) and NOT run through its keyed
222
+ * diff: dead rows carry no per-tick-changing field, so a full rebuild every call is correct
223
+ * and cheap, and renderSeats just above already self-cleans any PRIOR dead row as an
224
+ * unrecognized `data-key` (its own seen-set only knows about the live `seats` it was just
225
+ * given), so nothing here needs to track dead rows across calls.
211
226
  *
212
227
  * Cells route through window.AmicusLive.seatCells(...) — the SAME function
213
228
  * live rows use — so name masking (and every other column's blank/em-dash
@@ -264,7 +279,8 @@
264
279
  var A = window.AmicusApp;
265
280
  var d = A.state.detail;
266
281
  var seatLoss = d && d.verdict ? d.verdict.seatLoss : null;
267
- var runMeta = { critic: (d && d.run && d.run.critic) || null };
282
+ var runMeta = { critic: (d && d.run && d.run.critic) || null,
283
+ criticSeat: (d && d.run && d.run.criticSeat) || null };
268
284
  // Source-selection (v4.6.3 PR2, spec D4), live-path twin of renderSeatsPanel's fallback
269
285
  // above: the tick's own live.degrades wins when non-empty; state.detail.verdict.degrades is
270
286
  // usually absent mid-run (verdict.json doesn't exist until the run finishes) — fine, this
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "amicus",
3
- "version": "4.8.1",
3
+ "version": "4.9.0",
4
4
  "mcpName": "io.github.BourbonDog/amicus",
5
5
  "description": "Multi-model LLM Council + parallel AI window for Claude Code. Run structured council reviews across Gemini, GPT, DeepSeek and more — or fork a conversation to any model and fold the results back.",
6
6
  "keywords": [
@@ -103,13 +103,13 @@
103
103
  }
104
104
  },
105
105
  "degrades": {
106
- "description": "v4.6 Plan 1: every degrade/heal the sink announced — additive, written by run-degrade.js.",
106
+ "description": "v4.6 Plan 1: every degrade/heal the sink announced — additive, written by run-degrade.js. v4.9 widened kind with 'info': an announcement that is neither a loss nor a recovery (e.g. a task run's ledger-skipped note) — recorded here but never flipping the run degraded.",
107
107
  "type": "array",
108
108
  "items": {
109
109
  "type": "object",
110
110
  "required": ["kind", "channel", "what", "why", "effect"],
111
111
  "properties": {
112
- "kind": { "enum": ["degrade", "heal"] },
112
+ "kind": { "enum": ["degrade", "heal", "info"] },
113
113
  "channel": { "type": "string" },
114
114
  "what": { "type": "string" },
115
115
  "why": { "type": "string" },
@@ -21,6 +21,10 @@
21
21
  "seats": {
22
22
  "type": "array",
23
23
  "description": "v4.8, optional. The run's seat table, one {id, alias, role, lens, position} entry per BENCH seat in bench order, echoed verbatim from the tally input. The engine emits it only when the bench repeats an alias — the one case where the `alias#N` ids on findings[].raiserSeat, adjudications[].seat and runStats[].seat resolve to nothing else in the document, since meta.models is the ALIAS list. Bench-only: 'claude' is never a seat, so meta.models.length need not equal seats.length and the two must never be joined positionally. ABSENCE DOES NOT MEAN the bench had no repeated alias — hand-assembled and MCP-assembled input reaches tally() with no seat machinery behind it; it means only that no seat table is available. `items` is deliberately unconstrained: consumers must tolerate a table that is not an array of seat objects (the report and Workspace renderers fall back to alias space whole rather than failing), and the MCP input schema accepts the same latitude."
24
+ },
25
+ "intent": {
26
+ "enum": ["review", "task"],
27
+ "description": "v4.9, optional. The run's intent. The engine emits it only when 'task' — a review run's meta never materializes the key, keeping every review artifact byte-identical — but an explicit 'review' from hand-assembled input is accepted."
24
28
  }
25
29
  }
26
30
  },
@@ -96,7 +100,19 @@
96
100
  }
97
101
  }
98
102
  },
99
- "runStats": { "type": "array", "items": { "type": "object" } },
103
+ "runStats": {
104
+ "type": "array",
105
+ "items": {
106
+ "type": "object",
107
+ "properties": {
108
+ "ttftMs": {
109
+ "type": "integer",
110
+ "minimum": 0,
111
+ "description": "v4.9 W13, optional. Time-to-first-token for this row's leg: milliseconds from the leg asking OpenCode for output to the first poll carrying substantive activity (output, reasoning, a tool call, a tool result, or a tool call reaching a terminal status) — the very same predicate the no-output backstop disarms on, read at the one site in src/headless.js's poll loop, so the two can never drift. EMIT-WHEN-SET: absent means no substantive tick was ever OBSERVED, which is NOT the same as `0` (a first token inside the first poll, a real value that must survive) and NOT the same as `null`. KNOWN BIAS, stated so the C2 derivation reads the sample correctly: (a) the stamp is taken at poll time, not token time, so every value is an UPPER BOUND carrying up to one poll interval of slack; (b) absence is still not proof the model produced nothing — the activity block now runs ABOVE all four completion gates (so a leg that first produced output and finished inside one poll IS measured), but a leg ended by an external abort, by a consecutive-poll-failure bail, by --timeout expiry during the final sleep, or by the pre-send backstop ends without any poll observing what it may already have served; (c) PR #207 round 3 adds one more cause of absence — a reading taken across a BACKWARD wall-clock jump is negative (the probe is a Date.now() delta), and every producer DROPS such a reading rather than clamping it to 0, because a clamp would publish 'first token inside the first poll' — the single most consequential value in this distribution — for a leg that measured nothing of the kind. PROBE ONLY at v4.9 W13 (ruling R12, probe first / derive later): nothing in the engine derives a backstop, threshold, or routing decision from it. No `additionalProperties: false` on this row means additive fields were always accepted here — this documents the shape rather than changing what is accepted, exactly as streetCred[].seat above does."
112
+ }
113
+ }
114
+ }
115
+ },
100
116
  "tierCounts": {
101
117
  "type": "object",
102
118
  "required": ["Confirmed", "Contested", "Singleton", "Disputed"],
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "$id": "https://raw.githubusercontent.com/BourbonDog/amicus/main/schemas/council-verdict.schema.json",
4
4
  "title": "amicus council-verdict document",
5
- "description": "Verdict record (`council verdict --json`, amicus_verdict, verdict.json). overallVerdict is the chair's parsed VERDICT line — null in every Stage-4 manual path, populated by the headless engine.",
5
+ "description": "Verdict record (`council verdict --json`, amicus_verdict, verdict.json). overallVerdict is the chair's parsed terminal line — the VERDICT line on a review run, the ANSWER line on a task run (v4.9 W7) — null in every Stage-4 manual path, populated by the headless engine.",
6
6
  "type": "object",
7
7
  "required": [
8
8
  "schemaVersion",
@@ -56,13 +56,21 @@
56
56
  "claudeInCouncil": {
57
57
  "type": "boolean"
58
58
  },
59
+ "intent": {
60
+ "enum": ["review", "task"],
61
+ "description": "v4.9, optional. The run's intent, forwarded from the tally record's meta.intent. The engine emits it only when 'task' — a review verdict never materializes the key, keeping review-run verdict.json byte-identical — but an explicit 'review' from hand-assembled input is accepted."
62
+ },
59
63
  "overallVerdict": {
64
+ "description": "v4.9 W7: the chair's parsed terminal line on EITHER scale — a review run's VERDICT phrase ('Ship it' | 'Fix these first' | 'Fundamental rethink') or a task run's ANSWER phrase ('Converged' | 'Split' | 'Insufficient'). The two scales are disjoint by construction, so the value alone identifies the intent; `intent` above states it explicitly on task runs.",
60
65
  "oneOf": [
61
66
  {
62
67
  "enum": [
63
68
  "Ship it",
64
69
  "Fix these first",
65
- "Fundamental rethink"
70
+ "Fundamental rethink",
71
+ "Converged",
72
+ "Split",
73
+ "Insufficient"
66
74
  ]
67
75
  },
68
76
  {
@@ -222,13 +230,13 @@
222
230
  ]
223
231
  },
224
232
  "degrades": {
225
- "description": "v4.6 Plan 2: what this run lost — copied verbatim from the sink at verdict assembly. Additive; absent on a clean run.",
233
+ "description": "v4.6 Plan 2: what this run lost — copied verbatim from the sink at verdict assembly. Additive; absent when the sink recorded nothing. v4.9 widened kind with 'info': an announcement that is neither a loss nor a recovery (e.g. a task run's ledger-skipped note) — carried here without degrading the run.",
226
234
  "type": "array",
227
235
  "items": {
228
236
  "type": "object",
229
237
  "required": ["kind", "channel", "what", "why", "effect"],
230
238
  "properties": {
231
- "kind": { "enum": ["degrade", "heal"] },
239
+ "kind": { "enum": ["degrade", "heal", "info"] },
232
240
  "channel": { "type": "string" },
233
241
  "what": { "type": "string" },
234
242
  "why": { "type": "string" },
@@ -23,6 +23,11 @@
23
23
  "opencodeSessionId": { "type": ["string", "null"] },
24
24
  "usage": { "type": ["object", "null"] },
25
25
  "pack": { "type": "object" },
26
- "tag": { "type": "string" }
26
+ "tag": { "type": "string" },
27
+ "ttftMs": {
28
+ "type": "integer",
29
+ "minimum": 0,
30
+ "description": "v4.9 W13, optional. Time-to-first-token for this leg: milliseconds from the leg asking OpenCode for output to the first poll carrying substantive activity — the same predicate the no-output backstop disarms on. EMIT-WHEN-SET, like `pack` and `tag` above and unlike `opencodeSessionId`/`durationMs`: absent means no substantive tick was OBSERVED, which is neither `0` (a real first-token-inside-the-first-poll measurement) nor `null`. KNOWN BIAS, for whoever derives from this later: the value is stamped at poll time rather than token time, so it is an UPPER BOUND carrying up to one poll interval of slack; and absence is not proof the model served nothing — a leg ended by an external abort, by a consecutive-poll-failure bail, by --timeout expiry during the final sleep, or by the pre-send backstop can have served output no poll ever observed, and (PR #207 round 3) so can a leg whose only reading was taken across a backward wall-clock jump: the probe is a Date.now() delta, so a skewed reading is negative, and every producer DROPS such a reading rather than clamping it to 0 — a clamp would publish 'first token inside the first poll' for a leg that measured nothing of the kind. PROBE ONLY at v4.9 W13 — nothing derives from it yet."
31
+ }
27
32
  }
28
33
  }
@@ -110,7 +110,7 @@ Run as ordered phases; track as todos. **Three sequential waves of model calls**
110
110
  - Chair selection & fallback: §5.3.
111
111
 
112
112
  ### Stage 4 — Tiered decisions (peer-validated)
113
- - **Consensus tier** = **Confirmed** findings (≥ 2 peer agreements, agrees dominate) → offer one **bulk accept/deny** (user may name exceptions).
113
+ - **Consensus tier** = **Confirmed** findings (≥ 2 peer agreements with agrees dominating — or a lone corroborating peer with zero disputes) → offer one **bulk accept/deny** (user may name exceptions).
114
114
  - **Judgment tier** = **Disputed** (strong peer pushback), **Contested** (live dispute), or **Singleton** (only the raiser) → present **each individually**, showing the adjudication data and which model raised/disputed it.
115
115
  - Record every decision (accepted / denied / modified / deferred).
116
116
 
@@ -228,7 +228,7 @@ Instruct the chair to write a **synthesized verdict** that:
228
228
  - Distinguishes findings the bench broadly endorsed from contested or singleton claims
229
229
  - Arrives at an overall assessment of the artifact
230
230
 
231
- **Chair verdict scale (optional element, when ON):** append the addendum from `SEAT-BRIEFS.md § Chair verdict-scale addendum` to the chair packet — the chair must close with 3–5 **hard questions** the artifact's author probably hasn't asked themselves, then a final parseable line: `VERDICT: Ship it | Fix these first | Fundamental rethink`. Surface that line verbatim at the top of `report.md` and in the inline chat presentation. When debate mode is also ON, the chair packet is built from the *final* (post-rebuttal) tally and includes the defense/re-vote outcomes.
231
+ **Chair closing scale (standard since v4.9 — not an opt-in element):** append the addendum from `SEAT-BRIEFS.md § Chair closing-scale addendum` to the chair packet — the chair must close with 3–5 **hard questions** the artifact's author probably hasn't asked themselves, then a final parseable line: `VERDICT: Ship it | Fix these first | Fundamental rethink`. (On a task-intent run, use that section's ANSWER twin instead — `ANSWER: Converged | Split | Insufficient` — and never translate one scale into the other.) Surface that line verbatim at the top of `report.md` and in the inline chat presentation. When debate mode is also ON, the chair packet is built from the *final* (post-rebuttal) tally and includes the defense/re-vote outcomes.
232
232
 
233
233
  Save the chair's output to the run folder as `verdict.md`.
234
234
 
@@ -4,10 +4,13 @@ This file is the `second-opinion` skill's evolving memory of **how to actually d
4
4
  well**. Read it before Stage 0 (council selection and launch); update it, with the user's
5
5
  approval, at the end of each run (Stage 6). Keep it tight — merge and prune rather than append.
6
6
 
7
- _Last updated: 2026-08-03 (per-section fold-back, both directions — the haiku "hard-404"
8
- re-diagnosed as a `/v1`-less `ANTHROPIC_BASE_URL`, pre-degrade-era claims re-grounded in the
9
- announcement contract (`degrades[]`/`seatLoss`/exit 2/one Stage-1 retry), three model sections and
10
- the peer-consensus≠evidence rule upstreamed from the field ledger; see changelog)._
7
+ _Last updated: 2026-08-25 (per-section fold-back, both directions — the `NO_OUTPUT_BACKSTOP`
8
+ mechanism, conformance-vs-content plus the credit-exhaustion attribution correction, the
9
+ inlined-source-pack technique, and per-model calibration nuances upstreamed from the field ledger;
10
+ the `runStats` allowlist correction adopted back into the local ledger; see changelog). Prior:
11
+ 2026-08-03 (haiku "hard-404" re-diagnosed as a `/v1`-less `ANTHROPIC_BASE_URL`, pre-degrade-era
12
+ claims re-grounded in the announcement contract, three model sections and the peer-consensus≠
13
+ evidence rule upstreamed)._
11
14
 
12
15
  ## Global operating rules (all models)
13
16
  - **Fast path:** `council run` applies `--agent Plan` / `--no-context` / `--summary-length
@@ -34,6 +37,28 @@ the peer-consensus≠evidence rule upstreamed from the field ledger; see changel
34
37
  - **Read results from the JSON documents** (`--json`): a wave's `legs[].summary` / a run's
35
38
  `summary` is the model's output; `status`/`error`/`counts` are ground truth for failures. Never
36
39
  scrape stderr logs to judge success.
40
+ - **`conformance` measures FORMAT, not content — read an `unstructured` leg before writing it
41
+ off.** A leg that fails to emit a parseable findings block scores zero findings in the tally even
42
+ when its prose review is the bench's strongest — hand-extracted, one such "zero-finding" leg has
43
+ tied for the best peers-only street-cred on its bench and carried the round's deepest structural
44
+ finding no other seat found. Open every non-`clean` `review-<seat>.md` and judge it by substance
45
+ before discarding it. Before blaming a model's format for a run of `unstructured`/zero-finding
46
+ legs, **check account credit health** — a mid-run credit exhaustion produces the same signature
47
+ (repair and judge legs failing together) and has been traced to billing, not model behavior. Tell:
48
+ a cluster of legs failing in seconds with zero tokens, especially if OpenRouter-routed seats die
49
+ while direct-routed seats on the same run keep working.
50
+ - **`NO_OUTPUT_BACKSTOP` is a fixed no-first-token deadline (120s by default) — it kills slow
51
+ models, not just dead ones, and the once-only auto-retry cannot save them.** A model-agnostic
52
+ dead-man's switch arms at leg launch and disarms only on substantive activity (output growth,
53
+ reasoning-token growth, or a tool call); it is independent of `--timeout`, and the only override
54
+ is the env var `AMICUS_NO_OUTPUT_BACKSTOP_MS` — there is no CLI flag or per-model setting. The
55
+ retry reuses the same threshold, so a slow-but-alive model can burn its one retry for nothing and
56
+ still exit degraded. It fires identically on a genuinely dead route and on a live model that
57
+ simply thinks past 120s before its first token — **distinguish them with a solo re-run before
58
+ blaming the model.** Size an override from the *slowest observed leg × 2* with real headroom; a
59
+ merely-doubled setting has still been killed by the slowest legs seen in practice. It applies to
60
+ every leg in the process, so raising it loses the genuine dead-endpoint fast-fail for the whole
61
+ bench while it's set.
37
62
  - **Transient provider errors** (502s, connection drops): re-run the affected leg (solo
38
63
  `amicus start --json`, same briefing file) or the wave — see per-model notes for
39
64
  model-specific signals. Never present a half-finished run as an answer.
@@ -75,6 +100,15 @@ the peer-consensus≠evidence rule upstreamed from the field ledger; see changel
75
100
  multi-file packet despite the standard preamble. Treat a many-file briefing as a long-read task:
76
101
  expect stubs, retry solo once with the identical briefing, then substitute the model — or
77
102
  pre-concatenate the packet into one file for weaker readers.
103
+ - **For code/plan review, inline a curated source pack and forbid tools — it eliminates the
104
+ narrate-then-stub class outright.** Instead of pointing seats at a repo to read live, put the
105
+ artifact plus a line-numbered extract of exactly the cited source regions directly in the
106
+ briefing, each headed with its real file path, under an explicit "everything is inline; do NOT
107
+ use any tools or read any files" preamble. This has produced first-attempt findings from every
108
+ seat on a bench — including models whose documented failure mode is multi-file agentic reads —
109
+ and it makes citation-checking possible, since reviewers can verify line numbers against the
110
+ pack. Trade is a bigger briefing per seat; worth it whenever the question is "are these claims
111
+ true of this code?"
78
112
  - **`--agent Plan` can trigger a literal, hard "planning mode — cannot execute" refusal on some
79
113
  models** (terse, near-zero-token responses), not just the intended read-only tool restriction —
80
114
  and prompt wording alone has not fixed it; switching that model to `--agent Build` has, immediately.
@@ -106,6 +140,11 @@ the peer-consensus≠evidence rule upstreamed from the field ledger; see changel
106
140
  own `runStats` rows too — so anything keying `runStats` by model must use an **allowlist**, not a
107
141
  judge exclusion; see `docs/council.md`'s `runStats[].role` roster (under `amicus council tally`)
108
142
  for the exact set.
143
+ - **Budget Stage 2 at rough parity with Stage 1 on a multi-seat bench, not as a rounding error.**
144
+ Every judge reads the *whole* anonymized bundle, so judge input scales with `seats ×
145
+ review-length` while Stage-1 input is just the briefing — on a 4-seat bench the bundle is often
146
+ larger than the briefing itself. Rule of thumb: estimate Stage 2 ≈ Stage 1, then add the chair.
147
+ The old habit of treating cross-review as cheap comes from small benches with short reviews.
109
148
  - **Expect agreement inflation in Stage-2 adjudication.** The judge contract defines `agree` by
110
149
  worked example ("an 'I missed this — it's valid' counts as agree") but gives no example for
111
150
  `dispute` and no positive definition of `neutral`, while requiring a verdict on EVERY finding —
@@ -124,6 +163,11 @@ the peer-consensus≠evidence rule upstreamed from the field ledger; see changel
124
163
  `~/.config/amicus/config.json` take precedence over the shipped routes, so a bench you picked for
125
164
  price can resolve to a Pro/preview tier and trip a low `--max-cost` (exit 1) — or quietly cost
126
165
  5-10× what you budgeted. Confirm what each seat actually resolves to before a budget bench.
166
+ - **`amicus models` shows what THIS machine resolves an alias to, not the shipped catalog.** It
167
+ merges local `config.json` overrides silently, so a CI runner or a teammate's machine — which
168
+ carries no user config — can seat a materially different model under the identical alias and
169
+ price. To know what actually ships, read the alias table in `src/utils/curated-models.js`
170
+ directly rather than trusting a local `amicus models` listing.
127
171
  - **The chair cannot also hold a bench seat**, so a bench built from budget aliases cannot chair
128
172
  itself with one of them — pick the chair from *outside* the bench list.
129
173
  - **Stage-6 approvals:** write the proposed MODEL-NOTES diff to a run-folder file and put that path
@@ -199,6 +243,12 @@ the peer-consensus≠evidence rule upstreamed from the field ledger; see changel
199
243
  - **Asserts context-dependent facts (dates, "is this future?") without verifying** — and self-confirms them in adjudication. Cross-check any time-dependent claim it raises.
200
244
  - A good calibration anchor in cross-review: confirms observational findings, disputes interpretive overreach. A repeatable role: the seat most likely to catch **confidence inflation** in an otherwise-correct argument (theoretical risk asserted as demonstrated mechanism, proxies used as pseudo-diagnostics).
201
245
  - **When gpt disputes a specific numeric claim, weight that dispute heavily before a debate round erodes it** — it has been right and then talked out of the objection by a persuasive rebuttal (see the peer-consensus rule).
246
+ - **On long, dense, code-heavy artifacts it returns fast and skims — seat it for calibration, not
247
+ coverage, there.** Where it returns 25-32 findings on prose/resumes/business docs, on a dense
248
+ technical/code plan it has come in the fastest leg on the bench by a wide margin with only a
249
+ couple of findings against peers' 8 — both real, both confirmed, so this is shallow rather than
250
+ wrong; its lifetime confirm-rate held. Put a slower, deeper model on coverage and let gpt anchor
251
+ calibration on artifacts like this.
202
252
 
203
253
  ### Grok (`--model grok` → via OpenRouter)
204
254
  - Very fast legs; credible judge and chair (rejected its own weak findings as chair; honest blind self-rank).
@@ -221,8 +271,15 @@ the peer-consensus≠evidence rule upstreamed from the field ledger; see changel
221
271
  - Its specificity is also its risk: it reaches for **exhaustive quantitative claims ("every",
222
272
  "all", "none") that are directionally right and literally wrong** — and it has successfully
223
273
  DEFENDED one in a debate round against a correct dispute. Verify its universal quantifiers
224
- against a primary source (see the peer-consensus rule).
225
- - **Very slow legs (4-8× its peers)** it gates wave wall-clock; budget timeouts around it.
274
+ against a primary source (see the peer-consensus rule). It has also been observed doing the
275
+ opposite well: correctly scoping its own blocker-severity finding as *contingent on an unprobed
276
+ precondition* rather than asserting it outright — a genuine calibration improvement worth
277
+ crediting when it shows up.
278
+ - **Its time-to-first-token now sits directly on the `NO_OUTPUT_BACKSTOP` cliff, not comfortably
279
+ under it** — observed legs have run well past the 120s default, and even a doubled 180-240s
280
+ override has been killed by the slowest of them. Size the override from the *slowest observed
281
+ leg × 2* with real headroom (see the Global `NO_OUTPUT_BACKSTOP` rule); it still gates wave
282
+ wall-clock either way.
226
283
  - Stalls on long agentic reads (poller "Incomplete" with only a preamble). Reserve for short-artifact work.
227
284
 
228
285
  ### Mistral (`--model mistral` → via OpenRouter)
@@ -271,8 +328,16 @@ the peer-consensus≠evidence rule upstreamed from the field ledger; see changel
271
328
  - Honest under pressure both ways: it refused to fabricate on repair attempts, and it has
272
329
  withdrawn a contested finding cleanly in debate. An honest refusal still costs the seat — watch
273
330
  `conformance` per seat, not just the finding count.
274
-
275
- ### Qwen (`--model qwen` qwen3.7-max via OpenRouter; distinct from `qwen-coder`)
331
+ - **Cheap-seat pattern across repeat use: low finding count, high calibration value.** It has
332
+ repeatedly been the cheapest seat on its bench while contributing fewer findings by volume than
333
+ peers, but its findings have skewed toward internal-consistency catches (a claim contradicted by
334
+ an earlier section of the same artifact) rather than raw coverage — read them for that, not for
335
+ volume, before discounting a low count.
336
+ - **First use as chair was clean and notably good:** recognized when two surviving reviews had
337
+ split the labor rather than duplicated it and synthesized across that seam instead of flattening
338
+ it. A viable chair candidate alongside deepseek.
339
+
340
+ ### Qwen (`--model qwen` → qwen3.8-max via OpenRouter; distinct from `qwen-coder`)
276
341
  - Very large context (1M tokens per catalog). As a red-team substitute it has produced a thorough,
277
342
  well-organized adversarial review with accurate, specific line citations and genuinely unique
278
343
  catches that verified true against the source — weight its specific, cited claims heavily.
@@ -283,8 +348,12 @@ the peer-consensus≠evidence rule upstreamed from the field ledger; see changel
283
348
  short-artifact work: a book-length single-file read succeeded on retry with the anti-narration
284
349
  preamble, but multi-file packets have produced narration stubs twice in one run — with the
285
350
  preamble present, under both Plan and Build. It narrates rather than refuses under Plan.
351
+ - **Inlining removes its multi-file-read failure mode entirely.** On a fully inlined briefing (no
352
+ agentic file reads) it has tied for most findings on the bench and ranked #2 by every judge —
353
+ the counterpart to the multi-file-read stalls above. Seat qwen for inlined verification-
354
+ scaffolding review; keep it off agentic multi-file reads.
286
355
 
287
- ### minimax (`--model minimax` → via OpenRouter)
356
+ ### minimax (`--model minimax` → minimax-m2.7 via OpenRouter)
288
357
  - Fast (~2 min review legs), cheap, `clean` findings-JSON conformance on debut.
289
358
  - Took the **critic seat** brief exceptionally well: unanimously ranked #1 by its bench, full
290
359
  coverage on a ground-truth test, zero padded findings under the anti-padding rule. A strong
@@ -410,3 +479,13 @@ This section keeps only per-model **qualitative quirks** and **structural-confor
410
479
  gemini-3.1-pro), and gemini/deepseek/gpt/grok/kimi/Claude enrichments. Standing practice: each
411
480
  release cherry-picks generalizable lessons per-section (docs/publishing.md release checklist),
412
481
  never a bulk copy.
482
+ - **2026-08-25 (fold-back, both directions)** — Folded in from the field ledger: the
483
+ `NO_OUTPUT_BACKSTOP` no-first-token-deadline mechanism and sizing rule; `conformance` measures
484
+ format not content, with the credit-exhaustion attribution correction; the `amicus models`
485
+ local-vs-shipped alias-resolution caveat; the inlined-source-pack technique; the Stage-2
486
+ cost-parity rule; kimi's backstop-cliff and contingent-precondition calibration nuance; gpt's
487
+ technical-artifact shallowness and qwen's inlining rehabilitation; glm's calibration-over-volume
488
+ and chair-debut notes; minimax's pinned route id. Adopted back into the local ledger: the v4.7
489
+ `runStats` allowlist correction to the spend-tracking rule (the local copy's v4.6
490
+ judge-exclusion description was superseded). No client/engagement material, run identifiers, or
491
+ dollar figures carried over.