Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,12 @@ per ADR-0004), not by prompt text alone:
1. **Incorporate or stand down, never force.** Before pushing, the persona
reconciles with the *true* remote head. It rebases a foreign commit in, or
stops and escalates when that cannot be done cleanly. A foreign commit is
never discarded (`push_no_clobber`, `scripts/lib/git-push-guard.sh`).
never discarded (`push_no_clobber`, `scripts/lib/git-push-guard.sh`). A
commit is foreign unless the persona's own records say it wrote it: a SHA
it pushed, in this run or an earlier one (the `refs/notes/dev-lead-pushed`
ledger on the remote), or a `Dev-Lead-Stamp` trailer that matches the
commit's tree and parents. The committer identity is not a record, because
the owner commits as the same account (#2313).
2. **"Pushed" is a verdict, not an assumption.** After the push, the harness
compares the pre-pass head, the pushed SHA and the re-fetched remote head
(`cl_push_landed_verdict`, `scripts/lib/claim-landing.sh`). Any result other
Expand Down
5 changes: 4 additions & 1 deletion scripts/dev-lead-fix-ci.sh
Original file line number Diff line number Diff line change
Expand Up @@ -380,7 +380,10 @@ main() {
# before committing or commit fails with "fatal: empty ident name".
setup_git_identity
git add -A
git commit -m "fix(ci): auto-fix for $(echo "$CHECKS_JSON" | jq -r '.[0].name // "CI failure"') [skip ci-relay]"
# Dev-Lead-Stamp: lets a later run's push guard recognise this commit as
# dev-lead's own (#2313).
git commit -m "fix(ci): auto-fix for $(echo "$CHECKS_JSON" | jq -r '.[0].name // "CI failure"') [skip ci-relay]" \
-m "$(dev_lead_stamp_trailer)"
fi
# Sync-stub guard (#2185/#2212): on a `standards-sync` PR a CI fix must not
# change a stub copied verbatim from combsmith/.github/standards/ (e.g.
Expand Down
20 changes: 17 additions & 3 deletions scripts/dev-lead-fix-issue.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ set -euo pipefail

source "$(dirname "$0")/engine.sh"
source "$(dirname "$0")/lib/git-identity.sh"
# _push_guard_record_pushed: record what this run pushes in the cross-run ledger
# a later run's push guard reads (#2313).
source "$(dirname "$0")/lib/git-push-guard.sh"
# Pure completion-claim helpers (#1445): body_has_completion_claim /
# claim_is_retracted / supersede_claim_body + the PC_CLAIM_RETRACTED_MARKER.
source "$(dirname "$0")/lib/premature-closure-detect.sh"
Expand Down Expand Up @@ -224,11 +227,15 @@ checkpoint_push_partial_work() {
if $has_uncommitted; then
git add -A >/dev/null 2>&1 || return 1
git commit -m "${CHECKPOINT_COMMIT_PREFIX} partial work for issue #${ISSUE_NUMBER} (stage timeout — unreviewed, do not merge)" \
>/dev/null 2>&1 || return 1
-m "$(dev_lead_stamp_trailer)" >/dev/null 2>&1 || return 1
fi

# Push the branch so the work lands on the remote before the run exits.
# Push the branch so the work lands on the remote before the run exits, and
# record its commits as dev-lead's for later runs' push guard (#2313).
local published
published=$(git rev-list HEAD --not --remotes=origin 2>/dev/null || true)
git push --set-upstream origin "$br" >/dev/null 2>&1 || return 1
_push_guard_record_pushed "$published" origin

printf '%s\n' "$br"
return 0
Expand Down Expand Up @@ -669,7 +676,9 @@ ${lint_output}

if $has_uncommitted; then
git add -A
git commit -m "feat: implement issue #${ISSUE_NUMBER} — ${ISSUE_TITLE}"
# Dev-Lead-Stamp: lets a later run's push guard recognise this commit as
# dev-lead's own when it reconciles the PR branch (#2313).
git commit -m "feat: implement issue #${ISSUE_NUMBER} — ${ISSUE_TITLE}" -m "$(dev_lead_stamp_trailer)"
fi

# Empty net-diff guard (#1786, slice 1 of #1620): before pushing or opening a
Expand All @@ -682,7 +691,12 @@ ${lint_output}
abort_empty_net_diff "$base_ref"
fi

# Record the pushed commits (the engine's own included) as dev-lead's, so a
# later run's push guard does not mistake them for steering (#2313).
local published
published=$(git rev-list HEAD --not --remotes=origin 2>/dev/null || true)
git push --set-upstream origin "$branch"
_push_guard_record_pushed "$published" origin

# Head SHA of the durable, pushed work — the verifiable artifact the completion
# claim references (#1445, AC #2).
Expand Down
4 changes: 3 additions & 1 deletion scripts/dev-lead-fix-reviews.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3251,7 +3251,9 @@ commit_and_push() {
# an if-statement condition, so git commit failures would be silently swallowed
# otherwise. Using exit (not return) ensures CI fails visibly instead of posting a
# false "Changes committed and pushed" comment.
git commit -m "$commit_msg" || { echo "::error::git commit failed — check git identity configuration on the runner" >&2; exit 1; }
# The Dev-Lead-Stamp trailer is how a later run's push guard knows this
# commit is dev-lead's own; the committer email cannot say (#2313).
git commit -m "$commit_msg" -m "$(dev_lead_stamp_trailer)" || { echo "::error::git commit failed — check git identity configuration on the runner" >&2; exit 1; }
fi
# No-op guard (#1340, extended #1786): a fix/review/rebase pass that reverts
# the PR's own changes nets the base…head diff to zero. Pushing it would let a
Expand Down
7 changes: 6 additions & 1 deletion scripts/lib/auto-merge.sh
Original file line number Diff line number Diff line change
Expand Up @@ -147,10 +147,14 @@ push_with_merge_guard() {
return "$reconcile_rc"
fi

local errf
local errf publishing
errf="$(mktemp)"
# Record what this push publishes so a later reconcile in the run knows these
# commits are dev-lead's own (#2313).
publishing=$(_push_guard_publishing "${1:-}")
if git push "$@" 2>"$errf"; then
rm -f "$errf"
_push_guard_record_pushed "$publishing" "${1:-origin}"
# Refresh the held SHA after our own push so the EXIT-trap restore uses
# --match-head-commit against the new head rather than the pre-push SHA.
_AM_HEAD_SHA=$(git rev-parse HEAD 2>/dev/null || true)
Expand All @@ -175,6 +179,7 @@ push_with_merge_guard() {
echo "::notice::push rejected (non-fast-forward) and HEAD has diverged from ${upstream} — history was rewritten (likely a self-rebase); retrying with a pinned lease. Overwriting dev-lead's own commit(s): ${discarded:-<none>}" >&2
if _pinned_force_push "$@" 2>>"$errf"; then
rm -f "$errf"
_push_guard_record_pushed "$publishing" "${1:-origin}"
_AM_HEAD_SHA=$(git rev-parse HEAD 2>/dev/null || true)
return 0
fi
Expand Down
79 changes: 79 additions & 0 deletions scripts/lib/git-identity.sh
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,82 @@ setup_git_identity() {
fi
git config user.name "$bot"
}

# dev-lead commit stamp (#2313). Since ADR-0008 dev-lead commits with the same
# noreply email as the owner and every Claude Code session on the owner's
# machine, so the committer identity cannot tell dev-lead's commits from theirs.
# dev-lead marks the commits it creates with a trailer the push guard can check:
#
# Dev-Lead-Stamp: <hash of "tree <T>\nparent <P>..." — the commit's own tree and
# parents, in the order a commit object lists them>
#
# The harness stamps the commits it makes as it makes them. It never rewrites a
# commit after the fact to add one: the engine's claims, the #2143 test handoff
# and the #2269 this-pass checks all cite exact SHAs. Commits the engine makes
# with its own `git commit` are covered instead by the push guard's ledger of
# pushed SHAs (refs/notes/dev-lead-pushed, git-push-guard.sh).
#
# The stamp is bound to the commit's content and position. Amending a stamped
# commit (new tree) or cherry-picking or rebasing it (new parents) keeps the
# trailer text but breaks the match, so a copied stamp marks nothing. It is a
# record dev-lead keeps, not a signature: it guards against copies made by
# accident, not against someone who computes a stamp on purpose. A reword that
# changes only the message keeps tree and parents, so the stamp still matches:
# dev-lead may then rewrite that commit, which can lose the reworded message but
# never content that is not already in its own commit.
# Not readonly: the push guard sources this file too, and a second `readonly`
# assignment would abort a script running under `set -e`.
DEV_LEAD_STAMP_KEY='Dev-Lead-Stamp'

# dev_lead_stamp_of <tree> [parent...] — echo the stamp for a commit with this
# tree and these parents. Pure: hashes its arguments, reads no repository.
dev_lead_stamp_of() {
local tree="${1:-}"
[ -n "$tree" ] || return 1
shift
{
printf 'tree %s\n' "$tree"
local p
for p in "$@"; do printf 'parent %s\n' "$p"; done
} | git hash-object --stdin
}

# dev_lead_stamp_trailer — echo the `Dev-Lead-Stamp: <hash>` line for the commit
# `git commit` is about to make from the current index: tree = the staged tree,
# parents = HEAD (none on an unborn branch) plus MERGE_HEAD when a merge is being
# concluded. Pass it as the last `-m` of the commit so it lands in the trailer
# block. Echoes nothing and returns 1 when the index cannot be written; the
# commit then goes out unstamped and the push guard treats it as foreign.
dev_lead_stamp_trailer() {
local tree head merge_head_file stamp
local -a parents=()
tree=$(git write-tree 2>/dev/null) || return 1
if head=$(git rev-parse --verify --quiet HEAD 2>/dev/null); then
parents+=("$head")
fi
merge_head_file=$(git rev-parse --git-path MERGE_HEAD 2>/dev/null || true)
if [ -n "$merge_head_file" ] && [ -f "$merge_head_file" ]; then
local mh
while IFS= read -r mh; do
[ -n "$mh" ] && parents+=("$mh")
done < "$merge_head_file"
fi
stamp=$(dev_lead_stamp_of "$tree" ${parents[@]+"${parents[@]}"}) || return 1
printf '%s: %s\n' "$DEV_LEAD_STAMP_KEY" "$stamp"
}

# dev_lead_stamp_matches <tree> <parents> <message> — true when <message> has a
# Dev-Lead-Stamp line equal to the stamp of <tree> and the space-separated
# <parents>. Pure apart from `git hash-object --stdin`.
dev_lead_stamp_matches() {
local tree="${1:-}" parent_list="${2:-}" message="${3:-}" expected claimed
[ -n "$tree" ] || return 1
# Word-split on purpose: <parents> is the space-separated %P list.
# shellcheck disable=SC2086
expected=$(dev_lead_stamp_of "$tree" $parent_list 2>/dev/null) || return 1
[ -n "$expected" ] || return 1
while IFS= read -r claimed; do
[ "$claimed" = "$expected" ] && return 0
done < <(sed -nE "s/^${DEV_LEAD_STAMP_KEY}:[[:space:]]*([0-9a-f]+)[[:space:]]*\$/\1/p" <<<"$message")
return 1
}
Loading
Loading