deepflow 0.1.101 → 0.1.103
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/bin/install.js +55 -9
- package/bin/install.test.js +214 -0
- package/bin/plan-consolidator.js +16 -16
- package/bin/wave-runner.js +48 -16
- package/hooks/df-command-usage.js +287 -0
- package/hooks/df-command-usage.test.js +1019 -0
- package/hooks/df-subagent-registry.js +33 -14
- package/hooks/df-tool-usage.js +8 -0
- package/hooks/df-tool-usage.test.js +200 -0
- package/package.json +1 -1
- package/src/commands/df/execute.md +2 -2
- package/src/commands/df/plan.md +28 -18
package/bin/install.js
CHANGED
|
@@ -256,6 +256,7 @@ async function configureHooks(claudeDir) {
|
|
|
256
256
|
const snapshotGuardCmd = `node "${path.join(claudeDir, 'hooks', 'df-snapshot-guard.js')}"`;
|
|
257
257
|
const invariantCheckCmd = `node "${path.join(claudeDir, 'hooks', 'df-invariant-check.js')}"`;
|
|
258
258
|
const subagentRegistryCmd = `node "${path.join(claudeDir, 'hooks', 'df-subagent-registry.js')}"`;
|
|
259
|
+
const commandUsageCmd = `node "${path.join(claudeDir, 'hooks', 'df-command-usage.js')}"`;
|
|
259
260
|
|
|
260
261
|
let settings = {};
|
|
261
262
|
|
|
@@ -333,10 +334,10 @@ async function configureHooks(claudeDir) {
|
|
|
333
334
|
settings.hooks.SessionEnd = [];
|
|
334
335
|
}
|
|
335
336
|
|
|
336
|
-
// Remove any existing quota logger / dashboard push from SessionEnd
|
|
337
|
+
// Remove any existing quota logger / dashboard push / command usage from SessionEnd
|
|
337
338
|
settings.hooks.SessionEnd = settings.hooks.SessionEnd.filter(hook => {
|
|
338
339
|
const cmd = hook.hooks?.[0]?.command || '';
|
|
339
|
-
return !cmd.includes('df-quota-logger') && !cmd.includes('df-dashboard-push');
|
|
340
|
+
return !cmd.includes('df-quota-logger') && !cmd.includes('df-dashboard-push') && !cmd.includes('df-command-usage');
|
|
340
341
|
});
|
|
341
342
|
|
|
342
343
|
// Add quota logger to SessionEnd
|
|
@@ -354,17 +355,25 @@ async function configureHooks(claudeDir) {
|
|
|
354
355
|
command: dashboardPushCmd
|
|
355
356
|
}]
|
|
356
357
|
});
|
|
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)');
|
|
358
367
|
|
|
359
368
|
// Configure PostToolUse hook for tool usage instrumentation
|
|
360
369
|
if (!settings.hooks.PostToolUse) {
|
|
361
370
|
settings.hooks.PostToolUse = [];
|
|
362
371
|
}
|
|
363
372
|
|
|
364
|
-
// Remove any existing deepflow tool usage / execution history / worktree guard / snapshot guard / invariant check hooks from PostToolUse
|
|
373
|
+
// Remove any existing deepflow tool usage / execution history / worktree guard / snapshot guard / invariant check / command usage hooks from PostToolUse
|
|
365
374
|
settings.hooks.PostToolUse = settings.hooks.PostToolUse.filter(hook => {
|
|
366
375
|
const cmd = hook.hooks?.[0]?.command || '';
|
|
367
|
-
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');
|
|
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');
|
|
368
377
|
});
|
|
369
378
|
|
|
370
379
|
// Add tool usage hook
|
|
@@ -406,6 +415,14 @@ async function configureHooks(claudeDir) {
|
|
|
406
415
|
command: invariantCheckCmd
|
|
407
416
|
}]
|
|
408
417
|
});
|
|
418
|
+
|
|
419
|
+
// Add command usage hook to PostToolUse
|
|
420
|
+
settings.hooks.PostToolUse.push({
|
|
421
|
+
hooks: [{
|
|
422
|
+
type: 'command',
|
|
423
|
+
command: commandUsageCmd
|
|
424
|
+
}]
|
|
425
|
+
});
|
|
409
426
|
log('PostToolUse hook configured');
|
|
410
427
|
|
|
411
428
|
// Configure SubagentStop hook for subagent registry
|
|
@@ -428,6 +445,26 @@ async function configureHooks(claudeDir) {
|
|
|
428
445
|
});
|
|
429
446
|
log('SubagentStop hook configured');
|
|
430
447
|
|
|
448
|
+
// Configure PreToolUse hook for command usage instrumentation
|
|
449
|
+
if (!settings.hooks.PreToolUse) {
|
|
450
|
+
settings.hooks.PreToolUse = [];
|
|
451
|
+
}
|
|
452
|
+
|
|
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
|
+
|
|
431
468
|
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
|
432
469
|
}
|
|
433
470
|
|
|
@@ -611,7 +648,7 @@ async function uninstall() {
|
|
|
611
648
|
];
|
|
612
649
|
|
|
613
650
|
if (level === 'global') {
|
|
614
|
-
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');
|
|
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');
|
|
615
652
|
}
|
|
616
653
|
|
|
617
654
|
for (const item of toRemove) {
|
|
@@ -649,7 +686,7 @@ async function uninstall() {
|
|
|
649
686
|
if (settings.hooks?.SessionEnd) {
|
|
650
687
|
settings.hooks.SessionEnd = settings.hooks.SessionEnd.filter(hook => {
|
|
651
688
|
const cmd = hook.hooks?.[0]?.command || '';
|
|
652
|
-
return !cmd.includes('df-quota-logger') && !cmd.includes('df-dashboard-push');
|
|
689
|
+
return !cmd.includes('df-quota-logger') && !cmd.includes('df-dashboard-push') && !cmd.includes('df-command-usage');
|
|
653
690
|
});
|
|
654
691
|
if (settings.hooks.SessionEnd.length === 0) {
|
|
655
692
|
delete settings.hooks.SessionEnd;
|
|
@@ -658,12 +695,21 @@ async function uninstall() {
|
|
|
658
695
|
if (settings.hooks?.PostToolUse) {
|
|
659
696
|
settings.hooks.PostToolUse = settings.hooks.PostToolUse.filter(hook => {
|
|
660
697
|
const cmd = hook.hooks?.[0]?.command || '';
|
|
661
|
-
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');
|
|
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');
|
|
662
699
|
});
|
|
663
700
|
if (settings.hooks.PostToolUse.length === 0) {
|
|
664
701
|
delete settings.hooks.PostToolUse;
|
|
665
702
|
}
|
|
666
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
|
+
}
|
|
667
713
|
if (settings.hooks?.SubagentStop) {
|
|
668
714
|
settings.hooks.SubagentStop = settings.hooks.SubagentStop.filter(hook => {
|
|
669
715
|
const cmd = hook.hooks?.[0]?.command || '';
|
|
@@ -677,7 +723,7 @@ async function uninstall() {
|
|
|
677
723
|
delete settings.hooks;
|
|
678
724
|
}
|
|
679
725
|
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
|
680
|
-
console.log(` ${c.green}✓${c.reset} Removed SessionStart/SessionEnd/PostToolUse/SubagentStop hooks`);
|
|
726
|
+
console.log(` ${c.green}✓${c.reset} Removed SessionStart/SessionEnd/PreToolUse/PostToolUse/SubagentStop hooks`);
|
|
681
727
|
} catch (e) {
|
|
682
728
|
// Fail silently
|
|
683
729
|
}
|
package/bin/install.test.js
CHANGED
|
@@ -589,6 +589,220 @@ describe('Uninstaller — file removal and settings cleanup', () => {
|
|
|
589
589
|
});
|
|
590
590
|
});
|
|
591
591
|
|
|
592
|
+
// ---------------------------------------------------------------------------
|
|
593
|
+
// T4. command-usage hook registration (PreToolUse, PostToolUse, SessionEnd)
|
|
594
|
+
// ---------------------------------------------------------------------------
|
|
595
|
+
|
|
596
|
+
describe('T4 — command-usage hook registration in install.js', () => {
|
|
597
|
+
|
|
598
|
+
// -- Source-level checks: verify install.js registers df-command-usage.js --
|
|
599
|
+
|
|
600
|
+
test('source defines commandUsageCmd variable', () => {
|
|
601
|
+
const src = fs.readFileSync(path.resolve(__dirname, 'install.js'), 'utf8');
|
|
602
|
+
const pattern = /commandUsageCmd\s*=\s*`node.*df-command-usage\.js/;
|
|
603
|
+
assert.ok(
|
|
604
|
+
pattern.test(src),
|
|
605
|
+
'install.js should define commandUsageCmd pointing to df-command-usage.js'
|
|
606
|
+
);
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
test('source pushes command-usage hook to PreToolUse', () => {
|
|
610
|
+
const src = fs.readFileSync(path.resolve(__dirname, 'install.js'), 'utf8');
|
|
611
|
+
// Find PreToolUse section — should contain a push with commandUsageCmd
|
|
612
|
+
const preToolUseSection = src.match(/PreToolUse[\s\S]*?log\('PreToolUse hook configured'\)/);
|
|
613
|
+
assert.ok(preToolUseSection, 'Should have a PreToolUse configuration section');
|
|
614
|
+
assert.ok(
|
|
615
|
+
preToolUseSection[0].includes('commandUsageCmd'),
|
|
616
|
+
'PreToolUse section should push commandUsageCmd'
|
|
617
|
+
);
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
test('source pushes command-usage hook to PostToolUse', () => {
|
|
621
|
+
const src = fs.readFileSync(path.resolve(__dirname, 'install.js'), 'utf8');
|
|
622
|
+
// Find PostToolUse section
|
|
623
|
+
const postToolUseSection = src.match(/PostToolUse[\s\S]*?log\('PostToolUse hook configured'\)/);
|
|
624
|
+
assert.ok(postToolUseSection, 'Should have a PostToolUse configuration section');
|
|
625
|
+
assert.ok(
|
|
626
|
+
postToolUseSection[0].includes('commandUsageCmd'),
|
|
627
|
+
'PostToolUse section should push commandUsageCmd'
|
|
628
|
+
);
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
test('source pushes command-usage hook to SessionEnd', () => {
|
|
632
|
+
const src = fs.readFileSync(path.resolve(__dirname, 'install.js'), 'utf8');
|
|
633
|
+
// Find SessionEnd section — should include command-usage alongside quota-logger + dashboard-push
|
|
634
|
+
const sessionEndSection = src.match(/SessionEnd[\s\S]*?log\('Quota logger.*configured.*SessionEnd/);
|
|
635
|
+
assert.ok(sessionEndSection, 'Should have a SessionEnd configuration section');
|
|
636
|
+
assert.ok(
|
|
637
|
+
sessionEndSection[0].includes('commandUsageCmd'),
|
|
638
|
+
'SessionEnd section should push commandUsageCmd'
|
|
639
|
+
);
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
test('source creates PreToolUse array if missing', () => {
|
|
643
|
+
const src = fs.readFileSync(path.resolve(__dirname, 'install.js'), 'utf8');
|
|
644
|
+
assert.ok(
|
|
645
|
+
src.includes("if (!settings.hooks.PreToolUse)"),
|
|
646
|
+
'install.js should initialize PreToolUse array if not present'
|
|
647
|
+
);
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
// -- Dedup logic: filter removes existing command-usage before re-adding --
|
|
651
|
+
|
|
652
|
+
test('PreToolUse dedup filter removes existing df-command-usage entries', () => {
|
|
653
|
+
const preToolUse = [
|
|
654
|
+
{ hooks: [{ type: 'command', command: 'node /home/.claude/hooks/df-command-usage.js' }] },
|
|
655
|
+
{ hooks: [{ type: 'command', command: 'node /usr/local/my-custom.js' }] },
|
|
656
|
+
];
|
|
657
|
+
|
|
658
|
+
const filtered = preToolUse.filter(hook => {
|
|
659
|
+
const cmd = hook.hooks?.[0]?.command || '';
|
|
660
|
+
return !cmd.includes('df-command-usage');
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
assert.equal(filtered.length, 1, 'Should remove existing df-command-usage hook');
|
|
664
|
+
assert.ok(filtered[0].hooks[0].command.includes('my-custom.js'), 'Should keep non-deepflow hooks');
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
test('PostToolUse dedup filter removes df-command-usage alongside other deepflow hooks', () => {
|
|
668
|
+
const postToolUse = [
|
|
669
|
+
{ hooks: [{ type: 'command', command: 'node /home/.claude/hooks/df-tool-usage.js' }] },
|
|
670
|
+
{ hooks: [{ type: 'command', command: 'node /home/.claude/hooks/df-command-usage.js' }] },
|
|
671
|
+
{ hooks: [{ type: 'command', command: 'node /home/.claude/hooks/df-worktree-guard.js' }] },
|
|
672
|
+
{ hooks: [{ type: 'command', command: 'node /usr/local/keep-me.js' }] },
|
|
673
|
+
];
|
|
674
|
+
|
|
675
|
+
const filtered = postToolUse.filter(hook => {
|
|
676
|
+
const cmd = hook.hooks?.[0]?.command || '';
|
|
677
|
+
return !cmd.includes('df-tool-usage') &&
|
|
678
|
+
!cmd.includes('df-execution-history') &&
|
|
679
|
+
!cmd.includes('df-worktree-guard') &&
|
|
680
|
+
!cmd.includes('df-snapshot-guard') &&
|
|
681
|
+
!cmd.includes('df-invariant-check') &&
|
|
682
|
+
!cmd.includes('df-command-usage');
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
assert.equal(filtered.length, 1);
|
|
686
|
+
assert.ok(filtered[0].hooks[0].command.includes('keep-me.js'));
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
test('SessionEnd dedup filter removes df-command-usage alongside quota-logger and dashboard-push', () => {
|
|
690
|
+
const sessionEnd = [
|
|
691
|
+
{ hooks: [{ type: 'command', command: 'node /home/.claude/hooks/df-quota-logger.js' }] },
|
|
692
|
+
{ hooks: [{ type: 'command', command: 'node /home/.claude/hooks/df-dashboard-push.js' }] },
|
|
693
|
+
{ hooks: [{ type: 'command', command: 'node /home/.claude/hooks/df-command-usage.js' }] },
|
|
694
|
+
{ hooks: [{ type: 'command', command: 'node /usr/local/keep.js' }] },
|
|
695
|
+
];
|
|
696
|
+
|
|
697
|
+
const filtered = sessionEnd.filter(hook => {
|
|
698
|
+
const cmd = hook.hooks?.[0]?.command || '';
|
|
699
|
+
return !cmd.includes('df-quota-logger') &&
|
|
700
|
+
!cmd.includes('df-dashboard-push') &&
|
|
701
|
+
!cmd.includes('df-command-usage');
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
assert.equal(filtered.length, 1);
|
|
705
|
+
assert.ok(filtered[0].hooks[0].command.includes('keep.js'));
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
// -- Uninstall cleanup --
|
|
709
|
+
|
|
710
|
+
test('uninstall toRemove includes df-command-usage.js', () => {
|
|
711
|
+
const src = fs.readFileSync(path.resolve(__dirname, 'install.js'), 'utf8');
|
|
712
|
+
// Find the toRemove.push(...) line for hooks in uninstall
|
|
713
|
+
assert.ok(
|
|
714
|
+
src.includes("'hooks/df-command-usage.js'"),
|
|
715
|
+
'toRemove should include hooks/df-command-usage.js for uninstall'
|
|
716
|
+
);
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
test('uninstall SessionEnd filter removes df-command-usage', () => {
|
|
720
|
+
const src = fs.readFileSync(path.resolve(__dirname, 'install.js'), 'utf8');
|
|
721
|
+
// In the uninstall function, the SessionEnd filter should include df-command-usage
|
|
722
|
+
// Find the uninstall section's SessionEnd filter
|
|
723
|
+
const uninstallSection = src.match(/async function uninstall[\s\S]+$/);
|
|
724
|
+
assert.ok(uninstallSection, 'Should have uninstall function');
|
|
725
|
+
// Check SessionEnd filter in uninstall includes command-usage
|
|
726
|
+
const sessionEndFilter = uninstallSection[0].match(/SessionEnd[\s\S]*?\.filter[\s\S]*?\);/);
|
|
727
|
+
assert.ok(sessionEndFilter, 'Should have SessionEnd filter in uninstall');
|
|
728
|
+
assert.ok(
|
|
729
|
+
sessionEndFilter[0].includes('df-command-usage'),
|
|
730
|
+
'Uninstall SessionEnd filter should remove df-command-usage hooks'
|
|
731
|
+
);
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
test('uninstall PostToolUse filter removes df-command-usage', () => {
|
|
735
|
+
const src = fs.readFileSync(path.resolve(__dirname, 'install.js'), 'utf8');
|
|
736
|
+
const uninstallSection = src.match(/async function uninstall[\s\S]+$/);
|
|
737
|
+
const postToolUseFilter = uninstallSection[0].match(/PostToolUse[\s\S]*?\.filter[\s\S]*?\);/);
|
|
738
|
+
assert.ok(postToolUseFilter, 'Should have PostToolUse filter in uninstall');
|
|
739
|
+
assert.ok(
|
|
740
|
+
postToolUseFilter[0].includes('df-command-usage'),
|
|
741
|
+
'Uninstall PostToolUse filter should remove df-command-usage hooks'
|
|
742
|
+
);
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
test('uninstall cleans up PreToolUse hooks', () => {
|
|
746
|
+
const src = fs.readFileSync(path.resolve(__dirname, 'install.js'), 'utf8');
|
|
747
|
+
const uninstallSection = src.match(/async function uninstall[\s\S]+$/);
|
|
748
|
+
assert.ok(
|
|
749
|
+
uninstallSection[0].includes('PreToolUse'),
|
|
750
|
+
'Uninstall function should handle PreToolUse cleanup'
|
|
751
|
+
);
|
|
752
|
+
// Verify it filters out df-command-usage from PreToolUse
|
|
753
|
+
const preToolUseFilter = uninstallSection[0].match(/PreToolUse[\s\S]*?\.filter[\s\S]*?\);/);
|
|
754
|
+
assert.ok(preToolUseFilter, 'Should have PreToolUse filter in uninstall');
|
|
755
|
+
assert.ok(
|
|
756
|
+
preToolUseFilter[0].includes('df-command-usage'),
|
|
757
|
+
'Uninstall PreToolUse filter should remove df-command-usage hooks'
|
|
758
|
+
);
|
|
759
|
+
});
|
|
760
|
+
|
|
761
|
+
test('uninstall deletes PreToolUse key when array becomes empty', () => {
|
|
762
|
+
// Reproduce the uninstall logic for PreToolUse
|
|
763
|
+
const settings = {
|
|
764
|
+
hooks: {
|
|
765
|
+
PreToolUse: [
|
|
766
|
+
{ hooks: [{ type: 'command', command: 'node /home/.claude/hooks/df-command-usage.js' }] },
|
|
767
|
+
],
|
|
768
|
+
}
|
|
769
|
+
};
|
|
770
|
+
|
|
771
|
+
settings.hooks.PreToolUse = settings.hooks.PreToolUse.filter(hook => {
|
|
772
|
+
const cmd = hook.hooks?.[0]?.command || '';
|
|
773
|
+
return !cmd.includes('df-command-usage');
|
|
774
|
+
});
|
|
775
|
+
if (settings.hooks.PreToolUse.length === 0) {
|
|
776
|
+
delete settings.hooks.PreToolUse;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
assert.ok(!('PreToolUse' in settings.hooks), 'PreToolUse should be deleted when empty after filtering');
|
|
780
|
+
});
|
|
781
|
+
|
|
782
|
+
test('uninstall keeps PreToolUse when non-deepflow hooks remain', () => {
|
|
783
|
+
const settings = {
|
|
784
|
+
hooks: {
|
|
785
|
+
PreToolUse: [
|
|
786
|
+
{ hooks: [{ type: 'command', command: 'node /home/.claude/hooks/df-command-usage.js' }] },
|
|
787
|
+
{ hooks: [{ type: 'command', command: 'node /usr/local/custom-pre-hook.js' }] },
|
|
788
|
+
],
|
|
789
|
+
}
|
|
790
|
+
};
|
|
791
|
+
|
|
792
|
+
settings.hooks.PreToolUse = settings.hooks.PreToolUse.filter(hook => {
|
|
793
|
+
const cmd = hook.hooks?.[0]?.command || '';
|
|
794
|
+
return !cmd.includes('df-command-usage');
|
|
795
|
+
});
|
|
796
|
+
if (settings.hooks.PreToolUse.length === 0) {
|
|
797
|
+
delete settings.hooks.PreToolUse;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
assert.ok('PreToolUse' in settings.hooks, 'PreToolUse should be kept when custom hooks remain');
|
|
801
|
+
assert.equal(settings.hooks.PreToolUse.length, 1);
|
|
802
|
+
assert.ok(settings.hooks.PreToolUse[0].hooks[0].command.includes('custom-pre-hook.js'));
|
|
803
|
+
});
|
|
804
|
+
});
|
|
805
|
+
|
|
592
806
|
// ---------------------------------------------------------------------------
|
|
593
807
|
// 4. isInstalled helper logic
|
|
594
808
|
// ---------------------------------------------------------------------------
|
package/bin/plan-consolidator.js
CHANGED
|
@@ -208,7 +208,8 @@ function consolidate(specEntries, fileConflicts) {
|
|
|
208
208
|
|
|
209
209
|
/**
|
|
210
210
|
* Render consolidated tasks as PLAN.md-compatible markdown.
|
|
211
|
-
* Groups tasks under ### {specName} headings.
|
|
211
|
+
* Groups tasks under ### doing-{specName} headings with a details reference line.
|
|
212
|
+
* One line per task — no sub-bullets. Files omitted (live in mini-plans only).
|
|
212
213
|
* Compatible with wave-runner's parsePlan regex (see wave-runner.js parsePlan).
|
|
213
214
|
*/
|
|
214
215
|
function formatConsolidated(consolidated) {
|
|
@@ -221,11 +222,15 @@ function formatConsolidated(consolidated) {
|
|
|
221
222
|
|
|
222
223
|
for (const task of consolidated) {
|
|
223
224
|
if (task.specName !== lastSpec) {
|
|
224
|
-
|
|
225
|
+
// Close previous spec with trailing blank line (already added after last task)
|
|
226
|
+
const doingName = `doing-${task.specName}`;
|
|
227
|
+
const planPath = `.deepflow/plans/${doingName}.md`;
|
|
228
|
+
lines.push(`### ${doingName}\n`);
|
|
229
|
+
lines.push(`> Details: [\`${planPath}\`](${planPath})\n`);
|
|
225
230
|
lastSpec = task.specName;
|
|
226
231
|
}
|
|
227
232
|
|
|
228
|
-
// Task header line
|
|
233
|
+
// Task header line — one line, no sub-bullets
|
|
229
234
|
const tagPart = task.tags ? ` ${task.tags}` : '';
|
|
230
235
|
// Append conflict annotations to description if any
|
|
231
236
|
const conflictPart = task.conflictAnnotations.length > 0
|
|
@@ -233,23 +238,18 @@ function formatConsolidated(consolidated) {
|
|
|
233
238
|
: '';
|
|
234
239
|
const descPart = (task.description + conflictPart).trim();
|
|
235
240
|
const headerDesc = descPart ? `: ${descPart}` : '';
|
|
236
|
-
lines.push(`- [ ] **${task.globalId}**${tagPart}${headerDesc}`);
|
|
237
241
|
|
|
238
|
-
//
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
// Blocked by annotation
|
|
244
|
-
if (task.blockedBy.length > 0) {
|
|
245
|
-
lines.push(` - Blocked by: ${task.blockedBy.join(', ')}`);
|
|
246
|
-
} else {
|
|
247
|
-
lines.push(' - Blocked by: none');
|
|
248
|
-
}
|
|
242
|
+
// Blocked by suffix — omit entirely when empty
|
|
243
|
+
const blockedSuffix = task.blockedBy.length > 0
|
|
244
|
+
? ` | Blocked by: ${task.blockedBy.join(', ')}`
|
|
245
|
+
: '';
|
|
249
246
|
|
|
250
|
-
lines.push(
|
|
247
|
+
lines.push(`- [ ] **${task.globalId}**${tagPart}${headerDesc}${blockedSuffix}`);
|
|
251
248
|
}
|
|
252
249
|
|
|
250
|
+
// Trailing newline after last task
|
|
251
|
+
lines.push('');
|
|
252
|
+
|
|
253
253
|
return lines.join('\n');
|
|
254
254
|
}
|
|
255
255
|
|
package/bin/wave-runner.js
CHANGED
|
@@ -85,14 +85,38 @@ function parsePlan(text) {
|
|
|
85
85
|
// Match pending task header: - [ ] **T{N}**...
|
|
86
86
|
const taskMatch = line.match(/^\s*-\s+\[\s+\]\s+\*\*T(\d+)\*\*(?:\s+\[[^\]]*\])?[:\s]*(.*)/);
|
|
87
87
|
if (taskMatch) {
|
|
88
|
+
const rest = taskMatch[2].trim();
|
|
89
|
+
|
|
90
|
+
// Extract inline blocked-by (from " | Blocked by: T1, T2")
|
|
91
|
+
let inlineBlockedBy = [];
|
|
92
|
+
let descPart = rest;
|
|
93
|
+
const blockedInlineMatch = rest.match(/\s*\|\s*Blocked\s+by:\s+(.+)$/i);
|
|
94
|
+
if (blockedInlineMatch) {
|
|
95
|
+
descPart = rest.substring(0, rest.length - blockedInlineMatch[0].length).trim();
|
|
96
|
+
inlineBlockedBy = blockedInlineMatch[1]
|
|
97
|
+
.split(/[,\s]+/)
|
|
98
|
+
.map(s => s.trim())
|
|
99
|
+
.filter(s => /^T\d+$/.test(s));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Extract inline model/effort (from " — model/effort")
|
|
103
|
+
let inlineModel = null;
|
|
104
|
+
let inlineEffort = null;
|
|
105
|
+
const modelInlineMatch = descPart.match(/\s*\u2014\s*(haiku|sonnet|opus)\/(low|medium|high)\s*$/i);
|
|
106
|
+
if (modelInlineMatch) {
|
|
107
|
+
descPart = descPart.substring(0, descPart.length - modelInlineMatch[0].length).trim();
|
|
108
|
+
inlineModel = modelInlineMatch[1].toLowerCase();
|
|
109
|
+
inlineEffort = modelInlineMatch[2].toLowerCase();
|
|
110
|
+
}
|
|
111
|
+
|
|
88
112
|
current = {
|
|
89
113
|
id: `T${taskMatch[1]}`,
|
|
90
114
|
num: parseInt(taskMatch[1], 10),
|
|
91
|
-
description:
|
|
92
|
-
blockedBy:
|
|
93
|
-
model:
|
|
115
|
+
description: descPart,
|
|
116
|
+
blockedBy: inlineBlockedBy,
|
|
117
|
+
model: inlineModel,
|
|
94
118
|
files: null,
|
|
95
|
-
effort:
|
|
119
|
+
effort: inlineEffort,
|
|
96
120
|
spec: currentSpec,
|
|
97
121
|
};
|
|
98
122
|
tasks.push(current);
|
|
@@ -107,35 +131,43 @@ function parsePlan(text) {
|
|
|
107
131
|
}
|
|
108
132
|
|
|
109
133
|
if (current) {
|
|
110
|
-
// Match "Blocked by:" annotation
|
|
134
|
+
// Match "Blocked by:" annotation — only apply if inline parsing found no deps
|
|
111
135
|
const blockedMatch = line.match(/^\s+-\s+Blocked\s+by:\s+(.+)/i);
|
|
112
136
|
if (blockedMatch) {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
137
|
+
if (current.blockedBy.length === 0) {
|
|
138
|
+
const deps = blockedMatch[1]
|
|
139
|
+
.split(/[,\s]+/)
|
|
140
|
+
.map(s => s.trim())
|
|
141
|
+
.filter(s => /^T\d+$/.test(s));
|
|
142
|
+
current.blockedBy.push(...deps);
|
|
143
|
+
}
|
|
118
144
|
continue;
|
|
119
145
|
}
|
|
120
146
|
|
|
121
|
-
// Match "Model:" annotation
|
|
147
|
+
// Match "Model:" annotation — only apply if inline parsing found no model
|
|
122
148
|
const modelMatch = line.match(/^\s+-\s+Model:\s+(.+)/i);
|
|
123
149
|
if (modelMatch) {
|
|
124
|
-
current.model
|
|
150
|
+
if (current.model === null) {
|
|
151
|
+
current.model = modelMatch[1].trim();
|
|
152
|
+
}
|
|
125
153
|
continue;
|
|
126
154
|
}
|
|
127
155
|
|
|
128
|
-
// Match "Files:" annotation
|
|
156
|
+
// Match "Files:" annotation — always apply (no inline equivalent)
|
|
129
157
|
const filesMatch = line.match(/^\s+-\s+Files:\s+(.+)/i);
|
|
130
158
|
if (filesMatch) {
|
|
131
|
-
current.files
|
|
159
|
+
if (current.files === null) {
|
|
160
|
+
current.files = filesMatch[1].trim();
|
|
161
|
+
}
|
|
132
162
|
continue;
|
|
133
163
|
}
|
|
134
164
|
|
|
135
|
-
// Match "Effort:" annotation
|
|
165
|
+
// Match "Effort:" annotation — only apply if inline parsing found no effort
|
|
136
166
|
const effortMatch = line.match(/^\s+-\s+Effort:\s+(.+)/i);
|
|
137
167
|
if (effortMatch) {
|
|
138
|
-
current.effort
|
|
168
|
+
if (current.effort === null) {
|
|
169
|
+
current.effort = effortMatch[1].trim();
|
|
170
|
+
}
|
|
139
171
|
continue;
|
|
140
172
|
}
|
|
141
173
|
}
|