@axiomatic-labs/claudeflow 2.13.23 → 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: [] };
54
+ return { version: 1, disabledHandlers: [], disabledReminders: [], enforcementDisabled: false };
55
55
  }
56
56
 
57
57
  function readOverrides(projectRoot) {
@@ -60,6 +60,8 @@ function readOverrides(projectRoot) {
60
60
  return {
61
61
  version: data.version || 1,
62
62
  disabledHandlers: Array.isArray(data.disabledHandlers) ? data.disabledHandlers : [],
63
+ disabledReminders: Array.isArray(data.disabledReminders) ? data.disabledReminders : [],
64
+ enforcementDisabled: data.enforcementDisabled === true,
63
65
  };
64
66
  }
65
67
 
@@ -172,6 +174,41 @@ function toggleHookOverride(projectRoot, { event, matcher = '', handler, disable
172
174
  return { state: 'enabled' };
173
175
  }
174
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
+
192
+ // Toggle a single reminder by id. Same persistence file, same hot-reload
193
+ // semantics — `reload-reminder.js` re-reads overrides on every invocation.
194
+ function toggleReminderOverride(projectRoot, { id, disable }) {
195
+ if (typeof id !== 'string' || !id.trim()) return { state: 'error', reason: 'id-required' };
196
+ const overrides = readOverrides(projectRoot);
197
+ const list = overrides.disabledReminders;
198
+ const idx = list.indexOf(id);
199
+
200
+ if (disable) {
201
+ if (idx !== -1) return { state: 'noop', reason: 'already-disabled' };
202
+ list.push(id);
203
+ writeOverrides(projectRoot, overrides);
204
+ return { state: 'disabled' };
205
+ }
206
+ if (idx === -1) return { state: 'noop', reason: 'not-disabled' };
207
+ list.splice(idx, 1);
208
+ writeOverrides(projectRoot, overrides);
209
+ return { state: 'enabled' };
210
+ }
211
+
175
212
  module.exports = {
176
213
  OVERRIDES_REL,
177
214
  SETTINGS_REL,
@@ -180,6 +217,8 @@ module.exports = {
180
217
  readOverrides,
181
218
  writeOverrides,
182
219
  toggleHookOverride,
220
+ toggleReminderOverride,
221
+ toggleEnforcementMode,
183
222
  normalizeHandlerPath,
184
223
  matchesOverride,
185
224
  };
package/lib/panel.js CHANGED
@@ -24,9 +24,38 @@ const {
24
24
  const {
25
25
  readOverrides,
26
26
  toggleHookOverride,
27
+ toggleReminderOverride,
28
+ toggleEnforcementMode,
27
29
  normalizeHandlerPath,
28
30
  } = require('./hook-overrides.js');
29
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
+
43
+ // Lazy-load the reminder catalog from the template hook helper. The path
44
+ // resolves the same way at install time and from the template repo.
45
+ function loadReminderHelpers(cwd) {
46
+ const candidates = [
47
+ path.join(cwd, '.claude', 'hooks', 'shared', 'reload-reminder.js'),
48
+ ];
49
+ for (const c of candidates) {
50
+ if (fs.existsSync(c)) {
51
+ // Bypass require cache so updates to the template are picked up.
52
+ delete require.cache[require.resolve(c)];
53
+ return require(c);
54
+ }
55
+ }
56
+ return null;
57
+ }
58
+
30
59
  const IDLE_SHUTDOWN_MS = 30 * 60 * 1000; // 30 min
31
60
 
32
61
  // ─── data collectors ─────────────────────────────────────────────
@@ -95,6 +124,11 @@ function getHooksInfo(cwd) {
95
124
  for (const d of overrides.disabledHandlers || []) {
96
125
  disabledIndex.set(`${d.event}|${d.matcher || ''}|${d.handler}`, d);
97
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;
98
132
 
99
133
  // settings.json lists every hook (with each command wrapped through
100
134
  // run-with-override.js). The wrapper checks overrides at invocation time,
@@ -116,14 +150,21 @@ function getHooksInfo(cwd) {
116
150
  const isEmpty = !handlerPath;
117
151
  if (isEmpty) warnings++;
118
152
  const key = `${event}|${matcher}|${handlerPath}`;
119
- 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;
120
157
  if (isDisabled) disabledCount++;
121
158
  handlers.push({
122
159
  handler: handlerPath || '(empty command)',
123
160
  matcher,
124
161
  matcherDisplay: matcher || '(any)',
125
162
  empty: isEmpty,
163
+ enforcement,
126
164
  disabled: isDisabled,
165
+ disabledBy: masterDisabled && !manualDisabled ? 'enforcement-master'
166
+ : manualDisabled ? 'manual'
167
+ : null,
127
168
  rawCommand: cmd,
128
169
  });
129
170
  totalHandlers++;
@@ -227,6 +268,47 @@ function getActiveRunInfo(cwd) {
227
268
  };
228
269
  }
229
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
+
289
+ function getRemindersInfo(cwd) {
290
+ const helpers = loadReminderHelpers(cwd);
291
+ if (!helpers || typeof helpers.listReminderCatalog !== 'function') {
292
+ return { available: false, reminders: [], disabledCount: 0 };
293
+ }
294
+ const catalog = helpers.listReminderCatalog();
295
+ const overrides = readOverrides(cwd);
296
+ const disabledSet = new Set(overrides.disabledReminders || []);
297
+ const reminders = catalog.map((r) => ({
298
+ id: r.id,
299
+ label: r.label,
300
+ description: r.description,
301
+ slot: r.slot,
302
+ disabled: disabledSet.has(r.id),
303
+ }));
304
+ return {
305
+ available: true,
306
+ reminders,
307
+ disabledCount: reminders.filter((r) => r.disabled).length,
308
+ activeCount: reminders.filter((r) => !r.disabled).length,
309
+ };
310
+ }
311
+
230
312
  function getDoctorInfo(cwd) {
231
313
  const checks = [checkCdpPortMismatch(cwd), checkStaleLockfiles(cwd)];
232
314
  const issues = checks.filter((c) => c.severity !== 'ok' && c.severity !== 'info');
@@ -247,6 +329,8 @@ function collectStatus(cwd) {
247
329
  mcp: getMcpInfo(cwd),
248
330
  setupContext: getSetupContextInfo(cwd),
249
331
  activeRun: getActiveRunInfo(cwd),
332
+ reminders: getRemindersInfo(cwd),
333
+ enforcement: getEnforcementInfo(cwd),
250
334
  doctor: getDoctorInfo(cwd),
251
335
  };
252
336
  }
@@ -285,6 +369,19 @@ header .cwd { color: var(--muted); font-family: var(--mono); font-size: 12px; fl
285
369
  header .actions { display: flex; gap: 12px; align-items: center; color: var(--muted); font-size: 12px; }
286
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; }
287
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); }
288
385
  main { display: flex; min-height: calc(100vh - 56px); }
289
386
  nav { width: 220px; flex-shrink: 0; border-right: 1px solid var(--border); background: var(--panel); padding: 12px 0; }
290
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; }
@@ -338,6 +435,12 @@ details[open] summary { padding-bottom: 8px; border-bottom: 1px solid var(--bord
338
435
  .toggle input { accent-color: var(--accent); }
339
436
  .disabled-row { opacity: 0.55; }
340
437
  .disabled-row .matcher { font-style: italic; }
438
+ .reminder-row { padding: 10px 0; border-bottom: 1px solid var(--border); }
439
+ .reminder-row:last-child { border-bottom: 0; }
440
+ .reminder-row .reminder-label { font-size: 13px; }
441
+ .reminder-row .reminder-desc { font-size: 12px; margin-left: 22px; margin-top: 2px; }
442
+ .reminder-row .reminder-id { font-size: 11px; margin-left: 22px; margin-top: 2px; }
443
+ .reminder-row code { background: var(--panel-2); padding: 1px 5px; border-radius: 3px; font-size: 11px; }
341
444
  .toast { position: fixed; bottom: 18px; right: 18px; background: var(--panel); border: 1px solid var(--border); padding: 10px 14px; border-radius: 6px; font-size: 13px; box-shadow: 0 4px 12px rgba(0,0,0,0.4); }
342
445
  .toast.err { border-color: var(--err); }
343
446
  .toast.ok { border-color: var(--ok); }
@@ -350,6 +453,11 @@ details[open] summary { padding-bottom: 8px; border-bottom: 1px solid var(--bord
350
453
  <span class="version" id="version">…</span>
351
454
  <span class="cwd" id="cwd">…</span>
352
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>
353
461
  <label class="toggle"><input type="checkbox" id="auto" checked /> auto-refresh 5s</label>
354
462
  <button id="refresh">Refresh</button>
355
463
  </span>
@@ -363,6 +471,7 @@ const SECTIONS = [
363
471
  { id: 'overview', label: 'Overview' },
364
472
  { id: 'claudeMd', label: 'CLAUDE.md' },
365
473
  { id: 'hooks', label: 'Hooks' },
474
+ { id: 'reminders', label: 'Reminders' },
366
475
  { id: 'mcp', label: 'MCP & observer' },
367
476
  { id: 'setupContext', label: 'Setup context' },
368
477
  { id: 'activeRun', label: 'Active run' },
@@ -438,6 +547,21 @@ async function refresh() {
438
547
  function renderHeader() {
439
548
  document.getElementById('version').textContent = 'v' + state.version.installed;
440
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
+ }
441
565
  }
442
566
 
443
567
  function severityFor(id) {
@@ -446,6 +570,7 @@ function severityFor(id) {
446
570
  case 'overview': return s.doctor.issueCount > 0 ? 'warn' : 'ok';
447
571
  case 'claudeMd': return !s.claudeMd.exists ? 'err' : (s.claudeMd.optOut ? 'info' : 'ok');
448
572
  case 'hooks': return s.hooks.warnings > 0 ? 'warn' : 'ok';
573
+ case 'reminders': return s.reminders && s.reminders.disabledCount > 0 ? 'info' : 'ok';
449
574
  case 'mcp': return !s.mcp.configFound ? 'info' : (s.mcp.playwright.match && s.mcp.staleLockfiles.length === 0 ? 'ok' : 'warn');
450
575
  case 'setupContext': return !s.setupContext.exists ? 'err' : (s.setupContext.toolingComplete ? 'ok' : 'warn');
451
576
  case 'activeRun': return s.activeRun.active ? 'info' : 'info';
@@ -489,6 +614,7 @@ function renderContent() {
489
614
  overview: renderOverview,
490
615
  claudeMd: renderClaudeMd,
491
616
  hooks: renderHooks,
617
+ reminders: renderReminders,
492
618
  mcp: renderMcp,
493
619
  setupContext: renderSetup,
494
620
  activeRun: renderRun,
@@ -510,6 +636,12 @@ function renderOverview() {
510
636
  const sc = !s.setupContext.exists ? 'missing' : (s.setupContext.toolingComplete ? 'complete' : 'incomplete (' + s.setupContext.missingTooling.join(', ') + ')');
511
637
  const ar = s.activeRun.active ? s.activeRun.runId : 'none';
512
638
  const dc = s.doctor.issueCount === 0 ? '0 issues' : s.doctor.issueCount + ' issue(s) — run \`claudeflow doctor\`';
639
+ const rm = s.reminders && s.reminders.available
640
+ ? \`\${s.reminders.activeCount} active · \${s.reminders.disabledCount} disabled\`
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';
513
645
  return \`<h2>Overview</h2>
514
646
  <p class="sub">Snapshot at \${new Date(s.timestamp).toLocaleTimeString()}</p>
515
647
  <div class="card">
@@ -519,6 +651,8 @@ function renderOverview() {
519
651
  \${row('MCP & observer', mc, { kind: !s.mcp.configFound ? 'info' : (s.mcp.playwright.match ? 'ok' : 'warn'), text: !s.mcp.configFound ? '·' : (s.mcp.playwright.match ? '✓' : '!') })}
520
652
  \${row('Setup context', sc, { kind: s.setupContext.exists ? (s.setupContext.toolingComplete ? 'ok' : 'warn') : 'err', text: s.setupContext.exists ? (s.setupContext.toolingComplete ? '✓' : '!') : '✗' })}
521
653
  \${row('Active run', ar, { kind: 'info', text: s.activeRun.active ? '·' : '·' })}
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 ? '✓' : '✗') : '·' })}
522
656
  \${row('Doctor', dc, { kind: s.doctor.issueCount === 0 ? 'ok' : 'warn', text: s.doctor.issueCount === 0 ? '✓' : '!' })}
523
657
  </div>\`;
524
658
  }
@@ -549,10 +683,13 @@ function renderHooks() {
549
683
  const cls = hd.disabled ? 'handler disabled-row' : 'handler';
550
684
  const checked = hd.disabled ? '' : 'checked';
551
685
  const dataset = \`data-event="\${escapeHtml(ev.event)}" data-matcher="\${escapeHtml(hd.matcher)}" data-handler="\${escapeHtml(hd.handler)}"\`;
552
- 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';
553
690
  return \`<div class="\${cls}">
554
- <label class="toggle"><input type="checkbox" class="hook-toggle" \${dataset} \${checked} \${hd.empty ? 'disabled' : ''} />
555
- <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>
556
693
  <span class="matcher">\${escapeHtml(hd.matcherDisplay)}</span>
557
694
  </div>\`;
558
695
  }).join('');
@@ -564,6 +701,35 @@ function renderHooks() {
564
701
  \${items}\`;
565
702
  }
566
703
 
704
+ function renderReminders() {
705
+ const r = state.reminders;
706
+ if (!r || !r.available) return '<h2>Reminders</h2><p class="error">Reminder catalog unavailable (template helper not found).</p>';
707
+ const slotLabel = { reads: 'Reads / anchors', rules: 'Rules', 'claude-md': 'CLAUDE.md content' };
708
+ const grouped = {};
709
+ for (const item of r.reminders) {
710
+ if (!grouped[item.slot]) grouped[item.slot] = [];
711
+ grouped[item.slot].push(item);
712
+ }
713
+ const sections = Object.entries(grouped).map(([slot, items]) => {
714
+ const rows = items.map(item => {
715
+ const checked = item.disabled ? '' : 'checked';
716
+ const dataset = \`data-id="\${escapeHtml(item.id)}"\`;
717
+ const status = item.disabled ? '<span class="badge info">disabled</span>' : '';
718
+ const cls = item.disabled ? 'reminder-row disabled-row' : 'reminder-row';
719
+ return \`<div class="\${cls}">
720
+ <label class="toggle"><input type="checkbox" class="reminder-toggle" \${dataset} \${checked} />
721
+ <span class="reminder-label"><strong>\${escapeHtml(item.label)}</strong> \${status}</span></label>
722
+ <div class="reminder-desc muted">\${escapeHtml(item.description)}</div>
723
+ <div class="reminder-id muted">id: <code>\${escapeHtml(item.id)}</code></div>
724
+ </div>\`;
725
+ }).join('');
726
+ return \`<details open><summary>\${escapeHtml(slotLabel[slot] || slot)} <span class="muted">(\${items.length})</span></summary>\${rows}</details>\`;
727
+ }).join('');
728
+ return \`<h2>Reminders</h2>
729
+ <p class="sub">\${r.activeCount} active · \${r.disabledCount} disabled. Toggles are read by <code>reload-reminder.js</code> on every hook invocation — disabling takes effect on the next user prompt without reloading Claude Code.</p>
730
+ \${sections}\`;
731
+ }
732
+
567
733
  function renderMcp() {
568
734
  const m = state.mcp;
569
735
  if (!m.configFound) return '<h2>MCP & observer</h2><p class="muted">.mcp.json not found</p>';
@@ -644,14 +810,40 @@ async function toggleHook(event, matcher, handler, disable) {
644
810
  await refresh();
645
811
  }
646
812
 
813
+ async function toggleReminder(id, disable) {
814
+ try {
815
+ const r = await fetch('/api/reminders/toggle', {
816
+ method: 'POST',
817
+ headers: { 'Content-Type': 'application/json' },
818
+ body: JSON.stringify({ id, disable }),
819
+ });
820
+ const result = await r.json();
821
+ if (!r.ok || result.state === 'error') {
822
+ showToast('Toggle failed: ' + (result.reason || result.error || 'unknown'), 'err');
823
+ } else if (result.state === 'noop') {
824
+ showToast('Already in target state', 'ok');
825
+ } else {
826
+ showToast(disable ? 'Reminder silenced. Next prompt will skip it.' : 'Reminder restored. Next prompt will include it.', 'ok');
827
+ }
828
+ } catch (e) {
829
+ showToast('Network error: ' + e.message, 'err');
830
+ }
831
+ await refresh();
832
+ }
833
+
647
834
  document.addEventListener('change', (e) => {
648
835
  const t = e.target;
649
- if (t && t.classList && t.classList.contains('hook-toggle')) {
836
+ if (!t || !t.classList) return;
837
+ if (t.classList.contains('hook-toggle')) {
650
838
  const event = t.dataset.event;
651
839
  const matcher = t.dataset.matcher;
652
840
  const handler = t.dataset.handler;
653
841
  const disable = !t.checked;
654
842
  toggleHook(event, matcher, handler, disable);
843
+ } else if (t.classList.contains('reminder-toggle')) {
844
+ const id = t.dataset.id;
845
+ const disable = !t.checked;
846
+ toggleReminder(id, disable);
655
847
  }
656
848
  });
657
849
 
@@ -664,6 +856,34 @@ document.addEventListener('toggle', (e) => {
664
856
  }
665
857
  }, true);
666
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
+
667
887
  document.getElementById('refresh').onclick = refresh;
668
888
  document.getElementById('auto').onchange = (e) => {
669
889
  if (timer) clearInterval(timer);
@@ -733,6 +953,34 @@ function handler(cwd) {
733
953
  return send(status, JSON.stringify(result), 'application/json');
734
954
  }
735
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
+
970
+ if (req.method === 'POST' && route === '/api/reminders/toggle') {
971
+ const raw = await readRequestBody(req);
972
+ let payload;
973
+ try { payload = JSON.parse(raw); }
974
+ catch { return send(400, JSON.stringify({ error: 'invalid json' }), 'application/json'); }
975
+ const { id, disable } = payload;
976
+ if (typeof id !== 'string' || typeof disable !== 'boolean') {
977
+ return send(400, JSON.stringify({ error: 'id, disable required' }), 'application/json');
978
+ }
979
+ const result = toggleReminderOverride(cwd, { id, disable });
980
+ const status = result.state === 'error' ? 500 : 200;
981
+ return send(status, JSON.stringify(result), 'application/json');
982
+ }
983
+
736
984
  send(404, JSON.stringify({ error: 'not found' }), 'application/json');
737
985
  } catch (err) {
738
986
  res.writeHead(500, { 'Content-Type': 'application/json' });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axiomatic-labs/claudeflow",
3
- "version": "2.13.23",
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"