opencode-swarm-plugin 0.36.0 → 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 +16 -4
- package/.hive/memories.jsonl +274 -1
- package/.turbo/turbo-build.log +4 -4
- package/.turbo/turbo-test.log +318 -318
- package/CHANGELOG.md +113 -0
- package/bin/swarm.test.ts +106 -0
- package/bin/swarm.ts +413 -179
- package/dist/compaction-hook.d.ts +54 -4
- package/dist/compaction-hook.d.ts.map +1 -1
- package/dist/eval-capture.d.ts +122 -17
- package/dist/eval-capture.d.ts.map +1 -1
- package/dist/index.d.ts +1 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1278 -619
- package/dist/planning-guardrails.d.ts +121 -0
- package/dist/planning-guardrails.d.ts.map +1 -1
- package/dist/plugin.d.ts +9 -9
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +1283 -329
- package/dist/schemas/task.d.ts +0 -1
- package/dist/schemas/task.d.ts.map +1 -1
- package/dist/swarm-decompose.d.ts +0 -8
- package/dist/swarm-decompose.d.ts.map +1 -1
- package/dist/swarm-orchestrate.d.ts.map +1 -1
- package/dist/swarm-prompts.d.ts +0 -4
- package/dist/swarm-prompts.d.ts.map +1 -1
- package/dist/swarm-review.d.ts.map +1 -1
- package/dist/swarm.d.ts +0 -6
- package/dist/swarm.d.ts.map +1 -1
- package/evals/README.md +38 -0
- package/evals/coordinator-session.eval.ts +154 -0
- package/evals/fixtures/coordinator-sessions.ts +328 -0
- package/evals/lib/data-loader.ts +69 -0
- package/evals/scorers/coordinator-discipline.evalite-test.ts +536 -0
- package/evals/scorers/coordinator-discipline.ts +315 -0
- package/evals/scorers/index.ts +12 -0
- package/examples/plugin-wrapper-template.ts +303 -4
- package/package.json +2 -2
- package/src/compaction-hook.test.ts +8 -1
- package/src/compaction-hook.ts +31 -21
- package/src/eval-capture.test.ts +390 -0
- package/src/eval-capture.ts +163 -4
- package/src/hive.integration.test.ts +148 -0
- package/src/hive.ts +89 -0
- package/src/index.ts +68 -1
- package/src/planning-guardrails.test.ts +387 -2
- package/src/planning-guardrails.ts +289 -0
- package/src/plugin.ts +10 -10
- package/src/swarm-decompose.test.ts +195 -0
- package/src/swarm-decompose.ts +72 -1
- package/src/swarm-orchestrate.ts +44 -0
- package/src/swarm-prompts.ts +20 -0
- package/src/swarm-review.integration.test.ts +24 -29
- package/src/swarm-review.ts +41 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-swarm-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.37.0",
|
|
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",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"minimatch": "^10.1.1",
|
|
42
42
|
"pino": "^9.6.0",
|
|
43
43
|
"pino-roll": "^1.3.0",
|
|
44
|
-
"swarm-mail": "1.5.
|
|
44
|
+
"swarm-mail": "1.5.1",
|
|
45
45
|
"yaml": "^2.8.2",
|
|
46
46
|
"zod": "4.1.8"
|
|
47
47
|
},
|
|
@@ -64,7 +64,14 @@ describe("Compaction Hook", () => {
|
|
|
64
64
|
describe("SWARM_COMPACTION_CONTEXT", () => {
|
|
65
65
|
it("contains coordinator instructions", () => {
|
|
66
66
|
expect(SWARM_COMPACTION_CONTEXT).toContain("COORDINATOR");
|
|
67
|
-
expect(SWARM_COMPACTION_CONTEXT).toContain("
|
|
67
|
+
expect(SWARM_COMPACTION_CONTEXT).toContain("You Are The COORDINATOR");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("contains prohibition-first anti-patterns", () => {
|
|
71
|
+
expect(SWARM_COMPACTION_CONTEXT).toContain("NEVER");
|
|
72
|
+
expect(SWARM_COMPACTION_CONTEXT).toContain("edit");
|
|
73
|
+
expect(SWARM_COMPACTION_CONTEXT).toContain("write");
|
|
74
|
+
expect(SWARM_COMPACTION_CONTEXT).toContain("SPAWN A WORKER");
|
|
68
75
|
});
|
|
69
76
|
|
|
70
77
|
it("contains resume instructions", () => {
|
package/src/compaction-hook.ts
CHANGED
|
@@ -68,11 +68,31 @@ function getLog() {
|
|
|
68
68
|
* This is NOT about preserving state for a human - it's about the swarm continuing
|
|
69
69
|
* autonomously after context compression.
|
|
70
70
|
*/
|
|
71
|
-
export const SWARM_COMPACTION_CONTEXT = `## 🐝 SWARM ACTIVE -
|
|
71
|
+
export const SWARM_COMPACTION_CONTEXT = `## 🐝 SWARM ACTIVE - You Are The COORDINATOR
|
|
72
72
|
|
|
73
|
-
|
|
73
|
+
Context was compacted but the swarm is still running. You are the **COORDINATOR**.
|
|
74
74
|
|
|
75
|
-
|
|
75
|
+
### ⛔ NEVER DO THESE (Coordinator Anti-Patterns)
|
|
76
|
+
|
|
77
|
+
**CRITICAL: Coordinators NEVER do implementation work. ALWAYS spawn workers.**
|
|
78
|
+
|
|
79
|
+
- ❌ **NEVER** use \`edit\` or \`write\` tools - SPAWN A WORKER
|
|
80
|
+
- ❌ **NEVER** run tests with \`bash\` - SPAWN A WORKER
|
|
81
|
+
- ❌ **NEVER** implement features yourself - SPAWN A WORKER
|
|
82
|
+
- ❌ **NEVER** "just do it myself to save time" - NO. SPAWN A WORKER.
|
|
83
|
+
- ❌ **NEVER** reserve files with \`swarmmail_reserve\` - Workers reserve files
|
|
84
|
+
|
|
85
|
+
**If you catch yourself about to edit a file, STOP. Use \`swarm_spawn_subtask\` instead.**
|
|
86
|
+
|
|
87
|
+
### ✅ ALWAYS DO THESE (Coordinator Checklist)
|
|
88
|
+
|
|
89
|
+
On resume, execute this checklist IN ORDER:
|
|
90
|
+
|
|
91
|
+
1. \`swarm_status(epic_id="<epic>", project_key="<path>")\` - Get current state
|
|
92
|
+
2. \`swarmmail_inbox(limit=5)\` - Check for agent messages
|
|
93
|
+
3. For completed work: \`swarm_review\` → \`swarm_review_feedback\`
|
|
94
|
+
4. For open subtasks: \`swarm_spawn_subtask\` (NOT "do it yourself")
|
|
95
|
+
5. For blocked work: Investigate, unblock, reassign
|
|
76
96
|
|
|
77
97
|
### Preserve in Summary
|
|
78
98
|
|
|
@@ -89,41 +109,31 @@ Extract from session context:
|
|
|
89
109
|
\`\`\`
|
|
90
110
|
## 🐝 Swarm State
|
|
91
111
|
|
|
92
|
-
**Epic:** <
|
|
112
|
+
**Epic:** <cell-xxx> - <title>
|
|
93
113
|
**Project:** <path>
|
|
94
114
|
**Progress:** X/Y subtasks complete
|
|
95
115
|
|
|
96
116
|
**Active:**
|
|
97
|
-
- <
|
|
117
|
+
- <cell-xxx>: <title> [in_progress] → <agent> working on <files>
|
|
98
118
|
|
|
99
119
|
**Blocked:**
|
|
100
|
-
- <
|
|
120
|
+
- <cell-xxx>: <title> - BLOCKED: <reason>
|
|
101
121
|
|
|
102
122
|
**Completed:**
|
|
103
|
-
- <
|
|
123
|
+
- <cell-xxx>: <title> ✓
|
|
104
124
|
|
|
105
125
|
**Ready to Spawn:**
|
|
106
|
-
- <
|
|
126
|
+
- <cell-xxx>: <title> (files: <...>)
|
|
107
127
|
\`\`\`
|
|
108
128
|
|
|
109
|
-
###
|
|
110
|
-
|
|
111
|
-
1. \`swarm_status(epic_id="<epic>", project_key="<path>")\` - Get current state
|
|
112
|
-
2. \`swarmmail_inbox(limit=5)\` - Check for agent messages
|
|
113
|
-
3. \`swarm_review(project_key, epic_id, task_id, files_touched)\` - Review any completed work
|
|
114
|
-
4. \`swarm_review_feedback(project_key, task_id, worker_id, status, issues)\` - Approve or request changes
|
|
115
|
-
5. **Spawn ready subtasks** - Don't wait, fire them off
|
|
116
|
-
6. **Unblock blocked work** - Resolve dependencies, reassign if needed
|
|
117
|
-
7. **Collect completed work** - Close done subtasks, verify quality
|
|
118
|
-
|
|
119
|
-
### Keep the Swarm Cooking
|
|
129
|
+
### Your Role
|
|
120
130
|
|
|
121
131
|
- **Spawn aggressively** - If a subtask is ready and unblocked, spawn an agent
|
|
122
132
|
- **Monitor actively** - Check status, read messages, respond to blockers
|
|
133
|
+
- **Review work** - Use \`swarm_review\` and \`swarm_review_feedback\` for completed work
|
|
123
134
|
- **Close the loop** - When all subtasks done, verify and close the epic
|
|
124
|
-
- **Don't stop** - The swarm runs until the epic is closed
|
|
125
135
|
|
|
126
|
-
**You are
|
|
136
|
+
**You are the COORDINATOR. You orchestrate. You do NOT implement. Spawn workers.**
|
|
127
137
|
`;
|
|
128
138
|
|
|
129
139
|
/**
|
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for eval-capture coordinator event schemas and session capture
|
|
3
|
+
*/
|
|
4
|
+
import { type Mock, afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
5
|
+
import * as fs from "node:fs";
|
|
6
|
+
import * as os from "node:os";
|
|
7
|
+
import * as path from "node:path";
|
|
8
|
+
import {
|
|
9
|
+
type CoordinatorEvent,
|
|
10
|
+
CoordinatorEventSchema,
|
|
11
|
+
type CoordinatorSession,
|
|
12
|
+
CoordinatorSessionSchema,
|
|
13
|
+
captureCoordinatorEvent,
|
|
14
|
+
saveSession,
|
|
15
|
+
} from "./eval-capture.js";
|
|
16
|
+
|
|
17
|
+
describe("CoordinatorEvent schemas", () => {
|
|
18
|
+
describe("DECISION events", () => {
|
|
19
|
+
test("validates strategy_selected event", () => {
|
|
20
|
+
const event: CoordinatorEvent = {
|
|
21
|
+
session_id: "test-session",
|
|
22
|
+
epic_id: "bd-123",
|
|
23
|
+
timestamp: new Date().toISOString(),
|
|
24
|
+
event_type: "DECISION",
|
|
25
|
+
decision_type: "strategy_selected",
|
|
26
|
+
payload: {
|
|
27
|
+
strategy: "file-based",
|
|
28
|
+
reasoning: "Files are well isolated",
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
expect(() => CoordinatorEventSchema.parse(event)).not.toThrow();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("validates worker_spawned event", () => {
|
|
36
|
+
const event: CoordinatorEvent = {
|
|
37
|
+
session_id: "test-session",
|
|
38
|
+
epic_id: "bd-123",
|
|
39
|
+
timestamp: new Date().toISOString(),
|
|
40
|
+
event_type: "DECISION",
|
|
41
|
+
decision_type: "worker_spawned",
|
|
42
|
+
payload: {
|
|
43
|
+
worker_id: "GreenStorm",
|
|
44
|
+
subtask_id: "bd-123.1",
|
|
45
|
+
files: ["src/test.ts"],
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
expect(() => CoordinatorEventSchema.parse(event)).not.toThrow();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("validates review_completed event", () => {
|
|
53
|
+
const event: CoordinatorEvent = {
|
|
54
|
+
session_id: "test-session",
|
|
55
|
+
epic_id: "bd-123",
|
|
56
|
+
timestamp: new Date().toISOString(),
|
|
57
|
+
event_type: "DECISION",
|
|
58
|
+
decision_type: "review_completed",
|
|
59
|
+
payload: {
|
|
60
|
+
subtask_id: "bd-123.1",
|
|
61
|
+
approved: true,
|
|
62
|
+
issues_found: 0,
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
expect(() => CoordinatorEventSchema.parse(event)).not.toThrow();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("validates decomposition_complete event", () => {
|
|
70
|
+
const event: CoordinatorEvent = {
|
|
71
|
+
session_id: "test-session",
|
|
72
|
+
epic_id: "bd-123",
|
|
73
|
+
timestamp: new Date().toISOString(),
|
|
74
|
+
event_type: "DECISION",
|
|
75
|
+
decision_type: "decomposition_complete",
|
|
76
|
+
payload: {
|
|
77
|
+
subtask_count: 3,
|
|
78
|
+
strategy: "feature-based",
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
expect(() => CoordinatorEventSchema.parse(event)).not.toThrow();
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe("VIOLATION events", () => {
|
|
87
|
+
test("validates coordinator_edited_file event", () => {
|
|
88
|
+
const event: CoordinatorEvent = {
|
|
89
|
+
session_id: "test-session",
|
|
90
|
+
epic_id: "bd-123",
|
|
91
|
+
timestamp: new Date().toISOString(),
|
|
92
|
+
event_type: "VIOLATION",
|
|
93
|
+
violation_type: "coordinator_edited_file",
|
|
94
|
+
payload: {
|
|
95
|
+
file: "src/bad.ts",
|
|
96
|
+
operation: "edit",
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
expect(() => CoordinatorEventSchema.parse(event)).not.toThrow();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("validates coordinator_ran_tests event", () => {
|
|
104
|
+
const event: CoordinatorEvent = {
|
|
105
|
+
session_id: "test-session",
|
|
106
|
+
epic_id: "bd-123",
|
|
107
|
+
timestamp: new Date().toISOString(),
|
|
108
|
+
event_type: "VIOLATION",
|
|
109
|
+
violation_type: "coordinator_ran_tests",
|
|
110
|
+
payload: {
|
|
111
|
+
command: "bun test",
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
expect(() => CoordinatorEventSchema.parse(event)).not.toThrow();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("validates coordinator_reserved_files event", () => {
|
|
119
|
+
const event: CoordinatorEvent = {
|
|
120
|
+
session_id: "test-session",
|
|
121
|
+
epic_id: "bd-123",
|
|
122
|
+
timestamp: new Date().toISOString(),
|
|
123
|
+
event_type: "VIOLATION",
|
|
124
|
+
violation_type: "coordinator_reserved_files",
|
|
125
|
+
payload: {
|
|
126
|
+
files: ["src/auth.ts"],
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
expect(() => CoordinatorEventSchema.parse(event)).not.toThrow();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("validates no_worker_spawned event", () => {
|
|
134
|
+
const event: CoordinatorEvent = {
|
|
135
|
+
session_id: "test-session",
|
|
136
|
+
epic_id: "bd-123",
|
|
137
|
+
timestamp: new Date().toISOString(),
|
|
138
|
+
event_type: "VIOLATION",
|
|
139
|
+
violation_type: "no_worker_spawned",
|
|
140
|
+
payload: {
|
|
141
|
+
subtask_id: "bd-123.1",
|
|
142
|
+
reason: "Did work directly",
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
expect(() => CoordinatorEventSchema.parse(event)).not.toThrow();
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe("OUTCOME events", () => {
|
|
151
|
+
test("validates subtask_success event", () => {
|
|
152
|
+
const event: CoordinatorEvent = {
|
|
153
|
+
session_id: "test-session",
|
|
154
|
+
epic_id: "bd-123",
|
|
155
|
+
timestamp: new Date().toISOString(),
|
|
156
|
+
event_type: "OUTCOME",
|
|
157
|
+
outcome_type: "subtask_success",
|
|
158
|
+
payload: {
|
|
159
|
+
subtask_id: "bd-123.1",
|
|
160
|
+
duration_ms: 45000,
|
|
161
|
+
files_touched: ["src/auth.ts"],
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
expect(() => CoordinatorEventSchema.parse(event)).not.toThrow();
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test("validates subtask_retry event", () => {
|
|
169
|
+
const event: CoordinatorEvent = {
|
|
170
|
+
session_id: "test-session",
|
|
171
|
+
epic_id: "bd-123",
|
|
172
|
+
timestamp: new Date().toISOString(),
|
|
173
|
+
event_type: "OUTCOME",
|
|
174
|
+
outcome_type: "subtask_retry",
|
|
175
|
+
payload: {
|
|
176
|
+
subtask_id: "bd-123.1",
|
|
177
|
+
retry_count: 2,
|
|
178
|
+
reason: "Review rejected",
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
expect(() => CoordinatorEventSchema.parse(event)).not.toThrow();
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test("validates subtask_failed event", () => {
|
|
186
|
+
const event: CoordinatorEvent = {
|
|
187
|
+
session_id: "test-session",
|
|
188
|
+
epic_id: "bd-123",
|
|
189
|
+
timestamp: new Date().toISOString(),
|
|
190
|
+
event_type: "OUTCOME",
|
|
191
|
+
outcome_type: "subtask_failed",
|
|
192
|
+
payload: {
|
|
193
|
+
subtask_id: "bd-123.1",
|
|
194
|
+
error: "Type error in auth.ts",
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
expect(() => CoordinatorEventSchema.parse(event)).not.toThrow();
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test("validates epic_complete event", () => {
|
|
202
|
+
const event: CoordinatorEvent = {
|
|
203
|
+
session_id: "test-session",
|
|
204
|
+
epic_id: "bd-123",
|
|
205
|
+
timestamp: new Date().toISOString(),
|
|
206
|
+
event_type: "OUTCOME",
|
|
207
|
+
outcome_type: "epic_complete",
|
|
208
|
+
payload: {
|
|
209
|
+
success: true,
|
|
210
|
+
total_duration_ms: 180000,
|
|
211
|
+
subtasks_completed: 3,
|
|
212
|
+
},
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
expect(() => CoordinatorEventSchema.parse(event)).not.toThrow();
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
describe("CoordinatorSession schema", () => {
|
|
221
|
+
test("validates complete session", () => {
|
|
222
|
+
const session: CoordinatorSession = {
|
|
223
|
+
session_id: "test-session",
|
|
224
|
+
epic_id: "bd-123",
|
|
225
|
+
start_time: new Date().toISOString(),
|
|
226
|
+
end_time: new Date().toISOString(),
|
|
227
|
+
events: [
|
|
228
|
+
{
|
|
229
|
+
session_id: "test-session",
|
|
230
|
+
epic_id: "bd-123",
|
|
231
|
+
timestamp: new Date().toISOString(),
|
|
232
|
+
event_type: "DECISION",
|
|
233
|
+
decision_type: "strategy_selected",
|
|
234
|
+
payload: { strategy: "file-based" },
|
|
235
|
+
},
|
|
236
|
+
],
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
expect(() => CoordinatorSessionSchema.parse(session)).not.toThrow();
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test("validates session without end_time", () => {
|
|
243
|
+
const session: Partial<CoordinatorSession> = {
|
|
244
|
+
session_id: "test-session",
|
|
245
|
+
epic_id: "bd-123",
|
|
246
|
+
start_time: new Date().toISOString(),
|
|
247
|
+
events: [],
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
expect(() => CoordinatorSessionSchema.parse(session)).not.toThrow();
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
describe("captureCoordinatorEvent", () => {
|
|
255
|
+
let sessionDir: string;
|
|
256
|
+
let sessionId: string;
|
|
257
|
+
|
|
258
|
+
beforeEach(() => {
|
|
259
|
+
sessionDir = path.join(os.homedir(), ".config", "swarm-tools", "sessions");
|
|
260
|
+
sessionId = `test-${Date.now()}`;
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
afterEach(() => {
|
|
264
|
+
// Clean up test session file
|
|
265
|
+
const sessionPath = path.join(sessionDir, `${sessionId}.jsonl`);
|
|
266
|
+
if (fs.existsSync(sessionPath)) {
|
|
267
|
+
fs.unlinkSync(sessionPath);
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test("creates session directory if not exists", () => {
|
|
272
|
+
const event: CoordinatorEvent = {
|
|
273
|
+
session_id: sessionId,
|
|
274
|
+
epic_id: "bd-123",
|
|
275
|
+
timestamp: new Date().toISOString(),
|
|
276
|
+
event_type: "DECISION",
|
|
277
|
+
decision_type: "strategy_selected",
|
|
278
|
+
payload: { strategy: "file-based" },
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
captureCoordinatorEvent(event);
|
|
282
|
+
|
|
283
|
+
expect(fs.existsSync(sessionDir)).toBe(true);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
test("appends event to session file", () => {
|
|
287
|
+
const event: CoordinatorEvent = {
|
|
288
|
+
session_id: sessionId,
|
|
289
|
+
epic_id: "bd-123",
|
|
290
|
+
timestamp: new Date().toISOString(),
|
|
291
|
+
event_type: "DECISION",
|
|
292
|
+
decision_type: "strategy_selected",
|
|
293
|
+
payload: { strategy: "file-based" },
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
captureCoordinatorEvent(event);
|
|
297
|
+
|
|
298
|
+
const sessionPath = path.join(sessionDir, `${sessionId}.jsonl`);
|
|
299
|
+
expect(fs.existsSync(sessionPath)).toBe(true);
|
|
300
|
+
|
|
301
|
+
const content = fs.readFileSync(sessionPath, "utf-8");
|
|
302
|
+
const lines = content.trim().split("\n");
|
|
303
|
+
expect(lines).toHaveLength(1);
|
|
304
|
+
|
|
305
|
+
const parsed = JSON.parse(lines[0]);
|
|
306
|
+
expect(parsed.session_id).toBe(sessionId);
|
|
307
|
+
expect(parsed.event_type).toBe("DECISION");
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
test("appends multiple events to same session", () => {
|
|
311
|
+
const event1: CoordinatorEvent = {
|
|
312
|
+
session_id: sessionId,
|
|
313
|
+
epic_id: "bd-123",
|
|
314
|
+
timestamp: new Date().toISOString(),
|
|
315
|
+
event_type: "DECISION",
|
|
316
|
+
decision_type: "strategy_selected",
|
|
317
|
+
payload: { strategy: "file-based" },
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
const event2: CoordinatorEvent = {
|
|
321
|
+
session_id: sessionId,
|
|
322
|
+
epic_id: "bd-123",
|
|
323
|
+
timestamp: new Date().toISOString(),
|
|
324
|
+
event_type: "VIOLATION",
|
|
325
|
+
violation_type: "coordinator_edited_file",
|
|
326
|
+
payload: { file: "src/bad.ts" },
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
captureCoordinatorEvent(event1);
|
|
330
|
+
captureCoordinatorEvent(event2);
|
|
331
|
+
|
|
332
|
+
const sessionPath = path.join(sessionDir, `${sessionId}.jsonl`);
|
|
333
|
+
const content = fs.readFileSync(sessionPath, "utf-8");
|
|
334
|
+
const lines = content.trim().split("\n");
|
|
335
|
+
expect(lines).toHaveLength(2);
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
describe("saveSession", () => {
|
|
340
|
+
let sessionDir: string;
|
|
341
|
+
let sessionId: string;
|
|
342
|
+
|
|
343
|
+
beforeEach(() => {
|
|
344
|
+
sessionDir = path.join(os.homedir(), ".config", "swarm-tools", "sessions");
|
|
345
|
+
sessionId = `test-${Date.now()}`;
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
afterEach(() => {
|
|
349
|
+
// Clean up test session file
|
|
350
|
+
const sessionPath = path.join(sessionDir, `${sessionId}.jsonl`);
|
|
351
|
+
if (fs.existsSync(sessionPath)) {
|
|
352
|
+
fs.unlinkSync(sessionPath);
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
test("wraps events in session structure", () => {
|
|
357
|
+
// Capture some events
|
|
358
|
+
const event1: CoordinatorEvent = {
|
|
359
|
+
session_id: sessionId,
|
|
360
|
+
epic_id: "bd-123",
|
|
361
|
+
timestamp: new Date().toISOString(),
|
|
362
|
+
event_type: "DECISION",
|
|
363
|
+
decision_type: "strategy_selected",
|
|
364
|
+
payload: { strategy: "file-based" },
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
captureCoordinatorEvent(event1);
|
|
368
|
+
|
|
369
|
+
// Save session
|
|
370
|
+
const session = saveSession({
|
|
371
|
+
session_id: sessionId,
|
|
372
|
+
epic_id: "bd-123",
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
expect(session).toBeDefined();
|
|
376
|
+
expect(session.session_id).toBe(sessionId);
|
|
377
|
+
expect(session.events).toHaveLength(1);
|
|
378
|
+
expect(session.start_time).toBeDefined();
|
|
379
|
+
expect(session.end_time).toBeDefined();
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
test("returns null if session file does not exist", () => {
|
|
383
|
+
const session = saveSession({
|
|
384
|
+
session_id: "nonexistent",
|
|
385
|
+
epic_id: "bd-999",
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
expect(session).toBeNull();
|
|
389
|
+
});
|
|
390
|
+
});
|