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
package/src/index.ts
CHANGED
|
@@ -57,7 +57,13 @@ import {
|
|
|
57
57
|
import {
|
|
58
58
|
analyzeTodoWrite,
|
|
59
59
|
shouldAnalyzeTool,
|
|
60
|
+
detectCoordinatorViolation,
|
|
61
|
+
isInCoordinatorContext,
|
|
62
|
+
getCoordinatorContext,
|
|
63
|
+
setCoordinatorContext,
|
|
64
|
+
clearCoordinatorContext,
|
|
60
65
|
} from "./planning-guardrails";
|
|
66
|
+
import { createCompactionHook } from "./compaction-hook";
|
|
61
67
|
|
|
62
68
|
/**
|
|
63
69
|
* OpenCode Swarm Plugin
|
|
@@ -77,10 +83,10 @@ import {
|
|
|
77
83
|
* @param input - Plugin context from OpenCode
|
|
78
84
|
* @returns Plugin hooks including tools, events, and tool execution hooks
|
|
79
85
|
*/
|
|
80
|
-
|
|
86
|
+
const SwarmPlugin: Plugin = async (
|
|
81
87
|
input: PluginInput,
|
|
82
88
|
): Promise<Hooks> => {
|
|
83
|
-
const { $, directory } = input;
|
|
89
|
+
const { $, directory, client } = input;
|
|
84
90
|
|
|
85
91
|
// Set the working directory for hive commands
|
|
86
92
|
// This ensures hive operations run in the project directory, not ~/.config/opencode
|
|
@@ -189,6 +195,8 @@ export const SwarmPlugin: Plugin = async (
|
|
|
189
195
|
*
|
|
190
196
|
* Warns when agents are about to make planning mistakes:
|
|
191
197
|
* - Using todowrite for multi-file implementation (should use swarm)
|
|
198
|
+
* - Coordinator editing files directly (should spawn workers)
|
|
199
|
+
* - Coordinator running tests (workers should run tests)
|
|
192
200
|
*/
|
|
193
201
|
"tool.execute.before": async (input, output) => {
|
|
194
202
|
const toolName = input.tool;
|
|
@@ -200,6 +208,36 @@ export const SwarmPlugin: Plugin = async (
|
|
|
200
208
|
console.warn(`[swarm-plugin] ${analysis.warning}`);
|
|
201
209
|
}
|
|
202
210
|
}
|
|
211
|
+
|
|
212
|
+
// Check for coordinator violations when in coordinator context
|
|
213
|
+
if (isInCoordinatorContext()) {
|
|
214
|
+
const ctx = getCoordinatorContext();
|
|
215
|
+
const violation = detectCoordinatorViolation({
|
|
216
|
+
sessionId: ctx.sessionId || "unknown",
|
|
217
|
+
epicId: ctx.epicId || "unknown",
|
|
218
|
+
toolName,
|
|
219
|
+
toolArgs: output.args as Record<string, unknown>,
|
|
220
|
+
agentContext: "coordinator",
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
if (violation.isViolation) {
|
|
224
|
+
console.warn(`[swarm-plugin] ${violation.message}`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Activate coordinator context when swarm tools are used
|
|
229
|
+
if (toolName === "hive_create_epic" || toolName === "swarm_decompose") {
|
|
230
|
+
setCoordinatorContext({
|
|
231
|
+
isCoordinator: true,
|
|
232
|
+
sessionId: input.sessionID,
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Capture epic ID when epic is created
|
|
237
|
+
if (toolName === "hive_create_epic" && output.args) {
|
|
238
|
+
const args = output.args as { epic_title?: string };
|
|
239
|
+
// Epic ID will be set after execution in tool.execute.after
|
|
240
|
+
}
|
|
203
241
|
},
|
|
204
242
|
|
|
205
243
|
/**
|
|
@@ -257,10 +295,59 @@ export const SwarmPlugin: Plugin = async (
|
|
|
257
295
|
await releaseReservations();
|
|
258
296
|
}
|
|
259
297
|
|
|
298
|
+
// Capture epic ID when epic is created (for coordinator context)
|
|
299
|
+
if (toolName === "hive_create_epic" && output.output) {
|
|
300
|
+
try {
|
|
301
|
+
const result = JSON.parse(output.output);
|
|
302
|
+
if (result.epic?.id) {
|
|
303
|
+
setCoordinatorContext({
|
|
304
|
+
isCoordinator: true,
|
|
305
|
+
epicId: result.epic.id,
|
|
306
|
+
sessionId: input.sessionID,
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
} catch {
|
|
310
|
+
// Parsing failed - ignore
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Clear coordinator context when epic is closed
|
|
315
|
+
if (toolName === "hive_close" && output.output && isInCoordinatorContext()) {
|
|
316
|
+
const ctx = getCoordinatorContext();
|
|
317
|
+
try {
|
|
318
|
+
// Check if the closed cell is the active epic
|
|
319
|
+
const result = JSON.parse(output.output);
|
|
320
|
+
if (result.id === ctx.epicId) {
|
|
321
|
+
clearCoordinatorContext();
|
|
322
|
+
}
|
|
323
|
+
} catch {
|
|
324
|
+
// Parsing failed - ignore
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
260
328
|
// Note: hive_sync should be called explicitly at session end
|
|
261
329
|
// Auto-sync was removed because bd CLI is deprecated
|
|
262
330
|
// The hive_sync tool handles flushing to JSONL and git commit/push
|
|
263
331
|
},
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Compaction hook for swarm context preservation
|
|
335
|
+
*
|
|
336
|
+
* When OpenCode compacts session context, this hook injects swarm state
|
|
337
|
+
* to ensure coordinators can resume orchestration seamlessly.
|
|
338
|
+
*
|
|
339
|
+
* Uses SDK client to scan actual session messages for precise swarm state
|
|
340
|
+
* (epic IDs, subtask status, agent names) rather than relying solely on
|
|
341
|
+
* heuristic detection from hive/swarm-mail.
|
|
342
|
+
*
|
|
343
|
+
* Note: This hook is experimental and may not be in the published Hooks type yet.
|
|
344
|
+
*/
|
|
345
|
+
"experimental.session.compacting": createCompactionHook(client),
|
|
346
|
+
} as Hooks & {
|
|
347
|
+
"experimental.session.compacting"?: (
|
|
348
|
+
input: { sessionID: string },
|
|
349
|
+
output: { context: string[] },
|
|
350
|
+
) => Promise<void>;
|
|
264
351
|
};
|
|
265
352
|
};
|
|
266
353
|
|
|
@@ -976,7 +976,6 @@ describe("Swarm Tool Integrations", () => {
|
|
|
976
976
|
const result = await swarm_decompose.execute(
|
|
977
977
|
{
|
|
978
978
|
task: "Add user authentication",
|
|
979
|
-
max_subtasks: 3,
|
|
980
979
|
query_cass: true,
|
|
981
980
|
},
|
|
982
981
|
mockContext,
|
|
@@ -992,7 +991,6 @@ describe("Swarm Tool Integrations", () => {
|
|
|
992
991
|
const result = await swarm_decompose.execute(
|
|
993
992
|
{
|
|
994
993
|
task: "Add user authentication",
|
|
995
|
-
max_subtasks: 3,
|
|
996
994
|
query_cass: false,
|
|
997
995
|
},
|
|
998
996
|
mockContext,
|
|
@@ -1,5 +1,17 @@
|
|
|
1
|
-
import { describe, it, expect } from "bun:test";
|
|
2
|
-
import {
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
analyzeTodoWrite,
|
|
4
|
+
shouldAnalyzeTool,
|
|
5
|
+
detectCoordinatorViolation,
|
|
6
|
+
setCoordinatorContext,
|
|
7
|
+
getCoordinatorContext,
|
|
8
|
+
clearCoordinatorContext,
|
|
9
|
+
isInCoordinatorContext,
|
|
10
|
+
type ViolationDetectionResult,
|
|
11
|
+
} from "./planning-guardrails";
|
|
12
|
+
import * as fs from "node:fs";
|
|
13
|
+
import * as path from "node:path";
|
|
14
|
+
import * as os from "os";
|
|
3
15
|
|
|
4
16
|
describe("planning-guardrails", () => {
|
|
5
17
|
describe("shouldAnalyzeTool", () => {
|
|
@@ -103,4 +115,377 @@ describe("planning-guardrails", () => {
|
|
|
103
115
|
expect(result.totalCount).toBe(6);
|
|
104
116
|
});
|
|
105
117
|
});
|
|
118
|
+
|
|
119
|
+
describe("detectCoordinatorViolation", () => {
|
|
120
|
+
const sessionId = "test-session-123";
|
|
121
|
+
const epicId = "test-epic-456";
|
|
122
|
+
|
|
123
|
+
// Clean up session files after tests
|
|
124
|
+
afterEach(() => {
|
|
125
|
+
const sessionDir = path.join(os.homedir(), ".config", "swarm-tools", "sessions");
|
|
126
|
+
const sessionPath = path.join(sessionDir, `${sessionId}.jsonl`);
|
|
127
|
+
if (fs.existsSync(sessionPath)) {
|
|
128
|
+
fs.unlinkSync(sessionPath);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe("coordinator_edited_file violation", () => {
|
|
133
|
+
it("detects Edit tool call from coordinator", () => {
|
|
134
|
+
const result = detectCoordinatorViolation({
|
|
135
|
+
sessionId,
|
|
136
|
+
epicId,
|
|
137
|
+
toolName: "edit",
|
|
138
|
+
toolArgs: { filePath: "/path/to/file.ts", oldString: "old", newString: "new" },
|
|
139
|
+
agentContext: "coordinator",
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
expect(result.isViolation).toBe(true);
|
|
143
|
+
expect(result.violationType).toBe("coordinator_edited_file");
|
|
144
|
+
expect(result.message).toContain("Coordinators should spawn workers");
|
|
145
|
+
expect(result.payload.tool).toBe("edit");
|
|
146
|
+
expect(result.payload.file).toBe("/path/to/file.ts");
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("detects Write tool call from coordinator", () => {
|
|
150
|
+
const result = detectCoordinatorViolation({
|
|
151
|
+
sessionId,
|
|
152
|
+
epicId,
|
|
153
|
+
toolName: "write",
|
|
154
|
+
toolArgs: { filePath: "/path/to/new-file.ts", content: "export const foo = 1;" },
|
|
155
|
+
agentContext: "coordinator",
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
expect(result.isViolation).toBe(true);
|
|
159
|
+
expect(result.violationType).toBe("coordinator_edited_file");
|
|
160
|
+
expect(result.message).toContain("Coordinators should spawn workers");
|
|
161
|
+
expect(result.payload.tool).toBe("write");
|
|
162
|
+
expect(result.payload.file).toBe("/path/to/new-file.ts");
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("does not detect edit from worker agent", () => {
|
|
166
|
+
const result = detectCoordinatorViolation({
|
|
167
|
+
sessionId,
|
|
168
|
+
epicId,
|
|
169
|
+
toolName: "edit",
|
|
170
|
+
toolArgs: { filePath: "/path/to/file.ts", oldString: "old", newString: "new" },
|
|
171
|
+
agentContext: "worker",
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
expect(result.isViolation).toBe(false);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("does not detect Read tool (read-only)", () => {
|
|
178
|
+
const result = detectCoordinatorViolation({
|
|
179
|
+
sessionId,
|
|
180
|
+
epicId,
|
|
181
|
+
toolName: "read",
|
|
182
|
+
toolArgs: { filePath: "/path/to/file.ts" },
|
|
183
|
+
agentContext: "coordinator",
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
expect(result.isViolation).toBe(false);
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
describe("coordinator_ran_tests violation", () => {
|
|
191
|
+
it("detects bash test execution from coordinator", () => {
|
|
192
|
+
const result = detectCoordinatorViolation({
|
|
193
|
+
sessionId,
|
|
194
|
+
epicId,
|
|
195
|
+
toolName: "bash",
|
|
196
|
+
toolArgs: { command: "bun test src/module.test.ts" },
|
|
197
|
+
agentContext: "coordinator",
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
expect(result.isViolation).toBe(true);
|
|
201
|
+
expect(result.violationType).toBe("coordinator_ran_tests");
|
|
202
|
+
expect(result.message).toContain("Workers run tests");
|
|
203
|
+
expect(result.payload.command).toContain("bun test");
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("detects npm test from coordinator", () => {
|
|
207
|
+
const result = detectCoordinatorViolation({
|
|
208
|
+
sessionId,
|
|
209
|
+
epicId,
|
|
210
|
+
toolName: "bash",
|
|
211
|
+
toolArgs: { command: "npm run test:unit" },
|
|
212
|
+
agentContext: "coordinator",
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
expect(result.isViolation).toBe(true);
|
|
216
|
+
expect(result.violationType).toBe("coordinator_ran_tests");
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it("detects jest from coordinator", () => {
|
|
220
|
+
const result = detectCoordinatorViolation({
|
|
221
|
+
sessionId,
|
|
222
|
+
epicId,
|
|
223
|
+
toolName: "bash",
|
|
224
|
+
toolArgs: { command: "jest --coverage" },
|
|
225
|
+
agentContext: "coordinator",
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
expect(result.isViolation).toBe(true);
|
|
229
|
+
expect(result.violationType).toBe("coordinator_ran_tests");
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it("does not detect non-test bash commands", () => {
|
|
233
|
+
const result = detectCoordinatorViolation({
|
|
234
|
+
sessionId,
|
|
235
|
+
epicId,
|
|
236
|
+
toolName: "bash",
|
|
237
|
+
toolArgs: { command: "git status" },
|
|
238
|
+
agentContext: "coordinator",
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
expect(result.isViolation).toBe(false);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it("does not detect test execution from worker", () => {
|
|
245
|
+
const result = detectCoordinatorViolation({
|
|
246
|
+
sessionId,
|
|
247
|
+
epicId,
|
|
248
|
+
toolName: "bash",
|
|
249
|
+
toolArgs: { command: "bun test" },
|
|
250
|
+
agentContext: "worker",
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
expect(result.isViolation).toBe(false);
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
describe("coordinator_reserved_files violation", () => {
|
|
258
|
+
it("detects swarmmail_reserve from coordinator", () => {
|
|
259
|
+
const result = detectCoordinatorViolation({
|
|
260
|
+
sessionId,
|
|
261
|
+
epicId,
|
|
262
|
+
toolName: "swarmmail_reserve",
|
|
263
|
+
toolArgs: { paths: ["src/auth/**"], reason: "Working on auth" },
|
|
264
|
+
agentContext: "coordinator",
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
expect(result.isViolation).toBe(true);
|
|
268
|
+
expect(result.violationType).toBe("coordinator_reserved_files");
|
|
269
|
+
expect(result.message).toContain("Workers reserve files");
|
|
270
|
+
expect(result.payload.paths).toEqual(["src/auth/**"]);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it("detects agentmail_reserve from coordinator", () => {
|
|
274
|
+
const result = detectCoordinatorViolation({
|
|
275
|
+
sessionId,
|
|
276
|
+
epicId,
|
|
277
|
+
toolName: "agentmail_reserve",
|
|
278
|
+
toolArgs: { paths: ["src/lib/**"], reason: "Refactoring" },
|
|
279
|
+
agentContext: "coordinator",
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
expect(result.isViolation).toBe(true);
|
|
283
|
+
expect(result.violationType).toBe("coordinator_reserved_files");
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it("does not detect reserve from worker", () => {
|
|
287
|
+
const result = detectCoordinatorViolation({
|
|
288
|
+
sessionId,
|
|
289
|
+
epicId,
|
|
290
|
+
toolName: "swarmmail_reserve",
|
|
291
|
+
toolArgs: { paths: ["src/auth/**"], reason: "Working on auth" },
|
|
292
|
+
agentContext: "worker",
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
expect(result.isViolation).toBe(false);
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
describe("no_worker_spawned violation", () => {
|
|
300
|
+
it("detects no spawn after decomposition", () => {
|
|
301
|
+
const result = detectCoordinatorViolation({
|
|
302
|
+
sessionId,
|
|
303
|
+
epicId,
|
|
304
|
+
toolName: "hive_create_epic",
|
|
305
|
+
toolArgs: {
|
|
306
|
+
epic_title: "Add feature",
|
|
307
|
+
subtasks: [
|
|
308
|
+
{ title: "Task 1", files: ["a.ts"] },
|
|
309
|
+
{ title: "Task 2", files: ["b.ts"] },
|
|
310
|
+
],
|
|
311
|
+
},
|
|
312
|
+
agentContext: "coordinator",
|
|
313
|
+
checkNoSpawn: true,
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
expect(result.isViolation).toBe(true);
|
|
317
|
+
expect(result.violationType).toBe("no_worker_spawned");
|
|
318
|
+
expect(result.message).toContain("decomposition without spawning");
|
|
319
|
+
expect(result.payload.epic_title).toBe("Add feature");
|
|
320
|
+
expect(result.payload.subtask_count).toBe(2);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it("does not flag if workers were spawned", () => {
|
|
324
|
+
const result = detectCoordinatorViolation({
|
|
325
|
+
sessionId,
|
|
326
|
+
epicId,
|
|
327
|
+
toolName: "hive_create_epic",
|
|
328
|
+
toolArgs: {
|
|
329
|
+
epic_title: "Add feature",
|
|
330
|
+
subtasks: [
|
|
331
|
+
{ title: "Task 1", files: ["a.ts"] },
|
|
332
|
+
{ title: "Task 2", files: ["b.ts"] },
|
|
333
|
+
],
|
|
334
|
+
},
|
|
335
|
+
agentContext: "coordinator",
|
|
336
|
+
checkNoSpawn: false, // Workers were spawned
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
expect(result.isViolation).toBe(false);
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it("does not flag from worker agent", () => {
|
|
343
|
+
const result = detectCoordinatorViolation({
|
|
344
|
+
sessionId,
|
|
345
|
+
epicId,
|
|
346
|
+
toolName: "hive_create_epic",
|
|
347
|
+
toolArgs: {
|
|
348
|
+
epic_title: "Add feature",
|
|
349
|
+
subtasks: [{ title: "Task 1", files: ["a.ts"] }],
|
|
350
|
+
},
|
|
351
|
+
agentContext: "worker",
|
|
352
|
+
checkNoSpawn: true,
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
expect(result.isViolation).toBe(false);
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
describe("event capture integration", () => {
|
|
360
|
+
it("captures violation event to session file when violation detected", () => {
|
|
361
|
+
const result = detectCoordinatorViolation({
|
|
362
|
+
sessionId,
|
|
363
|
+
epicId,
|
|
364
|
+
toolName: "edit",
|
|
365
|
+
toolArgs: { filePath: "/test.ts", oldString: "a", newString: "b" },
|
|
366
|
+
agentContext: "coordinator",
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
expect(result.isViolation).toBe(true);
|
|
370
|
+
|
|
371
|
+
// Verify event was written to session file
|
|
372
|
+
const sessionDir = path.join(os.homedir(), ".config", "swarm-tools", "sessions");
|
|
373
|
+
const sessionPath = path.join(sessionDir, `${sessionId}.jsonl`);
|
|
374
|
+
expect(fs.existsSync(sessionPath)).toBe(true);
|
|
375
|
+
|
|
376
|
+
const content = fs.readFileSync(sessionPath, "utf-8");
|
|
377
|
+
const lines = content.trim().split("\n");
|
|
378
|
+
expect(lines.length).toBe(1);
|
|
379
|
+
|
|
380
|
+
const event = JSON.parse(lines[0]);
|
|
381
|
+
expect(event.event_type).toBe("VIOLATION");
|
|
382
|
+
expect(event.violation_type).toBe("coordinator_edited_file");
|
|
383
|
+
expect(event.session_id).toBe(sessionId);
|
|
384
|
+
expect(event.epic_id).toBe(epicId);
|
|
385
|
+
expect(event.payload.tool).toBe("edit");
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
it("does not capture event when no violation", () => {
|
|
389
|
+
const result = detectCoordinatorViolation({
|
|
390
|
+
sessionId,
|
|
391
|
+
epicId,
|
|
392
|
+
toolName: "read",
|
|
393
|
+
toolArgs: { filePath: "/test.ts" },
|
|
394
|
+
agentContext: "coordinator",
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
expect(result.isViolation).toBe(false);
|
|
398
|
+
|
|
399
|
+
// Verify no session file created
|
|
400
|
+
const sessionDir = path.join(os.homedir(), ".config", "swarm-tools", "sessions");
|
|
401
|
+
const sessionPath = path.join(sessionDir, `${sessionId}.jsonl`);
|
|
402
|
+
expect(fs.existsSync(sessionPath)).toBe(false);
|
|
403
|
+
});
|
|
404
|
+
});
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
describe("coordinator context", () => {
|
|
408
|
+
beforeEach(() => {
|
|
409
|
+
clearCoordinatorContext();
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
afterEach(() => {
|
|
413
|
+
clearCoordinatorContext();
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
describe("setCoordinatorContext", () => {
|
|
417
|
+
it("sets coordinator context", () => {
|
|
418
|
+
setCoordinatorContext({
|
|
419
|
+
isCoordinator: true,
|
|
420
|
+
epicId: "test-epic-123",
|
|
421
|
+
sessionId: "test-session-456",
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
const ctx = getCoordinatorContext();
|
|
425
|
+
expect(ctx.isCoordinator).toBe(true);
|
|
426
|
+
expect(ctx.epicId).toBe("test-epic-123");
|
|
427
|
+
expect(ctx.sessionId).toBe("test-session-456");
|
|
428
|
+
expect(ctx.activatedAt).toBeDefined();
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
it("merges with existing context", () => {
|
|
432
|
+
setCoordinatorContext({
|
|
433
|
+
isCoordinator: true,
|
|
434
|
+
sessionId: "session-1",
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
setCoordinatorContext({
|
|
438
|
+
epicId: "epic-1",
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
const ctx = getCoordinatorContext();
|
|
442
|
+
expect(ctx.isCoordinator).toBe(true);
|
|
443
|
+
expect(ctx.sessionId).toBe("session-1");
|
|
444
|
+
expect(ctx.epicId).toBe("epic-1");
|
|
445
|
+
});
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
describe("isInCoordinatorContext", () => {
|
|
449
|
+
it("returns false when not in coordinator context", () => {
|
|
450
|
+
expect(isInCoordinatorContext()).toBe(false);
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
it("returns true when in coordinator context", () => {
|
|
454
|
+
setCoordinatorContext({
|
|
455
|
+
isCoordinator: true,
|
|
456
|
+
epicId: "test-epic",
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
expect(isInCoordinatorContext()).toBe(true);
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
it("returns false after context is cleared", () => {
|
|
463
|
+
setCoordinatorContext({
|
|
464
|
+
isCoordinator: true,
|
|
465
|
+
epicId: "test-epic",
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
clearCoordinatorContext();
|
|
469
|
+
|
|
470
|
+
expect(isInCoordinatorContext()).toBe(false);
|
|
471
|
+
});
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
describe("clearCoordinatorContext", () => {
|
|
475
|
+
it("clears all context", () => {
|
|
476
|
+
setCoordinatorContext({
|
|
477
|
+
isCoordinator: true,
|
|
478
|
+
epicId: "test-epic",
|
|
479
|
+
sessionId: "test-session",
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
clearCoordinatorContext();
|
|
483
|
+
|
|
484
|
+
const ctx = getCoordinatorContext();
|
|
485
|
+
expect(ctx.isCoordinator).toBe(false);
|
|
486
|
+
expect(ctx.epicId).toBeUndefined();
|
|
487
|
+
expect(ctx.sessionId).toBeUndefined();
|
|
488
|
+
});
|
|
489
|
+
});
|
|
490
|
+
});
|
|
106
491
|
});
|