Uh oh!
There was an error while loading. Please reload this page.
fix(devx): keep check-regen-pending's self-test gate stub out of the fixture index - #9323
Merged
os-project-manager merged 1 commit intoAug 17, 2026
Merged
Conversation
…fixture index Co-Authored-By: Claude <noreply@anthropic.com>
os-project-manager
marked this pull request as ready for review
August 17, 2026 15:03
Uh oh!
There was an error while loading. Please reload this page.
os-project-manager
deleted the
claude/issue-9258-regen-selftest-merge-abort
branch
August 17, 2026 15:23
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes#9258
pnpm check:merge-driverrunsnode scripts/check-regen-pending.mjs --self-test, and its fixture crashed on ~40% of observed CI runs — 2 of 5 today, across 4 PRs, none of which touch this script:Root cause, measured
The card offered two candidate mechanisms and asked for one to be established. They turn out to be two halves of the same thing, and measurement separates them cleanly.
1. The defect: the fixture's
package.jsonis TRACKED. It is the harness's gate stub —runHookrewrites it to flip the stubcheck:spec-changesbetweenexit 0andexit 1— and it is no part of the two-commit scenario under test. It became tracked by accident: thegit add -Aon theside2branch sweeps it in. Probed directly:git log --diff-filter=A -- package.jsonnames theside2commit, and the stub is untracked through the whole first merge.2. The intermittency: git compares mtime at one-second granularity.
runHookis called betweengit merge --no-commit --no-ff side2andgit merge --abort, so it rewrites a tracked file just after the merge recorded that file's stat data in the index. The stub's content is byte-identical every time, so the file is only ever stat-dirty — andmerge --abortis areset --merge, whoseverify_uptodatecompares mtime in whole seconds. Land the rewrite in the same wall-clock second as the merge's index write and the entry reads clean; land it in the next second and the abort refuses to discard the file.So it was never "the tree is sometimes dirty". The tree is dirty on every single run. What varies is only whether git can see it.
Deterministic reproduction
The card asked for this first, and it is the part that makes the rest checkable. All on this box, git 2.43.0:
merge --abortfailedorigin/mainscript, as-isorigin/mainscript, rewrite delayed past a second boundaryThe forced-failure runs reproduce the CI signature byte for byte, including the frame the issue quotes:
The delay was injected by a PATH shim that pauses 1.1s after any
git merge --no-commit, modelling a loaded runner. Real git underneath, unmodified; the +400ms row shows the same knob reproducing the observed CI rate.The local box could never have found this by re-running: it passes 30/30 either way, exactly as the card warned. The variable is timing, and the shim is what supplies it.
The fix
Shape (c) of the three the card named: the stub never enters the index. One repo-local
info/excludeline, written right aftergit init, so that everygit addin the fixture — the ones here today and the ones added later — leaves the stub alone..git/info/excluderather than a.gitignore, because a.gitignorewould itself be a tracked file inside the merges under test: changing the scenario in order to protect the harness.Shapes (a) and (b) are rejected on the measurement, not on taste:
runHookthat dirties the tree is unreachable. ThatrunHookis the assertion — it has to run whileMERGE_HEADexists, which is the entire content of the case ("a SECOND merge cannot defer on top of an outstanding deferral").package.jsonto its committed content while a merge is in progress is a no-op. The content already is the committed content, byte for byte. Only the stat differs — so restoring content fixes nothing, and restoring the mtime would couple the harness to git's stat-comparison internals, which is a worse thing to depend on than the thing being fixed.No retry, no
try/catch, no|| trueanywhere near the abort — per the card, and because a swallowed abort failure is precisely the class of defect this script exists to prevent.The regression guard
One new assertion, placed where the invariant is load-bearing:
Deliberately a state assertion rather than the symptom: with the stub in the index the crash is a coin flip, so the state is the only thing reportable. Ablated (exclude removed, assertion kept) it goes red 5/5 with no shim and no crash — an intermittent crash converted into a deterministic, self-explaining assertion failure. It covers every
git addin the fixture, not only the one that broke.Verification
Gate union re-derived with
node scripts/pm/dispatch-gates.mjsfrom thegit merge-basediff (notorigin/main..HEAD), against the one changed path, and run at HEADcba224879:pnpm check:merge-driver(derived)✓ check-regen-pending self-test passed.pnpm check:nul-bytespnpm exec eslint scripts/check-regen-pending.mjs --no-inline-configpnpm check:required-contextsSelf-test pass rate: 30/30 before, 30/30 after on this box, and 0/10 vs 5/5 under the adverse timing that is the actual bug.
The second item, stopped and reported
The card asked, if cheap, to make the failure surface say which gate broke — today a merge-driver crash reports as a failing check named
ESLint.It is not cheap, and the reason is in-repo.
scripts/check-required-contexts.mjspins the jobname:literals as contract, andESLintis a registered entry (authorized: '#5617 maintainer ruling 2026-08-07 — applied to the settings the same day'). A GitHub required status check is matched by check-run name, so renaming the job makes the old required context sit permanently pending, which wedges every PR and the merge queue. The corresponding Settings entry cannot be changed from an agent seat — that same file recordsGET /repos/objectstack-ai/objectstack/branches/main/protectionanswering 403GitHub access is not enabled for this session, and I re-confirmed there is noghon this runner either.There is also no atomic ordering: repo-side rename and Settings-side rename cannot land together, so either sequence leaves a window where the gate is pending-forever or silently advisory — the second being #5617 verbatim. That needs a maintainer, so it is filed as #9325 rather than ridden in here — with the two-halves-cannot-land-atomically analysis, the registry pin, and three candidate shapes.
Changeset
None. The change is confined to
scripts/, a dev-tooling self-test fixture; nothing published moves. Carrying theskip-changesetlabel instead.Generated by Claude Code