amicus 4.7.1 → 4.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/CHANGELOG.md +988 -1
  3. package/README.md +5 -4
  4. package/docs/CITATIONS.md +122 -0
  5. package/docs/ROADMAP.md +33 -5
  6. package/docs/SHIMS.md +1 -1
  7. package/docs/configuration.md +3 -3
  8. package/docs/council.md +179 -32
  9. package/docs/doc-system.md +1 -1
  10. package/docs/publishing.md +2 -0
  11. package/docs/troubleshooting.md +3 -3
  12. package/docs/usage.md +2 -2
  13. package/electron/ipc-setup.js +18 -2
  14. package/electron/main.js +46 -3
  15. package/electron/setup-ui-aliases.js +2 -2
  16. package/electron/setup-ui-model.js +99 -9
  17. package/electron/setup-ui-styles.js +22 -0
  18. package/electron/setup-ui.js +231 -29
  19. package/electron/workspace-ui/index.html +9 -0
  20. package/electron/workspace-ui/live-dead-seats.js +228 -0
  21. package/electron/workspace-ui/live-model.js +10 -236
  22. package/electron/workspace-ui/live-seats.js +126 -0
  23. package/electron/workspace-ui/workspace-app.js +6 -41
  24. package/electron/workspace-ui/workspace-banners.js +95 -0
  25. package/electron/workspace-ui/workspace-lazy.js +55 -12
  26. package/electron/workspace-ui/workspace-matrix.js +2 -2
  27. package/electron/workspace-ui/workspace-panels.js +42 -10
  28. package/electron/workspace-ui/workspace-render.js +2 -2
  29. package/electron/workspace-ui/workspace-seats.js +101 -17
  30. package/package.json +4 -1
  31. package/schemas/council-run-live.schema.json +1 -0
  32. package/schemas/council-run.schema.json +19 -0
  33. package/schemas/council-tally.schema.json +34 -2
  34. package/schemas/council-verdict.schema.json +15 -0
  35. package/skills/second-opinion/COUNCIL-DESIGN.md +9 -4
  36. package/skills/second-opinion/MANUAL-ORCHESTRATION.md +1 -1
  37. package/skills/second-opinion/SKILL.md +25 -11
  38. package/src/cli-handlers-doctor.js +18 -14
  39. package/src/council/anonymize.js +80 -11
  40. package/src/council/briefings-chair.js +272 -0
  41. package/src/council/briefings-stage2.js +12 -140
  42. package/src/council/debate.js +120 -15
  43. package/src/council/ledger-join.js +284 -0
  44. package/src/council/ledger-stats.js +100 -0
  45. package/src/council/ledger.js +177 -88
  46. package/src/council/parse-stage2.js +1 -1
  47. package/src/council/peer-split.js +196 -0
  48. package/src/council/report-html.js +12 -5
  49. package/src/council/report-md.js +146 -0
  50. package/src/council/report.js +188 -112
  51. package/src/council/run-assemble.js +100 -79
  52. package/src/council/run-chair.js +17 -1
  53. package/src/council/run-debate-revote.js +268 -0
  54. package/src/council/run-debate.js +92 -102
  55. package/src/council/run-finish.js +70 -0
  56. package/src/council/run-launch.js +45 -18
  57. package/src/council/run-retry-group.js +266 -0
  58. package/src/council/run-retry-keys.js +74 -0
  59. package/src/council/run-retry-launch.js +55 -0
  60. package/src/council/run-retry-notes.js +65 -13
  61. package/src/council/run-retry.js +166 -161
  62. package/src/council/run-stage1-launch.js +27 -10
  63. package/src/council/run-stage1-rows.js +220 -0
  64. package/src/council/run-stage1-superseded.js +156 -0
  65. package/src/council/run-stage2.js +65 -5
  66. package/src/council/run-stages.js +72 -69
  67. package/src/council/run-state.js +1 -1
  68. package/src/council/run-stats-entry.js +71 -0
  69. package/src/council/run-verdict-files.js +52 -0
  70. package/src/council/run.js +42 -52
  71. package/src/council/seats.js +262 -0
  72. package/src/council/stage1-bind.js +142 -0
  73. package/src/council/street-cred.js +258 -0
  74. package/src/council/tally.js +100 -49
  75. package/src/council/verdict.js +49 -1
  76. package/src/headless.js +20 -6
  77. package/src/mcp-tools.js +71 -1
  78. package/src/observe/council-legs.js +7 -1
  79. package/src/observe/live-doc.js +3 -3
  80. package/src/sidecar/fanout-leg.js +22 -1
  81. package/src/sidecar/fanout-wave-io.js +26 -1
  82. package/src/sidecar/fanout.js +4 -10
  83. package/src/sidecar/leg-ids.js +19 -0
  84. package/src/sidecar/models-probe.js +7 -4
  85. package/src/sidecar/reopen-spend.js +1 -1
  86. package/src/sidecar/setup.js +137 -2
  87. package/src/utils/alias-audit.js +81 -3
  88. package/src/utils/config.js +109 -11
  89. package/src/utils/curated-models.js +17 -2
  90. package/src/utils/degrade.js +5 -0
  91. package/src/utils/doctor-alias-check.js +152 -0
  92. package/src/utils/model-canonicalization.js +64 -0
  93. package/src/utils/model-shortlist.js +100 -0
  94. package/src/utils/no-output-backstop.js +1 -1
  95. package/src/utils/provider-default-picker.js +93 -45
  96. package/src/utils/provider-default-prompt.js +1 -1
  97. package/src/utils/quick-picks.js +2 -2
  98. package/src/utils/remediation-hints.js +37 -0
  99. package/src/utils/session-index-prune.js +297 -0
  100. package/src/utils/session-metadata-tmp-sweep.js +1 -1
  101. package/src/workspace/artifact-guard.js +8 -114
  102. package/src/workspace/artifact-names.js +222 -0
  103. package/src/workspace/fold-format.js +9 -6
  104. package/src/workspace/live-normalize.js +6 -2
  105. package/src/workspace/matrix-model.js +141 -19
  106. package/src/workspace/run-detail.js +30 -4
  107. package/src/workspace/seat-space.js +143 -0
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Council Workspace — the seat-space PREDICATES (v4.8 PR5b).
3
+ *
4
+ * Extracted from artifact-names.js, which was at 300/300 with zero headroom — and which was
5
+ * itself split out of artifact-guard.js for the same gate one rev earlier. Comment prose was
6
+ * shaved three times during PR5a's fix waves to land defect fixes inside the limit; that was
7
+ * the signal this seam was overdue, not that the comments were too long.
8
+ *
9
+ * Split line: this module answers two QUESTIONS ABOUT A RUN — "is this seat table usable?"
10
+ * and "what has an orphan note proven?" — from run.json fields alone (`seats`, `degrades`,
11
+ * `runId`). It derives no names and knows nothing about artifacts. artifact-names.js keeps
12
+ * every NAME decision and imports both answers.
13
+ *
14
+ * ⚠️ Pure and total: no I/O, no fs, no throw path. Both functions take a schema-free
15
+ * `JSON.parse` of run.json and must tolerate any shape, which is why the conjuncts below are
16
+ * spelled out rather than assumed.
17
+ *
18
+ * ⚠️ The workspace -> council import lives here now (isSeatSpace, src/council/report.js).
19
+ * That direction is deliberate and predates this split: ONE seat-space predicate for the
20
+ * whole tree, never re-spelled — it is defined and exported at
21
+ * src/council/report.js :: isSeatSpace, and
22
+ * src/workspace/matrix-model.js:25 already imports it the same way. Council-4's B3 called the
23
+ * layering an inversion; it is not fixed here, and moving it OUT of the security-critical
24
+ * name-derivation file is a side effect of this split, not a response to that finding.
25
+ */
26
+ 'use strict';
27
+
28
+ const { isSeatSpace } = require('../council/report');
29
+
30
+ /**
31
+ * Is `seats` a seat table this guard may trust?
32
+ *
33
+ * `isSeatSpace` (shared, src/council/report.js) was written for `verdict.seats`, a
34
+ * producer-controlled document. This function reads a schema-free `JSON.parse` of
35
+ * run.json, so it adds three conjuncts `isSeatSpace` has no reason to carry:
36
+ * - non-empty ids — `{id:''}` otherwise emits `review-.md` and a `""` map key;
37
+ * - unique ids — the alias path's `new Set` used to guarantee this, and without it
38
+ * duplicate ids mint a one-element "collision" whose `join(' and ')` renders
39
+ * malformed English AND suppresses the real run.error banner (workspace-app.js
40
+ * returns after the collision branch);
41
+ * - STRING aliases (council-2 C3) — `isSeatSpace` checks only `id`, so `[{id:'a#1'}]` passed
42
+ * and put the RENDERER in seat space, where roster() resolves `labelByModel[s.alias]` ->
43
+ * undefined and AmicusRender.display() falls through to `pair.model`, printing the seat id
44
+ * `a#1` with blind mode ON — which defeats blind mode, a seat id containing its alias. The
45
+ * renderer now trusts this predicate wholesale (council-1 B1), so a conjunct the ALIAS
46
+ * path depends on belongs in it.
47
+ * Fails WHOLE: one malformed id sends the run back to the alias branch. Fail-safe but
48
+ * silent, and only reachable from a hand-edited run.json — parseList trims and filters,
49
+ * and the MCP path re-joins through the same code.
50
+ */
51
+ function isSeatTable(seats) {
52
+ if (!isSeatSpace(seats)) { return false; }
53
+ const ids = seats.map(s => s.id);
54
+ return ids.every(id => id !== '') && new Set(ids).size === ids.length
55
+ && seats.every(s => typeof s.alias === 'string' && s.alias !== '');
56
+ }
57
+
58
+ /**
59
+ * The alias names an ORPHANED leg wrote, taken from run.json's own degrade notes.
60
+ *
61
+ * The engine names an artifact from the seat when a leg binds and from the leg's
62
+ * `modelInput || model` when it does not (run-launch.js :: materializeReviews).
63
+ * Only the second case puts
64
+ * an alias-named file on disk, and `run-degrade.js` records exactly that case: channel
65
+ * 'seat-unbound' with `data.legId` set. `data.seat` on such a note IS the string the writer
66
+ * used, so the name derived here matches the file byte-for-byte — including where the leg
67
+ * reported no modelInput and the writer fell back to the resolved model id, which no
68
+ * seat.alias could have reproduced. Gating fallbacks on the NOTE, rather than emitting one
69
+ * per seat alias, is what keeps a healthy run (twin, --debate, or with a merely-dead seat)
70
+ * from claiming names nobody wrote and raising a run-integrity banner for it.
71
+ *
72
+ * ⚠️ Fix-wave revision 2 (council-1 B2, then council-2 A1/C1 which refuted revision 1).
73
+ * Contesting DROPS the owning seat's attribution, so the question is never "can the orphan
74
+ * own this kind?" — absence of evidence is not evidence, and reading it that way handed a
75
+ * bound seat a `rebuttal-` an orphan may have written, re-arming RN-1 in the debate
76
+ * namespace (MEASURED; the one regression this fix wave introduced). The question is:
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
81
+ * the runId this run.json carries. A -s2 note says the leg BOUND in Stage 1, so its review
82
+ * landed under a SEAT name: `review-<alias>.md` is provably not its. Nothing else is
83
+ * provable, and the near misses are why:
84
+ * - 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,
87
+ * because it BOUND. Measured: run-stages.test.js :: "M2: the placeholder never becomes…".
88
+ * - 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;
90
+ * run-debate-revote.js :: runRevoteWave, `seat = seatOf.get(leg) || null`). A rebuttal
91
+ * leg's unbound raiser still has no note recording it. A revote leg's unbound judge
92
+ * SOMETIMES gets one, since v4.8 T5.1 (channel `seat-unbound`, run-debate-revote.js's
93
+ * reVoteUnboundNote). ⚠️ SOMETIMES, not always — this read "DOES" until the
94
+ * whole-branch fix wave, and that was FALSE. The bullet's subject is EVERY debate leg
95
+ * with `seat === null`, and that set is STRICTLY LARGER than the set that gets a note.
96
+ * Two shapes land here SILENTLY — measured through the real runRevoteWave, not
97
+ * reasoned: a leg bound to a §3.4 PLACEHOLDER, and a leg bound to nothing whose
98
+ * bare-alias key IS one of the `judgeKeys` this wave launched. runRevoteWave publishes
99
+ * BOTH and notes neither, while
100
+ * `seatOf` still filters the placeholder back out — so `seat` is null in all three
101
+ * shapes and a note exists for exactly one of them.
102
+ * ⚠️ The PREDICATE that publishes both is `judgeKeys.includes(key)` ALONE. This read
103
+ * `boundLegs.has(leg) || judgeKeys.includes(key)` until v4.8 T5.5 deleted the first
104
+ * arm; the §3.4 placeholder shape survives that deletion on the SECOND arm, because a
105
+ * roster hole's own bare alias IS one of the judgeKeys — so both shapes above still
106
+ * land here, and this bullet is unchanged in substance. What no longer lands silently
107
+ * is a placeholder-bound leg carrying a FOREIGN alias: it is now refused and noted
108
+ * (run-debate.test.js's "T5.5: a taskId-bound leg carrying a FOREIGN alias is
109
+ * REFUSED" block), so the silent set shrank by exactly that shape. Pinned by run-debate.test.js's
110
+ * test named "roster hole whose leg is ALSO unbindable: the key IS published and the
111
+ * re-vote still applies", whose `ctx.degrade.all()` is `[]`; the placeholder shape is
112
+ * pinned for its seat (`['gpt', null]`) by that file's "§3.4 placeholder contract at
113
+ * the -rv call site" block, which asserts nothing about notes either way.
114
+ * ⚠️ The CONCLUSION is identical in all three shapes, which is why this bullet still
115
+ * holds: even when the note IS emitted, its `data` deliberately omits `seat`, carrying
116
+ * only `judge`/`key`, so this function's own `const alias = d.data.seat;` read (below)
117
+ * still finds nothing and this leg stays un-exonerated, the same net effect as before
118
+ * (reVoteUnboundNote's own docblock states why `seat` is withheld on purpose).
119
+ * - an unmatchable waveId exonerates nothing.
120
+ * @returns {Map<string, Set<string>>} orphan alias -> the kinds it provably did NOT write,
121
+ * in first-occurrence order (what keeps the emitted name list byte-identical).
122
+ */
123
+ const S2_EXONERATES = Object.freeze(['review']);
124
+ function orphanExonerations(run) {
125
+ const degrades = run && Array.isArray(run.degrades) ? run.degrades : [];
126
+ const s2WaveId = run && typeof run.runId === 'string' ? `${run.runId}-s2` : null;
127
+ const byAlias = new Map();
128
+ for (const d of degrades) {
129
+ if (!d || d.channel !== 'seat-unbound' || !d.data || !d.data.legId) { continue; }
130
+ const alias = d.data.seat;
131
+ if (typeof alias !== 'string' || alias === '') { continue; }
132
+ // ⚠️ `has`, not `||` (council-3 B1): an empty Set is TRUTHY, so `||` worked — a cleared
133
+ // set stayed cleared — but read as default-when-missing. A `.size` check re-opens the union.
134
+ const proven = byAlias.has(alias) ? byAlias.get(alias) : new Set(S2_EXONERATES);
135
+ // INTERSECTION across an alias's notes, never union: two notes for one alias means two
136
+ // orphaned legs, and a Stage-1 one proves nothing about the review the -s2 one exonerates.
137
+ if (!(s2WaveId && d.data.waveId === s2WaveId)) { proven.clear(); }
138
+ byAlias.set(alias, proven);
139
+ }
140
+ return byAlias;
141
+ }
142
+
143
+ module.exports = { isSeatTable, orphanExonerations };