@bobbyg603/mog 1.0.1 → 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 +3 -2
- package/package.json +1 -1
- package/src/index.ts +81 -4
- package/src/sandbox.ts +134 -20
- package/src/worktree.ts +9 -1
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<img width="300" alt="claude moggin" src="https://github.com/user-attachments/assets/
|
|
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
|
-
| `
|
|
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/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
|
|
137
|
-
const
|
|
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,
|
|
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
|
|
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
|
|
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
|
-
|
|
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> {
|
package/src/worktree.ts
CHANGED
|
@@ -34,7 +34,15 @@ export function ensureRepo(
|
|
|
34
34
|
log.die("Failed to determine default branch.");
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
const defaultBranch = branchProc.stdout.toString().trim();
|
|
38
|
+
|
|
39
|
+
// Update the main repo to latest before creating worktrees
|
|
40
|
+
log.info(`Updating ${repoDir} (${defaultBranch})...`);
|
|
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
|
+
|
|
45
|
+
return { defaultBranch };
|
|
38
46
|
}
|
|
39
47
|
|
|
40
48
|
export function createWorktree(
|