amicus 4.6.3 → 4.7.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 (99) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/CHANGELOG.md +349 -0
  3. package/README.md +19 -7
  4. package/bin/amicus.js +31 -0
  5. package/docs/ROADMAP.md +143 -36
  6. package/docs/configuration.md +52 -4
  7. package/docs/council.md +65 -12
  8. package/docs/doc-system.md +8 -7
  9. package/docs/schemas.md +10 -1
  10. package/docs/testing.md +1 -1
  11. package/docs/troubleshooting.md +33 -5
  12. package/docs/usage.md +64 -14
  13. package/electron/workspace-ui/index.html +3 -0
  14. package/electron/workspace-ui/live-model.js +52 -14
  15. package/electron/workspace-ui/workspace-app.js +14 -3
  16. package/electron/workspace-ui/workspace-lazy.js +233 -0
  17. package/electron/workspace-ui/workspace-matrix.js +12 -1
  18. package/electron/workspace-ui/workspace-panels.js +24 -171
  19. package/electron/workspace-ui/workspace-render.js +6 -2
  20. package/electron/workspace-ui/workspace-seats.js +68 -0
  21. package/electron/workspace-ui/workspace.css +6 -0
  22. package/package.json +8 -4
  23. package/schemas/council-run.schema.json +1 -0
  24. package/schemas/council-stats.schema.json +9 -1
  25. package/schemas/run.schema.json +2 -1
  26. package/schemas/spend.schema.json +1 -1
  27. package/schemas/wave.schema.json +2 -1
  28. package/scripts/postinstall.js +6 -3
  29. package/scripts/setup-hooks.js +49 -3
  30. package/skills/second-opinion/MANUAL-ORCHESTRATION.md +12 -0
  31. package/skills/second-opinion/MODEL-NOTES.md +5 -4
  32. package/skills/sidecar/SKILL.md +9 -2
  33. package/src/cli-council-run-bench.js +86 -0
  34. package/src/cli-handlers-council-run.js +65 -81
  35. package/src/cli-handlers-council.js +17 -5
  36. package/src/cli-handlers-fanout.js +179 -0
  37. package/src/cli-handlers-pack.js +24 -10
  38. package/src/cli-handlers-resume-continue.js +20 -0
  39. package/src/cli-handlers-run.js +19 -161
  40. package/src/cli-template-args.js +48 -0
  41. package/src/cli.js +39 -46
  42. package/src/council/debate.js +89 -10
  43. package/src/council/ledger.js +72 -11
  44. package/src/council/report.js +17 -6
  45. package/src/council/run-assemble.js +15 -3
  46. package/src/council/run-budget.js +2 -2
  47. package/src/council/run-chair.js +61 -5
  48. package/src/council/run-debate.js +51 -67
  49. package/src/council/run-launch.js +20 -2
  50. package/src/council/run-retry.js +17 -2
  51. package/src/council/run-stage1-launch.js +94 -0
  52. package/src/council/run-stage2.js +25 -4
  53. package/src/council/run-stages.js +79 -86
  54. package/src/council/run-state.js +10 -2
  55. package/src/council/run.js +26 -2
  56. package/src/council/tally.js +6 -2
  57. package/src/headless.js +69 -6
  58. package/src/mcp-council-awareness.js +1 -0
  59. package/src/mcp-council-bench.js +4 -0
  60. package/src/mcp-council-run.js +10 -0
  61. package/src/mcp-server.js +114 -54
  62. package/src/mcp-tools.js +12 -5
  63. package/src/pack/pack-cli.js +1 -1
  64. package/src/pack/pack-forward.js +12 -4
  65. package/src/pack/pack-resolve.js +3 -0
  66. package/src/pack/pack-store.js +20 -3
  67. package/src/pack/pack-validate.js +5 -1
  68. package/src/sidecar/budget.js +38 -4
  69. package/src/sidecar/continue.js +8 -23
  70. package/src/sidecar/fanout-budget.js +1 -2
  71. package/src/sidecar/fanout-leg-fallback.js +7 -3
  72. package/src/sidecar/fanout-retry.js +15 -3
  73. package/src/sidecar/fanout-wave-io.js +13 -1
  74. package/src/sidecar/fanout.js +11 -9
  75. package/src/sidecar/list-limit.js +50 -0
  76. package/src/sidecar/list-search.js +69 -0
  77. package/src/sidecar/read.js +90 -5
  78. package/src/sidecar/reopen-spend.js +32 -0
  79. package/src/sidecar/resume.js +1 -1
  80. package/src/sidecar/start-metadata.js +58 -0
  81. package/src/sidecar/start.js +8 -43
  82. package/src/sidecar/workspace-auto-open.js +2 -2
  83. package/src/spend-query.js +2 -1
  84. package/src/template/apply.js +7 -4
  85. package/src/template/render.js +6 -2
  86. package/src/template/store.js +1 -1
  87. package/src/utils/cli-preflight.js +27 -1
  88. package/src/utils/config.js +15 -0
  89. package/src/utils/doctor-engine-check.js +32 -0
  90. package/src/utils/engine-install-scan.js +98 -15
  91. package/src/utils/engine-repair.js +96 -2
  92. package/src/utils/remediation-hints.js +29 -0
  93. package/src/utils/result-schema-rebuild.js +1 -0
  94. package/src/utils/result-schema.js +6 -1
  95. package/src/utils/session-index-tmp-sweep.js +18 -3
  96. package/src/utils/session-index.js +1 -0
  97. package/src/utils/session-metadata-tmp-sweep.js +24 -4
  98. package/src/utils/spend-ledger.js +11 -4
  99. package/src/utils/validators.js +16 -0
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "amicus",
3
- "version": "4.6.3",
3
+ "version": "4.7.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
@@ -5,6 +5,355 @@ All notable changes to Amicus are documented here. Format follows
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [4.7.1] - 2026-08-09
9
+
10
+ ### Changed
11
+
12
+ - **`continue`, `resume`, and `--retry-failed` now inherit the parent session's/wave's tag,
13
+ instead of dropping it.** `amicus list` now shows a TAG for continuations (they no longer group
14
+ under `(unattributed)` for `--group-by tag`), and `--tag` is now **rejected** on `continue` and
15
+ `resume` — the tag can only come from the parent, it cannot be overridden. This deletes a
16
+ documented limitation, not a bug: `docs/usage.md` carried a paragraph headed "Known limitation: a
17
+ tag is not inherited", and v4.7.0's own release notes called the gap "future work, not
18
+ oversights." That limitation is now gone.
19
+ - **`opencode-ai` and `@opencode-ai/sdk` are pinned to exactly `1.18.15`** (previously `^1.2.20` /
20
+ `^1.1.36`). Dev and CI now run on the same engine version end users resolve, so this is the
21
+ first release whose test suite actually ran against the engine users get. Honest scope: the pin
22
+ makes the *resolved* engine a pure function of the amicus version — it does nothing for a user
23
+ whose installed amicus version hasn't moved, and it does not force any existing npx cache to
24
+ re-resolve (that mechanism was investigated for #133 and found not to exist).
25
+
26
+ ### Fixed
27
+
28
+ - **The no-output backstop message now reports what it observed, not what it guessed.** The old
29
+ three-line message asserted "likely a listed-but-not-serving model or a dead endpoint" with no
30
+ evidence behind it; it now names only what the deadline mechanism actually saw, and names
31
+ `AMICUS_NO_OUTPUT_BACKSTOP_MS` as the live governing window only when it is one, calling it out
32
+ as an overridden default otherwise.
33
+ - **The no-output backstop window now doubles on a Stage-1 retry, clamped to the leg timeout, so
34
+ retries can now heal a class of no-output failure that previously required a manual rerun.**
35
+ This is not a fix for the backstop class itself — only Stage-1 bench/critic/lens units retry;
36
+ judges, chair, chair-repair, and debate legs do not, and can still hit the backstop with no
37
+ automatic recovery.
38
+ - **`doctor` now reports engine version skew between installs instead of only grading on
39
+ presence.** A stale npx-cached copy of `opencode-ai` next to a newer global install now surfaces
40
+ as a WARN.
41
+ - **`npm root -g` now resolves on Windows** (it previously threw `ENOENT`/`EINVAL`, making the
42
+ global amicus install invisible to `doctor`). This also un-blinds `doctor --fix`'s donor
43
+ selection, which depends on seeing that global install to pick a healthy engine to copy from.
44
+ - **`npm test` no longer fails intermittently with `ENOENT: … src\__sizecheck_tmp__.js`.**
45
+ `tests/scripts/check-file-sizes.test.js` writes a real `src/__sizecheck_tmp__.js` and unlinks it a
46
+ few milliseconds later. It cannot move that fixture to a tmpdir: `checkAllTracked()` filters its
47
+ input through the anchored `src/**/*.js` include glob and only then resolves against
48
+ `process.cwd()`, so a tmpdir path is dropped before the read and the test would assert on an empty
49
+ violation list. Three suites walk `src/` in parallel jest workers, and a directory listing taken
50
+ while that file existed followed by a read issued after the unlink threw ENOENT — killing
51
+ `no-phantom-dependencies.test.js` during collection ("Test suite failed to run") on roughly 1 full
52
+ run in 2, with `cli-template-args.test.js` and `council/degrade-invariant.test.js` exposed
53
+ identically (both confirmed failing under a reproduction that widens the collision window). All
54
+ three now read through the new `tests/helpers/read-if-present.js`, which skips a file that vanished
55
+ mid-walk and still throws on any non-ENOENT failure, so an unreadable source file stays loud.
56
+ Pre-existing — reproduces at 0fe6128, unrelated to the graphify integration.
57
+
58
+ ### Internal
59
+
60
+ - **`finalizeSpendForReopen` extracted to `src/sidecar/reopen-spend.js`**, out of
61
+ `sidecar/continue.js`, to keep that file under the 300-line gate; `resume.js` was already
62
+ reaching across into `continue.js` for it, so the shared home is the honest one.
63
+ - **Deleted three unreachable helpers from `scripts/validate-docs.js`** (dead code with no call
64
+ site remaining after earlier `--check` work landed).
65
+ - **Three doc/marker-freshness gates, all jest-enforced — one genuinely new, two hardened.**
66
+ `tests/docs-command-coverage.test.js` and the anchor-link gate
67
+ (`tests/docs-council-toc-anchors.test.js`) both already existed and already ran in CI; this
68
+ branch hardens them (the former now derives its command list from `bin/amicus.js`'s switch
69
+ instead of hardcoding five entries, the latter generalizes from one file to sixteen). Only the
70
+ CLAUDE.md AUTO-marker/cross-link freshness gate in this changelog entry's own commit is genuinely
71
+ new — `generate-docs --check` had never run in CI before — so a stale `CLAUDE.md` can no longer
72
+ pass CI silently.
73
+
74
+ ## [4.7.0] - 2026-08-08
75
+
76
+ **"The count is the count."** Every number Amicus shows you is the number — what a council cost,
77
+ which legs ran, and which model earned the credit. Shipped across ten `v4.7-*` PRs — PR0
78
+ (extractions) through PR7, plus the PR3 riders and a closing documentation pass: the `runStats`
79
+ completeness half of CA-4, the GOA-7 ledger prerequisite, F8 session/wave tagging with `--search`,
80
+ and four correction sweeps.
81
+
82
+ ### CI
83
+
84
+ - **The macOS/node-24 unit leg's `--workerIdleMemoryLimit` drops from 1GB to 512MB.** That leg
85
+ segfaulted a jest worker again on 2026-08-07 (PR #126) — killing
86
+ `tests/utils/gateway-route-audit.test.js` while 6791 tests passed and 0 failed, with a green
87
+ rerun — which was the first such hit *with* the 1GB ceiling in force, so 1GB does not bound the
88
+ growth on this runner. Halving recycles a worker sooner, at a small wall-clock cost on a leg
89
+ already mid-pack (~3m59s vs ubuntu's ~3m58s). Three pins in
90
+ `tests/scripts/ci-workflow.test.js` now guard the mitigation, which previously had none: it
91
+ produces no assertion failure when it works and none when it is deleted, so nothing else in the
92
+ repo would notice its loss. If a fifth hit lands at 512MB, the next lever is `--maxWorkers` on
93
+ that leg — fewer concurrent heaps — not a smaller idle ceiling.
94
+
95
+ ### Added
96
+
97
+ - **`runStats` gains a row for every paid launch, not just one per requested seat** (v4.7 CA-4,
98
+ the row-per-launch design — closes the chair-cost accounting gap). Three new roles cover legs
99
+ that were billed but never rowed: `chair-attempt` (a failed ch1–ch3 chair launch), `repair`
100
+ (a Stage-1 `-p`, Stage-2 `-q`, chair-ch4, or debate-born `-d<N>r`/`-rv-…r` solo — a failed
101
+ defense or re-vote repair), and `superseded` (a first leg a later attempt replaced — an SL-2
102
+ retry or a debate repair); `wasChair` is always `false` on these. Every dead seat/critic/lens
103
+ with no recovery, and a chair walk that gives up entirely, now get an honest primary error row
104
+ too, extending the #83 judge treatment to every seat.
105
+ - **`runStats[].waveId`** (emit-only-when-set): every row backed by a real billed leg now names
106
+ the exact wave/leg it came from; e.g. the synthetic `claude` row, a give-up chair's error row,
107
+ and a leg-less dead-seat/critic/lens primary error row (the two SL-2 retry note-classes that
108
+ never produced a real leg for the seat at all) carry none.
109
+ - **The run-cost bijection invariant suite** (`tests/council/run-cost-bijection.test.js`): every
110
+ terminal run now proves Σ(`runStats` legged-row usage) equals `run.json`'s `usage` block —
111
+ cost, and the reported/estimated/unpriced/subtreeUnknown leg counts — across clean, repair,
112
+ chair-walk-failure, debate-repair, retry-healed, and retry-failed scenarios.
113
+ - **`runStats[].resolvedModel`** (v4.7 GOA-7, emit-only-when-set): every row built from a served
114
+ leg now records the executable id that actually served (post-fallback-substitution), never the
115
+ alias; carried verbatim through `tally.json`/`verdict.json` and onto `council-ledger.jsonl` rows.
116
+ - **`council stats` rows gain `aliases[]`** (every alias observed for the group, most recent
117
+ first — `aliases[0]` is the launch-preferred name) **and a `legacy` mark** on groups whose rows
118
+ all lack `resolvedModel`; the human table sizes the model column to the longest key (16-char
119
+ floor) and marks `legacy` in the notes column beside `low-N`.
120
+ - **`--tag <t>` on `start`/`fanout`/`council run`** (CLI + MCP, v4.7 F8 D13): labels a session
121
+ for `list`/`--search`/`spend --group-by tag`. Reject-style validation
122
+ (`^[A-Za-z0-9_-]{1,64}$`) — an invalid tag fails fast rather than being silently truncated or
123
+ charset-stripped the way `sanitizeCouncilName` cleans, since a stored tag is a search key.
124
+ Stored absent-not-null on `metadata.json`/`wave.json`/`run.json` and every result doc; the MCP
125
+ shared-server's in-process start path stamps it too. `--tag` is rejected alongside
126
+ `--retry-failed` (`BAD_ARGS`).
127
+ Council sub-waves (Stage-1, critic/lens solos, Stage-2, chair, debate) all carry the run's tag
128
+ on their own wave metadata. Riders: `continue`/`resume` don't yet inherit the parent session's
129
+ tag (their rows group under `(unattributed)` for `--group-by tag`), and `--retry-failed` doesn't
130
+ yet inherit the original wave's tag either — both are future work, not oversights.
131
+ - **`amicus list --search <q>` / MCP `amicus_list {search}`** (F8 D15, errata E-PR3-5):
132
+ case-insensitive substring filter over `id`, `tag`, and briefing material. Fan-out wave rows
133
+ match against the full `briefing.md` text (falling back to the row's 200-char excerpt when
134
+ unreadable); council-run rows match `briefing.md` written at MCP launch, or the
135
+ post-`--- MATERIAL / BRIEFING ---` portion of `briefing-stage1.md` for CLI-launched runs; leg
136
+ rows (spawned by a wave) match `id`/`tag` only, so a wave's briefing never surfaces once per
137
+ leg it spawned. A bare `--search` with no value is a usage error.
138
+ - **`amicus spend --group-by tag`** (F8 D16): spend-ledger rows now carry `tag` (leg and solo
139
+ rows alike); untagged history groups under `(unattributed)`, matching every other dimension's
140
+ convention. `SPEND_LEDGER_SCHEMA_VERSION` stays at `1` — this is an additive field, not the kind
141
+ of change that forced the council-ledger's `LEDGER_SCHEMA_VERSION` 1 → 2 bump above, which
142
+ existed to segment history by resolved-model id, a different need.
143
+
144
+ ### Changed
145
+
146
+ - **Tally/report/GUI cost totals now include legs that used to be silently dropped** — repairs,
147
+ failed chair attempts, and superseded/replaced legs all get a row now. Totals read HIGHER than
148
+ v4.6.x for an identical run, intentionally: this is the fix for the two-numbers-disagree
149
+ symptom (`council tally`'s sum vs. `run.json`'s total quietly diverging by the omitted spend).
150
+ - **The council-ledger join is now an explicit allowlist, not a skip-set**: `seat`, `critic`,
151
+ `lens:*`, `chair`, `claude`, `council`, `redteam`. Fail-closed by design — consumers keying
152
+ `runStats` by model must now exclude every role outside that set, the v4.7 analogue of v4.6's
153
+ "must exclude `role: 'judge'`" rule: a custom/free-form `role` label (e.g. a skill-authored tag)
154
+ still renders in the tally/report artifact but no longer contributes role/wasChair/conformance
155
+ to `amicus council stats` reliability numbers.
156
+ - **The Workspace seats panel no longer renders the three new non-seat launch rows**
157
+ (`chair-attempt`, `repair`, `superseded`) — they still appear in the report/tally cost tables,
158
+ just not as a seat.
159
+ - **`LEDGER_SCHEMA_VERSION` 1 → 2** (v4.7 GOA-7). Legacy-read, no migration: rows without
160
+ `resolvedModel` (all pre-v4.7 history, plus leg-less rows whose resolution is unknowable)
161
+ aggregate under their alias; a group is marked `legacy` only when every row in it lacks
162
+ `resolvedModel`.
163
+ - **`council stats` groups reliability by resolved model id** (`resolvedModel || model`) instead
164
+ of by alias alone — history splits honestly at the bump; a retargeted alias's new rows start
165
+ a fresh `low-N` group.
166
+ - **Chair fallback promotion (`pickFallbackChair`) excludes candidates by their full name set**
167
+ (group key + `aliases[]`) and launches the group's most-recent alias (`aliases[0]`) — a bench
168
+ seat's resolved-keyed group can no longer be promoted as its own chair.
169
+ - **`amicus list` and the MCP `amicus_list` tool now share one enumeration**
170
+ (`src/sidecar/read.js`, F8 D14): MCP rows gain `type`/`parentWave`/`legCount`, CLI rows gain
171
+ `mode`, and both gain `tag`. The MCP `status` input relaxed from a 3-value enum
172
+ (`'all' | 'running' | 'complete'`) to a free string, so `error`/`aborted`/`crashed`/
173
+ `timed-out`/`idle-timeout` — always real statuses, previously rejected outright by MCP's schema
174
+ even though the CLI already accepted them — now filter correctly there too. The CLI table
175
+ gained a `TAG` column.
176
+ - Test/comment/docs sweep (v4.7 PR4, theme a): ~30 census nits dispositioned — no behavior
177
+ changes.
178
+ - **`--out`/`-o` no longer accepts a flag-shaped value as a filename.** `--out -x` and
179
+ `--out=-x` previously wrote a file literally named `-x` (the dash-leading value was accepted
180
+ at face value); both forms now fail fast with `BAD_ARGS` ("-o/--out cannot start with '-':
181
+ got '-x'") instead. `-o -x` was already rejected before this change, via a separate internal
182
+ check — it still fails with the same code and exit status, now carrying the more accurate
183
+ message above in place of "-o/--out requires a value".
184
+ - **`{{project}}` template variable is now path-resolved** (absolute, normalized via
185
+ `path.resolve`), matching `{{artifact_path}}`'s existing behavior — it previously rendered the
186
+ raw `--cwd`/`process.cwd()` string verbatim. All three `TEMPLATE_RENDER` error paths also gain a
187
+ real, followable `hint` instead of `hint: null`.
188
+ - **MCP-launched council runs now record `run.template` provenance**, closing the one remaining
189
+ gap after Wave 1's D1 forwarded it on fanout/start — `mcp-council-run.js`'s template path no
190
+ longer discards `promptMeta`, so `run.json` carries `{name,hash}` the same way the CLI path
191
+ already did.
192
+
193
+ ### Fixed
194
+
195
+ - **`amicus list --all` now actually lists every project.** The flag has been documented since
196
+ before v4.7 (`--all` / "Show all projects" in `amicus list`'s help text) but `listSidecars`
197
+ never read it, so it silently behaved exactly like a bare `amicus list`. It now enumerates
198
+ every project the global sessions-index knows about (an advisory navigation aid, not
199
+ authoritative — a stale entry pointing at a missing or unreadable project is skipped, not
200
+ surfaced as an error), deduped by canonical project identity.
201
+ - **`amicus list --search` was accepted and silently ignored.** The generic CLI arg parser took
202
+ any `--search <value>`, but nothing downstream read it, so the flag — whose sibling convention,
203
+ `amicus models --search`, was already a repo-wide pattern — quietly did nothing. It's now
204
+ implemented; see Added, above.
205
+ - **`amicus fanout --quiet` was accepted and silently ignored.** `quiet` is a repo-wide known
206
+ flag, so the command parsed and exited 0 — but `handleFanout` never forwarded it into
207
+ `runFanout`, which printed the launch banner and per-leg lines anyway. Same
208
+ accepted-but-ignored shape as `list --search` above.
209
+ - **`amicus pack save --version <semver>` was accepted and silently ignored — it wrote no pack at
210
+ all.** `version` is a global boolean flag, so `parseArgs` set `args.version = true`, stranded the
211
+ semver in positionals, and `bin/amicus.js` printed the amicus version banner *before* command
212
+ dispatch — `handlePack` never ran, and the command exited 0 having saved nothing.
213
+ `--version=2.0.0` failed identically. Meanwhile the handler read `args.version` for a value it
214
+ could never receive, and both the help text and `docs/usage.md` documented `--version <semver>`
215
+ as a real option. The pack's own version is now spelled **`--pack-version <semver>`** (honored on
216
+ both the flags and `--from-run` paths), and `pack save --version` fails fast with `BAD_ARGS`
217
+ naming the right spelling. Every other `--version` still prints the banner. Third instance of the
218
+ accepted-but-ignored shape, after `list --search` and `fanout --quiet` above.
219
+ - **A fanout whose server fails to start no longer drops its pack.** `errorWave` — the third
220
+ `buildWaveResult` call site — inherited a pre-seeded `tag` from `metadata.json` but not a
221
+ pre-seeded `pack`, so an MCP-spawned wave that died at server start persisted a `wave.json`
222
+ missing the pack it was launched with (and `amicus read --json` prefers `wave.json`). The two
223
+ other call sites already inherited both; this was the lone holdout.
224
+ - **The test suite no longer writes session directories outside its sandbox.** Several suites
225
+ passed the literal `'/tmp'` as a project cwd — on Windows that resolves to `C:\tmp`, a real
226
+ directory — so every run leaked real session dirs onto the developer's filesystem and into the
227
+ global sessions-index. They now sandbox under `os.tmpdir()`, pinned by
228
+ `tests/hermetic-tmp-guard.test.js`. This residue was what made `--all` an 8-second, 21k-row
229
+ dump on a developer machine.
230
+ - **`pack list` warnings now print to stderr, not stdout.** They previously shared stdout with
231
+ `pack list`'s data output — unlike `pack save`, which already used stderr for the same
232
+ "Warning:" text — so `pack list | grep` mixed diagnostics into data. `--json` output is
233
+ unaffected.
234
+ - **A council pack combining `critic` and `lenses` with a by-name (string) bench is now rejected
235
+ at validate-time (`PACK_INVALID`), matching the array-bench form.** It previously survived
236
+ run-mode validation and could later surface a mutual-exclusion error naming a flag the user
237
+ never typed. One consequence: a string-bench pack naming both `critic` and `lenses` — even
238
+ invoked with an explicit flag — now hard-fails `PACK_INVALID` instead of running; array-bench
239
+ packs already behaved this way, so this closes a bench-shape-dependent inconsistency rather
240
+ than introducing a new restriction.
241
+ - **The `{{var.}}` (empty variable key) template error now has a followable message.** It
242
+ previously told the user to run `--var =<value>` — a form the CLI's own `--var` parser rejects,
243
+ making the remedy unfollowable by construction; it now correctly explains that
244
+ `{{var.<key>}}` requires a key and lists the known variables.
245
+ - **`doctor`'s tmp-sweep checks no longer report a name-shaped directory as an unremovable
246
+ orphan.** A directory whose name happened to match the tmp-file pattern (e.g.
247
+ `.metadata.json.<pid>.<rand>.tmp`) was picked up by the orphan scan, then permanently failed to
248
+ unlink — parking the check at `warn` forever ("swept 0, N remaining (too fresh or
249
+ unremovable)") even after every real orphan was cleared. Directories are now excluded from
250
+ both sweeps (the metadata sweep, which never follows symlinks, also excludes
251
+ symlinks/sockets/FIFOs) before the scan ever returns them, so the check reports `ok` once
252
+ nothing real is left.
253
+
254
+ ### Added (follow-up)
255
+
256
+ - **`amicus list --limit <n>`**: show only the *n* newest rows (`0` = unlimited, and absent
257
+ behaves exactly as before). When the cap elides rows it says so, naming the real total; in
258
+ `--json` mode that notice goes to stderr so stdout stays a single parseable document. Caps
259
+ output only — `--all` still enumerates every project, since capping the walk would rank rows it
260
+ never saw.
261
+
262
+ ### CI
263
+
264
+ - **The MCP-Registry skip-check's version match is no longer a BRE with unescaped dots.**
265
+ `$VERSION` (a semver like `4.7.0`) was interpolated directly into the grep pattern, where an
266
+ unescaped `.` matches any character; the fail direction is fail-toward-skip (a false match
267
+ would silently drop a publish), so the dots are now backslash-escaped before matching. Not
268
+ exploitable in practice, but tightens the same idempotency check the v4.6.3 CI fix below
269
+ hardened.
270
+
271
+ ### Fixed
272
+
273
+ - A pack file whose JSON body is not an object (`null`, an array, a bare number or string) used to
274
+ read as a *successful* load and then crash with an uncaught `TypeError` — `council run --pack` and
275
+ `pack show` both died, and `pack show --json` exited 0 with `"pack": null`. It is now a clean
276
+ `PACK_NOT_FOUND`.
277
+ - **The MCP `amicus_start` shared-server path skipped the budget gate entirely unless a pack set
278
+ `maxCost`.** It now gates unconditionally with the same `maxCost`/`maxCostPerMtok` config fallback
279
+ the CLI has always used. Runs that previously proceeded may now be refused — that is the fix.
280
+ - Budget refusal text is now surface-aware on the MCP `amicus_start` (shared-server) path: it no
281
+ longer names `--max-cost`/`--no-cost-gate`, flags that do not exist there. Other MCP-visible
282
+ refusals — the `amicus_start` spawn fallback and the fanout/council reservation hint recorded in
283
+ run docs — still carry CLI-flavoured text; that remainder is filed, not fixed here. On BOTH
284
+ surfaces the remedy now names only levers that can actually clear the branch that fired: raising
285
+ the ceiling was previously suggested for a per-$/Mtok refusal, which it could never clear.
286
+ - A council pack could set `options.timeout` to `true` or to a non-numeric string and have it reach
287
+ the engine — `validatePack` checks option key names, never value types, and the old post-merge
288
+ check was `timeout <= 0`, which `true` passes by coercing to `1`. Such a pack now exits 1.
289
+ - `--cwd` typed without a value parsed as boolean `true` and reached 16 consumer sites — crashing
290
+ `council run` with a raw `TypeError` and silently resolving templates against `<cwd>/true`. It now
291
+ exits 1 at the entry point. Same treatment for `council run`'s `--out-dir`, `--claude-review`,
292
+ `--run-id`, and `--timeout` (which also accepted `NaN`).
293
+ - **`council run --out-dir` could write outside the project.** The MCP path has been fenced since
294
+ v4.5; the CLI now applies the same containment check.
295
+ - A council member whose model name collides with an `Object.prototype` key (`toString`,
296
+ `constructor`, …) crashed the Workspace seats repaint and every live tick after it.
297
+
298
+ ### Added (v4.7 PR7)
299
+
300
+ - **The seats table's trailing flag column marks a seat that was retried and stayed failed**
301
+ (PR1F-4): `↻ retried once`, on the errored seat's own primary cost row. Previously that
302
+ information only existed on a dead-seat row the v4.7 CA-4 convergence now suppresses, so it had
303
+ no home at all; a status-cell suffix was rejected because a dead seat's row can legitimately
304
+ carry `status: 'complete'`, and `complete — retried once` reads as "it finished, twice." Terminal
305
+ path only (`seatsFromRunStats`) — a seat retried while its run is still live shows no marker
306
+ until the run finishes.
307
+
308
+ ### Fixed (v4.7 PR7)
309
+
310
+ - **Four stale-paint paths in the workspace prose panels, plus three unhandled-rejection sites,
311
+ closed together (T19-m1/T19-m2).** A panel could repaint stale content after a
312
+ close/flip-blind/reopen sequence, after two loads landed out of order, after the same run's
313
+ artifact manifest grew mid-flight, or after being collapsed mid-flight and reopened; separately,
314
+ three fire-and-forget panel-load promises could reject uncaught. Fixed behind a new
315
+ `workspace-lazy.js` extraction: an unconditional panel-cache drop on every issue, a monotonic
316
+ per-panel issue token so only the newest in-flight load can paint, two-argument
317
+ `.then(ok, fail)` termination on every load promise (with an announced eviction and a self-check
318
+ against a stale cache write), and a sync-safe wrap around the matrix drill-in call so a
319
+ synchronous throw still surfaces instead of escaping uncaught. Worth recording honestly: the
320
+ collapse-mid-flight path was not closed by the cache-drop half — that half *converted* it from a
321
+ deterministic stale paint into a race, by orphaning a still-in-flight load that no longer had a
322
+ cache entry to fence it. The issue token is what actually closes it. Both land in the same commit
323
+ series, so no released state ever carried the race.
324
+ - **`amicus_fanout`'s wave briefing is now the rendered prompt, not the raw one** (W1-M4),
325
+ matching the parity the `amicus_start` in-process path already had. Previously a spawned child
326
+ that aborted before rendering its own copy left `briefing.md` — the file
327
+ `amicus list --search`/`amicus read` treat as the wave's search corpus — permanently holding
328
+ unrendered template markup. **Behaviour change:** a wave launched from a pack that forwards a
329
+ `template` now writes a second file into the wave dir, `briefing-input.md` (the raw prompt handed
330
+ to the spawned child so its own render still drives `promptMeta.template` provenance), and
331
+ `--search` now matches the rendered text even for a wave whose MCP-spawned child aborted early —
332
+ previously it matched nothing meaningful in that case; the same rendered excerpt (first 200
333
+ characters) is also seeded into `metadata.json` at creation for every MCP fan-out wave, pack or
334
+ not, so `amicus list`'s BRIEFING column is populated for an aborted wave too, not just a
335
+ completed one. `amicus_start`'s identical divergence
336
+ (`mcp-server.js:669`) is unchanged; nobody has driven that path end to end yet, so it is filed in
337
+ `BACKLOG.md`, not fixed here.
338
+ - **`amicus_fanout` now rejects an empty prompt or a non-positive timeout before creating anything
339
+ on disk**, closing the one remaining pid-less `status: 'running'` orphan-wave class the schema
340
+ didn't already cover. **Behaviour change:** a request shaped like `{prompt: '', ...}` or
341
+ `{timeout: -1, ...}` previously created a wave directory and spawned a child that then failed on
342
+ its own terms; it now returns a plain-text error and creates nothing. The Zod schema closes this
343
+ for one entry point (`prompt: z.string().min(1, …)`, `timeout: z.number().positive(…)`); a second,
344
+ identical check lives in the handler itself, after pack merge, because a pack can push the same
345
+ invalid values through a door the schema never sees (`validatePack` checks option key names, not
346
+ value types).
347
+
348
+ ### Changed (v4.7 PR7)
349
+
350
+ - **The budget-refusal text recorded in a run/wave doc's degrade record is now surface-neutral**
351
+ (PR6F-1), since that doc is read by both the CLI and MCP: "the $N cost ceiling for this run
352
+ refused it" / "Raise this run's cost ceiling, or turn the cost gate off, to seat them," in place
353
+ of CLI-flavoured `--max-cost`/`--no-cost-gate` wording that named flags an MCP caller cannot type.
354
+ A separate, structurally-unreachable copy of the same CLI-flavoured trailer (nothing ever renders
355
+ its `hint` on that path) was removed rather than reworded.
356
+
8
357
  ## [4.6.3] - 2026-08-05
9
358
 
10
359
  ### Added
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  **A multi-model LLM Council for Claude — with a parallel AI window underneath.**
6
6
 
7
- ![The Amicus council mid-ritual: five models Gemini, Llama, Grok, Claude Opusreading the same material independently, chaired by GPT](./docs/council.png)
7
+ <p align="center"><img src="./docs/cards/council-hero.svg" width="560" alt="The Amicus council ritual across three convenings: independent review spokes, anonymous cross-review, and a non-Claude chair synthesizing the verdict — the third convening seats a local Qwen2.5-Coder on-device alongside cloud peers"></p>
8
8
 
9
9
  Hand Claude a plan, a design, a diff, an architecture decision, a manuscript — anything — and say *council review this*: Amicus routes it through several models from different families, has them anonymously cross-review each other, and a non-Claude chair synthesizes a verdict you turn into accept/deny edits. Or skip the ceremony and **fork** a single conversation to Gemini, GPT, DeepSeek, or any other model — it works in parallel with full context, and you **fold** the result back when you're ready. Claude orchestrates throughout; you stay in your editor.
10
10
 
@@ -62,7 +62,7 @@ One install delivers six things that work together:
62
62
 
63
63
  Claude is the orchestrator. The council and chat skills run *on top of* the engine; you talk to Claude, and Claude drives Amicus.
64
64
 
65
- ![What one install delivers: council skill, chat skill, CLI + MCP, live catalog](./docs/what-is-amicus.png)
65
+ <p align="center"><img src="./docs/cards/what-is-amicus.svg" alt="One install: the second-opinion council and sidecar skills riding the amicus CLI + MCP engine, with the live model catalog, watch/spend, and Council Workspace fed off it"></p>
66
66
 
67
67
  ---
68
68
 
@@ -89,6 +89,8 @@ flowchart LR
89
89
  E --> F["Reviewed copy<br/>+ run folder"]
90
90
  ```
91
91
 
92
+ <p align="center"><img src="./docs/cards/blind-ballots.svg" width="520" alt="Anonymized cross-review: reviews are relabeled A, B, C before the bench ranks them — one model disputes its own review"></p>
93
+
92
94
  **What a run produces** (in `output/<stem>-council/`):
93
95
 
94
96
  - `review-<model>.md` × N — each model's independent review.
@@ -106,6 +108,8 @@ flowchart LR
106
108
  - **Debate mode** — after cross-review, every Contested or Disputed finding goes back to its raiser to **defend, amend, or withdraw**, and the disputing judges re-vote. Exactly one rebuttal round, then the final tally.
107
109
  - **Claude in the council** — Claude adds its own fresh review to the bundle so the bench ranks and adjudicates it. Claude is *judged* but never votes or chairs, so the verdict stays independent.
108
110
 
111
+ <p align="center"><img src="./docs/cards/dial-it-up.svg" alt="The four opt-in council elements as toggles — critic seat and debate on, expert lenses and Claude-in-the-council off — with the up-front run-shape disclosure"></p>
112
+
109
113
  The critic and lens methodologies are adapted from the `/critic` and `/debate` agents in [John Renaldi's product-kit](https://github.com/jrenaldi79/plugin-marketplace) (MIT); the briefing boilerplate lives in [`skills/second-opinion/SEAT-BRIEFS.md`](./skills/second-opinion/SEAT-BRIEFS.md).
110
114
 
111
115
  **Cost is disclosed up front.** Before any model launches, you see the run shape — including any enabled optional elements — for example:
@@ -114,6 +118,8 @@ The critic and lens methodologies are adapted from the `/critic` and `/debate` a
114
118
 
115
119
  Then the council waits for your confirmation.
116
120
 
121
+ <p align="center"><img src="./docs/cards/priced-up-front.svg" width="520" alt="The budget gate: a $0.42 run passes, a $31.80 bench is blocked, and the receipt matches the estimate"></p>
122
+
117
123
  The skill lives at **[`skills/second-opinion/SKILL.md`](./skills/second-opinion/SKILL.md)**; the design spec behind it is **[`skills/second-opinion/COUNCIL-DESIGN.md`](./skills/second-opinion/COUNCIL-DESIGN.md)**. For what `amicus council tally|verdict|report|stats` actually take as input and produce — field-by-field schemas, verdict.json's provenance, and a full worked example run against the real CLI — see **[docs/council.md](./docs/council.md)**.
118
124
 
119
125
  ---
@@ -127,10 +133,14 @@ The council is the hero — start with the everyday way, and reach for the more
127
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)
128
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)
129
135
 
136
+ <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
+
130
138
  ### Headless council (CI)
131
139
 
132
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).
133
141
 
142
+ <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
+
134
144
  ### Free council (zero-cost)
135
145
 
136
146
  Want the cross-examination without the model spend? `amicus setup` offers a **Free OpenRouter council** mode — readline wizard option 2, and the Electron **Models** step. It detects the free `:free` models live from the catalog, lets you multi-pick (Enter takes a vendor-diverse default), and saves them as `councils.free` — a first-class `councils` config primitive seeded under collision-safe `free-*` aliases. Your `config.default` is left untouched, and all you need is an `OPENROUTER_API_KEY`.
@@ -397,7 +407,7 @@ When you don't need a full council — just one other model's take — fork a co
397
407
  |---------|--------------|
398
408
  | `amicus start` | Launch a new session (interactive or `--no-ui`). |
399
409
  | `amicus fanout` | Run N models on the same prompt in parallel (headless). |
400
- | `amicus list` | Show previous sessions. |
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. |
401
411
  | `amicus resume` | Reopen a previous session with full history. |
402
412
  | `amicus continue` | Start a new session building on a previous one. |
403
413
  | `amicus read` | Output a session's summary / conversation / metadata. |
@@ -440,13 +450,13 @@ $ amicus status demo123 --json
440
450
  "taskId": "demo123",
441
451
  "status": "complete",
442
452
  "elapsed": "5m 0s",
443
- "version": "4.6.3",
453
+ "version": "4.7.1",
444
454
  "model": "google/gemini-2.5-flash",
445
455
  "phase": "terminal"
446
456
  }
447
457
  ```
448
458
 
449
- `amicus list --status` accepts `running`, `complete`, `error`, `timed-out`, `aborted`, `crashed`, `idle-timeout`. 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).
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).
450
460
 
451
461
  ---
452
462
 
@@ -461,6 +471,8 @@ amicus models --search gemini # filter by substring
461
471
 
462
472
  `start`/`fanout` validate your model against the catalog before launching (skip with `--no-validate-model`). You can also always bypass aliases and pass a full model ID directly — bare `provider/model` (canonical) or `openrouter/provider/model` (explicit override); see Routing below. Catalog internals, alias management, and the full-id passthrough table are in **[docs/usage.md § Models](./docs/usage.md#amicus-models--the-model-catalog)**.
463
473
 
474
+ <p align="center"><img src="./docs/cards/build-the-bench.svg" alt="Direct keys, one OpenRouter key, or local runtimes seat a mixed bench — different families, different blind spots"></p>
475
+
464
476
  ### Routing
465
477
 
466
478
  - **Bare `provider/model`** (e.g. `openai/gpt-5.5`, `anthropic/claude-opus-4-8`, `google/gemini-3.6-flash`) is the canonical, policy-routed form — Amicus routes it **direct-first**: your direct provider key when one is configured, falling back to OpenRouter automatically when only an OpenRouter key exists.
@@ -474,7 +486,7 @@ Full details, the API-key/prefix table, and the migration notice are in **[docs/
474
486
 
475
487
  ## MCP integration
476
488
 
477
- The MCP server is auto-registered on install (Claude Code and Claude Desktop / Cowork). It exposes sixteen tools:
489
+ The MCP server is auto-registered on install (Claude Code and Claude Desktop / Cowork). It exposes these tools:
478
490
 
479
491
  | Tool | What it does |
480
492
  |------|--------------|
@@ -482,7 +494,7 @@ The MCP server is auto-registered on install (Claude Code and Claude Desktop / C
482
494
  | `amicus_status` | Poll a task (or a fanout wave) for completion. |
483
495
  | `amicus_wait` | Block inside one tool call until a session/wave finishes or the wait window closes. |
484
496
  | `amicus_read` | Read results: summary, conversation, metadata, or JSON. |
485
- | `amicus_list` | List past sessions. |
497
+ | `amicus_list` | List past sessions (`tag` field; `search` filters by id/tag/briefing substring). |
486
498
  | `amicus_resume` | Reopen a session. |
487
499
  | `amicus_continue` | New session building on a previous one. |
488
500
  | `amicus_abort` | Stop a running session. |
package/bin/amicus.js CHANGED
@@ -23,6 +23,8 @@ const { handleResume, handleContinue } = require('../src/cli-handlers-resume-con
23
23
  const { isOneShotCommand, armExitWatchdog } = require('../src/utils/lifecycle');
24
24
  const { suggestCommand } = require('../src/utils/input-validators');
25
25
  const { unknownFlags, getKnownFlags } = require('../src/utils/known-flags');
26
+ const { packSaveVersionConflict } = require('../src/utils/cli-preflight');
27
+ const { failJson } = require('../src/utils/error-doc');
26
28
  const { logger } = require('../src/utils/logger');
27
29
 
28
30
  const VERSION = require('../package.json').version;
@@ -53,6 +55,23 @@ async function main() {
53
55
  process.exit(1);
54
56
  }
55
57
 
58
+ // `--cwd` typed with no value parses as boolean `true` (src/cli.js:101) and
59
+ // `--cwd=` as '' (src/cli.js:72). DEFAULTS (src/cli.js:28) always seeds a
60
+ // real absolute string, so a non-string or empty cwd can ONLY mean "typed
61
+ // without a value" — which makes this guard provably free of false
62
+ // positives. Left unguarded it reached 16 `args.cwd || process.cwd()`
63
+ // sites across 9 handlers: council run threw a raw TypeError, template
64
+ // silently resolved <cwd>/true.
65
+ // No dash check here: absolute paths never start with '-', and
66
+ // `--cwd ./x` is legitimate.
67
+ if (typeof args.cwd !== 'string' || args.cwd === '') {
68
+ console.error('Error: --cwd requires a value');
69
+ console.error(command
70
+ ? `Run \`amicus ${command} --help\` to see valid options.`
71
+ : 'Run `amicus --help` to see valid options.');
72
+ process.exit(1);
73
+ }
74
+
56
75
  // Install crash handler for MCP-spawned processes (have --task-id)
57
76
  if (args['task-id'] && (command === 'start' || command === 'continue')) {
58
77
  const { installCrashHandler } = require('../src/sidecar/crash-handler');
@@ -86,6 +105,16 @@ async function main() {
86
105
  }
87
106
  }
88
107
 
108
+ // `pack save` documents a per-pack `--pack-version <semver>`. `--version` is a
109
+ // global BOOLEAN_FLAG, so `pack save … --version 2.0.0` used to fall straight
110
+ // into the banner below: exit 0, no pack written, the semver stranded in
111
+ // positionals. Reject that one combination by name instead of silently doing
112
+ // something else; every other --version still prints the banner.
113
+ const versionConflict = packSaveVersionConflict(args);
114
+ if (versionConflict) {
115
+ process.exit(failJson(!!args.json, versionConflict));
116
+ }
117
+
89
118
  // Handle --version
90
119
  if (args.version) {
91
120
  console.log(`amicus v${VERSION}`);
@@ -221,6 +250,8 @@ async function handleList(args) {
221
250
  status: args.status,
222
251
  all: args.all,
223
252
  json: args.json,
253
+ search: args.search,
254
+ limit: args.limit,
224
255
  project: args.cwd
225
256
  });
226
257
  }