@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobbyg603/mog",
3
- "version": "1.3.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,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(args[0])) {
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 = args[0];
134
- } else if (args.length >= 2) {
155
+ issueNum = filteredArgs[0];
156
+ } else if (filteredArgs.length >= 2) {
135
157
  // mog <owner/repo> <issue_number>
136
- repo = args[0];
137
- issueNum = args[1];
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 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}
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. 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.
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
- ## Issue: ${issue.title}
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. Add or update tests if applicable.
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
- ## Issue: ${issue.title}
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 — 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);