@pruddiman/dispatch 1.4.0 → 1.4.2

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
@@ -756,11 +756,11 @@ var init_codex = __esm({
756
756
  });
757
757
 
758
758
  // src/providers/detect.ts
759
- import { execFile as execFile6 } from "child_process";
760
- import { promisify as promisify6 } from "util";
759
+ import { execFile as execFile3 } from "child_process";
760
+ import { promisify as promisify3 } from "util";
761
761
  async function checkProviderInstalled(name) {
762
762
  try {
763
- await exec6(PROVIDER_BINARIES[name], ["--version"], {
763
+ await exec3(PROVIDER_BINARIES[name], ["--version"], {
764
764
  shell: process.platform === "win32",
765
765
  timeout: DETECTION_TIMEOUT_MS
766
766
  });
@@ -769,11 +769,11 @@ async function checkProviderInstalled(name) {
769
769
  return false;
770
770
  }
771
771
  }
772
- var exec6, DETECTION_TIMEOUT_MS, PROVIDER_BINARIES;
772
+ var exec3, DETECTION_TIMEOUT_MS, PROVIDER_BINARIES;
773
773
  var init_detect = __esm({
774
774
  "src/providers/detect.ts"() {
775
775
  "use strict";
776
- exec6 = promisify6(execFile6);
776
+ exec3 = promisify3(execFile3);
777
777
  DETECTION_TIMEOUT_MS = 5e3;
778
778
  PROVIDER_BINARIES = {
779
779
  opencode: "opencode",
@@ -828,6 +828,27 @@ var init_providers = __esm({
828
828
  }
829
829
  });
830
830
 
831
+ // src/helpers/cleanup.ts
832
+ function registerCleanup(fn) {
833
+ cleanups.push(fn);
834
+ }
835
+ async function runCleanup() {
836
+ const fns = cleanups.splice(0);
837
+ for (const fn of fns) {
838
+ try {
839
+ await fn();
840
+ } catch {
841
+ }
842
+ }
843
+ }
844
+ var cleanups;
845
+ var init_cleanup = __esm({
846
+ "src/helpers/cleanup.ts"() {
847
+ "use strict";
848
+ cleanups = [];
849
+ }
850
+ });
851
+
831
852
  // src/helpers/environment.ts
832
853
  function getEnvironmentInfo() {
833
854
  const platform = process.platform;
@@ -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, {
@@ -1048,8 +1048,8 @@ import { Command, Option, CommanderError } from "commander";
1048
1048
  import { cpus, freemem } from "os";
1049
1049
 
1050
1050
  // src/datasources/index.ts
1051
- import { execFile as execFile4 } from "child_process";
1052
- import { promisify as promisify4 } from "util";
1051
+ import { execFile as execFile5 } from "child_process";
1052
+ import { promisify as promisify5 } from "util";
1053
1053
 
1054
1054
  // src/datasources/github.ts
1055
1055
  import { execFile } from "child_process";
@@ -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"
@@ -1777,30 +1767,224 @@ async function fetchComments(workItemId, opts) {
1777
1767
  }
1778
1768
 
1779
1769
  // src/datasources/md.ts
1780
- import { execFile as execFile3 } from "child_process";
1781
- import { readFile, writeFile, readdir, mkdir, rename } from "fs/promises";
1782
- import { basename, dirname as dirname2, isAbsolute, join as join2, parse as parsePath, resolve } from "path";
1783
- import { promisify as promisify3 } from "util";
1770
+ import { execFile as execFile4 } from "child_process";
1771
+ import { readFile as readFile2, writeFile as writeFile2, readdir, mkdir as mkdir2, rename } from "fs/promises";
1772
+ import { basename, dirname as dirname3, isAbsolute, join as join3, parse as parsePath, resolve } from "path";
1773
+ import { promisify as promisify4 } from "util";
1784
1774
  import { glob } from "glob";
1785
1775
 
1786
- // src/helpers/errors.ts
1787
- var UnsupportedOperationError = class extends Error {
1788
- /** The name of the operation that is not supported. */
1789
- operation;
1790
- constructor(operation, message) {
1791
- const msg = message ?? `Operation not supported: ${operation}`;
1792
- super(msg);
1793
- this.name = "UnsupportedOperationError";
1794
- this.operation = operation;
1776
+ // src/config.ts
1777
+ init_providers();
1778
+ import { readFile, writeFile, mkdir } from "fs/promises";
1779
+ import { join as join2, dirname as dirname2 } from "path";
1780
+
1781
+ // src/config-prompts.ts
1782
+ init_logger();
1783
+ import { select, confirm, input } from "@inquirer/prompts";
1784
+ import chalk2 from "chalk";
1785
+ init_providers();
1786
+ async function runInteractiveConfigWizard(configDir) {
1787
+ console.log();
1788
+ log.info(chalk2.bold("Dispatch Configuration Wizard"));
1789
+ console.log();
1790
+ const existing = await loadConfig(configDir);
1791
+ const hasExisting = Object.keys(existing).length > 0;
1792
+ if (hasExisting) {
1793
+ log.dim("Current configuration:");
1794
+ for (const [key, value] of Object.entries(existing)) {
1795
+ if (value !== void 0) {
1796
+ log.dim(` ${key} = ${value}`);
1797
+ }
1798
+ }
1799
+ console.log();
1800
+ const reconfigure = await confirm({
1801
+ message: "Do you want to reconfigure?",
1802
+ default: true
1803
+ });
1804
+ if (!reconfigure) {
1805
+ log.dim("Configuration unchanged.");
1806
+ return;
1807
+ }
1808
+ console.log();
1809
+ }
1810
+ const installStatuses = await Promise.all(
1811
+ PROVIDER_NAMES.map((name) => checkProviderInstalled(name))
1812
+ );
1813
+ const provider = await select({
1814
+ message: "Select a provider:",
1815
+ choices: PROVIDER_NAMES.map((name, i) => ({
1816
+ name: `${installStatuses[i] ? chalk2.green("\u25CF") : chalk2.red("\u25CF")} ${name}`,
1817
+ value: name
1818
+ })),
1819
+ default: existing.provider
1820
+ });
1821
+ let selectedModel = existing.model;
1822
+ try {
1823
+ log.dim("Fetching available models...");
1824
+ const models = await listProviderModels(provider);
1825
+ if (models.length > 0) {
1826
+ const modelChoice = await select({
1827
+ message: "Select a model:",
1828
+ choices: [
1829
+ { name: "default (provider decides)", value: "" },
1830
+ ...models.map((m) => ({ name: m, value: m }))
1831
+ ],
1832
+ default: existing.model ?? ""
1833
+ });
1834
+ selectedModel = modelChoice || void 0;
1835
+ } else {
1836
+ log.dim("No models returned by provider \u2014 skipping model selection.");
1837
+ selectedModel = existing.model;
1838
+ }
1839
+ } catch {
1840
+ log.dim("Could not list models (provider may not be running) \u2014 skipping model selection.");
1841
+ selectedModel = existing.model;
1842
+ }
1843
+ const detectedSource = await detectDatasource(process.cwd());
1844
+ const datasourceDefault = existing.source ?? "auto";
1845
+ if (detectedSource) {
1846
+ log.info(
1847
+ `Detected datasource ${chalk2.cyan(detectedSource)} from git remote`
1848
+ );
1849
+ }
1850
+ const selectedSource = await select({
1851
+ message: "Select a datasource:",
1852
+ choices: [
1853
+ {
1854
+ name: "auto",
1855
+ value: "auto",
1856
+ description: "detect from git remote at runtime"
1857
+ },
1858
+ ...DATASOURCE_NAMES.map((name) => ({ name, value: name }))
1859
+ ],
1860
+ default: datasourceDefault
1861
+ });
1862
+ const source = selectedSource === "auto" ? void 0 : selectedSource;
1863
+ let org;
1864
+ let project;
1865
+ let workItemType;
1866
+ let iteration;
1867
+ let area;
1868
+ const effectiveSource = source ?? detectedSource;
1869
+ if (effectiveSource === "azdevops") {
1870
+ let defaultOrg = existing.org ?? "";
1871
+ let defaultProject = existing.project ?? "";
1872
+ try {
1873
+ const remoteUrl = await getGitRemoteUrl(process.cwd());
1874
+ if (remoteUrl) {
1875
+ const parsed = parseAzDevOpsRemoteUrl(remoteUrl);
1876
+ if (parsed) {
1877
+ if (!defaultOrg) defaultOrg = parsed.orgUrl;
1878
+ if (!defaultProject) defaultProject = parsed.project;
1879
+ }
1880
+ }
1881
+ } catch {
1882
+ }
1883
+ console.log();
1884
+ log.info(chalk2.bold("Azure DevOps settings") + chalk2.dim(" (leave empty to skip):"));
1885
+ const orgInput = await input({
1886
+ message: "Organization URL:",
1887
+ default: defaultOrg || void 0
1888
+ });
1889
+ if (orgInput.trim()) org = orgInput.trim();
1890
+ const projectInput = await input({
1891
+ message: "Project name:",
1892
+ default: defaultProject || void 0
1893
+ });
1894
+ if (projectInput.trim()) project = projectInput.trim();
1895
+ const workItemTypeInput = await input({
1896
+ message: "Work item type (e.g. User Story, Bug):",
1897
+ default: existing.workItemType ?? void 0
1898
+ });
1899
+ if (workItemTypeInput.trim()) workItemType = workItemTypeInput.trim();
1900
+ const iterationInput = await input({
1901
+ message: "Iteration path (e.g. MyProject\\Sprint 1, or @CurrentIteration):",
1902
+ default: existing.iteration ?? void 0
1903
+ });
1904
+ if (iterationInput.trim()) iteration = iterationInput.trim();
1905
+ const areaInput = await input({
1906
+ message: "Area path (e.g. MyProject\\Team A):",
1907
+ default: existing.area ?? void 0
1908
+ });
1909
+ if (areaInput.trim()) area = areaInput.trim();
1910
+ }
1911
+ const newConfig = {
1912
+ provider,
1913
+ source
1914
+ };
1915
+ if (selectedModel !== void 0) {
1916
+ newConfig.model = selectedModel;
1917
+ }
1918
+ if (org !== void 0) newConfig.org = org;
1919
+ if (project !== void 0) newConfig.project = project;
1920
+ if (workItemType !== void 0) newConfig.workItemType = workItemType;
1921
+ if (iteration !== void 0) newConfig.iteration = iteration;
1922
+ if (area !== void 0) newConfig.area = area;
1923
+ console.log();
1924
+ log.info(chalk2.bold("Configuration summary:"));
1925
+ for (const [key, value] of Object.entries(newConfig)) {
1926
+ if (value !== void 0) {
1927
+ console.log(` ${chalk2.cyan(key)} = ${value}`);
1928
+ }
1929
+ }
1930
+ if (selectedSource === "auto") {
1931
+ console.log(
1932
+ ` ${chalk2.cyan("source")} = auto (detect from git remote at runtime)`
1933
+ );
1934
+ }
1935
+ console.log();
1936
+ const shouldSave = await confirm({
1937
+ message: "Save this configuration?",
1938
+ default: true
1939
+ });
1940
+ if (shouldSave) {
1941
+ await saveConfig(newConfig, configDir);
1942
+ log.success("Configuration saved.");
1943
+ } else {
1944
+ log.dim("Configuration not saved.");
1795
1945
  }
1946
+ }
1947
+
1948
+ // src/config.ts
1949
+ var CONFIG_BOUNDS = {
1950
+ testTimeout: { min: 1, max: 120 },
1951
+ planTimeout: { min: 1, max: 120 },
1952
+ concurrency: { min: 1, max: 64 }
1796
1953
  };
1954
+ var CONFIG_KEYS = ["provider", "model", "source", "testTimeout", "planTimeout", "concurrency", "org", "project", "workItemType", "iteration", "area"];
1955
+ function getConfigPath(configDir) {
1956
+ const dir = configDir ?? join2(process.cwd(), ".dispatch");
1957
+ return join2(dir, "config.json");
1958
+ }
1959
+ async function loadConfig(configDir) {
1960
+ const configPath = getConfigPath(configDir);
1961
+ try {
1962
+ const raw = await readFile(configPath, "utf-8");
1963
+ return JSON.parse(raw);
1964
+ } catch {
1965
+ return {};
1966
+ }
1967
+ }
1968
+ async function saveConfig(config, configDir) {
1969
+ const configPath = getConfigPath(configDir);
1970
+ await mkdir(dirname2(configPath), { recursive: true });
1971
+ await writeFile(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
1972
+ }
1973
+ async function handleConfigCommand(_argv, configDir) {
1974
+ await runInteractiveConfigWizard(configDir);
1975
+ }
1797
1976
 
1798
1977
  // src/datasources/md.ts
1799
- var exec3 = promisify3(execFile3);
1978
+ init_logger();
1979
+ var exec4 = promisify4(execFile4);
1980
+ async function git2(args, cwd) {
1981
+ const { stdout } = await exec4("git", args, { cwd, shell: process.platform === "win32" });
1982
+ return stdout;
1983
+ }
1800
1984
  var DEFAULT_DIR = ".dispatch/specs";
1801
1985
  function resolveDir(opts) {
1802
1986
  const cwd = opts?.cwd ?? process.cwd();
1803
- return join2(cwd, DEFAULT_DIR);
1987
+ return join3(cwd, DEFAULT_DIR);
1804
1988
  }
1805
1989
  function resolveFilePath(issueId, opts) {
1806
1990
  const filename = issueId.endsWith(".md") ? issueId : `${issueId}.md`;
@@ -1809,7 +1993,18 @@ function resolveFilePath(issueId, opts) {
1809
1993
  const cwd = opts?.cwd ?? process.cwd();
1810
1994
  return resolve(cwd, filename);
1811
1995
  }
1812
- return join2(resolveDir(opts), filename);
1996
+ return join3(resolveDir(opts), filename);
1997
+ }
1998
+ async function resolveNumericFilePath(issueId, opts) {
1999
+ if (/^\d+$/.test(issueId)) {
2000
+ const dir = resolveDir(opts);
2001
+ const entries = await readdir(dir);
2002
+ const match = entries.find((f) => f.startsWith(`${issueId}-`) && f.endsWith(".md"));
2003
+ if (match) {
2004
+ return join3(dir, match);
2005
+ }
2006
+ }
2007
+ return resolveFilePath(issueId, opts);
1813
2008
  }
1814
2009
  function extractTitle(content, filename) {
1815
2010
  const match = content.match(/^#\s+(.+)$/m);
@@ -1828,13 +2023,14 @@ function extractTitle(content, filename) {
1828
2023
  return parsePath(filename).name;
1829
2024
  }
1830
2025
  function toIssueDetails(filename, content, dir) {
2026
+ const idMatch = /^(\d+)-/.exec(filename);
1831
2027
  return {
1832
- number: filename,
2028
+ number: idMatch ? idMatch[1] : filename,
1833
2029
  title: extractTitle(content, filename),
1834
2030
  body: content,
1835
2031
  labels: [],
1836
2032
  state: "open",
1837
- url: join2(dir, filename),
2033
+ url: join3(dir, filename),
1838
2034
  comments: [],
1839
2035
  acceptanceCriteria: ""
1840
2036
  };
@@ -1842,7 +2038,7 @@ function toIssueDetails(filename, content, dir) {
1842
2038
  var datasource3 = {
1843
2039
  name: "md",
1844
2040
  supportsGit() {
1845
- return false;
2041
+ return true;
1846
2042
  },
1847
2043
  async list(opts) {
1848
2044
  if (opts?.pattern) {
@@ -1851,9 +2047,9 @@ var datasource3 = {
1851
2047
  const mdFiles2 = files.filter((f) => f.endsWith(".md")).sort();
1852
2048
  const results2 = [];
1853
2049
  for (const filePath of mdFiles2) {
1854
- const content = await readFile(filePath, "utf-8");
2050
+ const content = await readFile2(filePath, "utf-8");
1855
2051
  const filename = basename(filePath);
1856
- const dir2 = dirname2(filePath);
2052
+ const dir2 = dirname3(filePath);
1857
2053
  results2.push(toIssueDetails(filename, content, dir2));
1858
2054
  }
1859
2055
  return results2;
@@ -1868,44 +2064,81 @@ var datasource3 = {
1868
2064
  const mdFiles = entries.filter((f) => f.endsWith(".md")).sort();
1869
2065
  const results = [];
1870
2066
  for (const filename of mdFiles) {
1871
- const filePath = join2(dir, filename);
1872
- const content = await readFile(filePath, "utf-8");
2067
+ const filePath = join3(dir, filename);
2068
+ const content = await readFile2(filePath, "utf-8");
1873
2069
  results.push(toIssueDetails(filename, content, dir));
1874
2070
  }
1875
2071
  return results;
1876
2072
  },
1877
2073
  async fetch(issueId, opts) {
2074
+ if (/^\d+$/.test(issueId)) {
2075
+ const dir2 = resolveDir(opts);
2076
+ const entries = await readdir(dir2);
2077
+ const match = entries.find((f) => f.startsWith(`${issueId}-`) && f.endsWith(".md"));
2078
+ if (match) {
2079
+ const content2 = await readFile2(join3(dir2, match), "utf-8");
2080
+ return toIssueDetails(match, content2, dir2);
2081
+ }
2082
+ }
1878
2083
  const filePath = resolveFilePath(issueId, opts);
1879
- const content = await readFile(filePath, "utf-8");
2084
+ const content = await readFile2(filePath, "utf-8");
1880
2085
  const filename = basename(filePath);
1881
- const dir = dirname2(filePath);
2086
+ const dir = dirname3(filePath);
1882
2087
  return toIssueDetails(filename, content, dir);
1883
2088
  },
1884
2089
  async update(issueId, _title, body, opts) {
1885
- const filePath = resolveFilePath(issueId, opts);
1886
- await writeFile(filePath, body, "utf-8");
2090
+ const filePath = await resolveNumericFilePath(issueId, opts);
2091
+ await writeFile2(filePath, body, "utf-8");
1887
2092
  },
1888
2093
  async close(issueId, opts) {
1889
- const filePath = resolveFilePath(issueId, opts);
2094
+ const filePath = await resolveNumericFilePath(issueId, opts);
1890
2095
  const filename = basename(filePath);
1891
- const archiveDir = join2(dirname2(filePath), "archive");
1892
- await mkdir(archiveDir, { recursive: true });
1893
- await rename(filePath, join2(archiveDir, filename));
2096
+ const archiveDir = join3(dirname3(filePath), "archive");
2097
+ await mkdir2(archiveDir, { recursive: true });
2098
+ await rename(filePath, join3(archiveDir, filename));
1894
2099
  },
1895
2100
  async create(title, body, opts) {
2101
+ const cwd = opts?.cwd ?? process.cwd();
2102
+ const configDir = join3(cwd, ".dispatch");
2103
+ const config = await loadConfig(configDir);
2104
+ const id = config.nextIssueId ?? 1;
1896
2105
  const dir = resolveDir(opts);
1897
- await mkdir(dir, { recursive: true });
1898
- const filename = `${slugify(title)}.md`;
1899
- const filePath = join2(dir, filename);
1900
- await writeFile(filePath, body, "utf-8");
1901
- return toIssueDetails(filename, body, dir);
1902
- },
1903
- async getDefaultBranch(_opts) {
1904
- return "main";
2106
+ await mkdir2(dir, { recursive: true });
2107
+ const filename = `${id}-${slugify(title)}.md`;
2108
+ const filePath = join3(dir, filename);
2109
+ await writeFile2(filePath, body, "utf-8");
2110
+ config.nextIssueId = id + 1;
2111
+ await saveConfig(config, configDir);
2112
+ return {
2113
+ ...toIssueDetails(filename, body, dir),
2114
+ number: String(id)
2115
+ };
2116
+ },
2117
+ async getDefaultBranch(opts) {
2118
+ const PREFIX = "refs/remotes/origin/";
2119
+ try {
2120
+ const ref = await git2(["symbolic-ref", "refs/remotes/origin/HEAD"], opts.cwd);
2121
+ const trimmed = ref.trim();
2122
+ const branch = trimmed.startsWith(PREFIX) ? trimmed.slice(PREFIX.length) : trimmed;
2123
+ if (!isValidBranchName(branch)) {
2124
+ throw new InvalidBranchNameError(branch, "from symbolic-ref output");
2125
+ }
2126
+ return branch;
2127
+ } catch (err) {
2128
+ if (err instanceof InvalidBranchNameError) {
2129
+ throw err;
2130
+ }
2131
+ try {
2132
+ await git2(["rev-parse", "--verify", "main"], opts.cwd);
2133
+ return "main";
2134
+ } catch {
2135
+ return "master";
2136
+ }
2137
+ }
1905
2138
  },
1906
2139
  async getUsername(opts) {
1907
2140
  try {
1908
- const { stdout } = await exec3("git", ["config", "user.name"], { cwd: opts.cwd, shell: process.platform === "win32" });
2141
+ const { stdout } = await exec4("git", ["config", "user.name"], { cwd: opts.cwd, shell: process.platform === "win32" });
1909
2142
  const name = stdout.trim();
1910
2143
  if (!name) return "local";
1911
2144
  return slugify(name);
@@ -1917,25 +2150,39 @@ var datasource3 = {
1917
2150
  const slug = slugify(title, 50);
1918
2151
  return `${username}/dispatch/${issueNumber}-${slug}`;
1919
2152
  },
1920
- async createAndSwitchBranch(_branchName, _opts) {
1921
- throw new UnsupportedOperationError("createAndSwitchBranch");
2153
+ async createAndSwitchBranch(branchName, opts) {
2154
+ try {
2155
+ await git2(["checkout", "-b", branchName], opts.cwd);
2156
+ } catch (err) {
2157
+ const message = log.extractMessage(err);
2158
+ if (message.includes("already exists")) {
2159
+ await git2(["checkout", branchName], opts.cwd);
2160
+ } else {
2161
+ throw err;
2162
+ }
2163
+ }
1922
2164
  },
1923
- async switchBranch(_branchName, _opts) {
1924
- throw new UnsupportedOperationError("switchBranch");
2165
+ async switchBranch(branchName, opts) {
2166
+ await git2(["checkout", branchName], opts.cwd);
1925
2167
  },
1926
2168
  async pushBranch(_branchName, _opts) {
1927
- throw new UnsupportedOperationError("pushBranch");
1928
2169
  },
1929
- async commitAllChanges(_message, _opts) {
1930
- throw new UnsupportedOperationError("commitAllChanges");
2170
+ async commitAllChanges(message, opts) {
2171
+ const cwd = opts.cwd;
2172
+ await git2(["add", "-A"], cwd);
2173
+ const status = await git2(["diff", "--cached", "--stat"], cwd);
2174
+ if (!status.trim()) {
2175
+ return;
2176
+ }
2177
+ await git2(["commit", "-m", message], cwd);
1931
2178
  },
1932
2179
  async createPullRequest(_branchName, _issueNumber, _title, _body, _opts) {
1933
- throw new UnsupportedOperationError("createPullRequest");
2180
+ return "";
1934
2181
  }
1935
2182
  };
1936
2183
 
1937
2184
  // src/datasources/index.ts
1938
- var exec4 = promisify4(execFile4);
2185
+ var exec5 = promisify5(execFile5);
1939
2186
  var DATASOURCES = {
1940
2187
  github: datasource,
1941
2188
  azdevops: datasource2,
@@ -1953,7 +2200,7 @@ function getDatasource(name) {
1953
2200
  }
1954
2201
  async function getGitRemoteUrl(cwd) {
1955
2202
  try {
1956
- const { stdout } = await exec4("git", ["remote", "get-url", "origin"], {
2203
+ const { stdout } = await exec5("git", ["remote", "get-url", "origin"], {
1957
2204
  cwd,
1958
2205
  shell: process.platform === "win32"
1959
2206
  });
@@ -2120,29 +2367,266 @@ async function resolveSource(issues, issueSource, cwd) {
2120
2367
  return null;
2121
2368
  }
2122
2369
 
2370
+ // src/orchestrator/datasource-helpers.ts
2371
+ init_logger();
2372
+ import { basename as basename2, join as join4 } from "path";
2373
+ import { mkdtemp, writeFile as writeFile3 } from "fs/promises";
2374
+ import { tmpdir } from "os";
2375
+ import { execFile as execFile6 } from "child_process";
2376
+ import { promisify as promisify6 } from "util";
2377
+ var exec6 = promisify6(execFile6);
2378
+ function parseIssueFilename(filePath) {
2379
+ const filename = basename2(filePath);
2380
+ const match = /^(\d+)-(.+)\.md$/.exec(filename);
2381
+ if (!match) return null;
2382
+ return { issueId: match[1], slug: match[2] };
2383
+ }
2384
+ async function fetchItemsById(issueIds, datasource4, fetchOpts) {
2385
+ const ids = issueIds.flatMap(
2386
+ (id) => id.split(",").map((s) => s.trim()).filter(Boolean)
2387
+ );
2388
+ const items = [];
2389
+ for (const id of ids) {
2390
+ try {
2391
+ const item = await datasource4.fetch(id, fetchOpts);
2392
+ items.push(item);
2393
+ } catch (err) {
2394
+ const prefix = id.includes("/") || id.includes("\\") || id.endsWith(".md") ? "" : "#";
2395
+ log.warn(`Could not fetch issue ${prefix}${id}: ${log.formatErrorChain(err)}`);
2396
+ }
2397
+ }
2398
+ return items;
2399
+ }
2400
+ async function writeItemsToTempDir(items) {
2401
+ const tempDir = await mkdtemp(join4(tmpdir(), "dispatch-"));
2402
+ const files = [];
2403
+ const issueDetailsByFile = /* @__PURE__ */ new Map();
2404
+ for (const item of items) {
2405
+ const slug = slugify(item.title, MAX_SLUG_LENGTH);
2406
+ const id = item.number.includes("/") || item.number.includes("\\") ? basename2(item.number, ".md") : item.number;
2407
+ const filename = `${id}-${slug}.md`;
2408
+ const filepath = join4(tempDir, filename);
2409
+ await writeFile3(filepath, item.body, "utf-8");
2410
+ files.push(filepath);
2411
+ issueDetailsByFile.set(filepath, item);
2412
+ }
2413
+ files.sort((a, b) => {
2414
+ const numA = parseInt(basename2(a).match(/^(\d+)/)?.[1] ?? "0", 10);
2415
+ const numB = parseInt(basename2(b).match(/^(\d+)/)?.[1] ?? "0", 10);
2416
+ if (numA !== numB) return numA - numB;
2417
+ return a.localeCompare(b);
2418
+ });
2419
+ return { files, issueDetailsByFile };
2420
+ }
2421
+ async function getCommitSummaries(defaultBranch, cwd) {
2422
+ try {
2423
+ const { stdout } = await exec6(
2424
+ "git",
2425
+ ["log", `${defaultBranch}..HEAD`, "--pretty=format:%s"],
2426
+ { cwd, shell: process.platform === "win32" }
2427
+ );
2428
+ return stdout.trim().split("\n").filter(Boolean);
2429
+ } catch {
2430
+ return [];
2431
+ }
2432
+ }
2433
+ async function getBranchDiff(defaultBranch, cwd) {
2434
+ try {
2435
+ const { stdout } = await exec6(
2436
+ "git",
2437
+ ["diff", `${defaultBranch}..HEAD`],
2438
+ { cwd, maxBuffer: 10 * 1024 * 1024, shell: process.platform === "win32" }
2439
+ );
2440
+ return stdout;
2441
+ } catch {
2442
+ return "";
2443
+ }
2444
+ }
2445
+ async function squashBranchCommits(defaultBranch, message, cwd) {
2446
+ const { stdout } = await exec6(
2447
+ "git",
2448
+ ["merge-base", defaultBranch, "HEAD"],
2449
+ { cwd, shell: process.platform === "win32" }
2450
+ );
2451
+ const mergeBase = stdout.trim();
2452
+ await exec6("git", ["reset", "--soft", mergeBase], { cwd, shell: process.platform === "win32" });
2453
+ await exec6("git", ["commit", "-m", message], { cwd, shell: process.platform === "win32" });
2454
+ }
2455
+ async function buildPrBody(details, tasks, results, defaultBranch, datasourceName, cwd) {
2456
+ const sections = [];
2457
+ const commits = await getCommitSummaries(defaultBranch, cwd);
2458
+ if (commits.length > 0) {
2459
+ sections.push("## Summary\n");
2460
+ for (const commit of commits) {
2461
+ sections.push(`- ${commit}`);
2462
+ }
2463
+ sections.push("");
2464
+ }
2465
+ const taskResults = new Map(
2466
+ results.filter((r) => tasks.includes(r.task)).map((r) => [r.task, r])
2467
+ );
2468
+ const completedTasks = tasks.filter((t) => taskResults.get(t)?.success);
2469
+ const failedTasks = tasks.filter((t) => {
2470
+ const r = taskResults.get(t);
2471
+ return r && !r.success;
2472
+ });
2473
+ if (completedTasks.length > 0 || failedTasks.length > 0) {
2474
+ sections.push("## Tasks\n");
2475
+ for (const task of completedTasks) {
2476
+ sections.push(`- [x] ${task.text}`);
2477
+ }
2478
+ for (const task of failedTasks) {
2479
+ sections.push(`- [ ] ${task.text}`);
2480
+ }
2481
+ sections.push("");
2482
+ }
2483
+ if (details.labels.length > 0) {
2484
+ sections.push(`**Labels:** ${details.labels.join(", ")}
2485
+ `);
2486
+ }
2487
+ if (datasourceName === "github") {
2488
+ sections.push(`Closes #${details.number}`);
2489
+ } else if (datasourceName === "azdevops") {
2490
+ sections.push(`Resolves AB#${details.number}`);
2491
+ }
2492
+ return sections.join("\n");
2493
+ }
2494
+ async function buildPrTitle(issueTitle, defaultBranch, cwd) {
2495
+ const commits = await getCommitSummaries(defaultBranch, cwd);
2496
+ if (commits.length === 0) {
2497
+ return issueTitle;
2498
+ }
2499
+ if (commits.length === 1) {
2500
+ return commits[0];
2501
+ }
2502
+ return `${commits[commits.length - 1]} (+${commits.length - 1} more)`;
2503
+ }
2504
+ function buildFeaturePrTitle(featureBranchName, issues) {
2505
+ if (issues.length === 1) {
2506
+ return issues[0].title;
2507
+ }
2508
+ const issueRefs = issues.map((d) => `#${d.number}`).join(", ");
2509
+ return `feat: ${featureBranchName} (${issueRefs})`;
2510
+ }
2511
+ function buildFeaturePrBody(issues, tasks, results, datasourceName) {
2512
+ const sections = [];
2513
+ sections.push("## Issues\n");
2514
+ for (const issue of issues) {
2515
+ sections.push(`- #${issue.number}: ${issue.title}`);
2516
+ }
2517
+ sections.push("");
2518
+ const taskResults = new Map(results.map((r) => [r.task, r]));
2519
+ const completedTasks = tasks.filter((t) => taskResults.get(t)?.success);
2520
+ const failedTasks = tasks.filter((t) => {
2521
+ const r = taskResults.get(t);
2522
+ return r && !r.success;
2523
+ });
2524
+ if (completedTasks.length > 0 || failedTasks.length > 0) {
2525
+ sections.push("## Tasks\n");
2526
+ for (const task of completedTasks) {
2527
+ sections.push(`- [x] ${task.text}`);
2528
+ }
2529
+ for (const task of failedTasks) {
2530
+ sections.push(`- [ ] ${task.text}`);
2531
+ }
2532
+ sections.push("");
2533
+ }
2534
+ for (const issue of issues) {
2535
+ if (datasourceName === "github") {
2536
+ sections.push(`Closes #${issue.number}`);
2537
+ } else if (datasourceName === "azdevops") {
2538
+ sections.push(`Resolves AB#${issue.number}`);
2539
+ }
2540
+ }
2541
+ return sections.join("\n");
2542
+ }
2543
+
2544
+ // src/helpers/worktree.ts
2545
+ import { join as join5, basename as basename3 } from "path";
2546
+ import { execFile as execFile7 } from "child_process";
2547
+ import { promisify as promisify7 } from "util";
2548
+ import { randomUUID as randomUUID3 } from "crypto";
2549
+ init_logger();
2550
+ var exec7 = promisify7(execFile7);
2551
+ var WORKTREE_DIR = ".dispatch/worktrees";
2552
+ async function git3(args, cwd) {
2553
+ const { stdout } = await exec7("git", args, { cwd, shell: process.platform === "win32" });
2554
+ return stdout;
2555
+ }
2556
+ function worktreeName(issueFilename) {
2557
+ const base = basename3(issueFilename);
2558
+ const withoutExt = base.replace(/\.md$/i, "");
2559
+ const match = withoutExt.match(/^(\d+)/);
2560
+ return match ? `issue-${match[1]}` : slugify(withoutExt);
2561
+ }
2562
+ async function createWorktree(repoRoot, issueFilename, branchName, startPoint) {
2563
+ const name = worktreeName(issueFilename);
2564
+ const worktreePath = join5(repoRoot, WORKTREE_DIR, name);
2565
+ try {
2566
+ const args = ["worktree", "add", worktreePath, "-b", branchName];
2567
+ if (startPoint) args.push(startPoint);
2568
+ await git3(args, repoRoot);
2569
+ log.debug(`Created worktree at ${worktreePath} on branch ${branchName}`);
2570
+ } catch (err) {
2571
+ const message = log.extractMessage(err);
2572
+ if (message.includes("already exists")) {
2573
+ await git3(["worktree", "add", worktreePath, branchName], repoRoot);
2574
+ log.debug(`Created worktree at ${worktreePath} using existing branch ${branchName}`);
2575
+ } else {
2576
+ throw err;
2577
+ }
2578
+ }
2579
+ return worktreePath;
2580
+ }
2581
+ async function removeWorktree(repoRoot, issueFilename) {
2582
+ const name = worktreeName(issueFilename);
2583
+ const worktreePath = join5(repoRoot, WORKTREE_DIR, name);
2584
+ try {
2585
+ await git3(["worktree", "remove", worktreePath], repoRoot);
2586
+ } catch {
2587
+ try {
2588
+ await git3(["worktree", "remove", "--force", worktreePath], repoRoot);
2589
+ } catch (err) {
2590
+ log.warn(`Could not remove worktree ${name}: ${log.formatErrorChain(err)}`);
2591
+ return;
2592
+ }
2593
+ }
2594
+ try {
2595
+ await git3(["worktree", "prune"], repoRoot);
2596
+ } catch (err) {
2597
+ log.warn(`Could not prune worktrees: ${log.formatErrorChain(err)}`);
2598
+ }
2599
+ }
2600
+ function generateFeatureBranchName() {
2601
+ const uuid = randomUUID3();
2602
+ const octet = uuid.split("-")[0];
2603
+ return `dispatch/feature-${octet}`;
2604
+ }
2605
+
2123
2606
  // src/orchestrator/runner.ts
2607
+ init_cleanup();
2124
2608
  init_logger();
2125
2609
 
2126
2610
  // src/helpers/confirm-large-batch.ts
2127
2611
  init_logger();
2128
- import { input } from "@inquirer/prompts";
2129
- import chalk2 from "chalk";
2612
+ import { input as input2 } from "@inquirer/prompts";
2613
+ import chalk3 from "chalk";
2130
2614
  var LARGE_BATCH_THRESHOLD = 100;
2131
2615
  async function confirmLargeBatch(count, threshold = LARGE_BATCH_THRESHOLD) {
2132
2616
  if (count <= threshold) return true;
2133
2617
  log.warn(
2134
- `This operation will process ${chalk2.bold(String(count))} specs, which exceeds the safety threshold of ${threshold}.`
2618
+ `This operation will process ${chalk3.bold(String(count))} specs, which exceeds the safety threshold of ${threshold}.`
2135
2619
  );
2136
- const answer = await input({
2137
- message: `Type ${chalk2.bold('"yes"')} to proceed:`
2620
+ const answer = await input2({
2621
+ message: `Type ${chalk3.bold('"yes"')} to proceed:`
2138
2622
  });
2139
2623
  return answer.trim().toLowerCase() === "yes";
2140
2624
  }
2141
2625
 
2142
2626
  // src/helpers/prereqs.ts
2143
- import { execFile as execFile5 } from "child_process";
2144
- import { promisify as promisify5 } from "util";
2145
- var exec5 = promisify5(execFile5);
2627
+ import { execFile as execFile8 } from "child_process";
2628
+ import { promisify as promisify8 } from "util";
2629
+ var exec8 = promisify8(execFile8);
2146
2630
  var MIN_NODE_VERSION = "20.12.0";
2147
2631
  function parseSemver(version) {
2148
2632
  const [major, minor, patch] = version.split(".").map(Number);
@@ -2158,7 +2642,7 @@ function semverGte(current, minimum) {
2158
2642
  async function checkPrereqs(context) {
2159
2643
  const failures = [];
2160
2644
  try {
2161
- await exec5("git", ["--version"], { shell: process.platform === "win32" });
2645
+ await exec8("git", ["--version"], { shell: process.platform === "win32" });
2162
2646
  } catch {
2163
2647
  failures.push("git is required but was not found on PATH. Install it from https://git-scm.com");
2164
2648
  }
@@ -2170,7 +2654,7 @@ async function checkPrereqs(context) {
2170
2654
  }
2171
2655
  if (context?.datasource === "github") {
2172
2656
  try {
2173
- await exec5("gh", ["--version"], { shell: process.platform === "win32" });
2657
+ await exec8("gh", ["--version"], { shell: process.platform === "win32" });
2174
2658
  } catch {
2175
2659
  failures.push(
2176
2660
  "gh (GitHub CLI) is required for the github datasource but was not found on PATH. Install it from https://cli.github.com/"
@@ -2179,7 +2663,7 @@ async function checkPrereqs(context) {
2179
2663
  }
2180
2664
  if (context?.datasource === "azdevops") {
2181
2665
  try {
2182
- await exec5("az", ["--version"], { shell: process.platform === "win32" });
2666
+ await exec8("az", ["--version"], { shell: process.platform === "win32" });
2183
2667
  } catch {
2184
2668
  failures.push(
2185
2669
  "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,13 +2675,13 @@ async function checkPrereqs(context) {
2191
2675
 
2192
2676
  // src/helpers/gitignore.ts
2193
2677
  init_logger();
2194
- import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
2195
- import { join as join3 } from "path";
2678
+ import { readFile as readFile3, writeFile as writeFile4 } from "fs/promises";
2679
+ import { join as join6 } from "path";
2196
2680
  async function ensureGitignoreEntry(repoRoot, entry) {
2197
- const gitignorePath = join3(repoRoot, ".gitignore");
2681
+ const gitignorePath = join6(repoRoot, ".gitignore");
2198
2682
  let contents = "";
2199
2683
  try {
2200
- contents = await readFile2(gitignorePath, "utf8");
2684
+ contents = await readFile3(gitignorePath, "utf8");
2201
2685
  } catch (err) {
2202
2686
  if (err instanceof Error && "code" in err && err.code === "ENOENT") {
2203
2687
  } else {
@@ -2213,7 +2697,7 @@ async function ensureGitignoreEntry(repoRoot, entry) {
2213
2697
  }
2214
2698
  try {
2215
2699
  const separator = contents.length > 0 && !contents.endsWith("\n") ? "\n" : "";
2216
- await writeFile2(gitignorePath, `${contents}${separator}${entry}
2700
+ await writeFile4(gitignorePath, `${contents}${separator}${entry}
2217
2701
  `, "utf8");
2218
2702
  log.debug(`Added '${entry}' to .gitignore`);
2219
2703
  } catch (err) {
@@ -2223,212 +2707,9 @@ async function ensureGitignoreEntry(repoRoot, entry) {
2223
2707
 
2224
2708
  // src/orchestrator/cli-config.ts
2225
2709
  init_logger();
2226
- import { join as join5 } from "path";
2710
+ import { join as join7 } from "path";
2227
2711
  import { access } from "fs/promises";
2228
- import { constants } from "fs";
2229
-
2230
- // src/config.ts
2231
- 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";
2234
-
2235
- // src/config-prompts.ts
2236
- init_logger();
2237
- import { select, confirm, input as input2 } from "@inquirer/prompts";
2238
- import chalk3 from "chalk";
2239
- init_providers();
2240
- async function runInteractiveConfigWizard(configDir) {
2241
- console.log();
2242
- log.info(chalk3.bold("Dispatch Configuration Wizard"));
2243
- console.log();
2244
- const existing = await loadConfig(configDir);
2245
- const hasExisting = Object.keys(existing).length > 0;
2246
- if (hasExisting) {
2247
- log.dim("Current configuration:");
2248
- for (const [key, value] of Object.entries(existing)) {
2249
- if (value !== void 0) {
2250
- log.dim(` ${key} = ${value}`);
2251
- }
2252
- }
2253
- console.log();
2254
- const reconfigure = await confirm({
2255
- message: "Do you want to reconfigure?",
2256
- default: true
2257
- });
2258
- if (!reconfigure) {
2259
- log.dim("Configuration unchanged.");
2260
- return;
2261
- }
2262
- console.log();
2263
- }
2264
- const installStatuses = await Promise.all(
2265
- PROVIDER_NAMES.map((name) => checkProviderInstalled(name))
2266
- );
2267
- const provider = await select({
2268
- message: "Select a provider:",
2269
- choices: PROVIDER_NAMES.map((name, i) => ({
2270
- name: `${installStatuses[i] ? chalk3.green("\u25CF") : chalk3.red("\u25CF")} ${name}`,
2271
- value: name
2272
- })),
2273
- default: existing.provider
2274
- });
2275
- let selectedModel = existing.model;
2276
- try {
2277
- log.dim("Fetching available models...");
2278
- const models = await listProviderModels(provider);
2279
- if (models.length > 0) {
2280
- const modelChoice = await select({
2281
- message: "Select a model:",
2282
- choices: [
2283
- { name: "default (provider decides)", value: "" },
2284
- ...models.map((m) => ({ name: m, value: m }))
2285
- ],
2286
- default: existing.model ?? ""
2287
- });
2288
- selectedModel = modelChoice || void 0;
2289
- } else {
2290
- log.dim("No models returned by provider \u2014 skipping model selection.");
2291
- selectedModel = existing.model;
2292
- }
2293
- } catch {
2294
- log.dim("Could not list models (provider may not be running) \u2014 skipping model selection.");
2295
- selectedModel = existing.model;
2296
- }
2297
- const detectedSource = await detectDatasource(process.cwd());
2298
- const datasourceDefault = existing.source ?? "auto";
2299
- if (detectedSource) {
2300
- log.info(
2301
- `Detected datasource ${chalk3.cyan(detectedSource)} from git remote`
2302
- );
2303
- }
2304
- const selectedSource = await select({
2305
- message: "Select a datasource:",
2306
- choices: [
2307
- {
2308
- name: "auto",
2309
- value: "auto",
2310
- description: "detect from git remote at runtime"
2311
- },
2312
- ...DATASOURCE_NAMES.map((name) => ({ name, value: name }))
2313
- ],
2314
- default: datasourceDefault
2315
- });
2316
- const source = selectedSource === "auto" ? void 0 : selectedSource;
2317
- let org;
2318
- let project;
2319
- let workItemType;
2320
- let iteration;
2321
- let area;
2322
- const effectiveSource = source ?? detectedSource;
2323
- if (effectiveSource === "azdevops") {
2324
- let defaultOrg = existing.org ?? "";
2325
- let defaultProject = existing.project ?? "";
2326
- try {
2327
- const remoteUrl = await getGitRemoteUrl(process.cwd());
2328
- if (remoteUrl) {
2329
- const parsed = parseAzDevOpsRemoteUrl(remoteUrl);
2330
- if (parsed) {
2331
- if (!defaultOrg) defaultOrg = parsed.orgUrl;
2332
- if (!defaultProject) defaultProject = parsed.project;
2333
- }
2334
- }
2335
- } catch {
2336
- }
2337
- console.log();
2338
- log.info(chalk3.bold("Azure DevOps settings") + chalk3.dim(" (leave empty to skip):"));
2339
- const orgInput = await input2({
2340
- message: "Organization URL:",
2341
- default: defaultOrg || void 0
2342
- });
2343
- if (orgInput.trim()) org = orgInput.trim();
2344
- const projectInput = await input2({
2345
- message: "Project name:",
2346
- default: defaultProject || void 0
2347
- });
2348
- if (projectInput.trim()) project = projectInput.trim();
2349
- const workItemTypeInput = await input2({
2350
- message: "Work item type (e.g. User Story, Bug):",
2351
- default: existing.workItemType ?? void 0
2352
- });
2353
- if (workItemTypeInput.trim()) workItemType = workItemTypeInput.trim();
2354
- const iterationInput = await input2({
2355
- message: "Iteration path (e.g. MyProject\\Sprint 1, or @CurrentIteration):",
2356
- default: existing.iteration ?? void 0
2357
- });
2358
- if (iterationInput.trim()) iteration = iterationInput.trim();
2359
- const areaInput = await input2({
2360
- message: "Area path (e.g. MyProject\\Team A):",
2361
- default: existing.area ?? void 0
2362
- });
2363
- if (areaInput.trim()) area = areaInput.trim();
2364
- }
2365
- const newConfig = {
2366
- provider,
2367
- source
2368
- };
2369
- if (selectedModel !== void 0) {
2370
- newConfig.model = selectedModel;
2371
- }
2372
- if (org !== void 0) newConfig.org = org;
2373
- if (project !== void 0) newConfig.project = project;
2374
- if (workItemType !== void 0) newConfig.workItemType = workItemType;
2375
- if (iteration !== void 0) newConfig.iteration = iteration;
2376
- if (area !== void 0) newConfig.area = area;
2377
- console.log();
2378
- log.info(chalk3.bold("Configuration summary:"));
2379
- for (const [key, value] of Object.entries(newConfig)) {
2380
- if (value !== void 0) {
2381
- console.log(` ${chalk3.cyan(key)} = ${value}`);
2382
- }
2383
- }
2384
- if (selectedSource === "auto") {
2385
- console.log(
2386
- ` ${chalk3.cyan("source")} = auto (detect from git remote at runtime)`
2387
- );
2388
- }
2389
- console.log();
2390
- const shouldSave = await confirm({
2391
- message: "Save this configuration?",
2392
- default: true
2393
- });
2394
- if (shouldSave) {
2395
- await saveConfig(newConfig, configDir);
2396
- log.success("Configuration saved.");
2397
- } else {
2398
- log.dim("Configuration not saved.");
2399
- }
2400
- }
2401
-
2402
- // src/config.ts
2403
- var CONFIG_BOUNDS = {
2404
- testTimeout: { min: 1, max: 120 },
2405
- planTimeout: { min: 1, max: 120 },
2406
- concurrency: { min: 1, max: 64 }
2407
- };
2408
- var CONFIG_KEYS = ["provider", "model", "source", "testTimeout", "planTimeout", "concurrency", "org", "project", "workItemType", "iteration", "area"];
2409
- function getConfigPath(configDir) {
2410
- const dir = configDir ?? join4(process.cwd(), ".dispatch");
2411
- return join4(dir, "config.json");
2412
- }
2413
- async function loadConfig(configDir) {
2414
- const configPath = getConfigPath(configDir);
2415
- try {
2416
- const raw = await readFile3(configPath, "utf-8");
2417
- return JSON.parse(raw);
2418
- } catch {
2419
- return {};
2420
- }
2421
- }
2422
- async function saveConfig(config, configDir) {
2423
- const configPath = getConfigPath(configDir);
2424
- await mkdir2(dirname3(configPath), { recursive: true });
2425
- await writeFile3(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
2426
- }
2427
- async function handleConfigCommand(_argv, configDir) {
2428
- await runInteractiveConfigWizard(configDir);
2429
- }
2430
-
2431
- // src/orchestrator/cli-config.ts
2712
+ import { constants } from "fs";
2432
2713
  var CONFIG_TO_CLI = {
2433
2714
  provider: "provider",
2434
2715
  model: "model",
@@ -2447,7 +2728,7 @@ function setCliField(target, key, value) {
2447
2728
  }
2448
2729
  async function resolveCliConfig(args) {
2449
2730
  const { explicitFlags } = args;
2450
- const configDir = join5(args.cwd, ".dispatch");
2731
+ const configDir = join7(args.cwd, ".dispatch");
2451
2732
  const config = await loadConfig(configDir);
2452
2733
  const merged = { ...args };
2453
2734
  for (const configKey of CONFIG_KEYS) {
@@ -2475,7 +2756,7 @@ async function resolveCliConfig(args) {
2475
2756
  }
2476
2757
  }
2477
2758
  const sourceConfigured = explicitFlags.has("issueSource") || config.source !== void 0;
2478
- const needsSource = !merged.fixTests && !merged.spec && !merged.respec;
2759
+ const needsSource = !(merged.fixTests && merged.issueIds.length === 0) && !merged.spec && !merged.respec;
2479
2760
  if (needsSource && !sourceConfigured) {
2480
2761
  const detected = await detectDatasource(merged.cwd);
2481
2762
  if (detected) {
@@ -2494,15 +2775,15 @@ async function resolveCliConfig(args) {
2494
2775
  }
2495
2776
 
2496
2777
  // src/orchestrator/spec-pipeline.ts
2497
- import { join as join7 } from "path";
2778
+ import { join as join9 } from "path";
2498
2779
  import { mkdir as mkdir4, readFile as readFile5, rename as rename2, unlink as unlink2 } from "fs/promises";
2499
2780
  import { glob as glob2 } from "glob";
2500
2781
  init_providers();
2501
2782
 
2502
2783
  // 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";
2784
+ import { mkdir as mkdir3, readFile as readFile4, writeFile as writeFile5, unlink } from "fs/promises";
2785
+ import { join as join8, resolve as resolve2, sep } from "path";
2786
+ import { randomUUID as randomUUID4 } from "crypto";
2506
2787
  init_logger();
2507
2788
  init_file_logger();
2508
2789
  init_environment();
@@ -2527,10 +2808,10 @@ async function boot5(opts) {
2527
2808
  durationMs: Date.now() - startTime
2528
2809
  };
2529
2810
  }
2530
- const tmpDir = join6(resolvedCwd, ".dispatch", "tmp");
2811
+ const tmpDir = join8(resolvedCwd, ".dispatch", "tmp");
2531
2812
  await mkdir3(tmpDir, { recursive: true });
2532
- const tmpFilename = `spec-${randomUUID3()}.md`;
2533
- const tmpPath = join6(tmpDir, tmpFilename);
2813
+ const tmpFilename = `spec-${randomUUID4()}.md`;
2814
+ const tmpPath = join8(tmpDir, tmpFilename);
2534
2815
  let prompt;
2535
2816
  if (issue) {
2536
2817
  prompt = buildSpecPrompt(issue, workingDir, tmpPath);
@@ -2577,7 +2858,7 @@ async function boot5(opts) {
2577
2858
  if (!validation.valid) {
2578
2859
  log.warn(`Spec validation warning for ${outputPath}: ${validation.reason}`);
2579
2860
  }
2580
- await writeFile4(resolvedOutput, cleanedContent, "utf-8");
2861
+ await writeFile5(resolvedOutput, cleanedContent, "utf-8");
2581
2862
  log.debug(`Wrote cleaned spec to ${resolvedOutput}`);
2582
2863
  try {
2583
2864
  await unlink(tmpPath);
@@ -2932,7 +3213,7 @@ function buildInlineTextItem(issues, outputDir) {
2932
3213
  const title = text.length > 80 ? text.slice(0, 80).trimEnd() + "\u2026" : text;
2933
3214
  const slug = slugify(text, MAX_SLUG_LENGTH);
2934
3215
  const filename = `${slug}.md`;
2935
- const filepath = join7(outputDir, filename);
3216
+ const filepath = join9(outputDir, filename);
2936
3217
  const details = {
2937
3218
  number: filepath,
2938
3219
  title,
@@ -2994,7 +3275,7 @@ function previewDryRun(validItems, items, isTrackerMode, isInlineText, outputDir
2994
3275
  let filepath;
2995
3276
  if (isTrackerMode) {
2996
3277
  const slug = slugify(details.title, 60);
2997
- filepath = join7(outputDir, `${id}-${slug}.md`);
3278
+ filepath = join9(outputDir, `${id}-${slug}.md`);
2998
3279
  } else {
2999
3280
  filepath = id;
3000
3281
  }
@@ -3057,7 +3338,7 @@ async function generateSpecsBatch(validItems, items, specAgent, instance, isTrac
3057
3338
  if (isTrackerMode) {
3058
3339
  const slug = slugify(details.title, MAX_SLUG_LENGTH);
3059
3340
  const filename = `${id}-${slug}.md`;
3060
- filepath = join7(outputDir, filename);
3341
+ filepath = join9(outputDir, filename);
3061
3342
  } else if (isInlineText) {
3062
3343
  filepath = id;
3063
3344
  } else {
@@ -3086,7 +3367,7 @@ async function generateSpecsBatch(validItems, items, specAgent, instance, isTrac
3086
3367
  const h1Title = extractTitle(result.data.content, filepath);
3087
3368
  const h1Slug = slugify(h1Title, MAX_SLUG_LENGTH);
3088
3369
  const finalFilename = isTrackerMode ? `${id}-${h1Slug}.md` : `${h1Slug}.md`;
3089
- const finalFilepath = join7(outputDir, finalFilename);
3370
+ const finalFilepath = join9(outputDir, finalFilename);
3090
3371
  if (finalFilepath !== filepath) {
3091
3372
  await rename2(filepath, finalFilepath);
3092
3373
  filepath = finalFilepath;
@@ -3105,7 +3386,15 @@ async function generateSpecsBatch(validItems, items, specAgent, instance, isTrac
3105
3386
  log.success(`Deleted local spec ${filepath} (now tracked as issue #${id})`);
3106
3387
  identifier = id;
3107
3388
  issueNumbers.push(id);
3108
- } else if (datasource4.name !== "md") {
3389
+ } else if (datasource4.name === "md") {
3390
+ const parsed = parseIssueFilename(filepath);
3391
+ if (parsed) {
3392
+ await datasource4.update(parsed.issueId, details.title, result.data.content, fetchOpts);
3393
+ log.success(`Updated spec #${parsed.issueId} in-place`);
3394
+ identifier = parsed.issueId;
3395
+ issueNumbers.push(parsed.issueId);
3396
+ }
3397
+ } else {
3109
3398
  const created = await datasource4.create(details.title, result.data.content, fetchOpts);
3110
3399
  log.success(`Created issue #${created.number} from ${filepath}`);
3111
3400
  await unlink2(filepath);
@@ -3191,7 +3480,7 @@ async function runSpecPipeline(opts) {
3191
3480
  model,
3192
3481
  serverUrl,
3193
3482
  cwd: specCwd,
3194
- outputDir = join7(specCwd, ".dispatch", "specs"),
3483
+ outputDir = join9(specCwd, ".dispatch", "specs"),
3195
3484
  org,
3196
3485
  project,
3197
3486
  workItemType,
@@ -3272,7 +3561,7 @@ import { readFile as readFile7 } from "fs/promises";
3272
3561
  import { glob as glob3 } from "glob";
3273
3562
 
3274
3563
  // src/parser.ts
3275
- import { readFile as readFile6, writeFile as writeFile5 } from "fs/promises";
3564
+ import { readFile as readFile6, writeFile as writeFile6 } from "fs/promises";
3276
3565
  var UNCHECKED_RE = /^(\s*[-*]\s)\[ \]\s+(.+)$/;
3277
3566
  var CHECKED_SUB = "$1[x] $2";
3278
3567
  var MODE_PREFIX_RE = /^\(([PSI])\)\s+/;
@@ -3340,7 +3629,7 @@ async function markTaskComplete(task) {
3340
3629
  );
3341
3630
  }
3342
3631
  lines[lineIndex] = updated;
3343
- await writeFile5(task.file, lines.join(eol), "utf-8");
3632
+ await writeFile6(task.file, lines.join(eol), "utf-8");
3344
3633
  }
3345
3634
  function groupTasksByMode(tasks) {
3346
3635
  if (tasks.length === 0) return [];
@@ -3611,9 +3900,9 @@ ${err.stack}` : ""}`);
3611
3900
  init_logger();
3612
3901
  init_file_logger();
3613
3902
  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";
3903
+ import { mkdir as mkdir5, writeFile as writeFile7 } from "fs/promises";
3904
+ import { join as join10, resolve as resolve3 } from "path";
3905
+ import { randomUUID as randomUUID5 } from "crypto";
3617
3906
  async function boot8(opts) {
3618
3907
  const { provider } = opts;
3619
3908
  if (!provider) {
@@ -3626,10 +3915,10 @@ async function boot8(opts) {
3626
3915
  async generate(genOpts) {
3627
3916
  try {
3628
3917
  const resolvedCwd = resolve3(genOpts.cwd);
3629
- const tmpDir = join8(resolvedCwd, ".dispatch", "tmp");
3918
+ const tmpDir = join10(resolvedCwd, ".dispatch", "tmp");
3630
3919
  await mkdir5(tmpDir, { recursive: true });
3631
- const tmpFilename = `commit-${randomUUID4()}.md`;
3632
- const tmpPath = join8(tmpDir, tmpFilename);
3920
+ const tmpFilename = `commit-${randomUUID5()}.md`;
3921
+ const tmpPath = join10(tmpDir, tmpFilename);
3633
3922
  const prompt = buildCommitPrompt(genOpts);
3634
3923
  fileLoggerStorage.getStore()?.prompt("commit", prompt);
3635
3924
  const sessionId = await provider.createSession();
@@ -3657,7 +3946,7 @@ async function boot8(opts) {
3657
3946
  };
3658
3947
  }
3659
3948
  const outputContent = formatOutputFile(parsed);
3660
- await writeFile6(tmpPath, outputContent, "utf-8");
3949
+ await writeFile7(tmpPath, outputContent, "utf-8");
3661
3950
  log.debug(`Wrote commit agent output to ${tmpPath}`);
3662
3951
  fileLoggerStorage.getStore()?.agentEvent("commit", "completed", `message: ${parsed.commitMessage.slice(0, 80)}`);
3663
3952
  return {
@@ -3809,68 +4098,6 @@ function formatOutputFile(parsed) {
3809
4098
  init_logger();
3810
4099
  init_cleanup();
3811
4100
 
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
4101
  // src/tui.ts
3875
4102
  import chalk6 from "chalk";
3876
4103
  var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
@@ -4127,181 +4354,6 @@ function createTui() {
4127
4354
 
4128
4355
  // src/orchestrator/dispatch-pipeline.ts
4129
4356
  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
4357
  init_timeout();
4306
4358
  import chalk7 from "chalk";
4307
4359
  init_file_logger();
@@ -5017,6 +5069,58 @@ async function dryRunMode(issueIds, cwd, source, org, project, workItemType, ite
5017
5069
  }
5018
5070
 
5019
5071
  // src/orchestrator/runner.ts
5072
+ async function runMultiIssueFixTests(opts) {
5073
+ const { runFixTestsPipeline: runFixTestsPipeline2 } = await Promise.resolve().then(() => (init_fix_tests_pipeline(), fix_tests_pipeline_exports));
5074
+ const datasource4 = getDatasource(opts.source);
5075
+ const fetchOpts = { cwd: opts.cwd, org: opts.org, project: opts.project };
5076
+ const items = await fetchItemsById(opts.issueIds, datasource4, fetchOpts);
5077
+ if (items.length === 0) {
5078
+ log.warn("No issues found for the given IDs");
5079
+ return { mode: "fix-tests", success: false, error: "No issues found" };
5080
+ }
5081
+ let username = "";
5082
+ try {
5083
+ username = await datasource4.getUsername({ cwd: opts.cwd });
5084
+ } catch (err) {
5085
+ log.warn(`Could not resolve git username for branch naming: ${log.formatErrorChain(err)}`);
5086
+ }
5087
+ log.info(`Running fix-tests for ${items.length} issue(s) in worktrees`);
5088
+ const issueResults = [];
5089
+ for (const item of items) {
5090
+ const branchName = datasource4.buildBranchName(item.number, item.title, username);
5091
+ const issueFilename = `${item.number}-fix-tests.md`;
5092
+ let worktreePath;
5093
+ try {
5094
+ worktreePath = await createWorktree(opts.cwd, issueFilename, branchName);
5095
+ registerCleanup(async () => {
5096
+ await removeWorktree(opts.cwd, issueFilename);
5097
+ });
5098
+ log.info(`Created worktree for issue #${item.number} at ${worktreePath}`);
5099
+ const result = await runFixTestsPipeline2({
5100
+ cwd: worktreePath,
5101
+ provider: opts.provider,
5102
+ serverUrl: opts.serverUrl,
5103
+ verbose: opts.verbose,
5104
+ testTimeout: opts.testTimeout
5105
+ });
5106
+ issueResults.push({ issueId: item.number, branch: branchName, success: result.success, error: result.error });
5107
+ } catch (err) {
5108
+ const message = log.extractMessage(err);
5109
+ log.error(`Fix-tests failed for issue #${item.number}: ${message}`);
5110
+ issueResults.push({ issueId: item.number, branch: branchName, success: false, error: message });
5111
+ } finally {
5112
+ if (worktreePath) {
5113
+ try {
5114
+ await removeWorktree(opts.cwd, issueFilename);
5115
+ } catch (err) {
5116
+ log.warn(`Could not remove worktree for issue #${item.number}: ${log.formatErrorChain(err)}`);
5117
+ }
5118
+ }
5119
+ }
5120
+ }
5121
+ const allSuccess = issueResults.length > 0 && issueResults.every((r) => r.success);
5122
+ return { mode: "fix-tests", success: allSuccess, issueResults };
5123
+ }
5020
5124
  async function boot9(opts) {
5021
5125
  const { cwd } = opts;
5022
5126
  const runner = {
@@ -5029,7 +5133,25 @@ async function boot9(opts) {
5029
5133
  }
5030
5134
  if (opts2.mode === "fix-tests") {
5031
5135
  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 });
5136
+ if (!opts2.issueIds || opts2.issueIds.length === 0) {
5137
+ return runFixTestsPipeline2({ cwd, provider: opts2.provider ?? "opencode", serverUrl: opts2.serverUrl, verbose: opts2.verbose ?? false, testTimeout: opts2.testTimeout });
5138
+ }
5139
+ const source = opts2.source;
5140
+ if (!source) {
5141
+ log.error("No datasource configured for multi-issue fix-tests.");
5142
+ return { mode: "fix-tests", success: false, error: "No datasource configured" };
5143
+ }
5144
+ return runMultiIssueFixTests({
5145
+ cwd,
5146
+ issueIds: opts2.issueIds,
5147
+ source,
5148
+ provider: opts2.provider ?? "opencode",
5149
+ serverUrl: opts2.serverUrl,
5150
+ verbose: opts2.verbose ?? false,
5151
+ testTimeout: opts2.testTimeout,
5152
+ org: opts2.org,
5153
+ project: opts2.project
5154
+ });
5033
5155
  }
5034
5156
  const { mode: _, ...rest } = opts2;
5035
5157
  return runner.orchestrate(rest);
@@ -5058,13 +5180,27 @@ async function boot9(opts) {
5058
5180
  log.error("--feature and --no-branch are mutually exclusive");
5059
5181
  process.exit(1);
5060
5182
  }
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
5183
  if (m.fixTests) {
5066
5184
  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 });
5185
+ if (m.issueIds.length === 0) {
5186
+ return runFixTestsPipeline2({ cwd: m.cwd, provider: m.provider, serverUrl: m.serverUrl, verbose: m.verbose, testTimeout: m.testTimeout });
5187
+ }
5188
+ const source = m.issueSource;
5189
+ if (!source) {
5190
+ log.error("No datasource configured. Use --source or run 'dispatch config' to set up defaults.");
5191
+ process.exit(1);
5192
+ }
5193
+ return runMultiIssueFixTests({
5194
+ cwd: m.cwd,
5195
+ issueIds: m.issueIds,
5196
+ source,
5197
+ provider: m.provider,
5198
+ serverUrl: m.serverUrl,
5199
+ verbose: m.verbose,
5200
+ testTimeout: m.testTimeout,
5201
+ org: m.org,
5202
+ project: m.project
5203
+ });
5068
5204
  }
5069
5205
  if (m.spec) {
5070
5206
  return this.generateSpecs({
@@ -5170,6 +5306,7 @@ var HELP = `
5170
5306
  dispatch --respec <glob> Regenerate specs matching a glob pattern
5171
5307
  dispatch --spec "description" Generate a spec from an inline text description
5172
5308
  dispatch --fix-tests Run tests and fix failures via AI agent
5309
+ dispatch --fix-tests <ids> Fix tests on specific issue branches (in worktrees)
5173
5310
 
5174
5311
  Dispatch options:
5175
5312
  --dry-run List tasks without dispatching (also works with --spec)
@@ -5224,6 +5361,10 @@ var HELP = `
5224
5361
  dispatch --spec "feature A should do x" --provider copilot
5225
5362
  dispatch --feature
5226
5363
  dispatch --feature my-feature
5364
+ dispatch --fix-tests
5365
+ dispatch --fix-tests 14
5366
+ dispatch --fix-tests 14 15 16
5367
+ dispatch --fix-tests 14,15,16
5227
5368
  dispatch config
5228
5369
  `.trimStart();
5229
5370
  function parseArgs(argv) {
@@ -5233,7 +5374,7 @@ function parseArgs(argv) {
5233
5374
  },
5234
5375
  writeErr: () => {
5235
5376
  }
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(
5377
+ }).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
5378
  new Option("--provider <name>", "Agent backend").choices(PROVIDER_NAMES)
5238
5379
  ).addOption(
5239
5380
  new Option("--source <name>", "Issue source").choices(
@@ -5397,7 +5538,7 @@ async function main() {
5397
5538
  process.exit(0);
5398
5539
  }
5399
5540
  if (args.version) {
5400
- console.log(`dispatch v${"1.4.0"}`);
5541
+ console.log(`dispatch v${"1.4.2"}`);
5401
5542
  process.exit(0);
5402
5543
  }
5403
5544
  const orchestrator = await boot9({ cwd: args.cwd });