opencode-swarm-plugin 0.42.5 → 0.42.6

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.
@@ -1102,4 +1102,169 @@ describe("getPromptInsights", () => {
1102
1102
  }
1103
1103
  });
1104
1104
  });
1105
+
1106
+ describe("getPromptInsights integration with swarm-insights", () => {
1107
+ test("coordinator role uses swarm-insights data layer", async () => {
1108
+ const { getPromptInsights } = await import("./swarm-prompts");
1109
+
1110
+ // Should call new data layer, not old swarm-mail analytics
1111
+ const result = await getPromptInsights({ role: "coordinator" });
1112
+
1113
+ // If we have data, it should be formatted by formatInsightsForPrompt
1114
+ if (result.length > 0) {
1115
+ // New format has "Historical Insights" section
1116
+ expect(result).toMatch(/Historical Insights|Strategy Performance|Common Pitfalls/i);
1117
+ }
1118
+ });
1119
+
1120
+ test("coordinator insights have expected structure when data exists", async () => {
1121
+ const { getPromptInsights } = await import("./swarm-prompts");
1122
+ const result = await getPromptInsights({ role: "coordinator" });
1123
+
1124
+ // Should have Historical Insights header
1125
+ if (result.length > 0) {
1126
+ expect(result).toContain("📊 Historical Insights");
1127
+ expect(result).toContain("Use these learnings when selecting decomposition strategies");
1128
+ }
1129
+ });
1130
+
1131
+ test("coordinator insights use formatInsightsForPrompt output", async () => {
1132
+ const { getPromptInsights } = await import("./swarm-prompts");
1133
+ const result = await getPromptInsights({ role: "coordinator" });
1134
+
1135
+ // formatInsightsForPrompt produces specific markdown patterns
1136
+ if (result.length > 0 && result.includes("Strategy")) {
1137
+ // Should have Strategy Performance or Common Pitfalls sections
1138
+ const hasExpectedSections =
1139
+ result.includes("Strategy Performance") ||
1140
+ result.includes("Common Pitfalls");
1141
+ expect(hasExpectedSections).toBe(true);
1142
+ }
1143
+ });
1144
+
1145
+ test("coordinator insights are concise (<500 tokens)", async () => {
1146
+ const { getPromptInsights } = await import("./swarm-prompts");
1147
+ const result = await getPromptInsights({ role: "coordinator" });
1148
+
1149
+ // formatInsightsForPrompt enforces maxTokens=500 by default
1150
+ // Rough estimate: 4 chars per token = 2000 chars max
1151
+ if (result.length > 0) {
1152
+ expect(result.length).toBeLessThan(2000);
1153
+ }
1154
+ });
1155
+
1156
+ test("gracefully handles missing data", async () => {
1157
+ const { getPromptInsights } = await import("./swarm-prompts");
1158
+
1159
+ // Should not throw if database is empty or missing
1160
+ const result = await getPromptInsights({ role: "coordinator" });
1161
+
1162
+ // Empty string is acceptable when no data
1163
+ expect(typeof result).toBe("string");
1164
+ });
1165
+
1166
+ test("imports from swarm-insights module", async () => {
1167
+ // Verify the imports exist
1168
+ const insights = await import("./swarm-insights");
1169
+
1170
+ expect(insights.getStrategyInsights).toBeDefined();
1171
+ expect(insights.getPatternInsights).toBeDefined();
1172
+ expect(insights.formatInsightsForPrompt).toBeDefined();
1173
+ });
1174
+ });
1175
+
1176
+ describe("worker insights integration with swarm-insights", () => {
1177
+ test("worker role uses getFileInsights from swarm-insights data layer", async () => {
1178
+ const { getPromptInsights } = await import("./swarm-prompts");
1179
+
1180
+ // Should call getFileInsights for file-specific insights
1181
+ const result = await getPromptInsights({
1182
+ role: "worker",
1183
+ files: ["src/auth.ts", "src/db.ts"],
1184
+ domain: "authentication"
1185
+ });
1186
+
1187
+ // If we have data, it should be formatted by formatInsightsForPrompt
1188
+ if (result.length > 0) {
1189
+ // New format has "File-Specific Gotchas" or semantic memory learnings
1190
+ expect(result).toMatch(/File-Specific Gotchas|Relevant Learnings/i);
1191
+ }
1192
+ });
1193
+
1194
+ test("worker insights include file-specific gotchas when available", async () => {
1195
+ const { getPromptInsights } = await import("./swarm-prompts");
1196
+ const result = await getPromptInsights({
1197
+ role: "worker",
1198
+ files: ["src/test-file.ts"],
1199
+ });
1200
+
1201
+ // Should contain either file gotchas or semantic memory results
1202
+ if (result.length > 0) {
1203
+ const hasFileInsights =
1204
+ result.includes("File-Specific Gotchas") ||
1205
+ result.includes("Relevant Learnings");
1206
+ expect(hasFileInsights).toBe(true);
1207
+ }
1208
+ });
1209
+
1210
+ test("worker insights combine event store failures + semantic memory", async () => {
1211
+ const { getPromptInsights } = await import("./swarm-prompts");
1212
+ const result = await getPromptInsights({
1213
+ role: "worker",
1214
+ files: ["src/complex.ts"],
1215
+ domain: "complex feature"
1216
+ });
1217
+
1218
+ // Should potentially have both sources of insight
1219
+ // At minimum, should return string (empty if no data)
1220
+ expect(typeof result).toBe("string");
1221
+ });
1222
+
1223
+ test("worker insights are concise (<300 tokens per file)", async () => {
1224
+ const { getPromptInsights } = await import("./swarm-prompts");
1225
+ const result = await getPromptInsights({
1226
+ role: "worker",
1227
+ files: ["src/file1.ts", "src/file2.ts", "src/file3.ts"],
1228
+ });
1229
+
1230
+ // <300 tokens per file = 900 tokens max for 3 files
1231
+ // Rough estimate: 4 chars per token = 3600 chars max
1232
+ if (result.length > 0) {
1233
+ expect(result.length).toBeLessThan(3600);
1234
+ }
1235
+ });
1236
+
1237
+ test("formatSubtaskPromptV2 includes file insights in shared_context", async () => {
1238
+ const result = await formatSubtaskPromptV2({
1239
+ bead_id: "test-123",
1240
+ epic_id: "epic-456",
1241
+ subtask_title: "Implement auth",
1242
+ subtask_description: "Add authentication flow",
1243
+ files: ["src/auth.ts", "src/user.ts"],
1244
+ shared_context: "Original context from coordinator",
1245
+ });
1246
+
1247
+ // shared_context should be replaced and insights potentially included
1248
+ // At minimum, the original context should be in the prompt
1249
+ expect(result).toContain("Original context from coordinator");
1250
+ });
1251
+
1252
+ test("worker insights gracefully handle missing files parameter", async () => {
1253
+ const { getPromptInsights } = await import("./swarm-prompts");
1254
+
1255
+ // Should not throw with no files or domain
1256
+ const result = await getPromptInsights({ role: "worker" });
1257
+
1258
+ // Empty string is acceptable when no context to query
1259
+ expect(typeof result).toBe("string");
1260
+ });
1261
+
1262
+ test("worker insights use swarm-insights getFileInsights", async () => {
1263
+ // Verify the function is imported
1264
+ const insights = await import("./swarm-insights");
1265
+
1266
+ expect(insights.getFileInsights).toBeDefined();
1267
+ expect(typeof insights.getFileInsights).toBe("function");
1268
+ });
1269
+ });
1105
1270
  });
@@ -1188,8 +1188,9 @@ export async function getPromptInsights(
1188
1188
  */
1189
1189
  async function getCoordinatorInsights(project_key?: string): Promise<string> {
1190
1190
  try {
1191
- // Import swarm-mail and analytics
1192
- const { createLibSQLAdapter, createSwarmMailAdapter, strategySuccessRates } = await import("swarm-mail");
1191
+ // Import swarm-mail and swarm-insights modules
1192
+ const { createLibSQLAdapter, createSwarmMailAdapter } = await import("swarm-mail");
1193
+ const { getStrategyInsights, getPatternInsights, formatInsightsForPrompt } = await import("./swarm-insights.js");
1193
1194
 
1194
1195
  // Create libSQL database adapter
1195
1196
  const dbAdapter = await createLibSQLAdapter({ url: "file:./.swarm-mail/streams.db" });
@@ -1197,48 +1198,32 @@ async function getCoordinatorInsights(project_key?: string): Promise<string> {
1197
1198
  // Create swarm-mail adapter with database
1198
1199
  const adapter = createSwarmMailAdapter(dbAdapter, project_key || "default");
1199
1200
 
1200
- // Get database for raw queries
1201
- const db = await adapter.getDatabase();
1201
+ // Query insights from the new data layer
1202
+ const [strategies, patterns] = await Promise.all([
1203
+ getStrategyInsights(adapter, ""),
1204
+ getPatternInsights(adapter),
1205
+ ]);
1202
1206
 
1203
- // Query strategy success rates
1204
- const query = strategySuccessRates({ project_key });
1205
- const result = await db.query(query.sql, Object.values(query.parameters || {}));
1207
+ // Bundle insights
1208
+ const bundle = {
1209
+ strategies,
1210
+ patterns,
1211
+ };
1212
+
1213
+ // Format for prompt injection (<500 tokens)
1214
+ const formatted = formatInsightsForPrompt(bundle, { maxTokens: 500 });
1206
1215
 
1207
- if (!result || !result.rows || result.rows.length === 0) {
1216
+ if (!formatted) {
1208
1217
  return "";
1209
1218
  }
1210
1219
 
1211
- // Format as markdown table
1212
- const rows = result.rows.map((r: any) => {
1213
- const strategy = r.strategy || "unknown";
1214
- const total = r.total_attempts || 0;
1215
- const successRate = r.success_rate || 0;
1216
- const emoji = successRate >= 80 ? "✅" : successRate >= 60 ? "⚠️" : "❌";
1217
-
1218
- return `| ${emoji} ${strategy} | ${successRate.toFixed(1)}% | ${total} |`;
1219
- });
1220
-
1221
- // Limit to top 5 strategies to prevent context bloat
1222
- const topRows = rows.slice(0, 5);
1223
-
1224
- // Add anti-pattern hints for low-success strategies
1225
- const antiPatterns = result.rows
1226
- .filter((r: any) => r.success_rate < 60)
1227
- .map((r: any) => `- AVOID: ${r.strategy} strategy (${r.success_rate.toFixed(1)}% success rate)`)
1228
- .slice(0, 3);
1229
-
1230
- const antiPatternsSection = antiPatterns.length > 0
1231
- ? `\n\n**Anti-Patterns:**\n${antiPatterns.join("\n")}`
1232
- : "";
1233
-
1220
+ // Add section header
1234
1221
  return `
1235
- ## 📊 Swarm Insights (Strategy Success Rates)
1222
+ ## 📊 Historical Insights
1236
1223
 
1237
- | Strategy | Success Rate | Total Attempts |
1238
- |----------|--------------|----------------|
1239
- ${topRows.join("\n")}
1224
+ ${formatted}
1240
1225
 
1241
- **Use these insights to select decomposition strategies.**${antiPatternsSection}
1226
+ **Use these learnings when selecting decomposition strategies and planning subtasks.**
1242
1227
  `;
1243
1228
  } catch (e) {
1244
1229
  console.warn("Failed to get coordinator insights:", e);
@@ -1254,7 +1239,10 @@ async function getWorkerInsights(
1254
1239
  domain?: string,
1255
1240
  ): Promise<string> {
1256
1241
  try {
1257
- const adapter = await getMemoryAdapter();
1242
+ // Import swarm-mail and swarm-insights modules
1243
+ const { createLibSQLAdapter, createSwarmMailAdapter } = await import("swarm-mail");
1244
+ const { getFileInsights, formatInsightsForPrompt } = await import("./swarm-insights.js");
1245
+ const memoryAdapter = await getMemoryAdapter();
1258
1246
 
1259
1247
  // Build query from files and domain
1260
1248
  let query = "";
@@ -1270,31 +1258,61 @@ async function getWorkerInsights(
1270
1258
  return ""; // No context to query
1271
1259
  }
1272
1260
 
1273
- // Query semantic memory for relevant learnings
1274
- const result = await adapter.find({
1275
- query: `${query} gotcha pitfall pattern bug`,
1276
- limit: 3,
1277
- });
1261
+ // Query BOTH event store (via swarm-insights) AND semantic memory
1262
+ const [fileInsights, memoryResult] = await Promise.all([
1263
+ // Get file-specific insights from event store
1264
+ (async () => {
1265
+ if (!files || files.length === 0) return [];
1266
+ try {
1267
+ const dbAdapter = await createLibSQLAdapter({ url: "file:./.swarm-mail/streams.db" });
1268
+ const swarmMail = createSwarmMailAdapter(dbAdapter, "default");
1269
+ return await getFileInsights(swarmMail, files);
1270
+ } catch (e) {
1271
+ console.warn("Failed to get file insights from event store:", e);
1272
+ return [];
1273
+ }
1274
+ })(),
1275
+
1276
+ // Get domain/file learnings from semantic memory
1277
+ memoryAdapter.find({
1278
+ query: `${query} gotcha pitfall pattern bug`,
1279
+ limit: 3,
1280
+ }),
1281
+ ]);
1278
1282
 
1279
- if (result.count === 0) {
1280
- return "";
1281
- }
1283
+ // Bundle insights for formatting
1284
+ const bundle = {
1285
+ files: fileInsights,
1286
+ };
1282
1287
 
1283
- // Format as bullet list
1284
- const learnings = result.results.map((r) => {
1285
- const content = r.content.length > 150
1286
- ? r.content.slice(0, 150) + "..."
1287
- : r.content;
1288
- return `- ${content}`;
1289
- });
1288
+ // Format file insights using swarm-insights formatter
1289
+ const formattedFileInsights = formatInsightsForPrompt(bundle, { maxTokens: 300 });
1290
1290
 
1291
- return `
1292
- ## 💡 Relevant Learnings (from past agents)
1291
+ // Format semantic memory learnings
1292
+ let formattedMemory = "";
1293
+ if (memoryResult.count > 0) {
1294
+ const learnings = memoryResult.results.map((r) => {
1295
+ const content = r.content.length > 150
1296
+ ? r.content.slice(0, 150) + "..."
1297
+ : r.content;
1298
+ return `- ${content}`;
1299
+ });
1300
+
1301
+ formattedMemory = `## 💡 Relevant Learnings (from past agents)
1293
1302
 
1294
1303
  ${learnings.join("\n")}
1295
1304
 
1296
- **Check semantic-memory for full details if needed.**
1297
- `;
1305
+ **Check semantic-memory for full details if needed.**`;
1306
+ }
1307
+
1308
+ // Combine both sources
1309
+ const sections = [formattedFileInsights, formattedMemory].filter(s => s.length > 0);
1310
+
1311
+ if (sections.length === 0) {
1312
+ return "";
1313
+ }
1314
+
1315
+ return sections.join("\n\n");
1298
1316
  } catch (e) {
1299
1317
  console.warn("Failed to get worker insights:", e);
1300
1318
  return "";