Files
ClaudeDo/docs/superpowers/plans/2026-06-04-debug-logging-traceability.md
2026-06-04 18:27:49 +02:00

726 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Serilog" Version="4.1.0" />
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
</ItemGroup>
<ItemGroup>
<InternalsVisibleTo Include="ClaudeDo.Worker.Tests" />
</ItemGroup>
</Project>
```
> 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
<Project Path="src/ClaudeDo.Logging/ClaudeDo.Logging.csproj" />
```
- [ ] **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
<ProjectReference Include="..\..\src\ClaudeDo.Logging\ClaudeDo.Logging.csproj" />
```
- [ ] **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<LogEvent> 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;
/// <summary>Ensures every log event carries a TaskId property (defaulting to "-")
/// so the output template's [{TaskId}] column never renders the raw token.</summary>
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<DebuggableAttribute>()
?.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;
/// <summary>Runtime build-configuration detection — the replacement for #if DEBUG.
/// Debug builds compile with the JIT optimizer disabled; Release builds enable it.</summary>
public static class BuildConfig
{
public static bool IsDebug { get; } =
Assembly.GetEntryAssembly()
?.GetCustomAttribute<DebuggableAttribute>()
?.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}";
/// <summary>Apply the shared ClaudeDo logging configuration.
/// Debug builds: Debug level, console + shared file. Release builds: Warning level, shared file only.</summary>
/// <param name="processTag">"worker" or "app" — tags every line so the interleaved file is readable.</param>
/// <param name="logRoot">Directory for the shared claudedo-.log (created if missing).</param>
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
<ProjectReference Include="..\ClaudeDo.Logging\ClaudeDo.Logging.csproj" />
```
- [ ] **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<T>` 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
<PackageReference Include="Serilog.Extensions.Logging" Version="8.0.0" />
```
and to the `ProjectReference` ItemGroup:
```xml
<ProjectReference Include="..\ClaudeDo.Logging\ClaudeDo.Logging.csproj" />
```
- [ ] **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<WorkerClient>`, 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<WorkerClient> _logger;
```
Change the constructor signature (line 68) from:
```csharp
public WorkerClient(string signalRUrl)
{
```
to:
```csharp
public WorkerClient(string signalRUrl, ILogger<WorkerClient> 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
/// <summary>Invoke a task-targeted hub method under a TaskId log scope, emitting a debug trace line.</summary>
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<AppSettings>().SignalRUrl));
```
with:
```csharp
sc.AddSingleton(sp => new WorkerClient(
sp.GetRequiredService<AppSettings>().SignalRUrl,
sp.GetRequiredService<ILogger<WorkerClient>>()));
```
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 `[<taskId>]`.
3. Build/install the Release app; confirm the log is near-silent (no `Debug`/`Information` noise, `Warning`+ only) and no console window logging.