amicus 4.8.0 → 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 (118) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/CHANGELOG.md +355 -0
  3. package/README.md +10 -5
  4. package/docs/CITATIONS.md +13 -5
  5. package/docs/ROADMAP.md +101 -10
  6. package/docs/configuration.md +55 -5
  7. package/docs/council.md +102 -14
  8. package/docs/troubleshooting.md +9 -2
  9. package/docs/usage.md +128 -12
  10. package/electron/ipc-setup.js +39 -2
  11. package/electron/main.js +46 -3
  12. package/electron/offer-session.js +51 -0
  13. package/electron/setup-ui-model.js +99 -9
  14. package/electron/setup-ui-styles.js +22 -0
  15. package/electron/setup-ui.js +244 -32
  16. package/electron/workspace-ui/live-dead-seats.js +163 -91
  17. package/electron/workspace-ui/live-seats.js +4 -4
  18. package/electron/workspace-ui/workspace-banners.js +30 -7
  19. package/electron/workspace-ui/workspace-matrix.js +23 -3
  20. package/electron/workspace-ui/workspace-seats.js +95 -79
  21. package/package.json +2 -1
  22. package/schemas/council-run.schema.json +2 -2
  23. package/schemas/council-tally.schema.json +17 -1
  24. package/schemas/council-verdict.schema.json +12 -4
  25. package/schemas/run.schema.json +6 -1
  26. package/skills/second-opinion/COUNCIL-DESIGN.md +1 -1
  27. package/skills/second-opinion/MANUAL-ORCHESTRATION.md +1 -1
  28. package/skills/second-opinion/MODEL-NOTES.md +88 -9
  29. package/skills/second-opinion/SEAT-BRIEFS.md +36 -4
  30. package/skills/second-opinion/SKILL.md +151 -36
  31. package/src/cli-council-run-bench.js +98 -6
  32. package/src/cli-handlers-council-run.js +18 -6
  33. package/src/cli-handlers-council.js +57 -7
  34. package/src/cli-handlers-doctor.js +12 -15
  35. package/src/cli.js +3 -1
  36. package/src/council/anonymize.js +2 -1
  37. package/src/council/briefings-chair-task.js +161 -0
  38. package/src/council/briefings-chair.js +33 -8
  39. package/src/council/briefings-debate.js +79 -13
  40. package/src/council/briefings-stage2-task.js +236 -0
  41. package/src/council/briefings-stage2.js +103 -26
  42. package/src/council/briefings-task.js +167 -0
  43. package/src/council/briefings.js +41 -4
  44. package/src/council/chair-fallback.js +95 -0
  45. package/src/council/debate.js +38 -21
  46. package/src/council/findings.js +3 -2
  47. package/src/council/ledger.js +2 -2
  48. package/src/council/parse-stage2.js +64 -16
  49. package/src/council/report-cost.js +61 -0
  50. package/src/council/report-html.js +26 -4
  51. package/src/council/report-md.js +30 -2
  52. package/src/council/report.js +40 -37
  53. package/src/council/run-assemble.js +21 -6
  54. package/src/council/run-chair.js +44 -95
  55. package/src/council/run-debate-revote.js +81 -49
  56. package/src/council/run-debate.js +51 -34
  57. package/src/council/run-finish.js +5 -3
  58. package/src/council/run-retry-keys.js +4 -4
  59. package/src/council/run-retry-launch.js +4 -4
  60. package/src/council/run-retry-notes.js +72 -15
  61. package/src/council/run-stage1-launch.js +4 -4
  62. package/src/council/run-stage1-rows.js +9 -6
  63. package/src/council/run-stage2.js +81 -47
  64. package/src/council/run-stages.js +9 -21
  65. package/src/council/run-stats-entry.js +46 -1
  66. package/src/council/run.js +28 -13
  67. package/src/council/seats.js +2 -2
  68. package/src/council/stage1-bind.js +3 -2
  69. package/src/council/verdict-seat-loss.js +124 -0
  70. package/src/council/verdict.js +108 -99
  71. package/src/headless.js +256 -49
  72. package/src/mcp-council-bench.js +64 -3
  73. package/src/mcp-council-run.js +10 -3
  74. package/src/mcp-server.js +52 -12
  75. package/src/mcp-tools.js +41 -5
  76. package/src/observe/council-legs.js +2 -2
  77. package/src/opencode-client.js +19 -1
  78. package/src/pack/pack-forward.js +15 -12
  79. package/src/pack/pack-resolve.js +1 -1
  80. package/src/prompt-builder.js +17 -1
  81. package/src/sidecar/fanout-leg.js +26 -0
  82. package/src/sidecar/fanout.js +1 -1
  83. package/src/sidecar/list-council.js +178 -0
  84. package/src/sidecar/list-limit.js +3 -1
  85. package/src/sidecar/list-search.js +2 -1
  86. package/src/sidecar/models.js +8 -1
  87. package/src/sidecar/read.js +34 -10
  88. package/src/sidecar/setup.js +124 -0
  89. package/src/template/render.js +16 -7
  90. package/src/utils/alias-audit.js +81 -3
  91. package/src/utils/alias-shadow-writer.js +220 -0
  92. package/src/utils/alias-shadow.js +294 -0
  93. package/src/utils/config.js +1 -1
  94. package/src/utils/curated-models.js +16 -8
  95. package/src/utils/degrade.js +12 -5
  96. package/src/utils/doctor-alias-check.js +149 -0
  97. package/src/utils/engine-log-parse.js +289 -0
  98. package/src/utils/engine-log-tail.js +114 -0
  99. package/src/utils/engine-log.js +250 -0
  100. package/src/utils/engine-skew-records.js +146 -0
  101. package/src/utils/engine-skew.js +300 -0
  102. package/src/utils/gateway-router.js +10 -2
  103. package/src/utils/model-canonicalization.js +64 -0
  104. package/src/utils/model-catalog.js +1 -1
  105. package/src/utils/model-shortlist.js +100 -0
  106. package/src/utils/provider-default-picker.js +93 -45
  107. package/src/utils/provider-default-prompt.js +1 -1
  108. package/src/utils/quick-picks.js +2 -2
  109. package/src/utils/remediation-hints.js +24 -0
  110. package/src/utils/result-schema.js +10 -0
  111. package/src/utils/text-sanitize.js +81 -0
  112. package/src/utils/ttft.js +57 -0
  113. package/src/utils/untrusted-fence.js +111 -1
  114. package/src/workspace/fold-format.js +28 -7
  115. package/src/workspace/live-normalize.js +2 -1
  116. package/src/workspace/matrix-model.js +6 -2
  117. package/src/workspace/run-detail.js +35 -9
  118. package/src/workspace/seat-space.js +10 -6
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "amicus",
3
- "version": "4.8.0",
3
+ "version": "4.9.0",
4
4
  "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.",
5
5
  "author": {
6
6
  "name": "Christian Wagner"
package/CHANGELOG.md CHANGED
@@ -3,6 +3,361 @@
3
3
  All notable changes to Amicus are documented here. Format follows
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions follow semver.
5
5
 
6
+ ## [4.9.0] - 2026-08-26
7
+
8
+ *The council does new work.*
9
+
10
+ ### Added
11
+
12
+ - **Task mode — the council stops being able only to critique (#134, #130, #146).**
13
+ `amicus council run --intent task`, or `intent: 'task'` on the `amicus_council_run` MCP tool,
14
+ points the whole pipeline at open-ended work instead of at a review. Every seat **produces the
15
+ deliverable** the briefing asks for — `briefings-task.js :: TASK_SEAT_ROLE` says it in as many
16
+ words, *"you are not reviewing the briefing, you are executing it"* — the Stage-2 judges rank
17
+ **which response best does the work** and adjudicate whether each declared claim holds, and the
18
+ chair synthesizes an **ANSWER**, never a verdict.
19
+ The two terminal scales are disjoint by construction: a review chair closes on
20
+ `Ship it | Fix these first | Fundamental rethink`
21
+ (`briefings-chair.js :: CHAIR_VERDICT_VALUES`, `parse-stage2.js :: CHAIR_VERDICTS`), a task chair
22
+ on `Converged | Split | Insufficient` (`briefings-chair-task.js :: CHAIR_ANSWER_VALUES`,
23
+ `parse-stage2.js :: CHAIR_ANSWERS`). They **share no value and no keyword** — `VERDICT:` and
24
+ `ANSWER:` — which is what lets each parser stay blind to the other's line, and it is
25
+ `parseChairTerminal(text, intent)` that picks between them. Each scale is spelled **five** times —
26
+ those two constants, the scale addendum, the chair repair prompt, and
27
+ `schemas/council-verdict.schema.json`'s `overallVerdict` enum, which now carries the review scale
28
+ followed by the task scale — and all five are drift-pinned together
29
+ (`tests/council/chair-scale-drift.test.js`, named mutant `ANSWERSCALEDRIFT`).
30
+ - **`intent` is emit-when-`task`, everywhere.** `review` is the default spelled out loud: both
31
+ transports strip it, and `run.js :: runCouncil` — a public door too — **normalizes** it to
32
+ absent (`delete`, never `= undefined`) rather than refusing it, so one input cannot mean two
33
+ things at two doors. Only a genuinely unknown value is `BAD_ARGS`. Nothing ever writes
34
+ `intent: "review"` onto `run.json`, `tally.json` or `verdict.json`, so every pre-v4.9 document
35
+ is already spelled correctly and no migration exists to need.
36
+ - **The Stage-1 contract does not fork.** The JSON skeleton, the
37
+ `blocker | major | minor | nit` enum and the required-non-empty `location` rule are review
38
+ mode's, verbatim; `findings.js` — one validator, one repair loop — changes by **zero executable
39
+ lines** (the only edits it took in this release are comments: a refreshed line citation, and the
40
+ empty-`findings[]` ruling now naming the task briefings' claims wording beside the review one).
41
+ Only the frame and the field glosses differ: in task mode `location` is the grounding
42
+ discipline, naming *what the claim rests on — a source, a computation, or the literal word
43
+ "assumption"*.
44
+ - **Intent selects the parser on the Stage-5 rebuild too**, where the run is long over and only
45
+ documents remain. Three carriers, because no one of them covers every leg — the record's own
46
+ `meta.intent`, `run.json`'s checkpoint, and the run folder's prior `verdict.json` — combined as
47
+ a **disjunction** (absence is no vote, not a vote for review). The two *document* reads carry the
48
+ same `!runId || doc.runId === runId` guard their siblings do — waived only when the *record* names
49
+ no run and never when the *document* does not. Without that guard a stale or foreign document in
50
+ the folder hands the rebuild another run's intent, which selects the chair parser. An
51
+ `overallVerdict` carried off the **wrong** scale is not trusted: it fails
52
+ `scale.includes(...)` and falls through to re-parse `chair-output.md`.
53
+ - **The renderers say what the run actually produced.** The primary heading is `## Answer summary`
54
+ on a task run and the exact old `## Verdict summary` on a review run; the tier table carries
55
+ *"Tiers report peer concurrence, never verification."* directly under the counts a skimmer
56
+ reads — task runs only, forked in **both** renderers and pinned separately per renderer so
57
+ neither can regress silently (mutants `SUMMARYLABEL`, `QUALIFIERDROP`). The fold
58
+ (`workspace/fold-format.js`) and the Workspace chip
59
+ (`electron/workspace-ui/workspace-matrix.js`) label the terminal line `ANSWER:` or `VERDICT:`
60
+ off the same key, matching on `=== 'task'` so a pre-v4.9 payload with no `intent` reads as
61
+ review instead of relabelling every legacy run. The chair's own briefing carries the caveat the
62
+ tier table cannot: *"Peer agreement on a claim is CONCURRENCE, not verification — models
63
+ correlate on priors."*
64
+ - **Task runs write zero reliability rows**, at two gates — `run-finish.js` (the engine's own
65
+ append) and `cli-handlers-council.js :: runTally` (the hand-assembled path) — and `council tally`
66
+ now **refuses** a `meta.intent` that is neither spelling rather than letting a near-miss
67
+ (`'Task'`) slide into the ledger, whatever `--no-ledger` says. The skip is **announced and
68
+ non-degrading**: a `Note:` on the new `ledger-skipped` channel, `kind: 'info'` — a third kind
69
+ beside `degrade`/`heal` that the degrade sink is structurally unable to flip `degraded.value`
70
+ on. ⚠️ It is emitted where it is *load-bearing*, not on every task run: the chair-fallback
71
+ promotion arm, which draws on ledger history a task run never fed. That arm is reached at most
72
+ once per `runChair`, and `runChair` runs once per run, so "announced once" is structural rather
73
+ than a flag's promise. `kind: 'info'` records get their own **Notes:** list in the report and
74
+ are kept out of the `## What was lost` heading — an announcement is not a loss.
75
+ - **`amicus council stats` on an empty ledger explains itself.** A fresh install and a task-only
76
+ install used to render the same blank table; the surface now names where rows come from
77
+ (*"reliability history comes from review runs; task runs never write rows here"*) rather than
78
+ asserting which of the two states the reader is in — an earlier wording claimed the latter and
79
+ was false on every fresh install.
80
+ - **`--claude-review` is refused with `--intent task`** — a file review is review machinery and
81
+ has no task-mode meaning.
82
+ - **Review runs are byte-identical *under the intent fork*.** Not "unchanged as far as we know":
83
+ the review path composes through the same dispatcher and is pinned at key *and* byte level, and
84
+ both report `.snap` documents are untouched by this release. Scoped deliberately — task mode
85
+ changes nothing a review run emits; two *other* items in this release do, and say so where they
86
+ land (the repair-attempt `runStats` conformance value, and the `tally-input.json` debate-row key
87
+ order the builder unification moves into agreement with `tally.json`).
88
+ - Limitations, ruled and recorded rather than discovered later: **one intent per run** (no mixed
89
+ bench), intent is **not pack-settable** (nothing in `src/pack/` reads it), and a task run builds
90
+ no reliability history by design. #130's *detector* half — the `location` heuristic that would
91
+ emit a divergence **observation** into `degrades[]` — is deliberately not in this release; what
92
+ ships is the declaration that removes its root cause, since the hard-coded review frame every
93
+ seat used to receive was #130's mechanical cause and #134's blocker at once.
94
+
95
+ - **The engine speaks for itself (#133, pieces 2 and 3).** A `NO_OUTPUT_BACKSTOP` death report no
96
+ longer stops at what the deadline observed — it quotes **the engine's own newest ERROR line for
97
+ that session**, appended as ` — engine log: <excerpt>`. Bounded read, both log schemes,
98
+ boundary-anchored session matching (a line whose structural session field names someone else is
99
+ skipped even when our id appears elsewhere on it), and a **union across every candidate log
100
+ directory that exists**, not the first one — the dirs are alternative homes for the same engine's
101
+ logs, so a stale `$XDG_DATA_HOME` or an empty dir left by a previous install would otherwise
102
+ shadow the dir holding the answer. A matching line whose message part is empty keeps the walk
103
+ going instead of ending the file's scan, because the real failure and a terse message-less
104
+ teardown line are typically neighbours.
105
+ One lookup's scan is memoized per `fs` implementation for 10 s so a whole wave of seats shares it.
106
+ ⚠️ **That reuse serves hits only.** A **miss** re-lists and re-reads before it is believed, and
107
+ drops the live slot behind it, because the engine writes a leg's error *when that leg dies* —
108
+ possibly after the cached scan, possibly into a file it has only just rolled over to, so an
109
+ absence in a previous call's listing is not an absence on disk. The accepted residual runs the
110
+ other way and is stated rather than buried: a warm hit serves the newest line *as of when the slot
111
+ was built*, so the TTL bounds the missed window (≤10 s of new lines) and bounds nothing about the
112
+ **age** of what is served — a minutes-old genuine error for this leg can be quoted while a fatal
113
+ line written two seconds ago sits unread. Quoting a true older diagnostic is the trade; silence
114
+ while the cause sits on disk is the failure this module exists to end.
115
+ Where the answering server's version differs from the running install's engine, the report appends
116
+ ` (engine skew: server <a> ≠ installed <b>)` — tracked per server, refreshed on every session
117
+ create, with a remedy line that names the honest action and says why `doctor` cannot see this
118
+ class. Everything here is best-effort: every path returns `null` rather than throwing, because a
119
+ log read must never break a leg's death report. **#133 closes.**
120
+
121
+ - **TTFT probe — probe, never derive (R12).** `ttftMs` is the elapsed time to the first
122
+ **substantive** tick, measured off the no-output backstop's *own* predicate. The distinction is
123
+ load-bearing and the plan's prose named the wrong one: `progressed` includes the empty assistant
124
+ placeholder OpenCode mints on prompt **acceptance**, so keying on it would stamp a
125
+ time-to-first-token on a leg that never produced a token. The probe and the backstop tick read one
126
+ shared `substantiveActivity` constant, and mutant `PROGRESSEDTWIN` pins the single fixture where
127
+ the two predicates disagree. It rides result docs, leg patches and `runStats` rows emit-when-set,
128
+ behind one validity rule — `utils/ttft.js :: isMeasuredTtft`, imported by three of the four gates
129
+ and hand-spelled by the fourth, `council/run-stats-entry.js`, which is pinned require-free (the
130
+ two spellings are held in step structurally, and a fifth site cannot appear unnoticed) — so an
131
+ unmeasured value is **dropped at the emit gate, never clamped**, and a leg with no measurement is
132
+ byte-identical to a pre-v4.9 one. The per-model backstop *derivation* deliberately waits for observations to derive
133
+ from (#135/#129 record).
134
+
135
+ - **Alias-shadow warning (#135 C5 / #129's own side observation).** One notice per run when a
136
+ user-config alias **shadows** a curated alias with a different id — because everything keyed on
137
+ the alias *name* (per-model operating notes, a bench a workflow spells, a council preset) then
138
+ quietly describes a model the alias no longer resolves to, and by the time `resolveModel` sees an
139
+ id the two sources have already collapsed into one string. Compared in **canonical form**, so a
140
+ gateway spelling of the same id stays silent (mutant `GATEWAYFORM`). Diagnosis only: it resolves
141
+ nothing, changes no id, no exit code and no artifact — a local override winning is the documented
142
+ contract; this just says so out loud.
143
+ Wired at three measured sites, and the third exists because of a measured surface gap:
144
+ `cli-council-run-bench.js :: resolveBench` is the one bench-resolution helper **both** council
145
+ transports execute (bench, chair and critic all inspected there), but the council child's stderr
146
+ is not a pipe the MCP server reads — it is an fd on `<runDir>/debug.log` — so from that site alone
147
+ an MCP caller would have to open a log file to find the notice. `mcp-council-bench.js ::
148
+ auditBenchAliases` therefore writes it into the **tool result's notices array**, a different
149
+ surface rather than a second copy: the child still writes its line to `debug.log`, the parent
150
+ writes its own to the tool result, and no single surface shows it twice. The third site is
151
+ `amicus models --check`, the one call that passes no name list. Every third-party fragment the
152
+ notice quotes rides the house sanitizer (mutants `NOTICERAW`, `MESSAGERAW`, `THROWNRAW`), and the
153
+ stderr writer is armed against a closed pipe (`STREAMFATAL`, `STREAMDEAF`).
154
+
155
+ - **`amicus list` finally shows council runs.** `amicus_list` has merged them since v4.0 §8; the
156
+ CLI never did, so a council launched from the terminal was invisible **to the terminal** —
157
+ `amicus list` reported *"No amicus sessions found."* in a project whose only work was a council.
158
+ The same enumerator now feeds both surfaces through `src/sidecar/list-council.js`: the MODEL cell
159
+ carries the live stage as `council(<stage>)`, mirroring the wave row's `wave(N legs)`, width-capped
160
+ so it can never butt against the STATUS column; `--search` reaches council material on both
161
+ surfaces. **Scope is stated rather than silent:** only the current project's council runs are
162
+ merged, `--all` included — council runs are found through per-project pointer files and there is
163
+ no cross-project council index to walk — so the human listing prints that disclosure, naming no
164
+ remedy because there is no flag that widens it. A merge that fails now **says so, with the cause**
165
+ (sanitized), instead of dropping every council row into output indistinguishable from a project
166
+ that has none.
167
+
168
+ - **The Workspace dead-seat surface — an unbound seat stops being invisible (SI-02, R4).**
169
+ `deriveSeatLoss` and both Workspace consumers now admit the `seat-unbound` family behind **one
170
+ retry-family gate**, because `seat-unbound` is a shared channel — orphan-leg notes, re-vote
171
+ refusals and Stage-2 judge notes ride it too, and each is pinned *excluded* in every consumer.
172
+ The two LOSS reads spell that gate identically (`(retryWaveId || firstFailure) && (seatId ||
173
+ seat)`); the RETRIED read in `workspace-seats.js` is deliberately **narrower** — `retryWaveId`
174
+ alone, since a never-attempted seat carries a `firstFailure` and badging it *retried* would be
175
+ false. One loss rule, one narrowing, both pinned (mutants `SKIPRETRIED-A`/`-B`, one per direction
176
+ the widening could come back) rather than left to drift. The producer half was one line: the partial arm emits a seat id
177
+ beside the alias-valued seat, `null` for a slot it could not identify and never the alias.
178
+ The critic path keys on seat identity where the record and the run can both spell it, so a dead
179
+ bench twin beside a live critic twin renders one correctly-labelled row — it used to render
180
+ **nothing at all**, the silent 0-row erasure this project's product principle rates as severely as
181
+ a crash — and a both-dead pair renders two rows with one critic label. The known-wrong pins were
182
+ **flipped, not renamed**. The third street-cred renderer joins its seat-keyed siblings, and its
183
+ stated precondition was **measured false and struck rather than satisfied**: `labelOf` never
184
+ needed a seat id, and mutant `BLINDSEATLEAK` pins that the literally-planned spelling would have
185
+ leaked one into a blind render. The `seatTableRejected` banner now discloses the seats-vs-artifacts
186
+ document split in wording that was measured before it was written — the first draft's sentence
187
+ named the cost panel as a seat surface, which it is not, and would have shipped false.
188
+
189
+ ### Fixed
190
+
191
+ - **W1's nine small repairs**, one wave, disjoint files, each TDD'd or pinned: the second-opinion
192
+ Stage-4 Confirmed gloss gains the lone-corroborating-peer case in both presentation headings;
193
+ `fmtProbeLine`'s silent parenthetical stops asserting endpoint acceptance and says only
194
+ *"(no output within the probe window)"*; the setup wizard's embedded alias tables are seeded
195
+ null-prototype **on the far side of the parse**, sibling tables and the `__proto__` alias-name
196
+ chain included; `MAX_CATALOG_AGE_MS` retires three copies to one exported source; repair-attempt
197
+ `runStats` rows stamp the **measured** outcome (refined twice by councils — the flat literal
198
+ mislabeled successful repairs, and the ch4 chair push had silently defaulted `clean`);
199
+ `KNOWN_VARIABLES` is single-sourced so validation and rendering derive from one exported set,
200
+ drift-tested — which satisfies the T3-m2 hard gate that composition's `{{input}}` was waiting on;
201
+ the MCP test-client timer leaks behind the standing *"Jest did not exit"* warning class are closed
202
+ in three suites (six open handles → zero, measured); a bare-id `model_not_found` carries the
203
+ doctor-repair hint when the OpenRouter twin classifies valid; and the setup TOCTOU is closed
204
+ **structurally** by consuming the same catalog snapshot the offer was built from — re-ruled twice
205
+ into per-window, session-scoped, read-without-consume offer sessions.
206
+ - **The outbound fences neutralize both tag ends.** `utils/untrusted-fence.js ::
207
+ defangOutboundFenceTags` escapes open *and* close tags for the whole house fence vocabulary over
208
+ both outbound surfaces — the council briefing tail and the parent-conversation section — so a body
209
+ carrying the **sibling** surface's tags cannot smuggle them through the surface that does not
210
+ happen to emit them (mutants `CTXFENCEBREAKOUT`, `BRIEFFENCEBREAKOUT`). ⚠️ Stated as a disclosure,
211
+ not a caveat: an entity escape is a convention about how a reading model interprets bytes, not a
212
+ parser guarantee. This is defense in depth — it removes the literal tag so the escape stops being
213
+ free — and the load-bearing protection is still the preamble both fences carry.
214
+ - **Curated pins move to the generation the bench actually runs** — `kimi` → `kimi-k3`, `qwen` →
215
+ `qwen3.8-max`, `glm` → `glm-5.3`. Not cosmetic: the review reproduced the standing alias-shadow
216
+ line the stale `glm` pin would have printed on every CI council run. Every old-id hit in the suite
217
+ was classified; no live pin of the old generation remains.
218
+
219
+ ### Internal
220
+
221
+ - **The `runStats` builders unify (PR1F-2 closes).** Debate's `mk`, the re-vote `legRow` and the
222
+ Claude row fold into `buildRunStatsEntry`, so every non-primary row shares one key order, one
223
+ set of defaults, and one emit-when-set rule. (What the fold does **not** buy: propagation —
224
+ `mk` passes a synthetic five-field leg, so entry-param fields and future leg-sourced fields
225
+ still require widening `mk` itself; the widening is a filed candidate.) Done by measurement,
226
+ because every pre-existing pin proved
227
+ order-insensitive: 21 byte-order goldens were written **before** any fold (23 in the suite
228
+ now, after two post-fold additions), and mutant
229
+ `UNIFYDRIFT-mk` reddens exactly one test in the whole repo — the golden. A 137-invocation status
230
+ census showed the divergent `'unknown'` default **never fires and structurally cannot**, so it was
231
+ deleted as measured-dead rather than chosen between. ⚠️ The filing's own blast-radius claim was
232
+ **corrected** rather than inherited: `run.json` carries no `runStats`, and `tally.json`/
233
+ `verdict.json` were *already* in the entry's key order via tally's allowlist re-projection — the
234
+ fold moves `tally-input.json` **into** agreement with them.
235
+ - **SI-16's three over-length council functions split at measured seams**, in-file and byte-faithful
236
+ (`runStage2` → `bindStage2Seats`, `runDebate` → `runDefenseWave`, `runRevoteWave` →
237
+ `repairRevoteLeg`), each mutant's red set recorded and each revert SHA-verified.
238
+ - **`seatKey` consolidated to one exported definition** (SI-DUP disposition (b)): the two remaining
239
+ standalone copies of the seat-or-alias rule now import it, with the load-bearing hand-inlined
240
+ fallback and the two `keyOf` sibling forms deliberately untouched.
241
+ - **MCP tool parameters re-measured and pinned** (F-1): 58 documented plus 2 allowlisted of 60 keys,
242
+ with the per-key rulings recorded in the test rather than in a comment that can rot. Separately
243
+ (F-5), `routing.tier` / `tier_onboarded` are documented from measurement, including the silent
244
+ `balanced` coercion.
245
+
246
+ ### CI
247
+
248
+ - **Council backstop headroom.** `council-review.yml` sets `AMICUS_NO_OUTPUT_BACKSTOP_MS` to
249
+ `480000` on the council step, single-spelled and pinned by four workflow tests, with the measured
250
+ 4.8.1-cycle stall evidence recorded in the comment beside it. The broader retry-policy question is
251
+ filed as issue 202 rather than guessed at here.
252
+ - The CI bench moves with the pins above: `kimi` off the bench, `deepseek` seated, `gemini-pro`
253
+ chairs.
254
+
255
+ ## [4.8.1] - 2026-08-25
256
+
257
+ ### Fixed
258
+
259
+ - **Setup Step 2 offered one card per curated model *family*, with no way to choose which model
260
+ within it — issue #138.** `pickCurrent` kept exactly one winner per namespace, so a family with
261
+ several live models collapsed to a single card, and the route pill it wrote stored a **provider**
262
+ id, never a **model** id, so nothing downstream could tell them apart either. The family regex
263
+ made this worse for one vendor outright: `/^deepseek-v[\d.]+(-pro)?$/` only matches a
264
+ version-numbered `-v` name with an optional `-pro` suffix, so `deepseek-r1`, `deepseek-chat`, and
265
+ `deepseek-flash` never had a route to *any* card — structurally excluded, not just hidden behind
266
+ the flagship. Both wizard surfaces — the Electron Settings window and the readline setup flow —
267
+ now offer a vendor-scoped drill-down after the family pick, built on the existing priced picker
268
+ via a new pure module, `model-shortlist.js`, which splits a vendor's rows into suggested/rest so
269
+ the UI can show a handful of recommended models plus an "all" escape hatch. Persistence is
270
+ unchanged: `config.aliases[<family>]` still holds a route-encoded id and `config.default` still
271
+ holds the alias *name* — only the picker got smarter, not the storage shape. Two review rounds on
272
+ this branch closed five further blockers before it shipped: a silent setup-fallback path, a
273
+ misleading model-count label, a false alias-deletion report, a same-run double-ask of the same
274
+ vendor's drill-down, and an unescaped catalog id.
275
+
276
+ - **`chooseRowId` fabricated bare `<vendor>/<model>` ids for catalog rows that exist only on
277
+ OpenRouter — issue #195.** Once the user held that vendor's own API key, the fabricated id routed
278
+ `direct`-first and `catalogGate` rejected it as `model_not_found`. Measured against the live
279
+ catalog: 19 of 69 `google` rows and 51 of 175 `openai` rows. The picker unconditionally stripped
280
+ the `openrouter/` prefix off any non-`DIVERGENT_VENDORS` OR-only row, regardless of whether the
281
+ direct namespace actually carried that model. It now reuses `classifyModel` — never a
282
+ reimplementation of its rule — and synthesizes the bare form only when that id would *not*
283
+ classify `invalid` on `direct`; `deepseek`'s empty-namespace rows (14/14) still synthesize bare,
284
+ unchanged. The identical unconditional strip existed in two more places that would otherwise have
285
+ silently undone the fix at write time — `canonicalizeResolved` (preselection matching) and
286
+ `applyProviderDefault` (the actual persist-to-config path, reached from both the Electron IPC
287
+ handler and the readline flow) — both now carry the same guard. Persistence was hardened further
288
+ in review: a failed or empty catalog fetch at persist time used to degrade straight back to the
289
+ old unconditional strip (confirmed against the real 601-row catalog), so `applyProviderDefault`
290
+ now requires *positive* catalog evidence (`classifyModel` returning `valid`, not merely
291
+ non-`invalid`) before it will strip a prefix — an empty catalog is inert, never a silent fallback.
292
+ ⚠️ **`doctor --fix` can now rewrite your config, not just diagnose it.** A new narrow repair,
293
+ `findFabricatedAliasRepairs`, finds aliases already persisted by v4.8.0 as a fabricated bare id —
294
+ one that classifies `invalid` on `direct` and has an unambiguous OpenRouter twin via
295
+ `pairAcrossGateways` — and rewrites them to the id the fixed picker would offer today. It is
296
+ gated on a **fresh** catalog fetch (declines with an explanatory hint if the same doctor run's own
297
+ `catalog` check reports the cache as stale, rather than repairing from data it has already told
298
+ you not to trust), idempotent, converts only to the one id `pairAcrossGateways` names, and every
299
+ repair is announced through the existing doctor-fix `heal` degrade channel, naming the alias and
300
+ both the old and new id. Outside that one class — a typo, a retired model, a user-invented id, or
301
+ a `DIVERGENT_VENDORS` alias — `doctor --fix` leaves the config alone exactly as before.
302
+
303
+ ### CI
304
+
305
+ - **Council review on this repo ran with whatever alias each seat's name happened to resolve to on
306
+ a bare runner, not the intended bench — #193.** A CI runner has no user config, so
307
+ `getEffectiveAliases()` returned exactly `DEFAULT_ALIASES` and every seat bound to the shipped
308
+ `curated-models.js` pin: `glm` reviewed at 5.1 while 5.2 and 5.3 had already shipped, and an alias
309
+ missing from that table was silently dropped by `classifyCouncilMembers` with only a `run.json`
310
+ note to show for it. It was invisible by construction — `run.json`, `events.jsonl`, and
311
+ `verdict.json` all record alias *names*, so they read identically whichever model actually
312
+ answered; `spend-ledger.jsonl`, written to `getConfigDir()`, is the only artifact that carries a
313
+ resolved id. `council-review.yml` now provisions `.github/amicus-ci-aliases.json` from the
314
+ **base** ref (a PR can never edit the map used to review it), validates it by shape, pre-flights
315
+ every seat before any spend, and uploads the ledger as the receipt; every fetch failure but a 404
316
+ now fails closed instead of silently swapping the bench. A companion job, `alias-pin-drift.yml`,
317
+ compares each curated pin against its newest same-vendor sibling on the same tier/variant and
318
+ opens a weekly bump issue — `models --check` alone is structurally blind to this, since it
319
+ validates a curated *family* against its own `idPattern`, not a flat cardless pin like
320
+ `glm`/`qwen`/`kimi`.
321
+ - **The council briefing now includes the `env:` blocks a diff can't show — #194.** PR #193 was
322
+ reviewed twice, and both benches unanimously raised the same blocker: `$GH_REPO`, `$MODELS`, and
323
+ `$CHAIR` looked undefined in the `run:` step under review. They were defined — in a workflow- or
324
+ job-level `env:` block the PR never touched, so it never appeared in the diff. Four seats agreeing
325
+ was one shared blind spot, not four independent findings. The briefing now appends the workflow-
326
+ and job-level `env:` blocks of every changed workflow (fetched from the base ref, exactly like the
327
+ alias map above), labelled explicitly as context and not part of the diff; step-level `env:` is
328
+ excluded because it already travels with its own hunk. A literal value under a
329
+ `TOKEN`/`SECRET`/`PASSWORD`/`KEY`-shaped name is withheld from the briefing — a pure `${{ }}`
330
+ reference is kept, since it names a secret rather than exposing one.
331
+ - **The council-review check is no longer hardcoded green — #197.** Its `none`-policy branch set
332
+ `CONCLUSION="success"` before the chair's verdict was even read, so three distinct outcomes — a
333
+ clean review, a review with real findings, and a review that never ran at all — rendered as the
334
+ identical green check. `fail_on` now defaults to `rethink`: the check fails only when the chair
335
+ returns "Fundamental rethink," and passes on both "Fix these first" and "Ship it" (a null or
336
+ absent chair verdict still maps to neutral, never failure). A stricter `fail_on: fix` default was
337
+ tried first and reverted within hours: three live runs against PR #196 each came back "Fix these
338
+ first," but of the confirmed findings only one was a real, reachable defect — several were
339
+ accurate statements about states the system cannot produce, and two rested on premises that were
340
+ factually wrong. Gating on that verdict would have blocked more good work than bad. `fail_on` is
341
+ spelled twice in `council-review.yml` — the `workflow_call` input default, and the `pull_request`
342
+ path's `||` fallback, which is the load-bearing one for every label-triggered review on this repo
343
+ — and both spellings moved together, with a drift test re-verified against the historical
344
+ one-site-only mutant at the new value. `none` (report-only) and `fix` (the stricter gate) both
345
+ remain available for a caller that wants them.
346
+
347
+ ### Internal
348
+
349
+ - **Burned down the citation allowlist and re-anchored the citations the gate cannot see —
350
+ citation-burndown, #192.** Twelve stale citations corrected, `CONFIG.grandfathered` emptied to
351
+ zero. Five of the eight previously-allowlisted entries were born stale — wrong in the commit that
352
+ introduced them, not rotted afterward; the no-output-backstop comment among them was a truth
353
+ failure rather than a numbering one, since the guard it warned was missing had been added by the
354
+ very commit that wrote the warning, so it is now reframed as closed history instead of
355
+ renumbered. Separately, several source comments cited a bare `(:129)`/`(:293)` line number with no
356
+ `.js` immediately before the colon — a shape `check-citations.js` cannot parse and so silently
357
+ never checks — and those are now `file.js :: symbol` anchors instead, both correct today and
358
+ visible to the gate from now on; one of them (`run.js:293`) had already rotted to the wrong line
359
+ and is corrected to `run.js:279` in the same pass.
360
+
6
361
  ## [4.8.0] - 2026-08-23
7
362
 
8
363
  ### Fixed
package/README.md CHANGED
@@ -132,12 +132,13 @@ The council is the hero — start with the everyday way, and reach for the more
132
132
  - **Headless, in CI, with no Claude runtime.** `amicus council run --prompt-file plan.md --council free` runs that same pipeline in one command — reviews → cross-review → tally → chair verdict — writing `verdict.json` and `report.html`. It needs no Claude session, so it drops straight into CI. → [Headless council (CI)](#headless-council-ci)
133
133
  - **With a debate round.** Add `--debate` and every Contested or Disputed finding goes back to its raiser to **defend, amend, or withdraw** while the disputing judges re-vote — exactly one rebuttal round, then the final tally. → [The Council](#the-council)
134
134
  - **On free, local, private models — at $0.** Point the council (and sidecars) at an OpenAI-compatible server already running on your machine — Ollama, LM Studio, or vLLM — with `amicus provider add`. No API key, no per-token bill, nothing leaves your machine, and it works offline. → [`amicus provider`](./docs/usage.md#amicus-provider)
135
+ - **Pointed at the work itself, not at a review of it.** Add `--intent task` and the same bench *produces* the deliverable instead of critiquing one. → [Task mode (v4.9)](#task-mode-v49)
135
136
 
136
137
  <p align="center"><img src="./docs/cards/same-table.svg" width="520" alt="A local Ollama model seated as an equal council member at $0.00 — a member, not a mode"></p>
137
138
 
138
139
  ### Headless council (CI)
139
140
 
140
- The same pipeline runs with no Claude runtime at all: `amicus council run --prompt-file briefing.md --models gemini,glm --chair deepseek --json` executes the review waves, the anonymized cross-review, the tally, and the chair verdict in one command, and writes the full run directory (`verdict.json` with the chair's parsed `overallVerdict`, `report.html`, every review and judge output). That is what powers the repo's own **Council Review GitHub Action v2** — on PRs labeled `council-review` it posts an adjudicated verdict as a check run plus a sticky comment, uploads the run directory as an evidence artifact, and can optionally gate merges via its `fail_on` input (default: report-only). Reference: [docs/council.md](./docs/council.md#amicus-council-run).
141
+ The same pipeline runs with no Claude runtime at all: `amicus council run --prompt-file briefing.md --models gemini,glm --chair deepseek --json` executes the review waves, the anonymized cross-review, the tally, and the chair verdict in one command, and writes the full run directory (`verdict.json` with the chair's parsed `overallVerdict`, `report.html`, every review and judge output). That is what powers the repo's own **Council Review GitHub Action v2** — on PRs labeled `council-review` it posts an adjudicated verdict as a check run plus a sticky comment, uploads the run directory as an evidence artifact, and gates merges by default via its `fail_on` input (fails only on a `Fundamental rethink` verdict; pass `fail_on: fix` to require `Ship it`, or `fail_on: none` for report-only). Reference: [docs/council.md](./docs/council.md#amicus-council-run).
141
142
 
142
143
  <p align="center"><img src="./docs/cards/ship-gate.svg" alt="A council gating a release pipeline: exit 0 ships it, exit 1 sends it back"></p>
143
144
 
@@ -168,6 +169,10 @@ amicus council run --pack review-bench --prompt-file plan.md --json
168
169
 
169
170
  Any flag you also type on that second line overrides just that value — a pack only fills in what you didn't say explicitly, and it's recorded on the run either way. Packs work the same way on `fanout`/`start` and on the `amicus_fanout`/`amicus_start`/`amicus_council_run` MCP tools. `amicus pack list`/`show`/`rm` manage them, and `--from-run <id>` builds one from a run you already liked instead of typing flags at all. Full reference: [docs/usage.md § Policy packs](./docs/usage.md#policy-packs).
170
171
 
172
+ ### Task mode (v4.9)
173
+
174
+ A council reviews by default. `amicus council run --intent task --prompt-file brief.md` — or `intent: 'task'` on the `amicus_council_run` MCP tool — points the same pipeline at open-ended work instead: every seat **produces** the analysis, answer, or artifact the briefing asks for, the judges rank *which response best does the work* and adjudicate the claims each one declared, and the chair synthesizes an **answer** — `Converged | Split | Insufficient` — never a review verdict. The two scales share no value, so a task run can never report `Ship it` and a review run can never report `Converged`. Task runs deliberately write nothing to the reliability ledger (rankings there measure concurrence, not defect confirmation) and say so on the surfaces that would otherwise look empty; a review run is byte-identical to before. Full reference: [docs/council.md § Task mode](./docs/council.md#task-mode---intent-task).
175
+
171
176
  ### Briefing templates (v4.5)
172
177
 
173
178
  `--template <name> --artifact <file>` (plus repeatable `--var k=v`) renders a `{{prompt}}`/`{{artifact}}`-style Markdown template before it's sent, on `start`/`fanout`/`council run` alike — templates live in `~/.config/amicus/templates/`, and a pack's `briefing.template` is how one reaches an MCP-invoked run (MCP has no template param of its own). `amicus template list|show` manage them; v4.5 ships one built-in, `review`. Full reference: [docs/usage.md § Briefing templates](./docs/usage.md#briefing-templates).
@@ -275,7 +280,7 @@ This opens a graphical wizard:
275
280
  | Step | What it does |
276
281
  |------|--------------|
277
282
  | **1. API Keys** | Enter keys for OpenRouter, Google, OpenAI, Anthropic, and/or DeepSeek. Each is validated live against the provider's API. Written to `~/.config/amicus/.env` with `0600` permissions. |
278
- | **2. Default Model** | Pick your go-to model from a searchable live picker (backed by the catalog). Used whenever you omit `--model`. |
283
+ | **2. Default Model** | Pick your go-to model from a searchable live picker (backed by the catalog) — each provider card also lets you drill down to a specific model instead of just the family default. Used whenever you omit `--model`. |
279
284
  | **3. Model Routing** | Decide which provider serves each model — e.g. route Gemini through a direct Google key and everything else through OpenRouter. |
280
285
  | **4. Review** | Confirm the configuration before saving. |
281
286
 
@@ -407,7 +412,7 @@ When you don't need a full council — just one other model's take — fork a co
407
412
  |---------|--------------|
408
413
  | `amicus start` | Launch a new session (interactive or `--no-ui`). |
409
414
  | `amicus fanout` | Run N models on the same prompt in parallel (headless). |
410
- | `amicus list` | Show previous sessions — shows the `--tag <t>` set at launch, `--search <q>` filters, `--limit <n>` caps the rows, `--all` spans every known project. |
415
+ | `amicus list` | Show previous sessions — shows the `--tag <t>` set at launch, `--search <q>` filters, `--limit <n>` caps the rows, `--all` spans every known project (council runs: current project only — the CLI says so in-table). |
411
416
  | `amicus resume` | Reopen a previous session with full history. |
412
417
  | `amicus continue` | Start a new session building on a previous one. |
413
418
  | `amicus read` | Output a session's summary / conversation / metadata. |
@@ -450,13 +455,13 @@ $ amicus status demo123 --json
450
455
  "taskId": "demo123",
451
456
  "status": "complete",
452
457
  "elapsed": "5m 0s",
453
- "version": "4.8.0",
458
+ "version": "4.9.0",
454
459
  "model": "google/gemini-2.5-flash",
455
460
  "phase": "terminal"
456
461
  }
457
462
  ```
458
463
 
459
- `amicus list --status` accepts `running`, `complete`, `error`, `timed-out`, `aborted`, `crashed`, `idle-timeout`. Rows carry a `TAG` column (set at launch with `--tag <t>`); `--search <q>` filters by id/tag/briefing substring, `--limit <n>` caps the row count (0 = unlimited), and `--all` lists across every known project. Full field-by-field docs (a running session's `messages`/`STALLED` reporting, wave-ID status shape, etc.) are in [docs/usage.md](./docs/usage.md).
464
+ `amicus list --status` accepts `running`, `complete`, `error`, `timed-out`, `aborted`, `crashed`, `idle-timeout`. Rows carry a `TAG` column (set at launch with `--tag <t>`); `--search <q>` filters by id/tag/briefing substring, `--limit <n>` caps the row count (0 = unlimited), and `--all` lists across every known project (council runs stay scoped to the current project — no cross-project index exists, and the listing discloses that). Full field-by-field docs (a running session's `messages`/`STALLED` reporting, wave-ID status shape, etc.) are in [docs/usage.md](./docs/usage.md).
460
465
 
461
466
  ---
462
467
 
package/docs/CITATIONS.md CHANGED
@@ -107,8 +107,16 @@ form; do not silently renumber it.
107
107
 
108
108
  ## The burn-down list
109
109
 
110
- `CONFIG.grandfathered` in `scripts/check-citations.js` holds citations that were
111
- already stale when the gate landed, so the gate could block from day one instead
112
- of shipping advisory. Every entry rotted when v4.8 PR0 split its target. Fix the
113
- citation, then **delete the entry** — a test asserts every entry still names a
114
- real citation, so the list cannot quietly accumulate dead weight.
110
+ `CONFIG.grandfathered` in `scripts/check-citations.js` is the escape hatch for
111
+ citations already stale when a gate change lands, so the gate can block from day
112
+ one instead of shipping advisory. **It is empty, and should stay that way.** Fix
113
+ the citation, then **delete the entry** — a test asserts every entry still names
114
+ a real citation, so the list cannot quietly accumulate dead weight.
115
+
116
+ The original eight were burned down in one pass. **Four were born stale** — wrong
117
+ in the commit that wrote them, not rotted by later drift: three reused a line
118
+ number from a split that had already landed (one from a split made in that very
119
+ commit), and one wrote a symbol name with `.js` appended as if it were a path.
120
+ The other four were ordinary rot from the PR0/PR5c splits. A gate that catches
121
+ only decay would never have caught the first four — which is the case for
122
+ `file.js :: symbol` being the default form.