feat(merge): verify once per batch via skipVerify + verify_merges
Per-merge verify made an N-task batch pay N x ~7 min, each run testing the same moving main. review_task/merge_task/continue_merge now take skipVerify: the merge lands but the gate AND the Done transition are deferred (status merged_verify_pending, task stays WaitingForReview). The new verify_merges tool runs the list verify command once for the explicitly listed tasks and promotes them to Done on success - explicit ids so the handler own submitted task and parked verify_failed tasks are never swept up; an Active worktree is refused per entry. PlanningMergeOrchestrator threads the flag through the unit merge (no more per-child verify) and skips FinalizeParentDoneAsync when verifies were deferred. The merge-helper Merge prompt approves with skipVerify=true and calls verify_merges once after the last merge. UI approve is unchanged; the no-Done-without-green-verify invariant stays server-enforced.
This commit is contained in:
@@ -44,6 +44,13 @@ public sealed class PlanningMergeOrchestrator : IActiveMergeState
|
||||
/// from a running Claude session) rather than a direct UI action. Conflicts on such a
|
||||
/// merge must not auto-pop the in-app resolver — the driving session owns resolution.</summary>
|
||||
public required bool ExternallyDriven { get; init; }
|
||||
/// <summary>Batch mode: every child merge defers the verify gate, and the parent is NOT
|
||||
/// finalized to Done — a later RunListVerifyAsync (verify_merges) runs the gate once and
|
||||
/// promotes it. Only meaningful when the list has a verify command at all.</summary>
|
||||
public required bool SkipVerify { get; init; }
|
||||
/// <summary>Set when any child merge came back merged_verify_pending — the drain must then
|
||||
/// end without finalizing the parent.</summary>
|
||||
public bool AnyVerifyPending { get; set; }
|
||||
public string? CurrentSubtaskId { get; set; }
|
||||
/// <summary>True from the moment the last child has merged until FinalizeParentDoneAsync
|
||||
/// returns. CurrentSubtaskId is already null in this window (no subtask left to merge),
|
||||
@@ -75,7 +82,7 @@ public sealed class PlanningMergeOrchestrator : IActiveMergeState
|
||||
|
||||
public async Task<PlanningMergeResult> StartAsync(
|
||||
string parentTaskId, string targetBranch, CancellationToken ct, bool externallyDriven = false,
|
||||
IProgress<ProgressNotificationValue>? progress = null)
|
||||
IProgress<ProgressNotificationValue>? progress = null, bool skipVerify = false)
|
||||
{
|
||||
string workingDir;
|
||||
List<TaskEntity> children;
|
||||
@@ -143,6 +150,7 @@ public sealed class PlanningMergeOrchestrator : IActiveMergeState
|
||||
IsPlanning = isPlanning,
|
||||
WorkingDir = workingDir,
|
||||
ExternallyDriven = externallyDriven,
|
||||
SkipVerify = skipVerify,
|
||||
};
|
||||
if (!_states.TryAdd(parentTaskId, state))
|
||||
throw new InvalidOperationException($"Merge already in progress for {parentTaskId}.");
|
||||
@@ -181,7 +189,7 @@ public sealed class PlanningMergeOrchestrator : IActiveMergeState
|
||||
"no in-progress merge to continue; if the worker was restarted during a conflict, use AbortPlanningMerge to reset the repository");
|
||||
|
||||
var current = state.CurrentSubtaskId;
|
||||
var result = await _merge.ContinueMergeAsync(current, ct, progress);
|
||||
var result = await _merge.ContinueMergeAsync(current, ct, progress, state.SkipVerify);
|
||||
|
||||
if (result.Status == TaskMergeService.StatusConflict)
|
||||
{
|
||||
@@ -189,7 +197,11 @@ public sealed class PlanningMergeOrchestrator : IActiveMergeState
|
||||
return PlanningMergeResult.Conflict;
|
||||
}
|
||||
|
||||
if (result.Status != TaskMergeService.StatusMerged)
|
||||
if (result.Status == TaskMergeService.StatusMergedVerifyPending)
|
||||
{
|
||||
state.AnyVerifyPending = true;
|
||||
}
|
||||
else if (result.Status != TaskMergeService.StatusMerged)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Planning continue blocked on subtask {Subtask}: {Msg}",
|
||||
@@ -262,7 +274,8 @@ public sealed class PlanningMergeOrchestrator : IActiveMergeState
|
||||
commitMessage: "", // blank -> TaskMergeService builds the conventional default
|
||||
leaveConflictsInTree: true,
|
||||
ct,
|
||||
progress);
|
||||
progress,
|
||||
state.SkipVerify);
|
||||
|
||||
if (result.Status == TaskMergeService.StatusConflict)
|
||||
{
|
||||
@@ -272,7 +285,11 @@ public sealed class PlanningMergeOrchestrator : IActiveMergeState
|
||||
return PlanningMergeResult.Conflict;
|
||||
}
|
||||
|
||||
if (result.Status != TaskMergeService.StatusMerged)
|
||||
if (result.Status == TaskMergeService.StatusMergedVerifyPending)
|
||||
{
|
||||
state.AnyVerifyPending = true;
|
||||
}
|
||||
else if (result.Status != TaskMergeService.StatusMerged)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Planning merge blocked on subtask {Subtask}: {Msg}",
|
||||
@@ -284,10 +301,21 @@ public sealed class PlanningMergeOrchestrator : IActiveMergeState
|
||||
await _broadcaster.PlanningSubtaskMerged(planningTaskId, subtaskId);
|
||||
}
|
||||
|
||||
state.CurrentSubtaskId = null;
|
||||
|
||||
// Batch mode with deferred verifies: the parent must NOT be finalized to Done here --
|
||||
// that would grant a Done no verify ever gated. It stays WaitingForReview until the
|
||||
// caller's verify_merges passes and promotes it. PlanningCompleted still fires: the
|
||||
// drain itself IS complete, and the UI needs its draining banner cleared.
|
||||
if (state.AnyVerifyPending)
|
||||
{
|
||||
await _broadcaster.PlanningCompleted(planningTaskId);
|
||||
return new PlanningMergeResult(TaskMergeService.StatusMergedVerifyPending, null);
|
||||
}
|
||||
|
||||
// No subtask left to merge, but the parent isn't Done yet -- HasActiveMerge must keep
|
||||
// reporting true through this window (CurrentSubtaskId is already null) so a Cancel
|
||||
// racing FinalizeParentDoneAsync's ApproveReviewAsync call is still refused.
|
||||
state.CurrentSubtaskId = null;
|
||||
state.IsFinalizing = true;
|
||||
var (finalized, reason) = await FinalizeParentDoneAsync(planningTaskId, state.IsPlanning, ct);
|
||||
if (finalized)
|
||||
|
||||
Reference in New Issue
Block a user