Repository navigation
dev-lead force-pushes from a stale base and silently discards maintainer steering commits (observed on #1604: a human commit was destroyed and its decision reverted) #1607
Description
Defect
dev-lead force-pushes its fix commits from a stale local base, silently discarding any commit pushed to the branch since it last fetched. A maintainer steering commit was destroyed this way today, and the change it made was reverted on main as a result.
Reproduction (observed, PR #1604)
| when | what |
|---|---|
13:06:43Z |
dev-lead pushes b7a9fa68 — branch head |
13:22:58Z |
maintainer pushes 75f5850e (parent b7a9fa68) — branch head |
13:25:42Z |
dev-lead pushes e938c584 — parent b7a9fa68, not 75f5850e |
13:34Z |
PR squash-merges as 9b3bc2b0; the maintainer commit is absent from main |
Both commits share the same parent, so dev-lead did not build on the branch as it stood — it rebuilt from the base it had cached and force-pushed. 75f5850e still exists as a reachable object via the API but is not an ancestor of main:
$ git merge-base --is-ancestor 75f5850e origin/main → NO
$ gh api …/commits/75f5850e --jq .parents[].sha → b7a9fa68
$ gh api …/commits/e938c584 --jq .parents[].sha → b7a9fa68
Why this is serious
-
It silently destroys human work. No conflict, no warning, no comment — the commit simply vanishes from the branch. The only way to notice is to diff
mainafterwards and find your change missing, which is how this was caught. -
It makes the sanctioned steering-commit workflow unsafe. The fleet's own operating rule (
R8) explicitly sanctions maintainer steering commits directly on dev-lead branches when cycle budgets are exhausted — precedents1dc8b119,1e3816be,d0d1ff18. If dev-lead can clobber them, that escape hatch cannot be relied on, and the two hand-applied steering commits onpetry-projects/.github#1021survived only because dev-lead happened not to push again afterwards. -
It reverted a decision, not just a diff. The discarded commit implemented explicit written steering on
#1596: "Do NOT raise the threshold to make this pass" — referring to theadvisory-review-gate.shsize guard ("non-blocking means fewer lines"). dev-lead had raised it500 → 515; the steering commit instead moved the helper intoscripts/lib/partial-evidence-marker.sh, guarded the three call sites, and restored-lt 500(gate at 498). After the force-push,maincarries the gate at 509 lines with the guard at 515 — the outcome the steering explicitly rejected.This is the same failure class as
.github#990(dev-lead implementing the opposite of a written maintainer decision),#1532(released past aneeds-human-reviewhold), and#1595— an agent proceeding past recorded human direction. Here the mechanism is git rather than labels.
Acceptance criteria
- Before pushing, dev-lead fetches the remote head and rebases/merges onto it; it must never force-push over a commit it has not incorporated.
- If the remote head has moved and cannot be incorporated cleanly, dev-lead stops and escalates rather than force-pushing.
- Any force-push is logged with the discarded SHA(s) so it is auditable.
- A push by an actor other than dev-lead's own identity is treated as steering: never discarded, and called out in the run summary.
- Test: place a commit on the branch after dev-lead's last fetch, then run a fix pass — assert the resulting head contains both commits.
- Bounded audit: check whether other dev-lead branches lost commits this way (compare each PR's commit list for sibling commits sharing a parent).
Immediate follow-up
The size-guard change from 75f5850e needs re-applying to main — the gate is currently 509 lines against a 515 guard, where the agreed outcome was 498 against 500. The commit content is intact and recoverable at 75f5850e.
Related: #1596 (the PR this happened on), .github#990, #1532, #1595, #1567 — the same class of agent-overrides-human defect.
Activity
github-actions commentedon Aug 31, 2026
Thank you for the thorough write-up and the git-level evidence — this is exactly the detail needed to act on it. The reproduction is clear and the acceptance criteria are well-scoped, so no major clarifying questions are needed; the one thing that would help prioritize the bounded audit: do you have a list of other PR numbers where dev-lead was the primary pusher, or should we derive that from the API against all merged PRs in the window?
don-petry commentedon Aug 31, 2026
Dev-Lead Implementation Plan
Issue: #1607 — dev-lead force-pushes from a stale base and silently discards maintainer steering commits
Root cause
Three force-push paths lease against the remote-tracking ref with a bare
--force-with-lease(no--force-if-includes) and never re-fetch/reconcile thetrue remote head before pushing:
scripts/lib/git-push-guard.sh :: push_no_clobber(fix-reviews)scripts/lib/auto-merge.sh :: push_with_merge_guard(fix-ci)prompts/dev-lead/rebase.mdstep 6 (engine-side direct force-push)When any earlier
git fetchin the run refreshesorigin/<branch>to themaintainer's commit, the lease silently passes and the force-push clobbers a
commit HEAD never incorporated — the observed #1604 destruction of
75f5850e.Scope
Make every dev-lead push fetch the true remote head and incorporate or
escalate before pushing; treat any non-dev-lead commit as steering that is
never discarded; log discarded SHAs; add a bounded audit for the same fingerprint
on other branches.
Implementation Checklist
scripts/lib/git-push-guard.sh: add_commit_is_dev_lead+incorporate_remote_head(fetch remote head; if it has commits HEAD lacks: classify dev-lead-own vs foreign/steering, rebase to incorporate foreign or escalate on conflict, flag steering to$GITHUB_STEP_SUMMARY, log SHAs). Wire intopush_no_clobber; add--force-if-includes; log discarded SHAs.scripts/lib/auto-merge.sh: source git-push-guard.sh;push_with_merge_guardcallsincorporate_remote_headfirst; add--force-if-includes; log discarded SHAs.prompts/dev-lead/rebase.md: step 6 force-push uses--force-with-lease --force-if-includes.scripts/lib/stale-base-siblings.sh: pure detector for the stale-base fingerprint (commits sharing a parent) — AC feat: feature ideation framework — adversarial proposer/challenger/synthesizer #6 bounded audit.scripts/dev-lead-stale-base-audit.sh: thin read-only wrapper over the detector.Tests to Write
tests/dev-lead/unit/test_incorporate_remote_head.bats: foreign commit after last fetch is incorporated (head contains both); un-incorporable foreign commit → escalate, remote preserved; own-rewrite → force-with-lease succeeds.tests/dev-lead/unit/test_push_no_clobber.bats: extend — AC feat: add Copilot engine support via REVIEW_ENGINE toggle #5 (post-fetch commit survives a fix push),--force-if-includespresent, steering never discarded, no bare--force.tests/dev-lead/unit/test_stale_base_siblings.bats: detector flags sibling commits sharing a parent; clean history passes.Test Command
bats tests/dev-lead/unit/(CI:.github/workflows/test-dev-lead.yml); lint:bash .dev-lead/scripts/dev-lead-lint.shdon-petry commentedon Aug 31, 2026
Dev-Lead: Implementation Complete — PR #1610
The implementation for issue #1607 is durable: commits are pushed and a pull request is open. This record is backed by verifiable artifacts you can confirm in one click.
85e39b09385aa45e67eb9df188def31518e35637Review happens on the PR. Acceptance criteria and test results are evidenced by the PR's diff and CI checks — not asserted here. If a later stage fails, this claim is retracted in place.
feat: implement issue #1607 — dev-lead force-pushes from a stale base…
fix(dev-lead): push guard trusts its own records, not commit email (#…