opencode-swarm-plugin 0.20.0 → 0.22.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.
Files changed (41) hide show
  1. package/.beads/issues.jsonl +213 -0
  2. package/INTEGRATION_EXAMPLE.md +66 -0
  3. package/README.md +352 -522
  4. package/dist/index.js +2046 -984
  5. package/dist/plugin.js +2051 -1017
  6. package/docs/analysis/subagent-coordination-patterns.md +2 -0
  7. package/docs/semantic-memory-cli-syntax.md +123 -0
  8. package/docs/swarm-mail-architecture.md +1147 -0
  9. package/evals/README.md +116 -0
  10. package/evals/evalite.config.ts +15 -0
  11. package/evals/example.eval.ts +32 -0
  12. package/evals/fixtures/decomposition-cases.ts +105 -0
  13. package/evals/lib/data-loader.test.ts +288 -0
  14. package/evals/lib/data-loader.ts +111 -0
  15. package/evals/lib/llm.ts +115 -0
  16. package/evals/scorers/index.ts +200 -0
  17. package/evals/scorers/outcome-scorers.test.ts +27 -0
  18. package/evals/scorers/outcome-scorers.ts +349 -0
  19. package/evals/swarm-decomposition.eval.ts +112 -0
  20. package/package.json +8 -1
  21. package/scripts/cleanup-test-memories.ts +346 -0
  22. package/src/beads.ts +49 -0
  23. package/src/eval-capture.ts +487 -0
  24. package/src/index.ts +45 -3
  25. package/src/learning.integration.test.ts +19 -4
  26. package/src/output-guardrails.test.ts +438 -0
  27. package/src/output-guardrails.ts +381 -0
  28. package/src/schemas/index.ts +18 -0
  29. package/src/schemas/swarm-context.ts +115 -0
  30. package/src/storage.ts +117 -5
  31. package/src/streams/events.test.ts +296 -0
  32. package/src/streams/events.ts +93 -0
  33. package/src/streams/migrations.test.ts +24 -20
  34. package/src/streams/migrations.ts +51 -0
  35. package/src/streams/projections.ts +187 -0
  36. package/src/streams/store.ts +275 -0
  37. package/src/swarm-orchestrate.ts +771 -189
  38. package/src/swarm-prompts.ts +84 -12
  39. package/src/swarm.integration.test.ts +124 -0
  40. package/vitest.integration.config.ts +6 -0
  41. package/vitest.integration.setup.ts +48 -0
@@ -37,6 +37,9 @@ import {
37
37
  releaseSwarmFiles,
38
38
  sendSwarmMessage,
39
39
  } from "./streams/swarm-mail";
40
+ import { getAgent } from "./streams/projections";
41
+ import { createEvent } from "./streams/events";
42
+ import { appendEvent } from "./streams/store";
40
43
  import {
41
44
  addStrike,
42
45
  clearStrikes,
@@ -836,7 +839,74 @@ export const swarm_progress = tool({
836
839
  importance: args.status === "blocked" ? "high" : "normal",
837
840
  });
838
841
 
839
- return `Progress reported: ${args.status}${args.progress_percent !== undefined ? ` (${args.progress_percent}%)` : ""}`;
842
+ // Auto-checkpoint at milestone progress (25%, 50%, 75%)
843
+ let checkpointCreated = false;
844
+ if (
845
+ args.progress_percent !== undefined &&
846
+ args.files_touched &&
847
+ args.files_touched.length > 0
848
+ ) {
849
+ const milestones = [25, 50, 75];
850
+ if (milestones.includes(args.progress_percent)) {
851
+ try {
852
+ // Create checkpoint event directly (non-fatal if it fails)
853
+ const checkpoint = {
854
+ epic_id: epicId,
855
+ bead_id: args.bead_id,
856
+ strategy: "file-based" as const,
857
+ files: args.files_touched,
858
+ dependencies: [] as string[],
859
+ directives: {},
860
+ recovery: {
861
+ last_checkpoint: Date.now(),
862
+ files_modified: args.files_touched,
863
+ progress_percent: args.progress_percent,
864
+ last_message: args.message,
865
+ },
866
+ };
867
+
868
+ const event = createEvent("swarm_checkpointed", {
869
+ project_key: args.project_key,
870
+ ...checkpoint,
871
+ });
872
+ await appendEvent(event, args.project_key);
873
+
874
+ // Update swarm_contexts table
875
+ const { getDatabase } = await import("./streams/index");
876
+ const db = await getDatabase(args.project_key);
877
+ const now = Date.now();
878
+ await db.query(
879
+ `INSERT INTO swarm_contexts (id, epic_id, bead_id, strategy, files, dependencies, directives, recovery, created_at, updated_at)
880
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
881
+ ON CONFLICT (id) DO UPDATE SET
882
+ files = EXCLUDED.files,
883
+ recovery = EXCLUDED.recovery,
884
+ updated_at = EXCLUDED.updated_at`,
885
+ [
886
+ args.bead_id,
887
+ epicId,
888
+ args.bead_id,
889
+ checkpoint.strategy,
890
+ JSON.stringify(checkpoint.files),
891
+ JSON.stringify(checkpoint.dependencies),
892
+ JSON.stringify(checkpoint.directives),
893
+ JSON.stringify(checkpoint.recovery),
894
+ now,
895
+ now,
896
+ ],
897
+ );
898
+ checkpointCreated = true;
899
+ } catch (error) {
900
+ // Non-fatal - log and continue
901
+ console.warn(
902
+ `[swarm_progress] Auto-checkpoint failed at ${args.progress_percent}%:`,
903
+ error,
904
+ );
905
+ }
906
+ }
907
+ }
908
+
909
+ return `Progress reported: ${args.status}${args.progress_percent !== undefined ? ` (${args.progress_percent}%)` : ""}${checkpointCreated ? " [checkpoint created]" : ""}`;
840
910
  },
841
911
  });
842
912
 
@@ -966,210 +1036,356 @@ export const swarm_complete = tool({
966
1036
  .describe(
967
1037
  "Skip ALL verification (UBS, typecheck, tests). Use sparingly! (default: false)",
968
1038
  ),
1039
+ planned_files: tool.schema
1040
+ .array(tool.schema.string())
1041
+ .optional()
1042
+ .describe("Files that were originally planned to be modified"),
1043
+ start_time: tool.schema
1044
+ .number()
1045
+ .optional()
1046
+ .describe("Task start timestamp (Unix ms) for duration calculation"),
1047
+ error_count: tool.schema
1048
+ .number()
1049
+ .optional()
1050
+ .describe("Number of errors encountered during task"),
1051
+ retry_count: tool.schema
1052
+ .number()
1053
+ .optional()
1054
+ .describe("Number of retry attempts during task"),
969
1055
  },
970
1056
  async execute(args) {
971
- // Run Verification Gate unless explicitly skipped
972
- let verificationResult: VerificationGateResult | null = null;
1057
+ // Extract epic ID early for error notifications
1058
+ const epicId = args.bead_id.includes(".")
1059
+ ? args.bead_id.split(".")[0]
1060
+ : args.bead_id;
973
1061
 
974
- if (!args.skip_verification && args.files_touched?.length) {
975
- verificationResult = await runVerificationGate(
976
- args.files_touched,
977
- args.skip_ubs_scan ?? false,
978
- );
1062
+ try {
1063
+ // Verify agent is registered in swarm-mail
1064
+ // This catches agents who skipped swarmmail_init
1065
+ const projectKey = args.project_key
1066
+ .replace(/\//g, "-")
1067
+ .replace(/\\/g, "-");
1068
+ let agentRegistered = false;
1069
+ let registrationWarning = "";
979
1070
 
980
- // Block completion if verification failed
981
- if (!verificationResult.passed) {
982
- return JSON.stringify(
983
- {
984
- success: false,
985
- error: "Verification Gate FAILED - fix issues before completing",
986
- verification: {
987
- passed: false,
988
- summary: verificationResult.summary,
989
- blockers: verificationResult.blockers,
990
- steps: verificationResult.steps.map((s) => ({
991
- name: s.name,
992
- passed: s.passed,
993
- skipped: s.skipped,
994
- skipReason: s.skipReason,
995
- error: s.error?.slice(0, 200),
996
- })),
997
- },
998
- hint:
999
- verificationResult.blockers.length > 0
1000
- ? `Fix these issues: ${verificationResult.blockers.map((b, i) => `${i + 1}. ${b}`).join(", ")}. Use skip_verification=true only as last resort.`
1001
- : "Fix the failing checks and try again. Use skip_verification=true only as last resort.",
1002
- gate_function:
1003
- "IDENTIFY → RUN → READ → VERIFY → CLAIM (you are at VERIFY, claim blocked)",
1004
- },
1005
- null,
1006
- 2,
1071
+ try {
1072
+ const agent = await getAgent(
1073
+ projectKey,
1074
+ args.agent_name,
1075
+ args.project_key,
1007
1076
  );
1077
+ agentRegistered = agent !== null;
1078
+
1079
+ if (!agentRegistered) {
1080
+ registrationWarning = `⚠️ WARNING: Agent '${args.agent_name}' was NOT registered in swarm-mail for project '${projectKey}'.
1081
+
1082
+ This usually means you skipped the MANDATORY swarmmail_init step.
1083
+
1084
+ **Impact:**
1085
+ - Your work was not tracked in the coordination system
1086
+ - File reservations may not have been managed
1087
+ - Other agents couldn't coordinate with you
1088
+ - Learning/eval data may be incomplete
1089
+
1090
+ **Next time:** Run swarmmail_init(project_path="${args.project_key}", task_description="<task>") FIRST, before any other work.
1091
+
1092
+ Continuing with completion, but this should be fixed for future subtasks.`;
1093
+
1094
+ console.warn(`[swarm_complete] ${registrationWarning}`);
1095
+ }
1096
+ } catch (error) {
1097
+ // Non-fatal - agent might be using legacy workflow
1098
+ console.warn(
1099
+ `[swarm_complete] Could not verify agent registration:`,
1100
+ error,
1101
+ );
1102
+ registrationWarning = `ℹ️ Could not verify swarm-mail registration (database may not be available). Consider running swarmmail_init next time.`;
1008
1103
  }
1009
- }
1010
1104
 
1011
- // Legacy UBS-only path for backward compatibility (when no files_touched)
1012
- let ubsResult: UbsScanResult | null = null;
1013
- if (
1014
- !args.skip_verification &&
1015
- !verificationResult &&
1016
- args.files_touched?.length &&
1017
- !args.skip_ubs_scan
1018
- ) {
1019
- ubsResult = await runUbsScan(args.files_touched);
1105
+ // Run Verification Gate unless explicitly skipped
1106
+ let verificationResult: VerificationGateResult | null = null;
1020
1107
 
1021
- // Block completion if critical bugs found
1022
- if (ubsResult && ubsResult.summary.critical > 0) {
1023
- return JSON.stringify(
1024
- {
1025
- success: false,
1026
- error: `UBS found ${ubsResult.summary.critical} critical bug(s) that must be fixed before completing`,
1027
- ubs_scan: {
1028
- critical_count: ubsResult.summary.critical,
1029
- bugs: ubsResult.bugs.filter((b) => b.severity === "critical"),
1030
- },
1031
- hint: `Fix these critical bugs: ${ubsResult.bugs
1032
- .filter((b) => b.severity === "critical")
1033
- .map((b) => `${b.file}:${b.line} - ${b.message}`)
1034
- .slice(0, 3)
1035
- .join(
1036
- "; ",
1037
- )}. Try: Run 'ubs scan ${args.files_touched?.join(" ") || "."} --json' for full report, fix reported issues, or use skip_ubs_scan=true to bypass (not recommended).`,
1038
- },
1039
- null,
1040
- 2,
1108
+ if (!args.skip_verification && args.files_touched?.length) {
1109
+ verificationResult = await runVerificationGate(
1110
+ args.files_touched,
1111
+ args.skip_ubs_scan ?? false,
1041
1112
  );
1113
+
1114
+ // Block completion if verification failed
1115
+ if (!verificationResult.passed) {
1116
+ return JSON.stringify(
1117
+ {
1118
+ success: false,
1119
+ error: "Verification Gate FAILED - fix issues before completing",
1120
+ verification: {
1121
+ passed: false,
1122
+ summary: verificationResult.summary,
1123
+ blockers: verificationResult.blockers,
1124
+ steps: verificationResult.steps.map((s) => ({
1125
+ name: s.name,
1126
+ passed: s.passed,
1127
+ skipped: s.skipped,
1128
+ skipReason: s.skipReason,
1129
+ error: s.error?.slice(0, 200),
1130
+ })),
1131
+ },
1132
+ hint:
1133
+ verificationResult.blockers.length > 0
1134
+ ? `Fix these issues: ${verificationResult.blockers.map((b, i) => `${i + 1}. ${b}`).join(", ")}. Use skip_verification=true only as last resort.`
1135
+ : "Fix the failing checks and try again. Use skip_verification=true only as last resort.",
1136
+ gate_function:
1137
+ "IDENTIFY → RUN → READ → VERIFY → CLAIM (you are at VERIFY, claim blocked)",
1138
+ },
1139
+ null,
1140
+ 2,
1141
+ );
1142
+ }
1042
1143
  }
1043
- }
1044
1144
 
1045
- // Parse and validate evaluation if provided
1046
- let parsedEvaluation: Evaluation | undefined;
1047
- if (args.evaluation) {
1048
- try {
1049
- parsedEvaluation = EvaluationSchema.parse(JSON.parse(args.evaluation));
1050
- } catch (error) {
1051
- return JSON.stringify(
1052
- {
1053
- success: false,
1054
- error: "Invalid evaluation format",
1055
- details: error instanceof z.ZodError ? error.issues : String(error),
1056
- },
1057
- null,
1058
- 2,
1145
+ // Legacy UBS-only path for backward compatibility (when no files_touched)
1146
+ let ubsResult: UbsScanResult | null = null;
1147
+ if (
1148
+ !args.skip_verification &&
1149
+ !verificationResult &&
1150
+ args.files_touched?.length &&
1151
+ !args.skip_ubs_scan
1152
+ ) {
1153
+ ubsResult = await runUbsScan(args.files_touched);
1154
+
1155
+ // Block completion if critical bugs found
1156
+ if (ubsResult && ubsResult.summary.critical > 0) {
1157
+ return JSON.stringify(
1158
+ {
1159
+ success: false,
1160
+ error: `UBS found ${ubsResult.summary.critical} critical bug(s) that must be fixed before completing`,
1161
+ ubs_scan: {
1162
+ critical_count: ubsResult.summary.critical,
1163
+ bugs: ubsResult.bugs.filter((b) => b.severity === "critical"),
1164
+ },
1165
+ hint: `Fix these critical bugs: ${ubsResult.bugs
1166
+ .filter((b) => b.severity === "critical")
1167
+ .map((b) => `${b.file}:${b.line} - ${b.message}`)
1168
+ .slice(0, 3)
1169
+ .join(
1170
+ "; ",
1171
+ )}. Try: Run 'ubs scan ${args.files_touched?.join(" ") || "."} --json' for full report, fix reported issues, or use skip_ubs_scan=true to bypass (not recommended).`,
1172
+ },
1173
+ null,
1174
+ 2,
1175
+ );
1176
+ }
1177
+ }
1178
+
1179
+ // Parse and validate evaluation if provided
1180
+ let parsedEvaluation: Evaluation | undefined;
1181
+ if (args.evaluation) {
1182
+ try {
1183
+ parsedEvaluation = EvaluationSchema.parse(
1184
+ JSON.parse(args.evaluation),
1185
+ );
1186
+ } catch (error) {
1187
+ return JSON.stringify(
1188
+ {
1189
+ success: false,
1190
+ error: "Invalid evaluation format",
1191
+ details:
1192
+ error instanceof z.ZodError ? error.issues : String(error),
1193
+ },
1194
+ null,
1195
+ 2,
1196
+ );
1197
+ }
1198
+
1199
+ // If evaluation failed, don't complete
1200
+ if (!parsedEvaluation.passed) {
1201
+ return JSON.stringify(
1202
+ {
1203
+ success: false,
1204
+ error: "Self-evaluation failed",
1205
+ retry_suggestion: parsedEvaluation.retry_suggestion,
1206
+ feedback: parsedEvaluation.overall_feedback,
1207
+ },
1208
+ null,
1209
+ 2,
1210
+ );
1211
+ }
1212
+ }
1213
+
1214
+ // Close the bead
1215
+ const closeResult =
1216
+ await Bun.$`bd close ${args.bead_id} --reason ${args.summary} --json`
1217
+ .quiet()
1218
+ .nothrow();
1219
+
1220
+ if (closeResult.exitCode !== 0) {
1221
+ throw new Error(
1222
+ `Failed to close bead because bd close command failed: ${closeResult.stderr.toString()}. Try: Verify bead exists and is not already closed with 'bd show ${args.bead_id}', check if bead ID is correct with 'beads_query()', or use beads_close tool directly.`,
1059
1223
  );
1060
1224
  }
1061
1225
 
1062
- // If evaluation failed, don't complete
1063
- if (!parsedEvaluation.passed) {
1064
- return JSON.stringify(
1065
- {
1066
- success: false,
1067
- error: "Self-evaluation failed",
1068
- retry_suggestion: parsedEvaluation.retry_suggestion,
1069
- feedback: parsedEvaluation.overall_feedback,
1070
- },
1071
- null,
1072
- 2,
1226
+ // Emit SubtaskOutcomeEvent for learning system
1227
+ try {
1228
+ const epicId = args.bead_id.includes(".")
1229
+ ? args.bead_id.split(".")[0]
1230
+ : args.bead_id;
1231
+
1232
+ const durationMs = args.start_time ? Date.now() - args.start_time : 0;
1233
+
1234
+ const event = createEvent("subtask_outcome", {
1235
+ project_key: args.project_key,
1236
+ epic_id: epicId,
1237
+ bead_id: args.bead_id,
1238
+ planned_files: args.planned_files || [],
1239
+ actual_files: args.files_touched || [],
1240
+ duration_ms: durationMs,
1241
+ error_count: args.error_count || 0,
1242
+ retry_count: args.retry_count || 0,
1243
+ success: true,
1244
+ });
1245
+ await appendEvent(event, args.project_key);
1246
+ } catch (error) {
1247
+ // Non-fatal - log and continue
1248
+ console.warn(
1249
+ "[swarm_complete] Failed to emit SubtaskOutcomeEvent:",
1250
+ error,
1073
1251
  );
1074
1252
  }
1075
- }
1076
1253
 
1077
- // Close the bead
1078
- const closeResult =
1079
- await Bun.$`bd close ${args.bead_id} --reason ${args.summary} --json`
1080
- .quiet()
1081
- .nothrow();
1254
+ // Automatic memory capture (MANDATORY on successful completion)
1255
+ // Extract strategy from bead metadata if available
1256
+ let capturedStrategy: LearningDecompositionStrategy | undefined;
1257
+ const durationMs = args.start_time ? Date.now() - args.start_time : 0;
1082
1258
 
1083
- if (closeResult.exitCode !== 0) {
1084
- throw new Error(
1085
- `Failed to close bead because bd close command failed: ${closeResult.stderr.toString()}. Try: Verify bead exists and is not already closed with 'bd show ${args.bead_id}', check if bead ID is correct with 'beads_query()', or use beads_close tool directly.`,
1259
+ // Build memory information from task completion
1260
+ const memoryInfo = formatMemoryStoreOnSuccess(
1261
+ args.bead_id,
1262
+ args.summary,
1263
+ args.files_touched || [],
1264
+ capturedStrategy,
1086
1265
  );
1087
- }
1088
1266
 
1089
- // Release file reservations for this agent using embedded swarm-mail
1090
- try {
1091
- await releaseSwarmFiles({
1092
- projectPath: args.project_key,
1093
- agentName: args.agent_name,
1094
- // Release all reservations for this agent
1095
- });
1096
- } catch (error) {
1097
- // Release might fail (e.g., no reservations existed)
1098
- // This is non-fatal - log and continue
1099
- console.warn(
1100
- `[swarm] Failed to release file reservations for ${args.agent_name}:`,
1101
- error,
1102
- );
1103
- }
1267
+ let memoryStored = false;
1268
+ let memoryError: string | undefined;
1104
1269
 
1105
- // Extract epic ID
1106
- const epicId = args.bead_id.includes(".")
1107
- ? args.bead_id.split(".")[0]
1108
- : args.bead_id;
1270
+ // Attempt to store in semantic-memory (non-blocking)
1271
+ try {
1272
+ const memoryAvailable = await isToolAvailable("semantic-memory");
1273
+ if (memoryAvailable) {
1274
+ // Call semantic-memory store command
1275
+ const storeResult =
1276
+ await Bun.$`semantic-memory store ${memoryInfo.information} --metadata ${memoryInfo.metadata}`
1277
+ .quiet()
1278
+ .nothrow();
1279
+
1280
+ if (storeResult.exitCode === 0) {
1281
+ memoryStored = true;
1282
+ console.log(
1283
+ `[swarm_complete] Stored learning for ${args.bead_id} in semantic-memory`,
1284
+ );
1285
+ } else {
1286
+ memoryError = `semantic-memory store failed: ${storeResult.stderr.toString().slice(0, 200)}`;
1287
+ console.warn(`[swarm_complete] ${memoryError}`);
1288
+ }
1289
+ } else {
1290
+ memoryError =
1291
+ "semantic-memory not available - learning stored in-memory only";
1292
+ warnMissingTool("semantic-memory");
1293
+ }
1294
+ } catch (error) {
1295
+ memoryError = `Failed to store memory: ${error instanceof Error ? error.message : String(error)}`;
1296
+ console.warn(`[swarm_complete] ${memoryError}`);
1297
+ }
1109
1298
 
1110
- // Send completion message using embedded swarm-mail
1111
- const completionBody = [
1112
- `## Subtask Complete: ${args.bead_id}`,
1113
- "",
1114
- `**Summary**: ${args.summary}`,
1115
- "",
1116
- parsedEvaluation
1117
- ? `**Self-Evaluation**: ${parsedEvaluation.passed ? "PASSED" : "FAILED"}`
1118
- : "",
1119
- parsedEvaluation?.overall_feedback
1120
- ? `**Feedback**: ${parsedEvaluation.overall_feedback}`
1121
- : "",
1122
- ]
1123
- .filter(Boolean)
1124
- .join("\n");
1299
+ // Release file reservations for this agent using embedded swarm-mail
1300
+ try {
1301
+ await releaseSwarmFiles({
1302
+ projectPath: args.project_key,
1303
+ agentName: args.agent_name,
1304
+ // Release all reservations for this agent
1305
+ });
1306
+ } catch (error) {
1307
+ // Release might fail (e.g., no reservations existed)
1308
+ // This is non-fatal - log and continue
1309
+ console.warn(
1310
+ `[swarm] Failed to release file reservations for ${args.agent_name}:`,
1311
+ error,
1312
+ );
1313
+ }
1125
1314
 
1126
- await sendSwarmMessage({
1127
- projectPath: args.project_key,
1128
- fromAgent: args.agent_name,
1129
- toAgents: [], // Thread broadcast
1130
- subject: `Complete: ${args.bead_id}`,
1131
- body: completionBody,
1132
- threadId: epicId,
1133
- importance: "normal",
1134
- });
1315
+ // Extract epic ID
1316
+ const epicId = args.bead_id.includes(".")
1317
+ ? args.bead_id.split(".")[0]
1318
+ : args.bead_id;
1319
+
1320
+ // Send completion message using embedded swarm-mail with memory capture status
1321
+ const completionBody = [
1322
+ `## Subtask Complete: ${args.bead_id}`,
1323
+ "",
1324
+ `**Summary**: ${args.summary}`,
1325
+ "",
1326
+ parsedEvaluation
1327
+ ? `**Self-Evaluation**: ${parsedEvaluation.passed ? "PASSED" : "FAILED"}`
1328
+ : "",
1329
+ parsedEvaluation?.overall_feedback
1330
+ ? `**Feedback**: ${parsedEvaluation.overall_feedback}`
1331
+ : "",
1332
+ "",
1333
+ `**Memory Capture**: ${memoryStored ? "✓ Stored in semantic-memory" : `✗ ${memoryError || "Failed"}`}`,
1334
+ ]
1335
+ .filter(Boolean)
1336
+ .join("\n");
1337
+
1338
+ await sendSwarmMessage({
1339
+ projectPath: args.project_key,
1340
+ fromAgent: args.agent_name,
1341
+ toAgents: [], // Thread broadcast
1342
+ subject: `Complete: ${args.bead_id}`,
1343
+ body: completionBody,
1344
+ threadId: epicId,
1345
+ importance: "normal",
1346
+ });
1135
1347
 
1136
- // Build success response with semantic-memory integration
1137
- const response = {
1138
- success: true,
1139
- bead_id: args.bead_id,
1140
- closed: true,
1141
- reservations_released: true,
1142
- message_sent: true,
1143
- verification_gate: verificationResult
1144
- ? {
1145
- passed: true,
1146
- summary: verificationResult.summary,
1147
- steps: verificationResult.steps.map((s) => ({
1148
- name: s.name,
1149
- passed: s.passed,
1150
- skipped: s.skipped,
1151
- skipReason: s.skipReason,
1152
- })),
1153
- }
1154
- : args.skip_verification
1155
- ? { skipped: true, reason: "skip_verification=true" }
1156
- : { skipped: true, reason: "no files_touched provided" },
1157
- ubs_scan: ubsResult
1158
- ? {
1159
- ran: true,
1160
- bugs_found: ubsResult.summary.total,
1161
- summary: ubsResult.summary,
1162
- warnings: ubsResult.bugs.filter((b) => b.severity !== "critical"),
1163
- }
1164
- : verificationResult
1165
- ? { ran: true, included_in_verification_gate: true }
1166
- : {
1167
- ran: false,
1168
- reason: args.skip_ubs_scan
1169
- ? "skipped"
1170
- : "no files or ubs unavailable",
1171
- },
1172
- learning_prompt: `## Reflection
1348
+ // Build success response with semantic-memory integration
1349
+ const response = {
1350
+ success: true,
1351
+ bead_id: args.bead_id,
1352
+ closed: true,
1353
+ reservations_released: true,
1354
+ message_sent: true,
1355
+ agent_registration: {
1356
+ verified: agentRegistered,
1357
+ warning: registrationWarning || undefined,
1358
+ },
1359
+ verification_gate: verificationResult
1360
+ ? {
1361
+ passed: true,
1362
+ summary: verificationResult.summary,
1363
+ steps: verificationResult.steps.map((s) => ({
1364
+ name: s.name,
1365
+ passed: s.passed,
1366
+ skipped: s.skipped,
1367
+ skipReason: s.skipReason,
1368
+ })),
1369
+ }
1370
+ : args.skip_verification
1371
+ ? { skipped: true, reason: "skip_verification=true" }
1372
+ : { skipped: true, reason: "no files_touched provided" },
1373
+ ubs_scan: ubsResult
1374
+ ? {
1375
+ ran: true,
1376
+ bugs_found: ubsResult.summary.total,
1377
+ summary: ubsResult.summary,
1378
+ warnings: ubsResult.bugs.filter((b) => b.severity !== "critical"),
1379
+ }
1380
+ : verificationResult
1381
+ ? { ran: true, included_in_verification_gate: true }
1382
+ : {
1383
+ ran: false,
1384
+ reason: args.skip_ubs_scan
1385
+ ? "skipped"
1386
+ : "no files or ubs unavailable",
1387
+ },
1388
+ learning_prompt: `## Reflection
1173
1389
 
1174
1390
  Did you learn anything reusable during this subtask? Consider:
1175
1391
 
@@ -1181,15 +1397,110 @@ Did you learn anything reusable during this subtask? Consider:
1181
1397
  If you discovered something valuable, use \`swarm_learn\` or \`skills_create\` to preserve it as a skill for future swarms.
1182
1398
 
1183
1399
  Files touched: ${args.files_touched?.join(", ") || "none recorded"}`,
1184
- // Add semantic-memory integration on success
1185
- memory_store: formatMemoryStoreOnSuccess(
1186
- args.bead_id,
1187
- args.summary,
1188
- args.files_touched || [],
1189
- ),
1190
- };
1400
+ // Automatic memory capture (MANDATORY)
1401
+ memory_capture: {
1402
+ attempted: true,
1403
+ stored: memoryStored,
1404
+ error: memoryError,
1405
+ information: memoryInfo.information,
1406
+ metadata: memoryInfo.metadata,
1407
+ note: memoryStored
1408
+ ? "Learning automatically stored in semantic-memory"
1409
+ : `Failed to store: ${memoryError}. Learning lost unless semantic-memory is available.`,
1410
+ },
1411
+ };
1191
1412
 
1192
- return JSON.stringify(response, null, 2);
1413
+ return JSON.stringify(response, null, 2);
1414
+ } catch (error) {
1415
+ // CRITICAL: Notify coordinator of failure via swarm mail
1416
+ const errorMessage =
1417
+ error instanceof Error ? error.message : String(error);
1418
+ const errorStack = error instanceof Error ? error.stack : undefined;
1419
+
1420
+ // Determine which step failed
1421
+ let failedStep = "unknown";
1422
+ if (errorMessage.includes("verification")) {
1423
+ failedStep = "Verification Gate (UBS/typecheck/tests)";
1424
+ } else if (errorMessage.includes("UBS") || errorMessage.includes("ubs")) {
1425
+ failedStep = "UBS scan";
1426
+ } else if (errorMessage.includes("evaluation")) {
1427
+ failedStep = "Self-evaluation parsing";
1428
+ } else if (
1429
+ errorMessage.includes("bead") ||
1430
+ errorMessage.includes("close")
1431
+ ) {
1432
+ failedStep = "Bead close";
1433
+ } else if (
1434
+ errorMessage.includes("memory") ||
1435
+ errorMessage.includes("semantic")
1436
+ ) {
1437
+ failedStep = "Memory storage (non-fatal)";
1438
+ } else if (
1439
+ errorMessage.includes("reservation") ||
1440
+ errorMessage.includes("release")
1441
+ ) {
1442
+ failedStep = "File reservation release";
1443
+ } else if (
1444
+ errorMessage.includes("message") ||
1445
+ errorMessage.includes("mail")
1446
+ ) {
1447
+ failedStep = "Swarm mail notification";
1448
+ }
1449
+
1450
+ // Build error notification body
1451
+ const errorBody = [
1452
+ `## ⚠️ SWARM_COMPLETE FAILED`,
1453
+ "",
1454
+ `**Bead**: ${args.bead_id}`,
1455
+ `**Agent**: ${args.agent_name}`,
1456
+ `**Failed Step**: ${failedStep}`,
1457
+ "",
1458
+ `### Error Message`,
1459
+ "```",
1460
+ errorMessage,
1461
+ "```",
1462
+ "",
1463
+ errorStack
1464
+ ? `### Stack Trace\n\`\`\`\n${errorStack.slice(0, 1000)}\n\`\`\`\n`
1465
+ : "",
1466
+ `### Context`,
1467
+ `- **Summary**: ${args.summary}`,
1468
+ `- **Files touched**: ${args.files_touched?.length ? args.files_touched.join(", ") : "none"}`,
1469
+ `- **Skip UBS**: ${args.skip_ubs_scan ?? false}`,
1470
+ `- **Skip verification**: ${args.skip_verification ?? false}`,
1471
+ "",
1472
+ `### Recovery Actions`,
1473
+ "1. Check error message for specific issue",
1474
+ "2. Review failed step (UBS scan, typecheck, bead close, etc.)",
1475
+ "3. Fix underlying issue or use skip flags if appropriate",
1476
+ "4. Retry swarm_complete after fixing",
1477
+ ]
1478
+ .filter(Boolean)
1479
+ .join("\n");
1480
+
1481
+ // Send urgent notification to coordinator
1482
+ try {
1483
+ await sendSwarmMessage({
1484
+ projectPath: args.project_key,
1485
+ fromAgent: args.agent_name,
1486
+ toAgents: [], // Thread broadcast to coordinator
1487
+ subject: `FAILED: swarm_complete for ${args.bead_id}`,
1488
+ body: errorBody,
1489
+ threadId: epicId,
1490
+ importance: "urgent",
1491
+ });
1492
+ } catch (mailError) {
1493
+ // Even swarm mail failed - log to console as last resort
1494
+ console.error(
1495
+ `[swarm_complete] CRITICAL: Failed to notify coordinator of failure for ${args.bead_id}:`,
1496
+ mailError,
1497
+ );
1498
+ console.error(`[swarm_complete] Original error:`, error);
1499
+ }
1500
+
1501
+ // Re-throw the original error after notifying
1502
+ throw error;
1503
+ }
1193
1504
  },
1194
1505
  });
1195
1506
 
@@ -1647,6 +1958,275 @@ export const swarm_check_strikes = tool({
1647
1958
  },
1648
1959
  });
1649
1960
 
1961
+ /**
1962
+ * Swarm context shape stored in swarm_contexts table
1963
+ */
1964
+ interface SwarmBeadContext {
1965
+ id: string;
1966
+ epic_id: string;
1967
+ bead_id: string;
1968
+ strategy: "file-based" | "feature-based" | "risk-based";
1969
+ files: string[];
1970
+ dependencies: string[];
1971
+ directives: {
1972
+ shared_context?: string;
1973
+ skills_to_load?: string[];
1974
+ coordinator_notes?: string;
1975
+ };
1976
+ recovery: {
1977
+ last_checkpoint: number;
1978
+ files_modified: string[];
1979
+ progress_percent: number;
1980
+ last_message?: string;
1981
+ error_context?: string;
1982
+ };
1983
+ created_at: number;
1984
+ updated_at: number;
1985
+ }
1986
+
1987
+ /**
1988
+ * Checkpoint swarm context for recovery
1989
+ *
1990
+ * Records the current state of a subtask to enable recovery after crashes,
1991
+ * context overflows, or agent restarts. Non-fatal errors - logs warnings
1992
+ * and continues if checkpoint fails.
1993
+ *
1994
+ * Integration:
1995
+ * - Called automatically by swarm_progress at milestone thresholds (25%, 50%, 75%)
1996
+ * - Can be called manually by agents at critical points
1997
+ * - Emits SwarmCheckpointedEvent for audit trail
1998
+ * - Updates swarm_contexts table for fast recovery queries
1999
+ */
2000
+ export const swarm_checkpoint = tool({
2001
+ description:
2002
+ "Checkpoint swarm context for recovery. Records current state for crash recovery. Non-fatal errors.",
2003
+ args: {
2004
+ project_key: tool.schema.string().describe("Project path"),
2005
+ agent_name: tool.schema.string().describe("Agent name"),
2006
+ bead_id: tool.schema.string().describe("Subtask bead ID"),
2007
+ epic_id: tool.schema.string().describe("Epic bead ID"),
2008
+ files_modified: tool.schema
2009
+ .array(tool.schema.string())
2010
+ .describe("Files modified so far"),
2011
+ progress_percent: tool.schema
2012
+ .number()
2013
+ .min(0)
2014
+ .max(100)
2015
+ .describe("Current progress"),
2016
+ directives: tool.schema
2017
+ .object({
2018
+ shared_context: tool.schema.string().optional(),
2019
+ skills_to_load: tool.schema.array(tool.schema.string()).optional(),
2020
+ coordinator_notes: tool.schema.string().optional(),
2021
+ })
2022
+ .optional()
2023
+ .describe("Coordinator directives for this subtask"),
2024
+ error_context: tool.schema
2025
+ .string()
2026
+ .optional()
2027
+ .describe("Error context if checkpoint is during error handling"),
2028
+ },
2029
+ async execute(args) {
2030
+ try {
2031
+ // Build checkpoint data
2032
+ const checkpoint: Omit<
2033
+ SwarmBeadContext,
2034
+ "id" | "created_at" | "updated_at"
2035
+ > = {
2036
+ epic_id: args.epic_id,
2037
+ bead_id: args.bead_id,
2038
+ strategy: "file-based", // TODO: Extract from decomposition metadata
2039
+ files: args.files_modified,
2040
+ dependencies: [], // TODO: Extract from bead metadata
2041
+ directives: args.directives || {},
2042
+ recovery: {
2043
+ last_checkpoint: Date.now(),
2044
+ files_modified: args.files_modified,
2045
+ progress_percent: args.progress_percent,
2046
+ error_context: args.error_context,
2047
+ },
2048
+ };
2049
+
2050
+ // Emit checkpoint event
2051
+ const event = createEvent("swarm_checkpointed", {
2052
+ project_key: args.project_key,
2053
+ epic_id: args.epic_id,
2054
+ bead_id: args.bead_id,
2055
+ strategy: checkpoint.strategy,
2056
+ files: checkpoint.files,
2057
+ dependencies: checkpoint.dependencies,
2058
+ directives: checkpoint.directives,
2059
+ recovery: checkpoint.recovery,
2060
+ });
2061
+
2062
+ await appendEvent(event, args.project_key);
2063
+
2064
+ // Update swarm_contexts table for fast recovery
2065
+ const { getDatabase } = await import("./streams/index");
2066
+ const db = await getDatabase(args.project_key);
2067
+
2068
+ const now = Date.now();
2069
+ await db.query(
2070
+ `INSERT INTO swarm_contexts (id, epic_id, bead_id, strategy, files, dependencies, directives, recovery, created_at, updated_at)
2071
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
2072
+ ON CONFLICT (id) DO UPDATE SET
2073
+ files = EXCLUDED.files,
2074
+ recovery = EXCLUDED.recovery,
2075
+ updated_at = EXCLUDED.updated_at`,
2076
+ [
2077
+ args.bead_id, // Use bead_id as unique ID
2078
+ args.epic_id,
2079
+ args.bead_id,
2080
+ checkpoint.strategy,
2081
+ JSON.stringify(checkpoint.files),
2082
+ JSON.stringify(checkpoint.dependencies),
2083
+ JSON.stringify(checkpoint.directives),
2084
+ JSON.stringify(checkpoint.recovery),
2085
+ now,
2086
+ now,
2087
+ ],
2088
+ );
2089
+
2090
+ return JSON.stringify(
2091
+ {
2092
+ success: true,
2093
+ checkpoint_timestamp: now,
2094
+ summary: `Checkpoint saved for ${args.bead_id} at ${args.progress_percent}%`,
2095
+ bead_id: args.bead_id,
2096
+ epic_id: args.epic_id,
2097
+ files_tracked: args.files_modified.length,
2098
+ },
2099
+ null,
2100
+ 2,
2101
+ );
2102
+ } catch (error) {
2103
+ // Non-fatal - log warning and continue
2104
+ console.warn(
2105
+ `[swarm_checkpoint] Failed to checkpoint ${args.bead_id}:`,
2106
+ error,
2107
+ );
2108
+ return JSON.stringify(
2109
+ {
2110
+ success: false,
2111
+ warning: "Checkpoint failed but continuing",
2112
+ error: error instanceof Error ? error.message : String(error),
2113
+ bead_id: args.bead_id,
2114
+ note: "This is non-fatal. Work can continue without checkpoint.",
2115
+ },
2116
+ null,
2117
+ 2,
2118
+ );
2119
+ }
2120
+ },
2121
+ });
2122
+
2123
+ /**
2124
+ * Recover swarm context from last checkpoint
2125
+ *
2126
+ * Queries swarm_contexts table for the most recent checkpoint of an epic.
2127
+ * Returns the full context including files, progress, and recovery state.
2128
+ * Emits SwarmRecoveredEvent for audit trail.
2129
+ *
2130
+ * Graceful fallback: Returns { found: false } if no checkpoint exists.
2131
+ */
2132
+ export const swarm_recover = tool({
2133
+ description:
2134
+ "Recover swarm context from last checkpoint. Returns context or null if not found.",
2135
+ args: {
2136
+ project_key: tool.schema.string().describe("Project path"),
2137
+ epic_id: tool.schema.string().describe("Epic bead ID to recover"),
2138
+ },
2139
+ async execute(args) {
2140
+ try {
2141
+ const { getDatabase } = await import("./streams/index");
2142
+ const db = await getDatabase(args.project_key);
2143
+
2144
+ // Query most recent checkpoint for this epic
2145
+ const result = await db.query<{
2146
+ id: string;
2147
+ epic_id: string;
2148
+ bead_id: string;
2149
+ strategy: string;
2150
+ files: string;
2151
+ dependencies: string;
2152
+ directives: string;
2153
+ recovery: string;
2154
+ created_at: number;
2155
+ updated_at: number;
2156
+ }>(
2157
+ `SELECT * FROM swarm_contexts
2158
+ WHERE epic_id = $1
2159
+ ORDER BY updated_at DESC
2160
+ LIMIT 1`,
2161
+ [args.epic_id],
2162
+ );
2163
+
2164
+ if (result.rows.length === 0) {
2165
+ return JSON.stringify(
2166
+ {
2167
+ found: false,
2168
+ message: `No checkpoint found for epic ${args.epic_id}`,
2169
+ epic_id: args.epic_id,
2170
+ },
2171
+ null,
2172
+ 2,
2173
+ );
2174
+ }
2175
+
2176
+ const row = result.rows[0];
2177
+ const context: SwarmBeadContext = {
2178
+ id: row.id,
2179
+ epic_id: row.epic_id,
2180
+ bead_id: row.bead_id,
2181
+ strategy: row.strategy as SwarmBeadContext["strategy"],
2182
+ files: JSON.parse(row.files),
2183
+ dependencies: JSON.parse(row.dependencies),
2184
+ directives: JSON.parse(row.directives),
2185
+ recovery: JSON.parse(row.recovery),
2186
+ created_at: row.created_at,
2187
+ updated_at: row.updated_at,
2188
+ };
2189
+
2190
+ // Emit recovery event
2191
+ const event = createEvent("swarm_recovered", {
2192
+ project_key: args.project_key,
2193
+ epic_id: args.epic_id,
2194
+ bead_id: context.bead_id,
2195
+ recovered_from_checkpoint: context.recovery.last_checkpoint,
2196
+ });
2197
+
2198
+ await appendEvent(event, args.project_key);
2199
+
2200
+ return JSON.stringify(
2201
+ {
2202
+ found: true,
2203
+ context,
2204
+ summary: `Recovered checkpoint from ${new Date(context.updated_at).toISOString()}`,
2205
+ age_seconds: Math.round((Date.now() - context.updated_at) / 1000),
2206
+ },
2207
+ null,
2208
+ 2,
2209
+ );
2210
+ } catch (error) {
2211
+ // Graceful fallback
2212
+ console.warn(
2213
+ `[swarm_recover] Failed to recover context for ${args.epic_id}:`,
2214
+ error,
2215
+ );
2216
+ return JSON.stringify(
2217
+ {
2218
+ found: false,
2219
+ error: error instanceof Error ? error.message : String(error),
2220
+ message: `Recovery failed for epic ${args.epic_id}`,
2221
+ epic_id: args.epic_id,
2222
+ },
2223
+ null,
2224
+ 2,
2225
+ );
2226
+ }
2227
+ },
2228
+ });
2229
+
1650
2230
  /**
1651
2231
  * Learn from completed work and optionally create a skill
1652
2232
  *
@@ -1865,5 +2445,7 @@ export const orchestrateTools = {
1865
2445
  swarm_get_error_context,
1866
2446
  swarm_resolve_error,
1867
2447
  swarm_check_strikes,
2448
+ swarm_checkpoint,
2449
+ swarm_recover,
1868
2450
  swarm_learn,
1869
2451
  };