@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.
- package/lib/hook-overrides.js +40 -1
- package/lib/panel.js +253 -5
- package/package.json +1 -1
package/lib/hook-overrides.js
CHANGED
|
@@ -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
|
|
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
|
|
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)} \${
|
|
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
|
|
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.
|
|
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"
|