docs: add implementation plan for continue and reset buttons
This commit is contained in:
@@ -0,0 +1,803 @@
|
|||||||
|
# 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.
|
||||||
Reference in New Issue
Block a user