@axiomatic-labs/claudeflow 2.13.24 → 2.13.25

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.
@@ -51,7 +51,7 @@ function normalizeHandlerPath(command) {
51
51
  }
52
52
 
53
53
  function defaultOverrides() {
54
- return { version: 1, disabledHandlers: [], disabledReminders: [] };
54
+ return { version: 1, disabledHandlers: [], disabledReminders: [], enforcementDisabled: false };
55
55
  }
56
56
 
57
57
  function readOverrides(projectRoot) {
@@ -61,6 +61,7 @@ function readOverrides(projectRoot) {
61
61
  version: data.version || 1,
62
62
  disabledHandlers: Array.isArray(data.disabledHandlers) ? data.disabledHandlers : [],
63
63
  disabledReminders: Array.isArray(data.disabledReminders) ? data.disabledReminders : [],
64
+ enforcementDisabled: data.enforcementDisabled === true,
64
65
  };
65
66
  }
66
67
 
@@ -173,6 +174,21 @@ function toggleHookOverride(projectRoot, { event, matcher = '', handler, disable
173
174
  return { state: 'enabled' };
174
175
  }
175
176
 
177
+ // Master enforcement switch. When OFF, the wrapper silences every hook
178
+ // listed in `.claude/hooks/shared/enforcement-catalog.js` regardless of
179
+ // per-handler manual toggles. Per-handler toggles outside the catalog
180
+ // remain in effect — they are independent layers.
181
+ function toggleEnforcementMode(projectRoot, { disable }) {
182
+ if (typeof disable !== 'boolean') return { state: 'error', reason: 'disable-required' };
183
+ const overrides = readOverrides(projectRoot);
184
+ if (overrides.enforcementDisabled === disable) {
185
+ return { state: 'noop', reason: disable ? 'already-off' : 'already-on' };
186
+ }
187
+ overrides.enforcementDisabled = disable;
188
+ writeOverrides(projectRoot, overrides);
189
+ return { state: disable ? 'off' : 'on' };
190
+ }
191
+
176
192
  // Toggle a single reminder by id. Same persistence file, same hot-reload
177
193
  // semantics — `reload-reminder.js` re-reads overrides on every invocation.
178
194
  function toggleReminderOverride(projectRoot, { id, disable }) {
@@ -202,6 +218,7 @@ module.exports = {
202
218
  writeOverrides,
203
219
  toggleHookOverride,
204
220
  toggleReminderOverride,
221
+ toggleEnforcementMode,
205
222
  normalizeHandlerPath,
206
223
  matchesOverride,
207
224
  };
package/lib/panel.js CHANGED
@@ -25,9 +25,21 @@ const {
25
25
  readOverrides,
26
26
  toggleHookOverride,
27
27
  toggleReminderOverride,
28
+ toggleEnforcementMode,
28
29
  normalizeHandlerPath,
29
30
  } = require('./hook-overrides.js');
30
31
 
32
+ function loadEnforcementCatalog(cwd) {
33
+ const p = path.join(cwd, '.claude', 'hooks', 'shared', 'enforcement-catalog.js');
34
+ if (!fs.existsSync(p)) return null;
35
+ try {
36
+ delete require.cache[require.resolve(p)];
37
+ return require(p);
38
+ } catch {
39
+ return null;
40
+ }
41
+ }
42
+
31
43
  // Lazy-load the reminder catalog from the template hook helper. The path
32
44
  // resolves the same way at install time and from the template repo.
33
45
  function loadReminderHelpers(cwd) {
@@ -112,6 +124,11 @@ function getHooksInfo(cwd) {
112
124
  for (const d of overrides.disabledHandlers || []) {
113
125
  disabledIndex.set(`${d.event}|${d.matcher || ''}|${d.handler}`, d);
114
126
  }
127
+ const catalog = loadEnforcementCatalog(cwd);
128
+ const isEnforcement = catalog && typeof catalog.isEnforcementHook === 'function'
129
+ ? catalog.isEnforcementHook
130
+ : () => false;
131
+ const enforcementDisabled = overrides.enforcementDisabled === true;
115
132
 
116
133
  // settings.json lists every hook (with each command wrapped through
117
134
  // run-with-override.js). The wrapper checks overrides at invocation time,
@@ -133,14 +150,21 @@ function getHooksInfo(cwd) {
133
150
  const isEmpty = !handlerPath;
134
151
  if (isEmpty) warnings++;
135
152
  const key = `${event}|${matcher}|${handlerPath}`;
136
- const isDisabled = disabledIndex.has(key);
153
+ const enforcement = isEnforcement(event, matcher, handlerPath);
154
+ const masterDisabled = enforcement && enforcementDisabled;
155
+ const manualDisabled = disabledIndex.has(key);
156
+ const isDisabled = manualDisabled || masterDisabled;
137
157
  if (isDisabled) disabledCount++;
138
158
  handlers.push({
139
159
  handler: handlerPath || '(empty command)',
140
160
  matcher,
141
161
  matcherDisplay: matcher || '(any)',
142
162
  empty: isEmpty,
163
+ enforcement,
143
164
  disabled: isDisabled,
165
+ disabledBy: masterDisabled && !manualDisabled ? 'enforcement-master'
166
+ : manualDisabled ? 'manual'
167
+ : null,
144
168
  rawCommand: cmd,
145
169
  });
146
170
  totalHandlers++;
@@ -244,6 +268,24 @@ function getActiveRunInfo(cwd) {
244
268
  };
245
269
  }
246
270
 
271
+ function getEnforcementInfo(cwd) {
272
+ const catalog = loadEnforcementCatalog(cwd);
273
+ const overrides = readOverrides(cwd);
274
+ if (!catalog || !Array.isArray(catalog.ENFORCEMENT_HOOKS)) {
275
+ return { available: false, on: !overrides.enforcementDisabled, count: 0 };
276
+ }
277
+ return {
278
+ available: true,
279
+ on: !overrides.enforcementDisabled,
280
+ count: catalog.ENFORCEMENT_HOOKS.length,
281
+ hooks: catalog.ENFORCEMENT_HOOKS.map((h) => ({
282
+ event: h.event,
283
+ handler: h.handler,
284
+ matcher: h.matcher,
285
+ })),
286
+ };
287
+ }
288
+
247
289
  function getRemindersInfo(cwd) {
248
290
  const helpers = loadReminderHelpers(cwd);
249
291
  if (!helpers || typeof helpers.listReminderCatalog !== 'function') {
@@ -288,6 +330,7 @@ function collectStatus(cwd) {
288
330
  setupContext: getSetupContextInfo(cwd),
289
331
  activeRun: getActiveRunInfo(cwd),
290
332
  reminders: getRemindersInfo(cwd),
333
+ enforcement: getEnforcementInfo(cwd),
291
334
  doctor: getDoctorInfo(cwd),
292
335
  };
293
336
  }
@@ -326,6 +369,19 @@ header .cwd { color: var(--muted); font-family: var(--mono); font-size: 12px; fl
326
369
  header .actions { display: flex; gap: 12px; align-items: center; color: var(--muted); font-size: 12px; }
327
370
  header button { background: var(--panel-2); border: 1px solid var(--border); color: var(--fg); padding: 6px 12px; border-radius: 6px; cursor: pointer; font: inherit; }
328
371
  header button:hover { border-color: var(--accent); }
372
+ .enforce-pill { display: inline-flex; align-items: center; gap: 8px; padding: 5px 12px; border-radius: 999px; border: 1px solid var(--border); background: var(--panel-2); font-size: 12px; transition: all 0.2s; }
373
+ .enforce-pill .enforce-label { color: var(--muted); font-family: var(--mono); letter-spacing: 0.5px; text-transform: uppercase; font-size: 11px; }
374
+ .enforce-pill .enforce-state { font-family: var(--mono); font-weight: 600; font-size: 12px; min-width: 26px; text-align: left; }
375
+ .enforce-pill[data-state="on"] { border-color: rgba(63, 185, 80, 0.5); background: rgba(63, 185, 80, 0.08); }
376
+ .enforce-pill[data-state="on"] .enforce-state { color: var(--ok); }
377
+ .enforce-pill[data-state="off"] { border-color: rgba(248, 81, 73, 0.6); background: rgba(248, 81, 73, 0.12); box-shadow: 0 0 0 1px rgba(248, 81, 73, 0.3); }
378
+ .enforce-pill[data-state="off"] .enforce-state { color: var(--err); }
379
+ .enforce-switch { position: relative; display: inline-block; width: 32px; height: 18px; }
380
+ .enforce-switch input { opacity: 0; width: 0; height: 0; }
381
+ .enforce-slider { position: absolute; cursor: pointer; inset: 0; background: var(--border); border-radius: 999px; transition: background 0.2s; }
382
+ .enforce-slider::before { content: ""; position: absolute; height: 14px; width: 14px; left: 2px; top: 2px; background: var(--fg); border-radius: 50%; transition: transform 0.2s; }
383
+ .enforce-switch input:checked + .enforce-slider { background: var(--ok); }
384
+ .enforce-switch input:checked + .enforce-slider::before { transform: translateX(14px); }
329
385
  main { display: flex; min-height: calc(100vh - 56px); }
330
386
  nav { width: 220px; flex-shrink: 0; border-right: 1px solid var(--border); background: var(--panel); padding: 12px 0; }
331
387
  nav a { display: flex; align-items: center; gap: 10px; padding: 10px 22px; color: var(--fg); text-decoration: none; cursor: pointer; border-left: 3px solid transparent; font-size: 13px; }
@@ -397,6 +453,11 @@ details[open] summary { padding-bottom: 8px; border-bottom: 1px solid var(--bord
397
453
  <span class="version" id="version">…</span>
398
454
  <span class="cwd" id="cwd">…</span>
399
455
  <span class="actions">
456
+ <span class="enforce-pill" id="enforce-pill" title="Master switch — toggles every blocking enforcement hook (freeze-step-contract, enforce-task-creation, stack-debt-guard, ecosystem-artifacts, workflow-enforcement). Validators and reminders are unaffected.">
457
+ <span class="enforce-label">Enforcement</span>
458
+ <label class="enforce-switch"><input type="checkbox" id="enforce-toggle" /><span class="enforce-slider"></span></label>
459
+ <span class="enforce-state" id="enforce-state">…</span>
460
+ </span>
400
461
  <label class="toggle"><input type="checkbox" id="auto" checked /> auto-refresh 5s</label>
401
462
  <button id="refresh">Refresh</button>
402
463
  </span>
@@ -486,6 +547,21 @@ async function refresh() {
486
547
  function renderHeader() {
487
548
  document.getElementById('version').textContent = 'v' + state.version.installed;
488
549
  document.getElementById('cwd').textContent = state.cwd;
550
+ const ef = state.enforcement;
551
+ const pill = document.getElementById('enforce-pill');
552
+ const cb = document.getElementById('enforce-toggle');
553
+ const lbl = document.getElementById('enforce-state');
554
+ if (ef && ef.available) {
555
+ pill.style.display = '';
556
+ pill.dataset.state = ef.on ? 'on' : 'off';
557
+ cb.checked = ef.on;
558
+ lbl.textContent = ef.on ? 'ON' : 'OFF';
559
+ pill.title = ef.on
560
+ ? 'Enforcement is ON — ' + ef.count + ' blocking hook(s) active. Click to disable all.'
561
+ : 'Enforcement is OFF — ' + ef.count + ' blocking hook(s) silenced. Reminders + validators still run.';
562
+ } else {
563
+ pill.style.display = 'none';
564
+ }
489
565
  }
490
566
 
491
567
  function severityFor(id) {
@@ -563,6 +639,9 @@ function renderOverview() {
563
639
  const rm = s.reminders && s.reminders.available
564
640
  ? \`\${s.reminders.activeCount} active · \${s.reminders.disabledCount} disabled\`
565
641
  : 'unavailable';
642
+ const ef = s.enforcement && s.enforcement.available
643
+ ? (s.enforcement.on ? \`ON (\${s.enforcement.count} blocking hooks)\` : \`OFF — \${s.enforcement.count} blocking hooks silenced\`)
644
+ : 'unavailable';
566
645
  return \`<h2>Overview</h2>
567
646
  <p class="sub">Snapshot at \${new Date(s.timestamp).toLocaleTimeString()}</p>
568
647
  <div class="card">
@@ -573,6 +652,7 @@ function renderOverview() {
573
652
  \${row('Setup context', sc, { kind: s.setupContext.exists ? (s.setupContext.toolingComplete ? 'ok' : 'warn') : 'err', text: s.setupContext.exists ? (s.setupContext.toolingComplete ? '✓' : '!') : '✗' })}
574
653
  \${row('Active run', ar, { kind: 'info', text: s.activeRun.active ? '·' : '·' })}
575
654
  \${row('Reminders', rm, { kind: s.reminders && s.reminders.available ? (s.reminders.disabledCount ? 'info' : 'ok') : 'err', text: s.reminders && s.reminders.disabledCount ? '·' : '✓' })}
655
+ \${row('Enforcement', ef, { kind: s.enforcement && s.enforcement.available ? (s.enforcement.on ? 'ok' : 'err') : 'err', text: s.enforcement && s.enforcement.available ? (s.enforcement.on ? '✓' : '✗') : '·' })}
576
656
  \${row('Doctor', dc, { kind: s.doctor.issueCount === 0 ? 'ok' : 'warn', text: s.doctor.issueCount === 0 ? '✓' : '!' })}
577
657
  </div>\`;
578
658
  }
@@ -603,10 +683,13 @@ function renderHooks() {
603
683
  const cls = hd.disabled ? 'handler disabled-row' : 'handler';
604
684
  const checked = hd.disabled ? '' : 'checked';
605
685
  const dataset = \`data-event="\${escapeHtml(ev.event)}" data-matcher="\${escapeHtml(hd.matcher)}" data-handler="\${escapeHtml(hd.handler)}"\`;
606
- const status = hd.disabled ? '<span class="badge info">disabled</span>' : '';
686
+ const enforcementBadge = hd.enforcement ? '<span class="badge warn" title="Member of the enforcement catalog — affected by the master switch">enforcement</span>' : '';
687
+ const masterBadge = hd.disabledBy === 'enforcement-master' ? '<span class="badge err" title="Silenced by the master Enforcement OFF switch in the header">master OFF</span>' : '';
688
+ const manualBadge = hd.disabledBy === 'manual' ? '<span class="badge info">disabled</span>' : '';
689
+ const lockedByMaster = hd.disabledBy === 'enforcement-master';
607
690
  return \`<div class="\${cls}">
608
- <label class="toggle"><input type="checkbox" class="hook-toggle" \${dataset} \${checked} \${hd.empty ? 'disabled' : ''} />
609
- <span>\${escapeHtml(hd.handler)} \${status} \${mark}</span></label>
691
+ <label class="toggle"><input type="checkbox" class="hook-toggle" \${dataset} \${checked} \${hd.empty || lockedByMaster ? 'disabled' : ''} />
692
+ <span>\${escapeHtml(hd.handler)} \${enforcementBadge} \${masterBadge} \${manualBadge} \${mark}</span></label>
610
693
  <span class="matcher">\${escapeHtml(hd.matcherDisplay)}</span>
611
694
  </div>\`;
612
695
  }).join('');
@@ -773,6 +856,34 @@ document.addEventListener('toggle', (e) => {
773
856
  }
774
857
  }, true);
775
858
 
859
+ async function toggleEnforcement(disable) {
860
+ try {
861
+ const r = await fetch('/api/enforcement/toggle', {
862
+ method: 'POST',
863
+ headers: { 'Content-Type': 'application/json' },
864
+ body: JSON.stringify({ disable }),
865
+ });
866
+ const result = await r.json();
867
+ if (!r.ok || result.state === 'error') {
868
+ showToast('Toggle failed: ' + (result.reason || result.error || 'unknown'), 'err');
869
+ } else if (result.state === 'noop') {
870
+ showToast('Enforcement already in target state', 'ok');
871
+ } else {
872
+ showToast(disable
873
+ ? 'Enforcement OFF — blocking hooks silenced. Validators + reminders still run.'
874
+ : 'Enforcement ON — full blocking restored.',
875
+ disable ? 'err' : 'ok');
876
+ }
877
+ } catch (e) {
878
+ showToast('Network error: ' + e.message, 'err');
879
+ }
880
+ await refresh();
881
+ }
882
+
883
+ document.getElementById('enforce-toggle').onchange = (e) => {
884
+ toggleEnforcement(!e.target.checked);
885
+ };
886
+
776
887
  document.getElementById('refresh').onclick = refresh;
777
888
  document.getElementById('auto').onchange = (e) => {
778
889
  if (timer) clearInterval(timer);
@@ -842,6 +953,20 @@ function handler(cwd) {
842
953
  return send(status, JSON.stringify(result), 'application/json');
843
954
  }
844
955
 
956
+ if (req.method === 'POST' && route === '/api/enforcement/toggle') {
957
+ const raw = await readRequestBody(req);
958
+ let payload;
959
+ try { payload = JSON.parse(raw); }
960
+ catch { return send(400, JSON.stringify({ error: 'invalid json' }), 'application/json'); }
961
+ const { disable } = payload;
962
+ if (typeof disable !== 'boolean') {
963
+ return send(400, JSON.stringify({ error: 'disable required' }), 'application/json');
964
+ }
965
+ const result = toggleEnforcementMode(cwd, { disable });
966
+ const status = result.state === 'error' ? 500 : 200;
967
+ return send(status, JSON.stringify(result), 'application/json');
968
+ }
969
+
845
970
  if (req.method === 'POST' && route === '/api/reminders/toggle') {
846
971
  const raw = await readRequestBody(req);
847
972
  let payload;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axiomatic-labs/claudeflow",
3
- "version": "2.13.24",
3
+ "version": "2.13.25",
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"