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,412 @@
1
+ /**
2
+ * Tests for hooks/df-spec-lint.js
3
+ *
4
+ * Validates that computeLayer, validateSpec, and extractSection correctly
5
+ * handle YAML frontmatter (including derives-from fields) without
6
+ * misinterpreting frontmatter lines as section headers.
7
+ *
8
+ * Uses Node.js built-in node:test to avoid adding dependencies.
9
+ */
10
+
11
+ 'use strict';
12
+
13
+ const { test, describe } = require('node:test');
14
+ const assert = require('node:assert/strict');
15
+ const fs = require('fs');
16
+ const path = require('path');
17
+ const os = require('os');
18
+
19
+ const { computeLayer, validateSpec, extractSection, parseFrontmatter } = require('./df-spec-lint');
20
+
21
+ // ---------------------------------------------------------------------------
22
+ // Helpers
23
+ // ---------------------------------------------------------------------------
24
+
25
+ /** Minimal L0 spec (just Objective) */
26
+ function minimalSpec(objective = 'Build the thing') {
27
+ return `## Objective\n${objective}\n`;
28
+ }
29
+
30
+ /** Full L3 spec with all required sections */
31
+ function fullSpec() {
32
+ return [
33
+ '## Objective',
34
+ 'Build the thing',
35
+ '',
36
+ '## Requirements',
37
+ '- REQ-1: Do something',
38
+ '',
39
+ '## Constraints',
40
+ 'Must be fast',
41
+ '',
42
+ '## Out of Scope',
43
+ 'Not doing X',
44
+ '',
45
+ '## Acceptance Criteria',
46
+ '- [ ] REQ-1 works',
47
+ '',
48
+ '## Technical Notes',
49
+ 'Use module Y',
50
+ ].join('\n');
51
+ }
52
+
53
+ /** Wrap content with YAML frontmatter */
54
+ function withFrontmatter(body, fields = {}) {
55
+ const yamlLines = Object.entries(fields).map(([k, v]) => `${k}: ${v}`);
56
+ return ['---', ...yamlLines, '---', '', body].join('\n');
57
+ }
58
+
59
+ // ---------------------------------------------------------------------------
60
+ // computeLayer — frontmatter handling
61
+ // ---------------------------------------------------------------------------
62
+
63
+ describe('computeLayer', () => {
64
+ test('returns L0 for spec with only Objective', () => {
65
+ assert.equal(computeLayer(minimalSpec()), 0);
66
+ });
67
+
68
+ test('returns L0 when frontmatter with derives-from precedes Objective', () => {
69
+ const content = withFrontmatter(minimalSpec(), {
70
+ 'derives-from': 'done-auth',
71
+ });
72
+ assert.equal(computeLayer(content), 0);
73
+ });
74
+
75
+ test('returns L3 for full spec with derives-from frontmatter', () => {
76
+ const content = withFrontmatter(fullSpec(), {
77
+ 'derives-from': 'done-auth',
78
+ name: 'spec-lineage',
79
+ });
80
+ assert.equal(computeLayer(content), 3);
81
+ });
82
+
83
+ test('frontmatter --- lines are not counted as section headers', () => {
84
+ // If --- were mistaken for headers, layer computation would break.
85
+ // Verify that a spec with frontmatter computes same layer as without.
86
+ const bare = fullSpec();
87
+ const wrapped = withFrontmatter(fullSpec(), {
88
+ 'derives-from': 'done-auth',
89
+ });
90
+ assert.equal(computeLayer(bare), computeLayer(wrapped));
91
+ });
92
+
93
+ test('derives-from value is not mistaken for a section name', () => {
94
+ // derives-from: done-auth — should not create a phantom header
95
+ const content = withFrontmatter(minimalSpec(), {
96
+ 'derives-from': 'done-auth',
97
+ });
98
+ // Still L0, not some higher layer from phantom headers
99
+ assert.equal(computeLayer(content), 0);
100
+ });
101
+
102
+ test('returns -1 when frontmatter exists but no Objective section', () => {
103
+ const content = withFrontmatter('Just some text, no headings.', {
104
+ 'derives-from': 'done-auth',
105
+ });
106
+ assert.equal(computeLayer(content), -1);
107
+ });
108
+ });
109
+
110
+ // ---------------------------------------------------------------------------
111
+ // validateSpec — frontmatter handling
112
+ // ---------------------------------------------------------------------------
113
+
114
+ describe('validateSpec with frontmatter', () => {
115
+ test('full spec with derives-from frontmatter produces no hard errors', () => {
116
+ const content = withFrontmatter(fullSpec(), {
117
+ 'derives-from': 'done-auth',
118
+ });
119
+ const result = validateSpec(content);
120
+ assert.deepEqual(result.hard, []);
121
+ });
122
+
123
+ test('layer is correctly reported when frontmatter is present', () => {
124
+ const content = withFrontmatter(fullSpec(), {
125
+ 'derives-from': 'done-auth',
126
+ });
127
+ const result = validateSpec(content);
128
+ assert.equal(result.layer, 3);
129
+ });
130
+
131
+ test('L0 spec with frontmatter reports missing sections as advisory only', () => {
132
+ const content = withFrontmatter(minimalSpec(), {
133
+ 'derives-from': 'done-auth',
134
+ });
135
+ const result = validateSpec(content);
136
+ // L0 only requires Objective — everything else is advisory
137
+ assert.deepEqual(result.hard, []);
138
+ assert.ok(result.advisory.length > 0, 'should have advisory warnings for missing sections');
139
+ });
140
+
141
+ test('frontmatter --- delimiters do not appear in hard or advisory messages', () => {
142
+ const content = withFrontmatter(fullSpec(), {
143
+ 'derives-from': 'done-auth',
144
+ });
145
+ const result = validateSpec(content);
146
+ const allMessages = [...result.hard, ...result.advisory];
147
+ for (const msg of allMessages) {
148
+ assert.ok(!msg.includes('---'), `Unexpected --- in message: ${msg}`);
149
+ }
150
+ });
151
+ });
152
+
153
+ // ---------------------------------------------------------------------------
154
+ // extractSection — frontmatter handling
155
+ // ---------------------------------------------------------------------------
156
+
157
+ describe('extractSection with frontmatter', () => {
158
+ test('extracts Objective section when frontmatter is present', () => {
159
+ const content = withFrontmatter(minimalSpec('Build the thing'), {
160
+ 'derives-from': 'done-auth',
161
+ });
162
+ const section = extractSection(content, 'Objective');
163
+ assert.ok(section !== null, 'Objective section should be found');
164
+ assert.ok(section.includes('Build the thing'));
165
+ });
166
+
167
+ test('extracts Requirements section with frontmatter', () => {
168
+ const content = withFrontmatter(fullSpec(), {
169
+ 'derives-from': 'done-auth',
170
+ });
171
+ const section = extractSection(content, 'Requirements');
172
+ assert.ok(section !== null);
173
+ assert.ok(section.includes('REQ-1'));
174
+ });
175
+
176
+ test('frontmatter content does not leak into extracted sections', () => {
177
+ const content = withFrontmatter(fullSpec(), {
178
+ 'derives-from': 'done-auth',
179
+ description: 'A spec about things',
180
+ });
181
+ const objective = extractSection(content, 'Objective');
182
+ assert.ok(objective !== null);
183
+ assert.ok(!objective.includes('derives-from'));
184
+ assert.ok(!objective.includes('done-auth'));
185
+ assert.ok(!objective.includes('description'));
186
+ });
187
+
188
+ test('returns null for non-existent section even with frontmatter', () => {
189
+ const content = withFrontmatter(minimalSpec(), {
190
+ 'derives-from': 'done-auth',
191
+ });
192
+ const section = extractSection(content, 'Nonexistent');
193
+ assert.equal(section, null);
194
+ });
195
+
196
+ test('extracts section using alias when frontmatter is present', () => {
197
+ const content = withFrontmatter(
198
+ '## Goal\nDo the thing\n\n## Requirements\n- REQ-1: stuff\n',
199
+ { 'derives-from': 'done-auth' }
200
+ );
201
+ // 'goal' is an alias for 'Objective'
202
+ const section = extractSection(content, 'Objective');
203
+ assert.ok(section !== null, 'Should find section via alias "Goal"');
204
+ assert.ok(section.includes('Do the thing'));
205
+ });
206
+ });
207
+
208
+ // ---------------------------------------------------------------------------
209
+ // Edge cases — frontmatter-like patterns inside body
210
+ // ---------------------------------------------------------------------------
211
+
212
+ describe('frontmatter edge cases', () => {
213
+ test('--- inside spec body (e.g. horizontal rule) does not break computeLayer', () => {
214
+ const content = [
215
+ '---',
216
+ 'derives-from: done-auth',
217
+ '---',
218
+ '',
219
+ '## Objective',
220
+ 'Build it',
221
+ '',
222
+ '---',
223
+ '',
224
+ '## Requirements',
225
+ '- REQ-1: Something',
226
+ ].join('\n');
227
+ // Should at least be L1 (has Objective + Requirements)
228
+ assert.ok(computeLayer(content) >= 1);
229
+ });
230
+
231
+ test('multiple derives-from fields in frontmatter do not affect layer', () => {
232
+ const content = withFrontmatter(fullSpec(), {
233
+ 'derives-from': 'done-auth, done-payments',
234
+ });
235
+ assert.equal(computeLayer(content), 3);
236
+ });
237
+ });
238
+
239
+ // ---------------------------------------------------------------------------
240
+ // parseFrontmatter — direct unit tests
241
+ // ---------------------------------------------------------------------------
242
+
243
+ describe('parseFrontmatter', () => {
244
+ test('parses key-value pairs and returns body without frontmatter', () => {
245
+ const content = [
246
+ '---',
247
+ 'derives-from: done-auth',
248
+ 'name: spec-lineage',
249
+ '---',
250
+ '',
251
+ '## Objective',
252
+ 'Build it',
253
+ ].join('\n');
254
+ const { frontmatter, body } = parseFrontmatter(content);
255
+ assert.equal(frontmatter['derives-from'], 'done-auth');
256
+ assert.equal(frontmatter['name'], 'spec-lineage');
257
+ assert.ok(body.includes('## Objective'));
258
+ assert.ok(body.includes('Build it'));
259
+ });
260
+
261
+ test('returns empty frontmatter and full body when no --- opener', () => {
262
+ const content = '## Objective\nBuild it\n';
263
+ const { frontmatter, body } = parseFrontmatter(content);
264
+ assert.deepEqual(frontmatter, {});
265
+ assert.equal(body, content);
266
+ });
267
+
268
+ test('returns empty frontmatter when opening --- exists but no closing ---', () => {
269
+ const content = '---\nderives-from: done-auth\n## Objective\nBuild it\n';
270
+ const { frontmatter, body } = parseFrontmatter(content);
271
+ assert.deepEqual(frontmatter, {});
272
+ assert.equal(body, content);
273
+ });
274
+
275
+ test('handles empty frontmatter block (--- immediately followed by ---)', () => {
276
+ const content = ['---', '---', '', '## Objective', 'Build it'].join('\n');
277
+ const { frontmatter, body } = parseFrontmatter(content);
278
+ assert.deepEqual(frontmatter, {});
279
+ assert.ok(body.includes('## Objective'));
280
+ });
281
+
282
+ test('trims whitespace from keys and values', () => {
283
+ const content = [
284
+ '---',
285
+ ' derives-from : done-auth ',
286
+ '---',
287
+ '',
288
+ '## Objective',
289
+ 'Build it',
290
+ ].join('\n');
291
+ const { frontmatter } = parseFrontmatter(content);
292
+ assert.equal(frontmatter['derives-from'], 'done-auth');
293
+ });
294
+
295
+ test('handles empty string input', () => {
296
+ const { frontmatter, body } = parseFrontmatter('');
297
+ assert.deepEqual(frontmatter, {});
298
+ assert.equal(body, '');
299
+ });
300
+
301
+ test('body does not include frontmatter delimiters', () => {
302
+ const content = withFrontmatter('## Objective\nBuild it', {
303
+ 'derives-from': 'done-auth',
304
+ });
305
+ const { body } = parseFrontmatter(content);
306
+ // Body should not start with ---
307
+ assert.ok(!body.trimStart().startsWith('---'));
308
+ });
309
+
310
+ test('handles value containing colons', () => {
311
+ const content = [
312
+ '---',
313
+ 'description: a spec: with colons: inside',
314
+ '---',
315
+ '',
316
+ 'body',
317
+ ].join('\n');
318
+ const { frontmatter } = parseFrontmatter(content);
319
+ assert.equal(frontmatter['description'], 'a spec: with colons: inside');
320
+ });
321
+ });
322
+
323
+ // ---------------------------------------------------------------------------
324
+ // derives-from validation in validateSpec
325
+ // ---------------------------------------------------------------------------
326
+
327
+ describe('derives-from validation', () => {
328
+ test('spec without derives-from produces no derives-from advisory', () => {
329
+ const content = fullSpec();
330
+ const result = validateSpec(content);
331
+ const derivesAdvisory = result.advisory.filter((m) => m.includes('derives-from'));
332
+ assert.equal(derivesAdvisory.length, 0);
333
+ });
334
+
335
+ test('derives-from with no specsDir skips reference check (no warning)', () => {
336
+ const content = withFrontmatter(fullSpec(), {
337
+ 'derives-from': 'nonexistent-spec',
338
+ });
339
+ // No specsDir passed — cannot verify, should not warn
340
+ const result = validateSpec(content);
341
+ const derivesAdvisory = result.advisory.filter((m) => m.includes('derives-from'));
342
+ assert.equal(derivesAdvisory.length, 0);
343
+ });
344
+
345
+ test('derives-from referencing missing spec emits advisory warning, not hard error', () => {
346
+ const content = withFrontmatter(fullSpec(), {
347
+ 'derives-from': 'nonexistent-spec',
348
+ });
349
+ // Use a real directory that won't contain spec files
350
+ const tmpDir = path.join(__dirname, '..', 'templates');
351
+ const result = validateSpec(content, { specsDir: tmpDir });
352
+ // Should be advisory, not hard
353
+ const derivesHard = result.hard.filter((m) => m.includes('derives-from'));
354
+ assert.equal(derivesHard.length, 0, 'missing derives-from reference must not be a hard error');
355
+ const derivesAdvisory = result.advisory.filter((m) => m.includes('derives-from'));
356
+ assert.ok(derivesAdvisory.length > 0, 'should emit advisory warning for missing reference');
357
+ });
358
+
359
+ test('advisory message includes the referenced spec name', () => {
360
+ const content = withFrontmatter(fullSpec(), {
361
+ 'derives-from': 'phantom-spec',
362
+ });
363
+ const tmpDir = path.join(__dirname, '..', 'templates');
364
+ const result = validateSpec(content, { specsDir: tmpDir });
365
+ const derivesAdvisory = result.advisory.filter((m) => m.includes('derives-from'));
366
+ assert.ok(
367
+ derivesAdvisory.some((m) => m.includes('phantom-spec')),
368
+ 'advisory should mention the referenced spec name'
369
+ );
370
+ });
371
+
372
+ test('derives-from referencing existing spec file produces no advisory', () => {
373
+ // Create a temp specs dir with a matching file
374
+ const tmpDir = fs.mkdtempSync(path.join(require('os').tmpdir(), 'spec-lint-test-'));
375
+ try {
376
+ fs.writeFileSync(path.join(tmpDir, 'done-auth.md'), '## Objective\nAuth\n');
377
+ const content = withFrontmatter(fullSpec(), {
378
+ 'derives-from': 'done-auth',
379
+ });
380
+ const result = validateSpec(content, { specsDir: tmpDir });
381
+ const derivesAdvisory = result.advisory.filter((m) => m.includes('derives-from'));
382
+ assert.equal(derivesAdvisory.length, 0, 'should not warn when reference exists');
383
+ } finally {
384
+ fs.rmSync(tmpDir, { recursive: true, force: true });
385
+ }
386
+ });
387
+
388
+ test('derives-from resolves done- prefixed files', () => {
389
+ // Reference "auth" but file is "done-auth.md" — should resolve
390
+ const tmpDir = fs.mkdtempSync(path.join(require('os').tmpdir(), 'spec-lint-test-'));
391
+ try {
392
+ fs.writeFileSync(path.join(tmpDir, 'done-auth.md'), '## Objective\nAuth\n');
393
+ const content = withFrontmatter(fullSpec(), {
394
+ 'derives-from': 'auth',
395
+ });
396
+ const result = validateSpec(content, { specsDir: tmpDir });
397
+ const derivesAdvisory = result.advisory.filter((m) => m.includes('derives-from'));
398
+ assert.equal(derivesAdvisory.length, 0, 'should resolve done- prefixed file');
399
+ } finally {
400
+ fs.rmSync(tmpDir, { recursive: true, force: true });
401
+ }
402
+ });
403
+
404
+ test('spec layer and hard errors are unaffected by derives-from presence', () => {
405
+ const withDerives = withFrontmatter(fullSpec(), { 'derives-from': 'done-auth' });
406
+ const without = fullSpec();
407
+ const resultWith = validateSpec(withDerives);
408
+ const resultWithout = validateSpec(without);
409
+ assert.equal(resultWith.layer, resultWithout.layer);
410
+ assert.deepEqual(resultWith.hard, resultWithout.hard);
411
+ });
412
+ });
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ // @hook-event: statusLine
2
3
  /**
3
4
  * deepflow statusline for Claude Code
4
5
  * Displays: update | model | project | context usage
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ // @hook-event: SubagentStop
2
3
  'use strict';
3
4
  const fs = require('fs');
4
5
  const path = require('path');
@@ -9,34 +10,53 @@ process.stdin.on('data', d => raw += d);
9
10
  process.stdin.on('end', () => {
10
11
  try {
11
12
  const event = JSON.parse(raw);
13
+ const { session_id, agent_type, agent_id, agent_transcript_path } = event;
12
14
 
13
- // Extract required fields from SubagentStop event
14
- const { session_id, agent_type, agent_id } = event;
15
+ // Parse subagent transcript to extract real model and token usage
16
+ let model = 'unknown';
17
+ let tokens_in = 0, tokens_out = 0, cache_read = 0, cache_creation = 0;
15
18
 
16
- // Map agent_type to model (case-sensitive)
17
- const MODEL_MAP = {
18
- 'reasoner': 'claude-opus-4-6',
19
- 'Explore': 'claude-haiku-4-5'
20
- };
21
- const model = MODEL_MAP[agent_type] ?? 'claude-sonnet-4-6';
19
+ if (agent_transcript_path && fs.existsSync(agent_transcript_path)) {
20
+ const lines = fs.readFileSync(agent_transcript_path, 'utf-8').split('\n');
21
+ for (const line of lines) {
22
+ const trimmed = line.trim();
23
+ if (!trimmed) continue;
24
+ try {
25
+ const evt = JSON.parse(trimmed);
26
+ const msg = evt.message || {};
27
+ const usage = msg.usage || evt.usage;
28
+ // Extract model from assistant messages
29
+ const m = msg.model || evt.model;
30
+ if (m && m !== 'unknown') model = m;
31
+ // Accumulate tokens
32
+ if (usage) {
33
+ tokens_in += usage.input_tokens || 0;
34
+ tokens_out += usage.output_tokens || 0;
35
+ cache_read += usage.cache_read_input_tokens || usage.cache_read_tokens || 0;
36
+ cache_creation += usage.cache_creation_input_tokens || usage.cache_creation_tokens || 0;
37
+ }
38
+ } catch { /* skip malformed lines */ }
39
+ }
40
+ }
22
41
 
23
- // Generate timestamp
24
- const timestamp = new Date().toISOString();
42
+ // Strip version suffix from model (e.g. claude-haiku-4-5-20251001 → claude-haiku-4-5)
43
+ model = model.replace(/-\d{8}$/, '').replace(/\[\d+[km]\]$/i, '');
25
44
 
26
- // Build registry entry
27
45
  const entry = {
28
46
  session_id,
29
47
  agent_type,
30
48
  agent_id,
31
49
  model,
32
- timestamp
50
+ tokens_in,
51
+ tokens_out,
52
+ cache_read,
53
+ cache_creation,
54
+ timestamp: new Date().toISOString()
33
55
  };
34
56
 
35
- // Append to registry file (fire-and-forget)
36
57
  const registryPath = path.join(os.homedir(), '.claude', 'subagent-sessions.jsonl');
37
58
  fs.appendFileSync(registryPath, JSON.stringify(entry) + '\n');
38
59
  } catch {
39
- // Exit 0 on any error (fail-open)
40
60
  process.exit(0);
41
61
  }
42
62
  });
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ // @hook-event: PostToolUse
2
3
  /**
3
4
  * deepflow tool usage logger
4
5
  * Logs every PostToolUse event to ~/.claude/tool-usage.jsonl for token instrumentation.
@@ -60,17 +61,34 @@ process.stdin.on('end', () => {
60
61
  const toolResponse = data.tool_response;
61
62
  const cwd = data.cwd || '';
62
63
 
64
+ let activeCommand = null;
65
+ try {
66
+ const markerPath = path.join(cwd || process.cwd(), '.deepflow', 'active-command.json');
67
+ const markerRaw = fs.readFileSync(markerPath, 'utf8');
68
+ activeCommand = JSON.parse(markerRaw).command || null;
69
+ } catch (_e) { /* no marker or unreadable — null */ }
70
+
71
+ // Extract a compact tool_input summary per tool type
72
+ const ti = data.tool_input || {};
73
+ let inputSummary = null;
74
+ if (toolName === 'Bash') inputSummary = ti.command || null;
75
+ else if (toolName === 'LSP') inputSummary = `${ti.operation || '?'}:${(ti.filePath || '').split('/').pop()}:${ti.line || '?'}`;
76
+ else if (toolName === 'Read') inputSummary = (ti.file_path || '').split('/').pop() + (ti.offset ? `:${ti.offset}-${ti.offset + (ti.limit || 0)}` : '');
77
+ else if (toolName === 'Grep') inputSummary = ti.pattern || null;
78
+ else if (toolName === 'Glob') inputSummary = ti.pattern || null;
79
+ else if (toolName === 'Agent') inputSummary = `${ti.subagent_type || '?'}/${ti.model || '?'}`;
80
+ else if (toolName === 'Edit' || toolName === 'Write') inputSummary = (ti.file_path || '').split('/').pop();
81
+
63
82
  const record = {
64
83
  timestamp: new Date().toISOString(),
65
84
  session_id: data.session_id || null,
66
85
  tool_name: toolName,
67
- command: (toolName === 'Bash' && data.tool_input && data.tool_input.command != null)
68
- ? data.tool_input.command
69
- : null,
86
+ input: inputSummary,
70
87
  output_size_est_tokens: Math.ceil(JSON.stringify(toolResponse).length / 4),
71
88
  project: cwd ? path.basename(cwd) : null,
72
89
  phase: inferPhase(cwd),
73
90
  task_id: extractTaskId(cwd),
91
+ active_command: activeCommand,
74
92
  };
75
93
 
76
94
  const logDir = path.dirname(TOOL_USAGE_LOG);