@pruddiman/dispatch 1.3.1 → 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);
@@ -422,7 +443,7 @@ var init_opencode = __esm({
422
443
 
423
444
  // src/helpers/timeout.ts
424
445
  function withTimeout(promise, ms, label) {
425
- const p = new Promise((resolve4, reject) => {
446
+ const p = new Promise((resolve5, reject) => {
426
447
  let settled = false;
427
448
  const timer = setTimeout(() => {
428
449
  if (settled) return;
@@ -434,7 +455,7 @@ function withTimeout(promise, ms, label) {
434
455
  if (settled) return;
435
456
  settled = true;
436
457
  clearTimeout(timer);
437
- resolve4(value);
458
+ resolve5(value);
438
459
  },
439
460
  (err) => {
440
461
  if (settled) return;
@@ -542,9 +563,9 @@ async function boot2(opts) {
542
563
  let unsubErr;
543
564
  try {
544
565
  await withTimeout(
545
- new Promise((resolve4, reject) => {
566
+ new Promise((resolve5, reject) => {
546
567
  unsubIdle = session.on("session.idle", () => {
547
- resolve4();
568
+ resolve5();
548
569
  });
549
570
  unsubErr = session.on("session.error", (event) => {
550
571
  reject(new Error(`Copilot session error: ${event.data.message}`));
@@ -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,23 +777,25 @@ 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"], {
764
- shell: process.platform === "win32"
784
+ await exec8(PROVIDER_BINARIES[name], ["--version"], {
785
+ shell: process.platform === "win32",
786
+ timeout: DETECTION_TIMEOUT_MS
765
787
  });
766
788
  return true;
767
789
  } catch {
768
790
  return false;
769
791
  }
770
792
  }
771
- var exec6, PROVIDER_BINARIES;
793
+ var exec8, DETECTION_TIMEOUT_MS, PROVIDER_BINARIES;
772
794
  var init_detect = __esm({
773
795
  "src/providers/detect.ts"() {
774
796
  "use strict";
775
- exec6 = promisify6(execFile6);
797
+ exec8 = promisify8(execFile8);
798
+ DETECTION_TIMEOUT_MS = 5e3;
776
799
  PROVIDER_BINARIES = {
777
800
  opencode: "opencode",
778
801
  copilot: "copilot",
@@ -826,24 +849,32 @@ var init_providers = __esm({
826
849
  }
827
850
  });
828
851
 
829
- // src/helpers/cleanup.ts
830
- function registerCleanup(fn) {
831
- cleanups.push(fn);
832
- }
833
- async function runCleanup() {
834
- const fns = cleanups.splice(0);
835
- for (const fn of fns) {
836
- try {
837
- await fn();
838
- } catch {
839
- }
852
+ // src/helpers/environment.ts
853
+ function getEnvironmentInfo() {
854
+ const platform = process.platform;
855
+ switch (platform) {
856
+ case "win32":
857
+ return { platform, os: "Windows", shell: "cmd.exe/PowerShell" };
858
+ case "darwin":
859
+ return { platform, os: "macOS", shell: "zsh/bash" };
860
+ default:
861
+ return { platform, os: "Linux", shell: "bash" };
840
862
  }
841
863
  }
842
- var cleanups;
843
- var init_cleanup = __esm({
844
- "src/helpers/cleanup.ts"() {
864
+ function formatEnvironmentPrompt() {
865
+ const env = getEnvironmentInfo();
866
+ return [
867
+ `## Environment`,
868
+ `- **Operating System:** ${env.os}`,
869
+ `- **Default Shell:** ${env.shell}`,
870
+ `- Always run commands directly in the shell. Do NOT write intermediate scripts (e.g. .bat, .ps1, .py files) unless the task explicitly requires creating a script.`
871
+ ].join("\n");
872
+ }
873
+ var getEnvironmentBlock;
874
+ var init_environment = __esm({
875
+ "src/helpers/environment.ts"() {
845
876
  "use strict";
846
- cleanups = [];
877
+ getEnvironmentBlock = formatEnvironmentPrompt;
847
878
  }
848
879
  });
849
880
 
@@ -880,15 +911,15 @@ async function detectTestCommand(cwd) {
880
911
  }
881
912
  }
882
913
  function runTestCommand(command, cwd) {
883
- return new Promise((resolve4) => {
914
+ return new Promise((resolve5) => {
884
915
  const [cmd, ...args] = command.split(" ");
885
916
  execFileCb(
886
917
  cmd,
887
918
  args,
888
- { cwd, maxBuffer: 10 * 1024 * 1024 },
919
+ { cwd, maxBuffer: 10 * 1024 * 1024, shell: process.platform === "win32" },
889
920
  (error, stdout, stderr) => {
890
921
  const exitCode = error && "code" in error ? error.code ?? 1 : error ? 1 : 0;
891
- resolve4({ exitCode, stdout, stderr, command });
922
+ resolve5({ exitCode, stdout, stderr, command });
892
923
  }
893
924
  );
894
925
  });
@@ -902,6 +933,8 @@ function buildFixTestsPrompt(testResult, cwd) {
902
933
  `**Test command:** ${testResult.command}`,
903
934
  `**Exit code:** ${testResult.exitCode}`,
904
935
  ``,
936
+ formatEnvironmentPrompt(),
937
+ ``,
905
938
  `## Test Output`,
906
939
  ``,
907
940
  "```",
@@ -1003,11 +1036,12 @@ var init_fix_tests_pipeline = __esm({
1003
1036
  init_cleanup();
1004
1037
  init_logger();
1005
1038
  init_file_logger();
1039
+ init_environment();
1006
1040
  }
1007
1041
  });
1008
1042
 
1009
1043
  // src/cli.ts
1010
- import { resolve as resolve3, join as join12 } from "path";
1044
+ import { resolve as resolve4, join as join12 } from "path";
1011
1045
  import { Command, Option, CommanderError } from "commander";
1012
1046
 
1013
1047
  // src/spec-generator.ts
@@ -1054,11 +1088,11 @@ function isValidBranchName(name) {
1054
1088
  // src/datasources/github.ts
1055
1089
  var exec = promisify(execFile);
1056
1090
  async function git(args, cwd) {
1057
- const { stdout } = await exec("git", args, { cwd });
1091
+ const { stdout } = await exec("git", args, { cwd, shell: process.platform === "win32" });
1058
1092
  return stdout;
1059
1093
  }
1060
1094
  async function gh(args, cwd) {
1061
- const { stdout } = await exec("gh", args, { cwd });
1095
+ const { stdout } = await exec("gh", args, { cwd, shell: process.platform === "win32" });
1062
1096
  return stdout;
1063
1097
  }
1064
1098
  function buildBranchName(issueNumber, title, username = "unknown") {
@@ -1104,7 +1138,7 @@ var datasource = {
1104
1138
  "--json",
1105
1139
  "number,title,body,labels,state,url"
1106
1140
  ],
1107
- { cwd }
1141
+ { cwd, shell: process.platform === "win32" }
1108
1142
  );
1109
1143
  let issues;
1110
1144
  try {
@@ -1136,7 +1170,7 @@ var datasource = {
1136
1170
  "--json",
1137
1171
  "number,title,body,labels,state,url,comments"
1138
1172
  ],
1139
- { cwd }
1173
+ { cwd, shell: process.platform === "win32" }
1140
1174
  );
1141
1175
  let issue;
1142
1176
  try {
@@ -1164,18 +1198,18 @@ var datasource = {
1164
1198
  },
1165
1199
  async update(issueId, title, body, opts = {}) {
1166
1200
  const cwd = opts.cwd || process.cwd();
1167
- await exec("gh", ["issue", "edit", issueId, "--title", title, "--body", body], { cwd });
1201
+ await exec("gh", ["issue", "edit", issueId, "--title", title, "--body", body], { cwd, shell: process.platform === "win32" });
1168
1202
  },
1169
1203
  async close(issueId, opts = {}) {
1170
1204
  const cwd = opts.cwd || process.cwd();
1171
- await exec("gh", ["issue", "close", issueId], { cwd });
1205
+ await exec("gh", ["issue", "close", issueId], { cwd, shell: process.platform === "win32" });
1172
1206
  },
1173
1207
  async create(title, body, opts = {}) {
1174
1208
  const cwd = opts.cwd || process.cwd();
1175
1209
  const { stdout } = await exec(
1176
1210
  "gh",
1177
1211
  ["issue", "create", "--title", title, "--body", body],
1178
- { cwd }
1212
+ { cwd, shell: process.platform === "win32" }
1179
1213
  );
1180
1214
  const url = stdout.trim();
1181
1215
  const match = url.match(/\/issues\/(\d+)$/);
@@ -1271,6 +1305,7 @@ import { execFile as execFile2 } from "child_process";
1271
1305
  import { promisify as promisify2 } from "util";
1272
1306
  init_logger();
1273
1307
  var exec2 = promisify2(execFile2);
1308
+ var doneStateCache = /* @__PURE__ */ new Map();
1274
1309
  function mapWorkItemToIssueDetails(item, id, comments, defaults) {
1275
1310
  const fields = item.fields ?? {};
1276
1311
  return {
@@ -1296,7 +1331,8 @@ async function detectWorkItemType(opts = {}) {
1296
1331
  if (opts.project) args.push("--project", opts.project);
1297
1332
  if (opts.org) args.push("--org", opts.org);
1298
1333
  const { stdout } = await exec2("az", args, {
1299
- cwd: opts.cwd || process.cwd()
1334
+ cwd: opts.cwd || process.cwd(),
1335
+ shell: process.platform === "win32"
1300
1336
  });
1301
1337
  const types = JSON.parse(stdout);
1302
1338
  if (!Array.isArray(types) || types.length === 0) return null;
@@ -1310,6 +1346,48 @@ async function detectWorkItemType(opts = {}) {
1310
1346
  return null;
1311
1347
  }
1312
1348
  }
1349
+ async function detectDoneState(workItemType, opts = {}) {
1350
+ const cacheKey = `${opts.org ?? ""}|${opts.project ?? ""}|${workItemType}`;
1351
+ const cached = doneStateCache.get(cacheKey);
1352
+ if (cached) return cached;
1353
+ try {
1354
+ const args = [
1355
+ "boards",
1356
+ "work-item",
1357
+ "type",
1358
+ "state",
1359
+ "list",
1360
+ "--type",
1361
+ workItemType,
1362
+ "--output",
1363
+ "json"
1364
+ ];
1365
+ if (opts.project) args.push("--project", opts.project);
1366
+ if (opts.org) args.push("--org", opts.org);
1367
+ const { stdout } = await exec2("az", args, {
1368
+ cwd: opts.cwd || process.cwd(),
1369
+ shell: process.platform === "win32"
1370
+ });
1371
+ const states = JSON.parse(stdout);
1372
+ if (Array.isArray(states)) {
1373
+ const completed = states.find((s) => s.category === "Completed");
1374
+ if (completed) {
1375
+ doneStateCache.set(cacheKey, completed.name);
1376
+ return completed.name;
1377
+ }
1378
+ const names = states.map((s) => s.name);
1379
+ const fallbacks = ["Done", "Closed", "Resolved", "Completed"];
1380
+ for (const f of fallbacks) {
1381
+ if (names.includes(f)) {
1382
+ doneStateCache.set(cacheKey, f);
1383
+ return f;
1384
+ }
1385
+ }
1386
+ }
1387
+ } catch {
1388
+ }
1389
+ return "Closed";
1390
+ }
1313
1391
  var datasource2 = {
1314
1392
  name: "azdevops",
1315
1393
  supportsGit() {
@@ -1318,6 +1396,7 @@ var datasource2 = {
1318
1396
  async list(opts = {}) {
1319
1397
  const conditions = [
1320
1398
  "[System.State] <> 'Closed'",
1399
+ "[System.State] <> 'Done'",
1321
1400
  "[System.State] <> 'Removed'"
1322
1401
  ];
1323
1402
  if (opts.iteration) {
@@ -1340,7 +1419,8 @@ var datasource2 = {
1340
1419
  if (opts.org) args.push("--org", opts.org);
1341
1420
  if (opts.project) args.push("--project", opts.project);
1342
1421
  const { stdout } = await exec2("az", args, {
1343
- cwd: opts.cwd || process.cwd()
1422
+ cwd: opts.cwd || process.cwd(),
1423
+ shell: process.platform === "win32"
1344
1424
  });
1345
1425
  let data;
1346
1426
  try {
@@ -1362,9 +1442,9 @@ var datasource2 = {
1362
1442
  "json"
1363
1443
  ];
1364
1444
  if (opts.org) batchArgs.push("--org", opts.org);
1365
- if (opts.project) batchArgs.push("--project", opts.project);
1366
1445
  const { stdout: batchStdout } = await exec2("az", batchArgs, {
1367
- cwd: opts.cwd || process.cwd()
1446
+ cwd: opts.cwd || process.cwd(),
1447
+ shell: process.platform === "win32"
1368
1448
  });
1369
1449
  let batchItems;
1370
1450
  try {
@@ -1406,11 +1486,9 @@ var datasource2 = {
1406
1486
  if (opts.org) {
1407
1487
  args.push("--org", opts.org);
1408
1488
  }
1409
- if (opts.project) {
1410
- args.push("--project", opts.project);
1411
- }
1412
1489
  const { stdout } = await exec2("az", args, {
1413
- cwd: opts.cwd || process.cwd()
1490
+ cwd: opts.cwd || process.cwd(),
1491
+ shell: process.platform === "win32"
1414
1492
  });
1415
1493
  let item;
1416
1494
  try {
@@ -1434,10 +1512,33 @@ var datasource2 = {
1434
1512
  body
1435
1513
  ];
1436
1514
  if (opts.org) args.push("--org", opts.org);
1437
- if (opts.project) args.push("--project", opts.project);
1438
- await exec2("az", args, { cwd: opts.cwd || process.cwd() });
1515
+ await exec2("az", args, { cwd: opts.cwd || process.cwd(), shell: process.platform === "win32" });
1439
1516
  },
1440
1517
  async close(issueId, opts = {}) {
1518
+ let workItemType = opts.workItemType;
1519
+ if (!workItemType) {
1520
+ const showArgs = [
1521
+ "boards",
1522
+ "work-item",
1523
+ "show",
1524
+ "--id",
1525
+ issueId,
1526
+ "--output",
1527
+ "json"
1528
+ ];
1529
+ if (opts.org) showArgs.push("--org", opts.org);
1530
+ const { stdout } = await exec2("az", showArgs, {
1531
+ cwd: opts.cwd || process.cwd(),
1532
+ shell: process.platform === "win32"
1533
+ });
1534
+ try {
1535
+ const item = JSON.parse(stdout);
1536
+ workItemType = item.fields?.["System.WorkItemType"] ?? void 0;
1537
+ } catch {
1538
+ workItemType = void 0;
1539
+ }
1540
+ }
1541
+ const state = workItemType ? await detectDoneState(workItemType, opts) : "Closed";
1441
1542
  const args = [
1442
1543
  "boards",
1443
1544
  "work-item",
@@ -1445,11 +1546,10 @@ var datasource2 = {
1445
1546
  "--id",
1446
1547
  issueId,
1447
1548
  "--state",
1448
- "Closed"
1549
+ state
1449
1550
  ];
1450
1551
  if (opts.org) args.push("--org", opts.org);
1451
- if (opts.project) args.push("--project", opts.project);
1452
- await exec2("az", args, { cwd: opts.cwd || process.cwd() });
1552
+ await exec2("az", args, { cwd: opts.cwd || process.cwd(), shell: process.platform === "win32" });
1453
1553
  },
1454
1554
  async create(title, body, opts = {}) {
1455
1555
  const workItemType = opts.workItemType ?? await detectWorkItemType(opts);
@@ -1474,7 +1574,8 @@ var datasource2 = {
1474
1574
  if (opts.org) args.push("--org", opts.org);
1475
1575
  if (opts.project) args.push("--project", opts.project);
1476
1576
  const { stdout } = await exec2("az", args, {
1477
- cwd: opts.cwd || process.cwd()
1577
+ cwd: opts.cwd || process.cwd(),
1578
+ shell: process.platform === "win32"
1478
1579
  });
1479
1580
  let item;
1480
1581
  try {
@@ -1491,7 +1592,7 @@ var datasource2 = {
1491
1592
  },
1492
1593
  async getDefaultBranch(opts) {
1493
1594
  try {
1494
- const { stdout } = await exec2("git", ["symbolic-ref", "refs/remotes/origin/HEAD"], { cwd: opts.cwd });
1595
+ const { stdout } = await exec2("git", ["symbolic-ref", "refs/remotes/origin/HEAD"], { cwd: opts.cwd, shell: process.platform === "win32" });
1495
1596
  const parts = stdout.trim().split("/");
1496
1597
  const branch = parts[parts.length - 1];
1497
1598
  if (!isValidBranchName(branch)) {
@@ -1503,7 +1604,7 @@ var datasource2 = {
1503
1604
  throw err;
1504
1605
  }
1505
1606
  try {
1506
- await exec2("git", ["rev-parse", "--verify", "main"], { cwd: opts.cwd });
1607
+ await exec2("git", ["rev-parse", "--verify", "main"], { cwd: opts.cwd, shell: process.platform === "win32" });
1507
1608
  return "main";
1508
1609
  } catch {
1509
1610
  return "master";
@@ -1512,19 +1613,19 @@ var datasource2 = {
1512
1613
  },
1513
1614
  async getUsername(opts) {
1514
1615
  try {
1515
- const { stdout } = await exec2("git", ["config", "user.name"], { cwd: opts.cwd });
1616
+ const { stdout } = await exec2("git", ["config", "user.name"], { cwd: opts.cwd, shell: process.platform === "win32" });
1516
1617
  const name = slugify(stdout.trim());
1517
1618
  if (name) return name;
1518
1619
  } catch {
1519
1620
  }
1520
1621
  try {
1521
- const { stdout } = await exec2("az", ["account", "show", "--query", "user.name", "-o", "tsv"], { cwd: opts.cwd });
1622
+ const { stdout } = await exec2("az", ["account", "show", "--query", "user.name", "-o", "tsv"], { cwd: opts.cwd, shell: process.platform === "win32" });
1522
1623
  const name = slugify(stdout.trim());
1523
1624
  if (name) return name;
1524
1625
  } catch {
1525
1626
  }
1526
1627
  try {
1527
- const { stdout } = await exec2("az", ["account", "show", "--query", "user.principalName", "-o", "tsv"], { cwd: opts.cwd });
1628
+ const { stdout } = await exec2("az", ["account", "show", "--query", "user.principalName", "-o", "tsv"], { cwd: opts.cwd, shell: process.platform === "win32" });
1528
1629
  const principal = stdout.trim();
1529
1630
  const prefix = principal.split("@")[0];
1530
1631
  const name = slugify(prefix);
@@ -1546,29 +1647,29 @@ var datasource2 = {
1546
1647
  throw new InvalidBranchNameError(branchName);
1547
1648
  }
1548
1649
  try {
1549
- await exec2("git", ["checkout", "-b", branchName], { cwd: opts.cwd });
1650
+ await exec2("git", ["checkout", "-b", branchName], { cwd: opts.cwd, shell: process.platform === "win32" });
1550
1651
  } catch (err) {
1551
1652
  const message = log.extractMessage(err);
1552
1653
  if (message.includes("already exists")) {
1553
- await exec2("git", ["checkout", branchName], { cwd: opts.cwd });
1654
+ await exec2("git", ["checkout", branchName], { cwd: opts.cwd, shell: process.platform === "win32" });
1554
1655
  } else {
1555
1656
  throw err;
1556
1657
  }
1557
1658
  }
1558
1659
  },
1559
1660
  async switchBranch(branchName, opts) {
1560
- await exec2("git", ["checkout", branchName], { cwd: opts.cwd });
1661
+ await exec2("git", ["checkout", branchName], { cwd: opts.cwd, shell: process.platform === "win32" });
1561
1662
  },
1562
1663
  async pushBranch(branchName, opts) {
1563
- await exec2("git", ["push", "--set-upstream", "origin", branchName], { cwd: opts.cwd });
1664
+ await exec2("git", ["push", "--set-upstream", "origin", branchName], { cwd: opts.cwd, shell: process.platform === "win32" });
1564
1665
  },
1565
1666
  async commitAllChanges(message, opts) {
1566
- await exec2("git", ["add", "-A"], { cwd: opts.cwd });
1567
- const { stdout } = await exec2("git", ["diff", "--cached", "--stat"], { cwd: opts.cwd });
1667
+ await exec2("git", ["add", "-A"], { cwd: opts.cwd, shell: process.platform === "win32" });
1668
+ const { stdout } = await exec2("git", ["diff", "--cached", "--stat"], { cwd: opts.cwd, shell: process.platform === "win32" });
1568
1669
  if (!stdout.trim()) {
1569
1670
  return;
1570
1671
  }
1571
- await exec2("git", ["commit", "-m", message], { cwd: opts.cwd });
1672
+ await exec2("git", ["commit", "-m", message], { cwd: opts.cwd, shell: process.platform === "win32" });
1572
1673
  },
1573
1674
  async createPullRequest(branchName, issueNumber, title, body, opts) {
1574
1675
  try {
@@ -1589,7 +1690,7 @@ var datasource2 = {
1589
1690
  "--output",
1590
1691
  "json"
1591
1692
  ],
1592
- { cwd: opts.cwd }
1693
+ { cwd: opts.cwd, shell: process.platform === "win32" }
1593
1694
  );
1594
1695
  let pr;
1595
1696
  try {
@@ -1614,7 +1715,7 @@ var datasource2 = {
1614
1715
  "--output",
1615
1716
  "json"
1616
1717
  ],
1617
- { cwd: opts.cwd }
1718
+ { cwd: opts.cwd, shell: process.platform === "win32" }
1618
1719
  );
1619
1720
  let prs;
1620
1721
  try {
@@ -1646,11 +1747,9 @@ async function fetchComments(workItemId, opts) {
1646
1747
  if (opts.org) {
1647
1748
  args.push("--org", opts.org);
1648
1749
  }
1649
- if (opts.project) {
1650
- args.push("--project", opts.project);
1651
- }
1652
1750
  const { stdout } = await exec2("az", args, {
1653
- cwd: opts.cwd || process.cwd()
1751
+ cwd: opts.cwd || process.cwd(),
1752
+ shell: process.platform === "win32"
1654
1753
  });
1655
1754
  const data = JSON.parse(stdout);
1656
1755
  if (data.comments && Array.isArray(data.comments)) {
@@ -1670,8 +1769,9 @@ async function fetchComments(workItemId, opts) {
1670
1769
  // src/datasources/md.ts
1671
1770
  import { execFile as execFile3 } from "child_process";
1672
1771
  import { readFile, writeFile, readdir, mkdir, rename } from "fs/promises";
1673
- import { join as join2, parse as parsePath } from "path";
1772
+ import { basename, dirname as dirname2, isAbsolute, join as join2, parse as parsePath, resolve } from "path";
1674
1773
  import { promisify as promisify3 } from "util";
1774
+ import { glob } from "glob";
1675
1775
 
1676
1776
  // src/helpers/errors.ts
1677
1777
  var UnsupportedOperationError = class extends Error {
@@ -1692,6 +1792,15 @@ function resolveDir(opts) {
1692
1792
  const cwd = opts?.cwd ?? process.cwd();
1693
1793
  return join2(cwd, DEFAULT_DIR);
1694
1794
  }
1795
+ function resolveFilePath(issueId, opts) {
1796
+ const filename = issueId.endsWith(".md") ? issueId : `${issueId}.md`;
1797
+ if (isAbsolute(filename)) return filename;
1798
+ if (/[/\\]/.test(filename)) {
1799
+ const cwd = opts?.cwd ?? process.cwd();
1800
+ return resolve(cwd, filename);
1801
+ }
1802
+ return join2(resolveDir(opts), filename);
1803
+ }
1695
1804
  function extractTitle(content, filename) {
1696
1805
  const match = content.match(/^#\s+(.+)$/m);
1697
1806
  if (match) return match[1].trim();
@@ -1726,6 +1835,19 @@ var datasource3 = {
1726
1835
  return false;
1727
1836
  },
1728
1837
  async list(opts) {
1838
+ if (opts?.pattern) {
1839
+ const cwd = opts.cwd ?? process.cwd();
1840
+ const files = await glob(opts.pattern, { cwd, absolute: true });
1841
+ const mdFiles2 = files.filter((f) => f.endsWith(".md")).sort();
1842
+ const results2 = [];
1843
+ for (const filePath of mdFiles2) {
1844
+ const content = await readFile(filePath, "utf-8");
1845
+ const filename = basename(filePath);
1846
+ const dir2 = dirname2(filePath);
1847
+ results2.push(toIssueDetails(filename, content, dir2));
1848
+ }
1849
+ return results2;
1850
+ }
1729
1851
  const dir = resolveDir(opts);
1730
1852
  let entries;
1731
1853
  try {
@@ -1743,23 +1865,20 @@ var datasource3 = {
1743
1865
  return results;
1744
1866
  },
1745
1867
  async fetch(issueId, opts) {
1746
- const dir = resolveDir(opts);
1747
- const filename = issueId.endsWith(".md") ? issueId : `${issueId}.md`;
1748
- const filePath = join2(dir, filename);
1868
+ const filePath = resolveFilePath(issueId, opts);
1749
1869
  const content = await readFile(filePath, "utf-8");
1870
+ const filename = basename(filePath);
1871
+ const dir = dirname2(filePath);
1750
1872
  return toIssueDetails(filename, content, dir);
1751
1873
  },
1752
1874
  async update(issueId, _title, body, opts) {
1753
- const dir = resolveDir(opts);
1754
- const filename = issueId.endsWith(".md") ? issueId : `${issueId}.md`;
1755
- const filePath = join2(dir, filename);
1875
+ const filePath = resolveFilePath(issueId, opts);
1756
1876
  await writeFile(filePath, body, "utf-8");
1757
1877
  },
1758
1878
  async close(issueId, opts) {
1759
- const dir = resolveDir(opts);
1760
- const filename = issueId.endsWith(".md") ? issueId : `${issueId}.md`;
1761
- const filePath = join2(dir, filename);
1762
- const archiveDir = join2(dir, "archive");
1879
+ const filePath = resolveFilePath(issueId, opts);
1880
+ const filename = basename(filePath);
1881
+ const archiveDir = join2(dirname2(filePath), "archive");
1763
1882
  await mkdir(archiveDir, { recursive: true });
1764
1883
  await rename(filePath, join2(archiveDir, filename));
1765
1884
  },
@@ -1776,7 +1895,7 @@ var datasource3 = {
1776
1895
  },
1777
1896
  async getUsername(opts) {
1778
1897
  try {
1779
- const { stdout } = await exec3("git", ["config", "user.name"], { cwd: opts.cwd });
1898
+ const { stdout } = await exec3("git", ["config", "user.name"], { cwd: opts.cwd, shell: process.platform === "win32" });
1780
1899
  const name = stdout.trim();
1781
1900
  if (!name) return "local";
1782
1901
  return slugify(name);
@@ -1825,7 +1944,8 @@ function getDatasource(name) {
1825
1944
  async function getGitRemoteUrl(cwd) {
1826
1945
  try {
1827
1946
  const { stdout } = await exec4("git", ["remote", "get-url", "origin"], {
1828
- cwd
1947
+ cwd,
1948
+ shell: process.platform === "win32"
1829
1949
  });
1830
1950
  return stdout.trim() || null;
1831
1951
  } catch {
@@ -1990,100 +2110,336 @@ async function resolveSource(issues, issueSource, cwd) {
1990
2110
  return null;
1991
2111
  }
1992
2112
 
1993
- // src/orchestrator/runner.ts
1994
- init_logger();
1995
-
1996
- // src/helpers/confirm-large-batch.ts
2113
+ // src/orchestrator/datasource-helpers.ts
1997
2114
  init_logger();
1998
- import { input } from "@inquirer/prompts";
1999
- import chalk2 from "chalk";
2000
- var LARGE_BATCH_THRESHOLD = 100;
2001
- async function confirmLargeBatch(count, threshold = LARGE_BATCH_THRESHOLD) {
2002
- if (count <= threshold) return true;
2003
- log.warn(
2004
- `This operation will process ${chalk2.bold(String(count))} specs, which exceeds the safety threshold of ${threshold}.`
2005
- );
2006
- const answer = await input({
2007
- message: `Type ${chalk2.bold('"yes"')} to proceed:`
2008
- });
2009
- return answer.trim().toLowerCase() === "yes";
2010
- }
2011
-
2012
- // src/helpers/prereqs.ts
2115
+ import { basename as basename2, join as join3 } from "path";
2116
+ import { mkdtemp, writeFile as writeFile2 } from "fs/promises";
2117
+ import { tmpdir } from "os";
2013
2118
  import { execFile as execFile5 } from "child_process";
2014
2119
  import { promisify as promisify5 } from "util";
2015
2120
  var exec5 = promisify5(execFile5);
2016
- var MIN_NODE_VERSION = "20.12.0";
2017
- function parseSemver(version) {
2018
- const [major, minor, patch] = version.split(".").map(Number);
2019
- return [major ?? 0, minor ?? 0, patch ?? 0];
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] };
2020
2126
  }
2021
- function semverGte(current, minimum) {
2022
- const [cMaj, cMin, cPat] = parseSemver(current);
2023
- const [mMaj, mMin, mPat] = parseSemver(minimum);
2024
- if (cMaj !== mMaj) return cMaj > mMaj;
2025
- if (cMin !== mMin) return cMin > mMin;
2026
- return cPat >= mPat;
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;
2027
2142
  }
2028
- async function checkPrereqs(context) {
2029
- const failures = [];
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) {
2030
2164
  try {
2031
- await exec5("git", ["--version"], { shell: process.platform === "win32" });
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);
2032
2171
  } catch {
2033
- failures.push("git is required but was not found on PATH. Install it from https://git-scm.com");
2172
+ return [];
2034
2173
  }
2035
- const nodeVersion = process.versions.node;
2036
- if (!semverGte(nodeVersion, MIN_NODE_VERSION)) {
2037
- failures.push(
2038
- `Node.js >= ${MIN_NODE_VERSION} is required but found ${nodeVersion}. Please upgrade Node.js`
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" }
2039
2181
  );
2182
+ return stdout;
2183
+ } catch {
2184
+ return "";
2040
2185
  }
2041
- if (context?.datasource === "github") {
2042
- try {
2043
- await exec5("gh", ["--version"], { shell: process.platform === "win32" });
2044
- } catch {
2045
- failures.push(
2046
- "gh (GitHub CLI) is required for the github datasource but was not found on PATH. Install it from https://cli.github.com/"
2047
- );
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}`);
2048
2204
  }
2205
+ sections.push("");
2049
2206
  }
2050
- if (context?.datasource === "azdevops") {
2051
- try {
2052
- await exec5("az", ["--version"], { shell: process.platform === "win32" });
2053
- } catch {
2054
- failures.push(
2055
- "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/"
2056
- );
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}`);
2057
2219
  }
2058
- }
2059
- return failures;
2060
- }
2061
-
2062
- // src/helpers/gitignore.ts
2063
- init_logger();
2064
- import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
2065
- import { join as join3 } from "path";
2066
- async function ensureGitignoreEntry(repoRoot, entry) {
2067
- const gitignorePath = join3(repoRoot, ".gitignore");
2068
- let contents = "";
2069
- try {
2070
- contents = await readFile2(gitignorePath, "utf8");
2071
- } catch (err) {
2072
- if (err instanceof Error && "code" in err && err.code === "ENOENT") {
2073
- } else {
2074
- log.warn(`Could not read .gitignore: ${String(err)}`);
2075
- return;
2220
+ for (const task of failedTasks) {
2221
+ sections.push(`- [ ] ${task.text}`);
2076
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
+
2348
+ // src/orchestrator/runner.ts
2349
+ init_cleanup();
2350
+ init_logger();
2351
+
2352
+ // src/helpers/confirm-large-batch.ts
2353
+ init_logger();
2354
+ import { input } from "@inquirer/prompts";
2355
+ import chalk2 from "chalk";
2356
+ var LARGE_BATCH_THRESHOLD = 100;
2357
+ async function confirmLargeBatch(count, threshold = LARGE_BATCH_THRESHOLD) {
2358
+ if (count <= threshold) return true;
2359
+ log.warn(
2360
+ `This operation will process ${chalk2.bold(String(count))} specs, which exceeds the safety threshold of ${threshold}.`
2361
+ );
2362
+ const answer = await input({
2363
+ message: `Type ${chalk2.bold('"yes"')} to proceed:`
2364
+ });
2365
+ return answer.trim().toLowerCase() === "yes";
2366
+ }
2367
+
2368
+ // src/helpers/prereqs.ts
2369
+ import { execFile as execFile7 } from "child_process";
2370
+ import { promisify as promisify7 } from "util";
2371
+ var exec7 = promisify7(execFile7);
2372
+ var MIN_NODE_VERSION = "20.12.0";
2373
+ function parseSemver(version) {
2374
+ const [major, minor, patch] = version.split(".").map(Number);
2375
+ return [major ?? 0, minor ?? 0, patch ?? 0];
2376
+ }
2377
+ function semverGte(current, minimum) {
2378
+ const [cMaj, cMin, cPat] = parseSemver(current);
2379
+ const [mMaj, mMin, mPat] = parseSemver(minimum);
2380
+ if (cMaj !== mMaj) return cMaj > mMaj;
2381
+ if (cMin !== mMin) return cMin > mMin;
2382
+ return cPat >= mPat;
2383
+ }
2384
+ async function checkPrereqs(context) {
2385
+ const failures = [];
2386
+ try {
2387
+ await exec7("git", ["--version"], { shell: process.platform === "win32" });
2388
+ } catch {
2389
+ failures.push("git is required but was not found on PATH. Install it from https://git-scm.com");
2390
+ }
2391
+ const nodeVersion = process.versions.node;
2392
+ if (!semverGte(nodeVersion, MIN_NODE_VERSION)) {
2393
+ failures.push(
2394
+ `Node.js >= ${MIN_NODE_VERSION} is required but found ${nodeVersion}. Please upgrade Node.js`
2395
+ );
2396
+ }
2397
+ if (context?.datasource === "github") {
2398
+ try {
2399
+ await exec7("gh", ["--version"], { shell: process.platform === "win32" });
2400
+ } catch {
2401
+ failures.push(
2402
+ "gh (GitHub CLI) is required for the github datasource but was not found on PATH. Install it from https://cli.github.com/"
2403
+ );
2404
+ }
2405
+ }
2406
+ if (context?.datasource === "azdevops") {
2407
+ try {
2408
+ await exec7("az", ["--version"], { shell: process.platform === "win32" });
2409
+ } catch {
2410
+ failures.push(
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/"
2412
+ );
2413
+ }
2414
+ }
2415
+ return failures;
2416
+ }
2417
+
2418
+ // src/helpers/gitignore.ts
2419
+ init_logger();
2420
+ import { readFile as readFile2, writeFile as writeFile3 } from "fs/promises";
2421
+ import { join as join5 } from "path";
2422
+ async function ensureGitignoreEntry(repoRoot, entry) {
2423
+ const gitignorePath = join5(repoRoot, ".gitignore");
2424
+ let contents = "";
2425
+ try {
2426
+ contents = await readFile2(gitignorePath, "utf8");
2427
+ } catch (err) {
2428
+ if (err instanceof Error && "code" in err && err.code === "ENOENT") {
2429
+ } else {
2430
+ log.warn(`Could not read .gitignore: ${String(err)}`);
2431
+ return;
2432
+ }
2433
+ }
2434
+ const lines = contents.split(/\r?\n/);
2435
+ const bare = entry.replace(/\/$/, "");
2436
+ const withSlash = bare + "/";
2437
+ if (lines.includes(entry) || lines.includes(bare) || lines.includes(withSlash)) {
2438
+ return;
2077
2439
  }
2078
- const lines = contents.split(/\r?\n/);
2079
- const bare = entry.replace(/\/$/, "");
2080
- const withSlash = bare + "/";
2081
- if (lines.includes(entry) || lines.includes(bare) || lines.includes(withSlash)) {
2082
- return;
2083
- }
2084
2440
  try {
2085
2441
  const separator = contents.length > 0 && !contents.endsWith("\n") ? "\n" : "";
2086
- await writeFile2(gitignorePath, `${contents}${separator}${entry}
2442
+ await writeFile3(gitignorePath, `${contents}${separator}${entry}
2087
2443
  `, "utf8");
2088
2444
  log.debug(`Added '${entry}' to .gitignore`);
2089
2445
  } catch (err) {
@@ -2093,14 +2449,14 @@ async function ensureGitignoreEntry(repoRoot, entry) {
2093
2449
 
2094
2450
  // src/orchestrator/cli-config.ts
2095
2451
  init_logger();
2096
- import { join as join5 } from "path";
2452
+ import { join as join7 } from "path";
2097
2453
  import { access } from "fs/promises";
2098
2454
  import { constants } from "fs";
2099
2455
 
2100
2456
  // src/config.ts
2101
2457
  init_providers();
2102
- import { readFile as readFile3, writeFile as writeFile3, mkdir as mkdir2 } from "fs/promises";
2103
- import { join as join4, dirname as dirname2 } 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";
2104
2460
 
2105
2461
  // src/config-prompts.ts
2106
2462
  init_logger();
@@ -2277,8 +2633,8 @@ var CONFIG_BOUNDS = {
2277
2633
  };
2278
2634
  var CONFIG_KEYS = ["provider", "model", "source", "testTimeout", "planTimeout", "concurrency", "org", "project", "workItemType", "iteration", "area"];
2279
2635
  function getConfigPath(configDir) {
2280
- const dir = configDir ?? join4(process.cwd(), ".dispatch");
2281
- return join4(dir, "config.json");
2636
+ const dir = configDir ?? join6(process.cwd(), ".dispatch");
2637
+ return join6(dir, "config.json");
2282
2638
  }
2283
2639
  async function loadConfig(configDir) {
2284
2640
  const configPath = getConfigPath(configDir);
@@ -2291,8 +2647,8 @@ async function loadConfig(configDir) {
2291
2647
  }
2292
2648
  async function saveConfig(config, configDir) {
2293
2649
  const configPath = getConfigPath(configDir);
2294
- await mkdir2(dirname2(configPath), { recursive: true });
2295
- await writeFile3(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
2650
+ await mkdir2(dirname3(configPath), { recursive: true });
2651
+ await writeFile4(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
2296
2652
  }
2297
2653
  async function handleConfigCommand(_argv, configDir) {
2298
2654
  await runInteractiveConfigWizard(configDir);
@@ -2317,7 +2673,7 @@ function setCliField(target, key, value) {
2317
2673
  }
2318
2674
  async function resolveCliConfig(args) {
2319
2675
  const { explicitFlags } = args;
2320
- const configDir = join5(args.cwd, ".dispatch");
2676
+ const configDir = join7(args.cwd, ".dispatch");
2321
2677
  const config = await loadConfig(configDir);
2322
2678
  const merged = { ...args };
2323
2679
  for (const configKey of CONFIG_KEYS) {
@@ -2345,7 +2701,7 @@ async function resolveCliConfig(args) {
2345
2701
  }
2346
2702
  }
2347
2703
  const sourceConfigured = explicitFlags.has("issueSource") || config.source !== void 0;
2348
- const needsSource = !merged.fixTests && !merged.spec && !merged.respec;
2704
+ const needsSource = !(merged.fixTests && merged.issueIds.length === 0) && !merged.spec && !merged.respec;
2349
2705
  if (needsSource && !sourceConfigured) {
2350
2706
  const detected = await detectDatasource(merged.cwd);
2351
2707
  if (detected) {
@@ -2364,17 +2720,18 @@ async function resolveCliConfig(args) {
2364
2720
  }
2365
2721
 
2366
2722
  // src/orchestrator/spec-pipeline.ts
2367
- import { join as join7 } from "path";
2723
+ import { join as join9 } from "path";
2368
2724
  import { mkdir as mkdir4, readFile as readFile5, rename as rename2, unlink as unlink2 } from "fs/promises";
2369
- import { glob } from "glob";
2725
+ import { glob as glob2 } from "glob";
2370
2726
  init_providers();
2371
2727
 
2372
2728
  // src/agents/spec.ts
2373
- import { mkdir as mkdir3, readFile as readFile4, writeFile as writeFile4, unlink } from "fs/promises";
2374
- import { join as join6, resolve, sep } from "path";
2375
- 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";
2376
2732
  init_logger();
2377
2733
  init_file_logger();
2734
+ init_environment();
2378
2735
  async function boot5(opts) {
2379
2736
  const { provider } = opts;
2380
2737
  if (!provider) {
@@ -2386,8 +2743,8 @@ async function boot5(opts) {
2386
2743
  const { issue, filePath, fileContent, inlineText, cwd: workingDir, outputPath } = genOpts;
2387
2744
  const startTime = Date.now();
2388
2745
  try {
2389
- const resolvedCwd = resolve(workingDir);
2390
- const resolvedOutput = resolve(outputPath);
2746
+ const resolvedCwd = resolve2(workingDir);
2747
+ const resolvedOutput = resolve2(outputPath);
2391
2748
  if (resolvedOutput !== resolvedCwd && !resolvedOutput.startsWith(resolvedCwd + sep)) {
2392
2749
  return {
2393
2750
  data: null,
@@ -2396,10 +2753,10 @@ async function boot5(opts) {
2396
2753
  durationMs: Date.now() - startTime
2397
2754
  };
2398
2755
  }
2399
- const tmpDir = join6(resolvedCwd, ".dispatch", "tmp");
2756
+ const tmpDir = join8(resolvedCwd, ".dispatch", "tmp");
2400
2757
  await mkdir3(tmpDir, { recursive: true });
2401
- const tmpFilename = `spec-${randomUUID3()}.md`;
2402
- const tmpPath = join6(tmpDir, tmpFilename);
2758
+ const tmpFilename = `spec-${randomUUID4()}.md`;
2759
+ const tmpPath = join8(tmpDir, tmpFilename);
2403
2760
  let prompt;
2404
2761
  if (issue) {
2405
2762
  prompt = buildSpecPrompt(issue, workingDir, tmpPath);
@@ -2446,7 +2803,7 @@ async function boot5(opts) {
2446
2803
  if (!validation.valid) {
2447
2804
  log.warn(`Spec validation warning for ${outputPath}: ${validation.reason}`);
2448
2805
  }
2449
- await writeFile4(resolvedOutput, cleanedContent, "utf-8");
2806
+ await writeFile5(resolvedOutput, cleanedContent, "utf-8");
2450
2807
  log.debug(`Wrote cleaned spec to ${resolvedOutput}`);
2451
2808
  try {
2452
2809
  await unlink(tmpPath);
@@ -2564,6 +2921,8 @@ function buildCommonSpecInstructions(params) {
2564
2921
  ``,
2565
2922
  `\`${cwd}\``,
2566
2923
  ``,
2924
+ formatEnvironmentPrompt(),
2925
+ ``,
2567
2926
  `## Instructions`,
2568
2927
  ``,
2569
2928
  `1. **Explore the codebase** \u2014 read relevant files, search for symbols, understand the project structure, language, frameworks, conventions, and patterns. Identify the tech stack (languages, package managers, frameworks, test runners) so your spec aligns with the project's actual standards.`,
@@ -2799,7 +3158,7 @@ function buildInlineTextItem(issues, outputDir) {
2799
3158
  const title = text.length > 80 ? text.slice(0, 80).trimEnd() + "\u2026" : text;
2800
3159
  const slug = slugify(text, MAX_SLUG_LENGTH);
2801
3160
  const filename = `${slug}.md`;
2802
- const filepath = join7(outputDir, filename);
3161
+ const filepath = join9(outputDir, filename);
2803
3162
  const details = {
2804
3163
  number: filepath,
2805
3164
  title,
@@ -2814,7 +3173,7 @@ function buildInlineTextItem(issues, outputDir) {
2814
3173
  return [{ id: filepath, details }];
2815
3174
  }
2816
3175
  async function resolveFileItems(issues, specCwd, concurrency) {
2817
- const files = await glob(issues, { cwd: specCwd, absolute: true });
3176
+ const files = await glob2(issues, { cwd: specCwd, absolute: true });
2818
3177
  if (files.length === 0) {
2819
3178
  log.error(`No files matched the pattern "${Array.isArray(issues) ? issues.join(", ") : issues}".`);
2820
3179
  return null;
@@ -2861,7 +3220,7 @@ function previewDryRun(validItems, items, isTrackerMode, isInlineText, outputDir
2861
3220
  let filepath;
2862
3221
  if (isTrackerMode) {
2863
3222
  const slug = slugify(details.title, 60);
2864
- filepath = join7(outputDir, `${id}-${slug}.md`);
3223
+ filepath = join9(outputDir, `${id}-${slug}.md`);
2865
3224
  } else {
2866
3225
  filepath = id;
2867
3226
  }
@@ -2924,7 +3283,7 @@ async function generateSpecsBatch(validItems, items, specAgent, instance, isTrac
2924
3283
  if (isTrackerMode) {
2925
3284
  const slug = slugify(details.title, MAX_SLUG_LENGTH);
2926
3285
  const filename = `${id}-${slug}.md`;
2927
- filepath = join7(outputDir, filename);
3286
+ filepath = join9(outputDir, filename);
2928
3287
  } else if (isInlineText) {
2929
3288
  filepath = id;
2930
3289
  } else {
@@ -2953,7 +3312,7 @@ async function generateSpecsBatch(validItems, items, specAgent, instance, isTrac
2953
3312
  const h1Title = extractTitle(result.data.content, filepath);
2954
3313
  const h1Slug = slugify(h1Title, MAX_SLUG_LENGTH);
2955
3314
  const finalFilename = isTrackerMode ? `${id}-${h1Slug}.md` : `${h1Slug}.md`;
2956
- const finalFilepath = join7(outputDir, finalFilename);
3315
+ const finalFilepath = join9(outputDir, finalFilename);
2957
3316
  if (finalFilepath !== filepath) {
2958
3317
  await rename2(filepath, finalFilepath);
2959
3318
  filepath = finalFilepath;
@@ -3058,7 +3417,7 @@ async function runSpecPipeline(opts) {
3058
3417
  model,
3059
3418
  serverUrl,
3060
3419
  cwd: specCwd,
3061
- outputDir = join7(specCwd, ".dispatch", "specs"),
3420
+ outputDir = join9(specCwd, ".dispatch", "specs"),
3062
3421
  org,
3063
3422
  project,
3064
3423
  workItemType,
@@ -3136,9 +3495,10 @@ async function runSpecPipeline(opts) {
3136
3495
  import { execFile as execFile9 } from "child_process";
3137
3496
  import { promisify as promisify9 } from "util";
3138
3497
  import { readFile as readFile7 } from "fs/promises";
3498
+ import { glob as glob3 } from "glob";
3139
3499
 
3140
3500
  // src/parser.ts
3141
- import { readFile as readFile6, writeFile as writeFile5 } from "fs/promises";
3501
+ import { readFile as readFile6, writeFile as writeFile6 } from "fs/promises";
3142
3502
  var UNCHECKED_RE = /^(\s*[-*]\s)\[ \]\s+(.+)$/;
3143
3503
  var CHECKED_SUB = "$1[x] $2";
3144
3504
  var MODE_PREFIX_RE = /^\(([PSI])\)\s+/;
@@ -3206,7 +3566,7 @@ async function markTaskComplete(task) {
3206
3566
  );
3207
3567
  }
3208
3568
  lines[lineIndex] = updated;
3209
- await writeFile5(task.file, lines.join(eol), "utf-8");
3569
+ await writeFile6(task.file, lines.join(eol), "utf-8");
3210
3570
  }
3211
3571
  function groupTasksByMode(tasks) {
3212
3572
  if (tasks.length === 0) return [];
@@ -3237,6 +3597,7 @@ function groupTasksByMode(tasks) {
3237
3597
  // src/agents/planner.ts
3238
3598
  init_logger();
3239
3599
  init_file_logger();
3600
+ init_environment();
3240
3601
  async function boot6(opts) {
3241
3602
  const { provider, cwd } = opts;
3242
3603
  if (!provider) {
@@ -3307,6 +3668,10 @@ function buildPlannerPrompt(task, cwd, fileContext, worktreeRoot) {
3307
3668
  `- All relative paths must resolve within the worktree root above.`
3308
3669
  );
3309
3670
  }
3671
+ sections.push(
3672
+ ``,
3673
+ formatEnvironmentPrompt()
3674
+ );
3310
3675
  sections.push(
3311
3676
  ``,
3312
3677
  `## Instructions`,
@@ -3343,6 +3708,7 @@ function buildPlannerPrompt(task, cwd, fileContext, worktreeRoot) {
3343
3708
  // src/dispatcher.ts
3344
3709
  init_logger();
3345
3710
  init_file_logger();
3711
+ init_environment();
3346
3712
  async function dispatchTask(instance, task, cwd, plan, worktreeRoot) {
3347
3713
  try {
3348
3714
  log.debug(`Dispatching task: ${task.file}:${task.line} \u2014 ${task.text.slice(0, 80)}`);
@@ -3375,6 +3741,8 @@ function buildPrompt(task, cwd, worktreeRoot) {
3375
3741
  `**Source file:** ${task.file}`,
3376
3742
  `**Task (line ${task.line}):** ${task.text}`,
3377
3743
  ``,
3744
+ getEnvironmentBlock(),
3745
+ ``,
3378
3746
  `Instructions:`,
3379
3747
  `- Complete ONLY this specific task \u2014 do not work on other tasks.`,
3380
3748
  `- Make the minimal, correct changes needed.`,
@@ -3392,6 +3760,8 @@ function buildPlannedPrompt(task, cwd, plan, worktreeRoot) {
3392
3760
  `**Source file:** ${task.file}`,
3393
3761
  `**Task (line ${task.line}):** ${task.text}`,
3394
3762
  ``,
3763
+ getEnvironmentBlock(),
3764
+ ``,
3395
3765
  `---`,
3396
3766
  ``,
3397
3767
  `## Execution Plan`,
@@ -3466,9 +3836,10 @@ ${err.stack}` : ""}`);
3466
3836
  // src/agents/commit.ts
3467
3837
  init_logger();
3468
3838
  init_file_logger();
3469
- import { mkdir as mkdir5, writeFile as writeFile6 } from "fs/promises";
3470
- import { join as join8, resolve as resolve2 } from "path";
3471
- import { randomUUID as randomUUID4 } from "crypto";
3839
+ init_environment();
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";
3472
3843
  async function boot8(opts) {
3473
3844
  const { provider } = opts;
3474
3845
  if (!provider) {
@@ -3480,11 +3851,11 @@ async function boot8(opts) {
3480
3851
  name: "commit",
3481
3852
  async generate(genOpts) {
3482
3853
  try {
3483
- const resolvedCwd = resolve2(genOpts.cwd);
3484
- const tmpDir = join8(resolvedCwd, ".dispatch", "tmp");
3854
+ const resolvedCwd = resolve3(genOpts.cwd);
3855
+ const tmpDir = join10(resolvedCwd, ".dispatch", "tmp");
3485
3856
  await mkdir5(tmpDir, { recursive: true });
3486
- const tmpFilename = `commit-${randomUUID4()}.md`;
3487
- const tmpPath = join8(tmpDir, tmpFilename);
3857
+ const tmpFilename = `commit-${randomUUID5()}.md`;
3858
+ const tmpPath = join10(tmpDir, tmpFilename);
3488
3859
  const prompt = buildCommitPrompt(genOpts);
3489
3860
  fileLoggerStorage.getStore()?.prompt("commit", prompt);
3490
3861
  const sessionId = await provider.createSession();
@@ -3512,7 +3883,7 @@ async function boot8(opts) {
3512
3883
  };
3513
3884
  }
3514
3885
  const outputContent = formatOutputFile(parsed);
3515
- await writeFile6(tmpPath, outputContent, "utf-8");
3886
+ await writeFile7(tmpPath, outputContent, "utf-8");
3516
3887
  log.debug(`Wrote commit agent output to ${tmpPath}`);
3517
3888
  fileLoggerStorage.getStore()?.agentEvent("commit", "completed", `message: ${parsed.commitMessage.slice(0, 80)}`);
3518
3889
  return {
@@ -3542,6 +3913,8 @@ function buildCommitPrompt(opts) {
3542
3913
  const sections = [
3543
3914
  `You are a **commit message agent**. Your job is to analyze the git diff below and generate a meaningful, conventional-commit-compliant commit message, a PR title, and a PR description.`,
3544
3915
  ``,
3916
+ formatEnvironmentPrompt(),
3917
+ ``,
3545
3918
  `## Conventional Commit Guidelines`,
3546
3919
  ``,
3547
3920
  `Follow the Conventional Commits specification (https://www.conventionalcommits.org/):`,
@@ -3662,68 +4035,6 @@ function formatOutputFile(parsed) {
3662
4035
  init_logger();
3663
4036
  init_cleanup();
3664
4037
 
3665
- // src/helpers/worktree.ts
3666
- import { join as join9, basename } from "path";
3667
- import { execFile as execFile7 } from "child_process";
3668
- import { promisify as promisify7 } from "util";
3669
- import { randomUUID as randomUUID5 } from "crypto";
3670
- init_logger();
3671
- var exec7 = promisify7(execFile7);
3672
- var WORKTREE_DIR = ".dispatch/worktrees";
3673
- async function git2(args, cwd) {
3674
- const { stdout } = await exec7("git", args, { cwd });
3675
- return stdout;
3676
- }
3677
- function worktreeName(issueFilename) {
3678
- const base = basename(issueFilename);
3679
- const withoutExt = base.replace(/\.md$/i, "");
3680
- const match = withoutExt.match(/^(\d+)/);
3681
- return match ? `issue-${match[1]}` : slugify(withoutExt);
3682
- }
3683
- async function createWorktree(repoRoot, issueFilename, branchName, startPoint) {
3684
- const name = worktreeName(issueFilename);
3685
- const worktreePath = join9(repoRoot, WORKTREE_DIR, name);
3686
- try {
3687
- const args = ["worktree", "add", worktreePath, "-b", branchName];
3688
- if (startPoint) args.push(startPoint);
3689
- await git2(args, repoRoot);
3690
- log.debug(`Created worktree at ${worktreePath} on branch ${branchName}`);
3691
- } catch (err) {
3692
- const message = log.extractMessage(err);
3693
- if (message.includes("already exists")) {
3694
- await git2(["worktree", "add", worktreePath, branchName], repoRoot);
3695
- log.debug(`Created worktree at ${worktreePath} using existing branch ${branchName}`);
3696
- } else {
3697
- throw err;
3698
- }
3699
- }
3700
- return worktreePath;
3701
- }
3702
- async function removeWorktree(repoRoot, issueFilename) {
3703
- const name = worktreeName(issueFilename);
3704
- const worktreePath = join9(repoRoot, WORKTREE_DIR, name);
3705
- try {
3706
- await git2(["worktree", "remove", worktreePath], repoRoot);
3707
- } catch {
3708
- try {
3709
- await git2(["worktree", "remove", "--force", worktreePath], repoRoot);
3710
- } catch (err) {
3711
- log.warn(`Could not remove worktree ${name}: ${log.formatErrorChain(err)}`);
3712
- return;
3713
- }
3714
- }
3715
- try {
3716
- await git2(["worktree", "prune"], repoRoot);
3717
- } catch (err) {
3718
- log.warn(`Could not prune worktrees: ${log.formatErrorChain(err)}`);
3719
- }
3720
- }
3721
- function generateFeatureBranchName() {
3722
- const uuid = randomUUID5();
3723
- const octet = uuid.split("-")[0];
3724
- return `dispatch/feature-${octet}`;
3725
- }
3726
-
3727
4038
  // src/tui.ts
3728
4039
  import chalk6 from "chalk";
3729
4040
  var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
@@ -3980,184 +4291,38 @@ function createTui() {
3980
4291
 
3981
4292
  // src/orchestrator/dispatch-pipeline.ts
3982
4293
  init_providers();
3983
-
3984
- // src/orchestrator/datasource-helpers.ts
3985
- init_logger();
3986
- import { basename as basename2, join as join10 } from "path";
3987
- import { mkdtemp, writeFile as writeFile7 } from "fs/promises";
3988
- import { tmpdir } from "os";
3989
- import { execFile as execFile8 } from "child_process";
3990
- import { promisify as promisify8 } from "util";
3991
- var exec8 = promisify8(execFile8);
3992
- function parseIssueFilename(filePath) {
3993
- const filename = basename2(filePath);
3994
- const match = /^(\d+)-(.+)\.md$/.exec(filename);
3995
- if (!match) return null;
3996
- return { issueId: match[1], slug: match[2] };
3997
- }
3998
- async function fetchItemsById(issueIds, datasource4, fetchOpts) {
3999
- const ids = issueIds.flatMap(
4000
- (id) => id.split(",").map((s) => s.trim()).filter(Boolean)
4001
- );
4294
+ init_timeout();
4295
+ import chalk7 from "chalk";
4296
+ init_file_logger();
4297
+ var exec9 = promisify9(execFile9);
4298
+ async function resolveGlobItems(patterns, cwd) {
4299
+ const files = await glob3(patterns, { cwd, absolute: true });
4300
+ if (files.length === 0) {
4301
+ log.warn(`No files matched the pattern(s): ${patterns.join(", ")}`);
4302
+ return [];
4303
+ }
4304
+ log.info(`Matched ${files.length} file(s) from glob pattern(s)`);
4002
4305
  const items = [];
4003
- for (const id of ids) {
4306
+ for (const filePath of files) {
4004
4307
  try {
4005
- const item = await datasource4.fetch(id, fetchOpts);
4006
- items.push(item);
4308
+ const content = await readFile7(filePath, "utf-8");
4309
+ const title = extractTitle(content, filePath);
4310
+ items.push({
4311
+ number: filePath,
4312
+ title,
4313
+ body: content,
4314
+ labels: [],
4315
+ state: "open",
4316
+ url: filePath,
4317
+ comments: [],
4318
+ acceptanceCriteria: ""
4319
+ });
4007
4320
  } catch (err) {
4008
- log.warn(`Could not fetch issue #${id}: ${log.formatErrorChain(err)}`);
4321
+ log.warn(`Could not read file ${filePath}: ${log.formatErrorChain(err)}`);
4009
4322
  }
4010
4323
  }
4011
4324
  return items;
4012
4325
  }
4013
- async function writeItemsToTempDir(items) {
4014
- const tempDir = await mkdtemp(join10(tmpdir(), "dispatch-"));
4015
- const files = [];
4016
- const issueDetailsByFile = /* @__PURE__ */ new Map();
4017
- for (const item of items) {
4018
- const slug = slugify(item.title, MAX_SLUG_LENGTH);
4019
- const filename = `${item.number}-${slug}.md`;
4020
- const filepath = join10(tempDir, filename);
4021
- await writeFile7(filepath, item.body, "utf-8");
4022
- files.push(filepath);
4023
- issueDetailsByFile.set(filepath, item);
4024
- }
4025
- files.sort((a, b) => {
4026
- const numA = parseInt(basename2(a).match(/^(\d+)/)?.[1] ?? "0", 10);
4027
- const numB = parseInt(basename2(b).match(/^(\d+)/)?.[1] ?? "0", 10);
4028
- if (numA !== numB) return numA - numB;
4029
- return a.localeCompare(b);
4030
- });
4031
- return { files, issueDetailsByFile };
4032
- }
4033
- async function getCommitSummaries(defaultBranch, cwd) {
4034
- try {
4035
- const { stdout } = await exec8(
4036
- "git",
4037
- ["log", `${defaultBranch}..HEAD`, "--pretty=format:%s"],
4038
- { cwd }
4039
- );
4040
- return stdout.trim().split("\n").filter(Boolean);
4041
- } catch {
4042
- return [];
4043
- }
4044
- }
4045
- async function getBranchDiff(defaultBranch, cwd) {
4046
- try {
4047
- const { stdout } = await exec8(
4048
- "git",
4049
- ["diff", `${defaultBranch}..HEAD`],
4050
- { cwd, maxBuffer: 10 * 1024 * 1024 }
4051
- );
4052
- return stdout;
4053
- } catch {
4054
- return "";
4055
- }
4056
- }
4057
- async function squashBranchCommits(defaultBranch, message, cwd) {
4058
- const { stdout } = await exec8(
4059
- "git",
4060
- ["merge-base", defaultBranch, "HEAD"],
4061
- { cwd }
4062
- );
4063
- const mergeBase = stdout.trim();
4064
- await exec8("git", ["reset", "--soft", mergeBase], { cwd });
4065
- await exec8("git", ["commit", "-m", message], { cwd });
4066
- }
4067
- async function buildPrBody(details, tasks, results, defaultBranch, datasourceName, cwd) {
4068
- const sections = [];
4069
- const commits = await getCommitSummaries(defaultBranch, cwd);
4070
- if (commits.length > 0) {
4071
- sections.push("## Summary\n");
4072
- for (const commit of commits) {
4073
- sections.push(`- ${commit}`);
4074
- }
4075
- sections.push("");
4076
- }
4077
- const taskResults = new Map(
4078
- results.filter((r) => tasks.includes(r.task)).map((r) => [r.task, r])
4079
- );
4080
- const completedTasks = tasks.filter((t) => taskResults.get(t)?.success);
4081
- const failedTasks = tasks.filter((t) => {
4082
- const r = taskResults.get(t);
4083
- return r && !r.success;
4084
- });
4085
- if (completedTasks.length > 0 || failedTasks.length > 0) {
4086
- sections.push("## Tasks\n");
4087
- for (const task of completedTasks) {
4088
- sections.push(`- [x] ${task.text}`);
4089
- }
4090
- for (const task of failedTasks) {
4091
- sections.push(`- [ ] ${task.text}`);
4092
- }
4093
- sections.push("");
4094
- }
4095
- if (details.labels.length > 0) {
4096
- sections.push(`**Labels:** ${details.labels.join(", ")}
4097
- `);
4098
- }
4099
- if (datasourceName === "github") {
4100
- sections.push(`Closes #${details.number}`);
4101
- } else if (datasourceName === "azdevops") {
4102
- sections.push(`Resolves AB#${details.number}`);
4103
- }
4104
- return sections.join("\n");
4105
- }
4106
- async function buildPrTitle(issueTitle, defaultBranch, cwd) {
4107
- const commits = await getCommitSummaries(defaultBranch, cwd);
4108
- if (commits.length === 0) {
4109
- return issueTitle;
4110
- }
4111
- if (commits.length === 1) {
4112
- return commits[0];
4113
- }
4114
- return `${commits[commits.length - 1]} (+${commits.length - 1} more)`;
4115
- }
4116
- function buildFeaturePrTitle(featureBranchName, issues) {
4117
- if (issues.length === 1) {
4118
- return issues[0].title;
4119
- }
4120
- const issueRefs = issues.map((d) => `#${d.number}`).join(", ");
4121
- return `feat: ${featureBranchName} (${issueRefs})`;
4122
- }
4123
- function buildFeaturePrBody(issues, tasks, results, datasourceName) {
4124
- const sections = [];
4125
- sections.push("## Issues\n");
4126
- for (const issue of issues) {
4127
- sections.push(`- #${issue.number}: ${issue.title}`);
4128
- }
4129
- sections.push("");
4130
- const taskResults = new Map(results.map((r) => [r.task, r]));
4131
- const completedTasks = tasks.filter((t) => taskResults.get(t)?.success);
4132
- const failedTasks = tasks.filter((t) => {
4133
- const r = taskResults.get(t);
4134
- return r && !r.success;
4135
- });
4136
- if (completedTasks.length > 0 || failedTasks.length > 0) {
4137
- sections.push("## Tasks\n");
4138
- for (const task of completedTasks) {
4139
- sections.push(`- [x] ${task.text}`);
4140
- }
4141
- for (const task of failedTasks) {
4142
- sections.push(`- [ ] ${task.text}`);
4143
- }
4144
- sections.push("");
4145
- }
4146
- for (const issue of issues) {
4147
- if (datasourceName === "github") {
4148
- sections.push(`Closes #${issue.number}`);
4149
- } else if (datasourceName === "azdevops") {
4150
- sections.push(`Resolves AB#${issue.number}`);
4151
- }
4152
- }
4153
- return sections.join("\n");
4154
- }
4155
-
4156
- // src/orchestrator/dispatch-pipeline.ts
4157
- init_timeout();
4158
- import chalk7 from "chalk";
4159
- init_file_logger();
4160
- var exec9 = promisify9(execFile9);
4161
4326
  var DEFAULT_PLAN_TIMEOUT_MIN = 10;
4162
4327
  var DEFAULT_PLAN_RETRIES = 1;
4163
4328
  async function runDispatchPipeline(opts, cwd) {
@@ -4223,7 +4388,14 @@ async function runDispatchPipeline(opts, cwd) {
4223
4388
  }
4224
4389
  const datasource4 = getDatasource(source);
4225
4390
  const fetchOpts = { cwd, org, project, workItemType, iteration, area };
4226
- const items = issueIds.length > 0 ? await fetchItemsById(issueIds, datasource4, fetchOpts) : await datasource4.list(fetchOpts);
4391
+ let items;
4392
+ if (issueIds.length > 0 && source === "md" && issueIds.some((id) => isGlobOrFilePath(id))) {
4393
+ items = await resolveGlobItems(issueIds, cwd);
4394
+ } else if (issueIds.length > 0) {
4395
+ items = await fetchItemsById(issueIds, datasource4, fetchOpts);
4396
+ } else {
4397
+ items = await datasource4.list(fetchOpts);
4398
+ }
4227
4399
  if (items.length === 0) {
4228
4400
  tui.state.phase = "done";
4229
4401
  tui.stop();
@@ -4295,12 +4467,32 @@ async function runDispatchPipeline(opts, cwd) {
4295
4467
  let featureBranchName;
4296
4468
  let featureDefaultBranch;
4297
4469
  if (feature) {
4470
+ if (typeof feature === "string") {
4471
+ if (!isValidBranchName(feature)) {
4472
+ log.error(`Invalid feature branch name: "${feature}"`);
4473
+ tui.state.phase = "done";
4474
+ tui.stop();
4475
+ return { total: allTasks.length, completed: 0, failed: allTasks.length, skipped: 0, results: [] };
4476
+ }
4477
+ featureBranchName = feature.includes("/") ? feature : `dispatch/${feature}`;
4478
+ } else {
4479
+ featureBranchName = generateFeatureBranchName();
4480
+ }
4298
4481
  try {
4299
4482
  featureDefaultBranch = await datasource4.getDefaultBranch(lifecycleOpts);
4300
4483
  await datasource4.switchBranch(featureDefaultBranch, lifecycleOpts);
4301
- featureBranchName = generateFeatureBranchName();
4302
- await datasource4.createAndSwitchBranch(featureBranchName, lifecycleOpts);
4303
- log.debug(`Created feature branch ${featureBranchName} from ${featureDefaultBranch}`);
4484
+ try {
4485
+ await datasource4.createAndSwitchBranch(featureBranchName, lifecycleOpts);
4486
+ log.debug(`Created feature branch ${featureBranchName} from ${featureDefaultBranch}`);
4487
+ } catch (createErr) {
4488
+ const message = log.extractMessage(createErr);
4489
+ if (message.includes("already exists")) {
4490
+ await datasource4.switchBranch(featureBranchName, lifecycleOpts);
4491
+ log.debug(`Switched to existing feature branch ${featureBranchName}`);
4492
+ } else {
4493
+ throw createErr;
4494
+ }
4495
+ }
4304
4496
  registerCleanup(async () => {
4305
4497
  try {
4306
4498
  await datasource4.switchBranch(featureDefaultBranch, lifecycleOpts);
@@ -4492,12 +4684,18 @@ ${err.stack}` : ""}`);
4492
4684
  fileLogger?.info(`Execution completed successfully (${Date.now() - startTime}ms)`);
4493
4685
  try {
4494
4686
  const parsed = parseIssueFilename(task.file);
4687
+ const updatedContent = await readFile7(task.file, "utf-8");
4495
4688
  if (parsed) {
4496
- const updatedContent = await readFile7(task.file, "utf-8");
4497
4689
  const issueDetails = issueDetailsByFile.get(task.file);
4498
4690
  const title = issueDetails?.title ?? parsed.slug;
4499
4691
  await datasource4.update(parsed.issueId, title, updatedContent, fetchOpts);
4500
4692
  log.success(`Synced task completion to issue #${parsed.issueId}`);
4693
+ } else {
4694
+ const issueDetails = issueDetailsByFile.get(task.file);
4695
+ if (issueDetails) {
4696
+ await datasource4.update(issueDetails.number, issueDetails.title, updatedContent, fetchOpts);
4697
+ log.success(`Synced task completion to issue #${issueDetails.number}`);
4698
+ }
4501
4699
  }
4502
4700
  } catch (err) {
4503
4701
  log.warn(`Could not sync task completion to datasource: ${log.formatErrorChain(err)}`);
@@ -4584,13 +4782,13 @@ ${err.stack}` : ""}`);
4584
4782
  }
4585
4783
  try {
4586
4784
  await datasource4.switchBranch(featureBranchName, lifecycleOpts);
4587
- await exec9("git", ["merge", branchName, "--no-ff", "-m", `merge: issue #${details.number}`], { cwd });
4785
+ await exec9("git", ["merge", branchName, "--no-ff", "-m", `merge: issue #${details.number}`], { cwd, shell: process.platform === "win32" });
4588
4786
  log.debug(`Merged ${branchName} into ${featureBranchName}`);
4589
4787
  } catch (err) {
4590
4788
  const mergeError = `Could not merge ${branchName} into feature branch: ${log.formatErrorChain(err)}`;
4591
4789
  log.warn(mergeError);
4592
4790
  try {
4593
- await exec9("git", ["merge", "--abort"], { cwd });
4791
+ await exec9("git", ["merge", "--abort"], { cwd, shell: process.platform === "win32" });
4594
4792
  } catch {
4595
4793
  }
4596
4794
  for (const task of fileTasks) {
@@ -4608,7 +4806,7 @@ ${err.stack}` : ""}`);
4608
4806
  return;
4609
4807
  }
4610
4808
  try {
4611
- await exec9("git", ["branch", "-d", branchName], { cwd });
4809
+ await exec9("git", ["branch", "-d", branchName], { cwd, shell: process.platform === "win32" });
4612
4810
  log.debug(`Deleted local branch ${branchName}`);
4613
4811
  } catch (err) {
4614
4812
  log.warn(`Could not delete local branch ${branchName}: ${log.formatErrorChain(err)}`);
@@ -4764,13 +4962,20 @@ async function dryRunMode(issueIds, cwd, source, org, project, workItemType, ite
4764
4962
  username = await datasource4.getUsername(lifecycleOpts);
4765
4963
  } catch {
4766
4964
  }
4767
- const items = issueIds.length > 0 ? await fetchItemsById(issueIds, datasource4, fetchOpts) : await datasource4.list(fetchOpts);
4965
+ let items;
4966
+ if (issueIds.length > 0 && source === "md" && issueIds.some((id) => isGlobOrFilePath(id))) {
4967
+ items = await resolveGlobItems(issueIds, cwd);
4968
+ } else if (issueIds.length > 0) {
4969
+ items = await fetchItemsById(issueIds, datasource4, fetchOpts);
4970
+ } else {
4971
+ items = await datasource4.list(fetchOpts);
4972
+ }
4768
4973
  if (items.length === 0) {
4769
4974
  const label = issueIds.length > 0 ? `issue(s) ${issueIds.join(", ")}` : `datasource: ${source}`;
4770
4975
  log.warn("No work items found from " + label);
4771
4976
  return { total: 0, completed: 0, failed: 0, skipped: 0, results: [] };
4772
4977
  }
4773
- const { files } = await writeItemsToTempDir(items);
4978
+ const { files, issueDetailsByFile } = await writeItemsToTempDir(items);
4774
4979
  const taskFiles = [];
4775
4980
  for (const file of files) {
4776
4981
  const tf = await parseTaskFile(file);
@@ -4787,7 +4992,7 @@ async function dryRunMode(issueIds, cwd, source, org, project, workItemType, ite
4787
4992
  `);
4788
4993
  for (const task of allTasks) {
4789
4994
  const parsed = parseIssueFilename(task.file);
4790
- const details = parsed ? items.find((item) => item.number === parsed.issueId) : void 0;
4995
+ const details = parsed ? items.find((item) => item.number === parsed.issueId) : issueDetailsByFile.get(task.file);
4791
4996
  const branchInfo = details ? ` [branch: ${datasource4.buildBranchName(details.number, details.title, username)}]` : "";
4792
4997
  log.task(allTasks.indexOf(task), allTasks.length, `${task.file}:${task.line} \u2014 ${task.text}${branchInfo}`);
4793
4998
  }
@@ -4801,6 +5006,58 @@ async function dryRunMode(issueIds, cwd, source, org, project, workItemType, ite
4801
5006
  }
4802
5007
 
4803
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
+ }
4804
5061
  async function boot9(opts) {
4805
5062
  const { cwd } = opts;
4806
5063
  const runner = {
@@ -4813,7 +5070,25 @@ async function boot9(opts) {
4813
5070
  }
4814
5071
  if (opts2.mode === "fix-tests") {
4815
5072
  const { runFixTestsPipeline: runFixTestsPipeline2 } = await Promise.resolve().then(() => (init_fix_tests_pipeline(), fix_tests_pipeline_exports));
4816
- 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
+ });
4817
5092
  }
4818
5093
  const { mode: _, ...rest } = opts2;
4819
5094
  return runner.orchestrate(rest);
@@ -4842,13 +5117,27 @@ async function boot9(opts) {
4842
5117
  log.error("--feature and --no-branch are mutually exclusive");
4843
5118
  process.exit(1);
4844
5119
  }
4845
- if (m.fixTests && m.issueIds.length > 0) {
4846
- log.error("--fix-tests cannot be combined with issue IDs");
4847
- process.exit(1);
4848
- }
4849
5120
  if (m.fixTests) {
4850
5121
  const { runFixTestsPipeline: runFixTestsPipeline2 } = await Promise.resolve().then(() => (init_fix_tests_pipeline(), fix_tests_pipeline_exports));
4851
- 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
+ });
4852
5141
  }
4853
5142
  if (m.spec) {
4854
5143
  return this.generateSpecs({
@@ -4954,13 +5243,14 @@ var HELP = `
4954
5243
  dispatch --respec <glob> Regenerate specs matching a glob pattern
4955
5244
  dispatch --spec "description" Generate a spec from an inline text description
4956
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)
4957
5247
 
4958
5248
  Dispatch options:
4959
5249
  --dry-run List tasks without dispatching (also works with --spec)
4960
5250
  --no-plan Skip the planner agent, dispatch directly
4961
5251
  --no-branch Skip branch creation, push, and PR lifecycle
4962
5252
  --no-worktree Skip git worktree isolation for parallel issues
4963
- --feature Group issues into a single feature branch and PR
5253
+ --feature [name] Group issues into a single feature branch and PR
4964
5254
  --force Ignore prior run state and re-run all tasks
4965
5255
  --concurrency <n> Max parallel dispatches (default: min(cpus, freeMB/500), max: ${MAX_CONCURRENCY})
4966
5256
  --provider <name> Agent backend: ${PROVIDER_NAMES.join(", ")} (default: opencode)
@@ -5006,6 +5296,12 @@ var HELP = `
5006
5296
  dispatch --respec "specs/*.md"
5007
5297
  dispatch --spec "add dark mode toggle to settings page"
5008
5298
  dispatch --spec "feature A should do x" --provider copilot
5299
+ dispatch --feature
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
5009
5305
  dispatch config
5010
5306
  `.trimStart();
5011
5307
  function parseArgs(argv) {
@@ -5015,7 +5311,7 @@ function parseArgs(argv) {
5015
5311
  },
5016
5312
  writeErr: () => {
5017
5313
  }
5018
- }).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", "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(
5019
5315
  new Option("--provider <name>", "Agent backend").choices(PROVIDER_NAMES)
5020
5316
  ).addOption(
5021
5317
  new Option("--source <name>", "Issue source").choices(
@@ -5063,7 +5359,7 @@ function parseArgs(argv) {
5063
5359
  if (isNaN(n) || n <= 0) throw new CommanderError(1, "commander.invalidArgument", "--test-timeout must be a positive number (minutes)");
5064
5360
  return n;
5065
5361
  }
5066
- ).option("--cwd <dir>", "Working directory", (val) => resolve3(val)).option("--output-dir <dir>", "Output directory", (val) => resolve3(val)).option("--org <url>", "Azure DevOps organization URL").option("--project <name>", "Azure DevOps project name").option("--server-url <url>", "Provider server URL");
5362
+ ).option("--cwd <dir>", "Working directory", (val) => resolve4(val)).option("--output-dir <dir>", "Output directory", (val) => resolve4(val)).option("--org <url>", "Azure DevOps organization URL").option("--project <name>", "Azure DevOps project name").option("--server-url <url>", "Provider server URL");
5067
5363
  try {
5068
5364
  program.parse(argv, { from: "user" });
5069
5365
  } catch (err) {
@@ -5098,7 +5394,7 @@ function parseArgs(argv) {
5098
5394
  }
5099
5395
  }
5100
5396
  if (opts.fixTests) args.fixTests = true;
5101
- if (opts.feature) args.feature = true;
5397
+ if (opts.feature) args.feature = opts.feature;
5102
5398
  if (opts.source !== void 0) args.issueSource = opts.source;
5103
5399
  if (opts.concurrency !== void 0) args.concurrency = opts.concurrency;
5104
5400
  if (opts.serverUrl !== void 0) args.serverUrl = opts.serverUrl;
@@ -5148,7 +5444,7 @@ async function main() {
5148
5444
  if (rawArgv[0] === "config") {
5149
5445
  const configProgram = new Command("dispatch-config").exitOverride().configureOutput({ writeOut: () => {
5150
5446
  }, writeErr: () => {
5151
- } }).helpOption(false).allowUnknownOption(true).allowExcessArguments(true).option("--cwd <dir>", "Working directory", (v) => resolve3(v));
5447
+ } }).helpOption(false).allowUnknownOption(true).allowExcessArguments(true).option("--cwd <dir>", "Working directory", (v) => resolve4(v));
5152
5448
  try {
5153
5449
  configProgram.parse(rawArgv.slice(1), { from: "user" });
5154
5450
  } catch (err) {
@@ -5179,7 +5475,7 @@ async function main() {
5179
5475
  process.exit(0);
5180
5476
  }
5181
5477
  if (args.version) {
5182
- console.log(`dispatch v${"1.3.1"}`);
5478
+ console.log(`dispatch v${"1.4.1"}`);
5183
5479
  process.exit(0);
5184
5480
  }
5185
5481
  const orchestrator = await boot9({ cwd: args.cwd });