deepflow 0.1.103 → 0.1.105

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.
Files changed (62) hide show
  1. package/bin/install-dynamic-hooks.test.js +461 -0
  2. package/bin/install.js +171 -250
  3. package/bin/install.test.js +205 -0
  4. package/bin/lineage-ingest.js +70 -0
  5. package/hooks/df-check-update.js +1 -0
  6. package/hooks/df-command-usage.js +18 -0
  7. package/hooks/df-dashboard-push.js +5 -4
  8. package/hooks/df-dashboard-push.test.js +256 -0
  9. package/hooks/df-execution-history.js +1 -0
  10. package/hooks/df-explore-protocol.js +83 -0
  11. package/hooks/df-explore-protocol.test.js +228 -0
  12. package/hooks/df-hook-event-tags.test.js +127 -0
  13. package/hooks/df-invariant-check.js +4 -3
  14. package/hooks/df-invariant-check.test.js +141 -0
  15. package/hooks/df-quota-logger.js +12 -23
  16. package/hooks/df-quota-logger.test.js +324 -0
  17. package/hooks/df-snapshot-guard.js +1 -0
  18. package/hooks/df-spec-lint.js +58 -1
  19. package/hooks/df-spec-lint.test.js +412 -0
  20. package/hooks/df-statusline.js +1 -0
  21. package/hooks/df-subagent-registry.js +1 -0
  22. package/hooks/df-tool-usage.js +13 -3
  23. package/hooks/df-worktree-guard.js +1 -0
  24. package/package.json +1 -1
  25. package/src/commands/df/debate.md +1 -1
  26. package/src/commands/df/eval.md +117 -0
  27. package/src/commands/df/execute.md +1 -1
  28. package/src/commands/df/fix.md +104 -0
  29. package/src/eval/git-memory.js +159 -0
  30. package/src/eval/git-memory.test.js +439 -0
  31. package/src/eval/hypothesis.js +80 -0
  32. package/src/eval/hypothesis.test.js +169 -0
  33. package/src/eval/loop.js +378 -0
  34. package/src/eval/loop.test.js +306 -0
  35. package/src/eval/metric-collector.js +163 -0
  36. package/src/eval/metric-collector.test.js +369 -0
  37. package/src/eval/metric-pivot.js +119 -0
  38. package/src/eval/metric-pivot.test.js +350 -0
  39. package/src/eval/mutator-prompt.js +106 -0
  40. package/src/eval/mutator-prompt.test.js +180 -0
  41. package/templates/config-template.yaml +5 -6
  42. package/templates/eval-fixture-template/config.yaml +39 -0
  43. package/templates/eval-fixture-template/fixture/.deepflow/decisions.md +5 -0
  44. package/templates/eval-fixture-template/fixture/hooks/invariant.js +28 -0
  45. package/templates/eval-fixture-template/fixture/package.json +12 -0
  46. package/templates/eval-fixture-template/fixture/specs/doing-example-task.md +18 -0
  47. package/templates/eval-fixture-template/fixture/src/commands/df/example.md +18 -0
  48. package/templates/eval-fixture-template/fixture/src/config.js +40 -0
  49. package/templates/eval-fixture-template/fixture/src/index.js +19 -0
  50. package/templates/eval-fixture-template/fixture/src/pipeline.js +40 -0
  51. package/templates/eval-fixture-template/fixture/src/skills/example-skill/SKILL.md +32 -0
  52. package/templates/eval-fixture-template/fixture/src/spec-loader.js +35 -0
  53. package/templates/eval-fixture-template/fixture/src/task-runner.js +32 -0
  54. package/templates/eval-fixture-template/fixture/src/verifier.js +37 -0
  55. package/templates/eval-fixture-template/hypotheses.md +14 -0
  56. package/templates/eval-fixture-template/spec.md +34 -0
  57. package/templates/eval-fixture-template/tests/behavior.test.js +69 -0
  58. package/templates/eval-fixture-template/tests/guard.test.js +108 -0
  59. package/templates/eval-fixture-template.test.js +318 -0
  60. package/templates/explore-agent.md +5 -74
  61. package/templates/explore-protocol.md +44 -0
  62. package/templates/spec-template.md +4 -0
package/bin/install.js CHANGED
@@ -134,6 +134,13 @@ async function main() {
134
134
  );
135
135
  log('Agents installed');
136
136
 
137
+ // Copy templates (explore-protocol, explore-agent, etc.)
138
+ copyDir(
139
+ path.join(PACKAGE_DIR, 'templates'),
140
+ path.join(CLAUDE_DIR, 'templates')
141
+ );
142
+ log('Templates installed');
143
+
137
144
  // Copy bin utilities (plan-consolidator, wave-runner, ratchet)
138
145
  const binDest = path.join(CLAUDE_DIR, 'bin');
139
146
  fs.mkdirSync(binDest, { recursive: true });
@@ -198,8 +205,9 @@ async function main() {
198
205
  console.log(' skills/ — gap-discovery, atomic-commits, code-completeness, browse-fetch, browse-verify, auto-cycle');
199
206
  console.log(' agents/ — reasoner (/df:auto — autonomous execution via /loop)');
200
207
  console.log(' bin/ — plan-consolidator, wave-runner, ratchet');
208
+ console.log(' templates/ — explore-protocol (auto-injected into Explore agents via hook)');
201
209
  if (level === 'global') {
202
- console.log(' hooks/ — statusline, update checker, invariant checker, worktree guard');
210
+ console.log(' hooks/ — statusline, update checker, invariant checker, worktree guard, explore protocol');
203
211
  }
204
212
  console.log(' hooks/df-spec-* — spec validation (auto-enforced by /df:spec and /df:plan)');
205
213
  console.log(' env/ — ENABLE_LSP_TOOL (code navigation via goToDefinition, findReferences, workspaceSymbol)');
@@ -230,12 +238,33 @@ function isInstalled(claudeDir) {
230
238
  function copyDir(src, dest) {
231
239
  if (!fs.existsSync(src)) return;
232
240
 
241
+ const resolvedSrcRoot = path.resolve(src);
242
+ const resolvedDestRoot = path.resolve(dest);
243
+
233
244
  fs.mkdirSync(dest, { recursive: true });
234
245
 
235
246
  for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
236
247
  const srcPath = path.join(src, entry.name);
237
248
  const destPath = path.join(dest, entry.name);
238
249
 
250
+ // Reject symlinks to prevent symlink attacks
251
+ if (entry.isSymbolicLink()) {
252
+ process.stderr.write(`[deepflow] skipping symlink: ${srcPath}\n`);
253
+ continue;
254
+ }
255
+
256
+ // Guard against path traversal — resolved paths must stay under their roots
257
+ const resolvedSrc = path.resolve(srcPath);
258
+ const resolvedDest = path.resolve(destPath);
259
+ if (!resolvedSrc.startsWith(resolvedSrcRoot + path.sep) && resolvedSrc !== resolvedSrcRoot) {
260
+ process.stderr.write(`[deepflow] skipping path traversal attempt (src): ${srcPath}\n`);
261
+ continue;
262
+ }
263
+ if (!resolvedDest.startsWith(resolvedDestRoot + path.sep) && resolvedDest !== resolvedDestRoot) {
264
+ process.stderr.write(`[deepflow] skipping path traversal attempt (dest): ${destPath}\n`);
265
+ continue;
266
+ }
267
+
239
268
  if (entry.isDirectory()) {
240
269
  copyDir(srcPath, destPath);
241
270
  } else {
@@ -244,19 +273,82 @@ function copyDir(src, dest) {
244
273
  }
245
274
  }
246
275
 
276
+ // Valid hook events (settings.hooks keys + special "statusLine")
277
+ const VALID_HOOK_EVENTS = new Set([
278
+ 'SessionStart', 'SessionEnd', 'PreToolUse', 'PostToolUse', 'SubagentStop', 'statusLine'
279
+ ]);
280
+
281
+ /**
282
+ * Scan hook source files for @hook-event tags. Returns:
283
+ * { eventMap: Map<event, [filename, ...]>, untagged: [filename, ...] }
284
+ */
285
+ function scanHookEvents(hooksSourceDir) {
286
+ const eventMap = new Map(); // event → [filenames]
287
+ const untagged = [];
288
+
289
+ if (!fs.existsSync(hooksSourceDir)) return { eventMap, untagged };
290
+
291
+ for (const file of fs.readdirSync(hooksSourceDir)) {
292
+ if (!file.endsWith('.js') || file.endsWith('.test.js')) continue;
293
+
294
+ const content = fs.readFileSync(path.join(hooksSourceDir, file), 'utf8');
295
+ const firstLines = content.split('\n').slice(0, 10).join('\n');
296
+ const match = firstLines.match(/\/\/\s*@hook-event:\s*(.+)/);
297
+
298
+ if (!match) {
299
+ untagged.push(file);
300
+ continue;
301
+ }
302
+
303
+ const events = match[1].split(',').map(e => e.trim()).filter(Boolean);
304
+ let hasValidEvent = false;
305
+
306
+ for (const event of events) {
307
+ if (!VALID_HOOK_EVENTS.has(event)) {
308
+ console.log(` ${c.yellow}!${c.reset} Warning: unknown event "${event}" in ${file} — skipped`);
309
+ continue;
310
+ }
311
+ hasValidEvent = true;
312
+ if (!eventMap.has(event)) eventMap.set(event, []);
313
+ eventMap.get(event).push(file);
314
+ }
315
+
316
+ if (!hasValidEvent) {
317
+ untagged.push(file);
318
+ }
319
+ }
320
+
321
+ return { eventMap, untagged };
322
+ }
323
+
324
+ /**
325
+ * Remove all deepflow hook entries (commands containing /hooks/df-) from settings.
326
+ * Preserves non-deepflow hooks.
327
+ */
328
+ function removeDeepflowHooks(settings) {
329
+ const isDeepflow = (hook) => {
330
+ const cmd = hook.hooks?.[0]?.command || '';
331
+ return cmd.includes('/hooks/df-');
332
+ };
333
+
334
+ // Clean settings.hooks.*
335
+ if (settings.hooks) {
336
+ for (const event of Object.keys(settings.hooks)) {
337
+ settings.hooks[event] = settings.hooks[event].filter(h => !isDeepflow(h));
338
+ if (settings.hooks[event].length === 0) delete settings.hooks[event];
339
+ }
340
+ if (Object.keys(settings.hooks).length === 0) delete settings.hooks;
341
+ }
342
+
343
+ // Clean settings.statusLine if it's a deepflow hook
344
+ if (settings.statusLine?.command && settings.statusLine.command.includes('/hooks/df-')) {
345
+ delete settings.statusLine;
346
+ }
347
+ }
348
+
247
349
  async function configureHooks(claudeDir) {
248
350
  const settingsPath = path.join(claudeDir, 'settings.json');
249
- const statuslineCmd = `node "${path.join(claudeDir, 'hooks', 'df-statusline.js')}"`;
250
- const updateCheckCmd = `node "${path.join(claudeDir, 'hooks', 'df-check-update.js')}"`;
251
- const quotaLoggerCmd = `node "${path.join(claudeDir, 'hooks', 'df-quota-logger.js')}"`;
252
- const toolUsageCmd = `node "${path.join(claudeDir, 'hooks', 'df-tool-usage.js')}"`;
253
- const dashboardPushCmd = `node "${path.join(claudeDir, 'hooks', 'df-dashboard-push.js')}"`;
254
- const executionHistoryCmd = `node "${path.join(claudeDir, 'hooks', 'df-execution-history.js')}"`;
255
- const worktreeGuardCmd = `node "${path.join(claudeDir, 'hooks', 'df-worktree-guard.js')}"`;
256
- const snapshotGuardCmd = `node "${path.join(claudeDir, 'hooks', 'df-snapshot-guard.js')}"`;
257
- const invariantCheckCmd = `node "${path.join(claudeDir, 'hooks', 'df-invariant-check.js')}"`;
258
- const subagentRegistryCmd = `node "${path.join(claudeDir, 'hooks', 'df-subagent-registry.js')}"`;
259
- const commandUsageCmd = `node "${path.join(claudeDir, 'hooks', 'df-command-usage.js')}"`;
351
+ const hooksSourceDir = path.join(PACKAGE_DIR, 'hooks');
260
352
 
261
353
  let settings = {};
262
354
 
@@ -277,194 +369,64 @@ async function configureHooks(claudeDir) {
277
369
  configurePermissions(settings);
278
370
  log('Agent permissions configured');
279
371
 
280
- // Configure statusline
281
- if (settings.statusLine) {
282
- if (process.stdin.isTTY) {
283
- const answer = await ask(
284
- ` ${c.yellow}!${c.reset} Existing statusLine found. Replace with deepflow? [y/N] `
285
- );
286
- if (answer.toLowerCase() === 'y') {
287
- settings.statusLine = { type: 'command', command: statuslineCmd };
288
- log('Statusline configured');
289
- } else {
290
- console.log(` ${c.yellow}!${c.reset} Skipped statusline configuration`);
291
- }
292
- } else {
293
- // Non-interactive (e.g. Claude Code bash tool) — skip prompt, keep existing
294
- console.log(` ${c.yellow}!${c.reset} Existing statusLine found — kept (non-interactive mode)`);
295
- }
296
- } else {
297
- settings.statusLine = { type: 'command', command: statuslineCmd };
298
- log('Statusline configured');
299
- }
300
-
301
- // Configure SessionStart hook for update checking
302
- if (!settings.hooks) {
303
- settings.hooks = {};
304
- }
305
- if (!settings.hooks.SessionStart) {
306
- settings.hooks.SessionStart = [];
307
- }
308
-
309
- // Remove any existing deepflow update check / quota logger hooks from SessionStart
310
- settings.hooks.SessionStart = settings.hooks.SessionStart.filter(hook => {
311
- const cmd = hook.hooks?.[0]?.command || '';
312
- return !cmd.includes('df-check-update') && !cmd.includes('df-quota-logger');
313
- });
372
+ // Scan hook files for @hook-event tags
373
+ const { eventMap, untagged } = scanHookEvents(hooksSourceDir);
314
374
 
315
- // Add update check hook
316
- settings.hooks.SessionStart.push({
317
- hooks: [{
318
- type: 'command',
319
- command: updateCheckCmd
320
- }]
321
- });
375
+ // Remember if there was a pre-existing non-deepflow statusLine
376
+ const hadExternalStatusLine = settings.statusLine &&
377
+ !settings.statusLine.command?.includes('/hooks/df-');
322
378
 
323
- // Add quota logger to SessionStart
324
- settings.hooks.SessionStart.push({
325
- hooks: [{
326
- type: 'command',
327
- command: quotaLoggerCmd
328
- }]
329
- });
330
- log('SessionStart hook configured');
379
+ // Remove all existing deepflow hooks (orphan cleanup + idempotency)
380
+ removeDeepflowHooks(settings);
331
381
 
332
- // Configure SessionEnd hook for quota logging
333
- if (!settings.hooks.SessionEnd) {
334
- settings.hooks.SessionEnd = [];
335
- }
382
+ // Wire hooks by event
383
+ if (!settings.hooks) settings.hooks = {};
336
384
 
337
- // Remove any existing quota logger / dashboard push / command usage from SessionEnd
338
- settings.hooks.SessionEnd = settings.hooks.SessionEnd.filter(hook => {
339
- const cmd = hook.hooks?.[0]?.command || '';
340
- return !cmd.includes('df-quota-logger') && !cmd.includes('df-dashboard-push') && !cmd.includes('df-command-usage');
341
- });
385
+ for (const [event, files] of eventMap) {
386
+ if (event === 'statusLine') {
387
+ // Handle statusLine separately — it's settings.statusLine, not settings.hooks
388
+ const statusFile = files[0]; // Only one statusline hook expected
389
+ const statusCmd = `node "${path.join(claudeDir, 'hooks', statusFile)}"`;
342
390
 
343
- // Add quota logger to SessionEnd
344
- settings.hooks.SessionEnd.push({
345
- hooks: [{
346
- type: 'command',
347
- command: quotaLoggerCmd
348
- }]
349
- });
350
-
351
- // Add dashboard push to SessionEnd (fire-and-forget, skips when dashboard_url unset)
352
- settings.hooks.SessionEnd.push({
353
- hooks: [{
354
- type: 'command',
355
- command: dashboardPushCmd
356
- }]
357
- });
358
-
359
- // Add command usage hook to SessionEnd (flush any pending command data)
360
- settings.hooks.SessionEnd.push({
361
- hooks: [{
362
- type: 'command',
363
- command: commandUsageCmd
364
- }]
365
- });
366
- log('Quota logger + dashboard push + command usage configured (SessionEnd)');
367
-
368
- // Configure PostToolUse hook for tool usage instrumentation
369
- if (!settings.hooks.PostToolUse) {
370
- settings.hooks.PostToolUse = [];
371
- }
372
-
373
- // Remove any existing deepflow tool usage / execution history / worktree guard / snapshot guard / invariant check / command usage hooks from PostToolUse
374
- settings.hooks.PostToolUse = settings.hooks.PostToolUse.filter(hook => {
375
- const cmd = hook.hooks?.[0]?.command || '';
376
- return !cmd.includes('df-tool-usage') && !cmd.includes('df-execution-history') && !cmd.includes('df-worktree-guard') && !cmd.includes('df-snapshot-guard') && !cmd.includes('df-invariant-check') && !cmd.includes('df-command-usage');
377
- });
378
-
379
- // Add tool usage hook
380
- settings.hooks.PostToolUse.push({
381
- hooks: [{
382
- type: 'command',
383
- command: toolUsageCmd
384
- }]
385
- });
386
-
387
- // Add execution history hook
388
- settings.hooks.PostToolUse.push({
389
- hooks: [{
390
- type: 'command',
391
- command: executionHistoryCmd
392
- }]
393
- });
394
-
395
- // Add worktree guard hook (blocks Write/Edit to main-branch files when df/* worktree exists)
396
- settings.hooks.PostToolUse.push({
397
- hooks: [{
398
- type: 'command',
399
- command: worktreeGuardCmd
400
- }]
401
- });
402
-
403
- // Add snapshot guard hook (blocks Write/Edit to ratchet-baseline files in auto-snapshot.txt)
404
- settings.hooks.PostToolUse.push({
405
- hooks: [{
406
- type: 'command',
407
- command: snapshotGuardCmd
408
- }]
409
- });
410
-
411
- // Add invariant check hook (exits 1 on hard failures after git commit)
412
- settings.hooks.PostToolUse.push({
413
- hooks: [{
414
- type: 'command',
415
- command: invariantCheckCmd
416
- }]
417
- });
391
+ if (hadExternalStatusLine) {
392
+ if (process.stdin.isTTY) {
393
+ const answer = await ask(
394
+ ` ${c.yellow}!${c.reset} Existing statusLine found. Replace with deepflow? [y/N] `
395
+ );
396
+ if (answer.toLowerCase() === 'y') {
397
+ settings.statusLine = { type: 'command', command: statusCmd };
398
+ log('Statusline configured');
399
+ } else {
400
+ console.log(` ${c.yellow}!${c.reset} Skipped statusline configuration`);
401
+ }
402
+ } else {
403
+ // Non-interactive (e.g. Claude Code bash tool) — skip prompt, keep existing
404
+ console.log(` ${c.yellow}!${c.reset} Existing statusLine found — kept (non-interactive mode)`);
405
+ }
406
+ } else {
407
+ settings.statusLine = { type: 'command', command: statusCmd };
408
+ log('Statusline configured');
409
+ }
410
+ continue;
411
+ }
418
412
 
419
- // Add command usage hook to PostToolUse
420
- settings.hooks.PostToolUse.push({
421
- hooks: [{
422
- type: 'command',
423
- command: commandUsageCmd
424
- }]
425
- });
426
- log('PostToolUse hook configured');
413
+ // Regular hook events
414
+ if (!settings.hooks[event]) settings.hooks[event] = [];
427
415
 
428
- // Configure SubagentStop hook for subagent registry
429
- if (!settings.hooks.SubagentStop) {
430
- settings.hooks.SubagentStop = [];
416
+ for (const file of files) {
417
+ const cmd = `node "${path.join(claudeDir, 'hooks', file)}"`;
418
+ settings.hooks[event].push({
419
+ hooks: [{ type: 'command', command: cmd }]
420
+ });
421
+ }
422
+ log(`${event} hook configured`);
431
423
  }
432
424
 
433
- // Remove any existing subagent registry hooks
434
- settings.hooks.SubagentStop = settings.hooks.SubagentStop.filter(hook => {
435
- const cmd = hook.hooks?.[0]?.command || '';
436
- return !cmd.includes('df-subagent-registry');
437
- });
438
-
439
- // Add subagent registry hook
440
- settings.hooks.SubagentStop.push({
441
- hooks: [{
442
- type: 'command',
443
- command: subagentRegistryCmd
444
- }]
445
- });
446
- log('SubagentStop hook configured');
447
-
448
- // Configure PreToolUse hook for command usage instrumentation
449
- if (!settings.hooks.PreToolUse) {
450
- settings.hooks.PreToolUse = [];
425
+ // Log untagged files (copied but not wired)
426
+ for (const file of untagged) {
427
+ console.log(` ${c.dim}${file} copied (no @hook-event tag — not wired)${c.reset}`);
451
428
  }
452
429
 
453
- // Remove any existing deepflow command usage hooks from PreToolUse
454
- settings.hooks.PreToolUse = settings.hooks.PreToolUse.filter(hook => {
455
- const cmd = hook.hooks?.[0]?.command || '';
456
- return !cmd.includes('df-command-usage');
457
- });
458
-
459
- // Add command usage hook to PreToolUse
460
- settings.hooks.PreToolUse.push({
461
- hooks: [{
462
- type: 'command',
463
- command: commandUsageCmd
464
- }]
465
- });
466
- log('PreToolUse hook configured');
467
-
468
430
  fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
469
431
  }
470
432
 
@@ -644,11 +606,20 @@ async function uninstall() {
644
606
  'agents/reasoner.md',
645
607
  'bin/plan-consolidator.js',
646
608
  'bin/wave-runner.js',
647
- 'bin/ratchet.js'
609
+ 'bin/ratchet.js',
610
+ 'templates'
648
611
  ];
649
612
 
650
613
  if (level === 'global') {
651
- toRemove.push('hooks/df-statusline.js', 'hooks/df-check-update.js', 'hooks/df-invariant-check.js', 'hooks/df-quota-logger.js', 'hooks/df-tool-usage.js', 'hooks/df-dashboard-push.js', 'hooks/df-execution-history.js', 'hooks/df-worktree-guard.js', 'hooks/df-snapshot-guard.js', 'hooks/df-subagent-registry.js', 'hooks/df-command-usage.js');
614
+ // Dynamically find all df-*.js hook files to remove
615
+ const hooksDir = path.join(CLAUDE_DIR, 'hooks');
616
+ if (fs.existsSync(hooksDir)) {
617
+ for (const file of fs.readdirSync(hooksDir)) {
618
+ if (file.startsWith('df-') && file.endsWith('.js')) {
619
+ toRemove.push(`hooks/${file}`);
620
+ }
621
+ }
622
+ }
652
623
  }
653
624
 
654
625
  for (const item of toRemove) {
@@ -668,76 +639,25 @@ async function uninstall() {
668
639
  }
669
640
  }
670
641
 
671
- // Remove SessionStart hook from settings
642
+ // Remove hook entries and settings from global settings.json
672
643
  if (level === 'global') {
673
644
  const settingsPath = path.join(CLAUDE_DIR, 'settings.json');
674
645
  if (fs.existsSync(settingsPath)) {
675
646
  try {
676
647
  const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
677
- if (settings.hooks?.SessionStart) {
678
- settings.hooks.SessionStart = settings.hooks.SessionStart.filter(hook => {
679
- const cmd = hook.hooks?.[0]?.command || '';
680
- return !cmd.includes('df-check-update') && !cmd.includes('df-quota-logger');
681
- });
682
- if (settings.hooks.SessionStart.length === 0) {
683
- delete settings.hooks.SessionStart;
684
- }
685
- }
686
- if (settings.hooks?.SessionEnd) {
687
- settings.hooks.SessionEnd = settings.hooks.SessionEnd.filter(hook => {
688
- const cmd = hook.hooks?.[0]?.command || '';
689
- return !cmd.includes('df-quota-logger') && !cmd.includes('df-dashboard-push') && !cmd.includes('df-command-usage');
690
- });
691
- if (settings.hooks.SessionEnd.length === 0) {
692
- delete settings.hooks.SessionEnd;
693
- }
694
- }
695
- if (settings.hooks?.PostToolUse) {
696
- settings.hooks.PostToolUse = settings.hooks.PostToolUse.filter(hook => {
697
- const cmd = hook.hooks?.[0]?.command || '';
698
- return !cmd.includes('df-tool-usage') && !cmd.includes('df-execution-history') && !cmd.includes('df-worktree-guard') && !cmd.includes('df-snapshot-guard') && !cmd.includes('df-invariant-check') && !cmd.includes('df-command-usage');
699
- });
700
- if (settings.hooks.PostToolUse.length === 0) {
701
- delete settings.hooks.PostToolUse;
702
- }
703
- }
704
- if (settings.hooks?.PreToolUse) {
705
- settings.hooks.PreToolUse = settings.hooks.PreToolUse.filter(hook => {
706
- const cmd = hook.hooks?.[0]?.command || '';
707
- return !cmd.includes('df-command-usage');
708
- });
709
- if (settings.hooks.PreToolUse.length === 0) {
710
- delete settings.hooks.PreToolUse;
711
- }
712
- }
713
- if (settings.hooks?.SubagentStop) {
714
- settings.hooks.SubagentStop = settings.hooks.SubagentStop.filter(hook => {
715
- const cmd = hook.hooks?.[0]?.command || '';
716
- return !cmd.includes('df-subagent-registry');
717
- });
718
- if (settings.hooks.SubagentStop.length === 0) {
719
- delete settings.hooks.SubagentStop;
720
- }
721
- }
722
- if (settings.hooks && Object.keys(settings.hooks).length === 0) {
723
- delete settings.hooks;
724
- }
725
- fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
726
- console.log(` ${c.green}✓${c.reset} Removed SessionStart/SessionEnd/PreToolUse/PostToolUse/SubagentStop hooks`);
727
- } catch (e) {
728
- // Fail silently
729
- }
730
- }
731
648
 
732
- // Remove ENABLE_LSP_TOOL and deepflow permissions from global settings
733
- if (fs.existsSync(settingsPath)) {
734
- try {
735
- const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
649
+ // Remove all deepflow hook wiring dynamically
650
+ removeDeepflowHooks(settings);
651
+ console.log(` ${c.green}✓${c.reset} Removed deepflow hooks from settings`);
652
+
653
+ // Remove ENABLE_LSP_TOOL
736
654
  if (settings.env?.ENABLE_LSP_TOOL) {
737
655
  delete settings.env.ENABLE_LSP_TOOL;
738
656
  if (settings.env && Object.keys(settings.env).length === 0) delete settings.env;
739
657
  console.log(` ${c.green}✓${c.reset} Removed ENABLE_LSP_TOOL from settings`);
740
658
  }
659
+
660
+ // Remove deepflow permissions
741
661
  if (settings.permissions?.allow) {
742
662
  const dfPerms = new Set(DEEPFLOW_PERMISSIONS);
743
663
  settings.permissions.allow = settings.permissions.allow.filter(p => !dfPerms.has(p));
@@ -745,6 +665,7 @@ async function uninstall() {
745
665
  if (settings.permissions && Object.keys(settings.permissions).length === 0) delete settings.permissions;
746
666
  console.log(` ${c.green}✓${c.reset} Removed deepflow permissions from settings`);
747
667
  }
668
+
748
669
  fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
749
670
  } catch (e) {
750
671
  // Fail silently