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

24 KiB
Raw Blame History

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:

<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:

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:

    <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
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:

    <ProjectReference Include="..\..\src\ClaudeDo.Logging\ClaudeDo.Logging.csproj" />
  • Step 2: Write the failing test

Create tests/ClaudeDo.Worker.Tests/Logging/DefaultTaskIdEnricherTests.cs:

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:

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
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:

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:

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
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:

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:

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
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):

    <ProjectReference Include="..\ClaudeDo.Logging\ClaudeDo.Logging.csproj" />
  • Step 2: Replace the inline Serilog config

In src/ClaudeDo.Worker/Program.cs, replace lines 34-40:

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:

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
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:

    <PackageReference Include="Serilog.Extensions.Logging" Version="8.0.0" />

and to the ProjectReference ItemGroup:

    <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:

        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):

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
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:

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;):

        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:

        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
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:

using Microsoft.Extensions.Logging;
using Serilog.Context;

Add a field beside private readonly HubConnection _hub; (line 32):

    private readonly ILogger<WorkerClient> _logger;

Change the constructor signature (line 68) from:

    public WorkerClient(string signalRUrl)
    {

to:

    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):

    /// <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):

    public Task RunNowAsync(string taskId)
        => InvokeForTaskAsync(taskId, "RunNow", taskId);

ContinueTaskAsync (line 248):

    public Task ContinueTaskAsync(string taskId, string followUpPrompt)
        => InvokeForTaskAsync(taskId, "ContinueTask", taskId, followUpPrompt);

ResetTaskAsync (line 253):

    public Task ResetTaskAsync(string taskId)
        => InvokeForTaskAsync(taskId, "ResetTask", taskId);

CancelTaskAsync (line 267):

    public Task CancelTaskAsync(string taskId)
        => InvokeForTaskAsync(taskId, "CancelTask", taskId);

ApproveReviewAsync (line 389):

    public Task ApproveReviewAsync(string taskId)
        => InvokeForTaskAsync(taskId, "ApproveReview", taskId);

RejectReviewToQueueAsync (line 394):

    public Task RejectReviewToQueueAsync(string taskId, string feedback)
        => InvokeForTaskAsync(taskId, "RejectReviewToQueue", taskId, feedback);

RejectReviewToIdleAsync (line 399):

    public Task RejectReviewToIdleAsync(string taskId)
        => InvokeForTaskAsync(taskId, "RejectReviewToIdle", taskId);

CancelReviewAsync (line 404):

    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:

        sc.AddSingleton(sp => new WorkerClient(sp.GetRequiredService<AppSettings>().SignalRUrl));

with:

        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
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:
dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release
dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj -c Release
  • Run the logging tests:
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.