amicus 4.9.6 → 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 (56) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/CHANGELOG.md +246 -0
  3. package/README.md +1 -1
  4. package/docs/ROADMAP.md +3 -3
  5. package/docs/architecture-map.md +24 -4
  6. package/docs/configuration.md +43 -15
  7. package/docs/council.md +140 -3
  8. package/docs/electron-testing.md +133 -0
  9. package/docs/troubleshooting.md +14 -7
  10. package/docs/usage.md +8 -4
  11. package/package.json +1 -1
  12. package/schemas/council-verdict.schema.json +3 -1
  13. package/skills/second-opinion/SEAT-BRIEFS.md +6 -0
  14. package/src/cli-council-run-tools.js +168 -0
  15. package/src/cli-handlers-council-run.js +6 -6
  16. package/src/cli.js +23 -1
  17. package/src/council/briefings-chair.js +1 -1
  18. package/src/council/briefings-task.js +11 -5
  19. package/src/council/briefings.js +25 -7
  20. package/src/council/report-lost-rows.js +89 -0
  21. package/src/council/report-md.js +3 -1
  22. package/src/council/report.js +3 -2
  23. package/src/council/run-degrade.js +22 -1
  24. package/src/council/run-finish.js +23 -1
  25. package/src/council/run-launch.js +33 -4
  26. package/src/council/run-retry-launch.js +9 -4
  27. package/src/council/run-retry.js +3 -0
  28. package/src/council/run-seat-tools-verify.js +296 -0
  29. package/src/council/run-seat-tools.js +274 -0
  30. package/src/council/run-server.js +41 -6
  31. package/src/council/run-stage1-launch.js +8 -3
  32. package/src/council/run.js +21 -21
  33. package/src/council/seat-tools.js +299 -0
  34. package/src/council/verdict-seats-reviewed.js +76 -6
  35. package/src/headless.js +136 -6
  36. package/src/mcp-council-pack-map.js +24 -0
  37. package/src/mcp-council-run.js +17 -15
  38. package/src/mcp-server.js +2 -2
  39. package/src/mcp-tools.js +15 -4
  40. package/src/opencode-client.js +26 -0
  41. package/src/pack/pack-validate.js +3 -1
  42. package/src/prompt-builder.js +2 -2
  43. package/src/sidecar/electron-exe-rel.js +131 -0
  44. package/src/sidecar/electron-install.js +7 -12
  45. package/src/sidecar/electron-layout.js +31 -31
  46. package/src/sidecar/electron-native-plan.js +23 -5
  47. package/src/sidecar/electron-native-rescue.js +55 -16
  48. package/src/sidecar/electron-rescue-notice.js +18 -1
  49. package/src/sidecar/fanout.js +7 -1
  50. package/src/sidecar/heartbeat.js +46 -0
  51. package/src/sidecar/session-utils.js +7 -34
  52. package/src/sidecar/zip-from-buffer.js +16 -5
  53. package/src/sidecar/zip-local-name-scan.js +238 -0
  54. package/src/sidecar/zip-name-scan.js +5 -0
  55. package/src/utils/agent-mapping.js +1 -1
  56. package/src/utils/degrade.js +8 -0
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "amicus",
3
- "version": "4.9.6",
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,252 @@
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
+
100
+ ## [4.9.7] - 2026-09-09
101
+
102
+ Three findings deferred from the v4.9.6 cut, and the two open questions filed beside them. The
103
+ council run that raised the three reviewed at **2 of 4 seats**, so each carried the weight of two
104
+ seats — and on the one that mattered most, the filing turned out to be right about the defect and
105
+ wrong about the fix.
106
+
107
+ ### Fixed
108
+
109
+ - **A promote can no longer delete a working install that was cross-installed for another
110
+ platform.** v4.9.6 captured and restored a `path.txt` naming another platform's executable, but
111
+ the guard that decides whether a `dist/` may be removed in place still asked only about THIS
112
+ platform's default name. On a package installed through `npm_config_platform`, a promote whose
113
+ retirement rename failed therefore restored the pointer and deleted the tree it pointed at —
114
+ reproduced against the shipped code with a real filesystem and only `renameSync` injected to
115
+ throw the EPERM the fallback exists for.
116
+
117
+ The guard now judges by what `path.txt` actually names. **As a union with the platform default,
118
+ never as a replacement** — which is the part the finding did not say, and implementing its
119
+ sentence literally destroys a working `dist/electron.exe` on three shapes the old guard protected:
120
+ a whitespace-only `path.txt`, one with a trailing newline (`existsSync` of a name ending in `\n`
121
+ is false on Windows), and a TRUNCATED one — `electr`, the shape this function's own best-effort
122
+ put-back can leave. The platform-default arm runs first and is unchanged, so the set of trees a
123
+ promote will delete can only ever shrink.
124
+
125
+ The `path.txt` name additionally has to be **contained** in `dist/` and has to be a **file**: a
126
+ `path.txt` of `..`, `.` or `../SIBLING` names something that exists but that the delete never
127
+ touches, and every truncation of the darwin name (`Electron.app`, `Electron.app/Contents`,
128
+ `.../MacOS`) is a real directory — accepting either would refuse every promote forever while
129
+ reporting that `dist/` "holds a usable Electron.app". The refusal message now names the executable
130
+ that was found rather than the one that was looked for, because `docs/troubleshooting.md` tells
131
+ the user that refusal means the tree holds a usable executable.
132
+
133
+ An **unreadable** `path.txt` now refuses rather than guessing; an absent one still heals. The rule
134
+ itself moved to `src/sidecar/electron-exe-rel.js` and `resolveElectronBinary` now calls it, with a
135
+ table-driven test asserting both sides agree over seven `path.txt` shapes on three platforms with
136
+ and without `ELECTRON_OVERRIDE_DIST_PATH`. Two copies of one rule are what let these drift, and
137
+ the finding's own words for the defect were "did not carry one function over".
138
+
139
+ - **(docs)** `.github/workflows/ci.yml` claimed `--omit=optional` "skips the Electron download". It
140
+ skips the optional *package*; there is no download on the default path to skip, because
141
+ `electron@43.1.1` ships no install script at all. Measured on run `34246117877` across all three
142
+ runner platforms.
143
+
144
+ ### Security
145
+
146
+ - **The rescue boundary now reads BOTH tables an archive declares its entry names in.** A zip
147
+ carries its names twice — in the central directory and in each local file header — and amicus
148
+ read only the first. An archive can blind that one while leaving every local header whole: cutting
149
+ the tail off does it by accident, and four one-field edits to a *complete* end-of-central-directory
150
+ record do it on purpose. Measured against the real extractor with the hatch armed, **seven such
151
+ archives carrying a `../../../` entry reached a native extractor, and two ran to completion and
152
+ promoted the result.** Nothing escaped only because the Windows tools refuse `..` themselves — the
153
+ exact reliance this subsystem says amicus will not make.
154
+
155
+ The two tables can also disagree, and the tools do not agree on which to believe: measured,
156
+ `tar.exe` wrote the name from the local header while `Expand-Archive` wrote the one from the
157
+ central directory. A refusal in either table now refuses the archive.
158
+
159
+ **It costs no measured availability.** On six real Electron artifacts the local walk enumerates
160
+ every name in 0-1 ms and agrees with the central table entry-for-entry; on a truncated archive,
161
+ where the central walk goes blind, it still reads them all. The archives this rescue exists for
162
+ are still rescued. And the notice printed before a spawn now says *which* names were checked —
163
+ all of them, some of them, or none — because a real artifact truncated by a few kilobytes leaves
164
+ both walks incomplete while every name it found was read and cleared, and a two-state notice would
165
+ have claimed nothing was checked over dozens that were.
166
+
167
+ **Two council rounds then found the first cut of this incomplete, and the second cut of it
168
+ incomplete again — both in the same direction.** A walk that could not follow the chain used to
169
+ STOP, and an entry it never reached is an entry it cannot refuse: a lying size jumped one, and
170
+ later a *standard* data-descriptor entry (general-purpose bit 3 with a zero size — an ordinary
171
+ streaming encoding, not a malformed one) hid everything behind it. Neither needed an exotic
172
+ archive. So the walk now DEMOTES its claim and keeps going wherever it still has an offset to
173
+ follow — because a name it can still read is a name it can still refuse — and where it genuinely
174
+ has none, it SWEEPS the region it could not reach for local headers and puts their names through
175
+ the same rule. It can only add refusals, so an archive that is rescued today stops being rescued
176
+ only if it declares a hostile name. **The practical consequence for a user: an archive carrying a
177
+ traversal name behind an unreadable point is now refused where it previously reached the native
178
+ extractor.**
179
+
180
+ The claim "every name was checked" also became a conjunction rather than a disjunction. The two
181
+ tables carry different names and the two extractors read different ones, so one readable table
182
+ never meant both were clean — measured, a stopped local walk beside a benign central directory
183
+ reported all-clear and printed nothing while `tar.exe` reached a `../../../` entry only the local
184
+ table carried.
185
+
186
+ ### Added
187
+
188
+ - **A macOS job that runs the real extract path over the real Electron `.app` artifact**
189
+ (`.github/workflows/darwin-bundle.yml`, `scripts/probe-darwin-extract.js`). v4.9.6 shipped a
190
+ symlink target-escape check that `extract-zip` does not have, and disclosed that it might *reject*
191
+ a layout that previously worked. **It does not** — measured against the real
192
+ `electron-v43.1.1-darwin-arm64.zip`: 585 records, 14 symlinks, every target relative, none with a
193
+ `..` component, none absolute, and none of the 585 entry names traversing a symlinked component.
194
+ The linux artifacts hold zero symlink entries, so that path is unreachable there at all.
195
+
196
+ What was left was coverage rather than risk, and the job closes it: a real `symlinkSync`, a real
197
+ `realpathSync` behind the chain control, the absolute-target branch on POSIX arithmetic, and
198
+ `Electron --version` loading the framework through two of the fourteen links. Ten assertions, each
199
+ naming the one-line mutation it catches. The parity diff against `@electron-internal/extract-zip`'s
200
+ own tree ships **report-only** on its first cut — that comparison has never been measured, and a
201
+ gate that has never been run once is not evidence.
202
+
203
+ ### Changed
204
+
205
+ - **The claim that the native extractors refuse traversal themselves is now re-measured on every CI
206
+ run, instead of being asserted once.** That claim is the compensating control the rescue's
207
+ boundary leans on, and a claim nothing re-measures is a claim that rots.
208
+ `tests/sidecar/native-extractor-containment.test.js` drives 12 escape shapes through every
209
+ strategy that resolves on the machine. Newly measured this way: Info-ZIP `unzip` contains its own
210
+ escapes by stripping them, and GNU `tar` cannot read a zip at all — both were previously written
211
+ down as unmeasured. **`ditto`, the first macOS strategy, is now the only unmeasured one**, and the
212
+ macOS runner already in the matrix measures it the first time this suite runs there.
213
+
214
+ A proposed runtime "fence" around the Electron package directory was **refused rather than
215
+ deferred**, and the reasons are recorded because they were measured: a names-diff fence is blind
216
+ to an overwrite of `dist/electron.exe` or `path.txt`; and signalling a breach through the
217
+ available channel deletes the user's cached artifact and reports it corrupt — which a *false*
218
+ breach, reachable from a concurrent provision, would then do to the air-gapped machine this whole
219
+ feature exists for.
220
+
221
+ Half of the finding that prompted it was already false: everything the rescue writes lives under a
222
+ private incoming directory that is deleted unconditionally, so only an absolute write outside that
223
+ tree survives a failed strategy. Two shipped documents said otherwise and are corrected. The child
224
+ now also runs with that directory as its working directory — measured neutral, and it costs a line.
225
+
226
+ - **The CI council bench reserves 64,000 output tokens per leg instead of the engine's 32,000
227
+ default** — for every PR whose base carries the change. The alias map is fetched from the BASE
228
+ REF, so a branch cannot change the reviewers of its own PR; the config therefore lands on `main`
229
+ separately and takes effect for runs opened after it, never for the branch carrying it. Measured:
230
+ the run on the PR that first carried this reported `outputBudget is unset` and lost a seat at the
231
+ 32,000 default exactly as before. Across the five paid council runs on the v4.9.6 branch the bench averaged **2.8 of 4
232
+ seats**, and four of the six lost seats died at the reservation with `finish: 'length'` and
233
+ 0–651 usable output tokens.
234
+
235
+ **64,000 is measured, not chosen.** Against the pinned engine with the bare descriptors CI
236
+ registers, it lands unclamped on all four seats and the chair in both catalogue states. The
237
+ binding ceiling is **65,536**, and on a cold read it is shared by **two** rows — the chair and
238
+ `deepseek-v4-flash-0731` — not by the chair alone: the flag at 100,000 arrives as
239
+ `max_tokens 65536` on both. A budget above it would land in full on three seats while two were
240
+ silently clamped, and a run would stop reserving one number. The pre-flight step now fails before
241
+ any spend on a budget above the ceiling or on one amicus would reject and silently replace with
242
+ 32,000; the alias map's own validator inspects aliases only, so nothing caught that before.
243
+
244
+ **What it does not fix, said up front:** the other two lost seats — and ten of the eleven failed
245
+ first attempts — produced nothing at all and never reached a reservation. Expect roughly 3.6 of 4,
246
+ not 4 of 4. Read the death class rather than the seat count: three of the four targeted legs ran
247
+ 529–596 s of a 960 s leg cap, so an `OUTPUT_LENGTH` death can convert into a generic timeout,
248
+ which is the same lost seat with a worse diagnosis. The leg cap deliberately did not move in the
249
+ same change — raising it busts the 75-minute job cap, and a bust cancels the job, which deletes
250
+ the run-directory artifact every diagnosis of this class rests on.
251
+
6
252
  ## [4.9.6] - 2026-09-08
7
253
 
8
254
  *Amicus never itself writes, or reports as verified, bytes it did not hash.*
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.6",
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.6** (2026-09-08). 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) 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.6 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
@@ -112,6 +116,7 @@ src/
112
116
  │ ├── electron-custody.js # CUSTODY of the Electron artifact: one open, one read, one Buffer.
113
117
  │ ├── electron-ensure.js # ensureElectron() — lazy first-GUI provisioning (#55).
114
118
  │ ├── electron-env-scrub.js # The ENV SCRUB — which environment names a hostile REPOSITORY can plant.
119
+ │ ├── electron-exe-rel.js # WHICH exe a package resolves through, and whether a `dist/` HOLDS one.
115
120
  │ ├── electron-install.js # Electron self-heal primitive (#53, #59).
116
121
  │ ├── electron-layout.js # The on-disk LAYOUT of an installed `electron` package: where the executable
117
122
  │ ├── electron-lock.js # Stale-aware single-flight lock for the electron self-heal (#53).
@@ -134,6 +139,7 @@ src/
134
139
  │ ├── fanout-validate.js
135
140
  │ ├── fanout-wave-io.js
136
141
  │ ├── fanout.js
142
+ │ ├── heartbeat.js
137
143
  │ ├── interactive-abort.js
138
144
  │ ├── interactive-mirror.js
139
145
  │ ├── interactive-process.js # Sidecar Interactive Process Helpers - Electron probe/env/process-exit plumbing
@@ -166,6 +172,7 @@ src/
166
172
  │ ├── workspace-window.js # Council Workspace launcher (v4.4 §4.3/§4.4) — setup-window.js pattern:
167
173
  │ ├── zip-entry-write.js # ONE ENTRY of an in-memory archive, and the classified failures every caller
168
174
  │ ├── zip-from-buffer.js # Extract an archive that is ALREADY IN MEMORY and ALREADY HASHED.
175
+ │ ├── zip-local-name-scan.js # THE OTHER TABLE AN ARCHIVE DECLARES ITS NAMES IN — the local file headers.
169
176
  │ ├── zip-name-scan.js # WHAT NAMES DOES THIS ARCHIVE DECLARE? A read-only walk of the central
170
177
  │ └── zip-stall-bound.js # WHEN AMICUS GIVES UP ON AN IN-MEMORY EXTRACTION, and how it stops the work.
171
178
  ├── template/
@@ -306,6 +313,7 @@ src/
306
313
  │ └── seat-space.js # Council Workspace — the seat-space PREDICATES (v4.8 PR5b).
307
314
  ├── cli-council-run-bench.js # Bench and input resolution for the council run command.
308
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`.
309
317
  ├── cli-handlers-abort.js # CLI Abort Handler (B21-rest extraction)
310
318
  ├── cli-handlers-council-run.js
311
319
  ├── cli-handlers-council.js
@@ -334,6 +342,7 @@ src/
334
342
  ├── jsonl-parser.js # JSONL Parser
335
343
  ├── mcp-council-awareness.js
336
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).
337
346
  ├── mcp-council-run.js
338
347
  ├── mcp-notify.js # Pure helpers + in-process registry for the MCP `onComplete: 'mcp-notify'`
339
348
  ├── mcp-server.js # @module mcp-server — Amicus MCP Server (stdio transport)
@@ -417,6 +426,8 @@ scripts/
417
426
  ├── integration-test.sh
418
427
  ├── mark-test-passed.js # Writes the current git HEAD SHA to .test-passed for the pre-push SHA cache
419
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).
430
+ ├── probe-darwin-extract.js
420
431
  ├── probe-max-tokens.js # Wire probe for issue #218: what max_tokens / reasoning / thinking does the
421
432
  ├── run-integration-keyless.js
422
433
  ├── setup-hooks.js # Configure git to run the version-controlled hooks in .husky/.
@@ -449,6 +460,7 @@ evals/
449
460
  |--------|---------|-------------|
450
461
  | `cli-council-run-bench.js` | Bench and input resolution for the council run command. | `resolveBench()`, `resolveChair()`, `resolveCritic()`, `CHAIR_DEFAULT()`, `parseList()` |
451
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()` |
452
464
  | `cli-handlers-abort.js` | CLI Abort Handler (B21-rest extraction) | `handleAbort()` |
453
465
  | `cli-handlers-council-run.js` | | `handleCouncilRun()`, `renderRunHuman()`, `CHAIR_DEFAULT()` |
454
466
  | `cli-handlers-council.js` | | `handleCouncil()` |
@@ -477,6 +489,7 @@ evals/
477
489
  | `jsonl-parser.js` | JSONL Parser | `parseJSONLLine()`, `readJSONL()`, `extractTimestamp()`, `formatMessage()`, `formatContext()` |
478
490
  | `mcp-council-awareness.js` | | `subWaveIds()`, `countWaveLegs()`, `elapsedOf()`, `enginePid()`, `buildCouncilStatusPayload()` |
479
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()` |
480
493
  | `mcp-council-run.js` | | `handleCouncilRunTool()`, `COUNCIL_PACK_PARAM_MAP()`, `buildCouncilStatusPayload()`, `listCouncilRuns()`, `abortCouncilRun()` |
481
494
  | `mcp-notify.js` | Pure helpers + in-process registry for the MCP `onComplete: 'mcp-notify'` | `validateOnComplete()`, `buildNotifyPayload()`, `requestMcpNotify()`, `consumeMcpNotify()` |
482
495
  | `mcp-server.js` | @module mcp-server — Amicus MCP Server (stdio transport) | `handlers()`, `startMcpServer()`, `getProjectDir()`, `resolveProjectDir()`, `getClientRoot()` |
@@ -491,7 +504,7 @@ evals/
491
504
  | `spend-query.js` | | `filterRows()`, `groupRows()`, `computeWasted()`, `emptyTokens()`, `addTokens()` |
492
505
  | `council/anonymize.js` | | `assignLabels()`, `toGlobalId()`, `toGlobalFindings()`, `rankingToOrder()`, `LETTERS()` |
493
506
  | `council/briefings-chair-task.js` | | `CHAIR_ANSWER_VALUES()`, `ANSWER_SCALE_ADDENDUM()`, `TASK_CHAIR_SYNTHESIS()`, `TASK_CHAIR_SYNTHESIS_NO_CLAIMS()`, `TASK_CONCURRENCE_CAVEAT()` |
494
- | `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()` |
495
508
  | `council/briefings-debate.js` | | `DEBATE_NO_TOOLS_PREAMBLE()`, `DEFENSE_CONTRACT()`, `REVOTE_CONTRACT()`, `buildDefenseBrief()`, `buildRevoteBundle()` |
496
509
  | `council/briefings-stage2-task.js` | | `TASK_JUDGE_FRAME()`, `TASK_JUDGE_A()`, `TASK_JUDGE_B()`, `TASK_JUDGE_B_NO_CLAIMS()`, `NO_CLAIMS_INDEX()` |
497
510
  | `council/briefings-stage2.js` | | `JUDGE_NO_TOOLS_PREAMBLE()`, `CHAIR_NO_TOOLS_PREAMBLE()`, `CHAIR_VERDICT_VALUES()`, `JUDGE_OUTPUT_CONTRACT()`, `VERDICT_SCALE_ADDENDUM()` |
@@ -508,6 +521,7 @@ evals/
508
521
  | `council/presets-cli.js` | | `runSave()`, `runList()`, `runShow()` |
509
522
  | `council/report-cost.js` | | `buildCostModel()` |
510
523
  | `council/report-html.js` | | `renderHtml()` |
524
+ | `council/report-lost-rows.js` | | `lostRowsOf()` |
511
525
  | `council/report-md.js` | | `renderMd()` |
512
526
  | `council/report.js` | | `buildReport()`, `toModel()`, `TIER_ORDER()`, `SYMBOL()`, `isSeatSpace()` |
513
527
  | `council/run-assemble.js` | | `buildRunStatsEntry()`, `worseConformance()`, `buildTallyInput()`, `writeTallyFiles()`, `writeVerdictFiles()` |
@@ -516,7 +530,7 @@ evals/
516
530
  | `council/run-debate-revote.js` | | `legOpts()`, `legRow()`, `runRevoteWave()` |
517
531
  | `council/run-debate-stage.js` | | `runDebateStage()` |
518
532
  | `council/run-debate.js` | | `runDebate()`, `nothingToDebate()`, `disputingJudges()`, `debateTargets()` |
519
- | `council/run-degrade.js` | | `createDegradeSink()` |
533
+ | `council/run-degrade.js` | | `createDegradeSink()`, `noteDroppedMembers()` |
520
534
  | `council/run-finalize.js` | | `statusForExit()`, `resolveTerminalExit()`, `writeRunTerminal()`, `SIGNAL_EXIT()` |
521
535
  | `council/run-finish.js` | | `finishRun()` |
522
536
  | `council/run-launch.js` | | `createLaunchers()`, `materializeReviews()`, `materializeDebate()`, `sanitizeName()`, `isAbortExit()` |
@@ -526,7 +540,9 @@ evals/
526
540
  | `council/run-retry-notes.js` | | `waveStillDeadNote()`, `skippedWaveNote()`, `srcLegStillDeadNote()`, `retryLegStillDeadNote()`, `missingLegStillDeadNote()` |
527
541
  | `council/run-retry-window.js` | The Stage-1 retry's no-output window: how long a RELAUNCHED leg may stay | `retryBackstopMs()` |
528
542
  | `council/run-retry.js` | | `groupStage1Losses()`, `retryStage1Losses()` |
529
- | `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()` |
530
546
  | `council/run-stage1-launch.js` | Stage-1 launch pass for the council engine. | `launchStage1()` |
531
547
  | `council/run-stage1-rows.js` | | `pushDeadSeatRows()`, `supersededRows()` |
532
548
  | `council/run-stage1-superseded.js` | | `supersededRows()` |
@@ -536,12 +552,13 @@ evals/
536
552
  | `council/run-stats-entry.js` | | `buildRunStatsEntry()` |
537
553
  | `council/run-verdict-files.js` | | `writeVerdictFiles()` |
538
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()` |
539
556
  | `council/seats.js` | | `buildSeats()`, `roleAt()`, `bindSeats()`, `artifactName()`, `displayName()` |
540
557
  | `council/stage1-bind.js` | | `bindStage1Waves()`, `orphanLegNote()`, `missingSeatDeadWave()`, `bindPaddedWave()` |
541
558
  | `council/street-cred.js` | | `computeStreetCred()`, `rankPositions()`, `credSeats()` |
542
559
  | `council/tally.js` | | `assignTier()`, `computeStreetCred()`, `tally()`, `COUNCIL_SCHEMA_VERSION()` |
543
560
  | `council/verdict-seat-loss.js` | | `summarizeSeatLoss()`, `deriveSeatLoss()` |
544
- | `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()` |
545
562
  | `council/verdict.js` | | `buildVerdict()`, `summarizeSeatLoss()`, `deriveSeatLoss()`, `readOverallVerdict()`, `readPriorVerdictSurfaces()` |
546
563
  | `design/tokens.js` | | `tokenCss()`, `TOKENS()` |
547
564
  | `observe/council-legs.js` | | `buildLegRows()` |
@@ -566,6 +583,7 @@ evals/
566
583
  | `sidecar/electron-custody.js` | CUSTODY of the Electron artifact: one open, one read, one Buffer. | `readArtifactBytes()`, `isSafeArtifactName()`, `MAX_ARTIFACT_BYTES()`, `READ_CHUNK()` |
567
584
  | `sidecar/electron-ensure.js` | ensureElectron() — lazy first-GUI provisioning (#55). | `ensureElectron()`, `_resetEnsureElectron()` |
568
585
  | `sidecar/electron-env-scrub.js` | The ENV SCRUB — which environment names a hostile REPOSITORY can plant. | `isRepoPlantedName()`, `withScrubbedRepoEnv()`, `REPO_ENV_PREFIXES()` |
586
+ | `sidecar/electron-exe-rel.js` | WHICH exe a package resolves through, and whether a `dist/` HOLDS one. | `platformExe()`, `writePathTxt()`, `heldExeRel()`, `distHeldExe()` |
569
587
  | `sidecar/electron-install.js` | Electron self-heal primitive (#53, #59). | `resolveElectronBinary()`, `isElectronUsable()`, `cachedZip()`, `repairElectron()`, `platformExe()` |
570
588
  | `sidecar/electron-layout.js` | The on-disk LAYOUT of an installed `electron` package: where the executable | `platformExe()`, `writePathTxt()`, `promoteDist()`, `extractBytesToDist()`, `sweepPromoteLitter()` |
571
589
  | `sidecar/electron-lock.js` | Stale-aware single-flight lock for the electron self-heal (#53). | `acquireRepairLock()`, `isStaleLock()`, `lockPathFor()`, `STALE_MS()` |
@@ -588,6 +606,7 @@ evals/
588
606
  | `sidecar/fanout-validate.js` | | `parseModelsList()`, `DEFAULT_MAX_LEGS()`, `validateFanoutModels()` |
589
607
  | `sidecar/fanout-wave-io.js` | | `writeWaveMetadata()`, `writeWaveDoc()`, `finishWave()`, `stampLegAttribution()` |
590
608
  | `sidecar/fanout.js` | | `parseModelsList()`, `deriveLegIds()`, `validateFanoutModels()`, `DEFAULT_MAX_LEGS()`, `runFanout()` |
609
+ | `sidecar/heartbeat.js` | | `HEARTBEAT_INTERVAL()`, `createHeartbeat()` |
591
610
  | `sidecar/interactive-abort.js` | | `startAbortWatch()`, `markResultAborted()`, `readAbortedMarker()`, `DEFAULT_INTERVAL_MS()` |
592
611
  | `sidecar/interactive-mirror.js` | | `startInteractiveMirror()` |
593
612
  | `sidecar/interactive-process.js` | Sidecar Interactive Process Helpers - Electron probe/env/process-exit plumbing | `getElectronPath()`, `checkElectronAvailable()`, `buildElectronEnv()`, `handleElectronProcess()` |
@@ -620,6 +639,7 @@ evals/
620
639
  | `sidecar/workspace-window.js` | Council Workspace launcher (v4.4 §4.3/§4.4) — setup-window.js pattern: | `launchWorkspaceWindow()`, `launchWorkspaceWindowDetached()` |
621
640
  | `sidecar/zip-entry-write.js` | ONE ENTRY of an in-memory archive, and the classified failures every caller | `failure()`, `badArchive()`, `badDestination()`, `outOfBound()`, `extractorUnavailable()` |
622
641
  | `sidecar/zip-from-buffer.js` | Extract an archive that is ALREADY IN MEMORY and ALREADY HASHED. | `extractZipBuffer()` |
642
+ | `sidecar/zip-local-name-scan.js` | THE OTHER TABLE AN ARCHIVE DECLARES ITS NAMES IN — the local file headers. | `scanLocalNames()`, `declaredCentralOffset()` |
623
643
  | `sidecar/zip-name-scan.js` | WHAT NAMES DOES THIS ARCHIVE DECLARE? A read-only walk of the central | `scanEntryNames()`, `nameRefusal()`, `SCAN_MS()`, `MAX_ENTRIES()` |
624
644
  | `sidecar/zip-stall-bound.js` | WHEN AMICUS GIVES UP ON AN IN-MEMORY EXTRACTION, and how it stops the work. | `IDLE_MS()`, `MAX_MS()`, `UNWIND_MS()`, `stalled()`, `awaitUnwind()` |
625
645
  | `template/apply.js` | | `applyTemplate()`, `ARTIFACT_CAP_BYTES()` |
@@ -405,26 +405,54 @@ These variables control the polling loop that drives headless sessions. The defa
405
405
  > entry early in the archive ends the walk before any later name is looked at.
406
406
  > Measured — the same three-entry archive, one flag bit apart — that moves it out
407
407
  > of the terminal class and into the rescuable one. Amicus therefore also **reads
408
- > the archive's central directory and refuses any entry name yauzl would have
409
- > refused**, before it hands anything to a native extractor. Two residuals
410
- > survive, and neither is engineered away:
408
+ > the entry names the archive declares and refuses any that yauzl would have
409
+ > refused**, before it hands anything to a native extractor and it reads them in
410
+ > **both of the tables a zip declares them in**, the central directory and the
411
+ > local file headers. That second walk is not belt-and-braces. An archive can
412
+ > blind the central directory while leaving every local header whole — a
413
+ > truncation does it by accident, and four one-field edits to a complete
414
+ > end-of-central-directory record do it on purpose — and measured, seven such
415
+ > archives carrying a `../../../` entry reached a native extractor before this
416
+ > was added, two of them running to completion. The tables can also **disagree**,
417
+ > and the tools do not agree on which one to believe: measured, `tar.exe` wrote
418
+ > the name from the LOCAL header and `Expand-Archive` the one from the CENTRAL
419
+ > directory. A refusal in either table refuses the archive.
411
420
  >
412
- > - An archive whose **central directory cannot be read at all** — a truncated
413
- > zip, which is the commonest thing this rescue exists for — declares no names
414
- > amicus can see, and it still goes to the native extractor. The only remaining
415
- > check is that extractor's own. `tar` and `Expand-Archive` were measured to
416
- > refuse a `..` entry themselves (`tar.exe`: `Path contains '..'`, exit 1;
417
- > `Expand-Archive`: `Can not process invalid archive entry '…'`; nothing written
418
- > outside the destination in either case). **`ditto` and Info-ZIP `unzip`, the
419
- > macOS and Linux strategies, are unmeasured.**
421
+ > Residuals survive, and none is engineered away:
422
+ >
423
+ > - An archive that defeats **both** walks still reaches the native extractor.
424
+ > That is rarer than it was a truncated zip, the commonest thing this rescue
425
+ > exists for, still yields all of its local names but it is not impossible,
426
+ > and the notice printed before the spawn now tells you **which** names were
427
+ > checked rather than letting you assume they all were.
420
428
  > - A **symlink whose target escapes** the extraction root is a payload, not a
421
429
  > name, so no name scan can see it. Amicus's own in-memory extractor refuses
422
430
  > those; a native extractor is not asked to.
431
+ > - The name walk trusts a local header's declared size to find the next one, and
432
+ > a wrong size is a property of exactly the corrupt archives this rescue is for.
433
+ > A desynchronised walk can read a "name" out of payload bytes. It fails toward
434
+ > **refusing**, so it can cost a rescue and never grant one.
435
+ >
436
+ > When a name check cannot see it, the remaining check is the extractor's own, and
437
+ > those are measured on every CI run rather than asserted here
438
+ > (`tests/sidecar/native-extractor-containment.test.js`, 12 escape shapes per
439
+ > strategy). At the time of writing: `tar.exe` refuses `..` (`Path contains '..'`,
440
+ > exit 1) and strips absolute and drive-letter names into the destination;
441
+ > `Expand-Archive` refuses `..` (`Can not process invalid archive entry '…'`) and
442
+ > errors out on drive-letter and `\\?\` shapes with an empty destination; Info-ZIP
443
+ > `unzip` contains by stripping the escaping components and writing the entry
444
+ > inside the destination under a mangled name (its own `-hh` text documents this
445
+ > as the default, with `-:` as the opt-out, which amicus does not pass); GNU `tar`
446
+ > cannot read a zip at all. **`ditto`, the first macOS strategy, is the one that
447
+ > remains unmeasured.**
423
448
  >
424
- > Amicus cleans up only *inside* the directory it asked the extractor to write to,
425
- > so anything a native tool wrote outside it would survive a failed strategy. This
426
- > is the concrete shape of "not a safe operation": if the archive came from
427
- > somewhere you do not trust, do not set this variable get another copy.
449
+ > Everything amicus writes for a rescue goes under a private incoming directory
450
+ > that is deleted unconditionally afterwards, and the child is run with that
451
+ > directory as its working directory, so a relative write lands where the cleanup
452
+ > reaches. What amicus cannot clean up is a write a native tool makes to an
453
+ > absolute path outside that tree. This is the concrete shape of "not a safe
454
+ > operation": if the archive came from somewhere you do not trust, do not set this
455
+ > variable — get another copy.
428
456
  >
429
457
  > **The one thing the strip does not cover, stated precisely** (measured against
430
458
  > `@electron/get` 5.0.0, and re-measured by