ai-sdlc 0.2.0-alpha.2 → 0.2.0-alpha.21

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 (88) hide show
  1. package/dist/agents/implementation.d.ts +62 -0
  2. package/dist/agents/implementation.d.ts.map +1 -1
  3. package/dist/agents/implementation.js +502 -89
  4. package/dist/agents/implementation.js.map +1 -1
  5. package/dist/agents/planning.js +2 -2
  6. package/dist/agents/planning.js.map +1 -1
  7. package/dist/agents/refinement.js +2 -2
  8. package/dist/agents/refinement.js.map +1 -1
  9. package/dist/agents/research.js +2 -2
  10. package/dist/agents/research.js.map +1 -1
  11. package/dist/agents/review.d.ts +12 -0
  12. package/dist/agents/review.d.ts.map +1 -1
  13. package/dist/agents/review.js +100 -9
  14. package/dist/agents/review.js.map +1 -1
  15. package/dist/agents/rework.js +3 -3
  16. package/dist/agents/rework.js.map +1 -1
  17. package/dist/agents/state-assessor.d.ts +3 -3
  18. package/dist/agents/state-assessor.d.ts.map +1 -1
  19. package/dist/agents/state-assessor.js +6 -6
  20. package/dist/agents/state-assessor.js.map +1 -1
  21. package/dist/agents/verification.d.ts +11 -0
  22. package/dist/agents/verification.d.ts.map +1 -1
  23. package/dist/agents/verification.js +74 -1
  24. package/dist/agents/verification.js.map +1 -1
  25. package/dist/cli/commands/migrate.js +1 -1
  26. package/dist/cli/commands/migrate.js.map +1 -1
  27. package/dist/cli/commands.d.ts +56 -2
  28. package/dist/cli/commands.d.ts.map +1 -1
  29. package/dist/cli/commands.js +806 -171
  30. package/dist/cli/commands.js.map +1 -1
  31. package/dist/cli/daemon.d.ts.map +1 -1
  32. package/dist/cli/daemon.js +23 -10
  33. package/dist/cli/daemon.js.map +1 -1
  34. package/dist/cli/formatting.js +1 -1
  35. package/dist/cli/formatting.js.map +1 -1
  36. package/dist/cli/runner.d.ts.map +1 -1
  37. package/dist/cli/runner.js +39 -20
  38. package/dist/cli/runner.js.map +1 -1
  39. package/dist/core/auth.d.ts +51 -2
  40. package/dist/core/auth.d.ts.map +1 -1
  41. package/dist/core/auth.js +267 -7
  42. package/dist/core/auth.js.map +1 -1
  43. package/dist/core/client.d.ts +6 -0
  44. package/dist/core/client.d.ts.map +1 -1
  45. package/dist/core/client.js +34 -2
  46. package/dist/core/client.js.map +1 -1
  47. package/dist/core/config.d.ts +36 -1
  48. package/dist/core/config.d.ts.map +1 -1
  49. package/dist/core/config.js +161 -1
  50. package/dist/core/config.js.map +1 -1
  51. package/dist/core/conflict-detector.d.ts +108 -0
  52. package/dist/core/conflict-detector.d.ts.map +1 -0
  53. package/dist/core/conflict-detector.js +413 -0
  54. package/dist/core/conflict-detector.js.map +1 -0
  55. package/dist/core/git-utils.d.ts +28 -0
  56. package/dist/core/git-utils.d.ts.map +1 -0
  57. package/dist/core/git-utils.js +146 -0
  58. package/dist/core/git-utils.js.map +1 -0
  59. package/dist/core/index.d.ts +16 -0
  60. package/dist/core/index.d.ts.map +1 -0
  61. package/dist/core/index.js +16 -0
  62. package/dist/core/index.js.map +1 -0
  63. package/dist/core/kanban.d.ts +1 -6
  64. package/dist/core/kanban.d.ts.map +1 -1
  65. package/dist/core/kanban.js +10 -49
  66. package/dist/core/kanban.js.map +1 -1
  67. package/dist/core/logger.d.ts +92 -0
  68. package/dist/core/logger.d.ts.map +1 -0
  69. package/dist/core/logger.js +221 -0
  70. package/dist/core/logger.js.map +1 -0
  71. package/dist/core/story.d.ts +108 -20
  72. package/dist/core/story.d.ts.map +1 -1
  73. package/dist/core/story.js +340 -59
  74. package/dist/core/story.js.map +1 -1
  75. package/dist/core/workflow-state.d.ts +45 -6
  76. package/dist/core/workflow-state.d.ts.map +1 -1
  77. package/dist/core/workflow-state.js +185 -12
  78. package/dist/core/workflow-state.js.map +1 -1
  79. package/dist/core/worktree.d.ts +77 -0
  80. package/dist/core/worktree.d.ts.map +1 -0
  81. package/dist/core/worktree.js +246 -0
  82. package/dist/core/worktree.js.map +1 -0
  83. package/dist/index.js +49 -3
  84. package/dist/index.js.map +1 -1
  85. package/dist/types/index.d.ts +110 -0
  86. package/dist/types/index.d.ts.map +1 -1
  87. package/dist/types/index.js.map +1 -1
  88. package/package.json +3 -1
@@ -1,15 +1,21 @@
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, 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';
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';
18
+ import { detectConflicts } from '../core/conflict-detector.js';
13
19
  /**
14
20
  * Initialize the .ai-sdlc folder structure
15
21
  */
@@ -47,7 +53,7 @@ export async function status(options) {
47
53
  console.log(c.warning('ai-sdlc not initialized. Run `ai-sdlc init` first.'));
48
54
  return;
49
55
  }
50
- const assessment = assessState(sdlcRoot);
56
+ const assessment = await assessState(sdlcRoot);
51
57
  const stats = getBoardStats(sdlcRoot);
52
58
  console.log();
53
59
  console.log(c.bold('═══ AI SDLC Board ═══'));
@@ -125,7 +131,7 @@ export async function add(title) {
125
131
  spinner.fail('ai-sdlc not initialized. Run `ai-sdlc init` first.');
126
132
  return;
127
133
  }
128
- const story = createStory(title, sdlcRoot);
134
+ const story = await createStory(title, sdlcRoot);
129
135
  spinner.succeed(c.success(`Created: ${story.path}`));
130
136
  console.log(c.dim(` ID: ${story.frontmatter.id}`));
131
137
  console.log(c.dim(` Slug: ${story.slug}`));
@@ -203,6 +209,208 @@ function generateFullSDLCActions(story, c) {
203
209
  }
204
210
  return actions;
205
211
  }
212
+ /**
213
+ * Actions that modify git and require validation
214
+ */
215
+ const GIT_MODIFYING_ACTIONS = ['implement', 'review', 'create_pr'];
216
+ /**
217
+ * Check if any actions in the list require git validation
218
+ */
219
+ function requiresGitValidation(actions) {
220
+ return actions.some(action => GIT_MODIFYING_ACTIONS.includes(action.type));
221
+ }
222
+ /**
223
+ * Determine if worktree mode should be used based on CLI flags, story frontmatter, and config.
224
+ * Priority order:
225
+ * 1. CLI --no-worktree flag (explicit disable)
226
+ * 2. CLI --worktree flag (explicit enable)
227
+ * 3. Story frontmatter.worktree_path exists (auto-enable for resuming)
228
+ * 4. Config worktree.enabled (default behavior)
229
+ */
230
+ export function determineWorktreeMode(options, worktreeConfig, targetStory) {
231
+ if (options.worktree === false)
232
+ return false;
233
+ if (options.worktree === true)
234
+ return true;
235
+ if (targetStory?.frontmatter.worktree_path)
236
+ return true;
237
+ return worktreeConfig.enabled;
238
+ }
239
+ /**
240
+ * Display git validation errors and warnings
241
+ */
242
+ function displayGitValidationResult(result, c) {
243
+ if (result.errors.length > 0) {
244
+ console.log();
245
+ console.log(c.error('Git validation failed:'));
246
+ for (const error of result.errors) {
247
+ console.log(c.error(` - ${error}`));
248
+ }
249
+ console.log();
250
+ console.log(c.info('To override this check, use --force (at your own risk)'));
251
+ }
252
+ if (result.warnings.length > 0) {
253
+ console.log();
254
+ console.log(c.warning('Git validation warnings:'));
255
+ for (const warning of result.warnings) {
256
+ console.log(c.warning(` - ${warning}`));
257
+ }
258
+ }
259
+ }
260
+ // ANSI escape sequence patterns for sanitization
261
+ const ANSI_CSI_PATTERN = /\x1B\[[0-9;]*[a-zA-Z]/g;
262
+ const ANSI_OSC_BEL_PATTERN = /\x1B\][^\x07]*\x07/g;
263
+ const ANSI_OSC_ESC_PATTERN = /\x1B\][^\x1B]*\x1B\\/g;
264
+ const ANSI_SINGLE_CHAR_PATTERN = /\x1B./g;
265
+ const CONTROL_CHARS_PATTERN = /[\x00-\x1F\x7F-\x9F]/g;
266
+ /**
267
+ * Sanitize a string for safe display in the terminal.
268
+ * Strips ANSI escape sequences (CSI, OSC, single-char), control characters,
269
+ * and truncates extremely long strings to prevent DoS attacks.
270
+ *
271
+ * This uses the same comprehensive ANSI stripping patterns as sanitizeReasonText
272
+ * from src/core/story.ts for consistency.
273
+ *
274
+ * @param str - The string to sanitize
275
+ * @returns Sanitized string safe for terminal display (max 500 chars)
276
+ */
277
+ function sanitizeForDisplay(str) {
278
+ const cleaned = str
279
+ .replace(ANSI_CSI_PATTERN, '') // CSI sequences (e.g., \x1B[31m)
280
+ .replace(ANSI_OSC_BEL_PATTERN, '') // OSC with BEL terminator (e.g., \x1B]...\x07)
281
+ .replace(ANSI_OSC_ESC_PATTERN, '') // OSC with ESC\ terminator (e.g., \x1B]...\x1B\\)
282
+ .replace(ANSI_SINGLE_CHAR_PATTERN, '') // Single-char escapes (e.g., \x1BH)
283
+ .replace(CONTROL_CHARS_PATTERN, ''); // Control characters (0x00-0x1F, 0x7F-0x9F)
284
+ // Truncate extremely long strings (DoS protection)
285
+ return cleaned.length > 500 ? cleaned.slice(0, 497) + '...' : cleaned;
286
+ }
287
+ /**
288
+ * Perform pre-flight conflict check before starting work on a story in a worktree.
289
+ * Warns about potential file conflicts with active stories and prompts for confirmation.
290
+ *
291
+ * **Race Condition (TOCTOU):** Multiple users can pass this check simultaneously
292
+ * before branches are created. This is an accepted risk - the window is small
293
+ * (~100ms) and git will catch conflicts during merge/PR creation. Adding file
294
+ * locks would significantly increase complexity for minimal security gain.
295
+ *
296
+ * **Security Notes:**
297
+ * - sdlcRoot is normalized and validated (absolute path, no null bytes, max 1024 chars)
298
+ * - All display output is sanitized to prevent terminal injection attacks
299
+ * - Story IDs are validated with sanitizeStoryId() then stripped with sanitizeForDisplay()
300
+ * - Error messages are generic to prevent information leakage
301
+ *
302
+ * @param targetStory - The story to check for conflicts
303
+ * @param sdlcRoot - Root directory of the .ai-sdlc folder (must be absolute, validated)
304
+ * @param options - Command options (force flag)
305
+ * @param options.force - Skip conflict check if true
306
+ * @returns PreFlightResult indicating whether to proceed and any warnings
307
+ * @throws Error if sdlcRoot is invalid (not absolute, null bytes, too long)
308
+ */
309
+ export async function preFlightConflictCheck(targetStory, sdlcRoot, options) {
310
+ const config = loadConfig();
311
+ const c = getThemedChalk(config);
312
+ // Skip if --force flag
313
+ if (options.force) {
314
+ console.log(c.warning('⚠️ Skipping conflict check (--force)'));
315
+ return { proceed: true, warnings: ['Conflict check skipped'] };
316
+ }
317
+ // Validate sdlcRoot parameter (normalize first to prevent bypass attacks)
318
+ const normalizedPath = path.normalize(sdlcRoot);
319
+ if (!path.isAbsolute(normalizedPath)) {
320
+ throw new Error('Invalid project path');
321
+ }
322
+ if (normalizedPath.includes('\0')) {
323
+ throw new Error('Invalid project path');
324
+ }
325
+ if (normalizedPath.length > 1024) {
326
+ throw new Error('Invalid project path');
327
+ }
328
+ // Check if target story is already in-progress
329
+ if (targetStory.frontmatter.status === 'in-progress') {
330
+ console.log(c.error('❌ Story is already in-progress'));
331
+ return { proceed: false, warnings: ['Story already in progress'] };
332
+ }
333
+ try {
334
+ // Query for all in-progress stories (excluding target)
335
+ // Use normalizedPath for all subsequent operations
336
+ const activeStories = findStoriesByStatus(normalizedPath, 'in-progress')
337
+ .filter(s => s.frontmatter.id !== targetStory.frontmatter.id);
338
+ if (activeStories.length === 0) {
339
+ console.log(c.success('✓ Conflict check: No overlapping files with active stories'));
340
+ return { proceed: true, warnings: [] };
341
+ }
342
+ // Run conflict detection (use normalizedPath)
343
+ const workingDir = path.dirname(normalizedPath);
344
+ const result = detectConflicts([targetStory, ...activeStories], workingDir, 'main');
345
+ // Filter conflicts involving target story
346
+ const relevantConflicts = result.conflicts.filter(conflict => conflict.storyA === targetStory.frontmatter.id || conflict.storyB === targetStory.frontmatter.id);
347
+ // Filter out 'none' severity conflicts (keep all displayable conflicts including low)
348
+ const displayableConflicts = relevantConflicts.filter(conflict => conflict.severity !== 'none');
349
+ if (displayableConflicts.length === 0) {
350
+ console.log(c.success('✓ Conflict check: No overlapping files with active stories'));
351
+ return { proceed: true, warnings: [] };
352
+ }
353
+ // Sort conflicts by severity (high -> medium -> low)
354
+ const severityOrder = { high: 0, medium: 1, low: 2, none: 3 };
355
+ const sortedConflicts = displayableConflicts.sort((a, b) => severityOrder[a.severity] - severityOrder[b.severity]);
356
+ // Display conflicts
357
+ console.log();
358
+ console.log(c.warning('⚠️ Potential conflicts detected:'));
359
+ console.log();
360
+ for (const conflict of sortedConflicts) {
361
+ const otherStoryId = conflict.storyA === targetStory.frontmatter.id ? conflict.storyB : conflict.storyA;
362
+ // Two-stage sanitization: validate structure, then strip for display
363
+ try {
364
+ const validatedTargetId = sanitizeStoryId(targetStory.frontmatter.id);
365
+ const validatedOtherId = sanitizeStoryId(otherStoryId);
366
+ const sanitizedTargetId = sanitizeForDisplay(validatedTargetId);
367
+ const sanitizedOtherId = sanitizeForDisplay(validatedOtherId);
368
+ console.log(c.warning(` ${sanitizedTargetId} may conflict with ${sanitizedOtherId}:`));
369
+ }
370
+ catch (error) {
371
+ // If validation fails, show generic error (defensive)
372
+ console.log(c.warning(` Story may have conflicting changes (invalid ID format)`));
373
+ }
374
+ // Display shared files
375
+ for (const file of conflict.sharedFiles) {
376
+ const severityLabel = conflict.severity === 'high' ? c.error('High') :
377
+ conflict.severity === 'medium' ? c.warning('Medium') :
378
+ c.info('Low');
379
+ const sanitizedFile = sanitizeForDisplay(file);
380
+ console.log(` - ${severityLabel}: ${sanitizedFile} (both stories modify this file)`);
381
+ }
382
+ // Display shared directories
383
+ for (const dir of conflict.sharedDirectories) {
384
+ const severityLabel = conflict.severity === 'high' ? c.error('High') :
385
+ conflict.severity === 'medium' ? c.warning('Medium') :
386
+ c.info('Low');
387
+ const sanitizedDir = sanitizeForDisplay(dir);
388
+ console.log(` - ${severityLabel}: ${sanitizedDir} (both stories modify files in this directory)`);
389
+ }
390
+ console.log();
391
+ const sanitizedRecommendation = sanitizeForDisplay(conflict.recommendation);
392
+ console.log(c.dim(` Recommendation: ${sanitizedRecommendation}`));
393
+ console.log();
394
+ }
395
+ // Non-interactive mode: default to declining
396
+ if (!process.stdin.isTTY) {
397
+ console.log(c.dim('Non-interactive mode: conflicts require --force to proceed'));
398
+ return { proceed: false, warnings: ['Conflicts detected'] };
399
+ }
400
+ // Interactive mode: prompt user
401
+ const shouldContinue = await confirmRemoval('Continue anyway?');
402
+ return {
403
+ proceed: shouldContinue,
404
+ warnings: shouldContinue ? ['User confirmed with conflicts'] : ['Conflicts detected']
405
+ };
406
+ }
407
+ catch (error) {
408
+ // Fail-open: allow proceeding if conflict detection fails
409
+ console.log(c.warning('⚠️ Conflict detection unavailable'));
410
+ console.log(c.dim('Proceeding without conflict check...'));
411
+ return { proceed: true, warnings: ['Conflict detection failed'] };
412
+ }
413
+ }
206
414
  /**
207
415
  * Run the workflow (process one action or all)
208
416
  */
@@ -212,8 +420,17 @@ export async function run(options) {
212
420
  const maxIterationsOverride = options.maxIterations !== undefined
213
421
  ? parseInt(options.maxIterations, 10)
214
422
  : undefined;
215
- const sdlcRoot = getSdlcRoot();
423
+ let sdlcRoot = getSdlcRoot();
216
424
  const c = getThemedChalk(config);
425
+ // Migrate global workflow state to story-specific location if needed
426
+ // Only run when NOT continuing (to avoid interrupting resumed workflows)
427
+ if (!options.continue) {
428
+ const { migrateGlobalWorkflowState } = await import('../core/workflow-state.js');
429
+ const migrationResult = await migrateGlobalWorkflowState(sdlcRoot);
430
+ if (migrationResult.migrated) {
431
+ console.log(c.info(migrationResult.message));
432
+ }
433
+ }
217
434
  // Handle daemon/watch mode
218
435
  if (options.watch) {
219
436
  console.log(c.info('🚀 Starting daemon mode...'));
@@ -251,8 +468,15 @@ export async function run(options) {
251
468
  let completedActions = [];
252
469
  let storyContentHash;
253
470
  if (options.continue) {
254
- // Try to load existing state
255
- const existingState = await loadWorkflowState(sdlcRoot);
471
+ // Determine storyId for loading state
472
+ // If --story is provided, use it; otherwise, try to infer from existing state
473
+ let resumeStoryId;
474
+ // First try: use --story flag if provided
475
+ if (options.story) {
476
+ resumeStoryId = options.story;
477
+ }
478
+ // Try to load existing state (with or without storyId)
479
+ const existingState = await loadWorkflowState(sdlcRoot, resumeStoryId);
256
480
  if (!existingState) {
257
481
  console.log(c.error('Error: No checkpoint found.'));
258
482
  console.log(c.dim('Remove --continue flag to start a new workflow.'));
@@ -298,20 +522,37 @@ export async function run(options) {
298
522
  console.log();
299
523
  }
300
524
  else {
525
+ // Early validation of story ID format before any operations that use it
526
+ // This prevents sanitizeStoryId from throwing before we can show a nice error
527
+ if (options.story && !/^[a-z0-9_-]+$/i.test(options.story.toLowerCase().trim())) {
528
+ console.log(c.error('Invalid story ID format. Only letters, numbers, hyphens, and underscores are allowed.'));
529
+ return;
530
+ }
301
531
  // Check if there's an existing state and suggest --continue
302
- if (hasWorkflowState(sdlcRoot) && !options.dryRun) {
532
+ // Check both global and story-specific state
533
+ const hasGlobalState = hasWorkflowState(sdlcRoot);
534
+ const hasStoryState = options.story ? hasWorkflowState(sdlcRoot, options.story) : false;
535
+ if ((hasGlobalState || hasStoryState) && !options.dryRun) {
303
536
  console.log(c.info('Note: Found previous checkpoint. Use --continue to resume.'));
304
537
  console.log();
305
538
  }
306
539
  // Start new workflow
307
540
  workflowId = generateWorkflowId();
308
541
  }
309
- let assessment = assessState(sdlcRoot);
542
+ let assessment = await assessState(sdlcRoot);
543
+ // Hoist targetStory to outer scope so it can be reused for worktree checks
544
+ let targetStory = null;
310
545
  // Filter actions by story if --story flag is provided
311
546
  if (options.story) {
312
547
  const normalizedInput = options.story.toLowerCase().trim();
548
+ // SECURITY: Validate story ID format to prevent path traversal and injection
549
+ // Only allow alphanumeric characters, hyphens, and underscores
550
+ if (!/^[a-z0-9_-]+$/i.test(normalizedInput)) {
551
+ console.log(c.error('Invalid story ID format. Only letters, numbers, hyphens, and underscores are allowed.'));
552
+ return;
553
+ }
313
554
  // Try to find story by ID first, then by slug (case-insensitive)
314
- let targetStory = findStoryById(sdlcRoot, normalizedInput);
555
+ targetStory = findStoryById(sdlcRoot, normalizedInput);
315
556
  if (!targetStory) {
316
557
  targetStory = findStoryBySlug(sdlcRoot, normalizedInput);
317
558
  }
@@ -383,7 +624,8 @@ export async function run(options) {
383
624
  }
384
625
  // Clear state if workflow is complete
385
626
  if (options.continue || hasWorkflowState(sdlcRoot)) {
386
- await clearWorkflowState(sdlcRoot);
627
+ // Using options.story - action not yet created in early exit path
628
+ await clearWorkflowState(sdlcRoot, options.story);
387
629
  console.log(c.dim('Checkpoint cleared.'));
388
630
  }
389
631
  return;
@@ -424,179 +666,300 @@ export async function run(options) {
424
666
  actionsToProcess = remainingActions;
425
667
  if (actionsToProcess.length === 0) {
426
668
  console.log(c.success('All actions from checkpoint already completed!'));
427
- await clearWorkflowState(sdlcRoot);
669
+ // Using options.story - action not yet created in early exit path
670
+ await clearWorkflowState(sdlcRoot, options.story);
428
671
  console.log(c.dim('Checkpoint cleared.'));
429
672
  return;
430
673
  }
431
674
  }
432
- // Process actions with retry support for Full SDLC mode
433
- let currentActions = [...actionsToProcess];
434
- let currentActionIndex = 0;
435
- let retryAttempt = 0;
436
- 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.'));
675
+ // Handle worktree creation based on flags, config, and story frontmatter
676
+ // IMPORTANT: This must happen BEFORE git validation because:
677
+ // 1. Worktree mode allows running from protected branches (main/master)
678
+ // 2. The worktree will be created on a feature branch
679
+ let worktreePath;
680
+ let originalCwd;
681
+ let worktreeCreated = false;
682
+ // Determine if worktree should be used
683
+ // Priority: CLI flags > story frontmatter > config > default (disabled)
684
+ const worktreeConfig = config.worktree ?? DEFAULT_WORKTREE_CONFIG;
685
+ // Reuse targetStory from earlier lookup (DRY - avoids duplicate story lookup)
686
+ const shouldUseWorktree = determineWorktreeMode(options, worktreeConfig, targetStory);
687
+ // Validate that worktree mode requires --story
688
+ if (shouldUseWorktree && !options.story) {
689
+ if (options.worktree === true) {
690
+ console.log(c.error('Error: --worktree requires --story flag'));
452
691
  return;
453
692
  }
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;
693
+ }
694
+ if (shouldUseWorktree && options.story && targetStory) {
695
+ // PRE-FLIGHT CHECK: Run conflict detection before creating worktree
696
+ const preFlightResult = await preFlightConflictCheck(targetStory, sdlcRoot, options);
697
+ if (!preFlightResult.proceed) {
698
+ console.log(c.error('❌ Aborting. Complete active stories first or use --force.'));
699
+ return;
700
+ }
701
+ // Log warnings if user proceeded despite conflicts (skip internal flag messages)
702
+ if (preFlightResult.warnings.length > 0 && !preFlightResult.warnings.includes('Conflict check skipped')) {
703
+ preFlightResult.warnings.forEach(w => console.log(c.dim(` ⚠ ${w}`)));
704
+ console.log();
705
+ }
706
+ const workingDir = path.dirname(sdlcRoot);
707
+ // Check if story already has an existing worktree (resume scenario)
708
+ const existingWorktreePath = targetStory.frontmatter.worktree_path;
709
+ if (existingWorktreePath && fs.existsSync(existingWorktreePath)) {
710
+ // Reuse existing worktree
711
+ originalCwd = process.cwd();
712
+ worktreePath = existingWorktreePath;
713
+ process.chdir(worktreePath);
714
+ sdlcRoot = getSdlcRoot();
715
+ worktreeCreated = true;
716
+ console.log(c.success(`✓ Resuming in existing worktree: ${worktreePath}`));
717
+ console.log(c.dim(` Branch: ai-sdlc/${targetStory.frontmatter.id}-${targetStory.slug}`));
718
+ console.log();
719
+ }
720
+ else {
721
+ // Create new worktree
722
+ // Resolve worktree base path from config
723
+ let resolvedBasePath;
724
+ try {
725
+ resolvedBasePath = validateWorktreeBasePath(worktreeConfig.basePath, workingDir);
726
+ }
727
+ catch (error) {
728
+ console.log(c.error(`Configuration Error: ${error instanceof Error ? error.message : String(error)}`));
729
+ console.log(c.dim('Fix worktree.basePath in .ai-sdlc.json or remove it to use default location'));
730
+ return;
731
+ }
732
+ const worktreeService = new GitWorktreeService(workingDir, resolvedBasePath);
733
+ // Validate git state for worktree creation
734
+ const validation = worktreeService.validateCanCreateWorktree();
735
+ if (!validation.valid) {
736
+ console.log(c.error(`Error: ${validation.error}`));
737
+ return;
738
+ }
739
+ try {
740
+ // Detect base branch
741
+ const baseBranch = worktreeService.detectBaseBranch();
742
+ // Create worktree
743
+ originalCwd = process.cwd();
744
+ worktreePath = worktreeService.create({
745
+ storyId: targetStory.frontmatter.id,
746
+ slug: targetStory.slug,
747
+ baseBranch,
748
+ });
749
+ // Change to worktree directory BEFORE updating story
750
+ // This ensures story updates happen in the worktree, not on main
751
+ // (allows parallel story launches from clean main)
752
+ process.chdir(worktreePath);
753
+ // Recalculate sdlcRoot for the worktree context
754
+ sdlcRoot = getSdlcRoot();
755
+ worktreeCreated = true;
756
+ // Now update story frontmatter with worktree path (writes to worktree copy)
757
+ // Re-resolve target story in worktree context
758
+ const worktreeStory = findStoryById(sdlcRoot, targetStory.frontmatter.id);
759
+ if (worktreeStory) {
760
+ const updatedStory = await updateStoryField(worktreeStory, 'worktree_path', worktreePath);
761
+ await writeStory(updatedStory);
762
+ // Update targetStory reference for downstream use
763
+ targetStory = updatedStory;
475
764
  }
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 : '∞';
765
+ console.log(c.success(`✓ Created worktree at: ${worktreePath}`));
766
+ console.log(c.dim(` Branch: ai-sdlc/${targetStory.frontmatter.id}-${targetStory.slug}`));
483
767
  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;
768
+ }
769
+ catch (error) {
770
+ // Restore directory on worktree creation failure
771
+ if (originalCwd) {
772
+ process.chdir(originalCwd);
507
773
  }
774
+ console.log(c.error(`Failed to create worktree: ${error instanceof Error ? error.message : String(error)}`));
775
+ return;
508
776
  }
509
777
  }
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) {
778
+ }
779
+ // Validate git state before processing actions that modify git
780
+ // Skip protected branch check if worktree mode is active (worktree is on feature branch)
781
+ // Exclude .ai-sdlc/** from clean check when worktree was created (story file was just updated)
782
+ if (!options.force && requiresGitValidation(actionsToProcess)) {
783
+ const workingDir = path.dirname(sdlcRoot);
784
+ const gitValidationOptions = worktreeCreated
785
+ ? { skipBranchCheck: true, excludePatterns: ['.ai-sdlc/**'] }
786
+ : {};
787
+ const gitValidation = validateGitState(workingDir, gitValidationOptions);
788
+ if (!gitValidation.valid) {
789
+ displayGitValidationResult(gitValidation, c);
790
+ if (worktreeCreated && originalCwd) {
791
+ process.chdir(originalCwd);
792
+ }
793
+ return;
794
+ }
795
+ if (gitValidation.warnings.length > 0) {
796
+ displayGitValidationResult(gitValidation, c);
797
+ console.log();
798
+ }
799
+ }
800
+ // Process actions with retry support for Full SDLC mode
801
+ let currentActions = [...actionsToProcess];
802
+ let currentActionIndex = 0;
803
+ let retryAttempt = 0;
804
+ const MAX_DISPLAY_RETRIES = 3; // For display purposes
805
+ try {
806
+ while (currentActionIndex < currentActions.length) {
807
+ const action = currentActions[currentActionIndex];
808
+ const totalActions = currentActions.length;
809
+ // Enhanced progress indicator for full SDLC mode
810
+ if (isFullSDLC && totalActions > 1) {
811
+ const retryIndicator = retryAttempt > 0 ? ` (retry ${retryAttempt})` : '';
812
+ console.log(c.info(`\n═══ Phase ${currentActionIndex + 1}/${totalActions}: ${action.type.toUpperCase()}${retryIndicator} ═══`));
813
+ }
814
+ const actionResult = await executeAction(action, sdlcRoot);
815
+ // Handle action failure in full SDLC mode
816
+ if (!actionResult.success && isFullSDLC) {
817
+ console.log();
818
+ console.log(c.error(`✗ Phase ${action.type} failed`));
819
+ console.log(c.dim(`Completed ${currentActionIndex} of ${totalActions} phases`));
820
+ console.log(c.info('Fix the error above and use --continue to resume.'));
821
+ return;
822
+ }
823
+ // Handle review rejection in Full SDLC mode - trigger retry loop
824
+ if (isFullSDLC && action.type === 'review' && actionResult.reviewResult) {
825
+ const reviewResult = actionResult.reviewResult;
826
+ if (reviewResult.decision === ReviewDecision.REJECTED) {
827
+ // Load fresh story state and config for retry check
828
+ const story = parseStory(action.storyPath);
829
+ const config = loadConfig();
830
+ // Check if we're at max retries (pass CLI override if provided)
831
+ if (isAtMaxRetries(story, config, maxIterationsOverride)) {
548
832
  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.`));
557
- await clearWorkflowState(sdlcRoot);
558
- console.log(c.dim('Checkpoint cleared.'));
833
+ console.log(c.error('═'.repeat(50)));
834
+ console.log(c.error(`✗ Review failed - maximum retries reached`));
835
+ console.log(c.error('═'.repeat(50)));
836
+ console.log(c.dim(`Story has reached the maximum retry limit.`));
837
+ console.log(c.dim(`Issues found: ${reviewResult.issues.length}`));
838
+ console.log(c.warning('Manual intervention required to address the review feedback.'));
839
+ console.log(c.info('You can:'));
840
+ console.log(c.dim(' 1. Fix issues manually and run again'));
841
+ console.log(c.dim(' 2. Reset retry count in the story frontmatter'));
842
+ // Using action.storyId - available from action loop context
843
+ await clearWorkflowState(sdlcRoot, action.storyId);
844
+ return;
559
845
  }
560
- else {
561
- // This shouldn't happen if our logic is correct, but handle it
846
+ // We can retry - reset RPIV cycle and loop back
847
+ const currentRetry = (story.frontmatter.retry_count || 0) + 1;
848
+ // Use CLI override, then story-specific, then config default
849
+ const effectiveMaxRetries = maxIterationsOverride !== undefined
850
+ ? maxIterationsOverride
851
+ : (story.frontmatter.max_retries ?? config.reviewConfig?.maxRetries ?? Infinity);
852
+ const maxRetriesDisplay = Number.isFinite(effectiveMaxRetries) ? effectiveMaxRetries : '∞';
853
+ console.log();
854
+ console.log(c.warning(`⟳ Review rejected with ${reviewResult.issues.length} issue(s) - initiating rework (attempt ${currentRetry}/${maxRetriesDisplay})`));
855
+ // Display executive summary
856
+ const summary = generateReviewSummary(reviewResult.issues, getTerminalWidth());
857
+ console.log(c.dim(` Summary: ${summary}`));
858
+ // Reset the RPIV cycle (this increments retry_count and resets flags)
859
+ await resetRPIVCycle(story, reviewResult.feedback);
860
+ // Log what's being reset
861
+ console.log(c.dim(` → Reset plan_complete, implementation_complete, reviews_complete`));
862
+ console.log(c.dim(` → Retry count: ${currentRetry}/${maxRetriesDisplay}`));
863
+ // Regenerate actions starting from the phase that needs rework
864
+ // For now, we restart from 'plan' since that's the typical flow after research
865
+ const freshStory = parseStory(action.storyPath);
866
+ const newActions = generateFullSDLCActions(freshStory, c);
867
+ if (newActions.length > 0) {
868
+ // Replace remaining actions with the new sequence
869
+ currentActions = newActions;
870
+ currentActionIndex = 0;
871
+ retryAttempt++;
872
+ console.log(c.info(` → Restarting SDLC from ${newActions[0].type} phase`));
562
873
  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.'));
874
+ continue; // Restart the loop with new actions
875
+ }
876
+ else {
877
+ // No actions to retry (shouldn't happen but handle gracefully)
878
+ console.log(c.error('Error: No actions generated for retry. Manual intervention required.'));
879
+ return;
565
880
  }
566
- break;
567
881
  }
568
882
  }
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;
883
+ // Save checkpoint after successful action
884
+ if (actionResult.success) {
885
+ completedActions.push({
886
+ type: action.type,
887
+ storyId: action.storyId,
888
+ storyPath: action.storyPath,
889
+ completedAt: new Date().toISOString(),
890
+ });
891
+ const state = {
892
+ version: '1.0',
893
+ workflowId,
894
+ timestamp: new Date().toISOString(),
895
+ currentAction: null,
896
+ completedActions,
897
+ context: {
898
+ sdlcRoot,
899
+ options: {
900
+ auto: options.auto,
901
+ dryRun: options.dryRun,
902
+ story: options.story,
903
+ fullSDLC: isFullSDLC,
904
+ },
905
+ storyContentHash: calculateStoryHash(action.storyPath),
906
+ },
907
+ };
908
+ await saveWorkflowState(state, sdlcRoot, action.storyId);
909
+ console.log(c.dim(` ✓ Progress saved (${completedActions.length} actions completed)`));
910
+ }
911
+ currentActionIndex++;
912
+ // Re-assess after each action in auto mode
913
+ if (options.auto) {
914
+ // For full SDLC mode, check if all phases are complete (and review passed)
915
+ if (isFullSDLC) {
916
+ // Check if we've completed all actions in our sequence
917
+ if (currentActionIndex >= currentActions.length) {
918
+ // Verify the review actually passed (reviews_complete should be true)
919
+ const finalStory = parseStory(action.storyPath);
920
+ if (finalStory.frontmatter.reviews_complete) {
921
+ console.log();
922
+ console.log(c.success('═'.repeat(50)));
923
+ console.log(c.success(`✓ Full SDLC completed successfully!`));
924
+ console.log(c.success('═'.repeat(50)));
925
+ console.log(c.dim(`Completed phases: ${currentActions.length}`));
926
+ if (retryAttempt > 0) {
927
+ console.log(c.dim(`Retry attempts: ${retryAttempt}`));
928
+ }
929
+ console.log(c.dim(`Story is now ready for PR creation.`));
930
+ // Using action.storyId - available from action loop context
931
+ await clearWorkflowState(sdlcRoot, action.storyId);
932
+ console.log(c.dim('Checkpoint cleared.'));
933
+ }
934
+ else {
935
+ // This shouldn't happen if our logic is correct, but handle it
936
+ console.log();
937
+ console.log(c.warning('All phases executed but reviews_complete is false.'));
938
+ console.log(c.dim('This may indicate an issue with the review process.'));
939
+ }
940
+ break;
941
+ }
942
+ }
943
+ else {
944
+ // Normal auto mode: re-assess state
945
+ const newAssessment = await assessState(sdlcRoot);
946
+ if (newAssessment.recommendedActions.length === 0) {
947
+ console.log(c.success('\n✓ All actions completed!'));
948
+ // Using action.storyId - available from action loop context
949
+ await clearWorkflowState(sdlcRoot, action.storyId);
950
+ console.log(c.dim('Checkpoint cleared.'));
951
+ break;
952
+ }
577
953
  }
578
954
  }
579
955
  }
580
956
  }
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;
957
+ finally {
958
+ // Restore original working directory if worktree was used
959
+ if (originalCwd) {
960
+ process.chdir(originalCwd);
961
+ }
962
+ }
600
963
  }
601
964
  /**
602
965
  * Execute a specific action
@@ -606,13 +969,19 @@ function resolveStoryPath(action, sdlcRoot) {
606
969
  async function executeAction(action, sdlcRoot) {
607
970
  const config = loadConfig();
608
971
  const c = getThemedChalk(config);
609
- // Validate and resolve the story path before executing
610
- const resolvedPath = resolveStoryPath(action, sdlcRoot);
611
- if (!resolvedPath) {
972
+ // Resolve story by ID to get current path (handles moves between folders)
973
+ let resolvedPath;
974
+ try {
975
+ const story = getStory(sdlcRoot, action.storyId);
976
+ resolvedPath = story.path;
977
+ }
978
+ catch (error) {
612
979
  console.log(c.error(`Error: Story not found for action "${action.type}"`));
613
980
  console.log(c.dim(` Story ID: ${action.storyId}`));
614
981
  console.log(c.dim(` Original path: ${action.storyPath}`));
615
- console.log(c.dim(' The story file may have been moved or deleted.'));
982
+ if (error instanceof Error) {
983
+ console.log(c.dim(` ${error.message}`));
984
+ }
616
985
  return { success: false };
617
986
  }
618
987
  // Update action path if it was stale
@@ -711,12 +1080,16 @@ async function executeAction(action, sdlcRoot) {
711
1080
  // Update story status to done (no file move in new architecture)
712
1081
  const { updateStoryStatus } = await import('../core/story.js');
713
1082
  const storyToMove = parseStory(action.storyPath);
714
- const updatedStory = updateStoryStatus(storyToMove, 'done');
1083
+ const updatedStory = await updateStoryStatus(storyToMove, 'done');
715
1084
  result = {
716
1085
  success: true,
717
1086
  story: updatedStory,
718
1087
  changesMade: ['Updated story status to done'],
719
1088
  };
1089
+ // Worktree cleanup prompt (if story has a worktree)
1090
+ if (storyToMove.frontmatter.worktree_path) {
1091
+ await handleWorktreeCleanup(storyToMove, config, c);
1092
+ }
720
1093
  break;
721
1094
  default:
722
1095
  throw new Error(`Unknown action type: ${action.type}`);
@@ -1210,7 +1583,7 @@ function isEmptySection(content) {
1210
1583
  /**
1211
1584
  * Unblock a story from the blocked folder and move it back to the workflow
1212
1585
  */
1213
- export function unblock(storyId, options) {
1586
+ export async function unblock(storyId, options) {
1214
1587
  const spinner = ora('Unblocking story...').start();
1215
1588
  const config = loadConfig();
1216
1589
  const c = getThemedChalk(config);
@@ -1221,7 +1594,7 @@ export function unblock(storyId, options) {
1221
1594
  return;
1222
1595
  }
1223
1596
  // Unblock the story (using renamed import to avoid naming conflict)
1224
- const unblockedStory = unblockStory(storyId, sdlcRoot, options);
1597
+ const unblockedStory = await unblockStory(storyId, sdlcRoot, options);
1225
1598
  // Determine destination folder from updated path
1226
1599
  const destinationFolder = unblockedStory.path.match(/\/([^/]+)\/[^/]+\.md$/)?.[1] || 'unknown';
1227
1600
  spinner.succeed(c.success(`Unblocked story ${storyId}, moved to ${destinationFolder}/`));
@@ -1335,4 +1708,266 @@ export async function migrate(options) {
1335
1708
  process.exit(1);
1336
1709
  }
1337
1710
  }
1711
+ /**
1712
+ * Helper function to prompt for removal confirmation
1713
+ */
1714
+ async function confirmRemoval(message) {
1715
+ // Sanitize message to prevent terminal injection attacks
1716
+ // Use consistent sanitizeForDisplay() for all terminal output
1717
+ const sanitizedMessage = sanitizeForDisplay(message);
1718
+ const rl = readline.createInterface({
1719
+ input: process.stdin,
1720
+ output: process.stdout,
1721
+ });
1722
+ return new Promise((resolve) => {
1723
+ rl.question(sanitizedMessage + ' (y/N): ', (answer) => {
1724
+ rl.close();
1725
+ resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
1726
+ });
1727
+ });
1728
+ }
1729
+ /**
1730
+ * Handle worktree cleanup when story moves to done
1731
+ * Prompts user in interactive mode to remove worktree
1732
+ */
1733
+ async function handleWorktreeCleanup(story, config, c) {
1734
+ const worktreePath = story.frontmatter.worktree_path;
1735
+ if (!worktreePath)
1736
+ return;
1737
+ const sdlcRoot = getSdlcRoot();
1738
+ const workingDir = path.dirname(sdlcRoot);
1739
+ const worktreeConfig = config.worktree ?? DEFAULT_WORKTREE_CONFIG;
1740
+ // Check if worktree exists
1741
+ if (!fs.existsSync(worktreePath)) {
1742
+ console.log(c.warning(` Note: Worktree path no longer exists: ${worktreePath}`));
1743
+ const updated = await updateStoryField(story, 'worktree_path', undefined);
1744
+ await writeStory(updated);
1745
+ console.log(c.dim(' Cleared worktree_path from frontmatter'));
1746
+ return;
1747
+ }
1748
+ // Only prompt in interactive mode
1749
+ if (!process.stdin.isTTY) {
1750
+ console.log(c.dim(` Worktree preserved (non-interactive mode): ${worktreePath}`));
1751
+ return;
1752
+ }
1753
+ // Prompt for cleanup
1754
+ console.log();
1755
+ console.log(c.info(` Story has a worktree at: ${worktreePath}`));
1756
+ const shouldRemove = await confirmRemoval(' Remove worktree?');
1757
+ if (!shouldRemove) {
1758
+ console.log(c.dim(' Worktree preserved'));
1759
+ return;
1760
+ }
1761
+ // Remove worktree
1762
+ try {
1763
+ let resolvedBasePath;
1764
+ try {
1765
+ resolvedBasePath = validateWorktreeBasePath(worktreeConfig.basePath, workingDir);
1766
+ }
1767
+ catch {
1768
+ resolvedBasePath = path.dirname(worktreePath);
1769
+ }
1770
+ const service = new GitWorktreeService(workingDir, resolvedBasePath);
1771
+ service.remove(worktreePath);
1772
+ const updated = await updateStoryField(story, 'worktree_path', undefined);
1773
+ await writeStory(updated);
1774
+ console.log(c.success(' ✓ Worktree removed'));
1775
+ }
1776
+ catch (error) {
1777
+ console.log(c.warning(` Failed to remove worktree: ${error instanceof Error ? error.message : String(error)}`));
1778
+ // Clear frontmatter anyway (user may have manually deleted)
1779
+ const updated = await updateStoryField(story, 'worktree_path', undefined);
1780
+ await writeStory(updated);
1781
+ }
1782
+ }
1783
+ /**
1784
+ * List all ai-sdlc managed worktrees
1785
+ */
1786
+ export async function listWorktrees() {
1787
+ const config = loadConfig();
1788
+ const c = getThemedChalk(config);
1789
+ try {
1790
+ const sdlcRoot = getSdlcRoot();
1791
+ const workingDir = path.dirname(sdlcRoot);
1792
+ const worktreeConfig = config.worktree ?? DEFAULT_WORKTREE_CONFIG;
1793
+ // Resolve worktree base path
1794
+ let resolvedBasePath;
1795
+ try {
1796
+ resolvedBasePath = validateWorktreeBasePath(worktreeConfig.basePath, workingDir);
1797
+ }
1798
+ catch (error) {
1799
+ // If basePath doesn't exist yet, create an empty list response
1800
+ console.log();
1801
+ console.log(c.bold('═══ Worktrees ═══'));
1802
+ console.log();
1803
+ console.log(c.dim('No worktrees found.'));
1804
+ console.log(c.dim('Use `ai-sdlc worktrees add <story-id>` to create one.'));
1805
+ console.log();
1806
+ return;
1807
+ }
1808
+ const service = new GitWorktreeService(workingDir, resolvedBasePath);
1809
+ const worktrees = service.list();
1810
+ console.log();
1811
+ console.log(c.bold('═══ Worktrees ═══'));
1812
+ console.log();
1813
+ if (worktrees.length === 0) {
1814
+ console.log(c.dim('No worktrees found.'));
1815
+ console.log(c.dim('Use `ai-sdlc worktrees add <story-id>` to create one.'));
1816
+ }
1817
+ else {
1818
+ // Table header
1819
+ console.log(c.dim('Story ID'.padEnd(12) + 'Branch'.padEnd(40) + 'Status'.padEnd(10) + 'Path'));
1820
+ console.log(c.dim('─'.repeat(80)));
1821
+ for (const wt of worktrees) {
1822
+ const storyId = wt.storyId || 'unknown';
1823
+ const branch = wt.branch.length > 38 ? wt.branch.substring(0, 35) + '...' : wt.branch;
1824
+ const status = wt.exists ? c.success('exists') : c.error('missing');
1825
+ const displayPath = wt.path.length > 50 ? '...' + wt.path.slice(-47) : wt.path;
1826
+ console.log(storyId.padEnd(12) +
1827
+ branch.padEnd(40) +
1828
+ (wt.exists ? 'exists ' : 'missing ') +
1829
+ displayPath);
1830
+ }
1831
+ console.log();
1832
+ console.log(c.dim(`Total: ${worktrees.length} worktree(s)`));
1833
+ }
1834
+ console.log();
1835
+ }
1836
+ catch (error) {
1837
+ console.log(c.error(`Error listing worktrees: ${error instanceof Error ? error.message : String(error)}`));
1838
+ process.exit(1);
1839
+ }
1840
+ }
1841
+ /**
1842
+ * Create a worktree for a specific story
1843
+ */
1844
+ export async function addWorktree(storyId) {
1845
+ const spinner = ora('Creating worktree...').start();
1846
+ const config = loadConfig();
1847
+ const c = getThemedChalk(config);
1848
+ try {
1849
+ const sdlcRoot = getSdlcRoot();
1850
+ const workingDir = path.dirname(sdlcRoot);
1851
+ if (!kanbanExists(sdlcRoot)) {
1852
+ spinner.fail('ai-sdlc not initialized. Run `ai-sdlc init` first.');
1853
+ return;
1854
+ }
1855
+ // Find the story
1856
+ const story = findStoryById(sdlcRoot, storyId) || findStoryBySlug(sdlcRoot, storyId);
1857
+ if (!story) {
1858
+ spinner.fail(c.error(`Story not found: "${storyId}"`));
1859
+ console.log(c.dim('Use `ai-sdlc status` to see available stories.'));
1860
+ return;
1861
+ }
1862
+ // Check if story already has a worktree
1863
+ if (story.frontmatter.worktree_path) {
1864
+ spinner.fail(c.error(`Story already has a worktree: ${story.frontmatter.worktree_path}`));
1865
+ return;
1866
+ }
1867
+ // Resolve worktree base path
1868
+ const worktreeConfig = config.worktree ?? DEFAULT_WORKTREE_CONFIG;
1869
+ let resolvedBasePath;
1870
+ try {
1871
+ resolvedBasePath = validateWorktreeBasePath(worktreeConfig.basePath, workingDir);
1872
+ }
1873
+ catch (error) {
1874
+ spinner.fail(c.error(`Configuration Error: ${error instanceof Error ? error.message : String(error)}`));
1875
+ console.log(c.dim('Fix worktree.basePath in .ai-sdlc.json or remove it to use default location'));
1876
+ return;
1877
+ }
1878
+ const service = new GitWorktreeService(workingDir, resolvedBasePath);
1879
+ // Validate git state
1880
+ const validation = service.validateCanCreateWorktree();
1881
+ if (!validation.valid) {
1882
+ spinner.fail(c.error(validation.error || 'Cannot create worktree'));
1883
+ return;
1884
+ }
1885
+ // Detect base branch
1886
+ const baseBranch = service.detectBaseBranch();
1887
+ // Create the worktree
1888
+ const worktreePath = service.create({
1889
+ storyId: story.frontmatter.id,
1890
+ slug: story.slug,
1891
+ baseBranch,
1892
+ });
1893
+ // Update story frontmatter
1894
+ const updatedStory = await updateStoryField(story, 'worktree_path', worktreePath);
1895
+ const branchName = service.getBranchName(story.frontmatter.id, story.slug);
1896
+ const storyWithBranch = await updateStoryField(updatedStory, 'branch', branchName);
1897
+ await writeStory(storyWithBranch);
1898
+ spinner.succeed(c.success(`Created worktree for ${story.frontmatter.id}`));
1899
+ console.log(c.dim(` Path: ${worktreePath}`));
1900
+ console.log(c.dim(` Branch: ${branchName}`));
1901
+ console.log(c.dim(` Base: ${baseBranch}`));
1902
+ }
1903
+ catch (error) {
1904
+ spinner.fail(c.error('Failed to create worktree'));
1905
+ console.error(c.error(` ${error instanceof Error ? error.message : String(error)}`));
1906
+ process.exit(1);
1907
+ }
1908
+ }
1909
+ /**
1910
+ * Remove a worktree for a specific story
1911
+ */
1912
+ export async function removeWorktree(storyId, options) {
1913
+ const config = loadConfig();
1914
+ const c = getThemedChalk(config);
1915
+ try {
1916
+ const sdlcRoot = getSdlcRoot();
1917
+ const workingDir = path.dirname(sdlcRoot);
1918
+ if (!kanbanExists(sdlcRoot)) {
1919
+ console.log(c.warning('ai-sdlc not initialized. Run `ai-sdlc init` first.'));
1920
+ return;
1921
+ }
1922
+ // Find the story
1923
+ const story = findStoryById(sdlcRoot, storyId) || findStoryBySlug(sdlcRoot, storyId);
1924
+ if (!story) {
1925
+ console.log(c.error(`Story not found: "${storyId}"`));
1926
+ console.log(c.dim('Use `ai-sdlc status` to see available stories.'));
1927
+ return;
1928
+ }
1929
+ // Check if story has a worktree
1930
+ if (!story.frontmatter.worktree_path) {
1931
+ console.log(c.warning(`Story ${storyId} does not have a worktree.`));
1932
+ return;
1933
+ }
1934
+ const worktreePath = story.frontmatter.worktree_path;
1935
+ // Confirm removal (unless --force)
1936
+ if (!options?.force) {
1937
+ console.log();
1938
+ console.log(c.warning('About to remove worktree:'));
1939
+ console.log(c.dim(` Story: ${story.frontmatter.title}`));
1940
+ console.log(c.dim(` Path: ${worktreePath}`));
1941
+ console.log();
1942
+ const confirmed = await confirmRemoval('Are you sure you want to remove this worktree?');
1943
+ if (!confirmed) {
1944
+ console.log(c.dim('Cancelled.'));
1945
+ return;
1946
+ }
1947
+ }
1948
+ const spinner = ora('Removing worktree...').start();
1949
+ // Resolve worktree base path
1950
+ const worktreeConfig = config.worktree ?? DEFAULT_WORKTREE_CONFIG;
1951
+ let resolvedBasePath;
1952
+ try {
1953
+ resolvedBasePath = validateWorktreeBasePath(worktreeConfig.basePath, workingDir);
1954
+ }
1955
+ catch {
1956
+ // If basePath doesn't exist, use the worktree path's parent
1957
+ resolvedBasePath = path.dirname(worktreePath);
1958
+ }
1959
+ const service = new GitWorktreeService(workingDir, resolvedBasePath);
1960
+ // Remove the worktree
1961
+ service.remove(worktreePath);
1962
+ // Clear worktree_path from frontmatter
1963
+ const updatedStory = await updateStoryField(story, 'worktree_path', undefined);
1964
+ await writeStory(updatedStory);
1965
+ spinner.succeed(c.success(`Removed worktree for ${story.frontmatter.id}`));
1966
+ console.log(c.dim(` Path: ${worktreePath}`));
1967
+ }
1968
+ catch (error) {
1969
+ console.log(c.error(`Failed to remove worktree: ${error instanceof Error ? error.message : String(error)}`));
1970
+ process.exit(1);
1971
+ }
1972
+ }
1338
1973
  //# sourceMappingURL=commands.js.map