amicus 4.9.7 → 4.9.8

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 (44) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/CHANGELOG.md +94 -0
  3. package/README.md +1 -1
  4. package/docs/ROADMAP.md +3 -3
  5. package/docs/architecture-map.md +19 -4
  6. package/docs/council.md +140 -3
  7. package/docs/usage.md +8 -4
  8. package/package.json +1 -1
  9. package/schemas/council-verdict.schema.json +3 -1
  10. package/skills/second-opinion/SEAT-BRIEFS.md +6 -0
  11. package/src/cli-council-run-tools.js +168 -0
  12. package/src/cli-handlers-council-run.js +6 -6
  13. package/src/cli.js +23 -1
  14. package/src/council/briefings-chair.js +1 -1
  15. package/src/council/briefings-task.js +11 -5
  16. package/src/council/briefings.js +25 -7
  17. package/src/council/report-lost-rows.js +89 -0
  18. package/src/council/report-md.js +3 -1
  19. package/src/council/report.js +3 -2
  20. package/src/council/run-degrade.js +22 -1
  21. package/src/council/run-finish.js +23 -1
  22. package/src/council/run-launch.js +33 -4
  23. package/src/council/run-retry-launch.js +9 -4
  24. package/src/council/run-retry.js +3 -0
  25. package/src/council/run-seat-tools-verify.js +296 -0
  26. package/src/council/run-seat-tools.js +274 -0
  27. package/src/council/run-server.js +41 -6
  28. package/src/council/run-stage1-launch.js +8 -3
  29. package/src/council/run.js +21 -21
  30. package/src/council/seat-tools.js +299 -0
  31. package/src/council/verdict-seats-reviewed.js +76 -6
  32. package/src/headless.js +136 -6
  33. package/src/mcp-council-pack-map.js +24 -0
  34. package/src/mcp-council-run.js +17 -15
  35. package/src/mcp-server.js +2 -2
  36. package/src/mcp-tools.js +15 -4
  37. package/src/opencode-client.js +26 -0
  38. package/src/pack/pack-validate.js +3 -1
  39. package/src/prompt-builder.js +2 -2
  40. package/src/sidecar/fanout.js +7 -1
  41. package/src/sidecar/heartbeat.js +46 -0
  42. package/src/sidecar/session-utils.js +7 -34
  43. package/src/utils/agent-mapping.js +1 -1
  44. package/src/utils/degrade.js +8 -0
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "amicus",
3
- "version": "4.9.7",
3
+ "version": "4.9.8",
4
4
  "description": "Multi-model LLM Council + parallel AI window for Claude Code. Run structured council reviews across Gemini, GPT, DeepSeek and more — or fork a conversation to any model and fold the results back.",
5
5
  "author": {
6
6
  "name": "Christian Wagner"
package/CHANGELOG.md CHANGED
@@ -3,6 +3,100 @@
3
3
  All notable changes to Amicus are documented here. Format follows
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions follow semver.
5
5
 
6
+ ## [4.9.8] - 2026-09-13
7
+
8
+ ### Changed
9
+
10
+ - **Council legs run as two per-run agents with an explicit tool allowlist.** Stage-1 seats and
11
+ their retries run as `council-seat` — task mode defaults to `webfetch`, review mode to no tools,
12
+ and `--tools <a,b,c>` (MCP: `tools`) opts more in, validated against the engine's own declared
13
+ ids before any leg launches; repair, judge, debate and chair legs run as `council-support` with
14
+ no tools at all. `task` and `skill` are refused (they spawn or escape the session), as are
15
+ `edit`/`write`/`apply_patch`/`question`/`invalid`; `--agent Plan|Build` is the escape hatch
16
+ (`Plan` = the pre-4.9.8 behaviour: edits denied, reads/searches/shell allowed). A local tool
17
+ needs `--out-dir` outside the project tree and scopes the seats to the tree with
18
+ `external_directory: deny`. With `read` opted in the seat is denied `.env`, `.env.*` and
19
+ `.envrc` files at the engine (the match is case-sensitive on Linux; the deny rules are
20
+ measured to render after the seat's own read allow; the refusal itself is exercised by the
21
+ release ritual's live run); `grep` and `bash` have no per-file fence. `bash` is outside
22
+ every fence (run directory, home, network) and the CLI says so when it is opted in; the CLI
23
+ also prints a Notice for `grep`/`glob` (no per-file fence) and for `--agent Plan` (every leg
24
+ can run commands); `tools: null` is treated as unset like `agent: null`; an `--agent` run's
25
+ directory stays inside the project; `--tools` and `--agent` cannot be combined; `todowrite`
26
+ counts as a tool that never touches the tree. After registration, the run also reads back
27
+ what the engine actually rendered for each council agent — over the run directory, its
28
+ `_scratch` support-leg directory, and the project tree when local — and refuses before
29
+ launch if an `opencode.json`/`.opencode/agent` file the engine loads for that directory (the
30
+ tree's, or your global config) widened it, or if the engine cannot be asked at all
31
+ once verification can run — including when the run's own shared OpenCode server fails to
32
+ start: a default (non-`--agent`) run now refuses before any launch instead of falling back,
33
+ and the per-wave fallback servers that used to absorb that failure for every run now serve
34
+ only an `--agent` run, whose verification is skipped by design; a reviewed tree's own config
35
+ was measured to merge into the registered agent by key order (ruling P2-R33; the
36
+ unconditional refusal on an unverifiable engine, a failed shared-server start included, is
37
+ P2-R38/P2-R43, `_scratch` coverage is P2-R39, and the external_directory exemption for the
38
+ engine's own tool-output cache — resolved XDG-first, matching `auth-json.js`/`engine-log.js`
39
+ — is narrowed by P2-R40/P2-R42). The tripwire also verifies the council agents' non-permission
40
+ surface (prompt, model, sampling, options, mode) as rendered, since a tree can set those too
41
+ (P2-R53); a council agent is registered by replacing any same-name entry; the local-tool
42
+ placement rule compares physical paths, so a run directory reached through a symlink into the
43
+ project is refused (P2-R54); the CLI prints a Notice for `read` naming its exact fence;
44
+ `tools: null` over MCP is unset (P2-R55).
45
+ Two of the three leg-loss classes in the 2026-09-11 study shared one
46
+ precondition — a seat reached for a tool it did not need (gemini `grep`/`glob` over the global
47
+ install, cohere `task {}`) — and this closes that door. Previously every leg ran as the engine's
48
+ `Plan` agent, which denies file edits but leaves the rest of the tool set available (the
49
+ study saw seats reach for `grep`, `glob` and `task` under it). (`docs/council.md` § Tool
50
+ access; spec §4; PR 2 of 3.)
51
+
52
+ ### Fixed
53
+
54
+ - **Headless legs are no longer declared complete while the engine is still answering.**
55
+ The stable-idle heuristic (`headless.js`, the v4.4 B4 "measured defect site") now
56
+ defers to `session.status`: while the engine reports `busy` (or `retry`, provider
57
+ backoff) and no tool call is live, flat output is treated as activity rather than
58
+ silence, and the leg waits for its message to finalize. In-flight reasoning and text
59
+ are invisible to the poller, so a seat that narrated between tool calls and then
60
+ answered for longer than 60 s was harvested mid-answer — three complete council
61
+ deliverables were discarded 39–107 s before they finished in the 2026-09-11
62
+ study (run D0), and the run reported `complete` with no degrades. The B4 bounded
63
+ tool-settle ceiling is unchanged and still governs whenever a tool call is live. When
64
+ `session.status` is unavailable the heuristic runs as the fallback it was always meant
65
+ to be. The trade is explicit: a busy-but-wedged (or permanently retrying) leg that
66
+ already produced output now runs to its `--timeout` (15 min default) and is named
67
+ `timeout`, instead of being declared complete at 60 s on a stub; the debug trace
68
+ names the veto once per flat stretch (and again whenever a non-zero count is reset,
69
+ e.g. a status flip-flop) and the exit line records the last engine status. When the
70
+ fallback heuristic ends an unfinalized message the leg logs a warning naming the
71
+ status it saw, and a retry whose next attempt lies beyond the leg deadline ends the
72
+ leg at once as `RETRY_BEYOND_DEADLINE` instead of holding the seat (never once the
73
+ last message has finalized — that leg completes normally — and never while a tool
74
+ call is live, where the tool-settle ceiling governs).
75
+ (`docs/superpowers/specs/2026-09-11-council-leg-completion-design.md` §3; PR 1 of 3.)
76
+ - **A seat whose findings nothing could verify is now visible in the report and in the verdict
77
+ census.** When a Stage-1 response carried no parseable findings block and the bounded repair
78
+ supplied one, the seat's `runStats` row has carried `findingsUnverified: true` since the repair
79
+ contract landed (LC-11) — through `tally.json` and `verdict.json` — and died at the renderer:
80
+ `report.html`'s **What was lost** table held only the degrade sink's records (dead legs and the
81
+ like), and `seatsReviewed` counted the seat as a full reviewer. Now the report (Markdown and
82
+ HTML) gains one **What was lost** row per such seat (channel `unverified-repair`: *seat X's
83
+ findings came from a repair of a response with no findings block — nothing verified them*) and
84
+ one per refused repair (channel `repair-refused`, naming the code), derived from `runStats` when
85
+ the report is built — so `amicus council report` on an older `verdict.json` shows them too, and
86
+ nothing in `run.json`, the exit code or the degrade sink changes. `verdict.json`'s census gains
87
+ two numbers, `seatsReviewed: {reviewed, unverified, refused, of}` (always written, never more
88
+ than `reviewed`; older documents still validate) — `refused` (council #248 round 2) counts
89
+ seats whose repair was refused: they tallied no findings and would otherwise read as full
90
+ reviewers on every gate surface, and the council-review check title and sticky comment print
91
+ `(N unverified)` and `(N refused)` when non-zero. A run also prints one `Notice:` line on
92
+ stderr at the end naming its unverified and refused seats, so a local run is no longer silent
93
+ about them; the exit code and every artifact are unchanged. The wording never says "stub":
94
+ the flag also fires on a real review whose trailing JSON block was malformed. Study run D0
95
+ (three narration stubs, three repairs) now reads `{reviewed: 3, unverified: 3, refused: 0,
96
+ of: 3}` in `verdict.json` — `seats reviewed: 3 of 3 (3 unverified)` in the sticky comment
97
+ — with three rows in the report, where it read `{reviewed: 3, of: 3}` and nothing. (#242;
98
+ `docs/superpowers/specs/2026-09-11-council-leg-completion-design.md` §5; PR 3 of 3.)
99
+
6
100
  ## [4.9.7] - 2026-09-09
7
101
 
8
102
  Three findings deferred from the v4.9.6 cut, and the two open questions filed beside them. The
package/README.md CHANGED
@@ -455,7 +455,7 @@ $ amicus status demo123 --json
455
455
  "taskId": "demo123",
456
456
  "status": "complete",
457
457
  "elapsed": "5m 0s",
458
- "version": "4.9.7",
458
+ "version": "4.9.8",
459
459
  "model": "google/gemini-2.5-flash",
460
460
  "phase": "terminal"
461
461
  }
package/docs/ROADMAP.md CHANGED
@@ -13,13 +13,13 @@ lives under **Backlog (tracked, not scheduled)** with everything else that is re
13
13
  Nothing about the content changed and no judgment about its value is implied; only its status. When
14
14
  an org buyer and the org to support them exist, it earns a number then.
15
15
 
16
- Amicus is at **v4.9.7** (2026-09-09). Each 4.x rev below leads with the benefit, not the
16
+ Amicus is at **v4.9.8** (2026-09-13). Each 4.x rev below leads with the benefit, not the
17
17
  plumbing; the patch releases since v4.9.0 carry no section of their own, because each corrected a
18
18
  defect rather than adding scope — where one added a surface (v4.9.4's `--thinking` refusals and
19
- `output-budget` doctor row, v4.9.5's Electron digest gate, v4.9.6's artifact custody, v4.9.7's dual name-table rescue boundary) it did so to
19
+ `output-budget` doctor row, v4.9.5's Electron digest gate, v4.9.6's artifact custody, v4.9.7's dual name-table rescue boundary, v4.9.8's per-run seat tool allowlist and its unverified/refused seat census) it did so to
20
20
  make an existing promise true, not to widen it. See `CHANGELOG.md` for what each one contained.
21
21
 
22
- **Status:** v4.0 through **v4.9.0** have **shipped**, plus the v4.9.1–v4.9.7 patch releases —
22
+ **Status:** v4.0 through **v4.9.0** have **shipped**, plus the v4.9.1–v4.9.8 patch releases —
23
23
  everything on this page is a record of what landed, not a plan. Composition — the scope that
24
24
  carried the number v4.6 here until the degrade-announcement-invariant milestone took the v4.6.0
25
25
  release (2026-08-02) — is now an unscheduled candidate for the next rev, tabled in its own section
@@ -38,6 +38,7 @@ src/
38
38
  │ ├── presets-cli.js
39
39
  │ ├── report-cost.js
40
40
  │ ├── report-html.js
41
+ │ ├── report-lost-rows.js
41
42
  │ ├── report-md.js
42
43
  │ ├── report.js
43
44
  │ ├── run-assemble.js
@@ -56,6 +57,8 @@ src/
56
57
  │ ├── run-retry-notes.js
57
58
  │ ├── run-retry-window.js # The Stage-1 retry's no-output window: how long a RELAUNCHED leg may stay
58
59
  │ ├── run-retry.js
60
+ │ ├── run-seat-tools-verify.js # The engine-rendering tripwire's pure pieces, plus `verificationDirectories`'
61
+ │ ├── run-seat-tools.js
59
62
  │ ├── run-server.js
60
63
  │ ├── run-stage1-launch.js # Stage-1 launch pass for the council engine.
61
64
  │ ├── run-stage1-rows.js
@@ -66,6 +69,7 @@ src/
66
69
  │ ├── run-stats-entry.js
67
70
  │ ├── run-verdict-files.js
68
71
  │ ├── run.js
72
+ │ ├── seat-tools.js
69
73
  │ ├── seats.js
70
74
  │ ├── stage1-bind.js
71
75
  │ ├── street-cred.js
@@ -135,6 +139,7 @@ src/
135
139
  │ ├── fanout-validate.js
136
140
  │ ├── fanout-wave-io.js
137
141
  │ ├── fanout.js
142
+ │ ├── heartbeat.js
138
143
  │ ├── interactive-abort.js
139
144
  │ ├── interactive-mirror.js
140
145
  │ ├── interactive-process.js # Sidecar Interactive Process Helpers - Electron probe/env/process-exit plumbing
@@ -308,6 +313,7 @@ src/
308
313
  │ └── seat-space.js # Council Workspace — the seat-space PREDICATES (v4.8 PR5b).
309
314
  ├── cli-council-run-bench.js # Bench and input resolution for the council run command.
310
315
  ├── cli-council-run-render.js
316
+ ├── cli-council-run-tools.js # `--tools`/`--agent` validation and the v4.7 out-dir fence for `council run`.
311
317
  ├── cli-handlers-abort.js # CLI Abort Handler (B21-rest extraction)
312
318
  ├── cli-handlers-council-run.js
313
319
  ├── cli-handlers-council.js
@@ -336,6 +342,7 @@ src/
336
342
  ├── jsonl-parser.js # JSONL Parser
337
343
  ├── mcp-council-awareness.js
338
344
  ├── mcp-council-bench.js
345
+ ├── mcp-council-pack-map.js # COUNCIL_PACK_PARAM_MAP, split out of mcp-council-run.js for the 300-line size gate (P2-R16).
339
346
  ├── mcp-council-run.js
340
347
  ├── mcp-notify.js # Pure helpers + in-process registry for the MCP `onComplete: 'mcp-notify'`
341
348
  ├── mcp-server.js # @module mcp-server — Amicus MCP Server (stdio transport)
@@ -419,6 +426,7 @@ scripts/
419
426
  ├── integration-test.sh
420
427
  ├── mark-test-passed.js # Writes the current git HEAD SHA to .test-passed for the pre-push SHA cache
421
428
  ├── postinstall.js # Post-install script for amicus
429
+ ├── probe-council-agents.js # Spec 2026-09-11 §4 — the ENGINE side of the council agents (ruling P2-R21).
422
430
  ├── probe-darwin-extract.js
423
431
  ├── probe-max-tokens.js # Wire probe for issue #218: what max_tokens / reasoning / thinking does the
424
432
  ├── run-integration-keyless.js
@@ -452,6 +460,7 @@ evals/
452
460
  |--------|---------|-------------|
453
461
  | `cli-council-run-bench.js` | Bench and input resolution for the council run command. | `resolveBench()`, `resolveChair()`, `resolveCritic()`, `CHAIR_DEFAULT()`, `parseList()` |
454
462
  | `cli-council-run-render.js` | | `renderRunHuman()` |
463
+ | `cli-council-run-tools.js` | `--tools`/`--agent` validation and the v4.7 out-dir fence for `council run`. | `checkCouncilRunTools()` |
455
464
  | `cli-handlers-abort.js` | CLI Abort Handler (B21-rest extraction) | `handleAbort()` |
456
465
  | `cli-handlers-council-run.js` | | `handleCouncilRun()`, `renderRunHuman()`, `CHAIR_DEFAULT()` |
457
466
  | `cli-handlers-council.js` | | `handleCouncil()` |
@@ -480,6 +489,7 @@ evals/
480
489
  | `jsonl-parser.js` | JSONL Parser | `parseJSONLLine()`, `readJSONL()`, `extractTimestamp()`, `formatMessage()`, `formatContext()` |
481
490
  | `mcp-council-awareness.js` | | `subWaveIds()`, `countWaveLegs()`, `elapsedOf()`, `enginePid()`, `buildCouncilStatusPayload()` |
482
491
  | `mcp-council-bench.js` | | `resolveBenchInput()`, `auditBenchAliases()` |
492
+ | `mcp-council-pack-map.js` | COUNCIL_PACK_PARAM_MAP, split out of mcp-council-run.js for the 300-line size gate (P2-R16). | `COUNCIL_PACK_PARAM_MAP()` |
483
493
  | `mcp-council-run.js` | | `handleCouncilRunTool()`, `COUNCIL_PACK_PARAM_MAP()`, `buildCouncilStatusPayload()`, `listCouncilRuns()`, `abortCouncilRun()` |
484
494
  | `mcp-notify.js` | Pure helpers + in-process registry for the MCP `onComplete: 'mcp-notify'` | `validateOnComplete()`, `buildNotifyPayload()`, `requestMcpNotify()`, `consumeMcpNotify()` |
485
495
  | `mcp-server.js` | @module mcp-server — Amicus MCP Server (stdio transport) | `handlers()`, `startMcpServer()`, `getProjectDir()`, `resolveProjectDir()`, `getClientRoot()` |
@@ -494,7 +504,7 @@ evals/
494
504
  | `spend-query.js` | | `filterRows()`, `groupRows()`, `computeWasted()`, `emptyTokens()`, `addTokens()` |
495
505
  | `council/anonymize.js` | | `assignLabels()`, `toGlobalId()`, `toGlobalFindings()`, `rankingToOrder()`, `LETTERS()` |
496
506
  | `council/briefings-chair-task.js` | | `CHAIR_ANSWER_VALUES()`, `ANSWER_SCALE_ADDENDUM()`, `TASK_CHAIR_SYNTHESIS()`, `TASK_CHAIR_SYNTHESIS_NO_CLAIMS()`, `TASK_CONCURRENCE_CAVEAT()` |
497
- | `council/briefings-chair.js` | | `dateLine()`, `CHAIR_NO_TOOLS_PREAMBLE()`, `chairRepairPromptFor()`, `CHAIR_VERDICT_VALUES()`, `VERDICT_SCALE_ADDENDUM()` |
507
+ | `council/briefings-chair.js` | | `dateLine()`, `CHAIR_NO_TOOLS_LEAD()`, `CHAIR_NO_TOOLS_PREAMBLE()`, `chairRepairPromptFor()`, `CHAIR_VERDICT_VALUES()` |
498
508
  | `council/briefings-debate.js` | | `DEBATE_NO_TOOLS_PREAMBLE()`, `DEFENSE_CONTRACT()`, `REVOTE_CONTRACT()`, `buildDefenseBrief()`, `buildRevoteBundle()` |
499
509
  | `council/briefings-stage2-task.js` | | `TASK_JUDGE_FRAME()`, `TASK_JUDGE_A()`, `TASK_JUDGE_B()`, `TASK_JUDGE_B_NO_CLAIMS()`, `NO_CLAIMS_INDEX()` |
500
510
  | `council/briefings-stage2.js` | | `JUDGE_NO_TOOLS_PREAMBLE()`, `CHAIR_NO_TOOLS_PREAMBLE()`, `CHAIR_VERDICT_VALUES()`, `JUDGE_OUTPUT_CONTRACT()`, `VERDICT_SCALE_ADDENDUM()` |
@@ -511,6 +521,7 @@ evals/
511
521
  | `council/presets-cli.js` | | `runSave()`, `runList()`, `runShow()` |
512
522
  | `council/report-cost.js` | | `buildCostModel()` |
513
523
  | `council/report-html.js` | | `renderHtml()` |
524
+ | `council/report-lost-rows.js` | | `lostRowsOf()` |
514
525
  | `council/report-md.js` | | `renderMd()` |
515
526
  | `council/report.js` | | `buildReport()`, `toModel()`, `TIER_ORDER()`, `SYMBOL()`, `isSeatSpace()` |
516
527
  | `council/run-assemble.js` | | `buildRunStatsEntry()`, `worseConformance()`, `buildTallyInput()`, `writeTallyFiles()`, `writeVerdictFiles()` |
@@ -519,7 +530,7 @@ evals/
519
530
  | `council/run-debate-revote.js` | | `legOpts()`, `legRow()`, `runRevoteWave()` |
520
531
  | `council/run-debate-stage.js` | | `runDebateStage()` |
521
532
  | `council/run-debate.js` | | `runDebate()`, `nothingToDebate()`, `disputingJudges()`, `debateTargets()` |
522
- | `council/run-degrade.js` | | `createDegradeSink()` |
533
+ | `council/run-degrade.js` | | `createDegradeSink()`, `noteDroppedMembers()` |
523
534
  | `council/run-finalize.js` | | `statusForExit()`, `resolveTerminalExit()`, `writeRunTerminal()`, `SIGNAL_EXIT()` |
524
535
  | `council/run-finish.js` | | `finishRun()` |
525
536
  | `council/run-launch.js` | | `createLaunchers()`, `materializeReviews()`, `materializeDebate()`, `sanitizeName()`, `isAbortExit()` |
@@ -529,7 +540,9 @@ evals/
529
540
  | `council/run-retry-notes.js` | | `waveStillDeadNote()`, `skippedWaveNote()`, `srcLegStillDeadNote()`, `retryLegStillDeadNote()`, `missingLegStillDeadNote()` |
530
541
  | `council/run-retry-window.js` | The Stage-1 retry's no-output window: how long a RELAUNCHED leg may stay | `retryBackstopMs()` |
531
542
  | `council/run-retry.js` | | `groupStage1Losses()`, `retryStage1Losses()` |
532
- | `council/run-server.js` | | `acquireRunServer()`, `releaseRunServer()`, `resolveRunServerModels()`, `recordServerFate()` |
543
+ | `council/run-seat-tools-verify.js` | The engine-rendering tripwire's pure pieces, plus `verificationDirectories`' | `verificationDirectories()`, `listEngineAgents()`, `verifyAgentRendering()`, `verifyAgentFields()`, `resolvePhysicalPath()` |
544
+ | `council/run-seat-tools.js` | | `preflightSeatTools()`, `validateSeatToolsAgainstEngine()`, `listEngineAgents()`, `verifyAgentRendering()`, `verifyAgentFields()` |
545
+ | `council/run-server.js` | | `acquireRunServer()`, `releaseRunServer()`, `resolveRunServerModels()`, `recordServerFate()`, `listEngineToolIds()` |
533
546
  | `council/run-stage1-launch.js` | Stage-1 launch pass for the council engine. | `launchStage1()` |
534
547
  | `council/run-stage1-rows.js` | | `pushDeadSeatRows()`, `supersededRows()` |
535
548
  | `council/run-stage1-superseded.js` | | `supersededRows()` |
@@ -539,12 +552,13 @@ evals/
539
552
  | `council/run-stats-entry.js` | | `buildRunStatsEntry()` |
540
553
  | `council/run-verdict-files.js` | | `writeVerdictFiles()` |
541
554
  | `council/run.js` | | `runCouncil()`, `pickFallbackChair()`, `SIGNAL_EXIT()` |
555
+ | `council/seat-tools.js` | | `REFUSED_TOOL_IDS()`, `REMOTE_TOOL_IDS()`, `NON_LOCAL_TOOL_IDS()`, `SEAT_READ_DENY_PATTERNS()`, `defaultToolsFor()` |
542
556
  | `council/seats.js` | | `buildSeats()`, `roleAt()`, `bindSeats()`, `artifactName()`, `displayName()` |
543
557
  | `council/stage1-bind.js` | | `bindStage1Waves()`, `orphanLegNote()`, `missingSeatDeadWave()`, `bindPaddedWave()` |
544
558
  | `council/street-cred.js` | | `computeStreetCred()`, `rankPositions()`, `credSeats()` |
545
559
  | `council/tally.js` | | `assignTier()`, `computeStreetCred()`, `tally()`, `COUNCIL_SCHEMA_VERSION()` |
546
560
  | `council/verdict-seat-loss.js` | | `summarizeSeatLoss()`, `deriveSeatLoss()` |
547
- | `council/verdict-seats-reviewed.js` | #202: the bench-seat census for verdict.json, as a spreadable fragment. | `seatsReviewedOf()` |
561
+ | `council/verdict-seats-reviewed.js` | #202: the bench-seat census for verdict.json, as a spreadable fragment. | `seatsReviewedOf()`, `isBenchRole()`, `isUnverifiedSeat()`, `isRefusedSeat()`, `seatLabel()` |
548
562
  | `council/verdict.js` | | `buildVerdict()`, `summarizeSeatLoss()`, `deriveSeatLoss()`, `readOverallVerdict()`, `readPriorVerdictSurfaces()` |
549
563
  | `design/tokens.js` | | `tokenCss()`, `TOKENS()` |
550
564
  | `observe/council-legs.js` | | `buildLegRows()` |
@@ -592,6 +606,7 @@ evals/
592
606
  | `sidecar/fanout-validate.js` | | `parseModelsList()`, `DEFAULT_MAX_LEGS()`, `validateFanoutModels()` |
593
607
  | `sidecar/fanout-wave-io.js` | | `writeWaveMetadata()`, `writeWaveDoc()`, `finishWave()`, `stampLegAttribution()` |
594
608
  | `sidecar/fanout.js` | | `parseModelsList()`, `deriveLegIds()`, `validateFanoutModels()`, `DEFAULT_MAX_LEGS()`, `runFanout()` |
609
+ | `sidecar/heartbeat.js` | | `HEARTBEAT_INTERVAL()`, `createHeartbeat()` |
595
610
  | `sidecar/interactive-abort.js` | | `startAbortWatch()`, `markResultAborted()`, `readAbortedMarker()`, `DEFAULT_INTERVAL_MS()` |
596
611
  | `sidecar/interactive-mirror.js` | | `startInteractiveMirror()` |
597
612
  | `sidecar/interactive-process.js` | Sidecar Interactive Process Helpers - Electron probe/env/process-exit plumbing | `getElectronPath()`, `checkElectronAvailable()`, `buildElectronEnv()`, `handleElectronProcess()` |
package/docs/council.md CHANGED
@@ -22,6 +22,7 @@ orchestration recipe. This page is the reference for the artifacts that recipe p
22
22
  - [The pipeline, end to end](#the-pipeline-end-to-end)
23
23
  - [`amicus council run`](#amicus-council-run)
24
24
  - [Task mode (`--intent task`)](#task-mode---intent-task)
25
+ - [Tool access per usage (`--tools`)](#tool-access-per-usage---tools)
25
26
  - [Debate mode](#debate-mode)
26
27
  - [Council Workspace (GUI)](#council-workspace-gui)
27
28
  - [Auto-open on `amicus_council_run` (v4.5)](#auto-open-on-amicus_council_run-v45)
@@ -114,6 +115,7 @@ amicus council run --prompt-file <briefing.md>
114
115
  [--template <name|path>] [--artifact <file>] [--var k=v] # v4.5, see docs/usage.md#briefing-templates
115
116
  [--pack <name|path>] # v4.5, see docs/usage.md#policy-packs
116
117
  [--intent review|task] # v4.9, see Task mode below
118
+ [--tools <a,b,c>] [--agent Plan|Build] # v4.9.8, see Tool access below
117
119
  ```
118
120
 
119
121
  **The headless engine (v4.0).** Everything the `second-opinion` skill orchestrates by hand in
@@ -372,6 +374,101 @@ empty-section wordings — is used verbatim in both intents. One vocabulary, two
372
374
  - **Task runs build no reliability history**, so they never contribute to — and never benefit from —
373
375
  `amicus council stats`, including the ledger-driven chair-fallback promotion.
374
376
 
377
+ ### Tool access per usage (`--tools`)
378
+
379
+ Tool access is a property of the **run**, set by the caller according to whether the seats
380
+ must go and get their material — not a property of the mode (spec 2026-09-11 §2). Every
381
+ council leg runs as one of two agents the run's own OpenCode server registers:
382
+
383
+ - **`council-seat`** — stage-1 seats (the bench wave, the critic, the lenses) and their retries.
384
+ Its tools are the intent's default ∪ `--tools`: **task mode** defaults to `webfetch` (the
385
+ co-worker can research); **review mode** defaults to none (the artifact under review arrives
386
+ in the briefing — `--artifact`, `--pack`, or pasted). `--tools read,grep,glob,bash` opts local
387
+ tools in, even for task mode — opt-in is deliberate.
388
+ - **`council-support`** — repair re-prompts, the Stage-2 judges, debate legs and the chair.
389
+ No tools, ever: their briefings already say so, and the agent now enforces it.
390
+ The wildcard deny also covers the engine's own doom-loop and question prompts, so the engine
391
+ never blocks a headless leg on a tool-level prompt; a model that asks its question in prose
392
+ simply ends its turn (what a leg does after a refusal is the §7 live check).
393
+
394
+ `task` and `skill` are refused (`task` spawns child sessions amicus cannot observe; `skill` is
395
+ where a seat starts reading the harness instead of the brief), as are `edit`, `write`,
396
+ `apply_patch` (a seat never modifies the tree), `question` (a headless leg has no human) and
397
+ `invalid`. Every seat tool — the intent's default included — is validated against the engine's
398
+ declared list when the engine lists its tools; an unknown id is `BAD_ARGS` naming what the engine
399
+ declares. `--agent Plan|Build` is the escape hatch: every leg runs on the engine's own agent, no
400
+ council agents, no allowlist; it cannot be combined with `--tools`. Its legs run with the run
401
+ directory (inside the project) as their working directory, so a seat can read the run's own
402
+ records, the label map included — use it only where that is acceptable.
403
+ `Build` is edit-capable: unlike the council agents' fenced allowlist, it can edit files and run
404
+ commands over that directory (the CLI prints a Notice when you opt into it); `Plan` is the
405
+ pre-4.9.8 default: it denies edits but allows reads, searches and the shell (measured on the
406
+ pinned engine), so it is the escape hatch that restores v4.9.7's behaviour exactly, and the CLI
407
+ prints a Notice for it too (every leg — judges and the chair included — can run commands).
408
+
409
+ Refusals land in a run directory that already exists, so the refusal itself is recorded (the same
410
+ order the other pre-spend checks use); nothing is launched and nothing is spent. When the engine
411
+ cannot list its tools at all, a defaults-only run continues on the recorded degrade while an
412
+ explicit opt-in is refused — in practice this only decides a defaults-only run's outcome when the
413
+ shared server is otherwise up and only the tool-ids endpoint itself fails; a server that cannot
414
+ start at all is now caught by the engine-rendered verification below regardless of intent (ruling
415
+ P2-R38).
416
+
417
+ **Engine-rendered verification.** After registration, the run reads back what the engine actually
418
+ rendered for `council-seat`/`council-support` — the run directory, its `_scratch` support-leg
419
+ directory, and the project tree too when a local tool is opted in (ruling P2-R39) — and refuses
420
+ before any launch if an `opencode.json` or `.opencode/agent` file the engine loads for that
421
+ directory (the tree's, or your global config) altered them — naming that config as the cause
422
+ and `--agent` as the knowingly-unprotected alternative (ruling P2-R33). An `external_directory`
423
+ allow after the wildcard deny is exempted only when it is the engine's own tool-output cache
424
+ under its own XDG-first data directory (`$XDG_DATA_HOME/opencode` when set, else
425
+ `~/.local/share/opencode` — ruling P2-R42, matching how `src/utils/auth-json.js`/
426
+ `src/utils/engine-log.js` already resolve it); any other one reads as a widened agent too
427
+ (ruling P2-R40). Unverifiable (no shared server to ask) REFUSES whenever
428
+ verification can run at all — a defaults-only run included, no more quiet degrade (ruling
429
+ P2-R38); it is skipped only when a caller supplies its own transport with no way to ask the
430
+ engine at all (test-only — production always VERIFIES and refuses when it has no server to ask).
431
+ When the run's own shared OpenCode server fails to start, that IS "no server to ask": a default
432
+ (non-`--agent`) run now refuses before any launch rather than falling back — the per-wave
433
+ fallback servers that used to absorb a shared-server failure for every run now serve only an
434
+ `--agent` run, whose verification is skipped by design and so never has to ask at all (ruling
435
+ P2-R43). `--agent Plan` restores v4.9.7's per-wave behaviour exactly.
436
+
437
+ **Run-directory placement with a local tool.** A seat that can read the project tree must
438
+ not be able to read this run's sibling sessions, so with any local tool opted in the run dir
439
+ must sit **outside** the project tree (`--out-dir`), under your home, tmp or
440
+ `AMICUS_PROJECT_ROOTS`; the seats are then scoped to the project tree (`external_directory:
441
+ deny`) while their metadata stays in the run dir. Over MCP the run dir stays inside the
442
+ project, so local tools are refused there with the CLI named; tools that never touch the tree
443
+ (`webfetch`, `websearch`, `todowrite`) over MCP are fine.
444
+
445
+ **What the seat is told.** With no tools it gets the same no-tools sentence as the chair
446
+ (`Do NOT use any tools or read any files; …`), with tools one line naming exactly them, and
447
+ under `--agent` a line saying it runs as the engine's named agent with its own tool set. The
448
+ config enforces; the sentence informs — study run E1 showed gemini makes zero tool calls when
449
+ told not to.
450
+
451
+ **Secrets.** With `read` opted in, the seat agent denies `.env`, `.env.*` and `.envrc` files at
452
+ the engine (the match is case-sensitive on Linux; no other spelling is fenced) — the seat gets a
453
+ refusal and the leg continues (the deny rules are measured to render after the seat's own
454
+ `read=allow`, and CI now models the engine's own evaluator — transcribed from its source — over
455
+ the real rendering, confirming `.env`/`.env.*`/`.envrc` deny and an ordinary file allows; the
456
+ refusal itself is exercised by the release ritual's live `--tools read` run, not by the probe).
457
+ The CLI prints a Notice for `read` too, naming those three fenced names and asking you to keep
458
+ other secrets out.
459
+ `grep`, `glob` and `bash` have no per-file fence: opting them in trusts every seat with
460
+ everything in the tree, `.env` included — grep returns its contents, glob lists its name — and
461
+ the CLI prints a Notice when you opt any of grep, glob or bash in. Keep secrets out of any tree
462
+ you point a `bash`, `grep` or `glob` seat at.
463
+ With a local tool the seat's engine session is rooted at the project tree, so the engine also
464
+ loads that tree's own opencode config; do not point a local-tools seat at a tree you do not trust.
465
+
466
+ `bash` is outside every fence: a bash seat runs commands as you — it can reach the run
467
+ directory outside the tree (this run's own records included: the label map that anonymizes the
468
+ bench and every review already on disk, so bench anonymity and independence do not hold under
469
+ bash), your home directory and the network, and the `webfetch` deny does not bind a shell. Opt
470
+ it in only where that is acceptable; the CLI prints a Notice when you do.
471
+
375
472
  ### Debate mode
376
473
 
377
474
  `--debate` adds a **Stage-2.5 rebuttal round** between cross-review and the final tally
@@ -594,6 +691,19 @@ things make that safe:
594
691
  that is present and empty counts as "I found nothing" — the same line `countAttemptedFindings`
595
692
  draws when it checks a repair against the count the original declared.
596
693
 
694
+ **When the original carried no block at all there is nothing to check the repair against**, so
695
+ the repair is accepted and the seat's `runStats` row carries `findingsUnverified: true` — the
696
+ seat is counted as reviewed, its findings are tallied, and nothing verified them. Since v4.9.8
697
+ (#242) that fact is surfaced rather than merely recorded: `verdict.json`'s `seatsReviewed`
698
+ counts the seat under `unverified`, and the report's **What was lost** gains a row for it
699
+ (*seat X's findings came from a repair of a response with no findings block — nothing verified
700
+ them*). A repair that returned a **different number** of findings than the original declared is
701
+ refused instead — `repairRefused: {code, detail}` on the row, `conformance: unstructured`, no
702
+ findings tallied, the seat's own prose still reaching the judges — and gets a `repair-refused`
703
+ row naming the code. Since council #248 round 2 it is also counted under `seatsReviewed.refused`,
704
+ printed on the check title and sticky comment as `(N refused)`, and named on the end-of-run
705
+ stderr line.
706
+
597
707
  This closes a contradiction that used to be shipped in every run: the anti-sycophancy clause in
598
708
  each Stage-1 briefing says "An empty severity category is a valid result", while the validator
599
709
  rejected exactly that answer — so the only way for a reviewer to satisfy the schema was to
@@ -669,7 +779,7 @@ under "Stage-2 → tally assembly recipe"). It needs **all five top-level keys**
669
779
  | `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. `raiserSeat?` (**v4.8**) — the raising **seat's** id (`deepseek#1`), emit-only-when-it-differs-from-the-alias, so a bench with no repeated alias never carries it. `raiser` stays the alias in every case. |
670
780
  | `adjudications[]` | array | One entry per (judge × finding): `{findingId, judge, verdict}`, `verdict ∈ {agree, dispute, neutral}`. `seat?` (**v4.8**) — the judging **seat's** id, on the same emit-when-different terms as `findings[].raiserSeat`; `judge` stays the alias. 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). ⚠️ **v4.8: that exclusion is seat-conditional.** When a vote *and* its finding both carry a seat id, the engine compares **seats** (`v.seat !== f.raiserSeat`), so on a bench that repeats an alias a twin's genuine vote on its twin's finding is now counted instead of discarded. When either side carries no seat id — a legacy document, a hand-assembled one, or a real run whose leg failed to bind to its seat — it falls back to comparing **aliases**, which is the pre-v4.8 behaviour and still drops that twin's vote. Never fill in a seat id you did not observe just to unlock the seat compare. ⚠️ **`""` is not a model id.** The schema accepts an empty string for `raiser` and for `judge`, but the engine cannot identify a vote it has no name for: when a finding's `raiser` is empty or missing, every vote whose `judge` is also empty or missing is excluded from `basis` and counted in `findings[].unattributedPeerDrops`. A **seat id on both sides overrides this** — it is a stronger identity than either name, so a seated vote is scored (or excluded as the raiser's own) regardless of what `raiser` and `judge` say. Send the real alias, or expect the vote not to be scored. |
671
781
  | `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"]`). `seat?` (**v4.8**) — the judge's own seat id, on the same emit-when-different terms as `adjudications[].seat`; `judge` stays the alias. `orderSeats?` (**v4.8**) — the seat-valued parallel of `order`, slot for slot (a tied slot is a nested array there too): each slot is a seat id where the ranked model's seat is known, `null` where it is not, and the whole key is emitted only when at least one slot is non-null. **Two consumers read it.** Street cred keys on `orderSeats` when present and falls back to the alias otherwise — the mechanism that lets a twin bench's two street-cred rows diverge instead of collapsing into one. **v4.8** — the **chair packet** is the second: its peer-rankings block zips `orderSeats` onto `order` slot for slot, so the chair reads seat ids where the run knows them and the ranked alias where it does not. A tied slot is zipped element by element, and a `null` slot renders the alias rather than the word `null`. |
672
- | `runStats[]` | array | One row per paid launch (v4.7 spec §5 D1/D2 — no longer capped at one row per model; see the role roster below): `{model, role, wasChair, conformance, status, durationMs, usage, waveId?, resolvedModel?, seat?}`. `seat?` (**v4.8**) is the row's seat **id**, emit-only-when-it-differs-from-that-seat's-own-alias — so only a bench that repeats an alias carries it. Only the two producers that *have* a seat pass one: the primary reviewing-seat rows and the dead-seat rows. A `judge`, `chair-attempt`, `repair` or `superseded` row never carries it (all four are excluded from the ledger join, so a seat stamp there could never win it), and neither do the off-bench chair rows or the synthetic `claude` row, which have no seat at all. Two seats of one alias that **both** died usually get **two** rows: each carries its own `seat` id where the run bound that seat's leg, and **no** `seat` where it could not — an unidentified dead seat is counted but never named. They still collapse into a **single** row in two cases, both of them seats the run genuinely cannot tell apart: both legs missing a task id, and a run with no seat table behind it — the deliberate floor, since inventing an identity there would be a guess. A retry wave that came back with **fewer legs than it launched** was a third such case and is **no longer**: v4.8 T-A4 made the retry reconcile count a key's SLOTS rather than test its presence, so both twins get a row (measured end to end through `runStage1`: 1 primary dead-seat row before, 2 after, with the superseded rows unchanged at 2 — one of the two rows carries `usage: null`, and **which one is arbitrary**: neither row names a seat, so the alias's billed total is split across its anonymous rows by row ORDER, never by identity). ⚠️ **Corrected in v4.8** — this cell previously claimed the two *always* collapse into one row carrying no `seat`. That was already wrong for **bound** twins the day it was written (the two-row behaviour landed 2026-08-13, this sentence 2026-08-14), and v4.8 closed the unbound half for every retry outcome, the partial return included (that last one in T-A4). May be `[]`. Any leg with no run document gets `durationMs: null, usage: null` — never invent a value. `waveId` is emit-only-when-set. `resolvedModel?` (v4.7) — the executable id that actually served the row's leg, emit-only-when-set; leg-less rows (the give-up chair row, dead seats with no leg, the claude row) never carry it. `model` stays the council alias. ⚠️ **One row shape carries `usage` with NO `waveId`, `resolvedModel` or `durationMs` (v4.8)**: an unidentified dead seat on an alias the bench repeats, where the run holds a billed retry leg it cannot attribute to either twin. The cost is real and is counted in the run total, but every per-seat execution fact is withheld rather than guessed — so do **not** assume `usage` implies `waveId`, and do not treat a null `durationMs` as "this seat cost nothing". |
782
+ | `runStats[]` | array | One row per paid launch (v4.7 spec §5 D1/D2 — no longer capped at one row per model; see the role roster below): `{model, role, wasChair, conformance, status, durationMs, usage, waveId?, resolvedModel?, seat?}` — plus `findingsUnverified?: true` / `repairRefused?: {code, detail}` (the two halves of the repair contract's outcome, see the repair paragraph under `amicus council validate`) and `ttftMs?` (v4.9 W13). `seat?` (**v4.8**) is the row's seat **id**, emit-only-when-it-differs-from-that-seat's-own-alias — so only a bench that repeats an alias carries it. Only the two producers that *have* a seat pass one: the primary reviewing-seat rows and the dead-seat rows. A `judge`, `chair-attempt`, `repair` or `superseded` row never carries it (all four are excluded from the ledger join, so a seat stamp there could never win it), and neither do the off-bench chair rows or the synthetic `claude` row, which have no seat at all. Two seats of one alias that **both** died usually get **two** rows: each carries its own `seat` id where the run bound that seat's leg, and **no** `seat` where it could not — an unidentified dead seat is counted but never named. They still collapse into a **single** row in two cases, both of them seats the run genuinely cannot tell apart: both legs missing a task id, and a run with no seat table behind it — the deliberate floor, since inventing an identity there would be a guess. A retry wave that came back with **fewer legs than it launched** was a third such case and is **no longer**: v4.8 T-A4 made the retry reconcile count a key's SLOTS rather than test its presence, so both twins get a row (measured end to end through `runStage1`: 1 primary dead-seat row before, 2 after, with the superseded rows unchanged at 2 — one of the two rows carries `usage: null`, and **which one is arbitrary**: neither row names a seat, so the alias's billed total is split across its anonymous rows by row ORDER, never by identity). ⚠️ **Corrected in v4.8** — this cell previously claimed the two *always* collapse into one row carrying no `seat`. That was already wrong for **bound** twins the day it was written (the two-row behaviour landed 2026-08-13, this sentence 2026-08-14), and v4.8 closed the unbound half for every retry outcome, the partial return included (that last one in T-A4). May be `[]`. Any leg with no run document gets `durationMs: null, usage: null` — never invent a value. `waveId` is emit-only-when-set. `resolvedModel?` (v4.7) — the executable id that actually served the row's leg, emit-only-when-set; leg-less rows (the give-up chair row, dead seats with no leg, the claude row) never carry it. `model` stays the council alias. ⚠️ **One row shape carries `usage` with NO `waveId`, `resolvedModel` or `durationMs` (v4.8)**: an unidentified dead seat on an alias the bench repeats, where the run holds a billed retry leg it cannot attribute to either twin. The cost is real and is counted in the run total, but every per-seat execution fact is withheld rather than guessed — so do **not** assume `usage` implies `waveId`, and do not treat a null `durationMs` as "this seat cost nothing". |
673
783
 
674
784
  **`runStats[].role` roster (v4.7 row-per-launch).** Every leg the run budget counts gets exactly
675
785
  one row, so a seat that needed a repair or lost a leg to a retry can now show up more than once.
@@ -852,6 +962,7 @@ named here. That is why the v4.8 keys below each needed their own line.
852
962
  - `seats` — **v4.8**, optional. The tally record's `meta.seats` (same `{id, alias, role, lens, position}` shape), promoted to the top level next to `seatLoss`. Present only when the tally record carried one, i.e. only when the bench repeated an alias. It is what makes the `alias#N` ids on `findings[].raiserSeat`, `adjudications[].seat` and `runStats[].seat` resolvable from the verdict **alone** — before v4.8 the verdict named seats it could not resolve. `council report` reads it to give each seat its own adjudication-matrix column; when it is absent, or is not an array of objects each carrying a string `id`, the **adjudication matrix** falls back to alias space whole and renders exactly as it did before v4.8. ⚠️ **That fallback is the matrix's alone — it is not a whole-document guarantee.** The street-cred table beside it labels each row from `streetCred[].seat` whenever the row carries one, a predicate independent of this key, so a verdict with seated `streetCred[]` rows and no usable `seats` renders seat ids in the street-cred table and aliases in the matrix (measured on an absent `seats`, a non-array `seats`, and an array-of-strings `seats`). In-process both fields come from the same twin bench and travel together; the split is reachable on a hand-assembled or externally-supplied record, which `buildVerdict`'s own docblock names. A verdict written before v4.8 carries no `streetCred[].seat` at all and is unaffected.
853
963
  - `findings[].raiserSeat` — **v4.8**, optional. The raising seat's id, carried through from the tally record; absent unless the bench repeated an alias. `findings[].raiser` stays the alias.
854
964
  - `findings[].sameModelCorroboration` — **v4.8**, optional, `true` only. Carried through from the tally record; see the tally-record notes above for the stamp's meaning **and for the two directions in which it is wrong** (it misses one model behind two aliases, and it fires falsely on one alias behind two executables).
965
+ - `seatsReviewed` — **#202**, optional: `{reviewed, unverified, refused, of}`, derived from `runStats` at build time (never passed in) and emitted only when the record carries at least one bench-role row (`seat`, `critic`, `lens:<slug>`), so `0 of 0` is never written. `of` counts bench seats post-retry (a healed seat once — its first attempt is `role:'superseded'`), `reviewed` those whose leg completed, and `unverified` (**v4.9.8**, #242) those among them whose findings came from a repair of a response with no parseable findings block (`runStats[].findingsUnverified`); the tally scored those findings, nothing verified the repair, and the flag is not a stub detector (a real review whose trailing JSON block was malformed carries it too). `refused` (**v4.9.8**, council #248 round 2) counts those whose repair was refused (`runStats[].repairRefused`): the seat contributed no findings at all and would otherwise read as a full reviewer on every gate surface. Both are always written with the census and never more than `reviewed` — the census and the report share one predicate (a bench role, a completed leg, and the literal `true` or a plain-object `repairRefused`), so a flagged row that is not a completed bench seat is counted nowhere and rendered nowhere. The report's `unverified-repair` and `repair-refused` rows name such seats; the check title and sticky comment print `(N unverified)` and `(N refused)` only when non-zero; at the end of a run whose counts are non-zero the CLI prints one `Notice:` line on stderr naming the seats.
855
966
  - All other keys (`runId`, `council`, `findings`, `streetCred`, `runStats`, `tierCounts`) are passed through unchanged from the tally record.
856
967
 
857
968
  **Write path:** always atomic — a `<out>.tmp-<pid>` file is written first, then renamed over the
@@ -897,8 +1008,12 @@ tier-count summary table — headed **Verdict summary** on a review run and **An
897
1008
  task run, since a task run produces an answer, not a verdict, and on a task run followed by the
898
1009
  one-line concurrence qualifier (*tiers report peer concurrence, never verification*) — a
899
1010
  **What was lost** section
900
- when the run degraded (plus a **Notes** list for informational records, e.g. a task run's
901
- ledger-skipped announcement), the **adjudication matrix** (finding × judge, `✓`/`✗`/`–` with
1011
+ when the run degraded — and, since v4.9.8 (#242), one row per seat whose findings came from a
1012
+ repair of a response with no findings block (channel `unverified-repair`) and one per refused
1013
+ repair (channel `repair-refused`, naming the code), derived from `runStats[]` when the report is
1014
+ built, so re-rendering an older `verdict.json` shows them too — plus a **Notes** list for
1015
+ informational records (e.g. a task run's ledger-skipped announcement), the **adjudication
1016
+ matrix** (finding × judge, `✓`/`✗`/`–` with
902
1017
  `*` marking the raiser's own vote), the **peers-only street-cred table**, **findings grouped by
903
1018
  tier** (Disputed first), and a **cost table** (per-model status/duration/cost + wave total,
904
1019
  sourced from `runStats[].usage`).
@@ -1263,6 +1378,28 @@ need Claude or a live council run to regenerate it.
1263
1378
 
1264
1379
  ---
1265
1380
 
1381
+ ## Leg completion and `session.status`
1382
+
1383
+ A headless leg ends on the first of: the engine reporting `idle`; its last message
1384
+ finalizing (two stable polls); the no-output backstop; the tool-stall detector; the
1385
+ tool-settle ceiling (when a tool call never settles); a retry the engine schedules
1386
+ past the leg deadline (`RETRY_BEYOND_DEADLINE`); or the leg `--timeout`. The
1387
+ flat-output heuristic that used to end a leg after 30 stable polls now runs only
1388
+ when `session.status` reports anything other than a working engine (busy or retry)
1389
+ — unavailable, unrecognised — **or** when a tool call is live (where the bounded
1390
+ ceiling governs). A finalized message always ends on the stable-finished path, whatever
1391
+ the status. A busy (or retrying) engine with no live tool is a model still answering
1392
+ — the poller cannot see its text until the message finalizes — so the leg waits. A
1393
+ busy-but-wedged session therefore ends by `--timeout`, and is named that. At debug
1394
+ level the trace names the veto once per flat stretch (and again whenever a non-zero
1395
+ count is reset) and the exit line records the last engine status. When the fallback
1396
+ heuristic ends an unfinalized message, the leg logs a warning (warn level) naming the
1397
+ status it saw. A retry whose next attempt lies beyond the leg deadline ends the leg
1398
+ at once as `RETRY_BEYOND_DEADLINE` — unless the last message has already finalized (the
1399
+ leg completes normally) or a tool call is live (the bounded tool-settle ceiling governs).
1400
+
1401
+ ---
1402
+
1266
1403
  ## See also
1267
1404
 
1268
1405
  - **[skills/second-opinion/SKILL.md](../skills/second-opinion/SKILL.md)** — the orchestration
package/docs/usage.md CHANGED
@@ -98,7 +98,7 @@ amicus start --model deepseek --prompt "Generate tests" --no-ui --timeout 30
98
98
  | `--var <k=v>` | Set `{{var.<key>}}`; repeatable. Requires `--template`. | |
99
99
  | `--tag <t>` | Label this session for `list`/`--search`/`spend --group-by tag` (1-64 chars, `[A-Za-z0-9_-]`; invalid values are rejected, never silently cleaned). | *(none)* |
100
100
 
101
- > Agents: **Chat** auto-approves reads and asks before writes/bash (interactive default); **Build** has full tool access (headless default); **Plan** is read-only analysis. `--agent Chat` is interactive-only and incompatible with `--no-ui`.
101
+ > Agents: **Chat** auto-approves reads and asks before writes/bash (interactive default); **Build** has full tool access (headless default); **Plan** denies edits (reads, searches and shell allowed). `--agent Chat` is interactive-only and incompatible with `--no-ui`.
102
102
 
103
103
  **Catalog validation.** For an explicit `--model`, the model is checked against the live catalog before launch — a typo'd name fails fast with same-vendor suggestions. For a model inherited from a previous session (`continue`/`resume` without `--model`), validation is **advisory**: a warning is printed but the session starts anyway. Skip with `--no-validate-model`.
104
104
 
@@ -204,6 +204,8 @@ amicus council run --prompt-file briefing.md --models gemini,glm --chair deepsee
204
204
  | `--var <k=v>` | Set `{{var.<key>}}`; repeatable. Requires `--template`. |
205
205
  | `--tag <t>` | Label this run for `list`/`--search`/`spend --group-by tag` (1-64 chars, `[A-Za-z0-9_-]`; rejected, not cleaned). Every stage's sub-waves (Stage-1, critic/lens solos, Stage-2, chair, debate) carry the same tag on their wave metadata. |
206
206
  | `--intent <review\|task>` | The run's intent (v4.9). `review` — the default — is never stored; `task` marks a task-mode run, recorded as `intent: "task"` on `run.json`/`verdict.json` and kept out of the reliability ledger. Over MCP: the `intent` param on `amicus_council_run`, and a hand-assembled `amicus_council_tally` input may carry `meta.intent` the same emit-when-`task` way. What forks stage by stage — and what stays byte-identical — is in [docs/council.md § Task mode](./council.md#task-mode---intent-task). |
207
+ | `--tools <a,b,c>` | Tool ids stage-1 seats may use, by the engine's own ids (v4.9.8). Task mode defaults to `webfetch`, review to none; `task`/`skill`/`edit`/`write`/`apply_patch`/`question`/`invalid` are refused; anything else is validated against the engine before launch. A local tool (anything that is not `webfetch`/`websearch`/`todowrite` — `read`, `grep`, `glob`, `bash`, …) needs `--out-dir` outside the project tree. With `read` opted in, `.env`, `.env.*` and `.envrc` files are denied at the engine (case-sensitive on Linux; no other spelling is fenced), and the CLI prints a Notice for `read` too, naming those three fenced names and asking you to keep other secrets out; `grep`, `glob` and `bash` have no such per-file fence (grep returns `.env` contents, glob lists `.env` names) and the CLI prints a Notice when you opt any of them in. `bash` is outside every fence (run directory, home, network — including this run's own records: the label map and every review already on disk, so bench anonymity and independence do not hold under bash). Cannot be combined with `--agent`. Over MCP: the `tools` param (tools that never touch the tree — `webfetch`, `websearch`, `todowrite` — only). See [docs/council.md § Tool access](./council.md#tool-access-per-usage---tools). |
208
+ | `--agent <Plan\|Build>` | Escape hatch: run every leg on the engine's own agent instead of the council agents (no allowlist). Its legs run with the run directory (inside the project) as their working directory, so a seat can read the run's own records, the label map included — use it only where that is acceptable. `Build` is edit-capable — it can edit files and run commands there, and the CLI prints a Notice when you opt into it; `Plan` denies edits but allows reads, searches and the shell — the pre-4.9.8 default, so it restores v4.9.7's behaviour, and the CLI prints a Notice for it too (every leg, judges and the chair included, can run commands). Cannot be combined with `--tools`. Over MCP: the `agent` param. |
207
209
 
208
210
  **Exit codes:** `0` full run · `2` degraded but reportable (fewer than 2 judges, chair failure —
209
211
  `overallVerdict: null` — a cost ceiling hit after the tally, or a `--max-cost` ceiling set over a
@@ -303,7 +305,7 @@ Then invoke it with `--pack <name|path>` on `start` / `fanout` / `council run`
303
305
  | `fanout` | `bench` (a saved council name, or an array of ≥2 members) | — | `timeout`, `maxCost`, `gateway`, `agent`, `thinking`, `summaryLength`, `noContext`, `contextTurns`, `contextMaxTokens` |
304
306
  | `solo` | `model` | — | `timeout`, `maxCost`, `gateway`, `agent`, `thinking`, `summaryLength`, `noUi`, `noContext`, `contextTurns`, `contextMaxTokens` |
305
307
 
306
- `council` packs do **not** accept `agent`, `thinking`, or `summaryLength` — they were inert on every surface (no council code path, CLI or MCP, ever reads a pack-filled one; the engine hardcodes agent `Plan`/summaryLength `verbose`), so they were dropped before release rather than shipped as dead weight a pack author would reasonably expect to work. A `council` pack that still sets one fails `pack save` with `PACK_INVALID`, naming the key. They remain valid, and functional, on `fanout`/`solo` packs. Those releases recorded `medium` on EVERY session's metadata, **a fanout leg's included**, whether or not the flag was typed (a level nothing ever sent), so a pack saved with `pack save --from-run` on 4.9.3 or earlier copied it into `options.thinking` on **fanout packs as well as solo ones** — where it then applies to every seat of the bench at once. Such a pack now SENDS it: refused on every model that does not declare `medium` (kimi-k3, Haiku 4.5, deepseek-v4-pro among the curated routes), and on a model that DOES declare it the level really goes out — so a pack that was inert can now change a run's cost and behaviour. Delete the key or re-save the pack from a run that requested a level.
308
+ `council` packs do **not** accept `agent`, `thinking`, or `summaryLength` — they were inert on every surface (no council code path, CLI or MCP, ever reads a pack-filled one; `--agent` / the MCP `agent` param are the only way to set a council run's agent — v4.9.8 — and summaryLength is hardcoded `verbose`), so they were dropped before release rather than shipped as dead weight a pack author would reasonably expect to work. A `council` pack that still sets one fails `pack save` with `PACK_INVALID`, naming the key. They remain valid, and functional, on `fanout`/`solo` packs. Those releases recorded `medium` on EVERY session's metadata, **a fanout leg's included**, whether or not the flag was typed (a level nothing ever sent), so a pack saved with `pack save --from-run` on 4.9.3 or earlier copied it into `options.thinking` on **fanout packs as well as solo ones** — where it then applies to every seat of the bench at once. Such a pack now SENDS it: refused on every model that does not declare `medium` (kimi-k3, Haiku 4.5, deepseek-v4-pro among the curated routes), and on a model that DOES declare it the level really goes out — so a pack that was inert can now change a run's cost and behaviour. Delete the key or re-save the pack from a run that requested a level.
307
309
 
308
310
  Every kind may also carry `description`, `version` (semver, default `1.0.0`), and `briefing.template` (a template **reference**, not rendered text — a pack never captures briefing prose).
309
311
 
@@ -540,7 +542,7 @@ $ amicus status demo123 --json
540
542
  "taskId": "demo123",
541
543
  "status": "complete",
542
544
  "elapsed": "5m 0s",
543
- "version": "4.9.7",
545
+ "version": "4.9.8",
544
546
  "model": "google/gemini-2.5-flash",
545
547
  "phase": "terminal"
546
548
  }
@@ -970,6 +972,8 @@ Every tool below also takes an optional `project` — an absolute path naming th
970
972
  - `debate` — add a Stage-2.5 rebuttal round before the chair synthesizes.
971
973
  - `claudeReviewFile` — path to Claude's own review, included as a judged entry. Claude is reviewed and ranked like a seat, but never judges or chairs.
972
974
  - `intent` — `task` marks a task-mode run (recorded on `run.json`/`verdict.json`, kept out of the reliability ledger): seats produce the deliverable and the chair closes with `ANSWER:` on a disjoint scale. `review` is the default and is never stored. See [docs/council.md § Task mode](./council.md#task-mode---intent-task).
975
+ - `tools` — tool ids stage-1 seats may use, by the engine's own ids (task mode defaults to `webfetch`, review to none). Over MCP only tools that never touch the tree (`webfetch`, `websearch`, `todowrite`) can be opted in: the MCP run directory stays inside the project, so a seat with a local tool (`read`, `grep`, `glob`, `bash`, …) is refused with a message naming the CLI's `--tools` + `--out-dir` (outside the project) instead. `task`, `skill`, `question`, `invalid`, `edit`, `write` and `apply_patch` are always refused. Cannot be combined with `agent`.
976
+ - `agent` — escape hatch: run every leg on the engine's own agent instead of the council agents, with no tool allowlist. `Plan` or `Build` only — unlike `amicus_start`'s `agent`, `Chat` is not accepted here. Cannot be combined with `tools`.
973
977
  - `ui` — auto-open the Council Workspace window for this run. Default: opens under Claude Code (local) when Electron and a display exist and `workspace.autoOpen` is not `false`.
974
978
  - `onComplete`, `pack`, `tag` — as on `amicus_fanout`.
975
979
 
@@ -987,7 +991,7 @@ The `--agent` option controls which OpenCode agent drives the session:
987
991
  |-------|-------------|-------------|
988
992
  | **Chat** | Interactive conversation | Reads freely, asks before writes/bash |
989
993
  | **Build** | Full-access primary agent (headless default) | Read, write, bash, task |
990
- | **Plan** | Read-only analysis | Read-only |
994
+ | **Plan** | Analysis without edits | Read, search, bash; no edits |
991
995
 
992
996
  `--agent Chat` is interactive-only and incompatible with `--no-ui`. Custom agents defined in `~/.config/opencode/agents/` or `.opencode/agents/` are also supported.
993
997
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "amicus",
3
- "version": "4.9.7",
3
+ "version": "4.9.8",
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": [
@@ -231,9 +231,11 @@
231
231
  },
232
232
  "seatsReviewed": {
233
233
  "type": "object",
234
- "description": "#202, optional. How much of the BENCH actually reviewed, derived from runStats: `of` counts every BENCH-role row — `seat`, `critic`, or `lens:<slug>`, exactly the roles seats.js :: buildSeats mints (one per bench seat POST-retry, so a healed seat is counted once and its first attempt is `role:'superseded'`), and `reviewed` counts those whose leg completed. Judges, chair and repair rows are not bench seats and are excluded. EMIT-WHEN-SET: a record with no bench rows carries no key at all, because `0 of 0` would read as a measurement of an empty bench rather than as the absence it is. WHY IT EXISTS: the sibling `seatLoss` above is present only when --critic was requested, and CI runs none — so seat loss was structurally absent from every CI verdict while a two-seat bench published a four-model street-cred table whose dead seats rendered `n/a`, indistinguishable from the legend's neutral (MEASURED, run 4424218c). No `additionalProperties: false` at the top level of this schema means an additive field was always accepted here; this documents the shape rather than changing what is accepted.",
234
+ "description": "#202, optional. How much of the BENCH actually reviewed, derived from runStats: `of` counts every BENCH-role row — `seat`, `critic`, or `lens:<slug>`, exactly the roles seats.js :: buildSeats mints (one per bench seat POST-retry, so a healed seat is counted once and its first attempt is `role:'superseded'`), and `reviewed` counts those whose leg completed. Judges, chair and repair rows are not bench seats and are excluded. EMIT-WHEN-SET: a record with no bench rows carries no key at all, because `0 of 0` would read as a measurement of an empty bench rather than as the absence it is. WHY IT EXISTS: the sibling `seatLoss` above is present only when --critic was requested, and CI runs none — so seat loss was structurally absent from every CI verdict while a two-seat bench published a four-model street-cred table whose dead seats rendered `n/a`, indistinguishable from the legend's neutral (MEASURED, run 4424218c). No `additionalProperties: false` at the top level of this schema means an additive field was always accepted here; this documents the shape rather than changing what is accepted. v4.9.8 (#242): `unverified` (bench seats among `reviewed` whose findings came from a repair of a response with no parseable findings block — runStats[].findingsUnverified) and, since council #248 round 2, `refused` (those whose repair was refused — runStats[].repairRefused) join the census. buildVerdict writes it whenever it writes the census, since that version (0 is a measurement); it is declared, not required, so documents written before it still validate.",
235
235
  "properties": {
236
236
  "reviewed": { "type": "integer", "minimum": 0, "description": "Bench seats whose leg completed." },
237
+ "unverified": { "type": "integer", "minimum": 0, "description": "v4.9.8 (#242): bench seats, counted in `reviewed` too, whose findings came from a repair of a response with no parseable findings block — the tally scored those findings and nothing verified the repair. Not a stub count. A refused repair (runStats[].repairRefused) is not counted here. Never more than `reviewed`: the census and the report share one predicate (a bench role, a completed leg, the literal true), so a flagged row that is not a completed bench seat is counted nowhere and rendered nowhere." },
238
+ "refused": { "type": "integer", "minimum": 0, "description": "v4.9.8 (#242; council #248 round 2): bench seats, counted in `reviewed` too, whose repair was refused (runStats[].repairRefused) — the repair broke its count contract, so the seat contributed NO findings; visible here because a seat that contributed nothing must not read as a full reviewer on the gate. Always written with the census since v4.9.8; never more than `reviewed` (the census and the report share one predicate)." },
237
239
  "of": { "type": "integer", "minimum": 1, "description": "Bench seats benched, post-retry." }
238
240
  },
239
241
  "required": ["reviewed", "of"],
@@ -27,6 +27,12 @@ the deliverable rests on (same JSON shape, same severity enum, `location` = sour
27
27
  the word `assumption`); the judge's ranking axis becomes *how well the work was done* rather than
28
28
  how accurate a critique was; and the chair closes on the ANSWER scale below.
29
29
 
30
+ **v4.9.8 — tools are per run.** The engine registers two agents per council run (`council-seat`
31
+ for stage-1 legs, `council-support` for everything else) and composes the seat's tools sentence
32
+ itself: no tools → the shared no-tools sentence; tools → *"Your tools: webfetch. You have no
33
+ others — …"* (`src/council/seat-tools.js :: seatToolsSentence`). The manual path copies that
34
+ line into each seat's `_tmp-*` briefing; see `docs/council.md § Tool access`.
35
+
30
36
  ---
31
37
 
32
38
  ## Standard anti-sycophancy clause (ALL Stage-1 briefings — not an optional element)