@bobbyg603/mog 1.0.2 → 1.2.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.
- package/README.md +3 -2
- package/package.json +1 -1
- package/src/github.ts +7 -2
- package/src/index.ts +88 -9
- package/src/sandbox.ts +152 -24
- package/src/worktree.ts +10 -9
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<img
|
|
1
|
+
<img height="320" alt="claude moggin issues" src="https://github.com/user-attachments/assets/72b347d2-b128-47b5-8dcd-e15248350fe0" />
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
# mog — Sandboxed Claude Issue Mogging
|
|
@@ -91,7 +91,8 @@ mog sparx-tech/hub-firmware 45
|
|
|
91
91
|
| Environment Variable | Default | Description |
|
|
92
92
|
|---|---|---|
|
|
93
93
|
| `MOG_REPOS_DIR` | `~/mog-repos` | Where repos are cloned and worktrees created (also the sandbox workspace) |
|
|
94
|
-
| `
|
|
94
|
+
| `MOG_MAX_ITERATIONS` | `10` | Max build loop iterations per issue |
|
|
95
|
+
| `MOG_MAX_CONTINUATIONS` | — | Legacy alias for `MOG_MAX_ITERATIONS` |
|
|
95
96
|
|
|
96
97
|
## Worktree management
|
|
97
98
|
|
package/package.json
CHANGED
package/src/github.ts
CHANGED
|
@@ -34,7 +34,8 @@ export function pushAndCreatePR(
|
|
|
34
34
|
branchName: string,
|
|
35
35
|
defaultBranch: string,
|
|
36
36
|
issueNum: string,
|
|
37
|
-
issue: Issue
|
|
37
|
+
issue: Issue,
|
|
38
|
+
summary?: string
|
|
38
39
|
): void {
|
|
39
40
|
// Check for unpushed commits or uncommitted changes
|
|
40
41
|
const unpushed = Bun.spawnSync(["git", "log", `origin/${defaultBranch}..HEAD`, "--oneline"], { cwd: worktreeDir });
|
|
@@ -75,11 +76,15 @@ export function pushAndCreatePR(
|
|
|
75
76
|
// Create PR
|
|
76
77
|
log.info("Opening pull request...");
|
|
77
78
|
|
|
79
|
+
const summarySection = summary
|
|
80
|
+
? `### What was done\n\n${summary}\n\n`
|
|
81
|
+
: "";
|
|
82
|
+
|
|
78
83
|
const prBody = `## Summary
|
|
79
84
|
|
|
80
85
|
Closes #${issueNum}
|
|
81
86
|
|
|
82
|
-
This PR was generated by [mog](https://github.com/bobbyg603/mog) using Claude Code in a Docker sandbox.
|
|
87
|
+
${summarySection}This PR was generated by [mog](https://github.com/bobbyg603/mog) using Claude Code in a Docker sandbox.
|
|
83
88
|
|
|
84
89
|
### Issue: ${issue.title}
|
|
85
90
|
|
package/src/index.ts
CHANGED
|
@@ -95,14 +95,16 @@ async function main() {
|
|
|
95
95
|
return;
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
-
const repo = args[0];
|
|
99
|
-
const issueNum = args[1];
|
|
98
|
+
const repo = args[0] as string;
|
|
99
|
+
const issueNum = args[1] as string;
|
|
100
100
|
|
|
101
|
-
if (!/^\d+$/.test(issueNum)) {
|
|
101
|
+
if (!repo || !issueNum || !/^\d+$/.test(issueNum)) {
|
|
102
102
|
log.die(`Invalid issue number: '${issueNum}'. Must be a positive integer.`);
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
-
const
|
|
105
|
+
const parts = repo.split("/");
|
|
106
|
+
const owner = parts[0] as string;
|
|
107
|
+
const repoName = parts[1] as string;
|
|
106
108
|
|
|
107
109
|
if (!owner || !repoName) {
|
|
108
110
|
log.die("Invalid repo format. Use: owner/repo");
|
|
@@ -133,8 +135,10 @@ async function main() {
|
|
|
133
135
|
reposDir, owner, repoName, defaultBranch, issueNum, issue.title
|
|
134
136
|
);
|
|
135
137
|
|
|
136
|
-
// Build
|
|
137
|
-
const
|
|
138
|
+
// Build prompts
|
|
139
|
+
const planningPrompt = buildPlanningPrompt(repo, issueNum, issue);
|
|
140
|
+
const buildingPromptFn = (remaining: string[], plan: string) =>
|
|
141
|
+
buildBuildingPrompt(repo, issueNum, issue, remaining, plan);
|
|
138
142
|
|
|
139
143
|
// Run Claude in sandbox
|
|
140
144
|
log.info("Launching Claude Code in sandbox...");
|
|
@@ -142,10 +146,10 @@ async function main() {
|
|
|
142
146
|
log.info(`Worktree: ${worktreeDir}`);
|
|
143
147
|
console.log();
|
|
144
148
|
|
|
145
|
-
await runClaude(SANDBOX_NAME, worktreeDir,
|
|
149
|
+
const summary = await runClaude(SANDBOX_NAME, worktreeDir, planningPrompt, buildingPromptFn);
|
|
146
150
|
|
|
147
151
|
// Push and create PR
|
|
148
|
-
pushAndCreatePR(repo, worktreeDir, branchName, defaultBranch, issueNum, issue);
|
|
152
|
+
pushAndCreatePR(repo, worktreeDir, branchName, defaultBranch, issueNum, issue, summary);
|
|
149
153
|
}
|
|
150
154
|
|
|
151
155
|
function getReposDir(): string {
|
|
@@ -197,7 +201,7 @@ function tryRecoverSandbox(reposDir: string): boolean {
|
|
|
197
201
|
return true;
|
|
198
202
|
}
|
|
199
203
|
|
|
200
|
-
function
|
|
204
|
+
function buildPlanningPrompt(repo: string, issueNum: string, issue: { title: string; body: string; labels: string }): string {
|
|
201
205
|
return `You are working on GitHub issue #${issueNum} for the repository ${repo}.
|
|
202
206
|
|
|
203
207
|
## Issue: ${issue.title}
|
|
@@ -208,6 +212,52 @@ ${issue.body}
|
|
|
208
212
|
### Labels
|
|
209
213
|
${issue.labels}
|
|
210
214
|
|
|
215
|
+
## Instructions
|
|
216
|
+
|
|
217
|
+
Your job in this step is to **plan only** — do NOT implement anything and do NOT commit.
|
|
218
|
+
|
|
219
|
+
1. Read and understand the codebase structure thoroughly.
|
|
220
|
+
2. Analyze the issue and break it down into small, atomic implementation tasks.
|
|
221
|
+
3. Create a file called \`IMPLEMENTATION_PLAN.md\` in the root of the repository with a checklist of tasks.
|
|
222
|
+
|
|
223
|
+
The plan should:
|
|
224
|
+
- Have 3-8 tasks (fewer for simple issues, more for complex ones)
|
|
225
|
+
- Order tasks by dependency (implement foundations first)
|
|
226
|
+
- Each task should be a single, atomic unit of work that results in one commit
|
|
227
|
+
- Use markdown checklist format: \`- [ ] Task description\`
|
|
228
|
+
|
|
229
|
+
Example format:
|
|
230
|
+
\`\`\`markdown
|
|
231
|
+
# Implementation Plan for #${issueNum}
|
|
232
|
+
|
|
233
|
+
- [ ] Add the FooBar interface to src/types.ts
|
|
234
|
+
- [ ] Implement the FooBar service in src/services/foobar.ts
|
|
235
|
+
- [ ] Update the main handler to use FooBar service
|
|
236
|
+
- [ ] Add unit tests for FooBar service
|
|
237
|
+
\`\`\`
|
|
238
|
+
|
|
239
|
+
Do NOT implement any code changes. Do NOT make any commits. Only create the plan file.`;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function buildBuildingPrompt(
|
|
243
|
+
repo: string,
|
|
244
|
+
issueNum: string,
|
|
245
|
+
issue: { title: string; body: string; labels: string },
|
|
246
|
+
remainingItems: string[],
|
|
247
|
+
planContent: string,
|
|
248
|
+
): string {
|
|
249
|
+
// Fallback: no plan — use original single-shot prompt
|
|
250
|
+
if (remainingItems.length === 0 && !planContent) {
|
|
251
|
+
return `You are working on GitHub issue #${issueNum} for the repository ${repo}.
|
|
252
|
+
|
|
253
|
+
## Issue: ${issue.title}
|
|
254
|
+
|
|
255
|
+
### Description
|
|
256
|
+
${issue.body}
|
|
257
|
+
|
|
258
|
+
### Labels
|
|
259
|
+
${issue.labels}
|
|
260
|
+
|
|
211
261
|
## Instructions
|
|
212
262
|
1. Read and understand the codebase structure first.
|
|
213
263
|
2. Implement the changes described in the issue above.
|
|
@@ -218,6 +268,35 @@ ${issue.labels}
|
|
|
218
268
|
|
|
219
269
|
When you are done, make a single commit (or a small, logical set of commits) with
|
|
220
270
|
a message like: "fix: <short description> (#${issueNum})"`;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const currentTask = remainingItems[0]?.replace("- [ ] ", "") || "Complete remaining work";
|
|
274
|
+
|
|
275
|
+
return `You are working on GitHub issue #${issueNum} for the repository ${repo}.
|
|
276
|
+
|
|
277
|
+
## Issue: ${issue.title}
|
|
278
|
+
|
|
279
|
+
### Description
|
|
280
|
+
${issue.body}
|
|
281
|
+
|
|
282
|
+
### Labels
|
|
283
|
+
${issue.labels}
|
|
284
|
+
|
|
285
|
+
## Current Implementation Plan
|
|
286
|
+
|
|
287
|
+
${planContent}
|
|
288
|
+
|
|
289
|
+
## Instructions
|
|
290
|
+
|
|
291
|
+
Implement ONLY the following task:
|
|
292
|
+
**${currentTask}**
|
|
293
|
+
|
|
294
|
+
Rules:
|
|
295
|
+
1. Implement ONLY this one task — do not work on other unchecked items.
|
|
296
|
+
2. Write clean code that follows the existing project conventions.
|
|
297
|
+
3. Update \`IMPLEMENTATION_PLAN.md\` to check off the completed item (change \`- [ ]\` to \`- [x]\`).
|
|
298
|
+
4. Commit ALL changes (including the updated plan file) with a message like: "feat: ${currentTask.toLowerCase()} (#${issueNum})"
|
|
299
|
+
5. Do NOT work on any other tasks after committing.`;
|
|
221
300
|
}
|
|
222
301
|
|
|
223
302
|
main().catch((err) => {
|
package/src/sandbox.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import fs from "fs";
|
|
1
2
|
import { log } from "./log";
|
|
2
3
|
|
|
3
4
|
interface StreamEvent {
|
|
@@ -15,39 +16,159 @@ interface StreamEvent {
|
|
|
15
16
|
is_error?: boolean;
|
|
16
17
|
}
|
|
17
18
|
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
const MAX_ITERATIONS = parseInt(
|
|
20
|
+
process.env.MOG_MAX_ITERATIONS || process.env.MOG_MAX_CONTINUATIONS || "30",
|
|
21
|
+
10,
|
|
22
|
+
);
|
|
23
|
+
const MAX_STALLS = 2;
|
|
24
|
+
const PLAN_FILENAME = "IMPLEMENTATION_PLAN.md";
|
|
21
25
|
|
|
22
|
-
export
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
if (hasNewCommits(sandboxName, worktreeDir)) {
|
|
29
|
-
return;
|
|
30
|
-
}
|
|
31
|
-
log.warn(`No commits yet — continuing Claude (attempt ${i + 2}/${MAX_CONTINUATIONS + 1})...`);
|
|
32
|
-
await execClaude(sandboxName, worktreeDir, ["--continue", "-p", CONTINUE_PROMPT]);
|
|
26
|
+
export function readPlanFile(worktreeDir: string): string | null {
|
|
27
|
+
const planPath = `${worktreeDir}/${PLAN_FILENAME}`;
|
|
28
|
+
try {
|
|
29
|
+
return fs.readFileSync(planPath, "utf-8");
|
|
30
|
+
} catch {
|
|
31
|
+
return null;
|
|
33
32
|
}
|
|
33
|
+
}
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
export function getUncheckedItems(planContent: string): string[] {
|
|
36
|
+
const matches = planContent.match(/^- \[ \] .+$/gm);
|
|
37
|
+
return matches || [];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function isPlanComplete(planContent: string): boolean {
|
|
41
|
+
const unchecked = getUncheckedItems(planContent);
|
|
42
|
+
const checked = planContent.match(/^- \[x\] .+$/gim);
|
|
43
|
+
return unchecked.length === 0 && (checked?.length ?? 0) > 0;
|
|
38
44
|
}
|
|
39
45
|
|
|
40
|
-
function
|
|
46
|
+
function getCommitCount(sandboxName: string, worktreeDir: string): number {
|
|
41
47
|
const result = Bun.spawnSync([
|
|
42
48
|
"docker", "sandbox", "exec",
|
|
43
49
|
"-w", worktreeDir,
|
|
44
50
|
sandboxName,
|
|
45
|
-
"git", "
|
|
51
|
+
"git", "rev-list", "HEAD", "--not", "--remotes", "--count",
|
|
52
|
+
]);
|
|
53
|
+
if (result.exitCode !== 0) return 0;
|
|
54
|
+
return parseInt(result.stdout.toString().trim(), 10) || 0;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function cleanupPlanFile(sandboxName: string, worktreeDir: string): void {
|
|
58
|
+
const rmResult = Bun.spawnSync([
|
|
59
|
+
"docker", "sandbox", "exec",
|
|
60
|
+
"-w", worktreeDir,
|
|
61
|
+
sandboxName,
|
|
62
|
+
"git", "rm", "-f", PLAN_FILENAME,
|
|
63
|
+
]);
|
|
64
|
+
if (rmResult.exitCode !== 0) return;
|
|
65
|
+
|
|
66
|
+
Bun.spawnSync([
|
|
67
|
+
"docker", "sandbox", "exec",
|
|
68
|
+
"-w", worktreeDir,
|
|
69
|
+
sandboxName,
|
|
70
|
+
"git", "commit", "-m", "chore: remove implementation plan",
|
|
46
71
|
]);
|
|
47
|
-
return result.exitCode === 0 && result.stdout.toString().trim().length > 0;
|
|
48
72
|
}
|
|
49
73
|
|
|
50
|
-
async function
|
|
74
|
+
export async function runClaude(
|
|
75
|
+
sandboxName: string,
|
|
76
|
+
worktreeDir: string,
|
|
77
|
+
planningPrompt: string,
|
|
78
|
+
buildingPromptFn: (remainingItems: string[], planContent: string) => string,
|
|
79
|
+
): Promise<string> {
|
|
80
|
+
let lastResult = "";
|
|
81
|
+
|
|
82
|
+
// Phase 1 — Planning
|
|
83
|
+
log.info("Phase 1: Creating implementation plan...");
|
|
84
|
+
await execClaude(sandboxName, worktreeDir, ["-p", planningPrompt]);
|
|
85
|
+
|
|
86
|
+
const planContent = readPlanFile(worktreeDir);
|
|
87
|
+
const unchecked = planContent ? getUncheckedItems(planContent) : [];
|
|
88
|
+
|
|
89
|
+
// Fallback: no plan file or no checklist items — single-shot mode
|
|
90
|
+
if (!planContent || unchecked.length === 0) {
|
|
91
|
+
log.warn("No implementation plan created — falling back to single-shot mode.");
|
|
92
|
+
const fallbackPrompt = buildingPromptFn([], "");
|
|
93
|
+
const fallbackResult = await execClaude(sandboxName, worktreeDir, ["-p", fallbackPrompt]);
|
|
94
|
+
if (fallbackResult) lastResult = fallbackResult;
|
|
95
|
+
|
|
96
|
+
for (let i = 0; i < MAX_ITERATIONS; i++) {
|
|
97
|
+
if (getCommitCount(sandboxName, worktreeDir) > 0) return lastResult;
|
|
98
|
+
log.warn(`No commits yet — continuing Claude (attempt ${i + 2}/${MAX_ITERATIONS + 1})...`);
|
|
99
|
+
const contResult = await execClaude(sandboxName, worktreeDir, [
|
|
100
|
+
"--continue", "-p",
|
|
101
|
+
"You stopped before finishing. The task is not done yet — there are no commits. Continue where you left off. Do NOT re-plan. Execute the implementation now and commit when done.",
|
|
102
|
+
]);
|
|
103
|
+
if (contResult) lastResult = contResult;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (getCommitCount(sandboxName, worktreeDir) === 0) {
|
|
107
|
+
log.warn("Claude did not produce any commits after all attempts.");
|
|
108
|
+
}
|
|
109
|
+
return lastResult;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
log.ok(`Implementation plan created with ${unchecked.length} task(s).`);
|
|
113
|
+
|
|
114
|
+
// Phase 2 — Building loop
|
|
115
|
+
let stallCount = 0;
|
|
116
|
+
|
|
117
|
+
for (let i = 0; i < MAX_ITERATIONS; i++) {
|
|
118
|
+
const currentPlan = readPlanFile(worktreeDir);
|
|
119
|
+
if (!currentPlan) {
|
|
120
|
+
log.warn("Plan file disappeared — stopping build loop.");
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const remaining = getUncheckedItems(currentPlan);
|
|
125
|
+
if (remaining.length === 0) {
|
|
126
|
+
log.ok("All plan items completed.");
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const commitsBefore = getCommitCount(sandboxName, worktreeDir);
|
|
131
|
+
const uncheckedBefore = remaining.length;
|
|
132
|
+
|
|
133
|
+
log.info(`Iteration ${i + 1}/${MAX_ITERATIONS}: ${remaining[0]!.replace("- [ ] ", "")}`);
|
|
134
|
+
log.info(`${remaining.length} task(s) remaining.`);
|
|
135
|
+
|
|
136
|
+
const buildResult = await execClaude(sandboxName, worktreeDir, ["-p", buildingPromptFn(remaining, currentPlan)]);
|
|
137
|
+
if (buildResult) lastResult = buildResult;
|
|
138
|
+
|
|
139
|
+
const planAfter = readPlanFile(worktreeDir);
|
|
140
|
+
const uncheckedAfter = planAfter ? getUncheckedItems(planAfter).length : 0;
|
|
141
|
+
const commitsAfter = getCommitCount(sandboxName, worktreeDir);
|
|
142
|
+
|
|
143
|
+
if (uncheckedAfter >= uncheckedBefore && commitsAfter <= commitsBefore) {
|
|
144
|
+
stallCount++;
|
|
145
|
+
log.warn(`No progress detected (stall ${stallCount}/${MAX_STALLS}).`);
|
|
146
|
+
if (stallCount >= MAX_STALLS) {
|
|
147
|
+
log.warn("Claude appears stuck — stopping build loop.");
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
} else {
|
|
151
|
+
stallCount = 0;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Phase 3 — Cleanup
|
|
156
|
+
cleanupPlanFile(sandboxName, worktreeDir);
|
|
157
|
+
|
|
158
|
+
const finalPlan = readPlanFile(worktreeDir);
|
|
159
|
+
if (finalPlan) {
|
|
160
|
+
const finalRemaining = getUncheckedItems(finalPlan);
|
|
161
|
+
if (finalRemaining.length > 0) {
|
|
162
|
+
log.warn(`${finalRemaining.length} task(s) were not completed.`);
|
|
163
|
+
}
|
|
164
|
+
} else {
|
|
165
|
+
log.ok("Plan file cleaned up.");
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return lastResult;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async function execClaude(sandboxName: string, worktreeDir: string, claudeArgs: string[]): Promise<string> {
|
|
51
172
|
const proc = Bun.spawn([
|
|
52
173
|
"docker", "sandbox", "exec",
|
|
53
174
|
"-w", worktreeDir,
|
|
@@ -64,6 +185,7 @@ async function execClaude(sandboxName: string, worktreeDir: string, claudeArgs:
|
|
|
64
185
|
const reader = proc.stdout.getReader();
|
|
65
186
|
const decoder = new TextDecoder();
|
|
66
187
|
let buffer = "";
|
|
188
|
+
let resultText = "";
|
|
67
189
|
|
|
68
190
|
while (true) {
|
|
69
191
|
const { done, value } = await reader.read();
|
|
@@ -80,7 +202,8 @@ async function execClaude(sandboxName: string, worktreeDir: string, claudeArgs:
|
|
|
80
202
|
|
|
81
203
|
try {
|
|
82
204
|
const event: StreamEvent = JSON.parse(line);
|
|
83
|
-
printEvent(event);
|
|
205
|
+
const r = printEvent(event);
|
|
206
|
+
if (r) resultText = r;
|
|
84
207
|
} catch {
|
|
85
208
|
// Skip malformed JSON lines
|
|
86
209
|
}
|
|
@@ -91,7 +214,8 @@ async function execClaude(sandboxName: string, worktreeDir: string, claudeArgs:
|
|
|
91
214
|
if (buffer.trim()) {
|
|
92
215
|
try {
|
|
93
216
|
const event: StreamEvent = JSON.parse(buffer);
|
|
94
|
-
printEvent(event);
|
|
217
|
+
const r = printEvent(event);
|
|
218
|
+
if (r) resultText = r;
|
|
95
219
|
} catch {
|
|
96
220
|
// Skip
|
|
97
221
|
}
|
|
@@ -106,9 +230,11 @@ async function execClaude(sandboxName: string, worktreeDir: string, claudeArgs:
|
|
|
106
230
|
}
|
|
107
231
|
log.warn(`Claude Code exited with code ${exitCode}.`);
|
|
108
232
|
}
|
|
233
|
+
|
|
234
|
+
return resultText;
|
|
109
235
|
}
|
|
110
236
|
|
|
111
|
-
function printEvent(event: StreamEvent):
|
|
237
|
+
function printEvent(event: StreamEvent): string | null {
|
|
112
238
|
if (event.type === "assistant" && event.message?.content) {
|
|
113
239
|
for (const block of event.message.content) {
|
|
114
240
|
if (block.type === "text" && block.text) {
|
|
@@ -123,8 +249,10 @@ function printEvent(event: StreamEvent): void {
|
|
|
123
249
|
log.err(event.result || "Unknown error");
|
|
124
250
|
} else if (event.result) {
|
|
125
251
|
log.done(event.result.slice(0, 200));
|
|
252
|
+
return event.result;
|
|
126
253
|
}
|
|
127
254
|
}
|
|
255
|
+
return null;
|
|
128
256
|
}
|
|
129
257
|
|
|
130
258
|
function getToolDetail(name: string, input?: Record<string, unknown>): string {
|
package/src/worktree.ts
CHANGED
|
@@ -36,11 +36,9 @@ export function ensureRepo(
|
|
|
36
36
|
|
|
37
37
|
const defaultBranch = branchProc.stdout.toString().trim();
|
|
38
38
|
|
|
39
|
-
//
|
|
40
|
-
log.info(`
|
|
39
|
+
// Fetch latest remote refs (no checkout/pull — avoids conflicts with existing worktrees)
|
|
40
|
+
log.info(`Fetching latest from origin (${defaultBranch})...`);
|
|
41
41
|
Bun.spawnSync(["git", "fetch", "origin", defaultBranch], { cwd: repoDir });
|
|
42
|
-
Bun.spawnSync(["git", "checkout", defaultBranch], { cwd: repoDir, stdout: "ignore", stderr: "ignore" });
|
|
43
|
-
Bun.spawnSync(["git", "pull", "origin", defaultBranch], { cwd: repoDir });
|
|
44
42
|
|
|
45
43
|
return { defaultBranch };
|
|
46
44
|
}
|
|
@@ -81,14 +79,17 @@ export function createWorktree(
|
|
|
81
79
|
);
|
|
82
80
|
|
|
83
81
|
if (result.exitCode !== 0) {
|
|
84
|
-
//
|
|
85
|
-
|
|
86
|
-
|
|
82
|
+
// Branch likely exists from a previous run — delete it and retry from origin
|
|
83
|
+
log.info(`Branch '${branchName}' already exists, recreating from origin/${defaultBranch}...`);
|
|
84
|
+
Bun.spawnSync(["git", "branch", "-D", branchName], { cwd: repoDir });
|
|
85
|
+
|
|
86
|
+
const retry = Bun.spawnSync(
|
|
87
|
+
["git", "worktree", "add", "-b", branchName, worktreeDir, `origin/${defaultBranch}`],
|
|
87
88
|
{ cwd: repoDir }
|
|
88
89
|
);
|
|
89
90
|
|
|
90
|
-
if (
|
|
91
|
-
log.die(`Failed to create worktree. Branch '${branchName}' may
|
|
91
|
+
if (retry.exitCode !== 0) {
|
|
92
|
+
log.die(`Failed to create worktree. Branch '${branchName}' may be in use by another worktree.`);
|
|
92
93
|
}
|
|
93
94
|
}
|
|
94
95
|
|