deepflow 0.1.91 → 0.1.92

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,556 @@
1
+ /**
2
+ * Tests for bin/wave-runner.js — DAG-based wave grouping of PLAN.md tasks.
3
+ *
4
+ * Tests cover:
5
+ * 1. PLAN.md parsing: task extraction, dependency parsing, completed-task skipping
6
+ * 2. Wave grouping: correct topological ordering via Kahn's algorithm
7
+ * 3. --recalc --failed: stuck tasks and transitive dependents excluded
8
+ * 4. Edge cases: no tasks, circular deps, all completed, single task
9
+ * 5. CLI arg parsing
10
+ * 6. Output formatting
11
+ *
12
+ * Uses Node.js built-in node:test to avoid adding dependencies.
13
+ */
14
+
15
+ 'use strict';
16
+
17
+ const { test, describe } = require('node:test');
18
+ const assert = require('node:assert/strict');
19
+ const fs = require('node:fs');
20
+ const path = require('node:path');
21
+ const os = require('node:os');
22
+ const { execFileSync } = require('node:child_process');
23
+
24
+ // ---------------------------------------------------------------------------
25
+ // Helpers
26
+ // ---------------------------------------------------------------------------
27
+
28
+ const WAVE_RUNNER_PATH = path.resolve(__dirname, 'wave-runner.js');
29
+ const WAVE_RUNNER_SRC = fs.readFileSync(WAVE_RUNNER_PATH, 'utf8');
30
+
31
+ function makeTmpDir() {
32
+ return fs.mkdtempSync(path.join(os.tmpdir(), 'df-wave-runner-test-'));
33
+ }
34
+
35
+ function rmrf(dir) {
36
+ if (fs.existsSync(dir)) {
37
+ fs.rmSync(dir, { recursive: true, force: true });
38
+ }
39
+ }
40
+
41
+ // ---------------------------------------------------------------------------
42
+ // Extract pure functions from wave-runner.js source for unit testing.
43
+ // ---------------------------------------------------------------------------
44
+
45
+ const extractedFns = (() => {
46
+ const modifiedSrc = WAVE_RUNNER_SRC
47
+ .replace(/^main\(\);?\s*$/m, '')
48
+ .replace(/^#!.*$/m, '');
49
+
50
+ const wrapped = `
51
+ ${modifiedSrc}
52
+ return { parseArgs, parsePlan, buildWaves, formatWaves };
53
+ `;
54
+
55
+ const factory = new Function('require', 'process', '__dirname', '__filename', 'module', 'exports', wrapped);
56
+ return factory(require, process, __dirname, __filename, module, exports);
57
+ })();
58
+
59
+ const { parseArgs, parsePlan, buildWaves, formatWaves } = extractedFns;
60
+
61
+ // ---------------------------------------------------------------------------
62
+ // CLI runner helper
63
+ // ---------------------------------------------------------------------------
64
+
65
+ function runWaveRunner(args = [], { cwd } = {}) {
66
+ try {
67
+ const stdout = execFileSync(
68
+ process.execPath,
69
+ [WAVE_RUNNER_PATH, ...args],
70
+ {
71
+ cwd: cwd || os.tmpdir(),
72
+ encoding: 'utf8',
73
+ }
74
+ );
75
+ return { stdout, stderr: '', code: 0 };
76
+ } catch (err) {
77
+ return {
78
+ stdout: err.stdout || '',
79
+ stderr: err.stderr || '',
80
+ code: err.status ?? 1,
81
+ };
82
+ }
83
+ }
84
+
85
+ // ---------------------------------------------------------------------------
86
+ // 1. parseArgs
87
+ // ---------------------------------------------------------------------------
88
+
89
+ describe('parseArgs — CLI argument parsing', () => {
90
+ test('defaults to PLAN.md with no recalc', () => {
91
+ const args = parseArgs(['node', 'wave-runner.js']);
92
+ assert.equal(args.plan, 'PLAN.md');
93
+ assert.equal(args.recalc, false);
94
+ assert.deepEqual(args.failed, []);
95
+ });
96
+
97
+ test('--plan overrides default path', () => {
98
+ const args = parseArgs(['node', 'wave-runner.js', '--plan', 'custom/PLAN.md']);
99
+ assert.equal(args.plan, 'custom/PLAN.md');
100
+ });
101
+
102
+ test('--recalc flag enables recalc mode', () => {
103
+ const args = parseArgs(['node', 'wave-runner.js', '--recalc']);
104
+ assert.equal(args.recalc, true);
105
+ });
106
+
107
+ test('--failed accepts comma-separated task IDs', () => {
108
+ const args = parseArgs(['node', 'wave-runner.js', '--recalc', '--failed', 'T3,T5,T7']);
109
+ assert.deepEqual(args.failed, ['T3', 'T5', 'T7']);
110
+ });
111
+
112
+ test('--failed accepts multiple --failed flags', () => {
113
+ const args = parseArgs(['node', 'wave-runner.js', '--recalc', '--failed', 'T3', '--failed', 'T5']);
114
+ assert.deepEqual(args.failed, ['T3', 'T5']);
115
+ });
116
+ });
117
+
118
+ // ---------------------------------------------------------------------------
119
+ // 2. parsePlan — PLAN.md task extraction
120
+ // ---------------------------------------------------------------------------
121
+
122
+ describe('parsePlan — PLAN.md parsing', () => {
123
+ test('extracts pending tasks with IDs and descriptions', () => {
124
+ const text = `
125
+ ## Tasks
126
+ - [ ] **T1**: Build the parser
127
+ - [ ] **T2**: Wire up CLI
128
+ `;
129
+ const tasks = parsePlan(text);
130
+ assert.equal(tasks.length, 2);
131
+ assert.equal(tasks[0].id, 'T1');
132
+ assert.equal(tasks[0].description, 'Build the parser');
133
+ assert.equal(tasks[1].id, 'T2');
134
+ assert.equal(tasks[1].description, 'Wire up CLI');
135
+ });
136
+
137
+ test('extracts blocked-by dependencies', () => {
138
+ const text = `
139
+ - [ ] **T1**: First task
140
+ - [ ] **T2**: Second task
141
+ - Blocked by: T1
142
+ - [ ] **T3**: Third task
143
+ - Blocked by: T1, T2
144
+ `;
145
+ const tasks = parsePlan(text);
146
+ assert.equal(tasks.length, 3);
147
+ assert.deepEqual(tasks[0].blockedBy, []);
148
+ assert.deepEqual(tasks[1].blockedBy, ['T1']);
149
+ assert.deepEqual(tasks[2].blockedBy, ['T1', 'T2']);
150
+ });
151
+
152
+ test('skips completed tasks (checked checkbox)', () => {
153
+ const text = `
154
+ - [x] **T1**: Done task
155
+ - [ ] **T2**: Pending task
156
+ `;
157
+ const tasks = parsePlan(text);
158
+ assert.equal(tasks.length, 1);
159
+ assert.equal(tasks[0].id, 'T2');
160
+ });
161
+
162
+ test('handles tasks with tag brackets', () => {
163
+ const text = `
164
+ - [ ] **T5** [SPIKE]: Investigate API design
165
+ `;
166
+ const tasks = parsePlan(text);
167
+ assert.equal(tasks.length, 1);
168
+ assert.equal(tasks[0].id, 'T5');
169
+ assert.equal(tasks[0].description, 'Investigate API design');
170
+ });
171
+
172
+ test('returns empty array for empty input', () => {
173
+ const tasks = parsePlan('');
174
+ assert.equal(tasks.length, 0);
175
+ });
176
+
177
+ test('returns empty array when all tasks are completed', () => {
178
+ const text = `
179
+ - [x] **T1**: Done
180
+ - [x] **T2**: Also done
181
+ `;
182
+ const tasks = parsePlan(text);
183
+ assert.equal(tasks.length, 0);
184
+ });
185
+
186
+ test('does not attach blocked-by annotations from completed tasks to next pending task', () => {
187
+ const text = `
188
+ - [x] **T1**: Completed
189
+ - Blocked by: T0
190
+ - [ ] **T2**: Pending with no deps
191
+ `;
192
+ const tasks = parsePlan(text);
193
+ assert.equal(tasks.length, 1);
194
+ assert.equal(tasks[0].id, 'T2');
195
+ assert.deepEqual(tasks[0].blockedBy, []);
196
+ });
197
+
198
+ test('handles input with no task lines', () => {
199
+ const text = `
200
+ # My Plan
201
+ Some descriptive text without any task checkboxes.
202
+ `;
203
+ const tasks = parsePlan(text);
204
+ assert.equal(tasks.length, 0);
205
+ });
206
+ });
207
+
208
+ // ---------------------------------------------------------------------------
209
+ // 3. buildWaves — DAG to wave grouping (AC-1, AC-2)
210
+ // ---------------------------------------------------------------------------
211
+
212
+ describe('buildWaves — topological wave grouping', () => {
213
+ test('independent tasks all go in wave 1', () => {
214
+ const tasks = [
215
+ { id: 'T1', num: 1, description: 'A', blockedBy: [] },
216
+ { id: 'T2', num: 2, description: 'B', blockedBy: [] },
217
+ { id: 'T3', num: 3, description: 'C', blockedBy: [] },
218
+ ];
219
+ const waves = buildWaves(tasks, new Set());
220
+ assert.equal(waves.length, 1);
221
+ assert.equal(waves[0].length, 3);
222
+ });
223
+
224
+ test('linear chain produces one task per wave', () => {
225
+ // T1 → T2 → T3
226
+ const tasks = [
227
+ { id: 'T1', num: 1, description: 'First', blockedBy: [] },
228
+ { id: 'T2', num: 2, description: 'Second', blockedBy: ['T1'] },
229
+ { id: 'T3', num: 3, description: 'Third', blockedBy: ['T2'] },
230
+ ];
231
+ const waves = buildWaves(tasks, new Set());
232
+ assert.equal(waves.length, 3);
233
+ assert.equal(waves[0][0].id, 'T1');
234
+ assert.equal(waves[1][0].id, 'T2');
235
+ assert.equal(waves[2][0].id, 'T3');
236
+ });
237
+
238
+ test('diamond DAG groups correctly', () => {
239
+ // T1
240
+ // / \
241
+ // T2 T3
242
+ // \ /
243
+ // T4
244
+ const tasks = [
245
+ { id: 'T1', num: 1, description: 'Root', blockedBy: [] },
246
+ { id: 'T2', num: 2, description: 'Left', blockedBy: ['T1'] },
247
+ { id: 'T3', num: 3, description: 'Right', blockedBy: ['T1'] },
248
+ { id: 'T4', num: 4, description: 'Join', blockedBy: ['T2', 'T3'] },
249
+ ];
250
+ const waves = buildWaves(tasks, new Set());
251
+ assert.equal(waves.length, 3);
252
+ assert.deepEqual(waves[0].map(t => t.id), ['T1']);
253
+ assert.deepEqual(waves[1].map(t => t.id), ['T2', 'T3']);
254
+ assert.deepEqual(waves[2].map(t => t.id), ['T4']);
255
+ });
256
+
257
+ test('tasks appear after all their blocking dependencies waves', () => {
258
+ // T1 (wave 1), T2 blocked by T1 (wave 2), T3 blocked by T1 (wave 2),
259
+ // T4 blocked by T2 and T3 (wave 3)
260
+ const tasks = [
261
+ { id: 'T1', num: 1, description: '', blockedBy: [] },
262
+ { id: 'T2', num: 2, description: '', blockedBy: ['T1'] },
263
+ { id: 'T3', num: 3, description: '', blockedBy: ['T1'] },
264
+ { id: 'T4', num: 4, description: '', blockedBy: ['T2', 'T3'] },
265
+ ];
266
+ const waves = buildWaves(tasks, new Set());
267
+
268
+ // Build a map of task → wave index
269
+ const taskWave = new Map();
270
+ waves.forEach((wave, wi) => {
271
+ for (const t of wave) taskWave.set(t.id, wi);
272
+ });
273
+
274
+ // T4 must be in a wave strictly after T2 and T3
275
+ assert.ok(taskWave.get('T4') > taskWave.get('T2'));
276
+ assert.ok(taskWave.get('T4') > taskWave.get('T3'));
277
+ // T2 and T3 must be after T1
278
+ assert.ok(taskWave.get('T2') > taskWave.get('T1'));
279
+ assert.ok(taskWave.get('T3') > taskWave.get('T1'));
280
+ });
281
+
282
+ test('deps on completed (non-pending) tasks are treated as satisfied', () => {
283
+ // T2 depends on T1, but T1 is not in the pending list (already completed)
284
+ const tasks = [
285
+ { id: 'T2', num: 2, description: 'Depends on completed T1', blockedBy: ['T1'] },
286
+ { id: 'T3', num: 3, description: 'Independent', blockedBy: [] },
287
+ ];
288
+ const waves = buildWaves(tasks, new Set());
289
+ // Both should be in wave 1 since T1 is not pending
290
+ assert.equal(waves.length, 1);
291
+ assert.equal(waves[0].length, 2);
292
+ });
293
+
294
+ test('tasks are sorted by task number within each wave', () => {
295
+ const tasks = [
296
+ { id: 'T10', num: 10, description: '', blockedBy: [] },
297
+ { id: 'T3', num: 3, description: '', blockedBy: [] },
298
+ { id: 'T7', num: 7, description: '', blockedBy: [] },
299
+ { id: 'T1', num: 1, description: '', blockedBy: [] },
300
+ ];
301
+ const waves = buildWaves(tasks, new Set());
302
+ assert.equal(waves.length, 1);
303
+ assert.deepEqual(waves[0].map(t => t.id), ['T1', 'T3', 'T7', 'T10']);
304
+ });
305
+
306
+ test('empty task list produces no waves', () => {
307
+ const waves = buildWaves([], new Set());
308
+ assert.equal(waves.length, 0);
309
+ });
310
+
311
+ test('circular dependency results in tasks not appearing in any wave', () => {
312
+ // T1 blocked by T2, T2 blocked by T1 — neither can ever be resolved
313
+ const tasks = [
314
+ { id: 'T1', num: 1, description: 'A', blockedBy: ['T2'] },
315
+ { id: 'T2', num: 2, description: 'B', blockedBy: ['T1'] },
316
+ ];
317
+ const waves = buildWaves(tasks, new Set());
318
+ // Kahn's algorithm: tasks in a cycle have in-degree > 0 forever, so no waves
319
+ assert.equal(waves.length, 0);
320
+ });
321
+
322
+ test('partial circular dependency: non-cyclic tasks still appear', () => {
323
+ // T1 and T2 form a cycle, but T3 is independent
324
+ const tasks = [
325
+ { id: 'T1', num: 1, description: '', blockedBy: ['T2'] },
326
+ { id: 'T2', num: 2, description: '', blockedBy: ['T1'] },
327
+ { id: 'T3', num: 3, description: '', blockedBy: [] },
328
+ ];
329
+ const waves = buildWaves(tasks, new Set());
330
+ assert.equal(waves.length, 1);
331
+ assert.deepEqual(waves[0].map(t => t.id), ['T3']);
332
+ });
333
+ });
334
+
335
+ // ---------------------------------------------------------------------------
336
+ // 4. buildWaves with stuckIds — --recalc --failed (AC-3)
337
+ // ---------------------------------------------------------------------------
338
+
339
+ describe('buildWaves with stuckIds — recalc/failed mode', () => {
340
+ test('stuck task is excluded from all waves', () => {
341
+ const tasks = [
342
+ { id: 'T1', num: 1, description: '', blockedBy: [] },
343
+ { id: 'T2', num: 2, description: '', blockedBy: [] },
344
+ ];
345
+ const waves = buildWaves(tasks, new Set(['T1']));
346
+ assert.equal(waves.length, 1);
347
+ assert.deepEqual(waves[0].map(t => t.id), ['T2']);
348
+ });
349
+
350
+ test('transitive dependents of stuck task are also excluded', () => {
351
+ // T1 → T2 → T3, T4 independent
352
+ const tasks = [
353
+ { id: 'T1', num: 1, description: '', blockedBy: [] },
354
+ { id: 'T2', num: 2, description: '', blockedBy: ['T1'] },
355
+ { id: 'T3', num: 3, description: '', blockedBy: ['T2'] },
356
+ { id: 'T4', num: 4, description: '', blockedBy: [] },
357
+ ];
358
+ // T1 failed → T2 and T3 (transitive dependents) should also be excluded
359
+ const waves = buildWaves(tasks, new Set(['T1']));
360
+ assert.equal(waves.length, 1);
361
+ assert.deepEqual(waves[0].map(t => t.id), ['T4']);
362
+ });
363
+
364
+ test('multiple stuck tasks exclude all their dependents', () => {
365
+ const tasks = [
366
+ { id: 'T1', num: 1, description: '', blockedBy: [] },
367
+ { id: 'T2', num: 2, description: '', blockedBy: [] },
368
+ { id: 'T3', num: 3, description: '', blockedBy: ['T1'] },
369
+ { id: 'T4', num: 4, description: '', blockedBy: ['T2'] },
370
+ { id: 'T5', num: 5, description: '', blockedBy: [] },
371
+ ];
372
+ const waves = buildWaves(tasks, new Set(['T1', 'T2']));
373
+ // T1, T2 stuck → T3, T4 excluded → only T5 remains
374
+ assert.equal(waves.length, 1);
375
+ assert.deepEqual(waves[0].map(t => t.id), ['T5']);
376
+ });
377
+
378
+ test('stuck task that does not exist in task list is ignored', () => {
379
+ const tasks = [
380
+ { id: 'T1', num: 1, description: '', blockedBy: [] },
381
+ ];
382
+ const waves = buildWaves(tasks, new Set(['T99']));
383
+ assert.equal(waves.length, 1);
384
+ assert.deepEqual(waves[0].map(t => t.id), ['T1']);
385
+ });
386
+
387
+ test('all tasks stuck results in no waves', () => {
388
+ const tasks = [
389
+ { id: 'T1', num: 1, description: '', blockedBy: [] },
390
+ { id: 'T2', num: 2, description: '', blockedBy: [] },
391
+ ];
392
+ const waves = buildWaves(tasks, new Set(['T1', 'T2']));
393
+ assert.equal(waves.length, 0);
394
+ });
395
+ });
396
+
397
+ // ---------------------------------------------------------------------------
398
+ // 5. formatWaves — output formatting
399
+ // ---------------------------------------------------------------------------
400
+
401
+ describe('formatWaves — output formatting', () => {
402
+ test('formats waves with task IDs and descriptions', () => {
403
+ const waves = [
404
+ [{ id: 'T1', description: 'Build parser' }, { id: 'T4', description: 'Setup CI' }],
405
+ [{ id: 'T2', description: 'Wire CLI' }],
406
+ ];
407
+ const output = formatWaves(waves);
408
+ assert.equal(output, 'Wave 1: T1 — Build parser, T4 — Setup CI\nWave 2: T2 — Wire CLI');
409
+ });
410
+
411
+ test('handles tasks with empty descriptions', () => {
412
+ const waves = [
413
+ [{ id: 'T1', description: '' }],
414
+ ];
415
+ const output = formatWaves(waves);
416
+ assert.equal(output, 'Wave 1: T1');
417
+ });
418
+
419
+ test('returns "(no pending tasks)" for empty waves', () => {
420
+ const output = formatWaves([]);
421
+ assert.equal(output, '(no pending tasks)');
422
+ });
423
+ });
424
+
425
+ // ---------------------------------------------------------------------------
426
+ // 6. Integration: parsePlan → buildWaves end-to-end
427
+ // ---------------------------------------------------------------------------
428
+
429
+ describe('Integration — parsePlan → buildWaves', () => {
430
+ test('realistic PLAN.md produces correct waves', () => {
431
+ const planText = `
432
+ # PLAN
433
+
434
+ ## Tasks
435
+
436
+ - [x] **T1**: Setup project structure
437
+ - [ ] **T2**: Implement parser
438
+ - Blocked by: T1
439
+ - [ ] **T3**: Add CLI interface
440
+ - Blocked by: T1
441
+ - [ ] **T4**: Integration tests
442
+ - Blocked by: T2, T3
443
+ - [ ] **T5**: Documentation
444
+ `;
445
+
446
+ const tasks = parsePlan(planText);
447
+ // T1 is completed, so only T2, T3, T4, T5 are pending
448
+ assert.equal(tasks.length, 4);
449
+
450
+ const waves = buildWaves(tasks, new Set());
451
+ // T2, T3, T5 have no pending deps (T1 is completed) → wave 1
452
+ // T4 depends on T2 and T3 → wave 2
453
+ assert.equal(waves.length, 2);
454
+ assert.deepEqual(waves[0].map(t => t.id), ['T2', 'T3', 'T5']);
455
+ assert.deepEqual(waves[1].map(t => t.id), ['T4']);
456
+ });
457
+
458
+ test('recalc with failed task excludes dependents from waves', () => {
459
+ const planText = `
460
+ - [ ] **T1**: Base implementation
461
+ - [ ] **T2**: Feature A
462
+ - Blocked by: T1
463
+ - [ ] **T3**: Feature B
464
+ - Blocked by: T1
465
+ - [ ] **T4**: Feature C
466
+ `;
467
+
468
+ const tasks = parsePlan(planText);
469
+ const waves = buildWaves(tasks, new Set(['T1']));
470
+ // T1 stuck → T2, T3 excluded → only T4
471
+ assert.equal(waves.length, 1);
472
+ assert.deepEqual(waves[0].map(t => t.id), ['T4']);
473
+ });
474
+ });
475
+
476
+ // ---------------------------------------------------------------------------
477
+ // 7. CLI subprocess tests
478
+ // ---------------------------------------------------------------------------
479
+
480
+ describe('CLI — wave-runner.js subprocess', () => {
481
+ test('exits 1 when PLAN.md is not found', () => {
482
+ const tmpDir = makeTmpDir();
483
+ try {
484
+ const { code, stderr } = runWaveRunner([], { cwd: tmpDir });
485
+ assert.equal(code, 1);
486
+ assert.ok(stderr.includes('not found'), `Expected "not found" in stderr: ${stderr}`);
487
+ } finally {
488
+ rmrf(tmpDir);
489
+ }
490
+ });
491
+
492
+ test('exits 0 and prints waves for valid PLAN.md', () => {
493
+ const tmpDir = makeTmpDir();
494
+ try {
495
+ fs.writeFileSync(
496
+ path.join(tmpDir, 'PLAN.md'),
497
+ '- [ ] **T1**: First\n- [ ] **T2**: Second\n - Blocked by: T1\n'
498
+ );
499
+ const { code, stdout } = runWaveRunner([], { cwd: tmpDir });
500
+ assert.equal(code, 0);
501
+ assert.ok(stdout.includes('Wave 1:'));
502
+ assert.ok(stdout.includes('T1'));
503
+ assert.ok(stdout.includes('Wave 2:'));
504
+ assert.ok(stdout.includes('T2'));
505
+ } finally {
506
+ rmrf(tmpDir);
507
+ }
508
+ });
509
+
510
+ test('--plan flag reads from custom path', () => {
511
+ const tmpDir = makeTmpDir();
512
+ try {
513
+ fs.writeFileSync(
514
+ path.join(tmpDir, 'custom.md'),
515
+ '- [ ] **T1**: Only task\n'
516
+ );
517
+ const { code, stdout } = runWaveRunner(['--plan', 'custom.md'], { cwd: tmpDir });
518
+ assert.equal(code, 0);
519
+ assert.ok(stdout.includes('T1'));
520
+ } finally {
521
+ rmrf(tmpDir);
522
+ }
523
+ });
524
+
525
+ test('--recalc --failed excludes failed task and dependents', () => {
526
+ const tmpDir = makeTmpDir();
527
+ try {
528
+ fs.writeFileSync(
529
+ path.join(tmpDir, 'PLAN.md'),
530
+ '- [ ] **T1**: Base\n- [ ] **T2**: Depends on T1\n - Blocked by: T1\n- [ ] **T3**: Independent\n'
531
+ );
532
+ const { code, stdout } = runWaveRunner(['--recalc', '--failed', 'T1'], { cwd: tmpDir });
533
+ assert.equal(code, 0);
534
+ assert.ok(stdout.includes('T3'), 'Independent task should appear');
535
+ assert.ok(!stdout.includes('T1'), 'Failed task should not appear');
536
+ assert.ok(!stdout.includes('T2'), 'Dependent of failed task should not appear');
537
+ } finally {
538
+ rmrf(tmpDir);
539
+ }
540
+ });
541
+
542
+ test('prints "(no pending tasks)" when all tasks are completed', () => {
543
+ const tmpDir = makeTmpDir();
544
+ try {
545
+ fs.writeFileSync(
546
+ path.join(tmpDir, 'PLAN.md'),
547
+ '- [x] **T1**: Done\n- [x] **T2**: Also done\n'
548
+ );
549
+ const { code, stdout } = runWaveRunner([], { cwd: tmpDir });
550
+ assert.equal(code, 0);
551
+ assert.ok(stdout.includes('(no pending tasks)'));
552
+ } finally {
553
+ rmrf(tmpDir);
554
+ }
555
+ });
556
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deepflow",
3
- "version": "0.1.91",
3
+ "version": "0.1.92",
4
4
  "description": "Doing reveals what thinking can't predict — spec-driven iterative development for Claude Code",
5
5
  "keywords": [
6
6
  "claude",