ai-sdlc 0.2.0-alpha.2 → 0.2.0-alpha.20
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 +62 -0
- package/dist/agents/implementation.d.ts.map +1 -1
- package/dist/agents/implementation.js +502 -89
- package/dist/agents/implementation.js.map +1 -1
- package/dist/agents/planning.js +2 -2
- package/dist/agents/planning.js.map +1 -1
- package/dist/agents/refinement.js +2 -2
- package/dist/agents/refinement.js.map +1 -1
- package/dist/agents/research.js +2 -2
- package/dist/agents/research.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 +100 -9
- package/dist/agents/review.js.map +1 -1
- package/dist/agents/rework.js +3 -3
- package/dist/agents/rework.js.map +1 -1
- package/dist/agents/state-assessor.d.ts +3 -3
- package/dist/agents/state-assessor.d.ts.map +1 -1
- package/dist/agents/state-assessor.js +6 -6
- package/dist/agents/state-assessor.js.map +1 -1
- package/dist/agents/verification.d.ts +11 -0
- package/dist/agents/verification.d.ts.map +1 -1
- package/dist/agents/verification.js +74 -1
- package/dist/agents/verification.js.map +1 -1
- package/dist/cli/commands/migrate.js +1 -1
- package/dist/cli/commands/migrate.js.map +1 -1
- package/dist/cli/commands.d.ts +30 -1
- package/dist/cli/commands.d.ts.map +1 -1
- package/dist/cli/commands.js +637 -171
- package/dist/cli/commands.js.map +1 -1
- package/dist/cli/daemon.d.ts.map +1 -1
- package/dist/cli/daemon.js +23 -10
- 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 +39 -20
- package/dist/cli/runner.js.map +1 -1
- package/dist/core/auth.d.ts +51 -2
- package/dist/core/auth.d.ts.map +1 -1
- package/dist/core/auth.js +267 -7
- package/dist/core/auth.js.map +1 -1
- package/dist/core/client.d.ts +6 -0
- package/dist/core/client.d.ts.map +1 -1
- package/dist/core/client.js +34 -2
- package/dist/core/client.js.map +1 -1
- package/dist/core/config.d.ts +36 -1
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +161 -1
- package/dist/core/config.js.map +1 -1
- package/dist/core/conflict-detector.d.ts +108 -0
- package/dist/core/conflict-detector.d.ts.map +1 -0
- package/dist/core/conflict-detector.js +413 -0
- package/dist/core/conflict-detector.js.map +1 -0
- package/dist/core/git-utils.d.ts +28 -0
- package/dist/core/git-utils.d.ts.map +1 -0
- package/dist/core/git-utils.js +146 -0
- package/dist/core/git-utils.js.map +1 -0
- package/dist/core/index.d.ts +16 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +16 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/kanban.d.ts +1 -6
- package/dist/core/kanban.d.ts.map +1 -1
- package/dist/core/kanban.js +10 -49
- package/dist/core/kanban.js.map +1 -1
- package/dist/core/logger.d.ts +92 -0
- package/dist/core/logger.d.ts.map +1 -0
- package/dist/core/logger.js +221 -0
- package/dist/core/logger.js.map +1 -0
- package/dist/core/story.d.ts +108 -20
- package/dist/core/story.d.ts.map +1 -1
- package/dist/core/story.js +340 -59
- package/dist/core/story.js.map +1 -1
- package/dist/core/workflow-state.d.ts +45 -6
- package/dist/core/workflow-state.d.ts.map +1 -1
- package/dist/core/workflow-state.js +185 -12
- package/dist/core/workflow-state.js.map +1 -1
- package/dist/core/worktree.d.ts +77 -0
- package/dist/core/worktree.d.ts.map +1 -0
- package/dist/core/worktree.js +246 -0
- package/dist/core/worktree.js.map +1 -0
- package/dist/index.js +49 -3
- package/dist/index.js.map +1 -1
- package/dist/types/index.d.ts +101 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js.map +1 -1
- package/package.json +3 -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
|
*/
|
|
@@ -47,7 +52,7 @@ export async function status(options) {
|
|
|
47
52
|
console.log(c.warning('ai-sdlc not initialized. Run `ai-sdlc init` first.'));
|
|
48
53
|
return;
|
|
49
54
|
}
|
|
50
|
-
const assessment = assessState(sdlcRoot);
|
|
55
|
+
const assessment = await assessState(sdlcRoot);
|
|
51
56
|
const stats = getBoardStats(sdlcRoot);
|
|
52
57
|
console.log();
|
|
53
58
|
console.log(c.bold('═══ AI SDLC Board ═══'));
|
|
@@ -125,7 +130,7 @@ export async function add(title) {
|
|
|
125
130
|
spinner.fail('ai-sdlc not initialized. Run `ai-sdlc init` first.');
|
|
126
131
|
return;
|
|
127
132
|
}
|
|
128
|
-
const story = createStory(title, sdlcRoot);
|
|
133
|
+
const story = await createStory(title, sdlcRoot);
|
|
129
134
|
spinner.succeed(c.success(`Created: ${story.path}`));
|
|
130
135
|
console.log(c.dim(` ID: ${story.frontmatter.id}`));
|
|
131
136
|
console.log(c.dim(` Slug: ${story.slug}`));
|
|
@@ -203,6 +208,54 @@ function generateFullSDLCActions(story, c) {
|
|
|
203
208
|
}
|
|
204
209
|
return actions;
|
|
205
210
|
}
|
|
211
|
+
/**
|
|
212
|
+
* Actions that modify git and require validation
|
|
213
|
+
*/
|
|
214
|
+
const GIT_MODIFYING_ACTIONS = ['implement', 'review', 'create_pr'];
|
|
215
|
+
/**
|
|
216
|
+
* Check if any actions in the list require git validation
|
|
217
|
+
*/
|
|
218
|
+
function requiresGitValidation(actions) {
|
|
219
|
+
return actions.some(action => GIT_MODIFYING_ACTIONS.includes(action.type));
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Determine if worktree mode should be used based on CLI flags, story frontmatter, and config.
|
|
223
|
+
* Priority order:
|
|
224
|
+
* 1. CLI --no-worktree flag (explicit disable)
|
|
225
|
+
* 2. CLI --worktree flag (explicit enable)
|
|
226
|
+
* 3. Story frontmatter.worktree_path exists (auto-enable for resuming)
|
|
227
|
+
* 4. Config worktree.enabled (default behavior)
|
|
228
|
+
*/
|
|
229
|
+
export function determineWorktreeMode(options, worktreeConfig, targetStory) {
|
|
230
|
+
if (options.worktree === false)
|
|
231
|
+
return false;
|
|
232
|
+
if (options.worktree === true)
|
|
233
|
+
return true;
|
|
234
|
+
if (targetStory?.frontmatter.worktree_path)
|
|
235
|
+
return true;
|
|
236
|
+
return worktreeConfig.enabled;
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Display git validation errors and warnings
|
|
240
|
+
*/
|
|
241
|
+
function displayGitValidationResult(result, c) {
|
|
242
|
+
if (result.errors.length > 0) {
|
|
243
|
+
console.log();
|
|
244
|
+
console.log(c.error('Git validation failed:'));
|
|
245
|
+
for (const error of result.errors) {
|
|
246
|
+
console.log(c.error(` - ${error}`));
|
|
247
|
+
}
|
|
248
|
+
console.log();
|
|
249
|
+
console.log(c.info('To override this check, use --force (at your own risk)'));
|
|
250
|
+
}
|
|
251
|
+
if (result.warnings.length > 0) {
|
|
252
|
+
console.log();
|
|
253
|
+
console.log(c.warning('Git validation warnings:'));
|
|
254
|
+
for (const warning of result.warnings) {
|
|
255
|
+
console.log(c.warning(` - ${warning}`));
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
206
259
|
/**
|
|
207
260
|
* Run the workflow (process one action or all)
|
|
208
261
|
*/
|
|
@@ -212,8 +265,17 @@ export async function run(options) {
|
|
|
212
265
|
const maxIterationsOverride = options.maxIterations !== undefined
|
|
213
266
|
? parseInt(options.maxIterations, 10)
|
|
214
267
|
: undefined;
|
|
215
|
-
|
|
268
|
+
let sdlcRoot = getSdlcRoot();
|
|
216
269
|
const c = getThemedChalk(config);
|
|
270
|
+
// Migrate global workflow state to story-specific location if needed
|
|
271
|
+
// Only run when NOT continuing (to avoid interrupting resumed workflows)
|
|
272
|
+
if (!options.continue) {
|
|
273
|
+
const { migrateGlobalWorkflowState } = await import('../core/workflow-state.js');
|
|
274
|
+
const migrationResult = await migrateGlobalWorkflowState(sdlcRoot);
|
|
275
|
+
if (migrationResult.migrated) {
|
|
276
|
+
console.log(c.info(migrationResult.message));
|
|
277
|
+
}
|
|
278
|
+
}
|
|
217
279
|
// Handle daemon/watch mode
|
|
218
280
|
if (options.watch) {
|
|
219
281
|
console.log(c.info('🚀 Starting daemon mode...'));
|
|
@@ -251,8 +313,15 @@ export async function run(options) {
|
|
|
251
313
|
let completedActions = [];
|
|
252
314
|
let storyContentHash;
|
|
253
315
|
if (options.continue) {
|
|
254
|
-
//
|
|
255
|
-
|
|
316
|
+
// Determine storyId for loading state
|
|
317
|
+
// If --story is provided, use it; otherwise, try to infer from existing state
|
|
318
|
+
let resumeStoryId;
|
|
319
|
+
// First try: use --story flag if provided
|
|
320
|
+
if (options.story) {
|
|
321
|
+
resumeStoryId = options.story;
|
|
322
|
+
}
|
|
323
|
+
// Try to load existing state (with or without storyId)
|
|
324
|
+
const existingState = await loadWorkflowState(sdlcRoot, resumeStoryId);
|
|
256
325
|
if (!existingState) {
|
|
257
326
|
console.log(c.error('Error: No checkpoint found.'));
|
|
258
327
|
console.log(c.dim('Remove --continue flag to start a new workflow.'));
|
|
@@ -298,20 +367,37 @@ export async function run(options) {
|
|
|
298
367
|
console.log();
|
|
299
368
|
}
|
|
300
369
|
else {
|
|
370
|
+
// Early validation of story ID format before any operations that use it
|
|
371
|
+
// This prevents sanitizeStoryId from throwing before we can show a nice error
|
|
372
|
+
if (options.story && !/^[a-z0-9_-]+$/i.test(options.story.toLowerCase().trim())) {
|
|
373
|
+
console.log(c.error('Invalid story ID format. Only letters, numbers, hyphens, and underscores are allowed.'));
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
301
376
|
// Check if there's an existing state and suggest --continue
|
|
302
|
-
|
|
377
|
+
// Check both global and story-specific state
|
|
378
|
+
const hasGlobalState = hasWorkflowState(sdlcRoot);
|
|
379
|
+
const hasStoryState = options.story ? hasWorkflowState(sdlcRoot, options.story) : false;
|
|
380
|
+
if ((hasGlobalState || hasStoryState) && !options.dryRun) {
|
|
303
381
|
console.log(c.info('Note: Found previous checkpoint. Use --continue to resume.'));
|
|
304
382
|
console.log();
|
|
305
383
|
}
|
|
306
384
|
// Start new workflow
|
|
307
385
|
workflowId = generateWorkflowId();
|
|
308
386
|
}
|
|
309
|
-
let assessment = assessState(sdlcRoot);
|
|
387
|
+
let assessment = await assessState(sdlcRoot);
|
|
388
|
+
// Hoist targetStory to outer scope so it can be reused for worktree checks
|
|
389
|
+
let targetStory = null;
|
|
310
390
|
// Filter actions by story if --story flag is provided
|
|
311
391
|
if (options.story) {
|
|
312
392
|
const normalizedInput = options.story.toLowerCase().trim();
|
|
393
|
+
// SECURITY: Validate story ID format to prevent path traversal and injection
|
|
394
|
+
// Only allow alphanumeric characters, hyphens, and underscores
|
|
395
|
+
if (!/^[a-z0-9_-]+$/i.test(normalizedInput)) {
|
|
396
|
+
console.log(c.error('Invalid story ID format. Only letters, numbers, hyphens, and underscores are allowed.'));
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
313
399
|
// Try to find story by ID first, then by slug (case-insensitive)
|
|
314
|
-
|
|
400
|
+
targetStory = findStoryById(sdlcRoot, normalizedInput);
|
|
315
401
|
if (!targetStory) {
|
|
316
402
|
targetStory = findStoryBySlug(sdlcRoot, normalizedInput);
|
|
317
403
|
}
|
|
@@ -383,7 +469,8 @@ export async function run(options) {
|
|
|
383
469
|
}
|
|
384
470
|
// Clear state if workflow is complete
|
|
385
471
|
if (options.continue || hasWorkflowState(sdlcRoot)) {
|
|
386
|
-
|
|
472
|
+
// Using options.story - action not yet created in early exit path
|
|
473
|
+
await clearWorkflowState(sdlcRoot, options.story);
|
|
387
474
|
console.log(c.dim('Checkpoint cleared.'));
|
|
388
475
|
}
|
|
389
476
|
return;
|
|
@@ -424,179 +511,289 @@ export async function run(options) {
|
|
|
424
511
|
actionsToProcess = remainingActions;
|
|
425
512
|
if (actionsToProcess.length === 0) {
|
|
426
513
|
console.log(c.success('All actions from checkpoint already completed!'));
|
|
427
|
-
|
|
514
|
+
// Using options.story - action not yet created in early exit path
|
|
515
|
+
await clearWorkflowState(sdlcRoot, options.story);
|
|
428
516
|
console.log(c.dim('Checkpoint cleared.'));
|
|
429
517
|
return;
|
|
430
518
|
}
|
|
431
519
|
}
|
|
432
|
-
//
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
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.'));
|
|
520
|
+
// Handle worktree creation based on flags, config, and story frontmatter
|
|
521
|
+
// IMPORTANT: This must happen BEFORE git validation because:
|
|
522
|
+
// 1. Worktree mode allows running from protected branches (main/master)
|
|
523
|
+
// 2. The worktree will be created on a feature branch
|
|
524
|
+
let worktreePath;
|
|
525
|
+
let originalCwd;
|
|
526
|
+
let worktreeCreated = false;
|
|
527
|
+
// Determine if worktree should be used
|
|
528
|
+
// Priority: CLI flags > story frontmatter > config > default (disabled)
|
|
529
|
+
const worktreeConfig = config.worktree ?? DEFAULT_WORKTREE_CONFIG;
|
|
530
|
+
// Reuse targetStory from earlier lookup (DRY - avoids duplicate story lookup)
|
|
531
|
+
const shouldUseWorktree = determineWorktreeMode(options, worktreeConfig, targetStory);
|
|
532
|
+
// Validate that worktree mode requires --story
|
|
533
|
+
if (shouldUseWorktree && !options.story) {
|
|
534
|
+
if (options.worktree === true) {
|
|
535
|
+
console.log(c.error('Error: --worktree requires --story flag'));
|
|
452
536
|
return;
|
|
453
537
|
}
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
538
|
+
}
|
|
539
|
+
if (shouldUseWorktree && options.story && targetStory) {
|
|
540
|
+
const workingDir = path.dirname(sdlcRoot);
|
|
541
|
+
// Check if story already has an existing worktree (resume scenario)
|
|
542
|
+
const existingWorktreePath = targetStory.frontmatter.worktree_path;
|
|
543
|
+
if (existingWorktreePath && fs.existsSync(existingWorktreePath)) {
|
|
544
|
+
// Reuse existing worktree
|
|
545
|
+
originalCwd = process.cwd();
|
|
546
|
+
worktreePath = existingWorktreePath;
|
|
547
|
+
process.chdir(worktreePath);
|
|
548
|
+
sdlcRoot = getSdlcRoot();
|
|
549
|
+
worktreeCreated = true;
|
|
550
|
+
console.log(c.success(`✓ Resuming in existing worktree: ${worktreePath}`));
|
|
551
|
+
console.log(c.dim(` Branch: ai-sdlc/${targetStory.frontmatter.id}-${targetStory.slug}`));
|
|
552
|
+
console.log();
|
|
553
|
+
}
|
|
554
|
+
else {
|
|
555
|
+
// Create new worktree
|
|
556
|
+
// Resolve worktree base path from config
|
|
557
|
+
let resolvedBasePath;
|
|
558
|
+
try {
|
|
559
|
+
resolvedBasePath = validateWorktreeBasePath(worktreeConfig.basePath, workingDir);
|
|
560
|
+
}
|
|
561
|
+
catch (error) {
|
|
562
|
+
console.log(c.error(`Configuration Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
563
|
+
console.log(c.dim('Fix worktree.basePath in .ai-sdlc.json or remove it to use default location'));
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
const worktreeService = new GitWorktreeService(workingDir, resolvedBasePath);
|
|
567
|
+
// Validate git state for worktree creation
|
|
568
|
+
const validation = worktreeService.validateCanCreateWorktree();
|
|
569
|
+
if (!validation.valid) {
|
|
570
|
+
console.log(c.error(`Error: ${validation.error}`));
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
try {
|
|
574
|
+
// Detect base branch
|
|
575
|
+
const baseBranch = worktreeService.detectBaseBranch();
|
|
576
|
+
// Create worktree
|
|
577
|
+
originalCwd = process.cwd();
|
|
578
|
+
worktreePath = worktreeService.create({
|
|
579
|
+
storyId: targetStory.frontmatter.id,
|
|
580
|
+
slug: targetStory.slug,
|
|
581
|
+
baseBranch,
|
|
582
|
+
});
|
|
583
|
+
// Change to worktree directory BEFORE updating story
|
|
584
|
+
// This ensures story updates happen in the worktree, not on main
|
|
585
|
+
// (allows parallel story launches from clean main)
|
|
586
|
+
process.chdir(worktreePath);
|
|
587
|
+
// Recalculate sdlcRoot for the worktree context
|
|
588
|
+
sdlcRoot = getSdlcRoot();
|
|
589
|
+
worktreeCreated = true;
|
|
590
|
+
// Now update story frontmatter with worktree path (writes to worktree copy)
|
|
591
|
+
// Re-resolve target story in worktree context
|
|
592
|
+
const worktreeStory = findStoryById(sdlcRoot, targetStory.frontmatter.id);
|
|
593
|
+
if (worktreeStory) {
|
|
594
|
+
const updatedStory = await updateStoryField(worktreeStory, 'worktree_path', worktreePath);
|
|
595
|
+
await writeStory(updatedStory);
|
|
596
|
+
// Update targetStory reference for downstream use
|
|
597
|
+
targetStory = updatedStory;
|
|
475
598
|
}
|
|
476
|
-
|
|
477
|
-
|
|
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 : '∞';
|
|
599
|
+
console.log(c.success(`✓ Created worktree at: ${worktreePath}`));
|
|
600
|
+
console.log(c.dim(` Branch: ai-sdlc/${targetStory.frontmatter.id}-${targetStory.slug}`));
|
|
483
601
|
console.log();
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
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;
|
|
602
|
+
}
|
|
603
|
+
catch (error) {
|
|
604
|
+
// Restore directory on worktree creation failure
|
|
605
|
+
if (originalCwd) {
|
|
606
|
+
process.chdir(originalCwd);
|
|
507
607
|
}
|
|
608
|
+
console.log(c.error(`Failed to create worktree: ${error instanceof Error ? error.message : String(error)}`));
|
|
609
|
+
return;
|
|
508
610
|
}
|
|
509
611
|
}
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
currentActionIndex
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
//
|
|
542
|
-
if (isFullSDLC) {
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
612
|
+
}
|
|
613
|
+
// Validate git state before processing actions that modify git
|
|
614
|
+
// Skip protected branch check if worktree mode is active (worktree is on feature branch)
|
|
615
|
+
// Exclude .ai-sdlc/** from clean check when worktree was created (story file was just updated)
|
|
616
|
+
if (!options.force && requiresGitValidation(actionsToProcess)) {
|
|
617
|
+
const workingDir = path.dirname(sdlcRoot);
|
|
618
|
+
const gitValidationOptions = worktreeCreated
|
|
619
|
+
? { skipBranchCheck: true, excludePatterns: ['.ai-sdlc/**'] }
|
|
620
|
+
: {};
|
|
621
|
+
const gitValidation = validateGitState(workingDir, gitValidationOptions);
|
|
622
|
+
if (!gitValidation.valid) {
|
|
623
|
+
displayGitValidationResult(gitValidation, c);
|
|
624
|
+
if (worktreeCreated && originalCwd) {
|
|
625
|
+
process.chdir(originalCwd);
|
|
626
|
+
}
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
if (gitValidation.warnings.length > 0) {
|
|
630
|
+
displayGitValidationResult(gitValidation, c);
|
|
631
|
+
console.log();
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
// Process actions with retry support for Full SDLC mode
|
|
635
|
+
let currentActions = [...actionsToProcess];
|
|
636
|
+
let currentActionIndex = 0;
|
|
637
|
+
let retryAttempt = 0;
|
|
638
|
+
const MAX_DISPLAY_RETRIES = 3; // For display purposes
|
|
639
|
+
try {
|
|
640
|
+
while (currentActionIndex < currentActions.length) {
|
|
641
|
+
const action = currentActions[currentActionIndex];
|
|
642
|
+
const totalActions = currentActions.length;
|
|
643
|
+
// Enhanced progress indicator for full SDLC mode
|
|
644
|
+
if (isFullSDLC && totalActions > 1) {
|
|
645
|
+
const retryIndicator = retryAttempt > 0 ? ` (retry ${retryAttempt})` : '';
|
|
646
|
+
console.log(c.info(`\n═══ Phase ${currentActionIndex + 1}/${totalActions}: ${action.type.toUpperCase()}${retryIndicator} ═══`));
|
|
647
|
+
}
|
|
648
|
+
const actionResult = await executeAction(action, sdlcRoot);
|
|
649
|
+
// Handle action failure in full SDLC mode
|
|
650
|
+
if (!actionResult.success && isFullSDLC) {
|
|
651
|
+
console.log();
|
|
652
|
+
console.log(c.error(`✗ Phase ${action.type} failed`));
|
|
653
|
+
console.log(c.dim(`Completed ${currentActionIndex} of ${totalActions} phases`));
|
|
654
|
+
console.log(c.info('Fix the error above and use --continue to resume.'));
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
// Handle review rejection in Full SDLC mode - trigger retry loop
|
|
658
|
+
if (isFullSDLC && action.type === 'review' && actionResult.reviewResult) {
|
|
659
|
+
const reviewResult = actionResult.reviewResult;
|
|
660
|
+
if (reviewResult.decision === ReviewDecision.REJECTED) {
|
|
661
|
+
// Load fresh story state and config for retry check
|
|
662
|
+
const story = parseStory(action.storyPath);
|
|
663
|
+
const config = loadConfig();
|
|
664
|
+
// Check if we're at max retries (pass CLI override if provided)
|
|
665
|
+
if (isAtMaxRetries(story, config, maxIterationsOverride)) {
|
|
548
666
|
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(
|
|
557
|
-
|
|
558
|
-
|
|
667
|
+
console.log(c.error('═'.repeat(50)));
|
|
668
|
+
console.log(c.error(`✗ Review failed - maximum retries reached`));
|
|
669
|
+
console.log(c.error('═'.repeat(50)));
|
|
670
|
+
console.log(c.dim(`Story has reached the maximum retry limit.`));
|
|
671
|
+
console.log(c.dim(`Issues found: ${reviewResult.issues.length}`));
|
|
672
|
+
console.log(c.warning('Manual intervention required to address the review feedback.'));
|
|
673
|
+
console.log(c.info('You can:'));
|
|
674
|
+
console.log(c.dim(' 1. Fix issues manually and run again'));
|
|
675
|
+
console.log(c.dim(' 2. Reset retry count in the story frontmatter'));
|
|
676
|
+
// Using action.storyId - available from action loop context
|
|
677
|
+
await clearWorkflowState(sdlcRoot, action.storyId);
|
|
678
|
+
return;
|
|
559
679
|
}
|
|
560
|
-
|
|
561
|
-
|
|
680
|
+
// We can retry - reset RPIV cycle and loop back
|
|
681
|
+
const currentRetry = (story.frontmatter.retry_count || 0) + 1;
|
|
682
|
+
// Use CLI override, then story-specific, then config default
|
|
683
|
+
const effectiveMaxRetries = maxIterationsOverride !== undefined
|
|
684
|
+
? maxIterationsOverride
|
|
685
|
+
: (story.frontmatter.max_retries ?? config.reviewConfig?.maxRetries ?? Infinity);
|
|
686
|
+
const maxRetriesDisplay = Number.isFinite(effectiveMaxRetries) ? effectiveMaxRetries : '∞';
|
|
687
|
+
console.log();
|
|
688
|
+
console.log(c.warning(`⟳ Review rejected with ${reviewResult.issues.length} issue(s) - initiating rework (attempt ${currentRetry}/${maxRetriesDisplay})`));
|
|
689
|
+
// Display executive summary
|
|
690
|
+
const summary = generateReviewSummary(reviewResult.issues, getTerminalWidth());
|
|
691
|
+
console.log(c.dim(` Summary: ${summary}`));
|
|
692
|
+
// Reset the RPIV cycle (this increments retry_count and resets flags)
|
|
693
|
+
await resetRPIVCycle(story, reviewResult.feedback);
|
|
694
|
+
// Log what's being reset
|
|
695
|
+
console.log(c.dim(` → Reset plan_complete, implementation_complete, reviews_complete`));
|
|
696
|
+
console.log(c.dim(` → Retry count: ${currentRetry}/${maxRetriesDisplay}`));
|
|
697
|
+
// Regenerate actions starting from the phase that needs rework
|
|
698
|
+
// For now, we restart from 'plan' since that's the typical flow after research
|
|
699
|
+
const freshStory = parseStory(action.storyPath);
|
|
700
|
+
const newActions = generateFullSDLCActions(freshStory, c);
|
|
701
|
+
if (newActions.length > 0) {
|
|
702
|
+
// Replace remaining actions with the new sequence
|
|
703
|
+
currentActions = newActions;
|
|
704
|
+
currentActionIndex = 0;
|
|
705
|
+
retryAttempt++;
|
|
706
|
+
console.log(c.info(` → Restarting SDLC from ${newActions[0].type} phase`));
|
|
562
707
|
console.log();
|
|
563
|
-
|
|
564
|
-
|
|
708
|
+
continue; // Restart the loop with new actions
|
|
709
|
+
}
|
|
710
|
+
else {
|
|
711
|
+
// No actions to retry (shouldn't happen but handle gracefully)
|
|
712
|
+
console.log(c.error('Error: No actions generated for retry. Manual intervention required.'));
|
|
713
|
+
return;
|
|
565
714
|
}
|
|
566
|
-
break;
|
|
567
715
|
}
|
|
568
716
|
}
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
717
|
+
// Save checkpoint after successful action
|
|
718
|
+
if (actionResult.success) {
|
|
719
|
+
completedActions.push({
|
|
720
|
+
type: action.type,
|
|
721
|
+
storyId: action.storyId,
|
|
722
|
+
storyPath: action.storyPath,
|
|
723
|
+
completedAt: new Date().toISOString(),
|
|
724
|
+
});
|
|
725
|
+
const state = {
|
|
726
|
+
version: '1.0',
|
|
727
|
+
workflowId,
|
|
728
|
+
timestamp: new Date().toISOString(),
|
|
729
|
+
currentAction: null,
|
|
730
|
+
completedActions,
|
|
731
|
+
context: {
|
|
732
|
+
sdlcRoot,
|
|
733
|
+
options: {
|
|
734
|
+
auto: options.auto,
|
|
735
|
+
dryRun: options.dryRun,
|
|
736
|
+
story: options.story,
|
|
737
|
+
fullSDLC: isFullSDLC,
|
|
738
|
+
},
|
|
739
|
+
storyContentHash: calculateStoryHash(action.storyPath),
|
|
740
|
+
},
|
|
741
|
+
};
|
|
742
|
+
await saveWorkflowState(state, sdlcRoot, action.storyId);
|
|
743
|
+
console.log(c.dim(` ✓ Progress saved (${completedActions.length} actions completed)`));
|
|
744
|
+
}
|
|
745
|
+
currentActionIndex++;
|
|
746
|
+
// Re-assess after each action in auto mode
|
|
747
|
+
if (options.auto) {
|
|
748
|
+
// For full SDLC mode, check if all phases are complete (and review passed)
|
|
749
|
+
if (isFullSDLC) {
|
|
750
|
+
// Check if we've completed all actions in our sequence
|
|
751
|
+
if (currentActionIndex >= currentActions.length) {
|
|
752
|
+
// Verify the review actually passed (reviews_complete should be true)
|
|
753
|
+
const finalStory = parseStory(action.storyPath);
|
|
754
|
+
if (finalStory.frontmatter.reviews_complete) {
|
|
755
|
+
console.log();
|
|
756
|
+
console.log(c.success('═'.repeat(50)));
|
|
757
|
+
console.log(c.success(`✓ Full SDLC completed successfully!`));
|
|
758
|
+
console.log(c.success('═'.repeat(50)));
|
|
759
|
+
console.log(c.dim(`Completed phases: ${currentActions.length}`));
|
|
760
|
+
if (retryAttempt > 0) {
|
|
761
|
+
console.log(c.dim(`Retry attempts: ${retryAttempt}`));
|
|
762
|
+
}
|
|
763
|
+
console.log(c.dim(`Story is now ready for PR creation.`));
|
|
764
|
+
// Using action.storyId - available from action loop context
|
|
765
|
+
await clearWorkflowState(sdlcRoot, action.storyId);
|
|
766
|
+
console.log(c.dim('Checkpoint cleared.'));
|
|
767
|
+
}
|
|
768
|
+
else {
|
|
769
|
+
// This shouldn't happen if our logic is correct, but handle it
|
|
770
|
+
console.log();
|
|
771
|
+
console.log(c.warning('All phases executed but reviews_complete is false.'));
|
|
772
|
+
console.log(c.dim('This may indicate an issue with the review process.'));
|
|
773
|
+
}
|
|
774
|
+
break;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
else {
|
|
778
|
+
// Normal auto mode: re-assess state
|
|
779
|
+
const newAssessment = await assessState(sdlcRoot);
|
|
780
|
+
if (newAssessment.recommendedActions.length === 0) {
|
|
781
|
+
console.log(c.success('\n✓ All actions completed!'));
|
|
782
|
+
// Using action.storyId - available from action loop context
|
|
783
|
+
await clearWorkflowState(sdlcRoot, action.storyId);
|
|
784
|
+
console.log(c.dim('Checkpoint cleared.'));
|
|
785
|
+
break;
|
|
786
|
+
}
|
|
577
787
|
}
|
|
578
788
|
}
|
|
579
789
|
}
|
|
580
790
|
}
|
|
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;
|
|
791
|
+
finally {
|
|
792
|
+
// Restore original working directory if worktree was used
|
|
793
|
+
if (originalCwd) {
|
|
794
|
+
process.chdir(originalCwd);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
600
797
|
}
|
|
601
798
|
/**
|
|
602
799
|
* Execute a specific action
|
|
@@ -606,13 +803,19 @@ function resolveStoryPath(action, sdlcRoot) {
|
|
|
606
803
|
async function executeAction(action, sdlcRoot) {
|
|
607
804
|
const config = loadConfig();
|
|
608
805
|
const c = getThemedChalk(config);
|
|
609
|
-
//
|
|
610
|
-
|
|
611
|
-
|
|
806
|
+
// Resolve story by ID to get current path (handles moves between folders)
|
|
807
|
+
let resolvedPath;
|
|
808
|
+
try {
|
|
809
|
+
const story = getStory(sdlcRoot, action.storyId);
|
|
810
|
+
resolvedPath = story.path;
|
|
811
|
+
}
|
|
812
|
+
catch (error) {
|
|
612
813
|
console.log(c.error(`Error: Story not found for action "${action.type}"`));
|
|
613
814
|
console.log(c.dim(` Story ID: ${action.storyId}`));
|
|
614
815
|
console.log(c.dim(` Original path: ${action.storyPath}`));
|
|
615
|
-
|
|
816
|
+
if (error instanceof Error) {
|
|
817
|
+
console.log(c.dim(` ${error.message}`));
|
|
818
|
+
}
|
|
616
819
|
return { success: false };
|
|
617
820
|
}
|
|
618
821
|
// Update action path if it was stale
|
|
@@ -711,12 +914,16 @@ async function executeAction(action, sdlcRoot) {
|
|
|
711
914
|
// Update story status to done (no file move in new architecture)
|
|
712
915
|
const { updateStoryStatus } = await import('../core/story.js');
|
|
713
916
|
const storyToMove = parseStory(action.storyPath);
|
|
714
|
-
const updatedStory = updateStoryStatus(storyToMove, 'done');
|
|
917
|
+
const updatedStory = await updateStoryStatus(storyToMove, 'done');
|
|
715
918
|
result = {
|
|
716
919
|
success: true,
|
|
717
920
|
story: updatedStory,
|
|
718
921
|
changesMade: ['Updated story status to done'],
|
|
719
922
|
};
|
|
923
|
+
// Worktree cleanup prompt (if story has a worktree)
|
|
924
|
+
if (storyToMove.frontmatter.worktree_path) {
|
|
925
|
+
await handleWorktreeCleanup(storyToMove, config, c);
|
|
926
|
+
}
|
|
720
927
|
break;
|
|
721
928
|
default:
|
|
722
929
|
throw new Error(`Unknown action type: ${action.type}`);
|
|
@@ -1210,7 +1417,7 @@ function isEmptySection(content) {
|
|
|
1210
1417
|
/**
|
|
1211
1418
|
* Unblock a story from the blocked folder and move it back to the workflow
|
|
1212
1419
|
*/
|
|
1213
|
-
export function unblock(storyId, options) {
|
|
1420
|
+
export async function unblock(storyId, options) {
|
|
1214
1421
|
const spinner = ora('Unblocking story...').start();
|
|
1215
1422
|
const config = loadConfig();
|
|
1216
1423
|
const c = getThemedChalk(config);
|
|
@@ -1221,7 +1428,7 @@ export function unblock(storyId, options) {
|
|
|
1221
1428
|
return;
|
|
1222
1429
|
}
|
|
1223
1430
|
// Unblock the story (using renamed import to avoid naming conflict)
|
|
1224
|
-
const unblockedStory = unblockStory(storyId, sdlcRoot, options);
|
|
1431
|
+
const unblockedStory = await unblockStory(storyId, sdlcRoot, options);
|
|
1225
1432
|
// Determine destination folder from updated path
|
|
1226
1433
|
const destinationFolder = unblockedStory.path.match(/\/([^/]+)\/[^/]+\.md$/)?.[1] || 'unknown';
|
|
1227
1434
|
spinner.succeed(c.success(`Unblocked story ${storyId}, moved to ${destinationFolder}/`));
|
|
@@ -1335,4 +1542,263 @@ export async function migrate(options) {
|
|
|
1335
1542
|
process.exit(1);
|
|
1336
1543
|
}
|
|
1337
1544
|
}
|
|
1545
|
+
/**
|
|
1546
|
+
* Helper function to prompt for removal confirmation
|
|
1547
|
+
*/
|
|
1548
|
+
async function confirmRemoval(message) {
|
|
1549
|
+
const rl = readline.createInterface({
|
|
1550
|
+
input: process.stdin,
|
|
1551
|
+
output: process.stdout,
|
|
1552
|
+
});
|
|
1553
|
+
return new Promise((resolve) => {
|
|
1554
|
+
rl.question(message + ' (y/N): ', (answer) => {
|
|
1555
|
+
rl.close();
|
|
1556
|
+
resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
|
|
1557
|
+
});
|
|
1558
|
+
});
|
|
1559
|
+
}
|
|
1560
|
+
/**
|
|
1561
|
+
* Handle worktree cleanup when story moves to done
|
|
1562
|
+
* Prompts user in interactive mode to remove worktree
|
|
1563
|
+
*/
|
|
1564
|
+
async function handleWorktreeCleanup(story, config, c) {
|
|
1565
|
+
const worktreePath = story.frontmatter.worktree_path;
|
|
1566
|
+
if (!worktreePath)
|
|
1567
|
+
return;
|
|
1568
|
+
const sdlcRoot = getSdlcRoot();
|
|
1569
|
+
const workingDir = path.dirname(sdlcRoot);
|
|
1570
|
+
const worktreeConfig = config.worktree ?? DEFAULT_WORKTREE_CONFIG;
|
|
1571
|
+
// Check if worktree exists
|
|
1572
|
+
if (!fs.existsSync(worktreePath)) {
|
|
1573
|
+
console.log(c.warning(` Note: Worktree path no longer exists: ${worktreePath}`));
|
|
1574
|
+
const updated = await updateStoryField(story, 'worktree_path', undefined);
|
|
1575
|
+
await writeStory(updated);
|
|
1576
|
+
console.log(c.dim(' Cleared worktree_path from frontmatter'));
|
|
1577
|
+
return;
|
|
1578
|
+
}
|
|
1579
|
+
// Only prompt in interactive mode
|
|
1580
|
+
if (!process.stdin.isTTY) {
|
|
1581
|
+
console.log(c.dim(` Worktree preserved (non-interactive mode): ${worktreePath}`));
|
|
1582
|
+
return;
|
|
1583
|
+
}
|
|
1584
|
+
// Prompt for cleanup
|
|
1585
|
+
console.log();
|
|
1586
|
+
console.log(c.info(` Story has a worktree at: ${worktreePath}`));
|
|
1587
|
+
const shouldRemove = await confirmRemoval(' Remove worktree?');
|
|
1588
|
+
if (!shouldRemove) {
|
|
1589
|
+
console.log(c.dim(' Worktree preserved'));
|
|
1590
|
+
return;
|
|
1591
|
+
}
|
|
1592
|
+
// Remove worktree
|
|
1593
|
+
try {
|
|
1594
|
+
let resolvedBasePath;
|
|
1595
|
+
try {
|
|
1596
|
+
resolvedBasePath = validateWorktreeBasePath(worktreeConfig.basePath, workingDir);
|
|
1597
|
+
}
|
|
1598
|
+
catch {
|
|
1599
|
+
resolvedBasePath = path.dirname(worktreePath);
|
|
1600
|
+
}
|
|
1601
|
+
const service = new GitWorktreeService(workingDir, resolvedBasePath);
|
|
1602
|
+
service.remove(worktreePath);
|
|
1603
|
+
const updated = await updateStoryField(story, 'worktree_path', undefined);
|
|
1604
|
+
await writeStory(updated);
|
|
1605
|
+
console.log(c.success(' ✓ Worktree removed'));
|
|
1606
|
+
}
|
|
1607
|
+
catch (error) {
|
|
1608
|
+
console.log(c.warning(` Failed to remove worktree: ${error instanceof Error ? error.message : String(error)}`));
|
|
1609
|
+
// Clear frontmatter anyway (user may have manually deleted)
|
|
1610
|
+
const updated = await updateStoryField(story, 'worktree_path', undefined);
|
|
1611
|
+
await writeStory(updated);
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
/**
|
|
1615
|
+
* List all ai-sdlc managed worktrees
|
|
1616
|
+
*/
|
|
1617
|
+
export async function listWorktrees() {
|
|
1618
|
+
const config = loadConfig();
|
|
1619
|
+
const c = getThemedChalk(config);
|
|
1620
|
+
try {
|
|
1621
|
+
const sdlcRoot = getSdlcRoot();
|
|
1622
|
+
const workingDir = path.dirname(sdlcRoot);
|
|
1623
|
+
const worktreeConfig = config.worktree ?? DEFAULT_WORKTREE_CONFIG;
|
|
1624
|
+
// Resolve worktree base path
|
|
1625
|
+
let resolvedBasePath;
|
|
1626
|
+
try {
|
|
1627
|
+
resolvedBasePath = validateWorktreeBasePath(worktreeConfig.basePath, workingDir);
|
|
1628
|
+
}
|
|
1629
|
+
catch (error) {
|
|
1630
|
+
// If basePath doesn't exist yet, create an empty list response
|
|
1631
|
+
console.log();
|
|
1632
|
+
console.log(c.bold('═══ Worktrees ═══'));
|
|
1633
|
+
console.log();
|
|
1634
|
+
console.log(c.dim('No worktrees found.'));
|
|
1635
|
+
console.log(c.dim('Use `ai-sdlc worktrees add <story-id>` to create one.'));
|
|
1636
|
+
console.log();
|
|
1637
|
+
return;
|
|
1638
|
+
}
|
|
1639
|
+
const service = new GitWorktreeService(workingDir, resolvedBasePath);
|
|
1640
|
+
const worktrees = service.list();
|
|
1641
|
+
console.log();
|
|
1642
|
+
console.log(c.bold('═══ Worktrees ═══'));
|
|
1643
|
+
console.log();
|
|
1644
|
+
if (worktrees.length === 0) {
|
|
1645
|
+
console.log(c.dim('No worktrees found.'));
|
|
1646
|
+
console.log(c.dim('Use `ai-sdlc worktrees add <story-id>` to create one.'));
|
|
1647
|
+
}
|
|
1648
|
+
else {
|
|
1649
|
+
// Table header
|
|
1650
|
+
console.log(c.dim('Story ID'.padEnd(12) + 'Branch'.padEnd(40) + 'Status'.padEnd(10) + 'Path'));
|
|
1651
|
+
console.log(c.dim('─'.repeat(80)));
|
|
1652
|
+
for (const wt of worktrees) {
|
|
1653
|
+
const storyId = wt.storyId || 'unknown';
|
|
1654
|
+
const branch = wt.branch.length > 38 ? wt.branch.substring(0, 35) + '...' : wt.branch;
|
|
1655
|
+
const status = wt.exists ? c.success('exists') : c.error('missing');
|
|
1656
|
+
const displayPath = wt.path.length > 50 ? '...' + wt.path.slice(-47) : wt.path;
|
|
1657
|
+
console.log(storyId.padEnd(12) +
|
|
1658
|
+
branch.padEnd(40) +
|
|
1659
|
+
(wt.exists ? 'exists ' : 'missing ') +
|
|
1660
|
+
displayPath);
|
|
1661
|
+
}
|
|
1662
|
+
console.log();
|
|
1663
|
+
console.log(c.dim(`Total: ${worktrees.length} worktree(s)`));
|
|
1664
|
+
}
|
|
1665
|
+
console.log();
|
|
1666
|
+
}
|
|
1667
|
+
catch (error) {
|
|
1668
|
+
console.log(c.error(`Error listing worktrees: ${error instanceof Error ? error.message : String(error)}`));
|
|
1669
|
+
process.exit(1);
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
/**
|
|
1673
|
+
* Create a worktree for a specific story
|
|
1674
|
+
*/
|
|
1675
|
+
export async function addWorktree(storyId) {
|
|
1676
|
+
const spinner = ora('Creating worktree...').start();
|
|
1677
|
+
const config = loadConfig();
|
|
1678
|
+
const c = getThemedChalk(config);
|
|
1679
|
+
try {
|
|
1680
|
+
const sdlcRoot = getSdlcRoot();
|
|
1681
|
+
const workingDir = path.dirname(sdlcRoot);
|
|
1682
|
+
if (!kanbanExists(sdlcRoot)) {
|
|
1683
|
+
spinner.fail('ai-sdlc not initialized. Run `ai-sdlc init` first.');
|
|
1684
|
+
return;
|
|
1685
|
+
}
|
|
1686
|
+
// Find the story
|
|
1687
|
+
const story = findStoryById(sdlcRoot, storyId) || findStoryBySlug(sdlcRoot, storyId);
|
|
1688
|
+
if (!story) {
|
|
1689
|
+
spinner.fail(c.error(`Story not found: "${storyId}"`));
|
|
1690
|
+
console.log(c.dim('Use `ai-sdlc status` to see available stories.'));
|
|
1691
|
+
return;
|
|
1692
|
+
}
|
|
1693
|
+
// Check if story already has a worktree
|
|
1694
|
+
if (story.frontmatter.worktree_path) {
|
|
1695
|
+
spinner.fail(c.error(`Story already has a worktree: ${story.frontmatter.worktree_path}`));
|
|
1696
|
+
return;
|
|
1697
|
+
}
|
|
1698
|
+
// Resolve worktree base path
|
|
1699
|
+
const worktreeConfig = config.worktree ?? DEFAULT_WORKTREE_CONFIG;
|
|
1700
|
+
let resolvedBasePath;
|
|
1701
|
+
try {
|
|
1702
|
+
resolvedBasePath = validateWorktreeBasePath(worktreeConfig.basePath, workingDir);
|
|
1703
|
+
}
|
|
1704
|
+
catch (error) {
|
|
1705
|
+
spinner.fail(c.error(`Configuration Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
1706
|
+
console.log(c.dim('Fix worktree.basePath in .ai-sdlc.json or remove it to use default location'));
|
|
1707
|
+
return;
|
|
1708
|
+
}
|
|
1709
|
+
const service = new GitWorktreeService(workingDir, resolvedBasePath);
|
|
1710
|
+
// Validate git state
|
|
1711
|
+
const validation = service.validateCanCreateWorktree();
|
|
1712
|
+
if (!validation.valid) {
|
|
1713
|
+
spinner.fail(c.error(validation.error || 'Cannot create worktree'));
|
|
1714
|
+
return;
|
|
1715
|
+
}
|
|
1716
|
+
// Detect base branch
|
|
1717
|
+
const baseBranch = service.detectBaseBranch();
|
|
1718
|
+
// Create the worktree
|
|
1719
|
+
const worktreePath = service.create({
|
|
1720
|
+
storyId: story.frontmatter.id,
|
|
1721
|
+
slug: story.slug,
|
|
1722
|
+
baseBranch,
|
|
1723
|
+
});
|
|
1724
|
+
// Update story frontmatter
|
|
1725
|
+
const updatedStory = await updateStoryField(story, 'worktree_path', worktreePath);
|
|
1726
|
+
const branchName = service.getBranchName(story.frontmatter.id, story.slug);
|
|
1727
|
+
const storyWithBranch = await updateStoryField(updatedStory, 'branch', branchName);
|
|
1728
|
+
await writeStory(storyWithBranch);
|
|
1729
|
+
spinner.succeed(c.success(`Created worktree for ${story.frontmatter.id}`));
|
|
1730
|
+
console.log(c.dim(` Path: ${worktreePath}`));
|
|
1731
|
+
console.log(c.dim(` Branch: ${branchName}`));
|
|
1732
|
+
console.log(c.dim(` Base: ${baseBranch}`));
|
|
1733
|
+
}
|
|
1734
|
+
catch (error) {
|
|
1735
|
+
spinner.fail(c.error('Failed to create worktree'));
|
|
1736
|
+
console.error(c.error(` ${error instanceof Error ? error.message : String(error)}`));
|
|
1737
|
+
process.exit(1);
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
/**
|
|
1741
|
+
* Remove a worktree for a specific story
|
|
1742
|
+
*/
|
|
1743
|
+
export async function removeWorktree(storyId, options) {
|
|
1744
|
+
const config = loadConfig();
|
|
1745
|
+
const c = getThemedChalk(config);
|
|
1746
|
+
try {
|
|
1747
|
+
const sdlcRoot = getSdlcRoot();
|
|
1748
|
+
const workingDir = path.dirname(sdlcRoot);
|
|
1749
|
+
if (!kanbanExists(sdlcRoot)) {
|
|
1750
|
+
console.log(c.warning('ai-sdlc not initialized. Run `ai-sdlc init` first.'));
|
|
1751
|
+
return;
|
|
1752
|
+
}
|
|
1753
|
+
// Find the story
|
|
1754
|
+
const story = findStoryById(sdlcRoot, storyId) || findStoryBySlug(sdlcRoot, storyId);
|
|
1755
|
+
if (!story) {
|
|
1756
|
+
console.log(c.error(`Story not found: "${storyId}"`));
|
|
1757
|
+
console.log(c.dim('Use `ai-sdlc status` to see available stories.'));
|
|
1758
|
+
return;
|
|
1759
|
+
}
|
|
1760
|
+
// Check if story has a worktree
|
|
1761
|
+
if (!story.frontmatter.worktree_path) {
|
|
1762
|
+
console.log(c.warning(`Story ${storyId} does not have a worktree.`));
|
|
1763
|
+
return;
|
|
1764
|
+
}
|
|
1765
|
+
const worktreePath = story.frontmatter.worktree_path;
|
|
1766
|
+
// Confirm removal (unless --force)
|
|
1767
|
+
if (!options?.force) {
|
|
1768
|
+
console.log();
|
|
1769
|
+
console.log(c.warning('About to remove worktree:'));
|
|
1770
|
+
console.log(c.dim(` Story: ${story.frontmatter.title}`));
|
|
1771
|
+
console.log(c.dim(` Path: ${worktreePath}`));
|
|
1772
|
+
console.log();
|
|
1773
|
+
const confirmed = await confirmRemoval('Are you sure you want to remove this worktree?');
|
|
1774
|
+
if (!confirmed) {
|
|
1775
|
+
console.log(c.dim('Cancelled.'));
|
|
1776
|
+
return;
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
const spinner = ora('Removing worktree...').start();
|
|
1780
|
+
// Resolve worktree base path
|
|
1781
|
+
const worktreeConfig = config.worktree ?? DEFAULT_WORKTREE_CONFIG;
|
|
1782
|
+
let resolvedBasePath;
|
|
1783
|
+
try {
|
|
1784
|
+
resolvedBasePath = validateWorktreeBasePath(worktreeConfig.basePath, workingDir);
|
|
1785
|
+
}
|
|
1786
|
+
catch {
|
|
1787
|
+
// If basePath doesn't exist, use the worktree path's parent
|
|
1788
|
+
resolvedBasePath = path.dirname(worktreePath);
|
|
1789
|
+
}
|
|
1790
|
+
const service = new GitWorktreeService(workingDir, resolvedBasePath);
|
|
1791
|
+
// Remove the worktree
|
|
1792
|
+
service.remove(worktreePath);
|
|
1793
|
+
// Clear worktree_path from frontmatter
|
|
1794
|
+
const updatedStory = await updateStoryField(story, 'worktree_path', undefined);
|
|
1795
|
+
await writeStory(updatedStory);
|
|
1796
|
+
spinner.succeed(c.success(`Removed worktree for ${story.frontmatter.id}`));
|
|
1797
|
+
console.log(c.dim(` Path: ${worktreePath}`));
|
|
1798
|
+
}
|
|
1799
|
+
catch (error) {
|
|
1800
|
+
console.log(c.error(`Failed to remove worktree: ${error instanceof Error ? error.message : String(error)}`));
|
|
1801
|
+
process.exit(1);
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1338
1804
|
//# sourceMappingURL=commands.js.map
|