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.
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +246 -0
- package/README.md +1 -1
- package/docs/ROADMAP.md +3 -3
- package/docs/architecture-map.md +24 -4
- package/docs/configuration.md +43 -15
- package/docs/council.md +140 -3
- package/docs/electron-testing.md +133 -0
- package/docs/troubleshooting.md +14 -7
- package/docs/usage.md +8 -4
- package/package.json +1 -1
- package/schemas/council-verdict.schema.json +3 -1
- package/skills/second-opinion/SEAT-BRIEFS.md +6 -0
- package/src/cli-council-run-tools.js +168 -0
- package/src/cli-handlers-council-run.js +6 -6
- package/src/cli.js +23 -1
- package/src/council/briefings-chair.js +1 -1
- package/src/council/briefings-task.js +11 -5
- package/src/council/briefings.js +25 -7
- package/src/council/report-lost-rows.js +89 -0
- package/src/council/report-md.js +3 -1
- package/src/council/report.js +3 -2
- package/src/council/run-degrade.js +22 -1
- package/src/council/run-finish.js +23 -1
- package/src/council/run-launch.js +33 -4
- package/src/council/run-retry-launch.js +9 -4
- package/src/council/run-retry.js +3 -0
- package/src/council/run-seat-tools-verify.js +296 -0
- package/src/council/run-seat-tools.js +274 -0
- package/src/council/run-server.js +41 -6
- package/src/council/run-stage1-launch.js +8 -3
- package/src/council/run.js +21 -21
- package/src/council/seat-tools.js +299 -0
- package/src/council/verdict-seats-reviewed.js +76 -6
- package/src/headless.js +136 -6
- package/src/mcp-council-pack-map.js +24 -0
- package/src/mcp-council-run.js +17 -15
- package/src/mcp-server.js +2 -2
- package/src/mcp-tools.js +15 -4
- package/src/opencode-client.js +26 -0
- package/src/pack/pack-validate.js +3 -1
- package/src/prompt-builder.js +2 -2
- package/src/sidecar/electron-exe-rel.js +131 -0
- package/src/sidecar/electron-install.js +7 -12
- package/src/sidecar/electron-layout.js +31 -31
- package/src/sidecar/electron-native-plan.js +23 -5
- package/src/sidecar/electron-native-rescue.js +55 -16
- package/src/sidecar/electron-rescue-notice.js +18 -1
- package/src/sidecar/fanout.js +7 -1
- package/src/sidecar/heartbeat.js +46 -0
- package/src/sidecar/session-utils.js +7 -34
- package/src/sidecar/zip-from-buffer.js +16 -5
- package/src/sidecar/zip-local-name-scan.js +238 -0
- package/src/sidecar/zip-name-scan.js +5 -0
- package/src/utils/agent-mapping.js +1 -1
- package/src/utils/degrade.js +8 -0
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?}
|
|
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 (
|
|
901
|
-
|
|
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/electron-testing.md
CHANGED
|
@@ -469,3 +469,136 @@ setTimeout(() => { ws.close(); process.exit(0); }, 3000);
|
|
|
469
469
|
```
|
|
470
470
|
|
|
471
471
|
**Note:** `window.sidecar` is `undefined` in the toolbar (see Known Limitations above). The toolbar communicates with the main process via `window.__amicusUpdateAction` polling, not IPC.
|
|
472
|
+
|
|
473
|
+
---
|
|
474
|
+
|
|
475
|
+
## The darwin `.app` bundle — what CI proves, and what it still does not
|
|
476
|
+
|
|
477
|
+
`.github/workflows/darwin-bundle.yml` drives `scripts/probe-darwin-extract.js` on
|
|
478
|
+
`macos-latest`. It is the only place amicus's real extract path meets a real `.app`
|
|
479
|
+
bundle, a real POSIX `fs.symlinkSync`, a real `fs.realpathSync` and a real dyld.
|
|
480
|
+
|
|
481
|
+
### The filing it closes, and the way it closes it
|
|
482
|
+
|
|
483
|
+
v4.9.6 added a symlink target-escape check (`src/sidecar/zip-entry-write.js ::
|
|
484
|
+
writeSymlink`) that `extract-zip` does not have. The v4.9.7 filing was that it could
|
|
485
|
+
**reject** a real darwin layout. That is **refuted by measurement on the real bytes**,
|
|
486
|
+
not by this job:
|
|
487
|
+
|
|
488
|
+
| artifact | bytes | records | symlinks | dir entries |
|
|
489
|
+
| --- | --- | --- | --- | --- |
|
|
490
|
+
| `electron-v43.1.1-darwin-arm64.zip` | 122,054,683 | 585 | 14 | 310 |
|
|
491
|
+
| `electron-v43.1.1-darwin-x64.zip` | 123,952,132 | 585 | 14 | 310 |
|
|
492
|
+
| `electron-v43.1.1-linux-x64.zip` | 124,861,804 | 74 | **0** | 0 |
|
|
493
|
+
| `electron-v43.1.1-linux-arm64.zip` | 124,456,257 | 74 | **0** | 0 |
|
|
494
|
+
|
|
495
|
+
All 14 darwin targets are relative (`A`, `Versions/Current/Resources`, …); none carries a
|
|
496
|
+
`..` component; none is absolute; **0 of the 585 entry names traverse a symlinked
|
|
497
|
+
component**. The check is purely lexical (`path.resolve` then `path.relative`), so on that
|
|
498
|
+
shape it cannot fire. The extraction root is itself realpath'd
|
|
499
|
+
(`src/sidecar/zip-from-buffer.js :: extractZipBuffer`), which is why macOS's
|
|
500
|
+
`/var` -> `/private/var` does not turn every link into an escape — the one mechanism that
|
|
501
|
+
could have made the filing correct.
|
|
502
|
+
|
|
503
|
+
**Linux is settled outright and gets no job**: both linux artifacts hold 74 entries and
|
|
504
|
+
zero symlinks, so `writeSymlink` is unreachable there. The filing's "darwin/linux" narrows
|
|
505
|
+
to "darwin".
|
|
506
|
+
|
|
507
|
+
### What each assertion pins, and the mutant it kills
|
|
508
|
+
|
|
509
|
+
| id | assertion | mutant it kills |
|
|
510
|
+
| --- | --- | --- |
|
|
511
|
+
| A1a | `resolveAnchor({selfElectronDir: null})` reads the `checksums.json` under test | dropping the `selfElectronDir` override, which silently re-anchors on the repo's own `node_modules` |
|
|
512
|
+
| A1b | `repairElectron({cacheOnly:true})` repairs **and** the gate says `verified` | any change that stops a byte-exact artifact verifying — extract and exec both stay green |
|
|
513
|
+
| A1c | with **no** anchor the same bytes extract but are marked `unverified` | dropping the `verdict !== 'verified'` mark, i.e. a silent degrade of the trust route |
|
|
514
|
+
| A2 | `resolveElectronBinary` names the real launcher and its exec bit survived | a wrong `platformExe` darwin arm, or a `path.txt` naming a different basename |
|
|
515
|
+
| A3a | all 14 archive symlinks are symlinks on disk with byte-identical targets | deleting the `if (symlink)` branch in `placeEntry` — links become regular files |
|
|
516
|
+
| A3b | the five structural framework links exist with their exact targets | writing the **resolved absolute** path instead of the archive's relative target (still runs on the runner; breaks when the tree moves) |
|
|
517
|
+
| A4 | parity vs the tree `@electron-internal/extract-zip` produced | a flat file mode, or a `continue` that silently drops entries — **report-only on its first cut** |
|
|
518
|
+
| A5 | `Electron --version` runs | the same mutant as A3a, observed through dyld: the launcher's Mach-O carries `LC_RPATH @executable_path/../Frameworks` and `LC_LOAD_DYLIB @rpath/Electron Framework.framework/Electron Framework`, so it loads the ~192 MB framework **through two of the fourteen links** |
|
|
519
|
+
| A6i | an escaping relative target is refused, nothing planted | dropping the `..`-prefix limbs of the three-limb test |
|
|
520
|
+
| A6ii | an **absolute POSIX** target is refused | dropping the `startsWith('..' + sep)` limb — on win32 `/etc/passwd` becomes `C:\etc\passwd` and a *different* limb catches it, so the Windows suite proves the wrong arithmetic |
|
|
521
|
+
| A6iii | SYMLINKCHAIN refused by a **real** `realpath`: 3 links made, no victim | reverting the target resolution to `path.dirname(dest)` — the lexical-dirname bug that extracts with no error at all |
|
|
522
|
+
|
|
523
|
+
A4 is deliberately **report-only, exit 0, full diff printed** on its first cut: nothing has
|
|
524
|
+
ever measured that the two extractors agree on directory modes under the runner's umask, so
|
|
525
|
+
making an unmeasured comparison a blocking gate buys a red for reasons unrelated to
|
|
526
|
+
symlinks. Promote it once one clean run exists. Everything else gates from day one.
|
|
527
|
+
|
|
528
|
+
A6iii is the assertion that matters most: in the jest suite the same archive is pinned by a
|
|
529
|
+
`realpathSync` **the test itself injects** (`tests/electron-custody.test.js`, describe
|
|
530
|
+
`symlinks — the darwin .app shape, which cannot be run here`). That is a rule read off the
|
|
531
|
+
surface its own writer wrote. On the runner the three `.` links exist on disk and the kernel
|
|
532
|
+
answers.
|
|
533
|
+
|
|
534
|
+
### `npm ci` does NOT provision Electron — the job asks for it explicitly
|
|
535
|
+
|
|
536
|
+
`electron@43.1.1` ships **no install script at all**: its `package.json` has no `scripts`
|
|
537
|
+
field (it exposes `install.js` only as the `install-electron` bin), and `package-lock.json`
|
|
538
|
+
carries no `hasInstallScript` for it. So npm never fetches the ~122 MB binary, on any
|
|
539
|
+
platform or any install path. Measured on run `34246117877`: plain `npm ci` on ubuntu,
|
|
540
|
+
macos and windows alike finished in 15-38 s and left amicus's own postinstall reporting
|
|
541
|
+
`the Electron GUI binary is not provisioned yet` — an empty electron cache. The lockfile's
|
|
542
|
+
per-platform installable counts (588 / 588 / 587) match the observed `added N packages`
|
|
543
|
+
exactly **with `electron` included**, so the package is present and only the binary is
|
|
544
|
+
missing.
|
|
545
|
+
|
|
546
|
+
The job therefore runs `node node_modules/electron/install.js` in its own step, with one
|
|
547
|
+
retry. It deliberately does **not** use `AMICUS_PREFETCH_ELECTRON=1`, which
|
|
548
|
+
`scripts/postinstall.js` routes through amicus's own `repairElectron` — the A4 diff would
|
|
549
|
+
then compare amicus against amicus.
|
|
550
|
+
|
|
551
|
+
### What it does not prove
|
|
552
|
+
|
|
553
|
+
- **darwin x64.** `macos-latest` is arm64. The x64 artifact was measured at an identical
|
|
554
|
+
shape (585 / 14 / 310), so the residual is small, but no x64 leg exists. Intel runner
|
|
555
|
+
labels changed during 2025 — check GitHub's current list before adding one.
|
|
556
|
+
- **`mas`.** Unreachable in production; no caller passes `platform: 'mas'`.
|
|
557
|
+
- **A case-sensitive APFS volume.** Runners default to case-insensitive; 0 case-insensitive
|
|
558
|
+
name collisions were measured across the 585 entries, but "low exposure" there is
|
|
559
|
+
inference, not measurement.
|
|
560
|
+
- **Future electron layouts, between bumps.** The job proves the version pinned in
|
|
561
|
+
`package-lock.json` at run time. A bump is caught by the `package-lock.json` path filter on
|
|
562
|
+
`pull_request`/`push`, not by the cron.
|
|
563
|
+
- **The native-rescue hatch.** `AMICUS_ALLOW_UNVERIFIED_ELECTRON=1`, `ditto` and Info-ZIP
|
|
564
|
+
`unzip` symlink behaviour on darwin stay unmeasured — that is the B2 lane, not this one.
|
|
565
|
+
- **`codesign`.** The archive carries **zero** `_CodeSignature` entries, so
|
|
566
|
+
`codesign --verify` on the extracted bundle would assert nothing. Only the embedded ad-hoc
|
|
567
|
+
Mach-O signature exists, and A5 succeeding is the only evidence it survived byte-exact
|
|
568
|
+
extraction. Do not add a codesign step and call it coverage.
|
|
569
|
+
- **Destination-failure classification on darwin** (ENOSPC, read-only `dist/`, EACCES) and
|
|
570
|
+
**`promoteDist` on APFS** — the job calls the promote once, on a happy path.
|
|
571
|
+
- **A trailing-slash symlink entry.** An entry whose *name* ends with `/` while its mode bits
|
|
572
|
+
say `IFLNK` is turned into a real directory before the symlink branch is reached, so the
|
|
573
|
+
escape check never runs. The real artifact has zero such entries, so no darwin job will
|
|
574
|
+
ever exercise it; it belongs in the platform-independent suite.
|
|
575
|
+
|
|
576
|
+
### Triggers, cost, and the required-check caveat
|
|
577
|
+
|
|
578
|
+
Paths-filtered `pull_request` **and** `push: [main]` (so a bump is proven at merge time),
|
|
579
|
+
plus a weekly cron and `workflow_dispatch`. The cron re-proves the pinned version against the
|
|
580
|
+
live release asset and the current runner image — the two inputs no path filter can see — and
|
|
581
|
+
is the weakest trigger on purpose: a schedule GitHub delays or drops is silent.
|
|
582
|
+
|
|
583
|
+
The job owns a ~122 MB download plus two ~600 MB extractions on a 3-vCPU / 8 GB runner;
|
|
584
|
+
budget 4-6 minutes. It is not free, and macOS *concurrency* rather than minutes is the
|
|
585
|
+
binding constraint on a public repo — `ci.yml` already burns two macOS legs per push.
|
|
586
|
+
|
|
587
|
+
Because both event triggers carry a `paths:` filter, the job reports **skipped** when nothing
|
|
588
|
+
matches, so **it cannot be a required status check as written**. Making it required means
|
|
589
|
+
dropping `paths:` and moving the guard inside the job (a `git diff --name-only` early exit) —
|
|
590
|
+
a pattern this repo does not currently use.
|
|
591
|
+
|
|
592
|
+
### Running it by hand on a Mac
|
|
593
|
+
|
|
594
|
+
```bash
|
|
595
|
+
npm ci --foreground-scripts
|
|
596
|
+
rm -rf node_modules/electron/dist node_modules/electron/path.txt
|
|
597
|
+
node node_modules/electron/install.js # the artifact + the A4 reference tree
|
|
598
|
+
node scripts/probe-darwin-extract.js --preflight
|
|
599
|
+
node scripts/probe-darwin-extract.js
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
The `rm -rf` is not cosmetic: `install.js` short-circuits on a populated `dist/`, and on a dev
|
|
603
|
+
Mac that `dist/` may well be amicus's own self-heal output — which would make A4 compare
|
|
604
|
+
amicus against amicus. The workflow does the same removal for the same reason.
|
package/docs/troubleshooting.md
CHANGED
|
@@ -452,10 +452,13 @@ That is what a swapped mirror or a planted cache file looks like. It is **also**
|
|
|
452
452
|
|
|
453
453
|
**Fix, in order:**
|
|
454
454
|
|
|
455
|
-
- **Online, do nothing.** Amicus downloads the artifact again with the digest pinned. A truncated download
|
|
455
|
+
- **Online, do nothing.** Amicus downloads the artifact again with the digest pinned. A truncated download
|
|
456
|
+
heals itself. The exception is an archive amicus refuses outright for path traversal — that is terminal
|
|
457
|
+
by design and is not re-fetched, because the problem is what the archive *contains*, not that it arrived
|
|
458
|
+
incomplete.
|
|
456
459
|
- **Air-gapped, re-copy first.** Copy the cache directory again from the machine that downloaded it. A partial copy is the usual cause, and a fresh copy costs you nothing.
|
|
457
460
|
- **Only if you cannot obtain another copy: the native-extractor rescue.** Set `AMICUS_ALLOW_UNVERIFIED_ELECTRON=1` and provision again. You do not have to have set it in advance: the run that printed the message above left the archive in place precisely so this one has something to work on. Amicus then writes the bytes it hashed to a path inside the Electron package and hands that path to your OS's own extractor (`tar` / `Expand-Archive` / `ditto` / `unzip`) — the same tools that handled the archive before amicus extracted in memory at all. **This is not a safe operation, and it is not described as one.** Between amicus writing the file and the child process opening it, anything running as your user can substitute it, and what that child extracts is promoted into `dist/` without being hashed again. Amicus prints the whole trade on stderr before it spawns anything, and the result is reported `unverified` even when the artifact's own sha256 matched. Unset the variable afterwards: it also downgrades a digest-mismatch refusal to a warning ([configuration.md](./configuration.md#gui-and-debug)).
|
|
458
|
-
- **What the rescue will *not* do,** whatever this variable is set to: retry an archive refused for path traversal (`REFUSED (unsafe archive)` — terminal by design), retry an extraction that *stalled* (the timeout exists to stop work, not to hand it to a child process), rescue bytes that contradict the published digest (they are known wrong), or paper over a full or unwritable disk. Each of those says something different from "this archive cannot be read", and only the last of those is a rescue amicus was given. Before it hands anything over, amicus also reads the archive's *entry names* and refuses any that would write outside the destination — because an archive can break the extractor early enough that its own traversal check never ran.
|
|
461
|
+
- **What the rescue will *not* do,** whatever this variable is set to: retry an archive refused for path traversal (`REFUSED (unsafe archive)` — terminal by design), retry an extraction that *stalled* (the timeout exists to stop work, not to hand it to a child process), rescue bytes that contradict the published digest (they are known wrong), or paper over a full or unwritable disk. Each of those says something different from "this archive cannot be read", and only the last of those is a rescue amicus was given. Before it hands anything over, amicus also reads the archive's *entry names* and refuses any that would write outside the destination — because an archive can break the extractor early enough that its own traversal check never ran. It reads them in **both** tables a zip declares them in, so cutting the tail off an archive no longer hides its names, and the two tables disagreeing does not let one through. It still cannot see a symlink target, and an archive that defeats both walks still reaches the extractor — the notice printed before the spawn says which names were checked; [configuration.md](./configuration.md#gui-and-debug) states every residual and names which extractors were measured to refuse a `..` entry themselves.
|
|
459
462
|
- Headless runs and the full council work without the GUI throughout.
|
|
460
463
|
|
|
461
464
|
---
|
|
@@ -514,11 +517,15 @@ The third line names which way the read failed:
|
|
|
514
517
|
guaranteed either way: a half-written tree is never what `dist/` contains, and a kill mid-extract
|
|
515
518
|
leaves the previous `dist/` exactly where it was.
|
|
516
519
|
- **A promote never removes a working `dist/` to make room.** If the old tree cannot be renamed out of
|
|
517
|
-
the way (a handle held on it, or an AV filter denying the move) and it holds a usable executable
|
|
518
|
-
the
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
520
|
+
the way (a handle held on it, or an AV filter denying the move) and it holds a usable executable —
|
|
521
|
+
the one `path.txt` names, or this platform's default when `path.txt` is absent, unreadable or blank
|
|
522
|
+
— the repair refuses and leaves it untouched rather than deleting it with no way back. That covers
|
|
523
|
+
a package cross-installed for another platform through `npm_config_platform`, whose `path.txt` names
|
|
524
|
+
an executable this platform never looks for; through v4.9.6 the guard asked only about this
|
|
525
|
+
platform's default name, so such a tree was read as "not an install" and a promote that FAILED
|
|
526
|
+
deleted it. If it holds neither, it is not an install, and it is replaced. In the one case where the
|
|
527
|
+
tree was renamed away and neither the swap nor the rollback could run, the previous `dist/` is
|
|
528
|
+
intact at `.amicus-retired-<hex>` and the error names it — rename it back to `dist/` to restore it.
|
|
522
529
|
- **A related refusal**, `Refusing to provision electron: … is not a usable artifact name`, means the
|
|
523
530
|
`version` in the Electron package's own `package.json` is not a plausible version string. Amicus
|
|
524
531
|
builds the artifact filename from it and refuses to use anything that is not a plain filename, since
|
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**
|
|
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
|
|
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.
|
|
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** |
|
|
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.
|
|
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)
|