docs(superpowers): add default-agents plan and design spec
This commit is contained in:
852
docs/superpowers/plans/2026-04-23-default-agents.md
Normal file
852
docs/superpowers/plans/2026-04-23-default-agents.md
Normal file
@@ -0,0 +1,852 @@
|
|||||||
|
# Default Agents 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:** Ship ClaudeDo with 6 default agents (code-reviewer, test-writer, debugger, security-reviewer, explorer, researcher) that seed into `~/.todo-app/agents/` on first launch, with a "Restore defaults" button in the settings modal.
|
||||||
|
|
||||||
|
**Architecture:** Bundled `.md` files in `src/ClaudeDo.Worker/DefaultAgents/` are copied to the Worker output folder. A new `DefaultAgentSeeder` service copies any missing file into the user's agents dir — run once at startup, and again on demand via a new `WorkerHub.RestoreDefaultAgents` method invoked by a button in `SettingsModalView`.
|
||||||
|
|
||||||
|
**Tech Stack:** .NET 8 / ASP.NET Core / SignalR / Avalonia 12 / CommunityToolkit.Mvvm / xUnit
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
**Create:**
|
||||||
|
- `src/ClaudeDo.Worker/DefaultAgents/code-reviewer.md`
|
||||||
|
- `src/ClaudeDo.Worker/DefaultAgents/test-writer.md`
|
||||||
|
- `src/ClaudeDo.Worker/DefaultAgents/debugger.md`
|
||||||
|
- `src/ClaudeDo.Worker/DefaultAgents/security-reviewer.md`
|
||||||
|
- `src/ClaudeDo.Worker/DefaultAgents/explorer.md`
|
||||||
|
- `src/ClaudeDo.Worker/DefaultAgents/researcher.md`
|
||||||
|
- `src/ClaudeDo.Worker/Services/DefaultAgentSeeder.cs`
|
||||||
|
- `tests/ClaudeDo.Worker.Tests/Services/DefaultAgentSeederTests.cs`
|
||||||
|
|
||||||
|
**Modify:**
|
||||||
|
- `src/ClaudeDo.Worker/ClaudeDo.Worker.csproj` — add Content item group for `DefaultAgents\*.md`
|
||||||
|
- `src/ClaudeDo.Worker/Program.cs` — register seeder, run `SeedMissingAsync()` once at startup
|
||||||
|
- `src/ClaudeDo.Worker/Hub/WorkerHub.cs` — inject `DefaultAgentSeeder`, add `RestoreDefaultAgents` method
|
||||||
|
- `src/ClaudeDo.Ui/Services/WorkerClient.cs` — add `RestoreDefaultAgentsAsync` method + `SeedResultDto` record
|
||||||
|
- `src/ClaudeDo.Ui/ViewModels/Modals/SettingsModalViewModel.cs` — add `RestoreDefaultAgentsCommand`
|
||||||
|
- `src/ClaudeDo.Ui/Views/Modals/SettingsModalView.axaml` — add button section
|
||||||
|
- `tests/ClaudeDo.Worker.Tests/Hub/AgentSettingsHubTests.cs` — add seeder integration test
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: Bundle default agent markdown files
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `src/ClaudeDo.Worker/DefaultAgents/code-reviewer.md`
|
||||||
|
- Create: `src/ClaudeDo.Worker/DefaultAgents/test-writer.md`
|
||||||
|
- Create: `src/ClaudeDo.Worker/DefaultAgents/debugger.md`
|
||||||
|
- Create: `src/ClaudeDo.Worker/DefaultAgents/security-reviewer.md`
|
||||||
|
- Create: `src/ClaudeDo.Worker/DefaultAgents/explorer.md`
|
||||||
|
- Create: `src/ClaudeDo.Worker/DefaultAgents/researcher.md`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write `code-reviewer.md`**
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
---
|
||||||
|
name: code-reviewer
|
||||||
|
description: Reviews code changes for bugs, logic errors, and convention violations. Flags only high-confidence issues.
|
||||||
|
---
|
||||||
|
|
||||||
|
You are a code reviewer. Your job is to inspect the diff for real problems, not nitpicks.
|
||||||
|
|
||||||
|
Focus on:
|
||||||
|
- Logic errors, off-by-one bugs, null/empty handling
|
||||||
|
- Broken invariants, race conditions, resource leaks
|
||||||
|
- Violations of the project's established conventions (read nearby code first)
|
||||||
|
- Missing error handling at system boundaries (external input, IO, network)
|
||||||
|
|
||||||
|
Skip:
|
||||||
|
- Style preferences the codebase doesn't enforce
|
||||||
|
- Speculative "what if" concerns
|
||||||
|
- Renaming for its own sake
|
||||||
|
|
||||||
|
Output: a short list of concrete issues with file:line references. If the diff is clean, say so in one sentence. Do not rewrite the code — call out the problem and let the implementer fix it.
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Write `test-writer.md`**
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
---
|
||||||
|
name: test-writer
|
||||||
|
description: Generates unit and integration tests for existing or new code. Follows the project's test patterns and frameworks.
|
||||||
|
---
|
||||||
|
|
||||||
|
You are a test-writer. Your job is to write focused, useful tests for code under review.
|
||||||
|
|
||||||
|
Process:
|
||||||
|
1. Read the target code and identify the observable behavior.
|
||||||
|
2. Read existing tests nearby to match the framework, fixtures, naming, and assertion style.
|
||||||
|
3. Write tests covering the happy path, boundary conditions, and the specific failure modes that matter.
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
- One behavior per test. Clear Arrange/Act/Assert.
|
||||||
|
- No tests for private implementation details — exercise public API.
|
||||||
|
- No mocks where real objects are cheap (in-memory DBs, temp dirs).
|
||||||
|
- Skip trivially-correct tests (getter returns what you set).
|
||||||
|
|
||||||
|
Output: the test file(s) ready to compile, matching the project's conventions. Include the command to run them.
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Write `debugger.md`**
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
---
|
||||||
|
name: debugger
|
||||||
|
description: Systematic root-cause analysis for bugs, test failures, and unexpected behavior. Hypothesize, isolate, verify.
|
||||||
|
---
|
||||||
|
|
||||||
|
You are a debugger. You do NOT guess at fixes — you find the root cause first.
|
||||||
|
|
||||||
|
Process:
|
||||||
|
1. Reproduce. Get a minimal, deterministic repro. If you can't reproduce it, say so and stop.
|
||||||
|
2. Isolate. Narrow the failing path (bisect, binary search, or tracing).
|
||||||
|
3. Hypothesize. State a specific, falsifiable cause.
|
||||||
|
4. Verify. Prove the hypothesis by observation (logs, debugger, targeted print) — not by "this seems likely".
|
||||||
|
5. Fix at the root, not the symptom. If the only fix is a workaround, explain why.
|
||||||
|
|
||||||
|
Anti-patterns to avoid:
|
||||||
|
- Making changes to "see if it works"
|
||||||
|
- Adding try/catch to silence errors
|
||||||
|
- Declaring the bug fixed without reproducing the fix
|
||||||
|
|
||||||
|
Output: repro steps, root cause, and the minimal fix. Include evidence (log excerpt, command output) that proves the cause.
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Write `security-reviewer.md`**
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
---
|
||||||
|
name: security-reviewer
|
||||||
|
description: Audits code for OWASP-class security issues — auth, injection, input handling, secret exposure.
|
||||||
|
---
|
||||||
|
|
||||||
|
You are a security reviewer. Focus on real, exploitable weaknesses — not theoretical hardening.
|
||||||
|
|
||||||
|
Check for:
|
||||||
|
- Injection: SQL, command, path traversal, XSS, template injection
|
||||||
|
- Auth: missing authorization, token handling, session fixation
|
||||||
|
- Input validation at system boundaries (HTTP, files, IPC)
|
||||||
|
- Secrets: hardcoded credentials, tokens in logs, leaked env vars
|
||||||
|
- Unsafe deserialization, XXE, SSRF
|
||||||
|
- Cryptography misuse (custom crypto, weak algorithms, fixed IVs)
|
||||||
|
|
||||||
|
Ignore:
|
||||||
|
- Internal trust-boundary assumptions the project already documents
|
||||||
|
- Defense-in-depth ideas with no concrete attack path
|
||||||
|
|
||||||
|
Output: a prioritized list — severity, file:line, the exploit path, the fix. If nothing is wrong, say so plainly.
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: Write `explorer.md`**
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
---
|
||||||
|
name: explorer
|
||||||
|
description: Fast codebase navigation — find files, search for patterns, answer "where/how" questions. Terse output.
|
||||||
|
---
|
||||||
|
|
||||||
|
You are an explorer. Your job is to find things in the codebase quickly and report back concisely.
|
||||||
|
|
||||||
|
Use:
|
||||||
|
- Glob/Grep for searches
|
||||||
|
- Read only for files you need to quote from
|
||||||
|
|
||||||
|
Do NOT:
|
||||||
|
- Refactor, edit, or "improve" anything
|
||||||
|
- Read files that aren't relevant to the question
|
||||||
|
- Dump raw tool output — summarize
|
||||||
|
|
||||||
|
Output style:
|
||||||
|
- Lead with the answer in one sentence.
|
||||||
|
- Back it up with file:line references.
|
||||||
|
- If you found nothing, say "no match" and what you searched for.
|
||||||
|
|
||||||
|
Keep responses short. The caller wants facts, not prose.
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 6: Write `researcher.md`**
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
---
|
||||||
|
name: researcher
|
||||||
|
description: General-purpose research and analysis for non-code tasks — summarize docs, investigate questions, draft prose.
|
||||||
|
---
|
||||||
|
|
||||||
|
You are a researcher. You handle tasks that don't fit the code-review/test/debug shape.
|
||||||
|
|
||||||
|
Good fits:
|
||||||
|
- Summarizing documents, specs, or long outputs
|
||||||
|
- Investigating an open question (what does X do, how does Y work, what are the tradeoffs)
|
||||||
|
- Drafting non-code text (release notes, emails, docs)
|
||||||
|
- Analyzing structured data (logs, CSV, JSON) and reporting findings
|
||||||
|
|
||||||
|
Process:
|
||||||
|
1. Restate the task in one sentence so you know what "done" looks like.
|
||||||
|
2. Gather just enough information — stop when you can answer, not when you run out of sources.
|
||||||
|
3. Distinguish facts ("the file says X") from inference ("so likely Y").
|
||||||
|
4. Cite sources (file:line, URL, log excerpt) for every claim.
|
||||||
|
|
||||||
|
Output: direct answer first, supporting evidence second. Keep it short unless asked for depth.
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 7: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/ClaudeDo.Worker/DefaultAgents/
|
||||||
|
git commit -m "feat(worker): add bundled default agent definitions"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 2: Wire bundled agents into build output
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/ClaudeDo.Worker/ClaudeDo.Worker.csproj`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add Content item group for DefaultAgents**
|
||||||
|
|
||||||
|
Edit `src/ClaudeDo.Worker/ClaudeDo.Worker.csproj`. After the existing `<ItemGroup>` blocks and before the final `<PropertyGroup>`, add:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<ItemGroup>
|
||||||
|
<Content Include="DefaultAgents\*.md">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</Content>
|
||||||
|
</ItemGroup>
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Build the worker and verify the files land in output**
|
||||||
|
|
||||||
|
Run: `dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj`
|
||||||
|
Expected: build succeeds.
|
||||||
|
|
||||||
|
Then verify output:
|
||||||
|
|
||||||
|
Run: `ls src/ClaudeDo.Worker/bin/Debug/net8.0/DefaultAgents/`
|
||||||
|
Expected: all 6 `.md` files present.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/ClaudeDo.Worker/ClaudeDo.Worker.csproj
|
||||||
|
git commit -m "build(worker): ship DefaultAgents folder in build output"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 3: Write DefaultAgentSeeder tests (failing)
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `tests/ClaudeDo.Worker.Tests/Services/DefaultAgentSeederTests.cs`
|
||||||
|
|
||||||
|
The service doesn't exist yet — these tests will fail to compile initially. That's fine; the next task implements it.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the test file**
|
||||||
|
|
||||||
|
Create `tests/ClaudeDo.Worker.Tests/Services/DefaultAgentSeederTests.cs`:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using ClaudeDo.Worker.Services;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Worker.Tests.Services;
|
||||||
|
|
||||||
|
public sealed class DefaultAgentSeederTests : IDisposable
|
||||||
|
{
|
||||||
|
private readonly string _bundleDir;
|
||||||
|
private readonly string _targetDir;
|
||||||
|
|
||||||
|
public DefaultAgentSeederTests()
|
||||||
|
{
|
||||||
|
var root = Path.Combine(Path.GetTempPath(), $"claudedo_seeder_{Guid.NewGuid():N}");
|
||||||
|
_bundleDir = Path.Combine(root, "bundle");
|
||||||
|
_targetDir = Path.Combine(root, "target");
|
||||||
|
Directory.CreateDirectory(_bundleDir);
|
||||||
|
Directory.CreateDirectory(_targetDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
try { Directory.Delete(Path.GetDirectoryName(_bundleDir)!, true); } catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task WriteBundleAsync(string name, string content)
|
||||||
|
{
|
||||||
|
await File.WriteAllTextAsync(Path.Combine(_bundleDir, name), content);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SeedMissing_CopiesAllFiles_WhenTargetEmpty()
|
||||||
|
{
|
||||||
|
await WriteBundleAsync("a.md", "A");
|
||||||
|
await WriteBundleAsync("b.md", "B");
|
||||||
|
var seeder = new DefaultAgentSeeder(_bundleDir, _targetDir);
|
||||||
|
|
||||||
|
var result = await seeder.SeedMissingAsync();
|
||||||
|
|
||||||
|
Assert.Equal(2, result.Copied);
|
||||||
|
Assert.Equal(0, result.Skipped);
|
||||||
|
Assert.True(File.Exists(Path.Combine(_targetDir, "a.md")));
|
||||||
|
Assert.True(File.Exists(Path.Combine(_targetDir, "b.md")));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SeedMissing_SkipsExistingFiles()
|
||||||
|
{
|
||||||
|
await WriteBundleAsync("a.md", "bundled");
|
||||||
|
await File.WriteAllTextAsync(Path.Combine(_targetDir, "a.md"), "user-modified");
|
||||||
|
var seeder = new DefaultAgentSeeder(_bundleDir, _targetDir);
|
||||||
|
|
||||||
|
var result = await seeder.SeedMissingAsync();
|
||||||
|
|
||||||
|
Assert.Equal(0, result.Copied);
|
||||||
|
Assert.Equal(1, result.Skipped);
|
||||||
|
var content = await File.ReadAllTextAsync(Path.Combine(_targetDir, "a.md"));
|
||||||
|
Assert.Equal("user-modified", content);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SeedMissing_MixedState_CopiesOnlyMissing()
|
||||||
|
{
|
||||||
|
await WriteBundleAsync("a.md", "A");
|
||||||
|
await WriteBundleAsync("b.md", "B");
|
||||||
|
await File.WriteAllTextAsync(Path.Combine(_targetDir, "a.md"), "existing");
|
||||||
|
var seeder = new DefaultAgentSeeder(_bundleDir, _targetDir);
|
||||||
|
|
||||||
|
var result = await seeder.SeedMissingAsync();
|
||||||
|
|
||||||
|
Assert.Equal(1, result.Copied);
|
||||||
|
Assert.Equal(1, result.Skipped);
|
||||||
|
Assert.Equal("existing", await File.ReadAllTextAsync(Path.Combine(_targetDir, "a.md")));
|
||||||
|
Assert.Equal("B", await File.ReadAllTextAsync(Path.Combine(_targetDir, "b.md")));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SeedMissing_ReturnsZero_WhenBundleDirMissing()
|
||||||
|
{
|
||||||
|
var missingBundle = Path.Combine(Path.GetTempPath(), $"claudedo_missing_{Guid.NewGuid():N}");
|
||||||
|
var seeder = new DefaultAgentSeeder(missingBundle, _targetDir);
|
||||||
|
|
||||||
|
var result = await seeder.SeedMissingAsync();
|
||||||
|
|
||||||
|
Assert.Equal(0, result.Copied);
|
||||||
|
Assert.Equal(0, result.Skipped);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SeedMissing_CreatesTargetDir_IfMissing()
|
||||||
|
{
|
||||||
|
await WriteBundleAsync("a.md", "A");
|
||||||
|
var missingTarget = Path.Combine(_targetDir, "nested", "created");
|
||||||
|
var seeder = new DefaultAgentSeeder(_bundleDir, missingTarget);
|
||||||
|
|
||||||
|
var result = await seeder.SeedMissingAsync();
|
||||||
|
|
||||||
|
Assert.Equal(1, result.Copied);
|
||||||
|
Assert.True(File.Exists(Path.Combine(missingTarget, "a.md")));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SeedMissing_IgnoresNonMarkdownFiles()
|
||||||
|
{
|
||||||
|
await WriteBundleAsync("a.md", "A");
|
||||||
|
await File.WriteAllTextAsync(Path.Combine(_bundleDir, "readme.txt"), "not an agent");
|
||||||
|
var seeder = new DefaultAgentSeeder(_bundleDir, _targetDir);
|
||||||
|
|
||||||
|
var result = await seeder.SeedMissingAsync();
|
||||||
|
|
||||||
|
Assert.Equal(1, result.Copied);
|
||||||
|
Assert.False(File.Exists(Path.Combine(_targetDir, "readme.txt")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run tests to confirm they fail to compile**
|
||||||
|
|
||||||
|
Run: `dotnet test tests/ClaudeDo.Worker.Tests --filter "FullyQualifiedName~DefaultAgentSeederTests"`
|
||||||
|
Expected: compile error — `The type or namespace name 'DefaultAgentSeeder' could not be found`.
|
||||||
|
|
||||||
|
This confirms the tests target the not-yet-written service.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 4: Implement DefaultAgentSeeder
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `src/ClaudeDo.Worker/Services/DefaultAgentSeeder.cs`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the service**
|
||||||
|
|
||||||
|
Create `src/ClaudeDo.Worker/Services/DefaultAgentSeeder.cs`:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Worker.Services;
|
||||||
|
|
||||||
|
public sealed record SeedResult(int Copied, int Skipped);
|
||||||
|
|
||||||
|
public sealed class DefaultAgentSeeder
|
||||||
|
{
|
||||||
|
private readonly string _bundleDir;
|
||||||
|
private readonly string _targetDir;
|
||||||
|
private readonly ILogger<DefaultAgentSeeder>? _logger;
|
||||||
|
|
||||||
|
public DefaultAgentSeeder(string bundleDir, string targetDir, ILogger<DefaultAgentSeeder>? logger = null)
|
||||||
|
{
|
||||||
|
_bundleDir = bundleDir;
|
||||||
|
_targetDir = targetDir;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<SeedResult> SeedMissingAsync(CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
if (!Directory.Exists(_bundleDir))
|
||||||
|
{
|
||||||
|
_logger?.LogWarning("DefaultAgents bundle dir not found: {Dir}", _bundleDir);
|
||||||
|
return new SeedResult(0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
Directory.CreateDirectory(_targetDir);
|
||||||
|
|
||||||
|
int copied = 0;
|
||||||
|
int skipped = 0;
|
||||||
|
|
||||||
|
foreach (var src in Directory.EnumerateFiles(_bundleDir, "*.md"))
|
||||||
|
{
|
||||||
|
ct.ThrowIfCancellationRequested();
|
||||||
|
var fileName = Path.GetFileName(src);
|
||||||
|
var dst = Path.Combine(_targetDir, fileName);
|
||||||
|
|
||||||
|
if (File.Exists(dst))
|
||||||
|
{
|
||||||
|
skipped++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var input = File.OpenRead(src);
|
||||||
|
using var output = File.Create(dst);
|
||||||
|
await input.CopyToAsync(output, ct);
|
||||||
|
copied++;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger?.LogWarning(ex, "Failed to copy default agent {File}", fileName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new SeedResult(copied, skipped);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the tests and verify they pass**
|
||||||
|
|
||||||
|
Run: `dotnet test tests/ClaudeDo.Worker.Tests --filter "FullyQualifiedName~DefaultAgentSeederTests"`
|
||||||
|
Expected: 6 tests pass.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/ClaudeDo.Worker/Services/DefaultAgentSeeder.cs tests/ClaudeDo.Worker.Tests/Services/DefaultAgentSeederTests.cs
|
||||||
|
git commit -m "feat(worker): add DefaultAgentSeeder for first-launch agent seeding"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 5: Wire seeder into Worker startup
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/ClaudeDo.Worker/Program.cs`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Register seeder and run SeedMissingAsync at startup**
|
||||||
|
|
||||||
|
In `src/ClaudeDo.Worker/Program.cs`, replace the "Agent file management." block (currently lines 36–39):
|
||||||
|
|
||||||
|
Find:
|
||||||
|
```csharp
|
||||||
|
// Agent file management.
|
||||||
|
var agentsDir = Path.Combine(ClaudeDo.Data.Paths.AppDataRoot(), "agents");
|
||||||
|
Directory.CreateDirectory(agentsDir);
|
||||||
|
builder.Services.AddSingleton(new AgentFileService(agentsDir));
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace with:
|
||||||
|
```csharp
|
||||||
|
// Agent file management.
|
||||||
|
var agentsDir = Path.Combine(ClaudeDo.Data.Paths.AppDataRoot(), "agents");
|
||||||
|
Directory.CreateDirectory(agentsDir);
|
||||||
|
builder.Services.AddSingleton(new AgentFileService(agentsDir));
|
||||||
|
|
||||||
|
var defaultAgentsBundleDir = Path.Combine(AppContext.BaseDirectory, "DefaultAgents");
|
||||||
|
builder.Services.AddSingleton(sp => new DefaultAgentSeeder(
|
||||||
|
defaultAgentsBundleDir,
|
||||||
|
agentsDir,
|
||||||
|
sp.GetService<Microsoft.Extensions.Logging.ILogger<DefaultAgentSeeder>>()));
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, after `var app = builder.Build();` and before `app.MapHub<WorkerHub>("/hub");`, add:
|
||||||
|
|
||||||
|
Find:
|
||||||
|
```csharp
|
||||||
|
using (var scope = app.Services.CreateScope())
|
||||||
|
{
|
||||||
|
ClaudeDoDbContext.MigrateAndConfigure(
|
||||||
|
scope.ServiceProvider.GetRequiredService<ClaudeDoDbContext>());
|
||||||
|
}
|
||||||
|
|
||||||
|
app.MapHub<WorkerHub>("/hub");
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace with:
|
||||||
|
```csharp
|
||||||
|
using (var scope = app.Services.CreateScope())
|
||||||
|
{
|
||||||
|
ClaudeDoDbContext.MigrateAndConfigure(
|
||||||
|
scope.ServiceProvider.GetRequiredService<ClaudeDoDbContext>());
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var seeder = app.Services.GetRequiredService<DefaultAgentSeeder>();
|
||||||
|
var seedResult = await seeder.SeedMissingAsync();
|
||||||
|
app.Logger.LogInformation(
|
||||||
|
"Default agents seeded: {Copied} copied, {Skipped} already present",
|
||||||
|
seedResult.Copied, seedResult.Skipped);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
app.Logger.LogWarning(ex, "Default agent seeding failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
app.MapHub<WorkerHub>("/hub");
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Build worker to verify compile**
|
||||||
|
|
||||||
|
Run: `dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj`
|
||||||
|
Expected: build succeeds.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/ClaudeDo.Worker/Program.cs
|
||||||
|
git commit -m "feat(worker): seed default agents on startup"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 6: Add RestoreDefaultAgents hub method (TDD)
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `tests/ClaudeDo.Worker.Tests/Hub/AgentSettingsHubTests.cs`
|
||||||
|
- Modify: `src/ClaudeDo.Worker/Hub/WorkerHub.cs`
|
||||||
|
|
||||||
|
The existing `AgentSettingsHubTests` doesn't exercise `WorkerHub` directly (it tests the repository). We'll add a new test file that tests the seeder restore flow end-to-end without SignalR plumbing — constructing the seeder with temp dirs and asserting the `SeedResult` round-trip. This mirrors how the file is structured today and keeps tests simple.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add a restore test**
|
||||||
|
|
||||||
|
Add to `tests/ClaudeDo.Worker.Tests/Hub/AgentSettingsHubTests.cs`. Inside the existing `AgentSettingsHubTests` class (before the closing brace), add:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[Fact]
|
||||||
|
public async Task RestoreDefaultAgents_CopiesMissingBundledFiles()
|
||||||
|
{
|
||||||
|
var root = Path.Combine(Path.GetTempPath(), $"claudedo_hub_restore_{Guid.NewGuid():N}");
|
||||||
|
var bundleDir = Path.Combine(root, "bundle");
|
||||||
|
var targetDir = Path.Combine(root, "target");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(bundleDir);
|
||||||
|
Directory.CreateDirectory(targetDir);
|
||||||
|
await File.WriteAllTextAsync(Path.Combine(bundleDir, "code-reviewer.md"), "body");
|
||||||
|
|
||||||
|
var seeder = new ClaudeDo.Worker.Services.DefaultAgentSeeder(bundleDir, targetDir);
|
||||||
|
var result = await seeder.SeedMissingAsync();
|
||||||
|
|
||||||
|
Assert.Equal(1, result.Copied);
|
||||||
|
Assert.Equal(0, result.Skipped);
|
||||||
|
Assert.True(File.Exists(Path.Combine(targetDir, "code-reviewer.md")));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
try { Directory.Delete(root, true); } catch { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the test to confirm it passes (seeder already exists)**
|
||||||
|
|
||||||
|
Run: `dotnet test tests/ClaudeDo.Worker.Tests --filter "FullyQualifiedName~AgentSettingsHubTests.RestoreDefaultAgents_CopiesMissingBundledFiles"`
|
||||||
|
Expected: 1 test passes.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Add RestoreDefaultAgents to WorkerHub**
|
||||||
|
|
||||||
|
Edit `src/ClaudeDo.Worker/Hub/WorkerHub.cs`.
|
||||||
|
|
||||||
|
Add a new DTO record near the top with the other DTOs (after the `ListConfigDto` line on line 30):
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public record SeedResultDto(int Copied, int Skipped);
|
||||||
|
```
|
||||||
|
|
||||||
|
Update the class field block. Find:
|
||||||
|
```csharp
|
||||||
|
private readonly QueueService _queue;
|
||||||
|
private readonly AgentFileService _agentService;
|
||||||
|
private readonly HubBroadcaster _broadcaster;
|
||||||
|
private readonly IDbContextFactory<ClaudeDoDbContext> _dbFactory;
|
||||||
|
private readonly WorktreeMaintenanceService _wtMaintenance;
|
||||||
|
private readonly TaskResetService _resetService;
|
||||||
|
private readonly TaskMergeService _mergeService;
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace with:
|
||||||
|
```csharp
|
||||||
|
private readonly QueueService _queue;
|
||||||
|
private readonly AgentFileService _agentService;
|
||||||
|
private readonly DefaultAgentSeeder _seeder;
|
||||||
|
private readonly HubBroadcaster _broadcaster;
|
||||||
|
private readonly IDbContextFactory<ClaudeDoDbContext> _dbFactory;
|
||||||
|
private readonly WorktreeMaintenanceService _wtMaintenance;
|
||||||
|
private readonly TaskResetService _resetService;
|
||||||
|
private readonly TaskMergeService _mergeService;
|
||||||
|
```
|
||||||
|
|
||||||
|
Update the constructor. Find:
|
||||||
|
```csharp
|
||||||
|
public WorkerHub(
|
||||||
|
QueueService queue,
|
||||||
|
AgentFileService agentService,
|
||||||
|
HubBroadcaster broadcaster,
|
||||||
|
IDbContextFactory<ClaudeDoDbContext> dbFactory,
|
||||||
|
WorktreeMaintenanceService wtMaintenance,
|
||||||
|
TaskResetService resetService,
|
||||||
|
TaskMergeService mergeService)
|
||||||
|
{
|
||||||
|
_queue = queue;
|
||||||
|
_agentService = agentService;
|
||||||
|
_broadcaster = broadcaster;
|
||||||
|
_dbFactory = dbFactory;
|
||||||
|
_wtMaintenance = wtMaintenance;
|
||||||
|
_resetService = resetService;
|
||||||
|
_mergeService = mergeService;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace with:
|
||||||
|
```csharp
|
||||||
|
public WorkerHub(
|
||||||
|
QueueService queue,
|
||||||
|
AgentFileService agentService,
|
||||||
|
DefaultAgentSeeder seeder,
|
||||||
|
HubBroadcaster broadcaster,
|
||||||
|
IDbContextFactory<ClaudeDoDbContext> dbFactory,
|
||||||
|
WorktreeMaintenanceService wtMaintenance,
|
||||||
|
TaskResetService resetService,
|
||||||
|
TaskMergeService mergeService)
|
||||||
|
{
|
||||||
|
_queue = queue;
|
||||||
|
_agentService = agentService;
|
||||||
|
_seeder = seeder;
|
||||||
|
_broadcaster = broadcaster;
|
||||||
|
_dbFactory = dbFactory;
|
||||||
|
_wtMaintenance = wtMaintenance;
|
||||||
|
_resetService = resetService;
|
||||||
|
_mergeService = mergeService;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Then add the hub method. After the existing `RefreshAgents` method (currently line 126):
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public async Task<SeedResultDto> RestoreDefaultAgents()
|
||||||
|
{
|
||||||
|
var result = await _seeder.SeedMissingAsync();
|
||||||
|
return new SeedResultDto(result.Copied, result.Skipped);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Build worker to verify compile**
|
||||||
|
|
||||||
|
Run: `dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj`
|
||||||
|
Expected: build succeeds.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Run all Worker tests to confirm no regressions**
|
||||||
|
|
||||||
|
Run: `dotnet test tests/ClaudeDo.Worker.Tests`
|
||||||
|
Expected: all tests pass.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/ClaudeDo.Worker/Hub/WorkerHub.cs tests/ClaudeDo.Worker.Tests/Hub/AgentSettingsHubTests.cs
|
||||||
|
git commit -m "feat(worker): expose RestoreDefaultAgents hub method"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 7: Add RestoreDefaultAgentsAsync to WorkerClient
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/ClaudeDo.Ui/Services/WorkerClient.cs`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add the DTO record**
|
||||||
|
|
||||||
|
At the bottom of `src/ClaudeDo.Ui/Services/WorkerClient.cs`, alongside the other public record declarations (after `ListConfigDto`, currently line 350), add:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public sealed record SeedResultDto(int Copied, int Skipped);
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Add the client method**
|
||||||
|
|
||||||
|
In the `WorkerClient` class, after the `RefreshAgentsAsync` method (currently line 232), add:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public async Task<SeedResultDto?> RestoreDefaultAgentsAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await _hub.InvokeAsync<SeedResultDto>("RestoreDefaultAgents");
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Build UI to verify compile**
|
||||||
|
|
||||||
|
Run: `dotnet build src/ClaudeDo.Ui/ClaudeDo.Ui.csproj`
|
||||||
|
Expected: build succeeds.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/ClaudeDo.Ui/Services/WorkerClient.cs
|
||||||
|
git commit -m "feat(ui): add RestoreDefaultAgentsAsync to WorkerClient"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 8: Add restore button to Settings modal
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/ClaudeDo.Ui/ViewModels/Modals/SettingsModalViewModel.cs`
|
||||||
|
- Modify: `src/ClaudeDo.Ui/Views/Modals/SettingsModalView.axaml`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add the command to the viewmodel**
|
||||||
|
|
||||||
|
In `src/ClaudeDo.Ui/ViewModels/Modals/SettingsModalViewModel.cs`, add a new command method. Place it after the existing `ConfirmResetAll` method (currently ending line 162), before the `OpenPath` method:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[RelayCommand]
|
||||||
|
private async Task RestoreDefaultAgents()
|
||||||
|
{
|
||||||
|
IsBusy = true;
|
||||||
|
StatusMessage = "";
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await _worker.RestoreDefaultAgentsAsync();
|
||||||
|
if (result is null)
|
||||||
|
StatusMessage = "Worker offline.";
|
||||||
|
else if (result.Copied == 0 && result.Skipped == 0)
|
||||||
|
StatusMessage = "No default agents bundled.";
|
||||||
|
else if (result.Copied == 0)
|
||||||
|
StatusMessage = "All default agents already present.";
|
||||||
|
else
|
||||||
|
StatusMessage = $"Restored {result.Copied} default agent(s).";
|
||||||
|
|
||||||
|
await _worker.RefreshAgentsAsync();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
StatusMessage = $"Restore failed: {ex.Message}";
|
||||||
|
}
|
||||||
|
finally { IsBusy = false; }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Add a button to the settings view**
|
||||||
|
|
||||||
|
Edit `src/ClaudeDo.Ui/Views/Modals/SettingsModalView.axaml`. Add a new section after the `WORKTREES` StackPanel block (which ends with `</StackPanel>` around line 185) and before the `ABOUT` section (`<!-- ABOUT -->` around line 187).
|
||||||
|
|
||||||
|
Find:
|
||||||
|
```xml
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- ABOUT -->
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace with:
|
||||||
|
```xml
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- AGENTS -->
|
||||||
|
<StackPanel Spacing="0">
|
||||||
|
<TextBlock Classes="section-label" Text="AGENTS"/>
|
||||||
|
<Border Classes="section">
|
||||||
|
<StackPanel Spacing="8">
|
||||||
|
<TextBlock Text="Restore bundled default agents (code-reviewer, test-writer, debugger, security-reviewer, explorer, researcher). Existing files are not overwritten."
|
||||||
|
FontSize="11"
|
||||||
|
TextWrapping="Wrap"
|
||||||
|
Foreground="{DynamicResource TextDimBrush}"/>
|
||||||
|
<Button Content="Restore default agents"
|
||||||
|
Command="{Binding RestoreDefaultAgentsCommand}"
|
||||||
|
IsEnabled="{Binding !IsBusy}"
|
||||||
|
HorizontalAlignment="Left"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- ABOUT -->
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Build UI to verify compile**
|
||||||
|
|
||||||
|
Run: `dotnet build src/ClaudeDo.Ui/ClaudeDo.Ui.csproj`
|
||||||
|
Expected: build succeeds.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Manual smoke test**
|
||||||
|
|
||||||
|
Start the worker and the UI. Open Settings (3-dots next to username). The AGENTS section should appear with a "Restore default agents" button.
|
||||||
|
|
||||||
|
1. With `~/.todo-app/agents/` empty (delete any existing `.md` files first, back them up if needed): click the button. Status should read "Restored N default agent(s)." The files should appear in the folder.
|
||||||
|
2. Click again. Status should read "All default agents already present."
|
||||||
|
3. Modify one of the restored files. Click restore. The modified file content should be preserved.
|
||||||
|
|
||||||
|
If any step fails, stop and fix before committing.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/ClaudeDo.Ui/ViewModels/Modals/SettingsModalViewModel.cs src/ClaudeDo.Ui/Views/Modals/SettingsModalView.axaml
|
||||||
|
git commit -m "feat(ui): add Restore default agents button to Settings modal"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
At the end, run the full test suite and build all projects:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj
|
||||||
|
dotnet build src/ClaudeDo.Ui/ClaudeDo.Ui.csproj
|
||||||
|
dotnet build src/ClaudeDo.Data/ClaudeDo.Data.csproj
|
||||||
|
dotnet test tests/ClaudeDo.Worker.Tests
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: all builds succeed, all tests pass.
|
||||||
|
|
||||||
|
Additionally, start the Worker once with an empty `~/.todo-app/agents/` folder and confirm the log line:
|
||||||
|
> `Default agents seeded: 6 copied, 0 already present`
|
||||||
|
|
||||||
|
Then confirm `~/.todo-app/agents/` contains all 6 markdown files.
|
||||||
177
docs/superpowers/specs/2026-04-23-default-agents-design.md
Normal file
177
docs/superpowers/specs/2026-04-23-default-agents-design.md
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
# Default Agents — Design
|
||||||
|
|
||||||
|
**Date:** 2026-04-23
|
||||||
|
**Status:** Approved
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Ship ClaudeDo with a curated set of default agents so that users have useful agents available on first launch, without losing the file-based ownership model (user-editable, user-deletable). Provide a "Restore defaults" action to recover missing defaults on demand.
|
||||||
|
|
||||||
|
## Agents to Ship
|
||||||
|
|
||||||
|
Six markdown agents covering the common stages of task execution plus one general-purpose agent:
|
||||||
|
|
||||||
|
| File | Focus |
|
||||||
|
|---|---|
|
||||||
|
| `code-reviewer.md` | Review diff for bugs, logic errors, convention adherence. Flags only high-confidence issues. |
|
||||||
|
| `test-writer.md` | Generate unit/integration tests for changed code. Follows existing test patterns. |
|
||||||
|
| `debugger.md` | Systematic root-cause analysis — reproduce, isolate, hypothesize, verify. |
|
||||||
|
| `security-reviewer.md` | OWASP-style audit focused on auth, SQL injection, input handling, secret exposure. |
|
||||||
|
| `explorer.md` | Fast codebase navigation and answering "where/how" questions. Terse output. |
|
||||||
|
| `researcher.md` | General-purpose research, doc summarization, analysis, investigation. Non-code. |
|
||||||
|
|
||||||
|
Each file uses Claude Code's standard agent frontmatter:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
---
|
||||||
|
name: <agent name>
|
||||||
|
description: <one-line description>
|
||||||
|
---
|
||||||
|
|
||||||
|
<system prompt body>
|
||||||
|
```
|
||||||
|
|
||||||
|
Content target: ~20–40 lines per file. Style matches the existing Claude Code agent conventions.
|
||||||
|
|
||||||
|
## Behavior
|
||||||
|
|
||||||
|
**Seed on first launch, restore on demand:**
|
||||||
|
|
||||||
|
1. Bundled agents live alongside the Worker binary at `<AppContext.BaseDirectory>/DefaultAgents/*.md`.
|
||||||
|
2. At Worker startup, for each bundled file: if `~/.todo-app/agents/<name>.md` does NOT exist, copy it in. If it exists, leave it alone — the user owns their copy.
|
||||||
|
3. A "Restore default agents" button in the settings modal re-runs the same check, restoring any that the user has deleted.
|
||||||
|
|
||||||
|
The seed path and the restore path are the same code — only the invocation differs (startup vs. hub call).
|
||||||
|
|
||||||
|
## Components
|
||||||
|
|
||||||
|
### `DefaultAgents/*.md` (bundled content)
|
||||||
|
|
||||||
|
Location: `src/ClaudeDo.Worker/DefaultAgents/`
|
||||||
|
Packaging: `<Content Include="DefaultAgents\*.md" CopyToOutputDirectory="PreserveNewest" />` in `ClaudeDo.Worker.csproj`.
|
||||||
|
At runtime they land at `<AppContext.BaseDirectory>/DefaultAgents/*.md` next to the executable.
|
||||||
|
|
||||||
|
### `DefaultAgentSeeder` (new service)
|
||||||
|
|
||||||
|
Location: `src/ClaudeDo.Worker/Services/DefaultAgentSeeder.cs`
|
||||||
|
|
||||||
|
```
|
||||||
|
public sealed class DefaultAgentSeeder
|
||||||
|
{
|
||||||
|
public DefaultAgentSeeder(string bundleDir, string targetDir);
|
||||||
|
public Task<SeedResult> SeedMissingAsync(CancellationToken ct = default);
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record SeedResult(int Copied, int Skipped);
|
||||||
|
```
|
||||||
|
|
||||||
|
Behavior of `SeedMissingAsync`:
|
||||||
|
- If `bundleDir` doesn't exist, log warning and return `(0, 0)`.
|
||||||
|
- Enumerate `*.md` in `bundleDir`.
|
||||||
|
- For each file, if the target path (`targetDir/<filename>`) is missing, copy it; else increment `Skipped`.
|
||||||
|
- Create `targetDir` if missing (consistent with existing `AgentFileService.WriteAsync`).
|
||||||
|
- Per-file exceptions are caught and logged; the seeder continues with the next file. The method itself does not throw for individual file failures.
|
||||||
|
|
||||||
|
### `Program.cs` wiring
|
||||||
|
|
||||||
|
After `AgentFileService` registration and before `app.Run()`:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var bundleDir = Path.Combine(AppContext.BaseDirectory, "DefaultAgents");
|
||||||
|
var seeder = new DefaultAgentSeeder(bundleDir, agentsDir);
|
||||||
|
await seeder.SeedMissingAsync();
|
||||||
|
builder.Services.AddSingleton(seeder);
|
||||||
|
```
|
||||||
|
|
||||||
|
The seeder is also registered as a singleton so the hub can invoke it for the restore flow.
|
||||||
|
|
||||||
|
### `WorkerHub.RestoreDefaultAgents`
|
||||||
|
|
||||||
|
Location: add method to existing `src/ClaudeDo.Worker/Hub/WorkerHub.cs`.
|
||||||
|
|
||||||
|
Signature: `public async Task<SeedResult> RestoreDefaultAgents()`
|
||||||
|
|
||||||
|
Behavior:
|
||||||
|
- Calls `DefaultAgentSeeder.SeedMissingAsync()`.
|
||||||
|
- Returns the `SeedResult` to the caller.
|
||||||
|
- No separate broadcast — the UI will call `GetAgents` after the restore returns, reusing the existing refresh path.
|
||||||
|
|
||||||
|
### UI — Settings Modal
|
||||||
|
|
||||||
|
Location: `src/ClaudeDo.Ui/Views/Modals/SettingsModalView.axaml` + `SettingsModalViewModel`.
|
||||||
|
|
||||||
|
Add a "Restore default agents" button to the modal. On click:
|
||||||
|
1. Disable the button, show a spinner label.
|
||||||
|
2. Call `WorkerClient.RestoreDefaultAgentsAsync()`.
|
||||||
|
3. Show a brief inline confirmation: `"Restored {Copied} agent(s)"` or `"All defaults already present"`.
|
||||||
|
4. Trigger the existing agent list refresh so the new files appear immediately in the rest of the UI.
|
||||||
|
|
||||||
|
### `WorkerClient` method
|
||||||
|
|
||||||
|
Add `Task<SeedResult> RestoreDefaultAgentsAsync(CancellationToken ct = default)` to `WorkerClient` — thin wrapper that invokes the hub method.
|
||||||
|
|
||||||
|
## Data Flow
|
||||||
|
|
||||||
|
**Startup:**
|
||||||
|
```
|
||||||
|
Worker starts → DefaultAgentSeeder.SeedMissingAsync()
|
||||||
|
→ copies missing files into ~/.todo-app/agents/
|
||||||
|
AgentFileService.ScanAsync() (on first GetAgents call) → sees the seeded files
|
||||||
|
```
|
||||||
|
|
||||||
|
**User restores:**
|
||||||
|
```
|
||||||
|
Settings modal button click
|
||||||
|
→ WorkerClient.RestoreDefaultAgentsAsync()
|
||||||
|
→ WorkerHub.RestoreDefaultAgents()
|
||||||
|
→ DefaultAgentSeeder.SeedMissingAsync()
|
||||||
|
→ returns SeedResult(copied, skipped)
|
||||||
|
UI shows confirmation, triggers GetAgents refresh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
| Failure | Behavior |
|
||||||
|
|---|---|
|
||||||
|
| Missing `DefaultAgents/` bundle dir | Log warning, return `(0, 0)`. Startup proceeds. |
|
||||||
|
| Individual file copy failure (disk, permissions) | Catch per-file, log, continue with the remaining files. |
|
||||||
|
| Corrupt bundled markdown (no valid frontmatter) | Copied anyway — the `AgentFileService` frontmatter parser already falls back to filename-as-name. |
|
||||||
|
| Startup seeder exception (unexpected) | Log as warning, do not crash the Worker. Agents can still be restored via the button. |
|
||||||
|
| Hub `RestoreDefaultAgents` exception | Propagate to client as SignalR error; UI shows a generic "Restore failed" message. |
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
**Unit:** `tests/ClaudeDo.Worker.Tests/Services/DefaultAgentSeederTests.cs`
|
||||||
|
|
||||||
|
- Seeds all files when target dir is empty.
|
||||||
|
- Skips files that already exist.
|
||||||
|
- Preserves existing user-modified files (file mtime / content unchanged).
|
||||||
|
- Returns accurate `SeedResult` counts.
|
||||||
|
- Handles missing bundle dir gracefully (returns `(0, 0)`, no throw).
|
||||||
|
- Creates target dir if it doesn't exist.
|
||||||
|
|
||||||
|
**Integration:** extend `tests/ClaudeDo.Worker.Tests/Hub/AgentSettingsHubTests.cs`
|
||||||
|
|
||||||
|
- `RestoreDefaultAgents` invokes the seeder and returns the count.
|
||||||
|
|
||||||
|
**No UI tests.** The project has no UI test harness; settings modal behavior is exercised manually.
|
||||||
|
|
||||||
|
## Build / Packaging
|
||||||
|
|
||||||
|
`src/ClaudeDo.Worker/ClaudeDo.Worker.csproj` gains:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<ItemGroup>
|
||||||
|
<Content Include="DefaultAgents\*.md">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</Content>
|
||||||
|
</ItemGroup>
|
||||||
|
```
|
||||||
|
|
||||||
|
No new NuGet dependencies.
|
||||||
|
|
||||||
|
## Out of Scope
|
||||||
|
|
||||||
|
- Editing bundled agents in-place (user edits their copy under `~/.todo-app/agents/`; bundle is read-only by convention).
|
||||||
|
- Versioning / updating user copies when the bundled version changes. If a bundled agent is improved in a later release, the user's copy is not overwritten. A future release may add a "diff / reset to bundled" flow, but not now.
|
||||||
|
- Packaging as embedded resources. Content files copied to output are simpler, inspectable on disk, and consistent with the file-based agent model.
|
||||||
Reference in New Issue
Block a user