Skip to content

Fix first-run setup failing with ENOENT on the Grail stamp - #11

Merged
jgfoster merged 1 commit into
fix/grail-retired-canonical-flagfrom
fix/first-run-grail-staging
Aug 25, 2026
Merged

Fix first-run setup failing with ENOENT on the Grail stamp#11
jgfoster merged 1 commit into
fix/grail-retired-canonical-flagfrom
fix/first-run-grail-staging

Conversation

@jgfoster

Copy link
Copy Markdown
Member

A user's first install died immediately after creating the database:

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 created <rootPath>/grail for that record to live in. Python support was then never staged, and every retry failed identically — a permanent first-run failure.

Note what is missing from their log: no Staging Grail … line between Database created and the error. That is the tell.

The same misordering broke upgrades, more quietly

Where <rootPath>/grail did exist, the stamp was written into the previous version's directory. grailNeedsUpdate then compared the bundled stamp against the bundled stamp, found them equal, and skipped staging. So:

  • a new version's Grail payload never reached disk, while wearing the new version's label;
  • writeCliScripts never re-ran, leaving bin/gemdb at its old contents.

It could not have worked in either order, in fact: stageGrail replaces the directory wholesale, so it deletes the stamp it was just handed.

Why it reached a release

Neither failure reproduces on a machine that has already run an earlier version — the directory is simply there. And the integration suite calls stageGrail itself against a fixture, so it never goes through prepare at all. The gap was a first-run path with no test that could reach it.

The fix

The order now lives in one named place with the reasons attached:

export function stageAndRecordGrail(extensionPath: string, fromPreloadedExtent: boolean): void {
  if (grailNeedsUpdate(extensionPath)) stageGrail(extensionPath);
  if (fromPreloadedExtent) recordGrailInstalled(extensionPath);
}

The condition moved too, and it matters as much as the order. createDatabase now returns {created, preloaded}, so the stamp follows this call having made the database from the shipped extent rather than "the extension ships an extent". An upgrade finds a database carrying whatever Grail was filed into it before; stamping there would claim an install that never happened. Leaving it unstamped is exactly what sends ensureRunning to file the new Grail in.

Tests

src/__tests__/grailStaging.test.ts starts from a root path that does not exist — the condition the existing tests could not express. Reverting to the old order fails two of its five cases, one reproducing the reported error verbatim:

AssertionError: expected [Function] to not throw an error but
'Error: ENOENT: no such file or directory, open '…/GemDB/grail/.gemdb-grail-stamp''

database.test.ts pins the new return value, including that a second call creates nothing.

114 unit tests (was 109), 46 integration. Lint, format, both typechecks clean.

For the affected user

Deleting ~/GemDB/db (or the whole ~/GemDB) and reloading gives the fast path: the database is recreated from the shipped extent and stamped, no file-in. Without that, the fix still recovers them — Grail stages, and because the database already exists it is correctly left unstamped, so the first Python they run files it in, which takes a few minutes once.

🤖 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 force-pushed the fix/first-run-grail-staging branch from fc33a93 to add7887 Compare August 24, 2026 23:27
@jgfoster
jgfoster changed the base branch from main to fix/grail-retired-canonical-flag August 24, 2026 23:27
@jgfoster
jgfoster merged commit cc5bf3b into fix/grail-retired-canonical-flag Aug 25, 2026
5 checks passed
@jgfoster
jgfoster deleted the fix/first-run-grail-staging branch August 25, 2026 12:51
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