24 KiB
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.0and theSerilogcore pulled transitively bySerilog.AspNetCore 8.0.3(Worker), align thisSerilogversion to whateverSerilog.AspNetCore 8.0.3resolves (checkdotnet 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.Loggingfrom 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;
dbPathis already computed just above (var dbPath = Paths.Expand(settings.DbPath);). Its parent directory is~/.todo-app, sologssits 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-*.logappears in~/.todo-app/logs/. - Console output (Rider run window) shows
Debug-level lines taggedapp/....
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:
WorkerClientis faked in tests via theIWorkerClientinterface (hand-rolled fakes implement the interface, they do not subclassWorkerClient). This change adds a ctor parameter to the concrete class only and does not alterIWorkerClient, 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):
- Run the Worker and App from Rider (Debug build). Confirm both write to one
~/.todo-app/logs/claudedo-*.logwithapp/...andworker/...lines. - Run a task; grep that file for the task's id — confirm UI (
UI invoking RunNow…) and Worker lines share the same[<taskId>]. - Build/install the Release app; confirm the log is near-silent (no
Debug/Informationnoise,Warning+ only) and no console window logging.
- Run the Worker and App from Rider (Debug build). Confirm both write to one