sigrank 0.18.5 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/CHANGELOG.md +158 -0
  2. package/MCP_SERVER_DESCRIPTION.md +172 -0
  3. package/README.md +94 -41
  4. package/adapters/index.mjs +1100 -0
  5. package/adapters/tokenpull.mjs +568 -0
  6. package/adapters.mjs +1 -857
  7. package/analytics/cascade.mjs +338 -0
  8. package/cascade.mjs +1 -274
  9. package/cli.mjs +1 -1853
  10. package/connect.mjs +1 -13
  11. package/identity/connect.mjs +13 -0
  12. package/identity/keystore.mjs +231 -0
  13. package/identity/sign.mjs +106 -0
  14. package/index.mjs +47 -162
  15. package/keystore.mjs +1 -242
  16. package/lib/constants.mjs +137 -0
  17. package/lib/pkg-version.mjs +59 -0
  18. package/manifest.json +46 -0
  19. package/narrate.mjs +1 -77
  20. package/package.json +41 -16
  21. package/preflight.mjs +1 -191
  22. package/presentation/cli.mjs +1926 -0
  23. package/presentation/narrate.mjs +219 -0
  24. package/presentation/tui-themes.mjs +189 -0
  25. package/presentation/tui.mjs +3316 -0
  26. package/prompts/check-my-efficiency.md +1 -0
  27. package/prompts/compare-with-leader.md +1 -0
  28. package/prompts/simulate-improvement.md +1 -0
  29. package/proxy.mjs +492 -0
  30. package/resources/class-tiers.md +81 -0
  31. package/resources/data-policy.md +31 -0
  32. package/resources/install-guide.md +59 -0
  33. package/resources/privacy-model.md +44 -0
  34. package/resources/scoring-formula.md +24 -0
  35. package/review.mjs +263 -0
  36. package/sign.mjs +1 -106
  37. package/submit/index.mjs +297 -0
  38. package/submit/preflight.mjs +191 -0
  39. package/submit.mjs +1 -303
  40. package/tokenpull.mjs +1 -576
  41. package/tokscale_analytics.mjs +864 -0
  42. package/tools/_framing.mjs +217 -0
  43. package/tools/_helpers.mjs +253 -0
  44. package/tools/_peer-matching.mjs +361 -0
  45. package/tools/_report.mjs +262 -0
  46. package/tools/_schemas.mjs +492 -0
  47. package/tools/_verifiers.mjs +148 -0
  48. package/tools/compare-operators.mjs +80 -0
  49. package/tools/compare-self.mjs +124 -0
  50. package/tools/describe-power-user.mjs +78 -0
  51. package/tools/diagnose-cascade.mjs +192 -0
  52. package/tools/discover-peers.mjs +198 -0
  53. package/tools/enroll.mjs +119 -0
  54. package/tools/get-best-operator.mjs +59 -0
  55. package/tools/get-leaderboard.mjs +27 -0
  56. package/tools/get-operator.mjs +33 -0
  57. package/tools/index.mjs +229 -0
  58. package/tools/optimize-efficiency.mjs +170 -0
  59. package/tools/rank-paste.mjs +51 -0
  60. package/tools/rank-windows.mjs +120 -0
  61. package/tools/self-improve.mjs +423 -0
  62. package/tools/simulate-change.mjs +207 -0
  63. package/tools/submit-paste.mjs +121 -0
  64. package/tools/submit-verified.mjs +153 -0
  65. package/tools/suggest-improvements.mjs +168 -0
  66. package/tools/tokenpull-compare.mjs +123 -0
  67. package/tools/tokenpull-submit.mjs +235 -0
  68. package/tools/tokenpull.mjs +113 -0
  69. package/tools/tokscale-breakdown.mjs +119 -0
  70. package/tools/tokscale-competitive-intel.mjs +45 -0
  71. package/tools/tokscale-cost-analysis.mjs +31 -0
  72. package/tools/tokscale-developer-profile.mjs +36 -0
  73. package/tools/tokscale-device-profile.mjs +35 -0
  74. package/tools/tokscale-market-share.mjs +38 -0
  75. package/tools/tokscale-mcp-usage.mjs +33 -0
  76. package/tools/tokscale-model-trends.mjs +32 -0
  77. package/tools/watch-tokenpull.mjs +134 -0
  78. package/tools.mjs +1 -3897
  79. package/tui.mjs +4 -2650
  80. package/banner.svg +0 -7
package/tools.mjs CHANGED
@@ -1,3897 +1 @@
1
- /**
2
- * tools.mjs — the SigRank MCP tool table + dispatcher, transport-free so it can be
3
- * unit-tested without spawning the stdio server (index.mjs imports from here).
4
- *
5
- * callTool() takes an opts bag with an injectable { apiBase, fetchImpl } so tests can
6
- * exercise the read/write network paths against a fake fetch — no live calls, no
7
- * writes to production. Pure cascade math lives in ./cascade.mjs; the deterministic
8
- * narration card in ./narrate.mjs. Token-only, no transcript content.
9
- */
10
-
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, execFileSync } 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";
34
-
35
- // Resolve local node_modules/.bin for bundled deps (ccusage, tokscale)
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 : ""}`;
39
-
40
- // ASYNC FIX (2026-06-27): execFile wrapped in a Promise — replaces execSync for
41
- // defense-in-depth (shell injection prevention + non-blocking). The platform param
42
- // is enum-validated at the MCP schema level, but execFile also prevents shell
43
- // interpolation attacks by passing args as an array (no shell parsing).
44
- // BIN FIX (2026-06-27): PATH includes local node_modules/.bin so bundled deps
45
- // are found even when sigrank isn't globally installed.
46
- function execFileAsync(cmd, args, timeoutMs) {
47
- return new Promise((resolve, reject) => {
48
- // NOTE: execFile does not accept a `stdio` option (it always pipes + buffers
49
- // stdout/stderr against maxBuffer) — a previous `stdio` key here was silently ignored.
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
- });
64
- }
65
-
66
- // ── Verifier readers (sync, on-device, token-only) ────────────────────────────
67
- // These mirror the implementations in cli.mjs / tui.mjs without the circular import.
68
- //
69
- // NOTE (P3 2026-06-27): Intentionally separate from tokenpull.mjs `freshVerifierPillars()`.
70
- // The MCP `tokenpull_compare` tool uses these file-based/cached readers (tokscale_report.json,
71
- // direct db read) for a quick comparison, while freshVerifierPillars runs all sources live
72
- // (bunx tokscale, scan+read tokendash) for the TUI/CLI dashboard. Different data sources =
73
- // different behavior; do NOT merge without understanding the trade-off.
74
-
75
- async function _ccusagePillars(platform = "claude") {
76
- try {
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;
95
- for (const r of rows) {
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;
101
- }
102
- }
103
- result[win] = { input: i, output: o, cacheCreate: cw, cacheRead: cr };
104
- }
105
- let i = 0,
106
- o = 0,
107
- cw = 0,
108
- cr = 0;
109
- for (const r of rows) {
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;
114
- }
115
- result["all"] = { input: i, output: o, cacheCreate: cw, cacheRead: cr };
116
- return result;
117
- } catch {
118
- return null;
119
- }
120
- }
121
-
122
- async function _tokenDashPillars() {
123
- const dbPath = path.join(os.homedir(), ".claude", "token-dashboard.db");
124
- if (!existsSync(dbPath)) return null;
125
- try {
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
- }
146
- }
147
-
148
- async function _tokscalePillars(platform = "claude") {
149
- // Try the bundled tokscale CLI first (fresh data), fall back to saved report file.
150
- try {
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
- );
166
- if (rows.length) {
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 };
177
- }
178
- } catch {
179
- /* fall through to file-based read */
180
- }
181
- // Fallback: read saved tokscale_report.json if it exists
182
- const p = path.join(os.homedir(), "tokscale_report.json");
183
- if (!existsSync(p)) return null;
184
- try {
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
- }
207
- }
208
-
209
- // Every board upload from the MCP is hashed + timestamped (ddmmyy) — provenance + dedup.
210
- function uploadStamp(content) {
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
- };
225
- }
226
-
227
- // Pull a platform's local usage → 4 windows of canonical pillars. Routes through
228
- // tokenpullAny() which handles Claude (native), Codex (estimated io_ratio), and all
229
- // other adapters from the registry. opts.adapter overrides for tests.
230
- async function pullByPlatform(platform, opts = {}) {
231
- if (opts.adapter) {
232
- // Test injection: bypass registry and use the mock adapter directly
233
- if (platform === "codex") {
234
- let ioRatio = 2.0;
235
- try {
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 });
244
- }
245
- return pullLocal({ adapter: opts.adapter, now: opts.now });
246
- }
247
- return tokenpullAny(platform || "claude", opts);
248
- }
249
-
250
- export const DEFAULT_API_BASE =
251
- process.env.SIGRANK_API_BASE || "https://signalaf.com";
252
- /** Default network timeout in ms (override via opts.fetchTimeout or SIGRANK_FETCH_TIMEOUT). */
253
- export const DEFAULT_FETCH_TIMEOUT =
254
- Number(process.env.SIGRANK_FETCH_TIMEOUT) || 10_000;
255
- /** Max accepted length for a single paste/string arg (chars). Token counts are tiny; anything
256
- * past this is malformed or abusive — reject cleanly before parsing/POSTing (E2 hardening). */
257
- const MAX_INPUT = 1_000_000;
258
-
259
- /** Resolve this package's version for the User-Agent stamp (best-effort). */
260
- function agentVersionStamp() {
261
- try {
262
- const pkg = JSON.parse(
263
- readFileSync(new URL("./package.json", import.meta.url), "utf-8"),
264
- );
265
- return pkg.version;
266
- } catch {
267
- return "unknown";
268
- }
269
- }
270
-
271
- /**
272
- * curl-based fetch fallback for when Node's fetch is blocked by Vercel's bot
273
- * protection (TLS fingerprinting). Returns a Response-like object with .ok,
274
- * .status, .json(), .text(). Uses execFileSync for synchronous curl execution.
275
- */
276
- function curlFetch(url, init = {}, timeoutMs = 10_000) {
277
- const args = ["-s", "-S", "-w", "\n__HTTP_STATUS__%{http_code}", "--max-time", String(Math.ceil(timeoutMs / 1000))];
278
- if (init.method) args.push("-X", init.method);
279
- const headers = init.headers || {};
280
- for (const [k, v] of Object.entries(headers)) {
281
- args.push("-H", `${k}: ${v}`);
282
- }
283
- if (init.body) {
284
- args.push("-d", init.body);
285
- }
286
- args.push(url);
287
- let stdout;
288
- try {
289
- stdout = execFileSync("curl", args, {
290
- encoding: "utf-8",
291
- maxBuffer: 10 * 1024 * 1024,
292
- timeout: timeoutMs + 2000,
293
- });
294
- } catch (e) {
295
- throw new Error(`curl transport failed: ${e.message}`);
296
- }
297
- // Parse the status code from the trailer
298
- const statusMatch = stdout.match(/__HTTP_STATUS__(\d+)$/);
299
- const status = statusMatch ? parseInt(statusMatch[1], 10) : 0;
300
- const bodyText = statusMatch ? stdout.slice(0, statusMatch.index) : stdout;
301
- return {
302
- ok: status >= 200 && status < 300,
303
- status,
304
- json: async () => { try { return JSON.parse(bodyText); } catch { return {}; } },
305
- text: async () => bodyText,
306
- };
307
- }
308
-
309
- // ── Smithery quality: output schemas + annotations ──────────────────────────
310
- // MCP tool annotations hint to clients about side-effects, read-only status, etc.
311
- const ANNOTATIONS = {
312
- readOnlyHint: { readOnlyHint: true },
313
- destructiveHint: { destructiveHint: false },
314
- idempotentHint: { idempotentHint: true },
315
- openWorldHint: { openWorldHint: false },
316
- };
317
-
318
- // Common output schema for cascade results (rank_paste, simulate_change, etc.)
319
- const CASCADE_OUTPUT = {
320
- type: "object",
321
- properties: {
322
- yield_: {
323
- type: "number",
324
- description:
325
- "Υ Yield — the headline efficiency metric (Cache Reads × Output / Input²)",
326
- },
327
- snr: { type: "number", description: "Signal-to-noise ratio" },
328
- leverage: {
329
- type: "number",
330
- description: "Cr/I — cache reads divided by input",
331
- },
332
- velocity: { type: "number", description: "O/I — output divided by input" },
333
- tenx_dev: { type: "number", description: "10xDEV score" },
334
- class: {
335
- type: "string",
336
- enum: ["Burner", "Builder", "10xer"],
337
- description: "Operator class tier",
338
- },
339
- card: {
340
- type: "string",
341
- description: "Deterministic prose summary of the cascade result",
342
- },
343
- warnings: {
344
- type: "array",
345
- items: { type: "string" },
346
- description: "Parse or data warnings if any",
347
- },
348
- },
349
- required: ["yield_", "class"],
350
- };
351
-
352
- const LEADERBOARD_OUTPUT = {
353
- type: "object",
354
- properties: {
355
- operators: {
356
- type: "array",
357
- description: "Array of ranked operators sorted by yield",
358
- items: {
359
- type: "object",
360
- properties: {
361
- codename: { type: "string", description: "Public display name" },
362
- yield_: { type: "number", description: "Υ Yield metric" },
363
- leverage: { type: "number", description: "Cr/I ratio" },
364
- velocity: { type: "number", description: "O/I ratio" },
365
- class: { type: "string", enum: ["Burner", "Builder", "10xer"] },
366
- rank: { type: "integer", description: "1-based rank position" },
367
- },
368
- },
369
- },
370
- },
371
- };
372
-
373
- const OPERATOR_OUTPUT = {
374
- type: "object",
375
- properties: {
376
- codename: { type: "string", description: "Operator display name" },
377
- yield_: { type: "number", description: "Υ Yield metric" },
378
- leverage: { type: "number", description: "Cr/I ratio" },
379
- velocity: { type: "number", description: "O/I ratio" },
380
- class: { type: "string", enum: ["Burner", "Builder", "10xer"] },
381
- rank: { type: "integer", description: "1-based rank position" },
382
- windows: {
383
- type: "array",
384
- description: "Per-window breakdowns (7d, 30d, 90d, all-time)",
385
- items: {
386
- type: "object",
387
- properties: {
388
- window: { type: "string", enum: ["7d", "30d", "90d", "all_time"] },
389
- pillars: {
390
- type: "object",
391
- properties: {
392
- input: { type: "integer" },
393
- output: { type: "integer" },
394
- cacheCreate: { type: "integer" },
395
- cacheRead: { type: "integer" },
396
- },
397
- },
398
- },
399
- },
400
- },
401
- },
402
- };
403
-
404
- const BEST_OPERATOR_OUTPUT = {
405
- type: "object",
406
- properties: {
407
- top_operators: {
408
- type: "array",
409
- description: "Top N operators ranked by yield",
410
- items: {
411
- type: "object",
412
- properties: {
413
- codename: { type: "string", description: "Public display name" },
414
- yield_: { type: "number", description: "Υ Yield metric" },
415
- leverage: { type: "number", description: "Cr/I ratio" },
416
- velocity: { type: "number", description: "O/I ratio" },
417
- class: { type: "string", enum: ["Burner", "Builder", "10xer"] },
418
- rank: { type: "integer", description: "1-based rank position" },
419
- behavioral_framing: {
420
- type: "string",
421
- description: "Plain-language interpretation of the operator's cascade in power-user terms",
422
- },
423
- },
424
- },
425
- },
426
- total_operators: { type: "integer", description: "Total operators on the board" },
427
- summary: {
428
- type: "string",
429
- description: "One-line summary of the top operator's achievement in behavioral terms",
430
- },
431
- },
432
- };
433
-
434
- const COMPARE_SELF_OUTPUT = {
435
- type: "object",
436
- properties: {
437
- your_metrics: {
438
- type: "object",
439
- description: "Your cascade metrics",
440
- properties: {
441
- codename: { type: "string" },
442
- yield_: { type: "number", description: "Υ Yield metric" },
443
- leverage: { type: "number", description: "Cr/I ratio" },
444
- velocity: { type: "number", description: "O/I ratio" },
445
- class: { type: "string", enum: ["Burner", "Builder", "10xer"] },
446
- rank: { type: "integer", description: "1-based rank position" },
447
- },
448
- },
449
- power_user_assessment: {
450
- type: "string",
451
- description: "Behavioral interpretation: are you an AI power user? Maps class tier to power-user language.",
452
- },
453
- comparison: {
454
- type: "object",
455
- description: "How you compare to board averages and archetypes",
456
- properties: {
457
- your_yield_vs_avg: { type: "string", description: "Your yield vs board average" },
458
- your_class_meaning: { type: "string", description: "What your class tier means in power-user terms" },
459
- percentile: { type: "number", description: "Your percentile rank (0-100)" },
460
- },
461
- },
462
- suggestion: {
463
- type: "string",
464
- description: "One actionable suggestion to improve your cascade efficiency",
465
- },
466
- },
467
- };
468
-
469
- const COMPARE_OPERATORS_OUTPUT = {
470
- type: "object",
471
- properties: {
472
- operator_a: {
473
- type: "object",
474
- description: "First operator's metrics",
475
- properties: {
476
- codename: { type: "string" },
477
- yield_: { type: "number" },
478
- leverage: { type: "number" },
479
- velocity: { type: "number" },
480
- class: { type: "string", enum: ["Burner", "Builder", "10xer"] },
481
- rank: { type: "integer" },
482
- },
483
- },
484
- operator_b: {
485
- type: "object",
486
- description: "Second operator's metrics",
487
- properties: {
488
- codename: { type: "string" },
489
- yield_: { type: "number" },
490
- leverage: { type: "number" },
491
- velocity: { type: "number" },
492
- class: { type: "string", enum: ["Burner", "Builder", "10xer"] },
493
- rank: { type: "integer" },
494
- },
495
- },
496
- verdict: {
497
- type: "string",
498
- description: "Who is more efficient and why, in behavioral terms",
499
- },
500
- yield_delta: { type: "number", description: "Yield difference (A - B)" },
501
- },
502
- };
503
-
504
- const SUBMIT_OUTPUT = {
505
- type: "object",
506
- properties: {
507
- status: {
508
- type: "string",
509
- enum: ["ok", "error", "skipped"],
510
- description: "Submission status",
511
- },
512
- preview: CASCADE_OUTPUT,
513
- server_response: {
514
- type: "object",
515
- description: "Server-side response including new rank if accepted",
516
- },
517
- reason: {
518
- type: "string",
519
- description: "Error or skip reason if status is not ok",
520
- },
521
- },
522
- };
523
-
524
- const TOKENPULL_OUTPUT = {
525
- type: "object",
526
- properties: {
527
- platform: { type: "string", description: "Source platform name" },
528
- generatedAt: { type: "string", description: "ISO timestamp of the pull" },
529
- windows: {
530
- type: "array",
531
- description: "Per-window token usage + cascade results",
532
- items: {
533
- type: "object",
534
- properties: {
535
- window: { type: "string", enum: ["7d", "30d", "90d", "all"] },
536
- pillars: {
537
- type: "object",
538
- properties: {
539
- input: { type: "integer" },
540
- output: { type: "integer" },
541
- cacheCreate: { type: "integer" },
542
- cacheRead: { type: "integer" },
543
- },
544
- },
545
- messages: {
546
- type: "integer",
547
- description: "Number of messages in window",
548
- },
549
- estimated: {
550
- type: "boolean",
551
- description: "True if cacheCreate was estimated",
552
- },
553
- cascade: CASCADE_OUTPUT,
554
- },
555
- },
556
- },
557
- },
558
- };
559
-
560
- const ENROLL_OUTPUT = {
561
- type: "object",
562
- properties: {
563
- status: {
564
- type: "string",
565
- enum: ["enrolled", "error"],
566
- description: "Enrollment result",
567
- },
568
- codename: { type: "string", description: "Operator codename if enrolled" },
569
- operator_id: { type: "string", description: "Operator ID if enrolled" },
570
- device_id: { type: "string", description: "Local device ID" },
571
- trust_status: { type: "string", description: "Trust level of the device" },
572
- reason: { type: "string", description: "Error reason if status is error" },
573
- },
574
- };
575
-
576
- const COMPARE_OUTPUT = {
577
- type: "object",
578
- properties: {
579
- platform: { type: "string" },
580
- sources: {
581
- type: "array",
582
- description: "Side-by-side comparison of each token source",
583
- items: {
584
- type: "object",
585
- properties: {
586
- source: {
587
- type: "string",
588
- description:
589
- "Source name (tokenpull, ccusage, token-dash, tokscale)",
590
- },
591
- pillars: { type: "object" },
592
- cascade: CASCADE_OUTPUT,
593
- delta_pct: {
594
- type: "object",
595
- description: "Delta % vs tokenpull baseline",
596
- },
597
- },
598
- },
599
- },
600
- },
601
- };
602
-
603
- const SIMULATE_OUTPUT = {
604
- type: "object",
605
- properties: {
606
- current: CASCADE_OUTPUT,
607
- simulated: CASCADE_OUTPUT,
608
- yield_delta: {
609
- type: "number",
610
- description: "Υ Yield change (simulated - current)",
611
- },
612
- class_change: {
613
- type: "string",
614
- description: "Class tier change description",
615
- },
616
- metric_diffs: {
617
- type: "object",
618
- description: "Per-metric before/after diffs",
619
- },
620
- },
621
- };
622
-
623
- const DIAGNOSE_OUTPUT = {
624
- type: "object",
625
- properties: {
626
- pillars: { type: "object", description: "The 4 raw token pillars" },
627
- cascade: CASCADE_OUTPUT,
628
- diagnosis: {
629
- type: "array",
630
- description: "Ranked list of efficiency leaks found, worst first",
631
- items: {
632
- type: "object",
633
- properties: {
634
- metric: {
635
- type: "string",
636
- description: "Which metric is underperforming",
637
- },
638
- severity: {
639
- type: "string",
640
- enum: ["critical", "warning", "info"],
641
- description: "How bad the leak is",
642
- },
643
- finding: { type: "string", description: "What the analysis found" },
644
- recommendation: {
645
- type: "string",
646
- description: "What to do about it",
647
- },
648
- estimated_yield_impact: {
649
- type: "string",
650
- description: "Estimated Υ improvement if fixed",
651
- },
652
- },
653
- },
654
- },
655
- summary: {
656
- type: "string",
657
- description: "One-line summary of the operator's cascade health",
658
- },
659
- },
660
- };
661
-
662
- const SUGGEST_OUTPUT = {
663
- type: "object",
664
- properties: {
665
- suggestions: {
666
- type: "array",
667
- description: "Ranked recommendations, highest Υ impact first",
668
- items: {
669
- type: "object",
670
- properties: {
671
- rank: { type: "number", description: "1-based rank by Υ impact" },
672
- action: { type: "string", description: "What to change" },
673
- pillar: { type: "string", description: "Which pillar to adjust" },
674
- delta: { type: "string", description: "How much to change it" },
675
- simulated_yield: {
676
- type: "number",
677
- description: "Projected Υ after the change",
678
- },
679
- yield_delta: { type: "number", description: "Υ change vs current" },
680
- class_after: {
681
- type: "string",
682
- description: "Projected class after the change",
683
- },
684
- rationale: { type: "string", description: "Why this helps" },
685
- },
686
- },
687
- },
688
- current_yield: {
689
- type: "number",
690
- description: "Current Υ before any changes",
691
- },
692
- current_class: { type: "string", description: "Current class tier" },
693
- best_single_change: {
694
- type: "string",
695
- description: "The single highest-impact change",
696
- },
697
- },
698
- };
699
-
700
- export const TOOLS = [
701
- {
702
- name: "rank_paste",
703
- description:
704
- '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.',
705
- annotations: {
706
- title: "Rank a paste",
707
- ...ANNOTATIONS.readOnlyHint,
708
- ...ANNOTATIONS.destructiveHint,
709
- ...ANNOTATIONS.idempotentHint,
710
- ...ANNOTATIONS.openWorldHint,
711
- },
712
- inputSchema: {
713
- type: "object",
714
- properties: {
715
- text: {
716
- type: "string",
717
- description:
718
- '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}',
719
- },
720
- },
721
- required: ["text"],
722
- description:
723
- "Requires the token counts as a string. No other parameters are accepted.",
724
- },
725
- outputSchema: CASCADE_OUTPUT,
726
- },
727
- {
728
- name: "get_leaderboard",
729
- description:
730
- "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.",
731
- annotations: { title: "Get leaderboard", ...ANNOTATIONS.readOnlyHint, ...ANNOTATIONS.openWorldHint },
732
- inputSchema: {
733
- type: "object",
734
- properties: {},
735
- description:
736
- "This tool takes no parameters. It always fetches the full public leaderboard.",
737
- },
738
- outputSchema: LEADERBOARD_OUTPUT,
739
- },
740
- {
741
- name: "get_operator",
742
- description:
743
- "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.",
744
- annotations: { title: "Get operator profile", ...ANNOTATIONS.readOnlyHint, ...ANNOTATIONS.openWorldHint },
745
- inputSchema: {
746
- type: "object",
747
- properties: {
748
- codename: {
749
- type: "string",
750
- description:
751
- '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.',
752
- },
753
- },
754
- required: ["codename"],
755
- description:
756
- "Requires the operator's codename. No other parameters are accepted.",
757
- },
758
- outputSchema: OPERATOR_OUTPUT,
759
- },
760
- {
761
- name: "submit_paste",
762
- description:
763
- "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.",
764
- annotations: {
765
- title: "Submit paste to board",
766
- ...ANNOTATIONS.destructiveHint,
767
- ...ANNOTATIONS.idempotentHint,
768
- ...ANNOTATIONS.openWorldHint,
769
- },
770
- inputSchema: {
771
- type: "object",
772
- properties: {
773
- text: {
774
- type: "string",
775
- description:
776
- '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}',
777
- },
778
- codename: {
779
- type: "string",
780
- description:
781
- '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.',
782
- },
783
- },
784
- required: ["text"],
785
- description:
786
- "Requires token counts (text). Codename is optional but required for board submission — omit it for preview-only mode.",
787
- },
788
- outputSchema: SUBMIT_OUTPUT,
789
- },
790
- {
791
- name: "tokenpull",
792
- description:
793
- "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.",
794
- annotations: { title: "Pull local token usage", ...ANNOTATIONS.readOnlyHint, ...ANNOTATIONS.openWorldHint },
795
- inputSchema: {
796
- type: "object",
797
- properties: {
798
- platform: {
799
- type: "string",
800
- enum: [...ALL_PLATFORMS, "multi"],
801
- description: `source platform (default: claude). Supported: ${ALL_PLATFORMS.join(", ")}, multi. 'multi' = combined cascade summed across all locally-detected platforms (needs 2+ active). 'devin' reads from ~/.local/share/devin/cli/sessions.db (SQLite, all windows). 'codex' is estimated via io_ratio. 'other' reads from a user-supplied JSON file (set SIGRANK_OTHER_PATH). Some platforms need setup (e.g. copilot requires COPILOT_OTEL_ENABLED=true).`,
802
- },
803
- },
804
- },
805
- outputSchema: TOKENPULL_OUTPUT,
806
- },
807
- {
808
- name: "tokenpull_submit",
809
- description:
810
- "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.",
811
- annotations: {
812
- title: "Pull and submit tokens",
813
- ...ANNOTATIONS.destructiveHint,
814
- ...ANNOTATIONS.idempotentHint,
815
- ...ANNOTATIONS.openWorldHint,
816
- },
817
- inputSchema: {
818
- type: "object",
819
- properties: {
820
- codename: {
821
- type: "string",
822
- description:
823
- 'Operator codename to publish under on the leaderboard (e.g. "Iron Lotus"). Required to submit — omit for local preview only.',
824
- },
825
- window: {
826
- type: "string",
827
- enum: ["7d", "30d", "90d", "all"],
828
- description:
829
- 'Submit only this time window (default: all 4 windows). Use "7d" for recent activity or "all" for all-time ranking.',
830
- },
831
- platform: {
832
- type: "string",
833
- enum: [...ALL_PLATFORMS, "multi"],
834
- description: `Source platform to pull from (default: claude). Supported: ${ALL_PLATFORMS.join(", ")}, multi. 'multi' = combined cascade summed across all locally-detected platforms (needs 2+ active). 'devin' reads from ~/.local/share/devin/cli/sessions.db (SQLite, all windows). 'other' reads from a user-supplied JSON file (set SIGRANK_OTHER_PATH). Each platform reads its own session logs locally.`,
835
- },
836
- },
837
- },
838
- outputSchema: SUBMIT_OUTPUT,
839
- },
840
- {
841
- name: "rank_windows",
842
- description:
843
- "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.",
844
- annotations: {
845
- title: "Rank all time windows",
846
- ...ANNOTATIONS.readOnlyHint,
847
- ...ANNOTATIONS.idempotentHint,
848
- ...ANNOTATIONS.openWorldHint,
849
- },
850
- inputSchema: {
851
- type: "object",
852
- properties: {
853
- "7d": {
854
- type: "string",
855
- description:
856
- "ccusage/tokscale paste or JSON for the 7-day window (optional)",
857
- },
858
- "30d": {
859
- type: "string",
860
- description:
861
- "ccusage/tokscale paste or JSON for the 30-day window (optional)",
862
- },
863
- "90d": {
864
- type: "string",
865
- description:
866
- "ccusage/tokscale paste or JSON for the 90-day window (optional)",
867
- },
868
- all: {
869
- type: "string",
870
- description:
871
- "ccusage/tokscale paste or JSON for the all-time window (optional)",
872
- },
873
- source_tool: {
874
- type: "string",
875
- enum: [
876
- "ccusage",
877
- "tokscale",
878
- "claude_max",
879
- "token_dashboard",
880
- "other",
881
- ],
882
- description:
883
- "which token reader produced the paste (for cross-tool variance tracking)",
884
- },
885
- },
886
- // at least one window paste is required (runtime check backs this up)
887
- anyOf: [
888
- { required: ["7d"] },
889
- { required: ["30d"] },
890
- { required: ["90d"] },
891
- { required: ["all"] },
892
- ],
893
- },
894
- outputSchema: {
895
- type: "object",
896
- properties: {
897
- windows: {
898
- type: "array",
899
- description: "Cascade results per window",
900
- items: {
901
- type: "object",
902
- properties: {
903
- window: { type: "string", enum: ["7d", "30d", "90d", "all"] },
904
- ...CASCADE_OUTPUT.properties,
905
- },
906
- },
907
- },
908
- },
909
- },
910
- },
911
- {
912
- name: "watch_tokenpull",
913
- description:
914
- "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).",
915
- annotations: {
916
- title: "Watch token pull",
917
- ...ANNOTATIONS.idempotentHint,
918
- ...ANNOTATIONS.openWorldHint,
919
- },
920
- inputSchema: {
921
- type: "object",
922
- properties: {
923
- platform: {
924
- type: "string",
925
- enum: ALL_PLATFORMS,
926
- description: "platform to watch (default: claude)",
927
- },
928
- interval_s: {
929
- type: "number",
930
- description:
931
- "advisory poll cadence in seconds (default: 60, min: 10) — echoed back as poll_interval_s; does not make the call block or loop",
932
- },
933
- window: {
934
- type: "string",
935
- enum: ["7d", "30d", "90d", "all"],
936
- description:
937
- "which window to watch (default: 7d — most sensitive to recent activity)",
938
- },
939
- submit: {
940
- type: "boolean",
941
- description:
942
- "auto-submit the watched window to the board as a VERIFIED operator each poll (requires `enroll`; default false = preview only)",
943
- },
944
- },
945
- },
946
- outputSchema: TOKENPULL_OUTPUT,
947
- },
948
- {
949
- name: "tokenpull_compare",
950
- description:
951
- "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.",
952
- annotations: { title: "Compare token sources", ...ANNOTATIONS.readOnlyHint, ...ANNOTATIONS.openWorldHint },
953
- inputSchema: {
954
- type: "object",
955
- properties: {
956
- platform: {
957
- type: "string",
958
- enum: ALL_PLATFORMS,
959
- description:
960
- "platform to compare (default: claude). token-dash and App only available for claude.",
961
- },
962
- },
963
- },
964
- outputSchema: COMPARE_OUTPUT,
965
- },
966
- {
967
- name: "enroll",
968
- description:
969
- '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.',
970
- annotations: {
971
- title: "Enroll device identity",
972
- ...ANNOTATIONS.destructiveHint,
973
- ...ANNOTATIONS.idempotentHint,
974
- ...ANNOTATIONS.openWorldHint,
975
- },
976
- inputSchema: {
977
- type: "object",
978
- properties: {
979
- code: {
980
- type: "string",
981
- description:
982
- "the key / connect code (SIGR-XXXXX-XXXXX-XXXXX) from Settings → New key (or Generate connect code)",
983
- },
984
- device_label: {
985
- type: "string",
986
- description:
987
- "optional label for this device (default: hostname · agent version)",
988
- },
989
- },
990
- required: ["code"],
991
- },
992
- outputSchema: ENROLL_OUTPUT,
993
- },
994
- {
995
- name: "submit_verified",
996
- description:
997
- "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.",
998
- annotations: {
999
- title: "Submit verified score",
1000
- ...ANNOTATIONS.destructiveHint,
1001
- ...ANNOTATIONS.idempotentHint,
1002
- ...ANNOTATIONS.openWorldHint,
1003
- },
1004
- inputSchema: {
1005
- type: "object",
1006
- properties: {
1007
- window: {
1008
- type: "string",
1009
- enum: ["7d", "30d", "90d", "all"],
1010
- description: "submit only this window (default: all 4)",
1011
- },
1012
- platform: {
1013
- type: "string",
1014
- enum: [...ALL_PLATFORMS, "multi"],
1015
- description:
1016
- "source platform (default: claude). 'multi' = combined cascade summed across all locally-detected platforms (needs 2+ active); empty windows are skipped.",
1017
- },
1018
- dry_run: {
1019
- type: "boolean",
1020
- description:
1021
- "build + sign but do NOT publish — returns the exact payload that would be POSTed (token counts only), so you can inspect before submitting",
1022
- },
1023
- },
1024
- },
1025
- outputSchema: SUBMIT_OUTPUT,
1026
- },
1027
- {
1028
- name: "simulate_change",
1029
- description:
1030
- "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}).",
1031
- annotations: {
1032
- title: "Simulate metric change",
1033
- ...ANNOTATIONS.readOnlyHint,
1034
- ...ANNOTATIONS.idempotentHint,
1035
- ...ANNOTATIONS.openWorldHint,
1036
- },
1037
- inputSchema: {
1038
- type: "object",
1039
- properties: {
1040
- text: {
1041
- type: "string",
1042
- description:
1043
- 'Current token pillars — ccusage JSON or "input output cacheCreate cacheRead" (same format as rank_paste).',
1044
- },
1045
- changes: {
1046
- type: "object",
1047
- description:
1048
- "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.",
1049
- properties: {
1050
- input: {
1051
- type: ["number", "string"],
1052
- description:
1053
- 'new input token count (absolute) or "+/-N" for a relative delta',
1054
- },
1055
- output: {
1056
- type: ["number", "string"],
1057
- description:
1058
- 'new output token count (absolute) or "+/-N" for a relative delta',
1059
- },
1060
- cacheCreate: {
1061
- type: ["number", "string"],
1062
- description:
1063
- 'new cache-create token count (absolute) or "+/-N" for a relative delta',
1064
- },
1065
- cacheRead: {
1066
- type: ["number", "string"],
1067
- description:
1068
- 'new cache-read token count (absolute) or "+/-N" for a relative delta',
1069
- },
1070
- },
1071
- },
1072
- },
1073
- required: ["text", "changes"],
1074
- },
1075
- outputSchema: SIMULATE_OUTPUT,
1076
- },
1077
- {
1078
- name: "diagnose_cascade",
1079
- description:
1080
- "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).",
1081
- annotations: {
1082
- title: "Diagnose cascade breakdown",
1083
- ...ANNOTATIONS.readOnlyHint,
1084
- ...ANNOTATIONS.idempotentHint,
1085
- ...ANNOTATIONS.openWorldHint,
1086
- },
1087
- inputSchema: {
1088
- type: "object",
1089
- properties: {
1090
- text: {
1091
- type: "string",
1092
- description:
1093
- 'Token pillars — ccusage JSON or "input output cacheCreate cacheRead" (same format as rank_paste).',
1094
- },
1095
- },
1096
- required: ["text"],
1097
- },
1098
- outputSchema: DIAGNOSE_OUTPUT,
1099
- },
1100
- {
1101
- name: "suggest_improvements",
1102
- description:
1103
- "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.",
1104
- annotations: {
1105
- title: "Suggest improvements",
1106
- ...ANNOTATIONS.readOnlyHint,
1107
- ...ANNOTATIONS.idempotentHint,
1108
- ...ANNOTATIONS.openWorldHint,
1109
- },
1110
- inputSchema: {
1111
- type: "object",
1112
- properties: {
1113
- text: {
1114
- type: "string",
1115
- description:
1116
- 'Token pillars — ccusage JSON or "input output cacheCreate cacheRead" (same format as rank_paste).',
1117
- },
1118
- },
1119
- required: ["text"],
1120
- },
1121
- outputSchema: SUGGEST_OUTPUT,
1122
- },
1123
- {
1124
- name: "self_improve",
1125
- description:
1126
- "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).",
1127
- annotations: {
1128
- title: "Self-improve plan",
1129
- ...ANNOTATIONS.readOnlyHint,
1130
- ...ANNOTATIONS.idempotentHint,
1131
- ...ANNOTATIONS.openWorldHint,
1132
- },
1133
- inputSchema: {
1134
- type: "object",
1135
- properties: {
1136
- text: {
1137
- type: "string",
1138
- description:
1139
- 'Optional: token pillars — ccusage JSON or "input output cacheCreate cacheRead". If omitted, runs tokenpull to get current pillars from local logs.',
1140
- },
1141
- window: {
1142
- type: "string",
1143
- enum: ["7d", "30d", "90d", "all"],
1144
- description:
1145
- "Which time window to pull when running tokenpull (default: 30d). Ignored if `text` is provided.",
1146
- },
1147
- scope: {
1148
- type: "string",
1149
- enum: ["daily", "weekly", "trend"],
1150
- description:
1151
- '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.',
1152
- },
1153
- },
1154
- },
1155
- outputSchema: {
1156
- type: "object",
1157
- properties: {
1158
- pillars: {
1159
- type: "object",
1160
- description: "The 4 raw token pillars used",
1161
- },
1162
- current_cascade: CASCADE_OUTPUT,
1163
- diagnosis: {
1164
- type: "array",
1165
- description: "Efficiency leaks found (from diagnose_cascade)",
1166
- },
1167
- suggestions: {
1168
- type: "array",
1169
- description: "Ranked improvements (from suggest_improvements)",
1170
- },
1171
- best_simulation: {
1172
- type: "object",
1173
- description: "Simulated result of the top suggestion",
1174
- },
1175
- cycle_summary: {
1176
- type: "string",
1177
- description: "One-line summary of the full cycle",
1178
- },
1179
- // Scope-specific fields
1180
- mode: {
1181
- type: "object",
1182
- description:
1183
- "Detected mode { mode, confidence } — present when scope is daily/weekly/trend",
1184
- },
1185
- quality_score: {
1186
- type: "number",
1187
- description: "Yield relative to mode expectation (daily scope)",
1188
- },
1189
- assessment: {
1190
- type: "string",
1191
- description: "One-line assessment for daily scope",
1192
- },
1193
- advice: {
1194
- type: "string",
1195
- description: "Advice for next session (daily scope)",
1196
- },
1197
- report: {
1198
- type: "object",
1199
- description: "Weekly report artifact (weekly scope)",
1200
- },
1201
- trend: { type: "object", description: "Trend analysis (trend scope)" },
1202
- },
1203
- },
1204
- },
1205
- {
1206
- name: "get_best_operator",
1207
- description:
1208
- "Returns the top N operators on the SigRank leaderboard with behavioral framing in power-user language. Wraps get_leaderboard and adds plain-language interpretation of each top operator's cascade: what their yield, leverage, and velocity mean in terms of AI power-user behavior (cache reuse, input economy, output productivity). Use this when users ask 'who is the best AI user?' or 'who tops the SigRank leaderboard?' or 'show me the AI user leaderboard'. Do NOT use get_leaderboard if you want the raw array without interpretation — use this for the power-user framing. Intent: BEST_OPERATOR.",
1209
- annotations: { title: "Get best operator", ...ANNOTATIONS.readOnlyHint, ...ANNOTATIONS.openWorldHint },
1210
- inputSchema: {
1211
- type: "object",
1212
- properties: {
1213
- n: {
1214
- type: "integer",
1215
- description:
1216
- "Number of top operators to return (default: 5, max: 20). Returns the top N by yield.",
1217
- minimum: 1,
1218
- maximum: 20,
1219
- },
1220
- },
1221
- description:
1222
- "Optional: how many top operators to return. Defaults to 5.",
1223
- },
1224
- outputSchema: BEST_OPERATOR_OUTPUT,
1225
- },
1226
- {
1227
- name: "compare_self",
1228
- description:
1229
- "Compares an operator's metrics against board averages and power-user archetypes, returning a behavioral assessment. Accepts either a codename (fetches from the board) or raw token pillars (computes locally). Returns: your yield/leverage/velocity/class/rank, a power-user assessment mapping your class tier to AI power-user language, comparison vs board averages (your percentile), and one actionable suggestion to improve. Use this when users ask 'how do I measure up to other AI users?' or 'am I a power user?' or 'compare me to others'. Intent: COMPARE_SELF.",
1230
- annotations: { title: "Compare self to board", ...ANNOTATIONS.readOnlyHint, ...ANNOTATIONS.openWorldHint },
1231
- inputSchema: {
1232
- type: "object",
1233
- properties: {
1234
- codename: {
1235
- type: "string",
1236
- description:
1237
- "Your codename on the SigRank leaderboard. If provided, fetches your live profile from the board. Case-insensitive.",
1238
- },
1239
- text: {
1240
- type: "string",
1241
- description:
1242
- 'Alternative: raw token pillars to score locally (ccusage JSON or "input output cacheCreate cacheRead"). Use this if you are not on the board yet but want to see how you would compare.',
1243
- },
1244
- },
1245
- description:
1246
- "Provide either `codename` (to fetch from the board) or `text` (to score locally). At least one is required.",
1247
- },
1248
- outputSchema: COMPARE_SELF_OUTPUT,
1249
- },
1250
- {
1251
- name: "compare_operators",
1252
- description:
1253
- "Compares two operators side-by-side with a behavioral verdict. Fetches both profiles from the board and returns their yield, leverage, velocity, class, and rank side-by-side, plus a verdict explaining who is more efficient and why in power-user language. Use this when users ask 'compare operator X vs Y' or 'who is more efficient' or 'how do two AI users compare'. Intent: COMPARE_OPERATORS.",
1254
- annotations: { title: "Compare two operators", ...ANNOTATIONS.readOnlyHint, ...ANNOTATIONS.openWorldHint },
1255
- inputSchema: {
1256
- type: "object",
1257
- properties: {
1258
- codename_a: {
1259
- type: "string",
1260
- description:
1261
- "First operator's codename from the SigRank leaderboard. Case-insensitive.",
1262
- },
1263
- codename_b: {
1264
- type: "string",
1265
- description:
1266
- "Second operator's codename from the SigRank leaderboard. Case-insensitive.",
1267
- },
1268
- },
1269
- required: ["codename_a", "codename_b"],
1270
- description:
1271
- "Requires both codenames. Both must exist on the board.",
1272
- },
1273
- outputSchema: COMPARE_OPERATORS_OUTPUT,
1274
- },
1275
- {
1276
- name: "describe_power_user",
1277
- description:
1278
- "Returns an explanatory description of what makes an AI power user, anchored in SigRank's metrics and operator classes. Explains the yield metric, leverage, velocity, and how class tiers (Burner/Builder/10xer) map to power-user behavior patterns. Use this when users ask 'what is an AI power user?' or 'what makes a good AI user?' or 'describe advanced AI user behavior'. Intent: DESCRIBE_POWER_USER (Informational).",
1279
- annotations: { title: "Describe power user", ...ANNOTATIONS.readOnlyHint, ...ANNOTATIONS.idempotentHint },
1280
- inputSchema: {
1281
- type: "object",
1282
- properties: {},
1283
- description:
1284
- "This tool takes no parameters. It returns a static explanatory response about AI power users.",
1285
- },
1286
- outputSchema: {
1287
- type: "object",
1288
- properties: {
1289
- description: { type: "string", description: "What is an AI power user" },
1290
- metrics_explained: {
1291
- type: "object",
1292
- description: "How SigRank metrics map to power-user behavior",
1293
- properties: {
1294
- yield_: { type: "string", description: "What yield measures in power-user terms" },
1295
- leverage: { type: "string", description: "What leverage means for power users" },
1296
- velocity: { type: "string", description: "What velocity means for power users" },
1297
- },
1298
- },
1299
- class_tiers: {
1300
- type: "array",
1301
- description: "Operator class tiers and their power-user meaning",
1302
- items: {
1303
- type: "object",
1304
- properties: {
1305
- class: { type: "string", enum: ["Burner", "Builder", "10xer"] },
1306
- meaning: { type: "string" },
1307
- },
1308
- },
1309
- },
1310
- link: { type: "string", description: "URL to learn more" },
1311
- },
1312
- },
1313
- },
1314
- {
1315
- name: "optimize_efficiency",
1316
- description:
1317
- "Returns actionable suggestions for improving your token cascade efficiency, tied to your current metrics. Accepts either a codename (fetches from board) or raw token pillars (computes locally). Returns: your current metrics, ranked efficiency suggestions tied to cascade shape (increase cache reuse, reduce input, increase output), and references to power-user practices. Use this when users ask 'how can I use AI more efficiently?' or 'reduce token burn' or 'optimize token usage' or 'stop tokenmaxxing'. Intent: OPTIMIZE_EFFICIENCY (Informational + Transactional).",
1318
- annotations: { title: "Optimize efficiency", ...ANNOTATIONS.readOnlyHint, ...ANNOTATIONS.idempotentHint },
1319
- inputSchema: {
1320
- type: "object",
1321
- properties: {
1322
- codename: {
1323
- type: "string",
1324
- description:
1325
- "Your codename on the SigRank leaderboard. If provided, fetches your live profile from the board.",
1326
- },
1327
- text: {
1328
- type: "string",
1329
- description:
1330
- 'Alternative: raw token pillars to score locally (ccusage JSON or "input output cacheCreate cacheRead"). Use this if you are not on the board yet.',
1331
- },
1332
- },
1333
- description:
1334
- "Provide either `codename` (to fetch from the board) or `text` (to score locally). At least one is required.",
1335
- },
1336
- outputSchema: {
1337
- type: "object",
1338
- properties: {
1339
- your_metrics: {
1340
- type: "object",
1341
- description: "Your current cascade metrics",
1342
- properties: {
1343
- yield_: { type: "number" },
1344
- leverage: { type: "number" },
1345
- velocity: { type: "number" },
1346
- class: { type: "string", enum: ["Burner", "Builder", "10xer"] },
1347
- },
1348
- },
1349
- suggestions: {
1350
- type: "array",
1351
- description: "Ranked efficiency suggestions",
1352
- items: {
1353
- type: "object",
1354
- properties: {
1355
- action: { type: "string", description: "What to change" },
1356
- why: { type: "string", description: "Why this helps your yield" },
1357
- power_user_practice: { type: "string", description: "The power-user practice this maps to" },
1358
- },
1359
- },
1360
- },
1361
- summary: { type: "string", description: "One-line summary of your efficiency status" },
1362
- },
1363
- },
1364
- },
1365
- {
1366
- name: "tokscale_breakdown",
1367
- description:
1368
- "Show a per-model breakdown of your token usage across all platforms detected by tokscale. Models under the threshold (default 1%) are lumped into 'other' to keep the display clean. Useful for seeing which models you actually use per platform (e.g. claude-opus-4-8 76%, claude-sonnet-4-6 11%, other 0.3%). Returns { platform: [{ model, input, output, cacheRead, cacheCreate, pct }] }.",
1369
- annotations: {
1370
- title: "Model breakdown",
1371
- ...ANNOTATIONS.readOnlyHint,
1372
- ...ANNOTATIONS.openWorldHint,
1373
- },
1374
- inputSchema: {
1375
- type: "object",
1376
- properties: {
1377
- threshold: {
1378
- type: "number",
1379
- description:
1380
- "Models below this fraction of their platform's total tokens are lumped into 'other'. Default 0.01 (1%). Set 0 to see every model.",
1381
- default: 0.01,
1382
- },
1383
- },
1384
- },
1385
- outputSchema: {
1386
- type: "object",
1387
- properties: {
1388
- platforms: {
1389
- type: "object",
1390
- description: "Map of platform name → array of { model, input, output, cacheRead, cacheCreate, pct }",
1391
- },
1392
- },
1393
- },
1394
- },
1395
- ];
1396
-
1397
- // tokenpull window key → the board's window_type enum.
1398
- const WINDOW_TYPE = { "7d": "7d", "30d": "30d", "90d": "90d", all: "all_time" };
1399
-
1400
- // E3: client-side auto-submit cooldown for watch_tokenpull. The server already dedups
1401
- // identical snapshots (exact hash → 422), but a noisy poll loop with submit:true could still
1402
- // churn the network/board with near-identical rows. Cap auto-submit to once per WATCH_SUBMIT_COOLDOWN_MS
1403
- // per platform+window (in-memory, per process). Keyed by platform:window so different
1404
- // platforms/windows don't block each other, and only armed on a non-error submit so a
1405
- // network failure doesn't lock out retries for 5 minutes.
1406
- const WATCH_SUBMIT_COOLDOWN_MS = 5 * 60 * 1000;
1407
- const _lastWatchSubmitAt = new Map();
1408
-
1409
- // ── Shared active-platform loader ───────────────────────────────────────────
1410
- // THE single data path for "show my cascade across platforms" — used by `me`,
1411
- // `watch`, and the TUI Dashboard so they can't drift apart again. Pulls each
1412
- // target platform via the tokenpull tool (enriched: pillars + cascade + card),
1413
- // keeps only platforms with real local data, and sorts claude → codex → rest.
1414
- // Pass `platforms` to scope it (e.g. ['claude'] for a fast first paint).
1415
- export async function pullActivePlatforms({ platforms } = {}, opts = {}) {
1416
- // If the caller specified explicit platforms, use those.
1417
- if (platforms && platforms.length) {
1418
- return _pullExplicit(platforms, opts);
1419
- }
1420
- // Auto-detect: run tokscale once to discover all active clients, then pull
1421
- // only those. This is much faster than trying all 17 adapters (most fail
1422
- // silently on a machine that doesn't have that tool installed).
1423
- //
1424
- // Flow: tokscale surfaces ALL clients → map to our platform names →
1425
- // ccusage is primary for Claude (more accurate) → other platforms use
1426
- // their adapters. Anything in tokscale NOT in ccusage gets included.
1427
- const detected = await _tokscaleDetectClients().catch(() => null);
1428
- if (detected && detected.length > 0) {
1429
- return _pullExplicit(detected, opts);
1430
- }
1431
- // Fallback: tokscale not installed or failed → try all adapters (old behavior)
1432
- return _pullExplicit(ALL_PLATFORMS, opts);
1433
- }
1434
-
1435
- /** Run `tokscale models --json` and return the list of our platform names. */
1436
- async function _tokscaleDetectClients() {
1437
- const raw = await execFileAsync("tokscale", ["models", "--json"], 60000);
1438
- const data = JSON.parse(raw);
1439
- const entries = Array.isArray(data?.entries)
1440
- ? data.entries
1441
- : Array.isArray(data)
1442
- ? data
1443
- : [];
1444
- if (!entries.length) return [];
1445
- // Complete tokscale client → our platform name mapping.
1446
- // Tokscale supports 40 clients (as of v3.1.0). We have native adapters for 16;
1447
- // the rest map to "other" (user-supplied JSON via SIGRANK_OTHER_PATH).
1448
- // Source: https://github.com/junhoyeo/tokscale
1449
- const TOKSCALE_CLIENT_MAP = {
1450
- // Native adapters
1451
- claude: "claude",
1452
- codex: "codex",
1453
- "devin-cli": "devin",
1454
- "devin-desktop": "devin",
1455
- gemini: "gemini",
1456
- amp: "amp",
1457
- kimi: "kimi",
1458
- qwen: "qwen",
1459
- goose: "goose",
1460
- kilo: "kilo",
1461
- kilocode: "kilo",
1462
- hermes: "hermes",
1463
- droid: "droid",
1464
- codebuff: "codebuff",
1465
- copilot: "copilot",
1466
- opencode: "opencode",
1467
- openclaw: "openclaw",
1468
- pi: "pi",
1469
- // No native adapter yet → "other"
1470
- cursor: "other",
1471
- roocode: "other",
1472
- mux: "other",
1473
- crush: "other",
1474
- antigravity: "other",
1475
- "antigravity-cli": "other",
1476
- zed: "other",
1477
- kiro: "other",
1478
- trae: "other",
1479
- warp: "other",
1480
- cline: "other",
1481
- gjc: "other",
1482
- grok: "other",
1483
- jcode: "other",
1484
- commandcode: "other",
1485
- micode: "other",
1486
- junie: "other",
1487
- zcode: "other",
1488
- opencodereview: "other",
1489
- codebuddy: "other",
1490
- workbuddy: "other",
1491
- // synthetic = re-attributed from other sources, skip
1492
- synthetic: null,
1493
- };
1494
- const clients = new Set();
1495
- for (const e of entries) {
1496
- if (!e || !e.client) continue;
1497
- if (e.model === "<synthetic>" || e.model === "unknown") continue;
1498
- const input = Number(e.input) || 0;
1499
- const output = Number(e.output) || 0;
1500
- if (input + output === 0) continue;
1501
- const platform = TOKSCALE_CLIENT_MAP[e.client];
1502
- if (platform === null) continue; // synthetic → skip
1503
- clients.add(platform || "other");
1504
- }
1505
- return [...clients];
1506
- }
1507
-
1508
- /**
1509
- * Run `tokscale models --json` and return a model-level breakdown per platform.
1510
- * Models under `threshold` (default 1%) of their platform's total are lumped
1511
- * into "other" to keep the display clean.
1512
- *
1513
- * Returns: { platform: [{ model, input, output, cacheRead, cacheCreate, pct }] }
1514
- */
1515
- export async function tokscaleModelBreakdown(threshold = 0.01) {
1516
- const raw = await execFileAsync("tokscale", ["models", "--json"], 60000);
1517
- const data = JSON.parse(raw);
1518
- const entries = Array.isArray(data?.entries)
1519
- ? data.entries
1520
- : Array.isArray(data)
1521
- ? data
1522
- : [];
1523
- if (!entries.length) return {};
1524
-
1525
- const TOKSCALE_CLIENT_MAP = {
1526
- claude: "claude", codex: "codex", "devin-cli": "devin",
1527
- "devin-desktop": "devin", gemini: "gemini", amp: "amp",
1528
- kimi: "kimi", qwen: "qwen", goose: "goose", kilo: "kilo",
1529
- kilocode: "kilo", hermes: "hermes", droid: "droid",
1530
- codebuff: "codebuff", copilot: "copilot", opencode: "opencode",
1531
- openclaw: "openclaw", pi: "pi",
1532
- cursor: "other", roocode: "other", mux: "other", crush: "other",
1533
- antigravity: "other", "antigravity-cli": "other", zed: "other",
1534
- kiro: "other", trae: "other", warp: "other", cline: "other",
1535
- gjc: "other", grok: "other", jcode: "other", commandcode: "other",
1536
- micode: "other", junie: "other", zcode: "other",
1537
- opencodereview: "other", codebuddy: "other", workbuddy: "other",
1538
- synthetic: null,
1539
- };
1540
-
1541
- // Group entries by our platform name
1542
- const byPlatform = {};
1543
- for (const e of entries) {
1544
- if (!e || !e.client) continue;
1545
- if (e.model === "<synthetic>" || e.model === "unknown") continue;
1546
- const input = Number(e.input) || 0;
1547
- const output = Number(e.output) || 0;
1548
- if (input + output === 0) continue;
1549
- const platform = TOKSCALE_CLIENT_MAP[e.client];
1550
- if (platform === null) continue;
1551
- const p = platform || "other";
1552
- if (!byPlatform[p]) byPlatform[p] = [];
1553
- byPlatform[p].push({
1554
- model: e.model,
1555
- input,
1556
- output,
1557
- cacheRead: Number(e.cacheRead) || 0,
1558
- cacheCreate: Number(e.cacheWrite) || 0,
1559
- });
1560
- }
1561
-
1562
- // For each platform: sort by total tokens desc, lump small models into "other"
1563
- const result = {};
1564
- for (const [platform, models] of Object.entries(byPlatform)) {
1565
- const total = models.reduce(
1566
- (s, m) => s + m.input + m.output + m.cacheRead + m.cacheCreate,
1567
- 0,
1568
- );
1569
- if (total === 0) continue;
1570
- const sorted = models.sort(
1571
- (a, b) =>
1572
- b.input + b.output + b.cacheRead + b.cacheCreate -
1573
- (a.input + a.output + a.cacheRead + a.cacheCreate),
1574
- );
1575
- const big = [];
1576
- const small = { model: "other", input: 0, output: 0, cacheRead: 0, cacheCreate: 0 };
1577
- for (const m of sorted) {
1578
- const t = m.input + m.output + m.cacheRead + m.cacheCreate;
1579
- if (t / total < threshold) {
1580
- small.input += m.input;
1581
- small.output += m.output;
1582
- small.cacheRead += m.cacheRead;
1583
- small.cacheCreate += m.cacheCreate;
1584
- } else {
1585
- big.push({ ...m, pct: t / total });
1586
- }
1587
- }
1588
- if (small.input + small.output + small.cacheRead + small.cacheCreate > 0) {
1589
- const t = small.input + small.output + small.cacheRead + small.cacheCreate;
1590
- big.push({ ...small, pct: t / total });
1591
- }
1592
- result[platform] = big;
1593
- }
1594
- return result;
1595
- }
1596
-
1597
- /** Pull a specific set of platforms in parallel, filter to active ones. */
1598
- async function _pullExplicit(platforms, opts = {}) {
1599
- const settled = await Promise.allSettled(
1600
- platforms.map((p) => callTool("tokenpull", { platform: p }, opts)),
1601
- );
1602
- const active = settled
1603
- .filter((r) => r.status === "fulfilled" && r.value)
1604
- .map((r) => r.value)
1605
- .filter((d) =>
1606
- (d.windows || []).some(
1607
- (w) => (w.pillars?.input ?? 0) + (w.pillars?.output ?? 0) > 0,
1608
- ),
1609
- );
1610
- const rank = (p) =>
1611
- p === "claude" ? -2 : p === "codex" ? -1 : ALL_PLATFORMS.indexOf(p);
1612
- active.sort((a, b) => rank(a.platform) - rank(b.platform));
1613
- return active;
1614
- }
1615
-
1616
- export async function callTool(name, args, opts = {}) {
1617
- const apiBase = opts.apiBase || DEFAULT_API_BASE;
1618
- const timeoutMs = opts.fetchTimeout ?? DEFAULT_FETCH_TIMEOUT;
1619
- const rawFetch = opts.fetchImpl || fetch;
1620
-
1621
- // Wrap every fetch with an AbortController timeout so a hung network call never
1622
- // blocks the MCP client indefinitely. Vercel's bot protection (security
1623
- // checkpoint) blocks Node's fetch via TLS fingerprinting — no UA fix helps.
1624
- // When fetch gets a 403 Vercel challenge, fall back to curl (which passes).
1625
- const doFetch = async (url, init = {}) => {
1626
- const ac = new AbortController();
1627
- const timer = setTimeout(() => ac.abort(), timeoutMs);
1628
- const headers = {
1629
- "user-agent": `node/${process.version} sigrank-mcp/${agentVersionStamp()}`,
1630
- ...(init.headers || {}),
1631
- };
1632
- try {
1633
- const res = await rawFetch(url, { ...init, headers, signal: ac.signal });
1634
- // Detect Vercel security checkpoint (403 + HTML body, not JSON)
1635
- if (res.status === 403) {
1636
- const text = await res.text();
1637
- if (text.includes("Vercel Security Checkpoint") || text.includes("x-vercel-challenge")) {
1638
- // Fall back to curl transport
1639
- return curlFetch(url, init, timeoutMs);
1640
- }
1641
- // Real 403 from the API — return a mock response object
1642
- return {
1643
- ok: false,
1644
- status: 403,
1645
- json: async () => { try { return JSON.parse(text); } catch { return {}; } },
1646
- text: async () => text,
1647
- };
1648
- }
1649
- return res;
1650
- } catch (e) {
1651
- if (e.name === "AbortError") throw e;
1652
- // Network error — try curl as fallback
1653
- try {
1654
- return await curlFetch(url, init, timeoutMs);
1655
- } catch {
1656
- throw e;
1657
- }
1658
- } finally {
1659
- clearTimeout(timer);
1660
- }
1661
- };
1662
-
1663
- const fetchJson = async (path) => {
1664
- const res = await doFetch(`${apiBase}${path}`, {
1665
- headers: { accept: "application/json" },
1666
- });
1667
- if (!res.ok) throw new Error(`SigRank API ${path} → HTTP ${res.status}`);
1668
- return res.json();
1669
- };
1670
-
1671
- // Helper: attach _parseWarnings from pillars onto the cascade result so they
1672
- // are always visible in the tool output for review.
1673
- const withParseWarnings = (pillars, cascadeResult) => {
1674
- if (pillars._parseWarnings && pillars._parseWarnings.length > 0) {
1675
- const existing = cascadeResult.warnings || [];
1676
- return {
1677
- ...cascadeResult,
1678
- warnings: [
1679
- ...existing,
1680
- ...pillars._parseWarnings.map((w) => `parse:${w}`),
1681
- ],
1682
- };
1683
- }
1684
- return cascadeResult;
1685
- };
1686
-
1687
- if (name === "rank_paste") {
1688
- if (!args?.text)
1689
- throw new Error("rank_paste requires a non-empty `text` argument.");
1690
- // E2: reject oversized pastes before parsing (parity with submit_paste / rank_windows).
1691
- if (typeof args.text === "string" && args.text.length > MAX_INPUT) {
1692
- return {
1693
- status: "error",
1694
- reason: "input_too_large",
1695
- detail: `text exceeds ${MAX_INPUT} chars (${args.text.length}). Paste only the token-count table, not full output.`,
1696
- };
1697
- }
1698
- const pillars = parsePillars(args.text);
1699
- const c = withParseWarnings(pillars, cascade(pillars));
1700
- return { ...c, card: narrate(c) };
1701
- }
1702
- if (name === "get_leaderboard") {
1703
- const params = new URLSearchParams({ metric: "yield_" });
1704
- if (args?.limit) params.set("limit", String(args.limit));
1705
- if (args?.window) params.set("window", args.window);
1706
- return fetchJson(`/api/v1/leaderboard?${params}`);
1707
- }
1708
- if (name === "get_operator") {
1709
- const codename = String(args?.codename || "").trim();
1710
- if (!codename)
1711
- throw new Error("get_operator requires a non-empty `codename` argument.");
1712
- return fetchJson(`/api/v1/operators/${encodeURIComponent(codename)}`);
1713
- }
1714
-
1715
- if (name === "enroll") {
1716
- // Redeem a web connect code → bind this device. Generates/loads the local keypair;
1717
- // sends ONLY the public key. operator binding happens server-side from the code row.
1718
- const code = String(args?.code || "").trim();
1719
- if (!code)
1720
- throw new Error(
1721
- "enroll requires a `code` — paste your connect code from signalaf.com → Settings → Connect a device.",
1722
- );
1723
- const id = opts.identity || ensureIdentity();
1724
- const deviceLabel = String(
1725
- args?.device_label || `${os.hostname()} · ${id.agent_version}`,
1726
- ).slice(0, 120);
1727
- const res = await doFetch(`${apiBase}/api/v1/devices/enroll`, {
1728
- method: "POST",
1729
- headers: {
1730
- "content-type": "application/json",
1731
- accept: "application/json",
1732
- },
1733
- body: JSON.stringify({
1734
- code,
1735
- device_id: id.device_id,
1736
- public_key: id.public_key,
1737
- device_label: deviceLabel,
1738
- agent_version: id.agent_version,
1739
- }),
1740
- });
1741
- let ack;
1742
- try {
1743
- ack = await res.json();
1744
- } catch {
1745
- ack = {};
1746
- }
1747
- if (res.status === 201 && ack.status === "enrolled") {
1748
- // Persist the binding locally (skipped when a test injects opts.identity → no keystore write).
1749
- if (!opts.identity)
1750
- recordEnrollment({
1751
- codename: ack.codename,
1752
- operator_id: ack.operator_id,
1753
- });
1754
- return {
1755
- status: "enrolled",
1756
- codename: ack.codename ?? null,
1757
- operator_id: ack.operator_id ?? null,
1758
- device_id: id.device_id,
1759
- trust_status: ack.trust_status ?? "trusted",
1760
- };
1761
- }
1762
- // Recovery: if the server says device_already_enrolled but includes the
1763
- // codename/operator_id, the device IS bound server-side — record it locally
1764
- // and return enrolled instead of erroring. This handles the case where the
1765
- // local binding was lost (partial write, version transition) but the device
1766
- // is still enrolled server-side.
1767
- if (
1768
- ack.reason === "device_already_enrolled" &&
1769
- ack.codename &&
1770
- ack.operator_id
1771
- ) {
1772
- if (!opts.identity)
1773
- recordEnrollment({
1774
- codename: ack.codename,
1775
- operator_id: ack.operator_id,
1776
- });
1777
- return {
1778
- status: "enrolled",
1779
- codename: ack.codename,
1780
- operator_id: ack.operator_id,
1781
- device_id: id.device_id,
1782
- trust_status: ack.trust_status ?? "trusted",
1783
- recovered: true,
1784
- };
1785
- }
1786
- return {
1787
- status: "error",
1788
- httpStatus: res.status,
1789
- reason: ack.reason || ack.status || `http_${res.status}`,
1790
- detail: ack.detail ?? null,
1791
- };
1792
- }
1793
-
1794
- if (name === "submit_verified") {
1795
- // The enrolled, signed publish path → /api/v1/snapshots (only verified rows rank).
1796
- const id = opts.identity || ensureIdentity();
1797
- if (!id.codename || !id.operator_id) {
1798
- return {
1799
- status: "not_enrolled",
1800
- detail: "Run `npx sigrank-mcp enroll` to bind this device first.",
1801
- };
1802
- }
1803
- const platform = args?.platform || "claude";
1804
-
1805
- // MULTI: the combined cross-platform cascade. The dashboard already SUMS every
1806
- // active platform's pillars (a "claude+codex" row) but never submitted it — this
1807
- // is that missing submission. Aggregate every locally-detected platform's pillars
1808
- // per window and publish as platform='multi' = the operator's TOTAL usage. Empty
1809
- // windows are skipped so a no-activity window never lands as a degenerate row.
1810
- if (platform === "multi") {
1811
- const detected = [];
1812
- for (const p of ALL_PLATFORMS) {
1813
- const r = await pullByPlatform(p, opts).catch(() => null);
1814
- const live =
1815
- r &&
1816
- (r.windows || []).some(
1817
- (w) =>
1818
- w.pillars.input +
1819
- w.pillars.output +
1820
- w.pillars.cacheCreate +
1821
- w.pillars.cacheRead >
1822
- 0,
1823
- );
1824
- if (live) detected.push(r);
1825
- }
1826
- if (detected.length < 2) {
1827
- return {
1828
- platform: "multi",
1829
- codename: id.codename,
1830
- operator_id: id.operator_id,
1831
- status: "skipped",
1832
- reason: "need_2_platforms",
1833
- detail: `multi needs 2+ active platforms; found ${detected.length} (${detected.map((d) => d.platform).join(", ") || "none"}).`,
1834
- windows: [],
1835
- };
1836
- }
1837
- const winKeys = args?.window
1838
- ? [args.window]
1839
- : ["7d", "30d", "90d", "all"];
1840
- const out = [];
1841
- for (const wk of winKeys) {
1842
- const sum = { input: 0, output: 0, cacheCreate: 0, cacheRead: 0 };
1843
- let msgs = 0;
1844
- for (const d of detected) {
1845
- const w = (d.windows || []).find((x) => x.window === wk);
1846
- if (!w) continue;
1847
- sum.input += w.pillars.input || 0;
1848
- sum.output += w.pillars.output || 0;
1849
- sum.cacheCreate += w.pillars.cacheCreate || 0;
1850
- sum.cacheRead += w.pillars.cacheRead || 0;
1851
- msgs += w.messages || 0;
1852
- }
1853
- if (sum.input + sum.output + sum.cacheCreate + sum.cacheRead <= 0)
1854
- continue; // skip empty window
1855
- const report = _computeReportBlock(sum);
1856
- const r = await submitSignedWindow(wk, sum, msgs, id, {
1857
- apiBase,
1858
- fetchImpl: doFetch,
1859
- platform: "multi",
1860
- now: opts.now,
1861
- dryRun: !!args?.dry_run,
1862
- report,
1863
- });
1864
- out.push({ window: wk, pillars: sum, ...r });
1865
- }
1866
- return {
1867
- platform: "multi",
1868
- codename: id.codename,
1869
- operator_id: id.operator_id,
1870
- sources: detected.map((d) => d.platform),
1871
- windows: out,
1872
- };
1873
- }
1874
-
1875
- const pulled = await pullByPlatform(platform, opts);
1876
- const targets = args?.window
1877
- ? pulled.windows.filter((w) => w.window === args.window)
1878
- : pulled.windows;
1879
- const out = [];
1880
- for (const w of targets) {
1881
- // Compute the cascade report block (mode + badges + health) for this window.
1882
- // Pure math, computed locally, submitted alongside the 4 token pillars.
1883
- const report = _computeReportBlock(w.pillars);
1884
- const r = await submitSignedWindow(w.window, w.pillars, w.messages, id, {
1885
- apiBase,
1886
- fetchImpl: doFetch,
1887
- platform: pulled.platform,
1888
- now: opts.now,
1889
- dryRun: !!args?.dry_run,
1890
- report,
1891
- });
1892
- out.push({ window: w.window, pillars: w.pillars, ...r });
1893
- }
1894
- return {
1895
- platform: pulled.platform,
1896
- codename: id.codename,
1897
- operator_id: id.operator_id,
1898
- generatedAt: pulled.generatedAt,
1899
- windows: out,
1900
- };
1901
- }
1902
-
1903
- if (name === "submit_paste") {
1904
- if (!args?.text)
1905
- throw new Error("submit_paste requires a non-empty `text` argument.");
1906
- if (typeof args.text === "string" && args.text.length > MAX_INPUT) {
1907
- return {
1908
- status: "error",
1909
- reason: "input_too_large",
1910
- detail: `text exceeds ${MAX_INPUT} chars (${args.text.length}). Paste only the token-count table, not full output.`,
1911
- };
1912
- }
1913
- // Local preview first — also validates the paste is parseable before any POST.
1914
- const pillars = parsePillars(args.text);
1915
- const c = withParseWarnings(pillars, cascade(pillars));
1916
- const codename = String(args?.codename || "").trim();
1917
- const card = narrate(c, codename || "This operator");
1918
-
1919
- // No codename → cannot publish (the board endpoint requires it). Fail fast at the
1920
- // tool boundary with a clear message instead of an opaque server 400.
1921
- if (!codename) {
1922
- return {
1923
- ...c,
1924
- card,
1925
- submission: {
1926
- status: "not_submitted",
1927
- reason: "codename_required",
1928
- detail:
1929
- "Pass a codename to publish to the board. Showing local preview only.",
1930
- },
1931
- };
1932
- }
1933
-
1934
- // Submit the RAW paste so the server re-parses + re-scores authoritatively — the
1935
- // MCP's local cascade is only a preview; the board stays the single source of truth.
1936
- const stamp = uploadStamp({
1937
- codename,
1938
- pillars: c.pillars,
1939
- source: "web_paste",
1940
- });
1941
- const res = await doFetch(`${apiBase}/api/v1/ingest-paste`, {
1942
- method: "POST",
1943
- headers: {
1944
- "content-type": "application/json",
1945
- accept: "application/json",
1946
- },
1947
- body: JSON.stringify({
1948
- codename,
1949
- raw_paste: String(args?.text || ""),
1950
- ...stamp,
1951
- }),
1952
- });
1953
- let ack;
1954
- try {
1955
- ack = await res.json();
1956
- } catch {
1957
- ack = {
1958
- status: "error",
1959
- detail: `HTTP ${res.status} (non-JSON response)`,
1960
- };
1961
- }
1962
- const ranked = !!(
1963
- res.ok &&
1964
- ack.verification_tier === "verified" &&
1965
- ack.persisted === true
1966
- );
1967
- return {
1968
- ...c,
1969
- card,
1970
- ranked,
1971
- submission: { ...stamp, httpStatus: res.status, ranked, ...ack },
1972
- };
1973
- }
1974
-
1975
- if (name === "tokenpull") {
1976
- // Local read → 4 windows of pillars → cascade each. Token-only, on-device.
1977
- const platform = args?.platform || "claude";
1978
-
1979
- // MULTI: sum every locally-detected platform's pillars per window. Same logic as
1980
- // submit_verified's multi flow — Devin (cloud, via tokscale) is included.
1981
- if (platform === "multi") {
1982
- const detected = [];
1983
- for (const p of ALL_PLATFORMS) {
1984
- const r = await pullByPlatform(p, opts).catch(() => null);
1985
- const live =
1986
- r &&
1987
- (r.windows || []).some(
1988
- (w) =>
1989
- w.pillars.input +
1990
- w.pillars.output +
1991
- w.pillars.cacheCreate +
1992
- w.pillars.cacheRead >
1993
- 0,
1994
- );
1995
- if (live) detected.push(r);
1996
- }
1997
- if (detected.length < 2) {
1998
- return {
1999
- platform: "multi",
2000
- status: "skipped",
2001
- reason: "need_2_platforms",
2002
- detail: `multi needs 2+ active platforms; found ${detected.length} (${detected.map((d) => d.platform).join(", ") || "none"}).`,
2003
- windows: [],
2004
- };
2005
- }
2006
- const winKeys = ["7d", "30d", "90d", "all"];
2007
- const windows = [];
2008
- for (const wk of winKeys) {
2009
- const sum = { input: 0, output: 0, cacheCreate: 0, cacheRead: 0 };
2010
- let msgs = 0;
2011
- for (const d of detected) {
2012
- const w = (d.windows || []).find((x) => x.window === wk);
2013
- if (!w) continue;
2014
- sum.input += w.pillars.input || 0;
2015
- sum.output += w.pillars.output || 0;
2016
- sum.cacheCreate += w.pillars.cacheCreate || 0;
2017
- sum.cacheRead += w.pillars.cacheRead || 0;
2018
- msgs += w.messages || 0;
2019
- }
2020
- if (sum.input + sum.output + sum.cacheCreate + sum.cacheRead <= 0)
2021
- continue;
2022
- const c = cascade(sum);
2023
- windows.push({
2024
- window: wk,
2025
- messages: msgs,
2026
- pillars: sum,
2027
- cascade: c,
2028
- card: narrate(c, `${wk} multi`),
2029
- });
2030
- }
2031
- return {
2032
- platform: "multi",
2033
- estimated: true,
2034
- sources: detected.map((d) => d.platform),
2035
- windows,
2036
- };
2037
- }
2038
-
2039
- const pulled = await pullByPlatform(platform, opts);
2040
- const windows = pulled.windows.map((w) => {
2041
- const c = cascade(w.pillars);
2042
- return {
2043
- window: w.window,
2044
- messages: w.messages,
2045
- pillars: w.pillars,
2046
- cascade: c,
2047
- card: narrate(c, `${w.window} ${platform}`),
2048
- };
2049
- });
2050
- return {
2051
- platform: pulled.platform,
2052
- estimated: pulled.estimated || false,
2053
- ...(pulled.ioRatio ? { ioRatio: pulled.ioRatio } : {}),
2054
- generatedAt: pulled.generatedAt,
2055
- files: pulled.files,
2056
- totalMessages: pulled.totalMessages,
2057
- windows,
2058
- };
2059
- }
2060
-
2061
- if (name === "tokenpull_submit") {
2062
- // Pull local usage, then publish each window's CANONICAL pillars to the board
2063
- // (server re-scores). The board stays platform-agnostic via the 4 pillars; the
2064
- // source platform rides along as a tag. Conversion already happened in the adapter.
2065
- const codename = String(args?.codename || "").trim();
2066
- const platform = args?.platform || "claude";
2067
-
2068
- // MULTI: same combined cross-platform cascade as submit_verified. Includes Devin
2069
- // (cloud, via tokscale). Aggregate every locally-detected platform's pillars per
2070
- // window and publish as platform='multi'.
2071
- if (platform === "multi") {
2072
- const detected = [];
2073
- for (const p of ALL_PLATFORMS) {
2074
- const r = await pullByPlatform(p, opts).catch(() => null);
2075
- const live =
2076
- r &&
2077
- (r.windows || []).some(
2078
- (w) =>
2079
- w.pillars.input +
2080
- w.pillars.output +
2081
- w.pillars.cacheCreate +
2082
- w.pillars.cacheRead >
2083
- 0,
2084
- );
2085
- if (live) detected.push(r);
2086
- }
2087
- if (detected.length < 2) {
2088
- return {
2089
- platform: "multi",
2090
- codename: codename || undefined,
2091
- status: "skipped",
2092
- reason: "need_2_platforms",
2093
- detail: `multi needs 2+ active platforms; found ${detected.length} (${detected.map((d) => d.platform).join(", ") || "none"}).`,
2094
- windows: [],
2095
- };
2096
- }
2097
- const winKeys = args?.window ? [args.window] : ["7d", "30d", "90d", "all"];
2098
- const out = [];
2099
- for (const wk of winKeys) {
2100
- const sum = { input: 0, output: 0, cacheCreate: 0, cacheRead: 0 };
2101
- let msgs = 0;
2102
- for (const d of detected) {
2103
- const w = (d.windows || []).find((x) => x.window === wk);
2104
- if (!w) continue;
2105
- sum.input += w.pillars.input || 0;
2106
- sum.output += w.pillars.output || 0;
2107
- sum.cacheCreate += w.pillars.cacheCreate || 0;
2108
- sum.cacheRead += w.pillars.cacheRead || 0;
2109
- msgs += w.messages || 0;
2110
- }
2111
- if (sum.input + sum.output + sum.cacheCreate + sum.cacheRead <= 0)
2112
- continue;
2113
- const c = cascade(sum);
2114
- const card = narrate(c, `${wk} multi`);
2115
- if (!codename) {
2116
- out.push({
2117
- window: wk,
2118
- pillars: sum,
2119
- cascade: c,
2120
- card,
2121
- submission: { status: "not_submitted", reason: "codename_required" },
2122
- });
2123
- continue;
2124
- }
2125
- const rawPaste = `${sum.input} ${sum.output} ${sum.cacheCreate} ${sum.cacheRead}`;
2126
- const windowType = WINDOW_TYPE[wk] || wk;
2127
- const stamp = uploadStamp({
2128
- codename,
2129
- window: windowType,
2130
- pillars: sum,
2131
- platform: "multi",
2132
- });
2133
- const res = await doFetch(`${apiBase}/api/v1/ingest-paste`, {
2134
- method: "POST",
2135
- headers: {
2136
- "content-type": "application/json",
2137
- accept: "application/json",
2138
- },
2139
- body: JSON.stringify({
2140
- codename,
2141
- raw_paste: rawPaste,
2142
- window_type: windowType,
2143
- telemetry: { platform: { primary: "multi" } },
2144
- ...stamp,
2145
- }),
2146
- });
2147
- let ack;
2148
- try {
2149
- ack = await res.json();
2150
- } catch {
2151
- ack = { status: "parse_error", httpStatus: res.status };
2152
- }
2153
- const ranked = ack?.ranked ?? ack?.accepted ?? false;
2154
- out.push({
2155
- window: wk,
2156
- pillars: sum,
2157
- cascade: c,
2158
- card,
2159
- submission: { ...stamp, httpStatus: res.status, ranked, ...ack },
2160
- });
2161
- }
2162
- return {
2163
- platform: "multi",
2164
- codename: codename || undefined,
2165
- sources: detected.map((d) => d.platform),
2166
- windows: out,
2167
- };
2168
- }
2169
-
2170
- const pulled = await pullByPlatform(platform, opts);
2171
- const targets = args?.window
2172
- ? pulled.windows.filter((w) => w.window === args.window)
2173
- : pulled.windows;
2174
- const out = [];
2175
- for (const w of targets) {
2176
- const c = cascade(w.pillars);
2177
- const card = narrate(c, `${w.window} window`);
2178
- if (!codename) {
2179
- out.push({
2180
- window: w.window,
2181
- pillars: w.pillars,
2182
- cascade: c,
2183
- card,
2184
- submission: { status: "not_submitted", reason: "codename_required" },
2185
- });
2186
- continue;
2187
- }
2188
- // canonical pillars → "input output cacheCreate cacheRead" (the parser's 4-bare-number form)
2189
- const rawPaste = `${w.pillars.input} ${w.pillars.output} ${w.pillars.cacheCreate} ${w.pillars.cacheRead}`;
2190
- const windowType = WINDOW_TYPE[w.window] || w.window;
2191
- const stamp = uploadStamp({
2192
- codename,
2193
- window: windowType,
2194
- pillars: w.pillars,
2195
- platform: pulled.platform,
2196
- });
2197
- const res = await doFetch(`${apiBase}/api/v1/ingest-paste`, {
2198
- method: "POST",
2199
- headers: {
2200
- "content-type": "application/json",
2201
- accept: "application/json",
2202
- },
2203
- body: JSON.stringify({
2204
- codename,
2205
- raw_paste: rawPaste,
2206
- window_type: windowType,
2207
- telemetry: { platform: { primary: pulled.platform } },
2208
- ...stamp,
2209
- }),
2210
- });
2211
- let ack;
2212
- try {
2213
- ack = await res.json();
2214
- } catch {
2215
- ack = { status: "error", detail: `HTTP ${res.status} (non-JSON)` };
2216
- }
2217
- // ranked = actually on the board (verified + persisted), not just received — parity
2218
- // with submit_verified (submit.mjs). An unenrolled/revoked device gets 202 but is NEVER ranked.
2219
- const ranked = !!(
2220
- res.ok &&
2221
- ack.verification_tier === "verified" &&
2222
- ack.persisted === true
2223
- );
2224
- out.push({
2225
- window: w.window,
2226
- pillars: w.pillars,
2227
- cascade: c,
2228
- card,
2229
- ranked,
2230
- submission: { ...stamp, httpStatus: res.status, ranked, ...ack },
2231
- });
2232
- }
2233
- return {
2234
- platform: pulled.platform,
2235
- codename: codename || null,
2236
- generatedAt: pulled.generatedAt,
2237
- windows: out,
2238
- };
2239
- }
2240
-
2241
- if (name === "rank_windows") {
2242
- // Score up to 4 named window pastes independently. Named-key parsing only —
2243
- // positional is unsafe here because dashboards list cache_read before cache_create
2244
- // (the WINDOWED_PROFILES swap gotcha). Each window goes through parsePillars →
2245
- // cascade → narrate individually; results are collected into a windows[] array
2246
- // in the same shape as tokenpull output for easy follow-up with tokenpull_submit.
2247
- const WINDOW_KEYS = ["7d", "30d", "90d", "all"];
2248
- const sourceTool = args?.source_tool || null;
2249
- // E2: reject any oversized window paste up front (token tables are tiny).
2250
- for (const wk of WINDOW_KEYS) {
2251
- const v = args?.[wk];
2252
- if (typeof v === "string" && v.length > MAX_INPUT) {
2253
- return {
2254
- status: "error",
2255
- reason: "input_too_large",
2256
- detail: `window '${wk}' exceeds ${MAX_INPUT} chars (${v.length}). Paste only the token-count table.`,
2257
- };
2258
- }
2259
- }
2260
- const windows = [];
2261
- for (const wk of WINDOW_KEYS) {
2262
- const text = args?.[wk];
2263
- if (!text || typeof text !== "string" || !text.trim()) continue;
2264
- const pillars = parsePillars(text);
2265
- const c = withParseWarnings(pillars, cascade(pillars));
2266
- const card = narrate(c, `${wk} window`);
2267
- windows.push({ window: wk, pillars, cascade: c, card });
2268
- }
2269
- if (windows.length === 0) {
2270
- throw new Error(
2271
- "rank_windows requires at least one window paste (7d, 30d, 90d, or all).",
2272
- );
2273
- }
2274
- return {
2275
- windows,
2276
- source_tool: sourceTool,
2277
- note: "Local preview only — use tokenpull_submit to publish to the board.",
2278
- };
2279
- }
2280
-
2281
- if (name === "watch_tokenpull") {
2282
- // Poll the local token logs at a configurable interval and return the cascade
2283
- // diff whenever new sessions appear. One poll cycle per MCP call — the client
2284
- // is responsible for re-calling at the desired cadence (MCP tools are stateless;
2285
- // a persistent background watcher lives outside the tool boundary).
2286
- //
2287
- // With submit:true + an enrolled device, this also signs + POSTs the watched window
2288
- // to the verified ingest path each poll (the server dedups identical re-submits).
2289
- const platform = args?.platform || "claude";
2290
- const watchWindow = args?.window || "7d";
2291
- const intervalS = Math.max(10, Number(args?.interval_s) || 60);
2292
-
2293
- const pulled = await pullByPlatform(platform, opts);
2294
- const win = pulled.windows.find((w) => w.window === watchWindow);
2295
- if (!win)
2296
- throw new Error(
2297
- `watch_tokenpull: window '${watchWindow}' not found in pull result.`,
2298
- );
2299
-
2300
- const c = cascade(win.pillars);
2301
- const card = narrate(c, `${watchWindow} ${platform}`);
2302
-
2303
- // AUTH.WIRE (D7 §7): when submit is on AND the device is enrolled, sign + POST the
2304
- // watched window to the verified ingest path. Default OFF = preview only. The server
2305
- // dedups identical re-submits (exact snapshot_hash → 422), so re-calling is safe.
2306
- let auth_submit = null;
2307
- if (args?.submit === true) {
2308
- const id = opts.identity || ensureIdentity();
2309
- if (id.codename && id.operator_id && id.private_key_pkcs8_b64) {
2310
- // E3: client-side cooldown — at most one auto-submit per platform+window per 5 min.
2311
- // Prevents a fast poll loop from churning the board even before the server's
2312
- // hash-dedup kicks in. Armed only on a non-error outcome so failed submits retry.
2313
- const clockNow = typeof opts.now === "number" ? opts.now : Date.now();
2314
- const cdKey = `${pulled.platform}:${watchWindow}`;
2315
- const last = _lastWatchSubmitAt.get(cdKey);
2316
- if (last != null && clockNow - last < WATCH_SUBMIT_COOLDOWN_MS) {
2317
- const waitS = Math.ceil(
2318
- (WATCH_SUBMIT_COOLDOWN_MS - (clockNow - last)) / 1000,
2319
- );
2320
- auth_submit = {
2321
- status: "cooldown",
2322
- detail: `auto-submit for '${cdKey}' on cooldown — next in ~${waitS}s (max once / 5 min).`,
2323
- retry_after_s: waitS,
2324
- };
2325
- } else {
2326
- auth_submit = await submitSignedWindow(
2327
- watchWindow,
2328
- win.pillars,
2329
- win.messages,
2330
- id,
2331
- {
2332
- apiBase,
2333
- fetchImpl: doFetch,
2334
- platform: pulled.platform,
2335
- now: opts.now,
2336
- },
2337
- );
2338
- if (auth_submit?.status !== "error")
2339
- _lastWatchSubmitAt.set(cdKey, clockNow);
2340
- }
2341
- } else {
2342
- auth_submit = {
2343
- status: "not_enrolled",
2344
- detail: "Run `npx sigrank-mcp enroll` to auto-submit verified runs.",
2345
- };
2346
- }
2347
- }
2348
- return {
2349
- platform: pulled.platform,
2350
- window: watchWindow,
2351
- pillars: win.pillars,
2352
- messages: win.messages,
2353
- cascade: c,
2354
- card,
2355
- generatedAt: pulled.generatedAt,
2356
- poll_interval_s: intervalS,
2357
- auth_submit,
2358
- note: "One snapshot per call — re-call at your poll interval to detect changes.",
2359
- };
2360
- }
2361
-
2362
- if (name === "tokenpull_compare") {
2363
- const platform = args?.platform || "claude";
2364
- const WINS = ["7d", "30d", "90d", "all"];
2365
-
2366
- // Pull all four sources in parallel (verifiers are now async via execFile)
2367
- const [tpResult, ccPillars, tdPillars, tsPillars] = await Promise.all([
2368
- pullByPlatform(platform, opts).catch(() => null),
2369
- _ccusagePillars(platform).catch(() => null),
2370
- (platform === "claude"
2371
- ? _tokenDashPillars()
2372
- : Promise.resolve(null)
2373
- ).catch(() => null),
2374
- _tokscalePillars(platform).catch(() => null),
2375
- ]);
2376
-
2377
- // Build tokenpull window lookup
2378
- const tpByWin = {};
2379
- for (const w of tpResult?.windows ?? []) tpByWin[w.window] = w.pillars;
2380
-
2381
- // Helper: delta % vs tokenpull baseline
2382
- const delta = (val, base) =>
2383
- base > 0 ? +(((val - base) / base) * 100).toFixed(1) : null;
2384
-
2385
- // Build per-source per-window comparison
2386
- const SOURCES = [
2387
- {
2388
- source: "tokenpull",
2389
- note: "JSONL deduped by msg id — canon source",
2390
- byWin: tpByWin,
2391
- },
2392
- {
2393
- source: "ccusage",
2394
- note: "ccusage CLI — monthly only",
2395
- byWin: ccPillars ?? {},
2396
- },
2397
- {
2398
- source: "token-dash",
2399
- note: "token-dashboard SQLite — all-time only",
2400
- byWin: tdPillars ?? {},
2401
- },
2402
- {
2403
- source: "tokscale",
2404
- note: "tokscale_report.json — all-time only",
2405
- byWin: tsPillars ?? {},
2406
- },
2407
- ];
2408
-
2409
- const comparison = {};
2410
- for (const win of WINS) {
2411
- const baseP = tpByWin[win];
2412
- comparison[win] = SOURCES.filter((s) => s.byWin[win] != null).map((s) => {
2413
- const p = s.byWin[win];
2414
- const cas = cascade(p);
2415
- const entry = {
2416
- source: s.source,
2417
- note: s.note,
2418
- pillars: p,
2419
- cascade: {
2420
- yield: cas.yield,
2421
- snr: cas.snr,
2422
- leverage: cas.leverage,
2423
- velocity: cas.velocity,
2424
- dev10x: cas.dev10x,
2425
- class: cas.class,
2426
- },
2427
- };
2428
- if (s.source !== "tokenpull" && baseP) {
2429
- entry.delta_vs_tokenpull = {
2430
- input: delta(p.input, baseP.input),
2431
- output: delta(p.output, baseP.output),
2432
- cacheCreate: delta(p.cacheCreate, baseP.cacheCreate),
2433
- cacheRead: delta(p.cacheRead, baseP.cacheRead),
2434
- };
2435
- }
2436
- return entry;
2437
- });
2438
- }
2439
-
2440
- // Sources available summary
2441
- const available = SOURCES.filter(
2442
- (s) => Object.keys(s.byWin).length > 0,
2443
- ).map((s) => s.source);
2444
-
2445
- return {
2446
- platform,
2447
- estimated: tpResult?.estimated ?? false,
2448
- generatedAt: tpResult?.generatedAt ?? new Date().toISOString(),
2449
- sources_available: available,
2450
- sources_missing: SOURCES.map((s) => s.source).filter(
2451
- (s) => !available.includes(s),
2452
- ),
2453
- comparison,
2454
- };
2455
- }
2456
-
2457
- if (name === "simulate_change") {
2458
- // The first prescriptive tool — "what if I changed my token mix?"
2459
- // Pure local math: parse current pillars, apply proposed changes, run the
2460
- // cascade on both, return the delta. No network, no submission.
2461
- if (!args?.text)
2462
- throw new Error(
2463
- "simulate_change requires a non-empty `text` argument (current pillars).",
2464
- );
2465
- if (typeof args.text === "string" && args.text.length > MAX_INPUT) {
2466
- return {
2467
- status: "error",
2468
- reason: "input_too_large",
2469
- detail: `text exceeds ${MAX_INPUT} chars.`,
2470
- };
2471
- }
2472
- if (!args?.changes || typeof args.changes !== "object") {
2473
- throw new Error(
2474
- "simulate_change requires a `changes` object with at least one pillar change.",
2475
- );
2476
- }
2477
-
2478
- const currentPillars = parsePillars(args.text);
2479
- const current = withParseWarnings(currentPillars, cascade(currentPillars));
2480
-
2481
- // Apply changes: each pillar is either an absolute number (replace) or a
2482
- // string starting with +/- (relative delta). Omitted pillars are unchanged.
2483
- const PILLAR_KEYS = ["input", "output", "cacheCreate", "cacheRead"];
2484
- const simulated = { ...currentPillars };
2485
- const appliedChanges = {};
2486
-
2487
- for (const key of PILLAR_KEYS) {
2488
- if (args.changes[key] == null) continue;
2489
- const raw = args.changes[key];
2490
- let newVal;
2491
-
2492
- if (typeof raw === "number") {
2493
- newVal = raw;
2494
- } else if (typeof raw === "string") {
2495
- const trimmed = raw.trim();
2496
- if (trimmed.startsWith("+") || trimmed.startsWith("-")) {
2497
- // Relative delta: add/subtract from current value
2498
- const delta = Number(trimmed);
2499
- if (!Number.isFinite(delta)) {
2500
- return {
2501
- status: "error",
2502
- reason: "invalid_change",
2503
- detail: `changes.${key}: "${raw}" is not a valid relative delta.`,
2504
- };
2505
- }
2506
- newVal = currentPillars[key] + delta;
2507
- } else {
2508
- // Absolute value as a string
2509
- newVal = Number(trimmed);
2510
- }
2511
- } else {
2512
- return {
2513
- status: "error",
2514
- reason: "invalid_change",
2515
- detail: `changes.${key}: expected number or string, got ${typeof raw}.`,
2516
- };
2517
- }
2518
-
2519
- if (!Number.isFinite(newVal)) {
2520
- return {
2521
- status: "error",
2522
- reason: "invalid_change",
2523
- detail: `changes.${key}: result is not a finite number.`,
2524
- };
2525
- }
2526
- // Clamp to non-negative — token counts can't be negative
2527
- if (newVal < 0) {
2528
- return {
2529
- status: "error",
2530
- reason: "invalid_change",
2531
- detail: `changes.${key}: result ${newVal} is negative — token counts must be >= 0.`,
2532
- };
2533
- }
2534
-
2535
- simulated[key] = newVal;
2536
- appliedChanges[key] = {
2537
- from: currentPillars[key],
2538
- to: newVal,
2539
- delta: newVal - currentPillars[key],
2540
- };
2541
- }
2542
-
2543
- if (Object.keys(appliedChanges).length === 0) {
2544
- return {
2545
- status: "error",
2546
- reason: "no_changes",
2547
- detail: "No pillar changes specified in the `changes` object.",
2548
- };
2549
- }
2550
-
2551
- const simulatedResult = cascade(simulated);
2552
-
2553
- // Compute deltas for every cascade metric
2554
- const metricDelta = (curr, sim) => {
2555
- if (curr == null && sim == null) return null;
2556
- if (curr == null) return { from: null, to: sim, delta: null };
2557
- if (sim == null) return { from: curr, to: null, delta: null };
2558
- return { from: curr, to: sim, delta: Number((sim - curr).toFixed(4)) };
2559
- };
2560
-
2561
- const classChanged = current.class !== simulatedResult.class;
2562
-
2563
- return {
2564
- current: {
2565
- pillars: currentPillars,
2566
- yield: current.yield,
2567
- snr: current.snr,
2568
- leverage: current.leverage,
2569
- velocity: current.velocity,
2570
- dev10x: current.dev10x,
2571
- class: current.class,
2572
- },
2573
- simulated: {
2574
- pillars: {
2575
- input: simulated.input,
2576
- output: simulated.output,
2577
- cacheCreate: simulated.cacheCreate,
2578
- cacheRead: simulated.cacheRead,
2579
- },
2580
- yield: simulatedResult.yield,
2581
- snr: simulatedResult.snr,
2582
- leverage: simulatedResult.leverage,
2583
- velocity: simulatedResult.velocity,
2584
- dev10x: simulatedResult.dev10x,
2585
- class: simulatedResult.class,
2586
- },
2587
- changes: appliedChanges,
2588
- deltas: {
2589
- yield: metricDelta(current.yield, simulatedResult.yield),
2590
- snr: metricDelta(current.snr, simulatedResult.snr),
2591
- leverage: metricDelta(current.leverage, simulatedResult.leverage),
2592
- velocity: metricDelta(current.velocity, simulatedResult.velocity),
2593
- dev10x: metricDelta(current.dev10x, simulatedResult.dev10x),
2594
- },
2595
- class_changed: classChanged,
2596
- ...(classChanged
2597
- ? { class_transition: `${current.class} → ${simulatedResult.class}` }
2598
- : {}),
2599
- ...(simulatedResult.warnings
2600
- ? { simulated_warnings: simulatedResult.warnings }
2601
- : {}),
2602
- note: "Local simulation only — no submission. The actual score depends on server-side RS.xx weights and class thresholds.",
2603
- };
2604
- }
2605
-
2606
- if (name === "diagnose_cascade") {
2607
- if (!args?.text)
2608
- throw new Error(
2609
- "diagnose_cascade requires a non-empty `text` argument (token pillars).",
2610
- );
2611
- if (typeof args.text === "string" && args.text.length > MAX_INPUT) {
2612
- return {
2613
- status: "error",
2614
- reason: "input_too_large",
2615
- detail: `text exceeds ${MAX_INPUT} chars.`,
2616
- };
2617
- }
2618
-
2619
- const p = parsePillars(args.text);
2620
- const result = withParseWarnings(p, cascade(p));
2621
- const { input: i, output: o, cacheCreate: cw, cacheRead: cr } = p;
2622
- const diagnosis = [];
2623
-
2624
- // Cache leverage check — the #1 cascade efficiency signal
2625
- const leverage = result.leverage;
2626
- if (leverage !== null) {
2627
- if (leverage < 10) {
2628
- diagnosis.push({
2629
- metric: "cache_leverage",
2630
- severity: "critical",
2631
- finding: `Cache leverage is ${leverage}× — you're reading only ${leverage}× your fresh input from cache. TRANSMITTER-class operators hit 200×+.`,
2632
- recommendation:
2633
- "Increase context reuse: load prior session context, use longer conversation threads, reference earlier outputs.",
2634
- estimated_yield_impact: `+${Math.round((1 - leverage / 50) * 100)}% Υ potential`,
2635
- });
2636
- } else if (leverage < 50) {
2637
- diagnosis.push({
2638
- metric: "cache_leverage",
2639
- severity: "warning",
2640
- finding: `Cache leverage is ${leverage}× — decent but below the ARCH+ threshold (~100×+).`,
2641
- recommendation:
2642
- "Push cache reads higher by reusing prior context more aggressively.",
2643
- estimated_yield_impact: `+${Math.round((1 - leverage / 100) * 50)}% Υ potential`,
2644
- });
2645
- }
2646
- }
2647
-
2648
- // Velocity check — output per input
2649
- const velocity = result.velocity;
2650
- if (velocity !== null) {
2651
- if (velocity < 0.5) {
2652
- diagnosis.push({
2653
- metric: "velocity",
2654
- severity: "critical",
2655
- finding: `Velocity is ${velocity} — generating only ${velocity}× your input as output. You're reading more than you produce.`,
2656
- recommendation:
2657
- "Increase output: ask the agent to generate more code/text per turn, reduce over-reading.",
2658
- estimated_yield_impact: `+${Math.round((0.5 - velocity) * 100)}% Υ per 0.1 velocity gain`,
2659
- });
2660
- } else if (velocity < 1.0) {
2661
- diagnosis.push({
2662
- metric: "velocity",
2663
- severity: "warning",
2664
- finding: `Velocity is ${velocity} — below 1.0 (output < input). Healthy operators hit 1.5×+.`,
2665
- recommendation:
2666
- "Generate more output per input token — larger edits, more complete responses.",
2667
- estimated_yield_impact: `+${Math.round((1 - velocity) * 30)}% Υ potential`,
2668
- });
2669
- }
2670
- }
2671
-
2672
- // SNR check — signal-to-noise
2673
- const snr = result.snr;
2674
- if (snr !== null && snr < 0.3) {
2675
- diagnosis.push({
2676
- metric: "snr",
2677
- severity: "warning",
2678
- finding: `SNR is ${snr} — less than 30% of your token flow is output. Input is dominating.`,
2679
- recommendation:
2680
- "Reduce fresh input (reuse context) or increase output generation.",
2681
- estimated_yield_impact:
2682
- "Indirect — improves both velocity and leverage",
2683
- });
2684
- }
2685
-
2686
- // Cache creation ratio — are you over-committing?
2687
- if (cw > 0 && o > 0) {
2688
- const commitRatio = cw / o;
2689
- if (commitRatio > 20) {
2690
- diagnosis.push({
2691
- metric: "cache_creation",
2692
- severity: "info",
2693
- 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.`,
2694
- recommendation:
2695
- "Ensure you're rereading committed context. If leverage is low, you're writing cache you never read.",
2696
- estimated_yield_impact: "Cost reduction, not Υ directly",
2697
- });
2698
- }
2699
- }
2700
-
2701
- // Input bloat — is fresh input too high relative to total?
2702
- const total = i + o + cw + cr;
2703
- if (total > 0) {
2704
- const inputPct = (i / total) * 100;
2705
- if (inputPct > 10) {
2706
- diagnosis.push({
2707
- metric: "input_bloat",
2708
- severity: "warning",
2709
- finding: `Fresh input is ${inputPct.toFixed(1)}% of your total token flow — high. Efficient operators keep input under 1% by leaning on cache.`,
2710
- recommendation:
2711
- "Reduce fresh input by reusing prior context instead of re-pasting it.",
2712
- estimated_yield_impact: `+${Math.round((inputPct - 1) * 5)}% Υ potential`,
2713
- });
2714
- }
2715
- }
2716
-
2717
- // 10xDEV check — is the full cascade compounding?
2718
- if (result.dev10x === null && cw === 0) {
2719
- diagnosis.push({
2720
- metric: "10xdev",
2721
- severity: "critical",
2722
- finding:
2723
- "No cache creation — the cascade cannot compound. You're operating in a non-compounding mode (like ChatGPT without prompt caching).",
2724
- recommendation:
2725
- "Switch to a platform with prompt caching (Claude Code) or enable caching if available.",
2726
- estimated_yield_impact: "Enables the full cascade — potentially 10×+ Υ",
2727
- });
2728
- } else if (result.dev10x !== null && result.dev10x < 1.0) {
2729
- diagnosis.push({
2730
- metric: "10xdev",
2731
- severity: "info",
2732
- finding: `10xDEV is ${result.dev10x} — below 1.0 (BASE threshold). The cascade is compounding but not strongly.`,
2733
- recommendation:
2734
- "Improve both leverage AND velocity — 10xDEV = log10(transmission × commitment × reuse).",
2735
- estimated_yield_impact: "Class tier improvement",
2736
- });
2737
- }
2738
-
2739
- // Sort by severity (critical > warning > info)
2740
- const sevOrder = { critical: 0, warning: 1, info: 2 };
2741
- diagnosis.sort((a, b) => sevOrder[a.severity] - sevOrder[b.severity]);
2742
-
2743
- const healthScore = diagnosis.filter(
2744
- (d) => d.severity === "critical",
2745
- ).length;
2746
- const summary =
2747
- healthScore === 0
2748
- ? `Cascade is healthy — Υ ${result.yield}, class ${result.class}. ${diagnosis.length} minor optimizations available.`
2749
- : `Cascade has ${healthScore} critical leak${healthScore > 1 ? "s" : ""} — Υ ${result.yield}, class ${result.class}. Fix the critical items first.`;
2750
-
2751
- return {
2752
- pillars: p,
2753
- cascade: {
2754
- yield_: result.yield,
2755
- snr: result.snr,
2756
- leverage: result.leverage,
2757
- velocity: result.velocity,
2758
- tenx_dev: result.dev10x,
2759
- class: result.class,
2760
- warnings: result.warnings,
2761
- },
2762
- diagnosis,
2763
- summary,
2764
- };
2765
- }
2766
-
2767
- if (name === "suggest_improvements") {
2768
- if (!args?.text)
2769
- throw new Error(
2770
- "suggest_improvements requires a non-empty `text` argument (token pillars).",
2771
- );
2772
- if (typeof args.text === "string" && args.text.length > MAX_INPUT) {
2773
- return {
2774
- status: "error",
2775
- reason: "input_too_large",
2776
- detail: `text exceeds ${MAX_INPUT} chars.`,
2777
- };
2778
- }
2779
-
2780
- const p = parsePillars(args.text);
2781
- const current = withParseWarnings(p, cascade(p));
2782
- const { input: i, output: o, cacheCreate: cw, cacheRead: cr } = p;
2783
-
2784
- // Generate candidate improvements, simulate each, rank by Υ impact
2785
- const candidates = [];
2786
-
2787
- // Strategy 1: Increase cache reads (the #1 lever)
2788
- const crBoosts = cr > 0 ? [1.5, 2, 3, 5] : [];
2789
- for (const mult of crBoosts) {
2790
- const sim = cascade({ ...p, cacheRead: Math.round(cr * mult) });
2791
- if (sim.yield !== null) {
2792
- candidates.push({
2793
- action: `Increase cache reads by ${Math.round((mult - 1) * 100)}%`,
2794
- pillar: "cacheRead",
2795
- delta: `+${Math.round(cr * (mult - 1)).toLocaleString()}`,
2796
- simulated_yield: sim.yield,
2797
- yield_delta: Number((sim.yield - (current.yield ?? 0)).toFixed(2)),
2798
- class_after: sim.class,
2799
- rationale:
2800
- "Cache reads are the strongest Υ multiplier. More reuse = higher leverage = higher yield.",
2801
- });
2802
- }
2803
- }
2804
- // For zero-cache operators, suggest enabling caching with a starter amount
2805
- if (cr === 0 && cw === 0) {
2806
- const starterAmounts = [
2807
- Math.round(i * 10),
2808
- Math.round(i * 50),
2809
- Math.round(i * 100),
2810
- ];
2811
- for (const amt of starterAmounts) {
2812
- const sim = cascade({
2813
- ...p,
2814
- cacheCreate: Math.round(amt * 0.5),
2815
- cacheRead: amt,
2816
- });
2817
- if (sim.yield !== null && sim.yield > 0) {
2818
- candidates.push({
2819
- action: `Enable caching with ${amt.toLocaleString()} cache reads`,
2820
- pillar: "cacheRead",
2821
- delta: `+${amt.toLocaleString()}`,
2822
- simulated_yield: sim.yield,
2823
- yield_delta: Number((sim.yield - (current.yield ?? 0)).toFixed(2)),
2824
- class_after: sim.class,
2825
- rationale:
2826
- "You have no cache — enabling it unlocks the cascade. Start by reusing prior context.",
2827
- });
2828
- }
2829
- }
2830
- }
2831
-
2832
- // Strategy 2: Reduce fresh input (Υ = Cr·O/I² — input is squared in the denominator)
2833
- const inputReductions = [0.9, 0.75, 0.5];
2834
- for (const mult of inputReductions) {
2835
- const newInput = Math.round(i * mult);
2836
- if (newInput < 1) continue;
2837
- const sim = cascade({ ...p, input: newInput });
2838
- if (sim.yield !== null) {
2839
- candidates.push({
2840
- action: `Reduce fresh input by ${Math.round((1 - mult) * 100)}%`,
2841
- pillar: "input",
2842
- delta: `-${Math.round(i * (1 - mult)).toLocaleString()}`,
2843
- simulated_yield: sim.yield,
2844
- yield_delta: Number((sim.yield - (current.yield ?? 0)).toFixed(2)),
2845
- class_after: sim.class,
2846
- rationale:
2847
- "Input is squared in the Υ denominator (Υ = Cr·O/I²). Reducing input has a quadratic payoff.",
2848
- });
2849
- }
2850
- }
2851
-
2852
- // Strategy 3: Increase output
2853
- const outputBoosts = [1.25, 1.5, 2];
2854
- for (const mult of outputBoosts) {
2855
- const sim = cascade({ ...p, output: Math.round(o * mult) });
2856
- if (sim.yield !== null) {
2857
- candidates.push({
2858
- action: `Increase output by ${Math.round((mult - 1) * 100)}%`,
2859
- pillar: "output",
2860
- delta: `+${Math.round(o * (mult - 1)).toLocaleString()}`,
2861
- simulated_yield: sim.yield,
2862
- yield_delta: Number((sim.yield - (current.yield ?? 0)).toFixed(2)),
2863
- class_after: sim.class,
2864
- rationale:
2865
- "Output is a linear multiplier in Υ. More output per session = higher yield.",
2866
- });
2867
- }
2868
- }
2869
-
2870
- // Strategy 4: Optimize cache creation (reduce if over-committing)
2871
- if (cw > o * 10) {
2872
- const sim = cascade({ ...p, cacheCreate: Math.round(o * 5) });
2873
- if (sim.yield !== null) {
2874
- candidates.push({
2875
- action: "Reduce cache creation to 5× output",
2876
- pillar: "cacheCreate",
2877
- delta: `-${Math.round(cw - o * 5).toLocaleString()}`,
2878
- simulated_yield: sim.yield,
2879
- yield_delta: Number((sim.yield - (current.yield ?? 0)).toFixed(2)),
2880
- class_after: sim.class,
2881
- rationale:
2882
- "You're over-committing cache (cw >> output). Trimming to a healthy ratio reduces cost without hurting yield.",
2883
- });
2884
- }
2885
- }
2886
-
2887
- // Sort by yield_delta descending, take top 8
2888
- candidates.sort((a, b) => b.yield_delta - a.yield_delta);
2889
- const top = candidates
2890
- .slice(0, 8)
2891
- .map((c, idx) => ({ rank: idx + 1, ...c }));
2892
-
2893
- const best = top[0];
2894
- return {
2895
- suggestions: top,
2896
- current_yield: current.yield,
2897
- current_class: current.class,
2898
- best_single_change: best
2899
- ? `${best.action} (Υ ${current.yield} → ${best.simulated_yield}, +${best.yield_delta} yield, class ${best.class_after})`
2900
- : "No improvements found — your cascade is already optimized.",
2901
- };
2902
- }
2903
-
2904
- if (name === "self_improve") {
2905
- // The full self-improvement cycle: pull → diagnose → suggest → simulate.
2906
- // If text is provided, use it as pillars. If not, run tokenpull first.
2907
- let pillars;
2908
- let pulledFrom = "provided";
2909
-
2910
- if (args?.text) {
2911
- if (typeof args.text === "string" && args.text.length > MAX_INPUT) {
2912
- return {
2913
- status: "error",
2914
- reason: "input_too_large",
2915
- detail: `text exceeds ${MAX_INPUT} chars.`,
2916
- };
2917
- }
2918
- pillars = parsePillars(args.text);
2919
- } else {
2920
- // Run tokenpull to get current pillars from local logs
2921
- const windowType = args?.window || "30d";
2922
- try {
2923
- const pullResult = await callTool(
2924
- "tokenpull",
2925
- { window: windowType },
2926
- opts,
2927
- );
2928
- if (pullResult?.status === "error" || !pullResult?.windows?.length) {
2929
- return {
2930
- status: "error",
2931
- reason: "tokenpull_failed",
2932
- detail:
2933
- "Could not pull pillars from local logs. Provide pillars via `text` argument instead.",
2934
- pull_result: pullResult,
2935
- };
2936
- }
2937
- // Use the first window's pillars
2938
- const w = pullResult.windows[0];
2939
- pillars = w.pillars;
2940
- pulledFrom = `tokenpull ${w.window || windowType}`;
2941
- } catch (e) {
2942
- return {
2943
- status: "error",
2944
- reason: "tokenpull_error",
2945
- detail: String(e.message || e),
2946
- hint: "Provide pillars via `text` argument instead.",
2947
- };
2948
- }
2949
- }
2950
-
2951
- const currentResult = withParseWarnings(pillars, cascade(pillars));
2952
- const { input: i, output: o, cacheCreate: cw, cacheRead: cr } = pillars;
2953
-
2954
- // ── Step 1: Diagnose ──────────────────────────────────────────────
2955
- const diagnosis = [];
2956
- const leverage = currentResult.leverage;
2957
- if (leverage !== null && leverage < 10) {
2958
- diagnosis.push({
2959
- metric: "cache_leverage",
2960
- severity: "critical",
2961
- finding: `Cache leverage is ${leverage}× — TRANSMITTER-class operators hit 200×+.`,
2962
- recommendation:
2963
- "Increase context reuse: load prior session context, use longer threads.",
2964
- });
2965
- } else if (leverage !== null && leverage < 50) {
2966
- diagnosis.push({
2967
- metric: "cache_leverage",
2968
- severity: "warning",
2969
- finding: `Cache leverage is ${leverage}× — below ARCH+ threshold (~100×+).`,
2970
- recommendation: "Push cache reads higher by reusing prior context.",
2971
- });
2972
- }
2973
-
2974
- const velocity = currentResult.velocity;
2975
- if (velocity !== null && velocity < 0.5) {
2976
- diagnosis.push({
2977
- metric: "velocity",
2978
- severity: "critical",
2979
- finding: `Velocity is ${velocity} — generating only ${velocity}× input as output.`,
2980
- recommendation: "Increase output per turn, reduce over-reading.",
2981
- });
2982
- } else if (velocity !== null && velocity < 1.0) {
2983
- diagnosis.push({
2984
- metric: "velocity",
2985
- severity: "warning",
2986
- finding: `Velocity is ${velocity} — below 1.0. Healthy operators hit 1.5×+.`,
2987
- recommendation: "Generate more output per input token.",
2988
- });
2989
- }
2990
-
2991
- if (currentResult.dev10x === null && cw === 0) {
2992
- diagnosis.push({
2993
- metric: "10xdev",
2994
- severity: "critical",
2995
- finding: "No cache creation — cascade cannot compound.",
2996
- recommendation:
2997
- "Switch to a platform with prompt caching (Claude Code).",
2998
- });
2999
- }
3000
-
3001
- const total = i + o + cw + cr;
3002
- if (total > 0 && (i / total) * 100 > 10) {
3003
- diagnosis.push({
3004
- metric: "input_bloat",
3005
- severity: "warning",
3006
- finding: `Fresh input is ${((i / total) * 100).toFixed(1)}% of total flow — efficient operators keep it under 1%.`,
3007
- recommendation: "Reduce fresh input by reusing prior context.",
3008
- });
3009
- }
3010
-
3011
- // ── Step 2: Suggest (generate + simulate candidates) ──────────────
3012
- const candidates = [];
3013
- // If cacheRead is 0, suggest absolute amounts instead of percentage boosts
3014
- const crBoosts = cr > 0 ? [1.5, 2, 3, 5] : [];
3015
- for (const mult of crBoosts) {
3016
- const sim = cascade({ ...pillars, cacheRead: Math.round(cr * mult) });
3017
- if (sim.yield !== null)
3018
- candidates.push({
3019
- action: `Increase cache reads by ${Math.round((mult - 1) * 100)}%`,
3020
- pillar: "cacheRead",
3021
- delta: `+${Math.round(cr * (mult - 1)).toLocaleString()}`,
3022
- simulated_yield: sim.yield,
3023
- yield_delta: Number(
3024
- (sim.yield - (currentResult.yield ?? 0)).toFixed(2),
3025
- ),
3026
- class_after: sim.class,
3027
- rationale: "Cache reads are the strongest Υ multiplier.",
3028
- });
3029
- }
3030
- // For zero-cache operators, suggest enabling caching with a starter amount
3031
- if (cr === 0 && cw === 0) {
3032
- const starterAmounts = [
3033
- Math.round(i * 10),
3034
- Math.round(i * 50),
3035
- Math.round(i * 100),
3036
- ];
3037
- for (const amt of starterAmounts) {
3038
- const sim = cascade({
3039
- ...pillars,
3040
- cacheCreate: Math.round(amt * 0.5),
3041
- cacheRead: amt,
3042
- });
3043
- if (sim.yield !== null && sim.yield > 0)
3044
- candidates.push({
3045
- action: `Enable caching with ${amt.toLocaleString()} cache reads`,
3046
- pillar: "cacheRead",
3047
- delta: `+${amt.toLocaleString()}`,
3048
- simulated_yield: sim.yield,
3049
- yield_delta: Number(
3050
- (sim.yield - (currentResult.yield ?? 0)).toFixed(2),
3051
- ),
3052
- class_after: sim.class,
3053
- rationale:
3054
- "You have no cache — enabling it unlocks the cascade. Start by reusing prior context.",
3055
- });
3056
- }
3057
- }
3058
- for (const mult of [0.9, 0.75, 0.5]) {
3059
- const newInput = Math.round(i * mult);
3060
- if (newInput < 1) continue;
3061
- const sim = cascade({ ...pillars, input: newInput });
3062
- if (sim.yield !== null)
3063
- candidates.push({
3064
- action: `Reduce fresh input by ${Math.round((1 - mult) * 100)}%`,
3065
- pillar: "input",
3066
- delta: `-${Math.round(i * (1 - mult)).toLocaleString()}`,
3067
- simulated_yield: sim.yield,
3068
- yield_delta: Number(
3069
- (sim.yield - (currentResult.yield ?? 0)).toFixed(2),
3070
- ),
3071
- class_after: sim.class,
3072
- rationale: "Input is squared in Υ denominator — quadratic payoff.",
3073
- });
3074
- }
3075
- for (const mult of [1.25, 1.5, 2]) {
3076
- const sim = cascade({ ...pillars, output: Math.round(o * mult) });
3077
- if (sim.yield !== null)
3078
- candidates.push({
3079
- action: `Increase output by ${Math.round((mult - 1) * 100)}%`,
3080
- pillar: "output",
3081
- delta: `+${Math.round(o * (mult - 1)).toLocaleString()}`,
3082
- simulated_yield: sim.yield,
3083
- yield_delta: Number(
3084
- (sim.yield - (currentResult.yield ?? 0)).toFixed(2),
3085
- ),
3086
- class_after: sim.class,
3087
- rationale: "Output is a linear multiplier in Υ.",
3088
- });
3089
- }
3090
- candidates.sort((a, b) => b.yield_delta - a.yield_delta);
3091
- const suggestions = candidates
3092
- .slice(0, 8)
3093
- .map((c, idx) => ({ rank: idx + 1, ...c }));
3094
-
3095
- // ── Step 3: Simulate the top suggestion ───────────────────────────
3096
- const best = suggestions[0];
3097
- let bestSimulation = null;
3098
- if (best) {
3099
- const simPillars = { ...pillars };
3100
- const delta = parseInt(best.delta.replace(/[+\-,]/g, ""), 10);
3101
- if (best.pillar === "cacheRead") simPillars.cacheRead = cr + delta;
3102
- else if (best.pillar === "input")
3103
- simPillars.input = Math.max(i - delta, 1);
3104
- else if (best.pillar === "output") simPillars.output = o + delta;
3105
- const simResult = cascade(simPillars);
3106
- bestSimulation = {
3107
- action: best.action,
3108
- current_yield: currentResult.yield,
3109
- simulated_yield: simResult.yield,
3110
- yield_delta: best.yield_delta,
3111
- current_class: currentResult.class,
3112
- simulated_class: simResult.class,
3113
- class_changed: currentResult.class !== simResult.class,
3114
- };
3115
- }
3116
-
3117
- // ── Step 4: Cycle summary ─────────────────────────────────────────
3118
- const criticalCount = diagnosis.filter(
3119
- (d) => d.severity === "critical",
3120
- ).length;
3121
- const cycleSummary = best
3122
- ? `Pulled from ${pulledFrom}. Υ ${currentResult.yield} (${currentResult.class}). ${criticalCount} critical, ${diagnosis.length - criticalCount} other findings. Best: ${best.action} → Υ ${best.simulated_yield} (+${best.yield_delta}).`
3123
- : `Pulled from ${pulledFrom}. Υ ${currentResult.yield} (${currentResult.class}). ${diagnosis.length} findings. No improvements suggested — cascade is optimized.`;
3124
-
3125
- // ── Step 5: Scope-specific analysis ───────────────────────────────
3126
- const scope = args?.scope || "daily";
3127
- const modeInfo = currentResult.mode; // { mode, confidence }
3128
- const scopeResult = {};
3129
-
3130
- if (scope === "daily") {
3131
- // Daily: mode + quality score + assessment + advice
3132
- const qs = qualityScore(currentResult.yield ?? 0, modeInfo.mode);
3133
- const expected = MODE_EXPECTED_YIELD[modeInfo.mode] ?? 0;
3134
- const assessment = _dailyAssessment(
3135
- modeInfo.mode,
3136
- currentResult.yield,
3137
- qs,
3138
- expected,
3139
- );
3140
- const advice = _dailyAdvice(modeInfo.mode, currentResult.yield, qs);
3141
- scopeResult.mode = modeInfo;
3142
- scopeResult.quality_score = Math.round(qs * 100) / 100;
3143
- scopeResult.assessment = assessment;
3144
- scopeResult.advice = advice;
3145
- }
3146
-
3147
- if (scope === "weekly" || scope === "trend") {
3148
- // Pull daily rows from ccusage to build weekly snapshots
3149
- const dailyRows = await _pullDailyRows(opts);
3150
- const weeklySnapshots = _compoundWeekly(dailyRows);
3151
- const modeDistribution = _modeDistribution(weeklySnapshots);
3152
- const modeWeightedYield = _modeWeightedYield(weeklySnapshots);
3153
-
3154
- // Compute badges
3155
- const historyForBadges = weeklySnapshots.map((w) => ({
3156
- date: w.weekStart,
3157
- mode: w.mode,
3158
- yield: w.yield,
3159
- pillars: w.pillars,
3160
- }));
3161
- const badges = computeBadges({
3162
- pillars,
3163
- cascade: currentResult,
3164
- history: historyForBadges,
3165
- isVerified: false, // set by submit_verified
3166
- rank: null, // server-side
3167
- });
3168
-
3169
- if (scope === "weekly") {
3170
- scopeResult.mode = modeInfo;
3171
- // Strip dailyModes from weekly_snapshots — privacy boundary (daily modes never leave the machine)
3172
- const safeSnapshots = weeklySnapshots.map(
3173
- ({ dailyModes, ...rest }) => rest,
3174
- );
3175
- scopeResult.report = {
3176
- current_mode: modeInfo.mode,
3177
- mode_confidence: modeInfo.confidence,
3178
- mode_distribution: modeDistribution,
3179
- mode_weighted_yield: modeWeightedYield,
3180
- peak_yield: _peakYield(weeklySnapshots),
3181
- health_score: _healthScore(weeklySnapshots, modeWeightedYield),
3182
- weekly_snapshots: safeSnapshots,
3183
- badges,
3184
- };
3185
- }
3186
-
3187
- if (scope === "trend") {
3188
- const yield7d = _yieldForDays(weeklySnapshots, 7);
3189
- const yield30d = _yieldForDays(weeklySnapshots, 30);
3190
- const yield90d = _yieldForDays(weeklySnapshots, 90);
3191
- const trajectory = _trajectory(yield7d, yield30d, yield90d);
3192
- scopeResult.mode = modeInfo;
3193
- scopeResult.trend = {
3194
- yield_7d: yield7d,
3195
- yield_30d: yield30d,
3196
- yield_90d: yield90d,
3197
- trajectory,
3198
- mode_distribution: modeDistribution,
3199
- phase_pattern: _phasePattern(weeklySnapshots),
3200
- };
3201
- }
3202
- }
3203
-
3204
- return {
3205
- pillars,
3206
- current_cascade: {
3207
- yield_: currentResult.yield,
3208
- snr: currentResult.snr,
3209
- leverage: currentResult.leverage,
3210
- velocity: currentResult.velocity,
3211
- tenx_dev: currentResult.dev10x,
3212
- class: currentResult.class,
3213
- mode: modeInfo,
3214
- warnings: currentResult.warnings,
3215
- },
3216
- diagnosis,
3217
- suggestions,
3218
- best_simulation: bestSimulation,
3219
- cycle_summary: cycleSummary,
3220
- ...scopeResult,
3221
- };
3222
- }
3223
-
3224
- // ── Intent-based tools (get_best_operator, compare_self, compare_operators) ──
3225
- // These wrap existing primitives with behavioral framing in power-user language.
3226
- // See artifacts/004_sigrank-mcp-intent-tools-spec.md for the intent taxonomy.
3227
-
3228
- if (name === "get_best_operator") {
3229
- const rawN = args?.n;
3230
- const n = Math.min(20, Math.max(1, rawN == null ? 5 : Number(rawN)));
3231
- const board = await fetchJson("/api/v1/leaderboard?metric=yield_");
3232
- const ops = (board.operators || board || []).slice(0, n);
3233
- const total = Array.isArray(board.operators || board)
3234
- ? (board.operators || board).length
3235
- : 0;
3236
-
3237
- const top = ops.map((op) => ({
3238
- ...op,
3239
- behavioral_framing: _behavioralFraming(op),
3240
- competitive: _competitiveLayer(op, board),
3241
- }));
3242
-
3243
- const best = top[0];
3244
- const summary = best
3245
- ? `${best.codename} tops the SigRank leaderboard at Υ ${best.yield_?.toLocaleString?.() || best.yield_} — ${_behavioralFraming(best)}`
3246
- : "No operators on the board yet.";
3247
-
3248
- return {
3249
- top_operators: top,
3250
- total_operators: total,
3251
- summary,
3252
- cta: "Check my rank",
3253
- shareable_url: best ? `${DEFAULT_API_BASE}/operator/${encodeURIComponent(best.codename)}` : null,
3254
- };
3255
- }
3256
-
3257
- if (name === "compare_self") {
3258
- const codename = String(args?.codename || "").trim();
3259
- const text = String(args?.text || "").trim();
3260
-
3261
- if (!codename && !text)
3262
- throw new Error(
3263
- "compare_self requires either `codename` (to fetch from the board) or `text` (raw token pillars to score locally).",
3264
- );
3265
-
3266
- let yourMetrics;
3267
- if (codename) {
3268
- yourMetrics = await fetchJson(
3269
- `/api/v1/operators/${encodeURIComponent(codename)}`,
3270
- );
3271
- } else {
3272
- if (text.length > MAX_INPUT) {
3273
- return {
3274
- error: "input_too_large",
3275
- detail: `text exceeds ${MAX_INPUT} chars.`,
3276
- };
3277
- }
3278
- const pillars = parsePillars(text);
3279
- const c = withParseWarnings(pillars, cascade(pillars));
3280
- yourMetrics = {
3281
- codename: "you (local)",
3282
- yield_: c.yield,
3283
- leverage: c.leverage,
3284
- velocity: c.velocity,
3285
- class: c.class,
3286
- rank: null,
3287
- };
3288
- }
3289
-
3290
- // Fetch board for comparison
3291
- const board = await fetchJson("/api/v1/leaderboard?metric=yield_");
3292
- const allOps = board.operators || board || [];
3293
- const yields = allOps.map((o) => o.yield_ || 0).sort((a, b) => a - b);
3294
- const avgYield = yields.length
3295
- ? yields.reduce((s, y) => s + y, 0) / yields.length
3296
- : 0;
3297
- const yourYield = yourMetrics.yield_ || 0;
3298
- const percentile = yields.length
3299
- ? Math.round(
3300
- (yields.filter((y) => y < yourYield).length / yields.length) * 100,
3301
- )
3302
- : 0;
3303
-
3304
- const klass = yourMetrics.class || "Burner";
3305
- const powerUserAssessment = _powerUserAssessment(klass, yourMetrics);
3306
- const classMeaning = _classMeaning(klass);
3307
-
3308
- const yieldVsAvg = yields.length
3309
- ? yourYield > avgYield
3310
- ? `${(yourYield / avgYield).toFixed(1)}× the board average`
3311
- : `${((yourYield / avgYield) * 100).toFixed(0)}% of the board average`
3312
- : "board has no other operators";
3313
-
3314
- const suggestion = _improvementSuggestion(klass, yourMetrics);
3315
-
3316
- // Competitive layer per SHARED_DESIGN_DECISIONS.md §3/§4/§5
3317
- const competitive = _competitiveLayer(yourMetrics, board);
3318
- const competitiveSummary = _competitiveSummary(yourMetrics, board);
3319
-
3320
- return {
3321
- your_metrics: yourMetrics,
3322
- power_user_assessment: powerUserAssessment,
3323
- comparison: {
3324
- your_yield_vs_avg: yieldVsAvg,
3325
- your_class_meaning: classMeaning,
3326
- percentile,
3327
- rank: competitive.rank,
3328
- total_operators: competitive.total_operators,
3329
- class_tier: competitive.class_tier,
3330
- delta_from_average: competitive.delta_from_average,
3331
- delta_from_top: competitive.delta_from_top,
3332
- },
3333
- competitive_summary: competitiveSummary,
3334
- shareable_url: competitive.shareable_url,
3335
- suggestion,
3336
- cta: "See where I stand",
3337
- };
3338
- }
3339
-
3340
- if (name === "compare_operators") {
3341
- const nameA = String(args?.codename_a || "").trim();
3342
- const nameB = String(args?.codename_b || "").trim();
3343
- if (!nameA || !nameB)
3344
- throw new Error(
3345
- "compare_operators requires both `codename_a` and `codename_b`.",
3346
- );
3347
-
3348
- const [opA, opB, board] = await Promise.all([
3349
- fetchJson(`/api/v1/operators/${encodeURIComponent(nameA)}`),
3350
- fetchJson(`/api/v1/operators/${encodeURIComponent(nameB)}`),
3351
- fetchJson("/api/v1/leaderboard?metric=yield_"),
3352
- ]);
3353
-
3354
- const yieldA = opA.yield_ || 0;
3355
- const yieldB = opB.yield_ || 0;
3356
- const delta = yieldA - yieldB;
3357
-
3358
- const winner = yieldA > yieldB ? opA : opB;
3359
- const loser = yieldA > yieldB ? opB : opA;
3360
- const verdict = `${winner.codename} is more token-efficient (${winner.yield_?.toLocaleString?.() || winner.yield_} vs ${loser.yield_?.toLocaleString?.() || loser.yield_} Υ). ${_behavioralFraming(winner)} ${loser.codename} ${_classMeaning(loser.class).toLowerCase()}`;
3361
-
3362
- return {
3363
- operator_a: {
3364
- codename: opA.codename,
3365
- yield_: opA.yield_,
3366
- leverage: opA.leverage,
3367
- velocity: opA.velocity,
3368
- class: opA.class,
3369
- rank: opA.rank,
3370
- competitive: _competitiveLayer(opA, board),
3371
- },
3372
- operator_b: {
3373
- codename: opB.codename,
3374
- yield_: opB.yield_,
3375
- leverage: opB.leverage,
3376
- velocity: opB.velocity,
3377
- class: opB.class,
3378
- rank: opB.rank,
3379
- competitive: _competitiveLayer(opB, board),
3380
- },
3381
- verdict,
3382
- yield_delta: delta,
3383
- cta: "Compare me to others",
3384
- };
3385
- }
3386
-
3387
- if (name === "describe_power_user") {
3388
- return {
3389
- description:
3390
- "An AI power user isn't someone who sends the most tokens — it's someone who compounds signal. " +
3391
- "Power users build workflows where cached context does the heavy lifting, fresh input stays lean, " +
3392
- "and output per session is high. SigRank quantifies this with the yield metric (Υ = cache_read × output / input²).",
3393
- metrics_explained: {
3394
- yield_: "Yield (Υ) measures how well you compound signal, not how much you burn. High yield = your cached context is doing work for you.",
3395
- leverage: "Leverage (Cr/I) measures how much you reuse prior work vs starting fresh. High leverage = you're building on cached results, not re-explaining everything.",
3396
- velocity: "Velocity (O/I) measures how much output you get per token spent. High velocity = you're productive, not just active.",
3397
- },
3398
- class_tiers: [
3399
- { class: "10xer", meaning: "AI power user archetype — disciplined, system-level reuse, high output per input. Leverage > 10×, high velocity." },
3400
- { class: "Builder", meaning: "Building momentum — moderate cache reuse, approaching power-user patterns. Growing leverage and velocity." },
3401
- { class: "Burner", meaning: "Early-stage — tokens burned more than compounded. Low leverage, low velocity. The shift: reuse prior context." },
3402
- ],
3403
- link: "https://signalaf.com/score — check your class tier and yield",
3404
- shareable_url: `${DEFAULT_API_BASE}/score`,
3405
- cta: "Learn the scoring",
3406
- };
3407
- }
3408
-
3409
- if (name === "optimize_efficiency") {
3410
- const codename = String(args?.codename || "").trim();
3411
- const text = String(args?.text || "").trim();
3412
-
3413
- if (!codename && !text)
3414
- throw new Error(
3415
- "optimize_efficiency requires either `codename` (to fetch from the board) or `text` (raw token pillars to score locally).",
3416
- );
3417
-
3418
- let metrics;
3419
- if (codename) {
3420
- metrics = await fetchJson(
3421
- `/api/v1/operators/${encodeURIComponent(codename)}`,
3422
- );
3423
- } else {
3424
- if (text.length > MAX_INPUT) {
3425
- return { error: "input_too_large", detail: `text exceeds ${MAX_INPUT} chars.` };
3426
- }
3427
- const pillars = parsePillars(text);
3428
- const c = withParseWarnings(pillars, cascade(pillars));
3429
- metrics = {
3430
- codename: "you (local)",
3431
- yield_: c.yield,
3432
- leverage: c.leverage,
3433
- velocity: c.velocity,
3434
- class: c.class,
3435
- };
3436
- }
3437
-
3438
- const klass = metrics.class || "Burner";
3439
- const l = metrics.leverage || 0;
3440
- const v = metrics.velocity || 0;
3441
- const y = metrics.yield_ || 0;
3442
-
3443
- // Build ranked suggestions based on current cascade shape
3444
- const suggestions = [];
3445
-
3446
- if (l < 5) {
3447
- suggestions.push({
3448
- action: "Increase cache reuse — reuse prompts, templates, and workflows instead of starting from scratch",
3449
- why: "Your leverage is " + l.toFixed(1) + "×, meaning most of your context is fresh input. Each reused cached token multiplies your yield because input² is in the denominator.",
3450
- power_user_practice: "Power users build template libraries and workflow patterns they invoke repeatedly, letting cached context accumulate.",
3451
- });
3452
- }
3453
- if (v < 1) {
3454
- suggestions.push({
3455
- action: "Increase output per session — produce more, don't just read",
3456
- why: "Your velocity is " + v.toFixed(2) + ", meaning you're consuming more input than producing output. Yield rewards output production.",
3457
- power_user_practice: "Power users maximize output per session — they ask AI to generate, transform, and produce, not just explain.",
3458
- });
3459
- }
3460
- if (l >= 5 && v >= 1 && klass !== "10xer") {
3461
- suggestions.push({
3462
- action: "Extend session length to compound cached context further",
3463
- why: "Your leverage (" + l.toFixed(1) + "×) and velocity (" + v.toFixed(2) + ") are solid. Longer sessions with consistent context will push your yield higher.",
3464
- power_user_practice: "Power users maintain long, context-rich sessions where the cache grows and compounds.",
3465
- });
3466
- }
3467
- if (suggestions.length === 0) {
3468
- suggestions.push({
3469
- action: "Maintain your cascade architecture — you're at the top tier",
3470
- why: "Your yield (" + y.toLocaleString() + "), leverage (" + l.toFixed(1) + "×), and velocity (" + v.toFixed(2) + ") are all strong. Keep doing what you're doing.",
3471
- power_user_practice: "Power users don't rest on their metrics — they experiment with new workflow patterns and measure the impact.",
3472
- });
3473
- }
3474
-
3475
- const summary = `Your Υ Yield is ${y.toLocaleString()} (${klass}). ${_improvementSuggestion(klass, metrics)}`;
3476
-
3477
- // Competitive layer per SHARED_DESIGN_DECISIONS.md §3/§4/§5
3478
- const board = await fetchJson("/api/v1/leaderboard?metric=yield_");
3479
- const competitive = _competitiveLayer(metrics, board);
3480
- const competitiveSummary = _competitiveSummary(metrics, board);
3481
-
3482
- return {
3483
- your_metrics: {
3484
- yield_: y,
3485
- leverage: l,
3486
- velocity: v,
3487
- class: klass,
3488
- },
3489
- competitive: {
3490
- rank: competitive.rank,
3491
- total_operators: competitive.total_operators,
3492
- percentile: competitive.percentile,
3493
- class_tier: competitive.class_tier,
3494
- delta_from_average: competitive.delta_from_average,
3495
- delta_from_top: competitive.delta_from_top,
3496
- },
3497
- competitive_summary: competitiveSummary,
3498
- shareable_url: competitive.shareable_url,
3499
- suggestions,
3500
- summary,
3501
- cta: "Improve my score",
3502
- };
3503
- }
3504
-
3505
- if (name === "tokscale_breakdown") {
3506
- const threshold = Number(args?.threshold ?? 0.01);
3507
- const platforms = await tokscaleModelBreakdown(threshold);
3508
- return { platforms };
3509
- }
3510
-
3511
- throw new Error(`Unknown tool: ${name}`);
3512
- }
3513
-
3514
- // ── Intent tool helpers — behavioral framing in power-user language ──────────
3515
-
3516
- /**
3517
- * Competitive layer for tool responses per SHARED_DESIGN_DECISIONS.md §3/§4/§5.
3518
- * Every tool response that includes operator data must show:
3519
- * rank, percentile, class_tier, delta_from_average, delta_from_top, shareable_url
3520
- * Response style: factual + competitive ("You rank #12 of 47 operators...")
3521
- */
3522
- function _competitiveLayer(op, board) {
3523
- const allOps = Array.isArray(board?.operators || board) ? (board.operators || board) : [];
3524
- const yields = allOps.map((o) => o.yield_ || 0).sort((a, b) => a - b);
3525
- const yourYield = op.yield_ || 0;
3526
- const total = allOps.length;
3527
-
3528
- // Rank: find operator's position (1-based)
3529
- let rank = op.rank || null;
3530
- if (!rank && total > 0) {
3531
- const sorted = [...allOps].sort((a, b) => (b.yield_ || 0) - (a.yield_ || 0));
3532
- const idx = sorted.findIndex((o) => o.codename === op.codename);
3533
- rank = idx >= 0 ? idx + 1 : null;
3534
- }
3535
-
3536
- // Percentile: % of operators with yield below this operator
3537
- const percentile = total > 0
3538
- ? Math.round((yields.filter((y) => y < yourYield).length / total) * 100)
3539
- : 0;
3540
-
3541
- // Delta from average
3542
- const avgYield = total > 0 ? yields.reduce((s, y) => s + y, 0) / total : 0;
3543
- const deltaFromAvg = avgYield > 0
3544
- ? { absolute: Math.round(yourYield - avgYield),
3545
- percent: Math.round(((yourYield - avgYield) / avgYield) * 100) }
3546
- : { absolute: 0, percent: 0 };
3547
-
3548
- // Delta from top operator
3549
- const topYield = total > 0 ? Math.max(...yields) : 0;
3550
- const deltaFromTop = topYield > 0
3551
- ? { absolute: Math.round(topYield - yourYield),
3552
- percent: Math.round(((topYield - yourYield) / topYield) * 100) }
3553
- : { absolute: 0, percent: 0 };
3554
-
3555
- // Shareable URL
3556
- const shareableUrl = op.codename && op.codename !== "you (local)"
3557
- ? `${DEFAULT_API_BASE}/operator/${encodeURIComponent(op.codename)}`
3558
- : null;
3559
-
3560
- return {
3561
- rank,
3562
- total_operators: total,
3563
- percentile,
3564
- class_tier: op.class || "Burner",
3565
- delta_from_average: deltaFromAvg,
3566
- delta_from_top: deltaFromTop,
3567
- shareable_url: shareableUrl,
3568
- };
3569
- }
3570
-
3571
- /** Factual + competitive summary line per SHARED_DESIGN_DECISIONS.md §4 */
3572
- function _competitiveSummary(op, board) {
3573
- const cl = _competitiveLayer(op, board);
3574
- const parts = [];
3575
-
3576
- if (cl.rank && cl.total_operators > 0) {
3577
- parts.push(`You rank #${cl.rank} of ${cl.total_operators} operators.`);
3578
- }
3579
-
3580
- const topOp = (board?.operators || board || []).reduce(
3581
- (best, o) => ((o.yield_ || 0) > (best?.yield_ || 0) ? o : best),
3582
- null,
3583
- );
3584
- if (topOp && topOp.codename) {
3585
- parts.push(`Top operator is ${topOp.codename} with Υ ${(topOp.yield_ || 0).toLocaleString()}.`);
3586
- }
3587
-
3588
- if (cl.delta_from_average.percent !== 0) {
3589
- const dir = cl.delta_from_average.percent > 0 ? "above" : "below";
3590
- parts.push(`You're ${Math.abs(cl.delta_from_average.percent)}% ${dir} average.`);
3591
- }
3592
-
3593
- if (cl.delta_from_top.percent > 0) {
3594
- parts.push(`${cl.delta_from_top.percent}% below top.`);
3595
- }
3596
-
3597
- return parts.join(" ");
3598
- }
3599
-
3600
- function _behavioralFraming(op) {
3601
- const y = op.yield_ || 0;
3602
- const l = op.leverage || 0;
3603
- const v = op.velocity || 0;
3604
- const klass = op.class || "Burner";
3605
-
3606
- if (klass === "10xer")
3607
- return `Disciplined, system-level reuse: ${l.toFixed(1)}× leverage means heavy cache reuse over fresh input, ${v.toFixed(2)} velocity means more output per token spent. This is the AI power-user archetype.`;
3608
- if (klass === "Builder")
3609
- return `Building cascade momentum: moderate cache reuse (${l.toFixed(1)}× leverage) with ${v.toFixed(2)} output velocity. Approaching power-user patterns — increase cache reuse to push into 10xer territory.`;
3610
- return `Early-stage cascade: ${v.toFixed(2)} output velocity with ${l.toFixed(1)}× leverage. Tokens are being burned more than compounded. Focus on reusing prior context (templates, prompts, workflows) to build leverage.`;
3611
- }
3612
-
3613
- function _powerUserAssessment(klass, metrics) {
3614
- const l = metrics.leverage || 0;
3615
- const v = metrics.velocity || 0;
3616
- if (klass === "10xer")
3617
- return `You are an AI power user. Your SigRank class (10xer) indicates you reuse prior work heavily (${l.toFixed(1)}× leverage), get more out of each token (${v.toFixed(2)} velocity), and keep input lean. This is consistent with AI power-user behavior: iterative, efficient, multi-use patterns.`;
3618
- if (klass === "Builder")
3619
- return `You are becoming an AI power user. Your Builder class shows growing cache reuse (${l.toFixed(1)}× leverage) and ${v.toFixed(2)} output velocity. You're building the habits — increase context reuse to push into 10xer territory.`;
3620
- return `You are not yet an AI power user. Your Burner class means tokens are being spent without compounding. The power-user shift: reuse prior context (prompts, templates, cached results) instead of starting fresh each time. Your leverage (${l.toFixed(1)}×) is the key metric to improve.`;
3621
- }
3622
-
3623
- function _classMeaning(klass) {
3624
- if (klass === "10xer")
3625
- return "AI power user archetype — disciplined, system-level reuse, high output per input.";
3626
- if (klass === "Builder")
3627
- return "Building momentum — moderate reuse, approaching power-user patterns.";
3628
- return "Early-stage — tokens burned more than compounded. Focus on cache reuse.";
3629
- }
3630
-
3631
- function _improvementSuggestion(klass, metrics) {
3632
- const l = metrics.leverage || 0;
3633
- const v = metrics.velocity || 0;
3634
- if (klass === "10xer")
3635
- return v < 1
3636
- ? "Your leverage is excellent but velocity is under 1.0 — you're reading more cache than producing output. Push for more output per session."
3637
- : "You're at the top tier. Maintain your cache architecture and experiment with longer sessions to compound yield further.";
3638
- if (klass === "Builder")
3639
- return l < 5
3640
- ? "Increase cache reuse: reuse prompts, templates, and workflows instead of starting from scratch. Each reused token multiplies your yield."
3641
- : "Your leverage is solid. Focus on output velocity — produce more per session to push your yield up.";
3642
- return "Start by reusing prior context. Instead of fresh prompts each time, build on cached results. Even a 2× increase in cache_read will dramatically improve your yield because input² is in the denominator.";
3643
- }
3644
-
3645
- // ── self_improve scope helpers ──────────────────────────────────────────────
3646
-
3647
- /** Pull daily rows from ccusage to build mode history. Returns array of { date, pillars, yield, mode }. */
3648
- async function _pullDailyRows(opts = {}) {
3649
- try {
3650
- const raw = await execFileAsync(
3651
- "ccusage",
3652
- ["claude", "daily", "--json"],
3653
- 15000,
3654
- );
3655
- const rows = JSON.parse(raw)?.daily ?? JSON.parse(raw);
3656
- if (!Array.isArray(rows)) return [];
3657
- return rows
3658
- .map((r) => {
3659
- const pillars = {
3660
- input: r.inputTokens ?? r.input_tokens ?? 0,
3661
- output: r.outputTokens ?? r.output_tokens ?? 0,
3662
- cacheCreate: r.cacheCreationTokens ?? r.cache_create_tokens ?? 0,
3663
- cacheRead: r.cacheReadTokens ?? r.cache_read_tokens ?? 0,
3664
- };
3665
- const cas = cascade(pillars);
3666
- const mode = detectMode(pillars);
3667
- return {
3668
- date: r.date ?? r.day ?? null,
3669
- pillars,
3670
- yield: cas.yield ?? 0,
3671
- mode: mode.mode,
3672
- mode_confidence: mode.confidence,
3673
- };
3674
- })
3675
- .filter((r) => r.date);
3676
- } catch {
3677
- return [];
3678
- }
3679
- }
3680
-
3681
- /** Compound daily rows into weekly snapshots. Each week = 7 days, starting Monday. */
3682
- function _compoundWeekly(dailyRows) {
3683
- if (!dailyRows || dailyRows.length === 0) return [];
3684
- // Group by ISO week (week starts Monday)
3685
- const weeks = new Map();
3686
- for (const row of dailyRows) {
3687
- const d = new Date(row.date);
3688
- const day = d.getDay() || 7; // Sunday=0 → 7
3689
- const monday = new Date(d);
3690
- monday.setDate(d.getDate() - day + 1);
3691
- const weekKey = monday.toISOString().slice(0, 10);
3692
- if (!weeks.has(weekKey)) {
3693
- weeks.set(weekKey, { weekStart: weekKey, days: [] });
3694
- }
3695
- weeks.get(weekKey).days.push(row);
3696
- }
3697
- // Compound each week
3698
- return Array.from(weeks.values())
3699
- .map((w) => {
3700
- const pillars = w.days.reduce(
3701
- (acc, d) => ({
3702
- input: acc.input + d.pillars.input,
3703
- output: acc.output + d.pillars.output,
3704
- cacheCreate: acc.cacheCreate + d.pillars.cacheCreate,
3705
- cacheRead: acc.cacheRead + d.pillars.cacheRead,
3706
- }),
3707
- { input: 0, output: 0, cacheCreate: 0, cacheRead: 0 },
3708
- );
3709
- const cas = cascade(pillars);
3710
- const mode = detectMode(pillars);
3711
- return {
3712
- weekStart: w.weekStart,
3713
- pillars,
3714
- yield: cas.yield ?? 0,
3715
- mode: mode.mode,
3716
- mode_confidence: mode.confidence,
3717
- dayCount: w.days.length,
3718
- dailyModes: w.days.map((d) => d.mode), // stays local — not in submitted report
3719
- };
3720
- })
3721
- .sort((a, b) => a.weekStart.localeCompare(b.weekStart));
3722
- }
3723
-
3724
- /** Compute mode distribution from weekly snapshots (weekly granularity = privacy boundary). */
3725
- function _modeDistribution(weeklySnapshots) {
3726
- if (!weeklySnapshots || weeklySnapshots.length === 0) return {};
3727
- const counts = { BUILD: 0, EDIT: 0, DEBUG: 0, MAINTAIN: 0, IDLE: 0 };
3728
- for (const w of weeklySnapshots) {
3729
- counts[w.mode] = (counts[w.mode] || 0) + 1;
3730
- }
3731
- const total = weeklySnapshots.length;
3732
- const dist = {};
3733
- for (const [mode, count] of Object.entries(counts)) {
3734
- if (count > 0) dist[mode] = Math.round((count / total) * 100) / 100;
3735
- }
3736
- return dist;
3737
- }
3738
-
3739
- /** Mode-weighted yield — average yield weighted by mode distribution. */
3740
- function _modeWeightedYield(weeklySnapshots) {
3741
- if (!weeklySnapshots || weeklySnapshots.length === 0) return 0;
3742
- const total = weeklySnapshots.length;
3743
- const sum = weeklySnapshots.reduce((acc, w) => acc + (w.yield || 0), 0);
3744
- return Math.round(sum / total);
3745
- }
3746
-
3747
- /** Peak yield across all weekly snapshots. */
3748
- function _peakYield(weeklySnapshots) {
3749
- if (!weeklySnapshots || weeklySnapshots.length === 0) return 0;
3750
- return Math.max(...weeklySnapshots.map((w) => w.yield || 0));
3751
- }
3752
-
3753
- /** Health score (0-100) — weighted composite of consistency, momentum, quality. */
3754
- function _healthScore(weeklySnapshots, modeWeightedYield) {
3755
- if (!weeklySnapshots || weeklySnapshots.length === 0) return 0;
3756
- // Simplified Phase 1 formula:
3757
- // 40% — consistency (how often in MAINTAIN)
3758
- // 30% — momentum (recent yield vs older yield)
3759
- // 30% — quality (mode-weighted yield relative to MAINTAIN expected)
3760
- const dist = _modeDistribution(weeklySnapshots);
3761
- const maintainShare = dist.MAINTAIN || 0;
3762
- const consistency = Math.min(maintainShare, 1) * 40;
3763
-
3764
- // Momentum: compare last 3 weeks to first 3 weeks
3765
- const recent = weeklySnapshots.slice(-3);
3766
- const older = weeklySnapshots.slice(0, 3);
3767
- const recentAvg =
3768
- recent.reduce((a, w) => a + (w.yield || 0), 0) / (recent.length || 1);
3769
- const olderAvg =
3770
- older.reduce((a, w) => a + (w.yield || 0), 0) / (older.length || 1);
3771
- const momentumRatio = olderAvg > 0 ? recentAvg / olderAvg : 1;
3772
- const momentum = (Math.min(momentumRatio, 2) / 2) * 30;
3773
-
3774
- // Quality: mode-weighted yield vs MAINTAIN expected (5000)
3775
- const quality = Math.min(modeWeightedYield / 5000, 1) * 30;
3776
-
3777
- return Math.round(consistency + momentum + quality);
3778
- }
3779
-
3780
- /** Yield for the last N days (from weekly snapshots — approximates by summing recent weeks). */
3781
- function _yieldForDays(weeklySnapshots, days) {
3782
- if (!weeklySnapshots || weeklySnapshots.length === 0) return 0;
3783
- const weeksNeeded = Math.ceil(days / 7);
3784
- const recent = weeklySnapshots.slice(-weeksNeeded);
3785
- if (recent.length === 0) return 0;
3786
- const sum = recent.reduce((a, w) => a + (w.yield || 0), 0);
3787
- return Math.round(sum / recent.length); // average weekly yield
3788
- }
3789
-
3790
- /** Trajectory description from 7d/30d/90d yields. */
3791
- function _trajectory(y7, y30, y90) {
3792
- if (y90 === 0) return "insufficient_data";
3793
- const r7v30 = y30 > 0 ? y7 / y30 : 1;
3794
- const r30v90 = y90 > 0 ? y30 / y90 : 1;
3795
- if (r7v30 > 1.2 && r30v90 > 1.0) return "accelerating";
3796
- if (r7v30 > 1.2) return "recent_surge";
3797
- if (r7v30 < 0.8 && r30v90 < 0.8) return "declining";
3798
- if (r7v30 < 0.8) return "recent_dip";
3799
- if (r30v90 > 1.1) return "steady_growth";
3800
- return "stable";
3801
- }
3802
-
3803
- /** Phase pattern description from mode distribution over time. */
3804
- function _phasePattern(weeklySnapshots) {
3805
- if (!weeklySnapshots || weeklySnapshots.length < 2)
3806
- return "insufficient_data";
3807
- const modes = weeklySnapshots.map((w) => w.mode);
3808
- const transitions = [];
3809
- for (let i = 1; i < modes.length; i++) {
3810
- if (modes[i] !== modes[i - 1]) {
3811
- transitions.push(`${modes[i - 1]}→${modes[i]}`);
3812
- }
3813
- }
3814
- if (transitions.length === 0) return `consistent_${modes[0].toLowerCase()}`;
3815
- // Check for smooth transitions (BUILD→MAINTAIN without DEBUG)
3816
- const smooth = transitions.every((t) => !t.includes("DEBUG"));
3817
- if (smooth && transitions.some((t) => t === "BUILD→MAINTAIN"))
3818
- return "smooth_ramp";
3819
- if (transitions.filter((t) => t.includes("DEBUG")).length > 2)
3820
- return "erratic";
3821
- return "cyclical";
3822
- }
3823
-
3824
- /** Daily assessment string. */
3825
- function _dailyAssessment(mode, yieldVal, qualityScoreVal, expectedYield) {
3826
- const qsPct = Math.round(qualityScoreVal * 100);
3827
- if (mode === "IDLE")
3828
- return "You're idle — no significant token activity today.";
3829
- if (mode === "MAINTAIN") {
3830
- if (qualityScoreVal >= 0.5)
3831
- return `You're in MAINTAIN mode. Yield ${yieldVal} is ${qsPct}% of MAINTAIN norm. The cascade is compounding.`;
3832
- return `You're in MAINTAIN mode but yield ${yieldVal} is only ${qsPct}% of expected (${expectedYield}). You may be coasting — push for more output.`;
3833
- }
3834
- if (mode === "BUILD") {
3835
- if (qualityScoreVal >= 0.5)
3836
- return `You're in BUILD mode. Yield ${yieldVal} is ${qsPct}% of BUILD norm. Greenfield work — expected to be low.`;
3837
- return `You're in BUILD mode. Yield ${yieldVal} is ${qsPct}% of expected (${expectedYield}). Building is slow — keep going.`;
3838
- }
3839
- if (mode === "EDIT") {
3840
- if (qualityScoreVal >= 0.5)
3841
- return `You're in EDIT mode. Yield ${yieldVal} is ${qsPct}% of EDIT norm. Fresh input but producing — good.`;
3842
- return `You're in EDIT mode. Yield ${yieldVal} is ${qsPct}% of expected (${expectedYield}). Push for more output per input.`;
3843
- }
3844
- if (mode === "DEBUG") {
3845
- if (qualityScoreVal >= 0.5)
3846
- return `You're in DEBUG mode. Yield ${yieldVal} is ${qsPct}% of DEBUG norm. Investigating — yield is expected to be low.`;
3847
- return `You're in DEBUG mode. Yield ${yieldVal} is ${qsPct}% of expected (${expectedYield}). Debugging is slow — consider loading prior context.`;
3848
- }
3849
- return `Mode: ${mode}. Yield: ${yieldVal}. Quality: ${qsPct}%.`;
3850
- }
3851
-
3852
- /** Daily advice string. */
3853
- function _dailyAdvice(mode, yieldVal, qs) {
3854
- if (mode === "MAINTAIN" && qs >= 0.5)
3855
- return "Keep the cascade going. Don't reset context — let it compound.";
3856
- if (mode === "MAINTAIN")
3857
- return "You're in MAINTAIN but underperforming. Push the agent for more output per turn.";
3858
- if (mode === "BUILD")
3859
- return "When you're done building, load prior context to transition to MAINTAIN. The cascade rewards reuse.";
3860
- if (mode === "EDIT")
3861
- return "You're producing but using fresh input. Try to reuse prior context to boost leverage.";
3862
- if (mode === "DEBUG")
3863
- return "When you're done debugging, load prior context to return to MAINTAIN. Don't let the debug phase drag on.";
3864
- if (mode === "IDLE")
3865
- return "No activity detected. Start a session to build your cascade.";
3866
- return "Keep working — the cascade will compound as you build context.";
3867
- }
3868
-
3869
- /**
3870
- * Compute the cascade report block for a submission payload.
3871
- * Pure math — mode detection + badges + health score from the current pillars.
3872
- * The server stores this as-is (does NOT recompute modes).
3873
- * Weekly granularity is the privacy boundary — no daily modes in the report.
3874
- */
3875
- function _computeReportBlock(pillars) {
3876
- const cas = cascade(pillars);
3877
- const modeInfo = detectMode(pillars);
3878
- const badges = computeBadges({
3879
- pillars,
3880
- cascade: cas,
3881
- history: [], // no history available at submit time — badges computed from current pillars only
3882
- isVerified: true, // submit_verified is the signed agent path
3883
- rank: null,
3884
- });
3885
- return {
3886
- current_mode: modeInfo.mode,
3887
- mode_confidence: modeInfo.confidence,
3888
- mode_distribution: { [modeInfo.mode]: 1.0 }, // single-window submission
3889
- mode_weighted_yield: cas.yield ?? 0,
3890
- peak_yield: cas.yield ?? 0,
3891
- health_score: _healthScore(
3892
- [{ mode: modeInfo.mode, yield: cas.yield ?? 0 }],
3893
- cas.yield ?? 0,
3894
- ),
3895
- badges,
3896
- };
3897
- }
1
+ export * from './tools/index.mjs';