@bobbyg603/mog 1.1.0 → 1.3.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 +9 -2
- package/package.json +1 -1
- package/src/github.ts +49 -2
- package/src/index.ts +66 -11
- package/src/sandbox.ts +25 -11
- package/src/worktree.ts +33 -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
|
|
@@ -19,7 +19,7 @@ That's it. `mog` will:
|
|
|
19
19
|
## Prerequisites
|
|
20
20
|
|
|
21
21
|
- **macOS or Windows** (Docker sandbox microVMs require Docker Desktop)
|
|
22
|
-
- **Docker Desktop
|
|
22
|
+
- **Docker Desktop 4.40+** — running and up to date. Docker sandbox support (required by mog) was introduced in Docker Desktop 4.40. Verify with `docker sandbox ls`.
|
|
23
23
|
- **Bun** — install from [bun.sh](https://bun.sh)
|
|
24
24
|
- **GitHub CLI** (`gh`) — authenticated via `gh auth login`
|
|
25
25
|
- **Git** with push access to your target repos
|
|
@@ -33,6 +33,9 @@ bun install -g @bobbyg603/mog
|
|
|
33
33
|
## Quick start
|
|
34
34
|
|
|
35
35
|
```bash
|
|
36
|
+
# 0. Verify Docker sandbox support is available
|
|
37
|
+
docker sandbox ls
|
|
38
|
+
|
|
36
39
|
# 1. One-time setup: create sandbox & authenticate
|
|
37
40
|
mog init
|
|
38
41
|
# This launches Claude Code — use /login to authenticate with your Max subscription
|
|
@@ -131,6 +134,10 @@ git worktree remove ../repo-worktrees/123-fix-broken-login
|
|
|
131
134
|
|
|
132
135
|
**"No changes detected"** — Claude may have struggled with the issue. Check the worktree manually, or re-run with a more detailed issue description.
|
|
133
136
|
|
|
137
|
+
**"Docker sandbox state is stale"** — Restart Docker Desktop, or remove and recreate the sandbox: `docker sandbox rm mog && mog init`.
|
|
138
|
+
|
|
139
|
+
**"docker: 'sandbox' is not a docker command"** — Your Docker Desktop version doesn't support sandboxes. Update Docker Desktop to **4.40 or later**, then verify with `docker sandbox ls`.
|
|
140
|
+
|
|
134
141
|
**"Failed to push"** — Ensure `gh` is authenticated with push access. Try `gh auth login` and select HTTPS.
|
|
135
142
|
|
|
136
143
|
## Managing the sandbox
|
package/package.json
CHANGED
package/src/github.ts
CHANGED
|
@@ -28,13 +28,56 @@ export function fetchIssue(repo: string, issueNum: string): Issue {
|
|
|
28
28
|
};
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
export function listIssues(repo: string, verbose: boolean): void {
|
|
32
|
+
log.info(`Fetching open issues for ${repo}...`);
|
|
33
|
+
|
|
34
|
+
const fields = verbose
|
|
35
|
+
? "number,title,body,labels,assignees"
|
|
36
|
+
: "number,title";
|
|
37
|
+
|
|
38
|
+
const proc = Bun.spawnSync([
|
|
39
|
+
"gh", "issue", "list",
|
|
40
|
+
"--repo", repo,
|
|
41
|
+
"--state", "open",
|
|
42
|
+
"--json", fields,
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
if (proc.exitCode !== 0) {
|
|
46
|
+
log.die(`Failed to fetch issues for ${repo}. Check the repo name.`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const issues = JSON.parse(proc.stdout.toString());
|
|
50
|
+
|
|
51
|
+
if (issues.length === 0) {
|
|
52
|
+
log.info("No open issues found.");
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
log.ok(`${issues.length} open issue(s):\n`);
|
|
57
|
+
|
|
58
|
+
for (const issue of issues) {
|
|
59
|
+
if (verbose) {
|
|
60
|
+
const labels = issue.labels?.map((l: { name: string }) => l.name).join(", ") || "none";
|
|
61
|
+
const assignees = issue.assignees?.map((a: { login: string }) => a.login).join(", ") || "unassigned";
|
|
62
|
+
console.log(` #${issue.number} ${issue.title}`);
|
|
63
|
+
console.log(` Labels: ${labels}`);
|
|
64
|
+
console.log(` Assignees: ${assignees}`);
|
|
65
|
+
console.log(` ${(issue.body || "No description.").split("\n")[0]}`);
|
|
66
|
+
console.log();
|
|
67
|
+
} else {
|
|
68
|
+
console.log(` #${issue.number} ${issue.title}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
31
73
|
export function pushAndCreatePR(
|
|
32
74
|
repo: string,
|
|
33
75
|
worktreeDir: string,
|
|
34
76
|
branchName: string,
|
|
35
77
|
defaultBranch: string,
|
|
36
78
|
issueNum: string,
|
|
37
|
-
issue: Issue
|
|
79
|
+
issue: Issue,
|
|
80
|
+
summary?: string
|
|
38
81
|
): void {
|
|
39
82
|
// Check for unpushed commits or uncommitted changes
|
|
40
83
|
const unpushed = Bun.spawnSync(["git", "log", `origin/${defaultBranch}..HEAD`, "--oneline"], { cwd: worktreeDir });
|
|
@@ -75,11 +118,15 @@ export function pushAndCreatePR(
|
|
|
75
118
|
// Create PR
|
|
76
119
|
log.info("Opening pull request...");
|
|
77
120
|
|
|
121
|
+
const summarySection = summary
|
|
122
|
+
? `### What was done\n\n${summary}\n\n`
|
|
123
|
+
: "";
|
|
124
|
+
|
|
78
125
|
const prBody = `## Summary
|
|
79
126
|
|
|
80
127
|
Closes #${issueNum}
|
|
81
128
|
|
|
82
|
-
This PR was generated by [mog](https://github.com/bobbyg603/mog) using Claude Code in a Docker sandbox.
|
|
129
|
+
${summarySection}This PR was generated by [mog](https://github.com/bobbyg603/mog) using Claude Code in a Docker sandbox.
|
|
83
130
|
|
|
84
131
|
### Issue: ${issue.title}
|
|
85
132
|
|
package/src/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
|
-
import { fetchIssue } from "./github";
|
|
4
|
-
import { ensureRepo, createWorktree } from "./worktree";
|
|
3
|
+
import { fetchIssue, listIssues } from "./github";
|
|
4
|
+
import { detectRepo, ensureRepo, createWorktree } from "./worktree";
|
|
5
5
|
import { runClaude } from "./sandbox";
|
|
6
6
|
import { pushAndCreatePR } from "./github";
|
|
7
7
|
import { log } from "./log";
|
|
@@ -54,7 +54,7 @@ async function init() {
|
|
|
54
54
|
log.ok("Snapshot saved.");
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
log.ok("mog is ready. Run: mog <owner/repo> <issue_number>");
|
|
57
|
+
log.ok("mog is ready. Run: mog <issue_number> (from a git repo) or mog <owner/repo> <issue_number>");
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
async function main() {
|
|
@@ -84,25 +84,80 @@ async function main() {
|
|
|
84
84
|
return;
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
|
|
87
|
+
// mog list [--verbose] or mog <owner/repo> list [--verbose]
|
|
88
|
+
if (args[0] === "list" || args[1] === "list") {
|
|
89
|
+
let repo: string;
|
|
90
|
+
const verbose = args.includes("--verbose");
|
|
91
|
+
|
|
92
|
+
if (args[0] === "list") {
|
|
93
|
+
const detected = detectRepo();
|
|
94
|
+
if (!detected) {
|
|
95
|
+
log.die("Could not detect repo from git remote. Run from inside a git repo or use: mog <owner/repo> list");
|
|
96
|
+
}
|
|
97
|
+
repo = detected;
|
|
98
|
+
} else {
|
|
99
|
+
repo = args[0];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
listIssues(repo, verbose);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (args.length < 1) {
|
|
88
107
|
console.log("Usage:");
|
|
89
108
|
console.log(" mog init — one-time setup (create sandbox & login)");
|
|
109
|
+
console.log(" mog <issue_num> — auto-detect repo from git remote");
|
|
90
110
|
console.log(" mog <owner/repo> <issue_num> — fetch issue, run Claude, open PR");
|
|
111
|
+
console.log(" mog list [--verbose] — list open issues (auto-detect repo)");
|
|
112
|
+
console.log(" mog <owner/repo> list [--verbose] — list open issues for a repo");
|
|
91
113
|
console.log();
|
|
92
114
|
console.log("Example:");
|
|
93
115
|
console.log(" mog init");
|
|
116
|
+
console.log(" mog 123");
|
|
94
117
|
console.log(" mog workingdevshero/automate-it 123");
|
|
118
|
+
console.log(" mog list");
|
|
119
|
+
console.log(" mog list --verbose");
|
|
95
120
|
return;
|
|
96
121
|
}
|
|
97
122
|
|
|
98
|
-
|
|
99
|
-
|
|
123
|
+
let repo: string;
|
|
124
|
+
let issueNum: string;
|
|
100
125
|
|
|
101
|
-
if (
|
|
102
|
-
|
|
126
|
+
if (/^\d+$/.test(args[0])) {
|
|
127
|
+
// mog <issue_number> — auto-detect repo
|
|
128
|
+
const detected = detectRepo();
|
|
129
|
+
if (!detected) {
|
|
130
|
+
log.die("Could not detect repo from git remote. Run from inside a git repo or use: mog <owner/repo> <issue_num>");
|
|
131
|
+
}
|
|
132
|
+
repo = detected;
|
|
133
|
+
issueNum = args[0];
|
|
134
|
+
} else if (args.length >= 2) {
|
|
135
|
+
// mog <owner/repo> <issue_number>
|
|
136
|
+
repo = args[0];
|
|
137
|
+
issueNum = args[1];
|
|
138
|
+
if (!/^\d+$/.test(issueNum)) {
|
|
139
|
+
log.die(`Invalid issue number: '${issueNum}'. Must be a positive integer.`);
|
|
140
|
+
}
|
|
141
|
+
} else {
|
|
142
|
+
console.log("Usage:");
|
|
143
|
+
console.log(" mog init — one-time setup (create sandbox & login)");
|
|
144
|
+
console.log(" mog <issue_num> — auto-detect repo from git remote");
|
|
145
|
+
console.log(" mog <owner/repo> <issue_num> — fetch issue, run Claude, open PR");
|
|
146
|
+
console.log(" mog list [--verbose] — list open issues (auto-detect repo)");
|
|
147
|
+
console.log(" mog <owner/repo> list [--verbose] — list open issues for a repo");
|
|
148
|
+
console.log();
|
|
149
|
+
console.log("Example:");
|
|
150
|
+
console.log(" mog init");
|
|
151
|
+
console.log(" mog 123");
|
|
152
|
+
console.log(" mog workingdevshero/automate-it 123");
|
|
153
|
+
console.log(" mog list");
|
|
154
|
+
console.log(" mog list --verbose");
|
|
155
|
+
return;
|
|
103
156
|
}
|
|
104
157
|
|
|
105
|
-
const
|
|
158
|
+
const parts = repo.split("/");
|
|
159
|
+
const owner = parts[0] as string;
|
|
160
|
+
const repoName = parts[1] as string;
|
|
106
161
|
|
|
107
162
|
if (!owner || !repoName) {
|
|
108
163
|
log.die("Invalid repo format. Use: owner/repo");
|
|
@@ -144,10 +199,10 @@ async function main() {
|
|
|
144
199
|
log.info(`Worktree: ${worktreeDir}`);
|
|
145
200
|
console.log();
|
|
146
201
|
|
|
147
|
-
await runClaude(SANDBOX_NAME, worktreeDir, planningPrompt, buildingPromptFn);
|
|
202
|
+
const summary = await runClaude(SANDBOX_NAME, worktreeDir, planningPrompt, buildingPromptFn);
|
|
148
203
|
|
|
149
204
|
// Push and create PR
|
|
150
|
-
pushAndCreatePR(repo, worktreeDir, branchName, defaultBranch, issueNum, issue);
|
|
205
|
+
pushAndCreatePR(repo, worktreeDir, branchName, defaultBranch, issueNum, issue, summary);
|
|
151
206
|
}
|
|
152
207
|
|
|
153
208
|
function getReposDir(): string {
|
package/src/sandbox.ts
CHANGED
|
@@ -76,7 +76,9 @@ export async function runClaude(
|
|
|
76
76
|
worktreeDir: string,
|
|
77
77
|
planningPrompt: string,
|
|
78
78
|
buildingPromptFn: (remainingItems: string[], planContent: string) => string,
|
|
79
|
-
): Promise<
|
|
79
|
+
): Promise<string> {
|
|
80
|
+
let lastResult = "";
|
|
81
|
+
|
|
80
82
|
// Phase 1 — Planning
|
|
81
83
|
log.info("Phase 1: Creating implementation plan...");
|
|
82
84
|
await execClaude(sandboxName, worktreeDir, ["-p", planningPrompt]);
|
|
@@ -88,21 +90,23 @@ export async function runClaude(
|
|
|
88
90
|
if (!planContent || unchecked.length === 0) {
|
|
89
91
|
log.warn("No implementation plan created — falling back to single-shot mode.");
|
|
90
92
|
const fallbackPrompt = buildingPromptFn([], "");
|
|
91
|
-
await execClaude(sandboxName, worktreeDir, ["-p", fallbackPrompt]);
|
|
93
|
+
const fallbackResult = await execClaude(sandboxName, worktreeDir, ["-p", fallbackPrompt]);
|
|
94
|
+
if (fallbackResult) lastResult = fallbackResult;
|
|
92
95
|
|
|
93
96
|
for (let i = 0; i < MAX_ITERATIONS; i++) {
|
|
94
|
-
if (getCommitCount(sandboxName, worktreeDir) > 0) return;
|
|
97
|
+
if (getCommitCount(sandboxName, worktreeDir) > 0) return lastResult;
|
|
95
98
|
log.warn(`No commits yet — continuing Claude (attempt ${i + 2}/${MAX_ITERATIONS + 1})...`);
|
|
96
|
-
await execClaude(sandboxName, worktreeDir, [
|
|
99
|
+
const contResult = await execClaude(sandboxName, worktreeDir, [
|
|
97
100
|
"--continue", "-p",
|
|
98
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.",
|
|
99
102
|
]);
|
|
103
|
+
if (contResult) lastResult = contResult;
|
|
100
104
|
}
|
|
101
105
|
|
|
102
106
|
if (getCommitCount(sandboxName, worktreeDir) === 0) {
|
|
103
107
|
log.warn("Claude did not produce any commits after all attempts.");
|
|
104
108
|
}
|
|
105
|
-
return;
|
|
109
|
+
return lastResult;
|
|
106
110
|
}
|
|
107
111
|
|
|
108
112
|
log.ok(`Implementation plan created with ${unchecked.length} task(s).`);
|
|
@@ -126,10 +130,11 @@ export async function runClaude(
|
|
|
126
130
|
const commitsBefore = getCommitCount(sandboxName, worktreeDir);
|
|
127
131
|
const uncheckedBefore = remaining.length;
|
|
128
132
|
|
|
129
|
-
log.info(`Iteration ${i + 1}/${MAX_ITERATIONS}: ${remaining[0]
|
|
133
|
+
log.info(`Iteration ${i + 1}/${MAX_ITERATIONS}: ${remaining[0]!.replace("- [ ] ", "")}`);
|
|
130
134
|
log.info(`${remaining.length} task(s) remaining.`);
|
|
131
135
|
|
|
132
|
-
await execClaude(sandboxName, worktreeDir, ["-p", buildingPromptFn(remaining, currentPlan)]);
|
|
136
|
+
const buildResult = await execClaude(sandboxName, worktreeDir, ["-p", buildingPromptFn(remaining, currentPlan)]);
|
|
137
|
+
if (buildResult) lastResult = buildResult;
|
|
133
138
|
|
|
134
139
|
const planAfter = readPlanFile(worktreeDir);
|
|
135
140
|
const uncheckedAfter = planAfter ? getUncheckedItems(planAfter).length : 0;
|
|
@@ -159,9 +164,11 @@ export async function runClaude(
|
|
|
159
164
|
} else {
|
|
160
165
|
log.ok("Plan file cleaned up.");
|
|
161
166
|
}
|
|
167
|
+
|
|
168
|
+
return lastResult;
|
|
162
169
|
}
|
|
163
170
|
|
|
164
|
-
async function execClaude(sandboxName: string, worktreeDir: string, claudeArgs: string[]): Promise<
|
|
171
|
+
async function execClaude(sandboxName: string, worktreeDir: string, claudeArgs: string[]): Promise<string> {
|
|
165
172
|
const proc = Bun.spawn([
|
|
166
173
|
"docker", "sandbox", "exec",
|
|
167
174
|
"-w", worktreeDir,
|
|
@@ -178,6 +185,7 @@ async function execClaude(sandboxName: string, worktreeDir: string, claudeArgs:
|
|
|
178
185
|
const reader = proc.stdout.getReader();
|
|
179
186
|
const decoder = new TextDecoder();
|
|
180
187
|
let buffer = "";
|
|
188
|
+
let resultText = "";
|
|
181
189
|
|
|
182
190
|
while (true) {
|
|
183
191
|
const { done, value } = await reader.read();
|
|
@@ -194,7 +202,8 @@ async function execClaude(sandboxName: string, worktreeDir: string, claudeArgs:
|
|
|
194
202
|
|
|
195
203
|
try {
|
|
196
204
|
const event: StreamEvent = JSON.parse(line);
|
|
197
|
-
printEvent(event);
|
|
205
|
+
const r = printEvent(event);
|
|
206
|
+
if (r) resultText = r;
|
|
198
207
|
} catch {
|
|
199
208
|
// Skip malformed JSON lines
|
|
200
209
|
}
|
|
@@ -205,7 +214,8 @@ async function execClaude(sandboxName: string, worktreeDir: string, claudeArgs:
|
|
|
205
214
|
if (buffer.trim()) {
|
|
206
215
|
try {
|
|
207
216
|
const event: StreamEvent = JSON.parse(buffer);
|
|
208
|
-
printEvent(event);
|
|
217
|
+
const r = printEvent(event);
|
|
218
|
+
if (r) resultText = r;
|
|
209
219
|
} catch {
|
|
210
220
|
// Skip
|
|
211
221
|
}
|
|
@@ -220,9 +230,11 @@ async function execClaude(sandboxName: string, worktreeDir: string, claudeArgs:
|
|
|
220
230
|
}
|
|
221
231
|
log.warn(`Claude Code exited with code ${exitCode}.`);
|
|
222
232
|
}
|
|
233
|
+
|
|
234
|
+
return resultText;
|
|
223
235
|
}
|
|
224
236
|
|
|
225
|
-
function printEvent(event: StreamEvent):
|
|
237
|
+
function printEvent(event: StreamEvent): string | null {
|
|
226
238
|
if (event.type === "assistant" && event.message?.content) {
|
|
227
239
|
for (const block of event.message.content) {
|
|
228
240
|
if (block.type === "text" && block.text) {
|
|
@@ -237,8 +249,10 @@ function printEvent(event: StreamEvent): void {
|
|
|
237
249
|
log.err(event.result || "Unknown error");
|
|
238
250
|
} else if (event.result) {
|
|
239
251
|
log.done(event.result.slice(0, 200));
|
|
252
|
+
return event.result;
|
|
240
253
|
}
|
|
241
254
|
}
|
|
255
|
+
return null;
|
|
242
256
|
}
|
|
243
257
|
|
|
244
258
|
function getToolDetail(name: string, input?: Record<string, unknown>): string {
|
package/src/worktree.ts
CHANGED
|
@@ -1,6 +1,29 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import { log } from "./log";
|
|
3
3
|
|
|
4
|
+
export function detectRepo(): string | null {
|
|
5
|
+
const result = Bun.spawnSync(["git", "remote", "get-url", "origin"]);
|
|
6
|
+
if (result.exitCode !== 0) {
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const url = result.stdout.toString().trim();
|
|
11
|
+
|
|
12
|
+
// SSH: git@github.com:owner/repo.git
|
|
13
|
+
const sshMatch = url.match(/^git@[^:]+:([^/]+)\/([^/]+?)(?:\.git)?$/);
|
|
14
|
+
if (sshMatch) {
|
|
15
|
+
return `${sshMatch[1]}/${sshMatch[2]}`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// HTTPS: https://github.com/owner/repo.git
|
|
19
|
+
const httpsMatch = url.match(/^https?:\/\/[^/]+\/([^/]+)\/([^/]+?)(?:\.git)?$/);
|
|
20
|
+
if (httpsMatch) {
|
|
21
|
+
return `${httpsMatch[1]}/${httpsMatch[2]}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
4
27
|
export function ensureRepo(
|
|
5
28
|
repo: string,
|
|
6
29
|
owner: string,
|
|
@@ -36,11 +59,9 @@ export function ensureRepo(
|
|
|
36
59
|
|
|
37
60
|
const defaultBranch = branchProc.stdout.toString().trim();
|
|
38
61
|
|
|
39
|
-
//
|
|
40
|
-
log.info(`
|
|
62
|
+
// Fetch latest remote refs (no checkout/pull — avoids conflicts with existing worktrees)
|
|
63
|
+
log.info(`Fetching latest from origin (${defaultBranch})...`);
|
|
41
64
|
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
65
|
|
|
45
66
|
return { defaultBranch };
|
|
46
67
|
}
|
|
@@ -81,14 +102,17 @@ export function createWorktree(
|
|
|
81
102
|
);
|
|
82
103
|
|
|
83
104
|
if (result.exitCode !== 0) {
|
|
84
|
-
//
|
|
85
|
-
|
|
86
|
-
|
|
105
|
+
// Branch likely exists from a previous run — delete it and retry from origin
|
|
106
|
+
log.info(`Branch '${branchName}' already exists, recreating from origin/${defaultBranch}...`);
|
|
107
|
+
Bun.spawnSync(["git", "branch", "-D", branchName], { cwd: repoDir });
|
|
108
|
+
|
|
109
|
+
const retry = Bun.spawnSync(
|
|
110
|
+
["git", "worktree", "add", "-b", branchName, worktreeDir, `origin/${defaultBranch}`],
|
|
87
111
|
{ cwd: repoDir }
|
|
88
112
|
);
|
|
89
113
|
|
|
90
|
-
if (
|
|
91
|
-
log.die(`Failed to create worktree. Branch '${branchName}' may
|
|
114
|
+
if (retry.exitCode !== 0) {
|
|
115
|
+
log.die(`Failed to create worktree. Branch '${branchName}' may be in use by another worktree.`);
|
|
92
116
|
}
|
|
93
117
|
}
|
|
94
118
|
|