feat(worker): WindowsTerminalPlanningLauncher with pre-flight checks

This commit is contained in:
mika kuns
2026-04-23 21:08:15 +02:00
parent d28164caf4
commit 43a3740980
3 changed files with 191 additions and 0 deletions

View File

@@ -0,0 +1,12 @@
namespace ClaudeDo.Worker.Planning;
public interface IPlanningTerminalLauncher
{
Task LaunchStartAsync(PlanningSessionStartContext ctx, CancellationToken cancellationToken);
Task LaunchResumeAsync(PlanningSessionResumeContext ctx, CancellationToken cancellationToken);
}
public sealed class PlanningLaunchException : Exception
{
public PlanningLaunchException(string message) : base(message) { }
}

View File

@@ -0,0 +1,132 @@
// Claude CLI flags (verified 2026-04-23 via Context7):
// Thinking budget: env var MAX_THINKING_TOKENS=20000 (no CLI flag exists)
// Allowed-tools: --allowedTools (camelCase), comma-separated tokens
// System prompt: --append-system-prompt-file <path> (file form)
// Session ID: no pre-assign flag; resume with --resume <id>
using System.Diagnostics;
namespace ClaudeDo.Worker.Planning;
public sealed class WindowsTerminalPlanningLauncher : IPlanningTerminalLauncher
{
private const string AllowedTools = "mcp__claudedo__*,Read,Grep,Glob,WebFetch,WebSearch,Skill";
private const string Model = "claude-sonnet-4-6";
private readonly string _wtPath;
private readonly string _claudePath;
public WindowsTerminalPlanningLauncher(string wtPath, string claudePath)
{
_wtPath = wtPath;
_claudePath = claudePath;
}
public Task LaunchStartAsync(PlanningSessionStartContext ctx, CancellationToken cancellationToken)
{
if (!Directory.Exists(ctx.WorkingDir))
throw new PlanningLaunchException($"Working directory does not exist: {ctx.WorkingDir}");
if (!File.Exists(ctx.Files.McpConfigPath))
throw new PlanningLaunchException($"MCP config file not found: {ctx.Files.McpConfigPath}");
var resolvedWt = Resolve(_wtPath);
if (resolvedWt is null)
throw new PlanningLaunchException($"Windows Terminal not found: {_wtPath}");
var resolvedClaude = Resolve(_claudePath);
if (resolvedClaude is null)
throw new PlanningLaunchException($"claude executable not found: {_claudePath}");
var initialPrompt = File.Exists(ctx.Files.InitialPromptPath)
? File.ReadAllText(ctx.Files.InitialPromptPath)
: string.Empty;
// Build cmd line: set MAX_THINKING_TOKENS=20000 && claude <args> "prompt"
// UseShellExecute=true means we cannot set psi.Environment, so we inject via cmd /k.
var claudeArgs = BuildStartArgs(ctx, initialPrompt, resolvedClaude);
var cmdLine = $"set MAX_THINKING_TOKENS=20000 && {claudeArgs}";
var psi = new ProcessStartInfo
{
FileName = resolvedWt,
UseShellExecute = true,
WorkingDirectory = ctx.WorkingDir,
};
// wt.exe -d <workingDir> cmd /k "<cmdLine>"
psi.ArgumentList.Add("-d");
psi.ArgumentList.Add(ctx.WorkingDir);
psi.ArgumentList.Add("cmd");
psi.ArgumentList.Add("/k");
psi.ArgumentList.Add(cmdLine);
Process.Start(psi);
return Task.CompletedTask;
}
public Task LaunchResumeAsync(PlanningSessionResumeContext ctx, CancellationToken cancellationToken)
{
if (!Directory.Exists(ctx.WorkingDir))
throw new PlanningLaunchException($"Working directory does not exist: {ctx.WorkingDir}");
if (!File.Exists(ctx.McpConfigPath))
throw new PlanningLaunchException($"MCP config file not found: {ctx.McpConfigPath}");
var resolvedWt = Resolve(_wtPath);
if (resolvedWt is null)
throw new PlanningLaunchException($"Windows Terminal not found: {_wtPath}");
var resolvedClaude = Resolve(_claudePath);
if (resolvedClaude is null)
throw new PlanningLaunchException($"claude executable not found: {_claudePath}");
var psi = new ProcessStartInfo
{
FileName = resolvedWt,
UseShellExecute = true,
WorkingDirectory = ctx.WorkingDir,
};
psi.ArgumentList.Add("-d");
psi.ArgumentList.Add(ctx.WorkingDir);
psi.ArgumentList.Add("cmd");
psi.ArgumentList.Add("/k");
psi.ArgumentList.Add(BuildResumeArgs(ctx, resolvedClaude));
Process.Start(psi);
return Task.CompletedTask;
}
private static string BuildStartArgs(PlanningSessionStartContext ctx, string initialPrompt, string claudePath)
{
// Build as a flat string for cmd /k; quote paths that may contain spaces.
return $"\"{claudePath}\" --mcp-config \"{ctx.Files.McpConfigPath}\" --append-system-prompt-file \"{ctx.Files.SystemPromptPath}\" --allowedTools \"{AllowedTools}\" --model {Model} \"{EscapeArg(initialPrompt)}\"";
}
private static string BuildResumeArgs(PlanningSessionResumeContext ctx, string claudePath)
{
return $"\"{claudePath}\" --resume {ctx.ClaudeSessionId} --mcp-config \"{ctx.McpConfigPath}\"";
}
private static string EscapeArg(string value) => value.Replace("\"", "\\\"");
private static string? Resolve(string pathOrName)
{
if (File.Exists(pathOrName))
return pathOrName;
// Try PATH resolution
var envPath = Environment.GetEnvironmentVariable("PATH") ?? string.Empty;
var extensions = new[] { "", ".exe", ".cmd", ".bat" };
foreach (var dir in envPath.Split(Path.PathSeparator))
{
foreach (var ext in extensions)
{
var candidate = Path.Combine(dir, pathOrName + ext);
if (File.Exists(candidate))
return candidate;
}
}
return null;
}
}