Skip to content

feat(routes): publish routes.d.ts through Effect FileSystem with an ensuringRemoved staging file (FileSystem phase 1, module 3) - #520

Merged
ScriptedAlchemy merged 3 commits into
mainfrom
feat/effect-filesystem-phase1-typegen
Sep 4, 2026
Merged

feat(routes): publish routes.d.ts through Effect FileSystem with an ensuringRemoved staging file (FileSystem phase 1, module 3)#520
ScriptedAlchemy merged 3 commits into
mainfrom
feat/effect-filesystem-phase1-typegen

Conversation

@ScriptedAlchemy

@ScriptedAlchemy ScriptedAlchemy commented Sep 4, 2026

Copy link
Copy Markdown
Owner

Phase 1 of the Effect FileSystem / Path adoption, module 3, on top of #508 (module 2: platform.ts, withTempDirectory, merged as 8c70ffa) and #501 (module 1: the scaffolder pilot, merged as 4c911b0).

The design listed src/routes/typegen.ts as phase-1 eligible but blocked on #497 (xref-typegen-default). #497 merged as fc4d6b6 without touching typegen.ts or graph.ts, so this PR does the migration.

What changes

Site (before) After
src/routes/typegen.ts writeRouteTypesnode:fs/promises rm (empty graph), mkdir -p, writeFile to <output>.<pid>.<uuid>.tmp, rename onto .agent-bundle/routes.d.ts, finally rm(tmp, { force: true }) writeRouteTypesProgram(root, graph): Effect<string, PlatformError, FileSystem> — the same steps over FileSystem.FileSystem (remove / makeDirectory / writeFileString / rename), with the staging file inside ensuringRemoved(temporary, ...). writeRouteTypes keeps its Promise<string> signature: runWithPlatform(writeRouteTypesProgram(root, graph)). Its only caller is dev/project-service.ts (project preparation, behind build / dev / every API call that prepares a project); it is not a public export.

New in src/effect/platform.ts: ensuringRemoved(path, use) — the try { use } finally { rm(path, { recursive: true, force: true }) } bracket for a single path: force, cleanup failure as a typed PlatformError that wins over the operation's failure (as the throwing finally did), cleanup on interruption (uninterruptibleMask + Effect.exit). withTempDirectory is now makeTempDirectory inside the mask + ensuringRemoved(directory, restore(use(directory))) — one bracket implementation (Codex P1 on the first push). tests/effect-platform.test.ts keeps its existing cases and adds one: an external Fiber.interrupt reaches an operation parked on Effect.never and the directory is removed afterwards, which fails if the restore is dropped.

Error contract: runWithPlatform unwraps PlatformError to the NodeJS.ErrnoException it carries, so the ENOTDIR / EEXIST / EXDEV rejections project-service.ts sees are the identical Node errors as before. No AB#### diagnostic is involved on this path.

Not touched:

  • src/routes/graph.ts — its one async read (readRouteModuleText, a readFile that swallows a racing deletion) sits inside compileRouteGraph, the compiler/cold-start discovery path the conventions keep raw; lifting that single call would add an Effect runtime per compile for no cleanup benefit. Recorded in docs/effect-conventions.md as a deliberate stay-raw, not a phase-2 item.
  • Everything on the hard keep-raw list, and the two phase-2 deferrals from feat(api): stage temporary artifacts and Codex schema output in Effect scoped temp directories (FileSystem phase 1, module 2) #508 (mcp-session-service.ts ownership transfer, script-playground-service.ts cleanup-failure contract).

Artifact parity

Rebuilt examples/host-test (90 files) on the #508 head and on this branch, diff -rq. Contents of every hook wrapper, MCP bundle, bin/*, installer, and manifest field are semantically identical, but this module is the first phase-1 change that is not byte-identical in the emitted hook wrappers, and I want that on the record rather than hidden:

Tests

  • tests/route-typegen-write.test.ts (new): real temp directory — publishes the declarations byte-for-byte equal to generateRouteTypes(graph) with no *.tmp left behind; removes a stale routes.d.ts for an empty graph and is a no-op when none exists; rejects with the Node ENOTDIR/EEXIST when .agent-bundle is a file (no staging file written). Over FileSystem.layerNoop with explicit overrides for makeDirectory / writeFileString / rename / remove (recording every call): an EXDEV on rename removes the staging file (force) and rethrows the Node error unchanged; an empty graph issues exactly one remove(output, { force: true }) and nothing else.
  • Locally: pnpm lint, pnpm typecheck, pnpm test:unit (3236 passed after the rebase onto main), pnpm docs:site:build (parity + dead-link checks green), examples/host-test build for the parity check above.

Docs

docs/effect-conventions.md: ensuringRemoved added next to withTempDirectory in the Adopt list and the platform.ts boundary paragraph; routes/typegen.ts listed as a phase-1 caller; routes/graph.ts recorded as stay-raw with the reason.

Review status

  • Awaiting automated review on the current head.

@changeset-bot

changeset-bot Bot commented Sep 4, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: d971655

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
agent-bundle Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 4, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-04T04:36:00.502821Z d971655 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

ScriptedAlchemy added a commit that referenced this pull request Sep 4, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6bbdde07e2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +82 to +86
Effect.uninterruptibleMask((restore) => Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const exit = yield* Effect.exit(restore(use));
yield* fs.remove(path, { force: true, recursive: true });
return yield* exit;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Route both cleanup brackets through one implementation

The new helper repeats the Effect.exit/fs.remove finalization that remains inline in withTempDirectory at lines 102–108, even though docs/effect-conventions.md now says ensuringRemoved is the bracket behind that function. Any future fix to cleanup, interruption, or error precedence can therefore update one path without the other—the exact divergence this repository's extract-and-rewire rule is intended to prevent. Factor the shared bracket so withTempDirectory delegates to it while still creating the directory inside the outer interruption mask.

AGENTS.md reference: AGENTS.md:L5-L10

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 4502442: withTempDirectory now creates the directory inside its mask and delegates to ensuringRemoved(directory, restore(use(directory))) — one bracket implementation. The inner mask is a no-op inside the outer one, so the operation is restored to the caller's interruptibility before it enters the bracket. Added a test that an external Fiber.interrupt reaches an operation parked on Effect.never and the directory is gone afterwards; it fails (2 s timeout) if the restore is dropped, which is the mistake this delegation could otherwise hide.

@pkg-pr-new

pkg-pr-new Bot commented Sep 4, 2026

Copy link
Copy Markdown
npm i https://pkg.pr.new/ScriptedAlchemy/agent-bundle@520
npm i https://pkg.pr.new/ScriptedAlchemy/agent-bundle/create-agent-bundle@520
npm i https://pkg.pr.new/ScriptedAlchemy/agent-bundle/@agent-bundle/runtime@520

commit: d971655

@ScriptedAlchemy

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Bravo.

Reviewed commit: 4502442232

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

…Removed staging file

writeRouteTypesProgram is the Effect program (FileSystem-only requirement,
PlatformError failure); writeRouteTypes keeps its Promise signature through
runWithPlatform. ensuringRemoved(path, use) in src/effect/platform.ts is the
try/finally rm(path, { force: true }) bracket, and withTempDirectory now
builds on it. Generated declarations, the write-then-rename, and thrown Node
errors are unchanged; unit tests cover the success path, the empty-graph
removal, mkdir failure, and an EXDEV rename through FileSystem.layerNoop.
…Removed

One bracket implementation; the directory is still created inside the outer
mask and the operation is restored to the caller's interruptibility before it
enters the inner bracket. New test: an external Fiber.interrupt reaches a
parked operation and the directory is removed afterwards (fails if the
restore is dropped).
@ScriptedAlchemy
ScriptedAlchemy changed the base branch from feat/effect-filesystem-phase1-tempdirs to main September 4, 2026 04:29
@ScriptedAlchemy
ScriptedAlchemy force-pushed the feat/effect-filesystem-phase1-typegen branch from 4502442 to d971655 Compare September 4, 2026 04:29
@ScriptedAlchemy

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Delightful!

Reviewed commit: d9716557da

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@ScriptedAlchemy
ScriptedAlchemy merged commit 5b9d3b7 into main Sep 4, 2026
13 of 17 checks passed
@ScriptedAlchemy
ScriptedAlchemy deleted the feat/effect-filesystem-phase1-typegen branch September 4, 2026 20:02
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