ai-sdlc 0.2.0-alpha.55 → 0.2.0-alpha.57
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/cli/commands.d.ts +1 -0
- package/dist/cli/commands.d.ts.map +1 -1
- package/dist/cli/commands.js +397 -50
- package/dist/cli/commands.js.map +1 -1
- package/dist/core/story.d.ts +8 -0
- package/dist/core/story.d.ts.map +1 -1
- package/dist/core/story.js +34 -0
- package/dist/core/story.js.map +1 -1
- package/dist/core/worktree.d.ts +82 -2
- package/dist/core/worktree.d.ts.map +1 -1
- package/dist/core/worktree.js +241 -2
- package/dist/core/worktree.js.map +1 -1
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli/commands.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"commands.d.ts","sourceRoot":"","sources":["../../src/cli/commands.ts"],"names":[],"mappings":"AAUA,OAAO,EAAE,KAAK,EAAU,UAAU,EAA0H,eAAe,EAAE,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"commands.d.ts","sourceRoot":"","sources":["../../src/cli/commands.ts"],"names":[],"mappings":"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"}
|
package/dist/cli/commands.js
CHANGED
|
@@ -2,10 +2,11 @@ import ora from 'ora';
|
|
|
2
2
|
import fs from 'fs';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import * as readline from 'readline';
|
|
5
|
+
import { spawnSync } from 'child_process';
|
|
5
6
|
import { getSdlcRoot, loadConfig, initConfig, validateWorktreeBasePath, DEFAULT_WORKTREE_CONFIG } from '../core/config.js';
|
|
6
7
|
import { initializeKanban, kanbanExists, assessState, getBoardStats, findStoryBySlug, findStoriesByStatus } from '../core/kanban.js';
|
|
7
8
|
import { createStory, parseStory, resetRPIVCycle, isAtMaxRetries, unblockStory, getStory, findStoryById, updateStoryField, writeStory, sanitizeStoryId, autoCompleteStoryAfterReview, incrementImplementationRetryCount, getEffectiveMaxImplementationRetries, isAtMaxImplementationRetries, updateStoryStatus } from '../core/story.js';
|
|
8
|
-
import { GitWorktreeService } from '../core/worktree.js';
|
|
9
|
+
import { GitWorktreeService, getLastCompletedPhase, getNextPhase } from '../core/worktree.js';
|
|
9
10
|
import { ReviewDecision } from '../types/index.js';
|
|
10
11
|
import { getThemedChalk } from '../core/theme.js';
|
|
11
12
|
import { saveWorkflowState, loadWorkflowState, clearWorkflowState, generateWorkflowId, calculateStoryHash, hasWorkflowState, } from '../core/workflow-state.js';
|
|
@@ -18,6 +19,13 @@ import { validateGitState } from '../core/git-utils.js';
|
|
|
18
19
|
import { StoryLogger } from '../core/story-logger.js';
|
|
19
20
|
import { detectConflicts } from '../core/conflict-detector.js';
|
|
20
21
|
import { getLogger } from '../core/logger.js';
|
|
22
|
+
/**
|
|
23
|
+
* Branch divergence threshold for warnings
|
|
24
|
+
* When a worktree branch has diverged by more than this number of commits
|
|
25
|
+
* from the base branch (ahead or behind), a warning will be displayed
|
|
26
|
+
* suggesting the user rebase to sync with latest changes.
|
|
27
|
+
*/
|
|
28
|
+
const DIVERGENCE_WARNING_THRESHOLD = 10;
|
|
21
29
|
/**
|
|
22
30
|
* Initialize the .ai-sdlc folder structure
|
|
23
31
|
*/
|
|
@@ -493,8 +501,8 @@ export async function preFlightConflictCheck(targetStory, sdlcRoot, options) {
|
|
|
493
501
|
if (normalizedPath.length > 1024) {
|
|
494
502
|
throw new Error('Invalid project path');
|
|
495
503
|
}
|
|
496
|
-
// Check if target story is already in-progress
|
|
497
|
-
if (targetStory.frontmatter.status === 'in-progress') {
|
|
504
|
+
// Check if target story is already in-progress (allow if resuming existing worktree)
|
|
505
|
+
if (targetStory.frontmatter.status === 'in-progress' && !targetStory.frontmatter.worktree_path) {
|
|
498
506
|
console.log(c.error('❌ Story is already in-progress'));
|
|
499
507
|
return { proceed: false, warnings: ['Story already in progress'] };
|
|
500
508
|
}
|
|
@@ -599,6 +607,7 @@ export async function run(options) {
|
|
|
599
607
|
step: options.step,
|
|
600
608
|
watch: options.watch,
|
|
601
609
|
worktree: options.worktree,
|
|
610
|
+
clean: options.clean,
|
|
602
611
|
force: options.force,
|
|
603
612
|
});
|
|
604
613
|
// Migrate global workflow state to story-specific location if needed
|
|
@@ -884,17 +893,154 @@ export async function run(options) {
|
|
|
884
893
|
}
|
|
885
894
|
const workingDir = path.dirname(sdlcRoot);
|
|
886
895
|
// Check if story already has an existing worktree (resume scenario)
|
|
896
|
+
// Note: We check only if existingWorktreePath is set, not if it exists.
|
|
897
|
+
// The validation logic will handle missing directories/branches.
|
|
887
898
|
const existingWorktreePath = targetStory.frontmatter.worktree_path;
|
|
888
|
-
if (existingWorktreePath
|
|
899
|
+
if (existingWorktreePath) {
|
|
900
|
+
// Validate worktree before resuming
|
|
901
|
+
const resolvedBasePath = validateWorktreeBasePath(worktreeConfig.basePath, workingDir);
|
|
902
|
+
// Security validation: ensure worktree_path is within the configured base directory
|
|
903
|
+
const absoluteWorktreePath = path.resolve(existingWorktreePath);
|
|
904
|
+
const absoluteBasePath = path.resolve(resolvedBasePath);
|
|
905
|
+
if (!absoluteWorktreePath.startsWith(absoluteBasePath)) {
|
|
906
|
+
console.log(c.error('Security Error: worktree_path is outside configured base directory'));
|
|
907
|
+
console.log(c.dim(` Worktree path: ${absoluteWorktreePath}`));
|
|
908
|
+
console.log(c.dim(` Expected base: ${absoluteBasePath}`));
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
// Warn if story is marked as done but has an existing worktree
|
|
912
|
+
if (targetStory.frontmatter.status === 'done') {
|
|
913
|
+
console.log(c.warning('⚠ Story is marked as done but has an existing worktree'));
|
|
914
|
+
console.log(c.dim(' This may be a stale worktree that should be cleaned up.'));
|
|
915
|
+
console.log();
|
|
916
|
+
// Prompt user for confirmation to proceed
|
|
917
|
+
const rl = readline.createInterface({
|
|
918
|
+
input: process.stdin,
|
|
919
|
+
output: process.stdout,
|
|
920
|
+
});
|
|
921
|
+
const answer = await new Promise((resolve) => {
|
|
922
|
+
rl.question(c.dim('Continue with this worktree? (y/N): '), (ans) => {
|
|
923
|
+
rl.close();
|
|
924
|
+
resolve(ans.toLowerCase().trim());
|
|
925
|
+
});
|
|
926
|
+
});
|
|
927
|
+
if (answer !== 'y' && answer !== 'yes') {
|
|
928
|
+
console.log(c.dim('Aborted. Consider removing the worktree_path from the story frontmatter.'));
|
|
929
|
+
return;
|
|
930
|
+
}
|
|
931
|
+
console.log();
|
|
932
|
+
}
|
|
933
|
+
const worktreeService = new GitWorktreeService(workingDir, resolvedBasePath);
|
|
934
|
+
const branchName = worktreeService.getBranchName(targetStory.frontmatter.id, targetStory.slug);
|
|
935
|
+
const validation = worktreeService.validateWorktreeForResume(existingWorktreePath, branchName);
|
|
936
|
+
if (!validation.canResume) {
|
|
937
|
+
console.log(c.error('Cannot resume worktree:'));
|
|
938
|
+
validation.issues.forEach(issue => console.log(c.dim(` ✗ ${issue}`)));
|
|
939
|
+
if (validation.requiresRecreation) {
|
|
940
|
+
const branchExists = !validation.issues.includes('Branch does not exist');
|
|
941
|
+
const dirMissing = validation.issues.includes('Worktree directory does not exist');
|
|
942
|
+
const dirExists = !dirMissing;
|
|
943
|
+
// Case 1: Directory missing but branch exists - recreate worktree from existing branch
|
|
944
|
+
// Case 2: Directory exists but branch missing - recreate with new branch
|
|
945
|
+
if ((branchExists && dirMissing) || (!branchExists && dirExists)) {
|
|
946
|
+
const reason = branchExists
|
|
947
|
+
? 'Branch exists - automatically recreating worktree directory'
|
|
948
|
+
: 'Directory exists - automatically recreating worktree with new branch';
|
|
949
|
+
console.log(c.dim(`\n✓ ${reason}`));
|
|
950
|
+
try {
|
|
951
|
+
// Remove the old worktree reference if it exists
|
|
952
|
+
const removeResult = spawnSync('git', ['worktree', 'remove', existingWorktreePath, '--force'], {
|
|
953
|
+
cwd: workingDir,
|
|
954
|
+
encoding: 'utf-8',
|
|
955
|
+
shell: false,
|
|
956
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
957
|
+
});
|
|
958
|
+
// Create the worktree at the same path
|
|
959
|
+
// If branch exists, checkout that branch; otherwise create a new branch
|
|
960
|
+
const baseBranch = worktreeService.detectBaseBranch();
|
|
961
|
+
const worktreeAddArgs = branchExists
|
|
962
|
+
? ['worktree', 'add', existingWorktreePath, branchName]
|
|
963
|
+
: ['worktree', 'add', '-b', branchName, existingWorktreePath, baseBranch];
|
|
964
|
+
const addResult = spawnSync('git', worktreeAddArgs, {
|
|
965
|
+
cwd: workingDir,
|
|
966
|
+
encoding: 'utf-8',
|
|
967
|
+
shell: false,
|
|
968
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
969
|
+
});
|
|
970
|
+
if (addResult.status !== 0) {
|
|
971
|
+
throw new Error(`Failed to recreate worktree: ${addResult.stderr}`);
|
|
972
|
+
}
|
|
973
|
+
// Install dependencies in the recreated worktree
|
|
974
|
+
worktreeService.installDependencies(existingWorktreePath);
|
|
975
|
+
console.log(c.success(`✓ Worktree recreated at ${existingWorktreePath}`));
|
|
976
|
+
getLogger().info('worktree', `Recreated worktree for ${targetStory.frontmatter.id} at ${existingWorktreePath}`);
|
|
977
|
+
}
|
|
978
|
+
catch (error) {
|
|
979
|
+
console.log(c.error(`Failed to recreate worktree: ${error instanceof Error ? error.message : String(error)}`));
|
|
980
|
+
console.log(c.dim('Please manually remove the worktree_path from the story frontmatter and try again.'));
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
else {
|
|
985
|
+
console.log(c.dim('\nWorktree needs manual intervention. Please remove the worktree_path from the story frontmatter and try again.'));
|
|
986
|
+
return;
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
else {
|
|
990
|
+
return;
|
|
991
|
+
}
|
|
992
|
+
}
|
|
889
993
|
// Reuse existing worktree
|
|
890
994
|
originalCwd = process.cwd();
|
|
891
995
|
worktreePath = existingWorktreePath;
|
|
892
996
|
process.chdir(worktreePath);
|
|
893
997
|
sdlcRoot = getSdlcRoot();
|
|
894
998
|
worktreeCreated = true;
|
|
999
|
+
// Re-load story from worktree context to get current state
|
|
1000
|
+
const worktreeStory = findStoryById(sdlcRoot, targetStory.frontmatter.id);
|
|
1001
|
+
if (worktreeStory) {
|
|
1002
|
+
targetStory = worktreeStory;
|
|
1003
|
+
}
|
|
1004
|
+
// Get phase information for resume context
|
|
1005
|
+
const lastPhase = getLastCompletedPhase(targetStory);
|
|
1006
|
+
const nextPhase = getNextPhase(targetStory);
|
|
1007
|
+
// Get worktree status for uncommitted changes info
|
|
1008
|
+
const worktreeInfo = {
|
|
1009
|
+
path: existingWorktreePath,
|
|
1010
|
+
branch: branchName,
|
|
1011
|
+
storyId: targetStory.frontmatter.id,
|
|
1012
|
+
exists: true,
|
|
1013
|
+
};
|
|
1014
|
+
const worktreeStatus = worktreeService.getWorktreeStatus(worktreeInfo);
|
|
1015
|
+
// Check branch divergence
|
|
1016
|
+
const divergence = worktreeService.checkBranchDivergence(branchName);
|
|
895
1017
|
console.log(c.success(`✓ Resuming in existing worktree: ${worktreePath}`));
|
|
896
|
-
console.log(c.dim(` Branch:
|
|
1018
|
+
console.log(c.dim(` Branch: ${branchName}`));
|
|
1019
|
+
if (lastPhase) {
|
|
1020
|
+
console.log(c.dim(` Last completed phase: ${lastPhase}`));
|
|
1021
|
+
}
|
|
1022
|
+
if (nextPhase) {
|
|
1023
|
+
console.log(c.dim(` Next phase: ${nextPhase}`));
|
|
1024
|
+
}
|
|
1025
|
+
// Display uncommitted changes if present
|
|
1026
|
+
if (worktreeStatus.workingDirectoryStatus !== 'clean') {
|
|
1027
|
+
const totalChanges = worktreeStatus.modifiedFiles.length + worktreeStatus.untrackedFiles.length;
|
|
1028
|
+
console.log(c.dim(` Uncommitted changes: ${totalChanges} file(s)`));
|
|
1029
|
+
if (worktreeStatus.modifiedFiles.length > 0) {
|
|
1030
|
+
console.log(c.dim(` Modified: ${worktreeStatus.modifiedFiles.slice(0, 3).join(', ')}${worktreeStatus.modifiedFiles.length > 3 ? '...' : ''}`));
|
|
1031
|
+
}
|
|
1032
|
+
if (worktreeStatus.untrackedFiles.length > 0) {
|
|
1033
|
+
console.log(c.dim(` Untracked: ${worktreeStatus.untrackedFiles.slice(0, 3).join(', ')}${worktreeStatus.untrackedFiles.length > 3 ? '...' : ''}`));
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
// Warn if branch has diverged significantly
|
|
1037
|
+
if (divergence.diverged && (divergence.ahead > DIVERGENCE_WARNING_THRESHOLD || divergence.behind > DIVERGENCE_WARNING_THRESHOLD)) {
|
|
1038
|
+
console.log(c.warning(` ⚠ Branch has diverged from base: ${divergence.ahead} ahead, ${divergence.behind} behind`));
|
|
1039
|
+
console.log(c.dim(` Consider rebasing to sync with latest changes`));
|
|
1040
|
+
}
|
|
897
1041
|
console.log();
|
|
1042
|
+
// Log resume event
|
|
1043
|
+
getLogger().info('worktree', `Resumed worktree for ${targetStory.frontmatter.id} at ${worktreePath}`);
|
|
898
1044
|
}
|
|
899
1045
|
else {
|
|
900
1046
|
// Create new worktree
|
|
@@ -913,55 +1059,256 @@ export async function run(options) {
|
|
|
913
1059
|
// This catches scenarios where workflow was interrupted after worktree creation
|
|
914
1060
|
// but before the story file was updated
|
|
915
1061
|
const existingWorktree = worktreeService.findByStoryId(targetStory.frontmatter.id);
|
|
1062
|
+
let shouldCreateNewWorktree = !existingWorktree || !existingWorktree.exists;
|
|
916
1063
|
if (existingWorktree && existingWorktree.exists) {
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
1064
|
+
// Handle --clean flag: cleanup and restart
|
|
1065
|
+
if (options.clean) {
|
|
1066
|
+
console.log(c.warning('Existing worktree found - cleaning up before restart...'));
|
|
1067
|
+
console.log();
|
|
1068
|
+
const worktreeStatus = worktreeService.getWorktreeStatus(existingWorktree);
|
|
1069
|
+
const unpushedResult = worktreeService.hasUnpushedCommits(existingWorktree.path);
|
|
1070
|
+
const commitCount = worktreeService.getCommitCount(existingWorktree.path);
|
|
1071
|
+
const branchOnRemote = worktreeService.branchExistsOnRemote(existingWorktree.branch);
|
|
1072
|
+
// Display summary of what will be deleted
|
|
1073
|
+
console.log(c.bold('Cleanup Summary:'));
|
|
1074
|
+
console.log(c.dim('─'.repeat(60)));
|
|
1075
|
+
console.log(`${c.dim('Worktree Path:')} ${worktreeStatus.path}`);
|
|
1076
|
+
console.log(`${c.dim('Branch:')} ${worktreeStatus.branch}`);
|
|
1077
|
+
console.log(`${c.dim('Total Commits:')} ${commitCount}`);
|
|
1078
|
+
console.log(`${c.dim('Unpushed Commits:')} ${unpushedResult.hasUnpushed ? c.warning(unpushedResult.count.toString()) : c.success('0')}`);
|
|
1079
|
+
console.log(`${c.dim('Modified Files:')} ${worktreeStatus.modifiedFiles.length > 0 ? c.warning(worktreeStatus.modifiedFiles.length.toString()) : c.success('0')}`);
|
|
1080
|
+
console.log(`${c.dim('Untracked Files:')} ${worktreeStatus.untrackedFiles.length > 0 ? c.warning(worktreeStatus.untrackedFiles.length.toString()) : c.success('0')}`);
|
|
1081
|
+
console.log(`${c.dim('Remote Branch:')} ${branchOnRemote ? c.warning('EXISTS') : c.dim('none')}`);
|
|
1082
|
+
console.log();
|
|
1083
|
+
// Warn about data loss
|
|
1084
|
+
if (worktreeStatus.modifiedFiles.length > 0 || worktreeStatus.untrackedFiles.length > 0 || unpushedResult.hasUnpushed) {
|
|
1085
|
+
console.log(c.error('⚠ WARNING: This will DELETE all uncommitted and unpushed work!'));
|
|
1086
|
+
console.log();
|
|
1087
|
+
}
|
|
1088
|
+
// Check for --force flag to skip confirmation
|
|
1089
|
+
const forceCleanup = options.force;
|
|
1090
|
+
if (!forceCleanup) {
|
|
1091
|
+
// Prompt for confirmation
|
|
1092
|
+
const confirmed = await new Promise((resolve) => {
|
|
1093
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
1094
|
+
rl.question(c.warning('Are you sure you want to proceed? (y/N): '), (answer) => {
|
|
1095
|
+
rl.close();
|
|
1096
|
+
resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
|
|
1097
|
+
});
|
|
1098
|
+
});
|
|
1099
|
+
if (!confirmed) {
|
|
1100
|
+
console.log(c.info('Cleanup cancelled.'));
|
|
1101
|
+
return;
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
console.log();
|
|
1105
|
+
const cleanupSpinner = ora('Cleaning up worktree...').start();
|
|
1106
|
+
try {
|
|
1107
|
+
// Remove worktree (force remove to handle uncommitted changes)
|
|
1108
|
+
const forceRemove = worktreeStatus.modifiedFiles.length > 0 || worktreeStatus.untrackedFiles.length > 0;
|
|
1109
|
+
worktreeService.remove(existingWorktree.path, forceRemove);
|
|
1110
|
+
cleanupSpinner.text = 'Worktree removed, deleting branch...';
|
|
1111
|
+
// Delete local branch
|
|
1112
|
+
worktreeService.deleteBranch(existingWorktree.branch, true);
|
|
1113
|
+
// Optionally delete remote branch if it exists
|
|
1114
|
+
if (branchOnRemote) {
|
|
1115
|
+
if (!forceCleanup) {
|
|
1116
|
+
cleanupSpinner.stop();
|
|
1117
|
+
const deleteRemote = await new Promise((resolve) => {
|
|
1118
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
1119
|
+
rl.question(c.warning('Branch exists on remote. Delete it too? (y/N): '), (answer) => {
|
|
1120
|
+
rl.close();
|
|
1121
|
+
resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
|
|
1122
|
+
});
|
|
1123
|
+
});
|
|
1124
|
+
if (deleteRemote) {
|
|
1125
|
+
cleanupSpinner.start('Deleting remote branch...');
|
|
1126
|
+
worktreeService.deleteRemoteBranch(existingWorktree.branch);
|
|
1127
|
+
}
|
|
1128
|
+
cleanupSpinner.start();
|
|
1129
|
+
}
|
|
1130
|
+
else {
|
|
1131
|
+
// --force provided, skip remote deletion by default (safer)
|
|
1132
|
+
cleanupSpinner.text = 'Skipping remote branch deletion (use manual cleanup if needed)';
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
// Reset story workflow state
|
|
1136
|
+
cleanupSpinner.text = 'Resetting story state...';
|
|
1137
|
+
const { resetWorkflowState } = await import('../core/story.js');
|
|
1138
|
+
targetStory = await resetWorkflowState(targetStory);
|
|
1139
|
+
// Clear workflow checkpoint if exists
|
|
1140
|
+
if (hasWorkflowState(sdlcRoot, targetStory.frontmatter.id)) {
|
|
1141
|
+
await clearWorkflowState(sdlcRoot, targetStory.frontmatter.id);
|
|
1142
|
+
}
|
|
1143
|
+
cleanupSpinner.succeed(c.success('✓ Cleanup complete - ready to create fresh worktree'));
|
|
1144
|
+
console.log();
|
|
1145
|
+
}
|
|
1146
|
+
catch (error) {
|
|
1147
|
+
cleanupSpinner.fail(c.error('Cleanup failed'));
|
|
1148
|
+
console.log(c.error(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
1149
|
+
return;
|
|
1150
|
+
}
|
|
1151
|
+
// After cleanup, create a fresh worktree
|
|
1152
|
+
shouldCreateNewWorktree = true;
|
|
1153
|
+
}
|
|
1154
|
+
else {
|
|
1155
|
+
// Not cleaning - resume in existing worktree (S-0063 feature)
|
|
1156
|
+
getLogger().info('worktree', `Detected existing worktree for ${targetStory.frontmatter.id} at ${existingWorktree.path}`);
|
|
1157
|
+
// Validate the existing worktree before resuming
|
|
1158
|
+
const branchName = worktreeService.getBranchName(targetStory.frontmatter.id, targetStory.slug);
|
|
1159
|
+
const validation = worktreeService.validateWorktreeForResume(existingWorktree.path, branchName);
|
|
1160
|
+
if (!validation.canResume) {
|
|
1161
|
+
console.log(c.error('Detected existing worktree but cannot resume:'));
|
|
1162
|
+
validation.issues.forEach(issue => console.log(c.dim(` ✗ ${issue}`)));
|
|
1163
|
+
if (validation.requiresRecreation) {
|
|
1164
|
+
const branchExists = !validation.issues.includes('Branch does not exist');
|
|
1165
|
+
const dirMissing = validation.issues.includes('Worktree directory does not exist');
|
|
1166
|
+
const dirExists = !dirMissing;
|
|
1167
|
+
// Case 1: Directory missing but branch exists - recreate worktree from existing branch
|
|
1168
|
+
// Case 2: Directory exists but branch missing - recreate with new branch
|
|
1169
|
+
if ((branchExists && dirMissing) || (!branchExists && dirExists)) {
|
|
1170
|
+
const reason = branchExists
|
|
1171
|
+
? 'Branch exists - automatically recreating worktree directory'
|
|
1172
|
+
: 'Directory exists - automatically recreating worktree with new branch';
|
|
1173
|
+
console.log(c.dim(`\n✓ ${reason}`));
|
|
1174
|
+
try {
|
|
1175
|
+
// Remove the old worktree reference if it exists
|
|
1176
|
+
const removeResult = spawnSync('git', ['worktree', 'remove', existingWorktree.path, '--force'], {
|
|
1177
|
+
cwd: workingDir,
|
|
1178
|
+
encoding: 'utf-8',
|
|
1179
|
+
shell: false,
|
|
1180
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
1181
|
+
});
|
|
1182
|
+
// Create the worktree at the same path
|
|
1183
|
+
// If branch exists, checkout that branch; otherwise create a new branch
|
|
1184
|
+
const baseBranch = worktreeService.detectBaseBranch();
|
|
1185
|
+
const worktreeAddArgs = branchExists
|
|
1186
|
+
? ['worktree', 'add', existingWorktree.path, branchName]
|
|
1187
|
+
: ['worktree', 'add', '-b', branchName, existingWorktree.path, baseBranch];
|
|
1188
|
+
const addResult = spawnSync('git', worktreeAddArgs, {
|
|
1189
|
+
cwd: workingDir,
|
|
1190
|
+
encoding: 'utf-8',
|
|
1191
|
+
shell: false,
|
|
1192
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
1193
|
+
});
|
|
1194
|
+
if (addResult.status !== 0) {
|
|
1195
|
+
throw new Error(`Failed to recreate worktree: ${addResult.stderr}`);
|
|
1196
|
+
}
|
|
1197
|
+
// Install dependencies in the recreated worktree
|
|
1198
|
+
worktreeService.installDependencies(existingWorktree.path);
|
|
1199
|
+
console.log(c.success(`✓ Worktree recreated at ${existingWorktree.path}`));
|
|
1200
|
+
getLogger().info('worktree', `Recreated worktree for ${targetStory.frontmatter.id} at ${existingWorktree.path}`);
|
|
1201
|
+
}
|
|
1202
|
+
catch (error) {
|
|
1203
|
+
console.log(c.error(`Failed to recreate worktree: ${error instanceof Error ? error.message : String(error)}`));
|
|
1204
|
+
console.log(c.dim('Please manually remove it with:'));
|
|
1205
|
+
console.log(c.dim(` git worktree remove ${existingWorktree.path}`));
|
|
1206
|
+
return;
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
else {
|
|
1210
|
+
console.log(c.dim('\nWorktree needs manual intervention. Please remove it manually with:'));
|
|
1211
|
+
console.log(c.dim(` git worktree remove ${existingWorktree.path}`));
|
|
1212
|
+
return;
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
else {
|
|
1216
|
+
return;
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
// Automatically resume in the existing worktree
|
|
1220
|
+
originalCwd = process.cwd();
|
|
1221
|
+
worktreePath = existingWorktree.path;
|
|
1222
|
+
process.chdir(worktreePath);
|
|
1223
|
+
sdlcRoot = getSdlcRoot();
|
|
1224
|
+
worktreeCreated = true;
|
|
1225
|
+
// Update story frontmatter with worktree path (sync state)
|
|
1226
|
+
const worktreeStory = findStoryById(sdlcRoot, targetStory.frontmatter.id);
|
|
1227
|
+
if (worktreeStory) {
|
|
1228
|
+
const updatedStory = await updateStoryField(worktreeStory, 'worktree_path', worktreePath);
|
|
1229
|
+
await writeStory(updatedStory);
|
|
1230
|
+
targetStory = updatedStory;
|
|
1231
|
+
}
|
|
1232
|
+
// Get phase information for resume context
|
|
1233
|
+
const lastPhase = getLastCompletedPhase(targetStory);
|
|
1234
|
+
const nextPhase = getNextPhase(targetStory);
|
|
1235
|
+
// Get worktree status for uncommitted changes info
|
|
1236
|
+
const worktreeStatus = worktreeService.getWorktreeStatus(existingWorktree);
|
|
1237
|
+
// Check branch divergence
|
|
1238
|
+
const divergence = worktreeService.checkBranchDivergence(branchName);
|
|
1239
|
+
console.log(c.success(`✓ Resuming in existing worktree: ${worktreePath}`));
|
|
1240
|
+
console.log(c.dim(` Branch: ${branchName}`));
|
|
1241
|
+
console.log(c.dim(` (Worktree path synced to story frontmatter)`));
|
|
1242
|
+
if (lastPhase) {
|
|
1243
|
+
console.log(c.dim(` Last completed phase: ${lastPhase}`));
|
|
1244
|
+
}
|
|
1245
|
+
if (nextPhase) {
|
|
1246
|
+
console.log(c.dim(` Next phase: ${nextPhase}`));
|
|
1247
|
+
}
|
|
1248
|
+
// Display uncommitted changes if present
|
|
1249
|
+
if (worktreeStatus.workingDirectoryStatus !== 'clean') {
|
|
1250
|
+
const totalChanges = worktreeStatus.modifiedFiles.length + worktreeStatus.untrackedFiles.length;
|
|
1251
|
+
console.log(c.dim(` Uncommitted changes: ${totalChanges} file(s)`));
|
|
1252
|
+
if (worktreeStatus.modifiedFiles.length > 0) {
|
|
1253
|
+
console.log(c.dim(` Modified: ${worktreeStatus.modifiedFiles.slice(0, 3).join(', ')}${worktreeStatus.modifiedFiles.length > 3 ? '...' : ''}`));
|
|
1254
|
+
}
|
|
1255
|
+
if (worktreeStatus.untrackedFiles.length > 0) {
|
|
1256
|
+
console.log(c.dim(` Untracked: ${worktreeStatus.untrackedFiles.slice(0, 3).join(', ')}${worktreeStatus.untrackedFiles.length > 3 ? '...' : ''}`));
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
// Warn if branch has diverged significantly
|
|
1260
|
+
if (divergence.diverged && (divergence.ahead > DIVERGENCE_WARNING_THRESHOLD || divergence.behind > DIVERGENCE_WARNING_THRESHOLD)) {
|
|
1261
|
+
console.log(c.warning(` ⚠ Branch has diverged from base: ${divergence.ahead} ahead, ${divergence.behind} behind`));
|
|
1262
|
+
console.log(c.dim(` Consider rebasing to sync with latest changes`));
|
|
1263
|
+
}
|
|
1264
|
+
console.log();
|
|
953
1265
|
}
|
|
954
|
-
console.log(c.success(`✓ Created worktree at: ${worktreePath}`));
|
|
955
|
-
console.log(c.dim(` Branch: ai-sdlc/${targetStory.frontmatter.id}-${targetStory.slug}`));
|
|
956
|
-
console.log();
|
|
957
1266
|
}
|
|
958
|
-
|
|
959
|
-
//
|
|
960
|
-
|
|
961
|
-
|
|
1267
|
+
if (shouldCreateNewWorktree) {
|
|
1268
|
+
// Validate git state for worktree creation
|
|
1269
|
+
const validation = worktreeService.validateCanCreateWorktree();
|
|
1270
|
+
if (!validation.valid) {
|
|
1271
|
+
console.log(c.error(`Error: ${validation.error}`));
|
|
1272
|
+
return;
|
|
1273
|
+
}
|
|
1274
|
+
try {
|
|
1275
|
+
// Detect base branch
|
|
1276
|
+
const baseBranch = worktreeService.detectBaseBranch();
|
|
1277
|
+
// Create worktree
|
|
1278
|
+
originalCwd = process.cwd();
|
|
1279
|
+
worktreePath = worktreeService.create({
|
|
1280
|
+
storyId: targetStory.frontmatter.id,
|
|
1281
|
+
slug: targetStory.slug,
|
|
1282
|
+
baseBranch,
|
|
1283
|
+
});
|
|
1284
|
+
// Change to worktree directory BEFORE updating story
|
|
1285
|
+
// This ensures story updates happen in the worktree, not on main
|
|
1286
|
+
// (allows parallel story launches from clean main)
|
|
1287
|
+
process.chdir(worktreePath);
|
|
1288
|
+
// Recalculate sdlcRoot for the worktree context
|
|
1289
|
+
sdlcRoot = getSdlcRoot();
|
|
1290
|
+
worktreeCreated = true;
|
|
1291
|
+
// Now update story frontmatter with worktree path (writes to worktree copy)
|
|
1292
|
+
// Re-resolve target story in worktree context
|
|
1293
|
+
const worktreeStory = findStoryById(sdlcRoot, targetStory.frontmatter.id);
|
|
1294
|
+
if (worktreeStory) {
|
|
1295
|
+
const updatedStory = await updateStoryField(worktreeStory, 'worktree_path', worktreePath);
|
|
1296
|
+
await writeStory(updatedStory);
|
|
1297
|
+
// Update targetStory reference for downstream use
|
|
1298
|
+
targetStory = updatedStory;
|
|
1299
|
+
}
|
|
1300
|
+
console.log(c.success(`✓ Created worktree at: ${worktreePath}`));
|
|
1301
|
+
console.log(c.dim(` Branch: ai-sdlc/${targetStory.frontmatter.id}-${targetStory.slug}`));
|
|
1302
|
+
console.log();
|
|
1303
|
+
}
|
|
1304
|
+
catch (error) {
|
|
1305
|
+
// Restore directory on worktree creation failure
|
|
1306
|
+
if (originalCwd) {
|
|
1307
|
+
process.chdir(originalCwd);
|
|
1308
|
+
}
|
|
1309
|
+
console.log(c.error(`Failed to create worktree: ${error instanceof Error ? error.message : String(error)}`));
|
|
1310
|
+
return;
|
|
962
1311
|
}
|
|
963
|
-
console.log(c.error(`Failed to create worktree: ${error instanceof Error ? error.message : String(error)}`));
|
|
964
|
-
return;
|
|
965
1312
|
}
|
|
966
1313
|
}
|
|
967
1314
|
}
|