popeye-cli 1.0.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. package/README.md +521 -125
  2. package/dist/adapters/claude.d.ts +16 -4
  3. package/dist/adapters/claude.d.ts.map +1 -1
  4. package/dist/adapters/claude.js +679 -33
  5. package/dist/adapters/claude.js.map +1 -1
  6. package/dist/adapters/gemini.d.ts +55 -0
  7. package/dist/adapters/gemini.d.ts.map +1 -0
  8. package/dist/adapters/gemini.js +318 -0
  9. package/dist/adapters/gemini.js.map +1 -0
  10. package/dist/adapters/openai.d.ts.map +1 -1
  11. package/dist/adapters/openai.js +41 -7
  12. package/dist/adapters/openai.js.map +1 -1
  13. package/dist/auth/claude.d.ts +11 -9
  14. package/dist/auth/claude.d.ts.map +1 -1
  15. package/dist/auth/claude.js +107 -71
  16. package/dist/auth/claude.js.map +1 -1
  17. package/dist/auth/gemini.d.ts +58 -0
  18. package/dist/auth/gemini.d.ts.map +1 -0
  19. package/dist/auth/gemini.js +172 -0
  20. package/dist/auth/gemini.js.map +1 -0
  21. package/dist/auth/index.d.ts +11 -7
  22. package/dist/auth/index.d.ts.map +1 -1
  23. package/dist/auth/index.js +23 -5
  24. package/dist/auth/index.js.map +1 -1
  25. package/dist/auth/keychain.d.ts +20 -7
  26. package/dist/auth/keychain.d.ts.map +1 -1
  27. package/dist/auth/keychain.js +85 -29
  28. package/dist/auth/keychain.js.map +1 -1
  29. package/dist/auth/openai.d.ts +2 -2
  30. package/dist/auth/openai.d.ts.map +1 -1
  31. package/dist/auth/openai.js +30 -32
  32. package/dist/auth/openai.js.map +1 -1
  33. package/dist/cli/interactive.d.ts.map +1 -1
  34. package/dist/cli/interactive.js +1151 -110
  35. package/dist/cli/interactive.js.map +1 -1
  36. package/dist/config/defaults.d.ts +6 -1
  37. package/dist/config/defaults.d.ts.map +1 -1
  38. package/dist/config/defaults.js +10 -2
  39. package/dist/config/defaults.js.map +1 -1
  40. package/dist/config/index.d.ts +10 -0
  41. package/dist/config/index.d.ts.map +1 -1
  42. package/dist/config/index.js +19 -0
  43. package/dist/config/index.js.map +1 -1
  44. package/dist/config/schema.d.ts +20 -0
  45. package/dist/config/schema.d.ts.map +1 -1
  46. package/dist/config/schema.js +7 -0
  47. package/dist/config/schema.js.map +1 -1
  48. package/dist/generators/python.d.ts.map +1 -1
  49. package/dist/generators/python.js +1 -0
  50. package/dist/generators/python.js.map +1 -1
  51. package/dist/generators/typescript.d.ts.map +1 -1
  52. package/dist/generators/typescript.js +1 -0
  53. package/dist/generators/typescript.js.map +1 -1
  54. package/dist/state/index.d.ts +108 -0
  55. package/dist/state/index.d.ts.map +1 -1
  56. package/dist/state/index.js +551 -4
  57. package/dist/state/index.js.map +1 -1
  58. package/dist/state/registry.d.ts +52 -0
  59. package/dist/state/registry.d.ts.map +1 -0
  60. package/dist/state/registry.js +215 -0
  61. package/dist/state/registry.js.map +1 -0
  62. package/dist/types/cli.d.ts +4 -0
  63. package/dist/types/cli.d.ts.map +1 -1
  64. package/dist/types/cli.js.map +1 -1
  65. package/dist/types/consensus.d.ts +69 -4
  66. package/dist/types/consensus.d.ts.map +1 -1
  67. package/dist/types/consensus.js +24 -3
  68. package/dist/types/consensus.js.map +1 -1
  69. package/dist/types/workflow.d.ts +55 -0
  70. package/dist/types/workflow.d.ts.map +1 -1
  71. package/dist/types/workflow.js +16 -0
  72. package/dist/types/workflow.js.map +1 -1
  73. package/dist/workflow/auto-fix.d.ts +45 -0
  74. package/dist/workflow/auto-fix.d.ts.map +1 -0
  75. package/dist/workflow/auto-fix.js +274 -0
  76. package/dist/workflow/auto-fix.js.map +1 -0
  77. package/dist/workflow/consensus.d.ts +44 -2
  78. package/dist/workflow/consensus.d.ts.map +1 -1
  79. package/dist/workflow/consensus.js +565 -17
  80. package/dist/workflow/consensus.js.map +1 -1
  81. package/dist/workflow/execution-mode.d.ts +10 -4
  82. package/dist/workflow/execution-mode.d.ts.map +1 -1
  83. package/dist/workflow/execution-mode.js +547 -58
  84. package/dist/workflow/execution-mode.js.map +1 -1
  85. package/dist/workflow/index.d.ts +14 -2
  86. package/dist/workflow/index.d.ts.map +1 -1
  87. package/dist/workflow/index.js +69 -6
  88. package/dist/workflow/index.js.map +1 -1
  89. package/dist/workflow/milestone-workflow.d.ts +34 -0
  90. package/dist/workflow/milestone-workflow.d.ts.map +1 -0
  91. package/dist/workflow/milestone-workflow.js +414 -0
  92. package/dist/workflow/milestone-workflow.js.map +1 -0
  93. package/dist/workflow/plan-mode.d.ts +14 -1
  94. package/dist/workflow/plan-mode.d.ts.map +1 -1
  95. package/dist/workflow/plan-mode.js +589 -47
  96. package/dist/workflow/plan-mode.js.map +1 -1
  97. package/dist/workflow/plan-storage.d.ts +142 -0
  98. package/dist/workflow/plan-storage.d.ts.map +1 -0
  99. package/dist/workflow/plan-storage.js +331 -0
  100. package/dist/workflow/plan-storage.js.map +1 -0
  101. package/dist/workflow/project-verification.d.ts +37 -0
  102. package/dist/workflow/project-verification.d.ts.map +1 -0
  103. package/dist/workflow/project-verification.js +381 -0
  104. package/dist/workflow/project-verification.js.map +1 -0
  105. package/dist/workflow/task-workflow.d.ts +37 -0
  106. package/dist/workflow/task-workflow.d.ts.map +1 -0
  107. package/dist/workflow/task-workflow.js +383 -0
  108. package/dist/workflow/task-workflow.js.map +1 -0
  109. package/dist/workflow/test-runner.d.ts +1 -0
  110. package/dist/workflow/test-runner.d.ts.map +1 -1
  111. package/dist/workflow/test-runner.js +9 -5
  112. package/dist/workflow/test-runner.js.map +1 -1
  113. package/dist/workflow/ui-designer.d.ts +82 -0
  114. package/dist/workflow/ui-designer.d.ts.map +1 -0
  115. package/dist/workflow/ui-designer.js +234 -0
  116. package/dist/workflow/ui-designer.js.map +1 -0
  117. package/dist/workflow/ui-setup.d.ts +58 -0
  118. package/dist/workflow/ui-setup.d.ts.map +1 -0
  119. package/dist/workflow/ui-setup.js +685 -0
  120. package/dist/workflow/ui-setup.js.map +1 -0
  121. package/dist/workflow/ui-verification.d.ts +114 -0
  122. package/dist/workflow/ui-verification.d.ts.map +1 -0
  123. package/dist/workflow/ui-verification.js +258 -0
  124. package/dist/workflow/ui-verification.js.map +1 -0
  125. package/dist/workflow/workflow-logger.d.ts +110 -0
  126. package/dist/workflow/workflow-logger.d.ts.map +1 -0
  127. package/dist/workflow/workflow-logger.js +267 -0
  128. package/dist/workflow/workflow-logger.js.map +1 -0
  129. package/package.json +2 -2
  130. package/src/adapters/claude.ts +815 -34
  131. package/src/adapters/gemini.ts +373 -0
  132. package/src/adapters/openai.ts +40 -7
  133. package/src/auth/claude.ts +120 -78
  134. package/src/auth/gemini.ts +207 -0
  135. package/src/auth/index.ts +28 -8
  136. package/src/auth/keychain.ts +95 -28
  137. package/src/auth/openai.ts +29 -36
  138. package/src/cli/interactive.ts +1357 -115
  139. package/src/config/defaults.ts +10 -2
  140. package/src/config/index.ts +21 -0
  141. package/src/config/schema.ts +7 -0
  142. package/src/generators/python.ts +1 -0
  143. package/src/generators/typescript.ts +1 -0
  144. package/src/state/index.ts +713 -4
  145. package/src/state/registry.ts +278 -0
  146. package/src/types/cli.ts +4 -0
  147. package/src/types/consensus.ts +65 -6
  148. package/src/types/workflow.ts +35 -0
  149. package/src/workflow/auto-fix.ts +340 -0
  150. package/src/workflow/consensus.ts +750 -16
  151. package/src/workflow/execution-mode.ts +673 -74
  152. package/src/workflow/index.ts +95 -6
  153. package/src/workflow/milestone-workflow.ts +576 -0
  154. package/src/workflow/plan-mode.ts +696 -50
  155. package/src/workflow/plan-storage.ts +482 -0
  156. package/src/workflow/project-verification.ts +471 -0
  157. package/src/workflow/task-workflow.ts +525 -0
  158. package/src/workflow/test-runner.ts +10 -5
  159. package/src/workflow/ui-designer.ts +337 -0
  160. package/src/workflow/ui-setup.ts +797 -0
  161. package/src/workflow/ui-verification.ts +357 -0
  162. package/src/workflow/workflow-logger.ts +353 -0
  163. package/tests/config/config.test.ts +1 -1
  164. package/tests/types/consensus.test.ts +3 -3
  165. package/tests/workflow/plan-mode.test.ts +213 -0
  166. package/tests/workflow/test-runner.test.ts +5 -3
@@ -3,6 +3,8 @@
3
3
  * Provides high-level API for managing project state
4
4
  */
5
5
 
6
+ import { promises as fs } from 'node:fs';
7
+ import path from 'node:path';
6
8
  import { v4 as uuidv4 } from 'uuid';
7
9
  import type {
8
10
  ProjectState,
@@ -20,9 +22,11 @@ import {
20
22
  deleteState,
21
23
  backupState,
22
24
  } from './persistence.js';
25
+ import { registerProject, unregisterProject } from './registry.js';
23
26
 
24
27
  // Re-export persistence utilities
25
28
  export * from './persistence.js';
29
+ export * from './registry.js';
26
30
 
27
31
  /**
28
32
  * Create a new project state
@@ -59,6 +63,10 @@ export async function createProject(
59
63
  };
60
64
 
61
65
  await saveState(projectDir, state);
66
+
67
+ // Register project in global registry
68
+ await registerProject(projectDir);
69
+
62
70
  return state;
63
71
  }
64
72
 
@@ -109,6 +117,12 @@ export async function updateState(
109
117
  };
110
118
 
111
119
  await saveState(projectDir, updated);
120
+
121
+ // Update registry (async, don't wait)
122
+ registerProject(projectDir).catch(() => {
123
+ // Silently ignore registry update failures
124
+ });
125
+
112
126
  return updated;
113
127
  }
114
128
 
@@ -139,10 +153,25 @@ export async function addMilestones(
139
153
  ): Promise<ProjectState> {
140
154
  const current = await loadProject(projectDir);
141
155
 
142
- const newMilestones: Milestone[] = milestones.map((m, index) => ({
143
- ...m,
144
- id: `milestone-${current.milestones.length + index + 1}`,
145
- }));
156
+ const newMilestones: Milestone[] = milestones.map((m, index) => {
157
+ const milestoneId = `milestone-${current.milestones.length + index + 1}`;
158
+
159
+ // Ensure all tasks have proper IDs and status
160
+ const tasksWithIds: Task[] = (m.tasks || []).map((t, taskIndex) => ({
161
+ ...t,
162
+ id: t.id || `${milestoneId}-task-${taskIndex + 1}`,
163
+ status: t.status || ('pending' as TaskStatus),
164
+ name: t.name || `Task ${taskIndex + 1}`,
165
+ description: t.description || t.name || `Task ${taskIndex + 1}`,
166
+ }));
167
+
168
+ return {
169
+ ...m,
170
+ id: milestoneId,
171
+ tasks: tasksWithIds,
172
+ status: m.status || 'pending',
173
+ };
174
+ });
146
175
 
147
176
  return updateState(projectDir, {
148
177
  milestones: [...current.milestones, ...newMilestones],
@@ -450,5 +479,685 @@ export async function resetToPhase(
450
479
  * @returns True if project was deleted
451
480
  */
452
481
  export async function deleteProject(projectDir: string): Promise<boolean> {
482
+ // Unregister from global registry
483
+ await unregisterProject(projectDir);
453
484
  return deleteState(projectDir);
454
485
  }
486
+
487
+ /**
488
+ * Detailed progress analysis comparing plan vs actual status
489
+ */
490
+ export interface ProjectProgressAnalysis {
491
+ // Overall status
492
+ isActuallyComplete: boolean;
493
+ statusMismatch: boolean; // true if status='complete' but work is incomplete
494
+ planMismatch: boolean; // true if plan file has more tasks than state
495
+
496
+ // Milestone breakdown (from state)
497
+ totalMilestones: number;
498
+ completedMilestones: number;
499
+ inProgressMilestones: number;
500
+ pendingMilestones: number;
501
+
502
+ // Task breakdown (from state)
503
+ totalTasks: number;
504
+ completedTasks: number;
505
+ inProgressTasks: number;
506
+ pendingTasks: number;
507
+ failedTasks: number;
508
+
509
+ // Plan file analysis
510
+ planTaskCount: number; // Tasks parsed from PLAN.md
511
+ planMilestoneCount: number; // Milestones parsed from PLAN.md
512
+ planParseError?: string; // Error if plan couldn't be read/parsed
513
+ missingFromState: string[]; // Task names in plan but not in state
514
+
515
+ // Percentage (based on plan task count if available, otherwise state)
516
+ percentComplete: number;
517
+
518
+ // Next items to work on
519
+ nextMilestone?: { id: string; name: string };
520
+ nextTask?: { id: string; name: string; milestone: string };
521
+
522
+ // Summary for display
523
+ progressSummary: string;
524
+
525
+ // Incomplete items for detailed view
526
+ incompleteMilestones: Array<{ id: string; name: string; tasksRemaining: number }>;
527
+ incompleteTasks: Array<{ id: string; name: string; milestone: string; status: string }>;
528
+ }
529
+
530
+ /**
531
+ * Parse plan file to count expected tasks and milestones
532
+ * Uses multiple strategies to identify actionable tasks
533
+ *
534
+ * @param planContent - The plan markdown content
535
+ * @returns Parsed task and milestone counts with task names
536
+ */
537
+ function parsePlanForTaskCount(planContent: string): {
538
+ milestoneCount: number;
539
+ taskCount: number;
540
+ taskNames: string[];
541
+ } {
542
+ const taskNames: string[] = [];
543
+
544
+ // Strategy 1: Look for explicit "Task N:" or "### Task" patterns
545
+ const explicitTaskPattern = /^#{2,4}\s*Task\s+(?:[\d.]+)?[:\s]+(.+)$/gim;
546
+ let match;
547
+ while ((match = explicitTaskPattern.exec(planContent)) !== null) {
548
+ const name = match[1].trim().replace(/^\*\*(.+)\*\*$/, '$1').slice(0, 100);
549
+ if (name.length >= 5 && !taskNames.includes(name)) {
550
+ taskNames.push(name);
551
+ }
552
+ }
553
+
554
+ // Strategy 2: Look for actionable bullet points (Implement, Create, Build, etc.)
555
+ const actionVerbs = [
556
+ 'implement', 'create', 'build', 'develop', 'write', 'add', 'set up', 'setup',
557
+ 'configure', 'install', 'integrate', 'design', 'define', 'establish',
558
+ 'generate', 'construct', 'deploy', 'test', 'validate', 'fix', 'update',
559
+ 'refactor', 'optimize', 'extend', 'enhance', 'modify', 'initialize',
560
+ ];
561
+
562
+ const bulletPattern = /^[-*+]\s+(.+)$/gm;
563
+ while ((match = bulletPattern.exec(planContent)) !== null) {
564
+ const text = match[1].trim().replace(/^\*\*(.+?)\*\*:?\s*/, '$1: ');
565
+ const textLower = text.toLowerCase();
566
+ const startsWithAction = actionVerbs.some(verb =>
567
+ textLower.startsWith(verb + ' ') || textLower.startsWith(verb + ':')
568
+ );
569
+ if (startsWithAction && text.length >= 10 && text.length <= 200 && !taskNames.includes(text)) {
570
+ taskNames.push(text.slice(0, 100));
571
+ }
572
+ }
573
+
574
+ // Strategy 3: Look for numbered items with actionable verbs
575
+ const numberedPattern = /^\d+[.)]\s+(.+)$/gm;
576
+ while ((match = numberedPattern.exec(planContent)) !== null) {
577
+ const text = match[1].trim().replace(/^\*\*(.+?)\*\*:?\s*/, '$1: ');
578
+ const textLower = text.toLowerCase();
579
+ const startsWithAction = actionVerbs.some(verb =>
580
+ textLower.startsWith(verb + ' ') || textLower.startsWith(verb + ':')
581
+ );
582
+ if (startsWithAction && text.length >= 10 && text.length <= 200 && !taskNames.includes(text)) {
583
+ taskNames.push(text.slice(0, 100));
584
+ }
585
+ }
586
+
587
+ // Count milestones
588
+ const milestonePattern = /^#{1,3}\s*(?:Milestone|Phase|Sprint|Stage)\s*[\d.]*[:\s]+/gim;
589
+ const milestoneMatches = planContent.match(milestonePattern) || [];
590
+ const milestoneCount = milestoneMatches.length || 1;
591
+
592
+ return {
593
+ milestoneCount,
594
+ taskCount: taskNames.length,
595
+ taskNames,
596
+ };
597
+ }
598
+
599
+ /**
600
+ * Read and parse the plan file from docs/PLAN.md
601
+ *
602
+ * @param projectDir - The project root directory
603
+ * @returns Plan analysis or error
604
+ */
605
+ async function readPlanFile(projectDir: string): Promise<{
606
+ success: boolean;
607
+ milestoneCount: number;
608
+ taskCount: number;
609
+ taskNames: string[];
610
+ error?: string;
611
+ }> {
612
+ const planPaths = [
613
+ path.join(projectDir, 'docs', 'PLAN.md'),
614
+ path.join(projectDir, 'docs', 'PLAN-DRAFT.md'),
615
+ ];
616
+
617
+ for (const planPath of planPaths) {
618
+ try {
619
+ const content = await fs.readFile(planPath, 'utf-8');
620
+ const parsed = parsePlanForTaskCount(content);
621
+ return {
622
+ success: true,
623
+ ...parsed,
624
+ };
625
+ } catch {
626
+ // Try next path
627
+ }
628
+ }
629
+
630
+ return {
631
+ success: false,
632
+ milestoneCount: 0,
633
+ taskCount: 0,
634
+ taskNames: [],
635
+ error: 'No plan file found in docs/',
636
+ };
637
+ }
638
+
639
+ /**
640
+ * Analyze project progress in detail
641
+ * Compares actual task/milestone completion against the plan file
642
+ *
643
+ * @param projectDir - The project root directory
644
+ * @returns Detailed progress analysis
645
+ */
646
+ export async function analyzeProjectProgress(projectDir: string): Promise<ProjectProgressAnalysis> {
647
+ const state = await loadProject(projectDir);
648
+
649
+ // Count milestone statuses from state
650
+ const totalMilestones = state.milestones.length;
651
+ const completedMilestones = state.milestones.filter(m => m.status === 'complete').length;
652
+ const inProgressMilestones = state.milestones.filter(m => m.status === 'in-progress').length;
653
+ const pendingMilestones = state.milestones.filter(m => m.status === 'pending').length;
654
+
655
+ // Collect all tasks from state and count statuses
656
+ const allTasks = state.milestones.flatMap(m =>
657
+ m.tasks.map(t => ({ ...t, milestoneName: m.name, milestoneId: m.id }))
658
+ );
659
+ const totalTasks = allTasks.length;
660
+ const completedTasks = allTasks.filter(t => t.status === 'complete').length;
661
+ const inProgressTasks = allTasks.filter(t => t.status === 'in-progress').length;
662
+ const pendingTasks = allTasks.filter(t => t.status === 'pending').length;
663
+ const failedTasks = allTasks.filter(t => t.status === 'failed').length;
664
+
665
+ // Read and parse the plan file for comparison
666
+ const planAnalysis = await readPlanFile(projectDir);
667
+ const planTaskCount = planAnalysis.taskCount;
668
+ const planMilestoneCount = planAnalysis.milestoneCount;
669
+ const planParseError = planAnalysis.error;
670
+
671
+ // Find tasks in plan that are not in state
672
+ const stateTaskNames = allTasks.map(t => t.name.toLowerCase());
673
+ const missingFromState = planAnalysis.taskNames.filter(planTask => {
674
+ const planTaskLower = planTask.toLowerCase();
675
+ // Check if any state task is similar (contains or is contained)
676
+ return !stateTaskNames.some(stateTask =>
677
+ stateTask.includes(planTaskLower.slice(0, 20)) ||
678
+ planTaskLower.includes(stateTask.slice(0, 20))
679
+ );
680
+ });
681
+
682
+ // Check for plan mismatch - plan has significantly more tasks than state
683
+ const planMismatch = planTaskCount > 0 && planTaskCount > totalTasks * 1.5;
684
+
685
+ // Calculate percentage - use plan task count if we have more tasks in plan
686
+ const effectiveTotal = planMismatch ? planTaskCount : totalTasks;
687
+ const percentComplete = effectiveTotal > 0
688
+ ? Math.round((completedTasks / effectiveTotal) * 100)
689
+ : 0;
690
+
691
+ // Determine if actually complete - must match plan if plan has more tasks
692
+ const isActuallyComplete = totalMilestones > 0 &&
693
+ completedMilestones === totalMilestones &&
694
+ completedTasks === totalTasks &&
695
+ !planMismatch; // Can't be complete if plan has more tasks
696
+
697
+ // Check for status mismatch
698
+ const statusMismatch = (state.status === 'complete' || state.phase === 'complete') &&
699
+ (!isActuallyComplete || planMismatch);
700
+
701
+ // Find next items to work on
702
+ let nextMilestone: { id: string; name: string } | undefined;
703
+ let nextTask: { id: string; name: string; milestone: string } | undefined;
704
+
705
+ for (const milestone of state.milestones) {
706
+ if (milestone.status === 'complete') continue;
707
+
708
+ if (!nextMilestone) {
709
+ nextMilestone = { id: milestone.id, name: milestone.name };
710
+ }
711
+
712
+ for (const task of milestone.tasks) {
713
+ if (task.status === 'pending' || task.status === 'in-progress' || task.status === 'failed') {
714
+ if (!nextTask) {
715
+ nextTask = { id: task.id, name: task.name, milestone: milestone.name };
716
+ }
717
+ break;
718
+ }
719
+ }
720
+
721
+ if (nextTask) break;
722
+ }
723
+
724
+ // Collect incomplete items
725
+ const incompleteMilestones = state.milestones
726
+ .filter(m => m.status !== 'complete')
727
+ .map(m => ({
728
+ id: m.id,
729
+ name: m.name,
730
+ tasksRemaining: m.tasks.filter(t => t.status !== 'complete').length,
731
+ }));
732
+
733
+ const incompleteTasks = allTasks
734
+ .filter(t => t.status !== 'complete')
735
+ .slice(0, 20)
736
+ .map(t => ({
737
+ id: t.id,
738
+ name: t.name,
739
+ milestone: t.milestoneName,
740
+ status: t.status,
741
+ }));
742
+
743
+ // Build progress summary
744
+ let progressSummary: string;
745
+ if (planMismatch) {
746
+ progressSummary = `PLAN MISMATCH: State has ${completedTasks}/${totalTasks} tasks but plan has ${planTaskCount} tasks. ` +
747
+ `Only ${percentComplete}% of plan completed.`;
748
+ } else if (isActuallyComplete) {
749
+ progressSummary = `All ${totalTasks} tasks complete across ${totalMilestones} milestones`;
750
+ } else if (statusMismatch) {
751
+ progressSummary = `WARNING: Status shows 'complete' but only ${completedTasks}/${effectiveTotal} tasks done (${percentComplete}%)`;
752
+ } else {
753
+ progressSummary = `${completedTasks}/${effectiveTotal} tasks complete (${percentComplete}%), ${completedMilestones}/${totalMilestones} milestones`;
754
+ }
755
+
756
+ return {
757
+ isActuallyComplete,
758
+ statusMismatch,
759
+ planMismatch,
760
+ totalMilestones,
761
+ completedMilestones,
762
+ inProgressMilestones,
763
+ pendingMilestones,
764
+ totalTasks,
765
+ completedTasks,
766
+ inProgressTasks,
767
+ pendingTasks,
768
+ failedTasks,
769
+ planTaskCount,
770
+ planMilestoneCount,
771
+ planParseError,
772
+ missingFromState,
773
+ percentComplete,
774
+ nextMilestone,
775
+ nextTask,
776
+ progressSummary,
777
+ incompleteMilestones,
778
+ incompleteTasks,
779
+ };
780
+ }
781
+
782
+ /**
783
+ * Verify if a project is actually complete
784
+ * Returns true only if ALL milestones and ALL tasks are marked complete,
785
+ * AND the plan file doesn't have more tasks than the state
786
+ *
787
+ * @param projectDir - The project root directory
788
+ * @returns True if project is genuinely complete
789
+ */
790
+ export async function verifyProjectCompletion(projectDir: string): Promise<{
791
+ isComplete: boolean;
792
+ reason?: string;
793
+ progress: ProjectProgressAnalysis;
794
+ }> {
795
+ const progress = await analyzeProjectProgress(projectDir);
796
+
797
+ // Not complete if plan has more tasks than state
798
+ if (progress.planMismatch) {
799
+ return {
800
+ isComplete: false,
801
+ reason: `Plan mismatch: plan has ${progress.planTaskCount} tasks but state only has ${progress.totalTasks}. ` +
802
+ `${progress.missingFromState.length} tasks from plan are missing.`,
803
+ progress,
804
+ };
805
+ }
806
+
807
+ if (progress.isActuallyComplete) {
808
+ return {
809
+ isComplete: true,
810
+ progress,
811
+ };
812
+ }
813
+
814
+ // Build reason for incompleteness
815
+ let reason: string;
816
+ if (progress.totalTasks === 0) {
817
+ reason = 'No tasks defined in the project';
818
+ } else if (progress.statusMismatch) {
819
+ reason = `Status mismatch: ${progress.completedTasks}/${progress.totalTasks} tasks actually complete`;
820
+ } else {
821
+ reason = `${progress.pendingTasks + progress.inProgressTasks + progress.failedTasks} tasks remaining`;
822
+ }
823
+
824
+ return {
825
+ isComplete: false,
826
+ reason,
827
+ progress,
828
+ };
829
+ }
830
+
831
+ /**
832
+ * Reset a falsely-completed project to allow resume
833
+ * Used when status='complete' but work is incomplete
834
+ *
835
+ * @param projectDir - The project root directory
836
+ * @returns Updated state
837
+ */
838
+ export async function resetIncompleteProject(projectDir: string): Promise<ProjectState> {
839
+ const verification = await verifyProjectCompletion(projectDir);
840
+
841
+ if (verification.isComplete) {
842
+ // Actually complete, no reset needed
843
+ return loadProject(projectDir);
844
+ }
845
+
846
+ const progress = verification.progress;
847
+
848
+ // Determine the correct phase
849
+ let newPhase: 'plan' | 'execution' | 'complete' = 'execution';
850
+ let newStatus: 'pending' | 'in-progress' | 'complete' | 'failed' = 'in-progress';
851
+
852
+ if (progress.totalTasks === 0) {
853
+ // No tasks - go back to plan phase
854
+ newPhase = 'plan';
855
+ newStatus = 'pending';
856
+ } else if (progress.completedTasks > 0) {
857
+ // Some work done - continue execution
858
+ newPhase = 'execution';
859
+ newStatus = 'in-progress';
860
+ } else {
861
+ // No work done yet
862
+ newPhase = 'execution';
863
+ newStatus = 'pending';
864
+ }
865
+
866
+ // Reset any failed tasks to pending for retry
867
+ const current = await loadProject(projectDir);
868
+ const updatedMilestones = current.milestones.map(m => ({
869
+ ...m,
870
+ // Reset milestone status if it was incorrectly marked complete
871
+ status: m.tasks.every(t => t.status === 'complete')
872
+ ? 'complete' as const
873
+ : m.tasks.some(t => t.status === 'complete' || t.status === 'in-progress')
874
+ ? 'in-progress' as const
875
+ : 'pending' as const,
876
+ tasks: m.tasks.map(t =>
877
+ t.status === 'failed'
878
+ ? { ...t, status: 'pending' as const, error: undefined }
879
+ : t
880
+ ),
881
+ }));
882
+
883
+ return updateState(projectDir, {
884
+ phase: newPhase,
885
+ status: newStatus,
886
+ milestones: updatedMilestones,
887
+ error: undefined,
888
+ });
889
+ }
890
+
891
+ /**
892
+ * Code quality check result
893
+ */
894
+ export interface CodeQualityCheckResult {
895
+ passed: boolean;
896
+ totalSourceFiles: number;
897
+ totalLinesOfCode: number;
898
+ hasMainEntryPoint: boolean;
899
+ mainEntryPointLines: number;
900
+ hasTests: boolean;
901
+ testFileCount: number;
902
+ hasSubstantiveCode: boolean;
903
+ warnings: string[];
904
+ issues: string[];
905
+ }
906
+
907
+ /**
908
+ * Verify that a project has actual, substantive code implementation
909
+ * Not just scaffolding or "Hello World"
910
+ *
911
+ * @param projectDir - The project root directory
912
+ * @returns Code quality verification result
913
+ */
914
+ export async function verifyCodeImplementation(projectDir: string): Promise<CodeQualityCheckResult> {
915
+ const warnings: string[] = [];
916
+ const issues: string[] = [];
917
+
918
+ let totalSourceFiles = 0;
919
+ let totalLinesOfCode = 0;
920
+ let hasMainEntryPoint = false;
921
+ let mainEntryPointLines = 0;
922
+ let hasTests = false;
923
+ let testFileCount = 0;
924
+ let hasSubstantiveCode = false;
925
+
926
+ // Load project to get language
927
+ const state = await loadProject(projectDir);
928
+ const language = state.language;
929
+
930
+ // Define file extensions for the language
931
+ const sourceExtensions = language === 'python'
932
+ ? ['.py']
933
+ : ['.ts', '.tsx', '.js', '.jsx'];
934
+
935
+ const testPatterns = language === 'python'
936
+ ? ['test_', '_test.py', 'tests.py']
937
+ : ['.test.ts', '.test.tsx', '.spec.ts', '.spec.tsx', '.test.js', '.test.jsx'];
938
+
939
+ // Main entry point names
940
+ const mainEntryNames = language === 'python'
941
+ ? ['main.py', '__main__.py', 'app.py', 'index.py']
942
+ : ['index.ts', 'index.tsx', 'main.ts', 'app.ts', 'index.js', 'main.js'];
943
+
944
+ // Directories to check for source code
945
+ const srcDirs = ['src', 'lib', 'app', '.'];
946
+
947
+ try {
948
+ // Count source files and lines
949
+ for (const srcDir of srcDirs) {
950
+ const dirPath = path.join(projectDir, srcDir);
951
+
952
+ try {
953
+ await fs.access(dirPath);
954
+ } catch {
955
+ continue; // Directory doesn't exist
956
+ }
957
+
958
+ // Recursively find source files
959
+ const files = await findSourceFiles(dirPath, sourceExtensions);
960
+
961
+ for (const file of files) {
962
+ const relativePath = path.relative(projectDir, file);
963
+
964
+ // Skip test files when counting source code
965
+ const isTestFile = testPatterns.some(pattern =>
966
+ path.basename(file).includes(pattern) ||
967
+ relativePath.includes('/test/') ||
968
+ relativePath.includes('/tests/')
969
+ );
970
+
971
+ if (isTestFile) {
972
+ testFileCount++;
973
+ hasTests = true;
974
+ continue;
975
+ }
976
+
977
+ totalSourceFiles++;
978
+
979
+ // Read file and count lines
980
+ const content = await fs.readFile(file, 'utf-8');
981
+ const lines = content.split('\n').filter(line =>
982
+ line.trim() && !line.trim().startsWith('#') && !line.trim().startsWith('//')
983
+ );
984
+ totalLinesOfCode += lines.length;
985
+
986
+ // Check if this is a main entry point
987
+ const basename = path.basename(file);
988
+ if (mainEntryNames.includes(basename)) {
989
+ hasMainEntryPoint = true;
990
+ mainEntryPointLines = lines.length;
991
+
992
+ // Check if main entry point has substantive code
993
+ if (lines.length < 10) {
994
+ issues.push(`Main entry point (${basename}) has only ${lines.length} lines - too minimal`);
995
+ } else if (lines.length < 30) {
996
+ warnings.push(`Main entry point (${basename}) has only ${lines.length} lines - may be incomplete`);
997
+ }
998
+
999
+ // Check for "hello world" only implementations
1000
+ const contentLower = content.toLowerCase();
1001
+ if (contentLower.includes('hello') &&
1002
+ (contentLower.includes('world') || contentLower.includes('from')) &&
1003
+ lines.length < 20) {
1004
+ issues.push(`Main entry point appears to be just a "Hello World" placeholder`);
1005
+ }
1006
+ }
1007
+ }
1008
+ }
1009
+
1010
+ // Determine if code is substantive
1011
+ hasSubstantiveCode = totalLinesOfCode >= 50 && totalSourceFiles >= 2;
1012
+
1013
+ // Add warnings/issues based on findings
1014
+ if (totalSourceFiles === 0) {
1015
+ issues.push('No source files found');
1016
+ } else if (totalSourceFiles === 1) {
1017
+ warnings.push('Only 1 source file found - project may be incomplete');
1018
+ }
1019
+
1020
+ if (totalLinesOfCode < 30) {
1021
+ issues.push(`Only ${totalLinesOfCode} lines of code - project appears to be scaffolding only`);
1022
+ } else if (totalLinesOfCode < 100) {
1023
+ warnings.push(`Only ${totalLinesOfCode} lines of code - project may be minimal`);
1024
+ }
1025
+
1026
+ if (!hasMainEntryPoint) {
1027
+ warnings.push('No main entry point file found');
1028
+ }
1029
+
1030
+ if (!hasTests) {
1031
+ warnings.push('No test files found');
1032
+ }
1033
+
1034
+ // Check if project has expected structure based on plan
1035
+ if (state.plan) {
1036
+ // Look for expected files mentioned in plan
1037
+ const planLower = state.plan.toLowerCase();
1038
+ const expectedPatterns = [
1039
+ { pattern: /api|endpoint|route/i, type: 'API endpoints' },
1040
+ { pattern: /database|model|schema/i, type: 'database models' },
1041
+ { pattern: /component|view|template/i, type: 'UI components' },
1042
+ { pattern: /service|controller|handler/i, type: 'business logic' },
1043
+ ];
1044
+
1045
+ for (const { pattern, type } of expectedPatterns) {
1046
+ if (pattern.test(planLower) && totalSourceFiles < 3) {
1047
+ warnings.push(`Plan mentions ${type} but only ${totalSourceFiles} source files found`);
1048
+ }
1049
+ }
1050
+ }
1051
+
1052
+ const passed = issues.length === 0 && hasSubstantiveCode;
1053
+
1054
+ return {
1055
+ passed,
1056
+ totalSourceFiles,
1057
+ totalLinesOfCode,
1058
+ hasMainEntryPoint,
1059
+ mainEntryPointLines,
1060
+ hasTests,
1061
+ testFileCount,
1062
+ hasSubstantiveCode,
1063
+ warnings,
1064
+ issues,
1065
+ };
1066
+ } catch (error) {
1067
+ return {
1068
+ passed: false,
1069
+ totalSourceFiles: 0,
1070
+ totalLinesOfCode: 0,
1071
+ hasMainEntryPoint: false,
1072
+ mainEntryPointLines: 0,
1073
+ hasTests: false,
1074
+ testFileCount: 0,
1075
+ hasSubstantiveCode: false,
1076
+ warnings,
1077
+ issues: [`Error verifying code: ${error instanceof Error ? error.message : 'Unknown error'}`],
1078
+ };
1079
+ }
1080
+ }
1081
+
1082
+ /**
1083
+ * Recursively find source files with given extensions
1084
+ */
1085
+ async function findSourceFiles(dir: string, extensions: string[]): Promise<string[]> {
1086
+ const files: string[] = [];
1087
+
1088
+ try {
1089
+ const entries = await fs.readdir(dir, { withFileTypes: true });
1090
+
1091
+ for (const entry of entries) {
1092
+ const fullPath = path.join(dir, entry.name);
1093
+
1094
+ // Skip node_modules, __pycache__, .git, etc.
1095
+ if (entry.isDirectory()) {
1096
+ if (['node_modules', '__pycache__', '.git', '.venv', 'venv', 'dist', 'build'].includes(entry.name)) {
1097
+ continue;
1098
+ }
1099
+ const subFiles = await findSourceFiles(fullPath, extensions);
1100
+ files.push(...subFiles);
1101
+ } else if (entry.isFile()) {
1102
+ if (extensions.some(ext => entry.name.endsWith(ext))) {
1103
+ files.push(fullPath);
1104
+ }
1105
+ }
1106
+ }
1107
+ } catch {
1108
+ // Directory access error - ignore
1109
+ }
1110
+
1111
+ return files;
1112
+ }
1113
+
1114
+ /**
1115
+ * Comprehensive project verification that checks both task completion AND code quality
1116
+ *
1117
+ * @param projectDir - The project root directory
1118
+ * @returns Full verification result
1119
+ */
1120
+ export async function comprehensiveProjectVerification(projectDir: string): Promise<{
1121
+ isGenuinelyComplete: boolean;
1122
+ taskVerification: Awaited<ReturnType<typeof verifyProjectCompletion>>;
1123
+ codeVerification: CodeQualityCheckResult;
1124
+ summary: string;
1125
+ }> {
1126
+ const taskVerification = await verifyProjectCompletion(projectDir);
1127
+ const codeVerification = await verifyCodeImplementation(projectDir);
1128
+
1129
+ const isGenuinelyComplete = taskVerification.isComplete && codeVerification.passed;
1130
+
1131
+ // Build summary
1132
+ const summaryLines: string[] = [];
1133
+
1134
+ summaryLines.push(`Task Status: ${taskVerification.isComplete ? 'COMPLETE' : 'INCOMPLETE'}`);
1135
+ summaryLines.push(` - ${taskVerification.progress.completedTasks}/${taskVerification.progress.totalTasks} tasks complete`);
1136
+
1137
+ summaryLines.push(`Code Quality: ${codeVerification.passed ? 'PASSED' : 'FAILED'}`);
1138
+ summaryLines.push(` - ${codeVerification.totalSourceFiles} source files, ${codeVerification.totalLinesOfCode} lines of code`);
1139
+ summaryLines.push(` - Tests: ${codeVerification.hasTests ? `${codeVerification.testFileCount} test files` : 'None'}`);
1140
+
1141
+ if (codeVerification.issues.length > 0) {
1142
+ summaryLines.push('Issues:');
1143
+ for (const issue of codeVerification.issues) {
1144
+ summaryLines.push(` - ${issue}`);
1145
+ }
1146
+ }
1147
+
1148
+ if (codeVerification.warnings.length > 0) {
1149
+ summaryLines.push('Warnings:');
1150
+ for (const warning of codeVerification.warnings) {
1151
+ summaryLines.push(` - ${warning}`);
1152
+ }
1153
+ }
1154
+
1155
+ summaryLines.push(`Overall: ${isGenuinelyComplete ? 'PROJECT GENUINELY COMPLETE' : 'PROJECT INCOMPLETE'}`);
1156
+
1157
+ return {
1158
+ isGenuinelyComplete,
1159
+ taskVerification,
1160
+ codeVerification,
1161
+ summary: summaryLines.join('\n'),
1162
+ };
1163
+ }