Skip to content

Commit b5e09b2

Browse files
hotlongclaude
andauthored
fix(devx): teach the os-regen pre-commit hook the deferred merge (#8205)
* fix(devx): teach the os-regen pre-commit hook the deferred merge (#8047) 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 * fix(devx): drop the issue-ID citation from the AGENTS.md deferral bullet `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 --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 814feb8 commit b5e09b2

5 files changed

Lines changed: 422 additions & 22 deletions

File tree

.githooks/pre-commit

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,18 @@
55
# which `pnpm install` runs. Cheap by construction: with no pending marker it
66
# exits before doing any work, which is every commit that did not just merge a
77
# generator-owned artifact (#4675).
8+
#
9+
# One commit is exempt, and only one: the MERGE commit itself, which the
10+
# sanctioned landing sequence (`scripts/pm/os-regen-merge.sh`, step 3) commits
11+
# BEFORE regenerating, so that "what main brought" stays readable apart from
12+
# "what the change produces". That commit records a deferral instead of being
13+
# refused; the immediately following commit must discharge it, and
14+
# `.githooks/pre-push` refuses the push if none ever does (#8047).
15+
#
16+
# ⚠️ git does not run this hook for a merge it completes ITSELF — only for one
17+
# you finish with `git commit`. A clean auto-committed merge therefore lands
18+
# with the marker untouched and the refusal falls on the next commit, which is
19+
# the same collection point either way.
820

921
if [ -z "$OS_SKIP_REGEN_CHECK" ]; then
1022
node "$(git rev-parse --show-toplevel)/scripts/check-regen-pending.mjs" || exit 1

.githooks/pre-push

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
#!/bin/sh
2+
# Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
3+
#
4+
# The collection half of the os-regen deferred merge (#8047). `pre-commit` may
5+
# accept a MERGE commit whose regeneration is deferred to the next commit — but
6+
# at the moment it decides, that next commit does not exist yet, so it can only
7+
# RECORD the deferral. Something has to be the event that notices the deferral
8+
# was never discharged. Only two events can follow a merge commit: another
9+
# commit (which `pre-commit` already refuses while the artifacts are stale) and
10+
# the push. This is the push.
11+
#
12+
# Registered by the same `core.hooksPath=.githooks` that registers `pre-commit`,
13+
# so it needs no change to `scripts/setup-git-hooks.mjs` and no separate opt-in.
14+
#
15+
# Cheap by construction, exactly like `pre-commit`: with no pending marker the
16+
# script exits before doing any work, which is every push in this repo that did
17+
# not just defer a merge. The ref list git writes on stdin is drained and
18+
# ignored on purpose — the marker is per-worktree state, not per-ref state, so
19+
# which refs are being pushed cannot change the answer.
20+
21+
if [ -z "$OS_SKIP_REGEN_CHECK" ]; then
22+
cat >/dev/null
23+
node "$(git rev-parse --show-toplevel)/scripts/check-regen-pending.mjs" --pre-push || exit 1
24+
fi

AGENTS.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,18 @@ Even inside your own worktree, operate defensively:
364364
`$GIT_DIR/os-regen-pending` and `pre-commit` refuses the commit until those
365365
artifacts check clean. Sequence after a merge unchanged from §9: rebuild, then
366366
`check:generated --fix` — you just cannot forget it. Worth knowing:
367+
- **The MERGE commit itself is the one exemption, and it is a deferral, not a
368+
pass** (maintainer ruling 2026-08-12). `scripts/pm/os-regen-merge.sh` is the
369+
in-repo authority for landing one of these branches, and its step 3 commits
370+
the merge **before**
371+
regenerating on purpose: the driver exits 0 while silently dropping one side, so
372+
only a separate regeneration commit on a known-good base lets a reviewer read
373+
"what main brought" apart from "what the change produces". `pre-commit` records
374+
that merge as a deferral and then holds you to it — the immediately following
375+
commit must discharge it (every commit until then is refused, and a second merge
376+
cannot defer on top of an outstanding one), and `.githooks/pre-push` refuses a
377+
push that still owes one. ⛔ So this step never needs `--no-verify`, which was
378+
the old spelling and skips *every* pre-commit check rather than this one.
367379
- **The driver is a LOCAL facility** — the merge queue rebuilds server-side where no
368380
custom driver runs, so the three hottest artifacts are **sharded** per
369381
category/entry (`authorable-surface/`, `json-schema.manifest/`, `api-surface/`) to

0 commit comments

Comments
 (0)