@bobbyg603/mog 1.3.0 → 1.5.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobbyg603/mog",
3
- "version": "1.3.0",
3
+ "version": "1.5.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,10 +22,15 @@ 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
 
@@ -70,6 +76,57 @@ export function listIssues(repo: string, verbose: boolean): void {
70
76
  }
71
77
  }
72
78
 
79
+ export interface PRFeedback {
80
+ prNumber: number;
81
+ prUrl: string;
82
+ reviews: string;
83
+ }
84
+
85
+ export function fetchPRFeedback(repo: string, branchName: string): PRFeedback | null {
86
+ const proc = Bun.spawnSync([
87
+ "gh", "pr", "list",
88
+ "--repo", repo,
89
+ "--head", branchName,
90
+ "--state", "open",
91
+ "--json", "number,url",
92
+ ]);
93
+
94
+ if (proc.exitCode !== 0) return null;
95
+
96
+ const prs = JSON.parse(proc.stdout.toString());
97
+ if (prs.length === 0) return null;
98
+
99
+ const prNumber = prs[0].number;
100
+ const prUrl = prs[0].url;
101
+
102
+ // Fetch review comments
103
+ const reviewProc = Bun.spawnSync([
104
+ "gh", "pr", "view", String(prNumber),
105
+ "--repo", repo,
106
+ "--json", "reviews,comments",
107
+ ]);
108
+
109
+ let reviews = "";
110
+ if (reviewProc.exitCode === 0) {
111
+ const data = JSON.parse(reviewProc.stdout.toString());
112
+
113
+ const reviewEntries = (data.reviews || [])
114
+ .filter((r: { body: string }) => r.body?.trim())
115
+ .map((r: { author: { login: string }; state: string; body: string }) =>
116
+ `**@${r.author.login}** (${r.state}):\n${r.body}`
117
+ );
118
+
119
+ const commentEntries = (data.comments || [])
120
+ .map((c: { author: { login: string }; body: string }) =>
121
+ `**@${c.author.login}:**\n${c.body}`
122
+ );
123
+
124
+ reviews = [...reviewEntries, ...commentEntries].join("\n\n");
125
+ }
126
+
127
+ return { prNumber, prUrl, reviews };
128
+ }
129
+
73
130
  export function pushAndCreatePR(
74
131
  repo: string,
75
132
  worktreeDir: string,
@@ -77,7 +134,8 @@ export function pushAndCreatePR(
77
134
  defaultBranch: string,
78
135
  issueNum: string,
79
136
  issue: Issue,
80
- summary?: string
137
+ summary?: string,
138
+ existingPR?: PRFeedback,
81
139
  ): void {
82
140
  // Check for unpushed commits or uncommitted changes
83
141
  const unpushed = Bun.spawnSync(["git", "log", `origin/${defaultBranch}..HEAD`, "--oneline"], { cwd: worktreeDir });
@@ -107,14 +165,44 @@ export function pushAndCreatePR(
107
165
  }
108
166
  }
109
167
 
110
- // Push
168
+ // Squash all commits into one
169
+ const commitCount = Bun.spawnSync(["git", "rev-list", "--count", `${defaultBranch}..HEAD`], { cwd: worktreeDir });
170
+ const count = parseInt(commitCount.stdout.toString().trim(), 10) || 0;
171
+ if (count > 1) {
172
+ log.info(`Squashing ${count} commits into one...`);
173
+ const prefix = issue.labels.includes("enhancement") || issue.labels.includes("feature") ? "feat" : "fix";
174
+ const squash = Bun.spawnSync(["git", "reset", "--soft", defaultBranch], { cwd: worktreeDir });
175
+ if (squash.exitCode === 0) {
176
+ const msg = `${prefix}: ${issue.title.toLowerCase()} (#${issueNum})`;
177
+ Bun.spawnSync(["git", "commit", "-m", msg], { cwd: worktreeDir });
178
+ log.ok("Commits squashed.");
179
+ } else {
180
+ log.warn("Failed to squash — pushing individual commits instead.");
181
+ }
182
+ }
183
+
184
+ // Push (force-with-lease when updating an existing PR)
111
185
  log.info(`Pushing branch '${branchName}' to origin...`);
112
- const push = Bun.spawnSync(["git", "push", "-u", "origin", branchName], { cwd: worktreeDir });
186
+ const pushArgs = existingPR
187
+ ? ["git", "push", "--force-with-lease", "-u", "origin", branchName]
188
+ : ["git", "push", "-u", "origin", branchName];
189
+ const push = Bun.spawnSync(pushArgs, { cwd: worktreeDir });
113
190
  if (push.exitCode !== 0) {
114
191
  log.die("Failed to push. Check your git credentials.");
115
192
  }
116
193
  log.ok("Branch pushed.");
117
194
 
195
+ if (existingPR) {
196
+ // Update existing PR
197
+ log.ok("Existing PR updated!");
198
+ console.log(`\x1b[0;32m${existingPR.prUrl}\x1b[0m`);
199
+ console.log();
200
+ log.ok(`All done! Issue #${issueNum} → Branch '${branchName}' → PR updated.`);
201
+ log.info(`Worktree: ${worktreeDir}`);
202
+ log.info(`To clean up the worktree later: git worktree remove ${worktreeDir}`);
203
+ return;
204
+ }
205
+
118
206
  // Create PR
119
207
  log.info("Opening pull request...");
120
208
 
package/src/index.ts CHANGED
@@ -1,9 +1,12 @@
1
1
  #!/usr/bin/env bun
2
2
 
3
- import { fetchIssue, listIssues } from "./github";
3
+ import fs from "fs";
4
+ import path from "path";
5
+ import { fetchIssue, listIssues, fetchPRFeedback } from "./github";
4
6
  import { detectRepo, ensureRepo, createWorktree } from "./worktree";
5
7
  import { runClaude } from "./sandbox";
6
8
  import { pushAndCreatePR } from "./github";
9
+ import type { PRFeedback } from "./github";
7
10
  import { log } from "./log";
8
11
 
9
12
  const SANDBOX_NAME = "mog";
@@ -111,30 +114,54 @@ async function main() {
111
114
  console.log(" mog list [--verbose] — list open issues (auto-detect repo)");
112
115
  console.log(" mog <owner/repo> list [--verbose] — list open issues for a repo");
113
116
  console.log();
117
+ console.log("Options:");
118
+ console.log(" --include <file> — copy a file into the worktree (repeatable)");
119
+ console.log(" --fresh — ignore existing PR, start a brand new one");
120
+ console.log();
114
121
  console.log("Example:");
115
122
  console.log(" mog init");
116
123
  console.log(" mog 123");
124
+ console.log(" mog 123 --include .env");
117
125
  console.log(" mog workingdevshero/automate-it 123");
118
126
  console.log(" mog list");
119
127
  console.log(" mog list --verbose");
120
128
  return;
121
129
  }
122
130
 
131
+ // Parse --include and --fresh flags
132
+ const includeFiles: string[] = [];
133
+ const filteredArgs: string[] = [];
134
+ let fresh = false;
135
+ for (let i = 0; i < args.length; i++) {
136
+ if (args[i] === "--include" && i + 1 < args.length) {
137
+ const filePath = path.resolve(args[i + 1]!);
138
+ if (!fs.existsSync(filePath)) {
139
+ log.die(`Include file not found: ${args[i + 1]}`);
140
+ }
141
+ includeFiles.push(filePath);
142
+ i++; // skip the path argument
143
+ } else if (args[i] === "--fresh") {
144
+ fresh = true;
145
+ } else {
146
+ filteredArgs.push(args[i]!);
147
+ }
148
+ }
149
+
123
150
  let repo: string;
124
151
  let issueNum: string;
125
152
 
126
- if (/^\d+$/.test(args[0])) {
153
+ if (/^\d+$/.test(filteredArgs[0])) {
127
154
  // mog <issue_number> — auto-detect repo
128
155
  const detected = detectRepo();
129
156
  if (!detected) {
130
157
  log.die("Could not detect repo from git remote. Run from inside a git repo or use: mog <owner/repo> <issue_num>");
131
158
  }
132
159
  repo = detected;
133
- issueNum = args[0];
134
- } else if (args.length >= 2) {
160
+ issueNum = filteredArgs[0];
161
+ } else if (filteredArgs.length >= 2) {
135
162
  // mog <owner/repo> <issue_number>
136
- repo = args[0];
137
- issueNum = args[1];
163
+ repo = filteredArgs[0];
164
+ issueNum = filteredArgs[1];
138
165
  if (!/^\d+$/.test(issueNum)) {
139
166
  log.die(`Invalid issue number: '${issueNum}'. Must be a positive integer.`);
140
167
  }
@@ -146,9 +173,14 @@ async function main() {
146
173
  console.log(" mog list [--verbose] — list open issues (auto-detect repo)");
147
174
  console.log(" mog <owner/repo> list [--verbose] — list open issues for a repo");
148
175
  console.log();
176
+ console.log("Options:");
177
+ console.log(" --include <file> — copy a file into the worktree (repeatable)");
178
+ console.log(" --fresh — ignore existing PR, start a brand new one");
179
+ console.log();
149
180
  console.log("Example:");
150
181
  console.log(" mog init");
151
182
  console.log(" mog 123");
183
+ console.log(" mog 123 --include .env");
152
184
  console.log(" mog workingdevshero/automate-it 123");
153
185
  console.log(" mog list");
154
186
  console.log(" mog list --verbose");
@@ -188,10 +220,32 @@ async function main() {
188
220
  reposDir, owner, repoName, defaultBranch, issueNum, issue.title
189
221
  );
190
222
 
223
+ // Check for existing PR (unless --fresh)
224
+ let existingPR: PRFeedback | undefined;
225
+ if (!fresh) {
226
+ const pr = fetchPRFeedback(repo, branchName);
227
+ if (pr) {
228
+ existingPR = pr;
229
+ log.ok(`Found existing PR #${pr.prNumber} — will include review feedback and update it.`);
230
+ }
231
+ }
232
+
233
+ // Copy included files into worktree
234
+ const copiedFiles: string[] = [];
235
+ for (const filePath of includeFiles) {
236
+ const basename = path.basename(filePath);
237
+ const dest = path.join(worktreeDir, basename);
238
+ fs.copyFileSync(filePath, dest);
239
+ copiedFiles.push(dest);
240
+ log.ok(`Included: ${basename}`);
241
+ }
242
+
191
243
  // Build prompts
192
- const planningPrompt = buildPlanningPrompt(repo, issueNum, issue);
244
+ const prFeedback = existingPR?.reviews || "";
245
+ const planningPrompt = buildPlanningPrompt(repo, issueNum, issue, prFeedback);
193
246
  const buildingPromptFn = (remaining: string[], plan: string) =>
194
247
  buildBuildingPrompt(repo, issueNum, issue, remaining, plan);
248
+ const reviewPrompt = buildReviewPrompt(repo, issueNum, issue);
195
249
 
196
250
  // Run Claude in sandbox
197
251
  log.info("Launching Claude Code in sandbox...");
@@ -199,10 +253,21 @@ async function main() {
199
253
  log.info(`Worktree: ${worktreeDir}`);
200
254
  console.log();
201
255
 
202
- const summary = await runClaude(SANDBOX_NAME, worktreeDir, planningPrompt, buildingPromptFn);
256
+ const summary = await runClaude(SANDBOX_NAME, worktreeDir, planningPrompt, buildingPromptFn, reviewPrompt);
257
+
258
+ // Remove included files so they don't end up in the PR
259
+ for (const filePath of copiedFiles) {
260
+ try {
261
+ fs.unlinkSync(filePath);
262
+ // Unstage if Claude happened to git add it
263
+ Bun.spawnSync(["git", "rm", "--cached", "--ignore-unmatch", path.basename(filePath)], { cwd: worktreeDir });
264
+ } catch {
265
+ // File may already be gone
266
+ }
267
+ }
203
268
 
204
- // Push and create PR
205
- pushAndCreatePR(repo, worktreeDir, branchName, defaultBranch, issueNum, issue, summary);
269
+ // Push and create/update PR
270
+ pushAndCreatePR(repo, worktreeDir, branchName, defaultBranch, issueNum, issue, summary, existingPR);
206
271
  }
207
272
 
208
273
  function getReposDir(): string {
@@ -254,29 +319,54 @@ function tryRecoverSandbox(reposDir: string): boolean {
254
319
  return true;
255
320
  }
256
321
 
257
- function buildPlanningPrompt(repo: string, issueNum: string, issue: { title: string; body: string; labels: string }): string {
258
- return `You are working on GitHub issue #${issueNum} for the repository ${repo}.
259
-
260
- ## Issue: ${issue.title}
322
+ function formatIssueContext(issueNum: string, issue: { title: string; body: string; labels: string; comments: string }, prFeedback?: string): string {
323
+ let context = `## Issue: ${issue.title}
261
324
 
262
325
  ### Description
263
326
  ${issue.body}
264
327
 
265
328
  ### Labels
266
- ${issue.labels}
329
+ ${issue.labels}`;
330
+
331
+ if (issue.comments) {
332
+ context += `
333
+
334
+ ### Comments
335
+ ${issue.comments}`;
336
+ }
337
+
338
+ if (prFeedback) {
339
+ context += `
340
+
341
+ ### Previous PR Review Feedback
342
+ A previous attempt at this issue was reviewed. Address the following feedback in your implementation:
343
+
344
+ ${prFeedback}`;
345
+ }
346
+
347
+ return context;
348
+ }
349
+
350
+ function buildPlanningPrompt(repo: string, issueNum: string, issue: { title: string; body: string; labels: string; comments: string }, prFeedback?: string): string {
351
+ return `You are working on GitHub issue #${issueNum} for the repository ${repo}.
352
+
353
+ ${formatIssueContext(issueNum, issue, prFeedback)}
267
354
 
268
355
  ## Instructions
269
356
 
270
357
  Your job in this step is to **plan only** — do NOT implement anything and do NOT commit.
271
358
 
272
359
  1. Read and understand the codebase structure thoroughly.
273
- 2. Analyze the issue and break it down into small, atomic implementation tasks.
274
- 3. Create a file called \`IMPLEMENTATION_PLAN.md\` in the root of the repository with a checklist of tasks.
360
+ 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.
361
+ 3. Analyze the issue and break it down into small, atomic implementation tasks.
362
+ 4. Create a file called \`IMPLEMENTATION_PLAN.md\` in the root of the repository with a checklist of tasks.
275
363
 
276
364
  The plan should:
277
365
  - Have 3-8 tasks (fewer for simple issues, more for complex ones)
278
366
  - Order tasks by dependency (implement foundations first)
279
367
  - Each task should be a single, atomic unit of work that results in one commit
368
+ - **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.
369
+ - If you find duplicate or near-duplicate logic across modules, include a task to consolidate it into a shared utility or function.
280
370
  - Use markdown checklist format: \`- [ ] Task description\`
281
371
 
282
372
  Example format:
@@ -295,7 +385,7 @@ Do NOT implement any code changes. Do NOT make any commits. Only create the plan
295
385
  function buildBuildingPrompt(
296
386
  repo: string,
297
387
  issueNum: string,
298
- issue: { title: string; body: string; labels: string },
388
+ issue: { title: string; body: string; labels: string; comments: string },
299
389
  remainingItems: string[],
300
390
  planContent: string,
301
391
  ): string {
@@ -303,19 +393,13 @@ function buildBuildingPrompt(
303
393
  if (remainingItems.length === 0 && !planContent) {
304
394
  return `You are working on GitHub issue #${issueNum} for the repository ${repo}.
305
395
 
306
- ## Issue: ${issue.title}
307
-
308
- ### Description
309
- ${issue.body}
310
-
311
- ### Labels
312
- ${issue.labels}
396
+ ${formatIssueContext(issueNum, issue)}
313
397
 
314
398
  ## Instructions
315
399
  1. Read and understand the codebase structure first.
316
400
  2. Implement the changes described in the issue above.
317
401
  3. Write clean, well-documented code that follows the existing project conventions.
318
- 4. Add or update tests if applicable.
402
+ 4. If the project has an existing test suite, add or update tests to cover the changes.
319
403
  5. Make sure the code builds/lints without errors if there's a build system.
320
404
  6. Commit your changes with a clear commit message referencing issue #${issueNum}.
321
405
 
@@ -327,13 +411,7 @@ a message like: "fix: <short description> (#${issueNum})"`;
327
411
 
328
412
  return `You are working on GitHub issue #${issueNum} for the repository ${repo}.
329
413
 
330
- ## Issue: ${issue.title}
331
-
332
- ### Description
333
- ${issue.body}
334
-
335
- ### Labels
336
- ${issue.labels}
414
+ ${formatIssueContext(issueNum, issue)}
337
415
 
338
416
  ## Current Implementation Plan
339
417
 
@@ -352,6 +430,31 @@ Rules:
352
430
  5. Do NOT work on any other tasks after committing.`;
353
431
  }
354
432
 
433
+ function buildReviewPrompt(
434
+ repo: string,
435
+ issueNum: string,
436
+ issue: { title: string; body: string; labels: string; comments: string },
437
+ ): string {
438
+ return `You are reviewing changes made for GitHub issue #${issueNum} in the repository ${repo}.
439
+
440
+ ${formatIssueContext(issueNum, issue)}
441
+
442
+ ## Instructions
443
+
444
+ All implementation tasks are complete. Your job is to **review the entire branch** for quality and completeness.
445
+
446
+ Run \`git diff main...HEAD\` (or the equivalent for the default branch) to see all changes made.
447
+
448
+ Check for:
449
+ 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.
450
+ 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.
451
+ 3. **Quality issues**: Look for missing edge cases, error handling gaps, or inconsistencies with the rest of the codebase.
452
+ 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.
453
+
454
+ If you find issues, fix them and commit each fix separately with a clear commit message referencing #${issueNum}.
455
+ If everything looks good, do nothing.`;
456
+ }
457
+
355
458
  main().catch((err) => {
356
459
  log.die(err.message);
357
460
  });
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);