amicus 4.5.3 → 4.6.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.
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +97 -0
- package/README.md +41 -13
- package/commands/council.md +1 -1
- package/docs/DISTRIBUTION.md +38 -11
- package/docs/usage.md +1 -1
- package/package.json +3 -2
- package/schemas/council-run.schema.json +20 -0
- package/schemas/council-verdict.schema.json +20 -0
- package/schemas/doctor.schema.json +23 -1
- package/src/cli-council-run-render.js +51 -0
- package/src/cli-handlers-council-run.js +45 -44
- package/src/cli-handlers-council.js +9 -3
- package/src/cli-handlers-doctor.js +16 -37
- package/src/cli-handlers-watch.js +1 -1
- package/src/cli.js +1 -1
- package/src/council/ledger.js +5 -1
- package/src/council/report-html.js +16 -1
- package/src/council/report.js +25 -1
- package/src/council/run-assemble.js +25 -7
- package/src/council/run-budget.js +14 -8
- package/src/council/run-chair.js +21 -4
- package/src/council/run-debate-stage.js +115 -0
- package/src/council/run-degrade.js +44 -0
- package/src/council/run-finalize.js +18 -3
- package/src/council/run-server.js +24 -7
- package/src/council/run-stage2.js +10 -2
- package/src/council/run-stages.js +23 -21
- package/src/council/run.js +39 -67
- package/src/council/verdict.js +74 -8
- package/src/mcp-council-bench.js +45 -0
- package/src/mcp-council-run.js +11 -28
- package/src/mcp-server.js +5 -1
- package/src/mcp-tools.js +8 -0
- package/src/utils/degrade.js +68 -0
- package/src/utils/doctor-degrade.js +51 -0
- package/src/utils/doctor-electron-mcp-check.js +64 -5
- package/src/utils/doctor-engine-check.js +14 -3
- package/src/utils/doctor-mcp-checks.js +10 -3
- package/src/utils/known-flags.js +2 -1
- package/src/utils/remediation-hints.js +5 -3
- package/src/utils/result-schema.js +6 -2
- package/src/utils/session-index-tmp-sweep.js +2 -1
- package/src/workspace/run-scan.js +5 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "amicus",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.6.0",
|
|
4
4
|
"description": "Multi-model LLM Council + parallel AI window for Claude Code. Run structured council reviews across Gemini, GPT, DeepSeek and more — or fork a conversation to any model and fold the results back.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Christian Wagner"
|
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,103 @@
|
|
|
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
|
+
## [Unreleased]
|
|
7
|
+
|
|
8
|
+
## [4.6.0] - 2026-08-02
|
|
9
|
+
|
|
10
|
+
### Added (v4.6 milestone — the degrade announcement invariant, plans 1-4)
|
|
11
|
+
|
|
12
|
+
- **The ten-channel degrade announcement contract.** A council run can no longer degrade its
|
|
13
|
+
exit code without announcing what was lost: every loss routes through one sink
|
|
14
|
+
(`src/council/run-degrade.js`, the only code allowed to flip `degraded.value` — enforced by a
|
|
15
|
+
source-scan invariant test) and lands with mandatory *what/why/effect* on stderr,
|
|
16
|
+
`run.json.degrades[]`, `verdict.json.degrades[]`, and the report's new **"What was lost"**
|
|
17
|
+
section, all rendered in one voice.
|
|
18
|
+
- **`verdict.seatLoss` is now derived from the degrade records** (closes #84) — a dead critic
|
|
19
|
+
*leg* finally flips `criticSeated`, and `seatLoss` can no longer disagree with `degrades[]`.
|
|
20
|
+
The v4.5.2 seatLoss shape is unchanged (its tests passed byte-unedited).
|
|
21
|
+
- **Stage-2 judge legs get `runStats` rows** (closes #83) — per-leg cost attribution for ~38%
|
|
22
|
+
of a run's spend that had none, judge-tagged in the report's cost table.
|
|
23
|
+
- **`doctor` speaks the same language**: `doctor --json` gains additive `degrades[]`;
|
|
24
|
+
`doctor --fix` prints `Recovered:` lines for every repair; the engine hints state causes as
|
|
25
|
+
**unverified** instead of asserting an antivirus guess.
|
|
26
|
+
- **The Workspace is discoverable from the CLI**: `watch` usage names `--ui` (closes #80), and
|
|
27
|
+
a CLI council run with Electron present prints how to open the live Workspace (closes #81 —
|
|
28
|
+
the silence half; auto-open parity remains a product decision).
|
|
29
|
+
|
|
30
|
+
### Changed (v4.6 — deliberate behavior changes)
|
|
31
|
+
|
|
32
|
+
- **A dropped preset member now degrades the run to exit 2 on every transport** (was: exit 0
|
|
33
|
+
with a `--json`-blind stderr notice). The loss is announced per-member with its reason.
|
|
34
|
+
- **A shared-server acquisition failure now exits degraded (2)** (was: stderr + run.json only,
|
|
35
|
+
exit 0) — the per-wave fallback is the racy configuration and the run says so.
|
|
36
|
+
- **Reported cost totals rise** for identical runs versus v4.5.x: judge legs now appear in
|
|
37
|
+
`runStats`. Consumers keying `runStats` by model must exclude `role: 'judge'` (as the
|
|
38
|
+
ledger's reliability join now does).
|
|
39
|
+
- In-run degrade notices hedge the exit-code claim truthfully ("will exit degraded (2)");
|
|
40
|
+
`engineMissing`/`reinstallEngineAv` hint prose changed to the unverified voice (commands
|
|
41
|
+
byte-identical).
|
|
42
|
+
|
|
43
|
+
### Fixed (v4.6)
|
|
44
|
+
|
|
45
|
+
- **A dead Stage-1 leg was announced on no surface at all** (closes #85) — the only trace was
|
|
46
|
+
its absence from the stage entry's `taskIds`. Now named everywhere, with a regression pin.
|
|
47
|
+
- **The Stage-5 verdict rebuild silently destroyed `seatLoss`/`degrades[]`** (closes #87) —
|
|
48
|
+
`tally.json` carries neither, so the decisions flow dropped both; now preserved from the run
|
|
49
|
+
folder's verdict the same way the chair's synthesis already was, on both CLI and MCP.
|
|
50
|
+
- **`watch --ui` against an `--out-dir` run failed with a symptom, not a cause** (closes #82) —
|
|
51
|
+
the error now names the launch-directory pointer and the working invocation.
|
|
52
|
+
|
|
53
|
+
### Fixed
|
|
54
|
+
|
|
55
|
+
- **`/amicus:council` lost all of its frontmatter at load time.** `commands/council.md`'s
|
|
56
|
+
`argument-hint` value began with `[material, path, or URL] [...]`, which YAML reads as a flow
|
|
57
|
+
sequence followed by a second, unexpected `[` — the whole block failed to parse, so the command
|
|
58
|
+
loaded with empty metadata: no description, no argument hint, and `disable-model-invocation:
|
|
59
|
+
true` silently dropped (the command was model-invocable, the opposite of the intent). The value
|
|
60
|
+
is now single-quoted. Present since `3900429` (Phase 9a, 2026-07-02); `claude plugin validate
|
|
61
|
+
.claude-plugin/plugin.json` failed on it, non-strict, that entire time.
|
|
62
|
+
- **The preflight that should have caught it was validating the wrong file.** With `.claude-plugin/`
|
|
63
|
+
holding both manifests, `claude plugin validate .` resolves the *marketplace* manifest and
|
|
64
|
+
reports `✔ Validation passed` without ever inspecting the plugin. `docs/DISTRIBUTION.md` §2 now
|
|
65
|
+
documents the path trap, prescribes `claude plugin validate .claude-plugin/plugin.json`, and
|
|
66
|
+
records why `--strict` is expected to fail here (the deliberately retained root-`CLAUDE.md`
|
|
67
|
+
warning).
|
|
68
|
+
|
|
69
|
+
### Changed
|
|
70
|
+
|
|
71
|
+
- **`tests/plugin-commands.test.js` now YAML-parses frontmatter** for `commands/council.md` and
|
|
72
|
+
both skills, asserting `description`, `argument-hint` (as a *string*), and
|
|
73
|
+
`disable-model-invocation: true` survive parsing. The previous
|
|
74
|
+
`expect(md).toContain('argument-hint:')` substring checks passed happily against a file that
|
|
75
|
+
could not parse. `yaml` added as a devDependency for this.
|
|
76
|
+
|
|
77
|
+
## [4.5.4] - 2026-08-01
|
|
78
|
+
|
|
79
|
+
### Fixed
|
|
80
|
+
|
|
81
|
+
- **README: corrected a false claim about Electron and install channels.** The install section
|
|
82
|
+
said "the standalone Electron window is npm-only." That is not true — the Council Workspace
|
|
83
|
+
auto-open gate (`src/sidecar/workspace-auto-open.js`) keys on `client === 'code-local'` plus
|
|
84
|
+
Electron presence, **not** on install channel, so a plugin-channel user in Claude Code local
|
|
85
|
+
does get the window. Removed.
|
|
86
|
+
|
|
87
|
+
### Changed
|
|
88
|
+
|
|
89
|
+
- **README now leads with npm as the recommended install**, with a per-channel comparison table.
|
|
90
|
+
The accurate reason npm is preferable for the interactive experience: the plugin's MCP config
|
|
91
|
+
sets `AMICUS_SKIP_POSTINSTALL=1`, and `scripts/postinstall.js` returns early on that — *before*
|
|
92
|
+
`provisionElectron()`. So the plugin channel gets no `amicus` on `PATH` (every window-opening
|
|
93
|
+
command becomes an `npx` call), no Electron provisioning or cache-heal, no reachable
|
|
94
|
+
`amicus doctor --fix` when the GUI breaks, and a fresh npx cache directory on every release.
|
|
95
|
+
The plugin block keeps its genuine strengths — native registration and the slash commands the
|
|
96
|
+
npm paths don't have — alongside an accurate statement of the tradeoff.
|
|
97
|
+
- **README documents the single-MCP-registration behavior** when both channels are installed.
|
|
98
|
+
Config, API keys, and session history are shared, but the MCP server is one registration named
|
|
99
|
+
`amicus` that resolves to whichever install registered most recently — so the copy the CLI runs
|
|
100
|
+
and the copy Claude's MCP tools run can differ. This is the #76 confusion; `amicus doctor`
|
|
101
|
+
reports the MCP launch path and `--fix` repairs that copy in place.
|
|
102
|
+
|
|
6
103
|
## [4.5.3] - 2026-08-01
|
|
7
104
|
|
|
8
105
|
### Fixed
|
package/README.md
CHANGED
|
@@ -169,26 +169,40 @@ Any flag you also type on that second line overrides just that value — a pack
|
|
|
169
169
|
|
|
170
170
|
> **Two install channels — read this first.** Amicus ships two ways, and CLI commands look different in each:
|
|
171
171
|
>
|
|
172
|
-
> - **npm global** (`npm install -g amicus` or the install script)
|
|
172
|
+
> - **npm global** (`npm install -g amicus` or the install script) — **the recommended path.** Puts `amicus`/`am` on your `PATH`, so every `amicus <command>` example in this README works as written, and provisions the Electron GUI that the parallel window runs in.
|
|
173
173
|
> - **Claude Code plugin** (`/plugin install amicus@bourbondog-amicus`) does **not** put a CLI on your `PATH`. CLI calls go through `npx -y amicus@latest <command>` instead — e.g. `amicus doctor` becomes `npx -y amicus@latest doctor`. In exchange, the plugin channel gets two things npm does **not**: the slash commands `/amicus:council` and `/amicus:sidecar`. **These are plugin-channel-ONLY — npm users don't get them** and drive the same skills by saying "council review this" / talking to Claude instead.
|
|
174
174
|
>
|
|
175
|
+
> See the [comparison table below](#1-install) for the full tradeoff — the short version is that npm is what you want for the interactive window, and the two can be installed side by side.
|
|
176
|
+
>
|
|
175
177
|
> **Convention used throughout this README:** plugin-channel users: prefix CLI examples with `npx -y amicus@latest` (skip the bare `amicus`/`am`). Individual code blocks are not duplicated per channel — this note is the one translation you need.
|
|
176
178
|
|
|
177
179
|
### 1. Install
|
|
178
180
|
|
|
179
|
-
|
|
181
|
+
Every path delivers the MCP server and both skills. They differ in what else you get:
|
|
180
182
|
|
|
181
|
-
|
|
183
|
+
| | **npm / install script** | **Claude Code plugin** |
|
|
184
|
+
|---|---|---|
|
|
185
|
+
| `amicus` / `am` on your `PATH` | ✅ | ❌ — every call is `npx -y amicus@latest <command>` |
|
|
186
|
+
| **Interactive Electron window** (`amicus start`, `watch --ui`) | ✅ provisioned at install | ⚠️ best-effort — see below |
|
|
187
|
+
| Self-heal when the GUI breaks (`amicus doctor --fix`) | ✅ | ❌ no CLI to run it with |
|
|
188
|
+
| MCP server + both skills | ✅ | ✅ |
|
|
189
|
+
| Slash commands `/amicus:council`, `/amicus:sidecar` | ❌ | ✅ |
|
|
182
190
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
#### With npm — recommended
|
|
194
|
+
|
|
195
|
+
The canonical path, and the one that gets you the full interactive experience (needs [Node.js](https://nodejs.org) ≥ 18):
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
npm install -g amicus
|
|
187
199
|
```
|
|
188
200
|
|
|
189
|
-
|
|
201
|
+
This is the path to pick unless you specifically want the plugin's slash commands. It puts `amicus`/`am` on your `PATH` — which is what the **parallel window** is driven by — and its postinstall provisions the Electron GUI, with `amicus doctor --fix` to repair it in place if anything goes wrong later.
|
|
202
|
+
|
|
203
|
+
#### With the install script
|
|
190
204
|
|
|
191
|
-
|
|
205
|
+
Same result as npm, one command — macOS, Linux, or Windows (needs [Node.js](https://nodejs.org) ≥ 18):
|
|
192
206
|
|
|
193
207
|
```bash
|
|
194
208
|
# macOS / Linux
|
|
@@ -200,12 +214,26 @@ curl -fsSL https://raw.githubusercontent.com/BourbonDog/amicus/main/install.sh |
|
|
|
200
214
|
irm https://raw.githubusercontent.com/BourbonDog/amicus/main/install.ps1 | iex
|
|
201
215
|
```
|
|
202
216
|
|
|
203
|
-
|
|
217
|
+
#### As a Claude Code plugin
|
|
204
218
|
|
|
205
|
-
|
|
206
|
-
|
|
219
|
+
The most native *registration* path if you use Claude Code, and the only one with slash commands:
|
|
220
|
+
|
|
221
|
+
```text
|
|
222
|
+
/plugin marketplace add BourbonDog/amicus
|
|
223
|
+
/plugin install amicus@bourbondog-amicus
|
|
224
|
+
/reload-plugins
|
|
207
225
|
```
|
|
208
226
|
|
|
227
|
+
Claude Code registers the MCP server and both skills for you — nothing to configure. You also get **`/amicus:council`** (run a full council review) and **`/amicus:sidecar`** (fork a conversation to another model), which the npm paths don't have.
|
|
228
|
+
|
|
229
|
+
> **Know the tradeoff before you pick this.** The plugin does not put `amicus` on your `PATH`, so every CLI call goes through `npx -y amicus@latest <command>` — including the ones that open the interactive window. It also skips amicus's postinstall, which is what provisions and self-heals the Electron GUI. The window still works when Electron lands in the npx cache, and the Council Workspace still auto-opens on a council run from Claude Code — but nothing repairs it when Electron *doesn't* land, and each new release re-resolves into a fresh cache directory. **If you want the parallel window as a daily driver, install with npm.**
|
|
230
|
+
>
|
|
231
|
+
> (Also: the first council/sidecar call downloads the OpenCode engine.)
|
|
232
|
+
|
|
233
|
+
**Running both is supported** — and is what you want if you like the slash commands *and* the window. Install with npm for the CLI and the GUI, then add the plugin for `/amicus:council`. Your config, API keys, and session history live outside either install and are shared automatically.
|
|
234
|
+
|
|
235
|
+
> One thing to know if you do: the MCP server is a **single registration named `amicus`**, so it resolves to one install — whichever registered most recently, which is usually the plugin's `npx -y amicus@latest mcp`. That's harmless (both serve the same tools), but it means the copy your CLI runs and the copy Claude's MCP tools run can differ. `amicus doctor` reports the MCP launch path explicitly and `--fix` repairs that copy in place, so if a GUI or engine problem ever shows up in Claude but not in your terminal, that's the first thing to check.
|
|
236
|
+
|
|
209
237
|
For the **npm** and **install-script** paths, a postinstall auto-configures everything — no manual registration:
|
|
210
238
|
|
|
211
239
|
- Registers the **MCP server** in Claude Code and in Claude Desktop / Cowork, so the Amicus tools appear natively.
|
|
@@ -413,7 +441,7 @@ $ amicus status demo123 --json
|
|
|
413
441
|
"taskId": "demo123",
|
|
414
442
|
"status": "complete",
|
|
415
443
|
"elapsed": "5m 0s",
|
|
416
|
-
"version": "4.
|
|
444
|
+
"version": "4.6.0",
|
|
417
445
|
"model": "google/gemini-2.5-flash",
|
|
418
446
|
"phase": "terminal"
|
|
419
447
|
}
|
package/commands/council.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
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] [optional elements — e.g. "with a critic seat", "debate mode", "expert lenses", "chair verdict scale"]
|
|
3
|
+
argument-hint: '[material, path, or URL] [analysis request + criteria] [optional elements — e.g. "with a critic seat", "debate mode", "expert lenses", "chair verdict scale"]'
|
|
4
4
|
disable-model-invocation: true
|
|
5
5
|
---
|
|
6
6
|
|
package/docs/DISTRIBUTION.md
CHANGED
|
@@ -50,17 +50,36 @@ users only see a version bump when we bump it, not on every commit. Treat
|
|
|
50
50
|
### Preflight (run before every submission or major post-listing update)
|
|
51
51
|
|
|
52
52
|
```bash
|
|
53
|
-
claude plugin validate .
|
|
53
|
+
claude plugin validate .claude-plugin/plugin.json # the PLUGIN check — see the path trap below
|
|
54
|
+
claude plugin validate . # the marketplace manifest (separate check)
|
|
54
55
|
claude --plugin-dir . # smoke: /amicus:council, /amicus:sidecar, /amicus:second-opinion, MCP tools
|
|
55
56
|
npm test
|
|
56
57
|
```
|
|
57
58
|
|
|
58
|
-
- `claude plugin validate
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
59
|
+
- **Path trap — `claude plugin validate .` does NOT validate the plugin.**
|
|
60
|
+
This repo's `.claude-plugin/` holds *both* `marketplace.json` and
|
|
61
|
+
`plugin.json`; given `.`, the CLI resolves the marketplace manifest, prints
|
|
62
|
+
`Validating marketplace manifest:` / `✔ Validation passed`, and never looks
|
|
63
|
+
at the plugin, its commands, or its skills. Always pass
|
|
64
|
+
`.claude-plugin/plugin.json` explicitly for the plugin check — that is the
|
|
65
|
+
surface the review pipeline evaluates. (This masked a real defect for a
|
|
66
|
+
month: `commands/council.md` shipped an unparseable `argument-hint` from
|
|
67
|
+
2026-07-02 until 2026-08-01 while `.` reported clean. Read the first line
|
|
68
|
+
of the output — it names which manifest was actually checked.)
|
|
69
|
+
- `claude plugin validate` is the same structural check the review pipeline
|
|
70
|
+
runs. It needs the Claude Code CLI installed locally — it is **not** wired
|
|
71
|
+
into this repo's CI (runners have no `claude` auth), so
|
|
72
|
+
`tests/plugin-manifest.test.js` and `tests/plugin-commands.test.js` are the
|
|
73
|
+
CI-side proxies. The latter now YAML-parses the command and skill
|
|
74
|
+
frontmatter (`yaml` is a devDependency for exactly this); substring
|
|
75
|
+
assertions like `toContain('argument-hint:')` cannot see a parse failure,
|
|
76
|
+
which is how the `council.md` defect passed CI.
|
|
77
|
+
- **`--strict` is expected to FAIL here, by design.** It promotes the
|
|
78
|
+
root-`CLAUDE.md` warning ("not loaded as project context") to an error. The
|
|
79
|
+
plugin root is the repo root, and `CLAUDE.md` is this repo's development
|
|
80
|
+
context — we keep it deliberately. Use the non-strict run as the gate and
|
|
81
|
+
read the warning list by eye; a *new* warning is the signal, not the
|
|
82
|
+
`CLAUDE.md` one.
|
|
64
83
|
- `claude --plugin-dir .` loads the plugin from the working tree so you can
|
|
65
84
|
manually confirm `/amicus:council`, `/amicus:sidecar`, and
|
|
66
85
|
`/amicus:second-opinion` all appear in the command picker exactly once,
|
|
@@ -68,10 +87,18 @@ npm test
|
|
|
68
87
|
- `npm test` must be green, specifically `tests/plugin-manifest.test.js`
|
|
69
88
|
and `tests/plugin-commands.test.js`.
|
|
70
89
|
|
|
71
|
-
**Known current-tree preflight result (checked 2026-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
90
|
+
**Known current-tree preflight result (checked 2026-08-01, on
|
|
91
|
+
`fix/plugin-frontmatter-validation`):**
|
|
92
|
+
`claude plugin validate .claude-plugin/plugin.json` → exit 0,
|
|
93
|
+
`✔ Validation passed with warnings` (the one retained root-`CLAUDE.md`
|
|
94
|
+
warning). With `--strict` → exit 1 on that same warning, as documented above.
|
|
95
|
+
`claude plugin validate .` → exit 0 on the marketplace manifest.
|
|
96
|
+
|
|
97
|
+
History of this line: it previously read "`claude plugin validate . --strict`
|
|
98
|
+
passes clean, exit 0" as of 2026-07-02 — measured against the *marketplace*
|
|
99
|
+
manifest via the path trap above, while the plugin itself had a hard
|
|
100
|
+
frontmatter error in `commands/council.md`. Older still: `--strict` flagged an
|
|
101
|
+
unknown `plugin.json → bugs` field, removed in commit `4207485`.
|
|
75
102
|
|
|
76
103
|
### Submit
|
|
77
104
|
|
package/docs/usage.md
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "amicus",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.6.0",
|
|
4
4
|
"mcpName": "io.github.BourbonDog/amicus",
|
|
5
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.",
|
|
6
6
|
"keywords": [
|
|
@@ -95,7 +95,8 @@
|
|
|
95
95
|
"lint-staged": "^16.3.2",
|
|
96
96
|
"puppeteer": "^24.36.0",
|
|
97
97
|
"sharp": "^0.35.3",
|
|
98
|
-
"ws": "^8.19.0"
|
|
98
|
+
"ws": "^8.19.0",
|
|
99
|
+
"yaml": "^2.9.0"
|
|
99
100
|
},
|
|
100
101
|
"engines": {
|
|
101
102
|
"node": ">=22.12.0"
|
|
@@ -82,6 +82,26 @@
|
|
|
82
82
|
"at": { "type": "string" }
|
|
83
83
|
}
|
|
84
84
|
},
|
|
85
|
+
"degrades": {
|
|
86
|
+
"description": "v4.6 Plan 1: every degrade/heal the sink announced — additive, written by run-degrade.js.",
|
|
87
|
+
"type": "array",
|
|
88
|
+
"items": {
|
|
89
|
+
"type": "object",
|
|
90
|
+
"required": ["kind", "channel", "what", "why", "effect"],
|
|
91
|
+
"properties": {
|
|
92
|
+
"kind": { "enum": ["degrade", "heal"] },
|
|
93
|
+
"channel": { "type": "string" },
|
|
94
|
+
"what": { "type": "string" },
|
|
95
|
+
"why": { "type": "string" },
|
|
96
|
+
"effect": { "type": "string" },
|
|
97
|
+
"remedy": { "type": "string" },
|
|
98
|
+
"data": {
|
|
99
|
+
"type": "object",
|
|
100
|
+
"description": "v4.6 Plan 2: optional structured facts for derivations (e.g. dead-leg {seat,status,reason}, dead-wave {waveId,models,reason}). Prose fields stay the human surface."
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
},
|
|
85
105
|
"debate": {
|
|
86
106
|
"type": "object",
|
|
87
107
|
"properties": {
|
|
@@ -205,6 +205,26 @@
|
|
|
205
205
|
"criticRequested",
|
|
206
206
|
"criticSeated"
|
|
207
207
|
]
|
|
208
|
+
},
|
|
209
|
+
"degrades": {
|
|
210
|
+
"description": "v4.6 Plan 2: what this run lost — copied verbatim from the sink at verdict assembly. Additive; absent on a clean run.",
|
|
211
|
+
"type": "array",
|
|
212
|
+
"items": {
|
|
213
|
+
"type": "object",
|
|
214
|
+
"required": ["kind", "channel", "what", "why", "effect"],
|
|
215
|
+
"properties": {
|
|
216
|
+
"kind": { "enum": ["degrade", "heal"] },
|
|
217
|
+
"channel": { "type": "string" },
|
|
218
|
+
"what": { "type": "string" },
|
|
219
|
+
"why": { "type": "string" },
|
|
220
|
+
"effect": { "type": "string" },
|
|
221
|
+
"remedy": { "type": "string" },
|
|
222
|
+
"data": {
|
|
223
|
+
"type": "object",
|
|
224
|
+
"description": "v4.6 Plan 2: optional structured facts for derivations (e.g. dead-leg {seat,status,reason}, dead-wave {waveId,models,reason}). Prose fields stay the human surface."
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
208
228
|
}
|
|
209
229
|
}
|
|
210
230
|
}
|
|
@@ -21,7 +21,29 @@
|
|
|
21
21
|
"name": { "type": "string" },
|
|
22
22
|
"status": { "type": "string" },
|
|
23
23
|
"message": { "type": ["string", "null"] },
|
|
24
|
-
"hint": { "type": ["string", "null"] }
|
|
24
|
+
"hint": { "type": ["string", "null"] },
|
|
25
|
+
"fixed": { "type": "boolean" },
|
|
26
|
+
"fixDetail": { "type": "string" }
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"degrades": {
|
|
31
|
+
"description": "v4.6 Plan 3: failed checks and --fix repairs in the shared degrade/heal vocabulary — additive, absent when clean.",
|
|
32
|
+
"type": "array",
|
|
33
|
+
"items": {
|
|
34
|
+
"type": "object",
|
|
35
|
+
"required": ["kind", "channel", "what", "why", "effect"],
|
|
36
|
+
"properties": {
|
|
37
|
+
"kind": { "enum": ["degrade", "heal"] },
|
|
38
|
+
"channel": { "type": "string" },
|
|
39
|
+
"what": { "type": "string" },
|
|
40
|
+
"why": { "type": "string" },
|
|
41
|
+
"effect": { "type": "string" },
|
|
42
|
+
"remedy": { "type": "string" },
|
|
43
|
+
"data": {
|
|
44
|
+
"type": "object",
|
|
45
|
+
"description": "v4.6 Plan 3: optional structured facts for derivations (doctor: {checkId}). Prose fields stay the human surface."
|
|
46
|
+
}
|
|
25
47
|
}
|
|
26
48
|
}
|
|
27
49
|
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// src/cli-council-run-render.js
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @module cli-council-run-render
|
|
6
|
+
* Human-readable render of a finished `amicus council run` (the non-`--json`
|
|
7
|
+
* output path, v4.0 spec §4). Split out of cli-handlers-council-run.js (v4.6
|
|
8
|
+
* Plan 4 Task 2): that file sat at 298/300 lines and Plan 4 Task 3 adds the
|
|
9
|
+
* #81 Workspace notice, which needed the room. `renderRunHuman` is
|
|
10
|
+
* self-contained — no dependency on the handler's flag-parsing or engine
|
|
11
|
+
* dispatch — so it moves verbatim to its own leaf; the old home
|
|
12
|
+
* (cli-handlers-council-run.js) requires it back and re-exports it, so every
|
|
13
|
+
* existing import path keeps working unchanged.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
function renderRunHuman(run) {
|
|
17
|
+
const lines = [
|
|
18
|
+
`Council run ${run.runId}: ${run.status} (exit ${run.exitCode})`,
|
|
19
|
+
` bench: ${(run.bench || []).join(', ')} chair: ${run.chair}`,
|
|
20
|
+
` dir: ${run.options && run.options.outDir}`,
|
|
21
|
+
];
|
|
22
|
+
// v4.4: a cost line that omits unpriced legs reads as the whole bill. The
|
|
23
|
+
// diagnosis measured council-wsgate02 printing $0.3720 for a run that really
|
|
24
|
+
// spent $0.9859. Say what we know, then say what we cannot know — and print
|
|
25
|
+
// the line even when NOTHING resolved (the old `typeof amount === 'number'`
|
|
26
|
+
// guard silently dropped it, so a fully unpriced run looked free).
|
|
27
|
+
const u = run.usage || null;
|
|
28
|
+
const unknownLegs = u && typeof u.unknownLegs === 'number'
|
|
29
|
+
? u.unknownLegs
|
|
30
|
+
: (u && u.cost && u.cost.unpricedLegs) || 0;
|
|
31
|
+
// v4.4 Task 2: a fully-priced run can still be short. `council-wsgate01`
|
|
32
|
+
// printed an unqualified $0.2821 for a run that really spent $0.3036 — every
|
|
33
|
+
// leg `reported`, and 100% of the gap one unattributed `explore` child session.
|
|
34
|
+
const subtreeLegs = u && typeof u.subtreeUnknownLegs === 'number'
|
|
35
|
+
? u.subtreeUnknownLegs
|
|
36
|
+
: (u && u.cost && u.cost.subtreeUnknownLegs) || 0;
|
|
37
|
+
if (u && u.cost && (typeof u.cost.amount === 'number' || unknownLegs > 0 || subtreeLegs > 0)) {
|
|
38
|
+
const known = typeof u.cost.amount === 'number' ? `$${u.cost.amount.toFixed(4)}` : '$0.0000';
|
|
39
|
+
const gaps = [];
|
|
40
|
+
if (unknownLegs > 0) { gaps.push(`${unknownLegs} leg(s) unknown`); }
|
|
41
|
+
if (subtreeLegs > 0) { gaps.push(`${subtreeLegs} leg(s) with unattributed subagent child-session spend`); }
|
|
42
|
+
const tail = gaps.length > 0
|
|
43
|
+
? ` + ${gaps.join(' + ')} — real spend is at least this much`
|
|
44
|
+
: '';
|
|
45
|
+
lines.push(` cost: ${known} (${u.cost.source})${tail}`);
|
|
46
|
+
}
|
|
47
|
+
if (run.error) { lines.push(` error: ${run.error.code}: ${run.error.message}`); }
|
|
48
|
+
return lines.join('\n') + '\n';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
module.exports = { renderRunHuman };
|
|
@@ -13,6 +13,10 @@ const path = require('path');
|
|
|
13
13
|
const { failJson, buildErrorDoc, ERROR_CODES } = require('./utils/error-doc');
|
|
14
14
|
const { validateTaskId } = require('./utils/validators');
|
|
15
15
|
const { GATEWAY_MODES } = require('./utils/model-descriptor');
|
|
16
|
+
// v4.6 Plan 4 Task 2: renderRunHuman moved to its own leaf (size gate); this
|
|
17
|
+
// file re-exports it below so every existing require() of this path still
|
|
18
|
+
// resolves it unchanged.
|
|
19
|
+
const { renderRunHuman } = require('./cli-council-run-render');
|
|
16
20
|
|
|
17
21
|
const CHAIR_DEFAULT = 'deepseek';
|
|
18
22
|
|
|
@@ -40,9 +44,9 @@ function sanitizeCouncilName(name) {
|
|
|
40
44
|
|
|
41
45
|
/**
|
|
42
46
|
* Resolve bench models from --models XOR --council (mirrors handleFanout).
|
|
43
|
-
* Also returns `presetName` (v4.3 Task 3, spec §7.1
|
|
44
|
-
*
|
|
45
|
-
*
|
|
47
|
+
* Also returns `presetName` (v4.3 Task 3, spec §7.1: trimmed --council name,
|
|
48
|
+
* else null) and `droppedMembers`: a preset's own drops, or — bare --models —
|
|
49
|
+
* the parsed `--dropped-members` MCP→child passthrough (v4.6 Plan 4 Task 4b).
|
|
46
50
|
*/
|
|
47
51
|
function resolveBench(args, useJson) {
|
|
48
52
|
const hasModels = typeof args.models === 'string' && args.models.trim();
|
|
@@ -68,54 +72,40 @@ function resolveBench(args, useJson) {
|
|
|
68
72
|
if (expanded.error) {
|
|
69
73
|
return { fail: failJson(useJson, { code: ERROR_CODES.BAD_ARGS, message: `Error: ${expanded.error}` }) };
|
|
70
74
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
}
|
|
74
|
-
// v4.5 Wave 2: threaded into runCouncil's options — the ONLY prior signal
|
|
75
|
-
// was the stderr-only Notice above, which --json mode never even prints.
|
|
75
|
+
// v4.5 Wave 2 → Plan 4 Task 4: threaded into runCouncil's options — the
|
|
76
|
+
// sink now announces each dropped member, with reason, on every transport and surface.
|
|
76
77
|
return { bench: expanded.models, presetName, droppedMembers: expanded.droppedMembers || [] };
|
|
77
78
|
}
|
|
78
|
-
|
|
79
|
+
if (args['dropped-members'] === undefined) {
|
|
80
|
+
return { bench: parseList(args.models), presetName: null, droppedMembers: [] };
|
|
81
|
+
}
|
|
82
|
+
let dm; try { dm = JSON.parse(args['dropped-members']); } catch { dm = null; }
|
|
83
|
+
if (!Array.isArray(dm) || !dm.every(d => d && typeof d.member === 'string' && typeof d.reason === 'string')) {
|
|
84
|
+
return { fail: failJson(useJson, { code: ERROR_CODES.BAD_ARGS,
|
|
85
|
+
message: 'Error: --dropped-members must be a JSON array of {member, reason} entries' }) };
|
|
86
|
+
}
|
|
87
|
+
return { bench: parseList(args.models), presetName: null, droppedMembers: dm };
|
|
79
88
|
}
|
|
80
89
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
// the line even when NOTHING resolved (the old `typeof amount === 'number'`
|
|
91
|
-
// guard silently dropped it, so a fully unpriced run looked free).
|
|
92
|
-
const u = run.usage || null;
|
|
93
|
-
const unknownLegs = u && typeof u.unknownLegs === 'number'
|
|
94
|
-
? u.unknownLegs
|
|
95
|
-
: (u && u.cost && u.cost.unpricedLegs) || 0;
|
|
96
|
-
// v4.4 Task 2: a fully-priced run can still be short. `council-wsgate01`
|
|
97
|
-
// printed an unqualified $0.2821 for a run that really spent $0.3036 — every
|
|
98
|
-
// leg `reported`, and 100% of the gap one unattributed `explore` child session.
|
|
99
|
-
const subtreeLegs = u && typeof u.subtreeUnknownLegs === 'number'
|
|
100
|
-
? u.subtreeUnknownLegs
|
|
101
|
-
: (u && u.cost && u.cost.subtreeUnknownLegs) || 0;
|
|
102
|
-
if (u && u.cost && (typeof u.cost.amount === 'number' || unknownLegs > 0 || subtreeLegs > 0)) {
|
|
103
|
-
const known = typeof u.cost.amount === 'number' ? `$${u.cost.amount.toFixed(4)}` : '$0.0000';
|
|
104
|
-
const gaps = [];
|
|
105
|
-
if (unknownLegs > 0) { gaps.push(`${unknownLegs} leg(s) unknown`); }
|
|
106
|
-
if (subtreeLegs > 0) { gaps.push(`${subtreeLegs} leg(s) with unattributed subagent child-session spend`); }
|
|
107
|
-
const tail = gaps.length > 0
|
|
108
|
-
? ` + ${gaps.join(' + ')} — real spend is at least this much`
|
|
109
|
-
: '';
|
|
110
|
-
lines.push(` cost: ${known} (${u.cost.source})${tail}`);
|
|
111
|
-
}
|
|
112
|
-
if (run.error) { lines.push(` error: ${run.error.code}: ${run.error.message}`); }
|
|
113
|
-
return lines.join('\n') + '\n';
|
|
90
|
+
/**
|
|
91
|
+
* Default real helpers; tests override via depsOverride (mirrors
|
|
92
|
+
* cli-handlers-spend.js's realDeps()/depsOverride convention).
|
|
93
|
+
*/
|
|
94
|
+
function realDeps() {
|
|
95
|
+
return {
|
|
96
|
+
// #81 (spec §2): same pure presence probe doctor's electron checks use (src/cli-handlers-doctor.js).
|
|
97
|
+
getElectronPath: () => require('./sidecar/interactive-process').getElectronPath(),
|
|
98
|
+
};
|
|
114
99
|
}
|
|
115
100
|
|
|
116
|
-
/**
|
|
117
|
-
|
|
101
|
+
/**
|
|
102
|
+
* @param {object} args parsed CLI args
|
|
103
|
+
* @param {object} [depsOverride] test seam (getElectronPath)
|
|
104
|
+
* @returns {Promise<number>} exit code
|
|
105
|
+
*/
|
|
106
|
+
async function handleCouncilRun(args, depsOverride = {}) {
|
|
118
107
|
const useJson = !!args.json;
|
|
108
|
+
const deps = { ...realDeps(), ...depsOverride };
|
|
119
109
|
|
|
120
110
|
// v4.5 Task 12 (B7/F5): resolve --pack FIRST, above the Task-5 template
|
|
121
111
|
// block, so a pack-filled args.template renders through that single
|
|
@@ -243,6 +233,17 @@ async function handleCouncilRun(args) {
|
|
|
243
233
|
const { readCache } = require('./utils/model-catalog');
|
|
244
234
|
const { runCouncil } = require('./council/run');
|
|
245
235
|
const cfg = loadConfig() || {};
|
|
236
|
+
|
|
237
|
+
// #81 (spec §2): the GUI's existence was announced on NO surface from the
|
|
238
|
+
// CLI path — MCP launches auto-open, the CLI stayed silent. Auto-open
|
|
239
|
+
// parity is a product decision (deliberately not taken here); the SILENCE
|
|
240
|
+
// is the spec's to fix. Presence probe only — never launches. Placed here
|
|
241
|
+
// (runId/runDir already resolved, still before the engine await) so the
|
|
242
|
+
// notice is useful WHILE the run is live, not just after it finishes.
|
|
243
|
+
if (!useJson && deps.getElectronPath()) {
|
|
244
|
+
process.stderr.write(`Notice: the Council Workspace can render this run live — open it with: amicus watch ${runId} --ui\n`);
|
|
245
|
+
}
|
|
246
|
+
|
|
246
247
|
const { exitCode, run } = await runCouncil({
|
|
247
248
|
briefing: promptRes.prompt, models: bench, chair, critic, lenses,
|
|
248
249
|
project, runId, runDir,
|
|
@@ -8,7 +8,7 @@ const { sumWaveUsage, formatCost } = require('./utils/pricing');
|
|
|
8
8
|
const { failJson, ERROR_CODES } = require('./utils/error-doc');
|
|
9
9
|
const { buildReport } = require('./council/report');
|
|
10
10
|
const { validateFindings, buildValidateDoc } = require('./council/findings');
|
|
11
|
-
const { buildVerdict, readOverallVerdict, writeVerdictAtomic } = require('./council/verdict');
|
|
11
|
+
const { buildVerdict, readOverallVerdict, readPriorVerdictSurfaces, writeVerdictAtomic } = require('./council/verdict');
|
|
12
12
|
const {
|
|
13
13
|
runSave: runCouncilSave,
|
|
14
14
|
runList: runCouncilList,
|
|
@@ -169,8 +169,14 @@ function runVerdict(args, useJson) {
|
|
|
169
169
|
// one of only two homes of the chair's synthesis (the other is
|
|
170
170
|
// chair-output.md); tally.json carries no copy. Recover it from the RUN
|
|
171
171
|
// folder — the tally's own directory, not `-o` — before rebuilding.
|
|
172
|
-
const
|
|
173
|
-
|
|
172
|
+
const runDir = path.dirname(path.resolve(tallyPath));
|
|
173
|
+
const overallVerdict = readOverallVerdict(runDir, record.meta.runId);
|
|
174
|
+
// #87: tally.json carries neither seatLoss nor degrades — recover both from
|
|
175
|
+
// the run folder's verdict the same way the chair line is recovered.
|
|
176
|
+
const prior = readPriorVerdictSurfaces(runDir, record.meta.runId);
|
|
177
|
+
verdict = buildVerdict(record, decisions, { overallVerdict,
|
|
178
|
+
...(prior.seatLoss ? { seatLoss: prior.seatLoss } : {}),
|
|
179
|
+
...(prior.degrades ? { degrades: prior.degrades } : {}) });
|
|
174
180
|
}
|
|
175
181
|
catch (e) {
|
|
176
182
|
return failJson(useJson, { code: ERROR_CODES.BAD_ARGS, message: `cannot build verdict: ${e.message}`,
|