Skip to content

fix(server): preserve remote update failures

MacroscopeApp / Macroscope - Effect Service Conventions failed Aug 1, 2026 in 5m 58s

Effect Service Conventions: 3 issues found

Three findings, all in the rewritten tests for the new service-launcher update path:

  • apps/server/src/cloud/bootService.test.ts — the service module is now imported with a bare make named import instead of the previous namespace import, erasing the module namespace at a service boundary (same in selfUpdate.test.ts and serviceLauncherClient.test.ts).
  • apps/server/src/cloud/bootService.test.ts — install gained a new multi-artifact rollback and status.current gained new conditions, but coverage collapsed to one happy-path test; the prior tests for failed activation cleanup, uninstall, the non-Linux unsupported path, and reconcileService idempotency were deleted although that behavior remains.
  • apps/server/src/cloud/selfUpdate.test.ts — retained update branches (non-exact version, desktop-managed refusal, concurrent-update guard, verbatim blocked-preflight reason) lost all coverage.

The production service modules follow the conventions: serverShutdown.ts and serviceLauncherClient.ts use the canonical tag/make/layer shape, dependencies are acquired from the Effect environment, and the earlier findings (module-global shutdown channel, free-form launcher error message, duplicated ServiceLauncherClient construction, catchIf-style recovery, named service import in selfUpdate.ts) are resolved.

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.

Files examined beyond the diff: apps/server/src/cloud/selfUpdate.ts, cloud/serviceLauncherClient.ts, cloud/bootService.ts, cloud/pinnedRuntime.ts, serverShutdown.ts, serverRuntimeStartup.ts, serverActivation.ts, server.ts, persistence/Layers/Sqlite.ts, persistence/Migrations.ts, environment/ServerEnvironment.ts, atomicWrite.ts, and the new test files.

Items considered and not flagged:

  • bootService.ts writeDurably uses node:fs/promises/node:crypto inside Effect.tryPromise rather than the acquired FileSystem/Path services. The file carries an explicit nodeBuiltinImport:off acknowledgement and the fsync-on-file-and-directory durability requirement is not expressible through the FileSystem service or the existing writeFileStringAtomically helper.
  • resolveServerSelfUpdateCapability derives managed from the ServiceLauncherHostProcess/HostProcessEnvironment references rather than from ServiceLauncherClient.managed. ServerEnvironment.layer is also composed in cli/connect.ts, where the client is not provided, so the reference-based helper is a deliberate seam and the two derivations cannot disagree inside the server process.
  • PinnedRuntimePreflightBlockedError carries a child-process-supplied reason alongside version; that reason crosses a process boundary and is the caller-visible migration-boundary message, so it is legitimate structured data rather than an unstructured message field.
  • ServiceLauncherClientError uses a multi-value operation discriminator with messages derived from it, which matches the allowed single service-level error model.

🔍 Agent activity — 1 tool call

💭 I'm checking the current PR head against prior violations to see what's been fixed and what new issues exist. Looking at the key files, I notice that a previous run flagged selfUpdate.ts for discarding the cause in a failWith call, and pinnedRuntime.ts for not using catchTags — though the diff shows catchTags is now being used with Effect.catchTags, so that appears to have been addressed. The namespace import of serviceLauncherClient in selfUpdate.ts also seems to have been fixed. Now I need to examine the current state of selfUpdate.ts at the latest commit to verify whether the failWith issue has been resolved. Looking at how failWith constructs the ServerSelfUpdateError in selfUpdate.ts, I see it's passing the reason from PinnedRuntimePreflightBlockedError.reason — which is a structured, user-facing message attribute — along with the original error as the cause. This was flagged before, but the prior comment indicated it was addressed by preserving the cause. The reason text being passed through from the cause's field is intentional pass-through of a user-facing message, so I shouldn't re-flag it. At line 186, there's another case where the error detail is being selected from the cause based on whether the operation was rejected — this is a launcher-supplied rejection reason surfaced to the user, and it's being conditionally selected based on the error's discriminator. Looking at the other flagged items: ServiceLauncherClientError now uses an operation discriminator with derived messages instead of free-form text, which addresses the prior concern. The ServiceLauncherTrial context is now properly provided through the layer system in server.ts rather than re-reading environment variables. The double-provisioning of ServiceLauncherClient.layer has also been fixed — it's now created once and provided to both the routes and runtime services. I should check the current server.ts and how ServerShutdown is being handled. Now looking at what's new: ServerRequestedExit in the CLI is a control-flow error for exit codes rather than a service failure, which is a bit of a gray area — it's not following the Schema.TaggedErrorClass convention for service errors, but it's also not really a service concern. The serverShutdown.ts file follows the proper pattern with Context.Service, make, and layer in the right order. serverActivation.ts is just a pure helper, so that's fine. But pinnedRuntime.ts has PinnedRuntimePreflightBlockedError that stores an unstructured reason message as its main data, which goes against the convention of using structured attributes and deriving the message from those — though in this case the reason is a user-facing message from the preflight subprocess that crosses a process boundary, so it's a judgment call whether to restructure it. Looking at the rest of the changes, this was actually my own prior suggestion to create a distinct tagged error for the blocked-preflight case carrying the version and child-provided reason, so I shouldn't flag it. Now scanning the other modified files for new violations — serverRuntimeStartup.ts has make taking options and layerWithOptions, which is fine, and server.ts calls ServiceLauncherClient.make() inside Layer.unwrap(Effect.gen(...)), also fine. Checking import ordering and namespace imports next. The imports look mostly good — serverRuntimeStartup.ts uses named imports for pure helpers like forkParked and namespace imports for service modules like ServiceLauncherClient and ServerShutdown. But I'm noticing Sqlite.ts imports ServiceLauncherTrial as a named import from the service module rather than using a namespace import, which breaks the convention that service boundaries should use namespace imports. Looking at the existing code in that file, ServerConfig is already being named-imported from the config module, so there's a precedent for named imports there. But ServiceLauncherTrial is a Context.Reference that acts like a service dependency, not a config value, so it should probably follow the namespace import pattern instead. The prior feedback suggested accessing the flag through the ServiceLauncherClient service directly, but they went with a Context.Reference instead. That's actually fine since the server provides it as a derived value from the single client instance, avoiding duplicated state. In other contexts where the reference isn't provided, it defaults to false, which is acceptable. Now I'm checking bootService.ts for any new issues with its imports... I should check the atomicWrite.ts helper to see if it already handles the durability concerns that writeDurably is trying to address. I also notice readState in bootService uses raw JSON parsing without the schema validation diagnostic suppression that's used elsewhere in the codebase, and I need to verify whether the error types conform to conventions. Additionally, servicePreflight.ts appears to be synchronous code using node:sqlite directly. The CLI command handler in servicePreflightCommand calls Console.log(JSON.stringify(runServicePreflight(...))) — the synchronous I/O happens during Effect handler invocation rather than being wrapped in Effect.sync, but since the handler function is called per invocation, the timing should be acceptable. Now I need to look at the client packages. In client-runtime/src/state/server.ts, the ServerUpdateTerminalError type now includes targetVersion, status, and reason fields, and validateServerUpdateReadyEvent can fail with a reason sourced from the launcher's outcome. The strongest issue I'm seeing is in bootService.ts where writeDurably uses node:fs/promises with Effect.tryPromise inside a service that already has FileSystem available, plus it depends on the global process.pid — I need to determine if this is actually problematic. Looking at the broader context, writeDurably is wrapped in Effect so it's not a raw runtime boundary violation, and the repo already has precedent with serviceLauncher.ts using node built-ins directly. That said, there's a clear bug where the directory path is computed with filePath.slice(0, Math.max(filePath.lastIndexOf("/"), 1)) instead of using the Path service, which will break on Windows. Now I need to focus on the actual "Effect Service Conventions" violations. Let me systematically check the diff: BootServiceInstallError needs its definition verified, Effect.catch in pinnedRuntime.ts around fs.rename is allowed since it handles the full error channel, Effect.callback with error unions in serviceLauncherClient is fine, and I see Effect.catchTags being used correctly. I should look at ServiceLauncherClientError next. The operation field has seven values and drives message derivation and control flow decisions. The convention creates a tension here: it suggests splitting semantically distinct failures into separate error classes when a discriminator like operation is used to pick user-facing messages, but it also allows a single service-level error with a multi-value discriminator when failures share the same semantics. The selfUpdate code checks error.operation === "rejected" to select different user-facing text, which is exactly the pattern the convention flags as potentially needing separate error classes. Now I'm moving through the remaining files—checking serverRuntimeStartup.ts in full, a region of server.ts, and cli/server.ts. In the CLI server file, I notice the shutdown layer is built using Layer.succeed after yield* ServerShutdown.make rather than calling ServerShutdown.layer directly, but that's fine since the code needs the instance for raceFirst. I also see ServerRequestedExit extends Error as a non-Schema error class, which raises the question of whether it's truly a service failure or something else. Looking at it more closely, ServerRequestedExit is really a control-flow signal carrying an exit code with Runtime symbols, and the conventions suggest service failures should use Schema.TaggedErrorClass. However, since this is at an application boundary and it legitimately needs to work with Runtime.errorExitCode which requires an Error subclass, I'll let it pass. Now I need to examine server.ts more carefully to check for layer composition issues and whether ServiceLauncherClient.layer is being double-provided. I'm noticing that BootServiceInstallError is a pre-existing generic error that only wraps Schema.Defect() without structural context, but the PR is now using it across many more places like writeDurably and launcher reads. This violates the convention that Schema.Defect() shouldn't be a substitute for more specific error handling. The new writeDurably helper is particularly problematic — it wraps all writes into BootServiceInstallError with just a cause, discarding the file path and step context that's actually known. This is a genuine new violation: the error should include a field identifying which artifact (launcher, state, or unit) failed, but the existing class shape doesn't support that without modification. I'm being cautious about flagging marginal issues since the check defaults to failure anyway. Let me look for stronger candidates — the new MigrationManifestMismatchError in Migrations.ts is well-structured with expectedCount and actualCount fields. I should also examine servicePreflight.ts more closely and check how reconcileService uses BootService in the CLI code. Let me search the diff for any consolidated imports directly from "effect" rather than subpaths. Looking at serviceLauncher.ts, it's a standalone bundle that imports Node built-ins and exports classes—not an Effect service, so that's fine. The main concern I'm seeing is that ServiceLauncherHostProcess has a Context.Reference with a default value that closes over the global process object and evaluates connected: process.connected && process.send !== undefined only once at initialization. In Effect v4, the default value is computed lazily and memoized per-runtime, so this is actually acceptable as a host-process seam—it's the same pattern as HostProcessExecutablePath from shared/hostProcess and aligns with the recommended fix from the previous run. The other candidate is in selfUpdate.ts where failWith derives its reason from error.reason or error.detail. The convention is to derive the wrapper's message exclusively from its own stable structural attributes, never from the cause. Here, ServerSelfUpdateError.reason is the wrapper's own field, but its value is copied directly from the cause's field—this was already flagged in the prior run. The code has since changed in response: it now passes error.reason (the blocked reason) and error.detail (the launcher rejection reason), both of which are intentional user-facing pass-throughs with the cause preserved, so I consider this addressed. The third candidate is in bootService.ts where writeDurably bypasses the FileSystem service entirely, using node:fs/promises and process.pid directly within a service that already has FileSystem and Path acquired. It also uses string slicing instead of the Path service. There's an existing writeFileStringAtomically helper in ../atomicWrite.ts that selfUpdate.ts stopped using, so I should check whether that helper is Effect/FileSystem-based and whether it does fsync—if so, the preference would be to use the existing helper and the acquired services rather than raw node built-ins and the global process object. I'm noticing that writeDurably is a helper function that directly uses Node's filesystem and crypto APIs at the module level, which violates the principle of not hiding dependencies in module globals or in Layer.succeed implementations that call runtime-backed APIs. The function is wrapping this with Effect.tryPromise, but the underlying issue is that it's bypassing the FileSystem and Path services that should be acquired through the dependency injection system. However, I see the file has a lint directive disabling the node builtin import diagnostic, which suggests this was an intentional decision. The real constraint here is that the durability requirement (fsync) can't actually be satisfied by Effect's FileSystem abstraction, and the service is tested with real filesystem operations anyway, so flagging this would contradict the guidance about not demanding unrelated cleanup. The error handling could be more specific about which file or step failed, but that's a minor issue. Now I need to check the remaining areas I haven't looked at yet: the service formatting code, the server environment setup, and the SQLite implementation. I'm checking whether ServiceLauncherTrial is properly wired in production—it's acquired from the environment and provided to runtimeServicesLive through launcherLayer, but I need to trace where layerConfig (the Sqlite layer) gets composed into the dependency graph, likely as part of RuntimeCoreDependenciesLive. Looking at the construction of ServiceLauncherClient, there's only one instance being created through ServerSelfUpdate.layer, which requires ProcessRunner.layer and gets ServiceLauncherClient from the environment—no duplication there. For ServerShutdown, the CLI provides it directly in cli/server.ts while tests use ServerShutdown.layer, so that's consistent. Now examining the orchestration reactor's start method, which accepts an optional Effect parameter, and checking whether ProviderCommandReactor.start and similar methods have any issues with how they're using these dependencies. I'm also noticing the failWith helper in selfUpdate.ts—it's a function that just wraps error construction, which looks like it might violate the guideline about not adding helpers whose only purpose is to construct errors with arguments. Need to verify whether this was pre-existing or newly added in the diff. Looking at the new service modules introduced, I see serverShutdown.ts and cloud/serviceLauncherClient.ts both follow the expected pattern with Context.Service, make, and layer definitions. ServiceLauncherClient.make accepts optional configuration for the current version, and while there's some interleaving of helper functions and references before the tag definition, that's a reasonable organizational choice. Now I'm noticing that isServiceLauncherManaged is being used in resolveServerSelfUpdateCapability within selfUpdate.ts, but this duplicates decode logic that ServiceLauncherClient.managed already exposes. I need to check if resolveServerSelfUpdateCapability is still actually being called anywhere, since this could be redundant state acquisition logic. I'm noticing there's redundant state management happening here — ServerEnvironment.make is calling through a chain that re-reads and re-decodes environment variables that ServiceLauncherClient.managed already exposes, but with stricter validation that causes make to fail on invalid context or version mismatches. This violates the dependency acquisition pattern — production code should be using the service from the environment rather than re-deriving state from raw environment variables, which is the same issue they fixed in Sqlite.isServiceLauncherTrial(). I need to check whether ServiceLauncherClient is actually available in ServerEnvironment's layer environment through the RuntimeDependenciesLive and launcherLayer chain. The layer composition shows that launcherLayer is provided to the merged layer, so ServerEnvironment could theoretically acquire ServiceLauncherClient directly. But there's a complication: ServerEnvironment might be used in CLI contexts where the launcher client isn't provided, which would cause a missing-service error. Since ServiceLauncherClient is only provided in server.ts, adding this dependency could break other usage contexts. The ServiceLauncherHostProcess and ServiceLauncherTrial are Context.References with defaults, so they handle missing services gracefully. The duplication appears intentional—a deliberate seam to avoid coupling ServerEnvironment to the launcher. The risk is that managed from ServerSelfUpdate and the serverSelfUpdate capability advertised in the environment descriptor could diverge, but in practice they stay in sync since version mismatches would prevent the server from starting entirely. Looking at the convention text about not creating per-feature managed runtimes and instead composing resources once in an application-owned layer, this does seem like a legitimate moderate finding: resolveServerSelfUpdateCapability re-derives managed instead of reading it from ServiceLauncherClient.managed. But this mirrors the earlier Sqlite issue they resolved with a Reference, and isServiceLauncherManaged was likely introduced as part of that fix to handle the global-process defaulting problem. They've addressed the hidden global concern by introducing ServiceLauncherHostProcess and HostProcessEnvironment, and replaced isServiceLauncherTrial with the ServiceLauncherTrial reference. But isServiceLauncherManaged remains, creating the same duplicate-derivation risk I noted before: the client fails on invalid context while this silently returns false, introducing a divergence risk. Though actually, thinking through whether a real divergence scenario exists — if rawContext is invalid, ServiceLauncherClient.make() fails and the server doesn't start, so isServiceLauncherManaged returning false becomes moot. The only case where they'd diverge is if the host is connected but context is absent, where both would return false anyway. So functionally they're equivalent, but it's still a "same-state derived twice" smell. I could add an inline comment to flag this, though I'm hesitant to add noise if it's not a concrete violation. Let me look deeper into the codebase to see if there's a clearer issue I'm missing. Looking at the service preflight command, it's wrapping a handler that calls runServicePreflight with database and launcher protocol parameters, then logs the result as JSON. The runServicePreflight function performs synchronous file operations... Actually, I'm realizing the arguments to Console.log get evaluated when the handler function is invoked at runtime, which means the synchronous I/O happens eagerly during effect construction rather than being deferred. Technically this violates the Effect pattern of keeping side effects lazy — a stricter approach would wrap it in Effect.sync. It's not a major issue since the handler runs at command time anyway, but it's worth noting as a minor deviation from Effect best practices around when side effects should be evaluated. Now I'm reviewing the migration manifest code with its error type that tracks expected versus actual counts, then checking the schema contract changes and the new server state code where error messages are properly structured with fields like target version and status. Finally, I'm thinking about test coverage — the guideline is to avoid adding large tests just to demonstrate a mechanical refactor. The PR deleted substantial test files: bootService.test.ts shrank from around 500 lines to 95, and selfUpdate.test.ts from roughly 630 to 95. Since this is a full rewrite of the update mechanism, removing tests for deleted behavior makes sense, but the problem is that critical code paths still exist — like quoteSystemdValue, renderBootServiceUnit, uninstall, and rollback logic — yet they've lost test coverage. The install and rollback logic in bootService was substantially rewritten with the stop/restore flow, but only a single happy-path test covers it, which doesn't meet the requirement for focused tests when backend behavior changes significantly. The new rollback restore path and status currency checks represent genuinely new behavior with no test coverage at all, which is a concrete violation of the change discipline rule. I need to be careful about scope though — the instruction says to report only violations in the PR's changed scope, so a missing-test finding on new behavior introduced here is fair game, but I should avoid flagging every deleted test as a violation since some covered old behavior that's legitimately gone. Looking at my three candidates, I'm leaning toward flagging the rollback/uninstall coverage gap as the strongest finding since it's new behavior with zero tests. The isServiceLauncherManaged duplication is moderate but less critical. For writeDurably, the explicit diagnostics suppression suggests the author acknowledged the dependency concern, so that's lower priority even though it does bypass the service abstraction layer. Let me verify the first candidate more carefully by checking ServerEnvironment around line 128 to see if there's actually a meaningful inconsistency. ServerEnvironment.make acquires dependencies from the environment, so adding ServiceLauncherClient there is workable. Both the References-with-defaults approach and direct acquisition would function, though I've already made a similar point about isServiceLauncherTrial and they went with the Reference pattern instead. Commenting again on isServiceLauncherManaged might feel like rehashing the same debate, but it's a separate function worth considering. The real issue I'm spotting is that ServiceLauncherClient.managed determines update capability in selfUpdate.make, while the descriptor advertises capability through isServiceLauncherManaged() — two independent sources of truth for the same thing. If the underlying state changes (like host.connected flipping after an IPC disconnect), they could diverge. I should flag this as a single concise comment suggesting that ServerEnvironment or resolveServerSelfUpdateCapability should read from ServiceLauncherClient.managed instead. But first I need to verify whether ServerEnvironment.make always has access to ServiceLauncherClient, or if there are contexts where it isn't provided. The Reference-based helper design weakens my finding because ServerEnvironment.layer is also used in cli/connect.ts where ServiceLauncherClient isn't available—requiring it there would break the CLI. In that context, isServiceLauncherManaged() returns false by default since there's no launcher, so forcing the client dependency would fail unless the context was valid, but make() only... Dropping candidate A. For candidate B on test coverage, the PR adds tests across multiple areas like bootService, pinnedRuntime, selfUpdate, and serviceLauncherClient, which is decent coverage. However, there's a genuine regression: tests for quoteSystemdValue, uninstall, and requireSystemdLinux were deleted even though those functions still exist in bootService.ts, and the new test file only has 2 tests instead of the previous coverage. The byte-for-byte assertion on renderBootServiceUnit was also dropped—the new test only checks for ExecStart and KillMode presence, not the exact unit content, which matters for status.current comparison. I think a focused comment on the test file noting this coverage loss for retained behavior and asking to restore tests for systemd value quoting/escaping, non-Linux unsupported paths, uninstall, and the rollback restoration logic is fair under "Change discipline." The install/rollback is new behavior that needs test coverage, especially verifying the rollback actually restores the previous launcher and state. Let me check the current bootService.test.ts to make sure I'm being accurate about what's there. Looking at the test coverage gaps in bootService—the rollback path that restores the previous launcher and state, the unsupported non-Linux branch, uninstall functionality, and the new status conditions are all untested despite substantial rewrites to the implementation. I'm also noticing that reconcileService in cli/service.ts had test coverage before but it seems to have been removed. I notice that reconcileService lost test coverage when the idempotency test was removed from bootService.test.ts, so I'll add a comment flagging this regression for the retained and newly-added behaviors like rollback restore, uninstall, unsupported platform, and reconcile idempotency. Now I'm looking back at selfUpdate.ts to see if there are other convention violations—specifically, I'm noticing some duplication in the capability computation around line 66-67 with resolveServerS Actually, there's a real inconsistency here: resolveServerSelfUpdateCapability is exported and defined at the top of the file, but make re-implements the same logic inline using launcher.managed instead of calling the function. This creates two sources of truth for the same decision within a single module, and it's a duplication introduced by this PR. The cleanest fix would be to have make call resolveServerSelfUpdateCapability with the managed flag derived from the server config mode rather than re-deriving it from launcher.ma So the two capability checks are actually identical in practice — both depend on the same conditions — which means it's pure duplication rather than a real inconsistency. That feels more like a style preference than a correctness issue. Given that the test-coverage finding is solid and actionable — the bootService install logic was completely rewritten with a new rollback mechanism but has zero tests, while the old tests for rollback scenarios were deleted — I should focus on that as my main finding rather than padding with borderline style notes. Looking at selfUpdate.test.ts now, I see the entire error-path and capability-resolution suite was stripped out, leaving only the happy path. The new error scenarios that were previously tested — like desktop-managed refusal, version mismatches, concurrent updates, and preflight blocks — are now untested even though the behaviors still exist in the code. That's a second clear coverage regression in the same PR, so I'll flag both test files with inline comments. For bootService.test.ts, I'll target line 95 (the expect(commands.some(...)).toBe(false) assertion in the last hunk). For selfUpdate.test.ts, I need to find the corresponding line in the diff where the error-path tests were removed. I'm noticing that line 90 appears in the diff as expected. Looking at the import patterns in the test files, there's an inconsistency — selfUpdate.test.ts imports make directly from the service module, while also importing ServiceLauncherClient and ServerShutdown as namespaces. The convention suggests importing the local service module as a namespace at service boundaries, though test files often deviate from this pattern by importing specific exports directly. But here's the issue: the PR actually regressed against this convention. bootService.test.ts was changed from import * as BootService to importing bare { make, renderBootServiceUnit }, which erases the module namespace entirely. The same regression appears in selfUpdate.test.ts and serviceLauncherClient.test.ts — they all shifted from namespace imports to named imports of specific exports like make. This is exactly what the convention flags as problematic, since it loses the module context that would normally be preserved with BootService.make or SelfUpdate.make. The serviceLauncher.ts file itself isn't a service module so that's fine, and forkParked is just a utility helper. There's a borderline case with ServiceLauncherTrial being imported as a context reference from a service module, but I'll focus the comment on the bare make imports since those are the clearer violations. The selfUpdate.ts file imports from a pinned runtime module that got cut off, but that's likely a utility dependency rather than a service module. The rule is clear about flagging consolidated named imports and using namespace imports at service boundaries, so a bare make in tests is genuinely ambiguous when multiple modules are involved. I'll post comments on the most affected test files — bootService.test.ts and selfUpdate.test.ts — targeting the import lines that appear in the diff to keep the feedback focused. I'm identifying several import issues across the test files where bare make imports are replacing the module namespace, and I need to flag these along with noting a test coverage gap for the bootService behavior changes. I'll consolidate the findings into three inline comments—one for each affected file—mentioning the import problem and any coverage gaps, rather than spreading them across five separate comments. I'm settling on three focused comments: one flagging the namespace import pattern across the test files, another on the missing test coverage for the new rollback logic and removed coverage for existing paths in bootService.test.ts, and a third on the coverage gaps in selfUpdate.test.ts. I'll keep the feedback concise and avoid being overly prescriptive about the duplicate capability derivation in selfUpdate.ts—just mention it briefly in context rather than demand a change. Writing the comment text... only the happy path; the previous tests for those branches were deleted. I should add focused tests for the blocked-preflight reason and at least one rejection branch. Now I'm ready to post the review with the state failure, titling it "Effect Service Conventions: 3 issues found" and submitting it.

1. modify_pr — Three findings, all in the rewritten test files for the new service-launcher update path. The production service modules (serviceLauncherClient.ts, serverShutdown.ts, selfUpdate.ts, Sqlite.ts) now follow the tag/make/layer shape, acquire their dependencies from the environment, and the earlier findings about the module-global shutdown channel, the free-form launcher error, the duplicated launcher client, and the catchTags recovery are resolved.


Agent Credits: 308 credits