@pruddiman/dispatch 1.5.0-beta.236e011 → 1.5.0-beta.30f06ab
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +346 -260
- 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 {
|
|
@@ -11867,6 +11925,12 @@ function registerLiveRun(runId) {
|
|
|
11867
11925
|
function unregisterLiveRun(runId) {
|
|
11868
11926
|
liveRuns.delete(runId);
|
|
11869
11927
|
}
|
|
11928
|
+
function addLogCallback(runId, cb) {
|
|
11929
|
+
const run = liveRuns.get(runId);
|
|
11930
|
+
if (run) {
|
|
11931
|
+
run.callbacks.push(cb);
|
|
11932
|
+
}
|
|
11933
|
+
}
|
|
11870
11934
|
function emitLog(runId, message, level = "info") {
|
|
11871
11935
|
const run = liveRuns.get(runId);
|
|
11872
11936
|
if (run) {
|
|
@@ -11971,6 +12035,12 @@ function listRunsByStatus(status, limit = 20) {
|
|
|
11971
12035
|
).all(status, limit);
|
|
11972
12036
|
return rows.map(rowToRun);
|
|
11973
12037
|
}
|
|
12038
|
+
function createTask(opts) {
|
|
12039
|
+
getDb().prepare(`
|
|
12040
|
+
INSERT INTO tasks (run_id, task_id, task_text, file, line, status)
|
|
12041
|
+
VALUES (?, ?, ?, ?, ?, 'pending')
|
|
12042
|
+
`).run(opts.runId, opts.taskId, opts.taskText, opts.file, opts.line);
|
|
12043
|
+
}
|
|
11974
12044
|
function updateTaskStatus(runId, taskId, status, opts) {
|
|
11975
12045
|
const now = Date.now();
|
|
11976
12046
|
const isTerminal = status === "success" || status === "failed" || status === "skipped";
|
|
@@ -12035,9 +12105,114 @@ var init_manager = __esm({
|
|
|
12035
12105
|
}
|
|
12036
12106
|
});
|
|
12037
12107
|
|
|
12108
|
+
// src/mcp/tools/_fork-run.ts
|
|
12109
|
+
import { fork } from "child_process";
|
|
12110
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
12111
|
+
import { dirname as dirname6, join as join16 } from "path";
|
|
12112
|
+
function forkDispatchRun(runId, server, workerMessage, options) {
|
|
12113
|
+
wireRunLogs(runId, server);
|
|
12114
|
+
const worker = fork(WORKER_PATH, [], { stdio: ["pipe", "pipe", "pipe", "ipc"] });
|
|
12115
|
+
worker.send(workerMessage);
|
|
12116
|
+
const heartbeat = setInterval(() => {
|
|
12117
|
+
emitLog(runId, `Run ${runId} still in progress...`);
|
|
12118
|
+
}, HEARTBEAT_INTERVAL_MS);
|
|
12119
|
+
worker.on("message", (msg) => {
|
|
12120
|
+
const msgType = msg["type"];
|
|
12121
|
+
switch (msgType) {
|
|
12122
|
+
case "progress": {
|
|
12123
|
+
const event = msg["event"];
|
|
12124
|
+
const eventType = event["type"];
|
|
12125
|
+
switch (eventType) {
|
|
12126
|
+
case "task_start":
|
|
12127
|
+
createTask({
|
|
12128
|
+
runId,
|
|
12129
|
+
taskId: event["taskId"],
|
|
12130
|
+
taskText: event["taskText"],
|
|
12131
|
+
file: event["file"] ?? event["taskId"].split(":")[0] ?? "",
|
|
12132
|
+
line: event["line"] ?? parseInt(event["taskId"].split(":")[1] ?? "0", 10)
|
|
12133
|
+
});
|
|
12134
|
+
updateTaskStatus(runId, event["taskId"], "running");
|
|
12135
|
+
emitLog(runId, `Task started: ${event["taskText"]}`);
|
|
12136
|
+
break;
|
|
12137
|
+
case "task_done":
|
|
12138
|
+
updateTaskStatus(runId, event["taskId"], "success");
|
|
12139
|
+
emitLog(runId, `Task done: ${event["taskText"]}`);
|
|
12140
|
+
break;
|
|
12141
|
+
case "task_failed":
|
|
12142
|
+
updateTaskStatus(runId, event["taskId"], "failed", { error: event["error"] });
|
|
12143
|
+
emitLog(runId, `Task failed: ${event["taskText"]} \u2014 ${event["error"]}`, "error");
|
|
12144
|
+
break;
|
|
12145
|
+
case "phase_change":
|
|
12146
|
+
emitLog(runId, event["message"] ?? `Phase: ${event["phase"]}`);
|
|
12147
|
+
break;
|
|
12148
|
+
case "log":
|
|
12149
|
+
emitLog(runId, event["message"]);
|
|
12150
|
+
break;
|
|
12151
|
+
}
|
|
12152
|
+
break;
|
|
12153
|
+
}
|
|
12154
|
+
case "spec_progress": {
|
|
12155
|
+
const event = msg["event"];
|
|
12156
|
+
const eventType = event["type"];
|
|
12157
|
+
switch (eventType) {
|
|
12158
|
+
case "item_start":
|
|
12159
|
+
emitLog(runId, `Generating spec for: ${event["itemTitle"] ?? event["itemId"]}`);
|
|
12160
|
+
break;
|
|
12161
|
+
case "item_done":
|
|
12162
|
+
emitLog(runId, `Spec done: ${event["itemTitle"] ?? event["itemId"]}`);
|
|
12163
|
+
break;
|
|
12164
|
+
case "item_failed":
|
|
12165
|
+
emitLog(runId, `Spec failed: ${event["itemTitle"] ?? event["itemId"]} \u2014 ${event["error"]}`, "error");
|
|
12166
|
+
break;
|
|
12167
|
+
case "log":
|
|
12168
|
+
emitLog(runId, event["message"]);
|
|
12169
|
+
break;
|
|
12170
|
+
}
|
|
12171
|
+
break;
|
|
12172
|
+
}
|
|
12173
|
+
case "done": {
|
|
12174
|
+
const result = msg["result"];
|
|
12175
|
+
if (options?.onDone) {
|
|
12176
|
+
options.onDone(result);
|
|
12177
|
+
} else if ("completed" in result) {
|
|
12178
|
+
updateRunCounters(runId, result["total"], result["completed"], result["failed"]);
|
|
12179
|
+
finishRun(runId, result["failed"] > 0 ? "failed" : "completed");
|
|
12180
|
+
emitLog(runId, `Dispatch complete: ${result["completed"]}/${result["total"]} tasks succeeded`);
|
|
12181
|
+
}
|
|
12182
|
+
break;
|
|
12183
|
+
}
|
|
12184
|
+
case "error": {
|
|
12185
|
+
finishRun(runId, "failed", msg["message"]);
|
|
12186
|
+
emitLog(runId, `Run error: ${msg["message"]}`, "error");
|
|
12187
|
+
break;
|
|
12188
|
+
}
|
|
12189
|
+
}
|
|
12190
|
+
});
|
|
12191
|
+
worker.on("exit", (code) => {
|
|
12192
|
+
clearInterval(heartbeat);
|
|
12193
|
+
if (code !== 0 && code !== null) {
|
|
12194
|
+
finishRun(runId, "failed", `Worker process exited with code ${code}`);
|
|
12195
|
+
emitLog(runId, `Worker process exited unexpectedly (code ${code})`, "error");
|
|
12196
|
+
}
|
|
12197
|
+
});
|
|
12198
|
+
return worker;
|
|
12199
|
+
}
|
|
12200
|
+
var __filename, __dirname, WORKER_PATH, HEARTBEAT_INTERVAL_MS;
|
|
12201
|
+
var init_fork_run = __esm({
|
|
12202
|
+
"src/mcp/tools/_fork-run.ts"() {
|
|
12203
|
+
"use strict";
|
|
12204
|
+
init_manager();
|
|
12205
|
+
init_server();
|
|
12206
|
+
__filename = fileURLToPath2(import.meta.url);
|
|
12207
|
+
__dirname = dirname6(__filename);
|
|
12208
|
+
WORKER_PATH = join16(__dirname, "..", "dispatch-worker.js");
|
|
12209
|
+
HEARTBEAT_INTERVAL_MS = 3e4;
|
|
12210
|
+
}
|
|
12211
|
+
});
|
|
12212
|
+
|
|
12038
12213
|
// src/mcp/tools/spec.ts
|
|
12039
12214
|
import { z as z2 } from "zod";
|
|
12040
|
-
import { join as
|
|
12215
|
+
import { join as join17, resolve as resolve4, sep as sep2 } from "path";
|
|
12041
12216
|
import { readdir as readdir2, readFile as readFile11 } from "fs/promises";
|
|
12042
12217
|
function registerSpecTools(server, cwd) {
|
|
12043
12218
|
server.tool(
|
|
@@ -12054,50 +12229,27 @@ function registerSpecTools(server, cwd) {
|
|
|
12054
12229
|
},
|
|
12055
12230
|
async (args) => {
|
|
12056
12231
|
const runId = createSpecRun({ cwd, issues: args.issues });
|
|
12057
|
-
|
|
12058
|
-
|
|
12059
|
-
|
|
12060
|
-
|
|
12061
|
-
|
|
12062
|
-
|
|
12063
|
-
|
|
12064
|
-
|
|
12065
|
-
|
|
12066
|
-
|
|
12067
|
-
|
|
12068
|
-
|
|
12069
|
-
|
|
12070
|
-
|
|
12071
|
-
emitLog(runId, `Generating spec for: ${event.itemTitle ?? event.itemId}`);
|
|
12072
|
-
break;
|
|
12073
|
-
case "item_done":
|
|
12074
|
-
emitLog(runId, `Spec done: ${event.itemTitle ?? event.itemId}`);
|
|
12075
|
-
break;
|
|
12076
|
-
case "item_failed":
|
|
12077
|
-
emitLog(runId, `Spec failed: ${event.itemTitle ?? event.itemId} \u2014 ${event.error}`, "error");
|
|
12078
|
-
break;
|
|
12079
|
-
case "log":
|
|
12080
|
-
emitLog(runId, event.message);
|
|
12081
|
-
break;
|
|
12082
|
-
default: {
|
|
12083
|
-
const _exhaustive = event;
|
|
12084
|
-
void _exhaustive;
|
|
12085
|
-
}
|
|
12086
|
-
}
|
|
12087
|
-
}
|
|
12088
|
-
});
|
|
12232
|
+
forkDispatchRun(runId, server, {
|
|
12233
|
+
type: "spec",
|
|
12234
|
+
cwd,
|
|
12235
|
+
opts: {
|
|
12236
|
+
issues: args.issues,
|
|
12237
|
+
provider: args.provider ?? "opencode",
|
|
12238
|
+
issueSource: args.source,
|
|
12239
|
+
concurrency: args.concurrency,
|
|
12240
|
+
dryRun: args.dryRun,
|
|
12241
|
+
cwd
|
|
12242
|
+
}
|
|
12243
|
+
}, {
|
|
12244
|
+
onDone: (result) => {
|
|
12245
|
+
if ("generated" in result) {
|
|
12089
12246
|
finishSpecRun(runId, "completed", {
|
|
12090
|
-
total: result
|
|
12091
|
-
generated: result
|
|
12092
|
-
failed: result
|
|
12247
|
+
total: result["total"],
|
|
12248
|
+
generated: result["generated"],
|
|
12249
|
+
failed: result["failed"]
|
|
12093
12250
|
});
|
|
12094
|
-
emitLog(runId, `Spec generation complete: ${result.generated} generated, ${result.failed} failed`);
|
|
12095
|
-
} catch (err) {
|
|
12096
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
12097
|
-
finishSpecRun(runId, "failed", { total: 0, generated: 0, failed: 0 }, msg);
|
|
12098
|
-
emitLog(runId, `Spec generation error: ${msg}`, "error");
|
|
12099
12251
|
}
|
|
12100
|
-
}
|
|
12252
|
+
}
|
|
12101
12253
|
});
|
|
12102
12254
|
return {
|
|
12103
12255
|
content: [{ type: "text", text: JSON.stringify({ runId, status: "running" }) }]
|
|
@@ -12109,15 +12261,25 @@ function registerSpecTools(server, cwd) {
|
|
|
12109
12261
|
"List spec files in the .dispatch/specs directory.",
|
|
12110
12262
|
{},
|
|
12111
12263
|
async () => {
|
|
12112
|
-
const specsDir =
|
|
12264
|
+
const specsDir = join17(cwd, ".dispatch", "specs");
|
|
12113
12265
|
let files = [];
|
|
12266
|
+
let dirError;
|
|
12114
12267
|
try {
|
|
12115
12268
|
const entries = await readdir2(specsDir);
|
|
12116
12269
|
files = entries.filter((f) => f.endsWith(".md")).sort();
|
|
12270
|
+
} catch (err) {
|
|
12271
|
+
const isNotFound = err instanceof Error && "code" in err && err.code === "ENOENT";
|
|
12272
|
+
if (!isNotFound) {
|
|
12273
|
+
dirError = `Error reading specs directory: ${err instanceof Error ? err.message : String(err)}`;
|
|
12274
|
+
}
|
|
12275
|
+
}
|
|
12276
|
+
let recentRuns = [];
|
|
12277
|
+
try {
|
|
12278
|
+
recentRuns = listSpecRuns(5);
|
|
12117
12279
|
} catch {
|
|
12118
12280
|
}
|
|
12119
12281
|
return {
|
|
12120
|
-
content: [{ type: "text", text: JSON.stringify({ files, specsDir }) }]
|
|
12282
|
+
content: [{ type: "text", text: JSON.stringify({ files, specsDir, recentRuns, ...dirError ? { error: dirError } : {} }) }]
|
|
12121
12283
|
};
|
|
12122
12284
|
}
|
|
12123
12285
|
);
|
|
@@ -12129,7 +12291,7 @@ function registerSpecTools(server, cwd) {
|
|
|
12129
12291
|
},
|
|
12130
12292
|
async (args) => {
|
|
12131
12293
|
const specsDir = resolve4(cwd, ".dispatch", "specs");
|
|
12132
|
-
const candidatePath = args.file.includes("/") || args.file.includes("\\") ? resolve4(specsDir, args.file) :
|
|
12294
|
+
const candidatePath = args.file.includes("/") || args.file.includes("\\") ? resolve4(specsDir, args.file) : join17(specsDir, args.file);
|
|
12133
12295
|
if (!candidatePath.startsWith(specsDir + sep2) && candidatePath !== specsDir) {
|
|
12134
12296
|
return {
|
|
12135
12297
|
content: [{ type: "text", text: `Access denied: path must be inside the specs directory` }],
|
|
@@ -12158,10 +12320,17 @@ function registerSpecTools(server, cwd) {
|
|
|
12158
12320
|
limit: z2.number().int().min(1).max(100).optional().describe("Max results (default 20)")
|
|
12159
12321
|
},
|
|
12160
12322
|
async (args) => {
|
|
12161
|
-
|
|
12162
|
-
|
|
12163
|
-
|
|
12164
|
-
|
|
12323
|
+
try {
|
|
12324
|
+
const runs = listSpecRuns(args.limit ?? 20);
|
|
12325
|
+
return {
|
|
12326
|
+
content: [{ type: "text", text: JSON.stringify(runs) }]
|
|
12327
|
+
};
|
|
12328
|
+
} catch (err) {
|
|
12329
|
+
return {
|
|
12330
|
+
content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }],
|
|
12331
|
+
isError: true
|
|
12332
|
+
};
|
|
12333
|
+
}
|
|
12165
12334
|
}
|
|
12166
12335
|
);
|
|
12167
12336
|
server.tool(
|
|
@@ -12171,26 +12340,33 @@ function registerSpecTools(server, cwd) {
|
|
|
12171
12340
|
runId: z2.string().describe("The runId returned by spec_generate")
|
|
12172
12341
|
},
|
|
12173
12342
|
async (args) => {
|
|
12174
|
-
|
|
12175
|
-
|
|
12343
|
+
try {
|
|
12344
|
+
const run = getSpecRun(args.runId);
|
|
12345
|
+
if (!run) {
|
|
12346
|
+
return {
|
|
12347
|
+
content: [{ type: "text", text: `Run ${args.runId} not found` }],
|
|
12348
|
+
isError: true
|
|
12349
|
+
};
|
|
12350
|
+
}
|
|
12176
12351
|
return {
|
|
12177
|
-
content: [{ type: "text", text:
|
|
12352
|
+
content: [{ type: "text", text: JSON.stringify(run) }]
|
|
12353
|
+
};
|
|
12354
|
+
} catch (err) {
|
|
12355
|
+
return {
|
|
12356
|
+
content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }],
|
|
12178
12357
|
isError: true
|
|
12179
12358
|
};
|
|
12180
12359
|
}
|
|
12181
|
-
return {
|
|
12182
|
-
content: [{ type: "text", text: JSON.stringify(run) }]
|
|
12183
|
-
};
|
|
12184
12360
|
}
|
|
12185
12361
|
);
|
|
12186
12362
|
}
|
|
12187
12363
|
var init_spec2 = __esm({
|
|
12188
12364
|
"src/mcp/tools/spec.ts"() {
|
|
12189
12365
|
"use strict";
|
|
12190
|
-
init_spec_pipeline();
|
|
12191
12366
|
init_manager();
|
|
12192
12367
|
init_interface2();
|
|
12193
12368
|
init_interface();
|
|
12369
|
+
init_fork_run();
|
|
12194
12370
|
}
|
|
12195
12371
|
});
|
|
12196
12372
|
|
|
@@ -12212,64 +12388,20 @@ function registerDispatchTools(server, cwd) {
|
|
|
12212
12388
|
},
|
|
12213
12389
|
async (args) => {
|
|
12214
12390
|
const runId = createRun({ cwd, issueIds: args.issueIds });
|
|
12215
|
-
|
|
12216
|
-
|
|
12217
|
-
|
|
12218
|
-
|
|
12219
|
-
|
|
12220
|
-
|
|
12221
|
-
|
|
12222
|
-
|
|
12223
|
-
|
|
12224
|
-
|
|
12225
|
-
|
|
12226
|
-
|
|
12227
|
-
|
|
12228
|
-
|
|
12229
|
-
retries: args.retries,
|
|
12230
|
-
progressCallback: (event) => {
|
|
12231
|
-
switch (event.type) {
|
|
12232
|
-
case "task_start":
|
|
12233
|
-
emitLog(runId, `Task started: ${event.taskText}`);
|
|
12234
|
-
updateTaskStatus(runId, event.taskId, "running");
|
|
12235
|
-
break;
|
|
12236
|
-
case "task_done":
|
|
12237
|
-
emitLog(runId, `Task done: ${event.taskText}`);
|
|
12238
|
-
updateTaskStatus(runId, event.taskId, "success");
|
|
12239
|
-
break;
|
|
12240
|
-
case "task_failed":
|
|
12241
|
-
emitLog(runId, `Task failed: ${event.taskText} \u2014 ${event.error}`, "error");
|
|
12242
|
-
updateTaskStatus(runId, event.taskId, "failed", { error: event.error });
|
|
12243
|
-
break;
|
|
12244
|
-
case "phase_change":
|
|
12245
|
-
emitLog(runId, event.message ?? `Phase: ${event.phase}`);
|
|
12246
|
-
break;
|
|
12247
|
-
case "log":
|
|
12248
|
-
emitLog(runId, event.message);
|
|
12249
|
-
break;
|
|
12250
|
-
default: {
|
|
12251
|
-
const _exhaustive = event;
|
|
12252
|
-
void _exhaustive;
|
|
12253
|
-
}
|
|
12254
|
-
}
|
|
12255
|
-
updateRunCounters(
|
|
12256
|
-
runId,
|
|
12257
|
-
0,
|
|
12258
|
-
// we'll update with final counts at the end
|
|
12259
|
-
0,
|
|
12260
|
-
0
|
|
12261
|
-
);
|
|
12262
|
-
}
|
|
12263
|
-
});
|
|
12264
|
-
updateRunCounters(runId, result.total, result.completed, result.failed);
|
|
12265
|
-
finishRun(runId, result.failed > 0 ? "failed" : "completed");
|
|
12266
|
-
emitLog(runId, `Dispatch complete: ${result.completed}/${result.total} tasks succeeded`);
|
|
12267
|
-
} catch (err) {
|
|
12268
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
12269
|
-
finishRun(runId, "failed", msg);
|
|
12270
|
-
emitLog(runId, `Dispatch error: ${msg}`, "error");
|
|
12271
|
-
}
|
|
12272
|
-
})();
|
|
12391
|
+
forkDispatchRun(runId, server, {
|
|
12392
|
+
type: "dispatch",
|
|
12393
|
+
cwd,
|
|
12394
|
+
opts: {
|
|
12395
|
+
issueIds: args.issueIds,
|
|
12396
|
+
dryRun: false,
|
|
12397
|
+
provider: args.provider ?? "opencode",
|
|
12398
|
+
source: args.source,
|
|
12399
|
+
concurrency: args.concurrency ?? 1,
|
|
12400
|
+
noPlan: args.noPlan,
|
|
12401
|
+
noBranch: args.noBranch,
|
|
12402
|
+
noWorktree: args.noWorktree,
|
|
12403
|
+
retries: args.retries
|
|
12404
|
+
}
|
|
12273
12405
|
});
|
|
12274
12406
|
return {
|
|
12275
12407
|
content: [{ type: "text", text: JSON.stringify({ runId, status: "running" }) }]
|
|
@@ -12310,12 +12442,13 @@ var init_dispatch = __esm({
|
|
|
12310
12442
|
init_manager();
|
|
12311
12443
|
init_interface2();
|
|
12312
12444
|
init_interface();
|
|
12445
|
+
init_fork_run();
|
|
12313
12446
|
}
|
|
12314
12447
|
});
|
|
12315
12448
|
|
|
12316
12449
|
// src/mcp/tools/monitor.ts
|
|
12317
12450
|
import { z as z4 } from "zod";
|
|
12318
|
-
import { join as
|
|
12451
|
+
import { join as join18 } from "path";
|
|
12319
12452
|
function registerMonitorTools(server, cwd) {
|
|
12320
12453
|
server.tool(
|
|
12321
12454
|
"status_get",
|
|
@@ -12324,17 +12457,24 @@ function registerMonitorTools(server, cwd) {
|
|
|
12324
12457
|
runId: z4.string().describe("The runId returned by dispatch_run or spec_generate")
|
|
12325
12458
|
},
|
|
12326
12459
|
async (args) => {
|
|
12327
|
-
|
|
12328
|
-
|
|
12460
|
+
try {
|
|
12461
|
+
const run = getRun(args.runId);
|
|
12462
|
+
if (!run) {
|
|
12463
|
+
return {
|
|
12464
|
+
content: [{ type: "text", text: `Run ${args.runId} not found` }],
|
|
12465
|
+
isError: true
|
|
12466
|
+
};
|
|
12467
|
+
}
|
|
12468
|
+
const tasks = getTasksForRun(args.runId);
|
|
12329
12469
|
return {
|
|
12330
|
-
content: [{ type: "text", text:
|
|
12470
|
+
content: [{ type: "text", text: JSON.stringify({ run, tasks }) }]
|
|
12471
|
+
};
|
|
12472
|
+
} catch (err) {
|
|
12473
|
+
return {
|
|
12474
|
+
content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }],
|
|
12331
12475
|
isError: true
|
|
12332
12476
|
};
|
|
12333
12477
|
}
|
|
12334
|
-
const tasks = getTasksForRun(args.runId);
|
|
12335
|
-
return {
|
|
12336
|
-
content: [{ type: "text", text: JSON.stringify({ run, tasks }) }]
|
|
12337
|
-
};
|
|
12338
12478
|
}
|
|
12339
12479
|
);
|
|
12340
12480
|
server.tool(
|
|
@@ -12345,10 +12485,17 @@ function registerMonitorTools(server, cwd) {
|
|
|
12345
12485
|
limit: z4.number().int().min(1).max(100).optional().describe("Max results (default 20)")
|
|
12346
12486
|
},
|
|
12347
12487
|
async (args) => {
|
|
12348
|
-
|
|
12349
|
-
|
|
12350
|
-
|
|
12351
|
-
|
|
12488
|
+
try {
|
|
12489
|
+
const runs = args.status ? listRunsByStatus(args.status, args.limit ?? 20) : listRuns(args.limit ?? 20);
|
|
12490
|
+
return {
|
|
12491
|
+
content: [{ type: "text", text: JSON.stringify(runs) }]
|
|
12492
|
+
};
|
|
12493
|
+
} catch (err) {
|
|
12494
|
+
return {
|
|
12495
|
+
content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }],
|
|
12496
|
+
isError: true
|
|
12497
|
+
};
|
|
12498
|
+
}
|
|
12352
12499
|
}
|
|
12353
12500
|
);
|
|
12354
12501
|
server.tool(
|
|
@@ -12364,7 +12511,7 @@ function registerMonitorTools(server, cwd) {
|
|
|
12364
12511
|
},
|
|
12365
12512
|
async (args) => {
|
|
12366
12513
|
try {
|
|
12367
|
-
const config = await loadConfig(
|
|
12514
|
+
const config = await loadConfig(join18(cwd, ".dispatch"));
|
|
12368
12515
|
const sourceName = args.source ?? config.source;
|
|
12369
12516
|
if (!sourceName) {
|
|
12370
12517
|
return {
|
|
@@ -12409,7 +12556,7 @@ function registerMonitorTools(server, cwd) {
|
|
|
12409
12556
|
},
|
|
12410
12557
|
async (args) => {
|
|
12411
12558
|
try {
|
|
12412
|
-
const config = await loadConfig(
|
|
12559
|
+
const config = await loadConfig(join18(cwd, ".dispatch"));
|
|
12413
12560
|
const sourceName = args.source ?? config.source;
|
|
12414
12561
|
if (!sourceName) {
|
|
12415
12562
|
return {
|
|
@@ -12457,7 +12604,7 @@ var init_monitor = __esm({
|
|
|
12457
12604
|
|
|
12458
12605
|
// src/mcp/tools/recovery.ts
|
|
12459
12606
|
import { z as z5 } from "zod";
|
|
12460
|
-
import { join as
|
|
12607
|
+
import { join as join19 } from "path";
|
|
12461
12608
|
function registerRecoveryTools(server, cwd) {
|
|
12462
12609
|
server.tool(
|
|
12463
12610
|
"run_retry",
|
|
@@ -12477,63 +12624,25 @@ function registerRecoveryTools(server, cwd) {
|
|
|
12477
12624
|
}
|
|
12478
12625
|
const tasks = getTasksForRun(args.runId);
|
|
12479
12626
|
const failedTasks = tasks.filter((t) => t.status === "failed");
|
|
12480
|
-
if (failedTasks.length === 0) {
|
|
12627
|
+
if (failedTasks.length === 0 && originalRun.status !== "failed") {
|
|
12481
12628
|
return {
|
|
12482
12629
|
content: [{ type: "text", text: JSON.stringify({ message: "No failed tasks found", originalRunId: args.runId }) }]
|
|
12483
12630
|
};
|
|
12484
12631
|
}
|
|
12485
|
-
const config = await loadConfig(
|
|
12632
|
+
const config = await loadConfig(join19(cwd, ".dispatch"));
|
|
12486
12633
|
const issueIds = issueIdsSchema.parse(JSON.parse(originalRun.issueIds));
|
|
12487
12634
|
const newRunId = createRun({ cwd, issueIds });
|
|
12488
|
-
|
|
12489
|
-
|
|
12490
|
-
|
|
12491
|
-
|
|
12492
|
-
|
|
12493
|
-
|
|
12494
|
-
|
|
12495
|
-
|
|
12496
|
-
|
|
12497
|
-
|
|
12498
|
-
|
|
12499
|
-
force: true,
|
|
12500
|
-
// re-run even previously completed tasks? No — force just skips run-state check
|
|
12501
|
-
progressCallback: (event) => {
|
|
12502
|
-
switch (event.type) {
|
|
12503
|
-
case "task_start":
|
|
12504
|
-
emitLog(newRunId, `Task started: ${event.taskText}`);
|
|
12505
|
-
updateTaskStatus(newRunId, event.taskId, "running");
|
|
12506
|
-
break;
|
|
12507
|
-
case "task_done":
|
|
12508
|
-
emitLog(newRunId, `Task done: ${event.taskText}`);
|
|
12509
|
-
updateTaskStatus(newRunId, event.taskId, "success");
|
|
12510
|
-
break;
|
|
12511
|
-
case "task_failed":
|
|
12512
|
-
emitLog(newRunId, `Task failed: ${event.taskText} \u2014 ${event.error}`, "error");
|
|
12513
|
-
updateTaskStatus(newRunId, event.taskId, "failed", { error: event.error });
|
|
12514
|
-
break;
|
|
12515
|
-
case "phase_change":
|
|
12516
|
-
emitLog(newRunId, event.message ?? `Phase: ${event.phase}`);
|
|
12517
|
-
break;
|
|
12518
|
-
case "log":
|
|
12519
|
-
emitLog(newRunId, event.message);
|
|
12520
|
-
break;
|
|
12521
|
-
default: {
|
|
12522
|
-
const _exhaustive = event;
|
|
12523
|
-
void _exhaustive;
|
|
12524
|
-
}
|
|
12525
|
-
}
|
|
12526
|
-
}
|
|
12527
|
-
});
|
|
12528
|
-
updateRunCounters(newRunId, result.total, result.completed, result.failed);
|
|
12529
|
-
finishRun(newRunId, result.failed > 0 ? "failed" : "completed");
|
|
12530
|
-
emitLog(newRunId, `Retry complete: ${result.completed}/${result.total} succeeded`);
|
|
12531
|
-
} catch (err) {
|
|
12532
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
12533
|
-
finishRun(newRunId, "failed", msg);
|
|
12534
|
-
emitLog(newRunId, `Retry error: ${msg}`, "error");
|
|
12535
|
-
}
|
|
12536
|
-
})();
|
|
12635
|
+
forkDispatchRun(newRunId, server, {
|
|
12636
|
+
type: "dispatch",
|
|
12637
|
+
cwd,
|
|
12638
|
+
opts: {
|
|
12639
|
+
issueIds,
|
|
12640
|
+
dryRun: false,
|
|
12641
|
+
provider: args.provider ?? config.provider ?? "opencode",
|
|
12642
|
+
source: config.source,
|
|
12643
|
+
concurrency: args.concurrency ?? config.concurrency ?? 1,
|
|
12644
|
+
force: true
|
|
12645
|
+
}
|
|
12537
12646
|
});
|
|
12538
12647
|
return {
|
|
12539
12648
|
content: [{ type: "text", text: JSON.stringify({ runId: newRunId, status: "running", originalRunId: args.runId }) }]
|
|
@@ -12564,54 +12673,20 @@ function registerRecoveryTools(server, cwd) {
|
|
|
12564
12673
|
isError: true
|
|
12565
12674
|
};
|
|
12566
12675
|
}
|
|
12567
|
-
const config = await loadConfig(
|
|
12676
|
+
const config = await loadConfig(join19(cwd, ".dispatch"));
|
|
12568
12677
|
const issueIds = issueIdsSchema.parse(JSON.parse(originalRun.issueIds));
|
|
12569
12678
|
const newRunId = createRun({ cwd, issueIds });
|
|
12570
|
-
|
|
12571
|
-
|
|
12572
|
-
|
|
12573
|
-
|
|
12574
|
-
|
|
12575
|
-
|
|
12576
|
-
|
|
12577
|
-
|
|
12578
|
-
|
|
12579
|
-
|
|
12580
|
-
|
|
12581
|
-
force: true,
|
|
12582
|
-
progressCallback: (event) => {
|
|
12583
|
-
switch (event.type) {
|
|
12584
|
-
case "task_start":
|
|
12585
|
-
emitLog(newRunId, `Task started: ${event.taskText}`);
|
|
12586
|
-
break;
|
|
12587
|
-
case "task_done":
|
|
12588
|
-
emitLog(newRunId, `Task done: ${event.taskText}`);
|
|
12589
|
-
break;
|
|
12590
|
-
case "task_failed":
|
|
12591
|
-
emitLog(newRunId, `Task failed: ${event.taskText} \u2014 ${event.error}`, "error");
|
|
12592
|
-
break;
|
|
12593
|
-
case "phase_change":
|
|
12594
|
-
emitLog(newRunId, event.message ?? `Phase: ${event.phase}`);
|
|
12595
|
-
break;
|
|
12596
|
-
case "log":
|
|
12597
|
-
emitLog(newRunId, event.message);
|
|
12598
|
-
break;
|
|
12599
|
-
default: {
|
|
12600
|
-
const _exhaustive = event;
|
|
12601
|
-
void _exhaustive;
|
|
12602
|
-
}
|
|
12603
|
-
}
|
|
12604
|
-
}
|
|
12605
|
-
});
|
|
12606
|
-
updateRunCounters(newRunId, result.total, result.completed, result.failed);
|
|
12607
|
-
finishRun(newRunId, result.failed > 0 ? "failed" : "completed");
|
|
12608
|
-
emitLog(newRunId, `Task retry complete`);
|
|
12609
|
-
} catch (err) {
|
|
12610
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
12611
|
-
finishRun(newRunId, "failed", msg);
|
|
12612
|
-
emitLog(newRunId, `Task retry error: ${msg}`, "error");
|
|
12613
|
-
}
|
|
12614
|
-
})();
|
|
12679
|
+
forkDispatchRun(newRunId, server, {
|
|
12680
|
+
type: "dispatch",
|
|
12681
|
+
cwd,
|
|
12682
|
+
opts: {
|
|
12683
|
+
issueIds,
|
|
12684
|
+
dryRun: false,
|
|
12685
|
+
provider: args.provider ?? config.provider ?? "opencode",
|
|
12686
|
+
source: config.source,
|
|
12687
|
+
concurrency: 1,
|
|
12688
|
+
force: true
|
|
12689
|
+
}
|
|
12615
12690
|
});
|
|
12616
12691
|
return {
|
|
12617
12692
|
content: [{ type: "text", text: JSON.stringify({ runId: newRunId, status: "running", taskId: args.taskId }) }]
|
|
@@ -12624,22 +12699,22 @@ var init_recovery = __esm({
|
|
|
12624
12699
|
"src/mcp/tools/recovery.ts"() {
|
|
12625
12700
|
"use strict";
|
|
12626
12701
|
init_manager();
|
|
12627
|
-
init_runner();
|
|
12628
12702
|
init_config();
|
|
12629
12703
|
init_interface2();
|
|
12704
|
+
init_fork_run();
|
|
12630
12705
|
issueIdsSchema = z5.array(z5.string());
|
|
12631
12706
|
}
|
|
12632
12707
|
});
|
|
12633
12708
|
|
|
12634
12709
|
// src/mcp/tools/config.ts
|
|
12635
|
-
import { join as
|
|
12710
|
+
import { join as join20 } from "path";
|
|
12636
12711
|
function registerConfigTools(server, cwd) {
|
|
12637
12712
|
server.tool(
|
|
12638
12713
|
"config_get",
|
|
12639
12714
|
"Get the current Dispatch configuration from .dispatch/config.json.",
|
|
12640
12715
|
{},
|
|
12641
12716
|
async () => {
|
|
12642
|
-
const config = await loadConfig(
|
|
12717
|
+
const config = await loadConfig(join20(cwd, ".dispatch"));
|
|
12643
12718
|
const { nextIssueId: _, ...safeConfig } = config;
|
|
12644
12719
|
return {
|
|
12645
12720
|
content: [{ type: "text", text: JSON.stringify(safeConfig) }]
|
|
@@ -12798,6 +12873,17 @@ async function createMcpServer(opts) {
|
|
|
12798
12873
|
}
|
|
12799
12874
|
};
|
|
12800
12875
|
}
|
|
12876
|
+
function wireRunLogs(runId, server) {
|
|
12877
|
+
addLogCallback(runId, (message, level) => {
|
|
12878
|
+
server.sendLoggingMessage({
|
|
12879
|
+
level: level === "error" ? "error" : level === "warn" ? "warning" : "info",
|
|
12880
|
+
logger: `dispatch.run.${runId}`,
|
|
12881
|
+
data: message
|
|
12882
|
+
}).catch((err) => {
|
|
12883
|
+
console.error("[dispatch-mcp] sendLoggingMessage error:", err);
|
|
12884
|
+
});
|
|
12885
|
+
});
|
|
12886
|
+
}
|
|
12801
12887
|
var init_server = __esm({
|
|
12802
12888
|
"src/mcp/server.ts"() {
|
|
12803
12889
|
"use strict";
|
|
@@ -12881,7 +12967,7 @@ init_cleanup();
|
|
|
12881
12967
|
init_providers();
|
|
12882
12968
|
init_datasources();
|
|
12883
12969
|
init_config();
|
|
12884
|
-
import { resolve as resolve5, join as
|
|
12970
|
+
import { resolve as resolve5, join as join21 } from "path";
|
|
12885
12971
|
import { Command, Option, CommanderError } from "commander";
|
|
12886
12972
|
var MAX_CONCURRENCY = CONFIG_BOUNDS.concurrency.max;
|
|
12887
12973
|
var HELP = `
|
|
@@ -13174,7 +13260,7 @@ async function main() {
|
|
|
13174
13260
|
}
|
|
13175
13261
|
throw err;
|
|
13176
13262
|
}
|
|
13177
|
-
const configDir =
|
|
13263
|
+
const configDir = join21(configProgram.opts().cwd ?? process.cwd(), ".dispatch");
|
|
13178
13264
|
await handleConfigCommand(rawArgv.slice(1), configDir);
|
|
13179
13265
|
process.exit(0);
|
|
13180
13266
|
}
|
|
@@ -13219,7 +13305,7 @@ async function main() {
|
|
|
13219
13305
|
process.exit(0);
|
|
13220
13306
|
}
|
|
13221
13307
|
if (args.version) {
|
|
13222
|
-
console.log(`dispatch v${"1.5.0-beta.
|
|
13308
|
+
console.log(`dispatch v${"1.5.0-beta.30f06ab"}`);
|
|
13223
13309
|
process.exit(0);
|
|
13224
13310
|
}
|
|
13225
13311
|
const orchestrator = await boot9({ cwd: args.cwd });
|