ai-sdlc 0.2.0-alpha.1 → 0.2.0-alpha.11

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