@pruddiman/dispatch 1.5.0-beta.b8296d7 → 1.5.0-beta.c0bcc82

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 {
@@ -11862,11 +11920,26 @@ var init_database = __esm({
11862
11920
  // src/mcp/state/manager.ts
11863
11921
  import { randomUUID as randomUUID6 } from "crypto";
11864
11922
  function registerLiveRun(runId) {
11865
- liveRuns.set(runId, { runId, callbacks: [] });
11923
+ liveRuns.set(runId, { runId, callbacks: [], completionCallbacks: [] });
11866
11924
  }
11867
11925
  function unregisterLiveRun(runId) {
11926
+ const run = liveRuns.get(runId);
11927
+ if (run) {
11928
+ for (const cb of run.completionCallbacks) {
11929
+ try {
11930
+ cb();
11931
+ } catch {
11932
+ }
11933
+ }
11934
+ }
11868
11935
  liveRuns.delete(runId);
11869
11936
  }
11937
+ function addLogCallback(runId, cb) {
11938
+ const run = liveRuns.get(runId);
11939
+ if (run) {
11940
+ run.callbacks.push(cb);
11941
+ }
11942
+ }
11870
11943
  function emitLog(runId, message, level = "info") {
11871
11944
  const run = liveRuns.get(runId);
11872
11945
  if (run) {
@@ -11879,6 +11952,48 @@ function emitLog(runId, message, level = "info") {
11879
11952
  }
11880
11953
  }
11881
11954
  }
11955
+ function isLiveRun(runId) {
11956
+ return liveRuns.has(runId);
11957
+ }
11958
+ function addCompletionCallback(runId, cb) {
11959
+ const run = liveRuns.get(runId);
11960
+ if (run) {
11961
+ run.completionCallbacks.push(cb);
11962
+ }
11963
+ }
11964
+ function waitForRunCompletion(runId, waitMs, getStatus) {
11965
+ const effectiveWait = Math.min(Math.max(waitMs, 0), 12e4);
11966
+ if (effectiveWait <= 0) return Promise.resolve(false);
11967
+ const currentStatus = getStatus();
11968
+ if (currentStatus !== null && currentStatus !== "running") {
11969
+ return Promise.resolve(true);
11970
+ }
11971
+ return new Promise((resolve6) => {
11972
+ let settled = false;
11973
+ let pollTimer;
11974
+ let timeoutTimer;
11975
+ const cleanup = () => {
11976
+ if (pollTimer) clearInterval(pollTimer);
11977
+ if (timeoutTimer) clearTimeout(timeoutTimer);
11978
+ };
11979
+ const settle = (completed) => {
11980
+ if (settled) return;
11981
+ settled = true;
11982
+ cleanup();
11983
+ resolve6(completed);
11984
+ };
11985
+ if (isLiveRun(runId)) {
11986
+ addCompletionCallback(runId, () => settle(true));
11987
+ }
11988
+ pollTimer = setInterval(() => {
11989
+ const s = getStatus();
11990
+ if (s !== null && s !== "running") {
11991
+ settle(true);
11992
+ }
11993
+ }, 2e3);
11994
+ timeoutTimer = setTimeout(() => settle(false), effectiveWait);
11995
+ });
11996
+ }
11882
11997
  function assertRunStatus(value) {
11883
11998
  if (RUN_STATUSES.includes(value)) return value;
11884
11999
  throw new Error(`Invalid RunStatus from database: "${value}"`);
@@ -11971,6 +12086,12 @@ function listRunsByStatus(status, limit = 20) {
11971
12086
  ).all(status, limit);
11972
12087
  return rows.map(rowToRun);
11973
12088
  }
12089
+ function createTask(opts) {
12090
+ getDb().prepare(`
12091
+ INSERT INTO tasks (run_id, task_id, task_text, file, line, status)
12092
+ VALUES (?, ?, ?, ?, ?, 'pending')
12093
+ `).run(opts.runId, opts.taskId, opts.taskText, opts.file, opts.line);
12094
+ }
11974
12095
  function updateTaskStatus(runId, taskId, status, opts) {
11975
12096
  const now = Date.now();
11976
12097
  const isTerminal = status === "success" || status === "failed" || status === "skipped";
@@ -12035,9 +12156,141 @@ var init_manager = __esm({
12035
12156
  }
12036
12157
  });
12037
12158
 
12159
+ // src/mcp/tools/_fork-run.ts
12160
+ import { fork } from "child_process";
12161
+ import { fileURLToPath as fileURLToPath2 } from "url";
12162
+ import { dirname as dirname6, join as join16 } from "path";
12163
+ function forkDispatchRun(runId, server, workerMessage, options) {
12164
+ wireRunLogs(runId, server);
12165
+ const worker = fork(WORKER_PATH, [], { stdio: ["pipe", "pipe", "pipe", "ipc"] });
12166
+ worker.send(workerMessage);
12167
+ const heartbeat = setInterval(() => {
12168
+ emitLog(runId, `Run ${runId} still in progress...`);
12169
+ }, HEARTBEAT_INTERVAL_MS);
12170
+ worker.on("message", (msg) => {
12171
+ const msgType = msg["type"];
12172
+ switch (msgType) {
12173
+ case "progress": {
12174
+ const event = msg["event"];
12175
+ const eventType = event["type"];
12176
+ switch (eventType) {
12177
+ case "task_start":
12178
+ createTask({
12179
+ runId,
12180
+ taskId: event["taskId"],
12181
+ taskText: event["taskText"],
12182
+ file: event["file"] ?? event["taskId"].split(":")[0] ?? "",
12183
+ line: event["line"] ?? parseInt(event["taskId"].split(":")[1] ?? "0", 10)
12184
+ });
12185
+ updateTaskStatus(runId, event["taskId"], "running");
12186
+ emitLog(runId, `Task started: ${event["taskText"]}`);
12187
+ break;
12188
+ case "task_done":
12189
+ updateTaskStatus(runId, event["taskId"], "success");
12190
+ emitLog(runId, `Task done: ${event["taskText"]}`);
12191
+ break;
12192
+ case "task_failed":
12193
+ updateTaskStatus(runId, event["taskId"], "failed", { error: event["error"] });
12194
+ emitLog(runId, `Task failed: ${event["taskText"]} \u2014 ${event["error"]}`, "error");
12195
+ break;
12196
+ case "phase_change":
12197
+ emitLog(runId, event["message"] ?? `Phase: ${event["phase"]}`);
12198
+ break;
12199
+ case "log":
12200
+ emitLog(runId, event["message"]);
12201
+ break;
12202
+ }
12203
+ break;
12204
+ }
12205
+ case "spec_progress": {
12206
+ const event = msg["event"];
12207
+ const eventType = event["type"];
12208
+ switch (eventType) {
12209
+ case "item_start":
12210
+ emitLog(runId, `Generating spec for: ${event["itemTitle"] ?? event["itemId"]}`);
12211
+ break;
12212
+ case "item_done":
12213
+ emitLog(runId, `Spec done: ${event["itemTitle"] ?? event["itemId"]}`);
12214
+ break;
12215
+ case "item_failed":
12216
+ emitLog(runId, `Spec failed: ${event["itemTitle"] ?? event["itemId"]} \u2014 ${event["error"]}`, "error");
12217
+ break;
12218
+ case "log":
12219
+ emitLog(runId, event["message"]);
12220
+ break;
12221
+ }
12222
+ break;
12223
+ }
12224
+ case "done": {
12225
+ const result = msg["result"];
12226
+ if (options?.onDone) {
12227
+ options.onDone(result);
12228
+ } else if ("completed" in result) {
12229
+ updateRunCounters(runId, result["total"], result["completed"], result["failed"]);
12230
+ finishRun(runId, result["failed"] > 0 ? "failed" : "completed");
12231
+ emitLog(runId, `Dispatch complete: ${result["completed"]}/${result["total"]} tasks succeeded`);
12232
+ }
12233
+ break;
12234
+ }
12235
+ case "error": {
12236
+ finishRun(runId, "failed", msg["message"]);
12237
+ emitLog(runId, `Run error: ${msg["message"]}`, "error");
12238
+ break;
12239
+ }
12240
+ }
12241
+ });
12242
+ worker.on("exit", (code) => {
12243
+ clearInterval(heartbeat);
12244
+ if (code !== 0 && code !== null) {
12245
+ finishRun(runId, "failed", `Worker process exited with code ${code}`);
12246
+ emitLog(runId, `Worker process exited unexpectedly (code ${code})`, "error");
12247
+ }
12248
+ });
12249
+ return worker;
12250
+ }
12251
+ var __filename, __dirname, WORKER_PATH, HEARTBEAT_INTERVAL_MS;
12252
+ var init_fork_run = __esm({
12253
+ "src/mcp/tools/_fork-run.ts"() {
12254
+ "use strict";
12255
+ init_manager();
12256
+ init_server();
12257
+ __filename = fileURLToPath2(import.meta.url);
12258
+ __dirname = dirname6(__filename);
12259
+ WORKER_PATH = join16(__dirname, "..", "dispatch-worker.js");
12260
+ HEARTBEAT_INTERVAL_MS = 3e4;
12261
+ }
12262
+ });
12263
+
12264
+ // src/mcp/tools/_resolve-config.ts
12265
+ import { join as join17 } from "path";
12266
+ async function loadMcpConfig(cwd, overrides) {
12267
+ const config = await loadConfig(join17(cwd, ".dispatch"));
12268
+ const provider = overrides?.provider ?? config.provider;
12269
+ if (!provider) {
12270
+ throw new Error(
12271
+ "Missing required configuration: provider. Run 'dispatch config' to set up defaults."
12272
+ );
12273
+ }
12274
+ let source = overrides?.source ?? config.source;
12275
+ if (!source) {
12276
+ const detected = await detectDatasource(cwd);
12277
+ if (detected) {
12278
+ source = detected;
12279
+ }
12280
+ }
12281
+ return { ...config, provider, source };
12282
+ }
12283
+ var init_resolve_config = __esm({
12284
+ "src/mcp/tools/_resolve-config.ts"() {
12285
+ "use strict";
12286
+ init_config();
12287
+ init_datasources();
12288
+ }
12289
+ });
12290
+
12038
12291
  // src/mcp/tools/spec.ts
12039
12292
  import { z as z2 } from "zod";
12040
- import { join as join16, resolve as resolve4, sep as sep2 } from "path";
12293
+ import { join as join18, resolve as resolve4, sep as sep2 } from "path";
12041
12294
  import { readdir as readdir2, readFile as readFile11 } from "fs/promises";
12042
12295
  function registerSpecTools(server, cwd) {
12043
12296
  server.tool(
@@ -12047,57 +12300,52 @@ function registerSpecTools(server, cwd) {
12047
12300
  issues: z2.string().describe(
12048
12301
  "Comma-separated issue IDs (e.g. '42,43'), a glob pattern (e.g. 'drafts/*.md'), or an inline description."
12049
12302
  ),
12050
- provider: z2.enum(PROVIDER_NAMES).optional().describe("Agent provider name (default: opencode)"),
12051
- source: z2.enum(DATASOURCE_NAMES).optional().describe("Issue datasource: github, azdevops, md"),
12303
+ provider: z2.enum(PROVIDER_NAMES).optional().describe("Agent provider name (default: from config)"),
12304
+ source: z2.enum(DATASOURCE_NAMES).optional().describe("Issue datasource: github, azdevops, md (default: from config)"),
12052
12305
  concurrency: z2.number().int().min(1).max(32).optional().describe("Max parallel spec generations"),
12053
12306
  dryRun: z2.boolean().optional().describe("Preview without generating")
12054
12307
  },
12055
12308
  async (args) => {
12309
+ let config;
12310
+ try {
12311
+ config = await loadMcpConfig(cwd, { provider: args.provider, source: args.source });
12312
+ } catch (err) {
12313
+ return {
12314
+ content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }],
12315
+ isError: true
12316
+ };
12317
+ }
12056
12318
  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
- });
12319
+ forkDispatchRun(runId, server, {
12320
+ type: "spec",
12321
+ cwd,
12322
+ opts: {
12323
+ issues: args.issues,
12324
+ provider: config.provider,
12325
+ model: config.model,
12326
+ issueSource: config.source,
12327
+ org: config.org,
12328
+ project: config.project,
12329
+ workItemType: config.workItemType,
12330
+ iteration: config.iteration,
12331
+ area: config.area,
12332
+ concurrency: args.concurrency ?? config.concurrency,
12333
+ specTimeout: config.specTimeout,
12334
+ specWarnTimeout: config.specWarnTimeout,
12335
+ specKillTimeout: config.specKillTimeout,
12336
+ dryRun: args.dryRun,
12337
+ cwd
12338
+ }
12339
+ }, {
12340
+ onDone: (result) => {
12341
+ if ("generated" in result) {
12089
12342
  finishSpecRun(runId, "completed", {
12090
- total: result.total,
12091
- generated: result.generated,
12092
- failed: result.failed
12343
+ total: result["total"],
12344
+ generated: result["generated"],
12345
+ failed: result["failed"]
12093
12346
  });
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
12347
  }
12100
- })();
12348
+ }
12101
12349
  });
12102
12350
  return {
12103
12351
  content: [{ type: "text", text: JSON.stringify({ runId, status: "running" }) }]
@@ -12109,15 +12357,25 @@ function registerSpecTools(server, cwd) {
12109
12357
  "List spec files in the .dispatch/specs directory.",
12110
12358
  {},
12111
12359
  async () => {
12112
- const specsDir = join16(cwd, ".dispatch", "specs");
12360
+ const specsDir = join18(cwd, ".dispatch", "specs");
12113
12361
  let files = [];
12362
+ let dirError;
12114
12363
  try {
12115
12364
  const entries = await readdir2(specsDir);
12116
12365
  files = entries.filter((f) => f.endsWith(".md")).sort();
12366
+ } catch (err) {
12367
+ const isNotFound = err instanceof Error && "code" in err && err.code === "ENOENT";
12368
+ if (!isNotFound) {
12369
+ dirError = `Error reading specs directory: ${err instanceof Error ? err.message : String(err)}`;
12370
+ }
12371
+ }
12372
+ let recentRuns = [];
12373
+ try {
12374
+ recentRuns = listSpecRuns(5);
12117
12375
  } catch {
12118
12376
  }
12119
12377
  return {
12120
- content: [{ type: "text", text: JSON.stringify({ files, specsDir }) }]
12378
+ content: [{ type: "text", text: JSON.stringify({ files, specsDir, recentRuns, ...dirError ? { error: dirError } : {} }) }]
12121
12379
  };
12122
12380
  }
12123
12381
  );
@@ -12129,7 +12387,7 @@ function registerSpecTools(server, cwd) {
12129
12387
  },
12130
12388
  async (args) => {
12131
12389
  const specsDir = resolve4(cwd, ".dispatch", "specs");
12132
- const candidatePath = args.file.includes("/") || args.file.includes("\\") ? resolve4(specsDir, args.file) : join16(specsDir, args.file);
12390
+ const candidatePath = args.file.includes("/") || args.file.includes("\\") ? resolve4(specsDir, args.file) : join18(specsDir, args.file);
12133
12391
  if (!candidatePath.startsWith(specsDir + sep2) && candidatePath !== specsDir) {
12134
12392
  return {
12135
12393
  content: [{ type: "text", text: `Access denied: path must be inside the specs directory` }],
@@ -12158,39 +12416,69 @@ function registerSpecTools(server, cwd) {
12158
12416
  limit: z2.number().int().min(1).max(100).optional().describe("Max results (default 20)")
12159
12417
  },
12160
12418
  async (args) => {
12161
- const runs = listSpecRuns(args.limit ?? 20);
12162
- return {
12163
- content: [{ type: "text", text: JSON.stringify(runs) }]
12164
- };
12419
+ try {
12420
+ const runs = listSpecRuns(args.limit ?? 20);
12421
+ return {
12422
+ content: [{ type: "text", text: JSON.stringify(runs) }]
12423
+ };
12424
+ } catch (err) {
12425
+ return {
12426
+ content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }],
12427
+ isError: true
12428
+ };
12429
+ }
12165
12430
  }
12166
12431
  );
12167
12432
  server.tool(
12168
12433
  "spec_run_status",
12169
- "Get the status of a specific spec generation run.",
12434
+ "Get the status of a specific spec generation run. Use waitMs to hold the response until the run completes or the timeout elapses.",
12170
12435
  {
12171
- runId: z2.string().describe("The runId returned by spec_generate")
12436
+ runId: z2.string().describe("The runId returned by spec_generate"),
12437
+ waitMs: z2.number().int().min(0).max(12e4).optional().default(0).describe("Hold response until run completes or timeout (ms). 0 = return immediately.")
12172
12438
  },
12173
12439
  async (args) => {
12174
- const run = getSpecRun(args.runId);
12175
- if (!run) {
12440
+ try {
12441
+ let run = getSpecRun(args.runId);
12442
+ if (!run) {
12443
+ return {
12444
+ content: [{ type: "text", text: `Run ${args.runId} not found` }],
12445
+ isError: true
12446
+ };
12447
+ }
12448
+ if (run.status === "running" && args.waitMs > 0) {
12449
+ const completed = await waitForRunCompletion(
12450
+ args.runId,
12451
+ args.waitMs,
12452
+ () => getSpecRun(args.runId)?.status ?? null
12453
+ );
12454
+ if (completed) {
12455
+ run = getSpecRun(args.runId);
12456
+ }
12457
+ }
12458
+ const response = { ...run };
12459
+ if (run.status === "running") {
12460
+ response.retryAfterMs = 5e3;
12461
+ }
12176
12462
  return {
12177
- content: [{ type: "text", text: `Run ${args.runId} not found` }],
12463
+ content: [{ type: "text", text: JSON.stringify(response) }]
12464
+ };
12465
+ } catch (err) {
12466
+ return {
12467
+ content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }],
12178
12468
  isError: true
12179
12469
  };
12180
12470
  }
12181
- return {
12182
- content: [{ type: "text", text: JSON.stringify(run) }]
12183
- };
12184
12471
  }
12185
12472
  );
12186
12473
  }
12187
12474
  var init_spec2 = __esm({
12188
12475
  "src/mcp/tools/spec.ts"() {
12189
12476
  "use strict";
12190
- init_spec_pipeline();
12191
12477
  init_manager();
12192
12478
  init_interface2();
12193
12479
  init_interface();
12480
+ init_fork_run();
12481
+ init_resolve_config();
12194
12482
  }
12195
12483
  });
12196
12484
 
@@ -12202,8 +12490,8 @@ function registerDispatchTools(server, cwd) {
12202
12490
  "Execute dispatch pipeline for one or more issue IDs. Returns a runId immediately; progress is pushed via logging notifications.",
12203
12491
  {
12204
12492
  issueIds: z3.array(z3.string()).min(1).describe("Issue IDs to dispatch (e.g. ['42', '43'])"),
12205
- provider: z3.enum(PROVIDER_NAMES).optional().describe("Agent provider (default: opencode)"),
12206
- source: z3.enum(DATASOURCE_NAMES).optional().describe("Issue datasource: github, azdevops, md"),
12493
+ provider: z3.enum(PROVIDER_NAMES).optional().describe("Agent provider (default: from config)"),
12494
+ source: z3.enum(DATASOURCE_NAMES).optional().describe("Issue datasource: github, azdevops, md (default: from config)"),
12207
12495
  concurrency: z3.number().int().min(1).max(32).optional().describe("Max parallel tasks"),
12208
12496
  noPlan: z3.boolean().optional().describe("Skip the planner agent"),
12209
12497
  noBranch: z3.boolean().optional().describe("Skip branch creation and PR lifecycle"),
@@ -12211,65 +12499,41 @@ function registerDispatchTools(server, cwd) {
12211
12499
  retries: z3.number().int().min(0).max(10).optional().describe("Retry attempts per task")
12212
12500
  },
12213
12501
  async (args) => {
12502
+ let config;
12503
+ try {
12504
+ config = await loadMcpConfig(cwd, { provider: args.provider, source: args.source });
12505
+ } catch (err) {
12506
+ return {
12507
+ content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }],
12508
+ isError: true
12509
+ };
12510
+ }
12214
12511
  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
- })();
12512
+ forkDispatchRun(runId, server, {
12513
+ type: "dispatch",
12514
+ cwd,
12515
+ opts: {
12516
+ issueIds: args.issueIds,
12517
+ dryRun: false,
12518
+ provider: config.provider,
12519
+ model: config.model,
12520
+ fastProvider: config.fastProvider,
12521
+ fastModel: config.fastModel,
12522
+ agents: config.agents,
12523
+ source: config.source,
12524
+ org: config.org,
12525
+ project: config.project,
12526
+ workItemType: config.workItemType,
12527
+ iteration: config.iteration,
12528
+ area: config.area,
12529
+ username: config.username,
12530
+ planTimeout: config.planTimeout,
12531
+ concurrency: args.concurrency ?? config.concurrency ?? 1,
12532
+ noPlan: args.noPlan,
12533
+ noBranch: args.noBranch,
12534
+ noWorktree: args.noWorktree,
12535
+ retries: args.retries
12536
+ }
12273
12537
  });
12274
12538
  return {
12275
12539
  content: [{ type: "text", text: JSON.stringify({ runId, status: "running" }) }]
@@ -12281,15 +12545,28 @@ function registerDispatchTools(server, cwd) {
12281
12545
  "Preview tasks that would be dispatched for the given issue IDs without executing anything.",
12282
12546
  {
12283
12547
  issueIds: z3.array(z3.string()).min(1).describe("Issue IDs to preview"),
12284
- source: z3.enum(DATASOURCE_NAMES).optional().describe("Issue datasource: github, azdevops, md")
12548
+ source: z3.enum(DATASOURCE_NAMES).optional().describe("Issue datasource: github, azdevops, md (default: from config)")
12285
12549
  },
12286
12550
  async (args) => {
12287
12551
  try {
12552
+ const config = await loadMcpConfig(cwd, { source: args.source });
12288
12553
  const orchestrator = await boot9({ cwd });
12289
12554
  const result = await orchestrator.orchestrate({
12290
12555
  issueIds: args.issueIds,
12291
12556
  dryRun: true,
12292
- source: args.source
12557
+ provider: config.provider,
12558
+ model: config.model,
12559
+ fastProvider: config.fastProvider,
12560
+ fastModel: config.fastModel,
12561
+ agents: config.agents,
12562
+ source: config.source,
12563
+ org: config.org,
12564
+ project: config.project,
12565
+ workItemType: config.workItemType,
12566
+ iteration: config.iteration,
12567
+ area: config.area,
12568
+ username: config.username,
12569
+ planTimeout: config.planTimeout
12293
12570
  });
12294
12571
  return {
12295
12572
  content: [{ type: "text", text: JSON.stringify(result) }]
@@ -12310,31 +12587,55 @@ var init_dispatch = __esm({
12310
12587
  init_manager();
12311
12588
  init_interface2();
12312
12589
  init_interface();
12590
+ init_fork_run();
12591
+ init_resolve_config();
12313
12592
  }
12314
12593
  });
12315
12594
 
12316
12595
  // src/mcp/tools/monitor.ts
12317
12596
  import { z as z4 } from "zod";
12318
- import { join as join17 } from "path";
12597
+ import { join as join19 } from "path";
12319
12598
  function registerMonitorTools(server, cwd) {
12320
12599
  server.tool(
12321
12600
  "status_get",
12322
- "Get the current status of a dispatch or spec run, including per-task details.",
12601
+ "Get the current status of a dispatch or spec run, including per-task details. Use waitMs to hold the response until the run completes or the timeout elapses.",
12323
12602
  {
12324
- runId: z4.string().describe("The runId returned by dispatch_run or spec_generate")
12603
+ runId: z4.string().describe("The runId returned by dispatch_run or spec_generate"),
12604
+ waitMs: z4.number().int().min(0).max(12e4).optional().default(0).describe("Hold response until run completes or timeout (ms). 0 = return immediately.")
12325
12605
  },
12326
12606
  async (args) => {
12327
- const run = getRun(args.runId);
12328
- if (!run) {
12607
+ try {
12608
+ let run = getRun(args.runId);
12609
+ if (!run) {
12610
+ return {
12611
+ content: [{ type: "text", text: `Run ${args.runId} not found` }],
12612
+ isError: true
12613
+ };
12614
+ }
12615
+ if (run.status === "running" && args.waitMs > 0) {
12616
+ const completed = await waitForRunCompletion(
12617
+ args.runId,
12618
+ args.waitMs,
12619
+ () => getRun(args.runId)?.status ?? null
12620
+ );
12621
+ if (completed) {
12622
+ run = getRun(args.runId);
12623
+ }
12624
+ }
12625
+ const tasks = getTasksForRun(args.runId);
12626
+ const response = { run, tasks };
12627
+ if (run.status === "running") {
12628
+ response.retryAfterMs = 5e3;
12629
+ }
12329
12630
  return {
12330
- content: [{ type: "text", text: `Run ${args.runId} not found` }],
12631
+ content: [{ type: "text", text: JSON.stringify(response) }]
12632
+ };
12633
+ } catch (err) {
12634
+ return {
12635
+ content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }],
12331
12636
  isError: true
12332
12637
  };
12333
12638
  }
12334
- const tasks = getTasksForRun(args.runId);
12335
- return {
12336
- content: [{ type: "text", text: JSON.stringify({ run, tasks }) }]
12337
- };
12338
12639
  }
12339
12640
  );
12340
12641
  server.tool(
@@ -12345,10 +12646,17 @@ function registerMonitorTools(server, cwd) {
12345
12646
  limit: z4.number().int().min(1).max(100).optional().describe("Max results (default 20)")
12346
12647
  },
12347
12648
  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
- };
12649
+ try {
12650
+ const runs = args.status ? listRunsByStatus(args.status, args.limit ?? 20) : listRuns(args.limit ?? 20);
12651
+ return {
12652
+ content: [{ type: "text", text: JSON.stringify(runs) }]
12653
+ };
12654
+ } catch (err) {
12655
+ return {
12656
+ content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }],
12657
+ isError: true
12658
+ };
12659
+ }
12352
12660
  }
12353
12661
  );
12354
12662
  server.tool(
@@ -12364,7 +12672,7 @@ function registerMonitorTools(server, cwd) {
12364
12672
  },
12365
12673
  async (args) => {
12366
12674
  try {
12367
- const config = await loadConfig(join17(cwd, ".dispatch"));
12675
+ const config = await loadConfig(join19(cwd, ".dispatch"));
12368
12676
  const sourceName = args.source ?? config.source;
12369
12677
  if (!sourceName) {
12370
12678
  return {
@@ -12409,7 +12717,7 @@ function registerMonitorTools(server, cwd) {
12409
12717
  },
12410
12718
  async (args) => {
12411
12719
  try {
12412
- const config = await loadConfig(join17(cwd, ".dispatch"));
12720
+ const config = await loadConfig(join19(cwd, ".dispatch"));
12413
12721
  const sourceName = args.source ?? config.source;
12414
12722
  if (!sourceName) {
12415
12723
  return {
@@ -12457,7 +12765,6 @@ var init_monitor = __esm({
12457
12765
 
12458
12766
  // src/mcp/tools/recovery.ts
12459
12767
  import { z as z5 } from "zod";
12460
- import { join as join18 } from "path";
12461
12768
  function registerRecoveryTools(server, cwd) {
12462
12769
  server.tool(
12463
12770
  "run_retry",
@@ -12477,63 +12784,44 @@ function registerRecoveryTools(server, cwd) {
12477
12784
  }
12478
12785
  const tasks = getTasksForRun(args.runId);
12479
12786
  const failedTasks = tasks.filter((t) => t.status === "failed");
12480
- if (failedTasks.length === 0) {
12787
+ if (failedTasks.length === 0 && originalRun.status !== "failed") {
12481
12788
  return {
12482
12789
  content: [{ type: "text", text: JSON.stringify({ message: "No failed tasks found", originalRunId: args.runId }) }]
12483
12790
  };
12484
12791
  }
12485
- const config = await loadConfig(join18(cwd, ".dispatch"));
12792
+ let config;
12793
+ try {
12794
+ config = await loadMcpConfig(cwd, { provider: args.provider });
12795
+ } catch (err) {
12796
+ return {
12797
+ content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }],
12798
+ isError: true
12799
+ };
12800
+ }
12486
12801
  const issueIds = issueIdsSchema.parse(JSON.parse(originalRun.issueIds));
12487
12802
  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
- })();
12803
+ forkDispatchRun(newRunId, server, {
12804
+ type: "dispatch",
12805
+ cwd,
12806
+ opts: {
12807
+ issueIds,
12808
+ dryRun: false,
12809
+ provider: config.provider,
12810
+ model: config.model,
12811
+ fastProvider: config.fastProvider,
12812
+ fastModel: config.fastModel,
12813
+ agents: config.agents,
12814
+ source: config.source,
12815
+ org: config.org,
12816
+ project: config.project,
12817
+ workItemType: config.workItemType,
12818
+ iteration: config.iteration,
12819
+ area: config.area,
12820
+ username: config.username,
12821
+ planTimeout: config.planTimeout,
12822
+ concurrency: args.concurrency ?? config.concurrency ?? 1,
12823
+ force: true
12824
+ }
12537
12825
  });
12538
12826
  return {
12539
12827
  content: [{ type: "text", text: JSON.stringify({ runId: newRunId, status: "running", originalRunId: args.runId }) }]
@@ -12564,54 +12852,39 @@ function registerRecoveryTools(server, cwd) {
12564
12852
  isError: true
12565
12853
  };
12566
12854
  }
12567
- const config = await loadConfig(join18(cwd, ".dispatch"));
12855
+ let config;
12856
+ try {
12857
+ config = await loadMcpConfig(cwd, { provider: args.provider });
12858
+ } catch (err) {
12859
+ return {
12860
+ content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }],
12861
+ isError: true
12862
+ };
12863
+ }
12568
12864
  const issueIds = issueIdsSchema.parse(JSON.parse(originalRun.issueIds));
12569
12865
  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
- })();
12866
+ forkDispatchRun(newRunId, server, {
12867
+ type: "dispatch",
12868
+ cwd,
12869
+ opts: {
12870
+ issueIds,
12871
+ dryRun: false,
12872
+ provider: config.provider,
12873
+ model: config.model,
12874
+ fastProvider: config.fastProvider,
12875
+ fastModel: config.fastModel,
12876
+ agents: config.agents,
12877
+ source: config.source,
12878
+ org: config.org,
12879
+ project: config.project,
12880
+ workItemType: config.workItemType,
12881
+ iteration: config.iteration,
12882
+ area: config.area,
12883
+ username: config.username,
12884
+ planTimeout: config.planTimeout,
12885
+ concurrency: 1,
12886
+ force: true
12887
+ }
12615
12888
  });
12616
12889
  return {
12617
12890
  content: [{ type: "text", text: JSON.stringify({ runId: newRunId, status: "running", taskId: args.taskId }) }]
@@ -12624,22 +12897,22 @@ var init_recovery = __esm({
12624
12897
  "src/mcp/tools/recovery.ts"() {
12625
12898
  "use strict";
12626
12899
  init_manager();
12627
- init_runner();
12628
- init_config();
12629
12900
  init_interface2();
12901
+ init_fork_run();
12902
+ init_resolve_config();
12630
12903
  issueIdsSchema = z5.array(z5.string());
12631
12904
  }
12632
12905
  });
12633
12906
 
12634
12907
  // src/mcp/tools/config.ts
12635
- import { join as join19 } from "path";
12908
+ import { join as join20 } from "path";
12636
12909
  function registerConfigTools(server, cwd) {
12637
12910
  server.tool(
12638
12911
  "config_get",
12639
12912
  "Get the current Dispatch configuration from .dispatch/config.json.",
12640
12913
  {},
12641
12914
  async () => {
12642
- const config = await loadConfig(join19(cwd, ".dispatch"));
12915
+ const config = await loadConfig(join20(cwd, ".dispatch"));
12643
12916
  const { nextIssueId: _, ...safeConfig } = config;
12644
12917
  return {
12645
12918
  content: [{ type: "text", text: JSON.stringify(safeConfig) }]
@@ -12658,7 +12931,33 @@ var init_config2 = __esm({
12658
12931
  import http from "http";
12659
12932
  import { randomUUID as randomUUID7 } from "crypto";
12660
12933
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
12934
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
12661
12935
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
12936
+ async function createStdioMcpServer(cwd) {
12937
+ const mcpServer = new McpServer(
12938
+ { name: "dispatch", version: "1.0.0" },
12939
+ { capabilities: { logging: {} } }
12940
+ );
12941
+ registerSpecTools(mcpServer, cwd);
12942
+ registerDispatchTools(mcpServer, cwd);
12943
+ registerMonitorTools(mcpServer, cwd);
12944
+ registerRecoveryTools(mcpServer, cwd);
12945
+ registerConfigTools(mcpServer, cwd);
12946
+ const transport = new StdioServerTransport();
12947
+ await mcpServer.connect(transport);
12948
+ return {
12949
+ close: async () => {
12950
+ await transport.close().catch((err) => {
12951
+ process.stderr.write(`[dispatch-mcp] transport.close error: ${String(err)}
12952
+ `);
12953
+ });
12954
+ await mcpServer.close().catch((err) => {
12955
+ process.stderr.write(`[dispatch-mcp] mcpServer.close error: ${String(err)}
12956
+ `);
12957
+ });
12958
+ }
12959
+ };
12960
+ }
12662
12961
  async function createMcpServer(opts) {
12663
12962
  const { port, host, cwd } = opts;
12664
12963
  const mcpServer = new McpServer(
@@ -12772,6 +13071,17 @@ async function createMcpServer(opts) {
12772
13071
  }
12773
13072
  };
12774
13073
  }
13074
+ function wireRunLogs(runId, server) {
13075
+ addLogCallback(runId, (message, level) => {
13076
+ server.sendLoggingMessage({
13077
+ level: level === "error" ? "error" : level === "warn" ? "warning" : "info",
13078
+ logger: `dispatch.run.${runId}`,
13079
+ data: message
13080
+ }).catch((err) => {
13081
+ console.error("[dispatch-mcp] sendLoggingMessage error:", err);
13082
+ });
13083
+ });
13084
+ }
12775
13085
  var init_server = __esm({
12776
13086
  "src/mcp/server.ts"() {
12777
13087
  "use strict";
@@ -12787,7 +13097,8 @@ var init_server = __esm({
12787
13097
  // src/mcp/index.ts
12788
13098
  var mcp_exports = {};
12789
13099
  __export(mcp_exports, {
12790
- startMcpServer: () => startMcpServer
13100
+ startMcpServer: () => startMcpServer,
13101
+ startStdioMcpServer: () => startStdioMcpServer
12791
13102
  });
12792
13103
  async function startMcpServer(opts) {
12793
13104
  const { port, host, cwd } = opts;
@@ -12813,6 +13124,32 @@ Received ${signal}, shutting down MCP server...`);
12813
13124
  process.on("SIGINT", () => void shutdown("SIGINT"));
12814
13125
  process.on("SIGTERM", () => void shutdown("SIGTERM"));
12815
13126
  }
13127
+ async function startStdioMcpServer(opts) {
13128
+ const { cwd } = opts;
13129
+ openDatabase(cwd);
13130
+ const handle = await createStdioMcpServer(cwd);
13131
+ process.stderr.write("Dispatch MCP server ready (stdio transport). Press Ctrl+C to stop.\n");
13132
+ async function shutdown(signal) {
13133
+ process.stderr.write(`
13134
+ Received ${signal}, shutting down MCP server...
13135
+ `);
13136
+ try {
13137
+ await handle.close();
13138
+ } catch (err) {
13139
+ process.stderr.write(`[dispatch-mcp] Error during server close: ${String(err)}
13140
+ `);
13141
+ }
13142
+ try {
13143
+ closeDatabase();
13144
+ } catch (err) {
13145
+ process.stderr.write(`[dispatch-mcp] Error closing database: ${String(err)}
13146
+ `);
13147
+ }
13148
+ process.exit(0);
13149
+ }
13150
+ process.on("SIGINT", () => void shutdown("SIGINT"));
13151
+ process.on("SIGTERM", () => void shutdown("SIGTERM"));
13152
+ }
12816
13153
  var init_mcp = __esm({
12817
13154
  "src/mcp/index.ts"() {
12818
13155
  "use strict";
@@ -12828,7 +13165,7 @@ init_cleanup();
12828
13165
  init_providers();
12829
13166
  init_datasources();
12830
13167
  init_config();
12831
- import { resolve as resolve5, join as join20 } from "path";
13168
+ import { resolve as resolve5, join as join21 } from "path";
12832
13169
  import { Command, Option, CommanderError } from "commander";
12833
13170
  var MAX_CONCURRENCY = CONFIG_BOUNDS.concurrency.max;
12834
13171
  var HELP = `
@@ -13121,14 +13458,14 @@ async function main() {
13121
13458
  }
13122
13459
  throw err;
13123
13460
  }
13124
- const configDir = join20(configProgram.opts().cwd ?? process.cwd(), ".dispatch");
13461
+ const configDir = join21(configProgram.opts().cwd ?? process.cwd(), ".dispatch");
13125
13462
  await handleConfigCommand(rawArgv.slice(1), configDir);
13126
13463
  process.exit(0);
13127
13464
  }
13128
13465
  if (rawArgv[0] === "mcp") {
13129
13466
  const mcpProgram = new Command("dispatch-mcp").exitOverride().configureOutput({ writeOut: () => {
13130
13467
  }, writeErr: () => {
13131
- } }).helpOption(false).allowUnknownOption(true).allowExcessArguments(true).option("--port <number>", "Port to listen on", (v) => parseInt(v, 10), 9110).option("--host <host>", "Host to bind to", "127.0.0.1").option("--cwd <dir>", "Working directory", (v) => resolve5(v));
13468
+ } }).helpOption(false).allowUnknownOption(true).allowExcessArguments(true).option("--http", "Use HTTP transport instead of stdio (for remote/multi-client use)").option("--port <number>", "Port to listen on (HTTP mode only)", (v) => parseInt(v, 10), 9110).option("--host <host>", "Host to bind to (HTTP mode only)", "127.0.0.1").option("--cwd <dir>", "Working directory", (v) => resolve5(v));
13132
13469
  try {
13133
13470
  mcpProgram.parse(rawArgv.slice(1), { from: "user" });
13134
13471
  } catch (err) {
@@ -13139,12 +13476,14 @@ async function main() {
13139
13476
  throw err;
13140
13477
  }
13141
13478
  const mcpOpts = mcpProgram.opts();
13142
- const { startMcpServer: startMcpServer2 } = await Promise.resolve().then(() => (init_mcp(), mcp_exports));
13143
- await startMcpServer2({
13144
- port: mcpOpts.port,
13145
- host: mcpOpts.host,
13146
- cwd: mcpOpts.cwd ?? process.cwd()
13147
- });
13479
+ const cwd = mcpOpts.cwd ?? process.cwd();
13480
+ if (mcpOpts.http) {
13481
+ const { startMcpServer: startMcpServer2 } = await Promise.resolve().then(() => (init_mcp(), mcp_exports));
13482
+ await startMcpServer2({ port: mcpOpts.port, host: mcpOpts.host, cwd });
13483
+ } else {
13484
+ const { startStdioMcpServer: startStdioMcpServer2 } = await Promise.resolve().then(() => (init_mcp(), mcp_exports));
13485
+ await startStdioMcpServer2({ cwd });
13486
+ }
13148
13487
  return;
13149
13488
  }
13150
13489
  const [args, explicitFlags] = parseArgs(rawArgv);
@@ -13164,7 +13503,7 @@ async function main() {
13164
13503
  process.exit(0);
13165
13504
  }
13166
13505
  if (args.version) {
13167
- console.log(`dispatch v${"1.5.0-beta.b8296d7"}`);
13506
+ console.log(`dispatch v${"1.5.0-beta.c0bcc82"}`);
13168
13507
  process.exit(0);
13169
13508
  }
13170
13509
  const orchestrator = await boot9({ cwd: args.cwd });