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/.turbo/turbo-test.log +300 -300
- package/CHANGELOG.md +58 -0
- package/bin/swarm.ts +34 -0
- package/package.json +1 -1
- package/src/model-selection.test.ts +188 -0
- package/src/model-selection.ts +68 -0
- package/src/schemas/task.ts +5 -0
- package/src/swarm-prompts.test.ts +173 -0
- package/src/swarm-prompts.ts +54 -9
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
|
@@ -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
|
+
}
|
package/src/schemas/task.ts
CHANGED
|
@@ -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
|
+
});
|
package/src/swarm-prompts.ts
CHANGED
|
@@ -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
|
-
**
|
|
300
|
-
-
|
|
301
|
-
-
|
|
302
|
-
-
|
|
303
|
-
-
|
|
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
|
-
**
|
|
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,
|