Repository navigation
[finding] check-regen-pending.mjs's fixtureSelfTest crashes intermittently at git merge --abort — runHook() dirties the fixture's package.json while a merge is in progress #9258
Description
Activity
os-project-manager commented
on Aug 17, 2026 CollaboratorAuthorMore actionsThird occurrence — it now has a measurable rate, and the rate is bad enough to cost real time
PM dispatch seat, session
session_01Y26DJEHSBhhAQ6wwfsHNza. Adding a data point rather than re-filing.PR #9291 (issue #9120), head
6e7fe991d, run 32027798108 at 12:06:50Z — identical signature, identical place:error: Entry 'package.json' not uptodate. Cannot merge. fatal: Could not reset index file to revision 'HEAD'.⚠️ #9291 does not touch this script either. Its diff is three files: a changeset,packages/rest/src/rest-env-resolution.test.ts, andpackages/rest/src/rest-server.ts.Observed rate today — the number that changes the priority
run PR check-regen-pending --self-test07:38:14Z #9253 ✅ pass 08:17:13Z #9256 ❌ crash 09:16:57Z #9273 ✅ pass 09:39:41Z #9278 ✅ pass 12:06:50Z #9291 ❌ crash ⭐ Two crashes in five observed runs, across four different PRs, none of which touch the script. When I filed this I could only say "intermittent"; ~40% on a small sample is not a rare edge — it is a coin-flip on the last line of defence, and every hit costs a re-run cycle plus a diagnosis by whoever is driving.
⚠️ The sample is small and I am not claiming 40% is the true rate. What it does establish is that this is not the once-in-a-while event the original filing left room for, and that waiting for it to disappear is not a strategy.The compounding cost, which is the real argument
Each occurrence presents as a failing check named "ESLint" on a PR whose diff has nothing to do with linting or with merge drivers. Whoever meets it has to open the log, discover the job runs a chain of gates, find that
check:merge-driveris the one that broke, and only then decide it is unrelated. I have now paid that cost twice today, and the second time I still had to re-derive it because "flaky" is not a diagnosis I am willing to assert without checking that the failing step is the same one.⇒ Two things worth doing, in this order:
- Fix the fixture (the ordering defect in the original filing —
runHookdirtiespackage.jsonbetweenmerge --no-commitandmerge --abort), and - ⭐ consider whether the job's name should say what it covers. A check called "ESLint" that fails inside the merge-driver self-test mis-routes every diagnosis it triggers.
⛔ Still not fixing it from this seat — it is not in any dispatched card's scope, and re-running is not a fix. Re-run of the failed job requested for #9291.
Generated by Claude Code
- Fix the fixture (the ordering defect in the original filing —
os-project-manager commented
on Aug 17, 2026 CollaboratorAuthorMore actionsClaiming this card from the dev seat dispatched under PM session
session_01Y26DJEHSBhhAQ6wwfsHNza.Branch:
claude/issue-9258-regen-selftest-merge-abort(pushed). Not touching the assignee — the PM holds the claim.
Generated by Claude Code
os-project-manager commented
on Aug 17, 2026 CollaboratorAuthorMore actions{ "issue": 9258, "status": "done", "branch": "claude/issue-9258-regen-selftest-merge-abort", "pr": "https://github.com/objectstack-ai/objectstack/pull/9323", "premise_still_valid": true, "summary": "Both of the card's candidate mechanisms turned out to be halves of one defect, and the measurement separates them. The DEFECT is that the fixture's package.json is TRACKED at all: it is the harness's gate stub, no part of the scenario under test, and the `git add -A` on the `side2` branch sweeps it in (confirmed by probe: `git log --diff-filter=A -- package.json` names the side2 commit, and the stub is untracked through the entire first merge). The INTERMITTENCY is git's one-second mtime granularity: runHook rewrites that tracked file between `merge --no-commit` and `merge --abort` with byte-identical content, so the file is only ever stat-dirty, and `merge --abort` (a `reset --merge`) sees it as modified exactly when the rewrite lands in a later wall-clock second than the merge's index write. The tree is dirty on every run; only git's ability to see it varies. Fixed with shape (c) of the three the card named — one repo-local `.git/info/exclude` line written right after `git init`, so every `git add` in the fixture, present and future, leaves the stub alone — plus one new state assertion pinning the invariant. Shapes (a) and (b) were rejected on measurement: (a) is unreachable because that runHook IS the assertion and must run under MERGE_HEAD, and (b) is a no-op because the content already is the committed content byte for byte.", "tests": "DETERMINISTIC REPRO (the deliverable the card asked for first). Injected timing with a PATH shim that pauses 1.1s after any `git merge --no-commit`, real git underneath unmodified, modelling a loaded runner. origin/main script + shim: 5/5 crashes, byte-identical CI signature including the quoted frame -- \"Error: Command failed: git merge --abort / error: Entry 'package.json' not uptodate. Cannot merge. / fatal: Could not reset index file to revision 'HEAD'. / at fixtureSelfTest (.../scripts/check-regen-pending.mjs:455:5)\". This PR's script + same shim: 0/10. An isolated replay harness dialled the rate directly: +1100ms 10/10 fail, +400ms 4/10 fail (the observed CI rate), stub-excluded 0/10 at every delay. PASS RATE, no shim: 30/30 before and 30/30 after -- stated as a NON-control, because this box is git 2.43.0 and never reproduced the crash naively; the shim rows are the evidence, the 30/30 rows only show no regression. REVERSE VERIFICATION, direction predicted before running and confirmed: ablating the exclude while keeping the new assertion turns the intermittent crash into a DETERMINISTIC assertion failure -- 5/5 red with no shim and no crash, printing ' x ...with the gate stub still OUT of the index, so `merge --abort` cannot trip on it'; with the shim added it also crashes 3/3. Fix was committed before the ablation and restored via `git checkout <branch> -- <path>` (worktree clean afterwards, byte-identical to the commit; no `git stash` anywhere). GATE UNION re-derived with `node scripts/pm/dispatch-gates.mjs` from the `git merge-base` diff (not `origin/main..HEAD`), which yields exactly one changed path, `scripts/check-regen-pending.mjs`, and one derived family, `check:merge-driver`. Run AFTER the final commit, at HEAD cba224879: `pnpm check:merge-driver` pass ('check-regen-pending self-test passed.'), `pnpm check:nul-bytes` pass (6067 files, no raw control bytes), `pnpm exec eslint scripts/check-regen-pending.mjs --no-inline-config` pass, `pnpm check:required-contexts` pass (added beyond the derived set because the second item made me consider a job rename; unchanged and green). No ablation of a built artifact is involved -- this is a pure scripts/ change with no dist/ in the loop.", "open_questions": [ { "question": "The card's second item (make the failure surface name the broken gate instead of reporting as 'ESLint') cannot be done from an agent seat, so it needs a maintainer decision on shape and sequencing. Filed as #9325; recording it here because the PM has to route it, not because it blocks this PR.", "options": [ "A -- rename the context to something naming the family (e.g. 'Lint and repo gates'), coordinating lint.yml, the REQUIRED_CONTEXTS registry literal in scripts/check-required-contexts.mjs, and the Settings ruleset entry in one sitting", "B -- split the lint job so gate families report as separate required contexts: best diagnosability, but it multiplies the required-context set and each new entry is a fresh permanently-pending risk", "C -- keep the name and surface the failing step via an `if: failure()` step writing to $GITHUB_STEP_SUMMARY: needs an `id:` on ~54 steps and still leaves the wrong name on the PR checks list" ], "recommendation": "A, with the sequencing done by whoever can see both halves. The blocker is not difficulty but access: `check-required-contexts.mjs` pins `ESLint` as contract (authorized by the #5617 maintainer ruling 2026-08-07), a required check is matched BY check-run name, and the Settings half answers 403 'GitHub access is not enabled for this session' to every agent seat -- re-confirmed today, and there is no `gh` CLI on this runner either. There is also no atomic ordering: rename-first leaves the old context permanently pending (wedging every PR and the merge queue), settings-first degrades ~54 gates to advisory with no signal, which is #5617 verbatim. C is the only agent-doable option and it is the one that does not fix the reported harm." } ], "out_of_scope_findings": [ "filed as #9325: the `ESLint` required context carries ~54 sequential `check:*` steps, so any of their failures publishes one red check whose name describes none of them -- the mis-routing that cost this card's diagnosis three times today. Left unlabeled and unassigned for PM triage; body carries the two-halves-cannot-land-atomically analysis, the REQUIRED_CONTEXTS pin, the 403 measurement, and three candidate shapes." ] }Notes for the PM, outside the JSON:
- PR fix(devx): keep check-regen-pending's self-test gate stub out of the fixture index #9323 is a draft,
skip-changesetapplied and read back as the union["size/s","skip-changeset"](the auto-labeler'ssize/shad already landed, so this was a union write, not a bare set). No changeset: the diff is onescripts/self-test fixture, nothing published moves. - CI status at handback is
in_progress— reporting at draft-PR time per the standing contract rather than waiting for convergence. ⚠️ The shared GitHub identity was rate-limited for roughly ten minutes mid-task (API rate limit already exceeded for user ID 314343378) — reads failed while writes still succeeded. It cost latency only; every step above completed. Worth knowing if other seats report the same window.
Generated by Claude Code
- PR fix(devx): keep check-regen-pending's self-test gate stub out of the fixture index #9323 is a draft,
- added a commit that references this issue
on Aug 18, 2026 - added a commit that references this issue
on Aug 23, 2026
Filed unassigned by the PM dispatch seat (session
session_01Y26DJEHSBhhAQ6wwfsHNza) while diagnosing a CI failure on PR #9256. ⛔ Not fixed here — filing rather than fixing because it is outside that PR's scope and needs its own verification surface.What happened
pnpm check:merge-driverrunsnode scripts/check-regen-pending.mjs --self-test. On PR #9256 the self-test crashed — not failed an assertion, crashed — in the fixture:Every assertion before that point passed. The crash is in the fixture's own sequencing, not in the behaviour under test.
⭐ It is intermittent, and that is measured, not assumed
Two runs on the same CI image, same
git version 2.54.0, 39 minutes apart, on the same unmodified script:✓ check-regen-pending self-test passed.— ran straight through the samemerge --abortmerge --abortpackages/cloud-connection/. #9253's is onecontent/docs/**file.⛔ A local pass is NOT a control here and I am not offering it as one. It does pass on clean
origin/mainlocally (22/22, exit 0) — but local git is 2.43.0 against CI's 2.54.0, so that run cannot speak to the CI behaviour. The#9253CI pass above is the control that counts, because it holds the git version fixed.Leading hypothesis (⚠️ hypothesis, not a measured root cause)
Around
:455the fixture does:runHook(gate)rewritespackage.json(that is how it flips the stubcheck:spec-changesbetweenexit 0andexit 1) — and it is called while the merge is in progress, betweenmerge --no-commitandmerge --abort.merge --abortresets the index toHEADand refuses when a worktree file it would discard is not up to date.Why it only sometimes fires is the part I have not established. A plausible mechanism is git's racily-clean handling — when the write lands in the same filesystem-timestamp granule as the index write, git must re-hash and the outcome depends on whether the content genuinely differs from the committed blob — but ⛔ I did not measure this, and whoever takes the card should treat it as a lead, not a finding.
Why it is worth fixing rather than re-running
The whole point of this script is the deferred-merge discipline, and its self-test is what makes that discipline trustworthy. A self-test that crashes ~intermittently trains readers to re-run it, which is exactly how a real regression in this gate would get waved through. It also fails under a check named "ESLint", so the failure surface gives no hint that the merge-driver gate is what broke — worth a look while in there.
Suggested shape (not decided)
Move the
git(['merge', '--abort'])before therunHookthat dirties the tree, or haverunHookrestorepackage.jsonto its committed content when a merge is in progress, or make the fixture'spackage.jsonuntracked so it is never in the index the abort resets. Whichever shape wins, it should come with a deterministic reproduction, since the current failure is the kind that a fix can appear to solve by chance.Re-check commands:
Refs: PR #9256 (where it surfaced), PR #9253 (the passing control).