edsger 0.19.14 → 0.19.16

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.
@@ -9,7 +9,7 @@ import { execSync } from 'child_process';
9
9
  import { fetchCodeRefineContext, } from './context.js';
10
10
  import { getFeedbacksForPhase, formatFeedbacksForContext, } from '../../services/feedbacks.js';
11
11
  import { createSystemPrompt, createCodeRefinePrompt } from './prompts.js';
12
- import { preparePhaseGitEnvironment, prepareCustomBranchGitEnvironment, hasUncommittedChanges, getUncommittedFiles, } from '../../utils/git-branch-manager.js';
12
+ import { preparePhaseGitEnvironmentAsync, prepareCustomBranchGitEnvironmentAsync, hasUncommittedChanges, getUncommittedFiles, } from '../../utils/git-branch-manager.js';
13
13
  import { getFeature } from '../../api/features/get-feature.js';
14
14
  import { getBranches, getBaseBranchInfo, updateBranch, } from '../../services/branches.js';
15
15
  import { verifyAndResolveComments, } from '../code-refine-verification/index.js';
@@ -76,6 +76,7 @@ export const refineCodeFromPRFeedback = async (options, config) => {
76
76
  let currentBranch = null;
77
77
  let allBranches = [];
78
78
  let baseBranchForRebase = 'main'; // Default base branch for rebase
79
+ let originalBaseBranchForRebase; // For --onto when base was squash-merged
79
80
  try {
80
81
  // Get all branches to determine base branch info
81
82
  allBranches = await getBranches({ featureId, verbose });
@@ -94,12 +95,17 @@ export const refineCodeFromPRFeedback = async (options, config) => {
94
95
  // - Base branch merged: returns feat branch (dev/xxx -> feat/xxx)
95
96
  // - Base branch not merged: returns dev branch
96
97
  baseBranchForRebase = baseBranchInfo.baseBranch;
98
+ // When base branch is merged (squash merge), we need originalBaseBranch for --onto
99
+ originalBaseBranchForRebase = baseBranchInfo.originalBaseBranch;
97
100
  if (verbose) {
98
101
  if (!currentBranch.base_branch_id) {
99
102
  // No base branch - first branch in chain
100
103
  }
101
104
  else if (baseBranchInfo.baseBranchMerged) {
102
105
  logInfo(` Base branch is merged, will rebase from feat branch: ${baseBranchForRebase}`);
106
+ if (originalBaseBranchForRebase) {
107
+ logInfo(` Using --onto from original base: ${originalBaseBranchForRebase}`);
108
+ }
103
109
  }
104
110
  else {
105
111
  logInfo(` Will rebase from base branch: ${baseBranchForRebase}`);
@@ -136,9 +142,24 @@ export const refineCodeFromPRFeedback = async (options, config) => {
136
142
  }
137
143
  }
138
144
  // Prepare git environment: switch to the appropriate branch and rebase from correct base
139
- const cleanupGit = currentBranch
140
- ? prepareCustomBranchGitEnvironment(branchName, baseBranchForRebase, verbose)
141
- : preparePhaseGitEnvironment(featureId, 'main', verbose);
145
+ // Use async version with automatic conflict resolution
146
+ const gitEnvResult = currentBranch
147
+ ? await prepareCustomBranchGitEnvironmentAsync({
148
+ featureBranch: branchName,
149
+ baseBranch: baseBranchForRebase,
150
+ originalBaseBranch: originalBaseBranchForRebase,
151
+ verbose,
152
+ resolveConflicts: true,
153
+ conflictResolverConfig: {
154
+ model: config.claude.model || 'sonnet',
155
+ },
156
+ })
157
+ : await preparePhaseGitEnvironmentAsync(featureId, 'main', verbose, true, { model: config.claude.model || 'sonnet' });
158
+ const cleanupGit = gitEnvResult.cleanup;
159
+ // Log if conflicts were automatically resolved
160
+ if (gitEnvResult.conflictsResolved && verbose) {
161
+ logInfo(`šŸ”§ Automatically resolved ${gitEnvResult.resolvedFiles?.length || 0} conflict(s) during rebase`);
162
+ }
142
163
  try {
143
164
  // Fetch initial code refine context (PR reviews and comments)
144
165
  if (verbose) {
@@ -7,7 +7,7 @@ import { logInfo, logError } from '../../utils/logger.js';
7
7
  import { Octokit } from '@octokit/rest';
8
8
  import { fetchCodeReviewContext, formatContextForPrompt, } from './context.js';
9
9
  import { getFeedbacksForPhase, formatFeedbacksForContext, } from '../../services/feedbacks.js';
10
- import { preparePhaseGitEnvironment, prepareCustomBranchGitEnvironment, } from '../../utils/git-branch-manager.js';
10
+ import { preparePhaseGitEnvironmentAsync, prepareCustomBranchGitEnvironmentAsync, } from '../../utils/git-branch-manager.js';
11
11
  import { getBranches, getBaseBranchInfo, updateBranch, } from '../../services/branches.js';
12
12
  import { getFeature } from '../../api/features/get-feature.js';
13
13
  function userMessage(content) {
@@ -128,6 +128,7 @@ export const reviewPullRequest = async (options, config) => {
128
128
  let currentBranch = null;
129
129
  let allBranches = [];
130
130
  let baseBranchForRebase = 'main'; // Default base branch for rebase
131
+ let originalBaseBranchForRebase; // For --onto when base was squash-merged
131
132
  try {
132
133
  // Get all branches to determine base branch info
133
134
  allBranches = await getBranches({ featureId, verbose });
@@ -147,12 +148,17 @@ export const reviewPullRequest = async (options, config) => {
147
148
  // - Base branch merged: returns feat branch (dev/xxx -> feat/xxx)
148
149
  // - Base branch not merged: returns dev branch
149
150
  baseBranchForRebase = baseBranchInfo.baseBranch;
151
+ // When base branch is merged (squash merge), we need originalBaseBranch for --onto
152
+ originalBaseBranchForRebase = baseBranchInfo.originalBaseBranch;
150
153
  if (verbose) {
151
154
  if (!currentBranch.base_branch_id) {
152
155
  // No base branch - first branch in chain
153
156
  }
154
157
  else if (baseBranchInfo.baseBranchMerged) {
155
158
  logInfo(` Base branch is merged, will rebase from feat branch: ${baseBranchForRebase}`);
159
+ if (originalBaseBranchForRebase) {
160
+ logInfo(` Using --onto from original base: ${originalBaseBranchForRebase}`);
161
+ }
156
162
  }
157
163
  else {
158
164
  logInfo(` Will rebase from base branch: ${baseBranchForRebase}`);
@@ -189,9 +195,24 @@ export const reviewPullRequest = async (options, config) => {
189
195
  }
190
196
  }
191
197
  // Prepare git environment: switch to the appropriate branch and rebase from correct base
192
- const cleanupGit = currentBranch
193
- ? prepareCustomBranchGitEnvironment(branchName, baseBranchForRebase, verbose)
194
- : preparePhaseGitEnvironment(featureId, 'main', verbose);
198
+ // Use async version with automatic conflict resolution
199
+ const gitEnvResult = currentBranch
200
+ ? await prepareCustomBranchGitEnvironmentAsync({
201
+ featureBranch: branchName,
202
+ baseBranch: baseBranchForRebase,
203
+ originalBaseBranch: originalBaseBranchForRebase,
204
+ verbose,
205
+ resolveConflicts: true,
206
+ conflictResolverConfig: {
207
+ model: config.claude.model || 'sonnet',
208
+ },
209
+ })
210
+ : await preparePhaseGitEnvironmentAsync(featureId, 'main', verbose, true, { model: config.claude.model || 'sonnet' });
211
+ const cleanupGit = gitEnvResult.cleanup;
212
+ // Log if conflicts were automatically resolved
213
+ if (gitEnvResult.conflictsResolved && verbose) {
214
+ logInfo(`šŸ”§ Automatically resolved ${gitEnvResult.resolvedFiles?.length || 0} conflict(s) during rebase`);
215
+ }
195
216
  try {
196
217
  // Fetch code review context (PR data, files, commits)
197
218
  if (verbose) {
@@ -12,7 +12,7 @@ export interface Branch {
12
12
  base_branch_id: string | null;
13
13
  pull_request_url: string | null;
14
14
  pull_request_number: number | null;
15
- status: 'pending' | 'in_progress' | 'ready_for_review' | 'reviewed' | 'refined' | 'merged' | 'closed';
15
+ status: 'pending' | 'in_progress' | 'ready_for_review' | 'reviewed' | 'refined' | 'merged' | 'completed' | 'closed';
16
16
  created_at: string;
17
17
  updated_at: string;
18
18
  }
@@ -56,19 +56,28 @@ export declare function hasMultipleBranches(options: PipelinePhaseOptions): Prom
56
56
  */
57
57
  export declare function getNextPendingBranch(options: PipelinePhaseOptions): Promise<Branch | null>;
58
58
  /**
59
- * Check if all branches are completed
59
+ * Check if all branches are completed (feat merged to main)
60
60
  */
61
61
  export declare function allBranchesCompleted(options: PipelinePhaseOptions): Promise<boolean>;
62
62
  /**
63
63
  * Get the base branch information for a branch.
64
64
  * Returns:
65
65
  * - If base_branch_id is null: use main (first branch in chain)
66
+ * - If base_branch_id is set and that branch is completed: use main
67
+ * (completed means feat merged to main)
66
68
  * - If base_branch_id is set and that branch is merged: use base branch's feat branch
67
69
  * (merged means dev merged to feat, not to main)
68
70
  * - If base_branch_id is set and that branch is not merged: use base branch's dev branch
71
+ *
72
+ * When base branch is merged/completed (squash merge), we need to use `git rebase --onto` to
73
+ * correctly rebase only the new commits. This function returns both the new base
74
+ * and the original base for this purpose:
75
+ * - completed: baseBranch=main, originalBaseBranch=feat/xxx
76
+ * - merged: baseBranch=feat/xxx, originalBaseBranch=dev/xxx
69
77
  */
70
78
  export declare function getBaseBranchInfo(branch: Branch, allBranches: Branch[], mainBranch?: string): Promise<{
71
79
  baseBranch: string;
80
+ originalBaseBranch?: string;
72
81
  needsRebase: boolean;
73
82
  baseBranchMerged: boolean;
74
83
  }>;
@@ -97,6 +97,8 @@ export function formatBranchesForContext(branches) {
97
97
  }
98
98
  const getStatusEmoji = (status) => {
99
99
  switch (status) {
100
+ case 'completed':
101
+ return 'šŸ';
100
102
  case 'merged':
101
103
  return 'āœ…';
102
104
  case 'refined':
@@ -148,21 +150,29 @@ export async function getNextPendingBranch(options) {
148
150
  return branches.find((b) => b.status === 'pending') || null;
149
151
  }
150
152
  /**
151
- * Check if all branches are completed
153
+ * Check if all branches are completed (feat merged to main)
152
154
  */
153
155
  export async function allBranchesCompleted(options) {
154
156
  const branches = await getBranches(options);
155
157
  if (branches.length === 0)
156
158
  return true;
157
- return branches.every((b) => b.status === 'merged' || b.status === 'closed');
159
+ return branches.every((b) => b.status === 'completed' || b.status === 'merged' || b.status === 'closed');
158
160
  }
159
161
  /**
160
162
  * Get the base branch information for a branch.
161
163
  * Returns:
162
164
  * - If base_branch_id is null: use main (first branch in chain)
165
+ * - If base_branch_id is set and that branch is completed: use main
166
+ * (completed means feat merged to main)
163
167
  * - If base_branch_id is set and that branch is merged: use base branch's feat branch
164
168
  * (merged means dev merged to feat, not to main)
165
169
  * - If base_branch_id is set and that branch is not merged: use base branch's dev branch
170
+ *
171
+ * When base branch is merged/completed (squash merge), we need to use `git rebase --onto` to
172
+ * correctly rebase only the new commits. This function returns both the new base
173
+ * and the original base for this purpose:
174
+ * - completed: baseBranch=main, originalBaseBranch=feat/xxx
175
+ * - merged: baseBranch=feat/xxx, originalBaseBranch=dev/xxx
166
176
  */
167
177
  export async function getBaseBranchInfo(branch, allBranches, mainBranch = 'main') {
168
178
  // No base branch - start from main (first branch in chain)
@@ -183,6 +193,25 @@ export async function getBaseBranchInfo(branch, allBranches, mainBranch = 'main'
183
193
  baseBranchMerged: true,
184
194
  };
185
195
  }
196
+ // Check if base branch is completed (feat merged to main)
197
+ if (baseBranch.status === 'completed') {
198
+ // Base branch's feat is merged to main - rebase directly from main
199
+ // Use feat branch as originalBaseBranch for --onto
200
+ if (!baseBranch.branch_name) {
201
+ return {
202
+ baseBranch: mainBranch,
203
+ needsRebase: false,
204
+ baseBranchMerged: true,
205
+ };
206
+ }
207
+ const featBranchName = baseBranch.branch_name.replace(/^dev\//, 'feat/');
208
+ return {
209
+ baseBranch: mainBranch,
210
+ originalBaseBranch: featBranchName, // Use feat branch for --onto
211
+ needsRebase: true,
212
+ baseBranchMerged: true,
213
+ };
214
+ }
186
215
  // Check if base branch is merged (dev merged to feat)
187
216
  if (baseBranch.status === 'merged') {
188
217
  // Base branch's dev is merged to feat - rebase from base branch's feat branch
@@ -197,6 +226,7 @@ export async function getBaseBranchInfo(branch, allBranches, mainBranch = 'main'
197
226
  const featBranchName = baseBranch.branch_name.replace(/^dev\//, 'feat/');
198
227
  return {
199
228
  baseBranch: featBranchName,
229
+ originalBaseBranch: baseBranch.branch_name, // Return original dev branch for --onto
200
230
  needsRebase: true,
201
231
  baseBranchMerged: true,
202
232
  };
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Conflict Resolver
3
+ * Uses Claude agent to complete the entire rebase process including conflict resolution
4
+ */
5
+ /**
6
+ * Configuration for conflict resolution
7
+ */
8
+ export interface ConflictResolverConfig {
9
+ model?: string;
10
+ verbose?: boolean;
11
+ maxTurns?: number;
12
+ featureBranch?: string;
13
+ baseBranch?: string;
14
+ /**
15
+ * Original base branch before squash merge.
16
+ * When provided, this indicates the rebase was started with --onto
17
+ * to only replay commits after this branch.
18
+ */
19
+ originalBaseBranch?: string;
20
+ }
21
+ /**
22
+ * Result of conflict resolution
23
+ */
24
+ export interface ConflictResolutionResult {
25
+ success: boolean;
26
+ resolvedFiles: string[];
27
+ failedFiles: string[];
28
+ error?: string;
29
+ }
30
+ /**
31
+ * Resolve all conflicts by letting Claude complete the entire rebase process
32
+ *
33
+ * This function gives Claude full control to:
34
+ * 1. Examine the conflict state
35
+ * 2. Read and understand conflicting files
36
+ * 3. Edit files to resolve conflicts
37
+ * 4. Stage resolved files and continue rebase
38
+ * 5. Repeat until rebase is complete
39
+ *
40
+ * @param config - Configuration for conflict resolution
41
+ * @returns Result of the conflict resolution
42
+ */
43
+ export declare function resolveConflictsWithAgent(config?: ConflictResolverConfig): Promise<ConflictResolutionResult>;
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Conflict Resolver
3
+ * Uses Claude agent to complete the entire rebase process including conflict resolution
4
+ */
5
+ import { query } from '@anthropic-ai/claude-agent-sdk';
6
+ import { logInfo, logError } from './logger.js';
7
+ import { getConflictFiles, isRebaseInProgress, abortRebase, getCurrentBranch, } from './git-branch-manager.js';
8
+ /**
9
+ * Create a prompt generator for the Claude agent
10
+ */
11
+ function* createPromptGenerator(prompt) {
12
+ yield {
13
+ type: 'user',
14
+ message: { role: 'user', content: prompt },
15
+ };
16
+ }
17
+ /**
18
+ * Create a system prompt for conflict resolution
19
+ */
20
+ function createConflictResolutionSystemPrompt() {
21
+ return `You are a skilled software engineer completing a git rebase operation that has conflicts.
22
+
23
+ Your task is to resolve ALL merge conflicts and complete the rebase successfully.
24
+
25
+ WORKFLOW:
26
+ 1. First, run "git status" to see the current state and identify all conflicting files
27
+ 2. For each conflicting file:
28
+ a. Read the file to see the conflict markers
29
+ b. Understand the intent of both sides (ours vs theirs)
30
+ c. Use the Edit tool to resolve the conflict - merge changes intelligently
31
+ d. Run "git add <file>" to stage the resolved file
32
+ 3. After ALL conflicts are resolved, run "git rebase --continue"
33
+ 4. If more conflicts appear from subsequent commits, repeat steps 1-3
34
+ 5. Continue until the rebase is complete (no more rebase in progress)
35
+
36
+ CONFLICT RESOLUTION RULES:
37
+ - Remove ALL conflict markers (<<<<<<, =======, >>>>>>>)
38
+ - Merge changes intelligently - don't just pick one side
39
+ - Preserve functionality from both sides when possible
40
+ - If changes are truly incompatible, prefer theirs (incoming) as it's usually newer
41
+ - Ensure the resulting code is syntactically correct
42
+ - DO NOT add comments explaining the merge
43
+
44
+ IMPORTANT:
45
+ - Use "git status" to check progress
46
+ - Use the Read tool to examine conflicting files
47
+ - Use the Edit tool to fix conflicts (remove markers, merge content)
48
+ - Use Bash for git commands (git add, git rebase --continue)
49
+ - Keep working until "git status" shows no rebase in progress
50
+ - If you encounter an error you cannot resolve, explain what went wrong`;
51
+ }
52
+ /**
53
+ * Create the initial prompt describing the rebase task
54
+ */
55
+ function createRebaseTaskPrompt(featureBranch, baseBranch, conflictFiles, originalBaseBranch) {
56
+ let context = `A git rebase operation is in progress and has encountered conflicts.
57
+
58
+ ## Current Situation
59
+ - Feature branch: ${featureBranch}
60
+ - Rebasing onto: ${baseBranch}`;
61
+ if (originalBaseBranch) {
62
+ context += `
63
+ - Original base branch: ${originalBaseBranch}
64
+ - Note: This is a rebase after squash-merge. The base branch (${originalBaseBranch}) was squash-merged
65
+ into ${baseBranch}. We used "git rebase --onto" to only replay commits that are new relative to
66
+ ${originalBaseBranch}. The "theirs" side in conflicts comes from ${baseBranch} (the squashed code).`;
67
+ }
68
+ context += `
69
+ - Conflicting files: ${conflictFiles.length > 0 ? conflictFiles.join(', ') : '(run git status to see)'}
70
+
71
+ ## Your Task
72
+ Complete the rebase by resolving all conflicts. Use git status, read the conflicting files, edit them to resolve conflicts, stage with git add, and continue the rebase.
73
+
74
+ Start by running "git status" to see the current state.`;
75
+ return context;
76
+ }
77
+ /**
78
+ * Resolve all conflicts by letting Claude complete the entire rebase process
79
+ *
80
+ * This function gives Claude full control to:
81
+ * 1. Examine the conflict state
82
+ * 2. Read and understand conflicting files
83
+ * 3. Edit files to resolve conflicts
84
+ * 4. Stage resolved files and continue rebase
85
+ * 5. Repeat until rebase is complete
86
+ *
87
+ * @param config - Configuration for conflict resolution
88
+ * @returns Result of the conflict resolution
89
+ */
90
+ export async function resolveConflictsWithAgent(config = {}) {
91
+ const { model = 'sonnet', verbose, maxTurns = 100, featureBranch, baseBranch, originalBaseBranch, } = config;
92
+ const resolvedFiles = [];
93
+ if (verbose) {
94
+ logInfo(`\nšŸ”§ Starting automatic conflict resolution with Claude agent...`);
95
+ }
96
+ // Get initial conflict information
97
+ const currentBranch = featureBranch || getCurrentBranch();
98
+ const targetBranch = baseBranch || 'main';
99
+ const initialConflictFiles = getConflictFiles();
100
+ if (verbose) {
101
+ logInfo(` Feature branch: ${currentBranch}`);
102
+ logInfo(` Rebasing onto: ${targetBranch}`);
103
+ if (originalBaseBranch) {
104
+ logInfo(` Original base (--onto): ${originalBaseBranch}`);
105
+ }
106
+ logInfo(` Initial conflicts: ${initialConflictFiles.length} file(s)`);
107
+ }
108
+ const systemPrompt = createConflictResolutionSystemPrompt();
109
+ const userPrompt = createRebaseTaskPrompt(currentBranch, targetBranch, initialConflictFiles, originalBaseBranch);
110
+ try {
111
+ let lastMessage = '';
112
+ for await (const message of query({
113
+ prompt: createPromptGenerator(userPrompt),
114
+ options: {
115
+ systemPrompt: {
116
+ type: 'preset',
117
+ preset: 'claude_code',
118
+ append: systemPrompt,
119
+ },
120
+ model: model,
121
+ maxTurns: maxTurns,
122
+ permissionMode: 'bypassPermissions',
123
+ },
124
+ })) {
125
+ if (verbose && message.type === 'assistant' && message.message?.content) {
126
+ // Log progress
127
+ const content = message.message.content;
128
+ if (typeof content === 'string') {
129
+ lastMessage = content;
130
+ }
131
+ else if (Array.isArray(content)) {
132
+ for (const block of content) {
133
+ if (block.type === 'text') {
134
+ lastMessage = block.text;
135
+ }
136
+ }
137
+ }
138
+ }
139
+ }
140
+ // Check if rebase completed successfully
141
+ if (!isRebaseInProgress()) {
142
+ if (verbose) {
143
+ logInfo(`āœ… Rebase completed successfully`);
144
+ }
145
+ // Get the list of files that were resolved (approximate from initial conflicts)
146
+ return {
147
+ success: true,
148
+ resolvedFiles: initialConflictFiles,
149
+ failedFiles: [],
150
+ };
151
+ }
152
+ else {
153
+ // Rebase still in progress - something went wrong
154
+ const remainingConflicts = getConflictFiles();
155
+ if (verbose) {
156
+ logError(`āŒ Rebase still in progress with ${remainingConflicts.length} conflicts`);
157
+ if (lastMessage) {
158
+ logError(` Last message: ${lastMessage.substring(0, 200)}...`);
159
+ }
160
+ }
161
+ abortRebase(verbose);
162
+ return {
163
+ success: false,
164
+ resolvedFiles: [],
165
+ failedFiles: remainingConflicts,
166
+ error: `Rebase not completed. Remaining conflicts: ${remainingConflicts.join(', ')}`,
167
+ };
168
+ }
169
+ }
170
+ catch (error) {
171
+ if (verbose) {
172
+ logError(`āŒ Error during conflict resolution: ${error instanceof Error ? error.message : String(error)}`);
173
+ }
174
+ // Try to abort rebase to leave repo in clean state
175
+ if (isRebaseInProgress()) {
176
+ abortRebase(verbose);
177
+ }
178
+ return {
179
+ success: false,
180
+ resolvedFiles: [],
181
+ failedFiles: getConflictFiles(),
182
+ error: error instanceof Error ? error.message : String(error),
183
+ };
184
+ }
185
+ }
@@ -2,6 +2,20 @@
2
2
  * Git Branch Manager
3
3
  * Shared utilities for consistent git branch management across all phases
4
4
  */
5
+ /**
6
+ * Conflict resolver function type
7
+ * Takes conflict file info and returns resolved content
8
+ */
9
+ export type ConflictResolver = (conflictFiles: ConflictFileInfo[]) => Promise<Map<string, string>>;
10
+ /**
11
+ * Information about a file with merge conflicts
12
+ */
13
+ export interface ConflictFileInfo {
14
+ filePath: string;
15
+ content: string;
16
+ oursContent: string;
17
+ theirsContent: string;
18
+ }
5
19
  /**
6
20
  * Get current Git branch name
7
21
  */
@@ -18,6 +32,34 @@ export declare function getUncommittedFiles(): string[];
18
32
  * Reset uncommitted changes in the working directory
19
33
  */
20
34
  export declare function resetUncommittedChanges(verbose?: boolean): void;
35
+ /**
36
+ * Check if rebase is in progress with conflicts
37
+ */
38
+ export declare function isRebaseInProgress(): boolean;
39
+ /**
40
+ * Get list of files with merge conflicts
41
+ */
42
+ export declare function getConflictFiles(): string[];
43
+ /**
44
+ * Check if there are any merge conflicts
45
+ */
46
+ export declare function hasConflicts(): boolean;
47
+ /**
48
+ * Get detailed information about conflict files including their content
49
+ */
50
+ export declare function getConflictFileInfos(): ConflictFileInfo[];
51
+ /**
52
+ * Write resolved content for a conflict file and stage it
53
+ */
54
+ export declare function resolveConflictFile(filePath: string, resolvedContent: string, verbose?: boolean): void;
55
+ /**
56
+ * Continue rebase after conflicts are resolved
57
+ */
58
+ export declare function continueRebase(verbose?: boolean): void;
59
+ /**
60
+ * Abort the current rebase
61
+ */
62
+ export declare function abortRebase(verbose?: boolean): void;
21
63
  /**
22
64
  * Check if a Git branch exists locally
23
65
  */
@@ -102,3 +144,78 @@ export declare function prepareCustomBranchGitEnvironment(featureBranch: string,
102
144
  * @param verbose - Whether to log verbose output
103
145
  */
104
146
  export declare function syncFeatBranchWithMain(featureId: string, githubToken: string, owner: string, repo: string, baseBranch?: string, verbose?: boolean): Promise<boolean>;
147
+ /**
148
+ * Options for async rebase with conflict resolution
149
+ */
150
+ export interface RebaseWithConflictResolutionOptions {
151
+ featureBranch: string;
152
+ baseBranch?: string;
153
+ /**
154
+ * Original base branch before squash merge.
155
+ * When set, uses `git rebase --onto baseBranch originalBaseBranch` to only
156
+ * rebase commits that are new relative to originalBaseBranch.
157
+ * This is required when the base branch was squash-merged, otherwise
158
+ * git will try to reapply all the original commits causing many conflicts.
159
+ */
160
+ originalBaseBranch?: string;
161
+ verbose?: boolean;
162
+ resolveConflicts?: boolean;
163
+ conflictResolverConfig?: {
164
+ model?: string;
165
+ maxTurns?: number;
166
+ };
167
+ }
168
+ /**
169
+ * Switch to feature branch and rebase with base branch, with optional automatic conflict resolution
170
+ * This is the async version that supports conflict resolution using Claude agent
171
+ *
172
+ * @param options - Options for the rebase operation
173
+ * @returns Object containing the previous branch name and conflict resolution result
174
+ */
175
+ export declare function switchToFeatureBranchAndRebaseAsync(options: RebaseWithConflictResolutionOptions): Promise<{
176
+ previousBranch: string;
177
+ conflictsResolved?: boolean;
178
+ resolvedFiles?: string[];
179
+ }>;
180
+ /**
181
+ * Async version of prepareCustomBranchGitEnvironment with conflict resolution support.
182
+ *
183
+ * Usage:
184
+ * const cleanup = await prepareCustomBranchGitEnvironmentAsync({
185
+ * featureBranch: 'feat/123/2-api',
186
+ * baseBranch: 'feat/123/1-database',
187
+ * verbose: true,
188
+ * resolveConflicts: true,
189
+ * })
190
+ * try {
191
+ * // ... phase logic ...
192
+ * } finally {
193
+ * cleanup()
194
+ * }
195
+ *
196
+ * @param options - Options for the git environment preparation
197
+ * @returns Cleanup function that will return to main branch
198
+ */
199
+ export declare function prepareCustomBranchGitEnvironmentAsync(options: RebaseWithConflictResolutionOptions): Promise<{
200
+ cleanup: () => void;
201
+ conflictsResolved?: boolean;
202
+ resolvedFiles?: string[];
203
+ }>;
204
+ /**
205
+ * Async version of preparePhaseGitEnvironment with conflict resolution support.
206
+ *
207
+ * @param featureId - The feature ID (will be used to construct branch name "dev/{featureId}")
208
+ * @param baseBranch - The base branch to rebase from (default: "main")
209
+ * @param verbose - Whether to log verbose output
210
+ * @param resolveConflicts - Whether to automatically resolve conflicts using Claude
211
+ * @param conflictResolverConfig - Configuration for the conflict resolver
212
+ * @returns Cleanup function and conflict resolution info
213
+ */
214
+ export declare function preparePhaseGitEnvironmentAsync(featureId: string, baseBranch?: string, verbose?: boolean, resolveConflicts?: boolean, conflictResolverConfig?: {
215
+ model?: string;
216
+ maxTurns?: number;
217
+ }): Promise<{
218
+ cleanup: () => void;
219
+ conflictsResolved?: boolean;
220
+ resolvedFiles?: string[];
221
+ }>;
@@ -3,6 +3,8 @@
3
3
  * Shared utilities for consistent git branch management across all phases
4
4
  */
5
5
  import { execSync } from 'child_process';
6
+ import * as fs from 'fs';
7
+ import * as path from 'path';
6
8
  import { Octokit } from '@octokit/rest';
7
9
  import { logInfo, logError } from './logger.js';
8
10
  /**
@@ -62,6 +64,148 @@ export function resetUncommittedChanges(verbose) {
62
64
  throw new Error(`Failed to reset uncommitted changes: ${error instanceof Error ? error.message : String(error)}`);
63
65
  }
64
66
  }
67
+ /**
68
+ * Check if rebase is in progress with conflicts
69
+ */
70
+ export function isRebaseInProgress() {
71
+ try {
72
+ // Check for .git/rebase-merge or .git/rebase-apply directory
73
+ const gitDir = execSync('git rev-parse --git-dir', {
74
+ encoding: 'utf-8',
75
+ }).trim();
76
+ const rebaseMerge = path.join(gitDir, 'rebase-merge');
77
+ const rebaseApply = path.join(gitDir, 'rebase-apply');
78
+ return fs.existsSync(rebaseMerge) || fs.existsSync(rebaseApply);
79
+ }
80
+ catch (error) {
81
+ return false;
82
+ }
83
+ }
84
+ /**
85
+ * Get list of files with merge conflicts
86
+ */
87
+ export function getConflictFiles() {
88
+ try {
89
+ const status = execSync('git diff --name-only --diff-filter=U', {
90
+ encoding: 'utf-8',
91
+ });
92
+ return status
93
+ .trim()
94
+ .split('\n')
95
+ .filter((line) => line.length > 0);
96
+ }
97
+ catch (error) {
98
+ return [];
99
+ }
100
+ }
101
+ /**
102
+ * Check if there are any merge conflicts
103
+ */
104
+ export function hasConflicts() {
105
+ return getConflictFiles().length > 0;
106
+ }
107
+ /**
108
+ * Get detailed information about conflict files including their content
109
+ */
110
+ export function getConflictFileInfos() {
111
+ const conflictFiles = getConflictFiles();
112
+ const infos = [];
113
+ for (const filePath of conflictFiles) {
114
+ try {
115
+ // Read the file with conflict markers
116
+ const content = fs.readFileSync(filePath, 'utf-8');
117
+ // Get our version (current branch)
118
+ let oursContent = '';
119
+ try {
120
+ oursContent = execSync(`git show :2:${filePath}`, {
121
+ encoding: 'utf-8',
122
+ });
123
+ }
124
+ catch (e) {
125
+ // File might be new on our side
126
+ oursContent = '';
127
+ }
128
+ // Get their version (incoming branch)
129
+ let theirsContent = '';
130
+ try {
131
+ theirsContent = execSync(`git show :3:${filePath}`, {
132
+ encoding: 'utf-8',
133
+ });
134
+ }
135
+ catch (e) {
136
+ // File might be new on their side
137
+ theirsContent = '';
138
+ }
139
+ infos.push({
140
+ filePath,
141
+ content,
142
+ oursContent,
143
+ theirsContent,
144
+ });
145
+ }
146
+ catch (error) {
147
+ // Skip files we can't read
148
+ continue;
149
+ }
150
+ }
151
+ return infos;
152
+ }
153
+ /**
154
+ * Write resolved content for a conflict file and stage it
155
+ */
156
+ export function resolveConflictFile(filePath, resolvedContent, verbose) {
157
+ try {
158
+ if (verbose) {
159
+ logInfo(`šŸ“ Writing resolved content for ${filePath}...`);
160
+ }
161
+ fs.writeFileSync(filePath, resolvedContent, 'utf-8');
162
+ execSync(`git add "${filePath}"`, { encoding: 'utf-8', stdio: 'pipe' });
163
+ if (verbose) {
164
+ logInfo(`āœ… Resolved and staged ${filePath}`);
165
+ }
166
+ }
167
+ catch (error) {
168
+ throw new Error(`Failed to resolve conflict in ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
169
+ }
170
+ }
171
+ /**
172
+ * Continue rebase after conflicts are resolved
173
+ */
174
+ export function continueRebase(verbose) {
175
+ try {
176
+ if (verbose) {
177
+ logInfo(`šŸ”„ Continuing rebase...`);
178
+ }
179
+ // Use GIT_EDITOR=true to skip the commit message editor
180
+ execSync('GIT_EDITOR=true git rebase --continue', {
181
+ encoding: 'utf-8',
182
+ stdio: verbose ? 'inherit' : 'pipe',
183
+ });
184
+ if (verbose) {
185
+ logInfo(`āœ… Rebase continued successfully`);
186
+ }
187
+ }
188
+ catch (error) {
189
+ throw new Error(`Failed to continue rebase: ${error instanceof Error ? error.message : String(error)}`);
190
+ }
191
+ }
192
+ /**
193
+ * Abort the current rebase
194
+ */
195
+ export function abortRebase(verbose) {
196
+ try {
197
+ if (verbose) {
198
+ logInfo(`šŸ”„ Aborting rebase...`);
199
+ }
200
+ execSync('git rebase --abort', { encoding: 'utf-8', stdio: 'pipe' });
201
+ if (verbose) {
202
+ logInfo(`āœ… Rebase aborted`);
203
+ }
204
+ }
205
+ catch (error) {
206
+ // Ignore errors - rebase might not be in progress
207
+ }
208
+ }
65
209
  /**
66
210
  * Check if a Git branch exists locally
67
211
  */
@@ -476,3 +620,238 @@ export async function syncFeatBranchWithMain(featureId, githubToken, owner, repo
476
620
  return false;
477
621
  }
478
622
  }
623
+ /**
624
+ * Switch to feature branch and rebase with base branch, with optional automatic conflict resolution
625
+ * This is the async version that supports conflict resolution using Claude agent
626
+ *
627
+ * @param options - Options for the rebase operation
628
+ * @returns Object containing the previous branch name and conflict resolution result
629
+ */
630
+ export async function switchToFeatureBranchAndRebaseAsync(options) {
631
+ const { featureBranch, baseBranch = 'main', originalBaseBranch, verbose, resolveConflicts = false, conflictResolverConfig, } = options;
632
+ const previousBranch = getCurrentBranch();
633
+ if (verbose) {
634
+ logInfo(`\nšŸ”„ Preparing feature branch: ${featureBranch}`);
635
+ logInfo(` Current branch: ${previousBranch}`);
636
+ }
637
+ // If feature branch doesn't exist, we need to create it from base branch
638
+ if (!branchExists(featureBranch)) {
639
+ if (verbose) {
640
+ logInfo(` Feature branch ${featureBranch} doesn't exist, will create from ${baseBranch}`);
641
+ }
642
+ // First, ensure we're on the base branch to create feature branch from it
643
+ if (previousBranch !== baseBranch) {
644
+ if (verbose) {
645
+ logInfo(` Switching to ${baseBranch} first...`);
646
+ }
647
+ switchToBranch(baseBranch, verbose);
648
+ }
649
+ // Pull latest base branch before creating feature branch
650
+ try {
651
+ if (verbose) {
652
+ logInfo(` Pulling latest ${baseBranch}...`);
653
+ }
654
+ pullLatestFromBranch(baseBranch, verbose);
655
+ }
656
+ catch (error) {
657
+ if (verbose) {
658
+ logInfo(` āš ļø Could not pull ${baseBranch}, will create branch from local ${baseBranch}`);
659
+ }
660
+ }
661
+ }
662
+ // Switch to feature branch (will create if doesn't exist)
663
+ if (getCurrentBranch() !== featureBranch) {
664
+ switchToBranch(featureBranch, verbose);
665
+ }
666
+ // Sync with remote feature branch if it exists
667
+ try {
668
+ if (hasUncommittedChanges()) {
669
+ if (verbose) {
670
+ logInfo(`āš ļø Found uncommitted changes. Resetting to clean state before sync...`);
671
+ }
672
+ resetUncommittedChanges(verbose);
673
+ }
674
+ execSync('git fetch origin', { encoding: 'utf-8', stdio: 'pipe' });
675
+ try {
676
+ execSync(`git rev-parse --verify origin/${featureBranch}`, {
677
+ encoding: 'utf-8',
678
+ stdio: 'pipe',
679
+ });
680
+ if (verbose) {
681
+ logInfo(`šŸ“„ Syncing with remote feature branch origin/${featureBranch}...`);
682
+ }
683
+ execSync(`git reset --hard origin/${featureBranch}`, {
684
+ encoding: 'utf-8',
685
+ stdio: 'pipe',
686
+ });
687
+ if (verbose) {
688
+ logInfo(`āœ… Synced local branch with origin/${featureBranch}`);
689
+ }
690
+ }
691
+ catch (e) {
692
+ if (verbose) {
693
+ logInfo(` Remote branch origin/${featureBranch} doesn't exist yet, will create on push`);
694
+ }
695
+ }
696
+ }
697
+ catch (error) {
698
+ if (verbose) {
699
+ logInfo(`āš ļø Could not sync with remote feature branch, continuing with local branch`);
700
+ }
701
+ }
702
+ // Rebase feature branch with latest base branch
703
+ if (verbose) {
704
+ if (originalBaseBranch) {
705
+ logInfo(`šŸ“„ Rebasing ${featureBranch} onto origin/${baseBranch} (from ${originalBaseBranch})...`);
706
+ }
707
+ else {
708
+ logInfo(`šŸ“„ Rebasing ${featureBranch} with origin/${baseBranch}...`);
709
+ }
710
+ }
711
+ try {
712
+ if (hasUncommittedChanges()) {
713
+ if (verbose) {
714
+ logInfo(`āš ļø Found uncommitted changes. Resetting to clean state before rebase...`);
715
+ }
716
+ resetUncommittedChanges(verbose);
717
+ }
718
+ // Fetch the base branch first
719
+ execSync(`git fetch origin ${baseBranch}`, {
720
+ encoding: 'utf-8',
721
+ stdio: 'pipe',
722
+ });
723
+ if (originalBaseBranch) {
724
+ // When base branch was squash-merged, use --onto to only rebase new commits
725
+ // This prevents re-applying commits that were already included in the squash
726
+ // Command: git rebase --onto <new-base> <old-base>
727
+ // This rebases commits from <old-base>..HEAD onto <new-base>
728
+ if (verbose) {
729
+ logInfo(` Using --onto to rebase only new commits (after ${originalBaseBranch})`);
730
+ }
731
+ execSync(`git rebase --onto origin/${baseBranch} origin/${originalBaseBranch}`, {
732
+ encoding: 'utf-8',
733
+ stdio: verbose ? 'inherit' : 'pipe',
734
+ });
735
+ }
736
+ else {
737
+ // Normal rebase
738
+ execSync(`git rebase origin/${baseBranch}`, {
739
+ encoding: 'utf-8',
740
+ stdio: verbose ? 'inherit' : 'pipe',
741
+ });
742
+ }
743
+ if (verbose) {
744
+ logInfo(`āœ… Successfully rebased ${featureBranch} with origin/${baseBranch}\n`);
745
+ }
746
+ return { previousBranch };
747
+ }
748
+ catch (error) {
749
+ // Check if there are conflicts that we can try to resolve
750
+ if (resolveConflicts && isRebaseInProgress() && hasConflicts()) {
751
+ if (verbose) {
752
+ logInfo(`\nāš ļø Rebase conflicts detected. Attempting automatic resolution...`);
753
+ }
754
+ // Dynamically import the conflict resolver to avoid circular dependencies
755
+ const { resolveConflictsWithAgent } = await import('./conflict-resolver.js');
756
+ const result = await resolveConflictsWithAgent({
757
+ model: conflictResolverConfig?.model,
758
+ maxTurns: conflictResolverConfig?.maxTurns,
759
+ verbose,
760
+ featureBranch,
761
+ baseBranch,
762
+ originalBaseBranch,
763
+ });
764
+ if (result.success) {
765
+ if (verbose) {
766
+ logInfo(`āœ… Successfully resolved conflicts and rebased ${featureBranch} with origin/${baseBranch}\n`);
767
+ }
768
+ return {
769
+ previousBranch,
770
+ conflictsResolved: true,
771
+ resolvedFiles: result.resolvedFiles,
772
+ };
773
+ }
774
+ else {
775
+ // Conflict resolution failed, abort rebase
776
+ abortRebase(verbose);
777
+ throw new Error(`Failed to automatically resolve conflicts during rebase ${featureBranch} with ${baseBranch}.\n` +
778
+ `${result.error || 'Unknown error'}\n` +
779
+ `Please resolve conflicts manually and try again.`);
780
+ }
781
+ }
782
+ // No conflict resolution attempted or not a conflict error
783
+ // Abort rebase and throw error
784
+ abortRebase(verbose);
785
+ if (verbose) {
786
+ logError(`āŒ Rebase failed and was aborted. Repository is in clean state.`);
787
+ }
788
+ throw new Error(`Failed to rebase ${featureBranch} with ${baseBranch}: ${error instanceof Error ? error.message : String(error)}\n` +
789
+ `This usually means there are conflicts between your feature branch and base branch.\n` +
790
+ `Please resolve conflicts manually and try again.`);
791
+ }
792
+ }
793
+ /**
794
+ * Async version of prepareCustomBranchGitEnvironment with conflict resolution support.
795
+ *
796
+ * Usage:
797
+ * const cleanup = await prepareCustomBranchGitEnvironmentAsync({
798
+ * featureBranch: 'feat/123/2-api',
799
+ * baseBranch: 'feat/123/1-database',
800
+ * verbose: true,
801
+ * resolveConflicts: true,
802
+ * })
803
+ * try {
804
+ * // ... phase logic ...
805
+ * } finally {
806
+ * cleanup()
807
+ * }
808
+ *
809
+ * @param options - Options for the git environment preparation
810
+ * @returns Cleanup function that will return to main branch
811
+ */
812
+ export async function prepareCustomBranchGitEnvironmentAsync(options) {
813
+ const { verbose } = options;
814
+ // Create cleanup function BEFORE any operations that might fail
815
+ const cleanup = () => {
816
+ returnToMainBranch('main', verbose);
817
+ };
818
+ try {
819
+ const result = await switchToFeatureBranchAndRebaseAsync(options);
820
+ return {
821
+ cleanup,
822
+ conflictsResolved: result.conflictsResolved,
823
+ resolvedFiles: result.resolvedFiles,
824
+ };
825
+ }
826
+ catch (error) {
827
+ try {
828
+ cleanup();
829
+ }
830
+ catch (cleanupError) {
831
+ if (verbose) {
832
+ logError(`āš ļø Cleanup failed after setup error: ${cleanupError}`);
833
+ }
834
+ }
835
+ throw error;
836
+ }
837
+ }
838
+ /**
839
+ * Async version of preparePhaseGitEnvironment with conflict resolution support.
840
+ *
841
+ * @param featureId - The feature ID (will be used to construct branch name "dev/{featureId}")
842
+ * @param baseBranch - The base branch to rebase from (default: "main")
843
+ * @param verbose - Whether to log verbose output
844
+ * @param resolveConflicts - Whether to automatically resolve conflicts using Claude
845
+ * @param conflictResolverConfig - Configuration for the conflict resolver
846
+ * @returns Cleanup function and conflict resolution info
847
+ */
848
+ export async function preparePhaseGitEnvironmentAsync(featureId, baseBranch = 'main', verbose, resolveConflicts, conflictResolverConfig) {
849
+ const featureBranch = `dev/${featureId}`;
850
+ return prepareCustomBranchGitEnvironmentAsync({
851
+ featureBranch,
852
+ baseBranch,
853
+ verbose,
854
+ resolveConflicts,
855
+ conflictResolverConfig,
856
+ });
857
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "edsger",
3
- "version": "0.19.14",
3
+ "version": "0.19.16",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "edsger": "dist/index.js"