amicus 1.1.0 → 1.2.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/marketplace.json +14 -0
- package/.claude-plugin/plugin.json +19 -0
- package/CHANGELOG.md +53 -0
- package/LICENSE +22 -1
- package/README.md +12 -2
- package/bin/amicus.js +13 -161
- package/package.json +6 -4
- package/scripts/postinstall.js +16 -7
- package/skills/second-opinion/COUNCIL-DESIGN.md +34 -34
- package/skills/second-opinion/MODEL-NOTES.md +23 -17
- package/skills/second-opinion/SKILL.md +77 -45
- package/src/cli-handlers-council.js +59 -0
- package/src/cli-handlers-doctor.js +173 -0
- package/src/cli-handlers-run.js +196 -0
- package/src/cli-handlers.js +1 -1
- package/src/cli.js +11 -2
- package/src/council/findings.js +48 -0
- package/src/council/ledger.js +82 -0
- package/src/council/tally.js +108 -0
- package/src/council/verdict.js +48 -0
- package/src/headless.js +43 -149
- package/src/mcp-server.js +6 -0
- package/src/sidecar/budget.js +83 -0
- package/src/sidecar/conversation-mirror.js +128 -0
- package/src/sidecar/fanout-leg.js +4 -1
- package/src/sidecar/fanout.js +34 -7
- package/src/sidecar/interactive-mirror.js +66 -0
- package/src/sidecar/interactive.js +35 -21
- package/src/sidecar/models.js +10 -9
- package/src/sidecar/session-finalize.js +26 -0
- package/src/sidecar/session-utils.js +5 -5
- package/src/sidecar/setup.js +2 -2
- package/src/sidecar/start.js +19 -6
- package/src/utils/activity-poller.js +47 -0
- package/src/utils/alias-resolver.js +1 -1
- package/src/utils/config.js +4 -4
- package/src/utils/error-doc.js +55 -0
- package/src/utils/lifecycle.js +1 -1
- package/src/utils/model-catalog.js +1 -1
- package/src/utils/pricing.js +93 -0
- package/src/utils/result-schema.js +21 -2
- package/src/utils/session-abort.js +40 -13
- package/src/utils/validators.js +17 -17
- /package/{skill → skills/sidecar}/SKILL.md +0 -0
|
@@ -33,7 +33,9 @@ _Last updated: 2026-06-10 (v3 migration: engine workarounds pruned — see chang
|
|
|
33
33
|
names — so rankings are directly comparable (one fanout wave distributes it).
|
|
34
34
|
- **Require a `FINAL RANKING:` block** at the end of the response (e.g. `1. Review C / 2. Review
|
|
35
35
|
A …`), plus a per-finding `agree | dispute | neutral` verdict with a one-line reason for each
|
|
36
|
-
finding referenced by label
|
|
36
|
+
finding referenced by run-global label id (e.g. `A2` = Review A's 2nd finding).
|
|
37
|
+
- After de-anonymizing, assemble the tally input (see SKILL.md Stage 2 assembly recipe) and run
|
|
38
|
+
`amicus council tally <input.json> --json` — do not hand-tally tiers or street-cred numbers.
|
|
37
39
|
|
|
38
40
|
## Per-model notes
|
|
39
41
|
|
|
@@ -59,28 +61,32 @@ _Last updated: 2026-06-10 (v3 migration: engine workarounds pruned — see chang
|
|
|
59
61
|
- Opus / o-series etc. are reachable via amicus **if their API keys are configured**. Add notes
|
|
60
62
|
here the first time each is used.
|
|
61
63
|
|
|
62
|
-
## Reviewer-reliability
|
|
64
|
+
## Reviewer-reliability
|
|
63
65
|
|
|
64
|
-
|
|
66
|
+
Quantitative reliability data (runs, avg peers-only street-cred, confirm-rate, fact-error rate, conformance distribution) is now generated from the append-only `council-ledger.jsonl` via:
|
|
65
67
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
outweigh disputes, ≥ 2 judges engaged).
|
|
68
|
+
```
|
|
69
|
+
amicus council stats [--json]
|
|
70
|
+
```
|
|
70
71
|
|
|
71
|
-
|
|
72
|
-
| --- | --- | --- | --- | --- |
|
|
73
|
-
| deepseek | 1 | 2.33 | 100% (12/12) | strong synthesis, resilient; chaired well |
|
|
74
|
-
| gpt | 1 | 2.67 | 92% (23/25) | thorough but verbose; self-ranked #1 → discount; OpenRouter |
|
|
75
|
-
| gemini | 1 | 3.67 | 89% (8/9) | fast, large-context; more absolute/adversarial ("blocker" inflation); ranked lowest |
|
|
72
|
+
**Do not hand-edit reliability numbers here.** The ledger is the authoritative source; `amicus council stats` aggregates it and flags low-N models (`runs < 3`). Consult `amicus council stats` in Stage 0 for bench recommendations. In Stage 6 the ledger row is appended automatically — no manual update needed.
|
|
76
73
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
74
|
+
This section keeps only per-model **qualitative quirks** and **structural-conformance notes** (`clean` / `repaired` / `unstructured`), which cannot be captured by the ledger:
|
|
75
|
+
|
|
76
|
+
### Qualitative notes (hand-curated)
|
|
77
|
+
|
|
78
|
+
- **deepseek** — strong synthesis, resilient; occasional transient 502 → re-run the leg. Proven chair. Conforms cleanly.
|
|
79
|
+
- **gpt** — thorough but verbose; peers have dinged it for volume-over-judgment. Self-ranked its own review #1 in the 2026-06-04 run → the peers-only street-cred rule (now enforced by `tally`) mitigates this. Conforms cleanly. Accessible via OpenRouter.
|
|
80
|
+
- **gemini** — fast, very large context; tends toward absolute severity labels ("blocker" inflation vs peers). Conforms cleanly; watch for preamble narration — instruct it to emit the JSON block verbatim after the prose.
|
|
80
81
|
|
|
81
82
|
## Cost guardrail
|
|
82
|
-
-
|
|
83
|
-
|
|
83
|
+
- The budget gate enforces this in code: a per-$/Mtok threshold (ON by default)
|
|
84
|
+
refuses o3/o3-pro-class models before a wave launches. This replaces the old
|
|
85
|
+
"remember not to" rule — it can no longer be forgotten.
|
|
86
|
+
- To run `o3`/`o3-pro` (≈ $10–60+/request) the user must ask by name AND you
|
|
87
|
+
pass `--no-cost-gate` (disables both guards) for that run. Still warn first.
|
|
88
|
+
- `--max-cost <$>` raises only the soft total ceiling; it does not unblock an
|
|
89
|
+
over-threshold model.
|
|
84
90
|
|
|
85
91
|
## General
|
|
86
92
|
- Model citations are usually real but **verify any load-bearing reference before publishing**;
|
|
@@ -54,11 +54,11 @@ in this run is written here. Use its absolute path in all `--prompt-file` argume
|
|
|
54
54
|
`--prompt-file` — never inline a briefing as a CLI argument. All `_tmp-*` files are cleaned up
|
|
55
55
|
after the run.
|
|
56
56
|
|
|
57
|
-
**Pick the council.** Default: **3 models from different families (non-Claude)**. Recommend them ranked by fit, consulting the reviewer-reliability
|
|
57
|
+
**Pick the council.** Default: **3 models from different families (non-Claude)**. Recommend them ranked by fit, consulting both the reviewer-reliability data from `amicus council stats` (the authoritative quantitative source — runs, avg peers-only street-cred, confirm-rate, fact-error rate) and the qualitative quirks in `MODEL-NOTES.md`. State the estimated cost. The estimate is the budget gate's pre-flight figure (per-$/Mtok pricing from the cached catalog; direct-provider legs without catalog pricing are disclosed as "cost unknown"). State it as an estimate, not a guarantee. **Disclose the run shape up front** before asking for confirmation — e.g.:
|
|
58
58
|
|
|
59
59
|
> This run uses 3 council models across 2 fanout waves + 1 chair call (~7 model runs), ~10 min.
|
|
60
60
|
|
|
61
|
-
Then **wait for confirmation**. Never launch without it.
|
|
61
|
+
Then **wait for confirmation**. Never launch without it. The budget gate enforces the cost guardrail in code: by default it refuses any leg whose price exceeds the per-$/Mtok threshold (the o3/o3-pro guard). To run an intentionally expensive model the user explicitly asked for by name, pass `--no-cost-gate`; to raise only the total ceiling, pass `--max-cost <$>`.
|
|
62
62
|
|
|
63
63
|
**Scale-down is explicit — state which mode applies:**
|
|
64
64
|
- **1 model** → thorough single pass; Stage 2 (cross-review) and Stage 3 (chair synthesis) are skipped entirely; Claude synthesizes directly. Transport: a single solo `amicus start --no-ui --json` (no fanout).
|
|
@@ -110,18 +110,32 @@ equivalent.
|
|
|
110
110
|
|
|
111
111
|
**Required structured output from every model.** Instruct each council model to produce:
|
|
112
112
|
|
|
113
|
-
1. A **
|
|
114
|
-
- `id` — sequential integer within this review (1, 2, 3…)
|
|
115
|
-
- `claim` — the specific issue or observation
|
|
116
|
-
- `severity` — one of: `blocker | major | minor | nit`
|
|
117
|
-
- `location` — section heading or verbatim quote identifying where in the artifact
|
|
118
|
-
- `rationale` — why this is a problem or worth noting
|
|
113
|
+
1. A **prose review** — the reviewer's full narrative assessment of the artifact.
|
|
119
114
|
|
|
120
|
-
2. A **
|
|
115
|
+
2. A **trailing fenced ` ```json ` block** immediately after the prose, containing:
|
|
116
|
+
```json
|
|
117
|
+
{
|
|
118
|
+
"overall": "one-paragraph take",
|
|
119
|
+
"findings": [
|
|
120
|
+
{ "id": 1, "severity": "blocker",
|
|
121
|
+
"claim": "…", "location": "…", "rationale": "…" }
|
|
122
|
+
]
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
- `id` — sequential integer within this review (`1..n`); at Stage-2 assembly Claude rewrites each into a **run-global label id** (`A1`, `B1`, …) by prefixing the review's anonymized label.
|
|
126
|
+
- `severity ∈ {blocker, major, minor, nit}`
|
|
127
|
+
- `claim`, `location`, `rationale` — non-empty strings.
|
|
121
128
|
|
|
122
|
-
Instruct models to emit the structured
|
|
129
|
+
Instruct models to emit the structured JSON verbatim after the prose, without preamble, so it parses cleanly.
|
|
123
130
|
|
|
124
|
-
|
|
131
|
+
**After the wave returns, validate each leg's findings block** using `validateFindings` (Unit A — `src/council/findings.js`). If a leg's JSON fails validation:
|
|
132
|
+
1. Issue a **solo `start --json`** re-prompt to that one model: "re-emit only the findings JSON, fixing: \<errors\>." Keep the first-pass prose. (Solo `start` is **not** subject to the WS-2 fanout cost gate, so a repair cannot be refused mid-council.)
|
|
133
|
+
2. If still malformed, retry **once more** (cap = **2** re-prompts total).
|
|
134
|
+
3. If still malformed after 2 retries, mark the review `unstructured` and hand-parse its prose into the schema. The review proceeds — never dropped for a formatting miss.
|
|
135
|
+
|
|
136
|
+
Record per-model **conformance** (`clean` | `repaired` | `unstructured`) for inclusion in the tally input's `runStats` and the Stage-6 MODEL-NOTES note.
|
|
137
|
+
|
|
138
|
+
Save each leg's full output (prose + findings block) to the run folder as `review-<model>.md`
|
|
125
139
|
(one file per reviewer) before moving on.
|
|
126
140
|
|
|
127
141
|
**"Claude in the council" (when toggled on):** Claude also produces a **fresh** Stage-1 review on the artifact in the identical findings format — a new structured pass on the artifact, not a formalization of anything said upstream. This review is added to the bundle as one more anonymous entry. Claude does not rank or adjudicate in Stage 2 (it holds the label map), and does not chair in Stage 3. Save it as `review-claude.md`.
|
|
@@ -172,7 +186,21 @@ FINAL RANKING:
|
|
|
172
186
|
|
|
173
187
|
**Task B — Adjudicate findings.** For every finding in the bundle, state: `agree | dispute | neutral` plus one-line reason. Reference each finding as **review-label + finding-id** — for example, `A2` means Review A's 2nd finding, `B1` means Review B's 1st finding. An "I missed this — it's valid" counts as `agree`.
|
|
174
188
|
|
|
175
|
-
As each judge's ranking + adjudication response returns, collect it (the raw per-judge responses are working intermediates, not separate run-folder artifacts). Once all are in,
|
|
189
|
+
As each judge's ranking + adjudication response returns, collect it (the raw per-judge responses are working intermediates, not separate run-folder artifacts). Once all are in, **assemble the de-anonymized tally input** and then call `amicus council tally`:
|
|
190
|
+
|
|
191
|
+
**Stage-2 → tally assembly recipe (Claude's work before calling `tally`):**
|
|
192
|
+
1. **Rewrite finding ids to run-global label ids.** Each Stage-1 review's local integer ids (`1`, `2`, `3`…) become `A1`, `A2`, `A3`… (where `A` is that review's anonymized label). The label↔model map (`Review A → deepseek`, etc.) is the key.
|
|
193
|
+
2. **Build `adjudications`** — for every judge across all findings: `findingId` = run-global label id; `judge` = the model id (de-anonymized via the map); `verdict ∈ {agree, dispute, neutral}`. Include every judge's verdict on every finding. The raiser's own adjudication of its own finding is **included in the input** (the tally engine excludes it when computing peers-only tiers — do not pre-filter it).
|
|
194
|
+
3. **Translate each judge's `FINAL RANKING:` block** — convert the label order (`1. Review C / 2. Review A / 3. Review B`) into a model `order` array via the same map (e.g. `{C→mistral, A→deepseek, B→gpt}` ⇒ `order: ["mistral","deepseek","gpt"]`). This is each entry in `rankings[]`.
|
|
195
|
+
4. **Populate `runStats`** from the per-leg run documents emitted by `fanout --json` (and any solo red-team/chair `start --json` docs): copy `model`, `status`, `durationMs`, `usage` verbatim. Any leg with no run doc gets `durationMs: null` and `usage: null` — never invent a value. Attach `role` (`council` | `redteam` | `claude`), `wasChair`, and `conformance` (`clean` | `repaired` | `unstructured`) as council-domain labels.
|
|
196
|
+
|
|
197
|
+
Then call:
|
|
198
|
+
|
|
199
|
+
```
|
|
200
|
+
amicus council tally <run-folder>/tally-input.json --json
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
The output `record` carries the deterministic tiers (Disputed / Confirmed / Contested / Singleton), `confidence` (`solid` | `thin`), both street-cred numbers (`withSelf` and `peersOnly`), the validated `runStats`, and `tierCounts`. **Claude may override a `thin`-confidence tier at the margins** before Stage 4 — record the override in `tierOverride: {from, to, reason}`; the matrix and `verdict.json` surface it. De-anonymize and write the tally results to `crossreview-matrix.md` — the adjudication grid plus the street-cred table. This data feeds Stage 3 (chair briefing) and is never re-anonymized or forwarded to any council model.
|
|
176
204
|
|
|
177
205
|
---
|
|
178
206
|
|
|
@@ -180,7 +208,7 @@ As each judge's ranking + adjudication response returns, collect it (the raw per
|
|
|
180
208
|
|
|
181
209
|
A designated **non-Claude** chair synthesizes the verdict across all reviews, rankings, and adjudications. The chair produces an independent verdict that Claude then presents — Claude does not paraphrase, edit, or re-synthesize it.
|
|
182
210
|
|
|
183
|
-
**Chair selection (confirmed in Stage 0).** Default: Claude recommends the strongest reasoner in the council (guided by
|
|
211
|
+
**Chair selection (confirmed in Stage 0).** Default: Claude recommends the strongest reasoner in the council (guided by `amicus council stats` (peers-only street-cred) and the qualitative quirks in `MODEL-NOTES.md`) and the user confirms before the run launches. The chair may be a council member who already participated in Stages 1 and 2 — it receives the de-anonymized full bundle, all ranking outputs, and all adjudications so it has the complete picture.
|
|
184
212
|
|
|
185
213
|
**Fallback order if the chair fails:**
|
|
186
214
|
1. Re-run the chair call (transient failure — `MODEL-NOTES.md` mitigations apply).
|
|
@@ -212,11 +240,11 @@ Save the chair's output to the run folder as `verdict.md`.
|
|
|
212
240
|
|
|
213
241
|
### Stage 4 — Tiered decisions (peer-validated)
|
|
214
242
|
|
|
215
|
-
All findings from the bundle are sorted into
|
|
243
|
+
All findings from the bundle are sorted into tiers based on the **peer-confidence tier assigned by `amicus council tally`** (see *Key mechanics → §5.2 Scoring* in COUNCIL-DESIGN.md for the full cascade): **Disputed** (strong peer pushback — `d ≥ 2` and `d > a`), **Confirmed** (≥ 2 peer agreements, agrees dominate), **Contested** (at least one live dispute), **Singleton** (at most one endorsement, no pushback). `confidence: thin` cells `(0,0)/(1,0)/(0,1)` are override-eligible (Claude records any override in `tierOverride`). Present the tiers in this order: Confirmed first (bulk decision), then Disputed and Contested and Singleton individually in the judgment tier.
|
|
216
244
|
|
|
217
245
|
**Scale-down:** In a 1-model run, Stage 2 was skipped — there is no peer-confidence data, so present every finding individually for decision (no tiers). In a 2-model run, the Confirmed tier rests on thin cross-review (one ranker per review, per Stage 0) — say so when presenting it.
|
|
218
246
|
|
|
219
|
-
**Consensus tier — Confirmed findings** (
|
|
247
|
+
**Consensus tier — Confirmed findings** (≥ 2 peer agreements, agrees dominate)
|
|
220
248
|
|
|
221
249
|
- Present the full list in one block: id, claim, severity, and which models raised / endorsed it.
|
|
222
250
|
- Offer one **bulk accept/deny decision** over the whole tier:
|
|
@@ -225,12 +253,13 @@ All findings from the bundle are sorted into two tiers based on the **peer-confi
|
|
|
225
253
|
|
|
226
254
|
- The user may accept the block, deny the block, or enumerate exceptions. Handle exceptions individually before moving on.
|
|
227
255
|
|
|
228
|
-
**Judgment tier — Contested and Singleton findings**
|
|
256
|
+
**Judgment tier — Disputed, Contested, and Singleton findings**
|
|
229
257
|
|
|
230
|
-
This is one tier with
|
|
258
|
+
This is one tier with three sub-types presented separately. Present each finding individually. Handle the sub-types distinctly:
|
|
231
259
|
|
|
232
|
-
- **
|
|
233
|
-
- **
|
|
260
|
+
- **Disputed** (`d ≥ 2` and `d > a` — strong peer pushback, the finding itself may be wrong): For each finding show the claim, severity, which model raised it, which peers dispute it and why. Ask for a decision before proceeding to the next: **accept / deny / modify**.
|
|
261
|
+
- **Contested** (`d ≥ 1` with a meaningful split): For each finding show the claim and severity, which model raised it, who agreed, who disputed, and the one-line reasons from the adjudications. Ask for a decision before proceeding to the next: **accept / deny / modify**.
|
|
262
|
+
- **Singleton** (only the original raiser; all other judges were neutral or silent — `d = 0` and `a < 2`): For each finding show the claim and severity and that no other judge engaged with it. Name the sole raiser. Ask for a decision before proceeding to the next: **accept / deny / modify**.
|
|
234
263
|
|
|
235
264
|
**Recording decisions.** Keep a running decision log throughout this stage — every finding's outcome (accepted / denied / modified, with any modification noted). This log feeds Stage 5 (only accepted changes go into the reviewed copy) and Stage 6 (the run-folder report).
|
|
236
265
|
|
|
@@ -249,15 +278,19 @@ Do not advance to Stage 5 until every finding in both tiers has a recorded decis
|
|
|
249
278
|
- Do not attempt to produce a modified copy.
|
|
250
279
|
- Write a **standalone reviewed report** instead: the full decision log, the chair's verdict, and clear callouts of what should be changed and where — formatted so the user can apply the changes manually.
|
|
251
280
|
|
|
252
|
-
**Run-folder artifacts — always write these** regardless of source type. The full artifact set and naming conventions are defined in the *Output & naming* section of this skill; write every artifact specified there. The
|
|
281
|
+
**Run-folder artifacts — always write these** regardless of source type. The full artifact set and naming conventions are defined in the *Output & naming* section of this skill; write every artifact specified there. The canonical run-folder files are:
|
|
253
282
|
- `review-<model>.md` × N (already saved in Stage 1)
|
|
254
283
|
- `crossreview-matrix.md` — the de-anonymized adjudication grid and street-cred table
|
|
255
284
|
- `verdict.md` (already saved in Stage 3)
|
|
285
|
+
- `verdict.json` — write via `buildVerdict(record, decisions)` (`src/council/verdict.js`): pass the tally `record` from Stage 2 and the Stage-4 decision map (accepted / denied / modified / deferred per finding, plus any `duplicateOf` links Claude identified). This is the schema-stamped machine-readable record of the full run. Write it with an atomic tmp+rename to the run folder.
|
|
256
286
|
- `report.md` — the chair's synthesis + the full Stage-4 decision log + a summary of what was
|
|
257
287
|
applied (+ the "How Claude's review fared" readout when "Claude in the council" is on) + a
|
|
258
288
|
**run-stats table**: one row per model call — **stage** (which stage you launched the call for)
|
|
259
|
-
plus **model, status, durationMs** read from the wave/run JSON
|
|
260
|
-
cost
|
|
289
|
+
plus **model, status, durationMs, and cost** read from the wave/run JSON `usage`
|
|
290
|
+
block. Cost is `usage.cost.amount` (USD); mark it with its `usage.cost.source`
|
|
291
|
+
— exact for `reported`, `~` for `estimated`, `?` for `unknown` — and never
|
|
292
|
+
invent a figure. Add a wave **total cost** row from the wave document's
|
|
293
|
+
`usage.cost` (`source: reported|estimated|mixed|unknown`). Any leg with no run doc → `durationMs: null`, `usage: null`; never invent a value.
|
|
261
294
|
|
|
262
295
|
Tell the user exactly which files were written and where.
|
|
263
296
|
|
|
@@ -274,10 +307,7 @@ This stage updates `MODEL-NOTES.md` to make future runs better. **Nothing is wri
|
|
|
274
307
|
|
|
275
308
|
Draft new or updated entries for the per-model sections of `MODEL-NOTES.md` that capture what was learned.
|
|
276
309
|
|
|
277
|
-
**
|
|
278
|
-
- **avg street-cred** — incorporate this run's rank position into each model's running average.
|
|
279
|
-
- **confirm-rate** — incorporate this run's share of each model's findings that ended up Confirmed.
|
|
280
|
-
- Merge into the existing row for that model; prune the notes column to stay tight.
|
|
310
|
+
**Ledger auto-append (automatic — no approval required).** After `verdict.json` is written, the ledger row is appended automatically when the tally record is finalized — `ledger.appendRun(record)` writes one row per (run × model) to the append-only `council-ledger.jsonl` under `getConfigDir()`. The run summary shows the appended row. This is a deterministic, content-free model-level record (no finding text, no claim strings, no artifact body content). The quantitative reviewer-reliability data in `MODEL-NOTES.md` is now sourced entirely from `amicus council stats` (which aggregates the ledger) — **do not hand-edit reliability numbers in MODEL-NOTES**.
|
|
281
311
|
|
|
282
312
|
**Compose the proposed MODEL-NOTES diff.** Combine the run-lessons updates and the reviewer-reliability table updates into a single proposed diff (old → new for every changed section). **Write the full diff to a file in the run folder** — `_tmp-proposed-model-notes-update.md` — so the user can open and review it before deciding. Presenting the diff as chat text alone is **not sufficient**: an approval dialog can hide the chat transcript, so the user may be asked to decide on a diff they never saw.
|
|
283
313
|
|
|
@@ -309,23 +339,21 @@ Claude **de-anonymizes only** at two points: when computing scores and when writ
|
|
|
309
339
|
|
|
310
340
|
### §5.2 Scoring
|
|
311
341
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
**Street-cred** = each model's **average rank position** across all judges' `FINAL RANKING:` blocks (lower is better). For example, if three judges rank DeepSeek 1st, 2nd, and 1st, its street-cred score is 1.33. Surface this as a compact table in the cross-review matrix and report. Street-cred drives the chair's weighting of reviewer findings in Stage 3 and feeds the reviewer-reliability table updated in Stage 6.
|
|
342
|
+
`amicus council tally` computes the two scoring signals from the assembled tally input. Claude's role is to assemble the input (Stage-2 assembly recipe in Stage 2 above) and to exercise judgment on `thin`-confidence overrides.
|
|
315
343
|
|
|
316
|
-
**
|
|
344
|
+
**Street-cred** — computed two ways:
|
|
345
|
+
- **withSelf** = each model's mean rank position across **all** judges' `FINAL RANKING:` blocks (lower is better).
|
|
346
|
+
- **peersOnly** = mean rank excluding the model's own ranking of itself.
|
|
317
347
|
|
|
318
|
-
|
|
319
|
-
- **Contested** — a meaningful split exists or explicit disputes were recorded.
|
|
320
|
-
- **Singleton** — only the original raiser stands behind it; all other judges were neutral or silent.
|
|
348
|
+
Both are surfaced in `crossreview-matrix.md` and `report.md`. The ledger and Stage-0 bench recommendations use **peersOnly** only.
|
|
321
349
|
|
|
322
|
-
|
|
350
|
+
**Per-finding peer-confidence tier** — assigned by the peers-only cascade in `amicus council tally` (see COUNCIL-DESIGN.md §5.2 for the full table): **Disputed** → **Confirmed** → **Contested** → **Singleton**. The raiser's own adjudication is excluded from the cascade. `confidence: thin` when total engaged peers `a + d ≤ 1` — cells `(0,0)`, `(1,0)`, `(0,1)`. **Claude may override a `thin` tier at the margins** before presenting Stage 4 — the override is recorded in `tierOverride: {from, to, reason}` and surfaced in the matrix and `verdict.json`.
|
|
323
351
|
|
|
324
352
|
---
|
|
325
353
|
|
|
326
354
|
### §5.3 Chair selection & fallback
|
|
327
355
|
|
|
328
|
-
The default is for Claude to **recommend a non-Claude chair** from the council — typically the model with the strongest reasoning capability or the best
|
|
356
|
+
The default is for Claude to **recommend a non-Claude chair** from the council — typically the model with the strongest reasoning capability or the best peers-only street-cred from `amicus council stats` — and the user confirms this recommendation before the run launches (Stage 0). The chair **may** be a council member who already participated in Stages 1 and 2; it receives the full de-anonymized picture (all reviews with model attribution, all rankings, all adjudications) so it can synthesize from a complete view.
|
|
329
357
|
|
|
330
358
|
**Fallback chain if the chair call fails:**
|
|
331
359
|
|
|
@@ -346,8 +374,8 @@ Enabling this toggle lets the bench judge Claude's own take, so you can see how
|
|
|
346
374
|
**Always fresh.** When the toggle is on, Claude performs a new structured Stage-1 review on the artifact — a fresh pass in the required findings format, not a formalization or summary of anything said earlier in the main conversation. Upstream feedback does not seed or constrain this review.
|
|
347
375
|
|
|
348
376
|
**"How Claude's review fared" readout.** Included in both `crossreview-matrix.md` and `report.md` when the toggle is on:
|
|
349
|
-
- Claude's street-cred rank among peers (its average rank position in the judges' `FINAL RANKING:` blocks).
|
|
350
|
-
- The Confirmed / Contested / Singleton split of Claude's findings — how many of its claims the bench endorsed,
|
|
377
|
+
- Claude's street-cred rank among peers (its `peersOnly` average rank position in the judges' `FINAL RANKING:` blocks — `withSelf == peersOnly` for Claude since it never casts rankings).
|
|
378
|
+
- The Disputed / Confirmed / Contested / Singleton split of Claude's findings — how many of its claims the bench pushed back on, endorsed, disputed, or ignored.
|
|
351
379
|
|
|
352
380
|
**Integrity.** When Claude presents results — including the bench's assessment of its own review — it reports the verdict at face value. Claude does not defend, contextualize away, or re-litigate findings the bench disputed or ranked poorly. The point of the toggle is an honest external read on Claude's review; undermining that defeats the purpose.
|
|
353
381
|
|
|
@@ -355,16 +383,16 @@ Enabling this toggle lets the bench judge Claude's own take, so you can see how
|
|
|
355
383
|
|
|
356
384
|
## Model-recommendation heuristics
|
|
357
385
|
|
|
358
|
-
Use these together with the
|
|
386
|
+
Use these together with `amicus council stats` (the ledger — authoritative quantitative reliability data: runs, avg peers-only street-cred, confirm-rate, fact-error rate) and the qualitative quirks in `MODEL-NOTES.md`:
|
|
359
387
|
|
|
360
388
|
- **Large or long material, broad coverage sweep** → favor a large-context model (e.g., Gemini) that won't truncate or degrade on the full source.
|
|
361
389
|
- **Reasoning-heavy critique, structured argument evaluation, citations** → favor a strong reasoner (e.g., DeepSeek, GPT, Opus) that will interrogate claims rather than accept them.
|
|
362
390
|
- **Code review** → favor a code-strong model (e.g., DeepSeek, GPT, Opus); general-purpose models often miss implementation-level issues.
|
|
363
391
|
- **Independence matters** → pick models from **different families**; two models from the same family produce correlated opinions and reduce the value of the cross-review.
|
|
364
392
|
- **Contrarian / red-team value** → when material is persuasive, consensus-prone, or high-stakes, assign one model an explicit red-team brief: argue against the others, hunt for what they will miss. This is especially valuable when the default council is likely to agree.
|
|
365
|
-
- **Consult
|
|
393
|
+
- **Consult `amicus council stats`** — a model's historical confirm-rate and avg peers-only street-cred (from the ledger) are the best predictors of council value for a given run type.
|
|
366
394
|
|
|
367
|
-
Always **rank recommendations by fit**, state the trade-off for each option, and surface the estimated cost. Never present a single option without explanation.
|
|
395
|
+
Always **rank recommendations by fit**, state the trade-off for each option, and surface the estimated cost (an estimate, not a guarantee; unpriced legs disclosed as "cost unknown"). Never present a single option without explanation.
|
|
368
396
|
|
|
369
397
|
---
|
|
370
398
|
|
|
@@ -373,9 +401,14 @@ Always **rank recommendations by fit**, state the trade-off for each option, and
|
|
|
373
401
|
- Run folder: `output/<stem>-council/` (or `./second-opinion/<stem>-council/` if no `output/` exists), containing:
|
|
374
402
|
- `review-<model>.md` ×N — raw Stage 1 reviews (plus `review-claude.md` when "Claude in the council" is on)
|
|
375
403
|
- `crossreview-matrix.md` — adjudication grid + de-anonymized street-cred table
|
|
376
|
-
- `verdict.md` — the chair's synthesis
|
|
404
|
+
- `verdict.md` — the chair's synthesis (prose)
|
|
405
|
+
- `verdict.json` — schema-stamped machine-readable record: tally output + Stage-4 decisions, written via `buildVerdict(record, decisions)` at Stage 5
|
|
377
406
|
- `report.md` — synthesis + decision log + what was applied (+ the "How Claude's review fared" readout when the toggle is on) + a
|
|
378
|
-
**run-stats table**: one row per model call — **stage** (which stage you launched the call for) plus **model, status, durationMs** read from the wave/run JSON
|
|
407
|
+
**run-stats table**: one row per model call — **stage** (which stage you launched the call for) plus **model, status, durationMs, and cost** read from the wave/run JSON `usage`
|
|
408
|
+
block. Cost is `usage.cost.amount` (USD); mark it with its `usage.cost.source`
|
|
409
|
+
— exact for `reported`, `~` for `estimated`, `?` for `unknown` — and never
|
|
410
|
+
invent a figure. Add a wave **total cost** row from the wave document's
|
|
411
|
+
`usage.cost` (`source: reported|estimated|mixed|unknown`). Any leg with no run doc → `durationMs: null`, `usage: null`.
|
|
379
412
|
- Reviewed copy: `<stem>-reviewed.<ext>`, next to the source.
|
|
380
413
|
- Temp working files (`_tmp-*.md`: extracts, stage briefings, red-team brief, bundle, chair packet, proposed
|
|
381
414
|
MODEL-NOTES diff) live in the run folder and are cleaned up at the end of the run — the proposed-diff file
|
|
@@ -385,6 +418,5 @@ Always **rank recommendations by fit**, state the trade-off for each option, and
|
|
|
385
418
|
|
|
386
419
|
## Files
|
|
387
420
|
|
|
388
|
-
- `MODEL-NOTES.md` — operating rules, per-model quirks, cost guardrail, and
|
|
389
|
-
- `COUNCIL-DESIGN.md` — the design spec this skill implements (v3). Consult it if a mechanics
|
|
390
|
-
question arises that the skill prose does not resolve.
|
|
421
|
+
- `MODEL-NOTES.md` — operating rules, per-model qualitative quirks, cost guardrail, and structural-conformance notes. **Read it before Stage 0 (council selection and launch); update qualitative notes (with approval) in Stage 6.** Quantitative reliability data (runs, avg street-cred, confirm-rate, fact-error rate) comes from `amicus council stats`, not this file.
|
|
422
|
+
- `COUNCIL-DESIGN.md` — the design spec this skill implements (v3 + WS-3). Consult it if a mechanics question arises that the skill prose does not resolve.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// src/cli-handlers-council.js
|
|
2
|
+
'use strict';
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const { tally } = require('./council/tally');
|
|
5
|
+
const { deriveReliability } = require('./council/ledger');
|
|
6
|
+
const { failJson, ERROR_CODES } = require('./utils/error-doc');
|
|
7
|
+
|
|
8
|
+
function runTally(inputPath, useJson) {
|
|
9
|
+
if (!inputPath) {
|
|
10
|
+
return failJson(useJson, { code: ERROR_CODES.BAD_ARGS, message: 'council tally needs an <input.json> path',
|
|
11
|
+
hint: 'amicus council tally <input.json> [--json]' });
|
|
12
|
+
}
|
|
13
|
+
let input;
|
|
14
|
+
try { input = JSON.parse(fs.readFileSync(inputPath, 'utf-8')); }
|
|
15
|
+
catch (e) {
|
|
16
|
+
return failJson(useJson, { code: ERROR_CODES.BAD_ARGS, message: `cannot read ${inputPath}: ${e.message}`,
|
|
17
|
+
hint: 'pass a valid tally input JSON file' });
|
|
18
|
+
}
|
|
19
|
+
let record;
|
|
20
|
+
try { record = tally(input); }
|
|
21
|
+
catch (e) {
|
|
22
|
+
return failJson(useJson, { code: ERROR_CODES.BAD_ARGS, message: `malformed tally input: ${e.message}`,
|
|
23
|
+
hint: 'input needs meta.models, findings[], adjudications[], rankings[]' });
|
|
24
|
+
}
|
|
25
|
+
process.stdout.write(useJson ? JSON.stringify(record, null, 2) + '\n' : renderRecord(record));
|
|
26
|
+
return 0;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function runStats(useJson) {
|
|
30
|
+
const agg = deriveReliability();
|
|
31
|
+
process.stdout.write(useJson ? JSON.stringify(agg, null, 2) + '\n' : renderStats(agg));
|
|
32
|
+
return 0;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function renderRecord(r) {
|
|
36
|
+
const t = r.tierCounts;
|
|
37
|
+
return `Council tally (${r.meta.runId})\n` +
|
|
38
|
+
` Confirmed ${t.Confirmed} Contested ${t.Contested} Singleton ${t.Singleton} Disputed ${t.Disputed}\n`;
|
|
39
|
+
}
|
|
40
|
+
function renderStats(agg) {
|
|
41
|
+
if (!agg.length) { return 'No council runs recorded yet.\n'; }
|
|
42
|
+
return 'model runs avg-cred confirm fact-err notes\n' +
|
|
43
|
+
agg.map(a => `${a.model.padEnd(16)} ${String(a.runs).padStart(4)} ` +
|
|
44
|
+
`${fmt(a.avgStreetCredPeersOnly)} ${fmt(a.lifetimeConfirmRate)} ${fmt(a.lifetimeFactErrorRate)}` +
|
|
45
|
+
`${a.lowN ? ' low-N' : ''}`).join('\n') + '\n';
|
|
46
|
+
}
|
|
47
|
+
function fmt(v) { return (v === null || v === undefined) ? ' — ' : v.toFixed(2); }
|
|
48
|
+
|
|
49
|
+
/** @param {{_:string[], json?:boolean}} args @returns {Promise<number>} */
|
|
50
|
+
async function handleCouncil(args) {
|
|
51
|
+
const sub = args._[1];
|
|
52
|
+
const useJson = !!args.json;
|
|
53
|
+
if (sub === 'tally') { return runTally(args._[2], useJson); }
|
|
54
|
+
if (sub === 'stats') { return runStats(useJson); }
|
|
55
|
+
return failJson(useJson, { code: ERROR_CODES.BAD_ARGS,
|
|
56
|
+
message: `unknown council subcommand '${sub || ''}'`, hint: 'amicus council tally|stats' });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
module.exports = { handleCouncil };
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
// src/cli-handlers-doctor.js
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const MAX_CATALOG_AGE_MS = 24 * 60 * 60 * 1000; // 24h (mirrors model-catalog DEFAULT_MAX_AGE_MS)
|
|
5
|
+
|
|
6
|
+
/** Default real helpers; tests override via deps. */
|
|
7
|
+
function realDeps() {
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const os = require('os');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
return {
|
|
12
|
+
nodeVersion: process.version,
|
|
13
|
+
readApiKeys: () => require('./utils/api-key-store').readApiKeys(),
|
|
14
|
+
getConfigDir: () => require('./utils/config').getConfigDir(),
|
|
15
|
+
resolveModel: () => require('./utils/config').resolveModel(),
|
|
16
|
+
readCache: () => require('./utils/model-catalog').readCache(),
|
|
17
|
+
collectAliasSources: () => require('./utils/alias-audit').collectAliasSources(),
|
|
18
|
+
findStaleAliases: (s, c) => require('./utils/alias-audit').findStaleAliases(s, c),
|
|
19
|
+
hasOpencodeBinary: () => {
|
|
20
|
+
const { ensureNodeModulesBinInPath } = require('./utils/path-setup');
|
|
21
|
+
ensureNodeModulesBinInPath();
|
|
22
|
+
const root = path.join(__dirname, '..', 'node_modules');
|
|
23
|
+
const candidates = process.platform === 'win32'
|
|
24
|
+
? [path.join(root, `opencode-windows-${os.arch() === 'arm64' ? 'arm64' : 'x64'}`, 'bin', 'opencode.exe'),
|
|
25
|
+
path.join(root, `opencode-windows-${os.arch() === 'arm64' ? 'arm64' : 'x64'}-baseline`, 'bin', 'opencode.exe')]
|
|
26
|
+
: [path.join(root, '.bin', 'opencode')];
|
|
27
|
+
return candidates.some(p => fs.existsSync(p));
|
|
28
|
+
},
|
|
29
|
+
getElectronPath: () => require('./sidecar/interactive').getElectronPath(),
|
|
30
|
+
discoverClaudeCodeMcps: () => require('./utils/mcp-discovery').discoverClaudeCodeMcps(),
|
|
31
|
+
discoverCoworkMcps: () => require('./utils/mcp-discovery').discoverCoworkMcps(),
|
|
32
|
+
skillInstalled: () => {
|
|
33
|
+
const dir = path.join(os.homedir(), '.claude', 'skills');
|
|
34
|
+
return fs.existsSync(path.join(dir, 'sidecar', 'SKILL.md'))
|
|
35
|
+
&& fs.existsSync(path.join(dir, 'second-opinion', 'SKILL.md'));
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Run one guarded check; a thrown fn becomes an error line. */
|
|
41
|
+
function guard(id, name, fn) {
|
|
42
|
+
try { return fn(); }
|
|
43
|
+
catch (e) { return { id, name, status: 'error', message: e.message, hint: null }; }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Compose the health checks. Never throws.
|
|
48
|
+
* @param {object} [depsOverride]
|
|
49
|
+
* @returns {Array<{id,name,status,message,hint}>}
|
|
50
|
+
*/
|
|
51
|
+
function runDoctorChecks(depsOverride = {}) {
|
|
52
|
+
const d = { ...realDeps(), ...depsOverride };
|
|
53
|
+
const checks = [];
|
|
54
|
+
|
|
55
|
+
checks.push(guard('node', 'Node.js', () => {
|
|
56
|
+
const major = parseInt(String(d.nodeVersion).replace(/^v/, '').split('.')[0], 10);
|
|
57
|
+
return major >= 18
|
|
58
|
+
? { id: 'node', name: 'Node.js', status: 'ok', message: d.nodeVersion, hint: null }
|
|
59
|
+
: { id: 'node', name: 'Node.js', status: 'error', message: `${d.nodeVersion} (need >=18)`, hint: 'Install Node 18 or newer from https://nodejs.org' };
|
|
60
|
+
}));
|
|
61
|
+
|
|
62
|
+
checks.push(guard('config-dir', 'Config directory', () => (
|
|
63
|
+
{ id: 'config-dir', name: 'Config directory', status: 'ok', message: d.getConfigDir(), hint: null }
|
|
64
|
+
)));
|
|
65
|
+
|
|
66
|
+
checks.push(guard('keys', 'API keys', () => {
|
|
67
|
+
const keys = d.readApiKeys();
|
|
68
|
+
const set = Object.keys(keys).filter(k => keys[k]);
|
|
69
|
+
return set.length > 0
|
|
70
|
+
? { id: 'keys', name: 'API keys', status: 'ok', message: `configured: ${set.join(', ')}`, hint: null }
|
|
71
|
+
: { id: 'keys', name: 'API keys', status: 'error', message: 'no provider keys configured', hint: 'amicus key <provider> <key> (or run: amicus setup)' };
|
|
72
|
+
}));
|
|
73
|
+
|
|
74
|
+
checks.push((() => {
|
|
75
|
+
try {
|
|
76
|
+
const model = d.resolveModel();
|
|
77
|
+
return { id: 'default-model', name: 'Default model', status: 'ok', message: model, hint: null };
|
|
78
|
+
} catch (e) {
|
|
79
|
+
return { id: 'default-model', name: 'Default model', status: 'error', message: e.message || 'no default model', hint: 'amicus setup' };
|
|
80
|
+
}
|
|
81
|
+
})());
|
|
82
|
+
|
|
83
|
+
checks.push(guard('catalog', 'Model catalog', () => {
|
|
84
|
+
const cache = d.readCache();
|
|
85
|
+
if (!cache || !cache.fetchedAt) {
|
|
86
|
+
return { id: 'catalog', name: 'Model catalog', status: 'warn', message: 'no cache yet', hint: 'amicus models --refresh' };
|
|
87
|
+
}
|
|
88
|
+
const ageMs = Date.now() - cache.fetchedAt;
|
|
89
|
+
const fresh = ageMs <= MAX_CATALOG_AGE_MS;
|
|
90
|
+
const hrs = Math.round(ageMs / 3600000);
|
|
91
|
+
return fresh
|
|
92
|
+
? { id: 'catalog', name: 'Model catalog', status: 'ok', message: `${cache.models.length} models, ${hrs}h old`, hint: null }
|
|
93
|
+
: { id: 'catalog', name: 'Model catalog', status: 'warn', message: `stale (${hrs}h old)`, hint: 'amicus models --refresh' };
|
|
94
|
+
}));
|
|
95
|
+
|
|
96
|
+
checks.push(guard('aliases', 'Model aliases', () => {
|
|
97
|
+
const cache = d.readCache();
|
|
98
|
+
const catalog = (cache && cache.models) || [];
|
|
99
|
+
const stale = d.findStaleAliases(d.collectAliasSources(), catalog);
|
|
100
|
+
return stale.length === 0
|
|
101
|
+
? { id: 'aliases', name: 'Model aliases', status: 'ok', message: catalog.length ? 'all resolve' : 'catalog empty — not checked', hint: null }
|
|
102
|
+
: { id: 'aliases', name: 'Model aliases', status: 'warn', message: `${stale.length} stale: ${stale.map(s => s.alias).join(', ')}`, hint: 'amicus models --check' };
|
|
103
|
+
}));
|
|
104
|
+
|
|
105
|
+
checks.push(guard('opencode-bin', 'OpenCode binary', () => (
|
|
106
|
+
d.hasOpencodeBinary()
|
|
107
|
+
? { id: 'opencode-bin', name: 'OpenCode binary', status: 'ok', message: 'found', hint: null }
|
|
108
|
+
: { id: 'opencode-bin', name: 'OpenCode binary', status: 'error', message: 'not found', hint: 'npm install -g amicus' }
|
|
109
|
+
)));
|
|
110
|
+
|
|
111
|
+
checks.push(guard('electron', 'Electron (interactive GUI)', () => (
|
|
112
|
+
d.getElectronPath()
|
|
113
|
+
? { id: 'electron', name: 'Electron (interactive GUI)', status: 'ok', message: 'installed', hint: null }
|
|
114
|
+
: { id: 'electron', name: 'Electron (interactive GUI)', status: 'warn', message: 'not installed — headless still works', hint: 'npm install -g amicus (reinstall to add Electron)' }
|
|
115
|
+
)));
|
|
116
|
+
|
|
117
|
+
checks.push(guard('skills', 'Skills installed', () => (
|
|
118
|
+
d.skillInstalled()
|
|
119
|
+
? { id: 'skills', name: 'Skills installed', status: 'ok', message: '~/.claude/skills/{sidecar,second-opinion}', hint: null }
|
|
120
|
+
: { id: 'skills', name: 'Skills installed', status: 'warn', message: 'one or both skills missing', hint: 'npm install -g amicus (re-runs the skill install)' }
|
|
121
|
+
)));
|
|
122
|
+
|
|
123
|
+
checks.push(guard('mcp', 'MCP registration', () => {
|
|
124
|
+
const code = d.discoverClaudeCodeMcps();
|
|
125
|
+
const cowork = d.discoverCoworkMcps();
|
|
126
|
+
const inCode = !!(code && code.amicus);
|
|
127
|
+
const inCowork = !!(cowork && cowork.amicus);
|
|
128
|
+
// Primary signal: Claude Code MCP registration. Cowork/Desktop is reported as bonus only.
|
|
129
|
+
if (!inCode) {
|
|
130
|
+
return { id: 'mcp', name: 'MCP registration', status: 'warn', message: 'not registered in Claude Code', hint: 'npm install -g amicus (or install the amicus plugin)' };
|
|
131
|
+
}
|
|
132
|
+
const extra = inCowork ? ', Cowork/Desktop' : '';
|
|
133
|
+
return { id: 'mcp', name: 'MCP registration', status: 'ok', message: `registered: Claude Code${extra}`, hint: null };
|
|
134
|
+
}));
|
|
135
|
+
|
|
136
|
+
return checks;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const MARK = { ok: '✓', warn: '⚠', error: '✗' }; // ✓ ⚠ ✗
|
|
140
|
+
|
|
141
|
+
function renderHuman(checks) {
|
|
142
|
+
let out = 'amicus doctor\n\n';
|
|
143
|
+
for (const c of checks) {
|
|
144
|
+
out += `${MARK[c.status] || '?'} ${c.name}: ${c.message}\n`;
|
|
145
|
+
if (c.hint && c.status !== 'ok') { out += ` → ${c.hint}\n`; }
|
|
146
|
+
}
|
|
147
|
+
const errors = checks.filter(c => c.status === 'error').length;
|
|
148
|
+
const warns = checks.filter(c => c.status === 'warn').length;
|
|
149
|
+
out += `\n${errors} error(s), ${warns} warning(s).\n`;
|
|
150
|
+
return out;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* `amicus doctor [--json]`. Injectable `runChecks` for tests.
|
|
155
|
+
* @param {{_:string[], json?:boolean}} args
|
|
156
|
+
* @param {(deps?:object)=>Array} [runChecks]
|
|
157
|
+
* @returns {Promise<number>} exit code
|
|
158
|
+
*/
|
|
159
|
+
async function handleDoctor(args, runChecks = runDoctorChecks) {
|
|
160
|
+
const useJson = !!args.json;
|
|
161
|
+
const checks = runChecks();
|
|
162
|
+
if (useJson) {
|
|
163
|
+
const { buildDoctorDoc } = require('./utils/result-schema');
|
|
164
|
+
const VERSION = require('../package.json').version;
|
|
165
|
+
const doc = buildDoctorDoc({ version: VERSION, timestamp: new Date().toISOString(), checks });
|
|
166
|
+
process.stdout.write(JSON.stringify(doc, null, 2) + '\n');
|
|
167
|
+
return doc.ok ? 0 : 1;
|
|
168
|
+
}
|
|
169
|
+
process.stdout.write(renderHuman(checks));
|
|
170
|
+
return checks.some(c => c.status === 'error') ? 1 : 0;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
module.exports = { runDoctorChecks, handleDoctor, MAX_CATALOG_AGE_MS };
|