@bobbyg603/mog 1.2.0 → 1.4.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
@@ -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** — running and up to date (must support `docker sandbox`)
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobbyg603/mog",
3
- "version": "1.2.0",
3
+ "version": "1.4.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
@@ -4,6 +4,7 @@ export interface Issue {
4
4
  title: string;
5
5
  body: string;
6
6
  labels: string;
7
+ comments: string;
7
8
  }
8
9
 
9
10
  export function fetchIssue(repo: string, issueNum: string): Issue {
@@ -12,7 +13,7 @@ export function fetchIssue(repo: string, issueNum: string): Issue {
12
13
  const proc = Bun.spawnSync([
13
14
  "gh", "issue", "view", issueNum,
14
15
  "--repo", repo,
15
- "--json", "title,body,labels",
16
+ "--json", "title,body,labels,comments",
16
17
  ]);
17
18
 
18
19
  if (proc.exitCode !== 0) {
@@ -21,13 +22,60 @@ export function fetchIssue(repo: string, issueNum: string): Issue {
21
22
 
22
23
  const json = JSON.parse(proc.stdout.toString());
23
24
 
25
+ const comments = (json.comments || [])
26
+ .map((c: { author: { login: string }; body: string }) => `**@${c.author.login}:** ${c.body}`)
27
+ .join("\n\n");
28
+
24
29
  return {
25
30
  title: json.title,
26
31
  body: json.body || "No description provided.",
27
32
  labels: json.labels?.map((l: { name: string }) => l.name).join(", ") || "none",
33
+ comments,
28
34
  };
29
35
  }
30
36
 
37
+ export function listIssues(repo: string, verbose: boolean): void {
38
+ log.info(`Fetching open issues for ${repo}...`);
39
+
40
+ const fields = verbose
41
+ ? "number,title,body,labels,assignees"
42
+ : "number,title";
43
+
44
+ const proc = Bun.spawnSync([
45
+ "gh", "issue", "list",
46
+ "--repo", repo,
47
+ "--state", "open",
48
+ "--json", fields,
49
+ ]);
50
+
51
+ if (proc.exitCode !== 0) {
52
+ log.die(`Failed to fetch issues for ${repo}. Check the repo name.`);
53
+ }
54
+
55
+ const issues = JSON.parse(proc.stdout.toString());
56
+
57
+ if (issues.length === 0) {
58
+ log.info("No open issues found.");
59
+ return;
60
+ }
61
+
62
+ log.ok(`${issues.length} open issue(s):\n`);
63
+
64
+ for (const issue of issues) {
65
+ if (verbose) {
66
+ const labels = issue.labels?.map((l: { name: string }) => l.name).join(", ") || "none";
67
+ const assignees = issue.assignees?.map((a: { login: string }) => a.login).join(", ") || "unassigned";
68
+ console.log(` #${issue.number} ${issue.title}`);
69
+ console.log(` Labels: ${labels}`);
70
+ console.log(` Assignees: ${assignees}`);
71
+ console.log(` ${(issue.body || "No description.").split("\n")[0]}`);
72
+ console.log();
73
+ } else {
74
+ console.log(` #${issue.number} ${issue.title}`);
75
+ }
76
+ }
77
+ }
78
+
31
79
  export function pushAndCreatePR(
32
80
  repo: string,
33
81
  worktreeDir: string,
@@ -65,6 +113,22 @@ export function pushAndCreatePR(
65
113
  }
66
114
  }
67
115
 
116
+ // Squash all commits into one
117
+ const commitCount = Bun.spawnSync(["git", "rev-list", "--count", `${defaultBranch}..HEAD`], { cwd: worktreeDir });
118
+ const count = parseInt(commitCount.stdout.toString().trim(), 10) || 0;
119
+ if (count > 1) {
120
+ log.info(`Squashing ${count} commits into one...`);
121
+ const prefix = issue.labels.includes("enhancement") || issue.labels.includes("feature") ? "feat" : "fix";
122
+ const squash = Bun.spawnSync(["git", "reset", "--soft", defaultBranch], { cwd: worktreeDir });
123
+ if (squash.exitCode === 0) {
124
+ const msg = `${prefix}: ${issue.title.toLowerCase()} (#${issueNum})`;
125
+ Bun.spawnSync(["git", "commit", "-m", msg], { cwd: worktreeDir });
126
+ log.ok("Commits squashed.");
127
+ } else {
128
+ log.warn("Failed to squash — pushing individual commits instead.");
129
+ }
130
+ }
131
+
68
132
  // Push
69
133
  log.info(`Pushing branch '${branchName}' to origin...`);
70
134
  const push = Bun.spawnSync(["git", "push", "-u", "origin", branchName], { cwd: worktreeDir });
package/src/index.ts CHANGED
@@ -1,7 +1,9 @@
1
1
  #!/usr/bin/env bun
2
2
 
3
- import { fetchIssue } from "./github";
4
- import { ensureRepo, createWorktree } from "./worktree";
3
+ import fs from "fs";
4
+ import path from "path";
5
+ import { fetchIssue, listIssues } from "./github";
6
+ import { detectRepo, ensureRepo, createWorktree } from "./worktree";
5
7
  import { runClaude } from "./sandbox";
6
8
  import { pushAndCreatePR } from "./github";
7
9
  import { log } from "./log";
@@ -54,7 +56,7 @@ async function init() {
54
56
  log.ok("Snapshot saved.");
55
57
  }
56
58
 
57
- log.ok("mog is ready. Run: mog <owner/repo> <issue_number>");
59
+ log.ok("mog is ready. Run: mog <issue_number> (from a git repo) or mog <owner/repo> <issue_number>");
58
60
  }
59
61
 
60
62
  async function main() {
@@ -84,22 +86,99 @@ async function main() {
84
86
  return;
85
87
  }
86
88
 
87
- if (args.length < 2) {
89
+ // mog list [--verbose] or mog <owner/repo> list [--verbose]
90
+ if (args[0] === "list" || args[1] === "list") {
91
+ let repo: string;
92
+ const verbose = args.includes("--verbose");
93
+
94
+ if (args[0] === "list") {
95
+ const detected = detectRepo();
96
+ if (!detected) {
97
+ log.die("Could not detect repo from git remote. Run from inside a git repo or use: mog <owner/repo> list");
98
+ }
99
+ repo = detected;
100
+ } else {
101
+ repo = args[0];
102
+ }
103
+
104
+ listIssues(repo, verbose);
105
+ return;
106
+ }
107
+
108
+ if (args.length < 1) {
88
109
  console.log("Usage:");
89
110
  console.log(" mog init — one-time setup (create sandbox & login)");
111
+ console.log(" mog <issue_num> — auto-detect repo from git remote");
90
112
  console.log(" mog <owner/repo> <issue_num> — fetch issue, run Claude, open PR");
113
+ console.log(" mog list [--verbose] — list open issues (auto-detect repo)");
114
+ console.log(" mog <owner/repo> list [--verbose] — list open issues for a repo");
115
+ console.log();
116
+ console.log("Options:");
117
+ console.log(" --include <file> — copy a file into the worktree (repeatable)");
91
118
  console.log();
92
119
  console.log("Example:");
93
120
  console.log(" mog init");
121
+ console.log(" mog 123");
122
+ console.log(" mog 123 --include .env");
94
123
  console.log(" mog workingdevshero/automate-it 123");
124
+ console.log(" mog list");
125
+ console.log(" mog list --verbose");
95
126
  return;
96
127
  }
97
128
 
98
- const repo = args[0] as string;
99
- const issueNum = args[1] as string;
129
+ // Parse --include flags
130
+ const includeFiles: string[] = [];
131
+ const filteredArgs: string[] = [];
132
+ for (let i = 0; i < args.length; i++) {
133
+ if (args[i] === "--include" && i + 1 < args.length) {
134
+ const filePath = path.resolve(args[i + 1]!);
135
+ if (!fs.existsSync(filePath)) {
136
+ log.die(`Include file not found: ${args[i + 1]}`);
137
+ }
138
+ includeFiles.push(filePath);
139
+ i++; // skip the path argument
140
+ } else {
141
+ filteredArgs.push(args[i]!);
142
+ }
143
+ }
100
144
 
101
- if (!repo || !issueNum || !/^\d+$/.test(issueNum)) {
102
- log.die(`Invalid issue number: '${issueNum}'. Must be a positive integer.`);
145
+ let repo: string;
146
+ let issueNum: string;
147
+
148
+ if (/^\d+$/.test(filteredArgs[0])) {
149
+ // mog <issue_number> — auto-detect repo
150
+ const detected = detectRepo();
151
+ if (!detected) {
152
+ log.die("Could not detect repo from git remote. Run from inside a git repo or use: mog <owner/repo> <issue_num>");
153
+ }
154
+ repo = detected;
155
+ issueNum = filteredArgs[0];
156
+ } else if (filteredArgs.length >= 2) {
157
+ // mog <owner/repo> <issue_number>
158
+ repo = filteredArgs[0];
159
+ issueNum = filteredArgs[1];
160
+ if (!/^\d+$/.test(issueNum)) {
161
+ log.die(`Invalid issue number: '${issueNum}'. Must be a positive integer.`);
162
+ }
163
+ } else {
164
+ console.log("Usage:");
165
+ console.log(" mog init — one-time setup (create sandbox & login)");
166
+ console.log(" mog <issue_num> — auto-detect repo from git remote");
167
+ console.log(" mog <owner/repo> <issue_num> — fetch issue, run Claude, open PR");
168
+ console.log(" mog list [--verbose] — list open issues (auto-detect repo)");
169
+ console.log(" mog <owner/repo> list [--verbose] — list open issues for a repo");
170
+ console.log();
171
+ console.log("Options:");
172
+ console.log(" --include <file> — copy a file into the worktree (repeatable)");
173
+ console.log();
174
+ console.log("Example:");
175
+ console.log(" mog init");
176
+ console.log(" mog 123");
177
+ console.log(" mog 123 --include .env");
178
+ console.log(" mog workingdevshero/automate-it 123");
179
+ console.log(" mog list");
180
+ console.log(" mog list --verbose");
181
+ return;
103
182
  }
104
183
 
105
184
  const parts = repo.split("/");
@@ -135,10 +214,21 @@ async function main() {
135
214
  reposDir, owner, repoName, defaultBranch, issueNum, issue.title
136
215
  );
137
216
 
217
+ // Copy included files into worktree
218
+ const copiedFiles: string[] = [];
219
+ for (const filePath of includeFiles) {
220
+ const basename = path.basename(filePath);
221
+ const dest = path.join(worktreeDir, basename);
222
+ fs.copyFileSync(filePath, dest);
223
+ copiedFiles.push(dest);
224
+ log.ok(`Included: ${basename}`);
225
+ }
226
+
138
227
  // Build prompts
139
228
  const planningPrompt = buildPlanningPrompt(repo, issueNum, issue);
140
229
  const buildingPromptFn = (remaining: string[], plan: string) =>
141
230
  buildBuildingPrompt(repo, issueNum, issue, remaining, plan);
231
+ const reviewPrompt = buildReviewPrompt(repo, issueNum, issue);
142
232
 
143
233
  // Run Claude in sandbox
144
234
  log.info("Launching Claude Code in sandbox...");
@@ -146,7 +236,18 @@ async function main() {
146
236
  log.info(`Worktree: ${worktreeDir}`);
147
237
  console.log();
148
238
 
149
- const summary = await runClaude(SANDBOX_NAME, worktreeDir, planningPrompt, buildingPromptFn);
239
+ const summary = await runClaude(SANDBOX_NAME, worktreeDir, planningPrompt, buildingPromptFn, reviewPrompt);
240
+
241
+ // Remove included files so they don't end up in the PR
242
+ for (const filePath of copiedFiles) {
243
+ try {
244
+ fs.unlinkSync(filePath);
245
+ // Unstage if Claude happened to git add it
246
+ Bun.spawnSync(["git", "rm", "--cached", "--ignore-unmatch", path.basename(filePath)], { cwd: worktreeDir });
247
+ } catch {
248
+ // File may already be gone
249
+ }
250
+ }
150
251
 
151
252
  // Push and create PR
152
253
  pushAndCreatePR(repo, worktreeDir, branchName, defaultBranch, issueNum, issue, summary);
@@ -201,29 +302,45 @@ function tryRecoverSandbox(reposDir: string): boolean {
201
302
  return true;
202
303
  }
203
304
 
204
- function buildPlanningPrompt(repo: string, issueNum: string, issue: { title: string; body: string; labels: string }): string {
205
- return `You are working on GitHub issue #${issueNum} for the repository ${repo}.
206
-
207
- ## Issue: ${issue.title}
305
+ function formatIssueContext(issueNum: string, issue: { title: string; body: string; labels: string; comments: string }): string {
306
+ let context = `## Issue: ${issue.title}
208
307
 
209
308
  ### Description
210
309
  ${issue.body}
211
310
 
212
311
  ### Labels
213
- ${issue.labels}
312
+ ${issue.labels}`;
313
+
314
+ if (issue.comments) {
315
+ context += `
316
+
317
+ ### Comments
318
+ ${issue.comments}`;
319
+ }
320
+
321
+ return context;
322
+ }
323
+
324
+ function buildPlanningPrompt(repo: string, issueNum: string, issue: { title: string; body: string; labels: string; comments: string }): string {
325
+ return `You are working on GitHub issue #${issueNum} for the repository ${repo}.
326
+
327
+ ${formatIssueContext(issueNum, issue)}
214
328
 
215
329
  ## Instructions
216
330
 
217
331
  Your job in this step is to **plan only** — do NOT implement anything and do NOT commit.
218
332
 
219
333
  1. Read and understand the codebase structure thoroughly.
220
- 2. Analyze the issue and break it down into small, atomic implementation tasks.
221
- 3. Create a file called \`IMPLEMENTATION_PLAN.md\` in the root of the repository with a checklist of tasks.
334
+ 2. **Search the entire codebase** for code related to the issue — look for similar patterns, duplicate logic, and any modules that handle the same concern. Use Grep and Glob liberally to find all relevant locations, not just the most obvious one.
335
+ 3. Analyze the issue and break it down into small, atomic implementation tasks.
336
+ 4. Create a file called \`IMPLEMENTATION_PLAN.md\` in the root of the repository with a checklist of tasks.
222
337
 
223
338
  The plan should:
224
339
  - Have 3-8 tasks (fewer for simple issues, more for complex ones)
225
340
  - Order tasks by dependency (implement foundations first)
226
341
  - Each task should be a single, atomic unit of work that results in one commit
342
+ - **Include tasks to update ALL locations** where the same pattern or concern exists — not just the most obvious one. If the same logic appears in multiple modules, the plan must cover all of them.
343
+ - If you find duplicate or near-duplicate logic across modules, include a task to consolidate it into a shared utility or function.
227
344
  - Use markdown checklist format: \`- [ ] Task description\`
228
345
 
229
346
  Example format:
@@ -242,7 +359,7 @@ Do NOT implement any code changes. Do NOT make any commits. Only create the plan
242
359
  function buildBuildingPrompt(
243
360
  repo: string,
244
361
  issueNum: string,
245
- issue: { title: string; body: string; labels: string },
362
+ issue: { title: string; body: string; labels: string; comments: string },
246
363
  remainingItems: string[],
247
364
  planContent: string,
248
365
  ): string {
@@ -250,19 +367,13 @@ function buildBuildingPrompt(
250
367
  if (remainingItems.length === 0 && !planContent) {
251
368
  return `You are working on GitHub issue #${issueNum} for the repository ${repo}.
252
369
 
253
- ## Issue: ${issue.title}
254
-
255
- ### Description
256
- ${issue.body}
257
-
258
- ### Labels
259
- ${issue.labels}
370
+ ${formatIssueContext(issueNum, issue)}
260
371
 
261
372
  ## Instructions
262
373
  1. Read and understand the codebase structure first.
263
374
  2. Implement the changes described in the issue above.
264
375
  3. Write clean, well-documented code that follows the existing project conventions.
265
- 4. Add or update tests if applicable.
376
+ 4. If the project has an existing test suite, add or update tests to cover the changes.
266
377
  5. Make sure the code builds/lints without errors if there's a build system.
267
378
  6. Commit your changes with a clear commit message referencing issue #${issueNum}.
268
379
 
@@ -274,13 +385,7 @@ a message like: "fix: <short description> (#${issueNum})"`;
274
385
 
275
386
  return `You are working on GitHub issue #${issueNum} for the repository ${repo}.
276
387
 
277
- ## Issue: ${issue.title}
278
-
279
- ### Description
280
- ${issue.body}
281
-
282
- ### Labels
283
- ${issue.labels}
388
+ ${formatIssueContext(issueNum, issue)}
284
389
 
285
390
  ## Current Implementation Plan
286
391
 
@@ -299,6 +404,31 @@ Rules:
299
404
  5. Do NOT work on any other tasks after committing.`;
300
405
  }
301
406
 
407
+ function buildReviewPrompt(
408
+ repo: string,
409
+ issueNum: string,
410
+ issue: { title: string; body: string; labels: string; comments: string },
411
+ ): string {
412
+ return `You are reviewing changes made for GitHub issue #${issueNum} in the repository ${repo}.
413
+
414
+ ${formatIssueContext(issueNum, issue)}
415
+
416
+ ## Instructions
417
+
418
+ All implementation tasks are complete. Your job is to **review the entire branch** for quality and completeness.
419
+
420
+ Run \`git diff main...HEAD\` (or the equivalent for the default branch) to see all changes made.
421
+
422
+ Check for:
423
+ 1. **Missed locations**: Search the codebase for similar patterns, logic, or code that handles the same concern as the changes. If the fix or feature was applied in one place but a similar pattern exists elsewhere, apply it there too.
424
+ 2. **Code duplication**: If the changes introduced logic that duplicates existing code (or if pre-existing duplication was missed), consolidate it into a shared function or utility.
425
+ 3. **Quality issues**: Look for missing edge cases, error handling gaps, or inconsistencies with the rest of the codebase.
426
+ 4. **Tests**: If the project has an existing test suite, verify that tests were added or updated to cover the changes. If not, add them. Do not create a test framework or test infrastructure from scratch.
427
+
428
+ If you find issues, fix them and commit each fix separately with a clear commit message referencing #${issueNum}.
429
+ If everything looks good, do nothing.`;
430
+ }
431
+
302
432
  main().catch((err) => {
303
433
  log.die(err.message);
304
434
  });
package/src/sandbox.ts CHANGED
@@ -76,6 +76,7 @@ export async function runClaude(
76
76
  worktreeDir: string,
77
77
  planningPrompt: string,
78
78
  buildingPromptFn: (remainingItems: string[], planContent: string) => string,
79
+ reviewPrompt?: string,
79
80
  ): Promise<string> {
80
81
  let lastResult = "";
81
82
 
@@ -152,7 +153,14 @@ export async function runClaude(
152
153
  }
153
154
  }
154
155
 
155
- // Phase 3 — Cleanup
156
+ // Phase 3 — Review
157
+ if (reviewPrompt) {
158
+ log.info("Phase 3: Reviewing changes for quality and completeness...");
159
+ const reviewResult = await execClaude(sandboxName, worktreeDir, ["-p", reviewPrompt]);
160
+ if (reviewResult) lastResult = reviewResult;
161
+ }
162
+
163
+ // Phase 4 — Cleanup
156
164
  cleanupPlanFile(sandboxName, worktreeDir);
157
165
 
158
166
  const finalPlan = readPlanFile(worktreeDir);
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,