opencode-swarm-plugin 0.35.0 → 0.36.1
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 +4 -4
- package/.hive/memories.jsonl +274 -1
- package/.turbo/turbo-build.log +4 -4
- package/.turbo/turbo-test.log +307 -307
- package/CHANGELOG.md +133 -0
- package/bin/swarm.ts +234 -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 +747 -34
- package/package.json +2 -2
- package/src/compaction-hook.test.ts +234 -281
- package/src/compaction-hook.ts +221 -63
- package/src/eval-capture.test.ts +390 -0
- package/src/eval-capture.ts +168 -10
- package/src/index.ts +89 -2
- package/src/learning.integration.test.ts +0 -2
- package/src/planning-guardrails.test.ts +387 -2
- package/src/planning-guardrails.ts +289 -0
- package/src/plugin.ts +10 -10
- package/src/schemas/task.ts +0 -1
- package/src/swarm-decompose.ts +21 -8
- package/src/swarm-orchestrate.ts +44 -0
- package/src/swarm-prompts.ts +20 -0
- package/src/swarm-review.ts +41 -0
- package/src/swarm.integration.test.ts +0 -40
|
@@ -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
|
+
});
|
package/src/eval-capture.ts
CHANGED
|
@@ -9,12 +9,15 @@
|
|
|
9
9
|
* 2. swarm_complete captures: outcome signals per subtask
|
|
10
10
|
* 3. swarm_record_outcome captures: learning signals
|
|
11
11
|
* 4. Human feedback (optional): accept/reject/modify
|
|
12
|
+
* 5. Coordinator events: decisions, violations, outcomes
|
|
13
|
+
* 6. Session capture: full coordinator session to ~/.config/swarm-tools/sessions/
|
|
12
14
|
*
|
|
13
15
|
* @module eval-capture
|
|
14
16
|
*/
|
|
17
|
+
import * as fs from "node:fs";
|
|
18
|
+
import * as os from "node:os";
|
|
19
|
+
import * as path from "node:path";
|
|
15
20
|
import { z } from "zod";
|
|
16
|
-
import * as fs from "fs";
|
|
17
|
-
import * as path from "path";
|
|
18
21
|
|
|
19
22
|
// ============================================================================
|
|
20
23
|
// Schemas
|
|
@@ -63,8 +66,8 @@ export const EvalRecordSchema = z.object({
|
|
|
63
66
|
context: z.string().optional(),
|
|
64
67
|
/** Strategy used for decomposition */
|
|
65
68
|
strategy: z.enum(["file-based", "feature-based", "risk-based", "auto"]),
|
|
66
|
-
/**
|
|
67
|
-
|
|
69
|
+
/** Number of subtasks generated */
|
|
70
|
+
subtask_count: z.number().int().min(1),
|
|
68
71
|
|
|
69
72
|
// OUTPUT (the decomposition)
|
|
70
73
|
/** Epic title */
|
|
@@ -119,6 +122,67 @@ export type PartialEvalRecord = Partial<EvalRecord> & {
|
|
|
119
122
|
task: string;
|
|
120
123
|
};
|
|
121
124
|
|
|
125
|
+
/**
|
|
126
|
+
* Coordinator Event - captures coordinator decisions, violations, and outcomes
|
|
127
|
+
*/
|
|
128
|
+
export const CoordinatorEventSchema = z.discriminatedUnion("event_type", [
|
|
129
|
+
// DECISION events
|
|
130
|
+
z.object({
|
|
131
|
+
session_id: z.string(),
|
|
132
|
+
epic_id: z.string(),
|
|
133
|
+
timestamp: z.string(),
|
|
134
|
+
event_type: z.literal("DECISION"),
|
|
135
|
+
decision_type: z.enum([
|
|
136
|
+
"strategy_selected",
|
|
137
|
+
"worker_spawned",
|
|
138
|
+
"review_completed",
|
|
139
|
+
"decomposition_complete",
|
|
140
|
+
]),
|
|
141
|
+
payload: z.any(),
|
|
142
|
+
}),
|
|
143
|
+
// VIOLATION events
|
|
144
|
+
z.object({
|
|
145
|
+
session_id: z.string(),
|
|
146
|
+
epic_id: z.string(),
|
|
147
|
+
timestamp: z.string(),
|
|
148
|
+
event_type: z.literal("VIOLATION"),
|
|
149
|
+
violation_type: z.enum([
|
|
150
|
+
"coordinator_edited_file",
|
|
151
|
+
"coordinator_ran_tests",
|
|
152
|
+
"coordinator_reserved_files",
|
|
153
|
+
"no_worker_spawned",
|
|
154
|
+
]),
|
|
155
|
+
payload: z.any(),
|
|
156
|
+
}),
|
|
157
|
+
// OUTCOME events
|
|
158
|
+
z.object({
|
|
159
|
+
session_id: z.string(),
|
|
160
|
+
epic_id: z.string(),
|
|
161
|
+
timestamp: z.string(),
|
|
162
|
+
event_type: z.literal("OUTCOME"),
|
|
163
|
+
outcome_type: z.enum([
|
|
164
|
+
"subtask_success",
|
|
165
|
+
"subtask_retry",
|
|
166
|
+
"subtask_failed",
|
|
167
|
+
"epic_complete",
|
|
168
|
+
]),
|
|
169
|
+
payload: z.any(),
|
|
170
|
+
}),
|
|
171
|
+
]);
|
|
172
|
+
export type CoordinatorEvent = z.infer<typeof CoordinatorEventSchema>;
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Coordinator Session - wraps a full coordinator session
|
|
176
|
+
*/
|
|
177
|
+
export const CoordinatorSessionSchema = z.object({
|
|
178
|
+
session_id: z.string(),
|
|
179
|
+
epic_id: z.string(),
|
|
180
|
+
start_time: z.string(),
|
|
181
|
+
end_time: z.string().optional(),
|
|
182
|
+
events: z.array(CoordinatorEventSchema),
|
|
183
|
+
});
|
|
184
|
+
export type CoordinatorSession = z.infer<typeof CoordinatorSessionSchema>;
|
|
185
|
+
|
|
122
186
|
// ============================================================================
|
|
123
187
|
// Storage
|
|
124
188
|
// ============================================================================
|
|
@@ -155,7 +219,7 @@ export function appendEvalRecord(
|
|
|
155
219
|
): void {
|
|
156
220
|
ensureEvalDataDir(projectPath);
|
|
157
221
|
const evalPath = getEvalDataPath(projectPath);
|
|
158
|
-
const line = JSON.stringify(record)
|
|
222
|
+
const line = `${JSON.stringify(record)}\n`;
|
|
159
223
|
fs.appendFileSync(evalPath, line, "utf-8");
|
|
160
224
|
}
|
|
161
225
|
|
|
@@ -211,7 +275,7 @@ export function updateEvalRecord(
|
|
|
211
275
|
|
|
212
276
|
// Rewrite the file
|
|
213
277
|
const evalPath = getEvalDataPath(projectPath);
|
|
214
|
-
const content = records.map((r) => JSON.stringify(r)).join("\n")
|
|
278
|
+
const content = `${records.map((r) => JSON.stringify(r)).join("\n")}\n`;
|
|
215
279
|
fs.writeFileSync(evalPath, content, "utf-8");
|
|
216
280
|
|
|
217
281
|
return true;
|
|
@@ -238,7 +302,6 @@ export function captureDecomposition(params: {
|
|
|
238
302
|
task: string;
|
|
239
303
|
context?: string;
|
|
240
304
|
strategy: "file-based" | "feature-based" | "risk-based" | "auto";
|
|
241
|
-
maxSubtasks: number;
|
|
242
305
|
epicTitle: string;
|
|
243
306
|
epicDescription?: string;
|
|
244
307
|
subtasks: Array<{
|
|
@@ -256,7 +319,7 @@ export function captureDecomposition(params: {
|
|
|
256
319
|
task: params.task,
|
|
257
320
|
context: params.context,
|
|
258
321
|
strategy: params.strategy,
|
|
259
|
-
|
|
322
|
+
subtask_count: params.subtasks.length,
|
|
260
323
|
epic_title: params.epicTitle,
|
|
261
324
|
epic_description: params.epicDescription,
|
|
262
325
|
subtasks: params.subtasks,
|
|
@@ -409,7 +472,7 @@ export function exportForEvalite(projectPath: string): Array<{
|
|
|
409
472
|
input: { task: string; context?: string };
|
|
410
473
|
expected: {
|
|
411
474
|
minSubtasks: number;
|
|
412
|
-
|
|
475
|
+
subtaskCount: number;
|
|
413
476
|
requiredFiles?: string[];
|
|
414
477
|
overallSuccess?: boolean;
|
|
415
478
|
};
|
|
@@ -426,7 +489,7 @@ export function exportForEvalite(projectPath: string): Array<{
|
|
|
426
489
|
},
|
|
427
490
|
expected: {
|
|
428
491
|
minSubtasks: 2,
|
|
429
|
-
|
|
492
|
+
subtaskCount: record.subtask_count,
|
|
430
493
|
requiredFiles: record.subtasks.flatMap((s) => s.files),
|
|
431
494
|
overallSuccess: record.overall_success,
|
|
432
495
|
},
|
|
@@ -485,3 +548,98 @@ export function getEvalDataStats(projectPath: string): {
|
|
|
485
548
|
avgTimeBalance,
|
|
486
549
|
};
|
|
487
550
|
}
|
|
551
|
+
|
|
552
|
+
// ============================================================================
|
|
553
|
+
// Coordinator Session Capture
|
|
554
|
+
// ============================================================================
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* Get the session directory path
|
|
558
|
+
*/
|
|
559
|
+
export function getSessionDir(): string {
|
|
560
|
+
return path.join(os.homedir(), ".config", "swarm-tools", "sessions");
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Get the session file path for a session ID
|
|
565
|
+
*/
|
|
566
|
+
export function getSessionPath(sessionId: string): string {
|
|
567
|
+
return path.join(getSessionDir(), `${sessionId}.jsonl`);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Ensure the session directory exists
|
|
572
|
+
*/
|
|
573
|
+
export function ensureSessionDir(): void {
|
|
574
|
+
const sessionDir = getSessionDir();
|
|
575
|
+
if (!fs.existsSync(sessionDir)) {
|
|
576
|
+
fs.mkdirSync(sessionDir, { recursive: true });
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* Capture a coordinator event to the session file
|
|
582
|
+
*
|
|
583
|
+
* Appends the event as a JSONL line to ~/.config/swarm-tools/sessions/{session_id}.jsonl
|
|
584
|
+
*/
|
|
585
|
+
export function captureCoordinatorEvent(event: CoordinatorEvent): void {
|
|
586
|
+
// Validate event
|
|
587
|
+
CoordinatorEventSchema.parse(event);
|
|
588
|
+
|
|
589
|
+
// Ensure directory exists
|
|
590
|
+
ensureSessionDir();
|
|
591
|
+
|
|
592
|
+
// Append to session file
|
|
593
|
+
const sessionPath = getSessionPath(event.session_id);
|
|
594
|
+
const line = `${JSON.stringify(event)}\n`;
|
|
595
|
+
fs.appendFileSync(sessionPath, line, "utf-8");
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* Read all events from a session file
|
|
600
|
+
*/
|
|
601
|
+
export function readSessionEvents(sessionId: string): CoordinatorEvent[] {
|
|
602
|
+
const sessionPath = getSessionPath(sessionId);
|
|
603
|
+
if (!fs.existsSync(sessionPath)) {
|
|
604
|
+
return [];
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
const content = fs.readFileSync(sessionPath, "utf-8");
|
|
608
|
+
const lines = content.trim().split("\n").filter(Boolean);
|
|
609
|
+
|
|
610
|
+
return lines.map((line) => {
|
|
611
|
+
const parsed = JSON.parse(line);
|
|
612
|
+
return CoordinatorEventSchema.parse(parsed);
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* Save a session - wraps all events in a CoordinatorSession structure
|
|
618
|
+
*
|
|
619
|
+
* Reads all events from the session file and wraps them in a session object.
|
|
620
|
+
* Returns null if the session file doesn't exist.
|
|
621
|
+
*/
|
|
622
|
+
export function saveSession(params: {
|
|
623
|
+
session_id: string;
|
|
624
|
+
epic_id: string;
|
|
625
|
+
}): CoordinatorSession | null {
|
|
626
|
+
const events = readSessionEvents(params.session_id);
|
|
627
|
+
if (events.length === 0) {
|
|
628
|
+
return null;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// Get timestamps from events
|
|
632
|
+
const timestamps = events.map((e) => new Date(e.timestamp).getTime());
|
|
633
|
+
const startTime = new Date(Math.min(...timestamps)).toISOString();
|
|
634
|
+
const endTime = new Date(Math.max(...timestamps)).toISOString();
|
|
635
|
+
|
|
636
|
+
const session: CoordinatorSession = {
|
|
637
|
+
session_id: params.session_id,
|
|
638
|
+
epic_id: params.epic_id,
|
|
639
|
+
start_time: startTime,
|
|
640
|
+
end_time: endTime,
|
|
641
|
+
events,
|
|
642
|
+
};
|
|
643
|
+
|
|
644
|
+
return session;
|
|
645
|
+
}
|