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

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.
@@ -1,9 +1,11 @@
1
1
  import ora from 'ora';
2
2
  import fs from 'fs';
3
3
  import path from 'path';
4
- import { getSdlcRoot, loadConfig, initConfig } from '../core/config.js';
4
+ import * as readline from 'readline';
5
+ import { getSdlcRoot, loadConfig, initConfig, validateWorktreeBasePath, DEFAULT_WORKTREE_CONFIG } from '../core/config.js';
5
6
  import { initializeKanban, kanbanExists, assessState, getBoardStats, findStoryBySlug } from '../core/kanban.js';
6
- import { createStory, parseStory, resetRPIVCycle, isAtMaxRetries, unblockStory, getStory, findStoryById } from '../core/story.js';
7
+ import { createStory, parseStory, resetRPIVCycle, isAtMaxRetries, unblockStory, getStory, findStoryById, updateStoryField, writeStory } from '../core/story.js';
8
+ import { GitWorktreeService } from '../core/worktree.js';
7
9
  import { ReviewDecision } from '../types/index.js';
8
10
  import { getThemedChalk } from '../core/theme.js';
9
11
  import { saveWorkflowState, loadWorkflowState, clearWorkflowState, generateWorkflowId, calculateStoryHash, hasWorkflowState, } from '../core/workflow-state.js';
@@ -12,6 +14,7 @@ import { getStoryFlags as getStoryFlagsUtil, formatStatus as formatStatusUtil }
12
14
  import { migrateToFolderPerStory } from './commands/migrate.js';
13
15
  import { generateReviewSummary } from '../agents/review.js';
14
16
  import { getTerminalWidth } from './formatting.js';
17
+ import { validateGitState } from '../core/git-utils.js';
15
18
  /**
16
19
  * Initialize the .ai-sdlc folder structure
17
20
  */
@@ -205,6 +208,37 @@ function generateFullSDLCActions(story, c) {
205
208
  }
206
209
  return actions;
207
210
  }
211
+ /**
212
+ * Actions that modify git and require validation
213
+ */
214
+ const GIT_MODIFYING_ACTIONS = ['implement', 'review', 'create_pr'];
215
+ /**
216
+ * Check if any actions in the list require git validation
217
+ */
218
+ function requiresGitValidation(actions) {
219
+ return actions.some(action => GIT_MODIFYING_ACTIONS.includes(action.type));
220
+ }
221
+ /**
222
+ * Display git validation errors and warnings
223
+ */
224
+ function displayGitValidationResult(result, c) {
225
+ if (result.errors.length > 0) {
226
+ console.log();
227
+ console.log(c.error('Git validation failed:'));
228
+ for (const error of result.errors) {
229
+ console.log(c.error(` - ${error}`));
230
+ }
231
+ console.log();
232
+ console.log(c.info('To override this check, use --force (at your own risk)'));
233
+ }
234
+ if (result.warnings.length > 0) {
235
+ console.log();
236
+ console.log(c.warning('Git validation warnings:'));
237
+ for (const warning of result.warnings) {
238
+ console.log(c.warning(` - ${warning}`));
239
+ }
240
+ }
241
+ }
208
242
  /**
209
243
  * Run the workflow (process one action or all)
210
244
  */
@@ -214,7 +248,7 @@ export async function run(options) {
214
248
  const maxIterationsOverride = options.maxIterations !== undefined
215
249
  ? parseInt(options.maxIterations, 10)
216
250
  : undefined;
217
- const sdlcRoot = getSdlcRoot();
251
+ let sdlcRoot = getSdlcRoot();
218
252
  const c = getThemedChalk(config);
219
253
  // Handle daemon/watch mode
220
254
  if (options.watch) {
@@ -312,6 +346,12 @@ export async function run(options) {
312
346
  // Filter actions by story if --story flag is provided
313
347
  if (options.story) {
314
348
  const normalizedInput = options.story.toLowerCase().trim();
349
+ // SECURITY: Validate story ID format to prevent path traversal and injection
350
+ // Only allow alphanumeric characters, hyphens, and underscores
351
+ if (!/^[a-z0-9_-]+$/i.test(normalizedInput)) {
352
+ console.log(c.error('Invalid story ID format. Only letters, numbers, hyphens, and underscores are allowed.'));
353
+ return;
354
+ }
315
355
  // Try to find story by ID first, then by slug (case-insensitive)
316
356
  let targetStory = findStoryById(sdlcRoot, normalizedInput);
317
357
  if (!targetStory) {
@@ -431,158 +471,261 @@ export async function run(options) {
431
471
  return;
432
472
  }
433
473
  }
474
+ // Validate git state before processing actions that modify git
475
+ if (!options.force && requiresGitValidation(actionsToProcess)) {
476
+ const workingDir = path.dirname(sdlcRoot);
477
+ const gitValidation = validateGitState(workingDir);
478
+ if (!gitValidation.valid) {
479
+ displayGitValidationResult(gitValidation, c);
480
+ return;
481
+ }
482
+ if (gitValidation.warnings.length > 0) {
483
+ displayGitValidationResult(gitValidation, c);
484
+ console.log();
485
+ }
486
+ }
487
+ // Handle worktree creation based on flags and config
488
+ let worktreePath;
489
+ let originalCwd;
490
+ // Determine if worktree should be used
491
+ // Priority: CLI flags > config > default (disabled)
492
+ const worktreeConfig = config.worktree ?? DEFAULT_WORKTREE_CONFIG;
493
+ const shouldUseWorktree = (() => {
494
+ // Explicit --no-worktree disables worktrees
495
+ if (options.worktree === false)
496
+ return false;
497
+ // Explicit --worktree enables worktrees
498
+ if (options.worktree === true)
499
+ return true;
500
+ // Fall back to config default
501
+ return worktreeConfig.enabled;
502
+ })();
503
+ // Validate that worktree mode requires --story
504
+ if (shouldUseWorktree && !options.story) {
505
+ if (options.worktree === true) {
506
+ // Explicit --worktree flag without --story is an error
507
+ console.log(c.error('Error: --worktree requires --story flag'));
508
+ return;
509
+ }
510
+ // Config-enabled worktree without --story just silently skips worktree
511
+ }
512
+ if (shouldUseWorktree && options.story) {
513
+ const workingDir = path.dirname(sdlcRoot);
514
+ // Resolve worktree base path from config
515
+ let resolvedBasePath;
516
+ try {
517
+ resolvedBasePath = validateWorktreeBasePath(worktreeConfig.basePath, workingDir);
518
+ }
519
+ catch (error) {
520
+ console.log(c.error(`Configuration Error: ${error instanceof Error ? error.message : String(error)}`));
521
+ console.log(c.dim('Fix worktree.basePath in .ai-sdlc.json or remove it to use default location'));
522
+ return;
523
+ }
524
+ const worktreeService = new GitWorktreeService(workingDir, resolvedBasePath);
525
+ // Validate git state for worktree creation
526
+ const validation = worktreeService.validateCanCreateWorktree();
527
+ if (!validation.valid) {
528
+ console.log(c.error(`Error: ${validation.error}`));
529
+ return;
530
+ }
531
+ // Get the target story (already loaded from --story processing above)
532
+ const targetStory = findStoryById(sdlcRoot, options.story) || findStoryBySlug(sdlcRoot, options.story);
533
+ if (!targetStory) {
534
+ console.log(c.error(`Error: Story not found: "${options.story}"`));
535
+ return;
536
+ }
537
+ try {
538
+ // Detect base branch
539
+ const baseBranch = worktreeService.detectBaseBranch();
540
+ // Create worktree
541
+ originalCwd = process.cwd();
542
+ worktreePath = worktreeService.create({
543
+ storyId: targetStory.frontmatter.id,
544
+ slug: targetStory.slug,
545
+ baseBranch,
546
+ });
547
+ // Update story frontmatter with worktree path
548
+ const updatedStory = updateStoryField(targetStory, 'worktree_path', worktreePath);
549
+ await writeStory(updatedStory);
550
+ // Change to worktree directory
551
+ process.chdir(worktreePath);
552
+ // Recalculate sdlcRoot for the worktree context
553
+ // Since we've changed cwd to the worktree, getSdlcRoot() will now return the worktree's .ai-sdlc path
554
+ // This ensures all subsequent agent operations work within the isolated worktree
555
+ sdlcRoot = getSdlcRoot();
556
+ console.log(c.success(`✓ Created worktree at: ${worktreePath}`));
557
+ console.log(c.dim(` Branch: ai-sdlc/${targetStory.frontmatter.id}-${targetStory.slug}`));
558
+ console.log();
559
+ }
560
+ catch (error) {
561
+ // Restore directory on worktree creation failure
562
+ if (originalCwd) {
563
+ process.chdir(originalCwd);
564
+ }
565
+ console.log(c.error(`Failed to create worktree: ${error instanceof Error ? error.message : String(error)}`));
566
+ return;
567
+ }
568
+ }
434
569
  // Process actions with retry support for Full SDLC mode
435
570
  let currentActions = [...actionsToProcess];
436
571
  let currentActionIndex = 0;
437
572
  let retryAttempt = 0;
438
573
  const MAX_DISPLAY_RETRIES = 3; // For display purposes
439
- while (currentActionIndex < currentActions.length) {
440
- const action = currentActions[currentActionIndex];
441
- const totalActions = currentActions.length;
442
- // Enhanced progress indicator for full SDLC mode
443
- if (isFullSDLC && totalActions > 1) {
444
- const retryIndicator = retryAttempt > 0 ? ` (retry ${retryAttempt})` : '';
445
- console.log(c.info(`\n═══ Phase ${currentActionIndex + 1}/${totalActions}: ${action.type.toUpperCase()}${retryIndicator} ═══`));
446
- }
447
- const actionResult = await executeAction(action, sdlcRoot);
448
- // Handle action failure in full SDLC mode
449
- if (!actionResult.success && isFullSDLC) {
450
- console.log();
451
- console.log(c.error(`✗ Phase ${action.type} failed`));
452
- console.log(c.dim(`Completed ${currentActionIndex} of ${totalActions} phases`));
453
- console.log(c.info('Fix the error above and use --continue to resume.'));
454
- return;
455
- }
456
- // Handle review rejection in Full SDLC mode - trigger retry loop
457
- if (isFullSDLC && action.type === 'review' && actionResult.reviewResult) {
458
- const reviewResult = actionResult.reviewResult;
459
- if (reviewResult.decision === ReviewDecision.REJECTED) {
460
- // Load fresh story state and config for retry check
461
- const story = parseStory(action.storyPath);
462
- const config = loadConfig();
463
- // Check if we're at max retries (pass CLI override if provided)
464
- if (isAtMaxRetries(story, config, maxIterationsOverride)) {
465
- console.log();
466
- console.log(c.error('═'.repeat(50)));
467
- console.log(c.error(`✗ Review failed - maximum retries reached`));
468
- console.log(c.error('═'.repeat(50)));
469
- console.log(c.dim(`Story has reached the maximum retry limit.`));
470
- console.log(c.dim(`Issues found: ${reviewResult.issues.length}`));
471
- console.log(c.warning('Manual intervention required to address the review feedback.'));
472
- console.log(c.info('You can:'));
473
- console.log(c.dim(' 1. Fix issues manually and run again'));
474
- console.log(c.dim(' 2. Reset retry count in the story frontmatter'));
475
- await clearWorkflowState(sdlcRoot);
476
- return;
477
- }
478
- // We can retry - reset RPIV cycle and loop back
479
- const currentRetry = (story.frontmatter.retry_count || 0) + 1;
480
- // Use CLI override, then story-specific, then config default
481
- const effectiveMaxRetries = maxIterationsOverride !== undefined
482
- ? maxIterationsOverride
483
- : (story.frontmatter.max_retries ?? config.reviewConfig?.maxRetries ?? Infinity);
484
- const maxRetriesDisplay = Number.isFinite(effectiveMaxRetries) ? effectiveMaxRetries : '∞';
574
+ try {
575
+ while (currentActionIndex < currentActions.length) {
576
+ const action = currentActions[currentActionIndex];
577
+ const totalActions = currentActions.length;
578
+ // Enhanced progress indicator for full SDLC mode
579
+ if (isFullSDLC && totalActions > 1) {
580
+ const retryIndicator = retryAttempt > 0 ? ` (retry ${retryAttempt})` : '';
581
+ console.log(c.info(`\n═══ Phase ${currentActionIndex + 1}/${totalActions}: ${action.type.toUpperCase()}${retryIndicator} ═══`));
582
+ }
583
+ const actionResult = await executeAction(action, sdlcRoot);
584
+ // Handle action failure in full SDLC mode
585
+ if (!actionResult.success && isFullSDLC) {
485
586
  console.log();
486
- console.log(c.warning(`⟳ Review rejected with ${reviewResult.issues.length} issue(s) - initiating rework (attempt ${currentRetry}/${maxRetriesDisplay})`));
487
- // Display executive summary
488
- const summary = generateReviewSummary(reviewResult.issues, getTerminalWidth());
489
- console.log(c.dim(` Summary: ${summary}`));
490
- // Reset the RPIV cycle (this increments retry_count and resets flags)
491
- resetRPIVCycle(story, reviewResult.feedback);
492
- // Log what's being reset
493
- console.log(c.dim(` → Reset plan_complete, implementation_complete, reviews_complete`));
494
- console.log(c.dim(` → Retry count: ${currentRetry}/${maxRetriesDisplay}`));
495
- // Regenerate actions starting from the phase that needs rework
496
- // For now, we restart from 'plan' since that's the typical flow after research
497
- const freshStory = parseStory(action.storyPath);
498
- const newActions = generateFullSDLCActions(freshStory, c);
499
- if (newActions.length > 0) {
500
- // Replace remaining actions with the new sequence
501
- currentActions = newActions;
502
- currentActionIndex = 0;
503
- retryAttempt++;
504
- console.log(c.info(` → Restarting SDLC from ${newActions[0].type} phase`));
505
- console.log();
506
- continue; // Restart the loop with new actions
507
- }
508
- else {
509
- // No actions to retry (shouldn't happen but handle gracefully)
510
- console.log(c.error('Error: No actions generated for retry. Manual intervention required.'));
511
- return;
512
- }
587
+ console.log(c.error(`✗ Phase ${action.type} failed`));
588
+ console.log(c.dim(`Completed ${currentActionIndex} of ${totalActions} phases`));
589
+ console.log(c.info('Fix the error above and use --continue to resume.'));
590
+ return;
513
591
  }
514
- }
515
- // Save checkpoint after successful action
516
- if (actionResult.success) {
517
- completedActions.push({
518
- type: action.type,
519
- storyId: action.storyId,
520
- storyPath: action.storyPath,
521
- completedAt: new Date().toISOString(),
522
- });
523
- const state = {
524
- version: '1.0',
525
- workflowId,
526
- timestamp: new Date().toISOString(),
527
- currentAction: null,
528
- completedActions,
529
- context: {
530
- sdlcRoot,
531
- options: {
532
- auto: options.auto,
533
- dryRun: options.dryRun,
534
- story: options.story,
535
- fullSDLC: isFullSDLC,
536
- },
537
- storyContentHash: calculateStoryHash(action.storyPath),
538
- },
539
- };
540
- await saveWorkflowState(state, sdlcRoot);
541
- console.log(c.dim(` ✓ Progress saved (${completedActions.length} actions completed)`));
542
- }
543
- currentActionIndex++;
544
- // Re-assess after each action in auto mode
545
- if (options.auto) {
546
- // For full SDLC mode, check if all phases are complete (and review passed)
547
- if (isFullSDLC) {
548
- // Check if we've completed all actions in our sequence
549
- if (currentActionIndex >= currentActions.length) {
550
- // Verify the review actually passed (reviews_complete should be true)
551
- const finalStory = parseStory(action.storyPath);
552
- if (finalStory.frontmatter.reviews_complete) {
592
+ // Handle review rejection in Full SDLC mode - trigger retry loop
593
+ if (isFullSDLC && action.type === 'review' && actionResult.reviewResult) {
594
+ const reviewResult = actionResult.reviewResult;
595
+ if (reviewResult.decision === ReviewDecision.REJECTED) {
596
+ // Load fresh story state and config for retry check
597
+ const story = parseStory(action.storyPath);
598
+ const config = loadConfig();
599
+ // Check if we're at max retries (pass CLI override if provided)
600
+ if (isAtMaxRetries(story, config, maxIterationsOverride)) {
553
601
  console.log();
554
- console.log(c.success('═'.repeat(50)));
555
- console.log(c.success(`✓ Full SDLC completed successfully!`));
556
- console.log(c.success('═'.repeat(50)));
557
- console.log(c.dim(`Completed phases: ${currentActions.length}`));
558
- if (retryAttempt > 0) {
559
- console.log(c.dim(`Retry attempts: ${retryAttempt}`));
560
- }
561
- console.log(c.dim(`Story is now ready for PR creation.`));
602
+ console.log(c.error('═'.repeat(50)));
603
+ console.log(c.error(`✗ Review failed - maximum retries reached`));
604
+ console.log(c.error('═'.repeat(50)));
605
+ console.log(c.dim(`Story has reached the maximum retry limit.`));
606
+ console.log(c.dim(`Issues found: ${reviewResult.issues.length}`));
607
+ console.log(c.warning('Manual intervention required to address the review feedback.'));
608
+ console.log(c.info('You can:'));
609
+ console.log(c.dim(' 1. Fix issues manually and run again'));
610
+ console.log(c.dim(' 2. Reset retry count in the story frontmatter'));
562
611
  await clearWorkflowState(sdlcRoot);
563
- console.log(c.dim('Checkpoint cleared.'));
612
+ return;
564
613
  }
565
- else {
566
- // This shouldn't happen if our logic is correct, but handle it
614
+ // We can retry - reset RPIV cycle and loop back
615
+ const currentRetry = (story.frontmatter.retry_count || 0) + 1;
616
+ // Use CLI override, then story-specific, then config default
617
+ const effectiveMaxRetries = maxIterationsOverride !== undefined
618
+ ? maxIterationsOverride
619
+ : (story.frontmatter.max_retries ?? config.reviewConfig?.maxRetries ?? Infinity);
620
+ const maxRetriesDisplay = Number.isFinite(effectiveMaxRetries) ? effectiveMaxRetries : '∞';
621
+ console.log();
622
+ console.log(c.warning(`⟳ Review rejected with ${reviewResult.issues.length} issue(s) - initiating rework (attempt ${currentRetry}/${maxRetriesDisplay})`));
623
+ // Display executive summary
624
+ const summary = generateReviewSummary(reviewResult.issues, getTerminalWidth());
625
+ console.log(c.dim(` Summary: ${summary}`));
626
+ // Reset the RPIV cycle (this increments retry_count and resets flags)
627
+ resetRPIVCycle(story, reviewResult.feedback);
628
+ // Log what's being reset
629
+ console.log(c.dim(` → Reset plan_complete, implementation_complete, reviews_complete`));
630
+ console.log(c.dim(` → Retry count: ${currentRetry}/${maxRetriesDisplay}`));
631
+ // Regenerate actions starting from the phase that needs rework
632
+ // For now, we restart from 'plan' since that's the typical flow after research
633
+ const freshStory = parseStory(action.storyPath);
634
+ const newActions = generateFullSDLCActions(freshStory, c);
635
+ if (newActions.length > 0) {
636
+ // Replace remaining actions with the new sequence
637
+ currentActions = newActions;
638
+ currentActionIndex = 0;
639
+ retryAttempt++;
640
+ console.log(c.info(` → Restarting SDLC from ${newActions[0].type} phase`));
567
641
  console.log();
568
- console.log(c.warning('All phases executed but reviews_complete is false.'));
569
- console.log(c.dim('This may indicate an issue with the review process.'));
642
+ continue; // Restart the loop with new actions
643
+ }
644
+ else {
645
+ // No actions to retry (shouldn't happen but handle gracefully)
646
+ console.log(c.error('Error: No actions generated for retry. Manual intervention required.'));
647
+ return;
570
648
  }
571
- break;
572
649
  }
573
650
  }
574
- else {
575
- // Normal auto mode: re-assess state
576
- const newAssessment = assessState(sdlcRoot);
577
- if (newAssessment.recommendedActions.length === 0) {
578
- console.log(c.success('\n✓ All actions completed!'));
579
- await clearWorkflowState(sdlcRoot);
580
- console.log(c.dim('Checkpoint cleared.'));
581
- break;
651
+ // Save checkpoint after successful action
652
+ if (actionResult.success) {
653
+ completedActions.push({
654
+ type: action.type,
655
+ storyId: action.storyId,
656
+ storyPath: action.storyPath,
657
+ completedAt: new Date().toISOString(),
658
+ });
659
+ const state = {
660
+ version: '1.0',
661
+ workflowId,
662
+ timestamp: new Date().toISOString(),
663
+ currentAction: null,
664
+ completedActions,
665
+ context: {
666
+ sdlcRoot,
667
+ options: {
668
+ auto: options.auto,
669
+ dryRun: options.dryRun,
670
+ story: options.story,
671
+ fullSDLC: isFullSDLC,
672
+ },
673
+ storyContentHash: calculateStoryHash(action.storyPath),
674
+ },
675
+ };
676
+ await saveWorkflowState(state, sdlcRoot);
677
+ console.log(c.dim(` ✓ Progress saved (${completedActions.length} actions completed)`));
678
+ }
679
+ currentActionIndex++;
680
+ // Re-assess after each action in auto mode
681
+ if (options.auto) {
682
+ // For full SDLC mode, check if all phases are complete (and review passed)
683
+ if (isFullSDLC) {
684
+ // Check if we've completed all actions in our sequence
685
+ if (currentActionIndex >= currentActions.length) {
686
+ // Verify the review actually passed (reviews_complete should be true)
687
+ const finalStory = parseStory(action.storyPath);
688
+ if (finalStory.frontmatter.reviews_complete) {
689
+ console.log();
690
+ console.log(c.success('═'.repeat(50)));
691
+ console.log(c.success(`✓ Full SDLC completed successfully!`));
692
+ console.log(c.success('═'.repeat(50)));
693
+ console.log(c.dim(`Completed phases: ${currentActions.length}`));
694
+ if (retryAttempt > 0) {
695
+ console.log(c.dim(`Retry attempts: ${retryAttempt}`));
696
+ }
697
+ console.log(c.dim(`Story is now ready for PR creation.`));
698
+ await clearWorkflowState(sdlcRoot);
699
+ console.log(c.dim('Checkpoint cleared.'));
700
+ }
701
+ else {
702
+ // This shouldn't happen if our logic is correct, but handle it
703
+ console.log();
704
+ console.log(c.warning('All phases executed but reviews_complete is false.'));
705
+ console.log(c.dim('This may indicate an issue with the review process.'));
706
+ }
707
+ break;
708
+ }
709
+ }
710
+ else {
711
+ // Normal auto mode: re-assess state
712
+ const newAssessment = assessState(sdlcRoot);
713
+ if (newAssessment.recommendedActions.length === 0) {
714
+ console.log(c.success('\n✓ All actions completed!'));
715
+ await clearWorkflowState(sdlcRoot);
716
+ console.log(c.dim('Checkpoint cleared.'));
717
+ break;
718
+ }
582
719
  }
583
720
  }
584
721
  }
585
722
  }
723
+ finally {
724
+ // Restore original working directory if worktree was used
725
+ if (originalCwd) {
726
+ process.chdir(originalCwd);
727
+ }
728
+ }
586
729
  }
587
730
  /**
588
731
  * Execute a specific action
@@ -709,6 +852,10 @@ async function executeAction(action, sdlcRoot) {
709
852
  story: updatedStory,
710
853
  changesMade: ['Updated story status to done'],
711
854
  };
855
+ // Worktree cleanup prompt (if story has a worktree)
856
+ if (storyToMove.frontmatter.worktree_path) {
857
+ await handleWorktreeCleanup(storyToMove, config, c);
858
+ }
712
859
  break;
713
860
  default:
714
861
  throw new Error(`Unknown action type: ${action.type}`);
@@ -1327,4 +1474,263 @@ export async function migrate(options) {
1327
1474
  process.exit(1);
1328
1475
  }
1329
1476
  }
1477
+ /**
1478
+ * Helper function to prompt for removal confirmation
1479
+ */
1480
+ async function confirmRemoval(message) {
1481
+ const rl = readline.createInterface({
1482
+ input: process.stdin,
1483
+ output: process.stdout,
1484
+ });
1485
+ return new Promise((resolve) => {
1486
+ rl.question(message + ' (y/N): ', (answer) => {
1487
+ rl.close();
1488
+ resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
1489
+ });
1490
+ });
1491
+ }
1492
+ /**
1493
+ * Handle worktree cleanup when story moves to done
1494
+ * Prompts user in interactive mode to remove worktree
1495
+ */
1496
+ async function handleWorktreeCleanup(story, config, c) {
1497
+ const worktreePath = story.frontmatter.worktree_path;
1498
+ if (!worktreePath)
1499
+ return;
1500
+ const sdlcRoot = getSdlcRoot();
1501
+ const workingDir = path.dirname(sdlcRoot);
1502
+ const worktreeConfig = config.worktree ?? DEFAULT_WORKTREE_CONFIG;
1503
+ // Check if worktree exists
1504
+ if (!fs.existsSync(worktreePath)) {
1505
+ console.log(c.warning(` Note: Worktree path no longer exists: ${worktreePath}`));
1506
+ const updated = updateStoryField(story, 'worktree_path', undefined);
1507
+ await writeStory(updated);
1508
+ console.log(c.dim(' Cleared worktree_path from frontmatter'));
1509
+ return;
1510
+ }
1511
+ // Only prompt in interactive mode
1512
+ if (!process.stdin.isTTY) {
1513
+ console.log(c.dim(` Worktree preserved (non-interactive mode): ${worktreePath}`));
1514
+ return;
1515
+ }
1516
+ // Prompt for cleanup
1517
+ console.log();
1518
+ console.log(c.info(` Story has a worktree at: ${worktreePath}`));
1519
+ const shouldRemove = await confirmRemoval(' Remove worktree?');
1520
+ if (!shouldRemove) {
1521
+ console.log(c.dim(' Worktree preserved'));
1522
+ return;
1523
+ }
1524
+ // Remove worktree
1525
+ try {
1526
+ let resolvedBasePath;
1527
+ try {
1528
+ resolvedBasePath = validateWorktreeBasePath(worktreeConfig.basePath, workingDir);
1529
+ }
1530
+ catch {
1531
+ resolvedBasePath = path.dirname(worktreePath);
1532
+ }
1533
+ const service = new GitWorktreeService(workingDir, resolvedBasePath);
1534
+ service.remove(worktreePath);
1535
+ const updated = updateStoryField(story, 'worktree_path', undefined);
1536
+ await writeStory(updated);
1537
+ console.log(c.success(' ✓ Worktree removed'));
1538
+ }
1539
+ catch (error) {
1540
+ console.log(c.warning(` Failed to remove worktree: ${error instanceof Error ? error.message : String(error)}`));
1541
+ // Clear frontmatter anyway (user may have manually deleted)
1542
+ const updated = updateStoryField(story, 'worktree_path', undefined);
1543
+ await writeStory(updated);
1544
+ }
1545
+ }
1546
+ /**
1547
+ * List all ai-sdlc managed worktrees
1548
+ */
1549
+ export async function listWorktrees() {
1550
+ const config = loadConfig();
1551
+ const c = getThemedChalk(config);
1552
+ try {
1553
+ const sdlcRoot = getSdlcRoot();
1554
+ const workingDir = path.dirname(sdlcRoot);
1555
+ const worktreeConfig = config.worktree ?? DEFAULT_WORKTREE_CONFIG;
1556
+ // Resolve worktree base path
1557
+ let resolvedBasePath;
1558
+ try {
1559
+ resolvedBasePath = validateWorktreeBasePath(worktreeConfig.basePath, workingDir);
1560
+ }
1561
+ catch (error) {
1562
+ // If basePath doesn't exist yet, create an empty list response
1563
+ console.log();
1564
+ console.log(c.bold('═══ Worktrees ═══'));
1565
+ console.log();
1566
+ console.log(c.dim('No worktrees found.'));
1567
+ console.log(c.dim('Use `ai-sdlc worktrees add <story-id>` to create one.'));
1568
+ console.log();
1569
+ return;
1570
+ }
1571
+ const service = new GitWorktreeService(workingDir, resolvedBasePath);
1572
+ const worktrees = service.list();
1573
+ console.log();
1574
+ console.log(c.bold('═══ Worktrees ═══'));
1575
+ console.log();
1576
+ if (worktrees.length === 0) {
1577
+ console.log(c.dim('No worktrees found.'));
1578
+ console.log(c.dim('Use `ai-sdlc worktrees add <story-id>` to create one.'));
1579
+ }
1580
+ else {
1581
+ // Table header
1582
+ console.log(c.dim('Story ID'.padEnd(12) + 'Branch'.padEnd(40) + 'Status'.padEnd(10) + 'Path'));
1583
+ console.log(c.dim('─'.repeat(80)));
1584
+ for (const wt of worktrees) {
1585
+ const storyId = wt.storyId || 'unknown';
1586
+ const branch = wt.branch.length > 38 ? wt.branch.substring(0, 35) + '...' : wt.branch;
1587
+ const status = wt.exists ? c.success('exists') : c.error('missing');
1588
+ const displayPath = wt.path.length > 50 ? '...' + wt.path.slice(-47) : wt.path;
1589
+ console.log(storyId.padEnd(12) +
1590
+ branch.padEnd(40) +
1591
+ (wt.exists ? 'exists ' : 'missing ') +
1592
+ displayPath);
1593
+ }
1594
+ console.log();
1595
+ console.log(c.dim(`Total: ${worktrees.length} worktree(s)`));
1596
+ }
1597
+ console.log();
1598
+ }
1599
+ catch (error) {
1600
+ console.log(c.error(`Error listing worktrees: ${error instanceof Error ? error.message : String(error)}`));
1601
+ process.exit(1);
1602
+ }
1603
+ }
1604
+ /**
1605
+ * Create a worktree for a specific story
1606
+ */
1607
+ export async function addWorktree(storyId) {
1608
+ const spinner = ora('Creating worktree...').start();
1609
+ const config = loadConfig();
1610
+ const c = getThemedChalk(config);
1611
+ try {
1612
+ const sdlcRoot = getSdlcRoot();
1613
+ const workingDir = path.dirname(sdlcRoot);
1614
+ if (!kanbanExists(sdlcRoot)) {
1615
+ spinner.fail('ai-sdlc not initialized. Run `ai-sdlc init` first.');
1616
+ return;
1617
+ }
1618
+ // Find the story
1619
+ const story = findStoryById(sdlcRoot, storyId) || findStoryBySlug(sdlcRoot, storyId);
1620
+ if (!story) {
1621
+ spinner.fail(c.error(`Story not found: "${storyId}"`));
1622
+ console.log(c.dim('Use `ai-sdlc status` to see available stories.'));
1623
+ return;
1624
+ }
1625
+ // Check if story already has a worktree
1626
+ if (story.frontmatter.worktree_path) {
1627
+ spinner.fail(c.error(`Story already has a worktree: ${story.frontmatter.worktree_path}`));
1628
+ return;
1629
+ }
1630
+ // Resolve worktree base path
1631
+ const worktreeConfig = config.worktree ?? DEFAULT_WORKTREE_CONFIG;
1632
+ let resolvedBasePath;
1633
+ try {
1634
+ resolvedBasePath = validateWorktreeBasePath(worktreeConfig.basePath, workingDir);
1635
+ }
1636
+ catch (error) {
1637
+ spinner.fail(c.error(`Configuration Error: ${error instanceof Error ? error.message : String(error)}`));
1638
+ console.log(c.dim('Fix worktree.basePath in .ai-sdlc.json or remove it to use default location'));
1639
+ return;
1640
+ }
1641
+ const service = new GitWorktreeService(workingDir, resolvedBasePath);
1642
+ // Validate git state
1643
+ const validation = service.validateCanCreateWorktree();
1644
+ if (!validation.valid) {
1645
+ spinner.fail(c.error(validation.error || 'Cannot create worktree'));
1646
+ return;
1647
+ }
1648
+ // Detect base branch
1649
+ const baseBranch = service.detectBaseBranch();
1650
+ // Create the worktree
1651
+ const worktreePath = service.create({
1652
+ storyId: story.frontmatter.id,
1653
+ slug: story.slug,
1654
+ baseBranch,
1655
+ });
1656
+ // Update story frontmatter
1657
+ const updatedStory = updateStoryField(story, 'worktree_path', worktreePath);
1658
+ const branchName = service.getBranchName(story.frontmatter.id, story.slug);
1659
+ const storyWithBranch = updateStoryField(updatedStory, 'branch', branchName);
1660
+ await writeStory(storyWithBranch);
1661
+ spinner.succeed(c.success(`Created worktree for ${story.frontmatter.id}`));
1662
+ console.log(c.dim(` Path: ${worktreePath}`));
1663
+ console.log(c.dim(` Branch: ${branchName}`));
1664
+ console.log(c.dim(` Base: ${baseBranch}`));
1665
+ }
1666
+ catch (error) {
1667
+ spinner.fail(c.error('Failed to create worktree'));
1668
+ console.error(c.error(` ${error instanceof Error ? error.message : String(error)}`));
1669
+ process.exit(1);
1670
+ }
1671
+ }
1672
+ /**
1673
+ * Remove a worktree for a specific story
1674
+ */
1675
+ export async function removeWorktree(storyId, options) {
1676
+ const config = loadConfig();
1677
+ const c = getThemedChalk(config);
1678
+ try {
1679
+ const sdlcRoot = getSdlcRoot();
1680
+ const workingDir = path.dirname(sdlcRoot);
1681
+ if (!kanbanExists(sdlcRoot)) {
1682
+ console.log(c.warning('ai-sdlc not initialized. Run `ai-sdlc init` first.'));
1683
+ return;
1684
+ }
1685
+ // Find the story
1686
+ const story = findStoryById(sdlcRoot, storyId) || findStoryBySlug(sdlcRoot, storyId);
1687
+ if (!story) {
1688
+ console.log(c.error(`Story not found: "${storyId}"`));
1689
+ console.log(c.dim('Use `ai-sdlc status` to see available stories.'));
1690
+ return;
1691
+ }
1692
+ // Check if story has a worktree
1693
+ if (!story.frontmatter.worktree_path) {
1694
+ console.log(c.warning(`Story ${storyId} does not have a worktree.`));
1695
+ return;
1696
+ }
1697
+ const worktreePath = story.frontmatter.worktree_path;
1698
+ // Confirm removal (unless --force)
1699
+ if (!options?.force) {
1700
+ console.log();
1701
+ console.log(c.warning('About to remove worktree:'));
1702
+ console.log(c.dim(` Story: ${story.frontmatter.title}`));
1703
+ console.log(c.dim(` Path: ${worktreePath}`));
1704
+ console.log();
1705
+ const confirmed = await confirmRemoval('Are you sure you want to remove this worktree?');
1706
+ if (!confirmed) {
1707
+ console.log(c.dim('Cancelled.'));
1708
+ return;
1709
+ }
1710
+ }
1711
+ const spinner = ora('Removing worktree...').start();
1712
+ // Resolve worktree base path
1713
+ const worktreeConfig = config.worktree ?? DEFAULT_WORKTREE_CONFIG;
1714
+ let resolvedBasePath;
1715
+ try {
1716
+ resolvedBasePath = validateWorktreeBasePath(worktreeConfig.basePath, workingDir);
1717
+ }
1718
+ catch {
1719
+ // If basePath doesn't exist, use the worktree path's parent
1720
+ resolvedBasePath = path.dirname(worktreePath);
1721
+ }
1722
+ const service = new GitWorktreeService(workingDir, resolvedBasePath);
1723
+ // Remove the worktree
1724
+ service.remove(worktreePath);
1725
+ // Clear worktree_path from frontmatter
1726
+ const updatedStory = updateStoryField(story, 'worktree_path', undefined);
1727
+ await writeStory(updatedStory);
1728
+ spinner.succeed(c.success(`Removed worktree for ${story.frontmatter.id}`));
1729
+ console.log(c.dim(` Path: ${worktreePath}`));
1730
+ }
1731
+ catch (error) {
1732
+ console.log(c.error(`Failed to remove worktree: ${error instanceof Error ? error.message : String(error)}`));
1733
+ process.exit(1);
1734
+ }
1735
+ }
1330
1736
  //# sourceMappingURL=commands.js.map