Skip to content

Land the first-run ENOENT fix on main (PR #11 merged to the wrong branch) - #13

Merged
jgfoster merged 1 commit into
mainfrom
fix/first-run-staging-to-main
Aug 25, 2026
Merged

Land the first-run ENOENT fix on main (PR #11 merged to the wrong branch)#13
jgfoster merged 1 commit into
mainfrom
fix/first-run-staging-to-main

Conversation

@jgfoster

Copy link
Copy Markdown
Member

#11 is marked merged, but its fix is not on main. This re-lands it, unchanged.

What happened

#11 was stacked on fix/grail-retired-canonical-flag so it could go green while #12 was still open. #12 then merged that branch into main, but the branch itself survived — precisely because #11 was still pointing at it — and GitHub only auto-retargets a stacked PR when its base branch is deleted. So #11 stayed based on the stale branch and, on merge, wrote into it rather than into main.

Net effect: main has the canonical-flag fix (#12) and not the ENOENT fix (#11).

$ git merge-base --is-ancestor add7887 origin/main   # the ENOENT fix
NO
$ git grep -c stageAndRecordGrail origin/main -- src/grail.ts
absent from main

The commit was not lost — it sits on origin/fix/grail-retired-canonical-flag behind merge commit cc5bf3b, which is the only reason that branch still needs to exist. My mistake, in setting the stack up that way.

What this PR is

add7887 cherry-picked onto main, byte-identical:

 CHANGELOG.md                         |   9 +
 CLAUDE.md                            |  19 +
 src/__integration__/database.test.ts |  14 +-
 src/__tests__/grailStaging.test.ts   | 114 +++++++++++++
 src/database.ts                      |  19 +-
 src/grail.ts                         |  30 +++
 src/lifecycle.ts                     |  18 +--

So it is still the fix for the reported first-run failure: setup dying with ENOENT … '<root>/grail/.gemdb-grail-stamp' right after "Database created", because Grail was recorded as filed in before anything created the directory that record lives in — and, on upgrades, the same misordering making GemDB skip staging a new payload entirely.

Verification

Re-run on top of current main, not carried over: 114 unit tests, 46 integration against a payload rebuilt from Grail HEAD (dab4588). Lint, format, both typechecks clean.

After this merges

fix/grail-retired-canonical-flag becomes safe to delete — it is currently the only remote copy of this commit.

🤖 Generated with Claude Code

A user's first install died just after "Database created":

    Setting up GemDB failed: ENOENT: no such file or directory,
    open '/Users/…/GemDB/grail/.gemdb-grail-stamp'

prepareFiles recorded Grail as filed into the database before anything
had created <rootPath>/grail for the record to live in. Python support
was then never staged, and every retry failed the same way.

The same misordering broke upgrades, more quietly. Where the directory
did exist, the stamp was written into the PREVIOUS version's copy;
grailNeedsUpdate then compared the bundled stamp with the bundled stamp,
found them equal, and skipped staging -- so a new version's payload never
reached disk, wearing the new version's label, and writeCliScripts never
re-ran to refresh bin/gemdb. It could not have worked either way, since
stageGrail replaces the directory wholesale and deletes the stamp it was
just handed.

Neither failure reproduces on a machine that has run an earlier version,
which is how both reached a release: every developer machine already had
the directory, and the integration suite calls stageGrail itself rather
than going through prepare.

The order now lives in one named place, stageAndRecordGrail, with the
reasons attached. The condition moved too: createDatabase returns
{created, preloaded} so the stamp follows this call having made the
database from the shipped extent, rather than the extension merely
shipping one. An upgrade finds a database carrying whatever Grail was
filed into it before, and stamping there would claim an install that
never happened; leaving it unstamped is what sends ensureRunning to file
the new Grail in.

src/__tests__/grailStaging.test.ts starts from a root path that does not
exist, which is the condition the existing tests could not express. Two
of its five cases fail with the calls reversed -- one reproducing the
reported ENOENT verbatim -- and database.test.ts pins the new return.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@jgfoster
jgfoster added this pull request to the merge queue Aug 25, 2026
Merged via the queue into main with commit 21755aa Aug 25, 2026
5 checks passed
@jgfoster
jgfoster deleted the fix/first-run-staging-to-main branch August 25, 2026 17:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant