feat: planning sessions foundation (Plan A) #4

Merged
claude merged 16 commits from feat/planning-sessions-foundation into main 2026-04-23 16:31:37 +00:00
2 changed files with 102 additions and 0 deletions
Showing only changes of commit a9e7479326 - Show all commits

View File

@@ -303,6 +303,49 @@ public sealed class TaskRepository
.FirstOrDefaultAsync(t => t.PlanningSessionToken == token, ct); .FirstOrDefaultAsync(t => t.PlanningSessionToken == token, ct);
} }
public async Task<int> FinalizePlanningAsync(
string parentId,
bool queueAgentTasks,
CancellationToken ct = default)
{
using var tx = await _context.Database.BeginTransactionAsync(ct);
var parent = await _context.Tasks
.AsNoTracking()
.Include(t => t.List).ThenInclude(l => l.Tags)
.FirstOrDefaultAsync(t => t.Id == parentId, ct);
if (parent is null || parent.Status != TaskStatus.Planning)
throw new InvalidOperationException($"Task {parentId} is not in Planning state.");
var listHasAgentTag = parent.List.Tags.Any(t => t.Name == "agent");
var drafts = await _context.Tasks
.Include(t => t.Tags)
.Where(t => t.ParentTaskId == parentId && t.Status == TaskStatus.Draft)
.ToListAsync(ct);
int count = 0;
foreach (var draft in drafts)
{
var childHasAgentTag = draft.Tags.Any(t => t.Name == "agent");
var shouldQueue = queueAgentTasks && (childHasAgentTag || listHasAgentTag);
draft.Status = shouldQueue ? TaskStatus.Queued : TaskStatus.Manual;
count++;
}
var finalizedAt = DateTime.UtcNow;
await _context.Tasks
.Where(t => t.Id == parentId)
.ExecuteUpdateAsync(s => s
.SetProperty(t => t.Status, TaskStatus.Planned)
.SetProperty(t => t.PlanningFinalizedAt, finalizedAt)
.SetProperty(t => t.PlanningSessionToken, (string?)null), ct);
await _context.SaveChangesAsync(ct);
await tx.CommitAsync(ct);
return count;
}
#endregion #endregion
#region Queue selection #region Queue selection

View File

@@ -187,4 +187,63 @@ public sealed class TaskRepositoryPlanningTests : IDisposable
var found = await _tasks.FindByPlanningTokenAsync("no-such-token"); var found = await _tasks.FindByPlanningTokenAsync("no-such-token");
Assert.Null(found); Assert.Null(found);
} }
[Fact]
public async Task FinalizePlanningAsync_TransitionsDraftsAndParent()
{
var listId = await CreateListAsync();
var parent = MakeTask(listId, TaskStatus.Manual);
await _tasks.AddAsync(parent);
await _tasks.SetPlanningStartedAsync(parent.Id, "tok");
var c1 = await _tasks.CreateChildAsync(parent.Id, "c1", null, tagNames: new[] { "agent" }, commitType: null);
var c2 = await _tasks.CreateChildAsync(parent.Id, "c2", null, tagNames: null, commitType: null);
var count = await _tasks.FinalizePlanningAsync(parent.Id, queueAgentTasks: true);
Assert.Equal(2, count);
var c1Loaded = await _tasks.GetByIdAsync(c1.Id);
var c2Loaded = await _tasks.GetByIdAsync(c2.Id);
var parentLoaded = await _tasks.GetByIdAsync(parent.Id);
Assert.Equal(TaskStatus.Queued, c1Loaded!.Status);
Assert.Equal(TaskStatus.Manual, c2Loaded!.Status);
Assert.Equal(TaskStatus.Planned, parentLoaded!.Status);
Assert.NotNull(parentLoaded.PlanningFinalizedAt);
Assert.Null(parentLoaded.PlanningSessionToken);
}
[Fact]
public async Task FinalizePlanningAsync_QueueAgentTasksFalse_AllToManual()
{
var listId = await CreateListAsync();
var parent = MakeTask(listId, TaskStatus.Manual);
await _tasks.AddAsync(parent);
await _tasks.SetPlanningStartedAsync(parent.Id, "tok");
var c = await _tasks.CreateChildAsync(parent.Id, "c", null, tagNames: new[] { "agent" }, commitType: null);
await _tasks.FinalizePlanningAsync(parent.Id, queueAgentTasks: false);
var cLoaded = await _tasks.GetByIdAsync(c.Id);
Assert.Equal(TaskStatus.Manual, cLoaded!.Status);
}
[Fact]
public async Task FinalizePlanningAsync_ParentWithAgentListTag_ChildIsQueued()
{
var listId = await CreateListAsync();
var agentTagId = await _tags.GetOrCreateAsync("agent");
await _lists.AddTagAsync(listId, agentTagId);
var parent = MakeTask(listId, TaskStatus.Manual);
await _tasks.AddAsync(parent);
await _tasks.SetPlanningStartedAsync(parent.Id, "tok");
var c = await _tasks.CreateChildAsync(parent.Id, "c", null, tagNames: null, commitType: null);
await _tasks.FinalizePlanningAsync(parent.Id, queueAgentTasks: true);
var cLoaded = await _tasks.GetByIdAsync(c.Id);
Assert.Equal(TaskStatus.Queued, cLoaded!.Status);
}
} }