# fix\(server\): fall back when the recorded PR base is unavailable remotely · PR \#22942 · pingdotgg/t3code

[View on GitCafe](https://git.cafe/pingdotgg/t3code/pull/22942)

Repository: [pingdotgg/t3code](https://git.cafe/pingdotgg/t3code)

Visibility: public

State: open

Source repository: pingdotgg/t3code

Source branch: fix/issue-22920

Target branch: main

Head commit: e90dbdbae28233cca48fd270c0be43a3d3e1b7f0

[Discussion and reviews](https://git.cafe/pingdotgg/t3code/pull/22942)

[Commits](https://git.cafe/pingdotgg/t3code/pull/22942/commits?format=markdown)

[Files changed](https://git.cafe/pingdotgg/t3code/pull/22942/diffs?format=markdown)

## Description

---

## Problem

Create PR accepts a recorded worktree base that exists only locally, then sends that branch name to the forge. In [#22920](https://github.com/pingdotgg/t3code/issues/22920), the child pushes successfully but GitHub rejects the unpublished parent with `base invalid`.

## Change

Validate recorded bases against remote-tracking refs before using them. If the base is unavailable, follow the existing provider-default / remote-default / main fallback and show the missing base in progress and the completion toast. Resolve the target repository for fork PRs so a parent published on the target remains valid when the child is on a fork; retain existing head selection and explicit remote bases. Validation adds no forge calls or fetches.

## Scope and approval

Fixes #22920
GitCafe: https://git.cafe/pingdotgg/t3code/issues/22920
Triaged bug via the automated issue flow.

Validation:
- Tests first against the unchanged resolver: 3 failed, 104 passed; the local-only parent was returned instead of main.
- `vp test run apps/server/src/git/GitManager.test.ts`: 134 passed. Cases include a published parent, absent key, deletion followed by fetch --prune, fork targets, non-origin remotes, target aliases, narrow refspecs, existing PR lookup, and bound GitCafe contexts.
- Literal-clone before/after proof: 2 passed before and 2 passed after.
- `vp run check:changed`: format, lint, knip, server GraphQL, scoped typecheck, and the touched test file all passed.
- Independent code review: GPT-6.1-Sol MERGE; Claude Opus 5.5 MERGE.
- Independent evidence audits: GPT-6.1-Sol SUFFICIENT; Claude Opus 5.5 SUFFICIENT. Both independently reran all 134 tests successfully.

## Verification

Before/after proof runs through GitManager with a local bare origin and an actual clone: main is pushed, parent has an unpublished commit, child is created from parent and pushed. The bare remote advertises only main and child. Both manual `branch.child.gh-merge-base=parent` and T3's `createWorktree(baseRefName: parent)` reproduce the behavior.

```text
Before (main baseline):
  result.pr.baseBranch = parent
  generatedBase = parent
  create request: --base parent --head child
  commit summary: Child commit

After (this branch):
  result.pr.baseBranch = main
  generatedBase = main
  create request: --base main --head child
  commit summary: Child commit + Parent commit
  progress and final toast:
    Base origin/parent is unavailable in this checkout. Using default branch main.
```

Remote-tracking refs are a local cache: a deleted branch can remain accepted until fetch/prune, and an unfetched branch can cause fallback. The notice describes availability in this checkout.

<!-- gc2-pull-create:84a55213e64cbdf69f7620df2a4a2616bfa1b59b6bfd33fa176924140f12671a -->

Implemented with GPT-6.1-Sol through the Codex harness; reviewed with GPT-6.1-Sol and Claude Opus 5.5 through T3 Code delegated agents.


## Focused recovery verification (fs-08)

Recovered the optional provider-detection failure in `GitManager.resolvePrBase`. Catch only `SourceControlProviderError` from the optional handle lookup, retain the published remote as the validation candidate, and skip target discovery without a handle. Git discovery and ref-validation errors remain outside the catch.

Repair commit: [54ced37fb2](https://git.cafe/pingdotgg/t3code/commit/54ced37fb260139ecb4a4cecb4c2a5bbb178de60).

- Failing before the repair: both new Create PR real-repo cases failed with `SourceControlProviderError` / `detectProvider` against the unchanged resolver (2 failed, 137 skipped).
- Passing after: both cases pass with a non-origin publication remote, `publish`. Assertions cover PR base/head, generated commit range and patch, create arguments, and missing-base progress/toast.
- `vp run check:changed c4de3ab8616aad8c03d0f104cef8dc1f64c9306c`: formatting, targeted lint, knip, server GraphQL, scoped typechecking (2 packages), and all 139 GitManager tests passed.

Runtime transcript from GitManager's Create PR harness (real Git repositories and bare remote; forge and text generation are boundary fakes):

```text
Optional provider handle lookup: SourceControlProviderError / detectProvider

Unpublished parent:
  bare publish remote heads: child, main
  PR result: created, base=main, head=child
  generated base: main
  commit summary: Child commit + Parent commit
  create request: pr create --base main --head child --title Child PR
  progress/toast: Base publish/parent is unavailable in this checkout. Using default branch main.

Published parent:
  bare publish remote heads: child, main, parent
  PR result: created, base=parent, head=child
  generated base: parent
  commit summary: Child commit
  create request: pr create --base parent --head child --title Child PR
  toast: Child PR
```

Pushed normally without rebasing, force-pushing, or merging. This focused repair was implemented and verified with GPT-6.1-Sol through the Codex harness.


## Evidence

> Supersedes the runtime transcripts in **Verification** and **Focused recovery verification** above. Those came from the GitManager vitest harness. The rows below come from the real server (`node apps/server/src/bin.ts start`) driven over WebSocket `git.runStackedAction {action: "create_pr"}`. Each run used its own isolated T3 home under `/tmp`, never `~/.t3`.

**Setup.** The remote is a **local bare repo acting as the remote**, reached through `pushInsteadOf`. Its URL is `https://github.invalid/acme/demo.git`, so T3 detects GitHub. A stubbed forge (an HTTPS CONNECT proxy that only lets through `github.invalid:443`, with a throwaway CA) serves repo info, the open-PR GraphQL lookup, and `POST /pulls`, whose body is logged as the forge request. Fake `gh` and `codex` binaries stand in for the real ones. Nothing reached a real forge or model. Each row's git refs were dumped before and after the run and were identical every time.

**Before** = merge-base on main [8d3b0611](https://git.cafe/pingdotgg/t3code/commit/8d3b061121fe65669d5d43d011b48cd3027a4108) · **After** = PR head [607524efc8](https://git.cafe/pingdotgg/t3code/commit/607524efc8673321b5f2d85a6f76c224e9071daf). The harness is committed at [014feb6beb](https://git.cafe/pingdotgg/t3code/commit/014feb6beb171234162c3bb22e7421e479433d23) and registered as knip entries in [e90dbdbae2](https://git.cafe/pingdotgg/t3code/commit/e90dbdbae28233cca48fd270c0be43a3d3e1b7f0); neither changes product code. Rerun with `node apps/server/scripts/evidence/pr-22943/run.mjs --server <checkout> --out <dir>`.

| Row | Fixture | Before: forge request | After: forge request | After: notice |
|---|---|---|---|---|
| R1 | `parent` pushed and present on the remote | `POST /pulls {"base":"parent","head":"child"}` | `POST /pulls {"base":"parent","head":"child"}` (unchanged) | none |
| R2a | `parent` pushed, deleted on the remote, `fetch --prune` | `{"base":"parent",…}` | `{"base":"main",…}` | `Base origin/parent is unavailable in this checkout. Using default branch main.` |
| R2b | `parent` deleted on the remote, not fetched (stale `origin/parent`) | `{"base":"parent",…}` | `{"base":"parent",…}` (known limitation, stated above) | none |
| R3 | `parent` never pushed | `{"base":"parent",…}` | `{"base":"main",…}` | same notice |
| R4a | no `main` anywhere, no `origin/HEAD`, repo info 404, `gh-merge-base=parent` never pushed | `{"base":"parent",…}`: PR silently created on a wrong base | **no request**: RPC fails with `GitCommandError … readRangeContext.log` | same notice, emitted before the error |
| R4b | R4a fixture with no recorded base (control) | no request, same git error | no request, same git error | none |

**Environment:** fs-08, Ubuntu 24.04.5 LTS (kernel 6.1.102), node v26.12.1, git 2.43.0.

**What else changed (before → after), besides the base:**
1. On the fallback path (R2a, R3, R4a) the notice replaces the `Generating PR content...` progress label. Event kinds and counts are unchanged.
2. In R2a and R3 the toast description adds the notice.
3. In R2a and R3 the commit range in the text-generation prompt widens from Child to Parent + Child, because the base is now `main`.
4. R2a, R3 and R4a each make one extra stubbed `GET /api/v3/repos/acme/demo`. This is the existing provider default-branch lookup on the fallback path. R1 and R2b make no extra requests, and no run made an extra fetch.
5. R4a no longer creates a PR on a wrong base. Its notice says "Using default branch main" even though `main` does not exist, and the error that follows is generic (`git log main..HEAD` → `ambiguous argument`). Not a regression, since R4b shows the same generic error before and after. A follow-up could report "could not determine a base branch".

Captures: [captures.md](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/68efca0eb702687a/captures.md) · [raw-logs.tar.gz](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/e10ac46b453f44c2/raw-logs.tar.gz) · Matrix: [matrix.md](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/ff2a0d6dabf580c4/matrix.md)

<details><summary>Verification log</summary>

- **Proof audit pass 1** (Claude Opus 5.5): derived the R1–R4b fixture matrix from the diff and from the gaps earlier rounds left (previous evidence came only from the test harness, and the deleted-base and unresolvable-default rows were missing). [matrix.md](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/ff2a0d6dabf580c4/matrix.md)
- **Evidence capture** (Claude Opus 5.5): ran all 12 runs (6 rows × before/after) against the real server. Every row matched the matrix. [captures.md](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/68efca0eb702687a/captures.md)
- **Proof audit pass 2** (Claude Opus 5.5): **SUFFICIENT**. Confirmed which SHA each server ran from its log paths, and confirmed the harness commit adds only the evidence directory. Rebuilt R2a, R3 and R4a from the committed script on both before and after; all six runs matched the captures. Isolation held: the only host the proxy let through was `github.invalid`, only the fake gh and codex were called, and every run had a fresh T3 home. [verdict.md](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/2c98582bfecc6e3d/verdict.md)
- **Harness deviations:** the bootstrap sets `shellEnvironmentPrepared: true` so the login-shell PATH cannot put the real `codex` ahead of the fake. One trial run before this fix spawned the real codex, which the proxy blocked; that trial was discarded. Every run refused the server's background calls to `raw.githubusercontent.com` and `ingest.t3.codes`.
- **Tests:** `vp test run apps/server/src/git/GitManager.test.ts`: 139 passed after merging main ([607524efc8](https://git.cafe/pingdotgg/t3code/commit/607524efc8673321b5f2d85a6f76c224e9071daf)). `vp run check:changed 607524efc8`: format, lint, knip, graphql and typecheck passed after the knip fix.
- **CI:** at [014feb6beb](https://git.cafe/pingdotgg/t3code/commit/014feb6beb171234162c3bb22e7421e479433d23), `Check` and `Lint` failed and the rest passed (Test, Test Server 1–6, Build and Typecheck, Release Smoke, Rust, Macroscope). Knip reproduces the `Check` failure locally (the harness files were reported as unused), and [e90dbdbae2](https://git.cafe/pingdotgg/t3code/commit/e90dbdbae28233cca48fd270c0be43a3d3e1b7f0) fixes it. `Lint` passes locally; I couldn't read its CI log through cafe. CI on e90dbdbae2 is pending.
- **Unverified rows:** none.
- Main merged in with a merge commit (no rebase, no force-push). Evidence was led and captured by Claude Opus 5.5 through T3 Code delegated agents (Claude Code harness).

</details>

