amicus 4.9.7 → 4.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +125 -0
- package/README.md +2 -1
- package/bin/amicus.js +5 -0
- package/docs/ROADMAP.md +33 -5
- package/docs/architecture-map.md +41 -6
- package/docs/configuration.md +14 -8
- package/docs/council.md +140 -3
- package/docs/usage.md +29 -6
- package/electron/ipc-setup.js +6 -9
- package/electron/setup-ui-alias-groups.js +29 -124
- 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-handlers.js +8 -1
- package/src/cli.js +34 -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/aliases-review-gate.js +65 -0
- package/src/sidecar/aliases-review-prompt.js +91 -0
- package/src/sidecar/aliases-review-render.js +116 -0
- package/src/sidecar/aliases-review.js +298 -0
- package/src/sidecar/aliases.js +279 -0
- package/src/sidecar/fanout.js +7 -1
- package/src/sidecar/heartbeat.js +46 -0
- package/src/sidecar/models.js +20 -7
- package/src/sidecar/session-utils.js +7 -34
- package/src/sidecar/setup.js +20 -18
- package/src/utils/agent-mapping.js +1 -1
- package/src/utils/alias-groups.js +128 -0
- package/src/utils/alias-proposals.js +151 -0
- package/src/utils/alias-resolver.js +1 -1
- package/src/utils/alias-state.js +88 -0
- package/src/utils/alias-store.js +65 -0
- package/src/utils/config.js +10 -5
- package/src/utils/degrade.js +8 -0
- package/src/utils/model-id-siblings.js +106 -0
- package/src/utils/model-validator.js +1 -1
- package/src/utils/quick-picks.js +13 -32
- package/src/utils/text-sanitize.js +27 -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/usage.md
CHANGED
|
@@ -21,6 +21,8 @@ amicus abort --all [--json]
|
|
|
21
21
|
# Setup & maintenance
|
|
22
22
|
amicus setup # Full wizard: keys, default model, aliases
|
|
23
23
|
amicus setup --api-keys # Open just the API-key step
|
|
24
|
+
amicus aliases # your aliases: following (shipped pin) / pinned (yours)
|
|
25
|
+
amicus aliases --review # walk the proposals — no copy-paste
|
|
24
26
|
amicus setup --add-alias fast=google/gemini-3.1-flash-lite-preview # bare canonical, direct-first
|
|
25
27
|
amicus models # List the live catalog
|
|
26
28
|
amicus models --search gemini # Filter by substring
|
|
@@ -98,7 +100,7 @@ amicus start --model deepseek --prompt "Generate tests" --no-ui --timeout 30
|
|
|
98
100
|
| `--var <k=v>` | Set `{{var.<key>}}`; repeatable. Requires `--template`. | |
|
|
99
101
|
| `--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
102
|
|
|
101
|
-
> Agents: **Chat** auto-approves reads and asks before writes/bash (interactive default); **Build** has full tool access (headless default); **Plan**
|
|
103
|
+
> 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
104
|
|
|
103
105
|
**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
106
|
|
|
@@ -204,6 +206,8 @@ amicus council run --prompt-file briefing.md --models gemini,glm --chair deepsee
|
|
|
204
206
|
| `--var <k=v>` | Set `{{var.<key>}}`; repeatable. Requires `--template`. |
|
|
205
207
|
| `--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
208
|
| `--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). |
|
|
209
|
+
| `--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). |
|
|
210
|
+
| `--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
211
|
|
|
208
212
|
**Exit codes:** `0` full run · `2` degraded but reportable (fewer than 2 judges, chair failure —
|
|
209
213
|
`overallVerdict: null` — a cost ceiling hit after the tally, or a `--max-cost` ceiling set over a
|
|
@@ -303,7 +307,7 @@ Then invoke it with `--pack <name|path>` on `start` / `fanout` / `council run`
|
|
|
303
307
|
| `fanout` | `bench` (a saved council name, or an array of ≥2 members) | — | `timeout`, `maxCost`, `gateway`, `agent`, `thinking`, `summaryLength`, `noContext`, `contextTurns`, `contextMaxTokens` |
|
|
304
308
|
| `solo` | `model` | — | `timeout`, `maxCost`, `gateway`, `agent`, `thinking`, `summaryLength`, `noUi`, `noContext`, `contextTurns`, `contextMaxTokens` |
|
|
305
309
|
|
|
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
|
|
310
|
+
`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
311
|
|
|
308
312
|
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
313
|
|
|
@@ -395,7 +399,24 @@ direct namespace while the OpenRouter route still serves — a gateway-only rout
|
|
|
395
399
|
no direct sibling is a routing choice, not staleness. Deliberately gateway-only
|
|
396
400
|
entries (e.g. `gpt-pro`) are annotated as such and are never offered a retarget.
|
|
397
401
|
|
|
398
|
-
|
|
402
|
+
### Aliases: following vs pinned
|
|
403
|
+
|
|
404
|
+
A curated alias (`gemini`, `gpt`, `opus`, `deepseek`, …) absent from `config.aliases` **follows** the pin Amicus ships and moves to the new target on every release; a present key **pins** it to that exact id and makes it reviewable.
|
|
405
|
+
|
|
406
|
+
```bash
|
|
407
|
+
amicus aliases # every alias: following (shipped pin) / pinned (yours), by vendor
|
|
408
|
+
amicus aliases --review # walk outdated pins in a numbered picker — no copy-paste
|
|
409
|
+
amicus aliases --unpin <name> # remove a pin: a curated name goes back to following, a custom name is deleted
|
|
410
|
+
amicus aliases --json # the same document, for scripts
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
`--review` offers, per outdated pin: accept a newer same-tier sibling, follow the shipped pin again, choose another catalog id, skip, or never ask again for that pairing — it needs a terminal (without one it prints the list and exits 1). Picking "follow the shipped pin" removes the key from `config.aliases`, and typing that SAME shipped id into "choose another" follows too — unconditionally, whether or not the catalog carries it or is fresh, since it is the same `follow` action either way. A newer same-tier sibling is judged by vendor path, prefix and suffix around the version number; a size or variant token glued to a number (`20b`, `8x22b`) is never read as part of that version, so a differently-sized variant of a model is never offered as a "newer" sibling of it. `amicus setup` no longer seeds all 21 curated ids — it pins only the default alias you chose, and only when its live flagship differs from the shipped pin, and says so.
|
|
414
|
+
|
|
415
|
+
Accepting a catalog-vouched id needs a catalog fresher than 24 hours — the picker refreshes it inline first (an authenticated model-list call to each provider you hold a key for, the same call `amicus models --refresh` makes); `follow` never needs the catalog.
|
|
416
|
+
|
|
417
|
+
`--unpin <name>` refuses instead of writing when `name` is also your `config.default` and is not a curated alias — deleting it would leave the default pointing at a key that no longer resolves, so pick a new default first (`amicus setup`) or unpin a different alias. A curated default is unaffected: it keeps resolving from the shipped table after the unpin, same as any other curated unpin.
|
|
418
|
+
|
|
419
|
+
**Drifted aliases.** `--check` (and the `doctor` aliases row) also flags **`DRIFTED:`** stored aliases — a stored alias whose target is still catalog-listed but no longer matches any route its family currently resolves to (the v4.6.1 `gemini` release-gate class, where `doctor` stayed green while the model behind it had moved on). Each drift line points at `amicus aliases --review`. Drift is informational only by default and does not change the exit code — pass `--strict` alongside `--check` to make curated per-gateway drift (stale or divergent direct/OpenRouter forms) exit non-zero too.
|
|
399
420
|
|
|
400
421
|
**Live probe (`--check --live`).** Presence in the catalog is not proof of service — a stored alias can point at a model id the catalog still lists but the provider has quietly stopped serving (the v4.6.1 `gemini` incident). `--check` alone can't see that; `--live` can, by actually asking. Scope is **stored aliases only** (`amicus setup --add-alias`) — curated defaults follow the catalog by construction and have no "was it actually served" question for a live probe to answer. **This spends real money — one tiny leg per stored alias** — every probed alias gets one ordinary engine leg on a single quiet fan-out wave, with a real session dir and a real spend-ledger row, exactly as if you'd run it yourself.
|
|
401
422
|
|
|
@@ -415,7 +436,7 @@ Each stored alias resolves to one of three outcomes:
|
|
|
415
436
|
|
|
416
437
|
**Validation on launch.** `start` and `fanout` validate the model against the catalog before launching. For an explicit `--model` on `continue`/`resume` this is **blocking** (a typo'd model fails fast with suggestions); for a model *inherited* from a prior session it's **advisory**. Skip it any time with `--no-validate-model`, or fix the catalog with `amicus models --refresh`.
|
|
417
438
|
|
|
418
|
-
**
|
|
439
|
+
**Curated aliases follow the shipped pins unless you pin them.** The curated aliases (`gemini`, `gpt`, `opus`, …) are shipped and follow the package's pins unless you pin them — pin one, or add an entirely new one, with `amicus setup --add-alias name=provider/model`. To see exactly what resolves on *your* machine, run `amicus aliases` — that is the source of truth.
|
|
419
440
|
|
|
420
441
|
**Full-id passthrough.** You can always bypass aliases and name a model directly. Bare `provider/model` is the canonical, policy-routed form; `openrouter/provider/model` is an explicit override. See [Routing](../README.md#routing) for the full explanation — summary:
|
|
421
442
|
|
|
@@ -540,7 +561,7 @@ $ amicus status demo123 --json
|
|
|
540
561
|
"taskId": "demo123",
|
|
541
562
|
"status": "complete",
|
|
542
563
|
"elapsed": "5m 0s",
|
|
543
|
-
"version": "4.
|
|
564
|
+
"version": "4.10.0",
|
|
544
565
|
"model": "google/gemini-2.5-flash",
|
|
545
566
|
"phase": "terminal"
|
|
546
567
|
}
|
|
@@ -970,6 +991,8 @@ Every tool below also takes an optional `project` — an absolute path naming th
|
|
|
970
991
|
- `debate` — add a Stage-2.5 rebuttal round before the chair synthesizes.
|
|
971
992
|
- `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
993
|
- `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).
|
|
994
|
+
- `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`.
|
|
995
|
+
- `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
996
|
- `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
997
|
- `onComplete`, `pack`, `tag` — as on `amicus_fanout`.
|
|
975
998
|
|
|
@@ -987,7 +1010,7 @@ The `--agent` option controls which OpenCode agent drives the session:
|
|
|
987
1010
|
|-------|-------------|-------------|
|
|
988
1011
|
| **Chat** | Interactive conversation | Reads freely, asks before writes/bash |
|
|
989
1012
|
| **Build** | Full-access primary agent (headless default) | Read, write, bash, task |
|
|
990
|
-
| **Plan** |
|
|
1013
|
+
| **Plan** | Analysis without edits | Read, search, bash; no edits |
|
|
991
1014
|
|
|
992
1015
|
`--agent Chat` is interactive-only and incompatible with `--no-ui`. Custom agents defined in `~/.config/opencode/agents/` or `.opencode/agents/` are also supported.
|
|
993
1016
|
|
package/electron/ipc-setup.js
CHANGED
|
@@ -176,21 +176,18 @@ function registerSetupHandlers(getMainWindow, { ipcMain = require('electron').ip
|
|
|
176
176
|
});
|
|
177
177
|
|
|
178
178
|
// Read-modify-write: never rewrite an alias the renderer didn't send.
|
|
179
|
-
// aliasWrites values: string = set, null = delete. First run
|
|
179
|
+
// aliasWrites values: string = set, null = delete. First run starts from
|
|
180
|
+
// an empty alias map (issue 238 Q9).
|
|
180
181
|
// councilPicks (optional): when length >= 2, seeds the free council via seedFreeCouncil.
|
|
181
182
|
ipcMain.handle('sidecar:save-config', async (_event, defaultModel, aliasWrites, councilPicks) => {
|
|
182
183
|
try {
|
|
183
184
|
const { loadConfig, saveConfig } = require('../src/utils/config');
|
|
184
185
|
let cfg = loadConfig();
|
|
185
186
|
if (!cfg) {
|
|
186
|
-
|
|
187
|
-
//
|
|
188
|
-
//
|
|
189
|
-
|
|
190
|
-
try {
|
|
191
|
-
catalogInfo = await require('../src/utils/model-catalog').getCatalogInfo();
|
|
192
|
-
} catch (_err) { /* offline: pinned seeds */ }
|
|
193
|
-
cfg = { aliases: toLiveSeedAliases(catalogInfo) };
|
|
187
|
+
// issue 238 Q9: nothing is seeded — a curated alias follows the
|
|
188
|
+
// shipped pin by being absent (D1). Only the renderer's explicit
|
|
189
|
+
// writes land.
|
|
190
|
+
cfg = { aliases: {} };
|
|
194
191
|
}
|
|
195
192
|
if (!cfg.aliases) { cfg.aliases = {}; }
|
|
196
193
|
if (defaultModel) { cfg.default = defaultModel; }
|
|
@@ -1,138 +1,42 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Setup UI - Alias grouping rule (issue 213)
|
|
3
3
|
*
|
|
4
|
-
* The
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* The grouping rule itself (groupAliases / aliasVendorOf / vendorLabel /
|
|
5
|
+
* titleCaseVendor / PREFERRED_VENDOR_ORDER) now lives in
|
|
6
|
+
* src/utils/alias-groups.js (issue 238 PR1 fix wave F5): `src/sidecar/aliases.js`
|
|
7
|
+
* (the CLI `amicus aliases` list) needed the same grouping, and `src/`
|
|
8
|
+
* requiring from `electron/` is a layering violation the whole-branch review
|
|
9
|
+
* caught. This file re-exports exactly those five symbols -- groupAliases,
|
|
10
|
+
* aliasVendorOf, vendorLabel, titleCaseVendor, PREFERRED_VENDOR_ORDER -- so
|
|
11
|
+
* electron/setup-ui-aliases.js, electron/setup-ui-alias-script.js and every
|
|
12
|
+
* existing test keep working untouched, and keeps its own genuinely UI-only
|
|
13
|
+
* export (NEW_ROUTES_GROUP_LABEL) defined here. `ALIAS_VENDOR_LABELS` (the
|
|
14
|
+
* vendor -> display-name table `vendorLabel` looks up) stayed
|
|
15
|
+
* module-private to alias-groups.js -- it is not one of the five and
|
|
16
|
+
* nothing imports it directly (issue 249 review round 1, finding D2).
|
|
8
17
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* .eslintrc.js. `groupModelsByFamily` (src/utils/model-fetcher.js) is
|
|
16
|
-
* deliberately NOT reused: it keys on `id.split('/')[0]`, so every
|
|
17
|
-
* `openrouter/...` alias would collapse into a single "OpenRouter" bucket —
|
|
18
|
-
* exactly the grouping this file exists to avoid. Its DISPLAY half
|
|
19
|
-
* (PROVIDER_FAMILY_NAMES) is reused below.
|
|
20
|
-
*
|
|
21
|
-
* SHARED-WITH-THE-BROWSER NOTE — deliberately NOT shared. The wizard's inline
|
|
22
|
-
* script cannot `require`, so the browser could only get this rule as a copy:
|
|
23
|
-
* hand-written (silent divergence — a 3-segment direct id like `a/b/c` already
|
|
24
|
-
* splits differently under the two obvious spellings) or serialised from the
|
|
25
|
-
* source below (which would put `slice('openrouter/'.length)` back into the
|
|
18
|
+
* SHARED-WITH-THE-BROWSER NOTE (still applies to the grouping rule, wherever
|
|
19
|
+
* it lives) — deliberately NOT shared. The wizard's inline script cannot
|
|
20
|
+
* `require`, so the browser could only get this rule as a copy: hand-written
|
|
21
|
+
* (silent divergence — a 3-segment direct id like `a/b/c` already splits
|
|
22
|
+
* differently under the two obvious spellings) or serialised from the
|
|
23
|
+
* source (which would put `slice('openrouter/'.length)` back into the
|
|
26
24
|
* page). The page carrying its own gateway-prefix strip is the exact shape
|
|
27
25
|
* issue 214 removed and that tests/setup-ui.test.js still guards
|
|
28
26
|
* ("ships no routing policy to the page: ... no prefix derivation"), because
|
|
29
27
|
* that copy is how a direct id gets fabricated for a namespace that never
|
|
30
28
|
* served it.
|
|
31
29
|
*
|
|
32
|
-
* So there is ONE grouping rule and it lives
|
|
33
|
-
* (setup-ui-alias-script.js) never
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const { vendorOf } = require('../src/sidecar/fallback-chains');
|
|
39
|
-
const { PROVIDER_FAMILY_NAMES, listDirectProviders } = require('../src/utils/provider-registry');
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Display names for vendors seen in alias routes.
|
|
43
|
-
*
|
|
44
|
-
* DISPLAY ONLY — deliberately not folded into provider-registry's PROVIDERS,
|
|
45
|
-
* which is a *capability* registry (env var, direct-vs-gateway, live fetch).
|
|
46
|
-
* KNOWN_PROVIDERS / PROVIDER_ENV_MAP are derived from that list, so adding
|
|
47
|
-
* `z-ai` there would claim Amicus can hold a z-ai API key. The five real
|
|
48
|
-
* providers keep their single source of truth via PROVIDER_FAMILY_NAMES.
|
|
49
|
-
*/
|
|
50
|
-
const ALIAS_VENDOR_LABELS = {
|
|
51
|
-
...PROVIDER_FAMILY_NAMES,
|
|
52
|
-
// Vendors reachable through the gateway (curated + commonly pinned)
|
|
53
|
-
'qwen': 'Qwen',
|
|
54
|
-
'mistralai': 'Mistral AI',
|
|
55
|
-
'z-ai': 'Z.AI',
|
|
56
|
-
'minimax': 'MiniMax',
|
|
57
|
-
'x-ai': 'xAI',
|
|
58
|
-
'moonshotai': 'Moonshot AI',
|
|
59
|
-
'bytedance-seed': 'ByteDance Seed',
|
|
60
|
-
'thinkingmachines': 'Thinking Machines',
|
|
61
|
-
'cognitivecomputations': 'Cognitive Computations',
|
|
62
|
-
'inclusionai': 'InclusionAI',
|
|
63
|
-
'nvidia': 'NVIDIA',
|
|
64
|
-
'cohere': 'Cohere',
|
|
65
|
-
'meta-llama': 'Meta Llama',
|
|
66
|
-
'nousresearch': 'Nous Research',
|
|
67
|
-
'perplexity': 'Perplexity',
|
|
68
|
-
'microsoft': 'Microsoft',
|
|
69
|
-
'ai21': 'AI21',
|
|
70
|
-
'amazon': 'Amazon',
|
|
71
|
-
// Local providers (src/utils/local-providers.js PRESETS / VALID_FLAVORS)
|
|
72
|
-
'ollama': 'Ollama',
|
|
73
|
-
'lmstudio': 'LM Studio',
|
|
74
|
-
'vllm': 'vLLM',
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
/** `some-new-vendor` -> `Some New Vendor`, so an unmapped vendor is not a raw slug. */
|
|
78
|
-
function titleCaseVendor(vendor) {
|
|
79
|
-
return String(vendor).split(/[-_]/).filter(Boolean)
|
|
80
|
-
.map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Vendor key for an alias route. Wraps the shared `vendorOf` with the two
|
|
85
|
-
* normalisations issue 213 flagged: case, and the leading `~` of a floating
|
|
86
|
-
* OpenRouter id (`openrouter/~z-ai/glm-latest` must not form a second group
|
|
87
|
-
* next to `z-ai`).
|
|
88
|
-
* @param {string} route @returns {string} '' when there is no usable route
|
|
30
|
+
* So there is ONE grouping rule and it lives server-side (now in
|
|
31
|
+
* src/utils/alias-groups.js). The client (setup-ui-alias-script.js) never
|
|
32
|
+
* derives a vendor: a route added during the session goes into its own
|
|
33
|
+
* clearly-labelled "New routes" group, and vendor filing happens when the
|
|
34
|
+
* server next renders the editor.
|
|
89
35
|
*/
|
|
90
|
-
function aliasVendorOf(route) {
|
|
91
|
-
const v = vendorOf(route).toLowerCase();
|
|
92
|
-
return v.charAt(0) === '~' ? v.slice(1) : v;
|
|
93
|
-
}
|
|
94
36
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
* route, and `__proto__`/`constructor` would otherwise return prototype junk.
|
|
99
|
-
* @param {string} vendor @returns {string}
|
|
100
|
-
*/
|
|
101
|
-
function vendorLabel(vendor) {
|
|
102
|
-
if (!vendor) { return 'Other'; }
|
|
103
|
-
const hit = Object.prototype.hasOwnProperty.call(ALIAS_VENDOR_LABELS, vendor)
|
|
104
|
-
? ALIAS_VENDOR_LABELS[vendor] : null;
|
|
105
|
-
return hit || titleCaseVendor(vendor);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/** Direct-route vendors render first; everything else sorts by label. */
|
|
109
|
-
const PREFERRED_VENDOR_ORDER = listDirectProviders();
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Bucket an alias map by route vendor.
|
|
113
|
-
* INVARIANT: every own key of `aliases` lands in exactly one returned group —
|
|
114
|
-
* there is no whitelist to miss, and the empty vendor is a real catch-all.
|
|
115
|
-
* Order within a group follows the config's own key order.
|
|
116
|
-
* @param {Object<string,string>} aliases
|
|
117
|
-
* @returns {Array<{vendor: string, label: string, keys: string[]}>}
|
|
118
|
-
*/
|
|
119
|
-
function groupAliases(aliases) {
|
|
120
|
-
const byVendor = new Map();
|
|
121
|
-
for (const key of Object.keys(aliases || {})) {
|
|
122
|
-
const vendor = aliasVendorOf(aliases[key]);
|
|
123
|
-
if (!byVendor.has(vendor)) { byVendor.set(vendor, []); }
|
|
124
|
-
byVendor.get(vendor).push(key);
|
|
125
|
-
}
|
|
126
|
-
const rank = (vendor) => {
|
|
127
|
-
if (!vendor) { return Number.MAX_SAFE_INTEGER; } // catch-all group last
|
|
128
|
-
const i = PREFERRED_VENDOR_ORDER.indexOf(vendor);
|
|
129
|
-
return i === -1 ? PREFERRED_VENDOR_ORDER.length : i;
|
|
130
|
-
};
|
|
131
|
-
return Array.from(byVendor.entries())
|
|
132
|
-
.map(([vendor, keys]) => ({ vendor, label: vendorLabel(vendor), keys }))
|
|
133
|
-
.sort((a, b) => rank(a.vendor) - rank(b.vendor) ||
|
|
134
|
-
a.label.toLowerCase().localeCompare(b.label.toLowerCase()));
|
|
135
|
-
}
|
|
37
|
+
const {
|
|
38
|
+
groupAliases, aliasVendorOf, vendorLabel, titleCaseVendor, PREFERRED_VENDOR_ORDER,
|
|
39
|
+
} = require('../src/utils/alias-groups');
|
|
136
40
|
|
|
137
41
|
/**
|
|
138
42
|
* Heading for the client-side group that holds routes added during THIS
|
|
@@ -153,9 +57,10 @@ function groupAliases(aliases) {
|
|
|
153
57
|
const NEW_ROUTES_GROUP_LABEL = 'New routes (this session)';
|
|
154
58
|
|
|
155
59
|
module.exports = {
|
|
156
|
-
ALIAS_VENDOR_LABELS,
|
|
157
60
|
NEW_ROUTES_GROUP_LABEL,
|
|
158
61
|
aliasVendorOf,
|
|
159
62
|
vendorLabel,
|
|
160
63
|
groupAliases,
|
|
64
|
+
titleCaseVendor,
|
|
65
|
+
PREFERRED_VENDOR_ORDER,
|
|
161
66
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "amicus",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.10.0",
|
|
4
4
|
"mcpName": "io.github.BourbonDog/amicus",
|
|
5
5
|
"description": "Multi-model LLM Council + parallel AI window for Claude Code. Run structured council reviews across Gemini, GPT, DeepSeek and more — or fork a conversation to any model and fold the results back.",
|
|
6
6
|
"keywords": [
|
|
@@ -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)
|