amicus 4.2.1 → 4.4.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 (82) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/CHANGELOG.md +46 -1
  3. package/README.md +8 -4
  4. package/bin/amicus.js +5 -0
  5. package/electron/ipc-workspace.js +283 -0
  6. package/electron/main.js +27 -0
  7. package/electron/preload-workspace.js +40 -0
  8. package/electron/workspace-shell.js +85 -0
  9. package/electron/workspace-ui/index.html +111 -0
  10. package/electron/workspace-ui/live-model.js +101 -0
  11. package/electron/workspace-ui/md-lite.js +119 -0
  12. package/electron/workspace-ui/workspace-app.js +240 -0
  13. package/electron/workspace-ui/workspace-matrix.js +212 -0
  14. package/electron/workspace-ui/workspace-panels.js +226 -0
  15. package/electron/workspace-ui/workspace-render.js +271 -0
  16. package/electron/workspace-ui/workspace-verbs.js +247 -0
  17. package/electron/workspace-ui/workspace.css +172 -0
  18. package/package.json +1 -1
  19. package/schemas/council-run-live.schema.json +57 -0
  20. package/schemas/council-run.schema.json +14 -0
  21. package/schemas/event.schema.json +15 -0
  22. package/schemas/progress.schema.json +37 -0
  23. package/schemas/run-live.schema.json +15 -0
  24. package/schemas/spend.schema.json +26 -1
  25. package/schemas/wave-live.schema.json +15 -0
  26. package/skills/second-opinion/MODEL-NOTES.md +53 -5
  27. package/src/cli-handlers-council-run.js +86 -8
  28. package/src/cli-handlers-run.js +26 -0
  29. package/src/cli-handlers-spend.js +94 -32
  30. package/src/cli-handlers-watch.js +116 -0
  31. package/src/cli.js +58 -1
  32. package/src/council/briefings.js +35 -2
  33. package/src/council/run-budget.js +224 -0
  34. package/src/council/run-chair.js +10 -2
  35. package/src/council/run-debate.js +5 -1
  36. package/src/council/run-launch.js +58 -7
  37. package/src/council/run-stages.js +30 -3
  38. package/src/council/run.js +44 -15
  39. package/src/headless.js +356 -15
  40. package/src/mcp-council-awareness.js +98 -3
  41. package/src/mcp-council-run.js +28 -4
  42. package/src/mcp-notify.js +54 -0
  43. package/src/mcp-server.js +51 -1
  44. package/src/mcp-spend.js +125 -0
  45. package/src/mcp-tools.js +39 -0
  46. package/src/mcp-wait.js +28 -2
  47. package/src/observe/council-legs.js +183 -0
  48. package/src/observe/events.js +156 -0
  49. package/src/observe/follow.js +26 -0
  50. package/src/observe/live-doc.js +56 -0
  51. package/src/observe/on-complete.js +117 -0
  52. package/src/observe/watch-render.js +168 -0
  53. package/src/opencode-client.js +15 -3
  54. package/src/sidecar/child-sessions.js +198 -0
  55. package/src/sidecar/continue.js +32 -0
  56. package/src/sidecar/conversation-mirror.js +111 -37
  57. package/src/sidecar/fallback-chains.js +65 -0
  58. package/src/sidecar/fanout-budget.js +71 -0
  59. package/src/sidecar/fanout-leg-fallback.js +189 -0
  60. package/src/sidecar/fanout-leg.js +81 -27
  61. package/src/sidecar/fanout-retry.js +208 -0
  62. package/src/sidecar/fanout-validate.js +42 -4
  63. package/src/sidecar/fanout.js +54 -41
  64. package/src/sidecar/progress.js +5 -0
  65. package/src/sidecar/resume.js +12 -0
  66. package/src/sidecar/start.js +13 -1
  67. package/src/sidecar/tool-part.js +196 -0
  68. package/src/sidecar/workspace-window.js +62 -0
  69. package/src/spend-query.js +119 -0
  70. package/src/utils/env-num.js +42 -0
  71. package/src/utils/error-classify.js +31 -0
  72. package/src/utils/model-tiers.js +1 -1
  73. package/src/utils/path-fence.js +82 -0
  74. package/src/utils/pricing.js +98 -9
  75. package/src/utils/spend-ledger.js +24 -1
  76. package/src/workspace/artifact-guard.js +187 -0
  77. package/src/workspace/blind-mode.js +32 -0
  78. package/src/workspace/fold-format.js +95 -0
  79. package/src/workspace/live-normalize.js +156 -0
  80. package/src/workspace/matrix-model.js +94 -0
  81. package/src/workspace/run-detail.js +223 -0
  82. package/src/workspace/run-scan.js +148 -0
@@ -0,0 +1,172 @@
1
+ /* Council Workspace styles — every color/font/space from src/design/tokens.css. */
2
+
3
+ * { box-sizing: border-box; }
4
+ /* ⚠️ DE-ROT (F33): REQUIRED — do not drop. `.dialog-backdrop { display: flex }` below is an
5
+ AUTHOR rule, and author origin beats the UA's `[hidden] { display: none }` regardless of
6
+ specificity — so `#dialog-abort` (and `#run-view`, also `hidden`-attribute-gated) would be
7
+ visible from first paint and Task 16's `$('dialog-abort').hidden = true` would never hide it.
8
+ Nothing under electron/ or src/ defines a `[hidden]` reset today; this line is the only guard. */
9
+ [hidden] { display: none !important; }
10
+ html, body { height: 100%; margin: 0; }
11
+ body {
12
+ background: var(--bg);
13
+ color: var(--text-1);
14
+ font-family: var(--font-sans);
15
+ font-size: var(--fs-13);
16
+ line-height: var(--lh-body);
17
+ }
18
+ .mono { font-family: var(--font-mono); }
19
+
20
+ #app { display: flex; height: 100vh; }
21
+
22
+ /* ---- left rail -------------------------------------------------------- */
23
+ #rail {
24
+ width: 280px; min-width: 220px; flex: none;
25
+ border-right: var(--bd); background: var(--surface-1);
26
+ display: flex; flex-direction: column;
27
+ }
28
+ .rail-head {
29
+ padding: var(--pad-card); border-bottom: var(--bd);
30
+ display: flex; flex-direction: column; gap: var(--space-2);
31
+ }
32
+ .rail-title { font-weight: var(--w-semibold); letter-spacing: var(--ls-label); }
33
+ .rail-project { color: var(--text-3); font-size: var(--fs-10); overflow: hidden; text-overflow: ellipsis; }
34
+ #run-list { list-style: none; margin: 0; padding: var(--space-2); overflow-y: auto; flex: 1; }
35
+ #run-list li {
36
+ padding: var(--space-4) var(--space-6); border-radius: var(--r-6);
37
+ cursor: pointer; border: 1px solid transparent;
38
+ }
39
+ #run-list li:hover { background: var(--surface-2); }
40
+ #run-list li.selected { background: var(--surface-sel); border-color: var(--accent-line); }
41
+ #run-list li.error-row { color: var(--text-3); cursor: default; }
42
+ #run-list li:focus-visible, .btn:focus-visible, #blind-toggle:focus-visible, summary:focus-visible {
43
+ outline: 2px solid var(--focus-ring); outline-offset: 1px;
44
+ }
45
+ .run-row-top { display: flex; justify-content: space-between; gap: var(--space-4); }
46
+ .run-row-sub { color: var(--text-3); font-size: var(--fs-11); display: flex; gap: var(--space-4); flex-wrap: wrap; }
47
+
48
+ /* ---- chips / status --------------------------------------------------- */
49
+ .chips { display: flex; gap: var(--space-3); flex-wrap: wrap; align-items: center; }
50
+ .chip {
51
+ padding: var(--pad-pill); border-radius: var(--r-full); font-size: var(--fs-10);
52
+ border: var(--bd-strong); color: var(--text-2); background: var(--surface-2);
53
+ }
54
+ .chip.running { color: var(--running); border-color: var(--running); }
55
+ .chip.running::before {
56
+ content: ''; display: inline-block; width: 6px; height: 6px; margin-right: var(--space-2);
57
+ border-radius: var(--r-full); background: var(--running); animation: ws-pulse 1.2s ease-in-out infinite;
58
+ }
59
+ .chip.complete { color: var(--ok); }
60
+ .chip.partial { color: var(--warn); }
61
+ .chip.error { color: var(--danger); }
62
+ .chip.aborted { color: var(--text-3); }
63
+ @keyframes ws-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.25; } }
64
+ @media (prefers-reduced-motion: reduce) { .chip.running::before { animation: none; } }
65
+
66
+ /* ---- detail ----------------------------------------------------------- */
67
+ #detail { flex: 1; overflow-y: auto; padding: var(--space-8); }
68
+ .empty-state { color: var(--text-3); padding: var(--space-24); text-align: center; }
69
+ #run-header { display: flex; flex-direction: column; gap: var(--space-4); margin-bottom: var(--space-6); }
70
+ .run-head-row { display: flex; align-items: center; gap: var(--space-8); flex-wrap: wrap; }
71
+ #run-title { font-size: var(--fs-18); margin: 0; }
72
+
73
+ .banner {
74
+ padding: var(--pad-card); border-radius: var(--r-6); margin-bottom: var(--space-6);
75
+ border: 1px solid var(--danger); color: var(--danger); background: var(--surface-1);
76
+ }
77
+ .banner.warn { border-color: var(--warn); color: var(--warn); }
78
+ .banner.info { border-color: var(--border-strong); color: var(--text-2); }
79
+
80
+ .stage-rail { display: flex; gap: var(--space-4); flex-wrap: wrap; margin-bottom: var(--space-8); }
81
+ .stage {
82
+ display: flex; align-items: center; gap: var(--space-3);
83
+ padding: var(--pad-pill); border-radius: var(--r-6); border: var(--bd);
84
+ color: var(--text-2); font-size: var(--fs-11);
85
+ }
86
+ .stage.complete { color: var(--ok); }
87
+ .stage.running { color: var(--running); border-color: var(--running); }
88
+ .stage.error { color: var(--danger); }
89
+ .stage.partial { color: var(--warn); }
90
+ .stage.skipped { color: var(--text-3); }
91
+
92
+ .panel { margin-bottom: var(--space-10); border: var(--bd); border-radius: var(--r-8); background: var(--surface-1); padding: var(--pad-card); }
93
+ .panel-title { font-size: var(--fs-13); font-weight: var(--w-semibold); margin: 0 0 var(--space-4) 0; cursor: default; }
94
+ details.panel > summary { cursor: pointer; }
95
+ .prose-host { color: var(--text-2); }
96
+ .prose-host h3, .prose-host h4, .prose-host h5, .prose-host h6 { color: var(--text-1); margin: var(--space-6) 0 var(--space-2); }
97
+ .prose-host pre {
98
+ background: var(--surface-3); border: var(--bd); border-radius: var(--r-6);
99
+ padding: var(--pad-card); overflow-x: auto; font-family: var(--font-mono); font-size: var(--fs-11);
100
+ }
101
+ .prose-host code { font-family: var(--font-mono); background: var(--surface-3); border-radius: var(--r-3); padding: 0 var(--space-2); }
102
+ .prose-section { border-top: var(--bd); padding-top: var(--space-4); margin-top: var(--space-4); }
103
+ .prose-section:first-child { border-top: none; margin-top: 0; padding-top: 0; }
104
+ mark { background: var(--gold-soft); color: var(--gold-400); border-radius: var(--r-2); }
105
+
106
+ /* ---- tables ------------------------------------------------------------ */
107
+ .table { width: 100%; border-collapse: collapse; font-size: var(--fs-12); }
108
+ .table th {
109
+ text-align: left; color: var(--text-3); font-weight: var(--w-medium);
110
+ border-bottom: var(--bd-strong); padding: var(--space-3) var(--space-4);
111
+ }
112
+ .table td { border-bottom: var(--bd); padding: var(--space-3) var(--space-4); }
113
+ .table td.num, .table th.num { text-align: right; font-family: var(--font-mono); }
114
+ .stalled-flag { color: var(--warn); }
115
+
116
+ /* ---- matrix tier rows (report-html light-ground pairs, as token vars) -- */
117
+ .matrix-wrap { overflow-x: auto; }
118
+ tr.tier-Confirmed td { background: var(--tier-confirmed); color: var(--tier-confirmed-ink); }
119
+ tr.tier-Contested td { background: var(--tier-contested); color: var(--tier-contested-ink); }
120
+ tr.tier-Disputed td { background: var(--tier-disputed); color: var(--tier-disputed-ink); }
121
+ tr.tier-Singleton td { background: var(--tier-singleton); color: var(--tier-singleton-ink); }
122
+ td.vote-cell { text-align: center; font-family: var(--font-mono); }
123
+ td.vote-cell.dispute { cursor: pointer; text-decoration: underline dotted; }
124
+ .thin-badge, .override-badge, .debate-badge {
125
+ font-size: var(--fs-9); border-radius: var(--r-full); padding: 0 var(--space-3);
126
+ border: 1px solid currentColor; margin-left: var(--space-2);
127
+ }
128
+
129
+ /* ---- gauge -------------------------------------------------------------- */
130
+ .gauge {
131
+ position: relative; flex: 1; min-width: 160px; max-width: 360px; height: 18px;
132
+ border: var(--bd-strong); border-radius: var(--r-full); background: var(--surface-2); overflow: hidden;
133
+ }
134
+ .gauge-fill { height: 100%; width: 0; background: var(--accent-soft); border-right: 2px solid var(--accent); }
135
+ .gauge.over .gauge-fill { background: var(--danger); border-right-color: var(--danger); }
136
+ /* v4.4 §8: `unknown` = at least one seat reported no usage, so the filled
137
+ fraction is a LOWER BOUND, not a measurement. Hatch the whole track (not just
138
+ the fill) so the empty remainder reads "indeterminate" rather than "headroom",
139
+ and dash the leading edge so it is visibly not a hard boundary. Paired with the
140
+ `≥` prefix renderGauge writes into .gauge-text. */
141
+ .gauge.unknown {
142
+ background-image: repeating-linear-gradient(135deg,
143
+ transparent 0 4px, var(--surface-3, var(--surface-2)) 4px 8px);
144
+ }
145
+ .gauge.unknown .gauge-fill { border-right-style: dashed; }
146
+ .gauge-text { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; font-size: var(--fs-10); color: var(--text-2); }
147
+
148
+ /* ---- buttons / dialog ---------------------------------------------------- */
149
+ .btn {
150
+ font-family: var(--font-sans); font-size: var(--fs-12); padding: var(--pad-btn-sm);
151
+ border-radius: var(--r-6); border: var(--bd-strong); background: var(--surface-2);
152
+ color: var(--text-1); cursor: pointer;
153
+ }
154
+ .btn:hover { background: var(--surface-3); }
155
+ .btn.danger { border-color: var(--danger); color: var(--danger); }
156
+ .btn.primary { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }
157
+ .btn[disabled] { opacity: 0.5; cursor: default; }
158
+ .blind-label { display: flex; align-items: center; gap: var(--space-3); color: var(--text-2); font-size: var(--fs-12); }
159
+
160
+ .dialog-backdrop {
161
+ position: fixed; inset: 0; background: var(--accent-glow);
162
+ backdrop-filter: blur(2px); display: flex; align-items: center; justify-content: center; z-index: 10;
163
+ }
164
+ .dialog {
165
+ background: var(--surface-2); border: var(--bd-strong); border-radius: var(--r-10);
166
+ box-shadow: var(--shadow-pop); padding: var(--space-12); max-width: 420px;
167
+ }
168
+ .dialog h2 { margin-top: 0; font-size: var(--fs-15); }
169
+ .dialog-actions { display: flex; justify-content: flex-end; gap: var(--space-4); margin-top: var(--space-8); }
170
+
171
+ .empty-note { color: var(--text-3); font-size: var(--fs-12); }
172
+ .truncate-note { color: var(--warn); font-size: var(--fs-11); margin-top: var(--space-3); }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "amicus",
3
- "version": "4.2.1",
3
+ "version": "4.4.0",
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": [
@@ -0,0 +1,57 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://github.com/BourbonDog/amicus/schemas/council-run-live.schema.json",
4
+ "title": "Amicus composed live council-run doc (amicus_status/buildCouncilStatusPayload, view:'live')",
5
+ "type": "object",
6
+ "required": ["taskId", "type", "runId", "runDir", "status", "currentStage", "stages", "legsTotal", "legsComplete", "elapsed", "exitCode", "version"],
7
+ "properties": {
8
+ "type": { "const": "council-run" },
9
+ "view": { "const": "live" },
10
+ "runId": { "type": "string" },
11
+ "runDir": { "type": "string" },
12
+ "status": { "type": "string" },
13
+ "currentStage": { "type": ["string", "null"] },
14
+ "stages": {
15
+ "type": "array",
16
+ "items": {
17
+ "type": "object",
18
+ "required": ["name", "status"],
19
+ "properties": {
20
+ "name": { "type": "string" },
21
+ "status": { "type": "string" },
22
+ "waveId": { "type": ["string", "null"] }
23
+ }
24
+ }
25
+ },
26
+ "legsTotal": { "type": ["number", "null"] },
27
+ "legsComplete": { "type": ["number", "null"] },
28
+ "elapsed": { "type": "string" },
29
+ "exitCode": { "type": ["number", "null"] },
30
+ "usage": { "type": "object" },
31
+ "legs": {
32
+ "description": "One row per active-stage leg id (DE-ROT Task 0.5 / F01), present unconditionally — a just-started leg with no usage flushed yet still gets a row.",
33
+ "type": "array",
34
+ "items": {
35
+ "type": "object",
36
+ "required": ["taskId", "model", "status"],
37
+ "properties": {
38
+ "taskId": { "type": "string" },
39
+ "model": { "type": ["string", "null"], "description": "Resolved executable id (metadata.model)." },
40
+ "modelInput": { "type": ["string", "null"], "description": "Council alias (run.json bench/chair/critic/lenses are keyed on this, not `model`); null when the leg's metadata.json has not been patched with it yet (F36)." },
41
+ "role": { "type": ["string", "null"], "description": "'seat' | 'critic' | 'lens:<slug>' | 'chair', derived via roleFor keyed on modelInput (chair via the owning stage's name instead, F34); null when modelInput is unknown." },
42
+ "status": { "type": "string" },
43
+ "messages": { "type": "number" },
44
+ "stage": { "type": "string" },
45
+ "latestPreview": { "type": ["string", "null"] },
46
+ "lastActivityAt": { "type": ["string", "null"] },
47
+ "stalled": { "type": "boolean" },
48
+ "usage": { "type": "object" },
49
+ "usageError": { "type": "string", "description": "Council review C3: set when enrichLegUsage (pricing resolution) throws for this leg's progress usage, so the failure is distinguishable from a leg that simply has not billed yet (no `usage` key, no `usageError` key either). Additive; absent on every leg that priced cleanly or has no usage yet." }
50
+ }
51
+ }
52
+ },
53
+ "stalled": { "type": "boolean" },
54
+ "stalledForSeconds": { "type": "number" }
55
+ },
56
+ "additionalProperties": true
57
+ }
@@ -34,6 +34,20 @@
34
34
  "options": { "type": "object" },
35
35
  "usage": { "type": "object" },
36
36
  "exitCode": { "type": ["number", "null"] },
37
+ "budgetRefusals": {
38
+ "description": "Waves the --max-cost ceiling refused at pre-flight (v4.4). Present only once at least one wave was refused. The run CONTINUES with a partial bench and exits degraded (2) — it is never rolled back and never aborted.",
39
+ "type": "array",
40
+ "items": {
41
+ "type": "object",
42
+ "required": ["waveId", "models"],
43
+ "properties": {
44
+ "waveId": { "type": ["string", "null"] },
45
+ "models": { "type": "array", "items": { "type": "string" } },
46
+ "reason": { "type": "string" },
47
+ "at": { "type": "string" }
48
+ }
49
+ }
50
+ },
37
51
  "debate": {
38
52
  "type": "object",
39
53
  "properties": {
@@ -0,0 +1,15 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://github.com/BourbonDog/amicus/schemas/event.schema.json",
4
+ "title": "Amicus observability event (events.jsonl line)",
5
+ "type": "object",
6
+ "required": ["schemaVersion", "type", "event", "ts", "id"],
7
+ "properties": {
8
+ "schemaVersion": { "const": 1 },
9
+ "type": { "const": "event" },
10
+ "event": { "enum": ["wave-started", "leg-started", "leg-fallback", "leg-terminal", "wave-terminal", "retry-started", "run-started", "stage-started", "stage-terminal", "run-terminal"] },
11
+ "ts": { "type": "string", "format": "date-time" },
12
+ "id": { "type": "string" }
13
+ },
14
+ "additionalProperties": true
15
+ }
@@ -0,0 +1,37 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://github.com/BourbonDog/amicus/schemas/progress.schema.json",
4
+ "title": "Amicus leg/solo progress snapshot (progress.json)",
5
+ "type": "object",
6
+ "required": ["schemaVersion", "type", "stage", "updatedAt"],
7
+ "properties": {
8
+ "schemaVersion": { "const": 1 },
9
+ "type": { "const": "progress" },
10
+ "stage": { "type": "string" },
11
+ "stageLabel": { "type": "string" },
12
+ "updatedAt": { "type": "string", "format": "date-time" },
13
+ "messagesReceived": { "type": "number" },
14
+ "latestTool": { "type": "string" },
15
+ "usage": {
16
+ "type": "object",
17
+ "properties": {
18
+ "tokens": { "type": "object" },
19
+ "costReported": { "type": "number" },
20
+ "subtree": {
21
+ "description": "v4.4.1 CA-1. Spend of the CHILD (subagent) OpenCode sessions this leg spawned, which OpenCode bills separately and does NOT roll into the parent session's cost. Attributed to this leg by the terminal walk in src/headless.js; resolved into usage.subtree by src/observe/live-doc.js enrichLegUsage so the live GUI and run.json agree. Absent when the leg spawned none.",
22
+ "type": "object",
23
+ "properties": {
24
+ "sessions": { "type": "number" },
25
+ "tokens": { "type": "object" },
26
+ "costReported": { "type": "number" }
27
+ }
28
+ },
29
+ "subtreeUnknown": {
30
+ "description": "The leg's own cost is stated, but there is positive evidence of a subagent subtree whose spend the walk could not account for. The total is a floor, not the bill. Present only when true.",
31
+ "const": true
32
+ }
33
+ }
34
+ }
35
+ },
36
+ "additionalProperties": true
37
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://github.com/BourbonDog/amicus/schemas/run-live.schema.json",
4
+ "title": "Amicus composed live single-session doc (amicus_status, view:'live')",
5
+ "description": "amicus_status's single-session response always stamps type:'run' (stampEnvelope); documented here as a const rather than the brief's 'may be absent' guess to match real output.",
6
+ "type": "object",
7
+ "required": ["taskId", "type", "status"],
8
+ "properties": {
9
+ "type": { "const": "run" },
10
+ "view": { "const": "live" },
11
+ "status": { "type": "string" },
12
+ "usage": { "type": "object" }
13
+ },
14
+ "additionalProperties": true
15
+ }
@@ -11,6 +11,31 @@
11
11
  "windowDays": { "type": ["number", "null"] },
12
12
  "total": { "type": "object" },
13
13
  "byModel": { "type": "array", "items": { "type": "object" } },
14
- "credit": { "type": ["object", "null"] }
14
+ "credit": { "type": ["object", "null"] },
15
+ "filters": { "type": "object" },
16
+ "groupBy": { "enum": ["model", "wave", "council", "project", "op", "day"] },
17
+ "groups": {
18
+ "type": "array",
19
+ "items": {
20
+ "type": "object",
21
+ "required": ["key", "amount", "runs"],
22
+ "properties": {
23
+ "key": { "type": "string" },
24
+ "amount": { "type": "number" },
25
+ "runs": { "type": "number" }
26
+ }
27
+ }
28
+ },
29
+ "wasted": {
30
+ "type": "object",
31
+ "required": ["amount", "runs", "byStatus"],
32
+ "properties": {
33
+ "amount": { "type": "number" },
34
+ "runs": { "type": "number" },
35
+ "byStatus": { "type": "object" }
36
+ }
37
+ },
38
+ "rows": { "type": "array" },
39
+ "rowsTruncated": { "type": "boolean" }
15
40
  }
16
41
  }
@@ -0,0 +1,15 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://github.com/BourbonDog/amicus/schemas/wave-live.schema.json",
4
+ "title": "Amicus composed live wave doc (amicus_status, view:'live')",
5
+ "type": "object",
6
+ "required": ["taskId", "type", "status", "legs"],
7
+ "properties": {
8
+ "type": { "const": "wave" },
9
+ "view": { "const": "live" },
10
+ "status": { "type": "string" },
11
+ "legs": { "type": "array", "items": { "type": "object", "properties": { "usage": { "type": "object" } } } },
12
+ "usage": { "type": "object" }
13
+ },
14
+ "additionalProperties": true
15
+ }
@@ -4,8 +4,8 @@ This file is the `second-opinion` skill's evolving memory of **how to actually d
4
4
  well**. Read it before Stage 0 (council selection and launch); update it, with the user's
5
5
  approval, at the end of each run (Stage 6). Keep it tight — merge and prune rather than append.
6
6
 
7
- _Last updated: 2026-07-14 (v2.2.0 fold-back: v3.1 optional council elements verified live;
8
- claim-class dedup adjudication limit; minimax and qwen-coder debut notes; see changelog)._
7
+ _Last updated: 2026-07-26 (v4.4.0 fold-back: alias-resolution hygiene, `council run --run-id`,
8
+ debate defense/re-vote waves now exercised, haiku and glm notes; see changelog)._
9
9
 
10
10
  ## Global operating rules (all models)
11
11
  - **Fast path:** `council run` applies `--agent Plan` / `--no-context` / `--summary-length
@@ -58,6 +58,20 @@ claim-class dedup adjudication limit; minimax and qwen-coder debut notes; see ch
58
58
  including ones outside a judge's focus. A council reviewing this contract flagged the asymmetry
59
59
  unanimously. Weigh a lone `Confirmed` tier accordingly, and prefer `Contested` evidence over
60
60
  vote counts when a finding matters.
61
+ - **`council run` prints nothing until the run is terminal.** The run id is generated internally and
62
+ the only stdout write happens after the run resolves, so a backgrounded `council run` gives you no
63
+ id to watch. **Pin it up front with `--run-id <id>`** and point `amicus watch <id>` at it directly.
64
+ - **Resolve every alias before you spend.** Two failure shapes, both cheap to pre-empt:
65
+ - **An alias that does not exist aborts the run before any spend** — `resolveModel` throws
66
+ `Unknown model alias '<x>'`. `deepseek-r1` is a recurring guess and is **not** shipped; the real
67
+ alias is `deepseek`. Check the alias table (`amicus models`) rather than inferring a name from a
68
+ model's marketing string.
69
+ - **A local alias override can silently upgrade a "cheap" seat to a frontier model.** Aliases in
70
+ `~/.config/amicus/config.json` take precedence over the shipped routes, so a bench you picked for
71
+ price can resolve to a Pro/preview tier and trip a low `--max-cost` (exit 1) — or quietly cost
72
+ 5-10× what you budgeted. Confirm what each seat actually resolves to before a budget bench.
73
+ - **The chair cannot also hold a bench seat**, so a bench built from budget aliases cannot chair
74
+ itself with one of them — pick the chair from *outside* the bench list.
61
75
  - **Stage-6 approvals:** write the proposed MODEL-NOTES diff to a run-folder file and put that path
62
76
  in the approval prompt — chat-text diffs can be hidden behind the approval dialog.
63
77
 
@@ -85,9 +99,13 @@ claim-class dedup adjudication limit; minimax and qwen-coder debut notes; see ch
85
99
  - **Optional council elements (v2.2.0) verified live:** critic seat (solo-alongside-fanout;
86
100
  `role: "critic"` passes through `council tally` untouched), debate mode's nothing-to-debate path
87
101
  (provisional `--no-ledger` tally → skip rebuttals → final ledger-recorded tally), and the chair
88
- verdict scale (parseable `VERDICT:` line + hard questions) all behaved per SEAT-BRIEFS. **Debate
89
- mode's defense/re-vote waves remain unexercised** an all-Confirmed consensus run has no
90
- rebuttal surface; exercise them on a contentious artifact before trusting that path blind.
102
+ verdict scale (parseable `VERDICT:` line + hard questions) all behaved per SEAT-BRIEFS.
103
+ - **Debate's defense/re-vote waves have now been exercised** (an ideation council, where severity
104
+ means impact rather than correctness): they parse and tally cleanly, and the round behaves as a
105
+ **rescope** mechanism rather than a defense — raisers overwhelmingly AMEND (downgrade an
106
+ overstated severity, narrow a scope) rather than DEFEND, and amendments are re-confirmed on the
107
+ re-vote. Expect high-amend / low-defend; the idea usually survives, the severity claim often does
108
+ not.
91
109
 
92
110
  ## Per-model notes
93
111
 
@@ -136,6 +154,26 @@ claim-class dedup adjudication limit; minimax and qwen-coder debut notes; see ch
136
154
  ### Claude (in-council, when toggle on)
137
155
  - Consistently the most *calibrated* reviewer (no severity inflation; findings overwhelmingly Confirmed; bench-best street-cred in recent runs) but sometimes the least *original* — it can miss the boldest single catch. Treat as a reliability floor, not a discovery engine.
138
156
 
157
+ ### haiku (`--model haiku`) — **verify before using; it has been hard-404ing**
158
+ - The direct-Anthropic route (`anthropic/claude-haiku-4-5-20251001`) returned a hard `Not Found`
159
+ on **every** invocation of a recent paid corpus — 3 of 3 legs across two separate runs, both as
160
+ **chair** (twice, incl. the fallback retry) and as a **bench seat** — in ~2 s with zero tokens.
161
+ - Both runs degraded *around* it silently rather than failing: one fell back to another chair, the
162
+ other collapsed its bench from 3 seats to 2 and exited 2. **A dead alias does not stop a council;
163
+ it shrinks it.** In the bench-seat case every finding came out `confidence: "thin"` with a single
164
+ peer corroborator, purely because the bench had halved — and nothing in `verdict.json` said so.
165
+ - Resolve the alias against the catalog (`amicus models --check`) before putting it on a paid
166
+ bench, and re-check the bench roster in `run.json` against what you asked for afterwards.
167
+
168
+ ### GLM (`--model glm` → z-ai via OpenRouter)
169
+ - Cheap and fast, and ranked best-by-peers on a clean debut — but a later run produced **35 KB of
170
+ prose with `conformance: unstructured` and 0 parsed findings**, twice. Its structured-output
171
+ reliability is **not** established; treat the debut as low-N.
172
+ - Useful behavioural note: when its structured output failed it **refused to fabricate** on the
173
+ repair attempts. Honest — but an honest refusal still costs you the seat, so a bench that leans
174
+ on `glm` for quorum can silently adjudicate a seat short while still paying for its tokens.
175
+ Watch `conformance` per seat, not just the finding count.
176
+
139
177
  ### minimax (`--model minimax` → via OpenRouter)
140
178
  - Fast (~2 min review legs), cheap, `clean` findings-JSON conformance on debut.
141
179
  - Took the **critic seat** brief exceptionally well: unanimously ranked #1 by its bench, full
@@ -209,6 +247,16 @@ This section keeps only per-model **qualitative quirks** and **structural-confor
209
247
  GUI-hangs-on-this-machine rule (resolved 2026-06-10; headless stays the council default by
210
248
  design). Config path updated to `~/.config/amicus/.env`.
211
249
  - **2026-07-02** — Folded back field lessons from runs 4-7 (AV-receiver, pork-shoulder, resume, novel ×2 councils): PowerShell `--models` quoting; current-date injection; long-read model selection; judge no-tools preamble; severity-inflation-justifies-dispute; five-keys tally schema; new Grok/Kimi/Mistral/Claude-in-council sections. Quantitative history stays in the ledger (`amicus council stats`).
250
+ - **2026-07-26 (v4.4.0)** — Fold-back from six paid councils (workspace/renderer review ×4, a
251
+ frontier cost-pipeline council, and an ideation council). Operating lessons: alias-resolution
252
+ hygiene before spending (a non-existent alias aborts the run; a local `config.json` override can
253
+ silently upgrade a "cheap" seat to a Pro tier and trip `--max-cost`); the chair may not also hold
254
+ a bench seat; `council run` prints nothing until terminal, so pin `--run-id` when backgrounding.
255
+ Debate's defense/re-vote waves exercised for the first time — they work, and behave as a rescope
256
+ (amend-heavy) rather than a defense. New per-model notes: **haiku** hard-404ed 3/3 legs across two
257
+ runs and both councils silently degraded around it; **glm** returned `unstructured` conformance
258
+ with 0 findings twice after a clean debut, and honestly refused to fabricate on repair — which
259
+ still costs the seat.
212
260
  - **2026-07-14 (v2.2.0)** — Optional council elements shipped and verified on a planted-flaw
213
261
  ground-truth council (critic seat, debate mode nothing-to-debate path, chair verdict scale;
214
262
  expert lenses defined but not yet field-run). New lessons: claim-class dedup glosses
@@ -20,7 +20,30 @@ function parseList(value) {
20
20
  return String(value).split(',').map(s => s.trim()).filter(Boolean);
21
21
  }
22
22
 
23
- /** Resolve bench models from --models XOR --council (mirrors handleFanout). */
23
+ /**
24
+ * Sanitize the internal `--council-name` passthrough before it can reach the
25
+ * spend ledger's `councilName` column (v4.3 Task 4 review fix, spec §7.3:
26
+ * spend docs hold only ids/numbers/paths "by construction"). That value is
27
+ * user-supplied (via mcp-council-run.js, ultimately an MCP caller's `input`),
28
+ * unbounded, and unvalidated — unlike a real `--council <preset>`, which is
29
+ * catalog-validated upstream. Strips control/non-printable characters, trims,
30
+ * and caps length so a hostile/malformed passthrough can't land raw in a
31
+ * `--group-by council` rollup. Precedence is untouched by this: it's applied
32
+ * only to the passthrough branch, never to the catalog-validated preset name.
33
+ * @param {string} name @returns {string|null} sanitized name, or null if empty after cleanup
34
+ */
35
+ function sanitizeCouncilName(name) {
36
+ // eslint-disable-next-line no-control-regex -- deliberately stripping C0/DEL control chars
37
+ const cleaned = String(name).replace(/[\x00-\x1F\x7F]/g, '').trim().slice(0, 64);
38
+ return cleaned || null;
39
+ }
40
+
41
+ /**
42
+ * Resolve bench models from --models XOR --council (mirrors handleFanout).
43
+ * Also returns `presetName` (v4.3 Task 3, spec §7.1): the trimmed --council
44
+ * name when that branch was taken, else null — threaded into runCouncil's
45
+ * `councilName` option so council ledger rows can be attributed to a preset.
46
+ */
24
47
  function resolveBench(args, useJson) {
25
48
  const hasModels = typeof args.models === 'string' && args.models.trim();
26
49
  const hasCouncil = args.council !== undefined && args.council !== false;
@@ -40,16 +63,17 @@ function resolveBench(args, useJson) {
40
63
  const { resolveCouncilMembers } = require('./utils/config');
41
64
  const { readCache } = require('./utils/model-catalog');
42
65
  const catalog = (readCache() || {}).models || [];
43
- const expanded = resolveCouncilMembers(args.council.trim(), catalog);
66
+ const presetName = args.council.trim();
67
+ const expanded = resolveCouncilMembers(presetName, catalog);
44
68
  if (expanded.error) {
45
69
  return { fail: failJson(useJson, { code: ERROR_CODES.BAD_ARGS, message: `Error: ${expanded.error}` }) };
46
70
  }
47
71
  if (expanded.dropped && expanded.dropped.length && !useJson) {
48
72
  process.stderr.write(`Notice: dropped unavailable council member(s): ${expanded.dropped.join(', ')}\n`);
49
73
  }
50
- return { bench: expanded.models };
74
+ return { bench: expanded.models, presetName };
51
75
  }
52
- return { bench: parseList(args.models) };
76
+ return { bench: parseList(args.models), presetName: null };
53
77
  }
54
78
 
55
79
  function renderRunHuman(run) {
@@ -58,8 +82,30 @@ function renderRunHuman(run) {
58
82
  ` bench: ${(run.bench || []).join(', ')} chair: ${run.chair}`,
59
83
  ` dir: ${run.options && run.options.outDir}`,
60
84
  ];
61
- if (run.usage && run.usage.cost && typeof run.usage.cost.amount === 'number') {
62
- lines.push(` cost: $${run.usage.cost.amount.toFixed(4)} (${run.usage.cost.source})`);
85
+ // v4.4: a cost line that omits unpriced legs reads as the whole bill. The
86
+ // diagnosis measured council-wsgate02 printing $0.3720 for a run that really
87
+ // spent $0.9859. Say what we know, then say what we cannot know — and print
88
+ // the line even when NOTHING resolved (the old `typeof amount === 'number'`
89
+ // guard silently dropped it, so a fully unpriced run looked free).
90
+ const u = run.usage || null;
91
+ const unknownLegs = u && typeof u.unknownLegs === 'number'
92
+ ? u.unknownLegs
93
+ : (u && u.cost && u.cost.unpricedLegs) || 0;
94
+ // v4.4 Task 2: a fully-priced run can still be short. `council-wsgate01`
95
+ // printed an unqualified $0.2821 for a run that really spent $0.3036 — every
96
+ // leg `reported`, and 100% of the gap one unattributed `explore` child session.
97
+ const subtreeLegs = u && typeof u.subtreeUnknownLegs === 'number'
98
+ ? u.subtreeUnknownLegs
99
+ : (u && u.cost && u.cost.subtreeUnknownLegs) || 0;
100
+ if (u && u.cost && (typeof u.cost.amount === 'number' || unknownLegs > 0 || subtreeLegs > 0)) {
101
+ const known = typeof u.cost.amount === 'number' ? `$${u.cost.amount.toFixed(4)}` : '$0.0000';
102
+ const gaps = [];
103
+ if (unknownLegs > 0) { gaps.push(`${unknownLegs} leg(s) unknown`); }
104
+ if (subtreeLegs > 0) { gaps.push(`${subtreeLegs} leg(s) with unattributed subagent child-session spend`); }
105
+ const tail = gaps.length > 0
106
+ ? ` + ${gaps.join(' + ')} — real spend is at least this much`
107
+ : '';
108
+ lines.push(` cost: ${known} (${u.cost.source})${tail}`);
63
109
  }
64
110
  if (run.error) { lines.push(` error: ${run.error.code}: ${run.error.message}`); }
65
111
  return lines.join('\n') + '\n';
@@ -85,6 +131,19 @@ async function handleCouncilRun(args) {
85
131
  const benchRes = resolveBench(args, useJson);
86
132
  if (benchRes.fail !== undefined) { return benchRes.fail; }
87
133
  const bench = benchRes.bench;
134
+ // v4.3 Task 3 (spec §7.1): the preset name, when this run came from a real
135
+ // --council <preset>. `--council-name` is an internal, undocumented passthrough
136
+ // set by mcp-council-run.js — the MCP handler always expands a preset to
137
+ // `--models` before spawning (so `--council`/`--models` stay mutually exclusive
138
+ // on this CLI surface), which would otherwise strand the preset name with no
139
+ // way to reach this process. Never fabricated: --models with neither flag
140
+ // stays null, matching spec §7.1 ("preset name … else null"). The
141
+ // passthrough branch is sanitized (see sanitizeCouncilName docblock) — the
142
+ // preset-name branch is catalog-validated upstream and never touched here,
143
+ // so precedence (a real --council preset always outranks the passthrough)
144
+ // is unchanged.
145
+ const councilName = benchRes.presetName
146
+ || (typeof args['council-name'] === 'string' ? sanitizeCouncilName(args['council-name']) : null);
88
147
  if (bench.length < 2) {
89
148
  return failJson(useJson, { code: ERROR_CODES.BAD_ARGS,
90
149
  message: 'Error: a council needs at least 2 seats (fanout semantics)' });
@@ -139,8 +198,11 @@ async function handleCouncilRun(args) {
139
198
  ? path.resolve(project, String(args['out-dir']))
140
199
  : path.resolve(project, `council-${runId}`);
141
200
 
142
- const { resolveGatewayMode } = require('./utils/config');
201
+ const { resolveGatewayMode, loadConfig } = require('./utils/config');
202
+ const { resolveFallbackConfig } = require('./sidecar/fallback-chains');
203
+ const { readCache } = require('./utils/model-catalog');
143
204
  const { runCouncil } = require('./council/run');
205
+ const cfg = loadConfig() || {};
144
206
  const { exitCode, run } = await runCouncil({
145
207
  briefing: promptRes.prompt, models: bench, chair, critic, lenses,
146
208
  project, runId, runDir,
@@ -148,6 +210,7 @@ async function handleCouncilRun(args) {
148
210
  gateway: resolveGatewayMode(args.gateway),
149
211
  noValidateModel: !!args['no-validate-model'],
150
212
  date: new Date().toISOString().slice(0, 10),
213
+ councilName,
151
214
  // v4.1 §4.5b/§4.5d. `--claude-review` is resolved here but VALIDATED by the
152
215
  // engine's preflightClaudeReview (run-assemble.js): the reserved-seat and
153
216
  // 'claude may not chair' guards live there on purpose so MCP, the GitHub
@@ -157,6 +220,21 @@ async function handleCouncilRun(args) {
157
220
  debate: !!args.debate,
158
221
  claudeReviewFile: args['claude-review'] ? path.resolve(args['claude-review']) : null,
159
222
  noCostGate: !!args['no-cost-gate'],
223
+ // v4.3 Task 13: --follow's json-vs-human mode mirrors the same --json this
224
+ // handler already resolved for the final run doc, so `--json --follow`
225
+ // NDJSON on stderr and the `--json` final doc on stdout agree.
226
+ follow: !!args.follow,
227
+ json: useJson,
228
+ onComplete: args['on-complete'],
229
+ // v4.3 Task 18 (spec §6.2): opt-in cheaper-model substitution for STAGE
230
+ // legs only (run-stages.js threads it through; the chair is excluded —
231
+ // see run-chair.js). --fallback forces on, --no-fallback forces off;
232
+ // unset defers to config `fallbacks.enabled`.
233
+ fallback: resolveFallbackConfig({
234
+ flagFallback: args.fallback === true ? true : (args['no-fallback'] ? false : undefined),
235
+ config: cfg,
236
+ }),
237
+ catalog: (readCache() || {}).models || [],
160
238
  });
161
239
 
162
240
  if (useJson) {
@@ -174,4 +252,4 @@ async function handleCouncilRun(args) {
174
252
  return exitCode;
175
253
  }
176
254
 
177
- module.exports = { handleCouncilRun, CHAIR_DEFAULT };
255
+ module.exports = { handleCouncilRun, renderRunHuman, CHAIR_DEFAULT };