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.
- package/README.md +521 -125
- package/dist/adapters/claude.d.ts +16 -4
- package/dist/adapters/claude.d.ts.map +1 -1
- package/dist/adapters/claude.js +679 -33
- package/dist/adapters/claude.js.map +1 -1
- package/dist/adapters/gemini.d.ts +55 -0
- package/dist/adapters/gemini.d.ts.map +1 -0
- package/dist/adapters/gemini.js +318 -0
- package/dist/adapters/gemini.js.map +1 -0
- package/dist/adapters/openai.d.ts.map +1 -1
- package/dist/adapters/openai.js +41 -7
- package/dist/adapters/openai.js.map +1 -1
- package/dist/auth/claude.d.ts +11 -9
- package/dist/auth/claude.d.ts.map +1 -1
- package/dist/auth/claude.js +107 -71
- package/dist/auth/claude.js.map +1 -1
- package/dist/auth/gemini.d.ts +58 -0
- package/dist/auth/gemini.d.ts.map +1 -0
- package/dist/auth/gemini.js +172 -0
- package/dist/auth/gemini.js.map +1 -0
- package/dist/auth/index.d.ts +11 -7
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +23 -5
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/keychain.d.ts +20 -7
- package/dist/auth/keychain.d.ts.map +1 -1
- package/dist/auth/keychain.js +85 -29
- package/dist/auth/keychain.js.map +1 -1
- package/dist/auth/openai.d.ts +2 -2
- package/dist/auth/openai.d.ts.map +1 -1
- package/dist/auth/openai.js +30 -32
- package/dist/auth/openai.js.map +1 -1
- package/dist/cli/interactive.d.ts.map +1 -1
- package/dist/cli/interactive.js +1151 -110
- package/dist/cli/interactive.js.map +1 -1
- package/dist/config/defaults.d.ts +6 -1
- package/dist/config/defaults.d.ts.map +1 -1
- package/dist/config/defaults.js +10 -2
- package/dist/config/defaults.js.map +1 -1
- package/dist/config/index.d.ts +10 -0
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +19 -0
- package/dist/config/index.js.map +1 -1
- package/dist/config/schema.d.ts +20 -0
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +7 -0
- package/dist/config/schema.js.map +1 -1
- package/dist/generators/python.d.ts.map +1 -1
- package/dist/generators/python.js +1 -0
- package/dist/generators/python.js.map +1 -1
- package/dist/generators/typescript.d.ts.map +1 -1
- package/dist/generators/typescript.js +1 -0
- package/dist/generators/typescript.js.map +1 -1
- package/dist/state/index.d.ts +108 -0
- package/dist/state/index.d.ts.map +1 -1
- package/dist/state/index.js +551 -4
- package/dist/state/index.js.map +1 -1
- package/dist/state/registry.d.ts +52 -0
- package/dist/state/registry.d.ts.map +1 -0
- package/dist/state/registry.js +215 -0
- package/dist/state/registry.js.map +1 -0
- package/dist/types/cli.d.ts +4 -0
- package/dist/types/cli.d.ts.map +1 -1
- package/dist/types/cli.js.map +1 -1
- package/dist/types/consensus.d.ts +69 -4
- package/dist/types/consensus.d.ts.map +1 -1
- package/dist/types/consensus.js +24 -3
- package/dist/types/consensus.js.map +1 -1
- package/dist/types/workflow.d.ts +55 -0
- package/dist/types/workflow.d.ts.map +1 -1
- package/dist/types/workflow.js +16 -0
- package/dist/types/workflow.js.map +1 -1
- package/dist/workflow/auto-fix.d.ts +45 -0
- package/dist/workflow/auto-fix.d.ts.map +1 -0
- package/dist/workflow/auto-fix.js +274 -0
- package/dist/workflow/auto-fix.js.map +1 -0
- package/dist/workflow/consensus.d.ts +44 -2
- package/dist/workflow/consensus.d.ts.map +1 -1
- package/dist/workflow/consensus.js +565 -17
- package/dist/workflow/consensus.js.map +1 -1
- package/dist/workflow/execution-mode.d.ts +10 -4
- package/dist/workflow/execution-mode.d.ts.map +1 -1
- package/dist/workflow/execution-mode.js +547 -58
- package/dist/workflow/execution-mode.js.map +1 -1
- package/dist/workflow/index.d.ts +14 -2
- package/dist/workflow/index.d.ts.map +1 -1
- package/dist/workflow/index.js +69 -6
- package/dist/workflow/index.js.map +1 -1
- package/dist/workflow/milestone-workflow.d.ts +34 -0
- package/dist/workflow/milestone-workflow.d.ts.map +1 -0
- package/dist/workflow/milestone-workflow.js +414 -0
- package/dist/workflow/milestone-workflow.js.map +1 -0
- package/dist/workflow/plan-mode.d.ts +14 -1
- package/dist/workflow/plan-mode.d.ts.map +1 -1
- package/dist/workflow/plan-mode.js +589 -47
- package/dist/workflow/plan-mode.js.map +1 -1
- package/dist/workflow/plan-storage.d.ts +142 -0
- package/dist/workflow/plan-storage.d.ts.map +1 -0
- package/dist/workflow/plan-storage.js +331 -0
- package/dist/workflow/plan-storage.js.map +1 -0
- package/dist/workflow/project-verification.d.ts +37 -0
- package/dist/workflow/project-verification.d.ts.map +1 -0
- package/dist/workflow/project-verification.js +381 -0
- package/dist/workflow/project-verification.js.map +1 -0
- package/dist/workflow/task-workflow.d.ts +37 -0
- package/dist/workflow/task-workflow.d.ts.map +1 -0
- package/dist/workflow/task-workflow.js +383 -0
- package/dist/workflow/task-workflow.js.map +1 -0
- package/dist/workflow/test-runner.d.ts +1 -0
- package/dist/workflow/test-runner.d.ts.map +1 -1
- package/dist/workflow/test-runner.js +9 -5
- package/dist/workflow/test-runner.js.map +1 -1
- package/dist/workflow/ui-designer.d.ts +82 -0
- package/dist/workflow/ui-designer.d.ts.map +1 -0
- package/dist/workflow/ui-designer.js +234 -0
- package/dist/workflow/ui-designer.js.map +1 -0
- package/dist/workflow/ui-setup.d.ts +58 -0
- package/dist/workflow/ui-setup.d.ts.map +1 -0
- package/dist/workflow/ui-setup.js +685 -0
- package/dist/workflow/ui-setup.js.map +1 -0
- package/dist/workflow/ui-verification.d.ts +114 -0
- package/dist/workflow/ui-verification.d.ts.map +1 -0
- package/dist/workflow/ui-verification.js +258 -0
- package/dist/workflow/ui-verification.js.map +1 -0
- package/dist/workflow/workflow-logger.d.ts +110 -0
- package/dist/workflow/workflow-logger.d.ts.map +1 -0
- package/dist/workflow/workflow-logger.js +267 -0
- package/dist/workflow/workflow-logger.js.map +1 -0
- package/package.json +2 -2
- package/src/adapters/claude.ts +815 -34
- package/src/adapters/gemini.ts +373 -0
- package/src/adapters/openai.ts +40 -7
- package/src/auth/claude.ts +120 -78
- package/src/auth/gemini.ts +207 -0
- package/src/auth/index.ts +28 -8
- package/src/auth/keychain.ts +95 -28
- package/src/auth/openai.ts +29 -36
- package/src/cli/interactive.ts +1357 -115
- package/src/config/defaults.ts +10 -2
- package/src/config/index.ts +21 -0
- package/src/config/schema.ts +7 -0
- package/src/generators/python.ts +1 -0
- package/src/generators/typescript.ts +1 -0
- package/src/state/index.ts +713 -4
- package/src/state/registry.ts +278 -0
- package/src/types/cli.ts +4 -0
- package/src/types/consensus.ts +65 -6
- package/src/types/workflow.ts +35 -0
- package/src/workflow/auto-fix.ts +340 -0
- package/src/workflow/consensus.ts +750 -16
- package/src/workflow/execution-mode.ts +673 -74
- package/src/workflow/index.ts +95 -6
- package/src/workflow/milestone-workflow.ts +576 -0
- package/src/workflow/plan-mode.ts +696 -50
- package/src/workflow/plan-storage.ts +482 -0
- package/src/workflow/project-verification.ts +471 -0
- package/src/workflow/task-workflow.ts +525 -0
- package/src/workflow/test-runner.ts +10 -5
- package/src/workflow/ui-designer.ts +337 -0
- package/src/workflow/ui-setup.ts +797 -0
- package/src/workflow/ui-verification.ts +357 -0
- package/src/workflow/workflow-logger.ts +353 -0
- package/tests/config/config.test.ts +1 -1
- package/tests/types/consensus.test.ts +3 -3
- package/tests/workflow/plan-mode.test.ts +213 -0
- package/tests/workflow/test-runner.test.ts +5 -3
package/src/state/index.ts
CHANGED
|
@@ -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
|
-
|
|
144
|
-
|
|
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
|
+
}
|