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:
committed by
mika kuns
co-authored by
Claude Opus 5
parent
50b6b56f87
commit
c874bf7e52
+17
-10
@@ -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,
|
||||
|
||||
+9
-6
@@ -10,19 +10,22 @@ namespace ClaudeDo.Worker.External;
|
||||
internal static class TaskBlocking
|
||||
{
|
||||
/// <summary>
|
||||
/// The predecessors of <paramref name="dependencyIds"/> that have not reached Done — an id
|
||||
/// missing from <paramref name="statuses"/> counts as unmet (the picker treats a dangling
|
||||
/// edge as blocking too).
|
||||
/// The predecessors of <paramref name="dependencyIds"/> the picker would still skip on: not
|
||||
/// Done <i>and</i> without a landed merge. Mirrors <c>QueuePicker</c>'s SQL predicate — a
|
||||
/// task merged with <c>skipVerify</c> sits in WaitingForReview until the batch verify runs,
|
||||
/// yet its code is on the target branch, so it no longer blocks. An id missing from
|
||||
/// <paramref name="predecessors"/> counts as unmet (a dangling edge blocks there too).
|
||||
/// </summary>
|
||||
public static List<(string Id, TaskStatus? Status)> Unmet(
|
||||
IEnumerable<string> dependencyIds, IReadOnlyDictionary<string, TaskStatus> statuses)
|
||||
IEnumerable<string> dependencyIds,
|
||||
IReadOnlyDictionary<string, (TaskStatus Status, bool Merged)> predecessors)
|
||||
{
|
||||
var unmet = new List<(string Id, TaskStatus? Status)>();
|
||||
foreach (var id in dependencyIds)
|
||||
{
|
||||
if (statuses.TryGetValue(id, out var status))
|
||||
if (predecessors.TryGetValue(id, out var p))
|
||||
{
|
||||
if (status != TaskStatus.Done) unmet.Add((id, status));
|
||||
if (p.Status != TaskStatus.Done && !p.Merged) unmet.Add((id, p.Status));
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
+11
-5
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user