amicus 1.8.0 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,13 +1,12 @@
1
1
  {
2
2
  "name": "amicus",
3
- "version": "1.8.0",
3
+ "version": "1.9.0",
4
4
  "description": "Multi-model LLM Council + parallel AI window for Claude Code. Run structured council reviews across Gemini, GPT, DeepSeek and more — or fork a conversation to any model and fold the results back.",
5
5
  "author": {
6
6
  "name": "Christian Wagner"
7
7
  },
8
8
  "homepage": "https://bourbondog.github.io/amicus/",
9
9
  "repository": "https://github.com/BourbonDog/amicus",
10
- "bugs": "https://github.com/BourbonDog/amicus/issues",
11
10
  "license": "MIT",
12
11
  "keywords": [
13
12
  "claude-code",
package/CHANGELOG.md CHANGED
@@ -5,6 +5,154 @@ All notable changes to Amicus are documented here. Format follows
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [1.9.0] - 2026-07-03
9
+
10
+ Engine pull-forwards, release-rail hardening, docs sync, and a new Council Review GitHub Action.
11
+
12
+ ### Added
13
+ - **`/amicus:council` slash command and a `/amicus:sidecar <model> <prompt…>` argument surface.** `commands/council.md`
14
+ wraps the `second-opinion` skill end-to-end via `$ARGUMENTS`; `skills/sidecar/SKILL.md` gained an
15
+ `argument-hint` and a slash-invocation section binding `$1` (model alias, falling back to gemini for
16
+ non-model-looking input) and `$ARGUMENTS` (full prompt). **Slash commands are plugin-channel-only:**
17
+ `commands/` ships in the npm tarball (via `package.json`'s `files` array) but the npm/`install.sh`/
18
+ `install.ps1` postinstall flow never copies it into a Claude Code commands directory — only
19
+ `skills/sidecar` and `skills/second-opinion` are installed that way. npm/postinstall users do not get
20
+ `/amicus:council` or `/amicus:sidecar`; only plugin installs (`claude plugin install`) do. This is a
21
+ known, accepted gap, not a bug — carried forward from the 9.1 review as a note that must keep
22
+ reappearing in release-facing docs so it doesn't get silently "fixed" into a false claim.
23
+ - **MCP Registry wiring.** `package.json` gained `mcpName: "io.github.BourbonDog/amicus"`; `server.json`
24
+ (repo root) describes the stdio launch (`npx amicus mcp`). `.github/workflows/publish.yml` now publishes
25
+ to `registry.modelcontextprotocol.io` via `mcp-publisher`, authenticated over the same GitHub OIDC token
26
+ used for npm Trusted Publishing — no registry secret required. This fires automatically on every `v*` tag
27
+ push, strictly after `npm publish` succeeds (npm-side ownership validation reads the published
28
+ `package.json`). See `docs/DISTRIBUTION.md` §3 for the full flow, the release-order dependency on the
29
+ Phase 4 tool-surface de-bloat, and the manual recovery path if the registry publish fails in CI.
30
+ - **Marketplace submission runbook and preflight guard.** `docs/DISTRIBUTION.md` documents the
31
+ `claude-community` submission process (individual-author Console form route), the preflight checklist
32
+ (`claude plugin validate . --strict`, `claude --plugin-dir .` smoke test, `npm test`), and what the
33
+ Anthropic review pipeline is expected to check.
34
+ - **Council Review GitHub Action (v1).** A new reusable, label-gated workflow (`.github/workflows/council-review.yml`)
35
+ runs an `amicus fanout` review wave (default cheap bench `deepseek,gemini,glm`, cost- and time-bounded) over a
36
+ pull request's diff and posts one sticky synthesis comment with the individual reviews collapsed underneath. v1 is
37
+ fanout-only — independent reviews plus a one-leg synthesis, no adjudicated verdict (that needs the skill-orchestrated
38
+ Stage-2 cross-review, which a code-only pipeline can't produce; deferred to v2). Fork-safe and no-checkout by
39
+ design: PR code is never checked out or executed, only its diff (capped, via `gh pr diff`) is read; the job soft-skips
40
+ with a notice when `OPENROUTER_API_KEY` is unavailable (e.g. a fork PR without repo secrets) rather than failing the
41
+ check; every use of PR-controlled text (title/body) reaches the shell only through `env:` indirection, never inlined
42
+ into a `run:` script. Untrusted model output is neutralized before it enters the PR comment — case-insensitive,
43
+ whitespace-tolerant rules strip anything that could forge the sticky-comment marker (and hijack the next run's
44
+ update), forge the "not an adjudicated verdict" footer disclosure, or break out of the comment's own `<details>`
45
+ wrapper — and the real footer is echoed last, after all model text, so its position can't be forged. The label
46
+ gate (`council-review`) is enforced with a string-safe comparison (`format('{0}', inputs.require_label) == 'false'`)
47
+ to avoid a loose-equality bug where GitHub coerces an empty `pull_request`-event input to falsy and would otherwise
48
+ bypass the gate on every same-repo PR. **Inert by default:** the workflow only runs once a repo both adds the
49
+ `OPENROUTER_API_KEY` Actions secret and applies the `council-review` label to a PR — installing it does nothing on
50
+ its own. Locked by `tests/scripts/council-review-workflow.test.js`.
51
+
52
+ ### Changed
53
+ - **Every prose channel that returns another model's output is now wrapped in the
54
+ `<untrusted_sidecar_output>` fence** (`amicus_status`/`amicus_list` previews remain sanitized-and-truncated
55
+ instead, by design — `sanitizePreview()` in `src/sidecar/progress-fields.js` defangs fence/tag characters
56
+ and caps length so the full untrusted text is only ever reachable through the fenced `amicus_read` path),
57
+ extending the protection `amicus_read` summaries already had: MCP wave and conversation reads, CLI
58
+ `amicus read` summary/conversation/wave output, and the foreground summary echo after
59
+ `start`/`continue`/`resume`. This is visible in CLI output. JSON output (`--json`), metadata mode, and
60
+ on-disk artifacts (`wave.json`, `summary.md`, `conversation.jsonl`) are byte-identical to before — the
61
+ fence is applied only at output time, never at write time.
62
+ - Internal: `interactive.js`'s Electron process helpers extracted to `src/sidecar/interactive-process.js`
63
+ (size-gate headroom; no behavior change).
64
+
65
+ ### Fixed
66
+ - **`plugin.json`'s unrecognized `bugs` field removed.** `claude plugin validate . --strict` now passes
67
+ clean (exit 0); it previously reported an unknown-field warning that `--strict` promotes to an error.
68
+ - **The Fold handoff is now documented operationally** (README + usage.md): the `[SIDECAR_FOLD]` stdout
69
+ block, where the summary lands (`summary.md`), and how the orchestrator reads it back (fenced, via
70
+ `amicus read`/`amicus_read`).
71
+ - **README↔usage.md drift corrected against the binary:** `amicus fanout` documents `--council`
72
+ (mutually exclusive with `--models`, exactly one required) in both files; `amicus list --status`
73
+ documents the full 7-value set (`running, complete, error, timed-out, aborted, crashed, idle-timeout`)
74
+ — note the `--json` schema's distinct `timeout` vocabulary is deliberately unchanged; fanout
75
+ `--session-id` support documented; `amicus status` gained real human and `--json` output examples;
76
+ `start --setup` documented as NOT relaxing the `--prompt`/`--prompt-file` requirement (with the exact
77
+ error string users see).
78
+ - **OpenRouter 402 recovery** added to the README troubleshooting table and docs/troubleshooting.md:
79
+ key save/validation never checks account balance, so the first council review / `start` / `fanout` call
80
+ can 402 (the `amicus council` subcommand itself is deterministic math and never calls a model) — recovery
81
+ via openrouter.ai/credits, `:free` models, and the non-blocking `amicus doctor` credit probe.
82
+ - docs/DISTRIBUTION.md's stale `/v0.1/` registry API path synced to `/v0/`.
83
+ - All of the above locked by `tests/docs-quick-sync.test.js` (17 pins).
84
+ - **Closing the GUI window no longer loses the session summary.** Closing without folding previously
85
+ destroyed the window immediately — the session finalized as `complete` with a placeholder summary, and
86
+ closing during an in-flight fold discarded the summary about to land. The window close is now intercepted
87
+ by a close guard (`electron/close-guard.js`): a close with no fold auto-triggers the same fold flow
88
+ (overlay + summary + `[SIDECAR_FOLD]` handoff) and then closes; a close during an in-flight fold lets it
89
+ finish — regardless of whether the fold was close-initiated or started from the toolbar/shortcut — instead
90
+ of falling through and destroying the window mid-summary; a failed or timed-out fold still closes the
91
+ window (the user is never trapped). This relies on `electron/fold.js` exposing a finer-grained
92
+ `isFolding()`/`hasCompleted()` split (a fold is "in flight" from the moment `triggerFold` is entered until
93
+ its `[SIDECAR_FOLD]` stdout write actually succeeds) alongside the original `hasFolded()`, so the guard can
94
+ tell "still running" apart from "actually done" — and a fold that settles without completing (including a
95
+ synchronous throw from the post-write nudge-overlay update, which the old code's `.catch()` couldn't
96
+ observe) still safely falls back to closing the window rather than leaving it permanently stuck open.
97
+ External abort remains immediate and never waits on a fold.
98
+ - **The MCP server no longer hardcodes `--client cowork`.** Under Claude Code — the primary caller — that
99
+ hardcode silently broke `includeContext:true` (empty context), parent-MCP discovery, and session-dir
100
+ resolution. The server now detects its caller from the MCP handshake's `clientInfo` (claude-code →
101
+ `code-local`; Claude Desktop/Cowork → `cowork`; unknown callers keep today's `cowork` behavior with a
102
+ one-time stderr notice) and threads the detected client through every spawn path and the in-process
103
+ shared-server path. A new `AMICUS_MCP_CLIENT` env var (set it in the MCP registration's `env` block)
104
+ explicitly overrides detection. One consequence: MCP-spawned GUI chat sessions under Claude Code now keep
105
+ the default SE-focused base prompt — `opencode-client.js`'s Cowork-specific general-purpose prompt swap
106
+ (`buildCoworkAgentPrompt()`) only fires when `options.client === 'cowork'`, which no longer matches a
107
+ Claude Code caller now that it's correctly tagged `code-local`.
108
+ - **Release-workflow re-runs now recover a half-published release instead of dead-ending.** A `publish.yml`
109
+ re-run after a post-`npm publish` failure previously died on `EPUBLISHCONFLICT` before ever reaching the
110
+ step that failed. Now the npm publish is skipped (loudly) when `amicus@<version>` is already live (E404
111
+ means not-published and proceeds; any other `npm view` error fails loud rather than skipping), a
112
+ tag↔`package.json` lockstep check fails fast before anything publishes, the MCP Registry publish is
113
+ skipped when the version is already registered (pre-check tolerates transport-level failures and falls
114
+ through to publishing), `mcp-publisher login github-oidc` gained the same 5×20s retry the publish call
115
+ already had, and `gh release create` is guarded by an existence check. `docs/DISTRIBUTION.md` §3 now
116
+ documents re-run as the primary recovery path, with the manual path as fallback. Locked by
117
+ `tests/scripts/publish-workflow.test.js`.
118
+ - **The `second-opinion` skill's frontmatter description no longer exceeds Claude Code's 1024-char cap.**
119
+ It was 1441 chars, so the router silently truncated the tail — which was the NOT-clause routing quick
120
+ single-model asks ("ask Gemini…", "what does DeepSeek think") to the `sidecar` skill. Rewritten to
121
+ 988 chars with every trigger phrase and the NOT boundary intact (same fix pattern as the sidecar skill's
122
+ 1.8.1 overhaul); locked by `tests/skill-second-opinion-docs.test.js`. Existing installs pick the fix up
123
+ when postinstall refreshes skill copies on the next upgrade.
124
+
125
+ ## [1.8.1] - 2026-07-02
126
+
127
+ Docs & skills accuracy sprint from the Phase-8 whole-branch review — no engine changes. Every item fixed a claim
128
+ that actively misdirected Claude or users, plus one headless completion-state bugfix.
129
+
130
+ ### Changed
131
+ - **`report.html` is now the default final council artifact**, and an inline verdict summary in chat is
132
+ MANDATORY at Stage 5 of the second-opinion skill.
133
+ - **The `sidecar` skill's frontmatter dropped the "second opinion from another model" trigger** — those requests
134
+ now route to the `second-opinion` skill instead.
135
+ - **MODEL-NOTES seed updated** with durable lessons from council runs 4-7 (new Grok/Kimi/Mistral/Claude-in-council
136
+ sections; shipped/local split defined). Existing installs: the machine-local copy is installed only-if-missing —
137
+ merge/refresh manually by pointing at the shipped file.
138
+ - **Council mechanics hardened:** mandatory no-tools preamble for judges and chair (plus scratch-cwd advice);
139
+ `--max-cost` / `--no-cost-gate` pass-through documented for repair and chair calls (the false solo-start
140
+ cost-gate exemption was removed); `--models` lists quoted in every example; current-date injection rule for
141
+ time-sensitive artifacts.
142
+
143
+ ### Fixed
144
+ - **Plugin quick-start now states the truth:** plugin installs do not put `amicus` on `PATH`; use
145
+ `npx -y amicus@latest <cmd>`. Both skills gained an npx-fallback/transport rule.
146
+ - **README/usage now document `doctor`, `key`, and `council`;** troubleshooting leads with `amicus doctor`; the
147
+ false "`amicus list` shows active servers" claim is replaced with real `netstat`/`lsof` guidance.
148
+ - **Headless runs that finish via idle detection no longer write `status:"error"` / `reason:"Incomplete"` to
149
+ `metadata.json`.** The poll loop's two genuine idle-completion exits — the SDK-authoritative `session.status`
150
+ idle signal and the stable-poll activity heuristic (both gated on real output, F1 #16) — broke out of the loop
151
+ without setting `completed`, so `resolveTerminalState` fell through to error and poisoned `amicus_list` /
152
+ `amicus_status` / wave rollups for successful runs, while the stdout `--json` doc correctly said
153
+ `status:"complete"`. Both exits now mark the run completed, matching the fold-marker branch. Dead-server
154
+ classification is unchanged: the consecutive-poll-failure fast-exit (F4) and crash paths still report an error.
155
+
8
156
  ## [1.8.0] - 2026-07-02
9
157
 
10
158
  ### Added
package/README.md CHANGED
@@ -57,7 +57,7 @@ Claude is the orchestrator. The council and chat skills run *on top of* the engi
57
57
 
58
58
  ## Quick start
59
59
 
60
- **Install** — pick whichever fits; all deliver the same CLI, MCP server, and both skills:
60
+ **Install** — pick whichever fits. Every path delivers the MCP server and both skills; the `amicus`/`am` CLI lands on your PATH with the **npm and install-script paths** (the plugin path runs the CLI on demand via `npx -y amicus@latest <command>`):
61
61
 
62
62
  **As a Claude Code plugin** — the most native path if you use Claude Code:
63
63
 
@@ -67,7 +67,7 @@ Claude is the orchestrator. The council and chat skills run *on top of* the engi
67
67
  /reload-plugins
68
68
  ```
69
69
 
70
- Claude Code registers the MCP server and both skills for you — nothing to configure. (The standalone Electron window is npm-only, and the first council/sidecar call downloads the OpenCode engine.)
70
+ Claude Code registers the MCP server and both skills for you — nothing to configure. (The plugin does not put `amicus` on your PATH — CLI calls go through `npx -y amicus@latest <command>`; the standalone Electron window is npm-only; and the first council/sidecar call downloads the OpenCode engine.)
71
71
 
72
72
  **With the install script** — macOS, Linux, or Windows (needs [Node.js](https://nodejs.org) ≥ 18):
73
73
 
@@ -96,6 +96,8 @@ For the **npm** and **install-script** paths, a postinstall auto-configures ever
96
96
 
97
97
  ```bash
98
98
  amicus setup
99
+ # plugin-only install (no CLI on PATH):
100
+ npx -y amicus@latest setup
99
101
  ```
100
102
 
101
103
  This opens a graphical wizard:
@@ -113,7 +115,7 @@ This opens a graphical wizard:
113
115
 
114
116
  > *council review this*
115
117
 
116
- Claude prepares the material, recommends a bench of models, discloses the run shape and cost, and orchestrates the rest. You make the accept/deny calls at the end. (The `second-opinion` skill installed in the previous step is what teaches Claude to recognize this — if nothing happens, confirm it landed in `~/.claude/skills/second-opinion/`.)
118
+ Claude prepares the material, recommends a bench of models, discloses the run shape and cost, and orchestrates the rest. You make the accept/deny calls at the end. (The `second-opinion` skill is what teaches Claude to recognize this — if nothing happens, run `amicus doctor` (or `npx -y amicus@latest doctor`). npm/install-script installs place the skill at `~/.claude/skills/second-opinion/`; plugin installs keep it inside the plugin itself — check `/plugin` in Claude Code to confirm amicus is enabled.)
117
119
 
118
120
  **Your first sidecar.** The sidecar is the lower-level path — you can invoke it by phrase through Claude too, but the CLI gives you the flags directly:
119
121
 
@@ -224,6 +226,8 @@ When you don't need a full council — just one other model's take — fork a co
224
226
 
225
227
  **Headless (`--no-ui`):** the agent works autonomously and emits the fold summary when it finishes — ideal for bulk work like test generation or documentation. Pair with `--json` for a machine-readable run document.
226
228
 
229
+ **How the fold handoff actually works.** A fold is a summary handoff, not a live handback — once you fold, the sidecar model's context is gone. Mechanically: clicking **FOLD** asks the model for a structured summary, then writes it to the Electron process's stdout as a `[SIDECAR_FOLD]`-tagged block; the amicus runner that spawned Electron captures that stdout and persists it as the session's `summary.md` (under the session directory, alongside `metadata.json` and `conversation.jsonl`). Nothing pushes the result back to you automatically — your orchestrating agent picks it up on request, via `amicus read <taskId>` (CLI) or the `amicus_read` MCP tool. Either path returns the summary wrapped in an `<untrusted_sidecar_output>` fence, since it's prose from another model entering your context and should be treated as data, not instructions.
230
+
227
231
  **Context sharing.** Your conversation history is passed automatically. Tune it:
228
232
 
229
233
  - `--context-turns <N>` — max conversation turns to include (default 50).
@@ -261,6 +265,9 @@ amicus update
261
265
  | `amicus read` | Output a session's summary / conversation / metadata. |
262
266
  | `amicus status <id>` | One-shot status for a session or fan-out wave (human or `--json`; `--wave <id>` alternative spelling). |
263
267
  | `amicus models` | List, search, refresh the catalog, or audit aliases. |
268
+ | `amicus doctor` | Diagnose your setup — keys, default model, catalog, aliases, OpenCode binary, Electron, skills, MCP registration, OpenRouter credit (`--json`; `--fix` self-heals what it can). |
269
+ | `amicus key` | Manage API keys non-interactively: `amicus key <provider> <key>` saves after live validation; `--remove`; bare `amicus key` lists providers. |
270
+ | `amicus council` | Council math: `tally <input.json>` (deterministic tiers + ledger append), `stats` (reviewer reliability), `report <verdict.json> [--md\|--html]`. |
264
271
  | `amicus abort` | Abort a running session (or `--all`). |
265
272
  | `amicus setup` | Configure default model, API keys, and aliases. |
266
273
  | `amicus update` | Update to the latest version. |
@@ -296,7 +303,7 @@ The `am` alias is interchangeable with `amicus` everywhere.
296
303
  | `--fold-shortcut <key>` | Customize the fold keyboard shortcut. | `Cmd/Ctrl+Shift+F` |
297
304
  | `--opencode-port <port>` | Port override for the OpenCode server. | |
298
305
  | `--session-dir <path>` | Explicit session-data directory. | |
299
- | `--setup` | Force-open configuration before launching. | |
306
+ | `--setup` | Force-open configuration before launching. Does **not** relax the `--prompt`/`--prompt-file` requirement — `start --setup` still fails fast with "Error: --prompt or --prompt-file is required" if neither is given. | |
300
307
  | `--no-validate-model` | Skip model-catalog validation before launch. | validation on |
301
308
 
302
309
  > Agents: **Chat** auto-approves reads and asks before writes/bash (interactive default); **Build** has full tool access (headless default); **Plan** is read-only analysis. `--agent Chat` is interactive-only and incompatible with `--no-ui`.
@@ -304,7 +311,7 @@ The `am` alias is interchangeable with `amicus` everywhere.
304
311
  ### `amicus fanout` — same prompt, many models
305
312
 
306
313
  ```bash
307
- amicus fanout --models gemini,deepseek,gpt --prompt "Review this design" --json
314
+ amicus fanout --models "gemini,deepseek,gpt" --prompt "Review this design" --json
308
315
  ```
309
316
 
310
317
  Fanout runs one **headless wave**: every leg gets the **same** prompt (this is the shared-prompt model the council's review stages are built on). When all legs are terminal it prints **one** JSON wave document on stdout.
@@ -314,6 +321,7 @@ Fanout runs one **headless wave**: every leg gets the **same** prompt (this is t
314
321
  - `--prompt <text>` / `--prompt-file <path>` — the shared briefing. `--prompt-file` avoids the ~32 KB Windows argument cap and is mutually exclusive with `--prompt`.
315
322
  - `--wave-id <id>` — set the wave ID explicitly (leg IDs become `<id>-1..N`).
316
323
  - `--json` — emit the wave document.
324
+ - `--session-id <id|current>` — session to pull shared context from (same as `start`; supported on `fanout` too).
317
325
  - Shared per-leg knobs: `--agent`, `--thinking`, `--timeout`, `--summary-length`, `--no-context`, the `--context-*` flags, the `--mcp*` flags, `--no-validate-model`, `--cwd`.
318
326
  - **Exit codes:** `0` all legs complete, `2` partial wave, `1` none complete / hard failure.
319
327
 
@@ -321,7 +329,8 @@ Fanout runs one **headless wave**: every leg gets the **same** prompt (this is t
321
329
 
322
330
  ```bash
323
331
  amicus list # current project
324
- amicus list --status running # filter by status (running, complete)
332
+ amicus list --status running # filter by status: running, complete, error,
333
+ # timed-out, aborted, crashed, idle-timeout
325
334
  amicus list --all # all projects
326
335
  amicus list --json # machine-readable
327
336
 
@@ -344,6 +353,32 @@ amicus setup --api-keys # open just the API-key window
344
353
  amicus setup --add-alias fast=openrouter/google/gemini-2.5-flash # add/override one alias
345
354
  ```
346
355
 
356
+ **`amicus status <id>` output.** Human-readable:
357
+
358
+ ```
359
+ $ amicus status demo123
360
+ Task: demo123
361
+ Status: complete (terminal)
362
+ Elapsed: 5m 0s
363
+ Model: google/gemini-2.5-flash
364
+ ```
365
+
366
+ `--json`:
367
+
368
+ ```
369
+ $ amicus status demo123 --json
370
+ {
371
+ "taskId": "demo123",
372
+ "status": "complete",
373
+ "elapsed": "5m 0s",
374
+ "version": "1.9.0",
375
+ "model": "google/gemini-2.5-flash",
376
+ "phase": "terminal"
377
+ }
378
+ ```
379
+
380
+ A running session additionally reports `messages`, `lastActivity`/`latest`, and (if stalled) a `STALLED` line with recovery guidance in `--json`. A wave ID (`amicus status <waveId>` / `--wave <waveId>`) instead reports `legsComplete`/`legsTotal` and a per-leg breakdown.
381
+
347
382
  ---
348
383
 
349
384
  ## Models
@@ -429,6 +464,7 @@ claude mcp add-json amicus '{"command":"npx","args":["-y","amicus@latest","mcp"]
429
464
  | `AMICUS_CONFIG_DIR` | Override the config directory (keys, catalog, sessions). | `~/.config/amicus` |
430
465
  | `AMICUS_FANOUT_MAX_LEGS` | Cap the number of legs in a single fanout wave; non-positive values fall back to 10. | `10` |
431
466
  | `AMICUS_SHARED_SERVER` | When `1`, multiple MCP sessions share a single OpenCode Go process, eliminating cold-start latency. Set to `0` for per-process isolation or to diagnose a crash loop. | `1` |
467
+ | `AMICUS_MCP_CLIENT` | Override the auto-detected MCP caller (`code-local`, `code-web`, or `cowork`). Amicus detects Claude Code vs. Claude Desktop/Cowork from the MCP `initialize` handshake; set this only if detection picks the wrong one. Note: `code-web` requires an explicit `--session-dir` and is not usable for MCP-spawned sessions. | auto-detected |
432
468
 
433
469
  **Headless poller tuning** (advanced — rarely needed)
434
470
 
@@ -505,6 +541,7 @@ Most Claude-adjacent tooling assumes macOS/Linux; Amicus doesn't.
505
541
  | `npm install -g amicus` fails with `EEXIST: … claude-sidecar` | The old upstream `claude-sidecar` package is still installed globally; npm won't overwrite another package's bin shims | `npm uninstall -g claude-sidecar`, then `npm install -g amicus`. Your config and sessions carry over (legacy paths are still read). |
506
542
  | Install fails partway, or `amicus doctor` reports the OpenCode binary "not found" | A **transient** error during the OpenCode engine's own postinstall (a spawn `ENOENT`, or an antivirus file-lock while it lays down its 11 per-platform binaries) can roll back the whole atomic install — retrying usually succeeds | Just re-run `npm install -g amicus`. If it still fails, clear the cache first: `npm cache clean --force && npm install -g amicus`. |
507
543
  | `401` / auth error | API key missing, or the model prefix doesn't match the key you have | Run `amicus setup`; make sure the prefix (`openrouter/…` vs `google/…` vs `openai/…` vs `anthropic/…`) matches the credentials you configured. |
544
+ | `402` / "Payment Required" on first council review / `start` / `fanout` call | Your OpenRouter key is real but has no credit. Key save (`amicus key openrouter <key>` or the setup wizard's key step) only checks that the key **authenticates** — it doesn't check balance, so a zero-credit key saves cleanly and only fails later, on the first real model call. (The `amicus council` subcommand itself is deterministic math and never calls a model.) | Add credit at [openrouter.ai/credits](https://openrouter.ai/credits), **or** switch to a zero-cost council: `amicus setup` → option 2 (Free OpenRouter council) builds one from live `:free`-suffixed models and saves it as `councils.free` — then run `amicus fanout --council free …`. See "Free council (zero-cost)" under [The Council](#the-council) above. |
508
545
  | Session not found | No session matches the given ID | Run `amicus list`, or omit `--session-id` to use the most recent. |
509
546
  | No conversation history found | Project-path encoding | Check `~/.claude/projects/`; `/` and `_` in the project path are encoded as `-` in the directory name. |
510
547
  | Headless run never finishes | Task is bigger than the default timeout | Raise it: `--timeout 30`. |
@@ -0,0 +1,22 @@
1
+ ---
2
+ description: Run a structured multi-model LLM council review of the given material — wraps the second-opinion skill (independent reviews → anonymous cross-review → non-Claude chair verdict → accept/deny decisions).
3
+ argument-hint: [material, path, or URL] [analysis request + criteria]
4
+ disable-model-invocation: true
5
+ ---
6
+
7
+ Run a full council review by invoking the `second-opinion` skill shipped in this
8
+ plugin (listed as `amicus:second-opinion`). Do not synthesize a verdict yourself —
9
+ the skill's chair model does that; you orchestrate.
10
+
11
+ Treat everything the user typed after the command as the review request:
12
+
13
+ $ARGUMENTS
14
+
15
+ Interpret it as three inputs: the **material** (inline text, a file path, or a URL),
16
+ the **analysis request**, and the **criteria**. If any of the three is missing or
17
+ ambiguous, ask for it before launching any model (the skill's Stage 0 covers this —
18
+ don't re-ask for what is already present).
19
+
20
+ Then follow the second-opinion skill end to end: Stage 0 intake/prep and run-folder
21
+ setup, council selection with a cost estimate and explicit user confirmation, the
22
+ three review waves, `amicus council tally`, and the accept/deny decision pass.
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Close Guard — auto-fold on window close (backlog B01)
3
+ *
4
+ * `mainWindow.on('close', ...)` used to destroy the window immediately
5
+ * whenever no fold had run yet, silently discarding the session summary on
6
+ * the most common "I'm done, close the window" path (red X / Cmd|Ctrl+Q /
7
+ * window-all-closed). createCloseGuard() centralizes the decision so it can
8
+ * be unit-tested without booting Electron:
9
+ *
10
+ * - No fold attempted yet: prevent the destroy, invoke the SAME triggerFold
11
+ * closure (fold.js's own `folded` flag guards re-entrancy — this module
12
+ * never constructs a second fold handler). triggerFold's existing success
13
+ * path already closes the window itself; its failure path resets
14
+ * folded=false.
15
+ * - Fold already IN FLIGHT: prevent the destroy and let the in-flight fold
16
+ * finish and close the window on its own — regardless of who started it
17
+ * (a second user close during an auto-fold, or a close during a
18
+ * toolbar/shortcut-initiated fold, must both be blocked; a single
19
+ * `hasFolded()`-style flag that goes true at triggerFold ENTRY cannot
20
+ * distinguish in-flight from done, so this relies on fold.js's finer
21
+ * `isFolding()`/`hasCompleted()` split instead).
22
+ * - Fold already COMPLETED (the `[SIDECAR_FOLD]` stdout write actually
23
+ * succeeded): proceed exactly like the pre-existing behavior (no
24
+ * interception).
25
+ * - A close-initiated fold SETTLES WITHOUT COMPLETION — whether triggerFold
26
+ * rejects, or its outer catch swallows the error and resolves (e.g. the
27
+ * nudge-overlay executeJavaScript call throws synchronously on a
28
+ * destroyed webContents; fold.js's trailing `.catch()` there only guards
29
+ * promise rejection, not a sync throw) — fall back to the pre-existing
30
+ * destroy path so the user is never trapped with a window that can no
31
+ * longer be closed. A latch ensures the fallback destroy is not
32
+ * re-intercepted by this same guard, and a second rapid close click while
33
+ * a fold is in flight does not spawn another fold.
34
+ *
35
+ * This module is intentionally ignorant of external abort — `amicus abort` /
36
+ * MCP amicus_abort SIGTERMs the Electron child process directly
37
+ * (src/sidecar/interactive-abort.js killElectron), which is an OS-level kill
38
+ * of the whole process, not a call into any renderer 'close' handler. Node
39
+ * terminates immediately on SIGTERM unless the process itself installs a
40
+ * 'SIGTERM' listener; main.js installs none, so that teardown can never route
41
+ * through this guard.
42
+ */
43
+
44
+ /**
45
+ * @param {object} deps
46
+ * @param {() => boolean} deps.hasFolded - fold.js's original entry-to-settle
47
+ * flag (kept for API-compatibility with other hasFolded() consumers; not
48
+ * used to gate the close decision here since it can't distinguish
49
+ * in-flight from done).
50
+ * @param {() => boolean} [deps.isFolding] - Whether a fold is currently IN
51
+ * FLIGHT (entered but not yet completed). Falls back to `hasFolded()` when
52
+ * omitted, preserving the pre-fix (buggy) behavior for any caller that
53
+ * hasn't upgraded — but main.js always passes fold.js's real isFolding().
54
+ * @param {() => boolean} [deps.hasCompleted] - Whether the fold's
55
+ * `[SIDECAR_FOLD]` stdout write has actually succeeded. Falls back to
56
+ * `hasFolded()` when omitted.
57
+ * @param {(mainWindow: object, contentView: object) => Promise<void>} deps.triggerFold
58
+ * - The SAME fold.js closure used by the shortcut/toolbar/IPC paths.
59
+ * @returns {{ handleClose: (event: object, mainWindow: object, contentView: object) => void }}
60
+ */
61
+ function createCloseGuard({ hasFolded, isFolding, hasCompleted, triggerFold }) {
62
+ const checkIsFolding = isFolding || hasFolded;
63
+ const checkHasCompleted = hasCompleted || hasFolded;
64
+
65
+ // Tracks whether THIS guard has already kicked off a close-initiated fold
66
+ // attempt (in flight or settled-but-not-yet-fallen-back). Separate from
67
+ // fold.js's own `folded` re-entrancy flag: this latch additionally prevents
68
+ // a second fold attempt or a preventDefault loop around the fallback
69
+ // destroy once the close-initiated fold has failed.
70
+ let closeFoldAttempted = false;
71
+ let fallbackFired = false;
72
+
73
+ function destroyIfPossible(mainWindow) {
74
+ if (fallbackFired) { return; }
75
+ fallbackFired = true;
76
+ if (mainWindow && !mainWindow.isDestroyed()) {
77
+ mainWindow.destroy();
78
+ }
79
+ }
80
+
81
+ function handleClose(event, mainWindow, contentView) {
82
+ if (checkHasCompleted()) {
83
+ // Fold already completed — proceed exactly like the pre-existing
84
+ // behavior (no interception, no destroy call from the guard itself;
85
+ // Electron's default close-then-'closed' sequence runs normally).
86
+ return;
87
+ }
88
+
89
+ if (fallbackFired) {
90
+ // The close-initiated fold already failed/timed out and the fallback
91
+ // destroy has already fired (or is in flight). Do not re-intercept —
92
+ // let this close event proceed so an already-invoked destroy()
93
+ // re-entering 'close', or a straggling user click, cannot loop.
94
+ return;
95
+ }
96
+
97
+ // Not completed yet — either no fold has been attempted, or one is
98
+ // in flight (close-initiated or toolbar/shortcut-initiated). Either way
99
+ // the destroy must be prevented so an in-flight summary is never
100
+ // discarded out from under it.
101
+ event.preventDefault();
102
+
103
+ if (checkIsFolding() || closeFoldAttempted) {
104
+ // A fold is already in flight (from this guard's own close-initiated
105
+ // attempt, or from the toolbar/shortcut path) — do not spawn a second
106
+ // one. Let it finish; its own success path closes the window, and if
107
+ // it settles without completing, the .then() below falls back to
108
+ // destroy so the window never gets permanently stuck open.
109
+ return;
110
+ }
111
+ closeFoldAttempted = true;
112
+
113
+ Promise.resolve(triggerFold(mainWindow, contentView)).then(() => {
114
+ // triggerFold can RESOLVE without ever calling mainWindow.close() —
115
+ // its outer catch swallows failures (including a synchronous throw
116
+ // from the post-write nudge-overlay executeJavaScript call, which can
117
+ // land AFTER the stdout write already flipped completed=true) and
118
+ // returns normally instead of rejecting. fold.js's catch always resets
119
+ // its `folded` flag back to false in that path, and only in that path
120
+ // — the success path that actually reaches mainWindow.close() leaves
121
+ // `folded` true. So hasFolded() being false here is the reliable
122
+ // "settled without the window actually closing" signal, independent
123
+ // of hasCompleted() (the summary can be safely on stdout while the
124
+ // window itself never got closed). Skipping this check and trusting
125
+ // completion alone would leave the window with no further close
126
+ // handler able to fire — permanently open.
127
+ if (!hasFolded()) {
128
+ destroyIfPossible(mainWindow);
129
+ }
130
+ }, () => {
131
+ // Close-initiated fold rejected outright. Never trap the user: fall
132
+ // back to the original destroy path.
133
+ destroyIfPossible(mainWindow);
134
+ });
135
+ }
136
+
137
+ return { handleClose };
138
+ }
139
+
140
+ module.exports = { createCloseGuard };
package/electron/fold.js CHANGED
@@ -19,14 +19,23 @@ const { tokenCss } = require('../src/design/tokens');
19
19
  * @param {string} state.sessionId - OpenCode session ID
20
20
  * @param {string} state.taskId - Sidecar task ID
21
21
  * @param {number} state.port - OpenCode server port
22
- * @returns {{ triggerFold: Function, hasFolded: Function }}
22
+ * @returns {{ triggerFold: Function, hasFolded: Function, isFolding: Function, hasCompleted: Function }}
23
23
  */
24
24
  function createFoldHandler(state) {
25
+ // `folded` is set synchronously at triggerFold ENTRY and covers both
26
+ // "in flight" and "done" — this is `hasFolded()`'s existing external
27
+ // contract (main.js wires it straight into createCloseGuard's `hasFolded`
28
+ // dep) and must not change. `completed` is the finer-grained signal: it
29
+ // only flips true AFTER the `[SIDECAR_FOLD]` stdout write actually
30
+ // succeeds, so callers that need to distinguish "still running" from
31
+ // "actually done" (close-guard.js) use isFolding()/hasCompleted() instead.
25
32
  let folded = false;
33
+ let completed = false;
26
34
 
27
35
  async function triggerFold(mainWindow, contentView) {
28
36
  if (folded) { return; }
29
37
  folded = true;
38
+ completed = false;
30
39
 
31
40
  // Show fold progress in toolbar and content overlay
32
41
  showFoldOverlay(mainWindow, contentView);
@@ -54,6 +63,7 @@ function createFoldHandler(state) {
54
63
  ].join('\n');
55
64
 
56
65
  process.stdout.write(output + '\n');
66
+ completed = true;
57
67
  logger.info('Fold completed', { taskId: state.taskId });
58
68
 
59
69
  // Show nudge overlay before closing
@@ -76,6 +86,17 @@ function createFoldHandler(state) {
76
86
  await new Promise(resolve => setTimeout(resolve, 2500));
77
87
  } catch (err) {
78
88
  logger.error('Fold failed', { error: err.message });
89
+ // Reset re-entrancy so a later close/shortcut/toolbar click can retry.
90
+ // NOTE: this can fire AFTER the `[SIDECAR_FOLD]` stdout write already
91
+ // succeeded (completed = true, set above) if the post-write nudge-overlay
92
+ // executeJavaScript call throws SYNCHRONOUSLY on a destroyed webContents
93
+ // (the trailing .catch() on that call only guards promise rejection, not
94
+ // a synchronous throw) — the summary is safely on stdout in that case,
95
+ // but the window was never closed by this run, so folded must still
96
+ // reset to let a fallback close proceed. completed is left as-is:
97
+ // close-guard.js checks it independently to decide whether a pending
98
+ // close can simply proceed (summary already landed) or must fall back
99
+ // to destroy (summary never made it out).
79
100
  folded = false;
80
101
  return;
81
102
  }
@@ -93,7 +114,23 @@ function createFoldHandler(state) {
93
114
  return folded;
94
115
  }
95
116
 
96
- return { triggerFold, hasFolded };
117
+ // isFolding(): true only while a fold is IN FLIGHT (entered but not yet
118
+ // completed) — the signal close-guard.js needs to keep a close blocked
119
+ // regardless of who initiated the fold (close-initiated or
120
+ // toolbar/shortcut-initiated), since hasFolded() alone can't distinguish
121
+ // in-flight from done.
122
+ function isFolding() {
123
+ return folded && !completed;
124
+ }
125
+
126
+ // hasCompleted(): true once (and only once) the `[SIDECAR_FOLD]` stdout
127
+ // write has actually succeeded — the signal close-guard.js needs to know
128
+ // the summary is safely handed off and a close may proceed.
129
+ function hasCompleted() {
130
+ return completed;
131
+ }
132
+
133
+ return { triggerFold, hasFolded, isFolding, hasCompleted };
97
134
  }
98
135
 
99
136
  /**
package/electron/main.js CHANGED
@@ -18,6 +18,7 @@ const { logger } = require('../src/utils/logger');
18
18
  const { getCompatEnv } = require('../src/utils/env-compat');
19
19
  const { buildToolbarHTML, TOOLBAR_H, getBrandName } = require('./toolbar');
20
20
  const { createFoldHandler } = require('./fold');
21
+ const { createCloseGuard } = require('./close-guard');
21
22
  const { registerSetupHandlers } = require('./ipc-setup');
22
23
  const { computeWindowPosition } = require('./window-position');
23
24
  const { attachLoadFailsafe, buildLoadErrorHTML } = require('./load-failsafe');
@@ -90,6 +91,15 @@ const foldHandler = createFoldHandler({
90
91
  taskId: TASK_ID,
91
92
  port: OPENCODE_PORT
92
93
  });
94
+ // Auto-fold on close (backlog B01): a user-initiated window close with no
95
+ // fold yet run must not silently discard the session summary. See
96
+ // close-guard.js for the full design (latch, fallback, abort isolation).
97
+ const closeGuard = createCloseGuard({
98
+ hasFolded: foldHandler.hasFolded,
99
+ isFolding: foldHandler.isFolding,
100
+ hasCompleted: foldHandler.hasCompleted,
101
+ triggerFold: foldHandler.triggerFold,
102
+ });
93
103
 
94
104
  // ============================================================================
95
105
  // Amicus Window (OpenCode + Toolbar)
@@ -284,8 +294,8 @@ function createAmicusWindow() {
284
294
  }, 500);
285
295
  }
286
296
 
287
- mainWindow.on('close', () => {
288
- if (!foldHandler.hasFolded() && mainWindow) { mainWindow.destroy(); }
297
+ mainWindow.on('close', (event) => {
298
+ closeGuard.handleClose(event, mainWindow, contentView);
289
299
  });
290
300
  mainWindow.on('closed', () => {
291
301
  mainWindow = null;
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "amicus",
3
- "version": "1.8.0",
3
+ "version": "1.9.0",
4
+ "mcpName": "io.github.BourbonDog/amicus",
4
5
  "description": "Multi-model LLM Council + parallel AI window for Claude Code. Run structured council reviews across Gemini, GPT, DeepSeek and more — or fork a conversation to any model and fold the results back.",
5
6
  "keywords": [
6
7
  "claude",
@@ -40,6 +41,7 @@
40
41
  "src/",
41
42
  "electron/",
42
43
  "skills/",
44
+ "commands/",
43
45
  ".claude-plugin/",
44
46
  "CHANGELOG.md",
45
47
  "scripts/postinstall.js",