@axiomatic-labs/claudeflow 2.13.64 → 2.13.66

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.
@@ -57,6 +57,7 @@ function defaultOverrides() {
57
57
  disabledReminders: [],
58
58
  enforcementDisabled: false,
59
59
  runToCompletionReminderEnabled: false,
60
+ cwdLockEnabled: false,
60
61
  };
61
62
  }
62
63
 
@@ -69,6 +70,7 @@ function readOverrides(projectRoot) {
69
70
  disabledReminders: Array.isArray(data.disabledReminders) ? data.disabledReminders : [],
70
71
  enforcementDisabled: data.enforcementDisabled === true,
71
72
  runToCompletionReminderEnabled: data.runToCompletionReminderEnabled === true,
73
+ cwdLockEnabled: data.cwdLockEnabled === true,
72
74
  };
73
75
  }
74
76
 
@@ -209,6 +211,20 @@ function toggleRunToCompletionReminder(projectRoot, { enable }) {
209
211
  return { state: enable ? 'on' : 'off' };
210
212
  }
211
213
 
214
+ // Toggle the cwd-lock hook. Independent of the master enforcement switch —
215
+ // the CwdChanged hook is not in the enforcement-catalog and ignores the
216
+ // master switch by design (you can lock cwd even with enforcement off).
217
+ function toggleCwdLock(projectRoot, { enable }) {
218
+ if (typeof enable !== 'boolean') return { state: 'error', reason: 'enable-required' };
219
+ const overrides = readOverrides(projectRoot);
220
+ if (overrides.cwdLockEnabled === enable) {
221
+ return { state: 'noop', reason: enable ? 'already-on' : 'already-off' };
222
+ }
223
+ overrides.cwdLockEnabled = enable;
224
+ writeOverrides(projectRoot, overrides);
225
+ return { state: enable ? 'on' : 'off' };
226
+ }
227
+
212
228
  // Toggle a single reminder by id. Same persistence file, same hot-reload
213
229
  // semantics — `reload-reminder.js` re-reads overrides on every invocation.
214
230
  function toggleReminderOverride(projectRoot, { id, disable }) {
@@ -240,6 +256,7 @@ module.exports = {
240
256
  toggleReminderOverride,
241
257
  toggleEnforcementMode,
242
258
  toggleRunToCompletionReminder,
259
+ toggleCwdLock,
243
260
  normalizeHandlerPath,
244
261
  matchesOverride,
245
262
  };
package/lib/panel.js CHANGED
@@ -30,6 +30,7 @@ const {
30
30
  toggleReminderOverride,
31
31
  toggleEnforcementMode,
32
32
  toggleRunToCompletionReminder,
33
+ toggleCwdLock,
33
34
  normalizeHandlerPath,
34
35
  } = require('./hook-overrides.js');
35
36
 
@@ -363,6 +364,14 @@ function getRunToCompletionInfo(cwd) {
363
364
  };
364
365
  }
365
366
 
367
+ function getCwdLockInfo(cwd) {
368
+ const overrides = readOverrides(cwd);
369
+ return {
370
+ on: overrides.cwdLockEnabled === true,
371
+ handler: 'CwdChanged/lock-cwd.js',
372
+ };
373
+ }
374
+
366
375
  function getActivePlanInfo(cwd) {
367
376
  const p = path.join(cwd, '.claudeflow', 'state', 'active-plan.json');
368
377
  let raw;
@@ -618,6 +627,7 @@ function collectStatus(cwd) {
618
627
  reminders: getRemindersInfo(cwd),
619
628
  enforcement: getEnforcementInfo(cwd),
620
629
  runToCompletion: getRunToCompletionInfo(cwd),
630
+ cwdLock: getCwdLockInfo(cwd),
621
631
  observer: getObserverInfo(cwd),
622
632
  activePlan: getActivePlanInfo(cwd),
623
633
  logs: getLogsInfo(cwd),
@@ -734,6 +744,9 @@ details[open] summary { padding-bottom: 8px; border-bottom: 1px solid var(--bord
734
744
  .logs-controls { display: flex; gap: 12px; align-items: center; margin-bottom: 14px; }
735
745
  .logs-controls select, .logs-controls button { background: var(--panel); border: 1px solid var(--border); color: var(--fg); padding: 6px 10px; border-radius: 6px; font: inherit; }
736
746
  .logs-controls button:hover { border-color: var(--accent); cursor: pointer; }
747
+ .observer-filter { background: var(--panel); border: 1px solid var(--border); color: var(--fg); padding: 4px 10px; border-radius: 999px; cursor: pointer; font-size: 12px; font: inherit; }
748
+ .observer-filter:hover { border-color: var(--accent, #6cf); }
749
+ .observer-filter.active { background: rgba(108,170,255,0.12); border-color: var(--accent, #6cf); color: var(--accent, #6cf); }
737
750
  .btn-warn { background: var(--panel); border: 1px solid rgba(245,194,52,0.5); color: var(--warn, #f5c234); padding: 6px 12px; border-radius: 6px; cursor: pointer; font: inherit; }
738
751
  .btn-warn:hover { border-color: var(--warn, #f5c234); background: rgba(245,194,52,0.08); }
739
752
  .btn-warn:disabled { opacity: 0.4; cursor: not-allowed; }
@@ -797,6 +810,11 @@ details[open] summary { padding-bottom: 8px; border-bottom: 1px solid var(--bord
797
810
  <label class="enforce-switch"><input type="checkbox" id="run-to-completion-toggle" /><span class="enforce-slider"></span></label>
798
811
  <span class="enforce-state" id="run-to-completion-state">…</span>
799
812
  </span>
813
+ <span class="enforce-pill" id="cwd-lock-pill" title="Hard block — fires on Claude Code CwdChanged event. When the agent runs cd Subdir + cmd and the cwd would persist away from the project root, the change is blocked with a guidance message. Use (cd Subdir + cmd) subshells or absolute paths instead.">
814
+ <span class="enforce-label">Lock cwd</span>
815
+ <label class="enforce-switch"><input type="checkbox" id="cwd-lock-toggle" /><span class="enforce-slider"></span></label>
816
+ <span class="enforce-state" id="cwd-lock-state">…</span>
817
+ </span>
800
818
  <label class="toggle"><input type="checkbox" id="auto" checked /> auto-refresh 5s</label>
801
819
  <button id="refresh">Refresh</button>
802
820
  </span>
@@ -829,6 +847,7 @@ const SECTIONS = [
829
847
 
830
848
  let state = null;
831
849
  let active = 'overview';
850
+ let observerFilter = 'all';
832
851
  let timer = null;
833
852
 
834
853
  // Preserve which <details> were open across re-renders (auto-refresh would
@@ -932,6 +951,20 @@ function renderHeader() {
932
951
  } else if (rtcPill) {
933
952
  rtcPill.style.display = 'none';
934
953
  }
954
+ const lock = state.cwdLock;
955
+ const lockPill = document.getElementById('cwd-lock-pill');
956
+ const lockCb = document.getElementById('cwd-lock-toggle');
957
+ const lockLbl = document.getElementById('cwd-lock-state');
958
+ if (lock && lockPill) {
959
+ lockPill.dataset.state = lock.on ? 'on' : 'off';
960
+ lockCb.checked = lock.on;
961
+ lockLbl.textContent = lock.on ? 'ON' : 'OFF';
962
+ lockPill.title = lock.on
963
+ ? 'Lock cwd is ON — any persistent change of working directory away from the project root is blocked via the CwdChanged hook. Use (cd Subdir + cmd) subshells or absolute paths.'
964
+ : 'Lock cwd is OFF — the agent can cd into subdirectories and the cwd will persist across tool calls. Click to enforce a single project-root cwd.';
965
+ } else if (lockPill) {
966
+ lockPill.style.display = 'none';
967
+ }
935
968
  }
936
969
 
937
970
  function severityFor(id) {
@@ -1424,11 +1457,54 @@ function renderObserver() {
1424
1457
  </div>\`
1425
1458
  : '<p class="muted">No browser snapshots received yet. (The observer collects when the agent navigates with mcp__claude-in-chrome__*.)</p>';
1426
1459
 
1460
+ // Build the filter index: for each surface (server label), find which
1461
+ // incidents belong to it. browser incidents are matched by origin URL
1462
+ // against each server's :PORT. Browser-other = browser incidents whose
1463
+ // origin doesn't match any configured server (e.g. external URL).
1464
+ const serverByOrigin = {};
1465
+ o.servers.forEach((srv) => {
1466
+ if (srv.port) serverByOrigin[\`localhost:\${srv.port}\`] = srv.label;
1467
+ if (srv.port) serverByOrigin[\`127.0.0.1:\${srv.port}\`] = srv.label;
1468
+ });
1469
+ function incidentBucket(inc) {
1470
+ if (inc.source === 'server') {
1471
+ return (inc.metadata && inc.metadata.label) || inc.surface || 'server-unknown';
1472
+ }
1473
+ if (inc.source === 'browser') {
1474
+ const url = (inc.metadata && (inc.metadata.url || inc.metadata.origin)) || '';
1475
+ const m = url.match(/^https?:\\/\\/([^/]+)/);
1476
+ if (m && serverByOrigin[m[1]]) return serverByOrigin[m[1]];
1477
+ return 'browser-other';
1478
+ }
1479
+ return inc.source || 'other';
1480
+ }
1481
+ const buckets = {};
1482
+ o.recent_incidents.forEach((inc) => {
1483
+ const b = incidentBucket(inc);
1484
+ if (!buckets[b]) buckets[b] = [];
1485
+ buckets[b].push(inc);
1486
+ });
1487
+ const surfacesInUse = Object.keys(buckets).sort();
1488
+ // Apply observerFilter client-side state. Default 'all' (no filter).
1489
+ const filteredIncidents = (typeof observerFilter === 'undefined' || observerFilter === 'all')
1490
+ ? o.recent_incidents
1491
+ : (buckets[observerFilter] || []);
1492
+
1493
+ const filterPill = (label, value, count) => {
1494
+ const active = (typeof observerFilter === 'undefined' && value === 'all') || observerFilter === value;
1495
+ const cls = active ? 'observer-filter active' : 'observer-filter';
1496
+ return \`<button class="\${cls}" data-observer-filter="\${escapeHtml(value)}">\${escapeHtml(label)}<span class="muted" style="margin-left:6px;font-size:11px;">\${count}</span></button>\`;
1497
+ };
1498
+ const filterPills = '<div class="observer-filters" style="display:flex;gap:6px;flex-wrap:wrap;margin:8px 0;">' +
1499
+ filterPill('All', 'all', o.recent_incidents.length) +
1500
+ surfacesInUse.map((surface) => filterPill(surface, surface, buckets[surface].length)).join('') +
1501
+ '</div>';
1502
+
1427
1503
  // Incidents feed
1428
- const incidentsBlock = o.recent_incidents.length === 0
1429
- ? '<p class="muted">No incidents recorded.</p>'
1504
+ const incidentsBlock = filteredIncidents.length === 0
1505
+ ? '<p class="muted">No incidents in this filter.</p>'
1430
1506
  : '<div class="incidents-feed">' +
1431
- o.recent_incidents.map((inc) => {
1507
+ filteredIncidents.map((inc) => {
1432
1508
  const sevClass = inc.severity === 'error' || inc.severity === 'critical' ? 'err' : (inc.severity === 'warning' ? 'warn' : 'info');
1433
1509
  const resolvedBadge = inc.resolved_at
1434
1510
  ? '<span class="badge ok" style="margin-left:6px;">resolved</span>'
@@ -1467,6 +1543,7 @@ function renderObserver() {
1467
1543
  <h3 style="margin-top:18px;font-size:14px;">Browser channel</h3>
1468
1544
  \${browserBlock}
1469
1545
  <h3 style="margin-top:18px;font-size:14px;">Incidents (most recent \${o.recent_incidents.length})</h3>
1546
+ \${filterPills}
1470
1547
  <div class="card" style="padding:0;">\${incidentsBlock}</div>\`;
1471
1548
  }
1472
1549
 
@@ -1617,6 +1694,11 @@ document.addEventListener('click', (e) => {
1617
1694
  if (t.id === 'clear-observer') {
1618
1695
  return clearObserverAction(t);
1619
1696
  }
1697
+ if (t.dataset && t.dataset.observerFilter) {
1698
+ observerFilter = t.dataset.observerFilter;
1699
+ renderContent();
1700
+ return;
1701
+ }
1620
1702
  });
1621
1703
 
1622
1704
  async function clearObserverAction(btn) {
@@ -1819,6 +1901,34 @@ document.getElementById('run-to-completion-toggle').onchange = (e) => {
1819
1901
  toggleRunToCompletion(e.target.checked);
1820
1902
  };
1821
1903
 
1904
+ async function toggleCwdLockUI(enable) {
1905
+ try {
1906
+ const r = await fetch('/api/cwd-lock/toggle', {
1907
+ method: 'POST',
1908
+ headers: { 'Content-Type': 'application/json' },
1909
+ body: JSON.stringify({ enable }),
1910
+ });
1911
+ const result = await r.json();
1912
+ if (!r.ok || result.state === 'error') {
1913
+ showToast('Toggle failed: ' + (result.reason || result.error || 'unknown'), 'err');
1914
+ } else if (result.state === 'noop') {
1915
+ showToast('Cwd lock already in target state', 'ok');
1916
+ } else {
1917
+ showToast(enable
1918
+ ? 'Cwd lock ON — cd away from project root will be BLOCKED on next CwdChanged.'
1919
+ : 'Cwd lock OFF — agent can freely change directory.',
1920
+ 'ok');
1921
+ }
1922
+ } catch (e) {
1923
+ showToast('Network error: ' + e.message, 'err');
1924
+ }
1925
+ await refresh();
1926
+ }
1927
+
1928
+ document.getElementById('cwd-lock-toggle').onchange = (e) => {
1929
+ toggleCwdLockUI(e.target.checked);
1930
+ };
1931
+
1822
1932
  document.getElementById('refresh').onclick = refresh;
1823
1933
  document.getElementById('auto').onchange = (e) => {
1824
1934
  if (timer) clearInterval(timer);
@@ -1928,6 +2038,20 @@ function handler(cwd) {
1928
2038
  return send(status, JSON.stringify(result), 'application/json');
1929
2039
  }
1930
2040
 
2041
+ if (req.method === 'POST' && route === '/api/cwd-lock/toggle') {
2042
+ const raw = await readRequestBody(req);
2043
+ let payload;
2044
+ try { payload = JSON.parse(raw); }
2045
+ catch { return send(400, JSON.stringify({ error: 'invalid json' }), 'application/json'); }
2046
+ const { enable } = payload;
2047
+ if (typeof enable !== 'boolean') {
2048
+ return send(400, JSON.stringify({ error: 'enable required' }), 'application/json');
2049
+ }
2050
+ const result = toggleCwdLock(cwd, { enable });
2051
+ const status = result.state === 'error' ? 500 : 200;
2052
+ return send(status, JSON.stringify(result), 'application/json');
2053
+ }
2054
+
1931
2055
  if (req.method === 'POST' && route === '/api/run-to-completion/toggle') {
1932
2056
  const raw = await readRequestBody(req);
1933
2057
  let payload;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axiomatic-labs/claudeflow",
3
- "version": "2.13.64",
3
+ "version": "2.13.66",
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"