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

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 (43) hide show
  1. package/dist/agents/implementation.d.ts +56 -0
  2. package/dist/agents/implementation.d.ts.map +1 -1
  3. package/dist/agents/implementation.js +414 -84
  4. package/dist/agents/implementation.js.map +1 -1
  5. package/dist/agents/review.d.ts +12 -0
  6. package/dist/agents/review.d.ts.map +1 -1
  7. package/dist/agents/review.js +91 -0
  8. package/dist/agents/review.js.map +1 -1
  9. package/dist/cli/commands.d.ts.map +1 -1
  10. package/dist/cli/commands.js +17 -25
  11. package/dist/cli/commands.js.map +1 -1
  12. package/dist/cli/daemon.d.ts.map +1 -1
  13. package/dist/cli/daemon.js +20 -7
  14. package/dist/cli/daemon.js.map +1 -1
  15. package/dist/cli/formatting.js +1 -1
  16. package/dist/cli/formatting.js.map +1 -1
  17. package/dist/cli/runner.d.ts.map +1 -1
  18. package/dist/cli/runner.js +34 -15
  19. package/dist/cli/runner.js.map +1 -1
  20. package/dist/core/auth.d.ts +8 -2
  21. package/dist/core/auth.d.ts.map +1 -1
  22. package/dist/core/auth.js +163 -7
  23. package/dist/core/auth.js.map +1 -1
  24. package/dist/core/client.d.ts.map +1 -1
  25. package/dist/core/client.js +11 -1
  26. package/dist/core/client.js.map +1 -1
  27. package/dist/core/config.d.ts +13 -1
  28. package/dist/core/config.d.ts.map +1 -1
  29. package/dist/core/config.js +67 -0
  30. package/dist/core/config.js.map +1 -1
  31. package/dist/core/kanban.d.ts +0 -5
  32. package/dist/core/kanban.d.ts.map +1 -1
  33. package/dist/core/kanban.js +3 -43
  34. package/dist/core/kanban.js.map +1 -1
  35. package/dist/core/story.d.ts +56 -2
  36. package/dist/core/story.d.ts.map +1 -1
  37. package/dist/core/story.js +175 -23
  38. package/dist/core/story.js.map +1 -1
  39. package/dist/index.js +0 -0
  40. package/dist/types/index.d.ts +12 -0
  41. package/dist/types/index.d.ts.map +1 -1
  42. package/dist/types/index.js.map +1 -1
  43. package/package.json +1 -1
@@ -93,10 +93,66 @@ export declare function checkACCoverage(story: Story): boolean;
93
93
  * or the maximum number of cycles is reached.
94
94
  */
95
95
  export declare function runTDDImplementation(story: Story, sdlcRoot: string, options?: TDDImplementationOptions): Promise<AgentResult>;
96
+ /**
97
+ * Options for the retry loop
98
+ */
99
+ export interface RetryAttemptOptions {
100
+ story: Story;
101
+ storyPath: string;
102
+ workingDir: string;
103
+ maxRetries: number;
104
+ reworkContext?: string;
105
+ onProgress?: AgentProgressCallback;
106
+ }
107
+ /**
108
+ * Attempt implementation with retry logic
109
+ *
110
+ * Runs the implementation loop, retrying on test failures up to maxRetries times.
111
+ * Includes no-change detection to exit early if the agent makes no progress.
112
+ *
113
+ * @param options Retry attempt options
114
+ * @param changesMade Array to track changes (mutated in place)
115
+ * @returns AgentResult with success/failure status
116
+ */
117
+ export declare function attemptImplementationWithRetries(options: RetryAttemptOptions, changesMade: string[]): Promise<AgentResult>;
96
118
  /**
97
119
  * Implementation Agent
98
120
  *
99
121
  * Executes the implementation plan, creating code changes and tests.
100
122
  */
101
123
  export declare function runImplementationAgent(storyPath: string, sdlcRoot: string, options?: AgentOptions): Promise<AgentResult>;
124
+ /**
125
+ * Capture the current git diff hash for no-change detection
126
+ * @param workingDir The working directory
127
+ * @returns SHA256 hash of git diff HEAD
128
+ */
129
+ export declare function captureCurrentDiffHash(workingDir: string): string;
130
+ /**
131
+ * Check if changes have occurred since last diff hash
132
+ * @param previousHash Previous diff hash
133
+ * @param currentHash Current diff hash
134
+ * @returns True if changes occurred (hashes are different)
135
+ */
136
+ export declare function hasChangesOccurred(previousHash: string, currentHash: string): boolean;
137
+ /**
138
+ * Sanitize test output to remove ANSI escape sequences and potential injection patterns
139
+ * @param output Test output string
140
+ * @returns Sanitized output
141
+ */
142
+ export declare function sanitizeTestOutput(output: string): string;
143
+ /**
144
+ * Truncate test output to prevent overwhelming the LLM
145
+ * @param output Test output string
146
+ * @param maxLength Maximum length (default 5000 chars)
147
+ * @returns Truncated and sanitized output with notice if truncated
148
+ */
149
+ export declare function truncateTestOutput(output: string, maxLength?: number): string;
150
+ /**
151
+ * Build retry prompt for implementation agent
152
+ * @param testOutput Test failure output
153
+ * @param attemptNumber Current attempt number (1-indexed)
154
+ * @param maxRetries Maximum number of retries
155
+ * @returns Prompt string for retry attempt
156
+ */
157
+ export declare function buildRetryPrompt(testOutput: string, buildOutput: string, attemptNumber: number, maxRetries: number): string;
102
158
  //# sourceMappingURL=implementation.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"implementation.d.ts","sourceRoot":"","sources":["../../src/agents/implementation.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"implementation.d.ts","sourceRoot":"","sources":["../../src/agents/implementation.ts"],"names":[],"mappings":"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"}
@@ -1,9 +1,10 @@
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
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:
@@ -160,12 +161,16 @@ function escapeShellArg(arg) {
160
161
  */
161
162
  export async function commitIfAllTestsPass(workingDir, message, testTimeout, testRunner = runAllTests) {
162
163
  try {
163
- // Check for uncommitted changes
164
- const status = execSync('git status --porcelain', {
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'], {
165
168
  cwd: workingDir,
166
- encoding: 'utf-8'
169
+ encoding: 'utf-8',
170
+ shell: false,
171
+ stdio: ['ignore', 'pipe', 'pipe'],
167
172
  });
168
- if (!status.trim()) {
173
+ if (statusResult.status !== 0 || !statusResult.stdout || !statusResult.stdout.trim()) {
169
174
  return { committed: false, reason: 'nothing to commit' };
170
175
  }
171
176
  // Run FULL test suite
@@ -173,12 +178,23 @@ export async function commitIfAllTestsPass(workingDir, message, testTimeout, tes
173
178
  if (!testResult.passed) {
174
179
  return { committed: false, reason: 'tests failed' };
175
180
  }
176
- // Commit changes
177
- execSync('git add -A', { cwd: workingDir, stdio: 'pipe' });
178
- execSync(`git commit -m ${escapeShellArg(message)}`, {
181
+ // Commit changes using spawn with shell: false
182
+ const addResult = spawnSync('git', ['add', '-A'], {
179
183
  cwd: workingDir,
180
- stdio: 'pipe'
184
+ shell: false,
185
+ stdio: 'pipe',
181
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
+ }
182
198
  return { committed: true };
183
199
  }
184
200
  catch (error) {
@@ -515,6 +531,189 @@ export async function runTDDImplementation(story, sdlcRoot, options = {}) {
515
531
  error: `TDD: Maximum cycles (${tddConfig.maxCycles}) reached without completing all acceptance criteria.`,
516
532
  };
517
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
+ }
518
717
  /**
519
718
  * Implementation Agent
520
719
  *
@@ -526,28 +725,50 @@ export async function runImplementationAgent(storyPath, sdlcRoot, options = {})
526
725
  const changesMade = [];
527
726
  const workingDir = path.dirname(sdlcRoot);
528
727
  try {
728
+ // Security: Validate working directory before git operations
729
+ validateWorkingDir(workingDir);
529
730
  // Create a feature branch for this story
530
731
  const branchName = `ai-sdlc/${story.slug}`;
732
+ // Security: Validate branch name before use
733
+ validateBranchName(branchName);
531
734
  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}`);
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');
538
743
  }
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}`);
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}`);
544
753
  }
545
- catch {
546
- // Not a git repo or other error, continue without branching
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
+ }
547
768
  }
769
+ // Update story with branch info
770
+ updateStoryField(story, 'branch', branchName);
548
771
  }
549
- // Update story with branch info
550
- updateStoryField(story, 'branch', branchName);
551
772
  }
552
773
  catch {
553
774
  // Not a git repo, continue without branching
@@ -571,7 +792,8 @@ export async function runImplementationAgent(storyPath, sdlcRoot, options = {})
571
792
  // Merge changes
572
793
  changesMade.push(...tddResult.changesMade);
573
794
  if (tddResult.success) {
574
- changesMade.push('Running verification before marking complete...');
795
+ // TDD completed all cycles - now verify with retry support
796
+ changesMade.push('Running final verification...');
575
797
  const verification = await verifyImplementation(tddResult.story, workingDir);
576
798
  updateStoryField(tddResult.story, 'last_test_run', {
577
799
  passed: verification.passed,
@@ -579,13 +801,18 @@ export async function runImplementationAgent(storyPath, sdlcRoot, options = {})
579
801
  timestamp: verification.timestamp,
580
802
  });
581
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);
582
807
  return {
583
808
  success: false,
584
809
  story: parseStory(currentStoryPath),
585
810
  changesMade,
586
- error: `Implementation blocked: ${verification.failures} test(s) failing. Fix tests before completing.`,
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)}`,
587
812
  };
588
813
  }
814
+ // Success - reset retry count
815
+ resetImplementationRetryCount(tddResult.story);
589
816
  updateStoryField(tddResult.story, 'implementation_complete', true);
590
817
  changesMade.push('Marked implementation_complete: true');
591
818
  return {
@@ -603,65 +830,24 @@ export async function runImplementationAgent(storyPath, sdlcRoot, options = {})
603
830
  };
604
831
  }
605
832
  }
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,
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,
637
843
  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
- };
844
+ }, changesMade);
845
+ // If retry loop failed, return the failure result
846
+ if (!retryResult.success) {
847
+ return retryResult;
664
848
  }
849
+ // If we get here, verification passed
850
+ const updatedStory = parseStory(currentStoryPath);
665
851
  // Commit changes after successful standard implementation
666
852
  try {
667
853
  const commitResult = await commitIfAllTestsPass(workingDir, `feat(${story.slug}): ${story.frontmatter.title}`, config.timeouts?.testTimeout || 300000);
@@ -693,4 +879,148 @@ ${implementationResult}
693
879
  };
694
880
  }
695
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
+ }
696
1026
  //# sourceMappingURL=implementation.js.map