# Continue & Reset Buttons for Failed Tasks — Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Add two buttons (Continue, Reset) to the details pane for `Failed` tasks so the user can either nudge the agent to continue or discard the worktree and return the task to `Manual`.

**Architecture:** Spec is at `docs/superpowers/specs/2026-04-21-continue-and-reset-failed-tasks-design.md`. Backend adds one git-discard helper, one task-repository method, a small orchestration service, and a new hub method `ResetTask`. `ContinueTask` is already wired in the hub. UI adds two commands in `DetailsIslandViewModel` and a button row in `DetailsIslandView`.

**Tech Stack:** .NET 8, EF Core (SQLite), ASP.NET Core SignalR, Avalonia 12, CommunityToolkit.Mvvm, xUnit 2.5 (integration tests with real SQLite + real git).

---

## File Structure

**New files:**
- `src/ClaudeDo.Worker/Services/TaskResetService.cs` — orchestrates the reset (load task, discard worktree, reset DB row, broadcast).
- `tests/ClaudeDo.Worker.Tests/Services/TaskResetServiceTests.cs` — integration tests for the orchestration.

**Modified files:**
- `src/ClaudeDo.Worker/Runner/WorktreeManager.cs` — add `DiscardAsync`.
- `src/ClaudeDo.Data/Repositories/TaskRepository.cs` — add `ResetToManualAsync`.
- `src/ClaudeDo.Worker/Hub/WorkerHub.cs` — add `ResetTask` endpoint; DI-inject the new service.
- `src/ClaudeDo.Worker/Program.cs` — register `TaskResetService` in DI.
- `src/ClaudeDo.Ui/Services/WorkerClient.cs` — add `ContinueTaskAsync` and `ResetTaskAsync` wrappers.
- `src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs` — add observable properties and commands.
- `src/ClaudeDo.Ui/Views/Islands/DetailsIslandView.axaml` — add button row bound to the new commands.
- `tests/ClaudeDo.Worker.Tests/Runner/WorktreeManagerTests.cs` — add `DiscardAsync` test.
- `tests/ClaudeDo.Worker.Tests/Repositories/TaskRepositoryTests.cs` — add `ResetToManualAsync` test.

---

## Task 1: `WorktreeManager.DiscardAsync` (TDD)

**Files:**
- Modify: `src/ClaudeDo.Worker/Runner/WorktreeManager.cs`
- Test: `tests/ClaudeDo.Worker.Tests/Runner/WorktreeManagerTests.cs`

`GitService` already exposes `WorktreeRemoveAsync(workingDir, path, force, ct)` and `BranchDeleteAsync(workingDir, branch, force, ct)` — verify via `git grep -n "public async Task WorktreeRemoveAsync\|public async Task BranchDeleteAsync" src/ClaudeDo.Data/Git`. If either is missing, stop and add the git wrapper first.

- [ ] **Step 1: Add the failing test**

Add at the bottom of `tests/ClaudeDo.Worker.Tests/Runner/WorktreeManagerTests.cs` (before the `Dispose` method):

```csharp
[Fact]
public async Task DiscardAsync_RemovesWorktreeAndBranch_AndSetsStateDiscarded()
{
    if (!GitAvailable) { Assert.True(true, "git not available -- skipping"); return; }

    var repo = CreateRepo();
    var (task, list) = MakeEntities(repo.RepoDir);
    var (mgr, db) = await CreateManagerAsync(task, list);

    var ctx = await mgr.CreateAsync(task, list, CancellationToken.None);
    var worktreePath = ctx.WorktreePath;

    WorktreeEntity wt;
    using (var readCtx = db.CreateContext())
        wt = (await new WorktreeRepository(readCtx).GetByTaskIdAsync(task.Id))!;

    await mgr.DiscardAsync(wt, list.WorkingDir!, CancellationToken.None);

    Assert.False(Directory.Exists(worktreePath), "worktree directory should be gone");

    using var readCtx2 = db.CreateContext();
    var row = await new WorktreeRepository(readCtx2).GetByTaskIdAsync(task.Id);
    Assert.NotNull(row);
    Assert.Equal(WorktreeState.Discarded, row!.State);

    // Branch should no longer exist on the main repo.
    var branchList = await new GitService().RunForOutputAsync(repo.RepoDir, new[] { "branch", "--list", ctx.BranchName }, CancellationToken.None);
    Assert.True(string.IsNullOrWhiteSpace(branchList),
        $"branch {ctx.BranchName} should be deleted, got: {branchList}");
}
```

Note on `RunForOutputAsync`: if `GitService` does not expose a generic run helper, replace the branch-check with a direct `System.Diagnostics.Process` invocation of `git branch --list <branch>` in the test. If such a helper exists with a different name, use it.

- [ ] **Step 2: Run test, verify it fails**

Run: `dotnet test tests/ClaudeDo.Worker.Tests --filter "FullyQualifiedName~DiscardAsync_RemovesWorktreeAndBranch" -v minimal`
Expected: FAIL — `DiscardAsync` does not exist.

- [ ] **Step 3: Implement `DiscardAsync`**

Add to `src/ClaudeDo.Worker/Runner/WorktreeManager.cs` after `CommitIfChangedAsync`:

```csharp
public async Task DiscardAsync(WorktreeEntity wt, string workingDir, CancellationToken ct)
{
    // Remove the git worktree first; --force drops uncommitted changes (user already confirmed).
    try
    {
        await _git.WorktreeRemoveAsync(workingDir, wt.Path, force: true, ct);
    }
    catch (Exception ex)
    {
        _logger.LogError(ex, "git worktree remove failed for {Path}", wt.Path);
        throw;
    }

    // Delete the branch. If worktree removal succeeded but branch delete fails,
    // we still record the worktree as Discarded — the folder is gone, and a dangling
    // branch is recoverable; leaving the DB out of sync is worse.
    try
    {
        await _git.BranchDeleteAsync(workingDir, wt.BranchName, force: true, ct);
    }
    catch (Exception ex)
    {
        _logger.LogWarning(ex, "git branch -D {Branch} failed after worktree removal; continuing", wt.BranchName);
    }

    using var context = _dbFactory.CreateDbContext();
    var wtRepo = new WorktreeRepository(context);
    await wtRepo.SetStateAsync(wt.TaskId, WorktreeState.Discarded, ct);

    _logger.LogInformation("Discarded worktree for task {TaskId} (branch {Branch})", wt.TaskId, wt.BranchName);
}
```

- [ ] **Step 4: Run test, verify it passes**

Run: `dotnet test tests/ClaudeDo.Worker.Tests --filter "FullyQualifiedName~DiscardAsync_RemovesWorktreeAndBranch" -v minimal`
Expected: PASS.

- [ ] **Step 5: Commit**

```bash
git add src/ClaudeDo.Worker/Runner/WorktreeManager.cs tests/ClaudeDo.Worker.Tests/Runner/WorktreeManagerTests.cs
git commit -m "feat(worker): add WorktreeManager.DiscardAsync for task reset"
```

---

## Task 2: `TaskRepository.ResetToManualAsync` (TDD)

**Files:**
- Modify: `src/ClaudeDo.Data/Repositories/TaskRepository.cs`
- Test: `tests/ClaudeDo.Worker.Tests/Repositories/TaskRepositoryTests.cs`

- [ ] **Step 1: Add the failing test**

Add to `tests/ClaudeDo.Worker.Tests/Repositories/TaskRepositoryTests.cs` (follow the existing test style in that file — reuse any helpers it already has for creating a list + task):

```csharp
[Fact]
public async Task ResetToManualAsync_ClearsResultFields_AndSetsStatusManual()
{
    using var db = new DbFixture();
    using var ctx = db.CreateContext();
    var listRepo = new ListRepository(ctx);
    var taskRepo = new TaskRepository(ctx);

    var list = new ListEntity { Id = Guid.NewGuid().ToString(), Name = "L", CreatedAt = DateTime.UtcNow };
    await listRepo.AddAsync(list);

    var task = new TaskEntity
    {
        Id = Guid.NewGuid().ToString(),
        ListId = list.Id,
        Title = "T",
        CreatedAt = DateTime.UtcNow,
        Status = TaskStatus.Failed,
        StartedAt = DateTime.UtcNow.AddMinutes(-5),
        FinishedAt = DateTime.UtcNow,
        Result = "boom",
    };
    await taskRepo.AddAsync(task);

    await taskRepo.ResetToManualAsync(task.Id);

    using var readCtx = db.CreateContext();
    var after = await new TaskRepository(readCtx).GetByIdAsync(task.Id);
    Assert.NotNull(after);
    Assert.Equal(TaskStatus.Manual, after!.Status);
    Assert.Null(after.StartedAt);
    Assert.Null(after.FinishedAt);
    Assert.Null(after.Result);
}
```

- [ ] **Step 2: Run test, verify it fails**

Run: `dotnet test tests/ClaudeDo.Worker.Tests --filter "FullyQualifiedName~ResetToManualAsync_ClearsResultFields" -v minimal`
Expected: FAIL — `ResetToManualAsync` does not exist.

- [ ] **Step 3: Implement `ResetToManualAsync`**

Add to `src/ClaudeDo.Data/Repositories/TaskRepository.cs` inside the `#region Status transitions` block, after `FlipAllRunningToFailedAsync`:

```csharp
public async Task ResetToManualAsync(string taskId, CancellationToken ct = default)
{
    await _context.Tasks
        .Where(t => t.Id == taskId)
        .ExecuteUpdateAsync(s => s
            .SetProperty(t => t.Status, TaskStatus.Manual)
            .SetProperty(t => t.StartedAt, (DateTime?)null)
            .SetProperty(t => t.FinishedAt, (DateTime?)null)
            .SetProperty(t => t.Result, (string?)null), ct);
}
```

- [ ] **Step 4: Run test, verify it passes**

Run: `dotnet test tests/ClaudeDo.Worker.Tests --filter "FullyQualifiedName~ResetToManualAsync_ClearsResultFields" -v minimal`
Expected: PASS.

- [ ] **Step 5: Commit**

```bash
git add src/ClaudeDo.Data/Repositories/TaskRepository.cs tests/ClaudeDo.Worker.Tests/Repositories/TaskRepositoryTests.cs
git commit -m "feat(data): add TaskRepository.ResetToManualAsync"
```

---

## Task 3: `TaskResetService` (TDD)

**Files:**
- Create: `src/ClaudeDo.Worker/Services/TaskResetService.cs`
- Create: `tests/ClaudeDo.Worker.Tests/Services/TaskResetServiceTests.cs`

This service orchestrates Task 1 + Task 2, plus the "reject if Running" safety check and the SignalR broadcast.

- [ ] **Step 1: Add the failing test — happy path**

Create `tests/ClaudeDo.Worker.Tests/Services/TaskResetServiceTests.cs`:

```csharp
using ClaudeDo.Data;
using ClaudeDo.Data.Git;
using ClaudeDo.Data.Models;
using ClaudeDo.Data.Repositories;
using ClaudeDo.Worker.Config;
using ClaudeDo.Worker.Hub;
using ClaudeDo.Worker.Runner;
using ClaudeDo.Worker.Services;
using ClaudeDo.Worker.Tests.Infrastructure;
using Microsoft.Extensions.Logging.Abstractions;
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;

namespace ClaudeDo.Worker.Tests.Services;

public class TaskResetServiceTests : IDisposable
{
    private readonly List<GitRepoFixture> _fixtures = new();
    private readonly List<DbFixture> _dbFixtures = new();
    private static bool GitAvailable => GitRepoFixture.IsGitAvailable();

    [Fact]
    public async Task ResetAsync_FailedTaskWithWorktree_ClearsEverything_AndPreservesRunHistory()
    {
        if (!GitAvailable) { Assert.True(true, "git not available -- skipping"); return; }

        var repo = new GitRepoFixture(); _fixtures.Add(repo);
        var db = new DbFixture(); _dbFixtures.Add(db);

        var list = new ListEntity { Id = Guid.NewGuid().ToString(), Name = "L", WorkingDir = repo.RepoDir, CreatedAt = DateTime.UtcNow };
        var task = new TaskEntity { Id = Guid.NewGuid().ToString(), ListId = list.Id, Title = "T", CreatedAt = DateTime.UtcNow };

        using (var seed = db.CreateContext())
        {
            await new ListRepository(seed).AddAsync(list);
            await new TaskRepository(seed).AddAsync(task);
        }

        var wtMgr = new WorktreeManager(new GitService(), db.CreateFactory(), new WorkerConfig(), NullLogger<WorktreeManager>.Instance);
        var wtCtx = await wtMgr.CreateAsync(task, list, CancellationToken.None);

        // Seed a Failed task with a run row (we'll assert it's preserved).
        using (var ctx = db.CreateContext())
        {
            await new TaskRepository(ctx).MarkFailedAsync(task.Id, DateTime.UtcNow, "it broke");
            await new TaskRunRepository(ctx).AddAsync(new TaskRunEntity
            {
                Id = Guid.NewGuid().ToString(),
                TaskId = task.Id,
                RunNumber = 1,
                IsRetry = false,
                Prompt = "p",
                SessionId = "s1",
                FinishedAt = DateTime.UtcNow,
            });
        }

        var broadcaster = new FakeHubBroadcaster();
        var svc = new TaskResetService(db.CreateFactory(), wtMgr, broadcaster, NullLogger<TaskResetService>.Instance);

        await svc.ResetAsync(task.Id, CancellationToken.None);

        using var readCtx = db.CreateContext();
        var after = await new TaskRepository(readCtx).GetByIdAsync(task.Id);
        Assert.Equal(TaskStatus.Manual, after!.Status);
        Assert.Null(after.Result);
        Assert.Null(after.StartedAt);
        Assert.Null(after.FinishedAt);

        var wtAfter = await new WorktreeRepository(readCtx).GetByTaskIdAsync(task.Id);
        Assert.Equal(WorktreeState.Discarded, wtAfter!.State);
        Assert.False(Directory.Exists(wtCtx.WorktreePath));

        var runs = await new TaskRunRepository(readCtx).GetByTaskIdAsync(task.Id);
        Assert.Single(runs);

        Assert.Contains(task.Id, broadcaster.TaskUpdatedIds);
        Assert.Contains(task.Id, broadcaster.WorktreeUpdatedIds);
    }

    [Fact]
    public async Task ResetAsync_RunningTask_Throws_AndDoesNotMutate()
    {
        var db = new DbFixture(); _dbFixtures.Add(db);
        var list = new ListEntity { Id = Guid.NewGuid().ToString(), Name = "L", CreatedAt = DateTime.UtcNow };
        var task = new TaskEntity { Id = Guid.NewGuid().ToString(), ListId = list.Id, Title = "T", CreatedAt = DateTime.UtcNow, Status = TaskStatus.Running, StartedAt = DateTime.UtcNow };

        using (var seed = db.CreateContext())
        {
            await new ListRepository(seed).AddAsync(list);
            await new TaskRepository(seed).AddAsync(task);
        }

        var wtMgr = new WorktreeManager(new GitService(), db.CreateFactory(), new WorkerConfig(), NullLogger<WorktreeManager>.Instance);
        var svc = new TaskResetService(db.CreateFactory(), wtMgr, new FakeHubBroadcaster(), NullLogger<TaskResetService>.Instance);

        await Assert.ThrowsAsync<InvalidOperationException>(() => svc.ResetAsync(task.Id, CancellationToken.None));

        using var readCtx = db.CreateContext();
        var after = await new TaskRepository(readCtx).GetByIdAsync(task.Id);
        Assert.Equal(TaskStatus.Running, after!.Status);
    }

    public void Dispose()
    {
        foreach (var f in _fixtures) f.Dispose();
        foreach (var d in _dbFixtures) d.Dispose();
    }

    private sealed class FakeHubBroadcaster : HubBroadcaster
    {
        public List<string> TaskUpdatedIds { get; } = new();
        public List<string> WorktreeUpdatedIds { get; } = new();
        public FakeHubBroadcaster() : base(new FakeHubContext()) { }
        public new Task TaskUpdated(string taskId) { TaskUpdatedIds.Add(taskId); return Task.CompletedTask; }
        public new Task WorktreeUpdated(string taskId) { WorktreeUpdatedIds.Add(taskId); return Task.CompletedTask; }
    }
}
```

Check existing fakes: the test file assumes `FakeHubContext` exists under `ClaudeDo.Worker.Tests.Infrastructure` (the Worker.Tests CLAUDE.md lists `FakeHubContext`, `FakeHubClients`, `FakeClientProxy`). If `HubBroadcaster` methods are not virtual, the `new` keyword above will not intercept calls — instead, use the real `HubBroadcaster` with `FakeHubContext` and inspect the fake's recorded calls. Adjust the test implementation to use whichever approach matches the existing test conventions (see `QueueServiceTests` for precedent).

- [ ] **Step 2: Run tests, verify they fail**

Run: `dotnet test tests/ClaudeDo.Worker.Tests --filter "FullyQualifiedName~TaskResetServiceTests" -v minimal`
Expected: FAIL — `TaskResetService` does not exist.

- [ ] **Step 3: Implement `TaskResetService`**

Create `src/ClaudeDo.Worker/Services/TaskResetService.cs`:

```csharp
using ClaudeDo.Data;
using ClaudeDo.Data.Repositories;
using ClaudeDo.Worker.Hub;
using ClaudeDo.Worker.Runner;
using Microsoft.EntityFrameworkCore;
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;

namespace ClaudeDo.Worker.Services;

public sealed class TaskResetService
{
    private readonly IDbContextFactory<ClaudeDoDbContext> _dbFactory;
    private readonly WorktreeManager _wtManager;
    private readonly HubBroadcaster _broadcaster;
    private readonly ILogger<TaskResetService> _logger;

    public TaskResetService(
        IDbContextFactory<ClaudeDoDbContext> dbFactory,
        WorktreeManager wtManager,
        HubBroadcaster broadcaster,
        ILogger<TaskResetService> logger)
    {
        _dbFactory = dbFactory;
        _wtManager = wtManager;
        _broadcaster = broadcaster;
        _logger = logger;
    }

    public async Task ResetAsync(string taskId, CancellationToken ct)
    {
        bool worktreeChanged = false;

        using (var ctx = _dbFactory.CreateDbContext())
        {
            var taskRepo = new TaskRepository(ctx);
            var task = await taskRepo.GetByIdAsync(taskId, ct)
                ?? throw new KeyNotFoundException($"Task '{taskId}' not found.");

            if (task.Status == TaskStatus.Running)
                throw new InvalidOperationException("Cannot reset a running task. Cancel it first.");

            var listRepo = new ListRepository(ctx);
            var list = await listRepo.GetByIdAsync(task.ListId, ct)
                ?? throw new InvalidOperationException("List not found.");

            var wtRepo = new WorktreeRepository(ctx);
            var wt = await wtRepo.GetByTaskIdAsync(taskId, ct);

            if (wt is not null && wt.State == Data.Models.WorktreeState.Active && list.WorkingDir is not null)
            {
                // DiscardAsync uses its own DbContext internally; we close this one first.
                ctx.Dispose();
                await _wtManager.DiscardAsync(wt, list.WorkingDir, ct);
                worktreeChanged = true;
            }
        }

        using (var ctx = _dbFactory.CreateDbContext())
        {
            var taskRepo = new TaskRepository(ctx);
            await taskRepo.ResetToManualAsync(taskId, ct);
        }

        await _broadcaster.TaskUpdated(taskId);
        if (worktreeChanged)
            await _broadcaster.WorktreeUpdated(taskId);

        _logger.LogInformation("Reset task {TaskId} to Manual (worktree discarded: {Discarded})", taskId, worktreeChanged);
    }
}
```

Note: the `ctx.Dispose()` inside a `using` block works because `Dispose` is idempotent. If you prefer, refactor to scope the first block with `{ }` + explicit `await using` and move the dispose before the call.

- [ ] **Step 4: Run tests, verify they pass**

Run: `dotnet test tests/ClaudeDo.Worker.Tests --filter "FullyQualifiedName~TaskResetServiceTests" -v minimal`
Expected: PASS.

- [ ] **Step 5: Commit**

```bash
git add src/ClaudeDo.Worker/Services/TaskResetService.cs tests/ClaudeDo.Worker.Tests/Services/TaskResetServiceTests.cs
git commit -m "feat(worker): add TaskResetService for discard + reset flow"
```

---

## Task 4: Wire `TaskResetService` into DI and add `WorkerHub.ResetTask`

**Files:**
- Modify: `src/ClaudeDo.Worker/Program.cs`
- Modify: `src/ClaudeDo.Worker/Hub/WorkerHub.cs`

- [ ] **Step 1: Register the service in DI**

Open `src/ClaudeDo.Worker/Program.cs`. Locate the block where `QueueService`, `WorktreeManager`, `HubBroadcaster`, `WorktreeMaintenanceService`, etc. are registered (look for `builder.Services.AddSingleton<QueueService>` or similar). Add next to them:

```csharp
builder.Services.AddSingleton<TaskResetService>();
```

Match the lifetime of sibling services (most are `AddSingleton`). If the sibling services use a different lifetime, match it.

- [ ] **Step 2: Inject it into `WorkerHub`**

Modify `src/ClaudeDo.Worker/Hub/WorkerHub.cs`:

In the field block (near `_wtMaintenance`):

```csharp
private readonly TaskResetService _resetService;
```

In the constructor signature, append `TaskResetService resetService` and assign it. The full updated constructor:

```csharp
public WorkerHub(
    QueueService queue,
    AgentFileService agentService,
    HubBroadcaster broadcaster,
    IDbContextFactory<ClaudeDoDbContext> dbFactory,
    WorktreeMaintenanceService wtMaintenance,
    TaskResetService resetService)
{
    _queue = queue;
    _agentService = agentService;
    _broadcaster = broadcaster;
    _dbFactory = dbFactory;
    _wtMaintenance = wtMaintenance;
    _resetService = resetService;
}
```

- [ ] **Step 3: Add the `ResetTask` hub method**

Add inside `WorkerHub` (place it near `ContinueTask` for symmetry):

```csharp
public async Task ResetTask(string taskId)
{
    try
    {
        await _resetService.ResetAsync(taskId, CancellationToken.None);
    }
    catch (InvalidOperationException ex)
    {
        throw new HubException(ex.Message);
    }
    catch (KeyNotFoundException)
    {
        throw new HubException("task not found");
    }
}
```

- [ ] **Step 4: Build the worker to verify wiring**

Run: `dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj`
Expected: SUCCESS (no compile errors). Note: `dotnet build ClaudeDo.slnx` requires .NET 9 — build individual csproj files instead.

- [ ] **Step 5: Run the full worker test suite**

Run: `dotnet test tests/ClaudeDo.Worker.Tests -v minimal`
Expected: PASS (all existing tests plus the new ones from Tasks 1-3).

- [ ] **Step 6: Commit**

```bash
git add src/ClaudeDo.Worker/Program.cs src/ClaudeDo.Worker/Hub/WorkerHub.cs
git commit -m "feat(worker): expose ResetTask hub method"
```

---

## Task 5: Add `ContinueTaskAsync` and `ResetTaskAsync` to `WorkerClient`

**Files:**
- Modify: `src/ClaudeDo.Ui/Services/WorkerClient.cs`

- [ ] **Step 1: Add both methods**

Open `src/ClaudeDo.Ui/Services/WorkerClient.cs`. Next to `RunNowAsync` (around line 166):

```csharp
public async Task ContinueTaskAsync(string taskId, string followUpPrompt)
{
    await _hub.InvokeAsync("ContinueTask", taskId, followUpPrompt);
}

public async Task ResetTaskAsync(string taskId)
{
    await _hub.InvokeAsync("ResetTask", taskId);
}
```

If the existing `RunNowAsync` fires a local event first (e.g. `RunNowRequestedEvent?.Invoke(taskId)`), do **not** mirror that — Continue/Reset don't need UI-local optimistic state; we rely on `TaskUpdated` broadcasts.

- [ ] **Step 2: Build the UI project**

Run: `dotnet build src/ClaudeDo.Ui/ClaudeDo.Ui.csproj`
Expected: SUCCESS.

- [ ] **Step 3: Commit**

```bash
git add src/ClaudeDo.Ui/Services/WorkerClient.cs
git commit -m "feat(ui): add ContinueTaskAsync and ResetTaskAsync to WorkerClient"
```

---

## Task 6: Add commands and state to `DetailsIslandViewModel`

**Files:**
- Modify: `src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs`

Background you need before editing:
- The VM already has a `Task` property (`TaskRowViewModel?`) that represents the selected task.
- Status is tracked via `AgentStatusLabel` and exposed as `IsRunning`/`IsDone`/`IsFailed`.
- `TaskRowViewModel` may not currently hold the latest `SessionId`. You need a way to read the latest run's `SessionId` for the selected task — query `TaskRunRepository` during the existing task-load flow. If the VM already loads task runs (search for `TaskRunRepository` usage in `DetailsIslandViewModel`), piggyback on that; otherwise add a DB query inside the task-load method.

- [ ] **Step 1: Add observable properties for button visibility/enablement**

In the observable-property block (after `_promptInput`, around line 29), add:

```csharp
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(ContinueCommand))]
[NotifyCanExecuteChangedFor(nameof(ResetCommand))]
private bool _showFailedActions;

[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(ContinueCommand))]
private string? _latestRunSessionId;
```

Also hook `AgentStatusLabel` changes to refresh `ShowFailedActions`. Update the existing `OnAgentStatusLabelChanged` partial method:

```csharp
partial void OnAgentStatusLabelChanged(string value)
{
    OnPropertyChanged(nameof(IsRunning));
    OnPropertyChanged(nameof(IsDone));
    OnPropertyChanged(nameof(IsFailed));
    ShowFailedActions = value == "Failed";
}
```

- [ ] **Step 2: Populate `LatestRunSessionId` during task load**

Find the method in `DetailsIslandViewModel` that loads details for the selected task (likely named `LoadAsync`, `OnTaskChanged`, or similar — search for where `Turns`, `Tokens`, or `AgentStatusLabel` are assigned). Inside that method, after loading the task entity:

```csharp
using var runCtx = _dbFactory.CreateDbContext();
var runRepo = new TaskRunRepository(runCtx);
var latestRun = await runRepo.GetLatestByTaskIdAsync(Task.Id);
LatestRunSessionId = latestRun?.SessionId;
```

Verify the method name `GetLatestByTaskIdAsync` exists on `TaskRunRepository` (it is used in `TaskRunner.ContinueAsync`). If the name differs, use whatever is exposed. Make sure this runs inside the same cancellation-safe block as the other loads — copy the existing pattern verbatim.

Also ensure `LatestRunSessionId` is reset to `null` when the selected task clears. If the VM has an `OnTaskChanged` partial method that clears other fields, add `LatestRunSessionId = null;` there too.

- [ ] **Step 3: Add the two commands**

Add at the end of the class (next to `RunNowAsync` / `CanRunNow`):

```csharp
[RelayCommand(CanExecute = nameof(CanContinue))]
private async System.Threading.Tasks.Task ContinueAsync()
{
    if (Task == null) return;
    await _worker.ContinueTaskAsync(Task.Id, "Continue working on this task.");
}

private bool CanContinue() =>
    Task != null
    && _worker.IsConnected
    && ShowFailedActions
    && !string.IsNullOrEmpty(LatestRunSessionId);

[RelayCommand(CanExecute = nameof(CanReset))]
private async System.Threading.Tasks.Task ResetAsync()
{
    if (Task == null) return;
    if (ConfirmAsync == null) return;

    var confirmed = await ConfirmAsync(
        $"Discard worktree and reset task?\nThis deletes branch claudedo/{Task.Id.Replace("-", "")} and all uncommitted changes.");
    if (!confirmed) return;

    await _worker.ResetTaskAsync(Task.Id);
}

private bool CanReset() =>
    Task != null
    && _worker.IsConnected
    && ShowFailedActions;
```

Also update the worker-connection PropertyChanged handler (around line 112) to notify the new commands:

```csharp
_worker.PropertyChanged += (_, e) =>
{
    if (e.PropertyName == nameof(WorkerClient.IsConnected))
    {
        RunNowCommand.NotifyCanExecuteChanged();
        ContinueCommand.NotifyCanExecuteChanged();
        ResetCommand.NotifyCanExecuteChanged();
    }
};
```

- [ ] **Step 4: Build the UI project**

Run: `dotnet build src/ClaudeDo.Ui/ClaudeDo.Ui.csproj`
Expected: SUCCESS.

- [ ] **Step 5: Commit**

```bash
git add src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs
git commit -m "feat(ui): add Continue and Reset commands to DetailsIslandViewModel"
```

---

## Task 7: Add the button row to `DetailsIslandView.axaml`

**Files:**
- Modify: `src/ClaudeDo.Ui/Views/Islands/DetailsIslandView.axaml`

- [ ] **Step 1: Inspect the existing layout**

Read `src/ClaudeDo.Ui/Views/Islands/DetailsIslandView.axaml` end-to-end so you understand the grid layout. The `AgentStripView` sits at `Grid.Row="0"`. Decide whether to add a new grid row below it or to extend the agent strip itself. Simplest: add the button row to `AgentStripView.axaml`, since that control already contains `RunNowCommand` / `StopCommand` buttons and is bound to the same VM.

- [ ] **Step 2: Add the buttons to `AgentStripView.axaml`**

Open `src/ClaudeDo.Ui/Views/Islands/AgentStripView.axaml`. Locate the existing `RunNowCommand` button (around line 49). After it, add:

```xml
<Button
    Content="Continue"
    Command="{Binding ContinueCommand}"
    IsVisible="{Binding ShowFailedActions}"
    ToolTip.Tip="Resume the failed Claude session with 'Continue working on this task.'"
    Margin="4,0,0,0"/>

<Button
    Content="Reset"
    Command="{Binding ResetCommand}"
    IsVisible="{Binding ShowFailedActions}"
    ToolTip.Tip="Discard the worktree and return the task to Manual"
    Margin="4,0,0,0"/>
```

Match the style (classes, padding, height) of the surrounding `RunNow` / `Stop` buttons — copy their `Classes`, `Padding`, and `Height` attributes verbatim so the row stays visually consistent.

For the Continue button's disabled-with-tooltip affordance when there's no session_id: the `CanExecute` binding already disables the button; Avalonia shows tooltips on disabled controls when `ToolTip.ShowOnDisabled="True"` — set that on the Continue button and add a second tooltip hinting at the reason is unnecessary since the button will simply be greyed out. If you want an explicit "No session to resume" hint, add a `Classes.disabled` trigger or use a `MultiBinding`; skip this refinement unless it is trivial in the existing theme.

- [ ] **Step 3: Wire the confirmation dialog**

The VM's `ResetAsync` uses `ConfirmAsync` (a `Func<string, Task<bool>>` already declared on the VM at line 100). Search the codebase for where `ConfirmAsync` is assigned on the `DetailsIslandViewModel` instance — there is an existing assignment because `DeleteTaskCommand` already uses it. No new wiring needed; the same dialog will handle Reset confirmations.

- [ ] **Step 4: Launch the UI and smoke-test**

1. In one terminal: `dotnet run --project src/ClaudeDo.Worker/ClaudeDo.Worker.csproj`
2. In another terminal: `dotnet run --project src/ClaudeDo.App/ClaudeDo.App.csproj`
3. Create a task that will fail (e.g. a task pointing at a non-existent working dir, or type something into Claude's prompt that makes it error). Wait for status `Failed`.
4. Verify the Continue and Reset buttons appear in the details pane.
5. Click Reset → confirm → verify the task row flips to `Manual`, the worktree directory is gone from disk, and the branch is gone from `git branch --list | grep claudedo/` in the target repo.
6. Create another failing task. Click Continue → verify a new run starts (status flips to `Running`), resumes the same Claude session, and completes (Done or Failed again).
7. Verify on a task that has no session_id (e.g. cancel before Claude emits anything), the Continue button is disabled.

- [ ] **Step 5: Commit**

```bash
git add src/ClaudeDo.Ui/Views/Islands/AgentStripView.axaml
git commit -m "feat(ui): add Continue and Reset buttons to agent strip"
```

---

## Task 8: Update project docs

**Files:**
- Modify: `src/ClaudeDo.Worker/CLAUDE.md`
- Modify: `src/ClaudeDo.Ui/CLAUDE.md`

- [ ] **Step 1: Update Worker CLAUDE.md**

Under the `SignalR Hub` section, extend the `WorkerHub methods` line:

```
**WorkerHub** methods: `Ping()`, `GetActive()`, `RunNow(taskId)`, `CancelTask(taskId)`, `WakeQueue()`, `ContinueTask(taskId, prompt)`, `ResetTask(taskId)`, `GetAgents()`, `RefreshAgents()`
```

Under `Key Components`, add one line:

```
- **TaskResetService** — discards a failed task's worktree and resets the task row to Manual; preserves run history.
```

- [ ] **Step 2: Update UI CLAUDE.md**

Extend the `WorkerClient` description to mention the two new methods:

```
- **WorkerClient** — ... Methods: StartAsync, RunNowAsync, CancelTaskAsync, ContinueTaskAsync, ResetTaskAsync, WakeQueueAsync. Events: ...
```

- [ ] **Step 3: Commit**

```bash
git add src/ClaudeDo.Worker/CLAUDE.md src/ClaudeDo.Ui/CLAUDE.md
git commit -m "docs: note ResetTask hub method and TaskResetService"
```

---

## Self-Review

**Spec coverage:**
- Continue button (canned prompt, one-click, disabled without session) → Task 6 (`ContinueCommand`, `CanContinue`) + Task 7 (button).
- Reset button (always enabled on Failed, confirm dialog) → Task 6 (`ResetCommand`, `CanReset`) + Task 7 (button + confirm).
- Buttons only on Failed → `ShowFailedActions` drives `IsVisible` (Task 6, Task 7).
- Hub `ResetTask` → Task 4.
- `WorktreeManager.DiscardAsync` → Task 1.
- `TaskRepository.ResetToManualAsync` → Task 2.
- Reject reset on Running → Task 3.
- Worktree-remove failure leaves task Failed → Task 3 (`DiscardAsync` throws before `ResetToManualAsync` is called).
- Run history preserved → Task 2 and Task 3 assertions.
- Tests — WorktreeManager.DiscardAsync, TaskRepository.ResetToManualAsync, TaskResetService full flow, reject running → Tasks 1, 2, 3.
- Test for "ResetTask rejects running" → Task 3 test 2.
- Test for "worktree remove failure leaves task Failed" → covered implicitly by the code structure (Task 3 does not call `ResetToManualAsync` if `DiscardAsync` throws). If you want an explicit test, add one in Task 3 by injecting a failure; marking optional as the control flow is straightforward.

**Placeholder scan:** no TBDs; every code step has code; commands include expected output.

**Type consistency:** `DiscardAsync(WorktreeEntity wt, string workingDir, ct)` used consistently (Tasks 1 and 3). `ResetToManualAsync(taskId, ct)` used consistently (Tasks 2 and 3). `ContinueTaskAsync`/`ResetTaskAsync` on `WorkerClient` match the hub method names. `ShowFailedActions`, `LatestRunSessionId`, `CanContinue`, `CanReset` referenced consistently across Task 6 and Task 7.
