@axiomatic-labs/claudeflow 2.13.34 → 2.13.35

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 +41 -6
  2. package/package.json +1 -1
package/lib/panel.js CHANGED
@@ -385,6 +385,7 @@ function listLogEvents(cwd, { limit = 200, before = null, type = 'all' } = {}) {
385
385
  if (type !== 'all') {
386
386
  if (type === 'enforcement' && !r.type.startsWith('enforcement')) continue;
387
387
  if (type === 'reminder' && !r.type.startsWith('reminder')) continue;
388
+ if (type === 'blocked' && r.type !== 'enforcement-blocked') continue;
388
389
  }
389
390
  // Strip large `content` from list view; clients fetch it via /api/logs/:id
390
391
  // Old sidecars (pre-token-estimate) lack `contentTokens`. Backfill on
@@ -408,6 +409,8 @@ function listLogEvents(cwd, { limit = 200, before = null, type = 'all' } = {}) {
408
409
  capturedFromStdout: r.capturedFromStdout === true,
409
410
  stdoutTruncated: r.stdoutTruncated === true,
410
411
  parseError: r.parseError || null,
412
+ decision: r.decision || null,
413
+ reason: r.reason || null,
411
414
  });
412
415
  }
413
416
  return { entries, total, thresholds };
@@ -452,11 +455,16 @@ function getLogsInfo(cwd) {
452
455
  let mostRecent = null;
453
456
  let latestReminderTokens = null;
454
457
  let latestReminderStatus = null;
458
+ let recentBlockedCount = 0;
455
459
  try {
456
460
  const files = fs.readdirSync(dir).filter((f) => f.endsWith('.json')).sort();
457
461
  total = files.length;
458
- // Walk from newest to find both latest event timestamp and latest reminder.
459
- for (let i = files.length - 1; i >= 0 && (mostRecent === null || latestReminderTokens === null); i--) {
462
+ // Walk from newest. Count blocks within the last 50 events for the
463
+ // sidebar severity. Latest-reminder lookup short-circuits when both
464
+ // mostRecent + latestReminderTokens are populated AND we've scanned
465
+ // enough for the block tally.
466
+ const scanCap = Math.min(50, files.length);
467
+ for (let i = files.length - 1; i >= files.length - scanCap; i--) {
460
468
  try {
461
469
  const r = JSON.parse(fs.readFileSync(path.join(dir, files[i]), 'utf8'));
462
470
  if (mostRecent === null) mostRecent = r.timestamp;
@@ -467,6 +475,7 @@ function getLogsInfo(cwd) {
467
475
  latestReminderTokens = tokens;
468
476
  latestReminderStatus = classifyTokens(tokens, thresholds);
469
477
  }
478
+ if (r.type === 'enforcement-blocked') recentBlockedCount++;
470
479
  } catch {}
471
480
  }
472
481
  } catch {}
@@ -477,6 +486,7 @@ function getLogsInfo(cwd) {
477
486
  thresholds,
478
487
  latestReminderTokens,
479
488
  latestReminderStatus,
489
+ recentBlockedCount,
480
490
  };
481
491
  }
482
492
 
@@ -638,6 +648,10 @@ details[open] summary { padding-bottom: 8px; border-bottom: 1px solid var(--bord
638
648
  .logs-status { font-family: var(--mono); font-size: 12px; white-space: nowrap; }
639
649
  .logs-status.injected { color: #58a6ff; }
640
650
  .logs-status.silenced { color: var(--err); font-weight: 600; }
651
+ .logs-status.blocked { color: var(--err); font-weight: 700; }
652
+ .logs-table tr.blocked .logs-handler { color: var(--err); }
653
+ .logs-table tr.blocked td:first-child { border-left: 3px solid var(--err) !important; }
654
+ .logs-reason-preview { font-size: 11px; color: var(--err); margin-top: 2px; max-width: 480px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
641
655
  .token-badge { display: inline-block; padding: 1px 7px; border-radius: 999px; font-size: 10px; font-family: var(--mono); margin-left: 6px; vertical-align: middle; }
642
656
  .token-badge.heavy { background: rgba(210,153,34,0.18); color: var(--warn); }
643
657
  .token-badge.high { background: rgba(248,81,73,0.20); color: var(--err); }
@@ -792,6 +806,7 @@ function severityFor(id) {
792
806
  case 'reminders': return s.reminders && s.reminders.disabledCount > 0 ? 'info' : 'ok';
793
807
  case 'logs': {
794
808
  if (!s.logs) return 'info';
809
+ if (s.logs.recentBlockedCount > 0) return 'err';
795
810
  if (s.logs.latestReminderStatus === 'high') return 'err';
796
811
  if (s.logs.latestReminderStatus === 'heavy') return 'warn';
797
812
  return s.logs.total > 0 ? 'ok' : 'info';
@@ -1028,6 +1043,17 @@ async function viewLogDetail(id) {
1028
1043
  : '<span class="badge warn" title="No content was captured — the hook may have crashed before writing or returned null.">⚠ no stdout</span>');
1029
1044
  html += '<h4 style="margin-top:14px">Provenance ' + provBadge + (ev.stdoutTruncated ? ' <span class="badge warn">⚠ truncated</span>' : '') + '</h4>';
1030
1045
  }
1046
+ // Enforcement-blocked detail: show decision + full reason verbatim.
1047
+ if (ev.type === 'enforcement-blocked') {
1048
+ html += '<h4 style="margin-top:14px">Decision: <span class="badge err">' + escapeHtml(ev.decision || 'block').toUpperCase() + '</span></h4>';
1049
+ if (ev.reason) {
1050
+ html += '<h4 style="margin-top:14px">Reason</h4>';
1051
+ html += '<pre style="border-color: rgba(248,81,73,0.5)">' + escapeHtml(ev.reason) + '</pre>';
1052
+ }
1053
+ if (ev.capturedStdout) {
1054
+ html += '<details style="margin-top:14px"><summary>Raw stdout (' + fmtBytes(ev.stdoutBytes || 0) + ')</summary><pre>' + escapeHtml(ev.capturedStdout) + '</pre></details>';
1055
+ }
1056
+ }
1031
1057
  if (typeof ev.contentBytes === 'number' && ev.contentBytes > 0) {
1032
1058
  const tokens = typeof ev.contentTokens === 'number' ? ev.contentTokens : Math.round(ev.contentBytes / 4);
1033
1059
  const th = (state.logs && state.logs.thresholds) || { heavy: 8000, high: 20000 };
@@ -1092,11 +1118,14 @@ function renderLogs() {
1092
1118
  const rows = logsState.entries.map((e) => {
1093
1119
  const isReminder = e.type.startsWith('reminder');
1094
1120
  const isSilenced = e.type === 'enforcement-silenced';
1121
+ const isBlocked = e.type === 'enforcement-blocked';
1095
1122
  const badge = isReminder
1096
1123
  ? '<span class="badge info" style="background:rgba(88,166,255,0.18); color:#58a6ff">REMINDER</span>'
1097
1124
  : '<span class="badge err">ENFORCEMENT</span>';
1098
1125
  const statusCell = isSilenced
1099
1126
  ? '<span class="logs-status silenced" title="Master Enforcement OFF silenced this hook — it did NOT run. Claude Code received no output for this event.">✗ SILENCED</span>'
1127
+ : isBlocked
1128
+ ? '<span class="logs-status blocked" title="Hook ran and BLOCKED this action. Claude Code rejected the tool call. Click View for the full reason.">⛔ BLOCKED</span>'
1100
1129
  : '<span class="logs-status injected" title="Hook ran and emitted additionalContext to Claude Code">✓ injected</span>';
1101
1130
  const tokenStatus = e.tokenStatus || 'none';
1102
1131
  const tokenBadge = tokenStatus === 'high'
@@ -1110,13 +1139,18 @@ function renderLogs() {
1110
1139
  const bytesStr = e.contentBytes ? fmtBytes(e.contentBytes) : '';
1111
1140
  const tokensTitle = e.contentBytes
1112
1141
  ? \`≈\${e.contentTokens} tokens (chars/4 heuristic) · \${fmtBytes(e.contentBytes)} raw · status: \${tokenStatus}\`
1113
- : 'No content (hook was silenced — never ran)';
1114
- const rowCls = \`token-\${tokenStatus}\${isSilenced ? ' silenced' : ''}\`;
1142
+ : isBlocked
1143
+ ? 'Hook BLOCKED the action. Click View for full reason.'
1144
+ : 'No content (hook was silenced — never ran)';
1145
+ const rowCls = \`token-\${tokenStatus}\${isSilenced ? ' silenced' : ''}\${isBlocked ? ' blocked' : ''}\`;
1146
+ const reasonPreview = isBlocked && e.reason
1147
+ ? \`<div class="logs-reason-preview" title="\${escapeHtml(e.reason)}">\${escapeHtml(e.reason.split('\\n')[0].slice(0, 120))}</div>\`
1148
+ : '';
1115
1149
  return \`<tr class="\${rowCls}">
1116
1150
  <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>
1117
1151
  <td>\${badge}</td>
1118
1152
  <td>\${statusCell}</td>
1119
- <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>
1153
+ <td class="logs-handler"><span class="muted">\${escapeHtml(e.event)}</span><br>\${escapeHtml(e.handler)}\${e.matcher ? \` <span class="muted">(\${escapeHtml(e.matcher)})</span>\` : ''}\${reasonPreview}</td>
1120
1154
  <td class="logs-size" title="\${escapeHtml(tokensTitle)}">\${tokensStr}\${tokenBadge}\${bytesStr ? \`<br><span class="muted" style="font-size:11px">\${bytesStr}</span>\` : ''}</td>
1121
1155
  <td><button class="logs-view" data-log-id="\${escapeHtml(e.id)}">View</button></td>
1122
1156
  </tr>\`;
@@ -1136,7 +1170,8 @@ function renderLogs() {
1136
1170
  <select id="logs-filter">
1137
1171
  <option value="all" \${logsState.type === 'all' ? 'selected' : ''}>All</option>
1138
1172
  <option value="reminder" \${logsState.type === 'reminder' ? 'selected' : ''}>Reminders only</option>
1139
- <option value="enforcement" \${logsState.type === 'enforcement' ? 'selected' : ''}>Enforcement-silenced only</option>
1173
+ <option value="enforcement" \${logsState.type === 'enforcement' ? 'selected' : ''}>Enforcement (silenced + blocked)</option>
1174
+ <option value="blocked" \${logsState.type === 'blocked' ? 'selected' : ''}>Blocked actions only</option>
1140
1175
  </select>
1141
1176
  </label>
1142
1177
  <button id="logs-reload">Reload</button>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axiomatic-labs/claudeflow",
3
- "version": "2.13.34",
3
+ "version": "2.13.35",
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"