amicus 4.8.1 → 4.9.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.
Files changed (117) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/CHANGELOG.md +316 -0
  3. package/README.md +8 -3
  4. package/docs/ROADMAP.md +101 -10
  5. package/docs/configuration.md +54 -4
  6. package/docs/council.md +102 -14
  7. package/docs/troubleshooting.md +9 -2
  8. package/docs/usage.md +128 -12
  9. package/electron/ipc-setup.js +41 -18
  10. package/electron/main.js +19 -5
  11. package/electron/offer-session.js +51 -0
  12. package/electron/setup-ui.js +46 -39
  13. package/electron/workspace-ui/live-dead-seats.js +163 -91
  14. package/electron/workspace-ui/workspace-banners.js +30 -7
  15. package/electron/workspace-ui/workspace-matrix.js +23 -3
  16. package/electron/workspace-ui/workspace-seats.js +95 -79
  17. package/package.json +1 -1
  18. package/schemas/council-run.schema.json +2 -2
  19. package/schemas/council-tally.schema.json +17 -1
  20. package/schemas/council-verdict.schema.json +12 -4
  21. package/schemas/run.schema.json +6 -1
  22. package/skills/second-opinion/COUNCIL-DESIGN.md +1 -1
  23. package/skills/second-opinion/MANUAL-ORCHESTRATION.md +1 -1
  24. package/skills/second-opinion/MODEL-NOTES.md +88 -9
  25. package/skills/second-opinion/SEAT-BRIEFS.md +36 -4
  26. package/skills/second-opinion/SKILL.md +151 -36
  27. package/src/cli-council-run-bench.js +98 -6
  28. package/src/cli-handlers-council-run.js +18 -6
  29. package/src/cli-handlers-council.js +57 -7
  30. package/src/cli-handlers-doctor.js +1 -1
  31. package/src/cli.js +3 -1
  32. package/src/council/anonymize.js +2 -1
  33. package/src/council/briefings-chair-task.js +161 -0
  34. package/src/council/briefings-chair.js +33 -8
  35. package/src/council/briefings-debate.js +79 -13
  36. package/src/council/briefings-stage2-task.js +236 -0
  37. package/src/council/briefings-stage2.js +103 -26
  38. package/src/council/briefings-task.js +167 -0
  39. package/src/council/briefings.js +41 -4
  40. package/src/council/chair-fallback.js +95 -0
  41. package/src/council/debate.js +38 -21
  42. package/src/council/findings.js +3 -2
  43. package/src/council/ledger.js +2 -2
  44. package/src/council/parse-stage2.js +63 -15
  45. package/src/council/report-cost.js +61 -0
  46. package/src/council/report-html.js +26 -4
  47. package/src/council/report-md.js +30 -2
  48. package/src/council/report.js +40 -37
  49. package/src/council/run-assemble.js +21 -6
  50. package/src/council/run-chair.js +44 -95
  51. package/src/council/run-debate-revote.js +81 -49
  52. package/src/council/run-debate.js +51 -34
  53. package/src/council/run-finish.js +5 -3
  54. package/src/council/run-retry-keys.js +4 -4
  55. package/src/council/run-retry-launch.js +4 -4
  56. package/src/council/run-retry-notes.js +72 -15
  57. package/src/council/run-stage1-launch.js +4 -4
  58. package/src/council/run-stage1-rows.js +9 -6
  59. package/src/council/run-stage2.js +81 -47
  60. package/src/council/run-stages.js +9 -21
  61. package/src/council/run-stats-entry.js +46 -1
  62. package/src/council/run.js +28 -13
  63. package/src/council/seats.js +2 -2
  64. package/src/council/stage1-bind.js +3 -2
  65. package/src/council/verdict-seat-loss.js +124 -0
  66. package/src/council/verdict.js +108 -99
  67. package/src/headless.js +256 -49
  68. package/src/mcp-council-bench.js +64 -3
  69. package/src/mcp-council-run.js +10 -3
  70. package/src/mcp-server.js +52 -12
  71. package/src/mcp-tools.js +41 -5
  72. package/src/observe/council-legs.js +2 -2
  73. package/src/opencode-client.js +19 -1
  74. package/src/pack/pack-forward.js +15 -12
  75. package/src/pack/pack-resolve.js +1 -1
  76. package/src/prompt-builder.js +17 -1
  77. package/src/sidecar/fanout-leg-fallback.js +2 -1
  78. package/src/sidecar/fanout-leg.js +26 -0
  79. package/src/sidecar/fanout.js +1 -1
  80. package/src/sidecar/list-council.js +178 -0
  81. package/src/sidecar/list-limit.js +3 -1
  82. package/src/sidecar/list-search.js +2 -1
  83. package/src/sidecar/models-render.js +71 -0
  84. package/src/sidecar/models.js +19 -45
  85. package/src/sidecar/read.js +34 -10
  86. package/src/sidecar/reopen-spend.js +2 -1
  87. package/src/sidecar/setup.js +13 -4
  88. package/src/sidecar/start.js +2 -1
  89. package/src/template/render.js +16 -7
  90. package/src/utils/alias-audit.js +10 -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 +19 -9
  95. package/src/utils/degrade.js +12 -5
  96. package/src/utils/doctor-alias-check.js +2 -5
  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 +21 -3
  103. package/src/utils/model-canonicalization.js +55 -6
  104. package/src/utils/model-catalog.js +27 -9
  105. package/src/utils/model-fetcher.js +69 -16
  106. package/src/utils/model-shortlist.js +5 -2
  107. package/src/utils/provider-default-picker.js +6 -3
  108. package/src/utils/quick-picks.js +45 -7
  109. package/src/utils/result-schema.js +17 -1
  110. package/src/utils/text-sanitize.js +81 -0
  111. package/src/utils/ttft.js +57 -0
  112. package/src/utils/untrusted-fence.js +111 -1
  113. package/src/workspace/fold-format.js +28 -7
  114. package/src/workspace/live-normalize.js +2 -1
  115. package/src/workspace/matrix-model.js +6 -2
  116. package/src/workspace/run-detail.js +33 -7
  117. package/src/workspace/seat-space.js +10 -6
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "amicus",
3
- "version": "4.8.1",
3
+ "version": "4.9.1",
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,322 @@
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.1] - 2026-08-27
7
+
8
+ *A silent provider failure, and the unservable model ids it produced.*
9
+
10
+ A session on `deepseek` failed with `you passed deepseek-v4-flash-0731`. Tracing that one error
11
+ found a chain: a provider fetch failed silently, the empty namespace it left behind was
12
+ indistinguishable from "never fetched", and that ambiguity licensed the wizard to synthesise and
13
+ persist a direct model id nothing serves.
14
+
15
+ ### Fixed
16
+
17
+ - **Per-provider catalog fetch failures are reported instead of collapsing to `[]` (#209).**
18
+ `fetchAllModels()` returned a flat row array, so a provider whose fetch was REJECTED could not be
19
+ told apart from one that legitimately serves no models — all four failure modes (non-200, timeout,
20
+ network error, parse error) resolved to a bare `[]`, and the existing reporting fired only when
21
+ *every* provider returned nothing. A 401ing key silently zeroed that vendor's namespace and every
22
+ picker then offered gateway-only routes with no indication why. `fetchAllModelsDetailed()` now
23
+ carries `{rows, failures}`, the cache persists `providerFailures` alongside the rows they
24
+ describe, and `amicus models --check` prints
25
+ `PROVIDER FETCH FAILED: <provider> (HTTP 401)` (`--json` carries the array).
26
+ `fetchAllModels()` keeps its signature as a rows-only wrapper.
27
+ - **A rejected namespace no longer yields a fabricated direct model id (#208).**
28
+ `classifyModel` returns `'unknown'` for an empty namespace because it could not distinguish
29
+ "never fetched" from "fetch rejected", and that licensed `directFormIfSafe` to strip the
30
+ `openrouter/` prefix and produce an id the direct API does not serve. Optimism is now suppressed
31
+ when *that vendor's* namespace fetch was rejected, and preserved when it was simply never
32
+ attempted. Threaded through the picker, the shortlist, both Electron wizard entry points and the
33
+ CLI setup path — the picker rebuilds its own `catalogInfo`, so without threading the guard was
34
+ live in unit tests and dead in production.
35
+ - **The persistence path requires catalog evidence (#214).**
36
+ `toStorableRoute` — whose result is written straight into `config.aliases` and used to seed fresh
37
+ configs — decided direct-vs-gateway with no catalog evidence at all. It now routes through
38
+ `directFormIfSafe`, and `toLiveSeedAliases` stops discarding evidence it was already handed.
39
+ - **Alias drift is computed against the same evidence the writer uses.**
40
+ `findDriftedStoredAliases` resolved its "current" value without `providerFailures`, so a rejected
41
+ namespace produced false drift whose suggested repair would have written back the very id #208
42
+ removed.
43
+ - **Guards no longer depend on an optional argument.** `directFormIfSafe`/`directFormIfProven`
44
+ keyed both the `DIVERGENT_VENDORS` and namespace-rejection checks on a caller-supplied `vendor`
45
+ that the API marks optional, while the catalog check derived its own — so a caller using the
46
+ documented shape silently lost two of three guards. The vendor is now derived from the id when
47
+ omitted.
48
+ - **The setup wizard keeps direct-first routing when the catalog is unavailable.** With no catalog,
49
+ quick picks fell back to raw `openrouter/…` routes, which Amicus treats as an explicit
50
+ force-OpenRouter literal — an offline setup would have pinned the user to the gateway
51
+ permanently.
52
+ - **`pre-commit` no longer fails on a clone that has never stashed.** The hook gated its
53
+ lint-staged workaround on being inside a worktree; the real trigger is a missing `refs/stash`,
54
+ which is true of any fresh clone whose first commit precedes its first `git stash`.
55
+
56
+ ### Changed
57
+
58
+ - **`toCanonicalDefault` is renamed `stripGatewayPrefix`** (`src/utils/curated-models.js`). The old
59
+ name read as the correct answer and three separate callers took it at its word and persisted ids
60
+ the direct API may not serve. It cannot be made evidence-taking — it *produces* the candidate
61
+ `classifyModel` checks — so the name now says what it is. Callers deriving an id that will be
62
+ called or stored must use `directFormIfSafe`/`directFormIfProven`. Internal utility, not a
63
+ documented API surface, but importers reaching into `src/utils/` will need the new name.
64
+ - **Route canonicalisation for the setup wizard is decided in the main process.** The renderer
65
+ carried a hand-copy of `toCanonicalDefault` that had neither of the real primitive's guards; it is
66
+ deleted, and the safe form now ships to the page as data. Routing policy no longer reaches the
67
+ renderer at all.
68
+ - **`gatewayOf(id)`** replaces three verbatim copies of the gateway-classification one-liner.
69
+ - An ESLint rule now bans hand-rolled `openrouter/` prefix stripping outside an audited allowlist.
70
+ - CI council bench: `qwen` moves from `qwen3.8-max` to `qwen3.8-27b` — same 1M context, ~4.8x
71
+ cheaper input. Bench-only; the shipped alias table is unchanged.
72
+
73
+ ## [4.9.0] - 2026-08-26
74
+
75
+ *The council does new work.*
76
+
77
+ ### Added
78
+
79
+ - **Task mode — the council stops being able only to critique (#134, #130, #146).**
80
+ `amicus council run --intent task`, or `intent: 'task'` on the `amicus_council_run` MCP tool,
81
+ points the whole pipeline at open-ended work instead of at a review. Every seat **produces the
82
+ deliverable** the briefing asks for — `briefings-task.js :: TASK_SEAT_ROLE` says it in as many
83
+ words, *"you are not reviewing the briefing, you are executing it"* — the Stage-2 judges rank
84
+ **which response best does the work** and adjudicate whether each declared claim holds, and the
85
+ chair synthesizes an **ANSWER**, never a verdict.
86
+ The two terminal scales are disjoint by construction: a review chair closes on
87
+ `Ship it | Fix these first | Fundamental rethink`
88
+ (`briefings-chair.js :: CHAIR_VERDICT_VALUES`, `parse-stage2.js :: CHAIR_VERDICTS`), a task chair
89
+ on `Converged | Split | Insufficient` (`briefings-chair-task.js :: CHAIR_ANSWER_VALUES`,
90
+ `parse-stage2.js :: CHAIR_ANSWERS`). They **share no value and no keyword** — `VERDICT:` and
91
+ `ANSWER:` — which is what lets each parser stay blind to the other's line, and it is
92
+ `parseChairTerminal(text, intent)` that picks between them. Each scale is spelled **five** times —
93
+ those two constants, the scale addendum, the chair repair prompt, and
94
+ `schemas/council-verdict.schema.json`'s `overallVerdict` enum, which now carries the review scale
95
+ followed by the task scale — and all five are drift-pinned together
96
+ (`tests/council/chair-scale-drift.test.js`, named mutant `ANSWERSCALEDRIFT`).
97
+ - **`intent` is emit-when-`task`, everywhere.** `review` is the default spelled out loud: both
98
+ transports strip it, and `run.js :: runCouncil` — a public door too — **normalizes** it to
99
+ absent (`delete`, never `= undefined`) rather than refusing it, so one input cannot mean two
100
+ things at two doors. Only a genuinely unknown value is `BAD_ARGS`. Nothing ever writes
101
+ `intent: "review"` onto `run.json`, `tally.json` or `verdict.json`, so every pre-v4.9 document
102
+ is already spelled correctly and no migration exists to need.
103
+ - **The Stage-1 contract does not fork.** The JSON skeleton, the
104
+ `blocker | major | minor | nit` enum and the required-non-empty `location` rule are review
105
+ mode's, verbatim; `findings.js` — one validator, one repair loop — changes by **zero executable
106
+ lines** (the only edits it took in this release are comments: a refreshed line citation, and the
107
+ empty-`findings[]` ruling now naming the task briefings' claims wording beside the review one).
108
+ Only the frame and the field glosses differ: in task mode `location` is the grounding
109
+ discipline, naming *what the claim rests on — a source, a computation, or the literal word
110
+ "assumption"*.
111
+ - **Intent selects the parser on the Stage-5 rebuild too**, where the run is long over and only
112
+ documents remain. Three carriers, because no one of them covers every leg — the record's own
113
+ `meta.intent`, `run.json`'s checkpoint, and the run folder's prior `verdict.json` — combined as
114
+ a **disjunction** (absence is no vote, not a vote for review). The two *document* reads carry the
115
+ same `!runId || doc.runId === runId` guard their siblings do — waived only when the *record* names
116
+ no run and never when the *document* does not. Without that guard a stale or foreign document in
117
+ the folder hands the rebuild another run's intent, which selects the chair parser. An
118
+ `overallVerdict` carried off the **wrong** scale is not trusted: it fails
119
+ `scale.includes(...)` and falls through to re-parse `chair-output.md`.
120
+ - **The renderers say what the run actually produced.** The primary heading is `## Answer summary`
121
+ on a task run and the exact old `## Verdict summary` on a review run; the tier table carries
122
+ *"Tiers report peer concurrence, never verification."* directly under the counts a skimmer
123
+ reads — task runs only, forked in **both** renderers and pinned separately per renderer so
124
+ neither can regress silently (mutants `SUMMARYLABEL`, `QUALIFIERDROP`). The fold
125
+ (`workspace/fold-format.js`) and the Workspace chip
126
+ (`electron/workspace-ui/workspace-matrix.js`) label the terminal line `ANSWER:` or `VERDICT:`
127
+ off the same key, matching on `=== 'task'` so a pre-v4.9 payload with no `intent` reads as
128
+ review instead of relabelling every legacy run. The chair's own briefing carries the caveat the
129
+ tier table cannot: *"Peer agreement on a claim is CONCURRENCE, not verification — models
130
+ correlate on priors."*
131
+ - **Task runs write zero reliability rows**, at two gates — `run-finish.js` (the engine's own
132
+ append) and `cli-handlers-council.js :: runTally` (the hand-assembled path) — and `council tally`
133
+ now **refuses** a `meta.intent` that is neither spelling rather than letting a near-miss
134
+ (`'Task'`) slide into the ledger, whatever `--no-ledger` says. The skip is **announced and
135
+ non-degrading**: a `Note:` on the new `ledger-skipped` channel, `kind: 'info'` — a third kind
136
+ beside `degrade`/`heal` that the degrade sink is structurally unable to flip `degraded.value`
137
+ on. ⚠️ It is emitted where it is *load-bearing*, not on every task run: the chair-fallback
138
+ promotion arm, which draws on ledger history a task run never fed. That arm is reached at most
139
+ once per `runChair`, and `runChair` runs once per run, so "announced once" is structural rather
140
+ than a flag's promise. `kind: 'info'` records get their own **Notes:** list in the report and
141
+ are kept out of the `## What was lost` heading — an announcement is not a loss.
142
+ - **`amicus council stats` on an empty ledger explains itself.** A fresh install and a task-only
143
+ install used to render the same blank table; the surface now names where rows come from
144
+ (*"reliability history comes from review runs; task runs never write rows here"*) rather than
145
+ asserting which of the two states the reader is in — an earlier wording claimed the latter and
146
+ was false on every fresh install.
147
+ - **`--claude-review` is refused with `--intent task`** — a file review is review machinery and
148
+ has no task-mode meaning.
149
+ - **Review runs are byte-identical *under the intent fork*.** Not "unchanged as far as we know":
150
+ the review path composes through the same dispatcher and is pinned at key *and* byte level, and
151
+ both report `.snap` documents are untouched by this release. Scoped deliberately — task mode
152
+ changes nothing a review run emits; two *other* items in this release do, and say so where they
153
+ land (the repair-attempt `runStats` conformance value, and the `tally-input.json` debate-row key
154
+ order the builder unification moves into agreement with `tally.json`).
155
+ - Limitations, ruled and recorded rather than discovered later: **one intent per run** (no mixed
156
+ bench), intent is **not pack-settable** (nothing in `src/pack/` reads it), and a task run builds
157
+ no reliability history by design. #130's *detector* half — the `location` heuristic that would
158
+ emit a divergence **observation** into `degrades[]` — is deliberately not in this release; what
159
+ ships is the declaration that removes its root cause, since the hard-coded review frame every
160
+ seat used to receive was #130's mechanical cause and #134's blocker at once.
161
+
162
+ - **The engine speaks for itself (#133, pieces 2 and 3).** A `NO_OUTPUT_BACKSTOP` death report no
163
+ longer stops at what the deadline observed — it quotes **the engine's own newest ERROR line for
164
+ that session**, appended as ` — engine log: <excerpt>`. Bounded read, both log schemes,
165
+ boundary-anchored session matching (a line whose structural session field names someone else is
166
+ skipped even when our id appears elsewhere on it), and a **union across every candidate log
167
+ directory that exists**, not the first one — the dirs are alternative homes for the same engine's
168
+ logs, so a stale `$XDG_DATA_HOME` or an empty dir left by a previous install would otherwise
169
+ shadow the dir holding the answer. A matching line whose message part is empty keeps the walk
170
+ going instead of ending the file's scan, because the real failure and a terse message-less
171
+ teardown line are typically neighbours.
172
+ One lookup's scan is memoized per `fs` implementation for 10 s so a whole wave of seats shares it.
173
+ ⚠️ **That reuse serves hits only.** A **miss** re-lists and re-reads before it is believed, and
174
+ drops the live slot behind it, because the engine writes a leg's error *when that leg dies* —
175
+ possibly after the cached scan, possibly into a file it has only just rolled over to, so an
176
+ absence in a previous call's listing is not an absence on disk. The accepted residual runs the
177
+ other way and is stated rather than buried: a warm hit serves the newest line *as of when the slot
178
+ was built*, so the TTL bounds the missed window (≤10 s of new lines) and bounds nothing about the
179
+ **age** of what is served — a minutes-old genuine error for this leg can be quoted while a fatal
180
+ line written two seconds ago sits unread. Quoting a true older diagnostic is the trade; silence
181
+ while the cause sits on disk is the failure this module exists to end.
182
+ Where the answering server's version differs from the running install's engine, the report appends
183
+ ` (engine skew: server <a> ≠ installed <b>)` — tracked per server, refreshed on every session
184
+ create, with a remedy line that names the honest action and says why `doctor` cannot see this
185
+ class. Everything here is best-effort: every path returns `null` rather than throwing, because a
186
+ log read must never break a leg's death report. **#133 closes.**
187
+
188
+ - **TTFT probe — probe, never derive (R12).** `ttftMs` is the elapsed time to the first
189
+ **substantive** tick, measured off the no-output backstop's *own* predicate. The distinction is
190
+ load-bearing and the plan's prose named the wrong one: `progressed` includes the empty assistant
191
+ placeholder OpenCode mints on prompt **acceptance**, so keying on it would stamp a
192
+ time-to-first-token on a leg that never produced a token. The probe and the backstop tick read one
193
+ shared `substantiveActivity` constant, and mutant `PROGRESSEDTWIN` pins the single fixture where
194
+ the two predicates disagree. It rides result docs, leg patches and `runStats` rows emit-when-set,
195
+ behind one validity rule — `utils/ttft.js :: isMeasuredTtft`, imported by three of the four gates
196
+ and hand-spelled by the fourth, `council/run-stats-entry.js`, which is pinned require-free (the
197
+ two spellings are held in step structurally, and a fifth site cannot appear unnoticed) — so an
198
+ unmeasured value is **dropped at the emit gate, never clamped**, and a leg with no measurement is
199
+ byte-identical to a pre-v4.9 one. The per-model backstop *derivation* deliberately waits for observations to derive
200
+ from (#135/#129 record).
201
+
202
+ - **Alias-shadow warning (#135 C5 / #129's own side observation).** One notice per run when a
203
+ user-config alias **shadows** a curated alias with a different id — because everything keyed on
204
+ the alias *name* (per-model operating notes, a bench a workflow spells, a council preset) then
205
+ quietly describes a model the alias no longer resolves to, and by the time `resolveModel` sees an
206
+ id the two sources have already collapsed into one string. Compared in **canonical form**, so a
207
+ gateway spelling of the same id stays silent (mutant `GATEWAYFORM`). Diagnosis only: it resolves
208
+ nothing, changes no id, no exit code and no artifact — a local override winning is the documented
209
+ contract; this just says so out loud.
210
+ Wired at three measured sites, and the third exists because of a measured surface gap:
211
+ `cli-council-run-bench.js :: resolveBench` is the one bench-resolution helper **both** council
212
+ transports execute (bench, chair and critic all inspected there), but the council child's stderr
213
+ is not a pipe the MCP server reads — it is an fd on `<runDir>/debug.log` — so from that site alone
214
+ an MCP caller would have to open a log file to find the notice. `mcp-council-bench.js ::
215
+ auditBenchAliases` therefore writes it into the **tool result's notices array**, a different
216
+ surface rather than a second copy: the child still writes its line to `debug.log`, the parent
217
+ writes its own to the tool result, and no single surface shows it twice. The third site is
218
+ `amicus models --check`, the one call that passes no name list. Every third-party fragment the
219
+ notice quotes rides the house sanitizer (mutants `NOTICERAW`, `MESSAGERAW`, `THROWNRAW`), and the
220
+ stderr writer is armed against a closed pipe (`STREAMFATAL`, `STREAMDEAF`).
221
+
222
+ - **`amicus list` finally shows council runs.** `amicus_list` has merged them since v4.0 §8; the
223
+ CLI never did, so a council launched from the terminal was invisible **to the terminal** —
224
+ `amicus list` reported *"No amicus sessions found."* in a project whose only work was a council.
225
+ The same enumerator now feeds both surfaces through `src/sidecar/list-council.js`: the MODEL cell
226
+ carries the live stage as `council(<stage>)`, mirroring the wave row's `wave(N legs)`, width-capped
227
+ so it can never butt against the STATUS column; `--search` reaches council material on both
228
+ surfaces. **Scope is stated rather than silent:** only the current project's council runs are
229
+ merged, `--all` included — council runs are found through per-project pointer files and there is
230
+ no cross-project council index to walk — so the human listing prints that disclosure, naming no
231
+ remedy because there is no flag that widens it. A merge that fails now **says so, with the cause**
232
+ (sanitized), instead of dropping every council row into output indistinguishable from a project
233
+ that has none.
234
+
235
+ - **The Workspace dead-seat surface — an unbound seat stops being invisible (SI-02, R4).**
236
+ `deriveSeatLoss` and both Workspace consumers now admit the `seat-unbound` family behind **one
237
+ retry-family gate**, because `seat-unbound` is a shared channel — orphan-leg notes, re-vote
238
+ refusals and Stage-2 judge notes ride it too, and each is pinned *excluded* in every consumer.
239
+ The two LOSS reads spell that gate identically (`(retryWaveId || firstFailure) && (seatId ||
240
+ seat)`); the RETRIED read in `workspace-seats.js` is deliberately **narrower** — `retryWaveId`
241
+ alone, since a never-attempted seat carries a `firstFailure` and badging it *retried* would be
242
+ false. One loss rule, one narrowing, both pinned (mutants `SKIPRETRIED-A`/`-B`, one per direction
243
+ the widening could come back) rather than left to drift. The producer half was one line: the partial arm emits a seat id
244
+ beside the alias-valued seat, `null` for a slot it could not identify and never the alias.
245
+ The critic path keys on seat identity where the record and the run can both spell it, so a dead
246
+ bench twin beside a live critic twin renders one correctly-labelled row — it used to render
247
+ **nothing at all**, the silent 0-row erasure this project's product principle rates as severely as
248
+ a crash — and a both-dead pair renders two rows with one critic label. The known-wrong pins were
249
+ **flipped, not renamed**. The third street-cred renderer joins its seat-keyed siblings, and its
250
+ stated precondition was **measured false and struck rather than satisfied**: `labelOf` never
251
+ needed a seat id, and mutant `BLINDSEATLEAK` pins that the literally-planned spelling would have
252
+ leaked one into a blind render. The `seatTableRejected` banner now discloses the seats-vs-artifacts
253
+ document split in wording that was measured before it was written — the first draft's sentence
254
+ named the cost panel as a seat surface, which it is not, and would have shipped false.
255
+
256
+ ### Fixed
257
+
258
+ - **W1's nine small repairs**, one wave, disjoint files, each TDD'd or pinned: the second-opinion
259
+ Stage-4 Confirmed gloss gains the lone-corroborating-peer case in both presentation headings;
260
+ `fmtProbeLine`'s silent parenthetical stops asserting endpoint acceptance and says only
261
+ *"(no output within the probe window)"*; the setup wizard's embedded alias tables are seeded
262
+ null-prototype **on the far side of the parse**, sibling tables and the `__proto__` alias-name
263
+ chain included; `MAX_CATALOG_AGE_MS` retires three copies to one exported source; repair-attempt
264
+ `runStats` rows stamp the **measured** outcome (refined twice by councils — the flat literal
265
+ mislabeled successful repairs, and the ch4 chair push had silently defaulted `clean`);
266
+ `KNOWN_VARIABLES` is single-sourced so validation and rendering derive from one exported set,
267
+ drift-tested — which satisfies the T3-m2 hard gate that composition's `{{input}}` was waiting on;
268
+ the MCP test-client timer leaks behind the standing *"Jest did not exit"* warning class are closed
269
+ in three suites (six open handles → zero, measured); a bare-id `model_not_found` carries the
270
+ doctor-repair hint when the OpenRouter twin classifies valid; and the setup TOCTOU is closed
271
+ **structurally** by consuming the same catalog snapshot the offer was built from — re-ruled twice
272
+ into per-window, session-scoped, read-without-consume offer sessions.
273
+ - **The outbound fences neutralize both tag ends.** `utils/untrusted-fence.js ::
274
+ defangOutboundFenceTags` escapes open *and* close tags for the whole house fence vocabulary over
275
+ both outbound surfaces — the council briefing tail and the parent-conversation section — so a body
276
+ carrying the **sibling** surface's tags cannot smuggle them through the surface that does not
277
+ happen to emit them (mutants `CTXFENCEBREAKOUT`, `BRIEFFENCEBREAKOUT`). ⚠️ Stated as a disclosure,
278
+ not a caveat: an entity escape is a convention about how a reading model interprets bytes, not a
279
+ parser guarantee. This is defense in depth — it removes the literal tag so the escape stops being
280
+ free — and the load-bearing protection is still the preamble both fences carry.
281
+ - **Curated pins move to the generation the bench actually runs** — `kimi` → `kimi-k3`, `qwen` →
282
+ `qwen3.8-max`, `glm` → `glm-5.3`. Not cosmetic: the review reproduced the standing alias-shadow
283
+ line the stale `glm` pin would have printed on every CI council run. Every old-id hit in the suite
284
+ was classified; no live pin of the old generation remains.
285
+
286
+ ### Internal
287
+
288
+ - **The `runStats` builders unify (PR1F-2 closes).** Debate's `mk`, the re-vote `legRow` and the
289
+ Claude row fold into `buildRunStatsEntry`, so every non-primary row shares one key order, one
290
+ set of defaults, and one emit-when-set rule. (What the fold does **not** buy: propagation —
291
+ `mk` passes a synthetic five-field leg, so entry-param fields and future leg-sourced fields
292
+ still require widening `mk` itself; the widening is a filed candidate.) Done by measurement,
293
+ because every pre-existing pin proved
294
+ order-insensitive: 21 byte-order goldens were written **before** any fold (23 in the suite
295
+ now, after two post-fold additions), and mutant
296
+ `UNIFYDRIFT-mk` reddens exactly one test in the whole repo — the golden. A 137-invocation status
297
+ census showed the divergent `'unknown'` default **never fires and structurally cannot**, so it was
298
+ deleted as measured-dead rather than chosen between. ⚠️ The filing's own blast-radius claim was
299
+ **corrected** rather than inherited: `run.json` carries no `runStats`, and `tally.json`/
300
+ `verdict.json` were *already* in the entry's key order via tally's allowlist re-projection — the
301
+ fold moves `tally-input.json` **into** agreement with them.
302
+ - **SI-16's three over-length council functions split at measured seams**, in-file and byte-faithful
303
+ (`runStage2` → `bindStage2Seats`, `runDebate` → `runDefenseWave`, `runRevoteWave` →
304
+ `repairRevoteLeg`), each mutant's red set recorded and each revert SHA-verified.
305
+ - **`seatKey` consolidated to one exported definition** (SI-DUP disposition (b)): the two remaining
306
+ standalone copies of the seat-or-alias rule now import it, with the load-bearing hand-inlined
307
+ fallback and the two `keyOf` sibling forms deliberately untouched.
308
+ - **MCP tool parameters re-measured and pinned** (F-1): 58 documented plus 2 allowlisted of 60 keys,
309
+ with the per-key rulings recorded in the test rather than in a comment that can rot. Separately
310
+ (F-5), `routing.tier` / `tier_onboarded` are documented from measurement, including the silent
311
+ `balanced` coercion.
312
+
313
+ ### CI
314
+
315
+ - **Council backstop headroom.** `council-review.yml` sets `AMICUS_NO_OUTPUT_BACKSTOP_MS` to
316
+ `480000` on the council step, single-spelled and pinned by four workflow tests, with the measured
317
+ 4.8.1-cycle stall evidence recorded in the comment beside it. The broader retry-policy question is
318
+ filed as issue 202 rather than guessed at here.
319
+ - The CI bench moves with the pins above: `kimi` off the bench, `deepseek` seated, `gemini-pro`
320
+ chairs.
321
+
6
322
  ## [4.8.1] - 2026-08-25
7
323
 
8
324
  ### Fixed
package/README.md CHANGED
@@ -132,6 +132,7 @@ 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
 
@@ -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).
@@ -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.1",
458
+ "version": "4.9.1",
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/ROADMAP.md CHANGED
@@ -13,10 +13,10 @@ lives under **Backlog (tracked, not scheduled)** with everything else that is re
13
13
  Nothing about the content changed and no judgment about its value is implied; only its status. When
14
14
  an org buyer and the org to support them exist, it earns a number then.
15
15
 
16
- Amicus is at **v4.7.0** (tagged 2026-08-08). Each 4.x rev below leads with the benefit, not the
16
+ Amicus is at **v4.9.0** (2026-08-26). Each 4.x rev below leads with the benefit, not the
17
17
  plumbing.
18
18
 
19
- **Status:** v4.0 through **v4.7.0** have **shipped** — everything on this page is a record of what
19
+ **Status:** v4.0 through **v4.9.0** have **shipped** — everything on this page is a record of what
20
20
  landed, not a plan. Composition — the scope that
21
21
  carried the number v4.6 here until the degrade-announcement-invariant milestone took the v4.6.0
22
22
  release (2026-08-02) — is now an unscheduled candidate for the next rev, tabled in its own section
@@ -206,7 +206,8 @@ list. Two findings drove it:
206
206
  An earlier draft of this note said *"all three had already rotted"*; that universal is false and
207
207
  is corrected here. The other two HAD already rotted before T2.4 began (`report.js:79` and
208
208
  `ledger.js:24` are comments at `ed5c0c02`), so their drift is pre-existing, not this release's.
209
- The report fallback is now at `src/council/report.js :: toModel`'s `total`; all three claims
209
+ The report fallback is now at `src/council/report-cost.js :: buildCostModel`'s `total` (it was
210
+ `report.js :: toModel`'s until v4.9 W8 extracted the cost table); all three claims
210
211
  themselves still hold. So CA-4's omissions are not a schema nicety — they under-report spend on the
211
212
  surface the owner relies on, which collides with the cost-truth principle (*reported > estimated
212
213
  > unknown; never fabricate $0*). An omitted leg is not "unknown" — it renders as money never
@@ -222,9 +223,9 @@ list. Two findings drove it:
222
223
  (`run-assemble.js:180-184`) and one `repair` row per `-q<N>` solo, failed ones included
223
224
  (`run-stage2.js:122`).
224
225
  ⚠️ **Scope correction:** the failed-chair third of the original CA-4 is **closed** — v4.6.2's
225
- `chairAttempts[]` records every attempt on `run.json` (`run-chair.js:133` cites LC-5 by name), and
226
+ `chairAttempts[]` records every attempt on `run.json` (`run-chair.js:71` cites LC-5 by name), and
226
227
  failed-chair cost already reaches `runStats` too: a failed ch1–ch3 attempt gets its own
227
- `chair-attempt` row there carrying that leg's real `usage` (`run-chair.js:154-156`), so no third
228
+ `chair-attempt` row there carrying that leg's real `usage` (`run-chair.js:91-95`), so no third
228
229
  row class was needed.
229
230
  - **GOA-7 prerequisite — segment the ledger by RESOLVED model, not alias** *(S–M)*: **shipped.**
230
231
  `ledger.js:124` now keys on `row.resolvedModel || row.model` and `LEDGER_SCHEMA_VERSION` is 2.
@@ -256,7 +257,9 @@ list. Two findings drove it:
256
257
  > *Next-rev hard gates*: the tight-file extraction pass (`cli-handlers-council-run.js` is at
257
258
  > **299/300 exactly**, `run-debate.js` at 299, two files **at 300**), and KNOWN_VARIABLES
258
259
  > single-sourcing **only if** `{{input}}` is ever scoped — it is not in this rev, so that gate
259
- > travels with composition rather than blocking here.
260
+ > travels with composition rather than blocking here. *(Update 2026-08-25: the KNOWN_VARIABLES
261
+ > gate is now satisfied — landed in v4.9 W1 ahead of any composition work. The tight-file
262
+ > numbers in this note are a dated snapshot; re-measure with `npm run check:sizes`.)*
260
263
 
261
264
  ### Deferred out of v4.4.1 into v4.5 (2026-07-27)
262
265
 
@@ -276,7 +279,7 @@ a proposed disposition — v4.5 ride-along / v4.6 / backlog — tabled for rulin
276
279
  | **CA-4** | `tally.json`'s `runStats` omits Stage-2 judges, repair solos and failed chair attempts (5 rows for 11 real legs in `wsgate04`) | `M` — a schema question, not a fix |
277
280
  | **CA-5** | `isSubagentToolCall` is still a `name === 'task'` string proxy | `M`, and **reduced** by v4.4.0: it is now only the fallback when the real subtree walk finds nothing |
278
281
  | **LC-1** | B53's stall kill is skipped while a tool-settle deferral is active | `S–M` — shipped deliberately; the author wants a second opinion, which needs data from real runs |
279
- | ~~**LC-5**~~ | ~~A chair fallback leaves no trace in `run.json`~~ — ✅ **CLOSED by v4.6.2**: `chairAttempts[]` records every attempt (`{waveId, model, outcome, reason}`), checkpointed after each; `run-chair.js:113` cites LC-5 by name. **Do not re-file.** | — |
282
+ | ~~**LC-5**~~ | ~~A chair fallback leaves no trace in `run.json`~~ — ✅ **CLOSED by v4.6.2**: `chairAttempts[]` records every attempt (`{waveId, model, outcome, reason}`), checkpointed after each; `run-chair.js@v4.6.2:113` cites LC-5 by name. **Do not re-file.** | — |
280
283
  | **RN-1** | `sanitizeName` collisions surface as a banner rather than a refusal | `S` + a product decision that was already argued once |
281
284
  | **RN-2** | `renderRunList` blind masking is best-effort — only the open run resolves labels | `M` |
282
285
  | **RN-5** | A blind-mode flip closes every open prose panel and repaints twice | `S–M` |
@@ -354,6 +357,93 @@ behaviour changes.
354
357
  CI off 1.2.20. First release whose suite ran against the engine users actually get
355
358
  - Plus a `sidecar/reopen-spend.js` extraction, a dead-code deletion, and three documentation gates
356
359
 
360
+ ## v4.8 — "Every seat counts as itself" *(seat identity)* — ✅ SHIPPED v4.8.0, 2026-08-23
361
+ **Benefit:** seat a model twice and the council finally treats the two seats as two reviewers —
362
+ each with its own vote, its own row, its own file and its own dead-seat badge. Before this rev a
363
+ repeated alias was a bench that quietly disagreed with itself about how many reviewers were in the
364
+ room.
365
+
366
+ **Scope note.** The design spec behind this number
367
+ (`docs/superpowers/specs/2026-08-10-v4.8-ask-anything-count-everyone-design.md`) carried two halves,
368
+ *ask anything* and *count everyone*. v4.8.0 shipped **count everyone** — the seat-identity spine —
369
+ across the `v48-*` PR train; **ask anything** (task mode, #134/#130, with #146 folded in) was sized
370
+ and moved whole to v4.9 rather than carried half-done. That is the ruling, not a slip.
371
+
372
+ - **Seats are first-class.** A seat id *is* its alias on every bench with no repeated `--models`
373
+ entry; where an alias occupies more than one position the seats are `<alias>#1`, `<alias>#2`, and
374
+ the artifacts follow (`review-<alias>-1.md`, and the same rule for `judge-`/`rebuttal-`/`revote-`).
375
+ `meta.seats` rides the tally/verdict documents index-parallel with `meta.models` *(L)*
376
+ - **⚠️ The peers-only filter excludes the raiser by SEAT, and findings on a repeated-alias bench
377
+ change tier in BOTH directions** — a genuine twin's corroboration is no longer discarded, and a
378
+ twin's *dispute* now demotes. Deliberate, measured case-by-case, and disclosed with its permanent
379
+ cost: `Disputed` feeds the append-only ledger's `factErrorRate`, which is never migrated.
380
+ Distinct-alias benches are byte-for-byte unaffected *(M)*
381
+ - **Two new honesty marks on `tally.json`/`verdict.json`** — `findings[].sameModelCorroboration`
382
+ (corroboration that came from another seat of the same model, so it is not independent) and
383
+ `findings[].unattributedPeerDrops` (a count of votes excluded from `basis` that the engine could
384
+ not attribute to anyone). Both emit-when-set; both shipped with their own wrong-in-two-directions
385
+ disclosures rather than as clean wins *(M)*
386
+ - **Prototype pollution closed across the alias tables** — a member literally named `toString`,
387
+ `constructor`, `valueOf` or `hasOwnProperty` is no longer a valid alias at any of five gates, and
388
+ `resolveModel('toString')` throws instead of returning the function itself. One table was not
389
+ enough: a spread into a plain `{}` re-creates the inherited prototype, so all three builders are
390
+ seeded *(M)*
391
+ - **A finding with no named raiser stops corroborating itself**, on one principle applied in order —
392
+ *attribute when you can, mark only when you cannot* — with seat ids deciding first *(M)*
393
+ - **The Workspace stops collapsing dead seats**, and a live seat no longer erases its dead twin;
394
+ dead rows, retry badges and DOM keys are keyed on the seat, with the producer emitting `null`
395
+ rather than the alias for a seat it could not identify. Residuals pinned by tests asserting the
396
+ known-wrong behaviour so they cannot rot silently *(M)*
397
+ - **`streetCred[]` stops dropping or inventing rows** when a hand-assembled `meta.seats` disagrees
398
+ with `meta.models`, and a mixed reliability-ledger pair group stops reading narrower than one with
399
+ no seat information at all *(S–M)*
400
+ > Why here: seat identity is a prerequisite, not a feature. Every surface that says *which model
401
+ > said what* — the peer split, street-cred, the ledger join, the Workspace panels, the artifact
402
+ > filenames — was keyed on the alias, so all of them told the same lie on the same bench shape.
403
+ > Fixing them one at a time would have been six half-fixes; the spine makes all six the same fix.
404
+ >
405
+ > **Lineage.** v4.6 made a loss announce itself, v4.7 made the accounting match reality, v4.8 makes
406
+ > the *attribution* match reality. Same invariant family, applied to identity.
407
+
408
+ **v4.8.1 (shipped 2026-08-25):** the fast-follow patch — setup Step 2 offered one card per curated
409
+ model *family* with no way to choose within it, and the route pill it wrote stored a **provider** id
410
+ rather than a model id, so nothing downstream could tell two models of one family apart either
411
+ (#138). Both wizard surfaces now offer a vendor-scoped drill-down on a new pure `model-shortlist.js`.
412
+
413
+ ## v4.9 — "The council does new work" *(task mode)* — ✅ SHIPPED v4.9.0, 2026-08-26
414
+ **Benefit:** the council stops being able only to critique. Point it at open-ended work with
415
+ `--intent task` and every seat *produces* the deliverable, the judges rank which response best does
416
+ the work, and the chair synthesizes an **answer** — `Converged | Split | Insufficient` — instead of
417
+ a verdict about a review that never happened.
418
+
419
+ - **★ Task mode** — the *ask anything* half deferred out of v4.8, shipped whole: intent plumbing,
420
+ Stage-1 task frames at every dispatch site, task judging and the task chair, honest renderers on
421
+ every surface, and zero reliability rows written by a task run. **Closes #134, #130 and #146** —
422
+ `--intent task` on the CLI, `intent: 'task'` over MCP, and a review run that is byte-identical
423
+ everywhere *(L)*
424
+ - **The engine speaks for itself** — a `NO_OUTPUT_BACKSTOP` death report now quotes the engine's own
425
+ newest ERROR line for that session, and names a server-vs-install engine skew when there is one,
426
+ with a remedy that says why `doctor` cannot see this class. **Closes #133** *(M)*
427
+ - **Bench signals** — the `ttftMs` probe (measured off the backstop's own substantive-activity
428
+ predicate, never derived), and a one-per-run warning when a local alias shadows a curated one with
429
+ a different id, surfaced on the CLI, over MCP and in `models --check` *(M)*
430
+ - **The dead-seat surface finishes the v4.8 job** — an unbound seat stops being invisible in the
431
+ Workspace, and the critic path keys on seat identity, closing the dead-bench-twin-beside-live-critic
432
+ erasure v4.8 disclosed as a residual *(M)*
433
+ - **`amicus list` shows council runs on the CLI**, as `amicus_list` has over MCP since v4.0, with
434
+ the current-project-only scope stated out loud rather than left silent *(S–M)*
435
+ - **Docs update** — task mode in `README.md`, `docs/council.md` and `docs/usage.md`; the `runStats`
436
+ builder unification, the SI-16 splits and the `seatKey` consolidation carry the internal half *(S)*
437
+ > Why here: #130 and #134 are the same problem from two directions — #130 is the bug report of what
438
+ > happens when a generative brief meets a review-shaped pipeline, #134 is the request to support
439
+ > generative briefs properly — and both trace to one hard-coded frame telling every seat it was a
440
+ > reviewer. One declaration serves both, which is why they were designed together and shipped
441
+ > together rather than as a detector and a feature.
442
+ >
443
+ > **Lineage.** v4.6 through v4.8 each made the council *more honest about a run it already knew how
444
+ > to do* — announcing losses, counting money, attributing seats. v4.9 changes what a council can be
445
+ > asked for in the first place, which is a different kind of rev and is scoped as one.
446
+
357
447
  ## Backlog (tracked, not scheduled)
358
448
 
359
449
  ### Enterprise-readiness *(unscheduled — gated on funding / cofounder)*
@@ -388,9 +478,10 @@ consumer today (the Council Review GitHub Action) runs a single review per PR, n
388
478
  agent driving it, or the `critique`/`refine` built-ins are wanted on their own — those are a much
389
479
  smaller slice than the chaining machinery and could ship independently of F6.
390
480
 
391
- ⚠️ **Carries its own hard gate:** KNOWN_VARIABLES single-sourcing (`src/template/render.js:45` keeps
392
- two hand-maintained copies of the known-variable set) must land **before** `{{input}}` does. See
393
- `BACKLOG.md` *Next-rev hard gates* that gate travels with this item, not with v4.7.
481
+ ⚠️ **Its hard gate is already satisfied:** KNOWN_VARIABLES single-sourcing landed in v4.9 W1
482
+ (2026-08-25) `src/template/render.js` now derives both validation and rendering from
483
+ `KNOWN_VARIABLES`, drift-tested, so `{{input}}` no longer waits on it. See `BACKLOG.md`
484
+ *Next-rev hard gates* (ticked).
394
485
 
395
486
  ### GUI power ergonomics (F10) *(unscheduled — dropped from v4.7, 2026-08-05)*
396
487
  Focus-follows-fold hotkey, distinguishable window titles, tiling presets *(S each)*.
@@ -78,6 +78,52 @@ not on every launch, and not when you explicitly chose the gateway with `--gatew
78
78
  `routing.prefer: "openrouter"` (or pass `--gateway openrouter` per call) to keep routing everything
79
79
  through OpenRouter as before.
80
80
 
81
+ ### Cost tier
82
+
83
+ `routing.tier` is your standing answer to "when a vendor offers several models, which end of its
84
+ range do you want by default?" It lives under `routing` for filing reasons only — **it does not
85
+ change how a call is routed**, and it is not a gateway knob. Its one and only effect is which row
86
+ the **cost-aware default picker** preselects (`src/utils/provider-default-picker.js`).
87
+
88
+ | Tier | Means | Example (Anthropic) |
89
+ |---|---|---|
90
+ | `"frontier"` | the most capable / most expensive of the vendor's line | `claude-opus-*` |
91
+ | `"balanced"` | the middle of the range — **the default** | `claude-sonnet-*` |
92
+ | `"economy"` | the cheapest of the line | `claude-haiku-*` |
93
+
94
+ Absent, misspelled or set to anything outside those three, the tier **coerces silently to
95
+ `"balanced"`** — a junk value never errors and never blocks a launch. Per-vendor tier resolution
96
+ lives in `src/utils/model-tiers.js`; when a vendor's catalog has no model matching the tier, the
97
+ picker falls back to the cheapest priced row, then the first row.
98
+
99
+ **`routing.tier` is hand-edited only.** No wizard step, CLI command or GUI control writes it — set
100
+ it yourself in `~/.config/amicus/config.json` (the same rule as [`maxCostPerMtok`](#cost-gate)
101
+ below):
102
+
103
+ ```jsonc
104
+ { "routing": { "prefer": "direct", "tier": "economy" } }
105
+ ```
106
+
107
+ **Where the picker actually runs.** It offers you that vendor's models, priced, with the tier's pick
108
+ flagged as recommended, and writes your choice to `aliases.<vendor>` (seeding `config.default` when
109
+ that is still unset). Three surfaces reach it:
110
+
111
+ - `amicus key <provider> <key>` — after a successful **cloud**-vendor key save (local-provider
112
+ bearer saves skip it).
113
+ - `amicus setup` — the readline wizard runs it once per keyed provider, in detection order.
114
+ - The **Electron setup window** — the same picker, with a family → model drill-down.
115
+
116
+ Non-interactively (`--json`, `--quiet`, or no TTY) the picker takes the recommended pick silently and
117
+ prints a one-line summary instead of prompting. It is also a graceful no-op for `openrouter`, which
118
+ is a gateway rather than a model vendor.
119
+
120
+ **`routing.tier_onboarded`** is bookkeeping, not a setting: a boolean written automatically the
121
+ first time `amicus start` prints the one-time tip pointing existing users at the picker
122
+ (`src/utils/start-helpers.js`). The tip only fires on an interactive run that already has a direct
123
+ provider key and has not used the picker yet, and the flag is set only when the line actually
124
+ printed — so a `--json` run never burns it. Don't hand-edit it; delete it if you want the tip once
125
+ more.
126
+
81
127
  ---
82
128
 
83
129
  ## Behavior
@@ -343,12 +389,16 @@ level includes everything above it.
343
389
  },
344
390
 
345
391
  // Gateway routing policy (see Routing above). `prefer` defaults to "direct"
346
- // when this key is absent entirely. `migration_notified` is written
347
- // automatically the first time the one-time direct-migration notice fires
348
- // for a vendordon't hand-edit it.
392
+ // when this key is absent entirely. `tier` is the cost-tier preference the
393
+ // model picker preselects on ("frontier" | "balanced" | "economy"; anything
394
+ // else coerces to "balanced") — hand-edited only, see Cost tier above.
395
+ // `migration_notified` and `tier_onboarded` are both written automatically
396
+ // when their one-time notices fire — don't hand-edit either.
349
397
  "routing": {
350
398
  "prefer": "direct",
351
- "migration_notified": { "openai": true }
399
+ "tier": "balanced",
400
+ "migration_notified": { "openai": true },
401
+ "tier_onboarded": true
352
402
  },
353
403
 
354
404
  // User-defined local / OpenAI-compatible providers (v4.2) — written by