@bobbyg603/mog 1.4.0 → 1.5.1

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.4.0",
3
+ "version": "1.5.1",
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
@@ -76,6 +76,57 @@ export function listIssues(repo: string, verbose: boolean): void {
76
76
  }
77
77
  }
78
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
+
79
130
  export function pushAndCreatePR(
80
131
  repo: string,
81
132
  worktreeDir: string,
@@ -83,7 +134,8 @@ export function pushAndCreatePR(
83
134
  defaultBranch: string,
84
135
  issueNum: string,
85
136
  issue: Issue,
86
- summary?: string
137
+ summary?: string,
138
+ existingPR?: PRFeedback,
87
139
  ): void {
88
140
  // Check for unpushed commits or uncommitted changes
89
141
  const unpushed = Bun.spawnSync(["git", "log", `origin/${defaultBranch}..HEAD`, "--oneline"], { cwd: worktreeDir });
@@ -129,14 +181,28 @@ export function pushAndCreatePR(
129
181
  }
130
182
  }
131
183
 
132
- // Push
184
+ // Push (force-with-lease when updating an existing PR)
133
185
  log.info(`Pushing branch '${branchName}' to origin...`);
134
- 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 });
135
190
  if (push.exitCode !== 0) {
136
191
  log.die("Failed to push. Check your git credentials.");
137
192
  }
138
193
  log.ok("Branch pushed.");
139
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
+
140
206
  // Create PR
141
207
  log.info("Opening pull request...");
142
208
 
package/src/index.ts CHANGED
@@ -2,10 +2,11 @@
2
2
 
3
3
  import fs from "fs";
4
4
  import path from "path";
5
- import { fetchIssue, listIssues } from "./github";
5
+ import { fetchIssue, listIssues, fetchPRFeedback } from "./github";
6
6
  import { detectRepo, ensureRepo, createWorktree } from "./worktree";
7
7
  import { runClaude } from "./sandbox";
8
8
  import { pushAndCreatePR } from "./github";
9
+ import type { PRFeedback } from "./github";
9
10
  import { log } from "./log";
10
11
 
11
12
  const SANDBOX_NAME = "mog";
@@ -115,6 +116,7 @@ async function main() {
115
116
  console.log();
116
117
  console.log("Options:");
117
118
  console.log(" --include <file> — copy a file into the worktree (repeatable)");
119
+ console.log(" --fresh — ignore existing PR, start a brand new one");
118
120
  console.log();
119
121
  console.log("Example:");
120
122
  console.log(" mog init");
@@ -126,9 +128,10 @@ async function main() {
126
128
  return;
127
129
  }
128
130
 
129
- // Parse --include flags
131
+ // Parse --include and --fresh flags
130
132
  const includeFiles: string[] = [];
131
133
  const filteredArgs: string[] = [];
134
+ let fresh = false;
132
135
  for (let i = 0; i < args.length; i++) {
133
136
  if (args[i] === "--include" && i + 1 < args.length) {
134
137
  const filePath = path.resolve(args[i + 1]!);
@@ -137,6 +140,8 @@ async function main() {
137
140
  }
138
141
  includeFiles.push(filePath);
139
142
  i++; // skip the path argument
143
+ } else if (args[i] === "--fresh") {
144
+ fresh = true;
140
145
  } else {
141
146
  filteredArgs.push(args[i]!);
142
147
  }
@@ -170,6 +175,7 @@ async function main() {
170
175
  console.log();
171
176
  console.log("Options:");
172
177
  console.log(" --include <file> — copy a file into the worktree (repeatable)");
178
+ console.log(" --fresh — ignore existing PR, start a brand new one");
173
179
  console.log();
174
180
  console.log("Example:");
175
181
  console.log(" mog init");
@@ -210,10 +216,22 @@ async function main() {
210
216
  const { defaultBranch } = ensureRepo(repo, owner, repoName, reposDir);
211
217
  log.info(`Default branch: ${defaultBranch}`);
212
218
 
213
- const { worktreeDir, branchName } = createWorktree(
219
+ const { worktreeDir, branchName, reused } = createWorktree(
214
220
  reposDir, owner, repoName, defaultBranch, issueNum, issue.title
215
221
  );
216
222
 
223
+ // Check for existing PR (unless --fresh)
224
+ let existingPR: PRFeedback | undefined;
225
+ let isRetry = reused;
226
+ if (!fresh) {
227
+ const pr = fetchPRFeedback(repo, branchName);
228
+ if (pr) {
229
+ existingPR = pr;
230
+ isRetry = true;
231
+ log.ok(`Found existing PR #${pr.prNumber} — will include review feedback and update it.`);
232
+ }
233
+ }
234
+
217
235
  // Copy included files into worktree
218
236
  const copiedFiles: string[] = [];
219
237
  for (const filePath of includeFiles) {
@@ -225,7 +243,8 @@ async function main() {
225
243
  }
226
244
 
227
245
  // Build prompts
228
- const planningPrompt = buildPlanningPrompt(repo, issueNum, issue);
246
+ const prFeedback = existingPR?.reviews || "";
247
+ const planningPrompt = buildPlanningPrompt(repo, issueNum, issue, prFeedback, isRetry);
229
248
  const buildingPromptFn = (remaining: string[], plan: string) =>
230
249
  buildBuildingPrompt(repo, issueNum, issue, remaining, plan);
231
250
  const reviewPrompt = buildReviewPrompt(repo, issueNum, issue);
@@ -249,8 +268,8 @@ async function main() {
249
268
  }
250
269
  }
251
270
 
252
- // Push and create PR
253
- pushAndCreatePR(repo, worktreeDir, branchName, defaultBranch, issueNum, issue, summary);
271
+ // Push and create/update PR
272
+ pushAndCreatePR(repo, worktreeDir, branchName, defaultBranch, issueNum, issue, summary, existingPR);
254
273
  }
255
274
 
256
275
  function getReposDir(): string {
@@ -302,7 +321,7 @@ function tryRecoverSandbox(reposDir: string): boolean {
302
321
  return true;
303
322
  }
304
323
 
305
- function formatIssueContext(issueNum: string, issue: { title: string; body: string; labels: string; comments: string }): string {
324
+ function formatIssueContext(issueNum: string, issue: { title: string; body: string; labels: string; comments: string }, prFeedback?: string, isRetry?: boolean): string {
306
325
  let context = `## Issue: ${issue.title}
307
326
 
308
327
  ### Description
@@ -318,13 +337,29 @@ ${issue.labels}`;
318
337
  ${issue.comments}`;
319
338
  }
320
339
 
340
+ if (isRetry) {
341
+ context += `
342
+
343
+ ### Re-attempt Notice
344
+ **This is a re-attempt of a previous run.** The issue description, comments, or requirements may have been updated since the last attempt. Carefully review ALL context above to catch anything that may have changed.`;
345
+
346
+ if (prFeedback) {
347
+ context += `
348
+
349
+ ### Previous PR Review Feedback
350
+ Address the following reviewer feedback in your implementation:
351
+
352
+ ${prFeedback}`;
353
+ }
354
+ }
355
+
321
356
  return context;
322
357
  }
323
358
 
324
- function buildPlanningPrompt(repo: string, issueNum: string, issue: { title: string; body: string; labels: string; comments: string }): string {
359
+ function buildPlanningPrompt(repo: string, issueNum: string, issue: { title: string; body: string; labels: string; comments: string }, prFeedback?: string, isRetry?: boolean): string {
325
360
  return `You are working on GitHub issue #${issueNum} for the repository ${repo}.
326
361
 
327
- ${formatIssueContext(issueNum, issue)}
362
+ ${formatIssueContext(issueNum, issue, prFeedback, isRetry)}
328
363
 
329
364
  ## Instructions
330
365
 
package/src/worktree.ts CHANGED
@@ -73,7 +73,7 @@ export function createWorktree(
73
73
  defaultBranch: string,
74
74
  issueNum: string,
75
75
  issueTitle: string
76
- ): { worktreeDir: string; branchName: string } {
76
+ ): { worktreeDir: string; branchName: string; reused: boolean } {
77
77
  const safeTitle = issueTitle
78
78
  .toLowerCase()
79
79
  .replace(/[^a-z0-9]/g, "-")
@@ -87,7 +87,7 @@ export function createWorktree(
87
87
 
88
88
  if (fs.existsSync(worktreeDir)) {
89
89
  log.warn(`Worktree already exists at ${worktreeDir}, reusing.`);
90
- return { worktreeDir, branchName };
90
+ return { worktreeDir, branchName, reused: true };
91
91
  }
92
92
 
93
93
  log.info(`Creating worktree for branch '${branchName}'...`);
@@ -120,5 +120,5 @@ export function createWorktree(
120
120
  Bun.spawnSync(["git", "submodule", "update", "--init", "--recursive"], { cwd: worktreeDir });
121
121
 
122
122
  log.ok(`Worktree created at ${worktreeDir}`);
123
- return { worktreeDir, branchName };
123
+ return { worktreeDir, branchName, reused: false };
124
124
  }