[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M7357KC2MZNHTDDSY4DT9CGJ":3,"skill-same:01M7357KC2MZNHTDDSY4DT9CGJ":49},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":18,"publishedAt":19,"linkOnly":20,"markdownContent":21,"markdownBody":22,"frontMatter":23,"refName":28,"commitSha":29,"skillDirectory":30,"forksCount":31,"repositoryHtmlUrl":32,"repositoryHomepageUrl":33,"ownerHtmlUrl":34,"ownerBlogUrl":6,"ownerDescription":6,"updatedAt":35,"install":36,"resources":48},"01M7357KC2MZNHTDDSY4DT9CGJ","push",null,"Pre-push check for active sessions.","Morrison-Lab\u002Fai-config","Morrison-Lab","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F104235520?v=4","01M734G0451S2G35MN4HFQK7XP","Morrison Lab","01M734G059BYFDFQA8YTPDGPMH",4,"MIT","https:\u002F\u002Fgithub.com\u002FMorrison-Lab\u002Fai-config\u002Fblob\u002Fmain\u002Fskills\u002Fpush\u002FSKILL.md",[],[],"2026-11-11 15:11:14",false,"---\nname: push\ndescription: \"Pre-push check for active sessions.\"\nuser-invocable: true\nallowed-tools:\n  - Bash\n  - AskUserQuestion\n---\n\n# push\n\nA guard you run **right before `git push`**. Other sessions, reviewers, and the\n`@claude` CI bot can all be working the same branch. Pushing blind risks\nclobbering their work or shoving commits onto a branch someone explicitly held.\nThis skill runs a short pre-push check, and if anything looks off it **stops and\nasks you** rather than pushing.\n\n## When this fires\n\n- The user says \"push\", \"push this\", \"push my changes\", or `\u002Fpush`.\n- Any time you're about to push commits to a **shared** branch (one with an open\n  PR, or one another session may be driving).\n\nIt does **not** need to fire for a brand-new branch that has never been pushed\nand has no PR — there's nothing to collide with. Still run the protected-branch\ncheck in that case.\n\n## Pre-push checks\n\nRun these in order. Stop at the first one that fires and ask the user (see\n[Asking for guidance](#asking-for-guidance)).\n\n### 0. A separate subagent reviewed this diff and cleared it\n\nBefore pushing, dispatch the [`adversarial-reviewer`](..\u002F..\u002F.claude\u002Fagents\u002Fadversarial-reviewer.md) subagent against `git diff origin\u002F\u003Cdefault-branch>...HEAD` and wait for its verdict.\nDispatch it in the **foreground** --- a background dispatch returns an agent id rather than a report, and you are waiting on the answer anyway.\nBrief it with the base ref, the paths, and the standards that apply.\nNever brief it with your rationale for the change, per [`adversarial-self-review`](..\u002F..\u002Fshared\u002Fworkflow\u002Fadversarial-self-review.md).\n\nAddress, rebut, or defer every finding it returns, then re-dispatch it, so the clean verdict describes the tree you are about to push rather than an earlier one.\n\n`hooks\u002Fno-push-without-self-review.py` gates this on Claude Code.\nMorrison-Lab\u002Fai-config's Cursor adapter skips that script\nuntil [#2241](https:\u002F\u002Fgithub.com\u002FMorrison-Lab\u002Fai-config\u002Fissues\u002F2241)\n([`memories\u002Fcursor.md`](..\u002F..\u002Fmemories\u002Fcursor.md)).\nOn Cursor Cloud, when `Task` lists `adversarial-reviewer`,\ncall `parse_report()` from the worktree's\n[`hooks\u002Fno-push-without-self-review.py`](..\u002F..\u002Fhooks\u002Fno-push-without-self-review.py)\non the report recovered from the child's transcript\nwhen the worktree hook script exists\n(see [`memories\u002Fcursor.md`](..\u002F..\u002Fmemories\u002Fcursor.md)).\nDo not import `~\u002F.claude\u002Fhooks\u002F`:\nit is a different revision from the branch under review.\nWhen the three-dot diff includes\n`hooks\u002Fno-push-without-self-review.py`,\nalso parse with `origin\u002F\u003Cdefault-branch>`'s copy, or obtain a CLI review.\nIf the worktree script is missing, obtain a CLI review\n(see [`adversarial-self-review`](..\u002F..\u002Fshared\u002Fworkflow\u002Fadversarial-self-review.md)).\nIf the verdict is not `clean`, or there is no fingerprint,\nor the fingerprint does not prefix-match HEAD, do not push.\nIf there is no fingerprint\n(including a stale-registered persona),\nobtain a CLI review, write that reviewer's report to a file\nunder `\u002Ftmp`, and call `parse_report()` on that file.\nOn Claude Code the guard admits a verdict from that subagent's own call result, or from a Bash call matching its external-reviewer pattern, which today recognizes `agy --print` and no other delegation CLI.\nEither way it admits the verdict only when it is a verdict *line* rather than a sentence quoting one, and only when the report names the commit it read (`Reviewed-Commit: \u003Csha>`, after the verdict) and that commit is what the push would actually ship --- refspec resolved, so `push origin some-other-branch` is not covered by a verdict for `HEAD`.\nSo an inline pass under a reviewer framing, a verdict quoted out of a file, the guard's own denial message, and a verdict for an earlier commit all fail to satisfy it.\nReview after committing, therefore, not before.\n\nOverride by prefixing the push itself with `ALLOW_UNREVIEWED_PUSH=1` when no verdict can exist for the guard to check --- and say in your reply that you used it and why:\n\n- the initial empty PR branch under [`pr-on-claim`](..\u002F..\u002Fshared\u002Fworkflow\u002Fpr-on-claim.md), which carries nothing to review;\n- a review delivered by a CLI the guard does NOT recognize, whose verdict never becomes an `Agent` call's result\n  (an `agy --print` review is not this case: the guard admits it directly, so overriding for one is both unnecessary and a worse record than the real thing);\n- a session where the reviewer agent is unregistered ([ai-config#1921](https:\u002F\u002Fgithub.com\u002FMorrison-Lab\u002Fai-config\u002Fissues\u002F1921)) or registered from a stale definition, which is the case on any rollout of a change to the persona itself;\n- an emergency.\n\nOn a session whose pushes go through Morrison-Lab\u002Fai-config's\nCursor adapter, default: do not prefix.\nThe prefix is inert for the adapter\nuntil [#2241](https:\u002F\u002Fgithub.com\u002FMorrison-Lab\u002Fai-config\u002Fissues\u002F2241)\nmakes the adapter run that guard\n(the skip and the `parse_report` gate are earlier in this step).\nIf a native `PreToolUse` deny from\n`no-push-without-self-review` is observed on the push,\nprefix for that native guard.\nThe empty [`pr-on-claim`](..\u002F..\u002Fshared\u002Fworkflow\u002Fpr-on-claim.md)\n`--allow-empty` branch\nhas no report to parse: do not invent one,\ndo not refuse that push for lack of a verdict,\nand say in the reply that the carve-out was used.\nThe carve-out is `git rev-list --count origin\u002F\u003Cdefault-branch>..HEAD`\nequal to 1 and `git diff --quiet HEAD^ HEAD` exit 0\nin the checkout whose push follows.\nExit 1 means a diff; exit 128 means the command failed.\nBoth conditions passing is the `--allow-empty` pr-on-claim commit.\n`git diff origin\u002F\u003Cdefault-branch>...HEAD` empty is tree equality,\nnot \"this branch carries nothing\".\nA net-zero tree of other commits is not the carve-out.\nAfter [#2241](https:\u002F\u002Fgithub.com\u002FMorrison-Lab\u002Fai-config\u002Fissues\u002F2241)\nmakes the adapter run that guard,\nthe prefix is the documented escape\nwhen the guard cannot see a verdict.\nThe adapter skip makes the prefix inert for the adapter only.\nDo not pair the project adapter with native Claude hooks\n(desktop Cursor with third-party Claude hooks plus this project adapter;\nsee [`memories\u002Fcursor.md`](..\u002F..\u002Fmemories\u002Fcursor.md)).\n\nThe prefix has to be on the pushing command, not merely somewhere on the line: an override the guard accepted from anywhere was how a commit message quoting this very paragraph disarmed it.\n\n**Then run the CI validators locally, and read the denominator line.**\n\n```bash\npython3 scripts\u002Frun-local-validation.py --changed --base origin\u002F\u003Cdefault-branch>\n```\n\nIt derives its step list from `.github\u002Fworkflows\u002Fvalidate.yml` (so it cannot\ndrift from CI), runs each step in order after every edit, and reports\n`N passed, M failed, K skipped, J not runnable locally, of T step(s)`.\nA step listed as **not runnable locally** (a `uses:`-only job whose check is\nnot vendored here, or a step reading a `${{ github.* }}` expression) is a check\nCI will run that this pass did not; say so when you report the push as\npre-validated.\nA `uses:` job that has a local equivalent (listed in the script's own\ntable of them) runs here too; `new-line-breaks`, for one, runs the vendored\n`scripts\u002Fvendor\u002Fgha-check-new-line-breaks.py`.\nRunning only the `validate` job's `run:` steps by hand skips those\n([`derive-dont-enumerate`](..\u002F..\u002Fshared\u002Fworkflow\u002Fderive-dont-enumerate.md#which-local-checks-predict-ci-is-itself-a-derivable-set-not-a-remembered-one),\nthe ninth occurrence); `--only new-line-breaks` runs that one check alone.\n`--changed` narrows the hook test suites, the slowest `validate` step, to those whose inputs the branch touched,\nand runs every suite when the branch touches a shared input such as `scripts\u002Flib\u002F`.\nIt also skips a path-filtered job, such as `new-line-breaks`, when the branch touches none of its files.\n`--only REGEX` runs a slice while iterating.\nA hand-picked slice is not a pre-push run, so run the full `--changed` set before the push.\n`hooks\u002Fwarn-partial-validation-before-push.py` notes a push made with no run of `run-local-validation.py` this session (ai-config#3533).\nIt does not yet tell a slice from a full run (ai-config#5428).\nAny non-zero row is a red CI job waiting to happen: fix it before pushing,\nper [`fully-clean`](..\u002F..\u002Fshared\u002Fworkflow\u002Ffully-clean.md).\n(ai-config#1940, #1262.)\n\n- **Do:** read the run's closing summary count, and account for every failed step by name before pushing (each step's rc is in the run's table).\n- **Don't:** grep the output for one marker such as `FAIL:` and treat its absence as a pass.\n  Failures print in several shapes (`FAIL:`, `error:`, a nonzero rc), and a 6,000-line run hid a second failed step this way ([#4227](https:\u002F\u002Fgithub.com\u002FMorrison-Lab\u002Fai-config\u002Fissues\u002F4227)).\n\n### 1. Protected branch\n\n```bash\ngit rev-parse --abbrev-ref HEAD\n```\n\nIf the current branch is `main` (or `master` \u002F the repo's default branch),\n**do not push.** Pushing to the default branch is almost never intended — surface\nit and ask whether to branch first.\n\n### 2. Remote HEAD advanced past your last commit\n\nAnother session or the author may have pushed since your last fetch.\n\n**Take this reading immediately before the push**, not at the start of the round and not when you last synced.\nA fetch from earlier in the session is a measurement of a moment that has passed, and it stopped being evidence the instant somebody else pushed --- see [`check-before-pushing`](..\u002F..\u002Fshared\u002Fworkflow\u002Fcheck-before-pushing.md).\n\n```bash\nBRANCH=$(git rev-parse --abbrev-ref HEAD)\ngit ls-remote --heads origin \"$BRANCH\"   # read-only; updates no ref\n```\n\nIf the tip it reports is not an ancestor of the ref you are **pushing** (`git merge-base --is-ancestor \u003Ctip> \u003Csource>`), **back off** --- another session (or the author) is driving this branch right now.\n`\u003Csource>` is `HEAD` only when the refspec says so: `git push origin feature-x` from `main` pushes local `feature-x`, and comparing against `HEAD` there reads a divergence as a fast-forward.\nDo not push (a plain push will be rejected anyway, and you must not force-push over their work).\nAsk the user.\n\nFetch to see *what* they pushed, once you already know something is there:\n\n```bash\ngit fetch origin \"$BRANCH\" 2>\u002Fdev\u002Fnull   # FETCH\ngit log --oneline HEAD..\"origin\u002F$BRANCH\" 2>\u002Fdev\u002Fnull\n```\n\nAn object you cannot resolve locally is the **stronger** signal, not the milder one: the remote moved after your last fetch and you cannot see what is there.\n\n### 3. Claim comment by someone else\n\nLook at the open PR for this branch for a claim comment posted by **another**\nsession or person. (Your own most-recent \"I'm working on this\" comment is fine —\nthat's your claim.)\n\n```bash\nPR=$(gh pr view --json number,headRefName -q .number 2>\u002Fdev\u002Fnull)   # VIEW_PR\ngh pr view \"$PR\" --json comments \\\n  -q '.comments[] | select(.body | test(\"hold off|paws off|back off|unclaim|released|PR is free|now mergeable\"; \"i\")) | \"\\(.author.login): \\(.body)\"'   # READ_PR_COMMENTS\n```\n\nThe alternation is deliberate, and it covers RELEASES as well as claims.\nClaims posted before 2026-08-24 say \"paws off\", and a claim stays live on activity rather than on age, so an old-wording claim can be live right now.\nThe release terms matter because the old wording made them free: `paws off released` contains `paws off`, so one grep surfaced both sides of the exchange.\n`claim released` contains neither claim term, so a claim-only query returns the claim and not its release --- and this check asks whether the claim \"hasn't been unclaimed\", which a claim-only output cannot answer.\nA released PR would read as live-claimed, and this skill would refuse a legitimate push.\nDerive the release terms rather than copying this list, which is a snapshot of what the corpus posts today: `grep -rn \"unclaim\\|released\\|PR is free\\|now mergeable\" skills\u002F commands\u002F`.\nA matcher narrowed to the new phrase returns nothing on such a thread, which reads exactly like an unclaimed one --- see [`claim-pr`](..\u002F..\u002Fshared\u002Fworkflow\u002Fclaim-pr.md).\n\nThe query returns the whole claim\u002Frelease exchange, newest last, so read its **last** member: if that is a *claim* rather than a release, and it is from someone **other than you**, and it is still live --- the PR shows a push or comment within the last 2 hours, per [`claim-pr`](..\u002F..\u002Fshared\u002Fworkflow\u002Fclaim-pr.md)'s expiration rule --- **do not push.**\nAsk the user.\nAn expired claim (over 2 idle hours) no longer blocks on its own, but take it over with a fresh claim comment and run this skill's other checks (branch-head advance, `@claude` run in flight) before pushing.\n\n### 4. Hold \u002F block labels\n\n```bash\ngh pr view \"$PR\" --json labels -q '.labels[].name'   # VIEW_PR\n```\n\nIf any label signals a hold — case-insensitive matches on `do-not-merge`,\n`do not merge`, `WIP`, `hold`, `blocked`, `on hold`, `dont-merge` — **do not\npush.** Ask the user.\n\n### 5. `@claude` agent run in flight\n\nDon't push while the bot is mid-session on the branch — your push can collide\nwith its commits or trigger a redundant re-run.\n\n```bash\ngh run list --branch \"$BRANCH\" --json status,name \\\n  -q '.[] | select(.status==\"in_progress\" or .status==\"queued\") | .name'   # LIST_WORKFLOW_RUNS\n```\n\nIf a `@claude` \u002F review workflow is `in_progress` or `queued`, wait for it to\nfinish, then re-check. If it's stuck, ask the user.\n\n### 6. The push's own form\n\nNever `git push --force` \u002F `-f`.\nIt overwrites the remote tip unconditionally, including commits another agent pushed since you last looked.\n\n```bash\ngit push --force-with-lease --force-if-includes   # PUSH\n```\n\n`--force-if-includes` (added in Git 2.30.0) is the half usually left off, and without it the lease is defeatable: `--force-with-lease` compares against your *remote-tracking ref*, so any background fetch --- a poller, another tool in the same checkout, a `--recurse-submodules` fetch --- silently refreshes that ref and the lease then passes over the very commits it existed to protect.\n`--force-if-includes` closes that by checking the remote-tracking tip against the local branch's reflog.\nIt is an *ancillary* option, so it only does anything alongside a bare `--force-with-lease`.\n\nA `stale info` refusal is **not** a reason to force, and reaching for one there is the reflex `memories\u002Fgit-branches.md` exists to stop: the lease is unsatisfiable rather than violated, so `--force` is unnecessary.\nWhether there is anything to *race* is a separate question the refusal does not answer --- a deleted branch leaves nothing to race, a peer's push leaves everything.\n`git ls-remote --heads origin \u003Cbranch>` settles existence --- empty output means the next push *creates* the branch.\nQuery `gh pr list --state all --head \u003Cbranch>` first:\nMERGED means auto-delete, not a first publish, so do not recreate\n(see [`use-existing-pr-branch`](..\u002F..\u002Fshared\u002Fworkflow\u002Fuse-existing-pr-branch.md)\nand [`check-before-pushing`](..\u002F..\u002Fshared\u002Fworkflow\u002Fcheck-before-pushing.md)).\nOtherwise a plain push is the fix (or `git fetch --prune` and a retry).\n`ALLOW_FORCE_PUSH=1` is an escape valve for a case the guard did not foresee.\nSay what the lease refused and why forcing is right when you use it.\n\n`hooks\u002Fno-clobbering-push.py` enforces this half mechanically --- it refuses a bare force push and warns on a divergence --- so it fires whether or not this skill was invoked.\n\n## Asking for guidance\n\nWhen a check fires, **do not push.** Use `AskUserQuestion` to surface exactly\nwhat fired and let the user decide. Give concrete options, e.g.:\n\n- **Wait \u002F re-check** — back off and re-run the checks shortly (HEAD-advanced or\n  `@claude`-in-flight cases).\n- **Push anyway** — the user knows the signal is stale (e.g. their own old hold\n  label, a resolved claim).\n- **Branch first** — for the protected-branch case, create a feature branch and\n  push that instead.\n- **Skip the push** — leave the commits local for now.\n\nInclude the specifics in the question (which label, whose claim, how many\ncommits ahead) so the user can answer without digging.\n\n## Pushing (checks clean)\n\nOnce every check passes, push with the standard upstream + retry backoff:\n\n```bash\ngit push -u origin HEAD   # PUSH\n```\n\n`HEAD` rather than `\"$BRANCH\"`, deliberately: a shell variable reaches the pre-push guard unexpanded, so it cannot resolve which commits the push would ship and refuses.\n`-u origin HEAD` sets the upstream to the current branch just the same.\n\nIf the push fails on a **network** error, retry up to 4 times with exponential\nbackoff (2s, 4s, 8s, 16s). Do **not** retry — and do **not** force-push — if it\nfails because the remote rejected a non-fast-forward (that's check #2 surfacing\nlate: fetch, reconcile, re-run the checks).\n\nAfter a successful push, if the branch has no PR yet, open one (ready for\nreview, not a draft).\nImmediately maintain an active monitoring loop or scheduled wake mechanism.\nActively query current-head CI\u002Fpipeline status (`gh pr checks` \u002F `glab ci list` or `glab mr view`) and review verdicts (`gh pr view` \u002F `glab mr view`) until that round reaches a terminal state, re-arming the poll while work remains.\n\n## Relationship to other skills\n\n- **`claim-pr`** — posts\u002Fremoves the claim comment this skill reads in check #3.\n  `push` is the read side; `claim-pr` is the write side.\n- **`session-lock`** — the local-checkout counterpart: it keeps parallel sessions on one machine from sharing a working tree.\n  `push` guards the *remote* branch; `session-lock` guards the *local* tree.\n- **`sync-pr-branch` \u002F `merge-main`** — when check #2 fires because `main` (not the branch) moved ahead, sync the branch first, then push.\n  `sync-pr-branch` ends in a push and should itself honor these checks.\n- **`ardi`** — its push step should run these checks; the \"detect an active\n  parallel session before pushing\" note in `claim-pr` is the same guard.\n- **[`check-before-pushing`](..\u002F..\u002Fshared\u002Fworkflow\u002Fcheck-before-pushing.md)** --- the standing rule these checks implement, and the home of the immediacy argument and the `--force-if-includes` mechanism.\n  `hooks\u002Fno-clobbering-push.py` is its instrument, and it runs on the `git push` itself rather than waiting to be invoked --- so it covers the bare push in the middle of an ARDI round that never reaches this skill.\n\n## Proactive hook compliance\n\n- **`no-push-without-self-review.py`**: Blocks `git push` unless a clean verdict for the exact commit\n  being pushed (`Reviewed-Commit: \u003CHEAD_SHA>`) came from an adversarial self-review subagent, or from\n  a review by a CLI the guard recognizes, which today means `agy --print` and no other.\n  Pushes to `Morrison-Lab\u002Fmln`, `mlg` and `mlr` are exempt (listed in the hook's exempt-repository constant).\n  Dispatch the reviewer in the foreground and resolve all findings before pushing.\n  Use `ALLOW_UNREVIEWED_PUSH=1` only for the empty `pr-on-claim` commit or documented exceptions.\n- **`no-clobbering-push.py`**: Denies bare `git push -f`\u002F`--force` and warns if remote tip has diverged.\n  Always run `git ls-remote --heads origin \u003Cbranch>` immediately before pushing,\n  and use `--force-with-lease --force-if-includes`.\n- **`no-unshipped-commit.py`**: Stop guard that blocks turn completion when unpushed commits remain on the branch.\n\n## Anti-patterns\n\n- ❌ Force-pushing over commits another session added (check #2)\n- ❌ Bare `git push --force` instead of `--force-with-lease --force-if-includes` (check #6)\n- ❌ Reusing an earlier fetch as the check --- the reading has to be taken immediately before the push (check #2)\n- ❌ Pushing past a fresh claim comment from someone else (check #3)\n- ❌ Pushing onto a `do-not-merge` \u002F `hold` PR without asking (check #4)\n- ❌ Pushing while a `@claude` run is mid-session on the branch (check #5)\n- ❌ Pushing directly to `main` \u002F the default branch (check #1)\n- ❌ Pushing and abandoning active monitoring without polling CI and review to completion\n- ❌ Reporting \"pushed\" when a check stopped you — say what fired and that you're\n  waiting on the user\n","# push\n\nA guard you run **right before `git push`**. Other sessions, reviewers, and the\n`@claude` CI bot can all be working the same branch. Pushing blind risks\nclobbering their work or shoving commits onto a branch someone explicitly held.\nThis skill runs a short pre-push check, and if anything looks off it **stops and\nasks you** rather than pushing.\n\n## When this fires\n\n- The user says \"push\", \"push this\", \"push my changes\", or `\u002Fpush`.\n- Any time you're about to push commits to a **shared** branch (one with an open\n  PR, or one another session may be driving).\n\nIt does **not** need to fire for a brand-new branch that has never been pushed\nand has no PR — there's nothing to collide with. Still run the protected-branch\ncheck in that case.\n\n## Pre-push checks\n\nRun these in order. Stop at the first one that fires and ask the user (see\n[Asking for guidance](#asking-for-guidance)).\n\n### 0. A separate subagent reviewed this diff and cleared it\n\nBefore pushing, dispatch the [`adversarial-reviewer`](..\u002F..\u002F.claude\u002Fagents\u002Fadversarial-reviewer.md) subagent against `git diff origin\u002F\u003Cdefault-branch>...HEAD` and wait for its verdict.\nDispatch it in the **foreground** --- a background dispatch returns an agent id rather than a report, and you are waiting on the answer anyway.\nBrief it with the base ref, the paths, and the standards that apply.\nNever brief it with your rationale for the change, per [`adversarial-self-review`](..\u002F..\u002Fshared\u002Fworkflow\u002Fadversarial-self-review.md).\n\nAddress, rebut, or defer every finding it returns, then re-dispatch it, so the clean verdict describes the tree you are about to push rather than an earlier one.\n\n`hooks\u002Fno-push-without-self-review.py` gates this on Claude Code.\nMorrison-Lab\u002Fai-config's Cursor adapter skips that script\nuntil [#2241](https:\u002F\u002Fgithub.com\u002FMorrison-Lab\u002Fai-config\u002Fissues\u002F2241)\n([`memories\u002Fcursor.md`](..\u002F..\u002Fmemories\u002Fcursor.md)).\nOn Cursor Cloud, when `Task` lists `adversarial-reviewer`,\ncall `parse_report()` from the worktree's\n[`hooks\u002Fno-push-without-self-review.py`](..\u002F..\u002Fhooks\u002Fno-push-without-self-review.py)\non the report recovered from the child's transcript\nwhen the worktree hook script exists\n(see [`memories\u002Fcursor.md`](..\u002F..\u002Fmemories\u002Fcursor.md)).\nDo not import `~\u002F.claude\u002Fhooks\u002F`:\nit is a different revision from the branch under review.\nWhen the three-dot diff includes\n`hooks\u002Fno-push-without-self-review.py`,\nalso parse with `origin\u002F\u003Cdefault-branch>`'s copy, or obtain a CLI review.\nIf the worktree script is missing, obtain a CLI review\n(see [`adversarial-self-review`](..\u002F..\u002Fshared\u002Fworkflow\u002Fadversarial-self-review.md)).\nIf the verdict is not `clean`, or there is no fingerprint,\nor the fingerprint does not prefix-match HEAD, do not push.\nIf there is no fingerprint\n(including a stale-registered persona),\nobtain a CLI review, write that reviewer's report to a file\nunder `\u002Ftmp`, and call `parse_report()` on that file.\nOn Claude Code the guard admits a verdict from that subagent's own call result, or from a Bash call matching its external-reviewer pattern, which today recognizes `agy --print` and no other delegation CLI.\nEither way it admits the verdict only when it is a verdict *line* rather than a sentence quoting one, and only when the report names the commit it read (`Reviewed-Commit: \u003Csha>`, after the verdict) and that commit is what the push would actually ship --- refspec resolved, so `push origin some-other-branch` is not covered by a verdict for `HEAD`.\nSo an inline pass under a reviewer framing, a verdict quoted out of a file, the guard's own denial message, and a verdict for an earlier commit all fail to satisfy it.\nReview after committing, therefore, not before.\n\nOverride by prefixing the push itself with `ALLOW_UNREVIEWED_PUSH=1` when no verdict can exist for the guard to check --- and say in your reply that you used it and why:\n\n- the initial empty PR branch under [`pr-on-claim`](..\u002F..\u002Fshared\u002Fworkflow\u002Fpr-on-claim.md), which carries nothing to review;\n- a review delivered by a CLI the guard does NOT recognize, whose verdict never becomes an `Agent` call's result\n  (an `agy --print` review is not this case: the guard admits it directly, so overriding for one is both unnecessary and a worse record than the real thing);\n- a session where the reviewer agent is unregistered ([ai-config#1921](https:\u002F\u002Fgithub.com\u002FMorrison-Lab\u002Fai-config\u002Fissues\u002F1921)) or registered from a stale definition, which is the case on any rollout of a change to the persona itself;\n- an emergency.\n\nOn a session whose pushes go through Morrison-Lab\u002Fai-config's\nCursor adapter, default: do not prefix.\nThe prefix is inert for the adapter\nuntil [#2241](https:\u002F\u002Fgithub.com\u002FMorrison-Lab\u002Fai-config\u002Fissues\u002F2241)\nmakes the adapter run that guard\n(the skip and the `parse_report` gate are earlier in this step).\nIf a native `PreToolUse` deny from\n`no-push-without-self-review` is observed on the push,\nprefix for that native guard.\nThe empty [`pr-on-claim`](..\u002F..\u002Fshared\u002Fworkflow\u002Fpr-on-claim.md)\n`--allow-empty` branch\nhas no report to parse: do not invent one,\ndo not refuse that push for lack of a verdict,\nand say in the reply that the carve-out was used.\nThe carve-out is `git rev-list --count origin\u002F\u003Cdefault-branch>..HEAD`\nequal to 1 and `git diff --quiet HEAD^ HEAD` exit 0\nin the checkout whose push follows.\nExit 1 means a diff; exit 128 means the command failed.\nBoth conditions passing is the `--allow-empty` pr-on-claim commit.\n`git diff origin\u002F\u003Cdefault-branch>...HEAD` empty is tree equality,\nnot \"this branch carries nothing\".\nA net-zero tree of other commits is not the carve-out.\nAfter [#2241](https:\u002F\u002Fgithub.com\u002FMorrison-Lab\u002Fai-config\u002Fissues\u002F2241)\nmakes the adapter run that guard,\nthe prefix is the documented escape\nwhen the guard cannot see a verdict.\nThe adapter skip makes the prefix inert for the adapter only.\nDo not pair the project adapter with native Claude hooks\n(desktop Cursor with third-party Claude hooks plus this project adapter;\nsee [`memories\u002Fcursor.md`](..\u002F..\u002Fmemories\u002Fcursor.md)).\n\nThe prefix has to be on the pushing command, not merely somewhere on the line: an override the guard accepted from anywhere was how a commit message quoting this very paragraph disarmed it.\n\n**Then run the CI validators locally, and read the denominator line.**\n\n```bash\npython3 scripts\u002Frun-local-validation.py --changed --base origin\u002F\u003Cdefault-branch>\n```\n\nIt derives its step list from `.github\u002Fworkflows\u002Fvalidate.yml` (so it cannot\ndrift from CI), runs each step in order after every edit, and reports\n`N passed, M failed, K skipped, J not runnable locally, of T step(s)`.\nA step listed as **not runnable locally** (a `uses:`-only job whose check is\nnot vendored here, or a step reading a `${{ github.* }}` expression) is a check\nCI will run that this pass did not; say so when you report the push as\npre-validated.\nA `uses:` job that has a local equivalent (listed in the script's own\ntable of them) runs here too; `new-line-breaks`, for one, runs the vendored\n`scripts\u002Fvendor\u002Fgha-check-new-line-breaks.py`.\nRunning only the `validate` job's `run:` steps by hand skips those\n([`derive-dont-enumerate`](..\u002F..\u002Fshared\u002Fworkflow\u002Fderive-dont-enumerate.md#which-local-checks-predict-ci-is-itself-a-derivable-set-not-a-remembered-one),\nthe ninth occurrence); `--only new-line-breaks` runs that one check alone.\n`--changed` narrows the hook test suites, the slowest `validate` step, to those whose inputs the branch touched,\nand runs every suite when the branch touches a shared input such as `scripts\u002Flib\u002F`.\nIt also skips a path-filtered job, such as `new-line-breaks`, when the branch touches none of its files.\n`--only REGEX` runs a slice while iterating.\nA hand-picked slice is not a pre-push run, so run the full `--changed` set before the push.\n`hooks\u002Fwarn-partial-validation-before-push.py` notes a push made with no run of `run-local-validation.py` this session (ai-config#3533).\nIt does not yet tell a slice from a full run (ai-config#5428).\nAny non-zero row is a red CI job waiting to happen: fix it before pushing,\nper [`fully-clean`](..\u002F..\u002Fshared\u002Fworkflow\u002Ffully-clean.md).\n(ai-config#1940, #1262.)\n\n- **Do:** read the run's closing summary count, and account for every failed step by name before pushing (each step's rc is in the run's table).\n- **Don't:** grep the output for one marker such as `FAIL:` and treat its absence as a pass.\n  Failures print in several shapes (`FAIL:`, `error:`, a nonzero rc), and a 6,000-line run hid a second failed step this way ([#4227](https:\u002F\u002Fgithub.com\u002FMorrison-Lab\u002Fai-config\u002Fissues\u002F4227)).\n\n### 1. Protected branch\n\n```bash\ngit rev-parse --abbrev-ref HEAD\n```\n\nIf the current branch is `main` (or `master` \u002F the repo's default branch),\n**do not push.** Pushing to the default branch is almost never intended — surface\nit and ask whether to branch first.\n\n### 2. Remote HEAD advanced past your last commit\n\nAnother session or the author may have pushed since your last fetch.\n\n**Take this reading immediately before the push**, not at the start of the round and not when you last synced.\nA fetch from earlier in the session is a measurement of a moment that has passed, and it stopped being evidence the instant somebody else pushed --- see [`check-before-pushing`](..\u002F..\u002Fshared\u002Fworkflow\u002Fcheck-before-pushing.md).\n\n```bash\nBRANCH=$(git rev-parse --abbrev-ref HEAD)\ngit ls-remote --heads origin \"$BRANCH\"   # read-only; updates no ref\n```\n\nIf the tip it reports is not an ancestor of the ref you are **pushing** (`git merge-base --is-ancestor \u003Ctip> \u003Csource>`), **back off** --- another session (or the author) is driving this branch right now.\n`\u003Csource>` is `HEAD` only when the refspec says so: `git push origin feature-x` from `main` pushes local `feature-x`, and comparing against `HEAD` there reads a divergence as a fast-forward.\nDo not push (a plain push will be rejected anyway, and you must not force-push over their work).\nAsk the user.\n\nFetch to see *what* they pushed, once you already know something is there:\n\n```bash\ngit fetch origin \"$BRANCH\" 2>\u002Fdev\u002Fnull   # FETCH\ngit log --oneline HEAD..\"origin\u002F$BRANCH\" 2>\u002Fdev\u002Fnull\n```\n\nAn object you cannot resolve locally is the **stronger** signal, not the milder one: the remote moved after your last fetch and you cannot see what is there.\n\n### 3. Claim comment by someone else\n\nLook at the open PR for this branch for a claim comment posted by **another**\nsession or person. (Your own most-recent \"I'm working on this\" comment is fine —\nthat's your claim.)\n\n```bash\nPR=$(gh pr view --json number,headRefName -q .number 2>\u002Fdev\u002Fnull)   # VIEW_PR\ngh pr view \"$PR\" --json comments \\\n  -q '.comments[] | select(.body | test(\"hold off|paws off|back off|unclaim|released|PR is free|now mergeable\"; \"i\")) | \"\\(.author.login): \\(.body)\"'   # READ_PR_COMMENTS\n```\n\nThe alternation is deliberate, and it covers RELEASES as well as claims.\nClaims posted before 2026-08-24 say \"paws off\", and a claim stays live on activity rather than on age, so an old-wording claim can be live right now.\nThe release terms matter because the old wording made them free: `paws off released` contains `paws off`, so one grep surfaced both sides of the exchange.\n`claim released` contains neither claim term, so a claim-only query returns the claim and not its release --- and this check asks whether the claim \"hasn't been unclaimed\", which a claim-only output cannot answer.\nA released PR would read as live-claimed, and this skill would refuse a legitimate push.\nDerive the release terms rather than copying this list, which is a snapshot of what the corpus posts today: `grep -rn \"unclaim\\|released\\|PR is free\\|now mergeable\" skills\u002F commands\u002F`.\nA matcher narrowed to the new phrase returns nothing on such a thread, which reads exactly like an unclaimed one --- see [`claim-pr`](..\u002F..\u002Fshared\u002Fworkflow\u002Fclaim-pr.md).\n\nThe query returns the whole claim\u002Frelease exchange, newest last, so read its **last** member: if that is a *claim* rather than a release, and it is from someone **other than you**, and it is still live --- the PR shows a push or comment within the last 2 hours, per [`claim-pr`](..\u002F..\u002Fshared\u002Fworkflow\u002Fclaim-pr.md)'s expiration rule --- **do not push.**\nAsk the user.\nAn expired claim (over 2 idle hours) no longer blocks on its own, but take it over with a fresh claim comment and run this skill's other checks (branch-head advance, `@claude` run in flight) before pushing.\n\n### 4. Hold \u002F block labels\n\n```bash\ngh pr view \"$PR\" --json labels -q '.labels[].name'   # VIEW_PR\n```\n\nIf any label signals a hold — case-insensitive matches on `do-not-merge`,\n`do not merge`, `WIP`, `hold`, `blocked`, `on hold`, `dont-merge` — **do not\npush.** Ask the user.\n\n### 5. `@claude` agent run in flight\n\nDon't push while the bot is mid-session on the branch — your push can collide\nwith its commits or trigger a redundant re-run.\n\n```bash\ngh run list --branch \"$BRANCH\" --json status,name \\\n  -q '.[] | select(.status==\"in_progress\" or .status==\"queued\") | .name'   # LIST_WORKFLOW_RUNS\n```\n\nIf a `@claude` \u002F review workflow is `in_progress` or `queued`, wait for it to\nfinish, then re-check. If it's stuck, ask the user.\n\n### 6. The push's own form\n\nNever `git push --force` \u002F `-f`.\nIt overwrites the remote tip unconditionally, including commits another agent pushed since you last looked.\n\n```bash\ngit push --force-with-lease --force-if-includes   # PUSH\n```\n\n`--force-if-includes` (added in Git 2.30.0) is the half usually left off, and without it the lease is defeatable: `--force-with-lease` compares against your *remote-tracking ref*, so any background fetch --- a poller, another tool in the same checkout, a `--recurse-submodules` fetch --- silently refreshes that ref and the lease then passes over the very commits it existed to protect.\n`--force-if-includes` closes that by checking the remote-tracking tip against the local branch's reflog.\nIt is an *ancillary* option, so it only does anything alongside a bare `--force-with-lease`.\n\nA `stale info` refusal is **not** a reason to force, and reaching for one there is the reflex `memories\u002Fgit-branches.md` exists to stop: the lease is unsatisfiable rather than violated, so `--force` is unnecessary.\nWhether there is anything to *race* is a separate question the refusal does not answer --- a deleted branch leaves nothing to race, a peer's push leaves everything.\n`git ls-remote --heads origin \u003Cbranch>` settles existence --- empty output means the next push *creates* the branch.\nQuery `gh pr list --state all --head \u003Cbranch>` first:\nMERGED means auto-delete, not a first publish, so do not recreate\n(see [`use-existing-pr-branch`](..\u002F..\u002Fshared\u002Fworkflow\u002Fuse-existing-pr-branch.md)\nand [`check-before-pushing`](..\u002F..\u002Fshared\u002Fworkflow\u002Fcheck-before-pushing.md)).\nOtherwise a plain push is the fix (or `git fetch --prune` and a retry).\n`ALLOW_FORCE_PUSH=1` is an escape valve for a case the guard did not foresee.\nSay what the lease refused and why forcing is right when you use it.\n\n`hooks\u002Fno-clobbering-push.py` enforces this half mechanically --- it refuses a bare force push and warns on a divergence --- so it fires whether or not this skill was invoked.\n\n## Asking for guidance\n\nWhen a check fires, **do not push.** Use `AskUserQuestion` to surface exactly\nwhat fired and let the user decide. Give concrete options, e.g.:\n\n- **Wait \u002F re-check** — back off and re-run the checks shortly (HEAD-advanced or\n  `@claude`-in-flight cases).\n- **Push anyway** — the user knows the signal is stale (e.g. their own old hold\n  label, a resolved claim).\n- **Branch first** — for the protected-branch case, create a feature branch and\n  push that instead.\n- **Skip the push** — leave the commits local for now.\n\nInclude the specifics in the question (which label, whose claim, how many\ncommits ahead) so the user can answer without digging.\n\n## Pushing (checks clean)\n\nOnce every check passes, push with the standard upstream + retry backoff:\n\n```bash\ngit push -u origin HEAD   # PUSH\n```\n\n`HEAD` rather than `\"$BRANCH\"`, deliberately: a shell variable reaches the pre-push guard unexpanded, so it cannot resolve which commits the push would ship and refuses.\n`-u origin HEAD` sets the upstream to the current branch just the same.\n\nIf the push fails on a **network** error, retry up to 4 times with exponential\nbackoff (2s, 4s, 8s, 16s). Do **not** retry — and do **not** force-push — if it\nfails because the remote rejected a non-fast-forward (that's check #2 surfacing\nlate: fetch, reconcile, re-run the checks).\n\nAfter a successful push, if the branch has no PR yet, open one (ready for\nreview, not a draft).\nImmediately maintain an active monitoring loop or scheduled wake mechanism.\nActively query current-head CI\u002Fpipeline status (`gh pr checks` \u002F `glab ci list` or `glab mr view`) and review verdicts (`gh pr view` \u002F `glab mr view`) until that round reaches a terminal state, re-arming the poll while work remains.\n\n## Relationship to other skills\n\n- **`claim-pr`** — posts\u002Fremoves the claim comment this skill reads in check #3.\n  `push` is the read side; `claim-pr` is the write side.\n- **`session-lock`** — the local-checkout counterpart: it keeps parallel sessions on one machine from sharing a working tree.\n  `push` guards the *remote* branch; `session-lock` guards the *local* tree.\n- **`sync-pr-branch` \u002F `merge-main`** — when check #2 fires because `main` (not the branch) moved ahead, sync the branch first, then push.\n  `sync-pr-branch` ends in a push and should itself honor these checks.\n- **`ardi`** — its push step should run these checks; the \"detect an active\n  parallel session before pushing\" note in `claim-pr` is the same guard.\n- **[`check-before-pushing`](..\u002F..\u002Fshared\u002Fworkflow\u002Fcheck-before-pushing.md)** --- the standing rule these checks implement, and the home of the immediacy argument and the `--force-if-includes` mechanism.\n  `hooks\u002Fno-clobbering-push.py` is its instrument, and it runs on the `git push` itself rather than waiting to be invoked --- so it covers the bare push in the middle of an ARDI round that never reaches this skill.\n\n## Proactive hook compliance\n\n- **`no-push-without-self-review.py`**: Blocks `git push` unless a clean verdict for the exact commit\n  being pushed (`Reviewed-Commit: \u003CHEAD_SHA>`) came from an adversarial self-review subagent, or from\n  a review by a CLI the guard recognizes, which today means `agy --print` and no other.\n  Pushes to `Morrison-Lab\u002Fmln`, `mlg` and `mlr` are exempt (listed in the hook's exempt-repository constant).\n  Dispatch the reviewer in the foreground and resolve all findings before pushing.\n  Use `ALLOW_UNREVIEWED_PUSH=1` only for the empty `pr-on-claim` commit or documented exceptions.\n- **`no-clobbering-push.py`**: Denies bare `git push -f`\u002F`--force` and warns if remote tip has diverged.\n  Always run `git ls-remote --heads origin \u003Cbranch>` immediately before pushing,\n  and use `--force-with-lease --force-if-includes`.\n- **`no-unshipped-commit.py`**: Stop guard that blocks turn completion when unpushed commits remain on the branch.\n\n## Anti-patterns\n\n- ❌ Force-pushing over commits another session added (check #2)\n- ❌ Bare `git push --force` instead of `--force-with-lease --force-if-includes` (check #6)\n- ❌ Reusing an earlier fetch as the check --- the reading has to be taken immediately before the push (check #2)\n- ❌ Pushing past a fresh claim comment from someone else (check #3)\n- ❌ Pushing onto a `do-not-merge` \u002F `hold` PR without asking (check #4)\n- ❌ Pushing while a `@claude` run is mid-session on the branch (check #5)\n- ❌ Pushing directly to `main` \u002F the default branch (check #1)\n- ❌ Pushing and abandoning active monitoring without polling CI and review to completion\n- ❌ Reporting \"pushed\" when a check stopped you — say what fired and that you're\n  waiting on the user\n",{"name":5,"description":7,"allowed-tools":24,"user-invocable":27},[25,26],"Bash","AskUserQuestion",true,"main","aed9be7a6beca85e267d19d9d354f50da6c37417","skills\u002Fpush",1,"https:\u002F\u002Fgithub.com\u002FMorrison-Lab\u002Fai-config","https:\u002F\u002Fmorrison-lab.github.io\u002Fai-config\u002F","https:\u002F\u002Fgithub.com\u002FMorrison-Lab","2026-11-13 04:48:19",{"source":8,"skillName":5,"commands":37,"prompt":47,"zipAvailable":27},[38,41,44],{"tool":39,"command":40},"npx","npx skills add Morrison-Lab\u002Fai-config --skill push",{"tool":42,"command":43},"bunx","bunx skills add Morrison-Lab\u002Fai-config --skill push",{"tool":45,"command":46},"pnpm","pnpm dlx skills add Morrison-Lab\u002Fai-config --skill push","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002FMorrison-Lab\u002Fai-config\u002Fblob\u002Fmain\u002Fskills\u002Fpush\u002FSKILL.md",[],[50,57,64,71,78,85],{"skillNo":51,"name":52,"nameZh":6,"description":53,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":54,"categories":55,"keywords":56,"publishedAt":19},"01M73593BZ33TKTNBDX8JNKYN8","resolve-conflicts","Resolve merge conflicts.","https:\u002F\u002Fgithub.com\u002FMorrison-Lab\u002Fai-config\u002Fblob\u002Fmain\u002Fskills\u002Fresolve-conflicts\u002FSKILL.md",[],[],{"skillNo":58,"name":59,"nameZh":6,"description":60,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":61,"categories":62,"keywords":63,"publishedAt":19},"01M73584TVDWNA1HMAQ8XXGSZV","rec","Provide recommendations for choices.","https:\u002F\u002Fgithub.com\u002FMorrison-Lab\u002Fai-config\u002Fblob\u002Fmain\u002Fskills\u002Frec\u002FSKILL.md",[],[],{"skillNo":65,"name":66,"nameZh":6,"description":67,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":68,"categories":69,"keywords":70,"publishedAt":19},"01M7354D93G0P37BRHR2T3HD82","handoff","Snapshot state to forge\u002Fmemory.","https:\u002F\u002Fgithub.com\u002FMorrison-Lab\u002Fai-config\u002Fblob\u002Fmain\u002Fskills\u002Fhandoff\u002FSKILL.md",[],[],{"skillNo":72,"name":73,"nameZh":6,"description":74,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":75,"categories":76,"keywords":77,"publishedAt":19},"01M7352Y31YM4STJ5MDR48ZQBW","empty-promise","Investigate and correct empty promises.","https:\u002F\u002Fgithub.com\u002FMorrison-Lab\u002Fai-config\u002Fblob\u002Fmain\u002Fskills\u002Fempty-promise\u002FSKILL.md",[],[],{"skillNo":79,"name":80,"nameZh":6,"description":81,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":82,"categories":83,"keywords":84,"publishedAt":19},"01M7352P8K341JQ8M8X264CMRV","draft-in-ezras-voice","Draft communications for Ezra in his own voice: announcements, emails, and comments sent under his name.","https:\u002F\u002Fgithub.com\u002FMorrison-Lab\u002Fai-config\u002Fblob\u002Fmain\u002Fskills\u002Fdraft-in-ezras-voice\u002FSKILL.md",[],[],{"skillNo":86,"name":87,"nameZh":6,"description":88,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":89,"categories":90,"keywords":91,"publishedAt":19},"01M7352K24A80Q7V05YC2K16SD","do-it-for-me","Do the step yourself; retry blocks, then lift auto mode.","https:\u002F\u002Fgithub.com\u002FMorrison-Lab\u002Fai-config\u002Fblob\u002Fmain\u002Fskills\u002Fdo-it-for-me\u002FSKILL.md",[],[]]