From c5a4e350e925bf248b89be3cf05120ded391052c Mon Sep 17 00:00:00 2001 From: mika kuns Date: Thu, 4 Jun 2026 18:27:49 +0200 Subject: [PATCH] docs(logging): implementation plan for build-config logging + traceability Co-Authored-By: Claude Opus 4.7 --- .../2026-06-04-debug-logging-traceability.md | 725 ++++++++++++++++++ 1 file changed, 725 insertions(+) create mode 100644 docs/superpowers/plans/2026-06-04-debug-logging-traceability.md diff --git a/docs/superpowers/plans/2026-06-04-debug-logging-traceability.md b/docs/superpowers/plans/2026-06-04-debug-logging-traceability.md new file mode 100644 index 0000000..5e716dd --- /dev/null +++ b/docs/superpowers/plans/2026-06-04-debug-logging-traceability.md @@ -0,0 +1,725 @@ +# Debug Logging & Frontend↔Backend Traceability Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Build-configuration-driven logging — verbose in Debug builds (Rider run button), minimal `Warning`+ in Release (installed app) — with both processes writing one shared `claudedo-.log` and a `TaskId` correlation key threading UI→Worker→UI. + +**Architecture:** A new `ClaudeDo.Logging` library owns all Serilog setup: a `BuildConfig.IsDebug` runtime check (via the entry assembly's `DebuggableAttribute`, no `#if DEBUG`), a default-`TaskId` enricher, and a `LoggingSetup.Configure` method that branches sinks/levels on `IsDebug`. Worker and App both call it. `TaskId` rides Serilog `LogContext`, pushed at the per-task entry points on each side. + +**Tech Stack:** .NET 8, Serilog (core + File + Console sinks), Serilog.Extensions.Logging (App bridge), Serilog.AspNetCore (Worker, already present), xUnit. + +--- + +### Task 1: Create the `ClaudeDo.Logging` project + +**Files:** +- Create: `src/ClaudeDo.Logging/ClaudeDo.Logging.csproj` +- Create: `src/ClaudeDo.Logging/Placeholder.cs` (temporary, removed in Task 2) +- Modify: `ClaudeDo.slnx` + +- [ ] **Step 1: Create the csproj** + +Create `src/ClaudeDo.Logging/ClaudeDo.Logging.csproj`: + +```xml + + + + net8.0 + enable + enable + + + + + + + + + + + + + +``` + +> If NuGet reports a version conflict between `Serilog 4.1.0` and the `Serilog` core pulled transitively by `Serilog.AspNetCore 8.0.3` (Worker), align this `Serilog` version to whatever `Serilog.AspNetCore 8.0.3` resolves (check `dotnet list package --include-transitive`) and rebuild. + +- [ ] **Step 2: Add a temporary placeholder so the project compiles** + +Create `src/ClaudeDo.Logging/Placeholder.cs`: + +```csharp +namespace ClaudeDo.Logging; + +internal static class Placeholder; +``` + +- [ ] **Step 3: Register the project in the solution** + +Edit `ClaudeDo.slnx` — add inside the `/src/` folder, after the `ClaudeDo.Localization` line: + +```xml + +``` + +- [ ] **Step 4: Build the new project** + +Run: `dotnet build src/ClaudeDo.Logging/ClaudeDo.Logging.csproj -c Release` +Expected: Build succeeded. + +- [ ] **Step 5: Commit** + +```bash +git add src/ClaudeDo.Logging/ClaudeDo.Logging.csproj src/ClaudeDo.Logging/Placeholder.cs ClaudeDo.slnx +git commit -m "build(logging): scaffold ClaudeDo.Logging project" +``` + +--- + +### Task 2: `DefaultTaskIdEnricher` (TDD) + +Adds `TaskId = "-"` to any log event that doesn't already carry a `TaskId` property, so the `[{TaskId}]` column never renders the raw token. A pushed `LogContext` value takes precedence (because `Enrich.FromLogContext()` runs first and the property is then already present). + +**Files:** +- Create: `src/ClaudeDo.Logging/DefaultTaskIdEnricher.cs` +- Delete: `src/ClaudeDo.Logging/Placeholder.cs` +- Create: `tests/ClaudeDo.Worker.Tests/Logging/DefaultTaskIdEnricherTests.cs` +- Modify: `tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj` (add project reference) + +- [ ] **Step 1: Reference `ClaudeDo.Logging` from the test project** + +Edit `tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj` — add to the existing `ProjectReference` ItemGroup: + +```xml + +``` + +- [ ] **Step 2: Write the failing test** + +Create `tests/ClaudeDo.Worker.Tests/Logging/DefaultTaskIdEnricherTests.cs`: + +```csharp +using ClaudeDo.Logging; +using Serilog; +using Serilog.Context; +using Serilog.Core; +using Serilog.Events; + +namespace ClaudeDo.Worker.Tests.Logging; + +public sealed class DefaultTaskIdEnricherTests +{ + private sealed class CollectingSink : ILogEventSink + { + public List Events { get; } = new(); + public void Emit(LogEvent logEvent) => Events.Add(logEvent); + } + + [Fact] + public void AddsDash_WhenNoTaskIdInScope() + { + var sink = new CollectingSink(); + using var logger = new LoggerConfiguration() + .Enrich.FromLogContext() + .Enrich.With(new DefaultTaskIdEnricher()) + .WriteTo.Sink(sink) + .CreateLogger(); + + logger.Information("hello"); + + var prop = Assert.Single(sink.Events).Properties["TaskId"]; + Assert.Equal("\"-\"", prop.ToString()); + } + + [Fact] + public void KeepsPushedTaskId_WhenInScope() + { + var sink = new CollectingSink(); + using var logger = new LoggerConfiguration() + .Enrich.FromLogContext() + .Enrich.With(new DefaultTaskIdEnricher()) + .WriteTo.Sink(sink) + .CreateLogger(); + + using (LogContext.PushProperty("TaskId", "task-42")) + logger.Information("hello"); + + var prop = Assert.Single(sink.Events).Properties["TaskId"]; + Assert.Equal("\"task-42\"", prop.ToString()); + } +} +``` + +- [ ] **Step 3: Run the test to verify it fails** + +Run: `dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --filter DefaultTaskIdEnricherTests` +Expected: FAIL — `DefaultTaskIdEnricher` does not exist (compile error). + +- [ ] **Step 4: Implement the enricher and remove the placeholder** + +Delete `src/ClaudeDo.Logging/Placeholder.cs`. + +Create `src/ClaudeDo.Logging/DefaultTaskIdEnricher.cs`: + +```csharp +using Serilog.Core; +using Serilog.Events; + +namespace ClaudeDo.Logging; + +/// Ensures every log event carries a TaskId property (defaulting to "-") +/// so the output template's [{TaskId}] column never renders the raw token. +public sealed class DefaultTaskIdEnricher : ILogEventEnricher +{ + public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) + { + if (!logEvent.Properties.ContainsKey("TaskId")) + logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("TaskId", "-")); + } +} +``` + +- [ ] **Step 5: Run the test to verify it passes** + +Run: `dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --filter DefaultTaskIdEnricherTests` +Expected: PASS (2 tests). + +- [ ] **Step 6: Commit** + +```bash +git add src/ClaudeDo.Logging/DefaultTaskIdEnricher.cs tests/ClaudeDo.Worker.Tests/Logging/DefaultTaskIdEnricherTests.cs tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj +git rm src/ClaudeDo.Logging/Placeholder.cs +git commit -m "feat(logging): default TaskId enricher with passing tests" +``` + +--- + +### Task 3: `BuildConfig.IsDebug` + +Detects whether the entry assembly was compiled in the Debug configuration (JIT optimizer disabled) — the runtime replacement for `#if DEBUG`. + +**Files:** +- Create: `src/ClaudeDo.Logging/BuildConfig.cs` +- Create: `tests/ClaudeDo.Worker.Tests/Logging/BuildConfigTests.cs` + +- [ ] **Step 1: Write the failing test** + +The test asserts the property returns *some* bool without throwing, and that the underlying detection logic agrees with the test assembly's own `DebuggableAttribute` (the test runs under whatever config `dotnet test` used). We assert the helper's result equals a locally-computed expectation so it passes under both Debug and Release test runs. + +Create `tests/ClaudeDo.Worker.Tests/Logging/BuildConfigTests.cs`: + +```csharp +using System.Diagnostics; +using System.Reflection; +using ClaudeDo.Logging; + +namespace ClaudeDo.Worker.Tests.Logging; + +public sealed class BuildConfigTests +{ + [Fact] + public void IsDebug_MatchesEntryAssemblyDebuggableAttribute() + { + var entry = Assembly.GetEntryAssembly(); + var expected = entry? + .GetCustomAttribute() + ?.IsJITOptimizerDisabled ?? false; + + Assert.Equal(expected, BuildConfig.IsDebug); + } +} +``` + +- [ ] **Step 2: Run the test to verify it fails** + +Run: `dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --filter BuildConfigTests` +Expected: FAIL — `BuildConfig` does not exist (compile error). + +- [ ] **Step 3: Implement `BuildConfig`** + +Create `src/ClaudeDo.Logging/BuildConfig.cs`: + +```csharp +using System.Diagnostics; +using System.Reflection; + +namespace ClaudeDo.Logging; + +/// Runtime build-configuration detection — the replacement for #if DEBUG. +/// Debug builds compile with the JIT optimizer disabled; Release builds enable it. +public static class BuildConfig +{ + public static bool IsDebug { get; } = + Assembly.GetEntryAssembly() + ?.GetCustomAttribute() + ?.IsJITOptimizerDisabled ?? false; +} +``` + +- [ ] **Step 4: Run the test to verify it passes** + +Run: `dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --filter BuildConfigTests` +Expected: PASS. + +- [ ] **Step 5: Commit** + +```bash +git add src/ClaudeDo.Logging/BuildConfig.cs tests/ClaudeDo.Worker.Tests/Logging/BuildConfigTests.cs +git commit -m "feat(logging): runtime Debug-build detection via DebuggableAttribute" +``` + +--- + +### Task 4: `LoggingSetup.Configure` + +The single shared configuration entry point. Applies enrichers, the output template, and branches sinks/levels on `BuildConfig.IsDebug`. + +**Files:** +- Create: `src/ClaudeDo.Logging/LoggingSetup.cs` +- Create: `tests/ClaudeDo.Worker.Tests/Logging/LoggingSetupTests.cs` + +- [ ] **Step 1: Write the failing test** + +Verifies a configured logger actually writes a `Warning` (emitted in both build configs) to a `claudedo-*.log` file under the given log root. + +Create `tests/ClaudeDo.Worker.Tests/Logging/LoggingSetupTests.cs`: + +```csharp +using ClaudeDo.Logging; +using Serilog; + +namespace ClaudeDo.Worker.Tests.Logging; + +public sealed class LoggingSetupTests +{ + [Fact] + public void Configure_WritesSharedLogFile() + { + var logRoot = Path.Combine(Path.GetTempPath(), "claudedo-logtest-" + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(logRoot); + try + { + var logger = LoggingSetup.Configure(new LoggerConfiguration(), "test", logRoot).CreateLogger(); + logger.Warning("marker-{Marker}", "xyz"); + logger.Dispose(); // flush + release the file handle + + var files = Directory.GetFiles(logRoot, "claudedo-*.log"); + var file = Assert.Single(files); + var contents = File.ReadAllText(file); + Assert.Contains("marker-", contents); + Assert.Contains("test/", contents); // {Process} tag in the template + } + finally + { + try { Directory.Delete(logRoot, recursive: true); } catch { /* best effort */ } + } + } +} +``` + +- [ ] **Step 2: Run the test to verify it fails** + +Run: `dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --filter LoggingSetupTests` +Expected: FAIL — `LoggingSetup` does not exist (compile error). + +- [ ] **Step 3: Implement `LoggingSetup`** + +Create `src/ClaudeDo.Logging/LoggingSetup.cs`: + +```csharp +using Serilog; +using Serilog.Events; + +namespace ClaudeDo.Logging; + +public static class LoggingSetup +{ + private const string OutputTemplate = + "[{Timestamp:HH:mm:ss.fff} {Level:u3}] {Process}/{SourceContext} [{TaskId}] {Message:lj}{NewLine}{Exception}"; + + /// Apply the shared ClaudeDo logging configuration. + /// Debug builds: Debug level, console + shared file. Release builds: Warning level, shared file only. + /// "worker" or "app" — tags every line so the interleaved file is readable. + /// Directory for the shared claudedo-.log (created if missing). + public static LoggerConfiguration Configure(LoggerConfiguration cfg, string processTag, string logRoot) + { + Directory.CreateDirectory(logRoot); + var logFile = Path.Combine(logRoot, "claudedo-.log"); + + cfg.Enrich.FromLogContext() + .Enrich.WithProperty("Process", processTag) + .Enrich.With(new DefaultTaskIdEnricher()); + + if (BuildConfig.IsDebug) + { + cfg.MinimumLevel.Debug() + .WriteTo.Console(outputTemplate: OutputTemplate) + .WriteTo.File( + logFile, + rollingInterval: RollingInterval.Day, + retainedFileCountLimit: 2, + shared: true, + outputTemplate: OutputTemplate); + } + else + { + cfg.MinimumLevel.Warning() + .WriteTo.File( + logFile, + rollingInterval: RollingInterval.Day, + retainedFileCountLimit: 2, + shared: true, + outputTemplate: OutputTemplate); + } + + return cfg; + } +} +``` + +- [ ] **Step 4: Run the test to verify it passes** + +Run: `dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --filter LoggingSetupTests` +Expected: PASS. + +- [ ] **Step 5: Commit** + +```bash +git add src/ClaudeDo.Logging/LoggingSetup.cs tests/ClaudeDo.Worker.Tests/Logging/LoggingSetupTests.cs +git commit -m "feat(logging): shared LoggingSetup with build-config sink branching" +``` + +--- + +### Task 5: Wire the Worker to the shared setup + +**Files:** +- Modify: `src/ClaudeDo.Worker/ClaudeDo.Worker.csproj` +- Modify: `src/ClaudeDo.Worker/Program.cs:34-40` + +- [ ] **Step 1: Add the project reference** + +Edit `src/ClaudeDo.Worker/ClaudeDo.Worker.csproj` — add to the existing `ProjectReference` ItemGroup (the one with `ClaudeDo.Data`): + +```xml + +``` + +- [ ] **Step 2: Replace the inline Serilog config** + +In `src/ClaudeDo.Worker/Program.cs`, replace lines 34-40: + +```csharp +builder.Host.UseSerilog((ctx, lc) => lc + .MinimumLevel.Information() + .WriteTo.File( + System.IO.Path.Combine(logRoot, "worker-.log"), + rollingInterval: RollingInterval.Day, + retainedFileCountLimit: 7, + shared: true)); +``` + +with: + +```csharp +builder.Host.UseSerilog((ctx, lc) => + ClaudeDo.Logging.LoggingSetup.Configure(lc, "worker", logRoot)); +``` + +- [ ] **Step 3: Build the Worker** + +Run: `dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj -c Release` +Expected: Build succeeded. (If the Worker is running and locks the Debug output, this Release build is unaffected.) + +- [ ] **Step 4: Commit** + +```bash +git add src/ClaudeDo.Worker/ClaudeDo.Worker.csproj src/ClaudeDo.Worker/Program.cs +git commit -m "feat(logging): route Worker logging through shared LoggingSetup" +``` + +--- + +### Task 6: Wire the App/Ui (currently log-silent) to the shared setup + +The App uses a plain `ServiceCollection` with **no** logging registered. Add the Serilog→`ILogger` bridge so all `ILogger` injections across App/Ui flow to the shared sinks, and flush on shutdown. + +**Files:** +- Modify: `src/ClaudeDo.App/ClaudeDo.App.csproj` +- Modify: `src/ClaudeDo.App/Program.cs` + +- [ ] **Step 1: Add packages and the project reference** + +Edit `src/ClaudeDo.App/ClaudeDo.App.csproj` — add to the package `ItemGroup`: + +```xml + +``` + +and to the `ProjectReference` ItemGroup: + +```xml + +``` + +- [ ] **Step 2: Add the logging registration in `BuildServices`** + +In `src/ClaudeDo.App/Program.cs`, inside `BuildServices()`, immediately after the `var sc = new ServiceCollection();` line (currently line 78), insert: + +```csharp + var logRoot = Path.Combine(Path.GetDirectoryName(dbPath)!, "logs"); + var serilogLogger = ClaudeDo.Logging.LoggingSetup + .Configure(new Serilog.LoggerConfiguration(), "app", logRoot) + .CreateLogger(); + sc.AddLogging(b => b.AddSerilog(serilogLogger, dispose: true)); +``` + +Add these usings to the top of `Program.cs` (the `AddSerilog` `ILoggingBuilder` extension lives in the `Serilog` namespace; `AddLogging` lives in `Microsoft.Extensions.DependencyInjection`, already imported): + +```csharp +using Serilog; +using Microsoft.Extensions.Logging; +``` + +> `dbPath` is already computed just above (`var dbPath = Paths.Expand(settings.DbPath);`). Its parent directory is `~/.todo-app`, so `logs` sits beside the Worker's log root. + +- [ ] **Step 3: Build the App** + +Run: `dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release` +Expected: Build succeeded (pulls in Ui + Data + Logging). + +- [ ] **Step 4: Verify manually from Rider (visual-verification gap)** + +This is a Debug-build behavior that cannot be asserted in a Release test run. Launch the App from Rider's run button and confirm: +- A `claudedo-*.log` appears in `~/.todo-app/logs/`. +- Console output (Rider run window) shows `Debug`-level lines tagged `app/...`. + +Flag to the user that this step needs their eyes. + +- [ ] **Step 5: Commit** + +```bash +git add src/ClaudeDo.App/ClaudeDo.App.csproj src/ClaudeDo.App/Program.cs +git commit -m "feat(logging): wire App/Ui logging to shared LoggingSetup" +``` + +--- + +### Task 7: Push `TaskId` into `LogContext` in the Worker + +Wraps the two per-task entry points so every nested log line (runner, state service, worktree, planning) carries the task's id automatically. + +**Files:** +- Modify: `src/ClaudeDo.Worker/Runner/TaskRunner.cs:47` (`RunAsync`) and `:171` (`ContinueAsync`) + +- [ ] **Step 1: Add the using directive** + +In `src/ClaudeDo.Worker/Runner/TaskRunner.cs`, add to the top usings: + +```csharp +using Serilog.Context; +``` + +- [ ] **Step 2: Push TaskId at the top of `RunAsync`** + +In `RunAsync` (line 47), insert as the very first statement of the method body (before `string? mcpToken = null;`): + +```csharp + using var _taskScope = LogContext.PushProperty("TaskId", task.Id); +``` + +- [ ] **Step 3: Push TaskId at the top of `ContinueAsync`** + +In `ContinueAsync` (line 171), insert as the very first statement of the method body (before `TaskEntity task;`). The parameter is `taskId`: + +```csharp + using var _taskScope = LogContext.PushProperty("TaskId", taskId); +``` + +- [ ] **Step 4: Build the Worker** + +Run: `dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj -c Release` +Expected: Build succeeded. + +- [ ] **Step 5: Commit** + +```bash +git add src/ClaudeDo.Worker/Runner/TaskRunner.cs +git commit -m "feat(logging): tag Worker task execution with TaskId for traceability" +``` + +--- + +### Task 8: Push `TaskId` and add trace lines on the App side + +`WorkerClient` currently logs nothing. Inject `ILogger`, add a small helper that pushes `TaskId` + emits a `Debug` trace line, and route the fire-and-forget task-targeted hub calls through it. This produces the UI half of the UI→Worker→UI trace under a shared `TaskId`. + +**Files:** +- Modify: `src/ClaudeDo.Ui/Services/WorkerClient.cs` +- Modify: `src/ClaudeDo.App/Program.cs:101` (registration) + +- [ ] **Step 1: Add usings and the logger field/ctor param** + +In `src/ClaudeDo.Ui/Services/WorkerClient.cs`, add to the usings: + +```csharp +using Microsoft.Extensions.Logging; +using Serilog.Context; +``` + +Add a field beside `private readonly HubConnection _hub;` (line 32): + +```csharp + private readonly ILogger _logger; +``` + +Change the constructor signature (line 68) from: + +```csharp + public WorkerClient(string signalRUrl) + { +``` + +to: + +```csharp + public WorkerClient(string signalRUrl, ILogger logger) + { + _logger = logger; +``` + +- [ ] **Step 2: Add the task-scoped invoke helper** + +In `src/ClaudeDo.Ui/Services/WorkerClient.cs`, add this private method next to `TryInvokeAsync` (after line 241): + +```csharp + /// Invoke a task-targeted hub method under a TaskId log scope, emitting a debug trace line. + private async Task InvokeForTaskAsync(string taskId, string method, params object?[] args) + { + using (LogContext.PushProperty("TaskId", taskId)) + { + _logger.LogDebug("UI invoking {Method} for task {TaskId}", method, taskId); + await _hub.InvokeCoreAsync(method, args); + } + } +``` + +- [ ] **Step 3: Route the fire-and-forget task actions through the helper** + +In the same file, replace each of these method bodies: + +`RunNowAsync` (line 243): +```csharp + public Task RunNowAsync(string taskId) + => InvokeForTaskAsync(taskId, "RunNow", taskId); +``` + +`ContinueTaskAsync` (line 248): +```csharp + public Task ContinueTaskAsync(string taskId, string followUpPrompt) + => InvokeForTaskAsync(taskId, "ContinueTask", taskId, followUpPrompt); +``` + +`ResetTaskAsync` (line 253): +```csharp + public Task ResetTaskAsync(string taskId) + => InvokeForTaskAsync(taskId, "ResetTask", taskId); +``` + +`CancelTaskAsync` (line 267): +```csharp + public Task CancelTaskAsync(string taskId) + => InvokeForTaskAsync(taskId, "CancelTask", taskId); +``` + +`ApproveReviewAsync` (line 389): +```csharp + public Task ApproveReviewAsync(string taskId) + => InvokeForTaskAsync(taskId, "ApproveReview", taskId); +``` + +`RejectReviewToQueueAsync` (line 394): +```csharp + public Task RejectReviewToQueueAsync(string taskId, string feedback) + => InvokeForTaskAsync(taskId, "RejectReviewToQueue", taskId, feedback); +``` + +`RejectReviewToIdleAsync` (line 399): +```csharp + public Task RejectReviewToIdleAsync(string taskId) + => InvokeForTaskAsync(taskId, "RejectReviewToIdle", taskId); +``` + +`CancelReviewAsync` (line 404): +```csharp + public Task CancelReviewAsync(string taskId) + => InvokeForTaskAsync(taskId, "CancelReview", taskId); +``` + +> These all previously did `await _hub.InvokeAsync(method, ...)` with no return value, so converting them to expression-bodied delegations preserves behavior. Do **not** touch methods that return DTOs (e.g. `MergeTaskAsync`) or the planning methods — keep this change scoped to the void task actions above. + +- [ ] **Step 4: Update the DI registration to pass the logger** + +In `src/ClaudeDo.App/Program.cs`, replace line 101: + +```csharp + sc.AddSingleton(sp => new WorkerClient(sp.GetRequiredService().SignalRUrl)); +``` + +with: + +```csharp + sc.AddSingleton(sp => new WorkerClient( + sp.GetRequiredService().SignalRUrl, + sp.GetRequiredService>())); +``` + +Add `using Microsoft.Extensions.Logging;` to the top of `Program.cs` if not already present. + +- [ ] **Step 5: Build the App** + +Run: `dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release` +Expected: Build succeeded. + +> Note: `WorkerClient` is faked in tests via the `IWorkerClient` *interface* (hand-rolled fakes implement the interface, they do not subclass `WorkerClient`). This change adds a ctor parameter to the concrete class only and does not alter `IWorkerClient`, so the fakes are unaffected. Confirm by building the test projects in the next step. + +- [ ] **Step 6: Build the test projects to confirm fakes still compile** + +Run: `dotnet build tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release && dotnet build tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release` +Expected: Build succeeded for both. + +- [ ] **Step 7: Run the full Worker.Tests suite** + +Run: `dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release` +Expected: PASS (all existing tests + the 4 new logging tests). + +- [ ] **Step 8: Commit** + +```bash +git add src/ClaudeDo.Ui/Services/WorkerClient.cs src/ClaudeDo.App/Program.cs +git commit -m "feat(logging): tag UI task actions with TaskId + debug trace lines" +``` + +--- + +## Final verification + +- [ ] **Build the whole desktop + worker stack in Release:** + +```bash +dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release +dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj -c Release +``` + +- [ ] **Run the logging tests:** + +```bash +dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --filter "FullyQualifiedName~Logging" +``` +Expected: PASS (DefaultTaskIdEnricher × 2, BuildConfig × 1, LoggingSetup × 1). + +- [ ] **Manual smoke test (visual-verification gap — needs the user):** + 1. Run the Worker and App from Rider (Debug build). Confirm both write to one `~/.todo-app/logs/claudedo-*.log` with `app/...` and `worker/...` lines. + 2. Run a task; grep that file for the task's id — confirm UI (`UI invoking RunNow…`) and Worker lines share the same `[]`. + 3. Build/install the Release app; confirm the log is near-silent (no `Debug`/`Information` noise, `Warning`+ only) and no console window logging.