From f25c7599bd9999dbd918ffd43b32e887b6b51af3 Mon Sep 17 00:00:00 2001 From: mika kuns Date: Thu, 4 Jun 2026 15:35:06 +0200 Subject: [PATCH] fix(children): exempt improvement children from orphan-dequeue sweep --- .../Repositories/TaskRepository.cs | 3 ++ .../TaskRepositoryOrphanGuardTests.cs | 30 ++++++++++++++++--- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/src/ClaudeDo.Data/Repositories/TaskRepository.cs b/src/ClaudeDo.Data/Repositories/TaskRepository.cs index a5e1ba0..d034f54 100644 --- a/src/ClaudeDo.Data/Repositories/TaskRepository.cs +++ b/src/ClaudeDo.Data/Repositories/TaskRepository.cs @@ -393,6 +393,9 @@ public sealed class TaskRepository { var orphanIds = await _context.Tasks .Where(t => t.ParentTaskId != null && t.Status == TaskStatus.Queued) + // Agent-suggested improvement children (CreatedBy == ParentTaskId) legitimately + // queue under a non-planning parent — they are not orphaned planning-chain members. + .Where(t => t.CreatedBy == null || t.CreatedBy != t.ParentTaskId) .Where(t => !_context.Tasks.Any(p => p.Id == t.ParentTaskId && p.PlanningPhase != PlanningPhase.None)) .Select(t => t.Id) diff --git a/tests/ClaudeDo.Worker.Tests/Repositories/TaskRepositoryOrphanGuardTests.cs b/tests/ClaudeDo.Worker.Tests/Repositories/TaskRepositoryOrphanGuardTests.cs index b4c9647..e8de8b5 100644 --- a/tests/ClaudeDo.Worker.Tests/Repositories/TaskRepositoryOrphanGuardTests.cs +++ b/tests/ClaudeDo.Worker.Tests/Repositories/TaskRepositoryOrphanGuardTests.cs @@ -61,15 +61,18 @@ public sealed class TaskRepositoryOrphanGuardTests : IDisposable // --- CreateChildAsync validation --- [Fact] - public async Task CreateChildAsync_Throws_When_Parent_Has_No_Planning_Phase() + public async Task CreateChildAsync_Succeeds_On_NonPlanning_Parent_For_Improvement_Children() { + // The planning-phase guard was removed: improvement children attach to a + // non-planning parent, with CreatedBy stamped to the parent id by the caller. var listId = await CreateListAsync(); var parent = MakeTask(listId, phase: PlanningPhase.None); await _tasks.AddAsync(parent); - var ex = await Assert.ThrowsAsync( - () => _tasks.CreateChildAsync(parent.Id, "child", null, null)); - Assert.Contains("not in a planning phase", ex.Message); + var child = await _tasks.CreateChildAsync(parent.Id, "child", null, null, createdBy: parent.Id); + Assert.Equal(parent.Id, child.ParentTaskId); + Assert.Equal(parent.Id, child.CreatedBy); + Assert.Equal(TaskStatus.Idle, child.Status); } [Fact] @@ -201,6 +204,25 @@ public sealed class TaskRepositoryOrphanGuardTests : IDisposable Assert.Equal(parent.Id, reloaded.ParentTaskId); // lineage stays } + [Fact] + public async Task Dequeue_Leaves_Queued_Improvement_Children_Alone() + { + // Improvement children (CreatedBy == ParentTaskId) queue under a non-planning + // parent on purpose — the orphan sweep must NOT reset them. + var listId = await CreateListAsync(); + var parent = MakeTask(listId, phase: PlanningPhase.None); + await _tasks.AddAsync(parent); + var child = MakeTask(listId, status: TaskStatus.Queued); + child.ParentTaskId = parent.Id; + child.CreatedBy = parent.Id; + await _tasks.AddAsync(child); + + var dequeued = await _tasks.DequeueOrphanedChildrenAsync(); + + Assert.Equal(0, dequeued); + Assert.Equal(TaskStatus.Queued, _ctx.Tasks.AsNoTracking().Single(t => t.Id == child.Id).Status); + } + [Fact] public async Task Dequeue_Leaves_Idle_Children_Of_NonPlanning_Parent_Alone() {