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.
- package/.beads/issues.jsonl +213 -0
- package/INTEGRATION_EXAMPLE.md +66 -0
- package/README.md +352 -522
- package/dist/index.js +2046 -984
- package/dist/plugin.js +2051 -1017
- package/docs/analysis/subagent-coordination-patterns.md +2 -0
- package/docs/semantic-memory-cli-syntax.md +123 -0
- package/docs/swarm-mail-architecture.md +1147 -0
- package/evals/README.md +116 -0
- package/evals/evalite.config.ts +15 -0
- package/evals/example.eval.ts +32 -0
- package/evals/fixtures/decomposition-cases.ts +105 -0
- package/evals/lib/data-loader.test.ts +288 -0
- package/evals/lib/data-loader.ts +111 -0
- package/evals/lib/llm.ts +115 -0
- package/evals/scorers/index.ts +200 -0
- package/evals/scorers/outcome-scorers.test.ts +27 -0
- package/evals/scorers/outcome-scorers.ts +349 -0
- package/evals/swarm-decomposition.eval.ts +112 -0
- package/package.json +8 -1
- package/scripts/cleanup-test-memories.ts +346 -0
- package/src/beads.ts +49 -0
- package/src/eval-capture.ts +487 -0
- package/src/index.ts +45 -3
- package/src/learning.integration.test.ts +19 -4
- package/src/output-guardrails.test.ts +438 -0
- package/src/output-guardrails.ts +381 -0
- package/src/schemas/index.ts +18 -0
- package/src/schemas/swarm-context.ts +115 -0
- package/src/storage.ts +117 -5
- package/src/streams/events.test.ts +296 -0
- package/src/streams/events.ts +93 -0
- package/src/streams/migrations.test.ts +24 -20
- package/src/streams/migrations.ts +51 -0
- package/src/streams/projections.ts +187 -0
- package/src/streams/store.ts +275 -0
- package/src/swarm-orchestrate.ts +771 -189
- package/src/swarm-prompts.ts +84 -12
- package/src/swarm.integration.test.ts +124 -0
- package/vitest.integration.config.ts +6 -0
- package/vitest.integration.setup.ts +48 -0
package/src/swarm-orchestrate.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
//
|
|
972
|
-
|
|
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
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
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
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
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
|
-
|
|
1012
|
-
|
|
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
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
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
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
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
|
-
//
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
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
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
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
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
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
|
-
|
|
1090
|
-
|
|
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
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
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
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
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
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
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
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
passed:
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
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
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
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
|
-
|
|
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
|
};
|