opencode-swarm-plugin 0.28.1 → 0.29.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/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +69 -0
- package/dist/compaction-hook.d.ts +20 -2
- package/dist/compaction-hook.d.ts.map +1 -1
- package/dist/hive.d.ts +8 -8
- package/dist/index.d.ts +2 -2
- package/dist/index.js +181 -78
- package/dist/plugin.js +70 -69
- package/dist/swarm-orchestrate.d.ts.map +1 -1
- package/dist/tool-availability.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/compaction-hook.test.ts +110 -0
- package/src/compaction-hook.ts +183 -29
- package/src/hive.integration.test.ts +5 -5
- package/src/hive.ts +7 -7
- package/src/learning.integration.test.ts +11 -3
- package/src/swarm-orchestrate.ts +76 -79
- package/src/swarm.integration.test.ts +88 -2
- package/src/tool-availability.ts +6 -24
package/dist/plugin.js
CHANGED
|
@@ -27156,7 +27156,6 @@ var EpicCreateResultSchema = exports_external.object({
|
|
|
27156
27156
|
subtasks: exports_external.array(CellSchema),
|
|
27157
27157
|
rollback_hint: exports_external.string().optional()
|
|
27158
27158
|
});
|
|
27159
|
-
var BeadSchema = CellSchema;
|
|
27160
27159
|
// src/schemas/evaluation.ts
|
|
27161
27160
|
init_zod();
|
|
27162
27161
|
var CriterionEvaluationSchema = exports_external.object({
|
|
@@ -28015,30 +28014,30 @@ var hive_sync = tool({
|
|
|
28015
28014
|
var hive_link_thread = tool({
|
|
28016
28015
|
description: "Add metadata linking cell to Agent Mail thread",
|
|
28017
28016
|
args: {
|
|
28018
|
-
|
|
28017
|
+
bead_id: tool.schema.string().describe("Cell ID"),
|
|
28019
28018
|
thread_id: tool.schema.string().describe("Agent Mail thread ID")
|
|
28020
28019
|
},
|
|
28021
28020
|
async execute(args, ctx) {
|
|
28022
28021
|
const projectKey = getHiveWorkingDirectory();
|
|
28023
28022
|
const adapter = await getHiveAdapter(projectKey);
|
|
28024
28023
|
try {
|
|
28025
|
-
const cell = await adapter.getCell(projectKey, args.
|
|
28024
|
+
const cell = await adapter.getCell(projectKey, args.bead_id);
|
|
28026
28025
|
if (!cell) {
|
|
28027
|
-
throw new HiveError(`Cell not found: ${args.
|
|
28026
|
+
throw new HiveError(`Cell not found: ${args.bead_id}`, "hive_link_thread");
|
|
28028
28027
|
}
|
|
28029
28028
|
const existingDesc = cell.description || "";
|
|
28030
28029
|
const threadMarker = `[thread:${args.thread_id}]`;
|
|
28031
28030
|
if (existingDesc.includes(threadMarker)) {
|
|
28032
|
-
return `Cell ${args.
|
|
28031
|
+
return `Cell ${args.bead_id} already linked to thread ${args.thread_id}`;
|
|
28033
28032
|
}
|
|
28034
28033
|
const newDesc = existingDesc ? `${existingDesc}
|
|
28035
28034
|
|
|
28036
28035
|
${threadMarker}` : threadMarker;
|
|
28037
|
-
await adapter.updateCell(projectKey, args.
|
|
28036
|
+
await adapter.updateCell(projectKey, args.bead_id, {
|
|
28038
28037
|
description: newDesc
|
|
28039
28038
|
});
|
|
28040
|
-
await adapter.markDirty(projectKey, args.
|
|
28041
|
-
return `Linked cell ${args.
|
|
28039
|
+
await adapter.markDirty(projectKey, args.bead_id);
|
|
28040
|
+
return `Linked cell ${args.bead_id} to thread ${args.thread_id}`;
|
|
28042
28041
|
} catch (error45) {
|
|
28043
28042
|
const message = error45 instanceof Error ? error45.message : String(error45);
|
|
28044
28043
|
throw new HiveError(`Failed to link thread: ${message}`, "hive_link_thread");
|
|
@@ -28269,27 +28268,11 @@ var toolCheckers = {
|
|
|
28269
28268
|
}
|
|
28270
28269
|
},
|
|
28271
28270
|
beads: async () => {
|
|
28272
|
-
|
|
28273
|
-
|
|
28274
|
-
|
|
28275
|
-
|
|
28276
|
-
|
|
28277
|
-
error: "bd command not found"
|
|
28278
|
-
};
|
|
28279
|
-
}
|
|
28280
|
-
try {
|
|
28281
|
-
const result = await Bun.$`bd --version`.quiet().nothrow();
|
|
28282
|
-
return {
|
|
28283
|
-
available: result.exitCode === 0,
|
|
28284
|
-
checkedAt: new Date().toISOString()
|
|
28285
|
-
};
|
|
28286
|
-
} catch (e) {
|
|
28287
|
-
return {
|
|
28288
|
-
available: false,
|
|
28289
|
-
checkedAt: new Date().toISOString(),
|
|
28290
|
-
error: String(e)
|
|
28291
|
-
};
|
|
28292
|
-
}
|
|
28271
|
+
return {
|
|
28272
|
+
available: false,
|
|
28273
|
+
checkedAt: new Date().toISOString(),
|
|
28274
|
+
error: "bd CLI is deprecated - use hive_* tools with HiveAdapter instead"
|
|
28275
|
+
};
|
|
28293
28276
|
},
|
|
28294
28277
|
"swarm-mail": async () => {
|
|
28295
28278
|
try {
|
|
@@ -32263,27 +32246,25 @@ var reviewTools = {
|
|
|
32263
32246
|
};
|
|
32264
32247
|
|
|
32265
32248
|
// src/swarm-orchestrate.ts
|
|
32266
|
-
async function queryEpicSubtasks(epicId) {
|
|
32267
|
-
const beadsAvailable = await isToolAvailable("beads");
|
|
32268
|
-
if (!beadsAvailable) {
|
|
32269
|
-
warnMissingTool("beads");
|
|
32270
|
-
return [];
|
|
32271
|
-
}
|
|
32272
|
-
const result = await Bun.$`bd list --parent ${epicId} --json`.quiet().nothrow();
|
|
32273
|
-
if (result.exitCode !== 0) {
|
|
32274
|
-
console.error(`[swarm] ERROR: Failed to query subtasks for epic ${epicId}:`, result.stderr.toString());
|
|
32275
|
-
return [];
|
|
32276
|
-
}
|
|
32249
|
+
async function queryEpicSubtasks(projectKey, epicId) {
|
|
32277
32250
|
try {
|
|
32278
|
-
const
|
|
32279
|
-
|
|
32251
|
+
const adapter = await getHiveAdapter(projectKey);
|
|
32252
|
+
const cells = await adapter.queryCells(projectKey, { parent_id: epicId });
|
|
32253
|
+
return cells.filter((cell) => cell.status !== "tombstone").map((cell) => ({
|
|
32254
|
+
id: cell.id,
|
|
32255
|
+
title: cell.title,
|
|
32256
|
+
description: cell.description || "",
|
|
32257
|
+
status: cell.status,
|
|
32258
|
+
priority: cell.priority,
|
|
32259
|
+
issue_type: cell.type,
|
|
32260
|
+
created_at: new Date(cell.created_at).toISOString(),
|
|
32261
|
+
updated_at: cell.updated_at ? new Date(cell.updated_at).toISOString() : undefined,
|
|
32262
|
+
dependencies: [],
|
|
32263
|
+
metadata: {}
|
|
32264
|
+
}));
|
|
32280
32265
|
} catch (error45) {
|
|
32281
|
-
|
|
32282
|
-
|
|
32283
|
-
return [];
|
|
32284
|
-
}
|
|
32285
|
-
console.error(`[swarm] ERROR: Failed to parse beads for epic ${epicId}:`, error45);
|
|
32286
|
-
throw error45;
|
|
32266
|
+
console.error(`[swarm] ERROR: Failed to query subtasks for epic ${epicId}:`, error45 instanceof Error ? error45.message : String(error45));
|
|
32267
|
+
return [];
|
|
32287
32268
|
}
|
|
32288
32269
|
}
|
|
32289
32270
|
async function querySwarmMessages(projectKey, threadId) {
|
|
@@ -32635,7 +32616,7 @@ var swarm_status = tool({
|
|
|
32635
32616
|
project_key: tool.schema.string().describe("Project path (for Agent Mail queries)")
|
|
32636
32617
|
},
|
|
32637
32618
|
async execute(args) {
|
|
32638
|
-
const subtasks = await queryEpicSubtasks(args.epic_id);
|
|
32619
|
+
const subtasks = await queryEpicSubtasks(args.project_key, args.epic_id);
|
|
32639
32620
|
const statusCounts = {
|
|
32640
32621
|
running: 0,
|
|
32641
32622
|
completed: 0,
|
|
@@ -32710,8 +32691,13 @@ var swarm_progress = tool({
|
|
|
32710
32691
|
};
|
|
32711
32692
|
const validated = AgentProgressSchema.parse(progress);
|
|
32712
32693
|
if (args.status === "blocked" || args.status === "in_progress") {
|
|
32713
|
-
|
|
32714
|
-
|
|
32694
|
+
try {
|
|
32695
|
+
const adapter = await getHiveAdapter(args.project_key);
|
|
32696
|
+
const newStatus = args.status === "blocked" ? "blocked" : "in_progress";
|
|
32697
|
+
await adapter.changeCellStatus(args.project_key, args.bead_id, newStatus);
|
|
32698
|
+
} catch (error45) {
|
|
32699
|
+
console.error(`[swarm] Failed to update cell status: ${error45 instanceof Error ? error45.message : String(error45)}`);
|
|
32700
|
+
}
|
|
32715
32701
|
}
|
|
32716
32702
|
const epicId = args.bead_id.includes(".") ? args.bead_id.split(".")[0] : args.bead_id;
|
|
32717
32703
|
await sendSwarmMessage3({
|
|
@@ -32855,6 +32841,22 @@ Or use skip_review=true to bypass (not recommended for production work).`
|
|
|
32855
32841
|
}
|
|
32856
32842
|
try {
|
|
32857
32843
|
const projectKey = args.project_key.replace(/\//g, "-").replace(/\\/g, "-");
|
|
32844
|
+
const adapter = await getHiveAdapter(args.project_key);
|
|
32845
|
+
const cell = await adapter.getCell(projectKey, args.bead_id);
|
|
32846
|
+
if (!cell) {
|
|
32847
|
+
return JSON.stringify({
|
|
32848
|
+
success: false,
|
|
32849
|
+
error: `Bead not found: ${args.bead_id}`,
|
|
32850
|
+
hint: "Check the bead ID is correct. Use hive_query to list open cells."
|
|
32851
|
+
});
|
|
32852
|
+
}
|
|
32853
|
+
if (cell.status === "closed") {
|
|
32854
|
+
return JSON.stringify({
|
|
32855
|
+
success: false,
|
|
32856
|
+
error: `Bead already closed: ${args.bead_id}`,
|
|
32857
|
+
hint: "This bead was already completed. No action needed."
|
|
32858
|
+
});
|
|
32859
|
+
}
|
|
32858
32860
|
let agentRegistered = false;
|
|
32859
32861
|
let registrationWarning = "";
|
|
32860
32862
|
try {
|
|
@@ -32939,32 +32941,25 @@ Continuing with completion, but this should be fixed for future subtasks.`;
|
|
|
32939
32941
|
}, null, 2);
|
|
32940
32942
|
}
|
|
32941
32943
|
}
|
|
32942
|
-
|
|
32943
|
-
|
|
32944
|
-
|
|
32945
|
-
const
|
|
32946
|
-
const isNoDatabaseError = stderrOutput.includes("no beads database found");
|
|
32947
|
-
const isNotFoundError = stderrOutput.includes("not found") || stderrOutput.includes("does not exist");
|
|
32944
|
+
try {
|
|
32945
|
+
await adapter.closeCell(args.project_key, args.bead_id, args.summary);
|
|
32946
|
+
} catch (closeError) {
|
|
32947
|
+
const errorMessage = closeError instanceof Error ? closeError.message : String(closeError);
|
|
32948
32948
|
return JSON.stringify({
|
|
32949
32949
|
success: false,
|
|
32950
32950
|
error: "Failed to close cell",
|
|
32951
|
-
failed_step: "
|
|
32952
|
-
details:
|
|
32951
|
+
failed_step: "closeCell",
|
|
32952
|
+
details: errorMessage,
|
|
32953
32953
|
bead_id: args.bead_id,
|
|
32954
32954
|
project_key: args.project_key,
|
|
32955
32955
|
recovery: {
|
|
32956
|
-
steps:
|
|
32957
|
-
`1.
|
|
32958
|
-
`2. Check
|
|
32959
|
-
`3. Cell ID prefix "${args.bead_id.split("-")[0]}" should match project`,
|
|
32960
|
-
`4. Try: hive_close(id="${args.bead_id}", reason="...")`
|
|
32961
|
-
] : [
|
|
32962
|
-
`1. Check cell exists: bd show ${args.bead_id}`,
|
|
32963
|
-
`2. Check cell status (might already be closed): hive_query()`,
|
|
32956
|
+
steps: [
|
|
32957
|
+
`1. Check cell exists: hive_query()`,
|
|
32958
|
+
`2. Check cell status (might already be closed)`,
|
|
32964
32959
|
`3. If cell is blocked, unblock first: hive_update(id="${args.bead_id}", status="in_progress")`,
|
|
32965
32960
|
`4. Try closing directly: hive_close(id="${args.bead_id}", reason="...")`
|
|
32966
32961
|
],
|
|
32967
|
-
hint:
|
|
32962
|
+
hint: "Cell may already be closed, or the ID is incorrect."
|
|
32968
32963
|
}
|
|
32969
32964
|
}, null, 2);
|
|
32970
32965
|
}
|
|
@@ -33165,12 +33160,13 @@ ${errorStack.slice(0, 1000)}
|
|
|
33165
33160
|
}
|
|
33166
33161
|
return JSON.stringify({
|
|
33167
33162
|
success: false,
|
|
33168
|
-
error: errorMessage
|
|
33163
|
+
error: `swarm_complete failed: ${errorMessage}`,
|
|
33169
33164
|
failed_step: failedStep,
|
|
33170
33165
|
bead_id: args.bead_id,
|
|
33171
33166
|
agent_name: args.agent_name,
|
|
33172
33167
|
coordinator_notified: notificationSent,
|
|
33173
33168
|
stack_trace: errorStack?.slice(0, 500),
|
|
33169
|
+
hint: "Check the error message above. Common issues: bead not found, session not initialized.",
|
|
33174
33170
|
context: {
|
|
33175
33171
|
summary: args.summary,
|
|
33176
33172
|
files_touched: args.files_touched || [],
|
|
@@ -35105,6 +35101,11 @@ var sessionStats = {
|
|
|
35105
35101
|
|
|
35106
35102
|
// src/index.ts
|
|
35107
35103
|
init_skills();
|
|
35104
|
+
|
|
35105
|
+
// src/compaction-hook.ts
|
|
35106
|
+
import { checkSwarmHealth as checkSwarmHealth3 } from "swarm-mail";
|
|
35107
|
+
|
|
35108
|
+
// src/index.ts
|
|
35108
35109
|
var SwarmPlugin = async (input) => {
|
|
35109
35110
|
const { $, directory } = input;
|
|
35110
35111
|
setHiveWorkingDirectory(directory);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"swarm-orchestrate.d.ts","sourceRoot":"","sources":["../src/swarm-orchestrate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAGH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;
|
|
1
|
+
{"version":3,"file":"swarm-orchestrate.d.ts","sourceRoot":"","sources":["../src/swarm-orchestrate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAGH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AA8hBxB;;;;;;;;;;GAUG;AACH,eAAO,MAAM,UAAU;;;;;;;;;;;;;CA8JrB,CAAC;AAEH;;;;GAIG;AACH,eAAO,MAAM,YAAY;;;;;;;;;;CAoFvB,CAAC;AAEH;;;;GAIG;AACH,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;CAkHzB,CAAC;AAEH;;;;;;;;GAQG;AACH,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;CA6E1B,CAAC;AAEH;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAqnBzB,CAAC;AAEH;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkJ/B,CAAC;AAEH;;;;;;;;GAQG;AACH,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;CA6CjC,CAAC;AAEH;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB;;;;;;;;;;CAmClC,CAAC;AAEH;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB;;;;;;;;CAmB9B,CAAC;AAEH;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;CAoJ9B,CAAC;AA4BH;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAqG3B,CAAC;AAEH;;;;;;;;GAQG;AACH,eAAO,MAAM,aAAa;;;;;;;;;;CAsGxB,CAAC;AAEH;;;;;;;;GAQG;AACH,eAAO,MAAM,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAgMtB,CAAC;AAMH,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAc5B,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tool-availability.d.ts","sourceRoot":"","sources":["../src/tool-availability.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAUH,MAAM,MAAM,QAAQ,GAChB,iBAAiB,GACjB,MAAM,GACN,KAAK,GACL,MAAM,GACN,OAAO,GACP,YAAY,GACZ,YAAY,CAAC;AAEjB,MAAM,WAAW,UAAU;IACzB,SAAS,EAAE,OAAO,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,QAAQ,CAAC;IACf,MAAM,EAAE,UAAU,CAAC;IACnB,gBAAgB,EAAE,MAAM,CAAC;CAC1B;
|
|
1
|
+
{"version":3,"file":"tool-availability.d.ts","sourceRoot":"","sources":["../src/tool-availability.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAUH,MAAM,MAAM,QAAQ,GAChB,iBAAiB,GACjB,MAAM,GACN,KAAK,GACL,MAAM,GACN,OAAO,GACP,YAAY,GACZ,YAAY,CAAC;AAEjB,MAAM,WAAW,UAAU;IACzB,SAAS,EAAE,OAAO,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,QAAQ,CAAC;IACf,MAAM,EAAE,UAAU,CAAC;IACnB,gBAAgB,EAAE,MAAM,CAAC;CAC1B;AA6OD;;;;;GAKG;AACH,wBAAsB,SAAS,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC,UAAU,CAAC,CAWnE;AAED;;GAEG;AACH,wBAAsB,eAAe,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC,CAGtE;AAED;;GAEG;AACH,wBAAsB,mBAAmB,CACvC,IAAI,EAAE,QAAQ,GACb,OAAO,CAAC,gBAAgB,CAAC,CAO3B;AAED;;GAEG;AACH,wBAAsB,aAAa,IAAI,OAAO,CAC5C,GAAG,CAAC,QAAQ,EAAE,gBAAgB,CAAC,CAChC,CA0BA;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,QAAQ,GAAG,IAAI,CAQpD;AAED;;;;GAIG;AACH,wBAAsB,WAAW,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAO/D;AAED;;;;;;;GAOG;AACH,wBAAsB,gBAAgB,CAAC,CAAC,EACtC,IAAI,EAAE,QAAQ,EACd,MAAM,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EACxB,QAAQ,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,GAC7B,OAAO,CAAC,CAAC,CAAC,CASZ;AAED;;GAEG;AACH,wBAAsB,eAAe,CAAC,CAAC,EACrC,IAAI,EAAE,QAAQ,EACd,MAAM,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GACvB,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC,CASxB;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,cAAc,IAAI,IAAI,CAGrC;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CACpC,YAAY,EAAE,GAAG,CAAC,QAAQ,EAAE,gBAAgB,CAAC,GAC5C,MAAM,CAWR"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-swarm-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.29.0",
|
|
4
4
|
"description": "Multi-agent swarm coordination for OpenCode with learning capabilities, beads integration, and Agent Mail",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"@opencode-ai/plugin": "^1.0.134",
|
|
35
35
|
"gray-matter": "^4.0.3",
|
|
36
36
|
"ioredis": "^5.4.1",
|
|
37
|
-
"swarm-mail": "0.
|
|
37
|
+
"swarm-mail": "0.4.0",
|
|
38
38
|
"zod": "4.1.8"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for Swarm-Aware Compaction Hook
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, expect, it, mock } from "bun:test";
|
|
6
|
+
import {
|
|
7
|
+
SWARM_COMPACTION_CONTEXT,
|
|
8
|
+
SWARM_DETECTION_FALLBACK,
|
|
9
|
+
createCompactionHook,
|
|
10
|
+
} from "./compaction-hook";
|
|
11
|
+
|
|
12
|
+
// Mock the dependencies
|
|
13
|
+
mock.module("./hive", () => ({
|
|
14
|
+
getHiveWorkingDirectory: () => "/test/project",
|
|
15
|
+
getHiveAdapter: async () => ({
|
|
16
|
+
queryCells: async () => [],
|
|
17
|
+
}),
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
mock.module("swarm-mail", () => ({
|
|
21
|
+
checkSwarmHealth: async () => ({
|
|
22
|
+
healthy: true,
|
|
23
|
+
database: "connected",
|
|
24
|
+
stats: {
|
|
25
|
+
events: 0,
|
|
26
|
+
agents: 0,
|
|
27
|
+
messages: 0,
|
|
28
|
+
reservations: 0,
|
|
29
|
+
},
|
|
30
|
+
}),
|
|
31
|
+
}));
|
|
32
|
+
|
|
33
|
+
describe("Compaction Hook", () => {
|
|
34
|
+
describe("SWARM_COMPACTION_CONTEXT", () => {
|
|
35
|
+
it("contains coordinator instructions", () => {
|
|
36
|
+
expect(SWARM_COMPACTION_CONTEXT).toContain("COORDINATOR");
|
|
37
|
+
expect(SWARM_COMPACTION_CONTEXT).toContain("Keep Cooking");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("contains resume instructions", () => {
|
|
41
|
+
expect(SWARM_COMPACTION_CONTEXT).toContain("swarm_status");
|
|
42
|
+
expect(SWARM_COMPACTION_CONTEXT).toContain("swarmmail_inbox");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("contains summary format", () => {
|
|
46
|
+
expect(SWARM_COMPACTION_CONTEXT).toContain("Swarm State");
|
|
47
|
+
expect(SWARM_COMPACTION_CONTEXT).toContain("Active:");
|
|
48
|
+
expect(SWARM_COMPACTION_CONTEXT).toContain("Blocked:");
|
|
49
|
+
expect(SWARM_COMPACTION_CONTEXT).toContain("Completed:");
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe("SWARM_DETECTION_FALLBACK", () => {
|
|
54
|
+
it("contains detection patterns", () => {
|
|
55
|
+
expect(SWARM_DETECTION_FALLBACK).toContain("swarm_decompose");
|
|
56
|
+
expect(SWARM_DETECTION_FALLBACK).toContain("swarmmail_init");
|
|
57
|
+
expect(SWARM_DETECTION_FALLBACK).toContain("hive_create_epic");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("contains ID patterns", () => {
|
|
61
|
+
expect(SWARM_DETECTION_FALLBACK).toContain("bd-xxx");
|
|
62
|
+
expect(SWARM_DETECTION_FALLBACK).toContain("Agent names");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("contains coordination language", () => {
|
|
66
|
+
expect(SWARM_DETECTION_FALLBACK).toContain("spawn");
|
|
67
|
+
expect(SWARM_DETECTION_FALLBACK).toContain("coordinator");
|
|
68
|
+
expect(SWARM_DETECTION_FALLBACK).toContain("reservation");
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe("createCompactionHook", () => {
|
|
73
|
+
it("returns a function", () => {
|
|
74
|
+
const hook = createCompactionHook();
|
|
75
|
+
expect(typeof hook).toBe("function");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("accepts input and output parameters", async () => {
|
|
79
|
+
const hook = createCompactionHook();
|
|
80
|
+
const input = { sessionID: "test-session" };
|
|
81
|
+
const output = { context: [] as string[] };
|
|
82
|
+
|
|
83
|
+
// Should not throw
|
|
84
|
+
await hook(input, output);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("does not inject context when no swarm detected", async () => {
|
|
88
|
+
const hook = createCompactionHook();
|
|
89
|
+
const output = { context: [] as string[] };
|
|
90
|
+
|
|
91
|
+
await hook({ sessionID: "test" }, output);
|
|
92
|
+
|
|
93
|
+
// With mocked empty data, should not inject
|
|
94
|
+
expect(output.context.length).toBe(0);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe("Detection confidence levels", () => {
|
|
99
|
+
it("HIGH confidence triggers full context", async () => {
|
|
100
|
+
// This would need proper mocking of active reservations
|
|
101
|
+
// For now, just verify the context strings exist
|
|
102
|
+
expect(SWARM_COMPACTION_CONTEXT).toContain("SWARM ACTIVE");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("LOW confidence triggers fallback prompt", async () => {
|
|
106
|
+
expect(SWARM_DETECTION_FALLBACK).toContain("Swarm Detection");
|
|
107
|
+
expect(SWARM_DETECTION_FALLBACK).toContain("Check Your Context");
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
});
|
package/src/compaction-hook.ts
CHANGED
|
@@ -5,6 +5,12 @@
|
|
|
5
5
|
* When context is compacted, this hook injects instructions for the summarizer
|
|
6
6
|
* to preserve swarm coordination state and enable seamless resumption.
|
|
7
7
|
*
|
|
8
|
+
* ## Philosophy: Err on the Side of Continuation
|
|
9
|
+
*
|
|
10
|
+
* It's better to inject swarm context unnecessarily than to lose an active swarm.
|
|
11
|
+
* The cost of a false positive (extra context) is low.
|
|
12
|
+
* The cost of a false negative (lost swarm) is high - wasted work, confused agents.
|
|
13
|
+
*
|
|
8
14
|
* Hook signature (from @opencode-ai/plugin):
|
|
9
15
|
* ```typescript
|
|
10
16
|
* "experimental.session.compacting"?: (
|
|
@@ -23,6 +29,9 @@
|
|
|
23
29
|
* ```
|
|
24
30
|
*/
|
|
25
31
|
|
|
32
|
+
import { getHiveAdapter, getHiveWorkingDirectory } from "./hive";
|
|
33
|
+
import { checkSwarmHealth } from "swarm-mail";
|
|
34
|
+
|
|
26
35
|
// ============================================================================
|
|
27
36
|
// Compaction Context
|
|
28
37
|
// ============================================================================
|
|
@@ -93,50 +102,187 @@ Extract from session context:
|
|
|
93
102
|
**You are not waiting for instructions. You are the coordinator. Coordinate.**
|
|
94
103
|
`;
|
|
95
104
|
|
|
105
|
+
/**
|
|
106
|
+
* Fallback detection prompt - tells the compactor what to look for
|
|
107
|
+
*
|
|
108
|
+
* Used when we can't definitively detect a swarm but want to be safe.
|
|
109
|
+
* The compactor can check the conversation context for these patterns.
|
|
110
|
+
*/
|
|
111
|
+
export const SWARM_DETECTION_FALLBACK = `## 🐝 Swarm Detection - Check Your Context
|
|
112
|
+
|
|
113
|
+
**IMPORTANT:** Before summarizing, check if this session involves an active swarm.
|
|
114
|
+
|
|
115
|
+
Look for ANY of these patterns in the conversation:
|
|
116
|
+
|
|
117
|
+
### Tool Calls (definite swarm sign)
|
|
118
|
+
- \`swarm_decompose\`, \`swarm_spawn_subtask\`, \`swarm_status\`, \`swarm_complete\`
|
|
119
|
+
- \`swarmmail_init\`, \`swarmmail_reserve\`, \`swarmmail_send\`
|
|
120
|
+
- \`hive_create_epic\`, \`hive_start\`, \`hive_close\`
|
|
121
|
+
|
|
122
|
+
### IDs and Names
|
|
123
|
+
- Cell IDs: \`bd-xxx\`, \`bd-xxx.N\` (subtask format)
|
|
124
|
+
- Agent names: BlueLake, RedMountain, GreenValley, etc.
|
|
125
|
+
- Epic references: "epic", "subtask", "parent"
|
|
126
|
+
|
|
127
|
+
### Coordination Language
|
|
128
|
+
- "spawn", "worker", "coordinator"
|
|
129
|
+
- "reserve", "reservation", "files"
|
|
130
|
+
- "blocked", "unblock", "dependency"
|
|
131
|
+
- "progress", "complete", "in_progress"
|
|
132
|
+
|
|
133
|
+
### If You Find Swarm Evidence
|
|
134
|
+
|
|
135
|
+
Include this in your summary:
|
|
136
|
+
1. Epic ID and title
|
|
137
|
+
2. Project path
|
|
138
|
+
3. Subtask status (running/blocked/done/pending)
|
|
139
|
+
4. Any blockers or issues
|
|
140
|
+
5. What should happen next
|
|
141
|
+
|
|
142
|
+
**Then tell the resumed session:**
|
|
143
|
+
"This is an active swarm. Check swarm_status and swarmmail_inbox immediately."
|
|
144
|
+
`;
|
|
145
|
+
|
|
96
146
|
// ============================================================================
|
|
97
|
-
//
|
|
147
|
+
// Swarm Detection
|
|
98
148
|
// ============================================================================
|
|
99
149
|
|
|
150
|
+
/**
|
|
151
|
+
* Detection result with confidence level
|
|
152
|
+
*/
|
|
153
|
+
interface SwarmDetection {
|
|
154
|
+
detected: boolean;
|
|
155
|
+
confidence: "high" | "medium" | "low" | "none";
|
|
156
|
+
reasons: string[];
|
|
157
|
+
}
|
|
158
|
+
|
|
100
159
|
/**
|
|
101
160
|
* Check for swarm sign - evidence a swarm passed through
|
|
102
161
|
*
|
|
103
|
-
*
|
|
104
|
-
* -
|
|
105
|
-
* - Open
|
|
106
|
-
* -
|
|
162
|
+
* Uses multiple signals with different confidence levels:
|
|
163
|
+
* - HIGH: Active reservations, in_progress cells
|
|
164
|
+
* - MEDIUM: Open subtasks, unclosed epics, recent activity
|
|
165
|
+
* - LOW: Any cells exist, swarm-mail initialized
|
|
107
166
|
*
|
|
108
|
-
*
|
|
167
|
+
* Philosophy: Err on the side of continuation.
|
|
109
168
|
*/
|
|
110
|
-
|
|
169
|
+
async function detectSwarm(): Promise<SwarmDetection> {
|
|
170
|
+
const reasons: string[] = [];
|
|
171
|
+
let highConfidence = false;
|
|
172
|
+
let mediumConfidence = false;
|
|
173
|
+
let lowConfidence = false;
|
|
111
174
|
|
|
112
|
-
async function hasSwarmSign(): Promise<boolean> {
|
|
113
175
|
try {
|
|
114
176
|
const projectKey = getHiveWorkingDirectory();
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
(
|
|
129
|
-
|
|
177
|
+
|
|
178
|
+
// Check 1: Active reservations in swarm-mail (HIGH confidence)
|
|
179
|
+
try {
|
|
180
|
+
const health = await checkSwarmHealth(projectKey);
|
|
181
|
+
if (health.healthy && health.stats) {
|
|
182
|
+
if (health.stats.reservations > 0) {
|
|
183
|
+
highConfidence = true;
|
|
184
|
+
reasons.push(`${health.stats.reservations} active file reservations`);
|
|
185
|
+
}
|
|
186
|
+
if (health.stats.agents > 0) {
|
|
187
|
+
mediumConfidence = true;
|
|
188
|
+
reasons.push(`${health.stats.agents} registered agents`);
|
|
189
|
+
}
|
|
190
|
+
if (health.stats.messages > 0) {
|
|
191
|
+
lowConfidence = true;
|
|
192
|
+
reasons.push(`${health.stats.messages} swarm messages`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
} catch {
|
|
196
|
+
// Swarm-mail not available, continue with other checks
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Check 2: Hive cells (various confidence levels)
|
|
200
|
+
try {
|
|
201
|
+
const adapter = await getHiveAdapter(projectKey);
|
|
202
|
+
const cells = await adapter.queryCells(projectKey, {});
|
|
203
|
+
|
|
204
|
+
if (Array.isArray(cells) && cells.length > 0) {
|
|
205
|
+
// HIGH: Any in_progress cells
|
|
206
|
+
const inProgress = cells.filter((c) => c.status === "in_progress");
|
|
207
|
+
if (inProgress.length > 0) {
|
|
208
|
+
highConfidence = true;
|
|
209
|
+
reasons.push(`${inProgress.length} cells in_progress`);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// MEDIUM: Open subtasks (cells with parent_id)
|
|
213
|
+
const subtasks = cells.filter(
|
|
214
|
+
(c) => c.status === "open" && c.parent_id
|
|
215
|
+
);
|
|
216
|
+
if (subtasks.length > 0) {
|
|
217
|
+
mediumConfidence = true;
|
|
218
|
+
reasons.push(`${subtasks.length} open subtasks`);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// MEDIUM: Unclosed epics
|
|
222
|
+
const openEpics = cells.filter(
|
|
223
|
+
(c) => c.type === "epic" && c.status !== "closed"
|
|
224
|
+
);
|
|
225
|
+
if (openEpics.length > 0) {
|
|
226
|
+
mediumConfidence = true;
|
|
227
|
+
reasons.push(`${openEpics.length} unclosed epics`);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// MEDIUM: Recently updated cells (last hour)
|
|
231
|
+
const oneHourAgo = Date.now() - 60 * 60 * 1000;
|
|
232
|
+
const recentCells = cells.filter((c) => c.updated_at > oneHourAgo);
|
|
233
|
+
if (recentCells.length > 0) {
|
|
234
|
+
mediumConfidence = true;
|
|
235
|
+
reasons.push(`${recentCells.length} cells updated in last hour`);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// LOW: Any cells exist at all
|
|
239
|
+
if (cells.length > 0) {
|
|
240
|
+
lowConfidence = true;
|
|
241
|
+
reasons.push(`${cells.length} total cells in hive`);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
} catch {
|
|
245
|
+
// Hive not available, continue
|
|
246
|
+
}
|
|
130
247
|
} catch {
|
|
131
|
-
|
|
248
|
+
// Project detection failed, use fallback
|
|
249
|
+
lowConfidence = true;
|
|
250
|
+
reasons.push("Could not detect project, using fallback");
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Determine overall confidence
|
|
254
|
+
let confidence: "high" | "medium" | "low" | "none";
|
|
255
|
+
if (highConfidence) {
|
|
256
|
+
confidence = "high";
|
|
257
|
+
} else if (mediumConfidence) {
|
|
258
|
+
confidence = "medium";
|
|
259
|
+
} else if (lowConfidence) {
|
|
260
|
+
confidence = "low";
|
|
261
|
+
} else {
|
|
262
|
+
confidence = "none";
|
|
132
263
|
}
|
|
264
|
+
|
|
265
|
+
return {
|
|
266
|
+
detected: confidence !== "none",
|
|
267
|
+
confidence,
|
|
268
|
+
reasons,
|
|
269
|
+
};
|
|
133
270
|
}
|
|
134
271
|
|
|
272
|
+
// ============================================================================
|
|
273
|
+
// Hook Registration
|
|
274
|
+
// ============================================================================
|
|
275
|
+
|
|
135
276
|
/**
|
|
136
277
|
* Create the compaction hook for use in plugin registration
|
|
137
278
|
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
279
|
+
* Injects swarm context based on detection confidence:
|
|
280
|
+
* - HIGH/MEDIUM: Full swarm context (definitely/probably a swarm)
|
|
281
|
+
* - LOW: Fallback detection prompt (let compactor check context)
|
|
282
|
+
* - NONE: No injection (probably not a swarm)
|
|
283
|
+
*
|
|
284
|
+
* Philosophy: Err on the side of continuation. A false positive costs
|
|
285
|
+
* a bit of context space. A false negative loses the swarm.
|
|
140
286
|
*
|
|
141
287
|
* @example
|
|
142
288
|
* ```typescript
|
|
@@ -153,9 +299,17 @@ export function createCompactionHook() {
|
|
|
153
299
|
_input: { sessionID: string },
|
|
154
300
|
output: { context: string[] },
|
|
155
301
|
): Promise<void> => {
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
|
|
302
|
+
const detection = await detectSwarm();
|
|
303
|
+
|
|
304
|
+
if (detection.confidence === "high" || detection.confidence === "medium") {
|
|
305
|
+
// Definite or probable swarm - inject full context
|
|
306
|
+
const header = `[Swarm detected: ${detection.reasons.join(", ")}]\n\n`;
|
|
307
|
+
output.context.push(header + SWARM_COMPACTION_CONTEXT);
|
|
308
|
+
} else if (detection.confidence === "low") {
|
|
309
|
+
// Possible swarm - inject fallback detection prompt
|
|
310
|
+
const header = `[Possible swarm: ${detection.reasons.join(", ")}]\n\n`;
|
|
311
|
+
output.context.push(header + SWARM_DETECTION_FALLBACK);
|
|
159
312
|
}
|
|
313
|
+
// confidence === "none" - no injection, probably not a swarm
|
|
160
314
|
};
|
|
161
315
|
}
|