fix(worker): depends_on-Nachfolger schon beim gelandeten Merge freigeben

Der Queue-Picker gatete eine task_dependencies-Kante auf status='done'.
review_task(skipVerify) laesst einen erfolgreich gemergten Task aber
bewusst auf WaitingForReview stehen, bis verify_merges einmal zentral
laeuft -- damit blockierte jeder Nachfolger ueber den gesamten Batch plus
Full-Suite-Verify hinweg, bei freien Slots.

Eine Kante gilt jetzt als erfuellt, wenn der Vorgaenger 'done' ist ODER
sein Merge gelandet ist (worktrees.state = 'merged'). TaskBlocking
(get_task/list_tasks/wait_for_task_change) und der UI-Wartehinweis folgen
derselben Regel, damit "blocked" nicht meldet, was der Picker claimt.

Abgeleitet: revert_merge/abort_merge setzen den Worktree auf Active und
blockieren wieder; reject_rerun/reject_park haben nie gemergt und bleiben
unerfuellt; ein Task ohne Worktree wird nur ueber 'done' erfuellt; ein
fehlgeschlagener verify_merges gibt Nachfolger frei, weil der Code so
oder so auf main liegt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Mika Kuns
2026-09-16 15:43:37 +02:00
committed by mika kuns
co-authored by Claude Opus 5
parent 50b6b56f87
commit c874bf7e52
8 changed files with 159 additions and 31 deletions
+11 -5
View File
@@ -1,5 +1,6 @@
using System.ComponentModel;
using ClaudeDo.Data;
using ClaudeDo.Data.Models;
using ClaudeDo.Data.Repositories;
using Microsoft.EntityFrameworkCore;
using ModelContextProtocol;
@@ -152,12 +153,17 @@ public sealed class TaskWaitMcpTools
.SelectMany(r => r.DependsOnTaskIds)
.Distinct()
.ToList();
var dependencyStatuses = dependencyIds.Count == 0
? new Dictionary<string, TaskStatus>()
var predecessors = dependencyIds.Count == 0
? new Dictionary<string, (TaskStatus Status, bool Merged)>()
: await ctx.Tasks.AsNoTracking()
.Where(t => dependencyIds.Contains(t.Id))
.Select(t => new { t.Id, t.Status })
.ToDictionaryAsync(t => t.Id, t => t.Status, ct);
.Select(t => new
{
t.Id,
t.Status,
Merged = t.Worktree != null && t.Worktree.State == WorktreeState.Merged,
})
.ToDictionaryAsync(t => t.Id, t => (t.Status, t.Merged), ct);
var result = new List<TaskStatusChangeDto>();
foreach (var id in taskIds)
@@ -175,7 +181,7 @@ public sealed class TaskWaitMcpTools
// every chained child and turn the wait into a one-turn-per-poll busy loop.
if (row.Status == TaskStatus.Queued && row.BlockedByTaskId is null)
{
var unmet = TaskBlocking.Unmet(row.DependsOnTaskIds, dependencyStatuses);
var unmet = TaskBlocking.Unmet(row.DependsOnTaskIds, predecessors);
if (unmet.Count > 0)
{
result.Add(new TaskStatusChangeDto(