edsger 0.52.0 → 0.54.0

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.
@@ -12,6 +12,7 @@ import { getIssue } from '../../api/issues/get-issue.js';
12
12
  import { DEFAULT_MODEL } from '../../constants.js';
13
13
  import { logIssuePhaseEvent } from '../../services/audit-logs.js';
14
14
  import { createBranches, getCurrentBranch, updateBranch, } from '../../services/branches.js';
15
+ import { getDefaultBranchForIssue } from '../../services/repo-config.js';
15
16
  import { prepareCustomBranchGitEnvironmentAsync, resetUncommittedChanges, } from '../../utils/git-branch-manager.js';
16
17
  import { gitPush } from '../../utils/git-push.js';
17
18
  import { logDebug, logError, logInfo } from '../../utils/logger.js';
@@ -128,7 +129,7 @@ function parseIterationResult(responseText) {
128
129
  * Handle a successful iteration: push, create PR, and log
129
130
  */
130
131
  async function handleSuccessfulIteration(opts) {
131
- const { issueId, devBranchName, currentBranch, iterationResult, totalIterations, iterationDuration, startTime, endTime, existingPrUrl, verbose, } = opts;
132
+ const { issueId, devBranchName, currentBranch, iterationResult, totalIterations, iterationDuration, startTime, endTime, existingPrUrl, defaultBranch, verbose, } = opts;
132
133
  // Get GitHub config for push authentication and PR creation
133
134
  const githubConfig = await getGitHubConfig(issueId, verbose);
134
135
  // Push to remote with authentication
@@ -144,7 +145,7 @@ async function handleSuccessfulIteration(opts) {
144
145
  let prUrl = existingPrUrl;
145
146
  let prNumber;
146
147
  if (!prUrl) {
147
- const prInfo = await createAutonomousPR(githubConfig, devBranchName, currentBranch, verbose);
148
+ const prInfo = await createAutonomousPR(githubConfig, devBranchName, currentBranch, defaultBranch, verbose);
148
149
  ({ prUrl, prNumber } = prInfo);
149
150
  }
150
151
  // Log iteration completion to audit logs
@@ -173,7 +174,7 @@ async function handleSuccessfulIteration(opts) {
173
174
  */
174
175
  async function createAutonomousPR(
175
176
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
176
- githubConfig, devBranchName, currentBranch, verbose) {
177
+ githubConfig, devBranchName, currentBranch, defaultBranch, verbose) {
177
178
  if (!githubConfig.configured ||
178
179
  !githubConfig.token ||
179
180
  !githubConfig.owner ||
@@ -190,7 +191,7 @@ githubConfig, devBranchName, currentBranch, verbose) {
190
191
  verbose,
191
192
  };
192
193
  logInfo(`šŸ“ Creating pull request for autonomous work...`);
193
- const prResult = await createBranchPullRequest(prConfig, devBranchName, currentBranch.name, currentBranch.description || 'Autonomous development implementation', 'main');
194
+ const prResult = await createBranchPullRequest(prConfig, devBranchName, currentBranch.name, currentBranch.description || 'Autonomous development implementation', defaultBranch);
194
195
  if (prResult.success) {
195
196
  logInfo(`āœ… Pull request created: ${prResult.pullRequestUrl}`);
196
197
  return {
@@ -299,10 +300,11 @@ export async function runAutonomousDevelopment(options, config, _checklistContex
299
300
  }
300
301
  }
301
302
  }
302
- // 4. Prepare git environment (checkout branch + rebase with main)
303
+ // 4. Prepare git environment (checkout branch + rebase with default branch)
304
+ const defaultBranch = await getDefaultBranchForIssue(issueId, verbose);
303
305
  const { cleanup: cleanupGit } = await prepareCustomBranchGitEnvironmentAsync({
304
306
  issueBranch: devBranchName,
305
- baseBranch: 'main',
307
+ baseBranch: defaultBranch,
306
308
  verbose,
307
309
  resolveConflicts: true,
308
310
  conflictResolverConfig: {
@@ -334,6 +336,7 @@ export async function runAutonomousDevelopment(options, config, _checklistContex
334
336
  startTime,
335
337
  endTime,
336
338
  existingPrUrl: prUrl,
339
+ defaultBranch,
337
340
  verbose,
338
341
  });
339
342
  prUrl = prInfo.prUrl || prUrl;
@@ -169,7 +169,7 @@ export const planIssueBranches = async (options, config) => {
169
169
  // Build the user prompt based on mode
170
170
  const contextInfo = formatContextForPrompt(context.issue, context.product, context.user_stories, context.test_cases);
171
171
  const existingBranchesInfo = formatExistingBranchesForPrompt(context.existing_branches);
172
- const systemPrompt = await createBranchPlanningSystemPrompt(config, issueId);
172
+ const systemPrompt = await createBranchPlanningSystemPrompt(config, issueId, undefined, verbose);
173
173
  const userPrompt = await buildUserPrompt({
174
174
  isIncrementalUpdate,
175
175
  feedbacksContext,
@@ -2,7 +2,7 @@ import { type EdsgerConfig } from '../../types/index.js';
2
2
  /**
3
3
  * Create the system prompt for branch planning
4
4
  */
5
- export declare function createBranchPlanningSystemPrompt(config: EdsgerConfig, issueId: string, projectDir?: string): Promise<string>;
5
+ export declare function createBranchPlanningSystemPrompt(config: EdsgerConfig, issueId: string, projectDir?: string, verbose?: boolean): Promise<string>;
6
6
  /**
7
7
  * Create the user prompt with context for branch planning
8
8
  */
@@ -1,9 +1,10 @@
1
+ import { getDefaultBranchForIssue } from '../../services/repo-config.js';
1
2
  import { resolveSkill, substituteVariables, } from '../../services/skill-resolver.js';
2
3
  import { OUTPUT_CONTRACTS } from '../output-contracts.js';
3
4
  /**
4
5
  * Create the system prompt for branch planning
5
6
  */
6
- export async function createBranchPlanningSystemPrompt(config, issueId, projectDir) {
7
+ export async function createBranchPlanningSystemPrompt(config, issueId, projectDir, verbose) {
7
8
  const skill = await resolveSkill('phase/branch-planning', {
8
9
  projectDir,
9
10
  });
@@ -12,7 +13,7 @@ export async function createBranchPlanningSystemPrompt(config, issueId, projectD
12
13
  }
13
14
  let { prompt } = skill;
14
15
  prompt = substituteVariables(prompt, {
15
- BASE_BRANCH: 'main',
16
+ BASE_BRANCH: await getDefaultBranchForIssue(issueId, verbose),
16
17
  ISSUE_ID: issueId,
17
18
  });
18
19
  return `${prompt}\n\n${OUTPUT_CONTRACTS['branch-planning']}`;
@@ -1,6 +1,7 @@
1
1
  import { query } from '@anthropic-ai/claude-agent-sdk';
2
2
  import { DEFAULT_MODEL } from '../../constants.js';
3
3
  import { formatFeedbacksForContext, getFeedbacksForPhase, } from '../../services/feedbacks.js';
4
+ import { getDefaultBranchForIssue } from '../../services/repo-config.js';
4
5
  import { preparePhaseGitEnvironment } from '../../utils/git-branch-manager.js';
5
6
  import { extractJsonFromResponse } from '../../utils/json-extractor.js';
6
7
  import { logDebug, logError, logInfo } from '../../utils/logger.js';
@@ -38,8 +39,11 @@ export const fixTestFailures = async (options, config) => {
38
39
  if (verbose) {
39
40
  logInfo(`Starting bug fixing for issue ID: ${issueId} (Attempt ${attemptNumber})`);
40
41
  }
41
- // Prepare git environment: switch to issue branch and rebase with main
42
- const cleanupGit = preparePhaseGitEnvironment(issueId, 'main', verbose);
42
+ // Prepare git environment: switch to issue branch and rebase with the
43
+ // repo's default branch (resolved via GitHub API for consistency with
44
+ // other phases — same source of truth for forks where origin/HEAD may lag).
45
+ const defaultBranch = await getDefaultBranchForIssue(issueId, verbose);
46
+ const cleanupGit = preparePhaseGitEnvironment(issueId, defaultBranch, verbose);
43
47
  try {
44
48
  // Fetch all required context information via MCP endpoints
45
49
  if (verbose) {
@@ -4,6 +4,7 @@
4
4
  */
5
5
  import { Octokit } from '@octokit/rest';
6
6
  import { gitPush } from '../../utils/git-push.js';
7
+ import { resolveDefaultBranch } from '../../utils/github-repo-info.js';
7
8
  import { logError, logInfo } from '../../utils/logger.js';
8
9
  // GitHub PR title best practice: keep under 72 characters
9
10
  const MAX_PR_TITLE_LENGTH = 72;
@@ -44,7 +45,8 @@ export function devBranchToFeatBranch(devBranchName) {
44
45
  * Create a pull request from a dev/ branch to its corresponding feat/ branch
45
46
  * This is used after code implementation in multi-branch issues
46
47
  */
47
- export async function createBranchPullRequest(config, devBranchName, issueName, branchDescription, baseBranch = 'main') {
48
+ export async function createBranchPullRequest(config, devBranchName, issueName, branchDescription, baseBranch) {
49
+ const resolvedBaseBranch = baseBranch ?? resolveDefaultBranch();
48
50
  const { githubToken, owner, repo, verbose } = config;
49
51
  if (!devBranchName.startsWith('dev/')) {
50
52
  return {
@@ -75,13 +77,13 @@ export async function createBranchPullRequest(config, devBranchName, issueName,
75
77
  logInfo(`šŸ“ Ensuring target branch ${featBranchName} exists...`);
76
78
  }
77
79
  try {
78
- // Get the base branch (main) SHA
80
+ // Get the base branch SHA
79
81
  const { data: baseBranchData } = await octokit.repos.getBranch({
80
82
  owner,
81
83
  repo,
82
- branch: baseBranch,
84
+ branch: resolvedBaseBranch,
83
85
  });
84
- // Create the feat/ branch from main
86
+ // Create the feat/ branch from the resolved base
85
87
  await octokit.git.createRef({
86
88
  owner,
87
89
  repo,
@@ -89,7 +91,7 @@ export async function createBranchPullRequest(config, devBranchName, issueName,
89
91
  sha: baseBranchData.commit.sha,
90
92
  });
91
93
  if (verbose) {
92
- logInfo(`āœ… Created target branch ${featBranchName} from ${baseBranch}`);
94
+ logInfo(`āœ… Created target branch ${featBranchName} from ${resolvedBaseBranch}`);
93
95
  }
94
96
  }
95
97
  catch (error) {
@@ -8,6 +8,7 @@ import { createBranches, getBaseBranchInfo, getBranches, getCurrentBranch, updat
8
8
  import { formatChecklistsForContext, } from '../../services/checklist.js';
9
9
  import { extractChecklistItems, runPhaseCoaching, } from '../../services/coaching/index.js';
10
10
  import { formatFeedbacksForContext, getFeedbacksForPhase, } from '../../services/feedbacks.js';
11
+ import { getDefaultBranchForIssue } from '../../services/repo-config.js';
11
12
  import { prepareCustomBranchGitEnvironmentAsync, syncFeatBranchWithMain, } from '../../utils/git-branch-manager.js';
12
13
  import { gitPush } from '../../utils/git-push.js';
13
14
  import { extractJsonFromResponse } from '../../utils/json-extractor.js';
@@ -172,7 +173,10 @@ message, lastAssistantResponse, issueId, verbose
172
173
  }
173
174
  // eslint-disable-next-line complexity
174
175
  export const implementIssueCode = async (options, config, checklistContext) => {
175
- const { issueId, verbose, baseBranch = 'main' } = options;
176
+ const { issueId, verbose } = options;
177
+ // Resolve the repo's default branch (handles forks where default is master, etc.).
178
+ // Caller may override via options.baseBranch (e.g. branch-chained PR base).
179
+ const baseBranch = options.baseBranch ?? (await getDefaultBranchForIssue(issueId, verbose));
176
180
  if (verbose) {
177
181
  logInfo(`Starting code implementation for issue ID: ${issueId}`);
178
182
  logInfo(`Base branch: ${baseBranch}`);
@@ -322,7 +326,7 @@ export const implementIssueCode = async (options, config, checklistContext) => {
322
326
  githubToken: githubConfig.token,
323
327
  owner: githubConfig.owner,
324
328
  repo: githubConfig.repo,
325
- baseBranch: 'main',
329
+ baseBranch,
326
330
  verbose,
327
331
  });
328
332
  }
@@ -6,6 +6,7 @@
6
6
  import { getGitHubConfig } from '../../api/github.js';
7
7
  import { DEFAULT_MODEL } from '../../constants.js';
8
8
  import { getBaseBranchInfo, getBranches, updateBranch, } from '../../services/branches.js';
9
+ import { getDefaultBranchForIssue } from '../../services/repo-config.js';
9
10
  import { getUncommittedFiles, hasUncommittedChanges, prepareCustomBranchGitEnvironmentAsync, preparePhaseGitEnvironmentAsync, syncFeatBranchWithMain, } from '../../utils/git-branch-manager.js';
10
11
  import { gitPushCurrentBranch } from '../../utils/git-push.js';
11
12
  import { logError, logInfo } from '../../utils/logger.js';
@@ -47,12 +48,14 @@ export const refineCodeFromPRFeedback = async (options, config, checklistContext
47
48
  if (verbose) {
48
49
  logInfo(`Starting code refine for issue ID: ${issueId}`);
49
50
  }
51
+ // Resolve the repo's default branch (handles forks where default is master, etc.)
52
+ const defaultBranch = await getDefaultBranchForIssue(issueId, verbose);
50
53
  // For multi-branch issues, find the branch that has been reviewed
51
54
  // (status = 'reviewed' after code-review phase) and use its branch_name for refine
52
55
  let branchName = `dev/${issueId}`; // Default for single-branch issues
53
56
  let currentBranch = null;
54
57
  let allBranches = [];
55
- let baseBranchForRebase = 'main'; // Default base branch for rebase
58
+ let baseBranchForRebase = defaultBranch; // Default base branch for rebase
56
59
  let rebaseTargetBranchForRebase; // Target for --onto rebase (e.g., main)
57
60
  let originalBaseBranchForRebase; // For --onto when base was squash-merged
58
61
  let baseBranchCompletedForRebase = false;
@@ -68,7 +71,7 @@ export const refineCodeFromPRFeedback = async (options, config, checklistContext
68
71
  logInfo(` Using dev branch: ${branchName}`);
69
72
  }
70
73
  // Determine the correct base branch for rebase using base_branch_id
71
- const baseBranchInfo = getBaseBranchInfo(currentBranch, allBranches, 'main');
74
+ const baseBranchInfo = getBaseBranchInfo(currentBranch, allBranches, defaultBranch);
72
75
  // Always use the baseBranch from getBaseBranchInfo - it handles all cases:
73
76
  // - No base branch: returns 'main'
74
77
  // - Base branch merged: returns feat branch (new branches created from feat/xxx)
@@ -114,14 +117,14 @@ export const refineCodeFromPRFeedback = async (options, config, checklistContext
114
117
  githubConfig.repo) {
115
118
  const featBranchName = branchName.replace(/^dev\//, 'feat/');
116
119
  if (verbose) {
117
- logInfo(`šŸ“„ Syncing ${featBranchName} with main before rebase...`);
120
+ logInfo(`šŸ“„ Syncing ${featBranchName} with ${defaultBranch} before rebase...`);
118
121
  }
119
122
  featSyncedToMain = await syncFeatBranchWithMain({
120
123
  featBranch: featBranchName,
121
124
  githubToken: githubConfig.token,
122
125
  owner: githubConfig.owner,
123
126
  repo: githubConfig.repo,
124
- baseBranch: 'main',
127
+ baseBranch: defaultBranch,
125
128
  verbose,
126
129
  });
127
130
  }
@@ -150,7 +153,7 @@ export const refineCodeFromPRFeedback = async (options, config, checklistContext
150
153
  baseBranchCompleted: baseBranchCompletedForRebase,
151
154
  githubToken,
152
155
  })
153
- : await preparePhaseGitEnvironmentAsync(issueId, 'main', verbose, true, {
156
+ : await preparePhaseGitEnvironmentAsync(issueId, defaultBranch, verbose, true, {
154
157
  model: DEFAULT_MODEL,
155
158
  });
156
159
  const cleanupGit = gitEnvResult.cleanup;
@@ -10,6 +10,7 @@ import { DEFAULT_MODEL } from '../../constants.js';
10
10
  import { getBaseBranchInfo, getBranches, updateBranch, } from '../../services/branches.js';
11
11
  import { formatChecklistsForContext, } from '../../services/checklist.js';
12
12
  import { formatFeedbacksForContext, getFeedbacksForPhase, } from '../../services/feedbacks.js';
13
+ import { getDefaultBranchForIssue } from '../../services/repo-config.js';
13
14
  import { prepareCustomBranchGitEnvironmentAsync, preparePhaseGitEnvironmentAsync, syncFeatBranchWithMain, } from '../../utils/git-branch-manager.js';
14
15
  import { logError, logInfo } from '../../utils/logger.js';
15
16
  import { createPromptGenerator, extractTextFromContent, tryExtractResult, } from '../pr-shared/agent-utils.js';
@@ -73,12 +74,14 @@ export const reviewPullRequest = async (options, config, checklistContext) => {
73
74
  if (verbose) {
74
75
  logInfo(`Starting code review for issue ID: ${issueId}`);
75
76
  }
77
+ // Resolve the repo's default branch (handles forks where default is master, etc.)
78
+ const defaultBranch = await getDefaultBranchForIssue(issueId, verbose);
76
79
  // For multi-branch issues, find the branch that is ready for review
77
80
  // and use its branch_name for review (branch_name is stored as dev/...)
78
81
  let branchName = `dev/${issueId}`; // Default for single-branch issues
79
82
  let currentBranch = null;
80
83
  let allBranches = [];
81
- let baseBranchForRebase = 'main'; // Default base branch for rebase
84
+ let baseBranchForRebase = defaultBranch; // Default base branch for rebase
82
85
  let rebaseTargetBranchForRebase; // Target for --onto rebase (e.g., main)
83
86
  let originalBaseBranchForRebase; // For --onto when base was squash-merged
84
87
  let baseBranchCompletedForRebase = false;
@@ -95,7 +98,7 @@ export const reviewPullRequest = async (options, config, checklistContext) => {
95
98
  logInfo(` Using dev branch: ${branchName}`);
96
99
  }
97
100
  // Determine the correct base branch for rebase using base_branch_id
98
- const baseBranchInfo = getBaseBranchInfo(currentBranch, allBranches, 'main');
101
+ const baseBranchInfo = getBaseBranchInfo(currentBranch, allBranches, defaultBranch);
99
102
  // Always use the baseBranch from getBaseBranchInfo - it handles all cases:
100
103
  // - No base branch: returns 'main'
101
104
  // - Base branch merged: returns feat branch (new branches created from feat/xxx)
@@ -141,14 +144,14 @@ export const reviewPullRequest = async (options, config, checklistContext) => {
141
144
  githubConfig.repo) {
142
145
  const featBranchName = branchName.replace(/^dev\//, 'feat/');
143
146
  if (verbose) {
144
- logInfo(`šŸ“„ Syncing ${featBranchName} with main before rebase...`);
147
+ logInfo(`šŸ“„ Syncing ${featBranchName} with ${defaultBranch} before rebase...`);
145
148
  }
146
149
  featSyncedToMain = await syncFeatBranchWithMain({
147
150
  featBranch: featBranchName,
148
151
  githubToken: githubConfig.token,
149
152
  owner: githubConfig.owner,
150
153
  repo: githubConfig.repo,
151
- baseBranch: 'main',
154
+ baseBranch: defaultBranch,
152
155
  verbose,
153
156
  });
154
157
  }
@@ -177,7 +180,7 @@ export const reviewPullRequest = async (options, config, checklistContext) => {
177
180
  baseBranchCompleted: baseBranchCompletedForRebase,
178
181
  githubToken,
179
182
  })
180
- : await preparePhaseGitEnvironmentAsync(issueId, 'main', verbose, true, {
183
+ : await preparePhaseGitEnvironmentAsync(issueId, defaultBranch, verbose, true, {
181
184
  model: DEFAULT_MODEL,
182
185
  });
183
186
  const cleanupGit = gitEnvResult.cleanup;
@@ -1,5 +1,11 @@
1
1
  import { query } from '@anthropic-ai/claude-agent-sdk';
2
2
  import { DEFAULT_MODEL } from '../../constants.js';
3
+ import { getDefaultBranchForIssue } from '../../services/repo-config.js';
4
+ import { preparePhaseGitEnvironment } from '../../utils/git-branch-manager.js';
5
+ import { extractJsonFromResponse } from '../../utils/json-extractor.js';
6
+ import { logDebug, logError, logInfo } from '../../utils/logger.js';
7
+ import { fetchCodeTestingContext, formatContextForPrompt, } from './context-fetcher.js';
8
+ import { createCodeTestingPromptWithContext, createCodeTestingSystemPrompt, } from './prompts.js';
3
9
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
4
10
  function processAssistantContent(content, verbose) {
5
11
  let text = '';
@@ -14,11 +20,6 @@ function processAssistantContent(content, verbose) {
14
20
  }
15
21
  return text;
16
22
  }
17
- import { preparePhaseGitEnvironment } from '../../utils/git-branch-manager.js';
18
- import { extractJsonFromResponse } from '../../utils/json-extractor.js';
19
- import { logDebug, logError, logInfo } from '../../utils/logger.js';
20
- import { fetchCodeTestingContext, formatContextForPrompt, } from './context-fetcher.js';
21
- import { createCodeTestingPromptWithContext, createCodeTestingSystemPrompt, } from './prompts.js';
22
23
  function userMessage(content) {
23
24
  return {
24
25
  type: 'user',
@@ -37,8 +38,11 @@ export const writeCodeTests = async (options, config) => {
37
38
  if (verbose) {
38
39
  logInfo(`Starting code testing phase for issue ID: ${issueId}`);
39
40
  }
40
- // Prepare git environment: switch to issue branch and rebase with main
41
- const cleanupGit = preparePhaseGitEnvironment(issueId, 'main', verbose);
41
+ // Prepare git environment: switch to issue branch and rebase with the
42
+ // repo's default branch (resolved via GitHub API for consistency with
43
+ // other phases — same source of truth for forks where origin/HEAD may lag).
44
+ const defaultBranch = await getDefaultBranchForIssue(issueId, verbose);
45
+ const cleanupGit = preparePhaseGitEnvironment(issueId, defaultBranch, verbose);
42
46
  try {
43
47
  // Fetch all required context information via MCP endpoints
44
48
  if (verbose) {
@@ -19,6 +19,8 @@ export interface PRExecutionContext {
19
19
  devBranchHeadSha: string;
20
20
  githubConfig: GitHubConfigInfo;
21
21
  forkInfo: RepoForkInfo;
22
+ /** Resolved default branch (e.g. main, master). Used for checkout/diff. */
23
+ defaultBranch: string;
22
24
  isIncrementalSync: boolean;
23
25
  lastSyncedCommit: string | null;
24
26
  diffStat: string;
@@ -1,10 +1,10 @@
1
- import { execFileSync, execSync } from 'child_process';
1
+ import { execFileSync } from 'child_process';
2
2
  import { getGitHubConfig } from '../../api/github.js';
3
3
  import { getIssue } from '../../api/issues/index.js';
4
4
  import { getPullRequests, } from '../../services/pull-requests.js';
5
5
  import { branchExists, remoteBranchExists, } from '../../utils/git-branch-manager.js';
6
6
  import { buildCredentialArgs } from '../../utils/git-push.js';
7
- import { getRepoForkInfo, } from '../../utils/github-repo-info.js';
7
+ import { getRepoForkInfo, resolveDefaultBranch, } from '../../utils/github-repo-info.js';
8
8
  import { logError, logInfo } from '../../utils/logger.js';
9
9
  /**
10
10
  * Get the dev branch name for an issue
@@ -16,14 +16,16 @@ function getDevBranchName(issueId) {
16
16
  * Get the HEAD SHA of a branch
17
17
  */
18
18
  function getBranchHeadSha(branchName) {
19
- return execSync(`git rev-parse ${branchName}`, { encoding: 'utf-8' }).trim();
19
+ return execFileSync('git', ['rev-parse', branchName], {
20
+ encoding: 'utf-8',
21
+ }).trim();
20
22
  }
21
23
  /**
22
24
  * Get diff stat between two refs
23
25
  */
24
26
  function getDiffStat(baseRef, headRef) {
25
27
  try {
26
- return execSync(`git diff --stat ${baseRef}...${headRef}`, {
28
+ return execFileSync('git', ['diff', '--stat', `${baseRef}...${headRef}`], {
27
29
  encoding: 'utf-8',
28
30
  }).trim();
29
31
  }
@@ -51,9 +53,7 @@ function parseGitStatus(status) {
51
53
  */
52
54
  function getChangedFiles(baseRef, headRef) {
53
55
  try {
54
- const output = execSync(`git diff --name-status ${baseRef}...${headRef}`, {
55
- encoding: 'utf-8',
56
- }).trim();
56
+ const output = execFileSync('git', ['diff', '--name-status', `${baseRef}...${headRef}`], { encoding: 'utf-8' }).trim();
57
57
  if (!output) {
58
58
  return [];
59
59
  }
@@ -77,7 +77,7 @@ function getChangedFiles(baseRef, headRef) {
77
77
  */
78
78
  function isAncestor(commitSha, ref) {
79
79
  try {
80
- execSync(`git merge-base --is-ancestor ${commitSha} ${ref}`, {
80
+ execFileSync('git', ['merge-base', '--is-ancestor', commitSha, ref], {
81
81
  encoding: 'utf-8',
82
82
  stdio: 'pipe',
83
83
  });
@@ -121,25 +121,50 @@ export async function fetchPRExecutionContext(issueId, verbose) {
121
121
  getPullRequests({ issueId, verbose }),
122
122
  getGitHubConfig(issueId, verbose),
123
123
  ]);
124
- // Fetch latest remote refs and fast-forward local main
124
+ // Detect fork status + default branch (used by the fast-forward step below)
125
+ let forkInfo = { isFork: false };
126
+ if (githubConfig.token && githubConfig.owner && githubConfig.repo) {
127
+ try {
128
+ forkInfo = await getRepoForkInfo(githubConfig.token, githubConfig.owner, githubConfig.repo);
129
+ if (verbose) {
130
+ logInfo(forkInfo.isFork
131
+ ? `šŸ“Œ Repository is a fork of ${forkInfo.upstream?.owner}/${forkInfo.upstream?.repo}`
132
+ : `šŸ“Œ Repository is not a fork`);
133
+ }
134
+ }
135
+ catch (error) {
136
+ if (verbose) {
137
+ logError(`Failed to detect fork status: ${error instanceof Error ? error.message : String(error)}`);
138
+ }
139
+ }
140
+ }
141
+ // Resolve the repo's default branch (e.g. main, master, develop). Falls back
142
+ // through GitHub API → local symbolic-ref → literal 'main'.
143
+ const defaultBranch = resolveDefaultBranch(forkInfo.defaultBranch);
144
+ // Fetch latest remote refs and fast-forward the local default branch.
145
+ // Use execFileSync (not execSync) so defaultBranch — which can flow in from
146
+ // the GitHub API's default_branch field — is never passed through a shell.
125
147
  try {
126
148
  const credArgs = buildCredentialArgs(githubConfig.token);
127
149
  execFileSync('git', [...credArgs, 'fetch', 'origin'], {
128
150
  encoding: 'utf-8',
129
151
  stdio: 'pipe',
130
152
  });
131
- execSync('git checkout main', { encoding: 'utf-8', stdio: 'pipe' });
132
- execSync('git merge --ff-only origin/main', {
153
+ execFileSync('git', ['checkout', defaultBranch], {
154
+ encoding: 'utf-8',
155
+ stdio: 'pipe',
156
+ });
157
+ execFileSync('git', ['merge', '--ff-only', `origin/${defaultBranch}`], {
133
158
  encoding: 'utf-8',
134
159
  stdio: 'pipe',
135
160
  });
136
161
  if (verbose) {
137
- logInfo('āœ… Local main synced with origin/main');
162
+ logInfo(`āœ… Local ${defaultBranch} synced with origin/${defaultBranch}`);
138
163
  }
139
164
  }
140
165
  catch (error) {
141
166
  if (verbose) {
142
- logInfo(`āš ļø Could not sync main with origin: ${error instanceof Error ? error.message : String(error)}`);
167
+ logInfo(`āš ļø Could not sync ${defaultBranch} with origin: ${error instanceof Error ? error.message : String(error)}`);
143
168
  }
144
169
  }
145
170
  // Verify dev branch exists
@@ -168,23 +193,6 @@ export async function fetchPRExecutionContext(issueId, verbose) {
168
193
  if (!githubConfig.configured) {
169
194
  throw new Error(`GitHub is not configured. ${githubConfig.message || 'Please configure GitHub integration.'}`);
170
195
  }
171
- // Detect fork status
172
- let forkInfo = { isFork: false };
173
- if (githubConfig.token && githubConfig.owner && githubConfig.repo) {
174
- try {
175
- forkInfo = await getRepoForkInfo(githubConfig.token, githubConfig.owner, githubConfig.repo);
176
- if (verbose) {
177
- logInfo(forkInfo.isFork
178
- ? `šŸ“Œ Repository is a fork of ${forkInfo.upstream?.owner}/${forkInfo.upstream?.repo}`
179
- : `šŸ“Œ Repository is not a fork`);
180
- }
181
- }
182
- catch (error) {
183
- if (verbose) {
184
- logError(`Failed to detect fork status: ${error instanceof Error ? error.message : String(error)}`);
185
- }
186
- }
187
- }
188
196
  // Determine sync mode
189
197
  const devRef = localExists ? devBranchName : `origin/${devBranchName}`;
190
198
  const devBranchHeadSha = getBranchHeadSha(devRef);
@@ -202,8 +210,8 @@ export async function fetchPRExecutionContext(issueId, verbose) {
202
210
  lastSyncedCommit = null;
203
211
  isIncrementalSync = false;
204
212
  }
205
- // Get diff info: for incremental, diff from last sync; for first run, diff from main
206
- const diffBase = isIncrementalSync && lastSyncedCommit ? lastSyncedCommit : 'main';
213
+ // Get diff info: for incremental, diff from last sync; for first run, diff from default branch
214
+ const diffBase = isIncrementalSync && lastSyncedCommit ? lastSyncedCommit : defaultBranch;
207
215
  const diffStat = getDiffStat(diffBase, devRef);
208
216
  const changedFiles = getChangedFiles(diffBase, devRef);
209
217
  if (verbose) {
@@ -223,6 +231,7 @@ export async function fetchPRExecutionContext(issueId, verbose) {
223
231
  devBranchHeadSha,
224
232
  githubConfig,
225
233
  forkInfo,
234
+ defaultBranch,
226
235
  isIncrementalSync,
227
236
  lastSyncedCommit,
228
237
  diffStat,
@@ -1,9 +1,10 @@
1
1
  import { query } from '@anthropic-ai/claude-agent-sdk';
2
- import { execSync } from 'child_process';
2
+ import { execFileSync } from 'child_process';
3
3
  import { DEFAULT_MODEL } from '../../constants.js';
4
4
  import { logIssuePhaseEvent } from '../../services/audit-logs.js';
5
5
  import { getPullRequests, } from '../../services/pull-requests.js';
6
6
  import { getCurrentBranch, returnToMainBranch, } from '../../utils/git-branch-manager.js';
7
+ import { resolveDefaultBranch } from '../../utils/github-repo-info.js';
7
8
  import { logDebug, logError, logInfo } from '../../utils/logger.js';
8
9
  import { fetchPRExecutionContext } from './context.js';
9
10
  import { assignNewFilesToPRs, removeDeletedFilesFromPRs, removeStaleFilesFromPRs, } from './file-assigner.js';
@@ -118,13 +119,16 @@ export const executeIssuePRs = async (options, config) => {
118
119
  ? '\nšŸ”„ Syncing PR branches with latest changes...'
119
120
  : '\nšŸ”§ Creating PR branches and moving code...');
120
121
  }
121
- // Ensure we're on main before the agent starts
122
+ // Ensure we're on the repo's default branch before the agent starts
122
123
  const currentBranch = getCurrentBranch();
123
- if (currentBranch !== 'main') {
124
+ if (currentBranch !== context.defaultBranch) {
124
125
  if (verbose) {
125
- logInfo(`Switching from ${currentBranch} to main...`);
126
+ logInfo(`Switching from ${currentBranch} to ${context.defaultBranch}...`);
126
127
  }
127
- execSync('git checkout main', { encoding: 'utf-8', stdio: 'pipe' });
128
+ execFileSync('git', ['checkout', context.defaultBranch], {
129
+ encoding: 'utf-8',
130
+ stdio: 'pipe',
131
+ });
128
132
  }
129
133
  let systemPrompt;
130
134
  let userPrompt;
@@ -137,11 +141,12 @@ export const executeIssuePRs = async (options, config) => {
137
141
  lastSyncedCommit: context.lastSyncedCommit,
138
142
  diffStat: context.diffStat,
139
143
  changedFiles: context.changedFiles,
144
+ defaultBranch: context.defaultBranch,
140
145
  });
141
146
  }
142
147
  else {
143
148
  systemPrompt = await createPRExecutionSystemPrompt(issueId, context.devBranchName);
144
- userPrompt = createPRExecutionPrompt(issueId, context.devBranchName, activePullRequests);
149
+ userPrompt = createPRExecutionPrompt(issueId, context.devBranchName, activePullRequests, context.defaultBranch);
145
150
  }
146
151
  // Execute agent query
147
152
  const agentResult = await executeAgentQuery(userPrompt, systemPrompt, config, verbose);
@@ -151,12 +156,15 @@ export const executeIssuePRs = async (options, config) => {
151
156
  if (verbose) {
152
157
  logInfo('\nšŸš€ Pushing branches to remote...');
153
158
  }
154
- // Ensure we're on main before pushing
159
+ // Ensure we're back on the default branch before pushing
155
160
  try {
156
- execSync('git checkout main', { encoding: 'utf-8', stdio: 'pipe' });
161
+ execFileSync('git', ['checkout', context.defaultBranch], {
162
+ encoding: 'utf-8',
163
+ stdio: 'pipe',
164
+ });
157
165
  }
158
166
  catch {
159
- // Ignore if already on main
167
+ // Ignore if already on the default branch
160
168
  }
161
169
  if (!context.githubConfig.owner || !context.githubConfig.repo) {
162
170
  throw new Error('GitHub owner and repo must be configured for PR execution');
@@ -191,7 +199,7 @@ export const executeIssuePRs = async (options, config) => {
191
199
  }
192
200
  // Verify the branch exists locally
193
201
  try {
194
- execSync(`git rev-parse --verify ${pr.branch_name}`, {
202
+ execFileSync('git', ['rev-parse', '--verify', pr.branch_name], {
195
203
  encoding: 'utf-8',
196
204
  stdio: 'pipe',
197
205
  });
@@ -209,10 +217,10 @@ export const executeIssuePRs = async (options, config) => {
209
217
  else {
210
218
  executionSummary.branchesCreated++;
211
219
  }
212
- // Resolve base branch for stacked PRs
220
+ // Resolve base branch for stacked PRs (default to repo's default branch)
213
221
  const baseBranch = pr.base_pr_id
214
- ? prIdToBranch.get(pr.base_pr_id) || 'main'
215
- : 'main';
222
+ ? prIdToBranch.get(pr.base_pr_id) || context.defaultBranch
223
+ : context.defaultBranch;
216
224
  // Push branch and build compare URL (using stacked base)
217
225
  const result = pushBranchAndBuildUrl(executionConfig, pr.branch_name, baseBranch);
218
226
  if (result.success) {
@@ -225,9 +233,9 @@ export const executeIssuePRs = async (options, config) => {
225
233
  executionSummary.failedBranches.push(pr.branch_name);
226
234
  }
227
235
  }
228
- // Return to main branch
236
+ // Return to the repo's default branch
229
237
  try {
230
- returnToMainBranch('main', verbose);
238
+ returnToMainBranch(context.defaultBranch, verbose);
231
239
  }
232
240
  catch {
233
241
  // Best effort
@@ -278,9 +286,10 @@ export const executeIssuePRs = async (options, config) => {
278
286
  catch (error) {
279
287
  const errorMessage = error instanceof Error ? error.message : String(error);
280
288
  logError(`PR execution failed: ${errorMessage}`);
281
- // Try to return to main branch
289
+ // Try to return to the repo's default branch (best effort — context may
290
+ // have failed before defaultBranch was resolved, so look it up locally)
282
291
  try {
283
- returnToMainBranch('main', false);
292
+ returnToMainBranch(resolveDefaultBranch(), false);
284
293
  }
285
294
  catch {
286
295
  // Best effort
@@ -11,7 +11,7 @@ export declare function createIncrementalSyncSystemPrompt(issueId: string, devBr
11
11
  /**
12
12
  * Create the user prompt for first-time branch creation
13
13
  */
14
- export declare function createPRExecutionPrompt(issueId: string, devBranchName: string, pullRequests: PullRequest[]): string;
14
+ export declare function createPRExecutionPrompt(issueId: string, devBranchName: string, pullRequests: PullRequest[], defaultBranch: string): string;
15
15
  /**
16
16
  * Create the user prompt for incremental sync
17
17
  */
@@ -22,5 +22,6 @@ export interface IncrementalSyncPromptOptions {
22
22
  lastSyncedCommit: string;
23
23
  diffStat: string;
24
24
  changedFiles: ChangedFileInfo[];
25
+ defaultBranch: string;
25
26
  }
26
27
  export declare function createIncrementalSyncPrompt(opts: IncrementalSyncPromptOptions): string;
@@ -35,25 +35,27 @@ export async function createIncrementalSyncSystemPrompt(issueId, devBranchName,
35
35
  return `${prompt}\n\n${OUTPUT_CONTRACTS['incremental-sync']}`;
36
36
  }
37
37
  /**
38
- * Resolve the base branch for a PR based on its dependency chain
38
+ * Resolve the base branch for a PR based on its dependency chain.
39
+ * Falls back to the repo's default branch (main, master, …) when the PR has
40
+ * no upstream dependency.
39
41
  */
40
- function resolveBaseBranch(pr, allPRs) {
42
+ function resolveBaseBranch(pr, allPRs, defaultBranch) {
41
43
  if (!pr.base_pr_id) {
42
- return 'main';
44
+ return defaultBranch;
43
45
  }
44
46
  const basePR = allPRs.find((p) => p.id === pr.base_pr_id);
45
- return basePR?.branch_name || 'main';
47
+ return basePR?.branch_name || defaultBranch;
46
48
  }
47
49
  /**
48
50
  * Create the user prompt for first-time branch creation
49
51
  */
50
- export function createPRExecutionPrompt(issueId, devBranchName, pullRequests) {
52
+ export function createPRExecutionPrompt(issueId, devBranchName, pullRequests, defaultBranch) {
51
53
  const prList = pullRequests
52
54
  .map((pr) => {
53
55
  const files = pr.files
54
56
  ? pr.files.map((f) => ` - ${f.path} (${f.change_type})`).join('\n')
55
57
  : ' (no files specified)';
56
- const baseBranch = resolveBaseBranch(pr, pullRequests);
58
+ const baseBranch = resolveBaseBranch(pr, pullRequests, defaultBranch);
57
59
  return `### PR ${pr.sequence}: ${pr.name}
58
60
  - Branch: \`${pr.branch_name}\`
59
61
  - Base: \`${baseBranch}\`
@@ -71,22 +73,22 @@ ${prList}
71
73
  ## Instructions
72
74
 
73
75
  For each PR above (in sequence order):
74
- 1. Switch to the **base branch** specified in "Base" above (either \`main\` or a previous PR's branch)
76
+ 1. Switch to the **base branch** specified in "Base" above (either \`${defaultBranch}\` or a previous PR's branch)
75
77
  2. Create the branch: \`git checkout -b <branch_name>\`
76
78
  3. Apply only the listed files from \`${devBranchName}\`
77
79
  4. Commit with a descriptive message
78
80
  5. Verify with \`git diff --stat <base>...<branch_name>\`
79
81
 
80
- After all branches are created, switch back to \`main\` and provide the execution result JSON.`;
82
+ After all branches are created, switch back to \`${defaultBranch}\` and provide the execution result JSON.`;
81
83
  }
82
84
  export function createIncrementalSyncPrompt(opts) {
83
- const { issueId: _issueId, devBranchName, pullRequests, lastSyncedCommit, diffStat, changedFiles, } = opts;
85
+ const { issueId: _issueId, devBranchName, pullRequests, lastSyncedCommit, diffStat, changedFiles, defaultBranch, } = opts;
84
86
  const prList = pullRequests
85
87
  .map((pr) => {
86
88
  const files = pr.files
87
89
  ? pr.files.map((f) => ` - ${f.path} (${f.change_type})`).join('\n')
88
90
  : ' (no files specified)';
89
- const baseBranch = resolveBaseBranch(pr, pullRequests);
91
+ const baseBranch = resolveBaseBranch(pr, pullRequests, defaultBranch);
90
92
  return `### PR ${pr.sequence}: ${pr.name}
91
93
  - Branch: \`${pr.branch_name}\`
92
94
  - Base: \`${baseBranch}\`
@@ -1,4 +1,4 @@
1
- import { execFileSync, execSync } from 'child_process';
1
+ import { execFileSync } from 'child_process';
2
2
  import { getGitHubConfig } from '../../api/github.js';
3
3
  import { getIssue } from '../../api/issues/index.js';
4
4
  import { getProduct } from '../../api/products.js';
@@ -6,7 +6,7 @@ import { getBranches } from '../../services/branches.js';
6
6
  import { getPullRequests, } from '../../services/pull-requests.js';
7
7
  import { branchExists, remoteBranchExists, } from '../../utils/git-branch-manager.js';
8
8
  import { buildCredentialArgs } from '../../utils/git-push.js';
9
- import { getRepoForkInfo, } from '../../utils/github-repo-info.js';
9
+ import { getRepoForkInfo, resolveDefaultBranch, } from '../../utils/github-repo-info.js';
10
10
  import { logError, logInfo } from '../../utils/logger.js';
11
11
  /**
12
12
  * Get the dev branch name for an issue
@@ -18,14 +18,16 @@ function getDevBranchName(issueId) {
18
18
  * Get the HEAD SHA of a branch
19
19
  */
20
20
  function getBranchHeadSha(branchName) {
21
- return execSync(`git rev-parse ${branchName}`, { encoding: 'utf-8' }).trim();
21
+ return execFileSync('git', ['rev-parse', branchName], {
22
+ encoding: 'utf-8',
23
+ }).trim();
22
24
  }
23
25
  /**
24
26
  * Get diff stat between two refs
25
27
  */
26
28
  function getDiffStat(baseRef, headRef) {
27
29
  try {
28
- return execSync(`git diff --stat ${baseRef}...${headRef}`, {
30
+ return execFileSync('git', ['diff', '--stat', `${baseRef}...${headRef}`], {
29
31
  encoding: 'utf-8',
30
32
  }).trim();
31
33
  }
@@ -38,9 +40,7 @@ function getDiffStat(baseRef, headRef) {
38
40
  */
39
41
  function getChangedFiles(baseRef, headRef) {
40
42
  try {
41
- const output = execSync(`git diff --name-only ${baseRef}...${headRef}`, {
42
- encoding: 'utf-8',
43
- }).trim();
43
+ const output = execFileSync('git', ['diff', '--name-only', `${baseRef}...${headRef}`], { encoding: 'utf-8' }).trim();
44
44
  if (!output) {
45
45
  return [];
46
46
  }
@@ -53,18 +53,18 @@ function getChangedFiles(baseRef, headRef) {
53
53
  /**
54
54
  * Determine the diff base ref for incremental re-runs
55
55
  * If existing PRs have last_synced_commit, use the earliest one
56
- * Otherwise use origin/main (remote-tracking ref, always up-to-date after fetch)
56
+ * Otherwise use origin/<defaultBranch> (remote-tracking ref, always up-to-date after fetch)
57
57
  */
58
- function determineDiffBaseRef(existingPRs, replaceExisting) {
58
+ function determineDiffBaseRef(existingPRs, defaultBranchRef, replaceExisting) {
59
59
  if (replaceExisting || existingPRs.length === 0) {
60
- return 'origin/main';
60
+ return defaultBranchRef;
61
61
  }
62
62
  // Find the minimum last_synced_commit (earliest sync point)
63
63
  const syncedCommits = existingPRs
64
64
  .map((pr) => pr.last_synced_commit)
65
65
  .filter((c) => c !== null);
66
66
  if (syncedCommits.length === 0) {
67
- return 'origin/main';
67
+ return defaultBranchRef;
68
68
  }
69
69
  // All PRs should have been synced to the same commit
70
70
  // Use the first one (they should all be equal after a successful sync)
@@ -131,12 +131,17 @@ export async function fetchPRSplittingContext(issueId, verbose, replaceExisting)
131
131
  }
132
132
  }
133
133
  }
134
+ // Resolve the repo's default branch (handles non-main defaults and forks).
135
+ // Prefer the GitHub API value (already fetched in forkInfo above); fall back
136
+ // to the local symbolic-ref, then to literal 'main'.
137
+ const defaultBranch = resolveDefaultBranch(forkInfo.defaultBranch);
138
+ const defaultBranchRef = `origin/${defaultBranch}`;
134
139
  // Determine diff range
135
140
  const devRef = localExists ? devBranchName : `origin/${devBranchName}`;
136
- const baseRef = determineDiffBaseRef(existingPullRequests, replaceExisting);
141
+ const baseRef = determineDiffBaseRef(existingPullRequests, defaultBranchRef, replaceExisting);
137
142
  const devBranchHeadSha = getBranchHeadSha(devRef);
138
143
  // Check if there are new changes since last sync
139
- if (baseRef !== 'origin/main' && baseRef === devBranchHeadSha) {
144
+ if (baseRef !== defaultBranchRef && baseRef === devBranchHeadSha) {
140
145
  if (verbose) {
141
146
  logInfo(`No new changes since last sync (HEAD: ${devBranchHeadSha})`);
142
147
  }
@@ -102,7 +102,7 @@ export const splitIssueIntoPRs = async (options, config) => {
102
102
  }
103
103
  const contextInfo = formatContextForPrompt(context.issue, context.product, context.existing_branches);
104
104
  const existingPRsInfo = formatExistingPRsForPrompt(context.existing_pull_requests);
105
- const systemPrompt = await createPRSplittingSystemPrompt(config, issueId);
105
+ const systemPrompt = await createPRSplittingSystemPrompt(config, issueId, undefined, verbose);
106
106
  let userPrompt;
107
107
  if (isIncrementalUpdate && feedbacksContext) {
108
108
  if (verbose) {
@@ -2,7 +2,7 @@ import { type EdsgerConfig } from '../../types/index.js';
2
2
  /**
3
3
  * Create the system prompt for PR splitting: Diff analysis and PR planning
4
4
  */
5
- export declare function createPRSplittingSystemPrompt(config: EdsgerConfig, issueId: string, projectDir?: string): Promise<string>;
5
+ export declare function createPRSplittingSystemPrompt(config: EdsgerConfig, issueId: string, projectDir?: string, verbose?: boolean): Promise<string>;
6
6
  /**
7
7
  * Create the user prompt with diff context for PR splitting
8
8
  */
@@ -1,9 +1,10 @@
1
+ import { getDefaultBranchForIssue } from '../../services/repo-config.js';
1
2
  import { resolveSkill, substituteVariables, } from '../../services/skill-resolver.js';
2
3
  import { OUTPUT_CONTRACTS } from '../output-contracts.js';
3
4
  /**
4
5
  * Create the system prompt for PR splitting: Diff analysis and PR planning
5
6
  */
6
- export async function createPRSplittingSystemPrompt(config, issueId, projectDir) {
7
+ export async function createPRSplittingSystemPrompt(config, issueId, projectDir, verbose) {
7
8
  const skill = await resolveSkill('phase/pr-splitting', {
8
9
  projectDir,
9
10
  });
@@ -12,7 +13,7 @@ export async function createPRSplittingSystemPrompt(config, issueId, projectDir)
12
13
  }
13
14
  let { prompt } = skill;
14
15
  prompt = substituteVariables(prompt, {
15
- BASE_BRANCH: 'main',
16
+ BASE_BRANCH: await getDefaultBranchForIssue(issueId, verbose),
16
17
  ISSUE_ID: issueId,
17
18
  });
18
19
  return `${prompt}\n\n${OUTPUT_CONTRACTS['pr-splitting']}`;
@@ -5,6 +5,7 @@
5
5
  import { Octokit } from '@octokit/rest';
6
6
  import { execFileSync, execSync } from 'child_process';
7
7
  import { buildCredentialArgs, gitPush } from '../../utils/git-push.js';
8
+ import { resolveDefaultBranch } from '../../utils/github-repo-info.js';
8
9
  import { logDebug } from '../../utils/logger.js';
9
10
  // GitHub PR title best practice: keep under 72 characters
10
11
  const MAX_PR_TITLE_LENGTH = 72;
@@ -108,18 +109,20 @@ const switchToBranch = (branch, verbose, githubToken) => {
108
109
  }
109
110
  };
110
111
  /**
111
- * Switch to main branch
112
+ * Switch to the repo's default branch (e.g. main, master). Caller passes the
113
+ * resolved branch name; falls back to local origin/HEAD if undefined.
112
114
  */
113
- const switchToMainBranch = (mainBranch = 'main', verbose) => {
115
+ const switchToMainBranch = (mainBranch, verbose) => {
116
+ const target = mainBranch ?? resolveDefaultBranch();
114
117
  try {
115
- logDebug(`Switching to ${mainBranch} branch...`, verbose);
118
+ logDebug(`Switching to ${target} branch...`, verbose);
116
119
  // Discard any uncommitted changes before switching
117
120
  discardUncommittedChanges(verbose);
118
- execSync(`git checkout ${mainBranch}`, { encoding: 'utf-8' });
119
- logDebug(`Switched to ${mainBranch} branch`, verbose);
121
+ execFileSync('git', ['checkout', target], { encoding: 'utf-8' });
122
+ logDebug(`Switched to ${target} branch`, verbose);
120
123
  }
121
124
  catch (error) {
122
- throw new Error(`Failed to switch to ${mainBranch} branch: ${error}`);
125
+ throw new Error(`Failed to switch to ${target} branch: ${error}`);
123
126
  }
124
127
  };
125
128
  /**
@@ -189,7 +192,7 @@ const generatePRBody = (issue) => {
189
192
  * Create a pull request for the issue
190
193
  */
191
194
  export async function createPullRequest(config, issue) {
192
- const { githubToken, owner, repo, baseBranch = 'main', verbose } = config;
195
+ const { githubToken, owner, repo, baseBranch = resolveDefaultBranch(), verbose, } = config;
193
196
  try {
194
197
  // Initialize Octokit with personal access token
195
198
  const octokit = new Octokit({
@@ -4,6 +4,7 @@
4
4
  import { getGitHubConfig } from '../../api/github.js';
5
5
  import { getIssue } from '../../api/issues/index.js';
6
6
  import { getCurrentBranch, updateBranch, } from '../../services/branches.js';
7
+ import { getDefaultBranchForIssue } from '../../services/repo-config.js';
7
8
  import { logDebug } from '../../utils/logger.js';
8
9
  import { createPullRequest } from './creator.js';
9
10
  /**
@@ -45,11 +46,12 @@ export async function handlePullRequestCreation({ issueId, results, testingResul
45
46
  }
46
47
  const { token: githubToken, owner: githubOwner, repo: githubRepo, } = githubConfig;
47
48
  logDebug(`Using GitHub config from product developer: ${githubOwner}/${githubRepo}`, verbose);
49
+ const baseBranch = await getDefaultBranchForIssue(issueId, verbose);
48
50
  const prResult = await createPullRequest({
49
51
  githubToken,
50
52
  owner: githubOwner,
51
53
  repo: githubRepo,
52
- baseBranch: 'main',
54
+ baseBranch,
53
55
  verbose,
54
56
  }, {
55
57
  id: issue.id,
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Per-issue repo configuration helpers.
3
+ *
4
+ * Right now this just exposes the resolved default branch for a given issue,
5
+ * combining the GitHub API answer (most authoritative) with local-git and
6
+ * literal-`main` fallbacks. Phases that previously hardcoded `'main'` should
7
+ * call `getDefaultBranchForIssue(issueId)` instead so forks whose default
8
+ * branch is `master` / `develop` / `trunk` work correctly.
9
+ */
10
+ /**
11
+ * Resolve the default branch for the repository attached to this issue.
12
+ * Order of precedence: GitHub API `default_branch` → local symbolic-ref → 'main'.
13
+ *
14
+ * Cached per `issueId` for the lifetime of the process. Never throws; on any
15
+ * error it falls back to local resolution.
16
+ */
17
+ export declare function getDefaultBranchForIssue(issueId: string, verbose?: boolean): Promise<string>;
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Per-issue repo configuration helpers.
3
+ *
4
+ * Right now this just exposes the resolved default branch for a given issue,
5
+ * combining the GitHub API answer (most authoritative) with local-git and
6
+ * literal-`main` fallbacks. Phases that previously hardcoded `'main'` should
7
+ * call `getDefaultBranchForIssue(issueId)` instead so forks whose default
8
+ * branch is `master` / `develop` / `trunk` work correctly.
9
+ */
10
+ import { getGitHubConfig } from '../api/github.js';
11
+ import { getRepoForkInfo, resolveDefaultBranch, } from '../utils/github-repo-info.js';
12
+ // Memoized resolutions keyed by issueId. A worker process handles one issue
13
+ // per invocation, but a phase can call getDefaultBranchForIssue from several
14
+ // places (entry, prompt builders, helper functions) — caching avoids
15
+ // re-hitting the GitHub API for each one.
16
+ const defaultBranchCache = new Map();
17
+ /**
18
+ * Resolve the default branch for the repository attached to this issue.
19
+ * Order of precedence: GitHub API `default_branch` → local symbolic-ref → 'main'.
20
+ *
21
+ * Cached per `issueId` for the lifetime of the process. Never throws; on any
22
+ * error it falls back to local resolution.
23
+ */
24
+ export async function getDefaultBranchForIssue(issueId, verbose) {
25
+ const cached = defaultBranchCache.get(issueId);
26
+ if (cached) {
27
+ return cached;
28
+ }
29
+ const promise = resolveOnce(issueId, verbose).catch((error) => {
30
+ // If the lookup itself blows up unexpectedly, drop the cached failure so
31
+ // a later call can retry; then fall back locally.
32
+ defaultBranchCache.delete(issueId);
33
+ throw error;
34
+ });
35
+ defaultBranchCache.set(issueId, promise);
36
+ return promise;
37
+ }
38
+ async function resolveOnce(issueId, verbose) {
39
+ try {
40
+ const config = await getGitHubConfig(issueId, verbose);
41
+ if (config.token && config.owner && config.repo) {
42
+ const info = await getRepoForkInfo(config.token, config.owner, config.repo);
43
+ return resolveDefaultBranch(info.defaultBranch);
44
+ }
45
+ }
46
+ catch {
47
+ // Fall through to local resolution
48
+ }
49
+ return resolveDefaultBranch();
50
+ }
@@ -7,10 +7,11 @@ import { Octokit } from '@octokit/rest';
7
7
  import { execFileSync, execSync } from 'child_process';
8
8
  import { abortRebase, branchExists, getCurrentBranch, hasConflicts, hasUncommittedChanges, isRebaseInProgress, pullLatestFromBranch, resetUncommittedChanges, returnToMainBranch, switchToBranch, } from './git-branch-manager.js';
9
9
  import { buildCredentialArgs, gitForcePush } from './git-push.js';
10
+ import { resolveDefaultBranch } from './github-repo-info.js';
10
11
  import { logError, logInfo } from './logger.js';
11
12
  // eslint-disable-next-line complexity -- git sync with fetch, merge/rebase, and conflict handling
12
13
  export async function syncFeatBranchWithMain(opts) {
13
- const { featBranch, githubToken, owner, repo, baseBranch = 'main', verbose, } = opts;
14
+ const { featBranch, githubToken, owner, repo, baseBranch = resolveDefaultBranch(), verbose, } = opts;
14
15
  try {
15
16
  const octokit = new Octokit({ auth: githubToken });
16
17
  // Check if feat branch exists
@@ -108,7 +109,7 @@ export async function syncFeatBranchWithMain(opts) {
108
109
  */
109
110
  // eslint-disable-next-line complexity -- async git branch management with rebase, conflict resolution, and recovery
110
111
  export async function switchToIssueBranchAndRebaseAsync(options) {
111
- const { issueBranch, baseBranch = 'main', rebaseTargetBranch, originalBaseBranch, verbose, resolveConflicts = false, conflictResolverConfig, forcePushAfterRebase = false, baseBranchCompleted = false, githubToken, } = options;
112
+ const { issueBranch, baseBranch = resolveDefaultBranch(), rebaseTargetBranch, originalBaseBranch, verbose, resolveConflicts = false, conflictResolverConfig, forcePushAfterRebase = false, baseBranchCompleted = false, githubToken, } = options;
112
113
  // Determine the actual target branch for rebase
113
114
  // If rebaseTargetBranch is set (e.g., main), use it; otherwise use baseBranch
114
115
  const actualRebaseTarget = rebaseTargetBranch || baseBranch;
@@ -375,9 +376,12 @@ export async function switchToIssueBranchAndRebaseAsync(options) {
375
376
  */
376
377
  export async function prepareCustomBranchGitEnvironmentAsync(options) {
377
378
  const { verbose } = options;
378
- // Create cleanup function BEFORE any operations that might fail
379
+ const resolvedDefault = resolveDefaultBranch();
380
+ // Create cleanup function BEFORE any operations that might fail.
381
+ // Always return to the repo's default branch (main, master, …), not the
382
+ // baseBranch — baseBranch may be an intermediate branch in a chained PR.
379
383
  const cleanup = () => {
380
- returnToMainBranch('main', verbose);
384
+ returnToMainBranch(resolvedDefault, verbose);
381
385
  };
382
386
  try {
383
387
  const result = await switchToIssueBranchAndRebaseAsync(options);
@@ -409,7 +413,7 @@ export async function prepareCustomBranchGitEnvironmentAsync(options) {
409
413
  * @param conflictResolverConfig - Configuration for the conflict resolver
410
414
  * @returns Cleanup function and conflict resolution info
411
415
  */
412
- export async function preparePhaseGitEnvironmentAsync(issueId, baseBranch = 'main', verbose, resolveConflicts, conflictResolverConfig) {
416
+ export async function preparePhaseGitEnvironmentAsync(issueId, baseBranch, verbose, resolveConflicts, conflictResolverConfig) {
413
417
  const issueBranch = `dev/${issueId}`;
414
418
  return prepareCustomBranchGitEnvironmentAsync({
415
419
  issueBranch,
@@ -6,6 +6,7 @@ import { execFileSync, execSync } from 'child_process';
6
6
  import * as fs from 'fs';
7
7
  import * as path from 'path';
8
8
  import { buildCredentialArgs } from './git-push.js';
9
+ import { resolveDefaultBranch } from './github-repo-info.js';
9
10
  import { logError, logInfo } from './logger.js';
10
11
  /**
11
12
  * Get current Git branch name
@@ -359,7 +360,9 @@ export function pullLatestFromBranch(branch, verbose, githubToken) {
359
360
  * @returns Object containing the previous branch name
360
361
  */
361
362
  // eslint-disable-next-line complexity -- git branch management with rebase, conflict handling, and fallback strategies
362
- export function switchToIssueBranchAndRebase(issueBranch, baseBranch = 'main', verbose, githubToken) {
363
+ export function switchToIssueBranchAndRebase(issueBranch, baseBranch, verbose, githubToken) {
364
+ // eslint-disable-next-line no-param-reassign -- assigning default to optional param
365
+ baseBranch ??= resolveDefaultBranch();
363
366
  const previousBranch = getCurrentBranch();
364
367
  if (verbose) {
365
368
  logInfo(`\nšŸ”„ Preparing issue branch: ${issueBranch}`);
@@ -502,7 +505,9 @@ export function switchToIssueBranchAndRebase(issueBranch, baseBranch = 'main', v
502
505
  * Return to main branch after phase completion
503
506
  * This should be called at the END of each phase
504
507
  */
505
- export function returnToMainBranch(baseBranch = 'main', verbose) {
508
+ export function returnToMainBranch(baseBranch, verbose) {
509
+ // eslint-disable-next-line no-param-reassign -- assigning default to optional param
510
+ baseBranch ??= resolveDefaultBranch();
506
511
  const currentBranch = getCurrentBranch();
507
512
  if (currentBranch === baseBranch) {
508
513
  if (verbose) {
@@ -551,7 +556,7 @@ export function returnToMainBranch(baseBranch = 'main', verbose) {
551
556
  * @param verbose - Whether to log verbose output
552
557
  * @returns Cleanup function that will return to main branch
553
558
  */
554
- export function preparePhaseGitEnvironment(issueId, baseBranch = 'main', verbose) {
559
+ export function preparePhaseGitEnvironment(issueId, baseBranch, verbose) {
555
560
  const issueBranch = `dev/${issueId}`;
556
561
  return prepareCustomBranchGitEnvironment(issueBranch, baseBranch, verbose);
557
562
  }
@@ -572,15 +577,20 @@ export function preparePhaseGitEnvironment(issueId, baseBranch = 'main', verbose
572
577
  * @param verbose - Whether to log verbose output
573
578
  * @returns Cleanup function that will return to main branch
574
579
  */
575
- export function prepareCustomBranchGitEnvironment(issueBranch, baseBranch = 'main', verbose) {
580
+ export function prepareCustomBranchGitEnvironment(issueBranch, baseBranch, verbose) {
581
+ // Resolve once so cleanup and rebase agree on the same target branch.
582
+ const resolvedDefault = resolveDefaultBranch();
583
+ const resolvedBase = baseBranch ?? resolvedDefault;
576
584
  // Create cleanup function BEFORE any operations that might fail
577
- // This ensures cleanup can be called even if switchToIssueBranchAndRebase throws
585
+ // This ensures cleanup can be called even if switchToIssueBranchAndRebase throws.
586
+ // We always return to the repo's default branch (e.g. main, master), not
587
+ // baseBranch — baseBranch may be an intermediate branch in a chained PR.
578
588
  const cleanup = () => {
579
- returnToMainBranch('main', verbose); // Always return to main, not the baseBranch
589
+ returnToMainBranch(resolvedDefault, verbose);
580
590
  };
581
591
  try {
582
592
  // Switch to issue branch and rebase with base branch
583
- switchToIssueBranchAndRebase(issueBranch, baseBranch, verbose);
593
+ switchToIssueBranchAndRebase(issueBranch, resolvedBase, verbose);
584
594
  }
585
595
  catch (error) {
586
596
  // If setup fails, try to cleanup before re-throwing
@@ -7,8 +7,20 @@ export interface RepoForkInfo {
7
7
  owner: string;
8
8
  repo: string;
9
9
  };
10
+ defaultBranch?: string;
10
11
  }
11
12
  /**
12
- * Detect if a repository is a fork and get upstream info
13
+ * Detect if a repository is a fork and get upstream + default branch info
13
14
  */
14
15
  export declare function getRepoForkInfo(token: string, owner: string, repo: string): Promise<RepoForkInfo>;
16
+ /**
17
+ * Resolve the repo's default branch with a multi-tier fallback:
18
+ * 1. `apiValue` from GitHub API (most authoritative)
19
+ * 2. local `git symbolic-ref refs/remotes/origin/HEAD`
20
+ * 3. literal `'main'`
21
+ *
22
+ * Use this anywhere we need to diff/merge against the upstream default branch.
23
+ * The hardcoded `origin/main` assumption breaks for repos whose default is
24
+ * `master`, `develop`, etc., or for forks that inherited a non-`main` default.
25
+ */
26
+ export declare function resolveDefaultBranch(apiValue?: string): string;
@@ -2,18 +2,44 @@
2
2
  * GitHub repository information utilities
3
3
  */
4
4
  import { Octokit } from '@octokit/rest';
5
+ import { execFileSync } from 'child_process';
5
6
  /**
6
- * Detect if a repository is a fork and get upstream info
7
+ * Detect if a repository is a fork and get upstream + default branch info
7
8
  */
8
9
  export async function getRepoForkInfo(token, owner, repo) {
9
10
  const octokit = new Octokit({ auth: token });
10
11
  const { data } = await octokit.repos.get({ owner, repo });
12
+ const result = {
13
+ isFork: false,
14
+ defaultBranch: data.default_branch,
15
+ };
11
16
  if (data.fork && data.parent) {
12
17
  const [upstreamOwner, upstreamRepo] = data.parent.full_name.split('/');
13
- return {
14
- isFork: true,
15
- upstream: { owner: upstreamOwner, repo: upstreamRepo },
16
- };
18
+ result.isFork = true;
19
+ result.upstream = { owner: upstreamOwner, repo: upstreamRepo };
20
+ }
21
+ return result;
22
+ }
23
+ /**
24
+ * Resolve the repo's default branch with a multi-tier fallback:
25
+ * 1. `apiValue` from GitHub API (most authoritative)
26
+ * 2. local `git symbolic-ref refs/remotes/origin/HEAD`
27
+ * 3. literal `'main'`
28
+ *
29
+ * Use this anywhere we need to diff/merge against the upstream default branch.
30
+ * The hardcoded `origin/main` assumption breaks for repos whose default is
31
+ * `master`, `develop`, etc., or for forks that inherited a non-`main` default.
32
+ */
33
+ export function resolveDefaultBranch(apiValue) {
34
+ if (apiValue) {
35
+ return apiValue;
36
+ }
37
+ try {
38
+ const ref = execFileSync('git', ['symbolic-ref', '--short', 'refs/remotes/origin/HEAD'], { encoding: 'utf-8', stdio: 'pipe' }).trim();
39
+ const stripped = ref.replace(/^origin\//, '');
40
+ return stripped || 'main';
41
+ }
42
+ catch {
43
+ return 'main';
17
44
  }
18
- return { isFork: false };
19
45
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "edsger",
3
- "version": "0.52.0",
3
+ "version": "0.54.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "edsger": "dist/index.js"