sigrank 0.16.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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,494 @@ 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
+ title: "Rank a paste",
557
+ ...ANNOTATIONS.readOnlyHint,
558
+ ...ANNOTATIONS.destructiveHint,
559
+ ...ANNOTATIONS.idempotentHint,
560
+ ...ANNOTATIONS.openWorldHint,
561
+ },
384
562
  inputSchema: {
385
- type: 'object',
563
+ type: "object",
386
564
  properties: {
387
565
  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}',
566
+ type: "string",
567
+ description:
568
+ '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
569
  },
391
570
  },
392
- required: ['text'],
393
- description: 'Requires the token counts as a string. No other parameters are accepted.',
571
+ required: ["text"],
572
+ description:
573
+ "Requires the token counts as a string. No other parameters are accepted.",
394
574
  },
395
575
  outputSchema: CASCADE_OUTPUT,
396
576
  },
397
577
  {
398
- name: 'get_leaderboard',
578
+ name: "get_leaderboard",
399
579
  description:
400
580
  "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
- annotations: { ...ANNOTATIONS.readOnlyHint, ...ANNOTATIONS.openWorldHint },
581
+ annotations: { title: "Get leaderboard", ...ANNOTATIONS.readOnlyHint, ...ANNOTATIONS.openWorldHint },
402
582
  inputSchema: {
403
- type: 'object',
583
+ type: "object",
404
584
  properties: {},
405
- description: 'This tool takes no parameters. It always fetches the full public leaderboard.',
585
+ description:
586
+ "This tool takes no parameters. It always fetches the full public leaderboard.",
406
587
  },
407
588
  outputSchema: LEADERBOARD_OUTPUT,
408
589
  },
409
590
  {
410
- name: 'get_operator',
591
+ name: "get_operator",
411
592
  description:
412
593
  "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
- annotations: { ...ANNOTATIONS.readOnlyHint, ...ANNOTATIONS.openWorldHint },
594
+ annotations: { title: "Get operator profile", ...ANNOTATIONS.readOnlyHint, ...ANNOTATIONS.openWorldHint },
414
595
  inputSchema: {
415
- type: 'object',
596
+ type: "object",
416
597
  properties: {
417
598
  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.',
599
+ type: "string",
600
+ description:
601
+ '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
602
  },
421
603
  },
422
- required: ['codename'],
423
- description: 'Requires the operator\'s codename. No other parameters are accepted.',
604
+ required: ["codename"],
605
+ description:
606
+ "Requires the operator's codename. No other parameters are accepted.",
424
607
  },
425
608
  outputSchema: OPERATOR_OUTPUT,
426
609
  },
427
610
  {
428
- name: 'submit_paste',
611
+ name: "submit_paste",
429
612
  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 },
613
+ "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.",
614
+ annotations: {
615
+ title: "Submit paste to board",
616
+ ...ANNOTATIONS.destructiveHint,
617
+ ...ANNOTATIONS.idempotentHint,
618
+ ...ANNOTATIONS.openWorldHint,
619
+ },
432
620
  inputSchema: {
433
- type: 'object',
621
+ type: "object",
434
622
  properties: {
435
623
  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}',
624
+ type: "string",
625
+ description:
626
+ '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
627
  },
439
628
  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.',
629
+ type: "string",
630
+ description:
631
+ '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
632
  },
443
633
  },
444
- required: ['text'],
445
- description: 'Requires token counts (text). Codename is optional but required for board submission — omit it for preview-only mode.',
634
+ required: ["text"],
635
+ description:
636
+ "Requires token counts (text). Codename is optional but required for board submission — omit it for preview-only mode.",
446
637
  },
447
638
  outputSchema: SUBMIT_OUTPUT,
448
639
  },
449
640
  {
450
- name: 'tokenpull',
641
+ name: "tokenpull",
451
642
  description:
452
643
  "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
- annotations: { ...ANNOTATIONS.readOnlyHint, ...ANNOTATIONS.openWorldHint },
644
+ annotations: { title: "Pull local token usage", ...ANNOTATIONS.readOnlyHint, ...ANNOTATIONS.openWorldHint },
454
645
  inputSchema: {
455
- type: 'object',
646
+ type: "object",
456
647
  properties: {
457
648
  platform: {
458
- type: 'string',
649
+ type: "string",
459
650
  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).`,
651
+ 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
652
  },
462
653
  },
463
654
  },
464
655
  outputSchema: TOKENPULL_OUTPUT,
465
656
  },
466
657
  {
467
- name: 'tokenpull_submit',
658
+ name: "tokenpull_submit",
468
659
  description:
469
660
  "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 },
661
+ annotations: {
662
+ title: "Pull and submit tokens",
663
+ ...ANNOTATIONS.destructiveHint,
664
+ ...ANNOTATIONS.idempotentHint,
665
+ ...ANNOTATIONS.openWorldHint,
666
+ },
471
667
  inputSchema: {
472
- type: 'object',
668
+ type: "object",
473
669
  properties: {
474
670
  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.',
671
+ type: "string",
672
+ description:
673
+ 'Operator codename to publish under on the leaderboard (e.g. "Iron Lotus"). Required to submit — omit for local preview only.',
477
674
  },
478
675
  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.',
676
+ type: "string",
677
+ enum: ["7d", "30d", "90d", "all"],
678
+ description:
679
+ 'Submit only this time window (default: all 4 windows). Use "7d" for recent activity or "all" for all-time ranking.',
482
680
  },
483
681
  platform: {
484
- type: 'string',
682
+ type: "string",
485
683
  enum: ALL_PLATFORMS,
486
- description: `Source platform to pull from (default: claude). Supported: ${ALL_PLATFORMS.join(', ')}. Each platform reads its own session logs locally.`,
684
+ description: `Source platform to pull from (default: claude). Supported: ${ALL_PLATFORMS.join(", ")}. Each platform reads its own session logs locally.`,
487
685
  },
488
686
  },
489
687
  },
490
688
  outputSchema: SUBMIT_OUTPUT,
491
689
  },
492
690
  {
493
- name: 'rank_windows',
691
+ name: "rank_windows",
494
692
  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 },
693
+ "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.",
694
+ annotations: {
695
+ title: "Rank all time windows",
696
+ ...ANNOTATIONS.readOnlyHint,
697
+ ...ANNOTATIONS.idempotentHint,
698
+ ...ANNOTATIONS.openWorldHint,
699
+ },
497
700
  inputSchema: {
498
- type: 'object',
701
+ type: "object",
499
702
  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)' },
703
+ "7d": {
704
+ type: "string",
705
+ description:
706
+ "ccusage/tokscale paste or JSON for the 7-day window (optional)",
707
+ },
708
+ "30d": {
709
+ type: "string",
710
+ description:
711
+ "ccusage/tokscale paste or JSON for the 30-day window (optional)",
712
+ },
713
+ "90d": {
714
+ type: "string",
715
+ description:
716
+ "ccusage/tokscale paste or JSON for the 90-day window (optional)",
717
+ },
718
+ all: {
719
+ type: "string",
720
+ description:
721
+ "ccusage/tokscale paste or JSON for the all-time window (optional)",
722
+ },
723
+ source_tool: {
724
+ type: "string",
725
+ enum: [
726
+ "ccusage",
727
+ "tokscale",
728
+ "claude_max",
729
+ "token_dashboard",
730
+ "other",
731
+ ],
732
+ description:
733
+ "which token reader produced the paste (for cross-tool variance tracking)",
734
+ },
505
735
  },
506
736
  // at least one window paste is required (runtime check backs this up)
507
- anyOf: [{ required: ['7d'] }, { required: ['30d'] }, { required: ['90d'] }, { required: ['all'] }],
737
+ anyOf: [
738
+ { required: ["7d"] },
739
+ { required: ["30d"] },
740
+ { required: ["90d"] },
741
+ { required: ["all"] },
742
+ ],
508
743
  },
509
744
  outputSchema: {
510
- type: 'object',
745
+ type: "object",
511
746
  properties: {
512
747
  windows: {
513
- type: 'array',
514
- description: 'Cascade results per window',
748
+ type: "array",
749
+ description: "Cascade results per window",
515
750
  items: {
516
- type: 'object',
751
+ type: "object",
517
752
  properties: {
518
- window: { type: 'string', enum: ['7d', '30d', '90d', 'all'] },
753
+ window: { type: "string", enum: ["7d", "30d", "90d", "all"] },
519
754
  ...CASCADE_OUTPUT.properties,
520
755
  },
521
756
  },
@@ -524,176 +759,303 @@ export const TOOLS = [
524
759
  },
525
760
  },
526
761
  {
527
- name: 'watch_tokenpull',
762
+ name: "watch_tokenpull",
528
763
  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 },
764
+ "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).",
765
+ annotations: {
766
+ title: "Watch token pull",
767
+ ...ANNOTATIONS.idempotentHint,
768
+ ...ANNOTATIONS.openWorldHint,
769
+ },
531
770
  inputSchema: {
532
- type: 'object',
771
+ type: "object",
533
772
  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)' },
773
+ platform: {
774
+ type: "string",
775
+ enum: ALL_PLATFORMS,
776
+ description: "platform to watch (default: claude)",
777
+ },
778
+ interval_s: {
779
+ type: "number",
780
+ description:
781
+ "advisory poll cadence in seconds (default: 60, min: 10) — echoed back as poll_interval_s; does not make the call block or loop",
782
+ },
783
+ window: {
784
+ type: "string",
785
+ enum: ["7d", "30d", "90d", "all"],
786
+ description:
787
+ "which window to watch (default: 7d — most sensitive to recent activity)",
788
+ },
789
+ submit: {
790
+ type: "boolean",
791
+ description:
792
+ "auto-submit the watched window to the board as a VERIFIED operator each poll (requires `enroll`; default false = preview only)",
793
+ },
538
794
  },
539
795
  },
540
796
  outputSchema: TOKENPULL_OUTPUT,
541
797
  },
542
798
  {
543
- name: 'tokenpull_compare',
799
+ name: "tokenpull_compare",
544
800
  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.',
546
- annotations: { ...ANNOTATIONS.readOnlyHint, ...ANNOTATIONS.openWorldHint },
801
+ "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.",
802
+ annotations: { title: "Compare token sources", ...ANNOTATIONS.readOnlyHint, ...ANNOTATIONS.openWorldHint },
547
803
  inputSchema: {
548
- type: 'object',
804
+ type: "object",
549
805
  properties: {
550
806
  platform: {
551
- type: 'string',
807
+ type: "string",
552
808
  enum: ALL_PLATFORMS,
553
- description: 'platform to compare (default: claude). token-dash and App only available for claude.',
809
+ description:
810
+ "platform to compare (default: claude). token-dash and App only available for claude.",
554
811
  },
555
812
  },
556
813
  },
557
814
  outputSchema: COMPARE_OUTPUT,
558
815
  },
559
816
  {
560
- name: 'enroll',
817
+ name: "enroll",
561
818
  description:
562
819
  '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 },
820
+ annotations: {
821
+ title: "Enroll device identity",
822
+ ...ANNOTATIONS.destructiveHint,
823
+ ...ANNOTATIONS.idempotentHint,
824
+ ...ANNOTATIONS.openWorldHint,
825
+ },
564
826
  inputSchema: {
565
- type: 'object',
827
+ type: "object",
566
828
  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)' },
829
+ code: {
830
+ type: "string",
831
+ description:
832
+ "the key / connect code (SIGR-XXXXX-XXXXX-XXXXX) from Settings → New key (or Generate connect code)",
833
+ },
834
+ device_label: {
835
+ type: "string",
836
+ description:
837
+ "optional label for this device (default: hostname · agent version)",
838
+ },
569
839
  },
570
- required: ['code'],
840
+ required: ["code"],
571
841
  },
572
842
  outputSchema: ENROLL_OUTPUT,
573
843
  },
574
844
  {
575
- name: 'submit_verified',
845
+ name: "submit_verified",
576
846
  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 },
847
+ "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.",
848
+ annotations: {
849
+ title: "Submit verified score",
850
+ ...ANNOTATIONS.destructiveHint,
851
+ ...ANNOTATIONS.idempotentHint,
852
+ ...ANNOTATIONS.openWorldHint,
853
+ },
579
854
  inputSchema: {
580
- type: 'object',
855
+ type: "object",
581
856
  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' },
857
+ window: {
858
+ type: "string",
859
+ enum: ["7d", "30d", "90d", "all"],
860
+ description: "submit only this window (default: all 4)",
861
+ },
862
+ platform: {
863
+ type: "string",
864
+ enum: [...ALL_PLATFORMS, "multi"],
865
+ description:
866
+ "source platform (default: claude). 'multi' = combined cascade summed across all locally-detected platforms (needs 2+ active); empty windows are skipped.",
867
+ },
868
+ dry_run: {
869
+ type: "boolean",
870
+ description:
871
+ "build + sign but do NOT publish — returns the exact payload that would be POSTed (token counts only), so you can inspect before submitting",
872
+ },
585
873
  },
586
874
  },
587
875
  outputSchema: SUBMIT_OUTPUT,
588
876
  },
589
877
  {
590
- name: 'simulate_change',
878
+ name: "simulate_change",
591
879
  description:
592
880
  "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 },
881
+ annotations: {
882
+ title: "Simulate metric change",
883
+ ...ANNOTATIONS.readOnlyHint,
884
+ ...ANNOTATIONS.idempotentHint,
885
+ ...ANNOTATIONS.openWorldHint,
886
+ },
594
887
  inputSchema: {
595
- type: 'object',
888
+ type: "object",
596
889
  properties: {
597
890
  text: {
598
- type: 'string',
599
- description: 'Current token pillars — ccusage JSON or "input output cacheCreate cacheRead" (same format as rank_paste).',
891
+ type: "string",
892
+ description:
893
+ 'Current token pillars — ccusage JSON or "input output cacheCreate cacheRead" (same format as rank_paste).',
600
894
  },
601
895
  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.',
896
+ type: "object",
897
+ description:
898
+ "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
899
  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' },
900
+ input: {
901
+ type: ["number", "string"],
902
+ description:
903
+ 'new input token count (absolute) or "+/-N" for a relative delta',
904
+ },
905
+ output: {
906
+ type: ["number", "string"],
907
+ description:
908
+ 'new output token count (absolute) or "+/-N" for a relative delta',
909
+ },
910
+ cacheCreate: {
911
+ type: ["number", "string"],
912
+ description:
913
+ 'new cache-create token count (absolute) or "+/-N" for a relative delta',
914
+ },
915
+ cacheRead: {
916
+ type: ["number", "string"],
917
+ description:
918
+ 'new cache-read token count (absolute) or "+/-N" for a relative delta',
919
+ },
609
920
  },
610
921
  },
611
922
  },
612
- required: ['text', 'changes'],
923
+ required: ["text", "changes"],
613
924
  },
614
925
  outputSchema: SIMULATE_OUTPUT,
615
926
  },
616
927
  {
617
- name: 'diagnose_cascade',
928
+ name: "diagnose_cascade",
618
929
  description:
619
930
  "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 },
931
+ annotations: {
932
+ title: "Diagnose cascade breakdown",
933
+ ...ANNOTATIONS.readOnlyHint,
934
+ ...ANNOTATIONS.idempotentHint,
935
+ ...ANNOTATIONS.openWorldHint,
936
+ },
621
937
  inputSchema: {
622
- type: 'object',
938
+ type: "object",
623
939
  properties: {
624
940
  text: {
625
- type: 'string',
626
- description: 'Token pillars — ccusage JSON or "input output cacheCreate cacheRead" (same format as rank_paste).',
941
+ type: "string",
942
+ description:
943
+ 'Token pillars — ccusage JSON or "input output cacheCreate cacheRead" (same format as rank_paste).',
627
944
  },
628
945
  },
629
- required: ['text'],
946
+ required: ["text"],
630
947
  },
631
948
  outputSchema: DIAGNOSE_OUTPUT,
632
949
  },
633
950
  {
634
- name: 'suggest_improvements',
951
+ name: "suggest_improvements",
635
952
  description:
636
953
  "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 },
954
+ annotations: {
955
+ title: "Suggest improvements",
956
+ ...ANNOTATIONS.readOnlyHint,
957
+ ...ANNOTATIONS.idempotentHint,
958
+ ...ANNOTATIONS.openWorldHint,
959
+ },
638
960
  inputSchema: {
639
- type: 'object',
961
+ type: "object",
640
962
  properties: {
641
963
  text: {
642
- type: 'string',
643
- description: 'Token pillars — ccusage JSON or "input output cacheCreate cacheRead" (same format as rank_paste).',
964
+ type: "string",
965
+ description:
966
+ 'Token pillars — ccusage JSON or "input output cacheCreate cacheRead" (same format as rank_paste).',
644
967
  },
645
968
  },
646
- required: ['text'],
969
+ required: ["text"],
647
970
  },
648
971
  outputSchema: SUGGEST_OUTPUT,
649
972
  },
650
973
  {
651
- name: 'self_improve',
974
+ name: "self_improve",
652
975
  description:
653
976
  "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 },
977
+ annotations: {
978
+ title: "Self-improve plan",
979
+ ...ANNOTATIONS.readOnlyHint,
980
+ ...ANNOTATIONS.idempotentHint,
981
+ ...ANNOTATIONS.openWorldHint,
982
+ },
655
983
  inputSchema: {
656
- type: 'object',
984
+ type: "object",
657
985
  properties: {
658
986
  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.',
987
+ type: "string",
988
+ description:
989
+ 'Optional: token pillars — ccusage JSON or "input output cacheCreate cacheRead". If omitted, runs tokenpull to get current pillars from local logs.',
661
990
  },
662
991
  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.',
992
+ type: "string",
993
+ enum: ["7d", "30d", "90d", "all"],
994
+ description:
995
+ "Which time window to pull when running tokenpull (default: 30d). Ignored if `text` is provided.",
666
996
  },
667
997
  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.',
998
+ type: "string",
999
+ enum: ["daily", "weekly", "trend"],
1000
+ description:
1001
+ '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
1002
  },
672
1003
  },
673
1004
  },
674
1005
  outputSchema: {
675
- type: 'object',
1006
+ type: "object",
676
1007
  properties: {
677
- pillars: { type: 'object', description: 'The 4 raw token pillars used' },
1008
+ pillars: {
1009
+ type: "object",
1010
+ description: "The 4 raw token pillars used",
1011
+ },
678
1012
  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' },
1013
+ diagnosis: {
1014
+ type: "array",
1015
+ description: "Efficiency leaks found (from diagnose_cascade)",
1016
+ },
1017
+ suggestions: {
1018
+ type: "array",
1019
+ description: "Ranked improvements (from suggest_improvements)",
1020
+ },
1021
+ best_simulation: {
1022
+ type: "object",
1023
+ description: "Simulated result of the top suggestion",
1024
+ },
1025
+ cycle_summary: {
1026
+ type: "string",
1027
+ description: "One-line summary of the full cycle",
1028
+ },
683
1029
  // 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)' },
1030
+ mode: {
1031
+ type: "object",
1032
+ description:
1033
+ "Detected mode { mode, confidence } — present when scope is daily/weekly/trend",
1034
+ },
1035
+ quality_score: {
1036
+ type: "number",
1037
+ description: "Yield relative to mode expectation (daily scope)",
1038
+ },
1039
+ assessment: {
1040
+ type: "string",
1041
+ description: "One-line assessment for daily scope",
1042
+ },
1043
+ advice: {
1044
+ type: "string",
1045
+ description: "Advice for next session (daily scope)",
1046
+ },
1047
+ report: {
1048
+ type: "object",
1049
+ description: "Weekly report artifact (weekly scope)",
1050
+ },
1051
+ trend: { type: "object", description: "Trend analysis (trend scope)" },
690
1052
  },
691
1053
  },
692
1054
  },
693
- ]
1055
+ ];
694
1056
 
695
1057
  // tokenpull window key → the board's window_type enum.
696
- const WINDOW_TYPE = { '7d': '7d', '30d': '30d', '90d': '90d', all: 'all_time' }
1058
+ const WINDOW_TYPE = { "7d": "7d", "30d": "30d", "90d": "90d", all: "all_time" };
697
1059
 
698
1060
  // E3: client-side auto-submit cooldown for watch_tokenpull. The server already dedups
699
1061
  // identical snapshots (exact hash → 422), but a noisy poll loop with submit:true could still
@@ -701,8 +1063,8 @@ const WINDOW_TYPE = { '7d': '7d', '30d': '30d', '90d': '90d', all: 'all_time' }
701
1063
  // per platform+window (in-memory, per process). Keyed by platform:window so different
702
1064
  // platforms/windows don't block each other, and only armed on a non-error submit so a
703
1065
  // network failure doesn't lock out retries for 5 minutes.
704
- const WATCH_SUBMIT_COOLDOWN_MS = 5 * 60 * 1000
705
- const _lastWatchSubmitAt = new Map()
1066
+ const WATCH_SUBMIT_COOLDOWN_MS = 5 * 60 * 1000;
1067
+ const _lastWatchSubmitAt = new Map();
706
1068
 
707
1069
  // ── Shared active-platform loader ───────────────────────────────────────────
708
1070
  // THE single data path for "show my cascade across platforms" — used by `me`,
@@ -711,73 +1073,105 @@ const _lastWatchSubmitAt = new Map()
711
1073
  // keeps only platforms with real local data, and sorts claude → codex → rest.
712
1074
  // Pass `platforms` to scope it (e.g. ['claude'] for a fast first paint).
713
1075
  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)))
1076
+ const targets = platforms && platforms.length ? platforms : ALL_PLATFORMS;
1077
+ const settled = await Promise.allSettled(
1078
+ targets.map((p) => callTool("tokenpull", { platform: p }, opts)),
1079
+ );
716
1080
  const active = settled
717
- .filter((r) => r.status === 'fulfilled' && r.value)
1081
+ .filter((r) => r.status === "fulfilled" && r.value)
718
1082
  .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
1083
+ .filter((d) =>
1084
+ (d.windows || []).some(
1085
+ (w) => (w.pillars?.input ?? 0) + (w.pillars?.output ?? 0) > 0,
1086
+ ),
1087
+ );
1088
+ const rank = (p) =>
1089
+ p === "claude" ? -2 : p === "codex" ? -1 : ALL_PLATFORMS.indexOf(p);
1090
+ active.sort((a, b) => rank(a.platform) - rank(b.platform));
1091
+ return active;
723
1092
  }
724
1093
 
725
1094
  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
1095
+ const apiBase = opts.apiBase || DEFAULT_API_BASE;
1096
+ const timeoutMs = opts.fetchTimeout ?? DEFAULT_FETCH_TIMEOUT;
1097
+ const rawFetch = opts.fetchImpl || fetch;
729
1098
 
730
1099
  // Wrap every fetch with an AbortController timeout so a hung network call never
731
1100
  // blocks the MCP client indefinitely.
732
1101
  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
- }
1102
+ const ac = new AbortController();
1103
+ const timer = setTimeout(() => ac.abort(), timeoutMs);
1104
+ return rawFetch(url, { ...init, signal: ac.signal }).finally(() =>
1105
+ clearTimeout(timer),
1106
+ );
1107
+ };
737
1108
 
738
1109
  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
- }
1110
+ const res = await doFetch(`${apiBase}${path}`, {
1111
+ headers: { accept: "application/json" },
1112
+ });
1113
+ if (!res.ok) throw new Error(`SigRank API ${path} → HTTP ${res.status}`);
1114
+ return res.json();
1115
+ };
743
1116
 
744
1117
  // Helper: attach _parseWarnings from pillars onto the cascade result so they
745
1118
  // are always visible in the tool output for review.
746
1119
  const withParseWarnings = (pillars, cascadeResult) => {
747
1120
  if (pillars._parseWarnings && pillars._parseWarnings.length > 0) {
748
- const existing = cascadeResult.warnings || []
749
- return { ...cascadeResult, warnings: [...existing, ...pillars._parseWarnings.map((w) => `parse:${w}`)] }
1121
+ const existing = cascadeResult.warnings || [];
1122
+ return {
1123
+ ...cascadeResult,
1124
+ warnings: [
1125
+ ...existing,
1126
+ ...pillars._parseWarnings.map((w) => `parse:${w}`),
1127
+ ],
1128
+ };
750
1129
  }
751
- return cascadeResult
752
- }
1130
+ return cascadeResult;
1131
+ };
753
1132
 
754
- if (name === 'rank_paste') {
755
- if (!args?.text) throw new Error('rank_paste requires a non-empty `text` argument.')
1133
+ if (name === "rank_paste") {
1134
+ if (!args?.text)
1135
+ throw new Error("rank_paste requires a non-empty `text` argument.");
756
1136
  // 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.` }
1137
+ if (typeof args.text === "string" && args.text.length > MAX_INPUT) {
1138
+ return {
1139
+ status: "error",
1140
+ reason: "input_too_large",
1141
+ detail: `text exceeds ${MAX_INPUT} chars (${args.text.length}). Paste only the token-count table, not full output.`,
1142
+ };
759
1143
  }
760
- const pillars = parsePillars(args.text)
761
- const c = withParseWarnings(pillars, cascade(pillars))
762
- return { ...c, card: narrate(c) }
1144
+ const pillars = parsePillars(args.text);
1145
+ const c = withParseWarnings(pillars, cascade(pillars));
1146
+ return { ...c, card: narrate(c) };
763
1147
  }
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)}`)
1148
+ if (name === "get_leaderboard")
1149
+ return fetchJson("/api/v1/leaderboard?metric=yield_");
1150
+ if (name === "get_operator") {
1151
+ const codename = String(args?.codename || "").trim();
1152
+ if (!codename)
1153
+ throw new Error("get_operator requires a non-empty `codename` argument.");
1154
+ return fetchJson(`/api/v1/operators/${encodeURIComponent(codename)}`);
769
1155
  }
770
1156
 
771
- if (name === 'enroll') {
1157
+ if (name === "enroll") {
772
1158
  // Redeem a web connect code → bind this device. Generates/loads the local keypair;
773
1159
  // 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)
1160
+ const code = String(args?.code || "").trim();
1161
+ if (!code)
1162
+ throw new Error(
1163
+ "enroll requires a `code` — paste your connect code from signalaf.com → Settings → Connect a device.",
1164
+ );
1165
+ const id = opts.identity || ensureIdentity();
1166
+ const deviceLabel = String(
1167
+ args?.device_label || `${os.hostname()} · ${id.agent_version}`,
1168
+ ).slice(0, 120);
778
1169
  const res = await doFetch(`${apiBase}/api/v1/devices/enroll`, {
779
- method: 'POST',
780
- headers: { 'content-type': 'application/json', accept: 'application/json' },
1170
+ method: "POST",
1171
+ headers: {
1172
+ "content-type": "application/json",
1173
+ accept: "application/json",
1174
+ },
781
1175
  body: JSON.stringify({
782
1176
  code,
783
1177
  device_id: id.device_id,
@@ -785,82 +1179,126 @@ export async function callTool(name, args, opts = {}) {
785
1179
  device_label: deviceLabel,
786
1180
  agent_version: id.agent_version,
787
1181
  }),
788
- })
789
- let ack
790
- try { ack = await res.json() } catch { ack = {} }
791
- if (res.status === 201 && ack.status === 'enrolled') {
1182
+ });
1183
+ let ack;
1184
+ try {
1185
+ ack = await res.json();
1186
+ } catch {
1187
+ ack = {};
1188
+ }
1189
+ if (res.status === 201 && ack.status === "enrolled") {
792
1190
  // 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 })
1191
+ if (!opts.identity)
1192
+ recordEnrollment({
1193
+ codename: ack.codename,
1194
+ operator_id: ack.operator_id,
1195
+ });
794
1196
  return {
795
- status: 'enrolled',
1197
+ status: "enrolled",
796
1198
  codename: ack.codename ?? null,
797
1199
  operator_id: ack.operator_id ?? null,
798
1200
  device_id: id.device_id,
799
- trust_status: ack.trust_status ?? 'trusted',
800
- }
1201
+ trust_status: ack.trust_status ?? "trusted",
1202
+ };
801
1203
  }
802
- return { status: 'error', httpStatus: res.status, reason: ack.reason || ack.status || `http_${res.status}`, detail: ack.detail ?? null }
1204
+ return {
1205
+ status: "error",
1206
+ httpStatus: res.status,
1207
+ reason: ack.reason || ack.status || `http_${res.status}`,
1208
+ detail: ack.detail ?? null,
1209
+ };
803
1210
  }
804
1211
 
805
- if (name === 'submit_verified') {
1212
+ if (name === "submit_verified") {
806
1213
  // The enrolled, signed publish path → /api/v1/snapshots (only verified rows rank).
807
- const id = opts.identity || ensureIdentity()
1214
+ const id = opts.identity || ensureIdentity();
808
1215
  if (!id.codename || !id.operator_id) {
809
- return { status: 'not_enrolled', detail: 'Run `npx sigrank-mcp enroll` to bind this device first.' }
1216
+ return {
1217
+ status: "not_enrolled",
1218
+ detail: "Run `npx sigrank-mcp enroll` to bind this device first.",
1219
+ };
810
1220
  }
811
- const platform = args?.platform || 'claude'
1221
+ const platform = args?.platform || "claude";
812
1222
 
813
1223
  // MULTI: the combined cross-platform cascade. The dashboard already SUMS every
814
1224
  // active platform's pillars (a "claude+codex" row) but never submitted it — this
815
1225
  // is that missing submission. Aggregate every locally-detected platform's pillars
816
1226
  // per window and publish as platform='multi' = the operator's TOTAL usage. Empty
817
1227
  // windows are skipped so a no-activity window never lands as a degenerate row.
818
- if (platform === 'multi') {
819
- const detected = []
1228
+ if (platform === "multi") {
1229
+ const detected = [];
820
1230
  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)
1231
+ const r = await pullByPlatform(p, opts).catch(() => null);
1232
+ const live =
1233
+ r &&
1234
+ (r.windows || []).some(
1235
+ (w) =>
1236
+ w.pillars.input +
1237
+ w.pillars.output +
1238
+ w.pillars.cacheCreate +
1239
+ w.pillars.cacheRead >
1240
+ 0,
1241
+ );
1242
+ if (live) detected.push(r);
826
1243
  }
827
1244
  if (detected.length < 2) {
828
1245
  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'}).`,
1246
+ platform: "multi",
1247
+ codename: id.codename,
1248
+ operator_id: id.operator_id,
1249
+ status: "skipped",
1250
+ reason: "need_2_platforms",
1251
+ detail: `multi needs 2+ active platforms; found ${detected.length} (${detected.map((d) => d.platform).join(", ") || "none"}).`,
832
1252
  windows: [],
833
- }
1253
+ };
834
1254
  }
835
- const winKeys = args?.window ? [args.window] : ['7d', '30d', '90d', 'all']
836
- const out = []
1255
+ const winKeys = args?.window
1256
+ ? [args.window]
1257
+ : ["7d", "30d", "90d", "all"];
1258
+ const out = [];
837
1259
  for (const wk of winKeys) {
838
- const sum = { input: 0, output: 0, cacheCreate: 0, cacheRead: 0 }
839
- let msgs = 0
1260
+ const sum = { input: 0, output: 0, cacheCreate: 0, cacheRead: 0 };
1261
+ let msgs = 0;
840
1262
  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
1263
+ const w = (d.windows || []).find((x) => x.window === wk);
1264
+ if (!w) continue;
1265
+ sum.input += w.pillars.input || 0;
1266
+ sum.output += w.pillars.output || 0;
1267
+ sum.cacheCreate += w.pillars.cacheCreate || 0;
1268
+ sum.cacheRead += w.pillars.cacheRead || 0;
1269
+ msgs += w.messages || 0;
848
1270
  }
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 })
1271
+ if (sum.input + sum.output + sum.cacheCreate + sum.cacheRead <= 0)
1272
+ continue; // skip empty window
1273
+ const report = _computeReportBlock(sum);
1274
+ const r = await submitSignedWindow(wk, sum, msgs, id, {
1275
+ apiBase,
1276
+ fetchImpl: doFetch,
1277
+ platform: "multi",
1278
+ now: opts.now,
1279
+ dryRun: !!args?.dry_run,
1280
+ report,
1281
+ });
1282
+ out.push({ window: wk, pillars: sum, ...r });
853
1283
  }
854
- return { platform: 'multi', codename: id.codename, operator_id: id.operator_id, sources: detected.map((d) => d.platform), windows: out }
1284
+ return {
1285
+ platform: "multi",
1286
+ codename: id.codename,
1287
+ operator_id: id.operator_id,
1288
+ sources: detected.map((d) => d.platform),
1289
+ windows: out,
1290
+ };
855
1291
  }
856
1292
 
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 = []
1293
+ const pulled = await pullByPlatform(platform, opts);
1294
+ const targets = args?.window
1295
+ ? pulled.windows.filter((w) => w.window === args.window)
1296
+ : pulled.windows;
1297
+ const out = [];
860
1298
  for (const w of targets) {
861
1299
  // Compute the cascade report block (mode + badges + health) for this window.
862
1300
  // Pure math, computed locally, submitted alongside the 4 token pillars.
863
- const report = _computeReportBlock(w.pillars)
1301
+ const report = _computeReportBlock(w.pillars);
864
1302
  const r = await submitSignedWindow(w.window, w.pillars, w.messages, id, {
865
1303
  apiBase,
866
1304
  fetchImpl: doFetch,
@@ -868,22 +1306,33 @@ export async function callTool(name, args, opts = {}) {
868
1306
  now: opts.now,
869
1307
  dryRun: !!args?.dry_run,
870
1308
  report,
871
- })
872
- out.push({ window: w.window, pillars: w.pillars, ...r })
1309
+ });
1310
+ out.push({ window: w.window, pillars: w.pillars, ...r });
873
1311
  }
874
- return { platform: pulled.platform, codename: id.codename, operator_id: id.operator_id, generatedAt: pulled.generatedAt, windows: out }
1312
+ return {
1313
+ platform: pulled.platform,
1314
+ codename: id.codename,
1315
+ operator_id: id.operator_id,
1316
+ generatedAt: pulled.generatedAt,
1317
+ windows: out,
1318
+ };
875
1319
  }
876
1320
 
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.` }
1321
+ if (name === "submit_paste") {
1322
+ if (!args?.text)
1323
+ throw new Error("submit_paste requires a non-empty `text` argument.");
1324
+ if (typeof args.text === "string" && args.text.length > MAX_INPUT) {
1325
+ return {
1326
+ status: "error",
1327
+ reason: "input_too_large",
1328
+ detail: `text exceeds ${MAX_INPUT} chars (${args.text.length}). Paste only the token-count table, not full output.`,
1329
+ };
881
1330
  }
882
1331
  // 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')
1332
+ const pillars = parsePillars(args.text);
1333
+ const c = withParseWarnings(pillars, cascade(pillars));
1334
+ const codename = String(args?.codename || "").trim();
1335
+ const card = narrate(c, codename || "This operator");
887
1336
 
888
1337
  // No codename → cannot publish (the board endpoint requires it). Fail fast at the
889
1338
  // tool boundary with a clear message instead of an opaque server 400.
@@ -891,104 +1340,198 @@ export async function callTool(name, args, opts = {}) {
891
1340
  return {
892
1341
  ...c,
893
1342
  card,
894
- submission: { status: 'not_submitted', reason: 'codename_required', detail: 'Pass a codename to publish to the board. Showing local preview only.' },
895
- }
1343
+ submission: {
1344
+ status: "not_submitted",
1345
+ reason: "codename_required",
1346
+ detail:
1347
+ "Pass a codename to publish to the board. Showing local preview only.",
1348
+ },
1349
+ };
896
1350
  }
897
1351
 
898
1352
  // Submit the RAW paste so the server re-parses + re-scores authoritatively — the
899
1353
  // 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' })
1354
+ const stamp = uploadStamp({
1355
+ codename,
1356
+ pillars: c.pillars,
1357
+ source: "web_paste",
1358
+ });
901
1359
  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 } }
1360
+ method: "POST",
1361
+ headers: {
1362
+ "content-type": "application/json",
1363
+ accept: "application/json",
1364
+ },
1365
+ body: JSON.stringify({
1366
+ codename,
1367
+ raw_paste: String(args?.text || ""),
1368
+ ...stamp,
1369
+ }),
1370
+ });
1371
+ let ack;
1372
+ try {
1373
+ ack = await res.json();
1374
+ } catch {
1375
+ ack = {
1376
+ status: "error",
1377
+ detail: `HTTP ${res.status} (non-JSON response)`,
1378
+ };
1379
+ }
1380
+ const ranked = !!(
1381
+ res.ok &&
1382
+ ack.verification_tier === "verified" &&
1383
+ ack.persisted === true
1384
+ );
1385
+ return {
1386
+ ...c,
1387
+ card,
1388
+ ranked,
1389
+ submission: { ...stamp, httpStatus: res.status, ranked, ...ack },
1390
+ };
910
1391
  }
911
1392
 
912
- if (name === 'tokenpull') {
1393
+ if (name === "tokenpull") {
913
1394
  // 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)
1395
+ const platform = args?.platform || "claude";
1396
+ const pulled = await pullByPlatform(platform, opts);
916
1397
  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 }
1398
+ const c = cascade(w.pillars);
1399
+ return {
1400
+ window: w.window,
1401
+ messages: w.messages,
1402
+ pillars: w.pillars,
1403
+ cascade: c,
1404
+ card: narrate(c, `${w.window} ${platform}`),
1405
+ };
1406
+ });
1407
+ return {
1408
+ platform: pulled.platform,
1409
+ estimated: pulled.estimated || false,
1410
+ ...(pulled.ioRatio ? { ioRatio: pulled.ioRatio } : {}),
1411
+ generatedAt: pulled.generatedAt,
1412
+ files: pulled.files,
1413
+ totalMessages: pulled.totalMessages,
1414
+ windows,
1415
+ };
921
1416
  }
922
1417
 
923
- if (name === 'tokenpull_submit') {
1418
+ if (name === "tokenpull_submit") {
924
1419
  // Pull local usage, then publish each window's CANONICAL pillars to the board
925
1420
  // (server re-scores). The board stays platform-agnostic via the 4 pillars; the
926
1421
  // 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 = []
1422
+ const codename = String(args?.codename || "").trim();
1423
+ const pulled = await pullByPlatform(args?.platform || "claude", opts);
1424
+ const targets = args?.window
1425
+ ? pulled.windows.filter((w) => w.window === args.window)
1426
+ : pulled.windows;
1427
+ const out = [];
931
1428
  for (const w of targets) {
932
- const c = cascade(w.pillars)
933
- const card = narrate(c, `${w.window} window`)
1429
+ const c = cascade(w.pillars);
1430
+ const card = narrate(c, `${w.window} window`);
934
1431
  if (!codename) {
935
- out.push({ window: w.window, pillars: w.pillars, cascade: c, card, submission: { status: 'not_submitted', reason: 'codename_required' } })
936
- continue
1432
+ out.push({
1433
+ window: w.window,
1434
+ pillars: w.pillars,
1435
+ cascade: c,
1436
+ card,
1437
+ submission: { status: "not_submitted", reason: "codename_required" },
1438
+ });
1439
+ continue;
937
1440
  }
938
1441
  // 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 })
1442
+ const rawPaste = `${w.pillars.input} ${w.pillars.output} ${w.pillars.cacheCreate} ${w.pillars.cacheRead}`;
1443
+ const windowType = WINDOW_TYPE[w.window] || w.window;
1444
+ const stamp = uploadStamp({
1445
+ codename,
1446
+ window: windowType,
1447
+ pillars: w.pillars,
1448
+ platform: pulled.platform,
1449
+ });
942
1450
  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)` } }
1451
+ method: "POST",
1452
+ headers: {
1453
+ "content-type": "application/json",
1454
+ accept: "application/json",
1455
+ },
1456
+ body: JSON.stringify({
1457
+ codename,
1458
+ raw_paste: rawPaste,
1459
+ window_type: windowType,
1460
+ telemetry: { platform: { primary: pulled.platform } },
1461
+ ...stamp,
1462
+ }),
1463
+ });
1464
+ let ack;
1465
+ try {
1466
+ ack = await res.json();
1467
+ } catch {
1468
+ ack = { status: "error", detail: `HTTP ${res.status} (non-JSON)` };
1469
+ }
949
1470
  // ranked = actually on the board (verified + persisted), not just received — parity
950
1471
  // 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 } })
1472
+ const ranked = !!(
1473
+ res.ok &&
1474
+ ack.verification_tier === "verified" &&
1475
+ ack.persisted === true
1476
+ );
1477
+ out.push({
1478
+ window: w.window,
1479
+ pillars: w.pillars,
1480
+ cascade: c,
1481
+ card,
1482
+ ranked,
1483
+ submission: { ...stamp, httpStatus: res.status, ranked, ...ack },
1484
+ });
953
1485
  }
954
- return { platform: pulled.platform, codename: codename || null, generatedAt: pulled.generatedAt, windows: out }
1486
+ return {
1487
+ platform: pulled.platform,
1488
+ codename: codename || null,
1489
+ generatedAt: pulled.generatedAt,
1490
+ windows: out,
1491
+ };
955
1492
  }
956
1493
 
957
- if (name === 'rank_windows') {
1494
+ if (name === "rank_windows") {
958
1495
  // Score up to 4 named window pastes independently. Named-key parsing only —
959
1496
  // positional is unsafe here because dashboards list cache_read before cache_create
960
1497
  // (the WINDOWED_PROFILES swap gotcha). Each window goes through parsePillars →
961
1498
  // cascade → narrate individually; results are collected into a windows[] array
962
1499
  // 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
1500
+ const WINDOW_KEYS = ["7d", "30d", "90d", "all"];
1501
+ const sourceTool = args?.source_tool || null;
965
1502
  // E2: reject any oversized window paste up front (token tables are tiny).
966
1503
  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.` }
1504
+ const v = args?.[wk];
1505
+ if (typeof v === "string" && v.length > MAX_INPUT) {
1506
+ return {
1507
+ status: "error",
1508
+ reason: "input_too_large",
1509
+ detail: `window '${wk}' exceeds ${MAX_INPUT} chars (${v.length}). Paste only the token-count table.`,
1510
+ };
970
1511
  }
971
1512
  }
972
- const windows = []
1513
+ const windows = [];
973
1514
  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 })
1515
+ const text = args?.[wk];
1516
+ if (!text || typeof text !== "string" || !text.trim()) continue;
1517
+ const pillars = parsePillars(text);
1518
+ const c = withParseWarnings(pillars, cascade(pillars));
1519
+ const card = narrate(c, `${wk} window`);
1520
+ windows.push({ window: wk, pillars, cascade: c, card });
980
1521
  }
981
1522
  if (windows.length === 0) {
982
- throw new Error('rank_windows requires at least one window paste (7d, 30d, 90d, or all).')
1523
+ throw new Error(
1524
+ "rank_windows requires at least one window paste (7d, 30d, 90d, or all).",
1525
+ );
983
1526
  }
984
1527
  return {
985
1528
  windows,
986
1529
  source_tool: sourceTool,
987
- note: 'Local preview only — use tokenpull_submit to publish to the board.',
988
- }
1530
+ note: "Local preview only — use tokenpull_submit to publish to the board.",
1531
+ };
989
1532
  }
990
1533
 
991
- if (name === 'watch_tokenpull') {
1534
+ if (name === "watch_tokenpull") {
992
1535
  // Poll the local token logs at a configurable interval and return the cascade
993
1536
  // diff whenever new sessions appear. One poll cycle per MCP call — the client
994
1537
  // is responsible for re-calling at the desired cadence (MCP tools are stateless;
@@ -996,44 +1539,63 @@ export async function callTool(name, args, opts = {}) {
996
1539
  //
997
1540
  // With submit:true + an enrolled device, this also signs + POSTs the watched window
998
1541
  // 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)
1542
+ const platform = args?.platform || "claude";
1543
+ const watchWindow = args?.window || "7d";
1544
+ const intervalS = Math.max(10, Number(args?.interval_s) || 60);
1002
1545
 
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.`)
1546
+ const pulled = await pullByPlatform(platform, opts);
1547
+ const win = pulled.windows.find((w) => w.window === watchWindow);
1548
+ if (!win)
1549
+ throw new Error(
1550
+ `watch_tokenpull: window '${watchWindow}' not found in pull result.`,
1551
+ );
1006
1552
 
1007
- const c = cascade(win.pillars)
1008
- const card = narrate(c, `${watchWindow} ${platform}`)
1553
+ const c = cascade(win.pillars);
1554
+ const card = narrate(c, `${watchWindow} ${platform}`);
1009
1555
 
1010
1556
  // AUTH.WIRE (D7 §7): when submit is on AND the device is enrolled, sign + POST the
1011
1557
  // watched window to the verified ingest path. Default OFF = preview only. The server
1012
1558
  // dedups identical re-submits (exact snapshot_hash → 422), so re-calling is safe.
1013
- let auth_submit = null
1559
+ let auth_submit = null;
1014
1560
  if (args?.submit === true) {
1015
- const id = opts.identity || ensureIdentity()
1561
+ const id = opts.identity || ensureIdentity();
1016
1562
  if (id.codename && id.operator_id && id.private_key_pkcs8_b64) {
1017
1563
  // E3: client-side cooldown — at most one auto-submit per platform+window per 5 min.
1018
1564
  // Prevents a fast poll loop from churning the board even before the server's
1019
1565
  // 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)
1566
+ const clockNow = typeof opts.now === "number" ? opts.now : Date.now();
1567
+ const cdKey = `${pulled.platform}:${watchWindow}`;
1568
+ const last = _lastWatchSubmitAt.get(cdKey);
1023
1569
  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 }
1570
+ const waitS = Math.ceil(
1571
+ (WATCH_SUBMIT_COOLDOWN_MS - (clockNow - last)) / 1000,
1572
+ );
1573
+ auth_submit = {
1574
+ status: "cooldown",
1575
+ detail: `auto-submit for '${cdKey}' on cooldown — next in ~${waitS}s (max once / 5 min).`,
1576
+ retry_after_s: waitS,
1577
+ };
1026
1578
  } 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)
1579
+ auth_submit = await submitSignedWindow(
1580
+ watchWindow,
1581
+ win.pillars,
1582
+ win.messages,
1583
+ id,
1584
+ {
1585
+ apiBase,
1586
+ fetchImpl: doFetch,
1587
+ platform: pulled.platform,
1588
+ now: opts.now,
1589
+ },
1590
+ );
1591
+ if (auth_submit?.status !== "error")
1592
+ _lastWatchSubmitAt.set(cdKey, clockNow);
1034
1593
  }
1035
1594
  } else {
1036
- auth_submit = { status: 'not_enrolled', detail: 'Run `npx sigrank-mcp enroll` to auto-submit verified runs.' }
1595
+ auth_submit = {
1596
+ status: "not_enrolled",
1597
+ detail: "Run `npx sigrank-mcp enroll` to auto-submit verified runs.",
1598
+ };
1037
1599
  }
1038
1600
  }
1039
1601
  return {
@@ -1046,154 +1608,210 @@ export async function callTool(name, args, opts = {}) {
1046
1608
  generatedAt: pulled.generatedAt,
1047
1609
  poll_interval_s: intervalS,
1048
1610
  auth_submit,
1049
- note: 'One snapshot per call — re-call at your poll interval to detect changes.',
1050
- }
1611
+ note: "One snapshot per call — re-call at your poll interval to detect changes.",
1612
+ };
1051
1613
  }
1052
1614
 
1053
- if (name === 'tokenpull_compare') {
1054
- const platform = args?.platform || 'claude'
1055
- const WINS = ['7d', '30d', '90d', 'all']
1615
+ if (name === "tokenpull_compare") {
1616
+ const platform = args?.platform || "claude";
1617
+ const WINS = ["7d", "30d", "90d", "all"];
1056
1618
 
1057
1619
  // Pull all four sources in parallel (verifiers are now async via execFile)
1058
1620
  const [tpResult, ccPillars, tdPillars, tsPillars] = await Promise.all([
1059
1621
  pullByPlatform(platform, opts).catch(() => null),
1060
1622
  _ccusagePillars(platform).catch(() => null),
1061
- (platform === 'claude' ? _tokenDashPillars() : Promise.resolve(null)).catch(() => null),
1623
+ (platform === "claude"
1624
+ ? _tokenDashPillars()
1625
+ : Promise.resolve(null)
1626
+ ).catch(() => null),
1062
1627
  _tokscalePillars(platform).catch(() => null),
1063
- ])
1628
+ ]);
1064
1629
 
1065
1630
  // Build tokenpull window lookup
1066
- const tpByWin = {}
1067
- for (const w of (tpResult?.windows ?? [])) tpByWin[w.window] = w.pillars
1631
+ const tpByWin = {};
1632
+ for (const w of tpResult?.windows ?? []) tpByWin[w.window] = w.pillars;
1068
1633
 
1069
1634
  // Helper: delta % vs tokenpull baseline
1070
- const delta = (val, base) => base > 0 ? +((( val - base) / base * 100).toFixed(1)) : null
1635
+ const delta = (val, base) =>
1636
+ base > 0 ? +(((val - base) / base) * 100).toFixed(1) : null;
1071
1637
 
1072
1638
  // Build per-source per-window comparison
1073
1639
  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
- ]
1640
+ {
1641
+ source: "tokenpull",
1642
+ note: "JSONL deduped by msg id — canon source",
1643
+ byWin: tpByWin,
1644
+ },
1645
+ {
1646
+ source: "ccusage",
1647
+ note: "ccusage CLI — monthly only",
1648
+ byWin: ccPillars ?? {},
1649
+ },
1650
+ {
1651
+ source: "token-dash",
1652
+ note: "token-dashboard SQLite — all-time only",
1653
+ byWin: tdPillars ?? {},
1654
+ },
1655
+ {
1656
+ source: "tokscale",
1657
+ note: "tokscale_report.json — all-time only",
1658
+ byWin: tsPillars ?? {},
1659
+ },
1660
+ ];
1079
1661
 
1080
- const comparison = {}
1662
+ const comparison = {};
1081
1663
  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
- })
1664
+ const baseP = tpByWin[win];
1665
+ comparison[win] = SOURCES.filter((s) => s.byWin[win] != null).map((s) => {
1666
+ const p = s.byWin[win];
1667
+ const cas = cascade(p);
1668
+ const entry = {
1669
+ source: s.source,
1670
+ note: s.note,
1671
+ pillars: p,
1672
+ cascade: {
1673
+ yield: cas.yield,
1674
+ snr: cas.snr,
1675
+ leverage: cas.leverage,
1676
+ velocity: cas.velocity,
1677
+ dev10x: cas.dev10x,
1678
+ class: cas.class,
1679
+ },
1680
+ };
1681
+ if (s.source !== "tokenpull" && baseP) {
1682
+ entry.delta_vs_tokenpull = {
1683
+ input: delta(p.input, baseP.input),
1684
+ output: delta(p.output, baseP.output),
1685
+ cacheCreate: delta(p.cacheCreate, baseP.cacheCreate),
1686
+ cacheRead: delta(p.cacheRead, baseP.cacheRead),
1687
+ };
1688
+ }
1689
+ return entry;
1690
+ });
1104
1691
  }
1105
1692
 
1106
1693
  // Sources available summary
1107
- const available = SOURCES
1108
- .filter(s => Object.keys(s.byWin).length > 0)
1109
- .map(s => s.source)
1694
+ const available = SOURCES.filter(
1695
+ (s) => Object.keys(s.byWin).length > 0,
1696
+ ).map((s) => s.source);
1110
1697
 
1111
1698
  return {
1112
1699
  platform,
1113
1700
  estimated: tpResult?.estimated ?? false,
1114
1701
  generatedAt: tpResult?.generatedAt ?? new Date().toISOString(),
1115
1702
  sources_available: available,
1116
- sources_missing: SOURCES.map(s => s.source).filter(s => !available.includes(s)),
1703
+ sources_missing: SOURCES.map((s) => s.source).filter(
1704
+ (s) => !available.includes(s),
1705
+ ),
1117
1706
  comparison,
1118
- }
1707
+ };
1119
1708
  }
1120
1709
 
1121
- if (name === 'simulate_change') {
1710
+ if (name === "simulate_change") {
1122
1711
  // The first prescriptive tool — "what if I changed my token mix?"
1123
1712
  // Pure local math: parse current pillars, apply proposed changes, run the
1124
1713
  // 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.` }
1714
+ if (!args?.text)
1715
+ throw new Error(
1716
+ "simulate_change requires a non-empty `text` argument (current pillars).",
1717
+ );
1718
+ if (typeof args.text === "string" && args.text.length > MAX_INPUT) {
1719
+ return {
1720
+ status: "error",
1721
+ reason: "input_too_large",
1722
+ detail: `text exceeds ${MAX_INPUT} chars.`,
1723
+ };
1128
1724
  }
1129
- if (!args?.changes || typeof args.changes !== 'object') {
1130
- throw new Error('simulate_change requires a `changes` object with at least one pillar change.')
1725
+ if (!args?.changes || typeof args.changes !== "object") {
1726
+ throw new Error(
1727
+ "simulate_change requires a `changes` object with at least one pillar change.",
1728
+ );
1131
1729
  }
1132
1730
 
1133
- const currentPillars = parsePillars(args.text)
1134
- const current = withParseWarnings(currentPillars, cascade(currentPillars))
1731
+ const currentPillars = parsePillars(args.text);
1732
+ const current = withParseWarnings(currentPillars, cascade(currentPillars));
1135
1733
 
1136
1734
  // Apply changes: each pillar is either an absolute number (replace) or a
1137
1735
  // string starting with +/- (relative delta). Omitted pillars are unchanged.
1138
- const PILLAR_KEYS = ['input', 'output', 'cacheCreate', 'cacheRead']
1139
- const simulated = { ...currentPillars }
1140
- const appliedChanges = {}
1736
+ const PILLAR_KEYS = ["input", "output", "cacheCreate", "cacheRead"];
1737
+ const simulated = { ...currentPillars };
1738
+ const appliedChanges = {};
1141
1739
 
1142
1740
  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('-')) {
1741
+ if (args.changes[key] == null) continue;
1742
+ const raw = args.changes[key];
1743
+ let newVal;
1744
+
1745
+ if (typeof raw === "number") {
1746
+ newVal = raw;
1747
+ } else if (typeof raw === "string") {
1748
+ const trimmed = raw.trim();
1749
+ if (trimmed.startsWith("+") || trimmed.startsWith("-")) {
1152
1750
  // Relative delta: add/subtract from current value
1153
- const delta = Number(trimmed)
1751
+ const delta = Number(trimmed);
1154
1752
  if (!Number.isFinite(delta)) {
1155
- return { status: 'error', reason: 'invalid_change', detail: `changes.${key}: "${raw}" is not a valid relative delta.` }
1753
+ return {
1754
+ status: "error",
1755
+ reason: "invalid_change",
1756
+ detail: `changes.${key}: "${raw}" is not a valid relative delta.`,
1757
+ };
1156
1758
  }
1157
- newVal = currentPillars[key] + delta
1759
+ newVal = currentPillars[key] + delta;
1158
1760
  } else {
1159
1761
  // Absolute value as a string
1160
- newVal = Number(trimmed)
1762
+ newVal = Number(trimmed);
1161
1763
  }
1162
1764
  } else {
1163
- return { status: 'error', reason: 'invalid_change', detail: `changes.${key}: expected number or string, got ${typeof raw}.` }
1765
+ return {
1766
+ status: "error",
1767
+ reason: "invalid_change",
1768
+ detail: `changes.${key}: expected number or string, got ${typeof raw}.`,
1769
+ };
1164
1770
  }
1165
1771
 
1166
1772
  if (!Number.isFinite(newVal)) {
1167
- return { status: 'error', reason: 'invalid_change', detail: `changes.${key}: result is not a finite number.` }
1773
+ return {
1774
+ status: "error",
1775
+ reason: "invalid_change",
1776
+ detail: `changes.${key}: result is not a finite number.`,
1777
+ };
1168
1778
  }
1169
1779
  // Clamp to non-negative — token counts can't be negative
1170
1780
  if (newVal < 0) {
1171
- return { status: 'error', reason: 'invalid_change', detail: `changes.${key}: result ${newVal} is negative — token counts must be >= 0.` }
1781
+ return {
1782
+ status: "error",
1783
+ reason: "invalid_change",
1784
+ detail: `changes.${key}: result ${newVal} is negative — token counts must be >= 0.`,
1785
+ };
1172
1786
  }
1173
1787
 
1174
- simulated[key] = newVal
1788
+ simulated[key] = newVal;
1175
1789
  appliedChanges[key] = {
1176
1790
  from: currentPillars[key],
1177
1791
  to: newVal,
1178
1792
  delta: newVal - currentPillars[key],
1179
- }
1793
+ };
1180
1794
  }
1181
1795
 
1182
1796
  if (Object.keys(appliedChanges).length === 0) {
1183
- return { status: 'error', reason: 'no_changes', detail: 'No pillar changes specified in the `changes` object.' }
1797
+ return {
1798
+ status: "error",
1799
+ reason: "no_changes",
1800
+ detail: "No pillar changes specified in the `changes` object.",
1801
+ };
1184
1802
  }
1185
1803
 
1186
- const simulatedResult = cascade(simulated)
1804
+ const simulatedResult = cascade(simulated);
1187
1805
 
1188
1806
  // Compute deltas for every cascade metric
1189
1807
  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
- }
1808
+ if (curr == null && sim == null) return null;
1809
+ if (curr == null) return { from: null, to: sim, delta: null };
1810
+ if (sim == null) return { from: curr, to: null, delta: null };
1811
+ return { from: curr, to: sim, delta: Number((sim - curr).toFixed(4)) };
1812
+ };
1195
1813
 
1196
- const classChanged = current.class !== simulatedResult.class
1814
+ const classChanged = current.class !== simulatedResult.class;
1197
1815
 
1198
1816
  return {
1199
1817
  current: {
@@ -1206,7 +1824,12 @@ export async function callTool(name, args, opts = {}) {
1206
1824
  class: current.class,
1207
1825
  },
1208
1826
  simulated: {
1209
- pillars: { input: simulated.input, output: simulated.output, cacheCreate: simulated.cacheCreate, cacheRead: simulated.cacheRead },
1827
+ pillars: {
1828
+ input: simulated.input,
1829
+ output: simulated.output,
1830
+ cacheCreate: simulated.cacheCreate,
1831
+ cacheRead: simulated.cacheRead,
1832
+ },
1210
1833
  yield: simulatedResult.yield,
1211
1834
  snr: simulatedResult.snr,
1212
1835
  leverage: simulatedResult.leverage,
@@ -1223,135 +1846,160 @@ export async function callTool(name, args, opts = {}) {
1223
1846
  dev10x: metricDelta(current.dev10x, simulatedResult.dev10x),
1224
1847
  },
1225
1848
  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
- }
1849
+ ...(classChanged
1850
+ ? { class_transition: `${current.class} → ${simulatedResult.class}` }
1851
+ : {}),
1852
+ ...(simulatedResult.warnings
1853
+ ? { simulated_warnings: simulatedResult.warnings }
1854
+ : {}),
1855
+ note: "Local simulation only — no submission. The actual score depends on server-side RS.xx weights and class thresholds.",
1856
+ };
1230
1857
  }
1231
1858
 
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.` }
1859
+ if (name === "diagnose_cascade") {
1860
+ if (!args?.text)
1861
+ throw new Error(
1862
+ "diagnose_cascade requires a non-empty `text` argument (token pillars).",
1863
+ );
1864
+ if (typeof args.text === "string" && args.text.length > MAX_INPUT) {
1865
+ return {
1866
+ status: "error",
1867
+ reason: "input_too_large",
1868
+ detail: `text exceeds ${MAX_INPUT} chars.`,
1869
+ };
1236
1870
  }
1237
1871
 
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 = []
1872
+ const p = parsePillars(args.text);
1873
+ const result = withParseWarnings(p, cascade(p));
1874
+ const { input: i, output: o, cacheCreate: cw, cacheRead: cr } = p;
1875
+ const diagnosis = [];
1242
1876
 
1243
1877
  // Cache leverage check — the #1 cascade efficiency signal
1244
- const leverage = result.leverage
1878
+ const leverage = result.leverage;
1245
1879
  if (leverage !== null) {
1246
1880
  if (leverage < 10) {
1247
1881
  diagnosis.push({
1248
- metric: 'cache_leverage',
1249
- severity: 'critical',
1882
+ metric: "cache_leverage",
1883
+ severity: "critical",
1250
1884
  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.',
1885
+ recommendation:
1886
+ "Increase context reuse: load prior session context, use longer conversation threads, reference earlier outputs.",
1252
1887
  estimated_yield_impact: `+${Math.round((1 - leverage / 50) * 100)}% Υ potential`,
1253
- })
1888
+ });
1254
1889
  } else if (leverage < 50) {
1255
1890
  diagnosis.push({
1256
- metric: 'cache_leverage',
1257
- severity: 'warning',
1891
+ metric: "cache_leverage",
1892
+ severity: "warning",
1258
1893
  finding: `Cache leverage is ${leverage}× — decent but below the ARCH+ threshold (~100×+).`,
1259
- recommendation: 'Push cache reads higher by reusing prior context more aggressively.',
1894
+ recommendation:
1895
+ "Push cache reads higher by reusing prior context more aggressively.",
1260
1896
  estimated_yield_impact: `+${Math.round((1 - leverage / 100) * 50)}% Υ potential`,
1261
- })
1897
+ });
1262
1898
  }
1263
1899
  }
1264
1900
 
1265
1901
  // Velocity check — output per input
1266
- const velocity = result.velocity
1902
+ const velocity = result.velocity;
1267
1903
  if (velocity !== null) {
1268
1904
  if (velocity < 0.5) {
1269
1905
  diagnosis.push({
1270
- metric: 'velocity',
1271
- severity: 'critical',
1906
+ metric: "velocity",
1907
+ severity: "critical",
1272
1908
  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.',
1909
+ recommendation:
1910
+ "Increase output: ask the agent to generate more code/text per turn, reduce over-reading.",
1274
1911
  estimated_yield_impact: `+${Math.round((0.5 - velocity) * 100)}% Υ per 0.1 velocity gain`,
1275
- })
1912
+ });
1276
1913
  } else if (velocity < 1.0) {
1277
1914
  diagnosis.push({
1278
- metric: 'velocity',
1279
- severity: 'warning',
1915
+ metric: "velocity",
1916
+ severity: "warning",
1280
1917
  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.',
1918
+ recommendation:
1919
+ "Generate more output per input token — larger edits, more complete responses.",
1282
1920
  estimated_yield_impact: `+${Math.round((1 - velocity) * 30)}% Υ potential`,
1283
- })
1921
+ });
1284
1922
  }
1285
1923
  }
1286
1924
 
1287
1925
  // SNR check — signal-to-noise
1288
- const snr = result.snr
1926
+ const snr = result.snr;
1289
1927
  if (snr !== null && snr < 0.3) {
1290
1928
  diagnosis.push({
1291
- metric: 'snr',
1292
- severity: 'warning',
1929
+ metric: "snr",
1930
+ severity: "warning",
1293
1931
  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
- })
1932
+ recommendation:
1933
+ "Reduce fresh input (reuse context) or increase output generation.",
1934
+ estimated_yield_impact:
1935
+ "Indirect — improves both velocity and leverage",
1936
+ });
1297
1937
  }
1298
1938
 
1299
1939
  // Cache creation ratio — are you over-committing?
1300
1940
  if (cw > 0 && o > 0) {
1301
- const commitRatio = cw / o
1941
+ const commitRatio = cw / o;
1302
1942
  if (commitRatio > 20) {
1303
1943
  diagnosis.push({
1304
- metric: 'cache_creation',
1305
- severity: 'info',
1944
+ metric: "cache_creation",
1945
+ severity: "info",
1306
1946
  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
- })
1947
+ recommendation:
1948
+ "Ensure you're rereading committed context. If leverage is low, you're writing cache you never read.",
1949
+ estimated_yield_impact: "Cost reduction, not Υ directly",
1950
+ });
1310
1951
  }
1311
1952
  }
1312
1953
 
1313
1954
  // Input bloat — is fresh input too high relative to total?
1314
- const total = i + o + cw + cr
1955
+ const total = i + o + cw + cr;
1315
1956
  if (total > 0) {
1316
- const inputPct = (i / total) * 100
1957
+ const inputPct = (i / total) * 100;
1317
1958
  if (inputPct > 10) {
1318
1959
  diagnosis.push({
1319
- metric: 'input_bloat',
1320
- severity: 'warning',
1960
+ metric: "input_bloat",
1961
+ severity: "warning",
1321
1962
  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.',
1963
+ recommendation:
1964
+ "Reduce fresh input by reusing prior context instead of re-pasting it.",
1323
1965
  estimated_yield_impact: `+${Math.round((inputPct - 1) * 5)}% Υ potential`,
1324
- })
1966
+ });
1325
1967
  }
1326
1968
  }
1327
1969
 
1328
1970
  // 10xDEV check — is the full cascade compounding?
1329
1971
  if (result.dev10x === null && cw === 0) {
1330
1972
  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
- })
1973
+ metric: "10xdev",
1974
+ severity: "critical",
1975
+ finding:
1976
+ "No cache creation — the cascade cannot compound. You're operating in a non-compounding mode (like ChatGPT without prompt caching).",
1977
+ recommendation:
1978
+ "Switch to a platform with prompt caching (Claude Code) or enable caching if available.",
1979
+ estimated_yield_impact: "Enables the full cascade — potentially 10×+ Υ",
1980
+ });
1337
1981
  } else if (result.dev10x !== null && result.dev10x < 1.0) {
1338
1982
  diagnosis.push({
1339
- metric: '10xdev',
1340
- severity: 'info',
1983
+ metric: "10xdev",
1984
+ severity: "info",
1341
1985
  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
- })
1986
+ recommendation:
1987
+ "Improve both leverage AND velocity — 10xDEV = log10(transmission × commitment × reuse).",
1988
+ estimated_yield_impact: "Class tier improvement",
1989
+ });
1345
1990
  }
1346
1991
 
1347
1992
  // 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])
1993
+ const sevOrder = { critical: 0, warning: 1, info: 2 };
1994
+ diagnosis.sort((a, b) => sevOrder[a.severity] - sevOrder[b.severity]);
1350
1995
 
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.`
1996
+ const healthScore = diagnosis.filter(
1997
+ (d) => d.severity === "critical",
1998
+ ).length;
1999
+ const summary =
2000
+ healthScore === 0
2001
+ ? `Cascade is healthy — Υ ${result.yield}, class ${result.class}. ${diagnosis.length} minor optimizations available.`
2002
+ : `Cascade has ${healthScore} critical leak${healthScore > 1 ? "s" : ""} — Υ ${result.yield}, class ${result.class}. Fix the critical items first.`;
1355
2003
 
1356
2004
  return {
1357
2005
  pillars: p,
@@ -1366,273 +2014,348 @@ export async function callTool(name, args, opts = {}) {
1366
2014
  },
1367
2015
  diagnosis,
1368
2016
  summary,
1369
- }
2017
+ };
1370
2018
  }
1371
2019
 
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.` }
2020
+ if (name === "suggest_improvements") {
2021
+ if (!args?.text)
2022
+ throw new Error(
2023
+ "suggest_improvements requires a non-empty `text` argument (token pillars).",
2024
+ );
2025
+ if (typeof args.text === "string" && args.text.length > MAX_INPUT) {
2026
+ return {
2027
+ status: "error",
2028
+ reason: "input_too_large",
2029
+ detail: `text exceeds ${MAX_INPUT} chars.`,
2030
+ };
1376
2031
  }
1377
2032
 
1378
- const p = parsePillars(args.text)
1379
- const current = withParseWarnings(p, cascade(p))
1380
- const { input: i, output: o, cacheCreate: cw, cacheRead: cr } = p
2033
+ const p = parsePillars(args.text);
2034
+ const current = withParseWarnings(p, cascade(p));
2035
+ const { input: i, output: o, cacheCreate: cw, cacheRead: cr } = p;
1381
2036
 
1382
2037
  // Generate candidate improvements, simulate each, rank by Υ impact
1383
- const candidates = []
2038
+ const candidates = [];
1384
2039
 
1385
2040
  // Strategy 1: Increase cache reads (the #1 lever)
1386
- const crBoosts = cr > 0 ? [1.5, 2, 3, 5] : []
2041
+ const crBoosts = cr > 0 ? [1.5, 2, 3, 5] : [];
1387
2042
  for (const mult of crBoosts) {
1388
- const sim = cascade({ ...p, cacheRead: Math.round(cr * mult) })
2043
+ const sim = cascade({ ...p, cacheRead: Math.round(cr * mult) });
1389
2044
  if (sim.yield !== null) {
1390
2045
  candidates.push({
1391
2046
  action: `Increase cache reads by ${Math.round((mult - 1) * 100)}%`,
1392
- pillar: 'cacheRead',
2047
+ pillar: "cacheRead",
1393
2048
  delta: `+${Math.round(cr * (mult - 1)).toLocaleString()}`,
1394
2049
  simulated_yield: sim.yield,
1395
2050
  yield_delta: Number((sim.yield - (current.yield ?? 0)).toFixed(2)),
1396
2051
  class_after: sim.class,
1397
- rationale: 'Cache reads are the strongest Υ multiplier. More reuse = higher leverage = higher yield.',
1398
- })
2052
+ rationale:
2053
+ "Cache reads are the strongest Υ multiplier. More reuse = higher leverage = higher yield.",
2054
+ });
1399
2055
  }
1400
2056
  }
1401
2057
  // For zero-cache operators, suggest enabling caching with a starter amount
1402
2058
  if (cr === 0 && cw === 0) {
1403
- const starterAmounts = [Math.round(i * 10), Math.round(i * 50), Math.round(i * 100)]
2059
+ const starterAmounts = [
2060
+ Math.round(i * 10),
2061
+ Math.round(i * 50),
2062
+ Math.round(i * 100),
2063
+ ];
1404
2064
  for (const amt of starterAmounts) {
1405
- const sim = cascade({ ...p, cacheCreate: Math.round(amt * 0.5), cacheRead: amt })
2065
+ const sim = cascade({
2066
+ ...p,
2067
+ cacheCreate: Math.round(amt * 0.5),
2068
+ cacheRead: amt,
2069
+ });
1406
2070
  if (sim.yield !== null && sim.yield > 0) {
1407
2071
  candidates.push({
1408
2072
  action: `Enable caching with ${amt.toLocaleString()} cache reads`,
1409
- pillar: 'cacheRead',
2073
+ pillar: "cacheRead",
1410
2074
  delta: `+${amt.toLocaleString()}`,
1411
2075
  simulated_yield: sim.yield,
1412
2076
  yield_delta: Number((sim.yield - (current.yield ?? 0)).toFixed(2)),
1413
2077
  class_after: sim.class,
1414
- rationale: 'You have no cache — enabling it unlocks the cascade. Start by reusing prior context.',
1415
- })
2078
+ rationale:
2079
+ "You have no cache — enabling it unlocks the cascade. Start by reusing prior context.",
2080
+ });
1416
2081
  }
1417
2082
  }
1418
2083
  }
1419
2084
 
1420
2085
  // Strategy 2: Reduce fresh input (Υ = Cr·O/I² — input is squared in the denominator)
1421
- const inputReductions = [0.9, 0.75, 0.5]
2086
+ const inputReductions = [0.9, 0.75, 0.5];
1422
2087
  for (const mult of inputReductions) {
1423
- const newInput = Math.round(i * mult)
1424
- if (newInput < 1) continue
1425
- const sim = cascade({ ...p, input: newInput })
2088
+ const newInput = Math.round(i * mult);
2089
+ if (newInput < 1) continue;
2090
+ const sim = cascade({ ...p, input: newInput });
1426
2091
  if (sim.yield !== null) {
1427
2092
  candidates.push({
1428
2093
  action: `Reduce fresh input by ${Math.round((1 - mult) * 100)}%`,
1429
- pillar: 'input',
2094
+ pillar: "input",
1430
2095
  delta: `-${Math.round(i * (1 - mult)).toLocaleString()}`,
1431
2096
  simulated_yield: sim.yield,
1432
2097
  yield_delta: Number((sim.yield - (current.yield ?? 0)).toFixed(2)),
1433
2098
  class_after: sim.class,
1434
- rationale: 'Input is squared in the Υ denominator (Υ = Cr·O/I²). Reducing input has a quadratic payoff.',
1435
- })
2099
+ rationale:
2100
+ "Input is squared in the Υ denominator (Υ = Cr·O/I²). Reducing input has a quadratic payoff.",
2101
+ });
1436
2102
  }
1437
2103
  }
1438
2104
 
1439
2105
  // Strategy 3: Increase output
1440
- const outputBoosts = [1.25, 1.5, 2]
2106
+ const outputBoosts = [1.25, 1.5, 2];
1441
2107
  for (const mult of outputBoosts) {
1442
- const sim = cascade({ ...p, output: Math.round(o * mult) })
2108
+ const sim = cascade({ ...p, output: Math.round(o * mult) });
1443
2109
  if (sim.yield !== null) {
1444
2110
  candidates.push({
1445
2111
  action: `Increase output by ${Math.round((mult - 1) * 100)}%`,
1446
- pillar: 'output',
2112
+ pillar: "output",
1447
2113
  delta: `+${Math.round(o * (mult - 1)).toLocaleString()}`,
1448
2114
  simulated_yield: sim.yield,
1449
2115
  yield_delta: Number((sim.yield - (current.yield ?? 0)).toFixed(2)),
1450
2116
  class_after: sim.class,
1451
- rationale: 'Output is a linear multiplier in Υ. More output per session = higher yield.',
1452
- })
2117
+ rationale:
2118
+ "Output is a linear multiplier in Υ. More output per session = higher yield.",
2119
+ });
1453
2120
  }
1454
2121
  }
1455
2122
 
1456
2123
  // Strategy 4: Optimize cache creation (reduce if over-committing)
1457
2124
  if (cw > o * 10) {
1458
- const sim = cascade({ ...p, cacheCreate: Math.round(o * 5) })
2125
+ const sim = cascade({ ...p, cacheCreate: Math.round(o * 5) });
1459
2126
  if (sim.yield !== null) {
1460
2127
  candidates.push({
1461
- action: 'Reduce cache creation to 5× output',
1462
- pillar: 'cacheCreate',
2128
+ action: "Reduce cache creation to 5× output",
2129
+ pillar: "cacheCreate",
1463
2130
  delta: `-${Math.round(cw - o * 5).toLocaleString()}`,
1464
2131
  simulated_yield: sim.yield,
1465
2132
  yield_delta: Number((sim.yield - (current.yield ?? 0)).toFixed(2)),
1466
2133
  class_after: sim.class,
1467
- rationale: 'You\'re over-committing cache (cw >> output). Trimming to a healthy ratio reduces cost without hurting yield.',
1468
- })
2134
+ rationale:
2135
+ "You're over-committing cache (cw >> output). Trimming to a healthy ratio reduces cost without hurting yield.",
2136
+ });
1469
2137
  }
1470
2138
  }
1471
2139
 
1472
2140
  // 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 }))
2141
+ candidates.sort((a, b) => b.yield_delta - a.yield_delta);
2142
+ const top = candidates
2143
+ .slice(0, 8)
2144
+ .map((c, idx) => ({ rank: idx + 1, ...c }));
1475
2145
 
1476
- const best = top[0]
2146
+ const best = top[0];
1477
2147
  return {
1478
2148
  suggestions: top,
1479
2149
  current_yield: current.yield,
1480
2150
  current_class: current.class,
1481
2151
  best_single_change: best
1482
2152
  ? `${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
- }
2153
+ : "No improvements found — your cascade is already optimized.",
2154
+ };
1485
2155
  }
1486
2156
 
1487
- if (name === 'self_improve') {
2157
+ if (name === "self_improve") {
1488
2158
  // The full self-improvement cycle: pull → diagnose → suggest → simulate.
1489
2159
  // If text is provided, use it as pillars. If not, run tokenpull first.
1490
- let pillars
1491
- let pulledFrom = 'provided'
2160
+ let pillars;
2161
+ let pulledFrom = "provided";
1492
2162
 
1493
2163
  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.` }
2164
+ if (typeof args.text === "string" && args.text.length > MAX_INPUT) {
2165
+ return {
2166
+ status: "error",
2167
+ reason: "input_too_large",
2168
+ detail: `text exceeds ${MAX_INPUT} chars.`,
2169
+ };
1496
2170
  }
1497
- pillars = parsePillars(args.text)
2171
+ pillars = parsePillars(args.text);
1498
2172
  } else {
1499
2173
  // Run tokenpull to get current pillars from local logs
1500
- const windowType = args?.window || '30d'
2174
+ const windowType = args?.window || "30d";
1501
2175
  try {
1502
- const pullResult = await callTool('tokenpull', { window: windowType }, opts)
1503
- if (pullResult?.status === 'error' || !pullResult?.windows?.length) {
2176
+ const pullResult = await callTool(
2177
+ "tokenpull",
2178
+ { window: windowType },
2179
+ opts,
2180
+ );
2181
+ if (pullResult?.status === "error" || !pullResult?.windows?.length) {
1504
2182
  return {
1505
- status: 'error',
1506
- reason: 'tokenpull_failed',
1507
- detail: 'Could not pull pillars from local logs. Provide pillars via `text` argument instead.',
2183
+ status: "error",
2184
+ reason: "tokenpull_failed",
2185
+ detail:
2186
+ "Could not pull pillars from local logs. Provide pillars via `text` argument instead.",
1508
2187
  pull_result: pullResult,
1509
- }
2188
+ };
1510
2189
  }
1511
2190
  // Use the first window's pillars
1512
- const w = pullResult.windows[0]
1513
- pillars = w.pillars
1514
- pulledFrom = `tokenpull ${w.window || windowType}`
2191
+ const w = pullResult.windows[0];
2192
+ pillars = w.pillars;
2193
+ pulledFrom = `tokenpull ${w.window || windowType}`;
1515
2194
  } catch (e) {
1516
2195
  return {
1517
- status: 'error',
1518
- reason: 'tokenpull_error',
2196
+ status: "error",
2197
+ reason: "tokenpull_error",
1519
2198
  detail: String(e.message || e),
1520
- hint: 'Provide pillars via `text` argument instead.',
1521
- }
2199
+ hint: "Provide pillars via `text` argument instead.",
2200
+ };
1522
2201
  }
1523
2202
  }
1524
2203
 
1525
- const currentResult = withParseWarnings(pillars, cascade(pillars))
1526
- const { input: i, output: o, cacheCreate: cw, cacheRead: cr } = pillars
2204
+ const currentResult = withParseWarnings(pillars, cascade(pillars));
2205
+ const { input: i, output: o, cacheCreate: cw, cacheRead: cr } = pillars;
1527
2206
 
1528
2207
  // ── Step 1: Diagnose ──────────────────────────────────────────────
1529
- const diagnosis = []
1530
- const leverage = currentResult.leverage
2208
+ const diagnosis = [];
2209
+ const leverage = currentResult.leverage;
1531
2210
  if (leverage !== null && leverage < 10) {
1532
2211
  diagnosis.push({
1533
- metric: 'cache_leverage', severity: 'critical',
2212
+ metric: "cache_leverage",
2213
+ severity: "critical",
1534
2214
  finding: `Cache leverage is ${leverage}× — TRANSMITTER-class operators hit 200×+.`,
1535
- recommendation: 'Increase context reuse: load prior session context, use longer threads.',
1536
- })
2215
+ recommendation:
2216
+ "Increase context reuse: load prior session context, use longer threads.",
2217
+ });
1537
2218
  } else if (leverage !== null && leverage < 50) {
1538
2219
  diagnosis.push({
1539
- metric: 'cache_leverage', severity: 'warning',
2220
+ metric: "cache_leverage",
2221
+ severity: "warning",
1540
2222
  finding: `Cache leverage is ${leverage}× — below ARCH+ threshold (~100×+).`,
1541
- recommendation: 'Push cache reads higher by reusing prior context.',
1542
- })
2223
+ recommendation: "Push cache reads higher by reusing prior context.",
2224
+ });
1543
2225
  }
1544
2226
 
1545
- const velocity = currentResult.velocity
2227
+ const velocity = currentResult.velocity;
1546
2228
  if (velocity !== null && velocity < 0.5) {
1547
2229
  diagnosis.push({
1548
- metric: 'velocity', severity: 'critical',
2230
+ metric: "velocity",
2231
+ severity: "critical",
1549
2232
  finding: `Velocity is ${velocity} — generating only ${velocity}× input as output.`,
1550
- recommendation: 'Increase output per turn, reduce over-reading.',
1551
- })
2233
+ recommendation: "Increase output per turn, reduce over-reading.",
2234
+ });
1552
2235
  } else if (velocity !== null && velocity < 1.0) {
1553
2236
  diagnosis.push({
1554
- metric: 'velocity', severity: 'warning',
2237
+ metric: "velocity",
2238
+ severity: "warning",
1555
2239
  finding: `Velocity is ${velocity} — below 1.0. Healthy operators hit 1.5×+.`,
1556
- recommendation: 'Generate more output per input token.',
1557
- })
2240
+ recommendation: "Generate more output per input token.",
2241
+ });
1558
2242
  }
1559
2243
 
1560
2244
  if (currentResult.dev10x === null && cw === 0) {
1561
2245
  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
- })
2246
+ metric: "10xdev",
2247
+ severity: "critical",
2248
+ finding: "No cache creation — cascade cannot compound.",
2249
+ recommendation:
2250
+ "Switch to a platform with prompt caching (Claude Code).",
2251
+ });
1566
2252
  }
1567
2253
 
1568
- const total = i + o + cw + cr
2254
+ const total = i + o + cw + cr;
1569
2255
  if (total > 0 && (i / total) * 100 > 10) {
1570
2256
  diagnosis.push({
1571
- metric: 'input_bloat', severity: 'warning',
2257
+ metric: "input_bloat",
2258
+ severity: "warning",
1572
2259
  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
- })
2260
+ recommendation: "Reduce fresh input by reusing prior context.",
2261
+ });
1575
2262
  }
1576
2263
 
1577
2264
  // ── Step 2: Suggest (generate + simulate candidates) ──────────────
1578
- const candidates = []
2265
+ const candidates = [];
1579
2266
  // If cacheRead is 0, suggest absolute amounts instead of percentage boosts
1580
- const crBoosts = cr > 0 ? [1.5, 2, 3, 5] : []
2267
+ const crBoosts = cr > 0 ? [1.5, 2, 3, 5] : [];
1581
2268
  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
- })
2269
+ const sim = cascade({ ...pillars, cacheRead: Math.round(cr * mult) });
2270
+ if (sim.yield !== null)
2271
+ candidates.push({
2272
+ action: `Increase cache reads by ${Math.round((mult - 1) * 100)}%`,
2273
+ pillar: "cacheRead",
2274
+ delta: `+${Math.round(cr * (mult - 1)).toLocaleString()}`,
2275
+ simulated_yield: sim.yield,
2276
+ yield_delta: Number(
2277
+ (sim.yield - (currentResult.yield ?? 0)).toFixed(2),
2278
+ ),
2279
+ class_after: sim.class,
2280
+ rationale: "Cache reads are the strongest Υ multiplier.",
2281
+ });
1589
2282
  }
1590
2283
  // For zero-cache operators, suggest enabling caching with a starter amount
1591
2284
  if (cr === 0 && cw === 0) {
1592
- const starterAmounts = [Math.round(i * 10), Math.round(i * 50), Math.round(i * 100)]
2285
+ const starterAmounts = [
2286
+ Math.round(i * 10),
2287
+ Math.round(i * 50),
2288
+ Math.round(i * 100),
2289
+ ];
1593
2290
  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
- })
2291
+ const sim = cascade({
2292
+ ...pillars,
2293
+ cacheCreate: Math.round(amt * 0.5),
2294
+ cacheRead: amt,
2295
+ });
2296
+ if (sim.yield !== null && sim.yield > 0)
2297
+ candidates.push({
2298
+ action: `Enable caching with ${amt.toLocaleString()} cache reads`,
2299
+ pillar: "cacheRead",
2300
+ delta: `+${amt.toLocaleString()}`,
2301
+ simulated_yield: sim.yield,
2302
+ yield_delta: Number(
2303
+ (sim.yield - (currentResult.yield ?? 0)).toFixed(2),
2304
+ ),
2305
+ class_after: sim.class,
2306
+ rationale:
2307
+ "You have no cache — enabling it unlocks the cascade. Start by reusing prior context.",
2308
+ });
1601
2309
  }
1602
2310
  }
1603
2311
  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
- })
2312
+ const newInput = Math.round(i * mult);
2313
+ if (newInput < 1) continue;
2314
+ const sim = cascade({ ...pillars, input: newInput });
2315
+ if (sim.yield !== null)
2316
+ candidates.push({
2317
+ action: `Reduce fresh input by ${Math.round((1 - mult) * 100)}%`,
2318
+ pillar: "input",
2319
+ delta: `-${Math.round(i * (1 - mult)).toLocaleString()}`,
2320
+ simulated_yield: sim.yield,
2321
+ yield_delta: Number(
2322
+ (sim.yield - (currentResult.yield ?? 0)).toFixed(2),
2323
+ ),
2324
+ class_after: sim.class,
2325
+ rationale: "Input is squared in Υ denominator — quadratic payoff.",
2326
+ });
1613
2327
  }
1614
2328
  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
- })
2329
+ const sim = cascade({ ...pillars, output: Math.round(o * mult) });
2330
+ if (sim.yield !== null)
2331
+ candidates.push({
2332
+ action: `Increase output by ${Math.round((mult - 1) * 100)}%`,
2333
+ pillar: "output",
2334
+ delta: `+${Math.round(o * (mult - 1)).toLocaleString()}`,
2335
+ simulated_yield: sim.yield,
2336
+ yield_delta: Number(
2337
+ (sim.yield - (currentResult.yield ?? 0)).toFixed(2),
2338
+ ),
2339
+ class_after: sim.class,
2340
+ rationale: "Output is a linear multiplier in Υ.",
2341
+ });
1622
2342
  }
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 }))
2343
+ candidates.sort((a, b) => b.yield_delta - a.yield_delta);
2344
+ const suggestions = candidates
2345
+ .slice(0, 8)
2346
+ .map((c, idx) => ({ rank: idx + 1, ...c }));
1625
2347
 
1626
2348
  // ── Step 3: Simulate the top suggestion ───────────────────────────
1627
- const best = suggestions[0]
1628
- let bestSimulation = null
2349
+ const best = suggestions[0];
2350
+ let bestSimulation = null;
1629
2351
  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)
2352
+ const simPillars = { ...pillars };
2353
+ const delta = parseInt(best.delta.replace(/[+\-,]/g, ""), 10);
2354
+ if (best.pillar === "cacheRead") simPillars.cacheRead = cr + delta;
2355
+ else if (best.pillar === "input")
2356
+ simPillars.input = Math.max(i - delta, 1);
2357
+ else if (best.pillar === "output") simPillars.output = o + delta;
2358
+ const simResult = cascade(simPillars);
1636
2359
  bestSimulation = {
1637
2360
  action: best.action,
1638
2361
  current_yield: currentResult.yield,
@@ -1641,58 +2364,67 @@ export async function callTool(name, args, opts = {}) {
1641
2364
  current_class: currentResult.class,
1642
2365
  simulated_class: simResult.class,
1643
2366
  class_changed: currentResult.class !== simResult.class,
1644
- }
2367
+ };
1645
2368
  }
1646
2369
 
1647
2370
  // ── Step 4: Cycle summary ─────────────────────────────────────────
1648
- const criticalCount = diagnosis.filter((d) => d.severity === 'critical').length
2371
+ const criticalCount = diagnosis.filter(
2372
+ (d) => d.severity === "critical",
2373
+ ).length;
1649
2374
  const cycleSummary = best
1650
2375
  ? `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.`
2376
+ : `Pulled from ${pulledFrom}. Υ ${currentResult.yield} (${currentResult.class}). ${diagnosis.length} findings. No improvements suggested — cascade is optimized.`;
1652
2377
 
1653
2378
  // ── Step 5: Scope-specific analysis ───────────────────────────────
1654
- const scope = args?.scope || 'daily'
1655
- const modeInfo = currentResult.mode // { mode, confidence }
1656
- const scopeResult = {}
2379
+ const scope = args?.scope || "daily";
2380
+ const modeInfo = currentResult.mode; // { mode, confidence }
2381
+ const scopeResult = {};
1657
2382
 
1658
- if (scope === 'daily') {
2383
+ if (scope === "daily") {
1659
2384
  // 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
2385
+ const qs = qualityScore(currentResult.yield ?? 0, modeInfo.mode);
2386
+ const expected = MODE_EXPECTED_YIELD[modeInfo.mode] ?? 0;
2387
+ const assessment = _dailyAssessment(
2388
+ modeInfo.mode,
2389
+ currentResult.yield,
2390
+ qs,
2391
+ expected,
2392
+ );
2393
+ const advice = _dailyAdvice(modeInfo.mode, currentResult.yield, qs);
2394
+ scopeResult.mode = modeInfo;
2395
+ scopeResult.quality_score = Math.round(qs * 100) / 100;
2396
+ scopeResult.assessment = assessment;
2397
+ scopeResult.advice = advice;
1668
2398
  }
1669
2399
 
1670
- if (scope === 'weekly' || scope === 'trend') {
2400
+ if (scope === "weekly" || scope === "trend") {
1671
2401
  // 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)
2402
+ const dailyRows = await _pullDailyRows(opts);
2403
+ const weeklySnapshots = _compoundWeekly(dailyRows);
2404
+ const modeDistribution = _modeDistribution(weeklySnapshots);
2405
+ const modeWeightedYield = _modeWeightedYield(weeklySnapshots);
1676
2406
 
1677
2407
  // Compute badges
1678
- const historyForBadges = weeklySnapshots.map(w => ({
2408
+ const historyForBadges = weeklySnapshots.map((w) => ({
1679
2409
  date: w.weekStart,
1680
2410
  mode: w.mode,
1681
2411
  yield: w.yield,
1682
2412
  pillars: w.pillars,
1683
- }))
2413
+ }));
1684
2414
  const badges = computeBadges({
1685
2415
  pillars,
1686
2416
  cascade: currentResult,
1687
2417
  history: historyForBadges,
1688
2418
  isVerified: false, // set by submit_verified
1689
2419
  rank: null, // server-side
1690
- })
2420
+ });
1691
2421
 
1692
- if (scope === 'weekly') {
1693
- scopeResult.mode = modeInfo
2422
+ if (scope === "weekly") {
2423
+ scopeResult.mode = modeInfo;
1694
2424
  // Strip dailyModes from weekly_snapshots — privacy boundary (daily modes never leave the machine)
1695
- const safeSnapshots = weeklySnapshots.map(({ dailyModes, ...rest }) => rest)
2425
+ const safeSnapshots = weeklySnapshots.map(
2426
+ ({ dailyModes, ...rest }) => rest,
2427
+ );
1696
2428
  scopeResult.report = {
1697
2429
  current_mode: modeInfo.mode,
1698
2430
  mode_confidence: modeInfo.confidence,
@@ -1702,15 +2434,15 @@ export async function callTool(name, args, opts = {}) {
1702
2434
  health_score: _healthScore(weeklySnapshots, modeWeightedYield),
1703
2435
  weekly_snapshots: safeSnapshots,
1704
2436
  badges,
1705
- }
2437
+ };
1706
2438
  }
1707
2439
 
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
2440
+ if (scope === "trend") {
2441
+ const yield7d = _yieldForDays(weeklySnapshots, 7);
2442
+ const yield30d = _yieldForDays(weeklySnapshots, 30);
2443
+ const yield90d = _yieldForDays(weeklySnapshots, 90);
2444
+ const trajectory = _trajectory(yield7d, yield30d, yield90d);
2445
+ scopeResult.mode = modeInfo;
1714
2446
  scopeResult.trend = {
1715
2447
  yield_7d: yield7d,
1716
2448
  yield_30d: yield30d,
@@ -1718,7 +2450,7 @@ export async function callTool(name, args, opts = {}) {
1718
2450
  trajectory,
1719
2451
  mode_distribution: modeDistribution,
1720
2452
  phase_pattern: _phasePattern(weeklySnapshots),
1721
- }
2453
+ };
1722
2454
  }
1723
2455
  }
1724
2456
 
@@ -1739,10 +2471,10 @@ export async function callTool(name, args, opts = {}) {
1739
2471
  best_simulation: bestSimulation,
1740
2472
  cycle_summary: cycleSummary,
1741
2473
  ...scopeResult,
1742
- }
2474
+ };
1743
2475
  }
1744
2476
 
1745
- throw new Error(`Unknown tool: ${name}`)
2477
+ throw new Error(`Unknown tool: ${name}`);
1746
2478
  }
1747
2479
 
1748
2480
  // ── self_improve scope helpers ──────────────────────────────────────────────
@@ -1750,196 +2482,223 @@ export async function callTool(name, args, opts = {}) {
1750
2482
  /** Pull daily rows from ccusage to build mode history. Returns array of { date, pillars, yield, mode }. */
1751
2483
  async function _pullDailyRows(opts = {}) {
1752
2484
  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)
2485
+ const raw = await execFileAsync(
2486
+ "ccusage",
2487
+ ["claude", "daily", "--json"],
2488
+ 15000,
2489
+ );
2490
+ const rows = JSON.parse(raw)?.daily ?? JSON.parse(raw);
2491
+ if (!Array.isArray(rows)) return [];
2492
+ return rows
2493
+ .map((r) => {
2494
+ const pillars = {
2495
+ input: r.inputTokens ?? r.input_tokens ?? 0,
2496
+ output: r.outputTokens ?? r.output_tokens ?? 0,
2497
+ cacheCreate: r.cacheCreationTokens ?? r.cache_create_tokens ?? 0,
2498
+ cacheRead: r.cacheReadTokens ?? r.cache_read_tokens ?? 0,
2499
+ };
2500
+ const cas = cascade(pillars);
2501
+ const mode = detectMode(pillars);
2502
+ return {
2503
+ date: r.date ?? r.day ?? null,
2504
+ pillars,
2505
+ yield: cas.yield ?? 0,
2506
+ mode: mode.mode,
2507
+ mode_confidence: mode.confidence,
2508
+ };
2509
+ })
2510
+ .filter((r) => r.date);
1773
2511
  } catch {
1774
- return []
2512
+ return [];
1775
2513
  }
1776
2514
  }
1777
2515
 
1778
2516
  /** Compound daily rows into weekly snapshots. Each week = 7 days, starting Monday. */
1779
2517
  function _compoundWeekly(dailyRows) {
1780
- if (!dailyRows || dailyRows.length === 0) return []
2518
+ if (!dailyRows || dailyRows.length === 0) return [];
1781
2519
  // Group by ISO week (week starts Monday)
1782
- const weeks = new Map()
2520
+ const weeks = new Map();
1783
2521
  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)
2522
+ const d = new Date(row.date);
2523
+ const day = d.getDay() || 7; // Sunday=0 → 7
2524
+ const monday = new Date(d);
2525
+ monday.setDate(d.getDate() - day + 1);
2526
+ const weekKey = monday.toISOString().slice(0, 10);
1789
2527
  if (!weeks.has(weekKey)) {
1790
- weeks.set(weekKey, { weekStart: weekKey, days: [] })
2528
+ weeks.set(weekKey, { weekStart: weekKey, days: [] });
1791
2529
  }
1792
- weeks.get(weekKey).days.push(row)
2530
+ weeks.get(weekKey).days.push(row);
1793
2531
  }
1794
2532
  // 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))
2533
+ return Array.from(weeks.values())
2534
+ .map((w) => {
2535
+ const pillars = w.days.reduce(
2536
+ (acc, d) => ({
2537
+ input: acc.input + d.pillars.input,
2538
+ output: acc.output + d.pillars.output,
2539
+ cacheCreate: acc.cacheCreate + d.pillars.cacheCreate,
2540
+ cacheRead: acc.cacheRead + d.pillars.cacheRead,
2541
+ }),
2542
+ { input: 0, output: 0, cacheCreate: 0, cacheRead: 0 },
2543
+ );
2544
+ const cas = cascade(pillars);
2545
+ const mode = detectMode(pillars);
2546
+ return {
2547
+ weekStart: w.weekStart,
2548
+ pillars,
2549
+ yield: cas.yield ?? 0,
2550
+ mode: mode.mode,
2551
+ mode_confidence: mode.confidence,
2552
+ dayCount: w.days.length,
2553
+ dailyModes: w.days.map((d) => d.mode), // stays local — not in submitted report
2554
+ };
2555
+ })
2556
+ .sort((a, b) => a.weekStart.localeCompare(b.weekStart));
1814
2557
  }
1815
2558
 
1816
2559
  /** Compute mode distribution from weekly snapshots (weekly granularity = privacy boundary). */
1817
2560
  function _modeDistribution(weeklySnapshots) {
1818
- if (!weeklySnapshots || weeklySnapshots.length === 0) return {}
1819
- const counts = { BUILD: 0, EDIT: 0, DEBUG: 0, MAINTAIN: 0, IDLE: 0 }
2561
+ if (!weeklySnapshots || weeklySnapshots.length === 0) return {};
2562
+ const counts = { BUILD: 0, EDIT: 0, DEBUG: 0, MAINTAIN: 0, IDLE: 0 };
1820
2563
  for (const w of weeklySnapshots) {
1821
- counts[w.mode] = (counts[w.mode] || 0) + 1
2564
+ counts[w.mode] = (counts[w.mode] || 0) + 1;
1822
2565
  }
1823
- const total = weeklySnapshots.length
1824
- const dist = {}
2566
+ const total = weeklySnapshots.length;
2567
+ const dist = {};
1825
2568
  for (const [mode, count] of Object.entries(counts)) {
1826
- if (count > 0) dist[mode] = Math.round((count / total) * 100) / 100
2569
+ if (count > 0) dist[mode] = Math.round((count / total) * 100) / 100;
1827
2570
  }
1828
- return dist
2571
+ return dist;
1829
2572
  }
1830
2573
 
1831
2574
  /** Mode-weighted yield — average yield weighted by mode distribution. */
1832
2575
  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)
2576
+ if (!weeklySnapshots || weeklySnapshots.length === 0) return 0;
2577
+ const total = weeklySnapshots.length;
2578
+ const sum = weeklySnapshots.reduce((acc, w) => acc + (w.yield || 0), 0);
2579
+ return Math.round(sum / total);
1837
2580
  }
1838
2581
 
1839
2582
  /** Peak yield across all weekly snapshots. */
1840
2583
  function _peakYield(weeklySnapshots) {
1841
- if (!weeklySnapshots || weeklySnapshots.length === 0) return 0
1842
- return Math.max(...weeklySnapshots.map(w => w.yield || 0))
2584
+ if (!weeklySnapshots || weeklySnapshots.length === 0) return 0;
2585
+ return Math.max(...weeklySnapshots.map((w) => w.yield || 0));
1843
2586
  }
1844
2587
 
1845
2588
  /** Health score (0-100) — weighted composite of consistency, momentum, quality. */
1846
2589
  function _healthScore(weeklySnapshots, modeWeightedYield) {
1847
- if (!weeklySnapshots || weeklySnapshots.length === 0) return 0
2590
+ if (!weeklySnapshots || weeklySnapshots.length === 0) return 0;
1848
2591
  // Simplified Phase 1 formula:
1849
2592
  // 40% — consistency (how often in MAINTAIN)
1850
2593
  // 30% — momentum (recent yield vs older yield)
1851
2594
  // 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
2595
+ const dist = _modeDistribution(weeklySnapshots);
2596
+ const maintainShare = dist.MAINTAIN || 0;
2597
+ const consistency = Math.min(maintainShare, 1) * 40;
1855
2598
 
1856
2599
  // 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
2600
+ const recent = weeklySnapshots.slice(-3);
2601
+ const older = weeklySnapshots.slice(0, 3);
2602
+ const recentAvg =
2603
+ recent.reduce((a, w) => a + (w.yield || 0), 0) / (recent.length || 1);
2604
+ const olderAvg =
2605
+ older.reduce((a, w) => a + (w.yield || 0), 0) / (older.length || 1);
2606
+ const momentumRatio = olderAvg > 0 ? recentAvg / olderAvg : 1;
2607
+ const momentum = (Math.min(momentumRatio, 2) / 2) * 30;
1863
2608
 
1864
2609
  // Quality: mode-weighted yield vs MAINTAIN expected (5000)
1865
- const quality = Math.min(modeWeightedYield / 5000, 1) * 30
2610
+ const quality = Math.min(modeWeightedYield / 5000, 1) * 30;
1866
2611
 
1867
- return Math.round(consistency + momentum + quality)
2612
+ return Math.round(consistency + momentum + quality);
1868
2613
  }
1869
2614
 
1870
2615
  /** Yield for the last N days (from weekly snapshots — approximates by summing recent weeks). */
1871
2616
  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
2617
+ if (!weeklySnapshots || weeklySnapshots.length === 0) return 0;
2618
+ const weeksNeeded = Math.ceil(days / 7);
2619
+ const recent = weeklySnapshots.slice(-weeksNeeded);
2620
+ if (recent.length === 0) return 0;
2621
+ const sum = recent.reduce((a, w) => a + (w.yield || 0), 0);
2622
+ return Math.round(sum / recent.length); // average weekly yield
1878
2623
  }
1879
2624
 
1880
2625
  /** Trajectory description from 7d/30d/90d yields. */
1881
2626
  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'
2627
+ if (y90 === 0) return "insufficient_data";
2628
+ const r7v30 = y30 > 0 ? y7 / y30 : 1;
2629
+ const r30v90 = y90 > 0 ? y30 / y90 : 1;
2630
+ if (r7v30 > 1.2 && r30v90 > 1.0) return "accelerating";
2631
+ if (r7v30 > 1.2) return "recent_surge";
2632
+ if (r7v30 < 0.8 && r30v90 < 0.8) return "declining";
2633
+ if (r7v30 < 0.8) return "recent_dip";
2634
+ if (r30v90 > 1.1) return "steady_growth";
2635
+ return "stable";
1891
2636
  }
1892
2637
 
1893
2638
  /** Phase pattern description from mode distribution over time. */
1894
2639
  function _phasePattern(weeklySnapshots) {
1895
- if (!weeklySnapshots || weeklySnapshots.length < 2) return 'insufficient_data'
1896
- const modes = weeklySnapshots.map(w => w.mode)
1897
- const transitions = []
2640
+ if (!weeklySnapshots || weeklySnapshots.length < 2)
2641
+ return "insufficient_data";
2642
+ const modes = weeklySnapshots.map((w) => w.mode);
2643
+ const transitions = [];
1898
2644
  for (let i = 1; i < modes.length; i++) {
1899
2645
  if (modes[i] !== modes[i - 1]) {
1900
- transitions.push(`${modes[i - 1]}→${modes[i]}`)
2646
+ transitions.push(`${modes[i - 1]}→${modes[i]}`);
1901
2647
  }
1902
2648
  }
1903
- if (transitions.length === 0) return `consistent_${modes[0].toLowerCase()}`
2649
+ if (transitions.length === 0) return `consistent_${modes[0].toLowerCase()}`;
1904
2650
  // 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'
2651
+ const smooth = transitions.every((t) => !t.includes("DEBUG"));
2652
+ if (smooth && transitions.some((t) => t === "BUILD→MAINTAIN"))
2653
+ return "smooth_ramp";
2654
+ if (transitions.filter((t) => t.includes("DEBUG")).length > 2)
2655
+ return "erratic";
2656
+ return "cyclical";
1909
2657
  }
1910
2658
 
1911
2659
  /** Daily assessment string. */
1912
2660
  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.`
2661
+ const qsPct = Math.round(qualityScoreVal * 100);
2662
+ if (mode === "IDLE")
2663
+ return "You're idle — no significant token activity today.";
2664
+ if (mode === "MAINTAIN") {
2665
+ if (qualityScoreVal >= 0.5)
2666
+ return `You're in MAINTAIN mode. Yield ${yieldVal} is ${qsPct}% of MAINTAIN norm. The cascade is compounding.`;
2667
+ return `You're in MAINTAIN mode but yield ${yieldVal} is only ${qsPct}% of expected (${expectedYield}). You may be coasting — push for more output.`;
1918
2668
  }
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.`
2669
+ if (mode === "BUILD") {
2670
+ if (qualityScoreVal >= 0.5)
2671
+ return `You're in BUILD mode. Yield ${yieldVal} is ${qsPct}% of BUILD norm. Greenfield work — expected to be low.`;
2672
+ return `You're in BUILD mode. Yield ${yieldVal} is ${qsPct}% of expected (${expectedYield}). Building is slow — keep going.`;
1922
2673
  }
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.`
2674
+ if (mode === "EDIT") {
2675
+ if (qualityScoreVal >= 0.5)
2676
+ return `You're in EDIT mode. Yield ${yieldVal} is ${qsPct}% of EDIT norm. Fresh input but producing — good.`;
2677
+ return `You're in EDIT mode. Yield ${yieldVal} is ${qsPct}% of expected (${expectedYield}). Push for more output per input.`;
1926
2678
  }
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.`
2679
+ if (mode === "DEBUG") {
2680
+ if (qualityScoreVal >= 0.5)
2681
+ return `You're in DEBUG mode. Yield ${yieldVal} is ${qsPct}% of DEBUG norm. Investigating — yield is expected to be low.`;
2682
+ return `You're in DEBUG mode. Yield ${yieldVal} is ${qsPct}% of expected (${expectedYield}). Debugging is slow — consider loading prior context.`;
1930
2683
  }
1931
- return `Mode: ${mode}. Yield: ${yieldVal}. Quality: ${qsPct}%.`
2684
+ return `Mode: ${mode}. Yield: ${yieldVal}. Quality: ${qsPct}%.`;
1932
2685
  }
1933
2686
 
1934
2687
  /** Daily advice string. */
1935
2688
  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.'
2689
+ if (mode === "MAINTAIN" && qs >= 0.5)
2690
+ return "Keep the cascade going. Don't reset context — let it compound.";
2691
+ if (mode === "MAINTAIN")
2692
+ return "You're in MAINTAIN but underperforming. Push the agent for more output per turn.";
2693
+ if (mode === "BUILD")
2694
+ return "When you're done building, load prior context to transition to MAINTAIN. The cascade rewards reuse.";
2695
+ if (mode === "EDIT")
2696
+ return "You're producing but using fresh input. Try to reuse prior context to boost leverage.";
2697
+ if (mode === "DEBUG")
2698
+ return "When you're done debugging, load prior context to return to MAINTAIN. Don't let the debug phase drag on.";
2699
+ if (mode === "IDLE")
2700
+ return "No activity detected. Start a session to build your cascade.";
2701
+ return "Keep working — the cascade will compound as you build context.";
1943
2702
  }
1944
2703
 
1945
2704
  /**
@@ -1949,22 +2708,25 @@ function _dailyAdvice(mode, yieldVal, qs) {
1949
2708
  * Weekly granularity is the privacy boundary — no daily modes in the report.
1950
2709
  */
1951
2710
  function _computeReportBlock(pillars) {
1952
- const cas = cascade(pillars)
1953
- const modeInfo = detectMode(pillars)
2711
+ const cas = cascade(pillars);
2712
+ const modeInfo = detectMode(pillars);
1954
2713
  const badges = computeBadges({
1955
2714
  pillars,
1956
2715
  cascade: cas,
1957
2716
  history: [], // no history available at submit time — badges computed from current pillars only
1958
2717
  isVerified: true, // submit_verified is the signed agent path
1959
2718
  rank: null,
1960
- })
2719
+ });
1961
2720
  return {
1962
2721
  current_mode: modeInfo.mode,
1963
2722
  mode_confidence: modeInfo.confidence,
1964
2723
  mode_distribution: { [modeInfo.mode]: 1.0 }, // single-window submission
1965
2724
  mode_weighted_yield: cas.yield ?? 0,
1966
2725
  peak_yield: cas.yield ?? 0,
1967
- health_score: _healthScore([{ mode: modeInfo.mode, yield: cas.yield ?? 0 }], cas.yield ?? 0),
2726
+ health_score: _healthScore(
2727
+ [{ mode: modeInfo.mode, yield: cas.yield ?? 0 }],
2728
+ cas.yield ?? 0,
2729
+ ),
1968
2730
  badges,
1969
- }
2731
+ };
1970
2732
  }