Repository navigation
Effect Service Conventions: 1 issue found
1 convention violation found.
apps/server/src/cli/pair.ts(lines 86-111):PairTailscaleErrorencodes four semantically distinct failures (status-unavailable,magicdns-missing,serves-other-environment,serve-failed) in a singlereasonliteral whose only consumer is aswitchin themessagegetter that selects a different user-facing remediation string per case. The conventions require separate error classes when areason/kind/phasediscriminator drives the user-facing message; a discriminator may stay a field only when it is diagnostic-only or the message is generically derived from it. Splitting also letscausebe required for the two reasons that always wrap a real failure and letsservePortbe carried only where it is meaningful.
An inline review comment with the suggested split has been posted on the PR.
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 reviewed
apps/server/src/cli/pair.ts(new)apps/server/src/cli/pair.test.ts(new)apps/server/src/serverRuntimeState.ts,apps/server/src/serverRuntimeState.test.tsapps/server/src/bin.ts(command registration)
Checks that passed
- Imports: Effect modules are imported as namespaces from subpaths (
effect/Effect,effect/Layer,effect/Option,effect/Schema, ...); no consolidated named imports from"effect". Named imports from@t3tools/contracts,@t3tools/tailscale, andeffect/unstable/{cli,http}are pure helpers/schemas/flags and match existing CLI modules (auth.ts,config.ts). Local service modules are imported as namespaces (import * as EnvironmentAuth,import * as ServerConfig) and used through their module shape (EnvironmentAuth.EnvironmentAuth,ServerConfig.layer,ServerConfig.make). - Service interface references:
ServerConfig.ServerConfig["Service"]is used for the inferred interface, per convention. - Dependency acquisition / runtime boundaries: no
ManagedRuntime.make,runPromise, orrunPromiseExit. Layers are composed only inside the CLI command handler (Effect.provide(FetchHttpClient.layer),EnvironmentAuth.runtimeLayeroverServerConfig.layer), matching the establishedrunWithEnvironmentAuthpattern inapps/server/src/cli/auth.ts. The singleLayer.succeedprovides a pure reference value (References.MinimumLogLevel), not a runtime-backed implementation. No service instance is injected as a constructor parameter in production code;HttpClient.HttpClientandEnvironmentAuth.EnvironmentAuthare acquired from the environment. - Errors:
NoRunningServerErrorderives its message from the structuredcheckedStatePathsattribute; nodetailfield copiescause.message; no raw wire payloads, tokens, or command output are placed in error attributes.Schema.TaggedErrorClassis used throughout, consistent with the rest ofapps/server. - Error handling: no
catchTag/catchIf-with-schema-predicate misuse; no single-use error factory helpers. - File layout: new command lives in the canonical
apps/server/src/cli/location with no compatibility shims;serverRuntimeState.tschange is additive (devUrl) with an updated existing test. - Tests: the added tests cover new backend behavior (pairing URL selection, Tailscale local target, discovery of a live vs. stale server), not a mechanical refactor, and use a local HTTP server as a test boundary rather than mocking business logic.
🔍 Agent activity — 1 tool call
💭 Looking at the new pair command files and checking the import conventions used throughout the codebase — specifically how named imports from effect subpaths are structured and whether they follow the established patterns for consolidating imports. For the error classes, PairTailscaleError uses a reason discriminator with four distinct cases to select different user-facing messages, which the conventions flag as potentially problematic — a large switch statement in the message getter suggests these might warrant separate error classes instead of a single error with a discriminator. The optional
causefield is justified since some failure modes like magicdns-missing don't have an underlying cause. Dependency acquisition looks fine at the CLI boundary, and the pure helper functions exported for testing are appropriate. Now checking how the tests use these utilities... I'm checking the tailscale.ts file for error patterns and then looking at how those errors are handled in startupAccess.ts to see if there's an existing pattern I can follow with reason literals. Now I'm diving into the pair.ts file to understand its full structure, and I want to check the startupAccess.ts helpers and the tailscale package to see what patterns already exist—like whether ensureTailscaleServe returns structured errors or if buildTailscaleHttpsBaseUrl is already available. I'm checking for convention violations in the error handling design. ThePairTailscaleErrorclass is using a 4-valuereasondiscriminator with a switch statement in themessagegetter to produce different user-facing messages, which violates the convention that semantically distinct failures should be split into separate error classes rather than consolidated with a large switch statement. The distinction here is clear: each reason (tailscaled not running, MagicDNS missing, port conflict, serve command failed) has entirely different remediation paths and messages, making them genuinely separate concerns. This contrasts with the allowed pattern seen inRemoteWatchFailure, which useskindwith a generic derived message. I'll flag this as a violation. I'm also checking whether other CLI commands like auth.ts follow a similar pattern withEnvironmentAuth.runtimeLayerto understand if this is an established convention in the codebase. The auth.ts pattern mirrors pair.ts's mintPairingLink approach, so that's consistent with the codebase. The main finding worth investigating is PairTailscaleError's reason discriminator and how it shapes the error message. I should also check whether readTailscaleStatus and ensureTailscaleServe already return structured errors, and whether wrapping them in a generic error loses important context. I'll also look at startupAccess.ts to see if there's already a similar tailscale error class I should be aware of. I'm checking whether the CLI runtime in bin.ts provides the ChildProcessSpawner service that's required by readTailscaleStatus and ensureTailscaleServe, since pair.ts only supplies FetchHttpClient.layer to the handler. If ChildProcessSpawner isn't available, the type checking would fail unless it's being provided somewhere in the CLI setup. Now looking at whether the PairTailscaleError pattern violates the convention. The rule is clear: when a switch statement in the error's message getter selects different user-facing messages for semantically distinct failures, those should be separate error classes. Here we have exactly that — four different failure modes (daemon unreachable, MagicDNS missing, port conflict, serve command failed) with a switch selecting the message, plus optional fields like servePort and cause that aren't relevant to all cases. This really does warrant splitting into separate error classes rather than keeping them bundled. Since this is a new file in the PR's scope and the pattern conflicts with the repo's conventions, I should flag it as a finding and fail the check. Looking at similar error classes across the codebase —RemoteWatchFailure,CheckpointDiffResultInvalidError,BunPtyOperationUnavailableError— they all use discriminators with generic messages. Thepair.tsapproach of having distinct remediation messages per reason is the anti-pattern here, so I'll post the inline comment. I've verified there are no other issues: noEffect.catchTagorcatchIfpatterns, no privateSchema.ischecks, noManagedRuntimeusage. TheformatPairOutputfunction includes tokens in CLI output which is fine, andNoRunningServerError's multi-line message is acceptable since it's derived from attributes. I'll target thereasonfield declaration around lines 89-110 in the new file and suggest splitting into separate error classes instead.
1. modify_pr — One finding on the new t3 pair CLI module: PairTailscaleError models four semantically distinct failures behind a single reason discriminator whose only consumer is a switch in the message getter. Everything else in the diff (namespace imports, Effect.fn helpers, layer construction at the CLI boundary, Schema.TaggedErrorClass usage, devUrl persistence) follows the service conventions.
Agent Credits: 171 credits