feat(automation): guard prepare-pr pushes against stale merge bases - #1418
Conversation
Design Review (Fable 5) — 🟡 CONCERNSAdvisory design-level review of Design-Verdict: CONCERNS Sound root-cause fix, but the guard binds to the push only by SKILL.md prose — the automation that failed must voluntarily run it. Watch
Suggestions
[DESIGN-REVIEWED] 18b6fce |
Opus 5 Review — ✅ no blocking findingsReviewed Verdict parsed from the review's SHA-scoped output markers for commit False positive or not applicable? A repository writer can comment: |
GPT 5.6 Review — ✅ no blocking findingsGPT 5.6 completed its review of This comment is updated in place on each push. Review detailsNo findings. False positive or not applicable? A repository writer can comment: |
8574f9d to
221c7a8
Compare
|
Addressed GPT 5.6 blocking finding (SKILL.md:120 guard-after-squash): The guard now runs at two points in the workflow:
Additional fixes:
Rebased onto fresh upstream/main (b15c412). One commit, 4-file diff, merge-tree clean. |
221c7a8 to
73c743a
Compare
73c743a to
45bd32f
Compare
45bd32f to
70cafcc
Compare
|
Ancestry check now asserts |
70cafcc to
729595b
Compare
729595b to
6453d50
Compare
4b1f08a to
f03dc0b
Compare
|
Re-squashed on ae859c7 (current main) — preserves the |
|
Pushed fd85c9c: credential redaction now covers bracketed IPv6 authorities ( |
|
Phase-1 sync fetch now uses an explicit |
UX Review (Fable 5) — ⏭️ skippedRevision |
|
Fetch diagnostics now redact before truncating — a slice can no longer split a credential out of regex reach ( |
|
Pushed 44a9fe6: fetch diagnostics now also redact scheme-less scp-style userinfo ( |
|
SCP userinfo redaction (pass 3) now covers dotless colon-path hosts ( |
|
Rebased onto fresh main (5cb9801). Post-authority URL redaction ( |
|
Force-pushed to Fixes in this push:
Verification: pytest push_guard + security_posture + pip_deps suites pass (144 tests); black/isort/flake8/mypy clean; brand gate clean; Auto-Improvement count matches main (9); push_guard.py self-test passes on this commit. |
|
Rebased to |
|
Force-pushed No other changes; the |
|
Round 12+13 fix pushed (head
All 116 test_push_guard tests pass locally; black/isort/flake8 clean; brand gate green; merge-tree clean. |
|
Amended push (head Additional fix folded in: Derived fetch diagnostics (GPT round-13) — raw 14 new tests added: |
|
Push a331766: remove stale |
|
Pushed 7e302f5 (rebased onto fresh main 7e1f744). Changes at this head:
Closes the GPT round-14/15 blocking findings (dead-code incoherence + post-squash ancestry guard that always rejects). |
…uash ancestry check Remove the dead redact_credentials helper, its three URL regexes (_URL_USERINFO_RE, _URL_AFTER_AUTHORITY_RE, _SCP_USERINFO_RE), and all unit tests that exercised them. The function has zero call sites after round 13 replaced both scripts' fetch-error paths with the derived _classify_fetch_error classifier. The push_guard.py NON_EGRESS entry in security_posture.py (which falsely claimed the scrubber was applied) is also removed — resolving the deterministic CI failure on test_allowlist_has_no_stale_entries. Fix SKILL.md Phase 3 step 1: the maintainer-clobber ancestry check now runs pre-squash (against the pre-squash HEAD which still carries prior push history), is skipped entirely on first push (when origin/<branch> does not exist), and is explicitly not re-run after the squash. The SHA-pinned --force-with-lease remains as the at-push protection against concurrent pushes. Closes the GPT round-14/15 blocking findings (dead-code incoherence + post-squash ancestry guard that always rejects).
Problem
A force-push automation loop can resolve
upstream/mainto a stale local ref when the fetch step fails silently or the worktree was branched from an integration trunk carrying unshipped commits. When this happens, the force-push replays dozens of already-merged commits onto the remote feature branch, clobbering maintainer work and requiring a manual restore.Why it matters
Every automated rebase/push flow in the prepare-pr skill is vulnerable to this failure mode. Without a pre-push guard, a single network hiccup or stale worktree can destroy upstream progress on a feature branch.
Fix
Add a three-check pre-push guard (
push_guard.py) to the prepare-pr skill:origin/<base>ref.origin/<base>is actually an ancestor oforigin/<base>(catches branches forked from local integration trunks).origin/<base>(default threshold: 5 commits for a single-commit PR workflow).Also hardens
preflight.pyto fail closed whengit fetchfails and adds a high-commit-count warning.The SKILL.md documentation is updated to integrate the guard into the Phase 3 push flow.
Tests
test/test_push_guard.py— 8 tests covering:--max-aheadthresholdSummary
This PR combines 8 related changes:
Changes and rationale
Find and fix how a pr-maintenance rebase loop resolved upstream/main to a stale mirror (PR 918 clobber)
Problem: On 2026-07-31 22:24 UTC the loop pr-918-fix-rebase-preexec-conflict force-pushed feature/registry-ssh-url-parity from a STALE base: merge-base d6d8a0d instead of current upstream/main, carrying 114 duplicate re-creations of already-merged PRs (e.g. #971 exists on main as c4a1f68, appeared on the branch as f5946fe). The parent commit 509551b existed on NO shared branch. The push clobbered the maintainer's admission fix and cost a manual restore, a dismissed approval, and several CI rounds. Full incident…
Why it matters: Any future pr-maintenance loop that rebases and force-pushes a feature/* branch can clobber human work on an upstream PR the same way. This is the highest-blast- radius failure mode our automation has exhibited: it destroys maintainer commits on a repo where we hold direct write access.
Fix (symptoms → root cause → change): Chain from symptom to root cause before changing anything: Reconstruct where the stale ref came from: inspect the pr-maintenance loop worktree creation path and every place the automation resolves 'upstream/main' (worktree-local refs, an unfetched upstream remote, the local-origin.git bare mirror, or a clone made from kiki-trunk state). Commits 509551b / d6d8a0d may still exist locally — 'git cat-file -t' and 'git branch --contains' across worktrees/mirrors to locate the stale copy. Fix the resolution so…
Tests: Unit/integration test proving a push candidate with a stale merge-base is refused before any network push. Existing pr-maintenance flow tests still pass. No behavior change for the normal single-commit fast path.
Review focus: automation, security boundaries, regression coverage
prepare-pr guard: gate on single_commit, fix vacuous ancestry claim, detect replayed commits
Tests: Regression pinning finding 1 is prose-only (SKILL.md), no test needed — but keep all 12 existing tests green after the rename. New: replayed-commit refusal — branch whose ahead-commits include a patch-equivalent of an upstream commit (cherry-picked/replayed onto a stale fork point, <=5 total so the count check alone passes) → exit 40. Fails at 221c7a8…
Review focus: automation
Republish PR 1418: push guard-fix commit onto feature/stale-base-rebase-root-cause
Review focus: pr-maintenance
PR 1418: fix reversed ancestry check in push_guard pre-squash mode
Review focus: pr-maintenance
PR 1418: redact query-string credentials + register redactor call sites in the posture allowlist
Review focus: pr-maintenance
PR 1418: rebuild branch from the 6 intended files — stale-tree squash reverted ~104 merged files
Review focus: pr-maintenance
PR 1418: redact dotless/short-host and bracketed scp userinfo
Review focus: pr-maintenance
push_guard: pin base fetch refspec so narrow clones cannot validate a stale tip
Tests: Regression pinning finding 1: repo with a narrow
remote.origin.fetchrefspec that excludes the base (e.g. setremote.origin.fetchto a non-base branch), base advances on the remote, branch sits on the OLD tip -> guard must REFUSE (exit 40) after the fix (fails at 70cafcc: guard passes because origin/ never updates). Normal full-refspec repo…Review focus: pr-maintenance
Review map
src/kiro_crew/builtin_skills/kirocrew-dev/prepare-pr/SKILL.mdsrc/kiro_crew/builtin_skills/kirocrew-dev/prepare-pr/scripts/push_guard.pysrc/kiro_crew/builtin_skills/kirocrew-dev/prepare-pr/scripts/preflight.pytest/test_push_guard.pysrc/kiro_crew/builtin_skills/kirocrew-dev/prepare-pr/SKILL.mdsrc/kiro_crew/builtin_skills/kirocrew-dev/prepare-pr/scripts/push_guard.pysrc/kiro_crew/builtin_skills/kirocrew-dev/prepare-pr/scripts/preflight.pytest/test_push_guard.pytest/test_pip_deps_consistency.pysrc/kiro_crew/builtin_skills/kirocrew-dev/prepare-pr/scripts/push_guard.pytest/test_push_guard.pysrc/kiro_crew/builtin_skills/kirocrew-dev/prepare-pr/scripts/push_guard.pysrc/kiro_crew/builtin_skills/kirocrew-dev/prepare-pr/scripts/preflight.pysrc/kiro_crew/security_posture.pytest/test_push_guard.pysrc/kiro_crew/builtin_skills/kirocrew-dev/prepare-pr/SKILL.mdsrc/kiro_crew/builtin_skills/kirocrew-dev/prepare-pr/scripts/push_guard.pysrc/kiro_crew/builtin_skills/kirocrew-dev/prepare-pr/scripts/preflight.pysrc/kiro_crew/security_posture.pytest/test_push_guard.py+1 more
src/kiro_crew/builtin_skills/kirocrew-dev/prepare-pr/scripts/push_guard.pysrc/kiro_crew/builtin_skills/kirocrew-dev/prepare-pr/scripts/preflight.pytest/test_push_guard.pysrc/kiro_crew/builtin_skills/kirocrew-dev/prepare-pr/scripts/push_guard.pysrc/kiro_crew/builtin_skills/kirocrew-dev/prepare-pr/scripts/preflight.pytest/test_push_guard.pyCommits
72507fffix(push_guard): redact SCP userinfo for dotless/IPv6 hosts; Windows-…Validation
Local build, static-analysis, and test gates completed before publication. Upstream CI and review checks on the current PR revision remain the authoritative merge signal.
Changed files (6 files, +2278/-37)
src/kiro_crew/builtin_skills/kirocrew-dev/prepare-pr/SKILL.md(+9/-7)src/kiro_crew/builtin_skills/kirocrew-dev/prepare-pr/scripts/preflight.py(+48/-8)src/kiro_crew/builtin_skills/kirocrew-dev/prepare-pr/scripts/push_guard.py(+467/-0)src/kiro_crew/security_posture.py(+8/-1)test/test_pip_deps_consistency.py(+29/-21)test/test_push_guard.py(+1717/-0)