@pruddiman/dispatch 1.4.0 → 1.4.1
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 +403 -325
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -210,6 +210,27 @@ var init_logger = __esm({
|
|
|
210
210
|
}
|
|
211
211
|
});
|
|
212
212
|
|
|
213
|
+
// src/helpers/cleanup.ts
|
|
214
|
+
function registerCleanup(fn) {
|
|
215
|
+
cleanups.push(fn);
|
|
216
|
+
}
|
|
217
|
+
async function runCleanup() {
|
|
218
|
+
const fns = cleanups.splice(0);
|
|
219
|
+
for (const fn of fns) {
|
|
220
|
+
try {
|
|
221
|
+
await fn();
|
|
222
|
+
} catch {
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
var cleanups;
|
|
227
|
+
var init_cleanup = __esm({
|
|
228
|
+
"src/helpers/cleanup.ts"() {
|
|
229
|
+
"use strict";
|
|
230
|
+
cleanups = [];
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
|
|
213
234
|
// src/helpers/guards.ts
|
|
214
235
|
function hasProperty(value, key) {
|
|
215
236
|
return typeof value === "object" && value !== null && Object.prototype.hasOwnProperty.call(value, key);
|
|
@@ -592,7 +613,7 @@ var init_copilot = __esm({
|
|
|
592
613
|
});
|
|
593
614
|
|
|
594
615
|
// src/providers/claude.ts
|
|
595
|
-
import { randomUUID } from "crypto";
|
|
616
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
596
617
|
import { unstable_v2_createSession } from "@anthropic-ai/claude-agent-sdk";
|
|
597
618
|
async function listModels3(_opts) {
|
|
598
619
|
return [
|
|
@@ -615,7 +636,7 @@ async function boot3(opts) {
|
|
|
615
636
|
try {
|
|
616
637
|
const sessionOpts = { model, permissionMode: "acceptEdits", ...cwd ? { cwd } : {} };
|
|
617
638
|
const session = unstable_v2_createSession(sessionOpts);
|
|
618
|
-
const sessionId =
|
|
639
|
+
const sessionId = randomUUID2();
|
|
619
640
|
sessions.set(sessionId, session);
|
|
620
641
|
log.debug(`Session created: ${sessionId}`);
|
|
621
642
|
return sessionId;
|
|
@@ -667,7 +688,7 @@ var init_claude = __esm({
|
|
|
667
688
|
});
|
|
668
689
|
|
|
669
690
|
// src/providers/codex.ts
|
|
670
|
-
import { randomUUID as
|
|
691
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
671
692
|
async function loadAgentLoop() {
|
|
672
693
|
return import("@openai/codex");
|
|
673
694
|
}
|
|
@@ -689,7 +710,7 @@ async function boot4(opts) {
|
|
|
689
710
|
async createSession() {
|
|
690
711
|
log.debug("Creating Codex session...");
|
|
691
712
|
try {
|
|
692
|
-
const sessionId =
|
|
713
|
+
const sessionId = randomUUID3();
|
|
693
714
|
const agent = new AgentLoop({
|
|
694
715
|
model,
|
|
695
716
|
config: { model, instructions: "" },
|
|
@@ -756,11 +777,11 @@ var init_codex = __esm({
|
|
|
756
777
|
});
|
|
757
778
|
|
|
758
779
|
// src/providers/detect.ts
|
|
759
|
-
import { execFile as
|
|
760
|
-
import { promisify as
|
|
780
|
+
import { execFile as execFile8 } from "child_process";
|
|
781
|
+
import { promisify as promisify8 } from "util";
|
|
761
782
|
async function checkProviderInstalled(name) {
|
|
762
783
|
try {
|
|
763
|
-
await
|
|
784
|
+
await exec8(PROVIDER_BINARIES[name], ["--version"], {
|
|
764
785
|
shell: process.platform === "win32",
|
|
765
786
|
timeout: DETECTION_TIMEOUT_MS
|
|
766
787
|
});
|
|
@@ -769,11 +790,11 @@ async function checkProviderInstalled(name) {
|
|
|
769
790
|
return false;
|
|
770
791
|
}
|
|
771
792
|
}
|
|
772
|
-
var
|
|
793
|
+
var exec8, DETECTION_TIMEOUT_MS, PROVIDER_BINARIES;
|
|
773
794
|
var init_detect = __esm({
|
|
774
795
|
"src/providers/detect.ts"() {
|
|
775
796
|
"use strict";
|
|
776
|
-
|
|
797
|
+
exec8 = promisify8(execFile8);
|
|
777
798
|
DETECTION_TIMEOUT_MS = 5e3;
|
|
778
799
|
PROVIDER_BINARIES = {
|
|
779
800
|
opencode: "opencode",
|
|
@@ -857,27 +878,6 @@ var init_environment = __esm({
|
|
|
857
878
|
}
|
|
858
879
|
});
|
|
859
880
|
|
|
860
|
-
// src/helpers/cleanup.ts
|
|
861
|
-
function registerCleanup(fn) {
|
|
862
|
-
cleanups.push(fn);
|
|
863
|
-
}
|
|
864
|
-
async function runCleanup() {
|
|
865
|
-
const fns = cleanups.splice(0);
|
|
866
|
-
for (const fn of fns) {
|
|
867
|
-
try {
|
|
868
|
-
await fn();
|
|
869
|
-
} catch {
|
|
870
|
-
}
|
|
871
|
-
}
|
|
872
|
-
}
|
|
873
|
-
var cleanups;
|
|
874
|
-
var init_cleanup = __esm({
|
|
875
|
-
"src/helpers/cleanup.ts"() {
|
|
876
|
-
"use strict";
|
|
877
|
-
cleanups = [];
|
|
878
|
-
}
|
|
879
|
-
});
|
|
880
|
-
|
|
881
881
|
// src/orchestrator/fix-tests-pipeline.ts
|
|
882
882
|
var fix_tests_pipeline_exports = {};
|
|
883
883
|
__export(fix_tests_pipeline_exports, {
|
|
@@ -1442,7 +1442,6 @@ var datasource2 = {
|
|
|
1442
1442
|
"json"
|
|
1443
1443
|
];
|
|
1444
1444
|
if (opts.org) batchArgs.push("--org", opts.org);
|
|
1445
|
-
if (opts.project) batchArgs.push("--project", opts.project);
|
|
1446
1445
|
const { stdout: batchStdout } = await exec2("az", batchArgs, {
|
|
1447
1446
|
cwd: opts.cwd || process.cwd(),
|
|
1448
1447
|
shell: process.platform === "win32"
|
|
@@ -1487,9 +1486,6 @@ var datasource2 = {
|
|
|
1487
1486
|
if (opts.org) {
|
|
1488
1487
|
args.push("--org", opts.org);
|
|
1489
1488
|
}
|
|
1490
|
-
if (opts.project) {
|
|
1491
|
-
args.push("--project", opts.project);
|
|
1492
|
-
}
|
|
1493
1489
|
const { stdout } = await exec2("az", args, {
|
|
1494
1490
|
cwd: opts.cwd || process.cwd(),
|
|
1495
1491
|
shell: process.platform === "win32"
|
|
@@ -1516,7 +1512,6 @@ var datasource2 = {
|
|
|
1516
1512
|
body
|
|
1517
1513
|
];
|
|
1518
1514
|
if (opts.org) args.push("--org", opts.org);
|
|
1519
|
-
if (opts.project) args.push("--project", opts.project);
|
|
1520
1515
|
await exec2("az", args, { cwd: opts.cwd || process.cwd(), shell: process.platform === "win32" });
|
|
1521
1516
|
},
|
|
1522
1517
|
async close(issueId, opts = {}) {
|
|
@@ -1532,7 +1527,6 @@ var datasource2 = {
|
|
|
1532
1527
|
"json"
|
|
1533
1528
|
];
|
|
1534
1529
|
if (opts.org) showArgs.push("--org", opts.org);
|
|
1535
|
-
if (opts.project) showArgs.push("--project", opts.project);
|
|
1536
1530
|
const { stdout } = await exec2("az", showArgs, {
|
|
1537
1531
|
cwd: opts.cwd || process.cwd(),
|
|
1538
1532
|
shell: process.platform === "win32"
|
|
@@ -1555,7 +1549,6 @@ var datasource2 = {
|
|
|
1555
1549
|
state
|
|
1556
1550
|
];
|
|
1557
1551
|
if (opts.org) args.push("--org", opts.org);
|
|
1558
|
-
if (opts.project) args.push("--project", opts.project);
|
|
1559
1552
|
await exec2("az", args, { cwd: opts.cwd || process.cwd(), shell: process.platform === "win32" });
|
|
1560
1553
|
},
|
|
1561
1554
|
async create(title, body, opts = {}) {
|
|
@@ -1754,9 +1747,6 @@ async function fetchComments(workItemId, opts) {
|
|
|
1754
1747
|
if (opts.org) {
|
|
1755
1748
|
args.push("--org", opts.org);
|
|
1756
1749
|
}
|
|
1757
|
-
if (opts.project) {
|
|
1758
|
-
args.push("--project", opts.project);
|
|
1759
|
-
}
|
|
1760
1750
|
const { stdout } = await exec2("az", args, {
|
|
1761
1751
|
cwd: opts.cwd || process.cwd(),
|
|
1762
1752
|
shell: process.platform === "win32"
|
|
@@ -2120,7 +2110,243 @@ async function resolveSource(issues, issueSource, cwd) {
|
|
|
2120
2110
|
return null;
|
|
2121
2111
|
}
|
|
2122
2112
|
|
|
2113
|
+
// src/orchestrator/datasource-helpers.ts
|
|
2114
|
+
init_logger();
|
|
2115
|
+
import { basename as basename2, join as join3 } from "path";
|
|
2116
|
+
import { mkdtemp, writeFile as writeFile2 } from "fs/promises";
|
|
2117
|
+
import { tmpdir } from "os";
|
|
2118
|
+
import { execFile as execFile5 } from "child_process";
|
|
2119
|
+
import { promisify as promisify5 } from "util";
|
|
2120
|
+
var exec5 = promisify5(execFile5);
|
|
2121
|
+
function parseIssueFilename(filePath) {
|
|
2122
|
+
const filename = basename2(filePath);
|
|
2123
|
+
const match = /^(\d+)-(.+)\.md$/.exec(filename);
|
|
2124
|
+
if (!match) return null;
|
|
2125
|
+
return { issueId: match[1], slug: match[2] };
|
|
2126
|
+
}
|
|
2127
|
+
async function fetchItemsById(issueIds, datasource4, fetchOpts) {
|
|
2128
|
+
const ids = issueIds.flatMap(
|
|
2129
|
+
(id) => id.split(",").map((s) => s.trim()).filter(Boolean)
|
|
2130
|
+
);
|
|
2131
|
+
const items = [];
|
|
2132
|
+
for (const id of ids) {
|
|
2133
|
+
try {
|
|
2134
|
+
const item = await datasource4.fetch(id, fetchOpts);
|
|
2135
|
+
items.push(item);
|
|
2136
|
+
} catch (err) {
|
|
2137
|
+
const prefix = id.includes("/") || id.includes("\\") || id.endsWith(".md") ? "" : "#";
|
|
2138
|
+
log.warn(`Could not fetch issue ${prefix}${id}: ${log.formatErrorChain(err)}`);
|
|
2139
|
+
}
|
|
2140
|
+
}
|
|
2141
|
+
return items;
|
|
2142
|
+
}
|
|
2143
|
+
async function writeItemsToTempDir(items) {
|
|
2144
|
+
const tempDir = await mkdtemp(join3(tmpdir(), "dispatch-"));
|
|
2145
|
+
const files = [];
|
|
2146
|
+
const issueDetailsByFile = /* @__PURE__ */ new Map();
|
|
2147
|
+
for (const item of items) {
|
|
2148
|
+
const slug = slugify(item.title, MAX_SLUG_LENGTH);
|
|
2149
|
+
const filename = `${item.number}-${slug}.md`;
|
|
2150
|
+
const filepath = join3(tempDir, filename);
|
|
2151
|
+
await writeFile2(filepath, item.body, "utf-8");
|
|
2152
|
+
files.push(filepath);
|
|
2153
|
+
issueDetailsByFile.set(filepath, item);
|
|
2154
|
+
}
|
|
2155
|
+
files.sort((a, b) => {
|
|
2156
|
+
const numA = parseInt(basename2(a).match(/^(\d+)/)?.[1] ?? "0", 10);
|
|
2157
|
+
const numB = parseInt(basename2(b).match(/^(\d+)/)?.[1] ?? "0", 10);
|
|
2158
|
+
if (numA !== numB) return numA - numB;
|
|
2159
|
+
return a.localeCompare(b);
|
|
2160
|
+
});
|
|
2161
|
+
return { files, issueDetailsByFile };
|
|
2162
|
+
}
|
|
2163
|
+
async function getCommitSummaries(defaultBranch, cwd) {
|
|
2164
|
+
try {
|
|
2165
|
+
const { stdout } = await exec5(
|
|
2166
|
+
"git",
|
|
2167
|
+
["log", `${defaultBranch}..HEAD`, "--pretty=format:%s"],
|
|
2168
|
+
{ cwd, shell: process.platform === "win32" }
|
|
2169
|
+
);
|
|
2170
|
+
return stdout.trim().split("\n").filter(Boolean);
|
|
2171
|
+
} catch {
|
|
2172
|
+
return [];
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
async function getBranchDiff(defaultBranch, cwd) {
|
|
2176
|
+
try {
|
|
2177
|
+
const { stdout } = await exec5(
|
|
2178
|
+
"git",
|
|
2179
|
+
["diff", `${defaultBranch}..HEAD`],
|
|
2180
|
+
{ cwd, maxBuffer: 10 * 1024 * 1024, shell: process.platform === "win32" }
|
|
2181
|
+
);
|
|
2182
|
+
return stdout;
|
|
2183
|
+
} catch {
|
|
2184
|
+
return "";
|
|
2185
|
+
}
|
|
2186
|
+
}
|
|
2187
|
+
async function squashBranchCommits(defaultBranch, message, cwd) {
|
|
2188
|
+
const { stdout } = await exec5(
|
|
2189
|
+
"git",
|
|
2190
|
+
["merge-base", defaultBranch, "HEAD"],
|
|
2191
|
+
{ cwd, shell: process.platform === "win32" }
|
|
2192
|
+
);
|
|
2193
|
+
const mergeBase = stdout.trim();
|
|
2194
|
+
await exec5("git", ["reset", "--soft", mergeBase], { cwd, shell: process.platform === "win32" });
|
|
2195
|
+
await exec5("git", ["commit", "-m", message], { cwd, shell: process.platform === "win32" });
|
|
2196
|
+
}
|
|
2197
|
+
async function buildPrBody(details, tasks, results, defaultBranch, datasourceName, cwd) {
|
|
2198
|
+
const sections = [];
|
|
2199
|
+
const commits = await getCommitSummaries(defaultBranch, cwd);
|
|
2200
|
+
if (commits.length > 0) {
|
|
2201
|
+
sections.push("## Summary\n");
|
|
2202
|
+
for (const commit of commits) {
|
|
2203
|
+
sections.push(`- ${commit}`);
|
|
2204
|
+
}
|
|
2205
|
+
sections.push("");
|
|
2206
|
+
}
|
|
2207
|
+
const taskResults = new Map(
|
|
2208
|
+
results.filter((r) => tasks.includes(r.task)).map((r) => [r.task, r])
|
|
2209
|
+
);
|
|
2210
|
+
const completedTasks = tasks.filter((t) => taskResults.get(t)?.success);
|
|
2211
|
+
const failedTasks = tasks.filter((t) => {
|
|
2212
|
+
const r = taskResults.get(t);
|
|
2213
|
+
return r && !r.success;
|
|
2214
|
+
});
|
|
2215
|
+
if (completedTasks.length > 0 || failedTasks.length > 0) {
|
|
2216
|
+
sections.push("## Tasks\n");
|
|
2217
|
+
for (const task of completedTasks) {
|
|
2218
|
+
sections.push(`- [x] ${task.text}`);
|
|
2219
|
+
}
|
|
2220
|
+
for (const task of failedTasks) {
|
|
2221
|
+
sections.push(`- [ ] ${task.text}`);
|
|
2222
|
+
}
|
|
2223
|
+
sections.push("");
|
|
2224
|
+
}
|
|
2225
|
+
if (details.labels.length > 0) {
|
|
2226
|
+
sections.push(`**Labels:** ${details.labels.join(", ")}
|
|
2227
|
+
`);
|
|
2228
|
+
}
|
|
2229
|
+
if (datasourceName === "github") {
|
|
2230
|
+
sections.push(`Closes #${details.number}`);
|
|
2231
|
+
} else if (datasourceName === "azdevops") {
|
|
2232
|
+
sections.push(`Resolves AB#${details.number}`);
|
|
2233
|
+
}
|
|
2234
|
+
return sections.join("\n");
|
|
2235
|
+
}
|
|
2236
|
+
async function buildPrTitle(issueTitle, defaultBranch, cwd) {
|
|
2237
|
+
const commits = await getCommitSummaries(defaultBranch, cwd);
|
|
2238
|
+
if (commits.length === 0) {
|
|
2239
|
+
return issueTitle;
|
|
2240
|
+
}
|
|
2241
|
+
if (commits.length === 1) {
|
|
2242
|
+
return commits[0];
|
|
2243
|
+
}
|
|
2244
|
+
return `${commits[commits.length - 1]} (+${commits.length - 1} more)`;
|
|
2245
|
+
}
|
|
2246
|
+
function buildFeaturePrTitle(featureBranchName, issues) {
|
|
2247
|
+
if (issues.length === 1) {
|
|
2248
|
+
return issues[0].title;
|
|
2249
|
+
}
|
|
2250
|
+
const issueRefs = issues.map((d) => `#${d.number}`).join(", ");
|
|
2251
|
+
return `feat: ${featureBranchName} (${issueRefs})`;
|
|
2252
|
+
}
|
|
2253
|
+
function buildFeaturePrBody(issues, tasks, results, datasourceName) {
|
|
2254
|
+
const sections = [];
|
|
2255
|
+
sections.push("## Issues\n");
|
|
2256
|
+
for (const issue of issues) {
|
|
2257
|
+
sections.push(`- #${issue.number}: ${issue.title}`);
|
|
2258
|
+
}
|
|
2259
|
+
sections.push("");
|
|
2260
|
+
const taskResults = new Map(results.map((r) => [r.task, r]));
|
|
2261
|
+
const completedTasks = tasks.filter((t) => taskResults.get(t)?.success);
|
|
2262
|
+
const failedTasks = tasks.filter((t) => {
|
|
2263
|
+
const r = taskResults.get(t);
|
|
2264
|
+
return r && !r.success;
|
|
2265
|
+
});
|
|
2266
|
+
if (completedTasks.length > 0 || failedTasks.length > 0) {
|
|
2267
|
+
sections.push("## Tasks\n");
|
|
2268
|
+
for (const task of completedTasks) {
|
|
2269
|
+
sections.push(`- [x] ${task.text}`);
|
|
2270
|
+
}
|
|
2271
|
+
for (const task of failedTasks) {
|
|
2272
|
+
sections.push(`- [ ] ${task.text}`);
|
|
2273
|
+
}
|
|
2274
|
+
sections.push("");
|
|
2275
|
+
}
|
|
2276
|
+
for (const issue of issues) {
|
|
2277
|
+
if (datasourceName === "github") {
|
|
2278
|
+
sections.push(`Closes #${issue.number}`);
|
|
2279
|
+
} else if (datasourceName === "azdevops") {
|
|
2280
|
+
sections.push(`Resolves AB#${issue.number}`);
|
|
2281
|
+
}
|
|
2282
|
+
}
|
|
2283
|
+
return sections.join("\n");
|
|
2284
|
+
}
|
|
2285
|
+
|
|
2286
|
+
// src/helpers/worktree.ts
|
|
2287
|
+
import { join as join4, basename as basename3 } from "path";
|
|
2288
|
+
import { execFile as execFile6 } from "child_process";
|
|
2289
|
+
import { promisify as promisify6 } from "util";
|
|
2290
|
+
import { randomUUID } from "crypto";
|
|
2291
|
+
init_logger();
|
|
2292
|
+
var exec6 = promisify6(execFile6);
|
|
2293
|
+
var WORKTREE_DIR = ".dispatch/worktrees";
|
|
2294
|
+
async function git2(args, cwd) {
|
|
2295
|
+
const { stdout } = await exec6("git", args, { cwd, shell: process.platform === "win32" });
|
|
2296
|
+
return stdout;
|
|
2297
|
+
}
|
|
2298
|
+
function worktreeName(issueFilename) {
|
|
2299
|
+
const base = basename3(issueFilename);
|
|
2300
|
+
const withoutExt = base.replace(/\.md$/i, "");
|
|
2301
|
+
const match = withoutExt.match(/^(\d+)/);
|
|
2302
|
+
return match ? `issue-${match[1]}` : slugify(withoutExt);
|
|
2303
|
+
}
|
|
2304
|
+
async function createWorktree(repoRoot, issueFilename, branchName, startPoint) {
|
|
2305
|
+
const name = worktreeName(issueFilename);
|
|
2306
|
+
const worktreePath = join4(repoRoot, WORKTREE_DIR, name);
|
|
2307
|
+
try {
|
|
2308
|
+
const args = ["worktree", "add", worktreePath, "-b", branchName];
|
|
2309
|
+
if (startPoint) args.push(startPoint);
|
|
2310
|
+
await git2(args, repoRoot);
|
|
2311
|
+
log.debug(`Created worktree at ${worktreePath} on branch ${branchName}`);
|
|
2312
|
+
} catch (err) {
|
|
2313
|
+
const message = log.extractMessage(err);
|
|
2314
|
+
if (message.includes("already exists")) {
|
|
2315
|
+
await git2(["worktree", "add", worktreePath, branchName], repoRoot);
|
|
2316
|
+
log.debug(`Created worktree at ${worktreePath} using existing branch ${branchName}`);
|
|
2317
|
+
} else {
|
|
2318
|
+
throw err;
|
|
2319
|
+
}
|
|
2320
|
+
}
|
|
2321
|
+
return worktreePath;
|
|
2322
|
+
}
|
|
2323
|
+
async function removeWorktree(repoRoot, issueFilename) {
|
|
2324
|
+
const name = worktreeName(issueFilename);
|
|
2325
|
+
const worktreePath = join4(repoRoot, WORKTREE_DIR, name);
|
|
2326
|
+
try {
|
|
2327
|
+
await git2(["worktree", "remove", worktreePath], repoRoot);
|
|
2328
|
+
} catch {
|
|
2329
|
+
try {
|
|
2330
|
+
await git2(["worktree", "remove", "--force", worktreePath], repoRoot);
|
|
2331
|
+
} catch (err) {
|
|
2332
|
+
log.warn(`Could not remove worktree ${name}: ${log.formatErrorChain(err)}`);
|
|
2333
|
+
return;
|
|
2334
|
+
}
|
|
2335
|
+
}
|
|
2336
|
+
try {
|
|
2337
|
+
await git2(["worktree", "prune"], repoRoot);
|
|
2338
|
+
} catch (err) {
|
|
2339
|
+
log.warn(`Could not prune worktrees: ${log.formatErrorChain(err)}`);
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
2342
|
+
function generateFeatureBranchName() {
|
|
2343
|
+
const uuid = randomUUID();
|
|
2344
|
+
const octet = uuid.split("-")[0];
|
|
2345
|
+
return `dispatch/feature-${octet}`;
|
|
2346
|
+
}
|
|
2347
|
+
|
|
2123
2348
|
// src/orchestrator/runner.ts
|
|
2349
|
+
init_cleanup();
|
|
2124
2350
|
init_logger();
|
|
2125
2351
|
|
|
2126
2352
|
// src/helpers/confirm-large-batch.ts
|
|
@@ -2140,9 +2366,9 @@ async function confirmLargeBatch(count, threshold = LARGE_BATCH_THRESHOLD) {
|
|
|
2140
2366
|
}
|
|
2141
2367
|
|
|
2142
2368
|
// src/helpers/prereqs.ts
|
|
2143
|
-
import { execFile as
|
|
2144
|
-
import { promisify as
|
|
2145
|
-
var
|
|
2369
|
+
import { execFile as execFile7 } from "child_process";
|
|
2370
|
+
import { promisify as promisify7 } from "util";
|
|
2371
|
+
var exec7 = promisify7(execFile7);
|
|
2146
2372
|
var MIN_NODE_VERSION = "20.12.0";
|
|
2147
2373
|
function parseSemver(version) {
|
|
2148
2374
|
const [major, minor, patch] = version.split(".").map(Number);
|
|
@@ -2158,7 +2384,7 @@ function semverGte(current, minimum) {
|
|
|
2158
2384
|
async function checkPrereqs(context) {
|
|
2159
2385
|
const failures = [];
|
|
2160
2386
|
try {
|
|
2161
|
-
await
|
|
2387
|
+
await exec7("git", ["--version"], { shell: process.platform === "win32" });
|
|
2162
2388
|
} catch {
|
|
2163
2389
|
failures.push("git is required but was not found on PATH. Install it from https://git-scm.com");
|
|
2164
2390
|
}
|
|
@@ -2170,7 +2396,7 @@ async function checkPrereqs(context) {
|
|
|
2170
2396
|
}
|
|
2171
2397
|
if (context?.datasource === "github") {
|
|
2172
2398
|
try {
|
|
2173
|
-
await
|
|
2399
|
+
await exec7("gh", ["--version"], { shell: process.platform === "win32" });
|
|
2174
2400
|
} catch {
|
|
2175
2401
|
failures.push(
|
|
2176
2402
|
"gh (GitHub CLI) is required for the github datasource but was not found on PATH. Install it from https://cli.github.com/"
|
|
@@ -2179,7 +2405,7 @@ async function checkPrereqs(context) {
|
|
|
2179
2405
|
}
|
|
2180
2406
|
if (context?.datasource === "azdevops") {
|
|
2181
2407
|
try {
|
|
2182
|
-
await
|
|
2408
|
+
await exec7("az", ["--version"], { shell: process.platform === "win32" });
|
|
2183
2409
|
} catch {
|
|
2184
2410
|
failures.push(
|
|
2185
2411
|
"az (Azure CLI) is required for the azdevops datasource but was not found on PATH. Install it from https://learn.microsoft.com/en-us/cli/azure/"
|
|
@@ -2191,10 +2417,10 @@ async function checkPrereqs(context) {
|
|
|
2191
2417
|
|
|
2192
2418
|
// src/helpers/gitignore.ts
|
|
2193
2419
|
init_logger();
|
|
2194
|
-
import { readFile as readFile2, writeFile as
|
|
2195
|
-
import { join as
|
|
2420
|
+
import { readFile as readFile2, writeFile as writeFile3 } from "fs/promises";
|
|
2421
|
+
import { join as join5 } from "path";
|
|
2196
2422
|
async function ensureGitignoreEntry(repoRoot, entry) {
|
|
2197
|
-
const gitignorePath =
|
|
2423
|
+
const gitignorePath = join5(repoRoot, ".gitignore");
|
|
2198
2424
|
let contents = "";
|
|
2199
2425
|
try {
|
|
2200
2426
|
contents = await readFile2(gitignorePath, "utf8");
|
|
@@ -2213,7 +2439,7 @@ async function ensureGitignoreEntry(repoRoot, entry) {
|
|
|
2213
2439
|
}
|
|
2214
2440
|
try {
|
|
2215
2441
|
const separator = contents.length > 0 && !contents.endsWith("\n") ? "\n" : "";
|
|
2216
|
-
await
|
|
2442
|
+
await writeFile3(gitignorePath, `${contents}${separator}${entry}
|
|
2217
2443
|
`, "utf8");
|
|
2218
2444
|
log.debug(`Added '${entry}' to .gitignore`);
|
|
2219
2445
|
} catch (err) {
|
|
@@ -2223,14 +2449,14 @@ async function ensureGitignoreEntry(repoRoot, entry) {
|
|
|
2223
2449
|
|
|
2224
2450
|
// src/orchestrator/cli-config.ts
|
|
2225
2451
|
init_logger();
|
|
2226
|
-
import { join as
|
|
2452
|
+
import { join as join7 } from "path";
|
|
2227
2453
|
import { access } from "fs/promises";
|
|
2228
2454
|
import { constants } from "fs";
|
|
2229
2455
|
|
|
2230
2456
|
// src/config.ts
|
|
2231
2457
|
init_providers();
|
|
2232
|
-
import { readFile as readFile3, writeFile as
|
|
2233
|
-
import { join as
|
|
2458
|
+
import { readFile as readFile3, writeFile as writeFile4, mkdir as mkdir2 } from "fs/promises";
|
|
2459
|
+
import { join as join6, dirname as dirname3 } from "path";
|
|
2234
2460
|
|
|
2235
2461
|
// src/config-prompts.ts
|
|
2236
2462
|
init_logger();
|
|
@@ -2407,8 +2633,8 @@ var CONFIG_BOUNDS = {
|
|
|
2407
2633
|
};
|
|
2408
2634
|
var CONFIG_KEYS = ["provider", "model", "source", "testTimeout", "planTimeout", "concurrency", "org", "project", "workItemType", "iteration", "area"];
|
|
2409
2635
|
function getConfigPath(configDir) {
|
|
2410
|
-
const dir = configDir ??
|
|
2411
|
-
return
|
|
2636
|
+
const dir = configDir ?? join6(process.cwd(), ".dispatch");
|
|
2637
|
+
return join6(dir, "config.json");
|
|
2412
2638
|
}
|
|
2413
2639
|
async function loadConfig(configDir) {
|
|
2414
2640
|
const configPath = getConfigPath(configDir);
|
|
@@ -2422,7 +2648,7 @@ async function loadConfig(configDir) {
|
|
|
2422
2648
|
async function saveConfig(config, configDir) {
|
|
2423
2649
|
const configPath = getConfigPath(configDir);
|
|
2424
2650
|
await mkdir2(dirname3(configPath), { recursive: true });
|
|
2425
|
-
await
|
|
2651
|
+
await writeFile4(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
2426
2652
|
}
|
|
2427
2653
|
async function handleConfigCommand(_argv, configDir) {
|
|
2428
2654
|
await runInteractiveConfigWizard(configDir);
|
|
@@ -2447,7 +2673,7 @@ function setCliField(target, key, value) {
|
|
|
2447
2673
|
}
|
|
2448
2674
|
async function resolveCliConfig(args) {
|
|
2449
2675
|
const { explicitFlags } = args;
|
|
2450
|
-
const configDir =
|
|
2676
|
+
const configDir = join7(args.cwd, ".dispatch");
|
|
2451
2677
|
const config = await loadConfig(configDir);
|
|
2452
2678
|
const merged = { ...args };
|
|
2453
2679
|
for (const configKey of CONFIG_KEYS) {
|
|
@@ -2475,7 +2701,7 @@ async function resolveCliConfig(args) {
|
|
|
2475
2701
|
}
|
|
2476
2702
|
}
|
|
2477
2703
|
const sourceConfigured = explicitFlags.has("issueSource") || config.source !== void 0;
|
|
2478
|
-
const needsSource = !merged.fixTests && !merged.spec && !merged.respec;
|
|
2704
|
+
const needsSource = !(merged.fixTests && merged.issueIds.length === 0) && !merged.spec && !merged.respec;
|
|
2479
2705
|
if (needsSource && !sourceConfigured) {
|
|
2480
2706
|
const detected = await detectDatasource(merged.cwd);
|
|
2481
2707
|
if (detected) {
|
|
@@ -2494,15 +2720,15 @@ async function resolveCliConfig(args) {
|
|
|
2494
2720
|
}
|
|
2495
2721
|
|
|
2496
2722
|
// src/orchestrator/spec-pipeline.ts
|
|
2497
|
-
import { join as
|
|
2723
|
+
import { join as join9 } from "path";
|
|
2498
2724
|
import { mkdir as mkdir4, readFile as readFile5, rename as rename2, unlink as unlink2 } from "fs/promises";
|
|
2499
2725
|
import { glob as glob2 } from "glob";
|
|
2500
2726
|
init_providers();
|
|
2501
2727
|
|
|
2502
2728
|
// src/agents/spec.ts
|
|
2503
|
-
import { mkdir as mkdir3, readFile as readFile4, writeFile as
|
|
2504
|
-
import { join as
|
|
2505
|
-
import { randomUUID as
|
|
2729
|
+
import { mkdir as mkdir3, readFile as readFile4, writeFile as writeFile5, unlink } from "fs/promises";
|
|
2730
|
+
import { join as join8, resolve as resolve2, sep } from "path";
|
|
2731
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
2506
2732
|
init_logger();
|
|
2507
2733
|
init_file_logger();
|
|
2508
2734
|
init_environment();
|
|
@@ -2527,10 +2753,10 @@ async function boot5(opts) {
|
|
|
2527
2753
|
durationMs: Date.now() - startTime
|
|
2528
2754
|
};
|
|
2529
2755
|
}
|
|
2530
|
-
const tmpDir =
|
|
2756
|
+
const tmpDir = join8(resolvedCwd, ".dispatch", "tmp");
|
|
2531
2757
|
await mkdir3(tmpDir, { recursive: true });
|
|
2532
|
-
const tmpFilename = `spec-${
|
|
2533
|
-
const tmpPath =
|
|
2758
|
+
const tmpFilename = `spec-${randomUUID4()}.md`;
|
|
2759
|
+
const tmpPath = join8(tmpDir, tmpFilename);
|
|
2534
2760
|
let prompt;
|
|
2535
2761
|
if (issue) {
|
|
2536
2762
|
prompt = buildSpecPrompt(issue, workingDir, tmpPath);
|
|
@@ -2577,7 +2803,7 @@ async function boot5(opts) {
|
|
|
2577
2803
|
if (!validation.valid) {
|
|
2578
2804
|
log.warn(`Spec validation warning for ${outputPath}: ${validation.reason}`);
|
|
2579
2805
|
}
|
|
2580
|
-
await
|
|
2806
|
+
await writeFile5(resolvedOutput, cleanedContent, "utf-8");
|
|
2581
2807
|
log.debug(`Wrote cleaned spec to ${resolvedOutput}`);
|
|
2582
2808
|
try {
|
|
2583
2809
|
await unlink(tmpPath);
|
|
@@ -2932,7 +3158,7 @@ function buildInlineTextItem(issues, outputDir) {
|
|
|
2932
3158
|
const title = text.length > 80 ? text.slice(0, 80).trimEnd() + "\u2026" : text;
|
|
2933
3159
|
const slug = slugify(text, MAX_SLUG_LENGTH);
|
|
2934
3160
|
const filename = `${slug}.md`;
|
|
2935
|
-
const filepath =
|
|
3161
|
+
const filepath = join9(outputDir, filename);
|
|
2936
3162
|
const details = {
|
|
2937
3163
|
number: filepath,
|
|
2938
3164
|
title,
|
|
@@ -2994,7 +3220,7 @@ function previewDryRun(validItems, items, isTrackerMode, isInlineText, outputDir
|
|
|
2994
3220
|
let filepath;
|
|
2995
3221
|
if (isTrackerMode) {
|
|
2996
3222
|
const slug = slugify(details.title, 60);
|
|
2997
|
-
filepath =
|
|
3223
|
+
filepath = join9(outputDir, `${id}-${slug}.md`);
|
|
2998
3224
|
} else {
|
|
2999
3225
|
filepath = id;
|
|
3000
3226
|
}
|
|
@@ -3057,7 +3283,7 @@ async function generateSpecsBatch(validItems, items, specAgent, instance, isTrac
|
|
|
3057
3283
|
if (isTrackerMode) {
|
|
3058
3284
|
const slug = slugify(details.title, MAX_SLUG_LENGTH);
|
|
3059
3285
|
const filename = `${id}-${slug}.md`;
|
|
3060
|
-
filepath =
|
|
3286
|
+
filepath = join9(outputDir, filename);
|
|
3061
3287
|
} else if (isInlineText) {
|
|
3062
3288
|
filepath = id;
|
|
3063
3289
|
} else {
|
|
@@ -3086,7 +3312,7 @@ async function generateSpecsBatch(validItems, items, specAgent, instance, isTrac
|
|
|
3086
3312
|
const h1Title = extractTitle(result.data.content, filepath);
|
|
3087
3313
|
const h1Slug = slugify(h1Title, MAX_SLUG_LENGTH);
|
|
3088
3314
|
const finalFilename = isTrackerMode ? `${id}-${h1Slug}.md` : `${h1Slug}.md`;
|
|
3089
|
-
const finalFilepath =
|
|
3315
|
+
const finalFilepath = join9(outputDir, finalFilename);
|
|
3090
3316
|
if (finalFilepath !== filepath) {
|
|
3091
3317
|
await rename2(filepath, finalFilepath);
|
|
3092
3318
|
filepath = finalFilepath;
|
|
@@ -3191,7 +3417,7 @@ async function runSpecPipeline(opts) {
|
|
|
3191
3417
|
model,
|
|
3192
3418
|
serverUrl,
|
|
3193
3419
|
cwd: specCwd,
|
|
3194
|
-
outputDir =
|
|
3420
|
+
outputDir = join9(specCwd, ".dispatch", "specs"),
|
|
3195
3421
|
org,
|
|
3196
3422
|
project,
|
|
3197
3423
|
workItemType,
|
|
@@ -3272,7 +3498,7 @@ import { readFile as readFile7 } from "fs/promises";
|
|
|
3272
3498
|
import { glob as glob3 } from "glob";
|
|
3273
3499
|
|
|
3274
3500
|
// src/parser.ts
|
|
3275
|
-
import { readFile as readFile6, writeFile as
|
|
3501
|
+
import { readFile as readFile6, writeFile as writeFile6 } from "fs/promises";
|
|
3276
3502
|
var UNCHECKED_RE = /^(\s*[-*]\s)\[ \]\s+(.+)$/;
|
|
3277
3503
|
var CHECKED_SUB = "$1[x] $2";
|
|
3278
3504
|
var MODE_PREFIX_RE = /^\(([PSI])\)\s+/;
|
|
@@ -3340,7 +3566,7 @@ async function markTaskComplete(task) {
|
|
|
3340
3566
|
);
|
|
3341
3567
|
}
|
|
3342
3568
|
lines[lineIndex] = updated;
|
|
3343
|
-
await
|
|
3569
|
+
await writeFile6(task.file, lines.join(eol), "utf-8");
|
|
3344
3570
|
}
|
|
3345
3571
|
function groupTasksByMode(tasks) {
|
|
3346
3572
|
if (tasks.length === 0) return [];
|
|
@@ -3611,9 +3837,9 @@ ${err.stack}` : ""}`);
|
|
|
3611
3837
|
init_logger();
|
|
3612
3838
|
init_file_logger();
|
|
3613
3839
|
init_environment();
|
|
3614
|
-
import { mkdir as mkdir5, writeFile as
|
|
3615
|
-
import { join as
|
|
3616
|
-
import { randomUUID as
|
|
3840
|
+
import { mkdir as mkdir5, writeFile as writeFile7 } from "fs/promises";
|
|
3841
|
+
import { join as join10, resolve as resolve3 } from "path";
|
|
3842
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
3617
3843
|
async function boot8(opts) {
|
|
3618
3844
|
const { provider } = opts;
|
|
3619
3845
|
if (!provider) {
|
|
@@ -3626,10 +3852,10 @@ async function boot8(opts) {
|
|
|
3626
3852
|
async generate(genOpts) {
|
|
3627
3853
|
try {
|
|
3628
3854
|
const resolvedCwd = resolve3(genOpts.cwd);
|
|
3629
|
-
const tmpDir =
|
|
3855
|
+
const tmpDir = join10(resolvedCwd, ".dispatch", "tmp");
|
|
3630
3856
|
await mkdir5(tmpDir, { recursive: true });
|
|
3631
|
-
const tmpFilename = `commit-${
|
|
3632
|
-
const tmpPath =
|
|
3857
|
+
const tmpFilename = `commit-${randomUUID5()}.md`;
|
|
3858
|
+
const tmpPath = join10(tmpDir, tmpFilename);
|
|
3633
3859
|
const prompt = buildCommitPrompt(genOpts);
|
|
3634
3860
|
fileLoggerStorage.getStore()?.prompt("commit", prompt);
|
|
3635
3861
|
const sessionId = await provider.createSession();
|
|
@@ -3657,7 +3883,7 @@ async function boot8(opts) {
|
|
|
3657
3883
|
};
|
|
3658
3884
|
}
|
|
3659
3885
|
const outputContent = formatOutputFile(parsed);
|
|
3660
|
-
await
|
|
3886
|
+
await writeFile7(tmpPath, outputContent, "utf-8");
|
|
3661
3887
|
log.debug(`Wrote commit agent output to ${tmpPath}`);
|
|
3662
3888
|
fileLoggerStorage.getStore()?.agentEvent("commit", "completed", `message: ${parsed.commitMessage.slice(0, 80)}`);
|
|
3663
3889
|
return {
|
|
@@ -3809,68 +4035,6 @@ function formatOutputFile(parsed) {
|
|
|
3809
4035
|
init_logger();
|
|
3810
4036
|
init_cleanup();
|
|
3811
4037
|
|
|
3812
|
-
// src/helpers/worktree.ts
|
|
3813
|
-
import { join as join9, basename as basename2 } from "path";
|
|
3814
|
-
import { execFile as execFile7 } from "child_process";
|
|
3815
|
-
import { promisify as promisify7 } from "util";
|
|
3816
|
-
import { randomUUID as randomUUID5 } from "crypto";
|
|
3817
|
-
init_logger();
|
|
3818
|
-
var exec7 = promisify7(execFile7);
|
|
3819
|
-
var WORKTREE_DIR = ".dispatch/worktrees";
|
|
3820
|
-
async function git2(args, cwd) {
|
|
3821
|
-
const { stdout } = await exec7("git", args, { cwd, shell: process.platform === "win32" });
|
|
3822
|
-
return stdout;
|
|
3823
|
-
}
|
|
3824
|
-
function worktreeName(issueFilename) {
|
|
3825
|
-
const base = basename2(issueFilename);
|
|
3826
|
-
const withoutExt = base.replace(/\.md$/i, "");
|
|
3827
|
-
const match = withoutExt.match(/^(\d+)/);
|
|
3828
|
-
return match ? `issue-${match[1]}` : slugify(withoutExt);
|
|
3829
|
-
}
|
|
3830
|
-
async function createWorktree(repoRoot, issueFilename, branchName, startPoint) {
|
|
3831
|
-
const name = worktreeName(issueFilename);
|
|
3832
|
-
const worktreePath = join9(repoRoot, WORKTREE_DIR, name);
|
|
3833
|
-
try {
|
|
3834
|
-
const args = ["worktree", "add", worktreePath, "-b", branchName];
|
|
3835
|
-
if (startPoint) args.push(startPoint);
|
|
3836
|
-
await git2(args, repoRoot);
|
|
3837
|
-
log.debug(`Created worktree at ${worktreePath} on branch ${branchName}`);
|
|
3838
|
-
} catch (err) {
|
|
3839
|
-
const message = log.extractMessage(err);
|
|
3840
|
-
if (message.includes("already exists")) {
|
|
3841
|
-
await git2(["worktree", "add", worktreePath, branchName], repoRoot);
|
|
3842
|
-
log.debug(`Created worktree at ${worktreePath} using existing branch ${branchName}`);
|
|
3843
|
-
} else {
|
|
3844
|
-
throw err;
|
|
3845
|
-
}
|
|
3846
|
-
}
|
|
3847
|
-
return worktreePath;
|
|
3848
|
-
}
|
|
3849
|
-
async function removeWorktree(repoRoot, issueFilename) {
|
|
3850
|
-
const name = worktreeName(issueFilename);
|
|
3851
|
-
const worktreePath = join9(repoRoot, WORKTREE_DIR, name);
|
|
3852
|
-
try {
|
|
3853
|
-
await git2(["worktree", "remove", worktreePath], repoRoot);
|
|
3854
|
-
} catch {
|
|
3855
|
-
try {
|
|
3856
|
-
await git2(["worktree", "remove", "--force", worktreePath], repoRoot);
|
|
3857
|
-
} catch (err) {
|
|
3858
|
-
log.warn(`Could not remove worktree ${name}: ${log.formatErrorChain(err)}`);
|
|
3859
|
-
return;
|
|
3860
|
-
}
|
|
3861
|
-
}
|
|
3862
|
-
try {
|
|
3863
|
-
await git2(["worktree", "prune"], repoRoot);
|
|
3864
|
-
} catch (err) {
|
|
3865
|
-
log.warn(`Could not prune worktrees: ${log.formatErrorChain(err)}`);
|
|
3866
|
-
}
|
|
3867
|
-
}
|
|
3868
|
-
function generateFeatureBranchName() {
|
|
3869
|
-
const uuid = randomUUID5();
|
|
3870
|
-
const octet = uuid.split("-")[0];
|
|
3871
|
-
return `dispatch/feature-${octet}`;
|
|
3872
|
-
}
|
|
3873
|
-
|
|
3874
4038
|
// src/tui.ts
|
|
3875
4039
|
import chalk6 from "chalk";
|
|
3876
4040
|
var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
@@ -4127,181 +4291,6 @@ function createTui() {
|
|
|
4127
4291
|
|
|
4128
4292
|
// src/orchestrator/dispatch-pipeline.ts
|
|
4129
4293
|
init_providers();
|
|
4130
|
-
|
|
4131
|
-
// src/orchestrator/datasource-helpers.ts
|
|
4132
|
-
init_logger();
|
|
4133
|
-
import { basename as basename3, join as join10 } from "path";
|
|
4134
|
-
import { mkdtemp, writeFile as writeFile7 } from "fs/promises";
|
|
4135
|
-
import { tmpdir } from "os";
|
|
4136
|
-
import { execFile as execFile8 } from "child_process";
|
|
4137
|
-
import { promisify as promisify8 } from "util";
|
|
4138
|
-
var exec8 = promisify8(execFile8);
|
|
4139
|
-
function parseIssueFilename(filePath) {
|
|
4140
|
-
const filename = basename3(filePath);
|
|
4141
|
-
const match = /^(\d+)-(.+)\.md$/.exec(filename);
|
|
4142
|
-
if (!match) return null;
|
|
4143
|
-
return { issueId: match[1], slug: match[2] };
|
|
4144
|
-
}
|
|
4145
|
-
async function fetchItemsById(issueIds, datasource4, fetchOpts) {
|
|
4146
|
-
const ids = issueIds.flatMap(
|
|
4147
|
-
(id) => id.split(",").map((s) => s.trim()).filter(Boolean)
|
|
4148
|
-
);
|
|
4149
|
-
const items = [];
|
|
4150
|
-
for (const id of ids) {
|
|
4151
|
-
try {
|
|
4152
|
-
const item = await datasource4.fetch(id, fetchOpts);
|
|
4153
|
-
items.push(item);
|
|
4154
|
-
} catch (err) {
|
|
4155
|
-
const prefix = id.includes("/") || id.includes("\\") || id.endsWith(".md") ? "" : "#";
|
|
4156
|
-
log.warn(`Could not fetch issue ${prefix}${id}: ${log.formatErrorChain(err)}`);
|
|
4157
|
-
}
|
|
4158
|
-
}
|
|
4159
|
-
return items;
|
|
4160
|
-
}
|
|
4161
|
-
async function writeItemsToTempDir(items) {
|
|
4162
|
-
const tempDir = await mkdtemp(join10(tmpdir(), "dispatch-"));
|
|
4163
|
-
const files = [];
|
|
4164
|
-
const issueDetailsByFile = /* @__PURE__ */ new Map();
|
|
4165
|
-
for (const item of items) {
|
|
4166
|
-
const slug = slugify(item.title, MAX_SLUG_LENGTH);
|
|
4167
|
-
const filename = `${item.number}-${slug}.md`;
|
|
4168
|
-
const filepath = join10(tempDir, filename);
|
|
4169
|
-
await writeFile7(filepath, item.body, "utf-8");
|
|
4170
|
-
files.push(filepath);
|
|
4171
|
-
issueDetailsByFile.set(filepath, item);
|
|
4172
|
-
}
|
|
4173
|
-
files.sort((a, b) => {
|
|
4174
|
-
const numA = parseInt(basename3(a).match(/^(\d+)/)?.[1] ?? "0", 10);
|
|
4175
|
-
const numB = parseInt(basename3(b).match(/^(\d+)/)?.[1] ?? "0", 10);
|
|
4176
|
-
if (numA !== numB) return numA - numB;
|
|
4177
|
-
return a.localeCompare(b);
|
|
4178
|
-
});
|
|
4179
|
-
return { files, issueDetailsByFile };
|
|
4180
|
-
}
|
|
4181
|
-
async function getCommitSummaries(defaultBranch, cwd) {
|
|
4182
|
-
try {
|
|
4183
|
-
const { stdout } = await exec8(
|
|
4184
|
-
"git",
|
|
4185
|
-
["log", `${defaultBranch}..HEAD`, "--pretty=format:%s"],
|
|
4186
|
-
{ cwd, shell: process.platform === "win32" }
|
|
4187
|
-
);
|
|
4188
|
-
return stdout.trim().split("\n").filter(Boolean);
|
|
4189
|
-
} catch {
|
|
4190
|
-
return [];
|
|
4191
|
-
}
|
|
4192
|
-
}
|
|
4193
|
-
async function getBranchDiff(defaultBranch, cwd) {
|
|
4194
|
-
try {
|
|
4195
|
-
const { stdout } = await exec8(
|
|
4196
|
-
"git",
|
|
4197
|
-
["diff", `${defaultBranch}..HEAD`],
|
|
4198
|
-
{ cwd, maxBuffer: 10 * 1024 * 1024, shell: process.platform === "win32" }
|
|
4199
|
-
);
|
|
4200
|
-
return stdout;
|
|
4201
|
-
} catch {
|
|
4202
|
-
return "";
|
|
4203
|
-
}
|
|
4204
|
-
}
|
|
4205
|
-
async function squashBranchCommits(defaultBranch, message, cwd) {
|
|
4206
|
-
const { stdout } = await exec8(
|
|
4207
|
-
"git",
|
|
4208
|
-
["merge-base", defaultBranch, "HEAD"],
|
|
4209
|
-
{ cwd, shell: process.platform === "win32" }
|
|
4210
|
-
);
|
|
4211
|
-
const mergeBase = stdout.trim();
|
|
4212
|
-
await exec8("git", ["reset", "--soft", mergeBase], { cwd, shell: process.platform === "win32" });
|
|
4213
|
-
await exec8("git", ["commit", "-m", message], { cwd, shell: process.platform === "win32" });
|
|
4214
|
-
}
|
|
4215
|
-
async function buildPrBody(details, tasks, results, defaultBranch, datasourceName, cwd) {
|
|
4216
|
-
const sections = [];
|
|
4217
|
-
const commits = await getCommitSummaries(defaultBranch, cwd);
|
|
4218
|
-
if (commits.length > 0) {
|
|
4219
|
-
sections.push("## Summary\n");
|
|
4220
|
-
for (const commit of commits) {
|
|
4221
|
-
sections.push(`- ${commit}`);
|
|
4222
|
-
}
|
|
4223
|
-
sections.push("");
|
|
4224
|
-
}
|
|
4225
|
-
const taskResults = new Map(
|
|
4226
|
-
results.filter((r) => tasks.includes(r.task)).map((r) => [r.task, r])
|
|
4227
|
-
);
|
|
4228
|
-
const completedTasks = tasks.filter((t) => taskResults.get(t)?.success);
|
|
4229
|
-
const failedTasks = tasks.filter((t) => {
|
|
4230
|
-
const r = taskResults.get(t);
|
|
4231
|
-
return r && !r.success;
|
|
4232
|
-
});
|
|
4233
|
-
if (completedTasks.length > 0 || failedTasks.length > 0) {
|
|
4234
|
-
sections.push("## Tasks\n");
|
|
4235
|
-
for (const task of completedTasks) {
|
|
4236
|
-
sections.push(`- [x] ${task.text}`);
|
|
4237
|
-
}
|
|
4238
|
-
for (const task of failedTasks) {
|
|
4239
|
-
sections.push(`- [ ] ${task.text}`);
|
|
4240
|
-
}
|
|
4241
|
-
sections.push("");
|
|
4242
|
-
}
|
|
4243
|
-
if (details.labels.length > 0) {
|
|
4244
|
-
sections.push(`**Labels:** ${details.labels.join(", ")}
|
|
4245
|
-
`);
|
|
4246
|
-
}
|
|
4247
|
-
if (datasourceName === "github") {
|
|
4248
|
-
sections.push(`Closes #${details.number}`);
|
|
4249
|
-
} else if (datasourceName === "azdevops") {
|
|
4250
|
-
sections.push(`Resolves AB#${details.number}`);
|
|
4251
|
-
}
|
|
4252
|
-
return sections.join("\n");
|
|
4253
|
-
}
|
|
4254
|
-
async function buildPrTitle(issueTitle, defaultBranch, cwd) {
|
|
4255
|
-
const commits = await getCommitSummaries(defaultBranch, cwd);
|
|
4256
|
-
if (commits.length === 0) {
|
|
4257
|
-
return issueTitle;
|
|
4258
|
-
}
|
|
4259
|
-
if (commits.length === 1) {
|
|
4260
|
-
return commits[0];
|
|
4261
|
-
}
|
|
4262
|
-
return `${commits[commits.length - 1]} (+${commits.length - 1} more)`;
|
|
4263
|
-
}
|
|
4264
|
-
function buildFeaturePrTitle(featureBranchName, issues) {
|
|
4265
|
-
if (issues.length === 1) {
|
|
4266
|
-
return issues[0].title;
|
|
4267
|
-
}
|
|
4268
|
-
const issueRefs = issues.map((d) => `#${d.number}`).join(", ");
|
|
4269
|
-
return `feat: ${featureBranchName} (${issueRefs})`;
|
|
4270
|
-
}
|
|
4271
|
-
function buildFeaturePrBody(issues, tasks, results, datasourceName) {
|
|
4272
|
-
const sections = [];
|
|
4273
|
-
sections.push("## Issues\n");
|
|
4274
|
-
for (const issue of issues) {
|
|
4275
|
-
sections.push(`- #${issue.number}: ${issue.title}`);
|
|
4276
|
-
}
|
|
4277
|
-
sections.push("");
|
|
4278
|
-
const taskResults = new Map(results.map((r) => [r.task, r]));
|
|
4279
|
-
const completedTasks = tasks.filter((t) => taskResults.get(t)?.success);
|
|
4280
|
-
const failedTasks = tasks.filter((t) => {
|
|
4281
|
-
const r = taskResults.get(t);
|
|
4282
|
-
return r && !r.success;
|
|
4283
|
-
});
|
|
4284
|
-
if (completedTasks.length > 0 || failedTasks.length > 0) {
|
|
4285
|
-
sections.push("## Tasks\n");
|
|
4286
|
-
for (const task of completedTasks) {
|
|
4287
|
-
sections.push(`- [x] ${task.text}`);
|
|
4288
|
-
}
|
|
4289
|
-
for (const task of failedTasks) {
|
|
4290
|
-
sections.push(`- [ ] ${task.text}`);
|
|
4291
|
-
}
|
|
4292
|
-
sections.push("");
|
|
4293
|
-
}
|
|
4294
|
-
for (const issue of issues) {
|
|
4295
|
-
if (datasourceName === "github") {
|
|
4296
|
-
sections.push(`Closes #${issue.number}`);
|
|
4297
|
-
} else if (datasourceName === "azdevops") {
|
|
4298
|
-
sections.push(`Resolves AB#${issue.number}`);
|
|
4299
|
-
}
|
|
4300
|
-
}
|
|
4301
|
-
return sections.join("\n");
|
|
4302
|
-
}
|
|
4303
|
-
|
|
4304
|
-
// src/orchestrator/dispatch-pipeline.ts
|
|
4305
4294
|
init_timeout();
|
|
4306
4295
|
import chalk7 from "chalk";
|
|
4307
4296
|
init_file_logger();
|
|
@@ -5017,6 +5006,58 @@ async function dryRunMode(issueIds, cwd, source, org, project, workItemType, ite
|
|
|
5017
5006
|
}
|
|
5018
5007
|
|
|
5019
5008
|
// src/orchestrator/runner.ts
|
|
5009
|
+
async function runMultiIssueFixTests(opts) {
|
|
5010
|
+
const { runFixTestsPipeline: runFixTestsPipeline2 } = await Promise.resolve().then(() => (init_fix_tests_pipeline(), fix_tests_pipeline_exports));
|
|
5011
|
+
const datasource4 = getDatasource(opts.source);
|
|
5012
|
+
const fetchOpts = { cwd: opts.cwd, org: opts.org, project: opts.project };
|
|
5013
|
+
const items = await fetchItemsById(opts.issueIds, datasource4, fetchOpts);
|
|
5014
|
+
if (items.length === 0) {
|
|
5015
|
+
log.warn("No issues found for the given IDs");
|
|
5016
|
+
return { mode: "fix-tests", success: false, error: "No issues found" };
|
|
5017
|
+
}
|
|
5018
|
+
let username = "";
|
|
5019
|
+
try {
|
|
5020
|
+
username = await datasource4.getUsername({ cwd: opts.cwd });
|
|
5021
|
+
} catch (err) {
|
|
5022
|
+
log.warn(`Could not resolve git username for branch naming: ${log.formatErrorChain(err)}`);
|
|
5023
|
+
}
|
|
5024
|
+
log.info(`Running fix-tests for ${items.length} issue(s) in worktrees`);
|
|
5025
|
+
const issueResults = [];
|
|
5026
|
+
for (const item of items) {
|
|
5027
|
+
const branchName = datasource4.buildBranchName(item.number, item.title, username);
|
|
5028
|
+
const issueFilename = `${item.number}-fix-tests.md`;
|
|
5029
|
+
let worktreePath;
|
|
5030
|
+
try {
|
|
5031
|
+
worktreePath = await createWorktree(opts.cwd, issueFilename, branchName);
|
|
5032
|
+
registerCleanup(async () => {
|
|
5033
|
+
await removeWorktree(opts.cwd, issueFilename);
|
|
5034
|
+
});
|
|
5035
|
+
log.info(`Created worktree for issue #${item.number} at ${worktreePath}`);
|
|
5036
|
+
const result = await runFixTestsPipeline2({
|
|
5037
|
+
cwd: worktreePath,
|
|
5038
|
+
provider: opts.provider,
|
|
5039
|
+
serverUrl: opts.serverUrl,
|
|
5040
|
+
verbose: opts.verbose,
|
|
5041
|
+
testTimeout: opts.testTimeout
|
|
5042
|
+
});
|
|
5043
|
+
issueResults.push({ issueId: item.number, branch: branchName, success: result.success, error: result.error });
|
|
5044
|
+
} catch (err) {
|
|
5045
|
+
const message = log.extractMessage(err);
|
|
5046
|
+
log.error(`Fix-tests failed for issue #${item.number}: ${message}`);
|
|
5047
|
+
issueResults.push({ issueId: item.number, branch: branchName, success: false, error: message });
|
|
5048
|
+
} finally {
|
|
5049
|
+
if (worktreePath) {
|
|
5050
|
+
try {
|
|
5051
|
+
await removeWorktree(opts.cwd, issueFilename);
|
|
5052
|
+
} catch (err) {
|
|
5053
|
+
log.warn(`Could not remove worktree for issue #${item.number}: ${log.formatErrorChain(err)}`);
|
|
5054
|
+
}
|
|
5055
|
+
}
|
|
5056
|
+
}
|
|
5057
|
+
}
|
|
5058
|
+
const allSuccess = issueResults.length > 0 && issueResults.every((r) => r.success);
|
|
5059
|
+
return { mode: "fix-tests", success: allSuccess, issueResults };
|
|
5060
|
+
}
|
|
5020
5061
|
async function boot9(opts) {
|
|
5021
5062
|
const { cwd } = opts;
|
|
5022
5063
|
const runner = {
|
|
@@ -5029,7 +5070,25 @@ async function boot9(opts) {
|
|
|
5029
5070
|
}
|
|
5030
5071
|
if (opts2.mode === "fix-tests") {
|
|
5031
5072
|
const { runFixTestsPipeline: runFixTestsPipeline2 } = await Promise.resolve().then(() => (init_fix_tests_pipeline(), fix_tests_pipeline_exports));
|
|
5032
|
-
|
|
5073
|
+
if (!opts2.issueIds || opts2.issueIds.length === 0) {
|
|
5074
|
+
return runFixTestsPipeline2({ cwd, provider: opts2.provider ?? "opencode", serverUrl: opts2.serverUrl, verbose: opts2.verbose ?? false, testTimeout: opts2.testTimeout });
|
|
5075
|
+
}
|
|
5076
|
+
const source = opts2.source;
|
|
5077
|
+
if (!source) {
|
|
5078
|
+
log.error("No datasource configured for multi-issue fix-tests.");
|
|
5079
|
+
return { mode: "fix-tests", success: false, error: "No datasource configured" };
|
|
5080
|
+
}
|
|
5081
|
+
return runMultiIssueFixTests({
|
|
5082
|
+
cwd,
|
|
5083
|
+
issueIds: opts2.issueIds,
|
|
5084
|
+
source,
|
|
5085
|
+
provider: opts2.provider ?? "opencode",
|
|
5086
|
+
serverUrl: opts2.serverUrl,
|
|
5087
|
+
verbose: opts2.verbose ?? false,
|
|
5088
|
+
testTimeout: opts2.testTimeout,
|
|
5089
|
+
org: opts2.org,
|
|
5090
|
+
project: opts2.project
|
|
5091
|
+
});
|
|
5033
5092
|
}
|
|
5034
5093
|
const { mode: _, ...rest } = opts2;
|
|
5035
5094
|
return runner.orchestrate(rest);
|
|
@@ -5058,13 +5117,27 @@ async function boot9(opts) {
|
|
|
5058
5117
|
log.error("--feature and --no-branch are mutually exclusive");
|
|
5059
5118
|
process.exit(1);
|
|
5060
5119
|
}
|
|
5061
|
-
if (m.fixTests && m.issueIds.length > 0) {
|
|
5062
|
-
log.error("--fix-tests cannot be combined with issue IDs");
|
|
5063
|
-
process.exit(1);
|
|
5064
|
-
}
|
|
5065
5120
|
if (m.fixTests) {
|
|
5066
5121
|
const { runFixTestsPipeline: runFixTestsPipeline2 } = await Promise.resolve().then(() => (init_fix_tests_pipeline(), fix_tests_pipeline_exports));
|
|
5067
|
-
|
|
5122
|
+
if (m.issueIds.length === 0) {
|
|
5123
|
+
return runFixTestsPipeline2({ cwd: m.cwd, provider: m.provider, serverUrl: m.serverUrl, verbose: m.verbose, testTimeout: m.testTimeout });
|
|
5124
|
+
}
|
|
5125
|
+
const source = m.issueSource;
|
|
5126
|
+
if (!source) {
|
|
5127
|
+
log.error("No datasource configured. Use --source or run 'dispatch config' to set up defaults.");
|
|
5128
|
+
process.exit(1);
|
|
5129
|
+
}
|
|
5130
|
+
return runMultiIssueFixTests({
|
|
5131
|
+
cwd: m.cwd,
|
|
5132
|
+
issueIds: m.issueIds,
|
|
5133
|
+
source,
|
|
5134
|
+
provider: m.provider,
|
|
5135
|
+
serverUrl: m.serverUrl,
|
|
5136
|
+
verbose: m.verbose,
|
|
5137
|
+
testTimeout: m.testTimeout,
|
|
5138
|
+
org: m.org,
|
|
5139
|
+
project: m.project
|
|
5140
|
+
});
|
|
5068
5141
|
}
|
|
5069
5142
|
if (m.spec) {
|
|
5070
5143
|
return this.generateSpecs({
|
|
@@ -5170,6 +5243,7 @@ var HELP = `
|
|
|
5170
5243
|
dispatch --respec <glob> Regenerate specs matching a glob pattern
|
|
5171
5244
|
dispatch --spec "description" Generate a spec from an inline text description
|
|
5172
5245
|
dispatch --fix-tests Run tests and fix failures via AI agent
|
|
5246
|
+
dispatch --fix-tests <ids> Fix tests on specific issue branches (in worktrees)
|
|
5173
5247
|
|
|
5174
5248
|
Dispatch options:
|
|
5175
5249
|
--dry-run List tasks without dispatching (also works with --spec)
|
|
@@ -5224,6 +5298,10 @@ var HELP = `
|
|
|
5224
5298
|
dispatch --spec "feature A should do x" --provider copilot
|
|
5225
5299
|
dispatch --feature
|
|
5226
5300
|
dispatch --feature my-feature
|
|
5301
|
+
dispatch --fix-tests
|
|
5302
|
+
dispatch --fix-tests 14
|
|
5303
|
+
dispatch --fix-tests 14 15 16
|
|
5304
|
+
dispatch --fix-tests 14,15,16
|
|
5227
5305
|
dispatch config
|
|
5228
5306
|
`.trimStart();
|
|
5229
5307
|
function parseArgs(argv) {
|
|
@@ -5233,7 +5311,7 @@ function parseArgs(argv) {
|
|
|
5233
5311
|
},
|
|
5234
5312
|
writeErr: () => {
|
|
5235
5313
|
}
|
|
5236
|
-
}).helpOption(false).argument("[issueIds...]").option("-h, --help", "Show help").option("-v, --version", "Show version").option("--dry-run", "List tasks without dispatching").option("--no-plan", "Skip the planner agent").option("--no-branch", "Skip branch creation").option("--no-worktree", "Skip git worktree isolation").option("--feature [name]", "Group issues into a single feature branch").option("--force", "Ignore prior run state").option("--verbose", "Show detailed debug output").option("--fix-tests", "Run tests and fix failures").option("--spec <values...>", "Spec mode: issue numbers, glob, or text").option("--respec [values...]", "Regenerate specs").addOption(
|
|
5314
|
+
}).helpOption(false).argument("[issueIds...]").option("-h, --help", "Show help").option("-v, --version", "Show version").option("--dry-run", "List tasks without dispatching").option("--no-plan", "Skip the planner agent").option("--no-branch", "Skip branch creation").option("--no-worktree", "Skip git worktree isolation").option("--feature [name]", "Group issues into a single feature branch").option("--force", "Ignore prior run state").option("--verbose", "Show detailed debug output").option("--fix-tests", "Run tests and fix failures (optionally pass issue IDs to target specific branches)").option("--spec <values...>", "Spec mode: issue numbers, glob, or text").option("--respec [values...]", "Regenerate specs").addOption(
|
|
5237
5315
|
new Option("--provider <name>", "Agent backend").choices(PROVIDER_NAMES)
|
|
5238
5316
|
).addOption(
|
|
5239
5317
|
new Option("--source <name>", "Issue source").choices(
|
|
@@ -5397,7 +5475,7 @@ async function main() {
|
|
|
5397
5475
|
process.exit(0);
|
|
5398
5476
|
}
|
|
5399
5477
|
if (args.version) {
|
|
5400
|
-
console.log(`dispatch v${"1.4.
|
|
5478
|
+
console.log(`dispatch v${"1.4.1"}`);
|
|
5401
5479
|
process.exit(0);
|
|
5402
5480
|
}
|
|
5403
5481
|
const orchestrator = await boot9({ cwd: args.cwd });
|