diff --git a/docs/superpowers/plans/2026-06-01-worker-lifecycle.md b/docs/superpowers/plans/2026-06-01-worker-lifecycle.md new file mode 100644 index 0000000..f1991ed --- /dev/null +++ b/docs/superpowers/plans/2026-06-01-worker-lifecycle.md @@ -0,0 +1,829 @@ +# Worker Lifecycle Redesign 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:** Make the worker owned by a single external mechanism (a per-user Startup-folder shortcut in production), stop the App from auto-spawning its own worker, and show an actionable prompt when the App can't connect. + +**Architecture:** Installer creates a `.lnk` in the Windows Startup folder instead of a Scheduled Task (migrating existing installs by deleting the old task). The App's `IslandsShellViewModel` drops `EnsureWorkerRunningAsync` and instead runs a one-shot grace timer that opens a `WorkerConnectionModal` (Start Worker / Rerun Installer / Dismiss) if still offline; the footer status pill becomes a button that reopens it. + +**Tech Stack:** .NET 8, WPF installer (COM `IShellLink` for shortcuts), Avalonia + CommunityToolkit.Mvvm UI, xUnit. + +--- + +## File Structure + +**Installer (`src/ClaudeDo.Installer`)** +- Create: `Core/ShortcutFactory.cs` — shared `IShellLink` COM helper (`CreateShortcut`). +- Create: `Core/AutostartShortcut.cs` — install/remove the worker Startup-folder `.lnk`. +- Modify: `Steps/CreateShortcutsStep.cs` — use `ShortcutFactory`, drop embedded COM. +- Modify: `Steps/RegisterAutostartStep.cs` — Startup shortcut + legacy-task delete (no more task XML). +- Modify: `Steps/StartWorkerStep.cs` — `Process.Start` instead of `schtasks /Run`. +- Modify: `Steps/StopWorkerStep.cs` — drop `schtasks /End`. +- Modify: `Core/UninstallRunner.cs` — remove the Startup `.lnk`. +- Delete: `Core/ScheduledTaskXml.cs` (and its test). + +**App (`src/ClaudeDo.Ui`)** +- Create: `ViewModels/Modals/WorkerConnectionModalViewModel.cs`. +- Create: `Views/Modals/WorkerConnectionModalView.axaml` (+ `.axaml.cs`). +- Modify: `ViewModels/IslandsShellViewModel.cs` — remove auto-spawn; add hook, command, grace timer, decision gate. +- Modify: `Views/MainWindow.axaml.cs` — wire the new modal. +- Modify: `Views/MainWindow.axaml` — clickable status pill. + +**Tests** +- Modify: `tests/ClaudeDo.Installer.Tests/` — delete `ScheduledTaskXmlTests.cs`; add `ShortcutFactoryTests.cs`, `AutostartShortcutTests.cs`. +- Add: `tests/ClaudeDo.Ui.Tests/ConnectionPromptGateTests.cs`. + +--- + +## Task 1: ShortcutFactory (shared COM helper) + +**Files:** +- Create: `src/ClaudeDo.Installer/Core/ShortcutFactory.cs` +- Modify: `src/ClaudeDo.Installer/Steps/CreateShortcutsStep.cs` +- Test: `tests/ClaudeDo.Installer.Tests/ShortcutFactoryTests.cs` + +- [ ] **Step 1: Write the failing test** + +`tests/ClaudeDo.Installer.Tests/ShortcutFactoryTests.cs`: +```csharp +using System.IO; +using ClaudeDo.Installer.Core; + +namespace ClaudeDo.Installer.Tests; + +public class ShortcutFactoryTests +{ + [Fact] + public void CreateShortcut_writes_lnk_file() + { + var dir = Path.Combine(Path.GetTempPath(), "cdshortcut-" + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(dir); + try + { + var target = Path.Combine(dir, "fake.exe"); + File.WriteAllText(target, ""); + var lnk = Path.Combine(dir, "x.lnk"); + + ShortcutFactory.CreateShortcut(lnk, target, dir, "desc"); + + Assert.True(File.Exists(lnk)); + } + finally { Directory.Delete(dir, recursive: true); } + } +} +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `dotnet test tests/ClaudeDo.Installer.Tests --filter ShortcutFactoryTests` +Expected: FAIL — `ShortcutFactory` does not exist (compile error). + +- [ ] **Step 3: Create `ShortcutFactory` (move COM interop out of `CreateShortcutsStep`)** + +`src/ClaudeDo.Installer/Core/ShortcutFactory.cs`: +```csharp +using System.Runtime.InteropServices; +using System.Runtime.InteropServices.ComTypes; +using System.Text; + +namespace ClaudeDo.Installer.Core; + +public static class ShortcutFactory +{ + public static void CreateShortcut(string shortcutPath, string targetPath, string workingDir, string description) + { + var link = (IShellLink)new ShellLink(); + link.SetPath(targetPath); + link.SetWorkingDirectory(workingDir); + link.SetDescription(description); + link.SetIconLocation(targetPath, 0); + + var file = (IPersistFile)link; + file.Save(shortcutPath, false); + } + + [ComImport] + [Guid("00021401-0000-0000-C000-000000000046")] + private class ShellLink { } + + [ComImport] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + [Guid("000214F9-0000-0000-C000-000000000046")] + private interface IShellLink + { + void GetPath([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszFile, int cchMaxPath, IntPtr pfd, int fFlags); + void GetIDList(out IntPtr ppidl); + void SetIDList(IntPtr pidl); + void GetDescription([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszName, int cchMaxName); + void SetDescription([MarshalAs(UnmanagedType.LPWStr)] string pszName); + void GetWorkingDirectory([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszDir, int cchMaxPath); + void SetWorkingDirectory([MarshalAs(UnmanagedType.LPWStr)] string pszDir); + void GetArguments([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszArgs, int cchMaxPath); + void SetArguments([MarshalAs(UnmanagedType.LPWStr)] string pszArgs); + void GetHotkey(out short pwHotkey); + void SetHotkey(short wHotkey); + void GetShowCmd(out int piShowCmd); + void SetShowCmd(int iShowCmd); + void GetIconLocation([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszIconPath, int cchIconPath, out int piIcon); + void SetIconLocation([MarshalAs(UnmanagedType.LPWStr)] string pszIconPath, int iIcon); + void SetRelativePath([MarshalAs(UnmanagedType.LPWStr)] string pszPathRel, int dwReserved); + void Resolve(IntPtr hwnd, int fFlags); + void SetPath([MarshalAs(UnmanagedType.LPWStr)] string pszFile); + } +} +``` + +- [ ] **Step 4: Replace the embedded COM in `CreateShortcutsStep` with the helper** + +In `src/ClaudeDo.Installer/Steps/CreateShortcutsStep.cs`: delete the private `CreateShortcut` method and the entire `#region COM Interop for IShellLink` block (lines 47-90), remove the now-unused `using System.Runtime.InteropServices;`, `using System.Runtime.InteropServices.ComTypes;`, and `using System.Text;`. Replace the two `CreateShortcut(...)` call sites with `ShortcutFactory.CreateShortcut(...)`: +```csharp +ShortcutFactory.CreateShortcut(startMenuPath, appExe, workingDir, "ClaudeDo Task Manager"); +``` +```csharp +ShortcutFactory.CreateShortcut(desktopPath, appExe, workingDir, "ClaudeDo Task Manager"); +``` + +- [ ] **Step 5: Run tests to verify they pass** + +Run: `dotnet test tests/ClaudeDo.Installer.Tests --filter ShortcutFactoryTests` +Expected: PASS. + +- [ ] **Step 6: Commit** + +```bash +git add src/ClaudeDo.Installer/Core/ShortcutFactory.cs src/ClaudeDo.Installer/Steps/CreateShortcutsStep.cs tests/ClaudeDo.Installer.Tests/ShortcutFactoryTests.cs +git commit -m "refactor(installer): extract ShortcutFactory COM helper" +``` + +--- + +## Task 2: AutostartShortcut helper + +**Files:** +- Create: `src/ClaudeDo.Installer/Core/AutostartShortcut.cs` +- Test: `tests/ClaudeDo.Installer.Tests/AutostartShortcutTests.cs` + +- [ ] **Step 1: Write the failing tests** + +`tests/ClaudeDo.Installer.Tests/AutostartShortcutTests.cs`: +```csharp +using System.IO; +using ClaudeDo.Installer.Core; + +namespace ClaudeDo.Installer.Tests; + +public class AutostartShortcutTests +{ + private static string TempDir() + { + var dir = Path.Combine(Path.GetTempPath(), "cdautostart-" + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(dir); + return dir; + } + + [Fact] + public void Install_creates_lnk_with_expected_name() + { + var startup = TempDir(); + var workerDir = TempDir(); + try + { + var workerExe = Path.Combine(workerDir, "ClaudeDo.Worker.exe"); + File.WriteAllText(workerExe, ""); + + AutostartShortcut.Install(startup, workerExe); + + Assert.True(File.Exists(Path.Combine(startup, AutostartShortcut.FileName))); + } + finally { Directory.Delete(startup, true); Directory.Delete(workerDir, true); } + } + + [Fact] + public void Remove_deletes_existing_lnk() + { + var startup = TempDir(); + var workerDir = TempDir(); + try + { + var workerExe = Path.Combine(workerDir, "ClaudeDo.Worker.exe"); + File.WriteAllText(workerExe, ""); + AutostartShortcut.Install(startup, workerExe); + + AutostartShortcut.Remove(startup); + + Assert.False(File.Exists(Path.Combine(startup, AutostartShortcut.FileName))); + } + finally { Directory.Delete(startup, true); Directory.Delete(workerDir, true); } + } + + [Fact] + public void Remove_is_noop_when_missing() + { + var startup = TempDir(); + try { AutostartShortcut.Remove(startup); } // must not throw + finally { Directory.Delete(startup, true); } + } +} +``` + +- [ ] **Step 2: Run tests to verify they fail** + +Run: `dotnet test tests/ClaudeDo.Installer.Tests --filter AutostartShortcutTests` +Expected: FAIL — `AutostartShortcut` does not exist. + +- [ ] **Step 3: Create `AutostartShortcut`** + +`src/ClaudeDo.Installer/Core/AutostartShortcut.cs`: +```csharp +using System.IO; + +namespace ClaudeDo.Installer.Core; + +public static class AutostartShortcut +{ + public const string FileName = "ClaudeDo Worker.lnk"; + + public static string DefaultStartupDir => + Environment.GetFolderPath(Environment.SpecialFolder.Startup); + + public static string PathIn(string startupDir) => Path.Combine(startupDir, FileName); + + public static void Install(string startupDir, string workerExe) + { + Directory.CreateDirectory(startupDir); + var workingDir = Path.GetDirectoryName(workerExe) ?? startupDir; + ShortcutFactory.CreateShortcut(PathIn(startupDir), workerExe, workingDir, "ClaudeDo background worker"); + } + + public static void Remove(string startupDir) + { + var path = PathIn(startupDir); + if (File.Exists(path)) File.Delete(path); + } +} +``` + +- [ ] **Step 4: Run tests to verify they pass** + +Run: `dotnet test tests/ClaudeDo.Installer.Tests --filter AutostartShortcutTests` +Expected: PASS (3 tests). + +- [ ] **Step 5: Commit** + +```bash +git add src/ClaudeDo.Installer/Core/AutostartShortcut.cs tests/ClaudeDo.Installer.Tests/AutostartShortcutTests.cs +git commit -m "feat(installer): add AutostartShortcut helper for Startup-folder lnk" +``` + +--- + +## Task 3: RegisterAutostartStep → Startup shortcut + task migration + +**Files:** +- Modify: `src/ClaudeDo.Installer/Steps/RegisterAutostartStep.cs` +- Delete: `src/ClaudeDo.Installer/Core/ScheduledTaskXml.cs` +- Delete: `tests/ClaudeDo.Installer.Tests/ScheduledTaskXmlTests.cs` + +- [ ] **Step 1: Replace the step body** + +Rewrite `src/ClaudeDo.Installer/Steps/RegisterAutostartStep.cs` to: +```csharp +using System.IO; +using ClaudeDo.Installer.Core; + +namespace ClaudeDo.Installer.Steps; + +public sealed class RegisterAutostartStep : IInstallStep +{ + public const string LegacyTaskName = "ClaudeDoWorker"; + private const string LegacyServiceName = "ClaudeDoWorker"; + + public string Name => "Register Autostart"; + + public async Task ExecuteAsync(InstallContext ctx, IProgress progress, CancellationToken ct) + { + var workerExe = Path.Combine(ctx.InstallDirectory, "worker", "ClaudeDo.Worker.exe"); + if (!File.Exists(workerExe)) + return StepResult.Fail($"Worker executable not found: {workerExe}"); + + // 1) Migrate away the legacy Windows service if present. + progress.Report("Checking for legacy worker service..."); + var (queryExit, _) = await ProcessRunner.RunAsync("sc.exe", $"query {LegacyServiceName}", null, progress, ct); + if (queryExit == 0) + { + progress.Report("Removing legacy worker service..."); + await ProcessRunner.RunAsync("sc.exe", $"stop {LegacyServiceName}", null, progress, ct); + await ProcessRunner.RunAsync("sc.exe", $"delete {LegacyServiceName}", null, progress, ct); + for (var i = 0; i < 30; i++) + { + ct.ThrowIfCancellationRequested(); + var (q, _) = await ProcessRunner.RunAsync("sc.exe", $"query {LegacyServiceName}", null, progress, ct); + if (q != 0) break; + await Task.Delay(1000, ct); + } + } + + // 2) Migrate away the legacy logon scheduled task if present (best-effort). + progress.Report("Removing legacy logon task..."); + await ProcessRunner.RunAsync("schtasks.exe", $"/Delete /TN \"{LegacyTaskName}\" /F", null, progress, ct); + + // 3) Register per-user autostart via a Startup-folder shortcut. + progress.Report("Creating Startup shortcut..."); + try + { + AutostartShortcut.Install(AutostartShortcut.DefaultStartupDir, workerExe); + } + catch (Exception ex) + { + return StepResult.Fail($"Failed to create Startup shortcut: {ex.Message}"); + } + + return StepResult.Ok(); + } +} +``` + +- [ ] **Step 2: Delete the obsolete scheduled-task code and its test** + +Run: +```bash +git rm src/ClaudeDo.Installer/Core/ScheduledTaskXml.cs tests/ClaudeDo.Installer.Tests/ScheduledTaskXmlTests.cs +``` + +- [ ] **Step 3: Build the installer to verify it compiles** + +Run: `dotnet build src/ClaudeDo.Installer/ClaudeDo.Installer.csproj` +Expected: Build succeeded. (If `RegisterAutostartStep.TaskName` was referenced elsewhere, the build will flag it — Task 4 and Task 5 update those references; if the build fails only there, proceed to those tasks before re-running.) + +- [ ] **Step 4: Commit** + +```bash +git add src/ClaudeDo.Installer/Steps/RegisterAutostartStep.cs +git commit -m "feat(installer): register autostart via Startup shortcut, drop scheduled task" +``` + +--- + +## Task 4: StartWorkerStep + StopWorkerStep + +**Files:** +- Modify: `src/ClaudeDo.Installer/Steps/StartWorkerStep.cs` +- Modify: `src/ClaudeDo.Installer/Steps/StopWorkerStep.cs` + +- [ ] **Step 1: Rewrite `StartWorkerStep` to launch the exe directly** + +`src/ClaudeDo.Installer/Steps/StartWorkerStep.cs`: +```csharp +using System.Diagnostics; +using System.IO; +using ClaudeDo.Installer.Core; + +namespace ClaudeDo.Installer.Steps; + +public sealed class StartWorkerStep : IInstallStep +{ + public string Name => "Start Worker"; + + public Task ExecuteAsync(InstallContext ctx, IProgress progress, CancellationToken ct) + { + var workerExe = Path.Combine(ctx.InstallDirectory, "worker", "ClaudeDo.Worker.exe"); + if (!File.Exists(workerExe)) + return Task.FromResult(StepResult.Fail($"Worker executable not found: {workerExe}")); + + progress.Report("Starting worker..."); + try + { + Process.Start(new ProcessStartInfo(workerExe) { UseShellExecute = true }); + return Task.FromResult(StepResult.Ok()); + } + catch (Exception ex) + { + return Task.FromResult(StepResult.Fail($"Failed to start worker: {ex.Message}")); + } + } +} +``` + +- [ ] **Step 2: Drop the `schtasks /End` call in `StopWorkerStep`** + +In `src/ClaudeDo.Installer/Steps/StopWorkerStep.cs`, remove these two lines (the task no longer exists; the process kill below is the real stop): +```csharp + progress.Report("Stopping worker task (if running)..."); + await ProcessRunner.RunAsync("schtasks.exe", $"/End /TN \"{TaskName}\"", null, progress, ct); +``` +Keep the `public const string TaskName = "ClaudeDoWorker";` line — `UninstallRunner` still references it for legacy-task cleanup (Task 5). The method keeps its `async` modifier (it still has `await Task.CompletedTask;`). + +- [ ] **Step 3: Build to verify it compiles** + +Run: `dotnet build src/ClaudeDo.Installer/ClaudeDo.Installer.csproj` +Expected: Build succeeded. + +- [ ] **Step 4: Commit** + +```bash +git add src/ClaudeDo.Installer/Steps/StartWorkerStep.cs src/ClaudeDo.Installer/Steps/StopWorkerStep.cs +git commit -m "feat(installer): start worker via Process.Start, drop schtasks stop" +``` + +--- + +## Task 5: UninstallRunner removes the Startup shortcut + +**Files:** +- Modify: `src/ClaudeDo.Installer/Core/UninstallRunner.cs` + +- [ ] **Step 1: Add Startup `.lnk` removal** + +In `src/ClaudeDo.Installer/Core/UninstallRunner.cs`, the shortcut-removal block (step 4, around lines 53-60) currently removes the Desktop and Start Menu `.lnk`s. Add the Startup shortcut removal right after them: +```csharp + // 4) Remove shortcuts (best-effort — a stuck .lnk must not block the rest). + progress.Report("Removing shortcuts..."); + TryDeleteFile(Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.CommonDesktopDirectory), + "ClaudeDo.lnk")); + TryDeleteFile(Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.CommonStartMenu), + "Programs", "ClaudeDo.lnk")); + TryDeleteFile(AutostartShortcut.PathIn(AutostartShortcut.DefaultStartupDir)); +``` +The existing `schtasks /Delete /TN "{StopWorkerStep.TaskName}" /F` line (step 3) stays — it cleans up the legacy task on machines that still have it. + +- [ ] **Step 2: Build to verify it compiles** + +Run: `dotnet build src/ClaudeDo.Installer/ClaudeDo.Installer.csproj` +Expected: Build succeeded. + +- [ ] **Step 3: Commit** + +```bash +git add src/ClaudeDo.Installer/Core/UninstallRunner.cs +git commit -m "feat(installer): remove Startup worker shortcut on uninstall" +``` + +--- + +## Task 6: App stops auto-spawning the worker + +**Files:** +- Modify: `src/ClaudeDo.Ui/ViewModels/IslandsShellViewModel.cs` + +- [ ] **Step 1: Remove the auto-spawn call** + +In `src/ClaudeDo.Ui/ViewModels/IslandsShellViewModel.cs`, delete this line from the constructor (line 224): +```csharp + _ = EnsureWorkerRunningAsync(); +``` + +- [ ] **Step 2: Remove the `EnsureWorkerRunningAsync` method and its flag** + +Delete the `_ensureRunningAttempted` field (line 308) and the whole `EnsureWorkerRunningAsync` method (lines 310-320): +```csharp + private bool _ensureRunningAttempted; + + private async Task EnsureWorkerRunningAsync() + { + if (_ensureRunningAttempted) return; + _ensureRunningAttempted = true; + await Task.Delay(TimeSpan.FromSeconds(4)); + if (Worker?.IsConnected == true) return; + var exe = _workerLocator.Find(); + if (exe is null) return; + try { System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo(exe) { UseShellExecute = true }); } + catch { /* logon task is the primary mechanism; this is a convenience */ } + } +``` +Keep `RestartWorkerAsync` / `RestartWorkerService` (still used by the existing Restart button). `_workerLocator` stays in use (RestartWorkerService + Task 8). + +- [ ] **Step 3: Build to verify it compiles** + +Run: `dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj` +Expected: Build succeeded (no remaining references to `EnsureWorkerRunningAsync`). + +- [ ] **Step 4: Commit** + +```bash +git add src/ClaudeDo.Ui/ViewModels/IslandsShellViewModel.cs +git commit -m "refactor(ui): stop auto-spawning the worker on app start" +``` + +--- + +## Task 7: WorkerConnectionModal (VM + View) + +**Files:** +- Create: `src/ClaudeDo.Ui/ViewModels/Modals/WorkerConnectionModalViewModel.cs` +- Create: `src/ClaudeDo.Ui/Views/Modals/WorkerConnectionModalView.axaml` +- Create: `src/ClaudeDo.Ui/Views/Modals/WorkerConnectionModalView.axaml.cs` + +- [ ] **Step 1: Create the ViewModel** + +`src/ClaudeDo.Ui/ViewModels/Modals/WorkerConnectionModalViewModel.cs`: +```csharp +using System; +using System.Diagnostics; +using ClaudeDo.Ui.Services; +using CommunityToolkit.Mvvm.Input; + +namespace ClaudeDo.Ui.ViewModels.Modals; + +public sealed partial class WorkerConnectionModalViewModel : ViewModelBase +{ + private readonly WorkerLocator _workerLocator; + private readonly InstallerLocator _installerLocator; + + public WorkerConnectionModalViewModel(WorkerLocator workerLocator, InstallerLocator installerLocator) + { + _workerLocator = workerLocator; + _installerLocator = installerLocator; + } + + public Action? CloseAction { get; set; } + + [RelayCommand] private void Close() => CloseAction?.Invoke(); + + [RelayCommand] + private void StartWorker() + { + var exe = _workerLocator.Find(); + if (exe is null) return; + try { Process.Start(new ProcessStartInfo(exe) { UseShellExecute = true }); } + catch { /* nothing useful to show */ } + CloseAction?.Invoke(); + } + + [RelayCommand] + private void RerunInstaller() + { + var path = _installerLocator.Find(); + if (path is null) return; + try + { + Process.Start(new ProcessStartInfo(path) { UseShellExecute = true }); + Environment.Exit(0); + } + catch { /* nothing useful to show */ } + } +} +``` + +- [ ] **Step 2: Create the View (mirrors `AboutModalView` + `ModalShell`)** + +`src/ClaudeDo.Ui/Views/Modals/WorkerConnectionModalView.axaml`: +```xml + + + + + + + + + + +``` + +- [ ] **Step 3: Build the app** + +Run: `dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj` +Expected: Build succeeded. + +- [ ] **Step 4: Manual verification** + +Start the worker (or leave it stopped) and run the App: +- Worker stopped → after ~12s the "WORKER NOT REACHABLE" dialog appears once. **Start Worker** launches it (footer pill turns ONLINE); **Rerun Installer** launches the installer and exits; **Dismiss** closes and does not reappear automatically. +- Click the footer status pill anytime → the dialog reopens. +- Worker running before launch → no dialog appears. + +- [ ] **Step 5: Commit** + +```bash +git add src/ClaudeDo.Ui/Views/MainWindow.axaml.cs src/ClaudeDo.Ui/Views/MainWindow.axaml +git commit -m "feat(ui): wire worker connection modal and make status pill clickable" +``` + +--- + +## Task 10: Full build + test sweep + +- [ ] **Step 1: Build the touched projects** + +Run: +```bash +dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj +dotnet build src/ClaudeDo.Installer/ClaudeDo.Installer.csproj +``` +Expected: both Build succeeded. + +- [ ] **Step 2: Run the affected test suites** + +Run: +```bash +dotnet test tests/ClaudeDo.Installer.Tests +dotnet test tests/ClaudeDo.Ui.Tests +``` +Expected: all pass; no references to the deleted `ScheduledTaskXml`. + +- [ ] **Step 3: Final commit (if any stragglers)** + +```bash +git add -A +git commit -m "chore: worker lifecycle redesign cleanup" || echo "nothing to commit" +```