@pruddiman/dispatch 1.3.0 → 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
@@ -1030,7 +1064,8 @@ function slugify(input3, maxLength) {
1030
1064
 
1031
1065
  // src/datasources/github.ts
1032
1066
  init_logger();
1033
- var exec = promisify(execFile);
1067
+
1068
+ // src/helpers/branch-validation.ts
1034
1069
  var InvalidBranchNameError = class extends Error {
1035
1070
  constructor(branch, reason) {
1036
1071
  const detail = reason ? ` (${reason})` : "";
@@ -1038,14 +1073,6 @@ var InvalidBranchNameError = class extends Error {
1038
1073
  this.name = "InvalidBranchNameError";
1039
1074
  }
1040
1075
  };
1041
- async function git(args, cwd) {
1042
- const { stdout } = await exec("git", args, { cwd });
1043
- return stdout;
1044
- }
1045
- async function gh(args, cwd) {
1046
- const { stdout } = await exec("gh", args, { cwd });
1047
- return stdout;
1048
- }
1049
1076
  var VALID_BRANCH_NAME_RE = /^[a-zA-Z0-9._\-/]+$/;
1050
1077
  function isValidBranchName(name) {
1051
1078
  if (name.length === 0 || name.length > 255) return false;
@@ -1057,6 +1084,17 @@ function isValidBranchName(name) {
1057
1084
  if (name.includes("//")) return false;
1058
1085
  return true;
1059
1086
  }
1087
+
1088
+ // src/datasources/github.ts
1089
+ var exec = promisify(execFile);
1090
+ async function git(args, cwd) {
1091
+ const { stdout } = await exec("git", args, { cwd, shell: process.platform === "win32" });
1092
+ return stdout;
1093
+ }
1094
+ async function gh(args, cwd) {
1095
+ const { stdout } = await exec("gh", args, { cwd, shell: process.platform === "win32" });
1096
+ return stdout;
1097
+ }
1060
1098
  function buildBranchName(issueNumber, title, username = "unknown") {
1061
1099
  const slug = slugify(title, 50);
1062
1100
  return `${username}/dispatch/${issueNumber}-${slug}`;
@@ -1100,7 +1138,7 @@ var datasource = {
1100
1138
  "--json",
1101
1139
  "number,title,body,labels,state,url"
1102
1140
  ],
1103
- { cwd }
1141
+ { cwd, shell: process.platform === "win32" }
1104
1142
  );
1105
1143
  let issues;
1106
1144
  try {
@@ -1132,7 +1170,7 @@ var datasource = {
1132
1170
  "--json",
1133
1171
  "number,title,body,labels,state,url,comments"
1134
1172
  ],
1135
- { cwd }
1173
+ { cwd, shell: process.platform === "win32" }
1136
1174
  );
1137
1175
  let issue;
1138
1176
  try {
@@ -1160,18 +1198,18 @@ var datasource = {
1160
1198
  },
1161
1199
  async update(issueId, title, body, opts = {}) {
1162
1200
  const cwd = opts.cwd || process.cwd();
1163
- 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" });
1164
1202
  },
1165
1203
  async close(issueId, opts = {}) {
1166
1204
  const cwd = opts.cwd || process.cwd();
1167
- await exec("gh", ["issue", "close", issueId], { cwd });
1205
+ await exec("gh", ["issue", "close", issueId], { cwd, shell: process.platform === "win32" });
1168
1206
  },
1169
1207
  async create(title, body, opts = {}) {
1170
1208
  const cwd = opts.cwd || process.cwd();
1171
1209
  const { stdout } = await exec(
1172
1210
  "gh",
1173
1211
  ["issue", "create", "--title", title, "--body", body],
1174
- { cwd }
1212
+ { cwd, shell: process.platform === "win32" }
1175
1213
  );
1176
1214
  const url = stdout.trim();
1177
1215
  const match = url.match(/\/issues\/(\d+)$/);
@@ -1267,13 +1305,34 @@ import { execFile as execFile2 } from "child_process";
1267
1305
  import { promisify as promisify2 } from "util";
1268
1306
  init_logger();
1269
1307
  var exec2 = promisify2(execFile2);
1308
+ var doneStateCache = /* @__PURE__ */ new Map();
1309
+ function mapWorkItemToIssueDetails(item, id, comments, defaults) {
1310
+ const fields = item.fields ?? {};
1311
+ return {
1312
+ number: String(item.id ?? id),
1313
+ title: fields["System.Title"] ?? defaults?.title ?? "",
1314
+ body: fields["System.Description"] ?? defaults?.body ?? "",
1315
+ labels: (fields["System.Tags"] ?? "").split(";").map((t) => t.trim()).filter(Boolean),
1316
+ state: fields["System.State"] ?? defaults?.state ?? "",
1317
+ url: item._links?.html?.href ?? item.url ?? "",
1318
+ comments,
1319
+ acceptanceCriteria: fields["Microsoft.VSTS.Common.AcceptanceCriteria"] ?? "",
1320
+ iterationPath: fields["System.IterationPath"] || void 0,
1321
+ areaPath: fields["System.AreaPath"] || void 0,
1322
+ assignee: fields["System.AssignedTo"]?.displayName || void 0,
1323
+ priority: fields["Microsoft.VSTS.Common.Priority"] ?? void 0,
1324
+ storyPoints: fields["Microsoft.VSTS.Scheduling.StoryPoints"] ?? fields["Microsoft.VSTS.Scheduling.Effort"] ?? fields["Microsoft.VSTS.Scheduling.Size"] ?? void 0,
1325
+ workItemType: fields["System.WorkItemType"] || defaults?.workItemType || void 0
1326
+ };
1327
+ }
1270
1328
  async function detectWorkItemType(opts = {}) {
1271
1329
  try {
1272
1330
  const args = ["boards", "work-item", "type", "list", "--output", "json"];
1273
1331
  if (opts.project) args.push("--project", opts.project);
1274
1332
  if (opts.org) args.push("--org", opts.org);
1275
1333
  const { stdout } = await exec2("az", args, {
1276
- cwd: opts.cwd || process.cwd()
1334
+ cwd: opts.cwd || process.cwd(),
1335
+ shell: process.platform === "win32"
1277
1336
  });
1278
1337
  const types = JSON.parse(stdout);
1279
1338
  if (!Array.isArray(types) || types.length === 0) return null;
@@ -1287,6 +1346,48 @@ async function detectWorkItemType(opts = {}) {
1287
1346
  return null;
1288
1347
  }
1289
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
+ }
1290
1391
  var datasource2 = {
1291
1392
  name: "azdevops",
1292
1393
  supportsGit() {
@@ -1295,6 +1396,7 @@ var datasource2 = {
1295
1396
  async list(opts = {}) {
1296
1397
  const conditions = [
1297
1398
  "[System.State] <> 'Closed'",
1399
+ "[System.State] <> 'Done'",
1298
1400
  "[System.State] <> 'Removed'"
1299
1401
  ];
1300
1402
  if (opts.iteration) {
@@ -1317,7 +1419,8 @@ var datasource2 = {
1317
1419
  if (opts.org) args.push("--org", opts.org);
1318
1420
  if (opts.project) args.push("--project", opts.project);
1319
1421
  const { stdout } = await exec2("az", args, {
1320
- cwd: opts.cwd || process.cwd()
1422
+ cwd: opts.cwd || process.cwd(),
1423
+ shell: process.platform === "win32"
1321
1424
  });
1322
1425
  let data;
1323
1426
  try {
@@ -1325,17 +1428,51 @@ var datasource2 = {
1325
1428
  } catch {
1326
1429
  throw new Error(`Failed to parse Azure CLI output: ${stdout.slice(0, 200)}`);
1327
1430
  }
1328
- const items = [];
1329
- if (Array.isArray(data)) {
1330
- for (const row of data) {
1331
- const id = String(row.id ?? row.ID ?? "");
1332
- if (id) {
1333
- const detail = await datasource2.fetch(id, opts);
1334
- items.push(detail);
1335
- }
1431
+ if (!Array.isArray(data)) return [];
1432
+ const ids = data.map((row) => String(row.id ?? row.ID ?? "")).filter(Boolean);
1433
+ if (ids.length === 0) return [];
1434
+ try {
1435
+ const batchArgs = [
1436
+ "boards",
1437
+ "work-item",
1438
+ "show",
1439
+ "--id",
1440
+ ...ids,
1441
+ "--output",
1442
+ "json"
1443
+ ];
1444
+ if (opts.org) batchArgs.push("--org", opts.org);
1445
+ if (opts.project) batchArgs.push("--project", opts.project);
1446
+ const { stdout: batchStdout } = await exec2("az", batchArgs, {
1447
+ cwd: opts.cwd || process.cwd(),
1448
+ shell: process.platform === "win32"
1449
+ });
1450
+ let batchItems;
1451
+ try {
1452
+ batchItems = JSON.parse(batchStdout);
1453
+ } catch {
1454
+ throw new Error(`Failed to parse Azure CLI output: ${batchStdout.slice(0, 200)}`);
1455
+ }
1456
+ const itemsArray = Array.isArray(batchItems) ? batchItems : [batchItems];
1457
+ const commentsArray = [];
1458
+ const CONCURRENCY = 5;
1459
+ for (let i = 0; i < itemsArray.length; i += CONCURRENCY) {
1460
+ const batch = itemsArray.slice(i, i + CONCURRENCY);
1461
+ const batchResults = await Promise.all(
1462
+ batch.map((item) => fetchComments(String(item.id), opts))
1463
+ );
1464
+ commentsArray.push(...batchResults);
1336
1465
  }
1466
+ return itemsArray.map(
1467
+ (item, i) => mapWorkItemToIssueDetails(item, String(item.id), commentsArray[i])
1468
+ );
1469
+ } catch (err) {
1470
+ log.debug(`Batch work-item show failed, falling back to individual fetches: ${log.extractMessage(err)}`);
1471
+ const results = await Promise.all(
1472
+ ids.map((id) => datasource2.fetch(id, opts))
1473
+ );
1474
+ return results;
1337
1475
  }
1338
- return items;
1339
1476
  },
1340
1477
  async fetch(issueId, opts = {}) {
1341
1478
  const args = [
@@ -1354,7 +1491,8 @@ var datasource2 = {
1354
1491
  args.push("--project", opts.project);
1355
1492
  }
1356
1493
  const { stdout } = await exec2("az", args, {
1357
- cwd: opts.cwd || process.cwd()
1494
+ cwd: opts.cwd || process.cwd(),
1495
+ shell: process.platform === "win32"
1358
1496
  });
1359
1497
  let item;
1360
1498
  try {
@@ -1362,24 +1500,8 @@ var datasource2 = {
1362
1500
  } catch {
1363
1501
  throw new Error(`Failed to parse Azure CLI output: ${stdout.slice(0, 200)}`);
1364
1502
  }
1365
- const fields = item.fields ?? {};
1366
1503
  const comments = await fetchComments(issueId, opts);
1367
- return {
1368
- number: String(item.id ?? issueId),
1369
- title: fields["System.Title"] ?? "",
1370
- body: fields["System.Description"] ?? "",
1371
- labels: (fields["System.Tags"] ?? "").split(";").map((t) => t.trim()).filter(Boolean),
1372
- state: fields["System.State"] ?? "",
1373
- url: item._links?.html?.href ?? item.url ?? "",
1374
- comments,
1375
- acceptanceCriteria: fields["Microsoft.VSTS.Common.AcceptanceCriteria"] ?? "",
1376
- iterationPath: fields["System.IterationPath"] || void 0,
1377
- areaPath: fields["System.AreaPath"] || void 0,
1378
- assignee: fields["System.AssignedTo"]?.displayName || void 0,
1379
- priority: fields["Microsoft.VSTS.Common.Priority"] ?? void 0,
1380
- storyPoints: fields["Microsoft.VSTS.Scheduling.StoryPoints"] ?? fields["Microsoft.VSTS.Scheduling.Effort"] ?? fields["Microsoft.VSTS.Scheduling.Size"] ?? void 0,
1381
- workItemType: fields["System.WorkItemType"] || void 0
1382
- };
1504
+ return mapWorkItemToIssueDetails(item, issueId, comments);
1383
1505
  },
1384
1506
  async update(issueId, title, body, opts = {}) {
1385
1507
  const args = [
@@ -1395,9 +1517,34 @@ var datasource2 = {
1395
1517
  ];
1396
1518
  if (opts.org) args.push("--org", opts.org);
1397
1519
  if (opts.project) args.push("--project", opts.project);
1398
- await exec2("az", args, { cwd: opts.cwd || process.cwd() });
1520
+ await exec2("az", args, { cwd: opts.cwd || process.cwd(), shell: process.platform === "win32" });
1399
1521
  },
1400
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";
1401
1548
  const args = [
1402
1549
  "boards",
1403
1550
  "work-item",
@@ -1405,11 +1552,11 @@ var datasource2 = {
1405
1552
  "--id",
1406
1553
  issueId,
1407
1554
  "--state",
1408
- "Closed"
1555
+ state
1409
1556
  ];
1410
1557
  if (opts.org) args.push("--org", opts.org);
1411
1558
  if (opts.project) args.push("--project", opts.project);
1412
- await exec2("az", args, { cwd: opts.cwd || process.cwd() });
1559
+ await exec2("az", args, { cwd: opts.cwd || process.cwd(), shell: process.platform === "win32" });
1413
1560
  },
1414
1561
  async create(title, body, opts = {}) {
1415
1562
  const workItemType = opts.workItemType ?? await detectWorkItemType(opts);
@@ -1434,7 +1581,8 @@ var datasource2 = {
1434
1581
  if (opts.org) args.push("--org", opts.org);
1435
1582
  if (opts.project) args.push("--project", opts.project);
1436
1583
  const { stdout } = await exec2("az", args, {
1437
- cwd: opts.cwd || process.cwd()
1584
+ cwd: opts.cwd || process.cwd(),
1585
+ shell: process.platform === "win32"
1438
1586
  });
1439
1587
  let item;
1440
1588
  try {
@@ -1442,32 +1590,28 @@ var datasource2 = {
1442
1590
  } catch {
1443
1591
  throw new Error(`Failed to parse Azure CLI output: ${stdout.slice(0, 200)}`);
1444
1592
  }
1445
- const fields = item.fields ?? {};
1446
- return {
1447
- number: String(item.id),
1448
- title: fields["System.Title"] ?? title,
1449
- body: fields["System.Description"] ?? body,
1450
- labels: (fields["System.Tags"] ?? "").split(";").map((t) => t.trim()).filter(Boolean),
1451
- state: fields["System.State"] ?? "New",
1452
- url: item._links?.html?.href ?? item.url ?? "",
1453
- comments: [],
1454
- acceptanceCriteria: fields["Microsoft.VSTS.Common.AcceptanceCriteria"] ?? "",
1455
- iterationPath: fields["System.IterationPath"] || void 0,
1456
- areaPath: fields["System.AreaPath"] || void 0,
1457
- assignee: fields["System.AssignedTo"]?.displayName || void 0,
1458
- priority: fields["Microsoft.VSTS.Common.Priority"] ?? void 0,
1459
- storyPoints: fields["Microsoft.VSTS.Scheduling.StoryPoints"] ?? fields["Microsoft.VSTS.Scheduling.Effort"] ?? fields["Microsoft.VSTS.Scheduling.Size"] ?? void 0,
1460
- workItemType: fields["System.WorkItemType"] || workItemType
1461
- };
1593
+ return mapWorkItemToIssueDetails(item, String(item.id), [], {
1594
+ title,
1595
+ body,
1596
+ state: "New",
1597
+ workItemType
1598
+ });
1462
1599
  },
1463
1600
  async getDefaultBranch(opts) {
1464
1601
  try {
1465
- 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" });
1466
1603
  const parts = stdout.trim().split("/");
1467
- return parts[parts.length - 1];
1468
- } catch {
1604
+ const branch = parts[parts.length - 1];
1605
+ if (!isValidBranchName(branch)) {
1606
+ throw new InvalidBranchNameError(branch, "from symbolic-ref output");
1607
+ }
1608
+ return branch;
1609
+ } catch (err) {
1610
+ if (err instanceof InvalidBranchNameError) {
1611
+ throw err;
1612
+ }
1469
1613
  try {
1470
- 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" });
1471
1615
  return "main";
1472
1616
  } catch {
1473
1617
  return "master";
@@ -1476,19 +1620,19 @@ var datasource2 = {
1476
1620
  },
1477
1621
  async getUsername(opts) {
1478
1622
  try {
1479
- 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" });
1480
1624
  const name = slugify(stdout.trim());
1481
1625
  if (name) return name;
1482
1626
  } catch {
1483
1627
  }
1484
1628
  try {
1485
- 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" });
1486
1630
  const name = slugify(stdout.trim());
1487
1631
  if (name) return name;
1488
1632
  } catch {
1489
1633
  }
1490
1634
  try {
1491
- 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" });
1492
1636
  const principal = stdout.trim();
1493
1637
  const prefix = principal.split("@")[0];
1494
1638
  const name = slugify(prefix);
@@ -1499,33 +1643,40 @@ var datasource2 = {
1499
1643
  },
1500
1644
  buildBranchName(issueNumber, title, username) {
1501
1645
  const slug = slugify(title, 50);
1502
- return `${username}/dispatch/${issueNumber}-${slug}`;
1646
+ const branch = `${username}/dispatch/${issueNumber}-${slug}`;
1647
+ if (!isValidBranchName(branch)) {
1648
+ throw new InvalidBranchNameError(branch);
1649
+ }
1650
+ return branch;
1503
1651
  },
1504
1652
  async createAndSwitchBranch(branchName, opts) {
1653
+ if (!isValidBranchName(branchName)) {
1654
+ throw new InvalidBranchNameError(branchName);
1655
+ }
1505
1656
  try {
1506
- await exec2("git", ["checkout", "-b", branchName], { cwd: opts.cwd });
1657
+ await exec2("git", ["checkout", "-b", branchName], { cwd: opts.cwd, shell: process.platform === "win32" });
1507
1658
  } catch (err) {
1508
1659
  const message = log.extractMessage(err);
1509
1660
  if (message.includes("already exists")) {
1510
- await exec2("git", ["checkout", branchName], { cwd: opts.cwd });
1661
+ await exec2("git", ["checkout", branchName], { cwd: opts.cwd, shell: process.platform === "win32" });
1511
1662
  } else {
1512
1663
  throw err;
1513
1664
  }
1514
1665
  }
1515
1666
  },
1516
1667
  async switchBranch(branchName, opts) {
1517
- await exec2("git", ["checkout", branchName], { cwd: opts.cwd });
1668
+ await exec2("git", ["checkout", branchName], { cwd: opts.cwd, shell: process.platform === "win32" });
1518
1669
  },
1519
1670
  async pushBranch(branchName, opts) {
1520
- 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" });
1521
1672
  },
1522
1673
  async commitAllChanges(message, opts) {
1523
- await exec2("git", ["add", "-A"], { cwd: opts.cwd });
1524
- 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" });
1525
1676
  if (!stdout.trim()) {
1526
1677
  return;
1527
1678
  }
1528
- await exec2("git", ["commit", "-m", message], { cwd: opts.cwd });
1679
+ await exec2("git", ["commit", "-m", message], { cwd: opts.cwd, shell: process.platform === "win32" });
1529
1680
  },
1530
1681
  async createPullRequest(branchName, issueNumber, title, body, opts) {
1531
1682
  try {
@@ -1546,7 +1697,7 @@ var datasource2 = {
1546
1697
  "--output",
1547
1698
  "json"
1548
1699
  ],
1549
- { cwd: opts.cwd }
1700
+ { cwd: opts.cwd, shell: process.platform === "win32" }
1550
1701
  );
1551
1702
  let pr;
1552
1703
  try {
@@ -1571,7 +1722,7 @@ var datasource2 = {
1571
1722
  "--output",
1572
1723
  "json"
1573
1724
  ],
1574
- { cwd: opts.cwd }
1725
+ { cwd: opts.cwd, shell: process.platform === "win32" }
1575
1726
  );
1576
1727
  let prs;
1577
1728
  try {
@@ -1607,7 +1758,8 @@ async function fetchComments(workItemId, opts) {
1607
1758
  args.push("--project", opts.project);
1608
1759
  }
1609
1760
  const { stdout } = await exec2("az", args, {
1610
- cwd: opts.cwd || process.cwd()
1761
+ cwd: opts.cwd || process.cwd(),
1762
+ shell: process.platform === "win32"
1611
1763
  });
1612
1764
  const data = JSON.parse(stdout);
1613
1765
  if (data.comments && Array.isArray(data.comments)) {
@@ -1627,8 +1779,9 @@ async function fetchComments(workItemId, opts) {
1627
1779
  // src/datasources/md.ts
1628
1780
  import { execFile as execFile3 } from "child_process";
1629
1781
  import { readFile, writeFile, readdir, mkdir, rename } from "fs/promises";
1630
- 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";
1631
1783
  import { promisify as promisify3 } from "util";
1784
+ import { glob } from "glob";
1632
1785
 
1633
1786
  // src/helpers/errors.ts
1634
1787
  var UnsupportedOperationError = class extends Error {
@@ -1649,6 +1802,15 @@ function resolveDir(opts) {
1649
1802
  const cwd = opts?.cwd ?? process.cwd();
1650
1803
  return join2(cwd, DEFAULT_DIR);
1651
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
+ }
1652
1814
  function extractTitle(content, filename) {
1653
1815
  const match = content.match(/^#\s+(.+)$/m);
1654
1816
  if (match) return match[1].trim();
@@ -1683,6 +1845,19 @@ var datasource3 = {
1683
1845
  return false;
1684
1846
  },
1685
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
+ }
1686
1861
  const dir = resolveDir(opts);
1687
1862
  let entries;
1688
1863
  try {
@@ -1700,23 +1875,20 @@ var datasource3 = {
1700
1875
  return results;
1701
1876
  },
1702
1877
  async fetch(issueId, opts) {
1703
- const dir = resolveDir(opts);
1704
- const filename = issueId.endsWith(".md") ? issueId : `${issueId}.md`;
1705
- const filePath = join2(dir, filename);
1878
+ const filePath = resolveFilePath(issueId, opts);
1706
1879
  const content = await readFile(filePath, "utf-8");
1880
+ const filename = basename(filePath);
1881
+ const dir = dirname2(filePath);
1707
1882
  return toIssueDetails(filename, content, dir);
1708
1883
  },
1709
1884
  async update(issueId, _title, body, opts) {
1710
- const dir = resolveDir(opts);
1711
- const filename = issueId.endsWith(".md") ? issueId : `${issueId}.md`;
1712
- const filePath = join2(dir, filename);
1885
+ const filePath = resolveFilePath(issueId, opts);
1713
1886
  await writeFile(filePath, body, "utf-8");
1714
1887
  },
1715
1888
  async close(issueId, opts) {
1716
- const dir = resolveDir(opts);
1717
- const filename = issueId.endsWith(".md") ? issueId : `${issueId}.md`;
1718
- const filePath = join2(dir, filename);
1719
- const archiveDir = join2(dir, "archive");
1889
+ const filePath = resolveFilePath(issueId, opts);
1890
+ const filename = basename(filePath);
1891
+ const archiveDir = join2(dirname2(filePath), "archive");
1720
1892
  await mkdir(archiveDir, { recursive: true });
1721
1893
  await rename(filePath, join2(archiveDir, filename));
1722
1894
  },
@@ -1733,7 +1905,7 @@ var datasource3 = {
1733
1905
  },
1734
1906
  async getUsername(opts) {
1735
1907
  try {
1736
- 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" });
1737
1909
  const name = stdout.trim();
1738
1910
  if (!name) return "local";
1739
1911
  return slugify(name);
@@ -1782,7 +1954,8 @@ function getDatasource(name) {
1782
1954
  async function getGitRemoteUrl(cwd) {
1783
1955
  try {
1784
1956
  const { stdout } = await exec4("git", ["remote", "get-url", "origin"], {
1785
- cwd
1957
+ cwd,
1958
+ shell: process.platform === "win32"
1786
1959
  });
1787
1960
  return stdout.trim() || null;
1788
1961
  } catch {
@@ -2057,7 +2230,7 @@ import { constants } from "fs";
2057
2230
  // src/config.ts
2058
2231
  init_providers();
2059
2232
  import { readFile as readFile3, writeFile as writeFile3, mkdir as mkdir2 } from "fs/promises";
2060
- import { join as join4, dirname as dirname2 } from "path";
2233
+ import { join as join4, dirname as dirname3 } from "path";
2061
2234
 
2062
2235
  // src/config-prompts.ts
2063
2236
  init_logger();
@@ -2248,7 +2421,7 @@ async function loadConfig(configDir) {
2248
2421
  }
2249
2422
  async function saveConfig(config, configDir) {
2250
2423
  const configPath = getConfigPath(configDir);
2251
- await mkdir2(dirname2(configPath), { recursive: true });
2424
+ await mkdir2(dirname3(configPath), { recursive: true });
2252
2425
  await writeFile3(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
2253
2426
  }
2254
2427
  async function handleConfigCommand(_argv, configDir) {
@@ -2323,15 +2496,16 @@ async function resolveCliConfig(args) {
2323
2496
  // src/orchestrator/spec-pipeline.ts
2324
2497
  import { join as join7 } from "path";
2325
2498
  import { mkdir as mkdir4, readFile as readFile5, rename as rename2, unlink as unlink2 } from "fs/promises";
2326
- import { glob } from "glob";
2499
+ import { glob as glob2 } from "glob";
2327
2500
  init_providers();
2328
2501
 
2329
2502
  // src/agents/spec.ts
2330
2503
  import { mkdir as mkdir3, readFile as readFile4, writeFile as writeFile4, unlink } from "fs/promises";
2331
- import { join as join6, resolve, sep } from "path";
2504
+ import { join as join6, resolve as resolve2, sep } from "path";
2332
2505
  import { randomUUID as randomUUID3 } from "crypto";
2333
2506
  init_logger();
2334
2507
  init_file_logger();
2508
+ init_environment();
2335
2509
  async function boot5(opts) {
2336
2510
  const { provider } = opts;
2337
2511
  if (!provider) {
@@ -2343,8 +2517,8 @@ async function boot5(opts) {
2343
2517
  const { issue, filePath, fileContent, inlineText, cwd: workingDir, outputPath } = genOpts;
2344
2518
  const startTime = Date.now();
2345
2519
  try {
2346
- const resolvedCwd = resolve(workingDir);
2347
- const resolvedOutput = resolve(outputPath);
2520
+ const resolvedCwd = resolve2(workingDir);
2521
+ const resolvedOutput = resolve2(outputPath);
2348
2522
  if (resolvedOutput !== resolvedCwd && !resolvedOutput.startsWith(resolvedCwd + sep)) {
2349
2523
  return {
2350
2524
  data: null,
@@ -2521,6 +2695,8 @@ function buildCommonSpecInstructions(params) {
2521
2695
  ``,
2522
2696
  `\`${cwd}\``,
2523
2697
  ``,
2698
+ formatEnvironmentPrompt(),
2699
+ ``,
2524
2700
  `## Instructions`,
2525
2701
  ``,
2526
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.`,
@@ -2771,7 +2947,7 @@ function buildInlineTextItem(issues, outputDir) {
2771
2947
  return [{ id: filepath, details }];
2772
2948
  }
2773
2949
  async function resolveFileItems(issues, specCwd, concurrency) {
2774
- const files = await glob(issues, { cwd: specCwd, absolute: true });
2950
+ const files = await glob2(issues, { cwd: specCwd, absolute: true });
2775
2951
  if (files.length === 0) {
2776
2952
  log.error(`No files matched the pattern "${Array.isArray(issues) ? issues.join(", ") : issues}".`);
2777
2953
  return null;
@@ -3093,6 +3269,7 @@ async function runSpecPipeline(opts) {
3093
3269
  import { execFile as execFile9 } from "child_process";
3094
3270
  import { promisify as promisify9 } from "util";
3095
3271
  import { readFile as readFile7 } from "fs/promises";
3272
+ import { glob as glob3 } from "glob";
3096
3273
 
3097
3274
  // src/parser.ts
3098
3275
  import { readFile as readFile6, writeFile as writeFile5 } from "fs/promises";
@@ -3194,6 +3371,7 @@ function groupTasksByMode(tasks) {
3194
3371
  // src/agents/planner.ts
3195
3372
  init_logger();
3196
3373
  init_file_logger();
3374
+ init_environment();
3197
3375
  async function boot6(opts) {
3198
3376
  const { provider, cwd } = opts;
3199
3377
  if (!provider) {
@@ -3264,6 +3442,10 @@ function buildPlannerPrompt(task, cwd, fileContext, worktreeRoot) {
3264
3442
  `- All relative paths must resolve within the worktree root above.`
3265
3443
  );
3266
3444
  }
3445
+ sections.push(
3446
+ ``,
3447
+ formatEnvironmentPrompt()
3448
+ );
3267
3449
  sections.push(
3268
3450
  ``,
3269
3451
  `## Instructions`,
@@ -3300,6 +3482,7 @@ function buildPlannerPrompt(task, cwd, fileContext, worktreeRoot) {
3300
3482
  // src/dispatcher.ts
3301
3483
  init_logger();
3302
3484
  init_file_logger();
3485
+ init_environment();
3303
3486
  async function dispatchTask(instance, task, cwd, plan, worktreeRoot) {
3304
3487
  try {
3305
3488
  log.debug(`Dispatching task: ${task.file}:${task.line} \u2014 ${task.text.slice(0, 80)}`);
@@ -3332,6 +3515,8 @@ function buildPrompt(task, cwd, worktreeRoot) {
3332
3515
  `**Source file:** ${task.file}`,
3333
3516
  `**Task (line ${task.line}):** ${task.text}`,
3334
3517
  ``,
3518
+ getEnvironmentBlock(),
3519
+ ``,
3335
3520
  `Instructions:`,
3336
3521
  `- Complete ONLY this specific task \u2014 do not work on other tasks.`,
3337
3522
  `- Make the minimal, correct changes needed.`,
@@ -3349,6 +3534,8 @@ function buildPlannedPrompt(task, cwd, plan, worktreeRoot) {
3349
3534
  `**Source file:** ${task.file}`,
3350
3535
  `**Task (line ${task.line}):** ${task.text}`,
3351
3536
  ``,
3537
+ getEnvironmentBlock(),
3538
+ ``,
3352
3539
  `---`,
3353
3540
  ``,
3354
3541
  `## Execution Plan`,
@@ -3423,8 +3610,9 @@ ${err.stack}` : ""}`);
3423
3610
  // src/agents/commit.ts
3424
3611
  init_logger();
3425
3612
  init_file_logger();
3613
+ init_environment();
3426
3614
  import { mkdir as mkdir5, writeFile as writeFile6 } from "fs/promises";
3427
- import { join as join8, resolve as resolve2 } from "path";
3615
+ import { join as join8, resolve as resolve3 } from "path";
3428
3616
  import { randomUUID as randomUUID4 } from "crypto";
3429
3617
  async function boot8(opts) {
3430
3618
  const { provider } = opts;
@@ -3437,7 +3625,7 @@ async function boot8(opts) {
3437
3625
  name: "commit",
3438
3626
  async generate(genOpts) {
3439
3627
  try {
3440
- const resolvedCwd = resolve2(genOpts.cwd);
3628
+ const resolvedCwd = resolve3(genOpts.cwd);
3441
3629
  const tmpDir = join8(resolvedCwd, ".dispatch", "tmp");
3442
3630
  await mkdir5(tmpDir, { recursive: true });
3443
3631
  const tmpFilename = `commit-${randomUUID4()}.md`;
@@ -3499,6 +3687,8 @@ function buildCommitPrompt(opts) {
3499
3687
  const sections = [
3500
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.`,
3501
3689
  ``,
3690
+ formatEnvironmentPrompt(),
3691
+ ``,
3502
3692
  `## Conventional Commit Guidelines`,
3503
3693
  ``,
3504
3694
  `Follow the Conventional Commits specification (https://www.conventionalcommits.org/):`,
@@ -3620,7 +3810,7 @@ init_logger();
3620
3810
  init_cleanup();
3621
3811
 
3622
3812
  // src/helpers/worktree.ts
3623
- import { join as join9, basename } from "path";
3813
+ import { join as join9, basename as basename2 } from "path";
3624
3814
  import { execFile as execFile7 } from "child_process";
3625
3815
  import { promisify as promisify7 } from "util";
3626
3816
  import { randomUUID as randomUUID5 } from "crypto";
@@ -3628,11 +3818,11 @@ init_logger();
3628
3818
  var exec7 = promisify7(execFile7);
3629
3819
  var WORKTREE_DIR = ".dispatch/worktrees";
3630
3820
  async function git2(args, cwd) {
3631
- const { stdout } = await exec7("git", args, { cwd });
3821
+ const { stdout } = await exec7("git", args, { cwd, shell: process.platform === "win32" });
3632
3822
  return stdout;
3633
3823
  }
3634
3824
  function worktreeName(issueFilename) {
3635
- const base = basename(issueFilename);
3825
+ const base = basename2(issueFilename);
3636
3826
  const withoutExt = base.replace(/\.md$/i, "");
3637
3827
  const match = withoutExt.match(/^(\d+)/);
3638
3828
  return match ? `issue-${match[1]}` : slugify(withoutExt);
@@ -3940,14 +4130,14 @@ init_providers();
3940
4130
 
3941
4131
  // src/orchestrator/datasource-helpers.ts
3942
4132
  init_logger();
3943
- import { basename as basename2, join as join10 } from "path";
4133
+ import { basename as basename3, join as join10 } from "path";
3944
4134
  import { mkdtemp, writeFile as writeFile7 } from "fs/promises";
3945
4135
  import { tmpdir } from "os";
3946
4136
  import { execFile as execFile8 } from "child_process";
3947
4137
  import { promisify as promisify8 } from "util";
3948
4138
  var exec8 = promisify8(execFile8);
3949
4139
  function parseIssueFilename(filePath) {
3950
- const filename = basename2(filePath);
4140
+ const filename = basename3(filePath);
3951
4141
  const match = /^(\d+)-(.+)\.md$/.exec(filename);
3952
4142
  if (!match) return null;
3953
4143
  return { issueId: match[1], slug: match[2] };
@@ -3962,7 +4152,8 @@ async function fetchItemsById(issueIds, datasource4, fetchOpts) {
3962
4152
  const item = await datasource4.fetch(id, fetchOpts);
3963
4153
  items.push(item);
3964
4154
  } catch (err) {
3965
- 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)}`);
3966
4157
  }
3967
4158
  }
3968
4159
  return items;
@@ -3980,8 +4171,8 @@ async function writeItemsToTempDir(items) {
3980
4171
  issueDetailsByFile.set(filepath, item);
3981
4172
  }
3982
4173
  files.sort((a, b) => {
3983
- const numA = parseInt(basename2(a).match(/^(\d+)/)?.[1] ?? "0", 10);
3984
- 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);
3985
4176
  if (numA !== numB) return numA - numB;
3986
4177
  return a.localeCompare(b);
3987
4178
  });
@@ -3992,7 +4183,7 @@ async function getCommitSummaries(defaultBranch, cwd) {
3992
4183
  const { stdout } = await exec8(
3993
4184
  "git",
3994
4185
  ["log", `${defaultBranch}..HEAD`, "--pretty=format:%s"],
3995
- { cwd }
4186
+ { cwd, shell: process.platform === "win32" }
3996
4187
  );
3997
4188
  return stdout.trim().split("\n").filter(Boolean);
3998
4189
  } catch {
@@ -4004,7 +4195,7 @@ async function getBranchDiff(defaultBranch, cwd) {
4004
4195
  const { stdout } = await exec8(
4005
4196
  "git",
4006
4197
  ["diff", `${defaultBranch}..HEAD`],
4007
- { cwd, maxBuffer: 10 * 1024 * 1024 }
4198
+ { cwd, maxBuffer: 10 * 1024 * 1024, shell: process.platform === "win32" }
4008
4199
  );
4009
4200
  return stdout;
4010
4201
  } catch {
@@ -4015,11 +4206,11 @@ async function squashBranchCommits(defaultBranch, message, cwd) {
4015
4206
  const { stdout } = await exec8(
4016
4207
  "git",
4017
4208
  ["merge-base", defaultBranch, "HEAD"],
4018
- { cwd }
4209
+ { cwd, shell: process.platform === "win32" }
4019
4210
  );
4020
4211
  const mergeBase = stdout.trim();
4021
- await exec8("git", ["reset", "--soft", mergeBase], { cwd });
4022
- 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" });
4023
4214
  }
4024
4215
  async function buildPrBody(details, tasks, results, defaultBranch, datasourceName, cwd) {
4025
4216
  const sections = [];
@@ -4115,6 +4306,34 @@ init_timeout();
4115
4306
  import chalk7 from "chalk";
4116
4307
  init_file_logger();
4117
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
+ }
4118
4337
  var DEFAULT_PLAN_TIMEOUT_MIN = 10;
4119
4338
  var DEFAULT_PLAN_RETRIES = 1;
4120
4339
  async function runDispatchPipeline(opts, cwd) {
@@ -4180,7 +4399,14 @@ async function runDispatchPipeline(opts, cwd) {
4180
4399
  }
4181
4400
  const datasource4 = getDatasource(source);
4182
4401
  const fetchOpts = { cwd, org, project, workItemType, iteration, area };
4183
- 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
+ }
4184
4410
  if (items.length === 0) {
4185
4411
  tui.state.phase = "done";
4186
4412
  tui.stop();
@@ -4252,12 +4478,32 @@ async function runDispatchPipeline(opts, cwd) {
4252
4478
  let featureBranchName;
4253
4479
  let featureDefaultBranch;
4254
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
+ }
4255
4492
  try {
4256
4493
  featureDefaultBranch = await datasource4.getDefaultBranch(lifecycleOpts);
4257
4494
  await datasource4.switchBranch(featureDefaultBranch, lifecycleOpts);
4258
- featureBranchName = generateFeatureBranchName();
4259
- await datasource4.createAndSwitchBranch(featureBranchName, lifecycleOpts);
4260
- 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
+ }
4261
4507
  registerCleanup(async () => {
4262
4508
  try {
4263
4509
  await datasource4.switchBranch(featureDefaultBranch, lifecycleOpts);
@@ -4449,12 +4695,18 @@ ${err.stack}` : ""}`);
4449
4695
  fileLogger?.info(`Execution completed successfully (${Date.now() - startTime}ms)`);
4450
4696
  try {
4451
4697
  const parsed = parseIssueFilename(task.file);
4698
+ const updatedContent = await readFile7(task.file, "utf-8");
4452
4699
  if (parsed) {
4453
- const updatedContent = await readFile7(task.file, "utf-8");
4454
4700
  const issueDetails = issueDetailsByFile.get(task.file);
4455
4701
  const title = issueDetails?.title ?? parsed.slug;
4456
4702
  await datasource4.update(parsed.issueId, title, updatedContent, fetchOpts);
4457
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
+ }
4458
4710
  }
4459
4711
  } catch (err) {
4460
4712
  log.warn(`Could not sync task completion to datasource: ${log.formatErrorChain(err)}`);
@@ -4541,13 +4793,13 @@ ${err.stack}` : ""}`);
4541
4793
  }
4542
4794
  try {
4543
4795
  await datasource4.switchBranch(featureBranchName, lifecycleOpts);
4544
- 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" });
4545
4797
  log.debug(`Merged ${branchName} into ${featureBranchName}`);
4546
4798
  } catch (err) {
4547
4799
  const mergeError = `Could not merge ${branchName} into feature branch: ${log.formatErrorChain(err)}`;
4548
4800
  log.warn(mergeError);
4549
4801
  try {
4550
- await exec9("git", ["merge", "--abort"], { cwd });
4802
+ await exec9("git", ["merge", "--abort"], { cwd, shell: process.platform === "win32" });
4551
4803
  } catch {
4552
4804
  }
4553
4805
  for (const task of fileTasks) {
@@ -4565,7 +4817,7 @@ ${err.stack}` : ""}`);
4565
4817
  return;
4566
4818
  }
4567
4819
  try {
4568
- await exec9("git", ["branch", "-d", branchName], { cwd });
4820
+ await exec9("git", ["branch", "-d", branchName], { cwd, shell: process.platform === "win32" });
4569
4821
  log.debug(`Deleted local branch ${branchName}`);
4570
4822
  } catch (err) {
4571
4823
  log.warn(`Could not delete local branch ${branchName}: ${log.formatErrorChain(err)}`);
@@ -4721,13 +4973,20 @@ async function dryRunMode(issueIds, cwd, source, org, project, workItemType, ite
4721
4973
  username = await datasource4.getUsername(lifecycleOpts);
4722
4974
  } catch {
4723
4975
  }
4724
- 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
+ }
4725
4984
  if (items.length === 0) {
4726
4985
  const label = issueIds.length > 0 ? `issue(s) ${issueIds.join(", ")}` : `datasource: ${source}`;
4727
4986
  log.warn("No work items found from " + label);
4728
4987
  return { total: 0, completed: 0, failed: 0, skipped: 0, results: [] };
4729
4988
  }
4730
- const { files } = await writeItemsToTempDir(items);
4989
+ const { files, issueDetailsByFile } = await writeItemsToTempDir(items);
4731
4990
  const taskFiles = [];
4732
4991
  for (const file of files) {
4733
4992
  const tf = await parseTaskFile(file);
@@ -4744,7 +5003,7 @@ async function dryRunMode(issueIds, cwd, source, org, project, workItemType, ite
4744
5003
  `);
4745
5004
  for (const task of allTasks) {
4746
5005
  const parsed = parseIssueFilename(task.file);
4747
- 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);
4748
5007
  const branchInfo = details ? ` [branch: ${datasource4.buildBranchName(details.number, details.title, username)}]` : "";
4749
5008
  log.task(allTasks.indexOf(task), allTasks.length, `${task.file}:${task.line} \u2014 ${task.text}${branchInfo}`);
4750
5009
  }
@@ -4917,7 +5176,7 @@ var HELP = `
4917
5176
  --no-plan Skip the planner agent, dispatch directly
4918
5177
  --no-branch Skip branch creation, push, and PR lifecycle
4919
5178
  --no-worktree Skip git worktree isolation for parallel issues
4920
- --feature Group issues into a single feature branch and PR
5179
+ --feature [name] Group issues into a single feature branch and PR
4921
5180
  --force Ignore prior run state and re-run all tasks
4922
5181
  --concurrency <n> Max parallel dispatches (default: min(cpus, freeMB/500), max: ${MAX_CONCURRENCY})
4923
5182
  --provider <name> Agent backend: ${PROVIDER_NAMES.join(", ")} (default: opencode)
@@ -4963,6 +5222,8 @@ var HELP = `
4963
5222
  dispatch --respec "specs/*.md"
4964
5223
  dispatch --spec "add dark mode toggle to settings page"
4965
5224
  dispatch --spec "feature A should do x" --provider copilot
5225
+ dispatch --feature
5226
+ dispatch --feature my-feature
4966
5227
  dispatch config
4967
5228
  `.trimStart();
4968
5229
  function parseArgs(argv) {
@@ -4972,7 +5233,7 @@ function parseArgs(argv) {
4972
5233
  },
4973
5234
  writeErr: () => {
4974
5235
  }
4975
- }).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(
4976
5237
  new Option("--provider <name>", "Agent backend").choices(PROVIDER_NAMES)
4977
5238
  ).addOption(
4978
5239
  new Option("--source <name>", "Issue source").choices(
@@ -5020,7 +5281,7 @@ function parseArgs(argv) {
5020
5281
  if (isNaN(n) || n <= 0) throw new CommanderError(1, "commander.invalidArgument", "--test-timeout must be a positive number (minutes)");
5021
5282
  return n;
5022
5283
  }
5023
- ).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");
5024
5285
  try {
5025
5286
  program.parse(argv, { from: "user" });
5026
5287
  } catch (err) {
@@ -5055,7 +5316,7 @@ function parseArgs(argv) {
5055
5316
  }
5056
5317
  }
5057
5318
  if (opts.fixTests) args.fixTests = true;
5058
- if (opts.feature) args.feature = true;
5319
+ if (opts.feature) args.feature = opts.feature;
5059
5320
  if (opts.source !== void 0) args.issueSource = opts.source;
5060
5321
  if (opts.concurrency !== void 0) args.concurrency = opts.concurrency;
5061
5322
  if (opts.serverUrl !== void 0) args.serverUrl = opts.serverUrl;
@@ -5105,7 +5366,7 @@ async function main() {
5105
5366
  if (rawArgv[0] === "config") {
5106
5367
  const configProgram = new Command("dispatch-config").exitOverride().configureOutput({ writeOut: () => {
5107
5368
  }, writeErr: () => {
5108
- } }).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));
5109
5370
  try {
5110
5371
  configProgram.parse(rawArgv.slice(1), { from: "user" });
5111
5372
  } catch (err) {
@@ -5136,7 +5397,7 @@ async function main() {
5136
5397
  process.exit(0);
5137
5398
  }
5138
5399
  if (args.version) {
5139
- console.log(`dispatch v${"1.3.0"}`);
5400
+ console.log(`dispatch v${"1.4.0"}`);
5140
5401
  process.exit(0);
5141
5402
  }
5142
5403
  const orchestrator = await boot9({ cwd: args.cwd });