docs(refine): add Refine Task implementation plan
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
801
docs/superpowers/plans/2026-06-04-refine-task.md
Normal file
801
docs/superpowers/plans/2026-06-04-refine-task.md
Normal file
@@ -0,0 +1,801 @@
|
|||||||
|
# Refine Task Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. Subagents use the `sonnet` model and stage files explicitly by path (never `git add -A`).
|
||||||
|
|
||||||
|
**Goal:** Add a one-click "Refine Task" button to each Idle task card that spawns a headless Claude session which rewrites the task's description and adds subtasks (steps), then updates the task live in the UI.
|
||||||
|
|
||||||
|
**Architecture:** A new headless `RefineRunner` (modeled on `PrimeRunner`) runs `claude -p` read-only in the list's working dir, using the globally-registered `claudedo` MCP. Claude calls `update_task` (existing) and a new `add_subtask` tool. The task stays `Idle`; refine only mutates Title/Description/subtasks. UI shows a busy state via new `RefineStarted`/`RefineFinished` SignalR events; content updates arrive via the existing `TaskUpdated` events.
|
||||||
|
|
||||||
|
**Tech Stack:** .NET 8, ASP.NET Core + SignalR, EF Core (SQLite), Avalonia 12 (CommunityToolkit.Mvvm), ModelContextProtocol server tools, xUnit.
|
||||||
|
|
||||||
|
**Spec:** `docs/superpowers/specs/2026-06-04-refine-task-design.md`
|
||||||
|
|
||||||
|
**Build/test reminders:** Build individual csproj with `-c Release` (a running Worker locks Debug). `dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj -c Release`, `dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release`, `dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release`. Keep `locales/en.json` and `locales/de.json` keys in parity.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File structure
|
||||||
|
|
||||||
|
**Create:**
|
||||||
|
- `src/ClaudeDo.Worker/Refine/RefineRunner.cs` — headless refine run orchestrator
|
||||||
|
- `src/ClaudeDo.Worker/Refine/RefinePrompt.cs` — prompt + CLI args + log path helper
|
||||||
|
- `src/ClaudeDo.Worker/Refine/Interfaces/IRefineRunner.cs` — interface + `RefineRunOutcome`
|
||||||
|
- `src/ClaudeDo.Worker/Refine/Interfaces/IRefineBroadcaster.cs` — `RefineStartedAsync`/`RefineFinishedAsync`
|
||||||
|
|
||||||
|
**Modify:**
|
||||||
|
- `src/ClaudeDo.Data/PromptFiles.cs` — add `Refine` to `PromptKind`, path, default
|
||||||
|
- `src/ClaudeDo.Worker/External/ExternalMcpService.cs` — add `add_subtask` tool
|
||||||
|
- `src/ClaudeDo.Worker/Hub/HubBroadcaster.cs` — implement `RefineStarted`/`RefineFinished` + `IRefineBroadcaster`
|
||||||
|
- `src/ClaudeDo.Worker/Hub/WorkerHub.cs` — add `RefineTask(string taskId)` method
|
||||||
|
- `src/ClaudeDo.Worker/Program.cs` — register `IRefineRunner`/`IRefineBroadcaster`
|
||||||
|
- `src/ClaudeDo.Ui/Services/Interfaces/IWorkerClient.cs` — `RefineTaskAsync` + `RefineStartedEvent`/`RefineFinishedEvent`
|
||||||
|
- `src/ClaudeDo.Ui/Services/WorkerClient.cs` — implement call + subscribe events
|
||||||
|
- `src/ClaudeDo.Ui/ViewModels/Islands/TaskRowViewModel.cs` — `IsRefining` + `CanRefine`
|
||||||
|
- `src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs` — `RefineTaskCommand` + event wiring
|
||||||
|
- `src/ClaudeDo.Ui/Design/IslandStyles.axaml` — `Icon.Refine` geometry
|
||||||
|
- `src/ClaudeDo.Ui/Views/Islands/TaskRowView.axaml` — refine button
|
||||||
|
- `locales/en.json`, `locales/de.json` — tooltip key
|
||||||
|
- Test fakes implementing `IWorkerClient` in `tests/ClaudeDo.Ui.Tests` (and any other project that hand-rolls it)
|
||||||
|
|
||||||
|
**Test:**
|
||||||
|
- `tests/ClaudeDo.Worker.Tests/External/AddSubtaskToolTests.cs`
|
||||||
|
- `tests/ClaudeDo.Worker.Tests/Refine/RefinePromptTests.cs`
|
||||||
|
- `tests/ClaudeDo.Worker.Tests/Refine/RefineRunnerTests.cs`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 1: `add_subtask` MCP tool
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/ClaudeDo.Worker/External/ExternalMcpService.cs`
|
||||||
|
- Test: `tests/ClaudeDo.Worker.Tests/External/AddSubtaskToolTests.cs`
|
||||||
|
|
||||||
|
The `ExternalMcpService` already injects `IDbContextFactory<ClaudeDoDbContext> _dbFactory`, `TaskRepository _tasks`, and `HubBroadcaster _broadcaster`. Reuse them; new up a `SubtaskRepository` from a fresh context (matching the `SetMyDay`/`GetDailyPrepCandidates` pattern in the same file).
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the failing test**
|
||||||
|
|
||||||
|
Create `tests/ClaudeDo.Worker.Tests/External/AddSubtaskToolTests.cs`. Follow the existing External tool test setup in that test project (look at a sibling test, e.g. an `ExternalMcpService`/`UpdateTask` test, for the in-memory-real-SQLite fixture + broadcaster fake construction; reuse that exact fixture pattern).
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using ClaudeDo.Data.Models;
|
||||||
|
using ClaudeDo.Data.Repositories;
|
||||||
|
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
|
||||||
|
|
||||||
|
public class AddSubtaskToolTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task AddSubtask_appends_row_with_next_order()
|
||||||
|
{
|
||||||
|
await using var f = new ExternalMcpServiceFixture(); // reuse the project's existing fixture helper
|
||||||
|
var list = await f.SeedListAsync();
|
||||||
|
var task = await f.SeedTaskAsync(list.Id, status: TaskStatus.Idle);
|
||||||
|
|
||||||
|
await f.Service.AddSubtask(task.Id, "First step", orderNum: null, CancellationToken.None);
|
||||||
|
await f.Service.AddSubtask(task.Id, "Second step", orderNum: null, CancellationToken.None);
|
||||||
|
|
||||||
|
await using var ctx = f.CreateContext();
|
||||||
|
var subs = await new SubtaskRepository(ctx).GetByTaskIdAsync(task.Id);
|
||||||
|
Assert.Equal(new[] { "First step", "Second step" }, subs.Select(s => s.Title));
|
||||||
|
Assert.Equal(new[] { 0, 1 }, subs.Select(s => s.OrderNum));
|
||||||
|
Assert.All(subs, s => Assert.False(s.Completed));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task AddSubtask_refuses_running_task()
|
||||||
|
{
|
||||||
|
await using var f = new ExternalMcpServiceFixture();
|
||||||
|
var list = await f.SeedListAsync();
|
||||||
|
var task = await f.SeedTaskAsync(list.Id, status: TaskStatus.Running);
|
||||||
|
|
||||||
|
await Assert.ThrowsAsync<InvalidOperationException>(
|
||||||
|
() => f.Service.AddSubtask(task.Id, "x", null, CancellationToken.None));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> If the test project has no reusable `ExternalMcpServiceFixture`, mirror the construction already used by the nearest existing `ExternalMcpService` test (same ctor args, real SQLite via `IDbContextFactory`, a no-op/recording broadcaster). Do not invent a new pattern.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the test to verify it fails** (compile error / method missing)
|
||||||
|
|
||||||
|
Run: `dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --filter AddSubtaskToolTests`
|
||||||
|
Expected: FAIL — `AddSubtask` not defined.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Implement `add_subtask`**
|
||||||
|
|
||||||
|
Add to `ExternalMcpService` (near `UpdateTask`):
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[McpServerTool, Description(
|
||||||
|
"Append a subtask (step) to a task. orderNum defaults to the end. " +
|
||||||
|
"Refuses if the task is currently Running. Subtasks are surfaced to the agent at run time and shown in the task's Steps list.")]
|
||||||
|
public async Task<TaskDto> AddSubtask(
|
||||||
|
string taskId,
|
||||||
|
string title,
|
||||||
|
int? orderNum,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(title))
|
||||||
|
throw new InvalidOperationException("title is required.");
|
||||||
|
|
||||||
|
await using var ctx = await _dbFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
var tasks = new TaskRepository(ctx);
|
||||||
|
var subtasks = new SubtaskRepository(ctx);
|
||||||
|
|
||||||
|
var task = await tasks.GetByIdAsync(taskId, cancellationToken)
|
||||||
|
?? throw new InvalidOperationException($"Task {taskId} not found.");
|
||||||
|
if (task.Status == TaskStatus.Running)
|
||||||
|
throw new InvalidOperationException("Cannot add a subtask to a running task. Cancel it first.");
|
||||||
|
|
||||||
|
var existing = await subtasks.GetByTaskIdAsync(taskId, cancellationToken);
|
||||||
|
var order = orderNum ?? (existing.Count == 0 ? 0 : existing.Max(s => s.OrderNum) + 1);
|
||||||
|
|
||||||
|
await subtasks.AddAsync(new SubtaskEntity
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid().ToString(),
|
||||||
|
TaskId = taskId,
|
||||||
|
Title = title.Trim(),
|
||||||
|
Completed = false,
|
||||||
|
OrderNum = order,
|
||||||
|
CreatedAt = DateTime.UtcNow,
|
||||||
|
}, cancellationToken);
|
||||||
|
|
||||||
|
await _broadcaster.TaskUpdated(taskId);
|
||||||
|
return ToDto(task);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Add `using ClaudeDo.Data.Repositories;` if not present (it is). `SubtaskEntity` is in `ClaudeDo.Data.Models` (already imported).
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run the test to verify it passes**
|
||||||
|
|
||||||
|
Run: `dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --filter AddSubtaskToolTests`
|
||||||
|
Expected: PASS (2 tests).
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/ClaudeDo.Worker/External/ExternalMcpService.cs tests/ClaudeDo.Worker.Tests/External/AddSubtaskToolTests.cs
|
||||||
|
git commit -m "feat(mcp): add add_subtask tool to claudedo MCP"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 2: Refine prompt (`PromptKind.Refine`)
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/ClaudeDo.Data/PromptFiles.cs`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add the enum value**
|
||||||
|
|
||||||
|
Change the enum line in `PromptFiles.cs`:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public enum PromptKind { System, Planning, PlanningInitial, Retry, DailyPrep, WeeklyReport, ImprovementChild, Refine }
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Add the path mapping**
|
||||||
|
|
||||||
|
In `PathFor`, add before the `_ => throw`:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
PromptKind.Refine => Path.Combine(Root, "refine.md"),
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Add the default mapping**
|
||||||
|
|
||||||
|
In `DefaultFor`, add:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
PromptKind.Refine => RefineDefault,
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Add the default prompt constant**
|
||||||
|
|
||||||
|
Add near the other `private const string ...Default` blocks:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
private const string RefineDefault = """
|
||||||
|
You are refining ONE ClaudeDo task so it is ready to run autonomously later.
|
||||||
|
You are NOT executing the task — only improving its specification.
|
||||||
|
|
||||||
|
The task you are refining:
|
||||||
|
- id: {taskId}
|
||||||
|
- title: {title}
|
||||||
|
- description: {description}
|
||||||
|
- current subtasks (steps):
|
||||||
|
{subtasks}
|
||||||
|
|
||||||
|
What to do:
|
||||||
|
1. If a repository is available, read the relevant code (read-only) to ground your
|
||||||
|
understanding. Do NOT edit, create, or delete any files. Do NOT run commands.
|
||||||
|
2. Rewrite the description so it is clear, specific, and self-contained: what to change,
|
||||||
|
where, and what "done" looks like. Keep scope tight — do not invent adjacent work.
|
||||||
|
3. Call mcp__claudedo__update_task to save the improved title (only if it genuinely
|
||||||
|
helps) and description.
|
||||||
|
4. If the work is clearer as discrete steps, add them as subtasks with
|
||||||
|
mcp__claudedo__add_subtask (one call per step, in order). Only add steps that are
|
||||||
|
not already present in the current subtasks above.
|
||||||
|
|
||||||
|
Use ONLY these tools: mcp__claudedo__get_task, mcp__claudedo__update_task,
|
||||||
|
mcp__claudedo__add_subtask, and read-only Read/Grep/Glob. When you have updated the
|
||||||
|
task, stop.
|
||||||
|
""";
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: Build to verify it compiles**
|
||||||
|
|
||||||
|
Run: `dotnet build src/ClaudeDo.Data/ClaudeDo.Data.csproj -c Release`
|
||||||
|
Expected: Build succeeded.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/ClaudeDo.Data/PromptFiles.cs
|
||||||
|
git commit -m "feat(prompts): add Refine prompt kind and default"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 3: RefineRunner, interfaces, prompt/args helper
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `src/ClaudeDo.Worker/Refine/Interfaces/IRefineRunner.cs`
|
||||||
|
- Create: `src/ClaudeDo.Worker/Refine/Interfaces/IRefineBroadcaster.cs`
|
||||||
|
- Create: `src/ClaudeDo.Worker/Refine/RefinePrompt.cs`
|
||||||
|
- Create: `src/ClaudeDo.Worker/Refine/RefineRunner.cs`
|
||||||
|
- Test: `tests/ClaudeDo.Worker.Tests/Refine/RefinePromptTests.cs`
|
||||||
|
- Test: `tests/ClaudeDo.Worker.Tests/Refine/RefineRunnerTests.cs`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Create `IRefineRunner.cs`**
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
namespace ClaudeDo.Worker.Refine;
|
||||||
|
|
||||||
|
public interface IRefineRunner
|
||||||
|
{
|
||||||
|
Task<RefineRunOutcome> RefineAsync(string taskId, CancellationToken ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record RefineRunOutcome(bool Success, string Message);
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Create `IRefineBroadcaster.cs`**
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
namespace ClaudeDo.Worker.Refine;
|
||||||
|
|
||||||
|
public interface IRefineBroadcaster
|
||||||
|
{
|
||||||
|
Task RefineStartedAsync(string taskId);
|
||||||
|
Task RefineFinishedAsync(string taskId, bool success, string? error);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Create `RefinePrompt.cs`**
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using ClaudeDo.Data;
|
||||||
|
using ClaudeDo.Data.Models;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Worker.Refine;
|
||||||
|
|
||||||
|
public static class RefinePrompt
|
||||||
|
{
|
||||||
|
public const string GetTaskTool = "mcp__claudedo__get_task";
|
||||||
|
public const string UpdateTaskTool = "mcp__claudedo__update_task";
|
||||||
|
public const string AddSubtaskTool = "mcp__claudedo__add_subtask";
|
||||||
|
|
||||||
|
public static string LogPath(string taskId) =>
|
||||||
|
System.IO.Path.Combine(Paths.AppDataRoot(), "logs", $"refine-{Short(taskId)}.log");
|
||||||
|
|
||||||
|
// canReadRepo=false drops the read-only filesystem tools (text-only fallback).
|
||||||
|
public static string BuildArgs(int maxTurns, bool canReadRepo)
|
||||||
|
{
|
||||||
|
var tools = canReadRepo
|
||||||
|
? $"{GetTaskTool} {UpdateTaskTool} {AddSubtaskTool} Read Grep Glob"
|
||||||
|
: $"{GetTaskTool} {UpdateTaskTool} {AddSubtaskTool}";
|
||||||
|
return "-p --output-format stream-json --verbose --permission-mode acceptEdits " +
|
||||||
|
$"--max-turns {maxTurns} --allowedTools {tools}";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string BuildPrompt(TaskEntity task, IEnumerable<SubtaskEntity> subtasks)
|
||||||
|
{
|
||||||
|
var open = subtasks.Where(s => !s.Completed).Select(s => $"- {s.Title}").ToList();
|
||||||
|
var subText = open.Count == 0 ? "(none)" : string.Join("\n", open);
|
||||||
|
return PromptFiles.Render(PromptKind.Refine, new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
["taskId"] = task.Id,
|
||||||
|
["title"] = task.Title,
|
||||||
|
["description"] = string.IsNullOrWhiteSpace(task.Description) ? "(empty)" : task.Description!,
|
||||||
|
["subtasks"] = subText,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string Short(string id) => id.Length >= 8 ? id[..8] : id;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Write `RefinePromptTests.cs`**
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using ClaudeDo.Data.Models;
|
||||||
|
using ClaudeDo.Worker.Refine;
|
||||||
|
|
||||||
|
public class RefinePromptTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void BuildArgs_includes_read_tools_when_repo_available()
|
||||||
|
{
|
||||||
|
var args = RefinePrompt.BuildArgs(20, canReadRepo: true);
|
||||||
|
Assert.Contains("--permission-mode acceptEdits", args);
|
||||||
|
Assert.Contains("mcp__claudedo__add_subtask", args);
|
||||||
|
Assert.Contains(" Read Grep Glob", args);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void BuildArgs_drops_read_tools_in_text_only_mode()
|
||||||
|
{
|
||||||
|
var args = RefinePrompt.BuildArgs(20, canReadRepo: false);
|
||||||
|
Assert.DoesNotContain("Glob", args);
|
||||||
|
Assert.Contains("mcp__claudedo__update_task", args);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void BuildPrompt_seeds_task_fields_and_open_subtasks()
|
||||||
|
{
|
||||||
|
var task = new TaskEntity { Id = "abc12345", ListId = "l", Title = "T", Description = "D",
|
||||||
|
Status = TaskStatus.Idle, CreatedAt = DateTime.UtcNow };
|
||||||
|
var subs = new[]
|
||||||
|
{
|
||||||
|
new SubtaskEntity { Id="1", TaskId="abc12345", Title="open one", Completed=false, OrderNum=0, CreatedAt=DateTime.UtcNow },
|
||||||
|
new SubtaskEntity { Id="2", TaskId="abc12345", Title="done one", Completed=true, OrderNum=1, CreatedAt=DateTime.UtcNow },
|
||||||
|
};
|
||||||
|
var prompt = RefinePrompt.BuildPrompt(task, subs);
|
||||||
|
Assert.Contains("abc12345", prompt);
|
||||||
|
Assert.Contains("open one", prompt);
|
||||||
|
Assert.DoesNotContain("done one", prompt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Run: `dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --filter RefinePromptTests`
|
||||||
|
Expected: PASS (3 tests).
|
||||||
|
|
||||||
|
- [ ] **Step 5: Create `RefineRunner.cs`**
|
||||||
|
|
||||||
|
`IClaudeProcess.RunAsync(arguments, prompt, workingDirectory, onStdoutLine, ct)` returns a result with `.IsSuccess` and `.ExitCode` (same as used by `PrimeRunner`). Resolve the working dir from the task's list; fall back to a sandbox dir + text-only when missing/invalid. Per-task single-flight via a guarded `HashSet<string>`.
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using ClaudeDo.Data;
|
||||||
|
using ClaudeDo.Data.Repositories;
|
||||||
|
using ClaudeDo.Worker.Runner;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Worker.Refine;
|
||||||
|
|
||||||
|
public sealed class RefineRunner : IRefineRunner
|
||||||
|
{
|
||||||
|
private static readonly TimeSpan RunTimeout = TimeSpan.FromMinutes(5);
|
||||||
|
private const int MaxTurns = 25;
|
||||||
|
|
||||||
|
private readonly IClaudeProcess _claude;
|
||||||
|
private readonly IDbContextFactory<ClaudeDoDbContext> _dbFactory;
|
||||||
|
private readonly ILogger<RefineRunner> _logger;
|
||||||
|
private readonly IRefineBroadcaster _broadcaster;
|
||||||
|
|
||||||
|
private readonly object _lock = new();
|
||||||
|
private readonly HashSet<string> _inFlight = new();
|
||||||
|
|
||||||
|
public RefineRunner(
|
||||||
|
IClaudeProcess claude,
|
||||||
|
IDbContextFactory<ClaudeDoDbContext> dbFactory,
|
||||||
|
ILogger<RefineRunner> logger,
|
||||||
|
IRefineBroadcaster broadcaster)
|
||||||
|
{
|
||||||
|
_claude = claude;
|
||||||
|
_dbFactory = dbFactory;
|
||||||
|
_logger = logger;
|
||||||
|
_broadcaster = broadcaster;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<RefineRunOutcome> RefineAsync(string taskId, CancellationToken ct)
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
if (!_inFlight.Add(taskId))
|
||||||
|
return new RefineRunOutcome(false, "Already refining this task");
|
||||||
|
}
|
||||||
|
|
||||||
|
var success = false;
|
||||||
|
string? error = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
ClaudeDo.Data.Models.TaskEntity task;
|
||||||
|
List<ClaudeDo.Data.Models.SubtaskEntity> subs;
|
||||||
|
string? workingDir;
|
||||||
|
await using (var dbCtx = await _dbFactory.CreateDbContextAsync(ct))
|
||||||
|
{
|
||||||
|
var tasks = new TaskRepository(dbCtx);
|
||||||
|
task = await tasks.GetByIdAsync(taskId, ct)
|
||||||
|
?? throw new InvalidOperationException($"Task {taskId} not found.");
|
||||||
|
if (task.Status != TaskStatus.Idle)
|
||||||
|
return new RefineRunOutcome(false, $"Task must be Idle to refine (is {task.Status}).");
|
||||||
|
subs = await new SubtaskRepository(dbCtx).GetByTaskIdAsync(taskId, ct);
|
||||||
|
var list = await new ListRepository(dbCtx).GetByIdAsync(task.ListId, ct);
|
||||||
|
workingDir = list?.WorkingDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
var canReadRepo = !string.IsNullOrWhiteSpace(workingDir) && Directory.Exists(workingDir);
|
||||||
|
var cwd = canReadRepo ? workingDir! : Paths.AppDataRoot();
|
||||||
|
Directory.CreateDirectory(cwd);
|
||||||
|
|
||||||
|
var logPath = RefinePrompt.LogPath(taskId);
|
||||||
|
try { if (File.Exists(logPath)) File.Delete(logPath); } catch { }
|
||||||
|
await using var logWriter = new LogWriter(logPath);
|
||||||
|
|
||||||
|
await _broadcaster.RefineStartedAsync(taskId);
|
||||||
|
|
||||||
|
var prompt = RefinePrompt.BuildPrompt(task, subs);
|
||||||
|
var args = RefinePrompt.BuildArgs(MaxTurns, canReadRepo);
|
||||||
|
|
||||||
|
using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
|
||||||
|
timeoutCts.CancelAfter(RunTimeout);
|
||||||
|
|
||||||
|
var result = await _claude.RunAsync(
|
||||||
|
arguments: args,
|
||||||
|
prompt: prompt,
|
||||||
|
workingDirectory: cwd,
|
||||||
|
onStdoutLine: async line => await logWriter.WriteLineAsync(line),
|
||||||
|
ct: timeoutCts.Token);
|
||||||
|
|
||||||
|
success = result.IsSuccess;
|
||||||
|
if (!success) error = $"exit code {result.ExitCode}";
|
||||||
|
return success
|
||||||
|
? new RefineRunOutcome(true, "Refine complete")
|
||||||
|
: new RefineRunOutcome(false, error!);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException) when (!ct.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
error = $"timed out after {RunTimeout.TotalMinutes:0} min";
|
||||||
|
return new RefineRunOutcome(false, error);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Refine run failed for {TaskId}", taskId);
|
||||||
|
error = ex.Message;
|
||||||
|
return new RefineRunOutcome(false, ex.Message);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
await _broadcaster.RefineFinishedAsync(taskId, success, error);
|
||||||
|
lock (_lock) { _inFlight.Remove(taskId); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 6: Write `RefineRunnerTests.cs` (guards, with a fake IClaudeProcess)**
|
||||||
|
|
||||||
|
The test project already has a fake/stub for `IClaudeProcess` used by Prime tests — reuse it (recording invocation + returning a configurable success result). Do NOT spawn the real CLI.
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class RefineRunnerTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task Refuses_when_task_not_idle()
|
||||||
|
{
|
||||||
|
await using var f = new RefineRunnerFixture(); // mirror Prime test fixture wiring
|
||||||
|
var task = await f.SeedTaskAsync(status: TaskStatus.Queued);
|
||||||
|
var outcome = await f.Runner.RefineAsync(task.Id, CancellationToken.None);
|
||||||
|
Assert.False(outcome.Success);
|
||||||
|
Assert.Equal(0, f.Claude.RunCount); // never invoked the CLI
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Idle_task_invokes_claude_once_and_brackets_with_events()
|
||||||
|
{
|
||||||
|
await using var f = new RefineRunnerFixture();
|
||||||
|
var task = await f.SeedTaskAsync(status: TaskStatus.Idle);
|
||||||
|
var outcome = await f.Runner.RefineAsync(task.Id, CancellationToken.None);
|
||||||
|
Assert.True(outcome.Success);
|
||||||
|
Assert.Equal(1, f.Claude.RunCount);
|
||||||
|
Assert.Equal(1, f.Broadcaster.Started);
|
||||||
|
Assert.Equal(1, f.Broadcaster.Finished);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> Build the `RefineRunnerFixture`/fakes by copying the Prime test's `IClaudeProcess` stub + real-SQLite `IDbContextFactory` setup and a recording `IRefineBroadcaster`. If a Prime fixture exists, mirror it; otherwise construct inline.
|
||||||
|
|
||||||
|
Run: `dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --filter RefineRunnerTests`
|
||||||
|
Expected: PASS (2 tests).
|
||||||
|
|
||||||
|
- [ ] **Step 7: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/ClaudeDo.Worker/Refine tests/ClaudeDo.Worker.Tests/Refine
|
||||||
|
git commit -m "feat(refine): add RefineRunner, prompt/args helper, and interfaces"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 4: Worker wiring — broadcaster, hub, DI
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/ClaudeDo.Worker/Hub/HubBroadcaster.cs`
|
||||||
|
- Modify: `src/ClaudeDo.Worker/Hub/WorkerHub.cs`
|
||||||
|
- Modify: `src/ClaudeDo.Worker/Program.cs`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Implement events on `HubBroadcaster`**
|
||||||
|
|
||||||
|
Add `IRefineBroadcaster` to the class's interface list (`public sealed class HubBroadcaster : ..., IRefineBroadcaster`) and add (mirroring the `Prep*` block):
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public Task RefineStarted(string taskId) => _hub.Clients.All.SendAsync("RefineStarted", taskId);
|
||||||
|
public Task RefineFinished(string taskId, bool success, string? error) =>
|
||||||
|
_hub.Clients.All.SendAsync("RefineFinished", taskId, success, error);
|
||||||
|
|
||||||
|
Task IRefineBroadcaster.RefineStartedAsync(string taskId) => RefineStarted(taskId);
|
||||||
|
Task IRefineBroadcaster.RefineFinishedAsync(string taskId, bool success, string? error) =>
|
||||||
|
RefineFinished(taskId, success, error);
|
||||||
|
```
|
||||||
|
|
||||||
|
Add `using ClaudeDo.Worker.Refine;`.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Add `RefineTask` to `WorkerHub`**
|
||||||
|
|
||||||
|
`WorkerHub` injects services via its constructor. Add a `private readonly IRefineRunner _refineRunner;` field, add the parameter to the constructor and assign it. Add the method (fire-and-forget; the runner brackets with its own events):
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public Task RefineTask(string taskId)
|
||||||
|
{
|
||||||
|
_ = _refineRunner.RefineAsync(taskId, CancellationToken.None);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Add `using ClaudeDo.Worker.Refine;`.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Register DI in `Program.cs`**
|
||||||
|
|
||||||
|
Near the Prime registrations:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
builder.Services.AddSingleton<IRefineRunner, RefineRunner>();
|
||||||
|
builder.Services.AddSingleton<IRefineBroadcaster>(sp => sp.GetRequiredService<HubBroadcaster>());
|
||||||
|
```
|
||||||
|
|
||||||
|
Add `using ClaudeDo.Worker.Refine;` if needed. (`HubBroadcaster` is already registered as a singleton — confirm and reuse that registration; do not double-register it.)
|
||||||
|
|
||||||
|
- [ ] **Step 4: Build the worker**
|
||||||
|
|
||||||
|
Run: `dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj -c Release`
|
||||||
|
Expected: Build succeeded.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/ClaudeDo.Worker/Hub/HubBroadcaster.cs src/ClaudeDo.Worker/Hub/WorkerHub.cs src/ClaudeDo.Worker/Program.cs
|
||||||
|
git commit -m "feat(refine): wire RefineTask hub method, broadcaster events, and DI"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 5: UI worker client — call + events + fakes
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/ClaudeDo.Ui/Services/Interfaces/IWorkerClient.cs`
|
||||||
|
- Modify: `src/ClaudeDo.Ui/Services/WorkerClient.cs`
|
||||||
|
- Modify: test fakes implementing `IWorkerClient`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Extend the interface**
|
||||||
|
|
||||||
|
In `IWorkerClient.cs` add (near `RunDailyPrepNowAsync` and the `Prep*` events):
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
Task RefineTaskAsync(string taskId);
|
||||||
|
|
||||||
|
event Action<string>? RefineStartedEvent;
|
||||||
|
event Action<string, bool, string?>? RefineFinishedEvent;
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Implement in `WorkerClient`**
|
||||||
|
|
||||||
|
Add the method (mirror `RunDailyPrepNowAsync`):
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public Task RefineTaskAsync(string taskId) => _hub.InvokeAsync("RefineTask", taskId);
|
||||||
|
```
|
||||||
|
|
||||||
|
Declare the events:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public event Action<string>? RefineStartedEvent;
|
||||||
|
public event Action<string, bool, string?>? RefineFinishedEvent;
|
||||||
|
```
|
||||||
|
|
||||||
|
Subscribe in the constructor (mirror the `Prep*` subscriptions block):
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
_hub.On<string>("RefineStarted", id =>
|
||||||
|
Dispatcher.UIThread.Post(() => RefineStartedEvent?.Invoke(id)));
|
||||||
|
_hub.On<string, bool, string?>("RefineFinished", (id, ok, err) =>
|
||||||
|
Dispatcher.UIThread.Post(() => RefineFinishedEvent?.Invoke(id, ok, err)));
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Update test fakes**
|
||||||
|
|
||||||
|
Find every hand-rolled `IWorkerClient` implementation (search the test projects) and add `RefineTaskAsync` (return `Task.CompletedTask`) plus the two events (`= delegate {}` or `add{}remove{}` no-ops as the fake convention dictates). Build each affected test project.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Build UI + test projects**
|
||||||
|
|
||||||
|
Run: `dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release`
|
||||||
|
Then build the UI test project(s). Expected: Build succeeded.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/ClaudeDo.Ui/Services/Interfaces/IWorkerClient.cs src/ClaudeDo.Ui/Services/WorkerClient.cs <fake files>
|
||||||
|
git commit -m "feat(ui): add RefineTask client call and refine events"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 6: UI — icon, button, view model, command
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/ClaudeDo.Ui/Design/IslandStyles.axaml`
|
||||||
|
- Modify: `src/ClaudeDo.Ui/Views/Islands/TaskRowView.axaml`
|
||||||
|
- Modify: `src/ClaudeDo.Ui/ViewModels/Islands/TaskRowViewModel.cs`
|
||||||
|
- Modify: `src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs`
|
||||||
|
- Modify: `locales/en.json`, `locales/de.json`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add the `Icon.Refine` geometry**
|
||||||
|
|
||||||
|
In `IslandStyles.axaml`, near the other `Icon.*` `StreamGeometry` resources, add the supplied SVG converted to path data (line-art, rendered stroked via `plan-icon`):
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<StreamGeometry x:Key="Icon.Refine">M3,5 L11,5 M3,9 L9,9 M3,13 L7,13 M19,1.8 L19.7,3.9 L21.7,4.6 L19.7,5.3 L19,7.4 L18.3,5.3 L16.3,4.6 L18.3,3.9 Z M18,10.5 L12.2,16.3 M16.6,9.1 L19.4,11.9 M12.2,16.3 L11,18.5 L13.2,17.5 Z</StreamGeometry>
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Add `IsRefining`/`CanRefine` to `TaskRowViewModel`**
|
||||||
|
|
||||||
|
Add the observable property (with the other `[ObservableProperty]` fields):
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[ObservableProperty] private bool _isRefining;
|
||||||
|
```
|
||||||
|
|
||||||
|
Add a computed gate (refine is only offered for Idle, non-parent tasks). Place near other `Can*` getters:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public bool CanRefine => Status == TaskStatus.Idle && PlanningPhase == PlanningPhase.None && !IsRefining;
|
||||||
|
```
|
||||||
|
|
||||||
|
If `Status`/`PlanningPhase`/`IsRefining` are `[ObservableProperty]`, raise `CanRefine` change notifications via partial `On<Prop>Changed` hooks:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
partial void OnStatusChanged(TaskStatus value) => OnPropertyChanged(nameof(CanRefine));
|
||||||
|
partial void OnPlanningPhaseChanged(PlanningPhase value) => OnPropertyChanged(nameof(CanRefine));
|
||||||
|
partial void OnIsRefiningChanged(bool value) => OnPropertyChanged(nameof(CanRefine));
|
||||||
|
```
|
||||||
|
|
||||||
|
> If `On...Changed` partials already exist for `Status`/`PlanningPhase`, add the `OnPropertyChanged(nameof(CanRefine))` line inside them instead of redeclaring.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Add `RefineTaskCommand` + event wiring to `TasksIslandViewModel`**
|
||||||
|
|
||||||
|
Add the command (mirror an existing per-row command like `ToggleStarCommand`, which takes a `TaskRowViewModel`):
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[RelayCommand]
|
||||||
|
private async Task RefineTask(TaskRowViewModel row)
|
||||||
|
{
|
||||||
|
if (row is null || !row.CanRefine) return;
|
||||||
|
row.IsRefining = true;
|
||||||
|
try { await _worker.RefineTaskAsync(row.Id); }
|
||||||
|
catch { row.IsRefining = false; }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> Use the same injected worker-client field name this VM already uses (e.g. `_worker`/`_client`). Match it.
|
||||||
|
|
||||||
|
Subscribe to the refine events where the VM wires other worker events (where `OnWorkerTaskUpdated` is subscribed). Add handlers that flip the row flag:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
private void OnRefineStarted(string taskId)
|
||||||
|
{
|
||||||
|
var row = Items.FirstOrDefault(r => r.Id == taskId);
|
||||||
|
if (row is not null) row.IsRefining = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnRefineFinished(string taskId, bool ok, string? error)
|
||||||
|
{
|
||||||
|
var row = Items.FirstOrDefault(r => r.Id == taskId);
|
||||||
|
if (row is not null) row.IsRefining = false;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Wire them next to the existing subscriptions (and unsubscribe in the same place the VM unsubscribes others, if it does):
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
_worker.RefineStartedEvent += OnRefineStarted;
|
||||||
|
_worker.RefineFinishedEvent += OnRefineFinished;
|
||||||
|
```
|
||||||
|
|
||||||
|
(Content changes—new description/subtasks—arrive through the existing `TaskUpdated` → `OnWorkerTaskUpdated` path; no extra work needed.)
|
||||||
|
|
||||||
|
- [ ] **Step 4: Add the button to `TaskRowView.axaml`**
|
||||||
|
|
||||||
|
Mirror the star button (`Grid.Column="5"` area). Add a refine `icon-btn` (e.g. as a new column or beside the star) bound to the parent ItemsControl's command, passing the row as parameter. Use the `plan-icon` stroked `Path` inside a `Viewbox` (matching the Plan-day button), gate visibility on `CanRefine`, and disable/spin on `IsRefining`:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<Button Classes="icon-btn refine-btn"
|
||||||
|
IsVisible="{Binding CanRefine}"
|
||||||
|
Command="{Binding $parent[ItemsControl].((vm:TasksIslandViewModel)DataContext).RefineTaskCommand}"
|
||||||
|
CommandParameter="{Binding}"
|
||||||
|
ToolTip.Tip="{loc:Tr tasks.refineTip}">
|
||||||
|
<Viewbox Width="16" Height="16">
|
||||||
|
<Path Classes="plan-icon" Data="{StaticResource Icon.Refine}"/>
|
||||||
|
</Viewbox>
|
||||||
|
</Button>
|
||||||
|
```
|
||||||
|
|
||||||
|
> Match the column layout already in `TaskRowView.axaml`. If a new grid column is needed, widen `ColumnDefinitions` accordingly and place the refine button left of the star (`Grid.Column`). Keep the existing `vm:` / `loc:` xmlns aliases the file already declares.
|
||||||
|
|
||||||
|
Optionally show a spinning/dimmed state while `IsRefining` (e.g. a style `Selector="Button.refine-btn:disabled"` or bind opacity to `IsRefining`). Keep it simple; a disabled look is enough.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Add localization keys**
|
||||||
|
|
||||||
|
Add to both `locales/en.json` and `locales/de.json` under the `tasks` group (keys must stay in parity):
|
||||||
|
|
||||||
|
- en: `"tasks.refineTip": "Refine this task with Claude"`
|
||||||
|
- de: `"tasks.refineTip": "Aufgabe mit Claude verfeinern"`
|
||||||
|
|
||||||
|
> Match the file's actual key structure (flat `"tasks.x"` vs nested `tasks: { x }`)—look at an existing `tasks.*` tooltip key (e.g. the plan-day tip) and follow it exactly.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Build UI**
|
||||||
|
|
||||||
|
Run: `dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release`
|
||||||
|
Then run the Localization parity tests: `dotnet test tests/ClaudeDo.Localization.Tests/ClaudeDo.Localization.Tests.csproj -c Release`
|
||||||
|
Expected: Build succeeded; locale parity passes.
|
||||||
|
|
||||||
|
- [ ] **Step 7: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/ClaudeDo.Ui/Design/IslandStyles.axaml src/ClaudeDo.Ui/Views/Islands/TaskRowView.axaml src/ClaudeDo.Ui/ViewModels/Islands/TaskRowViewModel.cs src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs locales/en.json locales/de.json
|
||||||
|
git commit -m "feat(ui): add Refine button, icon, and command to task card"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 7: Full build + test sweep, manual smoke
|
||||||
|
|
||||||
|
- [ ] **Step 1: Build all main projects**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release
|
||||||
|
dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj -c Release
|
||||||
|
```
|
||||||
|
Expected: Build succeeded for both.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the worker + UI test suites**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release
|
||||||
|
dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release
|
||||||
|
dotnet test tests/ClaudeDo.Localization.Tests/ClaudeDo.Localization.Tests.csproj -c Release
|
||||||
|
```
|
||||||
|
Expected: all green.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Manual smoke (visual + real CLI — flag to user)**
|
||||||
|
|
||||||
|
Cannot be automated (no real-Claude in tests). Verify by hand: start Worker + UI, on an Idle task click the refine icon → button shows busy → after the run the description improves and steps appear in the Steps card → task stays Idle. Confirm the refine icon is hidden for Queued/Running/Done tasks and for planning parents. **Report this as a visual-verification gap for the user to confirm.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notes on parallelism / execution
|
||||||
|
|
||||||
|
- Tasks 1–4 are backend and largely sequential (4 depends on 3). Tasks 1 and 2 are independent and could be done first in either order.
|
||||||
|
- Tasks 5–6 (UI) depend on Task 4's hub/event contract.
|
||||||
|
- Per project convention: subagents use `sonnet`, stage files by explicit path, and do NOT run git/build inside parallel agents — the orchestrator builds, tests, and commits after each task.
|
||||||
Reference in New Issue
Block a user