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<T> helper instead of 17 near-identical
registrations.
This commit is contained in:
mika kuns
2026-08-26 13:55:51 +02:00
parent c593be2f02
commit 0f007c5367
14 changed files with 103 additions and 151 deletions
@@ -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;
}
/// <summary>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.</summary>
public async Task<LaunchSpec> 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);
}
/// <summary>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.</summary>
public LaunchSpec BuildPlanningStart(PlanningSessionStartContext ctx)
{
var resolvedClaude = WindowsTerminalLauncher.Resolve(_claudePath)
@@ -194,6 +207,8 @@ public sealed class InteractiveLaunchSpecService : IInteractiveLaunchSpecService
env);
}
/// <summary>Maps a planning RESUME context (from PlanningSessionManager.ResumeAsync) into a
/// LaunchSpec for an embedded ConPTY planning session (--permission-mode plan --resume).</summary>
public LaunchSpec BuildPlanningResume(PlanningSessionResumeContext ctx)
{
var resolvedClaude = WindowsTerminalLauncher.Resolve(_claudePath)
@@ -212,6 +227,9 @@ public sealed class InteractiveLaunchSpecService : IInteractiveLaunchSpecService
env);
}
/// <summary>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.</summary>
public async Task<LaunchSpec> 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";
/// <summary>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/&lt;guid&gt; 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.</summary>
public async Task<LaunchSpec> BuildForMergeHelperAsync(IReadOnlyList<string> 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.
/// <summary>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.</summary>
public async Task<LaunchSpec> BuildForMergeHelperHandoffAsync(
string taskId, IReadOnlyList<string> 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.
/// <summary>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.</summary>
public async Task<string> CreateMergeHelperTaskAsync(
IReadOnlyList<string> taskIds, string listId, string title, string descriptionHeader, CancellationToken ct)
{