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

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.