726 lines
24 KiB
Markdown
726 lines
24 KiB
Markdown
# 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.
|