@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 CHANGED
@@ -1,4 +1,4 @@
1
- <img width="300" alt="claude moggin" src="https://github.com/user-attachments/assets/bed005f3-12c3-47ee-8b6e-6974ed4e0a79" />
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobbyg603/mog",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "One command to go from GitHub issue to pull request, powered by Claude Code in a Docker sandbox",
5
5
  "module": "src/index.ts",
6
6
  "type": "module",
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 [owner, repoName] = repo.split("/");
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<void> {
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].replace("- [ ] ", "")}`);
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<void> {
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): void {
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
- // Update the main repo to latest before creating worktrees
40
- log.info(`Updating ${repoDir} (${defaultBranch})...`);
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
- // Try using existing branch
85
- const fallback = Bun.spawnSync(
86
- ["git", "worktree", "add", worktreeDir, branchName],
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 (fallback.exitCode !== 0) {
91
- log.die(`Failed to create worktree. Branch '${branchName}' may already exist.`);
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