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