@bobbyg603/mog 1.1.0 → 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 +1 -1
- package/package.json +1 -1
- package/src/github.ts +7 -2
- package/src/index.ts +8 -6
- package/src/sandbox.ts +25 -11
- 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
|
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");
|
|
@@ -144,10 +146,10 @@ async function main() {
|
|
|
144
146
|
log.info(`Worktree: ${worktreeDir}`);
|
|
145
147
|
console.log();
|
|
146
148
|
|
|
147
|
-
await runClaude(SANDBOX_NAME, worktreeDir, planningPrompt, buildingPromptFn);
|
|
149
|
+
const summary = await runClaude(SANDBOX_NAME, worktreeDir, planningPrompt, buildingPromptFn);
|
|
148
150
|
|
|
149
151
|
// Push and create PR
|
|
150
|
-
pushAndCreatePR(repo, worktreeDir, branchName, defaultBranch, issueNum, issue);
|
|
152
|
+
pushAndCreatePR(repo, worktreeDir, branchName, defaultBranch, issueNum, issue, summary);
|
|
151
153
|
}
|
|
152
154
|
|
|
153
155
|
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
|
@@ -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
|
|