Skip to content

fix(pregate): a --base that names a commit gets a branch fix, not an impossible fetch (#3269) - #3290

Closed
szsunyuan wants to merge 1 commit into
garrytan:mainfrom
szsunyuan:fix/pregate-base-not-on-origin
Closed

szsunyuan wants to merge 1 commit into
garrytan:mainfrom
szsunyuan:fix/pregate-base-not-on-origin

Conversation

@szsunyuan

Copy link
Copy Markdown
Contributor

Why (in your own words)

gstack-pregate --base <sha> refuses with POLICY_SOURCE_UNAVAILABLE and the fix git fetch origin <sha> --depth=1. That fetch can never make the rerun pass:

  • A short SHA fails with "couldn't find remote ref".
  • A full SHA lands in FETCH_HEAD only, so origin/<sha> still doesn't exist. In a throwaway full clone, that --depth=1 fetch also wrote .git/shallow, which hides the history behind that commit.
  • In a fork clone, --base upstream/main fails the same way: pregate builds origin/upstream/main and suggests fetching it from the fork.

An agent that follows the fix gets the same refusal again, and a person learns nothing about what --base accepts.

Now, when <base> names a commit here but is not a local branch (a SHA, a tag, another remote's ref), the refusal says --base takes a branch name on origin and suggests one: origin/HEAD's branch, when it exists. The result code (POLICY_SOURCE_UNAVAILABLE) and the exit code (3) stay the same.

Fixes #3269.

Why this shape

  • It's the reporter's option 2. @dgrant offered two outcomes: "either resolve --base as a commit when it is one, or say that --base takes a branch name on origin and drop the fetch hint." This takes the second, for exactly the inputs the fetch can't help.
  • It does not take option 1. Pin files are read from the reviewed copy on origin/<base>, so a PR can't smuggle a command into its own gate (docs/pregate.md#trust-boundary). --policy-from head --allow-repo-commands is the path that labels a policy unreviewed. Accepting any local commit as the base, HEAD included, would read pin files from a commit nobody reviewed, without that label. Accepting only commits that an origin branch contains might be safe, but that's a trust-boundary call for you. Happy to follow up.
  • It keeps the fetch fix where the fetch works. A local branch whose origin/ copy was never fetched, or a name that resolves nowhere (the shallow gate checkout the hint was written for, F12), still gets git fetch origin <base> --depth=1, byte for byte. The existing pins for that are untouched and pass: test/pregate-wtree-refusal.test.ts:112, test/gstack-ship-policy.test.ts:115 and test/restamp-command-boundary.test.ts:84.
  • It reuses the existing mechanism. ContextRefusal.fix is already "a cause-specific fix that replaces the code's own" for WTREE_UNAVAILABLE (wtreeRefusal). describeRefusal returned before reading it for POLICY_SOURCE_UNAVAILABLE; now it reads it there too, and the line still says "pin files".
  • The new fix is an exact flag (--base main). That's what the agent-ready error contract asks for (docs/agent-ready-errors.md): "an exact command, flag or edit … A fix counts only when it is safe and authorized."
  • CONTRIBUTING's wave rule: if two PRs fix the same thing, keep the smaller one. This is 1 commit in 4 files (+61/−8), on current main (2d42e4ece), and fine to absorb into a wave.
  • Open PRs on the same files: it merges cleanly with main, fix(scope,review,ship): recognise Dart and Flutter in diff scope and test detection #3284 and fix(pregate): Dart import graph and test lanes #3285. With feat(pregate): user-owned policy file and local lanes #3286 (user-owned policy file), the only conflict is two adjacent import lines at the top of lib/pregate/context.ts; keep both. The new helper sits next to wtreeRefusal rather than above buildContext, where feat(pregate): user-owned policy file and local lanes #3286 adds its own.

What it does

  • lib/pregate/context.ts: on the refusal path only, buildContext asks two things:

    • Is <base> a local branch (refs/heads/<base>)?
    • Does it name a commit (<base>^{commit})?

    If it names a commit but is not a branch, the refusal gets the new detail and fix. Otherwise it's today's refusal, unchanged.

  • bin/gstack-pregate: --base <ref> becomes --base <branch> in the usage line and in the flag help. That matches the sealed and regate lines in the same file.

  • docs/troubleshooting.md#policy-source-unavailable: one paragraph. --base names a branch on origin; a SHA or another remote's ref never resolves as origin/<base>; pregate now says so instead of suggesting the fetch.

  • test/pregate-wtree-refusal.test.ts: one CLI test, next to the existing --base no-such-base envelope case.

    • A short SHA and upstream/main get the branch fix. The --json fix and the stderr line agree, and neither says git fetch.
    • A local branch with no origin copy keeps the fetch fix.

What it deliberately does not do

  • No SHA (or HEAD) accepted as the base. See "Why this shape".
  • No change to the shared RESULT_CODES.POLICY_SOURCE_UNAVAILABLE row. gstack-ship-policy, gstack-restamp, gstack-ship-queue, gstack-pregate regate and the other tools that build origin/<base> still print the fetch hint for a SHA. They're a follow-up if you want them; the check is one function.
  • No network lookup to tell an unpushed branch from an unfetched one. Branch names keep the fetch fix.
  • No change to other bins' --base <ref> help.
  • Release files and docs: VERSION / CHANGELOG / README / ETHOS untouched, and no generated files. /ship not run (the wave stamps the version).

AI was used for assistance.

Live evidence

Setup. A throwaway fork-style clone in a temp dir:

  • origin and upstream are local bare repos holding the same main.
  • The clone is on feature, and unpushed is a local branch that was never pushed.
  • GSTACK_HOME and the git config point into the temp dir.
  • Before runs bin/gstack-pregate from a detached upstream/main worktree; After runs this branch's.
  • pregate refuses before it writes anything.

Before (upstream/main at 2d42e4ece): every case gets the fetch hint.

$ gstack-pregate --base 577a8d4b --stage preflight
gstack-pregate: policy source unavailable: origin/<base> is not a resolvable commit, so the reviewed pin files cannot be read (origin/577a8d4b is not a resolvable commit); fix: git fetch origin 577a8d4b --depth=1 (the working-tree copy is never used silently) (POLICY_SOURCE_UNAVAILABLE)
exit=3
$ gstack-pregate --base upstream/main --stage preflight
gstack-pregate: policy source unavailable: origin/<base> is not a resolvable commit, so the reviewed pin files cannot be read (origin/upstream/main is not a resolvable commit); fix: git fetch origin upstream/main --depth=1 (the working-tree copy is never used silently) (POLICY_SOURCE_UNAVAILABLE)
exit=3
$ gstack-pregate --base unpushed --stage preflight
gstack-pregate: policy source unavailable: origin/<base> is not a resolvable commit, so the reviewed pin files cannot be read (origin/unpushed is not a resolvable commit); fix: git fetch origin unpushed --depth=1 (the working-tree copy is never used silently) (POLICY_SOURCE_UNAVAILABLE)
exit=3
$ gstack-pregate --base no-such-base --stage preflight
gstack-pregate: policy source unavailable: origin/<base> is not a resolvable commit, so the reviewed pin files cannot be read (origin/no-such-base is not a resolvable commit); fix: git fetch origin no-such-base --depth=1 (the working-tree copy is never used silently) (POLICY_SOURCE_UNAVAILABLE)
exit=3

After (this branch, 18c53ffa2): a SHA and another remote's ref get a branch to pass. A local branch and an unknown name keep the fetch fix, byte for byte.

$ gstack-pregate --base 577a8d4b --stage preflight
gstack-pregate: policy source unavailable: origin/<base> is not a resolvable commit, so the reviewed pin files cannot be read (origin/577a8d4b is not a resolvable commit; 577a8d4b names a commit here, but --base takes a branch name on origin); fix: pass a branch that exists on origin, such as --base main (git branch -r --list 'origin/*' lists them) (POLICY_SOURCE_UNAVAILABLE)
exit=3
$ gstack-pregate --base upstream/main --stage preflight
gstack-pregate: policy source unavailable: origin/<base> is not a resolvable commit, so the reviewed pin files cannot be read (origin/upstream/main is not a resolvable commit; upstream/main names a commit here, but --base takes a branch name on origin); fix: pass a branch that exists on origin, such as --base main (git branch -r --list 'origin/*' lists them) (POLICY_SOURCE_UNAVAILABLE)
exit=3
$ gstack-pregate --base unpushed --stage preflight
gstack-pregate: policy source unavailable: origin/<base> is not a resolvable commit, so the reviewed pin files cannot be read (origin/unpushed is not a resolvable commit); fix: git fetch origin unpushed --depth=1 (the working-tree copy is never used silently) (POLICY_SOURCE_UNAVAILABLE)
exit=3
$ gstack-pregate --base no-such-base --stage preflight
gstack-pregate: policy source unavailable: origin/<base> is not a resolvable commit, so the reviewed pin files cannot be read (origin/no-such-base is not a resolvable commit); fix: git fetch origin no-such-base --depth=1 (the working-tree copy is never used silently) (POLICY_SOURCE_UNAVAILABLE)
exit=3

The old hint, followed literally:

$ git fetch origin 577a8d4b --depth=1
fatal: couldn't find remote ref 577a8d4b
exit=128

Scope

  • Changed: lib/pregate/context.ts (+26/−6), bin/gstack-pregate (+2/−2), docs/troubleshooting.md (+6), test/pregate-wtree-refusal.test.ts (+27). No VERSION, no CHANGELOG, no generated files.
  • Verified live by:
    • The new test is red on upstream/main, green here.

      • On 2d42e4ece it fails at toContain('--base takes a branch name on origin'), receiving origin/789930c3 is not a resolvable commit.
      • Here, all 8 tests in the file pass.
    • Focused files, each in its own process with LC_ALL=C, all passing:

      File Tests
      pregate-wtree-refusal 8
      pregate-selection 28
      gstack-ship-policy 12
      restamp-command-boundary 12
      gstack-regen 16
      troubleshooting-anchors 205
    • Every file pregate selects for this diff. origin is my fork, so I pointed a temporary origin/pregate-base ref at upstream/main (2d42e4ece):

      • pregate selects 26 files, and I ran each alone.
      • 25 pass.
      • pregate-supervise fails 2 of 14 here and the same 2 on upstream/main. Both read bun test's per-file progress lines, most likely in Bun 1.4.2's format; this box has Bun 1.4.0, and CI pins 1.4.2.
    • Typecheck:

      • bun run typecheck exits 0.
      • bun run typecheck:test reports 12 diagnostics, all in files this PR doesn't touch. The list is identical on upstream/main (Bun 1.4.0's types).
    • Full bun run test: not green on this Linux box, on upstream/main either.

      • This run on 18c53ffa2 failed 367 tests in 71 files.

      • The main causes:

        • Bun 1.4.0 here is below gstack's 1.4.2 floor (the autoplan, outside-consent and skill-start files).
        • Chromium can't launch here (no Xvfb).
        • The /cso suites need bun run build:cso and Docker.
      • pregate's import graph links one of the 71 files to the changed files: pregate-supervise, above.

      • Per-file failure counts match my earlier full run of this change (on e9dfcf64f), except four files that failed in only one of the two runs:

        • this run: setup-gbrain-remote-caller and test-free-shards-settlement-lifecycle
        • the earlier run: qa-browser-deadline-evidence and test-free-shards-capture

        All four pass alone, with and without this change.

      • The serial shard 17 (bootstrap-retention) passed its 36 tests but was marked failed by the home-containment check. ~/.claude/sessions/<id>.json changed while it ran, and that file belongs to a live Claude Code session on this machine. The other real-home warnings are ~/.claude/plugins files from the same sessions. No new ~/.gstack/projects entries.

      • CI's Linux free suite is the gate.

    • Redaction: bin/gstack-redact --repo-visibility public on the diff reports no findings. git diff --check is clean.

    • slop-scan: 696 findings on both trees, none in the changed files. npx refuses to run inside the repo right now (EOVERRIDE: sharp is ^0.35.4 in dependencies and 0.35.5 in overrides, on upstream/main too), so I ran the cached slop-scan binary directly.

    • Paid evals: bun run eval:select --base upstream/main picks the whole PR gate (108 behaviors, 24 judges), because every gate case reaches bin/gstack-pregate and lib/pregate/context.ts. Fork PRs get no eval secrets, so I couldn't run it. Every changed line runs only when origin/<base> doesn't resolve, and a refusal without its own fix prints the same bytes as before (the unpushed and no-such-base rows above).

  • Did NOT test:
    • Windows. CI's Windows Free Tests runs this file, which is not curated out there.
    • The full-SHA fetch against a GitHub remote. I showed it with a local bare remote.

Liveness proof (required for external contributors)

image

Checklist

Test plan

  • The new test is red on upstream/main and green here
  • The focused files and every file pregate selects for this diff (above)
  • Before/After evidence for a SHA, another remote's ref, a local branch and an unknown name
  • Full bun run test: the same environment failures as upstream/main; none comes from the changed files
  • CI free tests on this PR (Free Tests and Windows Free Tests)

Fork PRs do not get eval secrets; the free files above are the gate.

Fixes #3269

…impossible fetch (garrytan#3269)

gstack-pregate --base <sha> refused with "fix: git fetch origin <sha>
--depth=1". That fetch can never create origin/<sha>: a short SHA fails
with "couldn't find remote ref", and a full SHA only reaches FETCH_HEAD.
A fork clone hits the same wall with --base upstream/main.

When origin/<base> does not resolve but <base> names a commit here and
no local branch (a SHA, a tag, another remote's ref), the refusal now
says --base takes a branch name on origin and suggests origin/HEAD's
branch. A local branch, or a name that resolves nowhere, keeps the
fetch fix: that is the unfetched or shallow checkout it repairs. Same
code, same exit 3, and pin files are still read only from the reviewed
origin/<base>.

Fixes garrytan#3269

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@trunk-io

trunk-io Bot commented Oct 19, 2026

Copy link
Copy Markdown

Merging to main in this repository is managed by Trunk.

  • To merge this pull request, check the box to the left or comment /trunk merge below.

After your PR is submitted to the merge queue, this comment will be automatically updated with its status. If the PR fails, failure details will also be posted here

@capy-ai

capy-ai Bot commented Oct 19, 2026

Copy link
Copy Markdown
Contributor

Thanks @szsunyuan — clean, well-argued fix, and the "why this shape" section made the trust-boundary call easy to agree with. Your commit is cherry-picked into #3306 with your authorship kept (it conflicted with #3286 in context.ts, and #3306 owns those files now). I'll close this once #3306 is through its gate.

@capy-ai

capy-ai Bot commented Oct 20, 2026

Copy link
Copy Markdown
Contributor

Thanks @szsunyuan. Your commit was cherry-picked into #3306 with your authorship kept, alongside the other non-JS pregate fixes (#3266-#3269). Closing this in favour of that port; it's stamped v1.91.161.0 and through its gate.

@capy-ai capy-ai Bot closed this Oct 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

gstack-pregate --base <sha>: POLICY_SOURCE_UNAVAILABLE suggests a git fetch that cannot fix it

1 participant