Skip to content

refactor(install): run the local Cursor installer as an Effect program with a typed DiagnosticError channel - #522

Merged
ScriptedAlchemy merged 3 commits into
mainfrom
refactor/effect-install-program
Sep 4, 2026
Merged

refactor(install): run the local Cursor installer as an Effect program with a typed DiagnosticError channel#522
ScriptedAlchemy merged 3 commits into
mainfrom
refactor/effect-install-program

Conversation

@ScriptedAlchemy

@ScriptedAlchemy ScriptedAlchemy commented Sep 4, 2026

Copy link
Copy Markdown
Owner

Summary

Effect-conformance cleanup 3 of the prior audit (install/install.ts), rebased over #452 (receipt-owned lifecycle) which rewrote the same region and merged first. The local Cursor installer is now an Effect program that lifts only its leaf I/O and fails on a typed DiagnosticError channel; the staging-parent cleanup is an explicit exit sequence instead of try/finally. The public-CLI installers stay lifted as units on purpose (see below).

What changed (packages/agent-bundle/src/install/install.ts)

  • installCursor (audit rows :661–666, 700–704 → post-feat(install): receipt-owned uninstall lifecycle, format/2 receipts, and doctor activation states (#101) #452 :896–902, :957–962) is Effect.fnUntraced(function* (options, identity, scope): Effect.fn.Return<InstallResult, DiagnosticError>). Only the leaf I/O is lifted — resolveCursorRoot, treeInventory, exists, mkdir, stageArtifact, rename/replaceInstalledTree, readInstalledManifest, compareInstalledTree, isRuntimeStateRemnant, writeInstallReceipt — the ownership/collision decision logic stays synchronous in the fiber, and throw failure(…) became return yield* Effect.fail(failure(…)). The former try { … } catch (error) { DiagnosticError passthrough | AB7004 } is Effect.mapError(installFailure('cursor')) over the whole program, so the channel type states the contract.
  • withStagedArtifact(stage, apply) replaces both try { apply } finally { rm(staged.parent) } blocks: Effect.exit(liftPromise(apply)), then the removal, then unwrap — a failed removal replaces the apply outcome exactly as the finally did. It is deliberately not a scope finalizer (docs/effect-conventions.md Stage 3 "Hurt": propagating teardown is an explicit sequence). Left for the FileSystem lane: turning stageArtifact's mkdtemp parent into makeTempDirectoryScoped. Note for that lane: a scoped temp directory's finalizer is infallible, so it would swallow the removal failure this code currently propagates — the lane should keep the removal explicit or document the contract change.
  • installProgram (audit row :722–750, Effect.fn.Return<…, unknown>): the Cursor branch composes installCursor directly; readIdentity and installPublicCli stay liftPromised as units, so the program's channel is written as DiagnosticError | LiftedRejection. That union is the actual contract, not an oversight: tests/install.test.ts ("delegates claude/codex installation to its public CLI without a shell") pins that a raw leaf failure in the public-CLI path (an unwritable receipt store) re-raises verbatim after the host-verb rollback and is not a DiagnosticError — the CLI entry maps it to AB7004 itself. Mapping it in the program would have changed that pinned behavior, so the public-CLI installers are out of scope for this PR (a separate decision for the maintainer: adopt the Cursor mapping for all hosts, or keep the raw passthrough).
  • installCursorMarketplace keeps its own try/catch mapping; the Cursor program lifts it as a unit (mapError(installFailure) is idempotent over a DiagnosticError).

Observable change (patch changeset)

One rare path: resolveCursorRoot used to run outside installCursor's try, so a Cursor home that exists but cannot be inspected (non-ENOENT lstat, e.g. an unreadable ~/.cursor) rejected with a bare Error; it now reports AB7004 with target: 'cursor' like every other Cursor install failure (the CLI printed AB7004: <message> either way; JSON output gains target). AB7002/AB7003/AB7005, successful installs, and the Claude/Codex paths are unchanged. Docs: the local-Cursor paragraph of docs/diagnostics.md (the single source for the generated reference/diagnostics page in both locales) now states the contract and the staging-directory cleanup; pnpm docs:site:build green.

Idioms and citations

Idiom Doc repos/effect/LLMS.md / repo section
Effect.fnUntraced generator for a reusable library program; return yield* on error Code Style → Guidelines LLMS.md § Using Effect.fn and Effect.fnUntraced; docs/effect-conventions.md § Generator style
Typed failure channel (DiagnosticError) instead of unknown; Effect.mapError at the seam Error Management → Expected Errors, Two Types of Errors agent-patterns/effect-errors.md "What to avoid: unknown in the fail channel"; docs/effect-conventions.md § Error mapping (DiagnosticError rethrows unchanged at the boundary)
Capture the attempt's Exit, run cleanup, unwrap (propagating teardown) Two Types of Errors (Effect.exit at boundaries) docs/effect-conventions.md Stage 3 "Hurt / gotchas"
Leaf I/O lifted through lift.ts, orchestration in Effect docs/effect-conventions.md § Stage 3 (dev seam) outcomes

Tests

  • New in tests/install.test.ts: reports a Cursor home it cannot inspect as AB7004 for the cursor host (unreadable home → AB7004, EACCES in the message, target: 'cursor'; skipped as root); removes the staging parent after a failed replacement and re-raises the refusal as AB7004 (a rebuilt artifact colliding with an unowned file is staged, refused by replaceInstalledTree, the staging parent is removed, the installed copy is untouched — exercises withStagedArtifact's exit sequence).
  • Existing install/uninstall/doctor suites unchanged and green (126 tests in the targeted run).
  • pnpm typecheck, pnpm lint green; pnpm test:unit green (3222 passed); integration installer-entry + host-install-session + dev-host-install green (12 passed).

Review status

No PR comments are posted by the author; every review thread is answered in this section.

Head Review Threads
3e60824 Codex completed (1 P1, 1 P2) install.ts:908 "Document the new Cursor AB7004 contract in both locales" → fixed in the docs commit: docs/diagnostics.md is the source both locale pages are generated from at build time (website/plugins/generated-reference.ts), so the paragraph was added there; pnpm docs:site:build green with language parity. install.test.ts:846 "Skip the unreadable-home test on Windows" → fixed: the guard is now process.platform === 'win32' || process.getuid?.() === 0. Changeset present on 4604f1f failed on a registry 504 downloading pnpm (infrastructure), re-run.
ec14d06 not re-reviewed CI: Verify (Node 24) failed in pnpm typecheck on tests/framework-plugin-registration.test.ts — main was broken by #518 and fixed by #523; this PR touches neither file. Rebased onto 5a86149 (#523) as 66f962a with no content change.
66f962a pending

@changeset-bot

changeset-bot Bot commented Sep 4, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 66f962a

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:41:24.569959Z 3e60824 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.

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: 3e60824129

ℹ️ 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 +906 to +908
const cursorRoot = yield* liftPromise(() => resolveCursorRoot(options)).pipe(
Effect.mapError(installFailure('cursor')),
);

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 Document the new Cursor AB7004 contract in both locales

When inspecting ~/.cursor fails for a reason other than ENOENT, this mapping changes the public CLI/JSON result from a raw error to AB7004 with target: "cursor". Neither website/docs/en/** nor website/docs/zh/** documents the changed failure contract; update the matching installation documentation or its generated source so both locales reflect it.

AGENTS.md reference: AGENTS.md:L71-L77

Useful? React with 👍 / 👎.

});

it('reports a Cursor home it cannot inspect as AB7004 for the cursor host, like every other Cursor install failure', async () => {
if (process.getuid?.() === 0) return; // root ignores directory modes; the lstat cannot be made to fail here.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Skip the unreadable-home test on Windows

When the unit suite runs on Windows, process.getuid is undefined, so this guard does not skip, while chmod(home, 0o000) does not make the directory untraversable as it does on POSIX. The install can consequently succeed and fail the subsequent DiagnosticError assertion; include a process.platform === 'win32' check or inject a failing lstat instead.

Useful? React with 👍 / 👎.

@pkg-pr-new

pkg-pr-new Bot commented Sep 4, 2026

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

commit: 66f962a

ScriptedAlchemy added a commit that referenced this pull request Sep 4, 2026
…m with a typed DiagnosticError channel

installCursor is an Effect.fnUntraced generator that lifts only its leaf
I/O and fails on DiagnosticError; the staging-parent cleanup is an explicit
exit sequence (capture the apply Exit, remove the stage, unwrap) instead of
try/finally. readIdentity and the public-CLI installers stay lifted as
units because their raw-failure passthrough is a pinned contract.
@ScriptedAlchemy
ScriptedAlchemy force-pushed the refactor/effect-install-program branch from ec14d06 to 66f962a Compare September 4, 2026 05:23
@ScriptedAlchemy
ScriptedAlchemy merged commit 8045094 into main Sep 4, 2026
13 checks passed
@ScriptedAlchemy
ScriptedAlchemy deleted the refactor/effect-install-program 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