feat(worker): create ephemeral worktree and write .mcp.json in StartAsync
Rewrites StartAsync to provision a git worktree before transitioning the task to Planning state, writes .mcp.json and .claude/settings.local.json into the worktree, and fixes ResumeAsync to supply the updated PlanningSessionResumeContext fields (Token, WorktreePath). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -73,6 +73,46 @@ public sealed class PlanningSessionManager
|
|||||||
if (task.Status != TaskStatus.Manual)
|
if (task.Status != TaskStatus.Manual)
|
||||||
throw new InvalidOperationException($"Task is in status {task.Status}; only Manual can start planning.");
|
throw new InvalidOperationException($"Task is in status {task.Status}; only Manual can start planning.");
|
||||||
|
|
||||||
|
var list = await lists.GetByIdAsync(task.ListId, ct)
|
||||||
|
?? throw new InvalidOperationException($"List {task.ListId} not found.");
|
||||||
|
var listWorkingDir = list.WorkingDir
|
||||||
|
?? throw new InvalidOperationException($"List {task.ListId} has no working directory configured.");
|
||||||
|
|
||||||
|
if (!await _git.IsGitRepoAsync(listWorkingDir, ct))
|
||||||
|
throw new InvalidOperationException($"Working directory is not a git repository: {listWorkingDir}");
|
||||||
|
|
||||||
|
var appSettings = await settings.GetAsync(ct);
|
||||||
|
var worktreePath = WorktreePathFor(taskId, appSettings.WorktreeStrategy, appSettings.CentralWorktreeRoot, listWorkingDir);
|
||||||
|
var branchName = BranchNameFor(taskId);
|
||||||
|
var baseCommit = await _git.RevParseHeadAsync(listWorkingDir, ct);
|
||||||
|
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(worktreePath)!);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _git.WorktreeAddAsync(listWorkingDir, branchName, worktreePath, baseCommit, ct);
|
||||||
|
}
|
||||||
|
catch (InvalidOperationException ex) when (ex.Message.Contains("already exists", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
// Self-heal: remove phantom worktrees, prune, delete branch, retry once.
|
||||||
|
var stalePaths = await _git.ListWorktreePathsForBranchAsync(listWorkingDir, branchName, ct);
|
||||||
|
foreach (var stale in stalePaths)
|
||||||
|
{
|
||||||
|
try { await _git.WorktreeRemoveAsync(listWorkingDir, stale, force: true, ct); } catch { }
|
||||||
|
}
|
||||||
|
try { await _git.WorktreePruneAsync(listWorkingDir, ct); } catch { }
|
||||||
|
try { await _git.BranchDeleteAsync(listWorkingDir, branchName, force: true, ct); } catch { }
|
||||||
|
await _git.WorktreeAddAsync(listWorkingDir, branchName, worktreePath, baseCommit, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write .mcp.json and .claude/settings.local.json into the worktree.
|
||||||
|
var mcpPath = Path.Combine(worktreePath, ".mcp.json");
|
||||||
|
await File.WriteAllTextAsync(mcpPath, BuildMcpConfigJson(), ct);
|
||||||
|
|
||||||
|
var claudeDir = Path.Combine(worktreePath, ".claude");
|
||||||
|
Directory.CreateDirectory(claudeDir);
|
||||||
|
await File.WriteAllTextAsync(Path.Combine(claudeDir, "settings.local.json"), SettingsLocalJson, ct);
|
||||||
|
|
||||||
|
// Session dir + token + prompt files.
|
||||||
var token = GenerateToken();
|
var token = GenerateToken();
|
||||||
var started = await tasks.SetPlanningStartedAsync(taskId, token, ct)
|
var started = await tasks.SetPlanningStartedAsync(taskId, token, ct)
|
||||||
?? throw new InvalidOperationException("Failed to transition task to Planning.");
|
?? throw new InvalidOperationException("Failed to transition task to Planning.");
|
||||||
@@ -82,17 +122,20 @@ public sealed class PlanningSessionManager
|
|||||||
|
|
||||||
var files = new PlanningSessionFiles(
|
var files = new PlanningSessionFiles(
|
||||||
sessionDir,
|
sessionDir,
|
||||||
Path.Combine(sessionDir, "mcp.json"),
|
|
||||||
Path.Combine(sessionDir, "system-prompt.md"),
|
Path.Combine(sessionDir, "system-prompt.md"),
|
||||||
Path.Combine(sessionDir, "initial-prompt.txt"));
|
Path.Combine(sessionDir, "initial-prompt.txt"));
|
||||||
|
|
||||||
await File.WriteAllTextAsync(files.McpConfigPath, BuildMcpConfigJson(), ct);
|
await WriteTokenFileAsync(TokenFilePathFor(sessionDir), token, ct);
|
||||||
await File.WriteAllTextAsync(files.SystemPromptPath, BuildSystemPrompt(), ct);
|
await File.WriteAllTextAsync(files.SystemPromptPath, BuildSystemPrompt(), ct);
|
||||||
await File.WriteAllTextAsync(files.InitialPromptPath, BuildInitialPrompt(task), ct);
|
await File.WriteAllTextAsync(files.InitialPromptPath, BuildInitialPrompt(task), ct);
|
||||||
|
|
||||||
var list = await lists.GetByIdAsync(task.ListId, ct)
|
return new PlanningSessionStartContext(
|
||||||
?? throw new InvalidOperationException($"List {task.ListId} not found.");
|
ParentTaskId: taskId,
|
||||||
return new PlanningSessionStartContext(taskId, list.WorkingDir ?? throw new InvalidOperationException($"List {task.ListId} has no working directory configured."), files);
|
WorkingDir: worktreePath,
|
||||||
|
Token: token,
|
||||||
|
WorktreePath: worktreePath,
|
||||||
|
BranchName: branchName,
|
||||||
|
Files: files);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<int> FinalizeAsync(string taskId, bool queueAgentTasks, CancellationToken ct)
|
public async Task<int> FinalizeAsync(string taskId, bool queueAgentTasks, CancellationToken ct)
|
||||||
@@ -139,13 +182,19 @@ public sealed class PlanningSessionManager
|
|||||||
throw new InvalidOperationException("No Claude session ID captured yet; cannot resume.");
|
throw new InvalidOperationException("No Claude session ID captured yet; cannot resume.");
|
||||||
|
|
||||||
var sessionDir = Path.Combine(_rootDirectory, taskId);
|
var sessionDir = Path.Combine(_rootDirectory, taskId);
|
||||||
var mcpConfigPath = Path.Combine(sessionDir, "mcp.json");
|
if (!Directory.Exists(sessionDir))
|
||||||
if (!File.Exists(mcpConfigPath))
|
|
||||||
throw new InvalidOperationException($"Session directory missing: {sessionDir}");
|
throw new InvalidOperationException($"Session directory missing: {sessionDir}");
|
||||||
|
|
||||||
var list = await lists.GetByIdAsync(task.ListId, ct)
|
var list = await lists.GetByIdAsync(task.ListId, ct)
|
||||||
?? throw new InvalidOperationException($"List {task.ListId} not found.");
|
?? throw new InvalidOperationException($"List {task.ListId} not found.");
|
||||||
return new PlanningSessionResumeContext(taskId, list.WorkingDir ?? throw new InvalidOperationException($"List {task.ListId} has no working directory configured."), task.PlanningSessionId, mcpConfigPath);
|
var listWorkingDir = list.WorkingDir
|
||||||
|
?? throw new InvalidOperationException($"List {task.ListId} has no working directory configured.");
|
||||||
|
|
||||||
|
var appSettings = await settings.GetAsync(ct);
|
||||||
|
var worktreePath = WorktreePathFor(taskId, appSettings.WorktreeStrategy, appSettings.CentralWorktreeRoot, listWorkingDir);
|
||||||
|
var token = task.PlanningSessionToken ?? string.Empty;
|
||||||
|
|
||||||
|
return new PlanningSessionResumeContext(taskId, listWorkingDir, task.PlanningSessionId, token, worktreePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string GenerateToken()
|
private static string GenerateToken()
|
||||||
|
|||||||
Reference in New Issue
Block a user