chore: merge main into task branch
This commit is contained in:
@@ -45,6 +45,7 @@ public sealed record BatchTaskDetailDto(
|
||||
int? SortOrder = null,
|
||||
int? RoadblockCount = null,
|
||||
string? RoadblockText = null,
|
||||
string? FailureReason = null,
|
||||
bool DescriptionTruncated = false,
|
||||
int? DescriptionFullLength = null,
|
||||
bool ResultTruncated = false,
|
||||
@@ -80,6 +81,7 @@ public sealed class BatchMcpTools
|
||||
{
|
||||
"listId", "title", "description", "status", "result", "createdBy", "createdAt",
|
||||
"startedAt", "finishedAt", "isMyDay", "sortOrder", "roadblockCount", "roadblockText",
|
||||
"failureReason",
|
||||
};
|
||||
|
||||
private readonly ExternalMcpService _svc;
|
||||
@@ -187,6 +189,7 @@ public sealed class BatchMcpTools
|
||||
SortOrder: Want("sortOrder") ? t.SortOrder : null,
|
||||
RoadblockCount: Want("roadblockCount") ? t.RoadblockCount : null,
|
||||
RoadblockText: Want("roadblockText") ? ExtractRoadblockText(t.Result) : null,
|
||||
FailureReason: Want("failureReason") ? t.FailureReason : null,
|
||||
DescriptionTruncated: descTruncated,
|
||||
DescriptionFullLength: descFullLength,
|
||||
ResultTruncated: resultTruncated,
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
using ClaudeDo.Data;
|
||||
using ClaudeDo.Data.Git;
|
||||
using ClaudeDo.Data.Repositories;
|
||||
using ClaudeDo.Worker.Hub;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace ClaudeDo.Worker.Lifecycle;
|
||||
|
||||
/// <summary>
|
||||
/// Startup-only sweep: warns when a list's repo has git-registered <c>claudedo/*</c> worktrees
|
||||
/// that the <c>worktrees</c> table doesn't know about (e.g. left behind by a manual cleanup
|
||||
/// outside the app). Detection only — nothing is removed or pruned.
|
||||
/// </summary>
|
||||
public sealed class UntrackedWorktreeRecovery : IHostedService
|
||||
{
|
||||
public const string Phase = "untracked-worktrees";
|
||||
|
||||
private const string BranchPrefix = "claudedo/";
|
||||
|
||||
private readonly IDbContextFactory<ClaudeDoDbContext> _dbFactory;
|
||||
private readonly GitService _git;
|
||||
private readonly HubBroadcaster _broadcaster;
|
||||
private readonly ILogger<UntrackedWorktreeRecovery> _logger;
|
||||
|
||||
public UntrackedWorktreeRecovery(
|
||||
IDbContextFactory<ClaudeDoDbContext> dbFactory,
|
||||
GitService git,
|
||||
HubBroadcaster broadcaster,
|
||||
ILogger<UntrackedWorktreeRecovery> logger)
|
||||
{
|
||||
_dbFactory = dbFactory;
|
||||
_git = git;
|
||||
_broadcaster = broadcaster;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await using var ctx = await _dbFactory.CreateDbContextAsync(cancellationToken);
|
||||
var lists = await new ListRepository(ctx).GetAllAsync(cancellationToken);
|
||||
var dbPaths = (await new WorktreeRepository(ctx).GetAllAsync(cancellationToken))
|
||||
.Select(w => w.Path)
|
||||
.ToList();
|
||||
|
||||
var foundAny = false;
|
||||
foreach (var list in lists.Where(l => !string.IsNullOrWhiteSpace(l.WorkingDir)))
|
||||
{
|
||||
List<(string Path, string? Branch)> registered;
|
||||
try
|
||||
{
|
||||
registered = await _git.ListWorktreesAsync(list.WorkingDir!, cancellationToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Untracked worktree recovery: git worktree list failed for {WorkingDir}", list.WorkingDir);
|
||||
continue;
|
||||
}
|
||||
|
||||
var claudedoPaths = registered
|
||||
.Where(w => w.Branch is not null && w.Branch.StartsWith(BranchPrefix, StringComparison.Ordinal))
|
||||
.Select(w => w.Path);
|
||||
|
||||
var untracked = FindUntrackedPaths(claudedoPaths, dbPaths);
|
||||
if (untracked.Count == 0) continue;
|
||||
|
||||
foundAny = true;
|
||||
_logger.LogWarning(
|
||||
"Untracked worktree recovery: found {Count} git worktree(s) under '{Repo}' that ClaudeDo's database doesn't know about; not touched",
|
||||
untracked.Count, list.WorkingDir);
|
||||
}
|
||||
|
||||
if (!foundAny)
|
||||
_logger.LogInformation("Untracked worktree recovery: no untracked worktrees found");
|
||||
|
||||
await _broadcaster.OperationProgress(OperationProgressOpKeys.StartupRecovery, Phase, lists.Count, lists.Count);
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
|
||||
/// <summary>Registered worktree paths not present among <paramref name="dbPaths"/>, comparing normalized full paths.</summary>
|
||||
public static List<string> FindUntrackedPaths(IEnumerable<string> registeredPaths, IEnumerable<string> dbPaths)
|
||||
{
|
||||
var known = dbPaths.Select(Normalize).ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
return registeredPaths.Where(p => !known.Contains(Normalize(p))).ToList();
|
||||
}
|
||||
|
||||
private static string Normalize(string path) =>
|
||||
Path.GetFullPath(path).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
|
||||
}
|
||||
@@ -77,6 +77,7 @@ builder.Services.AddHostedService<OrphanRecovery>();
|
||||
builder.Services.AddHostedService<AttachmentOrphanRecovery>();
|
||||
builder.Services.AddHostedService<PromptFileRecovery>();
|
||||
builder.Services.AddHostedService<LegacyWorktreeFolderRecovery>();
|
||||
builder.Services.AddHostedService<UntrackedWorktreeRecovery>();
|
||||
builder.Services.AddSignalR().AddJsonProtocol(options =>
|
||||
{
|
||||
options.PayloadSerializerOptions.Converters.Add(new System.Text.Json.Serialization.JsonStringEnumConverter());
|
||||
|
||||
Reference in New Issue
Block a user