opencode-swarm-plugin 0.30.4 → 0.30.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,63 @@
1
1
  # opencode-swarm-plugin
2
2
 
3
+ ## 0.30.6
4
+
5
+ ### Patch Changes
6
+
7
+ - [`32a2885`](https://github.com/joelhooks/swarm-tools/commit/32a2885115cc3e574e86d8e492f60ee189627488) Thanks [@joelhooks](https://github.com/joelhooks)! - chore: verify CI publish flow works
8
+
9
+ ## 0.30.5
10
+
11
+ ### Patch Changes
12
+
13
+ - [`08e61ab`](https://github.com/joelhooks/swarm-tools/commit/08e61abd96ced0443a5ac5dca0e8f362ed869075) Thanks [@joelhooks](https://github.com/joelhooks)! - ## 🐝 Workers Now Choose Their Own Model
14
+
15
+ Added intelligent model selection for swarm workers based on task characteristics.
16
+
17
+ **What changed:**
18
+
19
+ - `swarm setup` now asks for a "lite model" preference (docs/tests/simple edits)
20
+ - New `selectWorkerModel()` function auto-selects based on file types
21
+ - `swarm_spawn_subtask` includes `recommended_model` in metadata
22
+ - `DecomposedSubtask` schema supports optional explicit `model` field
23
+
24
+ **Model selection priority:**
25
+
26
+ 1. Explicit `model` field in subtask (if specified)
27
+ 2. File-type inference:
28
+ - All `.md`/`.mdx` files → lite model
29
+ - All `.test.`/`.spec.` files → lite model
30
+ 3. Mixed or implementation files → primary model
31
+
32
+ **Why it matters:**
33
+
34
+ - Cost savings: docs and tests don't need expensive models
35
+ - Faster execution: lite models are snappier for simple tasks
36
+ - Better defaults: right-sized models for each subtask type
37
+ - Still flexible: coordinators can override per-subtask
38
+
39
+ **Backward compatible:**
40
+
41
+ - Existing workflows continue to work
42
+ - Model selection is transparent to agents
43
+ - Defaults to primary model if lite model not configured
44
+
45
+ **Example:**
46
+
47
+ ```typescript
48
+ // Subtask with all markdown files
49
+ { files: ["README.md", "docs/guide.mdx"] }
50
+ // → selects lite model (haiku)
51
+
52
+ // Subtask with mixed files
53
+ { files: ["src/auth.ts", "README.md"] }
54
+ // → selects primary model (sonnet)
55
+
56
+ // Explicit override
57
+ { files: ["complex-refactor.ts"], model: "anthropic/claude-opus-4-5" }
58
+ // → uses opus as specified
59
+ ```
60
+
3
61
  ## 0.30.4
4
62
 
5
63
  ### Patch Changes
package/bin/swarm.ts CHANGED
@@ -1889,9 +1889,43 @@ async function setup() {
1889
1889
  process.exit(0);
1890
1890
  }
1891
1891
 
1892
+ // Lite model selection for simple tasks (docs, tests)
1893
+ const liteModel = await p.select({
1894
+ message: "Select lite model (for docs, tests, simple edits):",
1895
+ options: [
1896
+ {
1897
+ value: "anthropic/claude-haiku-4-5",
1898
+ label: "Claude Haiku 4.5",
1899
+ hint: "Fast and cost-effective (recommended)",
1900
+ },
1901
+ {
1902
+ value: "anthropic/claude-sonnet-4-5",
1903
+ label: "Claude Sonnet 4.5",
1904
+ hint: "More capable, slower",
1905
+ },
1906
+ {
1907
+ value: "openai/gpt-4o-mini",
1908
+ label: "GPT-4o Mini",
1909
+ hint: "Fast and cheap",
1910
+ },
1911
+ {
1912
+ value: "google/gemini-2.0-flash",
1913
+ label: "Gemini 2.0 Flash",
1914
+ hint: "Fast and capable",
1915
+ },
1916
+ ],
1917
+ initialValue: "anthropic/claude-haiku-4-5",
1918
+ });
1919
+
1920
+ if (p.isCancel(liteModel)) {
1921
+ p.cancel("Setup cancelled");
1922
+ process.exit(0);
1923
+ }
1924
+
1892
1925
  p.log.success("Selected models:");
1893
1926
  p.log.message(dim(` Coordinator: ${coordinatorModel}`));
1894
1927
  p.log.message(dim(` Worker: ${workerModel}`));
1928
+ p.log.message(dim(` Lite: ${liteModel}`));
1895
1929
 
1896
1930
  p.log.step("Setting up OpenCode integration...");
1897
1931
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-swarm-plugin",
3
- "version": "0.30.4",
3
+ "version": "0.30.6",
4
4
  "description": "Multi-agent swarm coordination for OpenCode with learning capabilities, beads integration, and Agent Mail",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -0,0 +1,188 @@
1
+ /**
2
+ * Model Selection Tests
3
+ *
4
+ * Tests for selectWorkerModel function that determines which model
5
+ * a worker should use based on subtask characteristics.
6
+ */
7
+ import { describe, test, expect } from "bun:test";
8
+ import { selectWorkerModel } from "./model-selection";
9
+ import type { DecomposedSubtask } from "./schemas/task";
10
+
11
+ // Mock config type matching expected SwarmConfig structure
12
+ interface TestConfig {
13
+ primaryModel: string;
14
+ liteModel?: string;
15
+ }
16
+
17
+ describe("selectWorkerModel", () => {
18
+ const mockConfig: TestConfig = {
19
+ primaryModel: "anthropic/claude-sonnet-4-5",
20
+ liteModel: "anthropic/claude-haiku-4-5",
21
+ };
22
+
23
+ test("uses explicit model field from subtask when provided", () => {
24
+ const subtask: DecomposedSubtask & { model?: string } = {
25
+ title: "Update docs",
26
+ description: "Update README",
27
+ files: ["README.md"],
28
+ estimated_effort: "trivial",
29
+ model: "anthropic/claude-opus-4-5", // Explicit override
30
+ };
31
+
32
+ const result = selectWorkerModel(subtask, mockConfig);
33
+ expect(result).toBe("anthropic/claude-opus-4-5");
34
+ });
35
+
36
+ test("uses liteModel for all markdown files", () => {
37
+ const subtask: DecomposedSubtask = {
38
+ title: "Update docs",
39
+ description: "Update all docs",
40
+ files: ["README.md", "CONTRIBUTING.md"],
41
+ estimated_effort: "small",
42
+ };
43
+
44
+ const result = selectWorkerModel(subtask, mockConfig);
45
+ expect(result).toBe("anthropic/claude-haiku-4-5");
46
+ });
47
+
48
+ test("uses liteModel for all MDX files", () => {
49
+ const subtask: DecomposedSubtask = {
50
+ title: "Update docs",
51
+ description: "Update content",
52
+ files: ["docs/intro.mdx", "docs/guide.mdx"],
53
+ estimated_effort: "small",
54
+ };
55
+
56
+ const result = selectWorkerModel(subtask, mockConfig);
57
+ expect(result).toBe("anthropic/claude-haiku-4-5");
58
+ });
59
+
60
+ test("uses liteModel for test files with .test. pattern", () => {
61
+ const subtask: DecomposedSubtask = {
62
+ title: "Write tests",
63
+ description: "Add unit tests",
64
+ files: ["src/auth.test.ts", "src/user.test.ts"],
65
+ estimated_effort: "small",
66
+ };
67
+
68
+ const result = selectWorkerModel(subtask, mockConfig);
69
+ expect(result).toBe("anthropic/claude-haiku-4-5");
70
+ });
71
+
72
+ test("uses liteModel for test files with .spec. pattern", () => {
73
+ const subtask: DecomposedSubtask = {
74
+ title: "Write specs",
75
+ description: "Add spec tests",
76
+ files: ["src/auth.spec.ts", "src/user.spec.ts"],
77
+ estimated_effort: "small",
78
+ };
79
+
80
+ const result = selectWorkerModel(subtask, mockConfig);
81
+ expect(result).toBe("anthropic/claude-haiku-4-5");
82
+ });
83
+
84
+ test("uses primaryModel when files are mixed (code + docs)", () => {
85
+ const subtask: DecomposedSubtask = {
86
+ title: "Implement feature with docs",
87
+ description: "Add feature and document it",
88
+ files: ["src/feature.ts", "README.md"],
89
+ estimated_effort: "medium",
90
+ };
91
+
92
+ const result = selectWorkerModel(subtask, mockConfig);
93
+ expect(result).toBe("anthropic/claude-sonnet-4-5");
94
+ });
95
+
96
+ test("uses primaryModel when files are mixed (code + tests)", () => {
97
+ const subtask: DecomposedSubtask = {
98
+ title: "Implement feature with tests",
99
+ description: "Add feature and tests",
100
+ files: ["src/feature.ts", "src/feature.test.ts"],
101
+ estimated_effort: "medium",
102
+ };
103
+
104
+ const result = selectWorkerModel(subtask, mockConfig);
105
+ expect(result).toBe("anthropic/claude-sonnet-4-5");
106
+ });
107
+
108
+ test("uses primaryModel for implementation files", () => {
109
+ const subtask: DecomposedSubtask = {
110
+ title: "Implement auth",
111
+ description: "Add authentication",
112
+ files: ["src/auth.ts", "src/middleware.ts"],
113
+ estimated_effort: "large",
114
+ };
115
+
116
+ const result = selectWorkerModel(subtask, mockConfig);
117
+ expect(result).toBe("anthropic/claude-sonnet-4-5");
118
+ });
119
+
120
+ test("defaults to primaryModel when liteModel not configured", () => {
121
+ const configWithoutLite: TestConfig = {
122
+ primaryModel: "anthropic/claude-sonnet-4-5",
123
+ // liteModel is undefined
124
+ };
125
+
126
+ const subtask: DecomposedSubtask = {
127
+ title: "Update docs",
128
+ description: "Update README",
129
+ files: ["README.md"],
130
+ estimated_effort: "trivial",
131
+ };
132
+
133
+ const result = selectWorkerModel(subtask, configWithoutLite);
134
+ expect(result).toBe("anthropic/claude-sonnet-4-5");
135
+ });
136
+
137
+ test("falls back to claude-haiku when liteModel not configured but primaryModel missing", () => {
138
+ const emptyConfig: TestConfig = {
139
+ primaryModel: "",
140
+ };
141
+
142
+ const subtask: DecomposedSubtask = {
143
+ title: "Update docs",
144
+ description: "Update README",
145
+ files: ["README.md"],
146
+ estimated_effort: "trivial",
147
+ };
148
+
149
+ const result = selectWorkerModel(subtask, emptyConfig);
150
+ expect(result).toBe("anthropic/claude-haiku-4-5");
151
+ });
152
+
153
+ test("handles empty files array by defaulting to primaryModel", () => {
154
+ const subtask: DecomposedSubtask = {
155
+ title: "Research task",
156
+ description: "Investigate options",
157
+ files: [],
158
+ estimated_effort: "small",
159
+ };
160
+
161
+ const result = selectWorkerModel(subtask, mockConfig);
162
+ expect(result).toBe("anthropic/claude-sonnet-4-5");
163
+ });
164
+
165
+ test("handles mixed markdown and mdx files", () => {
166
+ const subtask: DecomposedSubtask = {
167
+ title: "Update all docs",
168
+ description: "Update docs",
169
+ files: ["README.md", "docs/guide.mdx", "CHANGELOG.md"],
170
+ estimated_effort: "small",
171
+ };
172
+
173
+ const result = selectWorkerModel(subtask, mockConfig);
174
+ expect(result).toBe("anthropic/claude-haiku-4-5");
175
+ });
176
+
177
+ test("case insensitive file extension matching", () => {
178
+ const subtask: DecomposedSubtask = {
179
+ title: "Update docs",
180
+ description: "Update README",
181
+ files: ["README.MD", "CONTRIBUTING.MD"],
182
+ estimated_effort: "trivial",
183
+ };
184
+
185
+ const result = selectWorkerModel(subtask, mockConfig);
186
+ expect(result).toBe("anthropic/claude-haiku-4-5");
187
+ });
188
+ });
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Model Selection Module
3
+ *
4
+ * Determines which model a worker agent should use based on subtask
5
+ * characteristics like file types and complexity.
6
+ *
7
+ * Priority:
8
+ * 1. Explicit model field in subtask
9
+ * 2. File-type inference (docs/tests → lite model)
10
+ * 3. Default to primary model
11
+ */
12
+
13
+ import type { DecomposedSubtask } from "./schemas/task";
14
+
15
+ /**
16
+ * Configuration interface for swarm models
17
+ */
18
+ export interface SwarmConfig {
19
+ primaryModel: string;
20
+ liteModel?: string;
21
+ }
22
+
23
+ /**
24
+ * Select the appropriate model for a worker agent based on subtask characteristics
25
+ *
26
+ * Priority order:
27
+ * 1. Explicit `model` field in subtask (if present)
28
+ * 2. File-type inference:
29
+ * - All .md/.mdx files → liteModel
30
+ * - All .test./.spec. files → liteModel
31
+ * 3. Mixed files or implementation → primaryModel
32
+ *
33
+ * @param subtask - The subtask to evaluate
34
+ * @param config - Swarm configuration with model preferences
35
+ * @returns Model identifier string
36
+ */
37
+ export function selectWorkerModel(
38
+ subtask: DecomposedSubtask & { model?: string },
39
+ config: SwarmConfig,
40
+ ): string {
41
+ // Priority 1: Explicit model in subtask
42
+ if (subtask.model) {
43
+ return subtask.model;
44
+ }
45
+
46
+ const files = subtask.files || [];
47
+
48
+ // Priority 2: File-type inference
49
+ if (files.length > 0) {
50
+ const allDocs = files.every((f) => {
51
+ const lower = f.toLowerCase();
52
+ return lower.endsWith(".md") || lower.endsWith(".mdx");
53
+ });
54
+
55
+ const allTests = files.every((f) => {
56
+ const lower = f.toLowerCase();
57
+ return lower.includes(".test.") || lower.includes(".spec.");
58
+ });
59
+
60
+ if (allDocs || allTests) {
61
+ // Use lite model if configured, otherwise fall back to primary
62
+ return config.liteModel || config.primaryModel || "anthropic/claude-haiku-4-5";
63
+ }
64
+ }
65
+
66
+ // Priority 3: Default to primary model
67
+ return config.primaryModel || "anthropic/claude-haiku-4-5";
68
+ }
@@ -43,6 +43,11 @@ export const DecomposedSubtaskSchema = z.object({
43
43
  estimated_effort: EffortLevelSchema,
44
44
  /** Potential risks or complications (e.g., 'tight coupling', 'data migration required', 'breaking change') */
45
45
  risks: z.array(z.string()).optional().default([]),
46
+ /**
47
+ * Optional explicit model override for this subtask.
48
+ * If not specified, model will be selected based on file types.
49
+ */
50
+ model: z.string().optional(),
46
51
  });
47
52
  export type DecomposedSubtask = z.infer<typeof DecomposedSubtaskSchema>;
48
53
 
@@ -0,0 +1,173 @@
1
+ /**
2
+ * Tests for swarm-prompts.ts
3
+ *
4
+ * Validates that prompt templates contain required sections and emphasis
5
+ * for memory usage, coordination, and TDD workflow.
6
+ */
7
+
8
+ import { describe, expect, test } from "bun:test";
9
+ import {
10
+ formatSubtaskPromptV2,
11
+ SUBTASK_PROMPT_V2,
12
+ } from "./swarm-prompts";
13
+
14
+ describe("SUBTASK_PROMPT_V2", () => {
15
+ describe("memory query emphasis", () => {
16
+ test("Step 2 is semantic-memory_find and marked MANDATORY", () => {
17
+ expect(SUBTASK_PROMPT_V2).toContain("### Step 2:");
18
+ expect(SUBTASK_PROMPT_V2).toContain("semantic-memory_find");
19
+ // Must have MANDATORY in the step header
20
+ expect(SUBTASK_PROMPT_V2).toMatch(/### Step 2:.*MANDATORY/i);
21
+ });
22
+
23
+ test("memory query step has visual emphasis (emoji or caps)", () => {
24
+ // Should have emoji or CRITICAL/ALWAYS in caps
25
+ const step2Match = SUBTASK_PROMPT_V2.match(/### Step 2:[\s\S]*?### Step 3:/);
26
+ expect(step2Match).not.toBeNull();
27
+ if (!step2Match) return;
28
+ const step2Content = step2Match[0];
29
+
30
+ // Must have at least one of: emoji, CRITICAL, ALWAYS, MANDATORY
31
+ const hasEmphasis =
32
+ /🧠|⚠️|CRITICAL|ALWAYS|MANDATORY/.test(step2Content);
33
+ expect(hasEmphasis).toBe(true);
34
+ });
35
+
36
+ test("memory query step includes query examples by task type", () => {
37
+ const step2Match = SUBTASK_PROMPT_V2.match(/### Step 2:[\s\S]*?### Step 3:/);
38
+ expect(step2Match).not.toBeNull();
39
+ if (!step2Match) return;
40
+ const step2Content = step2Match[0];
41
+
42
+ // Should have examples for different task types
43
+ expect(step2Content).toContain("Bug fix");
44
+ expect(step2Content).toContain("New feature");
45
+ expect(step2Content).toContain("Refactor");
46
+ });
47
+
48
+ test("memory query step explains WHY it's mandatory", () => {
49
+ const step2Match = SUBTASK_PROMPT_V2.match(/### Step 2:[\s\S]*?### Step 3:/);
50
+ expect(step2Match).not.toBeNull();
51
+ if (!step2Match) return;
52
+ const step2Content = step2Match[0];
53
+
54
+ // Should explain consequences of skipping
55
+ expect(step2Content).toMatch(/skip|waste|repeat|already.solved/i);
56
+ });
57
+ });
58
+
59
+ describe("memory storage emphasis", () => {
60
+ test("has a dedicated section for storing learnings", () => {
61
+ // Should have a prominent section about storing memories
62
+ expect(SUBTASK_PROMPT_V2).toMatch(/##.*STORE.*LEARNING|### Step.*Store.*Learning/i);
63
+ });
64
+
65
+ test("storage section lists triggers for when to store", () => {
66
+ // Should mention triggers: bugs, gotchas, patterns, failed approaches
67
+ expect(SUBTASK_PROMPT_V2).toContain("bug");
68
+ expect(SUBTASK_PROMPT_V2).toMatch(/gotcha|quirk|workaround/i);
69
+ expect(SUBTASK_PROMPT_V2).toMatch(/pattern|domain/i);
70
+ });
71
+
72
+ test("storage section emphasizes WHY not just WHAT", () => {
73
+ expect(SUBTASK_PROMPT_V2).toMatch(/WHY.*not.*WHAT|why.*matters/i);
74
+ });
75
+
76
+ test("storage section warns against generic knowledge", () => {
77
+ expect(SUBTASK_PROMPT_V2).toMatch(/don't store.*generic|generic knowledge/i);
78
+ });
79
+ });
80
+
81
+ describe("checklist order", () => {
82
+ test("Step 1 is swarmmail_init", () => {
83
+ expect(SUBTASK_PROMPT_V2).toMatch(/### Step 1:[\s\S]*?swarmmail_init/);
84
+ });
85
+
86
+ test("Step 2 is semantic-memory_find (before skills)", () => {
87
+ const step2Pos = SUBTASK_PROMPT_V2.indexOf("### Step 2:");
88
+ const step3Pos = SUBTASK_PROMPT_V2.indexOf("### Step 3:");
89
+ const memoryFindPos = SUBTASK_PROMPT_V2.indexOf("semantic-memory_find");
90
+ const skillsPos = SUBTASK_PROMPT_V2.indexOf("skills_list");
91
+
92
+ // Memory find should be in Step 2, before skills in Step 3
93
+ expect(memoryFindPos).toBeGreaterThan(step2Pos);
94
+ expect(memoryFindPos).toBeLessThan(step3Pos);
95
+ expect(skillsPos).toBeGreaterThan(step3Pos);
96
+ });
97
+
98
+ test("semantic-memory_store comes before swarm_complete", () => {
99
+ const storePos = SUBTASK_PROMPT_V2.indexOf("semantic-memory_store");
100
+ const completePos = SUBTASK_PROMPT_V2.lastIndexOf("swarm_complete");
101
+
102
+ expect(storePos).toBeGreaterThan(0);
103
+ expect(storePos).toBeLessThan(completePos);
104
+ });
105
+
106
+ test("final step is swarm_complete (not hive_close)", () => {
107
+ // Find the last "### Step N:" pattern
108
+ const stepMatches = [...SUBTASK_PROMPT_V2.matchAll(/### Step (\d+):/g)];
109
+ expect(stepMatches.length).toBeGreaterThan(0);
110
+
111
+ const lastStepNum = Math.max(...stepMatches.map(m => parseInt(m[1])));
112
+ const lastStepMatch = SUBTASK_PROMPT_V2.match(
113
+ new RegExp(`### Step ${lastStepNum}:[\\s\\S]*?(?=## \\[|$)`)
114
+ );
115
+ expect(lastStepMatch).not.toBeNull();
116
+
117
+ const lastStepContent = lastStepMatch![0];
118
+ expect(lastStepContent).toContain("swarm_complete");
119
+ expect(lastStepContent).toMatch(/NOT.*hive_close|DO NOT.*hive_close/i);
120
+ });
121
+ });
122
+
123
+ describe("critical requirements section", () => {
124
+ test("lists memory query as non-negotiable", () => {
125
+ const criticalSection = SUBTASK_PROMPT_V2.match(/\[CRITICAL REQUIREMENTS\][\s\S]*?Begin now/);
126
+ expect(criticalSection).not.toBeNull();
127
+
128
+ expect(criticalSection![0]).toMatch(/semantic-memory_find|memory.*MUST|Step 2.*MUST/i);
129
+ });
130
+
131
+ test("lists consequences of skipping memory steps", () => {
132
+ const criticalSection = SUBTASK_PROMPT_V2.match(/\[CRITICAL REQUIREMENTS\][\s\S]*?Begin now/);
133
+ expect(criticalSection).not.toBeNull();
134
+
135
+ // Should mention consequences for skipping memory
136
+ expect(criticalSection![0]).toMatch(/repeat|waste|already.solved|mistakes/i);
137
+ });
138
+ });
139
+ });
140
+
141
+ describe("formatSubtaskPromptV2", () => {
142
+ test("substitutes all placeholders correctly", () => {
143
+ const result = formatSubtaskPromptV2({
144
+ bead_id: "test-bead-123",
145
+ epic_id: "test-epic-456",
146
+ subtask_title: "Test Subtask",
147
+ subtask_description: "Do the test thing",
148
+ files: ["src/test.ts", "src/test.test.ts"],
149
+ shared_context: "This is shared context",
150
+ project_path: "/path/to/project",
151
+ });
152
+
153
+ expect(result).toContain("test-bead-123");
154
+ expect(result).toContain("test-epic-456");
155
+ expect(result).toContain("Test Subtask");
156
+ expect(result).toContain("Do the test thing");
157
+ expect(result).toContain("src/test.ts");
158
+ expect(result).toContain("/path/to/project");
159
+ });
160
+
161
+ test("includes memory query step with MANDATORY emphasis", () => {
162
+ const result = formatSubtaskPromptV2({
163
+ bead_id: "test-bead",
164
+ epic_id: "test-epic",
165
+ subtask_title: "Test",
166
+ subtask_description: "",
167
+ files: [],
168
+ });
169
+
170
+ expect(result).toMatch(/Step 2:.*MANDATORY/i);
171
+ expect(result).toContain("semantic-memory_find");
172
+ });
173
+ });
@@ -291,18 +291,36 @@ swarmmail_init(project_path="{project_path}", task_description="{bead_id}: {subt
291
291
 
292
292
  **If you skip this step, your work will not be tracked and swarm_complete will fail.**
293
293
 
294
- ### Step 2: Query Past Learnings (BEFORE starting work)
294
+ ### Step 2: 🧠 Query Past Learnings (MANDATORY - BEFORE starting work)
295
+
296
+ **⚠️ CRITICAL: ALWAYS query semantic memory BEFORE writing ANY code.**
297
+
295
298
  \`\`\`
296
- semantic-memory_find(query="<keywords from your task>", limit=5)
299
+ semantic-memory_find(query="<keywords from your task>", limit=5, expand=true)
297
300
  \`\`\`
298
301
 
299
- **Check if past agents solved similar problems.** Search for:
300
- - Error messages if debugging
301
- - Domain concepts (e.g., "authentication", "caching")
302
- - Technology stack (e.g., "Next.js", "React")
303
- - Patterns (e.g., "event sourcing", "validation")
302
+ **Why this is MANDATORY:**
303
+ - Past agents may have already solved your exact problem
304
+ - Avoids repeating mistakes that wasted 30+ minutes before
305
+ - Discovers project-specific patterns and gotchas
306
+ - Finds known workarounds for tool/library quirks
307
+
308
+ **Search Query Examples by Task Type:**
309
+
310
+ - **Bug fix**: Use exact error message or "<symptom> <component>"
311
+ - **New feature**: Search "<domain concept> implementation pattern"
312
+ - **Refactor**: Query "<pattern name> migration approach"
313
+ - **Integration**: Look for "<library name> gotchas configuration"
314
+ - **Testing**: Find "testing <component type> characterization tests"
315
+ - **Performance**: Search "<technology> performance optimization"
316
+
317
+ **BEFORE you start coding:**
318
+ 1. Run semantic-memory_find with keywords from your task
319
+ 2. Read the results with expand=true for full content
320
+ 3. Check if any memory solves your problem or warns of pitfalls
321
+ 4. Adjust your approach based on past learnings
304
322
 
305
- **Past learnings save time and prevent repeating mistakes.**
323
+ **If you skip this step, you WILL waste time solving already-solved problems.**
306
324
 
307
325
  ### Step 3: Load Relevant Skills (if available)
308
326
  \`\`\`
@@ -739,7 +757,7 @@ export const swarm_subtask_prompt = tool({
739
757
  */
740
758
  export const swarm_spawn_subtask = tool({
741
759
  description:
742
- "Prepare a subtask for spawning. Returns prompt with Agent Mail/hive tracking instructions. IMPORTANT: Pass project_path for swarmmail_init.",
760
+ "Prepare a subtask for spawning. Returns prompt with Agent Mail/hive tracking instructions. IMPORTANT: Pass project_path for swarmmail_init. Automatically selects appropriate model based on file types.",
743
761
  args: {
744
762
  bead_id: tool.schema.string().describe("Subtask bead ID"),
745
763
  epic_id: tool.schema.string().describe("Parent epic bead ID"),
@@ -769,6 +787,10 @@ export const swarm_spawn_subtask = tool({
769
787
  })
770
788
  .optional()
771
789
  .describe("Recovery context from checkpoint compaction"),
790
+ model: tool.schema
791
+ .string()
792
+ .optional()
793
+ .describe("Optional explicit model override (auto-selected if not provided)"),
772
794
  },
773
795
  async execute(args) {
774
796
  const prompt = formatSubtaskPromptV2({
@@ -782,6 +804,28 @@ export const swarm_spawn_subtask = tool({
782
804
  recovery_context: args.recovery_context,
783
805
  });
784
806
 
807
+ // Import selectWorkerModel at function scope to avoid circular dependencies
808
+ const { selectWorkerModel } = await import("./model-selection.js");
809
+
810
+ // Create a mock subtask for model selection
811
+ const subtask = {
812
+ title: args.subtask_title,
813
+ description: args.subtask_description || "",
814
+ files: args.files,
815
+ estimated_effort: "medium" as const,
816
+ risks: [],
817
+ model: args.model,
818
+ };
819
+
820
+ // Use placeholder config - actual config should be passed from coordinator
821
+ // For now, we use reasonable defaults
822
+ const config = {
823
+ primaryModel: "anthropic/claude-sonnet-4-5",
824
+ liteModel: "anthropic/claude-haiku-4-5",
825
+ };
826
+
827
+ const selectedModel = selectWorkerModel(subtask, config);
828
+
785
829
  return JSON.stringify(
786
830
  {
787
831
  prompt,
@@ -790,6 +834,7 @@ export const swarm_spawn_subtask = tool({
790
834
  files: args.files,
791
835
  project_path: args.project_path,
792
836
  recovery_context: args.recovery_context,
837
+ recommended_model: selectedModel,
793
838
  },
794
839
  null,
795
840
  2,