You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Sessions are opening new branches faster than PRs merge — 58 PRs were open at
the time of this decision. The owner capped open PRs, not branches: a
session must finish and merge (or close) existing work before starting new
work. CLAUDE.md:49 currently says the opposite ("There is no open-PR
limit...") and must be corrected in the same change that adds the gate.
Design
Where: the pre-push hook chain (tools/git-hooks/pre-push, installed by scripts/install-git-hooks.sh into the shared .git/hooks/ dir used by the
main checkout and every worktree). This is where Phase 0c/Phase 6 already
enforce push-time policy, so the new gate rides the same chain rather than
adding a second, uninstalled one.
When it applies: only when the pushed ref is a refs/heads/* branch that
does not yet exist on the remote (remote_sha from git's pre-push stdin
protocol is all-zero) — i.e. a brand-new branch. A brand-new tag has a zero remote_sha too but is exempt (a tag is never a PR head). A push to a branch
that already has an open PR is never blocked, so fixing existing work is
always possible.
What counts: open PRs in this repo whose head branch lives in this repo
(not a fork), excluding bot authors. Measured against this repo's own
dependabot history: gh's GraphQL author.login for a bot reports the
App-slug form app/dependabot (never the REST-style dependabot[bot]), with is_bot: true — that field is what actually does the excluding; a [bot]-suffixed-login check is kept only as a defensive fallback for a shape
not observed in this API. One gh pr list --state open --limit 1000 --json number,title,createdAt,author,isCrossRepository --jq '...' call, time-boxed,
returns both the filtered count and the oldest 10 (by createdAt) in one
round trip.
Cap:AUTOBOT_OPEN_PR_CAP, default 40. Documented in the hook's own
header comment (the precedent for a hook-only, bash-read AUTOBOT_* var is AUTOBOT_PREPUSH_ALLOW_TIMEOUT in the same file — it is not in the Python env_registry/ENV_VARS.md because that registry's pre-commit check
(pipeline-scripts/check_env_var_registry.py) only AST-scans .py files for os.getenv("AUTOBOT_..."); a bash-only var has no home there).
At or above the cap: refuse the push. Message states the current count,
the cap, that pushes to existing PR branches still work, and lists the oldest
10 open PRs (number + title) to act on.
Fail closed: if gh is missing, unauthenticated, or the query fails, the
push is refused with a message saying the count could not be determined —
distinct from "under the cap" (measurement discipline: an empty/failed read
is not a true negative). No bypass flag, no env override that skips the
check — the only knob is the cap value.
Acceptance criteria
tools/git-hooks/pre-push refuses a new-branch push once the
filtered open-PR count is >= AUTOBOT_OPEN_PR_CAP (default 40), with a
message giving the count, the cap, remediation, and the oldest 10 open
PRs.
A push to a branch that already exists on the remote is never
blocked by this gate, even at/above the cap.
The count excludes fork PRs (isCrossRepository == true) and bot
authors (is_bot or a [bot]-suffixed login).
gh missing / unauthenticated / query failure refuses the push with a
"could not determine" message, never "allowed".
No env var or flag bypasses the check; AUTOBOT_OPEN_PR_CAP is the only
configurable input.
The check is reachable from a clean clone via scripts/install-git-hooks.sh (proof it is wired into the installed
chain, not just present in tools/git-hooks/).
CLAUDE.md:49's "There is no open-PR limit" line is replaced with the
new rule; docs/developer/*.md has no remaining contradicting text.
Tests under repo_tests/ drive the real hook script (not a
reimplementation) with a stubbed gh on PATH, covering: below cap
(allowed), at cap (refused, message has count+cap), existing-branch
push at cap (allowed), gh failure (refused, "could not determine"),
bot-authored PRs excluded from the count.
Non-goals
No change to branch/worktree limits — only open PRs are capped.
No dispatch-side enforcement in this issue; that is a CLAUDE_BATCH.md / AGENT_COORDINATION.md doc note, not new code.
The bot-login examples (dependabot[bot], github-actions[bot]) were wrong. Measured against this repo's own dependabot history (45 closed PRs): gh pr list --json author reports login: "app/dependabot", is_bot: true — the App-slug form, never the REST-style [bot] suffix. is_bot is what actually excludes them; the [bot]-suffix check in the hook's jq filter is a defensive fallback only, not the real mechanism. Issue body updated.
Added a scope fix: the cap now also requires local_ref to match refs/heads/*, not just remote_sha == 0000...0 — a brand-new tag push also has a zero remote_sha and was incorrectly tripping the cap.
Added a timeout (OPEN_PR_CAP_TIMEOUT=10, fails closed, no override) around the gh pr list call.
AC verification against merged main (post #17133 vehicle merge)
All 8 ACs verified directly in tools/git-hooks/pre-push + repo_tests/pre_push_open_pr_cap_17006_test.py:
Refuses at/above cap with count+cap+remediation+oldest-10 (check_open_pr_cap's message construction).
Existing-branch push never blocked — gated on [ "$remote_sha" = "$ZERO" ] && [[ "$local_ref" == refs/heads/* ]]; a new tag (zero remote_sha, not refs/heads/*) is explicitly exempt too.
Excludes fork PRs (isCrossRepository == false) and bots (is_bot primary, [bot]-suffix login fallback, with a comment citing measured dependabot behavior).
Fails closed on gh-missing/timeout/query-failure/unexpected-output, each with a distinct "could not determine... NOT under the cap" message.
No bypass — comment explicitly notes AUTOBOT_PREPUSH_ALLOW_TIMEOUT does NOT apply here; AUTOBOT_OPEN_PR_CAP is the only knob.
Wired into the installed chain — pre-push is in install-git-hooks.sh's MANAGED_HOOKS.
CLAUDE.md corrected — line 51 now states the cap; confirmed no contradicting "no open-PR limit" text remains.
Tests drive the real hook script with a stubbed gh, covering all 5 named scenarios plus 2 extra (new-tag-at-cap, default-cap-40).
Owner decision (2026-09-18)
Sessions are opening new branches faster than PRs merge — 58 PRs were open at
the time of this decision. The owner capped open PRs, not branches: a
session must finish and merge (or close) existing work before starting new
work.
CLAUDE.md:49currently says the opposite ("There is no open-PRlimit...") and must be corrected in the same change that adds the gate.
Design
Where: the pre-push hook chain (
tools/git-hooks/pre-push, installed byscripts/install-git-hooks.shinto the shared.git/hooks/dir used by themain checkout and every worktree). This is where Phase 0c/Phase 6 already
enforce push-time policy, so the new gate rides the same chain rather than
adding a second, uninstalled one.
When it applies: only when the pushed ref is a
refs/heads/*branch thatdoes not yet exist on the remote (
remote_shafrom git's pre-push stdinprotocol is all-zero) — i.e. a brand-new branch. A brand-new tag has a zero
remote_shatoo but is exempt (a tag is never a PR head). A push to a branchthat already has an open PR is never blocked, so fixing existing work is
always possible.
What counts: open PRs in this repo whose head branch lives in this repo
(not a fork), excluding bot authors. Measured against this repo's own
dependabot history: gh's GraphQL
author.loginfor a bot reports theApp-slug form
app/dependabot(never the REST-styledependabot[bot]), withis_bot: true— that field is what actually does the excluding; a[bot]-suffixed-login check is kept only as a defensive fallback for a shapenot observed in this API. One
gh pr list --state open --limit 1000 --json number,title,createdAt,author,isCrossRepository --jq '...'call, time-boxed,returns both the filtered count and the oldest 10 (by
createdAt) in oneround trip.
Cap:
AUTOBOT_OPEN_PR_CAP, default40. Documented in the hook's ownheader comment (the precedent for a hook-only, bash-read
AUTOBOT_*var isAUTOBOT_PREPUSH_ALLOW_TIMEOUTin the same file — it is not in the Pythonenv_registry/ENV_VARS.mdbecause that registry's pre-commit check(
pipeline-scripts/check_env_var_registry.py) only AST-scans.pyfiles foros.getenv("AUTOBOT_..."); a bash-only var has no home there).At or above the cap: refuse the push. Message states the current count,
the cap, that pushes to existing PR branches still work, and lists the oldest
10 open PRs (number + title) to act on.
Fail closed: if
ghis missing, unauthenticated, or the query fails, thepush is refused with a message saying the count could not be determined —
distinct from "under the cap" (measurement discipline: an empty/failed read
is not a true negative). No bypass flag, no env override that skips the
check — the only knob is the cap value.
Acceptance criteria
tools/git-hooks/pre-pushrefuses a new-branch push once thefiltered open-PR count is
>= AUTOBOT_OPEN_PR_CAP(default 40), with amessage giving the count, the cap, remediation, and the oldest 10 open
PRs.
blocked by this gate, even at/above the cap.
isCrossRepository == true) and botauthors (
is_botor a[bot]-suffixed login).ghmissing / unauthenticated / query failure refuses the push with a"could not determine" message, never "allowed".
AUTOBOT_OPEN_PR_CAPis the onlyconfigurable input.
scripts/install-git-hooks.sh(proof it is wired into the installedchain, not just present in
tools/git-hooks/).CLAUDE.md:49's "There is no open-PR limit" line is replaced with thenew rule;
docs/developer/*.mdhas no remaining contradicting text.repo_tests/drive the real hook script (not areimplementation) with a stubbed
ghonPATH, covering: below cap(allowed), at cap (refused, message has count+cap), existing-branch
push at cap (allowed),
ghfailure (refused, "could not determine"),bot-authored PRs excluded from the count.
Non-goals
CLAUDE_BATCH.md/AGENT_COORDINATION.mddoc note, not new code.