@pruddiman/dispatch 1.3.1 → 1.4.0

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
@@ -422,7 +422,7 @@ var init_opencode = __esm({
422
422
 
423
423
  // src/helpers/timeout.ts
424
424
  function withTimeout(promise, ms, label) {
425
- const p = new Promise((resolve4, reject) => {
425
+ const p = new Promise((resolve5, reject) => {
426
426
  let settled = false;
427
427
  const timer = setTimeout(() => {
428
428
  if (settled) return;
@@ -434,7 +434,7 @@ function withTimeout(promise, ms, label) {
434
434
  if (settled) return;
435
435
  settled = true;
436
436
  clearTimeout(timer);
437
- resolve4(value);
437
+ resolve5(value);
438
438
  },
439
439
  (err) => {
440
440
  if (settled) return;
@@ -542,9 +542,9 @@ async function boot2(opts) {
542
542
  let unsubErr;
543
543
  try {
544
544
  await withTimeout(
545
- new Promise((resolve4, reject) => {
545
+ new Promise((resolve5, reject) => {
546
546
  unsubIdle = session.on("session.idle", () => {
547
- resolve4();
547
+ resolve5();
548
548
  });
549
549
  unsubErr = session.on("session.error", (event) => {
550
550
  reject(new Error(`Copilot session error: ${event.data.message}`));
@@ -761,18 +761,20 @@ import { promisify as promisify6 } from "util";
761
761
  async function checkProviderInstalled(name) {
762
762
  try {
763
763
  await exec6(PROVIDER_BINARIES[name], ["--version"], {
764
- shell: process.platform === "win32"
764
+ shell: process.platform === "win32",
765
+ timeout: DETECTION_TIMEOUT_MS
765
766
  });
766
767
  return true;
767
768
  } catch {
768
769
  return false;
769
770
  }
770
771
  }
771
- var exec6, PROVIDER_BINARIES;
772
+ var exec6, DETECTION_TIMEOUT_MS, PROVIDER_BINARIES;
772
773
  var init_detect = __esm({
773
774
  "src/providers/detect.ts"() {
774
775
  "use strict";
775
776
  exec6 = promisify6(execFile6);
777
+ DETECTION_TIMEOUT_MS = 5e3;
776
778
  PROVIDER_BINARIES = {
777
779
  opencode: "opencode",
778
780
  copilot: "copilot",
@@ -826,6 +828,35 @@ var init_providers = __esm({
826
828
  }
827
829
  });
828
830
 
831
+ // src/helpers/environment.ts
832
+ function getEnvironmentInfo() {
833
+ const platform = process.platform;
834
+ switch (platform) {
835
+ case "win32":
836
+ return { platform, os: "Windows", shell: "cmd.exe/PowerShell" };
837
+ case "darwin":
838
+ return { platform, os: "macOS", shell: "zsh/bash" };
839
+ default:
840
+ return { platform, os: "Linux", shell: "bash" };
841
+ }
842
+ }
843
+ function formatEnvironmentPrompt() {
844
+ const env = getEnvironmentInfo();
845
+ return [
846
+ `## Environment`,
847
+ `- **Operating System:** ${env.os}`,
848
+ `- **Default Shell:** ${env.shell}`,
849
+ `- 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.`
850
+ ].join("\n");
851
+ }
852
+ var getEnvironmentBlock;
853
+ var init_environment = __esm({
854
+ "src/helpers/environment.ts"() {
855
+ "use strict";
856
+ getEnvironmentBlock = formatEnvironmentPrompt;
857
+ }
858
+ });
859
+
829
860
  // src/helpers/cleanup.ts
830
861
  function registerCleanup(fn) {
831
862
  cleanups.push(fn);
@@ -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 {
@@ -1364,7 +1444,8 @@ var datasource2 = {
1364
1444
  if (opts.org) batchArgs.push("--org", opts.org);
1365
1445
  if (opts.project) batchArgs.push("--project", opts.project);
1366
1446
  const { stdout: batchStdout } = await exec2("az", batchArgs, {
1367
- cwd: opts.cwd || process.cwd()
1447
+ cwd: opts.cwd || process.cwd(),
1448
+ shell: process.platform === "win32"
1368
1449
  });
1369
1450
  let batchItems;
1370
1451
  try {
@@ -1410,7 +1491,8 @@ var datasource2 = {
1410
1491
  args.push("--project", opts.project);
1411
1492
  }
1412
1493
  const { stdout } = await exec2("az", args, {
1413
- cwd: opts.cwd || process.cwd()
1494
+ cwd: opts.cwd || process.cwd(),
1495
+ shell: process.platform === "win32"
1414
1496
  });
1415
1497
  let item;
1416
1498
  try {
@@ -1435,9 +1517,34 @@ var datasource2 = {
1435
1517
  ];
1436
1518
  if (opts.org) args.push("--org", opts.org);
1437
1519
  if (opts.project) args.push("--project", opts.project);
1438
- await exec2("az", args, { cwd: opts.cwd || process.cwd() });
1520
+ await exec2("az", args, { cwd: opts.cwd || process.cwd(), shell: process.platform === "win32" });
1439
1521
  },
1440
1522
  async close(issueId, opts = {}) {
1523
+ let workItemType = opts.workItemType;
1524
+ if (!workItemType) {
1525
+ const showArgs = [
1526
+ "boards",
1527
+ "work-item",
1528
+ "show",
1529
+ "--id",
1530
+ issueId,
1531
+ "--output",
1532
+ "json"
1533
+ ];
1534
+ if (opts.org) showArgs.push("--org", opts.org);
1535
+ if (opts.project) showArgs.push("--project", opts.project);
1536
+ const { stdout } = await exec2("az", showArgs, {
1537
+ cwd: opts.cwd || process.cwd(),
1538
+ shell: process.platform === "win32"
1539
+ });
1540
+ try {
1541
+ const item = JSON.parse(stdout);
1542
+ workItemType = item.fields?.["System.WorkItemType"] ?? void 0;
1543
+ } catch {
1544
+ workItemType = void 0;
1545
+ }
1546
+ }
1547
+ const state = workItemType ? await detectDoneState(workItemType, opts) : "Closed";
1441
1548
  const args = [
1442
1549
  "boards",
1443
1550
  "work-item",
@@ -1445,11 +1552,11 @@ var datasource2 = {
1445
1552
  "--id",
1446
1553
  issueId,
1447
1554
  "--state",
1448
- "Closed"
1555
+ state
1449
1556
  ];
1450
1557
  if (opts.org) args.push("--org", opts.org);
1451
1558
  if (opts.project) args.push("--project", opts.project);
1452
- await exec2("az", args, { cwd: opts.cwd || process.cwd() });
1559
+ await exec2("az", args, { cwd: opts.cwd || process.cwd(), shell: process.platform === "win32" });
1453
1560
  },
1454
1561
  async create(title, body, opts = {}) {
1455
1562
  const workItemType = opts.workItemType ?? await detectWorkItemType(opts);
@@ -1474,7 +1581,8 @@ var datasource2 = {
1474
1581
  if (opts.org) args.push("--org", opts.org);
1475
1582
  if (opts.project) args.push("--project", opts.project);
1476
1583
  const { stdout } = await exec2("az", args, {
1477
- cwd: opts.cwd || process.cwd()
1584
+ cwd: opts.cwd || process.cwd(),
1585
+ shell: process.platform === "win32"
1478
1586
  });
1479
1587
  let item;
1480
1588
  try {
@@ -1491,7 +1599,7 @@ var datasource2 = {
1491
1599
  },
1492
1600
  async getDefaultBranch(opts) {
1493
1601
  try {
1494
- const { stdout } = await exec2("git", ["symbolic-ref", "refs/remotes/origin/HEAD"], { cwd: opts.cwd });
1602
+ const { stdout } = await exec2("git", ["symbolic-ref", "refs/remotes/origin/HEAD"], { cwd: opts.cwd, shell: process.platform === "win32" });
1495
1603
  const parts = stdout.trim().split("/");
1496
1604
  const branch = parts[parts.length - 1];
1497
1605
  if (!isValidBranchName(branch)) {
@@ -1503,7 +1611,7 @@ var datasource2 = {
1503
1611
  throw err;
1504
1612
  }
1505
1613
  try {
1506
- await exec2("git", ["rev-parse", "--verify", "main"], { cwd: opts.cwd });
1614
+ await exec2("git", ["rev-parse", "--verify", "main"], { cwd: opts.cwd, shell: process.platform === "win32" });
1507
1615
  return "main";
1508
1616
  } catch {
1509
1617
  return "master";
@@ -1512,19 +1620,19 @@ var datasource2 = {
1512
1620
  },
1513
1621
  async getUsername(opts) {
1514
1622
  try {
1515
- const { stdout } = await exec2("git", ["config", "user.name"], { cwd: opts.cwd });
1623
+ const { stdout } = await exec2("git", ["config", "user.name"], { cwd: opts.cwd, shell: process.platform === "win32" });
1516
1624
  const name = slugify(stdout.trim());
1517
1625
  if (name) return name;
1518
1626
  } catch {
1519
1627
  }
1520
1628
  try {
1521
- const { stdout } = await exec2("az", ["account", "show", "--query", "user.name", "-o", "tsv"], { cwd: opts.cwd });
1629
+ const { stdout } = await exec2("az", ["account", "show", "--query", "user.name", "-o", "tsv"], { cwd: opts.cwd, shell: process.platform === "win32" });
1522
1630
  const name = slugify(stdout.trim());
1523
1631
  if (name) return name;
1524
1632
  } catch {
1525
1633
  }
1526
1634
  try {
1527
- const { stdout } = await exec2("az", ["account", "show", "--query", "user.principalName", "-o", "tsv"], { cwd: opts.cwd });
1635
+ const { stdout } = await exec2("az", ["account", "show", "--query", "user.principalName", "-o", "tsv"], { cwd: opts.cwd, shell: process.platform === "win32" });
1528
1636
  const principal = stdout.trim();
1529
1637
  const prefix = principal.split("@")[0];
1530
1638
  const name = slugify(prefix);
@@ -1546,29 +1654,29 @@ var datasource2 = {
1546
1654
  throw new InvalidBranchNameError(branchName);
1547
1655
  }
1548
1656
  try {
1549
- await exec2("git", ["checkout", "-b", branchName], { cwd: opts.cwd });
1657
+ await exec2("git", ["checkout", "-b", branchName], { cwd: opts.cwd, shell: process.platform === "win32" });
1550
1658
  } catch (err) {
1551
1659
  const message = log.extractMessage(err);
1552
1660
  if (message.includes("already exists")) {
1553
- await exec2("git", ["checkout", branchName], { cwd: opts.cwd });
1661
+ await exec2("git", ["checkout", branchName], { cwd: opts.cwd, shell: process.platform === "win32" });
1554
1662
  } else {
1555
1663
  throw err;
1556
1664
  }
1557
1665
  }
1558
1666
  },
1559
1667
  async switchBranch(branchName, opts) {
1560
- await exec2("git", ["checkout", branchName], { cwd: opts.cwd });
1668
+ await exec2("git", ["checkout", branchName], { cwd: opts.cwd, shell: process.platform === "win32" });
1561
1669
  },
1562
1670
  async pushBranch(branchName, opts) {
1563
- await exec2("git", ["push", "--set-upstream", "origin", branchName], { cwd: opts.cwd });
1671
+ await exec2("git", ["push", "--set-upstream", "origin", branchName], { cwd: opts.cwd, shell: process.platform === "win32" });
1564
1672
  },
1565
1673
  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 });
1674
+ await exec2("git", ["add", "-A"], { cwd: opts.cwd, shell: process.platform === "win32" });
1675
+ const { stdout } = await exec2("git", ["diff", "--cached", "--stat"], { cwd: opts.cwd, shell: process.platform === "win32" });
1568
1676
  if (!stdout.trim()) {
1569
1677
  return;
1570
1678
  }
1571
- await exec2("git", ["commit", "-m", message], { cwd: opts.cwd });
1679
+ await exec2("git", ["commit", "-m", message], { cwd: opts.cwd, shell: process.platform === "win32" });
1572
1680
  },
1573
1681
  async createPullRequest(branchName, issueNumber, title, body, opts) {
1574
1682
  try {
@@ -1589,7 +1697,7 @@ var datasource2 = {
1589
1697
  "--output",
1590
1698
  "json"
1591
1699
  ],
1592
- { cwd: opts.cwd }
1700
+ { cwd: opts.cwd, shell: process.platform === "win32" }
1593
1701
  );
1594
1702
  let pr;
1595
1703
  try {
@@ -1614,7 +1722,7 @@ var datasource2 = {
1614
1722
  "--output",
1615
1723
  "json"
1616
1724
  ],
1617
- { cwd: opts.cwd }
1725
+ { cwd: opts.cwd, shell: process.platform === "win32" }
1618
1726
  );
1619
1727
  let prs;
1620
1728
  try {
@@ -1650,7 +1758,8 @@ async function fetchComments(workItemId, opts) {
1650
1758
  args.push("--project", opts.project);
1651
1759
  }
1652
1760
  const { stdout } = await exec2("az", args, {
1653
- cwd: opts.cwd || process.cwd()
1761
+ cwd: opts.cwd || process.cwd(),
1762
+ shell: process.platform === "win32"
1654
1763
  });
1655
1764
  const data = JSON.parse(stdout);
1656
1765
  if (data.comments && Array.isArray(data.comments)) {
@@ -1670,8 +1779,9 @@ async function fetchComments(workItemId, opts) {
1670
1779
  // src/datasources/md.ts
1671
1780
  import { execFile as execFile3 } from "child_process";
1672
1781
  import { readFile, writeFile, readdir, mkdir, rename } from "fs/promises";
1673
- import { join as join2, parse as parsePath } from "path";
1782
+ import { basename, dirname as dirname2, isAbsolute, join as join2, parse as parsePath, resolve } from "path";
1674
1783
  import { promisify as promisify3 } from "util";
1784
+ import { glob } from "glob";
1675
1785
 
1676
1786
  // src/helpers/errors.ts
1677
1787
  var UnsupportedOperationError = class extends Error {
@@ -1692,6 +1802,15 @@ function resolveDir(opts) {
1692
1802
  const cwd = opts?.cwd ?? process.cwd();
1693
1803
  return join2(cwd, DEFAULT_DIR);
1694
1804
  }
1805
+ function resolveFilePath(issueId, opts) {
1806
+ const filename = issueId.endsWith(".md") ? issueId : `${issueId}.md`;
1807
+ if (isAbsolute(filename)) return filename;
1808
+ if (/[/\\]/.test(filename)) {
1809
+ const cwd = opts?.cwd ?? process.cwd();
1810
+ return resolve(cwd, filename);
1811
+ }
1812
+ return join2(resolveDir(opts), filename);
1813
+ }
1695
1814
  function extractTitle(content, filename) {
1696
1815
  const match = content.match(/^#\s+(.+)$/m);
1697
1816
  if (match) return match[1].trim();
@@ -1726,6 +1845,19 @@ var datasource3 = {
1726
1845
  return false;
1727
1846
  },
1728
1847
  async list(opts) {
1848
+ if (opts?.pattern) {
1849
+ const cwd = opts.cwd ?? process.cwd();
1850
+ const files = await glob(opts.pattern, { cwd, absolute: true });
1851
+ const mdFiles2 = files.filter((f) => f.endsWith(".md")).sort();
1852
+ const results2 = [];
1853
+ for (const filePath of mdFiles2) {
1854
+ const content = await readFile(filePath, "utf-8");
1855
+ const filename = basename(filePath);
1856
+ const dir2 = dirname2(filePath);
1857
+ results2.push(toIssueDetails(filename, content, dir2));
1858
+ }
1859
+ return results2;
1860
+ }
1729
1861
  const dir = resolveDir(opts);
1730
1862
  let entries;
1731
1863
  try {
@@ -1743,23 +1875,20 @@ var datasource3 = {
1743
1875
  return results;
1744
1876
  },
1745
1877
  async fetch(issueId, opts) {
1746
- const dir = resolveDir(opts);
1747
- const filename = issueId.endsWith(".md") ? issueId : `${issueId}.md`;
1748
- const filePath = join2(dir, filename);
1878
+ const filePath = resolveFilePath(issueId, opts);
1749
1879
  const content = await readFile(filePath, "utf-8");
1880
+ const filename = basename(filePath);
1881
+ const dir = dirname2(filePath);
1750
1882
  return toIssueDetails(filename, content, dir);
1751
1883
  },
1752
1884
  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);
1885
+ const filePath = resolveFilePath(issueId, opts);
1756
1886
  await writeFile(filePath, body, "utf-8");
1757
1887
  },
1758
1888
  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");
1889
+ const filePath = resolveFilePath(issueId, opts);
1890
+ const filename = basename(filePath);
1891
+ const archiveDir = join2(dirname2(filePath), "archive");
1763
1892
  await mkdir(archiveDir, { recursive: true });
1764
1893
  await rename(filePath, join2(archiveDir, filename));
1765
1894
  },
@@ -1776,7 +1905,7 @@ var datasource3 = {
1776
1905
  },
1777
1906
  async getUsername(opts) {
1778
1907
  try {
1779
- const { stdout } = await exec3("git", ["config", "user.name"], { cwd: opts.cwd });
1908
+ const { stdout } = await exec3("git", ["config", "user.name"], { cwd: opts.cwd, shell: process.platform === "win32" });
1780
1909
  const name = stdout.trim();
1781
1910
  if (!name) return "local";
1782
1911
  return slugify(name);
@@ -1825,7 +1954,8 @@ function getDatasource(name) {
1825
1954
  async function getGitRemoteUrl(cwd) {
1826
1955
  try {
1827
1956
  const { stdout } = await exec4("git", ["remote", "get-url", "origin"], {
1828
- cwd
1957
+ cwd,
1958
+ shell: process.platform === "win32"
1829
1959
  });
1830
1960
  return stdout.trim() || null;
1831
1961
  } catch {
@@ -2100,7 +2230,7 @@ import { constants } from "fs";
2100
2230
  // src/config.ts
2101
2231
  init_providers();
2102
2232
  import { readFile as readFile3, writeFile as writeFile3, mkdir as mkdir2 } from "fs/promises";
2103
- import { join as join4, dirname as dirname2 } from "path";
2233
+ import { join as join4, dirname as dirname3 } from "path";
2104
2234
 
2105
2235
  // src/config-prompts.ts
2106
2236
  init_logger();
@@ -2291,7 +2421,7 @@ async function loadConfig(configDir) {
2291
2421
  }
2292
2422
  async function saveConfig(config, configDir) {
2293
2423
  const configPath = getConfigPath(configDir);
2294
- await mkdir2(dirname2(configPath), { recursive: true });
2424
+ await mkdir2(dirname3(configPath), { recursive: true });
2295
2425
  await writeFile3(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
2296
2426
  }
2297
2427
  async function handleConfigCommand(_argv, configDir) {
@@ -2366,15 +2496,16 @@ async function resolveCliConfig(args) {
2366
2496
  // src/orchestrator/spec-pipeline.ts
2367
2497
  import { join as join7 } from "path";
2368
2498
  import { mkdir as mkdir4, readFile as readFile5, rename as rename2, unlink as unlink2 } from "fs/promises";
2369
- import { glob } from "glob";
2499
+ import { glob as glob2 } from "glob";
2370
2500
  init_providers();
2371
2501
 
2372
2502
  // src/agents/spec.ts
2373
2503
  import { mkdir as mkdir3, readFile as readFile4, writeFile as writeFile4, unlink } from "fs/promises";
2374
- import { join as join6, resolve, sep } from "path";
2504
+ import { join as join6, resolve as resolve2, sep } from "path";
2375
2505
  import { randomUUID as randomUUID3 } from "crypto";
2376
2506
  init_logger();
2377
2507
  init_file_logger();
2508
+ init_environment();
2378
2509
  async function boot5(opts) {
2379
2510
  const { provider } = opts;
2380
2511
  if (!provider) {
@@ -2386,8 +2517,8 @@ async function boot5(opts) {
2386
2517
  const { issue, filePath, fileContent, inlineText, cwd: workingDir, outputPath } = genOpts;
2387
2518
  const startTime = Date.now();
2388
2519
  try {
2389
- const resolvedCwd = resolve(workingDir);
2390
- const resolvedOutput = resolve(outputPath);
2520
+ const resolvedCwd = resolve2(workingDir);
2521
+ const resolvedOutput = resolve2(outputPath);
2391
2522
  if (resolvedOutput !== resolvedCwd && !resolvedOutput.startsWith(resolvedCwd + sep)) {
2392
2523
  return {
2393
2524
  data: null,
@@ -2564,6 +2695,8 @@ function buildCommonSpecInstructions(params) {
2564
2695
  ``,
2565
2696
  `\`${cwd}\``,
2566
2697
  ``,
2698
+ formatEnvironmentPrompt(),
2699
+ ``,
2567
2700
  `## Instructions`,
2568
2701
  ``,
2569
2702
  `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.`,
@@ -2814,7 +2947,7 @@ function buildInlineTextItem(issues, outputDir) {
2814
2947
  return [{ id: filepath, details }];
2815
2948
  }
2816
2949
  async function resolveFileItems(issues, specCwd, concurrency) {
2817
- const files = await glob(issues, { cwd: specCwd, absolute: true });
2950
+ const files = await glob2(issues, { cwd: specCwd, absolute: true });
2818
2951
  if (files.length === 0) {
2819
2952
  log.error(`No files matched the pattern "${Array.isArray(issues) ? issues.join(", ") : issues}".`);
2820
2953
  return null;
@@ -3136,6 +3269,7 @@ async function runSpecPipeline(opts) {
3136
3269
  import { execFile as execFile9 } from "child_process";
3137
3270
  import { promisify as promisify9 } from "util";
3138
3271
  import { readFile as readFile7 } from "fs/promises";
3272
+ import { glob as glob3 } from "glob";
3139
3273
 
3140
3274
  // src/parser.ts
3141
3275
  import { readFile as readFile6, writeFile as writeFile5 } from "fs/promises";
@@ -3237,6 +3371,7 @@ function groupTasksByMode(tasks) {
3237
3371
  // src/agents/planner.ts
3238
3372
  init_logger();
3239
3373
  init_file_logger();
3374
+ init_environment();
3240
3375
  async function boot6(opts) {
3241
3376
  const { provider, cwd } = opts;
3242
3377
  if (!provider) {
@@ -3307,6 +3442,10 @@ function buildPlannerPrompt(task, cwd, fileContext, worktreeRoot) {
3307
3442
  `- All relative paths must resolve within the worktree root above.`
3308
3443
  );
3309
3444
  }
3445
+ sections.push(
3446
+ ``,
3447
+ formatEnvironmentPrompt()
3448
+ );
3310
3449
  sections.push(
3311
3450
  ``,
3312
3451
  `## Instructions`,
@@ -3343,6 +3482,7 @@ function buildPlannerPrompt(task, cwd, fileContext, worktreeRoot) {
3343
3482
  // src/dispatcher.ts
3344
3483
  init_logger();
3345
3484
  init_file_logger();
3485
+ init_environment();
3346
3486
  async function dispatchTask(instance, task, cwd, plan, worktreeRoot) {
3347
3487
  try {
3348
3488
  log.debug(`Dispatching task: ${task.file}:${task.line} \u2014 ${task.text.slice(0, 80)}`);
@@ -3375,6 +3515,8 @@ function buildPrompt(task, cwd, worktreeRoot) {
3375
3515
  `**Source file:** ${task.file}`,
3376
3516
  `**Task (line ${task.line}):** ${task.text}`,
3377
3517
  ``,
3518
+ getEnvironmentBlock(),
3519
+ ``,
3378
3520
  `Instructions:`,
3379
3521
  `- Complete ONLY this specific task \u2014 do not work on other tasks.`,
3380
3522
  `- Make the minimal, correct changes needed.`,
@@ -3392,6 +3534,8 @@ function buildPlannedPrompt(task, cwd, plan, worktreeRoot) {
3392
3534
  `**Source file:** ${task.file}`,
3393
3535
  `**Task (line ${task.line}):** ${task.text}`,
3394
3536
  ``,
3537
+ getEnvironmentBlock(),
3538
+ ``,
3395
3539
  `---`,
3396
3540
  ``,
3397
3541
  `## Execution Plan`,
@@ -3466,8 +3610,9 @@ ${err.stack}` : ""}`);
3466
3610
  // src/agents/commit.ts
3467
3611
  init_logger();
3468
3612
  init_file_logger();
3613
+ init_environment();
3469
3614
  import { mkdir as mkdir5, writeFile as writeFile6 } from "fs/promises";
3470
- import { join as join8, resolve as resolve2 } from "path";
3615
+ import { join as join8, resolve as resolve3 } from "path";
3471
3616
  import { randomUUID as randomUUID4 } from "crypto";
3472
3617
  async function boot8(opts) {
3473
3618
  const { provider } = opts;
@@ -3480,7 +3625,7 @@ async function boot8(opts) {
3480
3625
  name: "commit",
3481
3626
  async generate(genOpts) {
3482
3627
  try {
3483
- const resolvedCwd = resolve2(genOpts.cwd);
3628
+ const resolvedCwd = resolve3(genOpts.cwd);
3484
3629
  const tmpDir = join8(resolvedCwd, ".dispatch", "tmp");
3485
3630
  await mkdir5(tmpDir, { recursive: true });
3486
3631
  const tmpFilename = `commit-${randomUUID4()}.md`;
@@ -3542,6 +3687,8 @@ function buildCommitPrompt(opts) {
3542
3687
  const sections = [
3543
3688
  `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
3689
  ``,
3690
+ formatEnvironmentPrompt(),
3691
+ ``,
3545
3692
  `## Conventional Commit Guidelines`,
3546
3693
  ``,
3547
3694
  `Follow the Conventional Commits specification (https://www.conventionalcommits.org/):`,
@@ -3663,7 +3810,7 @@ init_logger();
3663
3810
  init_cleanup();
3664
3811
 
3665
3812
  // src/helpers/worktree.ts
3666
- import { join as join9, basename } from "path";
3813
+ import { join as join9, basename as basename2 } from "path";
3667
3814
  import { execFile as execFile7 } from "child_process";
3668
3815
  import { promisify as promisify7 } from "util";
3669
3816
  import { randomUUID as randomUUID5 } from "crypto";
@@ -3671,11 +3818,11 @@ init_logger();
3671
3818
  var exec7 = promisify7(execFile7);
3672
3819
  var WORKTREE_DIR = ".dispatch/worktrees";
3673
3820
  async function git2(args, cwd) {
3674
- const { stdout } = await exec7("git", args, { cwd });
3821
+ const { stdout } = await exec7("git", args, { cwd, shell: process.platform === "win32" });
3675
3822
  return stdout;
3676
3823
  }
3677
3824
  function worktreeName(issueFilename) {
3678
- const base = basename(issueFilename);
3825
+ const base = basename2(issueFilename);
3679
3826
  const withoutExt = base.replace(/\.md$/i, "");
3680
3827
  const match = withoutExt.match(/^(\d+)/);
3681
3828
  return match ? `issue-${match[1]}` : slugify(withoutExt);
@@ -3983,14 +4130,14 @@ init_providers();
3983
4130
 
3984
4131
  // src/orchestrator/datasource-helpers.ts
3985
4132
  init_logger();
3986
- import { basename as basename2, join as join10 } from "path";
4133
+ import { basename as basename3, join as join10 } from "path";
3987
4134
  import { mkdtemp, writeFile as writeFile7 } from "fs/promises";
3988
4135
  import { tmpdir } from "os";
3989
4136
  import { execFile as execFile8 } from "child_process";
3990
4137
  import { promisify as promisify8 } from "util";
3991
4138
  var exec8 = promisify8(execFile8);
3992
4139
  function parseIssueFilename(filePath) {
3993
- const filename = basename2(filePath);
4140
+ const filename = basename3(filePath);
3994
4141
  const match = /^(\d+)-(.+)\.md$/.exec(filename);
3995
4142
  if (!match) return null;
3996
4143
  return { issueId: match[1], slug: match[2] };
@@ -4005,7 +4152,8 @@ async function fetchItemsById(issueIds, datasource4, fetchOpts) {
4005
4152
  const item = await datasource4.fetch(id, fetchOpts);
4006
4153
  items.push(item);
4007
4154
  } catch (err) {
4008
- log.warn(`Could not fetch issue #${id}: ${log.formatErrorChain(err)}`);
4155
+ const prefix = id.includes("/") || id.includes("\\") || id.endsWith(".md") ? "" : "#";
4156
+ log.warn(`Could not fetch issue ${prefix}${id}: ${log.formatErrorChain(err)}`);
4009
4157
  }
4010
4158
  }
4011
4159
  return items;
@@ -4023,8 +4171,8 @@ async function writeItemsToTempDir(items) {
4023
4171
  issueDetailsByFile.set(filepath, item);
4024
4172
  }
4025
4173
  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);
4174
+ const numA = parseInt(basename3(a).match(/^(\d+)/)?.[1] ?? "0", 10);
4175
+ const numB = parseInt(basename3(b).match(/^(\d+)/)?.[1] ?? "0", 10);
4028
4176
  if (numA !== numB) return numA - numB;
4029
4177
  return a.localeCompare(b);
4030
4178
  });
@@ -4035,7 +4183,7 @@ async function getCommitSummaries(defaultBranch, cwd) {
4035
4183
  const { stdout } = await exec8(
4036
4184
  "git",
4037
4185
  ["log", `${defaultBranch}..HEAD`, "--pretty=format:%s"],
4038
- { cwd }
4186
+ { cwd, shell: process.platform === "win32" }
4039
4187
  );
4040
4188
  return stdout.trim().split("\n").filter(Boolean);
4041
4189
  } catch {
@@ -4047,7 +4195,7 @@ async function getBranchDiff(defaultBranch, cwd) {
4047
4195
  const { stdout } = await exec8(
4048
4196
  "git",
4049
4197
  ["diff", `${defaultBranch}..HEAD`],
4050
- { cwd, maxBuffer: 10 * 1024 * 1024 }
4198
+ { cwd, maxBuffer: 10 * 1024 * 1024, shell: process.platform === "win32" }
4051
4199
  );
4052
4200
  return stdout;
4053
4201
  } catch {
@@ -4058,11 +4206,11 @@ async function squashBranchCommits(defaultBranch, message, cwd) {
4058
4206
  const { stdout } = await exec8(
4059
4207
  "git",
4060
4208
  ["merge-base", defaultBranch, "HEAD"],
4061
- { cwd }
4209
+ { cwd, shell: process.platform === "win32" }
4062
4210
  );
4063
4211
  const mergeBase = stdout.trim();
4064
- await exec8("git", ["reset", "--soft", mergeBase], { cwd });
4065
- await exec8("git", ["commit", "-m", message], { cwd });
4212
+ await exec8("git", ["reset", "--soft", mergeBase], { cwd, shell: process.platform === "win32" });
4213
+ await exec8("git", ["commit", "-m", message], { cwd, shell: process.platform === "win32" });
4066
4214
  }
4067
4215
  async function buildPrBody(details, tasks, results, defaultBranch, datasourceName, cwd) {
4068
4216
  const sections = [];
@@ -4158,6 +4306,34 @@ init_timeout();
4158
4306
  import chalk7 from "chalk";
4159
4307
  init_file_logger();
4160
4308
  var exec9 = promisify9(execFile9);
4309
+ async function resolveGlobItems(patterns, cwd) {
4310
+ const files = await glob3(patterns, { cwd, absolute: true });
4311
+ if (files.length === 0) {
4312
+ log.warn(`No files matched the pattern(s): ${patterns.join(", ")}`);
4313
+ return [];
4314
+ }
4315
+ log.info(`Matched ${files.length} file(s) from glob pattern(s)`);
4316
+ const items = [];
4317
+ for (const filePath of files) {
4318
+ try {
4319
+ const content = await readFile7(filePath, "utf-8");
4320
+ const title = extractTitle(content, filePath);
4321
+ items.push({
4322
+ number: filePath,
4323
+ title,
4324
+ body: content,
4325
+ labels: [],
4326
+ state: "open",
4327
+ url: filePath,
4328
+ comments: [],
4329
+ acceptanceCriteria: ""
4330
+ });
4331
+ } catch (err) {
4332
+ log.warn(`Could not read file ${filePath}: ${log.formatErrorChain(err)}`);
4333
+ }
4334
+ }
4335
+ return items;
4336
+ }
4161
4337
  var DEFAULT_PLAN_TIMEOUT_MIN = 10;
4162
4338
  var DEFAULT_PLAN_RETRIES = 1;
4163
4339
  async function runDispatchPipeline(opts, cwd) {
@@ -4223,7 +4399,14 @@ async function runDispatchPipeline(opts, cwd) {
4223
4399
  }
4224
4400
  const datasource4 = getDatasource(source);
4225
4401
  const fetchOpts = { cwd, org, project, workItemType, iteration, area };
4226
- const items = issueIds.length > 0 ? await fetchItemsById(issueIds, datasource4, fetchOpts) : await datasource4.list(fetchOpts);
4402
+ let items;
4403
+ if (issueIds.length > 0 && source === "md" && issueIds.some((id) => isGlobOrFilePath(id))) {
4404
+ items = await resolveGlobItems(issueIds, cwd);
4405
+ } else if (issueIds.length > 0) {
4406
+ items = await fetchItemsById(issueIds, datasource4, fetchOpts);
4407
+ } else {
4408
+ items = await datasource4.list(fetchOpts);
4409
+ }
4227
4410
  if (items.length === 0) {
4228
4411
  tui.state.phase = "done";
4229
4412
  tui.stop();
@@ -4295,12 +4478,32 @@ async function runDispatchPipeline(opts, cwd) {
4295
4478
  let featureBranchName;
4296
4479
  let featureDefaultBranch;
4297
4480
  if (feature) {
4481
+ if (typeof feature === "string") {
4482
+ if (!isValidBranchName(feature)) {
4483
+ log.error(`Invalid feature branch name: "${feature}"`);
4484
+ tui.state.phase = "done";
4485
+ tui.stop();
4486
+ return { total: allTasks.length, completed: 0, failed: allTasks.length, skipped: 0, results: [] };
4487
+ }
4488
+ featureBranchName = feature.includes("/") ? feature : `dispatch/${feature}`;
4489
+ } else {
4490
+ featureBranchName = generateFeatureBranchName();
4491
+ }
4298
4492
  try {
4299
4493
  featureDefaultBranch = await datasource4.getDefaultBranch(lifecycleOpts);
4300
4494
  await datasource4.switchBranch(featureDefaultBranch, lifecycleOpts);
4301
- featureBranchName = generateFeatureBranchName();
4302
- await datasource4.createAndSwitchBranch(featureBranchName, lifecycleOpts);
4303
- log.debug(`Created feature branch ${featureBranchName} from ${featureDefaultBranch}`);
4495
+ try {
4496
+ await datasource4.createAndSwitchBranch(featureBranchName, lifecycleOpts);
4497
+ log.debug(`Created feature branch ${featureBranchName} from ${featureDefaultBranch}`);
4498
+ } catch (createErr) {
4499
+ const message = log.extractMessage(createErr);
4500
+ if (message.includes("already exists")) {
4501
+ await datasource4.switchBranch(featureBranchName, lifecycleOpts);
4502
+ log.debug(`Switched to existing feature branch ${featureBranchName}`);
4503
+ } else {
4504
+ throw createErr;
4505
+ }
4506
+ }
4304
4507
  registerCleanup(async () => {
4305
4508
  try {
4306
4509
  await datasource4.switchBranch(featureDefaultBranch, lifecycleOpts);
@@ -4492,12 +4695,18 @@ ${err.stack}` : ""}`);
4492
4695
  fileLogger?.info(`Execution completed successfully (${Date.now() - startTime}ms)`);
4493
4696
  try {
4494
4697
  const parsed = parseIssueFilename(task.file);
4698
+ const updatedContent = await readFile7(task.file, "utf-8");
4495
4699
  if (parsed) {
4496
- const updatedContent = await readFile7(task.file, "utf-8");
4497
4700
  const issueDetails = issueDetailsByFile.get(task.file);
4498
4701
  const title = issueDetails?.title ?? parsed.slug;
4499
4702
  await datasource4.update(parsed.issueId, title, updatedContent, fetchOpts);
4500
4703
  log.success(`Synced task completion to issue #${parsed.issueId}`);
4704
+ } else {
4705
+ const issueDetails = issueDetailsByFile.get(task.file);
4706
+ if (issueDetails) {
4707
+ await datasource4.update(issueDetails.number, issueDetails.title, updatedContent, fetchOpts);
4708
+ log.success(`Synced task completion to issue #${issueDetails.number}`);
4709
+ }
4501
4710
  }
4502
4711
  } catch (err) {
4503
4712
  log.warn(`Could not sync task completion to datasource: ${log.formatErrorChain(err)}`);
@@ -4584,13 +4793,13 @@ ${err.stack}` : ""}`);
4584
4793
  }
4585
4794
  try {
4586
4795
  await datasource4.switchBranch(featureBranchName, lifecycleOpts);
4587
- await exec9("git", ["merge", branchName, "--no-ff", "-m", `merge: issue #${details.number}`], { cwd });
4796
+ await exec9("git", ["merge", branchName, "--no-ff", "-m", `merge: issue #${details.number}`], { cwd, shell: process.platform === "win32" });
4588
4797
  log.debug(`Merged ${branchName} into ${featureBranchName}`);
4589
4798
  } catch (err) {
4590
4799
  const mergeError = `Could not merge ${branchName} into feature branch: ${log.formatErrorChain(err)}`;
4591
4800
  log.warn(mergeError);
4592
4801
  try {
4593
- await exec9("git", ["merge", "--abort"], { cwd });
4802
+ await exec9("git", ["merge", "--abort"], { cwd, shell: process.platform === "win32" });
4594
4803
  } catch {
4595
4804
  }
4596
4805
  for (const task of fileTasks) {
@@ -4608,7 +4817,7 @@ ${err.stack}` : ""}`);
4608
4817
  return;
4609
4818
  }
4610
4819
  try {
4611
- await exec9("git", ["branch", "-d", branchName], { cwd });
4820
+ await exec9("git", ["branch", "-d", branchName], { cwd, shell: process.platform === "win32" });
4612
4821
  log.debug(`Deleted local branch ${branchName}`);
4613
4822
  } catch (err) {
4614
4823
  log.warn(`Could not delete local branch ${branchName}: ${log.formatErrorChain(err)}`);
@@ -4764,13 +4973,20 @@ async function dryRunMode(issueIds, cwd, source, org, project, workItemType, ite
4764
4973
  username = await datasource4.getUsername(lifecycleOpts);
4765
4974
  } catch {
4766
4975
  }
4767
- const items = issueIds.length > 0 ? await fetchItemsById(issueIds, datasource4, fetchOpts) : await datasource4.list(fetchOpts);
4976
+ let items;
4977
+ if (issueIds.length > 0 && source === "md" && issueIds.some((id) => isGlobOrFilePath(id))) {
4978
+ items = await resolveGlobItems(issueIds, cwd);
4979
+ } else if (issueIds.length > 0) {
4980
+ items = await fetchItemsById(issueIds, datasource4, fetchOpts);
4981
+ } else {
4982
+ items = await datasource4.list(fetchOpts);
4983
+ }
4768
4984
  if (items.length === 0) {
4769
4985
  const label = issueIds.length > 0 ? `issue(s) ${issueIds.join(", ")}` : `datasource: ${source}`;
4770
4986
  log.warn("No work items found from " + label);
4771
4987
  return { total: 0, completed: 0, failed: 0, skipped: 0, results: [] };
4772
4988
  }
4773
- const { files } = await writeItemsToTempDir(items);
4989
+ const { files, issueDetailsByFile } = await writeItemsToTempDir(items);
4774
4990
  const taskFiles = [];
4775
4991
  for (const file of files) {
4776
4992
  const tf = await parseTaskFile(file);
@@ -4787,7 +5003,7 @@ async function dryRunMode(issueIds, cwd, source, org, project, workItemType, ite
4787
5003
  `);
4788
5004
  for (const task of allTasks) {
4789
5005
  const parsed = parseIssueFilename(task.file);
4790
- const details = parsed ? items.find((item) => item.number === parsed.issueId) : void 0;
5006
+ const details = parsed ? items.find((item) => item.number === parsed.issueId) : issueDetailsByFile.get(task.file);
4791
5007
  const branchInfo = details ? ` [branch: ${datasource4.buildBranchName(details.number, details.title, username)}]` : "";
4792
5008
  log.task(allTasks.indexOf(task), allTasks.length, `${task.file}:${task.line} \u2014 ${task.text}${branchInfo}`);
4793
5009
  }
@@ -4960,7 +5176,7 @@ var HELP = `
4960
5176
  --no-plan Skip the planner agent, dispatch directly
4961
5177
  --no-branch Skip branch creation, push, and PR lifecycle
4962
5178
  --no-worktree Skip git worktree isolation for parallel issues
4963
- --feature Group issues into a single feature branch and PR
5179
+ --feature [name] Group issues into a single feature branch and PR
4964
5180
  --force Ignore prior run state and re-run all tasks
4965
5181
  --concurrency <n> Max parallel dispatches (default: min(cpus, freeMB/500), max: ${MAX_CONCURRENCY})
4966
5182
  --provider <name> Agent backend: ${PROVIDER_NAMES.join(", ")} (default: opencode)
@@ -5006,6 +5222,8 @@ var HELP = `
5006
5222
  dispatch --respec "specs/*.md"
5007
5223
  dispatch --spec "add dark mode toggle to settings page"
5008
5224
  dispatch --spec "feature A should do x" --provider copilot
5225
+ dispatch --feature
5226
+ dispatch --feature my-feature
5009
5227
  dispatch config
5010
5228
  `.trimStart();
5011
5229
  function parseArgs(argv) {
@@ -5015,7 +5233,7 @@ function parseArgs(argv) {
5015
5233
  },
5016
5234
  writeErr: () => {
5017
5235
  }
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(
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(
5019
5237
  new Option("--provider <name>", "Agent backend").choices(PROVIDER_NAMES)
5020
5238
  ).addOption(
5021
5239
  new Option("--source <name>", "Issue source").choices(
@@ -5063,7 +5281,7 @@ function parseArgs(argv) {
5063
5281
  if (isNaN(n) || n <= 0) throw new CommanderError(1, "commander.invalidArgument", "--test-timeout must be a positive number (minutes)");
5064
5282
  return n;
5065
5283
  }
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");
5284
+ ).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
5285
  try {
5068
5286
  program.parse(argv, { from: "user" });
5069
5287
  } catch (err) {
@@ -5098,7 +5316,7 @@ function parseArgs(argv) {
5098
5316
  }
5099
5317
  }
5100
5318
  if (opts.fixTests) args.fixTests = true;
5101
- if (opts.feature) args.feature = true;
5319
+ if (opts.feature) args.feature = opts.feature;
5102
5320
  if (opts.source !== void 0) args.issueSource = opts.source;
5103
5321
  if (opts.concurrency !== void 0) args.concurrency = opts.concurrency;
5104
5322
  if (opts.serverUrl !== void 0) args.serverUrl = opts.serverUrl;
@@ -5148,7 +5366,7 @@ async function main() {
5148
5366
  if (rawArgv[0] === "config") {
5149
5367
  const configProgram = new Command("dispatch-config").exitOverride().configureOutput({ writeOut: () => {
5150
5368
  }, writeErr: () => {
5151
- } }).helpOption(false).allowUnknownOption(true).allowExcessArguments(true).option("--cwd <dir>", "Working directory", (v) => resolve3(v));
5369
+ } }).helpOption(false).allowUnknownOption(true).allowExcessArguments(true).option("--cwd <dir>", "Working directory", (v) => resolve4(v));
5152
5370
  try {
5153
5371
  configProgram.parse(rawArgv.slice(1), { from: "user" });
5154
5372
  } catch (err) {
@@ -5179,7 +5397,7 @@ async function main() {
5179
5397
  process.exit(0);
5180
5398
  }
5181
5399
  if (args.version) {
5182
- console.log(`dispatch v${"1.3.1"}`);
5400
+ console.log(`dispatch v${"1.4.0"}`);
5183
5401
  process.exit(0);
5184
5402
  }
5185
5403
  const orchestrator = await boot9({ cwd: args.cwd });