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

hotlong merged 2 commits into
mainfrom
claude/issue-8047-os-regen-hook-deferred-merge

Conversation

@hotlong

@hotlong hotlong commented Aug 12, 2026

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

vercel Bot commented Aug 12, 2026 •

Copy link
Copy Markdown

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

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 12, 2026 8:13pm

Request Review

@hotlong hotlong added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 12, 2026 — with Claude
@github-actions github-actions Bot 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
@hotlong
hotlong marked this pull request as ready for review August 12, 2026 20:35
@hotlong
hotlong added this pull request to the merge queue Aug 12, 2026
Merged via the queue into main with commit b5e09b2 Aug 12, 2026
21 checks passed
@hotlong
hotlong deleted the claude/issue-8047-os-regen-hook-deferred-merge branch August 12, 2026 20:53
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Aug 17, 2026
…ck did not run (objectstack-ai#8253)

`Check Documentation Links` went red three times on 2026-08-12 without
examining a single link. lychee-action@v2's `lychee-setup` step fetches the
release tarball with a bare `curl -sfLO` -- no retry -- and when that one
request loses, curl exits 22, the action's `Install lychee` and `Run Lychee`
steps both report `skipped`, and the job fails in ~9 seconds. All three job
logs read directly, byte-identical in shape:

  objectstack-ai#8128  17:23:49Z  run 31622391000 job 94200196323  exit 22, 497ms
  objectstack-ai#8205  20:08:47Z  run 31636151516 job 94246838620  exit 22, 177ms
  objectstack-ai#8225  21:21:28Z  run 31642129140 job 94266966755  exit 22

Transient, not systemic: each cleared on the next attempt, and a sibling PR
went green six minutes after the last failure.

Three changes, none of them a weakening -- `fail: true` stays on every path
and a retry that exhausts still fails the job.

Retry. The action is invoked twice: attempt 1 defers its verdict via
`continue-on-error`, a 15s wait follows, then an identical retry that carries
no such escape. A back-to-back retry would retry inside the same blip; every
observed recovery was tens of seconds to tens of minutes later. Zero cost on a
green run -- all three added steps are `skipped` when attempt 1 passes.

Legibility. A new step distinguishes "links are broken" from "the link check
never ran" and says so in the job summary and as an error annotation. The
discriminator is exact, not heuristic: the action's entrypoint.sh writes
`exit_code` to $GITHUB_OUTPUT *before* it exits, so a genuine broken-link
failure carries a value while a setup failure skips `Run Lychee` and leaves it
unset. It exits 1 on its own so the case stays red even if someone later makes
the retry lenient.

Version pin. Three comments in this file reasoned about "the pinned lychee
0.24.2" -- the `--offline` argument turns on which version runs -- but nothing
here pinned it; the version came from the action's own default, which moves
when the `v2` tag moves. `lycheeVersion: v0.24.2` asserts at the invocation
site what `--offline` already depends on.

The argv moves to a job-level `env` so the two invocations cannot drift.
Verified byte-identical to origin/main's inline args by parsing both.

Caching (the card's shape 1) is NOT included, on measurement: the action
`rm -rf`s its download directory and re-downloads unconditionally, so
`actions/cache` cannot reach it -- a cache step here would read as coverage
while doing nothing.

Part of objectstack-ai#8238


Claude-Session: https://claude.ai/code/session_01BytmXbyC9R2Wvpg2uW14fc

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m skip-changeset PR 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