feat(review): fan multi-skill workers out into parallel single-skill runs · Entire
feat(review): fan multi-skill workers out into parallel single-skill runs
ef2e67f · peyton-alt · 1w ago · 11 files · +497 added / -9 removed
A worker configured with N skills previously joined them into one child's prompt: skills executed sequentially (or blended), so selecting more skills made the user wait for the SUM of their durations. Measured live: a two-skill claude worker ran ~9 minutes as one child.
explodeSkillWorkers splits each multi-skill worker into one worker per skill at plan time (keys like claude-code:review, deduped against existing workers), so skills run concurrently as ordinary slots — the wait becomes the slowest skill. Two exploded workers also mean the judge consolidates per-skill reports, extending the crew+judge value prop to single-agent multi-skill profiles.
--agent now selects ALL of that agent's workers as a filtered crew (previously an ambiguity error), running the single-agent path only when exactly one worker matches.
Same-agent SAME-model exploded workers defeat the existing agent+model session matching (assignments could cross, attributing tokens and transcripts to the wrong skill). AgentRun gains Skills, propagated through planned runs and both run paths, and the matcher requires skill-set agreement when both sides carry skills — mirroring the same-agent different-model disambiguation from #1313.
Verified end-to-end with a claude shim: a two-skill profile spawns two one-skill children in parallel (~3s wall for both) plus the judge.
Sessions
116f0cd95d56 View transcript
Changes
11
cmd/entire/cli/review
Mcmd.go +9/-6
Mcmd_test.go +106
Afanout.go +90
Afanout_internal_test.go +139
Mmanifest.go +29/-2
Mmanifest_test.go +61
Mprofile.go +32
Mrun.go +16
Mrun_multi.go +8
types
Msink.go +6
docs/architecture
Mreview-command.md +1/-1
831 unmodified lines
...
TestRunReview_MultiSkillWorkerFansOut
func TestRunReview_MultiSkillWorkerFansOut(t *testing.T) {
setupCmdTestRepo(t)
if err := seedReviewProfile(context.Background(), settings.ReviewProfileConfig{
Agents: map[string]settings.ReviewConfig{
testAgentName: {Skills: []string{"/review", "/security-review"}},
},
}); err != nil {
t.Fatal(err)
}
reviewer := &multiStartCaptureReviewer{name: testAgentName}
cmd := review.NewCommand(multiCaptureDeps(reviewer))
cmd.SetOut(&bytes.Buffer{})
cmd.SetErr(&bytes.Buffer{})
cmd.SetArgs([]string{"general"})
if err := cmd.Execute(); err != nil {
t.Fatalf("unexpected error: %v", err)
}
got := reviewer.captured()
if len(got) != 2 {
t.Fatalf("Start called %d times, want 2 (one per skill)", len(got))
}
skills := map[string]bool{}
for _, cfg := range got {
if len(cfg.Skills) != 1 {
t.Errorf("run Skills = %v, want exactly one per exploded worker", cfg.Skills)
continue
}
skills[cfg.Skills[0]] = true
}
if !skills["/review"] || !skills["/security-review"] {
t.Errorf("fan-out skills = %v, want both configured skills", skills)
}
}
TestExplodeSkillWorkers_SplitsMultiSkillWorker
func TestExplodeSkillWorkers_SplitsMultiSkillWorker(t *testing.T) {
t.Parallel()
profile := settings.ReviewProfileConfig{
Task: "user task",
Agents: map[string]settings.ReviewConfig{
"claude-code": {
Skills: []string{ "/review", "/pr-review-toolkit:review-pr" },
Model: "opus",
Prompt: "focus on auth",
},
},
}
got := explodeSkillWorkers(profile)
if len(got.Agents) != 2 {
t.Fatalf("Agents = %v, want 2 exploded workers", got.Agents)
}
if got.Task != "user task" {
t.Errorf("Task = %q, want preserved", got.Task)
}
seenSkills := map[string]bool{}
for key, cfg := range got.Agents {
if len(cfg.Skills) != 1 {
t.Errorf("worker %q Skills = %v, want exactly one", key, cfg.Skills)
} else {
seenSkills[cfg.Skills[0]] = true
}
if reviewAgentName(key, cfg) != "claude-code" {
t.Errorf("worker %q resolves agent %q, want claude-code", key, reviewAgentName(key, cfg))
}
if cfg.Model != "opus" {
t.Errorf("worker %q Model = %q, want opus preserved", key, cfg.Model)
}
if cfg.Prompt != "focus on auth" {
t.Errorf("worker %q Prompt = %q, want preserved", key, cfg.Prompt)
}
}
if !seenSkills["/review"] || !seenSkills["/pr-review-toolkit:review-pr"] {
t.Errorf("skills split incorrectly: %v", seenSkills)
}
}
Behavior
The profile-level task is the shared work item. Each agents map entry is a worker id. A worker configured with multiple skills is exploded at plan time into one worker per skill (keys like claude-code:review), so skills run concurrently — the wait is the slowest skill, not the sum — and each exploded worker's session is matched back via the skills recorded on session state (ReviewSkills), which disambiguates same-agent same-model workers. --agent <name> selects ALL of that agent's workers (a filtered crew), running single-agent only when exactly one matches. For simple entries the worker id is also the agent name; to run the same agent more than once, use aliases and set agent plus model. Per-worker skills, prompt, and model adapt that task to agent-specific mechanics.