Skip to content

refactor(effect): run ordinary I/O of validators, services, eval, and post-build readers through FileSystem (phase 2, PR 1) - #540

Merged
ScriptedAlchemy merged 8 commits into
mainfrom
feat/effect-filesystem-phase2
Sep 4, 2026
Merged

refactor(effect): run ordinary I/O of validators, services, eval, and post-build readers through FileSystem (phase 2, PR 1)#540
ScriptedAlchemy merged 8 commits into
mainfrom
feat/effect-filesystem-phase2

Conversation

@ScriptedAlchemy

@ScriptedAlchemy ScriptedAlchemy commented Sep 4, 2026

Copy link
Copy Markdown
Owner

Summary

Effect FileSystem / Path adoption, phase 2, PR 1 of 2: the ordinary I/O of the modules behind command dispatch — host-contracts validators and native contracts, services/*, eval/*, and the post-build artifact readers — now runs as FileSystem programs through runWithPlatform, at each function's existing Promise signature. Behavior-preserving: same Node errors at the boundaries, same AB#### diagnostic codes and messages, same bytes. The keep-raw list in docs/effect-conventions.md is binding, and this PR adds to it where the pinned rc.112 API cannot express the site (lstat, O_NOFOLLOW, Dirent, dev/ino identity).

PR 2 (dev server runtime, makeScopedEffectRuntime(platformLayer) inside startDevServer) follows separately.

Migrated (→ FileSystem via runWithPlatform)

Module What moved Kept raw (file:line) and why
host-contracts/portable-plugin-validation.ts fileKind (stat, ENOENT/ENOTDIR/ELOOPmissing via isPlatformErrno), readDocuments, stdio/skill diagnostics (readDirectory) as one portablePluginByteDiagnostics program symlinkDiagnostics realpath/readdir(withFileTypes)/lstat (402–453): link-identity walk
host-contracts/claude-plugin-validation.ts validateClaudePluginFilesProgram (readJsonDocument, fileExists via fs.access), validationTargets, claudePluginManifestName matchingDocumentPaths readdir(withFileTypes) (164): Dirent kinds
host-contracts/cursor-plugin-validation.ts readRegularFile (stat type check + read), readDocument, readHooksDocument as readCursorPluginDocuments pathExists lstat (276), symlinkDiagnostics (473–507): symlink refusal
host-contracts/native-codex-contract.ts copyOpaqueCodexAuthStateProgram (statmakeDirectorycopyFilechmod mode & 0o777), smoke root makeTempDirectory, staging copy({ overwrite: true }) (matches cp), remove lstat before staging the candidate (382): dangling-symlink check
host-contracts/native-claude-contract.ts digestClaudeStateMcpServers read (#529's site) digestClaudeFileTree (311–321): lstat + Dirent digest walk
host-contracts/native-host-spine.ts digestFileTree (35–46): lstat + Dirent digest walk; comment added
services/hook-service.ts hook index read
services/mcp-service.ts #server manifest read is an Effect; the per-connection plugin-data dir is a withTempDirectory bracket around #connect (client close + stderr capture stop happen inside, before removal; cleanup failure wins as the throwing finally did)
services/mcp-run.ts manifest / .env reads (.env layering semantics untouched — #469 extends it), makeDirectory; SIGINT/SIGTERM forwarding is forwardingSignals, a scoped Effect.acquireRelease whose finalizer removes the listeners however the wait ends
eval/fixtures.ts copyPlannedEntries (makeDirectory / readFile / re-digest / writeFile / chmod) resolveFixtureSource lstat+realpath (64–77), plan-entry lstat (102), destination lstat (158)
eval/codex-home.ts createTemporaryCodexTrialHome (makeTempDirectory; ownership transfers to the harness, so not a bracket), removeTemporaryCodexTrialHome
eval/{artifact,harness,graders,claude-harness,codex-plugins}.ts manifest / evidence / call-log / grader / marketplace reads artifact.ts:63, graders.ts:109 lstat (symlink refusal); codex-plugins.ts:31 readdir(withFileTypes) (a symlinked skill dir is not a skill)
eval/codex-harness.ts candidate copy({ overwrite: true })
eval/workspace-diff.ts whole module (52–93): hashes through an O_RDONLY | O_NOFOLLOW descriptor and checks dev/ino/nlink against the discovering lstat; opendir for kinds. The brief's "scoped open" is not expressible: OpenFlag accepts only string flags. Added to the keep-raw list.
build/validate-artifact*.ts epoch marker, generated-schema, JSON, hook index/manifest, MCP manifest/main source, skill markdown reads; module bytes via readFileBytes (#521's digest-keyed import cache and acorn gating untouched) snapshotManifest lstat/read/lstat dev/ino identity (validate-artifact.ts:161–164); validate-artifact-modules.ts:67–114 lstat/realpath containment
build/pack-inventory.ts manifest / package.json / file digest reads exists lstat (83): dangling symlink counts as present
build/declaration-diagnostics.ts existsSync (93): synchronous probe beside createRequire's synchronous resolution

src/effect/platform.ts additions

  • readFileString(path) / readFileBytes(path): readFile(path, 'utf8') / readFile(path) as programs. Not fs.readFileString: it decodes through TextDecoder, which drops a leading UTF-8 BOM, while Node's utf8 keeps U+FEFF — the migrated sites JSON-parse and digest what they read, so bytes must decode as before (test pins it).
  • isPlatformErrno(error, ...codes): isErrno for a failure that may still be a PlatformError, for the ENOENT/ENOTDIR/ELOOP branches the try/catch predecessors had.
  • Every other module imports PlatformError as import type only (erased; the run-time import stays confined to platform.ts for the hook-wrapper bundle reason).

Layer wiring

Unchanged from phase 1: platformLayer is provided by runWithPlatform at each Promise edge. No module that cli.ts loads eagerly imports effect, @effect/platform-node-shared, or src/effect/platform.ts (cli.test.ts "answers --version, --help, and an argv error without loading the Effect terminal runtime" passes on this branch).

Tests

  • New tests/effect-filesystem-phase2.test.ts (10): BOM-exact readFileString/readFileBytes and ENOENT unwrapping (real dir); isPlatformErrno wrapped/bare/other; portable validator ELOOP at a stdio command → the same "does not resolve to a bundled regular file" diagnostic (real symlink loop); copyOpaqueCodexAuthStateProgram call order and mode with FileSystem.layerNoop, and the no-copy-on-missing-source error path; Codex trial home create/remove; fixture materialization modes + changed-source + existing-destination errors; forwardingSignals adds/removes the listeners with the scope and forwards to child.kill.
  • pnpm typecheck ✓, pnpm lint ✓ (0/0), unit suite ✓ 3264 passed / 0 failed.
  • public-api.test.ts: refactor(effect): yieldable framework error classes via Data.Error bases #543 (on main now) pins that no public export's declaration graph imports effect. The four Effect-typed exports this PR had added (portablePluginByteDiagnostics, readCursorPluginDocuments, validateClaudePluginFilesProgram, forwardingSignals) violated it, so a4f4dd4fd makes the three validator programs module-private and moves forwardingSignals to services/mcp-run-signals.ts (imported by mcp-run.ts for the implementation only, so mcp-run.d.ts no longer references it; the test imports the leaf module). "keeps every public declaration graph free of effect" and the emitted-artifact-effect-surface suite pass locally on the built package. The earlier consumer-fixture effect symlink was removed by refactor(effect): yieldable framework error classes via Data.Error bases #543 and is no longer needed.
  • Integration (prebuilt): mcp, hooks, target-mcp-runtime ✓ 59; cli ✓ 16 (incl. the cold-start module-load proof); artifact-validator, eval-* (claude-harness, cli, fixtures, harness, service), claude-plugin-validate-acceptance, build, api, prepack, package-build ✓ 288.
  • pnpm docs:site:build ✓ (TypeDoc, dead links, language parity).

Artifact byte-diff

examples/host-test (90 files) and examples/audiobook-curator (20 files) built at the same path from origin/main (fbc183b3d) and this head: byte-identical (diff -r). Note for anyone reproducing: across two different worktree paths only agent-bundle.manifest.jsonproject.modelDigest differs — it is path-keyed, unrelated to this PR.

Timings (this machine, idle, Node v22.23.2)

origin/main this PR
agent-bundle --version (3×40 runs, interleaved) median 64–68 ms, p90 69–80 ms median 65–68 ms, p90 69–82 ms
startDevServer on examples/host-test (5 runs ×2, includes the initial build) median 13.5–14.0 s median 10.6–13.4 s (noise; PR touches no dev module)

Docs

docs/effect-conventions.md: Adopt list (readFileString BOM rule, isPlatformErrno, the "mixed modules" rule), keep-raw list (workspace-diff, snapshotManifest, Dirent listings, existsSync), boundary-module paragraph (phase-2 callers, import type clarification), unstable-module table row. No user-facing behavior changed, so no website/ pages.

Changeset

.changeset/effect-filesystem-phase2-modules.mdagent-bundle patch.

Review status

9fdf809 (gh pr update-branch` over #543/#548/#549, the new protected-main procedure; squash-merged, so the merge commit does not land).

@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-04T07:08:41.470560Z 0408773 PR opened
ℹ️ 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.

@changeset-bot

changeset-bot Bot commented Sep 4, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: b90eea4

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

@pkg-pr-new

pkg-pr-new Bot commented Sep 4, 2026

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

commit: b90eea4

@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: 040877337a

ℹ️ 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 thread .changeset/effect-filesystem-phase2-modules.md Outdated
@ScriptedAlchemy
ScriptedAlchemy force-pushed the feat/effect-filesystem-phase2 branch from 5ec8de5 to 470a086 Compare September 4, 2026 07:59
… post-build readers through FileSystem (phase 2, PR 1)
…ture

The validators' and services' FileSystem programs now type their Effect
signatures, so the emitted declaration graph resolves `effect` — a runtime
dependency, like ajv/ignore/@rsbuild before it.
@ScriptedAlchemy
ScriptedAlchemy force-pushed the feat/effect-filesystem-phase2 branch from 470a086 to 90e4756 Compare September 4, 2026 08:24
@ScriptedAlchemy
ScriptedAlchemy enabled auto-merge (squash) September 4, 2026 08:24
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