diff --git a/src/ClaudeDo.Data/CLAUDE.md b/src/ClaudeDo.Data/CLAUDE.md
index 5809147..9152fbb 100644
--- a/src/ClaudeDo.Data/CLAUDE.md
+++ b/src/ClaudeDo.Data/CLAUDE.md
@@ -35,7 +35,7 @@ All repositories use EF Core LINQ queries via `ClaudeDoDbContext`. The atomic `Q
## Git
-- **GitService** — async wrapper around git CLI (ProcessStartInfo, no shell). Worktree ops (add — serialized to avoid a commondir race —, remove, prune, list paths for branch), branch ops (current, list local, checkout, delete), staging/commit (status porcelain, add-all, add-path, commit via stdin), diffs (working tree, branch vs base, commit range `base..head` — used to show a merged task's diff after the worktree is gone —, per-file, diff-stat, committed files, has-changes), merge (ff-only, no-ff, abort, mid-merge detection, conflicted files, show-stage for conflict hunks), `PreviewMergeAsync` (non-destructive mergeability check via `git merge-tree --write-tree`), `CountChangedFilesAsync`, rev-parse, is-git-repo
+- **GitService** — async wrapper around git CLI (ProcessStartInfo, no shell). Worktree ops (add — serialized to avoid a commondir race —, remove, prune, list paths for branch), branch ops (current, list local, checkout, delete), staging/commit (status porcelain, add-all, add-path, commit via stdin), diffs (working tree, branch vs base, commit range `base..head` — used to show a merged task's diff after the worktree is gone —, per-file, diff-stat, committed files, has-changes), merge (ff-only, no-ff, abort, mid-merge detection, conflicted files), `PreviewMergeAsync` (non-destructive mergeability check via `git merge-tree --write-tree`), `CountChangedFilesAsync`, rev-parse, is-git-repo
## Schema
diff --git a/src/ClaudeDo.Data/Git/GitService.cs b/src/ClaudeDo.Data/Git/GitService.cs
index b07c77c..aad3bf4 100644
--- a/src/ClaudeDo.Data/Git/GitService.cs
+++ b/src/ClaudeDo.Data/Git/GitService.cs
@@ -280,17 +280,6 @@ public sealed class GitService
.ToList();
}
- ///
- /// Reads a conflicted file's blob at a merge stage: 1=base, 2=ours, 3=theirs.
- /// Returns null when the stage doesn't exist (e.g. add/add conflict has no base).
- /// Output is NOT trimmed so file content round-trips exactly.
- ///
- public async Task ShowStageAsync(string repoDir, int stage, string path, CancellationToken ct = default)
- {
- var (exitCode, stdout, _) = await RunGitAsync(repoDir, ["show", $":{stage}:{path}"], ct, trimOutput: false);
- return exitCode == 0 ? stdout : null;
- }
-
public async Task AddPathAsync(string repoDir, string path, CancellationToken ct = default)
{
var (exitCode, _, stderr) = await RunGitAsync(repoDir, ["add", "--", path], ct);
diff --git a/src/ClaudeDo.Ui/CLAUDE.md b/src/ClaudeDo.Ui/CLAUDE.md
index 55a51bc..0650031 100644
--- a/src/ClaudeDo.Ui/CLAUDE.md
+++ b/src/ClaudeDo.Ui/CLAUDE.md
@@ -45,7 +45,7 @@ Design/ — Tokens.axaml (design tokens; merged before styles) + IslandStyle
## Services
-- **WorkerClient** / **IWorkerClient** — SignalR client connecting to `http://127.0.0.1:47821/hub`, auto-reconnect with exponential backoff. The surface tracks `WorkerHub` (see `src/ClaudeDo.Worker/CLAUDE.md` for the canonical method/event list); groups: task execution (RunNow/Cancel/Continue/Reset/SetTaskStatus), review (`ApproveReviewAsync(taskId, targetBranch) -> MergeResultDto`, reject-to-queue/idle, cancel review, `PreviewMergeAsync -> MergePreviewDto`), planning sessions (start/resume/discard/finalize, queue subtasks, pending draft count, interactive terminal, refine), planning aggregate/integration-branch diffs, unit-merge continue/abort, single-task conflict resolving (start/get-conflicts/write-resolution/continue/abort), worktrees (overview, set state, force remove, cleanup, reset all), agents, app settings, lists/config, weekly report, daily notes, daily prep (`RunDailyPrepNowAsync`, `ClearMyDayAsync`, `GetLastPrepLogAsync`), prime schedules. Events mirror `HubBroadcaster` (task/worktree/list/run updates, prep events, planning-merge events, refine events, worker log). Lifecycle (`StartAsync`/`StopAsync`) and a few admin methods live only on the concrete `WorkerClient`.
+- **WorkerClient** / **IWorkerClient** — SignalR client connecting to `http://127.0.0.1:47821/hub`, auto-reconnect with exponential backoff. The surface tracks `WorkerHub` (see `src/ClaudeDo.Worker/CLAUDE.md` for the canonical method/event list); groups: task execution (RunNow/Cancel/Continue/Reset/SetTaskStatus), review (`ApproveReviewAsync(taskId, targetBranch) -> MergeResultDto`, reject-to-queue/idle, cancel review, `PreviewMergeAsync -> MergePreviewDto`), planning sessions (start/resume/discard/finalize, queue subtasks, pending draft count, interactive terminal, refine), planning aggregate/integration-branch diffs, unit-merge continue/abort, single-task conflict resolving (start/get-conflict-documents/write-resolution/continue/abort), worktrees (overview, set state, force remove, cleanup, reset all), agents, app settings, lists/config, weekly report, daily notes, daily prep (`RunDailyPrepNowAsync`, `ClearMyDayAsync`, `GetLastPrepLogAsync`), prime schedules. Events mirror `HubBroadcaster` (task/worktree/list/run updates, prep events, planning-merge events, refine events, worker log). Lifecycle (`StartAsync`/`StopAsync`) and a few admin methods live only on the concrete `WorkerClient`.
- **INotesApi** / **WorkerNotesApi** — daily-note CRUD (`ListAsync(day)`, `AddAsync`, `UpdateAsync`, `DeleteAsync`); UI DTO `DailyNoteDto(Id, Date, Text, SortOrder)`.
- **IPrimeScheduleApi** — prime-schedule CRUD (`ListAsync`, `UpsertAsync`, `DeleteAsync`).
- **UpdateCheckService** — polls releases, exposes `LastCheckStatus`/`LatestVersion`/`CheckNowAsync` (feeds the shell's update banner).
diff --git a/src/ClaudeDo.Ui/Services/Interfaces/IWorkerClient.cs b/src/ClaudeDo.Ui/Services/Interfaces/IWorkerClient.cs
index 8e317d4..d357001 100644
--- a/src/ClaudeDo.Ui/Services/Interfaces/IWorkerClient.cs
+++ b/src/ClaudeDo.Ui/Services/Interfaces/IWorkerClient.cs
@@ -54,7 +54,6 @@ public interface IWorkerClient : INotifyPropertyChanged
// ── Conflict resolution (worker hub side implemented by Layer C) ──
Task StartConflictMergeAsync(string taskId, string targetBranch);
- Task GetMergeConflictsAsync(string taskId);
Task GetMergeConflictDocumentsAsync(string taskId);
Task WriteConflictResolutionAsync(string taskId, string path, string resolvedContent);
Task ContinueConflictMergeAsync(string taskId);
diff --git a/src/ClaudeDo.Ui/Services/WorkerClient.cs b/src/ClaudeDo.Ui/Services/WorkerClient.cs
index 623d6a9..9d12f5c 100644
--- a/src/ClaudeDo.Ui/Services/WorkerClient.cs
+++ b/src/ClaudeDo.Ui/Services/WorkerClient.cs
@@ -272,9 +272,6 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
public Task StartConflictMergeAsync(string taskId, string targetBranch)
=> _hub.InvokeAsync("StartConflictMerge", taskId, targetBranch);
- public Task GetMergeConflictsAsync(string taskId)
- => _hub.InvokeAsync("GetMergeConflicts", taskId);
-
public Task GetMergeConflictDocumentsAsync(string taskId)
=> _hub.InvokeAsync("GetMergeConflictDocuments", taskId);
@@ -559,9 +556,6 @@ public sealed record WorktreeResetDto(int Removed, int TasksAffected, bool Block
public record MergeResultDto(string Status, IReadOnlyList ConflictFiles, string? ErrorMessage);
public record MergePreviewDto(string Status, IReadOnlyList ConflictFiles, int ChangedFileCount);
public record MergeTargetsDto(string DefaultBranch, IReadOnlyList LocalBranches);
-public record MergeConflictsDto(string TaskId, IReadOnlyList Files);
-public record ConflictFileDto(string Path, IReadOnlyList Hunks);
-public record ConflictHunkDto(string Ours, string Theirs, string? Base);
public record MergeConflictDocumentsDto(string TaskId, IReadOnlyList Files);
public record ConflictDocumentDto(string Path, bool IsBinary, IReadOnlyList Segments);
public record MergeSegmentDto(bool IsConflict, string Text, string Ours, string? Base, string Theirs);
diff --git a/src/ClaudeDo.Worker/CLAUDE.md b/src/ClaudeDo.Worker/CLAUDE.md
index d7de8ad..730f1e8 100644
--- a/src/ClaudeDo.Worker/CLAUDE.md
+++ b/src/ClaudeDo.Worker/CLAUDE.md
@@ -150,7 +150,7 @@ Each CLI invocation is recorded in the `task_runs` table via `TaskRunRepository`
- Execution: `Ping`, `GetActive`, `RunNow`, `CancelTask`, `WakeQueue`, `ContinueTask`, `ResetTask`, `SetTaskStatus`, `RefineTask`
- Review/merge: `ApproveReview(taskId, targetBranch) -> MergeResultDto` (childless task: merges its worktree then Done, conflict stays WaitingForReview; task with children: drives `PlanningMergeOrchestrator` to merge the whole unit), `ContinuePlanningMerge` / `AbortPlanningMerge` (resolve a unit-merge conflict), `PreviewMerge(taskId, targetBranch) -> MergePreviewDto` (non-destructive mergeability check), `RejectReviewToQueue`, `RejectReviewToIdle`, `CancelReview`, `MergeTask`, `GetMergeTargets`
-- Single-task conflict resolver (Layer C): `StartConflictMerge`, `GetMergeConflicts` (hunks), `WriteConflictResolution`, `ContinueConflictMerge`, `AbortConflictMerge` (service-level `TaskMergeService.ContinueMergeAsync`/`AbortMergeAsync` keep their names)
+- Single-task conflict resolver (Layer C): `StartConflictMerge`, `GetMergeConflictDocuments` (segments), `WriteConflictResolution`, `ContinueConflictMerge`, `AbortConflictMerge` (service-level `TaskMergeService.ContinueMergeAsync`/`AbortMergeAsync` keep their names)
- Planning sessions: `StartPlanningSession`, `ResumePlanningSession`, `DiscardPlanningSession`, `FinalizePlanningSession`, `QueuePlanningSubtasks`, `GetPendingDraftCount`, `OpenInteractiveTerminal`, `GetPlanningAggregate` (per-subtask diffs), `BuildPlanningIntegrationBranch` (combined diff)
- Worktrees: `CleanupFinishedWorktrees`, `ResetAllWorktrees`, `GetWorktreesOverview`, `SetWorktreeState`, `ForceRemoveWorktree`
- Agents/settings/lists: `GetAgents`, `RefreshAgents`, `RestoreDefaultAgents`, `GetAppSettings`, `UpdateAppSettings`, `UpdateList`, `UpdateListConfig`, `GetListConfig`, `UpdateTaskAgentSettings`
diff --git a/src/ClaudeDo.Worker/Hub/WorkerHub.cs b/src/ClaudeDo.Worker/Hub/WorkerHub.cs
index 9979357..f040fb8 100644
--- a/src/ClaudeDo.Worker/Hub/WorkerHub.cs
+++ b/src/ClaudeDo.Worker/Hub/WorkerHub.cs
@@ -58,9 +58,6 @@ public record ForceRemoveResultDto(bool Removed, string? Reason);
public record MergeResultDto(string Status, IReadOnlyList ConflictFiles, string? ErrorMessage);
public record MergePreviewDto(string Status, IReadOnlyList ConflictFiles, int ChangedFileCount);
public record MergeTargetsDto(string DefaultBranch, IReadOnlyList LocalBranches);
-public record MergeConflictsDto(string TaskId, IReadOnlyList Files);
-public record ConflictFileDto(string Path, IReadOnlyList Hunks);
-public record ConflictHunkDto(string Ours, string Theirs, string? Base);
public record MergeConflictDocumentsDto(string TaskId, IReadOnlyList Files);
public record ConflictDocumentDto(string Path, bool IsBinary, IReadOnlyList Segments);
public record MergeSegmentDto(bool IsConflict, string Text, string Ours, string? Base, string Theirs);
@@ -375,17 +372,6 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
return new MergeResultDto(r.Status, r.ConflictFiles, r.ErrorMessage);
});
- public Task GetMergeConflicts(string taskId)
- => HubGuard(async () =>
- {
- var c = await _mergeService.GetConflictsAsync(taskId, CancellationToken.None);
- return new MergeConflictsDto(
- c.TaskId,
- c.Files.Select(f => new ConflictFileDto(
- f.Path,
- new[] { new ConflictHunkDto(f.Ours, f.Theirs, f.Base) })).ToList());
- });
-
public Task GetMergeConflictDocuments(string taskId)
=> HubGuard(async () =>
{
diff --git a/src/ClaudeDo.Worker/Lifecycle/TaskMergeService.cs b/src/ClaudeDo.Worker/Lifecycle/TaskMergeService.cs
index 6e3dfcf..2789589 100644
--- a/src/ClaudeDo.Worker/Lifecycle/TaskMergeService.cs
+++ b/src/ClaudeDo.Worker/Lifecycle/TaskMergeService.cs
@@ -23,16 +23,6 @@ public sealed record MergePreviewResult(
IReadOnlyList ConflictFiles,
int ChangedFileCount);
-public sealed record MergeConflicts(
- string TaskId,
- IReadOnlyList Files);
-
-public sealed record ConflictFileContent(
- string Path,
- string Ours,
- string Theirs,
- string? Base);
-
public sealed record ConflictDocuments(
string TaskId,
IReadOnlyList Files);
@@ -247,24 +237,6 @@ public sealed class TaskMergeService
return new MergeResult(StatusAborted, Array.Empty(), null);
}
- public async Task GetConflictsAsync(string taskId, CancellationToken ct)
- {
- var (_, list, _) = await LoadMergeContextAsync(taskId, ct);
- if (string.IsNullOrWhiteSpace(list.WorkingDir))
- throw new InvalidOperationException("list has no working directory");
-
- var files = await _git.ListConflictedFilesAsync(list.WorkingDir, ct);
- var result = new List(files.Count);
- foreach (var path in files)
- {
- var ours = await _git.ShowStageAsync(list.WorkingDir, 2, path, ct) ?? "";
- var theirs = await _git.ShowStageAsync(list.WorkingDir, 3, path, ct) ?? "";
- var @base = await _git.ShowStageAsync(list.WorkingDir, 1, path, ct);
- result.Add(new ConflictFileContent(path, ours, theirs, @base));
- }
- return new MergeConflicts(taskId, result);
- }
-
///
/// Reads each conflicted working-tree file and parses its conflict markers into line-level
/// segments (with the diff3 merge base when present). Binary files are flagged and skipped.
diff --git a/tests/ClaudeDo.Ui.Tests/StubWorkerClient.cs b/tests/ClaudeDo.Ui.Tests/StubWorkerClient.cs
index 092e1f9..cc166de 100644
--- a/tests/ClaudeDo.Ui.Tests/StubWorkerClient.cs
+++ b/tests/ClaudeDo.Ui.Tests/StubWorkerClient.cs
@@ -65,7 +65,6 @@ public abstract class StubWorkerClient : IWorkerClient
public virtual Task RejectReviewToIdleAsync(string taskId) => Task.CompletedTask;
public virtual Task CancelReviewAsync(string taskId) => Task.CompletedTask;
public virtual Task StartConflictMergeAsync(string taskId, string targetBranch) => Task.FromResult(new MergeResultDto("conflict", System.Array.Empty(), null));
- public virtual Task GetMergeConflictsAsync(string taskId) => Task.FromResult(new MergeConflictsDto(taskId, System.Array.Empty()));
public virtual Task GetMergeConflictDocumentsAsync(string taskId) => Task.FromResult(new MergeConflictDocumentsDto(taskId, System.Array.Empty()));
public virtual Task WriteConflictResolutionAsync(string taskId, string path, string resolvedContent) => Task.CompletedTask;
public virtual Task ContinueConflictMergeAsync(string taskId) => Task.FromResult(new MergeResultDto("merged", System.Array.Empty(), null));
diff --git a/tests/ClaudeDo.Worker.Tests/Services/TaskMergeServiceTests.cs b/tests/ClaudeDo.Worker.Tests/Services/TaskMergeServiceTests.cs
index 4c63d46..a4ef405 100644
--- a/tests/ClaudeDo.Worker.Tests/Services/TaskMergeServiceTests.cs
+++ b/tests/ClaudeDo.Worker.Tests/Services/TaskMergeServiceTests.cs
@@ -668,41 +668,6 @@ public class TaskMergeServiceTests : IDisposable
// Cleanup
GitRepoFixture.RunGit(repo.RepoDir, "merge", "--abort");
}
- [Fact]
- public async Task GetConflictsAsync_AfterConflictMerge_ReturnsOursAndTheirs()
- {
- if (!GitRepoFixture.IsGitAvailable()) return;
-
- var db = NewDb();
- var repo = NewRepo();
- GitRepoFixture.RunGit(repo.RepoDir, "branch", "-m", "main");
- File.WriteAllText(Path.Combine(repo.RepoDir, "README.md"), "# main change\n");
- GitRepoFixture.RunGit(repo.RepoDir, "commit", "-am", "main change");
-
- var wtPath = Path.Combine(Path.GetTempPath(), $"wt_{Guid.NewGuid():N}");
- _wtCleanups.Add((repo.RepoDir, wtPath));
- GitRepoFixture.RunGit(repo.RepoDir, "worktree", "add", "-b", "claudedo/c1", wtPath, repo.BaseCommit);
- File.WriteAllText(Path.Combine(wtPath, "README.md"), "# branch change\n");
- GitRepoFixture.RunGit(wtPath, "commit", "-am", "branch change");
-
- var (_, task) = await SeedListAndTask(db, workingDir: repo.RepoDir, status: TaskStatus.WaitingForReview);
- await SeedWorktree(db, task.Id, wtPath, "claudedo/c1", repo.BaseCommit);
-
- var (svc, _) = BuildService(db);
- var start = await svc.MergeAsync(task.Id, "main", false, "msg", leaveConflictsInTree: true, CancellationToken.None);
- Assert.Equal(TaskMergeService.StatusConflict, start.Status);
-
- var conflicts = await svc.GetConflictsAsync(task.Id, CancellationToken.None);
-
- Assert.Equal(task.Id, conflicts.TaskId);
- var file = Assert.Single(conflicts.Files);
- Assert.Equal("README.md", file.Path);
- Assert.Contains("main change", file.Ours);
- Assert.Contains("branch change", file.Theirs);
- Assert.NotNull(file.Base);
-
- GitRepoFixture.RunGit(repo.RepoDir, "merge", "--abort");
- }
[Fact]
public async Task WriteResolutionAsync_ThenContinue_CompletesMerge()
diff --git a/tests/ClaudeDo.Worker.Tests/UiVm/TasksIslandViewModelPlanningTests.cs b/tests/ClaudeDo.Worker.Tests/UiVm/TasksIslandViewModelPlanningTests.cs
index 96bbbe3..0128ff4 100644
--- a/tests/ClaudeDo.Worker.Tests/UiVm/TasksIslandViewModelPlanningTests.cs
+++ b/tests/ClaudeDo.Worker.Tests/UiVm/TasksIslandViewModelPlanningTests.cs
@@ -52,7 +52,6 @@ sealed class FakeWorkerClient : IWorkerClient
public Task PreviewMergeAsync(string taskId, string targetBranch) => Task.FromResult(null);
public Task MergeTaskAsync(string taskId, string targetBranch, bool removeWorktree, string commitMessage) => Task.FromResult(new MergeResultDto("merged", System.Array.Empty(), null));
public Task StartConflictMergeAsync(string taskId, string targetBranch) => Task.FromResult(new MergeResultDto("conflict", System.Array.Empty(), null));
- public Task GetMergeConflictsAsync(string taskId) => Task.FromResult(new MergeConflictsDto(taskId, System.Array.Empty()));
public Task GetMergeConflictDocumentsAsync(string taskId) => Task.FromResult(new MergeConflictDocumentsDto(taskId, System.Array.Empty()));
public Task WriteConflictResolutionAsync(string taskId, string path, string resolvedContent) => Task.CompletedTask;
public Task ContinueConflictMergeAsync(string taskId) => Task.FromResult(new MergeResultDto("merged", System.Array.Empty(), null));