fix(claude-do): merge fix(worker): List-Handler-Session kennt ihre eigene Handler-
ClaudeDo-Task: 997d0ffa-48ae-42d7-bea0-3aed4cf81768
This commit is contained in:
@@ -547,6 +547,9 @@ public static class PromptFiles
|
|||||||
Scope: {scope}
|
Scope: {scope}
|
||||||
Repo: {repo}
|
Repo: {repo}
|
||||||
|
|
||||||
|
Your own handler task id (for handoff_list_handler / submit_task_for_review — do NOT confuse
|
||||||
|
this with the ids of the tasks you are handling below): {handlerTaskId}
|
||||||
|
|
||||||
Handle the following tasks. Work Phases 0–5 as your instructions describe, asking me whenever you are unsure.
|
Handle the following tasks. Work Phases 0–5 as your instructions describe, asking me whenever you are unsure.
|
||||||
|
|
||||||
{tasks}
|
{tasks}
|
||||||
@@ -560,6 +563,9 @@ public static class PromptFiles
|
|||||||
Scope: {scope}
|
Scope: {scope}
|
||||||
Repo: {repo}
|
Repo: {repo}
|
||||||
|
|
||||||
|
Your own handler task id (for handoff_list_handler / submit_task_for_review — do NOT confuse
|
||||||
|
this with the ids of the surviving tasks below): {handlerTaskId}
|
||||||
|
|
||||||
A prior session already read, deduped and enhanced this list's tasks. Pick up at phase 3
|
A prior session already read, deduped and enhanced this list's tasks. Pick up at phase 3
|
||||||
for the tasks below — their descriptions are already sharpened.
|
for the tasks below — their descriptions are already sharpened.
|
||||||
|
|
||||||
|
|||||||
@@ -130,8 +130,10 @@ public interface IWorkerClient : INotifyPropertyChanged
|
|||||||
/// no task, no worktree.</summary>
|
/// no task, no worktree.</summary>
|
||||||
Task<LaunchSpec> GetAdHocLaunchSpecAsync(string directory, CancellationToken ct = default);
|
Task<LaunchSpec> GetAdHocLaunchSpecAsync(string directory, CancellationToken ct = default);
|
||||||
/// <summary>Launch spec for an embedded ConPTY "merge helper" session that drives the given
|
/// <summary>Launch spec for an embedded ConPTY "merge helper" session that drives the given
|
||||||
/// tasks to a merged/Done state. listId scopes the session (and cwd) to that list.</summary>
|
/// tasks to a merged/Done state. listId scopes the session (and cwd) to that list.
|
||||||
Task<LaunchSpec> GetMergeHelperLaunchSpecAsync(IReadOnlyList<string> taskIds, string listId, CancellationToken ct = default);
|
/// handlerTaskId (from CreateMergeHelperTaskAsync) is rendered into the brief so the session
|
||||||
|
/// can call handoff_list_handler on its own handler task.</summary>
|
||||||
|
Task<LaunchSpec> GetMergeHelperLaunchSpecAsync(IReadOnlyList<string> taskIds, string listId, string handlerTaskId, CancellationToken ct = default);
|
||||||
/// <summary>Creates the ClaudeDo task that owns a list-handler run (one per run, Idle/IsManual,
|
/// <summary>Creates the ClaudeDo task that owns a list-handler run (one per run, Idle/IsManual,
|
||||||
/// never queued) so the ConPTY tile can be task-based instead of ad-hoc. Returns the new task id.</summary>
|
/// never queued) so the ConPTY tile can be task-based instead of ad-hoc. Returns the new task id.</summary>
|
||||||
Task<string> CreateMergeHelperTaskAsync(
|
Task<string> CreateMergeHelperTaskAsync(
|
||||||
|
|||||||
@@ -614,8 +614,8 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
|
|||||||
public async Task<LaunchSpec> GetAdHocLaunchSpecAsync(string directory, CancellationToken ct = default)
|
public async Task<LaunchSpec> GetAdHocLaunchSpecAsync(string directory, CancellationToken ct = default)
|
||||||
=> await InvokeTimedAsync<LaunchSpec>("GetAdHocLaunchSpec", () => _hub.InvokeAsync<LaunchSpec>("GetAdHocLaunchSpec", directory, ct));
|
=> await InvokeTimedAsync<LaunchSpec>("GetAdHocLaunchSpec", () => _hub.InvokeAsync<LaunchSpec>("GetAdHocLaunchSpec", directory, ct));
|
||||||
|
|
||||||
public async Task<LaunchSpec> GetMergeHelperLaunchSpecAsync(IReadOnlyList<string> taskIds, string listId, CancellationToken ct = default)
|
public async Task<LaunchSpec> GetMergeHelperLaunchSpecAsync(IReadOnlyList<string> taskIds, string listId, string handlerTaskId, CancellationToken ct = default)
|
||||||
=> await InvokeTimedAsync<LaunchSpec>("GetMergeHelperLaunchSpec", () => _hub.InvokeAsync<LaunchSpec>("GetMergeHelperLaunchSpec", taskIds, listId, ct));
|
=> await InvokeTimedAsync<LaunchSpec>("GetMergeHelperLaunchSpec", () => _hub.InvokeAsync<LaunchSpec>("GetMergeHelperLaunchSpec", taskIds, listId, handlerTaskId, ct));
|
||||||
|
|
||||||
public async Task<string> CreateMergeHelperTaskAsync(
|
public async Task<string> CreateMergeHelperTaskAsync(
|
||||||
IReadOnlyList<string> taskIds, string listId, string title, string descriptionHeader, CancellationToken ct = default)
|
IReadOnlyList<string> taskIds, string listId, string title, string descriptionHeader, CancellationToken ct = default)
|
||||||
|
|||||||
@@ -279,7 +279,7 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
|
|||||||
}
|
}
|
||||||
|
|
||||||
AddConPtyPane(new ConPtyPaneViewModel(taskId, title,
|
AddConPtyPane(new ConPtyPaneViewModel(taskId, title,
|
||||||
() => DescribeAsync(() => _worker.GetMergeHelperLaunchSpecAsync(taskIds, listId))));
|
() => DescribeAsync(() => _worker.GetMergeHelperLaunchSpecAsync(taskIds, listId, taskId))));
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -864,12 +864,13 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
|||||||
|
|
||||||
// Builds the launch spec for an embedded ConPTY "merge helper" session that drives the given
|
// Builds the launch spec for an embedded ConPTY "merge helper" session that drives the given
|
||||||
// tasks to a merged/Done state via the mcp__claudedo__* tools. listId scopes the brief label
|
// tasks to a merged/Done state via the mcp__claudedo__* tools. listId scopes the brief label
|
||||||
// and cwd to that list.
|
// and cwd to that list. handlerTaskId is the id returned by CreateMergeHelperTask -- it is
|
||||||
public Task<LaunchSpec> GetMergeHelperLaunchSpec(string[] taskIds, string listId) => HubGuard(() =>
|
// rendered into the brief so the session can call handoff_list_handler on itself.
|
||||||
|
public Task<LaunchSpec> GetMergeHelperLaunchSpec(string[] taskIds, string listId, string handlerTaskId) => HubGuard(() =>
|
||||||
{
|
{
|
||||||
if (_interactiveLaunchSpec is null)
|
if (_interactiveLaunchSpec is null)
|
||||||
throw new InvalidOperationException("Interactive launch spec service is not configured.");
|
throw new InvalidOperationException("Interactive launch spec service is not configured.");
|
||||||
return _interactiveLaunchSpec.BuildForMergeHelperAsync(taskIds, listId, Context.ConnectionAborted);
|
return _interactiveLaunchSpec.BuildForMergeHelperAsync(taskIds, listId, handlerTaskId, Context.ConnectionAborted);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Creates the ClaudeDo task that owns a list-handler run, before the ConPTY tile opens --
|
// Creates the ClaudeDo task that owns a list-handler run, before the ConPTY tile opens --
|
||||||
|
|||||||
@@ -241,7 +241,7 @@ public sealed class InteractiveLaunchSpecService : IInteractiveLaunchSpecService
|
|||||||
private const string MergeHelperAllowedTools =
|
private const string MergeHelperAllowedTools =
|
||||||
"mcp__claudedo__*,Read,Grep,Glob,Edit,Bash,WebFetch,WebSearch,Skill,Task";
|
"mcp__claudedo__*,Read,Grep,Glob,Edit,Bash,WebFetch,WebSearch,Skill,Task";
|
||||||
|
|
||||||
public async Task<LaunchSpec> BuildForMergeHelperAsync(IReadOnlyList<string> taskIds, string listId, CancellationToken ct)
|
public async Task<LaunchSpec> BuildForMergeHelperAsync(IReadOnlyList<string> taskIds, string listId, string handlerTaskId, CancellationToken ct)
|
||||||
{
|
{
|
||||||
if (taskIds.Count == 0)
|
if (taskIds.Count == 0)
|
||||||
throw new InvalidOperationException("No tasks selected for the list handler.");
|
throw new InvalidOperationException("No tasks selected for the list handler.");
|
||||||
@@ -278,6 +278,7 @@ public sealed class InteractiveLaunchSpecService : IInteractiveLaunchSpecService
|
|||||||
{
|
{
|
||||||
["scope"] = $"List: {list.Name}",
|
["scope"] = $"List: {list.Name}",
|
||||||
["repo"] = repoDir,
|
["repo"] = repoDir,
|
||||||
|
["handlerTaskId"] = handlerTaskId,
|
||||||
["tasks"] = string.Join("\n", briefLines),
|
["tasks"] = string.Join("\n", briefLines),
|
||||||
}), ct);
|
}), ct);
|
||||||
|
|
||||||
@@ -371,6 +372,7 @@ public sealed class InteractiveLaunchSpecService : IInteractiveLaunchSpecService
|
|||||||
{
|
{
|
||||||
["scope"] = $"List: {list.Name}",
|
["scope"] = $"List: {list.Name}",
|
||||||
["repo"] = repoDir,
|
["repo"] = repoDir,
|
||||||
|
["handlerTaskId"] = taskId,
|
||||||
["tasks"] = string.Join("\n", briefLines),
|
["tasks"] = string.Join("\n", briefLines),
|
||||||
["finalNote"] = finalNote,
|
["finalNote"] = finalNote,
|
||||||
}), ct);
|
}), ct);
|
||||||
|
|||||||
@@ -34,9 +34,11 @@ public interface IInteractiveLaunchSpecService
|
|||||||
/// given tasks to a merged/Done state via the mcp__claudedo__* tools. Writes a per-session
|
/// 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
|
/// 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.
|
/// 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
|
/// Throws KeyNotFoundException if the list doesn't exist; InvalidOperationException if
|
||||||
/// taskIds is empty or the list has no existing working directory.</summary>
|
/// taskIds is empty or the list has no existing working directory.</summary>
|
||||||
Task<LaunchSpec> BuildForMergeHelperAsync(IReadOnlyList<string> taskIds, string listId, CancellationToken ct);
|
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
|
/// <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
|
/// "Let Claude handle it") and stamps the list repo's current HEAD as the review range's
|
||||||
|
|||||||
@@ -360,6 +360,13 @@ public class PromptFilesTests
|
|||||||
Assert.Contains("{tasks}", d);
|
Assert.Contains("{tasks}", d);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DefaultFor_merge_helper_initial_has_handler_task_id_token()
|
||||||
|
{
|
||||||
|
var d = PromptFiles.DefaultFor(PromptKind.MergeHelperInitial);
|
||||||
|
Assert.Contains("{handlerTaskId}", d);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void RenderTemplate_merge_helper_initial_substitutes_scope_and_tasks()
|
public void RenderTemplate_merge_helper_initial_substitutes_scope_and_tasks()
|
||||||
{
|
{
|
||||||
@@ -382,6 +389,13 @@ public class PromptFilesTests
|
|||||||
Assert.Contains("{tasks}", d);
|
Assert.Contains("{tasks}", d);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DefaultFor_merge_helper_handoff_has_handler_task_id_token()
|
||||||
|
{
|
||||||
|
var d = PromptFiles.DefaultFor(PromptKind.MergeHelperHandoff);
|
||||||
|
Assert.Contains("{handlerTaskId}", d);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void DefaultFor_merge_helper_handoff_points_at_phase_3()
|
public void DefaultFor_merge_helper_handoff_points_at_phase_3()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ public abstract class StubWorkerClient : IWorkerClient
|
|||||||
=> Task.FromResult(new LaunchSpec(".", "claude", Array.Empty<string>(), new Dictionary<string, string>()));
|
=> Task.FromResult(new LaunchSpec(".", "claude", Array.Empty<string>(), new Dictionary<string, string>()));
|
||||||
public virtual Task<LaunchSpec> GetAdHocLaunchSpecAsync(string directory, CancellationToken ct = default)
|
public virtual Task<LaunchSpec> GetAdHocLaunchSpecAsync(string directory, CancellationToken ct = default)
|
||||||
=> Task.FromResult(new LaunchSpec(directory, "claude", Array.Empty<string>(), new Dictionary<string, string>()));
|
=> Task.FromResult(new LaunchSpec(directory, "claude", Array.Empty<string>(), new Dictionary<string, string>()));
|
||||||
public virtual Task<LaunchSpec> GetMergeHelperLaunchSpecAsync(IReadOnlyList<string> taskIds, string listId, CancellationToken ct = default)
|
public virtual Task<LaunchSpec> GetMergeHelperLaunchSpecAsync(IReadOnlyList<string> taskIds, string listId, string handlerTaskId, CancellationToken ct = default)
|
||||||
=> Task.FromResult(new LaunchSpec(".", "claude", Array.Empty<string>(), new Dictionary<string, string>()));
|
=> Task.FromResult(new LaunchSpec(".", "claude", Array.Empty<string>(), new Dictionary<string, string>()));
|
||||||
public virtual Task<string> CreateMergeHelperTaskAsync(
|
public virtual Task<string> CreateMergeHelperTaskAsync(
|
||||||
IReadOnlyList<string> taskIds, string listId, string title, string descriptionHeader, CancellationToken ct = default)
|
IReadOnlyList<string> taskIds, string listId, string title, string descriptionHeader, CancellationToken ct = default)
|
||||||
|
|||||||
@@ -502,7 +502,7 @@ public class MissionControlViewModelTests : IDisposable
|
|||||||
|
|
||||||
private sealed class ThrowingMergeHelperLaunchSpecWorker : StubWorkerClient
|
private sealed class ThrowingMergeHelperLaunchSpecWorker : StubWorkerClient
|
||||||
{
|
{
|
||||||
public override Task<LaunchSpec> GetMergeHelperLaunchSpecAsync(IReadOnlyList<string> taskIds, string listId, CancellationToken ct = default)
|
public override Task<LaunchSpec> GetMergeHelperLaunchSpecAsync(IReadOnlyList<string> taskIds, string listId, string handlerTaskId, CancellationToken ct = default)
|
||||||
=> throw new InvalidOperationException("spec failed");
|
=> throw new InvalidOperationException("spec failed");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -628,7 +628,7 @@ public sealed class InteractiveLaunchSpecServiceTests : IDisposable
|
|||||||
var listId = await SeedListAsync(workingDir: _tempDir);
|
var listId = await SeedListAsync(workingDir: _tempDir);
|
||||||
var svc = BuildService();
|
var svc = BuildService();
|
||||||
await Assert.ThrowsAsync<InvalidOperationException>(
|
await Assert.ThrowsAsync<InvalidOperationException>(
|
||||||
() => svc.BuildForMergeHelperAsync(Array.Empty<string>(), listId, CancellationToken.None));
|
() => svc.BuildForMergeHelperAsync(Array.Empty<string>(), listId, Guid.NewGuid().ToString(), CancellationToken.None));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -640,7 +640,7 @@ public sealed class InteractiveLaunchSpecServiceTests : IDisposable
|
|||||||
|
|
||||||
var svc = BuildService();
|
var svc = BuildService();
|
||||||
var ex = await Assert.ThrowsAsync<InvalidOperationException>(
|
var ex = await Assert.ThrowsAsync<InvalidOperationException>(
|
||||||
() => svc.BuildForMergeHelperAsync(new[] { taskId }, listId, CancellationToken.None));
|
() => svc.BuildForMergeHelperAsync(new[] { taskId }, listId, Guid.NewGuid().ToString(), CancellationToken.None));
|
||||||
Assert.Contains("working directory", ex.Message);
|
Assert.Contains("working directory", ex.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -653,7 +653,7 @@ public sealed class InteractiveLaunchSpecServiceTests : IDisposable
|
|||||||
|
|
||||||
var svc = BuildService();
|
var svc = BuildService();
|
||||||
await Assert.ThrowsAsync<KeyNotFoundException>(
|
await Assert.ThrowsAsync<KeyNotFoundException>(
|
||||||
() => svc.BuildForMergeHelperAsync(new[] { taskId }, "no-such-list", CancellationToken.None));
|
() => svc.BuildForMergeHelperAsync(new[] { taskId }, "no-such-list", Guid.NewGuid().ToString(), CancellationToken.None));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -669,7 +669,7 @@ public sealed class InteractiveLaunchSpecServiceTests : IDisposable
|
|||||||
await SeedTaskAsync(t2, listId, TaskStatus.Idle, title: "Second task");
|
await SeedTaskAsync(t2, listId, TaskStatus.Idle, title: "Second task");
|
||||||
|
|
||||||
var svc = BuildService();
|
var svc = BuildService();
|
||||||
var spec = await svc.BuildForMergeHelperAsync(new[] { t1, t2 }, listId, CancellationToken.None);
|
var spec = await svc.BuildForMergeHelperAsync(new[] { t1, t2 }, listId, Guid.NewGuid().ToString(), CancellationToken.None);
|
||||||
var sessionDir = TrackSessionDir(spec);
|
var sessionDir = TrackSessionDir(spec);
|
||||||
|
|
||||||
Assert.Equal(repo, spec.Cwd);
|
Assert.Equal(repo, spec.Cwd);
|
||||||
@@ -726,7 +726,7 @@ public sealed class InteractiveLaunchSpecServiceTests : IDisposable
|
|||||||
await SeedTaskAsync(t1, listId, TaskStatus.WaitingForReview, title: "First task");
|
await SeedTaskAsync(t1, listId, TaskStatus.WaitingForReview, title: "First task");
|
||||||
|
|
||||||
var svc = BuildService();
|
var svc = BuildService();
|
||||||
var spec = await svc.BuildForMergeHelperAsync(new[] { t1 }, listId, CancellationToken.None);
|
var spec = await svc.BuildForMergeHelperAsync(new[] { t1 }, listId, Guid.NewGuid().ToString(), CancellationToken.None);
|
||||||
var sessionDir = TrackSessionDir(spec);
|
var sessionDir = TrackSessionDir(spec);
|
||||||
|
|
||||||
var args = spec.Args.ToList();
|
var args = spec.Args.ToList();
|
||||||
@@ -749,7 +749,7 @@ public sealed class InteractiveLaunchSpecServiceTests : IDisposable
|
|||||||
await SeedTaskAsync(t2, listId, TaskStatus.Idle, title: "Second task");
|
await SeedTaskAsync(t2, listId, TaskStatus.Idle, title: "Second task");
|
||||||
|
|
||||||
var svc = BuildService();
|
var svc = BuildService();
|
||||||
var spec = await svc.BuildForMergeHelperAsync(new[] { t1, t2 }, listId, CancellationToken.None);
|
var spec = await svc.BuildForMergeHelperAsync(new[] { t1, t2 }, listId, Guid.NewGuid().ToString(), CancellationToken.None);
|
||||||
var sessionDir = TrackSessionDir(spec);
|
var sessionDir = TrackSessionDir(spec);
|
||||||
|
|
||||||
var brief = File.ReadAllText(Path.Combine(sessionDir, "brief.md"));
|
var brief = File.ReadAllText(Path.Combine(sessionDir, "brief.md"));
|
||||||
@@ -761,6 +761,28 @@ public sealed class InteractiveLaunchSpecServiceTests : IDisposable
|
|||||||
Assert.Contains(t2, brief);
|
Assert.Contains(t2, brief);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Regression guard: no session ever learned its own handler task id, so it could never call
|
||||||
|
// handoff_list_handler(taskId, ...) on itself -- the handoff chain broke exactly where it was
|
||||||
|
// needed. The brief must carry it.
|
||||||
|
[Fact]
|
||||||
|
public async Task BuildForMergeHelperAsync_BriefIncludesHandlerTaskId()
|
||||||
|
{
|
||||||
|
var repo = Path.Combine(_tempDir, "repoHandlerId");
|
||||||
|
Directory.CreateDirectory(repo);
|
||||||
|
|
||||||
|
var listId = await SeedListAsync(workingDir: repo, name: "Alpha");
|
||||||
|
var t1 = Guid.NewGuid().ToString();
|
||||||
|
await SeedTaskAsync(t1, listId, TaskStatus.Idle, title: "First task");
|
||||||
|
var handlerTaskId = Guid.NewGuid().ToString();
|
||||||
|
|
||||||
|
var svc = BuildService();
|
||||||
|
var spec = await svc.BuildForMergeHelperAsync(new[] { t1 }, listId, handlerTaskId, CancellationToken.None);
|
||||||
|
var sessionDir = TrackSessionDir(spec);
|
||||||
|
|
||||||
|
var brief = File.ReadAllText(Path.Combine(sessionDir, "brief.md"));
|
||||||
|
Assert.Contains(handlerTaskId, brief);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task BuildForMergeHelperAsync_BriefIncludesTaskDescription()
|
public async Task BuildForMergeHelperAsync_BriefIncludesTaskDescription()
|
||||||
{
|
{
|
||||||
@@ -773,7 +795,7 @@ public sealed class InteractiveLaunchSpecServiceTests : IDisposable
|
|||||||
description: "Do the thing carefully and report back.");
|
description: "Do the thing carefully and report back.");
|
||||||
|
|
||||||
var svc = BuildService();
|
var svc = BuildService();
|
||||||
var spec = await svc.BuildForMergeHelperAsync(new[] { t1 }, listId, CancellationToken.None);
|
var spec = await svc.BuildForMergeHelperAsync(new[] { t1 }, listId, Guid.NewGuid().ToString(), CancellationToken.None);
|
||||||
var sessionDir = TrackSessionDir(spec);
|
var sessionDir = TrackSessionDir(spec);
|
||||||
|
|
||||||
var brief = File.ReadAllText(Path.Combine(sessionDir, "brief.md"));
|
var brief = File.ReadAllText(Path.Combine(sessionDir, "brief.md"));
|
||||||
@@ -791,7 +813,7 @@ public sealed class InteractiveLaunchSpecServiceTests : IDisposable
|
|||||||
await SeedTaskAsync(t1, listId, TaskStatus.Idle, title: "No description task", description: null);
|
await SeedTaskAsync(t1, listId, TaskStatus.Idle, title: "No description task", description: null);
|
||||||
|
|
||||||
var svc = BuildService();
|
var svc = BuildService();
|
||||||
var spec = await svc.BuildForMergeHelperAsync(new[] { t1 }, listId, CancellationToken.None);
|
var spec = await svc.BuildForMergeHelperAsync(new[] { t1 }, listId, Guid.NewGuid().ToString(), CancellationToken.None);
|
||||||
var sessionDir = TrackSessionDir(spec);
|
var sessionDir = TrackSessionDir(spec);
|
||||||
|
|
||||||
var brief = File.ReadAllText(Path.Combine(sessionDir, "brief.md"));
|
var brief = File.ReadAllText(Path.Combine(sessionDir, "brief.md"));
|
||||||
@@ -822,7 +844,7 @@ public sealed class InteractiveLaunchSpecServiceTests : IDisposable
|
|||||||
await SeedTaskAsync(t2, listId, TaskStatus.WaitingForReview, title: "Second task", description: "plain description");
|
await SeedTaskAsync(t2, listId, TaskStatus.WaitingForReview, title: "Second task", description: "plain description");
|
||||||
|
|
||||||
var svc = BuildService();
|
var svc = BuildService();
|
||||||
var spec = await svc.BuildForMergeHelperAsync(new[] { t1, t2 }, listId, CancellationToken.None);
|
var spec = await svc.BuildForMergeHelperAsync(new[] { t1, t2 }, listId, Guid.NewGuid().ToString(), CancellationToken.None);
|
||||||
var sessionDir = TrackSessionDir(spec);
|
var sessionDir = TrackSessionDir(spec);
|
||||||
|
|
||||||
var brief = File.ReadAllText(Path.Combine(sessionDir, "brief.md"));
|
var brief = File.ReadAllText(Path.Combine(sessionDir, "brief.md"));
|
||||||
@@ -967,6 +989,26 @@ public sealed class InteractiveLaunchSpecServiceTests : IDisposable
|
|||||||
Assert.DoesNotContain(args, a => a.EndsWith('\\') || a.EndsWith('/'));
|
Assert.DoesNotContain(args, a => a.EndsWith('\\') || a.EndsWith('/'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Regression guard: the handoff session must see the SAME handler task id as the run that
|
||||||
|
// called handoff_list_handler -- it's the id already passed in as taskId, just never rendered
|
||||||
|
// into the handoff kickoff.
|
||||||
|
[Fact]
|
||||||
|
public async Task BuildForMergeHelperHandoffAsync_HandoffIncludesSameHandlerTaskId()
|
||||||
|
{
|
||||||
|
var listId = await SeedListAsync(workingDir: _tempDir);
|
||||||
|
var handlerTaskId = Guid.NewGuid().ToString();
|
||||||
|
await SeedTaskAsync(handlerTaskId, listId, TaskStatus.Idle, title: "Handler");
|
||||||
|
var survivor = Guid.NewGuid().ToString();
|
||||||
|
await SeedTaskAsync(survivor, listId, TaskStatus.WaitingForReview);
|
||||||
|
|
||||||
|
var svc = BuildService();
|
||||||
|
var spec = await svc.BuildForMergeHelperHandoffAsync(handlerTaskId, new[] { survivor }, "wait", CancellationToken.None);
|
||||||
|
var sessionDir = TrackSessionDir(spec);
|
||||||
|
|
||||||
|
var handoff = File.ReadAllText(Path.Combine(sessionDir, "handoff.md"));
|
||||||
|
Assert.Contains(handlerTaskId, handoff);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task BuildForMergeHelperHandoffAsync_UnknownPhase_Throws()
|
public async Task BuildForMergeHelperHandoffAsync_UnknownPhase_Throws()
|
||||||
{
|
{
|
||||||
@@ -1126,7 +1168,7 @@ public sealed class InteractiveLaunchSpecServiceTests : IDisposable
|
|||||||
|
|
||||||
var svc = BuildService();
|
var svc = BuildService();
|
||||||
|
|
||||||
var triageSpec = await svc.BuildForMergeHelperAsync(new[] { survivor }, listId, CancellationToken.None);
|
var triageSpec = await svc.BuildForMergeHelperAsync(new[] { survivor }, listId, handlerTaskId, CancellationToken.None);
|
||||||
var triageDir = TrackSessionDir(triageSpec);
|
var triageDir = TrackSessionDir(triageSpec);
|
||||||
var executeSpec = await svc.BuildForMergeHelperHandoffAsync(handlerTaskId, new[] { survivor }, "merge", CancellationToken.None);
|
var executeSpec = await svc.BuildForMergeHelperHandoffAsync(handlerTaskId, new[] { survivor }, "merge", CancellationToken.None);
|
||||||
var executeDir = TrackSessionDir(executeSpec);
|
var executeDir = TrackSessionDir(executeSpec);
|
||||||
@@ -1177,7 +1219,7 @@ public sealed class InteractiveLaunchSpecServiceTests : IDisposable
|
|||||||
ClaudeSessionId: "sess-42", Token: "tok-2", WorktreePath: _worktreeDir);
|
ClaudeSessionId: "sess-42", Token: "tok-2", WorktreePath: _worktreeDir);
|
||||||
|
|
||||||
var svc = BuildService();
|
var svc = BuildService();
|
||||||
var mergeHelperSpec = await svc.BuildForMergeHelperAsync(new[] { survivor }, listId, CancellationToken.None);
|
var mergeHelperSpec = await svc.BuildForMergeHelperAsync(new[] { survivor }, listId, handlerTaskId, CancellationToken.None);
|
||||||
TrackSessionDir(mergeHelperSpec);
|
TrackSessionDir(mergeHelperSpec);
|
||||||
var handoffSpec = await svc.BuildForMergeHelperHandoffAsync(handlerTaskId, new[] { survivor }, "wait", CancellationToken.None);
|
var handoffSpec = await svc.BuildForMergeHelperHandoffAsync(handlerTaskId, new[] { survivor }, "wait", CancellationToken.None);
|
||||||
TrackSessionDir(handoffSpec);
|
TrackSessionDir(handoffSpec);
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ sealed class FakeWorkerClient : IWorkerClient
|
|||||||
public Task SubmitTaskForReviewAsync(string taskId, CancellationToken ct = default) => Task.CompletedTask;
|
public Task SubmitTaskForReviewAsync(string taskId, CancellationToken ct = default) => Task.CompletedTask;
|
||||||
public Task<LaunchSpec> GetInteractiveLaunchSpecAsync(string taskId, CancellationToken ct = default)
|
public Task<LaunchSpec> GetInteractiveLaunchSpecAsync(string taskId, CancellationToken ct = default)
|
||||||
=> Task.FromResult(new LaunchSpec(".", "claude", Array.Empty<string>(), new Dictionary<string, string>()));
|
=> Task.FromResult(new LaunchSpec(".", "claude", Array.Empty<string>(), new Dictionary<string, string>()));
|
||||||
public Task<LaunchSpec> GetMergeHelperLaunchSpecAsync(IReadOnlyList<string> taskIds, string listId, CancellationToken ct = default)
|
public Task<LaunchSpec> GetMergeHelperLaunchSpecAsync(IReadOnlyList<string> taskIds, string listId, string handlerTaskId, CancellationToken ct = default)
|
||||||
=> Task.FromResult(new LaunchSpec(".", "claude", Array.Empty<string>(), new Dictionary<string, string>()));
|
=> Task.FromResult(new LaunchSpec(".", "claude", Array.Empty<string>(), new Dictionary<string, string>()));
|
||||||
public Task<string> CreateMergeHelperTaskAsync(
|
public Task<string> CreateMergeHelperTaskAsync(
|
||||||
IReadOnlyList<string> taskIds, string listId, string title, string descriptionHeader, CancellationToken ct = default)
|
IReadOnlyList<string> taskIds, string listId, string title, string descriptionHeader, CancellationToken ct = default)
|
||||||
|
|||||||
Reference in New Issue
Block a user