{"record":{"id":"0fc792ee17464f87","repo":"can1357/oh-my-pi","slug":"head-changed-since-preflight-expected-head-12","errorCode":null,"errorMessage":"HEAD changed since preflight ({expected_head[:12]} → {head[:12]}); aborting push.","messagePattern":"HEAD changed since preflight \\((.+?) → (.+?)\\); aborting push\\.","errorType":"exception","errorClass":"HeadDriftError","httpStatus":null,"severity":"warning","filePath":"python/robomp/src/git_ops.py","lineNumber":808,"sourceCode":"    history rewrites (e.g. the agent doing `git commit --amend --reset-author\n    --no-edit` to fix author identity) while still refusing the push if origin\n    has moved since our last fetch — i.e. it never clobbers work the bot\n    didn't see.\n\n    When `expected_head` is supplied, this verifies the *local* HEAD matches\n    before pushing — anything else means an unexpected commit raced in inside\n    our own worktree between the orchestrator's preflight and this call, and\n    the push is aborted with `HeadDriftError`. This is a separate concern from\n    `--force-with-lease`, which compares against the remote ref.\n    \"\"\"\n    slot_kwargs = _slot_subprocess_kwargs(slot_uid)\n    git_safe_directory = safe_directory\n    if git_safe_directory is None and slot_kwargs:\n        git_safe_directory = repo_dir\n\n    head = rev_parse_head(repo_dir, safe_directory=git_safe_directory, **slot_kwargs)\n    if expected_head and head != expected_head:\n        raise HeadDriftError(\n            [\"git\", \"push\"],\n            128,\n            \"\",\n            f\"HEAD changed since preflight ({expected_head[:12]} → {head[:12]}); aborting push.\",\n        )\n    # Probe the local remote-tracking ref. Missing → first push; we pin the\n    # lease to the empty value so the push only succeeds if origin still has\n    # no `<branch>`. Present → pin to that SHA.\n    probe = _run_git(\n        [\"rev-parse\", \"--verify\", \"--quiet\", f\"refs/remotes/origin/{branch}\"],\n        cwd=repo_dir,\n        token=None,\n        safe_directory=git_safe_directory,\n        **slot_kwargs,\n    )\n    expected_remote = probe.stdout.strip() if probe.returncode == 0 else \"\"\n    if remote_url is None:\n        push_extra_env: dict[str, str] | None = None","sourceCodeStart":790,"sourceCodeEnd":826,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/python/robomp/src/git_ops.py#L790-L826","documentation":"`push` re-checks HEAD with `rev_parse_head` immediately before pushing; if it differs from the `expected_head` captured during preflight, it raises `HeadDriftError` (a GitCommandError subclass with code 128) and aborts the push. This guards against pushing a commit someone else (or another step) added to the local branch after the preflight snapshot — preventing unintended or unsafe pushes.","triggerScenarios":"Another process/IDE/hook commits or amends on the current branch between the preflight HEAD capture and the push call; an auto-formatter or pre-commit hook creating commits; concurrent runs of the tooling on the same repo.","commonSituations":"Long gap between preflight and push while the developer works in the repo, CI job sharing a checkout with another job, hooks that amend or create fixup commits automatically.","solutions":["Re-run the push operation from the start so preflight captures the current HEAD","Check `git log`/`git reflog` to see what moved HEAD — if the new commit is unwanted, reset it before retrying","Ensure no concurrent processes (other tool runs, IDE auto-commit) operate on the repo during push","If the drift is expected and safe, pass/refresh the expected_head to the current HEAD value"],"exampleFix":"// before\nexpected = rev_parse_head(repo_dir)\n...long work...\npush(repo_dir, expected_head=expected)  # HeadDriftError\n// after\nexpected = rev_parse_head(repo_dir)\npush(repo_dir, expected_head=expected)  # preflight and push adjacent; retry whole flow on drift","handlingStrategy":"retry","validationCode":"current = rev_parse_head(repo_dir)\nif expected_head and current != expected_head:\n    # refresh preflight instead of pushing\n    expected_head = current","typeGuard":null,"tryCatchPattern":"from tenacity import retry, retry_if_exception_type, stop_after_attempt\n@retry(retry=retry_if_exception_type(HeadDriftError), stop=stop_after_attempt(2))\ndef safe_push(repo_dir):\n    expected = rev_parse_head(repo_dir)\n    push(repo_dir, expected_head=expected)","preventionTips":["Minimize the window between preflight and push — capture expected_head as late as possible","Don't run concurrent tooling on the same checkout; use per-run worktrees","Check git reflog after drift to identify what moved HEAD before retrying","Treat drift as a signal: re-run the full preflight rather than overriding expected_head blindly"],"tags":["git","race-condition","push","concurrency"],"backgroundTag":"git-head-drift","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}