sigrank 0.16.0 → 0.16.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/tools.mjs CHANGED
@@ -8,24 +8,34 @@
8
8
  * narration card in ./narrate.mjs. Token-only, no transcript content.
9
9
  */
10
10
 
11
- import { cascade, parsePillars, detectMode, qualityScore, MODE_EXPECTED_YIELD } from './cascade.mjs'
12
- import { computeBadges } from './badges.mjs'
13
- import { narrate } from './narrate.mjs'
14
- import { tokenpull as pullLocal, tokenpullCodex as pullCodex, tokenpullAny } from './tokenpull.mjs'
15
- import { ALL_PLATFORMS } from './adapters.mjs'
16
- import { ensureIdentity, recordEnrollment } from './keystore.mjs'
17
- import { submitSignedWindow } from './submit.mjs'
18
- import { createHash } from 'node:crypto'
19
- import { execFile } from 'node:child_process'
20
- import { existsSync, readFileSync } from 'node:fs'
21
- import os from 'node:os'
22
- import path from 'node:path'
23
- import { fileURLToPath } from 'node:url'
11
+ import {
12
+ cascade,
13
+ parsePillars,
14
+ detectMode,
15
+ qualityScore,
16
+ MODE_EXPECTED_YIELD,
17
+ } from "./cascade.mjs";
18
+ import { computeBadges } from "./badges.mjs";
19
+ import { narrate } from "./narrate.mjs";
20
+ import {
21
+ tokenpull as pullLocal,
22
+ tokenpullCodex as pullCodex,
23
+ tokenpullAny,
24
+ } from "./tokenpull.mjs";
25
+ import { ALL_PLATFORMS } from "./adapters.mjs";
26
+ import { ensureIdentity, recordEnrollment } from "./keystore.mjs";
27
+ import { submitSignedWindow } from "./submit.mjs";
28
+ import { createHash } from "node:crypto";
29
+ import { execFile } from "node:child_process";
30
+ import { existsSync, readFileSync } from "node:fs";
31
+ import os from "node:os";
32
+ import path from "node:path";
33
+ import { fileURLToPath } from "node:url";
24
34
 
25
35
  // Resolve local node_modules/.bin for bundled deps (ccusage, tokscale)
26
- const _pkgRoot = path.dirname(fileURLToPath(import.meta.url))
27
- const _localBin = path.join(_pkgRoot, 'node_modules', '.bin')
28
- const _envPath = `${_localBin}${process.env.PATH ? ':' + process.env.PATH : ''}`
36
+ const _pkgRoot = path.dirname(fileURLToPath(import.meta.url));
37
+ const _localBin = path.join(_pkgRoot, "node_modules", ".bin");
38
+ const _envPath = `${_localBin}${process.env.PATH ? ":" + process.env.PATH : ""}`;
29
39
 
30
40
  // ASYNC FIX (2026-06-27): execFile wrapped in a Promise — replaces execSync for
31
41
  // defense-in-depth (shell injection prevention + non-blocking). The platform param
@@ -37,11 +47,20 @@ function execFileAsync(cmd, args, timeoutMs) {
37
47
  return new Promise((resolve, reject) => {
38
48
  // NOTE: execFile does not accept a `stdio` option (it always pipes + buffers
39
49
  // stdout/stderr against maxBuffer) — a previous `stdio` key here was silently ignored.
40
- execFile(cmd, args, { timeout: timeoutMs, maxBuffer: 10 * 1024 * 1024, env: { ...process.env, PATH: _envPath } }, (err, stdout) => {
41
- if (err) reject(err)
42
- else resolve(stdout.toString())
43
- })
44
- })
50
+ execFile(
51
+ cmd,
52
+ args,
53
+ {
54
+ timeout: timeoutMs,
55
+ maxBuffer: 10 * 1024 * 1024,
56
+ env: { ...process.env, PATH: _envPath },
57
+ },
58
+ (err, stdout) => {
59
+ if (err) reject(err);
60
+ else resolve(stdout.toString());
61
+ },
62
+ );
63
+ });
45
64
  }
46
65
 
47
66
  // ── Verifier readers (sync, on-device, token-only) ────────────────────────────
@@ -53,90 +72,156 @@ function execFileAsync(cmd, args, timeoutMs) {
53
72
  // (bunx tokscale, scan+read tokendash) for the TUI/CLI dashboard. Different data sources =
54
73
  // different behavior; do NOT merge without understanding the trade-off.
55
74
 
56
- async function _ccusagePillars(platform = 'claude') {
75
+ async function _ccusagePillars(platform = "claude") {
57
76
  try {
58
- const raw = await execFileAsync('ccusage', [platform, 'daily', '--json'], 15000)
59
- const rows = JSON.parse(raw)?.daily ?? JSON.parse(raw)
60
- const now = Date.now()
61
- const result = {}
62
- for (const [win, days] of Object.entries({ '7d': 7, '30d': 30, '90d': 90 })) {
63
- const since = new Date(now - days * 86400000)
64
- let i=0,o=0,cw=0,cr=0
77
+ const raw = await execFileAsync(
78
+ "ccusage",
79
+ [platform, "daily", "--json"],
80
+ 15000,
81
+ );
82
+ const rows = JSON.parse(raw)?.daily ?? JSON.parse(raw);
83
+ const now = Date.now();
84
+ const result = {};
85
+ for (const [win, days] of Object.entries({
86
+ "7d": 7,
87
+ "30d": 30,
88
+ "90d": 90,
89
+ })) {
90
+ const since = new Date(now - days * 86400000);
91
+ let i = 0,
92
+ o = 0,
93
+ cw = 0,
94
+ cr = 0;
65
95
  for (const r of rows) {
66
- if (new Date(r.date ?? r.day ?? '1970') >= since) {
67
- i += r.inputTokens ?? r.input_tokens ?? 0
68
- o += r.outputTokens ?? r.output_tokens ?? 0
69
- cw += r.cacheCreationTokens ?? r.cache_create_tokens ?? 0
70
- cr += r.cacheReadTokens ?? r.cache_read_tokens ?? 0
96
+ if (new Date(r.date ?? r.day ?? "1970") >= since) {
97
+ i += r.inputTokens ?? r.input_tokens ?? 0;
98
+ o += r.outputTokens ?? r.output_tokens ?? 0;
99
+ cw += r.cacheCreationTokens ?? r.cache_create_tokens ?? 0;
100
+ cr += r.cacheReadTokens ?? r.cache_read_tokens ?? 0;
71
101
  }
72
102
  }
73
- result[win] = { input: i, output: o, cacheCreate: cw, cacheRead: cr }
103
+ result[win] = { input: i, output: o, cacheCreate: cw, cacheRead: cr };
74
104
  }
75
- let i=0,o=0,cw=0,cr=0
105
+ let i = 0,
106
+ o = 0,
107
+ cw = 0,
108
+ cr = 0;
76
109
  for (const r of rows) {
77
- i += r.inputTokens ?? r.input_tokens ?? 0
78
- o += r.outputTokens ?? r.output_tokens ?? 0
79
- cw += r.cacheCreationTokens ?? r.cache_create_tokens ?? 0
80
- cr += r.cacheReadTokens ?? r.cache_read_tokens ?? 0
110
+ i += r.inputTokens ?? r.input_tokens ?? 0;
111
+ o += r.outputTokens ?? r.output_tokens ?? 0;
112
+ cw += r.cacheCreationTokens ?? r.cache_create_tokens ?? 0;
113
+ cr += r.cacheReadTokens ?? r.cache_read_tokens ?? 0;
81
114
  }
82
- result['all'] = { input: i, output: o, cacheCreate: cw, cacheRead: cr }
83
- return result
84
- } catch { return null }
115
+ result["all"] = { input: i, output: o, cacheCreate: cw, cacheRead: cr };
116
+ return result;
117
+ } catch {
118
+ return null;
119
+ }
85
120
  }
86
121
 
87
122
  async function _tokenDashPillars() {
88
- const dbPath = path.join(os.homedir(), '.claude', 'token-dashboard.db')
89
- if (!existsSync(dbPath)) return null
123
+ const dbPath = path.join(os.homedir(), ".claude", "token-dashboard.db");
124
+ if (!existsSync(dbPath)) return null;
90
125
  try {
91
- const raw = await execFileAsync('sqlite3', [dbPath,
92
- 'SELECT SUM(input_tokens),SUM(output_tokens),SUM(cache_create_5m_tokens)+SUM(cache_create_1h_tokens),SUM(cache_read_tokens) FROM messages'
93
- ], 5000)
94
- const [i,o,cw,cr] = raw.trim().split('|').map(Number)
95
- return { all: { input: i||0, output: o||0, cacheCreate: cw||0, cacheRead: cr||0 } }
96
- } catch { return null }
126
+ const raw = await execFileAsync(
127
+ "sqlite3",
128
+ [
129
+ dbPath,
130
+ "SELECT SUM(input_tokens),SUM(output_tokens),SUM(cache_create_5m_tokens)+SUM(cache_create_1h_tokens),SUM(cache_read_tokens) FROM messages",
131
+ ],
132
+ 5000,
133
+ );
134
+ const [i, o, cw, cr] = raw.trim().split("|").map(Number);
135
+ return {
136
+ all: {
137
+ input: i || 0,
138
+ output: o || 0,
139
+ cacheCreate: cw || 0,
140
+ cacheRead: cr || 0,
141
+ },
142
+ };
143
+ } catch {
144
+ return null;
145
+ }
97
146
  }
98
147
 
99
- async function _tokscalePillars(platform = 'claude') {
148
+ async function _tokscalePillars(platform = "claude") {
100
149
  // Try the bundled tokscale CLI first (fresh data), fall back to saved report file.
101
150
  try {
102
- const raw = await execFileAsync('tokscale', ['models', '--json'], 60000)
103
- const data = JSON.parse(raw)
104
- const entries = Array.isArray(data?.entries) ? data.entries : (Array.isArray(data) ? data : [])
105
- const rows = entries.filter(e =>
106
- e && e.client === platform && e.model !== '<synthetic>' && e.model !== 'unknown' && ((Number(e.input) || 0) > 0 || (Number(e.output) || 0) > 0)
107
- )
151
+ const raw = await execFileAsync("tokscale", ["models", "--json"], 60000);
152
+ const data = JSON.parse(raw);
153
+ const entries = Array.isArray(data?.entries)
154
+ ? data.entries
155
+ : Array.isArray(data)
156
+ ? data
157
+ : [];
158
+ const rows = entries.filter(
159
+ (e) =>
160
+ e &&
161
+ e.client === platform &&
162
+ e.model !== "<synthetic>" &&
163
+ e.model !== "unknown" &&
164
+ ((Number(e.input) || 0) > 0 || (Number(e.output) || 0) > 0),
165
+ );
108
166
  if (rows.length) {
109
- const acc = rows.reduce((a,e) => ({
110
- input: a.input+(Number(e.input)||0), output: a.output+(Number(e.output)||0),
111
- cacheCreate: a.cacheCreate+(Number(e.cacheWrite)||0), cacheRead: a.cacheRead+(Number(e.cacheRead)||0),
112
- }), { input:0, output:0, cacheCreate:0, cacheRead:0 })
113
- return { all: acc }
167
+ const acc = rows.reduce(
168
+ (a, e) => ({
169
+ input: a.input + (Number(e.input) || 0),
170
+ output: a.output + (Number(e.output) || 0),
171
+ cacheCreate: a.cacheCreate + (Number(e.cacheWrite) || 0),
172
+ cacheRead: a.cacheRead + (Number(e.cacheRead) || 0),
173
+ }),
174
+ { input: 0, output: 0, cacheCreate: 0, cacheRead: 0 },
175
+ );
176
+ return { all: acc };
114
177
  }
115
- } catch { /* fall through to file-based read */ }
178
+ } catch {
179
+ /* fall through to file-based read */
180
+ }
116
181
  // Fallback: read saved tokscale_report.json if it exists
117
- const p = path.join(os.homedir(), 'tokscale_report.json')
118
- if (!existsSync(p)) return null
182
+ const p = path.join(os.homedir(), "tokscale_report.json");
183
+ if (!existsSync(p)) return null;
119
184
  try {
120
- const data = JSON.parse(readFileSync(p, 'utf8'))
121
- const rows = (data.entries ?? []).filter(e =>
122
- e.client === platform && e.model !== '<synthetic>' && e.model !== 'unknown' && (e.input > 0 || e.output > 0)
123
- )
124
- if (!rows.length) return null
125
- const acc = rows.reduce((a,e) => ({
126
- input: a.input+(e.input??0), output: a.output+(e.output??0),
127
- cacheCreate: a.cacheCreate+(e.cacheWrite??0), cacheRead: a.cacheRead+(e.cacheRead??0),
128
- }), { input:0, output:0, cacheCreate:0, cacheRead:0 })
129
- return { all: acc }
130
- } catch { return null }
185
+ const data = JSON.parse(readFileSync(p, "utf8"));
186
+ const rows = (data.entries ?? []).filter(
187
+ (e) =>
188
+ e.client === platform &&
189
+ e.model !== "<synthetic>" &&
190
+ e.model !== "unknown" &&
191
+ (e.input > 0 || e.output > 0),
192
+ );
193
+ if (!rows.length) return null;
194
+ const acc = rows.reduce(
195
+ (a, e) => ({
196
+ input: a.input + (e.input ?? 0),
197
+ output: a.output + (e.output ?? 0),
198
+ cacheCreate: a.cacheCreate + (e.cacheWrite ?? 0),
199
+ cacheRead: a.cacheRead + (e.cacheRead ?? 0),
200
+ }),
201
+ { input: 0, output: 0, cacheCreate: 0, cacheRead: 0 },
202
+ );
203
+ return { all: acc };
204
+ } catch {
205
+ return null;
206
+ }
131
207
  }
132
208
 
133
209
  // Every board upload from the MCP is hashed + timestamped (ddmmyy) — provenance + dedup.
134
210
  function uploadStamp(content) {
135
- const hash = createHash('sha256').update(JSON.stringify(content)).digest('hex')
136
- const d = new Date()
137
- const p = (n) => String(n).padStart(2, '0')
138
- const ddmmyy = p(d.getUTCDate()) + p(d.getUTCMonth() + 1) + String(d.getUTCFullYear()).slice(-2)
139
- return { content_hash: hash, submitted_ddmmyy: ddmmyy, submitted_at: d.toISOString() }
211
+ const hash = createHash("sha256")
212
+ .update(JSON.stringify(content))
213
+ .digest("hex");
214
+ const d = new Date();
215
+ const p = (n) => String(n).padStart(2, "0");
216
+ const ddmmyy =
217
+ p(d.getUTCDate()) +
218
+ p(d.getUTCMonth() + 1) +
219
+ String(d.getUTCFullYear()).slice(-2);
220
+ return {
221
+ content_hash: hash,
222
+ submitted_ddmmyy: ddmmyy,
223
+ submitted_at: d.toISOString(),
224
+ };
140
225
  }
141
226
 
142
227
  // Pull a platform's local usage → 4 windows of canonical pillars. Routes through
@@ -145,26 +230,31 @@ function uploadStamp(content) {
145
230
  async function pullByPlatform(platform, opts = {}) {
146
231
  if (opts.adapter) {
147
232
  // Test injection: bypass registry and use the mock adapter directly
148
- if (platform === 'codex') {
149
- let ioRatio = 2.0
233
+ if (platform === "codex") {
234
+ let ioRatio = 2.0;
150
235
  try {
151
- const c = await pullLocal({})
152
- const all = c.windows.find((w) => w.window === 'all')
153
- if (all && all.pillars.output > 0) ioRatio = all.pillars.input / all.pillars.output
154
- } catch { /* no Claude data → Alpha 2.0 */ }
155
- return pullCodex({ ioRatio, adapter: opts.adapter, now: opts.now })
236
+ const c = await pullLocal({});
237
+ const all = c.windows.find((w) => w.window === "all");
238
+ if (all && all.pillars.output > 0)
239
+ ioRatio = all.pillars.input / all.pillars.output;
240
+ } catch {
241
+ /* no Claude data → Alpha 2.0 */
242
+ }
243
+ return pullCodex({ ioRatio, adapter: opts.adapter, now: opts.now });
156
244
  }
157
- return pullLocal({ adapter: opts.adapter, now: opts.now })
245
+ return pullLocal({ adapter: opts.adapter, now: opts.now });
158
246
  }
159
- return tokenpullAny(platform || 'claude', opts)
247
+ return tokenpullAny(platform || "claude", opts);
160
248
  }
161
249
 
162
- export const DEFAULT_API_BASE = process.env.SIGRANK_API_BASE || 'https://signalaf.com'
250
+ export const DEFAULT_API_BASE =
251
+ process.env.SIGRANK_API_BASE || "https://signalaf.com";
163
252
  /** Default network timeout in ms (override via opts.fetchTimeout or SIGRANK_FETCH_TIMEOUT). */
164
- export const DEFAULT_FETCH_TIMEOUT = Number(process.env.SIGRANK_FETCH_TIMEOUT) || 10_000
253
+ export const DEFAULT_FETCH_TIMEOUT =
254
+ Number(process.env.SIGRANK_FETCH_TIMEOUT) || 10_000;
165
255
  /** Max accepted length for a single paste/string arg (chars). Token counts are tiny; anything
166
256
  * past this is malformed or abusive — reject cleanly before parsing/POSTing (E2 hardening). */
167
- const MAX_INPUT = 1_000_000
257
+ const MAX_INPUT = 1_000_000;
168
258
 
169
259
  // ── Smithery quality: output schemas + annotations ──────────────────────────
170
260
  // MCP tool annotations hint to clients about side-effects, read-only status, etc.
@@ -173,349 +263,490 @@ const ANNOTATIONS = {
173
263
  destructiveHint: { destructiveHint: false },
174
264
  idempotentHint: { idempotentHint: true },
175
265
  openWorldHint: { openWorldHint: false },
176
- }
266
+ };
177
267
 
178
268
  // Common output schema for cascade results (rank_paste, simulate_change, etc.)
179
269
  const CASCADE_OUTPUT = {
180
- type: 'object',
270
+ type: "object",
181
271
  properties: {
182
- yield_: { type: 'number', description: 'Υ Yield — the headline efficiency metric (Cache Reads × Output / Input²)' },
183
- snr: { type: 'number', description: 'Signal-to-noise ratio' },
184
- leverage: { type: 'number', description: 'Cr/I — cache reads divided by input' },
185
- velocity: { type: 'number', description: 'O/I — output divided by input' },
186
- tenx_dev: { type: 'number', description: '10xDEV score' },
187
- class: { type: 'string', enum: ['Burner', 'Builder', '10xer'], description: 'Operator class tier' },
188
- card: { type: 'string', description: 'Deterministic prose summary of the cascade result' },
189
- warnings: { type: 'array', items: { type: 'string' }, description: 'Parse or data warnings if any' },
272
+ yield_: {
273
+ type: "number",
274
+ description:
275
+ "Υ Yield — the headline efficiency metric (Cache Reads × Output / Input²)",
276
+ },
277
+ snr: { type: "number", description: "Signal-to-noise ratio" },
278
+ leverage: {
279
+ type: "number",
280
+ description: "Cr/I — cache reads divided by input",
281
+ },
282
+ velocity: { type: "number", description: "O/I — output divided by input" },
283
+ tenx_dev: { type: "number", description: "10xDEV score" },
284
+ class: {
285
+ type: "string",
286
+ enum: ["Burner", "Builder", "10xer"],
287
+ description: "Operator class tier",
288
+ },
289
+ card: {
290
+ type: "string",
291
+ description: "Deterministic prose summary of the cascade result",
292
+ },
293
+ warnings: {
294
+ type: "array",
295
+ items: { type: "string" },
296
+ description: "Parse or data warnings if any",
297
+ },
190
298
  },
191
- required: ['yield_', 'class'],
192
- }
299
+ required: ["yield_", "class"],
300
+ };
193
301
 
194
302
  const LEADERBOARD_OUTPUT = {
195
- type: 'object',
303
+ type: "object",
196
304
  properties: {
197
305
  operators: {
198
- type: 'array',
199
- description: 'Array of ranked operators sorted by yield',
200
- items: {
201
- type: 'object',
202
- properties: {
203
- codename: { type: 'string', description: 'Public display name' },
204
- yield_: { type: 'number', description: 'Υ Yield metric' },
205
- leverage: { type: 'number', description: 'Cr/I ratio' },
206
- velocity: { type: 'number', description: 'O/I ratio' },
207
- class: { type: 'string', enum: ['Burner', 'Builder', '10xer'] },
208
- rank: { type: 'integer', description: '1-based rank position' },
209
- },
210
- },
306
+ type: "array",
307
+ description: "Array of ranked operators sorted by yield",
308
+ items: {
309
+ type: "object",
310
+ properties: {
311
+ codename: { type: "string", description: "Public display name" },
312
+ yield_: { type: "number", description: "Υ Yield metric" },
313
+ leverage: { type: "number", description: "Cr/I ratio" },
314
+ velocity: { type: "number", description: "O/I ratio" },
315
+ class: { type: "string", enum: ["Burner", "Builder", "10xer"] },
316
+ rank: { type: "integer", description: "1-based rank position" },
317
+ },
318
+ },
211
319
  },
212
320
  },
213
- }
321
+ };
214
322
 
215
323
  const OPERATOR_OUTPUT = {
216
- type: 'object',
324
+ type: "object",
217
325
  properties: {
218
- codename: { type: 'string', description: 'Operator display name' },
219
- yield_: { type: 'number', description: 'Υ Yield metric' },
220
- leverage: { type: 'number', description: 'Cr/I ratio' },
221
- velocity: { type: 'number', description: 'O/I ratio' },
222
- class: { type: 'string', enum: ['Burner', 'Builder', '10xer'] },
223
- rank: { type: 'integer', description: '1-based rank position' },
326
+ codename: { type: "string", description: "Operator display name" },
327
+ yield_: { type: "number", description: "Υ Yield metric" },
328
+ leverage: { type: "number", description: "Cr/I ratio" },
329
+ velocity: { type: "number", description: "O/I ratio" },
330
+ class: { type: "string", enum: ["Burner", "Builder", "10xer"] },
331
+ rank: { type: "integer", description: "1-based rank position" },
224
332
  windows: {
225
- type: 'array',
226
- description: 'Per-window breakdowns (7d, 30d, 90d, all-time)',
333
+ type: "array",
334
+ description: "Per-window breakdowns (7d, 30d, 90d, all-time)",
227
335
  items: {
228
- type: 'object',
336
+ type: "object",
229
337
  properties: {
230
- window: { type: 'string', enum: ['7d', '30d', '90d', 'all_time'] },
338
+ window: { type: "string", enum: ["7d", "30d", "90d", "all_time"] },
231
339
  pillars: {
232
- type: 'object',
340
+ type: "object",
233
341
  properties: {
234
- input: { type: 'integer' },
235
- output: { type: 'integer' },
236
- cacheCreate: { type: 'integer' },
237
- cacheRead: { type: 'integer' },
342
+ input: { type: "integer" },
343
+ output: { type: "integer" },
344
+ cacheCreate: { type: "integer" },
345
+ cacheRead: { type: "integer" },
238
346
  },
239
347
  },
240
348
  },
241
349
  },
242
350
  },
243
351
  },
244
- }
352
+ };
245
353
 
246
354
  const SUBMIT_OUTPUT = {
247
- type: 'object',
355
+ type: "object",
248
356
  properties: {
249
- status: { type: 'string', enum: ['ok', 'error', 'skipped'], description: 'Submission status' },
357
+ status: {
358
+ type: "string",
359
+ enum: ["ok", "error", "skipped"],
360
+ description: "Submission status",
361
+ },
250
362
  preview: CASCADE_OUTPUT,
251
- server_response: { type: 'object', description: 'Server-side response including new rank if accepted' },
252
- reason: { type: 'string', description: 'Error or skip reason if status is not ok' },
363
+ server_response: {
364
+ type: "object",
365
+ description: "Server-side response including new rank if accepted",
366
+ },
367
+ reason: {
368
+ type: "string",
369
+ description: "Error or skip reason if status is not ok",
370
+ },
253
371
  },
254
- }
372
+ };
255
373
 
256
374
  const TOKENPULL_OUTPUT = {
257
- type: 'object',
375
+ type: "object",
258
376
  properties: {
259
- platform: { type: 'string', description: 'Source platform name' },
260
- generatedAt: { type: 'string', description: 'ISO timestamp of the pull' },
377
+ platform: { type: "string", description: "Source platform name" },
378
+ generatedAt: { type: "string", description: "ISO timestamp of the pull" },
261
379
  windows: {
262
- type: 'array',
263
- description: 'Per-window token usage + cascade results',
380
+ type: "array",
381
+ description: "Per-window token usage + cascade results",
264
382
  items: {
265
- type: 'object',
383
+ type: "object",
266
384
  properties: {
267
- window: { type: 'string', enum: ['7d', '30d', '90d', 'all'] },
385
+ window: { type: "string", enum: ["7d", "30d", "90d", "all"] },
268
386
  pillars: {
269
- type: 'object',
387
+ type: "object",
270
388
  properties: {
271
- input: { type: 'integer' },
272
- output: { type: 'integer' },
273
- cacheCreate: { type: 'integer' },
274
- cacheRead: { type: 'integer' },
389
+ input: { type: "integer" },
390
+ output: { type: "integer" },
391
+ cacheCreate: { type: "integer" },
392
+ cacheRead: { type: "integer" },
275
393
  },
276
394
  },
277
- messages: { type: 'integer', description: 'Number of messages in window' },
278
- estimated: { type: 'boolean', description: 'True if cacheCreate was estimated' },
395
+ messages: {
396
+ type: "integer",
397
+ description: "Number of messages in window",
398
+ },
399
+ estimated: {
400
+ type: "boolean",
401
+ description: "True if cacheCreate was estimated",
402
+ },
279
403
  cascade: CASCADE_OUTPUT,
280
404
  },
281
405
  },
282
406
  },
283
407
  },
284
- }
408
+ };
285
409
 
286
410
  const ENROLL_OUTPUT = {
287
- type: 'object',
411
+ type: "object",
288
412
  properties: {
289
- status: { type: 'string', enum: ['enrolled', 'error'], description: 'Enrollment result' },
290
- codename: { type: 'string', description: 'Operator codename if enrolled' },
291
- operator_id: { type: 'string', description: 'Operator ID if enrolled' },
292
- device_id: { type: 'string', description: 'Local device ID' },
293
- trust_status: { type: 'string', description: 'Trust level of the device' },
294
- reason: { type: 'string', description: 'Error reason if status is error' },
413
+ status: {
414
+ type: "string",
415
+ enum: ["enrolled", "error"],
416
+ description: "Enrollment result",
417
+ },
418
+ codename: { type: "string", description: "Operator codename if enrolled" },
419
+ operator_id: { type: "string", description: "Operator ID if enrolled" },
420
+ device_id: { type: "string", description: "Local device ID" },
421
+ trust_status: { type: "string", description: "Trust level of the device" },
422
+ reason: { type: "string", description: "Error reason if status is error" },
295
423
  },
296
- }
424
+ };
297
425
 
298
426
  const COMPARE_OUTPUT = {
299
- type: 'object',
427
+ type: "object",
300
428
  properties: {
301
- platform: { type: 'string' },
429
+ platform: { type: "string" },
302
430
  sources: {
303
- type: 'array',
304
- description: 'Side-by-side comparison of each token source',
431
+ type: "array",
432
+ description: "Side-by-side comparison of each token source",
305
433
  items: {
306
- type: 'object',
434
+ type: "object",
307
435
  properties: {
308
- source: { type: 'string', description: 'Source name (tokenpull, ccusage, token-dash, tokscale)' },
309
- pillars: { type: 'object' },
436
+ source: {
437
+ type: "string",
438
+ description:
439
+ "Source name (tokenpull, ccusage, token-dash, tokscale)",
440
+ },
441
+ pillars: { type: "object" },
310
442
  cascade: CASCADE_OUTPUT,
311
- delta_pct: { type: 'object', description: 'Delta % vs tokenpull baseline' },
443
+ delta_pct: {
444
+ type: "object",
445
+ description: "Delta % vs tokenpull baseline",
446
+ },
312
447
  },
313
448
  },
314
449
  },
315
450
  },
316
- }
451
+ };
317
452
 
318
453
  const SIMULATE_OUTPUT = {
319
- type: 'object',
454
+ type: "object",
320
455
  properties: {
321
456
  current: CASCADE_OUTPUT,
322
457
  simulated: CASCADE_OUTPUT,
323
- yield_delta: { type: 'number', description: 'Υ Yield change (simulated - current)' },
324
- class_change: { type: 'string', description: 'Class tier change description' },
325
- metric_diffs: { type: 'object', description: 'Per-metric before/after diffs' },
458
+ yield_delta: {
459
+ type: "number",
460
+ description: "Υ Yield change (simulated - current)",
461
+ },
462
+ class_change: {
463
+ type: "string",
464
+ description: "Class tier change description",
465
+ },
466
+ metric_diffs: {
467
+ type: "object",
468
+ description: "Per-metric before/after diffs",
469
+ },
326
470
  },
327
- }
471
+ };
328
472
 
329
473
  const DIAGNOSE_OUTPUT = {
330
- type: 'object',
474
+ type: "object",
331
475
  properties: {
332
- pillars: { type: 'object', description: 'The 4 raw token pillars' },
476
+ pillars: { type: "object", description: "The 4 raw token pillars" },
333
477
  cascade: CASCADE_OUTPUT,
334
478
  diagnosis: {
335
- type: 'array',
336
- description: 'Ranked list of efficiency leaks found, worst first',
479
+ type: "array",
480
+ description: "Ranked list of efficiency leaks found, worst first",
337
481
  items: {
338
- type: 'object',
482
+ type: "object",
339
483
  properties: {
340
- metric: { type: 'string', description: 'Which metric is underperforming' },
341
- severity: { type: 'string', enum: ['critical', 'warning', 'info'], description: 'How bad the leak is' },
342
- finding: { type: 'string', description: 'What the analysis found' },
343
- recommendation: { type: 'string', description: 'What to do about it' },
344
- estimated_yield_impact: { type: 'string', description: 'Estimated Υ improvement if fixed' },
484
+ metric: {
485
+ type: "string",
486
+ description: "Which metric is underperforming",
487
+ },
488
+ severity: {
489
+ type: "string",
490
+ enum: ["critical", "warning", "info"],
491
+ description: "How bad the leak is",
492
+ },
493
+ finding: { type: "string", description: "What the analysis found" },
494
+ recommendation: {
495
+ type: "string",
496
+ description: "What to do about it",
497
+ },
498
+ estimated_yield_impact: {
499
+ type: "string",
500
+ description: "Estimated Υ improvement if fixed",
501
+ },
345
502
  },
346
503
  },
347
504
  },
348
- summary: { type: 'string', description: 'One-line summary of the operator\'s cascade health' },
505
+ summary: {
506
+ type: "string",
507
+ description: "One-line summary of the operator's cascade health",
508
+ },
349
509
  },
350
- }
510
+ };
351
511
 
352
512
  const SUGGEST_OUTPUT = {
353
- type: 'object',
513
+ type: "object",
354
514
  properties: {
355
515
  suggestions: {
356
- type: 'array',
357
- description: 'Ranked recommendations, highest Υ impact first',
516
+ type: "array",
517
+ description: "Ranked recommendations, highest Υ impact first",
358
518
  items: {
359
- type: 'object',
519
+ type: "object",
360
520
  properties: {
361
- rank: { type: 'number', description: '1-based rank by Υ impact' },
362
- action: { type: 'string', description: 'What to change' },
363
- pillar: { type: 'string', description: 'Which pillar to adjust' },
364
- delta: { type: 'string', description: 'How much to change it' },
365
- simulated_yield: { type: 'number', description: 'Projected Υ after the change' },
366
- yield_delta: { type: 'number', description: 'Υ change vs current' },
367
- class_after: { type: 'string', description: 'Projected class after the change' },
368
- rationale: { type: 'string', description: 'Why this helps' },
521
+ rank: { type: "number", description: "1-based rank by Υ impact" },
522
+ action: { type: "string", description: "What to change" },
523
+ pillar: { type: "string", description: "Which pillar to adjust" },
524
+ delta: { type: "string", description: "How much to change it" },
525
+ simulated_yield: {
526
+ type: "number",
527
+ description: "Projected Υ after the change",
528
+ },
529
+ yield_delta: { type: "number", description: "Υ change vs current" },
530
+ class_after: {
531
+ type: "string",
532
+ description: "Projected class after the change",
533
+ },
534
+ rationale: { type: "string", description: "Why this helps" },
369
535
  },
370
536
  },
371
537
  },
372
- current_yield: { type: 'number', description: 'Current Υ before any changes' },
373
- current_class: { type: 'string', description: 'Current class tier' },
374
- best_single_change: { type: 'string', description: 'The single highest-impact change' },
538
+ current_yield: {
539
+ type: "number",
540
+ description: "Current Υ before any changes",
541
+ },
542
+ current_class: { type: "string", description: "Current class tier" },
543
+ best_single_change: {
544
+ type: "string",
545
+ description: "The single highest-impact change",
546
+ },
375
547
  },
376
- }
548
+ };
377
549
 
378
550
  export const TOOLS = [
379
551
  {
380
- name: 'rank_paste',
552
+ name: "rank_paste",
381
553
  description:
382
554
  'Computes the SigRank yield cascade from a paste of token counts. Parses the input, runs the full cascade math locally (no network calls), and returns: Υ Yield (the headline efficiency metric, Υ = Cache Reads × Output / Input²), SNR (signal-to-noise ratio), Leverage ratio (Cr/I = cache reads divided by input), Velocity (O/I = output divided by input), 10xDEV score, operator class tier (Burner / Builder / 10xer), and a deterministic prose "card" summarizing the result in plain English. Accepts two input formats: (1) JSON object {"input":N,"output":N,"cacheCreate":N,"cacheRead":N} or (2) four whitespace-separated numbers in order: input output cacheCreate cacheRead. Returns an error if the input is malformed or has negative values. Use this for a quick one-off ranking without submitting to the board. Do NOT use this to submit your score — use submit_paste instead, which both ranks and publishes. Do NOT use this if you want to rank all four time windows at once — use rank_windows for that. After calling this, use submit_paste to publish the result if you want to appear on the leaderboard.',
383
- annotations: { ...ANNOTATIONS.readOnlyHint, ...ANNOTATIONS.destructiveHint, ...ANNOTATIONS.idempotentHint, ...ANNOTATIONS.openWorldHint },
555
+ annotations: {
556
+ ...ANNOTATIONS.readOnlyHint,
557
+ ...ANNOTATIONS.destructiveHint,
558
+ ...ANNOTATIONS.idempotentHint,
559
+ ...ANNOTATIONS.openWorldHint,
560
+ },
384
561
  inputSchema: {
385
- type: 'object',
562
+ type: "object",
386
563
  properties: {
387
564
  text: {
388
- type: 'string',
389
- description: 'Token counts to rank. Two formats accepted: (1) JSON object {"input":N,"output":N,"cacheCreate":N,"cacheRead":N} where all values are non-negative integers, or (2) four whitespace-separated numbers in order: input output cacheCreate cacheRead. Get these from `ccusage` output, the Claude Max usage dashboard, tokscale, or any token reader. Example valid input: {"input":1000000,"output":500000,"cacheCreate":50000,"cacheRead":800000}',
565
+ type: "string",
566
+ description:
567
+ 'Token counts to rank. Two formats accepted: (1) JSON object {"input":N,"output":N,"cacheCreate":N,"cacheRead":N} where all values are non-negative integers, or (2) four whitespace-separated numbers in order: input output cacheCreate cacheRead. Get these from `ccusage` output, the Claude Max usage dashboard, tokscale, or any token reader. Example valid input: {"input":1000000,"output":500000,"cacheCreate":50000,"cacheRead":800000}',
390
568
  },
391
569
  },
392
- required: ['text'],
393
- description: 'Requires the token counts as a string. No other parameters are accepted.',
570
+ required: ["text"],
571
+ description:
572
+ "Requires the token counts as a string. No other parameters are accepted.",
394
573
  },
395
574
  outputSchema: CASCADE_OUTPUT,
396
575
  },
397
576
  {
398
- name: 'get_leaderboard',
577
+ name: "get_leaderboard",
399
578
  description:
400
579
  "Fetches the live public SigRank leaderboard from signalaf.com. Reads all ranked operators sorted by yield (Υ = Cache Reads × Output / Input²) and returns an array of operator summaries. Each entry contains: codename (public display name), yield (Υ, the headline efficiency metric), leverage ratio (Cr/I = cache reads divided by input), velocity (O/I = output divided by input), class tier (Burner / Builder / 10xer), and rank position (integer, 1-based). Returns an empty array if no operators have submitted yet. Use this to see where operators stand overall, to find specific codenames for get_operator lookups, or to display the current rankings. Do NOT use this to check your own rank if you already know your codename — use get_operator instead for a single-operator profile with per-window breakdowns. After calling this, follow up with get_operator to get detailed metrics for any operator of interest.",
401
580
  annotations: { ...ANNOTATIONS.readOnlyHint, ...ANNOTATIONS.openWorldHint },
402
581
  inputSchema: {
403
- type: 'object',
582
+ type: "object",
404
583
  properties: {},
405
- description: 'This tool takes no parameters. It always fetches the full public leaderboard.',
584
+ description:
585
+ "This tool takes no parameters. It always fetches the full public leaderboard.",
406
586
  },
407
587
  outputSchema: LEADERBOARD_OUTPUT,
408
588
  },
409
589
  {
410
- name: 'get_operator',
590
+ name: "get_operator",
411
591
  description:
412
592
  "Fetches one operator's live profile from the SigRank board by their codename. Reads the operator's current submission data from signalaf.com and returns their detailed metrics: yield (Υ), leverage ratio (Cr/I), velocity (O/I), class tier (Burner / Builder / 10xer), rank position (integer, 1-based), and per-window breakdowns for each time range (7d, 30d, 90d, all-time) with the four canonical pillars (input, output, cacheCreate, cacheRead) per window. Returns an error if the codename is not found on the board. Use this to look up any operator who has submitted to the board — codenames are public and visible on the leaderboard. Do NOT use this to browse all operators — use get_leaderboard for that. After calling this, you can use simulate_change to model what would happen if the operator adjusted their token mix.",
413
593
  annotations: { ...ANNOTATIONS.readOnlyHint, ...ANNOTATIONS.openWorldHint },
414
594
  inputSchema: {
415
- type: 'object',
595
+ type: "object",
416
596
  properties: {
417
597
  codename: {
418
- type: 'string',
419
- description: 'The operator\'s public codename as shown on the SigRank leaderboard. Case-insensitive — "Ghost Falcon" and "ghost falcon" are equivalent. Must match a codename that exists on the board; returns an error if not found. To discover valid codenames, call get_leaderboard first.',
598
+ type: "string",
599
+ description:
600
+ 'The operator\'s public codename as shown on the SigRank leaderboard. Case-insensitive — "Ghost Falcon" and "ghost falcon" are equivalent. Must match a codename that exists on the board; returns an error if not found. To discover valid codenames, call get_leaderboard first.',
420
601
  },
421
602
  },
422
- required: ['codename'],
423
- description: 'Requires the operator\'s codename. No other parameters are accepted.',
603
+ required: ["codename"],
604
+ description:
605
+ "Requires the operator's codename. No other parameters are accepted.",
424
606
  },
425
607
  outputSchema: OPERATOR_OUTPUT,
426
608
  },
427
609
  {
428
- name: 'submit_paste',
610
+ name: "submit_paste",
429
611
  description:
430
- 'Ranks a paste of token counts AND publishes it to the live SigRank board at signalaf.com in one call. First computes the cascade locally for an instant preview (yield, leverage, velocity, class, card), then submits the raw paste to the board\'s web-paste endpoint, which re-parses and re-scores it server-side. The server score is authoritative — it may differ from the local preview if the board applies additional validation. Returns both the local preview and the server response (including the operator\'s new rank if accepted). A codename is required to publish — omit it for a local preview only (no board submission). Token-only, no auth required. Use this when you have token counts from ccusage or a dashboard and want to both see your score and publish it. Do NOT use this if you want to pull your local usage automatically — use tokenpull_submit for the zero-paste flow. Do NOT use this for multi-window dashboard pastes — use rank_windows to rank them first, then submit each window. After calling this, use get_operator with your codename to verify your submission appeared on the board.',
431
- annotations: { ...ANNOTATIONS.destructiveHint, ...ANNOTATIONS.idempotentHint, ...ANNOTATIONS.openWorldHint },
612
+ "Ranks a paste of token counts AND publishes it to the live SigRank board at signalaf.com in one call. First computes the cascade locally for an instant preview (yield, leverage, velocity, class, card), then submits the raw paste to the board's web-paste endpoint, which re-parses and re-scores it server-side. The server score is authoritative — it may differ from the local preview if the board applies additional validation. Returns both the local preview and the server response (including the operator's new rank if accepted). A codename is required to publish — omit it for a local preview only (no board submission). Token-only, no auth required. Use this when you have token counts from ccusage or a dashboard and want to both see your score and publish it. Do NOT use this if you want to pull your local usage automatically — use tokenpull_submit for the zero-paste flow. Do NOT use this for multi-window dashboard pastes — use rank_windows to rank them first, then submit each window. After calling this, use get_operator with your codename to verify your submission appeared on the board.",
613
+ annotations: {
614
+ ...ANNOTATIONS.destructiveHint,
615
+ ...ANNOTATIONS.idempotentHint,
616
+ ...ANNOTATIONS.openWorldHint,
617
+ },
432
618
  inputSchema: {
433
- type: 'object',
619
+ type: "object",
434
620
  properties: {
435
621
  text: {
436
- type: 'string',
437
- description: 'Token counts to rank and submit. Two formats: (1) JSON {"input":N,"output":N,"cacheCreate":N,"cacheRead":N} from ccusage (preferred — the board parses this reliably), or (2) four whitespace-separated numbers: input output cacheCreate cacheRead. The 4-number form ranks locally but the board may reject it. Example: {"input":1000000,"output":500000,"cacheCreate":50000,"cacheRead":800000}',
622
+ type: "string",
623
+ description:
624
+ 'Token counts to rank and submit. Two formats: (1) JSON {"input":N,"output":N,"cacheCreate":N,"cacheRead":N} from ccusage (preferred — the board parses this reliably), or (2) four whitespace-separated numbers: input output cacheCreate cacheRead. The 4-number form ranks locally but the board may reject it. Example: {"input":1000000,"output":500000,"cacheCreate":50000,"cacheRead":800000}',
438
625
  },
439
626
  codename: {
440
- type: 'string',
441
- description: 'Operator codename to publish under on the leaderboard (e.g. "Ghost Falcon"). Required to submit — omit for local preview only (no board submission, just returns the local cascade result). Must be a non-empty string.',
627
+ type: "string",
628
+ description:
629
+ 'Operator codename to publish under on the leaderboard (e.g. "Ghost Falcon"). Required to submit — omit for local preview only (no board submission, just returns the local cascade result). Must be a non-empty string.',
442
630
  },
443
631
  },
444
- required: ['text'],
445
- description: 'Requires token counts (text). Codename is optional but required for board submission — omit it for preview-only mode.',
632
+ required: ["text"],
633
+ description:
634
+ "Requires token counts (text). Codename is optional but required for board submission — omit it for preview-only mode.",
446
635
  },
447
636
  outputSchema: SUBMIT_OUTPUT,
448
637
  },
449
638
  {
450
- name: 'tokenpull',
639
+ name: "tokenpull",
451
640
  description:
452
641
  "Pull your LOCAL token usage from the platform's session logs and rank it across the four windows (7d/30d/90d/all-time) with the cascade — zero paste. Token-only: reads usage counts not message content. The numbers stay on your machine unless you submit them. Some platforms may have partial data (estimated=true when cacheCreate isn't available) or a dataGap note when the log format doesn't expose raw token counts.",
453
642
  annotations: { ...ANNOTATIONS.readOnlyHint, ...ANNOTATIONS.openWorldHint },
454
643
  inputSchema: {
455
- type: 'object',
644
+ type: "object",
456
645
  properties: {
457
646
  platform: {
458
- type: 'string',
647
+ type: "string",
459
648
  enum: ALL_PLATFORMS,
460
- description: `source platform (default: claude). Supported: ${ALL_PLATFORMS.join(', ')}. codex is estimated via io_ratio. Some platforms need setup (e.g. copilot requires COPILOT_OTEL_ENABLED=true).`,
649
+ description: `source platform (default: claude). Supported: ${ALL_PLATFORMS.join(", ")}. codex is estimated via io_ratio. Some platforms need setup (e.g. copilot requires COPILOT_OTEL_ENABLED=true).`,
461
650
  },
462
651
  },
463
652
  },
464
653
  outputSchema: TOKENPULL_OUTPUT,
465
654
  },
466
655
  {
467
- name: 'tokenpull_submit',
656
+ name: "tokenpull_submit",
468
657
  description:
469
658
  "Pull your LOCAL token usage from session logs AND publish it to the SigRank board in one call — the zero-paste flow. Reads the four canonical pillars (input, output, cacheCreate, cacheRead) per window from your local logs, computes the cascade, and submits each window to the board where it is re-scored server-side and tagged with the source platform. Requires a codename to publish; omit for a local preview only. Token-only — no prompt content is read or transmitted.",
470
- annotations: { ...ANNOTATIONS.destructiveHint, ...ANNOTATIONS.idempotentHint, ...ANNOTATIONS.openWorldHint },
659
+ annotations: {
660
+ ...ANNOTATIONS.destructiveHint,
661
+ ...ANNOTATIONS.idempotentHint,
662
+ ...ANNOTATIONS.openWorldHint,
663
+ },
471
664
  inputSchema: {
472
- type: 'object',
665
+ type: "object",
473
666
  properties: {
474
667
  codename: {
475
- type: 'string',
476
- description: 'Operator codename to publish under on the leaderboard (e.g. "Iron Lotus"). Required to submit — omit for local preview only.',
668
+ type: "string",
669
+ description:
670
+ 'Operator codename to publish under on the leaderboard (e.g. "Iron Lotus"). Required to submit — omit for local preview only.',
477
671
  },
478
672
  window: {
479
- type: 'string',
480
- enum: ['7d', '30d', '90d', 'all'],
481
- description: 'Submit only this time window (default: all 4 windows). Use "7d" for recent activity or "all" for all-time ranking.',
673
+ type: "string",
674
+ enum: ["7d", "30d", "90d", "all"],
675
+ description:
676
+ 'Submit only this time window (default: all 4 windows). Use "7d" for recent activity or "all" for all-time ranking.',
482
677
  },
483
678
  platform: {
484
- type: 'string',
679
+ type: "string",
485
680
  enum: ALL_PLATFORMS,
486
- description: `Source platform to pull from (default: claude). Supported: ${ALL_PLATFORMS.join(', ')}. Each platform reads its own session logs locally.`,
681
+ description: `Source platform to pull from (default: claude). Supported: ${ALL_PLATFORMS.join(", ")}. Each platform reads its own session logs locally.`,
487
682
  },
488
683
  },
489
684
  },
490
685
  outputSchema: SUBMIT_OUTPUT,
491
686
  },
492
687
  {
493
- name: 'rank_windows',
688
+ name: "rank_windows",
494
689
  description:
495
- 'Rank all four time windows (7d/30d/90d/all-time) in one call from a dashboard paste — paste the full table from ccusage, tokscale, or the Claude Max usage dashboard and get the cascade (Υ, SNR, Leverage, Velocity, 10xDEV, class, card) for each window. Each window is parsed and scored independently. Named keys required (input/output/cacheCreate/cacheRead); positional order is NOT safe here (dashboards list cache_read before cache_create — see WINDOWED_PROFILES gotcha). Omit windows you don\'t have — partial input is allowed (1–4 windows). Does NOT submit to the board; use tokenpull_submit for zero-paste publishing.',
496
- annotations: { ...ANNOTATIONS.readOnlyHint, ...ANNOTATIONS.idempotentHint, ...ANNOTATIONS.openWorldHint },
690
+ "Rank all four time windows (7d/30d/90d/all-time) in one call from a dashboard paste — paste the full table from ccusage, tokscale, or the Claude Max usage dashboard and get the cascade (Υ, SNR, Leverage, Velocity, 10xDEV, class, card) for each window. Each window is parsed and scored independently. Named keys required (input/output/cacheCreate/cacheRead); positional order is NOT safe here (dashboards list cache_read before cache_create — see WINDOWED_PROFILES gotcha). Omit windows you don't have — partial input is allowed (1–4 windows). Does NOT submit to the board; use tokenpull_submit for zero-paste publishing.",
691
+ annotations: {
692
+ ...ANNOTATIONS.readOnlyHint,
693
+ ...ANNOTATIONS.idempotentHint,
694
+ ...ANNOTATIONS.openWorldHint,
695
+ },
497
696
  inputSchema: {
498
- type: 'object',
697
+ type: "object",
499
698
  properties: {
500
- '7d': { type: 'string', description: 'ccusage/tokscale paste or JSON for the 7-day window (optional)' },
501
- '30d': { type: 'string', description: 'ccusage/tokscale paste or JSON for the 30-day window (optional)' },
502
- '90d': { type: 'string', description: 'ccusage/tokscale paste or JSON for the 90-day window (optional)' },
503
- all: { type: 'string', description: 'ccusage/tokscale paste or JSON for the all-time window (optional)' },
504
- source_tool: { type: 'string', enum: ['ccusage', 'tokscale', 'claude_max', 'token_dashboard', 'other'], description: 'which token reader produced the paste (for cross-tool variance tracking)' },
699
+ "7d": {
700
+ type: "string",
701
+ description:
702
+ "ccusage/tokscale paste or JSON for the 7-day window (optional)",
703
+ },
704
+ "30d": {
705
+ type: "string",
706
+ description:
707
+ "ccusage/tokscale paste or JSON for the 30-day window (optional)",
708
+ },
709
+ "90d": {
710
+ type: "string",
711
+ description:
712
+ "ccusage/tokscale paste or JSON for the 90-day window (optional)",
713
+ },
714
+ all: {
715
+ type: "string",
716
+ description:
717
+ "ccusage/tokscale paste or JSON for the all-time window (optional)",
718
+ },
719
+ source_tool: {
720
+ type: "string",
721
+ enum: [
722
+ "ccusage",
723
+ "tokscale",
724
+ "claude_max",
725
+ "token_dashboard",
726
+ "other",
727
+ ],
728
+ description:
729
+ "which token reader produced the paste (for cross-tool variance tracking)",
730
+ },
505
731
  },
506
732
  // at least one window paste is required (runtime check backs this up)
507
- anyOf: [{ required: ['7d'] }, { required: ['30d'] }, { required: ['90d'] }, { required: ['all'] }],
733
+ anyOf: [
734
+ { required: ["7d"] },
735
+ { required: ["30d"] },
736
+ { required: ["90d"] },
737
+ { required: ["all"] },
738
+ ],
508
739
  },
509
740
  outputSchema: {
510
- type: 'object',
741
+ type: "object",
511
742
  properties: {
512
743
  windows: {
513
- type: 'array',
514
- description: 'Cascade results per window',
744
+ type: "array",
745
+ description: "Cascade results per window",
515
746
  items: {
516
- type: 'object',
747
+ type: "object",
517
748
  properties: {
518
- window: { type: 'string', enum: ['7d', '30d', '90d', 'all'] },
749
+ window: { type: "string", enum: ["7d", "30d", "90d", "all"] },
519
750
  ...CASCADE_OUTPUT.properties,
520
751
  },
521
752
  },
@@ -524,176 +755,296 @@ export const TOOLS = [
524
755
  },
525
756
  },
526
757
  {
527
- name: 'watch_tokenpull',
758
+ name: "watch_tokenpull",
528
759
  description:
529
- 'One poll per call: pulls your local token logs and returns the current cascade for the watched window — the tool never blocks or loops. Re-call at your desired cadence to watch for changes (interval_s is advisory only and echoed back as poll_interval_s). With submit:true (and an enrolled device) each call may also sign + publish the watched window to the board, rate-limited to once per 5 min per platform+window; default is preview-only (no submit).',
530
- annotations: { ...ANNOTATIONS.readOnlyHint, ...ANNOTATIONS.idempotentHint, ...ANNOTATIONS.openWorldHint },
760
+ "One poll per call: pulls your local token logs and returns the current cascade for the watched window — the tool never blocks or loops. Re-call at your desired cadence to watch for changes (interval_s is advisory only and echoed back as poll_interval_s). With submit:true (and an enrolled device) each call may also sign + publish the watched window to the board, rate-limited to once per 5 min per platform+window; default is preview-only (no submit).",
761
+ annotations: {
762
+ ...ANNOTATIONS.idempotentHint,
763
+ ...ANNOTATIONS.openWorldHint,
764
+ },
531
765
  inputSchema: {
532
- type: 'object',
766
+ type: "object",
533
767
  properties: {
534
- platform: { type: 'string', enum: ALL_PLATFORMS, description: 'platform to watch (default: claude)' },
535
- interval_s: { type: 'number', description: 'advisory poll cadence in seconds (default: 60, min: 10) — echoed back as poll_interval_s; does not make the call block or loop' },
536
- window: { type: 'string', enum: ['7d', '30d', '90d', 'all'], description: 'which window to watch (default: 7d — most sensitive to recent activity)' },
537
- submit: { type: 'boolean', description: 'auto-submit the watched window to the board as a VERIFIED operator each poll (requires `enroll`; default false = preview only)' },
768
+ platform: {
769
+ type: "string",
770
+ enum: ALL_PLATFORMS,
771
+ description: "platform to watch (default: claude)",
772
+ },
773
+ interval_s: {
774
+ type: "number",
775
+ description:
776
+ "advisory poll cadence in seconds (default: 60, min: 10) — echoed back as poll_interval_s; does not make the call block or loop",
777
+ },
778
+ window: {
779
+ type: "string",
780
+ enum: ["7d", "30d", "90d", "all"],
781
+ description:
782
+ "which window to watch (default: 7d — most sensitive to recent activity)",
783
+ },
784
+ submit: {
785
+ type: "boolean",
786
+ description:
787
+ "auto-submit the watched window to the board as a VERIFIED operator each poll (requires `enroll`; default false = preview only)",
788
+ },
538
789
  },
539
790
  },
540
791
  outputSchema: TOKENPULL_OUTPUT,
541
792
  },
542
793
  {
543
- name: 'tokenpull_compare',
794
+ name: "tokenpull_compare",
544
795
  description:
545
- 'Pull token usage from ALL four local sources in parallel — tokenpull (JSONL canon), ccusage CLI, token-dashboard SQLite, and tokscale report — and return them side-by-side with delta % vs tokenpull as the baseline. Also computes the cascade (Υ, SNR, Leverage, class) for each source so you can see how each verifier scores. Useful for validating your numbers before submitting, or understanding discrepancies between tools. Claude only for token-dash; codex and others use tokenpull + ccusage + tokscale. Token-only, on-device.',
796
+ "Pull token usage from ALL four local sources in parallel — tokenpull (JSONL canon), ccusage CLI, token-dashboard SQLite, and tokscale report — and return them side-by-side with delta % vs tokenpull as the baseline. Also computes the cascade (Υ, SNR, Leverage, class) for each source so you can see how each verifier scores. Useful for validating your numbers before submitting, or understanding discrepancies between tools. Claude only for token-dash; codex and others use tokenpull + ccusage + tokscale. Token-only, on-device.",
546
797
  annotations: { ...ANNOTATIONS.readOnlyHint, ...ANNOTATIONS.openWorldHint },
547
798
  inputSchema: {
548
- type: 'object',
799
+ type: "object",
549
800
  properties: {
550
801
  platform: {
551
- type: 'string',
802
+ type: "string",
552
803
  enum: ALL_PLATFORMS,
553
- description: 'platform to compare (default: claude). token-dash and App only available for claude.',
804
+ description:
805
+ "platform to compare (default: claude). token-dash and App only available for claude.",
554
806
  },
555
807
  },
556
808
  },
557
809
  outputSchema: COMPARE_OUTPUT,
558
810
  },
559
811
  {
560
- name: 'enroll',
812
+ name: "enroll",
561
813
  description:
562
814
  'Bind THIS device to your SigRank operator so your signed token runs cascade to the live board. Paste the key from signalaf.com → Settings → "New key" (or "Generate connect code"). On first run it generates + stores a local ed25519 keypair (~/.sigrank-mcp/identity.json); only the PUBLIC key is ever sent. Need a new key? Click "New key" at signalaf.com → Settings, then paste it here.',
563
- annotations: { ...ANNOTATIONS.destructiveHint, ...ANNOTATIONS.idempotentHint, ...ANNOTATIONS.openWorldHint },
815
+ annotations: {
816
+ ...ANNOTATIONS.destructiveHint,
817
+ ...ANNOTATIONS.idempotentHint,
818
+ ...ANNOTATIONS.openWorldHint,
819
+ },
564
820
  inputSchema: {
565
- type: 'object',
821
+ type: "object",
566
822
  properties: {
567
- code: { type: 'string', description: 'the key / connect code (SIGR-XXXXX-XXXXX-XXXXX) from Settings → New key (or Generate connect code)' },
568
- device_label: { type: 'string', description: 'optional label for this device (default: hostname · agent version)' },
823
+ code: {
824
+ type: "string",
825
+ description:
826
+ "the key / connect code (SIGR-XXXXX-XXXXX-XXXXX) from Settings → New key (or Generate connect code)",
827
+ },
828
+ device_label: {
829
+ type: "string",
830
+ description:
831
+ "optional label for this device (default: hostname · agent version)",
832
+ },
569
833
  },
570
- required: ['code'],
834
+ required: ["code"],
571
835
  },
572
836
  outputSchema: ENROLL_OUTPUT,
573
837
  },
574
838
  {
575
- name: 'submit_verified',
839
+ name: "submit_verified",
576
840
  description:
577
- 'Publish your LOCAL token runs to the SigRank board as a VERIFIED operator — the enrolled, signed path. Reads your pillars (tokenpull), builds the canonical Schema 1.0 snapshot per window, ed25519-signs it with your device key, and POSTs to /api/v1/snapshots. Requires `npx sigrank-mcp enroll` first (a bound device). Only signed submissions from a trusted device rank on the board. Token-only; the private key never leaves your machine. Pass dry_run:true to inspect the exact signed payload without publishing.',
578
- annotations: { ...ANNOTATIONS.destructiveHint, ...ANNOTATIONS.idempotentHint, ...ANNOTATIONS.openWorldHint },
841
+ "Publish your LOCAL token runs to the SigRank board as a VERIFIED operator — the enrolled, signed path. Reads your pillars (tokenpull), builds the canonical Schema 1.0 snapshot per window, ed25519-signs it with your device key, and POSTs to /api/v1/snapshots. Requires `npx sigrank-mcp enroll` first (a bound device). Only signed submissions from a trusted device rank on the board. Token-only; the private key never leaves your machine. Pass dry_run:true to inspect the exact signed payload without publishing.",
842
+ annotations: {
843
+ ...ANNOTATIONS.destructiveHint,
844
+ ...ANNOTATIONS.idempotentHint,
845
+ ...ANNOTATIONS.openWorldHint,
846
+ },
579
847
  inputSchema: {
580
- type: 'object',
848
+ type: "object",
581
849
  properties: {
582
- window: { type: 'string', enum: ['7d', '30d', '90d', 'all'], description: 'submit only this window (default: all 4)' },
583
- platform: { type: 'string', enum: [...ALL_PLATFORMS, 'multi'], description: "source platform (default: claude). 'multi' = combined cascade summed across all locally-detected platforms (needs 2+ active); empty windows are skipped." },
584
- dry_run: { type: 'boolean', description: 'build + sign but do NOT publish — returns the exact payload that would be POSTed (token counts only), so you can inspect before submitting' },
850
+ window: {
851
+ type: "string",
852
+ enum: ["7d", "30d", "90d", "all"],
853
+ description: "submit only this window (default: all 4)",
854
+ },
855
+ platform: {
856
+ type: "string",
857
+ enum: [...ALL_PLATFORMS, "multi"],
858
+ description:
859
+ "source platform (default: claude). 'multi' = combined cascade summed across all locally-detected platforms (needs 2+ active); empty windows are skipped.",
860
+ },
861
+ dry_run: {
862
+ type: "boolean",
863
+ description:
864
+ "build + sign but do NOT publish — returns the exact payload that would be POSTed (token counts only), so you can inspect before submitting",
865
+ },
585
866
  },
586
867
  },
587
868
  outputSchema: SUBMIT_OUTPUT,
588
869
  },
589
870
  {
590
- name: 'simulate_change',
871
+ name: "simulate_change",
591
872
  description:
592
873
  "The first PRESCRIPTIVE SigRank tool — 'what if I changed my token mix?' Takes your current 4 pillars (input/output/cacheCreate/cacheRead) and one or more proposed changes, runs the canonical cascade on BOTH the current and simulated values, and returns the exact Υ Yield delta, class change, and per-metric diffs. This is the 'show me the payoff before I do the work' primitive: no network, no submission, pure local math. Use it to answer 'would increasing my cache-read by 50k tokens actually move my class?' before you change your workflow. Accepts the current pillars as JSON or 4 numbers (same as rank_paste) plus a `changes` object with any of the 4 pillar names mapped to new absolute values OR relative deltas (e.g. {cacheRead: '+50000'} or {input: 800000}).",
593
- annotations: { ...ANNOTATIONS.readOnlyHint, ...ANNOTATIONS.idempotentHint, ...ANNOTATIONS.openWorldHint },
874
+ annotations: {
875
+ ...ANNOTATIONS.readOnlyHint,
876
+ ...ANNOTATIONS.idempotentHint,
877
+ ...ANNOTATIONS.openWorldHint,
878
+ },
594
879
  inputSchema: {
595
- type: 'object',
880
+ type: "object",
596
881
  properties: {
597
882
  text: {
598
- type: 'string',
599
- description: 'Current token pillars — ccusage JSON or "input output cacheCreate cacheRead" (same format as rank_paste).',
883
+ type: "string",
884
+ description:
885
+ 'Current token pillars — ccusage JSON or "input output cacheCreate cacheRead" (same format as rank_paste).',
600
886
  },
601
887
  changes: {
602
- type: 'object',
603
- description: 'Proposed changes to apply. Keys: input, output, cacheCreate, cacheRead. Values are either absolute numbers (replace) or strings starting with +/- for relative deltas (add/subtract). Omitted pillars are unchanged.',
888
+ type: "object",
889
+ description:
890
+ "Proposed changes to apply. Keys: input, output, cacheCreate, cacheRead. Values are either absolute numbers (replace) or strings starting with +/- for relative deltas (add/subtract). Omitted pillars are unchanged.",
604
891
  properties: {
605
- input: { type: ['number', 'string'], description: 'new input token count (absolute) or "+/-N" for a relative delta' },
606
- output: { type: ['number', 'string'], description: 'new output token count (absolute) or "+/-N" for a relative delta' },
607
- cacheCreate: { type: ['number', 'string'], description: 'new cache-create token count (absolute) or "+/-N" for a relative delta' },
608
- cacheRead: { type: ['number', 'string'], description: 'new cache-read token count (absolute) or "+/-N" for a relative delta' },
892
+ input: {
893
+ type: ["number", "string"],
894
+ description:
895
+ 'new input token count (absolute) or "+/-N" for a relative delta',
896
+ },
897
+ output: {
898
+ type: ["number", "string"],
899
+ description:
900
+ 'new output token count (absolute) or "+/-N" for a relative delta',
901
+ },
902
+ cacheCreate: {
903
+ type: ["number", "string"],
904
+ description:
905
+ 'new cache-create token count (absolute) or "+/-N" for a relative delta',
906
+ },
907
+ cacheRead: {
908
+ type: ["number", "string"],
909
+ description:
910
+ 'new cache-read token count (absolute) or "+/-N" for a relative delta',
911
+ },
609
912
  },
610
913
  },
611
914
  },
612
- required: ['text', 'changes'],
915
+ required: ["text", "changes"],
613
916
  },
614
917
  outputSchema: SIMULATE_OUTPUT,
615
918
  },
616
919
  {
617
- name: 'diagnose_cascade',
920
+ name: "diagnose_cascade",
618
921
  description:
619
922
  "Analyzes your token cascade and diagnoses where you're leaking efficiency. Takes your 4 pillars (input/output/cacheCreate/cacheRead) and produces a ranked list of efficiency leaks with severity (critical/warning/info), findings, and recommendations. Checks: cache leverage (are you rereading what you wrote?), velocity (are you generating enough output per input?), SNR (is your signal drowning in noise?), cache creation ratio (are you over-committing?), input bloat (is fresh input too high?), and 10xDEV (is the full cascade compounding?). Each finding includes an estimated Υ impact. Pure local math — no network, no submission. Use this BEFORE simulate_change to understand what's wrong, then use simulate_change to test fixes. Accepts the same input formats as rank_paste (JSON or 4 whitespace numbers).",
620
- annotations: { ...ANNOTATIONS.readOnlyHint, ...ANNOTATIONS.idempotentHint, ...ANNOTATIONS.openWorldHint },
923
+ annotations: {
924
+ ...ANNOTATIONS.readOnlyHint,
925
+ ...ANNOTATIONS.idempotentHint,
926
+ ...ANNOTATIONS.openWorldHint,
927
+ },
621
928
  inputSchema: {
622
- type: 'object',
929
+ type: "object",
623
930
  properties: {
624
931
  text: {
625
- type: 'string',
626
- description: 'Token pillars — ccusage JSON or "input output cacheCreate cacheRead" (same format as rank_paste).',
932
+ type: "string",
933
+ description:
934
+ 'Token pillars — ccusage JSON or "input output cacheCreate cacheRead" (same format as rank_paste).',
627
935
  },
628
936
  },
629
- required: ['text'],
937
+ required: ["text"],
630
938
  },
631
939
  outputSchema: DIAGNOSE_OUTPUT,
632
940
  },
633
941
  {
634
- name: 'suggest_improvements',
942
+ name: "suggest_improvements",
635
943
  description:
636
944
  "Generates ranked, simulated improvement suggestions for your token cascade. Takes your 4 pillars, tests multiple improvement strategies (increase cache reads, reduce fresh input, increase output, optimize cache creation), simulates each with the canonical cascade engine, and returns them ranked by Υ yield impact. Each suggestion includes: the action, which pillar to change, how much to change it, the projected Υ after the change, the yield delta, the projected class tier, and a rationale. Also returns the single highest-impact change (best_single_change). Pure local math — no network, no submission. Use this after diagnose_cascade to get actionable next steps, then use simulate_change to fine-tune before committing. Accepts the same input formats as rank_paste.",
637
- annotations: { ...ANNOTATIONS.readOnlyHint, ...ANNOTATIONS.idempotentHint, ...ANNOTATIONS.openWorldHint },
945
+ annotations: {
946
+ ...ANNOTATIONS.readOnlyHint,
947
+ ...ANNOTATIONS.idempotentHint,
948
+ ...ANNOTATIONS.openWorldHint,
949
+ },
638
950
  inputSchema: {
639
- type: 'object',
951
+ type: "object",
640
952
  properties: {
641
953
  text: {
642
- type: 'string',
643
- description: 'Token pillars — ccusage JSON or "input output cacheCreate cacheRead" (same format as rank_paste).',
954
+ type: "string",
955
+ description:
956
+ 'Token pillars — ccusage JSON or "input output cacheCreate cacheRead" (same format as rank_paste).',
644
957
  },
645
958
  },
646
- required: ['text'],
959
+ required: ["text"],
647
960
  },
648
961
  outputSchema: SUGGEST_OUTPUT,
649
962
  },
650
963
  {
651
- name: 'self_improve',
964
+ name: "self_improve",
652
965
  description:
653
966
  "Runs the full self-improvement cycle in one call: (1) gets your current token pillars — either from the provided text or by running tokenpull on your local logs, (2) diagnoses where you're leaking efficiency (diagnose_cascade), (3) generates ranked improvement suggestions (suggest_improvements), (4) simulates the top suggestion (simulate_change), and (5) returns the complete cycle: diagnosis + suggestions + the simulated impact of the best change. This is the 'one-click optimize' tool — call it at the end of a session to see what to improve next time. If you provide pillars in `text`, it skips the tokenpull step. If you omit `text`, it runs tokenpull first (requires local ccusage logs). Pure local math — no network, no submission. The `scope` parameter adds mode detection (BUILD/EDIT/DEBUG/MAINTAIN/IDLE) and scoped analysis: 'daily' (default — current behavior + mode), 'weekly' (compound into weekly snapshots + report artifact), 'trend' (30d/90d trajectory analysis).",
654
- annotations: { ...ANNOTATIONS.readOnlyHint, ...ANNOTATIONS.idempotentHint, ...ANNOTATIONS.openWorldHint },
967
+ annotations: {
968
+ ...ANNOTATIONS.readOnlyHint,
969
+ ...ANNOTATIONS.idempotentHint,
970
+ ...ANNOTATIONS.openWorldHint,
971
+ },
655
972
  inputSchema: {
656
- type: 'object',
973
+ type: "object",
657
974
  properties: {
658
975
  text: {
659
- type: 'string',
660
- description: 'Optional: token pillars — ccusage JSON or "input output cacheCreate cacheRead". If omitted, runs tokenpull to get current pillars from local logs.',
976
+ type: "string",
977
+ description:
978
+ 'Optional: token pillars — ccusage JSON or "input output cacheCreate cacheRead". If omitted, runs tokenpull to get current pillars from local logs.',
661
979
  },
662
980
  window: {
663
- type: 'string',
664
- enum: ['7d', '30d', '90d', 'all'],
665
- description: 'Which time window to pull when running tokenpull (default: 30d). Ignored if `text` is provided.',
981
+ type: "string",
982
+ enum: ["7d", "30d", "90d", "all"],
983
+ description:
984
+ "Which time window to pull when running tokenpull (default: 30d). Ignored if `text` is provided.",
666
985
  },
667
986
  scope: {
668
- type: 'string',
669
- enum: ['daily', 'weekly', 'trend'],
670
- description: 'Analysis scope: "daily" (default — current behavior + mode detection), "weekly" (compound daily rows into weekly snapshots + report artifact with badges), "trend" (30d/90d trajectory + phase patterns). Daily modes never leave the machine — only weekly distribution goes in submitted reports.',
987
+ type: "string",
988
+ enum: ["daily", "weekly", "trend"],
989
+ description:
990
+ 'Analysis scope: "daily" (default — current behavior + mode detection), "weekly" (compound daily rows into weekly snapshots + report artifact with badges), "trend" (30d/90d trajectory + phase patterns). Daily modes never leave the machine — only weekly distribution goes in submitted reports.',
671
991
  },
672
992
  },
673
993
  },
674
994
  outputSchema: {
675
- type: 'object',
995
+ type: "object",
676
996
  properties: {
677
- pillars: { type: 'object', description: 'The 4 raw token pillars used' },
997
+ pillars: {
998
+ type: "object",
999
+ description: "The 4 raw token pillars used",
1000
+ },
678
1001
  current_cascade: CASCADE_OUTPUT,
679
- diagnosis: { type: 'array', description: 'Efficiency leaks found (from diagnose_cascade)' },
680
- suggestions: { type: 'array', description: 'Ranked improvements (from suggest_improvements)' },
681
- best_simulation: { type: 'object', description: 'Simulated result of the top suggestion' },
682
- cycle_summary: { type: 'string', description: 'One-line summary of the full cycle' },
1002
+ diagnosis: {
1003
+ type: "array",
1004
+ description: "Efficiency leaks found (from diagnose_cascade)",
1005
+ },
1006
+ suggestions: {
1007
+ type: "array",
1008
+ description: "Ranked improvements (from suggest_improvements)",
1009
+ },
1010
+ best_simulation: {
1011
+ type: "object",
1012
+ description: "Simulated result of the top suggestion",
1013
+ },
1014
+ cycle_summary: {
1015
+ type: "string",
1016
+ description: "One-line summary of the full cycle",
1017
+ },
683
1018
  // Scope-specific fields
684
- mode: { type: 'object', description: 'Detected mode { mode, confidence } — present when scope is daily/weekly/trend' },
685
- quality_score: { type: 'number', description: 'Yield relative to mode expectation (daily scope)' },
686
- assessment: { type: 'string', description: 'One-line assessment for daily scope' },
687
- advice: { type: 'string', description: 'Advice for next session (daily scope)' },
688
- report: { type: 'object', description: 'Weekly report artifact (weekly scope)' },
689
- trend: { type: 'object', description: 'Trend analysis (trend scope)' },
1019
+ mode: {
1020
+ type: "object",
1021
+ description:
1022
+ "Detected mode { mode, confidence } — present when scope is daily/weekly/trend",
1023
+ },
1024
+ quality_score: {
1025
+ type: "number",
1026
+ description: "Yield relative to mode expectation (daily scope)",
1027
+ },
1028
+ assessment: {
1029
+ type: "string",
1030
+ description: "One-line assessment for daily scope",
1031
+ },
1032
+ advice: {
1033
+ type: "string",
1034
+ description: "Advice for next session (daily scope)",
1035
+ },
1036
+ report: {
1037
+ type: "object",
1038
+ description: "Weekly report artifact (weekly scope)",
1039
+ },
1040
+ trend: { type: "object", description: "Trend analysis (trend scope)" },
690
1041
  },
691
1042
  },
692
1043
  },
693
- ]
1044
+ ];
694
1045
 
695
1046
  // tokenpull window key → the board's window_type enum.
696
- const WINDOW_TYPE = { '7d': '7d', '30d': '30d', '90d': '90d', all: 'all_time' }
1047
+ const WINDOW_TYPE = { "7d": "7d", "30d": "30d", "90d": "90d", all: "all_time" };
697
1048
 
698
1049
  // E3: client-side auto-submit cooldown for watch_tokenpull. The server already dedups
699
1050
  // identical snapshots (exact hash → 422), but a noisy poll loop with submit:true could still
@@ -701,8 +1052,8 @@ const WINDOW_TYPE = { '7d': '7d', '30d': '30d', '90d': '90d', all: 'all_time' }
701
1052
  // per platform+window (in-memory, per process). Keyed by platform:window so different
702
1053
  // platforms/windows don't block each other, and only armed on a non-error submit so a
703
1054
  // network failure doesn't lock out retries for 5 minutes.
704
- const WATCH_SUBMIT_COOLDOWN_MS = 5 * 60 * 1000
705
- const _lastWatchSubmitAt = new Map()
1055
+ const WATCH_SUBMIT_COOLDOWN_MS = 5 * 60 * 1000;
1056
+ const _lastWatchSubmitAt = new Map();
706
1057
 
707
1058
  // ── Shared active-platform loader ───────────────────────────────────────────
708
1059
  // THE single data path for "show my cascade across platforms" — used by `me`,
@@ -711,73 +1062,105 @@ const _lastWatchSubmitAt = new Map()
711
1062
  // keeps only platforms with real local data, and sorts claude → codex → rest.
712
1063
  // Pass `platforms` to scope it (e.g. ['claude'] for a fast first paint).
713
1064
  export async function pullActivePlatforms({ platforms } = {}, opts = {}) {
714
- const targets = platforms && platforms.length ? platforms : ALL_PLATFORMS
715
- const settled = await Promise.allSettled(targets.map((p) => callTool('tokenpull', { platform: p }, opts)))
1065
+ const targets = platforms && platforms.length ? platforms : ALL_PLATFORMS;
1066
+ const settled = await Promise.allSettled(
1067
+ targets.map((p) => callTool("tokenpull", { platform: p }, opts)),
1068
+ );
716
1069
  const active = settled
717
- .filter((r) => r.status === 'fulfilled' && r.value)
1070
+ .filter((r) => r.status === "fulfilled" && r.value)
718
1071
  .map((r) => r.value)
719
- .filter((d) => (d.windows || []).some((w) => ((w.pillars?.input ?? 0) + (w.pillars?.output ?? 0)) > 0))
720
- const rank = (p) => (p === 'claude' ? -2 : p === 'codex' ? -1 : ALL_PLATFORMS.indexOf(p))
721
- active.sort((a, b) => rank(a.platform) - rank(b.platform))
722
- return active
1072
+ .filter((d) =>
1073
+ (d.windows || []).some(
1074
+ (w) => (w.pillars?.input ?? 0) + (w.pillars?.output ?? 0) > 0,
1075
+ ),
1076
+ );
1077
+ const rank = (p) =>
1078
+ p === "claude" ? -2 : p === "codex" ? -1 : ALL_PLATFORMS.indexOf(p);
1079
+ active.sort((a, b) => rank(a.platform) - rank(b.platform));
1080
+ return active;
723
1081
  }
724
1082
 
725
1083
  export async function callTool(name, args, opts = {}) {
726
- const apiBase = opts.apiBase || DEFAULT_API_BASE
727
- const timeoutMs = opts.fetchTimeout ?? DEFAULT_FETCH_TIMEOUT
728
- const rawFetch = opts.fetchImpl || fetch
1084
+ const apiBase = opts.apiBase || DEFAULT_API_BASE;
1085
+ const timeoutMs = opts.fetchTimeout ?? DEFAULT_FETCH_TIMEOUT;
1086
+ const rawFetch = opts.fetchImpl || fetch;
729
1087
 
730
1088
  // Wrap every fetch with an AbortController timeout so a hung network call never
731
1089
  // blocks the MCP client indefinitely.
732
1090
  const doFetch = (url, init = {}) => {
733
- const ac = new AbortController()
734
- const timer = setTimeout(() => ac.abort(), timeoutMs)
735
- return rawFetch(url, { ...init, signal: ac.signal }).finally(() => clearTimeout(timer))
736
- }
1091
+ const ac = new AbortController();
1092
+ const timer = setTimeout(() => ac.abort(), timeoutMs);
1093
+ return rawFetch(url, { ...init, signal: ac.signal }).finally(() =>
1094
+ clearTimeout(timer),
1095
+ );
1096
+ };
737
1097
 
738
1098
  const fetchJson = async (path) => {
739
- const res = await doFetch(`${apiBase}${path}`, { headers: { accept: 'application/json' } })
740
- if (!res.ok) throw new Error(`SigRank API ${path} → HTTP ${res.status}`)
741
- return res.json()
742
- }
1099
+ const res = await doFetch(`${apiBase}${path}`, {
1100
+ headers: { accept: "application/json" },
1101
+ });
1102
+ if (!res.ok) throw new Error(`SigRank API ${path} → HTTP ${res.status}`);
1103
+ return res.json();
1104
+ };
743
1105
 
744
1106
  // Helper: attach _parseWarnings from pillars onto the cascade result so they
745
1107
  // are always visible in the tool output for review.
746
1108
  const withParseWarnings = (pillars, cascadeResult) => {
747
1109
  if (pillars._parseWarnings && pillars._parseWarnings.length > 0) {
748
- const existing = cascadeResult.warnings || []
749
- return { ...cascadeResult, warnings: [...existing, ...pillars._parseWarnings.map((w) => `parse:${w}`)] }
1110
+ const existing = cascadeResult.warnings || [];
1111
+ return {
1112
+ ...cascadeResult,
1113
+ warnings: [
1114
+ ...existing,
1115
+ ...pillars._parseWarnings.map((w) => `parse:${w}`),
1116
+ ],
1117
+ };
750
1118
  }
751
- return cascadeResult
752
- }
1119
+ return cascadeResult;
1120
+ };
753
1121
 
754
- if (name === 'rank_paste') {
755
- if (!args?.text) throw new Error('rank_paste requires a non-empty `text` argument.')
1122
+ if (name === "rank_paste") {
1123
+ if (!args?.text)
1124
+ throw new Error("rank_paste requires a non-empty `text` argument.");
756
1125
  // E2: reject oversized pastes before parsing (parity with submit_paste / rank_windows).
757
- if (typeof args.text === 'string' && args.text.length > MAX_INPUT) {
758
- return { status: 'error', reason: 'input_too_large', detail: `text exceeds ${MAX_INPUT} chars (${args.text.length}). Paste only the token-count table, not full output.` }
1126
+ if (typeof args.text === "string" && args.text.length > MAX_INPUT) {
1127
+ return {
1128
+ status: "error",
1129
+ reason: "input_too_large",
1130
+ detail: `text exceeds ${MAX_INPUT} chars (${args.text.length}). Paste only the token-count table, not full output.`,
1131
+ };
759
1132
  }
760
- const pillars = parsePillars(args.text)
761
- const c = withParseWarnings(pillars, cascade(pillars))
762
- return { ...c, card: narrate(c) }
1133
+ const pillars = parsePillars(args.text);
1134
+ const c = withParseWarnings(pillars, cascade(pillars));
1135
+ return { ...c, card: narrate(c) };
763
1136
  }
764
- if (name === 'get_leaderboard') return fetchJson('/api/v1/leaderboard?metric=yield_')
765
- if (name === 'get_operator') {
766
- const codename = String(args?.codename || '').trim()
767
- if (!codename) throw new Error('get_operator requires a non-empty `codename` argument.')
768
- return fetchJson(`/api/v1/operators/${encodeURIComponent(codename)}`)
1137
+ if (name === "get_leaderboard")
1138
+ return fetchJson("/api/v1/leaderboard?metric=yield_");
1139
+ if (name === "get_operator") {
1140
+ const codename = String(args?.codename || "").trim();
1141
+ if (!codename)
1142
+ throw new Error("get_operator requires a non-empty `codename` argument.");
1143
+ return fetchJson(`/api/v1/operators/${encodeURIComponent(codename)}`);
769
1144
  }
770
1145
 
771
- if (name === 'enroll') {
1146
+ if (name === "enroll") {
772
1147
  // Redeem a web connect code → bind this device. Generates/loads the local keypair;
773
1148
  // sends ONLY the public key. operator binding happens server-side from the code row.
774
- const code = String(args?.code || '').trim()
775
- if (!code) throw new Error('enroll requires a `code` — paste your connect code from signalaf.com → Settings → Connect a device.')
776
- const id = opts.identity || ensureIdentity()
777
- const deviceLabel = String(args?.device_label || `${os.hostname()} · ${id.agent_version}`).slice(0, 120)
1149
+ const code = String(args?.code || "").trim();
1150
+ if (!code)
1151
+ throw new Error(
1152
+ "enroll requires a `code` — paste your connect code from signalaf.com → Settings → Connect a device.",
1153
+ );
1154
+ const id = opts.identity || ensureIdentity();
1155
+ const deviceLabel = String(
1156
+ args?.device_label || `${os.hostname()} · ${id.agent_version}`,
1157
+ ).slice(0, 120);
778
1158
  const res = await doFetch(`${apiBase}/api/v1/devices/enroll`, {
779
- method: 'POST',
780
- headers: { 'content-type': 'application/json', accept: 'application/json' },
1159
+ method: "POST",
1160
+ headers: {
1161
+ "content-type": "application/json",
1162
+ accept: "application/json",
1163
+ },
781
1164
  body: JSON.stringify({
782
1165
  code,
783
1166
  device_id: id.device_id,
@@ -785,82 +1168,126 @@ export async function callTool(name, args, opts = {}) {
785
1168
  device_label: deviceLabel,
786
1169
  agent_version: id.agent_version,
787
1170
  }),
788
- })
789
- let ack
790
- try { ack = await res.json() } catch { ack = {} }
791
- if (res.status === 201 && ack.status === 'enrolled') {
1171
+ });
1172
+ let ack;
1173
+ try {
1174
+ ack = await res.json();
1175
+ } catch {
1176
+ ack = {};
1177
+ }
1178
+ if (res.status === 201 && ack.status === "enrolled") {
792
1179
  // Persist the binding locally (skipped when a test injects opts.identity → no keystore write).
793
- if (!opts.identity) recordEnrollment({ codename: ack.codename, operator_id: ack.operator_id })
1180
+ if (!opts.identity)
1181
+ recordEnrollment({
1182
+ codename: ack.codename,
1183
+ operator_id: ack.operator_id,
1184
+ });
794
1185
  return {
795
- status: 'enrolled',
1186
+ status: "enrolled",
796
1187
  codename: ack.codename ?? null,
797
1188
  operator_id: ack.operator_id ?? null,
798
1189
  device_id: id.device_id,
799
- trust_status: ack.trust_status ?? 'trusted',
800
- }
1190
+ trust_status: ack.trust_status ?? "trusted",
1191
+ };
801
1192
  }
802
- return { status: 'error', httpStatus: res.status, reason: ack.reason || ack.status || `http_${res.status}`, detail: ack.detail ?? null }
1193
+ return {
1194
+ status: "error",
1195
+ httpStatus: res.status,
1196
+ reason: ack.reason || ack.status || `http_${res.status}`,
1197
+ detail: ack.detail ?? null,
1198
+ };
803
1199
  }
804
1200
 
805
- if (name === 'submit_verified') {
1201
+ if (name === "submit_verified") {
806
1202
  // The enrolled, signed publish path → /api/v1/snapshots (only verified rows rank).
807
- const id = opts.identity || ensureIdentity()
1203
+ const id = opts.identity || ensureIdentity();
808
1204
  if (!id.codename || !id.operator_id) {
809
- return { status: 'not_enrolled', detail: 'Run `npx sigrank-mcp enroll` to bind this device first.' }
1205
+ return {
1206
+ status: "not_enrolled",
1207
+ detail: "Run `npx sigrank-mcp enroll` to bind this device first.",
1208
+ };
810
1209
  }
811
- const platform = args?.platform || 'claude'
1210
+ const platform = args?.platform || "claude";
812
1211
 
813
1212
  // MULTI: the combined cross-platform cascade. The dashboard already SUMS every
814
1213
  // active platform's pillars (a "claude+codex" row) but never submitted it — this
815
1214
  // is that missing submission. Aggregate every locally-detected platform's pillars
816
1215
  // per window and publish as platform='multi' = the operator's TOTAL usage. Empty
817
1216
  // windows are skipped so a no-activity window never lands as a degenerate row.
818
- if (platform === 'multi') {
819
- const detected = []
1217
+ if (platform === "multi") {
1218
+ const detected = [];
820
1219
  for (const p of ALL_PLATFORMS) {
821
- const r = await pullByPlatform(p, opts).catch(() => null)
822
- const live = r && (r.windows || []).some(
823
- (w) => (w.pillars.input + w.pillars.output + w.pillars.cacheCreate + w.pillars.cacheRead) > 0,
824
- )
825
- if (live) detected.push(r)
1220
+ const r = await pullByPlatform(p, opts).catch(() => null);
1221
+ const live =
1222
+ r &&
1223
+ (r.windows || []).some(
1224
+ (w) =>
1225
+ w.pillars.input +
1226
+ w.pillars.output +
1227
+ w.pillars.cacheCreate +
1228
+ w.pillars.cacheRead >
1229
+ 0,
1230
+ );
1231
+ if (live) detected.push(r);
826
1232
  }
827
1233
  if (detected.length < 2) {
828
1234
  return {
829
- platform: 'multi', codename: id.codename, operator_id: id.operator_id,
830
- status: 'skipped', reason: 'need_2_platforms',
831
- detail: `multi needs 2+ active platforms; found ${detected.length} (${detected.map((d) => d.platform).join(', ') || 'none'}).`,
1235
+ platform: "multi",
1236
+ codename: id.codename,
1237
+ operator_id: id.operator_id,
1238
+ status: "skipped",
1239
+ reason: "need_2_platforms",
1240
+ detail: `multi needs 2+ active platforms; found ${detected.length} (${detected.map((d) => d.platform).join(", ") || "none"}).`,
832
1241
  windows: [],
833
- }
1242
+ };
834
1243
  }
835
- const winKeys = args?.window ? [args.window] : ['7d', '30d', '90d', 'all']
836
- const out = []
1244
+ const winKeys = args?.window
1245
+ ? [args.window]
1246
+ : ["7d", "30d", "90d", "all"];
1247
+ const out = [];
837
1248
  for (const wk of winKeys) {
838
- const sum = { input: 0, output: 0, cacheCreate: 0, cacheRead: 0 }
839
- let msgs = 0
1249
+ const sum = { input: 0, output: 0, cacheCreate: 0, cacheRead: 0 };
1250
+ let msgs = 0;
840
1251
  for (const d of detected) {
841
- const w = (d.windows || []).find((x) => x.window === wk)
842
- if (!w) continue
843
- sum.input += w.pillars.input || 0
844
- sum.output += w.pillars.output || 0
845
- sum.cacheCreate += w.pillars.cacheCreate || 0
846
- sum.cacheRead += w.pillars.cacheRead || 0
847
- msgs += w.messages || 0
1252
+ const w = (d.windows || []).find((x) => x.window === wk);
1253
+ if (!w) continue;
1254
+ sum.input += w.pillars.input || 0;
1255
+ sum.output += w.pillars.output || 0;
1256
+ sum.cacheCreate += w.pillars.cacheCreate || 0;
1257
+ sum.cacheRead += w.pillars.cacheRead || 0;
1258
+ msgs += w.messages || 0;
848
1259
  }
849
- if (sum.input + sum.output + sum.cacheCreate + sum.cacheRead <= 0) continue // skip empty window
850
- const report = _computeReportBlock(sum)
851
- const r = await submitSignedWindow(wk, sum, msgs, id, { apiBase, fetchImpl: doFetch, platform: 'multi', now: opts.now, dryRun: !!args?.dry_run, report })
852
- out.push({ window: wk, pillars: sum, ...r })
1260
+ if (sum.input + sum.output + sum.cacheCreate + sum.cacheRead <= 0)
1261
+ continue; // skip empty window
1262
+ const report = _computeReportBlock(sum);
1263
+ const r = await submitSignedWindow(wk, sum, msgs, id, {
1264
+ apiBase,
1265
+ fetchImpl: doFetch,
1266
+ platform: "multi",
1267
+ now: opts.now,
1268
+ dryRun: !!args?.dry_run,
1269
+ report,
1270
+ });
1271
+ out.push({ window: wk, pillars: sum, ...r });
853
1272
  }
854
- return { platform: 'multi', codename: id.codename, operator_id: id.operator_id, sources: detected.map((d) => d.platform), windows: out }
1273
+ return {
1274
+ platform: "multi",
1275
+ codename: id.codename,
1276
+ operator_id: id.operator_id,
1277
+ sources: detected.map((d) => d.platform),
1278
+ windows: out,
1279
+ };
855
1280
  }
856
1281
 
857
- const pulled = await pullByPlatform(platform, opts)
858
- const targets = args?.window ? pulled.windows.filter((w) => w.window === args.window) : pulled.windows
859
- const out = []
1282
+ const pulled = await pullByPlatform(platform, opts);
1283
+ const targets = args?.window
1284
+ ? pulled.windows.filter((w) => w.window === args.window)
1285
+ : pulled.windows;
1286
+ const out = [];
860
1287
  for (const w of targets) {
861
1288
  // Compute the cascade report block (mode + badges + health) for this window.
862
1289
  // Pure math, computed locally, submitted alongside the 4 token pillars.
863
- const report = _computeReportBlock(w.pillars)
1290
+ const report = _computeReportBlock(w.pillars);
864
1291
  const r = await submitSignedWindow(w.window, w.pillars, w.messages, id, {
865
1292
  apiBase,
866
1293
  fetchImpl: doFetch,
@@ -868,22 +1295,33 @@ export async function callTool(name, args, opts = {}) {
868
1295
  now: opts.now,
869
1296
  dryRun: !!args?.dry_run,
870
1297
  report,
871
- })
872
- out.push({ window: w.window, pillars: w.pillars, ...r })
1298
+ });
1299
+ out.push({ window: w.window, pillars: w.pillars, ...r });
873
1300
  }
874
- return { platform: pulled.platform, codename: id.codename, operator_id: id.operator_id, generatedAt: pulled.generatedAt, windows: out }
1301
+ return {
1302
+ platform: pulled.platform,
1303
+ codename: id.codename,
1304
+ operator_id: id.operator_id,
1305
+ generatedAt: pulled.generatedAt,
1306
+ windows: out,
1307
+ };
875
1308
  }
876
1309
 
877
- if (name === 'submit_paste') {
878
- if (!args?.text) throw new Error('submit_paste requires a non-empty `text` argument.')
879
- if (typeof args.text === 'string' && args.text.length > MAX_INPUT) {
880
- return { status: 'error', reason: 'input_too_large', detail: `text exceeds ${MAX_INPUT} chars (${args.text.length}). Paste only the token-count table, not full output.` }
1310
+ if (name === "submit_paste") {
1311
+ if (!args?.text)
1312
+ throw new Error("submit_paste requires a non-empty `text` argument.");
1313
+ if (typeof args.text === "string" && args.text.length > MAX_INPUT) {
1314
+ return {
1315
+ status: "error",
1316
+ reason: "input_too_large",
1317
+ detail: `text exceeds ${MAX_INPUT} chars (${args.text.length}). Paste only the token-count table, not full output.`,
1318
+ };
881
1319
  }
882
1320
  // Local preview first — also validates the paste is parseable before any POST.
883
- const pillars = parsePillars(args.text)
884
- const c = withParseWarnings(pillars, cascade(pillars))
885
- const codename = String(args?.codename || '').trim()
886
- const card = narrate(c, codename || 'This operator')
1321
+ const pillars = parsePillars(args.text);
1322
+ const c = withParseWarnings(pillars, cascade(pillars));
1323
+ const codename = String(args?.codename || "").trim();
1324
+ const card = narrate(c, codename || "This operator");
887
1325
 
888
1326
  // No codename → cannot publish (the board endpoint requires it). Fail fast at the
889
1327
  // tool boundary with a clear message instead of an opaque server 400.
@@ -891,104 +1329,198 @@ export async function callTool(name, args, opts = {}) {
891
1329
  return {
892
1330
  ...c,
893
1331
  card,
894
- submission: { status: 'not_submitted', reason: 'codename_required', detail: 'Pass a codename to publish to the board. Showing local preview only.' },
895
- }
1332
+ submission: {
1333
+ status: "not_submitted",
1334
+ reason: "codename_required",
1335
+ detail:
1336
+ "Pass a codename to publish to the board. Showing local preview only.",
1337
+ },
1338
+ };
896
1339
  }
897
1340
 
898
1341
  // Submit the RAW paste so the server re-parses + re-scores authoritatively — the
899
1342
  // MCP's local cascade is only a preview; the board stays the single source of truth.
900
- const stamp = uploadStamp({ codename, pillars: c.pillars, source: 'web_paste' })
1343
+ const stamp = uploadStamp({
1344
+ codename,
1345
+ pillars: c.pillars,
1346
+ source: "web_paste",
1347
+ });
901
1348
  const res = await doFetch(`${apiBase}/api/v1/ingest-paste`, {
902
- method: 'POST',
903
- headers: { 'content-type': 'application/json', accept: 'application/json' },
904
- body: JSON.stringify({ codename, raw_paste: String(args?.text || ''), ...stamp }),
905
- })
906
- let ack
907
- try { ack = await res.json() } catch { ack = { status: 'error', detail: `HTTP ${res.status} (non-JSON response)` } }
908
- const ranked = !!(res.ok && ack.verification_tier === 'verified' && ack.persisted === true)
909
- return { ...c, card, ranked, submission: { ...stamp, httpStatus: res.status, ranked, ...ack } }
1349
+ method: "POST",
1350
+ headers: {
1351
+ "content-type": "application/json",
1352
+ accept: "application/json",
1353
+ },
1354
+ body: JSON.stringify({
1355
+ codename,
1356
+ raw_paste: String(args?.text || ""),
1357
+ ...stamp,
1358
+ }),
1359
+ });
1360
+ let ack;
1361
+ try {
1362
+ ack = await res.json();
1363
+ } catch {
1364
+ ack = {
1365
+ status: "error",
1366
+ detail: `HTTP ${res.status} (non-JSON response)`,
1367
+ };
1368
+ }
1369
+ const ranked = !!(
1370
+ res.ok &&
1371
+ ack.verification_tier === "verified" &&
1372
+ ack.persisted === true
1373
+ );
1374
+ return {
1375
+ ...c,
1376
+ card,
1377
+ ranked,
1378
+ submission: { ...stamp, httpStatus: res.status, ranked, ...ack },
1379
+ };
910
1380
  }
911
1381
 
912
- if (name === 'tokenpull') {
1382
+ if (name === "tokenpull") {
913
1383
  // Local read → 4 windows of pillars → cascade each. Token-only, on-device.
914
- const platform = args?.platform || 'claude'
915
- const pulled = await pullByPlatform(platform, opts)
1384
+ const platform = args?.platform || "claude";
1385
+ const pulled = await pullByPlatform(platform, opts);
916
1386
  const windows = pulled.windows.map((w) => {
917
- const c = cascade(w.pillars)
918
- return { window: w.window, messages: w.messages, pillars: w.pillars, cascade: c, card: narrate(c, `${w.window} ${platform}`) }
919
- })
920
- return { platform: pulled.platform, estimated: pulled.estimated || false, ...(pulled.ioRatio ? { ioRatio: pulled.ioRatio } : {}), generatedAt: pulled.generatedAt, files: pulled.files, totalMessages: pulled.totalMessages, windows }
1387
+ const c = cascade(w.pillars);
1388
+ return {
1389
+ window: w.window,
1390
+ messages: w.messages,
1391
+ pillars: w.pillars,
1392
+ cascade: c,
1393
+ card: narrate(c, `${w.window} ${platform}`),
1394
+ };
1395
+ });
1396
+ return {
1397
+ platform: pulled.platform,
1398
+ estimated: pulled.estimated || false,
1399
+ ...(pulled.ioRatio ? { ioRatio: pulled.ioRatio } : {}),
1400
+ generatedAt: pulled.generatedAt,
1401
+ files: pulled.files,
1402
+ totalMessages: pulled.totalMessages,
1403
+ windows,
1404
+ };
921
1405
  }
922
1406
 
923
- if (name === 'tokenpull_submit') {
1407
+ if (name === "tokenpull_submit") {
924
1408
  // Pull local usage, then publish each window's CANONICAL pillars to the board
925
1409
  // (server re-scores). The board stays platform-agnostic via the 4 pillars; the
926
1410
  // source platform rides along as a tag. Conversion already happened in the adapter.
927
- const codename = String(args?.codename || '').trim()
928
- const pulled = await pullByPlatform(args?.platform || 'claude', opts)
929
- const targets = args?.window ? pulled.windows.filter((w) => w.window === args.window) : pulled.windows
930
- const out = []
1411
+ const codename = String(args?.codename || "").trim();
1412
+ const pulled = await pullByPlatform(args?.platform || "claude", opts);
1413
+ const targets = args?.window
1414
+ ? pulled.windows.filter((w) => w.window === args.window)
1415
+ : pulled.windows;
1416
+ const out = [];
931
1417
  for (const w of targets) {
932
- const c = cascade(w.pillars)
933
- const card = narrate(c, `${w.window} window`)
1418
+ const c = cascade(w.pillars);
1419
+ const card = narrate(c, `${w.window} window`);
934
1420
  if (!codename) {
935
- out.push({ window: w.window, pillars: w.pillars, cascade: c, card, submission: { status: 'not_submitted', reason: 'codename_required' } })
936
- continue
1421
+ out.push({
1422
+ window: w.window,
1423
+ pillars: w.pillars,
1424
+ cascade: c,
1425
+ card,
1426
+ submission: { status: "not_submitted", reason: "codename_required" },
1427
+ });
1428
+ continue;
937
1429
  }
938
1430
  // canonical pillars → "input output cacheCreate cacheRead" (the parser's 4-bare-number form)
939
- const rawPaste = `${w.pillars.input} ${w.pillars.output} ${w.pillars.cacheCreate} ${w.pillars.cacheRead}`
940
- const windowType = WINDOW_TYPE[w.window] || w.window
941
- const stamp = uploadStamp({ codename, window: windowType, pillars: w.pillars, platform: pulled.platform })
1431
+ const rawPaste = `${w.pillars.input} ${w.pillars.output} ${w.pillars.cacheCreate} ${w.pillars.cacheRead}`;
1432
+ const windowType = WINDOW_TYPE[w.window] || w.window;
1433
+ const stamp = uploadStamp({
1434
+ codename,
1435
+ window: windowType,
1436
+ pillars: w.pillars,
1437
+ platform: pulled.platform,
1438
+ });
942
1439
  const res = await doFetch(`${apiBase}/api/v1/ingest-paste`, {
943
- method: 'POST',
944
- headers: { 'content-type': 'application/json', accept: 'application/json' },
945
- body: JSON.stringify({ codename, raw_paste: rawPaste, window_type: windowType, telemetry: { platform: { primary: pulled.platform } }, ...stamp }),
946
- })
947
- let ack
948
- try { ack = await res.json() } catch { ack = { status: 'error', detail: `HTTP ${res.status} (non-JSON)` } }
1440
+ method: "POST",
1441
+ headers: {
1442
+ "content-type": "application/json",
1443
+ accept: "application/json",
1444
+ },
1445
+ body: JSON.stringify({
1446
+ codename,
1447
+ raw_paste: rawPaste,
1448
+ window_type: windowType,
1449
+ telemetry: { platform: { primary: pulled.platform } },
1450
+ ...stamp,
1451
+ }),
1452
+ });
1453
+ let ack;
1454
+ try {
1455
+ ack = await res.json();
1456
+ } catch {
1457
+ ack = { status: "error", detail: `HTTP ${res.status} (non-JSON)` };
1458
+ }
949
1459
  // ranked = actually on the board (verified + persisted), not just received — parity
950
1460
  // with submit_verified (submit.mjs). An unenrolled/revoked device gets 202 but is NEVER ranked.
951
- const ranked = !!(res.ok && ack.verification_tier === 'verified' && ack.persisted === true)
952
- out.push({ window: w.window, pillars: w.pillars, cascade: c, card, ranked, submission: { ...stamp, httpStatus: res.status, ranked, ...ack } })
1461
+ const ranked = !!(
1462
+ res.ok &&
1463
+ ack.verification_tier === "verified" &&
1464
+ ack.persisted === true
1465
+ );
1466
+ out.push({
1467
+ window: w.window,
1468
+ pillars: w.pillars,
1469
+ cascade: c,
1470
+ card,
1471
+ ranked,
1472
+ submission: { ...stamp, httpStatus: res.status, ranked, ...ack },
1473
+ });
953
1474
  }
954
- return { platform: pulled.platform, codename: codename || null, generatedAt: pulled.generatedAt, windows: out }
1475
+ return {
1476
+ platform: pulled.platform,
1477
+ codename: codename || null,
1478
+ generatedAt: pulled.generatedAt,
1479
+ windows: out,
1480
+ };
955
1481
  }
956
1482
 
957
- if (name === 'rank_windows') {
1483
+ if (name === "rank_windows") {
958
1484
  // Score up to 4 named window pastes independently. Named-key parsing only —
959
1485
  // positional is unsafe here because dashboards list cache_read before cache_create
960
1486
  // (the WINDOWED_PROFILES swap gotcha). Each window goes through parsePillars →
961
1487
  // cascade → narrate individually; results are collected into a windows[] array
962
1488
  // in the same shape as tokenpull output for easy follow-up with tokenpull_submit.
963
- const WINDOW_KEYS = ['7d', '30d', '90d', 'all']
964
- const sourceTool = args?.source_tool || null
1489
+ const WINDOW_KEYS = ["7d", "30d", "90d", "all"];
1490
+ const sourceTool = args?.source_tool || null;
965
1491
  // E2: reject any oversized window paste up front (token tables are tiny).
966
1492
  for (const wk of WINDOW_KEYS) {
967
- const v = args?.[wk]
968
- if (typeof v === 'string' && v.length > MAX_INPUT) {
969
- return { status: 'error', reason: 'input_too_large', detail: `window '${wk}' exceeds ${MAX_INPUT} chars (${v.length}). Paste only the token-count table.` }
1493
+ const v = args?.[wk];
1494
+ if (typeof v === "string" && v.length > MAX_INPUT) {
1495
+ return {
1496
+ status: "error",
1497
+ reason: "input_too_large",
1498
+ detail: `window '${wk}' exceeds ${MAX_INPUT} chars (${v.length}). Paste only the token-count table.`,
1499
+ };
970
1500
  }
971
1501
  }
972
- const windows = []
1502
+ const windows = [];
973
1503
  for (const wk of WINDOW_KEYS) {
974
- const text = args?.[wk]
975
- if (!text || typeof text !== 'string' || !text.trim()) continue
976
- const pillars = parsePillars(text)
977
- const c = withParseWarnings(pillars, cascade(pillars))
978
- const card = narrate(c, `${wk} window`)
979
- windows.push({ window: wk, pillars, cascade: c, card })
1504
+ const text = args?.[wk];
1505
+ if (!text || typeof text !== "string" || !text.trim()) continue;
1506
+ const pillars = parsePillars(text);
1507
+ const c = withParseWarnings(pillars, cascade(pillars));
1508
+ const card = narrate(c, `${wk} window`);
1509
+ windows.push({ window: wk, pillars, cascade: c, card });
980
1510
  }
981
1511
  if (windows.length === 0) {
982
- throw new Error('rank_windows requires at least one window paste (7d, 30d, 90d, or all).')
1512
+ throw new Error(
1513
+ "rank_windows requires at least one window paste (7d, 30d, 90d, or all).",
1514
+ );
983
1515
  }
984
1516
  return {
985
1517
  windows,
986
1518
  source_tool: sourceTool,
987
- note: 'Local preview only — use tokenpull_submit to publish to the board.',
988
- }
1519
+ note: "Local preview only — use tokenpull_submit to publish to the board.",
1520
+ };
989
1521
  }
990
1522
 
991
- if (name === 'watch_tokenpull') {
1523
+ if (name === "watch_tokenpull") {
992
1524
  // Poll the local token logs at a configurable interval and return the cascade
993
1525
  // diff whenever new sessions appear. One poll cycle per MCP call — the client
994
1526
  // is responsible for re-calling at the desired cadence (MCP tools are stateless;
@@ -996,44 +1528,63 @@ export async function callTool(name, args, opts = {}) {
996
1528
  //
997
1529
  // With submit:true + an enrolled device, this also signs + POSTs the watched window
998
1530
  // to the verified ingest path each poll (the server dedups identical re-submits).
999
- const platform = args?.platform || 'claude'
1000
- const watchWindow = args?.window || '7d'
1001
- const intervalS = Math.max(10, Number(args?.interval_s) || 60)
1531
+ const platform = args?.platform || "claude";
1532
+ const watchWindow = args?.window || "7d";
1533
+ const intervalS = Math.max(10, Number(args?.interval_s) || 60);
1002
1534
 
1003
- const pulled = await pullByPlatform(platform, opts)
1004
- const win = pulled.windows.find((w) => w.window === watchWindow)
1005
- if (!win) throw new Error(`watch_tokenpull: window '${watchWindow}' not found in pull result.`)
1535
+ const pulled = await pullByPlatform(platform, opts);
1536
+ const win = pulled.windows.find((w) => w.window === watchWindow);
1537
+ if (!win)
1538
+ throw new Error(
1539
+ `watch_tokenpull: window '${watchWindow}' not found in pull result.`,
1540
+ );
1006
1541
 
1007
- const c = cascade(win.pillars)
1008
- const card = narrate(c, `${watchWindow} ${platform}`)
1542
+ const c = cascade(win.pillars);
1543
+ const card = narrate(c, `${watchWindow} ${platform}`);
1009
1544
 
1010
1545
  // AUTH.WIRE (D7 §7): when submit is on AND the device is enrolled, sign + POST the
1011
1546
  // watched window to the verified ingest path. Default OFF = preview only. The server
1012
1547
  // dedups identical re-submits (exact snapshot_hash → 422), so re-calling is safe.
1013
- let auth_submit = null
1548
+ let auth_submit = null;
1014
1549
  if (args?.submit === true) {
1015
- const id = opts.identity || ensureIdentity()
1550
+ const id = opts.identity || ensureIdentity();
1016
1551
  if (id.codename && id.operator_id && id.private_key_pkcs8_b64) {
1017
1552
  // E3: client-side cooldown — at most one auto-submit per platform+window per 5 min.
1018
1553
  // Prevents a fast poll loop from churning the board even before the server's
1019
1554
  // hash-dedup kicks in. Armed only on a non-error outcome so failed submits retry.
1020
- const clockNow = typeof opts.now === 'number' ? opts.now : Date.now()
1021
- const cdKey = `${pulled.platform}:${watchWindow}`
1022
- const last = _lastWatchSubmitAt.get(cdKey)
1555
+ const clockNow = typeof opts.now === "number" ? opts.now : Date.now();
1556
+ const cdKey = `${pulled.platform}:${watchWindow}`;
1557
+ const last = _lastWatchSubmitAt.get(cdKey);
1023
1558
  if (last != null && clockNow - last < WATCH_SUBMIT_COOLDOWN_MS) {
1024
- const waitS = Math.ceil((WATCH_SUBMIT_COOLDOWN_MS - (clockNow - last)) / 1000)
1025
- auth_submit = { status: 'cooldown', detail: `auto-submit for '${cdKey}' on cooldown — next in ~${waitS}s (max once / 5 min).`, retry_after_s: waitS }
1559
+ const waitS = Math.ceil(
1560
+ (WATCH_SUBMIT_COOLDOWN_MS - (clockNow - last)) / 1000,
1561
+ );
1562
+ auth_submit = {
1563
+ status: "cooldown",
1564
+ detail: `auto-submit for '${cdKey}' on cooldown — next in ~${waitS}s (max once / 5 min).`,
1565
+ retry_after_s: waitS,
1566
+ };
1026
1567
  } else {
1027
- auth_submit = await submitSignedWindow(watchWindow, win.pillars, win.messages, id, {
1028
- apiBase,
1029
- fetchImpl: doFetch,
1030
- platform: pulled.platform,
1031
- now: opts.now,
1032
- })
1033
- if (auth_submit?.status !== 'error') _lastWatchSubmitAt.set(cdKey, clockNow)
1568
+ auth_submit = await submitSignedWindow(
1569
+ watchWindow,
1570
+ win.pillars,
1571
+ win.messages,
1572
+ id,
1573
+ {
1574
+ apiBase,
1575
+ fetchImpl: doFetch,
1576
+ platform: pulled.platform,
1577
+ now: opts.now,
1578
+ },
1579
+ );
1580
+ if (auth_submit?.status !== "error")
1581
+ _lastWatchSubmitAt.set(cdKey, clockNow);
1034
1582
  }
1035
1583
  } else {
1036
- auth_submit = { status: 'not_enrolled', detail: 'Run `npx sigrank-mcp enroll` to auto-submit verified runs.' }
1584
+ auth_submit = {
1585
+ status: "not_enrolled",
1586
+ detail: "Run `npx sigrank-mcp enroll` to auto-submit verified runs.",
1587
+ };
1037
1588
  }
1038
1589
  }
1039
1590
  return {
@@ -1046,154 +1597,210 @@ export async function callTool(name, args, opts = {}) {
1046
1597
  generatedAt: pulled.generatedAt,
1047
1598
  poll_interval_s: intervalS,
1048
1599
  auth_submit,
1049
- note: 'One snapshot per call — re-call at your poll interval to detect changes.',
1050
- }
1600
+ note: "One snapshot per call — re-call at your poll interval to detect changes.",
1601
+ };
1051
1602
  }
1052
1603
 
1053
- if (name === 'tokenpull_compare') {
1054
- const platform = args?.platform || 'claude'
1055
- const WINS = ['7d', '30d', '90d', 'all']
1604
+ if (name === "tokenpull_compare") {
1605
+ const platform = args?.platform || "claude";
1606
+ const WINS = ["7d", "30d", "90d", "all"];
1056
1607
 
1057
1608
  // Pull all four sources in parallel (verifiers are now async via execFile)
1058
1609
  const [tpResult, ccPillars, tdPillars, tsPillars] = await Promise.all([
1059
1610
  pullByPlatform(platform, opts).catch(() => null),
1060
1611
  _ccusagePillars(platform).catch(() => null),
1061
- (platform === 'claude' ? _tokenDashPillars() : Promise.resolve(null)).catch(() => null),
1612
+ (platform === "claude"
1613
+ ? _tokenDashPillars()
1614
+ : Promise.resolve(null)
1615
+ ).catch(() => null),
1062
1616
  _tokscalePillars(platform).catch(() => null),
1063
- ])
1617
+ ]);
1064
1618
 
1065
1619
  // Build tokenpull window lookup
1066
- const tpByWin = {}
1067
- for (const w of (tpResult?.windows ?? [])) tpByWin[w.window] = w.pillars
1620
+ const tpByWin = {};
1621
+ for (const w of tpResult?.windows ?? []) tpByWin[w.window] = w.pillars;
1068
1622
 
1069
1623
  // Helper: delta % vs tokenpull baseline
1070
- const delta = (val, base) => base > 0 ? +((( val - base) / base * 100).toFixed(1)) : null
1624
+ const delta = (val, base) =>
1625
+ base > 0 ? +(((val - base) / base) * 100).toFixed(1) : null;
1071
1626
 
1072
1627
  // Build per-source per-window comparison
1073
1628
  const SOURCES = [
1074
- { source: 'tokenpull', note: 'JSONL deduped by msg id — canon source', byWin: tpByWin },
1075
- { source: 'ccusage', note: 'ccusage CLI — monthly only', byWin: ccPillars ?? {} },
1076
- { source: 'token-dash', note: 'token-dashboard SQLite — all-time only', byWin: tdPillars ?? {} },
1077
- { source: 'tokscale', note: 'tokscale_report.json — all-time only', byWin: tsPillars ?? {} },
1078
- ]
1629
+ {
1630
+ source: "tokenpull",
1631
+ note: "JSONL deduped by msg id — canon source",
1632
+ byWin: tpByWin,
1633
+ },
1634
+ {
1635
+ source: "ccusage",
1636
+ note: "ccusage CLI — monthly only",
1637
+ byWin: ccPillars ?? {},
1638
+ },
1639
+ {
1640
+ source: "token-dash",
1641
+ note: "token-dashboard SQLite — all-time only",
1642
+ byWin: tdPillars ?? {},
1643
+ },
1644
+ {
1645
+ source: "tokscale",
1646
+ note: "tokscale_report.json — all-time only",
1647
+ byWin: tsPillars ?? {},
1648
+ },
1649
+ ];
1079
1650
 
1080
- const comparison = {}
1651
+ const comparison = {};
1081
1652
  for (const win of WINS) {
1082
- const baseP = tpByWin[win]
1083
- comparison[win] = SOURCES
1084
- .filter(s => s.byWin[win] != null)
1085
- .map(s => {
1086
- const p = s.byWin[win]
1087
- const cas = cascade(p)
1088
- const entry = {
1089
- source: s.source,
1090
- note: s.note,
1091
- pillars: p,
1092
- cascade: { yield: cas.yield, snr: cas.snr, leverage: cas.leverage, velocity: cas.velocity, dev10x: cas.dev10x, class: cas.class },
1093
- }
1094
- if (s.source !== 'tokenpull' && baseP) {
1095
- entry.delta_vs_tokenpull = {
1096
- input: delta(p.input, baseP.input),
1097
- output: delta(p.output, baseP.output),
1098
- cacheCreate: delta(p.cacheCreate, baseP.cacheCreate),
1099
- cacheRead: delta(p.cacheRead, baseP.cacheRead),
1100
- }
1101
- }
1102
- return entry
1103
- })
1653
+ const baseP = tpByWin[win];
1654
+ comparison[win] = SOURCES.filter((s) => s.byWin[win] != null).map((s) => {
1655
+ const p = s.byWin[win];
1656
+ const cas = cascade(p);
1657
+ const entry = {
1658
+ source: s.source,
1659
+ note: s.note,
1660
+ pillars: p,
1661
+ cascade: {
1662
+ yield: cas.yield,
1663
+ snr: cas.snr,
1664
+ leverage: cas.leverage,
1665
+ velocity: cas.velocity,
1666
+ dev10x: cas.dev10x,
1667
+ class: cas.class,
1668
+ },
1669
+ };
1670
+ if (s.source !== "tokenpull" && baseP) {
1671
+ entry.delta_vs_tokenpull = {
1672
+ input: delta(p.input, baseP.input),
1673
+ output: delta(p.output, baseP.output),
1674
+ cacheCreate: delta(p.cacheCreate, baseP.cacheCreate),
1675
+ cacheRead: delta(p.cacheRead, baseP.cacheRead),
1676
+ };
1677
+ }
1678
+ return entry;
1679
+ });
1104
1680
  }
1105
1681
 
1106
1682
  // Sources available summary
1107
- const available = SOURCES
1108
- .filter(s => Object.keys(s.byWin).length > 0)
1109
- .map(s => s.source)
1683
+ const available = SOURCES.filter(
1684
+ (s) => Object.keys(s.byWin).length > 0,
1685
+ ).map((s) => s.source);
1110
1686
 
1111
1687
  return {
1112
1688
  platform,
1113
1689
  estimated: tpResult?.estimated ?? false,
1114
1690
  generatedAt: tpResult?.generatedAt ?? new Date().toISOString(),
1115
1691
  sources_available: available,
1116
- sources_missing: SOURCES.map(s => s.source).filter(s => !available.includes(s)),
1692
+ sources_missing: SOURCES.map((s) => s.source).filter(
1693
+ (s) => !available.includes(s),
1694
+ ),
1117
1695
  comparison,
1118
- }
1696
+ };
1119
1697
  }
1120
1698
 
1121
- if (name === 'simulate_change') {
1699
+ if (name === "simulate_change") {
1122
1700
  // The first prescriptive tool — "what if I changed my token mix?"
1123
1701
  // Pure local math: parse current pillars, apply proposed changes, run the
1124
1702
  // cascade on both, return the delta. No network, no submission.
1125
- if (!args?.text) throw new Error('simulate_change requires a non-empty `text` argument (current pillars).')
1126
- if (typeof args.text === 'string' && args.text.length > MAX_INPUT) {
1127
- return { status: 'error', reason: 'input_too_large', detail: `text exceeds ${MAX_INPUT} chars.` }
1703
+ if (!args?.text)
1704
+ throw new Error(
1705
+ "simulate_change requires a non-empty `text` argument (current pillars).",
1706
+ );
1707
+ if (typeof args.text === "string" && args.text.length > MAX_INPUT) {
1708
+ return {
1709
+ status: "error",
1710
+ reason: "input_too_large",
1711
+ detail: `text exceeds ${MAX_INPUT} chars.`,
1712
+ };
1128
1713
  }
1129
- if (!args?.changes || typeof args.changes !== 'object') {
1130
- throw new Error('simulate_change requires a `changes` object with at least one pillar change.')
1714
+ if (!args?.changes || typeof args.changes !== "object") {
1715
+ throw new Error(
1716
+ "simulate_change requires a `changes` object with at least one pillar change.",
1717
+ );
1131
1718
  }
1132
1719
 
1133
- const currentPillars = parsePillars(args.text)
1134
- const current = withParseWarnings(currentPillars, cascade(currentPillars))
1720
+ const currentPillars = parsePillars(args.text);
1721
+ const current = withParseWarnings(currentPillars, cascade(currentPillars));
1135
1722
 
1136
1723
  // Apply changes: each pillar is either an absolute number (replace) or a
1137
1724
  // string starting with +/- (relative delta). Omitted pillars are unchanged.
1138
- const PILLAR_KEYS = ['input', 'output', 'cacheCreate', 'cacheRead']
1139
- const simulated = { ...currentPillars }
1140
- const appliedChanges = {}
1725
+ const PILLAR_KEYS = ["input", "output", "cacheCreate", "cacheRead"];
1726
+ const simulated = { ...currentPillars };
1727
+ const appliedChanges = {};
1141
1728
 
1142
1729
  for (const key of PILLAR_KEYS) {
1143
- if (args.changes[key] == null) continue
1144
- const raw = args.changes[key]
1145
- let newVal
1146
-
1147
- if (typeof raw === 'number') {
1148
- newVal = raw
1149
- } else if (typeof raw === 'string') {
1150
- const trimmed = raw.trim()
1151
- if (trimmed.startsWith('+') || trimmed.startsWith('-')) {
1730
+ if (args.changes[key] == null) continue;
1731
+ const raw = args.changes[key];
1732
+ let newVal;
1733
+
1734
+ if (typeof raw === "number") {
1735
+ newVal = raw;
1736
+ } else if (typeof raw === "string") {
1737
+ const trimmed = raw.trim();
1738
+ if (trimmed.startsWith("+") || trimmed.startsWith("-")) {
1152
1739
  // Relative delta: add/subtract from current value
1153
- const delta = Number(trimmed)
1740
+ const delta = Number(trimmed);
1154
1741
  if (!Number.isFinite(delta)) {
1155
- return { status: 'error', reason: 'invalid_change', detail: `changes.${key}: "${raw}" is not a valid relative delta.` }
1742
+ return {
1743
+ status: "error",
1744
+ reason: "invalid_change",
1745
+ detail: `changes.${key}: "${raw}" is not a valid relative delta.`,
1746
+ };
1156
1747
  }
1157
- newVal = currentPillars[key] + delta
1748
+ newVal = currentPillars[key] + delta;
1158
1749
  } else {
1159
1750
  // Absolute value as a string
1160
- newVal = Number(trimmed)
1751
+ newVal = Number(trimmed);
1161
1752
  }
1162
1753
  } else {
1163
- return { status: 'error', reason: 'invalid_change', detail: `changes.${key}: expected number or string, got ${typeof raw}.` }
1754
+ return {
1755
+ status: "error",
1756
+ reason: "invalid_change",
1757
+ detail: `changes.${key}: expected number or string, got ${typeof raw}.`,
1758
+ };
1164
1759
  }
1165
1760
 
1166
1761
  if (!Number.isFinite(newVal)) {
1167
- return { status: 'error', reason: 'invalid_change', detail: `changes.${key}: result is not a finite number.` }
1762
+ return {
1763
+ status: "error",
1764
+ reason: "invalid_change",
1765
+ detail: `changes.${key}: result is not a finite number.`,
1766
+ };
1168
1767
  }
1169
1768
  // Clamp to non-negative — token counts can't be negative
1170
1769
  if (newVal < 0) {
1171
- return { status: 'error', reason: 'invalid_change', detail: `changes.${key}: result ${newVal} is negative — token counts must be >= 0.` }
1770
+ return {
1771
+ status: "error",
1772
+ reason: "invalid_change",
1773
+ detail: `changes.${key}: result ${newVal} is negative — token counts must be >= 0.`,
1774
+ };
1172
1775
  }
1173
1776
 
1174
- simulated[key] = newVal
1777
+ simulated[key] = newVal;
1175
1778
  appliedChanges[key] = {
1176
1779
  from: currentPillars[key],
1177
1780
  to: newVal,
1178
1781
  delta: newVal - currentPillars[key],
1179
- }
1782
+ };
1180
1783
  }
1181
1784
 
1182
1785
  if (Object.keys(appliedChanges).length === 0) {
1183
- return { status: 'error', reason: 'no_changes', detail: 'No pillar changes specified in the `changes` object.' }
1786
+ return {
1787
+ status: "error",
1788
+ reason: "no_changes",
1789
+ detail: "No pillar changes specified in the `changes` object.",
1790
+ };
1184
1791
  }
1185
1792
 
1186
- const simulatedResult = cascade(simulated)
1793
+ const simulatedResult = cascade(simulated);
1187
1794
 
1188
1795
  // Compute deltas for every cascade metric
1189
1796
  const metricDelta = (curr, sim) => {
1190
- if (curr == null && sim == null) return null
1191
- if (curr == null) return { from: null, to: sim, delta: null }
1192
- if (sim == null) return { from: curr, to: null, delta: null }
1193
- return { from: curr, to: sim, delta: Number((sim - curr).toFixed(4)) }
1194
- }
1797
+ if (curr == null && sim == null) return null;
1798
+ if (curr == null) return { from: null, to: sim, delta: null };
1799
+ if (sim == null) return { from: curr, to: null, delta: null };
1800
+ return { from: curr, to: sim, delta: Number((sim - curr).toFixed(4)) };
1801
+ };
1195
1802
 
1196
- const classChanged = current.class !== simulatedResult.class
1803
+ const classChanged = current.class !== simulatedResult.class;
1197
1804
 
1198
1805
  return {
1199
1806
  current: {
@@ -1206,7 +1813,12 @@ export async function callTool(name, args, opts = {}) {
1206
1813
  class: current.class,
1207
1814
  },
1208
1815
  simulated: {
1209
- pillars: { input: simulated.input, output: simulated.output, cacheCreate: simulated.cacheCreate, cacheRead: simulated.cacheRead },
1816
+ pillars: {
1817
+ input: simulated.input,
1818
+ output: simulated.output,
1819
+ cacheCreate: simulated.cacheCreate,
1820
+ cacheRead: simulated.cacheRead,
1821
+ },
1210
1822
  yield: simulatedResult.yield,
1211
1823
  snr: simulatedResult.snr,
1212
1824
  leverage: simulatedResult.leverage,
@@ -1223,135 +1835,160 @@ export async function callTool(name, args, opts = {}) {
1223
1835
  dev10x: metricDelta(current.dev10x, simulatedResult.dev10x),
1224
1836
  },
1225
1837
  class_changed: classChanged,
1226
- ...(classChanged ? { class_transition: `${current.class} → ${simulatedResult.class}` } : {}),
1227
- ...(simulatedResult.warnings ? { simulated_warnings: simulatedResult.warnings } : {}),
1228
- note: 'Local simulation only — no submission. The actual score depends on server-side RS.xx weights and class thresholds.',
1229
- }
1838
+ ...(classChanged
1839
+ ? { class_transition: `${current.class} → ${simulatedResult.class}` }
1840
+ : {}),
1841
+ ...(simulatedResult.warnings
1842
+ ? { simulated_warnings: simulatedResult.warnings }
1843
+ : {}),
1844
+ note: "Local simulation only — no submission. The actual score depends on server-side RS.xx weights and class thresholds.",
1845
+ };
1230
1846
  }
1231
1847
 
1232
- if (name === 'diagnose_cascade') {
1233
- if (!args?.text) throw new Error('diagnose_cascade requires a non-empty `text` argument (token pillars).')
1234
- if (typeof args.text === 'string' && args.text.length > MAX_INPUT) {
1235
- return { status: 'error', reason: 'input_too_large', detail: `text exceeds ${MAX_INPUT} chars.` }
1848
+ if (name === "diagnose_cascade") {
1849
+ if (!args?.text)
1850
+ throw new Error(
1851
+ "diagnose_cascade requires a non-empty `text` argument (token pillars).",
1852
+ );
1853
+ if (typeof args.text === "string" && args.text.length > MAX_INPUT) {
1854
+ return {
1855
+ status: "error",
1856
+ reason: "input_too_large",
1857
+ detail: `text exceeds ${MAX_INPUT} chars.`,
1858
+ };
1236
1859
  }
1237
1860
 
1238
- const p = parsePillars(args.text)
1239
- const result = withParseWarnings(p, cascade(p))
1240
- const { input: i, output: o, cacheCreate: cw, cacheRead: cr } = p
1241
- const diagnosis = []
1861
+ const p = parsePillars(args.text);
1862
+ const result = withParseWarnings(p, cascade(p));
1863
+ const { input: i, output: o, cacheCreate: cw, cacheRead: cr } = p;
1864
+ const diagnosis = [];
1242
1865
 
1243
1866
  // Cache leverage check — the #1 cascade efficiency signal
1244
- const leverage = result.leverage
1867
+ const leverage = result.leverage;
1245
1868
  if (leverage !== null) {
1246
1869
  if (leverage < 10) {
1247
1870
  diagnosis.push({
1248
- metric: 'cache_leverage',
1249
- severity: 'critical',
1871
+ metric: "cache_leverage",
1872
+ severity: "critical",
1250
1873
  finding: `Cache leverage is ${leverage}× — you're reading only ${leverage}× your fresh input from cache. TRANSMITTER-class operators hit 200×+.`,
1251
- recommendation: 'Increase context reuse: load prior session context, use longer conversation threads, reference earlier outputs.',
1874
+ recommendation:
1875
+ "Increase context reuse: load prior session context, use longer conversation threads, reference earlier outputs.",
1252
1876
  estimated_yield_impact: `+${Math.round((1 - leverage / 50) * 100)}% Υ potential`,
1253
- })
1877
+ });
1254
1878
  } else if (leverage < 50) {
1255
1879
  diagnosis.push({
1256
- metric: 'cache_leverage',
1257
- severity: 'warning',
1880
+ metric: "cache_leverage",
1881
+ severity: "warning",
1258
1882
  finding: `Cache leverage is ${leverage}× — decent but below the ARCH+ threshold (~100×+).`,
1259
- recommendation: 'Push cache reads higher by reusing prior context more aggressively.',
1883
+ recommendation:
1884
+ "Push cache reads higher by reusing prior context more aggressively.",
1260
1885
  estimated_yield_impact: `+${Math.round((1 - leverage / 100) * 50)}% Υ potential`,
1261
- })
1886
+ });
1262
1887
  }
1263
1888
  }
1264
1889
 
1265
1890
  // Velocity check — output per input
1266
- const velocity = result.velocity
1891
+ const velocity = result.velocity;
1267
1892
  if (velocity !== null) {
1268
1893
  if (velocity < 0.5) {
1269
1894
  diagnosis.push({
1270
- metric: 'velocity',
1271
- severity: 'critical',
1895
+ metric: "velocity",
1896
+ severity: "critical",
1272
1897
  finding: `Velocity is ${velocity} — generating only ${velocity}× your input as output. You're reading more than you produce.`,
1273
- recommendation: 'Increase output: ask the agent to generate more code/text per turn, reduce over-reading.',
1898
+ recommendation:
1899
+ "Increase output: ask the agent to generate more code/text per turn, reduce over-reading.",
1274
1900
  estimated_yield_impact: `+${Math.round((0.5 - velocity) * 100)}% Υ per 0.1 velocity gain`,
1275
- })
1901
+ });
1276
1902
  } else if (velocity < 1.0) {
1277
1903
  diagnosis.push({
1278
- metric: 'velocity',
1279
- severity: 'warning',
1904
+ metric: "velocity",
1905
+ severity: "warning",
1280
1906
  finding: `Velocity is ${velocity} — below 1.0 (output < input). Healthy operators hit 1.5×+.`,
1281
- recommendation: 'Generate more output per input token — larger edits, more complete responses.',
1907
+ recommendation:
1908
+ "Generate more output per input token — larger edits, more complete responses.",
1282
1909
  estimated_yield_impact: `+${Math.round((1 - velocity) * 30)}% Υ potential`,
1283
- })
1910
+ });
1284
1911
  }
1285
1912
  }
1286
1913
 
1287
1914
  // SNR check — signal-to-noise
1288
- const snr = result.snr
1915
+ const snr = result.snr;
1289
1916
  if (snr !== null && snr < 0.3) {
1290
1917
  diagnosis.push({
1291
- metric: 'snr',
1292
- severity: 'warning',
1918
+ metric: "snr",
1919
+ severity: "warning",
1293
1920
  finding: `SNR is ${snr} — less than 30% of your token flow is output. Input is dominating.`,
1294
- recommendation: 'Reduce fresh input (reuse context) or increase output generation.',
1295
- estimated_yield_impact: 'Indirect — improves both velocity and leverage',
1296
- })
1921
+ recommendation:
1922
+ "Reduce fresh input (reuse context) or increase output generation.",
1923
+ estimated_yield_impact:
1924
+ "Indirect — improves both velocity and leverage",
1925
+ });
1297
1926
  }
1298
1927
 
1299
1928
  // Cache creation ratio — are you over-committing?
1300
1929
  if (cw > 0 && o > 0) {
1301
- const commitRatio = cw / o
1930
+ const commitRatio = cw / o;
1302
1931
  if (commitRatio > 20) {
1303
1932
  diagnosis.push({
1304
- metric: 'cache_creation',
1305
- severity: 'info',
1933
+ metric: "cache_creation",
1934
+ severity: "info",
1306
1935
  finding: `Cache creation is ${commitRatio.toFixed(1)}× your output — high commitment. This is fine if you're rereading it (check leverage), but wasteful if not.`,
1307
- recommendation: "Ensure you're rereading committed context. If leverage is low, you're writing cache you never read.",
1308
- estimated_yield_impact: 'Cost reduction, not Υ directly',
1309
- })
1936
+ recommendation:
1937
+ "Ensure you're rereading committed context. If leverage is low, you're writing cache you never read.",
1938
+ estimated_yield_impact: "Cost reduction, not Υ directly",
1939
+ });
1310
1940
  }
1311
1941
  }
1312
1942
 
1313
1943
  // Input bloat — is fresh input too high relative to total?
1314
- const total = i + o + cw + cr
1944
+ const total = i + o + cw + cr;
1315
1945
  if (total > 0) {
1316
- const inputPct = (i / total) * 100
1946
+ const inputPct = (i / total) * 100;
1317
1947
  if (inputPct > 10) {
1318
1948
  diagnosis.push({
1319
- metric: 'input_bloat',
1320
- severity: 'warning',
1949
+ metric: "input_bloat",
1950
+ severity: "warning",
1321
1951
  finding: `Fresh input is ${inputPct.toFixed(1)}% of your total token flow — high. Efficient operators keep input under 1% by leaning on cache.`,
1322
- recommendation: 'Reduce fresh input by reusing prior context instead of re-pasting it.',
1952
+ recommendation:
1953
+ "Reduce fresh input by reusing prior context instead of re-pasting it.",
1323
1954
  estimated_yield_impact: `+${Math.round((inputPct - 1) * 5)}% Υ potential`,
1324
- })
1955
+ });
1325
1956
  }
1326
1957
  }
1327
1958
 
1328
1959
  // 10xDEV check — is the full cascade compounding?
1329
1960
  if (result.dev10x === null && cw === 0) {
1330
1961
  diagnosis.push({
1331
- metric: '10xdev',
1332
- severity: 'critical',
1333
- finding: "No cache creation — the cascade cannot compound. You're operating in a non-compounding mode (like ChatGPT without prompt caching).",
1334
- recommendation: 'Switch to a platform with prompt caching (Claude Code) or enable caching if available.',
1335
- estimated_yield_impact: 'Enables the full cascade — potentially 10×+ Υ',
1336
- })
1962
+ metric: "10xdev",
1963
+ severity: "critical",
1964
+ finding:
1965
+ "No cache creation — the cascade cannot compound. You're operating in a non-compounding mode (like ChatGPT without prompt caching).",
1966
+ recommendation:
1967
+ "Switch to a platform with prompt caching (Claude Code) or enable caching if available.",
1968
+ estimated_yield_impact: "Enables the full cascade — potentially 10×+ Υ",
1969
+ });
1337
1970
  } else if (result.dev10x !== null && result.dev10x < 1.0) {
1338
1971
  diagnosis.push({
1339
- metric: '10xdev',
1340
- severity: 'info',
1972
+ metric: "10xdev",
1973
+ severity: "info",
1341
1974
  finding: `10xDEV is ${result.dev10x} — below 1.0 (BASE threshold). The cascade is compounding but not strongly.`,
1342
- recommendation: 'Improve both leverage AND velocity — 10xDEV = log10(transmission × commitment × reuse).',
1343
- estimated_yield_impact: 'Class tier improvement',
1344
- })
1975
+ recommendation:
1976
+ "Improve both leverage AND velocity — 10xDEV = log10(transmission × commitment × reuse).",
1977
+ estimated_yield_impact: "Class tier improvement",
1978
+ });
1345
1979
  }
1346
1980
 
1347
1981
  // Sort by severity (critical > warning > info)
1348
- const sevOrder = { critical: 0, warning: 1, info: 2 }
1349
- diagnosis.sort((a, b) => sevOrder[a.severity] - sevOrder[b.severity])
1982
+ const sevOrder = { critical: 0, warning: 1, info: 2 };
1983
+ diagnosis.sort((a, b) => sevOrder[a.severity] - sevOrder[b.severity]);
1350
1984
 
1351
- const healthScore = diagnosis.filter((d) => d.severity === 'critical').length
1352
- const summary = healthScore === 0
1353
- ? `Cascade is healthy — Υ ${result.yield}, class ${result.class}. ${diagnosis.length} minor optimizations available.`
1354
- : `Cascade has ${healthScore} critical leak${healthScore > 1 ? 's' : ''} — Υ ${result.yield}, class ${result.class}. Fix the critical items first.`
1985
+ const healthScore = diagnosis.filter(
1986
+ (d) => d.severity === "critical",
1987
+ ).length;
1988
+ const summary =
1989
+ healthScore === 0
1990
+ ? `Cascade is healthy — Υ ${result.yield}, class ${result.class}. ${diagnosis.length} minor optimizations available.`
1991
+ : `Cascade has ${healthScore} critical leak${healthScore > 1 ? "s" : ""} — Υ ${result.yield}, class ${result.class}. Fix the critical items first.`;
1355
1992
 
1356
1993
  return {
1357
1994
  pillars: p,
@@ -1366,273 +2003,348 @@ export async function callTool(name, args, opts = {}) {
1366
2003
  },
1367
2004
  diagnosis,
1368
2005
  summary,
1369
- }
2006
+ };
1370
2007
  }
1371
2008
 
1372
- if (name === 'suggest_improvements') {
1373
- if (!args?.text) throw new Error('suggest_improvements requires a non-empty `text` argument (token pillars).')
1374
- if (typeof args.text === 'string' && args.text.length > MAX_INPUT) {
1375
- return { status: 'error', reason: 'input_too_large', detail: `text exceeds ${MAX_INPUT} chars.` }
2009
+ if (name === "suggest_improvements") {
2010
+ if (!args?.text)
2011
+ throw new Error(
2012
+ "suggest_improvements requires a non-empty `text` argument (token pillars).",
2013
+ );
2014
+ if (typeof args.text === "string" && args.text.length > MAX_INPUT) {
2015
+ return {
2016
+ status: "error",
2017
+ reason: "input_too_large",
2018
+ detail: `text exceeds ${MAX_INPUT} chars.`,
2019
+ };
1376
2020
  }
1377
2021
 
1378
- const p = parsePillars(args.text)
1379
- const current = withParseWarnings(p, cascade(p))
1380
- const { input: i, output: o, cacheCreate: cw, cacheRead: cr } = p
2022
+ const p = parsePillars(args.text);
2023
+ const current = withParseWarnings(p, cascade(p));
2024
+ const { input: i, output: o, cacheCreate: cw, cacheRead: cr } = p;
1381
2025
 
1382
2026
  // Generate candidate improvements, simulate each, rank by Υ impact
1383
- const candidates = []
2027
+ const candidates = [];
1384
2028
 
1385
2029
  // Strategy 1: Increase cache reads (the #1 lever)
1386
- const crBoosts = cr > 0 ? [1.5, 2, 3, 5] : []
2030
+ const crBoosts = cr > 0 ? [1.5, 2, 3, 5] : [];
1387
2031
  for (const mult of crBoosts) {
1388
- const sim = cascade({ ...p, cacheRead: Math.round(cr * mult) })
2032
+ const sim = cascade({ ...p, cacheRead: Math.round(cr * mult) });
1389
2033
  if (sim.yield !== null) {
1390
2034
  candidates.push({
1391
2035
  action: `Increase cache reads by ${Math.round((mult - 1) * 100)}%`,
1392
- pillar: 'cacheRead',
2036
+ pillar: "cacheRead",
1393
2037
  delta: `+${Math.round(cr * (mult - 1)).toLocaleString()}`,
1394
2038
  simulated_yield: sim.yield,
1395
2039
  yield_delta: Number((sim.yield - (current.yield ?? 0)).toFixed(2)),
1396
2040
  class_after: sim.class,
1397
- rationale: 'Cache reads are the strongest Υ multiplier. More reuse = higher leverage = higher yield.',
1398
- })
2041
+ rationale:
2042
+ "Cache reads are the strongest Υ multiplier. More reuse = higher leverage = higher yield.",
2043
+ });
1399
2044
  }
1400
2045
  }
1401
2046
  // For zero-cache operators, suggest enabling caching with a starter amount
1402
2047
  if (cr === 0 && cw === 0) {
1403
- const starterAmounts = [Math.round(i * 10), Math.round(i * 50), Math.round(i * 100)]
2048
+ const starterAmounts = [
2049
+ Math.round(i * 10),
2050
+ Math.round(i * 50),
2051
+ Math.round(i * 100),
2052
+ ];
1404
2053
  for (const amt of starterAmounts) {
1405
- const sim = cascade({ ...p, cacheCreate: Math.round(amt * 0.5), cacheRead: amt })
2054
+ const sim = cascade({
2055
+ ...p,
2056
+ cacheCreate: Math.round(amt * 0.5),
2057
+ cacheRead: amt,
2058
+ });
1406
2059
  if (sim.yield !== null && sim.yield > 0) {
1407
2060
  candidates.push({
1408
2061
  action: `Enable caching with ${amt.toLocaleString()} cache reads`,
1409
- pillar: 'cacheRead',
2062
+ pillar: "cacheRead",
1410
2063
  delta: `+${amt.toLocaleString()}`,
1411
2064
  simulated_yield: sim.yield,
1412
2065
  yield_delta: Number((sim.yield - (current.yield ?? 0)).toFixed(2)),
1413
2066
  class_after: sim.class,
1414
- rationale: 'You have no cache — enabling it unlocks the cascade. Start by reusing prior context.',
1415
- })
2067
+ rationale:
2068
+ "You have no cache — enabling it unlocks the cascade. Start by reusing prior context.",
2069
+ });
1416
2070
  }
1417
2071
  }
1418
2072
  }
1419
2073
 
1420
2074
  // Strategy 2: Reduce fresh input (Υ = Cr·O/I² — input is squared in the denominator)
1421
- const inputReductions = [0.9, 0.75, 0.5]
2075
+ const inputReductions = [0.9, 0.75, 0.5];
1422
2076
  for (const mult of inputReductions) {
1423
- const newInput = Math.round(i * mult)
1424
- if (newInput < 1) continue
1425
- const sim = cascade({ ...p, input: newInput })
2077
+ const newInput = Math.round(i * mult);
2078
+ if (newInput < 1) continue;
2079
+ const sim = cascade({ ...p, input: newInput });
1426
2080
  if (sim.yield !== null) {
1427
2081
  candidates.push({
1428
2082
  action: `Reduce fresh input by ${Math.round((1 - mult) * 100)}%`,
1429
- pillar: 'input',
2083
+ pillar: "input",
1430
2084
  delta: `-${Math.round(i * (1 - mult)).toLocaleString()}`,
1431
2085
  simulated_yield: sim.yield,
1432
2086
  yield_delta: Number((sim.yield - (current.yield ?? 0)).toFixed(2)),
1433
2087
  class_after: sim.class,
1434
- rationale: 'Input is squared in the Υ denominator (Υ = Cr·O/I²). Reducing input has a quadratic payoff.',
1435
- })
2088
+ rationale:
2089
+ "Input is squared in the Υ denominator (Υ = Cr·O/I²). Reducing input has a quadratic payoff.",
2090
+ });
1436
2091
  }
1437
2092
  }
1438
2093
 
1439
2094
  // Strategy 3: Increase output
1440
- const outputBoosts = [1.25, 1.5, 2]
2095
+ const outputBoosts = [1.25, 1.5, 2];
1441
2096
  for (const mult of outputBoosts) {
1442
- const sim = cascade({ ...p, output: Math.round(o * mult) })
2097
+ const sim = cascade({ ...p, output: Math.round(o * mult) });
1443
2098
  if (sim.yield !== null) {
1444
2099
  candidates.push({
1445
2100
  action: `Increase output by ${Math.round((mult - 1) * 100)}%`,
1446
- pillar: 'output',
2101
+ pillar: "output",
1447
2102
  delta: `+${Math.round(o * (mult - 1)).toLocaleString()}`,
1448
2103
  simulated_yield: sim.yield,
1449
2104
  yield_delta: Number((sim.yield - (current.yield ?? 0)).toFixed(2)),
1450
2105
  class_after: sim.class,
1451
- rationale: 'Output is a linear multiplier in Υ. More output per session = higher yield.',
1452
- })
2106
+ rationale:
2107
+ "Output is a linear multiplier in Υ. More output per session = higher yield.",
2108
+ });
1453
2109
  }
1454
2110
  }
1455
2111
 
1456
2112
  // Strategy 4: Optimize cache creation (reduce if over-committing)
1457
2113
  if (cw > o * 10) {
1458
- const sim = cascade({ ...p, cacheCreate: Math.round(o * 5) })
2114
+ const sim = cascade({ ...p, cacheCreate: Math.round(o * 5) });
1459
2115
  if (sim.yield !== null) {
1460
2116
  candidates.push({
1461
- action: 'Reduce cache creation to 5× output',
1462
- pillar: 'cacheCreate',
2117
+ action: "Reduce cache creation to 5× output",
2118
+ pillar: "cacheCreate",
1463
2119
  delta: `-${Math.round(cw - o * 5).toLocaleString()}`,
1464
2120
  simulated_yield: sim.yield,
1465
2121
  yield_delta: Number((sim.yield - (current.yield ?? 0)).toFixed(2)),
1466
2122
  class_after: sim.class,
1467
- rationale: 'You\'re over-committing cache (cw >> output). Trimming to a healthy ratio reduces cost without hurting yield.',
1468
- })
2123
+ rationale:
2124
+ "You're over-committing cache (cw >> output). Trimming to a healthy ratio reduces cost without hurting yield.",
2125
+ });
1469
2126
  }
1470
2127
  }
1471
2128
 
1472
2129
  // Sort by yield_delta descending, take top 8
1473
- candidates.sort((a, b) => b.yield_delta - a.yield_delta)
1474
- const top = candidates.slice(0, 8).map((c, idx) => ({ rank: idx + 1, ...c }))
2130
+ candidates.sort((a, b) => b.yield_delta - a.yield_delta);
2131
+ const top = candidates
2132
+ .slice(0, 8)
2133
+ .map((c, idx) => ({ rank: idx + 1, ...c }));
1475
2134
 
1476
- const best = top[0]
2135
+ const best = top[0];
1477
2136
  return {
1478
2137
  suggestions: top,
1479
2138
  current_yield: current.yield,
1480
2139
  current_class: current.class,
1481
2140
  best_single_change: best
1482
2141
  ? `${best.action} (Υ ${current.yield} → ${best.simulated_yield}, +${best.yield_delta} yield, class ${best.class_after})`
1483
- : 'No improvements found — your cascade is already optimized.',
1484
- }
2142
+ : "No improvements found — your cascade is already optimized.",
2143
+ };
1485
2144
  }
1486
2145
 
1487
- if (name === 'self_improve') {
2146
+ if (name === "self_improve") {
1488
2147
  // The full self-improvement cycle: pull → diagnose → suggest → simulate.
1489
2148
  // If text is provided, use it as pillars. If not, run tokenpull first.
1490
- let pillars
1491
- let pulledFrom = 'provided'
2149
+ let pillars;
2150
+ let pulledFrom = "provided";
1492
2151
 
1493
2152
  if (args?.text) {
1494
- if (typeof args.text === 'string' && args.text.length > MAX_INPUT) {
1495
- return { status: 'error', reason: 'input_too_large', detail: `text exceeds ${MAX_INPUT} chars.` }
2153
+ if (typeof args.text === "string" && args.text.length > MAX_INPUT) {
2154
+ return {
2155
+ status: "error",
2156
+ reason: "input_too_large",
2157
+ detail: `text exceeds ${MAX_INPUT} chars.`,
2158
+ };
1496
2159
  }
1497
- pillars = parsePillars(args.text)
2160
+ pillars = parsePillars(args.text);
1498
2161
  } else {
1499
2162
  // Run tokenpull to get current pillars from local logs
1500
- const windowType = args?.window || '30d'
2163
+ const windowType = args?.window || "30d";
1501
2164
  try {
1502
- const pullResult = await callTool('tokenpull', { window: windowType }, opts)
1503
- if (pullResult?.status === 'error' || !pullResult?.windows?.length) {
2165
+ const pullResult = await callTool(
2166
+ "tokenpull",
2167
+ { window: windowType },
2168
+ opts,
2169
+ );
2170
+ if (pullResult?.status === "error" || !pullResult?.windows?.length) {
1504
2171
  return {
1505
- status: 'error',
1506
- reason: 'tokenpull_failed',
1507
- detail: 'Could not pull pillars from local logs. Provide pillars via `text` argument instead.',
2172
+ status: "error",
2173
+ reason: "tokenpull_failed",
2174
+ detail:
2175
+ "Could not pull pillars from local logs. Provide pillars via `text` argument instead.",
1508
2176
  pull_result: pullResult,
1509
- }
2177
+ };
1510
2178
  }
1511
2179
  // Use the first window's pillars
1512
- const w = pullResult.windows[0]
1513
- pillars = w.pillars
1514
- pulledFrom = `tokenpull ${w.window || windowType}`
2180
+ const w = pullResult.windows[0];
2181
+ pillars = w.pillars;
2182
+ pulledFrom = `tokenpull ${w.window || windowType}`;
1515
2183
  } catch (e) {
1516
2184
  return {
1517
- status: 'error',
1518
- reason: 'tokenpull_error',
2185
+ status: "error",
2186
+ reason: "tokenpull_error",
1519
2187
  detail: String(e.message || e),
1520
- hint: 'Provide pillars via `text` argument instead.',
1521
- }
2188
+ hint: "Provide pillars via `text` argument instead.",
2189
+ };
1522
2190
  }
1523
2191
  }
1524
2192
 
1525
- const currentResult = withParseWarnings(pillars, cascade(pillars))
1526
- const { input: i, output: o, cacheCreate: cw, cacheRead: cr } = pillars
2193
+ const currentResult = withParseWarnings(pillars, cascade(pillars));
2194
+ const { input: i, output: o, cacheCreate: cw, cacheRead: cr } = pillars;
1527
2195
 
1528
2196
  // ── Step 1: Diagnose ──────────────────────────────────────────────
1529
- const diagnosis = []
1530
- const leverage = currentResult.leverage
2197
+ const diagnosis = [];
2198
+ const leverage = currentResult.leverage;
1531
2199
  if (leverage !== null && leverage < 10) {
1532
2200
  diagnosis.push({
1533
- metric: 'cache_leverage', severity: 'critical',
2201
+ metric: "cache_leverage",
2202
+ severity: "critical",
1534
2203
  finding: `Cache leverage is ${leverage}× — TRANSMITTER-class operators hit 200×+.`,
1535
- recommendation: 'Increase context reuse: load prior session context, use longer threads.',
1536
- })
2204
+ recommendation:
2205
+ "Increase context reuse: load prior session context, use longer threads.",
2206
+ });
1537
2207
  } else if (leverage !== null && leverage < 50) {
1538
2208
  diagnosis.push({
1539
- metric: 'cache_leverage', severity: 'warning',
2209
+ metric: "cache_leverage",
2210
+ severity: "warning",
1540
2211
  finding: `Cache leverage is ${leverage}× — below ARCH+ threshold (~100×+).`,
1541
- recommendation: 'Push cache reads higher by reusing prior context.',
1542
- })
2212
+ recommendation: "Push cache reads higher by reusing prior context.",
2213
+ });
1543
2214
  }
1544
2215
 
1545
- const velocity = currentResult.velocity
2216
+ const velocity = currentResult.velocity;
1546
2217
  if (velocity !== null && velocity < 0.5) {
1547
2218
  diagnosis.push({
1548
- metric: 'velocity', severity: 'critical',
2219
+ metric: "velocity",
2220
+ severity: "critical",
1549
2221
  finding: `Velocity is ${velocity} — generating only ${velocity}× input as output.`,
1550
- recommendation: 'Increase output per turn, reduce over-reading.',
1551
- })
2222
+ recommendation: "Increase output per turn, reduce over-reading.",
2223
+ });
1552
2224
  } else if (velocity !== null && velocity < 1.0) {
1553
2225
  diagnosis.push({
1554
- metric: 'velocity', severity: 'warning',
2226
+ metric: "velocity",
2227
+ severity: "warning",
1555
2228
  finding: `Velocity is ${velocity} — below 1.0. Healthy operators hit 1.5×+.`,
1556
- recommendation: 'Generate more output per input token.',
1557
- })
2229
+ recommendation: "Generate more output per input token.",
2230
+ });
1558
2231
  }
1559
2232
 
1560
2233
  if (currentResult.dev10x === null && cw === 0) {
1561
2234
  diagnosis.push({
1562
- metric: '10xdev', severity: 'critical',
1563
- finding: 'No cache creation — cascade cannot compound.',
1564
- recommendation: 'Switch to a platform with prompt caching (Claude Code).',
1565
- })
2235
+ metric: "10xdev",
2236
+ severity: "critical",
2237
+ finding: "No cache creation — cascade cannot compound.",
2238
+ recommendation:
2239
+ "Switch to a platform with prompt caching (Claude Code).",
2240
+ });
1566
2241
  }
1567
2242
 
1568
- const total = i + o + cw + cr
2243
+ const total = i + o + cw + cr;
1569
2244
  if (total > 0 && (i / total) * 100 > 10) {
1570
2245
  diagnosis.push({
1571
- metric: 'input_bloat', severity: 'warning',
2246
+ metric: "input_bloat",
2247
+ severity: "warning",
1572
2248
  finding: `Fresh input is ${((i / total) * 100).toFixed(1)}% of total flow — efficient operators keep it under 1%.`,
1573
- recommendation: 'Reduce fresh input by reusing prior context.',
1574
- })
2249
+ recommendation: "Reduce fresh input by reusing prior context.",
2250
+ });
1575
2251
  }
1576
2252
 
1577
2253
  // ── Step 2: Suggest (generate + simulate candidates) ──────────────
1578
- const candidates = []
2254
+ const candidates = [];
1579
2255
  // If cacheRead is 0, suggest absolute amounts instead of percentage boosts
1580
- const crBoosts = cr > 0 ? [1.5, 2, 3, 5] : []
2256
+ const crBoosts = cr > 0 ? [1.5, 2, 3, 5] : [];
1581
2257
  for (const mult of crBoosts) {
1582
- const sim = cascade({ ...pillars, cacheRead: Math.round(cr * mult) })
1583
- if (sim.yield !== null) candidates.push({
1584
- action: `Increase cache reads by ${Math.round((mult - 1) * 100)}%`,
1585
- pillar: 'cacheRead', delta: `+${Math.round(cr * (mult - 1)).toLocaleString()}`,
1586
- simulated_yield: sim.yield, yield_delta: Number((sim.yield - (currentResult.yield ?? 0)).toFixed(2)),
1587
- class_after: sim.class, rationale: 'Cache reads are the strongest Υ multiplier.',
1588
- })
2258
+ const sim = cascade({ ...pillars, cacheRead: Math.round(cr * mult) });
2259
+ if (sim.yield !== null)
2260
+ candidates.push({
2261
+ action: `Increase cache reads by ${Math.round((mult - 1) * 100)}%`,
2262
+ pillar: "cacheRead",
2263
+ delta: `+${Math.round(cr * (mult - 1)).toLocaleString()}`,
2264
+ simulated_yield: sim.yield,
2265
+ yield_delta: Number(
2266
+ (sim.yield - (currentResult.yield ?? 0)).toFixed(2),
2267
+ ),
2268
+ class_after: sim.class,
2269
+ rationale: "Cache reads are the strongest Υ multiplier.",
2270
+ });
1589
2271
  }
1590
2272
  // For zero-cache operators, suggest enabling caching with a starter amount
1591
2273
  if (cr === 0 && cw === 0) {
1592
- const starterAmounts = [Math.round(i * 10), Math.round(i * 50), Math.round(i * 100)]
2274
+ const starterAmounts = [
2275
+ Math.round(i * 10),
2276
+ Math.round(i * 50),
2277
+ Math.round(i * 100),
2278
+ ];
1593
2279
  for (const amt of starterAmounts) {
1594
- const sim = cascade({ ...pillars, cacheCreate: Math.round(amt * 0.5), cacheRead: amt })
1595
- if (sim.yield !== null && sim.yield > 0) candidates.push({
1596
- action: `Enable caching with ${amt.toLocaleString()} cache reads`,
1597
- pillar: 'cacheRead', delta: `+${amt.toLocaleString()}`,
1598
- simulated_yield: sim.yield, yield_delta: Number((sim.yield - (currentResult.yield ?? 0)).toFixed(2)),
1599
- class_after: sim.class, rationale: 'You have no cache — enabling it unlocks the cascade. Start by reusing prior context.',
1600
- })
2280
+ const sim = cascade({
2281
+ ...pillars,
2282
+ cacheCreate: Math.round(amt * 0.5),
2283
+ cacheRead: amt,
2284
+ });
2285
+ if (sim.yield !== null && sim.yield > 0)
2286
+ candidates.push({
2287
+ action: `Enable caching with ${amt.toLocaleString()} cache reads`,
2288
+ pillar: "cacheRead",
2289
+ delta: `+${amt.toLocaleString()}`,
2290
+ simulated_yield: sim.yield,
2291
+ yield_delta: Number(
2292
+ (sim.yield - (currentResult.yield ?? 0)).toFixed(2),
2293
+ ),
2294
+ class_after: sim.class,
2295
+ rationale:
2296
+ "You have no cache — enabling it unlocks the cascade. Start by reusing prior context.",
2297
+ });
1601
2298
  }
1602
2299
  }
1603
2300
  for (const mult of [0.9, 0.75, 0.5]) {
1604
- const newInput = Math.round(i * mult)
1605
- if (newInput < 1) continue
1606
- const sim = cascade({ ...pillars, input: newInput })
1607
- if (sim.yield !== null) candidates.push({
1608
- action: `Reduce fresh input by ${Math.round((1 - mult) * 100)}%`,
1609
- pillar: 'input', delta: `-${Math.round(i * (1 - mult)).toLocaleString()}`,
1610
- simulated_yield: sim.yield, yield_delta: Number((sim.yield - (currentResult.yield ?? 0)).toFixed(2)),
1611
- class_after: sim.class, rationale: 'Input is squared in Υ denominator — quadratic payoff.',
1612
- })
2301
+ const newInput = Math.round(i * mult);
2302
+ if (newInput < 1) continue;
2303
+ const sim = cascade({ ...pillars, input: newInput });
2304
+ if (sim.yield !== null)
2305
+ candidates.push({
2306
+ action: `Reduce fresh input by ${Math.round((1 - mult) * 100)}%`,
2307
+ pillar: "input",
2308
+ delta: `-${Math.round(i * (1 - mult)).toLocaleString()}`,
2309
+ simulated_yield: sim.yield,
2310
+ yield_delta: Number(
2311
+ (sim.yield - (currentResult.yield ?? 0)).toFixed(2),
2312
+ ),
2313
+ class_after: sim.class,
2314
+ rationale: "Input is squared in Υ denominator — quadratic payoff.",
2315
+ });
1613
2316
  }
1614
2317
  for (const mult of [1.25, 1.5, 2]) {
1615
- const sim = cascade({ ...pillars, output: Math.round(o * mult) })
1616
- if (sim.yield !== null) candidates.push({
1617
- action: `Increase output by ${Math.round((mult - 1) * 100)}%`,
1618
- pillar: 'output', delta: `+${Math.round(o * (mult - 1)).toLocaleString()}`,
1619
- simulated_yield: sim.yield, yield_delta: Number((sim.yield - (currentResult.yield ?? 0)).toFixed(2)),
1620
- class_after: sim.class, rationale: 'Output is a linear multiplier in Υ.',
1621
- })
2318
+ const sim = cascade({ ...pillars, output: Math.round(o * mult) });
2319
+ if (sim.yield !== null)
2320
+ candidates.push({
2321
+ action: `Increase output by ${Math.round((mult - 1) * 100)}%`,
2322
+ pillar: "output",
2323
+ delta: `+${Math.round(o * (mult - 1)).toLocaleString()}`,
2324
+ simulated_yield: sim.yield,
2325
+ yield_delta: Number(
2326
+ (sim.yield - (currentResult.yield ?? 0)).toFixed(2),
2327
+ ),
2328
+ class_after: sim.class,
2329
+ rationale: "Output is a linear multiplier in Υ.",
2330
+ });
1622
2331
  }
1623
- candidates.sort((a, b) => b.yield_delta - a.yield_delta)
1624
- const suggestions = candidates.slice(0, 8).map((c, idx) => ({ rank: idx + 1, ...c }))
2332
+ candidates.sort((a, b) => b.yield_delta - a.yield_delta);
2333
+ const suggestions = candidates
2334
+ .slice(0, 8)
2335
+ .map((c, idx) => ({ rank: idx + 1, ...c }));
1625
2336
 
1626
2337
  // ── Step 3: Simulate the top suggestion ───────────────────────────
1627
- const best = suggestions[0]
1628
- let bestSimulation = null
2338
+ const best = suggestions[0];
2339
+ let bestSimulation = null;
1629
2340
  if (best) {
1630
- const simPillars = { ...pillars }
1631
- const delta = parseInt(best.delta.replace(/[+\-,]/g, ''), 10)
1632
- if (best.pillar === 'cacheRead') simPillars.cacheRead = cr + delta
1633
- else if (best.pillar === 'input') simPillars.input = Math.max(i - delta, 1)
1634
- else if (best.pillar === 'output') simPillars.output = o + delta
1635
- const simResult = cascade(simPillars)
2341
+ const simPillars = { ...pillars };
2342
+ const delta = parseInt(best.delta.replace(/[+\-,]/g, ""), 10);
2343
+ if (best.pillar === "cacheRead") simPillars.cacheRead = cr + delta;
2344
+ else if (best.pillar === "input")
2345
+ simPillars.input = Math.max(i - delta, 1);
2346
+ else if (best.pillar === "output") simPillars.output = o + delta;
2347
+ const simResult = cascade(simPillars);
1636
2348
  bestSimulation = {
1637
2349
  action: best.action,
1638
2350
  current_yield: currentResult.yield,
@@ -1641,58 +2353,67 @@ export async function callTool(name, args, opts = {}) {
1641
2353
  current_class: currentResult.class,
1642
2354
  simulated_class: simResult.class,
1643
2355
  class_changed: currentResult.class !== simResult.class,
1644
- }
2356
+ };
1645
2357
  }
1646
2358
 
1647
2359
  // ── Step 4: Cycle summary ─────────────────────────────────────────
1648
- const criticalCount = diagnosis.filter((d) => d.severity === 'critical').length
2360
+ const criticalCount = diagnosis.filter(
2361
+ (d) => d.severity === "critical",
2362
+ ).length;
1649
2363
  const cycleSummary = best
1650
2364
  ? `Pulled from ${pulledFrom}. Υ ${currentResult.yield} (${currentResult.class}). ${criticalCount} critical, ${diagnosis.length - criticalCount} other findings. Best: ${best.action} → Υ ${best.simulated_yield} (+${best.yield_delta}).`
1651
- : `Pulled from ${pulledFrom}. Υ ${currentResult.yield} (${currentResult.class}). ${diagnosis.length} findings. No improvements suggested — cascade is optimized.`
2365
+ : `Pulled from ${pulledFrom}. Υ ${currentResult.yield} (${currentResult.class}). ${diagnosis.length} findings. No improvements suggested — cascade is optimized.`;
1652
2366
 
1653
2367
  // ── Step 5: Scope-specific analysis ───────────────────────────────
1654
- const scope = args?.scope || 'daily'
1655
- const modeInfo = currentResult.mode // { mode, confidence }
1656
- const scopeResult = {}
2368
+ const scope = args?.scope || "daily";
2369
+ const modeInfo = currentResult.mode; // { mode, confidence }
2370
+ const scopeResult = {};
1657
2371
 
1658
- if (scope === 'daily') {
2372
+ if (scope === "daily") {
1659
2373
  // Daily: mode + quality score + assessment + advice
1660
- const qs = qualityScore(currentResult.yield ?? 0, modeInfo.mode)
1661
- const expected = MODE_EXPECTED_YIELD[modeInfo.mode] ?? 0
1662
- const assessment = _dailyAssessment(modeInfo.mode, currentResult.yield, qs, expected)
1663
- const advice = _dailyAdvice(modeInfo.mode, currentResult.yield, qs)
1664
- scopeResult.mode = modeInfo
1665
- scopeResult.quality_score = Math.round(qs * 100) / 100
1666
- scopeResult.assessment = assessment
1667
- scopeResult.advice = advice
2374
+ const qs = qualityScore(currentResult.yield ?? 0, modeInfo.mode);
2375
+ const expected = MODE_EXPECTED_YIELD[modeInfo.mode] ?? 0;
2376
+ const assessment = _dailyAssessment(
2377
+ modeInfo.mode,
2378
+ currentResult.yield,
2379
+ qs,
2380
+ expected,
2381
+ );
2382
+ const advice = _dailyAdvice(modeInfo.mode, currentResult.yield, qs);
2383
+ scopeResult.mode = modeInfo;
2384
+ scopeResult.quality_score = Math.round(qs * 100) / 100;
2385
+ scopeResult.assessment = assessment;
2386
+ scopeResult.advice = advice;
1668
2387
  }
1669
2388
 
1670
- if (scope === 'weekly' || scope === 'trend') {
2389
+ if (scope === "weekly" || scope === "trend") {
1671
2390
  // Pull daily rows from ccusage to build weekly snapshots
1672
- const dailyRows = await _pullDailyRows(opts)
1673
- const weeklySnapshots = _compoundWeekly(dailyRows)
1674
- const modeDistribution = _modeDistribution(weeklySnapshots)
1675
- const modeWeightedYield = _modeWeightedYield(weeklySnapshots)
2391
+ const dailyRows = await _pullDailyRows(opts);
2392
+ const weeklySnapshots = _compoundWeekly(dailyRows);
2393
+ const modeDistribution = _modeDistribution(weeklySnapshots);
2394
+ const modeWeightedYield = _modeWeightedYield(weeklySnapshots);
1676
2395
 
1677
2396
  // Compute badges
1678
- const historyForBadges = weeklySnapshots.map(w => ({
2397
+ const historyForBadges = weeklySnapshots.map((w) => ({
1679
2398
  date: w.weekStart,
1680
2399
  mode: w.mode,
1681
2400
  yield: w.yield,
1682
2401
  pillars: w.pillars,
1683
- }))
2402
+ }));
1684
2403
  const badges = computeBadges({
1685
2404
  pillars,
1686
2405
  cascade: currentResult,
1687
2406
  history: historyForBadges,
1688
2407
  isVerified: false, // set by submit_verified
1689
2408
  rank: null, // server-side
1690
- })
2409
+ });
1691
2410
 
1692
- if (scope === 'weekly') {
1693
- scopeResult.mode = modeInfo
2411
+ if (scope === "weekly") {
2412
+ scopeResult.mode = modeInfo;
1694
2413
  // Strip dailyModes from weekly_snapshots — privacy boundary (daily modes never leave the machine)
1695
- const safeSnapshots = weeklySnapshots.map(({ dailyModes, ...rest }) => rest)
2414
+ const safeSnapshots = weeklySnapshots.map(
2415
+ ({ dailyModes, ...rest }) => rest,
2416
+ );
1696
2417
  scopeResult.report = {
1697
2418
  current_mode: modeInfo.mode,
1698
2419
  mode_confidence: modeInfo.confidence,
@@ -1702,15 +2423,15 @@ export async function callTool(name, args, opts = {}) {
1702
2423
  health_score: _healthScore(weeklySnapshots, modeWeightedYield),
1703
2424
  weekly_snapshots: safeSnapshots,
1704
2425
  badges,
1705
- }
2426
+ };
1706
2427
  }
1707
2428
 
1708
- if (scope === 'trend') {
1709
- const yield7d = _yieldForDays(weeklySnapshots, 7)
1710
- const yield30d = _yieldForDays(weeklySnapshots, 30)
1711
- const yield90d = _yieldForDays(weeklySnapshots, 90)
1712
- const trajectory = _trajectory(yield7d, yield30d, yield90d)
1713
- scopeResult.mode = modeInfo
2429
+ if (scope === "trend") {
2430
+ const yield7d = _yieldForDays(weeklySnapshots, 7);
2431
+ const yield30d = _yieldForDays(weeklySnapshots, 30);
2432
+ const yield90d = _yieldForDays(weeklySnapshots, 90);
2433
+ const trajectory = _trajectory(yield7d, yield30d, yield90d);
2434
+ scopeResult.mode = modeInfo;
1714
2435
  scopeResult.trend = {
1715
2436
  yield_7d: yield7d,
1716
2437
  yield_30d: yield30d,
@@ -1718,7 +2439,7 @@ export async function callTool(name, args, opts = {}) {
1718
2439
  trajectory,
1719
2440
  mode_distribution: modeDistribution,
1720
2441
  phase_pattern: _phasePattern(weeklySnapshots),
1721
- }
2442
+ };
1722
2443
  }
1723
2444
  }
1724
2445
 
@@ -1739,10 +2460,10 @@ export async function callTool(name, args, opts = {}) {
1739
2460
  best_simulation: bestSimulation,
1740
2461
  cycle_summary: cycleSummary,
1741
2462
  ...scopeResult,
1742
- }
2463
+ };
1743
2464
  }
1744
2465
 
1745
- throw new Error(`Unknown tool: ${name}`)
2466
+ throw new Error(`Unknown tool: ${name}`);
1746
2467
  }
1747
2468
 
1748
2469
  // ── self_improve scope helpers ──────────────────────────────────────────────
@@ -1750,196 +2471,223 @@ export async function callTool(name, args, opts = {}) {
1750
2471
  /** Pull daily rows from ccusage to build mode history. Returns array of { date, pillars, yield, mode }. */
1751
2472
  async function _pullDailyRows(opts = {}) {
1752
2473
  try {
1753
- const raw = await execFileAsync('ccusage', ['claude', 'daily', '--json'], 15000)
1754
- const rows = JSON.parse(raw)?.daily ?? JSON.parse(raw)
1755
- if (!Array.isArray(rows)) return []
1756
- return rows.map(r => {
1757
- const pillars = {
1758
- input: r.inputTokens ?? r.input_tokens ?? 0,
1759
- output: r.outputTokens ?? r.output_tokens ?? 0,
1760
- cacheCreate: r.cacheCreationTokens ?? r.cache_create_tokens ?? 0,
1761
- cacheRead: r.cacheReadTokens ?? r.cache_read_tokens ?? 0,
1762
- }
1763
- const cas = cascade(pillars)
1764
- const mode = detectMode(pillars)
1765
- return {
1766
- date: r.date ?? r.day ?? null,
1767
- pillars,
1768
- yield: cas.yield ?? 0,
1769
- mode: mode.mode,
1770
- mode_confidence: mode.confidence,
1771
- }
1772
- }).filter(r => r.date)
2474
+ const raw = await execFileAsync(
2475
+ "ccusage",
2476
+ ["claude", "daily", "--json"],
2477
+ 15000,
2478
+ );
2479
+ const rows = JSON.parse(raw)?.daily ?? JSON.parse(raw);
2480
+ if (!Array.isArray(rows)) return [];
2481
+ return rows
2482
+ .map((r) => {
2483
+ const pillars = {
2484
+ input: r.inputTokens ?? r.input_tokens ?? 0,
2485
+ output: r.outputTokens ?? r.output_tokens ?? 0,
2486
+ cacheCreate: r.cacheCreationTokens ?? r.cache_create_tokens ?? 0,
2487
+ cacheRead: r.cacheReadTokens ?? r.cache_read_tokens ?? 0,
2488
+ };
2489
+ const cas = cascade(pillars);
2490
+ const mode = detectMode(pillars);
2491
+ return {
2492
+ date: r.date ?? r.day ?? null,
2493
+ pillars,
2494
+ yield: cas.yield ?? 0,
2495
+ mode: mode.mode,
2496
+ mode_confidence: mode.confidence,
2497
+ };
2498
+ })
2499
+ .filter((r) => r.date);
1773
2500
  } catch {
1774
- return []
2501
+ return [];
1775
2502
  }
1776
2503
  }
1777
2504
 
1778
2505
  /** Compound daily rows into weekly snapshots. Each week = 7 days, starting Monday. */
1779
2506
  function _compoundWeekly(dailyRows) {
1780
- if (!dailyRows || dailyRows.length === 0) return []
2507
+ if (!dailyRows || dailyRows.length === 0) return [];
1781
2508
  // Group by ISO week (week starts Monday)
1782
- const weeks = new Map()
2509
+ const weeks = new Map();
1783
2510
  for (const row of dailyRows) {
1784
- const d = new Date(row.date)
1785
- const day = d.getDay() || 7 // Sunday=0 → 7
1786
- const monday = new Date(d)
1787
- monday.setDate(d.getDate() - day + 1)
1788
- const weekKey = monday.toISOString().slice(0, 10)
2511
+ const d = new Date(row.date);
2512
+ const day = d.getDay() || 7; // Sunday=0 → 7
2513
+ const monday = new Date(d);
2514
+ monday.setDate(d.getDate() - day + 1);
2515
+ const weekKey = monday.toISOString().slice(0, 10);
1789
2516
  if (!weeks.has(weekKey)) {
1790
- weeks.set(weekKey, { weekStart: weekKey, days: [] })
2517
+ weeks.set(weekKey, { weekStart: weekKey, days: [] });
1791
2518
  }
1792
- weeks.get(weekKey).days.push(row)
2519
+ weeks.get(weekKey).days.push(row);
1793
2520
  }
1794
2521
  // Compound each week
1795
- return Array.from(weeks.values()).map(w => {
1796
- const pillars = w.days.reduce((acc, d) => ({
1797
- input: acc.input + d.pillars.input,
1798
- output: acc.output + d.pillars.output,
1799
- cacheCreate: acc.cacheCreate + d.pillars.cacheCreate,
1800
- cacheRead: acc.cacheRead + d.pillars.cacheRead,
1801
- }), { input: 0, output: 0, cacheCreate: 0, cacheRead: 0 })
1802
- const cas = cascade(pillars)
1803
- const mode = detectMode(pillars)
1804
- return {
1805
- weekStart: w.weekStart,
1806
- pillars,
1807
- yield: cas.yield ?? 0,
1808
- mode: mode.mode,
1809
- mode_confidence: mode.confidence,
1810
- dayCount: w.days.length,
1811
- dailyModes: w.days.map(d => d.mode), // stays local — not in submitted report
1812
- }
1813
- }).sort((a, b) => a.weekStart.localeCompare(b.weekStart))
2522
+ return Array.from(weeks.values())
2523
+ .map((w) => {
2524
+ const pillars = w.days.reduce(
2525
+ (acc, d) => ({
2526
+ input: acc.input + d.pillars.input,
2527
+ output: acc.output + d.pillars.output,
2528
+ cacheCreate: acc.cacheCreate + d.pillars.cacheCreate,
2529
+ cacheRead: acc.cacheRead + d.pillars.cacheRead,
2530
+ }),
2531
+ { input: 0, output: 0, cacheCreate: 0, cacheRead: 0 },
2532
+ );
2533
+ const cas = cascade(pillars);
2534
+ const mode = detectMode(pillars);
2535
+ return {
2536
+ weekStart: w.weekStart,
2537
+ pillars,
2538
+ yield: cas.yield ?? 0,
2539
+ mode: mode.mode,
2540
+ mode_confidence: mode.confidence,
2541
+ dayCount: w.days.length,
2542
+ dailyModes: w.days.map((d) => d.mode), // stays local — not in submitted report
2543
+ };
2544
+ })
2545
+ .sort((a, b) => a.weekStart.localeCompare(b.weekStart));
1814
2546
  }
1815
2547
 
1816
2548
  /** Compute mode distribution from weekly snapshots (weekly granularity = privacy boundary). */
1817
2549
  function _modeDistribution(weeklySnapshots) {
1818
- if (!weeklySnapshots || weeklySnapshots.length === 0) return {}
1819
- const counts = { BUILD: 0, EDIT: 0, DEBUG: 0, MAINTAIN: 0, IDLE: 0 }
2550
+ if (!weeklySnapshots || weeklySnapshots.length === 0) return {};
2551
+ const counts = { BUILD: 0, EDIT: 0, DEBUG: 0, MAINTAIN: 0, IDLE: 0 };
1820
2552
  for (const w of weeklySnapshots) {
1821
- counts[w.mode] = (counts[w.mode] || 0) + 1
2553
+ counts[w.mode] = (counts[w.mode] || 0) + 1;
1822
2554
  }
1823
- const total = weeklySnapshots.length
1824
- const dist = {}
2555
+ const total = weeklySnapshots.length;
2556
+ const dist = {};
1825
2557
  for (const [mode, count] of Object.entries(counts)) {
1826
- if (count > 0) dist[mode] = Math.round((count / total) * 100) / 100
2558
+ if (count > 0) dist[mode] = Math.round((count / total) * 100) / 100;
1827
2559
  }
1828
- return dist
2560
+ return dist;
1829
2561
  }
1830
2562
 
1831
2563
  /** Mode-weighted yield — average yield weighted by mode distribution. */
1832
2564
  function _modeWeightedYield(weeklySnapshots) {
1833
- if (!weeklySnapshots || weeklySnapshots.length === 0) return 0
1834
- const total = weeklySnapshots.length
1835
- const sum = weeklySnapshots.reduce((acc, w) => acc + (w.yield || 0), 0)
1836
- return Math.round(sum / total)
2565
+ if (!weeklySnapshots || weeklySnapshots.length === 0) return 0;
2566
+ const total = weeklySnapshots.length;
2567
+ const sum = weeklySnapshots.reduce((acc, w) => acc + (w.yield || 0), 0);
2568
+ return Math.round(sum / total);
1837
2569
  }
1838
2570
 
1839
2571
  /** Peak yield across all weekly snapshots. */
1840
2572
  function _peakYield(weeklySnapshots) {
1841
- if (!weeklySnapshots || weeklySnapshots.length === 0) return 0
1842
- return Math.max(...weeklySnapshots.map(w => w.yield || 0))
2573
+ if (!weeklySnapshots || weeklySnapshots.length === 0) return 0;
2574
+ return Math.max(...weeklySnapshots.map((w) => w.yield || 0));
1843
2575
  }
1844
2576
 
1845
2577
  /** Health score (0-100) — weighted composite of consistency, momentum, quality. */
1846
2578
  function _healthScore(weeklySnapshots, modeWeightedYield) {
1847
- if (!weeklySnapshots || weeklySnapshots.length === 0) return 0
2579
+ if (!weeklySnapshots || weeklySnapshots.length === 0) return 0;
1848
2580
  // Simplified Phase 1 formula:
1849
2581
  // 40% — consistency (how often in MAINTAIN)
1850
2582
  // 30% — momentum (recent yield vs older yield)
1851
2583
  // 30% — quality (mode-weighted yield relative to MAINTAIN expected)
1852
- const dist = _modeDistribution(weeklySnapshots)
1853
- const maintainShare = dist.MAINTAIN || 0
1854
- const consistency = Math.min(maintainShare, 1) * 40
2584
+ const dist = _modeDistribution(weeklySnapshots);
2585
+ const maintainShare = dist.MAINTAIN || 0;
2586
+ const consistency = Math.min(maintainShare, 1) * 40;
1855
2587
 
1856
2588
  // Momentum: compare last 3 weeks to first 3 weeks
1857
- const recent = weeklySnapshots.slice(-3)
1858
- const older = weeklySnapshots.slice(0, 3)
1859
- const recentAvg = recent.reduce((a, w) => a + (w.yield || 0), 0) / (recent.length || 1)
1860
- const olderAvg = older.reduce((a, w) => a + (w.yield || 0), 0) / (older.length || 1)
1861
- const momentumRatio = olderAvg > 0 ? recentAvg / olderAvg : 1
1862
- const momentum = Math.min(momentumRatio, 2) / 2 * 30
2589
+ const recent = weeklySnapshots.slice(-3);
2590
+ const older = weeklySnapshots.slice(0, 3);
2591
+ const recentAvg =
2592
+ recent.reduce((a, w) => a + (w.yield || 0), 0) / (recent.length || 1);
2593
+ const olderAvg =
2594
+ older.reduce((a, w) => a + (w.yield || 0), 0) / (older.length || 1);
2595
+ const momentumRatio = olderAvg > 0 ? recentAvg / olderAvg : 1;
2596
+ const momentum = (Math.min(momentumRatio, 2) / 2) * 30;
1863
2597
 
1864
2598
  // Quality: mode-weighted yield vs MAINTAIN expected (5000)
1865
- const quality = Math.min(modeWeightedYield / 5000, 1) * 30
2599
+ const quality = Math.min(modeWeightedYield / 5000, 1) * 30;
1866
2600
 
1867
- return Math.round(consistency + momentum + quality)
2601
+ return Math.round(consistency + momentum + quality);
1868
2602
  }
1869
2603
 
1870
2604
  /** Yield for the last N days (from weekly snapshots — approximates by summing recent weeks). */
1871
2605
  function _yieldForDays(weeklySnapshots, days) {
1872
- if (!weeklySnapshots || weeklySnapshots.length === 0) return 0
1873
- const weeksNeeded = Math.ceil(days / 7)
1874
- const recent = weeklySnapshots.slice(-weeksNeeded)
1875
- if (recent.length === 0) return 0
1876
- const sum = recent.reduce((a, w) => a + (w.yield || 0), 0)
1877
- return Math.round(sum / recent.length) // average weekly yield
2606
+ if (!weeklySnapshots || weeklySnapshots.length === 0) return 0;
2607
+ const weeksNeeded = Math.ceil(days / 7);
2608
+ const recent = weeklySnapshots.slice(-weeksNeeded);
2609
+ if (recent.length === 0) return 0;
2610
+ const sum = recent.reduce((a, w) => a + (w.yield || 0), 0);
2611
+ return Math.round(sum / recent.length); // average weekly yield
1878
2612
  }
1879
2613
 
1880
2614
  /** Trajectory description from 7d/30d/90d yields. */
1881
2615
  function _trajectory(y7, y30, y90) {
1882
- if (y90 === 0) return 'insufficient_data'
1883
- const r7v30 = y30 > 0 ? y7 / y30 : 1
1884
- const r30v90 = y90 > 0 ? y30 / y90 : 1
1885
- if (r7v30 > 1.2 && r30v90 > 1.0) return 'accelerating'
1886
- if (r7v30 > 1.2) return 'recent_surge'
1887
- if (r7v30 < 0.8 && r30v90 < 0.8) return 'declining'
1888
- if (r7v30 < 0.8) return 'recent_dip'
1889
- if (r30v90 > 1.1) return 'steady_growth'
1890
- return 'stable'
2616
+ if (y90 === 0) return "insufficient_data";
2617
+ const r7v30 = y30 > 0 ? y7 / y30 : 1;
2618
+ const r30v90 = y90 > 0 ? y30 / y90 : 1;
2619
+ if (r7v30 > 1.2 && r30v90 > 1.0) return "accelerating";
2620
+ if (r7v30 > 1.2) return "recent_surge";
2621
+ if (r7v30 < 0.8 && r30v90 < 0.8) return "declining";
2622
+ if (r7v30 < 0.8) return "recent_dip";
2623
+ if (r30v90 > 1.1) return "steady_growth";
2624
+ return "stable";
1891
2625
  }
1892
2626
 
1893
2627
  /** Phase pattern description from mode distribution over time. */
1894
2628
  function _phasePattern(weeklySnapshots) {
1895
- if (!weeklySnapshots || weeklySnapshots.length < 2) return 'insufficient_data'
1896
- const modes = weeklySnapshots.map(w => w.mode)
1897
- const transitions = []
2629
+ if (!weeklySnapshots || weeklySnapshots.length < 2)
2630
+ return "insufficient_data";
2631
+ const modes = weeklySnapshots.map((w) => w.mode);
2632
+ const transitions = [];
1898
2633
  for (let i = 1; i < modes.length; i++) {
1899
2634
  if (modes[i] !== modes[i - 1]) {
1900
- transitions.push(`${modes[i - 1]}→${modes[i]}`)
2635
+ transitions.push(`${modes[i - 1]}→${modes[i]}`);
1901
2636
  }
1902
2637
  }
1903
- if (transitions.length === 0) return `consistent_${modes[0].toLowerCase()}`
2638
+ if (transitions.length === 0) return `consistent_${modes[0].toLowerCase()}`;
1904
2639
  // Check for smooth transitions (BUILD→MAINTAIN without DEBUG)
1905
- const smooth = transitions.every(t => !t.includes('DEBUG'))
1906
- if (smooth && transitions.some(t => t === 'BUILD→MAINTAIN')) return 'smooth_ramp'
1907
- if (transitions.filter(t => t.includes('DEBUG')).length > 2) return 'erratic'
1908
- return 'cyclical'
2640
+ const smooth = transitions.every((t) => !t.includes("DEBUG"));
2641
+ if (smooth && transitions.some((t) => t === "BUILD→MAINTAIN"))
2642
+ return "smooth_ramp";
2643
+ if (transitions.filter((t) => t.includes("DEBUG")).length > 2)
2644
+ return "erratic";
2645
+ return "cyclical";
1909
2646
  }
1910
2647
 
1911
2648
  /** Daily assessment string. */
1912
2649
  function _dailyAssessment(mode, yieldVal, qualityScoreVal, expectedYield) {
1913
- const qsPct = Math.round(qualityScoreVal * 100)
1914
- if (mode === 'IDLE') return 'You\'re idle — no significant token activity today.'
1915
- if (mode === 'MAINTAIN') {
1916
- if (qualityScoreVal >= 0.5) return `You're in MAINTAIN mode. Yield ${yieldVal} is ${qsPct}% of MAINTAIN norm. The cascade is compounding.`
1917
- return `You're in MAINTAIN mode but yield ${yieldVal} is only ${qsPct}% of expected (${expectedYield}). You may be coasting — push for more output.`
2650
+ const qsPct = Math.round(qualityScoreVal * 100);
2651
+ if (mode === "IDLE")
2652
+ return "You're idle — no significant token activity today.";
2653
+ if (mode === "MAINTAIN") {
2654
+ if (qualityScoreVal >= 0.5)
2655
+ return `You're in MAINTAIN mode. Yield ${yieldVal} is ${qsPct}% of MAINTAIN norm. The cascade is compounding.`;
2656
+ return `You're in MAINTAIN mode but yield ${yieldVal} is only ${qsPct}% of expected (${expectedYield}). You may be coasting — push for more output.`;
1918
2657
  }
1919
- if (mode === 'BUILD') {
1920
- if (qualityScoreVal >= 0.5) return `You're in BUILD mode. Yield ${yieldVal} is ${qsPct}% of BUILD norm. Greenfield work — expected to be low.`
1921
- return `You're in BUILD mode. Yield ${yieldVal} is ${qsPct}% of expected (${expectedYield}). Building is slow — keep going.`
2658
+ if (mode === "BUILD") {
2659
+ if (qualityScoreVal >= 0.5)
2660
+ return `You're in BUILD mode. Yield ${yieldVal} is ${qsPct}% of BUILD norm. Greenfield work — expected to be low.`;
2661
+ return `You're in BUILD mode. Yield ${yieldVal} is ${qsPct}% of expected (${expectedYield}). Building is slow — keep going.`;
1922
2662
  }
1923
- if (mode === 'EDIT') {
1924
- if (qualityScoreVal >= 0.5) return `You're in EDIT mode. Yield ${yieldVal} is ${qsPct}% of EDIT norm. Fresh input but producing — good.`
1925
- return `You're in EDIT mode. Yield ${yieldVal} is ${qsPct}% of expected (${expectedYield}). Push for more output per input.`
2663
+ if (mode === "EDIT") {
2664
+ if (qualityScoreVal >= 0.5)
2665
+ return `You're in EDIT mode. Yield ${yieldVal} is ${qsPct}% of EDIT norm. Fresh input but producing — good.`;
2666
+ return `You're in EDIT mode. Yield ${yieldVal} is ${qsPct}% of expected (${expectedYield}). Push for more output per input.`;
1926
2667
  }
1927
- if (mode === 'DEBUG') {
1928
- if (qualityScoreVal >= 0.5) return `You're in DEBUG mode. Yield ${yieldVal} is ${qsPct}% of DEBUG norm. Investigating — yield is expected to be low.`
1929
- return `You're in DEBUG mode. Yield ${yieldVal} is ${qsPct}% of expected (${expectedYield}). Debugging is slow — consider loading prior context.`
2668
+ if (mode === "DEBUG") {
2669
+ if (qualityScoreVal >= 0.5)
2670
+ return `You're in DEBUG mode. Yield ${yieldVal} is ${qsPct}% of DEBUG norm. Investigating — yield is expected to be low.`;
2671
+ return `You're in DEBUG mode. Yield ${yieldVal} is ${qsPct}% of expected (${expectedYield}). Debugging is slow — consider loading prior context.`;
1930
2672
  }
1931
- return `Mode: ${mode}. Yield: ${yieldVal}. Quality: ${qsPct}%.`
2673
+ return `Mode: ${mode}. Yield: ${yieldVal}. Quality: ${qsPct}%.`;
1932
2674
  }
1933
2675
 
1934
2676
  /** Daily advice string. */
1935
2677
  function _dailyAdvice(mode, yieldVal, qs) {
1936
- if (mode === 'MAINTAIN' && qs >= 0.5) return 'Keep the cascade going. Don\'t reset context — let it compound.'
1937
- if (mode === 'MAINTAIN') return 'You\'re in MAINTAIN but underperforming. Push the agent for more output per turn.'
1938
- if (mode === 'BUILD') return 'When you\'re done building, load prior context to transition to MAINTAIN. The cascade rewards reuse.'
1939
- if (mode === 'EDIT') return 'You\'re producing but using fresh input. Try to reuse prior context to boost leverage.'
1940
- if (mode === 'DEBUG') return 'When you\'re done debugging, load prior context to return to MAINTAIN. Don\'t let the debug phase drag on.'
1941
- if (mode === 'IDLE') return 'No activity detected. Start a session to build your cascade.'
1942
- return 'Keep working — the cascade will compound as you build context.'
2678
+ if (mode === "MAINTAIN" && qs >= 0.5)
2679
+ return "Keep the cascade going. Don't reset context — let it compound.";
2680
+ if (mode === "MAINTAIN")
2681
+ return "You're in MAINTAIN but underperforming. Push the agent for more output per turn.";
2682
+ if (mode === "BUILD")
2683
+ return "When you're done building, load prior context to transition to MAINTAIN. The cascade rewards reuse.";
2684
+ if (mode === "EDIT")
2685
+ return "You're producing but using fresh input. Try to reuse prior context to boost leverage.";
2686
+ if (mode === "DEBUG")
2687
+ return "When you're done debugging, load prior context to return to MAINTAIN. Don't let the debug phase drag on.";
2688
+ if (mode === "IDLE")
2689
+ return "No activity detected. Start a session to build your cascade.";
2690
+ return "Keep working — the cascade will compound as you build context.";
1943
2691
  }
1944
2692
 
1945
2693
  /**
@@ -1949,22 +2697,25 @@ function _dailyAdvice(mode, yieldVal, qs) {
1949
2697
  * Weekly granularity is the privacy boundary — no daily modes in the report.
1950
2698
  */
1951
2699
  function _computeReportBlock(pillars) {
1952
- const cas = cascade(pillars)
1953
- const modeInfo = detectMode(pillars)
2700
+ const cas = cascade(pillars);
2701
+ const modeInfo = detectMode(pillars);
1954
2702
  const badges = computeBadges({
1955
2703
  pillars,
1956
2704
  cascade: cas,
1957
2705
  history: [], // no history available at submit time — badges computed from current pillars only
1958
2706
  isVerified: true, // submit_verified is the signed agent path
1959
2707
  rank: null,
1960
- })
2708
+ });
1961
2709
  return {
1962
2710
  current_mode: modeInfo.mode,
1963
2711
  mode_confidence: modeInfo.confidence,
1964
2712
  mode_distribution: { [modeInfo.mode]: 1.0 }, // single-window submission
1965
2713
  mode_weighted_yield: cas.yield ?? 0,
1966
2714
  peak_yield: cas.yield ?? 0,
1967
- health_score: _healthScore([{ mode: modeInfo.mode, yield: cas.yield ?? 0 }], cas.yield ?? 0),
2715
+ health_score: _healthScore(
2716
+ [{ mode: modeInfo.mode, yield: cas.yield ?? 0 }],
2717
+ cas.yield ?? 0,
2718
+ ),
1968
2719
  badges,
1969
- }
2720
+ };
1970
2721
  }