From 0f007c5367ceaac88d0ecbd22c05c965aa9c6001 Mon Sep 17 00:00:00 2001 From: mika kuns Date: Wed, 26 Aug 2026 13:55:51 +0200 Subject: [PATCH] refactor: drop the remaining single-implementation interfaces IBaseDirtyChecker, IInteractiveLaunchSpecService and the LaunchSpec wrapper had one implementation and one caller each; ProcessRunnerAdapter existed only to give a static class an interface, and InstallArtifactLocator used inheritance for two constructor arguments. The external MCP container now shares its singletons through a Share helper instead of 17 near-identical registrations. --- src/ClaudeDo.Installer/App.xaml.cs | 2 +- src/ClaudeDo.Installer/Core/ProcessRunner.cs | 9 ++- .../Core/ProcessRunnerAdapter.cs | 9 --- .../Services/InstallArtifactLocator.cs | 35 ++++------- .../External/ExternalMcpService.cs | 4 +- src/ClaudeDo.Worker/Git/BaseDirtyChecker.cs | 7 +-- .../Planning/Interfaces/ITerminalLauncher.cs | 2 +- .../Planning/WindowsTerminalLauncher.cs | 8 +-- src/ClaudeDo.Worker/Program.cs | 58 ++++++++--------- .../Runner/InteractiveLaunchSpecService.cs | 42 ++++++++++++- .../IInteractiveLaunchSpecService.cs | 62 ------------------- src/ClaudeDo.Worker/Runner/LaunchSpec.cs | 10 --- src/ClaudeDo.Worker/State/TaskStateService.cs | 4 +- .../Infrastructure/TaskStateServiceBuilder.cs | 2 +- 14 files changed, 103 insertions(+), 151 deletions(-) delete mode 100644 src/ClaudeDo.Installer/Core/ProcessRunnerAdapter.cs delete mode 100644 src/ClaudeDo.Worker/Runner/Interfaces/IInteractiveLaunchSpecService.cs delete mode 100644 src/ClaudeDo.Worker/Runner/LaunchSpec.cs diff --git a/src/ClaudeDo.Installer/App.xaml.cs b/src/ClaudeDo.Installer/App.xaml.cs index 8465d71c..e1cba588 100644 --- a/src/ClaudeDo.Installer/App.xaml.cs +++ b/src/ClaudeDo.Installer/App.xaml.cs @@ -128,7 +128,7 @@ public partial class App : Application // Environment checks — stateless, so their infrastructure is shared; ClaudeCliLookup is // rebuilt per EnvironmentCheckService instance so a re-check doesn't reuse a stale result. - sc.AddSingleton(); + sc.AddSingleton(); sc.AddSingleton(); sc.AddSingleton(); sc.AddSingleton(); diff --git a/src/ClaudeDo.Installer/Core/ProcessRunner.cs b/src/ClaudeDo.Installer/Core/ProcessRunner.cs index cbe94e0b..29a16307 100644 --- a/src/ClaudeDo.Installer/Core/ProcessRunner.cs +++ b/src/ClaudeDo.Installer/Core/ProcessRunner.cs @@ -2,10 +2,17 @@ using System.Diagnostics; using System.IO; using System.Text; +using ClaudeDo.Installer.Core.Interfaces; + namespace ClaudeDo.Installer.Core; -public static class ProcessRunner +public sealed class ProcessRunner : IProcessRunner { + /// The injectable, progress-less shape. Everything else calls the static overload directly. + public Task<(int ExitCode, string Output)> RunAsync( + string fileName, string arguments, string? workingDirectory, CancellationToken ct) + => RunAsync(fileName, arguments, workingDirectory, progress: null, ct); + public static async Task<(int ExitCode, string Output)> RunAsync( string fileName, string arguments, diff --git a/src/ClaudeDo.Installer/Core/ProcessRunnerAdapter.cs b/src/ClaudeDo.Installer/Core/ProcessRunnerAdapter.cs deleted file mode 100644 index d1ec89e8..00000000 --- a/src/ClaudeDo.Installer/Core/ProcessRunnerAdapter.cs +++ /dev/null @@ -1,9 +0,0 @@ -using ClaudeDo.Installer.Core.Interfaces; - -namespace ClaudeDo.Installer.Core; - -public sealed class ProcessRunnerAdapter : IProcessRunner -{ - public Task<(int ExitCode, string Output)> RunAsync(string fileName, string arguments, string? workingDirectory, CancellationToken ct) => - ProcessRunner.RunAsync(fileName, arguments, workingDirectory, progress: null, ct); -} diff --git a/src/ClaudeDo.Ui/Services/InstallArtifactLocator.cs b/src/ClaudeDo.Ui/Services/InstallArtifactLocator.cs index 7536da8d..73d22a76 100644 --- a/src/ClaudeDo.Ui/Services/InstallArtifactLocator.cs +++ b/src/ClaudeDo.Ui/Services/InstallArtifactLocator.cs @@ -1,29 +1,19 @@ namespace ClaudeDo.Ui.Services; -public sealed class InstallerLocator : InstallArtifactLocator -{ - protected override string Subdir => "uninstaller"; - protected override string ExeName => "ClaudeDo.Installer.exe"; -} +// Two named types because DI resolves them by type, not by key. +public sealed class InstallerLocator() : InstallArtifactLocator("uninstaller", "ClaudeDo.Installer.exe"); -public sealed class WorkerLocator : InstallArtifactLocator -{ - protected override string Subdir => "worker"; - protected override string ExeName => "ClaudeDo.Worker.exe"; -} +public sealed class WorkerLocator() : InstallArtifactLocator("worker", "ClaudeDo.Worker.exe"); /// /// Locates an executable inside a ClaudeDo install: walk up from the running /// directory to the folder containing install.json, otherwise read the -/// uninstall registry key. Subclasses supply the subdirectory and exe name. +/// uninstall registry key (which is what makes this work from a dev build). /// -public abstract class InstallArtifactLocator +public abstract class InstallArtifactLocator(string subdir, string exeName) { private const string InstallJson = "install.json"; - protected abstract string Subdir { get; } - protected abstract string ExeName { get; } - public string? Find() => FindByWalkingUp(AppContext.BaseDirectory) ?? (OperatingSystem.IsWindows() ? FindByRegistry() : null); @@ -34,10 +24,7 @@ public abstract class InstallArtifactLocator while (dir is not null) { if (File.Exists(Path.Combine(dir.FullName, InstallJson))) - { - var candidate = Path.Combine(dir.FullName, Subdir, ExeName); - return File.Exists(candidate) ? candidate : null; - } + return Candidate(dir.FullName); dir = dir.Parent; } return null; @@ -52,10 +39,14 @@ public abstract class InstallArtifactLocator using var key = Microsoft.Win32.Registry.LocalMachine .OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Uninstall\ClaudeDo"); var location = key?.GetValue("InstallLocation") as string; - if (string.IsNullOrEmpty(location)) return null; - var candidate = Path.Combine(location, Subdir, ExeName); - return File.Exists(candidate) ? candidate : null; + return string.IsNullOrEmpty(location) ? null : Candidate(location); } catch { return null; } } + + private string? Candidate(string installDir) + { + var path = Path.Combine(installDir, subdir, exeName); + return File.Exists(path) ? path : null; + } } diff --git a/src/ClaudeDo.Worker/External/ExternalMcpService.cs b/src/ClaudeDo.Worker/External/ExternalMcpService.cs index 96a81b09..e858a1fc 100644 --- a/src/ClaudeDo.Worker/External/ExternalMcpService.cs +++ b/src/ClaudeDo.Worker/External/ExternalMcpService.cs @@ -202,7 +202,7 @@ public sealed class ExternalMcpService private readonly WorktreeMaintenanceService _maintenance; private readonly TaskMergeService _merge; private readonly PlanningMergeOrchestrator _planningMerge; - private readonly IBaseDirtyChecker _baseDirtyChecker; + private readonly BaseDirtyChecker _baseDirtyChecker; public ExternalMcpService( TaskRepository tasks, @@ -215,7 +215,7 @@ public sealed class ExternalMcpService WorktreeMaintenanceService maintenance, TaskMergeService merge, PlanningMergeOrchestrator planningMerge, - IBaseDirtyChecker baseDirtyChecker) + BaseDirtyChecker baseDirtyChecker) { _tasks = tasks; _lists = lists; diff --git a/src/ClaudeDo.Worker/Git/BaseDirtyChecker.cs b/src/ClaudeDo.Worker/Git/BaseDirtyChecker.cs index 9b7744a1..caa1e806 100644 --- a/src/ClaudeDo.Worker/Git/BaseDirtyChecker.cs +++ b/src/ClaudeDo.Worker/Git/BaseDirtyChecker.cs @@ -13,12 +13,7 @@ public sealed record DirtyBaseWarning(int ModifiedCount, int UntrackedCount); /// files. Non-blocking by design: callers surface the counts as a heads-up, never refuse the /// transition. /// -public interface IBaseDirtyChecker -{ - Task CheckAsync(string? workingDir, CancellationToken ct); -} - -public sealed class BaseDirtyChecker : IBaseDirtyChecker +public sealed class BaseDirtyChecker { // Long enough to collapse every task in one batch_update_task_status call over the same // list into a single `git status` invocation; short enough that a later, independent queue diff --git a/src/ClaudeDo.Worker/Planning/Interfaces/ITerminalLauncher.cs b/src/ClaudeDo.Worker/Planning/Interfaces/ITerminalLauncher.cs index 7b6eaad4..3ef030ba 100644 --- a/src/ClaudeDo.Worker/Planning/Interfaces/ITerminalLauncher.cs +++ b/src/ClaudeDo.Worker/Planning/Interfaces/ITerminalLauncher.cs @@ -2,7 +2,7 @@ namespace ClaudeDo.Worker.Planning; // Launches the Claude CLI in a visible terminal for human-driven planning sessions. // Not used for headless task execution (that path is ClaudeProcess, prompt over stdin) -// nor for embedded ConPTY interactive sessions (those use IInteractiveLaunchSpecService). +// nor for embedded ConPTY interactive sessions (those use InteractiveLaunchSpecService). public interface ITerminalLauncher { Task LaunchPlanningStartAsync(PlanningSessionStartContext ctx, CancellationToken cancellationToken); diff --git a/src/ClaudeDo.Worker/Planning/WindowsTerminalLauncher.cs b/src/ClaudeDo.Worker/Planning/WindowsTerminalLauncher.cs index cb20f4aa..a4a848bf 100644 --- a/src/ClaudeDo.Worker/Planning/WindowsTerminalLauncher.cs +++ b/src/ClaudeDo.Worker/Planning/WindowsTerminalLauncher.cs @@ -27,7 +27,7 @@ namespace ClaudeDo.Worker.Planning; // Spawns the Claude CLI inside a visible Windows Terminal window for an interactive // planning session (start/resume). Headless task execution does NOT come through here — // that path is ClaudeProcess (prompt over stdin, no terminal) — nor does an embedded -// ConPTY interactive session (IInteractiveLaunchSpecService), which reuses this class's +// ConPTY interactive session (InteractiveLaunchSpecService), which reuses this class's // arg-building helpers (BuildResumeArgs, BuildPlanningStart/ResumeArgs) for a bare // Exe/Args pair instead of a wrapped pwsh command line. public sealed class WindowsTerminalLauncher : ITerminalLauncher @@ -86,7 +86,7 @@ public sealed class WindowsTerminalLauncher : ITerminalLauncher return Task.CompletedTask; } - // The raw claude CLI args for a --resume launch, shared with IInteractiveLaunchSpecService + // The raw claude CLI args for a --resume launch, shared with InteractiveLaunchSpecService // (which needs the bare Exe/Args pair for a ConPTY host, not a wrapped pwsh command line). internal static IReadOnlyList BuildResumeArgs(string claudeSessionId) => new[] { "--resume", claudeSessionId }; @@ -99,7 +99,7 @@ public sealed class WindowsTerminalLauncher : ITerminalLauncher BuildPwshCommand(claudePath, BuildPlanningStartArgs(ctx)); // The raw claude CLI args for an interactive planning START, shared with the embedded-ConPTY - // planning path (IInteractiveLaunchSpecService), which needs the bare Exe/Args pair rather than + // planning path (InteractiveLaunchSpecService), which needs the bare Exe/Args pair rather than // a pwsh-wrapped command line. Arg order matters: variadic flags (--allowedTools, --add-dir) // come first; the single-line kickoff prompt is positional, so it must follow a single-value // flag (--append-system-prompt-file) or a variadic flag would swallow it. @@ -176,7 +176,7 @@ public sealed class WindowsTerminalLauncher : ITerminalLauncher ?? throw new TerminalLaunchException("Failed to start Windows Terminal process."); } - // Not private: reused by IInteractiveLaunchSpecService to resolve the claude executable + // Not private: reused by InteractiveLaunchSpecService to resolve the claude executable // for a ConPTY-hosted launch spec the same way a wt-launched terminal resolves it. internal static string? Resolve(string pathOrName) { diff --git a/src/ClaudeDo.Worker/Program.cs b/src/ClaudeDo.Worker/Program.cs index 131778fa..77330665 100644 --- a/src/ClaudeDo.Worker/Program.cs +++ b/src/ClaudeDo.Worker/Program.cs @@ -89,7 +89,7 @@ builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); -builder.Services.AddSingleton(); +builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); @@ -112,7 +112,7 @@ builder.Services.AddSingleton>(sp => () => sp.GetRequire // PlanningMergeOrchestrator itself depends on ITaskStateService, so TaskStateService can only // reach it lazily (Func) — same cycle-breaking shape as the Func above. builder.Services.AddSingleton>(sp => () => sp.GetRequiredService()); -builder.Services.AddSingleton(); +builder.Services.AddSingleton(); builder.Services.AddSingleton(sp => new TaskStateService( sp.GetRequiredService>(), sp.GetRequiredService(), @@ -120,7 +120,7 @@ builder.Services.AddSingleton(sp => new TaskStateService( sp.GetRequiredService(), sp.GetRequiredService(), sp.GetRequiredService>(), - sp.GetRequiredService(), + sp.GetRequiredService(), sp.GetRequiredService>())); // Agent file management. @@ -201,12 +201,6 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddMcpServer() - // Stateless: no Mcp-Session-Id, so a worker restart doesn't 404 the sessions that - // outlive it (ConPTY tiles in the UI process, externally registered claude sessions), - // and a 2026-07-28 client isn't refused back to the initialize handshake. Nothing here - // needs the stateful-only features (sampling, elicitation, resource subscriptions, - // unsolicited notifications); in-tool progress works in both modes. Implies no legacy - // SSE endpoint — setting EnableLegacySse alongside this throws at startup. .WithHttpTransport(o => o.Stateless = true) .WithTools() .WithTools() @@ -308,27 +302,38 @@ WebApplication? externalApp = null; if (cfg.ExternalMcpPort > 0) { var externalBuilder = WebApplication.CreateBuilder(); + // Hand the second container the SAME singleton instances, so both apps operate on one + // runtime state instead of two parallel copies of the queue/git/merge stack. + void Share() where T : class => + externalBuilder.Services.AddSingleton(app.Services.GetRequiredService()); + externalBuilder.Services.AddSingleton(cfg); - externalBuilder.Services.AddSingleton(app.Services.GetRequiredService()); - externalBuilder.Services.AddSingleton(app.Services.GetRequiredService()); - externalBuilder.Services.AddSingleton(app.Services.GetRequiredService()); - externalBuilder.Services.AddSingleton(app.Services.GetRequiredService>()); - externalBuilder.Services.AddSingleton(app.Services.GetRequiredService()); - externalBuilder.Services.AddSingleton(app.Services.GetRequiredService()); + Share(); + Share(); + Share(); + Share>(); + Share(); + Share(); + Share(); + Share(); + Share(); + Share(); + Share(); + Share(); + Share(); + Share(); + Share(); + Share(); + Share(); + externalBuilder.Services.AddScoped(sp => sp.GetRequiredService>().CreateDbContext()); externalBuilder.Services.AddScoped(); externalBuilder.Services.AddScoped(); externalBuilder.Services.AddScoped(); - externalBuilder.Services.AddSingleton(app.Services.GetRequiredService()); - externalBuilder.Services.AddSingleton(app.Services.GetRequiredService()); - externalBuilder.Services.AddSingleton(app.Services.GetRequiredService()); - externalBuilder.Services.AddSingleton(app.Services.GetRequiredService()); - externalBuilder.Services.AddSingleton(app.Services.GetRequiredService()); - externalBuilder.Services.AddSingleton(app.Services.GetRequiredService()); - externalBuilder.Services.AddSingleton(app.Services.GetRequiredService()); - externalBuilder.Services.AddSingleton(app.Services.GetRequiredService()); - externalBuilder.Services.AddSingleton(app.Services.GetRequiredService()); + externalBuilder.Services.AddScoped(); + externalBuilder.Services.AddSingleton(); + externalBuilder.Services.AddScoped(); externalBuilder.Services.AddScoped(); externalBuilder.Services.AddScoped(); externalBuilder.Services.AddScoped(); @@ -336,16 +341,11 @@ if (cfg.ExternalMcpPort > 0) externalBuilder.Services.AddScoped(); externalBuilder.Services.AddScoped(); externalBuilder.Services.AddScoped(); - externalBuilder.Services.AddSingleton(); externalBuilder.Services.AddScoped(); externalBuilder.Services.AddScoped(); externalBuilder.Services.AddScoped(); externalBuilder.Services.AddScoped(); - externalBuilder.Services.AddSingleton(app.Services.GetRequiredService()); - externalBuilder.Services.AddScoped(); externalBuilder.Services.AddScoped(); - externalBuilder.Services.AddSingleton(app.Services.GetRequiredService()); - externalBuilder.Services.AddScoped(); externalBuilder.Services.AddScoped(); externalBuilder.Services.AddMcpServer() .WithHttpTransport(o => o.Stateless = true) diff --git a/src/ClaudeDo.Worker/Runner/InteractiveLaunchSpecService.cs b/src/ClaudeDo.Worker/Runner/InteractiveLaunchSpecService.cs index 2929966b..d9548dec 100644 --- a/src/ClaudeDo.Worker/Runner/InteractiveLaunchSpecService.cs +++ b/src/ClaudeDo.Worker/Runner/InteractiveLaunchSpecService.cs @@ -29,7 +29,7 @@ namespace ClaudeDo.Worker.Runner; // hand. The always-on `mcp__claudedo__*` tools remain available via the globally-registered // MCP server (installer's RegisterMcpStep), exactly as they already are for a plain // `--resume` pickup in a Windows Terminal window. -public sealed class InteractiveLaunchSpecService : IInteractiveLaunchSpecService +public sealed class InteractiveLaunchSpecService { // Claude Code caps HTTP MCP tool calls at 60s unless raised; every ConPTY spec built by this // service lifts it well past wait_for_task_change's 900s server-side cap. Keep in sync with @@ -59,6 +59,16 @@ public sealed class InteractiveLaunchSpecService : IInteractiveLaunchSpecService _claudePath = cfg.ClaudeBin; } + /// Builds a LaunchSpec for opening an interactive session in a task's worktree. + /// Throws KeyNotFoundException if the task doesn't exist, InvalidOperationException + /// if it's Running/Queued. If the task has no usable worktree yet, one is created on + /// demand (same mechanism as an autonomous run) provided the task's list has a working + /// directory pointing at a git repo -- otherwise throws InvalidOperationException. Resumes + /// (--resume) this task's own last interactive session (TaskEntity.InteractiveSessionId) if + /// it has one, else the latest autonomous run's session; a task that has never run either + /// way, or whose worktree was just created fresh, gets a fresh-start spec instead -- pre- + /// assigned a new session id via --session-id and persisted to InteractiveSessionId before + /// launch, so a closed/aborted session can be resumed next time. public async Task BuildForTaskAsync(string taskId, CancellationToken ct) { await using var ctx = await _dbFactory.CreateDbContextAsync(ct); @@ -172,6 +182,9 @@ public sealed class InteractiveLaunchSpecService : IInteractiveLaunchSpecService return new LaunchSpec(sessionDir, resolvedClaude, args, env); } + /// Maps an already-prepared planning START context (worktree + prompt files + token, + /// produced by PlanningSessionManager.StartAsync) into a LaunchSpec for an embedded ConPTY + /// planning session — same planning CLI args as the wt launcher, planning env carried in Env. public LaunchSpec BuildPlanningStart(PlanningSessionStartContext ctx) { var resolvedClaude = WindowsTerminalLauncher.Resolve(_claudePath) @@ -194,6 +207,8 @@ public sealed class InteractiveLaunchSpecService : IInteractiveLaunchSpecService env); } + /// Maps a planning RESUME context (from PlanningSessionManager.ResumeAsync) into a + /// LaunchSpec for an embedded ConPTY planning session (--permission-mode plan --resume). public LaunchSpec BuildPlanningResume(PlanningSessionResumeContext ctx) { var resolvedClaude = WindowsTerminalLauncher.Resolve(_claudePath) @@ -212,6 +227,9 @@ public sealed class InteractiveLaunchSpecService : IInteractiveLaunchSpecService env); } + /// Builds a LaunchSpec for an ad-hoc interactive session in an arbitrary directory -- + /// no task, no worktree, no session-skills seeding. Throws InvalidOperationException if the + /// directory doesn't exist. public async Task BuildForDirectoryAsync(string directory, CancellationToken ct) { if (!Directory.Exists(directory)) @@ -241,6 +259,14 @@ public sealed class InteractiveLaunchSpecService : IInteractiveLaunchSpecService private const string MergeHelperAllowedTools = "mcp__claudedo__*,Read,Grep,Glob,Edit,Bash,WebFetch,WebSearch,Skill,Task"; + /// Builds a LaunchSpec for an embedded ConPTY "merge helper" session that drives the + /// given tasks to a merged/Done state via the mcp__claudedo__* tools. Writes a per-session + /// system prompt + task brief under ~/.todo-app/merge-helper-sessions/<guid> and exposes + /// that dir plus the list's repo dir via --add-dir. cwd is the list's working directory. + /// handlerTaskId (the id returned by CreateMergeHelperTaskAsync) is rendered into the brief so + /// the session can call handoff_list_handler/submit_task_for_review on its own handler task. + /// Throws KeyNotFoundException if the list doesn't exist; InvalidOperationException if + /// taskIds is empty or the list has no existing working directory. public async Task BuildForMergeHelperAsync(IReadOnlyList taskIds, string listId, string handlerTaskId, CancellationToken ct) { if (taskIds.Count == 0) @@ -322,6 +348,15 @@ public sealed class InteractiveLaunchSpecService : IInteractiveLaunchSpecService // instructions for the role it is actually about to run, never the Triage dedupe/enhance ones. // Writes a fresh handoff kickoff file in a NEW session dir -- the old ConPTY tile keeps running // against its own session-dir files untouched. + /// Builds a LaunchSpec for the fresh ConPTY session a merge-helper run hands off to for + /// the given phase -- reuses the SAME handler task id (no new task, HandlerBaseCommit untouched), + /// writing only a fresh handoff kickoff naming the surviving tasks. nextPhase selects both the + /// system prompt and the model: "wait"/"wait_final" -> MergeHelperWait + HandlerWaitAlias, + /// "merge"/"merge_final" -> MergeHelperMerge + HandlerMergeAlias; the "_final" variants render an + /// extra line in the handoff kickoff marking the final round and forbidding further reruns. Throws + /// ArgumentException for an unrecognized nextPhase; KeyNotFoundException if the task/list doesn't + /// exist; InvalidOperationException if survivingTaskIds is empty or the list has no working + /// directory. public async Task BuildForMergeHelperHandoffAsync( string taskId, IReadOnlyList survivingTaskIds, string nextPhase, CancellationToken ct) { @@ -450,6 +485,11 @@ public sealed class InteractiveLaunchSpecService : IInteractiveLaunchSpecService // IsManual=true so the queue picker, daily prep, and the "send to queue"/"refine" UI // affordances all skip it, matching the "reminder only a human/ConPTY session can act on" // semantics IsManual already carries elsewhere; the ConPTY session itself is still allowed. + /// Creates the ClaudeDo task that hosts a list-handler run (Mission Control's + /// "Let Claude handle it") and stamps the list repo's current HEAD as the review range's + /// base commit (see TaskEntity.HandlerBaseCommit). Returns the new task's id. Throws + /// KeyNotFoundException if the list doesn't exist; InvalidOperationException if taskIds + /// is empty or the list has no existing working directory. public async Task CreateMergeHelperTaskAsync( IReadOnlyList taskIds, string listId, string title, string descriptionHeader, CancellationToken ct) { diff --git a/src/ClaudeDo.Worker/Runner/Interfaces/IInteractiveLaunchSpecService.cs b/src/ClaudeDo.Worker/Runner/Interfaces/IInteractiveLaunchSpecService.cs deleted file mode 100644 index b1aa56b9..00000000 --- a/src/ClaudeDo.Worker/Runner/Interfaces/IInteractiveLaunchSpecService.cs +++ /dev/null @@ -1,62 +0,0 @@ -using ClaudeDo.Worker.Planning; - -namespace ClaudeDo.Worker.Runner; - -public interface IInteractiveLaunchSpecService -{ - /// Maps an already-prepared planning START context (worktree + prompt files + token, - /// produced by PlanningSessionManager.StartAsync) into a LaunchSpec for an embedded ConPTY - /// planning session — same planning CLI args as the wt launcher, planning env carried in Env. - LaunchSpec BuildPlanningStart(PlanningSessionStartContext ctx); - - /// Maps a planning RESUME context (from PlanningSessionManager.ResumeAsync) into a - /// LaunchSpec for an embedded ConPTY planning session (--permission-mode plan --resume). - LaunchSpec BuildPlanningResume(PlanningSessionResumeContext ctx); - - /// Builds a LaunchSpec for opening an interactive session in a task's worktree. - /// Throws KeyNotFoundException if the task doesn't exist, InvalidOperationException - /// if it's Running/Queued. If the task has no usable worktree yet, one is created on - /// demand (same mechanism as an autonomous run) provided the task's list has a working - /// directory pointing at a git repo -- otherwise throws InvalidOperationException. Resumes - /// (--resume) this task's own last interactive session (TaskEntity.InteractiveSessionId) if - /// it has one, else the latest autonomous run's session; a task that has never run either - /// way, or whose worktree was just created fresh, gets a fresh-start spec instead -- pre- - /// assigned a new session id via --session-id and persisted to InteractiveSessionId before - /// launch, so a closed/aborted session can be resumed next time. - Task BuildForTaskAsync(string taskId, CancellationToken ct); - - /// Builds a LaunchSpec for an ad-hoc interactive session in an arbitrary directory -- - /// no task, no worktree, no session-skills seeding. Throws InvalidOperationException if the - /// directory doesn't exist. - Task BuildForDirectoryAsync(string directory, CancellationToken ct); - - /// Builds a LaunchSpec for an embedded ConPTY "merge helper" session that drives the - /// given tasks to a merged/Done state via the mcp__claudedo__* tools. Writes a per-session - /// system prompt + task brief under ~/.todo-app/merge-helper-sessions/<guid> and exposes - /// that dir plus the list's repo dir via --add-dir. cwd is the list's working directory. - /// handlerTaskId (the id returned by CreateMergeHelperTaskAsync) is rendered into the brief so - /// the session can call handoff_list_handler/submit_task_for_review on its own handler task. - /// Throws KeyNotFoundException if the list doesn't exist; InvalidOperationException if - /// taskIds is empty or the list has no existing working directory. - Task BuildForMergeHelperAsync(IReadOnlyList taskIds, string listId, string handlerTaskId, CancellationToken ct); - - /// Creates the ClaudeDo task that hosts a list-handler run (Mission Control's - /// "Let Claude handle it") and stamps the list repo's current HEAD as the review range's - /// base commit (see TaskEntity.HandlerBaseCommit). Returns the new task's id. Throws - /// KeyNotFoundException if the list doesn't exist; InvalidOperationException if taskIds - /// is empty or the list has no existing working directory. - Task CreateMergeHelperTaskAsync( - IReadOnlyList taskIds, string listId, string title, string descriptionHeader, CancellationToken ct); - - /// Builds a LaunchSpec for the fresh ConPTY session a merge-helper run hands off to for - /// the given phase -- reuses the SAME handler task id (no new task, HandlerBaseCommit untouched), - /// writing only a fresh handoff kickoff naming the surviving tasks. nextPhase selects both the - /// system prompt and the model: "wait"/"wait_final" -> MergeHelperWait + HandlerWaitAlias, - /// "merge"/"merge_final" -> MergeHelperMerge + HandlerMergeAlias; the "_final" variants render an - /// extra line in the handoff kickoff marking the final round and forbidding further reruns. Throws - /// ArgumentException for an unrecognized nextPhase; KeyNotFoundException if the task/list doesn't - /// exist; InvalidOperationException if survivingTaskIds is empty or the list has no working - /// directory. - Task BuildForMergeHelperHandoffAsync( - string taskId, IReadOnlyList survivingTaskIds, string nextPhase, CancellationToken ct); -} diff --git a/src/ClaudeDo.Worker/Runner/LaunchSpec.cs b/src/ClaudeDo.Worker/Runner/LaunchSpec.cs deleted file mode 100644 index 171dd387..00000000 --- a/src/ClaudeDo.Worker/Runner/LaunchSpec.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace ClaudeDo.Worker.Runner; - -// What an embedded ConPTY terminal (UI process) needs to start a real `claude` process for -// a task's worktree, with the same setup as an autonomous run (session-skills seeded onto -// disk, the same run environment variables) plus the --resume-vs-fresh-start choice. -public sealed record LaunchSpec( - string Cwd, - string Exe, - IReadOnlyList Args, - IReadOnlyDictionary Env); diff --git a/src/ClaudeDo.Worker/State/TaskStateService.cs b/src/ClaudeDo.Worker/State/TaskStateService.cs index 7193e985..2a683bb8 100644 --- a/src/ClaudeDo.Worker/State/TaskStateService.cs +++ b/src/ClaudeDo.Worker/State/TaskStateService.cs @@ -18,7 +18,7 @@ public sealed class TaskStateService : ITaskStateService private readonly PlanningChainCoordinator _chain; private readonly RunCancellationRegistry _runCancels; private readonly Func _mergeState; - private readonly IBaseDirtyChecker _baseDirtyChecker; + private readonly BaseDirtyChecker _baseDirtyChecker; private readonly ILogger _logger; public TaskStateService( @@ -28,7 +28,7 @@ public sealed class TaskStateService : ITaskStateService PlanningChainCoordinator chain, RunCancellationRegistry runCancels, Func mergeState, - IBaseDirtyChecker baseDirtyChecker, + BaseDirtyChecker baseDirtyChecker, ILogger logger) { _dbFactory = dbFactory; diff --git a/tests/ClaudeDo.Worker.Tests/Infrastructure/TaskStateServiceBuilder.cs b/tests/ClaudeDo.Worker.Tests/Infrastructure/TaskStateServiceBuilder.cs index 845356f3..d9595326 100644 --- a/tests/ClaudeDo.Worker.Tests/Infrastructure/TaskStateServiceBuilder.cs +++ b/tests/ClaudeDo.Worker.Tests/Infrastructure/TaskStateServiceBuilder.cs @@ -24,7 +24,7 @@ public static class TaskStateServiceBuilder public static Built Build( IDbContextFactory dbFactory, Func? mergeState = null, - IBaseDirtyChecker? baseDirtyChecker = null) + BaseDirtyChecker? baseDirtyChecker = null) { var hub = new CapturingHubContext(); var broadcaster = new HubBroadcaster(hub);