deepflow 0.1.102 → 0.1.104
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-dynamic-hooks.test.js +461 -0
- package/bin/install.js +150 -204
- package/bin/install.test.js +214 -0
- package/bin/lineage-ingest.js +70 -0
- package/hooks/df-check-update.js +1 -0
- package/hooks/df-command-usage.js +305 -0
- package/hooks/df-command-usage.test.js +1019 -0
- package/hooks/df-dashboard-push.js +1 -0
- package/hooks/df-execution-history.js +1 -0
- package/hooks/df-explore-protocol.js +83 -0
- package/hooks/df-explore-protocol.test.js +228 -0
- package/hooks/df-hook-event-tags.test.js +127 -0
- package/hooks/df-invariant-check.js +1 -0
- package/hooks/df-quota-logger.js +1 -0
- package/hooks/df-snapshot-guard.js +1 -0
- package/hooks/df-spec-lint.js +58 -1
- package/hooks/df-spec-lint.test.js +412 -0
- package/hooks/df-statusline.js +1 -0
- package/hooks/df-subagent-registry.js +34 -14
- package/hooks/df-tool-usage.js +21 -3
- package/hooks/df-tool-usage.test.js +200 -0
- package/hooks/df-worktree-guard.js +1 -0
- package/package.json +1 -1
- package/src/commands/df/debate.md +1 -1
- package/src/commands/df/eval.md +117 -0
- package/src/commands/df/execute.md +1 -1
- package/src/commands/df/fix.md +104 -0
- package/src/eval/git-memory.js +159 -0
- package/src/eval/git-memory.test.js +439 -0
- package/src/eval/hypothesis.js +80 -0
- package/src/eval/hypothesis.test.js +169 -0
- package/src/eval/loop.js +378 -0
- package/src/eval/loop.test.js +306 -0
- package/src/eval/metric-collector.js +163 -0
- package/src/eval/metric-collector.test.js +369 -0
- package/src/eval/metric-pivot.js +119 -0
- package/src/eval/metric-pivot.test.js +350 -0
- package/src/eval/mutator-prompt.js +106 -0
- package/src/eval/mutator-prompt.test.js +180 -0
- package/templates/config-template.yaml +5 -0
- package/templates/eval-fixture-template/config.yaml +39 -0
- package/templates/eval-fixture-template/fixture/.deepflow/decisions.md +5 -0
- package/templates/eval-fixture-template/fixture/hooks/invariant.js +28 -0
- package/templates/eval-fixture-template/fixture/package.json +12 -0
- package/templates/eval-fixture-template/fixture/specs/doing-example-task.md +18 -0
- package/templates/eval-fixture-template/fixture/src/commands/df/example.md +18 -0
- package/templates/eval-fixture-template/fixture/src/config.js +40 -0
- package/templates/eval-fixture-template/fixture/src/index.js +19 -0
- package/templates/eval-fixture-template/fixture/src/pipeline.js +40 -0
- package/templates/eval-fixture-template/fixture/src/skills/example-skill/SKILL.md +32 -0
- package/templates/eval-fixture-template/fixture/src/spec-loader.js +35 -0
- package/templates/eval-fixture-template/fixture/src/task-runner.js +32 -0
- package/templates/eval-fixture-template/fixture/src/verifier.js +37 -0
- package/templates/eval-fixture-template/hypotheses.md +14 -0
- package/templates/eval-fixture-template/spec.md +34 -0
- package/templates/eval-fixture-template/tests/behavior.test.js +69 -0
- package/templates/eval-fixture-template/tests/guard.test.js +108 -0
- package/templates/eval-fixture-template.test.js +318 -0
- package/templates/explore-agent.md +5 -74
- package/templates/explore-protocol.md +44 -0
- 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)');
|
|
@@ -244,18 +252,82 @@ function copyDir(src, dest) {
|
|
|
244
252
|
}
|
|
245
253
|
}
|
|
246
254
|
|
|
255
|
+
// Valid hook events (settings.hooks keys + special "statusLine")
|
|
256
|
+
const VALID_HOOK_EVENTS = new Set([
|
|
257
|
+
'SessionStart', 'SessionEnd', 'PreToolUse', 'PostToolUse', 'SubagentStop', 'statusLine'
|
|
258
|
+
]);
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Scan hook source files for @hook-event tags. Returns:
|
|
262
|
+
* { eventMap: Map<event, [filename, ...]>, untagged: [filename, ...] }
|
|
263
|
+
*/
|
|
264
|
+
function scanHookEvents(hooksSourceDir) {
|
|
265
|
+
const eventMap = new Map(); // event → [filenames]
|
|
266
|
+
const untagged = [];
|
|
267
|
+
|
|
268
|
+
if (!fs.existsSync(hooksSourceDir)) return { eventMap, untagged };
|
|
269
|
+
|
|
270
|
+
for (const file of fs.readdirSync(hooksSourceDir)) {
|
|
271
|
+
if (!file.endsWith('.js') || file.endsWith('.test.js')) continue;
|
|
272
|
+
|
|
273
|
+
const content = fs.readFileSync(path.join(hooksSourceDir, file), 'utf8');
|
|
274
|
+
const firstLines = content.split('\n').slice(0, 10).join('\n');
|
|
275
|
+
const match = firstLines.match(/\/\/\s*@hook-event:\s*(.+)/);
|
|
276
|
+
|
|
277
|
+
if (!match) {
|
|
278
|
+
untagged.push(file);
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const events = match[1].split(',').map(e => e.trim()).filter(Boolean);
|
|
283
|
+
let hasValidEvent = false;
|
|
284
|
+
|
|
285
|
+
for (const event of events) {
|
|
286
|
+
if (!VALID_HOOK_EVENTS.has(event)) {
|
|
287
|
+
console.log(` ${c.yellow}!${c.reset} Warning: unknown event "${event}" in ${file} — skipped`);
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
hasValidEvent = true;
|
|
291
|
+
if (!eventMap.has(event)) eventMap.set(event, []);
|
|
292
|
+
eventMap.get(event).push(file);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (!hasValidEvent) {
|
|
296
|
+
untagged.push(file);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return { eventMap, untagged };
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Remove all deepflow hook entries (commands containing /hooks/df-) from settings.
|
|
305
|
+
* Preserves non-deepflow hooks.
|
|
306
|
+
*/
|
|
307
|
+
function removeDeepflowHooks(settings) {
|
|
308
|
+
const isDeepflow = (hook) => {
|
|
309
|
+
const cmd = hook.hooks?.[0]?.command || '';
|
|
310
|
+
return cmd.includes('/hooks/df-');
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
// Clean settings.hooks.*
|
|
314
|
+
if (settings.hooks) {
|
|
315
|
+
for (const event of Object.keys(settings.hooks)) {
|
|
316
|
+
settings.hooks[event] = settings.hooks[event].filter(h => !isDeepflow(h));
|
|
317
|
+
if (settings.hooks[event].length === 0) delete settings.hooks[event];
|
|
318
|
+
}
|
|
319
|
+
if (Object.keys(settings.hooks).length === 0) delete settings.hooks;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Clean settings.statusLine if it's a deepflow hook
|
|
323
|
+
if (settings.statusLine?.command && settings.statusLine.command.includes('/hooks/df-')) {
|
|
324
|
+
delete settings.statusLine;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
247
328
|
async function configureHooks(claudeDir) {
|
|
248
329
|
const settingsPath = path.join(claudeDir, 'settings.json');
|
|
249
|
-
const
|
|
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')}"`;
|
|
330
|
+
const hooksSourceDir = path.join(PACKAGE_DIR, 'hooks');
|
|
259
331
|
|
|
260
332
|
let settings = {};
|
|
261
333
|
|
|
@@ -276,158 +348,64 @@ async function configureHooks(claudeDir) {
|
|
|
276
348
|
configurePermissions(settings);
|
|
277
349
|
log('Agent permissions configured');
|
|
278
350
|
|
|
279
|
-
//
|
|
280
|
-
|
|
281
|
-
if (process.stdin.isTTY) {
|
|
282
|
-
const answer = await ask(
|
|
283
|
-
` ${c.yellow}!${c.reset} Existing statusLine found. Replace with deepflow? [y/N] `
|
|
284
|
-
);
|
|
285
|
-
if (answer.toLowerCase() === 'y') {
|
|
286
|
-
settings.statusLine = { type: 'command', command: statuslineCmd };
|
|
287
|
-
log('Statusline configured');
|
|
288
|
-
} else {
|
|
289
|
-
console.log(` ${c.yellow}!${c.reset} Skipped statusline configuration`);
|
|
290
|
-
}
|
|
291
|
-
} else {
|
|
292
|
-
// Non-interactive (e.g. Claude Code bash tool) — skip prompt, keep existing
|
|
293
|
-
console.log(` ${c.yellow}!${c.reset} Existing statusLine found — kept (non-interactive mode)`);
|
|
294
|
-
}
|
|
295
|
-
} else {
|
|
296
|
-
settings.statusLine = { type: 'command', command: statuslineCmd };
|
|
297
|
-
log('Statusline configured');
|
|
298
|
-
}
|
|
351
|
+
// Scan hook files for @hook-event tags
|
|
352
|
+
const { eventMap, untagged } = scanHookEvents(hooksSourceDir);
|
|
299
353
|
|
|
300
|
-
//
|
|
301
|
-
|
|
302
|
-
settings.hooks
|
|
303
|
-
}
|
|
304
|
-
if (!settings.hooks.SessionStart) {
|
|
305
|
-
settings.hooks.SessionStart = [];
|
|
306
|
-
}
|
|
354
|
+
// Remember if there was a pre-existing non-deepflow statusLine
|
|
355
|
+
const hadExternalStatusLine = settings.statusLine &&
|
|
356
|
+
!settings.statusLine.command?.includes('/hooks/df-');
|
|
307
357
|
|
|
308
|
-
// Remove
|
|
309
|
-
settings
|
|
310
|
-
const cmd = hook.hooks?.[0]?.command || '';
|
|
311
|
-
return !cmd.includes('df-check-update') && !cmd.includes('df-quota-logger');
|
|
312
|
-
});
|
|
313
|
-
|
|
314
|
-
// Add update check hook
|
|
315
|
-
settings.hooks.SessionStart.push({
|
|
316
|
-
hooks: [{
|
|
317
|
-
type: 'command',
|
|
318
|
-
command: updateCheckCmd
|
|
319
|
-
}]
|
|
320
|
-
});
|
|
321
|
-
|
|
322
|
-
// Add quota logger to SessionStart
|
|
323
|
-
settings.hooks.SessionStart.push({
|
|
324
|
-
hooks: [{
|
|
325
|
-
type: 'command',
|
|
326
|
-
command: quotaLoggerCmd
|
|
327
|
-
}]
|
|
328
|
-
});
|
|
329
|
-
log('SessionStart hook configured');
|
|
358
|
+
// Remove all existing deepflow hooks (orphan cleanup + idempotency)
|
|
359
|
+
removeDeepflowHooks(settings);
|
|
330
360
|
|
|
331
|
-
//
|
|
332
|
-
if (!settings.hooks
|
|
333
|
-
settings.hooks.SessionEnd = [];
|
|
334
|
-
}
|
|
361
|
+
// Wire hooks by event
|
|
362
|
+
if (!settings.hooks) settings.hooks = {};
|
|
335
363
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
364
|
+
for (const [event, files] of eventMap) {
|
|
365
|
+
if (event === 'statusLine') {
|
|
366
|
+
// Handle statusLine separately — it's settings.statusLine, not settings.hooks
|
|
367
|
+
const statusFile = files[0]; // Only one statusline hook expected
|
|
368
|
+
const statusCmd = `node "${path.join(claudeDir, 'hooks', statusFile)}"`;
|
|
341
369
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
370
|
+
if (hadExternalStatusLine) {
|
|
371
|
+
if (process.stdin.isTTY) {
|
|
372
|
+
const answer = await ask(
|
|
373
|
+
` ${c.yellow}!${c.reset} Existing statusLine found. Replace with deepflow? [y/N] `
|
|
374
|
+
);
|
|
375
|
+
if (answer.toLowerCase() === 'y') {
|
|
376
|
+
settings.statusLine = { type: 'command', command: statusCmd };
|
|
377
|
+
log('Statusline configured');
|
|
378
|
+
} else {
|
|
379
|
+
console.log(` ${c.yellow}!${c.reset} Skipped statusline configuration`);
|
|
380
|
+
}
|
|
381
|
+
} else {
|
|
382
|
+
// Non-interactive (e.g. Claude Code bash tool) — skip prompt, keep existing
|
|
383
|
+
console.log(` ${c.yellow}!${c.reset} Existing statusLine found — kept (non-interactive mode)`);
|
|
384
|
+
}
|
|
385
|
+
} else {
|
|
386
|
+
settings.statusLine = { type: 'command', command: statusCmd };
|
|
387
|
+
log('Statusline configured');
|
|
388
|
+
}
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
349
391
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
hooks: [{
|
|
353
|
-
type: 'command',
|
|
354
|
-
command: dashboardPushCmd
|
|
355
|
-
}]
|
|
356
|
-
});
|
|
357
|
-
log('Quota logger + dashboard push configured (SessionEnd)');
|
|
392
|
+
// Regular hook events
|
|
393
|
+
if (!settings.hooks[event]) settings.hooks[event] = [];
|
|
358
394
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
395
|
+
for (const file of files) {
|
|
396
|
+
const cmd = `node "${path.join(claudeDir, 'hooks', file)}"`;
|
|
397
|
+
settings.hooks[event].push({
|
|
398
|
+
hooks: [{ type: 'command', command: cmd }]
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
log(`${event} hook configured`);
|
|
362
402
|
}
|
|
363
403
|
|
|
364
|
-
//
|
|
365
|
-
|
|
366
|
-
|
|
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');
|
|
368
|
-
});
|
|
369
|
-
|
|
370
|
-
// Add tool usage hook
|
|
371
|
-
settings.hooks.PostToolUse.push({
|
|
372
|
-
hooks: [{
|
|
373
|
-
type: 'command',
|
|
374
|
-
command: toolUsageCmd
|
|
375
|
-
}]
|
|
376
|
-
});
|
|
377
|
-
|
|
378
|
-
// Add execution history hook
|
|
379
|
-
settings.hooks.PostToolUse.push({
|
|
380
|
-
hooks: [{
|
|
381
|
-
type: 'command',
|
|
382
|
-
command: executionHistoryCmd
|
|
383
|
-
}]
|
|
384
|
-
});
|
|
385
|
-
|
|
386
|
-
// Add worktree guard hook (blocks Write/Edit to main-branch files when df/* worktree exists)
|
|
387
|
-
settings.hooks.PostToolUse.push({
|
|
388
|
-
hooks: [{
|
|
389
|
-
type: 'command',
|
|
390
|
-
command: worktreeGuardCmd
|
|
391
|
-
}]
|
|
392
|
-
});
|
|
393
|
-
|
|
394
|
-
// Add snapshot guard hook (blocks Write/Edit to ratchet-baseline files in auto-snapshot.txt)
|
|
395
|
-
settings.hooks.PostToolUse.push({
|
|
396
|
-
hooks: [{
|
|
397
|
-
type: 'command',
|
|
398
|
-
command: snapshotGuardCmd
|
|
399
|
-
}]
|
|
400
|
-
});
|
|
401
|
-
|
|
402
|
-
// Add invariant check hook (exits 1 on hard failures after git commit)
|
|
403
|
-
settings.hooks.PostToolUse.push({
|
|
404
|
-
hooks: [{
|
|
405
|
-
type: 'command',
|
|
406
|
-
command: invariantCheckCmd
|
|
407
|
-
}]
|
|
408
|
-
});
|
|
409
|
-
log('PostToolUse hook configured');
|
|
410
|
-
|
|
411
|
-
// Configure SubagentStop hook for subagent registry
|
|
412
|
-
if (!settings.hooks.SubagentStop) {
|
|
413
|
-
settings.hooks.SubagentStop = [];
|
|
404
|
+
// Log untagged files (copied but not wired)
|
|
405
|
+
for (const file of untagged) {
|
|
406
|
+
console.log(` ${c.dim}${file} copied (no @hook-event tag — not wired)${c.reset}`);
|
|
414
407
|
}
|
|
415
408
|
|
|
416
|
-
// Remove any existing subagent registry hooks
|
|
417
|
-
settings.hooks.SubagentStop = settings.hooks.SubagentStop.filter(hook => {
|
|
418
|
-
const cmd = hook.hooks?.[0]?.command || '';
|
|
419
|
-
return !cmd.includes('df-subagent-registry');
|
|
420
|
-
});
|
|
421
|
-
|
|
422
|
-
// Add subagent registry hook
|
|
423
|
-
settings.hooks.SubagentStop.push({
|
|
424
|
-
hooks: [{
|
|
425
|
-
type: 'command',
|
|
426
|
-
command: subagentRegistryCmd
|
|
427
|
-
}]
|
|
428
|
-
});
|
|
429
|
-
log('SubagentStop hook configured');
|
|
430
|
-
|
|
431
409
|
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
|
432
410
|
}
|
|
433
411
|
|
|
@@ -607,11 +585,20 @@ async function uninstall() {
|
|
|
607
585
|
'agents/reasoner.md',
|
|
608
586
|
'bin/plan-consolidator.js',
|
|
609
587
|
'bin/wave-runner.js',
|
|
610
|
-
'bin/ratchet.js'
|
|
588
|
+
'bin/ratchet.js',
|
|
589
|
+
'templates'
|
|
611
590
|
];
|
|
612
591
|
|
|
613
592
|
if (level === 'global') {
|
|
614
|
-
|
|
593
|
+
// Dynamically find all df-*.js hook files to remove
|
|
594
|
+
const hooksDir = path.join(CLAUDE_DIR, 'hooks');
|
|
595
|
+
if (fs.existsSync(hooksDir)) {
|
|
596
|
+
for (const file of fs.readdirSync(hooksDir)) {
|
|
597
|
+
if (file.startsWith('df-') && file.endsWith('.js')) {
|
|
598
|
+
toRemove.push(`hooks/${file}`);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
}
|
|
615
602
|
}
|
|
616
603
|
|
|
617
604
|
for (const item of toRemove) {
|
|
@@ -631,67 +618,25 @@ async function uninstall() {
|
|
|
631
618
|
}
|
|
632
619
|
}
|
|
633
620
|
|
|
634
|
-
// Remove
|
|
621
|
+
// Remove hook entries and settings from global settings.json
|
|
635
622
|
if (level === 'global') {
|
|
636
623
|
const settingsPath = path.join(CLAUDE_DIR, 'settings.json');
|
|
637
624
|
if (fs.existsSync(settingsPath)) {
|
|
638
625
|
try {
|
|
639
626
|
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
640
|
-
if (settings.hooks?.SessionStart) {
|
|
641
|
-
settings.hooks.SessionStart = settings.hooks.SessionStart.filter(hook => {
|
|
642
|
-
const cmd = hook.hooks?.[0]?.command || '';
|
|
643
|
-
return !cmd.includes('df-check-update') && !cmd.includes('df-quota-logger');
|
|
644
|
-
});
|
|
645
|
-
if (settings.hooks.SessionStart.length === 0) {
|
|
646
|
-
delete settings.hooks.SessionStart;
|
|
647
|
-
}
|
|
648
|
-
}
|
|
649
|
-
if (settings.hooks?.SessionEnd) {
|
|
650
|
-
settings.hooks.SessionEnd = settings.hooks.SessionEnd.filter(hook => {
|
|
651
|
-
const cmd = hook.hooks?.[0]?.command || '';
|
|
652
|
-
return !cmd.includes('df-quota-logger') && !cmd.includes('df-dashboard-push');
|
|
653
|
-
});
|
|
654
|
-
if (settings.hooks.SessionEnd.length === 0) {
|
|
655
|
-
delete settings.hooks.SessionEnd;
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
if (settings.hooks?.PostToolUse) {
|
|
659
|
-
settings.hooks.PostToolUse = settings.hooks.PostToolUse.filter(hook => {
|
|
660
|
-
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');
|
|
662
|
-
});
|
|
663
|
-
if (settings.hooks.PostToolUse.length === 0) {
|
|
664
|
-
delete settings.hooks.PostToolUse;
|
|
665
|
-
}
|
|
666
|
-
}
|
|
667
|
-
if (settings.hooks?.SubagentStop) {
|
|
668
|
-
settings.hooks.SubagentStop = settings.hooks.SubagentStop.filter(hook => {
|
|
669
|
-
const cmd = hook.hooks?.[0]?.command || '';
|
|
670
|
-
return !cmd.includes('df-subagent-registry');
|
|
671
|
-
});
|
|
672
|
-
if (settings.hooks.SubagentStop.length === 0) {
|
|
673
|
-
delete settings.hooks.SubagentStop;
|
|
674
|
-
}
|
|
675
|
-
}
|
|
676
|
-
if (settings.hooks && Object.keys(settings.hooks).length === 0) {
|
|
677
|
-
delete settings.hooks;
|
|
678
|
-
}
|
|
679
|
-
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
|
680
|
-
console.log(` ${c.green}✓${c.reset} Removed SessionStart/SessionEnd/PostToolUse/SubagentStop hooks`);
|
|
681
|
-
} catch (e) {
|
|
682
|
-
// Fail silently
|
|
683
|
-
}
|
|
684
|
-
}
|
|
685
627
|
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
628
|
+
// Remove all deepflow hook wiring dynamically
|
|
629
|
+
removeDeepflowHooks(settings);
|
|
630
|
+
console.log(` ${c.green}✓${c.reset} Removed deepflow hooks from settings`);
|
|
631
|
+
|
|
632
|
+
// Remove ENABLE_LSP_TOOL
|
|
690
633
|
if (settings.env?.ENABLE_LSP_TOOL) {
|
|
691
634
|
delete settings.env.ENABLE_LSP_TOOL;
|
|
692
635
|
if (settings.env && Object.keys(settings.env).length === 0) delete settings.env;
|
|
693
636
|
console.log(` ${c.green}✓${c.reset} Removed ENABLE_LSP_TOOL from settings`);
|
|
694
637
|
}
|
|
638
|
+
|
|
639
|
+
// Remove deepflow permissions
|
|
695
640
|
if (settings.permissions?.allow) {
|
|
696
641
|
const dfPerms = new Set(DEEPFLOW_PERMISSIONS);
|
|
697
642
|
settings.permissions.allow = settings.permissions.allow.filter(p => !dfPerms.has(p));
|
|
@@ -699,6 +644,7 @@ async function uninstall() {
|
|
|
699
644
|
if (settings.permissions && Object.keys(settings.permissions).length === 0) delete settings.permissions;
|
|
700
645
|
console.log(` ${c.green}✓${c.reset} Removed deepflow permissions from settings`);
|
|
701
646
|
}
|
|
647
|
+
|
|
702
648
|
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
|
703
649
|
} catch (e) {
|
|
704
650
|
// Fail silently
|
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
|
// ---------------------------------------------------------------------------
|