ai-sdlc 0.2.0-alpha.3 → 0.2.0-alpha.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/README.md +53 -1058
  2. package/dist/agents/implementation.d.ts +62 -0
  3. package/dist/agents/implementation.d.ts.map +1 -1
  4. package/dist/agents/implementation.js +494 -90
  5. package/dist/agents/implementation.js.map +1 -1
  6. package/dist/agents/planning.d.ts.map +1 -1
  7. package/dist/agents/planning.js +22 -3
  8. package/dist/agents/planning.js.map +1 -1
  9. package/dist/agents/refinement.d.ts.map +1 -1
  10. package/dist/agents/refinement.js +22 -3
  11. package/dist/agents/refinement.js.map +1 -1
  12. package/dist/agents/research.d.ts +85 -1
  13. package/dist/agents/research.d.ts.map +1 -1
  14. package/dist/agents/research.js +506 -16
  15. package/dist/agents/research.js.map +1 -1
  16. package/dist/agents/review.d.ts +79 -2
  17. package/dist/agents/review.d.ts.map +1 -1
  18. package/dist/agents/review.js +568 -68
  19. package/dist/agents/review.js.map +1 -1
  20. package/dist/agents/rework.d.ts.map +1 -1
  21. package/dist/agents/rework.js +22 -3
  22. package/dist/agents/rework.js.map +1 -1
  23. package/dist/agents/state-assessor.d.ts +3 -3
  24. package/dist/agents/state-assessor.d.ts.map +1 -1
  25. package/dist/agents/state-assessor.js +6 -6
  26. package/dist/agents/state-assessor.js.map +1 -1
  27. package/dist/agents/test-pattern-detector.d.ts +49 -0
  28. package/dist/agents/test-pattern-detector.d.ts.map +1 -0
  29. package/dist/agents/test-pattern-detector.js +273 -0
  30. package/dist/agents/test-pattern-detector.js.map +1 -0
  31. package/dist/agents/verification.d.ts +11 -0
  32. package/dist/agents/verification.d.ts.map +1 -1
  33. package/dist/agents/verification.js +74 -1
  34. package/dist/agents/verification.js.map +1 -1
  35. package/dist/cli/commands/migrate.js +1 -1
  36. package/dist/cli/commands/migrate.js.map +1 -1
  37. package/dist/cli/commands.d.ts +59 -3
  38. package/dist/cli/commands.d.ts.map +1 -1
  39. package/dist/cli/commands.js +1053 -217
  40. package/dist/cli/commands.js.map +1 -1
  41. package/dist/cli/daemon.d.ts.map +1 -1
  42. package/dist/cli/daemon.js +40 -10
  43. package/dist/cli/daemon.js.map +1 -1
  44. package/dist/cli/formatting.js +1 -1
  45. package/dist/cli/formatting.js.map +1 -1
  46. package/dist/cli/runner.d.ts.map +1 -1
  47. package/dist/cli/runner.js +51 -20
  48. package/dist/cli/runner.js.map +1 -1
  49. package/dist/core/auth.d.ts +51 -2
  50. package/dist/core/auth.d.ts.map +1 -1
  51. package/dist/core/auth.js +267 -7
  52. package/dist/core/auth.js.map +1 -1
  53. package/dist/core/client.d.ts +6 -0
  54. package/dist/core/client.d.ts.map +1 -1
  55. package/dist/core/client.js +68 -4
  56. package/dist/core/client.js.map +1 -1
  57. package/dist/core/config.d.ts +36 -1
  58. package/dist/core/config.d.ts.map +1 -1
  59. package/dist/core/config.js +162 -1
  60. package/dist/core/config.js.map +1 -1
  61. package/dist/core/conflict-detector.d.ts +108 -0
  62. package/dist/core/conflict-detector.d.ts.map +1 -0
  63. package/dist/core/conflict-detector.js +413 -0
  64. package/dist/core/conflict-detector.js.map +1 -0
  65. package/dist/core/git-utils.d.ts +28 -0
  66. package/dist/core/git-utils.d.ts.map +1 -0
  67. package/dist/core/git-utils.js +146 -0
  68. package/dist/core/git-utils.js.map +1 -0
  69. package/dist/core/index.d.ts +17 -0
  70. package/dist/core/index.d.ts.map +1 -0
  71. package/dist/core/index.js +17 -0
  72. package/dist/core/index.js.map +1 -0
  73. package/dist/core/kanban.d.ts +1 -6
  74. package/dist/core/kanban.d.ts.map +1 -1
  75. package/dist/core/kanban.js +10 -49
  76. package/dist/core/kanban.js.map +1 -1
  77. package/dist/core/logger.d.ts +92 -0
  78. package/dist/core/logger.d.ts.map +1 -0
  79. package/dist/core/logger.js +221 -0
  80. package/dist/core/logger.js.map +1 -0
  81. package/dist/core/story-logger.d.ts +102 -0
  82. package/dist/core/story-logger.d.ts.map +1 -0
  83. package/dist/core/story-logger.js +265 -0
  84. package/dist/core/story-logger.js.map +1 -0
  85. package/dist/core/story.d.ts +133 -20
  86. package/dist/core/story.d.ts.map +1 -1
  87. package/dist/core/story.js +426 -61
  88. package/dist/core/story.js.map +1 -1
  89. package/dist/core/workflow-state.d.ts +45 -6
  90. package/dist/core/workflow-state.d.ts.map +1 -1
  91. package/dist/core/workflow-state.js +201 -12
  92. package/dist/core/workflow-state.js.map +1 -1
  93. package/dist/core/worktree.d.ts +77 -0
  94. package/dist/core/worktree.d.ts.map +1 -0
  95. package/dist/core/worktree.js +246 -0
  96. package/dist/core/worktree.js.map +1 -0
  97. package/dist/index.js +135 -5
  98. package/dist/index.js.map +1 -1
  99. package/dist/types/index.d.ts +163 -1
  100. package/dist/types/index.d.ts.map +1 -1
  101. package/dist/types/index.js +1 -0
  102. package/dist/types/index.js.map +1 -1
  103. package/package.json +3 -1
@@ -1,9 +1,11 @@
1
- import { execSync, spawn } from 'child_process';
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
- // Check for uncommitted changes
164
- const status = execSync('git status --porcelain', {
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 (!status.trim()) {
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
- execSync('git add -A', { cwd: workingDir, stdio: 'pipe' });
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
- stdio: 'pipe'
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
- execSync('git rev-parse --git-dir', { cwd: workingDir, stdio: 'pipe' });
534
- // Create and checkout branch (or checkout if exists)
535
- try {
536
- execSync(`git checkout -b ${branchName}`, { cwd: workingDir, stdio: 'pipe' });
537
- changesMade.push(`Created branch: ${branchName}`);
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
- catch {
540
- // Branch might already exist
541
- try {
542
- execSync(`git checkout ${branchName}`, { cwd: workingDir, stdio: 'pipe' });
543
- changesMade.push(`Checked out existing branch: ${branchName}`);
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
- catch {
546
- // Not a git repo or other error, continue without branching
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
- changesMade.push('Running verification before marking complete...');
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: `Implementation blocked: ${verification.failures} test(s) failing. Fix tests before completing.`,
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
- updateStoryField(tddResult.story, 'implementation_complete', true);
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
- let prompt = `Implement this story based on the plan:
608
-
609
- Title: ${story.frontmatter.title}
610
-
611
- Story content:
612
- ${story.content}`;
613
- if (options.reworkContext) {
614
- prompt += `
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
- // Add implementation notes to the story
640
- const implementationNotes = `
641
- ### Implementation Notes (${new Date().toISOString().split('T')[0]})
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: error instanceof Error ? error.message : String(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