opencode-swarm-plugin 0.36.1 → 0.37.0
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/.hive/issues.jsonl +12 -0
- package/.turbo/turbo-test.log +321 -321
- package/CHANGELOG.md +42 -0
- package/bin/swarm.test.ts +106 -0
- package/bin/swarm.ts +179 -0
- package/package.json +1 -1
- package/src/hive.integration.test.ts +148 -0
- package/src/hive.ts +89 -0
- package/src/swarm-decompose.test.ts +195 -0
- package/src/swarm-decompose.ts +52 -1
- package/src/swarm-review.integration.test.ts +24 -29
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Swarm Decompose Unit Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests for task decomposition, validation, and eval capture integration.
|
|
5
|
+
*
|
|
6
|
+
* TDD: Testing eval capture integration - verifies captureDecomposition() is called
|
|
7
|
+
* after successful validation with correct parameters.
|
|
8
|
+
*/
|
|
9
|
+
import { afterEach, beforeEach, describe, expect, test, mock } from "bun:test";
|
|
10
|
+
import * as fs from "node:fs";
|
|
11
|
+
import { swarm_validate_decomposition } from "./swarm-decompose";
|
|
12
|
+
import * as evalCapture from "./eval-capture.js";
|
|
13
|
+
|
|
14
|
+
// ============================================================================
|
|
15
|
+
// Test Setup
|
|
16
|
+
// ============================================================================
|
|
17
|
+
|
|
18
|
+
const mockContext = {
|
|
19
|
+
sessionID: `test-decompose-${Date.now()}`,
|
|
20
|
+
messageID: `test-message-${Date.now()}`,
|
|
21
|
+
agent: "test-agent",
|
|
22
|
+
abort: new AbortController().signal,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
let testProjectPath: string;
|
|
26
|
+
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
testProjectPath = `/tmp/test-swarm-decompose-${Date.now()}`;
|
|
29
|
+
fs.mkdirSync(testProjectPath, { recursive: true });
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
afterEach(() => {
|
|
33
|
+
if (fs.existsSync(testProjectPath)) {
|
|
34
|
+
fs.rmSync(testProjectPath, { recursive: true, force: true });
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// ============================================================================
|
|
39
|
+
// Eval Capture Integration Tests
|
|
40
|
+
// ============================================================================
|
|
41
|
+
|
|
42
|
+
describe("captureDecomposition integration", () => {
|
|
43
|
+
test("calls captureDecomposition after successful validation with all params", async () => {
|
|
44
|
+
// Mock captureDecomposition to spy on calls
|
|
45
|
+
const captureDecompositionSpy = mock(() => ({
|
|
46
|
+
id: "test-epic-123",
|
|
47
|
+
timestamp: new Date().toISOString(),
|
|
48
|
+
task: "Add user authentication",
|
|
49
|
+
}));
|
|
50
|
+
const original = evalCapture.captureDecomposition;
|
|
51
|
+
// @ts-expect-error - mocking for test
|
|
52
|
+
evalCapture.captureDecomposition = captureDecompositionSpy;
|
|
53
|
+
|
|
54
|
+
const validCellTree = JSON.stringify({
|
|
55
|
+
epic: {
|
|
56
|
+
title: "Add OAuth",
|
|
57
|
+
description: "Implement OAuth authentication",
|
|
58
|
+
},
|
|
59
|
+
subtasks: [
|
|
60
|
+
{
|
|
61
|
+
title: "Add OAuth provider config",
|
|
62
|
+
description: "Set up Google OAuth",
|
|
63
|
+
files: ["src/auth/google.ts", "src/auth/config.ts"],
|
|
64
|
+
dependencies: [],
|
|
65
|
+
estimated_complexity: 2,
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
title: "Add login UI",
|
|
69
|
+
description: "Create login button component",
|
|
70
|
+
files: ["src/components/LoginButton.tsx"],
|
|
71
|
+
dependencies: [0],
|
|
72
|
+
estimated_complexity: 1,
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const result = await swarm_validate_decomposition.execute(
|
|
78
|
+
{
|
|
79
|
+
response: validCellTree,
|
|
80
|
+
project_path: testProjectPath,
|
|
81
|
+
task: "Add user authentication",
|
|
82
|
+
context: "Using NextAuth.js",
|
|
83
|
+
strategy: "feature-based" as const,
|
|
84
|
+
epic_id: "test-epic-123",
|
|
85
|
+
},
|
|
86
|
+
mockContext,
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
const parsed = JSON.parse(result);
|
|
90
|
+
expect(parsed.valid).toBe(true);
|
|
91
|
+
|
|
92
|
+
// Verify captureDecomposition was called with correct params
|
|
93
|
+
expect(captureDecompositionSpy).toHaveBeenCalledTimes(1);
|
|
94
|
+
const callArgs = captureDecompositionSpy.mock.calls[0][0];
|
|
95
|
+
expect(callArgs.epicId).toBe("test-epic-123");
|
|
96
|
+
expect(callArgs.projectPath).toBe(testProjectPath);
|
|
97
|
+
expect(callArgs.task).toBe("Add user authentication");
|
|
98
|
+
expect(callArgs.context).toBe("Using NextAuth.js");
|
|
99
|
+
expect(callArgs.strategy).toBe("feature-based");
|
|
100
|
+
expect(callArgs.epicTitle).toBe("Add OAuth");
|
|
101
|
+
expect(callArgs.epicDescription).toBe("Implement OAuth authentication");
|
|
102
|
+
expect(callArgs.subtasks).toHaveLength(2);
|
|
103
|
+
expect(callArgs.subtasks[0].title).toBe("Add OAuth provider config");
|
|
104
|
+
|
|
105
|
+
// Restore
|
|
106
|
+
// @ts-expect-error - restoring mock
|
|
107
|
+
evalCapture.captureDecomposition = original;
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("does not call captureDecomposition when validation fails", async () => {
|
|
111
|
+
const captureDecompositionSpy = mock(() => ({}));
|
|
112
|
+
const original = evalCapture.captureDecomposition;
|
|
113
|
+
// @ts-expect-error - mocking for test
|
|
114
|
+
evalCapture.captureDecomposition = captureDecompositionSpy;
|
|
115
|
+
|
|
116
|
+
// Invalid CellTree - missing required fields
|
|
117
|
+
const invalidCellTree = JSON.stringify({
|
|
118
|
+
epic: { title: "Missing subtasks" },
|
|
119
|
+
// No subtasks array
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const result = await swarm_validate_decomposition.execute(
|
|
123
|
+
{
|
|
124
|
+
response: invalidCellTree,
|
|
125
|
+
project_path: testProjectPath,
|
|
126
|
+
task: "Add auth",
|
|
127
|
+
strategy: "auto" as const,
|
|
128
|
+
epic_id: "test-epic-456",
|
|
129
|
+
},
|
|
130
|
+
mockContext,
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
const parsed = JSON.parse(result);
|
|
134
|
+
expect(parsed.valid).toBe(false);
|
|
135
|
+
|
|
136
|
+
// Verify captureDecomposition was NOT called
|
|
137
|
+
expect(captureDecompositionSpy).not.toHaveBeenCalled();
|
|
138
|
+
|
|
139
|
+
// Restore
|
|
140
|
+
// @ts-expect-error - restoring mock
|
|
141
|
+
evalCapture.captureDecomposition = original;
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("handles optional context and description fields", async () => {
|
|
145
|
+
const captureDecompositionSpy = mock(() => ({
|
|
146
|
+
id: "test-epic-789",
|
|
147
|
+
timestamp: new Date().toISOString(),
|
|
148
|
+
task: "Fix the auth bug",
|
|
149
|
+
}));
|
|
150
|
+
const original = evalCapture.captureDecomposition;
|
|
151
|
+
// @ts-expect-error - mocking for test
|
|
152
|
+
evalCapture.captureDecomposition = captureDecompositionSpy;
|
|
153
|
+
|
|
154
|
+
const validCellTree = JSON.stringify({
|
|
155
|
+
epic: {
|
|
156
|
+
title: "Fix bug",
|
|
157
|
+
// No description
|
|
158
|
+
},
|
|
159
|
+
subtasks: [
|
|
160
|
+
{
|
|
161
|
+
title: "Add test",
|
|
162
|
+
files: ["src/test.ts"],
|
|
163
|
+
dependencies: [],
|
|
164
|
+
estimated_complexity: 1,
|
|
165
|
+
},
|
|
166
|
+
],
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
const result = await swarm_validate_decomposition.execute(
|
|
170
|
+
{
|
|
171
|
+
response: validCellTree,
|
|
172
|
+
project_path: testProjectPath,
|
|
173
|
+
task: "Fix the auth bug",
|
|
174
|
+
// No context
|
|
175
|
+
strategy: "risk-based" as const,
|
|
176
|
+
epic_id: "test-epic-789",
|
|
177
|
+
},
|
|
178
|
+
mockContext,
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
const parsed = JSON.parse(result);
|
|
182
|
+
expect(parsed.valid).toBe(true);
|
|
183
|
+
|
|
184
|
+
// Verify captureDecomposition was called without optional fields
|
|
185
|
+
expect(captureDecompositionSpy).toHaveBeenCalledTimes(1);
|
|
186
|
+
const callArgs = captureDecompositionSpy.mock.calls[0][0];
|
|
187
|
+
expect(callArgs.epicId).toBe("test-epic-789");
|
|
188
|
+
expect(callArgs.context).toBeUndefined();
|
|
189
|
+
expect(callArgs.epicDescription).toBeUndefined();
|
|
190
|
+
|
|
191
|
+
// Restore
|
|
192
|
+
// @ts-expect-error - restoring mock
|
|
193
|
+
evalCapture.captureDecomposition = original;
|
|
194
|
+
});
|
|
195
|
+
});
|
package/src/swarm-decompose.ts
CHANGED
|
@@ -535,11 +535,31 @@ export const swarm_decompose = tool({
|
|
|
535
535
|
* Use this after the agent responds to swarm:decompose to validate the structure.
|
|
536
536
|
*/
|
|
537
537
|
export const swarm_validate_decomposition = tool({
|
|
538
|
-
description: "Validate a decomposition response against CellTreeSchema",
|
|
538
|
+
description: "Validate a decomposition response against CellTreeSchema and capture for eval",
|
|
539
539
|
args: {
|
|
540
540
|
response: tool.schema
|
|
541
541
|
.string()
|
|
542
542
|
.describe("JSON response from agent (CellTree format)"),
|
|
543
|
+
project_path: tool.schema
|
|
544
|
+
.string()
|
|
545
|
+
.optional()
|
|
546
|
+
.describe("Project path for eval capture"),
|
|
547
|
+
task: tool.schema
|
|
548
|
+
.string()
|
|
549
|
+
.optional()
|
|
550
|
+
.describe("Original task description for eval capture"),
|
|
551
|
+
context: tool.schema
|
|
552
|
+
.string()
|
|
553
|
+
.optional()
|
|
554
|
+
.describe("Context provided for decomposition"),
|
|
555
|
+
strategy: tool.schema
|
|
556
|
+
.enum(["file-based", "feature-based", "risk-based", "auto"])
|
|
557
|
+
.optional()
|
|
558
|
+
.describe("Decomposition strategy used"),
|
|
559
|
+
epic_id: tool.schema
|
|
560
|
+
.string()
|
|
561
|
+
.optional()
|
|
562
|
+
.describe("Epic ID for eval capture"),
|
|
543
563
|
},
|
|
544
564
|
async execute(args) {
|
|
545
565
|
try {
|
|
@@ -597,6 +617,37 @@ export const swarm_validate_decomposition = tool({
|
|
|
597
617
|
validated.subtasks,
|
|
598
618
|
);
|
|
599
619
|
|
|
620
|
+
// Capture decomposition for eval if all required params provided
|
|
621
|
+
if (
|
|
622
|
+
args.project_path &&
|
|
623
|
+
args.task &&
|
|
624
|
+
args.strategy &&
|
|
625
|
+
args.epic_id
|
|
626
|
+
) {
|
|
627
|
+
try {
|
|
628
|
+
const { captureDecomposition } = await import("./eval-capture.js");
|
|
629
|
+
captureDecomposition({
|
|
630
|
+
epicId: args.epic_id,
|
|
631
|
+
projectPath: args.project_path,
|
|
632
|
+
task: args.task,
|
|
633
|
+
context: args.context,
|
|
634
|
+
strategy: args.strategy,
|
|
635
|
+
epicTitle: validated.epic.title,
|
|
636
|
+
epicDescription: validated.epic.description,
|
|
637
|
+
subtasks: validated.subtasks.map((s) => ({
|
|
638
|
+
title: s.title,
|
|
639
|
+
description: s.description,
|
|
640
|
+
files: s.files,
|
|
641
|
+
dependencies: s.dependencies,
|
|
642
|
+
estimated_complexity: s.estimated_complexity,
|
|
643
|
+
})),
|
|
644
|
+
});
|
|
645
|
+
} catch (error) {
|
|
646
|
+
// Non-fatal - don't block validation if capture fails
|
|
647
|
+
console.warn("[swarm_validate_decomposition] Failed to capture decomposition:", error);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
600
651
|
return JSON.stringify(
|
|
601
652
|
{
|
|
602
653
|
valid: true,
|
|
@@ -2,18 +2,15 @@
|
|
|
2
2
|
* Integration tests for swarm review feedback flow
|
|
3
3
|
*
|
|
4
4
|
* Tests the coordinator review feedback workflow with real HiveAdapter and swarm-mail.
|
|
5
|
-
* Verifies that review approval/rejection properly updates state
|
|
5
|
+
* Verifies that review approval/rejection properly updates state.
|
|
6
6
|
*
|
|
7
|
-
* **
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* - Database path resolution issue between send and receive
|
|
15
|
-
*
|
|
16
|
-
* Tests currently SKIPPED pending message retrieval fix.
|
|
7
|
+
* **ARCHITECTURE**: Coordinator-driven retry pattern (swarm_spawn_retry)
|
|
8
|
+
* - `approved` status: Sends message to worker (worker can complete)
|
|
9
|
+
* - `needs_changes` status: NO message sent (worker is dead, coordinator spawns retry)
|
|
10
|
+
* - After 3 rejections: Task marked blocked, NO message sent
|
|
11
|
+
*
|
|
12
|
+
* This aligns with the "worker is dead" philosophy - failed reviews require coordinator
|
|
13
|
+
* intervention via swarm_spawn_retry, not worker self-retry.
|
|
17
14
|
*/
|
|
18
15
|
|
|
19
16
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
@@ -195,23 +192,23 @@ describe("swarm_review integration", () => {
|
|
|
195
192
|
expect(feedbackParsed.attempt).toBe(1);
|
|
196
193
|
expect(feedbackParsed.remaining_attempts).toBe(2);
|
|
197
194
|
|
|
198
|
-
// Verify
|
|
199
|
-
expect(feedbackParsed.
|
|
200
|
-
|
|
201
|
-
|
|
195
|
+
// Verify retry_context is provided for coordinator to spawn retry
|
|
196
|
+
expect(feedbackParsed.retry_context).toBeDefined();
|
|
197
|
+
expect(feedbackParsed.retry_context.task_id).toBe(subtask.id);
|
|
198
|
+
expect(feedbackParsed.retry_context.attempt).toBe(1);
|
|
199
|
+
expect(feedbackParsed.retry_context.max_attempts).toBe(3);
|
|
200
|
+
expect(feedbackParsed.retry_context.issues).toEqual(issues);
|
|
201
|
+
expect(feedbackParsed.retry_context.next_action).toContain("swarm_spawn_retry");
|
|
202
|
+
|
|
203
|
+
// ARCHITECTURE CHANGE: No longer sends message to worker
|
|
204
|
+
// Worker is considered "dead" - coordinator must spawn retry
|
|
205
|
+
// Inbox should remain empty
|
|
202
206
|
const messages = await swarmMail.getInbox(
|
|
203
207
|
testProjectPath,
|
|
204
208
|
"TestWorker",
|
|
205
209
|
{ limit: 10 }
|
|
206
210
|
);
|
|
207
|
-
expect(messages.length).
|
|
208
|
-
|
|
209
|
-
const needsChangesMessage = messages.find((m) =>
|
|
210
|
-
m.subject.includes("NEEDS CHANGES")
|
|
211
|
-
);
|
|
212
|
-
expect(needsChangesMessage).toBeDefined();
|
|
213
|
-
expect(needsChangesMessage?.subject).toContain(subtask.id);
|
|
214
|
-
expect(needsChangesMessage?.subject).toContain("attempt 1/3");
|
|
211
|
+
expect(messages.length).toBe(0);
|
|
215
212
|
});
|
|
216
213
|
|
|
217
214
|
test("3-strike rule: task marked blocked after 3 rejections", async () => {
|
|
@@ -275,16 +272,14 @@ describe("swarm_review integration", () => {
|
|
|
275
272
|
const updatedCell = await hive.getCell(testProjectPath, subtask.id);
|
|
276
273
|
expect(updatedCell?.status).toBe("blocked");
|
|
277
274
|
|
|
278
|
-
//
|
|
275
|
+
// ARCHITECTURE CHANGE: No longer sends failure message
|
|
276
|
+
// Worker is dead, coordinator handles escalation
|
|
277
|
+
// Inbox should remain empty
|
|
279
278
|
const messages = await swarmMail.getInbox(
|
|
280
279
|
testProjectPath,
|
|
281
280
|
"TestWorker",
|
|
282
281
|
{ limit: 10 }
|
|
283
282
|
);
|
|
284
|
-
|
|
285
|
-
const failedMessage = messages.find((m) => m.subject.includes("FAILED"));
|
|
286
|
-
expect(failedMessage).toBeDefined();
|
|
287
|
-
expect(failedMessage?.subject).toContain("max review attempts reached");
|
|
288
|
-
expect(failedMessage?.importance).toBe("urgent");
|
|
283
|
+
expect(messages.length).toBe(0);
|
|
289
284
|
});
|
|
290
285
|
});
|