diff --git a/docs/superpowers/specs/2026-06-04-debug-logging-traceability-design.md b/docs/superpowers/specs/2026-06-04-debug-logging-traceability-design.md index 723a7e6..8de7ac3 100644 --- a/docs/superpowers/specs/2026-06-04-debug-logging-traceability-design.md +++ b/docs/superpowers/specs/2026-06-04-debug-logging-traceability-design.md @@ -5,16 +5,16 @@ ## Goal -Make debug logging rich enough to diagnose problems across the UI↔Worker boundary, while keeping the installed (production) build near-silent. Verbosity is decided by **build configuration** — no runtime knob, no config field: +Make debug logging rich enough to diagnose problems across the UI↔Worker boundary, while keeping the installed (production) build near-silent. Verbosity is decided by **build configuration, detected at runtime** — no runtime knob, no config field, no `#if DEBUG`: - **Debug build** (Rider run button) → verbose, console + file. - **Release build** (installed app) → minimal, file only. ## Decisions (from brainstorming) -1. **Mechanism:** `#if DEBUG` build-config split. Rider builds `Debug`; the installer ships `Release`. +1. **Mechanism:** runtime build-config detection via the entry assembly's `DebuggableAttribute` (JIT optimizer disabled ⇒ Debug build). A single `BuildConfig.IsDebug` helper drives ordinary `if` branching — no `#if DEBUG` directives. Rider's run button builds `Debug`; the installer ships `-c Release`. 2. **Scope:** Worker **and** App/Ui. The desktop side currently has no log sink at all — UI/IPC failures vanish today. -3. **Release behavior:** App/Ui log `Warning`+ to file (not silent — capture crashes). Worker unchanged at `Information`. +3. **Release behavior:** all three log `Warning`+ to file (not silent — capture crashes). Worker drops from its current `Information` to `Warning`. 4. **One shared log file** across both processes, unified timeline. 5. **Correlation:** TaskId-based (option A). Enrich log lines with `TaskId` when one is in scope. No changes to the SignalR contract (`IWorkerClient`/`WorkerHub` untouched → test fakes untouched). @@ -22,14 +22,14 @@ Make debug logging rich enough to diagnose problems across the UI↔Worker bound | Process | Debug build | Release build | |---|---|---| -| Worker | `Debug` level, console + shared file | `Information` level, shared file | +| Worker | `Debug` level, console + shared file | `Warning` level, shared file | | App/Ui | `Debug` level, console + shared file | `Warning` level, shared file | ## Shared log file - Single daily-rolling file: `~/.todo-app/logs/claudedo-.log` (Serilog appends the date). - `shared: true` on both processes' file sinks → Serilog coordinates multi-process writes via a global mutex. -- `retainedFileCountLimit: 7` (matches current Worker retention). +- `retainedFileCountLimit: 2`. - Each line is tagged with a `Process` property (`"worker"` / `"app"`) so the two sides are distinguishable in the interleaved timeline. > The existing `worker-.log` is replaced by `claudedo-.log`. Task-run NDJSON (`{taskId}_run{n}.ndjson`) and `daily-prep.log` are **out of scope** — they are data streams, not diagnostic logs, and stay exactly as they are. @@ -61,9 +61,11 @@ This adds **no parameters** to the SignalR surface — correlation rides on the A single shared helper keeps the two processes' Serilog setup from drifting. -- **New:** `ClaudeDo.Data/Logging/LoggingSetup.cs` (or similar shared location reachable by both App and Worker) exposing the output template, the default-TaskId enricher, and a `ConfigureLogger(LoggerConfiguration, Process, logRoot)` that applies the `#if DEBUG` level/sink choices. Both processes call it so level/template/retention stay in sync. - - *Placement note:* must live in a project both `ClaudeDo.App` and `ClaudeDo.Worker` reference. `ClaudeDo.Data` qualifies; if it should not take a Serilog dependency, use a tiny new shared project. Decide during planning. -- **Worker `Program.cs:34`:** replace the inline `UseSerilog` body with a call into the shared helper (`Process = "worker"`). +- **New project:** `ClaudeDo.Logging` — a small library both `ClaudeDo.App` and `ClaudeDo.Worker` reference (keeps `ClaudeDo.Data` free of any Serilog dependency). Contains: + - `BuildConfig.IsDebug` — checks the entry assembly's `DebuggableAttribute` (`IsJITOptimizerDisabled` ⇒ Debug build). Cached static. + - The output template and the default-TaskId enricher. + - `ConfigureLogger(LoggerConfiguration, processTag, logRoot)` — applies level/sink choices by branching on `BuildConfig.IsDebug` (Debug ⇒ `Debug` level + console + file; Release ⇒ `Warning` level + file only). Both processes call it so level/template/retention stay in sync. +- **Worker `Program.cs:34`:** replace the inline `UseSerilog` body with a call into the shared helper (`processTag = "worker"`). - **App `Program.cs`:** add Serilog packages; build a logger via the shared helper (`Process = "app"`) and register it with `sc.AddLogging(b => b.AddSerilog(logger, dispose: true))`. App currently registers **no** logging at all, so this also makes `ILogger` injection actually work UI-side. Remove/keep `.LogToTrace()` as appropriate (Avalonia internal trace, separate concern — leave it). - **App shutdown:** flush/close the logger (`Log.CloseAndFlush()` or dispose via the container's existing `finally`). @@ -77,7 +79,7 @@ A single shared helper keeps the two processes' Serilog setup from drifting. ## Testing - This is logging wiring; per project policy, no tests that spawn the real Claude CLI and no heavy test scaffolding for log output. -- Light verification: a unit-level check that the shared helper produces the expected minimum level per build config (Debug vs Release) and that the default enricher yields `-` when no `TaskId` is pushed. If asserting on `#if DEBUG` is awkward in a single test run, verify the Release path (the one that matters for prod) and smoke-test Debug manually from Rider. +- Light verification: a unit-level check that the default enricher yields `-` when no `TaskId` is pushed, and (if practical) that `ConfigureLogger` wires the expected sinks. `BuildConfig.IsDebug` reflects the test assembly's own build config, so it can't be flipped within one run — assert each branch by passing the level/flag explicitly rather than relying on the ambient value, or verify the Release path and smoke-test Debug manually from Rider. - Manual smoke test (documented, not automated): run from Rider, confirm console + `claudedo-.log` show `Debug` lines with `Process`/`SourceContext`; run a task and confirm both `app` and `worker` lines share the same `[TaskId]`. ## Out of scope