opencode-swarm-plugin 0.23.5 → 0.24.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.
@@ -905,8 +905,345 @@ export class DecompositionError extends SwarmError {
905
905
  }
906
906
  }
907
907
 
908
+ /**
909
+ * Planning phase state machine for Socratic planning
910
+ */
911
+ type PlanningPhase = "questioning" | "alternatives" | "recommendation" | "ready";
912
+
913
+ /**
914
+ * Planning mode that determines interaction level
915
+ */
916
+ type PlanningMode = "socratic" | "fast" | "auto" | "confirm-only";
917
+
918
+ /**
919
+ * Socratic planning output structure
920
+ */
921
+ interface SocraticPlanOutput {
922
+ mode: PlanningMode;
923
+ phase: PlanningPhase;
924
+ questions?: Array<{ question: string; options?: string[] }>;
925
+ alternatives?: Array<{
926
+ name: string;
927
+ description: string;
928
+ tradeoffs: string;
929
+ }>;
930
+ recommendation?: { approach: string; reasoning: string };
931
+ memory_context?: string;
932
+ codebase_context?: {
933
+ git_status?: string;
934
+ relevant_files?: string[];
935
+ };
936
+ ready_to_decompose: boolean;
937
+ next_action?: string;
938
+ }
939
+
940
+ /**
941
+ * Interactive planning tool with Socratic questioning
942
+ *
943
+ * Implements a planning phase BEFORE decomposition that:
944
+ * 1. Gathers context (git, files, semantic memory)
945
+ * 2. Asks clarifying questions (socratic mode)
946
+ * 3. Explores alternatives with tradeoffs
947
+ * 4. Recommends an approach with reasoning
948
+ * 5. Confirms before proceeding to decomposition
949
+ *
950
+ * Modes:
951
+ * - socratic: Full interactive planning with questions, alternatives, recommendations
952
+ * - fast: Skip brainstorming, go straight to decomposition with memory context
953
+ * - auto: Auto-select best approach based on task keywords, minimal interaction
954
+ * - confirm-only: Show decomposition, wait for yes/no confirmation
955
+ *
956
+ * Based on the Socratic Planner Pattern from obra/superpowers.
957
+ *
958
+ * @see docs/analysis-socratic-planner-pattern.md
959
+ */
960
+ export const swarm_plan_interactive = tool({
961
+ description:
962
+ "Interactive planning phase with Socratic questioning before decomposition. Supports multiple modes from full interactive to auto-proceed.",
963
+ args: {
964
+ task: tool.schema.string().min(1).describe("The task to plan"),
965
+ mode: tool.schema
966
+ .enum(["socratic", "fast", "auto", "confirm-only"])
967
+ .default("socratic")
968
+ .describe("Planning mode: socratic (full), fast (skip questions), auto (minimal), confirm-only (single yes/no)"),
969
+ context: tool.schema
970
+ .string()
971
+ .optional()
972
+ .describe("Optional additional context about the task"),
973
+ user_response: tool.schema
974
+ .string()
975
+ .optional()
976
+ .describe("User's response to a previous question (for multi-turn socratic mode)"),
977
+ phase: tool.schema
978
+ .enum(["questioning", "alternatives", "recommendation", "ready"])
979
+ .optional()
980
+ .describe("Current planning phase (for resuming multi-turn interaction)"),
981
+ },
982
+ async execute(args): Promise<string> {
983
+ // Import needed modules
984
+ const { selectStrategy, formatStrategyGuidelines, STRATEGIES } =
985
+ await import("./swarm-strategies");
986
+ const { formatMemoryQueryForDecomposition } = await import("./learning");
987
+
988
+ // Determine current phase
989
+ const currentPhase: PlanningPhase = args.phase || "questioning";
990
+ const mode: PlanningMode = args.mode || "socratic";
991
+
992
+ // Gather context - always do this regardless of mode
993
+ let memoryContext = "";
994
+ let codebaseContext: { git_status?: string; relevant_files?: string[] } = {};
995
+
996
+ // Generate semantic memory query instruction
997
+ // Note: Semantic memory is accessed via OpenCode's global tools, not as a direct import
998
+ // The coordinator should call semantic-memory_find before calling this tool
999
+ // and pass results in the context parameter
1000
+ try {
1001
+ const memoryQuery = formatMemoryQueryForDecomposition(args.task, 3);
1002
+ memoryContext = `[Memory Query Instruction]\n${memoryQuery.instruction}\nQuery: "${memoryQuery.query}"\nLimit: ${memoryQuery.limit}`;
1003
+ } catch (error) {
1004
+ console.warn("[swarm_plan_interactive] Memory query formatting failed:", error);
1005
+ }
1006
+
1007
+ // Get git context for codebase awareness
1008
+ try {
1009
+ const gitResult = await Bun.$`git status --short`.quiet().nothrow();
1010
+ if (gitResult.exitCode === 0) {
1011
+ codebaseContext.git_status = gitResult.stdout.toString().trim();
1012
+ }
1013
+ } catch (error) {
1014
+ // Git not available or not in a git repo - continue without it
1015
+ }
1016
+
1017
+ // Fast mode: Skip to recommendation
1018
+ if (mode === "fast") {
1019
+ const strategyResult = selectStrategy(args.task);
1020
+ const guidelines = formatStrategyGuidelines(strategyResult.strategy);
1021
+
1022
+ const output: SocraticPlanOutput = {
1023
+ mode: "fast",
1024
+ phase: "ready",
1025
+ recommendation: {
1026
+ approach: strategyResult.strategy,
1027
+ reasoning: `${strategyResult.reasoning}\n\n${guidelines}`,
1028
+ },
1029
+ memory_context: memoryContext || undefined,
1030
+ codebase_context: Object.keys(codebaseContext).length > 0 ? codebaseContext : undefined,
1031
+ ready_to_decompose: true,
1032
+ next_action: "Proceed to swarm_decompose or swarm_delegate_planning",
1033
+ };
1034
+
1035
+ return JSON.stringify(output, null, 2);
1036
+ }
1037
+
1038
+ // Auto mode: Auto-select and proceed
1039
+ if (mode === "auto") {
1040
+ const strategyResult = selectStrategy(args.task);
1041
+
1042
+ const output: SocraticPlanOutput = {
1043
+ mode: "auto",
1044
+ phase: "ready",
1045
+ recommendation: {
1046
+ approach: strategyResult.strategy,
1047
+ reasoning: `Auto-selected based on task keywords: ${strategyResult.reasoning}`,
1048
+ },
1049
+ memory_context: memoryContext || undefined,
1050
+ codebase_context: Object.keys(codebaseContext).length > 0 ? codebaseContext : undefined,
1051
+ ready_to_decompose: true,
1052
+ next_action: "Auto-proceeding to decomposition",
1053
+ };
1054
+
1055
+ return JSON.stringify(output, null, 2);
1056
+ }
1057
+
1058
+ // Confirm-only mode: Generate decomposition, show it, wait for yes/no
1059
+ if (mode === "confirm-only") {
1060
+ // This mode will be handled by calling swarm_delegate_planning
1061
+ // and then asking for confirmation on the result
1062
+ const output: SocraticPlanOutput = {
1063
+ mode: "confirm-only",
1064
+ phase: "ready",
1065
+ recommendation: {
1066
+ approach: "Will generate decomposition for your review",
1067
+ reasoning: "Use swarm_delegate_planning to generate the plan, then present it for yes/no confirmation",
1068
+ },
1069
+ memory_context: memoryContext || undefined,
1070
+ codebase_context: Object.keys(codebaseContext).length > 0 ? codebaseContext : undefined,
1071
+ ready_to_decompose: false,
1072
+ next_action: "Call swarm_delegate_planning, then show result and ask for confirmation",
1073
+ };
1074
+
1075
+ return JSON.stringify(output, null, 2);
1076
+ }
1077
+
1078
+ // Socratic mode: Full interactive planning
1079
+ // Phase 1: Questioning
1080
+ if (currentPhase === "questioning") {
1081
+ // Analyze task to identify what needs clarification
1082
+ const taskLower = args.task.toLowerCase();
1083
+ const questions: Array<{ question: string; options?: string[] }> = [];
1084
+
1085
+ // Check for vague task signals from skill
1086
+ const isVague = {
1087
+ noFiles: !taskLower.includes("src/") && !taskLower.includes("file"),
1088
+ vagueVerb:
1089
+ taskLower.includes("improve") ||
1090
+ taskLower.includes("fix") ||
1091
+ taskLower.includes("update") ||
1092
+ taskLower.includes("make better"),
1093
+ noSuccessCriteria: !taskLower.includes("test") && !taskLower.includes("verify"),
1094
+ };
1095
+
1096
+ // Generate clarifying questions (one at a time)
1097
+ if (isVague.noFiles) {
1098
+ questions.push({
1099
+ question: "Which part of the codebase should this change affect?",
1100
+ options: [
1101
+ "Core functionality (src/)",
1102
+ "UI components (components/)",
1103
+ "API routes (app/api/)",
1104
+ "Configuration and tooling",
1105
+ "Tests",
1106
+ ],
1107
+ });
1108
+ } else if (isVague.vagueVerb) {
1109
+ questions.push({
1110
+ question: "What specific change are you looking for?",
1111
+ options: [
1112
+ "Add new functionality",
1113
+ "Modify existing behavior",
1114
+ "Remove/deprecate something",
1115
+ "Refactor without behavior change",
1116
+ "Fix a bug",
1117
+ ],
1118
+ });
1119
+ } else if (isVague.noSuccessCriteria) {
1120
+ questions.push({
1121
+ question: "How will we know this task is complete?",
1122
+ options: [
1123
+ "All tests pass",
1124
+ "Feature works as demonstrated",
1125
+ "Code review approved",
1126
+ "Documentation updated",
1127
+ "Performance target met",
1128
+ ],
1129
+ });
1130
+ }
1131
+
1132
+ // If task seems clear, move to alternatives phase
1133
+ if (questions.length === 0) {
1134
+ const output: SocraticPlanOutput = {
1135
+ mode: "socratic",
1136
+ phase: "alternatives",
1137
+ memory_context: memoryContext || undefined,
1138
+ codebase_context: Object.keys(codebaseContext).length > 0 ? codebaseContext : undefined,
1139
+ ready_to_decompose: false,
1140
+ next_action: "Task is clear. Call again with phase=alternatives to explore approaches",
1141
+ };
1142
+ return JSON.stringify(output, null, 2);
1143
+ }
1144
+
1145
+ // Return first question only (Socratic principle: one at a time)
1146
+ const output: SocraticPlanOutput = {
1147
+ mode: "socratic",
1148
+ phase: "questioning",
1149
+ questions: [questions[0]],
1150
+ memory_context: memoryContext || undefined,
1151
+ codebase_context: Object.keys(codebaseContext).length > 0 ? codebaseContext : undefined,
1152
+ ready_to_decompose: false,
1153
+ next_action: "User should answer this question, then call again with user_response",
1154
+ };
1155
+
1156
+ return JSON.stringify(output, null, 2);
1157
+ }
1158
+
1159
+ // Phase 2: Alternatives
1160
+ if (currentPhase === "alternatives") {
1161
+ const strategyResult = selectStrategy(args.task);
1162
+
1163
+ // Build 2-3 alternative approaches
1164
+ const alternatives: Array<{
1165
+ name: string;
1166
+ description: string;
1167
+ tradeoffs: string;
1168
+ }> = [];
1169
+
1170
+ // Primary recommendation
1171
+ alternatives.push({
1172
+ name: strategyResult.strategy,
1173
+ description: strategyResult.reasoning,
1174
+ tradeoffs: `Confidence: ${(strategyResult.confidence * 100).toFixed(0)}%. ${STRATEGIES[strategyResult.strategy].description}`,
1175
+ });
1176
+
1177
+ // Add top 2 alternatives
1178
+ for (let i = 0; i < Math.min(2, strategyResult.alternatives.length); i++) {
1179
+ const alt = strategyResult.alternatives[i];
1180
+ alternatives.push({
1181
+ name: alt.strategy,
1182
+ description: STRATEGIES[alt.strategy].description,
1183
+ tradeoffs: `Match score: ${alt.score}. ${STRATEGIES[alt.strategy].antiPatterns[0] || "Consider trade-offs carefully"}`,
1184
+ });
1185
+ }
1186
+
1187
+ const output: SocraticPlanOutput = {
1188
+ mode: "socratic",
1189
+ phase: "alternatives",
1190
+ alternatives,
1191
+ memory_context: memoryContext || undefined,
1192
+ codebase_context: Object.keys(codebaseContext).length > 0 ? codebaseContext : undefined,
1193
+ ready_to_decompose: false,
1194
+ next_action: "User should choose an approach, then call again with phase=recommendation",
1195
+ };
1196
+
1197
+ return JSON.stringify(output, null, 2);
1198
+ }
1199
+
1200
+ // Phase 3: Recommendation
1201
+ if (currentPhase === "recommendation") {
1202
+ const strategyResult = selectStrategy(args.task);
1203
+ const guidelines = formatStrategyGuidelines(strategyResult.strategy);
1204
+
1205
+ const output: SocraticPlanOutput = {
1206
+ mode: "socratic",
1207
+ phase: "recommendation",
1208
+ recommendation: {
1209
+ approach: strategyResult.strategy,
1210
+ reasoning: `Based on your input and task analysis:\n\n${strategyResult.reasoning}\n\n${guidelines}`,
1211
+ },
1212
+ memory_context: memoryContext || undefined,
1213
+ codebase_context: Object.keys(codebaseContext).length > 0 ? codebaseContext : undefined,
1214
+ ready_to_decompose: false,
1215
+ next_action: "User should confirm to proceed. Then call again with phase=ready",
1216
+ };
1217
+
1218
+ return JSON.stringify(output, null, 2);
1219
+ }
1220
+
1221
+ // Phase 4: Ready
1222
+ if (currentPhase === "ready") {
1223
+ const output: SocraticPlanOutput = {
1224
+ mode: "socratic",
1225
+ phase: "ready",
1226
+ recommendation: {
1227
+ approach: "Confirmed by user",
1228
+ reasoning: "Ready to proceed with decomposition",
1229
+ },
1230
+ memory_context: memoryContext || undefined,
1231
+ codebase_context: Object.keys(codebaseContext).length > 0 ? codebaseContext : undefined,
1232
+ ready_to_decompose: true,
1233
+ next_action: "Proceed to swarm_decompose or swarm_delegate_planning",
1234
+ };
1235
+
1236
+ return JSON.stringify(output, null, 2);
1237
+ }
1238
+
1239
+ // Should never reach here
1240
+ throw new Error(`Invalid planning phase: ${currentPhase}`);
1241
+ },
1242
+ });
1243
+
908
1244
  export const decomposeTools = {
909
1245
  swarm_decompose,
910
1246
  swarm_validate_decomposition,
911
1247
  swarm_delegate_planning,
1248
+ swarm_plan_interactive,
912
1249
  };
@@ -871,30 +871,8 @@ export const swarm_progress = tool({
871
871
  });
872
872
  await appendEvent(event, args.project_key);
873
873
 
874
- // Update swarm_contexts table
875
- const { getDatabase } = await import("swarm-mail");
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
- );
874
+ // NOTE: The event handler (handleSwarmCheckpointed in store.ts) updates
875
+ // the swarm_contexts table. We follow event sourcing pattern here.
898
876
  checkpointCreated = true;
899
877
  } catch (error) {
900
878
  // Non-fatal - log and continue
@@ -1218,8 +1196,26 @@ Continuing with completion, but this should be fixed for future subtasks.`;
1218
1196
  .nothrow();
1219
1197
 
1220
1198
  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.`,
1199
+ const stderrOutput = closeResult.stderr.toString().trim();
1200
+ return JSON.stringify(
1201
+ {
1202
+ success: false,
1203
+ error: "Failed to close bead",
1204
+ failed_step: "bd close",
1205
+ details: stderrOutput || "Unknown error from bd close command",
1206
+ bead_id: args.bead_id,
1207
+ recovery: {
1208
+ steps: [
1209
+ `1. Check bead exists: bd show ${args.bead_id}`,
1210
+ `2. Check bead status (might already be closed): beads_query()`,
1211
+ `3. If bead is blocked, unblock first: beads_update(id="${args.bead_id}", status="in_progress")`,
1212
+ `4. Try closing directly: beads_close(id="${args.bead_id}", reason="...")`,
1213
+ ],
1214
+ hint: "If bead is in 'blocked' status, you must change it to 'in_progress' or 'open' before closing.",
1215
+ },
1216
+ },
1217
+ null,
1218
+ 2,
1223
1219
  );
1224
1220
  }
1225
1221
 
@@ -1479,6 +1475,7 @@ Files touched: ${args.files_touched?.join(", ") || "none recorded"}`,
1479
1475
  .join("\n");
1480
1476
 
1481
1477
  // Send urgent notification to coordinator
1478
+ let notificationSent = false;
1482
1479
  try {
1483
1480
  await sendSwarmMessage({
1484
1481
  projectPath: args.project_key,
@@ -1489,6 +1486,7 @@ Files touched: ${args.files_touched?.join(", ") || "none recorded"}`,
1489
1486
  threadId: epicId,
1490
1487
  importance: "urgent",
1491
1488
  });
1489
+ notificationSent = true;
1492
1490
  } catch (mailError) {
1493
1491
  // Even swarm mail failed - log to console as last resort
1494
1492
  console.error(
@@ -1498,8 +1496,41 @@ Files touched: ${args.files_touched?.join(", ") || "none recorded"}`,
1498
1496
  console.error(`[swarm_complete] Original error:`, error);
1499
1497
  }
1500
1498
 
1501
- // Re-throw the original error after notifying
1502
- throw error;
1499
+ // Return structured error instead of throwing
1500
+ // This ensures the agent sees the actual error message
1501
+ return JSON.stringify(
1502
+ {
1503
+ success: false,
1504
+ error: errorMessage,
1505
+ failed_step: failedStep,
1506
+ bead_id: args.bead_id,
1507
+ agent_name: args.agent_name,
1508
+ coordinator_notified: notificationSent,
1509
+ stack_trace: errorStack?.slice(0, 500),
1510
+ context: {
1511
+ summary: args.summary,
1512
+ files_touched: args.files_touched || [],
1513
+ skip_ubs_scan: args.skip_ubs_scan ?? false,
1514
+ skip_verification: args.skip_verification ?? false,
1515
+ },
1516
+ recovery: {
1517
+ steps: [
1518
+ "1. Check the error message above for specific issue",
1519
+ `2. Review failed step: ${failedStep}`,
1520
+ "3. Fix underlying issue or use skip flags if appropriate",
1521
+ "4. Retry swarm_complete after fixing",
1522
+ ],
1523
+ common_fixes: {
1524
+ "Verification Gate": "Use skip_verification=true to bypass (not recommended)",
1525
+ "UBS scan": "Use skip_ubs_scan=true to bypass",
1526
+ "Bead close": "Check bead status with beads_query(), may need beads_update() first",
1527
+ "Self-evaluation": "Check evaluation JSON format matches EvaluationSchema",
1528
+ },
1529
+ },
1530
+ },
1531
+ null,
1532
+ 2,
1533
+ );
1503
1534
  }
1504
1535
  },
1505
1536
  });
@@ -2061,31 +2092,11 @@ export const swarm_checkpoint = tool({
2061
2092
 
2062
2093
  await appendEvent(event, args.project_key);
2063
2094
 
2064
- // Update swarm_contexts table for fast recovery
2065
- const { getDatabase } = await import("swarm-mail");
2066
- const db = await getDatabase(args.project_key);
2095
+ // NOTE: The event handler (handleSwarmCheckpointed in store.ts) updates
2096
+ // the swarm_contexts table. We don't write directly here to follow
2097
+ // event sourcing pattern - single source of truth is the event log.
2067
2098
 
2068
2099
  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
2100
 
2090
2101
  return JSON.stringify(
2091
2102
  {
@@ -2174,15 +2185,21 @@ export const swarm_recover = tool({
2174
2185
  }
2175
2186
 
2176
2187
  const row = result.rows[0];
2188
+ // PGLite auto-parses JSON columns, so we need to handle both cases
2189
+ const parseIfString = <T>(val: unknown): T =>
2190
+ typeof val === "string" ? JSON.parse(val) : (val as T);
2191
+
2177
2192
  const context: SwarmBeadContext = {
2178
2193
  id: row.id,
2179
2194
  epic_id: row.epic_id,
2180
2195
  bead_id: row.bead_id,
2181
2196
  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),
2197
+ files: parseIfString<string[]>(row.files),
2198
+ dependencies: parseIfString<string[]>(row.dependencies),
2199
+ directives: parseIfString<SwarmBeadContext["directives"]>(
2200
+ row.directives,
2201
+ ),
2202
+ recovery: parseIfString<SwarmBeadContext["recovery"]>(row.recovery),
2186
2203
  created_at: row.created_at,
2187
2204
  updated_at: row.updated_at,
2188
2205
  };