@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 +1 -1
- package/src/github.ts +69 -3
- package/src/index.ts +44 -9
- package/src/worktree.ts +3 -3
package/package.json
CHANGED
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
|
|
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
|
|
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
|
}
|