amicus 4.9.3 → 4.9.4

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.
Files changed (60) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/CHANGELOG.md +242 -0
  3. package/README.md +1 -1
  4. package/docs/ROADMAP.md +5 -4
  5. package/docs/architecture-map.md +732 -0
  6. package/docs/configuration.md +148 -26
  7. package/docs/council.md +9 -0
  8. package/docs/doc-system.md +12 -9
  9. package/docs/testing.md +2 -1
  10. package/docs/troubleshooting.md +76 -0
  11. package/docs/usage.md +11 -6
  12. package/package.json +1 -1
  13. package/schemas/model-catalog.schema.json +2 -1
  14. package/schemas/run.schema.json +13 -0
  15. package/skills/sidecar/SKILL.md +1 -8
  16. package/src/cli-handlers-doctor.js +3 -0
  17. package/src/cli-handlers-fanout.js +10 -1
  18. package/src/cli-handlers-resume-continue.js +25 -0
  19. package/src/cli.js +5 -8
  20. package/src/council/briefings-chair.js +4 -2
  21. package/src/council/run-assemble.js +7 -2
  22. package/src/council/run-retry-notes.js +21 -1
  23. package/src/council/run-stages.js +8 -1
  24. package/src/headless.js +125 -7
  25. package/src/mcp-server.js +26 -0
  26. package/src/mcp-tools.js +4 -4
  27. package/src/opencode-client.js +84 -8
  28. package/src/pack/pack-validate.js +3 -0
  29. package/src/session-manager.js +2 -2
  30. package/src/sidecar/continue.js +6 -1
  31. package/src/sidecar/conversation-mirror.js +35 -11
  32. package/src/sidecar/fanout-leg-fallback.js +1 -0
  33. package/src/sidecar/fanout-leg.js +10 -2
  34. package/src/sidecar/fanout.js +2 -2
  35. package/src/sidecar/interactive.js +31 -4
  36. package/src/sidecar/models-ceiling-line.js +72 -0
  37. package/src/sidecar/models.js +4 -2
  38. package/src/sidecar/reopen-notices.js +97 -0
  39. package/src/sidecar/reopen-spend.js +3 -2
  40. package/src/sidecar/resume.js +15 -2
  41. package/src/sidecar/session-finalize.js +4 -1
  42. package/src/sidecar/session-utils.js +5 -1
  43. package/src/sidecar/start-metadata.js +1 -1
  44. package/src/sidecar/start.js +10 -5
  45. package/src/utils/config.js +33 -12
  46. package/src/utils/curated-models.js +8 -8
  47. package/src/utils/degrade.js +7 -0
  48. package/src/utils/doctor-output-budget-check.js +198 -0
  49. package/src/utils/engine-output-flag.js +105 -0
  50. package/src/utils/engine-variants.js +298 -0
  51. package/src/utils/http-get.js +284 -0
  52. package/src/utils/model-catalog.js +36 -4
  53. package/src/utils/model-ceilings-modelsdev.js +230 -0
  54. package/src/utils/model-fetcher.js +12 -36
  55. package/src/utils/model-output-limit.js +21 -13
  56. package/src/utils/output-length.js +90 -0
  57. package/src/utils/result-schema.js +7 -2
  58. package/src/utils/spend-ledger.js +5 -1
  59. package/src/utils/thinking-validators.js +27 -80
  60. package/src/utils/validators.js +2 -3
@@ -63,12 +63,37 @@ This request requires more credits, or fewer max_tokens.
63
63
  You requested up to 32000 tokens, but can only afford 354
64
64
  ```
65
65
 
66
- `outputBudget` lowers the reservation. Each model reserves `min(outputBudget, that model's real
67
- ceiling)`, so a small model keeps its own lower limit rather than being handed an over-ceiling value.
66
+ `outputBudget` sets the reservation, in either direction. Every leg reserves
67
+ `min(outputBudget, that model's real ceiling)` wherever a ceiling is known by the Amicus catalog
68
+ (a per-model `limit` descriptor) or, failing that, by the engine's own catalog (every engine Amicus
69
+ starts gets `OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX` set to the budget) — except on the direct
70
+ `openai` route, which carries no reservation field at all (M5/M13/M22). A model neither catalog
71
+ knows receives the budget itself, exactly as it received the raw 32,000 before.
72
+
73
+ If you set `outputBudget` on 4.9.3, one thing changes on upgrade: a budget below 32,000 now also
74
+ reaches routes the catalog cannot clamp — an unrefreshed direct row (other than `openai`, which no
75
+ budget reaches — M13/M22), a local-provider model, anything the catalog lacks — which 4.9.3 left at
76
+ the engine's 32,000. Those legs reserve `min(budget, the ceiling the engine's own catalog knows)`
77
+ (K12: 8,000 on a bare kimi row), or the budget as-is where the engine knows the model no better
78
+ (K13).
79
+
80
+ Setting `outputBudget` can only make the `--thinking` guard stricter or leave it unchanged, never
81
+ looser: the guard reads the engine's own row for the model, not the descriptor a budget makes Amicus
82
+ write, so whether a level is declared is decided identically with and without a budget. The one
83
+ refusal a budget can add is `VARIANT_OVER_BUDGET` — a declared level whose own thinking budget would
84
+ push the reservation past the budget — and that check exists only because a budget was set (council
85
+ #235 r3).
68
86
 
69
87
  | Setting | Values | Default | Effect |
70
88
  |---------|--------|---------|--------|
71
- | `outputBudget` (config.json, top-level) | positive integer | *unset* | Per-leg output reservation, clamped to each model's real ceiling. Unset means no limit is sent — OpenCode's 32,000 default applies, exactly as before. |
89
+ | `outputBudget` (config.json, top-level) | positive integer | *unset* | Per-leg output reservation, clamped to each model's real ceiling wherever one is known. Unset means no limit is sent and no engine flag is set — OpenCode's 32,000 default applies, exactly as before. |
90
+ | `modelsDevCeilings` (config.json, top-level) | `true` / `false` | `true` | Fill direct-provider context/ceiling numbers from models.dev at refresh. Set `false` to never contact models.dev; the anthropic / deepseek direct rows then carry no ceiling in the Amicus catalog and are clamped by the engine's own catalog instead (Google publishes its own ceiling and OpenRouter rows keep OpenRouter's); the direct `openai` rows send no output reservation at all regardless (#218 PR 4). |
91
+
92
+ `modelsDevCeilings` is the opt-out for the one third-party lookup a refresh makes. Only a literal
93
+ `false` turns it off; the key being absent means it runs. Even with it on, the call is **skipped
94
+ automatically when no candidate row is missing a number** — a refresh with nothing to fill never
95
+ contacts models.dev at all. `amicus models --refresh` names whichever of those happened on its
96
+ `Ceilings:` line.
72
97
 
73
98
  Set it by hand-editing `~/.config/amicus/config.json`:
74
99
 
@@ -76,28 +101,125 @@ Set it by hand-editing `~/.config/amicus/config.json`:
76
101
  { "outputBudget": 8000 }
77
102
  ```
78
103
 
79
- Two limits worth knowing before you set it:
80
-
81
- - **It can only lower the reservation, never raise it.** OpenCode computes
82
- `Math.min(limit.output, 32000)`, so any value at or above 32,000 leaves the reservation itself
83
- unchanged. Raising the ceiling past 32,000 needs OpenCode's own
84
- `OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX` environment variable a different lever that Amicus does
85
- not set.
86
-
87
- Setting a value ≥ 32,000 is **not** a complete no-op, though. Amicus still emits the `limit`
88
- descriptor, which carries the model's context length alongside the output figure — and OpenCode
89
- disables prompt compaction for any model whose context reads as `0`, which is what a model it does
90
- not recognise otherwise gets. So a high budget leaves `max_tokens` alone while still restoring
91
- compaction for those models. If you want neither effect, leave `outputBudget` unset.
92
- - **It needs a catalog that knows each model's ceiling.** Run `amicus models --refresh` after setting
93
- it. Models whose ceiling is unknown anything fetched before this field existed, and the direct
94
- `openai` / `anthropic` / `google` / `deepseek` lists, which don't publish one keep the old
95
- behaviour rather than receiving a guessed limit.
96
-
97
- This addresses reservation *rejections*. It does **not** stop a reasoning-heavy model from spending
98
- its whole allowance on reasoning and emitting nothing that is governed by reasoning effort
99
- (`--thinking`), not by `max_tokens`. Lowering the budget makes such a leg fail faster and cheaper; it
100
- does not make it produce output.
104
+ `amicus doctor`'s `output-budget` row then says what the value reaches: how many of your alias
105
+ routes the catalog can clamp it to, whether an `OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX` you exported
106
+ yourself is being honoured or overridden, and a malformed value in either place — the engine falls
107
+ back to 32,000 *silently* on those (measured), so the doctor row is where it surfaces.
108
+
109
+ Five things worth knowing before you set it. Every number below was measured on the wire by
110
+ `scripts/probe-max-tokens.js` against the pinned engine, or read in the pinned binary where it says
111
+ so; the row ids refer to the four probe tables filed in `BACKLOG.md` under "v4.9.4 records" (#218
112
+ P1, PR 2, PR 3 and PR 4).
113
+
114
+ - **Above 32,000 it is the engine flag doing the work.** OpenCode computes
115
+ `Math.min(limit.output, OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX)` with the flag defaulting to
116
+ 32,000 (read in the pinned binary; the rows below are its wire effects), so Amicus sets that
117
+ flag to your budget for every engine it starts around the spawn only. It never lands in
118
+ your shell, and a value you exported yourself is honoured untouched when `outputBudget` is
119
+ unset and overridden (for Amicus-started engines only) when it is set. A budget of 100,000 on a
120
+ model with a 943,718 ceiling reserves 100,000 (K6); on a model whose ceiling is 64,000 it
121
+ reserves 64,000 (K5). The flag is experimental on the engine's side, and a malformed value is
122
+ ignored without a word (D1/D2): CI runs the probe's flag rows on every push, and the full matrix
123
+ is re-run and re-filed after every engine bump. An SDK bump is covered by a test that drives the
124
+ real SDK against a fake engine and checks the flag reached the spawn.
125
+ - **It clamps best with a catalog that knows each model's ceiling.** Run `amicus models --refresh`
126
+ after setting it. OpenRouter rows carry OpenRouter's own ceiling and Google rows carry Google's;
127
+ the direct `openai` / `anthropic` / `deepseek` rows — and any other row whose number the provider
128
+ left empty or unusable — are filled from [models.dev](https://models.dev) at refresh. models.dev
129
+ fills a field only where the provider gave no usable positive integer (a null, a zero, a negative
130
+ or a malformed number), and never overwrites a usable provider value; the `openrouter/openrouter/*`
131
+ meta-routers and local-provider rows are never filled at all. The refresh output says how many
132
+ rows were filled or why none could be. One route no budget reaches: the direct `openai` provider —
133
+ the engine drives it through the Responses API, whose request carries no output-limit field at all
134
+ (M5 bare; M13 with the descriptor and the flag both at 8,000; M22 for gpt-4o), so neither lever
135
+ changes what goes out there; `doctor` lists those routes apart from the clamped and unclamped
136
+ counts. A route the Amicus catalog cannot clamp still gets the budget through the engine flag,
137
+ clamped by the engine's own catalog where it knows the model (K5: 100,000 → 64,000 on a bare haiku
138
+ row; K12: 8,000 on a bare kimi row, passed through under its ceiling); a model neither knows
139
+ receives the budget as-is (K13). That is the one way a raised budget can fail where the 32,000
140
+ default did not: a custom or local model neither catalog knows is asked for the full budget, and a
141
+ provider that enforces its ceiling refuses the request — loudly, with the provider's own error on
142
+ the leg, never silently. `amicus doctor` names such routes; lower the budget if you have one, or
143
+ give the model a catalog entry.
144
+ - **A thinking variant leaves the budget alone on every route but one.** `--thinking` now reaches
145
+ the engine (#218 PR 4) as its `variant` field, and the probe measured the reservation with a
146
+ variant in play on each provider whose request carries one (the direct openai route carries
147
+ none — M5/M13/M22): OpenRouter (M1: 8,000 stays 8,000 under `low`; M9: OpenRouter's Anthropic
148
+ row sends 32,000 with `high` on both of the engine's catalogues — a variant adds nothing there),
149
+ direct Google (M15: 8,000), direct DeepSeek (M16: 8,000) and an adaptive-thinking Anthropic model
150
+ (M10b: `claude-sonnet-5` at 8,000) all hold it. The one shape that does not is a direct
151
+ Anthropic variant declared as `thinking: {type: 'enabled', budgetTokens: N}` — today Haiku 4.5
152
+ (`high` 16,000, `max` 31,999) and Opus 4.5 (16,000 for `low`, `medium` and `high`) — where the
153
+ engine adds N on top (M2, measured on Haiku: 24,000 + 16,000 = 40,000; K2 — Opus 4.5 declares
154
+ the same `enabled` + `budgetTokens` shape, M0, and is refused under the same condition) and clamps the sum
155
+ to the model's ceiling (K3/K4/K10). Amicus cannot lower the descriptor by N before the spawn (N
156
+ is the engine's own number, read only from a running engine, and nothing changes a descriptor
157
+ afterwards — a runtime `PATCH /config` is accepted, changes nothing the engine serves, and
158
+ writes a `config.json` into the engine's working directory, M3/M4/M11), so with a budget below
159
+ that model's ceiling such a leg is **refused before anything is sent**, with the reservation it
160
+ would have made and three ways out: raise the budget to at least the ceiling (the sum is then
161
+ clamped to it, K4), route the model through OpenRouter (M1, M9), or use an adaptive-thinking
162
+ model (M10b). With no budget set the engine's own behaviour applies (32,000 + N, H3/H4). The
163
+ exact fit — a descriptor lowered by N lands the sum exactly on the budget (M17: 8,000 + 16,000
164
+ = 24,000) — is filed in the BACKLOG as the follow-up it would take.
165
+ - **The reservation comes out of the context window.** Input plus `max_tokens` has to fit the
166
+ window, and the engine subtracts this same reservation from the window before it decides to
167
+ compact (read in the pinned binary's `SessionCompaction.isOverflow`, not wire-measured: that is
168
+ the branch for a model without `limit.input`; a model with one loses at most 20,000, and a
169
+ `compaction.reserved` config overrides both). A budget of 100,000 leaves a 131,072-context
170
+ model 31,072 tokens for the prompt. `amicus doctor` warns when a budget takes at least half of
171
+ any alias route's window.
172
+ - **When a leg hits it, the run says so.** The engine records `finish: 'length'` on the leg's
173
+ assistant message whenever the provider stopped at the reservation (A, H1, L1–L4 — both provider
174
+ families). A leg whose finalized message carries **no answer text** — the whole reservation went
175
+ to reasoning, the #218 "Mode 2" rows (32,000 reasoning, 0–2 output, $0.63 billed) — now ends
176
+ `error` with a reason starting `OUTPUT_LENGTH:` that carries the engine's reasoning/output counts
177
+ for the leg and the budget in force (or the ambient `OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX` the
178
+ engine was started with, when no budget is set); it used to end `complete` with an empty summary
179
+ or, when the provider streamed the reasoning, with its *thinking* promoted to the review (L2/L4).
180
+ A leg whose finalized message carries answer text keeps its review — the answer text (a tool
181
+ loop's earlier answer text included; reasoning an earlier message promoted as a stand-in is
182
+ dropped the moment real answer text arrives) — and a council prints a `Note:` on the
183
+ `output-truncated` channel — informational, the exit code does not move — and marks the review as
184
+ cut in the chair packet's header. The counts are reported, never decided on: on OpenAI-compatible
185
+ routes the engine subtracts reasoning from completion (L3: 8 = 40 − 32); on the direct Anthropic
186
+ route it reports no split (L4: 24,000 output / 0 reasoning). Two limits: a leg whose hidden
187
+ reasoning outlasts the no-output backstop window dies under `NO_OUTPUT_BACKSTOP` first (the
188
+ message has not finalized yet), and the Note is Stage-1 only — a judge, chair or debate leg cut at
189
+ its reservation gets the death name but no Note. Separately, L5 settles the catalog question PR 2
190
+ parked: a descriptor above the engine's own ceiling is clamped to that ceiling with a thinking
191
+ variant (K10: 70,000 + 31,999 → 64,000) and without one (L5: 70,000 + flag 100,000 → 64,000 on
192
+ haiku), so a catalog row whose ceiling exceeds the engine's is harmless on the
193
+ wire. It is not harmless to #218 PR 4's `VARIANT_OVER_BUDGET` fit, which judges against the
194
+ ceiling **Amicus's own catalog** carries for the model — its row's `maxOutputTokens`, the
195
+ number a budget-derived descriptor is clamped TO, not the value that descriptor carries —
196
+ because once a budget is set `/config/providers` echoes that descriptor back (M3) and the
197
+ engine's own ceiling is no longer readable there. (With no row for the model the fit falls back
198
+ to the dump's own value, which is then the engine's own ceiling — the descriptor was bare,
199
+ K5/K12.) A divergence therefore costs something in BOTH directions. A row ABOVE the engine's
200
+ ceiling can refuse a leg the engine would have run: a haiku row of 100,000 with `outputBudget`
201
+ 70,000 refuses `high` on an 86,000 reservation it would never have made, where K10 just above
202
+ measures the engine clamping that same 70,000 descriptor — under a LARGER 31,999-token
203
+ variant — to 64,000, comfortably under the budget. A row BELOW it goes silent for any budget
204
+ between the two: the guard is `budget < ceiling`, so no fit runs; the descriptor Amicus writes
205
+ is the row itself (`min(ceiling, budget)`) and the engine adds N on top of THAT, so the leg
206
+ overshoots the budget whenever N exceeds the gap. The two catalogs can disagree without either
207
+ being stale in the ordinary sense: the engine serves its bundled catalogue until its startup
208
+ models.dev fetch lands, which moved a measured ceiling by 104,858 tokens (kimi 1,048,576 cold
209
+ vs 943,718 warm), so `amicus models --refresh` can open the gap rather than close it. No
210
+ `doctor` row detects either direction: `output-budget` compares no ceiling against the engine's
211
+ (it counts the routes with a known catalog ceiling, and warns only on starvation or a route
212
+ without one), and `catalog` is an age heuristic that compares no numbers.
213
+
214
+ This addresses reservation *rejections* and *clips*. It does **not** stop a reasoning-heavy model
215
+ from spending its whole allowance on reasoning and emitting nothing — that is governed by reasoning
216
+ effort — which `--thinking` now delivers to the engine as its `variant` field (#218 PR 4), checked
217
+ against what the model declares before anything is sent, on solo and fanout legs; a council seat has
218
+ no effort knob yet (filed).
219
+ Lowering the budget makes such a leg fail faster and cheaper; raising it gives the reasoning more
220
+ room; neither makes it produce output — but since #218 PR 3 the failure is at least *named*: the leg
221
+ ends `error` with an `OUTPUT_LENGTH:` reason instead of `complete` with nothing, or with its thinking
222
+ as the review (see [Troubleshooting](./troubleshooting.md#headless-leg-fails-with-output_length)).
101
223
 
102
224
  ---
103
225
 
@@ -206,7 +328,7 @@ These variables control the polling loop that drives headless sessions. The defa
206
328
  | `AMICUS_STABLE_IDLE_POLLS` | Number of consecutive idle polls required when no explicit completion signal is received (approximately 60 s at the 2 s default). This is the fallback heuristic for models or SDK versions that don't emit a clean completion event. | `30` |
207
329
  | `AMICUS_MAX_CONSECUTIVE_POLL_FAILURES` | Consecutive poll failures before the headless runner bails. At the 2 s interval this is approximately 30 s. Prevents a dead server from burning the full session timeout on futile polls. | `15` |
208
330
  | `AMICUS_TOOL_CALL_STALL_MS` | How long a tool call may sit pending with **no** result and no output growth before the leg is failed with `Tool call stalled: <tool>` and its OpenCode session aborted. This is the wedge guard: it targets a leg producing nothing at all, and it is skipped while a tool-settle deferral is active (`AMICUS_TOOL_SETTLE_GRACE_MS` owns that decision instead, and ends in a completion rather than a failure). **`0` is ignored** — it falls back to the default rather than disabling the guard, because a `0` threshold would kill every leg on its first poll. There is no way to switch this off; raise it if you legitimately run very long single tool calls. | `180000` |
209
- | `AMICUS_NO_OUTPUT_BACKSTOP_MS` | Fail a headless leg fast when the model has produced no output, reasoning, or tool calls for this long — the "accepted but not serving" class. Disarms permanently on the first sign of activity, so slow cold-prefill local models are unaffected. **Set `0` (or negative) to disable the backstop entirely** — silent legs then run to the ordinary timeout. | `300000` |
331
+ | `AMICUS_NO_OUTPUT_BACKSTOP_MS` | Fail a headless leg fast when the model has produced no output, reasoning, or tool calls for this long — the "accepted but not serving" class. Disarms permanently on the first sign of activity, so slow cold-prefill local models are unaffected. **Set `0` (or negative) to disable the backstop entirely** — silent legs then run to the ordinary timeout. A window that fires while a `--thinking` leg is still inside its declaration wait (#218 PR 4; bounded at five seconds — one `/config/providers` read on a warm, declared model, the full wait only on a cold or unknown one) cuts that wait short: the leg dies `NO_OUTPUT_BACKSTOP` before the level is validated and nothing is sent. Each individual read carries its own deadline — two seconds, or whatever is left of the five, whichever is shorter — so a catalogue endpoint that accepts and never answers cannot stretch that wait past it (council #235 r2, r4). A request already in flight when the backstop fires may still reach the provider — the backstop has never been able to recall a send (the v4.6.2 orphaned-promise design) — and the record then omits the `variant` it cannot vouch for. | `300000` |
210
332
  | `AMICUS_USAGE_SETTLE_POLLS` | How many extra `getMessages` reads run **after** a leg has already finished, to catch provider usage/cost that lands milliseconds after the completion signal (measured: real paid legs losing their cost by 29 ms and 155 ms). The loop breaks early as soon as every assistant message carries usage, so the common case is one extra read. **Set to `0` to disable the reconciliation entirely** — legs then report whatever usage was present at completion, which can be `$0` on a leg that really did cost money. | `3` |
211
333
  | `AMICUS_USAGE_SETTLE_INTERVAL_MS` | Delay between those settle reads. **`0` is honoured and means no delay** — the reads run back to back. It does **not** disable the reconciliation (that is `AMICUS_USAGE_SETTLE_POLLS=0`); it only removes the gap between attempts. | `400` |
212
334
  | `AMICUS_USAGE_SETTLE_CALL_TIMEOUT_MS` | Per-call deadline for a settle read and for the child-session (subagent) spend walk. Deliberately much tighter than `AMICUS_POLL_CALL_TIMEOUT_MS`: the leg is already finished, so a hung read must not add 30 s × 3 to a run's wall time. The effective value is the **smaller** of this and `AMICUS_POLL_CALL_TIMEOUT_MS`, so raising it above that has no effect. **`0` is honoured and means no timer is armed at all** — a hung settle read or subtree walk would then wait indefinitely. | `5000` |
package/docs/council.md CHANGED
@@ -343,6 +343,15 @@ them their own **Notes:** list and keeps them out of `## What was lost`. Related
343
343
  [`amicus council stats`](#amicus-council-stats) on an empty ledger now names where rows come from
344
344
  instead of implying that no council ever ran.
345
345
 
346
+ **A review cut at its output reservation is announced, not lost** (#218 PR 3). When a Stage-1 leg's
347
+ assistant message carries `finish: 'length'` and still delivered answer text, the run prints a
348
+ `Note:` on the `output-truncated` channel (`kind: "info"`, so it never degrades the run or moves the
349
+ exit code) naming the seat and the engine's reasoning/output token counts for the leg, with `Try:
350
+ raise outputBudget…`. The chair packet marks that review too — its header reads `--- Review by
351
+ <seat> — CUT at its output reservation (…) ---` — so the chair weighs it as partial. A length stop
352
+ with **no** answer text is a dead leg whose reason starts `OUTPUT_LENGTH:` — see
353
+ [Troubleshooting](./troubleshooting.md#headless-leg-fails-with-output_length).
354
+
346
355
  **Read the tiers correctly.** A task run's report carries the line *"Tiers report peer concurrence,
347
356
  never verification."* directly under the tier counts, and the chair's own packet carries the same
348
357
  caveat beside the adjudications it is weighing. Peer agreement on a generative bench is correlation
@@ -1,6 +1,6 @@
1
1
  # Documentation System
2
2
 
3
- Canonical reference for the auto-documentation system that keeps CLAUDE.md in sync with the codebase.
3
+ Canonical reference for the auto-documentation system that keeps the generated docs in sync with the codebase.
4
4
 
5
5
  ## Overview
6
6
 
@@ -10,7 +10,9 @@ Inspired by [OpenAI's Harness Engineering](https://openai.com/index/harness-engi
10
10
 
11
11
  ## Auto-Generated Sections
12
12
 
13
- Sections between `<!-- AUTO:name -->` markers in CLAUDE.md are maintained by `scripts/generate-docs.js`. Do NOT edit these by hand.
13
+ Sections between `<!-- AUTO:name -->` markers are maintained by `scripts/generate-docs.js`. Do NOT edit these by hand.
14
+
15
+ Each marker is routed to the document that owns it by the `MARKER_TARGETS` table in `scripts/generate-docs.js`. A marker with no entry there defaults to CLAUDE.md. Today both markers live in [architecture-map.md](architecture-map.md), which keeps the ~15k-token generated inventory out of the file loaded into every agent context.
14
16
 
15
17
  ### Marker Format
16
18
 
@@ -22,27 +24,28 @@ Sections between `<!-- AUTO:name -->` markers in CLAUDE.md are maintained by `sc
22
24
 
23
25
  ### Current Markers
24
26
 
25
- | Marker | Content | Source |
26
- |--------|---------|--------|
27
- | `tree` | ASCII directory tree with JSDoc annotations | Filesystem scan of `bin/`, `src/`, `electron/`, `scripts/`, `evals/` (note: `tests/` is NOT included) |
28
- | `modules` | Markdown table of all `src/**/*.js` modules | JSDoc description + `module.exports` extraction |
27
+ | Marker | Target document | Content | Source |
28
+ |--------|-----------------|---------|--------|
29
+ | `tree` | `docs/architecture-map.md` | ASCII directory tree with JSDoc annotations | Filesystem scan of `bin/`, `src/`, `electron/`, `scripts/`, `evals/` (note: `tests/` is NOT included) |
30
+ | `modules` | `docs/architecture-map.md` | Markdown table of all `src/**/*.js` modules | JSDoc description + `module.exports` extraction |
29
31
 
30
32
  ### How It Works
31
33
 
32
34
  1. `scripts/generate-docs.js` scans the codebase
33
35
  2. For each marker, it generates new content from the source of truth (filesystem, JSDoc)
34
- 3. It replaces the content between the open/close marker tags
35
- 4. If CLAUDE.md changed, it auto-stages the file with `git add`
36
+ 3. It replaces the content between the open/close marker tags, in whichever document `MARKER_TARGETS` routes that marker to
37
+ 4. It auto-stages every document it wrote with `git add`
36
38
 
37
39
  ### Adding a New Auto-Generated Section
38
40
 
39
- 1. Add a new marker pair to CLAUDE.md (short lowercase name, no hyphens required but keep it terse):
41
+ 1. Add a new marker pair to the document that should own it (short lowercase name, no hyphens required but keep it terse):
40
42
  ```markdown
41
43
  <!-- AUTO:my-section -->
42
44
  <!-- /AUTO:my-section -->
43
45
  ```
44
46
  2. Add a generator function in `scripts/generate-docs.js` (or `scripts/generate-docs-helpers.js`)
45
47
  3. Add it to the `generated` map in `main()`
48
+ 4. If it should NOT live in CLAUDE.md, add `markerName: 'relative/path.md'` to `MARKER_TARGETS`
46
49
  4. Add tests in `tests/scripts/generate-docs.test.js`
47
50
 
48
51
  ## Cross-Link Validation
package/docs/testing.md CHANGED
@@ -30,7 +30,7 @@ npm test tests/context.test.js # Single file (preferred during dev)
30
30
  npm test -- --coverage # Coverage report
31
31
  npm test -- -t "should extract" # Run tests matching pattern
32
32
 
33
- npm run test:integration # Integration tier, KEYLESS — credentials scrubbed, paid suites skip (free, ~10s)
33
+ npm run test:integration # Integration tier, KEYLESS — credentials scrubbed, paid suites skip (free, ~10s plus the ~15s engine-flag canary)
34
34
  npm run test:integration:live # Integration tier with real keys — SPENDS MONEY (serial: --runInBand is baked in)
35
35
  npm run test:all # Unit + integration with real keys — SPENDS MONEY (not a gate anywhere)
36
36
  npm run test:e2e:mcp # MCP E2E with real repomix (requires OPENROUTER_API_KEY)
@@ -103,6 +103,7 @@ Integration tests verify source-level invariants without mocking. They read actu
103
103
  | Test File | What It Verifies |
104
104
  |-----------|-----------------|
105
105
  | `spawn-pipe-deadlock.integration.test.js` | `spawnSidecarProcess()` in `src/mcp-server.js` uses `ignore` (not `pipe`) for stdio, no `detached: true`, uses `child.unref()` |
106
+ | `probe-flag-canary.integration.test.js` | Engine honours `OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX` on the wire (five `scripts/probe-max-tokens.js` rows: A, C3, K6, K12, K13, in the probe's own keyless sandbox; ~15 s, zero spend). The pinned engine's reading of the flag `outputBudget` relies on above 32,000 — an engine bump that drops it turns CI's keyless job red |
106
107
  | `electron-headless-mode.test.js` | `electron/main.js` gates `mainWindow.show()` behind `AMICUS_HEADLESS_TEST` env var |
107
108
 
108
109
  These tests catch regressions in critical spawn/process configuration that would be hard to debug in production.
@@ -252,6 +252,82 @@ The two clauses are independent: either can appear without the other, and the sk
252
252
 
253
253
  ---
254
254
 
255
+ ## Headless Leg Fails with `OUTPUT_LENGTH`
256
+
257
+ **Symptom:** A headless leg (`amicus start --no-ui`, or one leg of a `fanout`/council run) ends `error` with a reason starting `OUTPUT_LENGTH: the provider stopped at the max_tokens reservation (finish 'length') and no answer text arrived — 32000 reasoning / 0 output tokens; outputBudget is unset — the engine's 32000 default reservation governs — raise outputBudget …`. The middle clause may instead read `and only reasoning was streamed, no answer text` — the provider showed its reasoning and nothing else. On a council run the seat is a dead leg and is retried once like any other death — unchanged for the no-output shape; new for the promoted-thinking shape, which 4.9.3 counted as a review — and if the retry dies too the seat is announced (`Notice: seat … did not review — the leg ended 'error': OUTPUT_LENGTH: …`).
258
+
259
+ **Cause:** The model spent its whole output reservation reasoning and never started the answer. Every clause is an observation, not a guess: `finish 'length'` is the engine's record of the provider's own stop reason; the two counts are the engine's token record for that message (on OpenAI-compatible routes reasoning and output are split; on the direct Anthropic route everything lands in `output` and reasoning reads 0 — the message still says `finish 'length'`); the budget clause is the value the engine serving the leg was started with — read once at spawn and carried on the server handle (`config.json` at that moment; a later edit does not change what the leg reserved); with no budget set it names the ambient `OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX` the engine was started with when there was one (a plain positive integer is honoured; anything else falls back to 32,000 — measured, D1/D2) and the engine's 32,000 default otherwise. A server handle amicus did not start carries no spawn value; then `config.json` is read when the death is named. The reservation is 32,000 by default (see [Output budget](./configuration.md#output-budget-outputbudget)). The likeliest driver is reasoning effort — OpenRouter applies a model's default effort when none is sent, and `--thinking` is not available on a council seat (on a solo or fanout leg it reaches the engine as its `variant` field since #218 PR 4) — so raising the budget gives the reasoning room, and the leg still bills for it.
260
+
261
+ **Confirm:** `finish: 'length'` is on the leg's `metadata.json` and on its row in `~/.config/amicus/spend-ledger.jsonl` (the `finish` field, present only when the engine recorded one), beside the token counts. A council run's `run.json` carries it on the leg document.
262
+
263
+ **Fix:** Raise `outputBudget` in `config.json` (the reservation is `min(outputBudget, the model's ceiling)`; see the five bullets in [Output budget](./configuration.md#output-budget-outputbudget)), or seat a model whose default effort fits the reservation. If the same seat dies the same way on its retry, the retry cost you the reservation twice — a council seat has no effort knob (filed), so raise the budget or drop the seat; on a solo or fanout leg, lower `--thinking` (it reaches the engine since #218 PR 4). A leg that took longer than `AMICUS_NO_OUTPUT_BACKSTOP_MS` to reason with nothing visible dies as `NO_OUTPUT_BACKSTOP` first, not as this — see that section above.
264
+
265
+ ---
266
+
267
+ ## `--thinking` Refused Before Anything Is Sent (`VARIANT_UNDECLARED`, `VARIANT_OVER_BUDGET`)
268
+
269
+ **Symptom:** A solo run (`amicus start --no-ui`) or a fanout leg ends `error` with a reason starting
270
+ `VARIANT_UNDECLARED: openrouter/moonshotai/kimi-k3 does not declare a
271
+ 'medium' variant — the engine's catalogue lists low, high, max for it …` or `VARIANT_OVER_BUDGET:
272
+ the 'high' variant on anthropic/claude-haiku-4-5 carries a 16000-token thinking budget that the
273
+ engine adds ON TOP of the reservation on this route … with outputBudget 24000 this leg would
274
+ reserve 40000 …` (an interactive `amicus start` ends the same way with the reason prefixed
275
+ `Session setup failed: `). Nothing was billed: the request was never sent. On a fanout the other
276
+ legs run.
277
+
278
+ **Cause:** Since #218 PR 4, `--thinking` is sent as the engine's `variant` field and checked first
279
+ against what the engine's own catalogue declares for that model (`/config/providers`). A level the
280
+ model does not declare would be a silent no-op the engine still echoes on the artifact (probe
281
+ F3/M7), so it is refused. The declared set is read from the engine's catalogue at that moment: on a
282
+ cold `~/.cache/opencode` (first engine start after an install or a cleared cache) the bundled
283
+ catalogue can declare a different set than the live one for the same model (PR 4 record:
284
+ `openrouter/anthropic/claude-haiku-4.5` `high`/`max` cold, `low`/`medium`/`high` warm), so a level
285
+ refused on one run can be accepted on the next; the reason lists the set in force, or says the row
286
+ declares none at all. The dump says whose row it is: Amicus writes exactly one cell into a model's
287
+ entry (`limit`, at `src/utils/config.js:406`), so a row that also carries a release
288
+ date, family, display name, pricing or capabilities reads as a declaration, and an empty
289
+ `variants` there is a real answer (record M23). That dump is the engine's MERGED view of its own
290
+ catalogue and your `opencode.json`, so declaring model metadata there (a display name, family,
291
+ release date, pricing or capabilities) makes Amicus read the row as declared: a model you add that
292
+ way with no `variants` block is refused rather than waited for — add the block, or omit
293
+ `--thinking`. A declared
294
+ level whose entry carries a thinking budget the engine adds on top of the reservation (direct
295
+ Anthropic Haiku 4.5 — M2; Opus 4.5 declares the same shape, M0) is refused when `outputBudget`
296
+ is below the model's ceiling — or when no ceiling is declared anywhere, since nothing then clamps
297
+ the sum — because the leg would reserve more than the budget promises. The
298
+ reason names the model, the level, what the catalogue lists (or that it lists nothing at all), and
299
+ — for the budget case — the exact reservation, the budget and the ceiling.
300
+
301
+ **Confirm:** The reason is on the session's `metadata.json` (`reason`) and, for a fanout, on the
302
+ leg document in `wave.json`; the ledger row for it reads `status: "error"` with zero tokens and no
303
+ `variant` (nothing was spent, but the run is still attributed — its `cost.source` is `unknown`,
304
+ not a price). `amicus models` does not list variants; the declared set is in the reason itself.
305
+
306
+ **Fix:** Pick a level the reason lists, or omit `--thinking` to run at the provider's own default.
307
+ For `VARIANT_OVER_BUDGET`: raise `outputBudget` to at least the ceiling the reason names (the sum is
308
+ then clamped to the ceiling — the number the reason names is Amicus's own catalog's ceiling for the
309
+ model, which is what the fit can read once a budget is set, M3; for a model Amicus's catalog has no
310
+ row for it is instead the engine's own ceiling, straight from the dump, K5/K12). When neither
311
+ Amicus's catalog nor the dump declares a ceiling, the reason names none and says so: declare a
312
+ `limit` for the model in your `opencode.json` instead — its `output` has to leave room for the
313
+ thinking budget the engine adds on top, so at most `outputBudget` minus that budget (a value at or
314
+ above `outputBudget` silences the check without shrinking what the leg reserves, and declaring one
315
+ never clamps the sum — only the model's real ceiling does that, K3/K9/K10) — or clear
316
+ `outputBudget`. You can also route the model
317
+ through OpenRouter (a variant leaves the reservation at the budget there — M1, M9), or use an
318
+ adaptive-thinking model such as `claude-sonnet-5`. When the reason says to refresh first, the
319
+ ceiling it named came from Amicus's catalog rather than the engine, and the two can disagree. A
320
+ model the engine's catalogue does not know yet (a custom or local model, or one newer than the
321
+ engine's bundled list before its startup refresh lands) is never refused: the level is sent after a
322
+ bounded wait, the run logs `Variant sent unverified`, and the same note is printed as a `Notice:`
323
+ line on stderr — the structured log alone is dropped at the shipped default log level, so stderr
324
+ is where you will actually see it. A budget changes none of that: whether the level is declared is
325
+ decided identically with and without one, and `VARIANT_OVER_BUDGET` above is the only refusal a
326
+ budget can add. A model the engine's catalogue does not know yet still waits and is still sent
327
+ unverified, with `variantUnverified: true` on the leg document.
328
+
329
+ ---
330
+
255
331
  ## Multiple Active Sessions / Wrong Session Picked Up
256
332
 
257
333
  **Symptom:** Amicus resumes or reads from the wrong session.
package/docs/usage.md CHANGED
@@ -24,7 +24,7 @@ amicus setup --api-keys # Open just the API-key step
24
24
  amicus setup --add-alias fast=google/gemini-3.1-flash-lite-preview # bare canonical, direct-first
25
25
  amicus models # List the live catalog
26
26
  amicus models --search gemini # Filter by substring
27
- amicus models --refresh # Force-fetch from provider APIs
27
+ amicus models --refresh # Force-fetch from provider APIs (+ ceilings from models.dev)
28
28
  amicus models --check # Audit aliases against catalog
29
29
  amicus mcp # Start MCP server (stdio transport)
30
30
  amicus update # Update to latest version
@@ -76,7 +76,7 @@ amicus start --model deepseek --prompt "Generate tests" --no-ui --timeout 30
76
76
  | `--context-since <duration>` | Time filter (e.g. `2h`); overrides turns. | |
77
77
  | `--context-max-tokens <N>` | Max context tokens. | 80000 |
78
78
  | `--no-context` | Skip parent conversation history. | off |
79
- | `--thinking <level>` | Reasoning effort: `none`, `minimal`, `low`, `medium`, `high`, `xhigh`. | model default |
79
+ | `--thinking <level>` | Reasoning effort: `none`, `minimal`, `low`, `medium`, `high`, `xhigh`, `max`. A level the model does not declare is refused before anything is sent. A model the engine's catalogue does not know in time is sent the level unverified (`variantUnverified: true` on the record, and a `Notice:` line on stderr). Whether the level is declared does not depend on `outputBudget`; the one refusal a budget can add is `VARIANT_OVER_BUDGET`, when a declared level's own thinking budget would push the reservation past the budget. A model whose row the engine's own catalogue supplied is judged on that row: an empty variants set is a refusal, never an unverified send. Only a model the engine has no row for — its `/config/providers` entry is nothing but the descriptor Amicus registered — waits and is then sent unverified. `continue` and `resume` do not take a level — it belongs to the run that starts a session, and both reject `--thinking` rather than ignore it. | provider default (nothing sent) |
80
80
  | `--summary-length <length>` | Fold summary verbosity: `brief`, `normal`, `verbose`. | `normal` |
81
81
  | `--mcp <spec>` | Add an MCP server (`name=url` or `name=command`). | |
82
82
  | `--mcp-config <path>` | Path to an `opencode.json` with MCP config. | |
@@ -303,7 +303,7 @@ Then invoke it with `--pack <name|path>` on `start` / `fanout` / `council run`
303
303
  | `fanout` | `bench` (a saved council name, or an array of ≥2 members) | — | `timeout`, `maxCost`, `gateway`, `agent`, `thinking`, `summaryLength`, `noContext`, `contextTurns`, `contextMaxTokens` |
304
304
  | `solo` | `model` | — | `timeout`, `maxCost`, `gateway`, `agent`, `thinking`, `summaryLength`, `noUi`, `noContext`, `contextTurns`, `contextMaxTokens` |
305
305
 
306
- `council` packs do **not** accept `agent`, `thinking`, or `summaryLength` — they were inert on every surface (no council code path, CLI or MCP, ever reads a pack-filled one; the engine hardcodes agent `Plan`/summaryLength `verbose`), so they were dropped before release rather than shipped as dead weight a pack author would reasonably expect to work. A `council` pack that still sets one fails `pack save` with `PACK_INVALID`, naming the key. They remain valid, and functional, on `fanout`/`solo` packs.
306
+ `council` packs do **not** accept `agent`, `thinking`, or `summaryLength` — they were inert on every surface (no council code path, CLI or MCP, ever reads a pack-filled one; the engine hardcodes agent `Plan`/summaryLength `verbose`), so they were dropped before release rather than shipped as dead weight a pack author would reasonably expect to work. A `council` pack that still sets one fails `pack save` with `PACK_INVALID`, naming the key. They remain valid, and functional, on `fanout`/`solo` packs. Those releases recorded `medium` on EVERY session's metadata, **a fanout leg's included**, whether or not the flag was typed (a level nothing ever sent), so a pack saved with `pack save --from-run` on 4.9.3 or earlier copied it into `options.thinking` on **fanout packs as well as solo ones** — where it then applies to every seat of the bench at once. Such a pack now SENDS it: refused on every model that does not declare `medium` (kimi-k3, Haiku 4.5, deepseek-v4-pro among the curated routes), and on a model that DOES declare it the level really goes out — so a pack that was inert can now change a run's cost and behaviour. Delete the key or re-save the pack from a run that requested a level.
307
307
 
308
308
  Every kind may also carry `description`, `version` (semver, default `1.0.0`), and `briefing.template` (a template **reference**, not rendered text — a pack never captures briefing prose).
309
309
 
@@ -381,7 +381,7 @@ Amicus does **not** ship a frozen table of model names. Aliases and validation r
381
381
  ```bash
382
382
  amicus models # List the catalog
383
383
  amicus models --search gemini # Filter by substring over id and name
384
- amicus models --refresh # Force-refresh from provider APIs
384
+ amicus models --refresh # Force-fetch from provider APIs (+ ceilings from models.dev)
385
385
  amicus models --check # Audit your aliases against the catalog
386
386
  amicus models --check --strict # + exit non-zero on curated per-gateway drift too
387
387
  amicus models --check --live # + probe every stored alias with a real leg (spends)
@@ -520,6 +520,8 @@ usage error when combined with `continue`, `resume`, or `--retry-failed` — the
520
520
  set. An untagged parent still leaves the key absent (not `null`) on the new metadata, and its spend
521
521
  row still groups under `(unattributed)`, exactly as an untagged `start`/`fanout` would.
522
522
 
523
+ **Effort level on a reopen.** Neither `continue` nor `resume` sends an effort level — both reject `--thinking` outright and a level is not carried across a reopen — so when the session being reopened recorded one, each prints a `Notice:` on stderr naming the level it is dropping and saying the leg runs at the provider's default. That line reports what the session's metadata RECORDS, not what was typed: 4.9.3 and earlier stamped `thinking: medium` on every session whether or not the flag was given (and never sent it), so on a session from those releases the Notice names that stamp and says so in the same breath.
524
+
523
525
  **`amicus status <id>` output.** Human-readable:
524
526
 
525
527
  ```
@@ -538,7 +540,7 @@ $ amicus status demo123 --json
538
540
  "taskId": "demo123",
539
541
  "status": "complete",
540
542
  "elapsed": "5m 0s",
541
- "version": "4.9.3",
543
+ "version": "4.9.4",
542
544
  "model": "google/gemini-2.5-flash",
543
545
  "phase": "terminal"
544
546
  }
@@ -597,6 +599,7 @@ Runs every check below, in order, and prints a ✓/⚠/✗ line for each plus a
597
599
  | `default-model` | Your default model alias resolves | error |
598
600
  | `catalog` | Model-catalog cache present and within the 24h TTL | warn |
599
601
  | `aliases` | Your configured aliases still resolve against the catalog | warn |
602
+ | `output-budget` **(#218)** | `outputBudget` is a positive integer and says which alias routes have a known catalog ceiling; whether an ambient `OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX` is honoured or overridden; flags a malformed `outputBudget`, and a malformed ambient flag whenever it would govern — beside a valid budget the ambient value is only reported as overridden (only a plain positive integer is measured to be honoured; `64000abc` and `0` fell back to 32,000 without a word) — and a budget that takes at least half of a route's context window; lists `openai/` alias routes apart (no output reservation on a leg that resolves direct, and which gateway a leg takes is a launch-time decision the row cannot read) | warn |
600
603
  | `anthropic-base-url` | `ANTHROPIC_BASE_URL` isn't host-form (host-form 404s every direct-Anthropic leg unless normalized) | warn |
601
604
  | `opencode-bin` | The OpenCode engine binary is on `PATH` | error |
602
605
  | `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) |
@@ -614,6 +617,8 @@ Runs every check below, in order, and prints a ✓/⚠/✗ line for each plus a
614
617
 
615
618
  **`local-providers`** probes every configured local provider (2s timeout each) the same way `amicus provider test` does, and reports per-id reachability in one line, e.g. `ollama: 3 models @ http://127.0.0.1:11434/v1; my-vllm: unreachable @ http://127.0.0.1:8000/v1`. No providers configured at all is a plain `ok` ("none configured") — this check can never fail your doctor run outright, only warn: a napping `ollama serve` isn't treated as broken setup.
616
619
 
620
+ **`output-budget`** reads the stored `outputBudget` and the `OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX` this process sees, and reports what the value reaches (see [Output budget](configuration.md#output-budget-outputbudget)): how many alias routes have a known catalog ceiling and which do not (the engine clamps those its own catalog knows; a model neither knows receives the budget as-is and may be refused by a provider that enforces its ceiling — a warning, with the remedy of lowering the budget, only when the budget is above the engine's 32,000 default, since below it the flag never raises what goes out; the same rule decides whether a missing catalog cache is a warning), and whether a budget takes at least half of a route's context window. An `openai/` alias route is listed apart from both counts: on a leg that resolves **direct** to that provider the engine drives it through the Responses API, whose request carries no output-limit field at all (probe M5/M13/M22), so neither the stored budget nor the ambient flag reaches it. Which gateway a leg takes is a launch-time decision this row cannot read (`--gateway`, `routing.prefer`, key presence), and the `openrouter/openai/…` form of the same model does carry the reservation (M1/M9). The malformed cases are the ones worth the row: only a plain decimal integer is measured to be honoured — `64000abc` and `0` were ignored *silently* and sent 32,000 (probe rows D1/D2) — so any other form is reported as unmeasured rather than healthy, and `doctor` is where that surfaces — including a malformed `outputBudget` sitting next to an ambient flag, where the row says which value actually governs and — if the ambient value is valid — gives it the same route analysis a budget gets.
621
+
617
622
  `--fix` self-heals five of the checks above in place: reprovisions Electron, copies the OpenCode engine into a broken npx-cache install, removes a duplicate legacy MCP entry, sweeps orphaned session-index tmp files, and sweeps orphaned per-session metadata tmp files (both tmp sweeps only ones older than 60s). It does **not** start a local server for you — `local-providers` stays a warning until you start the server yourself.
618
623
 
619
624
  Exit code is `1` if anything is `error`, else `0` (same rule drives `--json`'s `ok` field).
@@ -883,7 +888,7 @@ Every tool below also takes an optional `project` — an absolute path naming th
883
888
  - `prompt` — the task briefing: objective, background, files of interest, success criteria.
884
889
  - `agent` — OpenCode agent mode (`Chat`, `Plan`, `Build`); see [OpenCode Agent Types](#opencode-agent-types) below.
885
890
  - `noUi` — run headless instead of opening the Electron window. Default `false`.
886
- - `thinking` — reasoning effort (`low` | `medium` | `high`). Default `medium`.
891
+ - `thinking` — reasoning effort (`none` | `minimal` | `low` | `medium` | `high` | `xhigh` | `max`). Omitted: nothing is sent and the provider's own default effort governs. A level the model does not declare is refused before anything is sent. A model the engine's catalogue does not know in time is sent the level unverified (`variantUnverified: true` on the record, and a `Notice:` line on stderr). Whether the level is declared does not depend on `outputBudget`; the one refusal a budget can add is `VARIANT_OVER_BUDGET`, when a declared level's own thinking budget would push the reservation past the budget. A model whose row the engine's own catalogue supplied is judged on that row: an empty variants set is a refusal, never an unverified send. Only a model the engine has no row for — its `/config/providers` entry is nothing but the descriptor Amicus registered — waits and is then sent unverified.
887
892
  - `timeout` — headless timeout in minutes (default 15); applies only when `noUi` is true.
888
893
  - `contextTurns` — max parent-conversation turns to include. Default 50; the MCP twin of `--context-turns`.
889
894
  - `contextSince` — time window for parent context (`30m`, `2h`, `1d`); overrides `contextTurns` when set. The MCP twin of `--context-since`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "amicus",
3
- "version": "4.9.3",
3
+ "version": "4.9.4",
4
4
  "mcpName": "io.github.BourbonDog/amicus",
5
5
  "description": "Multi-model LLM Council + parallel AI window for Claude Code. Run structured council reviews across Gemini, GPT, DeepSeek and more — or fork a conversation to any model and fold the results back.",
6
6
  "keywords": [
@@ -14,6 +14,7 @@
14
14
  "count": { "type": "number" },
15
15
  "models": { "type": "array", "items": { "type": "object" } },
16
16
  "lastRefreshAttempt": { "type": ["number", "null"] },
17
- "lastRefreshError": { "type": ["string", "null"] }
17
+ "lastRefreshError": { "type": ["string", "null"] },
18
+ "ceilingEnrichment": { "type": ["object", "null"] }
18
19
  }
19
20
  }
@@ -24,6 +24,19 @@
24
24
  "usage": { "type": ["object", "null"] },
25
25
  "pack": { "type": "object" },
26
26
  "tag": { "type": "string" },
27
+ "finish": {
28
+ "type": "string",
29
+ "description": "#218 PR 3, optional. The engine's finish reason for the leg's last assistant message ('stop', 'length', 'tool-calls', …), copied verbatim from the assistant message's `finish`. EMIT-WHEN-SET like `ttftMs`: absent means the engine recorded none (the message never finalized, or an older engine). 'length' means the provider stopped at the max_tokens reservation — with answer text that is a cut review (announced as a Note on the `output-truncated` channel); without, the leg is status 'error' with an `OUTPUT_LENGTH:` reason."
30
+ },
31
+ "variant": {
32
+ "type": "string",
33
+ "description": "#218 PR 4, optional. The effort level amicus SENT as the engine's `variant` prompt field for this leg, after checking the engine's own declaration for the model (/config/providers). EMIT-WHEN-SENT: absent means no --thinking was requested, or the request was refused before anything was sent (then `status` is 'error' and `error` starts VARIANT_UNDECLARED or VARIANT_OVER_BUDGET — prefixed `Session setup failed: ` on an interactive `amicus start`). Not the engine's own `variant` echo, which it records even for a level it dropped (probe F3/M7). A leg whose no-output backstop fired before the prompt send resolved carries no `variant` even though one went out — the send result never arrived, so the record cannot claim it."
34
+ },
35
+ "variantUnverified": {
36
+ "type": "boolean",
37
+ "enum": [true],
38
+ "description": "#218 PR 4, optional, emit-when-true. The variant was sent to a model the engine's catalogue did not know within the wait (its row carries none of the cells a catalogue fills — no display name, family, release date, pricing or capabilities — only the descriptor amicus registered: a model newer than the engine's bundled catalogue before its startup refresh landed, or a custom/local model): the engine applied it only if it learned the model before building the request, and dropped it silently otherwise; amicus cannot tell which."
39
+ },
27
40
  "ttftMs": {
28
41
  "type": "integer",
29
42
  "minimum": 0,
@@ -236,14 +236,7 @@ amicus start \
236
236
  - `--context-turns <N>`: Max conversation turns to include (default: 50)
237
237
  - `--context-since <duration>`: Time filter for context (e.g., `2h`, `30m`, `1d`). Overrides `--context-turns`.
238
238
  - `--context-max-tokens <N>`: Max context size (default: 80000)
239
- - `--thinking <level>`: Model thinking/reasoning effort level:
240
- - `none` - No extended thinking
241
- - `minimal` - Minimal thinking (may be adjusted if unsupported by model)
242
- - `low` - Low thinking effort
243
- - `medium` - Medium thinking effort (default)
244
- - `high` - High thinking effort
245
- - `xhigh` - Extra high thinking effort
246
- Note: If the model doesn't support the specified level, it will be automatically adjusted.
239
+ - `--thinking <level>`: Reasoning effort — `none`, `minimal`, `low`, `medium`, `high`, `xhigh`, `max` (the levels the curated routes declare between them). Omit it for the provider's own default effort (nothing is sent then). A level the model does not declare is refused before anything is sent (`VARIANT_UNDECLARED`, naming the declared set); a declared level whose thinking budget would push the reservation over `outputBudget` on the direct Anthropic route is refused too (`VARIANT_OVER_BUDGET`); a model the engine's catalogue does not know in time is sent the level unverified (`variantUnverified: true` on the record). Nothing is adjusted.
247
240
  - `--summary-length <length>`: Summary verbosity:
248
241
  - `brief` - Concise summary
249
242
  - `normal` - Standard summary (default)
@@ -14,6 +14,7 @@ const electronMcpCheck = require('./utils/doctor-electron-mcp-check');
14
14
  const localProvidersCheck = require('./utils/doctor-local-providers-check');
15
15
  // v4.6.2 PR1 (spec §4) — the 'anthropic-base-url' check body.
16
16
  const baseUrlCheck = require('./utils/doctor-base-url-check');
17
+ const outputBudgetCheck = require('./utils/doctor-output-budget-check'); // #218 PR 2 — the 'output-budget' row.
17
18
  // B3 (council review of PR 198, issue 195) — the 'aliases' check body,
18
19
  // including its --fix repair of fabricated bare ids. Same split rationale.
19
20
  const aliasCheck = require('./utils/doctor-alias-check');
@@ -45,6 +46,7 @@ function realDeps() {
45
46
  resolveModel: () => require('./utils/config').resolveModel(),
46
47
  readCache: () => require('./utils/model-catalog').readCache(),
47
48
  collectAliasSources: () => require('./utils/alias-audit').collectAliasSources(),
49
+ readOutputBudgetRaw: () => (require('./utils/config').loadConfig() || {}).outputBudget, // #218 PR 2: as stored, so a malformed value is echoed
48
50
  findStaleAliases: (s, c) => require('./utils/alias-audit').findStaleAliases(s, c),
49
51
  findDriftedStoredAliases: (s, c) => require('./utils/alias-audit').findDriftedStoredAliases(s, c),
50
52
  // B3: the narrow fabricated-bare-id repair class (pure detection) + the
@@ -171,6 +173,7 @@ async function runDoctorChecks(depsOverride = {}) {
171
173
  // utils/doctor-alias-check.js for the check body and utils/alias-audit.js's
172
174
  // findFabricatedAliasRepairs for the detection rule.
173
175
  checks.push(guard('aliases', 'Model aliases', () => aliasCheck.evaluateAliasesCheck(d)));
176
+ checks.push(guard('output-budget', 'Output budget', () => outputBudgetCheck.evaluateOutputBudget(d))); // #218 PR 2
174
177
 
175
178
  checks.push(guard('anthropic-base-url', 'ANTHROPIC_BASE_URL',
176
179
  () => baseUrlCheck.evaluateAnthropicBaseUrl(d)));
@@ -8,7 +8,7 @@
8
8
 
9
9
  'use strict';
10
10
 
11
- const { validateTaskId, validateTag } = require('./utils/validators');
11
+ const { validateTaskId, validateTag, validateThinkingLevel } = require('./utils/validators');
12
12
  const { failJson, ERROR_CODES } = require('./utils/error-doc');
13
13
  const { GATEWAY_MODES } = require('./utils/model-descriptor');
14
14
  const { applyTemplateForArgs } = require('./cli-template-args');
@@ -54,6 +54,15 @@ async function handleFanout(args) {
54
54
  process.exit(failJson(useJson, { code: ERROR_CODES.BAD_ARGS, message: `Error: --gateway must be one of: ${GATEWAY_MODES.join(', ')}` }));
55
55
  }
56
56
 
57
+ // #218 PR 4 whole-branch review (VCMD-2): the same vocabulary check `start` runs in
58
+ // validateStartArgs (cli.js) — fanout never did, so a typo (or a pack's) reached sendPrompt:
59
+ // sent unverified to an unknown model, a per-leg VARIANT_UNDECLARED after the spawn on a
60
+ // known one. Named mutant "FANOUTTHINKINGUNCHECKED" (tests/fanout-cli.test.js): drop this block.
61
+ const thinkingCheck = validateThinkingLevel(args.thinking);
62
+ if (!thinkingCheck.valid) {
63
+ process.exit(failJson(useJson, { code: ERROR_CODES.BAD_ARGS, message: thinkingCheck.error }));
64
+ }
65
+
57
66
  const { resolvePromptSource } = require('./utils/prompt-source');
58
67
  let promptRes;
59
68
  if (args.prompt !== undefined || args['prompt-file'] !== undefined || args.template === undefined) {