amicus 4.4.0 → 4.5.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.
Files changed (109) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/CHANGELOG.md +162 -0
  3. package/README.md +17 -2
  4. package/bin/amicus.js +10 -0
  5. package/docs/DISTRIBUTION.md +234 -0
  6. package/docs/ROADMAP.md +226 -0
  7. package/docs/SHIMS.md +62 -0
  8. package/docs/architecture.md +104 -0
  9. package/docs/configuration.md +395 -0
  10. package/docs/council.md +970 -0
  11. package/docs/doc-system.md +92 -0
  12. package/docs/electron-testing.md +471 -0
  13. package/docs/jsdoc-setup.md +75 -0
  14. package/docs/opencode-integration.md +114 -0
  15. package/docs/publishing.md +60 -0
  16. package/docs/schemas.md +56 -0
  17. package/docs/testing.md +589 -0
  18. package/docs/troubleshooting.md +298 -0
  19. package/docs/usage.md +849 -0
  20. package/electron/fold.js +1 -1
  21. package/electron/main.js +4 -1
  22. package/electron/setup-ui-aliases.js +6 -6
  23. package/electron/workspace-ui/live-model.js +12 -1
  24. package/electron/workspace-ui/md-lite.js +52 -8
  25. package/electron/workspace-ui/workspace-app.js +39 -17
  26. package/electron/workspace-ui/workspace-matrix.js +46 -9
  27. package/electron/workspace-ui/workspace-panels.js +88 -19
  28. package/electron/workspace-ui/workspace-render.js +17 -1
  29. package/electron/workspace-ui/workspace-verbs.js +48 -2
  30. package/package.json +8 -3
  31. package/schemas/council-run-live.schema.json +1 -1
  32. package/schemas/council-run.schema.json +34 -0
  33. package/schemas/error.schema.json +1 -1
  34. package/schemas/event.schema.json +1 -1
  35. package/schemas/pack.schema.json +30 -0
  36. package/schemas/progress.schema.json +13 -1
  37. package/schemas/run-live.schema.json +1 -1
  38. package/schemas/run.schema.json +2 -1
  39. package/schemas/spend.schema.json +52 -4
  40. package/schemas/wave-live.schema.json +1 -1
  41. package/schemas/wave.schema.json +2 -1
  42. package/skills/second-opinion/SKILL.md +5 -0
  43. package/src/cli-handlers-council-run.js +51 -8
  44. package/src/cli-handlers-pack.js +238 -0
  45. package/src/cli-handlers-run.js +36 -8
  46. package/src/cli-handlers-spend.js +20 -2
  47. package/src/cli-handlers-template.js +53 -0
  48. package/src/cli-handlers-watch.js +11 -0
  49. package/src/cli.js +68 -5
  50. package/src/council/briefings-debate.js +27 -7
  51. package/src/council/briefings-stage2.js +155 -25
  52. package/src/council/briefings.js +24 -1
  53. package/src/council/findings.js +199 -9
  54. package/src/council/parse-stage2.js +10 -2
  55. package/src/council/presets-cli.js +23 -11
  56. package/src/council/report.js +19 -8
  57. package/src/council/run-assemble.js +42 -1
  58. package/src/council/run-budget.js +64 -11
  59. package/src/council/run-chair.js +4 -1
  60. package/src/council/run-debate.js +4 -2
  61. package/src/council/run-finalize.js +102 -0
  62. package/src/council/run-launch.js +29 -1
  63. package/src/council/run-server.js +248 -0
  64. package/src/council/run-stage2.js +118 -0
  65. package/src/council/run-stages.js +134 -110
  66. package/src/council/run-state.js +40 -1
  67. package/src/council/run.js +45 -47
  68. package/src/council/tally.js +10 -0
  69. package/src/headless.js +180 -7
  70. package/src/mcp-council-run.js +108 -4
  71. package/src/mcp-server.js +203 -7
  72. package/src/mcp-tools.js +15 -5
  73. package/src/observe/council-legs.js +60 -3
  74. package/src/observe/live-doc.js +18 -1
  75. package/src/observe/watch-render.js +4 -1
  76. package/src/pack/pack-cli.js +38 -0
  77. package/src/pack/pack-forward.js +96 -0
  78. package/src/pack/pack-resolve.js +297 -0
  79. package/src/pack/pack-store.js +130 -0
  80. package/src/pack/pack-validate.js +113 -0
  81. package/src/sidecar/child-sessions.js +1 -2
  82. package/src/sidecar/fanout-leg-fallback.js +69 -21
  83. package/src/sidecar/fanout-leg.js +6 -0
  84. package/src/sidecar/fanout-signals.js +61 -0
  85. package/src/sidecar/fanout-wave-io.js +75 -0
  86. package/src/sidecar/fanout.js +82 -74
  87. package/src/sidecar/progress-fields.js +26 -4
  88. package/src/sidecar/progress.js +42 -1
  89. package/src/sidecar/session-utils.js +23 -14
  90. package/src/sidecar/start.js +5 -4
  91. package/src/sidecar/workspace-auto-open.js +69 -0
  92. package/src/sidecar/workspace-window.js +46 -1
  93. package/src/spend-query.js +17 -5
  94. package/src/template/apply.js +88 -0
  95. package/src/template/render.js +86 -0
  96. package/src/template/store.js +106 -0
  97. package/src/utils/config.js +65 -25
  98. package/src/utils/error-doc.js +5 -0
  99. package/src/utils/lifecycle.js +37 -1
  100. package/src/utils/path-fence.js +39 -1
  101. package/src/utils/pricing.js +26 -10
  102. package/src/utils/result-schema-rebuild.js +1 -0
  103. package/src/utils/result-schema.js +8 -2
  104. package/src/utils/server-setup.js +79 -1
  105. package/src/utils/spend-ledger.js +24 -3
  106. package/src/workspace/artifact-guard.js +66 -7
  107. package/src/workspace/fold-format.js +33 -4
  108. package/src/workspace/live-normalize.js +28 -15
  109. package/src/workspace/run-detail.js +13 -1
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "amicus",
3
- "version": "4.4.0",
3
+ "version": "4.5.0",
4
4
  "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.",
5
5
  "author": {
6
6
  "name": "Christian Wagner"
package/CHANGELOG.md CHANGED
@@ -3,6 +3,168 @@
3
3
  All notable changes to Amicus are documented here. Format follows
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions follow semver.
5
5
 
6
+ ## [4.5.0] - 2026-07-28
7
+
8
+ "Save and share your councils" — complex run configurations become one command, repeatable and
9
+ shareable, and the flagship Council Workspace stops being opt-in on its best client.
10
+
11
+ ### Added
12
+
13
+ - **Policy packs — save a full run configuration and invoke it by name.** `amicus pack save <name>
14
+ --kind council|fanout|solo [flags]` (or `--from-run <id>`, which captures an existing council
15
+ run / fanout wave / solo session instead of typing flags) writes one JSON file per pack to
16
+ `~/.config/amicus/packs/<name>.json`; `pack list` / `pack show` / `pack rm` manage them. `--pack
17
+ <name|path>` on `amicus start` / `fanout` / `council run` — and the new `pack` param on the
18
+ `amicus_start` / `amicus_fanout` / `amicus_council_run` MCP tools — loads a pack's bench,
19
+ chair/critic/lenses, options, and briefing template as this run's defaults. **Explicit flags
20
+ always override the pack's values, and the pack is recorded either way** — `pack: {name,
21
+ version, hash, source}` lands on the resulting session `metadata.json`, wave `metadata.json` /
22
+ `wave.json`, or council `run.json`. Precedence throughout: **flag > pack > config default >
23
+ built-in default**. A pack is validated on save (hard-fail — `PACK_INVALID` — with any
24
+ non-fatal warnings printed to stderr) and again whenever it's used to launch a run; `pack show`
25
+ never fails on an invalid pack, only reports what's wrong with it; `pack show` and `pack rm` both
26
+ return `PACK_NOT_FOUND` for a missing pack. New `schemas/pack.schema.json`.
27
+ - **MCP pack semantics.** Over MCP, `pack` resolves entirely **in-process**, on the same call that
28
+ reads it — a resolved pack is never forwarded as `--pack` to a spawned child. Two knobs get
29
+ special-cased handling for CLI parity: a pack's `options.maxCost` and `briefing.template` have no
30
+ MCP schema param of their own on `amicus_start`/`amicus_fanout`, but they still apply — forwarded
31
+ to the spawned CLI child's argv as `--max-cost`/`--template` (`amicus_fanout` always spawns;
32
+ `amicus_start`'s spawn-fallback path does the same), or, on `amicus_start`'s in-process
33
+ shared-server path, applied via the same budget-gate/template-render code the CLI itself uses,
34
+ before any session is created — so a shared pack's spend cap and briefing template never silently
35
+ vanish over MCP. (`amicus_council_run` already has real MCP params for both.) Any *other* pack
36
+ knob with no destination in a tool's own MCP input schema is never silently dropped either: it
37
+ surfaces as an explicit `Notice: pack '<name>' sets <key>, which <tool> does not support over
38
+ MCP — ignored.` content block, naming the pack's own camelCase option key (e.g. `contextTurns`,
39
+ never the CLI's `context-turns`). Concretely, `amicus_fanout` has no MCP destination for
40
+ `options.contextTurns` / `options.contextMaxTokens` (both notice); `amicus_start` has real params
41
+ for both, so neither does.
42
+ - **Council packs do not accept `agent`/`thinking`/`summaryLength`.** They were inert on every
43
+ surface — no council code path, CLI or MCP, ever reads a pack-filled one; the engine hardcodes
44
+ agent `Plan`/summaryLength `verbose` regardless of what a pack says — so `KIND_OPTIONS.council`
45
+ never accepted them; a council pack that sets one fails `pack save` (`PACK_INVALID`), naming the
46
+ offending key. They remain valid, and functional, on `fanout`/`solo` packs.
47
+ - **Briefing templates.** `amicus template list|show`, plus `--template <name|path>` / `--artifact
48
+ <file>` / `--var <k=v>` (repeatable) on `start` / `fanout` / `council run`, render a
49
+ `{{variable}}` briefing before it's sent. Templates are Markdown files in
50
+ `~/.config/amicus/templates/` — a same-named user file shadows a built-in, the same precedent
51
+ saved councils already use — and v4.5 ships one built-in, `review`. Known variables: `{{prompt}}`,
52
+ `{{artifact}}`, `{{artifact_path}}`, `{{date}}`, `{{project}}`, `{{var.<key>}}`. Rendering is
53
+ strict by design: an unknown variable, a slot with no data behind it, or data passed with no slot
54
+ to receive it are all hard errors (`TEMPLATE_RENDER`) rather than a silently dropped value. MCP
55
+ has no `template` param of its own on any of the three run tools — a pack's `briefing.template` is
56
+ the only way a template reaches an MCP-invoked run.
57
+ - **The Council Workspace auto-opens on `amicus_council_run` from Claude Code (local).** When the
58
+ MCP tool `amicus_council_run` is invoked from Claude Code (local), the same Electron window that
59
+ `amicus watch <runId> --ui` has always opened by hand now launches automatically, detached, right
60
+ after the run starts — no more separate `--ui` call to see the flagship v4.4 surface. The CLI
61
+ `amicus council run` is unaffected (there is no MCP client to detect on that path). Decision
62
+ order: an explicit `ui: false` param beats everything; the hard guards (Electron not installed —
63
+ this path never installs it; Linux with no `DISPLAY`) beat even an explicit `ui: true`; `ui: true`
64
+ then overrides both the new `workspace.autoOpen` config key and the client check; short of an
65
+ explicit param, `workspace.autoOpen === false` disables it, and any client other than Claude Code
66
+ (local) simply doesn't auto-open. New `workspace.autoOpen` config key (`config.json`, default on
67
+ — only an explicit `false` turns it off). The tool response carries `workspaceOpened: boolean`
68
+ and, only when it did not open, `workspaceOpenReason` (`param-suppressed`, `electron-absent`,
69
+ `no-display`, `config-disabled`, `client-not-code-local`, or a `spawn-failed:` /
70
+ `auto-open-failed:` detail).
71
+
72
+ ### Fixed
73
+
74
+ - **A failed council seat no longer renders as perpetually live.** `createSession`'s early return
75
+ under a shared server bypassed the terminal `progress.json` write, leaving a stage marked
76
+ in-progress in `progress.json` after `metadata.json` had already recorded the error. The
77
+ terminal-write logic is now one shared helper (`writeTerminalProgressSafe`), called at all three
78
+ early-return sites plus the original one, so the paths can no longer drift apart.
79
+ - **Collided artifact names no longer misattribute one model's prose to another's.** Two bench
80
+ models whose sanitized filenames collide (e.g. `vendor/a` and `vendor?a` both → `vendor-a`) share
81
+ one physical file on disk; the Council Workspace compounded that with a rendering bug that showed
82
+ the first model's review/judge prose under the *second* model's panel — including, in a `--debate`
83
+ run, the rebuttal/re-vote drill-in. Collided names now get a deterministic suffix (`~2`, `~3`,
84
+ …), and every Workspace file lookup consults the resulting name map instead of recomputing a bare
85
+ sanitized name — the second colliding model's row is now correctly dropped by the existing
86
+ presence filter (its suffixed name was never physically written) instead of showing the wrong
87
+ model's text, and a run-integrity banner names the collision so the gap reads as a known
88
+ limitation rather than missing data.
89
+ - **A blind-mode toggle no longer collapses every open prose panel or repaints twice.** Flipping
90
+ Blind mid-run used to unconditionally recompute the blind default, forcing one paint with the
91
+ wrong value, a restore, and a second compensating repaint — and reset every lazy-loaded panel's
92
+ open/loaded state along the way, closing whatever the user had expanded. Blind state and
93
+ lazy-panel state now key off whether the run — and, separately, its status — actually changed
94
+ since the last render, so a same-run toggle updates in place and paints once without closing any
95
+ open panel; a run reaching its terminal status still recomputes the blind default and auto-reveals
96
+ exactly as before.
97
+ - **`renderSeats` now reorders rows to match the composed run document.** The keyed seat-table
98
+ update already added and removed rows on change but never moved one, so the table's row order
99
+ froze at first render — visibly wrong once a repair solo or a new wave changed the underlying leg
100
+ order mid-run. Existing rows are now moved into place at the end of every render pass.
101
+ - **`amicus council show` no longer reports a catalog-delisted bench member as healthy, and a
102
+ dropped member is no longer invisible to scripted/MCP callers.** `show`'s resolved/dropped split
103
+ checked only whether a member's alias mapped to *some* id, never whether that id was still in
104
+ the cached catalog — so a preset member whose alias now resolves to a catalog-absent id (e.g. a
105
+ direct-vendor route with no matching cached row) read as fully healthy in `show` while the real
106
+ run path (`resolveCouncilMembers`) silently dropped it on every actual run. `show` now reuses
107
+ that exact check — alias resolution, then catalog membership, with the same local-provider/
108
+ offline-catalog rule that a catalog it cannot consult never blocks a member, only a non-empty
109
+ catalog that omits it does. Separately, `council run --json` already suppressed the human-mode
110
+ `Notice: dropped unavailable council member(s): ...` line, and `run.json` carried no field for
111
+ it at all — a JSON-mode or MCP caller had zero signal a bench member vanished short of diffing
112
+ `bench` against the preset's nominal member list. `run.json` now carries an additive
113
+ `droppedMembers: [{member, reason}]` array (present only when at least one member was actually
114
+ dropped), reaching the `--json` envelope and the `amicus_council_run` MCP response body for
115
+ free. Resolution behavior itself — which members run, exit codes, spend — is unchanged; this is
116
+ observability only.
117
+
118
+ ### Changed
119
+
120
+ - **`amicus_start` / `amicus_fanout`'s MCP schemas no longer declare a JSON-Schema `default` for
121
+ `agent`, `noUi`, or `includeContext`.** Client-visible metadata only — nothing behavioral: the
122
+ defaults are still applied at the same read sites they always were, and are still stated in each
123
+ param's own description. (`amicus_resume` / `amicus_continue`'s `noUi` keep their schema-level
124
+ default; they were not part of this pass.)
125
+
126
+ ### Removed
127
+
128
+ - **The inert `repairCanHonorContract` guard.** 4.4.1's empty-findings acceptance flipped this
129
+ predicate permanently true by its own design, so `run-stages.js`'s `repairable &&` check could no
130
+ longer short-circuit on it and no test failed if the function were deleted outright — a
131
+ silent-deletion hazard that would otherwise re-arm the deadlock it used to guard against the day
132
+ empty-set validation tightens again. Removed deliberately instead: the underlying reasoning moved
133
+ to its call site, and the zero-findings regression test's comment now explains why the case it
134
+ covers still holds without the guard.
135
+
136
+ ## [4.4.1] - 2026-07-27
137
+
138
+ A fast-follow patch on 4.4.0. Every item is a correction to something already shipped, and almost all of it was measured against real paid council runs rather than reasoned about — the five gate councils that certified the Council Workspace are also what found these. Five behaviour changes ride along and are called out under **Changed**, because a user upgrading a patch should not discover them by surprise.
139
+
140
+ ### Fixed
141
+
142
+ - **The repair path is whole for the first time.** 4.4.0 gave the Stage-1 findings repair the review it was repairing; the judge, chair, defense and re-vote repair prompts carried the identical omission, and `buildChairRepairPrompt` took no arguments at all. A repair leg is a *fresh* session with no memory of the turn it is repairing, so shipping only the validation errors asked a model to correct something it had never seen. **Three of the five paid gate councils burned a seat on it:** `wsgate02`'s `qwen` and `wsgate04`'s `glm` each refused twice ("I don't have a previous review to correct"), so a 4-model bench silently adjudicated on 3 while still paying for the fourth; `costgate01`'s `grok` complied instead — by **inventing a self-referential finding about its own empty output**, which entered `tally.json`, the street-cred rankings, the chair synthesis, and a human's decision. Every repair call site now embeds the text that actually failed, verbatim and uncapped, tracked across attempts so the errors and the artifact always describe the same generation. Separately, a repair that **silently changes the finding count is now refused** rather than adjudicated: the repair contract is "the same findings, fixed", and a count change is exactly the fabrication shape above. Where the original block was absent or unparseable there is no count to compare, so the repair is accepted but marked `findingsUnverified` rather than implying a check happened. A repair's output never replaces the review's prose — that would hand the judges a narrative-free review and put a JSON dump in the Stage-2 bundle.
143
+ - **Three of four seats on a paid council were silently truncated by the fence extractor.** The closing-fence pattern was unanchored, so the **first triple-backtick anywhere inside a JSON body ended the match** — and a review *of markdown* inevitably quotes a fence. On the $1.95 renderer-review council, `glm`, `opus` and `minimax` all came back `NOT_PARSEABLE` and collapsed to `conformance: unstructured`; replayed against the same artifacts after the fix they yield 6, 5 and 4 findings respectively. **15 of 17 findings were lost or left to a paid repair wave to rescue, and the chair synthesised from the two that survived without knowing the rest existed.** The extractor now enumerates every fenced opener independently, reads each one both ways (close-at-line-start primary, same-line close as fallback), and lets **`JSON.parse` arbitrate** — the last opener whose body actually parses wins. This is the repo's only fence extractor and all five consumers funnel through it, so judge, debate-defense and re-vote parsing carried the identical defect and are fixed by the same change; each now has its own test so a future re-implementation cannot regress one silently. The malformed-versus-absent distinction is preserved deliberately: a cut-off emit that never closed at all is still *absent*, because the repair path answers "no findings block" and "a broken findings block" differently.
144
+ - **A council run no longer races itself for OpenCode's database.** A run started a fresh OpenCode server for the Stage-1 seat wave, the critic solo, each findings repair, the Stage-2 judge wave, each judge repair, each debate wave and the chair chain — 10+ spawns, each one a fresh chance to lose OpenCode's SQLite startup race. Stage 1 launches its seat wave and its critic solo under one `Promise.all`, so two of those starts are ~140 ms apart *by construction*: one run **lost four of five seats in 736 ms** to `database is locked` and failed quorum, which is what made `--critic` a coin flip. A run now acquires **one** server and forwards it into every launch, closed once on the single path every terminal outcome already funnels through. It never fails closed — a shared server that will not start is a notice, and the run falls back to one server per wave exactly as before. The Stage-2 anonymization boundary was verified rather than assumed: judges run in the run's `_scratch` directory and scoping is per-call, so nothing about it ever lived in the server process. Separately, a **lock-class** start failure (`database is locked`, `database table is locked`, `SQLITE_BUSY` — and nothing else) is now retried 3 times over ≤750 ms, which covers the races a single process cannot remove: two amicus processes, or a CLI run beside a live MCP server. A missing binary, an auth failure, a port conflict and a failed health check are deterministic and fall straight through rather than tripling the latency before the same error.
145
+ - **`amicus spend` stopped reporting a total that `council run` calls inexact.** A leg whose spawned subagent could not be accounted for wrote a **priced** ledger row, so `unpricedRows` never caught it and the product's two truthfulness surfaces disagreed about the same dollars. Such a row now carries the flag and is counted as `unattributedSubtreeRows` **beside — never instead of —** `unpricedRows`, since a row can be both; the human table gains a second, distinctly worded line, because "we could not see this leg at all" and "we saw this leg but not what it spawned" are different facts. The MCP `amicus_spend` tool inherits it unchanged. And the unknown-spend notice is no longer **sticky**: it guarded on a boolean, so the first unknown leg was announced and every one created afterwards — Stage 2, repairs, debate, chair — was silently swallowed. It now re-announces on a growing count, and says "so far this run" rather than repeating a cumulative number as if it were new.
146
+ - **A leg observed only through cache tokens reports `unknown`, not a falsely free `$0`.** The observation gate accepted `cacheRead`/`cacheWrite`/`reasoning`, but the estimate prices `input`/`output` only — so a leg observed in neither passed the gate and resolved to `estimated $0.0000`, the same authoritative false zero 4.4.0 exists to eliminate, in the one corner its predicate did not cover. The v4.2 free-local `$0` tier is untouched: a real local seat reports genuine token counts and still resolves to `estimated ~$0.0000`.
147
+ - **A leg that both fell back to another model and left an unattributable subtree no longer reports `costExact: true`.** Folding a leg's attempts together returned a bare `{tokens, cost}` and dropped the subtree flag, so `run.json` claimed a complete total for a number that was a floor. The fold now preserves every key: the flag ORs across attempts (a gap admitted once cannot be erased by a later clean attempt) and a measured subtree sums rather than last-wins. Separately, the spend reader promised to skip corrupt ledger lines but let a valid-JSON *scalar* through as a row, inflating both `runs` and `unpricedRows`.
148
+ - **A failed leg no longer renders as complete in the Council Workspace.** A seat that errored or timed out could show a green check, because `timed-out` was missing from the terminal-state lists the mirror consults. Alongside it: the leg-role guard is now symmetric, a leg that throws writes a terminal progress record instead of leaving its last live one as the final word, and swallowed read failures are logged rather than discarded.
149
+ - **A permission failure is no longer reported as "not written yet".** An unreadable run artifact was indistinguishable from one the run had not produced yet, which had been producing a **silent chairless fold reporting `{ok: true}`** — the fold said it succeeded and carried no verdict. Three dead fallbacks and an unwired `legsTotal`/`legsComplete` pair were deleted in the same pass: a documented field that is never populated is worse than no field.
150
+ - **A Stage-1 wave that dies before its legs start no longer leaves the run looking healthy.** Such a wave wrote no `wave.json` at all, so nothing downstream could tell "the wave failed" from "the wave has not reported yet"; it now degrades the run loudly. A shared-server acquisition that fails likewise degrades loudly instead of silently, and its **success** is recorded too — previously only the failure was.
151
+ - **The `haiku` alias was never broken.** It hard-404'd 3 of 3 times across two paid councils — as chair (twice, including the fallback retry) and as a bench seat, ~2 s and zero tokens each — and the standing diagnosis was a rotten model id. It is not: the cause is an `ANTHROPIC_BASE_URL` without its `/v1` suffix, an **environment** misconfiguration. Documented as such, because the prescribed "fix or remove the alias" would have deleted a working route. The more useful half of the lesson stands unchanged from 4.4.0: a dead alias does not stop a council, it shrinks one.
152
+ - **Renderer and markdown robustness.** The workspace's markdown renderer no longer re-slices its input on every inline token (identical output on 219,543 verified inputs — exhaustive to length 6 plus 200,000 fuzz cases — removing a dependence on a V8 string-representation detail nothing stated or tested); heading text is trimmed rather than baking trailing blanks into a text node; and an unreachable heading-level clamp is gone, pinned so a future widening fails loudly instead of silently emitting an `<h7>`.
153
+
154
+ ### Changed
155
+
156
+ - **A review that honestly finds nothing is now a valid review.** An empty finding set was a hard error, which structurally pressured a model to invent a finding — directly contradicting the anti-sycophancy clause shipped in every Stage-1 briefing, and `costgate01`'s `grok` did exactly that, reaching a human's decision. A **present-but-empty** `findings` array with a non-empty `overall` now validates. The lines that already existed are preserved: a broken emit keeps its own codes, an empty set with a blank or missing `overall` is still an error, and a *missing* `findings` key is still an error — only an array that is present and empty means "I read it and found nothing". The briefing now says what the validator enforces, which also makes the repair prompt's "emit an empty findings array and say so" branch describe an answer that can actually pass rather than a trap costing two paid legs. Downstream, an all-clean bench degrades gracefully: Stage 2 still runs (peer ranking, and therefore street-cred, is unaffected by an empty findings pool), and the judge bundle and chair packet **state** the empty index instead of rendering a heading over nothing under an order to adjudicate ids that do not exist — an instruction a judge obeying it answers by inventing an id, which buys up to two paid repair solos per judge. The debate stage is genuinely skipped and already records its reason. **This changes what a council means when every seat comes back clean.**
157
+ - **A leg that exceeds the tool-settle grace ceiling now has its OpenCode session aborted.** 4.4.0 bounded the wait and completed the leg anyway; it left the underlying session running and **billing** for output nobody would read. The leg's completion and partial output are unchanged — only the session is stopped, after the child-session walk so subtree attribution survives, and before the server closes, since on a shared server (i.e. every council run) the server is not closed here at all — which is precisely the pathological case. Whether the abort landed rides the result, the terminal `progress.json` and the leg's `metadata.json` as `toolSettleAborted`, and it is recorded as `false` when the abort was attempted and failed rather than omitted: "we tried to stop it and could not; it may still be billing" is the useful half of that signal. A failed or hung abort can never alter a leg that already succeeded.
158
+ - **A run under a `--max-cost` ceiling now exits `2` when its own total is inexact.** A fully-unpriced council could never trip the ceiling, so a ceiling silently bounded nothing while the run exited `0` — an unqualified success for a number the run itself was reporting as a floor. When a ceiling is set *and* the total is inexact, the run now exits `2`, through the same degraded path a budget-refused wave already uses; a signal and a real error are never re-labelled. **The ceiling still never blocks a run** and still trips on **known** spend only — a fully-unpriced bench under a $0.01 ceiling runs every stage to completion. The docs now say "`--max-cost` bounds **known** spend", which is what it has always done. Anything gating on `council run`'s exit status should expect `2` where it previously saw `0` on an inexact run.
159
+ - **`amicus watch <id> --ui` now validates the run id.** A malformed id skipped validation entirely and surfaced whatever the run lookup produced — a vaguer error than the identical typo gets on the terminal path. It now **exits `1` with the validator's message instead of launching the workspace**. Bare `amicus watch --ui` still opens the project run-list landing; the check applies only when an id is actually supplied.
160
+ - **`npm i -g amicus` now installs the documentation.** The package's `files[]` excluded `docs/` entirely, so every word of documentation was unreachable from an install — and the moment a user most needs troubleshooting text already on disk is an opaque `Not Found` with zero tokens, which is exactly when they have no reason to trust a browser tab instead. The 15 top-level `docs/*.md` pages now ship (**+15 files, 285 → 300; +93 KB packed, +8.8%**). Images and the plan/spec archive deliberately do not — ~425 KB with no offline value. A handful of roadmap references point at repo working files that still do not ship; they are now labelled as such rather than reading as broken paths.
161
+ - **The fold's `Cost:` line stops saying the same thing twice.** It appended the source name on top of a glyph that already encoded the same fact, printing `~$0.0100 (estimated)` and `? (unknown)`. Those two words are gone — `~` already means inexact and a bare `?` already means unknown. Two sources keep their word because the glyph vocabulary cannot express them: **`reported`**, since a plain `$0.4321` is also what an unrecognised source renders as, so the absence of a glyph cannot mean "exact"; and **`mixed`**, since `~` says *inexact* without saying *which kind* — collapsing `mixed` to a bare `~$…` makes it indistinguishable from `estimated`, and the two are not the same claim. `mixed` asserts that part of the number is genuinely measured.
162
+
163
+ ### Security
164
+
165
+ - **The Council Workspace's markdown renderer has finally been reviewed by a bench.** The component that turns **another model's prose into DOM** was in none of the five gate councils' review sets and shipped in 4.4.0 uncertified; one chair called that out explicitly and rated it blocking, while another cleared the same file by reading its *consumers* rather than the file. A paid council was run against the file itself. **It found nothing exploitable** — no DOM injection (`textContent`/`createTextNode` only, fixed tag names, no HTML parsing of model-controlled content) and no prototype pollution — and the findings it did raise are architectural, fixed above under **Fixed**. The certification is recorded here rather than in a working note because the disagreement it settles was a public one. Its adversarial coverage is now pinned: prototype-pollution inertness, `javascript:`/`data:` URL inertness, malformed and unmatched inline backticks, the resource budget at exactly the artifact-size cap under both pathological shapes, and a widened attribute-sink guard — the previous banned-token scan covered four HTML-string APIs and would **not** have caught `setAttribute('href', userText)`, with a negative control proving the guard is not vacuous. Two rendering-fidelity findings were surfaced and deliberately not fixed; neither is a security property.
166
+ - **The Council Workspace's read-only posture is now enforced rather than asserted.** That the workspace never writes into a run directory was checked nowhere. It now is, by a guard that **parses** rather than greps: these files are dense with prose *about* writes, and a text scan that goes red for a comment gets weakened rather than fixed, so the guard builds an AST (comments are structurally absent from it) and matches string literals by exact equality. It pins that no write API appears in the workspace source, that the registered IPC channel set is **exactly** the seven known channels — so a new channel cannot be added without being classified — and that the one verb which legitimately writes, Abort, **delegates** to the engine's own abort path rather than writing itself. A positive control scans the engine's own writer and requires it to come back dirty, so a broken scan cannot pass by finding nothing. The Workspace's CSP likewise gains a real regression guard: "no violations" is also what a *loosened* policy produces, so the check now appends an inline `<script>` and requires the refusal to appear.
167
+
6
168
  ## [4.4.0] - 2026-07-26
7
169
 
8
170
  ### Added
package/README.md CHANGED
@@ -52,7 +52,7 @@ One install delivers six things that work together:
52
52
  - **The `amicus` CLI (with an `am` alias) and an MCP server.** The engine underneath both skills: launches sessions, shares context, runs parallel waves, and exposes the same surface to Claude as MCP tools.
53
53
  - **A self-updating model catalog.** Aliases and validation resolve against a live catalog fetched from provider APIs (cached locally), so model names stay current without a hard-coded table.
54
54
  - **Observability.** `amicus watch <id>` renders any live or finished run (fan-out or council) from any terminal; `--follow` streams milestones as they happen; `--on-complete` fires a hook when a run lands; `--retry-failed` plus opt-in cheaper-model fallbacks recover dead legs without relaunching the whole wave; `amicus spend` answers "what did this cost, and where" with per-run attribution.
55
- - **Council Workspace.** `amicus watch <runId> --ui`: a window that shows a council *thinking* — live seats, the anonymized judge packet, the adjudication matrix, dissent drill-in, chair verdict, and cost-by-seat — for both live and historical runs.
55
+ - **Council Workspace.** `amicus watch <runId> --ui`: a window that shows a council *thinking* — live seats, the anonymized judge packet, the adjudication matrix, dissent drill-in, chair verdict, and cost-by-seat — for both live and historical runs. It also **auto-opens** on an MCP-invoked council run from Claude Code (local), so you no longer have to remember the flag (see [The Council](#the-council)).
56
56
 
57
57
  Claude is the orchestrator. The council and chat skills run *on top of* the engine; you talk to Claude, and Claude drives Amicus.
58
58
 
@@ -267,6 +267,17 @@ The `amicus_fanout` MCP tool takes the same `council` parameter, and the `second
267
267
 
268
268
  **Council presets.** Save your own named member lists with `amicus council save <name> --models a,b,c` (≥2 resolvable aliases or `provider/model` IDs), then run them with `--council <name>` anywhere a council runs. `amicus council list` shows saved presets plus three built-in benches that work with no setup at all — `free` (the same zero-cost dynamic pick described above, used when you haven't seeded `councils.free`), `budget` (cheap workhorses, one per vendor family), and `frontier` (premium flagships, one per vendor family). `amicus council show <name>` resolves any of them (saved or built-in) and reports which members are currently usable. A saved council always shadows a built-in of the same name — exactly how the wizard's `councils.free` seeding already worked.
269
269
 
270
+ **Policy packs (v4.5).** A council preset only saves the bench. A **pack** saves the whole run — bench, chair, critic/lenses, cost/timeout options, and a briefing template — as one named, shareable JSON file:
271
+
272
+ ```bash
273
+ amicus pack save review-bench --kind council --bench gemini,deepseek,gpt --chair opus --timeout 20 --max-cost 2
274
+ amicus council run --pack review-bench --prompt-file plan.md --json
275
+ ```
276
+
277
+ Any flag you also type on that second line overrides just that value — a pack only fills in what you didn't say explicitly, and it's recorded on the run either way. Packs work the same way on `fanout`/`start` and on the `amicus_fanout`/`amicus_start`/`amicus_council_run` MCP tools. `amicus pack list`/`show`/`rm` manage them, and `--from-run <id>` builds one from a run you already liked instead of typing flags at all. Full reference: [docs/usage.md § Policy packs](./docs/usage.md#policy-packs).
278
+
279
+ **Briefing templates (v4.5).** `--template <name> --artifact <file>` (plus repeatable `--var k=v`) renders a `{{prompt}}`/`{{artifact}}`-style Markdown template before it's sent, on `start`/`fanout`/`council run` alike — templates live in `~/.config/amicus/templates/`, and a pack's `briefing.template` is how one reaches an MCP-invoked run (MCP has no template param of its own). `amicus template list|show` manage them; v4.5 ships one built-in, `review`. Full reference: [docs/usage.md § Briefing templates](./docs/usage.md#briefing-templates).
280
+
270
281
  ---
271
282
 
272
283
  ## The parallel window
@@ -323,6 +334,8 @@ amicus update
323
334
  | `amicus provider` | Add/list/test/remove local, OpenAI-compatible providers (LM Studio, Ollama, vLLM) — configured with `--preset` or `--url`, at **$0** marginal cost (`--json` on every subcommand). |
324
335
  | `amicus council` | Council math: `tally <input.json>` (deterministic tiers + ledger append), `stats` (reviewer reliability), `report <verdict.json> [--md\|--html]`, `validate <file>` (findings-block check, exit 0/2/1), `verdict <tally.json> [--decisions <d.json>] [-o <out.json>]` (build + write verdict.json). Presets: `save <name> --models a,b,c`, `list [--json]`, `show <name> [--json]` — see [The Council](#the-council) for the built-in `free`/`budget`/`frontier` benches. |
325
336
  | `amicus council run` | The headless council engine: Stage-1 reviews → anonymized cross-review → deterministic tally → non-Claude chair verdict, in one command with no Claude runtime. Add `--debate` for a Stage-2.5 rebuttal round (raisers defend/amend/withdraw, disputing judges re-vote) and `--claude-review <file>` to enter Claude's own review as judged review N+1. Writes a run directory with `verdict.json` (including `overallVerdict`) and `report.html` — see [docs/council.md](./docs/council.md#amicus-council-run). |
337
+ | `amicus pack` | Save a full run configuration — bench, chair/critic/lenses, options, briefing template — and invoke it by name: `save <name> --kind council\|fanout\|solo [flags]` (or `--from-run <id>`), `list`, `show <name>`, `rm <name>`. `--pack <name>` on `start`/`fanout`/`council run` loads one; explicit flags always override it. See [docs/usage.md § Policy packs](./docs/usage.md#policy-packs). |
338
+ | `amicus template` | `list`/`show <name>` a briefing template. `--template <name> [--artifact <file>] [--var k=v]` on `start`/`fanout`/`council run` renders one before the briefing is sent. See [docs/usage.md § Briefing templates](./docs/usage.md#briefing-templates). |
326
339
  | `amicus abort` | Abort a running session (or `--all`). |
327
340
  | `amicus setup` | Configure default model, API keys, and aliases. |
328
341
  | `amicus update` | Update to the latest version. |
@@ -351,7 +364,7 @@ $ amicus status demo123 --json
351
364
  "taskId": "demo123",
352
365
  "status": "complete",
353
366
  "elapsed": "5m 0s",
354
- "version": "4.4.0",
367
+ "version": "4.5.0",
355
368
  "model": "google/gemini-2.5-flash",
356
369
  "phase": "terminal"
357
370
  }
@@ -462,6 +475,8 @@ Run `amicus doctor` first — it checks keys, catalog, OpenCode binary, Electron
462
475
  | Install fails partway, or `amicus doctor` reports the OpenCode binary "not found" | A **transient** error during the OpenCode engine's own postinstall (a spawn `ENOENT`, or an antivirus file-lock while it lays down its 11 per-platform binaries) can roll back the whole atomic install — retrying usually succeeds | Just re-run `npm install -g amicus`. If it still fails, clear the cache first: `npm cache clean --force && npm install -g amicus`. |
463
476
  | `401` / auth error | No usable key for the model's vendor — bare `provider/model` ids fall back to `OPENROUTER_API_KEY` automatically, so this means neither the direct key nor an OpenRouter key is configured (or `--gateway direct`/`openrouter` forced a gateway whose key is missing) | Run `amicus setup`, or `amicus key <provider> <key>` to add the missing key; see [Routing](#routing). |
464
477
  | `402` / "Payment Required" on first council review / `start` / `fanout` call | Your OpenRouter key is real but has no credit. Key save (`amicus key openrouter <key>` or the setup wizard's key step) only checks that the key **authenticates** — it doesn't check balance, so a zero-credit key saves cleanly and only fails later, on the first real model call. (The `amicus council` subcommand itself is deterministic math and never calls a model.) | Add credit at [openrouter.ai/credits](https://openrouter.ai/credits), **or** switch to a zero-cost council: `amicus setup` → option 2 (Free OpenRouter council) builds one from live `:free`-suffixed models and saves it as `councils.free` — then run `amicus fanout --council free …`. See "Free council (zero-cost)" under [The Council](#the-council) above. |
478
+ | Every direct `anthropic/…` model (`haiku`, `sonnet`, `opus`, `claude`) errors `Not Found` in ~2 s at zero tokens, but the same model works via `openrouter/anthropic/…` | An inherited `ANTHROPIC_BASE_URL` missing its `/v1` path segment. The engine appends only `/messages`, so requests hit `https://api.anthropic.com/messages` → HTTP 404 with an empty body → the bare status text. A shell spawned by Claude Code sets the `/v1`-less form for you. The model id, alias, and key are all fine. | `export ANTHROPIC_BASE_URL=https://api.anthropic.com/v1`, or unset it entirely, or pass `--gateway openrouter`. In a council a dead seat **degrades the run instead of failing it** — smoke-test each seat with one throwaway `amicus start` before paying for a council. See [docs/troubleshooting.md](./docs/troubleshooting.md#every-direct-anthropic-model-fails-with-not-found). |
479
+ | `Model 'X' is unverified against the direct catalog; attempting anyway` for a model that plainly exists | Not a claim the model is wrong — amicus **couldn't check**. That vendor's direct catalog fetch failed (usually a stale or truncated stored key), leaving its namespace empty, and an empty namespace never blocks a launch. The engine may still run the model from its own credential store, so a working model warns forever. | `amicus models --refresh` and watch for a provider that stays empty; re-save the good key with `amicus key <provider> <apikey>`. See [docs/troubleshooting.md](./docs/troubleshooting.md#model-x-is-unverified-against-the-direct-catalog-attempting-anyway). |
465
480
  | Session not found | No session matches the given ID | Run `amicus list`, or omit `--session-id` to use the most recent. |
466
481
  | No conversation history found | Project-path encoding | Check `~/.claude/projects/`; `/` and `_` in the project path are encoded as `-` in the directory name. |
467
482
  | Headless run never finishes | Task is bigger than the default timeout | Raise it: `--timeout 30`. |
package/bin/amicus.js CHANGED
@@ -152,6 +152,16 @@ async function main() {
152
152
  case 'update':
153
153
  await handleUpdate();
154
154
  break;
155
+ case 'template': {
156
+ const { handleTemplate } = require('../src/cli-handlers-template');
157
+ exitCode = await handleTemplate(args);
158
+ break;
159
+ }
160
+ case 'pack': {
161
+ const { handlePack } = require('../src/cli-handlers-pack');
162
+ exitCode = await handlePack(args);
163
+ break;
164
+ }
155
165
  default: {
156
166
  console.error(`Unknown command: ${command}`);
157
167
  // suggestCommand honors a cap-3 contract (up to 3 candidates, closest
@@ -0,0 +1,234 @@
1
+ # Distribution channels
2
+
3
+ Amicus ships through three channels. This doc is the runbook for each — what's
4
+ live today, what the submission/publish steps are, and what to check before
5
+ pulling the trigger on a release-facing action.
6
+
7
+ ## 1. npm (existing)
8
+
9
+ **Status: live.** Tag `v*` on `main` → `.github/workflows/publish.yml` → npm
10
+ **Trusted Publishing** (GitHub OIDC, no long-lived npm token in CI) →
11
+ `npm publish --access public --provenance`. The workflow also cuts a GitHub
12
+ Release and asks Claude to draft categorized release notes from the commit
13
+ log + diff since the previous tag.
14
+
15
+ Postinstall (`scripts/postinstall.js`) registers the MCP server in Claude
16
+ Code / Claude Desktop / Cowork and copies both skills (`sidecar`,
17
+ `second-opinion`) into `~/.claude/skills/`. It does **not** copy
18
+ `commands/council.md` — that only ships via the plugin channel below. This
19
+ is a known, accepted gap for npm/install-script users (slash commands are
20
+ plugin-channel-only by design; see the npm-vs-plugin note at the top of the
21
+ CHANGELOG's Unreleased section).
22
+
23
+ ## 2. Claude Code community marketplace (claude-community)
24
+
25
+ **Status: submitted 2026-07-01 — awaiting Anthropic review.** (Update this
26
+ line as the process advances: `submitted <date> / approved <date> / listed <date>`.
27
+ Note: the submission predates the Phase-9 polish on `main` — reviewers pulling
28
+ the repo see the current surface, including `commands/` and a clean
29
+ `claude plugin validate . --strict`.)
30
+
31
+ Two Anthropic-run marketplaces exist:
32
+ - **`claude-plugins-official`** — curated by Anthropic, no application process.
33
+ - **`claude-community`** — third-party plugins, submitted for review. This is
34
+ the one amicus targets.
35
+
36
+ Approved plugins are pinned to a commit SHA in
37
+ `anthropics/claude-plugins-community/.claude-plugin/marketplace.json`. CI in
38
+ that repo auto-bumps the pin as we push to `main`, and the public catalog
39
+ syncs from it nightly (a delay after any push is normal, not a bug). **PRs
40
+ opened directly against `anthropics/claude-plugins-community` are closed
41
+ automatically** — the only way in is the submission form below.
42
+
43
+ Because the pin auto-bumps on every push to `main`, every push after listing
44
+ reaches marketplace users on their next sync. `plugin.json` already pins an
45
+ explicit `version` synced to `package.json` (enforced by
46
+ `tests/plugin-manifest.test.js`), so this is the existing safe behavior:
47
+ users only see a version bump when we bump it, not on every commit. Treat
48
+ `main` as release-quality once listed.
49
+
50
+ ### Preflight (run before every submission or major post-listing update)
51
+
52
+ ```bash
53
+ claude plugin validate . --strict
54
+ claude --plugin-dir . # smoke: /amicus:council, /amicus:sidecar, /amicus:second-opinion, MCP tools
55
+ npm test
56
+ ```
57
+
58
+ - `claude plugin validate . --strict` is the same structural check the
59
+ review pipeline runs; `--strict` promotes unrecognized-field warnings to
60
+ errors so nothing slips through that CI would later flag. It needs the
61
+ Claude Code CLI installed locally — it is **not** wired into this repo's
62
+ CI (runners have no `claude` auth), so `tests/plugin-manifest.test.js` is
63
+ the CI-side proxy for manifest completeness.
64
+ - `claude --plugin-dir .` loads the plugin from the working tree so you can
65
+ manually confirm `/amicus:council`, `/amicus:sidecar`, and
66
+ `/amicus:second-opinion` all appear in the command picker exactly once,
67
+ and that the `amicus` MCP server connects (tools list populates).
68
+ - `npm test` must be green, specifically `tests/plugin-manifest.test.js`
69
+ and `tests/plugin-commands.test.js`.
70
+
71
+ **Known current-tree preflight result (checked 2026-07-02, `p9/distribution`
72
+ at a1bea3c):** `claude plugin validate . --strict` passes clean, exit 0.
73
+ (History: `--strict` previously flagged an unknown `plugin.json → bugs`
74
+ field; that field was removed in commit `4207485`, so the warning is gone.)
75
+
76
+ ### Submit
77
+
78
+ - **Individual-author route:** https://platform.claude.com/plugins/submit
79
+ (Console form). This is the route for us — Christian has no Team/Enterprise
80
+ org, so the directory-management admin route below doesn't apply.
81
+ - **Team/Enterprise route:** https://claude.ai/admin-settings/directory/submissions/plugins/new
82
+ (requires Team/Enterprise org + directory management permissions — not
83
+ applicable here, listed for completeness).
84
+ - **Metadata to enter in the form:**
85
+ - Repository: `https://github.com/BourbonDog/amicus` (public, MIT)
86
+ - Plugin name: `amicus`
87
+ - Description: pulled from `.claude-plugin/plugin.json` → `description`
88
+ - Contact: `sendtowags@outlook.com`
89
+ - **Timing:** submit only after Task 9a (`commands/council.md` + the sidecar
90
+ argument surface) has merged to `main`, so the SHA the reviewer evaluates
91
+ and the SHA that eventually gets pinned both include the slash commands.
92
+ Submitting before 9a merges means the reviewed surface is incomplete.
93
+ - Never open a PR against `anthropics/claude-plugins-community` — it's a
94
+ read-only mirror for the public catalog; PRs there are auto-closed.
95
+
96
+ ### What review checks (and what it might ask about)
97
+
98
+ The pipeline runs `claude plugin validate` plus automated safety screening.
99
+ There's no published SLA for turnaround — **budget this as unscheduled** and
100
+ don't put any downstream work on the critical path of approval.
101
+
102
+ Anticipated reviewer question: the npm package runs a postinstall
103
+ (`scripts/postinstall.js`) and the repo ships `install.sh` / `install.ps1`.
104
+ The **plugin channel itself never triggers postinstall** —
105
+ `.claude-plugin/plugin.json`'s `mcpServers.amicus.env` sets
106
+ `AMICUS_SKIP_POSTINSTALL=1`, so a plugin install only ever runs
107
+ `npx -y amicus@latest mcp` with that guard set, not the interactive
108
+ setup/registration flow. This paragraph is the answer if a reviewer asks.
109
+
110
+ ### After approval
111
+
112
+ 1. Search `"amicus"` in
113
+ https://github.com/anthropics/claude-plugins-community/blob/main/.claude-plugin/marketplace.json
114
+ to confirm the listing synced (nightly delay after approval is normal).
115
+ 2. End-to-end verify:
116
+ ```bash
117
+ claude plugin marketplace add anthropics/claude-plugins-community
118
+ claude plugin install amicus@claude-community
119
+ ```
120
+ 3. Update the Status line at the top of this section with the
121
+ submitted/approved/listed dates.
122
+ 4. Optionally add an "Install as a Claude Code plugin" section to
123
+ `README.md` referencing `@claude-community` (not done as part of this
124
+ task — premature before approval, since the listing doesn't exist yet).
125
+
126
+ *(Everything in this subsection past "search 'amicus' in
127
+ `marketplace.json`" is unverified as of 2026-07-02 in the sense that we
128
+ haven't been through it yet — it's the documented expected flow per
129
+ Anthropic's published docs, not something we've personally exercised
130
+ end-to-end against the real submission.)*
131
+
132
+ ## 3. MCP Registry
133
+
134
+ **Status: wired, not yet published (Phase 9c).** `server.json` (repo root)
135
+ and the `mcpName` field in `package.json` now exist, and
136
+ `.github/workflows/publish.yml` publishes to the MCP Registry
137
+ (`registry.modelcontextprotocol.io`) as the last three steps before the GitHub
138
+ Release, on every `v*` tag push. This has not fired yet — the first tag
139
+ push after this merge is the first real publish attempt.
140
+
141
+ **Namespace:** `io.github.BourbonDog/amicus` (case-sensitive — the registry
142
+ grants `io.github.<Login>/*` using the exact-case GitHub login/repository
143
+ owner). Confirmed unclaimed via
144
+ `https://registry.modelcontextprotocol.io/v0/servers?search=amicus`
145
+ (0 results, checked 2026-07-02).
146
+
147
+ **Flow:** tag↔`package.json` version lockstep is verified first (fails fast
148
+ with `::error::` on a mis-tag) → npm publish (existing, OIDC), itself guarded
149
+ by a version-exists check so a re-run does not re-attempt a version already
150
+ live on npm → `mcp-publisher` binary installed → `server.json` version
151
+ synced from the tag via `jq` (belt-and-braces; the in-repo
152
+ `server.json`/`package.json`/`packages[0]` versions are also kept in
153
+ lockstep by hand at release time and enforced by
154
+ `tests/scripts/package-manifest.test.js`) → MCP Registry publish, itself
155
+ pre-checked against the registry API so a re-run does not double-publish →
156
+ `mcp-publisher login github-oidc` (no secret needed, uses the same
157
+ `id-token: write` OIDC permission as the npm Trusted Publishing step),
158
+ retried up to 5 times on transient OIDC token-exchange failures → `mcp-publisher
159
+ publish`, retried up to 5 times (npm propagation lag) before hard-failing the
160
+ job → GitHub Release creation, guarded by an existence check so a re-run
161
+ does not fail on a release that already exists. The registry steps run
162
+ strictly after `npm publish` because npm-side ownership validation reads
163
+ `mcpName` from the *published* `package.json`.
164
+
165
+ **Release-order dependency (historical — satisfied before the first registry
166
+ publish):** the first post-merge `v*` tag had to wait for the Phase 4
167
+ tool-surface de-bloat, because the server still registered 13 deprecated
168
+ `sidecar_*` aliases alongside the real tools and the first registry publish
169
+ snapshots whatever surface exists at that time. Both have long since landed.
170
+ The rule that outlives the episode: **a registry publish snapshots the tool
171
+ surface, so land any tool-surface change before you cut the tag.** For the
172
+ current tool count, see the MCP table in [README.md](../README.md) — it is the
173
+ single source of truth and moves with each release (v4.0 added
174
+ `amicus_council_run`).
175
+
176
+ **Registry preview caveat:** the MCP Registry is still in preview per its
177
+ own docs (breaking changes/data resets possible before general
178
+ availability). The publish steps are additive to the existing npm/GitHub
179
+ Release flow and do not touch it; a registry publish failure after 5 retries
180
+ does fail the workflow job (hard `exit 1`), which means the 'Create GitHub
181
+ Release' step does not run on that path. If that trade-off proves unwanted
182
+ in practice, add `continue-on-error: true` to the 'Publish to MCP Registry'
183
+ step.
184
+
185
+ **First-publish de-risk:** before relying on CI for the first real publish,
186
+ run once locally: download `mcp-publisher` (Windows: the tarball flow from
187
+ the quickstart docs), `mcp-publisher login github` (device-flow auth as
188
+ BourbonDog), then `mcp-publisher publish` — to fail fast on any
189
+ namespace/validation error outside of CI. If publish returns "You do not
190
+ have permission…", the error message states the granted pattern; align
191
+ `server.json`'s `name` casing to it exactly.
192
+
193
+ **If the registry publish fails in CI (Phase 11 hardening):** re-running the
194
+ workflow is now the primary recovery path. Every publish-ish step in
195
+ `publish.yml` is idempotency-guarded, so a re-run skips whatever already
196
+ succeeded and only retries the step that actually failed:
197
+ - **npm publish** checks `npm view amicus@<version>` first and skips with a
198
+ `::notice::` if that version is already on the registry (instead of
199
+ hitting `EPUBLISHCONFLICT`).
200
+ - **MCP Registry publish** pre-checks
201
+ `registry.modelcontextprotocol.io/v0/servers/io.github.BourbonDog%2Famicus/versions/<version>`
202
+ (HTTP 200 = already published, 404 = not yet) and skips with a
203
+ `::notice::` if present, before attempting login or publish.
204
+ - **`mcp-publisher login github-oidc`** now retries up to 5 times (20s
205
+ apart) on transient OIDC token-exchange failures, same pattern as the
206
+ publish retry.
207
+ - **GitHub Release creation** checks `gh release view <tag>` first and skips
208
+ with a `::notice::` if the release already exists.
209
+
210
+ So: fix whatever caused the failure (registry outage, OIDC hiccup, etc.),
211
+ then re-run the failed job from the Actions tab (or `gh run rerun
212
+ --failed`). Do not delete and re-push the tag — the existing job re-run is
213
+ sufficient, and steps that already succeeded (npm publish, an earlier
214
+ registry publish, an existing release) are detected and skipped rather than
215
+ re-attempted or double-published.
216
+
217
+ **Caveat — content-level 422s are NOT re-run-recoverable.** A workflow
218
+ re-run checks out the tag, so a `server.json` validation error (the registry
219
+ returns HTTP 422 naming the failing field) reproduces identically on re-run.
220
+ Fix `server.json` on main and recover via the manual path below, or let the
221
+ fix ride the next tag. Known registry constraint (learned live): the
222
+ top-level `description` is capped at **100 characters** — v1.9.0's first
223
+ publish attempt 422'd on a 199-char description (2026-07-03); now pinned by
224
+ `tests/scripts/package-manifest.test.js`.
225
+
226
+ **Manual recovery (fallback, if re-run is not viable):**
227
+ 1. **Registry publish:** run the same local de-risk flow above for real —
228
+ `mcp-publisher login github` (device-flow login as BourbonDog), sync
229
+ `server.json`'s `.version` and `.packages[0].version` to the tag that
230
+ already published to npm, then `mcp-publisher publish`.
231
+ 2. **GitHub Release:** cut it by hand:
232
+ `gh release create <tag> --generate-notes --latest`. The "Generate release
233
+ notes with Claude" step is optional polish — skip it or run it manually
234
+ against the API.