amicus 4.6.3 → 4.7.1
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 +19 -7
- package/bin/amicus.js +31 -0
- package/docs/ROADMAP.md +143 -36
- package/docs/configuration.md +52 -4
- package/docs/council.md +65 -12
- package/docs/doc-system.md +8 -7
- package/docs/schemas.md +10 -1
- package/docs/testing.md +1 -1
- package/docs/troubleshooting.md +33 -5
- package/docs/usage.md +64 -14
- package/electron/workspace-ui/index.html +3 -0
- package/electron/workspace-ui/live-model.js +52 -14
- package/electron/workspace-ui/workspace-app.js +14 -3
- 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 +6 -2
- package/electron/workspace-ui/workspace-seats.js +68 -0
- package/electron/workspace-ui/workspace.css +6 -0
- package/package.json +8 -4
- 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/scripts/postinstall.js +6 -3
- package/scripts/setup-hooks.js +49 -3
- package/skills/second-opinion/MANUAL-ORCHESTRATION.md +12 -0
- package/skills/second-opinion/MODEL-NOTES.md +5 -4
- package/skills/sidecar/SKILL.md +9 -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 +17 -5
- package/src/cli-handlers-fanout.js +179 -0
- package/src/cli-handlers-pack.js +24 -10
- package/src/cli-handlers-resume-continue.js +20 -0
- 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/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 +61 -5
- package/src/council/run-debate.js +51 -67
- package/src/council/run-launch.js +20 -2
- package/src/council/run-retry.js +17 -2
- 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/headless.js +69 -6
- 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/sidecar/budget.js +38 -4
- package/src/sidecar/continue.js +8 -23
- package/src/sidecar/fanout-budget.js +1 -2
- package/src/sidecar/fanout-leg-fallback.js +7 -3
- package/src/sidecar/fanout-retry.js +15 -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/reopen-spend.js +32 -0
- package/src/sidecar/resume.js +1 -1
- 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/cli-preflight.js +27 -1
- package/src/utils/config.js +15 -0
- package/src/utils/doctor-engine-check.js +32 -0
- package/src/utils/engine-install-scan.js +98 -15
- package/src/utils/engine-repair.js +96 -2
- package/src/utils/remediation-hints.js +29 -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 +24 -4
- 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,11 +378,12 @@ 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.
|
|
@@ -387,7 +394,7 @@ direct namespace while the OpenRouter route still serves — a gateway-only rout
|
|
|
387
394
|
no direct sibling is a routing choice, not staleness. Deliberately gateway-only
|
|
388
395
|
entries (e.g. `gpt-pro`) are annotated as such and are never offered a retarget.
|
|
389
396
|
|
|
390
|
-
**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 —
|
|
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.
|
|
391
398
|
|
|
392
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.
|
|
393
400
|
|
|
@@ -424,7 +431,9 @@ Each stored alias resolves to one of three outcomes:
|
|
|
424
431
|
amicus list # Current project
|
|
425
432
|
amicus list --status running # Filter: running, complete, error, timed-out,
|
|
426
433
|
# aborted, crashed, idle-timeout
|
|
427
|
-
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
|
|
428
437
|
amicus list --json # Machine-readable
|
|
429
438
|
|
|
430
439
|
amicus read <id> # Fold summary (default)
|
|
@@ -450,6 +459,47 @@ amicus setup --api-keys # Open just the API-key window
|
|
|
450
459
|
amicus setup --add-alias fast=google/gemini-2.5-flash # Add/override one alias (bare canonical)
|
|
451
460
|
```
|
|
452
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`. `amicus continue` and `amicus resume` don't take a `--tag` of
|
|
491
|
+
their own — they automatically inherit the parent session's tag (see below).
|
|
492
|
+
|
|
493
|
+
**Tag inheritance.** `amicus continue` and `amicus resume` carry the parent session's tag forward
|
|
494
|
+
onto the new/reopened session's metadata and spend row, with no `--tag` flag needed — resume reuses
|
|
495
|
+
the parent's own session directory, and continue copies the tag onto the new session it creates, so
|
|
496
|
+
a multi-hop `continue` chain keeps the same tag at every depth. A `fanout --retry-failed` wave
|
|
497
|
+
likewise replays the original wave's tag automatically. Because the tag in each of these cases
|
|
498
|
+
comes from the session/wave being reopened rather than a fresh launch, `--tag` is rejected as a
|
|
499
|
+
usage error when combined with `continue`, `resume`, or `--retry-failed` — there is nothing new to
|
|
500
|
+
set. An untagged parent still leaves the key absent (not `null`) on the new metadata, and its spend
|
|
501
|
+
row still groups under `(unattributed)`, exactly as an untagged `start`/`fanout` would.
|
|
502
|
+
|
|
453
503
|
**`amicus status <id>` output.** Human-readable:
|
|
454
504
|
|
|
455
505
|
```
|
|
@@ -468,7 +518,7 @@ $ amicus status demo123 --json
|
|
|
468
518
|
"taskId": "demo123",
|
|
469
519
|
"status": "complete",
|
|
470
520
|
"elapsed": "5m 0s",
|
|
471
|
-
"version": "4.
|
|
521
|
+
"version": "4.7.1",
|
|
472
522
|
"model": "google/gemini-2.5-flash",
|
|
473
523
|
"phase": "terminal"
|
|
474
524
|
}
|
|
@@ -528,7 +578,7 @@ Runs every check below, in order, and prints a ✓/⚠/✗ line for each plus a
|
|
|
528
578
|
| `aliases` | Your configured aliases still resolve against the catalog | warn |
|
|
529
579
|
| `anthropic-base-url` | `ANTHROPIC_BASE_URL` isn't host-form (host-form 404s every direct-Anthropic leg unless normalized) | warn |
|
|
530
580
|
| `opencode-bin` | The OpenCode engine binary is on `PATH` | error |
|
|
531
|
-
| `engine-mcp` | The engine copy `npx -y amicus@latest mcp` would actually launch (catches a broken npx-cache copy a healthy local install would hide) | warn (error only if there's exactly one npx-cache copy and it's broken) |
|
|
581
|
+
| `engine-mcp` | The engine copy `npx -y amicus@latest mcp` would actually launch (catches a broken npx-cache copy a healthy local install would hide, and a version-skewed one — present but the wrong opencode-ai release vs. the global install, #133) | warn (error only if there's exactly one npx-cache copy and it's broken; also warns, never errors, on engine version skew between the npx copies and the global install) |
|
|
532
582
|
| `electron` | Electron (the interactive GUI) is installed | warn — headless still works |
|
|
533
583
|
| `skills` | Both skills exist under `~/.claude/skills/` | warn |
|
|
534
584
|
| `mcp` | Amicus is registered as an MCP server in Claude Code | warn |
|
|
@@ -575,7 +625,7 @@ Reads `~/.config/amicus/spend-ledger.jsonl` (one row per completed run/leg) and
|
|
|
575
625
|
| `--model <id-or-prefix>` | rows whose model id starts with the given string |
|
|
576
626
|
| `--op <op>` | rows for one operation (`start`, `leg`, `continue`, `resume`, …) |
|
|
577
627
|
| `--failed` | rows with an explicit non-`complete` status (see the caveat below) |
|
|
578
|
-
| `--group-by <dim>` | bucket totals by `model` (default) \| `wave` \| `council` \| `project` \| `op` \| `day` |
|
|
628
|
+
| `--group-by <dim>` | bucket totals by `model` (default) \| `wave` \| `council` \| `project` \| `op` \| `day` \| `tag` (untagged rows group under `(unattributed)`) |
|
|
579
629
|
| `--rows` | also emit the raw filtered rows (capped at 1000; `--json` sets `rowsTruncated: true` past the cap) |
|
|
580
630
|
|
|
581
631
|
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>
|
|
@@ -94,9 +94,27 @@
|
|
|
94
94
|
];
|
|
95
95
|
}
|
|
96
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
|
+
|
|
97
113
|
/** Terminal fallback: derive seat-shaped rows from tally runStats cost rows. */
|
|
98
114
|
function seatsFromRunStats(costRows) {
|
|
99
|
-
return (costRows || []).
|
|
115
|
+
return (costRows || []).filter(function (r) {
|
|
116
|
+
return !SEATS_PANEL_EXCLUDED_ROLES[r.role];
|
|
117
|
+
}).map(function (r) {
|
|
100
118
|
return {
|
|
101
119
|
// ⚠️ DE-ROT (F37): composite id — a v4.1 `--debate` run emits extra runStats rows for
|
|
102
120
|
// the SAME bench alias (role 'rebuttal'/'revote', src/council/debate.js:88-96). With no
|
|
@@ -130,13 +148,23 @@
|
|
|
130
148
|
* loss, an out-of-range index, or a zero-model unit) carry neither, so they
|
|
131
149
|
* correctly fall back to the plain phrasing.
|
|
132
150
|
*
|
|
133
|
-
* D6 filter (zero usable legs ONLY, "no ghost when a
|
|
134
|
-
* candidate already present in `liveSeats`
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
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.
|
|
140
168
|
*
|
|
141
169
|
* Role-aware D6 (v4.6.3 PR2, spec D3): a bare model match used to suppress
|
|
142
170
|
* regardless of what the LIVE row's role was — so a model that died as
|
|
@@ -152,9 +180,9 @@
|
|
|
152
180
|
* suppression below (`byRole[alias + '|critic']`) relies on `roleFor`'s
|
|
153
181
|
* critic branch (src/council/run-stages.js), which only fires when lenses
|
|
154
182
|
* are absent — safe today only because --critic and --lenses are mutually
|
|
155
|
-
* exclusive (cli-handlers-council-run.js
|
|
156
|
-
* loosens, a healed critic on a lens run would carry
|
|
157
|
-
* suppression would render a ghost dead row for it.
|
|
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.
|
|
158
186
|
*
|
|
159
187
|
* Old-run resilience (v4.6.3 PR2, spec D4): pre-`degrades[]` runs (v4.5.2)
|
|
160
188
|
* carry the BENCH half of a seat loss only in `seatLoss.deadBenchSeats`
|
|
@@ -180,7 +208,12 @@
|
|
|
180
208
|
*/
|
|
181
209
|
function deadSeats(degrades, seatLoss, liveSeats, runMeta) {
|
|
182
210
|
var critic = runMeta && runMeta.critic ? runMeta.critic : null;
|
|
183
|
-
|
|
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);
|
|
184
217
|
var order = [];
|
|
185
218
|
function add(model, retried, role) {
|
|
186
219
|
if (!model || seen[model]) { return; }
|
|
@@ -220,12 +253,17 @@
|
|
|
220
253
|
// a dead-critic candidate is cleared only by a live CRITIC leg. A null
|
|
221
254
|
// role is NOT reviewing: counting it would suppress silently, the exact
|
|
222
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.
|
|
223
261
|
function isReviewing(role) {
|
|
224
262
|
return role === 'seat' || role === 'critic' ||
|
|
225
263
|
(typeof role === 'string' && role.indexOf('lens:') === 0);
|
|
226
264
|
}
|
|
227
|
-
var reviewing =
|
|
228
|
-
var byRole =
|
|
265
|
+
var reviewing = Object.create(null);
|
|
266
|
+
var byRole = Object.create(null);
|
|
229
267
|
// ⚠️ Fable review (PR4b fix wave): same F34/F36 alias-selection seatCells already uses
|
|
230
268
|
// (`seat.modelInput || seat.model`, above) — a LIVE payload seat's `model` is the RESOLVED
|
|
231
269
|
// executable id, not the alias a degrade record names; `modelInput` carries the alias.
|
|
@@ -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,6 +69,13 @@
|
|
|
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) {
|
|
70
80
|
// F09 guard (v4.6.3 PR2): a reply for a run the user has since
|
|
71
81
|
// navigated away from must never overwrite the run now open. Guard on
|
|
@@ -83,8 +93,9 @@
|
|
|
83
93
|
// navigated away from must never overwrite the run now open. Capture `runId` and check
|
|
84
94
|
// it's still `state.runId` before writing; a rejection (dead channel, closed window) is
|
|
85
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.)
|
|
86
97
|
if (detail && detail.run && detail.run.debate) {
|
|
87
|
-
invoke('workspace:read-artifact', runId, 'debate.json').then(function (res) {
|
|
98
|
+
state.debateFetch = invoke('workspace:read-artifact', runId, 'debate.json').then(function (res) {
|
|
88
99
|
if (state.runId !== runId) { return; }
|
|
89
100
|
try { state.debate = JSON.parse(res.text); } catch (err) { state.debate = null; }
|
|
90
101
|
}).catch(function () {
|
|
@@ -133,7 +144,7 @@
|
|
|
133
144
|
state.detailRunStatus = d.run.status;
|
|
134
145
|
}
|
|
135
146
|
$('blind-toggle').checked = state.blind;
|
|
136
|
-
state.labelByModel =
|
|
147
|
+
state.labelByModel = Object.create(null);
|
|
137
148
|
d.derived.names.forEach(function (p) { state.labelByModel[p.model] = p.label; });
|
|
138
149
|
|
|
139
150
|
$('run-title').textContent = d.runId;
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Council Workspace — lazy prose-panel loading (v4.4 §5.2). v4.7 PR7 extraction of the
|
|
3
|
+
* loading machinery out of workspace-panels.js, which was at 294/300 with the T19 stale-paint
|
|
4
|
+
* fixes still to land; the same treatment workspace-seats.js got in v4.6.2 PR4 (D8).
|
|
5
|
+
*
|
|
6
|
+
* Split line: this file owns WHEN and WHETHER an artifact read is issued and which reply is
|
|
7
|
+
* allowed to paint (the `loading` promise cache, the run/issue staleness fences).
|
|
8
|
+
* workspace-panels.js keeps NAME RESOLUTION (sanitizeName / resolveArtifactName — the RN-1
|
|
9
|
+
* disambiguation pair, pinned by tests/electron/workspace-ui-static.test.js) and the panel
|
|
10
|
+
* adapters. Cross-calls resolve `window.Amicus*` at CALL time, never at this file's load time
|
|
11
|
+
* — the house discipline for every renderer script.
|
|
12
|
+
*
|
|
13
|
+
* Loads AFTER workspace-seats.js and BEFORE workspace-panels.js (index.html), whose
|
|
14
|
+
* wireLazyPanels/proseLoader are thin delegates into this namespace.
|
|
15
|
+
*/
|
|
16
|
+
(function () {
|
|
17
|
+
'use strict';
|
|
18
|
+
|
|
19
|
+
// ---- lazy prose panels (spec §5.2: load on first open; NOT cached across a same-run rewire
|
|
20
|
+
// — a blind flip or the live loop's terminal refresh both drop it, T19-m1) -----------------
|
|
21
|
+
// ⚠️ DE-ROT (F09): a NEW toggle listener stacking on every renderDetail() call is the bug this
|
|
22
|
+
// shape exists to avoid — see wireLazyPanels()/proseLoader() below. Register the three
|
|
23
|
+
// listeners ONCE at boot (workspace-app.js's boot block calls proseLoader per panel id) and
|
|
24
|
+
// dispatch through this module-level `loaders` map, which renderDetail (via wireLazyPanels)
|
|
25
|
+
// overwrites per run.
|
|
26
|
+
//
|
|
27
|
+
// ⚠️ PRE-FLIGHT (P4): the load is AWAITABLE — drillIntoJudge needs to know when it has
|
|
28
|
+
// settled (the old code guessed with setTimeout(render, 300), which could fire before an
|
|
29
|
+
// unbounded N-artifact IPC round trip finished and silently render nothing). loadPanel()
|
|
30
|
+
// is idempotent per panel id and returns its in-flight promise; the promise cache
|
|
31
|
+
// (`loading`) and the per-run spec (`loaders`) are both keyed by panel id. `loading` is
|
|
32
|
+
// dropped by wireLazyPanels() on BOTH a run CHANGE and a same-run rewire (T19-m1, Task 3) —
|
|
33
|
+
// the run-change drop is what stops F09's stale-run artifact requests; the same-run drop is
|
|
34
|
+
// what T19-m1's `issue` token (below) fences against repainting a superseded same-run wave.
|
|
35
|
+
var loaders = {}; // panelId -> {bodyId, files} (rewritten per run by wireLazyPanels)
|
|
36
|
+
var loading = {}; // panelId -> Promise (dropped on every run CHANGE or same-run rewire)
|
|
37
|
+
// Task 19 (RN-5): the run wireLazyPanels() last reset panels/loading for — gates the reset
|
|
38
|
+
// below to run CHANGES only. A same-run call (renderDetail() runs this on every blind toggle
|
|
39
|
+
// too, and on the live loop's terminal refresh) instead drops the cached load for EVERY
|
|
40
|
+
// tracked panel — open or closed (T19-m1, Task 3) — and reissues a fetch for whichever panel
|
|
41
|
+
// is currently open (Fix 1).
|
|
42
|
+
var lastWiredRunId = null;
|
|
43
|
+
// ⚠️ T19-m1 (v4.7 PR7): the only staleness fence used to be the runId captured at issue time,
|
|
44
|
+
// which cannot distinguish two requests issued for the SAME run — a blind flip, a manifest that
|
|
45
|
+
// grew, or any same-run rewire issues a second load while the first is still in flight, and
|
|
46
|
+
// whichever settles LAST won the paint. Monotonic per-panel issue number: the completion handler
|
|
47
|
+
// paints only if it is still the newest issue. Keys are the three fixed panel-id literals (never
|
|
48
|
+
// a model name), so a bare object is safe here — unlike the model-keyed maps in live-model.js.
|
|
49
|
+
var issue = {}; // panelId -> monotonically increasing issue number
|
|
50
|
+
|
|
51
|
+
function loadPanel(panelId, bodyId, files) {
|
|
52
|
+
var A = window.AmicusApp;
|
|
53
|
+
if (loading[panelId]) { return loading[panelId]; }
|
|
54
|
+
// ⚠️ R4 COUNCIL REVIEW (fourth live paid council, major, unanimous): this is the third
|
|
55
|
+
// instance of the F09 class of bug (a stale async response overwriting shared DOM after
|
|
56
|
+
// the user has navigated away) — already fixed once for the toggle-listener stack (F09
|
|
57
|
+
// itself) and once for the fire-and-forget debate.json fetch in workspace-app.js (guards
|
|
58
|
+
// with `if (state.runId !== runId) return;`). wireLazyPanels() clearing `loading[panelId]`
|
|
59
|
+
// on every run switch permits a NEW request to be issued, but never fenced the PRIOR
|
|
60
|
+
// request's eventual resolution — open reviews-panel on run A, switch to run B (which
|
|
61
|
+
// issues its own request), and A's response — however late — used to overwrite whatever
|
|
62
|
+
// B had just rendered. Capture the runId this request was issued for, and guard as the
|
|
63
|
+
// FIRST statement of the completion handler, exactly like the debate.json fix.
|
|
64
|
+
var runId = A.state.runId;
|
|
65
|
+
// ⚠️ T19-m1 (v4.7 PR7): captured the same way as `runId` above — read once, at issue time —
|
|
66
|
+
// so the completion guard below compares against the value in force when THIS request was
|
|
67
|
+
// issued, not whatever `issue[panelId]` has become by the time it resolves.
|
|
68
|
+
var token = (issue[panelId] = (issue[panelId] || 0) + 1);
|
|
69
|
+
var pending = Promise.all(files().map(function (f) {
|
|
70
|
+
return A.invoke('workspace:read-artifact', runId, f.name).then(function (res) {
|
|
71
|
+
return { name: f.name, title: f.title, text: res.text || '', truncated: res.truncated, error: res.error };
|
|
72
|
+
});
|
|
73
|
+
})).then(function (sections) {
|
|
74
|
+
if (A.state.runId !== runId || issue[panelId] !== token) { return; } // stale: superseded
|
|
75
|
+
window.AmicusRender.renderProseSections(A.$(bodyId), sections.map(function (s) {
|
|
76
|
+
return s.error ? { name: s.name, title: s.title, error: s.name + ' — ' + s.error } : s;
|
|
77
|
+
}));
|
|
78
|
+
A.$(panelId).dataset.loaded = '1'; // display/debug marker only — `loading` is the real gate
|
|
79
|
+
}, function (err) {
|
|
80
|
+
// ⚠️ T19-m2 (v4.7 PR7). Two-argument .then(onFulfilled, onRejected) — NOT a trailing
|
|
81
|
+
// .catch. workspace-verbs.js:76-84 already ruled on this exact construct: with a trailing
|
|
82
|
+
// .catch a THROW inside onFulfilled is routed here too, so a painter bug would be absorbed
|
|
83
|
+
// into a silent blank panel that ALSO evicts its own cache and therefore retries forever.
|
|
84
|
+
// With the two-argument form this handler only ever sees a genuinely rejected invoke().
|
|
85
|
+
// The `=== pending` self-check is load-bearing: without it a late rejection can evict a
|
|
86
|
+
// NEWER in-flight promise from `loading[panelId]`. That newer promise still resolves and
|
|
87
|
+
// paints fine on its own — it is not stranded — but the eviction leaves no cache entry
|
|
88
|
+
// behind for it, so a toggle that lands before it settles finds `loading[panelId]` empty
|
|
89
|
+
// and fires a spurious duplicate fetch. And the log is not optional — a silent
|
|
90
|
+
// eviction is the correct-but-silent degrade the product principle rejects.
|
|
91
|
+
if (loading[panelId] === pending) { delete loading[panelId]; }
|
|
92
|
+
console.error('workspace lazy panel: read-artifact failed for ' + panelId, err);
|
|
93
|
+
});
|
|
94
|
+
loading[panelId] = pending;
|
|
95
|
+
return pending;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Registered ONCE at boot (per panel id); reads the current run's spec off `loaders`. */
|
|
99
|
+
function proseLoader(panelId) {
|
|
100
|
+
var A = window.AmicusApp;
|
|
101
|
+
var panel = A.$(panelId);
|
|
102
|
+
panel.addEventListener('toggle', function () {
|
|
103
|
+
if (!panel.open) { return; }
|
|
104
|
+
var spec = loaders[panelId];
|
|
105
|
+
if (spec) { loadPanel(panelId, spec.bodyId, spec.files); }
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Rewrites the per-run spec map on every call. On a run CHANGE (tracked via the module-level
|
|
111
|
+
* `lastWiredRunId`, above), resets panel open/loaded state and drops the previous run's
|
|
112
|
+
* cached load promises — exactly what F09's stale-run protection needs. On a SAME-run call
|
|
113
|
+
* (Task 19, RN-5: renderDetail() calls this on every blind toggle too, and the live loop's
|
|
114
|
+
* terminal refresh) any panel the user already has open is instead refreshed in place — see
|
|
115
|
+
* Fix 1 below — never left showing stale-blind content, never collapsed. Registers no
|
|
116
|
+
* listeners itself.
|
|
117
|
+
*/
|
|
118
|
+
function wireLazyPanels() {
|
|
119
|
+
var A = window.AmicusApp;
|
|
120
|
+
// ⚠️ Fix-wave (Fix 4): keyed off `A.state.detail.runId`, not `A.state.runId` — the latter is
|
|
121
|
+
// set synchronously at the top of openRun(), before its workspace:get-run reply lands, so an
|
|
122
|
+
// out-of-order reply could make the two diverge. workspace-app.js's own run-change gate
|
|
123
|
+
// (renderDetail(), above `d.runId`) reads off the SAME `state.detail.runId`, so the two
|
|
124
|
+
// provably agree on whether this is a run change.
|
|
125
|
+
var sameRun = A.state.detail.runId === lastWiredRunId;
|
|
126
|
+
if (!sameRun) {
|
|
127
|
+
['reviews-panel', 'bundle-panel', 'judges-panel'].forEach(function (id) {
|
|
128
|
+
var p = A.$(id);
|
|
129
|
+
p.dataset.loaded = '0';
|
|
130
|
+
p.open = false;
|
|
131
|
+
delete loading[id];
|
|
132
|
+
});
|
|
133
|
+
lastWiredRunId = A.state.detail.runId;
|
|
134
|
+
}
|
|
135
|
+
var bench = A.state.detail.run.bench || [];
|
|
136
|
+
var debated = !!A.state.detail.run.debate;
|
|
137
|
+
// ⚠️ CODE REVIEW (round 2, finding 2): readRunArtifact's error for a genuinely-missing
|
|
138
|
+
// artifact is NOT translated into a friendly "not written yet" note anywhere in this
|
|
139
|
+
// read path — it lands in the panel verbatim, absolute host path and all. `run.debate` is
|
|
140
|
+
// seeded on run.json's FIRST write, so it's truthy on every --debate run, including ones
|
|
141
|
+
// where the re-vote wave never actually ran (no contested findings, cost ceiling, abort) —
|
|
142
|
+
// requesting revote-<model>.md speculatively in that (near-certain) case means one ugly
|
|
143
|
+
// error row per bench model for a condition that isn't an error at all. run-detail.js
|
|
144
|
+
// already computes a presence manifest (state.detail.artifacts) for exactly these
|
|
145
|
+
// allowlisted names via fs.statSync — filter on it instead of requesting known-absent
|
|
146
|
+
// files. Applies to review-/judge- too (the same latent gap, just plan-mandated rather
|
|
147
|
+
// than new).
|
|
148
|
+
var artifacts = A.state.detail.artifacts || {};
|
|
149
|
+
function present(name) { return !!(artifacts[name] && artifacts[name].present); }
|
|
150
|
+
// ⚠️ v4.4.1 RN-9: these two titles used to hand-roll `A.state.blind && label ? label : m`
|
|
151
|
+
// inline. Both now go through AmicusRender.display() — the single blind-flip definition the
|
|
152
|
+
// re-vote title below already used — so the next blind-mode ruling lands in one place instead
|
|
153
|
+
// of being re-applied by hand in every file that happens to render an identity.
|
|
154
|
+
loaders['reviews-panel'] = { bodyId: 'reviews-body', files: function () {
|
|
155
|
+
return bench.map(function (m) {
|
|
156
|
+
var label = A.state.labelByModel[m];
|
|
157
|
+
return { name: window.AmicusPanels.resolveArtifactName(m, 'review'), title: window.AmicusRender.display({ model: m, label: label }, A.state.blind) };
|
|
158
|
+
}).filter(function (f) { return present(f.name); });
|
|
159
|
+
} };
|
|
160
|
+
loaders['bundle-panel'] = { bodyId: 'bundle-body', files: function () {
|
|
161
|
+
// ⚠️ v4.4.1 RN-4: the presence filter is NOT optional here either. Without it, a run whose
|
|
162
|
+
// Stage 2 never ran (a one-seat bench, an abort before the cross-review, a cost ceiling)
|
|
163
|
+
// requested a file the manifest already knows is absent and rendered readRunArtifact's raw
|
|
164
|
+
// error string in the panel — "absolute host path and all", per this file's own round-2
|
|
165
|
+
// note above `present()`. reviews-panel and judges-panel have always filtered; this was the
|
|
166
|
+
// odd one out.
|
|
167
|
+
return [{ name: 'bundle-stage2.md', title: 'bundle-stage2.md (verbatim)' }]
|
|
168
|
+
.filter(function (f) { return present(f.name); });
|
|
169
|
+
} };
|
|
170
|
+
loaders['judges-panel'] = { bodyId: 'judges-body', files: function () {
|
|
171
|
+
var files = bench.map(function (m) {
|
|
172
|
+
var label = A.state.labelByModel[m];
|
|
173
|
+
return { name: window.AmicusPanels.resolveArtifactName(m, 'judge'), title: 'Judge ' + window.AmicusRender.display({ model: m, label: label }, A.state.blind) };
|
|
174
|
+
});
|
|
175
|
+
if (debated) {
|
|
176
|
+
// ⚠️ DE-ROT (F38): on a --debate run, a matrix dispute cell can be a RE-VOTE whose
|
|
177
|
+
// prose lives in revote-<model>.md (not judge-<model>.md). Included per bench model
|
|
178
|
+
// like judge-*.md above, but — per the presence filter — only when the manifest
|
|
179
|
+
// confirms the file actually exists (see the code-review note above `present()`).
|
|
180
|
+
// ⚠️ CODE REVIEW (round 2, finding 3): this title is new code (unlike the review-/
|
|
181
|
+
// judge- titles above, which mirror the brief verbatim), so it goes through
|
|
182
|
+
// AmicusRender.display() — the single blind-flip definition — rather than adding a
|
|
183
|
+
// fourth hand-rolled copy of the same ternary.
|
|
184
|
+
// ⚠️ Task 18 fix-wave (RN-1, review finding 1): this name used to be recomputed via a
|
|
185
|
+
// bare sanitizeName(m) call, ignoring the disambiguation map entirely — for a colliding
|
|
186
|
+
// pair BOTH models resolved to the same bare revote-<sanitized>.md name, reintroducing
|
|
187
|
+
// for re-votes the exact cross-match bug Task 18 fixed for review-/judge-. Routed
|
|
188
|
+
// through resolveArtifactName(m, 'revote') like the other three sites; its built-in
|
|
189
|
+
// legacy fallback keeps older detail payloads (no artifactsByModel map) correct too.
|
|
190
|
+
files = files.concat(bench.map(function (m) {
|
|
191
|
+
var label = A.state.labelByModel[m];
|
|
192
|
+
return { name: window.AmicusPanels.resolveArtifactName(m, 'revote'), title: 'Re-vote ' + window.AmicusRender.display({ model: m, label: label }, A.state.blind) };
|
|
193
|
+
}));
|
|
194
|
+
}
|
|
195
|
+
return files.filter(function (f) { return present(f.name); });
|
|
196
|
+
} };
|
|
197
|
+
// ⚠️ Fix-wave (Fix 1, RN-9): a same-run call (the blind toggle, or the live loop's
|
|
198
|
+
// running -> terminal refresh) must re-render any panel the user already has open, or it
|
|
199
|
+
// keeps showing content painted under the PREVIOUS blind state. renderProseSections()
|
|
200
|
+
// (workspace-render.js) clears its container before repainting, so this replaces sections
|
|
201
|
+
// in place rather than appending duplicates. Drop the cached promise first so loadPanel()
|
|
202
|
+
// actually re-fetches instead of returning its already-settled one.
|
|
203
|
+
if (sameRun) {
|
|
204
|
+
['reviews-panel', 'bundle-panel', 'judges-panel'].forEach(function (id) {
|
|
205
|
+
var p = A.$(id);
|
|
206
|
+
// ⚠️ T19-m1 (v4.7 PR7): the cache drop used to be INSIDE the `p.open` guard, so a panel
|
|
207
|
+
// the user had collapsed kept its settled promise across a blind flip — reopening it
|
|
208
|
+
// returned that promise and repainted the previous blind state with no new fetch (recon
|
|
209
|
+
// path A). Dropping unconditionally closes path A — but by itself it only CONVERTS path D
|
|
210
|
+
// (collapse mid-flight) into a race: it drops the cache entry even while that panel's
|
|
211
|
+
// fetch is still outstanding, so a reopen before it settles issues a SECOND concurrent
|
|
212
|
+
// fetch, and the orphaned first one has no fence but `runId` (unchanged for a same-run
|
|
213
|
+
// rewire). `issue` (declared above) and loadPanel()'s completion guard are what actually
|
|
214
|
+
// close D. Unconditional drop costs a re-read of that panel's artifacts on the next open;
|
|
215
|
+
// renderDetail fires on run open, blind toggle, and the live loop's terminal refresh only
|
|
216
|
+
// (the tick calls applyLive, not renderDetail), so this is not a per-poll storm.
|
|
217
|
+
delete loading[id];
|
|
218
|
+
if (p.open) { loadPanel(id, loaders[id].bodyId, loaders[id].files); }
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// `loaders` stays module-private; drillIntoJudge (still in workspace-panels.js) needs to ask
|
|
224
|
+
// whether this run has a spec for a panel, and nothing else needs the map itself.
|
|
225
|
+
function panelSpec(panelId) { return loaders[panelId] || null; }
|
|
226
|
+
|
|
227
|
+
window.AmicusLazy = {
|
|
228
|
+
loadPanel: loadPanel,
|
|
229
|
+
proseLoader: proseLoader,
|
|
230
|
+
wireLazyPanels: wireLazyPanels,
|
|
231
|
+
panelSpec: panelSpec,
|
|
232
|
+
};
|
|
233
|
+
})();
|
|
@@ -76,7 +76,18 @@
|
|
|
76
76
|
'aria-label': verdictTitle(cell),
|
|
77
77
|
}, [cell.sym + (cell.isRaiser ? '*' : '')]);
|
|
78
78
|
if (cell.verdict === 'dispute') {
|
|
79
|
-
|
|
79
|
+
// ⚠️ T19-m2 (v4.7 PR7): drillIntoJudge's derived promise was discarded here, so a
|
|
80
|
+
// rejection — or a throw inside its own post-load body, which loadPanel's onRejected
|
|
81
|
+
// does NOT cover — escaped unhandled. `Promise.resolve(onDrill(...))` would NOT fix the
|
|
82
|
+
// synchronous half: onDrill(...) is evaluated BEFORE Promise.resolve sees it, so the
|
|
83
|
+
// throw escapes anyway (measured). Calling it inside the .then callback moves both the
|
|
84
|
+
// sync and async failure modes onto the chain. A trailing .catch is correct HERE
|
|
85
|
+
// (unlike loadPanel — see workspace-verbs.js:76-84) because there is no separate
|
|
86
|
+
// onFulfilled whose throw it could absorb: the callback IS the whole operation.
|
|
87
|
+
td.addEventListener('click', function () {
|
|
88
|
+
Promise.resolve().then(function () { return onDrill(cell.judge, row.id); })
|
|
89
|
+
.catch(function (err) { console.error('workspace matrix: drill into judge failed', err); });
|
|
90
|
+
});
|
|
80
91
|
}
|
|
81
92
|
cells.push(td);
|
|
82
93
|
});
|