@bobbyg603/mog 1.0.2 → 1.1.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/089db43c-7381-4e62-87bc-af2e7cd0129f" />
1
+ <img width="300" alt="claude moggin" src="https://github.com/user-attachments/assets/bed005f3-12c3-47ee-8b6e-6974ed4e0a79" />
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
- | `MOG_MAX_CONTINUATIONS` | `5` | Max times Claude is re-prompted if it stops without committing |
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobbyg603/mog",
3
- "version": "1.0.2",
3
+ "version": "1.1.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/index.ts CHANGED
@@ -133,8 +133,10 @@ async function main() {
133
133
  reposDir, owner, repoName, defaultBranch, issueNum, issue.title
134
134
  );
135
135
 
136
- // Build prompt
137
- const prompt = buildPrompt(repo, issueNum, issue);
136
+ // Build prompts
137
+ const planningPrompt = buildPlanningPrompt(repo, issueNum, issue);
138
+ const buildingPromptFn = (remaining: string[], plan: string) =>
139
+ buildBuildingPrompt(repo, issueNum, issue, remaining, plan);
138
140
 
139
141
  // Run Claude in sandbox
140
142
  log.info("Launching Claude Code in sandbox...");
@@ -142,7 +144,7 @@ async function main() {
142
144
  log.info(`Worktree: ${worktreeDir}`);
143
145
  console.log();
144
146
 
145
- await runClaude(SANDBOX_NAME, worktreeDir, prompt);
147
+ await runClaude(SANDBOX_NAME, worktreeDir, planningPrompt, buildingPromptFn);
146
148
 
147
149
  // Push and create PR
148
150
  pushAndCreatePR(repo, worktreeDir, branchName, defaultBranch, issueNum, issue);
@@ -197,7 +199,7 @@ function tryRecoverSandbox(reposDir: string): boolean {
197
199
  return true;
198
200
  }
199
201
 
200
- function buildPrompt(repo: string, issueNum: string, issue: { title: string; body: string; labels: string }): string {
202
+ function buildPlanningPrompt(repo: string, issueNum: string, issue: { title: string; body: string; labels: string }): string {
201
203
  return `You are working on GitHub issue #${issueNum} for the repository ${repo}.
202
204
 
203
205
  ## Issue: ${issue.title}
@@ -208,6 +210,52 @@ ${issue.body}
208
210
  ### Labels
209
211
  ${issue.labels}
210
212
 
213
+ ## Instructions
214
+
215
+ Your job in this step is to **plan only** — do NOT implement anything and do NOT commit.
216
+
217
+ 1. Read and understand the codebase structure thoroughly.
218
+ 2. Analyze the issue and break it down into small, atomic implementation tasks.
219
+ 3. Create a file called \`IMPLEMENTATION_PLAN.md\` in the root of the repository with a checklist of tasks.
220
+
221
+ The plan should:
222
+ - Have 3-8 tasks (fewer for simple issues, more for complex ones)
223
+ - Order tasks by dependency (implement foundations first)
224
+ - Each task should be a single, atomic unit of work that results in one commit
225
+ - Use markdown checklist format: \`- [ ] Task description\`
226
+
227
+ Example format:
228
+ \`\`\`markdown
229
+ # Implementation Plan for #${issueNum}
230
+
231
+ - [ ] Add the FooBar interface to src/types.ts
232
+ - [ ] Implement the FooBar service in src/services/foobar.ts
233
+ - [ ] Update the main handler to use FooBar service
234
+ - [ ] Add unit tests for FooBar service
235
+ \`\`\`
236
+
237
+ Do NOT implement any code changes. Do NOT make any commits. Only create the plan file.`;
238
+ }
239
+
240
+ function buildBuildingPrompt(
241
+ repo: string,
242
+ issueNum: string,
243
+ issue: { title: string; body: string; labels: string },
244
+ remainingItems: string[],
245
+ planContent: string,
246
+ ): string {
247
+ // Fallback: no plan — use original single-shot prompt
248
+ if (remainingItems.length === 0 && !planContent) {
249
+ return `You are working on GitHub issue #${issueNum} for the repository ${repo}.
250
+
251
+ ## Issue: ${issue.title}
252
+
253
+ ### Description
254
+ ${issue.body}
255
+
256
+ ### Labels
257
+ ${issue.labels}
258
+
211
259
  ## Instructions
212
260
  1. Read and understand the codebase structure first.
213
261
  2. Implement the changes described in the issue above.
@@ -218,6 +266,35 @@ ${issue.labels}
218
266
 
219
267
  When you are done, make a single commit (or a small, logical set of commits) with
220
268
  a message like: "fix: <short description> (#${issueNum})"`;
269
+ }
270
+
271
+ const currentTask = remainingItems[0]?.replace("- [ ] ", "") || "Complete remaining work";
272
+
273
+ return `You are working on GitHub issue #${issueNum} for the repository ${repo}.
274
+
275
+ ## Issue: ${issue.title}
276
+
277
+ ### Description
278
+ ${issue.body}
279
+
280
+ ### Labels
281
+ ${issue.labels}
282
+
283
+ ## Current Implementation Plan
284
+
285
+ ${planContent}
286
+
287
+ ## Instructions
288
+
289
+ Implement ONLY the following task:
290
+ **${currentTask}**
291
+
292
+ Rules:
293
+ 1. Implement ONLY this one task — do not work on other unchecked items.
294
+ 2. Write clean code that follows the existing project conventions.
295
+ 3. Update \`IMPLEMENTATION_PLAN.md\` to check off the completed item (change \`- [ ]\` to \`- [x]\`).
296
+ 4. Commit ALL changes (including the updated plan file) with a message like: "feat: ${currentTask.toLowerCase()} (#${issueNum})"
297
+ 5. Do NOT work on any other tasks after committing.`;
221
298
  }
222
299
 
223
300
  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,36 +16,149 @@ interface StreamEvent {
15
16
  is_error?: boolean;
16
17
  }
17
18
 
18
- const MAX_CONTINUATIONS = parseInt(process.env.MOG_MAX_CONTINUATIONS || "5", 10);
19
- const CONTINUE_PROMPT = `You stopped before finishing. The task is not done yet — there are no commits.
20
- Continue where you left off. Do NOT re-plan. Execute the implementation now and commit when done.`;
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 async function runClaude(sandboxName: string, worktreeDir: string, prompt: string): Promise<void> {
23
- // Initial run
24
- await execClaude(sandboxName, worktreeDir, ["-p", prompt]);
25
-
26
- // Continue loop: if no commits were made, nudge Claude to keep going
27
- for (let i = 0; i < MAX_CONTINUATIONS; i++) {
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
- if (!hasNewCommits(sandboxName, worktreeDir)) {
36
- log.warn("Claude did not produce any commits after all attempts.");
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 hasNewCommits(sandboxName: string, worktreeDir: string): boolean {
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", "log", "--oneline", "HEAD", "--not", "--remotes", "-1",
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;
72
+ }
73
+
74
+ export async function runClaude(
75
+ sandboxName: string,
76
+ worktreeDir: string,
77
+ planningPrompt: string,
78
+ buildingPromptFn: (remainingItems: string[], planContent: string) => string,
79
+ ): Promise<void> {
80
+ // Phase 1 — Planning
81
+ log.info("Phase 1: Creating implementation plan...");
82
+ await execClaude(sandboxName, worktreeDir, ["-p", planningPrompt]);
83
+
84
+ const planContent = readPlanFile(worktreeDir);
85
+ const unchecked = planContent ? getUncheckedItems(planContent) : [];
86
+
87
+ // Fallback: no plan file or no checklist items — single-shot mode
88
+ if (!planContent || unchecked.length === 0) {
89
+ log.warn("No implementation plan created — falling back to single-shot mode.");
90
+ const fallbackPrompt = buildingPromptFn([], "");
91
+ await execClaude(sandboxName, worktreeDir, ["-p", fallbackPrompt]);
92
+
93
+ for (let i = 0; i < MAX_ITERATIONS; i++) {
94
+ if (getCommitCount(sandboxName, worktreeDir) > 0) return;
95
+ log.warn(`No commits yet — continuing Claude (attempt ${i + 2}/${MAX_ITERATIONS + 1})...`);
96
+ await execClaude(sandboxName, worktreeDir, [
97
+ "--continue", "-p",
98
+ "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
+ ]);
100
+ }
101
+
102
+ if (getCommitCount(sandboxName, worktreeDir) === 0) {
103
+ log.warn("Claude did not produce any commits after all attempts.");
104
+ }
105
+ return;
106
+ }
107
+
108
+ log.ok(`Implementation plan created with ${unchecked.length} task(s).`);
109
+
110
+ // Phase 2 — Building loop
111
+ let stallCount = 0;
112
+
113
+ for (let i = 0; i < MAX_ITERATIONS; i++) {
114
+ const currentPlan = readPlanFile(worktreeDir);
115
+ if (!currentPlan) {
116
+ log.warn("Plan file disappeared — stopping build loop.");
117
+ break;
118
+ }
119
+
120
+ const remaining = getUncheckedItems(currentPlan);
121
+ if (remaining.length === 0) {
122
+ log.ok("All plan items completed.");
123
+ break;
124
+ }
125
+
126
+ const commitsBefore = getCommitCount(sandboxName, worktreeDir);
127
+ const uncheckedBefore = remaining.length;
128
+
129
+ log.info(`Iteration ${i + 1}/${MAX_ITERATIONS}: ${remaining[0].replace("- [ ] ", "")}`);
130
+ log.info(`${remaining.length} task(s) remaining.`);
131
+
132
+ await execClaude(sandboxName, worktreeDir, ["-p", buildingPromptFn(remaining, currentPlan)]);
133
+
134
+ const planAfter = readPlanFile(worktreeDir);
135
+ const uncheckedAfter = planAfter ? getUncheckedItems(planAfter).length : 0;
136
+ const commitsAfter = getCommitCount(sandboxName, worktreeDir);
137
+
138
+ if (uncheckedAfter >= uncheckedBefore && commitsAfter <= commitsBefore) {
139
+ stallCount++;
140
+ log.warn(`No progress detected (stall ${stallCount}/${MAX_STALLS}).`);
141
+ if (stallCount >= MAX_STALLS) {
142
+ log.warn("Claude appears stuck — stopping build loop.");
143
+ break;
144
+ }
145
+ } else {
146
+ stallCount = 0;
147
+ }
148
+ }
149
+
150
+ // Phase 3 — Cleanup
151
+ cleanupPlanFile(sandboxName, worktreeDir);
152
+
153
+ const finalPlan = readPlanFile(worktreeDir);
154
+ if (finalPlan) {
155
+ const finalRemaining = getUncheckedItems(finalPlan);
156
+ if (finalRemaining.length > 0) {
157
+ log.warn(`${finalRemaining.length} task(s) were not completed.`);
158
+ }
159
+ } else {
160
+ log.ok("Plan file cleaned up.");
161
+ }
48
162
  }
49
163
 
50
164
  async function execClaude(sandboxName: string, worktreeDir: string, claudeArgs: string[]): Promise<void> {