Skip to content

fix(pipeline): shallow single-branch clone omits origin/baseBranch, breaking PR diffs and auto-rebase (canonical) #74

Description

@chrisleekr

Finding

The pipeline checks out the head branch only via single-branch shallow clone, but every PR-aware prompt and the auto-rebase workflow then tells the agent to use origin/<baseBranch> — a remote-tracking ref the clone never created. So the agent's git diff / git rebase / git log commands cannot resolve the ref on every PR mention.

src/core/checkout.ts:59 runs git clone --depth=<cloneDepth> --branch=<branch> --single-branch … where branch = ctx.isPR ? ctx.headBranch : ctx.defaultBranch (src/core/checkout.ts:40). Per the git-clone docs, --single-branch rewrites remote.origin.fetch to +refs/heads/<headBranch>:refs/remotes/origin/<headBranch>. Only that one ref is fetched and a later plain git fetch origin will not populate any other branch. Yet src/core/prompt-builder.ts:38-39 tells the agent the PR base is origin/<baseBranch> and to run git diff origin/<baseBranch>...HEAD, repeated at L122 and again in the allowed-tools list at L205 (Bash(git diff origin/<baseBranch>...HEAD)).

The same shape breaks the auto-rebase loop. src/workflows/handlers/branch-refresh.ts:86-88 (added in commit befe07c, reinforced by 3079014's cascade review/resolve loop) emits git fetch origin then git rebase origin/<baseRef> — the rebase exits with fatal: invalid upstream 'origin/main'. cloneDepth (src/config.ts:93, default 50) controls history length, not which refs are fetched. No git fetch of the base branch exists anywhere in src/core/ or src/daemon/ (verified by grep). The agent has to discover the failure at runtime, eat retry turns probing git remote -v / git branch -r, and either give up or self-heal by guessing git remote set-branches --add + git fetch. Direct burned cost on every PR comment.

Diagram

flowchart TD
    Trigger["Webhook: PR comment with the bot mention"]:::event
    Clone["src/core/checkout.ts:59<br/>git clone --depth=50 --branch=headBranch<br/>--single-branch repoUrl workDir"]:::current
    Refspec["remote.origin.fetch in workDir/.git/config:<br/>+refs/heads/headBranch:refs/remotes/origin/headBranch<br/>baseBranch is NOT in the refspec"]:::warn
    Promptpath["src/core/prompt-builder.ts L38-39, L122, L205<br/>tells agent: git diff origin/baseBranch...HEAD"]:::current
    Refreshpath["src/workflows/handlers/branch-refresh.ts L86-88<br/>tells agent: git fetch origin<br/>then git rebase origin/baseRef"]:::current
    Diff["Agent: git diff origin/baseBranch...HEAD"]:::action
    Rebase["Agent: git rebase origin/baseRef"]:::action
    Fatal["fatal: ambiguous argument origin/baseBranch<br/>or fatal: invalid upstream origin/baseRef"]:::error
    Waste["Wasted turns and tokens<br/>review may run against HEAD only<br/>auto-rebase aborts mid-loop"]:::error
    Fix["Fix in src/core/checkout.ts after clone:<br/>git -C workDir remote set-branches --add origin baseBranch<br/>git -C workDir fetch --depth=cloneDepth origin baseBranch"]:::fix
    Healthy["origin/baseBranch resolves<br/>diff and rebase work first try"]:::ok

    Trigger --> Clone --> Refspec
    Refspec --> Promptpath --> Diff
    Refspec --> Refreshpath --> Rebase
    Diff --> Fatal
    Rebase --> Fatal
    Fatal --> Waste
    Waste -. proposed fix .-> Fix --> Healthy

    classDef event fill:#0b5394;color:#ffffff;stroke:#053061;stroke-width:1px
    classDef current fill:#856404;color:#ffffff;stroke:#533f03;stroke-width:1px
    classDef warn fill:#a04000;color:#ffffff;stroke:#5e2607;stroke-width:1px
    classDef action fill:#21618c;color:#ffffff;stroke:#1b4f72;stroke-width:1px
    classDef error fill:#922b21;color:#ffffff;stroke:#641e16;stroke-width:1px
    classDef fix fill:#196f3d;color:#ffffff;stroke:#0e6251;stroke-width:1px
    classDef ok fill:#196f3d;color:#ffffff;stroke:#0e6251;stroke-width:1px
Loading

Rationale

This is a silent correctness issue on the most common code path — every bot mention on a PR. Default agent model is Opus 4.7 (src/config.ts:371), so wasted recovery turns cost real money: a typical non-trivial PR review is ~15-40 turns; observed Opus per-turn cost is roughly US$0.05–0.20, so even three burned recovery turns is ~US$0.15–0.60 of pure waste, doubled by the cascade review/resolve loop (reviewResolveMaxIterations defaults to 2 — src/config.ts:354). When the agent falls back to git diff HEAD (no base ref) it reviews the PR against itself, masking regressions. The cascade work in #61, #63 made it strictly worse — branch-refresh now relies on origin/<baseRef> for clean rebase, and formatRefreshDirective (src/workflows/handlers/branch-refresh.ts:65-69) prints a clean-looking directive whose underlying rebase fails.

The fix is small, local, additive: a single git remote set-branches --add origin <baseBranch> plus git fetch --depth=<cloneDepth> origin <baseBranch> after the existing clone. Pattern is documented in git-fetch(1) and the Jay Goodby writeup on tracking remote branches after --single-branch. Bandwidth cost is one extra shallow fetch (typically a few hundred KB) only paid on PR events where head and base differ. It also aligns this server with upstream claude-code-action, which gets origin/<baseBranch> for free via actions/checkout@v4 with fetch-depth: 0.

References

Internal:

  • src/core/checkout.ts:40-59 — single-branch shallow clone command and branch selection
  • src/config.ts:93cloneDepth default 50 (history length, not refspec)
  • src/core/prompt-builder.ts:38-39, 122, 205 — agent told to use git diff origin/<baseBranch>...HEAD in three places
  • src/workflows/handlers/branch-refresh.ts:86-88 — auto-rebase emits git fetch origin then git rebase origin/<baseRef>
  • src/core/pipeline.ts:212-216enrichedCtx.baseBranch is computed and passed to the prompt builder
  • src/core/fetcher.ts:317baseBranch hydrated from GraphQL baseRefName but never used to drive a fetch
  • CLAUDE.md Pipeline step 5 — never mentions fetching the base ref

External:

Suggested Next Steps

  1. In src/core/checkout.ts, after the clone (line 59) and before returning, conditionally run git -C <workDir> remote set-branches --add origin <baseBranch> then git -C <workDir> fetch --depth=<cloneDepth> origin <baseBranch> when ctx.isPR is true, a baseBranch is available, and it differs from the cloned branch. Cleanest plumbing: do the supplemental fetch inside runPipeline between the checkout call and executeAgent (so the GraphQL-derived baseBranch from src/core/fetcher.ts:317 is authoritative), or pass enrichedCtx.baseBranch into checkoutRepo as an explicit parameter.
  2. Add a unit test in test/core/ (new checkout.test.ts) that runs the checkout against a local fixture repo with two branches and asserts both origin/<headBranch> and origin/<baseBranch> show up in git branch -r. DI groundwork already landed in chore(test): DI refactor for checkout.ts and executor.ts to enable module-level unit tests #7.
  3. Trim the now-redundant "(NOT 'main' or 'master')" recovery prose in src/core/prompt-builder.ts once the fix lands, and verify formatRefreshDirective works end-to-end on a same-repo PR that is behind base.
  4. Add a Pino info-level log line at the supplemental-fetch site (fields: baseBranch, headBranch, depth) anchored to the existing log at src/core/checkout.ts:58.

Areas Evaluated

Note on sibling placeholders

Earlier same-run sibling issues #71, #72, #73 were created while probing harness payload-size and write-permission limits. They carry no findings and can be closed safely; this is the canonical research artifact for run #25011408648.

Generated by scheduled research workflow run #25011408648 on 2026-04-27

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions