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
|
@@ -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
|
+
});
|
package/hooks/df-statusline.js
CHANGED
|
@@ -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
|
-
//
|
|
14
|
-
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
//
|
|
24
|
-
|
|
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
|
-
|
|
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
|
});
|
package/hooks/df-tool-usage.js
CHANGED
|
@@ -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
|
-
|
|
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);
|