amicus 2.1.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "amicus",
3
- "version": "2.1.0",
3
+ "version": "3.0.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
@@ -5,6 +5,89 @@ All notable changes to Amicus are documented here. Format follows
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [3.0.0] - 2026-07-15
9
+
10
+ ### ⚠️ Breaking
11
+
12
+ - **Node >=22.12 is now required** (`engines.node`). Amicus 3.0 fails fast on older Node with a
13
+ clear message instead of a confusing error deep in provisioning. This is driven by
14
+ `@electron/get` 5.x (ESM-only, requires Node >=22.12), which the Electron self-heal depends on.
15
+ Node 18/20 users — **including headless / council-only users who never touch the GUI** — must
16
+ upgrade Node.
17
+ - **Electron upgraded 28 -> 43.1.1**, which drops OS support for **Windows 8/8.1, Windows Server
18
+ 2012/2012 R2, and macOS 11**. The interactive GUI will not run there; headless runs and the
19
+ council are unaffected.
20
+
21
+ ### Changed
22
+
23
+ - **Electron 28.3.3 -> 43.1.1**, clearing the outstanding high-severity `npm audit` finding
24
+ (ASAR Integrity Bypass, GHSA-vmqv-hx8q-j7mg). Amicus runs Electron **unpackaged**, so the
25
+ ASAR-integrity attack class never applied to its deployment — the concrete effect is a clean
26
+ audit and staying on a supported Electron line.
27
+ - **Content view migrated from the deprecated `BrowserView` to `WebContentsView`**
28
+ (`mainWindow.contentView.addChildView`). All four windows now set `sandbox` explicitly.
29
+ - **`@electron/get` 2.x -> 5.x** (now a direct `dependency`, ESM-only). The self-heal defers a
30
+ lazy dynamic `import()` to the network path and bounds the download with an `AbortSignal`
31
+ timeout (5.x dropped the old `got`-style timeout).
32
+ - CI matrices raised to Node 22/24.
33
+
34
+ ### Fixed
35
+
36
+ - Runtime Node-version guard (`src/utils/node-version-guard.js`) fires early in `bin/amicus.js`,
37
+ before heavy imports, so an unsupported Node fails with an actionable message.
38
+
39
+ ### Known limitations
40
+
41
+ - `@electron/get` 5.x uses native `fetch`, which does **not** honor `HTTPS_PROXY` / `NO_PROXY`.
42
+ Provisioning Electron behind a corporate proxy needs a manual cache copy or `ELECTRON_MIRROR`
43
+ (see `docs/troubleshooting.md`). Headless runs and the council never download Electron.
44
+
45
+ ## [2.2.0] - 2026-07-14
46
+
47
+ ### Added
48
+
49
+ - **Optional council elements** (second-opinion skill): four new opt-in behaviors, presented
50
+ together with Claude-in-the-council as a single numbered Stage-0 menu — all default OFF, enabled
51
+ only when the user names them, and the launch confirmation must enumerate what's on:
52
+ - **Critic seat** — one bench member swaps to a four-pass adversarial brief (adversarial pass,
53
+ edge-case hunt, consistency check, executability test), launched as a concurrent solo beside
54
+ the fanout wave (`role: "critic"`). Its findings enter the same anonymized bundle and are
55
+ peer-adjudicated like any other seat's — the council disciplines the critic.
56
+ - **Expert lenses** — each seat reviews through a distinct expert perspective (panel domain
57
+ scoped with the user: business/technical/customer/financial/custom). Lens runs always tally
58
+ `--no-ledger` and the report discloses the weakened cross-review anonymity.
59
+ - **Debate mode** — a new Stage 2.5 rebuttal round: Contested/Disputed findings go back to their
60
+ raisers to DEFEND / AMEND / WITHDRAW, disputing judges re-vote, then the final ledger-recorded
61
+ tally. Exactly one round; withdrawn findings are auto-denied and listed in the report.
62
+ - **Chair verdict scale** — the chair closes with 3–5 hard questions and a final parseable
63
+ `VERDICT: Ship it | Fix these first | Fundamental rethink` line, surfaced at the top of the
64
+ report.
65
+ - New `skills/second-opinion/SEAT-BRIEFS.md` — briefing boilerplate for the elements plus a
66
+ standard anti-sycophancy clause now required in **every** Stage-1 briefing. Critic and lens
67
+ methodologies adapted from the `/critic` and `/debate` agents in John Renaldi's product-kit
68
+ (MIT), with deliberate deviations documented (no findings quota; verdict moved to the chair).
69
+ - Zero engine changes: free-form `runStats[].role` labels, tally-input re-assembly, and the
70
+ existing `--no-ledger` flag cover all four elements.
71
+
72
+ ### Changed
73
+
74
+ - `COUNCIL-DESIGN.md` gains §12 documenting the elements, their caveats (critic
75
+ self-identification in cross-review; lens anonymity/ledger trade-offs), and parallel panels as
76
+ future work. The `/council` command accepts pre-requested elements in its arguments.
77
+ - MODEL-NOTES seed: fold-back of the v2.2.0 verification council — claim-class dedup
78
+ adjudication limit, minimax debut (strong critic seat), qwen-coder debut.
79
+
80
+ ### Fixed
81
+
82
+ - Resurrected both Windows e2e integration suites, silently dead since before the rebrand:
83
+ Node's `spawn()` cannot execute `node_modules/.bin` `.cmd` shims on Windows, so the Electron
84
+ toolbar suite (electron shim) and the OpenCode server test helper ENOENT'd without a visible
85
+ error. The helper now uses the shared `ensureNodeModulesBinInPath()` (which adds the platform
86
+ `opencode.exe` dirs to PATH) and the toolbar suite spawns the real binary via
87
+ `require('electron')`. Also refreshed two stale pins the dead suites never caught: the wave
88
+ document's `schemaVersion` (now pinned to the shared `SCHEMA_VERSION` constant instead of a
89
+ literal `1`) and the pre-rebrand `Sidecar` toolbar brand assertion (now `Amicus`).
90
+
8
91
  ## [2.1.0] - 2026-07-04
9
92
 
10
93
  ### Added
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  **A multi-model LLM Council for Claude — with a parallel AI window underneath.**
6
6
 
7
- ![Amicus: an LLM Council and a parallel AI window for Claude](./docs/hero.png)
7
+ ![The Amicus council mid-ritual: five models Gemini 3 Pro, Llama 4, Grok 4, Claude Opus — reading the same material independently, chaired by GPT-5](./docs/council.png)
8
8
 
9
9
  Hand Claude a plan, a design, a diff, an architecture decision, a manuscript — anything — and say *council review this*: Amicus routes it through several models from different families, has them anonymously cross-review each other, and a non-Claude chair synthesizes a verdict you turn into accept/deny edits. Or skip the ceremony and **fork** a single conversation to Gemini, GPT, DeepSeek, or any other model — it works in parallel with full context, and you **fold** the result back when you're ready. Claude orchestrates throughout; you stay in your editor.
10
10
 
@@ -209,11 +209,19 @@ Everything you need before your first run, and what's optional.
209
209
  findings-by-tier, cost — no chair prose). This is the default artifact handed to the user.
210
210
  - For an **editable source**, the accepted edits land in `<stem>-reviewed.<ext>` next to the original.
211
211
 
212
- **Claude in the council** (default off): you can add Claude's own fresh review to the bundle so the bench ranks and adjudicates it Claude is *judged* but never votes or chairs, so the verdict stays independent.
212
+ **Optional council elements** (v2.2.0, all default off): five opt-in behaviors, offered once as a menu at launchnothing turns on unless you name it, and the confirmation lists exactly what's on.
213
213
 
214
- **Cost is disclosed up front.** Before any model launches, you see the run shape for example:
214
+ - **Critic seat** one reviewer swaps to a four-pass adversarial brief (adversarial pass, edge-case hunt, consistency check, executability test). Its findings enter the same anonymized bundle as everyone else's, so the bench disciplines the critic: manufactured negativity lands Disputed and dies in the tally.
215
+ - **Expert lenses** — each reviewer takes a distinct expert perspective; you pick the panel domain (business, technical, customer, financial, or custom). Lens runs never feed the reliability ledger, and the report discloses the weakened cross-review anonymity.
216
+ - **Debate mode** — after cross-review, every Contested or Disputed finding goes back to its raiser to **defend, amend, or withdraw**, and the disputing judges re-vote. Exactly one rebuttal round, then the final tally.
217
+ - **Chair verdict scale** — the chair closes with 3–5 hard questions and one parseable line: `VERDICT: Ship it | Fix these first | Fundamental rethink`.
218
+ - **Claude in the council** — Claude adds its own fresh review to the bundle so the bench ranks and adjudicates it. Claude is *judged* but never votes or chairs, so the verdict stays independent.
215
219
 
216
- > This run uses 3 council models across 2 fanout waves + 1 chair call (~7 model runs).
220
+ The critic and lens methodologies are adapted from the `/critic` and `/debate` agents in [John Renaldi's product-kit](https://github.com/jrenaldi79/plugin-marketplace) (MIT); the briefing boilerplate lives in [`skills/second-opinion/SEAT-BRIEFS.md`](./skills/second-opinion/SEAT-BRIEFS.md).
221
+
222
+ **Cost is disclosed up front.** Before any model launches, you see the run shape — including any enabled optional elements — for example:
223
+
224
+ > This run uses 3 council models across 2 fanout waves + 1 chair call, with critic seat + debate mode ON (~7 base runs + up to 6 rebuttal calls).
217
225
 
218
226
  Then the council waits for your confirmation.
219
227
 
@@ -311,7 +319,7 @@ $ amicus status demo123 --json
311
319
  "taskId": "demo123",
312
320
  "status": "complete",
313
321
  "elapsed": "5m 0s",
314
- "version": "2.1.0",
322
+ "version": "3.0.0",
315
323
  "model": "google/gemini-2.5-flash",
316
324
  "phase": "terminal"
317
325
  }
package/bin/amicus.js CHANGED
@@ -7,6 +7,11 @@
7
7
  * Routes commands to appropriate handlers.
8
8
  */
9
9
 
10
+ // Node version guard: fail fast on unsupported Node versions
11
+ const { checkNodeVersion } = require('../src/utils/node-version-guard');
12
+ const _nv = checkNodeVersion(process.version);
13
+ if (!_nv.ok) { process.stderr.write(_nv.message + '\n'); process.exit(1); }
14
+
10
15
  // Load API keys from all sources: process.env > amicus .env > auth.json
11
16
  const { loadCredentials } = require('../src/utils/env-loader');
12
17
  loadCredentials();
@@ -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]
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
 
@@ -24,3 +24,9 @@ and explicit user confirmation; Stage 1 independent reviews, running
24
24
  cross-review, followed by `amicus council tally` once cross-review settles;
25
25
  Stage 3 council-chair synthesis; Stage 4 the accept/deny decision pass; and
26
26
  Stage 5, which runs `amicus council verdict` to write the final `verdict.json`.
27
+
28
+ The user may also name **optional council elements** in the arguments (critic seat,
29
+ expert lenses, debate mode, verdict scale, Claude in the council). All elements
30
+ default OFF; the skill's Stage-0 menu is the single opt-in point. If the user named
31
+ elements here, carry them into Stage 0 as pre-requested — confirm them back by name
32
+ instead of re-asking — and never enable an element the user did not explicitly name.
@@ -54,9 +54,9 @@
54
54
  * @param {() => boolean} [deps.hasCompleted] - Whether the fold's
55
55
  * `[SIDECAR_FOLD]` stdout write has actually succeeded. Falls back to
56
56
  * `hasFolded()` when omitted.
57
- * @param {(mainWindow: object, contentView: object) => Promise<void>} deps.triggerFold
57
+ * @param {(mainWindow: object, opencodeView: object) => Promise<void>} deps.triggerFold
58
58
  * - The SAME fold.js closure used by the shortcut/toolbar/IPC paths.
59
- * @returns {{ handleClose: (event: object, mainWindow: object, contentView: object) => void }}
59
+ * @returns {{ handleClose: (event: object, mainWindow: object, opencodeView: object) => void }}
60
60
  */
61
61
  function createCloseGuard({ hasFolded, isFolding, hasCompleted, triggerFold }) {
62
62
  const checkIsFolding = isFolding || hasFolded;
@@ -78,7 +78,7 @@ function createCloseGuard({ hasFolded, isFolding, hasCompleted, triggerFold }) {
78
78
  }
79
79
  }
80
80
 
81
- function handleClose(event, mainWindow, contentView) {
81
+ function handleClose(event, mainWindow, opencodeView) {
82
82
  if (checkHasCompleted()) {
83
83
  // Fold already completed — proceed exactly like the pre-existing
84
84
  // behavior (no interception, no destroy call from the guard itself;
@@ -110,7 +110,7 @@ function createCloseGuard({ hasFolded, isFolding, hasCompleted, triggerFold }) {
110
110
  }
111
111
  closeFoldAttempted = true;
112
112
 
113
- Promise.resolve(triggerFold(mainWindow, contentView)).then(() => {
113
+ Promise.resolve(triggerFold(mainWindow, opencodeView)).then(() => {
114
114
  // triggerFold can RESOLVE without ever calling mainWindow.close() —
115
115
  // its outer catch swallows failures (including a synchronous throw
116
116
  // from the post-write nudge-overlay executeJavaScript call, which can
package/electron/fold.js CHANGED
@@ -41,13 +41,13 @@ function createFoldHandler(state) {
41
41
  let folded = false;
42
42
  let completed = false;
43
43
 
44
- async function triggerFold(mainWindow, contentView) {
44
+ async function triggerFold(mainWindow, opencodeView) {
45
45
  if (folded) { return; }
46
46
  folded = true;
47
47
  completed = false;
48
48
 
49
49
  // Show fold progress in toolbar and content overlay
50
- showFoldOverlay(mainWindow, contentView);
50
+ showFoldOverlay(mainWindow, opencodeView);
51
51
 
52
52
  try {
53
53
  // Ask the model to generate a structured summary
@@ -76,8 +76,8 @@ function createFoldHandler(state) {
76
76
  logger.info('Fold completed', { taskId: state.taskId });
77
77
 
78
78
  // Show nudge overlay before closing
79
- if (contentView) {
80
- await contentView.webContents.executeJavaScript(`
79
+ if (opencodeView) {
80
+ await opencodeView.webContents.executeJavaScript(`
81
81
  (function() {
82
82
  var overlay = document.getElementById('amicus-fold-overlay');
83
83
  if (overlay) {
@@ -147,7 +147,7 @@ function createFoldHandler(state) {
147
147
  * Note: The JS strings below contain only hardcoded markup (no user input),
148
148
  * so there is no XSS risk from DOM manipulation.
149
149
  */
150
- function showFoldOverlay(mainWindow, contentView) {
150
+ function showFoldOverlay(mainWindow, opencodeView) {
151
151
  if (mainWindow) {
152
152
  mainWindow.webContents.executeJavaScript(`
153
153
  (function() {
@@ -174,7 +174,7 @@ function showFoldOverlay(mainWindow, contentView) {
174
174
  })();
175
175
  `).catch(() => {});
176
176
  }
177
- if (contentView) {
177
+ if (opencodeView) {
178
178
  // Scope token vars to the overlay container so var(--x) resolves without
179
179
  // touching OpenCode's own :root (which would clobber its CSS variables).
180
180
  const rawCss = tokenCss({ absoluteFontUrls: true });
@@ -182,9 +182,9 @@ function showFoldOverlay(mainWindow, contentView) {
182
182
  // custom properties are defined on #amicus-fold-overlay and inherited by
183
183
  // its descendants. @font-face blocks are left at global scope (no selector).
184
184
  const scopedCss = rawCss.replace(/:root\s*\{/, '#amicus-fold-overlay {');
185
- contentView.webContents.insertCSS(scopedCss).catch(() => {});
185
+ opencodeView.webContents.insertCSS(scopedCss).catch(() => {});
186
186
 
187
- contentView.webContents.executeJavaScript(`
187
+ opencodeView.webContents.executeJavaScript(`
188
188
  (function() {
189
189
  if (!document.getElementById('fold-spin-style')) {
190
190
  var style = document.createElement('style');
@@ -5,8 +5,8 @@
5
5
  * unit-testable (main.js itself runs heavy Electron side effects at import).
6
6
  *
7
7
  * - isPrivilegedSender: pin privileged IPC handlers to the toolbar window so a
8
- * compromised/remote page in the OpenCode BrowserView cannot invoke them (M9).
9
- * - isAllowedContentNavigation: pin the OpenCode BrowserView to its localhost
8
+ * compromised/remote page in the OpenCode WebContentsView cannot invoke them (M9).
9
+ * - isAllowedContentNavigation: pin the OpenCode WebContentsView to its localhost
10
10
  * origin so it cannot be navigated off to an attacker-controlled page (M9).
11
11
  * - handleFatalException: EPIPE stays a no-op; any other uncaught exception is
12
12
  * logged and then quits the app rather than leaving a wedged invisible shell
@@ -26,7 +26,7 @@ function isPrivilegedSender(event, getToolbarWindow) {
26
26
  }
27
27
 
28
28
  /**
29
- * Whether a navigation target is allowed for the OpenCode content BrowserView.
29
+ * Whether a navigation target is allowed for the OpenCode content WebContentsView.
30
30
  * Only the OpenCode localhost origin (any path) is permitted; everything else
31
31
  * (external http(s), file:, etc.) is blocked. data: URLs are allowed so the
32
32
  * in-app load-error page can render.
package/electron/main.js CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Amicus Electron Shell - v3
3
3
  *
4
- * Uses BrowserView to split the window into two physical areas:
4
+ * Uses WebContentsView to split the window into two physical areas:
5
5
  * - Top: OpenCode Web UI (gets its own viewport, no CSS conflicts)
6
6
  * - Bottom 40px: Amicus toolbar (branding, task ID, timer, fold button)
7
7
  *
@@ -12,7 +12,7 @@
12
12
  * Spec Reference: §4.4 Electron Wrapper
13
13
  */
14
14
 
15
- const { app, BrowserWindow, BrowserView, globalShortcut, ipcMain, screen } = require('electron');
15
+ const { app, BrowserWindow, WebContentsView, globalShortcut, ipcMain, screen } = require('electron');
16
16
  const path = require('path');
17
17
  const { logger } = require('../src/utils/logger');
18
18
  const { buildToolbarHTML, TOOLBAR_H, getBrandName } = require('./toolbar');
@@ -84,7 +84,7 @@ const OPENCODE_URL = `http://localhost:${OPENCODE_PORT}`;
84
84
  // ============================================================================
85
85
 
86
86
  let mainWindow = null;
87
- let contentView = null;
87
+ let opencodeView = null;
88
88
  let currentToolbarH = TOOLBAR_H;
89
89
 
90
90
  const foldHandler = createFoldHandler({
@@ -127,7 +127,7 @@ function createAmicusWindow() {
127
127
  icon: ICON_PATH,
128
128
  webPreferences: {
129
129
  preload: path.join(__dirname, 'preload.js'),
130
- contextIsolation: true, nodeIntegration: false,
130
+ contextIsolation: true, nodeIntegration: false, sandbox: true,
131
131
  }
132
132
  });
133
133
 
@@ -157,27 +157,27 @@ function createAmicusWindow() {
157
157
  mainWindow.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(toolbarHtml)}`);
158
158
  mainWindow.webContents.on('page-title-updated', (e) => e.preventDefault());
159
159
 
160
- // BrowserView for OpenCode content. It uses a MINIMAL preload that exposes no
160
+ // WebContentsView for OpenCode content. It uses a MINIMAL preload that exposes no
161
161
  // privileged bridge — the OpenCode page must not be able to reach the fold /
162
162
  // settings / update IPC (M9). The toolbar window keeps preload.js.
163
- contentView = new BrowserView({
163
+ opencodeView = new WebContentsView({
164
164
  webPreferences: {
165
165
  preload: path.join(__dirname, 'preload-content.js'),
166
- contextIsolation: true, nodeIntegration: false,
166
+ contextIsolation: true, nodeIntegration: false, sandbox: true,
167
167
  }
168
168
  });
169
169
 
170
170
  // Pin the content view to the OpenCode localhost origin: block any attempt to
171
171
  // navigate it off-origin or open new windows (defense-in-depth, M9). data:
172
172
  // URLs (the in-app load-error page) are still allowed by the guard.
173
- contentView.webContents.on('will-navigate', (event, targetUrl) => {
173
+ opencodeView.webContents.on('will-navigate', (event, targetUrl) => {
174
174
  if (!isAllowedContentNavigation(targetUrl, OPENCODE_URL)) {
175
175
  logger.warn('Blocked content-view navigation', { targetUrl });
176
176
  event.preventDefault();
177
177
  }
178
178
  });
179
- contentView.webContents.setWindowOpenHandler(() => ({ action: 'deny' }));
180
- // Load OpenCode off-screen first; only attach BrowserView after rebranding
179
+ opencodeView.webContents.setWindowOpenHandler(() => ({ action: 'deny' }));
180
+ // Load OpenCode off-screen first; only attach WebContentsView after rebranding
181
181
  // to prevent the OpenCode logo/splash from flashing during load.
182
182
  mainWindow.on('resize', updateContentBounds);
183
183
 
@@ -190,9 +190,9 @@ function createAmicusWindow() {
190
190
  // is token-driven — it inlines tokenCss() and remaps OpenCode's own :root
191
191
  // custom properties — so it tracks the toolbar without brittle class
192
192
  // selectors. insertCSS is more reliable than preload DOM injection in a
193
- // BrowserView. NOTE: the live visual match is a user-side CDP/manual check.
194
- contentView.webContents.on('dom-ready', () => {
195
- contentView.webContents.insertCSS(buildOpencodeThemeCSS()).catch(() => {});
193
+ // WebContentsView. NOTE: the live visual match is a user-side CDP/manual check.
194
+ opencodeView.webContents.on('dom-ready', () => {
195
+ opencodeView.webContents.insertCSS(buildOpencodeThemeCSS()).catch(() => {});
196
196
  });
197
197
 
198
198
  // Navigate directly to the session URL to bypass the project selection screen.
@@ -205,7 +205,7 @@ function createAmicusWindow() {
205
205
  // failsafe, a failed/stalled UI load leaves an invisible window and a
206
206
  // silently hung process (the historical "Starting up... | 0 messages" bug).
207
207
  const failsafe = attachLoadFailsafe({
208
- webContents: contentView.webContents,
208
+ webContents: opencodeView.webContents,
209
209
  timeoutMs: parseInt(process.env.AMICUS_GUI_LOAD_TIMEOUT_MS || '', 10) || undefined,
210
210
  onFail: ({ reason, errorCode, errorDescription, validatedURL }) => {
211
211
  logger.error('OpenCode UI failed to load', {
@@ -215,12 +215,12 @@ function createAmicusWindow() {
215
215
  const html = buildLoadErrorHTML({
216
216
  url: validatedURL || contentUrl, errorCode, errorDescription
217
217
  });
218
- contentView.webContents
218
+ opencodeView.webContents
219
219
  .loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(html)}`)
220
220
  .catch(() => {});
221
221
  }
222
222
  // On timeout, show whatever is in flight rather than aborting the load.
223
- mainWindow.addBrowserView(contentView);
223
+ mainWindow.contentView.addChildView(opencodeView);
224
224
  updateContentBounds();
225
225
  if (!process.env.AMICUS_HEADLESS_TEST) {
226
226
  mainWindow.show();
@@ -228,16 +228,16 @@ function createAmicusWindow() {
228
228
  }
229
229
  });
230
230
 
231
- contentView.webContents.loadURL(contentUrl);
231
+ opencodeView.webContents.loadURL(contentUrl);
232
232
 
233
- contentView.webContents.on('did-finish-load', () => {
233
+ opencodeView.webContents.on('did-finish-load', () => {
234
234
  // Wait for React to render, then rebrand and show window
235
235
  setTimeout(() => {
236
236
  rebrandUI().then(() => {
237
237
  // Disarm only once the window is actually about to show, so a wedged
238
238
  // rebrand/executeJavaScript is still covered by the timeout.
239
239
  failsafe.cancel();
240
- mainWindow.addBrowserView(contentView);
240
+ mainWindow.contentView.addChildView(opencodeView);
241
241
  updateContentBounds();
242
242
  if (!process.env.AMICUS_HEADLESS_TEST) {
243
243
  mainWindow.show();
@@ -247,7 +247,7 @@ function createAmicusWindow() {
247
247
  });
248
248
 
249
249
  globalShortcut.register(FOLD_SHORTCUT, () => {
250
- foldHandler.triggerFold(mainWindow, contentView);
250
+ foldHandler.triggerFold(mainWindow, opencodeView);
251
251
  });
252
252
 
253
253
  // Poll toolbar for button clicks (IPC doesn't work with data: URLs).
@@ -258,7 +258,7 @@ function createAmicusWindow() {
258
258
  if (!action) { return; }
259
259
  mainWindow.webContents.executeJavaScript('window.__amicusToolbarAction = null');
260
260
  if (action === 'fold') {
261
- foldHandler.triggerFold(mainWindow, contentView);
261
+ foldHandler.triggerFold(mainWindow, opencodeView);
262
262
  } else if (action === 'open-settings') {
263
263
  createSettingsChildWindow();
264
264
  }
@@ -300,11 +300,11 @@ function createAmicusWindow() {
300
300
  }
301
301
 
302
302
  mainWindow.on('close', (event) => {
303
- closeGuard.handleClose(event, mainWindow, contentView);
303
+ closeGuard.handleClose(event, mainWindow, opencodeView);
304
304
  });
305
305
  mainWindow.on('closed', () => {
306
306
  mainWindow = null;
307
- contentView = null;
307
+ opencodeView = null;
308
308
  globalShortcut.unregisterAll();
309
309
  app.quit();
310
310
  });
@@ -334,7 +334,7 @@ async function createSetupWindow() {
334
334
  resizable: false,
335
335
  webPreferences: {
336
336
  preload: path.join(__dirname, 'preload-setup.js'),
337
- contextIsolation: true, nodeIntegration: false,
337
+ contextIsolation: true, nodeIntegration: false, sandbox: false, // preload-setup.js require()s shell (not sandbox-safe)
338
338
  }
339
339
  });
340
340
 
@@ -356,9 +356,9 @@ async function createSetupWindow() {
356
356
  // ============================================================================
357
357
 
358
358
  function updateContentBounds() {
359
- if (!mainWindow || !contentView) { return; }
359
+ if (!mainWindow || !opencodeView) { return; }
360
360
  const [w, h] = mainWindow.getContentSize();
361
- contentView.setBounds({ x: 0, y: 0, width: w, height: h - currentToolbarH });
361
+ opencodeView.setBounds({ x: 0, y: 0, width: w, height: h - currentToolbarH });
362
362
  }
363
363
 
364
364
  // Amicus wordmark SVG in the same pixel/block art style as the OpenCode logo.
@@ -387,12 +387,12 @@ const AMICUS_WORDMARK = [
387
387
  ].join('');
388
388
 
389
389
  function rebrandUI() {
390
- if (!contentView) { return Promise.resolve(); }
390
+ if (!opencodeView) { return Promise.resolve(); }
391
391
  const brandName = getBrandName(CLIENT);
392
392
  // The OpenCode logo may be hidden (display:none/visibility:hidden) by preload.js
393
393
  // or insertCSS before this runs. Use a MutationObserver with a fallback timeout
394
394
  // to catch it whenever React renders it into the DOM.
395
- return contentView.webContents.executeJavaScript(`
395
+ return opencodeView.webContents.executeJavaScript(`
396
396
  (function() {
397
397
  document.title = '${brandName}';
398
398
  var header = document.querySelector('#root > div > header');
@@ -436,7 +436,7 @@ function rebrandUI() {
436
436
  // ============================================================================
437
437
 
438
438
  // These handlers are privileged (fold/settings/update/resize). Only the toolbar
439
- // window may invoke them — the OpenCode content BrowserView must not (M9). The
439
+ // window may invoke them — the OpenCode content WebContentsView must not (M9). The
440
440
  // content view no longer gets a bridge preload, but we still validate the
441
441
  // sender as belt-and-suspenders in case a future preload change reintroduces one.
442
442
  const fromToolbar = (event) => isPrivilegedSender(event, () => mainWindow);
@@ -444,7 +444,7 @@ const fromToolbar = (event) => isPrivilegedSender(event, () => mainWindow);
444
444
  // Amicus mode: fold
445
445
  ipcMain.handle('sidecar:fold', (event) => {
446
446
  if (!fromToolbar(event)) { return; }
447
- return foldHandler.triggerFold(mainWindow, contentView);
447
+ return foldHandler.triggerFold(mainWindow, opencodeView);
448
448
  });
449
449
 
450
450
  // Amicus mode: open settings in a child window
@@ -498,7 +498,7 @@ function createSettingsChildWindow() {
498
498
  resizable: false,
499
499
  webPreferences: {
500
500
  preload: path.join(__dirname, 'preload-setup.js'),
501
- contextIsolation: true, nodeIntegration: false,
501
+ contextIsolation: true, nodeIntegration: false, sandbox: false, // shares preload-setup.js (shell)
502
502
  }
503
503
  });
504
504
 
@@ -3,7 +3,7 @@
3
3
  /**
4
4
  * Issue #49 — token-driven theme for the embedded OpenCode web UI.
5
5
  *
6
- * main.js injects this via `contentView.webContents.insertCSS(...)` on
6
+ * main.js injects this via `opencodeView.webContents.insertCSS(...)` on
7
7
  * `dom-ready`. Before #49 that hook only HID OpenCode's header/wordmark; this
8
8
  * module extends it so the embedded chat surface inherits the clay/gold tokens
9
9
  * and matches the token-driven Amicus toolbar (toolbar.js).
@@ -13,7 +13,7 @@
13
13
  * PREFER overriding OpenCode's OWN :root CSS custom properties — a much more
14
14
  * stable surface than `.css-abc123` class selectors. We:
15
15
  * 1. inline the canonical token CSS (tokenCss) so OUR vars + @font-face are
16
- * available inside the BrowserView (absolute font URLs, same as toolbar.js
16
+ * available inside the WebContentsView (absolute font URLs, same as toolbar.js
17
17
  * / setup-ui-styles.js / load-failsafe.js do),
18
18
  * 2. remap a generous superset of OpenCode's plausible theme custom-property
19
19
  * names to our tokens (var(--...)), covering the prefixes OpenCode has
@@ -115,7 +115,7 @@ const ELEMENT_FALLBACKS = `
115
115
  `;
116
116
 
117
117
  /**
118
- * Build the full theme CSS string injected into the OpenCode BrowserView.
118
+ * Build the full theme CSS string injected into the OpenCode WebContentsView.
119
119
  * @returns {string} hide-chrome + inlined tokens + :root overrides + fallbacks.
120
120
  */
121
121
  function buildOpencodeThemeCSS() {
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Content Preload - OpenCode BrowserView (minimal, no privileged bridge)
2
+ * Content Preload - OpenCode WebContentsView (minimal, no privileged bridge)
3
3
  *
4
4
  * The OpenCode Web UI is remote-ish content: it should NOT be able to reach the
5
5
  * privileged sidecar IPC (fold/open-settings/perform-update/...). This preload
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "amicus",
3
- "version": "2.1.0",
3
+ "version": "3.0.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": [
@@ -69,6 +69,7 @@
69
69
  "check:tarball": "node scripts/check-tarball-lifecycle.js"
70
70
  },
71
71
  "dependencies": {
72
+ "@electron/get": "^5.0.0",
72
73
  "@modelcontextprotocol/sdk": "^1.27.0",
73
74
  "@opencode-ai/sdk": "^1.1.36",
74
75
  "dotenv": "^17.2.3",
@@ -78,7 +79,7 @@
78
79
  "zod": "^3.0.0"
79
80
  },
80
81
  "optionalDependencies": {
81
- "electron": "^28.0.0"
82
+ "electron": "^43.1.1"
82
83
  },
83
84
  "devDependencies": {
84
85
  "chrome-remote-interface": "^0.33.3",
@@ -90,7 +91,7 @@
90
91
  "ws": "^8.19.0"
91
92
  },
92
93
  "engines": {
93
- "node": ">=18.0.0"
94
+ "node": ">=22.12.0"
94
95
  },
95
96
  "lint-staged": {
96
97
  "src/**/*.js": [
@@ -1,8 +1,8 @@
1
1
  # Second Opinion v3 — "LLM Council" Design
2
2
 
3
3
  _Status: implemented (v3). v2 (2026-06-03) added the council mechanics; v3 (2026-06-10) swapped the
4
- transport onto the Amicus fanout/JSON engine primitives. v2 history lives in git
5
- (`V2-COUNCIL-DESIGN.md`, deleted at v3)._
4
+ transport onto the Amicus fanout/JSON engine primitives; v3.1 (2026-07-14) added the optional
5
+ council elements (§12). v2 history lives in git (`V2-COUNCIL-DESIGN.md`, deleted at v3)._
6
6
  _Design for `SKILL.md` and `MODEL-NOTES.md` of the `second-opinion` skill._
7
7
 
8
8
  ## 1. Intent
@@ -224,6 +224,70 @@ The append-only `council-ledger.jsonl` (consumed via `amicus council stats`) is
224
224
  four qualitative tiers, if tiers prove too coarse in practice.
225
225
 
226
226
  ## 11. Implementation surface
227
- - `SKILL.md` — the Stage 0–6 council flow on the v3 transport (WS-3: findings contract, tally assembly recipe, `amicus council tally/stats`, `verdict.json`, ledger auto-append).
227
+ - `SKILL.md` — the Stage 0–6 council flow on the v3 transport (WS-3: findings contract, tally assembly recipe, `amicus council tally/stats`, `verdict.json`, ledger auto-append). v3.1 adds the Stage-0 optional-elements menu, the Stage-1 critic/lens launch blocks, Stage 2.5, and the chair verdict-scale addendum.
228
+ - `SEAT-BRIEFS.md` — v3.1 briefing boilerplate for the optional elements + the standard anti-sycophancy clause.
228
229
  - `MODEL-NOTES.md` — qualitative per-model quirks, structural-conformance notes, cost guardrail, Stage-2 briefing tips. Quantitative reliability data now generated by `amicus council stats` (ledger). Engine workarounds that F1/F2/F4 made obsolete were pruned at v3.
229
- - `src/council/` — the deterministic helpers (`findings.js`, `tally.js`, `verdict.js`, `ledger.js`). No other files.
230
+ - `src/council/` — the deterministic helpers (`findings.js`, `tally.js`, `verdict.js`, `ledger.js`). No other files. **v3.1 required zero engine changes**: `runStats[].role` labels are free-form, the rebuttal round is a tally-input re-assembly, and `--no-ledger` already existed.
231
+
232
+ ## 12. Optional council elements (v3.1)
233
+
234
+ The `/critic` and `/debate` behaviors from John Renaldi's product-kit plugin (MIT — the
235
+ elite-advisor and expert-debate-facilitator agents), grafted onto the council. Both source
236
+ agents are single-model theater: one model playing an adversarial reviewer or a panel of
237
+ disagreeing experts, with no external check on manufactured negativity or fake disagreement.
238
+ The council supplies exactly that check — independent models and deterministic peer-validated
239
+ tiers — so the methodologies port over as *optional elements* whose output is disciplined by
240
+ Stage-2 adjudication.
241
+
242
+ **Opt-in contract.** All elements default OFF. They are presented once, as a single numbered
243
+ menu at Stage 0 (after bench/chair selection, before launch confirmation); the user must name
244
+ an element to enable it — silence means off, and the launch confirmation enumerates enabled
245
+ elements by name. "Claude in the council" (§5.4, semantics unchanged) is presented as one item
246
+ of the same menu so there is exactly one opt-in point per run. Elements compose freely except
247
+ critic + lenses on one seat (the critic takes one seat; lenses take the rest).
248
+
249
+ ### 12.1 Critic seat
250
+ One bench member swaps its standard brief for an adversarial four-pass brief (adversarial
251
+ pass, edge-case hunt, consistency check, executability test — `SEAT-BRIEFS.md`). Launches as
252
+ a concurrent solo run (the red-team variant pattern); same findings contract; `role: "critic"`.
253
+ Judges are never told the seat exists. Two deliberate deviations from the source agent: the
254
+ minimum-10-findings quota is dropped (quotas force invented findings, which waste the bench's
255
+ adjudication capital — the anti-padding rule replaces it), and the hard questions +
256
+ Ship/Fix/Rethink verdict moved to the chair (§12.4). Known cost: the critic model can
257
+ self-identify its review in the Stage-2 bundle by shape, weakening self-bias wash-out for that
258
+ one seat — disclosed in the report. A single-seat brief variant is judged acceptable to record
259
+ to the ledger (precedent: the red-team variant records normally).
260
+
261
+ ### 12.2 Expert lenses
262
+ Each seat gets a distinct expert-perspective brief (panel domain scoped with the user:
263
+ business/technical/specialty/customer/financial/custom). All legs launch as concurrent solos
264
+ (no shared-prompt wave); lens↔model assignment is random and private; `role: "lens:<slug>"`.
265
+ Two standing caveats, both disclosed: (a) **anonymity leak** — every judge can spot its own
266
+ lens-flavored review, so self-bias wash-out is weakened bench-wide; (b) **ledger pollution** —
267
+ lens reviews are not comparable to standard reviews (street-cred and confirm-rate assume a
268
+ shared task), so lens-run tallies always pass `--no-ledger`. The bench-wide task change is what
269
+ separates lenses (no ledger) from the single-seat critic (ledger ok).
270
+
271
+ ### 12.3 Debate mode (Stage 2.5 rebuttal round)
272
+ The debate agent's challenge/stress-test rounds, grounded in findings instead of persona
273
+ prose: the Stage-2 tally runs `--no-ledger` (provisional); every Contested/Disputed finding
274
+ goes back to its raiser (defense mini-wave — solo per raiser: DEFEND / AMEND / WITHDRAW), the
275
+ disputing judges re-vote on defended/amended findings (one shared fanout), and the final tally
276
+ runs with the ledger. Exactly one round — the source agent's open-ended rounds become a fixed
277
+ convergence step, or cost is unbounded. Withdrawn findings stay in `findings[]` and are
278
+ auto-recorded `denied` (never presented for user decision). Missing/unparseable rebuttal or
279
+ re-vote lines mean the original claim/verdict stands. No new decision vocabulary and no engine
280
+ changes: `buildVerdict` and the renderer see a normal tally record.
281
+
282
+ ### 12.4 Chair verdict scale
283
+ A chair-packet addendum: the chair closes with 3–5 hard questions plus a final parseable
284
+ `VERDICT: Ship it | Fix these first | Fundamental rethink` line, surfaced verbatim at the top
285
+ of `report.md` and in chat. Gives the council the single overall call its tier counts never
286
+ provided. Possible later engine touch: an `overallVerdict` field in `verdict.json` + a
287
+ renderer line (not needed for the prose version).
288
+
289
+ ### 12.5 Future work — parallel panels
290
+ The debate agent's parallel-panels + cross-panel synthesis maps to two full council runs over
291
+ different benches or lens-sets plus a super-chair synthesis of where the panels agree,
292
+ disagree, and what each missed. Deferred: cost doubles and the run-folder conventions need
293
+ namespacing. Revisit if lens runs prove valuable.
@@ -4,8 +4,8 @@ This file is the `second-opinion` skill's evolving memory of **how to actually d
4
4
  well**. Read it before Stage 0 (council selection and launch); update it, with the user's
5
5
  approval, at the end of each run (Stage 6). Keep it tight — merge and prune rather than append.
6
6
 
7
- _Last updated: 2026-07-02 (runs 4-7 folded back from the field ledger: PowerShell quoting, date
8
- injection, long-read failures, judge tool-wander; see changelog)._
7
+ _Last updated: 2026-07-14 (v2.2.0 fold-back: v3.1 optional council elements verified live;
8
+ claim-class dedup adjudication limit; minimax and qwen-coder debut notes; see changelog)._
9
9
 
10
10
  ## Global operating rules (all models)
11
11
  - **Council runs are headless by design** (autonomous batch work): `fanout` is headless by
@@ -54,6 +54,19 @@ injection, long-read failures, judge tool-wander; see changelog)._
54
54
  no-tools preamble is now mandatory in SKILL.md Stage 2/3 — keep it verbatim.
55
55
  - The tally input needs **all five keys** (`meta` incl. `meta.models`, `findings`, `adjudications`,
56
56
  `rankings`, `runStats`) — see the SKILL.md Stage-2 recipe step 0.
57
+ - **Claim-class dedup glosses rationale errors.** When several reviews raise the same claim, judges
58
+ adjudicate the class ("same as A3 — agree") and skip verifying each finding's own rationale text:
59
+ a direction-inverted arithmetic detail inside an otherwise-correct finding drew unanimous agrees
60
+ despite an explicit "dispute material factual errors in the rationale" instruction. If
61
+ rationale-level precision matters, instruct judges to independently verify the numbers in each
62
+ finding; otherwise expect class-level adjudication and let the chair reconcile details (a chair
63
+ has caught and corrected such an error unprompted).
64
+ - **Optional council elements (v2.2.0) verified live:** critic seat (solo-alongside-fanout;
65
+ `role: "critic"` passes through `council tally` untouched), debate mode's nothing-to-debate path
66
+ (provisional `--no-ledger` tally → skip rebuttals → final ledger-recorded tally), and the chair
67
+ verdict scale (parseable `VERDICT:` line + hard questions) all behaved per SEAT-BRIEFS. **Debate
68
+ mode's defense/re-vote waves remain unexercised** — an all-Confirmed consensus run has no
69
+ rebuttal surface; exercise them on a contentious artifact before trusting that path blind.
57
70
 
58
71
  ## Per-model notes
59
72
 
@@ -102,6 +115,21 @@ injection, long-read failures, judge tool-wander; see changelog)._
102
115
  ### Claude (in-council, when toggle on)
103
116
  - Consistently the most *calibrated* reviewer (no severity inflation; findings overwhelmingly Confirmed; bench-best street-cred in recent runs) but sometimes the least *original* — it can miss the boldest single catch. Treat as a reliability floor, not a discovery engine.
104
117
 
118
+ ### minimax (`--model minimax` → via OpenRouter)
119
+ - Fast (~2 min review legs), cheap, `clean` findings-JSON conformance on debut.
120
+ - Took the **critic seat** brief exceptionally well: unanimously ranked #1 by its bench, full
121
+ coverage on a ground-truth test, zero padded findings under the anti-padding rule. A strong
122
+ default critic-seat candidate.
123
+ - **Quirks:** occasional CJK character intrusions mid-English prose (cosmetic; the findings JSON
124
+ is unaffected); can invert the direction of an arithmetic detail inside an otherwise-correct
125
+ rationale — see the claim-class-dedup tip in Stage-2 briefing tips.
126
+
127
+ ### qwen-coder (`--model qwen-coder` → via OpenRouter; distinct from `qwen`)
128
+ - Very fast (16–22 s legs, review and judging alike), `clean` conformance, competent
129
+ core-blocker coverage on prose/PRD artifacts.
130
+ - Ranked last on its debut bench — misses the offline, interruption-handling, and
131
+ test-methodology gap classes stronger seats catch. Fine budget-bench filler; do not chair it.
132
+
105
133
  ### (others — add as used)
106
134
  - Opus / o-series etc. are reachable via amicus **if their API keys are configured**. Add notes
107
135
  here the first time each is used.
@@ -160,3 +188,8 @@ This section keeps only per-model **qualitative quirks** and **structural-confor
160
188
  GUI-hangs-on-this-machine rule (resolved 2026-06-10; headless stays the council default by
161
189
  design). Config path updated to `~/.config/amicus/.env`.
162
190
  - **2026-07-02** — Folded back field lessons from runs 4-7 (AV-receiver, pork-shoulder, resume, novel ×2 councils): PowerShell `--models` quoting; current-date injection; long-read model selection; judge no-tools preamble; severity-inflation-justifies-dispute; five-keys tally schema; new Grok/Kimi/Mistral/Claude-in-council sections. Quantitative history stays in the ledger (`amicus council stats`).
191
+ - **2026-07-14 (v2.2.0)** — Optional council elements shipped and verified on a planted-flaw
192
+ ground-truth council (critic seat, debate mode nothing-to-debate path, chair verdict scale;
193
+ expert lenses defined but not yet field-run). New lessons: claim-class dedup glosses
194
+ rationale-level errors in Stage-2; minimax debut (strong critic seat, CJK-intrusion quirk);
195
+ qwen-coder debut (fast budget filler). Debate mode's defense/re-vote waves still unexercised.
@@ -0,0 +1,190 @@
1
+ # Seat Briefs — Optional Council Elements
2
+
3
+ Briefing boilerplate for the **optional council elements** offered at Stage 0 of the
4
+ `second-opinion` skill (SKILL.md → "Optional council elements"). When an element is toggled
5
+ ON, Claude copies the relevant block below into the run's `_tmp-*` briefing files and fills
6
+ the `<placeholders>`. Everything here is orchestration prose — the findings contract,
7
+ `council validate`, tally, and verdict mechanics are unchanged unless a section says
8
+ otherwise.
9
+
10
+ _The critic and lens methodologies are adapted from the `/critic` (elite-advisor) and
11
+ `/debate` (expert-debate-facilitator) agents in John Renaldi's product-kit plugin (MIT)._
12
+ _Deliberate deviations from those sources: no minimum-findings quota (quotas force invented
13
+ findings, which waste the bench's adjudication capital), and the hard questions +
14
+ Ship/Fix/Rethink verdict belong to the chair (synthesis-level judgments stay with the
15
+ non-Claude chair, per the council's core rule)._
16
+
17
+ ---
18
+
19
+ ## Standard anti-sycophancy clause (ALL Stage-1 briefings — not an optional element)
20
+
21
+ Include this block **verbatim in every Stage-1 review briefing**, standard seats included.
22
+ This is briefing hygiene, not a toggle:
23
+
24
+ > Do not soften findings to be agreeable. Lead with your most severe finding. No praise
25
+ > cushions before criticism, and never perform enthusiasm you don't hold — if the artifact
26
+ > is mediocre, say so and show why. Do not pad: report every real finding and no invented
27
+ > ones. An empty severity category is a valid result.
28
+
29
+ ---
30
+
31
+ ## Critic seat brief
32
+
33
+ Body of `_tmp-briefing-critic.md`. Follow it with the standard structured-output contract
34
+ (prose review + trailing findings JSON, identical to every other seat) and the material.
35
+ One bench member gets this brief **instead of** the standard review brief; the rest of the
36
+ bench runs the normal fanout wave.
37
+
38
+ > You are this review bench's designated critic. Assume problems exist; your job is to find
39
+ > them, not to confirm the artifact is fine. Work through four passes and fold everything
40
+ > into one findings list:
41
+ >
42
+ > 1. **Adversarial pass** — for every claim: what evidence supports it, or is it an
43
+ > assumption presented as fact? For every goal: is it measurable — would the author know
44
+ > if they hit it or missed it? For every decision: what alternatives were considered, or
45
+ > was this just the first idea? For every scope boundary: real constraint, or avoidance
46
+ > of hard work?
47
+ > 2. **Edge-case hunt** — walk every journey, requirement, and scenario. What happens on the
48
+ > unexpected input, the failed integration, the malformed data? At zero, at one, at
49
+ > scale? With the confused, frustrated, or adversarial user instead of the happy-path
50
+ > one? Report only unhandled cases — if the artifact addresses an edge case, move on
51
+ > silently.
52
+ > 3. **Consistency check** — cross-reference sections against each other: do goals have
53
+ > metrics, and metrics targets? do requirements trace back to stated needs? do the
54
+ > milestones fit the declared scope? does anything contradict a stated non-goal or
55
+ > constraint?
56
+ > 4. **Executability test** — could someone act on this artifact without coming back with
57
+ > clarifying questions? Wherever the answer is no, name the specific section and exactly
58
+ > what is missing.
59
+ >
60
+ > Be specific: name the section, the line, the exact gap — not "the requirements need work"
61
+ > but which requirement, and what about it is untestable. Report every real finding and no
62
+ > invented ones; do not pad to look thorough. An empty pass is a valid result.
63
+
64
+ **Orchestration notes (not part of the brief):**
65
+ - Launch as a separate concurrent solo run alongside the fanout wave — the exact red-team
66
+ variant pattern in SKILL.md Stage 1.
67
+ - Same findings contract, same `council validate` + repair loop, same anonymization into the
68
+ Stage-2 bundle. Judges are never told a critic seat exists.
69
+ - Record `role: "critic"` on the seat's `runStats` entry.
70
+ - Disclose in `report.md`: the critic model can recognize its own review in the Stage-2
71
+ bundle by its adversarial shape, so self-bias wash-out is weakened for that one seat.
72
+
73
+ ---
74
+
75
+ ## Expert lens briefs
76
+
77
+ **Panel scoping first (Stage 0, when the element is toggled ON).** Ask the user which
78
+ domain the panel should come from, then propose one distinct lens per seat:
79
+
80
+ - **Business / Venture** — VCs, operators, market strategists (viability, positioning, GTM)
81
+ - **Technical** — architects, security specialists, infrastructure engineers (feasibility,
82
+ scalability, build-vs-buy)
83
+ - **Specialty technical** — user names the field (ML, biotech, regulatory, hardware, …)
84
+ - **Customer / Market** — user researchers, skeptical buyers, channel partners (demand,
85
+ willingness to pay, adoption barriers)
86
+ - **Financial** — CFOs, pricing strategists, unit-economics experts
87
+ - **Mixed / Custom** — the user defines the lens set
88
+
89
+ Pick lenses that will produce productive tension (a growth-stage VC + a bootstrapped
90
+ operator + a skeptical enterprise buyer beats three near-identical strategists). Confirm the
91
+ lens set with the user before launch.
92
+
93
+ **Per-seat brief opener** (`_tmp-briefing-lens-<slug>.md`, one per seat; follow with the
94
+ standard structured-output contract and the material):
95
+
96
+ > Review this artifact strictly through the lens of a <lens — e.g. "growth-stage VC",
97
+ > "security architect", "skeptical enterprise buyer", "CFO focused on unit economics">.
98
+ > Raise only findings that perspective is qualified to raise, at the depth a top
99
+ > practitioner of it would reach. Stay in-domain: if something matters but is outside your
100
+ > lens, leave it to the other reviewers.
101
+
102
+ **Orchestration notes (not part of the brief):**
103
+ - Every seat gets a distinct brief, so there is no shared-prompt wave — launch ALL legs as
104
+ concurrent solo runs.
105
+ - The lens↔model assignment is random and lives only in the private label map. Never tell
106
+ any reviewer which lenses the other seats hold, and never mention lenses in the Stage-2
107
+ bundle or judging instructions — judges rank on accuracy and insight only.
108
+ - Record `role: "lens:<slug>"` on each `runStats` entry.
109
+ - The Stage-2 tally runs `--no-ledger` — lens reviews are not comparable to standard
110
+ reviews, so they must not feed cross-run reliability stats.
111
+ - Disclose in `report.md`: anonymity is weakened (each judge can spot its own lens-flavored
112
+ review), street-cred is not comparable across lenses, and the run was not recorded to the
113
+ reliability ledger.
114
+
115
+ ---
116
+
117
+ ## Rebuttal-round templates (debate mode, Stage 2.5)
118
+
119
+ ### Defense brief
120
+
121
+ `_tmp-rebuttal-<label>.md` — one concurrent solo run per raiser that has at least one
122
+ Contested or Disputed finding. First line is the no-tools preamble, verbatim:
123
+
124
+ > Do NOT use any tools or read any files; everything is in this message; begin immediately
125
+ > with the first finding id.
126
+ >
127
+ > You reviewed an artifact and raised the findings below. Peer reviewers (anonymous)
128
+ > disputed them for the stated reasons. For EACH finding, respond with exactly one line
129
+ > starting with the finding id and one verb:
130
+ >
131
+ > `<id>: DEFEND — <your strongest evidence-based defense, one paragraph maximum>`
132
+ > `<id>: AMEND — <the corrected claim, full replacement text>`
133
+ > `<id>: WITHDRAW`
134
+ >
135
+ > Withdraw anything you cannot defend with evidence — an unsupported repeat of the original
136
+ > claim is weaker than a withdrawal.
137
+ >
138
+ > [one block per finding: id · claim · severity · the peers' dispute reasons, anonymized]
139
+
140
+ ### Re-vote bundle
141
+
142
+ `_tmp-revote-bundle.md` — ONE shared bundle, sent as a single fanout wave to every judge
143
+ that disputed at least one defended or amended finding. First line is the no-tools preamble,
144
+ verbatim:
145
+
146
+ > Do NOT use any tools or read any files; everything is in this message; begin immediately
147
+ > with the first finding id.
148
+ >
149
+ > You previously adjudicated the findings below and disputed them. The (anonymous) raiser
150
+ > has now responded. Re-adjudicate ONLY these findings in light of the response. For each,
151
+ > respond with exactly one line:
152
+ >
153
+ > `<id>: agree | dispute | neutral — <one-line reason>`
154
+ >
155
+ > Changing your verdict when the defense is convincing is good judging, not weakness; so is
156
+ > holding your dispute when it isn't.
157
+ >
158
+ > [one block per finding: id · claim (amended claims marked AMENDED) · the raiser's
159
+ > defense]
160
+
161
+ **Orchestration notes (not part of the briefs):**
162
+ - Exactly ONE rebuttal round, ever. Whatever remains unsettled after the re-vote keeps its
163
+ final tier.
164
+ - A missing or unparseable defense line = the original claim stands undefended. A missing
165
+ re-vote line = that judge's original verdict stands.
166
+ - WITHDRAWN findings stay in the tally input (they were raised) and are auto-recorded as
167
+ `denied` in Stage 4 — never presented for a user decision. List them in `report.md` under
168
+ "Withdrawn by raiser (debate mode)".
169
+
170
+ ---
171
+
172
+ ## Chair verdict-scale addendum
173
+
174
+ Append to the chair packet (`_tmp-chair-packet.md`) when the element is toggled ON:
175
+
176
+ > After your synthesis, add two closing sections:
177
+ >
178
+ > 1. **HARD QUESTIONS** — three to five questions the artifact's author has probably not
179
+ > asked themselves, chosen so that an unanswerable question reveals a structural gap in
180
+ > the artifact (not gotchas — questions whose answers should exist).
181
+ > 2. A final line, alone on the last line, in exactly this format:
182
+ >
183
+ > `VERDICT: Ship it` | `VERDICT: Fix these first` | `VERDICT: Fundamental rethink`
184
+ >
185
+ > Pick one. "Ship it" = solid, nothing blocking. "Fix these first" = specific gaps must
186
+ > be resolved before the artifact is useful — name them. "Fundamental rethink" =
187
+ > structural problems that cannot be patched — say what is wrong at the foundation.
188
+
189
+ **Orchestration note:** surface the chair's `VERDICT:` line verbatim at the top of
190
+ `report.md` and in the inline chat presentation of the results.
@@ -50,7 +50,7 @@ Operating lessons from each run fold back into `MODEL-NOTES.md` (with approval),
50
50
 
51
51
  ## The council flow
52
52
 
53
- The flow runs as a **Stage 0 intake/prep step** followed by **three sequential review waves** (Stage 1 → Stage 2 → Stage 3), each wave dependent on the prior. Within each wave, models run in parallel. Track the stages as todos and do not advance until the prior wave's outputs are in hand.
53
+ The flow runs as a **Stage 0 intake/prep step** followed by **three sequential review waves** (Stage 1 → Stage 2 → Stage 3), each wave dependent on the prior — plus an optional **Stage 2.5 rebuttal round** when debate mode is on. Within each wave, models run in parallel. Track the stages as todos and do not advance until the prior wave's outputs are in hand.
54
54
 
55
55
  ### Stage 0 — Intake & prep
56
56
 
@@ -73,7 +73,7 @@ in this run is written here. Use its absolute path in all `--prompt-file` argume
73
73
  know "today" and have raised false "future-dated" blockers; state the date explicitly, e.g.
74
74
  "Today's date is YYYY-MM-DD."
75
75
 
76
- **Pick the council.** Default: **3 models from different families (non-Claude)**. Recommend them ranked by fit, consulting both the reviewer-reliability data from `amicus council stats` (the authoritative quantitative source — runs, avg peers-only street-cred, confirm-rate, fact-error rate) and the qualitative quirks in `MODEL-NOTES.md`. State the estimated cost. The estimate is the budget gate's pre-flight figure (per-$/Mtok pricing from the cached catalog; direct-provider legs without catalog pricing are disclosed as "cost unknown"). State it as an estimate, not a guarantee. **Disclose the run shape up front** before asking for confirmation — e.g.:
76
+ **Pick the council.** Default: **3 models from different families (non-Claude)**. Recommend them ranked by fit, consulting both the reviewer-reliability data from `amicus council stats` (the authoritative quantitative source — runs, avg peers-only street-cred, confirm-rate, fact-error rate) and the qualitative quirks in `MODEL-NOTES.md`. State the estimated cost. The estimate is the budget gate's pre-flight figure (per-$/Mtok pricing from the cached catalog; direct-provider legs without catalog pricing are disclosed as "cost unknown"). State it as an estimate, not a guarantee. **Disclose the run shape up front** before asking for confirmation — naming any enabled optional council elements and their cost impact (see the optional-elements menu below) — e.g.:
77
77
 
78
78
  **Free council (zero-cost).** If the user asks for a "free council" / "zero-cost council",
79
79
  read `councils.free` from `~/.config/amicus/config.json` and run
@@ -93,6 +93,10 @@ read `councils.free` from `~/.config/amicus/config.json` and run
93
93
 
94
94
  > This run uses 3 council models across 2 fanout waves + 1 chair call (~7 model runs), ~10 min.
95
95
 
96
+ or, with elements enabled:
97
+
98
+ > This run uses 3 council models across 2 fanout waves + 1 chair call, with **critic seat + debate mode ON** (~7 base runs + up to 6 rebuttal calls), ~15 min.
99
+
96
100
  Then **wait for confirmation**. Never launch without it. The budget gate enforces the cost guardrail in code: by default it refuses any leg whose price exceeds the per-$/Mtok threshold (the o3/o3-pro guard). To run an intentionally expensive model the user explicitly asked for by name, pass `--no-cost-gate`; to raise only the total ceiling, pass `--max-cost <$>`.
97
101
 
98
102
  **Scale-down is explicit — state which mode applies:**
@@ -102,11 +106,27 @@ Then **wait for confirmation**. Never launch without it. The budget gate enforce
102
106
 
103
107
  The scale-down levels count **non-Claude judges**; enabling "Claude in the council" adds a judged review but not a judge, so it does not change these levels. If the bench drops below 2 surviving reviews during the run, apply the Stage-1 wave-degrade rule (offer re-run or a disclosed single-pass downgrade).
104
108
 
105
- **Present the "Claude in the council" toggle (default: off).** At launch, offer:
109
+ **Present the optional council elements (all default OFF — explicit opt-in only).** After the
110
+ bench and chair are picked and before asking for launch confirmation, present this menu once
111
+ (adjust the run-shape numbers to the actual bench):
112
+
113
+ > Optional council elements — all OFF unless you name them. Reply with any you want (e.g. "1 and 3", "critic + debate mode", or "none"):
114
+ >
115
+ > 1. **Critic seat** — one reviewer swaps to an adversarial brief (adversarial pass, edge-case hunt, consistency check, executability test). Same review count; that seat runs as a solo call. Trade-off: that reviewer can recognize its own review during cross-review (disclosed in the report).
116
+ > 2. **Expert lenses** — each reviewer gets a distinct expert perspective; you pick the panel domain (business, technical, customer, financial, custom). Same review count, launched as solo calls. Trade-offs: weakens cross-review anonymity (disclosed) and the run is not recorded to the reliability ledger.
117
+ > 3. **Debate mode** — after cross-review, Contested and Disputed findings go back to their raisers to defend, amend, or withdraw, and the disputing judges re-vote before the final tally. Adds 1–2 short waves (up to ~2N extra calls, ~+5 min).
118
+ > 4. **Chair verdict scale** — the chair ends its synthesis with `VERDICT: Ship it | Fix these first | Fundamental rethink` plus 3–5 hard questions. No extra calls.
119
+ > 5. **Claude in the council** — I add my own fresh review to the bundle so the bench can rank and adjudicate it; I'm judged but do not vote (Stage 2) or chair (Stage 3), so the verdict stays independent. +1 review in the bundle, no extra council calls.
106
120
 
107
- > Claude-in-the-council (default off): I can add my own fresh review to the bundle so the bench can rank and adjudicate it. You'd see how my take compares to the other reviewers. This means Claude is judged but does not vote (Stage 2) or chair (Stage 3) — the verdict stays independent. Turn on?
121
+ Rules for this menu:
108
122
 
109
- When off, Claude does not contribute a review and does not appear in the bundle. When on, see Stage 1.
123
+ - **Never enable an element the user did not explicitly name.** Silence, "no", or "none" = all off. Do not infer opt-in from the nature of the material ("this doc could use a critic…") — offer, don't decide.
124
+ - If elements were **pre-requested in the invoking command** (e.g. `/council … with a critic seat and debate mode`), confirm them back by name ("Critic seat and debate mode are ON per your request; the others are off") instead of re-asking.
125
+ - **The launch confirmation must enumerate the enabled elements by name** — an element not named in the confirmation is off. Restate its cost/shape impact there (see the run-shape disclosure above).
126
+ - Elements compose freely, with one exception: **critic seat + expert lenses together** means one bench member takes the critic brief and the *remaining* seats take lenses — never stack both briefs on one seat.
127
+ - Briefing boilerplate for elements 1–4 lives in **`SEAT-BRIEFS.md`** next to this file — read it when any of them is toggled on.
128
+
129
+ When Claude-in-the-council is off, Claude does not contribute a review and does not appear in the bundle. When on, see Stage 1 and §5.4.
110
130
 
111
131
  ---
112
132
 
@@ -140,6 +160,10 @@ amicus start --model <redteam-model> --no-ui --json \
140
160
 
141
161
  Its stdout is a single run document; the `summary` field is the review.
142
162
 
163
+ **Critic seat (optional element, when ON):** one bench member — recommended by Claude at Stage 0, typically a strong reasoner that is not the chair — receives the critic brief from `SEAT-BRIEFS.md § Critic seat brief` **instead of** the standard review brief. Launch it exactly like the red-team variant: a separate concurrent solo run (`_tmp-briefing-critic.md`) alongside the fanout wave of the remaining members — same total review count. Everything downstream is unchanged: same structured-output contract, same `council validate` + repair loop, same anonymization into the Stage-2 bundle (judges are never told a critic seat exists). Record `role: "critic"` on that seat's `runStats` entry. One standing disclosure for `report.md`: the critic model can recognize its own review in the Stage-2 bundle by its adversarial shape, so self-bias wash-out is weakened for that one seat.
164
+
165
+ **Expert lenses (optional element, when ON):** every seat gets a distinct per-seat brief, so there is no shared-prompt wave — launch **all** legs as concurrent solo runs (`_tmp-briefing-lens-<slug>.md`, one per seat), using the lens templates and panel-scoping rules in `SEAT-BRIEFS.md § Expert lens briefs`. The lens↔model assignment is random and lives only in the private label map — no reviewer learns the other seats' lenses. Same structured-output contract and validation. Record `role: "lens:<slug>"` on each `runStats` entry. Two standing consequences, both disclosed in `report.md`: Stage-2 anonymity is weakened (each judge can spot its own lens-flavored review), and the Stage-2 tally runs `--no-ledger` (lens reviews are not comparable to standard reviews and must not feed cross-run reliability stats). Wave-degrade rules apply to these solos exactly as to fanout legs.
166
+
143
167
  **Cowork / no-Bash environments:** use the MCP tools instead — `amicus_fanout` (briefing via
144
168
  file) returns `{waveId, taskIds[]}` immediately. Preferred: call `amicus_wait` with the waveId —
145
169
  one blocking call per wave; re-call it while it returns `timedOut: true`. Fallback: poll
@@ -166,6 +190,8 @@ self-contained (`--no-context`), so MCP transport is equivalent.
166
190
 
167
191
  Instruct models to emit the structured JSON verbatim after the prose, without preamble, so it parses cleanly.
168
192
 
193
+ **Every Stage-1 briefing — standard seats included — must contain the standard anti-sycophancy clause from `SEAT-BRIEFS.md` verbatim** (do not soften, lead with the most severe finding, no praise cushions, no padding — an empty severity category is a valid result). This is briefing hygiene, not an optional element.
194
+
169
195
  Save each leg's full output (prose + findings block) to the run folder as `review-<model>.md`
170
196
  (one file per reviewer) before moving on.
171
197
 
@@ -232,6 +258,8 @@ FINAL RANKING:
232
258
 
233
259
  **Task B — Adjudicate findings.** For every finding in the bundle, state: `agree | dispute | neutral` plus one-line reason. Reference each finding as **review-label + finding-id** — for example, `A2` means Review A's 2nd finding, `B1` means Review B's 1st finding. An "I missed this — it's valid" counts as `agree`.
234
260
 
261
+ **When critic seat or expert lenses are ON:** the bundle and judging instructions must not mention seats, lenses, or briefs — judges rank and adjudicate on accuracy and insight only. The element briefs are Stage-1 information; leaking them into Stage 2 tells every judge which review is which.
262
+
235
263
  As each judge's ranking + adjudication response returns, collect it (the raw per-judge responses are working intermediates, not separate run-folder artifacts). Once all are in, **assemble the de-anonymized tally input** and then call `amicus council tally`:
236
264
 
237
265
  **Stage-2 → tally assembly recipe (Claude's work before calling `tally`):**
@@ -251,12 +279,34 @@ Then call, saving the printed `record` to `<run-folder>/tally.json` (Stage 5's `
251
279
  amicus council tally <run-folder>/tally-input.json --json > <run-folder>/tally.json
252
280
  ```
253
281
 
282
+ **Ledger flags for optional elements:** when **expert lenses** are ON, always pass `--no-ledger` — lens runs never feed cross-run reliability stats. When **debate mode** is ON, this Stage-2 tally is *provisional* — pass `--no-ledger` here; the final, ledger-recorded tally happens at the end of Stage 2.5 (the critic seat and Claude-in-the-council change nothing about ledger handling).
283
+
254
284
  **Windows PowerShell 5.1 caveat:** that `>` redirect writes UTF-16 under legacy Windows PowerShell 5.1 (fine on pwsh 7+ or bash), which corrupts `tally.json` for Stage 5's `amicus council verdict` and surfaces as a confusing `BAD_ARGS` there instead of here — on 5.1 pipe through `| Out-File -Encoding utf8` (or run under pwsh 7+) instead of a bare `>`.
255
285
 
256
286
  The output `record` carries the deterministic tiers (Disputed / Confirmed / Contested / Singleton), `confidence` (`solid` | `thin`), both street-cred numbers (`withSelf` and `peersOnly`), the validated `runStats`, and `tierCounts`. **Claude may override a `thin`-confidence tier at the margins** before Stage 4 — record the override in `tierOverride: {from, to, reason}`; the matrix and `verdict.json` surface it. De-anonymize and write the tally results to `crossreview-matrix.md` — the adjudication grid plus the street-cred table. This data feeds Stage 3 (chair briefing) and is never re-anonymized or forwarded to any council model.
257
287
 
258
288
  ---
259
289
 
290
+ ### Stage 2.5 — Rebuttal round ("debate mode", optional element — skip this entire stage when OFF)
291
+
292
+ One structured challenge round on the findings the bench did not settle: every **Contested** and **Disputed** finding goes back to its raiser to defend, amend, or withdraw; the judges that disputed it re-vote; then the final tally. **Exactly ONE round, ever** — never iterate further; whatever remains unsettled after the re-vote keeps its final tier. Briefing templates are in `SEAT-BRIEFS.md § Rebuttal-round templates`; every rebuttal briefing opens with the no-tools preamble, same as Stage 2.
293
+
294
+ The Stage-2 tally above ran `--no-ledger` (provisional). If it produced **zero Contested + Disputed findings**, skip the rebuttal waves entirely: re-run the tally on the unchanged input *without* `--no-ledger` to record it, note "debate mode: nothing to debate" for `report.md`, and proceed to Stage 3.
295
+
296
+ **1. Defense mini-wave.** For each raiser with ≥ 1 Contested/Disputed finding, write `_tmp-rebuttal-<label>.md`: its findings (run-global ids and claims), each with the peers' dispute reasons — anonymized, no judge identities. Launch one concurrent solo run per raiser (same flags and budget-gate handling as the Stage-1 solos). Parse each response line: `<id>: DEFEND — …` | `<id>: AMEND — <replacement claim>` | `<id>: WITHDRAW`. A missing or unparseable line = the original claim stands undefended (original verdicts carry).
297
+
298
+ **2. Re-vote mini-wave.** Build ONE shared `_tmp-revote-bundle.md` holding every defended or amended finding plus its (anonymous) defense. Send it as a single fanout wave to the judges that disputed at least one of those findings — judges that never disputed sit this round out. Parse verdict lines `<id>: agree | dispute | neutral — <reason>`; a judge's missing line = its original verdict stands.
299
+
300
+ **3. Final tally.** Re-assemble the tally input: re-vote verdicts replace those judges' original adjudications on those findings; AMENDED claims replace the originals (`id`, `raiser`, and `severity` unchanged); WITHDRAWN findings **stay in `findings[]`** (they were raised) and take whatever tier the final cascade assigns. Run `amicus council tally` **without** `--no-ledger` (unless expert lenses are also ON — lens runs never ledger) and save this record as the run's `tally.json`. This final record — not the provisional one — is what Stages 3–5 consume; the chair packet and `crossreview-matrix.md` are built from it, with verdict changes from the re-vote called out before/after.
301
+
302
+ **Withdrawals downstream:** WITHDRAWN findings are auto-recorded in `decisions.json` as `{"id": …, "decision": "denied"}` — never presented for a user decision in Stage 4 — and listed in `report.md` under "Withdrawn by raiser (debate mode)".
303
+
304
+ **Degrade rules:** a dead defense leg → all of that raiser's contested findings stand undefended. A dead re-vote leg → that judge's original verdicts carry. Never re-run the round.
305
+
306
+ **Cost/shape:** adds up to 2 short waves (≤ N defense solos + 1 re-vote fanout), disclosed at Stage 0. If the actual rebuttal surface turns out much larger than estimated (many contested findings), say so before launching the mini-waves.
307
+
308
+ ---
309
+
260
310
  ### Stage 3 — Council-chair synthesis
261
311
 
262
312
  A designated **non-Claude** chair synthesizes the verdict across all reviews, rankings, and adjudications. The chair produces an independent verdict that Claude then presents — Claude does not paraphrase, edit, or re-synthesize it.
@@ -291,6 +341,8 @@ Instruct the chair to write a **synthesized verdict** that:
291
341
  - Distinguishes findings the bench broadly endorsed from contested or singleton claims
292
342
  - Arrives at an overall assessment of the artifact
293
343
 
344
+ **Chair verdict scale (optional element, when ON):** append the addendum from `SEAT-BRIEFS.md § Chair verdict-scale addendum` to the chair packet — the chair must close with 3–5 **hard questions** the artifact's author probably hasn't asked themselves, then a final parseable line: `VERDICT: Ship it | Fix these first | Fundamental rethink`. Surface that line verbatim at the top of `report.md` and in the inline chat presentation. When debate mode is also ON, the chair packet is built from the *final* (post-rebuttal) tally and includes the defense/re-vote outcomes.
345
+
294
346
  Save the chair's output to the run folder as `verdict.md`.
295
347
 
296
348
  ---
@@ -301,6 +353,8 @@ All findings from the bundle are sorted into tiers based on the **peer-confidenc
301
353
 
302
354
  **Scale-down:** In a 1-model run, Stage 2 was skipped — there is no peer-confidence data, so present every finding individually for decision (no tiers). In a 2-model run, the Confirmed tier rests on thin cross-review (one ranker per review, per Stage 0) — say so when presenting it.
303
355
 
356
+ **Debate mode:** the tiers below come from the *final* (post-rebuttal) tally. Findings WITHDRAWN by their raiser in Stage 2.5 are already recorded as `denied` — do not present them for decision; note them as withdrawn when walking the tiers.
357
+
304
358
  **Consensus tier — Confirmed findings** (≥ 2 peer agreements, agrees dominate)
305
359
 
306
360
  - Present the full list in one block: id, claim, severity, and which models raised / endorsed it.
@@ -341,7 +395,12 @@ Do not advance to Stage 5 until every finding in both tiers has a recorded decis
341
395
  - `verdict.md` (already saved in Stage 3)
342
396
  - `verdict.json` — write by running `amicus council verdict <run-folder>/tally.json --decisions <run-folder>/decisions.json -o <run-folder>/verdict.json` (a thin CLI wrapper over `buildVerdict(record, decisions)` + `writeVerdictAtomic`, `src/council/verdict.js`). `<run-folder>/tally.json` is the `record` saved from the Stage-2 `amicus council tally` call. `<run-folder>/decisions.json` is a **JSON array**, one object per finding: `{id, decision, applied?, duplicateOf?, tierOverride?}` — `id` is the run-global label id (e.g. `A1`); `decision` is the Stage-4 outcome (accepted / denied / modified / deferred); `applied` (optional bool) marks whether the accepted change was actually applied to the artifact in Stage 5; `duplicateOf` (optional) links to another finding's id when Claude identified a duplicate; `tierOverride` (optional) carries any `{from, to, reason}` override recorded in Stage 2. Save this array to `<run-folder>/decisions.json` first, then run the command — it parses the tally record and the decisions file, calls `buildVerdict`, and writes the schema-stamped machine-readable record to the run folder via the same atomic tmp+rename convention the function always used.
343
397
  - `report.md` — the chair's synthesis + the full Stage-4 decision log + a summary of what was
344
- applied (+ the "How Claude's review fared" readout when "Claude in the council" is on) + a
398
+ applied (+ the "How Claude's review fared" readout when "Claude in the council" is on) + an
399
+ **Optional elements** section whenever any element was ON: which elements ran; the chair's
400
+ `VERDICT:` line verbatim at the top of the report (chair verdict scale); the "Withdrawn by
401
+ raiser (debate mode)" list and re-vote verdict changes (debate mode); and the standing
402
+ disclosures — critic self-identification in cross-review (critic seat), weakened anonymity +
403
+ non-comparable street-cred + `--no-ledger` (expert lenses) + a
345
404
  **run-stats table**: one row per model call — **stage** (which stage you launched the call for)
346
405
  plus **model, status, durationMs, and cost** read from the wave/run JSON `usage`
347
406
  block. Cost is `usage.cost.amount` (USD); mark it with its `usage.cost.source`
@@ -377,7 +436,7 @@ The `MODEL-NOTES.md` **next to this file** is your machine-local run ledger: npm
377
436
 
378
437
  Draft new or updated entries for the per-model sections of `MODEL-NOTES.md` that capture what was learned.
379
438
 
380
- **Ledger auto-append (automatic — no approval required).** Running `amicus council tally` (the Stage-2 finalize call) appends one row per (run × model) to the append-only `council-ledger.jsonl` under `getConfigDir()` — no separate step is needed. Pass `--no-ledger` to compute a tally record *without* recording it (e.g. a re-tally that shouldn't double-count). The run summary shows the appended row. This is a deterministic, content-free model-level record (no finding text, no claim strings, no artifact body content). The quantitative reviewer-reliability data in `MODEL-NOTES.md` is now sourced entirely from `amicus council stats` (which aggregates the ledger) — **do not hand-edit reliability numbers in MODEL-NOTES**.
439
+ **Ledger auto-append (automatic — no approval required).** Running `amicus council tally` (the finalize call — Stage 2, or the Stage-2.5 final tally when debate mode is on; skipped entirely on expert-lens runs, which always pass `--no-ledger`) appends one row per (run × model) to the append-only `council-ledger.jsonl` under `getConfigDir()` — no separate step is needed. Pass `--no-ledger` to compute a tally record *without* recording it (e.g. a re-tally that shouldn't double-count). The run summary shows the appended row. This is a deterministic, content-free model-level record (no finding text, no claim strings, no artifact body content). The quantitative reviewer-reliability data in `MODEL-NOTES.md` is now sourced entirely from `amicus council stats` (which aggregates the ledger) — **do not hand-edit reliability numbers in MODEL-NOTES**.
381
440
 
382
441
  **Compose the proposed MODEL-NOTES diff.** Combine the run-lessons updates and the reviewer-reliability table updates into a single proposed diff (old → new for every changed section). **Write the full diff to a file in the run folder** — `_tmp-proposed-model-notes-update.md` — so the user can open and review it before deciding. Presenting the diff as chat text alone is **not sufficient**: an approval dialog can hide the chat transcript, so the user may be asked to decide on a diff they never saw.
383
442
 
@@ -476,13 +535,15 @@ Always **rank recommendations by fit**, state the trade-off for each option, and
476
535
  - `report.md` — Claude-authored; full contract defined once in *Stage 5 → Run-folder artifacts* above (chair's synthesis + Stage-4 decision log + run-stats table).
477
536
  - `report.html` — a **separate, deterministic** artifact generated by the `amicus council report <verdict.json> --html` renderer directly from `verdict.json` (no chair prose, no decision-log narrative — see Stage 5's *Renderer* note); the default artifact to share.
478
537
  - Reviewed copy: `<stem>-reviewed.<ext>`, next to the source.
479
- - Temp working files (`_tmp-*.md`: extracts, stage briefings, red-team brief, bundle, chair packet, proposed
480
- MODEL-NOTES diff) live in the run folder and are cleaned up at the end of the run — the proposed-diff file
481
- only after the Stage-6 approval decision is resolved.
538
+ - Temp working files (`_tmp-*.md`: extracts, stage briefings, red-team brief, critic/lens seat briefs
539
+ (`_tmp-briefing-critic.md`, `_tmp-briefing-lens-<slug>.md`), rebuttal briefs (`_tmp-rebuttal-<label>.md`,
540
+ `_tmp-revote-bundle.md`), bundle, chair packet, proposed MODEL-NOTES diff) live in the run folder and are
541
+ cleaned up at the end of the run — the proposed-diff file only after the Stage-6 approval decision is resolved.
482
542
 
483
543
  ---
484
544
 
485
545
  ## Files
486
546
 
487
547
  - `MODEL-NOTES.md` — operating rules, per-model qualitative quirks, cost guardrail, and structural-conformance notes. **Read it before Stage 0 (council selection and launch); update qualitative notes (with approval) in Stage 6.** Quantitative reliability data (runs, avg street-cred, confirm-rate, fact-error rate) comes from `amicus council stats`, not this file. This copy is machine-local (never overwritten on update); the shipped seed lives in the amicus repo and absorbs durable lessons at release time.
488
- - `COUNCIL-DESIGN.md` — the design spec this skill implements (v3 + WS-3). Consult it if a mechanics question arises that the skill prose does not resolve.
548
+ - `SEAT-BRIEFS.md` — briefing boilerplate for the optional council elements (critic seat, expert lenses, rebuttal round, chair verdict scale) plus the standard anti-sycophancy clause every Stage-1 briefing carries. **Read it whenever any element is toggled on at Stage 0** (the anti-sycophancy clause applies to every run).
549
+ - `COUNCIL-DESIGN.md` — the design spec this skill implements (v3 + WS-3; §12 covers the optional council elements). Consult it if a mechanics question arises that the skill prose does not resolve.
@@ -146,7 +146,7 @@ function cacheRootFor(env = process.env) {
146
146
  * @returns {Promise<void>}
147
147
  */
148
148
  async function controlledProvision({
149
- electronDir, platform, arch, version, downloadArtifact, extract, fs, env = process.env,
149
+ electronDir, platform, arch, version, downloadArtifact, extract, fs, env = process.env, downloadMs = 480000,
150
150
  }) {
151
151
  const zip = await downloadArtifact({
152
152
  version,
@@ -155,11 +155,7 @@ async function controlledProvision({
155
155
  cacheRoot: cacheRootFor(env),
156
156
  platform,
157
157
  arch,
158
- checksums: undefined,
159
- // Bound the fetch so a stalled/blocked network aborts (got v11 timeouts:
160
- // socket = inactivity, request = total) instead of hanging the repair —
161
- // a hung-then-killed download is what orphaned the single-flight lock.
162
- downloadOptions: { timeout: { socket: 60000, request: 480000 } },
158
+ downloadOptions: { signal: AbortSignal.timeout(downloadMs) }, // 5.x native fetch: bound stalled downloads, free the lock
163
159
  });
164
160
  await extractFromCache({ zip, electronDir, platform, extract, fs });
165
161
  }
@@ -214,7 +210,11 @@ async function repairElectron({
214
210
  const spawn = deps.spawn || ((cmd, args, o) => spawnSync(cmd, args, { ...o, timeout: timeoutMs || 480000 }));
215
211
  const findZip = deps.cachedZip || ((o) => cachedZip(o));
216
212
  const acquireLock = deps.acquireLock || ((o) => acquireRepairLock({ ...o, fs }));
217
- const downloadArtifact = deps.downloadArtifact || require('@electron/get').downloadArtifact;
213
+ // Lazy: import the ESM-only @electron/get only on the network path, so cacheOnly
214
+ // repairs and injected mocks stay parseable under Jest (which can't import() ESM).
215
+ const resolveDownloadArtifact = deps.downloadArtifact
216
+ ? async () => deps.downloadArtifact
217
+ : async () => (await import('@electron/get')).downloadArtifact;
218
218
 
219
219
  if (!version) {
220
220
  try {
@@ -268,8 +268,9 @@ async function repairElectron({
268
268
  // download that produced no usable exe is a FAILURE (no false success; #53).
269
269
  let controlledExtracted = false;
270
270
  try {
271
+ const downloadArtifact = await resolveDownloadArtifact();
271
272
  await controlledProvision({
272
- electronDir, platform, arch, version, downloadArtifact, extract, fs, env: process.env,
273
+ electronDir, platform, arch, version, downloadArtifact, extract, fs, env: process.env, downloadMs: timeoutMs,
273
274
  });
274
275
  controlledExtracted = true; // download + extract returned without throwing
275
276
  } catch {
@@ -0,0 +1,16 @@
1
+ 'use strict';
2
+ const MIN_NODE = '22.12.0';
3
+
4
+ /** @param {string} current @param {string} min @returns {{ok:boolean,message:string|null}} */
5
+ function checkNodeVersion(current, min = MIN_NODE) {
6
+ const c = current.replace(/^v/, '').split('.').map(Number);
7
+ const m = min.split('.').map(Number);
8
+ for (let i = 0; i < 3; i++) {
9
+ if ((c[i] || 0) > (m[i] || 0)) { return { ok: true, message: null }; }
10
+ if ((c[i] || 0) < (m[i] || 0)) {
11
+ return { ok: false, message: `Amicus 3.0 requires Node >=${min}; you are on ${current.replace(/^v/, '')}. Upgrade Node and retry.` };
12
+ }
13
+ }
14
+ return { ok: true, message: null };
15
+ }
16
+ module.exports = { checkNodeVersion, MIN_NODE };