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