@axiomatic-labs/claudeflow 2.13.65 → 2.13.67

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
 
@@ -330,11 +331,18 @@ function getObserverInfo(cwd) {
330
331
  const unresolvedIncidents = summary && Array.isArray(summary.unresolved_incidents)
331
332
  ? summary.unresolved_incidents
332
333
  : [];
333
- const allIncidents = state && state.incidents && typeof state.incidents === 'object'
334
+ const allInState = state && state.incidents && typeof state.incidents === 'object'
334
335
  ? Object.values(state.incidents)
335
336
  .sort((a, b) => String(b.last_seen_at || '').localeCompare(String(a.last_seen_at || '')))
336
337
  .slice(0, 50)
337
338
  : [];
339
+ // Default surface to UI: ACTIVE only. "Clear observer state" resolves all
340
+ // active incidents, so after clear the active list is empty (which matches
341
+ // the user's expectation that "Clear" empties the feed). Resolved incidents
342
+ // are kept in state.incidents for audit and can be surfaced via the
343
+ // "Show resolved" filter pill.
344
+ const activeIncidents = allInState.filter((inc) => !inc.resolved_at && inc.active !== false);
345
+ const resolvedIncidents = allInState.filter((inc) => inc.resolved_at || inc.active === false);
338
346
 
339
347
  return {
340
348
  daemon,
@@ -346,7 +354,10 @@ function getObserverInfo(cwd) {
346
354
  servers,
347
355
  browser,
348
356
  unresolved_incidents: unresolvedIncidents,
349
- recent_incidents: allIncidents,
357
+ active_incidents: activeIncidents,
358
+ resolved_incidents: resolvedIncidents,
359
+ // Kept for backwards compat with older render code — same as active_incidents.
360
+ recent_incidents: activeIncidents,
350
361
  totals: summary ? {
351
362
  unresolved: summary.unresolved_incidents_total || 0,
352
363
  browser: summary.browser_incidents_total || 0,
@@ -363,6 +374,14 @@ function getRunToCompletionInfo(cwd) {
363
374
  };
364
375
  }
365
376
 
377
+ function getCwdLockInfo(cwd) {
378
+ const overrides = readOverrides(cwd);
379
+ return {
380
+ on: overrides.cwdLockEnabled === true,
381
+ handler: 'CwdChanged/lock-cwd.js',
382
+ };
383
+ }
384
+
366
385
  function getActivePlanInfo(cwd) {
367
386
  const p = path.join(cwd, '.claudeflow', 'state', 'active-plan.json');
368
387
  let raw;
@@ -618,6 +637,7 @@ function collectStatus(cwd) {
618
637
  reminders: getRemindersInfo(cwd),
619
638
  enforcement: getEnforcementInfo(cwd),
620
639
  runToCompletion: getRunToCompletionInfo(cwd),
640
+ cwdLock: getCwdLockInfo(cwd),
621
641
  observer: getObserverInfo(cwd),
622
642
  activePlan: getActivePlanInfo(cwd),
623
643
  logs: getLogsInfo(cwd),
@@ -800,6 +820,11 @@ details[open] summary { padding-bottom: 8px; border-bottom: 1px solid var(--bord
800
820
  <label class="enforce-switch"><input type="checkbox" id="run-to-completion-toggle" /><span class="enforce-slider"></span></label>
801
821
  <span class="enforce-state" id="run-to-completion-state">…</span>
802
822
  </span>
823
+ <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.">
824
+ <span class="enforce-label">Lock cwd</span>
825
+ <label class="enforce-switch"><input type="checkbox" id="cwd-lock-toggle" /><span class="enforce-slider"></span></label>
826
+ <span class="enforce-state" id="cwd-lock-state">…</span>
827
+ </span>
803
828
  <label class="toggle"><input type="checkbox" id="auto" checked /> auto-refresh 5s</label>
804
829
  <button id="refresh">Refresh</button>
805
830
  </span>
@@ -833,6 +858,7 @@ const SECTIONS = [
833
858
  let state = null;
834
859
  let active = 'overview';
835
860
  let observerFilter = 'all';
861
+ let observerShowResolved = false;
836
862
  let timer = null;
837
863
 
838
864
  // Preserve which <details> were open across re-renders (auto-refresh would
@@ -936,6 +962,20 @@ function renderHeader() {
936
962
  } else if (rtcPill) {
937
963
  rtcPill.style.display = 'none';
938
964
  }
965
+ const lock = state.cwdLock;
966
+ const lockPill = document.getElementById('cwd-lock-pill');
967
+ const lockCb = document.getElementById('cwd-lock-toggle');
968
+ const lockLbl = document.getElementById('cwd-lock-state');
969
+ if (lock && lockPill) {
970
+ lockPill.dataset.state = lock.on ? 'on' : 'off';
971
+ lockCb.checked = lock.on;
972
+ lockLbl.textContent = lock.on ? 'ON' : 'OFF';
973
+ lockPill.title = lock.on
974
+ ? '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.'
975
+ : '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.';
976
+ } else if (lockPill) {
977
+ lockPill.style.display = 'none';
978
+ }
939
979
  }
940
980
 
941
981
  function severityFor(id) {
@@ -1428,10 +1468,13 @@ function renderObserver() {
1428
1468
  </div>\`
1429
1469
  : '<p class="muted">No browser snapshots received yet. (The observer collects when the agent navigates with mcp__claude-in-chrome__*.)</p>';
1430
1470
 
1431
- // Build the filter index: for each surface (server label), find which
1432
- // incidents belong to it. browser incidents are matched by origin URL
1433
- // against each server's :PORT. Browser-other = browser incidents whose
1434
- // origin doesn't match any configured server (e.g. external URL).
1471
+ // Active vs resolved toggle (client-side state observerShowResolved).
1472
+ // After "Clear observer state", all active incidents are marked resolved;
1473
+ // by default the UI shows only active. Toggle the pill to see history.
1474
+ const showResolved = (typeof observerShowResolved !== 'undefined') && observerShowResolved;
1475
+ const sourceList = showResolved ? o.resolved_incidents : o.active_incidents;
1476
+
1477
+ // Build the filter index ON THE CURRENT VIEW (active or resolved).
1435
1478
  const serverByOrigin = {};
1436
1479
  o.servers.forEach((srv) => {
1437
1480
  if (srv.port) serverByOrigin[\`localhost:\${srv.port}\`] = srv.label;
@@ -1450,7 +1493,7 @@ function renderObserver() {
1450
1493
  return inc.source || 'other';
1451
1494
  }
1452
1495
  const buckets = {};
1453
- o.recent_incidents.forEach((inc) => {
1496
+ sourceList.forEach((inc) => {
1454
1497
  const b = incidentBucket(inc);
1455
1498
  if (!buckets[b]) buckets[b] = [];
1456
1499
  buckets[b].push(inc);
@@ -1458,7 +1501,7 @@ function renderObserver() {
1458
1501
  const surfacesInUse = Object.keys(buckets).sort();
1459
1502
  // Apply observerFilter client-side state. Default 'all' (no filter).
1460
1503
  const filteredIncidents = (typeof observerFilter === 'undefined' || observerFilter === 'all')
1461
- ? o.recent_incidents
1504
+ ? sourceList
1462
1505
  : (buckets[observerFilter] || []);
1463
1506
 
1464
1507
  const filterPill = (label, value, count) => {
@@ -1466,9 +1509,13 @@ function renderObserver() {
1466
1509
  const cls = active ? 'observer-filter active' : 'observer-filter';
1467
1510
  return \`<button class="\${cls}" data-observer-filter="\${escapeHtml(value)}">\${escapeHtml(label)}<span class="muted" style="margin-left:6px;font-size:11px;">\${count}</span></button>\`;
1468
1511
  };
1469
- const filterPills = '<div class="observer-filters" style="display:flex;gap:6px;flex-wrap:wrap;margin:8px 0;">' +
1470
- filterPill('All', 'all', o.recent_incidents.length) +
1512
+ // Show resolved toggle pill separate from surface filters.
1513
+ const showResolvedClass = showResolved ? 'observer-filter active' : 'observer-filter';
1514
+ const showResolvedPill = \`<button class="\${showResolvedClass}" data-observer-show-resolved="toggle" style="margin-left:auto;">\${showResolved ? 'Showing resolved' : 'Show resolved'}<span class="muted" style="margin-left:6px;font-size:11px;">\${o.resolved_incidents.length}</span></button>\`;
1515
+ const filterPills = '<div class="observer-filters" style="display:flex;gap:6px;flex-wrap:wrap;align-items:center;margin:8px 0;">' +
1516
+ filterPill('All', 'all', sourceList.length) +
1471
1517
  surfacesInUse.map((surface) => filterPill(surface, surface, buckets[surface].length)).join('') +
1518
+ showResolvedPill +
1472
1519
  '</div>';
1473
1520
 
1474
1521
  // Incidents feed
@@ -1513,7 +1560,7 @@ function renderObserver() {
1513
1560
  <div class="card" style="padding:0;">\${serversTable}</div>
1514
1561
  <h3 style="margin-top:18px;font-size:14px;">Browser channel</h3>
1515
1562
  \${browserBlock}
1516
- <h3 style="margin-top:18px;font-size:14px;">Incidents (most recent \${o.recent_incidents.length})</h3>
1563
+ <h3 style="margin-top:18px;font-size:14px;">\${showResolved ? 'Resolved incidents (' + o.resolved_incidents.length + ')' : 'Active incidents (' + o.active_incidents.length + ')'}</h3>
1517
1564
  \${filterPills}
1518
1565
  <div class="card" style="padding:0;">\${incidentsBlock}</div>\`;
1519
1566
  }
@@ -1670,6 +1717,13 @@ document.addEventListener('click', (e) => {
1670
1717
  renderContent();
1671
1718
  return;
1672
1719
  }
1720
+ if (t.dataset && t.dataset.observerShowResolved) {
1721
+ observerShowResolved = !observerShowResolved;
1722
+ // Reset surface filter when toggling, since counts change.
1723
+ observerFilter = 'all';
1724
+ renderContent();
1725
+ return;
1726
+ }
1673
1727
  });
1674
1728
 
1675
1729
  async function clearObserverAction(btn) {
@@ -1872,6 +1926,34 @@ document.getElementById('run-to-completion-toggle').onchange = (e) => {
1872
1926
  toggleRunToCompletion(e.target.checked);
1873
1927
  };
1874
1928
 
1929
+ async function toggleCwdLockUI(enable) {
1930
+ try {
1931
+ const r = await fetch('/api/cwd-lock/toggle', {
1932
+ method: 'POST',
1933
+ headers: { 'Content-Type': 'application/json' },
1934
+ body: JSON.stringify({ enable }),
1935
+ });
1936
+ const result = await r.json();
1937
+ if (!r.ok || result.state === 'error') {
1938
+ showToast('Toggle failed: ' + (result.reason || result.error || 'unknown'), 'err');
1939
+ } else if (result.state === 'noop') {
1940
+ showToast('Cwd lock already in target state', 'ok');
1941
+ } else {
1942
+ showToast(enable
1943
+ ? 'Cwd lock ON — cd away from project root will be BLOCKED on next CwdChanged.'
1944
+ : 'Cwd lock OFF — agent can freely change directory.',
1945
+ 'ok');
1946
+ }
1947
+ } catch (e) {
1948
+ showToast('Network error: ' + e.message, 'err');
1949
+ }
1950
+ await refresh();
1951
+ }
1952
+
1953
+ document.getElementById('cwd-lock-toggle').onchange = (e) => {
1954
+ toggleCwdLockUI(e.target.checked);
1955
+ };
1956
+
1875
1957
  document.getElementById('refresh').onclick = refresh;
1876
1958
  document.getElementById('auto').onchange = (e) => {
1877
1959
  if (timer) clearInterval(timer);
@@ -1981,6 +2063,20 @@ function handler(cwd) {
1981
2063
  return send(status, JSON.stringify(result), 'application/json');
1982
2064
  }
1983
2065
 
2066
+ if (req.method === 'POST' && route === '/api/cwd-lock/toggle') {
2067
+ const raw = await readRequestBody(req);
2068
+ let payload;
2069
+ try { payload = JSON.parse(raw); }
2070
+ catch { return send(400, JSON.stringify({ error: 'invalid json' }), 'application/json'); }
2071
+ const { enable } = payload;
2072
+ if (typeof enable !== 'boolean') {
2073
+ return send(400, JSON.stringify({ error: 'enable required' }), 'application/json');
2074
+ }
2075
+ const result = toggleCwdLock(cwd, { enable });
2076
+ const status = result.state === 'error' ? 500 : 200;
2077
+ return send(status, JSON.stringify(result), 'application/json');
2078
+ }
2079
+
1984
2080
  if (req.method === 'POST' && route === '/api/run-to-completion/toggle') {
1985
2081
  const raw = await readRequestBody(req);
1986
2082
  let payload;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axiomatic-labs/claudeflow",
3
- "version": "2.13.65",
3
+ "version": "2.13.67",
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"