deepflow 0.1.90 → 0.1.91

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.
@@ -0,0 +1,105 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * deepflow snapshot guard
4
+ * PostToolUse hook: blocks Write/Edit to files listed in .deepflow/auto-snapshot.txt.
5
+ *
6
+ * REQ-3 AC-3: exit(1) to block the tool call when all conditions hold:
7
+ * 1. tool_name is Write or Edit
8
+ * 2. file_path matches an entry in .deepflow/auto-snapshot.txt
9
+ *
10
+ * Physical barrier independent of prompt instructions — prevents agents from
11
+ * modifying pre-existing test files that are part of the ratchet baseline.
12
+ *
13
+ * Exits silently (code 0) on parse errors or missing snapshot file — never breaks
14
+ * tool execution in non-deepflow projects.
15
+ */
16
+
17
+ 'use strict';
18
+
19
+ const fs = require('fs');
20
+ const path = require('path');
21
+
22
+ function loadSnapshotPaths(cwd) {
23
+ try {
24
+ const snapshotPath = path.join(cwd, '.deepflow', 'auto-snapshot.txt');
25
+ if (!fs.existsSync(snapshotPath)) {
26
+ return null;
27
+ }
28
+ const content = fs.readFileSync(snapshotPath, 'utf8');
29
+ const lines = content.split('\n')
30
+ .map(l => l.trim())
31
+ .filter(l => l.length > 0 && !l.startsWith('#'));
32
+ return lines;
33
+ } catch (_) {
34
+ return null;
35
+ }
36
+ }
37
+
38
+ function isSnapshotFile(filePath, snapshotPaths, cwd) {
39
+ // Normalize filePath: resolve relative to cwd if not absolute
40
+ const absFilePath = path.isAbsolute(filePath)
41
+ ? filePath
42
+ : path.resolve(cwd, filePath);
43
+
44
+ for (const entry of snapshotPaths) {
45
+ // Snapshot entries may be absolute or relative to cwd
46
+ const absEntry = path.isAbsolute(entry)
47
+ ? entry
48
+ : path.resolve(cwd, entry);
49
+
50
+ if (absFilePath === absEntry) {
51
+ return true;
52
+ }
53
+ }
54
+ return false;
55
+ }
56
+
57
+ let raw = '';
58
+ process.stdin.setEncoding('utf8');
59
+ process.stdin.on('data', chunk => { raw += chunk; });
60
+ process.stdin.on('end', () => {
61
+ try {
62
+ const data = JSON.parse(raw);
63
+ const toolName = data.tool_name || '';
64
+
65
+ // Only guard Write and Edit
66
+ if (toolName !== 'Write' && toolName !== 'Edit') {
67
+ process.exit(0);
68
+ }
69
+
70
+ const filePath = (data.tool_input && data.tool_input.file_path) || '';
71
+ const cwd = data.cwd || process.cwd();
72
+
73
+ if (!filePath) {
74
+ process.exit(0);
75
+ }
76
+
77
+ const snapshotPaths = loadSnapshotPaths(cwd);
78
+
79
+ // No snapshot file present — not a deepflow project or ratchet not initialized
80
+ if (snapshotPaths === null) {
81
+ process.exit(0);
82
+ }
83
+
84
+ // Empty snapshot — nothing to protect
85
+ if (snapshotPaths.length === 0) {
86
+ process.exit(0);
87
+ }
88
+
89
+ if (!isSnapshotFile(filePath, snapshotPaths, cwd)) {
90
+ process.exit(0);
91
+ }
92
+
93
+ // File is in the snapshot — block the write
94
+ console.error(
95
+ `[df-snapshot-guard] Blocked ${toolName} to "${filePath}" — this file is listed in ` +
96
+ `.deepflow/auto-snapshot.txt (ratchet baseline). ` +
97
+ `Pre-existing test files must not be modified by agents. ` +
98
+ `If you need to update this file, do so manually outside the autonomous loop.`
99
+ );
100
+ process.exit(1);
101
+ } catch (_e) {
102
+ // Parse or unexpected error — fail open so we never break non-deepflow projects
103
+ process.exit(0);
104
+ }
105
+ });
@@ -0,0 +1,506 @@
1
+ /**
2
+ * Tests for hooks/df-snapshot-guard.js
3
+ *
4
+ * Tests the PostToolUse hook that blocks Write/Edit to files listed in
5
+ * .deepflow/auto-snapshot.txt (ratchet baseline protection).
6
+ *
7
+ * Uses Node.js built-in node:test to avoid adding dependencies.
8
+ */
9
+
10
+ 'use strict';
11
+
12
+ const { test, describe, beforeEach, afterEach } = require('node:test');
13
+ const assert = require('node:assert/strict');
14
+ const fs = require('node:fs');
15
+ const path = require('node:path');
16
+ const os = require('node:os');
17
+ const { execFileSync } = require('node:child_process');
18
+
19
+ // ---------------------------------------------------------------------------
20
+ // Helpers
21
+ // ---------------------------------------------------------------------------
22
+
23
+ const HOOK_PATH = path.resolve(__dirname, 'df-snapshot-guard.js');
24
+
25
+ function makeTmpDir() {
26
+ return fs.mkdtempSync(path.join(os.tmpdir(), 'df-snapshot-guard-test-'));
27
+ }
28
+
29
+ function rmrf(dir) {
30
+ if (fs.existsSync(dir)) {
31
+ fs.rmSync(dir, { recursive: true, force: true });
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Run the snapshot guard hook as a child process with JSON piped to stdin.
37
+ * Returns { stdout, stderr, code }.
38
+ */
39
+ function runHook(input, { cwd } = {}) {
40
+ const json = JSON.stringify(input);
41
+ try {
42
+ const stdout = execFileSync(
43
+ process.execPath,
44
+ [HOOK_PATH],
45
+ {
46
+ input: json,
47
+ cwd: cwd || os.tmpdir(),
48
+ encoding: 'utf8',
49
+ timeout: 5000,
50
+ }
51
+ );
52
+ return { stdout, stderr: '', code: 0 };
53
+ } catch (err) {
54
+ return {
55
+ stdout: err.stdout || '',
56
+ stderr: err.stderr || '',
57
+ code: err.status ?? 1,
58
+ };
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Create .deepflow/auto-snapshot.txt in the given directory with the specified entries.
64
+ */
65
+ function writeSnapshot(dir, entries) {
66
+ const deepflowDir = path.join(dir, '.deepflow');
67
+ fs.mkdirSync(deepflowDir, { recursive: true });
68
+ fs.writeFileSync(path.join(deepflowDir, 'auto-snapshot.txt'), entries.join('\n'));
69
+ }
70
+
71
+ // ---------------------------------------------------------------------------
72
+ // 1. Pass-through cases (exit 0)
73
+ // ---------------------------------------------------------------------------
74
+
75
+ describe('df-snapshot-guard — pass-through (exit 0)', () => {
76
+ let tmpDir;
77
+
78
+ beforeEach(() => {
79
+ tmpDir = makeTmpDir();
80
+ });
81
+
82
+ afterEach(() => {
83
+ rmrf(tmpDir);
84
+ });
85
+
86
+ test('exits 0 for non-Write/Edit tools (e.g. Read)', () => {
87
+ writeSnapshot(tmpDir, ['src/app.js']);
88
+ const result = runHook({
89
+ tool_name: 'Read',
90
+ tool_input: { file_path: path.join(tmpDir, 'src/app.js') },
91
+ cwd: tmpDir,
92
+ });
93
+ assert.equal(result.code, 0);
94
+ });
95
+
96
+ test('exits 0 for Bash tool', () => {
97
+ writeSnapshot(tmpDir, ['src/app.js']);
98
+ const result = runHook({
99
+ tool_name: 'Bash',
100
+ tool_input: { command: 'echo hello' },
101
+ cwd: tmpDir,
102
+ });
103
+ assert.equal(result.code, 0);
104
+ });
105
+
106
+ test('exits 0 when snapshot file does not exist', () => {
107
+ // No .deepflow/auto-snapshot.txt created
108
+ const result = runHook({
109
+ tool_name: 'Write',
110
+ tool_input: { file_path: path.join(tmpDir, 'test.js') },
111
+ cwd: tmpDir,
112
+ });
113
+ assert.equal(result.code, 0);
114
+ });
115
+
116
+ test('exits 0 when snapshot file is empty', () => {
117
+ writeSnapshot(tmpDir, []);
118
+ const result = runHook({
119
+ tool_name: 'Write',
120
+ tool_input: { file_path: path.join(tmpDir, 'test.js') },
121
+ cwd: tmpDir,
122
+ });
123
+ assert.equal(result.code, 0);
124
+ });
125
+
126
+ test('exits 0 when snapshot contains only comments and blank lines', () => {
127
+ const deepflowDir = path.join(tmpDir, '.deepflow');
128
+ fs.mkdirSync(deepflowDir, { recursive: true });
129
+ fs.writeFileSync(
130
+ path.join(deepflowDir, 'auto-snapshot.txt'),
131
+ '# This is a comment\n\n# Another comment\n \n'
132
+ );
133
+ const result = runHook({
134
+ tool_name: 'Write',
135
+ tool_input: { file_path: path.join(tmpDir, 'test.js') },
136
+ cwd: tmpDir,
137
+ });
138
+ assert.equal(result.code, 0);
139
+ });
140
+
141
+ test('exits 0 when file_path does not match any snapshot entry', () => {
142
+ writeSnapshot(tmpDir, ['bin/install.test.js', 'test/integration.test.js']);
143
+ const result = runHook({
144
+ tool_name: 'Write',
145
+ tool_input: { file_path: path.join(tmpDir, 'src/new-file.js') },
146
+ cwd: tmpDir,
147
+ });
148
+ assert.equal(result.code, 0);
149
+ });
150
+
151
+ test('exits 0 when file_path is empty string', () => {
152
+ writeSnapshot(tmpDir, ['test.js']);
153
+ const result = runHook({
154
+ tool_name: 'Edit',
155
+ tool_input: { file_path: '' },
156
+ cwd: tmpDir,
157
+ });
158
+ assert.equal(result.code, 0);
159
+ });
160
+
161
+ test('exits 0 when tool_input is missing file_path', () => {
162
+ writeSnapshot(tmpDir, ['test.js']);
163
+ const result = runHook({
164
+ tool_name: 'Write',
165
+ tool_input: {},
166
+ cwd: tmpDir,
167
+ });
168
+ assert.equal(result.code, 0);
169
+ });
170
+
171
+ test('exits 0 on invalid JSON input (fail open)', () => {
172
+ // Send raw invalid JSON via child process
173
+ try {
174
+ execFileSync(process.execPath, [HOOK_PATH], {
175
+ input: 'not valid json{{{',
176
+ encoding: 'utf8',
177
+ timeout: 5000,
178
+ });
179
+ // exit 0 — pass
180
+ } catch (err) {
181
+ assert.fail(`Hook should exit 0 on parse error but got exit code ${err.status}`);
182
+ }
183
+ });
184
+
185
+ test('exits 0 when tool_name is missing', () => {
186
+ writeSnapshot(tmpDir, ['test.js']);
187
+ const result = runHook({
188
+ tool_input: { file_path: path.join(tmpDir, 'test.js') },
189
+ cwd: tmpDir,
190
+ });
191
+ assert.equal(result.code, 0);
192
+ });
193
+ });
194
+
195
+ // ---------------------------------------------------------------------------
196
+ // 2. Blocking cases (exit 1)
197
+ // ---------------------------------------------------------------------------
198
+
199
+ describe('df-snapshot-guard — blocks protected files (exit 1)', () => {
200
+ let tmpDir;
201
+
202
+ beforeEach(() => {
203
+ tmpDir = makeTmpDir();
204
+ });
205
+
206
+ afterEach(() => {
207
+ rmrf(tmpDir);
208
+ });
209
+
210
+ test('blocks Write to a file listed in snapshot (absolute path)', () => {
211
+ const protectedFile = path.join(tmpDir, 'bin', 'install.test.js');
212
+ writeSnapshot(tmpDir, [protectedFile]);
213
+ const result = runHook({
214
+ tool_name: 'Write',
215
+ tool_input: { file_path: protectedFile },
216
+ cwd: tmpDir,
217
+ });
218
+ assert.equal(result.code, 1);
219
+ assert.ok(result.stderr.includes('df-snapshot-guard'));
220
+ assert.ok(result.stderr.includes('Blocked'));
221
+ });
222
+
223
+ test('blocks Edit to a file listed in snapshot (absolute path)', () => {
224
+ const protectedFile = path.join(tmpDir, 'test', 'integration.test.js');
225
+ writeSnapshot(tmpDir, [protectedFile]);
226
+ const result = runHook({
227
+ tool_name: 'Edit',
228
+ tool_input: { file_path: protectedFile },
229
+ cwd: tmpDir,
230
+ });
231
+ assert.equal(result.code, 1);
232
+ assert.ok(result.stderr.includes('Blocked'));
233
+ });
234
+
235
+ test('blocks Write to a file listed as relative path in snapshot', () => {
236
+ writeSnapshot(tmpDir, ['bin/install.test.js']);
237
+ const result = runHook({
238
+ tool_name: 'Write',
239
+ tool_input: { file_path: path.join(tmpDir, 'bin', 'install.test.js') },
240
+ cwd: tmpDir,
241
+ });
242
+ assert.equal(result.code, 1);
243
+ });
244
+
245
+ test('blocks when file_path is relative and snapshot entry is relative', () => {
246
+ writeSnapshot(tmpDir, ['test/foo.test.js']);
247
+ const result = runHook({
248
+ tool_name: 'Edit',
249
+ tool_input: { file_path: 'test/foo.test.js' },
250
+ cwd: tmpDir,
251
+ });
252
+ assert.equal(result.code, 1);
253
+ });
254
+
255
+ test('blocks when file_path is absolute and snapshot entry is relative', () => {
256
+ writeSnapshot(tmpDir, ['src/helper.test.js']);
257
+ const absPath = path.join(tmpDir, 'src', 'helper.test.js');
258
+ const result = runHook({
259
+ tool_name: 'Write',
260
+ tool_input: { file_path: absPath },
261
+ cwd: tmpDir,
262
+ });
263
+ assert.equal(result.code, 1);
264
+ });
265
+
266
+ test('blocks when file_path is relative and snapshot entry is absolute', () => {
267
+ const absEntry = path.join(tmpDir, 'lib', 'core.test.js');
268
+ writeSnapshot(tmpDir, [absEntry]);
269
+ const result = runHook({
270
+ tool_name: 'Edit',
271
+ tool_input: { file_path: 'lib/core.test.js' },
272
+ cwd: tmpDir,
273
+ });
274
+ assert.equal(result.code, 1);
275
+ });
276
+
277
+ test('stderr message includes file path and ratchet explanation', () => {
278
+ writeSnapshot(tmpDir, ['test/unit.test.js']);
279
+ const result = runHook({
280
+ tool_name: 'Write',
281
+ tool_input: { file_path: path.join(tmpDir, 'test', 'unit.test.js') },
282
+ cwd: tmpDir,
283
+ });
284
+ assert.equal(result.code, 1);
285
+ assert.ok(result.stderr.includes('ratchet baseline'));
286
+ assert.ok(result.stderr.includes('auto-snapshot.txt'));
287
+ });
288
+
289
+ test('blocks only matching file among multiple snapshot entries', () => {
290
+ writeSnapshot(tmpDir, [
291
+ 'test/a.test.js',
292
+ 'test/b.test.js',
293
+ 'test/c.test.js',
294
+ ]);
295
+
296
+ // Writing to b.test.js should be blocked
297
+ const resultBlocked = runHook({
298
+ tool_name: 'Write',
299
+ tool_input: { file_path: path.join(tmpDir, 'test', 'b.test.js') },
300
+ cwd: tmpDir,
301
+ });
302
+ assert.equal(resultBlocked.code, 1);
303
+
304
+ // Writing to d.test.js should pass through
305
+ const resultAllowed = runHook({
306
+ tool_name: 'Write',
307
+ tool_input: { file_path: path.join(tmpDir, 'test', 'd.test.js') },
308
+ cwd: tmpDir,
309
+ });
310
+ assert.equal(resultAllowed.code, 0);
311
+ });
312
+ });
313
+
314
+ // ---------------------------------------------------------------------------
315
+ // 3. Edge cases
316
+ // ---------------------------------------------------------------------------
317
+
318
+ describe('df-snapshot-guard — edge cases', () => {
319
+ let tmpDir;
320
+
321
+ beforeEach(() => {
322
+ tmpDir = makeTmpDir();
323
+ });
324
+
325
+ afterEach(() => {
326
+ rmrf(tmpDir);
327
+ });
328
+
329
+ test('snapshot with comment lines ignores comments', () => {
330
+ const deepflowDir = path.join(tmpDir, '.deepflow');
331
+ fs.mkdirSync(deepflowDir, { recursive: true });
332
+ fs.writeFileSync(
333
+ path.join(deepflowDir, 'auto-snapshot.txt'),
334
+ '# Header comment\ntest/real.test.js\n# Another comment\n'
335
+ );
336
+ const result = runHook({
337
+ tool_name: 'Write',
338
+ tool_input: { file_path: path.join(tmpDir, 'test', 'real.test.js') },
339
+ cwd: tmpDir,
340
+ });
341
+ assert.equal(result.code, 1);
342
+ });
343
+
344
+ test('snapshot entries with leading/trailing whitespace are trimmed', () => {
345
+ const deepflowDir = path.join(tmpDir, '.deepflow');
346
+ fs.mkdirSync(deepflowDir, { recursive: true });
347
+ fs.writeFileSync(
348
+ path.join(deepflowDir, 'auto-snapshot.txt'),
349
+ ' test/spaced.test.js \n'
350
+ );
351
+ const result = runHook({
352
+ tool_name: 'Edit',
353
+ tool_input: { file_path: path.join(tmpDir, 'test', 'spaced.test.js') },
354
+ cwd: tmpDir,
355
+ });
356
+ assert.equal(result.code, 1);
357
+ });
358
+
359
+ test('uses process.cwd() when cwd is not in input', () => {
360
+ // When cwd is not provided in JSON, hook falls back to process.cwd()
361
+ // We can't easily control process.cwd() in the child, but we can verify
362
+ // it doesn't crash — the snapshot won't exist in the child's cwd so exit 0
363
+ const result = runHook({
364
+ tool_name: 'Write',
365
+ tool_input: { file_path: '/some/random/file.js' },
366
+ });
367
+ assert.equal(result.code, 0);
368
+ });
369
+
370
+ test('handles snapshot file with only one entry', () => {
371
+ writeSnapshot(tmpDir, ['single.js']);
372
+ const blocked = runHook({
373
+ tool_name: 'Write',
374
+ tool_input: { file_path: path.join(tmpDir, 'single.js') },
375
+ cwd: tmpDir,
376
+ });
377
+ assert.equal(blocked.code, 1);
378
+
379
+ const allowed = runHook({
380
+ tool_name: 'Write',
381
+ tool_input: { file_path: path.join(tmpDir, 'other.js') },
382
+ cwd: tmpDir,
383
+ });
384
+ assert.equal(allowed.code, 0);
385
+ });
386
+
387
+ test('does not block similarly named but different files', () => {
388
+ writeSnapshot(tmpDir, ['test/foo.test.js']);
389
+ // foo.test.jsx is NOT the same as foo.test.js
390
+ const result = runHook({
391
+ tool_name: 'Write',
392
+ tool_input: { file_path: path.join(tmpDir, 'test', 'foo.test.jsx') },
393
+ cwd: tmpDir,
394
+ });
395
+ assert.equal(result.code, 0);
396
+ });
397
+
398
+ test('does not block a parent directory of a snapshot entry', () => {
399
+ writeSnapshot(tmpDir, ['test/sub/deep.test.js']);
400
+ const result = runHook({
401
+ tool_name: 'Write',
402
+ tool_input: { file_path: path.join(tmpDir, 'test', 'sub') },
403
+ cwd: tmpDir,
404
+ });
405
+ assert.equal(result.code, 0);
406
+ });
407
+ });
408
+
409
+ // ---------------------------------------------------------------------------
410
+ // 4. install.js integration — snapshot guard hook registration
411
+ // ---------------------------------------------------------------------------
412
+
413
+ describe('install.js — snapshot guard hook registration', () => {
414
+ const installSrc = fs.readFileSync(
415
+ path.resolve(__dirname, '..', 'bin', 'install.js'),
416
+ 'utf8'
417
+ );
418
+
419
+ test('defines snapshotGuardCmd variable', () => {
420
+ assert.ok(
421
+ installSrc.includes('snapshotGuardCmd'),
422
+ 'install.js should define snapshotGuardCmd variable'
423
+ );
424
+ });
425
+
426
+ test('snapshotGuardCmd references df-snapshot-guard.js', () => {
427
+ assert.match(
428
+ installSrc,
429
+ /snapshotGuardCmd\s*=\s*`node.*df-snapshot-guard\.js/,
430
+ 'snapshotGuardCmd should reference df-snapshot-guard.js'
431
+ );
432
+ });
433
+
434
+ test('pushes snapshot guard to PostToolUse hooks', () => {
435
+ // Verify there is a .push() call that includes snapshotGuardCmd
436
+ assert.match(
437
+ installSrc,
438
+ /PostToolUse\.push\(\{[\s\S]*?snapshotGuardCmd[\s\S]*?\}\)/,
439
+ 'install.js should push snapshotGuardCmd to PostToolUse'
440
+ );
441
+ });
442
+
443
+ test('PostToolUse filter includes df-snapshot-guard removal', () => {
444
+ // The filter should clean up existing snapshot guard hooks before re-adding
445
+ assert.ok(
446
+ installSrc.includes("df-snapshot-guard"),
447
+ 'PostToolUse filter should reference df-snapshot-guard for cleanup'
448
+ );
449
+ });
450
+
451
+ test('uninstall toRemove includes df-snapshot-guard.js', () => {
452
+ assert.ok(
453
+ installSrc.includes("'hooks/df-snapshot-guard.js'"),
454
+ 'Uninstall toRemove should include hooks/df-snapshot-guard.js'
455
+ );
456
+ });
457
+
458
+ test('uninstall PostToolUse filter removes df-snapshot-guard', () => {
459
+ // Find the uninstall section's PostToolUse filter
460
+ // It should include df-snapshot-guard in the filter pattern
461
+ const uninstallSection = installSrc.slice(installSrc.indexOf('async function uninstall'));
462
+ assert.ok(
463
+ uninstallSection.includes('df-snapshot-guard'),
464
+ 'Uninstall PostToolUse filter should include df-snapshot-guard'
465
+ );
466
+ });
467
+
468
+ test('PostToolUse cleanup removes snapshot guard and keeps custom hooks', () => {
469
+ const postToolUse = [
470
+ { hooks: [{ type: 'command', command: 'node /home/.claude/hooks/df-tool-usage.js' }] },
471
+ { hooks: [{ type: 'command', command: 'node /home/.claude/hooks/df-snapshot-guard.js' }] },
472
+ { hooks: [{ type: 'command', command: 'node /usr/local/my-custom-hook.js' }] },
473
+ ];
474
+
475
+ const filtered = postToolUse.filter(hook => {
476
+ const cmd = hook.hooks?.[0]?.command || '';
477
+ return !cmd.includes('df-tool-usage') &&
478
+ !cmd.includes('df-execution-history') &&
479
+ !cmd.includes('df-worktree-guard') &&
480
+ !cmd.includes('df-snapshot-guard') &&
481
+ !cmd.includes('df-invariant-check');
482
+ });
483
+
484
+ assert.equal(filtered.length, 1);
485
+ assert.ok(filtered[0].hooks[0].command.includes('my-custom-hook.js'));
486
+ });
487
+
488
+ test('filterSessionStart does NOT remove snapshot guard (it is PostToolUse only)', () => {
489
+ // Reproduce the SessionStart filter logic — snapshot guard should not appear
490
+ function filterSessionStart(hooks) {
491
+ return hooks.filter(hook => {
492
+ const cmd = hook.hooks?.[0]?.command || '';
493
+ return !cmd.includes('df-check-update') &&
494
+ !cmd.includes('df-consolidation-check') &&
495
+ !cmd.includes('df-quota-logger');
496
+ });
497
+ }
498
+
499
+ const hooks = [
500
+ { hooks: [{ type: 'command', command: 'node /home/.claude/hooks/df-snapshot-guard.js' }] },
501
+ ];
502
+
503
+ const filtered = filterSessionStart(hooks);
504
+ assert.equal(filtered.length, 1, 'SessionStart filter should NOT remove snapshot guard hooks');
505
+ });
506
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deepflow",
3
- "version": "0.1.90",
3
+ "version": "0.1.91",
4
4
  "description": "Doing reveals what thinking can't predict — spec-driven iterative development for Claude Code",
5
5
  "keywords": [
6
6
  "claude",