diff --git a/src/ClaudeDo.Worker/Findings/FindingsStoreLocator.cs b/src/ClaudeDo.Worker/Findings/FindingsStoreLocator.cs
new file mode 100644
index 00000000..2effbe0c
--- /dev/null
+++ b/src/ClaudeDo.Worker/Findings/FindingsStoreLocator.cs
@@ -0,0 +1,62 @@
+using ClaudeDo.Data.Models;
+using ClaudeDo.Data.Repositories;
+
+namespace ClaudeDo.Worker.Findings;
+
+///
+/// Maps an MCP call to the project whose findings store it targets. Always resolves to the list's
+/// WorkingDir — the main checkout — even when the caller runs inside a worktree, because concurrent
+/// writes into worktree copies would produce INDEX.md merge conflicts.
+///
+public sealed class FindingsStoreLocator : IFindingsStoreLocator
+{
+ private readonly TaskRepository _tasks;
+ private readonly ListRepository _lists;
+
+ public FindingsStoreLocator(TaskRepository tasks, ListRepository lists)
+ {
+ _tasks = tasks;
+ _lists = lists;
+ }
+
+ public async Task ResolveForTaskAsync(string taskId, CancellationToken ct)
+ {
+ var task = await _tasks.GetByIdAsync(taskId, ct)
+ ?? throw new InvalidOperationException($"Task {taskId} not found.");
+ var list = await _lists.GetByIdAsync(task.ListId, ct)
+ ?? throw new InvalidOperationException($"List {task.ListId} not found.");
+ return ToTarget(list);
+ }
+
+ public async Task ResolveForListAsync(string listIdOrName, CancellationToken ct)
+ {
+ var all = await _lists.GetAllAsync(ct);
+
+ if (!string.IsNullOrWhiteSpace(listIdOrName))
+ {
+ var match = all.FirstOrDefault(l => l.Id == listIdOrName)
+ ?? all.FirstOrDefault(l => string.Equals(l.Name, listIdOrName, StringComparison.OrdinalIgnoreCase))
+ ?? throw new InvalidOperationException(
+ $"No list matches '{listIdOrName}'. Known lists: {Names(all)}.");
+ return ToTarget(match);
+ }
+
+ var withDir = all.Where(l => !string.IsNullOrWhiteSpace(l.WorkingDir)).ToList();
+ return withDir.Count switch
+ {
+ 1 => ToTarget(withDir[0]),
+ 0 => throw new InvalidOperationException("No list has a working directory, so there is nowhere to save a finding."),
+ _ => throw new InvalidOperationException(
+ $"Several lists have a working directory — pass 'list' to pick one: {Names(withDir)}."),
+ };
+ }
+
+ private static string Names(IEnumerable lists) => string.Join(", ", lists.Select(l => l.Name));
+
+ private static FindingsTarget ToTarget(ListEntity list)
+ {
+ if (string.IsNullOrWhiteSpace(list.WorkingDir))
+ throw new InvalidOperationException($"List '{list.Name}' has no working directory, so it has no findings store.");
+ return new FindingsTarget(list.Id, list.Name, list.WorkingDir, list.FindingsTracked);
+ }
+}
diff --git a/src/ClaudeDo.Worker/Findings/Interfaces/IFindingsStoreLocator.cs b/src/ClaudeDo.Worker/Findings/Interfaces/IFindingsStoreLocator.cs
new file mode 100644
index 00000000..ff075921
--- /dev/null
+++ b/src/ClaudeDo.Worker/Findings/Interfaces/IFindingsStoreLocator.cs
@@ -0,0 +1,10 @@
+namespace ClaudeDo.Worker.Findings;
+
+/// The main checkout a finding belongs to, plus whether its store is committed with the repo.
+public sealed record FindingsTarget(string ListId, string ListName, string WorkingDir, bool Tracked);
+
+public interface IFindingsStoreLocator
+{
+ Task ResolveForTaskAsync(string taskId, CancellationToken ct);
+ Task ResolveForListAsync(string listIdOrName, CancellationToken ct);
+}
diff --git a/tests/ClaudeDo.Worker.Tests/Findings/FindingsStoreLocatorTests.cs b/tests/ClaudeDo.Worker.Tests/Findings/FindingsStoreLocatorTests.cs
new file mode 100644
index 00000000..221a38c9
--- /dev/null
+++ b/tests/ClaudeDo.Worker.Tests/Findings/FindingsStoreLocatorTests.cs
@@ -0,0 +1,83 @@
+using ClaudeDo.Data.Models;
+using ClaudeDo.Data.Repositories;
+using ClaudeDo.Worker.Findings;
+using ClaudeDo.Worker.Tests.Infrastructure;
+
+namespace ClaudeDo.Worker.Tests.Findings;
+
+public sealed class FindingsStoreLocatorTests : IDisposable
+{
+ private readonly DbFixture _fx = new();
+
+ public void Dispose() => _fx.Dispose();
+
+ [Fact]
+ public async Task ResolveForTask_UsesTheTasksListWorkingDir()
+ {
+ using var db = _fx.CreateContext();
+ db.Lists.Add(new ListEntity { Id = "l1", Name = "A", CreatedAt = DateTime.UtcNow, WorkingDir = @"C:\repo-a", FindingsTracked = true });
+ db.Tasks.Add(new TaskEntity { Id = "t1", ListId = "l1", Title = "x", CreatedAt = DateTime.UtcNow });
+ await db.SaveChangesAsync();
+ var locator = new FindingsStoreLocator(new TaskRepository(db), new ListRepository(db));
+
+ var target = await locator.ResolveForTaskAsync("t1", CancellationToken.None);
+
+ Assert.Equal(@"C:\repo-a", target.WorkingDir);
+ Assert.True(target.Tracked);
+ }
+
+ [Fact]
+ public async Task ResolveForList_MatchesByIdOrName()
+ {
+ using var db = _fx.CreateContext();
+ db.Lists.Add(new ListEntity { Id = "l1", Name = "Alpha", CreatedAt = DateTime.UtcNow, WorkingDir = @"C:\repo-a" });
+ db.Lists.Add(new ListEntity { Id = "l2", Name = "Beta", CreatedAt = DateTime.UtcNow, WorkingDir = @"C:\repo-b" });
+ await db.SaveChangesAsync();
+ var locator = new FindingsStoreLocator(new TaskRepository(db), new ListRepository(db));
+
+ Assert.Equal(@"C:\repo-b", (await locator.ResolveForListAsync("l2", CancellationToken.None)).WorkingDir);
+ Assert.Equal(@"C:\repo-a", (await locator.ResolveForListAsync("Alpha", CancellationToken.None)).WorkingDir);
+ }
+
+ [Fact]
+ public async Task ResolveForList_WithoutArgument_UsesTheOnlyListWithAWorkingDir()
+ {
+ using var db = _fx.CreateContext();
+ db.Lists.Add(new ListEntity { Id = "l1", Name = "Alpha", CreatedAt = DateTime.UtcNow, WorkingDir = @"C:\repo-a" });
+ db.Lists.Add(new ListEntity { Id = "l2", Name = "NoDir", CreatedAt = DateTime.UtcNow, WorkingDir = null });
+ await db.SaveChangesAsync();
+ var locator = new FindingsStoreLocator(new TaskRepository(db), new ListRepository(db));
+
+ var target = await locator.ResolveForListAsync("", CancellationToken.None);
+
+ Assert.Equal(@"C:\repo-a", target.WorkingDir);
+ }
+
+ [Fact]
+ public async Task ResolveForList_WithoutArgument_ThrowsAndNamesCandidatesWhenAmbiguous()
+ {
+ using var db = _fx.CreateContext();
+ db.Lists.Add(new ListEntity { Id = "l1", Name = "Alpha", CreatedAt = DateTime.UtcNow, WorkingDir = @"C:\repo-a" });
+ db.Lists.Add(new ListEntity { Id = "l2", Name = "Beta", CreatedAt = DateTime.UtcNow, WorkingDir = @"C:\repo-b" });
+ await db.SaveChangesAsync();
+ var locator = new FindingsStoreLocator(new TaskRepository(db), new ListRepository(db));
+
+ var ex = await Assert.ThrowsAsync(
+ () => locator.ResolveForListAsync("", CancellationToken.None));
+
+ Assert.Contains("Alpha", ex.Message);
+ Assert.Contains("Beta", ex.Message);
+ }
+
+ [Fact]
+ public async Task ResolveForList_ThrowsWhenTheListHasNoWorkingDir()
+ {
+ using var db = _fx.CreateContext();
+ db.Lists.Add(new ListEntity { Id = "l1", Name = "Alpha", CreatedAt = DateTime.UtcNow, WorkingDir = null });
+ await db.SaveChangesAsync();
+ var locator = new FindingsStoreLocator(new TaskRepository(db), new ListRepository(db));
+
+ await Assert.ThrowsAsync(
+ () => locator.ResolveForListAsync("Alpha", CancellationToken.None));
+ }
+}