amicus 4.7.1 → 4.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +988 -1
- package/README.md +5 -4
- package/docs/CITATIONS.md +122 -0
- package/docs/ROADMAP.md +33 -5
- package/docs/SHIMS.md +1 -1
- package/docs/configuration.md +3 -3
- 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/ipc-setup.js +18 -2
- package/electron/main.js +46 -3
- package/electron/setup-ui-aliases.js +2 -2
- package/electron/setup-ui-model.js +99 -9
- package/electron/setup-ui-styles.js +22 -0
- package/electron/setup-ui.js +231 -29
- 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 +4 -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 +18 -14
- 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/parse-stage2.js +1 -1
- 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 +137 -2
- package/src/utils/alias-audit.js +81 -3
- 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/doctor-alias-check.js +152 -0
- package/src/utils/model-canonicalization.js +64 -0
- package/src/utils/model-shortlist.js +100 -0
- package/src/utils/no-output-backstop.js +1 -1
- package/src/utils/provider-default-picker.js +93 -45
- package/src/utils/provider-default-prompt.js +1 -1
- package/src/utils/quick-picks.js +2 -2
- package/src/utils/remediation-hints.js +37 -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 +30 -4
- package/src/workspace/seat-space.js +143 -0
package/docs/council.md
CHANGED
|
@@ -39,7 +39,7 @@ orchestration recipe. This page is the reference for the artifacts that recipe p
|
|
|
39
39
|
|
|
40
40
|
```
|
|
41
41
|
Stage 1 reviews (models, via `amicus fanout`)
|
|
42
|
-
│ each leg saved as review-<
|
|
42
|
+
│ each leg saved as review-<seat>.md (prose + trailing ```json findings block)
|
|
43
43
|
▼
|
|
44
44
|
amicus council validate <leg-file> ← per-leg findings-block check (tri-state exit)
|
|
45
45
|
│ ok:true → findings[] usable
|
|
@@ -211,9 +211,9 @@ Key semantics:
|
|
|
211
211
|
```
|
|
212
212
|
council-<runId>/
|
|
213
213
|
briefing-stage1.md # composed seat briefing (user briefing + templates)
|
|
214
|
-
review-<
|
|
214
|
+
review-<seat>.md x N # Stage-1 outputs (one per bench seat)
|
|
215
215
|
bundle-stage2.md # anonymized judge bundle (identical for all judges)
|
|
216
|
-
judge-<
|
|
216
|
+
judge-<seat>.md x N # Stage-2 raw outputs (one per judging seat)
|
|
217
217
|
chair-packet.md # de-anonymized chair packet (+ verdict-scale addendum)
|
|
218
218
|
chair-output.md # chair raw output
|
|
219
219
|
tally-input.json # the assembled five-keys object (auditability)
|
|
@@ -224,6 +224,22 @@ council-<runId>/
|
|
|
224
224
|
_scratch/ # cwd for judge legs (isolation)
|
|
225
225
|
```
|
|
226
226
|
|
|
227
|
+
**`<seat>` in those filenames is the seat id, not the model alias.** A seat id *is* its alias
|
|
228
|
+
whenever that alias occupies exactly one bench position — which is every bench with no repeated
|
|
229
|
+
`--models` entry, so these filenames are unchanged there. When the same alias occupies more than
|
|
230
|
+
one seat, the seats are `<alias>#1`, `<alias>#2`, … and the files they write are
|
|
231
|
+
`review-<alias>-1.md`, `review-<alias>-2.md`, and so on. The same rule names the `judge-`,
|
|
232
|
+
`rebuttal-` and `revote-` files below. The Council Workspace reads these names directly: its
|
|
233
|
+
allowlist is built from `run.seats`, so each seat's own file opens under that seat.
|
|
234
|
+
|
|
235
|
+
One case is deliberately left unattributed. When a leg cannot be bound to a seat, the engine still
|
|
236
|
+
writes its output — under the **alias**, because that is all it knows (`review-<alias>.md`). That
|
|
237
|
+
file stays readable, but it is attributed to **no seat**: `run.json` records that *an* orphan
|
|
238
|
+
happened, not *which* seat produced it, and guessing would be exactly the silent mis-attribution
|
|
239
|
+
seat identity exists to prevent. If such a name collides with another seat's own artifact — possible
|
|
240
|
+
when one alias sanitizes onto another seat's filename — neither is attributed and the run-integrity
|
|
241
|
+
banner names both claimants.
|
|
242
|
+
|
|
227
243
|
Two more files appear when the run was started through **`amicus_council_run`** rather than the
|
|
228
244
|
CLI, both written by the MCP handler before it spawns the engine:
|
|
229
245
|
|
|
@@ -276,14 +292,14 @@ one round.
|
|
|
276
292
|
`--no-ledger` and writes `tally-provisional.json`. If nothing landed Contested or Disputed,
|
|
277
293
|
there is nothing to debate — the engine skips straight to the final tally
|
|
278
294
|
(`debate.outcome: "nothing-to-debate"`).
|
|
279
|
-
2. **Defense.** Every Contested/Disputed finding goes back to
|
|
280
|
-
run — `rebuttal-<
|
|
281
|
-
`WITHDRAW` per finding. A dead or unparseable defense leg means the
|
|
282
|
-
undefended.
|
|
283
|
-
3. **Re-vote.** Defended/amended findings go back to the
|
|
295
|
+
2. **Defense.** Every Contested/Disputed finding goes back to the **seat** that raised it as one
|
|
296
|
+
concurrent solo run — `rebuttal-<seat>.md` per raising seat — asking for exactly one of
|
|
297
|
+
`DEFEND` / `AMEND` / `WITHDRAW` per finding. A dead or unparseable defense leg means the
|
|
298
|
+
original claim stands undefended.
|
|
299
|
+
3. **Re-vote.** Defended/amended findings go back to the **seats** that disputed them, as ONE
|
|
284
300
|
shared fanout wave — `revote-bundle.md` (the shared prompt, written to the run dir like
|
|
285
|
-
Stage 2's `bundle-stage2.md`) + `revote-<
|
|
286
|
-
line leaves that
|
|
301
|
+
Stage 2's `bundle-stage2.md`) + `revote-<seat>.md` per re-voting seat. A missing/unparseable
|
|
302
|
+
re-vote line leaves that seat's original verdict standing.
|
|
287
303
|
4. **Final tally.** The engine reassembles the tally input with the defense/re-vote outcomes
|
|
288
304
|
folded in and re-tallies — this final, post-rebuttal tally is the one that appends to the
|
|
289
305
|
reliability ledger (a lens run is the only thing that suppresses the append — `council run` has no `--no-ledger` escape hatch; that flag is parsed only by `council tally`). Withdrawn findings stay
|
|
@@ -303,7 +319,13 @@ after the re-vote keeps its final tier.
|
|
|
303
319
|
finding with what happened in the round and the tier it held before the re-vote.
|
|
304
320
|
- Extra run-dir artifacts, written only when a defense/re-vote actually ran:
|
|
305
321
|
`tally-provisional.json`, `revote-bundle.md`, `debate.json` (the round's structured record),
|
|
306
|
-
`rebuttal-<
|
|
322
|
+
`rebuttal-<seat>.md` × (raising seats), `revote-<seat>.md` × (disputing seats).
|
|
323
|
+
- **Both waves are sized in seats, so a bench that repeats an alias costs more here.** Two seats
|
|
324
|
+
sharing an alias that both raise a contested finding get two defense solos, and two that both
|
|
325
|
+
dispute get two re-vote legs — up to two extra billed legs per duplicated pair per round, plus a
|
|
326
|
+
bounded repair solo for either of them if its output does not parse. `revoteJudges` above counts
|
|
327
|
+
the re-vote legs; nothing in `run.json`'s `debate` object counts the defense solos, though every
|
|
328
|
+
one of them appends its own wave id to `stages[]` under `debate-defense`.
|
|
307
329
|
- `--claude-review <file>` enters Claude's own review (from a file, no leg launched) as a judged
|
|
308
330
|
entry; per the reserved-seat rule, it is never asked to defend in the debate round — its
|
|
309
331
|
Contested/Disputed findings simply stand, the same "originals stand" outcome as a dead defense
|
|
@@ -534,8 +556,10 @@ amicus council tally <input.json> [--json] [--no-ledger]
|
|
|
534
556
|
```
|
|
535
557
|
|
|
536
558
|
Reads a **tally-input** JSON file, computes the deterministic tally **record**, prints it (human
|
|
537
|
-
summary, or the full record with `--json`), and — unless `--no-ledger` — appends one row per
|
|
538
|
-
model to the reliability ledger.
|
|
559
|
+
summary, or the full record with `--json`), and — unless `--no-ledger` — appends one row per distinct
|
|
560
|
+
(`model`, `resolvedModel`) pair to the reliability ledger (v4.8 — see **Ledger append** below;
|
|
561
|
+
that is one row per model on an ordinary bench). Thin CLI wrapper over `tally()`
|
|
562
|
+
(`src/council/tally.js`).
|
|
539
563
|
|
|
540
564
|
### Tally-input schema
|
|
541
565
|
|
|
@@ -547,14 +571,15 @@ under "Stage-2 → tally assembly recipe"). It needs **all five top-level keys**
|
|
|
547
571
|
| Field | Type | Meaning |
|
|
548
572
|
|---|---|---|
|
|
549
573
|
| `meta.runId` | string | Run identifier (conventionally the run-folder stem). |
|
|
550
|
-
| `meta.models` | string[] | Every reviewed model id, including `"claude"` when "Claude in the council" is on — this is the street-cred universe. |
|
|
574
|
+
| `meta.models` | string[] | Every reviewed model id, including `"claude"` when "Claude in the council" is on — this is the street-cred universe: `streetCred[]` has exactly one row per `meta.models` entry, always (`streetCred.length === meta.models.length`, holds on any input — **v4.8 follow-up**, closing a case where a `meta.seats` table that disagreed with `meta.models` in count used to drop or invent a row), in `meta.models` order — the k-th occurrence of a repeated alias takes the k-th row, so a non-adjacent repeat (e.g. `["a","b","a"]`) is never grouped by alias. |
|
|
551
575
|
| `meta.chair` | string | The confirmed chair model id. |
|
|
552
576
|
| `meta.claudeInCouncil` | boolean | The Stage-0 toggle. |
|
|
553
577
|
| `meta.runType`, `meta.date` | string (optional) | Free-form labels carried through to `verdict.json`. |
|
|
554
|
-
| `
|
|
555
|
-
| `
|
|
556
|
-
| `
|
|
557
|
-
| `
|
|
578
|
+
| `meta.seats` | array (optional) | **v4.8** — the run's seat table, one `{id, alias, role, lens, position}` entry per **bench** seat in bench order. The engine emits it **only when the bench repeats an alias**: that is 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 never assume `meta.models.length === meta.seats.length`, and never join the two positionally. ⚠️ **Absence never means "the bench had no repeated alias."** Hand-assembled and MCP-assembled tally input reaches `tally()` with no seat machinery behind it at all; absence means only "no seat table available". |
|
|
579
|
+
| `findings[]` | array | One entry per finding across all reviews: `{id, raiser, severity}` (`claim` may ride along but isn't required by the tally engine). `id` is the run-global label (e.g. `A1`, `B2`) assigned during Stage-2 assembly, not the reviewer's local integer id. `raiserSeat?` (**v4.8**) — the raising **seat's** id (`deepseek#1`), emit-only-when-it-differs-from-the-alias, so a bench with no repeated alias never carries it. `raiser` stays the alias in every case. |
|
|
580
|
+
| `adjudications[]` | array | One entry per (judge × finding): `{findingId, judge, verdict}`, `verdict ∈ {agree, dispute, neutral}`. `seat?` (**v4.8**) — the judging **seat's** id, on the same emit-when-different terms as `findings[].raiserSeat`; `judge` stays the alias. Include every judge's verdict on every finding, **including the raiser's own adjudication of its own finding** — the engine excludes it automatically when scoring (don't pre-filter it). ⚠️ **v4.8: that exclusion is seat-conditional.** When a vote *and* its finding both carry a seat id, the engine compares **seats** (`v.seat !== f.raiserSeat`), so on a bench that repeats an alias a twin's genuine vote on its twin's finding is now counted instead of discarded. When either side carries no seat id — a legacy document, a hand-assembled one, or a real run whose leg failed to bind to its seat — it falls back to comparing **aliases**, which is the pre-v4.8 behaviour and still drops that twin's vote. Never fill in a seat id you did not observe just to unlock the seat compare. ⚠️ **`""` is not a model id.** The schema accepts an empty string for `raiser` and for `judge`, but the engine cannot identify a vote it has no name for: when a finding's `raiser` is empty or missing, every vote whose `judge` is also empty or missing is excluded from `basis` and counted in `findings[].unattributedPeerDrops`. A **seat id on both sides overrides this** — it is a stronger identity than either name, so a seated vote is scored (or excluded as the raiser's own) regardless of what `raiser` and `judge` say. Send the real alias, or expect the vote not to be scored. |
|
|
581
|
+
| `rankings[]` | array | One entry per judge: `{judge, order}`. `order` is that judge's `FINAL RANKING:` block translated to model ids, e.g. `["gpt", "deepseek"]` (ties may use a nested array, e.g. `[["gpt","deepseek"], "mistral"]`). `seat?` (**v4.8**) — the judge's own seat id, on the same emit-when-different terms as `adjudications[].seat`; `judge` stays the alias. `orderSeats?` (**v4.8**) — the seat-valued parallel of `order`, slot for slot (a tied slot is a nested array there too): each slot is a seat id where the ranked model's seat is known, `null` where it is not, and the whole key is emitted only when at least one slot is non-null. **Two consumers read it.** Street cred keys on `orderSeats` when present and falls back to the alias otherwise — the mechanism that lets a twin bench's two street-cred rows diverge instead of collapsing into one. **v4.8** — the **chair packet** is the second: its peer-rankings block zips `orderSeats` onto `order` slot for slot, so the chair reads seat ids where the run knows them and the ranked alias where it does not. A tied slot is zipped element by element, and a `null` slot renders the alias rather than the word `null`. |
|
|
582
|
+
| `runStats[]` | array | One row per paid launch (v4.7 spec §5 D1/D2 — no longer capped at one row per model; see the role roster below): `{model, role, wasChair, conformance, status, durationMs, usage, waveId?, resolvedModel?, seat?}`. `seat?` (**v4.8**) is the row's seat **id**, emit-only-when-it-differs-from-that-seat's-own-alias — so only a bench that repeats an alias carries it. Only the two producers that *have* a seat pass one: the primary reviewing-seat rows and the dead-seat rows. A `judge`, `chair-attempt`, `repair` or `superseded` row never carries it (all four are excluded from the ledger join, so a seat stamp there could never win it), and neither do the off-bench chair rows or the synthetic `claude` row, which have no seat at all. Two seats of one alias that **both** died usually get **two** rows: each carries its own `seat` id where the run bound that seat's leg, and **no** `seat` where it could not — an unidentified dead seat is counted but never named. They still collapse into a **single** row in two cases, both of them seats the run genuinely cannot tell apart: both legs missing a task id, and a run with no seat table behind it — the deliberate floor, since inventing an identity there would be a guess. A retry wave that came back with **fewer legs than it launched** was a third such case and is **no longer**: v4.8 T-A4 made the retry reconcile count a key's SLOTS rather than test its presence, so both twins get a row (measured end to end through `runStage1`: 1 primary dead-seat row before, 2 after, with the superseded rows unchanged at 2 — one of the two rows carries `usage: null`, and **which one is arbitrary**: neither row names a seat, so the alias's billed total is split across its anonymous rows by row ORDER, never by identity). ⚠️ **Corrected in v4.8** — this cell previously claimed the two *always* collapse into one row carrying no `seat`. That was already wrong for **bound** twins the day it was written (the two-row behaviour landed 2026-08-13, this sentence 2026-08-14), and v4.8 closed the unbound half for every retry outcome, the partial return included (that last one in T-A4). May be `[]`. Any leg with no run document gets `durationMs: null, usage: null` — never invent a value. `waveId` is emit-only-when-set. `resolvedModel?` (v4.7) — the executable id that actually served the row's leg, emit-only-when-set; leg-less rows (the give-up chair row, dead seats with no leg, the claude row) never carry it. `model` stays the council alias. ⚠️ **One row shape carries `usage` with NO `waveId`, `resolvedModel` or `durationMs` (v4.8)**: an unidentified dead seat on an alias the bench repeats, where the run holds a billed retry leg it cannot attribute to either twin. The cost is real and is counted in the run total, but every per-seat execution fact is withheld rather than guessed — so do **not** assume `usage` implies `waveId`, and do not treat a null `durationMs` as "this seat cost nothing". |
|
|
558
583
|
|
|
559
584
|
**`runStats[].role` roster (v4.7 row-per-launch).** Every leg the run budget counts gets exactly
|
|
560
585
|
one row, so a seat that needed a repair or lost a leg to a retry can now show up more than once.
|
|
@@ -577,10 +602,15 @@ suffixed label (`rebuttal`/`revote` render unsuffixed); `council tally` has no p
|
|
|
577
602
|
at all, only an aggregate. And only `chair-attempt`/`repair`/`superseded` are filtered out of the
|
|
578
603
|
Workspace seats panel — `rebuttal`/`revote` rows still render there.
|
|
579
604
|
|
|
580
|
-
`runStats[].waveId` names the exact wave/leg a row was built from, present **iff
|
|
581
|
-
leg
|
|
605
|
+
`runStats[].waveId` names the exact wave/leg a row was built from, present **iff the row can
|
|
606
|
+
name a billed leg as its OWN** — e.g. the synthetic `claude` row, a give-up chair's error row, and a
|
|
582
607
|
leg-less dead-seat/critic/lens primary error row (the two SL-2 retry note-classes that never
|
|
583
|
-
produced a real leg for the seat at all) carry none.
|
|
608
|
+
produced a real leg for the seat at all) carry none. ⚠️ **Corrected in v4.8** — this read *"iff a
|
|
609
|
+
real billed leg backs the row"*, which stopped being true when unidentified dead twins gained the
|
|
610
|
+
row shape described in the `runStats[]` cell above: a real billed leg backs those rows (their
|
|
611
|
+
`usage` is on the record and in the run total) and they still carry no `waveId`, because the leg
|
|
612
|
+
belongs to a seat the row cannot claim to be. **So `waveId` is not a safe filter for "rows carrying
|
|
613
|
+
billed usage" — read `usage` for that.** It's the join key the leg–row bijection
|
|
584
614
|
invariant suite (`tests/council/run-cost-bijection.test.js`) uses to prove every budget-counted
|
|
585
615
|
leg lands on exactly one row. `resolvedModel` follows the same emit-only-when-set discipline and
|
|
586
616
|
the same never-invent rule — it is never derived from the alias.
|
|
@@ -601,7 +631,7 @@ groups by `resolvedModel || model` — see the stats section below.
|
|
|
601
631
|
"type": "council-tally",
|
|
602
632
|
"meta": { "...": "echoed from input" },
|
|
603
633
|
"judged": true,
|
|
604
|
-
"streetCred": [ { "model": "gpt", "withSelf": 1, "peersOnly": 1, "perJudgeRank": { "...": "..." } } ],
|
|
634
|
+
"streetCred": [ { "model": "gpt", "withSelf": 1, "peersOnly": 1, "perJudgeRank": { "...": "..." }, "seat": "gpt#1" } ],
|
|
605
635
|
"findings": [
|
|
606
636
|
{ "id": "A1", "raiser": "deepseek", "severity": "major", "tier": "Confirmed",
|
|
607
637
|
"basis": { "a": 1, "d": 0, "n": 0 }, "confidence": "thin",
|
|
@@ -617,12 +647,16 @@ groups by `resolvedModel || model` — see the stats section below.
|
|
|
617
647
|
| `schemaVersion` | Tally-record schema version (currently `2`, council family v2 — see `type` below). This is a *separate* version line from the `--json` **error-envelope** schema version used by `BAD_ARGS` failures (also currently `2`) — the two happen to share a value right now but evolve independently; don't conflate them when scripting against output. |
|
|
618
648
|
| `type` | Document-type discriminator; always `"council-tally"` (council family v2 envelope). |
|
|
619
649
|
| `judged` | `true` only when `rankings.length >= 2`. `false` (1 or 0 rankings) means street-cred numbers exist but rest on thin cross-review. |
|
|
620
|
-
| `streetCred[].withSelf` | Mean rank position across **all** judges' rankings (lower = better). |
|
|
621
|
-
| `streetCred[].peersOnly` | Mean rank position
|
|
650
|
+
| `streetCred[].withSelf` | Mean rank position across **all** judges' rankings (lower = better). **v4.8: `streetCred[]` is now one row per SEAT, not one row per alias** — a bench that repeats an alias used to emit two byte-identical rows under that shared alias; each seat now gets its own row, with its own numbers. A unique-alias bench is unaffected: one alias is one seat there, so the row set and every number stay byte-identical to before. |
|
|
651
|
+
| `streetCred[].peersOnly` | Mean rank position excluding this row's own ranking of itself (lower = better). **v4.8: the exclusion is seat-conditional.** When this row and a judge both carry a seat id, the engine compares **seats** — so on a bench that repeats an alias, a twin's OTHER seat is a real peer and its ranking of this seat counts, even though it shares this row's alias. Only when either side lacks a seat id (a unique-alias bench, or a document with no seat channel) does it fall back to comparing **aliases**, which is the pre-v4.8 behaviour and the only case a document without seats can produce. This is the number used everywhere else (ledger, `stats`, bench recommendations). |
|
|
652
|
+
| `streetCred[].seat` | **v4.8**, optional. This row's own seat id, on the same emit-when-**different**-from-the-alias terms as the tally-input schema's `rankings[].seat` above. Absent unless the bench repeated an alias. |
|
|
622
653
|
| `findings[].tier` | One of `Confirmed \| Contested \| Singleton \| Disputed` — see the cascade below. |
|
|
623
|
-
| `findings[].basis` | `{a, d, n}` = peer agree/dispute/neutral counts (raiser's own vote excluded when a raiser is known). |
|
|
654
|
+
| `findings[].basis` | `{a, d, n}` = peer agree/dispute/neutral counts (the raiser's own vote is excluded when a raiser is known). **v4.8: the exclusion is seat-conditional.** When the vote *and* the finding both carry a seat id the engine excludes by **seat**, so a twin's real vote on its twin's finding now counts; otherwise it falls back to excluding by **alias**, exactly as before. Consequence on a bench that repeats an alias: findings move tier in **both** directions — a lone twin corroboration promotes `Singleton → Confirmed` and `thin → solid`, and a twin *dispute* can demote `Confirmed → Contested` or `Contested → Disputed`. ⚠️ **v4.8: the seat comparison decides first, and it no longer needs a known raiser.** When the vote *and* the finding both carry a seat id, that pair settles it however `raiser` reads: same seat means the raiser's own vote (excluded), different seats mean a real peer (counted). Only when the seats cannot decide does the raiser's name matter. ⚠️ **And when the raiser is *not* known, every vote whose `judge` is equally unidentifiable is excluded too.** An empty-string or missing `raiser` is not an identity, so a vote with an empty-string or missing `judge` cannot be told apart from the raiser's own and does not count as peer signal; every **named** judge still counts. Together these close the case where a finding's own raiser voted its finding up — a document with `raiser: ""` and a `judge: ""` vote used to score that vote into `basis`, and so did one whose vote carried the raiser's own seat id. Votes excluded because nobody could attribute them are announced in `unattributedPeerDrops` below; votes the seats attributed are not, because nothing about them is ambiguous. The tier can fall as a result — a finding whose only votes are unidentifiable is `Singleton`, not `Confirmed` or `Disputed`. |
|
|
624
655
|
| `findings[].confidence` | `"thin"` when `a + d <= 1` (only one peer engaged), else `"solid"`. Thin-confidence findings are the ones Claude may override before Stage 4. |
|
|
625
656
|
| `findings[].tierOverride` | `null` unless Claude recorded an override; shape `{from, to, reason}`. |
|
|
657
|
+
| `findings[].raiserSeat` | **v4.8**, optional — echoed verbatim from the input finding (see the tally-input schema above). Absent unless the bench repeats an alias. |
|
|
658
|
+
| `findings[].sameModelCorroboration` | **v4.8**, optional, `true` only — a warning stamp: after the seat-aware exclusion above, at least one *agreeing* peer of this finding shares the raiser's **alias**, i.e. the corroboration came from another seat of the same model and is not independent. Emitted only when true (never `false`), so a document without it is byte-identical to a pre-v4.8 one. ⚠️ **Alias-only, and it errs in both directions:** it *misses* `--models gpt-5,openai/gpt-5` (genuinely one model under two aliases — votes carry no `resolvedModel` to compare) and it *fires falsely* on a **split alias**, one alias whose two seats happened to resolve to different executables. The reliability ledger uses a different notion of identity for the same run — it treats `(alias, resolvedModel)` as the key — so the two documents can disagree about what "the same model" means. Treat the stamp as "worth a second look", never as proof. |
|
|
659
|
+
| `findings[].unattributedPeerDrops` | **v4.8**, optional, integer `> 0` only — a count of the votes excluded from `basis` that the engine could not attribute to anyone. **Two shapes produce it.** (1) *Raiser named:* the alias fallback excluded a vote while exactly one side of the pair (the finding's `raiserSeat` or the vote's `seat`) carried a seat id and the other did not — a seat-less vote sharing the raiser's alias cannot be told apart from the raiser's own, so excluding it is the safe call **and** may be discarding a real twin's signal. (2) *Raiser not named:* the finding's `raiser` is empty or missing and so is the vote's `judge`, so the vote may be the unnamed raiser's own. ⚠️ **Neither shape includes a vote the seat ids settled.** When the vote and the finding both carry a seat id the engine knows whose vote it is, so excluding it *attributes* it rather than losing it and nothing is announced — this count is for genuine ambiguity only. Emitted only when `> 0`, so a document with no unattributable drop is byte-identical to a pre-v4.8 one. ⚠️ **On shape (1) this field announces the drop; it does not change it.** That vote was already excluded from `basis` before v4.8 and still is — deliberately, by owner ruling R2. ⚠️ **And shape (1) records a POSSIBLE loss, not an established one.** A seat-less vote sharing the raiser's alias is *either* a real twin's signal being discarded *or* the raiser's own vote being correctly excluded — the shape is defined by the engine not being able to tell, so it cannot also be read as knowing. Not being able to tell is precisely why the drop is **announced** rather than silently taken. SI-22.1 / SI-22.2 track that possibility, which this release does not close; they do not record a loss anyone has established. ⚠️ **On shape (2) the exclusion *is* the change**, and `basis` moves: those votes used to be counted, which let an unnamed raiser corroborate its own finding. It is not yet rendered anywhere. As of v4.8 T2.3 the only **artifacts** it reaches are the tally documents — `tally.json`, and `tally-provisional.json` on a debate run, which is the same `tally()` record written as an audit artifact before any debate leg launches. `verdict.js`'s findings literal is closed and does not copy it, so it never reaches `verdict.json`; `debate.json`'s findings rows are built from a separate closed literal, so it never reaches those either; and no renderer (`council report`, the Workspace matrix, the defense brief) displays it. ⚠️ It **is** present in memory, on the `byRaiser` rows `debate.js`'s `debateTargets` hands to the defense brief — the brief simply never prints it. |
|
|
626
660
|
| `tierCounts` | Convenience totals across all findings — this is what `renderRecord`'s human-readable summary prints. |
|
|
627
661
|
|
|
628
662
|
**The peers-only tier cascade** (`assignTier(a, d)` — exhaustive over all `(a, d)`):
|
|
@@ -638,9 +672,29 @@ A lone corroborating peer (`a=1, d=0`) ranks as **Confirmed (thin)** — it must
|
|
|
638
672
|
a lone disputing peer (`a=0, d=1`, which is **Contested (thin)**). A 2-vs-2 split is **Contested**
|
|
639
673
|
(large-bench tie), not Disputed — `d > a` is required for Disputed, not just `d >= 2`.
|
|
640
674
|
|
|
641
|
-
**Ledger append.** Unless `--no-ledger`, `tally` writes one row per
|
|
642
|
-
`council-ledger.jsonl` (append-only, JSON Lines).
|
|
643
|
-
|
|
675
|
+
**Ledger append.** Unless `--no-ledger`, `tally` writes one row per distinct
|
|
676
|
+
(`model`, `resolvedModel`) pair on the bench to `council-ledger.jsonl` (append-only, JSON Lines).
|
|
677
|
+
That is one row per `meta.models` entry on an ordinary bench, where every alias is unique and
|
|
678
|
+
contributed exactly one joinable `runStats` row — but **not** when one alias was served by one
|
|
679
|
+
executable across more than one seat (v4.8): `--models a,a` whose two seats resolved to the same
|
|
680
|
+
executable writes **one** row, not two, and a chair that is also a bench seat writes one row when
|
|
681
|
+
its chair leg and seat leg resolved to the **same** executable and **two** when they resolved to
|
|
682
|
+
different ones. An alias whose seats resolved *differently* writes one row per executable, so no leg
|
|
683
|
+
is erased. Two *distinct* aliases that share one resolution still write one row **each** — the
|
|
684
|
+
collapse is per alias; it is `council stats` that aggregates them into one executable-keyed group.
|
|
685
|
+
Use `--no-ledger` for a re-tally that shouldn't
|
|
686
|
+
double-count (e.g. re-running after fixing a malformed input): a re-tally appends a second full set
|
|
687
|
+
of rows, which **doubles the conformance histogram** in `council stats` (a tally — nothing divides
|
|
688
|
+
it). It does **not** move the lifetime averages: a duplicated set of rows has the same mean as the
|
|
689
|
+
original. Since v4.8 it no longer doubles `runs`/`low-N` either — those count distinct `meta.runId`
|
|
690
|
+
values, and a re-tally of the same input carries the same one — so a harness that writes a
|
|
691
|
+
**constant** `runId` across genuinely different runs will pin that group at `runs: 1` forever.
|
|
692
|
+
⚠️ That holds only when `meta.runId` is a **non-empty string**. `council-tally.schema.json` declares
|
|
693
|
+
it as a bare `string`, so `"runId": ""` is valid input, and an empty string is not an identity —
|
|
694
|
+
each such row counts individually, so re-tallying *that* file still inflates `runs` and `low-N`.
|
|
695
|
+
Measured on a one-model bench, three tallies of the same input: `runs` 1 → 2 → 3, with `low-N`
|
|
696
|
+
clearing on the third. A numeric `runId` behaves identically; a real string stays pinned at 1.
|
|
697
|
+
Two standing uses from the skill's
|
|
644
698
|
optional elements (v2.2.0): **debate mode** tallies provisionally with `--no-ledger` after Stage 2
|
|
645
699
|
and records only the final post-rebuttal tally, and **expert-lens runs** always pass `--no-ledger`
|
|
646
700
|
(lens reviews aren't comparable to standard reviews, so they must not feed `stats`). This is
|
|
@@ -668,8 +722,14 @@ For each finding, `buildVerdict` looks up the matching decision by `id` and fold
|
|
|
668
722
|
`applied` (default `false`), `duplicateOf` (default `null`), and `tierOverride` (decision's
|
|
669
723
|
override wins over the tally record's, if both are present — the effective `tier` becomes
|
|
670
724
|
`tierOverride.to` when an override exists). Everything else (`basis`, `confidence`,
|
|
671
|
-
`adjudications`, `streetCred`, `runStats`, `tierCounts`)
|
|
672
|
-
unchanged.
|
|
725
|
+
`adjudications`, `raiserSeat`, `sameModelCorroboration`, `streetCred`, `runStats`, `tierCounts`)
|
|
726
|
+
passes through from the tally record unchanged, and `meta.seats` is carried across as the
|
|
727
|
+
top-level `seats`.
|
|
728
|
+
|
|
729
|
+
⚠️ **`buildVerdict` is a closed projection, not a copy.** Both of its literals name every key they
|
|
730
|
+
emit — the top level renames `meta.models` to `council`, and each finding is rebuilt from a fixed
|
|
731
|
+
field list — so a key added to the tally record does **not** reach `verdict.json` until it is
|
|
732
|
+
named here. That is why the v4.8 keys below each needed their own line.
|
|
673
733
|
|
|
674
734
|
**Output schema** (`verdict.json`, schema v2 — independent of the tally record's own
|
|
675
735
|
`schemaVersion`):
|
|
@@ -688,7 +748,7 @@ unchanged.
|
|
|
688
748
|
"duplicateOf": null, "adjudications": [ "..." ],
|
|
689
749
|
"decision": "accepted", "applied": true }
|
|
690
750
|
],
|
|
691
|
-
"streetCred": [ { "model": "gpt", "withSelf": 1, "peersOnly": 1 } ],
|
|
751
|
+
"streetCred": [ { "model": "gpt", "withSelf": 1, "peersOnly": 1, "seat": "gpt#1" } ],
|
|
692
752
|
"runStats": [ "..." ],
|
|
693
753
|
"tierCounts": { "Confirmed": 1, "Contested": 1, "Singleton": 1, "Disputed": 0 }
|
|
694
754
|
}
|
|
@@ -698,6 +758,9 @@ unchanged.
|
|
|
698
758
|
- `schemaVersion` — verdict-document schema version (currently `2`).
|
|
699
759
|
- `type` — document-type discriminator; always `"council-verdict"` (council family v2 envelope).
|
|
700
760
|
- `overallVerdict` — the chair's verdict-scale outcome: one of `"Ship it"`, `"Fix these first"`, `"Fundamental rethink"`, or `null` when no chair verdict was produced (populated by the headless engine during Stage 3; `null` for a plain `council verdict` merge without engine integration).
|
|
761
|
+
- `seats` — **v4.8**, optional. The tally record's `meta.seats` (same `{id, alias, role, lens, position}` shape), promoted to the top level next to `seatLoss`. Present only when the tally record carried one, i.e. only when the bench repeated an alias. It is what makes the `alias#N` ids on `findings[].raiserSeat`, `adjudications[].seat` and `runStats[].seat` resolvable from the verdict **alone** — before v4.8 the verdict named seats it could not resolve. `council report` reads it to give each seat its own adjudication-matrix column; when it is absent, or is not an array of objects each carrying a string `id`, the **adjudication matrix** falls back to alias space whole and renders exactly as it did before v4.8. ⚠️ **That fallback is the matrix's alone — it is not a whole-document guarantee.** The street-cred table beside it labels each row from `streetCred[].seat` whenever the row carries one, a predicate independent of this key, so a verdict with seated `streetCred[]` rows and no usable `seats` renders seat ids in the street-cred table and aliases in the matrix (measured on an absent `seats`, a non-array `seats`, and an array-of-strings `seats`). In-process both fields come from the same twin bench and travel together; the split is reachable on a hand-assembled or externally-supplied record, which `buildVerdict`'s own docblock names. A verdict written before v4.8 carries no `streetCred[].seat` at all and is unaffected.
|
|
762
|
+
- `findings[].raiserSeat` — **v4.8**, optional. The raising seat's id, carried through from the tally record; absent unless the bench repeated an alias. `findings[].raiser` stays the alias.
|
|
763
|
+
- `findings[].sameModelCorroboration` — **v4.8**, optional, `true` only. Carried through from the tally record; see the tally-record notes above for the stamp's meaning **and for the two directions in which it is wrong** (it misses one model behind two aliases, and it fires falsely on one alias behind two executables).
|
|
701
764
|
- All other keys (`runId`, `council`, `findings`, `streetCred`, `runStats`, `tierCounts`) are passed through unchanged from the tally record.
|
|
702
765
|
|
|
703
766
|
**Write path:** always atomic — a `<out>.tmp-<pid>` file is written first, then renamed over the
|
|
@@ -735,7 +798,8 @@ amicus council report <verdict.json> [--wave <wave.json>] [--md|--html]
|
|
|
735
798
|
Pure renderer — reads `verdict.json` (+ optional `wave.json`, used only to source the wave's total
|
|
736
799
|
cost) and produces **one self-contained string**: Markdown (default) or a self-contained HTML page
|
|
737
800
|
(`--html`). No scoring, no anonymization, no synthesis — those already happened upstream. Thin CLI
|
|
738
|
-
wrapper over `buildReport()` (`src/council/report.js
|
|
801
|
+
wrapper over `buildReport()` (`src/council/report.js`, which builds the neutral model and
|
|
802
|
+
dispatches to `report-md.js` / `report-html.js` — the two renderers that own the string formats).
|
|
739
803
|
|
|
740
804
|
**What it renders**, in this order: a header (run type, id, date, chair, council members), a
|
|
741
805
|
verdict-summary tier-count table, the **adjudication matrix** (finding × judge, `✓`/`✗`/`–` with
|
|
@@ -743,6 +807,84 @@ verdict-summary tier-count table, the **adjudication matrix** (finding × judge,
|
|
|
743
807
|
tier** (Disputed first), and a **cost table** (per-model status/duration/cost + wave total,
|
|
744
808
|
sourced from `runStats[].usage`).
|
|
745
809
|
|
|
810
|
+
⚠️ **v4.8 — on a bench that repeats an alias, the matrix is keyed by SEAT.** When `verdict.json`
|
|
811
|
+
carries a `seats` table the columns are titled `deepseek#1` / `deepseek#2` rather than two
|
|
812
|
+
identical `deepseek` headers, the Raiser cell names the raising **seat**, and the `*` marks that
|
|
813
|
+
seat's column only — so exactly one of two same-alias columns carries it. This is a bug fix as
|
|
814
|
+
much as a rename: the old alias key was **last-wins**, so the second seat's vote overwrote the
|
|
815
|
+
first's and a finding whose `basis` was `a0/d1` could render as two agreements, both starred. Now
|
|
816
|
+
the rendered row and the finding's `basis` agree. **Benches with no repeated alias are
|
|
817
|
+
byte-identical to v4.7** — every seat id there *is* its alias, so nothing in the document differs
|
|
818
|
+
and nothing in the render does either. A verdict with no `seats` table renders its **matrix** in
|
|
819
|
+
alias space exactly as it always has — including anything written before v4.8, which carries no
|
|
820
|
+
seat fields at all. ⚠️ **The street-cred table below the matrix is not covered by that sentence.** Its row labels
|
|
821
|
+
come from `streetCred[].seat`, never from `seats`, so a hand-assembled verdict carrying seated
|
|
822
|
+
street-cred rows without a usable `seats` table shows `deepseek#1`/`deepseek#2` there while the
|
|
823
|
+
matrix stays in alias space. See the `seats` key note in the verdict-document schema above.
|
|
824
|
+
|
|
825
|
+
⚠️ **v4.8 — the `UNATTRIBUTED` column.** A vote the matrix cannot attribute to a column is no
|
|
826
|
+
longer dropped from the render. The vote→column join **refuses** a key that identifies nothing — an
|
|
827
|
+
empty string, a missing or non-string `judge`, or a seat id or alias that names no column on the
|
|
828
|
+
bench — and folds every such vote into one extra column, headed `UNATTRIBUTED` and placed last
|
|
829
|
+
among the judge columns. This closes the gap this section used to disclose: a judge whose Stage-2
|
|
830
|
+
leg never bound to its seat emits no `adjudications[].seat`, so in seat space its vote keys to a
|
|
831
|
+
bare alias no column reads. Before v4.8 that vote counted in `basis` and rendered nowhere; now it
|
|
832
|
+
counts in `basis` **and** renders.
|
|
833
|
+
|
|
834
|
+
- **`basis` does not move.** This is a rendering change only — such a vote was always counted and
|
|
835
|
+
still is. It is the same property the seat re-key above exists for: what the row shows and what
|
|
836
|
+
`basis` says now agree.
|
|
837
|
+
- **The column appears only when a vote actually folds.** A document in which every vote is
|
|
838
|
+
attributable renders exactly as it did before — no extra column, and never an empty one.
|
|
839
|
+
- ⚠️ **Read the header as “no column on this bench”, not “nobody knows who voted”.** The rule the
|
|
840
|
+
renderer applies is about the **column**, not the voter: a vote lands here when its key names no
|
|
841
|
+
column on *this document's* bench. Usually that also means the voter is unidentifiable — but not
|
|
842
|
+
always. A vote whose `judge` names a model the report deliberately keeps off the bench folds here
|
|
843
|
+
too, with its `judge` field intact in the document. The column says *this vote had nowhere to go*.
|
|
844
|
+
- **Every folded vote on one finding shares one cell, last-wins.** One column is the deliberate
|
|
845
|
+
design — it records a fact about the document, not one per voter — so two folded votes on the same
|
|
846
|
+
finding collapse to the later one's verdict. What tells them apart is a seat id, and supplying one
|
|
847
|
+
is a producer-side fix, not a rendering one.
|
|
848
|
+
- **The Council Workspace matrix applies the same refusal**, over the `tally.json` roster described
|
|
849
|
+
in the paragraph below. The two are deliberately separate implementations rather than a shared
|
|
850
|
+
module, and they are held in agreement by an exhaustive cross-product test rather than by
|
|
851
|
+
construction. ⚠️ **One known exception, disclosed rather than fixed:** on a
|
|
852
|
+
`--claude-review` run the report filters the reserved `claude` seat off its bench while the
|
|
853
|
+
Workspace keeps it, so a hand-authored `judge: "claude"` vote folds to `UNATTRIBUTED` in the
|
|
854
|
+
report and lands in the `claude` column in the Workspace. No engine run emits such a vote. The
|
|
855
|
+
two rosters are built from different sources, and reconciling them is deliberately out of scope
|
|
856
|
+
here.
|
|
857
|
+
- ⚠️ **One residual, disclosed rather than fixed:** if a bench seat is *literally* named
|
|
858
|
+
`UNATTRIBUTED`, no extra column is added and folded votes land in that seat's own column. Nothing
|
|
859
|
+
reserves the name, so this is reachable only by naming a seat that way on purpose.
|
|
860
|
+
- This is **not** the same thing as `findings[].unattributedPeerDrops` in the tally-record schema
|
|
861
|
+
above. That field counts votes the *peer filter* excluded from `basis` on the raiser side, and
|
|
862
|
+
ruling R2 deliberately leaves those excluded; this column renders a vote that
|
|
863
|
+
**is** in `basis` but had no column to land in. Different mechanism, different document, opposite
|
|
864
|
+
effect on `basis`.
|
|
865
|
+
|
|
866
|
+
In the Council Workspace the same matrix is built from `tally.json` (via `tally.meta.seats`) and
|
|
867
|
+
behaves identically, with one deliberate difference: it keeps rendering the blank `claude` column
|
|
868
|
+
that the report filters out. **Blind mode never renders a seat id** — a seat id contains its
|
|
869
|
+
alias, so both twins collapse to `Review A` there, exactly as before v4.8. Its legend is worded
|
|
870
|
+
`* raiser` where the report's reads `` `*` raiser's own vote ``; the two say the same thing, and
|
|
871
|
+
both now refer to the raiser's seat. The report's legend gains a **second** line the Workspace
|
|
872
|
+
matrix does not carry — `` `†` `` marks a finding corroborated only by another seat running the
|
|
873
|
+
same model — so from v4.8.0 the two legends are no longer interchangeable. That line, and the `†`
|
|
874
|
+
itself, appear only on a run that actually raised such a finding, which is a twin bench only.
|
|
875
|
+
|
|
876
|
+
⚠️ **v4.8 — a SECOND report/Workspace divergence, in the street-cred table, filed not fixed.** The
|
|
877
|
+
sentence above is scoped to the **matrix**, and stays true. The street-cred table is a different
|
|
878
|
+
story: both renderers (`report-md.js`, `report-html.js`) label each row `seat || model`, so a twin
|
|
879
|
+
bench reads `gemini#1` / `gemini#2`, while the Workspace's street-cred table still labels from the
|
|
880
|
+
model alias
|
|
881
|
+
and reads `gemini` twice with different numbers under one identical name. **On a bench that repeats
|
|
882
|
+
an alias the two street-cred tables now disagree**; on any bench with no repeated alias they are
|
|
883
|
+
identical, because every seat id there *is* its alias. The report side is the corrected one. The
|
|
884
|
+
Workspace side is not a one-liner — its blind mode resolves the label through `opts.labelOf(...)`,
|
|
885
|
+
which would have to accept a seat id first — and is filed in `BACKLOG.md` for v4.9 with that
|
|
886
|
+
signature change as its gate.
|
|
887
|
+
|
|
746
888
|
This is the same renderer the `second-opinion` skill calls in Stage 5 to produce `report.html`.
|
|
747
889
|
**`report.md` and this renderer's output are two different files** — `report.md` is Claude-authored
|
|
748
890
|
prose that includes a copy of this renderer's Markdown as one section, not this renderer's own
|
|
@@ -955,6 +1097,11 @@ _Legend: ✓ agree · ✗ dispute · – neutral · `*` raiser's own vote_
|
|
|
955
1097
|
| **Wave total** | | | $0.0900 |
|
|
956
1098
|
```
|
|
957
1099
|
|
|
1100
|
+
This bench's two aliases are distinct, so its matrix and legend are byte-for-byte what every
|
|
1101
|
+
pre-v4.8 run produced. On a bench that **repeats** an alias the columns split by seat
|
|
1102
|
+
(`deepseek#1` / `deepseek#2`) and the legend's `*` marks the raiser's **seat** — see "What it
|
|
1103
|
+
renders" above.
|
|
1104
|
+
|
|
958
1105
|
**6. Stats** — after this run's `tally` call appended to the ledger:
|
|
959
1106
|
|
|
960
1107
|
```bash
|
package/docs/doc-system.md
CHANGED
|
@@ -62,7 +62,7 @@ No plans index is generated **today**. `buildPlansIndex()` in `scripts/generate-
|
|
|
62
62
|
never fires. Note that guard is a runtime `fs.existsSync` check, not a disabled feature: creating
|
|
63
63
|
`docs/plans/` would silently reactivate it, emitting a bare list of `- [name](path)` links without
|
|
64
64
|
the per-plan first heading or date the old text here promised. Plans actually live in
|
|
65
|
-
`docs/superpowers/plans/`,
|
|
65
|
+
`docs/superpowers/plans/`, which are working documents pruned at each release cut — specs in `docs/superpowers/specs/` are the permanent record.
|
|
66
66
|
|
|
67
67
|
## Commands
|
|
68
68
|
|
package/docs/publishing.md
CHANGED
|
@@ -58,3 +58,5 @@ Run top-to-bottom before `npm version`:
|
|
|
58
58
|
5. Bump `.claude-plugin/plugin.json` `version` to match `package.json` (no script syncs it). Also bump `server.json` — both `.version` and `.packages[0].version` — to the same value (no script syncs this either). Also bump the `status --json` **example blocks** in `README.md` and `docs/usage.md` (`"version": "x.y.z"`) — they are drift-test-pinned to `package.json` by `tests/docs-quick-sync.test.js` and have now missed the cut twice (v2.1.0 and v2.2.0).
|
|
59
59
|
6. Update `CHANGELOG.md` (move Unreleased → the new version).
|
|
60
60
|
7. `npm version <x.y.z> --no-git-tag-version` + plugin.json lockstep + server.json lockstep (`.version` and `.packages[0].version`), single `chore(release): vX.Y.Z` commit, then push main + tag (publish.yml does the rest — see the canonical ritual in Phase 2 of the 2026-07-01 review-execution plan). If you forget the server.json bump, `tests/scripts/package-manifest.test.js` fails the suite (`server.json versions stay in lockstep with package.json`) — that's your safety net, but don't rely on it; do the bump.
|
|
61
|
+
|
|
62
|
+
- [ ] **Prune the rev's plan docs.** Delete `docs/superpowers/plans/*` belonging to this release in the release-cut commit. Specs in `docs/superpowers/specs/` are permanent and stay. Plans are working documents — they are committed on-branch so they cannot be silently rewritten mid-build, and removed at the cut so `main` never accumulates prescriptions that are false the moment they ship. Git history remains the audit trail. `npx jest tests/docs-plan-refs.test.js` fails if anything still cites a specific plan.
|
package/docs/troubleshooting.md
CHANGED
|
@@ -233,15 +233,15 @@ For full headless configuration, see [docs/configuration.md](./configuration.md)
|
|
|
233
233
|
|
|
234
234
|
## Headless Leg Fails with `NO_OUTPUT_BACKSTOP`
|
|
235
235
|
|
|
236
|
-
**Symptom:** A headless leg (`amicus start --no-ui`, or one leg of a `fanout`/council run) fails with an error starting `NO_OUTPUT_BACKSTOP: no output, reasoning, or tool calls in Ns — the AMICUS_NO_OUTPUT_BACKSTOP_MS window (0 disables)`. You may instead see `...in Ns — a caller-set window overriding the AMICUS_NO_OUTPUT_BACKSTOP_MS default` — this covers two different cases with the same wording: a Stage-1 retry (its window is the resolved `AMICUS_NO_OUTPUT_BACKSTOP_MS` value **doubled**, so a
|
|
236
|
+
**Symptom:** A headless leg (`amicus start --no-ui`, or one leg of a `fanout`/council run) fails with an error starting `NO_OUTPUT_BACKSTOP: no output, reasoning, or tool calls in Ns — the AMICUS_NO_OUTPUT_BACKSTOP_MS window (0 disables)`. You may instead see `...in Ns — a caller-set window overriding the AMICUS_NO_OUTPUT_BACKSTOP_MS default` — this covers two different cases with the same wording: a Stage-1 retry (its window is the resolved `AMICUS_NO_OUTPUT_BACKSTOP_MS` value **doubled**, so a 300 s default reads `600s` on the retry) and the `amicus models --check --live` probe (a fixed, non-tunable `30s` — see below). Both are "caller-set" in the sense that the specific window wasn't read live from the env var at that call, but only the retry case is actually governed by it.
|
|
237
237
|
|
|
238
|
-
**Cause:** The mechanism only knows that the deadline passed with no substantive activity — no output, reasoning, or tool calls — for the backstop window (
|
|
238
|
+
**Cause:** The mechanism only knows that the deadline passed with no substantive activity — no output, reasoning, or tool calls — for the backstop window (300 s by default for an ordinary leg). It does **not** know *why*. Do not read this as "the endpoint is dead" or "the model isn't being served" — the message deliberately stops short of that claim, because at one of its two firing sites the backstop can win the race before the prompt send even resolves, so acceptance itself was never observed. Treat `NO_OUTPUT_BACKSTOP` as "silence past the deadline," not a diagnosis.
|
|
239
239
|
|
|
240
240
|
**Lesson from #133:** an earlier version of this message *did* guess a cause ("likely a listed-but-not-serving model or a dead endpoint"), and that guess sent 30 minutes of debugging at model ids and API keys on a real incident. The actual cause was an OpenCode engine version skew between the npx-cached copy and the global install — sitting the whole time in `~/.local/share/opencode/log/opencode.log`. **Before chasing model/endpoint theories, check that log for the session's real error.**
|
|
241
241
|
|
|
242
242
|
**Confirm:** `amicus models --check` — audits your configured aliases against the live catalog and flags drift/staleness. A model that still resolves locally but has quietly been retired upstream is one possible explanation, but a static audit only compares against the catalog's *listing* — it can't tell you whether a still-listed model actually answers. For that, run `amicus models --check --live` on demand: it sends one tiny real request to every stored alias and reports `SERVED` / `SILENT` / `ERROR` per alias, where `SILENT` (`accepted-but-silent`) is this exact `NO_OUTPUT_BACKSTOP` failure, caught deliberately instead of by accident. It spends real money (one tiny leg per stored alias) — see [docs/usage.md § `amicus models`](./usage.md#amicus-models--the-model-catalog) for cost, scope, and exit-code details.
|
|
243
243
|
|
|
244
|
-
**Fix:** Check the `~/.local/share/opencode/log/opencode.log` for the session's real error first (see #133 above). Then check the alias's target (`amicus models --search <term>` to find the current id, then re-point the alias) — a stale alias is a common case, but not the only one. If the failing leg was a Stage-1 retry (the window read `
|
|
244
|
+
**Fix:** Check the `~/.local/share/opencode/log/opencode.log` for the session's real error first (see #133 above). Then check the alias's target (`amicus models --search <term>` to find the current id, then re-point the alias) — a stale alias is a common case, but not the only one. If the failing leg was a Stage-1 retry (the window read `600s`, or generally double your configured/default value), `AMICUS_NO_OUTPUT_BACKSTOP_MS` **is** the lever — raising it raises both the first attempt's window and the retry's doubled one, unless your `--timeout` is low enough that the doubled window is clamped to the leg timeout. On an ordinary (non-retry) leg, raise it only if a model legitimately needs more than 300 s to produce its first token. The live probe's fixed 30 s window is the one case the env var genuinely cannot touch. See [docs/configuration.md § Headless Poller Tuning](./configuration.md#headless-poller-tuning).
|
|
245
245
|
|
|
246
246
|
---
|
|
247
247
|
|
package/docs/usage.md
CHANGED
|
@@ -403,7 +403,7 @@ Each stored alias resolves to one of three outcomes:
|
|
|
403
403
|
| Outcome | Example line | Meaning |
|
|
404
404
|
|---------|--------------|---------|
|
|
405
405
|
| `SERVED` | `SERVED: gemini -> openrouter/google/gemini-3.6-flash ($0.0004)` | The model answered; cost shown in parens. |
|
|
406
|
-
| `SILENT` (`accepted-but-silent`) | `SILENT: probetest -> anthropic/claude-opus-4-8 — NO_OUTPUT_BACKSTOP: … (accepted but not serving)` |
|
|
406
|
+
| `SILENT` (`accepted-but-silent`) | `SILENT: probetest -> anthropic/claude-opus-4-8 — NO_OUTPUT_BACKSTOP: … (accepted but not serving)` | Nothing arrived at all within the probe's 30 s backstop window — shorter than the ordinary 300 s leg default, and not tunable. ⚠️ `accepted-but-silent` is the CLASSIFICATION's name, not a proven fact about the endpoint: a fired backstop shows only that no output, reasoning or tool call arrived in the window, which a stalled gateway or a dropped connection produces just as readily as a model that accepted and then said nothing. Either way it is the "listed but not actually serving" failure this check exists to catch. |
|
|
407
407
|
| `ERROR` | `ERROR: gpt -> openai/gpt-5.6-terra — 402 Payment Required` | Routing, auth, or provider failure; the raw error is printed. |
|
|
408
408
|
|
|
409
409
|
**Exit code.** The probe's non-served count folds into the same exit code as the static audit — `max(existing exit, min(nonServedCount, 100))` — so a single `SILENT` or `ERROR` fails the check even when every alias is otherwise catalog-fresh. No stored aliases prints `Live probe: no stored aliases to probe` and never affects the exit code. `--json` adds `probe` (the per-alias array) and `probeCount` (its length) to the `alias-audit` document — both additive, `[]`/`0` when `--live` wasn't passed.
|
|
@@ -518,7 +518,7 @@ $ amicus status demo123 --json
|
|
|
518
518
|
"taskId": "demo123",
|
|
519
519
|
"status": "complete",
|
|
520
520
|
"elapsed": "5m 0s",
|
|
521
|
-
"version": "4.
|
|
521
|
+
"version": "4.8.1",
|
|
522
522
|
"model": "google/gemini-2.5-flash",
|
|
523
523
|
"phase": "terminal"
|
|
524
524
|
}
|
package/electron/ipc-setup.js
CHANGED
|
@@ -80,10 +80,26 @@ function registerSetupHandlers(getMainWindow, { ipcMain = require('electron').ip
|
|
|
80
80
|
// Task 8: apply a per-provider default picker choice. Read-modify-write,
|
|
81
81
|
// no-clobber -- applyProviderDefault only ever writes aliases[vendor] and
|
|
82
82
|
// seeds config.default when absent (see provider-default-picker.js).
|
|
83
|
-
|
|
83
|
+
// Fetches the catalog before applying (issue 195): applyProviderDefault
|
|
84
|
+
// uses directFormIfProven (model-canonicalization.js) to decide whether to
|
|
85
|
+
// strip an OpenRouter prefix off chosenId, and needs the catalog to do it.
|
|
86
|
+
ipcMain.handle('sidecar:set-provider-default', async (_event, provider, chosenId) => {
|
|
87
|
+
let catalog = [];
|
|
88
|
+
try {
|
|
89
|
+
const { getCatalog } = require('../src/utils/model-catalog');
|
|
90
|
+
catalog = await getCatalog();
|
|
91
|
+
} catch (err) {
|
|
92
|
+
// Best-effort only -- a fetch failure leaves `catalog` empty, which
|
|
93
|
+
// directFormIfProven (F1, council review of PR 198) reads as NO
|
|
94
|
+
// evidence, never as license to strip: chosenId is persisted exactly
|
|
95
|
+
// as given, not re-derived. Applying an already-made picker choice
|
|
96
|
+
// must never abort on a catalog hiccup, and must never fabricate an
|
|
97
|
+
// id on one either -- that was the exact bug issue 195 fixed.
|
|
98
|
+
logger.error('set-provider-default catalog fetch error', { error: err.message });
|
|
99
|
+
}
|
|
84
100
|
try {
|
|
85
101
|
const { applyProviderDefault } = require('../src/utils/provider-default-picker');
|
|
86
|
-
return applyProviderDefault(provider, chosenId, { seedDefaultIfAbsent: true });
|
|
102
|
+
return applyProviderDefault(provider, chosenId, { seedDefaultIfAbsent: true, catalog });
|
|
87
103
|
} catch (err) {
|
|
88
104
|
logger.error('set-provider-default handler error', { error: err.message });
|
|
89
105
|
return { success: false, error: err.message };
|
package/electron/main.js
CHANGED
|
@@ -327,11 +327,24 @@ function createAmicusWindow() {
|
|
|
327
327
|
async function createSetupWindow() {
|
|
328
328
|
// Lazy-load setup UI to avoid loading it for sidecar mode
|
|
329
329
|
const { buildSetupHTML } = require('./setup-ui');
|
|
330
|
-
const { resolveQuickPicks } = require('../src/utils/quick-picks');
|
|
330
|
+
const { resolveQuickPicks, toStorableRoute } = require('../src/utils/quick-picks');
|
|
331
331
|
let quickPicks;
|
|
332
|
+
const shortlists = {};
|
|
332
333
|
try {
|
|
333
334
|
const catalog = await require('../src/utils/model-catalog').getCatalog();
|
|
334
335
|
quickPicks = resolveQuickPicks(catalog);
|
|
336
|
+
|
|
337
|
+
// issue 138: one vendor shortlist per family card, resolved server-side from
|
|
338
|
+
// the same catalog the quick picks came from (no extra IPC round-trip).
|
|
339
|
+
const { buildModelShortlist } = require('../src/utils/model-shortlist');
|
|
340
|
+
for (const p of quickPicks) {
|
|
341
|
+
try {
|
|
342
|
+
shortlists[p.alias] = buildModelShortlist(p.vendorPath, {
|
|
343
|
+
catalog,
|
|
344
|
+
recommendedId: toStorableRoute(p),
|
|
345
|
+
});
|
|
346
|
+
} catch (_e) { /* a shortlist failure must never block the wizard */ }
|
|
347
|
+
}
|
|
335
348
|
} catch (_err) {
|
|
336
349
|
quickPicks = undefined; // buildSetupHTML falls back to pinned
|
|
337
350
|
}
|
|
@@ -348,7 +361,7 @@ async function createSetupWindow() {
|
|
|
348
361
|
}
|
|
349
362
|
});
|
|
350
363
|
|
|
351
|
-
const html = buildSetupHTML({ client: CLIENT, quickPicks });
|
|
364
|
+
const html = buildSetupHTML({ client: CLIENT, quickPicks, shortlists });
|
|
352
365
|
mainWindow.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(html)}`);
|
|
353
366
|
mainWindow.webContents.on('page-title-updated', (e) => e.preventDefault());
|
|
354
367
|
|
|
@@ -499,6 +512,36 @@ registerSetupHandlers(() => mainWindow);
|
|
|
499
512
|
function createSettingsChildWindow() {
|
|
500
513
|
const { buildSetupHTML } = require('./setup-ui');
|
|
501
514
|
|
|
515
|
+
// issue 138: mirror createSetupWindow's catalog resolution so Step 2 shows
|
|
516
|
+
// live per-model options here too, instead of falling back to the pinned
|
|
517
|
+
// `[offline list]` badge. This window must stay synchronous (opening
|
|
518
|
+
// Settings must never trigger a network fetch), so read the on-disk cache
|
|
519
|
+
// directly with readCache() rather than the async fetch-and-refresh helper
|
|
520
|
+
// createSetupWindow awaits. A missing or corrupt cache reads back as null
|
|
521
|
+
// and degrades to the same pinned fallback buildSetupHTML already applies
|
|
522
|
+
// when no quickPicks are given.
|
|
523
|
+
const { resolveQuickPicks, toStorableRoute } = require('../src/utils/quick-picks');
|
|
524
|
+
const { readCache } = require('../src/utils/model-catalog');
|
|
525
|
+
let quickPicks;
|
|
526
|
+
const shortlists = {};
|
|
527
|
+
try {
|
|
528
|
+
const cacheDoc = readCache();
|
|
529
|
+
const catalog = cacheDoc ? cacheDoc.models : [];
|
|
530
|
+
quickPicks = resolveQuickPicks(catalog);
|
|
531
|
+
|
|
532
|
+
const { buildModelShortlist } = require('../src/utils/model-shortlist');
|
|
533
|
+
for (const p of quickPicks) {
|
|
534
|
+
try {
|
|
535
|
+
shortlists[p.alias] = buildModelShortlist(p.vendorPath, {
|
|
536
|
+
catalog,
|
|
537
|
+
recommendedId: toStorableRoute(p),
|
|
538
|
+
});
|
|
539
|
+
} catch (_e) { /* a shortlist failure must never block the wizard */ }
|
|
540
|
+
}
|
|
541
|
+
} catch (_err) {
|
|
542
|
+
quickPicks = undefined; // buildSetupHTML falls back to pinned
|
|
543
|
+
}
|
|
544
|
+
|
|
502
545
|
const settingsWin = new BrowserWindow({
|
|
503
546
|
width: 560, height: 680,
|
|
504
547
|
parent: mainWindow, modal: false,
|
|
@@ -512,7 +555,7 @@ function createSettingsChildWindow() {
|
|
|
512
555
|
}
|
|
513
556
|
});
|
|
514
557
|
|
|
515
|
-
const html = buildSetupHTML({ client: CLIENT });
|
|
558
|
+
const html = buildSetupHTML({ client: CLIENT, quickPicks, shortlists });
|
|
516
559
|
settingsWin.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(html)}`);
|
|
517
560
|
settingsWin.webContents.on('page-title-updated', (e) => e.preventDefault());
|
|
518
561
|
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* delete, and add functionality for the setup wizard Step 3.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
/** Grouping metadata for the
|
|
8
|
+
/** Grouping metadata for the 21 default aliases */
|
|
9
9
|
const ALIAS_GROUPS = [
|
|
10
10
|
{ name: 'Gemini', keys: ['gemini', 'gemini-pro'] },
|
|
11
11
|
{ name: 'GPT', keys: ['gpt', 'gpt-pro', 'codex'] },
|
|
@@ -13,7 +13,7 @@ const ALIAS_GROUPS = [
|
|
|
13
13
|
{ name: 'DeepSeek', keys: ['deepseek'] },
|
|
14
14
|
{ name: 'Qwen', keys: ['qwen', 'qwen-coder', 'qwen-flash'] },
|
|
15
15
|
{ name: 'Mistral', keys: ['mistral'] },
|
|
16
|
-
{ name: 'Other', keys: ['glm', 'minimax', 'grok', 'kimi', 'seed'] },
|
|
16
|
+
{ name: 'Other', keys: ['glm', 'minimax', 'grok', 'kimi', 'seed', 'inkling'] },
|
|
17
17
|
];
|
|
18
18
|
|
|
19
19
|
/**
|