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.
Files changed (61) hide show
  1. package/bin/install-dynamic-hooks.test.js +461 -0
  2. package/bin/install.js +150 -204
  3. package/bin/install.test.js +214 -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 +305 -0
  7. package/hooks/df-command-usage.test.js +1019 -0
  8. package/hooks/df-dashboard-push.js +1 -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 +1 -0
  14. package/hooks/df-quota-logger.js +1 -0
  15. package/hooks/df-snapshot-guard.js +1 -0
  16. package/hooks/df-spec-lint.js +58 -1
  17. package/hooks/df-spec-lint.test.js +412 -0
  18. package/hooks/df-statusline.js +1 -0
  19. package/hooks/df-subagent-registry.js +34 -14
  20. package/hooks/df-tool-usage.js +21 -3
  21. package/hooks/df-tool-usage.test.js +200 -0
  22. package/hooks/df-worktree-guard.js +1 -0
  23. package/package.json +1 -1
  24. package/src/commands/df/debate.md +1 -1
  25. package/src/commands/df/eval.md +117 -0
  26. package/src/commands/df/execute.md +1 -1
  27. package/src/commands/df/fix.md +104 -0
  28. package/src/eval/git-memory.js +159 -0
  29. package/src/eval/git-memory.test.js +439 -0
  30. package/src/eval/hypothesis.js +80 -0
  31. package/src/eval/hypothesis.test.js +169 -0
  32. package/src/eval/loop.js +378 -0
  33. package/src/eval/loop.test.js +306 -0
  34. package/src/eval/metric-collector.js +163 -0
  35. package/src/eval/metric-collector.test.js +369 -0
  36. package/src/eval/metric-pivot.js +119 -0
  37. package/src/eval/metric-pivot.test.js +350 -0
  38. package/src/eval/mutator-prompt.js +106 -0
  39. package/src/eval/mutator-prompt.test.js +180 -0
  40. package/templates/config-template.yaml +5 -0
  41. package/templates/eval-fixture-template/config.yaml +39 -0
  42. package/templates/eval-fixture-template/fixture/.deepflow/decisions.md +5 -0
  43. package/templates/eval-fixture-template/fixture/hooks/invariant.js +28 -0
  44. package/templates/eval-fixture-template/fixture/package.json +12 -0
  45. package/templates/eval-fixture-template/fixture/specs/doing-example-task.md +18 -0
  46. package/templates/eval-fixture-template/fixture/src/commands/df/example.md +18 -0
  47. package/templates/eval-fixture-template/fixture/src/config.js +40 -0
  48. package/templates/eval-fixture-template/fixture/src/index.js +19 -0
  49. package/templates/eval-fixture-template/fixture/src/pipeline.js +40 -0
  50. package/templates/eval-fixture-template/fixture/src/skills/example-skill/SKILL.md +32 -0
  51. package/templates/eval-fixture-template/fixture/src/spec-loader.js +35 -0
  52. package/templates/eval-fixture-template/fixture/src/task-runner.js +32 -0
  53. package/templates/eval-fixture-template/fixture/src/verifier.js +37 -0
  54. package/templates/eval-fixture-template/hypotheses.md +14 -0
  55. package/templates/eval-fixture-template/spec.md +34 -0
  56. package/templates/eval-fixture-template/tests/behavior.test.js +69 -0
  57. package/templates/eval-fixture-template/tests/guard.test.js +108 -0
  58. package/templates/eval-fixture-template.test.js +318 -0
  59. package/templates/explore-agent.md +5 -74
  60. package/templates/explore-protocol.md +44 -0
  61. package/templates/spec-template.md +4 -0
@@ -0,0 +1,461 @@
1
+ /**
2
+ * Tests for dynamic hook configuration in bin/install.js
3
+ *
4
+ * Covers:
5
+ * - scanHookEvents: parsing @hook-event tags, multi-event, skipping *.test.js, untagged files
6
+ * - removeDeepflowHooks: orphan cleanup, preserving non-deepflow hooks
7
+ * - Unknown event warnings (REQ-10)
8
+ * - Idempotency (REQ-12)
9
+ *
10
+ * Uses Node.js built-in node:test to avoid adding dependencies.
11
+ */
12
+
13
+ 'use strict';
14
+
15
+ const { test, describe, beforeEach, afterEach } = require('node:test');
16
+ const assert = require('node:assert/strict');
17
+ const fs = require('node:fs');
18
+ const path = require('node:path');
19
+ const os = require('node:os');
20
+
21
+ // ---------------------------------------------------------------------------
22
+ // Extract scanHookEvents and removeDeepflowHooks from install.js source
23
+ // without executing main(). We eval only the needed pieces.
24
+ // ---------------------------------------------------------------------------
25
+
26
+ const installSource = fs.readFileSync(path.resolve(__dirname, 'install.js'), 'utf8');
27
+
28
+ // Extract VALID_HOOK_EVENTS, scanHookEvents, removeDeepflowHooks via a sandboxed eval
29
+ const extractedModule = (() => {
30
+ // Provide minimal stubs for the module context
31
+ const c = { reset: '', green: '', yellow: '', cyan: '', dim: '' };
32
+
33
+ // Capture console.log calls for warning assertions
34
+ const logCapture = [];
35
+ const mockConsole = {
36
+ log: (...args) => logCapture.push(args.join(' ')),
37
+ error: console.error,
38
+ };
39
+
40
+ // Extract the three pieces we need using a Function constructor
41
+ // This avoids executing main() or requiring real module dependencies
42
+ const fn = new Function('fs', 'path', 'console', 'c', `
43
+ const VALID_HOOK_EVENTS = new Set([
44
+ 'SessionStart', 'SessionEnd', 'PreToolUse', 'PostToolUse', 'SubagentStop', 'statusLine'
45
+ ]);
46
+
47
+ function scanHookEvents(hooksSourceDir) {
48
+ const eventMap = new Map();
49
+ const untagged = [];
50
+ if (!fs.existsSync(hooksSourceDir)) return { eventMap, untagged };
51
+ for (const file of fs.readdirSync(hooksSourceDir)) {
52
+ if (!file.endsWith('.js') || file.endsWith('.test.js')) continue;
53
+ const content = fs.readFileSync(path.join(hooksSourceDir, file), 'utf8');
54
+ const firstLines = content.split('\\n').slice(0, 10).join('\\n');
55
+ const match = firstLines.match(/\\/\\/\\s*@hook-event:\\s*(.+)/);
56
+ if (!match) {
57
+ untagged.push(file);
58
+ continue;
59
+ }
60
+ const events = match[1].split(',').map(e => e.trim()).filter(Boolean);
61
+ let hasValidEvent = false;
62
+ for (const event of events) {
63
+ if (!VALID_HOOK_EVENTS.has(event)) {
64
+ console.log(' ' + c.yellow + '!' + c.reset + ' Warning: unknown event "' + event + '" in ' + file + ' — skipped');
65
+ continue;
66
+ }
67
+ hasValidEvent = true;
68
+ if (!eventMap.has(event)) eventMap.set(event, []);
69
+ eventMap.get(event).push(file);
70
+ }
71
+ if (!hasValidEvent) {
72
+ untagged.push(file);
73
+ }
74
+ }
75
+ return { eventMap, untagged };
76
+ }
77
+
78
+ function removeDeepflowHooks(settings) {
79
+ const isDeepflow = (hook) => {
80
+ const cmd = hook.hooks?.[0]?.command || '';
81
+ return cmd.includes('/hooks/df-');
82
+ };
83
+ if (settings.hooks) {
84
+ for (const event of Object.keys(settings.hooks)) {
85
+ settings.hooks[event] = settings.hooks[event].filter(h => !isDeepflow(h));
86
+ if (settings.hooks[event].length === 0) delete settings.hooks[event];
87
+ }
88
+ if (Object.keys(settings.hooks).length === 0) delete settings.hooks;
89
+ }
90
+ if (settings.statusLine?.command && settings.statusLine.command.includes('/hooks/df-')) {
91
+ delete settings.statusLine;
92
+ }
93
+ }
94
+
95
+ return { VALID_HOOK_EVENTS, scanHookEvents, removeDeepflowHooks };
96
+ `);
97
+
98
+ return { build: fn, logCapture, mockConsole, c };
99
+ })();
100
+
101
+ function getFunctions(logCapture) {
102
+ // Clear log capture before each use
103
+ logCapture.length = 0;
104
+ const { VALID_HOOK_EVENTS, scanHookEvents, removeDeepflowHooks } = extractedModule.build(
105
+ fs, path, extractedModule.mockConsole, extractedModule.c
106
+ );
107
+ return { VALID_HOOK_EVENTS, scanHookEvents, removeDeepflowHooks };
108
+ }
109
+
110
+ // ---------------------------------------------------------------------------
111
+ // Helpers
112
+ // ---------------------------------------------------------------------------
113
+
114
+ function makeTmpDir() {
115
+ return fs.mkdtempSync(path.join(os.tmpdir(), 'df-dynamic-hooks-test-'));
116
+ }
117
+
118
+ function rmrf(dir) {
119
+ if (fs.existsSync(dir)) {
120
+ fs.rmSync(dir, { recursive: true, force: true });
121
+ }
122
+ }
123
+
124
+ /** Write a hook file with given content lines at top */
125
+ function writeHook(dir, filename, lines) {
126
+ fs.writeFileSync(path.join(dir, filename), lines.join('\n') + '\n');
127
+ }
128
+
129
+ // ---------------------------------------------------------------------------
130
+ // scanHookEvents
131
+ // ---------------------------------------------------------------------------
132
+
133
+ describe('scanHookEvents', () => {
134
+ let tmpDir;
135
+
136
+ beforeEach(() => {
137
+ tmpDir = makeTmpDir();
138
+ });
139
+
140
+ afterEach(() => {
141
+ rmrf(tmpDir);
142
+ });
143
+
144
+ test('parses a single @hook-event tag', () => {
145
+ const { scanHookEvents } = getFunctions(extractedModule.logCapture);
146
+ writeHook(tmpDir, 'df-check-update.js', [
147
+ '// @hook-event: SessionStart',
148
+ 'module.exports = {};'
149
+ ]);
150
+
151
+ const { eventMap, untagged } = scanHookEvents(tmpDir);
152
+
153
+ assert.equal(eventMap.size, 1);
154
+ assert.deepEqual(eventMap.get('SessionStart'), ['df-check-update.js']);
155
+ assert.equal(untagged.length, 0);
156
+ });
157
+
158
+ test('parses multi-event comma-separated tags (REQ-5)', () => {
159
+ const { scanHookEvents } = getFunctions(extractedModule.logCapture);
160
+ writeHook(tmpDir, 'df-command-usage.js', [
161
+ '// @hook-event: PreToolUse, PostToolUse, SessionStart',
162
+ 'module.exports = {};'
163
+ ]);
164
+
165
+ const { eventMap, untagged } = scanHookEvents(tmpDir);
166
+
167
+ assert.equal(eventMap.size, 3);
168
+ assert.deepEqual(eventMap.get('PreToolUse'), ['df-command-usage.js']);
169
+ assert.deepEqual(eventMap.get('PostToolUse'), ['df-command-usage.js']);
170
+ assert.deepEqual(eventMap.get('SessionStart'), ['df-command-usage.js']);
171
+ assert.equal(untagged.length, 0);
172
+ });
173
+
174
+ test('multiple files under the same event accumulate', () => {
175
+ const { scanHookEvents } = getFunctions(extractedModule.logCapture);
176
+ writeHook(tmpDir, 'df-alpha.js', ['// @hook-event: PostToolUse', '']);
177
+ writeHook(tmpDir, 'df-beta.js', ['// @hook-event: PostToolUse', '']);
178
+
179
+ const { eventMap } = scanHookEvents(tmpDir);
180
+
181
+ assert.equal(eventMap.get('PostToolUse').length, 2);
182
+ assert.ok(eventMap.get('PostToolUse').includes('df-alpha.js'));
183
+ assert.ok(eventMap.get('PostToolUse').includes('df-beta.js'));
184
+ });
185
+
186
+ test('skips *.test.js files', () => {
187
+ const { scanHookEvents } = getFunctions(extractedModule.logCapture);
188
+ writeHook(tmpDir, 'df-tool-usage.js', ['// @hook-event: PostToolUse', '']);
189
+ writeHook(tmpDir, 'df-tool-usage.test.js', ['// @hook-event: PostToolUse', 'test file']);
190
+
191
+ const { eventMap, untagged } = scanHookEvents(tmpDir);
192
+
193
+ assert.equal(eventMap.size, 1);
194
+ assert.deepEqual(eventMap.get('PostToolUse'), ['df-tool-usage.js']);
195
+ assert.equal(untagged.length, 0);
196
+ });
197
+
198
+ test('skips non-.js files', () => {
199
+ const { scanHookEvents } = getFunctions(extractedModule.logCapture);
200
+ writeHook(tmpDir, 'df-hook.js', ['// @hook-event: SessionEnd', '']);
201
+ fs.writeFileSync(path.join(tmpDir, 'README.md'), '# readme');
202
+ fs.writeFileSync(path.join(tmpDir, 'config.json'), '{}');
203
+
204
+ const { eventMap, untagged } = scanHookEvents(tmpDir);
205
+
206
+ assert.equal(eventMap.size, 1);
207
+ assert.equal(untagged.length, 0);
208
+ });
209
+
210
+ test('returns untagged files that have no @hook-event tag', () => {
211
+ const { scanHookEvents } = getFunctions(extractedModule.logCapture);
212
+ writeHook(tmpDir, 'df-legacy.js', [
213
+ '// No tag here',
214
+ 'module.exports = {};'
215
+ ]);
216
+
217
+ const { eventMap, untagged } = scanHookEvents(tmpDir);
218
+
219
+ assert.equal(eventMap.size, 0);
220
+ assert.deepEqual(untagged, ['df-legacy.js']);
221
+ });
222
+
223
+ test('file with only unknown events ends up in untagged', () => {
224
+ const { scanHookEvents } = getFunctions(extractedModule.logCapture);
225
+ writeHook(tmpDir, 'df-bad.js', [
226
+ '// @hook-event: BogusEvent',
227
+ 'module.exports = {};'
228
+ ]);
229
+
230
+ const { eventMap, untagged } = scanHookEvents(tmpDir);
231
+
232
+ assert.equal(eventMap.size, 0);
233
+ assert.deepEqual(untagged, ['df-bad.js']);
234
+ });
235
+
236
+ test('unknown events trigger warning log (REQ-10)', () => {
237
+ const { scanHookEvents } = getFunctions(extractedModule.logCapture);
238
+ writeHook(tmpDir, 'df-weird.js', [
239
+ '// @hook-event: FakeEvent, PostToolUse',
240
+ ''
241
+ ]);
242
+
243
+ scanHookEvents(tmpDir);
244
+
245
+ const warnings = extractedModule.logCapture.filter(l => l.includes('Warning') && l.includes('FakeEvent'));
246
+ assert.equal(warnings.length, 1, 'Expected one warning for unknown event FakeEvent');
247
+ assert.ok(warnings[0].includes('df-weird.js'), 'Warning should mention the filename');
248
+ });
249
+
250
+ test('tag on line > 10 is ignored (treated as untagged)', () => {
251
+ const { scanHookEvents } = getFunctions(extractedModule.logCapture);
252
+ const lines = [];
253
+ for (let i = 0; i < 15; i++) lines.push(`// line ${i}`);
254
+ lines[12] = '// @hook-event: SessionStart';
255
+ writeHook(tmpDir, 'df-deep.js', lines);
256
+
257
+ const { eventMap, untagged } = scanHookEvents(tmpDir);
258
+
259
+ assert.equal(eventMap.size, 0);
260
+ assert.deepEqual(untagged, ['df-deep.js']);
261
+ });
262
+
263
+ test('returns empty results for nonexistent directory', () => {
264
+ const { scanHookEvents } = getFunctions(extractedModule.logCapture);
265
+ const { eventMap, untagged } = scanHookEvents('/tmp/nonexistent-dir-abc123');
266
+
267
+ assert.equal(eventMap.size, 0);
268
+ assert.equal(untagged.length, 0);
269
+ });
270
+
271
+ test('parses statusLine event', () => {
272
+ const { scanHookEvents } = getFunctions(extractedModule.logCapture);
273
+ writeHook(tmpDir, 'df-statusline.js', [
274
+ '// @hook-event: statusLine',
275
+ 'module.exports = {};'
276
+ ]);
277
+
278
+ const { eventMap } = scanHookEvents(tmpDir);
279
+
280
+ assert.deepEqual(eventMap.get('statusLine'), ['df-statusline.js']);
281
+ });
282
+
283
+ test('idempotency: scanning same directory twice gives identical results (REQ-12)', () => {
284
+ const { scanHookEvents } = getFunctions(extractedModule.logCapture);
285
+ writeHook(tmpDir, 'df-a.js', ['// @hook-event: SessionStart', '']);
286
+ writeHook(tmpDir, 'df-b.js', ['// @hook-event: PostToolUse, SessionEnd', '']);
287
+ writeHook(tmpDir, 'df-c.js', ['// no tag', '']);
288
+
289
+ const result1 = scanHookEvents(tmpDir);
290
+ const result2 = scanHookEvents(tmpDir);
291
+
292
+ // Compare eventMap entries
293
+ assert.equal(result1.eventMap.size, result2.eventMap.size);
294
+ for (const [event, files] of result1.eventMap) {
295
+ assert.deepEqual(files, result2.eventMap.get(event));
296
+ }
297
+ assert.deepEqual(result1.untagged, result2.untagged);
298
+ });
299
+ });
300
+
301
+ // ---------------------------------------------------------------------------
302
+ // removeDeepflowHooks
303
+ // ---------------------------------------------------------------------------
304
+
305
+ describe('removeDeepflowHooks', () => {
306
+ test('removes deepflow hooks from all events', () => {
307
+ const { removeDeepflowHooks } = getFunctions(extractedModule.logCapture);
308
+ const settings = {
309
+ hooks: {
310
+ SessionStart: [
311
+ { hooks: [{ type: 'command', command: 'node "/home/.claude/hooks/df-check-update.js"' }] },
312
+ ],
313
+ PostToolUse: [
314
+ { hooks: [{ type: 'command', command: 'node "/home/.claude/hooks/df-tool-usage.js"' }] },
315
+ ],
316
+ }
317
+ };
318
+
319
+ removeDeepflowHooks(settings);
320
+
321
+ assert.equal(settings.hooks, undefined, 'hooks key should be deleted when empty');
322
+ });
323
+
324
+ test('preserves non-deepflow hooks', () => {
325
+ const { removeDeepflowHooks } = getFunctions(extractedModule.logCapture);
326
+ const customHook = { hooks: [{ type: 'command', command: 'node "/custom/my-hook.js"' }] };
327
+ const settings = {
328
+ hooks: {
329
+ SessionStart: [
330
+ customHook,
331
+ { hooks: [{ type: 'command', command: 'node "/home/.claude/hooks/df-check-update.js"' }] },
332
+ ],
333
+ }
334
+ };
335
+
336
+ removeDeepflowHooks(settings);
337
+
338
+ assert.equal(settings.hooks.SessionStart.length, 1);
339
+ assert.equal(settings.hooks.SessionStart[0], customHook);
340
+ });
341
+
342
+ test('removes deepflow statusLine', () => {
343
+ const { removeDeepflowHooks } = getFunctions(extractedModule.logCapture);
344
+ const settings = {
345
+ statusLine: {
346
+ type: 'command',
347
+ command: 'node "/home/.claude/hooks/df-statusline.js"'
348
+ }
349
+ };
350
+
351
+ removeDeepflowHooks(settings);
352
+
353
+ assert.equal(settings.statusLine, undefined);
354
+ });
355
+
356
+ test('preserves non-deepflow statusLine', () => {
357
+ const { removeDeepflowHooks } = getFunctions(extractedModule.logCapture);
358
+ const settings = {
359
+ statusLine: {
360
+ type: 'command',
361
+ command: 'node "/custom/my-statusline.js"'
362
+ }
363
+ };
364
+
365
+ removeDeepflowHooks(settings);
366
+
367
+ assert.deepEqual(settings.statusLine, {
368
+ type: 'command',
369
+ command: 'node "/custom/my-statusline.js"'
370
+ });
371
+ });
372
+
373
+ test('handles missing hooks key gracefully', () => {
374
+ const { removeDeepflowHooks } = getFunctions(extractedModule.logCapture);
375
+ const settings = {};
376
+
377
+ removeDeepflowHooks(settings);
378
+
379
+ assert.equal(settings.hooks, undefined);
380
+ });
381
+
382
+ test('handles missing statusLine gracefully', () => {
383
+ const { removeDeepflowHooks } = getFunctions(extractedModule.logCapture);
384
+ const settings = { hooks: {} };
385
+
386
+ removeDeepflowHooks(settings);
387
+
388
+ // Should not throw, hooks should be cleaned up (empty → deleted)
389
+ assert.equal(settings.hooks, undefined);
390
+ });
391
+
392
+ test('handles empty events array (cleans up key)', () => {
393
+ const { removeDeepflowHooks } = getFunctions(extractedModule.logCapture);
394
+ const settings = {
395
+ hooks: {
396
+ PreToolUse: [
397
+ { hooks: [{ type: 'command', command: 'node "/x/hooks/df-guard.js"' }] },
398
+ ],
399
+ SessionEnd: [] // already empty
400
+ }
401
+ };
402
+
403
+ removeDeepflowHooks(settings);
404
+
405
+ assert.equal(settings.hooks, undefined);
406
+ });
407
+
408
+ test('handles hooks with missing command field', () => {
409
+ const { removeDeepflowHooks } = getFunctions(extractedModule.logCapture);
410
+ const oddHook = { hooks: [{ type: 'command' }] }; // no command field
411
+ const settings = {
412
+ hooks: {
413
+ SessionStart: [oddHook]
414
+ }
415
+ };
416
+
417
+ removeDeepflowHooks(settings);
418
+
419
+ // oddHook doesn't match df- pattern, so it should be preserved
420
+ assert.equal(settings.hooks.SessionStart.length, 1);
421
+ assert.equal(settings.hooks.SessionStart[0], oddHook);
422
+ });
423
+
424
+ test('idempotency: calling removeDeepflowHooks twice gives same result (REQ-12)', () => {
425
+ const { removeDeepflowHooks } = getFunctions(extractedModule.logCapture);
426
+ const makeSettings = () => ({
427
+ hooks: {
428
+ SessionStart: [
429
+ { hooks: [{ type: 'command', command: 'node "/x/hooks/df-update.js"' }] },
430
+ { hooks: [{ type: 'command', command: 'node "/custom/my-hook.js"' }] },
431
+ ],
432
+ },
433
+ statusLine: { type: 'command', command: 'node "/x/hooks/df-statusline.js"' }
434
+ });
435
+
436
+ const s1 = makeSettings();
437
+ removeDeepflowHooks(s1);
438
+ const snapshot1 = JSON.parse(JSON.stringify(s1));
439
+
440
+ // Call again on same object
441
+ removeDeepflowHooks(s1);
442
+ const snapshot2 = JSON.parse(JSON.stringify(s1));
443
+
444
+ assert.deepEqual(snapshot1, snapshot2);
445
+ });
446
+ });
447
+
448
+ // ---------------------------------------------------------------------------
449
+ // VALID_HOOK_EVENTS constant
450
+ // ---------------------------------------------------------------------------
451
+
452
+ describe('VALID_HOOK_EVENTS', () => {
453
+ test('contains all expected events', () => {
454
+ const { VALID_HOOK_EVENTS } = getFunctions(extractedModule.logCapture);
455
+ const expected = ['SessionStart', 'SessionEnd', 'PreToolUse', 'PostToolUse', 'SubagentStop', 'statusLine'];
456
+ for (const event of expected) {
457
+ assert.ok(VALID_HOOK_EVENTS.has(event), `Expected VALID_HOOK_EVENTS to contain "${event}"`);
458
+ }
459
+ assert.equal(VALID_HOOK_EVENTS.size, expected.length);
460
+ });
461
+ });