@pruddiman/dispatch 1.5.0-beta.236e011 → 1.5.0-beta.30f06ab

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/cli.js CHANGED
@@ -7988,6 +7988,7 @@ import { execFile as execFile7 } from "child_process";
7988
7988
  import { promisify as promisify7 } from "util";
7989
7989
  import { randomUUID as randomUUID3 } from "crypto";
7990
7990
  import { existsSync as existsSync2 } from "fs";
7991
+ import { rm } from "fs/promises";
7991
7992
  async function git4(args, cwd) {
7992
7993
  const { stdout } = await exec7("git", args, { cwd, shell: process.platform === "win32" });
7993
7994
  return stdout;
@@ -8002,40 +8003,83 @@ async function createWorktree(repoRoot, issueFilename, branchName, startPoint) {
8002
8003
  const name = worktreeName(issueFilename);
8003
8004
  const worktreePath = join7(repoRoot, WORKTREE_DIR, name);
8004
8005
  if (existsSync2(worktreePath)) {
8005
- log.debug(`Reusing existing worktree at ${worktreePath}`);
8006
- return worktreePath;
8006
+ try {
8007
+ const listOutput = await git4(["worktree", "list", "--porcelain"], repoRoot);
8008
+ const isRegistered = listOutput.includes(`worktree ${worktreePath}
8009
+ `);
8010
+ if (isRegistered) {
8011
+ try {
8012
+ await git4(["checkout", "--force", branchName], worktreePath);
8013
+ await git4(["clean", "-fd"], worktreePath);
8014
+ log.debug(`Reusing validated worktree at ${worktreePath}`);
8015
+ return worktreePath;
8016
+ } catch {
8017
+ log.debug(`Worktree checkout failed, removing and recreating: ${worktreePath}`);
8018
+ }
8019
+ } else {
8020
+ log.debug(`Directory exists but not a registered worktree: ${worktreePath}`);
8021
+ }
8022
+ } catch {
8023
+ log.debug(`Worktree validation failed for ${worktreePath}`);
8024
+ }
8025
+ await rm(worktreePath, { recursive: true, force: true });
8026
+ await git4(["worktree", "prune"], repoRoot).catch(() => {
8027
+ });
8007
8028
  }
8008
- try {
8009
- const args = ["worktree", "add", worktreePath, "-b", branchName];
8010
- if (startPoint) args.push(startPoint);
8011
- await git4(args, repoRoot);
8012
- log.debug(`Created worktree at ${worktreePath} on branch ${branchName}`);
8013
- } catch (err) {
8014
- const message = log.extractMessage(err);
8015
- if (message.includes("already exists")) {
8016
- try {
8017
- await git4(["worktree", "add", worktreePath, branchName], repoRoot);
8018
- log.debug(`Created worktree at ${worktreePath} using existing branch ${branchName}`);
8019
- return worktreePath;
8020
- } catch (retryErr) {
8021
- const retryMsg = log.extractMessage(retryErr);
8022
- if (retryMsg.includes("already used by worktree")) {
8023
- await git4(["worktree", "prune"], repoRoot);
8029
+ const MAX_RETRIES = 5;
8030
+ const BASE_DELAY_MS = 200;
8031
+ let lastError;
8032
+ for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
8033
+ try {
8034
+ const args = ["worktree", "add", worktreePath, "-b", branchName];
8035
+ if (startPoint) args.push(startPoint);
8036
+ await git4(args, repoRoot);
8037
+ log.debug(`Created worktree at ${worktreePath} on branch ${branchName}`);
8038
+ return worktreePath;
8039
+ } catch (err) {
8040
+ lastError = err;
8041
+ const message = log.extractMessage(err);
8042
+ if (message.includes("already exists")) {
8043
+ try {
8044
+ await git4(["worktree", "add", worktreePath, branchName], repoRoot);
8045
+ log.debug(`Created worktree at ${worktreePath} using existing branch ${branchName}`);
8046
+ return worktreePath;
8047
+ } catch (retryErr) {
8048
+ lastError = retryErr;
8049
+ const retryMsg = log.extractMessage(retryErr);
8050
+ if (retryMsg.includes("already used by worktree")) {
8051
+ await git4(["worktree", "prune"], repoRoot);
8052
+ try {
8053
+ await git4(["worktree", "add", worktreePath, branchName], repoRoot);
8054
+ log.debug(`Created worktree at ${worktreePath} after pruning stale ref`);
8055
+ return worktreePath;
8056
+ } catch (pruneRetryErr) {
8057
+ lastError = pruneRetryErr;
8058
+ }
8059
+ } else {
8060
+ throw retryErr;
8061
+ }
8062
+ }
8063
+ } else if (message.includes("already used by worktree")) {
8064
+ await git4(["worktree", "prune"], repoRoot);
8065
+ try {
8024
8066
  await git4(["worktree", "add", worktreePath, branchName], repoRoot);
8025
8067
  log.debug(`Created worktree at ${worktreePath} after pruning stale ref`);
8026
- } else {
8027
- throw retryErr;
8068
+ return worktreePath;
8069
+ } catch (pruneRetryErr) {
8070
+ lastError = pruneRetryErr;
8028
8071
  }
8072
+ } else if (!message.includes("lock") && !message.includes("already")) {
8073
+ throw err;
8074
+ }
8075
+ if (attempt < MAX_RETRIES) {
8076
+ const delay = BASE_DELAY_MS * Math.pow(2, attempt - 1);
8077
+ log.debug(`Worktree creation attempt ${attempt}/${MAX_RETRIES} failed (${message}), retrying in ${delay}ms...`);
8078
+ await new Promise((resolve6) => setTimeout(resolve6, delay));
8029
8079
  }
8030
- } else if (message.includes("already used by worktree")) {
8031
- await git4(["worktree", "prune"], repoRoot);
8032
- await git4(["worktree", "add", worktreePath, branchName], repoRoot);
8033
- log.debug(`Created worktree at ${worktreePath} after pruning stale ref`);
8034
- } else {
8035
- throw err;
8036
8080
  }
8037
8081
  }
8038
- return worktreePath;
8082
+ throw lastError;
8039
8083
  }
8040
8084
  async function removeWorktree(repoRoot, issueFilename) {
8041
8085
  const name = worktreeName(issueFilename);
@@ -9967,6 +10011,13 @@ async function dispatchTask(instance, task, cwd, plan, worktreeRoot) {
9967
10011
  fileLoggerStorage.getStore()?.warn("dispatchTask: null response");
9968
10012
  return { task, success: false, error: "No response from agent" };
9969
10013
  }
10014
+ const isRateLimited = rateLimitPatterns.some((p) => p.test(response));
10015
+ if (isRateLimited) {
10016
+ const truncated = response.slice(0, 200);
10017
+ log.debug(`Task dispatch hit rate limit: ${truncated}`);
10018
+ fileLoggerStorage.getStore()?.warn(`dispatchTask: rate limit detected \u2014 ${truncated}`);
10019
+ return { task, success: false, error: `Rate limit: ${truncated}` };
10020
+ }
9970
10021
  log.debug(`Task dispatch completed (${response.length} chars response)`);
9971
10022
  fileLoggerStorage.getStore()?.response("dispatchTask", response);
9972
10023
  return { task, success: true };
@@ -10042,12 +10093,19 @@ function buildWorktreeIsolation(worktreeRoot) {
10042
10093
  `- **Worktree isolation:** You are operating inside a git worktree at \`${worktreeRoot}\`. You MUST NOT read, write, or execute commands that access files outside this directory. All file paths must resolve within \`${worktreeRoot}\`.`
10043
10094
  ];
10044
10095
  }
10096
+ var rateLimitPatterns;
10045
10097
  var init_dispatcher = __esm({
10046
10098
  "src/dispatcher.ts"() {
10047
10099
  "use strict";
10048
10100
  init_logger();
10049
10101
  init_file_logger();
10050
10102
  init_environment();
10103
+ rateLimitPatterns = [
10104
+ /you[''\u2019]?ve hit your (rate )?limit/i,
10105
+ /rate limit exceeded/i,
10106
+ /too many requests/i,
10107
+ /quota exceeded/i
10108
+ ];
10051
10109
  }
10052
10110
  });
10053
10111
 
@@ -10837,7 +10895,7 @@ ${err.stack}` : ""}`);
10837
10895
  const taskId = buildTaskId(task);
10838
10896
  const taskText = task.text;
10839
10897
  if (type === "task_start") {
10840
- progressCallback({ type, taskId, taskText, phase: extra?.phase });
10898
+ progressCallback({ type, taskId, taskText, phase: extra?.phase, file: task.file, line: task.line });
10841
10899
  } else if (type === "task_done") {
10842
10900
  progressCallback({ type, taskId, taskText });
10843
10901
  } else {
@@ -11867,6 +11925,12 @@ function registerLiveRun(runId) {
11867
11925
  function unregisterLiveRun(runId) {
11868
11926
  liveRuns.delete(runId);
11869
11927
  }
11928
+ function addLogCallback(runId, cb) {
11929
+ const run = liveRuns.get(runId);
11930
+ if (run) {
11931
+ run.callbacks.push(cb);
11932
+ }
11933
+ }
11870
11934
  function emitLog(runId, message, level = "info") {
11871
11935
  const run = liveRuns.get(runId);
11872
11936
  if (run) {
@@ -11971,6 +12035,12 @@ function listRunsByStatus(status, limit = 20) {
11971
12035
  ).all(status, limit);
11972
12036
  return rows.map(rowToRun);
11973
12037
  }
12038
+ function createTask(opts) {
12039
+ getDb().prepare(`
12040
+ INSERT INTO tasks (run_id, task_id, task_text, file, line, status)
12041
+ VALUES (?, ?, ?, ?, ?, 'pending')
12042
+ `).run(opts.runId, opts.taskId, opts.taskText, opts.file, opts.line);
12043
+ }
11974
12044
  function updateTaskStatus(runId, taskId, status, opts) {
11975
12045
  const now = Date.now();
11976
12046
  const isTerminal = status === "success" || status === "failed" || status === "skipped";
@@ -12035,9 +12105,114 @@ var init_manager = __esm({
12035
12105
  }
12036
12106
  });
12037
12107
 
12108
+ // src/mcp/tools/_fork-run.ts
12109
+ import { fork } from "child_process";
12110
+ import { fileURLToPath as fileURLToPath2 } from "url";
12111
+ import { dirname as dirname6, join as join16 } from "path";
12112
+ function forkDispatchRun(runId, server, workerMessage, options) {
12113
+ wireRunLogs(runId, server);
12114
+ const worker = fork(WORKER_PATH, [], { stdio: ["pipe", "pipe", "pipe", "ipc"] });
12115
+ worker.send(workerMessage);
12116
+ const heartbeat = setInterval(() => {
12117
+ emitLog(runId, `Run ${runId} still in progress...`);
12118
+ }, HEARTBEAT_INTERVAL_MS);
12119
+ worker.on("message", (msg) => {
12120
+ const msgType = msg["type"];
12121
+ switch (msgType) {
12122
+ case "progress": {
12123
+ const event = msg["event"];
12124
+ const eventType = event["type"];
12125
+ switch (eventType) {
12126
+ case "task_start":
12127
+ createTask({
12128
+ runId,
12129
+ taskId: event["taskId"],
12130
+ taskText: event["taskText"],
12131
+ file: event["file"] ?? event["taskId"].split(":")[0] ?? "",
12132
+ line: event["line"] ?? parseInt(event["taskId"].split(":")[1] ?? "0", 10)
12133
+ });
12134
+ updateTaskStatus(runId, event["taskId"], "running");
12135
+ emitLog(runId, `Task started: ${event["taskText"]}`);
12136
+ break;
12137
+ case "task_done":
12138
+ updateTaskStatus(runId, event["taskId"], "success");
12139
+ emitLog(runId, `Task done: ${event["taskText"]}`);
12140
+ break;
12141
+ case "task_failed":
12142
+ updateTaskStatus(runId, event["taskId"], "failed", { error: event["error"] });
12143
+ emitLog(runId, `Task failed: ${event["taskText"]} \u2014 ${event["error"]}`, "error");
12144
+ break;
12145
+ case "phase_change":
12146
+ emitLog(runId, event["message"] ?? `Phase: ${event["phase"]}`);
12147
+ break;
12148
+ case "log":
12149
+ emitLog(runId, event["message"]);
12150
+ break;
12151
+ }
12152
+ break;
12153
+ }
12154
+ case "spec_progress": {
12155
+ const event = msg["event"];
12156
+ const eventType = event["type"];
12157
+ switch (eventType) {
12158
+ case "item_start":
12159
+ emitLog(runId, `Generating spec for: ${event["itemTitle"] ?? event["itemId"]}`);
12160
+ break;
12161
+ case "item_done":
12162
+ emitLog(runId, `Spec done: ${event["itemTitle"] ?? event["itemId"]}`);
12163
+ break;
12164
+ case "item_failed":
12165
+ emitLog(runId, `Spec failed: ${event["itemTitle"] ?? event["itemId"]} \u2014 ${event["error"]}`, "error");
12166
+ break;
12167
+ case "log":
12168
+ emitLog(runId, event["message"]);
12169
+ break;
12170
+ }
12171
+ break;
12172
+ }
12173
+ case "done": {
12174
+ const result = msg["result"];
12175
+ if (options?.onDone) {
12176
+ options.onDone(result);
12177
+ } else if ("completed" in result) {
12178
+ updateRunCounters(runId, result["total"], result["completed"], result["failed"]);
12179
+ finishRun(runId, result["failed"] > 0 ? "failed" : "completed");
12180
+ emitLog(runId, `Dispatch complete: ${result["completed"]}/${result["total"]} tasks succeeded`);
12181
+ }
12182
+ break;
12183
+ }
12184
+ case "error": {
12185
+ finishRun(runId, "failed", msg["message"]);
12186
+ emitLog(runId, `Run error: ${msg["message"]}`, "error");
12187
+ break;
12188
+ }
12189
+ }
12190
+ });
12191
+ worker.on("exit", (code) => {
12192
+ clearInterval(heartbeat);
12193
+ if (code !== 0 && code !== null) {
12194
+ finishRun(runId, "failed", `Worker process exited with code ${code}`);
12195
+ emitLog(runId, `Worker process exited unexpectedly (code ${code})`, "error");
12196
+ }
12197
+ });
12198
+ return worker;
12199
+ }
12200
+ var __filename, __dirname, WORKER_PATH, HEARTBEAT_INTERVAL_MS;
12201
+ var init_fork_run = __esm({
12202
+ "src/mcp/tools/_fork-run.ts"() {
12203
+ "use strict";
12204
+ init_manager();
12205
+ init_server();
12206
+ __filename = fileURLToPath2(import.meta.url);
12207
+ __dirname = dirname6(__filename);
12208
+ WORKER_PATH = join16(__dirname, "..", "dispatch-worker.js");
12209
+ HEARTBEAT_INTERVAL_MS = 3e4;
12210
+ }
12211
+ });
12212
+
12038
12213
  // src/mcp/tools/spec.ts
12039
12214
  import { z as z2 } from "zod";
12040
- import { join as join16, resolve as resolve4, sep as sep2 } from "path";
12215
+ import { join as join17, resolve as resolve4, sep as sep2 } from "path";
12041
12216
  import { readdir as readdir2, readFile as readFile11 } from "fs/promises";
12042
12217
  function registerSpecTools(server, cwd) {
12043
12218
  server.tool(
@@ -12054,50 +12229,27 @@ function registerSpecTools(server, cwd) {
12054
12229
  },
12055
12230
  async (args) => {
12056
12231
  const runId = createSpecRun({ cwd, issues: args.issues });
12057
- setImmediate(() => {
12058
- void (async () => {
12059
- try {
12060
- emitLog(runId, `Starting spec generation for: ${args.issues}`);
12061
- const result = await runSpecPipeline({
12062
- issues: args.issues,
12063
- provider: args.provider ?? "opencode",
12064
- issueSource: args.source,
12065
- concurrency: args.concurrency,
12066
- dryRun: args.dryRun,
12067
- cwd,
12068
- progressCallback: (event) => {
12069
- switch (event.type) {
12070
- case "item_start":
12071
- emitLog(runId, `Generating spec for: ${event.itemTitle ?? event.itemId}`);
12072
- break;
12073
- case "item_done":
12074
- emitLog(runId, `Spec done: ${event.itemTitle ?? event.itemId}`);
12075
- break;
12076
- case "item_failed":
12077
- emitLog(runId, `Spec failed: ${event.itemTitle ?? event.itemId} \u2014 ${event.error}`, "error");
12078
- break;
12079
- case "log":
12080
- emitLog(runId, event.message);
12081
- break;
12082
- default: {
12083
- const _exhaustive = event;
12084
- void _exhaustive;
12085
- }
12086
- }
12087
- }
12088
- });
12232
+ forkDispatchRun(runId, server, {
12233
+ type: "spec",
12234
+ cwd,
12235
+ opts: {
12236
+ issues: args.issues,
12237
+ provider: args.provider ?? "opencode",
12238
+ issueSource: args.source,
12239
+ concurrency: args.concurrency,
12240
+ dryRun: args.dryRun,
12241
+ cwd
12242
+ }
12243
+ }, {
12244
+ onDone: (result) => {
12245
+ if ("generated" in result) {
12089
12246
  finishSpecRun(runId, "completed", {
12090
- total: result.total,
12091
- generated: result.generated,
12092
- failed: result.failed
12247
+ total: result["total"],
12248
+ generated: result["generated"],
12249
+ failed: result["failed"]
12093
12250
  });
12094
- emitLog(runId, `Spec generation complete: ${result.generated} generated, ${result.failed} failed`);
12095
- } catch (err) {
12096
- const msg = err instanceof Error ? err.message : String(err);
12097
- finishSpecRun(runId, "failed", { total: 0, generated: 0, failed: 0 }, msg);
12098
- emitLog(runId, `Spec generation error: ${msg}`, "error");
12099
12251
  }
12100
- })();
12252
+ }
12101
12253
  });
12102
12254
  return {
12103
12255
  content: [{ type: "text", text: JSON.stringify({ runId, status: "running" }) }]
@@ -12109,15 +12261,25 @@ function registerSpecTools(server, cwd) {
12109
12261
  "List spec files in the .dispatch/specs directory.",
12110
12262
  {},
12111
12263
  async () => {
12112
- const specsDir = join16(cwd, ".dispatch", "specs");
12264
+ const specsDir = join17(cwd, ".dispatch", "specs");
12113
12265
  let files = [];
12266
+ let dirError;
12114
12267
  try {
12115
12268
  const entries = await readdir2(specsDir);
12116
12269
  files = entries.filter((f) => f.endsWith(".md")).sort();
12270
+ } catch (err) {
12271
+ const isNotFound = err instanceof Error && "code" in err && err.code === "ENOENT";
12272
+ if (!isNotFound) {
12273
+ dirError = `Error reading specs directory: ${err instanceof Error ? err.message : String(err)}`;
12274
+ }
12275
+ }
12276
+ let recentRuns = [];
12277
+ try {
12278
+ recentRuns = listSpecRuns(5);
12117
12279
  } catch {
12118
12280
  }
12119
12281
  return {
12120
- content: [{ type: "text", text: JSON.stringify({ files, specsDir }) }]
12282
+ content: [{ type: "text", text: JSON.stringify({ files, specsDir, recentRuns, ...dirError ? { error: dirError } : {} }) }]
12121
12283
  };
12122
12284
  }
12123
12285
  );
@@ -12129,7 +12291,7 @@ function registerSpecTools(server, cwd) {
12129
12291
  },
12130
12292
  async (args) => {
12131
12293
  const specsDir = resolve4(cwd, ".dispatch", "specs");
12132
- const candidatePath = args.file.includes("/") || args.file.includes("\\") ? resolve4(specsDir, args.file) : join16(specsDir, args.file);
12294
+ const candidatePath = args.file.includes("/") || args.file.includes("\\") ? resolve4(specsDir, args.file) : join17(specsDir, args.file);
12133
12295
  if (!candidatePath.startsWith(specsDir + sep2) && candidatePath !== specsDir) {
12134
12296
  return {
12135
12297
  content: [{ type: "text", text: `Access denied: path must be inside the specs directory` }],
@@ -12158,10 +12320,17 @@ function registerSpecTools(server, cwd) {
12158
12320
  limit: z2.number().int().min(1).max(100).optional().describe("Max results (default 20)")
12159
12321
  },
12160
12322
  async (args) => {
12161
- const runs = listSpecRuns(args.limit ?? 20);
12162
- return {
12163
- content: [{ type: "text", text: JSON.stringify(runs) }]
12164
- };
12323
+ try {
12324
+ const runs = listSpecRuns(args.limit ?? 20);
12325
+ return {
12326
+ content: [{ type: "text", text: JSON.stringify(runs) }]
12327
+ };
12328
+ } catch (err) {
12329
+ return {
12330
+ content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }],
12331
+ isError: true
12332
+ };
12333
+ }
12165
12334
  }
12166
12335
  );
12167
12336
  server.tool(
@@ -12171,26 +12340,33 @@ function registerSpecTools(server, cwd) {
12171
12340
  runId: z2.string().describe("The runId returned by spec_generate")
12172
12341
  },
12173
12342
  async (args) => {
12174
- const run = getSpecRun(args.runId);
12175
- if (!run) {
12343
+ try {
12344
+ const run = getSpecRun(args.runId);
12345
+ if (!run) {
12346
+ return {
12347
+ content: [{ type: "text", text: `Run ${args.runId} not found` }],
12348
+ isError: true
12349
+ };
12350
+ }
12176
12351
  return {
12177
- content: [{ type: "text", text: `Run ${args.runId} not found` }],
12352
+ content: [{ type: "text", text: JSON.stringify(run) }]
12353
+ };
12354
+ } catch (err) {
12355
+ return {
12356
+ content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }],
12178
12357
  isError: true
12179
12358
  };
12180
12359
  }
12181
- return {
12182
- content: [{ type: "text", text: JSON.stringify(run) }]
12183
- };
12184
12360
  }
12185
12361
  );
12186
12362
  }
12187
12363
  var init_spec2 = __esm({
12188
12364
  "src/mcp/tools/spec.ts"() {
12189
12365
  "use strict";
12190
- init_spec_pipeline();
12191
12366
  init_manager();
12192
12367
  init_interface2();
12193
12368
  init_interface();
12369
+ init_fork_run();
12194
12370
  }
12195
12371
  });
12196
12372
 
@@ -12212,64 +12388,20 @@ function registerDispatchTools(server, cwd) {
12212
12388
  },
12213
12389
  async (args) => {
12214
12390
  const runId = createRun({ cwd, issueIds: args.issueIds });
12215
- setImmediate(() => {
12216
- void (async () => {
12217
- try {
12218
- const orchestrator = await boot9({ cwd });
12219
- emitLog(runId, `Starting dispatch for issues: ${args.issueIds.join(", ")}`);
12220
- const result = await orchestrator.orchestrate({
12221
- issueIds: args.issueIds,
12222
- dryRun: false,
12223
- provider: args.provider ?? "opencode",
12224
- source: args.source,
12225
- concurrency: args.concurrency ?? 1,
12226
- noPlan: args.noPlan,
12227
- noBranch: args.noBranch,
12228
- noWorktree: args.noWorktree,
12229
- retries: args.retries,
12230
- progressCallback: (event) => {
12231
- switch (event.type) {
12232
- case "task_start":
12233
- emitLog(runId, `Task started: ${event.taskText}`);
12234
- updateTaskStatus(runId, event.taskId, "running");
12235
- break;
12236
- case "task_done":
12237
- emitLog(runId, `Task done: ${event.taskText}`);
12238
- updateTaskStatus(runId, event.taskId, "success");
12239
- break;
12240
- case "task_failed":
12241
- emitLog(runId, `Task failed: ${event.taskText} \u2014 ${event.error}`, "error");
12242
- updateTaskStatus(runId, event.taskId, "failed", { error: event.error });
12243
- break;
12244
- case "phase_change":
12245
- emitLog(runId, event.message ?? `Phase: ${event.phase}`);
12246
- break;
12247
- case "log":
12248
- emitLog(runId, event.message);
12249
- break;
12250
- default: {
12251
- const _exhaustive = event;
12252
- void _exhaustive;
12253
- }
12254
- }
12255
- updateRunCounters(
12256
- runId,
12257
- 0,
12258
- // we'll update with final counts at the end
12259
- 0,
12260
- 0
12261
- );
12262
- }
12263
- });
12264
- updateRunCounters(runId, result.total, result.completed, result.failed);
12265
- finishRun(runId, result.failed > 0 ? "failed" : "completed");
12266
- emitLog(runId, `Dispatch complete: ${result.completed}/${result.total} tasks succeeded`);
12267
- } catch (err) {
12268
- const msg = err instanceof Error ? err.message : String(err);
12269
- finishRun(runId, "failed", msg);
12270
- emitLog(runId, `Dispatch error: ${msg}`, "error");
12271
- }
12272
- })();
12391
+ forkDispatchRun(runId, server, {
12392
+ type: "dispatch",
12393
+ cwd,
12394
+ opts: {
12395
+ issueIds: args.issueIds,
12396
+ dryRun: false,
12397
+ provider: args.provider ?? "opencode",
12398
+ source: args.source,
12399
+ concurrency: args.concurrency ?? 1,
12400
+ noPlan: args.noPlan,
12401
+ noBranch: args.noBranch,
12402
+ noWorktree: args.noWorktree,
12403
+ retries: args.retries
12404
+ }
12273
12405
  });
12274
12406
  return {
12275
12407
  content: [{ type: "text", text: JSON.stringify({ runId, status: "running" }) }]
@@ -12310,12 +12442,13 @@ var init_dispatch = __esm({
12310
12442
  init_manager();
12311
12443
  init_interface2();
12312
12444
  init_interface();
12445
+ init_fork_run();
12313
12446
  }
12314
12447
  });
12315
12448
 
12316
12449
  // src/mcp/tools/monitor.ts
12317
12450
  import { z as z4 } from "zod";
12318
- import { join as join17 } from "path";
12451
+ import { join as join18 } from "path";
12319
12452
  function registerMonitorTools(server, cwd) {
12320
12453
  server.tool(
12321
12454
  "status_get",
@@ -12324,17 +12457,24 @@ function registerMonitorTools(server, cwd) {
12324
12457
  runId: z4.string().describe("The runId returned by dispatch_run or spec_generate")
12325
12458
  },
12326
12459
  async (args) => {
12327
- const run = getRun(args.runId);
12328
- if (!run) {
12460
+ try {
12461
+ const run = getRun(args.runId);
12462
+ if (!run) {
12463
+ return {
12464
+ content: [{ type: "text", text: `Run ${args.runId} not found` }],
12465
+ isError: true
12466
+ };
12467
+ }
12468
+ const tasks = getTasksForRun(args.runId);
12329
12469
  return {
12330
- content: [{ type: "text", text: `Run ${args.runId} not found` }],
12470
+ content: [{ type: "text", text: JSON.stringify({ run, tasks }) }]
12471
+ };
12472
+ } catch (err) {
12473
+ return {
12474
+ content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }],
12331
12475
  isError: true
12332
12476
  };
12333
12477
  }
12334
- const tasks = getTasksForRun(args.runId);
12335
- return {
12336
- content: [{ type: "text", text: JSON.stringify({ run, tasks }) }]
12337
- };
12338
12478
  }
12339
12479
  );
12340
12480
  server.tool(
@@ -12345,10 +12485,17 @@ function registerMonitorTools(server, cwd) {
12345
12485
  limit: z4.number().int().min(1).max(100).optional().describe("Max results (default 20)")
12346
12486
  },
12347
12487
  async (args) => {
12348
- const runs = args.status ? listRunsByStatus(args.status, args.limit ?? 20) : listRuns(args.limit ?? 20);
12349
- return {
12350
- content: [{ type: "text", text: JSON.stringify(runs) }]
12351
- };
12488
+ try {
12489
+ const runs = args.status ? listRunsByStatus(args.status, args.limit ?? 20) : listRuns(args.limit ?? 20);
12490
+ return {
12491
+ content: [{ type: "text", text: JSON.stringify(runs) }]
12492
+ };
12493
+ } catch (err) {
12494
+ return {
12495
+ content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }],
12496
+ isError: true
12497
+ };
12498
+ }
12352
12499
  }
12353
12500
  );
12354
12501
  server.tool(
@@ -12364,7 +12511,7 @@ function registerMonitorTools(server, cwd) {
12364
12511
  },
12365
12512
  async (args) => {
12366
12513
  try {
12367
- const config = await loadConfig(join17(cwd, ".dispatch"));
12514
+ const config = await loadConfig(join18(cwd, ".dispatch"));
12368
12515
  const sourceName = args.source ?? config.source;
12369
12516
  if (!sourceName) {
12370
12517
  return {
@@ -12409,7 +12556,7 @@ function registerMonitorTools(server, cwd) {
12409
12556
  },
12410
12557
  async (args) => {
12411
12558
  try {
12412
- const config = await loadConfig(join17(cwd, ".dispatch"));
12559
+ const config = await loadConfig(join18(cwd, ".dispatch"));
12413
12560
  const sourceName = args.source ?? config.source;
12414
12561
  if (!sourceName) {
12415
12562
  return {
@@ -12457,7 +12604,7 @@ var init_monitor = __esm({
12457
12604
 
12458
12605
  // src/mcp/tools/recovery.ts
12459
12606
  import { z as z5 } from "zod";
12460
- import { join as join18 } from "path";
12607
+ import { join as join19 } from "path";
12461
12608
  function registerRecoveryTools(server, cwd) {
12462
12609
  server.tool(
12463
12610
  "run_retry",
@@ -12477,63 +12624,25 @@ function registerRecoveryTools(server, cwd) {
12477
12624
  }
12478
12625
  const tasks = getTasksForRun(args.runId);
12479
12626
  const failedTasks = tasks.filter((t) => t.status === "failed");
12480
- if (failedTasks.length === 0) {
12627
+ if (failedTasks.length === 0 && originalRun.status !== "failed") {
12481
12628
  return {
12482
12629
  content: [{ type: "text", text: JSON.stringify({ message: "No failed tasks found", originalRunId: args.runId }) }]
12483
12630
  };
12484
12631
  }
12485
- const config = await loadConfig(join18(cwd, ".dispatch"));
12632
+ const config = await loadConfig(join19(cwd, ".dispatch"));
12486
12633
  const issueIds = issueIdsSchema.parse(JSON.parse(originalRun.issueIds));
12487
12634
  const newRunId = createRun({ cwd, issueIds });
12488
- setImmediate(() => {
12489
- void (async () => {
12490
- try {
12491
- const orchestrator = await boot9({ cwd });
12492
- emitLog(newRunId, `Retrying ${failedTasks.length} failed task(s) from run ${args.runId}`);
12493
- const result = await orchestrator.orchestrate({
12494
- issueIds,
12495
- dryRun: false,
12496
- provider: args.provider ?? config.provider ?? "opencode",
12497
- source: config.source,
12498
- concurrency: args.concurrency ?? config.concurrency ?? 1,
12499
- force: true,
12500
- // re-run even previously completed tasks? No — force just skips run-state check
12501
- progressCallback: (event) => {
12502
- switch (event.type) {
12503
- case "task_start":
12504
- emitLog(newRunId, `Task started: ${event.taskText}`);
12505
- updateTaskStatus(newRunId, event.taskId, "running");
12506
- break;
12507
- case "task_done":
12508
- emitLog(newRunId, `Task done: ${event.taskText}`);
12509
- updateTaskStatus(newRunId, event.taskId, "success");
12510
- break;
12511
- case "task_failed":
12512
- emitLog(newRunId, `Task failed: ${event.taskText} \u2014 ${event.error}`, "error");
12513
- updateTaskStatus(newRunId, event.taskId, "failed", { error: event.error });
12514
- break;
12515
- case "phase_change":
12516
- emitLog(newRunId, event.message ?? `Phase: ${event.phase}`);
12517
- break;
12518
- case "log":
12519
- emitLog(newRunId, event.message);
12520
- break;
12521
- default: {
12522
- const _exhaustive = event;
12523
- void _exhaustive;
12524
- }
12525
- }
12526
- }
12527
- });
12528
- updateRunCounters(newRunId, result.total, result.completed, result.failed);
12529
- finishRun(newRunId, result.failed > 0 ? "failed" : "completed");
12530
- emitLog(newRunId, `Retry complete: ${result.completed}/${result.total} succeeded`);
12531
- } catch (err) {
12532
- const msg = err instanceof Error ? err.message : String(err);
12533
- finishRun(newRunId, "failed", msg);
12534
- emitLog(newRunId, `Retry error: ${msg}`, "error");
12535
- }
12536
- })();
12635
+ forkDispatchRun(newRunId, server, {
12636
+ type: "dispatch",
12637
+ cwd,
12638
+ opts: {
12639
+ issueIds,
12640
+ dryRun: false,
12641
+ provider: args.provider ?? config.provider ?? "opencode",
12642
+ source: config.source,
12643
+ concurrency: args.concurrency ?? config.concurrency ?? 1,
12644
+ force: true
12645
+ }
12537
12646
  });
12538
12647
  return {
12539
12648
  content: [{ type: "text", text: JSON.stringify({ runId: newRunId, status: "running", originalRunId: args.runId }) }]
@@ -12564,54 +12673,20 @@ function registerRecoveryTools(server, cwd) {
12564
12673
  isError: true
12565
12674
  };
12566
12675
  }
12567
- const config = await loadConfig(join18(cwd, ".dispatch"));
12676
+ const config = await loadConfig(join19(cwd, ".dispatch"));
12568
12677
  const issueIds = issueIdsSchema.parse(JSON.parse(originalRun.issueIds));
12569
12678
  const newRunId = createRun({ cwd, issueIds });
12570
- setImmediate(() => {
12571
- void (async () => {
12572
- try {
12573
- const orchestrator = await boot9({ cwd });
12574
- emitLog(newRunId, `Retrying task: ${task.taskText}`);
12575
- const result = await orchestrator.orchestrate({
12576
- issueIds,
12577
- dryRun: false,
12578
- provider: args.provider ?? config.provider ?? "opencode",
12579
- source: config.source,
12580
- concurrency: 1,
12581
- force: true,
12582
- progressCallback: (event) => {
12583
- switch (event.type) {
12584
- case "task_start":
12585
- emitLog(newRunId, `Task started: ${event.taskText}`);
12586
- break;
12587
- case "task_done":
12588
- emitLog(newRunId, `Task done: ${event.taskText}`);
12589
- break;
12590
- case "task_failed":
12591
- emitLog(newRunId, `Task failed: ${event.taskText} \u2014 ${event.error}`, "error");
12592
- break;
12593
- case "phase_change":
12594
- emitLog(newRunId, event.message ?? `Phase: ${event.phase}`);
12595
- break;
12596
- case "log":
12597
- emitLog(newRunId, event.message);
12598
- break;
12599
- default: {
12600
- const _exhaustive = event;
12601
- void _exhaustive;
12602
- }
12603
- }
12604
- }
12605
- });
12606
- updateRunCounters(newRunId, result.total, result.completed, result.failed);
12607
- finishRun(newRunId, result.failed > 0 ? "failed" : "completed");
12608
- emitLog(newRunId, `Task retry complete`);
12609
- } catch (err) {
12610
- const msg = err instanceof Error ? err.message : String(err);
12611
- finishRun(newRunId, "failed", msg);
12612
- emitLog(newRunId, `Task retry error: ${msg}`, "error");
12613
- }
12614
- })();
12679
+ forkDispatchRun(newRunId, server, {
12680
+ type: "dispatch",
12681
+ cwd,
12682
+ opts: {
12683
+ issueIds,
12684
+ dryRun: false,
12685
+ provider: args.provider ?? config.provider ?? "opencode",
12686
+ source: config.source,
12687
+ concurrency: 1,
12688
+ force: true
12689
+ }
12615
12690
  });
12616
12691
  return {
12617
12692
  content: [{ type: "text", text: JSON.stringify({ runId: newRunId, status: "running", taskId: args.taskId }) }]
@@ -12624,22 +12699,22 @@ var init_recovery = __esm({
12624
12699
  "src/mcp/tools/recovery.ts"() {
12625
12700
  "use strict";
12626
12701
  init_manager();
12627
- init_runner();
12628
12702
  init_config();
12629
12703
  init_interface2();
12704
+ init_fork_run();
12630
12705
  issueIdsSchema = z5.array(z5.string());
12631
12706
  }
12632
12707
  });
12633
12708
 
12634
12709
  // src/mcp/tools/config.ts
12635
- import { join as join19 } from "path";
12710
+ import { join as join20 } from "path";
12636
12711
  function registerConfigTools(server, cwd) {
12637
12712
  server.tool(
12638
12713
  "config_get",
12639
12714
  "Get the current Dispatch configuration from .dispatch/config.json.",
12640
12715
  {},
12641
12716
  async () => {
12642
- const config = await loadConfig(join19(cwd, ".dispatch"));
12717
+ const config = await loadConfig(join20(cwd, ".dispatch"));
12643
12718
  const { nextIssueId: _, ...safeConfig } = config;
12644
12719
  return {
12645
12720
  content: [{ type: "text", text: JSON.stringify(safeConfig) }]
@@ -12798,6 +12873,17 @@ async function createMcpServer(opts) {
12798
12873
  }
12799
12874
  };
12800
12875
  }
12876
+ function wireRunLogs(runId, server) {
12877
+ addLogCallback(runId, (message, level) => {
12878
+ server.sendLoggingMessage({
12879
+ level: level === "error" ? "error" : level === "warn" ? "warning" : "info",
12880
+ logger: `dispatch.run.${runId}`,
12881
+ data: message
12882
+ }).catch((err) => {
12883
+ console.error("[dispatch-mcp] sendLoggingMessage error:", err);
12884
+ });
12885
+ });
12886
+ }
12801
12887
  var init_server = __esm({
12802
12888
  "src/mcp/server.ts"() {
12803
12889
  "use strict";
@@ -12881,7 +12967,7 @@ init_cleanup();
12881
12967
  init_providers();
12882
12968
  init_datasources();
12883
12969
  init_config();
12884
- import { resolve as resolve5, join as join20 } from "path";
12970
+ import { resolve as resolve5, join as join21 } from "path";
12885
12971
  import { Command, Option, CommanderError } from "commander";
12886
12972
  var MAX_CONCURRENCY = CONFIG_BOUNDS.concurrency.max;
12887
12973
  var HELP = `
@@ -13174,7 +13260,7 @@ async function main() {
13174
13260
  }
13175
13261
  throw err;
13176
13262
  }
13177
- const configDir = join20(configProgram.opts().cwd ?? process.cwd(), ".dispatch");
13263
+ const configDir = join21(configProgram.opts().cwd ?? process.cwd(), ".dispatch");
13178
13264
  await handleConfigCommand(rawArgv.slice(1), configDir);
13179
13265
  process.exit(0);
13180
13266
  }
@@ -13219,7 +13305,7 @@ async function main() {
13219
13305
  process.exit(0);
13220
13306
  }
13221
13307
  if (args.version) {
13222
- console.log(`dispatch v${"1.5.0-beta.236e011"}`);
13308
+ console.log(`dispatch v${"1.5.0-beta.30f06ab"}`);
13223
13309
  process.exit(0);
13224
13310
  }
13225
13311
  const orchestrator = await boot9({ cwd: args.cwd });