ai-sdlc 0.2.0-alpha.1 → 0.2.0-alpha.10
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/dist/agents/implementation.d.ts +69 -0
- package/dist/agents/implementation.d.ts.map +1 -1
- package/dist/agents/implementation.js +481 -79
- package/dist/agents/implementation.js.map +1 -1
- package/dist/agents/refinement.js +4 -4
- package/dist/agents/refinement.js.map +1 -1
- package/dist/agents/review.d.ts +12 -0
- package/dist/agents/review.d.ts.map +1 -1
- package/dist/agents/review.js +98 -7
- package/dist/agents/review.js.map +1 -1
- package/dist/cli/commands.d.ts +16 -0
- package/dist/cli/commands.d.ts.map +1 -1
- package/dist/cli/commands.js +556 -158
- package/dist/cli/commands.js.map +1 -1
- package/dist/cli/daemon.d.ts.map +1 -1
- package/dist/cli/daemon.js +20 -7
- 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 +34 -15
- package/dist/cli/runner.js.map +1 -1
- package/dist/core/auth.d.ts +8 -2
- package/dist/core/auth.d.ts.map +1 -1
- package/dist/core/auth.js +163 -7
- package/dist/core/auth.js.map +1 -1
- package/dist/core/client.d.ts.map +1 -1
- package/dist/core/client.js +11 -1
- package/dist/core/client.js.map +1 -1
- package/dist/core/config.d.ts +32 -1
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +135 -1
- package/dist/core/config.js.map +1 -1
- package/dist/core/git-utils.d.ts +19 -0
- package/dist/core/git-utils.d.ts.map +1 -0
- package/dist/core/git-utils.js +95 -0
- package/dist/core/git-utils.js.map +1 -0
- package/dist/core/kanban.d.ts +0 -5
- package/dist/core/kanban.d.ts.map +1 -1
- package/dist/core/kanban.js +7 -46
- package/dist/core/kanban.js.map +1 -1
- package/dist/core/story.d.ts +56 -2
- package/dist/core/story.d.ts.map +1 -1
- package/dist/core/story.js +227 -29
- package/dist/core/story.js.map +1 -1
- package/dist/core/worktree.d.ts +68 -0
- package/dist/core/worktree.d.ts.map +1 -0
- package/dist/core/worktree.js +195 -0
- package/dist/core/worktree.js.map +1 -0
- package/dist/index.js +29 -2
- package/dist/index.js.map +1 -1
- package/dist/types/index.d.ts +40 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { spawn, spawnSync } from 'child_process';
|
|
2
2
|
import path from 'path';
|
|
3
|
-
import { parseStory, writeStory,
|
|
3
|
+
import { parseStory, writeStory, updateStoryStatus, updateStoryField, resetImplementationRetryCount, incrementImplementationRetryCount, getEffectiveMaxImplementationRetries, } from '../core/story.js';
|
|
4
4
|
import { runAgentQuery } from '../core/client.js';
|
|
5
5
|
import { loadConfig, DEFAULT_TDD_CONFIG } from '../core/config.js';
|
|
6
6
|
import { verifyImplementation } from './verification.js';
|
|
7
|
+
import { createHash } from 'crypto';
|
|
7
8
|
export const TDD_SYSTEM_PROMPT = `You are practicing strict Test-Driven Development.
|
|
8
9
|
|
|
9
10
|
Your workflow MUST follow this exact cycle:
|
|
@@ -38,7 +39,6 @@ When implementing:
|
|
|
38
39
|
5. Update the plan checkboxes as you complete tasks
|
|
39
40
|
6. Do NOT create temporary files, shell scripts, or documentation files - keep all notes in the story file
|
|
40
41
|
7. Follow the Testing Pyramid: prioritize unit tests (colocated with source, e.g., src/foo.test.ts), then integration tests (in tests/integration/)
|
|
41
|
-
8. Do NOT commit changes - that happens in the review phase
|
|
42
42
|
|
|
43
43
|
CRITICAL RULES ABOUT TESTS:
|
|
44
44
|
- Test updates are PART of implementation, not a separate phase
|
|
@@ -142,6 +142,66 @@ export async function runAllTests(workingDir, testTimeout) {
|
|
|
142
142
|
});
|
|
143
143
|
});
|
|
144
144
|
}
|
|
145
|
+
/**
|
|
146
|
+
* Security: Escape shell arguments for safe use in commands
|
|
147
|
+
* For use with execSync when shell execution is required
|
|
148
|
+
*/
|
|
149
|
+
function escapeShellArg(arg) {
|
|
150
|
+
// Replace single quotes with '\'' and wrap in single quotes
|
|
151
|
+
return `'${arg.replace(/'/g, "'\\''")}'`;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Commit changes if all tests pass
|
|
155
|
+
*
|
|
156
|
+
* @param workingDir - The working directory for git operations
|
|
157
|
+
* @param message - The commit message
|
|
158
|
+
* @param testTimeout - Timeout for running tests
|
|
159
|
+
* @param testRunner - Optional test runner for dependency injection (defaults to runAllTests)
|
|
160
|
+
* @returns Object indicating whether commit was made and reason if not
|
|
161
|
+
*/
|
|
162
|
+
export async function commitIfAllTestsPass(workingDir, message, testTimeout, testRunner = runAllTests) {
|
|
163
|
+
try {
|
|
164
|
+
// Security: Validate working directory before use
|
|
165
|
+
validateWorkingDir(workingDir);
|
|
166
|
+
// Check for uncommitted changes using spawn with shell: false
|
|
167
|
+
const statusResult = spawnSync('git', ['status', '--porcelain'], {
|
|
168
|
+
cwd: workingDir,
|
|
169
|
+
encoding: 'utf-8',
|
|
170
|
+
shell: false,
|
|
171
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
172
|
+
});
|
|
173
|
+
if (statusResult.status !== 0 || !statusResult.stdout || !statusResult.stdout.trim()) {
|
|
174
|
+
return { committed: false, reason: 'nothing to commit' };
|
|
175
|
+
}
|
|
176
|
+
// Run FULL test suite
|
|
177
|
+
const testResult = await testRunner(workingDir, testTimeout);
|
|
178
|
+
if (!testResult.passed) {
|
|
179
|
+
return { committed: false, reason: 'tests failed' };
|
|
180
|
+
}
|
|
181
|
+
// Commit changes using spawn with shell: false
|
|
182
|
+
const addResult = spawnSync('git', ['add', '-A'], {
|
|
183
|
+
cwd: workingDir,
|
|
184
|
+
shell: false,
|
|
185
|
+
stdio: 'pipe',
|
|
186
|
+
});
|
|
187
|
+
if (addResult.status !== 0) {
|
|
188
|
+
throw new Error(`git add failed: ${addResult.stderr}`);
|
|
189
|
+
}
|
|
190
|
+
const commitResult = spawnSync('git', ['commit', '-m', message], {
|
|
191
|
+
cwd: workingDir,
|
|
192
|
+
shell: false,
|
|
193
|
+
stdio: 'pipe',
|
|
194
|
+
});
|
|
195
|
+
if (commitResult.status !== 0) {
|
|
196
|
+
throw new Error(`git commit failed: ${commitResult.stderr}`);
|
|
197
|
+
}
|
|
198
|
+
return { committed: true };
|
|
199
|
+
}
|
|
200
|
+
catch (error) {
|
|
201
|
+
// Re-throw git errors for caller to handle
|
|
202
|
+
throw error;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
145
205
|
/**
|
|
146
206
|
* Extract test file path from agent output
|
|
147
207
|
*/
|
|
@@ -425,6 +485,20 @@ export async function runTDDImplementation(story, sdlcRoot, options = {}) {
|
|
|
425
485
|
};
|
|
426
486
|
}
|
|
427
487
|
changesMade.push('REFACTOR: All tests still pass');
|
|
488
|
+
// Commit changes after successful TDD cycle
|
|
489
|
+
try {
|
|
490
|
+
const commitResult = await commitIfAllTestsPass(workingDir, `feat(${story.slug}): TDD cycle ${cycleNumber} - ${redResult.testName}`, testTimeout, allTests);
|
|
491
|
+
if (commitResult.committed) {
|
|
492
|
+
changesMade.push(`Committed: TDD cycle ${cycleNumber} - ${redResult.testName}`);
|
|
493
|
+
}
|
|
494
|
+
else {
|
|
495
|
+
changesMade.push(`Skipped commit: ${commitResult.reason}`);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
catch (error) {
|
|
499
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
500
|
+
changesMade.push(`Commit warning: ${errorMsg} (continuing implementation)`);
|
|
501
|
+
}
|
|
428
502
|
// Record the completed cycle
|
|
429
503
|
const cycle = recordTDDCycle(cycleNumber, redResult, greenResult, refactorResult);
|
|
430
504
|
// Update story with cycle history
|
|
@@ -457,6 +531,189 @@ export async function runTDDImplementation(story, sdlcRoot, options = {}) {
|
|
|
457
531
|
error: `TDD: Maximum cycles (${tddConfig.maxCycles}) reached without completing all acceptance criteria.`,
|
|
458
532
|
};
|
|
459
533
|
}
|
|
534
|
+
/**
|
|
535
|
+
* Attempt implementation with retry logic
|
|
536
|
+
*
|
|
537
|
+
* Runs the implementation loop, retrying on test failures up to maxRetries times.
|
|
538
|
+
* Includes no-change detection to exit early if the agent makes no progress.
|
|
539
|
+
*
|
|
540
|
+
* @param options Retry attempt options
|
|
541
|
+
* @param changesMade Array to track changes (mutated in place)
|
|
542
|
+
* @returns AgentResult with success/failure status
|
|
543
|
+
*/
|
|
544
|
+
export async function attemptImplementationWithRetries(options, changesMade) {
|
|
545
|
+
const { story, storyPath, workingDir, maxRetries, reworkContext, onProgress } = options;
|
|
546
|
+
let attemptNumber = 0;
|
|
547
|
+
let lastVerification = null;
|
|
548
|
+
let lastDiffHash = ''; // Initialize to empty string, will capture after first failure
|
|
549
|
+
const attemptHistory = [];
|
|
550
|
+
while (attemptNumber <= maxRetries) {
|
|
551
|
+
attemptNumber++;
|
|
552
|
+
let prompt = `Implement this story based on the plan:
|
|
553
|
+
|
|
554
|
+
Title: ${story.frontmatter.title}
|
|
555
|
+
|
|
556
|
+
Story content:
|
|
557
|
+
${story.content}`;
|
|
558
|
+
if (reworkContext) {
|
|
559
|
+
prompt += `
|
|
560
|
+
|
|
561
|
+
---
|
|
562
|
+
${reworkContext}
|
|
563
|
+
---
|
|
564
|
+
|
|
565
|
+
IMPORTANT: This is a refinement iteration. The previous implementation did not pass review.
|
|
566
|
+
You MUST fix all the issues listed above. Pay special attention to blocker and critical
|
|
567
|
+
severity issues - these must be resolved. Review the specific feedback and make targeted fixes.`;
|
|
568
|
+
}
|
|
569
|
+
// Add retry context if this is a retry attempt
|
|
570
|
+
if (attemptNumber > 1 && lastVerification) {
|
|
571
|
+
prompt += '\n\n' + buildRetryPrompt(lastVerification.testsOutput, lastVerification.buildOutput, attemptNumber, maxRetries);
|
|
572
|
+
}
|
|
573
|
+
else {
|
|
574
|
+
prompt += `
|
|
575
|
+
|
|
576
|
+
Execute the implementation plan. For each task:
|
|
577
|
+
1. Read relevant existing files
|
|
578
|
+
2. Make necessary code changes
|
|
579
|
+
3. Write tests if applicable
|
|
580
|
+
4. Verify the changes work
|
|
581
|
+
|
|
582
|
+
Use the available tools to read files, write code, and run commands as needed.`;
|
|
583
|
+
}
|
|
584
|
+
// Send progress callback for all attempts (not just retries)
|
|
585
|
+
if (onProgress) {
|
|
586
|
+
if (attemptNumber === 1) {
|
|
587
|
+
onProgress({ type: 'assistant_message', content: `Starting implementation attempt 1/${maxRetries + 1}...` });
|
|
588
|
+
}
|
|
589
|
+
else {
|
|
590
|
+
onProgress({ type: 'assistant_message', content: `Analyzing test failures, retrying implementation (${attemptNumber - 1}/${maxRetries})...` });
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
const implementationResult = await runAgentQuery({
|
|
594
|
+
prompt,
|
|
595
|
+
systemPrompt: IMPLEMENTATION_SYSTEM_PROMPT,
|
|
596
|
+
workingDirectory: workingDir,
|
|
597
|
+
onProgress,
|
|
598
|
+
});
|
|
599
|
+
// Add implementation notes to the story
|
|
600
|
+
const notePrefix = attemptNumber > 1 ? `Implementation Notes - Retry ${attemptNumber - 1}` : 'Implementation Notes';
|
|
601
|
+
const implementationNotes = `
|
|
602
|
+
### ${notePrefix} (${new Date().toISOString().split('T')[0]})
|
|
603
|
+
|
|
604
|
+
${implementationResult}
|
|
605
|
+
`;
|
|
606
|
+
// Append to story content
|
|
607
|
+
const updatedStory = parseStory(storyPath);
|
|
608
|
+
updatedStory.content += '\n\n' + implementationNotes;
|
|
609
|
+
writeStory(updatedStory);
|
|
610
|
+
changesMade.push(attemptNumber > 1 ? `Added retry ${attemptNumber - 1} notes` : 'Added implementation notes');
|
|
611
|
+
changesMade.push('Running verification before marking complete...');
|
|
612
|
+
const verification = await verifyImplementation(updatedStory, workingDir);
|
|
613
|
+
updateStoryField(updatedStory, 'last_test_run', {
|
|
614
|
+
passed: verification.passed,
|
|
615
|
+
failures: verification.failures,
|
|
616
|
+
timestamp: verification.timestamp,
|
|
617
|
+
});
|
|
618
|
+
if (verification.passed) {
|
|
619
|
+
// Success! Reset retry count and return success
|
|
620
|
+
resetImplementationRetryCount(updatedStory);
|
|
621
|
+
changesMade.push('Verification passed - implementation successful');
|
|
622
|
+
// Send success progress callback
|
|
623
|
+
if (onProgress) {
|
|
624
|
+
onProgress({ type: 'assistant_message', content: `Implementation succeeded on attempt ${attemptNumber}` });
|
|
625
|
+
}
|
|
626
|
+
return {
|
|
627
|
+
success: true,
|
|
628
|
+
story: parseStory(storyPath),
|
|
629
|
+
changesMade,
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
// Verification failed - check for retry conditions
|
|
633
|
+
lastVerification = verification;
|
|
634
|
+
// Capture current diff hash for no-change detection
|
|
635
|
+
const currentDiffHash = captureCurrentDiffHash(workingDir);
|
|
636
|
+
// Track retry attempt
|
|
637
|
+
incrementImplementationRetryCount(updatedStory);
|
|
638
|
+
// Extract first 100 chars of test and build output for history
|
|
639
|
+
const testSnippet = verification.testsOutput.substring(0, 100).replace(/\n/g, ' ');
|
|
640
|
+
const buildSnippet = verification.buildOutput.substring(0, 100).replace(/\n/g, ' ');
|
|
641
|
+
// Determine if there are build failures (build output contains error indicators)
|
|
642
|
+
const hasBuildErrors = verification.buildOutput &&
|
|
643
|
+
(verification.buildOutput.includes('error') ||
|
|
644
|
+
verification.buildOutput.includes('Error') ||
|
|
645
|
+
verification.buildOutput.includes('failed'));
|
|
646
|
+
const buildFailures = hasBuildErrors ? 1 : 0;
|
|
647
|
+
// Record this attempt in history with both test and build failures
|
|
648
|
+
attemptHistory.push({
|
|
649
|
+
attempt: attemptNumber,
|
|
650
|
+
testFailures: verification.failures,
|
|
651
|
+
buildFailures,
|
|
652
|
+
testSnippet,
|
|
653
|
+
buildSnippet,
|
|
654
|
+
});
|
|
655
|
+
// Add structured retry entry to changes array
|
|
656
|
+
if (attemptNumber > 1) {
|
|
657
|
+
changesMade.push(`Implementation retry ${attemptNumber - 1}/${maxRetries}: ${verification.failures} test(s) failing`);
|
|
658
|
+
}
|
|
659
|
+
else {
|
|
660
|
+
changesMade.push(`Attempt ${attemptNumber}: ${verification.failures} test(s) failing`);
|
|
661
|
+
}
|
|
662
|
+
// Check for no-change scenario (agent made no progress)
|
|
663
|
+
// Only check after first failure (attemptNumber > 1)
|
|
664
|
+
// Check this BEFORE max retries to fail fast on identical changes
|
|
665
|
+
if (attemptNumber > 1 && lastDiffHash && lastDiffHash === currentDiffHash) {
|
|
666
|
+
return {
|
|
667
|
+
success: false,
|
|
668
|
+
story: parseStory(storyPath),
|
|
669
|
+
changesMade,
|
|
670
|
+
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)}`,
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
// Check if we've reached max retries
|
|
674
|
+
if (attemptHistory.length > maxRetries) {
|
|
675
|
+
const attemptSummary = attemptHistory
|
|
676
|
+
.map((a) => {
|
|
677
|
+
const parts = [];
|
|
678
|
+
if (a.testFailures > 0) {
|
|
679
|
+
parts.push(`${a.testFailures} test(s)`);
|
|
680
|
+
}
|
|
681
|
+
if (a.buildFailures > 0) {
|
|
682
|
+
parts.push(`${a.buildFailures} build error(s)`);
|
|
683
|
+
}
|
|
684
|
+
const errors = parts.length > 0 ? parts.join(', ') : 'verification failed';
|
|
685
|
+
const snippets = [];
|
|
686
|
+
if (a.testSnippet && a.testSnippet.trim()) {
|
|
687
|
+
snippets.push(`[test: ${a.testSnippet}]`);
|
|
688
|
+
}
|
|
689
|
+
if (a.buildSnippet && a.buildSnippet.trim()) {
|
|
690
|
+
snippets.push(`[build: ${a.buildSnippet}]`);
|
|
691
|
+
}
|
|
692
|
+
const snippetText = snippets.length > 0 ? ` - ${snippets.join(' ')}` : '';
|
|
693
|
+
return ` Attempt ${a.attempt}: ${errors}${snippetText}`;
|
|
694
|
+
})
|
|
695
|
+
.join('\n');
|
|
696
|
+
return {
|
|
697
|
+
success: false,
|
|
698
|
+
story: parseStory(storyPath),
|
|
699
|
+
changesMade,
|
|
700
|
+
error: `Implementation blocked after ${attemptNumber} attempts:\n${attemptSummary}\n\nLast test output:\n${truncateTestOutput(verification.testsOutput, 5000)}`,
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
lastDiffHash = currentDiffHash;
|
|
704
|
+
// Continue to next retry attempt - send progress update
|
|
705
|
+
if (onProgress) {
|
|
706
|
+
onProgress({ type: 'assistant_message', content: `Retry ${attemptNumber} failed: ${verification.failures} test(s) failing, attempting retry ${attemptNumber + 1}...` });
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
// If we exit the loop without returning, all retries exhausted (shouldn't normally reach here)
|
|
710
|
+
return {
|
|
711
|
+
success: false,
|
|
712
|
+
story: parseStory(storyPath),
|
|
713
|
+
changesMade,
|
|
714
|
+
error: 'Implementation failed: All retry attempts exhausted without resolution.',
|
|
715
|
+
};
|
|
716
|
+
}
|
|
460
717
|
/**
|
|
461
718
|
* Implementation Agent
|
|
462
719
|
*
|
|
@@ -468,38 +725,60 @@ export async function runImplementationAgent(storyPath, sdlcRoot, options = {})
|
|
|
468
725
|
const changesMade = [];
|
|
469
726
|
const workingDir = path.dirname(sdlcRoot);
|
|
470
727
|
try {
|
|
728
|
+
// Security: Validate working directory before git operations
|
|
729
|
+
validateWorkingDir(workingDir);
|
|
471
730
|
// Create a feature branch for this story
|
|
472
731
|
const branchName = `ai-sdlc/${story.slug}`;
|
|
732
|
+
// Security: Validate branch name before use
|
|
733
|
+
validateBranchName(branchName);
|
|
473
734
|
try {
|
|
474
|
-
// Check if we're in a git repo
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
735
|
+
// Check if we're in a git repo using spawn with shell: false
|
|
736
|
+
const revParseResult = spawnSync('git', ['rev-parse', '--git-dir'], {
|
|
737
|
+
cwd: workingDir,
|
|
738
|
+
shell: false,
|
|
739
|
+
stdio: 'pipe',
|
|
740
|
+
});
|
|
741
|
+
if (revParseResult.status !== 0) {
|
|
742
|
+
changesMade.push('No git repo detected, skipping branch creation');
|
|
480
743
|
}
|
|
481
|
-
|
|
482
|
-
//
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
744
|
+
else {
|
|
745
|
+
// Create and checkout branch (or checkout if exists) using spawn with shell: false
|
|
746
|
+
const checkoutNewResult = spawnSync('git', ['checkout', '-b', branchName], {
|
|
747
|
+
cwd: workingDir,
|
|
748
|
+
shell: false,
|
|
749
|
+
stdio: 'pipe',
|
|
750
|
+
});
|
|
751
|
+
if (checkoutNewResult.status === 0) {
|
|
752
|
+
changesMade.push(`Created branch: ${branchName}`);
|
|
486
753
|
}
|
|
487
|
-
|
|
488
|
-
//
|
|
754
|
+
else {
|
|
755
|
+
// Branch might already exist, try to checkout
|
|
756
|
+
const checkoutResult = spawnSync('git', ['checkout', branchName], {
|
|
757
|
+
cwd: workingDir,
|
|
758
|
+
shell: false,
|
|
759
|
+
stdio: 'pipe',
|
|
760
|
+
});
|
|
761
|
+
if (checkoutResult.status === 0) {
|
|
762
|
+
changesMade.push(`Checked out existing branch: ${branchName}`);
|
|
763
|
+
}
|
|
764
|
+
else {
|
|
765
|
+
// Not a git repo or other error, continue without branching
|
|
766
|
+
changesMade.push('Failed to create or checkout branch, continuing without branching');
|
|
767
|
+
}
|
|
489
768
|
}
|
|
769
|
+
// Update story with branch info
|
|
770
|
+
updateStoryField(story, 'branch', branchName);
|
|
490
771
|
}
|
|
491
|
-
// Update story with branch info
|
|
492
|
-
updateStoryField(story, 'branch', branchName);
|
|
493
772
|
}
|
|
494
773
|
catch {
|
|
495
774
|
// Not a git repo, continue without branching
|
|
496
775
|
changesMade.push('No git repo detected, skipping branch creation');
|
|
497
776
|
}
|
|
498
|
-
//
|
|
777
|
+
// Update status to in-progress if not already there
|
|
499
778
|
if (story.frontmatter.status !== 'in-progress') {
|
|
500
|
-
story =
|
|
779
|
+
story = updateStoryStatus(story, 'in-progress');
|
|
501
780
|
currentStoryPath = story.path;
|
|
502
|
-
changesMade.push('
|
|
781
|
+
changesMade.push('Updated status to in-progress');
|
|
503
782
|
}
|
|
504
783
|
// Check if TDD is enabled for this story
|
|
505
784
|
const config = loadConfig(workingDir);
|
|
@@ -513,7 +792,8 @@ export async function runImplementationAgent(storyPath, sdlcRoot, options = {})
|
|
|
513
792
|
// Merge changes
|
|
514
793
|
changesMade.push(...tddResult.changesMade);
|
|
515
794
|
if (tddResult.success) {
|
|
516
|
-
|
|
795
|
+
// TDD completed all cycles - now verify with retry support
|
|
796
|
+
changesMade.push('Running final verification...');
|
|
517
797
|
const verification = await verifyImplementation(tddResult.story, workingDir);
|
|
518
798
|
updateStoryField(tddResult.story, 'last_test_run', {
|
|
519
799
|
passed: verification.passed,
|
|
@@ -521,13 +801,18 @@ export async function runImplementationAgent(storyPath, sdlcRoot, options = {})
|
|
|
521
801
|
timestamp: verification.timestamp,
|
|
522
802
|
});
|
|
523
803
|
if (!verification.passed) {
|
|
804
|
+
// TDD final verification failed - this is unexpected since TDD should ensure all tests pass
|
|
805
|
+
// Reset retry count since this is the first failure at this stage
|
|
806
|
+
resetImplementationRetryCount(tddResult.story);
|
|
524
807
|
return {
|
|
525
808
|
success: false,
|
|
526
809
|
story: parseStory(currentStoryPath),
|
|
527
810
|
changesMade,
|
|
528
|
-
error: `
|
|
811
|
+
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)}`,
|
|
529
812
|
};
|
|
530
813
|
}
|
|
814
|
+
// Success - reset retry count
|
|
815
|
+
resetImplementationRetryCount(tddResult.story);
|
|
531
816
|
updateStoryField(tddResult.story, 'implementation_complete', true);
|
|
532
817
|
changesMade.push('Marked implementation_complete: true');
|
|
533
818
|
return {
|
|
@@ -545,64 +830,37 @@ export async function runImplementationAgent(storyPath, sdlcRoot, options = {})
|
|
|
545
830
|
};
|
|
546
831
|
}
|
|
547
832
|
}
|
|
548
|
-
// Standard implementation (non-TDD mode)
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
---
|
|
559
|
-
${options.reworkContext}
|
|
560
|
-
---
|
|
561
|
-
|
|
562
|
-
IMPORTANT: This is a refinement iteration. The previous implementation did not pass review.
|
|
563
|
-
You MUST fix all the issues listed above. Pay special attention to blocker and critical
|
|
564
|
-
severity issues - these must be resolved. Review the specific feedback and make targeted fixes.`;
|
|
565
|
-
}
|
|
566
|
-
prompt += `
|
|
567
|
-
|
|
568
|
-
Execute the implementation plan. For each task:
|
|
569
|
-
1. Read relevant existing files
|
|
570
|
-
2. Make necessary code changes
|
|
571
|
-
3. Write tests if applicable
|
|
572
|
-
4. Verify the changes work
|
|
573
|
-
|
|
574
|
-
Use the available tools to read files, write code, and run commands as needed.`;
|
|
575
|
-
const implementationResult = await runAgentQuery({
|
|
576
|
-
prompt,
|
|
577
|
-
systemPrompt: IMPLEMENTATION_SYSTEM_PROMPT,
|
|
578
|
-
workingDirectory: workingDir,
|
|
833
|
+
// Standard implementation (non-TDD mode) with retry logic
|
|
834
|
+
// Use per-story override if set, otherwise config default (capped at upper bound)
|
|
835
|
+
const maxRetries = getEffectiveMaxImplementationRetries(story, config);
|
|
836
|
+
// Use extracted retry function for better testability
|
|
837
|
+
const retryResult = await attemptImplementationWithRetries({
|
|
838
|
+
story,
|
|
839
|
+
storyPath: currentStoryPath,
|
|
840
|
+
workingDir,
|
|
841
|
+
maxRetries,
|
|
842
|
+
reworkContext: options.reworkContext,
|
|
579
843
|
onProgress: options.onProgress,
|
|
580
|
-
});
|
|
581
|
-
//
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
`;
|
|
587
|
-
// Append to story content
|
|
844
|
+
}, changesMade);
|
|
845
|
+
// If retry loop failed, return the failure result
|
|
846
|
+
if (!retryResult.success) {
|
|
847
|
+
return retryResult;
|
|
848
|
+
}
|
|
849
|
+
// If we get here, verification passed
|
|
588
850
|
const updatedStory = parseStory(currentStoryPath);
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
story: parseStory(currentStoryPath),
|
|
603
|
-
changesMade,
|
|
604
|
-
error: `Implementation blocked: ${verification.failures} test(s) failing. Fix tests before completing.`,
|
|
605
|
-
};
|
|
851
|
+
// Commit changes after successful standard implementation
|
|
852
|
+
try {
|
|
853
|
+
const commitResult = await commitIfAllTestsPass(workingDir, `feat(${story.slug}): ${story.frontmatter.title}`, config.timeouts?.testTimeout || 300000);
|
|
854
|
+
if (commitResult.committed) {
|
|
855
|
+
changesMade.push(`Committed: ${story.frontmatter.title}`);
|
|
856
|
+
}
|
|
857
|
+
else {
|
|
858
|
+
changesMade.push(`Skipped commit: ${commitResult.reason}`);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
catch (error) {
|
|
862
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
863
|
+
changesMade.push(`Commit warning: ${errorMsg} (continuing implementation)`);
|
|
606
864
|
}
|
|
607
865
|
updateStoryField(updatedStory, 'implementation_complete', true);
|
|
608
866
|
changesMade.push('Marked implementation_complete: true');
|
|
@@ -621,4 +879,148 @@ ${implementationResult}
|
|
|
621
879
|
};
|
|
622
880
|
}
|
|
623
881
|
}
|
|
882
|
+
/**
|
|
883
|
+
* Validate working directory path for safety
|
|
884
|
+
* @param workingDir The working directory path to validate
|
|
885
|
+
* @throws Error if path contains shell metacharacters or traversal attempts
|
|
886
|
+
*/
|
|
887
|
+
function validateWorkingDir(workingDir) {
|
|
888
|
+
// Check for shell metacharacters that could be used in command injection
|
|
889
|
+
if (/[;&|`$()<>]/.test(workingDir)) {
|
|
890
|
+
throw new Error('Invalid working directory: contains shell metacharacters');
|
|
891
|
+
}
|
|
892
|
+
// Prevent path traversal attempts
|
|
893
|
+
const normalizedPath = path.normalize(workingDir);
|
|
894
|
+
if (normalizedPath.includes('..')) {
|
|
895
|
+
throw new Error('Invalid working directory: path traversal attempt detected');
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
/**
|
|
899
|
+
* Validate branch name for safety
|
|
900
|
+
* @param branchName The branch name to validate
|
|
901
|
+
* @throws Error if branch name contains invalid characters
|
|
902
|
+
*/
|
|
903
|
+
function validateBranchName(branchName) {
|
|
904
|
+
// Git branch names must match safe pattern (alphanumeric, dash, slash, underscore)
|
|
905
|
+
if (!/^[a-zA-Z0-9_/-]+$/.test(branchName)) {
|
|
906
|
+
throw new Error('Invalid branch name: contains unsafe characters');
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
/**
|
|
910
|
+
* Capture the current git diff hash for no-change detection
|
|
911
|
+
* @param workingDir The working directory
|
|
912
|
+
* @returns SHA256 hash of git diff HEAD
|
|
913
|
+
*/
|
|
914
|
+
export function captureCurrentDiffHash(workingDir) {
|
|
915
|
+
try {
|
|
916
|
+
// Security: Validate working directory before use
|
|
917
|
+
validateWorkingDir(workingDir);
|
|
918
|
+
// Use spawnSync with shell: false to prevent command injection
|
|
919
|
+
const result = spawnSync('git', ['diff', 'HEAD'], {
|
|
920
|
+
cwd: workingDir,
|
|
921
|
+
shell: false,
|
|
922
|
+
encoding: 'utf-8',
|
|
923
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
924
|
+
});
|
|
925
|
+
if (result.status === 0 && result.stdout) {
|
|
926
|
+
return createHash('sha256').update(result.stdout).digest('hex');
|
|
927
|
+
}
|
|
928
|
+
// Git command failed, return empty hash
|
|
929
|
+
return '';
|
|
930
|
+
}
|
|
931
|
+
catch (error) {
|
|
932
|
+
// If validation fails or git command fails, return empty hash
|
|
933
|
+
return '';
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
/**
|
|
937
|
+
* Check if changes have occurred since last diff hash
|
|
938
|
+
* @param previousHash Previous diff hash
|
|
939
|
+
* @param currentHash Current diff hash
|
|
940
|
+
* @returns True if changes occurred (hashes are different)
|
|
941
|
+
*/
|
|
942
|
+
export function hasChangesOccurred(previousHash, currentHash) {
|
|
943
|
+
return previousHash !== currentHash;
|
|
944
|
+
}
|
|
945
|
+
/**
|
|
946
|
+
* Sanitize test output to remove ANSI escape sequences and potential injection patterns
|
|
947
|
+
* @param output Test output string
|
|
948
|
+
* @returns Sanitized output
|
|
949
|
+
*/
|
|
950
|
+
export function sanitizeTestOutput(output) {
|
|
951
|
+
if (!output)
|
|
952
|
+
return '';
|
|
953
|
+
let sanitized = output
|
|
954
|
+
// Remove ANSI CSI sequences (SGR parameters - colors, styles)
|
|
955
|
+
.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, '')
|
|
956
|
+
// Remove ANSI DCS sequences (Device Control String)
|
|
957
|
+
.replace(/\x1BP[^\x1B]*\x1B\\/g, '')
|
|
958
|
+
// Remove ANSI PM sequences (Privacy Message)
|
|
959
|
+
.replace(/\x1B\^[^\x1B]*\x1B\\/g, '')
|
|
960
|
+
// Remove ANSI OSC sequences (Operating System Command) - terminated by BEL or ST
|
|
961
|
+
.replace(/\x1B\][^\x07\x1B]*(\x07|\x1B\\)/g, '')
|
|
962
|
+
// Remove any remaining standalone escape characters
|
|
963
|
+
.replace(/\x1B/g, '')
|
|
964
|
+
// Remove other control characters except newline, tab, carriage return
|
|
965
|
+
.replace(/[\x00-\x08\x0B-\x0C\x0E-\x1F\x7F-\x9F]/g, '');
|
|
966
|
+
return sanitized;
|
|
967
|
+
}
|
|
968
|
+
/**
|
|
969
|
+
* Truncate test output to prevent overwhelming the LLM
|
|
970
|
+
* @param output Test output string
|
|
971
|
+
* @param maxLength Maximum length (default 5000 chars)
|
|
972
|
+
* @returns Truncated and sanitized output with notice if truncated
|
|
973
|
+
*/
|
|
974
|
+
export function truncateTestOutput(output, maxLength = 5000) {
|
|
975
|
+
if (!output)
|
|
976
|
+
return '';
|
|
977
|
+
// First sanitize to remove ANSI and control characters
|
|
978
|
+
const sanitized = sanitizeTestOutput(output);
|
|
979
|
+
if (sanitized.length <= maxLength) {
|
|
980
|
+
return sanitized;
|
|
981
|
+
}
|
|
982
|
+
const truncated = sanitized.substring(0, maxLength);
|
|
983
|
+
return truncated + `\n\n[Output truncated. Showing first ${maxLength} characters of ${sanitized.length} total.]`;
|
|
984
|
+
}
|
|
985
|
+
/**
|
|
986
|
+
* Build retry prompt for implementation agent
|
|
987
|
+
* @param testOutput Test failure output
|
|
988
|
+
* @param attemptNumber Current attempt number (1-indexed)
|
|
989
|
+
* @param maxRetries Maximum number of retries
|
|
990
|
+
* @returns Prompt string for retry attempt
|
|
991
|
+
*/
|
|
992
|
+
export function buildRetryPrompt(testOutput, buildOutput, attemptNumber, maxRetries) {
|
|
993
|
+
const truncatedTestOutput = truncateTestOutput(testOutput);
|
|
994
|
+
const truncatedBuildOutput = truncateTestOutput(buildOutput);
|
|
995
|
+
let prompt = `CRITICAL: Tests are failing. You attempted implementation but verification failed.
|
|
996
|
+
|
|
997
|
+
This is retry attempt ${attemptNumber} of ${maxRetries}. Previous attempts failed with similar errors.
|
|
998
|
+
|
|
999
|
+
`;
|
|
1000
|
+
if (buildOutput && buildOutput.trim().length > 0) {
|
|
1001
|
+
prompt += `Build Output:
|
|
1002
|
+
\`\`\`
|
|
1003
|
+
${truncatedBuildOutput}
|
|
1004
|
+
\`\`\`
|
|
1005
|
+
|
|
1006
|
+
`;
|
|
1007
|
+
}
|
|
1008
|
+
if (testOutput && testOutput.trim().length > 0) {
|
|
1009
|
+
prompt += `Test Output:
|
|
1010
|
+
\`\`\`
|
|
1011
|
+
${truncatedTestOutput}
|
|
1012
|
+
\`\`\`
|
|
1013
|
+
|
|
1014
|
+
`;
|
|
1015
|
+
}
|
|
1016
|
+
prompt += `Your task:
|
|
1017
|
+
1. ANALYZE the test/build output above - what is actually failing?
|
|
1018
|
+
2. Compare EXPECTED vs ACTUAL results in the errors
|
|
1019
|
+
3. Identify the root cause in your implementation code
|
|
1020
|
+
4. Fix ONLY the production code (do NOT modify tests unless they're clearly wrong)
|
|
1021
|
+
5. Re-run verification
|
|
1022
|
+
|
|
1023
|
+
Focus on fixing the specific failures shown above.`;
|
|
1024
|
+
return prompt;
|
|
1025
|
+
}
|
|
624
1026
|
//# sourceMappingURL=implementation.js.map
|