@axiomatic-labs/claudeflow 2.13.76 → 2.13.78

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 +188 -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,9 @@ 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';
889
+ let validatorsScopeFilter = 'all';
862
890
  let timer = null;
863
891
 
864
892
  // Preserve which <details> were open across re-renders (auto-refresh would
@@ -1061,6 +1089,7 @@ function renderContent() {
1061
1089
  reminders: renderReminders,
1062
1090
  logs: renderLogs,
1063
1091
  observer: renderObserver,
1092
+ validators: renderValidators,
1064
1093
  activePlan: renderActivePlan,
1065
1094
  mcp: renderMcp,
1066
1095
  setupContext: renderSetup,
@@ -1632,6 +1661,102 @@ function renderObserver() {
1632
1661
  <div class="card" style="padding:0;">\${incidentsBlock}</div>\`;
1633
1662
  }
1634
1663
 
1664
+ function renderValidators() {
1665
+ const v = (state && state.validators) || { available: false, runs: [], stats: { total: 0, pass: 0, block: 0, skip: 0, error: 0 } };
1666
+ if (!v.available || v.runs.length === 0) {
1667
+ 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>\`;
1668
+ }
1669
+
1670
+ const validators = [...new Set(v.runs.map((r) => r.validator))].sort();
1671
+ const scopes = [...new Set(v.runs.map((r) => r.scope))].sort();
1672
+ const validatorPill = (label, value, count) => {
1673
+ const a = (validatorsFilter === value);
1674
+ const cls = a ? 'pill pill-active' : 'pill';
1675
+ return \`<button class="\${cls}" data-validators-filter="\${escapeHtml(value)}">\${escapeHtml(label)}<span class="muted" style="margin-left:6px;font-size:11px;">\${count}</span></button>\`;
1676
+ };
1677
+ const statusPill = (label, value, count) => {
1678
+ const a = (validatorsStatusFilter === value);
1679
+ const cls = a ? 'pill pill-active' : 'pill';
1680
+ 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>\`;
1681
+ };
1682
+ const scopePill = (label, value, count) => {
1683
+ const a = (validatorsScopeFilter === value);
1684
+ const cls = a ? 'pill pill-active' : 'pill';
1685
+ return \`<button class="\${cls}" data-validators-scope-filter="\${escapeHtml(value)}">\${escapeHtml(label)}<span class="muted" style="margin-left:6px;font-size:11px;">\${count}</span></button>\`;
1686
+ };
1687
+
1688
+ const filteredByValidator = validatorsFilter === 'all'
1689
+ ? v.runs
1690
+ : v.runs.filter((r) => r.validator === validatorsFilter);
1691
+ const filteredByScope = validatorsScopeFilter === 'all'
1692
+ ? filteredByValidator
1693
+ : filteredByValidator.filter((r) => r.scope === validatorsScopeFilter);
1694
+ const filtered = validatorsStatusFilter === 'all'
1695
+ ? filteredByScope
1696
+ : filteredByScope.filter((r) => r.status === validatorsStatusFilter);
1697
+
1698
+ const STATUS_BADGE = {
1699
+ pass: '<span style="color:#3fb950;font-weight:600;">pass</span>',
1700
+ block: '<span style="color:#f85149;font-weight:600;">block</span>',
1701
+ skip: '<span class="muted">skip</span>',
1702
+ error: '<span style="color:#d29922;font-weight:600;">error</span>',
1703
+ };
1704
+
1705
+ const runRows = filtered.map((r) => {
1706
+ const when = formatRelativeTime(r.finished_at || r.started_at);
1707
+ const dur = (r.duration_ms && r.duration_ms > 0) ? \` · \${r.duration_ms}ms\` : '';
1708
+ const status = STATUS_BADGE[r.status] || escapeHtml(r.status);
1709
+ const findingsLine = (r.findings && r.findings.length > 0)
1710
+ ? \`<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>\`
1711
+ : '';
1712
+ return \`<div style="border-bottom:1px solid var(--border);padding:10px 12px;">
1713
+ <div style="display:flex;justify-content:space-between;gap:12px;">
1714
+ <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>
1715
+ <div style="font-size:12px;">\${status} <span class="muted">· \${when}\${dur}</span></div>
1716
+ </div>
1717
+ <div class="muted" style="font-size:12px;margin-top:4px;">\${escapeHtml(r.reason || '')}</div>
1718
+ \${findingsLine}
1719
+ </div>\`;
1720
+ }).join('');
1721
+
1722
+ const validatorPills = [validatorPill('all', 'all', v.runs.length), ...validators.map((vname) =>
1723
+ validatorPill(vname, vname, v.runs.filter((r) => r.validator === vname).length)
1724
+ )].join(' ');
1725
+
1726
+ const scopePills = [scopePill('all', 'all', v.runs.length), ...scopes.map((s) =>
1727
+ scopePill(s, s, v.runs.filter((r) => r.scope === s).length)
1728
+ )].join(' ');
1729
+
1730
+ const statusPills = ['all', 'pass', 'block', 'skip', 'error'].map((s) =>
1731
+ statusPill(s, s, s === 'all' ? v.runs.length : v.runs.filter((r) => r.status === s).length)
1732
+ ).join(' ');
1733
+
1734
+ // Clear button label changes when a scope filter is active so the user
1735
+ // sees the action they're about to take. "Clear (Admin-Office)" is
1736
+ // unambiguous; "Clear history" wiped everything pre-v2.13.78.
1737
+ const clearLabel = validatorsScopeFilter === 'all'
1738
+ ? 'Clear all history'
1739
+ : \`Clear "\${escapeHtml(validatorsScopeFilter)}" history\`;
1740
+
1741
+ return \`<h2>Validators
1742
+ <button id="clear-validators" class="btn btn-ghost" style="float:right;font-size:12px;" data-scope="\${escapeHtml(validatorsScopeFilter)}">\${clearLabel}</button>
1743
+ </h2>
1744
+ <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>
1745
+ <div style="margin:12px 0;">
1746
+ <div style="margin-bottom:6px;"><span class="muted" style="font-size:11px;text-transform:uppercase;">Validator</span></div>
1747
+ \${validatorPills}
1748
+ </div>
1749
+ <div style="margin:12px 0;">
1750
+ <div style="margin-bottom:6px;"><span class="muted" style="font-size:11px;text-transform:uppercase;">Scope</span></div>
1751
+ \${scopePills}
1752
+ </div>
1753
+ <div style="margin:12px 0;">
1754
+ <div style="margin-bottom:6px;"><span class="muted" style="font-size:11px;text-transform:uppercase;">Status</span></div>
1755
+ \${statusPills}
1756
+ </div>
1757
+ <div class="card" style="padding:0;">\${runRows || '<p class="muted" style="padding:12px;">No runs match the current filter.</p>'}</div>\`;
1758
+ }
1759
+
1635
1760
  function formatRelativeTime(iso) {
1636
1761
  if (!iso) return 'never';
1637
1762
  try {
@@ -1791,8 +1916,57 @@ document.addEventListener('click', (e) => {
1791
1916
  renderContent();
1792
1917
  return;
1793
1918
  }
1919
+ if (t.dataset && t.dataset.validatorsFilter) {
1920
+ validatorsFilter = t.dataset.validatorsFilter;
1921
+ renderContent();
1922
+ return;
1923
+ }
1924
+ if (t.dataset && t.dataset.validatorsStatusFilter) {
1925
+ validatorsStatusFilter = t.dataset.validatorsStatusFilter;
1926
+ renderContent();
1927
+ return;
1928
+ }
1929
+ if (t.dataset && t.dataset.validatorsScopeFilter) {
1930
+ validatorsScopeFilter = t.dataset.validatorsScopeFilter;
1931
+ renderContent();
1932
+ return;
1933
+ }
1934
+ if (t.id === 'clear-validators') {
1935
+ return clearValidatorsAction(t);
1936
+ }
1794
1937
  });
1795
1938
 
1939
+ async function clearValidatorsAction(btn) {
1940
+ const scope = (btn.dataset && btn.dataset.scope) || 'all';
1941
+ const isScoped = scope !== 'all';
1942
+ const message = isScoped
1943
+ ? \`Clear validator history for scope "\${scope}"? Runs from other scopes are preserved. Validators continue running.\`
1944
+ : 'Clear ALL validator run history? This wipes the entire log. Validators continue running.';
1945
+ if (!confirm(message)) return;
1946
+ const originalLabel = btn.textContent;
1947
+ btn.disabled = true;
1948
+ btn.textContent = 'Clearing…';
1949
+ try {
1950
+ const url = isScoped
1951
+ ? '/api/validators/clear?scope=' + encodeURIComponent(scope)
1952
+ : '/api/validators/clear';
1953
+ const r = await fetch(url, { method: 'POST' });
1954
+ const result = await r.json();
1955
+ if (r.ok && result.ok) {
1956
+ const removed = typeof result.removed === 'number' ? \` (removed \${result.removed})\` : '';
1957
+ showToast(isScoped ? \`Cleared "\${scope}"\${removed}\` : \`Cleared all history\${removed}\`, 'ok');
1958
+ } else {
1959
+ showToast('Clear failed: ' + (result.error || 'unknown'), 'err');
1960
+ }
1961
+ } catch (e) {
1962
+ showToast('Network error: ' + e.message, 'err');
1963
+ } finally {
1964
+ btn.disabled = false;
1965
+ btn.textContent = originalLabel;
1966
+ await refresh();
1967
+ }
1968
+ }
1969
+
1796
1970
  async function clearObserverAction(btn) {
1797
1971
  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
1972
  const originalLabel = btn.textContent;
@@ -2220,6 +2394,19 @@ function handler(cwd) {
2220
2394
  return send(502, JSON.stringify({ ok: false, error: 'observer returned ' + result.status, daemon_response: result.body }), 'application/json');
2221
2395
  }
2222
2396
 
2397
+ if (req.method === 'POST' && route === '/api/validators/clear') {
2398
+ try {
2399
+ const validatorState = require(path.join(cwd, '.claudeflow', 'runtime', 'validator-state.js'));
2400
+ const scope = url.searchParams.get('scope');
2401
+ const removed = scope && scope !== 'all'
2402
+ ? validatorState.clearScope(cwd, scope)
2403
+ : (() => { const s = validatorState.readState(cwd); const n = s ? s.runs.length : 0; validatorState.clearState(cwd); return n; })();
2404
+ return send(200, JSON.stringify({ ok: true, removed, scope: scope || 'all' }), 'application/json');
2405
+ } catch (err) {
2406
+ return send(500, JSON.stringify({ ok: false, error: err.message }), 'application/json');
2407
+ }
2408
+ }
2409
+
2223
2410
  if (req.method === 'POST' && route === '/api/observer/restart') {
2224
2411
  // Re-run the SessionStart/browser-error-daemon.js hook script
2225
2412
  // synchronously. The script calls ensureObserver() which spawns
@@ -2389,6 +2576,7 @@ module.exports.start = start;
2389
2576
  module.exports.createPanelServer = handler;
2390
2577
  module.exports.derivePanelPort = derivePanelPort;
2391
2578
  module.exports.getObserverInfo = getObserverInfo;
2579
+ module.exports.getValidatorsInfo = getValidatorsInfo;
2392
2580
  module.exports.collectStatus = collectStatus;
2393
2581
  module.exports.getClaudeMdInfo = getClaudeMdInfo;
2394
2582
  module.exports.getHooksInfo = getHooksInfo;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axiomatic-labs/claudeflow",
3
- "version": "2.13.76",
3
+ "version": "2.13.78",
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"