@axiomatic-labs/claudeflow 2.13.75 → 2.13.77

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 +143 -0
  2. package/package.json +1 -1
package/lib/panel.js CHANGED
@@ -286,6 +286,29 @@ function getEnforcementInfo(cwd) {
286
286
  };
287
287
  }
288
288
 
289
+ function getValidatorsInfo(cwd) {
290
+ const file = path.join(cwd, '.claudeflow', 'tmp', 'validators-state.json');
291
+ let state = null;
292
+ try {
293
+ const raw = fs.readFileSync(file, 'utf8');
294
+ if (raw.trim()) state = JSON.parse(raw);
295
+ } catch {}
296
+ if (!state || !Array.isArray(state.runs)) {
297
+ return { ok: true, available: false, updated_at: null, runs: [], stats: { total: 0, pass: 0, block: 0, skip: 0, error: 0 } };
298
+ }
299
+ const stats = { total: state.runs.length, pass: 0, block: 0, skip: 0, error: 0 };
300
+ for (const r of state.runs) {
301
+ if (stats[r.status] !== undefined) stats[r.status] += 1;
302
+ }
303
+ return {
304
+ ok: true,
305
+ available: true,
306
+ updated_at: state.updated_at || null,
307
+ runs: state.runs,
308
+ stats,
309
+ };
310
+ }
311
+
289
312
  function getObserverInfo(cwd) {
290
313
  // Read both the canonical state file (full snapshot) AND the summary file
291
314
  // (which is what hooks consume — has the incident filter applied).
@@ -639,6 +662,7 @@ function collectStatus(cwd) {
639
662
  runToCompletion: getRunToCompletionInfo(cwd),
640
663
  cwdLock: getCwdLockInfo(cwd),
641
664
  observer: getObserverInfo(cwd),
665
+ validators: getValidatorsInfo(cwd),
642
666
  activePlan: getActivePlanInfo(cwd),
643
667
  logs: getLogsInfo(cwd),
644
668
  doctor: getDoctorInfo(cwd),
@@ -848,6 +872,7 @@ const SECTIONS = [
848
872
  { id: 'reminders', label: 'Reminders' },
849
873
  { id: 'logs', label: 'Logs' },
850
874
  { id: 'observer', label: 'Observer' },
875
+ { id: 'validators', label: 'Validators' },
851
876
  { id: 'activePlan', label: 'Active plan' },
852
877
  { id: 'mcp', label: 'MCP & observer' },
853
878
  { id: 'setupContext', label: 'Setup context' },
@@ -859,6 +884,8 @@ let state = null;
859
884
  let active = 'overview';
860
885
  let observerFilter = 'all';
861
886
  let observerShowResolved = false;
887
+ let validatorsFilter = 'all';
888
+ let validatorsStatusFilter = 'all';
862
889
  let timer = null;
863
890
 
864
891
  // Preserve which <details> were open across re-renders (auto-refresh would
@@ -1061,6 +1088,7 @@ function renderContent() {
1061
1088
  reminders: renderReminders,
1062
1089
  logs: renderLogs,
1063
1090
  observer: renderObserver,
1091
+ validators: renderValidators,
1064
1092
  activePlan: renderActivePlan,
1065
1093
  mcp: renderMcp,
1066
1094
  setupContext: renderSetup,
@@ -1632,6 +1660,78 @@ function renderObserver() {
1632
1660
  <div class="card" style="padding:0;">\${incidentsBlock}</div>\`;
1633
1661
  }
1634
1662
 
1663
+ function renderValidators() {
1664
+ const v = (state && state.validators) || { available: false, runs: [], stats: { total: 0, pass: 0, block: 0, skip: 0, error: 0 } };
1665
+ if (!v.available || v.runs.length === 0) {
1666
+ return \`<h2>Validators</h2><p class="muted">No validator runs recorded yet. The security-scan and validate-file hooks log here on every Stop / SubagentStop / TaskCompleted event.</p>\`;
1667
+ }
1668
+
1669
+ const validators = [...new Set(v.runs.map((r) => r.validator))].sort();
1670
+ const validatorPill = (label, value, count) => {
1671
+ const a = (validatorsFilter === value);
1672
+ const cls = a ? 'pill pill-active' : 'pill';
1673
+ return \`<button class="\${cls}" data-validators-filter="\${escapeHtml(value)}">\${escapeHtml(label)}<span class="muted" style="margin-left:6px;font-size:11px;">\${count}</span></button>\`;
1674
+ };
1675
+ const statusPill = (label, value, count) => {
1676
+ const a = (validatorsStatusFilter === value);
1677
+ const cls = a ? 'pill pill-active' : 'pill';
1678
+ return \`<button class="\${cls}" data-validators-status-filter="\${escapeHtml(value)}">\${escapeHtml(label)}<span class="muted" style="margin-left:6px;font-size:11px;">\${count}</span></button>\`;
1679
+ };
1680
+
1681
+ const filteredByValidator = validatorsFilter === 'all'
1682
+ ? v.runs
1683
+ : v.runs.filter((r) => r.validator === validatorsFilter);
1684
+ const filtered = validatorsStatusFilter === 'all'
1685
+ ? filteredByValidator
1686
+ : filteredByValidator.filter((r) => r.status === validatorsStatusFilter);
1687
+
1688
+ const STATUS_BADGE = {
1689
+ pass: '<span style="color:#3fb950;font-weight:600;">pass</span>',
1690
+ block: '<span style="color:#f85149;font-weight:600;">block</span>',
1691
+ skip: '<span class="muted">skip</span>',
1692
+ error: '<span style="color:#d29922;font-weight:600;">error</span>',
1693
+ };
1694
+
1695
+ const runRows = filtered.map((r) => {
1696
+ const when = formatRelativeTime(r.finished_at || r.started_at);
1697
+ const dur = (r.duration_ms && r.duration_ms > 0) ? \` · \${r.duration_ms}ms\` : '';
1698
+ const status = STATUS_BADGE[r.status] || escapeHtml(r.status);
1699
+ const findingsLine = (r.findings && r.findings.length > 0)
1700
+ ? \`<details style="margin-top:6px;"><summary class="muted" style="cursor:pointer;font-size:11px;">\${r.findings_count} finding(s) — show</summary><pre style="margin:6px 0 0 0;font-size:11px;white-space:pre-wrap;">\${escapeHtml(JSON.stringify(r.findings, null, 2))}</pre></details>\`
1701
+ : '';
1702
+ return \`<div style="border-bottom:1px solid var(--border);padding:10px 12px;">
1703
+ <div style="display:flex;justify-content:space-between;gap:12px;">
1704
+ <div><strong>\${escapeHtml(r.validator)}</strong> <span class="muted">→</span> \${escapeHtml(r.scope)} <span class="muted" style="margin-left:8px;font-size:11px;">\${escapeHtml(r.hook_event || '')}</span></div>
1705
+ <div style="font-size:12px;">\${status} <span class="muted">· \${when}\${dur}</span></div>
1706
+ </div>
1707
+ <div class="muted" style="font-size:12px;margin-top:4px;">\${escapeHtml(r.reason || '')}</div>
1708
+ \${findingsLine}
1709
+ </div>\`;
1710
+ }).join('');
1711
+
1712
+ const validatorPills = [validatorPill('all', 'all', v.runs.length), ...validators.map((vname) =>
1713
+ validatorPill(vname, vname, v.runs.filter((r) => r.validator === vname).length)
1714
+ )].join(' ');
1715
+
1716
+ const statusPills = ['all', 'pass', 'block', 'skip', 'error'].map((s) =>
1717
+ statusPill(s, s, s === 'all' ? v.runs.length : v.runs.filter((r) => r.status === s).length)
1718
+ ).join(' ');
1719
+
1720
+ return \`<h2>Validators
1721
+ <button id="clear-validators" class="btn btn-ghost" style="float:right;font-size:12px;">Clear history</button>
1722
+ </h2>
1723
+ <p class="muted" style="margin-top:0;">Last updated \${formatRelativeTime(v.updated_at)} · \${v.stats.total} total run(s): \${v.stats.pass} pass, \${v.stats.block} block, \${v.stats.skip} skip, \${v.stats.error} error.</p>
1724
+ <div style="margin:12px 0;">
1725
+ <div style="margin-bottom:6px;"><span class="muted" style="font-size:11px;text-transform:uppercase;">Validator</span></div>
1726
+ \${validatorPills}
1727
+ </div>
1728
+ <div style="margin:12px 0;">
1729
+ <div style="margin-bottom:6px;"><span class="muted" style="font-size:11px;text-transform:uppercase;">Status</span></div>
1730
+ \${statusPills}
1731
+ </div>
1732
+ <div class="card" style="padding:0;">\${runRows || '<p class="muted" style="padding:12px;">No runs match the current filter.</p>'}</div>\`;
1733
+ }
1734
+
1635
1735
  function formatRelativeTime(iso) {
1636
1736
  if (!iso) return 'never';
1637
1737
  try {
@@ -1791,8 +1891,40 @@ document.addEventListener('click', (e) => {
1791
1891
  renderContent();
1792
1892
  return;
1793
1893
  }
1894
+ if (t.dataset && t.dataset.validatorsFilter) {
1895
+ validatorsFilter = t.dataset.validatorsFilter;
1896
+ renderContent();
1897
+ return;
1898
+ }
1899
+ if (t.dataset && t.dataset.validatorsStatusFilter) {
1900
+ validatorsStatusFilter = t.dataset.validatorsStatusFilter;
1901
+ renderContent();
1902
+ return;
1903
+ }
1904
+ if (t.id === 'clear-validators') {
1905
+ return clearValidatorsAction(t);
1906
+ }
1794
1907
  });
1795
1908
 
1909
+ async function clearValidatorsAction(btn) {
1910
+ if (!confirm('Clear validator run history? This wipes the log shown in this tab. Validators continue running.')) return;
1911
+ const originalLabel = btn.textContent;
1912
+ btn.disabled = true;
1913
+ btn.textContent = 'Clearing…';
1914
+ try {
1915
+ const r = await fetch('/api/validators/clear', { method: 'POST' });
1916
+ const result = await r.json();
1917
+ if (r.ok && result.ok) showToast('Validator history cleared', 'ok');
1918
+ else showToast('Clear failed: ' + (result.error || 'unknown'), 'err');
1919
+ } catch (e) {
1920
+ showToast('Network error: ' + e.message, 'err');
1921
+ } finally {
1922
+ btn.disabled = false;
1923
+ btn.textContent = originalLabel;
1924
+ await refresh();
1925
+ }
1926
+ }
1927
+
1796
1928
  async function clearObserverAction(btn) {
1797
1929
  if (!confirm('Resolve all active observer incidents and clear server logs?\\n\\nIncident history is preserved in state.incidents (marked resolved). Future events repopulate normally. This cannot be undone.')) return;
1798
1930
  const originalLabel = btn.textContent;
@@ -2220,6 +2352,16 @@ function handler(cwd) {
2220
2352
  return send(502, JSON.stringify({ ok: false, error: 'observer returned ' + result.status, daemon_response: result.body }), 'application/json');
2221
2353
  }
2222
2354
 
2355
+ if (req.method === 'POST' && route === '/api/validators/clear') {
2356
+ try {
2357
+ const validatorState = require(path.join(cwd, '.claudeflow', 'runtime', 'validator-state.js'));
2358
+ validatorState.clearState(cwd);
2359
+ return send(200, JSON.stringify({ ok: true }), 'application/json');
2360
+ } catch (err) {
2361
+ return send(500, JSON.stringify({ ok: false, error: err.message }), 'application/json');
2362
+ }
2363
+ }
2364
+
2223
2365
  if (req.method === 'POST' && route === '/api/observer/restart') {
2224
2366
  // Re-run the SessionStart/browser-error-daemon.js hook script
2225
2367
  // synchronously. The script calls ensureObserver() which spawns
@@ -2389,6 +2531,7 @@ module.exports.start = start;
2389
2531
  module.exports.createPanelServer = handler;
2390
2532
  module.exports.derivePanelPort = derivePanelPort;
2391
2533
  module.exports.getObserverInfo = getObserverInfo;
2534
+ module.exports.getValidatorsInfo = getValidatorsInfo;
2392
2535
  module.exports.collectStatus = collectStatus;
2393
2536
  module.exports.getClaudeMdInfo = getClaudeMdInfo;
2394
2537
  module.exports.getHooksInfo = getHooksInfo;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axiomatic-labs/claudeflow",
3
- "version": "2.13.75",
3
+ "version": "2.13.77",
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"