Repository navigation
fix: auto-commit worktree changes and validate commits before PR creation #2006
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -208,6 +208,56 @@ Write a professional PR description. Output ONLY the Markdown body — no preamb | |
| } | ||
| } | ||
|
|
||
| // ============================================================================= | ||
| // Auto-Commit Uncommitted Changes | ||
| // ============================================================================= | ||
|
|
||
| /** | ||
| * Stage and commit any uncommitted changes in the worktree. | ||
| * Called before pushing to ensure the branch has commits to push. | ||
| * Returns an error string on failure, or undefined on success (or no changes). | ||
| */ | ||
| function autoCommitWorktreeChanges( | ||
| worktreePath: string, | ||
| gitPath: string, | ||
| specId: string, | ||
| ): string | undefined { | ||
| try { | ||
| // Check for uncommitted changes (staged or unstaged, tracked or untracked) | ||
| const status = execFileSync( | ||
| gitPath, | ||
| ['status', '--porcelain'], | ||
| { cwd: worktreePath, encoding: 'utf-8', stdio: 'pipe' }, | ||
| ).trim(); | ||
|
|
||
| if (!status) { | ||
| // No uncommitted changes — nothing to do | ||
| return undefined; | ||
| } | ||
|
|
||
| // Stage all changes | ||
| execFileSync( | ||
| gitPath, | ||
| ['add', '.'], | ||
| { cwd: worktreePath, encoding: 'utf-8', stdio: 'pipe' }, | ||
| ); | ||
|
|
||
| // Commit with a descriptive message | ||
| execFileSync( | ||
| gitPath, | ||
| ['commit', '-m', `auto-claude: implement ${specId}`], | ||
| { cwd: worktreePath, encoding: 'utf-8', stdio: 'pipe' }, | ||
| ); | ||
|
|
||
| return undefined; | ||
| } catch (err: unknown) { | ||
| const stderr = err instanceof Error && 'stderr' in err | ||
| ? String((err as NodeJS.ErrnoException & { stderr?: string }).stderr) | ||
| : String(err); | ||
| return stderr || 'Auto-commit failed'; | ||
|
Comment on lines
+247
to
+257
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bug: The Suggested FixAt the call site for Prompt for AI AgentDid we get this right? 👍 / 👎 to inform future reviews. |
||
| } | ||
| } | ||
|
|
||
| // ============================================================================= | ||
| // Push Branch | ||
| // ============================================================================= | ||
|
|
@@ -296,7 +346,17 @@ export async function createPR(config: CreatePRConfig): Promise<CreatePRResult> | |
| thinkingLevel = 'low', | ||
| } = config; | ||
|
|
||
| // Step 1: Push the branch to origin | ||
| // Strip remote prefix from base branch once — used in all steps below | ||
| const effectiveBase = baseBranch.startsWith('origin/') | ||
| ? baseBranch.slice('origin/'.length) | ||
| : baseBranch; | ||
|
|
||
| // Step 1a: Auto-commit any uncommitted changes in the worktree before pushing. | ||
| // This handles the case where the agent wrote files but didn't run `git commit`. | ||
| // A failure here is non-fatal — we still attempt the push in case prior commits exist. | ||
| autoCommitWorktreeChanges(worktreePath, gitPath, specId); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The return value of |
||
|
|
||
| // Step 1b: Push the branch to origin | ||
| const pushError = pushBranch(worktreePath, gitPath, branchName); | ||
| if (pushError) { | ||
| // If it looks like the branch is already up-to-date, don't bail | ||
|
|
@@ -307,6 +367,26 @@ export async function createPR(config: CreatePRConfig): Promise<CreatePRResult> | |
| } | ||
| } | ||
|
|
||
| // Step 1c: Verify there is at least one commit ahead of the base branch. | ||
| // Without this check, `gh pr create` fails with a confusing GraphQL error. | ||
| try { | ||
| const commitCount = execFileSync( | ||
| gitPath, | ||
| ['rev-list', '--count', `origin/${effectiveBase}..HEAD`], | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The commit count check hardcodes the remote name as |
||
| { cwd: worktreePath, encoding: 'utf-8', stdio: 'pipe' }, | ||
| ).trim(); | ||
|
Comment on lines
+373
to
+377
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
||
| if (commitCount === '0') { | ||
| return { | ||
| success: false, | ||
| error: `No commits found between '${effectiveBase}' and '${branchName}'. ` + | ||
| 'The agent may not have committed any changes. ' + | ||
| 'Please verify the implementation completed successfully before creating a PR.', | ||
| }; | ||
| } | ||
| } catch { | ||
| // Unable to count commits — proceed and let `gh pr create` surface the error | ||
| } | ||
|
|
||
| // Step 2: Gather context for AI description | ||
| const { diffSummary, commitLog } = gatherPRContext(worktreePath, gitPath, baseBranch); | ||
|
Comment on lines
390
to
391
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Inconsistent use of This call passes the original Pass 🔧 Proposed fix // Step 2: Gather context for AI description
- const { diffSummary, commitLog } = gatherPRContext(worktreePath, gitPath, baseBranch);
+ const { diffSummary, commitLog } = gatherPRContext(worktreePath, gitPath, effectiveBase);🤖 Prompt for AI Agents |
||
|
|
||
|
|
@@ -324,12 +404,7 @@ export async function createPR(config: CreatePRConfig): Promise<CreatePRResult> | |
|
|
||
| const prBody = aiBody || extractSpecSummary(projectDir, specId); | ||
|
|
||
| // Step 4: Strip remote prefix from base branch if present | ||
| const effectiveBase = baseBranch.startsWith('origin/') | ||
| ? baseBranch.slice('origin/'.length) | ||
| : baseBranch; | ||
|
|
||
| // Step 5: Build gh pr create command | ||
| // Step 4: Build gh pr create command | ||
| const ghArgs = [ | ||
| 'pr', 'create', | ||
| '--base', effectiveBase, | ||
|
|
@@ -342,7 +417,7 @@ export async function createPR(config: CreatePRConfig): Promise<CreatePRResult> | |
| ghArgs.push('--draft'); | ||
| } | ||
|
|
||
| // Step 6: Execute gh pr create with retry on network errors | ||
| // Step 5: Execute gh pr create with retry on network errors | ||
| for (let attempt = 0; attempt < 3; attempt++) { | ||
| try { | ||
| const output = execFileSync(ghPath, ghArgs, { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
Consider security implications of staging all files.
Using
git add .stages all unignored files in the worktree, which could include files the agent created that weren't intended for commit (e.g., debug files, downloaded dependencies, or files with sensitive data that aren't in.gitignore).Consider a more targeted approach using
git add -u(staged only tracked files) or parsing the status output to selectively stage expected file patterns.🤖 Prompt for AI Agents