fix(dev-lead): push guard trusts its own records, not commit email
Problem
The #1607 push guard decided "is this remote commit dev-lead's own?" from the committer email. Since ADR-0008, dev-lead commits as 36422719+don-petry@users.noreply.github.com. The owner's machine and every Claude Code session use that address too. So every owner commit on a dev-lead branch was classified as dev-lead's. incorporate_remote_head then took the "own history rewrite" path, and the caller force-pushed over the commit. This happened on combsmith/.github #1314 and #1315 on 2026-10-11 (timeline in #2313).
Change
_commit_is_dev_lead now trusts only evidence that dev-lead records itself. The committer identity is no longer consulted.
| Evidence | Where it comes from | What a copy does |
|---|---|---|
| SHA this run pushed | _PUSH_GUARD_PUSHED, appended after each successful push_no_clobber / push_with_merge_guard | n/a (exact SHA) |
Dev-Lead-Stamp: <hash> trailer | dev_lead_stamp_trailer (git-identity.sh) on every commit the harness makes: fix-reviews commit_and_push, fix-ci, and fix-issue (final + checkpoint). The hash covers the commit's own tree and parents. | An amend, cherry-pick or rebase keeps the trailer text but changes the tree or parents, so the copy does not match |
Any other commit counts as steering: the guard rebases it in, or stops with return 2. The decision is a pure function, _push_guard_ownership_verdict (ADR-0004). ADR-0011 rule 1 now defines what "foreign" means.
Option 1 from the issue alone would cover only this run's pushes. The stamp covers dev-lead's rewrite of a commit an earlier run made.
Risk
- Commits the model makes itself are not stamped. A commit the engine creates with its own
git commitis recorded only once this run pushes it. A later run treats it as foreign, so a rewrite of it rebases or stops instead of force-pushing. That trade fails safe: nothing is lost, and the worst case is an escalation. - The stamp is a record, not a signature. It stops accidental copies, not someone who computes a stamp on purpose.
- The trailer line is 56 characters, within commitlint's 100-character footer limit.
Test plan
- New bats cases in
tests/dev-lead/unit/test_incorporate_remote_head.bats(#2313):- an owner commit with dev-lead's exact email is kept (rebased in), and a conflicting one stops the push with return 2
- dev-lead's rewrite of a commit an earlier run stamped still force-pushes, and the discarded SHA is logged
- a commit this run pushed is its own even without a stamp
- a stamp copied onto another author's commit (the owner amending dev-lead's stamped commit) is not trusted, and the owner's commit survives
- the pure verdict covers a pushed SHA, a matching stamp, a stamp with the wrong parent, a merge's parent order, a SHA-prefix near miss, and no record at all
- dev-lead's exact noreply address alone is foreign (replaces the #1610 email test)
- The new cases fail against
main's guard: the owner-commit, conflict, spoofed-stamp, in-run ledger, verdict and email cases all go red. -
test_push_no_clobber.bats: the seed commit is now stamped as an earlier run's commit. - Locally green:
test_incorporate_remote_head,test_push_no_clobber,test_claim_landing,test_claim_sweepandtest_auto_merge(142 tests). - shellcheck
--severity=warning -xon all ofscripts/,check-bats-negation,check-duplicate-decls,check-fixed-tmp-paths, and markdownlint on ADR-0011. - Full
tests/dev-lead/unit/suite: CI.
Rollback
Revert this commit. Without the guard's ledger and stamp check, the unused trailer on new commits does nothing.
Monitoring
When an owner commit is incorporated, the run summary now says "maintainer steering incorporated (#1607)". Before this change, that line never fired for owner commits. The thing to watch is any head_ref_force_pushed on a dev-lead PR that the owner also pushed to.
Closes #2313
🤖 Generated with Claude Code