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.
- package/dist/phases/branch-planning/context.d.ts +2 -2
- package/dist/phases/branch-planning/context.js +2 -2
- package/dist/phases/branch-planning/index.js +6 -4
- package/dist/phases/branch-planning/prompts.js +8 -6
- package/dist/phases/code-implementation/branch-pr-creator.d.ts +31 -0
- package/dist/phases/code-implementation/branch-pr-creator.js +187 -0
- package/dist/phases/code-implementation/index.js +72 -25
- package/dist/phases/code-refine/index.js +29 -3
- package/dist/phases/code-review/index.js +29 -3
- package/dist/phases/functional-testing/analyzer.js +29 -3
- package/dist/services/branches.d.ts +87 -0
- package/dist/services/branches.js +230 -0
- package/package.json +1 -1
|
@@ -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
|
|
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:
|
|
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 {
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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
|
|
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
|
|
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
|
-
- \`
|
|
21
|
-
- \`
|
|
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": "
|
|
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": "
|
|
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 = "
|
|
66
|
-
3. Branch 3 (Frontend): depends_on_branch_name = "
|
|
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,
|
|
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
|
|
29
|
+
let currentBranch = null;
|
|
29
30
|
try {
|
|
30
|
-
|
|
31
|
-
if (
|
|
32
|
-
logInfo(`📋 Using feature branch: ${
|
|
33
|
-
logInfo(` Branch name: ${
|
|
34
|
-
logInfo(` Status: ${
|
|
35
|
-
logInfo(` 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 (
|
|
46
|
+
if (currentBranch && currentBranch.status === 'pending') {
|
|
46
47
|
try {
|
|
47
|
-
await
|
|
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 (
|
|
63
|
+
if (currentBranch && currentBranch.base_branch_id) {
|
|
63
64
|
try {
|
|
64
|
-
const allBranches = await
|
|
65
|
+
const allBranches = await getBranches({
|
|
65
66
|
featureId,
|
|
66
67
|
verbose: false,
|
|
67
68
|
});
|
|
68
|
-
const baseBranchInfo = await getBaseBranchInfo(
|
|
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: ${
|
|
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
|
-
//
|
|
95
|
-
|
|
96
|
-
const
|
|
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',
|
|
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${
|
|
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
|
-
//
|
|
123
|
-
const branchName =
|
|
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,
|
|
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 (
|
|
452
|
+
if (currentBranch) {
|
|
409
453
|
try {
|
|
410
|
-
|
|
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
|
-
|
|
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
|
|
85
|
-
const cleanupGit =
|
|
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
|
-
//
|
|
124
|
-
|
|
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
|
-
//
|
|
26
|
-
|
|
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
|
+
}
|