deepflow 0.1.101 → 0.1.103

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.js CHANGED
@@ -256,6 +256,7 @@ async function configureHooks(claudeDir) {
256
256
  const snapshotGuardCmd = `node "${path.join(claudeDir, 'hooks', 'df-snapshot-guard.js')}"`;
257
257
  const invariantCheckCmd = `node "${path.join(claudeDir, 'hooks', 'df-invariant-check.js')}"`;
258
258
  const subagentRegistryCmd = `node "${path.join(claudeDir, 'hooks', 'df-subagent-registry.js')}"`;
259
+ const commandUsageCmd = `node "${path.join(claudeDir, 'hooks', 'df-command-usage.js')}"`;
259
260
 
260
261
  let settings = {};
261
262
 
@@ -333,10 +334,10 @@ async function configureHooks(claudeDir) {
333
334
  settings.hooks.SessionEnd = [];
334
335
  }
335
336
 
336
- // Remove any existing quota logger / dashboard push from SessionEnd
337
+ // Remove any existing quota logger / dashboard push / command usage from SessionEnd
337
338
  settings.hooks.SessionEnd = settings.hooks.SessionEnd.filter(hook => {
338
339
  const cmd = hook.hooks?.[0]?.command || '';
339
- return !cmd.includes('df-quota-logger') && !cmd.includes('df-dashboard-push');
340
+ return !cmd.includes('df-quota-logger') && !cmd.includes('df-dashboard-push') && !cmd.includes('df-command-usage');
340
341
  });
341
342
 
342
343
  // Add quota logger to SessionEnd
@@ -354,17 +355,25 @@ async function configureHooks(claudeDir) {
354
355
  command: dashboardPushCmd
355
356
  }]
356
357
  });
357
- log('Quota logger + dashboard push configured (SessionEnd)');
358
+
359
+ // Add command usage hook to SessionEnd (flush any pending command data)
360
+ settings.hooks.SessionEnd.push({
361
+ hooks: [{
362
+ type: 'command',
363
+ command: commandUsageCmd
364
+ }]
365
+ });
366
+ log('Quota logger + dashboard push + command usage configured (SessionEnd)');
358
367
 
359
368
  // Configure PostToolUse hook for tool usage instrumentation
360
369
  if (!settings.hooks.PostToolUse) {
361
370
  settings.hooks.PostToolUse = [];
362
371
  }
363
372
 
364
- // Remove any existing deepflow tool usage / execution history / worktree guard / snapshot guard / invariant check hooks from PostToolUse
373
+ // Remove any existing deepflow tool usage / execution history / worktree guard / snapshot guard / invariant check / command usage hooks from PostToolUse
365
374
  settings.hooks.PostToolUse = settings.hooks.PostToolUse.filter(hook => {
366
375
  const cmd = hook.hooks?.[0]?.command || '';
367
- return !cmd.includes('df-tool-usage') && !cmd.includes('df-execution-history') && !cmd.includes('df-worktree-guard') && !cmd.includes('df-snapshot-guard') && !cmd.includes('df-invariant-check');
376
+ return !cmd.includes('df-tool-usage') && !cmd.includes('df-execution-history') && !cmd.includes('df-worktree-guard') && !cmd.includes('df-snapshot-guard') && !cmd.includes('df-invariant-check') && !cmd.includes('df-command-usage');
368
377
  });
369
378
 
370
379
  // Add tool usage hook
@@ -406,6 +415,14 @@ async function configureHooks(claudeDir) {
406
415
  command: invariantCheckCmd
407
416
  }]
408
417
  });
418
+
419
+ // Add command usage hook to PostToolUse
420
+ settings.hooks.PostToolUse.push({
421
+ hooks: [{
422
+ type: 'command',
423
+ command: commandUsageCmd
424
+ }]
425
+ });
409
426
  log('PostToolUse hook configured');
410
427
 
411
428
  // Configure SubagentStop hook for subagent registry
@@ -428,6 +445,26 @@ async function configureHooks(claudeDir) {
428
445
  });
429
446
  log('SubagentStop hook configured');
430
447
 
448
+ // Configure PreToolUse hook for command usage instrumentation
449
+ if (!settings.hooks.PreToolUse) {
450
+ settings.hooks.PreToolUse = [];
451
+ }
452
+
453
+ // Remove any existing deepflow command usage hooks from PreToolUse
454
+ settings.hooks.PreToolUse = settings.hooks.PreToolUse.filter(hook => {
455
+ const cmd = hook.hooks?.[0]?.command || '';
456
+ return !cmd.includes('df-command-usage');
457
+ });
458
+
459
+ // Add command usage hook to PreToolUse
460
+ settings.hooks.PreToolUse.push({
461
+ hooks: [{
462
+ type: 'command',
463
+ command: commandUsageCmd
464
+ }]
465
+ });
466
+ log('PreToolUse hook configured');
467
+
431
468
  fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
432
469
  }
433
470
 
@@ -611,7 +648,7 @@ async function uninstall() {
611
648
  ];
612
649
 
613
650
  if (level === 'global') {
614
- toRemove.push('hooks/df-statusline.js', 'hooks/df-check-update.js', 'hooks/df-invariant-check.js', 'hooks/df-quota-logger.js', 'hooks/df-tool-usage.js', 'hooks/df-dashboard-push.js', 'hooks/df-execution-history.js', 'hooks/df-worktree-guard.js', 'hooks/df-snapshot-guard.js', 'hooks/df-subagent-registry.js');
651
+ toRemove.push('hooks/df-statusline.js', 'hooks/df-check-update.js', 'hooks/df-invariant-check.js', 'hooks/df-quota-logger.js', 'hooks/df-tool-usage.js', 'hooks/df-dashboard-push.js', 'hooks/df-execution-history.js', 'hooks/df-worktree-guard.js', 'hooks/df-snapshot-guard.js', 'hooks/df-subagent-registry.js', 'hooks/df-command-usage.js');
615
652
  }
616
653
 
617
654
  for (const item of toRemove) {
@@ -649,7 +686,7 @@ async function uninstall() {
649
686
  if (settings.hooks?.SessionEnd) {
650
687
  settings.hooks.SessionEnd = settings.hooks.SessionEnd.filter(hook => {
651
688
  const cmd = hook.hooks?.[0]?.command || '';
652
- return !cmd.includes('df-quota-logger') && !cmd.includes('df-dashboard-push');
689
+ return !cmd.includes('df-quota-logger') && !cmd.includes('df-dashboard-push') && !cmd.includes('df-command-usage');
653
690
  });
654
691
  if (settings.hooks.SessionEnd.length === 0) {
655
692
  delete settings.hooks.SessionEnd;
@@ -658,12 +695,21 @@ async function uninstall() {
658
695
  if (settings.hooks?.PostToolUse) {
659
696
  settings.hooks.PostToolUse = settings.hooks.PostToolUse.filter(hook => {
660
697
  const cmd = hook.hooks?.[0]?.command || '';
661
- return !cmd.includes('df-tool-usage') && !cmd.includes('df-execution-history') && !cmd.includes('df-worktree-guard') && !cmd.includes('df-snapshot-guard') && !cmd.includes('df-invariant-check');
698
+ return !cmd.includes('df-tool-usage') && !cmd.includes('df-execution-history') && !cmd.includes('df-worktree-guard') && !cmd.includes('df-snapshot-guard') && !cmd.includes('df-invariant-check') && !cmd.includes('df-command-usage');
662
699
  });
663
700
  if (settings.hooks.PostToolUse.length === 0) {
664
701
  delete settings.hooks.PostToolUse;
665
702
  }
666
703
  }
704
+ if (settings.hooks?.PreToolUse) {
705
+ settings.hooks.PreToolUse = settings.hooks.PreToolUse.filter(hook => {
706
+ const cmd = hook.hooks?.[0]?.command || '';
707
+ return !cmd.includes('df-command-usage');
708
+ });
709
+ if (settings.hooks.PreToolUse.length === 0) {
710
+ delete settings.hooks.PreToolUse;
711
+ }
712
+ }
667
713
  if (settings.hooks?.SubagentStop) {
668
714
  settings.hooks.SubagentStop = settings.hooks.SubagentStop.filter(hook => {
669
715
  const cmd = hook.hooks?.[0]?.command || '';
@@ -677,7 +723,7 @@ async function uninstall() {
677
723
  delete settings.hooks;
678
724
  }
679
725
  fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
680
- console.log(` ${c.green}✓${c.reset} Removed SessionStart/SessionEnd/PostToolUse/SubagentStop hooks`);
726
+ console.log(` ${c.green}✓${c.reset} Removed SessionStart/SessionEnd/PreToolUse/PostToolUse/SubagentStop hooks`);
681
727
  } catch (e) {
682
728
  // Fail silently
683
729
  }
@@ -589,6 +589,220 @@ describe('Uninstaller — file removal and settings cleanup', () => {
589
589
  });
590
590
  });
591
591
 
592
+ // ---------------------------------------------------------------------------
593
+ // T4. command-usage hook registration (PreToolUse, PostToolUse, SessionEnd)
594
+ // ---------------------------------------------------------------------------
595
+
596
+ describe('T4 — command-usage hook registration in install.js', () => {
597
+
598
+ // -- Source-level checks: verify install.js registers df-command-usage.js --
599
+
600
+ test('source defines commandUsageCmd variable', () => {
601
+ const src = fs.readFileSync(path.resolve(__dirname, 'install.js'), 'utf8');
602
+ const pattern = /commandUsageCmd\s*=\s*`node.*df-command-usage\.js/;
603
+ assert.ok(
604
+ pattern.test(src),
605
+ 'install.js should define commandUsageCmd pointing to df-command-usage.js'
606
+ );
607
+ });
608
+
609
+ test('source pushes command-usage hook to PreToolUse', () => {
610
+ const src = fs.readFileSync(path.resolve(__dirname, 'install.js'), 'utf8');
611
+ // Find PreToolUse section — should contain a push with commandUsageCmd
612
+ const preToolUseSection = src.match(/PreToolUse[\s\S]*?log\('PreToolUse hook configured'\)/);
613
+ assert.ok(preToolUseSection, 'Should have a PreToolUse configuration section');
614
+ assert.ok(
615
+ preToolUseSection[0].includes('commandUsageCmd'),
616
+ 'PreToolUse section should push commandUsageCmd'
617
+ );
618
+ });
619
+
620
+ test('source pushes command-usage hook to PostToolUse', () => {
621
+ const src = fs.readFileSync(path.resolve(__dirname, 'install.js'), 'utf8');
622
+ // Find PostToolUse section
623
+ const postToolUseSection = src.match(/PostToolUse[\s\S]*?log\('PostToolUse hook configured'\)/);
624
+ assert.ok(postToolUseSection, 'Should have a PostToolUse configuration section');
625
+ assert.ok(
626
+ postToolUseSection[0].includes('commandUsageCmd'),
627
+ 'PostToolUse section should push commandUsageCmd'
628
+ );
629
+ });
630
+
631
+ test('source pushes command-usage hook to SessionEnd', () => {
632
+ const src = fs.readFileSync(path.resolve(__dirname, 'install.js'), 'utf8');
633
+ // Find SessionEnd section — should include command-usage alongside quota-logger + dashboard-push
634
+ const sessionEndSection = src.match(/SessionEnd[\s\S]*?log\('Quota logger.*configured.*SessionEnd/);
635
+ assert.ok(sessionEndSection, 'Should have a SessionEnd configuration section');
636
+ assert.ok(
637
+ sessionEndSection[0].includes('commandUsageCmd'),
638
+ 'SessionEnd section should push commandUsageCmd'
639
+ );
640
+ });
641
+
642
+ test('source creates PreToolUse array if missing', () => {
643
+ const src = fs.readFileSync(path.resolve(__dirname, 'install.js'), 'utf8');
644
+ assert.ok(
645
+ src.includes("if (!settings.hooks.PreToolUse)"),
646
+ 'install.js should initialize PreToolUse array if not present'
647
+ );
648
+ });
649
+
650
+ // -- Dedup logic: filter removes existing command-usage before re-adding --
651
+
652
+ test('PreToolUse dedup filter removes existing df-command-usage entries', () => {
653
+ const preToolUse = [
654
+ { hooks: [{ type: 'command', command: 'node /home/.claude/hooks/df-command-usage.js' }] },
655
+ { hooks: [{ type: 'command', command: 'node /usr/local/my-custom.js' }] },
656
+ ];
657
+
658
+ const filtered = preToolUse.filter(hook => {
659
+ const cmd = hook.hooks?.[0]?.command || '';
660
+ return !cmd.includes('df-command-usage');
661
+ });
662
+
663
+ assert.equal(filtered.length, 1, 'Should remove existing df-command-usage hook');
664
+ assert.ok(filtered[0].hooks[0].command.includes('my-custom.js'), 'Should keep non-deepflow hooks');
665
+ });
666
+
667
+ test('PostToolUse dedup filter removes df-command-usage alongside other deepflow hooks', () => {
668
+ const postToolUse = [
669
+ { hooks: [{ type: 'command', command: 'node /home/.claude/hooks/df-tool-usage.js' }] },
670
+ { hooks: [{ type: 'command', command: 'node /home/.claude/hooks/df-command-usage.js' }] },
671
+ { hooks: [{ type: 'command', command: 'node /home/.claude/hooks/df-worktree-guard.js' }] },
672
+ { hooks: [{ type: 'command', command: 'node /usr/local/keep-me.js' }] },
673
+ ];
674
+
675
+ const filtered = postToolUse.filter(hook => {
676
+ const cmd = hook.hooks?.[0]?.command || '';
677
+ return !cmd.includes('df-tool-usage') &&
678
+ !cmd.includes('df-execution-history') &&
679
+ !cmd.includes('df-worktree-guard') &&
680
+ !cmd.includes('df-snapshot-guard') &&
681
+ !cmd.includes('df-invariant-check') &&
682
+ !cmd.includes('df-command-usage');
683
+ });
684
+
685
+ assert.equal(filtered.length, 1);
686
+ assert.ok(filtered[0].hooks[0].command.includes('keep-me.js'));
687
+ });
688
+
689
+ test('SessionEnd dedup filter removes df-command-usage alongside quota-logger and dashboard-push', () => {
690
+ const sessionEnd = [
691
+ { hooks: [{ type: 'command', command: 'node /home/.claude/hooks/df-quota-logger.js' }] },
692
+ { hooks: [{ type: 'command', command: 'node /home/.claude/hooks/df-dashboard-push.js' }] },
693
+ { hooks: [{ type: 'command', command: 'node /home/.claude/hooks/df-command-usage.js' }] },
694
+ { hooks: [{ type: 'command', command: 'node /usr/local/keep.js' }] },
695
+ ];
696
+
697
+ const filtered = sessionEnd.filter(hook => {
698
+ const cmd = hook.hooks?.[0]?.command || '';
699
+ return !cmd.includes('df-quota-logger') &&
700
+ !cmd.includes('df-dashboard-push') &&
701
+ !cmd.includes('df-command-usage');
702
+ });
703
+
704
+ assert.equal(filtered.length, 1);
705
+ assert.ok(filtered[0].hooks[0].command.includes('keep.js'));
706
+ });
707
+
708
+ // -- Uninstall cleanup --
709
+
710
+ test('uninstall toRemove includes df-command-usage.js', () => {
711
+ const src = fs.readFileSync(path.resolve(__dirname, 'install.js'), 'utf8');
712
+ // Find the toRemove.push(...) line for hooks in uninstall
713
+ assert.ok(
714
+ src.includes("'hooks/df-command-usage.js'"),
715
+ 'toRemove should include hooks/df-command-usage.js for uninstall'
716
+ );
717
+ });
718
+
719
+ test('uninstall SessionEnd filter removes df-command-usage', () => {
720
+ const src = fs.readFileSync(path.resolve(__dirname, 'install.js'), 'utf8');
721
+ // In the uninstall function, the SessionEnd filter should include df-command-usage
722
+ // Find the uninstall section's SessionEnd filter
723
+ const uninstallSection = src.match(/async function uninstall[\s\S]+$/);
724
+ assert.ok(uninstallSection, 'Should have uninstall function');
725
+ // Check SessionEnd filter in uninstall includes command-usage
726
+ const sessionEndFilter = uninstallSection[0].match(/SessionEnd[\s\S]*?\.filter[\s\S]*?\);/);
727
+ assert.ok(sessionEndFilter, 'Should have SessionEnd filter in uninstall');
728
+ assert.ok(
729
+ sessionEndFilter[0].includes('df-command-usage'),
730
+ 'Uninstall SessionEnd filter should remove df-command-usage hooks'
731
+ );
732
+ });
733
+
734
+ test('uninstall PostToolUse filter removes df-command-usage', () => {
735
+ const src = fs.readFileSync(path.resolve(__dirname, 'install.js'), 'utf8');
736
+ const uninstallSection = src.match(/async function uninstall[\s\S]+$/);
737
+ const postToolUseFilter = uninstallSection[0].match(/PostToolUse[\s\S]*?\.filter[\s\S]*?\);/);
738
+ assert.ok(postToolUseFilter, 'Should have PostToolUse filter in uninstall');
739
+ assert.ok(
740
+ postToolUseFilter[0].includes('df-command-usage'),
741
+ 'Uninstall PostToolUse filter should remove df-command-usage hooks'
742
+ );
743
+ });
744
+
745
+ test('uninstall cleans up PreToolUse hooks', () => {
746
+ const src = fs.readFileSync(path.resolve(__dirname, 'install.js'), 'utf8');
747
+ const uninstallSection = src.match(/async function uninstall[\s\S]+$/);
748
+ assert.ok(
749
+ uninstallSection[0].includes('PreToolUse'),
750
+ 'Uninstall function should handle PreToolUse cleanup'
751
+ );
752
+ // Verify it filters out df-command-usage from PreToolUse
753
+ const preToolUseFilter = uninstallSection[0].match(/PreToolUse[\s\S]*?\.filter[\s\S]*?\);/);
754
+ assert.ok(preToolUseFilter, 'Should have PreToolUse filter in uninstall');
755
+ assert.ok(
756
+ preToolUseFilter[0].includes('df-command-usage'),
757
+ 'Uninstall PreToolUse filter should remove df-command-usage hooks'
758
+ );
759
+ });
760
+
761
+ test('uninstall deletes PreToolUse key when array becomes empty', () => {
762
+ // Reproduce the uninstall logic for PreToolUse
763
+ const settings = {
764
+ hooks: {
765
+ PreToolUse: [
766
+ { hooks: [{ type: 'command', command: 'node /home/.claude/hooks/df-command-usage.js' }] },
767
+ ],
768
+ }
769
+ };
770
+
771
+ settings.hooks.PreToolUse = settings.hooks.PreToolUse.filter(hook => {
772
+ const cmd = hook.hooks?.[0]?.command || '';
773
+ return !cmd.includes('df-command-usage');
774
+ });
775
+ if (settings.hooks.PreToolUse.length === 0) {
776
+ delete settings.hooks.PreToolUse;
777
+ }
778
+
779
+ assert.ok(!('PreToolUse' in settings.hooks), 'PreToolUse should be deleted when empty after filtering');
780
+ });
781
+
782
+ test('uninstall keeps PreToolUse when non-deepflow hooks remain', () => {
783
+ const settings = {
784
+ hooks: {
785
+ PreToolUse: [
786
+ { hooks: [{ type: 'command', command: 'node /home/.claude/hooks/df-command-usage.js' }] },
787
+ { hooks: [{ type: 'command', command: 'node /usr/local/custom-pre-hook.js' }] },
788
+ ],
789
+ }
790
+ };
791
+
792
+ settings.hooks.PreToolUse = settings.hooks.PreToolUse.filter(hook => {
793
+ const cmd = hook.hooks?.[0]?.command || '';
794
+ return !cmd.includes('df-command-usage');
795
+ });
796
+ if (settings.hooks.PreToolUse.length === 0) {
797
+ delete settings.hooks.PreToolUse;
798
+ }
799
+
800
+ assert.ok('PreToolUse' in settings.hooks, 'PreToolUse should be kept when custom hooks remain');
801
+ assert.equal(settings.hooks.PreToolUse.length, 1);
802
+ assert.ok(settings.hooks.PreToolUse[0].hooks[0].command.includes('custom-pre-hook.js'));
803
+ });
804
+ });
805
+
592
806
  // ---------------------------------------------------------------------------
593
807
  // 4. isInstalled helper logic
594
808
  // ---------------------------------------------------------------------------
@@ -208,7 +208,8 @@ function consolidate(specEntries, fileConflicts) {
208
208
 
209
209
  /**
210
210
  * Render consolidated tasks as PLAN.md-compatible markdown.
211
- * Groups tasks under ### {specName} headings.
211
+ * Groups tasks under ### doing-{specName} headings with a details reference line.
212
+ * One line per task — no sub-bullets. Files omitted (live in mini-plans only).
212
213
  * Compatible with wave-runner's parsePlan regex (see wave-runner.js parsePlan).
213
214
  */
214
215
  function formatConsolidated(consolidated) {
@@ -221,11 +222,15 @@ function formatConsolidated(consolidated) {
221
222
 
222
223
  for (const task of consolidated) {
223
224
  if (task.specName !== lastSpec) {
224
- lines.push(`### ${task.specName}\n`);
225
+ // Close previous spec with trailing blank line (already added after last task)
226
+ const doingName = `doing-${task.specName}`;
227
+ const planPath = `.deepflow/plans/${doingName}.md`;
228
+ lines.push(`### ${doingName}\n`);
229
+ lines.push(`> Details: [\`${planPath}\`](${planPath})\n`);
225
230
  lastSpec = task.specName;
226
231
  }
227
232
 
228
- // Task header line
233
+ // Task header line — one line, no sub-bullets
229
234
  const tagPart = task.tags ? ` ${task.tags}` : '';
230
235
  // Append conflict annotations to description if any
231
236
  const conflictPart = task.conflictAnnotations.length > 0
@@ -233,23 +238,18 @@ function formatConsolidated(consolidated) {
233
238
  : '';
234
239
  const descPart = (task.description + conflictPart).trim();
235
240
  const headerDesc = descPart ? `: ${descPart}` : '';
236
- lines.push(`- [ ] **${task.globalId}**${tagPart}${headerDesc}`);
237
241
 
238
- // Files annotation
239
- if (task.files.length > 0) {
240
- lines.push(` - Files: ${task.files.join(', ')}`);
241
- }
242
-
243
- // Blocked by annotation
244
- if (task.blockedBy.length > 0) {
245
- lines.push(` - Blocked by: ${task.blockedBy.join(', ')}`);
246
- } else {
247
- lines.push(' - Blocked by: none');
248
- }
242
+ // Blocked by suffix — omit entirely when empty
243
+ const blockedSuffix = task.blockedBy.length > 0
244
+ ? ` | Blocked by: ${task.blockedBy.join(', ')}`
245
+ : '';
249
246
 
250
- lines.push('');
247
+ lines.push(`- [ ] **${task.globalId}**${tagPart}${headerDesc}${blockedSuffix}`);
251
248
  }
252
249
 
250
+ // Trailing newline after last task
251
+ lines.push('');
252
+
253
253
  return lines.join('\n');
254
254
  }
255
255
 
@@ -85,14 +85,38 @@ function parsePlan(text) {
85
85
  // Match pending task header: - [ ] **T{N}**...
86
86
  const taskMatch = line.match(/^\s*-\s+\[\s+\]\s+\*\*T(\d+)\*\*(?:\s+\[[^\]]*\])?[:\s]*(.*)/);
87
87
  if (taskMatch) {
88
+ const rest = taskMatch[2].trim();
89
+
90
+ // Extract inline blocked-by (from " | Blocked by: T1, T2")
91
+ let inlineBlockedBy = [];
92
+ let descPart = rest;
93
+ const blockedInlineMatch = rest.match(/\s*\|\s*Blocked\s+by:\s+(.+)$/i);
94
+ if (blockedInlineMatch) {
95
+ descPart = rest.substring(0, rest.length - blockedInlineMatch[0].length).trim();
96
+ inlineBlockedBy = blockedInlineMatch[1]
97
+ .split(/[,\s]+/)
98
+ .map(s => s.trim())
99
+ .filter(s => /^T\d+$/.test(s));
100
+ }
101
+
102
+ // Extract inline model/effort (from " — model/effort")
103
+ let inlineModel = null;
104
+ let inlineEffort = null;
105
+ const modelInlineMatch = descPart.match(/\s*\u2014\s*(haiku|sonnet|opus)\/(low|medium|high)\s*$/i);
106
+ if (modelInlineMatch) {
107
+ descPart = descPart.substring(0, descPart.length - modelInlineMatch[0].length).trim();
108
+ inlineModel = modelInlineMatch[1].toLowerCase();
109
+ inlineEffort = modelInlineMatch[2].toLowerCase();
110
+ }
111
+
88
112
  current = {
89
113
  id: `T${taskMatch[1]}`,
90
114
  num: parseInt(taskMatch[1], 10),
91
- description: taskMatch[2].trim(),
92
- blockedBy: [],
93
- model: null,
115
+ description: descPart,
116
+ blockedBy: inlineBlockedBy,
117
+ model: inlineModel,
94
118
  files: null,
95
- effort: null,
119
+ effort: inlineEffort,
96
120
  spec: currentSpec,
97
121
  };
98
122
  tasks.push(current);
@@ -107,35 +131,43 @@ function parsePlan(text) {
107
131
  }
108
132
 
109
133
  if (current) {
110
- // Match "Blocked by:" annotation
134
+ // Match "Blocked by:" annotation — only apply if inline parsing found no deps
111
135
  const blockedMatch = line.match(/^\s+-\s+Blocked\s+by:\s+(.+)/i);
112
136
  if (blockedMatch) {
113
- const deps = blockedMatch[1]
114
- .split(/[,\s]+/)
115
- .map(s => s.trim())
116
- .filter(s => /^T\d+$/.test(s));
117
- current.blockedBy.push(...deps);
137
+ if (current.blockedBy.length === 0) {
138
+ const deps = blockedMatch[1]
139
+ .split(/[,\s]+/)
140
+ .map(s => s.trim())
141
+ .filter(s => /^T\d+$/.test(s));
142
+ current.blockedBy.push(...deps);
143
+ }
118
144
  continue;
119
145
  }
120
146
 
121
- // Match "Model:" annotation
147
+ // Match "Model:" annotation — only apply if inline parsing found no model
122
148
  const modelMatch = line.match(/^\s+-\s+Model:\s+(.+)/i);
123
149
  if (modelMatch) {
124
- current.model = modelMatch[1].trim();
150
+ if (current.model === null) {
151
+ current.model = modelMatch[1].trim();
152
+ }
125
153
  continue;
126
154
  }
127
155
 
128
- // Match "Files:" annotation
156
+ // Match "Files:" annotation — always apply (no inline equivalent)
129
157
  const filesMatch = line.match(/^\s+-\s+Files:\s+(.+)/i);
130
158
  if (filesMatch) {
131
- current.files = filesMatch[1].trim();
159
+ if (current.files === null) {
160
+ current.files = filesMatch[1].trim();
161
+ }
132
162
  continue;
133
163
  }
134
164
 
135
- // Match "Effort:" annotation
165
+ // Match "Effort:" annotation — only apply if inline parsing found no effort
136
166
  const effortMatch = line.match(/^\s+-\s+Effort:\s+(.+)/i);
137
167
  if (effortMatch) {
138
- current.effort = effortMatch[1].trim();
168
+ if (current.effort === null) {
169
+ current.effort = effortMatch[1].trim();
170
+ }
139
171
  continue;
140
172
  }
141
173
  }