sigrank 0.15.4 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/tools.mjs CHANGED
@@ -8,7 +8,8 @@
8
8
  * narration card in ./narrate.mjs. Token-only, no transcript content.
9
9
  */
10
10
 
11
- import { cascade, parsePillars } from './cascade.mjs'
11
+ import { cascade, parsePillars, detectMode, qualityScore, MODE_EXPECTED_YIELD } from './cascade.mjs'
12
+ import { computeBadges } from './badges.mjs'
12
13
  import { narrate } from './narrate.mjs'
13
14
  import { tokenpull as pullLocal, tokenpullCodex as pullCodex, tokenpullAny } from './tokenpull.mjs'
14
15
  import { ALL_PLATFORMS } from './adapters.mjs'
@@ -165,11 +166,221 @@ export const DEFAULT_FETCH_TIMEOUT = Number(process.env.SIGRANK_FETCH_TIMEOUT) |
165
166
  * past this is malformed or abusive — reject cleanly before parsing/POSTing (E2 hardening). */
166
167
  const MAX_INPUT = 1_000_000
167
168
 
169
+ // ── Smithery quality: output schemas + annotations ──────────────────────────
170
+ // MCP tool annotations hint to clients about side-effects, read-only status, etc.
171
+ const ANNOTATIONS = {
172
+ readOnlyHint: { readOnlyHint: true },
173
+ destructiveHint: { destructiveHint: false },
174
+ idempotentHint: { idempotentHint: true },
175
+ openWorldHint: { openWorldHint: false },
176
+ }
177
+
178
+ // Common output schema for cascade results (rank_paste, simulate_change, etc.)
179
+ const CASCADE_OUTPUT = {
180
+ type: 'object',
181
+ properties: {
182
+ yield_: { type: 'number', description: 'Υ Yield — the headline efficiency metric (Cache Reads × Output / Input²)' },
183
+ snr: { type: 'number', description: 'Signal-to-noise ratio' },
184
+ leverage: { type: 'number', description: 'Cr/I — cache reads divided by input' },
185
+ velocity: { type: 'number', description: 'O/I — output divided by input' },
186
+ tenx_dev: { type: 'number', description: '10xDEV score' },
187
+ class: { type: 'string', enum: ['Burner', 'Builder', '10xer'], description: 'Operator class tier' },
188
+ card: { type: 'string', description: 'Deterministic prose summary of the cascade result' },
189
+ warnings: { type: 'array', items: { type: 'string' }, description: 'Parse or data warnings if any' },
190
+ },
191
+ required: ['yield_', 'class'],
192
+ }
193
+
194
+ const LEADERBOARD_OUTPUT = {
195
+ type: 'object',
196
+ properties: {
197
+ operators: {
198
+ type: 'array',
199
+ description: 'Array of ranked operators sorted by yield',
200
+ items: {
201
+ type: 'object',
202
+ properties: {
203
+ codename: { type: 'string', description: 'Public display name' },
204
+ yield_: { type: 'number', description: 'Υ Yield metric' },
205
+ leverage: { type: 'number', description: 'Cr/I ratio' },
206
+ velocity: { type: 'number', description: 'O/I ratio' },
207
+ class: { type: 'string', enum: ['Burner', 'Builder', '10xer'] },
208
+ rank: { type: 'integer', description: '1-based rank position' },
209
+ },
210
+ },
211
+ },
212
+ },
213
+ }
214
+
215
+ const OPERATOR_OUTPUT = {
216
+ type: 'object',
217
+ properties: {
218
+ codename: { type: 'string', description: 'Operator display name' },
219
+ yield_: { type: 'number', description: 'Υ Yield metric' },
220
+ leverage: { type: 'number', description: 'Cr/I ratio' },
221
+ velocity: { type: 'number', description: 'O/I ratio' },
222
+ class: { type: 'string', enum: ['Burner', 'Builder', '10xer'] },
223
+ rank: { type: 'integer', description: '1-based rank position' },
224
+ windows: {
225
+ type: 'array',
226
+ description: 'Per-window breakdowns (7d, 30d, 90d, all-time)',
227
+ items: {
228
+ type: 'object',
229
+ properties: {
230
+ window: { type: 'string', enum: ['7d', '30d', '90d', 'all_time'] },
231
+ pillars: {
232
+ type: 'object',
233
+ properties: {
234
+ input: { type: 'integer' },
235
+ output: { type: 'integer' },
236
+ cacheCreate: { type: 'integer' },
237
+ cacheRead: { type: 'integer' },
238
+ },
239
+ },
240
+ },
241
+ },
242
+ },
243
+ },
244
+ }
245
+
246
+ const SUBMIT_OUTPUT = {
247
+ type: 'object',
248
+ properties: {
249
+ status: { type: 'string', enum: ['ok', 'error', 'skipped'], description: 'Submission status' },
250
+ preview: CASCADE_OUTPUT,
251
+ server_response: { type: 'object', description: 'Server-side response including new rank if accepted' },
252
+ reason: { type: 'string', description: 'Error or skip reason if status is not ok' },
253
+ },
254
+ }
255
+
256
+ const TOKENPULL_OUTPUT = {
257
+ type: 'object',
258
+ properties: {
259
+ platform: { type: 'string', description: 'Source platform name' },
260
+ generatedAt: { type: 'string', description: 'ISO timestamp of the pull' },
261
+ windows: {
262
+ type: 'array',
263
+ description: 'Per-window token usage + cascade results',
264
+ items: {
265
+ type: 'object',
266
+ properties: {
267
+ window: { type: 'string', enum: ['7d', '30d', '90d', 'all'] },
268
+ pillars: {
269
+ type: 'object',
270
+ properties: {
271
+ input: { type: 'integer' },
272
+ output: { type: 'integer' },
273
+ cacheCreate: { type: 'integer' },
274
+ cacheRead: { type: 'integer' },
275
+ },
276
+ },
277
+ messages: { type: 'integer', description: 'Number of messages in window' },
278
+ estimated: { type: 'boolean', description: 'True if cacheCreate was estimated' },
279
+ cascade: CASCADE_OUTPUT,
280
+ },
281
+ },
282
+ },
283
+ },
284
+ }
285
+
286
+ const ENROLL_OUTPUT = {
287
+ type: 'object',
288
+ properties: {
289
+ status: { type: 'string', enum: ['enrolled', 'error'], description: 'Enrollment result' },
290
+ codename: { type: 'string', description: 'Operator codename if enrolled' },
291
+ operator_id: { type: 'string', description: 'Operator ID if enrolled' },
292
+ device_id: { type: 'string', description: 'Local device ID' },
293
+ trust_status: { type: 'string', description: 'Trust level of the device' },
294
+ reason: { type: 'string', description: 'Error reason if status is error' },
295
+ },
296
+ }
297
+
298
+ const COMPARE_OUTPUT = {
299
+ type: 'object',
300
+ properties: {
301
+ platform: { type: 'string' },
302
+ sources: {
303
+ type: 'array',
304
+ description: 'Side-by-side comparison of each token source',
305
+ items: {
306
+ type: 'object',
307
+ properties: {
308
+ source: { type: 'string', description: 'Source name (tokenpull, ccusage, token-dash, tokscale)' },
309
+ pillars: { type: 'object' },
310
+ cascade: CASCADE_OUTPUT,
311
+ delta_pct: { type: 'object', description: 'Delta % vs tokenpull baseline' },
312
+ },
313
+ },
314
+ },
315
+ },
316
+ }
317
+
318
+ const SIMULATE_OUTPUT = {
319
+ type: 'object',
320
+ properties: {
321
+ current: CASCADE_OUTPUT,
322
+ simulated: CASCADE_OUTPUT,
323
+ yield_delta: { type: 'number', description: 'Υ Yield change (simulated - current)' },
324
+ class_change: { type: 'string', description: 'Class tier change description' },
325
+ metric_diffs: { type: 'object', description: 'Per-metric before/after diffs' },
326
+ },
327
+ }
328
+
329
+ const DIAGNOSE_OUTPUT = {
330
+ type: 'object',
331
+ properties: {
332
+ pillars: { type: 'object', description: 'The 4 raw token pillars' },
333
+ cascade: CASCADE_OUTPUT,
334
+ diagnosis: {
335
+ type: 'array',
336
+ description: 'Ranked list of efficiency leaks found, worst first',
337
+ items: {
338
+ type: 'object',
339
+ properties: {
340
+ metric: { type: 'string', description: 'Which metric is underperforming' },
341
+ severity: { type: 'string', enum: ['critical', 'warning', 'info'], description: 'How bad the leak is' },
342
+ finding: { type: 'string', description: 'What the analysis found' },
343
+ recommendation: { type: 'string', description: 'What to do about it' },
344
+ estimated_yield_impact: { type: 'string', description: 'Estimated Υ improvement if fixed' },
345
+ },
346
+ },
347
+ },
348
+ summary: { type: 'string', description: 'One-line summary of the operator\'s cascade health' },
349
+ },
350
+ }
351
+
352
+ const SUGGEST_OUTPUT = {
353
+ type: 'object',
354
+ properties: {
355
+ suggestions: {
356
+ type: 'array',
357
+ description: 'Ranked recommendations, highest Υ impact first',
358
+ items: {
359
+ type: 'object',
360
+ properties: {
361
+ rank: { type: 'number', description: '1-based rank by Υ impact' },
362
+ action: { type: 'string', description: 'What to change' },
363
+ pillar: { type: 'string', description: 'Which pillar to adjust' },
364
+ delta: { type: 'string', description: 'How much to change it' },
365
+ simulated_yield: { type: 'number', description: 'Projected Υ after the change' },
366
+ yield_delta: { type: 'number', description: 'Υ change vs current' },
367
+ class_after: { type: 'string', description: 'Projected class after the change' },
368
+ rationale: { type: 'string', description: 'Why this helps' },
369
+ },
370
+ },
371
+ },
372
+ current_yield: { type: 'number', description: 'Current Υ before any changes' },
373
+ current_class: { type: 'string', description: 'Current class tier' },
374
+ best_single_change: { type: 'string', description: 'The single highest-impact change' },
375
+ },
376
+ }
377
+
168
378
  export const TOOLS = [
169
379
  {
170
380
  name: 'rank_paste',
171
381
  description:
172
382
  'Computes the SigRank yield cascade from a paste of token counts. Parses the input, runs the full cascade math locally (no network calls), and returns: Υ Yield (the headline efficiency metric, Υ = Cache Reads × Output / Input²), SNR (signal-to-noise ratio), Leverage ratio (Cr/I = cache reads divided by input), Velocity (O/I = output divided by input), 10xDEV score, operator class tier (Burner / Builder / 10xer), and a deterministic prose "card" summarizing the result in plain English. Accepts two input formats: (1) JSON object {"input":N,"output":N,"cacheCreate":N,"cacheRead":N} or (2) four whitespace-separated numbers in order: input output cacheCreate cacheRead. Returns an error if the input is malformed or has negative values. Use this for a quick one-off ranking without submitting to the board. Do NOT use this to submit your score — use submit_paste instead, which both ranks and publishes. Do NOT use this if you want to rank all four time windows at once — use rank_windows for that. After calling this, use submit_paste to publish the result if you want to appear on the leaderboard.',
383
+ annotations: { ...ANNOTATIONS.readOnlyHint, ...ANNOTATIONS.destructiveHint, ...ANNOTATIONS.idempotentHint, ...ANNOTATIONS.openWorldHint },
173
384
  inputSchema: {
174
385
  type: 'object',
175
386
  properties: {
@@ -181,21 +392,25 @@ export const TOOLS = [
181
392
  required: ['text'],
182
393
  description: 'Requires the token counts as a string. No other parameters are accepted.',
183
394
  },
395
+ outputSchema: CASCADE_OUTPUT,
184
396
  },
185
397
  {
186
398
  name: 'get_leaderboard',
187
399
  description:
188
400
  "Fetches the live public SigRank leaderboard from signalaf.com. Reads all ranked operators sorted by yield (Υ = Cache Reads × Output / Input²) and returns an array of operator summaries. Each entry contains: codename (public display name), yield (Υ, the headline efficiency metric), leverage ratio (Cr/I = cache reads divided by input), velocity (O/I = output divided by input), class tier (Burner / Builder / 10xer), and rank position (integer, 1-based). Returns an empty array if no operators have submitted yet. Use this to see where operators stand overall, to find specific codenames for get_operator lookups, or to display the current rankings. Do NOT use this to check your own rank if you already know your codename — use get_operator instead for a single-operator profile with per-window breakdowns. After calling this, follow up with get_operator to get detailed metrics for any operator of interest.",
401
+ annotations: { ...ANNOTATIONS.readOnlyHint, ...ANNOTATIONS.openWorldHint },
189
402
  inputSchema: {
190
403
  type: 'object',
191
404
  properties: {},
192
405
  description: 'This tool takes no parameters. It always fetches the full public leaderboard.',
193
406
  },
407
+ outputSchema: LEADERBOARD_OUTPUT,
194
408
  },
195
409
  {
196
410
  name: 'get_operator',
197
411
  description:
198
412
  "Fetches one operator's live profile from the SigRank board by their codename. Reads the operator's current submission data from signalaf.com and returns their detailed metrics: yield (Υ), leverage ratio (Cr/I), velocity (O/I), class tier (Burner / Builder / 10xer), rank position (integer, 1-based), and per-window breakdowns for each time range (7d, 30d, 90d, all-time) with the four canonical pillars (input, output, cacheCreate, cacheRead) per window. Returns an error if the codename is not found on the board. Use this to look up any operator who has submitted to the board — codenames are public and visible on the leaderboard. Do NOT use this to browse all operators — use get_leaderboard for that. After calling this, you can use simulate_change to model what would happen if the operator adjusted their token mix.",
413
+ annotations: { ...ANNOTATIONS.readOnlyHint, ...ANNOTATIONS.openWorldHint },
199
414
  inputSchema: {
200
415
  type: 'object',
201
416
  properties: {
@@ -207,11 +422,13 @@ export const TOOLS = [
207
422
  required: ['codename'],
208
423
  description: 'Requires the operator\'s codename. No other parameters are accepted.',
209
424
  },
425
+ outputSchema: OPERATOR_OUTPUT,
210
426
  },
211
427
  {
212
428
  name: 'submit_paste',
213
429
  description:
214
430
  'Ranks a paste of token counts AND publishes it to the live SigRank board at signalaf.com in one call. First computes the cascade locally for an instant preview (yield, leverage, velocity, class, card), then submits the raw paste to the board\'s web-paste endpoint, which re-parses and re-scores it server-side. The server score is authoritative — it may differ from the local preview if the board applies additional validation. Returns both the local preview and the server response (including the operator\'s new rank if accepted). A codename is required to publish — omit it for a local preview only (no board submission). Token-only, no auth required. Use this when you have token counts from ccusage or a dashboard and want to both see your score and publish it. Do NOT use this if you want to pull your local usage automatically — use tokenpull_submit for the zero-paste flow. Do NOT use this for multi-window dashboard pastes — use rank_windows to rank them first, then submit each window. After calling this, use get_operator with your codename to verify your submission appeared on the board.',
431
+ annotations: { ...ANNOTATIONS.destructiveHint, ...ANNOTATIONS.idempotentHint, ...ANNOTATIONS.openWorldHint },
215
432
  inputSchema: {
216
433
  type: 'object',
217
434
  properties: {
@@ -227,11 +444,13 @@ export const TOOLS = [
227
444
  required: ['text'],
228
445
  description: 'Requires token counts (text). Codename is optional but required for board submission — omit it for preview-only mode.',
229
446
  },
447
+ outputSchema: SUBMIT_OUTPUT,
230
448
  },
231
449
  {
232
450
  name: 'tokenpull',
233
451
  description:
234
452
  "Pull your LOCAL token usage from the platform's session logs and rank it across the four windows (7d/30d/90d/all-time) with the cascade — zero paste. Token-only: reads usage counts not message content. The numbers stay on your machine unless you submit them. Some platforms may have partial data (estimated=true when cacheCreate isn't available) or a dataGap note when the log format doesn't expose raw token counts.",
453
+ annotations: { ...ANNOTATIONS.readOnlyHint, ...ANNOTATIONS.openWorldHint },
235
454
  inputSchema: {
236
455
  type: 'object',
237
456
  properties: {
@@ -242,11 +461,13 @@ export const TOOLS = [
242
461
  },
243
462
  },
244
463
  },
464
+ outputSchema: TOKENPULL_OUTPUT,
245
465
  },
246
466
  {
247
467
  name: 'tokenpull_submit',
248
468
  description:
249
469
  "Pull your LOCAL token usage from session logs AND publish it to the SigRank board in one call — the zero-paste flow. Reads the four canonical pillars (input, output, cacheCreate, cacheRead) per window from your local logs, computes the cascade, and submits each window to the board where it is re-scored server-side and tagged with the source platform. Requires a codename to publish; omit for a local preview only. Token-only — no prompt content is read or transmitted.",
470
+ annotations: { ...ANNOTATIONS.destructiveHint, ...ANNOTATIONS.idempotentHint, ...ANNOTATIONS.openWorldHint },
250
471
  inputSchema: {
251
472
  type: 'object',
252
473
  properties: {
@@ -266,11 +487,13 @@ export const TOOLS = [
266
487
  },
267
488
  },
268
489
  },
490
+ outputSchema: SUBMIT_OUTPUT,
269
491
  },
270
492
  {
271
493
  name: 'rank_windows',
272
494
  description:
273
495
  'Rank all four time windows (7d/30d/90d/all-time) in one call from a dashboard paste — paste the full table from ccusage, tokscale, or the Claude Max usage dashboard and get the cascade (Υ, SNR, Leverage, Velocity, 10xDEV, class, card) for each window. Each window is parsed and scored independently. Named keys required (input/output/cacheCreate/cacheRead); positional order is NOT safe here (dashboards list cache_read before cache_create — see WINDOWED_PROFILES gotcha). Omit windows you don\'t have — partial input is allowed (1–4 windows). Does NOT submit to the board; use tokenpull_submit for zero-paste publishing.',
496
+ annotations: { ...ANNOTATIONS.readOnlyHint, ...ANNOTATIONS.idempotentHint, ...ANNOTATIONS.openWorldHint },
274
497
  inputSchema: {
275
498
  type: 'object',
276
499
  properties: {
@@ -283,11 +506,28 @@ export const TOOLS = [
283
506
  // at least one window paste is required (runtime check backs this up)
284
507
  anyOf: [{ required: ['7d'] }, { required: ['30d'] }, { required: ['90d'] }, { required: ['all'] }],
285
508
  },
509
+ outputSchema: {
510
+ type: 'object',
511
+ properties: {
512
+ windows: {
513
+ type: 'array',
514
+ description: 'Cascade results per window',
515
+ items: {
516
+ type: 'object',
517
+ properties: {
518
+ window: { type: 'string', enum: ['7d', '30d', '90d', 'all'] },
519
+ ...CASCADE_OUTPUT.properties,
520
+ },
521
+ },
522
+ },
523
+ },
524
+ },
286
525
  },
287
526
  {
288
527
  name: 'watch_tokenpull',
289
528
  description:
290
529
  'One poll per call: pulls your local token logs and returns the current cascade for the watched window — the tool never blocks or loops. Re-call at your desired cadence to watch for changes (interval_s is advisory only and echoed back as poll_interval_s). With submit:true (and an enrolled device) each call may also sign + publish the watched window to the board, rate-limited to once per 5 min per platform+window; default is preview-only (no submit).',
530
+ annotations: { ...ANNOTATIONS.readOnlyHint, ...ANNOTATIONS.idempotentHint, ...ANNOTATIONS.openWorldHint },
291
531
  inputSchema: {
292
532
  type: 'object',
293
533
  properties: {
@@ -297,11 +537,13 @@ export const TOOLS = [
297
537
  submit: { type: 'boolean', description: 'auto-submit the watched window to the board as a VERIFIED operator each poll (requires `enroll`; default false = preview only)' },
298
538
  },
299
539
  },
540
+ outputSchema: TOKENPULL_OUTPUT,
300
541
  },
301
542
  {
302
543
  name: 'tokenpull_compare',
303
544
  description:
304
545
  'Pull token usage from ALL four local sources in parallel — tokenpull (JSONL canon), ccusage CLI, token-dashboard SQLite, and tokscale report — and return them side-by-side with delta % vs tokenpull as the baseline. Also computes the cascade (Υ, SNR, Leverage, class) for each source so you can see how each verifier scores. Useful for validating your numbers before submitting, or understanding discrepancies between tools. Claude only for token-dash; codex and others use tokenpull + ccusage + tokscale. Token-only, on-device.',
546
+ annotations: { ...ANNOTATIONS.readOnlyHint, ...ANNOTATIONS.openWorldHint },
305
547
  inputSchema: {
306
548
  type: 'object',
307
549
  properties: {
@@ -312,11 +554,13 @@ export const TOOLS = [
312
554
  },
313
555
  },
314
556
  },
557
+ outputSchema: COMPARE_OUTPUT,
315
558
  },
316
559
  {
317
560
  name: 'enroll',
318
561
  description:
319
562
  'Bind THIS device to your SigRank operator so your signed token runs cascade to the live board. Paste the key from signalaf.com → Settings → "New key" (or "Generate connect code"). On first run it generates + stores a local ed25519 keypair (~/.sigrank-mcp/identity.json); only the PUBLIC key is ever sent. Need a new key? Click "New key" at signalaf.com → Settings, then paste it here.',
563
+ annotations: { ...ANNOTATIONS.destructiveHint, ...ANNOTATIONS.idempotentHint, ...ANNOTATIONS.openWorldHint },
320
564
  inputSchema: {
321
565
  type: 'object',
322
566
  properties: {
@@ -325,11 +569,13 @@ export const TOOLS = [
325
569
  },
326
570
  required: ['code'],
327
571
  },
572
+ outputSchema: ENROLL_OUTPUT,
328
573
  },
329
574
  {
330
575
  name: 'submit_verified',
331
576
  description:
332
577
  'Publish your LOCAL token runs to the SigRank board as a VERIFIED operator — the enrolled, signed path. Reads your pillars (tokenpull), builds the canonical Schema 1.0 snapshot per window, ed25519-signs it with your device key, and POSTs to /api/v1/snapshots. Requires `npx sigrank-mcp enroll` first (a bound device). Only signed submissions from a trusted device rank on the board. Token-only; the private key never leaves your machine. Pass dry_run:true to inspect the exact signed payload without publishing.',
578
+ annotations: { ...ANNOTATIONS.destructiveHint, ...ANNOTATIONS.idempotentHint, ...ANNOTATIONS.openWorldHint },
333
579
  inputSchema: {
334
580
  type: 'object',
335
581
  properties: {
@@ -338,11 +584,13 @@ export const TOOLS = [
338
584
  dry_run: { type: 'boolean', description: 'build + sign but do NOT publish — returns the exact payload that would be POSTed (token counts only), so you can inspect before submitting' },
339
585
  },
340
586
  },
587
+ outputSchema: SUBMIT_OUTPUT,
341
588
  },
342
589
  {
343
590
  name: 'simulate_change',
344
591
  description:
345
592
  "The first PRESCRIPTIVE SigRank tool — 'what if I changed my token mix?' Takes your current 4 pillars (input/output/cacheCreate/cacheRead) and one or more proposed changes, runs the canonical cascade on BOTH the current and simulated values, and returns the exact Υ Yield delta, class change, and per-metric diffs. This is the 'show me the payoff before I do the work' primitive: no network, no submission, pure local math. Use it to answer 'would increasing my cache-read by 50k tokens actually move my class?' before you change your workflow. Accepts the current pillars as JSON or 4 numbers (same as rank_paste) plus a `changes` object with any of the 4 pillar names mapped to new absolute values OR relative deltas (e.g. {cacheRead: '+50000'} or {input: 800000}).",
593
+ annotations: { ...ANNOTATIONS.readOnlyHint, ...ANNOTATIONS.idempotentHint, ...ANNOTATIONS.openWorldHint },
346
594
  inputSchema: {
347
595
  type: 'object',
348
596
  properties: {
@@ -363,6 +611,84 @@ export const TOOLS = [
363
611
  },
364
612
  required: ['text', 'changes'],
365
613
  },
614
+ outputSchema: SIMULATE_OUTPUT,
615
+ },
616
+ {
617
+ name: 'diagnose_cascade',
618
+ description:
619
+ "Analyzes your token cascade and diagnoses where you're leaking efficiency. Takes your 4 pillars (input/output/cacheCreate/cacheRead) and produces a ranked list of efficiency leaks with severity (critical/warning/info), findings, and recommendations. Checks: cache leverage (are you rereading what you wrote?), velocity (are you generating enough output per input?), SNR (is your signal drowning in noise?), cache creation ratio (are you over-committing?), input bloat (is fresh input too high?), and 10xDEV (is the full cascade compounding?). Each finding includes an estimated Υ impact. Pure local math — no network, no submission. Use this BEFORE simulate_change to understand what's wrong, then use simulate_change to test fixes. Accepts the same input formats as rank_paste (JSON or 4 whitespace numbers).",
620
+ annotations: { ...ANNOTATIONS.readOnlyHint, ...ANNOTATIONS.idempotentHint, ...ANNOTATIONS.openWorldHint },
621
+ inputSchema: {
622
+ type: 'object',
623
+ properties: {
624
+ text: {
625
+ type: 'string',
626
+ description: 'Token pillars — ccusage JSON or "input output cacheCreate cacheRead" (same format as rank_paste).',
627
+ },
628
+ },
629
+ required: ['text'],
630
+ },
631
+ outputSchema: DIAGNOSE_OUTPUT,
632
+ },
633
+ {
634
+ name: 'suggest_improvements',
635
+ description:
636
+ "Generates ranked, simulated improvement suggestions for your token cascade. Takes your 4 pillars, tests multiple improvement strategies (increase cache reads, reduce fresh input, increase output, optimize cache creation), simulates each with the canonical cascade engine, and returns them ranked by Υ yield impact. Each suggestion includes: the action, which pillar to change, how much to change it, the projected Υ after the change, the yield delta, the projected class tier, and a rationale. Also returns the single highest-impact change (best_single_change). Pure local math — no network, no submission. Use this after diagnose_cascade to get actionable next steps, then use simulate_change to fine-tune before committing. Accepts the same input formats as rank_paste.",
637
+ annotations: { ...ANNOTATIONS.readOnlyHint, ...ANNOTATIONS.idempotentHint, ...ANNOTATIONS.openWorldHint },
638
+ inputSchema: {
639
+ type: 'object',
640
+ properties: {
641
+ text: {
642
+ type: 'string',
643
+ description: 'Token pillars — ccusage JSON or "input output cacheCreate cacheRead" (same format as rank_paste).',
644
+ },
645
+ },
646
+ required: ['text'],
647
+ },
648
+ outputSchema: SUGGEST_OUTPUT,
649
+ },
650
+ {
651
+ name: 'self_improve',
652
+ description:
653
+ "Runs the full self-improvement cycle in one call: (1) gets your current token pillars — either from the provided text or by running tokenpull on your local logs, (2) diagnoses where you're leaking efficiency (diagnose_cascade), (3) generates ranked improvement suggestions (suggest_improvements), (4) simulates the top suggestion (simulate_change), and (5) returns the complete cycle: diagnosis + suggestions + the simulated impact of the best change. This is the 'one-click optimize' tool — call it at the end of a session to see what to improve next time. If you provide pillars in `text`, it skips the tokenpull step. If you omit `text`, it runs tokenpull first (requires local ccusage logs). Pure local math — no network, no submission. The `scope` parameter adds mode detection (BUILD/EDIT/DEBUG/MAINTAIN/IDLE) and scoped analysis: 'daily' (default — current behavior + mode), 'weekly' (compound into weekly snapshots + report artifact), 'trend' (30d/90d trajectory analysis).",
654
+ annotations: { ...ANNOTATIONS.readOnlyHint, ...ANNOTATIONS.idempotentHint, ...ANNOTATIONS.openWorldHint },
655
+ inputSchema: {
656
+ type: 'object',
657
+ properties: {
658
+ text: {
659
+ type: 'string',
660
+ description: 'Optional: token pillars — ccusage JSON or "input output cacheCreate cacheRead". If omitted, runs tokenpull to get current pillars from local logs.',
661
+ },
662
+ window: {
663
+ type: 'string',
664
+ enum: ['7d', '30d', '90d', 'all'],
665
+ description: 'Which time window to pull when running tokenpull (default: 30d). Ignored if `text` is provided.',
666
+ },
667
+ scope: {
668
+ type: 'string',
669
+ enum: ['daily', 'weekly', 'trend'],
670
+ description: 'Analysis scope: "daily" (default — current behavior + mode detection), "weekly" (compound daily rows into weekly snapshots + report artifact with badges), "trend" (30d/90d trajectory + phase patterns). Daily modes never leave the machine — only weekly distribution goes in submitted reports.',
671
+ },
672
+ },
673
+ },
674
+ outputSchema: {
675
+ type: 'object',
676
+ properties: {
677
+ pillars: { type: 'object', description: 'The 4 raw token pillars used' },
678
+ current_cascade: CASCADE_OUTPUT,
679
+ diagnosis: { type: 'array', description: 'Efficiency leaks found (from diagnose_cascade)' },
680
+ suggestions: { type: 'array', description: 'Ranked improvements (from suggest_improvements)' },
681
+ best_simulation: { type: 'object', description: 'Simulated result of the top suggestion' },
682
+ cycle_summary: { type: 'string', description: 'One-line summary of the full cycle' },
683
+ // Scope-specific fields
684
+ mode: { type: 'object', description: 'Detected mode { mode, confidence } — present when scope is daily/weekly/trend' },
685
+ quality_score: { type: 'number', description: 'Yield relative to mode expectation (daily scope)' },
686
+ assessment: { type: 'string', description: 'One-line assessment for daily scope' },
687
+ advice: { type: 'string', description: 'Advice for next session (daily scope)' },
688
+ report: { type: 'object', description: 'Weekly report artifact (weekly scope)' },
689
+ trend: { type: 'object', description: 'Trend analysis (trend scope)' },
690
+ },
691
+ },
366
692
  },
367
693
  ]
368
694
 
@@ -521,7 +847,8 @@ export async function callTool(name, args, opts = {}) {
521
847
  msgs += w.messages || 0
522
848
  }
523
849
  if (sum.input + sum.output + sum.cacheCreate + sum.cacheRead <= 0) continue // skip empty window
524
- const r = await submitSignedWindow(wk, sum, msgs, id, { apiBase, fetchImpl: doFetch, platform: 'multi', now: opts.now, dryRun: !!args?.dry_run })
850
+ const report = _computeReportBlock(sum)
851
+ const r = await submitSignedWindow(wk, sum, msgs, id, { apiBase, fetchImpl: doFetch, platform: 'multi', now: opts.now, dryRun: !!args?.dry_run, report })
525
852
  out.push({ window: wk, pillars: sum, ...r })
526
853
  }
527
854
  return { platform: 'multi', codename: id.codename, operator_id: id.operator_id, sources: detected.map((d) => d.platform), windows: out }
@@ -531,12 +858,16 @@ export async function callTool(name, args, opts = {}) {
531
858
  const targets = args?.window ? pulled.windows.filter((w) => w.window === args.window) : pulled.windows
532
859
  const out = []
533
860
  for (const w of targets) {
861
+ // Compute the cascade report block (mode + badges + health) for this window.
862
+ // Pure math, computed locally, submitted alongside the 4 token pillars.
863
+ const report = _computeReportBlock(w.pillars)
534
864
  const r = await submitSignedWindow(w.window, w.pillars, w.messages, id, {
535
865
  apiBase,
536
866
  fetchImpl: doFetch,
537
867
  platform: pulled.platform,
538
868
  now: opts.now,
539
869
  dryRun: !!args?.dry_run,
870
+ report,
540
871
  })
541
872
  out.push({ window: w.window, pillars: w.pillars, ...r })
542
873
  }
@@ -898,5 +1229,742 @@ export async function callTool(name, args, opts = {}) {
898
1229
  }
899
1230
  }
900
1231
 
1232
+ if (name === 'diagnose_cascade') {
1233
+ if (!args?.text) throw new Error('diagnose_cascade requires a non-empty `text` argument (token pillars).')
1234
+ if (typeof args.text === 'string' && args.text.length > MAX_INPUT) {
1235
+ return { status: 'error', reason: 'input_too_large', detail: `text exceeds ${MAX_INPUT} chars.` }
1236
+ }
1237
+
1238
+ const p = parsePillars(args.text)
1239
+ const result = withParseWarnings(p, cascade(p))
1240
+ const { input: i, output: o, cacheCreate: cw, cacheRead: cr } = p
1241
+ const diagnosis = []
1242
+
1243
+ // Cache leverage check — the #1 cascade efficiency signal
1244
+ const leverage = result.leverage
1245
+ if (leverage !== null) {
1246
+ if (leverage < 10) {
1247
+ diagnosis.push({
1248
+ metric: 'cache_leverage',
1249
+ severity: 'critical',
1250
+ finding: `Cache leverage is ${leverage}× — you're reading only ${leverage}× your fresh input from cache. TRANSMITTER-class operators hit 200×+.`,
1251
+ recommendation: 'Increase context reuse: load prior session context, use longer conversation threads, reference earlier outputs.',
1252
+ estimated_yield_impact: `+${Math.round((1 - leverage / 50) * 100)}% Υ potential`,
1253
+ })
1254
+ } else if (leverage < 50) {
1255
+ diagnosis.push({
1256
+ metric: 'cache_leverage',
1257
+ severity: 'warning',
1258
+ finding: `Cache leverage is ${leverage}× — decent but below the ARCH+ threshold (~100×+).`,
1259
+ recommendation: 'Push cache reads higher by reusing prior context more aggressively.',
1260
+ estimated_yield_impact: `+${Math.round((1 - leverage / 100) * 50)}% Υ potential`,
1261
+ })
1262
+ }
1263
+ }
1264
+
1265
+ // Velocity check — output per input
1266
+ const velocity = result.velocity
1267
+ if (velocity !== null) {
1268
+ if (velocity < 0.5) {
1269
+ diagnosis.push({
1270
+ metric: 'velocity',
1271
+ severity: 'critical',
1272
+ finding: `Velocity is ${velocity} — generating only ${velocity}× your input as output. You're reading more than you produce.`,
1273
+ recommendation: 'Increase output: ask the agent to generate more code/text per turn, reduce over-reading.',
1274
+ estimated_yield_impact: `+${Math.round((0.5 - velocity) * 100)}% Υ per 0.1 velocity gain`,
1275
+ })
1276
+ } else if (velocity < 1.0) {
1277
+ diagnosis.push({
1278
+ metric: 'velocity',
1279
+ severity: 'warning',
1280
+ finding: `Velocity is ${velocity} — below 1.0 (output < input). Healthy operators hit 1.5×+.`,
1281
+ recommendation: 'Generate more output per input token — larger edits, more complete responses.',
1282
+ estimated_yield_impact: `+${Math.round((1 - velocity) * 30)}% Υ potential`,
1283
+ })
1284
+ }
1285
+ }
1286
+
1287
+ // SNR check — signal-to-noise
1288
+ const snr = result.snr
1289
+ if (snr !== null && snr < 0.3) {
1290
+ diagnosis.push({
1291
+ metric: 'snr',
1292
+ severity: 'warning',
1293
+ finding: `SNR is ${snr} — less than 30% of your token flow is output. Input is dominating.`,
1294
+ recommendation: 'Reduce fresh input (reuse context) or increase output generation.',
1295
+ estimated_yield_impact: 'Indirect — improves both velocity and leverage',
1296
+ })
1297
+ }
1298
+
1299
+ // Cache creation ratio — are you over-committing?
1300
+ if (cw > 0 && o > 0) {
1301
+ const commitRatio = cw / o
1302
+ if (commitRatio > 20) {
1303
+ diagnosis.push({
1304
+ metric: 'cache_creation',
1305
+ severity: 'info',
1306
+ finding: `Cache creation is ${commitRatio.toFixed(1)}× your output — high commitment. This is fine if you're rereading it (check leverage), but wasteful if not.`,
1307
+ recommendation: "Ensure you're rereading committed context. If leverage is low, you're writing cache you never read.",
1308
+ estimated_yield_impact: 'Cost reduction, not Υ directly',
1309
+ })
1310
+ }
1311
+ }
1312
+
1313
+ // Input bloat — is fresh input too high relative to total?
1314
+ const total = i + o + cw + cr
1315
+ if (total > 0) {
1316
+ const inputPct = (i / total) * 100
1317
+ if (inputPct > 10) {
1318
+ diagnosis.push({
1319
+ metric: 'input_bloat',
1320
+ severity: 'warning',
1321
+ finding: `Fresh input is ${inputPct.toFixed(1)}% of your total token flow — high. Efficient operators keep input under 1% by leaning on cache.`,
1322
+ recommendation: 'Reduce fresh input by reusing prior context instead of re-pasting it.',
1323
+ estimated_yield_impact: `+${Math.round((inputPct - 1) * 5)}% Υ potential`,
1324
+ })
1325
+ }
1326
+ }
1327
+
1328
+ // 10xDEV check — is the full cascade compounding?
1329
+ if (result.dev10x === null && cw === 0) {
1330
+ diagnosis.push({
1331
+ metric: '10xdev',
1332
+ severity: 'critical',
1333
+ finding: "No cache creation — the cascade cannot compound. You're operating in a non-compounding mode (like ChatGPT without prompt caching).",
1334
+ recommendation: 'Switch to a platform with prompt caching (Claude Code) or enable caching if available.',
1335
+ estimated_yield_impact: 'Enables the full cascade — potentially 10×+ Υ',
1336
+ })
1337
+ } else if (result.dev10x !== null && result.dev10x < 1.0) {
1338
+ diagnosis.push({
1339
+ metric: '10xdev',
1340
+ severity: 'info',
1341
+ finding: `10xDEV is ${result.dev10x} — below 1.0 (BASE threshold). The cascade is compounding but not strongly.`,
1342
+ recommendation: 'Improve both leverage AND velocity — 10xDEV = log10(transmission × commitment × reuse).',
1343
+ estimated_yield_impact: 'Class tier improvement',
1344
+ })
1345
+ }
1346
+
1347
+ // Sort by severity (critical > warning > info)
1348
+ const sevOrder = { critical: 0, warning: 1, info: 2 }
1349
+ diagnosis.sort((a, b) => sevOrder[a.severity] - sevOrder[b.severity])
1350
+
1351
+ const healthScore = diagnosis.filter((d) => d.severity === 'critical').length
1352
+ const summary = healthScore === 0
1353
+ ? `Cascade is healthy — Υ ${result.yield}, class ${result.class}. ${diagnosis.length} minor optimizations available.`
1354
+ : `Cascade has ${healthScore} critical leak${healthScore > 1 ? 's' : ''} — Υ ${result.yield}, class ${result.class}. Fix the critical items first.`
1355
+
1356
+ return {
1357
+ pillars: p,
1358
+ cascade: {
1359
+ yield_: result.yield,
1360
+ snr: result.snr,
1361
+ leverage: result.leverage,
1362
+ velocity: result.velocity,
1363
+ tenx_dev: result.dev10x,
1364
+ class: result.class,
1365
+ warnings: result.warnings,
1366
+ },
1367
+ diagnosis,
1368
+ summary,
1369
+ }
1370
+ }
1371
+
1372
+ if (name === 'suggest_improvements') {
1373
+ if (!args?.text) throw new Error('suggest_improvements requires a non-empty `text` argument (token pillars).')
1374
+ if (typeof args.text === 'string' && args.text.length > MAX_INPUT) {
1375
+ return { status: 'error', reason: 'input_too_large', detail: `text exceeds ${MAX_INPUT} chars.` }
1376
+ }
1377
+
1378
+ const p = parsePillars(args.text)
1379
+ const current = withParseWarnings(p, cascade(p))
1380
+ const { input: i, output: o, cacheCreate: cw, cacheRead: cr } = p
1381
+
1382
+ // Generate candidate improvements, simulate each, rank by Υ impact
1383
+ const candidates = []
1384
+
1385
+ // Strategy 1: Increase cache reads (the #1 lever)
1386
+ const crBoosts = cr > 0 ? [1.5, 2, 3, 5] : []
1387
+ for (const mult of crBoosts) {
1388
+ const sim = cascade({ ...p, cacheRead: Math.round(cr * mult) })
1389
+ if (sim.yield !== null) {
1390
+ candidates.push({
1391
+ action: `Increase cache reads by ${Math.round((mult - 1) * 100)}%`,
1392
+ pillar: 'cacheRead',
1393
+ delta: `+${Math.round(cr * (mult - 1)).toLocaleString()}`,
1394
+ simulated_yield: sim.yield,
1395
+ yield_delta: Number((sim.yield - (current.yield ?? 0)).toFixed(2)),
1396
+ class_after: sim.class,
1397
+ rationale: 'Cache reads are the strongest Υ multiplier. More reuse = higher leverage = higher yield.',
1398
+ })
1399
+ }
1400
+ }
1401
+ // For zero-cache operators, suggest enabling caching with a starter amount
1402
+ if (cr === 0 && cw === 0) {
1403
+ const starterAmounts = [Math.round(i * 10), Math.round(i * 50), Math.round(i * 100)]
1404
+ for (const amt of starterAmounts) {
1405
+ const sim = cascade({ ...p, cacheCreate: Math.round(amt * 0.5), cacheRead: amt })
1406
+ if (sim.yield !== null && sim.yield > 0) {
1407
+ candidates.push({
1408
+ action: `Enable caching with ${amt.toLocaleString()} cache reads`,
1409
+ pillar: 'cacheRead',
1410
+ delta: `+${amt.toLocaleString()}`,
1411
+ simulated_yield: sim.yield,
1412
+ yield_delta: Number((sim.yield - (current.yield ?? 0)).toFixed(2)),
1413
+ class_after: sim.class,
1414
+ rationale: 'You have no cache — enabling it unlocks the cascade. Start by reusing prior context.',
1415
+ })
1416
+ }
1417
+ }
1418
+ }
1419
+
1420
+ // Strategy 2: Reduce fresh input (Υ = Cr·O/I² — input is squared in the denominator)
1421
+ const inputReductions = [0.9, 0.75, 0.5]
1422
+ for (const mult of inputReductions) {
1423
+ const newInput = Math.round(i * mult)
1424
+ if (newInput < 1) continue
1425
+ const sim = cascade({ ...p, input: newInput })
1426
+ if (sim.yield !== null) {
1427
+ candidates.push({
1428
+ action: `Reduce fresh input by ${Math.round((1 - mult) * 100)}%`,
1429
+ pillar: 'input',
1430
+ delta: `-${Math.round(i * (1 - mult)).toLocaleString()}`,
1431
+ simulated_yield: sim.yield,
1432
+ yield_delta: Number((sim.yield - (current.yield ?? 0)).toFixed(2)),
1433
+ class_after: sim.class,
1434
+ rationale: 'Input is squared in the Υ denominator (Υ = Cr·O/I²). Reducing input has a quadratic payoff.',
1435
+ })
1436
+ }
1437
+ }
1438
+
1439
+ // Strategy 3: Increase output
1440
+ const outputBoosts = [1.25, 1.5, 2]
1441
+ for (const mult of outputBoosts) {
1442
+ const sim = cascade({ ...p, output: Math.round(o * mult) })
1443
+ if (sim.yield !== null) {
1444
+ candidates.push({
1445
+ action: `Increase output by ${Math.round((mult - 1) * 100)}%`,
1446
+ pillar: 'output',
1447
+ delta: `+${Math.round(o * (mult - 1)).toLocaleString()}`,
1448
+ simulated_yield: sim.yield,
1449
+ yield_delta: Number((sim.yield - (current.yield ?? 0)).toFixed(2)),
1450
+ class_after: sim.class,
1451
+ rationale: 'Output is a linear multiplier in Υ. More output per session = higher yield.',
1452
+ })
1453
+ }
1454
+ }
1455
+
1456
+ // Strategy 4: Optimize cache creation (reduce if over-committing)
1457
+ if (cw > o * 10) {
1458
+ const sim = cascade({ ...p, cacheCreate: Math.round(o * 5) })
1459
+ if (sim.yield !== null) {
1460
+ candidates.push({
1461
+ action: 'Reduce cache creation to 5× output',
1462
+ pillar: 'cacheCreate',
1463
+ delta: `-${Math.round(cw - o * 5).toLocaleString()}`,
1464
+ simulated_yield: sim.yield,
1465
+ yield_delta: Number((sim.yield - (current.yield ?? 0)).toFixed(2)),
1466
+ class_after: sim.class,
1467
+ rationale: 'You\'re over-committing cache (cw >> output). Trimming to a healthy ratio reduces cost without hurting yield.',
1468
+ })
1469
+ }
1470
+ }
1471
+
1472
+ // Sort by yield_delta descending, take top 8
1473
+ candidates.sort((a, b) => b.yield_delta - a.yield_delta)
1474
+ const top = candidates.slice(0, 8).map((c, idx) => ({ rank: idx + 1, ...c }))
1475
+
1476
+ const best = top[0]
1477
+ return {
1478
+ suggestions: top,
1479
+ current_yield: current.yield,
1480
+ current_class: current.class,
1481
+ best_single_change: best
1482
+ ? `${best.action} (Υ ${current.yield} → ${best.simulated_yield}, +${best.yield_delta} yield, class ${best.class_after})`
1483
+ : 'No improvements found — your cascade is already optimized.',
1484
+ }
1485
+ }
1486
+
1487
+ if (name === 'self_improve') {
1488
+ // The full self-improvement cycle: pull → diagnose → suggest → simulate.
1489
+ // If text is provided, use it as pillars. If not, run tokenpull first.
1490
+ let pillars
1491
+ let pulledFrom = 'provided'
1492
+
1493
+ if (args?.text) {
1494
+ if (typeof args.text === 'string' && args.text.length > MAX_INPUT) {
1495
+ return { status: 'error', reason: 'input_too_large', detail: `text exceeds ${MAX_INPUT} chars.` }
1496
+ }
1497
+ pillars = parsePillars(args.text)
1498
+ } else {
1499
+ // Run tokenpull to get current pillars from local logs
1500
+ const windowType = args?.window || '30d'
1501
+ try {
1502
+ const pullResult = await callTool('tokenpull', { window: windowType }, opts)
1503
+ if (pullResult?.status === 'error' || !pullResult?.windows?.length) {
1504
+ return {
1505
+ status: 'error',
1506
+ reason: 'tokenpull_failed',
1507
+ detail: 'Could not pull pillars from local logs. Provide pillars via `text` argument instead.',
1508
+ pull_result: pullResult,
1509
+ }
1510
+ }
1511
+ // Use the first window's pillars
1512
+ const w = pullResult.windows[0]
1513
+ pillars = w.pillars
1514
+ pulledFrom = `tokenpull ${w.window || windowType}`
1515
+ } catch (e) {
1516
+ return {
1517
+ status: 'error',
1518
+ reason: 'tokenpull_error',
1519
+ detail: String(e.message || e),
1520
+ hint: 'Provide pillars via `text` argument instead.',
1521
+ }
1522
+ }
1523
+ }
1524
+
1525
+ const currentResult = withParseWarnings(pillars, cascade(pillars))
1526
+ const { input: i, output: o, cacheCreate: cw, cacheRead: cr } = pillars
1527
+
1528
+ // ── Step 1: Diagnose ──────────────────────────────────────────────
1529
+ const diagnosis = []
1530
+ const leverage = currentResult.leverage
1531
+ if (leverage !== null && leverage < 10) {
1532
+ diagnosis.push({
1533
+ metric: 'cache_leverage', severity: 'critical',
1534
+ finding: `Cache leverage is ${leverage}× — TRANSMITTER-class operators hit 200×+.`,
1535
+ recommendation: 'Increase context reuse: load prior session context, use longer threads.',
1536
+ })
1537
+ } else if (leverage !== null && leverage < 50) {
1538
+ diagnosis.push({
1539
+ metric: 'cache_leverage', severity: 'warning',
1540
+ finding: `Cache leverage is ${leverage}× — below ARCH+ threshold (~100×+).`,
1541
+ recommendation: 'Push cache reads higher by reusing prior context.',
1542
+ })
1543
+ }
1544
+
1545
+ const velocity = currentResult.velocity
1546
+ if (velocity !== null && velocity < 0.5) {
1547
+ diagnosis.push({
1548
+ metric: 'velocity', severity: 'critical',
1549
+ finding: `Velocity is ${velocity} — generating only ${velocity}× input as output.`,
1550
+ recommendation: 'Increase output per turn, reduce over-reading.',
1551
+ })
1552
+ } else if (velocity !== null && velocity < 1.0) {
1553
+ diagnosis.push({
1554
+ metric: 'velocity', severity: 'warning',
1555
+ finding: `Velocity is ${velocity} — below 1.0. Healthy operators hit 1.5×+.`,
1556
+ recommendation: 'Generate more output per input token.',
1557
+ })
1558
+ }
1559
+
1560
+ if (currentResult.dev10x === null && cw === 0) {
1561
+ diagnosis.push({
1562
+ metric: '10xdev', severity: 'critical',
1563
+ finding: 'No cache creation — cascade cannot compound.',
1564
+ recommendation: 'Switch to a platform with prompt caching (Claude Code).',
1565
+ })
1566
+ }
1567
+
1568
+ const total = i + o + cw + cr
1569
+ if (total > 0 && (i / total) * 100 > 10) {
1570
+ diagnosis.push({
1571
+ metric: 'input_bloat', severity: 'warning',
1572
+ finding: `Fresh input is ${((i / total) * 100).toFixed(1)}% of total flow — efficient operators keep it under 1%.`,
1573
+ recommendation: 'Reduce fresh input by reusing prior context.',
1574
+ })
1575
+ }
1576
+
1577
+ // ── Step 2: Suggest (generate + simulate candidates) ──────────────
1578
+ const candidates = []
1579
+ // If cacheRead is 0, suggest absolute amounts instead of percentage boosts
1580
+ const crBoosts = cr > 0 ? [1.5, 2, 3, 5] : []
1581
+ for (const mult of crBoosts) {
1582
+ const sim = cascade({ ...pillars, cacheRead: Math.round(cr * mult) })
1583
+ if (sim.yield !== null) candidates.push({
1584
+ action: `Increase cache reads by ${Math.round((mult - 1) * 100)}%`,
1585
+ pillar: 'cacheRead', delta: `+${Math.round(cr * (mult - 1)).toLocaleString()}`,
1586
+ simulated_yield: sim.yield, yield_delta: Number((sim.yield - (currentResult.yield ?? 0)).toFixed(2)),
1587
+ class_after: sim.class, rationale: 'Cache reads are the strongest Υ multiplier.',
1588
+ })
1589
+ }
1590
+ // For zero-cache operators, suggest enabling caching with a starter amount
1591
+ if (cr === 0 && cw === 0) {
1592
+ const starterAmounts = [Math.round(i * 10), Math.round(i * 50), Math.round(i * 100)]
1593
+ for (const amt of starterAmounts) {
1594
+ const sim = cascade({ ...pillars, cacheCreate: Math.round(amt * 0.5), cacheRead: amt })
1595
+ if (sim.yield !== null && sim.yield > 0) candidates.push({
1596
+ action: `Enable caching with ${amt.toLocaleString()} cache reads`,
1597
+ pillar: 'cacheRead', delta: `+${amt.toLocaleString()}`,
1598
+ simulated_yield: sim.yield, yield_delta: Number((sim.yield - (currentResult.yield ?? 0)).toFixed(2)),
1599
+ class_after: sim.class, rationale: 'You have no cache — enabling it unlocks the cascade. Start by reusing prior context.',
1600
+ })
1601
+ }
1602
+ }
1603
+ for (const mult of [0.9, 0.75, 0.5]) {
1604
+ const newInput = Math.round(i * mult)
1605
+ if (newInput < 1) continue
1606
+ const sim = cascade({ ...pillars, input: newInput })
1607
+ if (sim.yield !== null) candidates.push({
1608
+ action: `Reduce fresh input by ${Math.round((1 - mult) * 100)}%`,
1609
+ pillar: 'input', delta: `-${Math.round(i * (1 - mult)).toLocaleString()}`,
1610
+ simulated_yield: sim.yield, yield_delta: Number((sim.yield - (currentResult.yield ?? 0)).toFixed(2)),
1611
+ class_after: sim.class, rationale: 'Input is squared in Υ denominator — quadratic payoff.',
1612
+ })
1613
+ }
1614
+ for (const mult of [1.25, 1.5, 2]) {
1615
+ const sim = cascade({ ...pillars, output: Math.round(o * mult) })
1616
+ if (sim.yield !== null) candidates.push({
1617
+ action: `Increase output by ${Math.round((mult - 1) * 100)}%`,
1618
+ pillar: 'output', delta: `+${Math.round(o * (mult - 1)).toLocaleString()}`,
1619
+ simulated_yield: sim.yield, yield_delta: Number((sim.yield - (currentResult.yield ?? 0)).toFixed(2)),
1620
+ class_after: sim.class, rationale: 'Output is a linear multiplier in Υ.',
1621
+ })
1622
+ }
1623
+ candidates.sort((a, b) => b.yield_delta - a.yield_delta)
1624
+ const suggestions = candidates.slice(0, 8).map((c, idx) => ({ rank: idx + 1, ...c }))
1625
+
1626
+ // ── Step 3: Simulate the top suggestion ───────────────────────────
1627
+ const best = suggestions[0]
1628
+ let bestSimulation = null
1629
+ if (best) {
1630
+ const simPillars = { ...pillars }
1631
+ const delta = parseInt(best.delta.replace(/[+\-,]/g, ''), 10)
1632
+ if (best.pillar === 'cacheRead') simPillars.cacheRead = cr + delta
1633
+ else if (best.pillar === 'input') simPillars.input = Math.max(i - delta, 1)
1634
+ else if (best.pillar === 'output') simPillars.output = o + delta
1635
+ const simResult = cascade(simPillars)
1636
+ bestSimulation = {
1637
+ action: best.action,
1638
+ current_yield: currentResult.yield,
1639
+ simulated_yield: simResult.yield,
1640
+ yield_delta: best.yield_delta,
1641
+ current_class: currentResult.class,
1642
+ simulated_class: simResult.class,
1643
+ class_changed: currentResult.class !== simResult.class,
1644
+ }
1645
+ }
1646
+
1647
+ // ── Step 4: Cycle summary ─────────────────────────────────────────
1648
+ const criticalCount = diagnosis.filter((d) => d.severity === 'critical').length
1649
+ const cycleSummary = best
1650
+ ? `Pulled from ${pulledFrom}. Υ ${currentResult.yield} (${currentResult.class}). ${criticalCount} critical, ${diagnosis.length - criticalCount} other findings. Best: ${best.action} → Υ ${best.simulated_yield} (+${best.yield_delta}).`
1651
+ : `Pulled from ${pulledFrom}. Υ ${currentResult.yield} (${currentResult.class}). ${diagnosis.length} findings. No improvements suggested — cascade is optimized.`
1652
+
1653
+ // ── Step 5: Scope-specific analysis ───────────────────────────────
1654
+ const scope = args?.scope || 'daily'
1655
+ const modeInfo = currentResult.mode // { mode, confidence }
1656
+ const scopeResult = {}
1657
+
1658
+ if (scope === 'daily') {
1659
+ // Daily: mode + quality score + assessment + advice
1660
+ const qs = qualityScore(currentResult.yield ?? 0, modeInfo.mode)
1661
+ const expected = MODE_EXPECTED_YIELD[modeInfo.mode] ?? 0
1662
+ const assessment = _dailyAssessment(modeInfo.mode, currentResult.yield, qs, expected)
1663
+ const advice = _dailyAdvice(modeInfo.mode, currentResult.yield, qs)
1664
+ scopeResult.mode = modeInfo
1665
+ scopeResult.quality_score = Math.round(qs * 100) / 100
1666
+ scopeResult.assessment = assessment
1667
+ scopeResult.advice = advice
1668
+ }
1669
+
1670
+ if (scope === 'weekly' || scope === 'trend') {
1671
+ // Pull daily rows from ccusage to build weekly snapshots
1672
+ const dailyRows = await _pullDailyRows(opts)
1673
+ const weeklySnapshots = _compoundWeekly(dailyRows)
1674
+ const modeDistribution = _modeDistribution(weeklySnapshots)
1675
+ const modeWeightedYield = _modeWeightedYield(weeklySnapshots)
1676
+
1677
+ // Compute badges
1678
+ const historyForBadges = weeklySnapshots.map(w => ({
1679
+ date: w.weekStart,
1680
+ mode: w.mode,
1681
+ yield: w.yield,
1682
+ pillars: w.pillars,
1683
+ }))
1684
+ const badges = computeBadges({
1685
+ pillars,
1686
+ cascade: currentResult,
1687
+ history: historyForBadges,
1688
+ isVerified: false, // set by submit_verified
1689
+ rank: null, // server-side
1690
+ })
1691
+
1692
+ if (scope === 'weekly') {
1693
+ scopeResult.mode = modeInfo
1694
+ // Strip dailyModes from weekly_snapshots — privacy boundary (daily modes never leave the machine)
1695
+ const safeSnapshots = weeklySnapshots.map(({ dailyModes, ...rest }) => rest)
1696
+ scopeResult.report = {
1697
+ current_mode: modeInfo.mode,
1698
+ mode_confidence: modeInfo.confidence,
1699
+ mode_distribution: modeDistribution,
1700
+ mode_weighted_yield: modeWeightedYield,
1701
+ peak_yield: _peakYield(weeklySnapshots),
1702
+ health_score: _healthScore(weeklySnapshots, modeWeightedYield),
1703
+ weekly_snapshots: safeSnapshots,
1704
+ badges,
1705
+ }
1706
+ }
1707
+
1708
+ if (scope === 'trend') {
1709
+ const yield7d = _yieldForDays(weeklySnapshots, 7)
1710
+ const yield30d = _yieldForDays(weeklySnapshots, 30)
1711
+ const yield90d = _yieldForDays(weeklySnapshots, 90)
1712
+ const trajectory = _trajectory(yield7d, yield30d, yield90d)
1713
+ scopeResult.mode = modeInfo
1714
+ scopeResult.trend = {
1715
+ yield_7d: yield7d,
1716
+ yield_30d: yield30d,
1717
+ yield_90d: yield90d,
1718
+ trajectory,
1719
+ mode_distribution: modeDistribution,
1720
+ phase_pattern: _phasePattern(weeklySnapshots),
1721
+ }
1722
+ }
1723
+ }
1724
+
1725
+ return {
1726
+ pillars,
1727
+ current_cascade: {
1728
+ yield_: currentResult.yield,
1729
+ snr: currentResult.snr,
1730
+ leverage: currentResult.leverage,
1731
+ velocity: currentResult.velocity,
1732
+ tenx_dev: currentResult.dev10x,
1733
+ class: currentResult.class,
1734
+ mode: modeInfo,
1735
+ warnings: currentResult.warnings,
1736
+ },
1737
+ diagnosis,
1738
+ suggestions,
1739
+ best_simulation: bestSimulation,
1740
+ cycle_summary: cycleSummary,
1741
+ ...scopeResult,
1742
+ }
1743
+ }
1744
+
901
1745
  throw new Error(`Unknown tool: ${name}`)
902
1746
  }
1747
+
1748
+ // ── self_improve scope helpers ──────────────────────────────────────────────
1749
+
1750
+ /** Pull daily rows from ccusage to build mode history. Returns array of { date, pillars, yield, mode }. */
1751
+ async function _pullDailyRows(opts = {}) {
1752
+ try {
1753
+ const raw = await execFileAsync('ccusage', ['claude', 'daily', '--json'], 15000)
1754
+ const rows = JSON.parse(raw)?.daily ?? JSON.parse(raw)
1755
+ if (!Array.isArray(rows)) return []
1756
+ return rows.map(r => {
1757
+ const pillars = {
1758
+ input: r.inputTokens ?? r.input_tokens ?? 0,
1759
+ output: r.outputTokens ?? r.output_tokens ?? 0,
1760
+ cacheCreate: r.cacheCreationTokens ?? r.cache_create_tokens ?? 0,
1761
+ cacheRead: r.cacheReadTokens ?? r.cache_read_tokens ?? 0,
1762
+ }
1763
+ const cas = cascade(pillars)
1764
+ const mode = detectMode(pillars)
1765
+ return {
1766
+ date: r.date ?? r.day ?? null,
1767
+ pillars,
1768
+ yield: cas.yield ?? 0,
1769
+ mode: mode.mode,
1770
+ mode_confidence: mode.confidence,
1771
+ }
1772
+ }).filter(r => r.date)
1773
+ } catch {
1774
+ return []
1775
+ }
1776
+ }
1777
+
1778
+ /** Compound daily rows into weekly snapshots. Each week = 7 days, starting Monday. */
1779
+ function _compoundWeekly(dailyRows) {
1780
+ if (!dailyRows || dailyRows.length === 0) return []
1781
+ // Group by ISO week (week starts Monday)
1782
+ const weeks = new Map()
1783
+ for (const row of dailyRows) {
1784
+ const d = new Date(row.date)
1785
+ const day = d.getDay() || 7 // Sunday=0 → 7
1786
+ const monday = new Date(d)
1787
+ monday.setDate(d.getDate() - day + 1)
1788
+ const weekKey = monday.toISOString().slice(0, 10)
1789
+ if (!weeks.has(weekKey)) {
1790
+ weeks.set(weekKey, { weekStart: weekKey, days: [] })
1791
+ }
1792
+ weeks.get(weekKey).days.push(row)
1793
+ }
1794
+ // Compound each week
1795
+ return Array.from(weeks.values()).map(w => {
1796
+ const pillars = w.days.reduce((acc, d) => ({
1797
+ input: acc.input + d.pillars.input,
1798
+ output: acc.output + d.pillars.output,
1799
+ cacheCreate: acc.cacheCreate + d.pillars.cacheCreate,
1800
+ cacheRead: acc.cacheRead + d.pillars.cacheRead,
1801
+ }), { input: 0, output: 0, cacheCreate: 0, cacheRead: 0 })
1802
+ const cas = cascade(pillars)
1803
+ const mode = detectMode(pillars)
1804
+ return {
1805
+ weekStart: w.weekStart,
1806
+ pillars,
1807
+ yield: cas.yield ?? 0,
1808
+ mode: mode.mode,
1809
+ mode_confidence: mode.confidence,
1810
+ dayCount: w.days.length,
1811
+ dailyModes: w.days.map(d => d.mode), // stays local — not in submitted report
1812
+ }
1813
+ }).sort((a, b) => a.weekStart.localeCompare(b.weekStart))
1814
+ }
1815
+
1816
+ /** Compute mode distribution from weekly snapshots (weekly granularity = privacy boundary). */
1817
+ function _modeDistribution(weeklySnapshots) {
1818
+ if (!weeklySnapshots || weeklySnapshots.length === 0) return {}
1819
+ const counts = { BUILD: 0, EDIT: 0, DEBUG: 0, MAINTAIN: 0, IDLE: 0 }
1820
+ for (const w of weeklySnapshots) {
1821
+ counts[w.mode] = (counts[w.mode] || 0) + 1
1822
+ }
1823
+ const total = weeklySnapshots.length
1824
+ const dist = {}
1825
+ for (const [mode, count] of Object.entries(counts)) {
1826
+ if (count > 0) dist[mode] = Math.round((count / total) * 100) / 100
1827
+ }
1828
+ return dist
1829
+ }
1830
+
1831
+ /** Mode-weighted yield — average yield weighted by mode distribution. */
1832
+ function _modeWeightedYield(weeklySnapshots) {
1833
+ if (!weeklySnapshots || weeklySnapshots.length === 0) return 0
1834
+ const total = weeklySnapshots.length
1835
+ const sum = weeklySnapshots.reduce((acc, w) => acc + (w.yield || 0), 0)
1836
+ return Math.round(sum / total)
1837
+ }
1838
+
1839
+ /** Peak yield across all weekly snapshots. */
1840
+ function _peakYield(weeklySnapshots) {
1841
+ if (!weeklySnapshots || weeklySnapshots.length === 0) return 0
1842
+ return Math.max(...weeklySnapshots.map(w => w.yield || 0))
1843
+ }
1844
+
1845
+ /** Health score (0-100) — weighted composite of consistency, momentum, quality. */
1846
+ function _healthScore(weeklySnapshots, modeWeightedYield) {
1847
+ if (!weeklySnapshots || weeklySnapshots.length === 0) return 0
1848
+ // Simplified Phase 1 formula:
1849
+ // 40% — consistency (how often in MAINTAIN)
1850
+ // 30% — momentum (recent yield vs older yield)
1851
+ // 30% — quality (mode-weighted yield relative to MAINTAIN expected)
1852
+ const dist = _modeDistribution(weeklySnapshots)
1853
+ const maintainShare = dist.MAINTAIN || 0
1854
+ const consistency = Math.min(maintainShare, 1) * 40
1855
+
1856
+ // Momentum: compare last 3 weeks to first 3 weeks
1857
+ const recent = weeklySnapshots.slice(-3)
1858
+ const older = weeklySnapshots.slice(0, 3)
1859
+ const recentAvg = recent.reduce((a, w) => a + (w.yield || 0), 0) / (recent.length || 1)
1860
+ const olderAvg = older.reduce((a, w) => a + (w.yield || 0), 0) / (older.length || 1)
1861
+ const momentumRatio = olderAvg > 0 ? recentAvg / olderAvg : 1
1862
+ const momentum = Math.min(momentumRatio, 2) / 2 * 30
1863
+
1864
+ // Quality: mode-weighted yield vs MAINTAIN expected (5000)
1865
+ const quality = Math.min(modeWeightedYield / 5000, 1) * 30
1866
+
1867
+ return Math.round(consistency + momentum + quality)
1868
+ }
1869
+
1870
+ /** Yield for the last N days (from weekly snapshots — approximates by summing recent weeks). */
1871
+ function _yieldForDays(weeklySnapshots, days) {
1872
+ if (!weeklySnapshots || weeklySnapshots.length === 0) return 0
1873
+ const weeksNeeded = Math.ceil(days / 7)
1874
+ const recent = weeklySnapshots.slice(-weeksNeeded)
1875
+ if (recent.length === 0) return 0
1876
+ const sum = recent.reduce((a, w) => a + (w.yield || 0), 0)
1877
+ return Math.round(sum / recent.length) // average weekly yield
1878
+ }
1879
+
1880
+ /** Trajectory description from 7d/30d/90d yields. */
1881
+ function _trajectory(y7, y30, y90) {
1882
+ if (y90 === 0) return 'insufficient_data'
1883
+ const r7v30 = y30 > 0 ? y7 / y30 : 1
1884
+ const r30v90 = y90 > 0 ? y30 / y90 : 1
1885
+ if (r7v30 > 1.2 && r30v90 > 1.0) return 'accelerating'
1886
+ if (r7v30 > 1.2) return 'recent_surge'
1887
+ if (r7v30 < 0.8 && r30v90 < 0.8) return 'declining'
1888
+ if (r7v30 < 0.8) return 'recent_dip'
1889
+ if (r30v90 > 1.1) return 'steady_growth'
1890
+ return 'stable'
1891
+ }
1892
+
1893
+ /** Phase pattern description from mode distribution over time. */
1894
+ function _phasePattern(weeklySnapshots) {
1895
+ if (!weeklySnapshots || weeklySnapshots.length < 2) return 'insufficient_data'
1896
+ const modes = weeklySnapshots.map(w => w.mode)
1897
+ const transitions = []
1898
+ for (let i = 1; i < modes.length; i++) {
1899
+ if (modes[i] !== modes[i - 1]) {
1900
+ transitions.push(`${modes[i - 1]}→${modes[i]}`)
1901
+ }
1902
+ }
1903
+ if (transitions.length === 0) return `consistent_${modes[0].toLowerCase()}`
1904
+ // Check for smooth transitions (BUILD→MAINTAIN without DEBUG)
1905
+ const smooth = transitions.every(t => !t.includes('DEBUG'))
1906
+ if (smooth && transitions.some(t => t === 'BUILD→MAINTAIN')) return 'smooth_ramp'
1907
+ if (transitions.filter(t => t.includes('DEBUG')).length > 2) return 'erratic'
1908
+ return 'cyclical'
1909
+ }
1910
+
1911
+ /** Daily assessment string. */
1912
+ function _dailyAssessment(mode, yieldVal, qualityScoreVal, expectedYield) {
1913
+ const qsPct = Math.round(qualityScoreVal * 100)
1914
+ if (mode === 'IDLE') return 'You\'re idle — no significant token activity today.'
1915
+ if (mode === 'MAINTAIN') {
1916
+ if (qualityScoreVal >= 0.5) return `You're in MAINTAIN mode. Yield ${yieldVal} is ${qsPct}% of MAINTAIN norm. The cascade is compounding.`
1917
+ return `You're in MAINTAIN mode but yield ${yieldVal} is only ${qsPct}% of expected (${expectedYield}). You may be coasting — push for more output.`
1918
+ }
1919
+ if (mode === 'BUILD') {
1920
+ if (qualityScoreVal >= 0.5) return `You're in BUILD mode. Yield ${yieldVal} is ${qsPct}% of BUILD norm. Greenfield work — expected to be low.`
1921
+ return `You're in BUILD mode. Yield ${yieldVal} is ${qsPct}% of expected (${expectedYield}). Building is slow — keep going.`
1922
+ }
1923
+ if (mode === 'EDIT') {
1924
+ if (qualityScoreVal >= 0.5) return `You're in EDIT mode. Yield ${yieldVal} is ${qsPct}% of EDIT norm. Fresh input but producing — good.`
1925
+ return `You're in EDIT mode. Yield ${yieldVal} is ${qsPct}% of expected (${expectedYield}). Push for more output per input.`
1926
+ }
1927
+ if (mode === 'DEBUG') {
1928
+ if (qualityScoreVal >= 0.5) return `You're in DEBUG mode. Yield ${yieldVal} is ${qsPct}% of DEBUG norm. Investigating — yield is expected to be low.`
1929
+ return `You're in DEBUG mode. Yield ${yieldVal} is ${qsPct}% of expected (${expectedYield}). Debugging is slow — consider loading prior context.`
1930
+ }
1931
+ return `Mode: ${mode}. Yield: ${yieldVal}. Quality: ${qsPct}%.`
1932
+ }
1933
+
1934
+ /** Daily advice string. */
1935
+ function _dailyAdvice(mode, yieldVal, qs) {
1936
+ if (mode === 'MAINTAIN' && qs >= 0.5) return 'Keep the cascade going. Don\'t reset context — let it compound.'
1937
+ if (mode === 'MAINTAIN') return 'You\'re in MAINTAIN but underperforming. Push the agent for more output per turn.'
1938
+ if (mode === 'BUILD') return 'When you\'re done building, load prior context to transition to MAINTAIN. The cascade rewards reuse.'
1939
+ if (mode === 'EDIT') return 'You\'re producing but using fresh input. Try to reuse prior context to boost leverage.'
1940
+ if (mode === 'DEBUG') return 'When you\'re done debugging, load prior context to return to MAINTAIN. Don\'t let the debug phase drag on.'
1941
+ if (mode === 'IDLE') return 'No activity detected. Start a session to build your cascade.'
1942
+ return 'Keep working — the cascade will compound as you build context.'
1943
+ }
1944
+
1945
+ /**
1946
+ * Compute the cascade report block for a submission payload.
1947
+ * Pure math — mode detection + badges + health score from the current pillars.
1948
+ * The server stores this as-is (does NOT recompute modes).
1949
+ * Weekly granularity is the privacy boundary — no daily modes in the report.
1950
+ */
1951
+ function _computeReportBlock(pillars) {
1952
+ const cas = cascade(pillars)
1953
+ const modeInfo = detectMode(pillars)
1954
+ const badges = computeBadges({
1955
+ pillars,
1956
+ cascade: cas,
1957
+ history: [], // no history available at submit time — badges computed from current pillars only
1958
+ isVerified: true, // submit_verified is the signed agent path
1959
+ rank: null,
1960
+ })
1961
+ return {
1962
+ current_mode: modeInfo.mode,
1963
+ mode_confidence: modeInfo.confidence,
1964
+ mode_distribution: { [modeInfo.mode]: 1.0 }, // single-window submission
1965
+ mode_weighted_yield: cas.yield ?? 0,
1966
+ peak_yield: cas.yield ?? 0,
1967
+ health_score: _healthScore([{ mode: modeInfo.mode, yield: cas.yield ?? 0 }], cas.yield ?? 0),
1968
+ badges,
1969
+ }
1970
+ }