@axiomatic-labs/claudeflow 2.13.30 → 2.13.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/lib/panel.js +132 -14
  2. package/package.json +1 -1
package/lib/panel.js CHANGED
@@ -309,6 +309,39 @@ function getRemindersInfo(cwd) {
309
309
  };
310
310
  }
311
311
 
312
+ // ─── token-budget thresholds ─────────────────────────────────────
313
+ //
314
+ // Anthropic does NOT publish official limits for per-turn additionalContext.
315
+ // These defaults are heuristics derived from:
316
+ // - GitHub issue anthropics/claude-code#45188 (system prompt 100K+ tokens
317
+ // made sessions unusable without /compact)
318
+ // - Community guidance: keep injected per-turn content well under 10–15%
319
+ // of the 200K context window so user content + tool results have room
320
+ // - Practical observation that 5–10K tokens for project rules is common
321
+ //
322
+ // Override per project via env vars before launching `claudeflow panel`:
323
+ // CLAUDEFLOW_TOKEN_HEAVY=8000 (yellow threshold)
324
+ // CLAUDEFLOW_TOKEN_HIGH=20000 (red threshold)
325
+ const TOKEN_HEAVY_DEFAULT = 8000;
326
+ const TOKEN_HIGH_DEFAULT = 20000;
327
+
328
+ function tokenThresholds() {
329
+ const heavy = parseInt(process.env.CLAUDEFLOW_TOKEN_HEAVY, 10);
330
+ const high = parseInt(process.env.CLAUDEFLOW_TOKEN_HIGH, 10);
331
+ return {
332
+ heavy: Number.isFinite(heavy) && heavy > 0 ? heavy : TOKEN_HEAVY_DEFAULT,
333
+ high: Number.isFinite(high) && high > 0 ? high : TOKEN_HIGH_DEFAULT,
334
+ };
335
+ }
336
+
337
+ function classifyTokens(tokens, thresholds = tokenThresholds()) {
338
+ if (!tokens || tokens <= 0) return 'none';
339
+ if (tokens >= thresholds.high) return 'high';
340
+ if (tokens >= thresholds.heavy) return 'heavy';
341
+ if (tokens < 2000) return 'light';
342
+ return 'normal';
343
+ }
344
+
312
345
  // ─── log events (Logs tab) ───────────────────────────────────────
313
346
  //
314
347
  // Sidecar files live in `.claude/tmp/log-events/<id>.json`. Each is one
@@ -331,10 +364,11 @@ function listLogEvents(cwd, { limit = 200, before = null, type = 'all' } = {}) {
331
364
  try {
332
365
  files = fs.readdirSync(dir).filter((f) => f.endsWith('.json'));
333
366
  } catch {
334
- return { entries: [], total: 0 };
367
+ return { entries: [], total: 0, thresholds: tokenThresholds() };
335
368
  }
336
369
  files.sort().reverse(); // filenames begin with sortable timestamp; newest first
337
370
  const total = files.length;
371
+ const thresholds = tokenThresholds();
338
372
 
339
373
  let beforeIdx = 0;
340
374
  if (before) {
@@ -369,10 +403,14 @@ function listLogEvents(cwd, { limit = 200, before = null, type = 'all' } = {}) {
369
403
  summary: r.summary,
370
404
  contentBytes: r.contentBytes || 0,
371
405
  contentTokens: tokens,
406
+ tokenStatus: classifyTokens(tokens, thresholds),
372
407
  activeReminderIds: r.activeReminderIds || null,
408
+ capturedFromStdout: r.capturedFromStdout === true,
409
+ stdoutTruncated: r.stdoutTruncated === true,
410
+ parseError: r.parseError || null,
373
411
  });
374
412
  }
375
- return { entries, total };
413
+ return { entries, total, thresholds };
376
414
  }
377
415
 
378
416
  function readLogEvent(cwd, id) {
@@ -386,19 +424,37 @@ function readLogEvent(cwd, id) {
386
424
 
387
425
  function getLogsInfo(cwd) {
388
426
  const dir = logEventsDir(cwd);
427
+ const thresholds = tokenThresholds();
389
428
  let total = 0;
390
429
  let mostRecent = null;
430
+ let latestReminderTokens = null;
431
+ let latestReminderStatus = null;
391
432
  try {
392
433
  const files = fs.readdirSync(dir).filter((f) => f.endsWith('.json')).sort();
393
434
  total = files.length;
394
- if (total > 0) {
435
+ // Walk from newest to find both latest event timestamp and latest reminder.
436
+ for (let i = files.length - 1; i >= 0 && (mostRecent === null || latestReminderTokens === null); i--) {
395
437
  try {
396
- const r = JSON.parse(fs.readFileSync(path.join(dir, files[files.length - 1]), 'utf8'));
397
- mostRecent = r.timestamp;
438
+ const r = JSON.parse(fs.readFileSync(path.join(dir, files[i]), 'utf8'));
439
+ if (mostRecent === null) mostRecent = r.timestamp;
440
+ if (latestReminderTokens === null && r.type && r.type.startsWith('reminder')) {
441
+ const tokens = typeof r.contentTokens === 'number'
442
+ ? r.contentTokens
443
+ : (r.contentBytes ? Math.round(r.contentBytes / 4) : 0);
444
+ latestReminderTokens = tokens;
445
+ latestReminderStatus = classifyTokens(tokens, thresholds);
446
+ }
398
447
  } catch {}
399
448
  }
400
449
  } catch {}
401
- return { available: true, total, mostRecent };
450
+ return {
451
+ available: true,
452
+ total,
453
+ mostRecent,
454
+ thresholds,
455
+ latestReminderTokens,
456
+ latestReminderStatus,
457
+ };
402
458
  }
403
459
 
404
460
  function getDoctorInfo(cwd) {
@@ -546,6 +602,14 @@ details[open] summary { padding-bottom: 8px; border-bottom: 1px solid var(--bord
546
602
  .logs-size { font-family: var(--mono); font-size: 12px; color: var(--muted); white-space: nowrap; }
547
603
  .logs-view { background: var(--panel-2); border: 1px solid var(--border); color: var(--fg); padding: 4px 12px; border-radius: 5px; font: inherit; font-size: 12px; cursor: pointer; }
548
604
  .logs-view:hover { border-color: var(--accent); }
605
+ .logs-table tr.token-light td:first-child { border-left: 3px solid #3fb950; }
606
+ .logs-table tr.token-normal td:first-child { border-left: 3px solid var(--border); }
607
+ .logs-table tr.token-heavy td:first-child { border-left: 3px solid var(--warn); }
608
+ .logs-table tr.token-high td:first-child { border-left: 3px solid var(--err); }
609
+ .token-badge { display: inline-block; padding: 1px 7px; border-radius: 999px; font-size: 10px; font-family: var(--mono); margin-left: 6px; vertical-align: middle; }
610
+ .token-badge.heavy { background: rgba(210,153,34,0.18); color: var(--warn); }
611
+ .token-badge.high { background: rgba(248,81,73,0.20); color: var(--err); }
612
+ .token-badge.light { background: rgba(63,185,80,0.18); color: var(--ok); }
549
613
  #log-drawer { display: none; position: fixed; top: 0; right: 0; bottom: 0; width: 60%; max-width: 920px; background: var(--panel); border-left: 1px solid var(--border); box-shadow: -8px 0 24px rgba(0,0,0,0.4); z-index: 100; overflow-y: auto; }
550
614
  #log-drawer .drawer-head { display: flex; justify-content: space-between; align-items: center; padding: 14px 22px; border-bottom: 1px solid var(--border); position: sticky; top: 0; background: var(--panel); }
551
615
  #log-drawer .drawer-head .title { font-weight: 600; }
@@ -694,7 +758,12 @@ function severityFor(id) {
694
758
  case 'claudeMd': return !s.claudeMd.exists ? 'err' : (s.claudeMd.optOut ? 'info' : 'ok');
695
759
  case 'hooks': return s.hooks.warnings > 0 ? 'warn' : 'ok';
696
760
  case 'reminders': return s.reminders && s.reminders.disabledCount > 0 ? 'info' : 'ok';
697
- case 'logs': return s.logs && s.logs.total > 0 ? 'ok' : 'info';
761
+ case 'logs': {
762
+ if (!s.logs) return 'info';
763
+ if (s.logs.latestReminderStatus === 'high') return 'err';
764
+ if (s.logs.latestReminderStatus === 'heavy') return 'warn';
765
+ return s.logs.total > 0 ? 'ok' : 'info';
766
+ }
698
767
  case 'mcp': return !s.mcp.configFound ? 'info' : (s.mcp.playwright.match && s.mcp.staleLockfiles.length === 0 ? 'ok' : 'warn');
699
768
  case 'setupContext': return !s.setupContext.exists ? 'err' : (s.setupContext.toolingComplete ? 'ok' : 'warn');
700
769
  case 'activeRun': return s.activeRun.active ? 'info' : 'info';
@@ -767,9 +836,17 @@ function renderOverview() {
767
836
  const ef = s.enforcement && s.enforcement.available
768
837
  ? (s.enforcement.on ? \`ON (\${s.enforcement.count} blocking hooks)\` : \`OFF — \${s.enforcement.count} blocking hooks silenced\`)
769
838
  : 'unavailable';
839
+ const reminderTokens = s.logs && s.logs.latestReminderTokens;
840
+ const reminderStatus = s.logs && s.logs.latestReminderStatus;
770
841
  const lg = s.logs && s.logs.available
771
- ? \`\${s.logs.total} captured\${s.logs.mostRecent ? ' · last ' + fmtRelativeTime(s.logs.mostRecent) : ''}\`
842
+ ? \`\${s.logs.total} captured\${s.logs.mostRecent ? ' · last ' + fmtRelativeTime(s.logs.mostRecent) : ''}\${reminderTokens ? ' · latest reminder ≈' + reminderTokens.toLocaleString() + ' tokens' + (reminderStatus === 'heavy' ? ' ⚠ heavy' : reminderStatus === 'high' ? ' ✗ HIGH' : '') : ''}\`
772
843
  : 'no events';
844
+ const logsKind = (() => {
845
+ if (!s.logs || !s.logs.available) return 'info';
846
+ if (reminderStatus === 'high') return 'err';
847
+ if (reminderStatus === 'heavy') return 'warn';
848
+ return s.logs.total > 0 ? 'ok' : 'info';
849
+ })();
773
850
  return \`<h2>Overview</h2>
774
851
  <p class="sub">Snapshot at \${new Date(s.timestamp).toLocaleTimeString()}</p>
775
852
  <div class="card">
@@ -781,7 +858,7 @@ function renderOverview() {
781
858
  \${row('Active run', ar, { kind: 'info', text: s.activeRun.active ? '·' : '·' })}
782
859
  \${row('Reminders', rm, { kind: s.reminders && s.reminders.available ? (s.reminders.disabledCount ? 'info' : 'ok') : 'err', text: s.reminders && s.reminders.disabledCount ? '·' : '✓' })}
783
860
  \${row('Enforcement', ef, { kind: s.enforcement && s.enforcement.available ? (s.enforcement.on ? 'ok' : 'err') : 'err', text: s.enforcement && s.enforcement.available ? (s.enforcement.on ? '✓' : '✗') : '·' })}
784
- \${row('Logs', lg, { kind: s.logs && s.logs.total > 0 ? 'ok' : 'info', text: '·' })}
861
+ \${row('Logs', lg, { kind: logsKind, text: logsKind === 'err' ? '' : logsKind === 'warn' ? '!' : '·' })}
785
862
  \${row('Doctor', dc, { kind: s.doctor.issueCount === 0 ? 'ok' : 'warn', text: s.doctor.issueCount === 0 ? '✓' : '!' })}
786
863
  </div>\`;
787
864
  }
@@ -908,9 +985,38 @@ async function viewLogDetail(id) {
908
985
  html += '<h4 style="margin-top:14px">Active reminders at fire time</h4>';
909
986
  html += '<p class="sub">' + ev.activeReminderIds.map(escapeHtml).join(', ') + '</p>';
910
987
  }
988
+ // Provenance: did we authentically capture the hook's stdout, or is the
989
+ // record empty / parsed-failed / truncated? Make this visible at the top
990
+ // of the drawer so the user can trust (or distrust) the content shown.
991
+ if (typeof ev.capturedFromStdout === 'boolean') {
992
+ const provBadge = ev.capturedFromStdout
993
+ ? '<span class="badge ok" title="Bytes captured directly from the hook&#39;s stdout — this is the actual additionalContext that Claude Code received.">✓ stdout-captured</span>'
994
+ : (ev.parseError
995
+ ? '<span class="badge err" title="Hook stdout was captured but JSON.parse failed: ' + escapeHtml(ev.parseError) + '">✗ parse failed</span>'
996
+ : '<span class="badge warn" title="No content was captured — the hook may have crashed before writing or returned null.">⚠ no stdout</span>');
997
+ html += '<h4 style="margin-top:14px">Provenance ' + provBadge + (ev.stdoutTruncated ? ' <span class="badge warn">⚠ truncated</span>' : '') + '</h4>';
998
+ }
911
999
  if (typeof ev.contentBytes === 'number' && ev.contentBytes > 0) {
912
1000
  const tokens = typeof ev.contentTokens === 'number' ? ev.contentTokens : Math.round(ev.contentBytes / 4);
913
- html += '<h4 style="margin-top:14px">Injected content ≈' + tokens.toLocaleString() + ' tokens <span class="muted" style="font-size:11px">(approx · ' + fmtBytes(ev.contentBytes) + ' raw)</span></h4>';
1001
+ const th = (state.logs && state.logs.thresholds) || { heavy: 8000, high: 20000 };
1002
+ const cls = tokens >= th.high ? 'high' : tokens >= th.heavy ? 'heavy' : tokens < 2000 ? 'light' : 'normal';
1003
+ const statusBadge = cls === 'high'
1004
+ ? '<span class="token-badge high">⚠ HIGH</span>'
1005
+ : cls === 'heavy'
1006
+ ? '<span class="token-badge heavy">⚠ HEAVY</span>'
1007
+ : '';
1008
+ html += '<h4 style="margin-top:14px">Injected content — ≈' + tokens.toLocaleString() + ' tokens ' + statusBadge + ' <span class="muted" style="font-size:11px">(approx · ' + fmtBytes(ev.contentBytes) + ' raw)</span></h4>';
1009
+ if (cls === 'heavy' || cls === 'high') {
1010
+ const advice = cls === 'high'
1011
+ ? 'This reminder exceeds <strong>' + th.high.toLocaleString() + ' tokens</strong> — eats meaningful context budget. Strongly consider:'
1012
+ : 'This reminder exceeds <strong>' + th.heavy.toLocaleString() + ' tokens</strong> — heavy. Consider:';
1013
+ html += '<div class="card" style="border-color:' + (cls === 'high' ? 'rgba(248,81,73,0.4)' : 'rgba(210,153,34,0.4)') + '"><p>' + advice + '</p>';
1014
+ html += '<ul style="margin:6px 0 0 18px; padding:0">';
1015
+ html += '<li>Trim CLAUDE.md (currently the largest contributor) — see the CLAUDE.md tab</li>';
1016
+ html += '<li>Disable individual reminder IDs you don\\\'t need in the Reminders tab</li>';
1017
+ html += '<li>Tune the threshold via <code>CLAUDEFLOW_TOKEN_HEAVY</code> / <code>CLAUDEFLOW_TOKEN_HIGH</code> env vars if your project genuinely needs more</li>';
1018
+ html += '</ul></div>';
1019
+ }
914
1020
  }
915
1021
  if (typeof ev.content === 'string' && ev.content.length > 0) {
916
1022
  html += '<pre>' + escapeHtml(ev.content) + '</pre>';
@@ -950,23 +1056,30 @@ function renderLogs() {
950
1056
  loadLogsPage();
951
1057
  }
952
1058
  const totalLabel = state.logs && state.logs.total != null ? \`\${state.logs.total} total events\` : '';
1059
+ const thresholds = (state.logs && state.logs.thresholds) || { heavy: 8000, high: 20000 };
953
1060
  const rows = logsState.entries.map((e) => {
954
1061
  const isReminder = e.type.startsWith('reminder');
955
1062
  const badge = isReminder
956
1063
  ? '<span class="badge info" style="background:rgba(88,166,255,0.18); color:#58a6ff">REMINDER</span>'
957
1064
  : '<span class="badge err">ENFORCEMENT</span>';
1065
+ const tokenStatus = e.tokenStatus || 'none';
1066
+ const tokenBadge = tokenStatus === 'high'
1067
+ ? '<span class="token-badge high" title="High — exceeds the high-water threshold (' + thresholds.high.toLocaleString() + ' tokens). Consider trimming.">⚠ HIGH</span>'
1068
+ : tokenStatus === 'heavy'
1069
+ ? '<span class="token-badge heavy" title="Heavy — exceeds the heavy threshold (' + thresholds.heavy.toLocaleString() + ' tokens). Consider trimming.">⚠ HEAVY</span>'
1070
+ : '';
958
1071
  const tokensStr = e.contentTokens
959
1072
  ? \`≈\${e.contentTokens.toLocaleString()}\`
960
1073
  : '—';
961
1074
  const bytesStr = e.contentBytes ? fmtBytes(e.contentBytes) : '';
962
1075
  const tokensTitle = e.contentBytes
963
- ? \`≈\${e.contentTokens} tokens (approx, chars/4 heuristic) · \${fmtBytes(e.contentBytes)} raw\`
1076
+ ? \`≈\${e.contentTokens} tokens (chars/4 heuristic) · \${fmtBytes(e.contentBytes)} raw · status: \${tokenStatus}\`
964
1077
  : 'No content (enforcement-silenced)';
965
- return \`<tr>
1078
+ return \`<tr class="token-\${tokenStatus}">
966
1079
  <td class="logs-time"><span title="\${escapeHtml(e.timestamp)}">\${escapeHtml(fmtLocalTime(e.timestamp))}</span><br><span class="muted" style="font-size:11px">\${fmtRelativeTime(e.timestamp)}</span></td>
967
1080
  <td>\${badge}</td>
968
1081
  <td class="logs-handler"><span class="muted">\${escapeHtml(e.event)}</span><br>\${escapeHtml(e.handler)}\${e.matcher ? \` <span class="muted">(\${escapeHtml(e.matcher)})</span>\` : ''}</td>
969
- <td class="logs-size" title="\${escapeHtml(tokensTitle)}">\${tokensStr}\${bytesStr ? \`<br><span class="muted" style="font-size:11px">\${bytesStr}</span>\` : ''}</td>
1082
+ <td class="logs-size" title="\${escapeHtml(tokensTitle)}">\${tokensStr}\${tokenBadge}\${bytesStr ? \`<br><span class="muted" style="font-size:11px">\${bytesStr}</span>\` : ''}</td>
970
1083
  <td><button class="logs-view" data-log-id="\${escapeHtml(e.id)}">View</button></td>
971
1084
  </tr>\`;
972
1085
  }).join('');
@@ -975,6 +1088,11 @@ function renderLogs() {
975
1088
  : '';
976
1089
  return \`<h2>Logs</h2>
977
1090
  <p class="sub">Real-time trace of every reminder injection and enforcement-silenced hook. \${totalLabel} · sidecars in <code>.claude/tmp/log-events/</code> (capped at 2000, oldest evicted).</p>
1091
+ <p class="sub muted" style="font-size:11px">
1092
+ Token-budget thresholds: <strong style="color:var(--warn)">heavy ≥\${thresholds.heavy.toLocaleString()}</strong> · <strong style="color:var(--err)">high ≥\${thresholds.high.toLocaleString()}</strong>.
1093
+ Heuristic — Anthropic doesn't publish official limits for per-turn additionalContext.
1094
+ Override via <code>CLAUDEFLOW_TOKEN_HEAVY</code> / <code>CLAUDEFLOW_TOKEN_HIGH</code> env vars.
1095
+ </p>
978
1096
  <div class="logs-controls">
979
1097
  <label>Filter:
980
1098
  <select id="logs-filter">
@@ -986,7 +1104,7 @@ function renderLogs() {
986
1104
  <button id="logs-reload">Reload</button>
987
1105
  </div>
988
1106
  \${empty || \`<table class="logs-table">
989
- <thead><tr><th>Time</th><th>Type</th><th>Handler</th><th title="Approximate token count (chars/4 heuristic — within ±10–15% of Claude's actual tokenizer for markdown/code mix)">≈ Tokens</th><th></th></tr></thead>
1107
+ <thead><tr><th>Time</th><th>Type</th><th>Handler</th><th title="Approximate token count (chars/4 heuristic, ±10–15% vs Claude's real tokenizer). Heavy ≥\${thresholds.heavy.toLocaleString()} → yellow border. High ≥\${thresholds.high.toLocaleString()} → red border. Anthropic does not publish official thresholds for per-turn additionalContext.">≈ Tokens</th><th></th></tr></thead>
990
1108
  <tbody>\${rows}</tbody>
991
1109
  </table>\`}
992
1110
  \${logsState.allLoaded || logsState.entries.length === 0 ? '' : '<div style="margin-top:12px"><button id="logs-load-more">Load older</button></div>'}\`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axiomatic-labs/claudeflow",
3
- "version": "2.13.30",
3
+ "version": "2.13.32",
4
4
  "description": "Claudeflow — AI-powered development toolkit for Claude Code. Skills, agents, hooks, and quality gates that ship production apps.",
5
5
  "bin": {
6
6
  "claudeflow": "./bin/cli.js"