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
+17 -10
View File
@@ -216,14 +216,19 @@ public sealed class ExternalMcpService
.Distinct()
.ToList();
var dependencyStatuses = new Dictionary<string, TaskStatus>();
var predecessors = new Dictionary<string, (TaskStatus Status, bool Merged)>();
if (dependencyIds.Count > 0)
{
await using var ctx = await _dbFactory.CreateDbContextAsync(ct);
dependencyStatuses = await ctx.Tasks.AsNoTracking()
predecessors = 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 Dictionary<string, (bool Blocked, string? Reason)>();
@@ -239,7 +244,7 @@ public sealed class ExternalMcpService
}
else
{
var unmet = TaskBlocking.Unmet(t.DependsOnTaskIds, dependencyStatuses);
var unmet = TaskBlocking.Unmet(t.DependsOnTaskIds, predecessors);
result[t.Id] = unmet.Count == 0 ? (false, null) : (true, TaskBlocking.Describe(unmet));
}
}
@@ -290,13 +295,14 @@ public sealed class ExternalMcpService
"for normal coding, 'opus' only for complex or cross-cutting work. null inherits the " +
"list/global default (normally sonnet).")]
string? model = null,
[Description("Id of a task that must reach Done (i.e. be merged) before the picker will claim this one, " +
"even once queued. Shorthand for a one-element dependsOnTaskIds; ignored when that is given. " +
[Description("Id of a task whose merge must have landed before the picker will claim this one, even once " +
"queued (Done, or merged and still awaiting the batch verify). Shorthand for a " +
"one-element dependsOnTaskIds; ignored when that is given. " +
"Rejected if it doesn't exist, is this task's own id, or would create a " +
"dependency cycle. If that predecessor instead ends up Failed or Cancelled, this task simply " +
"stays blocked rather than starving silently -- check get_task/list_tasks' blocked field.")]
string? dependsOnTaskId = null,
[Description("Ids of ALL tasks that must reach Done before the picker will claim this one. Use this when a " +
[Description("Ids of ALL tasks whose merges must have landed before the picker will claim this one. Use this when a " +
"task wires together, or verifies, the results of several independent tasks -- one waiting on " +
"A and B is the normal shape of a wave-style plan. Same rejection rules as dependsOnTaskId.")]
string[]? dependsOnTaskIds = null,
@@ -460,12 +466,13 @@ public sealed class ExternalMcpService
string? title = null,
string? description = null,
string? commitType = null,
[Description("Id of a task that must reach Done before the picker will claim this one. Shorthand for a " +
[Description("Id of a task whose merge must have landed (Done, or merged and still awaiting the batch " +
"verify) before the picker will claim this one. Shorthand for a " +
"one-element dependsOnTaskIds; ignored when that is given. Pass an empty " +
"string to clear all existing links; null leaves them unchanged. Rejected if it doesn't exist, " +
"is this task's own id, or would create a dependency cycle.")]
string? dependsOnTaskId = null,
[Description("Ids of ALL tasks that must reach Done before the picker will claim this one, replacing the " +
[Description("Ids of ALL tasks whose merges must have landed before the picker will claim this one, replacing the " +
"task's current set. Pass an empty array to clear it; null leaves it unchanged. Use this when " +
"a task wires together, or verifies, the results of several independent tasks.")]
string[]? dependsOnTaskIds = null,