edsger 0.19.5 → 0.19.6

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.
@@ -1,12 +1,12 @@
1
1
  import type { FeatureInfo, UserStory, TestCase } from '../../types/features.js';
2
2
  import { type ProductInfo } from '../../api/products.js';
3
- import { type FeatureBranch } from '../../services/feature-branches.js';
3
+ import { type Branch } from '../../services/branches.js';
4
4
  export interface BranchPlanningContext {
5
5
  feature: FeatureInfo;
6
6
  product: ProductInfo;
7
7
  user_stories: UserStory[];
8
8
  test_cases: TestCase[];
9
- existing_branches: FeatureBranch[];
9
+ existing_branches: Branch[];
10
10
  }
11
11
  /**
12
12
  * Fetch all context information needed for branch planning via MCP endpoints
@@ -1,7 +1,7 @@
1
1
  import { logInfo, logError } from '../../utils/logger.js';
2
2
  import { getFeature, getUserStories, getTestCases, } from '../../api/features/index.js';
3
3
  import { getProduct } from '../../api/products.js';
4
- import { getFeatureBranches, } from '../../services/feature-branches.js';
4
+ import { getBranches } from '../../services/branches.js';
5
5
  /**
6
6
  * Fetch all context information needed for branch planning via MCP endpoints
7
7
  */
@@ -15,7 +15,7 @@ export async function fetchBranchPlanningContext(featureId, verbose) {
15
15
  getFeature(featureId, verbose),
16
16
  getUserStories(featureId, verbose),
17
17
  getTestCases(featureId, verbose),
18
- getFeatureBranches({ featureId, verbose }).catch(() => []),
18
+ getBranches({ featureId, verbose }).catch(() => []),
19
19
  ]);
20
20
  const product = await getProduct(feature.product_id, verbose);
21
21
  if (verbose) {
@@ -3,7 +3,7 @@ import { logInfo, logError } from '../../utils/logger.js';
3
3
  import { fetchBranchPlanningContext } from './context.js';
4
4
  import { createBranchPlanningSystemPrompt, createBranchPlanningPromptWithContext, createImprovementPrompt, formatContextForPrompt, formatExistingBranchesForPrompt, } from './prompts.js';
5
5
  import { buildSuccessResult, buildErrorResult, buildNoChangeResult, validatePlannedBranches, sortBranchesByDependency, } from './outcome.js';
6
- import { createFeatureBranches, clearFeatureBranches, } from '../../services/feature-branches.js';
6
+ import { createBranches, clearBranches, } from '../../services/branches.js';
7
7
  import { logFeaturePhaseEvent } from '../../services/audit-logs.js';
8
8
  import { getFeedbacksForPhase, formatFeedbacksForContext, } from '../../services/feedbacks.js';
9
9
  function userMessage(content) {
@@ -73,7 +73,7 @@ export const planFeatureBranches = async (options, config) => {
73
73
  if (verbose) {
74
74
  logInfo(`Clearing ${context.existing_branches.length} existing branches for full re-plan...`);
75
75
  }
76
- await clearFeatureBranches({ featureId, verbose }, true);
76
+ await clearBranches({ featureId, verbose }, true);
77
77
  }
78
78
  // Format context for prompt
79
79
  const contextInfo = formatContextForPrompt(context.feature, context.product, context.user_stories, context.test_cases);
@@ -125,7 +125,7 @@ export const planFeatureBranches = async (options, config) => {
125
125
  if (verbose) {
126
126
  logInfo(`🔄 Clearing ${context.existing_branches.length} existing branches for incremental update...`);
127
127
  }
128
- await clearFeatureBranches({ featureId, verbose: false }, true);
128
+ await clearBranches({ featureId, verbose: false }, true);
129
129
  }
130
130
  // Create branches in database with proper dependencies
131
131
  // We need to create them one by one to get IDs for base_branch_id
@@ -148,7 +148,9 @@ export const planFeatureBranches = async (options, config) => {
148
148
  base_branch_id: baseBranchId,
149
149
  status: 'pending',
150
150
  };
151
- const [created] = await createFeatureBranches({ featureId, verbose: false }, [branchInput]);
151
+ const [created] = await createBranches({ featureId, verbose: false }, [
152
+ branchInput,
153
+ ]);
152
154
  if (created) {
153
155
  createdBranches.push(created);
154
156
  if (branch.branch_name) {
@@ -17,10 +17,12 @@ Your task is to analyze the technical design and split the feature implementatio
17
17
  ## Branch Naming Convention
18
18
 
19
19
  Branch names should follow this pattern:
20
- - \`feat/{featureId}/1-{short-description}\`
21
- - \`feat/{featureId}/2-{short-description}\`
20
+ - \`dev/{featureId}/1-{short-description}\`
21
+ - \`dev/{featureId}/2-{short-description}\`
22
22
  - etc.
23
23
 
24
+ The \`dev/\` prefix indicates this is a development branch. A PR will be created from \`dev/...\` to \`feat/...\` after implementation.
25
+
24
26
  Feature ID for this feature: ${featureId}
25
27
 
26
28
  ## Output Format
@@ -39,7 +41,7 @@ You MUST respond with a valid JSON object in this exact format:
39
41
  {
40
42
  "name": "Short descriptive name",
41
43
  "description": "Detailed description of what this branch implements",
42
- "branch_name": "feat/${featureId}/1-short-name",
44
+ "branch_name": "dev/${featureId}/1-short-name",
43
45
  "depends_on_branch_name": null,
44
46
  "scope": [
45
47
  "List of specific items to implement"
@@ -58,12 +60,12 @@ You MUST respond with a valid JSON object in this exact format:
58
60
 
59
61
  Use \`depends_on_branch_name\` to specify which branch must be completed before this one can start:
60
62
  - First branch: \`"depends_on_branch_name": null\` (starts from main)
61
- - Subsequent branches: \`"depends_on_branch_name": "feat/${featureId}/1-database"\` (starts from the specified branch)
63
+ - Subsequent branches: \`"depends_on_branch_name": "dev/${featureId}/1-database"\` (starts from the specified branch)
62
64
 
63
65
  Example for a typical feature:
64
66
  1. Branch 1 (Database): depends_on_branch_name = null (starts from main)
65
- 2. Branch 2 (API): depends_on_branch_name = "feat/${featureId}/1-database" (starts from branch 1)
66
- 3. Branch 3 (Frontend): depends_on_branch_name = "feat/${featureId}/2-api" (starts from branch 2)
67
+ 2. Branch 2 (API): depends_on_branch_name = "dev/${featureId}/1-database" (starts from branch 1)
68
+ 3. Branch 3 (Frontend): depends_on_branch_name = "dev/${featureId}/2-api" (starts from branch 2)
67
69
 
68
70
  This allows parallel development - Branch 2 can start before Branch 1 is merged, based on Branch 1's code.
69
71
 
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Branch PR Creator for multi-branch feature development
3
+ * Creates pull requests from dev/ branches to feat/ branches
4
+ */
5
+ export interface BranchPullRequestConfig {
6
+ readonly githubToken: string;
7
+ readonly owner: string;
8
+ readonly repo: string;
9
+ readonly verbose?: boolean;
10
+ }
11
+ export interface BranchPullRequestResult {
12
+ readonly success: boolean;
13
+ readonly pullRequestUrl?: string;
14
+ readonly pullRequestNumber?: number;
15
+ readonly error?: string;
16
+ }
17
+ /**
18
+ * Convert a feat/ branch name to a dev/ branch name
19
+ * e.g., "feat/abc123/1-database" -> "dev/abc123/1-database"
20
+ */
21
+ export declare function featBranchToDevBranch(featBranchName: string): string;
22
+ /**
23
+ * Convert a dev/ branch name to a feat/ branch name
24
+ * e.g., "dev/abc123/1-database" -> "feat/abc123/1-database"
25
+ */
26
+ export declare function devBranchToFeatBranch(devBranchName: string): string;
27
+ /**
28
+ * Create a pull request from a dev/ branch to its corresponding feat/ branch
29
+ * This is used after code implementation in multi-branch features
30
+ */
31
+ export declare function createBranchPullRequest(config: BranchPullRequestConfig, devBranchName: string, featureName: string, branchDescription: string, baseBranch?: string): Promise<BranchPullRequestResult>;
@@ -0,0 +1,187 @@
1
+ /**
2
+ * Branch PR Creator for multi-branch feature development
3
+ * Creates pull requests from dev/ branches to feat/ branches
4
+ */
5
+ import { Octokit } from '@octokit/rest';
6
+ import { execSync } from 'child_process';
7
+ import { logInfo, logError } from '../../utils/logger.js';
8
+ /**
9
+ * Convert a feat/ branch name to a dev/ branch name
10
+ * e.g., "feat/abc123/1-database" -> "dev/abc123/1-database"
11
+ */
12
+ export function featBranchToDevBranch(featBranchName) {
13
+ if (featBranchName.startsWith('feat/')) {
14
+ return 'dev/' + featBranchName.slice(5);
15
+ }
16
+ // If already dev/ or other format, return as-is
17
+ return featBranchName;
18
+ }
19
+ /**
20
+ * Convert a dev/ branch name to a feat/ branch name
21
+ * e.g., "dev/abc123/1-database" -> "feat/abc123/1-database"
22
+ */
23
+ export function devBranchToFeatBranch(devBranchName) {
24
+ if (devBranchName.startsWith('dev/')) {
25
+ return 'feat/' + devBranchName.slice(4);
26
+ }
27
+ // If already feat/ or other format, return as-is
28
+ return devBranchName;
29
+ }
30
+ /**
31
+ * Push a branch to remote
32
+ */
33
+ async function pushBranch(branchName, verbose) {
34
+ try {
35
+ if (verbose) {
36
+ logInfo(`📤 Pushing branch ${branchName} to remote...`);
37
+ }
38
+ // Try to push with -u flag (sets upstream if not already set)
39
+ try {
40
+ execSync(`git push -u origin ${branchName}`, {
41
+ encoding: 'utf-8',
42
+ stdio: verbose ? 'inherit' : 'pipe',
43
+ });
44
+ return { success: true };
45
+ }
46
+ catch (error) {
47
+ // If push fails, try without -u flag
48
+ try {
49
+ execSync(`git push origin ${branchName}`, {
50
+ encoding: 'utf-8',
51
+ stdio: verbose ? 'inherit' : 'pipe',
52
+ });
53
+ return { success: true };
54
+ }
55
+ catch (retryError) {
56
+ const errorMessage = retryError instanceof Error ? retryError.message : String(retryError);
57
+ return { success: false, error: errorMessage };
58
+ }
59
+ }
60
+ }
61
+ catch (error) {
62
+ const errorMessage = error instanceof Error ? error.message : String(error);
63
+ return { success: false, error: errorMessage };
64
+ }
65
+ }
66
+ /**
67
+ * Create a pull request from a dev/ branch to its corresponding feat/ branch
68
+ * This is used after code implementation in multi-branch features
69
+ */
70
+ export async function createBranchPullRequest(config, devBranchName, featureName, branchDescription, baseBranch = 'main') {
71
+ const { githubToken, owner, repo, verbose } = config;
72
+ if (!devBranchName.startsWith('dev/')) {
73
+ return {
74
+ success: false,
75
+ error: `Expected dev/ branch name, got: ${devBranchName}`,
76
+ };
77
+ }
78
+ const featBranchName = devBranchToFeatBranch(devBranchName);
79
+ try {
80
+ const octokit = new Octokit({ auth: githubToken });
81
+ // Push the dev branch to remote first
82
+ if (verbose) {
83
+ logInfo(`📤 Pushing ${devBranchName} to remote...`);
84
+ }
85
+ const pushResult = await pushBranch(devBranchName, verbose);
86
+ if (!pushResult.success) {
87
+ return {
88
+ success: false,
89
+ error: `Failed to push dev branch: ${pushResult.error}`,
90
+ };
91
+ }
92
+ // Create the feat/ branch from base branch (main) if it doesn't exist
93
+ if (verbose) {
94
+ logInfo(`📝 Ensuring target branch ${featBranchName} exists...`);
95
+ }
96
+ try {
97
+ // Get the base branch (main) SHA
98
+ const { data: baseBranchData } = await octokit.repos.getBranch({
99
+ owner,
100
+ repo,
101
+ branch: baseBranch,
102
+ });
103
+ // Create the feat/ branch from main
104
+ await octokit.git.createRef({
105
+ owner,
106
+ repo,
107
+ ref: `refs/heads/${featBranchName}`,
108
+ sha: baseBranchData.commit.sha,
109
+ });
110
+ if (verbose) {
111
+ logInfo(`✅ Created target branch ${featBranchName} from ${baseBranch}`);
112
+ }
113
+ }
114
+ catch (error) {
115
+ // If branch already exists, that's fine
116
+ if (!error.message?.includes('Reference already exists')) {
117
+ throw error;
118
+ }
119
+ if (verbose) {
120
+ logInfo(`ℹ️ Target branch ${featBranchName} already exists`);
121
+ }
122
+ }
123
+ // Check if PR already exists
124
+ if (verbose) {
125
+ logInfo(`🔍 Checking for existing PR from ${devBranchName} to ${featBranchName}...`);
126
+ }
127
+ const { data: existingPRs } = await octokit.pulls.list({
128
+ owner,
129
+ repo,
130
+ head: `${owner}:${devBranchName}`,
131
+ base: featBranchName,
132
+ state: 'open',
133
+ });
134
+ if (existingPRs.length > 0) {
135
+ const existingPR = existingPRs[0];
136
+ if (verbose) {
137
+ logInfo(`ℹ️ Found existing PR #${existingPR.number}: ${existingPR.html_url}`);
138
+ }
139
+ return {
140
+ success: true,
141
+ pullRequestUrl: existingPR.html_url,
142
+ pullRequestNumber: existingPR.number,
143
+ };
144
+ }
145
+ // Generate PR title and body
146
+ const title = `feat: ${featureName}`;
147
+ const body = `## Branch Implementation
148
+
149
+ ${branchDescription}
150
+
151
+ ---
152
+ **Branch**: \`${devBranchName}\` → \`${featBranchName}\`
153
+ **Created by**: Automated Workflow
154
+ `;
155
+ if (verbose) {
156
+ logInfo(`📝 Creating PR from ${devBranchName} to ${featBranchName}...`);
157
+ }
158
+ // Create the pull request
159
+ const { data: newPR } = await octokit.pulls.create({
160
+ owner,
161
+ repo,
162
+ title,
163
+ body,
164
+ head: devBranchName,
165
+ base: featBranchName,
166
+ draft: false,
167
+ });
168
+ if (verbose) {
169
+ logInfo(`✅ Pull request created: ${newPR.html_url}`);
170
+ }
171
+ return {
172
+ success: true,
173
+ pullRequestUrl: newPR.html_url,
174
+ pullRequestNumber: newPR.number,
175
+ };
176
+ }
177
+ catch (error) {
178
+ const errorMessage = error instanceof Error ? error.message : String(error);
179
+ if (verbose) {
180
+ logError(`❌ Failed to create pull request: ${errorMessage}`);
181
+ }
182
+ return {
183
+ success: false,
184
+ error: errorMessage,
185
+ };
186
+ }
187
+ }
@@ -7,7 +7,8 @@ import { logFeaturePhaseEvent } from '../../services/audit-logs.js';
7
7
  import { buildImplementationResult, buildVerificationFailureResult, buildNoResultsError, } from './outcome.js';
8
8
  import { performVerificationCycle } from '../code-implementation-verification/index.js';
9
9
  import { prepareCustomBranchGitEnvironment, } from '../../utils/git-branch-manager.js';
10
- import { getCurrentBranch, updateFeatureBranch, getFeatureBranches, getBaseBranchInfo, } from '../../services/feature-branches.js';
10
+ import { getCurrentBranch, updateBranch, getBranches, getBaseBranchInfo, } from '../../services/branches.js';
11
+ import { createBranchPullRequest, } from './branch-pr-creator.js';
11
12
  function userMessage(content) {
12
13
  return {
13
14
  type: 'user',
@@ -25,14 +26,14 @@ export const implementFeatureCode = async (options, config, checklistContext) =>
25
26
  logInfo(`Base branch: ${baseBranch}`);
26
27
  }
27
28
  // Check for feature branches - use current branch if available
28
- let currentFeatureBranch = null;
29
+ let currentBranch = null;
29
30
  try {
30
- currentFeatureBranch = await getCurrentBranch({ featureId, verbose });
31
- if (currentFeatureBranch && verbose) {
32
- logInfo(`📋 Using feature branch: ${currentFeatureBranch.name}`);
33
- logInfo(` Branch name: ${currentFeatureBranch.branch_name || 'To be created'}`);
34
- logInfo(` Status: ${currentFeatureBranch.status}`);
35
- logInfo(` Description: ${currentFeatureBranch.description || 'No description'}`);
31
+ currentBranch = await getCurrentBranch({ featureId, verbose });
32
+ if (currentBranch && verbose) {
33
+ logInfo(`📋 Using feature branch: ${currentBranch.name}`);
34
+ logInfo(` Branch name: ${currentBranch.branch_name || 'To be created'}`);
35
+ logInfo(` Status: ${currentBranch.status}`);
36
+ logInfo(` Description: ${currentBranch.description || 'No description'}`);
36
37
  }
37
38
  }
38
39
  catch (error) {
@@ -42,9 +43,9 @@ export const implementFeatureCode = async (options, config, checklistContext) =>
42
43
  }
43
44
  }
44
45
  // Update feature branch status to in_progress if we have one
45
- if (currentFeatureBranch && currentFeatureBranch.status === 'pending') {
46
+ if (currentBranch && currentBranch.status === 'pending') {
46
47
  try {
47
- await updateFeatureBranch(currentFeatureBranch.id, { status: 'in_progress' }, verbose);
48
+ await updateBranch(currentBranch.id, { status: 'in_progress' }, verbose);
48
49
  if (verbose) {
49
50
  logInfo(`✅ Updated feature branch status to in_progress`);
50
51
  }
@@ -59,18 +60,18 @@ export const implementFeatureCode = async (options, config, checklistContext) =>
59
60
  // If the current feature branch depends on another branch, use that as the base
60
61
  let actualBaseBranch = baseBranch;
61
62
  let needsRebase = false;
62
- if (currentFeatureBranch && currentFeatureBranch.base_branch_id) {
63
+ if (currentBranch && currentBranch.base_branch_id) {
63
64
  try {
64
- const allBranches = await getFeatureBranches({
65
+ const allBranches = await getBranches({
65
66
  featureId,
66
67
  verbose: false,
67
68
  });
68
- const baseBranchInfo = await getBaseBranchInfo(currentFeatureBranch, allBranches, baseBranch);
69
+ const baseBranchInfo = await getBaseBranchInfo(currentBranch, allBranches, baseBranch);
69
70
  actualBaseBranch = baseBranchInfo.baseBranch;
70
71
  needsRebase = baseBranchInfo.needsRebase;
71
72
  if (verbose) {
72
73
  logInfo(`🔗 Branch chaining detected:`);
73
- logInfo(` Current branch: ${currentFeatureBranch.name}`);
74
+ logInfo(` Current branch: ${currentBranch.name}`);
74
75
  logInfo(` Base branch: ${actualBaseBranch}`);
75
76
  if (baseBranchInfo.baseBranchMerged) {
76
77
  logInfo(` Parent branch merged: yes (using main)`);
@@ -91,9 +92,13 @@ export const implementFeatureCode = async (options, config, checklistContext) =>
91
92
  }
92
93
  }
93
94
  // Prepare git environment: switch to feature branch and rebase with base
94
- // Use custom branch environment if we have a specific feature branch name
95
- const featureBranchName = currentFeatureBranch?.branch_name || `dev/${featureId}`;
96
- const cleanupGit = prepareCustomBranchGitEnvironment(featureBranchName, actualBaseBranch, verbose);
95
+ // Branch names are stored as dev/... (development branch)
96
+ // PR will be created from dev/... to feat/... after implementation
97
+ const devBranchName = currentBranch?.branch_name || `dev/${featureId}`;
98
+ if (verbose && currentBranch) {
99
+ logInfo(`🔄 Using dev branch for development: ${devBranchName}`);
100
+ }
101
+ const cleanupGit = prepareCustomBranchGitEnvironment(devBranchName, actualBaseBranch, verbose);
97
102
  try {
98
103
  // Fetch all required context information via MCP endpoints
99
104
  if (verbose) {
@@ -104,12 +109,12 @@ export const implementFeatureCode = async (options, config, checklistContext) =>
104
109
  // For multi-branch features, filter by the current branch to get branch-specific feedbacks
105
110
  let feedbacksInfo;
106
111
  try {
107
- const feedbacksContext = await getFeedbacksForPhase({ featureId, verbose }, 'code_implementation', currentFeatureBranch?.id // Pass branch_id if we have a current branch
112
+ const feedbacksContext = await getFeedbacksForPhase({ featureId, verbose }, 'code_implementation', currentBranch?.id // Pass branch_id if we have a current branch
108
113
  );
109
114
  if (feedbacksContext.feedbacks.length > 0) {
110
115
  feedbacksInfo = await formatFeedbacksForContext(feedbacksContext);
111
116
  if (verbose) {
112
- logInfo(`Added ${feedbacksContext.feedbacks.length} human feedbacks to implementation context${currentFeatureBranch ? ` (including branch-specific for "${currentFeatureBranch.name}")` : ''}`);
117
+ logInfo(`Added ${feedbacksContext.feedbacks.length} human feedbacks to implementation context${currentBranch ? ` (including branch-specific for "${currentBranch.name}")` : ''}`);
113
118
  }
114
119
  }
115
120
  }
@@ -119,12 +124,12 @@ export const implementFeatureCode = async (options, config, checklistContext) =>
119
124
  logInfo(`Note: Could not fetch feedbacks (${error instanceof Error ? error.message : String(error)})`);
120
125
  }
121
126
  }
122
- // Determine branch name: use feature branch if available, otherwise default
123
- const branchName = currentFeatureBranch?.branch_name || `dev/${featureId}`;
127
+ // Use the dev branch name for development (already computed above)
128
+ const branchName = devBranchName;
124
129
  const systemPrompt = createSystemPrompt(config, actualBaseBranch, // Use computed base branch for branch chaining
125
130
  featureId, branchName);
126
131
  const initialImplementationPrompt = createImplementationPromptWithContext(featureId, context, actualBaseBranch, // Use computed base branch for branch chaining
127
- checklistContext, verbose, feedbacksInfo, currentFeatureBranch);
132
+ checklistContext, verbose, feedbacksInfo, currentBranch);
128
133
  const maxIterations = options.maxVerificationIterations || 10;
129
134
  let currentIteration = 0;
130
135
  let currentPrompt = initialImplementationPrompt;
@@ -404,12 +409,54 @@ export const implementFeatureCode = async (options, config, checklistContext) =>
404
409
  logError(` Code committed for manual review`);
405
410
  return buildVerificationFailureResult(featureId, branch_name || branchName, summary || 'Implementation completed with verification failures', files_modified || [], commit_hash || '', verificationResult, currentIteration);
406
411
  }
412
+ // Create pull request from dev/ branch to feat/ branch if this is a multi-branch feature
413
+ let pullRequestUrl = null;
414
+ let pullRequestNumber = null;
415
+ if (currentBranch && devBranchName.startsWith('dev/')) {
416
+ // Get GitHub configuration from environment
417
+ const githubToken = process.env.GITHUB_TOKEN || process.env.EDSGER_GITHUB_TOKEN;
418
+ const repoUrl = process.env.GITHUB_REPOSITORY || '';
419
+ const [owner, repo] = repoUrl.split('/').slice(-2);
420
+ if (githubToken && owner && repo) {
421
+ const prConfig = {
422
+ githubToken,
423
+ owner,
424
+ repo,
425
+ verbose,
426
+ };
427
+ // Derive feat/ branch name from dev/ branch name for PR target
428
+ const { devBranchToFeatBranch } = await import('./branch-pr-creator.js');
429
+ const featBranchName = devBranchToFeatBranch(devBranchName);
430
+ if (verbose) {
431
+ logInfo(`📝 Creating pull request from ${devBranchName} to ${featBranchName}...`);
432
+ }
433
+ const prResult = await createBranchPullRequest(prConfig, devBranchName, currentBranch.name, currentBranch.description || 'Feature branch implementation', baseBranch);
434
+ if (prResult.success) {
435
+ pullRequestUrl = prResult.pullRequestUrl || null;
436
+ pullRequestNumber = prResult.pullRequestNumber || null;
437
+ if (verbose) {
438
+ logInfo(`✅ Pull request created: ${pullRequestUrl}`);
439
+ }
440
+ }
441
+ else if (verbose) {
442
+ logError(`⚠️ Failed to create pull request: ${prResult.error}`);
443
+ logInfo(' Implementation is complete, but PR was not created.');
444
+ }
445
+ }
446
+ else if (verbose) {
447
+ logInfo('⚠️ GitHub configuration not available, skipping PR creation');
448
+ logInfo(' Set GITHUB_TOKEN and GITHUB_REPOSITORY to enable automatic PR creation');
449
+ }
450
+ }
407
451
  // Update feature branch status to ready_for_review if we have one
408
- if (currentFeatureBranch) {
452
+ if (currentBranch) {
409
453
  try {
410
- await updateFeatureBranch(currentFeatureBranch.id, {
454
+ // Note: branch_name is stored as dev/... and used directly by all phases.
455
+ // The feat/ branch name is derived at runtime using devBranchToFeatBranch() for PR targets.
456
+ await updateBranch(currentBranch.id, {
411
457
  status: 'ready_for_review',
412
- branch_name: branch_name || branchName,
458
+ pull_request_url: pullRequestUrl,
459
+ pull_request_number: pullRequestNumber,
413
460
  }, verbose);
414
461
  if (verbose) {
415
462
  logInfo(`✅ Updated feature branch status to ready_for_review`);
@@ -8,8 +8,9 @@ import { execSync } from 'child_process';
8
8
  import { fetchCodeRefineContext, } from './context.js';
9
9
  import { getFeedbacksForPhase, formatFeedbacksForContext, } from '../../services/feedbacks.js';
10
10
  import { createSystemPrompt, createCodeRefinePrompt } from './prompts.js';
11
- import { preparePhaseGitEnvironment, hasUncommittedChanges, getUncommittedFiles, syncFeatBranchWithMain, } from '../../utils/git-branch-manager.js';
11
+ import { preparePhaseGitEnvironment, prepareCustomBranchGitEnvironment, hasUncommittedChanges, getUncommittedFiles, syncFeatBranchWithMain, } from '../../utils/git-branch-manager.js';
12
12
  import { getFeature } from '../../api/features/get-feature.js';
13
+ import { getReadyForReviewBranch } from '../../services/branches.js';
13
14
  import { parsePullRequestUrl } from './context.js';
14
15
  function userMessage(content) {
15
16
  return {
@@ -64,6 +65,29 @@ export const refineCodeFromPRFeedback = async (options, config) => {
64
65
  if (verbose) {
65
66
  logInfo(`Starting code refine for feature ID: ${featureId}`);
66
67
  }
68
+ // For multi-branch features, find the branch that is ready for review
69
+ // and use its branch_name for refine (branch_name is stored as dev/...)
70
+ let branchName = `dev/${featureId}`; // Default for single-branch features
71
+ let currentBranch = null;
72
+ try {
73
+ currentBranch = await getReadyForReviewBranch({ featureId, verbose });
74
+ if (currentBranch && currentBranch.branch_name) {
75
+ // Use branch_name directly (already stored as dev/...)
76
+ branchName = currentBranch.branch_name;
77
+ if (verbose) {
78
+ logInfo(`📋 Found ready_for_review branch: ${currentBranch.name}`);
79
+ logInfo(` Using dev branch: ${branchName}`);
80
+ }
81
+ }
82
+ else if (verbose) {
83
+ logInfo(`ℹ️ No ready_for_review branch found, using default: ${branchName}`);
84
+ }
85
+ }
86
+ catch (error) {
87
+ if (verbose) {
88
+ logInfo(`Note: Could not fetch feature branches, using default branch`);
89
+ }
90
+ }
67
91
  // Sync feat branch with main before preparing git environment
68
92
  // This prevents extra commits from appearing in PR when dev branch is rebased
69
93
  try {
@@ -81,8 +105,10 @@ export const refineCodeFromPRFeedback = async (options, config) => {
81
105
  }
82
106
  // Continue even if sync fails - it's not critical
83
107
  }
84
- // Prepare git environment: switch to feature branch and rebase with main
85
- const cleanupGit = preparePhaseGitEnvironment(featureId, 'main', verbose);
108
+ // Prepare git environment: switch to the appropriate branch
109
+ const cleanupGit = currentBranch
110
+ ? prepareCustomBranchGitEnvironment(branchName, 'main', verbose)
111
+ : preparePhaseGitEnvironment(featureId, 'main', verbose);
86
112
  try {
87
113
  // Fetch code refine context (PR reviews and comments)
88
114
  if (verbose) {
@@ -7,7 +7,8 @@ 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 } from '../../utils/git-branch-manager.js';
10
+ import { preparePhaseGitEnvironment, prepareCustomBranchGitEnvironment, } from '../../utils/git-branch-manager.js';
11
+ import { getReadyForReviewBranch } from '../../services/branches.js';
11
12
  function userMessage(content) {
12
13
  return {
13
14
  type: 'user',
@@ -120,8 +121,33 @@ export const reviewPullRequest = async (options, config) => {
120
121
  if (verbose) {
121
122
  logInfo(`Starting code review for feature ID: ${featureId}`);
122
123
  }
123
- // Prepare git environment: switch to feature branch and rebase with main
124
- const cleanupGit = preparePhaseGitEnvironment(featureId, 'main', verbose);
124
+ // For multi-branch features, find the branch that is ready for review
125
+ // and use its branch_name for review (branch_name is stored as dev/...)
126
+ let branchName = `dev/${featureId}`; // Default for single-branch features
127
+ let currentBranch = null;
128
+ try {
129
+ currentBranch = await getReadyForReviewBranch({ featureId, verbose });
130
+ if (currentBranch && currentBranch.branch_name) {
131
+ // Use branch_name directly (already stored as dev/...)
132
+ branchName = currentBranch.branch_name;
133
+ if (verbose) {
134
+ logInfo(`📋 Found ready_for_review branch: ${currentBranch.name}`);
135
+ logInfo(` Using dev branch: ${branchName}`);
136
+ }
137
+ }
138
+ else if (verbose) {
139
+ logInfo(`ℹ️ No ready_for_review branch found, using default: ${branchName}`);
140
+ }
141
+ }
142
+ catch (error) {
143
+ if (verbose) {
144
+ logInfo(`Note: Could not fetch feature branches, using default branch`);
145
+ }
146
+ }
147
+ // Prepare git environment: switch to the appropriate branch
148
+ const cleanupGit = currentBranch
149
+ ? prepareCustomBranchGitEnvironment(branchName, 'main', verbose)
150
+ : preparePhaseGitEnvironment(featureId, 'main', verbose);
125
151
  try {
126
152
  // Fetch code review context (PR data, files, commits)
127
153
  if (verbose) {
@@ -6,7 +6,8 @@ import { saveFunctionalTestResultsWithRetry } from './http-fallback.js';
6
6
  import { fetchFunctionalTestingContext, formatContextForPrompt, } from './context-fetcher.js';
7
7
  import { updateFeatureStatus } from '../../api/features/index.js';
8
8
  import { createTestReport, } from './test-report-creator.js';
9
- import { preparePhaseGitEnvironment } from '../../utils/git-branch-manager.js';
9
+ import { preparePhaseGitEnvironment, prepareCustomBranchGitEnvironment, } from '../../utils/git-branch-manager.js';
10
+ import { getReadyForReviewBranch } from '../../services/branches.js';
10
11
  function userMessage(content) {
11
12
  return {
12
13
  type: 'user',
@@ -22,8 +23,33 @@ export const runFunctionalTesting = async (options, config, checklistContext) =>
22
23
  if (verbose) {
23
24
  logInfo(`Starting functional testing for feature ID: ${featureId}`);
24
25
  }
25
- // Prepare git environment: switch to feature branch and rebase with main
26
- const cleanupGit = preparePhaseGitEnvironment(featureId, 'main', verbose);
26
+ // For multi-branch features, find the branch that is ready for review
27
+ // and use its branch_name for testing (branch_name is stored as dev/...)
28
+ let branchName = `dev/${featureId}`; // Default for single-branch features
29
+ let currentBranch = null;
30
+ try {
31
+ currentBranch = await getReadyForReviewBranch({ featureId, verbose });
32
+ if (currentBranch && currentBranch.branch_name) {
33
+ // Use branch_name directly (already stored as dev/...)
34
+ branchName = currentBranch.branch_name;
35
+ if (verbose) {
36
+ logInfo(`📋 Found ready_for_review branch: ${currentBranch.name}`);
37
+ logInfo(` Using dev branch: ${branchName}`);
38
+ }
39
+ }
40
+ else if (verbose) {
41
+ logInfo(`ℹ️ No ready_for_review branch found, using default: ${branchName}`);
42
+ }
43
+ }
44
+ catch (error) {
45
+ if (verbose) {
46
+ logInfo(`Note: Could not fetch feature branches, using default branch`);
47
+ }
48
+ }
49
+ // Prepare git environment: switch to the appropriate branch
50
+ const cleanupGit = currentBranch
51
+ ? prepareCustomBranchGitEnvironment(branchName, 'main', verbose)
52
+ : preparePhaseGitEnvironment(featureId, 'main', verbose);
27
53
  try {
28
54
  // Fetch all required context information via MCP endpoints
29
55
  if (verbose) {
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Branches service for pipeline integration
3
+ * Allows phases to manage branches via MCP
4
+ */
5
+ import { PipelinePhaseOptions } from '../types/pipeline.js';
6
+ export interface Branch {
7
+ id: string;
8
+ feature_id: string;
9
+ name: string;
10
+ description: string | null;
11
+ branch_name: string | null;
12
+ base_branch_id: string | null;
13
+ pull_request_url: string | null;
14
+ pull_request_number: number | null;
15
+ status: 'pending' | 'in_progress' | 'ready_for_review' | 'merged' | 'closed';
16
+ created_at: string;
17
+ updated_at: string;
18
+ }
19
+ export interface CreateBranchInput {
20
+ name: string;
21
+ description?: string;
22
+ branch_name?: string;
23
+ base_branch_id?: string;
24
+ status?: Branch['status'];
25
+ }
26
+ /**
27
+ * List all branches for a feature
28
+ */
29
+ export declare function getBranches(options: PipelinePhaseOptions): Promise<Branch[]>;
30
+ /**
31
+ * Get the current active branch for a feature
32
+ */
33
+ export declare function getCurrentBranch(options: PipelinePhaseOptions): Promise<Branch | null>;
34
+ /**
35
+ * Create branches
36
+ */
37
+ export declare function createBranches(options: PipelinePhaseOptions, branches: CreateBranchInput[]): Promise<Branch[]>;
38
+ /**
39
+ * Update a branch
40
+ */
41
+ export declare function updateBranch(branchId: string, updates: Partial<Omit<Branch, 'id' | 'feature_id' | 'created_at' | 'updated_at'>>, verbose?: boolean): Promise<Branch>;
42
+ /**
43
+ * Clear all branches for a feature (used before re-planning)
44
+ */
45
+ export declare function clearBranches(options: PipelinePhaseOptions, force?: boolean): Promise<number>;
46
+ /**
47
+ * Format branches for context (to include in prompts)
48
+ */
49
+ export declare function formatBranchesForContext(branches: Branch[]): string;
50
+ /**
51
+ * Check if feature has multiple branches planned
52
+ */
53
+ export declare function hasMultipleBranches(options: PipelinePhaseOptions): Promise<boolean>;
54
+ /**
55
+ * Get next pending branch to work on
56
+ */
57
+ export declare function getNextPendingBranch(options: PipelinePhaseOptions): Promise<Branch | null>;
58
+ /**
59
+ * Check if all branches are completed
60
+ */
61
+ export declare function allBranchesCompleted(options: PipelinePhaseOptions): Promise<boolean>;
62
+ /**
63
+ * Get the base branch information for a branch.
64
+ * Returns:
65
+ * - If base_branch_id is set and that branch is merged: use main
66
+ * - If base_branch_id is set and that branch is not merged: use that branch's branch_name
67
+ * - If base_branch_id is null: use main
68
+ */
69
+ export declare function getBaseBranchInfo(branch: Branch, allBranches: Branch[], mainBranch?: string): Promise<{
70
+ baseBranch: string;
71
+ needsRebase: boolean;
72
+ baseBranchMerged: boolean;
73
+ }>;
74
+ /**
75
+ * Get branch by ID
76
+ */
77
+ export declare function getBranchById(branchId: string, verbose?: boolean): Promise<Branch | null>;
78
+ /**
79
+ * Get the branch that is ready for review (for post-implementation phases)
80
+ * Returns the first branch with status 'ready_for_review'
81
+ */
82
+ export declare function getReadyForReviewBranch(options: PipelinePhaseOptions): Promise<Branch | null>;
83
+ /**
84
+ * Get the branch that is in progress (in_progress or ready_for_review)
85
+ * Useful for phases that need to work on the current active branch
86
+ */
87
+ export declare function getActiveBranch(options: PipelinePhaseOptions): Promise<Branch | null>;
@@ -0,0 +1,230 @@
1
+ /**
2
+ * Branches service for pipeline integration
3
+ * Allows phases to manage branches via MCP
4
+ */
5
+ import { callMcpEndpoint } from '../api/mcp-client.js';
6
+ /**
7
+ * List all branches for a feature
8
+ */
9
+ export async function getBranches(options) {
10
+ const { featureId, verbose } = options;
11
+ if (verbose) {
12
+ console.log(`📋 Fetching branches for feature: ${featureId}`);
13
+ }
14
+ const result = (await callMcpEndpoint('branches/list', {
15
+ feature_id: featureId,
16
+ }));
17
+ const branches = result?.branches || [];
18
+ if (verbose) {
19
+ console.log(`✅ Found ${branches.length} branches`);
20
+ }
21
+ return branches;
22
+ }
23
+ /**
24
+ * Get the current active branch for a feature
25
+ */
26
+ export async function getCurrentBranch(options) {
27
+ const { featureId, verbose } = options;
28
+ if (verbose) {
29
+ console.log(`📋 Getting current branch for feature: ${featureId}`);
30
+ }
31
+ const result = (await callMcpEndpoint('branches/current', {
32
+ feature_id: featureId,
33
+ }));
34
+ return result?.current_branch || null;
35
+ }
36
+ /**
37
+ * Create branches
38
+ */
39
+ export async function createBranches(options, branches) {
40
+ const { featureId, verbose } = options;
41
+ if (verbose) {
42
+ console.log(`📋 Creating ${branches.length} branches for feature: ${featureId}`);
43
+ }
44
+ const result = (await callMcpEndpoint('branches/create', {
45
+ feature_id: featureId,
46
+ branches: branches,
47
+ }));
48
+ const createdBranches = result?.created_branches || [];
49
+ if (verbose) {
50
+ console.log(`✅ Created ${createdBranches.length} branches`);
51
+ createdBranches.forEach((b, idx) => {
52
+ console.log(` ${idx + 1}. ${b.name} (status: ${b.status})`);
53
+ });
54
+ }
55
+ return createdBranches;
56
+ }
57
+ /**
58
+ * Update a branch
59
+ */
60
+ export async function updateBranch(branchId, updates, verbose) {
61
+ if (verbose) {
62
+ console.log(`📋 Updating branch: ${branchId}`);
63
+ }
64
+ const result = (await callMcpEndpoint('branches/update', {
65
+ branch_id: branchId,
66
+ ...updates,
67
+ }));
68
+ if (verbose) {
69
+ console.log(`✅ Branch updated successfully`);
70
+ }
71
+ return result?.branch;
72
+ }
73
+ /**
74
+ * Clear all branches for a feature (used before re-planning)
75
+ */
76
+ export async function clearBranches(options, force = false) {
77
+ const { featureId, verbose } = options;
78
+ if (verbose) {
79
+ console.log(`📋 Clearing branches for feature: ${featureId}`);
80
+ }
81
+ const result = (await callMcpEndpoint('branches/clear', {
82
+ feature_id: featureId,
83
+ force: force,
84
+ }));
85
+ const deletedCount = result?.deleted_count || 0;
86
+ if (verbose) {
87
+ console.log(`✅ Cleared ${deletedCount} branches`);
88
+ }
89
+ return deletedCount;
90
+ }
91
+ /**
92
+ * Format branches for context (to include in prompts)
93
+ */
94
+ export function formatBranchesForContext(branches) {
95
+ if (!branches || branches.length === 0) {
96
+ return 'No branches defined yet.';
97
+ }
98
+ const branchList = branches
99
+ .map((b, idx) => {
100
+ const statusEmoji = b.status === 'merged'
101
+ ? '✅'
102
+ : b.status === 'in_progress'
103
+ ? '🔄'
104
+ : b.status === 'ready_for_review'
105
+ ? '👀'
106
+ : b.status === 'closed'
107
+ ? '❌'
108
+ : '⏳';
109
+ return `${idx + 1}. **${b.name}** ${statusEmoji}
110
+ - Status: ${b.status}
111
+ - Branch: ${b.branch_name || 'Not created'}
112
+ - PR: ${b.pull_request_url || 'Not created'}
113
+ - Description: ${b.description || 'No description'}`;
114
+ })
115
+ .join('\n\n');
116
+ return `# Branches
117
+
118
+ ${branchList}
119
+
120
+ Total: ${branches.length} branches
121
+ Merged: ${branches.filter((b) => b.status === 'merged').length}
122
+ In Progress: ${branches.filter((b) => b.status === 'in_progress').length}
123
+ Pending: ${branches.filter((b) => b.status === 'pending').length}`;
124
+ }
125
+ /**
126
+ * Check if feature has multiple branches planned
127
+ */
128
+ export async function hasMultipleBranches(options) {
129
+ const branches = await getBranches(options);
130
+ return branches.length > 1;
131
+ }
132
+ /**
133
+ * Get next pending branch to work on
134
+ */
135
+ export async function getNextPendingBranch(options) {
136
+ const branches = await getBranches(options);
137
+ return branches.find((b) => b.status === 'pending') || null;
138
+ }
139
+ /**
140
+ * Check if all branches are completed
141
+ */
142
+ export async function allBranchesCompleted(options) {
143
+ const branches = await getBranches(options);
144
+ if (branches.length === 0)
145
+ return true;
146
+ return branches.every((b) => b.status === 'merged' || b.status === 'closed');
147
+ }
148
+ /**
149
+ * Get the base branch information for a branch.
150
+ * Returns:
151
+ * - If base_branch_id is set and that branch is merged: use main
152
+ * - If base_branch_id is set and that branch is not merged: use that branch's branch_name
153
+ * - If base_branch_id is null: use main
154
+ */
155
+ export async function getBaseBranchInfo(branch, allBranches, mainBranch = 'main') {
156
+ // No base branch - start from main
157
+ if (!branch.base_branch_id) {
158
+ return {
159
+ baseBranch: mainBranch,
160
+ needsRebase: false,
161
+ baseBranchMerged: true,
162
+ };
163
+ }
164
+ // Find the base branch
165
+ const baseBranch = allBranches.find((b) => b.id === branch.base_branch_id);
166
+ if (!baseBranch) {
167
+ // Base branch not found - fall back to main
168
+ return {
169
+ baseBranch: mainBranch,
170
+ needsRebase: false,
171
+ baseBranchMerged: true,
172
+ };
173
+ }
174
+ // Check if base branch is merged
175
+ if (baseBranch.status === 'merged') {
176
+ // Base branch is merged to main - we should base on main and rebase if needed
177
+ return { baseBranch: mainBranch, needsRebase: true, baseBranchMerged: true };
178
+ }
179
+ // Base branch is not merged - we should base on that branch
180
+ if (!baseBranch.branch_name) {
181
+ // Base branch doesn't have a git branch yet - fall back to main
182
+ return {
183
+ baseBranch: mainBranch,
184
+ needsRebase: false,
185
+ baseBranchMerged: false,
186
+ };
187
+ }
188
+ return {
189
+ baseBranch: baseBranch.branch_name,
190
+ needsRebase: false,
191
+ baseBranchMerged: false,
192
+ };
193
+ }
194
+ /**
195
+ * Get branch by ID
196
+ */
197
+ export async function getBranchById(branchId, verbose) {
198
+ if (verbose) {
199
+ console.log(`📋 Fetching branch: ${branchId}`);
200
+ }
201
+ const result = (await callMcpEndpoint('branches/get', {
202
+ branch_id: branchId,
203
+ }));
204
+ return result?.branch || null;
205
+ }
206
+ /**
207
+ * Get the branch that is ready for review (for post-implementation phases)
208
+ * Returns the first branch with status 'ready_for_review'
209
+ */
210
+ export async function getReadyForReviewBranch(options) {
211
+ const branches = await getBranches(options);
212
+ return branches.find((b) => b.status === 'ready_for_review') || null;
213
+ }
214
+ /**
215
+ * Get the branch that is in progress (in_progress or ready_for_review)
216
+ * Useful for phases that need to work on the current active branch
217
+ */
218
+ export async function getActiveBranch(options) {
219
+ const branches = await getBranches(options);
220
+ // First try to find ready_for_review (after implementation)
221
+ const readyBranch = branches.find((b) => b.status === 'ready_for_review');
222
+ if (readyBranch)
223
+ return readyBranch;
224
+ // Then try in_progress
225
+ const inProgressBranch = branches.find((b) => b.status === 'in_progress');
226
+ if (inProgressBranch)
227
+ return inProgressBranch;
228
+ // Fall back to pending
229
+ return branches.find((b) => b.status === 'pending') || null;
230
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "edsger",
3
- "version": "0.19.5",
3
+ "version": "0.19.6",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "edsger": "dist/index.js"