ai-sdlc 0.2.0 → 0.3.0-alpha.1

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 (96) hide show
  1. package/README.md +19 -6
  2. package/dist/agents/implementation.d.ts +30 -1
  3. package/dist/agents/implementation.d.ts.map +1 -1
  4. package/dist/agents/implementation.js +172 -17
  5. package/dist/agents/implementation.js.map +1 -1
  6. package/dist/agents/index.d.ts +2 -0
  7. package/dist/agents/index.d.ts.map +1 -1
  8. package/dist/agents/index.js +2 -0
  9. package/dist/agents/index.js.map +1 -1
  10. package/dist/agents/orchestrator.d.ts +61 -0
  11. package/dist/agents/orchestrator.d.ts.map +1 -0
  12. package/dist/agents/orchestrator.js +443 -0
  13. package/dist/agents/orchestrator.js.map +1 -0
  14. package/dist/agents/planning.d.ts +1 -1
  15. package/dist/agents/planning.d.ts.map +1 -1
  16. package/dist/agents/planning.js +33 -1
  17. package/dist/agents/planning.js.map +1 -1
  18. package/dist/agents/review.d.ts +50 -1
  19. package/dist/agents/review.d.ts.map +1 -1
  20. package/dist/agents/review.js +389 -44
  21. package/dist/agents/review.js.map +1 -1
  22. package/dist/agents/rework.d.ts.map +1 -1
  23. package/dist/agents/rework.js +3 -1
  24. package/dist/agents/rework.js.map +1 -1
  25. package/dist/agents/single-task.d.ts +41 -0
  26. package/dist/agents/single-task.d.ts.map +1 -0
  27. package/dist/agents/single-task.js +357 -0
  28. package/dist/agents/single-task.js.map +1 -0
  29. package/dist/agents/verification.d.ts.map +1 -1
  30. package/dist/agents/verification.js +26 -12
  31. package/dist/agents/verification.js.map +1 -1
  32. package/dist/cli/batch-processor.d.ts +64 -0
  33. package/dist/cli/batch-processor.d.ts.map +1 -0
  34. package/dist/cli/batch-processor.js +85 -0
  35. package/dist/cli/batch-processor.js.map +1 -0
  36. package/dist/cli/batch-validator.d.ts +80 -0
  37. package/dist/cli/batch-validator.d.ts.map +1 -0
  38. package/dist/cli/batch-validator.js +121 -0
  39. package/dist/cli/batch-validator.js.map +1 -0
  40. package/dist/cli/commands.d.ts +8 -0
  41. package/dist/cli/commands.d.ts.map +1 -1
  42. package/dist/cli/commands.js +777 -48
  43. package/dist/cli/commands.js.map +1 -1
  44. package/dist/cli/daemon.d.ts.map +1 -1
  45. package/dist/cli/daemon.js +5 -0
  46. package/dist/cli/daemon.js.map +1 -1
  47. package/dist/cli/runner.d.ts.map +1 -1
  48. package/dist/cli/runner.js +20 -9
  49. package/dist/cli/runner.js.map +1 -1
  50. package/dist/core/client.d.ts +19 -1
  51. package/dist/core/client.d.ts.map +1 -1
  52. package/dist/core/client.js +191 -5
  53. package/dist/core/client.js.map +1 -1
  54. package/dist/core/config.d.ts +9 -1
  55. package/dist/core/config.d.ts.map +1 -1
  56. package/dist/core/config.js +51 -2
  57. package/dist/core/config.js.map +1 -1
  58. package/dist/core/index.d.ts +2 -0
  59. package/dist/core/index.d.ts.map +1 -1
  60. package/dist/core/index.js +2 -0
  61. package/dist/core/index.js.map +1 -1
  62. package/dist/core/llm-utils.d.ts +103 -0
  63. package/dist/core/llm-utils.d.ts.map +1 -0
  64. package/dist/core/llm-utils.js +368 -0
  65. package/dist/core/llm-utils.js.map +1 -0
  66. package/dist/core/process-manager.d.ts +15 -0
  67. package/dist/core/process-manager.d.ts.map +1 -0
  68. package/dist/core/process-manager.js +132 -0
  69. package/dist/core/process-manager.js.map +1 -0
  70. package/dist/core/story.d.ts +35 -1
  71. package/dist/core/story.d.ts.map +1 -1
  72. package/dist/core/story.js +107 -1
  73. package/dist/core/story.js.map +1 -1
  74. package/dist/core/task-parser.d.ts +59 -0
  75. package/dist/core/task-parser.d.ts.map +1 -0
  76. package/dist/core/task-parser.js +235 -0
  77. package/dist/core/task-parser.js.map +1 -0
  78. package/dist/core/task-progress.d.ts +92 -0
  79. package/dist/core/task-progress.d.ts.map +1 -0
  80. package/dist/core/task-progress.js +280 -0
  81. package/dist/core/task-progress.js.map +1 -0
  82. package/dist/core/worktree.d.ts +111 -2
  83. package/dist/core/worktree.d.ts.map +1 -1
  84. package/dist/core/worktree.js +310 -2
  85. package/dist/core/worktree.js.map +1 -1
  86. package/dist/index.js +11 -0
  87. package/dist/index.js.map +1 -1
  88. package/dist/services/error-classifier.d.ts +119 -0
  89. package/dist/services/error-classifier.d.ts.map +1 -0
  90. package/dist/services/error-classifier.js +182 -0
  91. package/dist/services/error-classifier.js.map +1 -0
  92. package/dist/types/index.d.ts +230 -0
  93. package/dist/types/index.d.ts.map +1 -1
  94. package/dist/types/index.js.map +1 -1
  95. package/package.json +3 -2
  96. package/templates/story.md +5 -0
@@ -2,10 +2,11 @@ import ora from 'ora';
2
2
  import fs from 'fs';
3
3
  import path from 'path';
4
4
  import * as readline from 'readline';
5
+ import { spawnSync } from 'child_process';
5
6
  import { getSdlcRoot, loadConfig, initConfig, validateWorktreeBasePath, DEFAULT_WORKTREE_CONFIG } from '../core/config.js';
6
7
  import { initializeKanban, kanbanExists, assessState, getBoardStats, findStoryBySlug, findStoriesByStatus } from '../core/kanban.js';
7
- import { createStory, parseStory, resetRPIVCycle, isAtMaxRetries, unblockStory, getStory, findStoryById, updateStoryField, writeStory, sanitizeStoryId } from '../core/story.js';
8
- import { GitWorktreeService } from '../core/worktree.js';
8
+ import { createStory, parseStory, resetRPIVCycle, isAtMaxRetries, unblockStory, getStory, findStoryById, updateStoryField, writeStory, sanitizeStoryId, autoCompleteStoryAfterReview, incrementImplementationRetryCount, getEffectiveMaxImplementationRetries, isAtMaxImplementationRetries, updateStoryStatus } from '../core/story.js';
9
+ import { GitWorktreeService, getLastCompletedPhase, getNextPhase } from '../core/worktree.js';
9
10
  import { ReviewDecision } from '../types/index.js';
10
11
  import { getThemedChalk } from '../core/theme.js';
11
12
  import { saveWorkflowState, loadWorkflowState, clearWorkflowState, generateWorkflowId, calculateStoryHash, hasWorkflowState, } from '../core/workflow-state.js';
@@ -18,6 +19,13 @@ import { validateGitState } from '../core/git-utils.js';
18
19
  import { StoryLogger } from '../core/story-logger.js';
19
20
  import { detectConflicts } from '../core/conflict-detector.js';
20
21
  import { getLogger } from '../core/logger.js';
22
+ /**
23
+ * Branch divergence threshold for warnings
24
+ * When a worktree branch has diverged by more than this number of commits
25
+ * from the base branch (ahead or behind), a warning will be displayed
26
+ * suggesting the user rebase to sync with latest changes.
27
+ */
28
+ const DIVERGENCE_WARNING_THRESHOLD = 10;
21
29
  /**
22
30
  * Initialize the .ai-sdlc folder structure
23
31
  */
@@ -279,6 +287,35 @@ function validateAutoStoryOptions(options) {
279
287
  ' - ai-sdlc run --story <id> --step <phase> (single phase)');
280
288
  }
281
289
  }
290
+ /**
291
+ * Validates flag combinations for --batch conflicts
292
+ * @throws Error if conflicting flags are detected
293
+ */
294
+ function validateBatchOptions(options) {
295
+ if (!options.batch) {
296
+ return; // No batch flag, nothing to validate
297
+ }
298
+ // --batch and --story are mutually exclusive
299
+ if (options.story) {
300
+ throw new Error('Cannot combine --batch with --story flag.\n' +
301
+ 'Use either:\n' +
302
+ ' - ai-sdlc run --batch S-001,S-002,S-003 (batch processing)\n' +
303
+ ' - ai-sdlc run --auto --story <id> (single story)');
304
+ }
305
+ // --batch and --watch are mutually exclusive
306
+ if (options.watch) {
307
+ throw new Error('Cannot combine --batch with --watch flag.\n' +
308
+ 'Use either:\n' +
309
+ ' - ai-sdlc run --batch S-001,S-002,S-003 (batch processing)\n' +
310
+ ' - ai-sdlc run --watch (daemon mode)');
311
+ }
312
+ // --batch and --continue are mutually exclusive
313
+ if (options.continue) {
314
+ throw new Error('Cannot combine --batch with --continue flag.\n' +
315
+ 'Batch mode does not support resuming from checkpoints.\n' +
316
+ 'Use: ai-sdlc run --batch S-001,S-002,S-003');
317
+ }
318
+ }
282
319
  /**
283
320
  * Determines if a specific phase should be executed based on story state
284
321
  * @param story The story to check
@@ -380,6 +417,51 @@ function displayGitValidationResult(result, c) {
380
417
  }
381
418
  }
382
419
  }
420
+ /**
421
+ * Display detailed information about an existing worktree
422
+ */
423
+ function displayExistingWorktreeInfo(status, c) {
424
+ console.log();
425
+ console.log(c.warning('A worktree already exists for this story:'));
426
+ console.log();
427
+ console.log(c.bold(' Worktree Path:'), status.path);
428
+ console.log(c.bold(' Branch: '), status.branch);
429
+ if (status.lastCommit) {
430
+ console.log(c.bold(' Last Commit: '), `${status.lastCommit.hash.substring(0, 7)} - ${status.lastCommit.message}`);
431
+ console.log(c.bold(' Committed: '), status.lastCommit.timestamp);
432
+ }
433
+ const statusLabel = status.workingDirectoryStatus === 'clean'
434
+ ? c.success('clean')
435
+ : c.warning(status.workingDirectoryStatus);
436
+ console.log(c.bold(' Working Dir: '), statusLabel);
437
+ if (status.modifiedFiles.length > 0) {
438
+ console.log();
439
+ console.log(c.warning(' Modified files:'));
440
+ for (const file of status.modifiedFiles.slice(0, 5)) {
441
+ console.log(c.dim(` M ${file}`));
442
+ }
443
+ if (status.modifiedFiles.length > 5) {
444
+ console.log(c.dim(` ... and ${status.modifiedFiles.length - 5} more`));
445
+ }
446
+ }
447
+ if (status.untrackedFiles.length > 0) {
448
+ console.log();
449
+ console.log(c.warning(' Untracked files:'));
450
+ for (const file of status.untrackedFiles.slice(0, 5)) {
451
+ console.log(c.dim(` ? ${file}`));
452
+ }
453
+ if (status.untrackedFiles.length > 5) {
454
+ console.log(c.dim(` ... and ${status.untrackedFiles.length - 5} more`));
455
+ }
456
+ }
457
+ console.log();
458
+ console.log(c.info('To resume work in this worktree:'));
459
+ console.log(c.dim(` cd ${status.path}`));
460
+ console.log();
461
+ console.log(c.info('To remove the worktree and start fresh:'));
462
+ console.log(c.dim(` ai-sdlc worktrees remove ${status.storyId}`));
463
+ console.log();
464
+ }
383
465
  // ANSI escape sequence patterns for sanitization
384
466
  const ANSI_CSI_PATTERN = /\x1B\[[0-9;]*[a-zA-Z]/g;
385
467
  const ANSI_OSC_BEL_PATTERN = /\x1B\][^\x07]*\x07/g;
@@ -448,8 +530,8 @@ export async function preFlightConflictCheck(targetStory, sdlcRoot, options) {
448
530
  if (normalizedPath.length > 1024) {
449
531
  throw new Error('Invalid project path');
450
532
  }
451
- // Check if target story is already in-progress
452
- if (targetStory.frontmatter.status === 'in-progress') {
533
+ // Check if target story is already in-progress (allow if resuming existing worktree)
534
+ if (targetStory.frontmatter.status === 'in-progress' && !targetStory.frontmatter.worktree_path) {
453
535
  console.log(c.error('❌ Story is already in-progress'));
454
536
  return { proceed: false, warnings: ['Story already in progress'] };
455
537
  }
@@ -534,6 +616,149 @@ export async function preFlightConflictCheck(targetStory, sdlcRoot, options) {
534
616
  return { proceed: true, warnings: ['Conflict detection failed'] };
535
617
  }
536
618
  }
619
+ /**
620
+ * Process multiple stories sequentially through full SDLC
621
+ * Internal function used by batch mode
622
+ */
623
+ async function processBatchInternal(storyIds, sdlcRoot, options) {
624
+ const startTime = Date.now();
625
+ const config = loadConfig();
626
+ const c = getThemedChalk(config);
627
+ const { formatBatchProgress, formatBatchSummary, logStoryCompletion, promptContinueOnError } = await import('./batch-processor.js');
628
+ const result = {
629
+ total: storyIds.length,
630
+ succeeded: 0,
631
+ failed: 0,
632
+ skipped: 0,
633
+ errors: [],
634
+ duration: 0,
635
+ };
636
+ console.log();
637
+ console.log(c.bold('═══ Starting Batch Processing ═══'));
638
+ console.log(c.dim(` Stories: ${storyIds.join(', ')}`));
639
+ console.log(c.dim(` Dry run: ${options.dryRun ? 'yes' : 'no'}`));
640
+ console.log();
641
+ // Process each story sequentially
642
+ for (let i = 0; i < storyIds.length; i++) {
643
+ const storyId = storyIds[i];
644
+ // Get story and check status
645
+ let story;
646
+ try {
647
+ story = getStory(sdlcRoot, storyId);
648
+ }
649
+ catch (error) {
650
+ result.failed++;
651
+ result.errors.push({
652
+ storyId,
653
+ error: `Story not found: ${error instanceof Error ? error.message : String(error)}`,
654
+ });
655
+ console.log(c.error(`[${i + 1}/${storyIds.length}] ✗ Story not found: ${storyId}`));
656
+ console.log();
657
+ // Ask if user wants to continue (or abort in non-interactive)
658
+ const shouldContinue = await promptContinueOnError(storyId, c);
659
+ if (!shouldContinue) {
660
+ console.log(c.warning('Batch processing aborted.'));
661
+ break;
662
+ }
663
+ continue;
664
+ }
665
+ // Skip if already done
666
+ if (story.frontmatter.status === 'done') {
667
+ result.skipped++;
668
+ console.log(c.dim(`[${i + 1}/${storyIds.length}] ⊘ Skipping ${storyId} (already completed)`));
669
+ console.log();
670
+ continue;
671
+ }
672
+ // Show progress header
673
+ const progress = {
674
+ currentIndex: i,
675
+ total: storyIds.length,
676
+ currentStory: story,
677
+ };
678
+ console.log(c.info(formatBatchProgress(progress)));
679
+ console.log();
680
+ // Dry-run mode: just show what would be done
681
+ if (options.dryRun) {
682
+ console.log(c.dim(' Would process story through full SDLC'));
683
+ console.log(c.dim(` Status: ${story.frontmatter.status}`));
684
+ console.log();
685
+ result.succeeded++;
686
+ continue;
687
+ }
688
+ // Process story through full SDLC by recursively calling run()
689
+ // We set auto: true to ensure full SDLC execution
690
+ try {
691
+ await run({
692
+ auto: true,
693
+ story: storyId,
694
+ dryRun: false,
695
+ worktree: options.worktree,
696
+ force: options.force,
697
+ });
698
+ // Check if story completed successfully (moved to done)
699
+ const finalStory = getStory(sdlcRoot, storyId);
700
+ if (finalStory.frontmatter.status === 'done') {
701
+ result.succeeded++;
702
+ logStoryCompletion(storyId, true, c);
703
+ }
704
+ else {
705
+ // Story didn't reach done state - treat as failure
706
+ result.failed++;
707
+ result.errors.push({
708
+ storyId,
709
+ error: `Story did not complete (status: ${finalStory.frontmatter.status})`,
710
+ });
711
+ logStoryCompletion(storyId, false, c);
712
+ // Ask if user wants to continue (or abort in non-interactive)
713
+ const shouldContinue = await promptContinueOnError(storyId, c);
714
+ if (!shouldContinue) {
715
+ console.log(c.warning('Batch processing aborted.'));
716
+ break;
717
+ }
718
+ }
719
+ }
720
+ catch (error) {
721
+ result.failed++;
722
+ result.errors.push({
723
+ storyId,
724
+ error: error instanceof Error ? error.message : String(error),
725
+ });
726
+ logStoryCompletion(storyId, false, c);
727
+ // Ask if user wants to continue (or abort in non-interactive)
728
+ const shouldContinue = await promptContinueOnError(storyId, c);
729
+ if (!shouldContinue) {
730
+ console.log(c.warning('Batch processing aborted.'));
731
+ break;
732
+ }
733
+ }
734
+ console.log();
735
+ }
736
+ // Display final summary
737
+ result.duration = Date.now() - startTime;
738
+ const summaryLines = formatBatchSummary(result);
739
+ summaryLines.forEach((line) => {
740
+ if (line.includes('✓')) {
741
+ console.log(c.success(line));
742
+ }
743
+ else if (line.includes('✗')) {
744
+ console.log(c.error(line));
745
+ }
746
+ else if (line.includes('⊘')) {
747
+ console.log(c.warning(line));
748
+ }
749
+ else if (line.startsWith(' -')) {
750
+ console.log(c.dim(line));
751
+ }
752
+ else {
753
+ console.log(line);
754
+ }
755
+ });
756
+ // Return non-zero exit code if any failures occurred
757
+ if (result.failed > 0) {
758
+ process.exitCode = 1;
759
+ }
760
+ return result;
761
+ }
537
762
  /**
538
763
  * Run the workflow (process one action or all)
539
764
  */
@@ -554,6 +779,7 @@ export async function run(options) {
554
779
  step: options.step,
555
780
  watch: options.watch,
556
781
  worktree: options.worktree,
782
+ clean: options.clean,
557
783
  force: options.force,
558
784
  });
559
785
  // Migrate global workflow state to story-specific location if needed
@@ -572,6 +798,51 @@ export async function run(options) {
572
798
  await startDaemon({ maxIterations: maxIterationsOverride });
573
799
  return; // Daemon runs indefinitely
574
800
  }
801
+ // Handle batch mode
802
+ if (options.batch) {
803
+ // Validate batch options first
804
+ try {
805
+ validateBatchOptions(options);
806
+ }
807
+ catch (error) {
808
+ console.log(c.error(`Error: ${error instanceof Error ? error.message : String(error)}`));
809
+ return;
810
+ }
811
+ // Import batch validation modules
812
+ const { parseStoryIdList, deduplicateStoryIds, validateStoryIds } = await import('./batch-validator.js');
813
+ // Parse and validate story IDs
814
+ const rawStoryIds = parseStoryIdList(options.batch);
815
+ if (rawStoryIds.length === 0) {
816
+ console.log(c.error('Error: Empty batch - no story IDs provided'));
817
+ console.log(c.dim('Usage: ai-sdlc run --batch S-001,S-002,S-003'));
818
+ return;
819
+ }
820
+ // Deduplicate story IDs
821
+ const storyIds = deduplicateStoryIds(rawStoryIds);
822
+ if (storyIds.length < rawStoryIds.length) {
823
+ const duplicateCount = rawStoryIds.length - storyIds.length;
824
+ console.log(c.dim(`Note: Removed ${duplicateCount} duplicate story ID(s)`));
825
+ }
826
+ // Validate all stories exist before processing
827
+ const validation = validateStoryIds(storyIds, sdlcRoot);
828
+ if (!validation.valid) {
829
+ console.log(c.error('Error: Batch validation failed'));
830
+ console.log();
831
+ for (const error of validation.errors) {
832
+ console.log(c.error(` - ${error.message}`));
833
+ }
834
+ console.log();
835
+ console.log(c.dim('Fix the errors above and try again.'));
836
+ return;
837
+ }
838
+ // Process the batch using internal function
839
+ await processBatchInternal(storyIds, sdlcRoot, {
840
+ dryRun: options.dryRun,
841
+ worktree: options.worktree,
842
+ force: options.force,
843
+ });
844
+ return; // Batch processing complete
845
+ }
575
846
  // Valid step names for --step option
576
847
  const validSteps = ['refine', 'research', 'plan', 'implement', 'review'];
577
848
  // Validate --step option early
@@ -839,17 +1110,154 @@ export async function run(options) {
839
1110
  }
840
1111
  const workingDir = path.dirname(sdlcRoot);
841
1112
  // Check if story already has an existing worktree (resume scenario)
1113
+ // Note: We check only if existingWorktreePath is set, not if it exists.
1114
+ // The validation logic will handle missing directories/branches.
842
1115
  const existingWorktreePath = targetStory.frontmatter.worktree_path;
843
- if (existingWorktreePath && fs.existsSync(existingWorktreePath)) {
1116
+ if (existingWorktreePath) {
1117
+ // Validate worktree before resuming
1118
+ const resolvedBasePath = validateWorktreeBasePath(worktreeConfig.basePath, workingDir);
1119
+ // Security validation: ensure worktree_path is within the configured base directory
1120
+ const absoluteWorktreePath = path.resolve(existingWorktreePath);
1121
+ const absoluteBasePath = path.resolve(resolvedBasePath);
1122
+ if (!absoluteWorktreePath.startsWith(absoluteBasePath)) {
1123
+ console.log(c.error('Security Error: worktree_path is outside configured base directory'));
1124
+ console.log(c.dim(` Worktree path: ${absoluteWorktreePath}`));
1125
+ console.log(c.dim(` Expected base: ${absoluteBasePath}`));
1126
+ return;
1127
+ }
1128
+ // Warn if story is marked as done but has an existing worktree
1129
+ if (targetStory.frontmatter.status === 'done') {
1130
+ console.log(c.warning('⚠ Story is marked as done but has an existing worktree'));
1131
+ console.log(c.dim(' This may be a stale worktree that should be cleaned up.'));
1132
+ console.log();
1133
+ // Prompt user for confirmation to proceed
1134
+ const rl = readline.createInterface({
1135
+ input: process.stdin,
1136
+ output: process.stdout,
1137
+ });
1138
+ const answer = await new Promise((resolve) => {
1139
+ rl.question(c.dim('Continue with this worktree? (y/N): '), (ans) => {
1140
+ rl.close();
1141
+ resolve(ans.toLowerCase().trim());
1142
+ });
1143
+ });
1144
+ if (answer !== 'y' && answer !== 'yes') {
1145
+ console.log(c.dim('Aborted. Consider removing the worktree_path from the story frontmatter.'));
1146
+ return;
1147
+ }
1148
+ console.log();
1149
+ }
1150
+ const worktreeService = new GitWorktreeService(workingDir, resolvedBasePath);
1151
+ const branchName = worktreeService.getBranchName(targetStory.frontmatter.id, targetStory.slug);
1152
+ const validation = worktreeService.validateWorktreeForResume(existingWorktreePath, branchName);
1153
+ if (!validation.canResume) {
1154
+ console.log(c.error('Cannot resume worktree:'));
1155
+ validation.issues.forEach(issue => console.log(c.dim(` ✗ ${issue}`)));
1156
+ if (validation.requiresRecreation) {
1157
+ const branchExists = !validation.issues.includes('Branch does not exist');
1158
+ const dirMissing = validation.issues.includes('Worktree directory does not exist');
1159
+ const dirExists = !dirMissing;
1160
+ // Case 1: Directory missing but branch exists - recreate worktree from existing branch
1161
+ // Case 2: Directory exists but branch missing - recreate with new branch
1162
+ if ((branchExists && dirMissing) || (!branchExists && dirExists)) {
1163
+ const reason = branchExists
1164
+ ? 'Branch exists - automatically recreating worktree directory'
1165
+ : 'Directory exists - automatically recreating worktree with new branch';
1166
+ console.log(c.dim(`\n✓ ${reason}`));
1167
+ try {
1168
+ // Remove the old worktree reference if it exists
1169
+ const removeResult = spawnSync('git', ['worktree', 'remove', existingWorktreePath, '--force'], {
1170
+ cwd: workingDir,
1171
+ encoding: 'utf-8',
1172
+ shell: false,
1173
+ stdio: ['ignore', 'pipe', 'pipe'],
1174
+ });
1175
+ // Create the worktree at the same path
1176
+ // If branch exists, checkout that branch; otherwise create a new branch
1177
+ const baseBranch = worktreeService.detectBaseBranch();
1178
+ const worktreeAddArgs = branchExists
1179
+ ? ['worktree', 'add', existingWorktreePath, branchName]
1180
+ : ['worktree', 'add', '-b', branchName, existingWorktreePath, baseBranch];
1181
+ const addResult = spawnSync('git', worktreeAddArgs, {
1182
+ cwd: workingDir,
1183
+ encoding: 'utf-8',
1184
+ shell: false,
1185
+ stdio: ['ignore', 'pipe', 'pipe'],
1186
+ });
1187
+ if (addResult.status !== 0) {
1188
+ throw new Error(`Failed to recreate worktree: ${addResult.stderr}`);
1189
+ }
1190
+ // Install dependencies in the recreated worktree
1191
+ worktreeService.installDependencies(existingWorktreePath);
1192
+ console.log(c.success(`✓ Worktree recreated at ${existingWorktreePath}`));
1193
+ getLogger().info('worktree', `Recreated worktree for ${targetStory.frontmatter.id} at ${existingWorktreePath}`);
1194
+ }
1195
+ catch (error) {
1196
+ console.log(c.error(`Failed to recreate worktree: ${error instanceof Error ? error.message : String(error)}`));
1197
+ console.log(c.dim('Please manually remove the worktree_path from the story frontmatter and try again.'));
1198
+ return;
1199
+ }
1200
+ }
1201
+ else {
1202
+ console.log(c.dim('\nWorktree needs manual intervention. Please remove the worktree_path from the story frontmatter and try again.'));
1203
+ return;
1204
+ }
1205
+ }
1206
+ else {
1207
+ return;
1208
+ }
1209
+ }
844
1210
  // Reuse existing worktree
845
1211
  originalCwd = process.cwd();
846
1212
  worktreePath = existingWorktreePath;
847
1213
  process.chdir(worktreePath);
848
1214
  sdlcRoot = getSdlcRoot();
849
1215
  worktreeCreated = true;
1216
+ // Re-load story from worktree context to get current state
1217
+ const worktreeStory = findStoryById(sdlcRoot, targetStory.frontmatter.id);
1218
+ if (worktreeStory) {
1219
+ targetStory = worktreeStory;
1220
+ }
1221
+ // Get phase information for resume context
1222
+ const lastPhase = getLastCompletedPhase(targetStory);
1223
+ const nextPhase = getNextPhase(targetStory);
1224
+ // Get worktree status for uncommitted changes info
1225
+ const worktreeInfo = {
1226
+ path: existingWorktreePath,
1227
+ branch: branchName,
1228
+ storyId: targetStory.frontmatter.id,
1229
+ exists: true,
1230
+ };
1231
+ const worktreeStatus = worktreeService.getWorktreeStatus(worktreeInfo);
1232
+ // Check branch divergence
1233
+ const divergence = worktreeService.checkBranchDivergence(branchName);
850
1234
  console.log(c.success(`✓ Resuming in existing worktree: ${worktreePath}`));
851
- console.log(c.dim(` Branch: ai-sdlc/${targetStory.frontmatter.id}-${targetStory.slug}`));
1235
+ console.log(c.dim(` Branch: ${branchName}`));
1236
+ if (lastPhase) {
1237
+ console.log(c.dim(` Last completed phase: ${lastPhase}`));
1238
+ }
1239
+ if (nextPhase) {
1240
+ console.log(c.dim(` Next phase: ${nextPhase}`));
1241
+ }
1242
+ // Display uncommitted changes if present
1243
+ if (worktreeStatus.workingDirectoryStatus !== 'clean') {
1244
+ const totalChanges = worktreeStatus.modifiedFiles.length + worktreeStatus.untrackedFiles.length;
1245
+ console.log(c.dim(` Uncommitted changes: ${totalChanges} file(s)`));
1246
+ if (worktreeStatus.modifiedFiles.length > 0) {
1247
+ console.log(c.dim(` Modified: ${worktreeStatus.modifiedFiles.slice(0, 3).join(', ')}${worktreeStatus.modifiedFiles.length > 3 ? '...' : ''}`));
1248
+ }
1249
+ if (worktreeStatus.untrackedFiles.length > 0) {
1250
+ console.log(c.dim(` Untracked: ${worktreeStatus.untrackedFiles.slice(0, 3).join(', ')}${worktreeStatus.untrackedFiles.length > 3 ? '...' : ''}`));
1251
+ }
1252
+ }
1253
+ // Warn if branch has diverged significantly
1254
+ if (divergence.diverged && (divergence.ahead > DIVERGENCE_WARNING_THRESHOLD || divergence.behind > DIVERGENCE_WARNING_THRESHOLD)) {
1255
+ console.log(c.warning(` ⚠ Branch has diverged from base: ${divergence.ahead} ahead, ${divergence.behind} behind`));
1256
+ console.log(c.dim(` Consider rebasing to sync with latest changes`));
1257
+ }
852
1258
  console.log();
1259
+ // Log resume event
1260
+ getLogger().info('worktree', `Resumed worktree for ${targetStory.frontmatter.id} at ${worktreePath}`);
853
1261
  }
854
1262
  else {
855
1263
  // Create new worktree
@@ -864,59 +1272,274 @@ export async function run(options) {
864
1272
  return;
865
1273
  }
866
1274
  const worktreeService = new GitWorktreeService(workingDir, resolvedBasePath);
867
- // Validate git state for worktree creation
868
- const validation = worktreeService.validateCanCreateWorktree();
869
- if (!validation.valid) {
870
- console.log(c.error(`Error: ${validation.error}`));
871
- return;
872
- }
873
- try {
874
- // Detect base branch
875
- const baseBranch = worktreeService.detectBaseBranch();
876
- // Create worktree
877
- originalCwd = process.cwd();
878
- worktreePath = worktreeService.create({
879
- storyId: targetStory.frontmatter.id,
880
- slug: targetStory.slug,
881
- baseBranch,
882
- });
883
- // Change to worktree directory BEFORE updating story
884
- // This ensures story updates happen in the worktree, not on main
885
- // (allows parallel story launches from clean main)
886
- process.chdir(worktreePath);
887
- // Recalculate sdlcRoot for the worktree context
888
- sdlcRoot = getSdlcRoot();
889
- worktreeCreated = true;
890
- // Now update story frontmatter with worktree path (writes to worktree copy)
891
- // Re-resolve target story in worktree context
892
- const worktreeStory = findStoryById(sdlcRoot, targetStory.frontmatter.id);
893
- if (worktreeStory) {
894
- const updatedStory = await updateStoryField(worktreeStory, 'worktree_path', worktreePath);
895
- await writeStory(updatedStory);
896
- // Update targetStory reference for downstream use
897
- targetStory = updatedStory;
1275
+ // Check for existing worktree NOT recorded in story frontmatter
1276
+ // This catches scenarios where workflow was interrupted after worktree creation
1277
+ // but before the story file was updated
1278
+ const existingWorktree = worktreeService.findByStoryId(targetStory.frontmatter.id);
1279
+ let shouldCreateNewWorktree = !existingWorktree || !existingWorktree.exists;
1280
+ if (existingWorktree && existingWorktree.exists) {
1281
+ // Handle --clean flag: cleanup and restart
1282
+ if (options.clean) {
1283
+ console.log(c.warning('Existing worktree found - cleaning up before restart...'));
1284
+ console.log();
1285
+ const worktreeStatus = worktreeService.getWorktreeStatus(existingWorktree);
1286
+ const unpushedResult = worktreeService.hasUnpushedCommits(existingWorktree.path);
1287
+ const commitCount = worktreeService.getCommitCount(existingWorktree.path);
1288
+ const branchOnRemote = worktreeService.branchExistsOnRemote(existingWorktree.branch);
1289
+ // Display summary of what will be deleted
1290
+ console.log(c.bold('Cleanup Summary:'));
1291
+ console.log(c.dim('─'.repeat(60)));
1292
+ console.log(`${c.dim('Worktree Path:')} ${worktreeStatus.path}`);
1293
+ console.log(`${c.dim('Branch:')} ${worktreeStatus.branch}`);
1294
+ console.log(`${c.dim('Total Commits:')} ${commitCount}`);
1295
+ console.log(`${c.dim('Unpushed Commits:')} ${unpushedResult.hasUnpushed ? c.warning(unpushedResult.count.toString()) : c.success('0')}`);
1296
+ console.log(`${c.dim('Modified Files:')} ${worktreeStatus.modifiedFiles.length > 0 ? c.warning(worktreeStatus.modifiedFiles.length.toString()) : c.success('0')}`);
1297
+ console.log(`${c.dim('Untracked Files:')} ${worktreeStatus.untrackedFiles.length > 0 ? c.warning(worktreeStatus.untrackedFiles.length.toString()) : c.success('0')}`);
1298
+ console.log(`${c.dim('Remote Branch:')} ${branchOnRemote ? c.warning('EXISTS') : c.dim('none')}`);
1299
+ console.log();
1300
+ // Warn about data loss
1301
+ if (worktreeStatus.modifiedFiles.length > 0 || worktreeStatus.untrackedFiles.length > 0 || unpushedResult.hasUnpushed) {
1302
+ console.log(c.error('⚠ WARNING: This will DELETE all uncommitted and unpushed work!'));
1303
+ console.log();
1304
+ }
1305
+ // Check for --force flag to skip confirmation
1306
+ const forceCleanup = options.force;
1307
+ if (!forceCleanup) {
1308
+ // Prompt for confirmation
1309
+ const confirmed = await new Promise((resolve) => {
1310
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
1311
+ rl.question(c.warning('Are you sure you want to proceed? (y/N): '), (answer) => {
1312
+ rl.close();
1313
+ resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
1314
+ });
1315
+ });
1316
+ if (!confirmed) {
1317
+ console.log(c.info('Cleanup cancelled.'));
1318
+ return;
1319
+ }
1320
+ }
1321
+ console.log();
1322
+ const cleanupSpinner = ora('Cleaning up worktree...').start();
1323
+ try {
1324
+ // Remove worktree (force remove to handle uncommitted changes)
1325
+ const forceRemove = worktreeStatus.modifiedFiles.length > 0 || worktreeStatus.untrackedFiles.length > 0;
1326
+ worktreeService.remove(existingWorktree.path, forceRemove);
1327
+ cleanupSpinner.text = 'Worktree removed, deleting branch...';
1328
+ // Delete local branch
1329
+ worktreeService.deleteBranch(existingWorktree.branch, true);
1330
+ // Optionally delete remote branch if it exists
1331
+ if (branchOnRemote) {
1332
+ if (!forceCleanup) {
1333
+ cleanupSpinner.stop();
1334
+ const deleteRemote = await new Promise((resolve) => {
1335
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
1336
+ rl.question(c.warning('Branch exists on remote. Delete it too? (y/N): '), (answer) => {
1337
+ rl.close();
1338
+ resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
1339
+ });
1340
+ });
1341
+ if (deleteRemote) {
1342
+ cleanupSpinner.start('Deleting remote branch...');
1343
+ worktreeService.deleteRemoteBranch(existingWorktree.branch);
1344
+ }
1345
+ cleanupSpinner.start();
1346
+ }
1347
+ else {
1348
+ // --force provided, skip remote deletion by default (safer)
1349
+ cleanupSpinner.text = 'Skipping remote branch deletion (use manual cleanup if needed)';
1350
+ }
1351
+ }
1352
+ // Reset story workflow state
1353
+ cleanupSpinner.text = 'Resetting story state...';
1354
+ const { resetWorkflowState } = await import('../core/story.js');
1355
+ targetStory = await resetWorkflowState(targetStory);
1356
+ // Clear workflow checkpoint if exists
1357
+ if (hasWorkflowState(sdlcRoot, targetStory.frontmatter.id)) {
1358
+ await clearWorkflowState(sdlcRoot, targetStory.frontmatter.id);
1359
+ }
1360
+ cleanupSpinner.succeed(c.success('✓ Cleanup complete - ready to create fresh worktree'));
1361
+ console.log();
1362
+ }
1363
+ catch (error) {
1364
+ cleanupSpinner.fail(c.error('Cleanup failed'));
1365
+ console.log(c.error(`Error: ${error instanceof Error ? error.message : String(error)}`));
1366
+ return;
1367
+ }
1368
+ // After cleanup, create a fresh worktree
1369
+ shouldCreateNewWorktree = true;
1370
+ }
1371
+ else {
1372
+ // Not cleaning - resume in existing worktree (S-0063 feature)
1373
+ getLogger().info('worktree', `Detected existing worktree for ${targetStory.frontmatter.id} at ${existingWorktree.path}`);
1374
+ // Validate the existing worktree before resuming
1375
+ const branchName = worktreeService.getBranchName(targetStory.frontmatter.id, targetStory.slug);
1376
+ const validation = worktreeService.validateWorktreeForResume(existingWorktree.path, branchName);
1377
+ if (!validation.canResume) {
1378
+ console.log(c.error('Detected existing worktree but cannot resume:'));
1379
+ validation.issues.forEach(issue => console.log(c.dim(` ✗ ${issue}`)));
1380
+ if (validation.requiresRecreation) {
1381
+ const branchExists = !validation.issues.includes('Branch does not exist');
1382
+ const dirMissing = validation.issues.includes('Worktree directory does not exist');
1383
+ const dirExists = !dirMissing;
1384
+ // Case 1: Directory missing but branch exists - recreate worktree from existing branch
1385
+ // Case 2: Directory exists but branch missing - recreate with new branch
1386
+ if ((branchExists && dirMissing) || (!branchExists && dirExists)) {
1387
+ const reason = branchExists
1388
+ ? 'Branch exists - automatically recreating worktree directory'
1389
+ : 'Directory exists - automatically recreating worktree with new branch';
1390
+ console.log(c.dim(`\n✓ ${reason}`));
1391
+ try {
1392
+ // Remove the old worktree reference if it exists
1393
+ const removeResult = spawnSync('git', ['worktree', 'remove', existingWorktree.path, '--force'], {
1394
+ cwd: workingDir,
1395
+ encoding: 'utf-8',
1396
+ shell: false,
1397
+ stdio: ['ignore', 'pipe', 'pipe'],
1398
+ });
1399
+ // Create the worktree at the same path
1400
+ // If branch exists, checkout that branch; otherwise create a new branch
1401
+ const baseBranch = worktreeService.detectBaseBranch();
1402
+ const worktreeAddArgs = branchExists
1403
+ ? ['worktree', 'add', existingWorktree.path, branchName]
1404
+ : ['worktree', 'add', '-b', branchName, existingWorktree.path, baseBranch];
1405
+ const addResult = spawnSync('git', worktreeAddArgs, {
1406
+ cwd: workingDir,
1407
+ encoding: 'utf-8',
1408
+ shell: false,
1409
+ stdio: ['ignore', 'pipe', 'pipe'],
1410
+ });
1411
+ if (addResult.status !== 0) {
1412
+ throw new Error(`Failed to recreate worktree: ${addResult.stderr}`);
1413
+ }
1414
+ // Install dependencies in the recreated worktree
1415
+ worktreeService.installDependencies(existingWorktree.path);
1416
+ console.log(c.success(`✓ Worktree recreated at ${existingWorktree.path}`));
1417
+ getLogger().info('worktree', `Recreated worktree for ${targetStory.frontmatter.id} at ${existingWorktree.path}`);
1418
+ }
1419
+ catch (error) {
1420
+ console.log(c.error(`Failed to recreate worktree: ${error instanceof Error ? error.message : String(error)}`));
1421
+ console.log(c.dim('Please manually remove it with:'));
1422
+ console.log(c.dim(` git worktree remove ${existingWorktree.path}`));
1423
+ return;
1424
+ }
1425
+ }
1426
+ else {
1427
+ console.log(c.dim('\nWorktree needs manual intervention. Please remove it manually with:'));
1428
+ console.log(c.dim(` git worktree remove ${existingWorktree.path}`));
1429
+ return;
1430
+ }
1431
+ }
1432
+ else {
1433
+ return;
1434
+ }
1435
+ }
1436
+ // Automatically resume in the existing worktree
1437
+ originalCwd = process.cwd();
1438
+ worktreePath = existingWorktree.path;
1439
+ process.chdir(worktreePath);
1440
+ sdlcRoot = getSdlcRoot();
1441
+ worktreeCreated = true;
1442
+ // Update story frontmatter with worktree path (sync state)
1443
+ const worktreeStory = findStoryById(sdlcRoot, targetStory.frontmatter.id);
1444
+ if (worktreeStory) {
1445
+ const updatedStory = await updateStoryField(worktreeStory, 'worktree_path', worktreePath);
1446
+ await writeStory(updatedStory);
1447
+ targetStory = updatedStory;
1448
+ }
1449
+ // Get phase information for resume context
1450
+ const lastPhase = getLastCompletedPhase(targetStory);
1451
+ const nextPhase = getNextPhase(targetStory);
1452
+ // Get worktree status for uncommitted changes info
1453
+ const worktreeStatus = worktreeService.getWorktreeStatus(existingWorktree);
1454
+ // Check branch divergence
1455
+ const divergence = worktreeService.checkBranchDivergence(branchName);
1456
+ console.log(c.success(`✓ Resuming in existing worktree: ${worktreePath}`));
1457
+ console.log(c.dim(` Branch: ${branchName}`));
1458
+ console.log(c.dim(` (Worktree path synced to story frontmatter)`));
1459
+ if (lastPhase) {
1460
+ console.log(c.dim(` Last completed phase: ${lastPhase}`));
1461
+ }
1462
+ if (nextPhase) {
1463
+ console.log(c.dim(` Next phase: ${nextPhase}`));
1464
+ }
1465
+ // Display uncommitted changes if present
1466
+ if (worktreeStatus.workingDirectoryStatus !== 'clean') {
1467
+ const totalChanges = worktreeStatus.modifiedFiles.length + worktreeStatus.untrackedFiles.length;
1468
+ console.log(c.dim(` Uncommitted changes: ${totalChanges} file(s)`));
1469
+ if (worktreeStatus.modifiedFiles.length > 0) {
1470
+ console.log(c.dim(` Modified: ${worktreeStatus.modifiedFiles.slice(0, 3).join(', ')}${worktreeStatus.modifiedFiles.length > 3 ? '...' : ''}`));
1471
+ }
1472
+ if (worktreeStatus.untrackedFiles.length > 0) {
1473
+ console.log(c.dim(` Untracked: ${worktreeStatus.untrackedFiles.slice(0, 3).join(', ')}${worktreeStatus.untrackedFiles.length > 3 ? '...' : ''}`));
1474
+ }
1475
+ }
1476
+ // Warn if branch has diverged significantly
1477
+ if (divergence.diverged && (divergence.ahead > DIVERGENCE_WARNING_THRESHOLD || divergence.behind > DIVERGENCE_WARNING_THRESHOLD)) {
1478
+ console.log(c.warning(` ⚠ Branch has diverged from base: ${divergence.ahead} ahead, ${divergence.behind} behind`));
1479
+ console.log(c.dim(` Consider rebasing to sync with latest changes`));
1480
+ }
1481
+ console.log();
898
1482
  }
899
- console.log(c.success(`✓ Created worktree at: ${worktreePath}`));
900
- console.log(c.dim(` Branch: ai-sdlc/${targetStory.frontmatter.id}-${targetStory.slug}`));
901
- console.log();
902
1483
  }
903
- catch (error) {
904
- // Restore directory on worktree creation failure
905
- if (originalCwd) {
906
- process.chdir(originalCwd);
1484
+ if (shouldCreateNewWorktree) {
1485
+ // Validate git state for worktree creation
1486
+ const validation = worktreeService.validateCanCreateWorktree();
1487
+ if (!validation.valid) {
1488
+ console.log(c.error(`Error: ${validation.error}`));
1489
+ return;
1490
+ }
1491
+ try {
1492
+ // Detect base branch
1493
+ const baseBranch = worktreeService.detectBaseBranch();
1494
+ // Create worktree
1495
+ originalCwd = process.cwd();
1496
+ worktreePath = worktreeService.create({
1497
+ storyId: targetStory.frontmatter.id,
1498
+ slug: targetStory.slug,
1499
+ baseBranch,
1500
+ });
1501
+ // Change to worktree directory BEFORE updating story
1502
+ // This ensures story updates happen in the worktree, not on main
1503
+ // (allows parallel story launches from clean main)
1504
+ process.chdir(worktreePath);
1505
+ // Recalculate sdlcRoot for the worktree context
1506
+ sdlcRoot = getSdlcRoot();
1507
+ worktreeCreated = true;
1508
+ // Now update story frontmatter with worktree path (writes to worktree copy)
1509
+ // Re-resolve target story in worktree context
1510
+ const worktreeStory = findStoryById(sdlcRoot, targetStory.frontmatter.id);
1511
+ if (worktreeStory) {
1512
+ const updatedStory = await updateStoryField(worktreeStory, 'worktree_path', worktreePath);
1513
+ await writeStory(updatedStory);
1514
+ // Update targetStory reference for downstream use
1515
+ targetStory = updatedStory;
1516
+ }
1517
+ console.log(c.success(`✓ Created worktree at: ${worktreePath}`));
1518
+ console.log(c.dim(` Branch: ai-sdlc/${targetStory.frontmatter.id}-${targetStory.slug}`));
1519
+ console.log();
1520
+ }
1521
+ catch (error) {
1522
+ // Restore directory on worktree creation failure
1523
+ if (originalCwd) {
1524
+ process.chdir(originalCwd);
1525
+ }
1526
+ console.log(c.error(`Failed to create worktree: ${error instanceof Error ? error.message : String(error)}`));
1527
+ return;
907
1528
  }
908
- console.log(c.error(`Failed to create worktree: ${error instanceof Error ? error.message : String(error)}`));
909
- return;
910
1529
  }
911
1530
  }
912
1531
  }
913
1532
  // Validate git state before processing actions that modify git
914
1533
  // Skip protected branch check if worktree mode is active (worktree is on feature branch)
915
- // Exclude .ai-sdlc/** from clean check when worktree was created (story file was just updated)
1534
+ // Skip clean check entirely when worktree was just created:
1535
+ // - The worktree starts from a clean base branch
1536
+ // - npm install may modify package-lock.json
1537
+ // - Story file was just updated with worktree_path
1538
+ // - There's no prior user work to protect in a fresh worktree
916
1539
  if (!options.force && requiresGitValidation(actionsToProcess)) {
917
1540
  const workingDir = path.dirname(sdlcRoot);
918
1541
  const gitValidationOptions = worktreeCreated
919
- ? { skipBranchCheck: true, excludePatterns: ['.ai-sdlc/**'] }
1542
+ ? { skipBranchCheck: true, skipCleanCheck: true }
920
1543
  : {};
921
1544
  const gitValidation = validateGitState(workingDir, gitValidationOptions);
922
1545
  if (!gitValidation.valid) {
@@ -1013,6 +1636,56 @@ export async function run(options) {
1013
1636
  return;
1014
1637
  }
1015
1638
  }
1639
+ else if (reviewResult.decision === ReviewDecision.RECOVERY) {
1640
+ // Implementation recovery: reset implementation_complete and increment implementation retry count
1641
+ // This is distinct from REJECTED which resets the entire RPIV cycle
1642
+ const story = parseStory(action.storyPath);
1643
+ const config = loadConfig();
1644
+ const retryCount = story.frontmatter.implementation_retry_count || 0;
1645
+ const maxRetries = getEffectiveMaxImplementationRetries(story, config);
1646
+ const maxRetriesDisplay = Number.isFinite(maxRetries) ? maxRetries : '∞';
1647
+ console.log();
1648
+ console.log(c.warning(`🔄 Implementation recovery triggered (attempt ${retryCount + 1}/${maxRetriesDisplay})`));
1649
+ console.log(c.dim(` Reason: ${story.frontmatter.last_restart_reason || 'No source code changes detected'}`));
1650
+ // Increment implementation retry count
1651
+ await incrementImplementationRetryCount(story);
1652
+ // Check if we've exceeded max implementation retries after incrementing
1653
+ const freshStory = parseStory(action.storyPath);
1654
+ if (isAtMaxImplementationRetries(freshStory, config)) {
1655
+ console.log();
1656
+ console.log(c.error('═'.repeat(50)));
1657
+ console.log(c.error(`✗ Implementation recovery failed - maximum retries reached`));
1658
+ console.log(c.error('═'.repeat(50)));
1659
+ console.log(c.dim(`Story has reached the maximum implementation retry limit (${maxRetries}).`));
1660
+ console.log(c.warning('Marking story as blocked. Manual intervention required.'));
1661
+ // Mark story as blocked
1662
+ await updateStoryStatus(freshStory, 'blocked');
1663
+ console.log(c.info('Story status updated to: blocked'));
1664
+ await clearWorkflowState(sdlcRoot, action.storyId);
1665
+ process.exit(1);
1666
+ }
1667
+ // Regenerate actions to restart from implementation phase
1668
+ const newActions = generateFullSDLCActions(freshStory, c);
1669
+ if (newActions.length > 0) {
1670
+ currentActions = newActions;
1671
+ currentActionIndex = 0;
1672
+ console.log(c.info(` → Restarting from ${newActions[0].type} phase`));
1673
+ console.log();
1674
+ continue; // Restart the loop with new actions
1675
+ }
1676
+ else {
1677
+ console.log(c.error('Error: No actions generated for recovery. Manual intervention required.'));
1678
+ process.exit(1);
1679
+ }
1680
+ }
1681
+ else if (reviewResult.decision === ReviewDecision.FAILED) {
1682
+ // Review agent failed - don't increment retry count
1683
+ console.log();
1684
+ console.log(c.error(`✗ Review process failed: ${reviewResult.error || 'Unknown error'}`));
1685
+ console.log(c.warning('This does not count as a retry attempt. You can retry manually.'));
1686
+ await clearWorkflowState(sdlcRoot, action.storyId);
1687
+ process.exit(1);
1688
+ }
1016
1689
  }
1017
1690
  // Save checkpoint after successful action
1018
1691
  if (actionResult.success) {
@@ -1230,6 +1903,53 @@ async function executeAction(action, sdlcRoot) {
1230
1903
  }
1231
1904
  },
1232
1905
  });
1906
+ // Auto-complete story if review was approved
1907
+ if (result && result.success) {
1908
+ const reviewResult = result;
1909
+ let story = parseStory(action.storyPath);
1910
+ story = await autoCompleteStoryAfterReview(story, config, reviewResult);
1911
+ // Log auto-completion if it occurred
1912
+ if (reviewResult.decision === ReviewDecision.APPROVED && config.reviewConfig.autoCompleteOnApproval) {
1913
+ spinner.text = c.success('Review approved - auto-completing story');
1914
+ storyLogger?.log('INFO', `Story auto-completed after review approval: "${story.frontmatter.title}"`);
1915
+ // Auto-create PR in automated mode
1916
+ const workflowState = await loadWorkflowState(sdlcRoot, story.frontmatter.id);
1917
+ const isAutoMode = workflowState?.context.options.auto ?? false;
1918
+ if (isAutoMode || config.reviewConfig.autoCreatePROnApproval) {
1919
+ try {
1920
+ // Create PR (this will automatically commit any uncommitted changes)
1921
+ spinner.text = c.dim('Creating pull request...');
1922
+ const { createPullRequest } = await import('../agents/review.js');
1923
+ const prResult = await createPullRequest(action.storyPath, sdlcRoot);
1924
+ if (prResult.success) {
1925
+ spinner.text = c.success('Review approved - PR created');
1926
+ storyLogger?.log('INFO', `PR created successfully for ${story.frontmatter.id}`);
1927
+ }
1928
+ else {
1929
+ // PR creation failed - mark as blocked
1930
+ const { updateStoryStatus } = await import('../core/story.js');
1931
+ const blockedStory = await updateStoryStatus(story, 'blocked');
1932
+ await writeStory(blockedStory);
1933
+ spinner.text = c.warning('Review approved but PR creation failed - story marked as blocked');
1934
+ storyLogger?.log('WARN', `PR creation failed for ${story.frontmatter.id}: ${prResult.error || 'Unknown error'}`);
1935
+ }
1936
+ }
1937
+ catch (error) {
1938
+ // Error during PR creation - mark as blocked
1939
+ const { updateStoryStatus } = await import('../core/story.js');
1940
+ const blockedStory = await updateStoryStatus(story, 'blocked');
1941
+ await writeStory(blockedStory);
1942
+ const errorMsg = error instanceof Error ? error.message : String(error);
1943
+ spinner.text = c.warning(`Review approved but auto-PR failed: ${errorMsg}`);
1944
+ storyLogger?.log('ERROR', `Auto-PR failed for ${story.frontmatter.id}: ${errorMsg}`);
1945
+ }
1946
+ }
1947
+ // Handle worktree cleanup if story has a worktree
1948
+ if (story.frontmatter.worktree_path) {
1949
+ await handleWorktreeCleanup(story, config, c);
1950
+ }
1951
+ }
1952
+ }
1233
1953
  break;
1234
1954
  case 'rework':
1235
1955
  const { runReworkAgent } = await import('../agents/rework.js');
@@ -1981,6 +2701,15 @@ async function handleWorktreeCleanup(story, config, c) {
1981
2701
  await writeStory(updated);
1982
2702
  }
1983
2703
  }
2704
+ /**
2705
+ * Security: Escape shell arguments for safe use in commands
2706
+ * For use with execSync when shell execution is required
2707
+ * @internal Exported for testing
2708
+ */
2709
+ export function escapeShellArg(arg) {
2710
+ // Replace single quotes with '\'' and wrap in single quotes
2711
+ return `'${arg.replace(/'/g, "'\\''")}'`;
2712
+ }
1984
2713
  /**
1985
2714
  * List all ai-sdlc managed worktrees
1986
2715
  */