opencode-swarm-plugin 0.33.0 → 0.35.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/.hive/memories.jsonl +255 -1
- package/.turbo/turbo-build.log +4 -4
- package/.turbo/turbo-test.log +289 -289
- package/CHANGELOG.md +133 -0
- package/README.md +29 -1
- package/bin/swarm.test.ts +342 -1
- package/bin/swarm.ts +351 -4
- package/dist/compaction-hook.d.ts +1 -1
- package/dist/compaction-hook.d.ts.map +1 -1
- package/dist/index.d.ts +95 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11848 -124
- package/dist/logger.d.ts +34 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/plugin.js +11722 -112
- package/dist/swarm-orchestrate.d.ts +105 -0
- package/dist/swarm-orchestrate.d.ts.map +1 -1
- package/dist/swarm-prompts.d.ts +54 -2
- package/dist/swarm-prompts.d.ts.map +1 -1
- package/dist/swarm-research.d.ts +127 -0
- package/dist/swarm-research.d.ts.map +1 -0
- package/dist/swarm-review.d.ts.map +1 -1
- package/dist/swarm.d.ts +56 -1
- package/dist/swarm.d.ts.map +1 -1
- package/evals/compaction-resumption.eval.ts +289 -0
- package/evals/coordinator-behavior.eval.ts +307 -0
- package/evals/fixtures/compaction-cases.ts +350 -0
- package/evals/scorers/compaction-scorers.ts +305 -0
- package/evals/scorers/index.ts +12 -0
- package/package.json +5 -2
- package/src/compaction-hook.test.ts +639 -1
- package/src/compaction-hook.ts +488 -18
- package/src/index.ts +29 -0
- package/src/logger.test.ts +189 -0
- package/src/logger.ts +135 -0
- package/src/swarm-decompose.ts +0 -7
- package/src/swarm-prompts.test.ts +164 -1
- package/src/swarm-prompts.ts +179 -12
- package/src/swarm-review.test.ts +177 -0
- package/src/swarm-review.ts +12 -47
package/src/compaction-hook.ts
CHANGED
|
@@ -31,6 +31,28 @@
|
|
|
31
31
|
|
|
32
32
|
import { getHiveAdapter, getHiveWorkingDirectory } from "./hive";
|
|
33
33
|
import { checkSwarmHealth } from "swarm-mail";
|
|
34
|
+
import { createChildLogger } from "./logger";
|
|
35
|
+
|
|
36
|
+
let _logger: any | undefined;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Get logger instance (lazy initialization for testability)
|
|
40
|
+
*
|
|
41
|
+
* Logs to: ~/.config/swarm-tools/logs/compaction.1log
|
|
42
|
+
*
|
|
43
|
+
* Log structure:
|
|
44
|
+
* - START: session_id, trigger
|
|
45
|
+
* - GATHER: source (swarm-mail|hive), duration_ms, stats/counts
|
|
46
|
+
* - DETECT: confidence, detected, reason_count, reasons
|
|
47
|
+
* - INJECT: confidence, context_length, context_type (full|fallback|none)
|
|
48
|
+
* - COMPLETE: duration_ms, success, detected, confidence, context_injected
|
|
49
|
+
*/
|
|
50
|
+
function getLog() {
|
|
51
|
+
if (!_logger) {
|
|
52
|
+
_logger = createChildLogger("compaction");
|
|
53
|
+
}
|
|
54
|
+
return _logger;
|
|
55
|
+
}
|
|
34
56
|
|
|
35
57
|
// ============================================================================
|
|
36
58
|
// Compaction Context
|
|
@@ -145,6 +167,252 @@ Include this in your summary:
|
|
|
145
167
|
"This is an active swarm. Check swarm_status and swarmmail_inbox immediately."
|
|
146
168
|
`;
|
|
147
169
|
|
|
170
|
+
// ============================================================================
|
|
171
|
+
// Dynamic Context Building
|
|
172
|
+
// ============================================================================
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Build dynamic swarm state section from detected state
|
|
176
|
+
*
|
|
177
|
+
* This injects SPECIFIC values instead of placeholders, making the context
|
|
178
|
+
* immediately actionable on resume.
|
|
179
|
+
*/
|
|
180
|
+
function buildDynamicSwarmState(state: SwarmState): string {
|
|
181
|
+
const parts: string[] = [];
|
|
182
|
+
|
|
183
|
+
parts.push("## 🐝 Current Swarm State\n");
|
|
184
|
+
|
|
185
|
+
if (state.epicId && state.epicTitle) {
|
|
186
|
+
parts.push(`**Epic:** ${state.epicId} - ${state.epicTitle}`);
|
|
187
|
+
|
|
188
|
+
const totalSubtasks = state.subtasks.closed + state.subtasks.in_progress +
|
|
189
|
+
state.subtasks.open + state.subtasks.blocked;
|
|
190
|
+
|
|
191
|
+
if (totalSubtasks > 0) {
|
|
192
|
+
parts.push(`**Subtasks:**`);
|
|
193
|
+
if (state.subtasks.closed > 0) parts.push(` - ${state.subtasks.closed} closed`);
|
|
194
|
+
if (state.subtasks.in_progress > 0) parts.push(` - ${state.subtasks.in_progress} in_progress`);
|
|
195
|
+
if (state.subtasks.open > 0) parts.push(` - ${state.subtasks.open} open`);
|
|
196
|
+
if (state.subtasks.blocked > 0) parts.push(` - ${state.subtasks.blocked} blocked`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
parts.push(`**Project:** ${state.projectPath}`);
|
|
201
|
+
|
|
202
|
+
if (state.epicId) {
|
|
203
|
+
parts.push(`\n## 🎯 YOU ARE THE COORDINATOR`);
|
|
204
|
+
parts.push(``);
|
|
205
|
+
parts.push(`**Primary role:** Orchestrate workers, review their output, unblock dependencies.`);
|
|
206
|
+
parts.push(`**Spawn workers** for implementation tasks - don't do them yourself.`);
|
|
207
|
+
parts.push(``);
|
|
208
|
+
parts.push(`**RESUME STEPS:**`);
|
|
209
|
+
parts.push(`1. Check swarm status: \`swarm_status(epic_id="${state.epicId}", project_key="${state.projectPath}")\``);
|
|
210
|
+
parts.push(`2. Check inbox for worker messages: \`swarmmail_inbox(limit=5)\``);
|
|
211
|
+
parts.push(`3. For in_progress subtasks: Review worker results with \`swarm_review\``);
|
|
212
|
+
parts.push(`4. For open subtasks: Spawn workers with \`swarm_spawn_subtask\``);
|
|
213
|
+
parts.push(`5. For blocked subtasks: Investigate and unblock`);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return parts.join("\n");
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ============================================================================
|
|
220
|
+
// SDK Message Scanning
|
|
221
|
+
// ============================================================================
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Tool part with completed state containing input/output
|
|
225
|
+
*/
|
|
226
|
+
interface ToolPart {
|
|
227
|
+
id: string;
|
|
228
|
+
sessionID: string;
|
|
229
|
+
messageID: string;
|
|
230
|
+
type: "tool";
|
|
231
|
+
callID: string;
|
|
232
|
+
tool: string;
|
|
233
|
+
state: ToolState;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Tool state (completed tools have input/output we need)
|
|
238
|
+
*/
|
|
239
|
+
type ToolState = {
|
|
240
|
+
status: "completed";
|
|
241
|
+
input: { [key: string]: unknown };
|
|
242
|
+
output: string;
|
|
243
|
+
title: string;
|
|
244
|
+
metadata: { [key: string]: unknown };
|
|
245
|
+
time: { start: number; end: number };
|
|
246
|
+
} | {
|
|
247
|
+
status: string;
|
|
248
|
+
[key: string]: unknown;
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* SDK Client type (minimal interface for scanSessionMessages)
|
|
253
|
+
*/
|
|
254
|
+
interface OpencodeClient {
|
|
255
|
+
session: {
|
|
256
|
+
messages: (opts: { sessionID: string; limit?: number }) => Promise<{
|
|
257
|
+
info: { id: string; sessionID: string };
|
|
258
|
+
parts: ToolPart[];
|
|
259
|
+
}[]>;
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Scanned swarm state extracted from session messages
|
|
265
|
+
*/
|
|
266
|
+
export interface ScannedSwarmState {
|
|
267
|
+
epicId?: string;
|
|
268
|
+
epicTitle?: string;
|
|
269
|
+
projectPath?: string;
|
|
270
|
+
agentName?: string;
|
|
271
|
+
subtasks: Map<string, { title: string; status: string; worker?: string; files?: string[] }>;
|
|
272
|
+
lastAction?: { tool: string; args: unknown; timestamp: number };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Scan session messages for swarm state using SDK client
|
|
277
|
+
*
|
|
278
|
+
* Extracts swarm coordination state from actual tool calls:
|
|
279
|
+
* - swarm_spawn_subtask → subtask tracking
|
|
280
|
+
* - swarmmail_init → agent name, project path
|
|
281
|
+
* - hive_create_epic → epic ID and title
|
|
282
|
+
* - swarm_status → epic reference
|
|
283
|
+
* - swarm_complete → subtask completion
|
|
284
|
+
*
|
|
285
|
+
* @param client - OpenCode SDK client (undefined if not available)
|
|
286
|
+
* @param sessionID - Session to scan
|
|
287
|
+
* @param limit - Max messages to fetch (default 100)
|
|
288
|
+
* @returns Extracted swarm state
|
|
289
|
+
*/
|
|
290
|
+
export async function scanSessionMessages(
|
|
291
|
+
client: OpencodeClient | undefined,
|
|
292
|
+
sessionID: string,
|
|
293
|
+
limit: number = 100
|
|
294
|
+
): Promise<ScannedSwarmState> {
|
|
295
|
+
const state: ScannedSwarmState = {
|
|
296
|
+
subtasks: new Map(),
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
if (!client) {
|
|
300
|
+
return state;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
try {
|
|
304
|
+
const messages = await client.session.messages({ sessionID, limit });
|
|
305
|
+
|
|
306
|
+
for (const message of messages) {
|
|
307
|
+
for (const part of message.parts) {
|
|
308
|
+
if (part.type !== "tool" || part.state.status !== "completed") {
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const { tool, state: toolState } = part;
|
|
313
|
+
const { input, output, time } = toolState as Extract<ToolState, { status: "completed" }>;
|
|
314
|
+
|
|
315
|
+
// Track last action
|
|
316
|
+
state.lastAction = {
|
|
317
|
+
tool,
|
|
318
|
+
args: input,
|
|
319
|
+
timestamp: time.end,
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
// Extract swarm state based on tool type
|
|
323
|
+
switch (tool) {
|
|
324
|
+
case "hive_create_epic": {
|
|
325
|
+
try {
|
|
326
|
+
const parsed = JSON.parse(output);
|
|
327
|
+
if (parsed.epic?.id) {
|
|
328
|
+
state.epicId = parsed.epic.id;
|
|
329
|
+
}
|
|
330
|
+
if (input.epic_title && typeof input.epic_title === "string") {
|
|
331
|
+
state.epicTitle = input.epic_title;
|
|
332
|
+
}
|
|
333
|
+
} catch {
|
|
334
|
+
// Invalid JSON, skip
|
|
335
|
+
}
|
|
336
|
+
break;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
case "swarmmail_init": {
|
|
340
|
+
try {
|
|
341
|
+
const parsed = JSON.parse(output);
|
|
342
|
+
if (parsed.agent_name) {
|
|
343
|
+
state.agentName = parsed.agent_name;
|
|
344
|
+
}
|
|
345
|
+
if (parsed.project_key) {
|
|
346
|
+
state.projectPath = parsed.project_key;
|
|
347
|
+
}
|
|
348
|
+
} catch {
|
|
349
|
+
// Invalid JSON, skip
|
|
350
|
+
}
|
|
351
|
+
break;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
case "swarm_spawn_subtask": {
|
|
355
|
+
const beadId = input.bead_id as string | undefined;
|
|
356
|
+
const epicId = input.epic_id as string | undefined;
|
|
357
|
+
const title = input.subtask_title as string | undefined;
|
|
358
|
+
const files = input.files as string[] | undefined;
|
|
359
|
+
|
|
360
|
+
if (beadId && title) {
|
|
361
|
+
let worker: string | undefined;
|
|
362
|
+
try {
|
|
363
|
+
const parsed = JSON.parse(output);
|
|
364
|
+
worker = parsed.worker;
|
|
365
|
+
} catch {
|
|
366
|
+
// No worker in output
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
state.subtasks.set(beadId, {
|
|
370
|
+
title,
|
|
371
|
+
status: "spawned",
|
|
372
|
+
worker,
|
|
373
|
+
files,
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
if (epicId && !state.epicId) {
|
|
377
|
+
state.epicId = epicId;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
break;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
case "swarm_complete": {
|
|
384
|
+
const beadId = input.bead_id as string | undefined;
|
|
385
|
+
if (beadId && state.subtasks.has(beadId)) {
|
|
386
|
+
const existing = state.subtasks.get(beadId)!;
|
|
387
|
+
state.subtasks.set(beadId, {
|
|
388
|
+
...existing,
|
|
389
|
+
status: "completed",
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
break;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
case "swarm_status": {
|
|
396
|
+
const epicId = input.epic_id as string | undefined;
|
|
397
|
+
if (epicId && !state.epicId) {
|
|
398
|
+
state.epicId = epicId;
|
|
399
|
+
}
|
|
400
|
+
const projectKey = input.project_key as string | undefined;
|
|
401
|
+
if (projectKey && !state.projectPath) {
|
|
402
|
+
state.projectPath = projectKey;
|
|
403
|
+
}
|
|
404
|
+
break;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
} catch (error) {
|
|
410
|
+
// SDK not available or error fetching messages - return what we have
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
return state;
|
|
414
|
+
}
|
|
415
|
+
|
|
148
416
|
// ============================================================================
|
|
149
417
|
// Swarm Detection
|
|
150
418
|
// ============================================================================
|
|
@@ -156,6 +424,23 @@ interface SwarmDetection {
|
|
|
156
424
|
detected: boolean;
|
|
157
425
|
confidence: "high" | "medium" | "low" | "none";
|
|
158
426
|
reasons: string[];
|
|
427
|
+
/** Specific swarm state data for context injection */
|
|
428
|
+
state?: SwarmState;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Specific swarm state captured during detection
|
|
433
|
+
*/
|
|
434
|
+
interface SwarmState {
|
|
435
|
+
epicId?: string;
|
|
436
|
+
epicTitle?: string;
|
|
437
|
+
projectPath: string;
|
|
438
|
+
subtasks: {
|
|
439
|
+
closed: number;
|
|
440
|
+
in_progress: number;
|
|
441
|
+
open: number;
|
|
442
|
+
blocked: number;
|
|
443
|
+
};
|
|
159
444
|
}
|
|
160
445
|
|
|
161
446
|
/**
|
|
@@ -173,13 +458,38 @@ async function detectSwarm(): Promise<SwarmDetection> {
|
|
|
173
458
|
let highConfidence = false;
|
|
174
459
|
let mediumConfidence = false;
|
|
175
460
|
let lowConfidence = false;
|
|
461
|
+
let state: SwarmState | undefined;
|
|
176
462
|
|
|
177
463
|
try {
|
|
178
464
|
const projectKey = getHiveWorkingDirectory();
|
|
465
|
+
|
|
466
|
+
// Initialize state with project path
|
|
467
|
+
state = {
|
|
468
|
+
projectPath: projectKey,
|
|
469
|
+
subtasks: {
|
|
470
|
+
closed: 0,
|
|
471
|
+
in_progress: 0,
|
|
472
|
+
open: 0,
|
|
473
|
+
blocked: 0,
|
|
474
|
+
},
|
|
475
|
+
};
|
|
179
476
|
|
|
180
477
|
// Check 1: Active reservations in swarm-mail (HIGH confidence)
|
|
478
|
+
const swarmMailStart = Date.now();
|
|
181
479
|
try {
|
|
182
480
|
const health = await checkSwarmHealth(projectKey);
|
|
481
|
+
const duration = Date.now() - swarmMailStart;
|
|
482
|
+
|
|
483
|
+
getLog().debug(
|
|
484
|
+
{
|
|
485
|
+
source: "swarm-mail",
|
|
486
|
+
duration_ms: duration,
|
|
487
|
+
healthy: health.healthy,
|
|
488
|
+
stats: health.stats,
|
|
489
|
+
},
|
|
490
|
+
"checked swarm-mail health",
|
|
491
|
+
);
|
|
492
|
+
|
|
183
493
|
if (health.healthy && health.stats) {
|
|
184
494
|
if (health.stats.reservations > 0) {
|
|
185
495
|
highConfidence = true;
|
|
@@ -194,14 +504,24 @@ async function detectSwarm(): Promise<SwarmDetection> {
|
|
|
194
504
|
reasons.push(`${health.stats.messages} swarm messages`);
|
|
195
505
|
}
|
|
196
506
|
}
|
|
197
|
-
} catch {
|
|
507
|
+
} catch (error) {
|
|
508
|
+
getLog().debug(
|
|
509
|
+
{
|
|
510
|
+
source: "swarm-mail",
|
|
511
|
+
duration_ms: Date.now() - swarmMailStart,
|
|
512
|
+
error: error instanceof Error ? error.message : String(error),
|
|
513
|
+
},
|
|
514
|
+
"swarm-mail check failed",
|
|
515
|
+
);
|
|
198
516
|
// Swarm-mail not available, continue with other checks
|
|
199
517
|
}
|
|
200
518
|
|
|
201
519
|
// Check 2: Hive cells (various confidence levels)
|
|
520
|
+
const hiveStart = Date.now();
|
|
202
521
|
try {
|
|
203
522
|
const adapter = await getHiveAdapter(projectKey);
|
|
204
523
|
const cells = await adapter.queryCells(projectKey, {});
|
|
524
|
+
const duration = Date.now() - hiveStart;
|
|
205
525
|
|
|
206
526
|
if (Array.isArray(cells) && cells.length > 0) {
|
|
207
527
|
// HIGH: Any in_progress cells
|
|
@@ -213,7 +533,7 @@ async function detectSwarm(): Promise<SwarmDetection> {
|
|
|
213
533
|
|
|
214
534
|
// MEDIUM: Open subtasks (cells with parent_id)
|
|
215
535
|
const subtasks = cells.filter(
|
|
216
|
-
(c) => c.status === "open" && c.parent_id
|
|
536
|
+
(c) => c.status === "open" && c.parent_id,
|
|
217
537
|
);
|
|
218
538
|
if (subtasks.length > 0) {
|
|
219
539
|
mediumConfidence = true;
|
|
@@ -222,11 +542,37 @@ async function detectSwarm(): Promise<SwarmDetection> {
|
|
|
222
542
|
|
|
223
543
|
// MEDIUM: Unclosed epics
|
|
224
544
|
const openEpics = cells.filter(
|
|
225
|
-
(c) => c.type === "epic" && c.status !== "closed"
|
|
545
|
+
(c) => c.type === "epic" && c.status !== "closed",
|
|
226
546
|
);
|
|
227
547
|
if (openEpics.length > 0) {
|
|
228
548
|
mediumConfidence = true;
|
|
229
549
|
reasons.push(`${openEpics.length} unclosed epics`);
|
|
550
|
+
|
|
551
|
+
// Capture in_progress epic data for state
|
|
552
|
+
const inProgressEpic = openEpics.find((c) => c.status === "in_progress");
|
|
553
|
+
if (inProgressEpic && state) {
|
|
554
|
+
state.epicId = inProgressEpic.id;
|
|
555
|
+
state.epicTitle = inProgressEpic.title;
|
|
556
|
+
|
|
557
|
+
// Count subtasks for this epic
|
|
558
|
+
const epicSubtasks = cells.filter((c) => c.parent_id === inProgressEpic.id);
|
|
559
|
+
state.subtasks.closed = epicSubtasks.filter((c) => c.status === "closed").length;
|
|
560
|
+
state.subtasks.in_progress = epicSubtasks.filter((c) => c.status === "in_progress").length;
|
|
561
|
+
state.subtasks.open = epicSubtasks.filter((c) => c.status === "open").length;
|
|
562
|
+
state.subtasks.blocked = epicSubtasks.filter((c) => c.status === "blocked").length;
|
|
563
|
+
|
|
564
|
+
getLog().debug(
|
|
565
|
+
{
|
|
566
|
+
epic_id: state.epicId,
|
|
567
|
+
epic_title: state.epicTitle,
|
|
568
|
+
subtasks_closed: state.subtasks.closed,
|
|
569
|
+
subtasks_in_progress: state.subtasks.in_progress,
|
|
570
|
+
subtasks_open: state.subtasks.open,
|
|
571
|
+
subtasks_blocked: state.subtasks.blocked,
|
|
572
|
+
},
|
|
573
|
+
"captured epic state for context",
|
|
574
|
+
);
|
|
575
|
+
}
|
|
230
576
|
}
|
|
231
577
|
|
|
232
578
|
// MEDIUM: Recently updated cells (last hour)
|
|
@@ -242,14 +588,46 @@ async function detectSwarm(): Promise<SwarmDetection> {
|
|
|
242
588
|
lowConfidence = true;
|
|
243
589
|
reasons.push(`${cells.length} total cells in hive`);
|
|
244
590
|
}
|
|
591
|
+
|
|
592
|
+
getLog().debug(
|
|
593
|
+
{
|
|
594
|
+
source: "hive",
|
|
595
|
+
duration_ms: duration,
|
|
596
|
+
total_cells: cells.length,
|
|
597
|
+
in_progress: inProgress.length,
|
|
598
|
+
open_subtasks: subtasks.length,
|
|
599
|
+
open_epics: openEpics.length,
|
|
600
|
+
recent_updates: recentCells.length,
|
|
601
|
+
},
|
|
602
|
+
"checked hive cells",
|
|
603
|
+
);
|
|
604
|
+
} else {
|
|
605
|
+
getLog().debug(
|
|
606
|
+
{ source: "hive", duration_ms: duration, total_cells: 0 },
|
|
607
|
+
"hive empty",
|
|
608
|
+
);
|
|
245
609
|
}
|
|
246
|
-
} catch {
|
|
610
|
+
} catch (error) {
|
|
611
|
+
getLog().debug(
|
|
612
|
+
{
|
|
613
|
+
source: "hive",
|
|
614
|
+
duration_ms: Date.now() - hiveStart,
|
|
615
|
+
error: error instanceof Error ? error.message : String(error),
|
|
616
|
+
},
|
|
617
|
+
"hive check failed",
|
|
618
|
+
);
|
|
247
619
|
// Hive not available, continue
|
|
248
620
|
}
|
|
249
|
-
} catch {
|
|
621
|
+
} catch (error) {
|
|
250
622
|
// Project detection failed, use fallback
|
|
251
623
|
lowConfidence = true;
|
|
252
624
|
reasons.push("Could not detect project, using fallback");
|
|
625
|
+
getLog().debug(
|
|
626
|
+
{
|
|
627
|
+
error: error instanceof Error ? error.message : String(error),
|
|
628
|
+
},
|
|
629
|
+
"project detection failed",
|
|
630
|
+
);
|
|
253
631
|
}
|
|
254
632
|
|
|
255
633
|
// Determine overall confidence
|
|
@@ -264,11 +642,25 @@ async function detectSwarm(): Promise<SwarmDetection> {
|
|
|
264
642
|
confidence = "none";
|
|
265
643
|
}
|
|
266
644
|
|
|
267
|
-
|
|
645
|
+
const result = {
|
|
268
646
|
detected: confidence !== "none",
|
|
269
647
|
confidence,
|
|
270
648
|
reasons,
|
|
649
|
+
state,
|
|
271
650
|
};
|
|
651
|
+
|
|
652
|
+
getLog().debug(
|
|
653
|
+
{
|
|
654
|
+
detected: result.detected,
|
|
655
|
+
confidence: result.confidence,
|
|
656
|
+
reason_count: result.reasons.length,
|
|
657
|
+
reasons: result.reasons,
|
|
658
|
+
has_state: !!result.state,
|
|
659
|
+
},
|
|
660
|
+
"swarm detection complete",
|
|
661
|
+
);
|
|
662
|
+
|
|
663
|
+
return result;
|
|
272
664
|
}
|
|
273
665
|
|
|
274
666
|
// ============================================================================
|
|
@@ -298,20 +690,98 @@ async function detectSwarm(): Promise<SwarmDetection> {
|
|
|
298
690
|
*/
|
|
299
691
|
export function createCompactionHook() {
|
|
300
692
|
return async (
|
|
301
|
-
|
|
693
|
+
input: { sessionID: string },
|
|
302
694
|
output: { context: string[] },
|
|
303
695
|
): Promise<void> => {
|
|
304
|
-
const
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
696
|
+
const startTime = Date.now();
|
|
697
|
+
|
|
698
|
+
getLog().info(
|
|
699
|
+
{
|
|
700
|
+
session_id: input.sessionID,
|
|
701
|
+
trigger: "session_compaction",
|
|
702
|
+
},
|
|
703
|
+
"compaction started",
|
|
704
|
+
);
|
|
705
|
+
|
|
706
|
+
try {
|
|
707
|
+
const detection = await detectSwarm();
|
|
708
|
+
|
|
709
|
+
if (
|
|
710
|
+
detection.confidence === "high" ||
|
|
711
|
+
detection.confidence === "medium"
|
|
712
|
+
) {
|
|
713
|
+
// Definite or probable swarm - inject full context
|
|
714
|
+
const header = `[Swarm detected: ${detection.reasons.join(", ")}]\n\n`;
|
|
715
|
+
|
|
716
|
+
// Build dynamic state section if we have specific data
|
|
717
|
+
let dynamicState = "";
|
|
718
|
+
if (detection.state && detection.state.epicId) {
|
|
719
|
+
dynamicState = buildDynamicSwarmState(detection.state) + "\n\n";
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
const contextContent = header + dynamicState + SWARM_COMPACTION_CONTEXT;
|
|
723
|
+
output.context.push(contextContent);
|
|
724
|
+
|
|
725
|
+
getLog().info(
|
|
726
|
+
{
|
|
727
|
+
confidence: detection.confidence,
|
|
728
|
+
context_length: contextContent.length,
|
|
729
|
+
context_type: "full",
|
|
730
|
+
reasons: detection.reasons,
|
|
731
|
+
has_dynamic_state: !!dynamicState,
|
|
732
|
+
epic_id: detection.state?.epicId,
|
|
733
|
+
},
|
|
734
|
+
"injected swarm context",
|
|
735
|
+
);
|
|
736
|
+
} else if (detection.confidence === "low") {
|
|
737
|
+
// Possible swarm - inject fallback detection prompt
|
|
738
|
+
const header = `[Possible swarm: ${detection.reasons.join(", ")}]\n\n`;
|
|
739
|
+
const contextContent = header + SWARM_DETECTION_FALLBACK;
|
|
740
|
+
output.context.push(contextContent);
|
|
741
|
+
|
|
742
|
+
getLog().info(
|
|
743
|
+
{
|
|
744
|
+
confidence: detection.confidence,
|
|
745
|
+
context_length: contextContent.length,
|
|
746
|
+
context_type: "fallback",
|
|
747
|
+
reasons: detection.reasons,
|
|
748
|
+
},
|
|
749
|
+
"injected swarm context",
|
|
750
|
+
);
|
|
751
|
+
} else {
|
|
752
|
+
getLog().debug(
|
|
753
|
+
{
|
|
754
|
+
confidence: detection.confidence,
|
|
755
|
+
context_type: "none",
|
|
756
|
+
},
|
|
757
|
+
"no swarm detected, skipping injection",
|
|
758
|
+
);
|
|
759
|
+
}
|
|
760
|
+
// confidence === "none" - no injection, probably not a swarm
|
|
761
|
+
|
|
762
|
+
const duration = Date.now() - startTime;
|
|
763
|
+
getLog().info(
|
|
764
|
+
{
|
|
765
|
+
duration_ms: duration,
|
|
766
|
+
success: true,
|
|
767
|
+
detected: detection.detected,
|
|
768
|
+
confidence: detection.confidence,
|
|
769
|
+
context_injected: output.context.length > 0,
|
|
770
|
+
},
|
|
771
|
+
"compaction complete",
|
|
772
|
+
);
|
|
773
|
+
} catch (error) {
|
|
774
|
+
const duration = Date.now() - startTime;
|
|
775
|
+
getLog().error(
|
|
776
|
+
{
|
|
777
|
+
duration_ms: duration,
|
|
778
|
+
success: false,
|
|
779
|
+
error: error instanceof Error ? error.message : String(error),
|
|
780
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
781
|
+
},
|
|
782
|
+
"compaction failed",
|
|
783
|
+
);
|
|
784
|
+
// Don't throw - compaction hook failures shouldn't break the session
|
|
314
785
|
}
|
|
315
|
-
// confidence === "none" - no injection, probably not a swarm
|
|
316
786
|
};
|
|
317
787
|
}
|
package/src/index.ts
CHANGED
|
@@ -684,6 +684,35 @@ export {
|
|
|
684
684
|
} from "./memory-tools";
|
|
685
685
|
export type { Memory, SearchResult, SearchOptions } from "swarm-mail";
|
|
686
686
|
|
|
687
|
+
/**
|
|
688
|
+
* Re-export logger infrastructure
|
|
689
|
+
*
|
|
690
|
+
* Includes:
|
|
691
|
+
* - getLogger - Gets or creates the main logger instance
|
|
692
|
+
* - createChildLogger - Creates a module-specific child logger with separate log file
|
|
693
|
+
* - logger - Default logger instance for immediate use
|
|
694
|
+
*
|
|
695
|
+
* Features:
|
|
696
|
+
* - Daily log rotation via pino-roll (numeric format: swarm.1log, swarm.2log, etc.)
|
|
697
|
+
* - 14-day retention
|
|
698
|
+
* - Module-specific child loggers
|
|
699
|
+
* - Pretty mode for development (SWARM_LOG_PRETTY=1)
|
|
700
|
+
* - Logs to ~/.config/swarm-tools/logs/
|
|
701
|
+
*
|
|
702
|
+
* @example
|
|
703
|
+
* ```typescript
|
|
704
|
+
* import { logger, createChildLogger } from "opencode-swarm-plugin";
|
|
705
|
+
*
|
|
706
|
+
* // Use default logger
|
|
707
|
+
* logger.info("Application started");
|
|
708
|
+
*
|
|
709
|
+
* // Create module-specific logger
|
|
710
|
+
* const compactionLog = createChildLogger("compaction");
|
|
711
|
+
* compactionLog.info("Compaction started");
|
|
712
|
+
* ```
|
|
713
|
+
*/
|
|
714
|
+
export { getLogger, createChildLogger, logger } from "./logger";
|
|
715
|
+
|
|
687
716
|
/**
|
|
688
717
|
* Re-export swarm-research module
|
|
689
718
|
*
|