Repository navigation
[PR-limits Phase 3] Wire the admission gate into dev-lead-fix-issue.sh (enforce the org-wide cap) #1003
Description
Story
Wire the already-built PR-limit admission gate into this repo's dev-lead PR-creation path so the org-wide open-automation-PR cap is actually enforced at the source, per the Pull-Request-Limits ADR (petry-projects/.github docs/initiatives/pull-request-limits-adr.md). This is Phase 3 (the "apply path") of epic combsmith/.github#505.
Background (all delivered, all inert today)
GitHub exposes no native max-open-PRs surface (ADR §2–§3), so enforcement is source-side: before a PR-creating automation opens a PR, it asks a guard whether the standing queue of open, non-draft automation PRs is at the cap. Two pieces already exist on petry-projects/.github main (public repo):
- Guard library —
scripts/lib/pr-limit-gate.sh, functionplg_admission_gate <source>→ printsdecision=allow|defer, returns 0 on allow / 1 on defer. HonorsDRY_RUN/DEV_LEAD_DRY_RUN. Exempt actors/labels are never counted nor blocked. - Config (single source of truth) —
standards/pr-limits.json: signed-off org-wideautomation_open_pr_cap: 50(a soft ceiling; ~44 automation PRs open at baseline, so it blocks nothing today),per_source_caps: {}, exempt actors incl.dependabot[bot]+securitylabel.
Nothing calls plg_admission_gate yet. This story adds the one call site.
Integration point (verified)
scripts/dev-lead-fix-issue.sh is the single choke point where automation PRs are created:
- L221
git checkout -b "$branch" - L277
git push --set-upstream origin "$branch" - L279
gh pr create ...
scripts/initiative-driver.sh does not open PRs — it dispatches to dev-lead, which does. So gating dev-lead-fix-issue.sh covers dev-lead and initiative-driven PRs.
Acceptance criteria
- Admission check before branch/PR creation. Immediately before
git checkout -b(L221) inscripts/dev-lead-fix-issue.sh, source the guard and callplg_admission_gate "dev-lead". Ondefer, do not create the branch/PR. - Guard + config sourced from the public repo (single source of truth). Fetch
scripts/lib/pr-limit-gate.shandstandards/pr-limits.jsonfrompetry-projects/.github@mainat runtime (e.g.gh api .../contentswith the existing$GH_TOKEN, or raw URL) rather than vendoring a copy — the config must stay authoritative inpetry-projects/.github. Point the guard at the fetched config via$PR_LIMITS_CONFIG. - Graceful defer — never drop the issue. On defer: leave the issue in its ready/
dev-leadstate (or re-queue via the existing retry path) so it is re-attempted when the queue drains, and post a single concise issue comment notingdeferred: open-automation-PR queue N/50 reached — will retry(idempotent; do not spam on every retry). No PR is opened. - Dry-run honored. When
DEV_LEAD_DRY_RUN=true, the gate prints the would-be decision but returns allow (guard already implements this) — so dry-run runs never defer. Verify the engine's existing dry-run path is unchanged. - Fail-open. A guard/fetch error (network, missing config, bad JSON) must allow PR creation and log a warning — never wedge the engine. (The guard's counting is already fail-open; extend the same posture to the fetch/source step.)
- Tests. Add coverage mirroring the existing
tests/dev-lead/patterns (integration or e2e withghstubbed): under-cap → PR created as today; at/over-cap → deferred (nogh pr create, issue left for retry, one comment); dry-run over-cap → PR still created. Register per this repo's bats/test-runner convention.
Non-goals
- Changing the cap value or exempt list (owned by
standards/pr-limits.jsonin petry-projects/.github; signed off at 50). - Per-source sub-caps (none configured; single org-wide ceiling).
- The success-metric report (epic Phase 4 / petry-projects — workflow failures detected 2026-06-09 #510) and the prose standard (docs(agents): tag protection + mutable-ref exception (#505) #509).
Scope note
Enforcement is org-wide (this is the central engine serving all petry-projects repos) and the cap counts org-wide. At 50 vs a ~44 baseline it refuses nothing today; it only bounds runaway growth. If a repo-scoped pilot is preferred instead, add a simple target_repo allowlist guard around the gate call and note it — but default is org-wide-soft.
References (verified to exist)
- petry-projects/.github:
scripts/lib/pr-limit-gate.sh,standards/pr-limits.json,docs/initiatives/pull-request-limits-adr.md(§7.2–§7.4) - this repo:
scripts/dev-lead-fix-issue.sh(L221/L277/L279),scripts/dev-lead-retry.sh,tests/dev-lead/
Phase 3 apply path for epic combsmith/.github#505. Kept deliberately small: one call site + runtime fetch + defer handling + tests. Note this edits the engine's own core PR-creation script — implement carefully and keep the dry-run/fail-open guarantees.
Activity
github-actions commentedon Jul 1, 2026
Thanks for the thorough write-up and for linking all the upstream references — this is a well-scoped Phase 3 story. Before we scope the implementation, could you clarify the preferred runtime fetch mechanism: should
dev-lead-fix-issue.shusegh api(authenticated, respects$GH_TOKEN) or a rawcurlagainst the public URL forpr-limit-gate.shandpr-limits.json? That choice affects the fail-open error handling and whether we need to cache the fetched files across retries.don-petry commentedon Jul 1, 2026
Dev-Lead Implementation Plan
Issue: #1003 — [PR-limits Phase 3] Wire the admission gate into dev-lead-fix-issue.sh (enforce the org-wide cap)
Scope
Add a single admission-gate call site in
scripts/dev-lead-fix-issue.shimmediately before branch/PR creation: fetch the guard (scripts/lib/pr-limit-gate.sh) + config (standards/pr-limits.json) frompetry-projects/.github@mainat runtime, callplg_admission_gate "dev-lead", and ondeferskip PR creation while leaving the issue in its ready/dev-leadstate with one idempotent deferral comment — all fail-open and dry-run-safe.Implementation Checklist
scripts/dev-lead-fix-issue.sh: add_plg_fetch(runtime fetch of guard + config from the public standards repo viagh api .../contents+ base64, with aPLG_STANDARDS_DIRlocal-override for tests, mirroringseed-repo-template.sh:_fetch_standard)scripts/dev-lead-fix-issue.sh: addadmission_gate_allows— sources the fetched guard, points it at the fetched config via$PR_LIMITS_CONFIG, callsplg_admission_gate "dev-lead"; fail-open (return allow +::warning::) on any fetch/source/undefined-function errorscripts/dev-lead-fix-issue.sh: add idempotentpost_deferral_comment(marker<!-- dev-lead-issue-deferred -->, checks prior comments so retries don't spam)scripts/dev-lead-fix-issue.sh: call the gate inmain()immediately beforegit checkout -b(L221); on defer, post the comment, leave the issue labeleddev-lead, andexit 0(no branch, no PR)Tests to Write
tests/dev-lead/unit/test_fix_issue_pr_limit_gate.bats(mirrorstest_fix_issue.batsstub patterns; injects a fake guard + config viaPLG_STANDARDS_DIR)allow→ PR created as today (gh pr createinvoked, exit 0)defer→ nogh pr create, one deferral comment posted, issue left for retry, exit 0[dry-run], not deferred)::warning::Test Command
bats tests/dev-lead/unit/test_fix_issue_pr_limit_gate.bats(full:bats tests/dev-lead/unit/) andbash .dev-lead/scripts/dev-lead-lint.shdon-petry commentedon Jul 1, 2026
Dev-Lead: Implementation Complete
Wired the PR-limit admission gate into the single dev-lead PR-creation choke point so the org-wide open-automation-PR cap is enforced at the source (Phase 3, epic combsmith/.github#505).
Plan Execution
_plg_fetch— runtime fetch of the guard + config frompetry-projects/.github@main(contents API + base64 via$GH_TOKEN;PLG_STANDARDS_DIRlocal-checkout override for tests/offline, mirroringseed-repo-template.sh). Config stays authoritative in the public repo — not vendored.admission_gate_allows— sources the fetched guard, points it at the fetched config via$PR_LIMITS_CONFIG, callsplg_admission_gate "dev-lead"; fail-open (allow +::warning::) on any fetch/source/undefined-function error.deferral_comment_exists/post_deferral_comment— single idempotent note (marker<!-- dev-lead-issue-deferred -->), so drain-retries don't spam.main()immediately beforegit checkout -b: on defer, open nothing, leave the issue in its ready/dev-leadstate, post the note,exit 0.tests/dev-lead/stub patterns; auto-registered (CI runsbats tests/dev-lead/unit/).Test Results
Gate tests (
bats tests/dev-lead/unit/test_fix_issue_pr_limit_gate.bats):Regression — existing fix-issue suite:
ok 16 fix-issue: lint fails → posts lint-failure comment on the issue(16/16). Fulltests/dev-lead/unit/= 654/655; the one failure (test_engine_mcp.bats#28, "REVIEW_MCP_DEBUG healthy handshake") is pre-existing and unrelated — it fails identically on a clean tree (verified viagit stash) and touchesengine.shMCP debug, not this change. Required lintbash .dev-lead/scripts/dev-lead-lint.sh→ all checks passed;shellcheck -S warning scripts/dev-lead-fix-issue.sh→ clean.Files Changed
scripts/dev-lead-fix-issue.sh: added the fetch + gate + idempotent-defer helpers and the single call site before branch/PR creation.tests/dev-lead/unit/test_fix_issue_pr_limit_gate.bats(new): under-cap→PR created; at/over-cap→deferred (nogh pr create, one comment, issue left for retry); idempotent (no second comment); dry-run over-cap→existing path unchanged; fetch-failure→fail-open.Notes
DEV_LEAD_DRY_RUNearly-exit (the point "immediately beforegit checkout -b"), so a dry-run never reaches the gate and never defers — the existing dry-run path is unchanged, exactly as AC4 requires. AC6's "dry-run over-cap → PR still created" is therefore verified as "dry-run over-cap → not deferred, existing dry-run path runs unchanged" (a real dry-run never opens a PR regardless).return 1from it means a real defer (its own counting step already fail-opens).don-petry commentedon Jul 1, 2026
Dev-Lead: issue #1003 implementation failed — will retry
The engine failed (
engine-error— e.g. a timeout or transient engine error). This was attempt 1 of 3; dev-lead will retry automatically. You can also re-apply thedev-leadlabel to retry now.engine-errordon-petry commentedon Jul 1, 2026
Superseded by hand-authored PR #1004 (which
Closes #1003). The dev-lead engine implemented + validated this design here but hit the 1800s CI timeout before it could push, so removing thedev-leadlabel to stop futile 30-min retries. Tracking completion via #1004.feat(pr-limits): wire the admission gate into dev-lead-fix-issue.sh (#…