amicus 4.5.4 → 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 +71 -0
- package/README.md +1 -1
- 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,77 @@
|
|
|
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
|
+
|
|
6
77
|
## [4.5.4] - 2026-08-01
|
|
7
78
|
|
|
8
79
|
### Fixed
|
package/README.md
CHANGED
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}`,
|
|
@@ -171,40 +171,8 @@ async function runDoctorChecks(depsOverride = {}) {
|
|
|
171
171
|
checks.push(await guardAsync('electron-mcp', 'Electron (MCP launch path)',
|
|
172
172
|
() => electronMcpCheck.evaluateElectronMcp({ ...d, fixTimeoutMs: FIX_TIMEOUT_MS })));
|
|
173
173
|
|
|
174
|
-
checks.push(await guardAsync('electron', 'Electron (interactive GUI)',
|
|
175
|
-
|
|
176
|
-
return { id: 'electron', name: 'Electron (interactive GUI)', status: 'ok', message: 'installed', hint: null };
|
|
177
|
-
}
|
|
178
|
-
// Broken (missing / quarantined). With --fix, self-heal in place (#56):
|
|
179
|
-
// repairElectron provisions the binary; {deferred} (no cache, no network)
|
|
180
|
-
// maps to WARN — a deferred download is not a failure. Without --fix, just
|
|
181
|
-
// point the user at `amicus doctor --fix`.
|
|
182
|
-
if (d.fix) {
|
|
183
|
-
let res;
|
|
184
|
-
try {
|
|
185
|
-
res = await d.repairElectron({ timeoutMs: FIX_TIMEOUT_MS });
|
|
186
|
-
} catch (e) {
|
|
187
|
-
return { id: 'electron', name: 'Electron (interactive GUI)', status: 'warn', message: `repair failed: ${e.message} — headless still works`, hint: HINTS.doctorFix };
|
|
188
|
-
}
|
|
189
|
-
res = res || {};
|
|
190
|
-
if (res.repaired) {
|
|
191
|
-
return { id: 'electron', name: 'Electron (interactive GUI)', status: 'ok', message: 'installed (self-healed)', hint: null };
|
|
192
|
-
}
|
|
193
|
-
const why = res.reason ? ` — ${res.reason}` : '';
|
|
194
|
-
// Quarantine (AV deleted electron.exe post-extract) is NOT a deferral and
|
|
195
|
-
// must NEVER be silently retried: surface the allow-list instruction as a
|
|
196
|
-
// WARN and STOP. No re-run of repairElectron here (no loop).
|
|
197
|
-
const detail = res.quarantined
|
|
198
|
-
? `antivirus quarantine${why}`
|
|
199
|
-
: res.deferred
|
|
200
|
-
? `deferred${why}`
|
|
201
|
-
: res.contended
|
|
202
|
-
? `repair already in progress${why}`
|
|
203
|
-
: `not provisioned${why}`;
|
|
204
|
-
return { id: 'electron', name: 'Electron (interactive GUI)', status: 'warn', message: `${detail} — headless still works`, hint: HINTS.doctorFix };
|
|
205
|
-
}
|
|
206
|
-
return { id: 'electron', name: 'Electron (interactive GUI)', status: 'warn', message: 'not installed — headless still works', hint: HINTS.doctorFix };
|
|
207
|
-
}));
|
|
174
|
+
checks.push(await guardAsync('electron', 'Electron (interactive GUI)',
|
|
175
|
+
() => electronMcpCheck.evaluateElectronInteractive(d, { fixTimeoutMs: FIX_TIMEOUT_MS })));
|
|
208
176
|
|
|
209
177
|
checks.push(guard('skills', 'Skills installed', () => (
|
|
210
178
|
d.skillInstalled()
|
|
@@ -256,7 +224,7 @@ async function runDoctorChecks(depsOverride = {}) {
|
|
|
256
224
|
|
|
257
225
|
const MARK = { ok: '✓', warn: '⚠', error: '✗' }; // ✓ ⚠ ✗
|
|
258
226
|
|
|
259
|
-
function renderHuman(checks) {
|
|
227
|
+
function renderHuman(checks, degrades = []) {
|
|
260
228
|
let out = 'amicus doctor\n\n';
|
|
261
229
|
for (const c of checks) {
|
|
262
230
|
out += `${MARK[c.status] || '?'} ${c.name}: ${c.message}\n`;
|
|
@@ -265,6 +233,13 @@ function renderHuman(checks) {
|
|
|
265
233
|
const errors = checks.filter(c => c.status === 'error').length;
|
|
266
234
|
const warns = checks.filter(c => c.status === 'warn').length;
|
|
267
235
|
out += `\n${errors} error(s), ${warns} warning(s).\n`;
|
|
236
|
+
// D7: every --fix repair announces what it did, in the one voice. Failures
|
|
237
|
+
// are NOT repeated here — the ✗ rows above already carry them; degrade
|
|
238
|
+
// records are the --json/artifact surface.
|
|
239
|
+
const { formatDegrade } = require('./utils/degrade');
|
|
240
|
+
for (const r of degrades.filter(x => x.kind === 'heal')) {
|
|
241
|
+
out += formatDegrade(r);
|
|
242
|
+
}
|
|
268
243
|
return out;
|
|
269
244
|
}
|
|
270
245
|
|
|
@@ -281,14 +256,18 @@ async function handleDoctor(args, runChecks = runDoctorChecks) {
|
|
|
281
256
|
// self-heal in place. Omitted (not false) when absent so the injected
|
|
282
257
|
// test-double sees a clean "no fix" call.
|
|
283
258
|
const checks = await runChecks(args.fix ? { fix: true } : undefined);
|
|
259
|
+
// v4.6 Plan 3: collect once, both paths consume — the shared degrade/heal
|
|
260
|
+
// vocabulary (spec §4/§6). Never affects the exit-code logic below.
|
|
261
|
+
const { collectDoctorDegrades } = require('./utils/doctor-degrade');
|
|
262
|
+
const degrades = collectDoctorDegrades(checks);
|
|
284
263
|
if (useJson) {
|
|
285
264
|
const { buildDoctorDoc } = require('./utils/result-schema');
|
|
286
265
|
const VERSION = require('../package.json').version;
|
|
287
|
-
const doc = buildDoctorDoc({ version: VERSION, timestamp: new Date().toISOString(), checks });
|
|
266
|
+
const doc = buildDoctorDoc({ version: VERSION, timestamp: new Date().toISOString(), checks, degrades });
|
|
288
267
|
process.stdout.write(JSON.stringify(doc, null, 2) + '\n');
|
|
289
268
|
return doc.ok ? 0 : 1;
|
|
290
269
|
}
|
|
291
|
-
process.stdout.write(renderHuman(checks));
|
|
270
|
+
process.stdout.write(renderHuman(checks, degrades));
|
|
292
271
|
return checks.some(c => c.status === 'error') ? 1 : 0;
|
|
293
272
|
}
|
|
294
273
|
|
|
@@ -104,7 +104,7 @@ async function handleWatch(args) {
|
|
|
104
104
|
const { failJson, ERROR_CODES } = require('./utils/error-doc');
|
|
105
105
|
const id = args._[1];
|
|
106
106
|
if (!id || id === true) {
|
|
107
|
-
process.stderr.write('Error: id is required for watch\nUsage: amicus watch <id> [--json] [--plain] [--interval <sec>]\n');
|
|
107
|
+
process.stderr.write('Error: id is required for watch\nUsage: amicus watch <id> [--json] [--plain] [--interval <sec>] [--ui]\n');
|
|
108
108
|
return 1;
|
|
109
109
|
}
|
|
110
110
|
const check = validateTaskId(String(id));
|
package/src/cli.js
CHANGED
|
@@ -397,7 +397,7 @@ Commands:
|
|
|
397
397
|
council verdict <tally.json> [--decisions <d.json>] [-o <out.json>] Build + write verdict.json
|
|
398
398
|
doctor Check your setup: keys, catalog, binary, skills, MCP (--json)
|
|
399
399
|
spend [--since 7d] [--json] Cross-run cost rollup from the spend ledger
|
|
400
|
-
watch <id> [--json] [--plain] [--interval <sec>] Live-render a run from any terminal
|
|
400
|
+
watch <id> [--json] [--plain] [--interval <sec>] [--ui] Live-render a run from any terminal
|
|
401
401
|
abort Abort a running session (or --all)
|
|
402
402
|
setup Configure default model and aliases
|
|
403
403
|
--api-keys Open API key setup window
|
package/src/council/ledger.js
CHANGED
|
@@ -18,7 +18,11 @@ function countSeverity(findings) {
|
|
|
18
18
|
function buildLedgerRows(record) {
|
|
19
19
|
const { meta, findings, streetCred, runStats, judged } = record;
|
|
20
20
|
const sc = new Map(streetCred.map(s => [s.model, s]));
|
|
21
|
-
|
|
21
|
+
// Judge rows (#83, v4.6 Plan 2) are Stage-2 cost attribution, not seats: the
|
|
22
|
+
// join below is keyed by MODEL, and judges ARE bench models — without this
|
|
23
|
+
// exclusion the judge row would silently overwrite the seat row.
|
|
24
|
+
const rs = new Map(runStats.filter(r => !DEBATE_ROLES.has(r.role) && r.role !== 'judge')
|
|
25
|
+
.map(r => [r.model, r]));
|
|
22
26
|
return meta.models.map(model => {
|
|
23
27
|
const raised = findings.filter(f => f.raiser === model);
|
|
24
28
|
const s = sc.get(model) || {};
|