# Tasks 11-12: Eliminate DaemonLiveOptions / Collapse to Layer.launch -- Audit Report

**Summary:** Both tasks have significant design gaps. Task 11's `Layer.mergeAll` composition is incorrect for layers with inter-dependencies. Task 12 has three critical issues: (1) `Layer.launch` does NOT handle SIGINT/SIGTERM -- it relies on something external to interrupt the fiber, and the existing `SignalHandlerLayer` only completes a Deferred without triggering interruption; (2) the `--daemon` path changes `startDaemonProcess` from `Promise<DaemonHandle>` to a fire-and-forget `Fiber`, breaking the call site; (3) the `--foreground` path needs `addProject`, `discoverProjects`, `getStatus`, `port` but the plan's `ManagedRuntime` sketch doesn't show how to provide these.

---

## Findings

| # | Category | Action | Issue | File:Line | Amendment / Question |
|---|----------|--------|-------|-----------|----------------------|
| 1 | Fragile Code | **Amend Plan** | Task 11 uses `Layer.mergeAll` for ~20 layers including ones with inter-dependencies. `TlsCertLive(configDir)` needs `DaemonConfigRefTag` (it reads `configRef` from context). `AuthManagerFromConfigLive` needs `DaemonConfigRefTag`. `ProjectRegistryLive` likely needs `DaemonEventBusTag` and `ConfigWriterTag`. `IpcDispatchLive` needs `ProjectRegistryTag`, `InstanceManagerTag`, `AuthManagerTag`, `DaemonConfigRefTag`. `WebSocketRoutingLive` needs `AuthManagerTag`, `ProjectRegistryTag`, `DaemonConfigRefTag`. `ConfigPersistenceLive` needs `DaemonEventBusTag`, `DaemonConfigRefTag`, `ConfigWriterTag`. `Layer.mergeAll` does NOT satisfy inter-layer dependencies -- it only merges outputs. The conventions doc (line 52-53) explicitly says: "Use `Layer.provide` to express dependency relationships, not flat `Layer.mergeAll`. `mergeAll` is only for truly independent Layers." | `docs/plans/2026-05-07-daemon-effect-phase8-plan.md` Task 11, lines 1013-1046; `docs/plans/effect-ts-next-wave/conventions.md` lines 52-53 | Replace the single `Layer.mergeAll(...)` with a tiered composition: (1) foundation tier (`DaemonEventBusLive`, `PinoLoggerLive`, `DaemonConfigRefLive`) via `mergeAll`; (2) services tier (`AuthManagerFromConfigLive`, `TlsCertLive`, `ConfigPersistenceLive`) via `Layer.provide(foundationTier)`; (3) registries tier (`ProjectRegistryLive`, `InstanceManagerLive`, `RelayFactoryLive`) via `Layer.provide(servicesTier)`; (4) dispatch/routing tier (`IpcDispatchLive`, `WebSocketRoutingLive`, etc.) via `Layer.provide(registriesTier)`; (5) infrastructure (`SignalHandlerLayer`, `ProcessErrorHandlerLayer`, `makePidFileLive`) via `mergeAll` (truly independent). Final composition merges independent + provides dependent tiers. |
| 2 | Implicit Assumptions | **Amend Plan** | Task 12 assumes `Layer.launch` handles SIGINT/SIGTERM automatically. Verified: `Layer.launch` (in `node_modules/effect/src/internal/layer.ts:669-675`) simply calls `buildWithScope(scope)` then `core.never`. It does NOT install signal handlers. `Effect.runFork` also does NOT install signal handlers (`node_modules/effect/src/internal/runtime.ts` has no SIGINT/SIGTERM references). The existing `SignalHandlerLayer` catches signals and completes a `Deferred<void>`, but nothing in the plan's Task 12 code awaits that Deferred or uses it to trigger fiber interruption. With `SignalHandlerLayer` registered, Node's default SIGINT behavior (process exit) is suppressed, so the process would hang on Ctrl+C. | `node_modules/effect/src/internal/layer.ts:669-675`; `src/lib/effect/daemon-layers.ts:75-101` | The plan must either: (A) Use `NodeRuntime.runMain` instead of `Effect.runFork` -- `runMain` installs signal handlers that interrupt the fiber; OR (B) Add a mechanism where the `SignalHandlerLayer`'s Deferred completion triggers scope close (e.g., replace `Effect.never` in the daemon program with `Deferred.await(shutdownSignal)` wrapped in a Layer, and compose it into `makeDaemonLive`); OR (C) Keep `SignalHandlerLayer` but add a shutdown fiber that awaits the Deferred and then calls `runtime.dispose()`. Option (A) is simplest but changes the return type. Option (B) matches existing architecture best. Amend Task 12 to specify which approach and show the wiring. |
| 3 | Missing Wiring | **Amend Plan** | Task 12's `--daemon` path: `startDaemonProcess` currently returns `Promise<DaemonHandle>`. The plan changes it to `Effect.runFork(Layer.launch(...))` which returns `Fiber.RuntimeFiber<never, E>`. The `--daemon` call site in `cli-core.ts:122` uses `await startDaemonProcess(...)` -- this would never resolve (since `Layer.launch` returns `Effect<never>`). The current `--daemon` path works because `startDaemonProcess` returns after setup is complete and the HTTP/IPC servers keep the process alive. With `Layer.launch`, the effect never completes. | `src/bin/cli-core.ts:122-139`; plan Task 12 lines 1090-1091 | Amend: for the `--daemon` path, `startDaemonProcess` should use `NodeRuntime.runMain(Layer.launch(makeDaemonLive(options)))` (no return value needed -- process stays alive). Update `cli-core.ts` `--daemon` path to call it without `await` or change return type to `void`. The current `await` + `return` pattern in cli-core.ts (line 139) expects the function to eventually return so the process stays alive via servers -- with `runMain` the process stays alive via `Effect.never`. |
| 4 | Missing Wiring | **Amend Plan** | Task 12's `--foreground` path needs `DaemonHandle` with methods `addProject`, `discoverProjects`, `getStatus`, `port`. The plan shows: `const runtime = ManagedRuntime.make(makeDaemonLive(options)); await runtime.runPromise(Effect.void); const handle = await runtime.runPromise(DaemonHandleTag);` But: (a) `DaemonHandleTag` is never defined anywhere in the plan; (b) no Layer in `makeDaemonLive` provides a `DaemonHandleTag` service; (c) the `DaemonHandle` interface (`daemon-main.ts:265-291`) has methods like `addProject(directory, slug?, instanceId?): Promise<StoredProject>` and `registry: ProjectRegistry` -- these can't be trivially provided by a Context.Tag without defining the full implementation; (d) `runtime.runPromise(Effect.void)` doesn't "start" the daemon -- `ManagedRuntime` builds layers lazily on first `runPromise` call, so this would build layers and return immediately, but `Effect.never` is NOT part of `ManagedRuntime`'s behavior (it builds the layer, not launches it). | `src/bin/cli-core.ts:162-184`; `src/lib/effect/daemon-main.ts:265-291`; plan Task 12 lines 1094-1097 | Amend: (1) Define `DaemonHandleTag` as a `Context.Tag` for a service that exposes `addProject`, `discoverProjects`, `getStatus`, `port`, `stop`; (2) Add a `DaemonHandleLive` Layer to `makeDaemonLive` that constructs the handle from `ProjectRegistryTag`, `InstanceManagerTag`, `DaemonConfigRefTag`; (3) For foreground mode, use `ManagedRuntime.make(makeDaemonLive(options))` then `runtime.runPromise(DaemonHandleTag)` to get the handle. This requires `DaemonHandleLive` to NOT include `Effect.never` -- it should just compose the services without blocking. The `--foreground` caller keeps the process alive (the HTTP servers do). |
| 5 | State Issues | **Amend Plan** | Task 12 deletes the `stop()` function. Currently `stop()` is called from: (a) IPC `shutdown` command via `ipcContext.scheduleShutdown` -> `setTimeout(() => stop(), DAEMON_SHUTDOWN_DELAY_MS)` (`daemon-main.ts:1019`); (b) foreground mode could call `daemon.stop()`. With `Layer.launch`, teardown happens when the fiber is interrupted, but there's no mechanism for the IPC `shutdown` command to trigger that interruption. The plan doesn't show how `IpcDispatchLive`'s shutdown handler interrupts the root fiber. | `src/lib/effect/daemon-main.ts:764-790, 1018-1019` | Amend: Add explicit shutdown mechanism. Options: (A) IPC shutdown handler completes the `ShutdownSignalTag` Deferred, and the main program fiber awaits it (see finding #2); (B) IPC shutdown handler calls `Fiber.interrupt` on the root fiber (requires passing a fiber ref). Option A is cleaner and aligns with existing `SignalHandlerLayer`. Amend Task 8 (IPC handlers) to have the shutdown handler complete the Deferred, and amend Task 12 to show how Deferred completion triggers teardown. |
| 6 | Incorrect Code | **Amend Plan** | Task 12 shows `Effect.runFork(Layer.launch(makeDaemonLive(options)))` but `Layer.launch` returns `Effect<never, E, RIn>`. If `makeDaemonLive` has unsatisfied requirements (`RIn != never`), `Effect.runFork` will fail at compile time. Currently `makeDaemonLive` returns a Layer with `RIn = never` (all deps satisfied internally), but the plan's new composition in Task 11 adds Layers like `HttpServerLive`, `IpcServerLive` etc. that don't exist yet as self-contained Layers -- they currently take `DaemonLifecycleContext` as constructor args (`makeHttpServerLive(ctx)`, `makeIpcServerLive(ctx, ipcContext, getStatus)`). Task 11 must convert these to Layers that get their deps from Context, but the plan doesn't detail this conversion. | `src/lib/effect/daemon-layers.ts:215-265` | Amend: Task 11 must specify how `makeHttpServerLive`, `makeIpcServerLive`, `makeOnboardingServerLive` are converted from taking `DaemonLifecycleContext` params to reading deps from Context Tags. The `DaemonLifecycleContext` is deeply mutable (server refs assigned during startup). Converting it requires either: (a) Ref-based Tags for each server reference, or (b) keeping `DaemonLifecycleContext` as a `Layer.succeed` value but constructing it from Context. This is not a small change and warrants its own sub-steps. |
| 7 | Implicit Assumptions | **Accept** | The plan's conventions doc (line 78) says "Use `Layer.launch` for the top-level daemon program" and the design doc (line 55) says "`Layer.launch` tears down in reverse order on SIGINT/SIGTERM" -- but as verified, `Layer.launch` does NOT handle signals. The conventions doc itself has this incorrect assumption baked in. | `docs/plans/effect-ts-next-wave/conventions.md:78` | The conventions doc should be corrected but this is informational for this audit -- the actionable fix is in finding #2. |
| 8 | Insufficient Test Coverage | **Amend Plan** | Task 12's test is a stub: `it.scoped("starts and stops cleanly", () => Effect.gen(function* () { /* Provide test layers with mock HTTP server, mock IPC ... */ }))`. Given the complexity of the shutdown path (finding #2, #5), the test must verify: (a) Layer builds successfully with all mock deps; (b) shutdown signal (Deferred completion or fiber interrupt) triggers orderly teardown; (c) all finalizers run (PID file removed, servers closed); (d) IPC shutdown command triggers teardown. Without these, the critical shutdown path is untested. | Plan Task 12, lines 1134-1145 | Amend: Expand the test stub to specify at minimum: (1) A test that builds a mock `makeDaemonLive` with test Layers, verifies services are accessible, then interrupts the fiber and asserts finalizers ran; (2) A test that verifies IPC shutdown command completes the ShutdownSignal Deferred; (3) A test for the foreground mode `DaemonHandle` interface. |

---

**No issues found in:** (all assigned categories had findings)

## Key Recommendations

1. **Most critical:** Fix the SIGINT/SIGTERM gap (finding #2). Without this, the daemon cannot be stopped. Recommend using `NodeRuntime.runMain` for the `--daemon` path and wiring the `ShutdownSignalTag` Deferred into the main program fiber so it exits when the IPC shutdown command fires.

2. **Second priority:** Fix the `Layer.mergeAll` composition (finding #1). This will cause runtime errors when layers fail to find their dependencies in Context.

3. **Third priority:** Define `DaemonHandleTag` and its Layer (finding #4). Without this, the `--foreground` path in cli-core.ts is broken.
