@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 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 = randomUUID();
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 randomUUID2 } from "crypto";
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 = randomUUID2();
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 execFile6 } from "child_process";
760
- import { promisify as promisify6 } from "util";
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 exec6(PROVIDER_BINARIES[name], ["--version"], {
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 exec6, DETECTION_TIMEOUT_MS, PROVIDER_BINARIES;
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
- exec6 = promisify6(execFile6);
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 execFile5 } from "child_process";
2144
- import { promisify as promisify5 } from "util";
2145
- var exec5 = promisify5(execFile5);
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 exec5("git", ["--version"], { shell: process.platform === "win32" });
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 exec5("gh", ["--version"], { shell: process.platform === "win32" });
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 exec5("az", ["--version"], { shell: process.platform === "win32" });
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 writeFile2 } from "fs/promises";
2195
- import { join as join3 } from "path";
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 = join3(repoRoot, ".gitignore");
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 writeFile2(gitignorePath, `${contents}${separator}${entry}
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 join5 } from "path";
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 writeFile3, mkdir as mkdir2 } from "fs/promises";
2233
- import { join as join4, dirname as dirname3 } from "path";
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 ?? join4(process.cwd(), ".dispatch");
2411
- return join4(dir, "config.json");
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 writeFile3(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
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 = join5(args.cwd, ".dispatch");
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 join7 } from "path";
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 writeFile4, unlink } from "fs/promises";
2504
- import { join as join6, resolve as resolve2, sep } from "path";
2505
- import { randomUUID as randomUUID3 } from "crypto";
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 = join6(resolvedCwd, ".dispatch", "tmp");
2756
+ const tmpDir = join8(resolvedCwd, ".dispatch", "tmp");
2531
2757
  await mkdir3(tmpDir, { recursive: true });
2532
- const tmpFilename = `spec-${randomUUID3()}.md`;
2533
- const tmpPath = join6(tmpDir, tmpFilename);
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 writeFile4(resolvedOutput, cleanedContent, "utf-8");
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 = join7(outputDir, filename);
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 = join7(outputDir, `${id}-${slug}.md`);
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 = join7(outputDir, filename);
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 = join7(outputDir, finalFilename);
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 = join7(specCwd, ".dispatch", "specs"),
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 writeFile5 } from "fs/promises";
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 writeFile5(task.file, lines.join(eol), "utf-8");
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 writeFile6 } from "fs/promises";
3615
- import { join as join8, resolve as resolve3 } from "path";
3616
- import { randomUUID as randomUUID4 } from "crypto";
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 = join8(resolvedCwd, ".dispatch", "tmp");
3855
+ const tmpDir = join10(resolvedCwd, ".dispatch", "tmp");
3630
3856
  await mkdir5(tmpDir, { recursive: true });
3631
- const tmpFilename = `commit-${randomUUID4()}.md`;
3632
- const tmpPath = join8(tmpDir, tmpFilename);
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 writeFile6(tmpPath, outputContent, "utf-8");
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
- return runFixTestsPipeline2({ cwd, provider: "opencode", serverUrl: void 0, verbose: false, testTimeout: opts2.testTimeout });
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
- return runFixTestsPipeline2({ cwd: m.cwd, provider: m.provider, serverUrl: m.serverUrl, verbose: m.verbose, testTimeout: m.testTimeout });
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.0"}`);
5478
+ console.log(`dispatch v${"1.4.1"}`);
5401
5479
  process.exit(0);
5402
5480
  }
5403
5481
  const orchestrator = await boot9({ cwd: args.cwd });