ai-sdlc 0.2.0-alpha.3 → 0.2.0-alpha.30
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 +53 -1058
- package/dist/agents/implementation.d.ts +62 -0
- package/dist/agents/implementation.d.ts.map +1 -1
- package/dist/agents/implementation.js +494 -90
- package/dist/agents/implementation.js.map +1 -1
- package/dist/agents/planning.d.ts.map +1 -1
- package/dist/agents/planning.js +22 -3
- package/dist/agents/planning.js.map +1 -1
- package/dist/agents/refinement.d.ts.map +1 -1
- package/dist/agents/refinement.js +22 -3
- package/dist/agents/refinement.js.map +1 -1
- package/dist/agents/research.d.ts +85 -1
- package/dist/agents/research.d.ts.map +1 -1
- package/dist/agents/research.js +506 -16
- package/dist/agents/research.js.map +1 -1
- package/dist/agents/review.d.ts +79 -2
- package/dist/agents/review.d.ts.map +1 -1
- package/dist/agents/review.js +568 -68
- package/dist/agents/review.js.map +1 -1
- package/dist/agents/rework.d.ts.map +1 -1
- package/dist/agents/rework.js +22 -3
- package/dist/agents/rework.js.map +1 -1
- package/dist/agents/state-assessor.d.ts +3 -3
- package/dist/agents/state-assessor.d.ts.map +1 -1
- package/dist/agents/state-assessor.js +6 -6
- package/dist/agents/state-assessor.js.map +1 -1
- package/dist/agents/test-pattern-detector.d.ts +49 -0
- package/dist/agents/test-pattern-detector.d.ts.map +1 -0
- package/dist/agents/test-pattern-detector.js +273 -0
- package/dist/agents/test-pattern-detector.js.map +1 -0
- package/dist/agents/verification.d.ts +11 -0
- package/dist/agents/verification.d.ts.map +1 -1
- package/dist/agents/verification.js +74 -1
- package/dist/agents/verification.js.map +1 -1
- package/dist/cli/commands/migrate.js +1 -1
- package/dist/cli/commands/migrate.js.map +1 -1
- package/dist/cli/commands.d.ts +59 -3
- package/dist/cli/commands.d.ts.map +1 -1
- package/dist/cli/commands.js +1053 -217
- package/dist/cli/commands.js.map +1 -1
- package/dist/cli/daemon.d.ts.map +1 -1
- package/dist/cli/daemon.js +40 -10
- package/dist/cli/daemon.js.map +1 -1
- package/dist/cli/formatting.js +1 -1
- package/dist/cli/formatting.js.map +1 -1
- package/dist/cli/runner.d.ts.map +1 -1
- package/dist/cli/runner.js +51 -20
- package/dist/cli/runner.js.map +1 -1
- package/dist/core/auth.d.ts +51 -2
- package/dist/core/auth.d.ts.map +1 -1
- package/dist/core/auth.js +267 -7
- package/dist/core/auth.js.map +1 -1
- package/dist/core/client.d.ts +6 -0
- package/dist/core/client.d.ts.map +1 -1
- package/dist/core/client.js +68 -4
- package/dist/core/client.js.map +1 -1
- package/dist/core/config.d.ts +36 -1
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +162 -1
- package/dist/core/config.js.map +1 -1
- package/dist/core/conflict-detector.d.ts +108 -0
- package/dist/core/conflict-detector.d.ts.map +1 -0
- package/dist/core/conflict-detector.js +413 -0
- package/dist/core/conflict-detector.js.map +1 -0
- package/dist/core/git-utils.d.ts +28 -0
- package/dist/core/git-utils.d.ts.map +1 -0
- package/dist/core/git-utils.js +146 -0
- package/dist/core/git-utils.js.map +1 -0
- package/dist/core/index.d.ts +17 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +17 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/kanban.d.ts +1 -6
- package/dist/core/kanban.d.ts.map +1 -1
- package/dist/core/kanban.js +10 -49
- package/dist/core/kanban.js.map +1 -1
- package/dist/core/logger.d.ts +92 -0
- package/dist/core/logger.d.ts.map +1 -0
- package/dist/core/logger.js +221 -0
- package/dist/core/logger.js.map +1 -0
- package/dist/core/story-logger.d.ts +102 -0
- package/dist/core/story-logger.d.ts.map +1 -0
- package/dist/core/story-logger.js +265 -0
- package/dist/core/story-logger.js.map +1 -0
- package/dist/core/story.d.ts +133 -20
- package/dist/core/story.d.ts.map +1 -1
- package/dist/core/story.js +426 -61
- package/dist/core/story.js.map +1 -1
- package/dist/core/workflow-state.d.ts +45 -6
- package/dist/core/workflow-state.d.ts.map +1 -1
- package/dist/core/workflow-state.js +201 -12
- package/dist/core/workflow-state.js.map +1 -1
- package/dist/core/worktree.d.ts +77 -0
- package/dist/core/worktree.d.ts.map +1 -0
- package/dist/core/worktree.js +246 -0
- package/dist/core/worktree.js.map +1 -0
- package/dist/index.js +135 -5
- package/dist/index.js.map +1 -1
- package/dist/types/index.d.ts +163 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +1 -0
- package/dist/types/index.js.map +1 -1
- package/package.json +3 -1
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { spawn, spawnSync } from 'child_process';
|
|
2
2
|
import path from 'path';
|
|
3
|
-
import { parseStory, writeStory, updateStoryStatus, updateStoryField } from '../core/story.js';
|
|
3
|
+
import { parseStory, writeStory, updateStoryStatus, updateStoryField, resetImplementationRetryCount, incrementImplementationRetryCount, getEffectiveMaxImplementationRetries, } from '../core/story.js';
|
|
4
4
|
import { runAgentQuery } from '../core/client.js';
|
|
5
|
+
import { getLogger } from '../core/logger.js';
|
|
5
6
|
import { loadConfig, DEFAULT_TDD_CONFIG } from '../core/config.js';
|
|
6
7
|
import { verifyImplementation } from './verification.js';
|
|
8
|
+
import { createHash } from 'crypto';
|
|
7
9
|
export const TDD_SYSTEM_PROMPT = `You are practicing strict Test-Driven Development.
|
|
8
10
|
|
|
9
11
|
Your workflow MUST follow this exact cycle:
|
|
@@ -160,12 +162,16 @@ function escapeShellArg(arg) {
|
|
|
160
162
|
*/
|
|
161
163
|
export async function commitIfAllTestsPass(workingDir, message, testTimeout, testRunner = runAllTests) {
|
|
162
164
|
try {
|
|
163
|
-
//
|
|
164
|
-
|
|
165
|
+
// Security: Validate working directory before use
|
|
166
|
+
validateWorkingDir(workingDir);
|
|
167
|
+
// Check for uncommitted changes using spawn with shell: false
|
|
168
|
+
const statusResult = spawnSync('git', ['status', '--porcelain'], {
|
|
165
169
|
cwd: workingDir,
|
|
166
|
-
encoding: 'utf-8'
|
|
170
|
+
encoding: 'utf-8',
|
|
171
|
+
shell: false,
|
|
172
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
167
173
|
});
|
|
168
|
-
if (!
|
|
174
|
+
if (statusResult.status !== 0 || !statusResult.stdout || !statusResult.stdout.trim()) {
|
|
169
175
|
return { committed: false, reason: 'nothing to commit' };
|
|
170
176
|
}
|
|
171
177
|
// Run FULL test suite
|
|
@@ -173,12 +179,23 @@ export async function commitIfAllTestsPass(workingDir, message, testTimeout, tes
|
|
|
173
179
|
if (!testResult.passed) {
|
|
174
180
|
return { committed: false, reason: 'tests failed' };
|
|
175
181
|
}
|
|
176
|
-
// Commit changes
|
|
177
|
-
|
|
178
|
-
execSync(`git commit -m ${escapeShellArg(message)}`, {
|
|
182
|
+
// Commit changes using spawn with shell: false
|
|
183
|
+
const addResult = spawnSync('git', ['add', '-A'], {
|
|
179
184
|
cwd: workingDir,
|
|
180
|
-
|
|
185
|
+
shell: false,
|
|
186
|
+
stdio: 'pipe',
|
|
181
187
|
});
|
|
188
|
+
if (addResult.status !== 0) {
|
|
189
|
+
throw new Error(`git add failed: ${addResult.stderr}`);
|
|
190
|
+
}
|
|
191
|
+
const commitResult = spawnSync('git', ['commit', '-m', message], {
|
|
192
|
+
cwd: workingDir,
|
|
193
|
+
shell: false,
|
|
194
|
+
stdio: 'pipe',
|
|
195
|
+
});
|
|
196
|
+
if (commitResult.status !== 0) {
|
|
197
|
+
throw new Error(`git commit failed: ${commitResult.stderr}`);
|
|
198
|
+
}
|
|
182
199
|
return { committed: true };
|
|
183
200
|
}
|
|
184
201
|
catch (error) {
|
|
@@ -492,7 +509,7 @@ export async function runTDDImplementation(story, sdlcRoot, options = {}) {
|
|
|
492
509
|
story.frontmatter.tdd_test_history = history.slice(-100);
|
|
493
510
|
story.frontmatter.tdd_current_test = cycle;
|
|
494
511
|
// Persist the TDD cycle history to disk
|
|
495
|
-
writeStory(story);
|
|
512
|
+
await writeStory(story);
|
|
496
513
|
changesMade.push(`Completed TDD cycle ${cycleNumber}`);
|
|
497
514
|
// Check if all AC are now covered
|
|
498
515
|
// Re-read story to get latest content (agent may have updated checkboxes)
|
|
@@ -515,39 +532,250 @@ export async function runTDDImplementation(story, sdlcRoot, options = {}) {
|
|
|
515
532
|
error: `TDD: Maximum cycles (${tddConfig.maxCycles}) reached without completing all acceptance criteria.`,
|
|
516
533
|
};
|
|
517
534
|
}
|
|
535
|
+
/**
|
|
536
|
+
* Attempt implementation with retry logic
|
|
537
|
+
*
|
|
538
|
+
* Runs the implementation loop, retrying on test failures up to maxRetries times.
|
|
539
|
+
* Includes no-change detection to exit early if the agent makes no progress.
|
|
540
|
+
*
|
|
541
|
+
* @param options Retry attempt options
|
|
542
|
+
* @param changesMade Array to track changes (mutated in place)
|
|
543
|
+
* @returns AgentResult with success/failure status
|
|
544
|
+
*/
|
|
545
|
+
export async function attemptImplementationWithRetries(options, changesMade) {
|
|
546
|
+
const { story, storyPath, workingDir, maxRetries, reworkContext, onProgress } = options;
|
|
547
|
+
let attemptNumber = 0;
|
|
548
|
+
let lastVerification = null;
|
|
549
|
+
let lastDiffHash = ''; // Initialize to empty string, will capture after first failure
|
|
550
|
+
const attemptHistory = [];
|
|
551
|
+
while (attemptNumber <= maxRetries) {
|
|
552
|
+
attemptNumber++;
|
|
553
|
+
let prompt = `Implement this story based on the plan:
|
|
554
|
+
|
|
555
|
+
Title: ${story.frontmatter.title}
|
|
556
|
+
|
|
557
|
+
Story content:
|
|
558
|
+
${story.content}`;
|
|
559
|
+
if (reworkContext) {
|
|
560
|
+
prompt += `
|
|
561
|
+
|
|
562
|
+
---
|
|
563
|
+
${reworkContext}
|
|
564
|
+
---
|
|
565
|
+
|
|
566
|
+
IMPORTANT: This is a refinement iteration. The previous implementation did not pass review.
|
|
567
|
+
You MUST fix all the issues listed above. Pay special attention to blocker and critical
|
|
568
|
+
severity issues - these must be resolved. Review the specific feedback and make targeted fixes.`;
|
|
569
|
+
}
|
|
570
|
+
// Add retry context if this is a retry attempt
|
|
571
|
+
if (attemptNumber > 1 && lastVerification) {
|
|
572
|
+
prompt += '\n\n' + buildRetryPrompt(lastVerification.testsOutput, lastVerification.buildOutput, attemptNumber, maxRetries);
|
|
573
|
+
}
|
|
574
|
+
else {
|
|
575
|
+
prompt += `
|
|
576
|
+
|
|
577
|
+
Execute the implementation plan. For each task:
|
|
578
|
+
1. Read relevant existing files
|
|
579
|
+
2. Make necessary code changes
|
|
580
|
+
3. Write tests if applicable
|
|
581
|
+
4. Verify the changes work
|
|
582
|
+
|
|
583
|
+
Use the available tools to read files, write code, and run commands as needed.`;
|
|
584
|
+
}
|
|
585
|
+
// Send progress callback for all attempts (not just retries)
|
|
586
|
+
if (onProgress) {
|
|
587
|
+
if (attemptNumber === 1) {
|
|
588
|
+
onProgress({ type: 'assistant_message', content: `Starting implementation attempt 1/${maxRetries + 1}...` });
|
|
589
|
+
}
|
|
590
|
+
else {
|
|
591
|
+
onProgress({ type: 'assistant_message', content: `Analyzing test failures, retrying implementation (${attemptNumber - 1}/${maxRetries})...` });
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
const implementationResult = await runAgentQuery({
|
|
595
|
+
prompt,
|
|
596
|
+
systemPrompt: IMPLEMENTATION_SYSTEM_PROMPT,
|
|
597
|
+
workingDirectory: workingDir,
|
|
598
|
+
onProgress,
|
|
599
|
+
});
|
|
600
|
+
// Add implementation notes to the story
|
|
601
|
+
const notePrefix = attemptNumber > 1 ? `Implementation Notes - Retry ${attemptNumber - 1}` : 'Implementation Notes';
|
|
602
|
+
const implementationNotes = `
|
|
603
|
+
### ${notePrefix} (${new Date().toISOString().split('T')[0]})
|
|
604
|
+
|
|
605
|
+
${implementationResult}
|
|
606
|
+
`;
|
|
607
|
+
// Append to story content
|
|
608
|
+
const updatedStory = parseStory(storyPath);
|
|
609
|
+
updatedStory.content += '\n\n' + implementationNotes;
|
|
610
|
+
await writeStory(updatedStory);
|
|
611
|
+
changesMade.push(attemptNumber > 1 ? `Added retry ${attemptNumber - 1} notes` : 'Added implementation notes');
|
|
612
|
+
changesMade.push('Running verification before marking complete...');
|
|
613
|
+
const verification = await verifyImplementation(updatedStory, workingDir);
|
|
614
|
+
await updateStoryField(updatedStory, 'last_test_run', {
|
|
615
|
+
passed: verification.passed,
|
|
616
|
+
failures: verification.failures,
|
|
617
|
+
timestamp: verification.timestamp,
|
|
618
|
+
});
|
|
619
|
+
if (verification.passed) {
|
|
620
|
+
// Success! Reset retry count and return success
|
|
621
|
+
await resetImplementationRetryCount(updatedStory);
|
|
622
|
+
changesMade.push('Verification passed - implementation successful');
|
|
623
|
+
// Send success progress callback
|
|
624
|
+
if (onProgress) {
|
|
625
|
+
onProgress({ type: 'assistant_message', content: `Implementation succeeded on attempt ${attemptNumber}` });
|
|
626
|
+
}
|
|
627
|
+
return {
|
|
628
|
+
success: true,
|
|
629
|
+
story: parseStory(storyPath),
|
|
630
|
+
changesMade,
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
// Verification failed - check for retry conditions
|
|
634
|
+
lastVerification = verification;
|
|
635
|
+
// Capture current diff hash for no-change detection
|
|
636
|
+
const currentDiffHash = captureCurrentDiffHash(workingDir);
|
|
637
|
+
// Track retry attempt
|
|
638
|
+
await incrementImplementationRetryCount(updatedStory);
|
|
639
|
+
// Extract first 100 chars of test and build output for history
|
|
640
|
+
const testSnippet = verification.testsOutput.substring(0, 100).replace(/\n/g, ' ');
|
|
641
|
+
const buildSnippet = verification.buildOutput.substring(0, 100).replace(/\n/g, ' ');
|
|
642
|
+
// Determine if there are build failures (build output contains error indicators)
|
|
643
|
+
const hasBuildErrors = verification.buildOutput &&
|
|
644
|
+
(verification.buildOutput.includes('error') ||
|
|
645
|
+
verification.buildOutput.includes('Error') ||
|
|
646
|
+
verification.buildOutput.includes('failed'));
|
|
647
|
+
const buildFailures = hasBuildErrors ? 1 : 0;
|
|
648
|
+
// Record this attempt in history with both test and build failures
|
|
649
|
+
attemptHistory.push({
|
|
650
|
+
attempt: attemptNumber,
|
|
651
|
+
testFailures: verification.failures,
|
|
652
|
+
buildFailures,
|
|
653
|
+
testSnippet,
|
|
654
|
+
buildSnippet,
|
|
655
|
+
});
|
|
656
|
+
// Add structured retry entry to changes array
|
|
657
|
+
if (attemptNumber > 1) {
|
|
658
|
+
changesMade.push(`Implementation retry ${attemptNumber - 1}/${maxRetries}: ${verification.failures} test(s) failing`);
|
|
659
|
+
}
|
|
660
|
+
else {
|
|
661
|
+
changesMade.push(`Attempt ${attemptNumber}: ${verification.failures} test(s) failing`);
|
|
662
|
+
}
|
|
663
|
+
// Check for no-change scenario (agent made no progress)
|
|
664
|
+
// Only check after first failure (attemptNumber > 1)
|
|
665
|
+
// Check this BEFORE max retries to fail fast on identical changes
|
|
666
|
+
if (attemptNumber > 1 && lastDiffHash && lastDiffHash === currentDiffHash) {
|
|
667
|
+
return {
|
|
668
|
+
success: false,
|
|
669
|
+
story: parseStory(storyPath),
|
|
670
|
+
changesMade,
|
|
671
|
+
error: `Implementation blocked: No progress detected on retry attempt ${attemptNumber - 1}. Agent made identical changes. Stopping retries early.\n\nLast test output:\n${truncateTestOutput(verification.testsOutput, 1000)}`,
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
// Check if we've reached max retries
|
|
675
|
+
if (attemptHistory.length > maxRetries) {
|
|
676
|
+
const attemptSummary = attemptHistory
|
|
677
|
+
.map((a) => {
|
|
678
|
+
const parts = [];
|
|
679
|
+
if (a.testFailures > 0) {
|
|
680
|
+
parts.push(`${a.testFailures} test(s)`);
|
|
681
|
+
}
|
|
682
|
+
if (a.buildFailures > 0) {
|
|
683
|
+
parts.push(`${a.buildFailures} build error(s)`);
|
|
684
|
+
}
|
|
685
|
+
const errors = parts.length > 0 ? parts.join(', ') : 'verification failed';
|
|
686
|
+
const snippets = [];
|
|
687
|
+
if (a.testSnippet && a.testSnippet.trim()) {
|
|
688
|
+
snippets.push(`[test: ${a.testSnippet}]`);
|
|
689
|
+
}
|
|
690
|
+
if (a.buildSnippet && a.buildSnippet.trim()) {
|
|
691
|
+
snippets.push(`[build: ${a.buildSnippet}]`);
|
|
692
|
+
}
|
|
693
|
+
const snippetText = snippets.length > 0 ? ` - ${snippets.join(' ')}` : '';
|
|
694
|
+
return ` Attempt ${a.attempt}: ${errors}${snippetText}`;
|
|
695
|
+
})
|
|
696
|
+
.join('\n');
|
|
697
|
+
return {
|
|
698
|
+
success: false,
|
|
699
|
+
story: parseStory(storyPath),
|
|
700
|
+
changesMade,
|
|
701
|
+
error: `Implementation blocked after ${attemptNumber} attempts:\n${attemptSummary}\n\nLast test output:\n${truncateTestOutput(verification.testsOutput, 5000)}`,
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
lastDiffHash = currentDiffHash;
|
|
705
|
+
// Continue to next retry attempt - send progress update
|
|
706
|
+
if (onProgress) {
|
|
707
|
+
onProgress({ type: 'assistant_message', content: `Retry ${attemptNumber} failed: ${verification.failures} test(s) failing, attempting retry ${attemptNumber + 1}...` });
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
// If we exit the loop without returning, all retries exhausted (shouldn't normally reach here)
|
|
711
|
+
return {
|
|
712
|
+
success: false,
|
|
713
|
+
story: parseStory(storyPath),
|
|
714
|
+
changesMade,
|
|
715
|
+
error: 'Implementation failed: All retry attempts exhausted without resolution.',
|
|
716
|
+
};
|
|
717
|
+
}
|
|
518
718
|
/**
|
|
519
719
|
* Implementation Agent
|
|
520
720
|
*
|
|
521
721
|
* Executes the implementation plan, creating code changes and tests.
|
|
522
722
|
*/
|
|
523
723
|
export async function runImplementationAgent(storyPath, sdlcRoot, options = {}) {
|
|
724
|
+
const logger = getLogger();
|
|
725
|
+
const startTime = Date.now();
|
|
524
726
|
let story = parseStory(storyPath);
|
|
525
727
|
let currentStoryPath = storyPath;
|
|
526
728
|
const changesMade = [];
|
|
527
729
|
const workingDir = path.dirname(sdlcRoot);
|
|
730
|
+
logger.info('implementation', 'Starting implementation phase', {
|
|
731
|
+
storyId: story.frontmatter.id,
|
|
732
|
+
retryCount: story.frontmatter.implementation_retry_count || 0,
|
|
733
|
+
});
|
|
528
734
|
try {
|
|
735
|
+
// Security: Validate working directory before git operations
|
|
736
|
+
validateWorkingDir(workingDir);
|
|
529
737
|
// Create a feature branch for this story
|
|
530
738
|
const branchName = `ai-sdlc/${story.slug}`;
|
|
739
|
+
// Security: Validate branch name before use
|
|
740
|
+
validateBranchName(branchName);
|
|
531
741
|
try {
|
|
532
|
-
// Check if we're in a git repo
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
742
|
+
// Check if we're in a git repo using spawn with shell: false
|
|
743
|
+
const revParseResult = spawnSync('git', ['rev-parse', '--git-dir'], {
|
|
744
|
+
cwd: workingDir,
|
|
745
|
+
shell: false,
|
|
746
|
+
stdio: 'pipe',
|
|
747
|
+
});
|
|
748
|
+
if (revParseResult.status !== 0) {
|
|
749
|
+
changesMade.push('No git repo detected, skipping branch creation');
|
|
538
750
|
}
|
|
539
|
-
|
|
540
|
-
//
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
751
|
+
else {
|
|
752
|
+
// Create and checkout branch (or checkout if exists) using spawn with shell: false
|
|
753
|
+
const checkoutNewResult = spawnSync('git', ['checkout', '-b', branchName], {
|
|
754
|
+
cwd: workingDir,
|
|
755
|
+
shell: false,
|
|
756
|
+
stdio: 'pipe',
|
|
757
|
+
});
|
|
758
|
+
if (checkoutNewResult.status === 0) {
|
|
759
|
+
changesMade.push(`Created branch: ${branchName}`);
|
|
544
760
|
}
|
|
545
|
-
|
|
546
|
-
//
|
|
761
|
+
else {
|
|
762
|
+
// Branch might already exist, try to checkout
|
|
763
|
+
const checkoutResult = spawnSync('git', ['checkout', branchName], {
|
|
764
|
+
cwd: workingDir,
|
|
765
|
+
shell: false,
|
|
766
|
+
stdio: 'pipe',
|
|
767
|
+
});
|
|
768
|
+
if (checkoutResult.status === 0) {
|
|
769
|
+
changesMade.push(`Checked out existing branch: ${branchName}`);
|
|
770
|
+
}
|
|
771
|
+
else {
|
|
772
|
+
// Not a git repo or other error, continue without branching
|
|
773
|
+
changesMade.push('Failed to create or checkout branch, continuing without branching');
|
|
774
|
+
}
|
|
547
775
|
}
|
|
776
|
+
// Update story with branch info
|
|
777
|
+
await updateStoryField(story, 'branch', branchName);
|
|
548
778
|
}
|
|
549
|
-
// Update story with branch info
|
|
550
|
-
updateStoryField(story, 'branch', branchName);
|
|
551
779
|
}
|
|
552
780
|
catch {
|
|
553
781
|
// Not a git repo, continue without branching
|
|
@@ -555,7 +783,7 @@ export async function runImplementationAgent(storyPath, sdlcRoot, options = {})
|
|
|
555
783
|
}
|
|
556
784
|
// Update status to in-progress if not already there
|
|
557
785
|
if (story.frontmatter.status !== 'in-progress') {
|
|
558
|
-
story = updateStoryStatus(story, 'in-progress');
|
|
786
|
+
story = await updateStoryStatus(story, 'in-progress');
|
|
559
787
|
currentStoryPath = story.path;
|
|
560
788
|
changesMade.push('Updated status to in-progress');
|
|
561
789
|
}
|
|
@@ -571,22 +799,28 @@ export async function runImplementationAgent(storyPath, sdlcRoot, options = {})
|
|
|
571
799
|
// Merge changes
|
|
572
800
|
changesMade.push(...tddResult.changesMade);
|
|
573
801
|
if (tddResult.success) {
|
|
574
|
-
|
|
802
|
+
// TDD completed all cycles - now verify with retry support
|
|
803
|
+
changesMade.push('Running final verification...');
|
|
575
804
|
const verification = await verifyImplementation(tddResult.story, workingDir);
|
|
576
|
-
updateStoryField(tddResult.story, 'last_test_run', {
|
|
805
|
+
await updateStoryField(tddResult.story, 'last_test_run', {
|
|
577
806
|
passed: verification.passed,
|
|
578
807
|
failures: verification.failures,
|
|
579
808
|
timestamp: verification.timestamp,
|
|
580
809
|
});
|
|
581
810
|
if (!verification.passed) {
|
|
811
|
+
// TDD final verification failed - this is unexpected since TDD should ensure all tests pass
|
|
812
|
+
// Reset retry count since this is the first failure at this stage
|
|
813
|
+
await resetImplementationRetryCount(tddResult.story);
|
|
582
814
|
return {
|
|
583
815
|
success: false,
|
|
584
816
|
story: parseStory(currentStoryPath),
|
|
585
817
|
changesMade,
|
|
586
|
-
error: `
|
|
818
|
+
error: `TDD implementation blocked: ${verification.failures} test(s) failing after completing all cycles.\nThis is unexpected - TDD cycles should ensure all tests pass.\n\nTest output:\n${truncateTestOutput(verification.testsOutput, 1000)}`,
|
|
587
819
|
};
|
|
588
820
|
}
|
|
589
|
-
|
|
821
|
+
// Success - reset retry count
|
|
822
|
+
await resetImplementationRetryCount(tddResult.story);
|
|
823
|
+
await updateStoryField(tddResult.story, 'implementation_complete', true);
|
|
590
824
|
changesMade.push('Marked implementation_complete: true');
|
|
591
825
|
return {
|
|
592
826
|
success: true,
|
|
@@ -603,65 +837,24 @@ export async function runImplementationAgent(storyPath, sdlcRoot, options = {})
|
|
|
603
837
|
};
|
|
604
838
|
}
|
|
605
839
|
}
|
|
606
|
-
// Standard implementation (non-TDD mode)
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
---
|
|
617
|
-
${options.reworkContext}
|
|
618
|
-
---
|
|
619
|
-
|
|
620
|
-
IMPORTANT: This is a refinement iteration. The previous implementation did not pass review.
|
|
621
|
-
You MUST fix all the issues listed above. Pay special attention to blocker and critical
|
|
622
|
-
severity issues - these must be resolved. Review the specific feedback and make targeted fixes.`;
|
|
623
|
-
}
|
|
624
|
-
prompt += `
|
|
625
|
-
|
|
626
|
-
Execute the implementation plan. For each task:
|
|
627
|
-
1. Read relevant existing files
|
|
628
|
-
2. Make necessary code changes
|
|
629
|
-
3. Write tests if applicable
|
|
630
|
-
4. Verify the changes work
|
|
631
|
-
|
|
632
|
-
Use the available tools to read files, write code, and run commands as needed.`;
|
|
633
|
-
const implementationResult = await runAgentQuery({
|
|
634
|
-
prompt,
|
|
635
|
-
systemPrompt: IMPLEMENTATION_SYSTEM_PROMPT,
|
|
636
|
-
workingDirectory: workingDir,
|
|
840
|
+
// Standard implementation (non-TDD mode) with retry logic
|
|
841
|
+
// Use per-story override if set, otherwise config default (capped at upper bound)
|
|
842
|
+
const maxRetries = getEffectiveMaxImplementationRetries(story, config);
|
|
843
|
+
// Use extracted retry function for better testability
|
|
844
|
+
const retryResult = await attemptImplementationWithRetries({
|
|
845
|
+
story,
|
|
846
|
+
storyPath: currentStoryPath,
|
|
847
|
+
workingDir,
|
|
848
|
+
maxRetries,
|
|
849
|
+
reworkContext: options.reworkContext,
|
|
637
850
|
onProgress: options.onProgress,
|
|
638
|
-
});
|
|
639
|
-
//
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
${implementationResult}
|
|
644
|
-
`;
|
|
645
|
-
// Append to story content
|
|
646
|
-
const updatedStory = parseStory(currentStoryPath);
|
|
647
|
-
updatedStory.content += '\n\n' + implementationNotes;
|
|
648
|
-
writeStory(updatedStory);
|
|
649
|
-
changesMade.push('Added implementation notes');
|
|
650
|
-
changesMade.push('Running verification before marking complete...');
|
|
651
|
-
const verification = await verifyImplementation(updatedStory, workingDir);
|
|
652
|
-
updateStoryField(updatedStory, 'last_test_run', {
|
|
653
|
-
passed: verification.passed,
|
|
654
|
-
failures: verification.failures,
|
|
655
|
-
timestamp: verification.timestamp,
|
|
656
|
-
});
|
|
657
|
-
if (!verification.passed) {
|
|
658
|
-
return {
|
|
659
|
-
success: false,
|
|
660
|
-
story: parseStory(currentStoryPath),
|
|
661
|
-
changesMade,
|
|
662
|
-
error: `Implementation blocked: ${verification.failures} test(s) failing. Fix tests before completing.`,
|
|
663
|
-
};
|
|
851
|
+
}, changesMade);
|
|
852
|
+
// If retry loop failed, return the failure result
|
|
853
|
+
if (!retryResult.success) {
|
|
854
|
+
return retryResult;
|
|
664
855
|
}
|
|
856
|
+
// If we get here, verification passed
|
|
857
|
+
const updatedStory = parseStory(currentStoryPath);
|
|
665
858
|
// Commit changes after successful standard implementation
|
|
666
859
|
try {
|
|
667
860
|
const commitResult = await commitIfAllTestsPass(workingDir, `feat(${story.slug}): ${story.frontmatter.title}`, config.timeouts?.testTimeout || 300000);
|
|
@@ -676,8 +869,13 @@ ${implementationResult}
|
|
|
676
869
|
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
677
870
|
changesMade.push(`Commit warning: ${errorMsg} (continuing implementation)`);
|
|
678
871
|
}
|
|
679
|
-
updateStoryField(updatedStory, 'implementation_complete', true);
|
|
872
|
+
await updateStoryField(updatedStory, 'implementation_complete', true);
|
|
680
873
|
changesMade.push('Marked implementation_complete: true');
|
|
874
|
+
logger.info('implementation', 'Implementation phase complete', {
|
|
875
|
+
storyId: story.frontmatter.id,
|
|
876
|
+
durationMs: Date.now() - startTime,
|
|
877
|
+
changesCount: changesMade.length,
|
|
878
|
+
});
|
|
681
879
|
return {
|
|
682
880
|
success: true,
|
|
683
881
|
story: parseStory(currentStoryPath),
|
|
@@ -685,12 +883,218 @@ ${implementationResult}
|
|
|
685
883
|
};
|
|
686
884
|
}
|
|
687
885
|
catch (error) {
|
|
886
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
887
|
+
logger.error('implementation', 'Implementation phase failed', {
|
|
888
|
+
storyId: story.frontmatter.id,
|
|
889
|
+
durationMs: Date.now() - startTime,
|
|
890
|
+
error: errorMessage,
|
|
891
|
+
});
|
|
688
892
|
return {
|
|
689
893
|
success: false,
|
|
690
894
|
story,
|
|
691
895
|
changesMade,
|
|
692
|
-
error:
|
|
896
|
+
error: errorMessage,
|
|
693
897
|
};
|
|
694
898
|
}
|
|
695
899
|
}
|
|
900
|
+
/**
|
|
901
|
+
* Validate working directory path for safety
|
|
902
|
+
* @param workingDir The working directory path to validate
|
|
903
|
+
* @throws Error if path contains shell metacharacters or traversal attempts
|
|
904
|
+
*/
|
|
905
|
+
function validateWorkingDir(workingDir) {
|
|
906
|
+
// Check for shell metacharacters that could be used in command injection
|
|
907
|
+
if (/[;&|`$()<>]/.test(workingDir)) {
|
|
908
|
+
throw new Error('Invalid working directory: contains shell metacharacters');
|
|
909
|
+
}
|
|
910
|
+
// Prevent path traversal attempts
|
|
911
|
+
const normalizedPath = path.normalize(workingDir);
|
|
912
|
+
if (normalizedPath.includes('..')) {
|
|
913
|
+
throw new Error('Invalid working directory: path traversal attempt detected');
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
/**
|
|
917
|
+
* Validate branch name for safety
|
|
918
|
+
* @param branchName The branch name to validate
|
|
919
|
+
* @throws Error if branch name contains invalid characters
|
|
920
|
+
*/
|
|
921
|
+
function validateBranchName(branchName) {
|
|
922
|
+
// Git branch names must match safe pattern (alphanumeric, dash, slash, underscore)
|
|
923
|
+
if (!/^[a-zA-Z0-9_/-]+$/.test(branchName)) {
|
|
924
|
+
throw new Error('Invalid branch name: contains unsafe characters');
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
/**
|
|
928
|
+
* Capture the current git diff hash for no-change detection
|
|
929
|
+
* @param workingDir The working directory
|
|
930
|
+
* @returns SHA256 hash of git diff HEAD
|
|
931
|
+
*/
|
|
932
|
+
export function captureCurrentDiffHash(workingDir) {
|
|
933
|
+
try {
|
|
934
|
+
// Security: Validate working directory before use
|
|
935
|
+
validateWorkingDir(workingDir);
|
|
936
|
+
// Use spawnSync with shell: false to prevent command injection
|
|
937
|
+
const result = spawnSync('git', ['diff', 'HEAD'], {
|
|
938
|
+
cwd: workingDir,
|
|
939
|
+
shell: false,
|
|
940
|
+
encoding: 'utf-8',
|
|
941
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
942
|
+
});
|
|
943
|
+
if (result.status === 0 && result.stdout) {
|
|
944
|
+
return createHash('sha256').update(result.stdout).digest('hex');
|
|
945
|
+
}
|
|
946
|
+
// Git command failed, return empty hash
|
|
947
|
+
return '';
|
|
948
|
+
}
|
|
949
|
+
catch (error) {
|
|
950
|
+
// If validation fails or git command fails, return empty hash
|
|
951
|
+
return '';
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
/**
|
|
955
|
+
* Check if changes have occurred since last diff hash
|
|
956
|
+
* @param previousHash Previous diff hash
|
|
957
|
+
* @param currentHash Current diff hash
|
|
958
|
+
* @returns True if changes occurred (hashes are different)
|
|
959
|
+
*/
|
|
960
|
+
export function hasChangesOccurred(previousHash, currentHash) {
|
|
961
|
+
return previousHash !== currentHash;
|
|
962
|
+
}
|
|
963
|
+
/**
|
|
964
|
+
* Sanitize test output to remove ANSI escape sequences and potential injection patterns
|
|
965
|
+
* @param output Test output string
|
|
966
|
+
* @returns Sanitized output
|
|
967
|
+
*/
|
|
968
|
+
export function sanitizeTestOutput(output) {
|
|
969
|
+
if (!output)
|
|
970
|
+
return '';
|
|
971
|
+
let sanitized = output
|
|
972
|
+
// Remove ANSI CSI sequences (SGR parameters - colors, styles)
|
|
973
|
+
.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, '')
|
|
974
|
+
// Remove ANSI DCS sequences (Device Control String)
|
|
975
|
+
.replace(/\x1BP[^\x1B]*\x1B\\/g, '')
|
|
976
|
+
// Remove ANSI PM sequences (Privacy Message)
|
|
977
|
+
.replace(/\x1B\^[^\x1B]*\x1B\\/g, '')
|
|
978
|
+
// Remove ANSI OSC sequences (Operating System Command) - terminated by BEL or ST
|
|
979
|
+
.replace(/\x1B\][^\x07\x1B]*(\x07|\x1B\\)/g, '')
|
|
980
|
+
// Remove any remaining standalone escape characters
|
|
981
|
+
.replace(/\x1B/g, '')
|
|
982
|
+
// Remove other control characters except newline, tab, carriage return
|
|
983
|
+
.replace(/[\x00-\x08\x0B-\x0C\x0E-\x1F\x7F-\x9F]/g, '');
|
|
984
|
+
return sanitized;
|
|
985
|
+
}
|
|
986
|
+
/**
|
|
987
|
+
* Truncate test output to prevent overwhelming the LLM
|
|
988
|
+
* @param output Test output string
|
|
989
|
+
* @param maxLength Maximum length (default 5000 chars)
|
|
990
|
+
* @returns Truncated and sanitized output with notice if truncated
|
|
991
|
+
*/
|
|
992
|
+
export function truncateTestOutput(output, maxLength = 5000) {
|
|
993
|
+
if (!output)
|
|
994
|
+
return '';
|
|
995
|
+
// First sanitize to remove ANSI and control characters
|
|
996
|
+
const sanitized = sanitizeTestOutput(output);
|
|
997
|
+
if (sanitized.length <= maxLength) {
|
|
998
|
+
return sanitized;
|
|
999
|
+
}
|
|
1000
|
+
const truncated = sanitized.substring(0, maxLength);
|
|
1001
|
+
return truncated + `\n\n[Output truncated. Showing first ${maxLength} characters of ${sanitized.length} total.]`;
|
|
1002
|
+
}
|
|
1003
|
+
/**
|
|
1004
|
+
* Detect if errors are related to missing dependencies
|
|
1005
|
+
* Returns module names that are missing, if any
|
|
1006
|
+
*/
|
|
1007
|
+
export function detectMissingDependencies(output) {
|
|
1008
|
+
if (!output)
|
|
1009
|
+
return [];
|
|
1010
|
+
const missingModules = [];
|
|
1011
|
+
// Pattern: Cannot find module 'package-name'
|
|
1012
|
+
const cannotFindPattern = /Cannot find module ['"]([^'"]+)['"]/g;
|
|
1013
|
+
let match;
|
|
1014
|
+
while ((match = cannotFindPattern.exec(output)) !== null) {
|
|
1015
|
+
const moduleName = match[1];
|
|
1016
|
+
// Only include external packages, not relative imports
|
|
1017
|
+
if (!moduleName.startsWith('.') && !moduleName.startsWith('/')) {
|
|
1018
|
+
// Extract base package name (handle scoped packages like @types/foo)
|
|
1019
|
+
const baseName = moduleName.startsWith('@')
|
|
1020
|
+
? moduleName.split('/').slice(0, 2).join('/')
|
|
1021
|
+
: moduleName.split('/')[0];
|
|
1022
|
+
if (!missingModules.includes(baseName)) {
|
|
1023
|
+
missingModules.push(baseName);
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
// Pattern: Module not found: Error: Can't resolve 'package-name'
|
|
1028
|
+
const cantResolvePattern = /(?:Module not found|Can't resolve)[:\s]+['"]([^'"]+)['"]/g;
|
|
1029
|
+
while ((match = cantResolvePattern.exec(output)) !== null) {
|
|
1030
|
+
const moduleName = match[1];
|
|
1031
|
+
if (!moduleName.startsWith('.') && !moduleName.startsWith('/')) {
|
|
1032
|
+
const baseName = moduleName.startsWith('@')
|
|
1033
|
+
? moduleName.split('/').slice(0, 2).join('/')
|
|
1034
|
+
: moduleName.split('/')[0];
|
|
1035
|
+
if (!missingModules.includes(baseName)) {
|
|
1036
|
+
missingModules.push(baseName);
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
return missingModules;
|
|
1041
|
+
}
|
|
1042
|
+
/**
|
|
1043
|
+
* Build retry prompt for implementation agent
|
|
1044
|
+
* @param testOutput Test failure output
|
|
1045
|
+
* @param buildOutput Build output
|
|
1046
|
+
* @param attemptNumber Current attempt number (1-indexed)
|
|
1047
|
+
* @param maxRetries Maximum number of retries
|
|
1048
|
+
* @returns Prompt string for retry attempt
|
|
1049
|
+
*/
|
|
1050
|
+
export function buildRetryPrompt(testOutput, buildOutput, attemptNumber, maxRetries) {
|
|
1051
|
+
const truncatedTestOutput = truncateTestOutput(testOutput);
|
|
1052
|
+
const truncatedBuildOutput = truncateTestOutput(buildOutput);
|
|
1053
|
+
// Detect if this is a dependency issue
|
|
1054
|
+
const combinedOutput = (buildOutput || '') + '\n' + (testOutput || '');
|
|
1055
|
+
const missingDeps = detectMissingDependencies(combinedOutput);
|
|
1056
|
+
let prompt = `CRITICAL: Tests are failing. You attempted implementation but verification failed.
|
|
1057
|
+
|
|
1058
|
+
This is retry attempt ${attemptNumber} of ${maxRetries}. Previous attempts failed with similar errors.
|
|
1059
|
+
|
|
1060
|
+
`;
|
|
1061
|
+
// Add special guidance for missing dependencies
|
|
1062
|
+
if (missingDeps.length > 0) {
|
|
1063
|
+
prompt += `**DEPENDENCY ISSUE DETECTED**
|
|
1064
|
+
|
|
1065
|
+
The errors indicate missing npm packages: ${missingDeps.join(', ')}
|
|
1066
|
+
|
|
1067
|
+
This is NOT a code bug - the packages need to be installed. Before making any code changes:
|
|
1068
|
+
1. Run \`npm install ${missingDeps.join(' ')}\` to add the missing packages
|
|
1069
|
+
2. If these are type definitions, also run \`npm install -D @types/${missingDeps.filter(d => !d.startsWith('@')).join(' @types/')}\`
|
|
1070
|
+
3. Re-run the build/tests after installing
|
|
1071
|
+
|
|
1072
|
+
`;
|
|
1073
|
+
}
|
|
1074
|
+
if (buildOutput && buildOutput.trim().length > 0) {
|
|
1075
|
+
prompt += `Build Output:
|
|
1076
|
+
\`\`\`
|
|
1077
|
+
${truncatedBuildOutput}
|
|
1078
|
+
\`\`\`
|
|
1079
|
+
|
|
1080
|
+
`;
|
|
1081
|
+
}
|
|
1082
|
+
if (testOutput && testOutput.trim().length > 0) {
|
|
1083
|
+
prompt += `Test Output:
|
|
1084
|
+
\`\`\`
|
|
1085
|
+
${truncatedTestOutput}
|
|
1086
|
+
\`\`\`
|
|
1087
|
+
|
|
1088
|
+
`;
|
|
1089
|
+
}
|
|
1090
|
+
prompt += `Your task:
|
|
1091
|
+
1. ANALYZE the test/build output above - what is actually failing?
|
|
1092
|
+
2. Compare EXPECTED vs ACTUAL results in the errors
|
|
1093
|
+
3. Identify the root cause in your implementation code
|
|
1094
|
+
4. Fix ONLY the production code (do NOT modify tests unless they're clearly wrong)
|
|
1095
|
+
5. Re-run verification
|
|
1096
|
+
|
|
1097
|
+
Focus on fixing the specific failures shown above.`;
|
|
1098
|
+
return prompt;
|
|
1099
|
+
}
|
|
696
1100
|
//# sourceMappingURL=implementation.js.map
|