opencode-swarm-plugin 0.12.11 → 0.12.14
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/.beads/issues.jsonl +4 -0
- package/dist/index.js +1458 -1365
- package/dist/plugin.js +1451 -1365
- package/package.json +2 -2
- package/src/learning.ts +29 -2
- package/src/rate-limiter.ts +100 -27
- package/src/swarm.ts +135 -45
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-swarm-plugin",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.14",
|
|
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",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
}
|
|
16
16
|
},
|
|
17
17
|
"scripts": {
|
|
18
|
-
"build": "bun build ./src/index.ts --outdir ./dist --target
|
|
18
|
+
"build": "bun build ./src/index.ts --outdir ./dist --target node && bun build ./src/plugin.ts --outfile ./dist/plugin.js --target node",
|
|
19
19
|
"dev": "bun --watch src/index.ts",
|
|
20
20
|
"test": "bun test src/schemas/",
|
|
21
21
|
"test:watch": "bun test --watch",
|
package/src/learning.ts
CHANGED
|
@@ -231,11 +231,14 @@ export function calculateDecayedValue(
|
|
|
231
231
|
now: Date = new Date(),
|
|
232
232
|
halfLifeDays: number = 90,
|
|
233
233
|
): number {
|
|
234
|
+
// Prevent division by zero
|
|
235
|
+
const safeHalfLife = halfLifeDays <= 0 ? 1 : halfLifeDays;
|
|
236
|
+
|
|
234
237
|
const eventTime = new Date(timestamp).getTime();
|
|
235
238
|
const nowTime = now.getTime();
|
|
236
239
|
const ageDays = Math.max(0, (nowTime - eventTime) / (24 * 60 * 60 * 1000));
|
|
237
240
|
|
|
238
|
-
return Math.pow(0.5, ageDays /
|
|
241
|
+
return Math.pow(0.5, ageDays / safeHalfLife);
|
|
239
242
|
}
|
|
240
243
|
|
|
241
244
|
/**
|
|
@@ -252,6 +255,18 @@ export function calculateCriterionWeight(
|
|
|
252
255
|
events: FeedbackEvent[],
|
|
253
256
|
config: LearningConfig = DEFAULT_LEARNING_CONFIG,
|
|
254
257
|
): CriterionWeight {
|
|
258
|
+
// Return early with default weight if events array is empty
|
|
259
|
+
if (events.length === 0) {
|
|
260
|
+
return {
|
|
261
|
+
criterion: "unknown",
|
|
262
|
+
weight: 1.0,
|
|
263
|
+
helpful_count: 0,
|
|
264
|
+
harmful_count: 0,
|
|
265
|
+
last_validated: undefined,
|
|
266
|
+
half_life_days: config.halfLifeDays,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
255
270
|
const now = new Date();
|
|
256
271
|
let helpfulSum = 0;
|
|
257
272
|
let harmfulSum = 0;
|
|
@@ -284,7 +299,7 @@ export function calculateCriterionWeight(
|
|
|
284
299
|
const weight = total > 0 ? Math.max(0.1, helpfulSum / total) : 1.0;
|
|
285
300
|
|
|
286
301
|
return {
|
|
287
|
-
criterion: events[0]
|
|
302
|
+
criterion: events[0].criterion,
|
|
288
303
|
weight,
|
|
289
304
|
helpful_count: helpfulCount,
|
|
290
305
|
harmful_count: harmfulCount,
|
|
@@ -476,12 +491,24 @@ export interface FeedbackStorage {
|
|
|
476
491
|
|
|
477
492
|
/**
|
|
478
493
|
* In-memory feedback storage (for testing and short-lived sessions)
|
|
494
|
+
*
|
|
495
|
+
* Uses LRU eviction to prevent unbounded memory growth.
|
|
479
496
|
*/
|
|
480
497
|
export class InMemoryFeedbackStorage implements FeedbackStorage {
|
|
481
498
|
private events: FeedbackEvent[] = [];
|
|
499
|
+
private readonly maxSize: number;
|
|
500
|
+
|
|
501
|
+
constructor(maxSize: number = 10000) {
|
|
502
|
+
this.maxSize = maxSize;
|
|
503
|
+
}
|
|
482
504
|
|
|
483
505
|
async store(event: FeedbackEvent): Promise<void> {
|
|
484
506
|
this.events.push(event);
|
|
507
|
+
|
|
508
|
+
// Evict oldest events if we exceed max size (LRU)
|
|
509
|
+
if (this.events.length > this.maxSize) {
|
|
510
|
+
this.events = this.events.slice(this.events.length - this.maxSize);
|
|
511
|
+
}
|
|
485
512
|
}
|
|
486
513
|
|
|
487
514
|
async getByCriterion(criterion: string): Promise<FeedbackEvent[]> {
|
package/src/rate-limiter.ts
CHANGED
|
@@ -28,11 +28,40 @@
|
|
|
28
28
|
*/
|
|
29
29
|
|
|
30
30
|
import Redis from "ioredis";
|
|
31
|
-
import { Database } from "bun:sqlite";
|
|
32
31
|
import { mkdirSync, existsSync } from "node:fs";
|
|
33
32
|
import { dirname, join } from "node:path";
|
|
34
33
|
import { homedir } from "node:os";
|
|
35
34
|
|
|
35
|
+
// SQLite is optional - only available in Bun runtime
|
|
36
|
+
// We use dynamic import to avoid breaking Node.js environments
|
|
37
|
+
interface BunDatabase {
|
|
38
|
+
run(sql: string, params?: unknown[]): void;
|
|
39
|
+
query<T>(sql: string): {
|
|
40
|
+
get(...params: unknown[]): T | null;
|
|
41
|
+
};
|
|
42
|
+
prepare(sql: string): {
|
|
43
|
+
run(...params: unknown[]): void;
|
|
44
|
+
};
|
|
45
|
+
close(): void;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let sqliteAvailable = false;
|
|
49
|
+
let createDatabase: ((path: string) => BunDatabase) | null = null;
|
|
50
|
+
|
|
51
|
+
// Try to load bun:sqlite at module load time
|
|
52
|
+
try {
|
|
53
|
+
if (typeof globalThis.Bun !== "undefined") {
|
|
54
|
+
// We're in Bun runtime - dynamic import will work
|
|
55
|
+
const bunSqlite = await import("bun:sqlite");
|
|
56
|
+
createDatabase = (path: string) =>
|
|
57
|
+
new bunSqlite.Database(path) as unknown as BunDatabase;
|
|
58
|
+
sqliteAvailable = true;
|
|
59
|
+
}
|
|
60
|
+
} catch {
|
|
61
|
+
// Not in Bun runtime, SQLite fallback unavailable
|
|
62
|
+
sqliteAvailable = false;
|
|
63
|
+
}
|
|
64
|
+
|
|
36
65
|
// ============================================================================
|
|
37
66
|
// Types
|
|
38
67
|
// ============================================================================
|
|
@@ -290,16 +319,20 @@ export class RedisRateLimiter implements RateLimiter {
|
|
|
290
319
|
* Uses sliding window via COUNT query with timestamp filter.
|
|
291
320
|
*/
|
|
292
321
|
export class SqliteRateLimiter implements RateLimiter {
|
|
293
|
-
private db:
|
|
322
|
+
private db: BunDatabase;
|
|
294
323
|
|
|
295
324
|
constructor(dbPath: string) {
|
|
325
|
+
if (!sqliteAvailable || !createDatabase) {
|
|
326
|
+
throw new Error("SQLite is not available in this runtime (requires Bun)");
|
|
327
|
+
}
|
|
328
|
+
|
|
296
329
|
// Ensure directory exists
|
|
297
330
|
const dir = dirname(dbPath);
|
|
298
331
|
if (!existsSync(dir)) {
|
|
299
332
|
mkdirSync(dir, { recursive: true });
|
|
300
333
|
}
|
|
301
334
|
|
|
302
|
-
this.db =
|
|
335
|
+
this.db = createDatabase(dbPath);
|
|
303
336
|
this.initialize();
|
|
304
337
|
}
|
|
305
338
|
|
|
@@ -381,13 +414,23 @@ export class SqliteRateLimiter implements RateLimiter {
|
|
|
381
414
|
|
|
382
415
|
// Count requests in window
|
|
383
416
|
const result = this.db
|
|
384
|
-
.query<{ count: number }
|
|
417
|
+
.query<{ count: number }>(
|
|
385
418
|
`SELECT COUNT(*) as count FROM rate_limits
|
|
386
419
|
WHERE agent_name = ? AND endpoint = ? AND window = ? AND timestamp > ?`,
|
|
387
420
|
)
|
|
388
421
|
.get(agentName, endpoint, window, windowStart);
|
|
389
422
|
|
|
390
|
-
|
|
423
|
+
// Validate result before accessing properties
|
|
424
|
+
if (!result || typeof result.count !== "number") {
|
|
425
|
+
// Query failed or returned invalid data, assume no usage
|
|
426
|
+
return {
|
|
427
|
+
allowed: true,
|
|
428
|
+
remaining: limit,
|
|
429
|
+
resetAt: now + windowDuration,
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const count = result.count;
|
|
391
434
|
const remaining = Math.max(0, limit - count);
|
|
392
435
|
const allowed = count < limit;
|
|
393
436
|
|
|
@@ -395,13 +438,14 @@ export class SqliteRateLimiter implements RateLimiter {
|
|
|
395
438
|
let resetAt = now + windowDuration;
|
|
396
439
|
if (!allowed) {
|
|
397
440
|
const oldest = this.db
|
|
398
|
-
.query<{ timestamp: number }
|
|
441
|
+
.query<{ timestamp: number }>(
|
|
399
442
|
`SELECT MIN(timestamp) as timestamp FROM rate_limits
|
|
400
443
|
WHERE agent_name = ? AND endpoint = ? AND window = ? AND timestamp > ?`,
|
|
401
444
|
)
|
|
402
445
|
.get(agentName, endpoint, window, windowStart);
|
|
403
446
|
|
|
404
|
-
|
|
447
|
+
// Validate oldest result before accessing properties
|
|
448
|
+
if (oldest && typeof oldest.timestamp === "number") {
|
|
405
449
|
resetAt = oldest.timestamp + windowDuration;
|
|
406
450
|
}
|
|
407
451
|
}
|
|
@@ -582,6 +626,11 @@ export async function createRateLimiter(options?: {
|
|
|
582
626
|
}
|
|
583
627
|
|
|
584
628
|
if (backend === "sqlite") {
|
|
629
|
+
if (!sqliteAvailable) {
|
|
630
|
+
throw new Error(
|
|
631
|
+
"SQLite backend requested but not available (requires Bun runtime)",
|
|
632
|
+
);
|
|
633
|
+
}
|
|
585
634
|
return new SqliteRateLimiter(sqlitePath);
|
|
586
635
|
}
|
|
587
636
|
|
|
@@ -590,31 +639,55 @@ export async function createRateLimiter(options?: {
|
|
|
590
639
|
return new RedisRateLimiter(redis);
|
|
591
640
|
}
|
|
592
641
|
|
|
593
|
-
// Auto-select: try Redis first, fall back to SQLite
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
642
|
+
// Auto-select: try Redis first with retry, fall back to SQLite
|
|
643
|
+
const maxRetries = 3;
|
|
644
|
+
const retryDelays = [100, 500, 1000]; // exponential backoff
|
|
645
|
+
|
|
646
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
647
|
+
try {
|
|
648
|
+
const redis = new Redis(redisUrl, {
|
|
649
|
+
connectTimeout: 2000,
|
|
650
|
+
maxRetriesPerRequest: 1,
|
|
651
|
+
retryStrategy: () => null, // Don't retry on failure
|
|
652
|
+
lazyConnect: true,
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
// Test connection
|
|
656
|
+
await redis.connect();
|
|
657
|
+
await redis.ping();
|
|
658
|
+
|
|
659
|
+
return new RedisRateLimiter(redis);
|
|
660
|
+
} catch (error) {
|
|
661
|
+
const isLastAttempt = attempt === maxRetries - 1;
|
|
662
|
+
|
|
663
|
+
if (isLastAttempt) {
|
|
664
|
+
// All retries exhausted, fall back to SQLite or in-memory
|
|
665
|
+
if (!hasWarnedAboutFallback) {
|
|
666
|
+
const fallbackType = sqliteAvailable ? "SQLite" : "in-memory";
|
|
667
|
+
const fallbackLocation = sqliteAvailable ? ` at ${sqlitePath}` : "";
|
|
668
|
+
console.warn(
|
|
669
|
+
`[rate-limiter] Redis connection failed after ${maxRetries} attempts (${redisUrl}), falling back to ${fallbackType}${fallbackLocation}`,
|
|
670
|
+
);
|
|
671
|
+
hasWarnedAboutFallback = true;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// Use SQLite if available, otherwise fall back to in-memory
|
|
675
|
+
if (sqliteAvailable) {
|
|
676
|
+
return new SqliteRateLimiter(sqlitePath);
|
|
677
|
+
}
|
|
678
|
+
return new InMemoryRateLimiter();
|
|
679
|
+
}
|
|
605
680
|
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
// Redis connection failed, fall back to SQLite
|
|
609
|
-
if (!hasWarnedAboutFallback) {
|
|
610
|
-
console.warn(
|
|
611
|
-
`[rate-limiter] Redis connection failed (${redisUrl}), falling back to SQLite at ${sqlitePath}`,
|
|
612
|
-
);
|
|
613
|
-
hasWarnedAboutFallback = true;
|
|
681
|
+
// Wait before retrying (exponential backoff)
|
|
682
|
+
await new Promise((resolve) => setTimeout(resolve, retryDelays[attempt]));
|
|
614
683
|
}
|
|
684
|
+
}
|
|
615
685
|
|
|
686
|
+
// Fallback (should never reach here due to return in last attempt)
|
|
687
|
+
if (sqliteAvailable) {
|
|
616
688
|
return new SqliteRateLimiter(sqlitePath);
|
|
617
689
|
}
|
|
690
|
+
return new InMemoryRateLimiter();
|
|
618
691
|
}
|
|
619
692
|
|
|
620
693
|
/**
|
package/src/swarm.ts
CHANGED
|
@@ -901,9 +901,10 @@ async function queryEpicSubtasks(epicId: string): Promise<Bead[]> {
|
|
|
901
901
|
.nothrow();
|
|
902
902
|
|
|
903
903
|
if (result.exitCode !== 0) {
|
|
904
|
-
// Don't throw - just return empty and
|
|
905
|
-
console.
|
|
906
|
-
`[swarm] Failed to query subtasks
|
|
904
|
+
// Don't throw - just return empty and log error prominently
|
|
905
|
+
console.error(
|
|
906
|
+
`[swarm] ERROR: Failed to query subtasks for epic ${epicId}:`,
|
|
907
|
+
result.stderr.toString(),
|
|
907
908
|
);
|
|
908
909
|
return [];
|
|
909
910
|
}
|
|
@@ -913,9 +914,16 @@ async function queryEpicSubtasks(epicId: string): Promise<Bead[]> {
|
|
|
913
914
|
return z.array(BeadSchema).parse(parsed);
|
|
914
915
|
} catch (error) {
|
|
915
916
|
if (error instanceof z.ZodError) {
|
|
916
|
-
console.
|
|
917
|
+
console.error(
|
|
918
|
+
`[swarm] ERROR: Invalid bead data for epic ${epicId}:`,
|
|
919
|
+
error.message,
|
|
920
|
+
);
|
|
917
921
|
return [];
|
|
918
922
|
}
|
|
923
|
+
console.error(
|
|
924
|
+
`[swarm] ERROR: Failed to parse beads for epic ${epicId}:`,
|
|
925
|
+
error,
|
|
926
|
+
);
|
|
919
927
|
throw error;
|
|
920
928
|
}
|
|
921
929
|
}
|
|
@@ -944,8 +952,12 @@ async function querySwarmMessages(
|
|
|
944
952
|
llm_mode: false, // Just need the count
|
|
945
953
|
});
|
|
946
954
|
return summary.summary.total_messages;
|
|
947
|
-
} catch {
|
|
948
|
-
// Thread might not exist yet
|
|
955
|
+
} catch (error) {
|
|
956
|
+
// Thread might not exist yet, or query failed
|
|
957
|
+
console.warn(
|
|
958
|
+
`[swarm] Failed to query swarm messages for thread ${threadId}:`,
|
|
959
|
+
error,
|
|
960
|
+
);
|
|
949
961
|
return 0;
|
|
950
962
|
}
|
|
951
963
|
}
|
|
@@ -989,22 +1001,31 @@ interface CassSearchResult {
|
|
|
989
1001
|
}>;
|
|
990
1002
|
}
|
|
991
1003
|
|
|
1004
|
+
/**
|
|
1005
|
+
* CASS query result with status
|
|
1006
|
+
*/
|
|
1007
|
+
type CassQueryResult =
|
|
1008
|
+
| { status: "unavailable" }
|
|
1009
|
+
| { status: "failed"; error?: string }
|
|
1010
|
+
| { status: "empty"; query: string }
|
|
1011
|
+
| { status: "success"; data: CassSearchResult };
|
|
1012
|
+
|
|
992
1013
|
/**
|
|
993
1014
|
* Query CASS for similar past tasks
|
|
994
1015
|
*
|
|
995
1016
|
* @param task - Task description to search for
|
|
996
1017
|
* @param limit - Maximum results to return
|
|
997
|
-
* @returns
|
|
1018
|
+
* @returns Structured result with status indicator
|
|
998
1019
|
*/
|
|
999
1020
|
async function queryCassHistory(
|
|
1000
1021
|
task: string,
|
|
1001
1022
|
limit: number = 3,
|
|
1002
|
-
): Promise<
|
|
1023
|
+
): Promise<CassQueryResult> {
|
|
1003
1024
|
// Check if CASS is available first
|
|
1004
1025
|
const cassAvailable = await isToolAvailable("cass");
|
|
1005
1026
|
if (!cassAvailable) {
|
|
1006
1027
|
warnMissingTool("cass");
|
|
1007
|
-
return
|
|
1028
|
+
return { status: "unavailable" };
|
|
1008
1029
|
}
|
|
1009
1030
|
|
|
1010
1031
|
try {
|
|
@@ -1013,25 +1034,38 @@ async function queryCassHistory(
|
|
|
1013
1034
|
.nothrow();
|
|
1014
1035
|
|
|
1015
1036
|
if (result.exitCode !== 0) {
|
|
1016
|
-
|
|
1037
|
+
const error = result.stderr.toString();
|
|
1038
|
+
console.warn(
|
|
1039
|
+
`[swarm] CASS search failed (exit ${result.exitCode}):`,
|
|
1040
|
+
error,
|
|
1041
|
+
);
|
|
1042
|
+
return { status: "failed", error };
|
|
1017
1043
|
}
|
|
1018
1044
|
|
|
1019
1045
|
const output = result.stdout.toString();
|
|
1020
1046
|
if (!output.trim()) {
|
|
1021
|
-
return {
|
|
1047
|
+
return { status: "empty", query: task };
|
|
1022
1048
|
}
|
|
1023
1049
|
|
|
1024
1050
|
try {
|
|
1025
1051
|
const parsed = JSON.parse(output);
|
|
1026
|
-
|
|
1052
|
+
const searchResult: CassSearchResult = {
|
|
1027
1053
|
query: task,
|
|
1028
1054
|
results: Array.isArray(parsed) ? parsed : parsed.results || [],
|
|
1029
1055
|
};
|
|
1030
|
-
|
|
1031
|
-
|
|
1056
|
+
|
|
1057
|
+
if (searchResult.results.length === 0) {
|
|
1058
|
+
return { status: "empty", query: task };
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
return { status: "success", data: searchResult };
|
|
1062
|
+
} catch (error) {
|
|
1063
|
+
console.warn(`[swarm] Failed to parse CASS output:`, error);
|
|
1064
|
+
return { status: "failed", error: String(error) };
|
|
1032
1065
|
}
|
|
1033
|
-
} catch {
|
|
1034
|
-
|
|
1066
|
+
} catch (error) {
|
|
1067
|
+
console.error(`[swarm] CASS query error:`, error);
|
|
1068
|
+
return { status: "failed", error: String(error) };
|
|
1035
1069
|
}
|
|
1036
1070
|
}
|
|
1037
1071
|
|
|
@@ -1223,13 +1257,35 @@ export const swarm_plan_prompt = tool({
|
|
|
1223
1257
|
|
|
1224
1258
|
// Query CASS for similar past tasks
|
|
1225
1259
|
let cassContext = "";
|
|
1226
|
-
let
|
|
1260
|
+
let cassResultInfo: {
|
|
1261
|
+
queried: boolean;
|
|
1262
|
+
results_found?: number;
|
|
1263
|
+
included_in_context?: boolean;
|
|
1264
|
+
reason?: string;
|
|
1265
|
+
};
|
|
1227
1266
|
|
|
1228
1267
|
if (args.query_cass !== false) {
|
|
1229
|
-
cassResult = await queryCassHistory(
|
|
1230
|
-
|
|
1231
|
-
|
|
1268
|
+
const cassResult = await queryCassHistory(
|
|
1269
|
+
args.task,
|
|
1270
|
+
args.cass_limit ?? 3,
|
|
1271
|
+
);
|
|
1272
|
+
if (cassResult.status === "success") {
|
|
1273
|
+
cassContext = formatCassHistoryForPrompt(cassResult.data);
|
|
1274
|
+
cassResultInfo = {
|
|
1275
|
+
queried: true,
|
|
1276
|
+
results_found: cassResult.data.results.length,
|
|
1277
|
+
included_in_context: true,
|
|
1278
|
+
};
|
|
1279
|
+
} else {
|
|
1280
|
+
cassResultInfo = {
|
|
1281
|
+
queried: true,
|
|
1282
|
+
results_found: 0,
|
|
1283
|
+
included_in_context: false,
|
|
1284
|
+
reason: cassResult.status,
|
|
1285
|
+
};
|
|
1232
1286
|
}
|
|
1287
|
+
} else {
|
|
1288
|
+
cassResultInfo = { queried: false, reason: "disabled" };
|
|
1233
1289
|
}
|
|
1234
1290
|
|
|
1235
1291
|
// Format strategy guidelines
|
|
@@ -1271,13 +1327,7 @@ export const swarm_plan_prompt = tool({
|
|
|
1271
1327
|
},
|
|
1272
1328
|
validation_note:
|
|
1273
1329
|
"Parse agent response as JSON and validate with swarm_validate_decomposition",
|
|
1274
|
-
cass_history:
|
|
1275
|
-
? {
|
|
1276
|
-
queried: true,
|
|
1277
|
-
results_found: cassResult.results.length,
|
|
1278
|
-
included_in_context: cassResult.results.length > 0,
|
|
1279
|
-
}
|
|
1280
|
-
: { queried: false, reason: "disabled or unavailable" },
|
|
1330
|
+
cass_history: cassResultInfo,
|
|
1281
1331
|
},
|
|
1282
1332
|
null,
|
|
1283
1333
|
2,
|
|
@@ -1324,13 +1374,35 @@ export const swarm_decompose = tool({
|
|
|
1324
1374
|
async execute(args) {
|
|
1325
1375
|
// Query CASS for similar past tasks
|
|
1326
1376
|
let cassContext = "";
|
|
1327
|
-
let
|
|
1377
|
+
let cassResultInfo: {
|
|
1378
|
+
queried: boolean;
|
|
1379
|
+
results_found?: number;
|
|
1380
|
+
included_in_context?: boolean;
|
|
1381
|
+
reason?: string;
|
|
1382
|
+
};
|
|
1328
1383
|
|
|
1329
1384
|
if (args.query_cass !== false) {
|
|
1330
|
-
cassResult = await queryCassHistory(
|
|
1331
|
-
|
|
1332
|
-
|
|
1385
|
+
const cassResult = await queryCassHistory(
|
|
1386
|
+
args.task,
|
|
1387
|
+
args.cass_limit ?? 3,
|
|
1388
|
+
);
|
|
1389
|
+
if (cassResult.status === "success") {
|
|
1390
|
+
cassContext = formatCassHistoryForPrompt(cassResult.data);
|
|
1391
|
+
cassResultInfo = {
|
|
1392
|
+
queried: true,
|
|
1393
|
+
results_found: cassResult.data.results.length,
|
|
1394
|
+
included_in_context: true,
|
|
1395
|
+
};
|
|
1396
|
+
} else {
|
|
1397
|
+
cassResultInfo = {
|
|
1398
|
+
queried: true,
|
|
1399
|
+
results_found: 0,
|
|
1400
|
+
included_in_context: false,
|
|
1401
|
+
reason: cassResult.status,
|
|
1402
|
+
};
|
|
1333
1403
|
}
|
|
1404
|
+
} else {
|
|
1405
|
+
cassResultInfo = { queried: false, reason: "disabled" };
|
|
1334
1406
|
}
|
|
1335
1407
|
|
|
1336
1408
|
// Combine user context with CASS history
|
|
@@ -1363,13 +1435,7 @@ export const swarm_decompose = tool({
|
|
|
1363
1435
|
},
|
|
1364
1436
|
validation_note:
|
|
1365
1437
|
"Parse agent response as JSON and validate with BeadTreeSchema from schemas/bead.ts",
|
|
1366
|
-
cass_history:
|
|
1367
|
-
? {
|
|
1368
|
-
queried: true,
|
|
1369
|
-
results_found: cassResult.results.length,
|
|
1370
|
-
included_in_context: cassResult.results.length > 0,
|
|
1371
|
-
}
|
|
1372
|
-
: { queried: false, reason: "disabled or unavailable" },
|
|
1438
|
+
cass_history: cassResultInfo,
|
|
1373
1439
|
},
|
|
1374
1440
|
null,
|
|
1375
1441
|
2,
|
|
@@ -1726,9 +1792,21 @@ async function runUbsScan(files: string[]): Promise<UbsScanResult | null> {
|
|
|
1726
1792
|
|
|
1727
1793
|
try {
|
|
1728
1794
|
const parsed = JSON.parse(output);
|
|
1795
|
+
|
|
1796
|
+
// Basic validation of structure
|
|
1797
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
1798
|
+
throw new Error("UBS output is not an object");
|
|
1799
|
+
}
|
|
1800
|
+
if (!Array.isArray(parsed.bugs)) {
|
|
1801
|
+
console.warn("[swarm] UBS output missing bugs array, using empty");
|
|
1802
|
+
}
|
|
1803
|
+
if (typeof parsed.summary !== "object" || parsed.summary === null) {
|
|
1804
|
+
console.warn("[swarm] UBS output missing summary object, using empty");
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1729
1807
|
return {
|
|
1730
1808
|
exitCode: result.exitCode,
|
|
1731
|
-
bugs: parsed.bugs
|
|
1809
|
+
bugs: Array.isArray(parsed.bugs) ? parsed.bugs : [],
|
|
1732
1810
|
summary: parsed.summary || {
|
|
1733
1811
|
total: 0,
|
|
1734
1812
|
critical: 0,
|
|
@@ -1737,8 +1815,13 @@ async function runUbsScan(files: string[]): Promise<UbsScanResult | null> {
|
|
|
1737
1815
|
low: 0,
|
|
1738
1816
|
},
|
|
1739
1817
|
};
|
|
1740
|
-
} catch {
|
|
1741
|
-
// UBS output wasn't JSON
|
|
1818
|
+
} catch (error) {
|
|
1819
|
+
// UBS output wasn't JSON - this is an error condition
|
|
1820
|
+
console.error(
|
|
1821
|
+
`[swarm] CRITICAL: UBS scan failed to parse JSON output:`,
|
|
1822
|
+
error,
|
|
1823
|
+
);
|
|
1824
|
+
console.error(`[swarm] Raw output:`, output);
|
|
1742
1825
|
return {
|
|
1743
1826
|
exitCode: result.exitCode,
|
|
1744
1827
|
bugs: [],
|
|
@@ -1937,10 +2020,17 @@ export const swarm_complete = tool({
|
|
|
1937
2020
|
}
|
|
1938
2021
|
|
|
1939
2022
|
// Release file reservations for this agent
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
2023
|
+
try {
|
|
2024
|
+
await mcpCall("release_file_reservations", {
|
|
2025
|
+
project_key: args.project_key,
|
|
2026
|
+
agent_name: args.agent_name,
|
|
2027
|
+
});
|
|
2028
|
+
} catch (error) {
|
|
2029
|
+
console.warn(
|
|
2030
|
+
`[swarm] Failed to release file reservations for ${args.agent_name}:`,
|
|
2031
|
+
error,
|
|
2032
|
+
);
|
|
2033
|
+
}
|
|
1944
2034
|
|
|
1945
2035
|
// Extract epic ID
|
|
1946
2036
|
const epicId = args.bead_id.includes(".")
|