amicus 4.3.0 → 4.4.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.
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +64 -0
- package/README.md +6 -3
- package/docs/DISTRIBUTION.md +234 -0
- package/docs/ROADMAP.md +200 -0
- package/docs/SHIMS.md +62 -0
- package/docs/architecture.md +104 -0
- package/docs/configuration.md +371 -0
- package/docs/council.md +911 -0
- package/docs/doc-system.md +92 -0
- package/docs/electron-testing.md +471 -0
- package/docs/jsdoc-setup.md +75 -0
- package/docs/opencode-integration.md +114 -0
- package/docs/publishing.md +60 -0
- package/docs/schemas.md +55 -0
- package/docs/testing.md +589 -0
- package/docs/troubleshooting.md +298 -0
- package/docs/usage.md +699 -0
- package/electron/fold.js +1 -1
- package/electron/ipc-workspace.js +283 -0
- package/electron/main.js +31 -1
- package/electron/preload-workspace.js +40 -0
- package/electron/setup-ui-aliases.js +6 -6
- package/electron/workspace-shell.js +85 -0
- package/electron/workspace-ui/index.html +111 -0
- package/electron/workspace-ui/live-model.js +112 -0
- package/electron/workspace-ui/md-lite.js +163 -0
- package/electron/workspace-ui/workspace-app.js +240 -0
- package/electron/workspace-ui/workspace-matrix.js +249 -0
- package/electron/workspace-ui/workspace-panels.js +237 -0
- package/electron/workspace-ui/workspace-render.js +277 -0
- package/electron/workspace-ui/workspace-verbs.js +293 -0
- package/electron/workspace-ui/workspace.css +172 -0
- package/package.json +8 -3
- package/schemas/council-run-live.schema.json +25 -1
- package/schemas/council-run.schema.json +34 -0
- package/schemas/progress.schema.json +26 -1
- package/schemas/spend.schema.json +52 -4
- package/skills/second-opinion/MODEL-NOTES.md +53 -5
- package/src/cli-handlers-council-run.js +25 -3
- package/src/cli-handlers-spend.js +50 -5
- package/src/cli-handlers-watch.js +48 -10
- package/src/cli.js +4 -2
- package/src/council/briefings-debate.js +27 -7
- package/src/council/briefings-stage2.js +155 -25
- package/src/council/briefings.js +59 -3
- package/src/council/findings.js +236 -9
- package/src/council/parse-stage2.js +10 -2
- package/src/council/report.js +19 -8
- package/src/council/run-assemble.js +42 -1
- package/src/council/run-budget.js +277 -0
- package/src/council/run-chair.js +4 -1
- package/src/council/run-debate.js +4 -2
- package/src/council/run-finalize.js +102 -0
- package/src/council/run-launch.js +73 -7
- package/src/council/run-server.js +248 -0
- package/src/council/run-stage2.js +118 -0
- package/src/council/run-stages.js +148 -113
- package/src/council/run-state.js +23 -1
- package/src/council/run.js +52 -53
- package/src/council/tally.js +10 -0
- package/src/headless.js +519 -17
- package/src/mcp-council-awareness.js +53 -3
- package/src/observe/council-legs.js +240 -0
- package/src/observe/live-doc.js +39 -4
- package/src/observe/watch-render.js +23 -1
- package/src/opencode-client.js +15 -3
- package/src/sidecar/child-sessions.js +197 -0
- package/src/sidecar/conversation-mirror.js +111 -37
- package/src/sidecar/fanout-budget.js +71 -0
- package/src/sidecar/fanout-leg-fallback.js +69 -21
- package/src/sidecar/fanout-leg.js +29 -1
- package/src/sidecar/fanout-signals.js +61 -0
- package/src/sidecar/fanout-wave-io.js +75 -0
- package/src/sidecar/fanout.js +65 -81
- package/src/sidecar/progress-fields.js +26 -4
- package/src/sidecar/progress.js +8 -1
- package/src/sidecar/session-utils.js +23 -14
- package/src/sidecar/tool-part.js +196 -0
- package/src/sidecar/workspace-window.js +62 -0
- package/src/spend-query.js +33 -6
- package/src/utils/env-num.js +42 -0
- package/src/utils/lifecycle.js +37 -1
- package/src/utils/path-fence.js +120 -0
- package/src/utils/pricing.js +114 -9
- package/src/utils/server-setup.js +79 -1
- package/src/utils/spend-ledger.js +24 -3
- package/src/workspace/artifact-guard.js +208 -0
- package/src/workspace/blind-mode.js +32 -0
- package/src/workspace/fold-format.js +124 -0
- package/src/workspace/live-normalize.js +169 -0
- package/src/workspace/matrix-model.js +94 -0
- package/src/workspace/run-detail.js +229 -0
- 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.
|
|
3
|
+
"version": "4.4.1",
|
|
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": [
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
"commands/",
|
|
44
44
|
".claude-plugin/",
|
|
45
45
|
"CHANGELOG.md",
|
|
46
|
+
"docs/*.md",
|
|
46
47
|
"scripts/postinstall.js",
|
|
47
48
|
"scripts/setup-hooks.js"
|
|
48
49
|
],
|
|
@@ -50,11 +51,11 @@
|
|
|
50
51
|
"start": "node --experimental-top-level-await --experimental-vm-modules bin/amicus.js",
|
|
51
52
|
"test": "jest",
|
|
52
53
|
"test:integration": "node scripts/run-integration-keyless.js",
|
|
53
|
-
"test:integration:live": "jest --testPathIgnorePatterns='worktrees' --testMatch='**/tests/**/*.integration.test.js'",
|
|
54
|
+
"test:integration:live": "jest --runInBand --testPathIgnorePatterns='worktrees' --testMatch='**/tests/**/*.integration.test.js'",
|
|
54
55
|
"test:all": "jest --testPathIgnorePatterns='/node_modules/' --testPathIgnorePatterns='worktrees' && node scripts/mark-test-passed.js",
|
|
55
56
|
"test:e2e:mcp": "jest tests/mcp-repomix-e2e.integration.test.js --testTimeout=180000 --forceExit",
|
|
56
57
|
"posttest": "node scripts/mark-test-passed.js",
|
|
57
|
-
"lint": "eslint src/",
|
|
58
|
+
"lint": "eslint src/ electron/",
|
|
58
59
|
"postinstall": "node scripts/postinstall.js",
|
|
59
60
|
"test:thinking": "node scripts/benchmark-thinking.js",
|
|
60
61
|
"test:thinking:quick": "MODELS=gemini node scripts/benchmark-thinking.js",
|
|
@@ -88,6 +89,7 @@
|
|
|
88
89
|
"ajv": "^8.20.0",
|
|
89
90
|
"chrome-remote-interface": "^0.33.3",
|
|
90
91
|
"eslint": "^8.0.0",
|
|
92
|
+
"espree": "^9.6.1",
|
|
91
93
|
"jest": "^29.0.0",
|
|
92
94
|
"lint-staged": "^16.3.2",
|
|
93
95
|
"puppeteer": "^24.36.0",
|
|
@@ -100,6 +102,9 @@
|
|
|
100
102
|
"lint-staged": {
|
|
101
103
|
"src/**/*.js": [
|
|
102
104
|
"eslint --fix"
|
|
105
|
+
],
|
|
106
|
+
"electron/**/*.js": [
|
|
107
|
+
"eslint --fix"
|
|
103
108
|
]
|
|
104
109
|
}
|
|
105
110
|
}
|
|
@@ -27,7 +27,31 @@
|
|
|
27
27
|
"legsComplete": { "type": ["number", "null"] },
|
|
28
28
|
"elapsed": { "type": "string" },
|
|
29
29
|
"exitCode": { "type": ["number", "null"] },
|
|
30
|
-
"usage": { "type": "object" }
|
|
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" }
|
|
31
55
|
},
|
|
32
56
|
"additionalProperties": true
|
|
33
57
|
}
|
|
@@ -34,6 +34,40 @@
|
|
|
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
|
+
},
|
|
51
|
+
"sharedServer": {
|
|
52
|
+
"description": "The run acquired its single shared OpenCode server and threaded it through every wave (v4.4.1 Task 0.5). Present only on a successful acquisition; mutually exclusive with sharedServerUnavailable. `goPid` is the shared server's process id — no wave writes a goPid into its own metadata.json while riding an injected server, so this field is the POSITIVE, direct evidence that one server served the run, replacing the goPid-absence inference.",
|
|
53
|
+
"type": "object",
|
|
54
|
+
"required": ["acquired"],
|
|
55
|
+
"properties": {
|
|
56
|
+
"acquired": { "const": true },
|
|
57
|
+
"at": { "type": "string" },
|
|
58
|
+
"goPid": { "type": ["number", "null"] },
|
|
59
|
+
"models": { "type": "number" }
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
"sharedServerUnavailable": {
|
|
63
|
+
"description": "The run could not start its single shared OpenCode server and fell back to one server per wave (v4.4.1 Task 0.5). Present only when the acquisition failed. The run CONTINUES — this is never fatal — but the per-wave configuration is the one that races on OpenCode's SQLite, so results may be degraded.",
|
|
64
|
+
"type": "object",
|
|
65
|
+
"required": ["error"],
|
|
66
|
+
"properties": {
|
|
67
|
+
"error": { "type": "string" },
|
|
68
|
+
"at": { "type": "string" }
|
|
69
|
+
}
|
|
70
|
+
},
|
|
37
71
|
"debate": {
|
|
38
72
|
"type": "object",
|
|
39
73
|
"properties": {
|
|
@@ -12,11 +12,36 @@
|
|
|
12
12
|
"updatedAt": { "type": "string", "format": "date-time" },
|
|
13
13
|
"messagesReceived": { "type": "number" },
|
|
14
14
|
"latestTool": { "type": "string" },
|
|
15
|
+
"toolSettleTimedOut": {
|
|
16
|
+
"description": "v4.4 B4. The leg was completed while at least one tool call was still non-terminal, after AMICUS_TOOL_SETTLE_GRACE_MS elapsed. Its partial output was kept and it was NOT failed, but its reported cost is a floor. Present only when true.",
|
|
17
|
+
"const": true
|
|
18
|
+
},
|
|
19
|
+
"unsettledToolCalls": {
|
|
20
|
+
"description": "How many tool calls were still live when the settle grace was exceeded. A count here (progress.json is a compact snapshot); the full list rides the runHeadless result. Present only alongside toolSettleTimedOut.",
|
|
21
|
+
"type": "number"
|
|
22
|
+
},
|
|
23
|
+
"toolSettleAborted": {
|
|
24
|
+
"description": "v4.4.1 LC-2. Whether the leg's OpenCode session was successfully aborted at the settle ceiling so it would stop billing. `false` means the abort was attempted and failed — the session may still be billing. Present only alongside toolSettleTimedOut.",
|
|
25
|
+
"type": "boolean"
|
|
26
|
+
},
|
|
15
27
|
"usage": {
|
|
16
28
|
"type": "object",
|
|
17
29
|
"properties": {
|
|
18
30
|
"tokens": { "type": "object" },
|
|
19
|
-
"costReported": { "type": "number" }
|
|
31
|
+
"costReported": { "type": "number" },
|
|
32
|
+
"subtree": {
|
|
33
|
+
"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.",
|
|
34
|
+
"type": "object",
|
|
35
|
+
"properties": {
|
|
36
|
+
"sessions": { "type": "number" },
|
|
37
|
+
"tokens": { "type": "object" },
|
|
38
|
+
"costReported": { "type": "number" }
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"subtreeUnknown": {
|
|
42
|
+
"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.",
|
|
43
|
+
"const": true
|
|
44
|
+
}
|
|
20
45
|
}
|
|
21
46
|
}
|
|
22
47
|
},
|
|
@@ -9,8 +9,28 @@
|
|
|
9
9
|
"schemaVersion": { "const": 2 },
|
|
10
10
|
"type": { "const": "spend" },
|
|
11
11
|
"windowDays": { "type": ["number", "null"] },
|
|
12
|
-
"total": {
|
|
13
|
-
|
|
12
|
+
"total": {
|
|
13
|
+
"type": "object",
|
|
14
|
+
"properties": {
|
|
15
|
+
"amount": { "type": "number" },
|
|
16
|
+
"runs": { "type": "number" },
|
|
17
|
+
"unpricedRows": { "$ref": "#/$defs/unpricedRows" },
|
|
18
|
+
"unattributedSubtreeRows": { "$ref": "#/$defs/unattributedSubtreeRows" }
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"byModel": {
|
|
22
|
+
"type": "array",
|
|
23
|
+
"items": {
|
|
24
|
+
"type": "object",
|
|
25
|
+
"properties": {
|
|
26
|
+
"model": { "type": "string" },
|
|
27
|
+
"amount": { "type": "number" },
|
|
28
|
+
"runs": { "type": "number" },
|
|
29
|
+
"unpricedRows": { "$ref": "#/$defs/unpricedRows" },
|
|
30
|
+
"unattributedSubtreeRows": { "$ref": "#/$defs/unattributedSubtreeRows" }
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
},
|
|
14
34
|
"credit": { "type": ["object", "null"] },
|
|
15
35
|
"filters": { "type": "object" },
|
|
16
36
|
"groupBy": { "enum": ["model", "wave", "council", "project", "op", "day"] },
|
|
@@ -22,7 +42,9 @@
|
|
|
22
42
|
"properties": {
|
|
23
43
|
"key": { "type": "string" },
|
|
24
44
|
"amount": { "type": "number" },
|
|
25
|
-
"runs": { "type": "number" }
|
|
45
|
+
"runs": { "type": "number" },
|
|
46
|
+
"unpricedRows": { "$ref": "#/$defs/unpricedRows" },
|
|
47
|
+
"unattributedSubtreeRows": { "$ref": "#/$defs/unattributedSubtreeRows" }
|
|
26
48
|
}
|
|
27
49
|
}
|
|
28
50
|
},
|
|
@@ -32,10 +54,36 @@
|
|
|
32
54
|
"properties": {
|
|
33
55
|
"amount": { "type": "number" },
|
|
34
56
|
"runs": { "type": "number" },
|
|
57
|
+
"unpricedRows": { "$ref": "#/$defs/unpricedRows" },
|
|
58
|
+
"unattributedSubtreeRows": { "$ref": "#/$defs/unattributedSubtreeRows" },
|
|
35
59
|
"byStatus": { "type": "object" }
|
|
36
60
|
}
|
|
37
61
|
},
|
|
38
|
-
"rows": {
|
|
62
|
+
"rows": {
|
|
63
|
+
"type": "array",
|
|
64
|
+
"items": {
|
|
65
|
+
"type": "object",
|
|
66
|
+
"description": "A raw spend-ledger.jsonl row (internal v1 shape, echoed verbatim when --rows is passed). Always an object: readSpendRows drops any ledger line that does not parse as one, scalars and arrays included. Only the fields this document makes claims about are pinned beyond that.",
|
|
67
|
+
"properties": {
|
|
68
|
+
"subtreeUnknown": {
|
|
69
|
+
"type": "boolean",
|
|
70
|
+
"description": "v4.4.1. Present (and always true) only when this leg's own cost resolved but it spawned a child session whose spend could not be determined — the row is PRICED yet its cost is a floor. OMITTED entirely otherwise, so an ordinary row is byte-identical to a pre-4.4.1 one."
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
},
|
|
39
75
|
"rowsTruncated": { "type": "boolean" }
|
|
76
|
+
},
|
|
77
|
+
"$defs": {
|
|
78
|
+
"unpricedRows": {
|
|
79
|
+
"type": "integer",
|
|
80
|
+
"minimum": 0,
|
|
81
|
+
"description": "Rows that contributed NOTHING to `amount` (no numeric cost). The bucket's figure omits them entirely: real spend is at least this much."
|
|
82
|
+
},
|
|
83
|
+
"unattributedSubtreeRows": {
|
|
84
|
+
"type": "integer",
|
|
85
|
+
"minimum": 0,
|
|
86
|
+
"description": "v4.4.1 CA-2. Rows that ARE priced and DO contribute to `amount`, but whose leg spawned a subagent whose child-session spend could not be determined — the second, independent reason the figure is a floor. A row can be counted in both this and `unpricedRows`; the two are never substitutes."
|
|
87
|
+
}
|
|
40
88
|
}
|
|
41
89
|
}
|
|
@@ -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-
|
|
8
|
-
|
|
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.
|
|
89
|
-
|
|
90
|
-
|
|
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
|
|
@@ -82,8 +82,30 @@ function renderRunHuman(run) {
|
|
|
82
82
|
` bench: ${(run.bench || []).join(', ')} chair: ${run.chair}`,
|
|
83
83
|
` dir: ${run.options && run.options.outDir}`,
|
|
84
84
|
];
|
|
85
|
-
|
|
86
|
-
|
|
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}`);
|
|
87
109
|
}
|
|
88
110
|
if (run.error) { lines.push(` error: ${run.error.code}: ${run.error.message}`); }
|
|
89
111
|
return lines.join('\n') + '\n';
|
|
@@ -230,4 +252,4 @@ async function handleCouncilRun(args) {
|
|
|
230
252
|
return exitCode;
|
|
231
253
|
}
|
|
232
254
|
|
|
233
|
-
module.exports = { handleCouncilRun, CHAIR_DEFAULT };
|
|
255
|
+
module.exports = { handleCouncilRun, renderRunHuman, CHAIR_DEFAULT };
|
|
@@ -43,22 +43,40 @@ function parseSinceDays(since) {
|
|
|
43
43
|
* A row with a null cost.amount contributes 0 to totals but is still counted
|
|
44
44
|
* in `runs` and its source bucket — visibility into "how many runs are
|
|
45
45
|
* unpriced" matters as much as the dollar figure.
|
|
46
|
+
*
|
|
47
|
+
* v4.4: `unpricedRows` is the count of rows that contributed NOTHING to
|
|
48
|
+
* `amount` (no numeric cost). `sourceMix.unknown` is adjacent but not the same
|
|
49
|
+
* question — it buckets by the row's declared source, whereas this counts what
|
|
50
|
+
* the arithmetic actually saw. It exists so the renderers can say "$X plus N
|
|
51
|
+
* unknown" instead of coercing null→0 and printing a measured-looking $0.0000
|
|
52
|
+
* (diagnosis §8, final paragraph).
|
|
53
|
+
*
|
|
54
|
+
* v4.4.1 CA-2: `unattributedSubtreeRows` is its sibling for the OTHER way the
|
|
55
|
+
* figure understates. Such a row is fully priced — it is in `amount`, it is in
|
|
56
|
+
* the `r` bucket, `unpricedRows` never sees it — but the leg spawned a child
|
|
57
|
+
* session whose spend the walk could not determine, which is exactly what makes
|
|
58
|
+
* `council run` report `costExact: false` about the same dollars. Without this
|
|
59
|
+
* counter `amicus spend` disagreed with `council run` and looked complete.
|
|
46
60
|
* @param {Array<object>} rows
|
|
47
61
|
*/
|
|
48
62
|
function aggregateSpend(rows) {
|
|
49
|
-
const total = { amount: 0, tokens: emptyTokens(), runs: rows.length, sourceMix: { reported: 0, estimated: 0, unknown: 0 } };
|
|
63
|
+
const total = { amount: 0, tokens: emptyTokens(), runs: rows.length, unpricedRows: 0, unattributedSubtreeRows: 0, sourceMix: { reported: 0, estimated: 0, unknown: 0 } };
|
|
50
64
|
const byModelMap = new Map();
|
|
51
65
|
for (const r of rows) {
|
|
52
66
|
const model = r.model || 'unknown';
|
|
53
67
|
if (!byModelMap.has(model)) {
|
|
54
|
-
byModelMap.set(model, { model, amount: 0, tokens: emptyTokens(), runs: 0, sourceMix: { reported: 0, estimated: 0, unknown: 0 } });
|
|
68
|
+
byModelMap.set(model, { model, amount: 0, tokens: emptyTokens(), runs: 0, unpricedRows: 0, unattributedSubtreeRows: 0, sourceMix: { reported: 0, estimated: 0, unknown: 0 } });
|
|
55
69
|
}
|
|
56
70
|
const bucket = byModelMap.get(model);
|
|
57
71
|
bucket.runs += 1;
|
|
58
72
|
addTokens(bucket.tokens, r.tokens);
|
|
59
73
|
addTokens(total.tokens, r.tokens);
|
|
60
74
|
const cost = r.cost || {};
|
|
61
|
-
const
|
|
75
|
+
const priced = typeof cost.amount === 'number';
|
|
76
|
+
const amount = priced ? cost.amount : 0;
|
|
77
|
+
if (!priced) { bucket.unpricedRows += 1; total.unpricedRows += 1; }
|
|
78
|
+
// Beside, never instead of: a row can be both unpriced and subtree-unknown.
|
|
79
|
+
if (r.subtreeUnknown) { bucket.unattributedSubtreeRows += 1; total.unattributedSubtreeRows += 1; }
|
|
62
80
|
bucket.amount += amount;
|
|
63
81
|
total.amount += amount;
|
|
64
82
|
// Any source string outside {reported,estimated} buckets as unknown —
|
|
@@ -139,6 +157,18 @@ async function fetchCreditFooter(deps) {
|
|
|
139
157
|
return res || null;
|
|
140
158
|
}
|
|
141
159
|
|
|
160
|
+
/**
|
|
161
|
+
* Cost cell for one rollup bucket. v4.4: a bucket in which NO row carried a
|
|
162
|
+
* numeric amount has no dollar figure to show — it renders formatCost's `?`
|
|
163
|
+
* rather than `$0.0000`, which previously read as a measured zero and was the
|
|
164
|
+
* only thing contradicting the (correct but easily missed) `u` count.
|
|
165
|
+
*/
|
|
166
|
+
function bucketCost(bucket) {
|
|
167
|
+
const unpriced = bucket.unpricedRows || 0;
|
|
168
|
+
const hasKnown = (bucket.runs || 0) > unpriced;
|
|
169
|
+
return formatCost({ amount: hasKnown ? bucket.amount : null, source: dominantSource(bucket.sourceMix) });
|
|
170
|
+
}
|
|
171
|
+
|
|
142
172
|
function renderHuman({ total, byModel, windowDays, credit, wasted }) {
|
|
143
173
|
if (total.runs === 0) { return 'No spend recorded yet.\n'; }
|
|
144
174
|
let out = windowDays ? `amicus spend (last ${windowDays}d)\n\n` : 'amicus spend (all time)\n\n';
|
|
@@ -148,9 +178,24 @@ function renderHuman({ total, byModel, windowDays, credit, wasted }) {
|
|
|
148
178
|
const tokCol = `${m.tokens.input}/${m.tokens.output}`;
|
|
149
179
|
out += `${String(m.model).slice(0, 48).padEnd(48)} ${String(m.runs).padStart(4)} ` +
|
|
150
180
|
`${tokCol.padStart(15)} ` +
|
|
151
|
-
`${
|
|
181
|
+
`${bucketCost(m).padStart(9)} ${mix}\n`;
|
|
182
|
+
}
|
|
183
|
+
out += `\nTotal: ${bucketCost(total)} across ${total.runs} run(s)\n`;
|
|
184
|
+
// v4.4: the total is a FLOOR whenever any row is unpriced. Stated on its own
|
|
185
|
+
// line rather than squeezed into the fixed-width cost column.
|
|
186
|
+
if (total.unpricedRows > 0) {
|
|
187
|
+
out += `${total.unpricedRows} unpriced row(s) — cost unknown and NOT in the total; `
|
|
188
|
+
+ 'real spend is at least this much.\n';
|
|
189
|
+
}
|
|
190
|
+
// v4.4.1 CA-2: a DIFFERENT statement from the line above, and deliberately
|
|
191
|
+
// worded as one. "We could not see this leg at all" (unpriced) vs "we saw this
|
|
192
|
+
// leg, and its own cost IS in the total, but not what it spawned" — the second
|
|
193
|
+
// is why `council run` reports costExact:false on a row that looks complete
|
|
194
|
+
// here. Both lines can appear at once; a row can be in both counts.
|
|
195
|
+
if (total.unattributedSubtreeRows > 0) {
|
|
196
|
+
out += `${total.unattributedSubtreeRows} row(s) spawned a subagent whose CHILD session `
|
|
197
|
+
+ 'spend could NOT be determined — real spend is HIGHER than this total.\n';
|
|
152
198
|
}
|
|
153
|
-
out += `\nTotal: ${formatCost({ amount: total.amount, source: dominantSource(total.sourceMix) })} across ${total.runs} run(s)\n`;
|
|
154
199
|
if (wasted && wasted.runs > 0) {
|
|
155
200
|
out += `Wasted (failed runs): ${formatCost({ amount: wasted.amount, source: 'mixed' })} across ${wasted.runs} rows — see amicus spend --failed\n`;
|
|
156
201
|
}
|