# 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.
