Skip to content

fix(desktop): unpack the t3 Claude plugin so handoff works in package…

MacroscopeApp / Macroscope - Effect Service Conventions failed Aug 28, 2026 in 3m 27s

Effect Service Conventions: 3 issues found

Three error-modeling convention violations in the new handoff code (3 inline comments posted).

  • apps/server/src/cli/handoff.ts (L33-L35): HandoffCliError carries an unstructured message as its only attribute and absorbs seven semantically distinct failures (missing session env, empty stdin, empty --name, stdin read failure, transport failure, unreadable body, non-2xx status, undecodable response) with pre-formatted strings and no cause. Convention: one class per failure with structured attributes plus a derived override get message(), as in cli/pair.ts / cli/project.ts.
  • apps/server/src/cli/handoff.ts (L101-L107): the transport wrapper builds its message from String(cause) and drops the cause; the response.json and decode wrappers (L110-L116, L125-L131) drop their causes too. Messages must derive only from structural attributes, with the underlying failure preserved as cause.
  • apps/server/src/handoff/HandoffService.ts (L81-L90, L127-L133): the parent-shell read and the compensating thread.delete are both reported as HandoffDispatchError with stage: "create", so the discriminator that identifies the failure — and that is surfaced verbatim in the route's 500 message — is wrong for two of the wrapping sites. The known childThreadId is also not captured.

Details

Note

Your check run agent prompt is: .macroscope/check-run-agents/effect-service-conventions.md
More information about how Check Run Agents work can be found in our Help Center.

Scope examined

  • apps/server/src/handoff/protocol.ts, HandoffService.ts, HandoffHttpServer.ts, callbackEnvironment.ts and their tests
  • apps/server/src/cli/handoff.ts, apps/server/src/bin.ts, apps/server/src/cli/connect.ts
  • apps/server/src/provider/Drivers/ClaudePlugin.ts, ClaudeSkills.ts, apps/server/src/provider/Layers/ClaudeAdapter.ts, ClaudeProvider.ts
  • apps/server/src/mcp/McpSessionRegistry.ts, apps/server/src/server.ts, apps/server/scripts/cli.ts

Checked and found compliant

  • Dependency acquisition: performHandoff obtains OrchestrationEngineService, ProjectionSnapshotQuery, and Crypto.Crypto from the environment; resolveT3ClaudePluginLocation declares FileSystem.FileSystem | Path.Path requirements. No service instances are passed as constructor parameters in production paths — the Layer.succeed/instance passing in HandoffService.test.ts and ClaudePlugin.test.ts are legitimate test layers.
  • Runtime boundaries: no ManagedRuntime.make, runPromise, or runPromiseExit introduced. Effect.provide(FetchHttpClient.layer) sits at the CLI command handler, matching the existing boundary in cli/pair.ts.
  • Imports: subpath namespace imports for effect/Effect, effect/Schema, etc.; the named imports from effect/unstable/http and effect/unstable/cli match established usage across apps/server/src and are not consolidated "effect" imports.
  • New options are configuration, not injection: discoverClaudeSkills(..., { t3PluginDir }) and makeHandoffCallbackEnvironment(session, options) pass plain values.
  • Error handling control flow: HandoffHttpServer uses Effect.catchTags({ ... }) for its known tagged failures rather than catchTag/catchIf.
  • Data.TaggedError for InvalidHandoffRequestError: not flagged — it mirrors the existing route-local pattern in apps/server/src/http.ts (DecodeOtlpTraceRecordsError) and carries a real cause.
  • McpSessionRegistry.resolveActiveMcpToken: uses the module's pre-existing live-registry handle pattern (issueActiveMcpCredential, touchActiveMcpThread, revokeAllActiveMcpCredentials); changing it would be a refactor of existing infrastructure rather than a violation introduced here.
  • Tests: new behavior (handoff service semantics, plugin resolution, callback env) is covered by focused tests without mocking core logic.

🔍 Agent activity — 14 tool calls

💭 I'm reviewing the diff across the handoff-related files for adherence to Effect service conventions, looking at HandoffService, HandoffHttpServer, callbackEnvironment, protocol, ClaudePlugin, and McpSessionRegistry. I'm noting that McpSessionRegistry's resolveActiveMcpToken relies on a module-level global registry, which is a hidden dependency but appears to be an existing pattern already used in that file. I'm also flagging that HandoffHttpServer defines InvalidHandoffRequestError using Data.TaggedError instead of Schema.TaggedErrorClass, which conflicts with the convention for structured service failures, and I'm checking the handoff CLI's error handling for similar issues. Looking closer, HandoffCliError only stores an unstructured message string rather than deriving it from structured attributes, and several mapError wrappers build messages directly from stringified causes—both clear violations of the convention that messages must come from stable structural fields, not from cause or a stringified defect, and doing so also loses the original cause. I'm also noting the CLI handler provides FetchHttpClient.layer inline (likely acceptable at the CLI boundary) and that HandoffDispatchError's stage field seems mislabeled in deleteChildThread, where a delete compensation uses the "create" stage tag. I should verify against similar files like HandoffHttpServer.ts, which imports HttpRouter and related modules the same way, suggesting this subpath style is already established repo convention. Looking at ClaudePlugin.ts, resolveT3ClaudePluginLocation uses FileSystem and Path as environment dependencies, and ClaudeAdapter.ts correctly provides these as local service instances via Effect.provideService — that's a legitimate pattern, not a violation. Now I'm checking how Effect.ignoreCause is being used with logging enabled. I also notice the frontend uiStateStore.ts isn't using Effect at all, and HandoffCliError has another spot where Effect.tryPromise's catch handler discards the underlying cause entirely — that's another violation of the convention to preserve the original error when wrapping. So my main findings converge on two issues: HandoffCliError uses a flat, unstructured message field with no cause preservation, and InvalidHandoffRequestError relies on Data.TaggedError instead of Schema.TaggedErrorClass with only an opaque cause field lacking real structural context. Now I'm weighing whether the tag itself might already be precise enough to justify keeping just a cause field without extra structure, per the convention that allows this when no additional variable context exists — so the real question is whether Data.TaggedError versus Schema.TaggedErrorClass is actually a flaggable convention violation, which means I should check how prevalent each pattern is elsewhere in the repo and look for documented guidance in AGENTS.md before deciding. I'm also noting other smaller things worth checking: HandoffHttpServer's unauthorized response being a module-level constant, and McpSessionRegistry's resolveActiveMcpToken returning a possibly-undefined effect rather than using Option, which could be a style inconsistency worth flagging too.

1. browse_code — **/AGENTS.md

2. git_tools — Data.TaggedError

💭 I notice Data.TaggedError is already used elsewhere in the repo, like http.ts's route decode error, so this local InvalidHandoffRequestError being styled that way is more of a legacy consistency choice than a real violation. Since the instructions favor Schema.TaggedErrorClass but this is just a non-exported route error with a structured cause attribute, I'll flag it only weakly rather than as a hard issue. The check apparently defaults to failure unless things are clean, so I need to weigh whether these findings are worth surfacing. HandoffCliError stands out as the strongest case—it only carries an unstructured message string, which clearly violates the convention of deriving messages from structured attributes. But since this is a CLI-facing error whose message gets printed to users, I want to check how other CLI files like connect.ts define their errors before deciding if this is a legitimate exception to the rule.

3. browse_code — apps/server/src/cli/*.ts

4. git_tools — TaggedErrorClass|TaggedError

💭 I should check pair.ts and project.ts to see how they structure their error classes, particularly the attributes and message getter pattern.

5. browse_code — apps/server/src/cli/pair.ts

6. browse_code — apps/server/src/cli/project.ts

💭 I'm confirming that the new HandoffCliError violates repo conventions—it uses a single unstructured message field baked at six different construction sites instead of deriving the message from structured attributes, and some of those sites even smuggle in String(cause) directly. That's a solid first finding. My second finding concerns InvalidHandoffRequestError using Data.TaggedError where I'd expect Schema.TaggedErrorClass, though I want to check the http.ts precedent before being fully confident there. Meanwhile HandoffDispatchError's stage discriminator seems fine in principle, but I'm noticing deleteChildThread mislabels its stage as "create" for what's actually a delete dispatch, and the parent-lookup failure gets the same mislabel—which breaks the convention of capturing stable, accurate domain context. For finding #4, this reads module state directly rather than going through the Effect environment. But since McpSessionRegistry is built around a module-level active registry as its core design, this looks like an existing pattern rather than something new introduced by the PR, so demanding a refactor here would mean asking for unrelated architectural cleanup. I want to check McpSessionRegistry.ts directly to confirm before deciding whether to flag it.

7. browse_code — apps/server/src/mcp/McpSessionRegistry.ts

8. browse_code — apps/server/src/http.ts

💭 I'm treating the module-global registry pattern as acceptable since it follows existing infrastructure conventions, and pushing for a router-context refactor would be unrelated cleanup outside this PR's scope. Now I'm moving on to check the rename in connect.ts. Actually, CLOUD_CLI_LIVE_SERVER_TIMEOUT got garbled into CLOUT3_CLI_LIVE_SERVER_TIMEOUT — looks like an accidental find/replace error rather than an intentional convention change. It's tempting to flag since it's a real defect, but it's not an Effect Service Convention violation, so I'll stay disciplined and leave it out of this review. Now I'm scanning other parts of the changed code for convention issues — checking whether the CLI imports from effect/unstable/cli and effect/unstable/http match the repo's established import patterns, and whether providing FetchHttpClient.layer inside the handler improperly hides an HttpClient dependency that should be surfaced at a higher layer.

9. git_tools — FetchHttpClient

10. git_tools — from "effect/unstable/http"

💭 The imports and Effect.provide usage match repo conventions from pair.ts. I'm checking HandoffService exports—performHandoff as a standalone Effect.fn looks right, and the error types HandoffDispatchError and HandoffParentNotFoundError have appropriate fields, though the latter seems to be missing a message. Missing a message getter isn't necessarily a violation here since there's no unstructured message field to derive from—that convention is satisfied. I'll skip flagging this as marginal. Now I want to pull up the actual CLI file to get precise line numbers for inline comments, since for a new file the diff comments need to reference the right-side line numbers.

11. browse_code — apps/server/src/cli/handoff.ts

💭 I'm confirming two violations in the CLI file: one where a single generic message field is reused across seven distinct failure cases instead of deriving structured messages, and another where the error message stringifies the cause directly and drops it instead of preserving it as a proper cause field. Also noting the origin hostname interpolation is fine per convention, but flagging that readErrorMessage(body) copies raw server response text into the error message — borderline since it's the trusted server's own message, so I'll leave it as secondary rather than a primary fix. Moving on to check line 48 where the catch block appears to drop the cause entirely.

12. browse_code — apps/server/src/handoff/HandoffService.ts

💭 I also notice HandoffDispatchError is missing a childThreadId field, even though the failure is really about the child thread — the wrapping site should retain that entity context. Right now the stage discriminator only carries parentThreadId, so I should flag adding childThreadId along with the stage mislabeling, since "create" is being used incorrectly for a parent-shell read and for the delete compensation step. Beyond the caller-facing message being wrong, using stage: "create" for the parent lookup is misleading since that's not a dispatch at all — it might warrant its own error or a distinct stage label so the field actually identifies the failure structurally. I'll leave two inline comments, one flagging the mislabeled parent-lookup stage and one flagging the delete-compensation being reported as "create," and also note that neither new error class exposes a message getter the way the rest of the repo's error classes typically do. Now I'm checking the ClaudeAdapter and ClaudeProvider changes for dependency issues, specifically noticing that resolving the plugin location re-provides Path and FileSystem services that were already pulled from the environment in the same scope. It's redundant rather than a real DI violation, but worth flagging as unnecessary boilerplate. I also confirm HostProcessExecutablePath is properly acquired from the environment, and since it's pre-existing code just newly imported, I don't need to flag its internal implementation even though it might touch imperative APIs.

13. git_tools — HostProcessExecutablePath

💭 I'm looking at ClaudePlugin.ts, noting how it acquires FileSystem and Path from the environment using Effect.fn, and how it falls back gracefully with orElseSucceed rather than throwing, returning undefined deliberately when the plugin can't be used—keeping the failure typed in Effect instead of relying on exceptions. Checking ClaudeSkills.ts, the added optional t3PluginDir field is a straightforward config injection with no issues. Reviewing HandoffHttpServer, I see precedent for Data.TaggedError usage, catchTags handling errors cleanly, and decode failures mapped into InvalidHandoffRequestError consistent with prior patterns—including request.json failures folded into the same flow, which is fine. I also verify HandoffService pulls Crypto from the environment correctly, and that the build script in apps/server/scripts/cli.ts uses Effect's filesystem copy and logging appropriately. Now I'm weighing whether any of this constitutes a real convention violation worth failing the check for, versus just stylistic preference, and starting to consider the HandoffCliError case specifically. This is a genuine breach of an explicit rule—messages should be derived from structured attributes, not from stringified error causes—so I'm marking this a failure. I need to map the exact diff line numbers for the new file, since for new files the right side numbering equals the file's own line numbers. The first comment targets handoff.ts lines 33-35: HandoffCliError bundles seven unrelated failure modes (missing env var, empty stdin, empty name, transport failure, unreadable body, non-2xx response, undecodable response) into one class with only a loose message string and no cause field, which breaks from the established pattern in cli/pair.ts and cli/project.ts of structured attributes with an overridden message getter. I'll suggest splitting these into separate error classes, but since this spans multiple call sites I can't offer a single diff suggestion—maybe a brief illustrative snippet instead, kept short. The second comment covers lines 101-107, where the message is built by stringifying the cause and then discarding it entirely—I want to suggest preserving the cause as a proper field. Now I'm working through how to fold in related issues efficiently: the stdin read on line 48 also drops its cause, so I'll roll that into the first comment rather than create a new one. I'm deciding on a final structure of three comments total—two on handoff.ts covering the error class shape and the stringified-cause issue (referencing lines 48, 74, 83, 89, 104, and 119), and one on HandoffService.ts covering the mislabeled stage around lines 81-90 while also noting the similar problem at line 128, rather than splitting that into a fourth separate comment. Adding proper stage handling there would require touching the literals and dispatch signature across multiple spots, so an inline diff won't work cleanly—I'll just describe the fix in the comment text instead. Now I'm finalizing the exact line ranges for each comment before writing them out.

14. modify_pr — Reviewed the new Effect code in this PR (apps/server/src/handoff/*, apps/server/src/cli/handoff.ts, apps/server/src/provider/Drivers/ClaudePlugin.ts, and the Claude adapter/provider wiring) against the Effect service conventions. Dependency acquisition looks right: performHandoff and resolveT3ClaudePluginLocation take their dependencies from the environment (OrchestrationEngineService, ProjectionSnapshotQuery, Crypto, FileSystem, Path), the new t3PluginDir option is pure configuration, Effect.catchTags is used for the route's known failures, and the CLI's Effect.provide(FetchHttpClient.layer) at the command handler matches the existing cli/pair.ts boundary. Findings are limited to error modeling: the new HandoffCliError is a single class whose only data is a pre-formatted message (one site interpolating a stringified cause), and HandoffDispatchError's stage is set to "create" for two failures that are neither the create dispatch nor a dispatch at all.


Agent Credits: 125 credits