amicus 4.4.0 → 4.5.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.
Files changed (109) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/CHANGELOG.md +162 -0
  3. package/README.md +17 -2
  4. package/bin/amicus.js +10 -0
  5. package/docs/DISTRIBUTION.md +234 -0
  6. package/docs/ROADMAP.md +226 -0
  7. package/docs/SHIMS.md +62 -0
  8. package/docs/architecture.md +104 -0
  9. package/docs/configuration.md +395 -0
  10. package/docs/council.md +970 -0
  11. package/docs/doc-system.md +92 -0
  12. package/docs/electron-testing.md +471 -0
  13. package/docs/jsdoc-setup.md +75 -0
  14. package/docs/opencode-integration.md +114 -0
  15. package/docs/publishing.md +60 -0
  16. package/docs/schemas.md +56 -0
  17. package/docs/testing.md +589 -0
  18. package/docs/troubleshooting.md +298 -0
  19. package/docs/usage.md +849 -0
  20. package/electron/fold.js +1 -1
  21. package/electron/main.js +4 -1
  22. package/electron/setup-ui-aliases.js +6 -6
  23. package/electron/workspace-ui/live-model.js +12 -1
  24. package/electron/workspace-ui/md-lite.js +52 -8
  25. package/electron/workspace-ui/workspace-app.js +39 -17
  26. package/electron/workspace-ui/workspace-matrix.js +46 -9
  27. package/electron/workspace-ui/workspace-panels.js +88 -19
  28. package/electron/workspace-ui/workspace-render.js +17 -1
  29. package/electron/workspace-ui/workspace-verbs.js +48 -2
  30. package/package.json +8 -3
  31. package/schemas/council-run-live.schema.json +1 -1
  32. package/schemas/council-run.schema.json +34 -0
  33. package/schemas/error.schema.json +1 -1
  34. package/schemas/event.schema.json +1 -1
  35. package/schemas/pack.schema.json +30 -0
  36. package/schemas/progress.schema.json +13 -1
  37. package/schemas/run-live.schema.json +1 -1
  38. package/schemas/run.schema.json +2 -1
  39. package/schemas/spend.schema.json +52 -4
  40. package/schemas/wave-live.schema.json +1 -1
  41. package/schemas/wave.schema.json +2 -1
  42. package/skills/second-opinion/SKILL.md +5 -0
  43. package/src/cli-handlers-council-run.js +51 -8
  44. package/src/cli-handlers-pack.js +238 -0
  45. package/src/cli-handlers-run.js +36 -8
  46. package/src/cli-handlers-spend.js +20 -2
  47. package/src/cli-handlers-template.js +53 -0
  48. package/src/cli-handlers-watch.js +11 -0
  49. package/src/cli.js +68 -5
  50. package/src/council/briefings-debate.js +27 -7
  51. package/src/council/briefings-stage2.js +155 -25
  52. package/src/council/briefings.js +24 -1
  53. package/src/council/findings.js +199 -9
  54. package/src/council/parse-stage2.js +10 -2
  55. package/src/council/presets-cli.js +23 -11
  56. package/src/council/report.js +19 -8
  57. package/src/council/run-assemble.js +42 -1
  58. package/src/council/run-budget.js +64 -11
  59. package/src/council/run-chair.js +4 -1
  60. package/src/council/run-debate.js +4 -2
  61. package/src/council/run-finalize.js +102 -0
  62. package/src/council/run-launch.js +29 -1
  63. package/src/council/run-server.js +248 -0
  64. package/src/council/run-stage2.js +118 -0
  65. package/src/council/run-stages.js +134 -110
  66. package/src/council/run-state.js +40 -1
  67. package/src/council/run.js +45 -47
  68. package/src/council/tally.js +10 -0
  69. package/src/headless.js +180 -7
  70. package/src/mcp-council-run.js +108 -4
  71. package/src/mcp-server.js +203 -7
  72. package/src/mcp-tools.js +15 -5
  73. package/src/observe/council-legs.js +60 -3
  74. package/src/observe/live-doc.js +18 -1
  75. package/src/observe/watch-render.js +4 -1
  76. package/src/pack/pack-cli.js +38 -0
  77. package/src/pack/pack-forward.js +96 -0
  78. package/src/pack/pack-resolve.js +297 -0
  79. package/src/pack/pack-store.js +130 -0
  80. package/src/pack/pack-validate.js +113 -0
  81. package/src/sidecar/child-sessions.js +1 -2
  82. package/src/sidecar/fanout-leg-fallback.js +69 -21
  83. package/src/sidecar/fanout-leg.js +6 -0
  84. package/src/sidecar/fanout-signals.js +61 -0
  85. package/src/sidecar/fanout-wave-io.js +75 -0
  86. package/src/sidecar/fanout.js +82 -74
  87. package/src/sidecar/progress-fields.js +26 -4
  88. package/src/sidecar/progress.js +42 -1
  89. package/src/sidecar/session-utils.js +23 -14
  90. package/src/sidecar/start.js +5 -4
  91. package/src/sidecar/workspace-auto-open.js +69 -0
  92. package/src/sidecar/workspace-window.js +46 -1
  93. package/src/spend-query.js +17 -5
  94. package/src/template/apply.js +88 -0
  95. package/src/template/render.js +86 -0
  96. package/src/template/store.js +106 -0
  97. package/src/utils/config.js +65 -25
  98. package/src/utils/error-doc.js +5 -0
  99. package/src/utils/lifecycle.js +37 -1
  100. package/src/utils/path-fence.js +39 -1
  101. package/src/utils/pricing.js +26 -10
  102. package/src/utils/result-schema-rebuild.js +1 -0
  103. package/src/utils/result-schema.js +8 -2
  104. package/src/utils/server-setup.js +79 -1
  105. package/src/utils/spend-ledger.js +24 -3
  106. package/src/workspace/artifact-guard.js +66 -7
  107. package/src/workspace/fold-format.js +33 -4
  108. package/src/workspace/live-normalize.js +28 -15
  109. package/src/workspace/run-detail.js +13 -1
@@ -0,0 +1,970 @@
1
+ # Council Reference — Pipeline, Schemas, and a Worked Example
2
+
3
+ > Quick summary is in the [README "The Council" section](../README.md#the-council). This document
4
+ > is the field-by-field reference for the `amicus council` subcommands: what each one reads, what
5
+ > it writes, and how the pipeline chains together. It is self-contained — every command and JSON
6
+ > example below is real and was run against the CLI while writing this doc.
7
+
8
+ This page exists because `amicus council tally|stats|report|validate|verdict|save|list|show` are
9
+ **deterministic local math and file I/O** — none of them call a model. The one exception is
10
+ [`amicus council run`](#amicus-council-run) (v4.0), the **headless engine**: it drives the whole
11
+ pipeline below — Stage-1 reviews → anonymized cross-review → tally → chair verdict — in one
12
+ command, and it does call models. In the interactive path, the models run in the
13
+ `second-opinion` skill's Stage 1/2/3 waves (`amicus fanout` / `amicus start`); these subcommands
14
+ consume and produce the JSON that glues those stages together. If you're driving a live council
15
+ run, follow **[skills/second-opinion/SKILL.md](../skills/second-opinion/SKILL.md)** — it's the
16
+ orchestration recipe. This page is the reference for the artifacts that recipe produces.
17
+
18
+ ---
19
+
20
+ ## Table of contents
21
+
22
+ - [The pipeline, end to end](#the-pipeline-end-to-end)
23
+ - [`amicus council run`](#amicus-council-run)
24
+ - [Debate mode](#debate-mode)
25
+ - [Council Workspace (GUI)](#council-workspace-gui)
26
+ - [Auto-open on `amicus_council_run` (v4.5)](#auto-open-on-amicus_council_run-v45)
27
+ - [`amicus council validate`](#amicus-council-validate)
28
+ - [`amicus council tally`](#amicus-council-tally)
29
+ - [`amicus council verdict`](#amicus-council-verdict)
30
+ - [`amicus council report`](#amicus-council-report)
31
+ - [`amicus council stats`](#amicus-council-stats)
32
+ - [Council presets: `save` / `list` / `show`](#council-presets-save--list--show)
33
+ - [Worked example](#worked-example)
34
+ - [Where artifacts live](#where-artifacts-live)
35
+
36
+ ---
37
+
38
+ ## The pipeline, end to end
39
+
40
+ ```
41
+ Stage 1 reviews (models, via `amicus fanout`)
42
+ │ each leg saved as review-<model>.md (prose + trailing ```json findings block)
43
+
44
+ amicus council validate <leg-file> ← per-leg findings-block check (tri-state exit)
45
+ │ ok:true → findings[] usable
46
+
47
+ Stage 2 cross-review (models rank + adjudicate, via `amicus fanout`)
48
+ │ Claude assembles the de-anonymized tally-input JSON (see SKILL.md's
49
+ │ "Stage-2 → tally assembly recipe")
50
+
51
+ amicus council tally <tally-input.json> ← deterministic tiers + street-cred + ledger append
52
+ │ writes/prints the tally RECORD (save it as tally.json)
53
+
54
+ Stage 4 decisions (Claude presents tiers, user accepts/denies/modifies)
55
+ │ Claude writes decisions.json — one {id, decision, ...} object per finding
56
+
57
+ amicus council verdict <tally.json> --decisions <decisions.json> -o verdict.json
58
+ │ merges the tally record + decisions into the schema-stamped verdict.json
59
+
60
+ amicus council report <verdict.json> [--md|--html]
61
+ │ renders the adjudication matrix, street-cred table, findings-by-tier, cost
62
+
63
+ report.md (Claude-authored synthesis) + report.html (deterministic renderer output)
64
+
65
+ Independently, at any time:
66
+ amicus council stats ← ledger-derived reviewer reliability (all past runs)
67
+ amicus council save|list|show ← named --models lists for `--council <name>`
68
+ ```
69
+
70
+ Three things to hold onto:
71
+
72
+ 1. **`tally` is the finalize step.** It both computes the deterministic record *and* appends one
73
+ row per (run × model) to the append-only reliability ledger (`~/.config/amicus/council-ledger.jsonl`,
74
+ or `$AMICUS_CONFIG_DIR/council-ledger.jsonl` when that env var is set) — unless you pass
75
+ `--no-ledger`. `stats` reads that ledger back.
76
+ 2. **`verdict` doesn't recompute anything.** It's a pure merge of the tally record (deterministic
77
+ tiers/street-cred, already computed) with your Stage-4 decisions (accept/deny/modify, made by a
78
+ human via Claude). See [provenance](#amicus-council-verdict) below.
79
+ 3. **`report` never touches a model.** It reads `verdict.json` (+ optional `wave.json` for the cost
80
+ total) and renders it — Markdown or a self-contained HTML page. All prose synthesis (the actual
81
+ "what does this mean" writeup) is `report.md`, written by Claude in Stage 5 of the skill — a
82
+ *different* artifact from what this renderer produces. See
83
+ [Where artifacts live](#where-artifacts-live).
84
+ 4. **Optional council elements (v2.2.0) are orchestration-level — nothing in these subcommands
85
+ changed for them.** The skill's opt-in elements (critic seat, expert lenses, debate mode, chair
86
+ verdict scale — see [SKILL.md](../skills/second-opinion/SKILL.md) and
87
+ [SEAT-BRIEFS.md](../skills/second-opinion/SEAT-BRIEFS.md)) ride on existing engine surfaces:
88
+ seat roles travel as free-form `runStats[].role` labels (`"critic"`, `"lens:<slug>"`), debate
89
+ mode runs the Stage-2 tally with `--no-ledger` (provisional) and re-tallies after the rebuttal
90
+ round (that second, post-rebuttal tally is the ledger-recorded one), and lens runs always pass
91
+ `--no-ledger` so non-comparable reviews never feed `stats`.
92
+
93
+ ---
94
+
95
+ ## `amicus council run`
96
+
97
+ ```
98
+ amicus council run --prompt-file <briefing.md>
99
+ (--council <preset> | --models a,b,c) # >=2 seats, XOR (fanout semantics)
100
+ [--chair <model>] # default deepseek; must NOT be a bench seat
101
+ [--critic <model>] # must BE a bench seat; adversarial brief
102
+ [--lenses s1,s2,...] # count == seat count; forces no-ledger;
103
+ # mutually exclusive with --critic in v4.0
104
+ [--out-dir <dir>] # default ./council-<runId>/
105
+ [--json] [--max-cost <usd>] [--timeout <min>]
106
+ [--gateway auto|direct|openrouter] [--no-validate-model]
107
+ [--template <name|path>] [--artifact <file>] [--var k=v] # v4.5, see docs/usage.md#briefing-templates
108
+ [--pack <name|path>] # v4.5, see docs/usage.md#policy-packs
109
+ ```
110
+
111
+ **The headless engine (v4.0).** Everything the `second-opinion` skill orchestrates by hand in
112
+ Stages 1–3+5 — seat briefings (anti-sycophancy clause included), the Stage-1 review wave,
113
+ anonymization and run-global finding-id rewriting, the identical judge bundle, the Stage-2
114
+ cross-review wave with bounded repair re-prompts, the tally, chair synthesis with the verdict
115
+ scale, and the deterministic report — runs as **one command with no Claude runtime**. Stage 4
116
+ stays human: the engine is report-only and never fabricates accept/deny decisions
117
+ (`amicus council verdict --decisions` remains the post-hoc path).
118
+
119
+ Key semantics:
120
+
121
+ - `--prompt-file` is **required** — councils always have real briefings (no inline `--prompt`).
122
+ - Seat/chair/critic/lens validation happens **pre-flight** and fails through the error envelope
123
+ (exit 1) before any spend. The chair must not be a bench seat.
124
+ - **`--pack <name|path>` (v4.5)** loads a saved bench/chair/critic/lenses/options/template as this
125
+ run's defaults — any flag you also typed always overrides the pack's value for that field, and
126
+ the pack is recorded on `run.json` (`pack: {name, version, hash, source}`) either way. When a
127
+ pre-flight error names a value the pack supplied (e.g. "chair is a bench seat"), the message adds
128
+ `(set by pack '<name>')` so a pack-caused failure is never mistaken for a typo in your own flags.
129
+ Full reference: [docs/usage.md § Policy packs](./usage.md#policy-packs).
130
+ - `--timeout` is the **per-leg** timeout (existing fanout semantics); there is no run-level
131
+ watchdog in v4.0 — bound the aggregate with your CI job timeout.
132
+ - `--max-cost` is a **whole-run** ceiling checked before each paid stage launch (Stage-1 wave,
133
+ repair solos, Stage-2 wave, chair). Hitting it mid-run stops launching and finalizes what
134
+ exists; in-flight legs are never aborted for cost.
135
+ - **It bounds KNOWN spend, and it never blocks a run.** A leg whose cost could not be
136
+ determined contributes nothing to the figure the ceiling is checked against, is never guessed
137
+ at, and never halts anything — the standing ruling is fail *loud*, not fail *closed*. The
138
+ measured consequence of the older, quieter version: `council-wsgate02` really spent
139
+ **$0.9859 against a $0.75 ceiling (131%)** while amicus believed $0.3720 — and exited `0`.
140
+ So when a ceiling is set **and** the run's total is inexact (`usage.costExact: false` — any
141
+ `unknownLegs` or `subtreeUnknownLegs`), the run **exits `2`**, through the same degrade
142
+ channel as a bench the ceiling shrank — the stages, verdict and usage block are untouched;
143
+ `exitCode` becomes `2` and `status` becomes `partial`, as for every other degradation, because
144
+ `0`/`complete` reads as "clean, and inside your ceiling" and a run publishing a floor has not
145
+ earned that. With **no** ceiling there is nothing to be inexact against and an unpriced leg
146
+ leaves the exit code (and status) alone.
147
+ - Each launch is measured against the **remaining** allowance (ceiling − known spend −
148
+ allowances already claimed by a wave that is launching right now). Stage 1 launches its seat
149
+ wave and its critic/lens waves concurrently, so the claim is atomic — two waves can never
150
+ both spend the same remaining dollars. **If the ceiling refuses one of them, the run
151
+ continues with a partial bench**: launched waves are never rolled back and the run is never
152
+ aborted for cost. The refusal is printed as a `Notice:` naming the wave and its models,
153
+ recorded on `run.json` as `budgetRefusals[]`, and degrades the exit code to `2`. If it takes
154
+ the bench below two reviews, the usual `COUNCIL_QUORUM` failure (exit 1) applies.
155
+ - A run starts **one** OpenCode server and threads it through every wave (two concurrent
156
+ starts race on OpenCode's SQLite). Whether that worked is always on the record, in the
157
+ affirmative as well as the negative — exactly one of these two keys is present:
158
+ - `sharedServer` `{acquired: true, at, goPid, models}` — the run got its shared server and
159
+ every wave rode it. `goPid` is the server's pid; no wave writes a `goPid` into its own
160
+ `metadata.json` while riding an injected server, so this is direct evidence rather than
161
+ an inference drawn from a `goPid` appearing where it should not.
162
+ - `sharedServerUnavailable` `{error, at}` — the server could not start and the run **still
163
+ proceeded** on one server per wave, the configuration that races. Also printed as a
164
+ `Notice:`. It does not change the exit code; treat its presence as "expect degraded
165
+ results".
166
+ - **A `--council <preset>` member that resolution drops is recorded on `run.json`, not just
167
+ printed.** A preset member whose alias no longer resolves, or whose resolved id has fallen out
168
+ of the cached model catalog, is silently excluded from `bench` (the same graceful-degradation
169
+ `resolveCouncilMembers` applies everywhere) — human mode also prints a `Notice: dropped
170
+ unavailable council member(s): ...`, but `--json` mode (every scripted/MCP caller) printed
171
+ nothing at all. `run.json` now carries an additive `droppedMembers: [{member, reason}]` array —
172
+ present only when at least one member was actually dropped. The `--json` envelope carries it
173
+ via the same run.json serialization; the `amicus_council_run` MCP response body includes it
174
+ via an explicit conditional spread (`...(droppedMembers.length ? { droppedMembers } : {})`),
175
+ hand-built separate from run.json. `amicus council show <name>` reports the identical resolved/dropped split (and the
176
+ same per-member reason) as a preview, before you spend anything.
177
+ - Chair failure recovery: one retry of the same chair → promote the highest peers-only
178
+ street-cred model (from `amicus council stats`) that is not a bench seat → give up and write
179
+ the verdict with `overallVerdict: null`.
180
+ - SIGINT/SIGTERM abort the active wave/solo, finalize `run.json` as `aborted`, exit 130/143.
181
+ `amicus abort <councilRunId>` (and the MCP tools via the sessions-dir pointer file
182
+ `council-<runId>.json`) work on council runs; `status`/`list` resolve them the same way.
183
+ There is no CLI `wait` — the MCP `amicus_wait` tool is the blocking primitive and resolves
184
+ council runs the same way.
185
+
186
+ **Exit codes and degradation:**
187
+
188
+ | Condition | Behavior | Exit |
189
+ |---|---|---|
190
+ | All stages complete, chair verdict parsed | full run | 0 |
191
+ | Fewer than 2 completed Stage-1 reviews | stop; error doc `COUNCIL_QUORUM` | 1 |
192
+ | At least 2 reviews but fewer than 2 completed judges | proceed; tally `judged:false` | 2 |
193
+ | Chair fails (1 retry + 1 fallback promotion) | `verdict.json` written, `overallVerdict:null` | 2 |
194
+ | Chair output missing `VERDICT:` line after 1 repair | chair prose kept, `overallVerdict:null` | 2 |
195
+ | Cost ceiling hit after the tally exists | verdict written (no chair), `overallVerdict:null` | 2 |
196
+ | Cost ceiling hit before the tally | stop; error doc `COST_EXCEEDED` | 1 |
197
+ | Cost ceiling refused a wave at pre-flight | partial bench; `Notice:` + `run.json` `budgetRefusals[]` | 2 |
198
+ | `--max-cost` set and `usage.costExact:false` | stages/verdict/usage untouched; `run.json` `status` becomes `partial` — the total is a floor, so it is not reported as clean | 2 |
199
+ | Aborted | `run.json` status `aborted` | 130/143 |
200
+
201
+ **The run directory** (durable state; skill-compatible layout):
202
+
203
+ ```
204
+ council-<runId>/
205
+ briefing-stage1.md # composed seat briefing (user briefing + templates)
206
+ review-<model>.md x N # Stage-1 outputs
207
+ bundle-stage2.md # anonymized judge bundle (identical for all judges)
208
+ judge-<model>.md x N # Stage-2 raw outputs
209
+ chair-packet.md # de-anonymized chair packet (+ verdict-scale addendum)
210
+ chair-output.md # chair raw output
211
+ tally-input.json # the assembled five-keys object (auditability)
212
+ tally.json # engine tally record (council family v2)
213
+ verdict.json # undecided verdict (tiers + overallVerdict)
214
+ report.html # deterministic renderer output
215
+ run.json # manifest: schemaVersion 2, type council-run, stages, usage
216
+ _scratch/ # cwd for judge legs (isolation)
217
+ ```
218
+
219
+ Two more files appear when the run was started through **`amicus_council_run`** rather than the
220
+ CLI, both written by the MCP handler before it spawns the engine:
221
+
222
+ ```
223
+ briefing.md # the briefing the tool copied in (the child briefs off this copy)
224
+ spawn.pid # the spawned child's pid
225
+ ```
226
+
227
+ `spawn.pid` exists so a child that dies *before* the engine checkpoints its own pid into
228
+ `run.json` is still detectable: `amicus status` crash detection and `amicus abort`'s process
229
+ fallback both read `run.json`'s pid first and fall back to this file. It is deliberately a
230
+ separate single-write file rather than a field patched into `run.json` — the spawning process and
231
+ the engine child both write `run.json`, and its checkpoint is a read-merge-write with no
232
+ cross-process lock.
233
+
234
+ `verdict.json` here is the **undecided** verdict — same schema as [`amicus council
235
+ verdict`](#amicus-council-verdict)'s output (council family v2) plus **`overallVerdict`**
236
+ (`"Ship it" | "Fix these first" | "Fundamental rethink" | null`), parsed from the chair's final
237
+ `VERDICT:` line. Example excerpt:
238
+
239
+ ```json
240
+ {
241
+ "schemaVersion": 2,
242
+ "runId": "pr-142-council",
243
+ "chair": "deepseek",
244
+ "overallVerdict": "Fix these first",
245
+ "tierCounts": { "Confirmed": 2, "Contested": 1, "Singleton": 1, "Disputed": 0 }
246
+ }
247
+ ```
248
+
249
+ One-shot CI-shaped example (this is exactly what the Council Review GitHub Action runs):
250
+
251
+ ```bash
252
+ $ amicus council run --models gemini,glm --chair deepseek \
253
+ --prompt-file briefing.md --out-dir council-run \
254
+ --json --max-cost 2.00 --timeout 10 --no-validate-model
255
+ ```
256
+
257
+ Consumers gate on **tiers + the chair verdict line** (`overallVerdict`), per the engine's
258
+ report-only Stage-4 policy. Headless runs pin `meta.claudeInCouncil: false`,
259
+ `meta.runType: "headless"`, and the chair is excluded from the street-cred universe.
260
+
261
+ ### Debate mode
262
+
263
+ `--debate` adds a **Stage-2.5 rebuttal round** between cross-review and the final tally
264
+ (COUNCIL-DESIGN.md §12.3): **provisional tally → defense → re-vote → final tally**, exactly
265
+ one round.
266
+
267
+ 1. **Provisional tally.** Immediately after Stage-2 cross-review, the engine tallies with
268
+ `--no-ledger` and writes `tally-provisional.json`. If nothing landed Contested or Disputed,
269
+ there is nothing to debate — the engine skips straight to the final tally
270
+ (`debate.outcome: "nothing-to-debate"`).
271
+ 2. **Defense.** Every Contested/Disputed finding goes back to its raiser as one concurrent solo
272
+ run — `rebuttal-<model>.md` per raiser — asking for exactly one of `DEFEND` / `AMEND` /
273
+ `WITHDRAW` per finding. A dead or unparseable defense leg means the original claim stands
274
+ undefended.
275
+ 3. **Re-vote.** Defended/amended findings go back to the judges who disputed them, as ONE
276
+ shared fanout wave — `revote-bundle.md` (the shared prompt, written to the run dir like
277
+ Stage 2's `bundle-stage2.md`) + `revote-<model>.md` per judge. A missing/unparseable re-vote
278
+ line leaves that judge's original verdict standing.
279
+ 4. **Final tally.** The engine reassembles the tally input with the defense/re-vote outcomes
280
+ folded in and re-tallies — this final, post-rebuttal tally is the one that appends to the
281
+ reliability ledger (a lens run is the only thing that suppresses the append — `council run` has no `--no-ledger` escape hatch; that flag is parsed only by `council tally`). Withdrawn findings stay
282
+ in `findings[]` and are auto-recorded `denied` at Stage 4 — never presented for a user
283
+ decision.
284
+
285
+ **Exactly one round** — there is no second defense/re-vote cycle; whatever remains unsettled
286
+ after the re-vote keeps its final tier.
287
+
288
+ **Where it shows up:**
289
+ - `run.json`'s `debate` object summarizes the round: `{enabled, outcome, contested, disputed,
290
+ defended, amended, withdrawn, noResponse, revoteJudges, revoteApplied, verdictChanges}`.
291
+ `outcome` is `"nothing-to-debate"`, `"ran"`, or `"skipped-cost-ceiling"` (the whole-run cost
292
+ ceiling was hit before a warranted re-vote).
293
+ - Every debated finding in `tally.json`/`verdict.json` carries a `findings[].debate` object —
294
+ `{action: "defended"|"amended"|"withdrawn"|"no-response", previousTier}` — decorating the
295
+ finding with what happened in the round and the tier it held before the re-vote.
296
+ - Extra run-dir artifacts, written only when a defense/re-vote actually ran:
297
+ `tally-provisional.json`, `revote-bundle.md`, `debate.json` (the round's structured record),
298
+ `rebuttal-<model>.md` × (raisers), `revote-<model>.md` × (disputing judges).
299
+ - `--claude-review <file>` enters Claude's own review (from a file, no leg launched) as a judged
300
+ entry; per the reserved-seat rule, it is never asked to defend in the debate round — its
301
+ Contested/Disputed findings simply stand, the same "originals stand" outcome as a dead defense
302
+ leg.
303
+
304
+ ---
305
+
306
+ ## Council Workspace (GUI)
307
+
308
+ Watch a council think — not just tail a log:
309
+
310
+ ```bash
311
+ amicus watch <councilRunId> --ui # open one run
312
+ amicus watch --ui # open the run list for this project
313
+ ```
314
+
315
+ This is a third Electron mode (`AMICUS_MODE=council-workspace`), opened by `amicus watch --ui`
316
+ itself — not a separate command or a separate launch surface (that's v4.5). One window per
317
+ launch:
318
+
319
+ - **Run list** (bare `--ui`, no id) — every council run in the current project, newest first,
320
+ discovered from the sessions-dir pointer files (`council-<runId>.json`); each row shows status,
321
+ the chair's `overallVerdict` chip once one exists, and cost.
322
+ - **Run detail** (`--ui <runId>`) — header + status/verdict chips, a stage rail, a live **Seats**
323
+ table (model, role, status, stage, messages, tokens, cost, last activity, a stall flag), the
324
+ Stage-1 reviews, the **verbatim** anonymized Stage-2 packet (`bundle-stage2.md`, shown as-is —
325
+ never re-rendered), judge prose, the **adjudication matrix** (finding × judge, tier-colored
326
+ rows, `a/d/n` basis counts, a `thin` badge when `a+d<=1`, an override badge when a Stage-4
327
+ decision changed a finding's tier, capped at 500 rows with a "showing N of M" note past that),
328
+ dissent drill-in (click a ✗ cell to open that judge's prose with the finding id highlighted —
329
+ the Stage-2 contract carries no structured reason field, so rationale lives in prose; on a
330
+ `--debate` run, a re-voted cell instead opens `revote-<model>.md` and also surfaces the
331
+ structured `reason` `debate.json` records for that re-vote), chair verdict + street-cred +
332
+ Stage-4 decisions, and a cost-by-seat table with a `--max-cost` ceiling gauge.
333
+
334
+ **Historical runs** render entirely from the run directory — open any old `council-<runId>` at any
335
+ time; nothing here requires the run to still be live.
336
+
337
+ **Live updates.** While a run is in progress, the window polls the same v4.3 data layer `amicus
338
+ watch` reads from a terminal — every 1.5s while the window is visible and focused, every 5s
339
+ otherwise, stopping once the run reaches a terminal status. A stall (no leg activity for a while)
340
+ surfaces as a banner with an Abort shortcut next to it; if a live-data read itself fails, a
341
+ separate "live data unavailable" banner appears while the last-known panels stay on screen — the
342
+ poll keeps retrying rather than blanking anything.
343
+
344
+ **Blind mode** (toggle, top right): labels (`Review A`, `Review B`…) instead of model names —
345
+ **ON by default while a run is live, OFF once it reaches a terminal status**, flippable either way
346
+ at any time. This is a **reading aid against anchoring bias, not a security control** — the label
347
+ map is `run.json`'s own `labelMap` field, sitting in plaintext in the run directory like every
348
+ other artifact; nothing stops you opening it in a text editor.
349
+
350
+ **Masks the roster, not just the seat table** (amended 2026-07-25 — see §6 resolved-Q2 amendment
351
+ in the design spec). Blind mode covers *every* place a model id co-occurs with review authorship:
352
+ the header's bench/critic/chair chips, the currently-open run's own row in the run-list rail
353
+ (its chair chip), seat rows, cost rows, and revote titles — the same `display(pair, blind)`
354
+ formatter backs all of them, so there is one place to get this right instead of five. Best-effort
355
+ only for other rows in the run list: each row's chair can only be masked when a label happens to
356
+ be known for it (in practice, only the currently-open run's own row), so an unopened run's row
357
+ still shows its raw chair id — consistent with "reading aid," not a hard guarantee.
358
+
359
+ **Two verbs, nothing else:**
360
+
361
+ - **Abort** — confirm-gated, hidden once the run is terminal. It calls the same council-aware
362
+ abort path `amicus abort` uses. This is the one place the workspace changes anything on disk,
363
+ and it does so by delegating to the engine's own abort handling — not a direct write from the
364
+ workspace code itself.
365
+ - **Fold** — writes the nonced `[SIDECAR_FOLD:…]` block plus the chair's verdict to the launching
366
+ terminal's stdout (no model call — it reformats what's already on disk). Folding again after a
367
+ successful fold just reports "already folded"; it doesn't write a second time.
368
+
369
+ Apart from Abort, the workspace is **read-only against the run directory**. `--ui` is
370
+ interactive-only — there is no `--json` for it, and passing both fails fast rather than silently
371
+ falling back to the terminal renderer. Closing the window never auto-folds — everything is
372
+ already on disk, so nothing is lost; reopen with `amicus watch <runId> --ui` and fold whenever
373
+ you're ready.
374
+
375
+ **Degraded states are rendered honestly, never hidden:** a run whose `run.json` can't be parsed at
376
+ all shows an "unreadable" banner with the error and the run directory path; a run written by a
377
+ different amicus schema version shows a schema-mismatch banner instead of guessing at a rendering;
378
+ a tally with fewer than 2 completed judges shows an explicit "tally is peers-reduced" note **above**
379
+ the adjudication matrix — the matrix still renders over the surviving judges, and the note is what
380
+ stops it being read as more authoritative than the underlying data supports; and a
381
+ chair-less verdict (retry + fallback promotion both failed, or the cost ceiling was hit before the
382
+ chair ran) shows "no chair verdict" plus the engine's own reason, never a blank panel.
383
+
384
+ **Posture, briefly** — this page renders another model's prose, so it's the most locked-down page
385
+ in the app: full `sandbox`/`contextIsolation`, a minimal preload exposing exactly one `invoke()`
386
+ gated by a 7-channel allowlist, a CSP with **no network directive at all** (`default-src 'none'`),
387
+ and every model-derived string reaches the DOM through `textContent`/`createTextNode` only —
388
+ never `innerHTML`, enforced by a static source scan in the test suite.
389
+
390
+ ### Auto-open on `amicus_council_run` (v4.5)
391
+
392
+ The window above no longer needs a separate `amicus watch <runId> --ui` call every time. When the
393
+ **MCP tool** `amicus_council_run` is invoked from **Claude Code (local)**, Amicus launches this same
394
+ Council Workspace window automatically, detached, right after the run starts — the flagship v4.4
395
+ surface is no longer opt-in on the client best able to show it. The plain CLI `amicus council run`
396
+ is unaffected: `detectClient` (`src/utils/client-detect.js`) only resolves from the MCP `initialize`
397
+ handshake, so a terminal invocation has no client to detect and never auto-opens; use
398
+ `amicus watch <runId> --ui` there as before.
399
+
400
+ **Decision order** (`shouldAutoOpenWorkspace`, `src/sidecar/workspace-auto-open.js`) — read top to
401
+ bottom, first match wins:
402
+
403
+ 1. The tool's `ui: false` param — **beats everything**, including every guard below.
404
+ 2. The hard guards, which beat even an explicit `ui: true`: Electron is not installed (this path
405
+ **never** installs it — that would be a surprise ~100 MB download on someone's first MCP council
406
+ run) → does not open; on Linux, no `DISPLAY` in the environment → does not open.
407
+ 3. The tool's `ui: true` param — overrides both the config key and the client check below (but
408
+ never a hard guard above).
409
+ 4. `workspace.autoOpen === false` in `config.json` (see
410
+ [Configuration § Config file format](./configuration.md#config-file-format)) → does not open.
411
+ 5. The caller isn't Claude Code (local) (i.e. `client !== 'code-local'` — Claude Desktop/Cowork and
412
+ Claude Code web are deliberately excluded from the default) → does not open.
413
+ 6. Otherwise → opens.
414
+
415
+ **Response fields.** `amicus_council_run`'s result always carries `workspaceOpened: boolean`, and,
416
+ **only when it did not open**, `workspaceOpenReason` — one of `param-suppressed`, `electron-absent`,
417
+ `no-display`, `config-disabled`, `client-not-code-local`, or a `spawn-failed:`/`auto-open-failed:`
418
+ detail if the decision said to open but the launch itself failed. The launch is fire-and-forget: it
419
+ never blocks the tool's response, and a launch failure never fails the council run itself — the run
420
+ proceeds exactly as it would with no Workspace at all, and `amicus watch <runId> --ui` still works
421
+ as the manual fallback.
422
+
423
+ **The `ui` param and the `workspace.autoOpen` config key are independent knobs, not aliases of each
424
+ other** — `ui` is a **per-call** override (either direction), while `workspace.autoOpen` sets the
425
+ **standing default** every call without an explicit `ui` falls back to. Turning the config default
426
+ off does not stop you from asking for the window on one particular run with `ui: true`, and leaving
427
+ the default on does not stop you from suppressing it on one noisy run with `ui: false`.
428
+
429
+ ---
430
+
431
+ ## `amicus council validate`
432
+
433
+ ```
434
+ amicus council validate <file> [--json]
435
+ ```
436
+
437
+ Validates a Stage-1 reviewer's saved output (`review-<model>.md`: prose + a trailing ` ```json `
438
+ fenced block) against the findings-block contract, without calling a model. Thin CLI wrapper over
439
+ `validateFindings` (`src/council/findings.js`).
440
+
441
+ **What it checks**, in order:
442
+ - A ` ```json ` fenced block exists (last one in the file wins if there are several) —
443
+ `NO_FENCED_BLOCK` if not.
444
+ - It parses as JSON — `NOT_PARSEABLE` if not.
445
+ - `findings` is present and is an array — `EMPTY_FINDINGS` if it is missing or is some other
446
+ type. **An array that is present and empty is valid**, provided `overall` is a non-empty
447
+ string; `EMPTY_FINDINGS` if the array is empty *and* `overall` is missing, blank, or not a
448
+ string. See "A clean review is a valid review" below.
449
+ - Every finding has a **sequential integer `id`** starting at 1 (`NON_SEQUENTIAL_ID` /
450
+ `DUPLICATE_ID` otherwise), a `severity` in `{blocker, major, minor, nit}` (`BAD_SEVERITY`
451
+ otherwise), and non-empty string `claim`, `location`, `rationale` (`MISSING_FIELD` otherwise).
452
+
453
+ **A clean review is a valid review.** A reviewer that read the material, found nothing wrong,
454
+ and said so passes validation with `"findings": []` — it is not sent to a repair re-prompt and
455
+ its seat is recorded `conformance: clean`, exactly like any other well-formed review. Three
456
+ things make that safe:
457
+
458
+ - A **broken** emit is a different outcome with its own code: no fenced block at all is
459
+ `NO_FENCED_BLOCK`, and a block that does not parse is `NOT_PARSEABLE`. Both return before the
460
+ empty-set rule is ever reached, so "my output broke" is never mistaken for "I found nothing".
461
+ - **`overall` is what carries the claim.** An empty findings array with a blank, missing, or
462
+ non-string `overall` is a hollow shell, not a judgement, and stays `EMPTY_FINDINGS`. The
463
+ Stage-1 briefing states the same rule to the model: `overall` is always required, `findings`
464
+ may be `[]`, and a finding is never to be invented to fill it.
465
+ - A **missing** `findings` key is not a declaration of zero and stays an error. Only an array
466
+ that is present and empty counts as "I found nothing" — the same line `countAttemptedFindings`
467
+ draws when it checks a repair against the count the original declared.
468
+
469
+ This closes a contradiction that used to be shipped in every run: the anti-sycophancy clause in
470
+ each Stage-1 briefing says "An empty severity category is a valid result", while the validator
471
+ rejected exactly that answer — so the only way for a reviewer to satisfy the schema was to
472
+ produce a finding. Downstream, an all-clean bench degrades cleanly rather than silently: Stage 2
473
+ still runs (the peer **ranking** — and therefore street-cred — is unaffected by an empty findings
474
+ pool, only the adjudication half is vacuous), the judge bundle and chair packet state the empty
475
+ findings index explicitly instead of rendering a heading over nothing, `tierCounts` comes out
476
+ all-zeros, per-model `confirmRate`/`factErrorRate` are `null` (no denominator to divide by), and
477
+ a `--debate` run records `debate.outcome: "nothing-to-debate"` on `run.json`.
478
+
479
+ **Exit codes are a tri-state contract, not the usual 0/1:**
480
+
481
+ | Exit | Meaning |
482
+ |---|---|
483
+ | `0` | `ok:true` — the findings block is well-formed. |
484
+ | `2` | `ok:false` — parsed as a *result*, but validation failed. A distinct, scriptable outcome — not a crash. |
485
+ | `1` | `BAD_ARGS` envelope — the file path is missing or unreadable. |
486
+
487
+ **Output** (`--json`): `{ok, findings, errors}`. `errors[]` is `{code, detail}[]` using the codes
488
+ above. Verified against the real binary:
489
+
490
+ ```bash
491
+ $ amicus council validate review-deepseek.md --json
492
+ ```
493
+ ```json
494
+ {
495
+ "ok": true,
496
+ "findings": [
497
+ { "id": 1, "severity": "major",
498
+ "claim": "The three-tier pricing table omits a monthly/annual toggle.",
499
+ "location": "pricing-page.html, .pricing-table section",
500
+ "rationale": "Users can't compare annual savings without it, and competitors all show one." }
501
+ ],
502
+ "errors": []
503
+ }
504
+ ```
505
+
506
+ A malformed file (no fenced block) returns exit `2`:
507
+
508
+ ```json
509
+ { "ok": false, "findings": [], "errors": [ { "code": "NO_FENCED_BLOCK", "detail": "no ```json block found" } ] }
510
+ ```
511
+
512
+ ---
513
+
514
+ ## `amicus council tally`
515
+
516
+ ```
517
+ amicus council tally <input.json> [--json] [--no-ledger]
518
+ ```
519
+
520
+ Reads a **tally-input** JSON file, computes the deterministic tally **record**, prints it (human
521
+ summary, or the full record with `--json`), and — unless `--no-ledger` — appends one row per
522
+ model to the reliability ledger. Thin CLI wrapper over `tally()` (`src/council/tally.js`).
523
+
524
+ ### Tally-input schema
525
+
526
+ Claude assembles this file at the end of Stage 2 (the full assembly recipe is in
527
+ [SKILL.md](../skills/second-opinion/SKILL.md#stage-2--cross-review), under "Stage-2 → tally
528
+ assembly recipe"). It needs **all five top-level keys** — `tally()` throws
529
+ `Cannot read properties of undefined (reading 'map')` if `meta` or `findings` is missing:
530
+
531
+ | Field | Type | Meaning |
532
+ |---|---|---|
533
+ | `meta.runId` | string | Run identifier (conventionally the run-folder stem). |
534
+ | `meta.models` | string[] | Every reviewed model id, including `"claude"` when "Claude in the council" is on — this is the street-cred universe. |
535
+ | `meta.chair` | string | The confirmed chair model id. |
536
+ | `meta.claudeInCouncil` | boolean | The Stage-0 toggle. |
537
+ | `meta.runType`, `meta.date` | string (optional) | Free-form labels carried through to `verdict.json`. |
538
+ | `findings[]` | array | One entry per finding across all reviews: `{id, raiser, severity}` (`claim` may ride along but isn't required by the tally engine). `id` is the run-global label (e.g. `A1`, `B2`) assigned during Stage-2 assembly, not the reviewer's local integer id. |
539
+ | `adjudications[]` | array | One entry per (judge × finding): `{findingId, judge, verdict}`, `verdict ∈ {agree, dispute, neutral}`. Include every judge's verdict on every finding, **including the raiser's own adjudication of its own finding** — the engine excludes it automatically when scoring (don't pre-filter it). |
540
+ | `rankings[]` | array | One entry per judge: `{judge, order}`. `order` is that judge's `FINAL RANKING:` block translated to model ids, e.g. `["gpt", "deepseek"]` (ties may use a nested array, e.g. `[["gpt","deepseek"], "mistral"]`). |
541
+ | `runStats[]` | array | One entry per model call: `{model, role, wasChair, conformance, status, durationMs, usage}`. May be `[]`. Any leg with no run document gets `durationMs: null, usage: null` — never invent a value. |
542
+
543
+ ### Tally-record schema (what `tally()` returns / prints)
544
+
545
+ ```json
546
+ {
547
+ "schemaVersion": 2,
548
+ "type": "council-tally",
549
+ "meta": { "...": "echoed from input" },
550
+ "judged": true,
551
+ "streetCred": [ { "model": "gpt", "withSelf": 1, "peersOnly": 1, "perJudgeRank": { "...": "..." } } ],
552
+ "findings": [
553
+ { "id": "A1", "raiser": "deepseek", "severity": "major", "tier": "Confirmed",
554
+ "basis": { "a": 1, "d": 0, "n": 0 }, "confidence": "thin",
555
+ "tierOverride": null, "adjudications": [ { "judge": "deepseek", "verdict": "agree" } ] }
556
+ ],
557
+ "runStats": [ { "...": "validated, echoed from input" } ],
558
+ "tierCounts": { "Confirmed": 1, "Contested": 1, "Singleton": 1, "Disputed": 0 }
559
+ }
560
+ ```
561
+
562
+ | Field | Notes |
563
+ |---|---|
564
+ | `schemaVersion` | Tally-record schema version (currently `2`, council family v2 — see `type` below). This is a *separate* version line from the `--json` **error-envelope** schema version used by `BAD_ARGS` failures (also currently `2`) — the two happen to share a value right now but evolve independently; don't conflate them when scripting against output. |
565
+ | `type` | Document-type discriminator; always `"council-tally"` (council family v2 envelope). |
566
+ | `judged` | `true` only when `rankings.length >= 2`. `false` (1 or 0 rankings) means street-cred numbers exist but rest on thin cross-review. |
567
+ | `streetCred[].withSelf` | Mean rank position across **all** judges' rankings (lower = better). |
568
+ | `streetCred[].peersOnly` | Mean rank position **excluding** the model's own ranking of itself. This is the number used everywhere else (ledger, `stats`, bench recommendations). |
569
+ | `findings[].tier` | One of `Confirmed \| Contested \| Singleton \| Disputed` — see the cascade below. |
570
+ | `findings[].basis` | `{a, d, n}` = peer agree/dispute/neutral counts (raiser's own vote excluded when a raiser is known). |
571
+ | `findings[].confidence` | `"thin"` when `a + d <= 1` (only one peer engaged), else `"solid"`. Thin-confidence findings are the ones Claude may override before Stage 4. |
572
+ | `findings[].tierOverride` | `null` unless Claude recorded an override; shape `{from, to, reason}`. |
573
+ | `tierCounts` | Convenience totals across all findings — this is what `renderRecord`'s human-readable summary prints. |
574
+
575
+ **The peers-only tier cascade** (`assignTier(a, d)` — exhaustive over all `(a, d)`):
576
+
577
+ | Condition | Tier | Confidence |
578
+ |---|---|---|
579
+ | `d >= 2 && d > a` | Disputed | solid |
580
+ | `a >= 2 && a > d`, or `a === 1 && d === 0` | Confirmed | solid (≥2 agree) / thin (lone peer) |
581
+ | `d >= 1` (and not Disputed) | Contested | thin if `a+d<=1`, else solid |
582
+ | else (`a === 0 && d === 0`) | Singleton | thin |
583
+
584
+ A lone corroborating peer (`a=1, d=0`) ranks as **Confirmed (thin)** — it must not rank weaker than
585
+ a lone disputing peer (`a=0, d=1`, which is **Contested (thin)**). A 2-vs-2 split is **Contested**
586
+ (large-bench tie), not Disputed — `d > a` is required for Disputed, not just `d >= 2`.
587
+
588
+ **Ledger append.** Unless `--no-ledger`, `tally` writes one row per `meta.models` entry to
589
+ `council-ledger.jsonl` (append-only, JSON Lines). Use `--no-ledger` for a re-tally that shouldn't
590
+ double-count (e.g. re-running after fixing a malformed input). Two standing uses from the skill's
591
+ optional elements (v2.2.0): **debate mode** tallies provisionally with `--no-ledger` after Stage 2
592
+ and records only the final post-rebuttal tally, and **expert-lens runs** always pass `--no-ledger`
593
+ (lens reviews aren't comparable to standard reviews, so they must not feed `stats`). This is
594
+ best-effort: a ledger write failure prints a notice to stderr but does not fail the tally.
595
+
596
+ ---
597
+
598
+ ## `amicus council verdict`
599
+
600
+ ```
601
+ amicus council verdict <tally.json> [--decisions <decisions.json>] [-o|--out <out.json>]
602
+ ```
603
+
604
+ **Provenance — this is the answer to "where does verdict.json come from":** `verdict.json` is a
605
+ **pure merge** of two inputs, computed by `buildVerdict(record, decisions)`
606
+ (`src/council/verdict.js`) with **no recomputation of tiers or street-cred**:
607
+
608
+ 1. `<tally.json>` — the tally **record** exactly as printed by `amicus council tally --json`
609
+ (Claude saves it to disk after Stage 2; see the worked example below).
610
+ 2. `--decisions <decisions.json>` — a **JSON array**, one object per finding, produced by Claude
611
+ during Stage 4 (the accept/deny/modify pass): `{id, decision, applied?, duplicateOf?, tierOverride?}`.
612
+ Optional — defaults to `[]`, which produces a verdict with every finding's `decision: null`.
613
+
614
+ For each finding, `buildVerdict` looks up the matching decision by `id` and folds in `decision`,
615
+ `applied` (default `false`), `duplicateOf` (default `null`), and `tierOverride` (decision's
616
+ override wins over the tally record's, if both are present — the effective `tier` becomes
617
+ `tierOverride.to` when an override exists). Everything else (`basis`, `confidence`,
618
+ `adjudications`, `streetCred`, `runStats`, `tierCounts`) passes through from the tally record
619
+ unchanged.
620
+
621
+ **Output schema** (`verdict.json`, schema v2 — independent of the tally record's own
622
+ `schemaVersion`):
623
+
624
+ ```json
625
+ {
626
+ "schemaVersion": 2,
627
+ "type": "council-verdict",
628
+ "overallVerdict": null,
629
+ "runId": "...", "runType": "...", "date": "...", "chair": "...",
630
+ "council": ["deepseek", "gpt"],
631
+ "claudeInCouncil": false,
632
+ "findings": [
633
+ { "id": "A1", "raiser": "deepseek", "severity": "major", "tier": "Confirmed",
634
+ "basis": { "a": 1, "d": 0, "n": 0 }, "confidence": "thin", "tierOverride": null,
635
+ "duplicateOf": null, "adjudications": [ "..." ],
636
+ "decision": "accepted", "applied": true }
637
+ ],
638
+ "streetCred": [ { "model": "gpt", "withSelf": 1, "peersOnly": 1 } ],
639
+ "runStats": [ "..." ],
640
+ "tierCounts": { "Confirmed": 1, "Contested": 1, "Singleton": 1, "Disputed": 0 }
641
+ }
642
+ ```
643
+
644
+ **Key notes:**
645
+ - `schemaVersion` — verdict-document schema version (currently `2`).
646
+ - `type` — document-type discriminator; always `"council-verdict"` (council family v2 envelope).
647
+ - `overallVerdict` — the chair's verdict-scale outcome: one of `"Ship it"`, `"Fix these first"`, `"Fundamental rethink"`, or `null` when no chair verdict was produced (populated by the headless engine during Stage 3; `null` for a plain `council verdict` merge without engine integration).
648
+ - All other keys (`runId`, `council`, `findings`, `streetCred`, `runStats`, `tierCounts`) are passed through unchanged from the tally record.
649
+
650
+ **Write path:** always atomic — a `<out>.tmp-<pid>` file is written first, then renamed over the
651
+ target (`writeVerdictAtomic`), matching the repo's `wave.json` convention. Default output path is
652
+ `./verdict.json`; override with `-o`/`--out`.
653
+
654
+ **Re-rendering after Stage 4 (`--render`).** `council verdict`'s `--render` flag refreshes
655
+ `report.html` next to the freshly-decided verdict in the same call — without it, `report.html`
656
+ stays the engine's undecided pre-Stage-4 render, and a user opening it would see decisions that
657
+ were never actually made. It calls the same renderer `amicus council report` uses
658
+ (`buildReport(..., {format: 'html'})`) and writes the HTML into the output path's directory; a
659
+ render failure after a successful verdict write reports the error but leaves `verdict.json` on
660
+ disk (re-run `amicus council report <verdict.json> --html` manually to recover — the verdict
661
+ itself is not lost). The MCP equivalent is `amicus_verdict`'s `render: true` + `outDir:
662
+ <run-folder>`. Because that tool is pure/stateless and writes nothing unless both are given, this
663
+ is a **second** call after the first: call once with `record`/`decisions` and no `render` to get
664
+ the decided verdict back as JSON and write it to `<run-folder>/verdict.json` yourself, then call
665
+ again with `render: true` and `outDir: <run-folder>` — this refreshes `<outDir>/report.html` on
666
+ disk and also returns the verdict's Markdown rendering (for `report.md`); it still does not write
667
+ `verdict.json` itself.
668
+
669
+ **Windows PowerShell 5.1 caveat** (also called out in SKILL.md): redirecting `council tally`'s
670
+ `--json` output with a bare `>` writes UTF-16 under legacy PowerShell 5.1, which then makes
671
+ `council verdict` fail to parse `tally.json` with a confusing `BAD_ARGS` — pipe through
672
+ `| Out-File -Encoding utf8` on 5.1, or run under pwsh 7+/bash.
673
+
674
+ ---
675
+
676
+ ## `amicus council report`
677
+
678
+ ```
679
+ amicus council report <verdict.json> [--wave <wave.json>] [--md|--html]
680
+ ```
681
+
682
+ Pure renderer — reads `verdict.json` (+ optional `wave.json`, used only to source the wave's total
683
+ cost) and produces **one self-contained string**: Markdown (default) or a self-contained HTML page
684
+ (`--html`). No scoring, no anonymization, no synthesis — those already happened upstream. Thin CLI
685
+ wrapper over `buildReport()` (`src/council/report.js` / `report-html.js`).
686
+
687
+ **What it renders**, in this order: a header (run type, id, date, chair, council members), a
688
+ verdict-summary tier-count table, the **adjudication matrix** (finding × judge, `✓`/`✗`/`–` with
689
+ `*` marking the raiser's own vote), the **peers-only street-cred table**, **findings grouped by
690
+ tier** (Disputed first), and a **cost table** (per-model status/duration/cost + wave total,
691
+ sourced from `runStats[].usage`).
692
+
693
+ This is the same renderer the `second-opinion` skill calls in Stage 5 to produce `report.html`.
694
+ **`report.md` and this renderer's output are two different files** — `report.md` is Claude-authored
695
+ prose that includes a copy of this renderer's Markdown as one section, not this renderer's own
696
+ output; see [Where artifacts live](#where-artifacts-live) for the exact contract.
697
+
698
+ **`--html` output** is a self-contained page (one file, inlined styles) — the default artifact to
699
+ hand a user, per the skill.
700
+
701
+ ---
702
+
703
+ ## `amicus council stats`
704
+
705
+ ```
706
+ amicus council stats [--json]
707
+ ```
708
+
709
+ Reads the **append-only ledger** (`council-ledger.jsonl`, written by every non-`--no-ledger`
710
+ `council tally` call) and aggregates per-model reliability across **all past council runs on this
711
+ machine** — this is historical, cross-run data, not anything from a single tally/verdict. Thin CLI
712
+ wrapper over `deriveReliability()` (`src/council/ledger.js`).
713
+
714
+ Since v4.0 (council schema v2), `--json` wraps the rows in the family envelope —
715
+ `{ "schemaVersion": 2, "type": "council-stats", "models": [ … ] }` — the per-model row
716
+ shape below is unchanged. (Pre-4.0 emitted the bare array.)
717
+
718
+ **Output**, one row per model that has ever appeared in `meta.models`:
719
+
720
+ | Field | Meaning |
721
+ |---|---|
722
+ | `runs` | Number of ledger rows for this model (one per council run it participated in). |
723
+ | `lowN` | `true` when `runs < 3` — treat the numbers as noisy. |
724
+ | `avgStreetCredPeersOnly` | Mean of `streetCredPeersOnly` across all runs (`null` if the model was never judged). |
725
+ | `lifetimeConfirmRate` | Mean, across runs, of `(findings this model raised that landed Confirmed) / (findings this model raised)`. `null` when `judged` was false for every run or the model raised nothing. |
726
+ | `lifetimeFactErrorRate` | Same shape, but for the `Disputed` tier — a proxy for how often the bench caught this model asserting something wrong. |
727
+ | `conformance` | Tally of `{clean, repaired, unstructured}` counts — how often this model's Stage-1 findings JSON needed a repair re-prompt. |
728
+
729
+ This is the data source the `second-opinion` skill's Stage 0 model recommendations and the
730
+ `MODEL-NOTES.md` quantitative table both read — **never hand-edit reliability numbers there**;
731
+ they come from here.
732
+
733
+ ---
734
+
735
+ ## Council presets: `save` / `list` / `show`
736
+
737
+ ```
738
+ amicus council save <name> --models a,b,c
739
+ amicus council list [--json]
740
+ amicus council show <name> [--json]
741
+ ```
742
+
743
+ A named preset is just a saved `--models`-style list that `--council <name>` (on `fanout` and the
744
+ `amicus_fanout` MCP tool) can run in one shot, instead of spelling out `--models` every time.
745
+
746
+ - **`save`** validates ≥2 members (each must resolve via the same alias/catalog logic
747
+ `resolveCouncilMembers` uses — a known alias, or a raw `provider/model` id containing `/`) and
748
+ writes them to `~/.config/amicus/config.json` under `councils.<name>`. Overwrites silently
749
+ report `overwritten: true` in `--json` mode — this is also how you shadow a built-in bench.
750
+ - **`list`** shows your saved councils **plus** the three built-in benches (`free`, `budget`,
751
+ `frontier`), each marked `builtin: true`. If a saved council shares a name with a built-in,
752
+ **both** entries are listed — the saved one (`builtin: false`) is the one `--council <name>`
753
+ actually resolves to; the built-in entry gets `shadowed: true`.
754
+ - **`show <name>`** resolves a name exactly like `--council` does (user config first, built-in
755
+ fallback) and reports the raw members plus a `resolved`/`dropped` split — diagnostic-only, so it
756
+ still reports even for a council currently below the 2-member usable minimum (unlike an actual
757
+ run, which refuses below 2).
758
+
759
+ **Built-in benches** (work with zero setup): `free` (zero-cost `:free`-suffixed OpenRouter models,
760
+ resolved dynamically from the live catalog), `budget` (cheap workhorses, one per vendor family),
761
+ `frontier` (premium flagships, one per vendor family).
762
+
763
+ ---
764
+
765
+ ## Worked example
766
+
767
+ Everything below was run against the real `amicus` binary while writing this page — no mocked
768
+ output. Two reviewers (`deepseek`, `gpt`) reviewed a pricing page; deepseek raised one finding,
769
+ gpt raised two.
770
+
771
+ **1. `tally-input.json`** — assembled by Claude at the end of Stage 2:
772
+
773
+ ```json
774
+ {
775
+ "meta": {
776
+ "runId": "pricing-page-council",
777
+ "runType": "design-review",
778
+ "date": "2026-07-02T18:00:00Z",
779
+ "models": ["deepseek", "gpt"],
780
+ "chair": "deepseek",
781
+ "claudeInCouncil": false
782
+ },
783
+ "findings": [
784
+ { "id": "A1", "raiser": "deepseek", "severity": "major",
785
+ "claim": "The three-tier pricing table omits a monthly/annual toggle." },
786
+ { "id": "B1", "raiser": "gpt", "severity": "minor",
787
+ "claim": "The 'Enterprise' tier has no visible CTA button." },
788
+ { "id": "B2", "raiser": "gpt", "severity": "blocker",
789
+ "claim": "Listed prices contradict the numbers in the FAQ section." }
790
+ ],
791
+ "adjudications": [
792
+ { "findingId": "A1", "judge": "deepseek", "verdict": "agree" },
793
+ { "findingId": "A1", "judge": "gpt", "verdict": "agree" },
794
+ { "findingId": "B1", "judge": "deepseek", "verdict": "neutral" },
795
+ { "findingId": "B1", "judge": "gpt", "verdict": "agree" },
796
+ { "findingId": "B2", "judge": "deepseek", "verdict": "dispute" },
797
+ { "findingId": "B2", "judge": "gpt", "verdict": "agree" }
798
+ ],
799
+ "rankings": [
800
+ { "judge": "deepseek", "order": ["gpt", "deepseek"] },
801
+ { "judge": "gpt", "order": ["gpt", "deepseek"] }
802
+ ],
803
+ "runStats": [
804
+ { "model": "deepseek", "role": "council", "wasChair": true, "conformance": "clean",
805
+ "status": "complete", "durationMs": 41230,
806
+ "usage": { "cost": { "amount": 0.038, "source": "reported" } } },
807
+ { "model": "gpt", "role": "council", "wasChair": false, "conformance": "clean",
808
+ "status": "complete", "durationMs": 37810,
809
+ "usage": { "cost": { "amount": 0.052, "source": "reported" } } }
810
+ ]
811
+ }
812
+ ```
813
+
814
+ **2. Tally** — note deepseek's `B2` dispute makes it Contested even though gpt agrees; `A1` gets
815
+ one uncontested peer agreement (Confirmed, thin); `B1` draws no agree/dispute at all (Singleton):
816
+
817
+ ```bash
818
+ $ amicus council tally tally-input.json --json > tally.json
819
+ $ amicus council tally tally-input.json
820
+ Council tally (pricing-page-council)
821
+ Confirmed 1 Contested 1 Singleton 1 Disputed 0
822
+ Cost: $0.0900
823
+ ```
824
+
825
+ **3. Stage 4 decisions** — Claude presents the three tiers, the user decides:
826
+
827
+ ```json
828
+ [
829
+ { "id": "A1", "decision": "accepted", "applied": true },
830
+ { "id": "B1", "decision": "deferred" },
831
+ { "id": "B2", "decision": "accepted", "applied": true }
832
+ ]
833
+ ```
834
+
835
+ **4. Verdict** — merges the tally record with the decisions above:
836
+
837
+ ```bash
838
+ $ amicus council verdict tally.json --decisions decisions.json -o verdict.json
839
+ Verdict (schema v2, pricing-page-council) → verdict.json
840
+ accepted 2 deferred 1
841
+ ```
842
+
843
+ **5. Report** — deterministic rendering of `verdict.json` (`--md` shown; `--html` produces the
844
+ same content as a self-contained page):
845
+
846
+ ```bash
847
+ $ amicus council report verdict.json --md
848
+ ```
849
+ ```
850
+ # Council Report — design-review (pricing-page-council)
851
+
852
+ _2026-07-02T18:00:00Z · chair: deepseek · council: deepseek, gpt_
853
+
854
+ ## Verdict summary
855
+
856
+ | Tier | Count |
857
+ |---|---|
858
+ | Disputed | 0 |
859
+ | Contested | 1 |
860
+ | Confirmed | 1 |
861
+ | Singleton | 1 |
862
+
863
+ ## Adjudication matrix
864
+
865
+ | Finding | Sev | Raiser | deepseek | gpt | Tier | Decision |
866
+ |---|---|---|---|---|---|---|
867
+ | A1 | major | deepseek | ✓* | ✓ | Confirmed | accepted |
868
+ | B1 | minor | gpt | – | ✓* | Singleton | deferred |
869
+ | B2 | blocker | gpt | ✗ | ✓* | Contested | accepted |
870
+
871
+ _Legend: ✓ agree · ✗ dispute · – neutral · `*` raiser's own vote_
872
+
873
+ ## Street-cred (peers-only; lower = better)
874
+
875
+ | Model | peers-only | with-self |
876
+ |---|---|---|
877
+ | deepseek | 2.00 | 2.00 |
878
+ | gpt | 1.00 | 1.00 |
879
+
880
+ ## Findings by tier
881
+
882
+ ### Contested
883
+ - **B2** (blocker, raiser gpt) — a0/d1/n0 — accepted (applied)
884
+
885
+ ### Confirmed
886
+ - **A1** (major, raiser deepseek) — a1/d0/n0 — accepted (applied)
887
+
888
+ ### Singleton
889
+ - **B1** (minor, raiser gpt) — a0/d0/n1 — deferred
890
+
891
+ ## Cost
892
+
893
+ | Model | Status | Duration | Cost |
894
+ |---|---|---|---|
895
+ | deepseek | complete | 41s | $0.0380 |
896
+ | gpt | complete | 38s | $0.0520 |
897
+ | **Wave total** | | | $0.0900 |
898
+ ```
899
+
900
+ **6. Stats** — after this run's `tally` call appended to the ledger:
901
+
902
+ ```bash
903
+ $ amicus council stats
904
+ model runs avg-cred confirm fact-err notes
905
+ deepseek 1 2.00 1.00 0.00 low-N
906
+ gpt 1 1.00 0.00 0.00 low-N
907
+ ```
908
+
909
+ (`low-N` because each model has only 1 recorded run — `runs < 3`.)
910
+
911
+ **7. Presets**, for reference (independent of the run above):
912
+
913
+ ```bash
914
+ $ amicus council save my-bench --models deepseek,gpt,gemini
915
+ Saved council 'my-bench': deepseek, gpt, gemini
916
+
917
+ $ amicus council list
918
+ Councils:
919
+ my-bench deepseek, gpt, gemini
920
+ free [built-in]
921
+ budget [built-in]
922
+ frontier [built-in]
923
+
924
+ $ amicus council show my-bench
925
+ Council 'my-bench'
926
+ members: deepseek, gpt, gemini
927
+ resolved: deepseek, gpt, gemini
928
+ ```
929
+
930
+ ---
931
+
932
+ ## Where artifacts live
933
+
934
+ Every run writes to a run folder — `output/<stem>-council/` (or `./second-opinion/<stem>-council/`
935
+ if no `output/` directory exists), per the skill's Stage 0. This section cross-checks against
936
+ [SKILL.md's "Output & naming"](../skills/second-opinion/SKILL.md#output--naming) — treat that
937
+ section as authoritative if the two ever drift; file an issue if they do.
938
+
939
+ | File | Written by | Contains |
940
+ |---|---|---|
941
+ | `review-<model>.md` × N | Claude, from each Stage-1 leg's output | Prose review + trailing findings JSON block (this is what `council validate` checks). |
942
+ | `crossreview-matrix.md` | Claude, after Stage 2 | De-anonymized adjudication grid + street-cred table (hand-assembled from the tally record, not this renderer). |
943
+ | `tally.json` | `amicus council tally --json`, redirected to disk | The tally record — input to `council verdict`. |
944
+ | `verdict.md` | Claude, saved from the Stage-3 chair call | The chair's raw synthesized verdict (prose, unedited by Claude). |
945
+ | `verdict.json` | `amicus council verdict` | Schema-stamped merge of the tally record + Stage-4 decisions. See [provenance](#amicus-council-verdict). |
946
+ | `decisions.json` | Claude, during Stage 4 | The array passed to `council verdict --decisions`. |
947
+ | `report.md` | **Claude**, in Stage 5 | The chair's synthesis + the full Stage-4 decision log + a run-stats table (stage/model/status/duration/cost per call). **This is Claude-authored prose, not this page's renderer output.** |
948
+ | `report.html` | `amicus council report verdict.json --html` | The **deterministic** rendering shown in the [worked example](#worked-example) above — adjudication matrix, street-cred table, findings-by-tier, cost. No chair prose, no decision-log narrative. This is the default artifact handed to the user. |
949
+ | `<stem>-reviewed.<ext>` | Claude, in Stage 5 | The source artifact with accepted findings applied (editable-source runs only). |
950
+
951
+ **The one thing worth over-stating:** `report.md` and `report.html` are **not** the same content in
952
+ two formats. `report.html` is `amicus council report`'s pure render of `verdict.json` — deterministic,
953
+ no model involved. `report.md` is Claude's own synthesis document, written by hand in Stage 5,
954
+ which *includes* a copy of the same renderer's Markdown output as one section but also carries the
955
+ chair's prose verdict and the full decision log that the renderer never sees. If you only need the
956
+ deterministic data, run `amicus council report` yourself against any `verdict.json` — you don't
957
+ need Claude or a live council run to regenerate it.
958
+
959
+ ---
960
+
961
+ ## See also
962
+
963
+ - **[skills/second-opinion/SKILL.md](../skills/second-opinion/SKILL.md)** — the orchestration
964
+ recipe that drives an actual council run (model selection, briefings, anonymization, the Stage-2
965
+ tally assembly recipe, Stage 4 decision presentation).
966
+ - **[skills/second-opinion/COUNCIL-DESIGN.md](../skills/second-opinion/COUNCIL-DESIGN.md)** — the
967
+ design spec behind the tier cascade and scoring model.
968
+ - **[docs/usage.md](./usage.md)** — full CLI/MCP flag reference for every command, including
969
+ `council`.
970
+ - **[README "The Council"](../README.md#the-council)** — the narrative overview and cost framing.