Skip to content

fix(devx): teach the os-regen pre-commit hook the deferred merge - #8205

Merged
hotlong merged 2 commits into
mainfrom
claude/issue-8047-os-regen-hook-deferred-merge
Aug 12, 2026
Merged

fix(devx): teach the os-regen pre-commit hook the deferred merge#8205
hotlong merged 2 commits into
mainfrom
claude/issue-8047-os-regen-hook-deferred-merge

Conversation

@hotlong

Copy link
Copy Markdown
Contributor

Fixes#8047. Implements the maintainer ruling of 2026-08-12 (option 1): the os-regen pre-commit hook learns the deferred case.

The contradiction this removes

Two in-repo authorities disagreed on main. scripts/pm/os-regen-merge.sh — the sanctioned landing sequence for a branch touching merge=os-regen paths — commits the merge before regenerating, deliberately: the driver exits 0 while silently dropping one side, so only a separate regeneration commit on a known-good base lets a reviewer read "what main brought" apart from "what the change produces". The pre-commit hook refused exactly that commit. Measured on PR #7851, the way out people learned was to skip the entire pre-commit hook — one false positive traded for a blanket bypass.

Both halves

Accept. A merge commit whose artifacts are stale is now deferred, not passed: pre-commit appends deferred-at <head> <merge-head> to the existing marker and lets the commit through, printing what it recorded.

Collect. A change that only stopped refusing would convert a false positive into no coverage. Two properties prevent that:

  • One commit deep, by construction. A deferral is entered only while MERGE_HEAD exists; a second merge attempted while one is outstanding is refused; every non-merge commit after it is refused by the ordinary staleness check. Nothing can land between the merge and its discharge, so "the immediately following commit" is enforced by there being no other commit it could be.
  • Collected where collection becomes possible. At the instant the merge commit is created, the commit that discharges it does not exist — pre-commit can only record. The two events that can follow are the next commit (this same check) and the push, so .githooks/pre-push is new: registered by the existing core.hooksPath=.githooks with no registrar change, and 77 ms plus a marker stat on every push that did not defer. An undischarged deferral cannot leave the machine.

Prose reconciled

All three authorities that disagreed now say the same thing: scripts/pm/os-regen-merge.sh's header (the authority — its four-step is unchanged), AGENTS.md §11, and what the hook prints. The refusal points at os-regen-merge.sh instead of advertising a bypass.

Measurements behind the design

  • The marker machinery is real, but "it already has the concept" was half right.$GIT_DIR/os-regen-pending exists and self-clears; there was no deferral path — the clearing message refers to artifacts the driver deferred, not to a deferred merge commit. The deferral record is new state, deliberately inside the existing marker so there is one state machine, not a marker plus a flag.
  • git merge does not run pre-commit for a merge it completes itself (verified, git 2.43). A clean auto-committed merge lands with the marker untouched and the refusal falls on the next commit — the same collection point. Only a merge finished with git commit reaches the new accept path, which is the PR feat(spec,lint): gate managed apiMethods ⊆ affordances where the author is (#7521) #7851 shape.
  • The governed set cannot drift. The hook reads scripts/regen-artifacts.mjs, and check:merge-driver reconciles that table against .gitattributes in both directions already. Nothing to fix.
  • Mixed-version clones fail safe. An older checkout reading a marker written by this one reports the deferred-at line as an unknown pending path and refuses — no silent pass.

Out of scope, unchanged as directed: .gitattributes's merge-driver registration, and scripts/git-merge-regen.mjs's behaviour.

Tests

node scripts/check-regen-pending.mjs --self-test (the second half of pnpm check:merge-driver) grows three sections: the pre-push hook's index mode, a decision table over all five states including the two a fixture cannot reach, and a fixture that builds throwaway git repos and replays the two-commit sequence. The behaviour is a property of a pair of commits, which no single-tree assertion can express. Gates are redirected at the fixture's own package.json via OS_REGEN_GATE_CWD, so the replay costs no spec build and a mistake fails safe — a directory without those scripts reads as stale.

Reverse verification, direction predicted before running: on one fixture state (MERGE_HEAD present, marker set, artifacts stale) origin/main's hook exits 1 — the false positive, and its trailer is the --no-verify advice — while this one exits 0 with the deferral recorded. One commit later, still stale, both exit 1.


Generated by Claude Code

Two in-repo authorities contradicted each other on main. `scripts/pm/os-regen-merge.sh`
— the sanctioned landing sequence for a branch touching `merge=os-regen` paths —
commits the merge BEFORE regenerating, deliberately: the driver exits 0 while
silently dropping one side, so only a separate regeneration commit on a known-good
base lets a reviewer read "what main brought" apart from "what the change produces".
The `os-regen` pre-commit hook refused exactly that commit. The way out people
learned (measured on PR #7851) was to skip the entire pre-commit hook, which trades
one false positive for a blanket bypass. Maintainer ruling 2026-08-12: the hook moves.
The merge commit is now DEFERRED, not passed: `pre-commit` records
`deferred-at <head> <merge-head>` in the existing marker and lets the commit through.
Two properties keep that a split rather than an escape hatch.
One commit deep, by construction. A deferral is entered only while MERGE_HEAD exists,
a second merge attempted while one is outstanding is refused, and every non-merge
commit after it is refused by the ordinary staleness check — so nothing can land
between the merge and its discharge. "The immediately following commit" is enforced by
there being no other commit it could be.
Collected where collection becomes possible. At the moment the merge commit is
created, the commit that discharges it does not exist, so pre-commit can only record.
The two events that can follow are the next commit (this same check) and the push, so
`.githooks/pre-push` — new, registered by the existing `core.hooksPath=.githooks`,
77ms and a marker stat on every push that did not defer — refuses a push that still
owes one. An undischarged deferral cannot leave the machine.
Prose reconciled across all three authorities that disagreed: the script header (the
authority), AGENTS.md §11, and what the hook prints. The hook's refusal now names
`scripts/pm/os-regen-merge.sh` instead of advertising a bypass.
Tests: `--self-test` grows a decision table over all five states plus a fixture that
builds throwaway git repos and replays the two-commit sequence — behaviour about a
PAIR of commits, which no single-tree assertion can express. Gates are redirected at
the fixture's own package.json so the replay costs no spec build; a mistake there
fails safe (a directory without those scripts reads as stale). The pre-push hook's
index mode is asserted too — git ignores a non-executable hook in silence.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q983j8EGKjUs8r14n76TeH
@vercel

vercelBot commented Aug 12, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectstackIgnoredIgnoredAug 12, 2026 8:13pm

Request Review

@hotlonghotlong added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 12, 2026 — with Claude
@github-actionsgithub-actionsBot added the documentation Improvements or additions to documentation label Aug 12, 2026
`check:pm-skill-id-lint` refuses issue numbers in AGENTS.md operative text:
a lesson has to be self-contained (failure mode + discipline + boundary) and
a ruling keeps its date, so the number carries no information a reader needs.
The bullet already states the mechanism in full; the citation becomes the
ruling's date. Gate green locally, self-test included.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q983j8EGKjUs8r14n76TeH
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[finding] The os-regen pre-commit hook and the documented four-step landing procedure give opposite instructions — one of them has to change

2 participants

@hotlong@claude