opencode-swarm-plugin 0.30.0 → 0.30.3
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-build.log +4 -4
- package/CHANGELOG.md +93 -0
- package/README.md +3 -6
- package/bin/swarm.ts +151 -22
- package/dist/hive.d.ts.map +1 -1
- package/dist/index.d.ts +94 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +18826 -3467
- package/dist/memory-tools.d.ts +209 -0
- package/dist/memory-tools.d.ts.map +1 -0
- package/dist/memory.d.ts +124 -0
- package/dist/memory.d.ts.map +1 -0
- package/dist/plugin.js +18766 -3418
- package/dist/schemas/index.d.ts +7 -0
- package/dist/schemas/index.d.ts.map +1 -1
- package/dist/schemas/worker-handoff.d.ts +78 -0
- package/dist/schemas/worker-handoff.d.ts.map +1 -0
- package/dist/swarm-orchestrate.d.ts +50 -0
- package/dist/swarm-orchestrate.d.ts.map +1 -1
- package/dist/swarm-prompts.d.ts +1 -1
- package/dist/swarm-prompts.d.ts.map +1 -1
- package/dist/swarm-review.d.ts +4 -0
- package/dist/swarm-review.d.ts.map +1 -1
- package/docs/planning/ADR-008-worker-handoff-protocol.md +293 -0
- package/package.json +4 -2
- package/src/hive.integration.test.ts +114 -0
- package/src/hive.ts +33 -22
- package/src/index.ts +38 -3
- package/src/memory-tools.test.ts +111 -0
- package/src/memory-tools.ts +273 -0
- package/src/memory.integration.test.ts +266 -0
- package/src/memory.test.ts +334 -0
- package/src/memory.ts +441 -0
- package/src/schemas/index.ts +18 -0
- package/src/schemas/worker-handoff.test.ts +271 -0
- package/src/schemas/worker-handoff.ts +131 -0
- package/src/swarm-orchestrate.ts +262 -24
- package/src/swarm-prompts.ts +48 -5
- package/src/swarm-review.ts +7 -0
- package/src/swarm.integration.test.ts +386 -9
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for WorkerHandoff schema validation
|
|
3
|
+
*
|
|
4
|
+
* WorkerHandoff replaces prose instructions with structured contracts.
|
|
5
|
+
* These tests ensure runtime validation catches malformed handoffs.
|
|
6
|
+
*/
|
|
7
|
+
import { describe, expect, test } from "bun:test";
|
|
8
|
+
import {
|
|
9
|
+
WorkerHandoffContractSchema,
|
|
10
|
+
WorkerHandoffContextSchema,
|
|
11
|
+
WorkerHandoffEscalationSchema,
|
|
12
|
+
WorkerHandoffSchema,
|
|
13
|
+
type WorkerHandoff,
|
|
14
|
+
type WorkerHandoffContract,
|
|
15
|
+
type WorkerHandoffContext,
|
|
16
|
+
type WorkerHandoffEscalation,
|
|
17
|
+
} from "./worker-handoff";
|
|
18
|
+
|
|
19
|
+
describe("WorkerHandoffContractSchema", () => {
|
|
20
|
+
test("valid contract parses correctly", () => {
|
|
21
|
+
const validContract = {
|
|
22
|
+
task_id: "opencode-swarm-monorepo-lf2p4u-abc123",
|
|
23
|
+
files_owned: ["src/auth/service.ts", "src/auth/schema.ts"],
|
|
24
|
+
files_readonly: ["src/lib/jwt.ts"],
|
|
25
|
+
dependencies_completed: ["opencode-swarm-monorepo-lf2p4u-abc122"],
|
|
26
|
+
success_criteria: [
|
|
27
|
+
"Auth service implements JWT strategy",
|
|
28
|
+
"All tests pass",
|
|
29
|
+
],
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const result = WorkerHandoffContractSchema.safeParse(validContract);
|
|
33
|
+
expect(result.success).toBe(true);
|
|
34
|
+
if (result.success) {
|
|
35
|
+
expect(result.data.task_id).toBe("opencode-swarm-monorepo-lf2p4u-abc123");
|
|
36
|
+
expect(result.data.files_owned).toHaveLength(2);
|
|
37
|
+
expect(result.data.success_criteria).toHaveLength(2);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("missing task_id fails", () => {
|
|
42
|
+
const invalidContract = {
|
|
43
|
+
files_owned: ["src/auth.ts"],
|
|
44
|
+
files_readonly: [],
|
|
45
|
+
dependencies_completed: [],
|
|
46
|
+
success_criteria: ["Auth works"],
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const result = WorkerHandoffContractSchema.safeParse(invalidContract);
|
|
50
|
+
expect(result.success).toBe(false);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("empty files_owned is valid (read-only tasks)", () => {
|
|
54
|
+
const readOnlyContract = {
|
|
55
|
+
task_id: "opencode-swarm-monorepo-lf2p4u-abc123",
|
|
56
|
+
files_owned: [], // Read-only task
|
|
57
|
+
files_readonly: ["src/types.ts"],
|
|
58
|
+
dependencies_completed: [],
|
|
59
|
+
success_criteria: ["Documentation updated"],
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const result = WorkerHandoffContractSchema.safeParse(readOnlyContract);
|
|
63
|
+
expect(result.success).toBe(true);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("empty success_criteria fails", () => {
|
|
67
|
+
const invalidContract = {
|
|
68
|
+
task_id: "opencode-swarm-monorepo-lf2p4u-abc123",
|
|
69
|
+
files_owned: ["src/auth.ts"],
|
|
70
|
+
files_readonly: [],
|
|
71
|
+
dependencies_completed: [],
|
|
72
|
+
success_criteria: [], // Must have at least one
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const result = WorkerHandoffContractSchema.safeParse(invalidContract);
|
|
76
|
+
expect(result.success).toBe(false);
|
|
77
|
+
if (!result.success) {
|
|
78
|
+
expect(result.error.issues[0].message).toContain(
|
|
79
|
+
"at least one success criterion",
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("invalid task_id format fails", () => {
|
|
85
|
+
const invalidContract = {
|
|
86
|
+
task_id: "invalid-format", // Missing hash component
|
|
87
|
+
files_owned: ["src/auth.ts"],
|
|
88
|
+
files_readonly: [],
|
|
89
|
+
dependencies_completed: [],
|
|
90
|
+
success_criteria: ["Auth works"],
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const result = WorkerHandoffContractSchema.safeParse(invalidContract);
|
|
94
|
+
expect(result.success).toBe(false);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe("WorkerHandoffContextSchema", () => {
|
|
99
|
+
test("valid context parses correctly", () => {
|
|
100
|
+
const validContext = {
|
|
101
|
+
epic_summary: "Add authentication system",
|
|
102
|
+
your_role: "Implement JWT auth service",
|
|
103
|
+
what_others_did: "Schema defined by agent-1",
|
|
104
|
+
what_comes_next: "Integration tests in next subtask",
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const result = WorkerHandoffContextSchema.safeParse(validContext);
|
|
108
|
+
expect(result.success).toBe(true);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("missing required fields fails", () => {
|
|
112
|
+
const invalidContext = {
|
|
113
|
+
epic_summary: "Add auth",
|
|
114
|
+
your_role: "Implement service",
|
|
115
|
+
// Missing what_others_did and what_comes_next
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const result = WorkerHandoffContextSchema.safeParse(invalidContext);
|
|
119
|
+
expect(result.success).toBe(false);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("empty strings are valid", () => {
|
|
123
|
+
const contextWithEmptyStrings = {
|
|
124
|
+
epic_summary: "Add auth",
|
|
125
|
+
your_role: "Implement service",
|
|
126
|
+
what_others_did: "", // Valid for first subtask
|
|
127
|
+
what_comes_next: "", // Valid for last subtask
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const result = WorkerHandoffContextSchema.safeParse(
|
|
131
|
+
contextWithEmptyStrings,
|
|
132
|
+
);
|
|
133
|
+
expect(result.success).toBe(true);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe("WorkerHandoffEscalationSchema", () => {
|
|
138
|
+
test("valid escalation parses correctly", () => {
|
|
139
|
+
const validEscalation = {
|
|
140
|
+
blocked_contact: "Message coordinator via swarmmail_send(importance='high')",
|
|
141
|
+
scope_change_protocol:
|
|
142
|
+
"Request approval before expanding scope beyond files_owned",
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const result = WorkerHandoffEscalationSchema.safeParse(validEscalation);
|
|
146
|
+
expect(result.success).toBe(true);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("missing required fields fails", () => {
|
|
150
|
+
const invalidEscalation = {
|
|
151
|
+
blocked_contact: "Message coordinator",
|
|
152
|
+
// Missing scope_change_protocol
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const result = WorkerHandoffEscalationSchema.safeParse(invalidEscalation);
|
|
156
|
+
expect(result.success).toBe(false);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
describe("WorkerHandoffSchema", () => {
|
|
161
|
+
test("complete valid handoff parses correctly", () => {
|
|
162
|
+
const validHandoff = {
|
|
163
|
+
contract: {
|
|
164
|
+
task_id: "opencode-swarm-monorepo-lf2p4u-abc123",
|
|
165
|
+
files_owned: ["src/auth/service.ts"],
|
|
166
|
+
files_readonly: ["src/lib/jwt.ts"],
|
|
167
|
+
dependencies_completed: [],
|
|
168
|
+
success_criteria: ["Service implemented", "Tests pass"],
|
|
169
|
+
},
|
|
170
|
+
context: {
|
|
171
|
+
epic_summary: "Add authentication",
|
|
172
|
+
your_role: "Implement auth service",
|
|
173
|
+
what_others_did: "Schema defined",
|
|
174
|
+
what_comes_next: "Integration tests",
|
|
175
|
+
},
|
|
176
|
+
escalation: {
|
|
177
|
+
blocked_contact: "Message coordinator",
|
|
178
|
+
scope_change_protocol: "Request approval first",
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const result = WorkerHandoffSchema.safeParse(validHandoff);
|
|
183
|
+
expect(result.success).toBe(true);
|
|
184
|
+
if (result.success) {
|
|
185
|
+
const handoff: WorkerHandoff = result.data;
|
|
186
|
+
expect(handoff.contract.task_id).toBe(
|
|
187
|
+
"opencode-swarm-monorepo-lf2p4u-abc123",
|
|
188
|
+
);
|
|
189
|
+
expect(handoff.context.your_role).toBe("Implement auth service");
|
|
190
|
+
expect(handoff.escalation.blocked_contact).toBe("Message coordinator");
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test("missing contract section fails", () => {
|
|
195
|
+
const invalidHandoff = {
|
|
196
|
+
context: {
|
|
197
|
+
epic_summary: "Add auth",
|
|
198
|
+
your_role: "Implement",
|
|
199
|
+
what_others_did: "Schema",
|
|
200
|
+
what_comes_next: "Tests",
|
|
201
|
+
},
|
|
202
|
+
escalation: {
|
|
203
|
+
blocked_contact: "Message coordinator",
|
|
204
|
+
scope_change_protocol: "Request approval",
|
|
205
|
+
},
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const result = WorkerHandoffSchema.safeParse(invalidHandoff);
|
|
209
|
+
expect(result.success).toBe(false);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test("nested validation catches contract errors", () => {
|
|
213
|
+
const handoffWithInvalidContract = {
|
|
214
|
+
contract: {
|
|
215
|
+
task_id: "opencode-swarm-monorepo-lf2p4u-abc123",
|
|
216
|
+
files_owned: ["src/auth.ts"],
|
|
217
|
+
files_readonly: [],
|
|
218
|
+
dependencies_completed: [],
|
|
219
|
+
success_criteria: [], // Invalid: empty
|
|
220
|
+
},
|
|
221
|
+
context: {
|
|
222
|
+
epic_summary: "Add auth",
|
|
223
|
+
your_role: "Implement",
|
|
224
|
+
what_others_did: "Schema",
|
|
225
|
+
what_comes_next: "Tests",
|
|
226
|
+
},
|
|
227
|
+
escalation: {
|
|
228
|
+
blocked_contact: "Message coordinator",
|
|
229
|
+
scope_change_protocol: "Request approval",
|
|
230
|
+
},
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
const result = WorkerHandoffSchema.safeParse(handoffWithInvalidContract);
|
|
234
|
+
expect(result.success).toBe(false);
|
|
235
|
+
if (!result.success) {
|
|
236
|
+
const errorMessage = result.error.issues[0].message;
|
|
237
|
+
expect(errorMessage).toContain("at least one success criterion");
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test("type inference works correctly", () => {
|
|
242
|
+
const handoff: WorkerHandoff = {
|
|
243
|
+
contract: {
|
|
244
|
+
task_id: "opencode-swarm-monorepo-lf2p4u-abc123",
|
|
245
|
+
files_owned: [],
|
|
246
|
+
files_readonly: [],
|
|
247
|
+
dependencies_completed: [],
|
|
248
|
+
success_criteria: ["Done"],
|
|
249
|
+
},
|
|
250
|
+
context: {
|
|
251
|
+
epic_summary: "Summary",
|
|
252
|
+
your_role: "Role",
|
|
253
|
+
what_others_did: "Nothing",
|
|
254
|
+
what_comes_next: "More work",
|
|
255
|
+
},
|
|
256
|
+
escalation: {
|
|
257
|
+
blocked_contact: "Coordinator",
|
|
258
|
+
scope_change_protocol: "Ask first",
|
|
259
|
+
},
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
// Type check only - verify TypeScript inference
|
|
263
|
+
const contract: WorkerHandoffContract = handoff.contract;
|
|
264
|
+
const context: WorkerHandoffContext = handoff.context;
|
|
265
|
+
const escalation: WorkerHandoffEscalation = handoff.escalation;
|
|
266
|
+
|
|
267
|
+
expect(contract.task_id).toBeDefined();
|
|
268
|
+
expect(context.your_role).toBeDefined();
|
|
269
|
+
expect(escalation.blocked_contact).toBeDefined();
|
|
270
|
+
});
|
|
271
|
+
});
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WorkerHandoff schemas - structured contracts replacing prose instructions
|
|
3
|
+
*
|
|
4
|
+
* Replaces the 400-line SUBTASK_PROMPT_V2 with machine-readable contracts.
|
|
5
|
+
* Workers receive typed handoffs with explicit files, criteria, and escalation paths.
|
|
6
|
+
*/
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Contract section - the binding agreement between coordinator and worker
|
|
11
|
+
*
|
|
12
|
+
* Defines:
|
|
13
|
+
* - What task to complete (task_id)
|
|
14
|
+
* - What files to modify (files_owned) vs read (files_readonly)
|
|
15
|
+
* - What's already done (dependencies_completed)
|
|
16
|
+
* - How to know you're done (success_criteria)
|
|
17
|
+
*/
|
|
18
|
+
export const WorkerHandoffContractSchema = z.object({
|
|
19
|
+
/**
|
|
20
|
+
* Cell ID for this subtask.
|
|
21
|
+
* Format: `{project}-{hash}` or `{project}-{hash}.{index}`
|
|
22
|
+
* Example: `opencode-swarm-monorepo-lf2p4u-abc123`
|
|
23
|
+
*
|
|
24
|
+
* Requires at least 3 segments (project can be multi-word, must have hash).
|
|
25
|
+
*/
|
|
26
|
+
task_id: z
|
|
27
|
+
.string()
|
|
28
|
+
.regex(
|
|
29
|
+
/^[a-z0-9]+(-[a-z0-9]+){2,}(\.[\w-]+)?$/,
|
|
30
|
+
"Invalid task ID format (expected: project-slug-hash with minimum 3 segments)",
|
|
31
|
+
),
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Files this worker owns (exclusive write access).
|
|
35
|
+
* Empty array is valid for read-only tasks (e.g., documentation review).
|
|
36
|
+
*/
|
|
37
|
+
files_owned: z.array(z.string()).default([]),
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Files this worker can read but must not modify.
|
|
41
|
+
* Coordinator reserves these for other workers.
|
|
42
|
+
*/
|
|
43
|
+
files_readonly: z.array(z.string()).default([]),
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Subtask IDs that must complete before this one.
|
|
47
|
+
* Empty if no dependencies (can start immediately).
|
|
48
|
+
*/
|
|
49
|
+
dependencies_completed: z.array(z.string()).default([]),
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Success criteria - how to know the task is complete.
|
|
53
|
+
* Must have at least one criterion to prevent ambiguous completion.
|
|
54
|
+
*/
|
|
55
|
+
success_criteria: z
|
|
56
|
+
.array(z.string())
|
|
57
|
+
.min(1, "Must have at least one success criterion"),
|
|
58
|
+
});
|
|
59
|
+
export type WorkerHandoffContract = z.infer<typeof WorkerHandoffContractSchema>;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Context section - the narrative explaining the "why"
|
|
63
|
+
*
|
|
64
|
+
* Provides:
|
|
65
|
+
* - Big picture (epic_summary)
|
|
66
|
+
* - This worker's specific role
|
|
67
|
+
* - What's already been done
|
|
68
|
+
* - What comes after
|
|
69
|
+
*/
|
|
70
|
+
export const WorkerHandoffContextSchema = z.object({
|
|
71
|
+
/**
|
|
72
|
+
* High-level summary of the entire epic.
|
|
73
|
+
* Helps worker understand how their piece fits.
|
|
74
|
+
*/
|
|
75
|
+
epic_summary: z.string(),
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* This worker's specific role/responsibility.
|
|
79
|
+
* Should align with files_owned in contract.
|
|
80
|
+
*/
|
|
81
|
+
your_role: z.string(),
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* What previous subtasks accomplished.
|
|
85
|
+
* Empty string is valid for first subtask.
|
|
86
|
+
*/
|
|
87
|
+
what_others_did: z.string(),
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* What happens after this subtask completes.
|
|
91
|
+
* Empty string is valid for last subtask.
|
|
92
|
+
*/
|
|
93
|
+
what_comes_next: z.string(),
|
|
94
|
+
});
|
|
95
|
+
export type WorkerHandoffContext = z.infer<typeof WorkerHandoffContextSchema>;
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Escalation section - what to do when things go wrong
|
|
99
|
+
*
|
|
100
|
+
* Defines:
|
|
101
|
+
* - How to report blockers
|
|
102
|
+
* - Protocol for scope changes
|
|
103
|
+
*/
|
|
104
|
+
export const WorkerHandoffEscalationSchema = z.object({
|
|
105
|
+
/**
|
|
106
|
+
* Instructions for reporting blockers.
|
|
107
|
+
* Typically: "Message coordinator via swarmmail_send(importance='high')"
|
|
108
|
+
*/
|
|
109
|
+
blocked_contact: z.string(),
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Protocol for requesting scope changes.
|
|
113
|
+
* Typically: "Request approval before expanding scope beyond files_owned"
|
|
114
|
+
*/
|
|
115
|
+
scope_change_protocol: z.string(),
|
|
116
|
+
});
|
|
117
|
+
export type WorkerHandoffEscalation = z.infer<
|
|
118
|
+
typeof WorkerHandoffEscalationSchema
|
|
119
|
+
>;
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Complete WorkerHandoff - combines all three sections
|
|
123
|
+
*
|
|
124
|
+
* This is the full structured contract that replaces prose instructions.
|
|
125
|
+
*/
|
|
126
|
+
export const WorkerHandoffSchema = z.object({
|
|
127
|
+
contract: WorkerHandoffContractSchema,
|
|
128
|
+
context: WorkerHandoffContextSchema,
|
|
129
|
+
escalation: WorkerHandoffEscalationSchema,
|
|
130
|
+
});
|
|
131
|
+
export type WorkerHandoff = z.infer<typeof WorkerHandoffSchema>;
|