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:
@@ -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<IProcessRunner, ProcessRunnerAdapter>();
|
||||
sc.AddSingleton<IProcessRunner, ProcessRunner>();
|
||||
sc.AddSingleton<IProcessLauncher, ProcessLauncher>();
|
||||
sc.AddSingleton<IPortOwnerResolver, NetstatPortOwnerResolver>();
|
||||
sc.AddSingleton<ClaudeHelpLauncher>();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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");
|
||||
|
||||
/// <summary>
|
||||
/// 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).
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
/// </summary>
|
||||
public interface IBaseDirtyChecker
|
||||
{
|
||||
Task<DirtyBaseWarning?> 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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<string> 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)
|
||||
{
|
||||
|
||||
@@ -89,7 +89,7 @@ builder.Services.AddSingleton<PendingQuestionRegistry>();
|
||||
builder.Services.AddSingleton<IRepoCloner, GitRepoCloner>();
|
||||
builder.Services.AddSingleton<ISessionSkillRegistry, SessionSkillRegistry>();
|
||||
builder.Services.AddSingleton<ISessionSkillSeeder, SessionSkillSeeder>();
|
||||
builder.Services.AddSingleton<IInteractiveLaunchSpecService, InteractiveLaunchSpecService>();
|
||||
builder.Services.AddSingleton<InteractiveLaunchSpecService>();
|
||||
builder.Services.AddSingleton<TaskRunner>();
|
||||
builder.Services.AddSingleton<WorktreeMaintenanceService>();
|
||||
builder.Services.AddSingleton<TaskResetService>();
|
||||
@@ -112,7 +112,7 @@ builder.Services.AddSingleton<Func<ITaskStateService>>(sp => () => sp.GetRequire
|
||||
// PlanningMergeOrchestrator itself depends on ITaskStateService, so TaskStateService can only
|
||||
// reach it lazily (Func<IActiveMergeState>) — same cycle-breaking shape as the Func above.
|
||||
builder.Services.AddSingleton<Func<IActiveMergeState>>(sp => () => sp.GetRequiredService<PlanningMergeOrchestrator>());
|
||||
builder.Services.AddSingleton<IBaseDirtyChecker, BaseDirtyChecker>();
|
||||
builder.Services.AddSingleton<BaseDirtyChecker>();
|
||||
builder.Services.AddSingleton<ITaskStateService>(sp => new TaskStateService(
|
||||
sp.GetRequiredService<IDbContextFactory<ClaudeDoDbContext>>(),
|
||||
sp.GetRequiredService<HubBroadcaster>(),
|
||||
@@ -120,7 +120,7 @@ builder.Services.AddSingleton<ITaskStateService>(sp => new TaskStateService(
|
||||
sp.GetRequiredService<PlanningChainCoordinator>(),
|
||||
sp.GetRequiredService<RunCancellationRegistry>(),
|
||||
sp.GetRequiredService<Func<IActiveMergeState>>(),
|
||||
sp.GetRequiredService<IBaseDirtyChecker>(),
|
||||
sp.GetRequiredService<BaseDirtyChecker>(),
|
||||
sp.GetRequiredService<ILogger<TaskStateService>>()));
|
||||
|
||||
// Agent file management.
|
||||
@@ -201,12 +201,6 @@ builder.Services.AddScoped<PlanningMcpService>();
|
||||
builder.Services.AddScoped<FindingsStoreLocator>();
|
||||
builder.Services.AddScoped<TaskRunFindingsMcpTools>();
|
||||
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<PlanningMcpService>()
|
||||
.WithTools<TaskRunMcpService>()
|
||||
@@ -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<T>() where T : class =>
|
||||
externalBuilder.Services.AddSingleton(app.Services.GetRequiredService<T>());
|
||||
|
||||
externalBuilder.Services.AddSingleton(cfg);
|
||||
externalBuilder.Services.AddSingleton(app.Services.GetRequiredService<HubBroadcaster>());
|
||||
externalBuilder.Services.AddSingleton(app.Services.GetRequiredService<QueueService>());
|
||||
externalBuilder.Services.AddSingleton(app.Services.GetRequiredService<OverrideSlotService>());
|
||||
externalBuilder.Services.AddSingleton(app.Services.GetRequiredService<IDbContextFactory<ClaudeDoDbContext>>());
|
||||
externalBuilder.Services.AddSingleton(app.Services.GetRequiredService<ITaskStateService>());
|
||||
externalBuilder.Services.AddSingleton(app.Services.GetRequiredService<IQueueWaker>());
|
||||
Share<HubBroadcaster>();
|
||||
Share<QueueService>();
|
||||
Share<OverrideSlotService>();
|
||||
Share<IDbContextFactory<ClaudeDoDbContext>>();
|
||||
Share<ITaskStateService>();
|
||||
Share<IQueueWaker>();
|
||||
Share<WorktreeManager>();
|
||||
Share<AgentFileService>();
|
||||
Share<TaskResetService>();
|
||||
Share<GitService>();
|
||||
Share<BaseDirtyChecker>();
|
||||
Share<WorktreeMaintenanceService>();
|
||||
Share<TaskMergeService>();
|
||||
Share<PlanningMergeOrchestrator>();
|
||||
Share<InteractiveReviewSubmissionService>();
|
||||
Share<AttachmentStore>();
|
||||
Share<FindingsStore>();
|
||||
|
||||
externalBuilder.Services.AddScoped<ClaudeDoDbContext>(sp =>
|
||||
sp.GetRequiredService<IDbContextFactory<ClaudeDoDbContext>>().CreateDbContext());
|
||||
externalBuilder.Services.AddScoped<TaskRepository>();
|
||||
externalBuilder.Services.AddScoped<ListRepository>();
|
||||
externalBuilder.Services.AddScoped<TaskRunRepository>();
|
||||
externalBuilder.Services.AddSingleton(app.Services.GetRequiredService<WorktreeManager>());
|
||||
externalBuilder.Services.AddSingleton(app.Services.GetRequiredService<AgentFileService>());
|
||||
externalBuilder.Services.AddSingleton(app.Services.GetRequiredService<TaskResetService>());
|
||||
externalBuilder.Services.AddSingleton(app.Services.GetRequiredService<GitService>());
|
||||
externalBuilder.Services.AddSingleton(app.Services.GetRequiredService<IBaseDirtyChecker>());
|
||||
externalBuilder.Services.AddSingleton(app.Services.GetRequiredService<WorktreeMaintenanceService>());
|
||||
externalBuilder.Services.AddSingleton(app.Services.GetRequiredService<TaskMergeService>());
|
||||
externalBuilder.Services.AddSingleton(app.Services.GetRequiredService<PlanningMergeOrchestrator>());
|
||||
externalBuilder.Services.AddSingleton(app.Services.GetRequiredService<InteractiveReviewSubmissionService>());
|
||||
externalBuilder.Services.AddScoped<TaskAttachmentRepository>();
|
||||
externalBuilder.Services.AddSingleton<HandoffRoundTracker>();
|
||||
externalBuilder.Services.AddScoped<FindingsStoreLocator>();
|
||||
externalBuilder.Services.AddScoped<ExternalMcpService>();
|
||||
externalBuilder.Services.AddScoped<BatchMcpTools>();
|
||||
externalBuilder.Services.AddScoped<ListMcpTools>();
|
||||
@@ -336,16 +341,11 @@ if (cfg.ExternalMcpPort > 0)
|
||||
externalBuilder.Services.AddScoped<RunHistoryMcpTools>();
|
||||
externalBuilder.Services.AddScoped<AgentMcpTools>();
|
||||
externalBuilder.Services.AddScoped<LifecycleMcpTools>();
|
||||
externalBuilder.Services.AddSingleton<HandoffRoundTracker>();
|
||||
externalBuilder.Services.AddScoped<HandoffMcpTools>();
|
||||
externalBuilder.Services.AddScoped<AppSettingsMcpTools>();
|
||||
externalBuilder.Services.AddScoped<TaskWaitMcpTools>();
|
||||
externalBuilder.Services.AddScoped<QueueStateMcpTools>();
|
||||
externalBuilder.Services.AddSingleton(app.Services.GetRequiredService<AttachmentStore>());
|
||||
externalBuilder.Services.AddScoped<TaskAttachmentRepository>();
|
||||
externalBuilder.Services.AddScoped<AttachmentMcpTools>();
|
||||
externalBuilder.Services.AddSingleton(app.Services.GetRequiredService<FindingsStore>());
|
||||
externalBuilder.Services.AddScoped<FindingsStoreLocator>();
|
||||
externalBuilder.Services.AddScoped<FindingsMcpTools>();
|
||||
externalBuilder.Services.AddMcpServer()
|
||||
.WithHttpTransport(o => o.Stateless = true)
|
||||
|
||||
@@ -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/<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.</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)
|
||||
{
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
using ClaudeDo.Worker.Planning;
|
||||
|
||||
namespace ClaudeDo.Worker.Runner;
|
||||
|
||||
public interface IInteractiveLaunchSpecService
|
||||
{
|
||||
/// <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>
|
||||
LaunchSpec BuildPlanningStart(PlanningSessionStartContext ctx);
|
||||
|
||||
/// <summary>Maps a planning RESUME context (from PlanningSessionManager.ResumeAsync) into a
|
||||
/// LaunchSpec for an embedded ConPTY planning session (--permission-mode plan --resume).</summary>
|
||||
LaunchSpec BuildPlanningResume(PlanningSessionResumeContext ctx);
|
||||
|
||||
/// <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>
|
||||
Task<LaunchSpec> BuildForTaskAsync(string taskId, CancellationToken ct);
|
||||
|
||||
/// <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>
|
||||
Task<LaunchSpec> BuildForDirectoryAsync(string directory, CancellationToken ct);
|
||||
|
||||
/// <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/<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.</summary>
|
||||
Task<LaunchSpec> BuildForMergeHelperAsync(IReadOnlyList<string> taskIds, string listId, string handlerTaskId, CancellationToken ct);
|
||||
|
||||
/// <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>
|
||||
Task<string> CreateMergeHelperTaskAsync(
|
||||
IReadOnlyList<string> taskIds, string listId, string title, string descriptionHeader, CancellationToken ct);
|
||||
|
||||
/// <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>
|
||||
Task<LaunchSpec> BuildForMergeHelperHandoffAsync(
|
||||
string taskId, IReadOnlyList<string> survivingTaskIds, string nextPhase, CancellationToken ct);
|
||||
}
|
||||
@@ -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<string> Args,
|
||||
IReadOnlyDictionary<string, string> Env);
|
||||
@@ -18,7 +18,7 @@ public sealed class TaskStateService : ITaskStateService
|
||||
private readonly PlanningChainCoordinator _chain;
|
||||
private readonly RunCancellationRegistry _runCancels;
|
||||
private readonly Func<IActiveMergeState> _mergeState;
|
||||
private readonly IBaseDirtyChecker _baseDirtyChecker;
|
||||
private readonly BaseDirtyChecker _baseDirtyChecker;
|
||||
private readonly ILogger<TaskStateService> _logger;
|
||||
|
||||
public TaskStateService(
|
||||
@@ -28,7 +28,7 @@ public sealed class TaskStateService : ITaskStateService
|
||||
PlanningChainCoordinator chain,
|
||||
RunCancellationRegistry runCancels,
|
||||
Func<IActiveMergeState> mergeState,
|
||||
IBaseDirtyChecker baseDirtyChecker,
|
||||
BaseDirtyChecker baseDirtyChecker,
|
||||
ILogger<TaskStateService> logger)
|
||||
{
|
||||
_dbFactory = dbFactory;
|
||||
|
||||
@@ -24,7 +24,7 @@ public static class TaskStateServiceBuilder
|
||||
public static Built Build(
|
||||
IDbContextFactory<ClaudeDoDbContext> dbFactory,
|
||||
Func<IActiveMergeState>? mergeState = null,
|
||||
IBaseDirtyChecker? baseDirtyChecker = null)
|
||||
BaseDirtyChecker? baseDirtyChecker = null)
|
||||
{
|
||||
var hub = new CapturingHubContext();
|
||||
var broadcaster = new HubBroadcaster(hub);
|
||||
|
||||
Reference in New Issue
Block a user