Merge branch 'claudedo/340b4a2df0f14235a34fe0d02b2dc06f'
This commit is contained in:
+30
-14
@@ -104,11 +104,17 @@ public sealed record ResolveConflictHunkResultDto(
|
||||
// worktree-less handler task's HandlerBaseCommit == HandlerHeadCommit) -- distinguishable from
|
||||
// a merge that is merely small, so an empty branch can't be misread as "changedFileCount: 0
|
||||
// means tiny" when it actually means "nothing to review".
|
||||
// VerifyExitCode/VerifyDurationMs/VerifyOutputTail are null unless a verify run was attempted
|
||||
// (see PreviewMerge/PreviewMergeSet descriptions) -- 0 exit means the merge-tree result built/
|
||||
// tested clean; a non-zero or -1 (timeout/failed to start) exit means it doesn't, with the tail
|
||||
// of its output in VerifyOutputTail.
|
||||
public sealed record MergePreviewToolDto(
|
||||
string Status, IReadOnlyList<string> ConflictFiles, int ChangedFileCount, int Behind, bool IsEmpty = false);
|
||||
string Status, IReadOnlyList<string> ConflictFiles, int ChangedFileCount, int Behind, bool IsEmpty = false,
|
||||
int? VerifyExitCode = null, long? VerifyDurationMs = null, string? VerifyOutputTail = null);
|
||||
|
||||
public sealed record MergePreviewSetEntryDto(
|
||||
string TaskId, string Status, IReadOnlyList<string> ConflictFiles, int ChangedFileCount, int Behind, string? Error, bool IsEmpty = false);
|
||||
string TaskId, string Status, IReadOnlyList<string> ConflictFiles, int ChangedFileCount, int Behind, string? Error, bool IsEmpty = false,
|
||||
int? VerifyExitCode = null, long? VerifyDurationMs = null, string? VerifyOutputTail = null);
|
||||
|
||||
public sealed record FileOverlapDto(string File, IReadOnlyList<string> TaskIds);
|
||||
|
||||
@@ -1055,19 +1061,22 @@ public sealed class ExternalMcpService
|
||||
"Check whether a task would merge cleanly before touching anything — `git merge-tree --write-tree`, so the " +
|
||||
"working tree, index and HEAD are untouched. status is 'clean' or 'conflict' (conflictFiles lists where git " +
|
||||
"would stop); behind counts commits on targetBranch not yet on this branch, which flags a stale branch even " +
|
||||
"when the preview is clean. IMPORTANT: a clean preview says nothing about whether the result compiles or " +
|
||||
"passes tests — git can merge two changes cleanly (one file deleting a symbol another still references) and " +
|
||||
"still break the build. isEmpty=true means the task's review range contributed nothing; check that flag " +
|
||||
"rather than reading a small changedFileCount as empty. Throws if the task has neither an active worktree " +
|
||||
"nor a handler commit range, or the list's working directory is missing from disk.")]
|
||||
"when the preview is clean. If the list has a verify command configured, a clean preview is additionally " +
|
||||
"built/tested in a scratch worktree (still without touching the real working tree) — verifyExitCode 0 means " +
|
||||
"it built clean, non-zero or -1 (timeout/failed to start) means it doesn't, with the tail of its output in " +
|
||||
"verifyOutputTail; verifyExitCode stays null when no verify command is configured. isEmpty=true means the " +
|
||||
"task's review range contributed nothing; check that flag rather than reading a small changedFileCount as " +
|
||||
"empty. Throws if the task has neither an active worktree nor a handler commit range, or the list's working " +
|
||||
"directory is missing from disk.")]
|
||||
public async Task<MergePreviewToolDto> PreviewMerge(
|
||||
string taskId,
|
||||
[Description("Branch to preview against; defaults to the repo's current branch.")]
|
||||
string? targetBranch = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var (preview, behind, _, isEmpty) = await PreviewMergeCoreAsync(taskId, targetBranch, cancellationToken);
|
||||
return new MergePreviewToolDto(preview.Status, preview.ConflictFiles, preview.ChangedFileCount, behind, isEmpty);
|
||||
var (preview, behind, _, isEmpty) = await PreviewMergeCoreAsync(taskId, targetBranch, runVerify: true, cancellationToken);
|
||||
return new MergePreviewToolDto(preview.Status, preview.ConflictFiles, preview.ChangedFileCount, behind, isEmpty,
|
||||
preview.VerifyExitCode, preview.VerifyDurationMs, preview.VerifyOutputTail);
|
||||
}
|
||||
|
||||
[McpServerTool, Description(
|
||||
@@ -1079,11 +1088,17 @@ public sealed class ExternalMcpService
|
||||
"SupersetTaskId's — the strongest hint you get post-hoc that TaskId may be redundant with SupersetTaskId, " +
|
||||
"worth checking before merging both. IMPORTANT: neither overlap nor subset is a safety guarantee — two " +
|
||||
"tasks touching entirely different files (one deleting a symbol, another still referencing it) can still " +
|
||||
"collide unflagged, and as with preview_merge a clean result does not mean the merge builds.")]
|
||||
"collide unflagged, and as with preview_merge a clean result does not mean the merge builds. " +
|
||||
"runVerify=false (default) never builds — set it true to also run each task's list's verify command in a " +
|
||||
"scratch worktree per entry (same fields as preview_merge); this can take a long time across many tasks, " +
|
||||
"since builds run one at a time.")]
|
||||
public async Task<MergePreviewSetResultDto> PreviewMergeSet(
|
||||
IReadOnlyList<string> taskIds,
|
||||
[Description("Branch to preview every task against; defaults to the repo's current branch.")]
|
||||
string? targetBranch = null,
|
||||
[Description("true: also run the verify command (if configured) for each task, one build at a time. " +
|
||||
"false (default): no builds, however many tasks are given.")]
|
||||
bool runVerify = false,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (taskIds is null || taskIds.Count == 0)
|
||||
@@ -1096,9 +1111,10 @@ public sealed class ExternalMcpService
|
||||
{
|
||||
try
|
||||
{
|
||||
var (preview, behind, changedFiles, isEmpty) = await PreviewMergeCoreAsync(taskId, targetBranch, cancellationToken);
|
||||
var (preview, behind, changedFiles, isEmpty) = await PreviewMergeCoreAsync(taskId, targetBranch, runVerify, cancellationToken);
|
||||
entries.Add(new MergePreviewSetEntryDto(
|
||||
taskId, preview.Status, preview.ConflictFiles, preview.ChangedFileCount, behind, null, isEmpty));
|
||||
taskId, preview.Status, preview.ConflictFiles, preview.ChangedFileCount, behind, null, isEmpty,
|
||||
preview.VerifyExitCode, preview.VerifyDurationMs, preview.VerifyOutputTail));
|
||||
filesByTask[taskId] = changedFiles;
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
@@ -1156,7 +1172,7 @@ public sealed class ExternalMcpService
|
||||
// HandlerBaseCommit..HandlerHeadCommit range, reporting a synthetic "clean" preview of that
|
||||
// range's own diff-stat instead of throwing "has no worktree".
|
||||
private async Task<(MergePreviewResult Preview, int Behind, IReadOnlyList<string> ChangedFiles, bool IsEmpty)> PreviewMergeCoreAsync(
|
||||
string taskId, string? targetBranch, CancellationToken ct)
|
||||
string taskId, string? targetBranch, bool runVerify, CancellationToken ct)
|
||||
{
|
||||
using var ctx = _dbFactory.CreateDbContext();
|
||||
var task = await new TaskRepository(ctx).GetByIdAsync(taskId, ct)
|
||||
@@ -1173,7 +1189,7 @@ public sealed class ExternalMcpService
|
||||
if (string.IsNullOrWhiteSpace(list.WorkingDir) || !Directory.Exists(list.WorkingDir))
|
||||
throw new InvalidOperationException("The list's working directory no longer exists.");
|
||||
|
||||
var preview = await _merge.PreviewAsync(taskId, targetBranch ?? "", ct);
|
||||
var preview = await _merge.PreviewAsync(taskId, targetBranch ?? "", runVerify, ct);
|
||||
if (preview.Status == TaskMergeService.PreviewUnavailable)
|
||||
throw new InvalidOperationException(
|
||||
"Merge preview unavailable for this task (worktree inactive or repo is not a git repository).");
|
||||
|
||||
Reference in New Issue
Block a user