amicus 4.8.0 → 4.9.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 (118) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/CHANGELOG.md +355 -0
  3. package/README.md +10 -5
  4. package/docs/CITATIONS.md +13 -5
  5. package/docs/ROADMAP.md +101 -10
  6. package/docs/configuration.md +55 -5
  7. package/docs/council.md +102 -14
  8. package/docs/troubleshooting.md +9 -2
  9. package/docs/usage.md +128 -12
  10. package/electron/ipc-setup.js +39 -2
  11. package/electron/main.js +46 -3
  12. package/electron/offer-session.js +51 -0
  13. package/electron/setup-ui-model.js +99 -9
  14. package/electron/setup-ui-styles.js +22 -0
  15. package/electron/setup-ui.js +244 -32
  16. package/electron/workspace-ui/live-dead-seats.js +163 -91
  17. package/electron/workspace-ui/live-seats.js +4 -4
  18. package/electron/workspace-ui/workspace-banners.js +30 -7
  19. package/electron/workspace-ui/workspace-matrix.js +23 -3
  20. package/electron/workspace-ui/workspace-seats.js +95 -79
  21. package/package.json +2 -1
  22. package/schemas/council-run.schema.json +2 -2
  23. package/schemas/council-tally.schema.json +17 -1
  24. package/schemas/council-verdict.schema.json +12 -4
  25. package/schemas/run.schema.json +6 -1
  26. package/skills/second-opinion/COUNCIL-DESIGN.md +1 -1
  27. package/skills/second-opinion/MANUAL-ORCHESTRATION.md +1 -1
  28. package/skills/second-opinion/MODEL-NOTES.md +88 -9
  29. package/skills/second-opinion/SEAT-BRIEFS.md +36 -4
  30. package/skills/second-opinion/SKILL.md +151 -36
  31. package/src/cli-council-run-bench.js +98 -6
  32. package/src/cli-handlers-council-run.js +18 -6
  33. package/src/cli-handlers-council.js +57 -7
  34. package/src/cli-handlers-doctor.js +12 -15
  35. package/src/cli.js +3 -1
  36. package/src/council/anonymize.js +2 -1
  37. package/src/council/briefings-chair-task.js +161 -0
  38. package/src/council/briefings-chair.js +33 -8
  39. package/src/council/briefings-debate.js +79 -13
  40. package/src/council/briefings-stage2-task.js +236 -0
  41. package/src/council/briefings-stage2.js +103 -26
  42. package/src/council/briefings-task.js +167 -0
  43. package/src/council/briefings.js +41 -4
  44. package/src/council/chair-fallback.js +95 -0
  45. package/src/council/debate.js +38 -21
  46. package/src/council/findings.js +3 -2
  47. package/src/council/ledger.js +2 -2
  48. package/src/council/parse-stage2.js +64 -16
  49. package/src/council/report-cost.js +61 -0
  50. package/src/council/report-html.js +26 -4
  51. package/src/council/report-md.js +30 -2
  52. package/src/council/report.js +40 -37
  53. package/src/council/run-assemble.js +21 -6
  54. package/src/council/run-chair.js +44 -95
  55. package/src/council/run-debate-revote.js +81 -49
  56. package/src/council/run-debate.js +51 -34
  57. package/src/council/run-finish.js +5 -3
  58. package/src/council/run-retry-keys.js +4 -4
  59. package/src/council/run-retry-launch.js +4 -4
  60. package/src/council/run-retry-notes.js +72 -15
  61. package/src/council/run-stage1-launch.js +4 -4
  62. package/src/council/run-stage1-rows.js +9 -6
  63. package/src/council/run-stage2.js +81 -47
  64. package/src/council/run-stages.js +9 -21
  65. package/src/council/run-stats-entry.js +46 -1
  66. package/src/council/run.js +28 -13
  67. package/src/council/seats.js +2 -2
  68. package/src/council/stage1-bind.js +3 -2
  69. package/src/council/verdict-seat-loss.js +124 -0
  70. package/src/council/verdict.js +108 -99
  71. package/src/headless.js +256 -49
  72. package/src/mcp-council-bench.js +64 -3
  73. package/src/mcp-council-run.js +10 -3
  74. package/src/mcp-server.js +52 -12
  75. package/src/mcp-tools.js +41 -5
  76. package/src/observe/council-legs.js +2 -2
  77. package/src/opencode-client.js +19 -1
  78. package/src/pack/pack-forward.js +15 -12
  79. package/src/pack/pack-resolve.js +1 -1
  80. package/src/prompt-builder.js +17 -1
  81. package/src/sidecar/fanout-leg.js +26 -0
  82. package/src/sidecar/fanout.js +1 -1
  83. package/src/sidecar/list-council.js +178 -0
  84. package/src/sidecar/list-limit.js +3 -1
  85. package/src/sidecar/list-search.js +2 -1
  86. package/src/sidecar/models.js +8 -1
  87. package/src/sidecar/read.js +34 -10
  88. package/src/sidecar/setup.js +124 -0
  89. package/src/template/render.js +16 -7
  90. package/src/utils/alias-audit.js +81 -3
  91. package/src/utils/alias-shadow-writer.js +220 -0
  92. package/src/utils/alias-shadow.js +294 -0
  93. package/src/utils/config.js +1 -1
  94. package/src/utils/curated-models.js +16 -8
  95. package/src/utils/degrade.js +12 -5
  96. package/src/utils/doctor-alias-check.js +149 -0
  97. package/src/utils/engine-log-parse.js +289 -0
  98. package/src/utils/engine-log-tail.js +114 -0
  99. package/src/utils/engine-log.js +250 -0
  100. package/src/utils/engine-skew-records.js +146 -0
  101. package/src/utils/engine-skew.js +300 -0
  102. package/src/utils/gateway-router.js +10 -2
  103. package/src/utils/model-canonicalization.js +64 -0
  104. package/src/utils/model-catalog.js +1 -1
  105. package/src/utils/model-shortlist.js +100 -0
  106. package/src/utils/provider-default-picker.js +93 -45
  107. package/src/utils/provider-default-prompt.js +1 -1
  108. package/src/utils/quick-picks.js +2 -2
  109. package/src/utils/remediation-hints.js +24 -0
  110. package/src/utils/result-schema.js +10 -0
  111. package/src/utils/text-sanitize.js +81 -0
  112. package/src/utils/ttft.js +57 -0
  113. package/src/utils/untrusted-fence.js +111 -1
  114. package/src/workspace/fold-format.js +28 -7
  115. package/src/workspace/live-normalize.js +2 -1
  116. package/src/workspace/matrix-model.js +6 -2
  117. package/src/workspace/run-detail.js +35 -9
  118. package/src/workspace/seat-space.js +10 -6
@@ -50,7 +50,8 @@ function seatOf(leg) {
50
50
  model: leg.model || null,
51
51
  modelInput: leg.modelInput || null,
52
52
  role: leg.role || null,
53
- // ⚠️ v4.8 R5: the leg's seat id, and the reason live-dead-seats.js:209's
53
+ // ⚠️ v4.8 R5: the leg's seat id, and the reason live-dead-seats.js ::
54
+ // deadSeats' `if (s.seat)` suppression arm's
54
55
  // `if (s.seat)` arm is reachable at all. Null on a unique-alias bench, so the
55
56
  // guard there stays load-bearing — see the comment at that line.
56
57
  seat: leg.seat || null,
@@ -15,7 +15,8 @@
15
15
  * unconditionally (src/council/tally.js :: tally) — it is never a real source for
16
16
  * either the override badge or the post-override tier. Only `buildVerdict`
17
17
  * materializes `{from,to,reason}` and rewrites `tier` to `tierOverride.to`
18
- * (src/council/verdict.js:122-126). So both fields are joined in from
18
+ * (src/council/verdict.js :: buildVerdict's `findings` map re-anchored BY SYMBOL at v4.9 W9,
19
+ * which shifted the old `:128-132`). So both fields are joined in from
19
20
  * verdict.findings[] by `id`; when verdict is absent/unparseable (caller
20
21
  * passes null/undefined, or a finding has no verdict-side counterpart) the
21
22
  * row falls back to tally's own (pre-override) tier and renders no badge.
@@ -183,7 +184,10 @@ function buildMatrixModel(tally, labelMap, verdict) {
183
184
  // ⚠️ DE-ROT (F29): v4.1 decorates tally.json findings in place with
184
185
  // `debate: {action, previousTier}` (src/council/debate.js :: decorateRecord; action ∈
185
186
  // defended|amended|withdrawn|no-response) and verdict.json carries it through
186
- // (src/council/verdict.js:43). Consumed by electron/workspace-ui/workspace-matrix.js's
187
+ // (src/council/verdict.js :: buildVerdict's `if (f.debate)` carry-through the old
188
+ // `:43` was ALREADY rotted onto summarizeSeatLoss, which has since left verdict.js
189
+ // altogether; re-anchored by symbol, not re-numbered). Consumed by
190
+ // electron/workspace-ui/workspace-matrix.js's
187
191
  // renderMatrix, which renders a `.debate-badge` in the tier cell (alongside the
188
192
  // thin/tierOverride badges) so a withdrawn/amended/defended/no-response finding never
189
193
  // renders as an ordinary live row. Absent on non-debate runs, hence `|| null`.
@@ -124,13 +124,13 @@ function costPanel(run, tally) {
124
124
 
125
125
  // ⚠️ PRE-FLIGHT (P3): F04's correction is implemented here rather than left as prose.
126
126
  // VERIFIED on shipped main (Task 0): `finalize(exitCode, error)` writes `error: error || null`
127
- // (src/council/run.js:98-100), and `return finalize(degraded.value ? 2 : 0)` (:293) is the ONLY
127
+ // (run-finalize.js :: writeRunTerminal), and `return finalize(degraded.value ? 2 : 0)` (run.js:294) is the ONLY
128
128
  // exit-2 path — it passes NO error. Every error-bearing call is `finalize(1, …)`. So on a
129
129
  // `status:'partial'` run — precisely the run this panel exists to explain — `run.error` is
130
130
  // GUARANTEED null, and the old one-line formula rendered "undefined: undefined".
131
131
  // Name the stage instead. Stage status is a closed set (DE-ROT F19): running / complete /
132
- // skipped / error, and `run-chair.js:114` writes 'error' for a chair that failed after retry +
133
- // fallback promotion, 'skipped' (:89) for one the cost ceiling skipped.
132
+ // skipped / error, and `run-chair.js :: chairStatus` writes 'error' for a chair that failed after retry +
133
+ // fallback promotion, 'skipped' (run-chair.js :: skippedForCost) for one the cost ceiling skipped.
134
134
  function degradedReason(run) {
135
135
  // exit-1 path: the engine wrote a structured {code, message}.
136
136
  if (run.error && run.error.code) { return `${run.error.code}: ${run.error.message}`; }
@@ -142,13 +142,32 @@ function degradedReason(run) {
142
142
  return null;
143
143
  }
144
144
 
145
+ // v4.9 W8 T-B: `intent` rides this payload so the renderer can label a task run's
146
+ // chip `ANSWER:` (electron/workspace-ui/workspace-matrix.js :: renderVerdict is a
147
+ // plain browser script and cannot require() src/, so a fork key it cannot read is
148
+ // a fork it cannot make). Unlike verdict.json — emit-when-'task' per the W5 ruling,
149
+ // because that artifact has a byte-identity contract — this is an in-memory IPC
150
+ // model whose literal is CLOSED and always materializes every key with a default,
151
+ // so `intent` is materialized both ways and defaults to 'review'. The renderer
152
+ // still treats an ABSENT key as review: pre-v4.9 payloads carry none.
153
+ // ⚠️ v4.9 fix round 2 (council B2): sourced from `run` as well as `verdict`.
154
+ // Reading `verdict.intent` ALONE meant the two panels that exist to explain a
155
+ // broken run could not label it: on the narrow leg where a task run exits before
156
+ // the verdict write — or leaves a truncated one — there is no verdict.intent to
157
+ // read, and the panel defaulted to review, so the Workspace chip said "no chair
158
+ // verdict" about a run that was never on that scale. `run.json` checkpoints
159
+ // `intent: 'task'` at start (run.js :: runCouncil) and `run` is already this
160
+ // function's first parameter, so the honest source costs nothing to reach.
145
161
  function verdictPanel(run, verdict) {
146
162
  const reason = degradedReason(run);
163
+ // PR #200 round-5 B3: parens on the whole disjunction — behaviour-identical.
164
+ const intent = ((verdict && verdict.intent === 'task') || (run && run.intent === 'task')) ? 'task' : 'review';
147
165
  if (!verdict || verdict.parseError) {
148
- return { present: false, overallVerdict: null, tierCounts: null, streetCred: [], decisions: [], reason };
166
+ return { present: false, overallVerdict: null, tierCounts: null, streetCred: [], decisions: [], reason, intent };
149
167
  }
150
168
  return {
151
169
  present: true,
170
+ intent,
152
171
  overallVerdict: verdict.overallVerdict === undefined ? null : verdict.overallVerdict,
153
172
  tierCounts: verdict.tierCounts || null,
154
173
  streetCred: Array.isArray(verdict.streetCred) ? verdict.streetCred : [],
@@ -233,11 +252,18 @@ function getRunDetail(project, runId) {
233
252
  // of a copy of the question makes that divergence unrepresentable.
234
253
  seatSpace: isSeatTable(run.seats),
235
254
  // ⚠️ Fix-wave 3 (council-3 C2): seats[] is PRESENT but unusable, so the run silently
236
- // lost per-seat behaviour — two seats on one model become indistinguishable in every
237
- // panel. isSeatTable fails WHOLE (one malformed entry drops the entire table), which
238
- // is the fail-safe direction but is exactly the correct-but-silent degrade the product
239
- // principle rejects. Emitted here, beside the predicate that decides it, rather than
240
- // re-derived renderer-side; workspace-app.js's renderBanners is its only consumer.
255
+ // lost per-seat ARTIFACT NAMING — two seats on one model become indistinguishable in
256
+ // the reviews/judges/re-vote panels, whose roster this flag sends back to `bench`
257
+ // (workspace-lazy.js :: roster). isSeatTable fails WHOLE (one malformed entry drops the
258
+ // entire table), which is the fail-safe direction but is exactly the correct-but-silent
259
+ // degrade the product principle rejects. Emitted here, beside the predicate that decides
260
+ // it, rather than re-derived renderer-side; workspace-banners.js is its only consumer.
261
+ // ⚠️ "in every panel" — what this comment said until v4.9 W9 — was already FALSE when
262
+ // written, and W9's PR5b-1 measured it: `costPanel` above maps `tally.runStats`, whose
263
+ // `seat` is stamped from the in-memory seat at assembly (run-stats-entry.js ::
264
+ // buildRunStatsEntry), so the seats panel keeps one row per seat no matter what
265
+ // run.seats looks like. Ruling V15 discloses that split in the banner rather than
266
+ // forcing the seats panel down to `seatSpace` for uniformity.
241
267
  // NOT the same as `!seatSpace`: a run with no seats[] at all is a legacy run, not a
242
268
  // broken one, and must not be bannered.
243
269
  seatTableRejected: Array.isArray(run.seats) && run.seats.length > 0 && !isSeatTable(run.seats),
@@ -75,18 +75,22 @@ function isSeatTable(seats) {
75
75
  * bound seat a `rebuttal-` an orphan may have written, re-arming RN-1 in the debate
76
76
  * namespace (MEASURED; the one regression this fix wave introduced). The question is:
77
77
  * does the note POSITIVELY PROVE the orphan did NOT write this kind? Exactly one such
78
- * proof exists. `orphanLegNote` has three call sites — run-stages.js:71 and :140 (Stage-1
79
- * and its retry) and run-stage2.js:110 (the -s2 judge wave) — and `data.waveId` separates
80
- * them EXACTLY, not heuristically: run-stage2.js:67/69/90 build it as `${runId}-s2` from
78
+ * proof exists. `orphanLegNote` has three call sites — two in run-stages.js :: runStage1
79
+ * (Stage-1's own bind and its retry pass; cited `:71 and :140` until the v4.9 W9 fix round
80
+ * moved a builder out of that function and both numbers rotted — anchored by SYMBOL now, per
81
+ * the anti-rot rule) and run-stage2.js :: bindStage2Seats (the -s2 judge wave) — and
82
+ * `data.waveId` separates them EXACTLY, not heuristically: run-stage2.js :: bindStage2Seats
83
+ * and the launch site in run-stage2.js :: runStage2 both build it as `${runId}-s2` from
81
84
  * the runId this run.json carries. A -s2 note says the leg BOUND in Stage 1, so its review
82
85
  * landed under a SEAT name: `review-<alias>.md` is provably not its. Nothing else is
83
86
  * provable, and the near misses are why:
84
87
  * - a Stage-1 note exonerates NOTHING, not even judge-: that orphan is re-admitted to
85
- * Stage 2 under a PLACEHOLDER seat (run-stage2.js:92-97) that `judgeSeatOf` filters out
86
- * (:105-107), so its judge leg takes the alias branch too — and emits no -s2 note,
88
+ * Stage 2 under a PLACEHOLDER seat that stage1-bind.js :: bindPaddedWave pads in and
89
+ * drops from `judgeSeatOf` (called from run-stage2.js :: bindStage2Seats since the
90
+ * v4.9 W2 split), so its judge leg takes the alias branch too — and emits no -s2 note,
87
91
  * because it BOUND. Measured: run-stages.test.js :: "M2: the placeholder never becomes…".
88
92
  * - rebuttal-/revote- are never exonerated: a debate leg whose raiser/judge key names no
89
- * seat takes materializeDebate's alias branch (run-debate.js :: runDebate;
93
+ * seat takes materializeDebate's alias branch (run-debate.js :: runDefenseWave;
90
94
  * run-debate-revote.js :: runRevoteWave, `seat = seatOf.get(leg) || null`). A rebuttal
91
95
  * leg's unbound raiser still has no note recording it. A revote leg's unbound judge
92
96
  * SOMETIMES gets one, since v4.8 T5.1 (channel `seat-unbound`, run-debate-revote.js's