amicus 4.6.2 → 4.7.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 +349 -0
- package/README.md +24 -13
- package/bin/amicus.js +31 -0
- package/docs/ROADMAP.md +172 -36
- package/docs/configuration.md +56 -6
- package/docs/council.md +63 -10
- package/docs/doc-system.md +8 -7
- package/docs/schemas.md +10 -1
- package/docs/troubleshooting.md +27 -1
- package/docs/usage.md +68 -15
- package/electron/workspace-ui/index.html +3 -0
- package/electron/workspace-ui/live-model.js +132 -21
- package/electron/workspace-ui/workspace-app.js +20 -4
- package/electron/workspace-ui/workspace-lazy.js +233 -0
- package/electron/workspace-ui/workspace-matrix.js +12 -1
- package/electron/workspace-ui/workspace-panels.js +24 -171
- package/electron/workspace-ui/workspace-render.js +15 -5
- package/electron/workspace-ui/workspace-seats.js +88 -5
- package/electron/workspace-ui/workspace-verbs.js +1 -1
- package/electron/workspace-ui/workspace.css +6 -0
- package/package.json +5 -2
- package/schemas/council-run.schema.json +1 -0
- package/schemas/council-stats.schema.json +9 -1
- package/schemas/run.schema.json +2 -1
- package/schemas/spend.schema.json +1 -1
- package/schemas/wave.schema.json +2 -1
- package/skills/second-opinion/MANUAL-ORCHESTRATION.md +12 -0
- package/skills/second-opinion/MODEL-NOTES.md +5 -4
- package/skills/sidecar/SKILL.md +7 -2
- package/src/cli-council-run-bench.js +86 -0
- package/src/cli-handlers-council-run.js +65 -81
- package/src/cli-handlers-council.js +24 -3
- package/src/cli-handlers-doctor.js +9 -3
- package/src/cli-handlers-fanout.js +179 -0
- package/src/cli-handlers-pack.js +24 -10
- package/src/cli-handlers-run.js +19 -161
- package/src/cli-template-args.js +48 -0
- package/src/cli.js +39 -46
- package/src/council/debate.js +89 -10
- package/src/council/ledger.js +72 -11
- package/src/council/presets-cli.js +6 -2
- package/src/council/report.js +17 -6
- package/src/council/run-assemble.js +15 -3
- package/src/council/run-budget.js +2 -2
- package/src/council/run-chair.js +70 -11
- package/src/council/run-debate.js +51 -67
- package/src/council/run-launch.js +9 -2
- package/src/council/run-retry.js +4 -1
- package/src/council/run-stage1-launch.js +94 -0
- package/src/council/run-stage2.js +25 -4
- package/src/council/run-stages.js +79 -86
- package/src/council/run-state.js +10 -2
- package/src/council/run.js +26 -2
- package/src/council/tally.js +6 -2
- package/src/mcp-council-awareness.js +1 -0
- package/src/mcp-council-bench.js +4 -0
- package/src/mcp-council-run.js +10 -0
- package/src/mcp-server.js +114 -54
- package/src/mcp-tools.js +12 -5
- package/src/pack/pack-cli.js +1 -1
- package/src/pack/pack-forward.js +12 -4
- package/src/pack/pack-resolve.js +3 -0
- package/src/pack/pack-store.js +20 -3
- package/src/pack/pack-validate.js +5 -1
- package/src/session-manager.js +6 -2
- package/src/sidecar/budget.js +38 -4
- package/src/sidecar/fanout-budget.js +1 -2
- package/src/sidecar/fanout-leg-fallback.js +7 -3
- package/src/sidecar/fanout-wave-io.js +13 -1
- package/src/sidecar/fanout.js +11 -9
- package/src/sidecar/list-limit.js +50 -0
- package/src/sidecar/list-search.js +69 -0
- package/src/sidecar/read.js +90 -5
- package/src/sidecar/start-metadata.js +58 -0
- package/src/sidecar/start.js +8 -43
- package/src/sidecar/workspace-auto-open.js +2 -2
- package/src/spend-query.js +2 -1
- package/src/template/apply.js +7 -4
- package/src/template/render.js +6 -2
- package/src/template/store.js +1 -1
- package/src/utils/alias-audit.js +19 -0
- package/src/utils/cli-preflight.js +27 -1
- package/src/utils/config.js +15 -0
- package/src/utils/curated-models.js +43 -7
- package/src/utils/gateway-route-audit.js +16 -3
- package/src/utils/model-fetcher.js +8 -6
- package/src/utils/remediation-hints.js +14 -0
- package/src/utils/result-schema-rebuild.js +1 -0
- package/src/utils/result-schema.js +6 -1
- package/src/utils/session-index-tmp-sweep.js +18 -3
- package/src/utils/session-index.js +1 -0
- package/src/utils/session-metadata-tmp-sweep.js +156 -0
- package/src/utils/spend-ledger.js +11 -4
- package/src/utils/validators.js +16 -0
package/docs/usage.md
CHANGED
|
@@ -9,7 +9,7 @@ The `am` alias is interchangeable with `amicus` everywhere.
|
|
|
9
9
|
amicus start --model <model> --prompt "<task>"
|
|
10
10
|
amicus start --model <model> --prompt-file briefing.md --no-ui --json
|
|
11
11
|
amicus fanout --models "gemini,deepseek,gpt" --prompt "Review this" --json
|
|
12
|
-
amicus list [--status <filter>] [--all] [--json]
|
|
12
|
+
amicus list [--status <filter>] [--all] [--search <q>] [--limit <n>] [--json]
|
|
13
13
|
amicus resume <task_id> [--no-ui --json]
|
|
14
14
|
amicus continue <task_id> --prompt "Next step..." [--no-ui --json]
|
|
15
15
|
amicus read <task_id> [--conversation|--metadata|--json]
|
|
@@ -96,6 +96,7 @@ amicus start --model deepseek --prompt "Generate tests" --no-ui --timeout 30
|
|
|
96
96
|
| `--template <name\|path>` | Render a [briefing template](#briefing-templates) (`{{prompt}}`, `{{artifact}}`, `{{artifact_path}}`, `{{date}}`, `{{project}}`, `{{var.*}}`). | |
|
|
97
97
|
| `--artifact <file>` | File whose content fills `{{artifact}}`/`{{artifact_path}}` (256 KB cap). Requires `--template`. | |
|
|
98
98
|
| `--var <k=v>` | Set `{{var.<key>}}`; repeatable. Requires `--template`. | |
|
|
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)* |
|
|
99
100
|
|
|
100
101
|
> Agents: **Chat** auto-approves reads and asks before writes/bash (interactive default); **Build** has full tool access (headless default); **Plan** is read-only analysis. `--agent Chat` is interactive-only and incompatible with `--no-ui`.
|
|
101
102
|
|
|
@@ -126,6 +127,7 @@ amicus fanout --council free --prompt "Review this design" --json
|
|
|
126
127
|
| `--wave-id <id>` | Set the wave ID explicitly; leg IDs become `<wave-id>-1` … `<wave-id>-N`. |
|
|
127
128
|
| `--session-id <id\|"current">` | Session ID to pull shared context from (default `current`). Same semantics as on `start`. |
|
|
128
129
|
| `--json` | Emit the wave document on stdout. |
|
|
130
|
+
| `--quiet` | Suppress per-leg progress ticks and the final wave-result summary (preflight errors are suppressed too). |
|
|
129
131
|
| `--max-cost <$>` | Refuse the wave if the estimated total exceeds `$` (soft ceiling). |
|
|
130
132
|
| `--no-cost-gate` | Disable the budget gate (per-$/Mtok threshold + ceiling) for this run. |
|
|
131
133
|
| `--no-validate-model` | Skip catalog validation. |
|
|
@@ -134,6 +136,7 @@ amicus fanout --council free --prompt "Review this design" --json
|
|
|
134
136
|
| `--template <name\|path>` | Render a [briefing template](#briefing-templates) (`{{prompt}}`, `{{artifact}}`, `{{artifact_path}}`, `{{date}}`, `{{project}}`, `{{var.*}}`), shared by every leg. |
|
|
135
137
|
| `--artifact <file>` | File whose content fills `{{artifact}}`/`{{artifact_path}}` (256 KB cap). Requires `--template`. |
|
|
136
138
|
| `--var <k=v>` | Set `{{var.<key>}}`; repeatable. Requires `--template`. |
|
|
139
|
+
| `--tag <t>` | Label this wave (and every leg's spend row) for `list`/`--search`/`spend --group-by tag` (1-64 chars, `[A-Za-z0-9_-]`; rejected, not cleaned). Mutually exclusive with `--retry-failed` (`BAD_ARGS`). |
|
|
137
140
|
|
|
138
141
|
**Shared per-leg knobs.** Every leg in the wave also accepts the same per-leg options as `start`:
|
|
139
142
|
`--agent`, `--thinking`, `--timeout`, `--summary-length`, `--no-context`, `--context-*`, `--mcp*`,
|
|
@@ -187,7 +190,7 @@ amicus council run --prompt-file briefing.md --models gemini,glm --chair deepsee
|
|
|
187
190
|
| `--chair <model>` | Verdict synthesizer. Default `deepseek`; must **not** be a bench seat (pre-flight error). |
|
|
188
191
|
| `--critic <model>` | Optional adversarial seat; must **be** a bench seat. Mutually exclusive with `--lenses`. |
|
|
189
192
|
| `--lenses <s1,s2,...>` | Expert lenses, one per seat (count must equal seat count); forces `--no-ledger` semantics. |
|
|
190
|
-
| `--out-dir <dir>` | Run directory. Default `./council-<runId>/`. |
|
|
193
|
+
| `--out-dir <dir>` | Run directory. Default `./council-<runId>/`. Must resolve inside the project directory — a path that escapes it is rejected with `BAD_ARGS`. |
|
|
191
194
|
| `--json` | Emit the council-run document on stdout (error envelope + documented exit codes on failure). |
|
|
192
195
|
| `--max-cost <$>` | **Whole-run** ceiling on **known** spend, checked before each paid stage launch. A leg whose cost cannot be determined does not count against it and never halts the run; when the total is inexact and a ceiling is set, the run exits `2`. |
|
|
193
196
|
| `--timeout <min>` | **Per-leg** timeout (fanout semantics); bound the aggregate with your CI job timeout. |
|
|
@@ -199,6 +202,7 @@ amicus council run --prompt-file briefing.md --models gemini,glm --chair deepsee
|
|
|
199
202
|
| `--template <name\|path>` | Render a [briefing template](#briefing-templates) (`{{prompt}}`, `{{artifact}}`, `{{artifact_path}}`, `{{date}}`, `{{project}}`, `{{var.*}}`). |
|
|
200
203
|
| `--artifact <file>` | File whose content fills `{{artifact}}`/`{{artifact_path}}` (256 KB cap). Requires `--template`. |
|
|
201
204
|
| `--var <k=v>` | Set `{{var.<key>}}`; repeatable. Requires `--template`. |
|
|
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. |
|
|
202
206
|
|
|
203
207
|
**Exit codes:** `0` full run · `2` degraded but reportable (fewer than 2 judges, chair failure —
|
|
204
208
|
`overallVerdict: null` — a cost ceiling hit after the tally, or a `--max-cost` ceiling set over a
|
|
@@ -254,7 +258,7 @@ amicus template show review # Print a template's raw text
|
|
|
254
258
|
amicus start --model gemini --template review --artifact plan.md --var focus=performance --no-ui --json
|
|
255
259
|
```
|
|
256
260
|
|
|
257
|
-
**Known variables** (`src/template/render.js`): `{{prompt}}`, `{{artifact}}`, `{{artifact_path}}`, `{{date}}` (`YYYY-MM-DD`), `{{project}}`, `{{var.<key>}}` (from repeatable `--var key=value`). There is no `{{input}}` in v4.5 — that chaining variable, and the `critique`/`refine` built-ins that need it, arrive with
|
|
261
|
+
**Known variables** (`src/template/render.js`): `{{prompt}}`, `{{artifact}}`, `{{artifact_path}}`, `{{date}}` (`YYYY-MM-DD`), `{{project}}`, `{{var.<key>}}` (from repeatable `--var key=value`). There is no `{{input}}` in v4.5 — that chaining variable, and the `critique`/`refine` built-ins that need it, arrive with composable waves (`--input-from`) in a future release.
|
|
258
262
|
|
|
259
263
|
**Strict by design — a typo fails loudly instead of silently dropping text:**
|
|
260
264
|
|
|
@@ -302,6 +306,8 @@ Then invoke it with `--pack <name|path>` on `start` / `fanout` / `council run`
|
|
|
302
306
|
|
|
303
307
|
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).
|
|
304
308
|
|
|
309
|
+
The `version` field is set with **`--pack-version <semver>`**, not `--version` — the latter is amicus's own global "print the version" flag, which is intercepted before command dispatch and so can never carry a pack's version. `pack save --version` is rejected with `BAD_ARGS` naming the right spelling.
|
|
310
|
+
|
|
305
311
|
**Precedence: flag > pack > config default > built-in default.** A pack only fills in values you didn't type explicitly on the command line — anything you do pass always wins, and the pack is recorded on the run either way (see below), so a hand-tuned invocation of a saved pack is never ambiguous about what actually ran.
|
|
306
312
|
|
|
307
313
|
**`--from-run <id>`** builds a pack from an existing council run, fanout wave, or solo session instead of flags — resolution order is council pointer → wave `metadata.json` → solo `metadata.json`. It captures the bench/model, chair/critic/lenses, and the run options that were actually used; **briefing text is never captured**, only a template *reference* when the source run recorded one.
|
|
@@ -356,7 +362,7 @@ pack. Confirmed against the run's own `run.json` for this exact invocation (irre
|
|
|
356
362
|
{
|
|
357
363
|
"bench": ["gemini", "deepseek", "gpt"],
|
|
358
364
|
"chair": "opus",
|
|
359
|
-
"pack": { "name": "review-bench", "hash": "da084ba56162", "source": "dir" },
|
|
365
|
+
"pack": { "name": "review-bench", "version": "1.0.0", "hash": "da084ba56162", "source": "dir" },
|
|
360
366
|
"options": { "timeout": 20, "maxCost": 2, "gateway": "auto", "outDir": "..." }
|
|
361
367
|
}
|
|
362
368
|
```
|
|
@@ -372,16 +378,23 @@ pack is still recorded on the run either way.
|
|
|
372
378
|
Amicus does **not** ship a frozen table of model names. Aliases and validation resolve against a **live catalog** fetched from provider APIs and cached at `~/.config/amicus/model-catalog.json` (24-hour TTL; the fetch works without an API key).
|
|
373
379
|
|
|
374
380
|
```bash
|
|
375
|
-
amicus models
|
|
376
|
-
amicus models --search gemini
|
|
377
|
-
amicus models --refresh
|
|
378
|
-
amicus models --check
|
|
379
|
-
amicus models --check --
|
|
381
|
+
amicus models # List the catalog
|
|
382
|
+
amicus models --search gemini # Filter by substring over id and name
|
|
383
|
+
amicus models --refresh # Force-refresh from provider APIs
|
|
384
|
+
amicus models --check # Audit your aliases against the catalog
|
|
385
|
+
amicus models --check --strict # + exit non-zero on curated per-gateway drift too
|
|
386
|
+
amicus models --check --live # + probe every stored alias with a real leg (spends)
|
|
380
387
|
```
|
|
381
388
|
|
|
382
389
|
`amicus models --check` exits with the **number of stale aliases** (capped at 100) and prints same-vendor replacement suggestions for each, so it drops cleanly into CI.
|
|
383
390
|
|
|
384
|
-
**
|
|
391
|
+
**Gateway-only routes.** A curated alias whose direct form is *derived* from its OpenRouter route (rather than
|
|
392
|
+
authored) is not reported STALE when that direct form is missing from the vendor's
|
|
393
|
+
direct namespace while the OpenRouter route still serves — a gateway-only route with
|
|
394
|
+
no direct sibling is a routing choice, not staleness. Deliberately gateway-only
|
|
395
|
+
entries (e.g. `gpt-pro`) are annotated as such and are never offered a retarget.
|
|
396
|
+
|
|
397
|
+
**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 prints the exact `amicus setup --add-alias <alias>=<current>` refresh command. 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.
|
|
385
398
|
|
|
386
399
|
**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.
|
|
387
400
|
|
|
@@ -418,7 +431,9 @@ Each stored alias resolves to one of three outcomes:
|
|
|
418
431
|
amicus list # Current project
|
|
419
432
|
amicus list --status running # Filter: running, complete, error, timed-out,
|
|
420
433
|
# aborted, crashed, idle-timeout
|
|
421
|
-
amicus list --all # All projects
|
|
434
|
+
amicus list --all # All projects (cross-project, via the session index)
|
|
435
|
+
amicus list --search foo # Substring match: id, tag, briefing material (case-insensitive)
|
|
436
|
+
amicus list --limit 20 # Only the 20 newest rows (0 = unlimited); pairs well with --all
|
|
422
437
|
amicus list --json # Machine-readable
|
|
423
438
|
|
|
424
439
|
amicus read <id> # Fold summary (default)
|
|
@@ -444,6 +459,43 @@ amicus setup --api-keys # Open just the API-key window
|
|
|
444
459
|
amicus setup --add-alias fast=google/gemini-2.5-flash # Add/override one alias (bare canonical)
|
|
445
460
|
```
|
|
446
461
|
|
|
462
|
+
**`amicus list` flags.** The CLI and the MCP `amicus_list` tool share one enumeration
|
|
463
|
+
(`src/sidecar/read.js`'s `enumerateSessions`) for ordinary start/fanout rows, so those rows carry
|
|
464
|
+
the same core fields everywhere: `id`, `model`, `status`, `mode` (`interactive`/`headless`),
|
|
465
|
+
`type` (`run` by default), `parentWave` (`null` unless the row is a fan-out leg), `legCount`
|
|
466
|
+
(`null` unless the row is a wave), and `tag` — the one field that's omitted, not `null`, when the
|
|
467
|
+
session has none. The human-readable CLI table adds a `TAG` column. Council-run rows are an
|
|
468
|
+
**MCP-only** row class: the CLI's own directory scan skips their `council-<runId>.json` pointer
|
|
469
|
+
files (a pointer's filename fails the session-ID pattern every other row's directory name must
|
|
470
|
+
match), so `amicus list` never shows a council run — only the MCP `amicus_list` tool merges them
|
|
471
|
+
in, each carrying `type: 'council-run'`, a fixed `mode: 'headless'`, and its own 80-char sanitized
|
|
472
|
+
`briefing` preview plus a `stage` field naming whichever stage is currently running. The MCP tool
|
|
473
|
+
also re-sanitizes every other row's `briefing` to that same 80-char cap and, for any row still
|
|
474
|
+
`status: 'running'`, adds live-progress fields (`phase`, `messageCount`, `lastActivityAt`,
|
|
475
|
+
`latestPreview`) — enrichments the CLI table doesn't apply, since it prints the raw 30-char slice
|
|
476
|
+
in-line instead. `--all` (CLI only) enumerates every project the global, advisory sessions-index
|
|
477
|
+
knows about, deduped by canonical project identity, and stamps each row's `project` field (the
|
|
478
|
+
CLI table adds a trailing `PROJECT` column whenever `--all` is set); a stale index entry pointing
|
|
479
|
+
at a missing or unreadable project is skipped rather than surfaced as an error. `--search <q>`
|
|
480
|
+
(both surfaces) is a case-insensitive substring filter over `id`, `tag`, and briefing material: a
|
|
481
|
+
fan-out wave row reads its full `briefing.md` off disk (falling back to the row's 200-char excerpt
|
|
482
|
+
if that file isn't readable), and a leg row (one spawned by a wave) matches on `id`/`tag` only —
|
|
483
|
+
its briefing is the parent wave's, and matching it there would surface the same wave once per leg
|
|
484
|
+
it spawned. On the MCP tool specifically, a council-run row's search material is `briefing.md`
|
|
485
|
+
written at MCP launch time, or falls back to the portion of `briefing-stage1.md` after
|
|
486
|
+
`--- MATERIAL / BRIEFING ---` (CLI-launched runs only ever have the latter file) — this clause is
|
|
487
|
+
MCP-only, since the CLI never lists a council row to search in the first place. A bare `--search`
|
|
488
|
+
with no value is a usage error on the CLI. Tag itself is set at launch with `--tag <t>` on
|
|
489
|
+
`start`/`fanout`/`council run` (see those sections above), and is also a dimension for
|
|
490
|
+
`amicus spend --group-by tag`.
|
|
491
|
+
|
|
492
|
+
**Known limitation: a tag is not inherited.** `--tag` only ever gets written at launch. `amicus
|
|
493
|
+
continue` and `amicus resume` write their spend rows with no tag, and a `fanout --retry-failed`
|
|
494
|
+
wave does not carry the original wave's tag either (`--tag` combined with `--retry-failed` is
|
|
495
|
+
rejected as a usage error). Since the spend ledger writes `tag: null` whenever none was set, those
|
|
496
|
+
rows group under `(unattributed)` in `amicus spend --group-by tag` rather than under the tag their
|
|
497
|
+
lineage started with.
|
|
498
|
+
|
|
447
499
|
**`amicus status <id>` output.** Human-readable:
|
|
448
500
|
|
|
449
501
|
```
|
|
@@ -462,7 +514,7 @@ $ amicus status demo123 --json
|
|
|
462
514
|
"taskId": "demo123",
|
|
463
515
|
"status": "complete",
|
|
464
516
|
"elapsed": "5m 0s",
|
|
465
|
-
"version": "4.
|
|
517
|
+
"version": "4.7.0",
|
|
466
518
|
"model": "google/gemini-2.5-flash",
|
|
467
519
|
"phase": "terminal"
|
|
468
520
|
}
|
|
@@ -514,7 +566,7 @@ Runs every check below, in order, and prints a ✓/⚠/✗ line for each plus a
|
|
|
514
566
|
|
|
515
567
|
| Check | What it verifies | Can fail as |
|
|
516
568
|
|---|---|---|
|
|
517
|
-
| `node` | Node.js ≥
|
|
569
|
+
| `node` | Node.js ≥ 22.12 | error |
|
|
518
570
|
| `config-dir` | The resolved config directory | *(always ok)* |
|
|
519
571
|
| `keys` | At least one cloud-vendor key configured | error |
|
|
520
572
|
| `default-model` | Your default model alias resolves | error |
|
|
@@ -528,13 +580,14 @@ Runs every check below, in order, and prints a ✓/⚠/✗ line for each plus a
|
|
|
528
580
|
| `mcp` | Amicus is registered as an MCP server in Claude Code | warn |
|
|
529
581
|
| `mcp-legacy` | No duplicate legacy `sidecar` MCP entry survives alongside `amicus` | warn |
|
|
530
582
|
| `sessions-index-tmp` | No orphaned `sessions-index.json.*.tmp` files | warn |
|
|
583
|
+
| `session-metadata-tmp` | No orphaned per-session `.metadata.json.*.tmp` files (the B09 class) | warn |
|
|
531
584
|
| `openrouter-credit` | Remaining OpenRouter credit (skipped — reports `ok` — when no OpenRouter key is set) | warn |
|
|
532
585
|
| `local-providers` **(v4.2)** | Every provider in `config.providers` is reachable | warn |
|
|
533
586
|
| `project-root` | Your cwd looks like a real project, not an app/install dir | warn |
|
|
534
587
|
|
|
535
588
|
**`local-providers`** probes every configured local provider (2s timeout each) the same way `amicus provider test` does, and reports per-id reachability in one line, e.g. `ollama: 3 models @ http://127.0.0.1:11434/v1; my-vllm: unreachable @ http://127.0.0.1:8000/v1`. No providers configured at all is a plain `ok` ("none configured") — this check can never fail your doctor run outright, only warn: a napping `ollama serve` isn't treated as broken setup.
|
|
536
589
|
|
|
537
|
-
`--fix` self-heals
|
|
590
|
+
`--fix` self-heals five of the checks above in place: reprovisions Electron, copies the OpenCode engine into a broken npx-cache install, removes a duplicate legacy MCP entry, sweeps orphaned session-index tmp files, and sweeps orphaned per-session metadata tmp files (both tmp sweeps only ones older than 60s). It does **not** start a local server for you — `local-providers` stays a warning until you start the server yourself.
|
|
538
591
|
|
|
539
592
|
Exit code is `1` if anything is `error`, else `0` (same rule drives `--json`'s `ok` field).
|
|
540
593
|
|
|
@@ -568,7 +621,7 @@ Reads `~/.config/amicus/spend-ledger.jsonl` (one row per completed run/leg) and
|
|
|
568
621
|
| `--model <id-or-prefix>` | rows whose model id starts with the given string |
|
|
569
622
|
| `--op <op>` | rows for one operation (`start`, `leg`, `continue`, `resume`, …) |
|
|
570
623
|
| `--failed` | rows with an explicit non-`complete` status (see the caveat below) |
|
|
571
|
-
| `--group-by <dim>` | bucket totals by `model` (default) \| `wave` \| `council` \| `project` \| `op` \| `day` |
|
|
624
|
+
| `--group-by <dim>` | bucket totals by `model` (default) \| `wave` \| `council` \| `project` \| `op` \| `day` \| `tag` (untagged rows group under `(unattributed)`) |
|
|
572
625
|
| `--rows` | also emit the raw filtered rows (capped at 1000; `--json` sets `rowsTruncated: true` past the cap) |
|
|
573
626
|
|
|
574
627
|
All filters compose, e.g. `amicus spend --project . --group-by model --since 7d`.
|
|
@@ -105,6 +105,9 @@
|
|
|
105
105
|
<!-- ⚠️ D8 extraction (Task 1, v4.6.2 PR4): workspace-seats.js must load BEFORE
|
|
106
106
|
workspace-panels.js, whose renderSeatsPanel delegates to it. -->
|
|
107
107
|
<script src="./workspace-seats.js"></script>
|
|
108
|
+
<!-- ⚠️ v4.7 PR7 extraction: workspace-lazy.js must load BEFORE workspace-panels.js,
|
|
109
|
+
whose wireLazyPanels/proseLoader delegate to it. -->
|
|
110
|
+
<script src="./workspace-lazy.js"></script>
|
|
108
111
|
<!-- ⚠️ DE-ROT (F05): panels + verbs are the Task 13 split of workspace-app.js.
|
|
109
112
|
Load order matters: workspace-app.js boots last and owns `state`. -->
|
|
110
113
|
<script src="./workspace-panels.js"></script>
|
|
@@ -37,9 +37,17 @@
|
|
|
37
37
|
return (state.visible && state.focused) ? 1500 : 5000;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
/** True when `status` is a terminal run status. The single consumption
|
|
41
|
+
* point for TERMINAL_STATUSES membership (v4.6.3 PR2 dedup) — the array
|
|
42
|
+
* itself stays exported and byte-identical to src/workspace/run-detail.js
|
|
43
|
+
* (drift-pinned). */
|
|
44
|
+
function isTerminal(status) {
|
|
45
|
+
return TERMINAL_STATUSES.indexOf(status) !== -1;
|
|
46
|
+
}
|
|
47
|
+
|
|
40
48
|
/** Blind default per spec resolved Q2: ON live, OFF terminal. */
|
|
41
49
|
function defaultBlind(status) {
|
|
42
|
-
return
|
|
50
|
+
return !isTerminal(status);
|
|
43
51
|
}
|
|
44
52
|
|
|
45
53
|
function dash(v) {
|
|
@@ -86,9 +94,27 @@
|
|
|
86
94
|
];
|
|
87
95
|
}
|
|
88
96
|
|
|
97
|
+
// v4.7 D6/E1: three row-per-launch producer roles added alongside the
|
|
98
|
+
// existing chair-attempt (run-chair.js), repair (run-stages.js/
|
|
99
|
+
// run-stage2.js/run-chair.js) and superseded (run-stages.js + debate.js)
|
|
100
|
+
// rows — launch-accounting extras, not seats, and unlike rebuttal/revote
|
|
101
|
+
// (F37, kept rendering on purpose below) they have no seats-panel meaning
|
|
102
|
+
// of their own. There is no pre-existing allowlist in this function (E1) —
|
|
103
|
+
// this is a plain exclusion added on top of the untouched id/shape logic.
|
|
104
|
+
// Object.create(null): a plain `{...}` literal inherits Object.prototype, so a role
|
|
105
|
+
// literally named 'constructor'/'toString'/etc would resolve to an inherited (truthy)
|
|
106
|
+
// function via bracket lookup instead of `undefined` — silently mis-excluding (or
|
|
107
|
+
// mis-including) that seat. A null-prototype object has no inherited keys to collide with.
|
|
108
|
+
var SEATS_PANEL_EXCLUDED_ROLES = Object.create(null);
|
|
109
|
+
SEATS_PANEL_EXCLUDED_ROLES['chair-attempt'] = true;
|
|
110
|
+
SEATS_PANEL_EXCLUDED_ROLES.repair = true;
|
|
111
|
+
SEATS_PANEL_EXCLUDED_ROLES.superseded = true;
|
|
112
|
+
|
|
89
113
|
/** Terminal fallback: derive seat-shaped rows from tally runStats cost rows. */
|
|
90
114
|
function seatsFromRunStats(costRows) {
|
|
91
|
-
return (costRows || []).
|
|
115
|
+
return (costRows || []).filter(function (r) {
|
|
116
|
+
return !SEATS_PANEL_EXCLUDED_ROLES[r.role];
|
|
117
|
+
}).map(function (r) {
|
|
92
118
|
return {
|
|
93
119
|
// ⚠️ DE-ROT (F37): composite id — a v4.1 `--debate` run emits extra runStats rows for
|
|
94
120
|
// the SAME bench alias (role 'rebuttal'/'revote', src/council/debate.js:88-96). With no
|
|
@@ -122,28 +148,79 @@
|
|
|
122
148
|
* loss, an out-of-range index, or a zero-model unit) carry neither, so they
|
|
123
149
|
* correctly fall back to the plain phrasing.
|
|
124
150
|
*
|
|
125
|
-
* D6 filter (zero usable legs ONLY, "no ghost when a
|
|
126
|
-
* candidate already present in `liveSeats`
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
151
|
+
* D6 filter (zero usable legs ONLY, "no ghost when a live row already
|
|
152
|
+
* exists"): a candidate already present in `liveSeats` is dropped. Two
|
|
153
|
+
* different things can put it there, and both are reasons to suppress:
|
|
154
|
+
* an SL-2 retry actually healed it (a real recovered review), OR
|
|
155
|
+
* (owner-ruled, v4.7 CA-4 dead-seat convergence) the seat never recovered
|
|
156
|
+
* at all but the row-per-launch machinery still gives its dead leg an
|
|
157
|
+
* honest primary ERROR row (every billed leg gets a row now, including
|
|
158
|
+
* failures — run-stages.js/run-assemble.js) — so "it has a cost row" no
|
|
159
|
+
* longer implies "it healed". Either way that live row IS the seat's
|
|
160
|
+
* record; rendering a second "did not review" ghost row beside it would
|
|
161
|
+
* be a duplicate, not new information, so suppressing it here is the
|
|
162
|
+
* ACCEPTED terminal-path behavior: exactly one row per seat, whatever its
|
|
163
|
+
* status. This is the one thing standing between a recovered (or
|
|
164
|
+
* honestly-erred) seat and a duplicate/ghost row — same failure family as
|
|
165
|
+
* the F37 debate-role collision and the RN-11 keyed-row lessons just above
|
|
166
|
+
* (seatsFromRunStats, seatCells): an identity that is not carefully
|
|
167
|
+
* matched silently duplicates or overwrites instead of failing loud.
|
|
168
|
+
*
|
|
169
|
+
* Role-aware D6 (v4.6.3 PR2, spec D3): a bare model match used to suppress
|
|
170
|
+
* regardless of what the LIVE row's role was — so a model that died as
|
|
171
|
+
* critic but whose chair-fallback walk happened to land on that same alias
|
|
172
|
+
* (and succeeded, producing a live `role: 'chair'` cost row) silently
|
|
173
|
+
* erased the dead-critic row it was never a replacement for (spec §5, the
|
|
174
|
+
* PR 102 rider). Candidates now carry a `role` (`'critic'` via alias equality
|
|
175
|
+
* with `runMeta.critic` — mirroring `deriveSeatLoss`, verdict.js:72 — or
|
|
176
|
+
* `null`), and only REVIEWING-role live legs (`seat`/`critic`/`lens:*`)
|
|
177
|
+
* suppress at all; a `'critic'` candidate is cleared only by a live
|
|
178
|
+
* CRITIC-role leg for that alias, never by a chair/judge/rebuttal/revote
|
|
179
|
+
* row landing on the same model. Hidden dependency: the recovered-critic
|
|
180
|
+
* suppression below (`byRole[alias + '|critic']`) relies on `roleFor`'s
|
|
181
|
+
* critic branch (src/council/run-stages.js), which only fires when lenses
|
|
182
|
+
* are absent — safe today only because --critic and --lenses are mutually
|
|
183
|
+
* exclusive (src/cli-handlers-council-run.js's `critic && lenses` check); if
|
|
184
|
+
* that exclusion ever loosens, a healed critic on a lens run would carry
|
|
185
|
+
* role 'seat' and this suppression would render a ghost dead row for it.
|
|
186
|
+
*
|
|
187
|
+
* Old-run resilience (v4.6.3 PR2, spec D4): pre-`degrades[]` runs (v4.5.2)
|
|
188
|
+
* carry the BENCH half of a seat loss only in `seatLoss.deadBenchSeats`
|
|
189
|
+
* (string[] of aliases, verdict.js deriveSeatLoss) — `degrades[]` never
|
|
190
|
+
* existed on either doc for these runs. Consumed after the critic backstop,
|
|
191
|
+
* candidates get `role: null` (deadBenchSeats carries no critic/bench
|
|
192
|
+
* distinction beyond what `criticRequested` already covers above) and flow
|
|
193
|
+
* through the same `seen`-keyed dedup and role-aware suppression as every
|
|
194
|
+
* other candidate — deriveSeatLoss does not dedup its own array, so `seen`
|
|
195
|
+
* is what keeps a repeated alias (or one also named by a real degrade
|
|
196
|
+
* record) from rendering twice.
|
|
132
197
|
*
|
|
133
198
|
* @param {Array<object>} degrades run.json's `degrades[]` (may be absent)
|
|
134
|
-
* @param {?object} seatLoss verdict.json's `seatLoss` (may be absent)
|
|
199
|
+
* @param {?object} seatLoss verdict.json's `seatLoss` (may be absent) —
|
|
200
|
+
* `criticRequested`/`criticSeated` back the critic candidate above,
|
|
201
|
+
* `deadBenchSeats` (string[] of aliases) feeds the bench candidates below
|
|
135
202
|
* @param {Array<{model: string}>} liveSeats seatsFromRunStats(...)'s output
|
|
136
203
|
* (or any seat list keyed the same way — the live seat map)
|
|
137
|
-
* @
|
|
204
|
+
* @param {?{critic: ?string}} runMeta run.critic (alias, or null/absent
|
|
205
|
+
* when no critic was requested) — degrade records carry no role field, so
|
|
206
|
+
* this is the ONLY way a degrade-sourced candidate is identified as critic
|
|
207
|
+
* @returns {Array<{model: string, statusText: string, role: ?string}>}
|
|
138
208
|
*/
|
|
139
|
-
function deadSeats(degrades, seatLoss, liveSeats) {
|
|
140
|
-
var
|
|
209
|
+
function deadSeats(degrades, seatLoss, liveSeats, runMeta) {
|
|
210
|
+
var critic = runMeta && runMeta.critic ? runMeta.critic : null;
|
|
211
|
+
// ⚠️ Object.create(null) throughout this family (also workspace-render.js's
|
|
212
|
+
// `existing`/`seen` and workspace-app.js's `labelByModel`): a model literally
|
|
213
|
+
// named `toString` is truthy off a bare object, so it was dropped here and —
|
|
214
|
+
// worse — crashed workspace-render.js:212 reading `.children` off an inherited
|
|
215
|
+
// function, killing the seats repaint and every tick after it.
|
|
216
|
+
var seen = Object.create(null);
|
|
141
217
|
var order = [];
|
|
142
|
-
function add(model, retried) {
|
|
218
|
+
function add(model, retried, role) {
|
|
143
219
|
if (!model || seen[model]) { return; }
|
|
144
220
|
seen[model] = true;
|
|
145
221
|
order.push({
|
|
146
222
|
model: model,
|
|
223
|
+
role: role || null,
|
|
147
224
|
statusText: retried ? 'did not review — retried once' : 'did not review',
|
|
148
225
|
});
|
|
149
226
|
}
|
|
@@ -152,16 +229,41 @@
|
|
|
152
229
|
if (d.channel !== 'dead-leg' && d.channel !== 'dead-wave') { return; }
|
|
153
230
|
var data = d.data || {};
|
|
154
231
|
var retried = !!(data.retryWaveId || data.firstFailure);
|
|
232
|
+
// Critic identification mirrors deriveSeatLoss (verdict.js): alias
|
|
233
|
+
// equality with run.critic — degrade records carry no role field.
|
|
155
234
|
if (d.channel === 'dead-leg') {
|
|
156
|
-
add(data.seat, retried);
|
|
235
|
+
add(data.seat, retried, critic && data.seat === critic ? 'critic' : null);
|
|
157
236
|
} else {
|
|
158
|
-
(data.models || []).forEach(function (m) {
|
|
237
|
+
(data.models || []).forEach(function (m) {
|
|
238
|
+
add(m, retried, critic && m === critic ? 'critic' : null);
|
|
239
|
+
});
|
|
159
240
|
}
|
|
160
241
|
});
|
|
161
242
|
if (seatLoss && seatLoss.criticRequested && !seatLoss.criticSeated) {
|
|
162
|
-
add(seatLoss.criticRequested, false);
|
|
243
|
+
add(seatLoss.criticRequested, false, 'critic');
|
|
163
244
|
}
|
|
164
|
-
|
|
245
|
+
if (seatLoss) {
|
|
246
|
+
// Pre-degrades[] era (v4.5.2): the bench half of a seat loss lives
|
|
247
|
+
// only here. Alias strings; deriveSeatLoss does not dedup — `seen`
|
|
248
|
+
// absorbs repeats and degrade-sourced duplicates.
|
|
249
|
+
(seatLoss.deadBenchSeats || []).forEach(function (m) { add(m, false, null); });
|
|
250
|
+
}
|
|
251
|
+
// Role-aware D6 (v4.6.3 PR2): only REVIEWING-role live legs suppress —
|
|
252
|
+
// a chair/judge/rebuttal/revote row must not hide a dead reviewer, and
|
|
253
|
+
// a dead-critic candidate is cleared only by a live CRITIC leg. A null
|
|
254
|
+
// role is NOT reviewing: counting it would suppress silently, the exact
|
|
255
|
+
// class the announcement invariant forbids.
|
|
256
|
+
// Role 'claude' is deliberately absent: it is emitted only by claudeRunStatsRow
|
|
257
|
+
// (src/council/run-assemble.js:129-132) for a seat that never launches a leg, and
|
|
258
|
+
// preflightClaudeReview (run-assemble.js:86-102) rejects 'claude' as chair/critic/
|
|
259
|
+
// bench — so no 'claude' leg can die. If that reservation ever loosens, this
|
|
260
|
+
// allowlist is the single place to extend.
|
|
261
|
+
function isReviewing(role) {
|
|
262
|
+
return role === 'seat' || role === 'critic' ||
|
|
263
|
+
(typeof role === 'string' && role.indexOf('lens:') === 0);
|
|
264
|
+
}
|
|
265
|
+
var reviewing = Object.create(null);
|
|
266
|
+
var byRole = Object.create(null);
|
|
165
267
|
// ⚠️ Fable review (PR4b fix wave): same F34/F36 alias-selection seatCells already uses
|
|
166
268
|
// (`seat.modelInput || seat.model`, above) — a LIVE payload seat's `model` is the RESOLVED
|
|
167
269
|
// executable id, not the alias a degrade record names; `modelInput` carries the alias.
|
|
@@ -170,14 +272,23 @@
|
|
|
170
272
|
// so D6 failed to suppress it — both rows rendered until the stage boundary dropped the
|
|
171
273
|
// errored row. Terminal-path cost rows (seatsFromRunStats) carry no `modelInput` at all and
|
|
172
274
|
// are already alias-only, so `|| s.model` leaves that path unchanged.
|
|
173
|
-
(liveSeats || []).forEach(function (s) {
|
|
174
|
-
|
|
275
|
+
(liveSeats || []).forEach(function (s) {
|
|
276
|
+
if (!isReviewing(s.role)) { return; }
|
|
277
|
+
var alias = s.modelInput || s.model; // F36: alias space, never resolved ids
|
|
278
|
+
reviewing[alias] = true;
|
|
279
|
+
byRole[alias + '|' + s.role] = true;
|
|
280
|
+
});
|
|
281
|
+
return order.filter(function (s) {
|
|
282
|
+
if (s.role === 'critic') { return !byRole[s.model + '|critic']; }
|
|
283
|
+
return !reviewing[s.model];
|
|
284
|
+
});
|
|
175
285
|
}
|
|
176
286
|
|
|
177
287
|
// ⚠️ DE-ROT (F41): STAGE_LABELS is exported so applyLive() can label post-open stages.
|
|
178
288
|
var api = { pollDelay: pollDelay, seatCells: seatCells, seatsFromRunStats: seatsFromRunStats,
|
|
179
289
|
deadSeats: deadSeats,
|
|
180
|
-
defaultBlind: defaultBlind,
|
|
290
|
+
defaultBlind: defaultBlind, isTerminal: isTerminal, dash: dash,
|
|
291
|
+
TERMINAL_STATUSES: TERMINAL_STATUSES, STAGE_LABELS: STAGE_LABELS };
|
|
181
292
|
if (typeof module !== 'undefined' && module.exports) { module.exports = api; }
|
|
182
293
|
if (typeof window !== 'undefined') { window.AmicusLive = api; }
|
|
183
294
|
})();
|
|
@@ -26,6 +26,9 @@
|
|
|
26
26
|
debate: null, // ⚠️ DE-ROT (F38): parsed debate.json, fetched once per run-open; stays
|
|
27
27
|
// null on a non-debate run, an aborted/skipped debate, or a parse failure —
|
|
28
28
|
// drillIntoJudge's judge-*.md fallback covers all three.
|
|
29
|
+
debateFetch: null, // T18-m1: test seam — the fire-and-forget debate.json fetch promise
|
|
30
|
+
// (null when no fetch was issued for the open run). Nothing renders
|
|
31
|
+
// from this; tests may `await state.debateFetch` to sequence past it.
|
|
29
32
|
blind: false,
|
|
30
33
|
// Task 19 (RN-5) + fix-wave (RN-5 amendment): the (run id, status) pair renderDetail() last
|
|
31
34
|
// computed state.blind's default for. Together they gate the recompute (in renderDetail(),
|
|
@@ -34,7 +37,7 @@
|
|
|
34
37
|
// live loop's running -> terminal refresh, or the abort-confirm re-read) still auto-reveals.
|
|
35
38
|
detailRunId: null,
|
|
36
39
|
detailRunStatus: null,
|
|
37
|
-
labelByModel:
|
|
40
|
+
labelByModel: Object.create(null),
|
|
38
41
|
listTimer: null,
|
|
39
42
|
liveTimer: null, // LIVE (Task 15)
|
|
40
43
|
liveEpoch: 0, // LIVE (Task 15): bumped by stopLiveLoop() to invalidate in-flight ticks
|
|
@@ -66,7 +69,19 @@
|
|
|
66
69
|
function openRun(runId) {
|
|
67
70
|
state.runId = runId;
|
|
68
71
|
state.debate = null;
|
|
72
|
+
// Fix-wave item 2: reset synchronously alongside state.debate so the contract comment
|
|
73
|
+
// above ("null when no fetch was issued for the open run") holds in EVERY window —
|
|
74
|
+
// including between this openRun(runId) call and its get-run reply, where the old
|
|
75
|
+
// in-.then reset left state.debateFetch pointing at the PREVIOUS run's promise. The F09
|
|
76
|
+
// stale-reply guard (below) early-returns before ever touching debateFetch again, so
|
|
77
|
+
// moving the reset here changes no other behavior.
|
|
78
|
+
state.debateFetch = null;
|
|
69
79
|
return invoke('workspace:get-run', runId).then(function (detail) {
|
|
80
|
+
// F09 guard (v4.6.3 PR2): a reply for a run the user has since
|
|
81
|
+
// navigated away from must never overwrite the run now open. Guard on
|
|
82
|
+
// runId movement ONLY — a same-run re-open (the live loop's terminal
|
|
83
|
+
// refresh) must still apply its fresher reply.
|
|
84
|
+
if (state.runId !== runId) { return; }
|
|
70
85
|
state.detail = detail;
|
|
71
86
|
// ⚠️ DE-ROT (F38): debate.json is the re-vote index the matrix drill-in needs — fetched
|
|
72
87
|
// once per run-open (never per render), fire-and-forget. An aborted or cost-ceiling
|
|
@@ -78,8 +93,9 @@
|
|
|
78
93
|
// navigated away from must never overwrite the run now open. Capture `runId` and check
|
|
79
94
|
// it's still `state.runId` before writing; a rejection (dead channel, closed window) is
|
|
80
95
|
// caught too, so it never surfaces as an unhandled rejection in the renderer.
|
|
96
|
+
// (state.debateFetch reset moved synchronously above, beside state.debate — fix-wave item 2.)
|
|
81
97
|
if (detail && detail.run && detail.run.debate) {
|
|
82
|
-
invoke('workspace:read-artifact', runId, 'debate.json').then(function (res) {
|
|
98
|
+
state.debateFetch = invoke('workspace:read-artifact', runId, 'debate.json').then(function (res) {
|
|
83
99
|
if (state.runId !== runId) { return; }
|
|
84
100
|
try { state.debate = JSON.parse(res.text); } catch (err) { state.debate = null; }
|
|
85
101
|
}).catch(function () {
|
|
@@ -128,7 +144,7 @@
|
|
|
128
144
|
state.detailRunStatus = d.run.status;
|
|
129
145
|
}
|
|
130
146
|
$('blind-toggle').checked = state.blind;
|
|
131
|
-
state.labelByModel =
|
|
147
|
+
state.labelByModel = Object.create(null);
|
|
132
148
|
d.derived.names.forEach(function (p) { state.labelByModel[p.model] = p.label; });
|
|
133
149
|
|
|
134
150
|
$('run-title').textContent = d.runId;
|
|
@@ -146,7 +162,7 @@
|
|
|
146
162
|
P.renderVerdictPanel();
|
|
147
163
|
R.renderCost($('cost-body'), d.derived.cost, state.blind, labelOf);
|
|
148
164
|
P.wireLazyPanels();
|
|
149
|
-
var isTerminal = window.AmicusLive.
|
|
165
|
+
var isTerminal = window.AmicusLive.isTerminal(d.run.status);
|
|
150
166
|
$('abort-btn').hidden = isTerminal;
|
|
151
167
|
V.startLiveLoop();
|
|
152
168
|
}
|