opencode-swarm-plugin 0.12.18 → 0.12.20

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/dist/index.js CHANGED
@@ -21608,7 +21608,7 @@ var BeadTreeSchema = exports_external.object({
21608
21608
  title: exports_external.string().min(1),
21609
21609
  description: exports_external.string().optional().default("")
21610
21610
  }),
21611
- subtasks: exports_external.array(SubtaskSpecSchema).min(1).max(10)
21611
+ subtasks: exports_external.array(SubtaskSpecSchema).min(1)
21612
21612
  });
21613
21613
  var EpicCreateArgsSchema = exports_external.object({
21614
21614
  epic_title: exports_external.string().min(1),
@@ -21617,7 +21617,7 @@ var EpicCreateArgsSchema = exports_external.object({
21617
21617
  title: exports_external.string().min(1),
21618
21618
  priority: exports_external.number().int().min(0).max(3).default(2),
21619
21619
  files: exports_external.array(exports_external.string()).optional().default([])
21620
- })).min(1).max(10)
21620
+ })).min(1)
21621
21621
  });
21622
21622
  var EpicCreateResultSchema = exports_external.object({
21623
21623
  success: exports_external.boolean(),
@@ -21720,13 +21720,13 @@ var SubtaskDependencySchema = exports_external.object({
21720
21720
  var TaskDecompositionSchema = exports_external.object({
21721
21721
  task: exports_external.string(),
21722
21722
  reasoning: exports_external.string().optional(),
21723
- subtasks: exports_external.array(DecomposedSubtaskSchema).min(1).max(10),
21723
+ subtasks: exports_external.array(DecomposedSubtaskSchema).min(1),
21724
21724
  dependencies: exports_external.array(SubtaskDependencySchema).optional().default([]),
21725
21725
  shared_context: exports_external.string().optional()
21726
21726
  });
21727
21727
  var DecomposeArgsSchema = exports_external.object({
21728
21728
  task: exports_external.string().min(1),
21729
- max_subtasks: exports_external.number().int().min(1).max(10).default(5),
21729
+ max_subtasks: exports_external.number().int().min(1).default(5),
21730
21730
  context: exports_external.string().optional()
21731
21731
  });
21732
21732
  var SpawnedAgentSchema = exports_external.object({
@@ -22153,7 +22153,40 @@ var beads_sync = tool({
22153
22153
  }
22154
22154
  }
22155
22155
  if (autoPull) {
22156
+ const dirtyCheckResult = await runGitCommand([
22157
+ "status",
22158
+ "--porcelain",
22159
+ "--untracked-files=no"
22160
+ ]);
22161
+ const hasDirtyFiles = dirtyCheckResult.stdout.trim() !== "";
22162
+ let didStash = false;
22163
+ if (hasDirtyFiles) {
22164
+ console.warn("[beads] Detected unstaged changes, stashing before pull...");
22165
+ const stashResult = await runGitCommand([
22166
+ "stash",
22167
+ "push",
22168
+ "-m",
22169
+ "beads_sync: auto-stash before pull",
22170
+ "--include-untracked"
22171
+ ]);
22172
+ if (stashResult.exitCode === 0) {
22173
+ didStash = true;
22174
+ console.warn("[beads] Changes stashed successfully");
22175
+ } else {
22176
+ console.warn(`[beads] Stash failed (${stashResult.stderr}), attempting pull anyway...`);
22177
+ }
22178
+ }
22156
22179
  const pullResult = await withTimeout(runGitCommand(["pull", "--rebase"]), TIMEOUT_MS, "git pull --rebase");
22180
+ if (didStash) {
22181
+ console.warn("[beads] Restoring stashed changes...");
22182
+ const unstashResult = await runGitCommand(["stash", "pop"]);
22183
+ if (unstashResult.exitCode !== 0) {
22184
+ console.error(`[beads] WARNING: Failed to restore stashed changes: ${unstashResult.stderr}`);
22185
+ console.error("[beads] Your changes are in 'git stash list' - run 'git stash pop' manually");
22186
+ } else {
22187
+ console.warn("[beads] Stashed changes restored");
22188
+ }
22189
+ }
22157
22190
  if (pullResult.exitCode !== 0) {
22158
22191
  throw new BeadError(`Failed to pull: ${pullResult.stderr}`, "git pull --rebase", pullResult.exitCode);
22159
22192
  }
@@ -23014,6 +23047,20 @@ function isRetryableError(error45) {
23014
23047
  }
23015
23048
  return false;
23016
23049
  }
23050
+ function isProjectNotFoundError(error45) {
23051
+ if (error45 instanceof Error) {
23052
+ const message = error45.message.toLowerCase();
23053
+ return message.includes("project") && (message.includes("not found") || message.includes("does not exist"));
23054
+ }
23055
+ return false;
23056
+ }
23057
+ function isAgentNotFoundError(error45) {
23058
+ if (error45 instanceof Error) {
23059
+ const message = error45.message.toLowerCase();
23060
+ return message.includes("agent") && (message.includes("not found") || message.includes("does not exist"));
23061
+ }
23062
+ return false;
23063
+ }
23017
23064
  var agentMailAvailable = null;
23018
23065
  async function checkAgentMailAvailable() {
23019
23066
  if (agentMailAvailable !== null) {
@@ -23144,6 +23191,68 @@ async function mcpCall(toolName, args) {
23144
23191
  }
23145
23192
  throw lastError || new Error("Unknown error in mcpCall");
23146
23193
  }
23194
+ async function reRegisterProject(projectKey) {
23195
+ try {
23196
+ console.warn(`[agent-mail] Re-registering project "${projectKey}" after server restart...`);
23197
+ await mcpCall("ensure_project", {
23198
+ human_key: projectKey
23199
+ });
23200
+ console.warn(`[agent-mail] Project "${projectKey}" re-registered successfully`);
23201
+ return true;
23202
+ } catch (error45) {
23203
+ console.error(`[agent-mail] Failed to re-register project "${projectKey}":`, error45);
23204
+ return false;
23205
+ }
23206
+ }
23207
+ async function reRegisterAgent(projectKey, agentName, taskDescription) {
23208
+ try {
23209
+ console.warn(`[agent-mail] Re-registering agent "${agentName}" for project "${projectKey}"...`);
23210
+ await mcpCall("register_agent", {
23211
+ project_key: projectKey,
23212
+ program: "opencode",
23213
+ model: "claude-opus-4",
23214
+ name: agentName,
23215
+ task_description: taskDescription || "Re-registered after server restart"
23216
+ });
23217
+ console.warn(`[agent-mail] Agent "${agentName}" re-registered successfully`);
23218
+ return true;
23219
+ } catch (error45) {
23220
+ console.error(`[agent-mail] Failed to re-register agent "${agentName}":`, error45);
23221
+ return false;
23222
+ }
23223
+ }
23224
+ async function mcpCallWithAutoInit(toolName, args, options) {
23225
+ const maxAttempts = options?.maxReregistrationAttempts ?? 1;
23226
+ let reregistrationAttempts = 0;
23227
+ while (true) {
23228
+ try {
23229
+ return await mcpCall(toolName, args);
23230
+ } catch (error45) {
23231
+ const isProjectError = isProjectNotFoundError(error45);
23232
+ const isAgentError = isAgentNotFoundError(error45);
23233
+ if (!isProjectError && !isAgentError) {
23234
+ throw error45;
23235
+ }
23236
+ if (reregistrationAttempts >= maxAttempts) {
23237
+ console.error(`[agent-mail] Exhausted ${maxAttempts} re-registration attempt(s) for ${toolName}`);
23238
+ throw error45;
23239
+ }
23240
+ reregistrationAttempts++;
23241
+ console.warn(`[agent-mail] Detected "${isProjectError ? "project" : "agent"} not found" for ${toolName}, ` + `attempting re-registration (attempt ${reregistrationAttempts}/${maxAttempts})...`);
23242
+ const projectOk = await reRegisterProject(args.project_key);
23243
+ if (!projectOk) {
23244
+ throw error45;
23245
+ }
23246
+ if (args.agent_name && (isAgentError || toolName !== "ensure_project")) {
23247
+ const agentOk = await reRegisterAgent(args.project_key, args.agent_name, options?.taskDescription);
23248
+ if (!agentOk) {
23249
+ console.warn(`[agent-mail] Agent re-registration failed, but continuing with retry...`);
23250
+ }
23251
+ }
23252
+ console.warn(`[agent-mail] Retrying ${toolName} after re-registration...`);
23253
+ }
23254
+ }
23255
+ }
23147
23256
  function requireState(sessionID) {
23148
23257
  let state = sessionStates.get(sessionID);
23149
23258
  if (!state) {
@@ -24928,10 +25037,10 @@ var swarm_plan_prompt = tool({
24928
25037
  args: {
24929
25038
  task: tool.schema.string().min(1).describe("Task description to decompose"),
24930
25039
  strategy: tool.schema.enum(["file-based", "feature-based", "risk-based", "auto"]).optional().describe("Decomposition strategy (default: auto-detect)"),
24931
- max_subtasks: tool.schema.number().int().min(2).max(10).default(5).describe("Maximum number of subtasks (default: 5)"),
25040
+ max_subtasks: tool.schema.number().int().min(2).default(5).describe("Maximum number of subtasks (default: 5)"),
24932
25041
  context: tool.schema.string().optional().describe("Additional context (codebase info, constraints, etc.)"),
24933
25042
  query_cass: tool.schema.boolean().optional().describe("Query CASS for similar past tasks (default: true)"),
24934
- cass_limit: tool.schema.number().int().min(1).max(10).optional().describe("Max CASS results to include (default: 3)")
25043
+ cass_limit: tool.schema.number().int().min(1).optional().describe("Max CASS results to include (default: 3)")
24935
25044
  },
24936
25045
  async execute(args) {
24937
25046
  let selectedStrategy;
@@ -25001,10 +25110,10 @@ var swarm_decompose = tool({
25001
25110
  description: "Generate decomposition prompt for breaking task into parallelizable subtasks. Optionally queries CASS for similar past tasks.",
25002
25111
  args: {
25003
25112
  task: tool.schema.string().min(1).describe("Task description to decompose"),
25004
- max_subtasks: tool.schema.number().int().min(2).max(10).default(5).describe("Maximum number of subtasks (default: 5)"),
25113
+ max_subtasks: tool.schema.number().int().min(2).default(5).describe("Maximum number of subtasks (default: 5)"),
25005
25114
  context: tool.schema.string().optional().describe("Additional context (codebase info, constraints, etc.)"),
25006
25115
  query_cass: tool.schema.boolean().optional().describe("Query CASS for similar past tasks (default: true)"),
25007
- cass_limit: tool.schema.number().int().min(1).max(10).optional().describe("Max CASS results to include (default: 3)")
25116
+ cass_limit: tool.schema.number().int().min(1).optional().describe("Max CASS results to include (default: 3)")
25008
25117
  },
25009
25118
  async execute(args) {
25010
25119
  let cassContext = "";
@@ -25217,8 +25326,9 @@ var swarm_progress = tool({
25217
25326
  await Bun.$`bd update ${args.bead_id} --status ${beadStatus} --json`.quiet().nothrow();
25218
25327
  }
25219
25328
  const epicId = args.bead_id.includes(".") ? args.bead_id.split(".")[0] : args.bead_id;
25220
- await mcpCall("send_message", {
25329
+ await mcpCallWithAutoInit("send_message", {
25221
25330
  project_key: args.project_key,
25331
+ agent_name: args.agent_name,
25222
25332
  sender_name: args.agent_name,
25223
25333
  to: [],
25224
25334
  subject: `Progress: ${args.bead_id} - ${args.status}`,
@@ -25308,8 +25418,9 @@ ${args.files_affected.map((f) => `- \`${f}\``).join(`
25308
25418
  ].filter(Boolean).join(`
25309
25419
  `);
25310
25420
  const mailImportance = args.importance === "blocker" ? "urgent" : args.importance === "warning" ? "high" : "normal";
25311
- await mcpCall("send_message", {
25421
+ await mcpCallWithAutoInit("send_message", {
25312
25422
  project_key: state.projectKey,
25423
+ agent_name: state.agentName,
25313
25424
  sender_name: state.agentName,
25314
25425
  to: [],
25315
25426
  subject: `[${args.importance.toUpperCase()}] Context update from ${state.agentName}`,
@@ -25381,7 +25492,7 @@ var swarm_complete = tool({
25381
25492
  throw new SwarmError(`Failed to close bead: ${closeResult.stderr.toString()}`, "complete");
25382
25493
  }
25383
25494
  try {
25384
- await mcpCall("release_file_reservations", {
25495
+ await mcpCallWithAutoInit("release_file_reservations", {
25385
25496
  project_key: args.project_key,
25386
25497
  agent_name: args.agent_name
25387
25498
  });
@@ -25398,8 +25509,9 @@ var swarm_complete = tool({
25398
25509
  parsedEvaluation?.overall_feedback ? `**Feedback**: ${parsedEvaluation.overall_feedback}` : ""
25399
25510
  ].filter(Boolean).join(`
25400
25511
  `);
25401
- await mcpCall("send_message", {
25512
+ await mcpCallWithAutoInit("send_message", {
25402
25513
  project_key: args.project_key,
25514
+ agent_name: args.agent_name,
25403
25515
  sender_name: args.agent_name,
25404
25516
  to: [],
25405
25517
  subject: `Complete: ${args.bead_id}`,
package/dist/plugin.js CHANGED
@@ -21608,7 +21608,7 @@ var BeadTreeSchema = exports_external.object({
21608
21608
  title: exports_external.string().min(1),
21609
21609
  description: exports_external.string().optional().default("")
21610
21610
  }),
21611
- subtasks: exports_external.array(SubtaskSpecSchema).min(1).max(10)
21611
+ subtasks: exports_external.array(SubtaskSpecSchema).min(1)
21612
21612
  });
21613
21613
  var EpicCreateArgsSchema = exports_external.object({
21614
21614
  epic_title: exports_external.string().min(1),
@@ -21617,7 +21617,7 @@ var EpicCreateArgsSchema = exports_external.object({
21617
21617
  title: exports_external.string().min(1),
21618
21618
  priority: exports_external.number().int().min(0).max(3).default(2),
21619
21619
  files: exports_external.array(exports_external.string()).optional().default([])
21620
- })).min(1).max(10)
21620
+ })).min(1)
21621
21621
  });
21622
21622
  var EpicCreateResultSchema = exports_external.object({
21623
21623
  success: exports_external.boolean(),
@@ -21720,13 +21720,13 @@ var SubtaskDependencySchema = exports_external.object({
21720
21720
  var TaskDecompositionSchema = exports_external.object({
21721
21721
  task: exports_external.string(),
21722
21722
  reasoning: exports_external.string().optional(),
21723
- subtasks: exports_external.array(DecomposedSubtaskSchema).min(1).max(10),
21723
+ subtasks: exports_external.array(DecomposedSubtaskSchema).min(1),
21724
21724
  dependencies: exports_external.array(SubtaskDependencySchema).optional().default([]),
21725
21725
  shared_context: exports_external.string().optional()
21726
21726
  });
21727
21727
  var DecomposeArgsSchema = exports_external.object({
21728
21728
  task: exports_external.string().min(1),
21729
- max_subtasks: exports_external.number().int().min(1).max(10).default(5),
21729
+ max_subtasks: exports_external.number().int().min(1).default(5),
21730
21730
  context: exports_external.string().optional()
21731
21731
  });
21732
21732
  var SpawnedAgentSchema = exports_external.object({
@@ -22153,7 +22153,40 @@ var beads_sync = tool({
22153
22153
  }
22154
22154
  }
22155
22155
  if (autoPull) {
22156
+ const dirtyCheckResult = await runGitCommand([
22157
+ "status",
22158
+ "--porcelain",
22159
+ "--untracked-files=no"
22160
+ ]);
22161
+ const hasDirtyFiles = dirtyCheckResult.stdout.trim() !== "";
22162
+ let didStash = false;
22163
+ if (hasDirtyFiles) {
22164
+ console.warn("[beads] Detected unstaged changes, stashing before pull...");
22165
+ const stashResult = await runGitCommand([
22166
+ "stash",
22167
+ "push",
22168
+ "-m",
22169
+ "beads_sync: auto-stash before pull",
22170
+ "--include-untracked"
22171
+ ]);
22172
+ if (stashResult.exitCode === 0) {
22173
+ didStash = true;
22174
+ console.warn("[beads] Changes stashed successfully");
22175
+ } else {
22176
+ console.warn(`[beads] Stash failed (${stashResult.stderr}), attempting pull anyway...`);
22177
+ }
22178
+ }
22156
22179
  const pullResult = await withTimeout(runGitCommand(["pull", "--rebase"]), TIMEOUT_MS, "git pull --rebase");
22180
+ if (didStash) {
22181
+ console.warn("[beads] Restoring stashed changes...");
22182
+ const unstashResult = await runGitCommand(["stash", "pop"]);
22183
+ if (unstashResult.exitCode !== 0) {
22184
+ console.error(`[beads] WARNING: Failed to restore stashed changes: ${unstashResult.stderr}`);
22185
+ console.error("[beads] Your changes are in 'git stash list' - run 'git stash pop' manually");
22186
+ } else {
22187
+ console.warn("[beads] Stashed changes restored");
22188
+ }
22189
+ }
22157
22190
  if (pullResult.exitCode !== 0) {
22158
22191
  throw new BeadError(`Failed to pull: ${pullResult.stderr}`, "git pull --rebase", pullResult.exitCode);
22159
22192
  }
@@ -22984,6 +23017,20 @@ function isRetryableError(error45) {
22984
23017
  }
22985
23018
  return false;
22986
23019
  }
23020
+ function isProjectNotFoundError(error45) {
23021
+ if (error45 instanceof Error) {
23022
+ const message = error45.message.toLowerCase();
23023
+ return message.includes("project") && (message.includes("not found") || message.includes("does not exist"));
23024
+ }
23025
+ return false;
23026
+ }
23027
+ function isAgentNotFoundError(error45) {
23028
+ if (error45 instanceof Error) {
23029
+ const message = error45.message.toLowerCase();
23030
+ return message.includes("agent") && (message.includes("not found") || message.includes("does not exist"));
23031
+ }
23032
+ return false;
23033
+ }
22987
23034
  var agentMailAvailable = null;
22988
23035
  async function checkAgentMailAvailable() {
22989
23036
  if (agentMailAvailable !== null) {
@@ -23114,6 +23161,68 @@ async function mcpCall(toolName, args) {
23114
23161
  }
23115
23162
  throw lastError || new Error("Unknown error in mcpCall");
23116
23163
  }
23164
+ async function reRegisterProject(projectKey) {
23165
+ try {
23166
+ console.warn(`[agent-mail] Re-registering project "${projectKey}" after server restart...`);
23167
+ await mcpCall("ensure_project", {
23168
+ human_key: projectKey
23169
+ });
23170
+ console.warn(`[agent-mail] Project "${projectKey}" re-registered successfully`);
23171
+ return true;
23172
+ } catch (error45) {
23173
+ console.error(`[agent-mail] Failed to re-register project "${projectKey}":`, error45);
23174
+ return false;
23175
+ }
23176
+ }
23177
+ async function reRegisterAgent(projectKey, agentName, taskDescription) {
23178
+ try {
23179
+ console.warn(`[agent-mail] Re-registering agent "${agentName}" for project "${projectKey}"...`);
23180
+ await mcpCall("register_agent", {
23181
+ project_key: projectKey,
23182
+ program: "opencode",
23183
+ model: "claude-opus-4",
23184
+ name: agentName,
23185
+ task_description: taskDescription || "Re-registered after server restart"
23186
+ });
23187
+ console.warn(`[agent-mail] Agent "${agentName}" re-registered successfully`);
23188
+ return true;
23189
+ } catch (error45) {
23190
+ console.error(`[agent-mail] Failed to re-register agent "${agentName}":`, error45);
23191
+ return false;
23192
+ }
23193
+ }
23194
+ async function mcpCallWithAutoInit(toolName, args, options) {
23195
+ const maxAttempts = options?.maxReregistrationAttempts ?? 1;
23196
+ let reregistrationAttempts = 0;
23197
+ while (true) {
23198
+ try {
23199
+ return await mcpCall(toolName, args);
23200
+ } catch (error45) {
23201
+ const isProjectError = isProjectNotFoundError(error45);
23202
+ const isAgentError = isAgentNotFoundError(error45);
23203
+ if (!isProjectError && !isAgentError) {
23204
+ throw error45;
23205
+ }
23206
+ if (reregistrationAttempts >= maxAttempts) {
23207
+ console.error(`[agent-mail] Exhausted ${maxAttempts} re-registration attempt(s) for ${toolName}`);
23208
+ throw error45;
23209
+ }
23210
+ reregistrationAttempts++;
23211
+ console.warn(`[agent-mail] Detected "${isProjectError ? "project" : "agent"} not found" for ${toolName}, ` + `attempting re-registration (attempt ${reregistrationAttempts}/${maxAttempts})...`);
23212
+ const projectOk = await reRegisterProject(args.project_key);
23213
+ if (!projectOk) {
23214
+ throw error45;
23215
+ }
23216
+ if (args.agent_name && (isAgentError || toolName !== "ensure_project")) {
23217
+ const agentOk = await reRegisterAgent(args.project_key, args.agent_name, options?.taskDescription);
23218
+ if (!agentOk) {
23219
+ console.warn(`[agent-mail] Agent re-registration failed, but continuing with retry...`);
23220
+ }
23221
+ }
23222
+ console.warn(`[agent-mail] Retrying ${toolName} after re-registration...`);
23223
+ }
23224
+ }
23225
+ }
23117
23226
  function requireState(sessionID) {
23118
23227
  let state = sessionStates.get(sessionID);
23119
23228
  if (!state) {
@@ -24867,10 +24976,10 @@ var swarm_plan_prompt = tool({
24867
24976
  args: {
24868
24977
  task: tool.schema.string().min(1).describe("Task description to decompose"),
24869
24978
  strategy: tool.schema.enum(["file-based", "feature-based", "risk-based", "auto"]).optional().describe("Decomposition strategy (default: auto-detect)"),
24870
- max_subtasks: tool.schema.number().int().min(2).max(10).default(5).describe("Maximum number of subtasks (default: 5)"),
24979
+ max_subtasks: tool.schema.number().int().min(2).default(5).describe("Maximum number of subtasks (default: 5)"),
24871
24980
  context: tool.schema.string().optional().describe("Additional context (codebase info, constraints, etc.)"),
24872
24981
  query_cass: tool.schema.boolean().optional().describe("Query CASS for similar past tasks (default: true)"),
24873
- cass_limit: tool.schema.number().int().min(1).max(10).optional().describe("Max CASS results to include (default: 3)")
24982
+ cass_limit: tool.schema.number().int().min(1).optional().describe("Max CASS results to include (default: 3)")
24874
24983
  },
24875
24984
  async execute(args) {
24876
24985
  let selectedStrategy;
@@ -24940,10 +25049,10 @@ var swarm_decompose = tool({
24940
25049
  description: "Generate decomposition prompt for breaking task into parallelizable subtasks. Optionally queries CASS for similar past tasks.",
24941
25050
  args: {
24942
25051
  task: tool.schema.string().min(1).describe("Task description to decompose"),
24943
- max_subtasks: tool.schema.number().int().min(2).max(10).default(5).describe("Maximum number of subtasks (default: 5)"),
25052
+ max_subtasks: tool.schema.number().int().min(2).default(5).describe("Maximum number of subtasks (default: 5)"),
24944
25053
  context: tool.schema.string().optional().describe("Additional context (codebase info, constraints, etc.)"),
24945
25054
  query_cass: tool.schema.boolean().optional().describe("Query CASS for similar past tasks (default: true)"),
24946
- cass_limit: tool.schema.number().int().min(1).max(10).optional().describe("Max CASS results to include (default: 3)")
25055
+ cass_limit: tool.schema.number().int().min(1).optional().describe("Max CASS results to include (default: 3)")
24947
25056
  },
24948
25057
  async execute(args) {
24949
25058
  let cassContext = "";
@@ -25156,8 +25265,9 @@ var swarm_progress = tool({
25156
25265
  await Bun.$`bd update ${args.bead_id} --status ${beadStatus} --json`.quiet().nothrow();
25157
25266
  }
25158
25267
  const epicId = args.bead_id.includes(".") ? args.bead_id.split(".")[0] : args.bead_id;
25159
- await mcpCall("send_message", {
25268
+ await mcpCallWithAutoInit("send_message", {
25160
25269
  project_key: args.project_key,
25270
+ agent_name: args.agent_name,
25161
25271
  sender_name: args.agent_name,
25162
25272
  to: [],
25163
25273
  subject: `Progress: ${args.bead_id} - ${args.status}`,
@@ -25247,8 +25357,9 @@ ${args.files_affected.map((f) => `- \`${f}\``).join(`
25247
25357
  ].filter(Boolean).join(`
25248
25358
  `);
25249
25359
  const mailImportance = args.importance === "blocker" ? "urgent" : args.importance === "warning" ? "high" : "normal";
25250
- await mcpCall("send_message", {
25360
+ await mcpCallWithAutoInit("send_message", {
25251
25361
  project_key: state.projectKey,
25362
+ agent_name: state.agentName,
25252
25363
  sender_name: state.agentName,
25253
25364
  to: [],
25254
25365
  subject: `[${args.importance.toUpperCase()}] Context update from ${state.agentName}`,
@@ -25320,7 +25431,7 @@ var swarm_complete = tool({
25320
25431
  throw new SwarmError(`Failed to close bead: ${closeResult.stderr.toString()}`, "complete");
25321
25432
  }
25322
25433
  try {
25323
- await mcpCall("release_file_reservations", {
25434
+ await mcpCallWithAutoInit("release_file_reservations", {
25324
25435
  project_key: args.project_key,
25325
25436
  agent_name: args.agent_name
25326
25437
  });
@@ -25337,8 +25448,9 @@ var swarm_complete = tool({
25337
25448
  parsedEvaluation?.overall_feedback ? `**Feedback**: ${parsedEvaluation.overall_feedback}` : ""
25338
25449
  ].filter(Boolean).join(`
25339
25450
  `);
25340
- await mcpCall("send_message", {
25451
+ await mcpCallWithAutoInit("send_message", {
25341
25452
  project_key: args.project_key,
25453
+ agent_name: args.agent_name,
25342
25454
  sender_name: args.agent_name,
25343
25455
  to: [],
25344
25456
  subject: `Complete: ${args.bead_id}`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-swarm-plugin",
3
- "version": "0.12.18",
3
+ "version": "0.12.20",
4
4
  "description": "Multi-agent swarm coordination for OpenCode with learning capabilities, beads integration, and Agent Mail",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -11,12 +11,15 @@
11
11
  import { describe, it, expect, beforeAll } from "vitest";
12
12
  import {
13
13
  mcpCall,
14
+ mcpCallWithAutoInit,
14
15
  sessionStates,
15
16
  setState,
16
17
  clearState,
17
18
  requireState,
18
19
  MAX_INBOX_LIMIT,
19
20
  AgentMailNotInitializedError,
21
+ isProjectNotFoundError,
22
+ isAgentNotFoundError,
20
23
  type AgentMailState,
21
24
  } from "./agent-mail";
22
25
 
@@ -1318,4 +1321,109 @@ describe("agent-mail integration", () => {
1318
1321
  clearState(worker2Ctx.sessionID);
1319
1322
  });
1320
1323
  });
1324
+
1325
+ // ============================================================================
1326
+ // Self-Healing Tests (mcpCallWithAutoInit)
1327
+ // ============================================================================
1328
+
1329
+ describe("self-healing (mcpCallWithAutoInit)", () => {
1330
+ it("detects project not found errors correctly", () => {
1331
+ const projectError = new Error("Project 'migrate-egghead' not found.");
1332
+ const agentError = new Error("Agent 'BlueLake' not found in project");
1333
+ const otherError = new Error("Network timeout");
1334
+
1335
+ expect(isProjectNotFoundError(projectError)).toBe(true);
1336
+ expect(isProjectNotFoundError(agentError)).toBe(false);
1337
+ expect(isProjectNotFoundError(otherError)).toBe(false);
1338
+
1339
+ expect(isAgentNotFoundError(agentError)).toBe(true);
1340
+ expect(isAgentNotFoundError(projectError)).toBe(false);
1341
+ expect(isAgentNotFoundError(otherError)).toBe(false);
1342
+ });
1343
+
1344
+ it("auto-registers project on 'not found' error", async () => {
1345
+ const ctx = createTestContext();
1346
+
1347
+ // First, ensure project exists and register an agent
1348
+ const { state } = await initTestAgent(ctx, `AutoInit_${Date.now()}`);
1349
+
1350
+ // Now use mcpCallWithAutoInit - it should work normally
1351
+ // (no error to recover from, but verifies the wrapper works)
1352
+ await mcpCallWithAutoInit("send_message", {
1353
+ project_key: state.projectKey,
1354
+ agent_name: state.agentName,
1355
+ sender_name: state.agentName,
1356
+ to: [],
1357
+ subject: "Test auto-init wrapper",
1358
+ body_md: "This should work normally",
1359
+ thread_id: "test-thread",
1360
+ importance: "normal",
1361
+ });
1362
+
1363
+ // Verify message was sent by checking inbox
1364
+ const inbox = await mcpCall<Array<{ subject: string }>>("fetch_inbox", {
1365
+ project_key: state.projectKey,
1366
+ agent_name: state.agentName,
1367
+ limit: 5,
1368
+ include_bodies: false,
1369
+ });
1370
+
1371
+ // The message should be in the inbox (sent to empty 'to' = broadcast)
1372
+ // Note: depending on Agent Mail behavior, broadcast might not show in sender's inbox
1373
+ // This test mainly verifies the wrapper doesn't break normal operation
1374
+
1375
+ // Cleanup
1376
+ clearState(ctx.sessionID);
1377
+ });
1378
+
1379
+ it("recovers from simulated project not found by re-registering", async () => {
1380
+ const ctx = createTestContext();
1381
+
1382
+ // Create a fresh project key that doesn't exist yet
1383
+ const freshProjectKey = `/test/fresh-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
1384
+ const agentName = `Recovery_${Date.now()}`;
1385
+
1386
+ // First ensure the project exists (simulating initial setup)
1387
+ await mcpCall("ensure_project", { human_key: freshProjectKey });
1388
+ await mcpCall("register_agent", {
1389
+ project_key: freshProjectKey,
1390
+ program: "opencode-test",
1391
+ model: "test-model",
1392
+ name: agentName,
1393
+ task_description: "Recovery test agent",
1394
+ });
1395
+
1396
+ // Now use mcpCallWithAutoInit for an operation
1397
+ // This should work, and if the project somehow got lost, it would re-register
1398
+ await mcpCallWithAutoInit("send_message", {
1399
+ project_key: freshProjectKey,
1400
+ agent_name: agentName,
1401
+ sender_name: agentName,
1402
+ to: [],
1403
+ subject: "Recovery test",
1404
+ body_md: "Testing self-healing",
1405
+ thread_id: "recovery-test",
1406
+ importance: "normal",
1407
+ });
1408
+
1409
+ // If we got here without error, the wrapper is working
1410
+ // (In a real scenario where the server restarted, it would have re-registered)
1411
+ });
1412
+
1413
+ it("passes through non-recoverable errors", async () => {
1414
+ const ctx = createTestContext();
1415
+ const { state } = await initTestAgent(ctx, `ErrorPass_${Date.now()}`);
1416
+
1417
+ // Try to call a non-existent tool - should throw, not retry forever
1418
+ await expect(
1419
+ mcpCallWithAutoInit("nonexistent_tool_xyz", {
1420
+ project_key: state.projectKey,
1421
+ agent_name: state.agentName,
1422
+ }),
1423
+ ).rejects.toThrow(/Unknown tool/);
1424
+
1425
+ // Cleanup
1426
+ clearState(ctx.sessionID);
1427
+ });
1428
+ });
1321
1429
  });
package/src/agent-mail.ts CHANGED
@@ -541,6 +541,39 @@ function isRetryableError(error: unknown): boolean {
541
541
  return false;
542
542
  }
543
543
 
544
+ /**
545
+ * Check if an error indicates the project was not found
546
+ *
547
+ * This happens when Agent Mail server restarts and loses project registrations.
548
+ * The fix is to re-register the project and retry the operation.
549
+ */
550
+ export function isProjectNotFoundError(error: unknown): boolean {
551
+ if (error instanceof Error) {
552
+ const message = error.message.toLowerCase();
553
+ return (
554
+ message.includes("project") &&
555
+ (message.includes("not found") || message.includes("does not exist"))
556
+ );
557
+ }
558
+ return false;
559
+ }
560
+
561
+ /**
562
+ * Check if an error indicates the agent was not found
563
+ *
564
+ * Similar to project not found - server restart loses agent registrations.
565
+ */
566
+ export function isAgentNotFoundError(error: unknown): boolean {
567
+ if (error instanceof Error) {
568
+ const message = error.message.toLowerCase();
569
+ return (
570
+ message.includes("agent") &&
571
+ (message.includes("not found") || message.includes("does not exist"))
572
+ );
573
+ }
574
+ return false;
575
+ }
576
+
544
577
  // ============================================================================
545
578
  // MCP Client
546
579
  // ============================================================================
@@ -823,6 +856,153 @@ export async function mcpCall<T>(
823
856
  throw lastError || new Error("Unknown error in mcpCall");
824
857
  }
825
858
 
859
+ /**
860
+ * Re-register a project with Agent Mail server
861
+ *
862
+ * Called when we detect "Project not found" error, indicating server restart.
863
+ * This is a lightweight operation that just ensures the project exists.
864
+ */
865
+ async function reRegisterProject(projectKey: string): Promise<boolean> {
866
+ try {
867
+ console.warn(
868
+ `[agent-mail] Re-registering project "${projectKey}" after server restart...`,
869
+ );
870
+ await mcpCall<ProjectInfo>("ensure_project", {
871
+ human_key: projectKey,
872
+ });
873
+ console.warn(
874
+ `[agent-mail] Project "${projectKey}" re-registered successfully`,
875
+ );
876
+ return true;
877
+ } catch (error) {
878
+ console.error(
879
+ `[agent-mail] Failed to re-register project "${projectKey}":`,
880
+ error,
881
+ );
882
+ return false;
883
+ }
884
+ }
885
+
886
+ /**
887
+ * Re-register an agent with Agent Mail server
888
+ *
889
+ * Called when we detect "Agent not found" error, indicating server restart.
890
+ */
891
+ async function reRegisterAgent(
892
+ projectKey: string,
893
+ agentName: string,
894
+ taskDescription?: string,
895
+ ): Promise<boolean> {
896
+ try {
897
+ console.warn(
898
+ `[agent-mail] Re-registering agent "${agentName}" for project "${projectKey}"...`,
899
+ );
900
+ await mcpCall<AgentInfo>("register_agent", {
901
+ project_key: projectKey,
902
+ program: "opencode",
903
+ model: "claude-opus-4",
904
+ name: agentName,
905
+ task_description: taskDescription || "Re-registered after server restart",
906
+ });
907
+ console.warn(
908
+ `[agent-mail] Agent "${agentName}" re-registered successfully`,
909
+ );
910
+ return true;
911
+ } catch (error) {
912
+ console.error(
913
+ `[agent-mail] Failed to re-register agent "${agentName}":`,
914
+ error,
915
+ );
916
+ return false;
917
+ }
918
+ }
919
+
920
+ /**
921
+ * MCP call with automatic project/agent re-registration on "not found" errors
922
+ *
923
+ * This is the self-healing wrapper that handles Agent Mail server restarts.
924
+ * When the server restarts, it loses all project and agent registrations.
925
+ * This wrapper detects those errors and automatically re-registers before retrying.
926
+ *
927
+ * Use this instead of raw mcpCall when you have project_key and agent_name context.
928
+ *
929
+ * @param toolName - The MCP tool to call
930
+ * @param args - Arguments including project_key and optionally agent_name
931
+ * @param options - Optional configuration for re-registration
932
+ * @returns The result of the MCP call
933
+ */
934
+ export async function mcpCallWithAutoInit<T>(
935
+ toolName: string,
936
+ args: Record<string, unknown> & { project_key: string; agent_name?: string },
937
+ options?: {
938
+ /** Task description for agent re-registration */
939
+ taskDescription?: string;
940
+ /** Max re-registration attempts (default: 1) */
941
+ maxReregistrationAttempts?: number;
942
+ },
943
+ ): Promise<T> {
944
+ const maxAttempts = options?.maxReregistrationAttempts ?? 1;
945
+ let reregistrationAttempts = 0;
946
+
947
+ while (true) {
948
+ try {
949
+ return await mcpCall<T>(toolName, args);
950
+ } catch (error) {
951
+ // Check if this is a recoverable "not found" error
952
+ const isProjectError = isProjectNotFoundError(error);
953
+ const isAgentError = isAgentNotFoundError(error);
954
+
955
+ if (!isProjectError && !isAgentError) {
956
+ // Not a recoverable error, rethrow
957
+ throw error;
958
+ }
959
+
960
+ // Check if we've exhausted re-registration attempts
961
+ if (reregistrationAttempts >= maxAttempts) {
962
+ console.error(
963
+ `[agent-mail] Exhausted ${maxAttempts} re-registration attempt(s) for ${toolName}`,
964
+ );
965
+ throw error;
966
+ }
967
+
968
+ reregistrationAttempts++;
969
+ console.warn(
970
+ `[agent-mail] Detected "${isProjectError ? "project" : "agent"} not found" for ${toolName}, ` +
971
+ `attempting re-registration (attempt ${reregistrationAttempts}/${maxAttempts})...`,
972
+ );
973
+
974
+ // Re-register project first (always needed)
975
+ const projectOk = await reRegisterProject(args.project_key);
976
+ if (!projectOk) {
977
+ throw error; // Can't recover without project
978
+ }
979
+
980
+ // Re-register agent if we have one and it was an agent error
981
+ // (or if the original call needs an agent)
982
+ if (args.agent_name && (isAgentError || toolName !== "ensure_project")) {
983
+ const agentOk = await reRegisterAgent(
984
+ args.project_key,
985
+ args.agent_name,
986
+ options?.taskDescription,
987
+ );
988
+ if (!agentOk) {
989
+ // Agent re-registration failed, but project is OK
990
+ // Some operations might still work, so continue
991
+ console.warn(
992
+ `[agent-mail] Agent re-registration failed, but continuing with retry...`,
993
+ );
994
+ }
995
+ }
996
+
997
+ // Retry the original call
998
+ console.warn(
999
+ `[agent-mail] Retrying ${toolName} after re-registration...`,
1000
+ );
1001
+ // Loop continues to retry
1002
+ }
1003
+ }
1004
+ }
1005
+
826
1006
  /**
827
1007
  * Get Agent Mail state for a session, or throw if not initialized
828
1008
  *
@@ -1490,4 +1670,6 @@ export {
1490
1670
  restartServer,
1491
1671
  RETRY_CONFIG,
1492
1672
  RECOVERY_CONFIG,
1673
+ // Note: isProjectNotFoundError, isAgentNotFoundError, mcpCallWithAutoInit
1674
+ // are exported at their definitions
1493
1675
  };
package/src/beads.ts CHANGED
@@ -717,11 +717,61 @@ export const beads_sync = tool({
717
717
 
718
718
  // 5. Pull if requested (with rebase to avoid merge commits)
719
719
  if (autoPull) {
720
+ // Check for unstaged changes that would block pull --rebase
721
+ const dirtyCheckResult = await runGitCommand([
722
+ "status",
723
+ "--porcelain",
724
+ "--untracked-files=no",
725
+ ]);
726
+ const hasDirtyFiles = dirtyCheckResult.stdout.trim() !== "";
727
+ let didStash = false;
728
+
729
+ // Stash dirty files before pull (self-healing for "unstaged changes" error)
730
+ if (hasDirtyFiles) {
731
+ console.warn(
732
+ "[beads] Detected unstaged changes, stashing before pull...",
733
+ );
734
+ const stashResult = await runGitCommand([
735
+ "stash",
736
+ "push",
737
+ "-m",
738
+ "beads_sync: auto-stash before pull",
739
+ "--include-untracked",
740
+ ]);
741
+ if (stashResult.exitCode === 0) {
742
+ didStash = true;
743
+ console.warn("[beads] Changes stashed successfully");
744
+ } else {
745
+ // Stash failed - try pull anyway, it might work
746
+ console.warn(
747
+ `[beads] Stash failed (${stashResult.stderr}), attempting pull anyway...`,
748
+ );
749
+ }
750
+ }
751
+
720
752
  const pullResult = await withTimeout(
721
753
  runGitCommand(["pull", "--rebase"]),
722
754
  TIMEOUT_MS,
723
755
  "git pull --rebase",
724
756
  );
757
+
758
+ // Restore stashed changes regardless of pull result
759
+ if (didStash) {
760
+ console.warn("[beads] Restoring stashed changes...");
761
+ const unstashResult = await runGitCommand(["stash", "pop"]);
762
+ if (unstashResult.exitCode !== 0) {
763
+ // Unstash failed - this is bad, user needs to know
764
+ console.error(
765
+ `[beads] WARNING: Failed to restore stashed changes: ${unstashResult.stderr}`,
766
+ );
767
+ console.error(
768
+ "[beads] Your changes are in 'git stash list' - run 'git stash pop' manually",
769
+ );
770
+ } else {
771
+ console.warn("[beads] Stashed changes restored");
772
+ }
773
+ }
774
+
725
775
  if (pullResult.exitCode !== 0) {
726
776
  throw new BeadError(
727
777
  `Failed to pull: ${pullResult.stderr}`,
@@ -117,7 +117,7 @@ export const BeadTreeSchema = z.object({
117
117
  title: z.string().min(1),
118
118
  description: z.string().optional().default(""),
119
119
  }),
120
- subtasks: z.array(SubtaskSpecSchema).min(1).max(10),
120
+ subtasks: z.array(SubtaskSpecSchema).min(1),
121
121
  });
122
122
  export type BeadTree = z.infer<typeof BeadTreeSchema>;
123
123
 
@@ -133,8 +133,7 @@ export const EpicCreateArgsSchema = z.object({
133
133
  files: z.array(z.string()).optional().default([]),
134
134
  }),
135
135
  )
136
- .min(1)
137
- .max(10),
136
+ .min(1),
138
137
  });
139
138
  export type EpicCreateArgs = z.infer<typeof EpicCreateArgsSchema>;
140
139
 
@@ -57,7 +57,7 @@ export type SubtaskDependency = z.infer<typeof SubtaskDependencySchema>;
57
57
  export const TaskDecompositionSchema = z.object({
58
58
  task: z.string(), // Original task description
59
59
  reasoning: z.string().optional(), // Why this decomposition
60
- subtasks: z.array(DecomposedSubtaskSchema).min(1).max(10),
60
+ subtasks: z.array(DecomposedSubtaskSchema).min(1),
61
61
  dependencies: z.array(SubtaskDependencySchema).optional().default([]),
62
62
  shared_context: z.string().optional(), // Context to pass to all agents
63
63
  });
@@ -68,7 +68,7 @@ export type TaskDecomposition = z.infer<typeof TaskDecompositionSchema>;
68
68
  */
69
69
  export const DecomposeArgsSchema = z.object({
70
70
  task: z.string().min(1),
71
- max_subtasks: z.number().int().min(1).max(10).default(5),
71
+ max_subtasks: z.number().int().min(1).default(5),
72
72
  context: z.string().optional(),
73
73
  });
74
74
  export type DecomposeArgs = z.infer<typeof DecomposeArgsSchema>;
package/src/swarm.ts CHANGED
@@ -25,7 +25,7 @@ import {
25
25
  type SpawnedAgent,
26
26
  type Bead,
27
27
  } from "./schemas";
28
- import { mcpCall, requireState } from "./agent-mail";
28
+ import { mcpCall, mcpCallWithAutoInit, requireState } from "./agent-mail";
29
29
  import {
30
30
  OutcomeSignalsSchema,
31
31
  DecompositionStrategySchema,
@@ -1222,7 +1222,7 @@ export const swarm_plan_prompt = tool({
1222
1222
  .number()
1223
1223
  .int()
1224
1224
  .min(2)
1225
- .max(10)
1225
+
1226
1226
  .default(5)
1227
1227
  .describe("Maximum number of subtasks (default: 5)"),
1228
1228
  context: tool.schema
@@ -1237,7 +1237,7 @@ export const swarm_plan_prompt = tool({
1237
1237
  .number()
1238
1238
  .int()
1239
1239
  .min(1)
1240
- .max(10)
1240
+
1241
1241
  .optional()
1242
1242
  .describe("Max CASS results to include (default: 3)"),
1243
1243
  },
@@ -1352,7 +1352,7 @@ export const swarm_decompose = tool({
1352
1352
  .number()
1353
1353
  .int()
1354
1354
  .min(2)
1355
- .max(10)
1355
+
1356
1356
  .default(5)
1357
1357
  .describe("Maximum number of subtasks (default: 5)"),
1358
1358
  context: tool.schema
@@ -1367,7 +1367,7 @@ export const swarm_decompose = tool({
1367
1367
  .number()
1368
1368
  .int()
1369
1369
  .min(1)
1370
- .max(10)
1370
+
1371
1371
  .optional()
1372
1372
  .describe("Max CASS results to include (default: 3)"),
1373
1373
  },
@@ -1721,9 +1721,10 @@ export const swarm_progress = tool({
1721
1721
  ? args.bead_id.split(".")[0]
1722
1722
  : args.bead_id;
1723
1723
 
1724
- // Send progress message to thread
1725
- await mcpCall("send_message", {
1724
+ // Send progress message to thread (with auto-reinit on server restart)
1725
+ await mcpCallWithAutoInit("send_message", {
1726
1726
  project_key: args.project_key,
1727
+ agent_name: args.agent_name,
1727
1728
  sender_name: args.agent_name,
1728
1729
  to: [], // Coordinator will pick it up from thread
1729
1730
  subject: `Progress: ${args.bead_id} - ${args.status}`,
@@ -1892,8 +1893,10 @@ export const swarm_broadcast = tool({
1892
1893
  : "normal";
1893
1894
 
1894
1895
  // Send as broadcast to thread (empty 'to' = all agents in thread)
1895
- await mcpCall("send_message", {
1896
+ // Uses auto-reinit wrapper to handle server restarts gracefully
1897
+ await mcpCallWithAutoInit("send_message", {
1896
1898
  project_key: state.projectKey,
1899
+ agent_name: state.agentName,
1897
1900
  sender_name: state.agentName,
1898
1901
  to: [], // Broadcast to thread
1899
1902
  subject: `[${args.importance.toUpperCase()}] Context update from ${state.agentName}`,
@@ -2020,12 +2023,16 @@ export const swarm_complete = tool({
2020
2023
  }
2021
2024
 
2022
2025
  // Release file reservations for this agent
2026
+ // Uses auto-reinit wrapper to handle server restarts - this was the original
2027
+ // failure point that prompted the self-healing implementation
2023
2028
  try {
2024
- await mcpCall("release_file_reservations", {
2029
+ await mcpCallWithAutoInit("release_file_reservations", {
2025
2030
  project_key: args.project_key,
2026
2031
  agent_name: args.agent_name,
2027
2032
  });
2028
2033
  } catch (error) {
2034
+ // Even with auto-reinit, release might fail (e.g., no reservations existed)
2035
+ // This is non-fatal - log and continue
2029
2036
  console.warn(
2030
2037
  `[swarm] Failed to release file reservations for ${args.agent_name}:`,
2031
2038
  error,
@@ -2053,8 +2060,9 @@ export const swarm_complete = tool({
2053
2060
  .filter(Boolean)
2054
2061
  .join("\n");
2055
2062
 
2056
- await mcpCall("send_message", {
2063
+ await mcpCallWithAutoInit("send_message", {
2057
2064
  project_key: args.project_key,
2065
+ agent_name: args.agent_name,
2058
2066
  sender_name: args.agent_name,
2059
2067
  to: [], // Thread broadcast
2060
2068
  subject: `Complete: ${args.bead_id}`,