Skip to content

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

Closed

Description

@don-petry

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

  1. 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 main afterwards and find your change missing, which is how this was caught.

  2. 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 — precedents 1dc8b119, 1e3816be, d0d1ff18. If dev-lead can clobber them, that escape hatch cannot be relied on, and the two hand-applied steering commits on petry-projects/.github#1021 survived only because dev-lead happened not to push again afterwards.

  3. 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 the advisory-review-gate.sh size guard ("non-blocking means fewer lines"). dev-lead had raised it 500 → 515; the steering commit instead moved the helper into scripts/lib/partial-evidence-marker.sh, guarded the three call sites, and restored -lt 500 (gate at 498). After the force-push, main carries 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 a needs-human-review hold), 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

  1. github-actions commented on Aug 31, 2026

    @github-actions
    Contributor

    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?

  2. added
    dev-leadFor dev-lead agent pickup
    and removed
    needs-human-reviewFlagged by automated PR review agent
    on Aug 31, 2026
  3. don-petry commented on Aug 31, 2026

    @don-petry
    CollaboratorAuthor

    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 the
    true 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.md step 6 (engine-side direct force-push)

    When any earlier git fetch in the run refreshes origin/<branch> to the
    maintainer'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 into push_no_clobber; add --force-if-includes; log discarded SHAs.
    • scripts/lib/auto-merge.sh: source git-push-guard.sh; push_with_merge_guard calls incorporate_remote_head first; 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-includes present, 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.sh

  4. don-petry commented on Aug 31, 2026

    @don-petry
    CollaboratorAuthor

    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.

    Review 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.

  5. added a commit that references this issue on Aug 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugBug reportsdev-leadFor dev-lead agent pickup

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions