Commit efea8a9ce1e6
dots/pi/agent/extensions/github/actions/pr.ts
@@ -25,6 +25,7 @@ import {
getReviewDecisionText,
formatRelativeDate,
execGh,
+ resolveGitCwd,
findPRTemplate,
} from "../utils";
@@ -301,6 +302,41 @@ export async function handlePRCreate(
const args = ["pr", "create", "--title", params.title];
+ // Auto-detect --head when not explicitly provided.
+ // This prevents "No commits between main and main" errors when the tool's
+ // cwd (ctx.cwd) is the main repo but work was done in a worktree on a
+ // different branch. We resolve the effective git cwd (worktree-aware) and
+ // read the branch from there. If it differs from the default branch we set
+ // --head so gh targets the correct branch even if the process cwd falls
+ // back to the main repo checkout.
+ if (!params.head) {
+ try {
+ const gitCwd = await resolveGitCwd(pi, ctx);
+ const branchResult = await pi.exec(
+ "git", ["rev-parse", "--abbrev-ref", "HEAD"],
+ { cwd: gitCwd, timeout: 5000 },
+ );
+ if (branchResult.code === 0) {
+ const branch = branchResult.stdout.trim();
+ const defaultBranches = ["main", "master"];
+ if (branch && !defaultBranches.includes(branch)) {
+ // Detect fork owner so --head is <owner>:<branch>
+ const ownerResult = await pi.exec(
+ "gh", ["repo", "view", "--json", "owner", "-q", ".owner.login"],
+ { cwd: gitCwd, timeout: 10000 },
+ );
+ if (ownerResult.code === 0 && ownerResult.stdout.trim()) {
+ args.push("--head", `${ownerResult.stdout.trim()}:${branch}`);
+ } else {
+ args.push("--head", branch);
+ }
+ }
+ }
+ } catch {
+ // Ignore detection errors — gh will use defaults
+ }
+ }
+
// Add template if found and body not explicitly provided
if (template && !params.body) {
args.push("--template", template);
dots/pi/agent/extensions/github/utils.ts
@@ -118,6 +118,34 @@ async function detectWorktreeContext(
} catch {
// Ignore errors in bash history scanning
}
+
+ // Strategy 4: Check `git worktree list` from the main repo for any linked
+ // worktrees under the conventional ~/.local/share/worktrees/ directory.
+ // Only use this if there is exactly one linked worktree (ambiguous otherwise).
+ try {
+ const wtResult = await pi.exec(
+ "git", ["worktree", "list", "--porcelain"],
+ { cwd: ctx.cwd, timeout: 5000 },
+ );
+ if (wtResult.code === 0) {
+ const lines = wtResult.stdout.split("\n");
+ const worktrees: string[] = [];
+ for (const line of lines) {
+ if (line.startsWith("worktree ")) {
+ const wtPath = line.slice("worktree ".length);
+ if (wtPath.includes("/.local/share/worktrees/")) {
+ worktrees.push(wtPath);
+ }
+ }
+ }
+ if (worktrees.length === 1) {
+ cachedWorktreeRoot = worktrees[0];
+ return cachedWorktreeRoot;
+ }
+ }
+ } catch {
+ // Ignore errors
+ }
return null;
}
@@ -130,6 +158,20 @@ export function resetGitRoot() {
cachedWorktreeRoot = null;
}
+/**
+ * Resolve the effective git working directory.
+ * Prefers worktree context, falls back to git root, then ctx.cwd.
+ */
+export async function resolveGitCwd(
+ pi: ExtensionAPI,
+ ctx: ExtensionContext,
+): Promise<string> {
+ const worktree = await detectWorktreeContext(pi, ctx);
+ if (worktree) return worktree;
+ const root = await findGitRoot(pi, ctx.cwd);
+ return root ?? ctx.cwd;
+}
+
/**
* Execute gh command with correct working directory.
* Automatically uses git repository root if available.
@@ -141,15 +183,7 @@ export async function execGh(
args: string[],
options?: { signal?: AbortSignal; timeout?: number }
) {
- // First check if we're working in a worktree context
- const worktree = await detectWorktreeContext(pi, ctx);
- if (worktree) {
- return await pi.exec("gh", args, { ...options, cwd: worktree });
- }
-
- // Fall back to regular git root detection
- const root = await findGitRoot(pi, ctx.cwd);
- const cwd = root ?? ctx.cwd;
+ const cwd = await resolveGitCwd(pi, ctx);
return await pi.exec("gh", args, { ...options, cwd });
}