@axiomatic-labs/claudeflow 2.13.24 → 2.13.26

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/install.js CHANGED
@@ -334,6 +334,7 @@ function ensureGlobalCli(version) {
334
334
  autoInstalled: !needsUpgrade,
335
335
  upgraded: needsUpgrade,
336
336
  fromVersion: installedVersion || undefined,
337
+ shellPath: verifyShellCanFindCli(),
337
338
  };
338
339
  }
339
340
  return {
@@ -368,6 +369,67 @@ function readGlobalCliVersion() {
368
369
  }
369
370
  }
370
371
 
372
+ // Returns { ok: boolean | null, binPath?: string, shellRcFile?: string,
373
+ // nvmDetected?: boolean, suggestedFix?: string }
374
+ //
375
+ // Distinguishes "package installed globally" from "package findable from
376
+ // the user's interactive shell". Catches the common nvm-lazy-load setup
377
+ // where `npm install -g` writes a binary the shell can't see until nvm
378
+ // is sourced — typically discovered only when the user opens a fresh
379
+ // terminal and types `claudeflow` and gets "command not found".
380
+ function verifyShellCanFindCli() {
381
+ // Skip on Windows; lazy-load + nvm-windows behave differently and the
382
+ // login-shell trick below is brittle there.
383
+ if (process.platform === 'win32') return { ok: null };
384
+
385
+ const shellPath = process.env.SHELL;
386
+ if (!shellPath || !fs.existsSync(shellPath)) return { ok: null };
387
+
388
+ // Run a non-inherited interactive login shell — that's the closest
389
+ // simulation of "user opens a new terminal" without polluting the
390
+ // current process.
391
+ let foundOnUserPath = null;
392
+ try {
393
+ const out = execSync(`${shellPath} -lic 'command -v claudeflow 2>/dev/null'`, {
394
+ encoding: 'utf8',
395
+ timeout: 6000,
396
+ stdio: ['ignore', 'pipe', 'ignore'],
397
+ }).trim();
398
+ foundOnUserPath = out.length > 0;
399
+ } catch {
400
+ // Some shells exit non-zero when `command -v` finds nothing — treat
401
+ // that as "not found" rather than as an inconclusive check.
402
+ foundOnUserPath = false;
403
+ }
404
+
405
+ if (foundOnUserPath) return { ok: true };
406
+
407
+ // Resolve where the binary actually lives so the warning is concrete.
408
+ let binPath = null;
409
+ try {
410
+ const prefix = execSync('npm config get prefix', { encoding: 'utf8', timeout: 5000 }).trim();
411
+ binPath = path.join(prefix, 'bin', 'claudeflow');
412
+ } catch {}
413
+
414
+ const home = process.env.HOME || '';
415
+ const nvmDetected = !!process.env.NVM_DIR
416
+ || fs.existsSync(path.join(home, '.nvm', 'nvm.sh'));
417
+ const isZsh = shellPath.endsWith('/zsh');
418
+ const shellRcFile = isZsh ? '~/.zshrc' : (shellPath.endsWith('/bash') ? '~/.bashrc' : '~/.profile');
419
+
420
+ // Wrapper-function fix that mirrors the user's existing nvm lazy-load
421
+ // pattern (e.g., npx, node, npm). Single line, paste into rcFile, done.
422
+ const wrapperFix = `claudeflow() { unset -f claudeflow; [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"; claudeflow "$@"; }`;
423
+
424
+ return {
425
+ ok: false,
426
+ binPath,
427
+ shellRcFile,
428
+ nvmDetected,
429
+ suggestedFix: wrapperFix,
430
+ };
431
+ }
432
+
371
433
  function showGettingStarted(cwd, cliStatus = { available: false, autoInstalled: false }) {
372
434
  const MANIFESTS = ['package.json', 'pyproject.toml', 'Gemfile', 'go.mod', 'Cargo.toml', 'composer.json'];
373
435
  const SKIP_DIRS = new Set(['node_modules', 'vendor', '__pycache__', 'dist', 'build', '.next', '.nuxt', '.output', '.claude']);
@@ -401,7 +463,27 @@ function showGettingStarted(cwd, cliStatus = { available: false, autoInstalled:
401
463
  ` ${ui.DIM}✓ Upgraded global ${ui.CYAN}claudeflow${ui.RESET}${ui.DIM} CLI${cliStatus.fromVersion ? ` from ${cliStatus.fromVersion}` : ''}.${ui.RESET}`
402
464
  );
403
465
  console.log('');
404
- } else if (!cliStatus.available) {
466
+ }
467
+
468
+ // Shell PATH warning — if the global package was installed but the
469
+ // user's interactive shell can't find the binary (typical with
470
+ // nvm lazy-load), emit a concrete fix.
471
+ if (cliStatus.shellPath && cliStatus.shellPath.ok === false) {
472
+ const sp = cliStatus.shellPath;
473
+ console.log(` ${ui.YELLOW}⚠ ${ui.BOLD}claudeflow${ui.RESET}${ui.YELLOW} CLI installed but NOT on your shell PATH.${ui.RESET}`);
474
+ if (sp.binPath) {
475
+ console.log(` ${ui.DIM}Binary lives at: ${sp.binPath}${ui.RESET}`);
476
+ }
477
+ if (sp.nvmDetected) {
478
+ console.log(` ${ui.DIM}Likely cause: nvm lazy-load. Add this to ${sp.shellRcFile}, then reopen terminal:${ui.RESET}`);
479
+ console.log(` ${ui.CYAN}${sp.suggestedFix}${ui.RESET}`);
480
+ } else {
481
+ console.log(` ${ui.DIM}Add the npm prefix bin directory to your PATH in ${sp.shellRcFile}.${ui.RESET}`);
482
+ }
483
+ console.log('');
484
+ }
485
+
486
+ if (!cliStatus.available) {
405
487
  console.log(` ${ui.DIM}To get the ${ui.CYAN}claudeflow${ui.RESET}${ui.DIM} shell command, run:${ui.RESET}`);
406
488
  console.log(` ${ui.CYAN}npm i -g @axiomatic-labs/claudeflow${ui.RESET}`);
407
489
  if (cliStatus.error && cliStatus.error !== 'skipped') {
@@ -434,14 +516,21 @@ function showGettingStarted(cwd, cliStatus = { available: false, autoInstalled:
434
516
  }
435
517
 
436
518
  console.log('');
437
- console.log(` ${ui.BOLD}Commands:${ui.RESET}`);
519
+ console.log(` ${ui.BOLD}Slash commands (inside Claude Code):${ui.RESET}`);
438
520
  console.log(` ${ui.CYAN}/claudeflow-install${ui.RESET} ${ui.DIM}Route setup automatically for new or existing projects${ui.RESET}`);
439
521
  console.log(` ${ui.CYAN}/claudeflow-build${ui.RESET} ${ui.DIM}Execute an approved spec for larger or higher-risk changes${ui.RESET}`);
440
522
  console.log(` ${ui.CYAN}/claudeflow-design-tokens${ui.RESET} ${ui.DIM}Create design reference from a site or screenshot${ui.RESET}`);
441
523
  console.log(` ${ui.CYAN}/claudeflow-create-ui${ui.RESET} ${ui.DIM}Build pages from visual references${ui.RESET}`);
442
524
  console.log(` ${ui.CYAN}/claudeflow-checkpoints${ui.RESET} ${ui.DIM}Auto-save work with git checkpoints${ui.RESET}`);
443
525
  console.log(` ${ui.CYAN}/claudeflow-add-skill${ui.RESET} ${ui.DIM}Add new technology skills (e.g. /claudeflow-add-skill stripe)${ui.RESET}`);
444
- console.log(` ${ui.CYAN}/claudeflow-add-tools${ui.RESET} ${ui.DIM}Discover and install MCP servers + CLI tools${ui.RESET}`);
526
+ console.log(` ${ui.CYAN}/claudeflow-add-tools${ui.RESET} ${ui.DIM}Discover and install MCP servers + CLI tools${ui.RESET}`);
527
+ console.log('');
528
+ console.log(` ${ui.BOLD}Shell commands (outside Claude Code):${ui.RESET}`);
529
+ console.log(` ${ui.CYAN}claudeflow${ui.RESET} ${ui.DIM}Start Claude Code with this project's claudeflow context${ui.RESET}`);
530
+ console.log(` ${ui.CYAN}claudeflow panel${ui.RESET} ${ui.DIM}Open the local web dashboard — toggle hooks, reminders, enforcement${ui.RESET}`);
531
+ console.log(` ${ui.CYAN}claudeflow doctor${ui.RESET} ${ui.DIM}Diagnose CDP-port and lockfile issues; ${ui.CYAN}--fix${ui.RESET}${ui.DIM} to auto-repair${ui.RESET}`);
532
+ console.log(` ${ui.CYAN}claudeflow install${ui.RESET} ${ui.DIM}Refresh the template + global CLI in this project${ui.RESET}`);
533
+ console.log(` ${ui.CYAN}claudeflow version${ui.RESET} ${ui.DIM}Show installed framework + CLI versions${ui.RESET}`);
445
534
  console.log('');
446
535
  console.log(` ${ui.DIM}Docs: https://claudeflow.dev${ui.RESET}`);
447
536
  console.log('');
@@ -1107,4 +1196,6 @@ module.exports = Object.assign(run, {
1107
1196
  mergeClaudeSettings,
1108
1197
  commandExists,
1109
1198
  readGlobalCliVersion,
1199
+ verifyShellCanFindCli,
1200
+ showGettingStarted,
1110
1201
  });
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.26",
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"