@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 +619 -280
- package/dist/cli.js.map +1 -1
- package/dist/mcp/dispatch-worker.js +11247 -0
- package/dist/mcp/dispatch-worker.js.map +1 -0
- package/package.json +1 -1
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
|
-
|
|
8006
|
-
|
|
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
|
-
|
|
8009
|
-
|
|
8010
|
-
|
|
8011
|
-
|
|
8012
|
-
|
|
8013
|
-
|
|
8014
|
-
|
|
8015
|
-
|
|
8016
|
-
|
|
8017
|
-
|
|
8018
|
-
|
|
8019
|
-
|
|
8020
|
-
|
|
8021
|
-
|
|
8022
|
-
|
|
8023
|
-
await git4(["worktree", "
|
|
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
|
-
|
|
8027
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
12058
|
-
|
|
12059
|
-
|
|
12060
|
-
|
|
12061
|
-
|
|
12062
|
-
|
|
12063
|
-
|
|
12064
|
-
|
|
12065
|
-
|
|
12066
|
-
|
|
12067
|
-
|
|
12068
|
-
|
|
12069
|
-
|
|
12070
|
-
|
|
12071
|
-
|
|
12072
|
-
|
|
12073
|
-
|
|
12074
|
-
|
|
12075
|
-
|
|
12076
|
-
|
|
12077
|
-
|
|
12078
|
-
|
|
12079
|
-
|
|
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
|
|
12091
|
-
generated: result
|
|
12092
|
-
failed: result
|
|
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 =
|
|
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) :
|
|
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
|
-
|
|
12162
|
-
|
|
12163
|
-
|
|
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
|
-
|
|
12175
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
12216
|
-
|
|
12217
|
-
|
|
12218
|
-
|
|
12219
|
-
|
|
12220
|
-
|
|
12221
|
-
|
|
12222
|
-
|
|
12223
|
-
|
|
12224
|
-
|
|
12225
|
-
|
|
12226
|
-
|
|
12227
|
-
|
|
12228
|
-
|
|
12229
|
-
|
|
12230
|
-
|
|
12231
|
-
|
|
12232
|
-
|
|
12233
|
-
|
|
12234
|
-
|
|
12235
|
-
|
|
12236
|
-
|
|
12237
|
-
|
|
12238
|
-
|
|
12239
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
12328
|
-
|
|
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:
|
|
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
|
-
|
|
12349
|
-
|
|
12350
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
12489
|
-
|
|
12490
|
-
|
|
12491
|
-
|
|
12492
|
-
|
|
12493
|
-
|
|
12494
|
-
|
|
12495
|
-
|
|
12496
|
-
|
|
12497
|
-
|
|
12498
|
-
|
|
12499
|
-
|
|
12500
|
-
|
|
12501
|
-
|
|
12502
|
-
|
|
12503
|
-
|
|
12504
|
-
|
|
12505
|
-
|
|
12506
|
-
|
|
12507
|
-
|
|
12508
|
-
|
|
12509
|
-
|
|
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
|
-
|
|
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
|
-
|
|
12571
|
-
|
|
12572
|
-
|
|
12573
|
-
|
|
12574
|
-
|
|
12575
|
-
|
|
12576
|
-
|
|
12577
|
-
|
|
12578
|
-
|
|
12579
|
-
|
|
12580
|
-
|
|
12581
|
-
|
|
12582
|
-
|
|
12583
|
-
|
|
12584
|
-
|
|
12585
|
-
|
|
12586
|
-
|
|
12587
|
-
|
|
12588
|
-
|
|
12589
|
-
|
|
12590
|
-
|
|
12591
|
-
|
|
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
|
|
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(
|
|
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
|
|
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 =
|
|
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
|
|
13143
|
-
|
|
13144
|
-
|
|
13145
|
-
host: mcpOpts.host,
|
|
13146
|
-
|
|
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.
|
|
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 });
|