@bobbyg603/mog 1.3.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/package.json +1 -1
- package/src/github.ts +23 -1
- package/src/index.ts +106 -29
- package/src/sandbox.ts +9 -1
package/package.json
CHANGED
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
|
|
|
@@ -107,6 +113,22 @@ export function pushAndCreatePR(
|
|
|
107
113
|
}
|
|
108
114
|
}
|
|
109
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
|
+
|
|
110
132
|
// Push
|
|
111
133
|
log.info(`Pushing branch '${branchName}' to origin...`);
|
|
112
134
|
const push = Bun.spawnSync(["git", "push", "-u", "origin", branchName], { cwd: worktreeDir });
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import path from "path";
|
|
3
5
|
import { fetchIssue, listIssues } from "./github";
|
|
4
6
|
import { detectRepo, ensureRepo, createWorktree } from "./worktree";
|
|
5
7
|
import { runClaude } from "./sandbox";
|
|
@@ -111,30 +113,50 @@ async function main() {
|
|
|
111
113
|
console.log(" mog list [--verbose] — list open issues (auto-detect repo)");
|
|
112
114
|
console.log(" mog <owner/repo> list [--verbose] — list open issues for a repo");
|
|
113
115
|
console.log();
|
|
116
|
+
console.log("Options:");
|
|
117
|
+
console.log(" --include <file> — copy a file into the worktree (repeatable)");
|
|
118
|
+
console.log();
|
|
114
119
|
console.log("Example:");
|
|
115
120
|
console.log(" mog init");
|
|
116
121
|
console.log(" mog 123");
|
|
122
|
+
console.log(" mog 123 --include .env");
|
|
117
123
|
console.log(" mog workingdevshero/automate-it 123");
|
|
118
124
|
console.log(" mog list");
|
|
119
125
|
console.log(" mog list --verbose");
|
|
120
126
|
return;
|
|
121
127
|
}
|
|
122
128
|
|
|
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
|
+
}
|
|
144
|
+
|
|
123
145
|
let repo: string;
|
|
124
146
|
let issueNum: string;
|
|
125
147
|
|
|
126
|
-
if (/^\d+$/.test(
|
|
148
|
+
if (/^\d+$/.test(filteredArgs[0])) {
|
|
127
149
|
// mog <issue_number> — auto-detect repo
|
|
128
150
|
const detected = detectRepo();
|
|
129
151
|
if (!detected) {
|
|
130
152
|
log.die("Could not detect repo from git remote. Run from inside a git repo or use: mog <owner/repo> <issue_num>");
|
|
131
153
|
}
|
|
132
154
|
repo = detected;
|
|
133
|
-
issueNum =
|
|
134
|
-
} else if (
|
|
155
|
+
issueNum = filteredArgs[0];
|
|
156
|
+
} else if (filteredArgs.length >= 2) {
|
|
135
157
|
// mog <owner/repo> <issue_number>
|
|
136
|
-
repo =
|
|
137
|
-
issueNum =
|
|
158
|
+
repo = filteredArgs[0];
|
|
159
|
+
issueNum = filteredArgs[1];
|
|
138
160
|
if (!/^\d+$/.test(issueNum)) {
|
|
139
161
|
log.die(`Invalid issue number: '${issueNum}'. Must be a positive integer.`);
|
|
140
162
|
}
|
|
@@ -146,9 +168,13 @@ async function main() {
|
|
|
146
168
|
console.log(" mog list [--verbose] — list open issues (auto-detect repo)");
|
|
147
169
|
console.log(" mog <owner/repo> list [--verbose] — list open issues for a repo");
|
|
148
170
|
console.log();
|
|
171
|
+
console.log("Options:");
|
|
172
|
+
console.log(" --include <file> — copy a file into the worktree (repeatable)");
|
|
173
|
+
console.log();
|
|
149
174
|
console.log("Example:");
|
|
150
175
|
console.log(" mog init");
|
|
151
176
|
console.log(" mog 123");
|
|
177
|
+
console.log(" mog 123 --include .env");
|
|
152
178
|
console.log(" mog workingdevshero/automate-it 123");
|
|
153
179
|
console.log(" mog list");
|
|
154
180
|
console.log(" mog list --verbose");
|
|
@@ -188,10 +214,21 @@ async function main() {
|
|
|
188
214
|
reposDir, owner, repoName, defaultBranch, issueNum, issue.title
|
|
189
215
|
);
|
|
190
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
|
+
|
|
191
227
|
// Build prompts
|
|
192
228
|
const planningPrompt = buildPlanningPrompt(repo, issueNum, issue);
|
|
193
229
|
const buildingPromptFn = (remaining: string[], plan: string) =>
|
|
194
230
|
buildBuildingPrompt(repo, issueNum, issue, remaining, plan);
|
|
231
|
+
const reviewPrompt = buildReviewPrompt(repo, issueNum, issue);
|
|
195
232
|
|
|
196
233
|
// Run Claude in sandbox
|
|
197
234
|
log.info("Launching Claude Code in sandbox...");
|
|
@@ -199,7 +236,18 @@ async function main() {
|
|
|
199
236
|
log.info(`Worktree: ${worktreeDir}`);
|
|
200
237
|
console.log();
|
|
201
238
|
|
|
202
|
-
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
|
+
}
|
|
203
251
|
|
|
204
252
|
// Push and create PR
|
|
205
253
|
pushAndCreatePR(repo, worktreeDir, branchName, defaultBranch, issueNum, issue, summary);
|
|
@@ -254,29 +302,45 @@ function tryRecoverSandbox(reposDir: string): boolean {
|
|
|
254
302
|
return true;
|
|
255
303
|
}
|
|
256
304
|
|
|
257
|
-
function
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
## Issue: ${issue.title}
|
|
305
|
+
function formatIssueContext(issueNum: string, issue: { title: string; body: string; labels: string; comments: string }): string {
|
|
306
|
+
let context = `## Issue: ${issue.title}
|
|
261
307
|
|
|
262
308
|
### Description
|
|
263
309
|
${issue.body}
|
|
264
310
|
|
|
265
311
|
### Labels
|
|
266
|
-
${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)}
|
|
267
328
|
|
|
268
329
|
## Instructions
|
|
269
330
|
|
|
270
331
|
Your job in this step is to **plan only** — do NOT implement anything and do NOT commit.
|
|
271
332
|
|
|
272
333
|
1. Read and understand the codebase structure thoroughly.
|
|
273
|
-
2.
|
|
274
|
-
3.
|
|
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.
|
|
275
337
|
|
|
276
338
|
The plan should:
|
|
277
339
|
- Have 3-8 tasks (fewer for simple issues, more for complex ones)
|
|
278
340
|
- Order tasks by dependency (implement foundations first)
|
|
279
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.
|
|
280
344
|
- Use markdown checklist format: \`- [ ] Task description\`
|
|
281
345
|
|
|
282
346
|
Example format:
|
|
@@ -295,7 +359,7 @@ Do NOT implement any code changes. Do NOT make any commits. Only create the plan
|
|
|
295
359
|
function buildBuildingPrompt(
|
|
296
360
|
repo: string,
|
|
297
361
|
issueNum: string,
|
|
298
|
-
issue: { title: string; body: string; labels: string },
|
|
362
|
+
issue: { title: string; body: string; labels: string; comments: string },
|
|
299
363
|
remainingItems: string[],
|
|
300
364
|
planContent: string,
|
|
301
365
|
): string {
|
|
@@ -303,19 +367,13 @@ function buildBuildingPrompt(
|
|
|
303
367
|
if (remainingItems.length === 0 && !planContent) {
|
|
304
368
|
return `You are working on GitHub issue #${issueNum} for the repository ${repo}.
|
|
305
369
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
### Description
|
|
309
|
-
${issue.body}
|
|
310
|
-
|
|
311
|
-
### Labels
|
|
312
|
-
${issue.labels}
|
|
370
|
+
${formatIssueContext(issueNum, issue)}
|
|
313
371
|
|
|
314
372
|
## Instructions
|
|
315
373
|
1. Read and understand the codebase structure first.
|
|
316
374
|
2. Implement the changes described in the issue above.
|
|
317
375
|
3. Write clean, well-documented code that follows the existing project conventions.
|
|
318
|
-
4.
|
|
376
|
+
4. If the project has an existing test suite, add or update tests to cover the changes.
|
|
319
377
|
5. Make sure the code builds/lints without errors if there's a build system.
|
|
320
378
|
6. Commit your changes with a clear commit message referencing issue #${issueNum}.
|
|
321
379
|
|
|
@@ -327,13 +385,7 @@ a message like: "fix: <short description> (#${issueNum})"`;
|
|
|
327
385
|
|
|
328
386
|
return `You are working on GitHub issue #${issueNum} for the repository ${repo}.
|
|
329
387
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
### Description
|
|
333
|
-
${issue.body}
|
|
334
|
-
|
|
335
|
-
### Labels
|
|
336
|
-
${issue.labels}
|
|
388
|
+
${formatIssueContext(issueNum, issue)}
|
|
337
389
|
|
|
338
390
|
## Current Implementation Plan
|
|
339
391
|
|
|
@@ -352,6 +404,31 @@ Rules:
|
|
|
352
404
|
5. Do NOT work on any other tasks after committing.`;
|
|
353
405
|
}
|
|
354
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
|
+
|
|
355
432
|
main().catch((err) => {
|
|
356
433
|
log.die(err.message);
|
|
357
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 —
|
|
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);
|