@pruddiman/dispatch 1.2.1 → 1.3.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
@@ -9,6 +9,84 @@ var __export = (target, all) => {
9
9
  __defProp(target, name, { get: all[name], enumerable: true });
10
10
  };
11
11
 
12
+ // src/helpers/file-logger.ts
13
+ import { mkdirSync, writeFileSync, appendFileSync } from "fs";
14
+ import { join, dirname } from "path";
15
+ import { AsyncLocalStorage } from "async_hooks";
16
+ var fileLoggerStorage, FileLogger;
17
+ var init_file_logger = __esm({
18
+ "src/helpers/file-logger.ts"() {
19
+ "use strict";
20
+ fileLoggerStorage = new AsyncLocalStorage();
21
+ FileLogger = class _FileLogger {
22
+ filePath;
23
+ static sanitizeIssueId(issueId) {
24
+ const raw = String(issueId);
25
+ return raw.replace(/[^a-zA-Z0-9._-]/g, "_");
26
+ }
27
+ constructor(issueId, cwd) {
28
+ const safeIssueId = _FileLogger.sanitizeIssueId(issueId);
29
+ this.filePath = join(cwd, ".dispatch", "logs", `issue-${safeIssueId}.log`);
30
+ mkdirSync(dirname(this.filePath), { recursive: true });
31
+ writeFileSync(this.filePath, "", "utf-8");
32
+ }
33
+ write(level, message) {
34
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
35
+ const line = `[${timestamp}] [${level}] ${message}
36
+ `;
37
+ appendFileSync(this.filePath, line, "utf-8");
38
+ }
39
+ info(message) {
40
+ this.write("INFO", message);
41
+ }
42
+ debug(message) {
43
+ this.write("DEBUG", message);
44
+ }
45
+ warn(message) {
46
+ this.write("WARN", message);
47
+ }
48
+ error(message) {
49
+ this.write("ERROR", message);
50
+ }
51
+ success(message) {
52
+ this.write("SUCCESS", message);
53
+ }
54
+ task(message) {
55
+ this.write("TASK", message);
56
+ }
57
+ dim(message) {
58
+ this.write("DIM", message);
59
+ }
60
+ prompt(label, content) {
61
+ const separator = "\u2500".repeat(40);
62
+ this.write("PROMPT", `${label}
63
+ ${separator}
64
+ ${content}
65
+ ${separator}`);
66
+ }
67
+ response(label, content) {
68
+ const separator = "\u2500".repeat(40);
69
+ this.write("RESPONSE", `${label}
70
+ ${separator}
71
+ ${content}
72
+ ${separator}`);
73
+ }
74
+ phase(name) {
75
+ const banner = "\u2550".repeat(40);
76
+ this.write("PHASE", `${banner}
77
+ ${name}
78
+ ${banner}`);
79
+ }
80
+ agentEvent(agent, event, detail) {
81
+ const msg = detail ? `[${agent}] ${event}: ${detail}` : `[${agent}] ${event}`;
82
+ this.write("AGENT", msg);
83
+ }
84
+ close() {
85
+ }
86
+ };
87
+ }
88
+ });
89
+
12
90
  // src/helpers/logger.ts
13
91
  import chalk from "chalk";
14
92
  function resolveLogLevel() {
@@ -24,10 +102,14 @@ function resolveLogLevel() {
24
102
  function shouldLog(level) {
25
103
  return LOG_LEVEL_SEVERITY[level] >= LOG_LEVEL_SEVERITY[currentLevel];
26
104
  }
105
+ function stripAnsi(str) {
106
+ return str.replace(/\x1B\[[0-9;]*m/g, "");
107
+ }
27
108
  var LOG_LEVEL_SEVERITY, currentLevel, MAX_CAUSE_CHAIN_DEPTH, log;
28
109
  var init_logger = __esm({
29
110
  "src/helpers/logger.ts"() {
30
111
  "use strict";
112
+ init_file_logger();
31
113
  LOG_LEVEL_SEVERITY = {
32
114
  debug: 0,
33
115
  info: 1,
@@ -41,26 +123,32 @@ var init_logger = __esm({
41
123
  info(msg) {
42
124
  if (!shouldLog("info")) return;
43
125
  console.log(chalk.blue("\u2139"), msg);
126
+ fileLoggerStorage.getStore()?.info(stripAnsi(msg));
44
127
  },
45
128
  success(msg) {
46
129
  if (!shouldLog("info")) return;
47
130
  console.log(chalk.green("\u2714"), msg);
131
+ fileLoggerStorage.getStore()?.success(stripAnsi(msg));
48
132
  },
49
133
  warn(msg) {
50
134
  if (!shouldLog("warn")) return;
51
135
  console.error(chalk.yellow("\u26A0"), msg);
136
+ fileLoggerStorage.getStore()?.warn(stripAnsi(msg));
52
137
  },
53
138
  error(msg) {
54
139
  if (!shouldLog("error")) return;
55
140
  console.error(chalk.red("\u2716"), msg);
141
+ fileLoggerStorage.getStore()?.error(stripAnsi(msg));
56
142
  },
57
143
  task(index, total, msg) {
58
144
  if (!shouldLog("info")) return;
59
145
  console.log(chalk.cyan(`[${index + 1}/${total}]`), msg);
146
+ fileLoggerStorage.getStore()?.task(stripAnsi(`[${index + 1}/${total}] ${msg}`));
60
147
  },
61
148
  dim(msg) {
62
149
  if (!shouldLog("info")) return;
63
150
  console.log(chalk.dim(msg));
151
+ fileLoggerStorage.getStore()?.dim(stripAnsi(msg));
64
152
  },
65
153
  /**
66
154
  * Print a debug/verbose message. Only visible when the log level is
@@ -70,6 +158,7 @@ var init_logger = __esm({
70
158
  debug(msg) {
71
159
  if (!shouldLog("debug")) return;
72
160
  console.log(chalk.dim(` \u2937 ${msg}`));
161
+ fileLoggerStorage.getStore()?.debug(stripAnsi(msg));
73
162
  },
74
163
  /**
75
164
  * Extract and format the full error cause chain. Node.js network errors
@@ -671,7 +760,9 @@ import { execFile as execFile6 } from "child_process";
671
760
  import { promisify as promisify6 } from "util";
672
761
  async function checkProviderInstalled(name) {
673
762
  try {
674
- await exec6(PROVIDER_BINARIES[name], ["--version"]);
763
+ await exec6(PROVIDER_BINARIES[name], ["--version"], {
764
+ shell: process.platform === "win32"
765
+ });
675
766
  return true;
676
767
  } catch {
677
768
  return false;
@@ -765,11 +856,11 @@ __export(fix_tests_pipeline_exports, {
765
856
  runTestCommand: () => runTestCommand
766
857
  });
767
858
  import { readFile as readFile8 } from "fs/promises";
768
- import { join as join10 } from "path";
859
+ import { join as join11 } from "path";
769
860
  import { execFile as execFileCb } from "child_process";
770
861
  async function detectTestCommand(cwd) {
771
862
  try {
772
- const raw = await readFile8(join10(cwd, "package.json"), "utf-8");
863
+ const raw = await readFile8(join11(cwd, "package.json"), "utf-8");
773
864
  let pkg;
774
865
  try {
775
866
  pkg = JSON.parse(raw);
@@ -844,45 +935,66 @@ async function runFixTestsPipeline(opts) {
844
935
  log.dim(` Working directory: ${cwd}`);
845
936
  return { mode: "fix-tests", success: false };
846
937
  }
847
- try {
848
- log.info("Running test suite...");
849
- const testResult = await runTestCommand(testCommand, cwd);
850
- if (testResult.exitCode === 0) {
851
- log.success("All tests pass \u2014 nothing to fix.");
852
- return { mode: "fix-tests", success: true };
853
- }
854
- log.warn(
855
- `Tests failed (exit code ${testResult.exitCode}). Dispatching AI to fix...`
856
- );
857
- const provider = opts.provider ?? "opencode";
858
- const instance = await bootProvider(provider, { url: opts.serverUrl, cwd });
859
- registerCleanup(() => instance.cleanup());
860
- const prompt = buildFixTestsPrompt(testResult, cwd);
861
- log.debug(`Prompt built (${prompt.length} chars)`);
862
- const sessionId = await instance.createSession();
863
- const response = await instance.prompt(sessionId, prompt);
864
- if (response === null) {
865
- log.error("No response from AI agent.");
938
+ const fileLogger = opts.verbose ? new FileLogger("fix-tests", cwd) : null;
939
+ const pipelineBody = async () => {
940
+ try {
941
+ log.info("Running test suite...");
942
+ const testResult = await runTestCommand(testCommand, cwd);
943
+ fileLoggerStorage.getStore()?.info(`Test run complete (exit code: ${testResult.exitCode})`);
944
+ if (testResult.exitCode === 0) {
945
+ log.success("All tests pass \u2014 nothing to fix.");
946
+ return { mode: "fix-tests", success: true };
947
+ }
948
+ log.warn(
949
+ `Tests failed (exit code ${testResult.exitCode}). Dispatching AI to fix...`
950
+ );
951
+ const provider = opts.provider ?? "opencode";
952
+ const instance = await bootProvider(provider, { url: opts.serverUrl, cwd });
953
+ registerCleanup(() => instance.cleanup());
954
+ const prompt = buildFixTestsPrompt(testResult, cwd);
955
+ log.debug(`Prompt built (${prompt.length} chars)`);
956
+ fileLoggerStorage.getStore()?.prompt("fix-tests", prompt);
957
+ const sessionId = await instance.createSession();
958
+ const response = await instance.prompt(sessionId, prompt);
959
+ if (response === null) {
960
+ fileLoggerStorage.getStore()?.error("No response from AI agent.");
961
+ log.error("No response from AI agent.");
962
+ await instance.cleanup();
963
+ return { mode: "fix-tests", success: false, error: "No response from agent" };
964
+ }
965
+ if (response) fileLoggerStorage.getStore()?.response("fix-tests", response);
966
+ log.success("AI agent completed fixes.");
967
+ fileLoggerStorage.getStore()?.phase("Verification");
968
+ log.info("Re-running tests to verify fixes...");
969
+ const verifyResult = await runTestCommand(testCommand, cwd);
866
970
  await instance.cleanup();
867
- return { mode: "fix-tests", success: false, error: "No response from agent" };
868
- }
869
- log.success("AI agent completed fixes.");
870
- log.info("Re-running tests to verify fixes...");
871
- const verifyResult = await runTestCommand(testCommand, cwd);
872
- await instance.cleanup();
873
- if (verifyResult.exitCode === 0) {
874
- log.success("All tests pass after fixes!");
875
- return { mode: "fix-tests", success: true };
971
+ fileLoggerStorage.getStore()?.info(`Verification result: exit code ${verifyResult.exitCode}`);
972
+ if (verifyResult.exitCode === 0) {
973
+ log.success("All tests pass after fixes!");
974
+ return { mode: "fix-tests", success: true };
975
+ }
976
+ log.warn(
977
+ `Tests still failing after fix attempt (exit code ${verifyResult.exitCode}).`
978
+ );
979
+ return { mode: "fix-tests", success: false, error: "Tests still failing after fix attempt" };
980
+ } catch (err) {
981
+ const message = log.extractMessage(err);
982
+ fileLoggerStorage.getStore()?.error(`Fix-tests pipeline failed: ${message}${err instanceof Error && err.stack ? `
983
+ ${err.stack}` : ""}`);
984
+ log.error(`Fix-tests pipeline failed: ${log.formatErrorChain(err)}`);
985
+ return { mode: "fix-tests", success: false, error: message };
876
986
  }
877
- log.warn(
878
- `Tests still failing after fix attempt (exit code ${verifyResult.exitCode}).`
879
- );
880
- return { mode: "fix-tests", success: false, error: "Tests still failing after fix attempt" };
881
- } catch (err) {
882
- const message = log.extractMessage(err);
883
- log.error(`Fix-tests pipeline failed: ${log.formatErrorChain(err)}`);
884
- return { mode: "fix-tests", success: false, error: message };
987
+ };
988
+ if (fileLogger) {
989
+ return fileLoggerStorage.run(fileLogger, async () => {
990
+ try {
991
+ return await pipelineBody();
992
+ } finally {
993
+ fileLogger.close();
994
+ }
995
+ });
885
996
  }
997
+ return pipelineBody();
886
998
  }
887
999
  var init_fix_tests_pipeline = __esm({
888
1000
  "src/orchestrator/fix-tests-pipeline.ts"() {
@@ -890,11 +1002,13 @@ var init_fix_tests_pipeline = __esm({
890
1002
  init_providers();
891
1003
  init_cleanup();
892
1004
  init_logger();
1005
+ init_file_logger();
893
1006
  }
894
1007
  });
895
1008
 
896
1009
  // src/cli.ts
897
- import { resolve as resolve3, join as join11 } from "path";
1010
+ import { resolve as resolve3, join as join12 } from "path";
1011
+ import { Command, Option, CommanderError } from "commander";
898
1012
 
899
1013
  // src/spec-generator.ts
900
1014
  import { cpus, freemem } from "os";
@@ -909,8 +1023,8 @@ import { promisify } from "util";
909
1023
 
910
1024
  // src/helpers/slugify.ts
911
1025
  var MAX_SLUG_LENGTH = 60;
912
- function slugify(input2, maxLength) {
913
- const slug = input2.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
1026
+ function slugify(input3, maxLength) {
1027
+ const slug = input3.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
914
1028
  return maxLength != null ? slug.slice(0, maxLength) : slug;
915
1029
  }
916
1030
 
@@ -1179,7 +1293,26 @@ var datasource2 = {
1179
1293
  return true;
1180
1294
  },
1181
1295
  async list(opts = {}) {
1182
- const wiql = "SELECT [System.Id] FROM workitems WHERE [System.State] <> 'Closed' AND [System.State] <> 'Removed' ORDER BY [System.CreatedDate] DESC";
1296
+ const conditions = [
1297
+ "[System.State] <> 'Closed'",
1298
+ "[System.State] <> 'Removed'"
1299
+ ];
1300
+ if (opts.iteration) {
1301
+ const iterValue = String(opts.iteration).trim();
1302
+ if (iterValue === "@CurrentIteration") {
1303
+ conditions.push(`[System.IterationPath] UNDER @CurrentIteration`);
1304
+ } else {
1305
+ const escaped = iterValue.replace(/'/g, "''");
1306
+ if (escaped) conditions.push(`[System.IterationPath] UNDER '${escaped}'`);
1307
+ }
1308
+ }
1309
+ if (opts.area) {
1310
+ const area = String(opts.area).trim().replace(/'/g, "''");
1311
+ if (area) {
1312
+ conditions.push(`[System.AreaPath] UNDER '${area}'`);
1313
+ }
1314
+ }
1315
+ const wiql = `SELECT [System.Id] FROM workitems WHERE ${conditions.join(" AND ")} ORDER BY [System.CreatedDate] DESC`;
1183
1316
  const args = ["boards", "query", "--wiql", wiql, "--output", "json"];
1184
1317
  if (opts.org) args.push("--org", opts.org);
1185
1318
  if (opts.project) args.push("--project", opts.project);
@@ -1239,7 +1372,13 @@ var datasource2 = {
1239
1372
  state: fields["System.State"] ?? "",
1240
1373
  url: item._links?.html?.href ?? item.url ?? "",
1241
1374
  comments,
1242
- acceptanceCriteria: fields["Microsoft.VSTS.Common.AcceptanceCriteria"] ?? ""
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
1243
1382
  };
1244
1383
  },
1245
1384
  async update(issueId, title, body, opts = {}) {
@@ -1312,7 +1451,13 @@ var datasource2 = {
1312
1451
  state: fields["System.State"] ?? "New",
1313
1452
  url: item._links?.html?.href ?? item.url ?? "",
1314
1453
  comments: [],
1315
- acceptanceCriteria: fields["Microsoft.VSTS.Common.AcceptanceCriteria"] ?? ""
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
1316
1461
  };
1317
1462
  },
1318
1463
  async getDefaultBranch(opts) {
@@ -1332,12 +1477,25 @@ var datasource2 = {
1332
1477
  async getUsername(opts) {
1333
1478
  try {
1334
1479
  const { stdout } = await exec2("git", ["config", "user.name"], { cwd: opts.cwd });
1335
- const name = stdout.trim();
1336
- if (!name) return "unknown";
1337
- return slugify(name);
1480
+ const name = slugify(stdout.trim());
1481
+ if (name) return name;
1338
1482
  } catch {
1339
- return "unknown";
1340
1483
  }
1484
+ try {
1485
+ const { stdout } = await exec2("az", ["account", "show", "--query", "user.name", "-o", "tsv"], { cwd: opts.cwd });
1486
+ const name = slugify(stdout.trim());
1487
+ if (name) return name;
1488
+ } catch {
1489
+ }
1490
+ try {
1491
+ const { stdout } = await exec2("az", ["account", "show", "--query", "user.principalName", "-o", "tsv"], { cwd: opts.cwd });
1492
+ const principal = stdout.trim();
1493
+ const prefix = principal.split("@")[0];
1494
+ const name = slugify(prefix);
1495
+ if (name) return name;
1496
+ } catch {
1497
+ }
1498
+ return "unknown";
1341
1499
  },
1342
1500
  buildBranchName(issueNumber, title, username) {
1343
1501
  const slug = slugify(title, 50);
@@ -1469,7 +1627,7 @@ async function fetchComments(workItemId, opts) {
1469
1627
  // src/datasources/md.ts
1470
1628
  import { execFile as execFile3 } from "child_process";
1471
1629
  import { readFile, writeFile, readdir, mkdir, rename } from "fs/promises";
1472
- import { join, parse as parsePath } from "path";
1630
+ import { join as join2, parse as parsePath } from "path";
1473
1631
  import { promisify as promisify3 } from "util";
1474
1632
 
1475
1633
  // src/helpers/errors.ts
@@ -1489,7 +1647,7 @@ var exec3 = promisify3(execFile3);
1489
1647
  var DEFAULT_DIR = ".dispatch/specs";
1490
1648
  function resolveDir(opts) {
1491
1649
  const cwd = opts?.cwd ?? process.cwd();
1492
- return join(cwd, DEFAULT_DIR);
1650
+ return join2(cwd, DEFAULT_DIR);
1493
1651
  }
1494
1652
  function extractTitle(content, filename) {
1495
1653
  const match = content.match(/^#\s+(.+)$/m);
@@ -1514,7 +1672,7 @@ function toIssueDetails(filename, content, dir) {
1514
1672
  body: content,
1515
1673
  labels: [],
1516
1674
  state: "open",
1517
- url: join(dir, filename),
1675
+ url: join2(dir, filename),
1518
1676
  comments: [],
1519
1677
  acceptanceCriteria: ""
1520
1678
  };
@@ -1535,7 +1693,7 @@ var datasource3 = {
1535
1693
  const mdFiles = entries.filter((f) => f.endsWith(".md")).sort();
1536
1694
  const results = [];
1537
1695
  for (const filename of mdFiles) {
1538
- const filePath = join(dir, filename);
1696
+ const filePath = join2(dir, filename);
1539
1697
  const content = await readFile(filePath, "utf-8");
1540
1698
  results.push(toIssueDetails(filename, content, dir));
1541
1699
  }
@@ -1544,29 +1702,29 @@ var datasource3 = {
1544
1702
  async fetch(issueId, opts) {
1545
1703
  const dir = resolveDir(opts);
1546
1704
  const filename = issueId.endsWith(".md") ? issueId : `${issueId}.md`;
1547
- const filePath = join(dir, filename);
1705
+ const filePath = join2(dir, filename);
1548
1706
  const content = await readFile(filePath, "utf-8");
1549
1707
  return toIssueDetails(filename, content, dir);
1550
1708
  },
1551
1709
  async update(issueId, _title, body, opts) {
1552
1710
  const dir = resolveDir(opts);
1553
1711
  const filename = issueId.endsWith(".md") ? issueId : `${issueId}.md`;
1554
- const filePath = join(dir, filename);
1712
+ const filePath = join2(dir, filename);
1555
1713
  await writeFile(filePath, body, "utf-8");
1556
1714
  },
1557
1715
  async close(issueId, opts) {
1558
1716
  const dir = resolveDir(opts);
1559
1717
  const filename = issueId.endsWith(".md") ? issueId : `${issueId}.md`;
1560
- const filePath = join(dir, filename);
1561
- const archiveDir = join(dir, "archive");
1718
+ const filePath = join2(dir, filename);
1719
+ const archiveDir = join2(dir, "archive");
1562
1720
  await mkdir(archiveDir, { recursive: true });
1563
- await rename(filePath, join(archiveDir, filename));
1721
+ await rename(filePath, join2(archiveDir, filename));
1564
1722
  },
1565
1723
  async create(title, body, opts) {
1566
1724
  const dir = resolveDir(opts);
1567
1725
  await mkdir(dir, { recursive: true });
1568
1726
  const filename = `${slugify(title)}.md`;
1569
- const filePath = join(dir, filename);
1727
+ const filePath = join2(dir, filename);
1570
1728
  await writeFile(filePath, body, "utf-8");
1571
1729
  return toIssueDetails(filename, body, dir);
1572
1730
  },
@@ -1646,6 +1804,36 @@ async function detectDatasource(cwd) {
1646
1804
  }
1647
1805
  return null;
1648
1806
  }
1807
+ function parseAzDevOpsRemoteUrl(url) {
1808
+ const httpsMatch = url.match(
1809
+ /^https?:\/\/(?:[^@]+@)?dev\.azure\.com\/([^/]+)\/([^/]+)\/_git\//i
1810
+ );
1811
+ if (httpsMatch) {
1812
+ return {
1813
+ orgUrl: `https://dev.azure.com/${decodeURIComponent(httpsMatch[1])}`,
1814
+ project: decodeURIComponent(httpsMatch[2])
1815
+ };
1816
+ }
1817
+ const sshMatch = url.match(
1818
+ /^git@ssh\.dev\.azure\.com:v3\/([^/]+)\/([^/]+)\//i
1819
+ );
1820
+ if (sshMatch) {
1821
+ return {
1822
+ orgUrl: `https://dev.azure.com/${decodeURIComponent(sshMatch[1])}`,
1823
+ project: decodeURIComponent(sshMatch[2])
1824
+ };
1825
+ }
1826
+ const legacyMatch = url.match(
1827
+ /^https?:\/\/([^.]+)\.visualstudio\.com\/(?:DefaultCollection\/)?([^/]+)\/_git\//i
1828
+ );
1829
+ if (legacyMatch) {
1830
+ return {
1831
+ orgUrl: `https://dev.azure.com/${decodeURIComponent(legacyMatch[1])}`,
1832
+ project: decodeURIComponent(legacyMatch[2])
1833
+ };
1834
+ }
1835
+ return null;
1836
+ }
1649
1837
 
1650
1838
  // src/spec-generator.ts
1651
1839
  init_logger();
@@ -1662,16 +1850,16 @@ var RECOGNIZED_H2 = /* @__PURE__ */ new Set([
1662
1850
  function defaultConcurrency() {
1663
1851
  return Math.max(1, Math.min(cpus().length, Math.floor(freemem() / 1024 / 1024 / MB_PER_CONCURRENT_TASK)));
1664
1852
  }
1665
- function isIssueNumbers(input2) {
1666
- if (Array.isArray(input2)) return false;
1667
- return /^\d+(,\s*\d+)*$/.test(input2);
1853
+ function isIssueNumbers(input3) {
1854
+ if (Array.isArray(input3)) return false;
1855
+ return /^\d+(,\s*\d+)*$/.test(input3);
1668
1856
  }
1669
- function isGlobOrFilePath(input2) {
1670
- if (Array.isArray(input2)) return true;
1671
- if (/[*?\[{]/.test(input2)) return true;
1672
- if (/[/\\]/.test(input2)) return true;
1673
- if (/^\.\.?\//.test(input2)) return true;
1674
- if (/\.(md|txt|yaml|yml|json|ts|js|tsx|jsx)$/i.test(input2)) return true;
1857
+ function isGlobOrFilePath(input3) {
1858
+ if (Array.isArray(input3)) return true;
1859
+ if (/[*?\[{]/.test(input3)) return true;
1860
+ if (/[/\\]/.test(input3)) return true;
1861
+ if (/^\.\.?[\/\\]/.test(input3)) return true;
1862
+ if (/\.(md|txt|yaml|yml|json|ts|js|tsx|jsx)$/i.test(input3)) return true;
1675
1863
  return false;
1676
1864
  }
1677
1865
  function extractSpecContent(raw) {
@@ -1797,7 +1985,7 @@ function semverGte(current, minimum) {
1797
1985
  async function checkPrereqs(context) {
1798
1986
  const failures = [];
1799
1987
  try {
1800
- await exec5("git", ["--version"]);
1988
+ await exec5("git", ["--version"], { shell: process.platform === "win32" });
1801
1989
  } catch {
1802
1990
  failures.push("git is required but was not found on PATH. Install it from https://git-scm.com");
1803
1991
  }
@@ -1809,7 +1997,7 @@ async function checkPrereqs(context) {
1809
1997
  }
1810
1998
  if (context?.datasource === "github") {
1811
1999
  try {
1812
- await exec5("gh", ["--version"]);
2000
+ await exec5("gh", ["--version"], { shell: process.platform === "win32" });
1813
2001
  } catch {
1814
2002
  failures.push(
1815
2003
  "gh (GitHub CLI) is required for the github datasource but was not found on PATH. Install it from https://cli.github.com/"
@@ -1818,7 +2006,7 @@ async function checkPrereqs(context) {
1818
2006
  }
1819
2007
  if (context?.datasource === "azdevops") {
1820
2008
  try {
1821
- await exec5("az", ["--version"]);
2009
+ await exec5("az", ["--version"], { shell: process.platform === "win32" });
1822
2010
  } catch {
1823
2011
  failures.push(
1824
2012
  "az (Azure CLI) is required for the azdevops datasource but was not found on PATH. Install it from https://learn.microsoft.com/en-us/cli/azure/"
@@ -1831,17 +2019,23 @@ async function checkPrereqs(context) {
1831
2019
  // src/helpers/gitignore.ts
1832
2020
  init_logger();
1833
2021
  import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
1834
- import { join as join2 } from "path";
2022
+ import { join as join3 } from "path";
1835
2023
  async function ensureGitignoreEntry(repoRoot, entry) {
1836
- const gitignorePath = join2(repoRoot, ".gitignore");
2024
+ const gitignorePath = join3(repoRoot, ".gitignore");
1837
2025
  let contents = "";
1838
2026
  try {
1839
2027
  contents = await readFile2(gitignorePath, "utf8");
1840
- } catch {
2028
+ } catch (err) {
2029
+ if (err instanceof Error && "code" in err && err.code === "ENOENT") {
2030
+ } else {
2031
+ log.warn(`Could not read .gitignore: ${String(err)}`);
2032
+ return;
2033
+ }
1841
2034
  }
1842
- const lines = contents.split("\n").map((l) => l.trim());
2035
+ const lines = contents.split(/\r?\n/);
1843
2036
  const bare = entry.replace(/\/$/, "");
1844
- if (lines.includes(entry) || lines.includes(bare)) {
2037
+ const withSlash = bare + "/";
2038
+ if (lines.includes(entry) || lines.includes(bare) || lines.includes(withSlash)) {
1845
2039
  return;
1846
2040
  }
1847
2041
  try {
@@ -1856,18 +2050,18 @@ async function ensureGitignoreEntry(repoRoot, entry) {
1856
2050
 
1857
2051
  // src/orchestrator/cli-config.ts
1858
2052
  init_logger();
1859
- import { join as join4 } from "path";
2053
+ import { join as join5 } from "path";
1860
2054
  import { access } from "fs/promises";
1861
2055
  import { constants } from "fs";
1862
2056
 
1863
2057
  // src/config.ts
1864
2058
  init_providers();
1865
2059
  import { readFile as readFile3, writeFile as writeFile3, mkdir as mkdir2 } from "fs/promises";
1866
- import { join as join3, dirname } from "path";
2060
+ import { join as join4, dirname as dirname2 } from "path";
1867
2061
 
1868
2062
  // src/config-prompts.ts
1869
2063
  init_logger();
1870
- import { select, confirm } from "@inquirer/prompts";
2064
+ import { select, confirm, input as input2 } from "@inquirer/prompts";
1871
2065
  import chalk3 from "chalk";
1872
2066
  init_providers();
1873
2067
  async function runInteractiveConfigWizard(configDir) {
@@ -1947,6 +2141,54 @@ async function runInteractiveConfigWizard(configDir) {
1947
2141
  default: datasourceDefault
1948
2142
  });
1949
2143
  const source = selectedSource === "auto" ? void 0 : selectedSource;
2144
+ let org;
2145
+ let project;
2146
+ let workItemType;
2147
+ let iteration;
2148
+ let area;
2149
+ const effectiveSource = source ?? detectedSource;
2150
+ if (effectiveSource === "azdevops") {
2151
+ let defaultOrg = existing.org ?? "";
2152
+ let defaultProject = existing.project ?? "";
2153
+ try {
2154
+ const remoteUrl = await getGitRemoteUrl(process.cwd());
2155
+ if (remoteUrl) {
2156
+ const parsed = parseAzDevOpsRemoteUrl(remoteUrl);
2157
+ if (parsed) {
2158
+ if (!defaultOrg) defaultOrg = parsed.orgUrl;
2159
+ if (!defaultProject) defaultProject = parsed.project;
2160
+ }
2161
+ }
2162
+ } catch {
2163
+ }
2164
+ console.log();
2165
+ log.info(chalk3.bold("Azure DevOps settings") + chalk3.dim(" (leave empty to skip):"));
2166
+ const orgInput = await input2({
2167
+ message: "Organization URL:",
2168
+ default: defaultOrg || void 0
2169
+ });
2170
+ if (orgInput.trim()) org = orgInput.trim();
2171
+ const projectInput = await input2({
2172
+ message: "Project name:",
2173
+ default: defaultProject || void 0
2174
+ });
2175
+ if (projectInput.trim()) project = projectInput.trim();
2176
+ const workItemTypeInput = await input2({
2177
+ message: "Work item type (e.g. User Story, Bug):",
2178
+ default: existing.workItemType ?? void 0
2179
+ });
2180
+ if (workItemTypeInput.trim()) workItemType = workItemTypeInput.trim();
2181
+ const iterationInput = await input2({
2182
+ message: "Iteration path (e.g. MyProject\\Sprint 1, or @CurrentIteration):",
2183
+ default: existing.iteration ?? void 0
2184
+ });
2185
+ if (iterationInput.trim()) iteration = iterationInput.trim();
2186
+ const areaInput = await input2({
2187
+ message: "Area path (e.g. MyProject\\Team A):",
2188
+ default: existing.area ?? void 0
2189
+ });
2190
+ if (areaInput.trim()) area = areaInput.trim();
2191
+ }
1950
2192
  const newConfig = {
1951
2193
  provider,
1952
2194
  source
@@ -1954,6 +2196,11 @@ async function runInteractiveConfigWizard(configDir) {
1954
2196
  if (selectedModel !== void 0) {
1955
2197
  newConfig.model = selectedModel;
1956
2198
  }
2199
+ if (org !== void 0) newConfig.org = org;
2200
+ if (project !== void 0) newConfig.project = project;
2201
+ if (workItemType !== void 0) newConfig.workItemType = workItemType;
2202
+ if (iteration !== void 0) newConfig.iteration = iteration;
2203
+ if (area !== void 0) newConfig.area = area;
1957
2204
  console.log();
1958
2205
  log.info(chalk3.bold("Configuration summary:"));
1959
2206
  for (const [key, value] of Object.entries(newConfig)) {
@@ -1985,10 +2232,10 @@ var CONFIG_BOUNDS = {
1985
2232
  planTimeout: { min: 1, max: 120 },
1986
2233
  concurrency: { min: 1, max: 64 }
1987
2234
  };
1988
- var CONFIG_KEYS = ["provider", "model", "source", "testTimeout", "planTimeout", "concurrency"];
2235
+ var CONFIG_KEYS = ["provider", "model", "source", "testTimeout", "planTimeout", "concurrency", "org", "project", "workItemType", "iteration", "area"];
1989
2236
  function getConfigPath(configDir) {
1990
- const dir = configDir ?? join3(process.cwd(), ".dispatch");
1991
- return join3(dir, "config.json");
2237
+ const dir = configDir ?? join4(process.cwd(), ".dispatch");
2238
+ return join4(dir, "config.json");
1992
2239
  }
1993
2240
  async function loadConfig(configDir) {
1994
2241
  const configPath = getConfigPath(configDir);
@@ -2001,7 +2248,7 @@ async function loadConfig(configDir) {
2001
2248
  }
2002
2249
  async function saveConfig(config, configDir) {
2003
2250
  const configPath = getConfigPath(configDir);
2004
- await mkdir2(dirname(configPath), { recursive: true });
2251
+ await mkdir2(dirname2(configPath), { recursive: true });
2005
2252
  await writeFile3(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
2006
2253
  }
2007
2254
  async function handleConfigCommand(_argv, configDir) {
@@ -2015,14 +2262,19 @@ var CONFIG_TO_CLI = {
2015
2262
  source: "issueSource",
2016
2263
  testTimeout: "testTimeout",
2017
2264
  planTimeout: "planTimeout",
2018
- concurrency: "concurrency"
2265
+ concurrency: "concurrency",
2266
+ org: "org",
2267
+ project: "project",
2268
+ workItemType: "workItemType",
2269
+ iteration: "iteration",
2270
+ area: "area"
2019
2271
  };
2020
2272
  function setCliField(target, key, value) {
2021
2273
  target[key] = value;
2022
2274
  }
2023
2275
  async function resolveCliConfig(args) {
2024
2276
  const { explicitFlags } = args;
2025
- const configDir = join4(args.cwd, ".dispatch");
2277
+ const configDir = join5(args.cwd, ".dispatch");
2026
2278
  const config = await loadConfig(configDir);
2027
2279
  const merged = { ...args };
2028
2280
  for (const configKey of CONFIG_KEYS) {
@@ -2069,16 +2321,17 @@ async function resolveCliConfig(args) {
2069
2321
  }
2070
2322
 
2071
2323
  // src/orchestrator/spec-pipeline.ts
2072
- import { join as join6 } from "path";
2324
+ import { join as join7 } from "path";
2073
2325
  import { mkdir as mkdir4, readFile as readFile5, rename as rename2, unlink as unlink2 } from "fs/promises";
2074
2326
  import { glob } from "glob";
2075
2327
  init_providers();
2076
2328
 
2077
2329
  // src/agents/spec.ts
2078
2330
  import { mkdir as mkdir3, readFile as readFile4, writeFile as writeFile4, unlink } from "fs/promises";
2079
- import { join as join5, resolve, sep } from "path";
2331
+ import { join as join6, resolve, sep } from "path";
2080
2332
  import { randomUUID as randomUUID3 } from "crypto";
2081
2333
  init_logger();
2334
+ init_file_logger();
2082
2335
  async function boot5(opts) {
2083
2336
  const { provider } = opts;
2084
2337
  if (!provider) {
@@ -2100,10 +2353,10 @@ async function boot5(opts) {
2100
2353
  durationMs: Date.now() - startTime
2101
2354
  };
2102
2355
  }
2103
- const tmpDir = join5(resolvedCwd, ".dispatch", "tmp");
2356
+ const tmpDir = join6(resolvedCwd, ".dispatch", "tmp");
2104
2357
  await mkdir3(tmpDir, { recursive: true });
2105
2358
  const tmpFilename = `spec-${randomUUID3()}.md`;
2106
- const tmpPath = join5(tmpDir, tmpFilename);
2359
+ const tmpPath = join6(tmpDir, tmpFilename);
2107
2360
  let prompt;
2108
2361
  if (issue) {
2109
2362
  prompt = buildSpecPrompt(issue, workingDir, tmpPath);
@@ -2119,6 +2372,7 @@ async function boot5(opts) {
2119
2372
  durationMs: Date.now() - startTime
2120
2373
  };
2121
2374
  }
2375
+ fileLoggerStorage.getStore()?.prompt("spec", prompt);
2122
2376
  const sessionId = await provider.createSession();
2123
2377
  log.debug(`Spec prompt built (${prompt.length} chars)`);
2124
2378
  const response = await provider.prompt(sessionId, prompt);
@@ -2131,6 +2385,7 @@ async function boot5(opts) {
2131
2385
  };
2132
2386
  }
2133
2387
  log.debug(`Spec agent response (${response.length} chars)`);
2388
+ fileLoggerStorage.getStore()?.response("spec", response);
2134
2389
  let rawContent;
2135
2390
  try {
2136
2391
  rawContent = await readFile4(tmpPath, "utf-8");
@@ -2154,6 +2409,7 @@ async function boot5(opts) {
2154
2409
  await unlink(tmpPath);
2155
2410
  } catch {
2156
2411
  }
2412
+ fileLoggerStorage.getStore()?.agentEvent("spec", "completed", `${Date.now() - startTime}ms`);
2157
2413
  return {
2158
2414
  data: {
2159
2415
  content: cleanedContent,
@@ -2165,6 +2421,8 @@ async function boot5(opts) {
2165
2421
  };
2166
2422
  } catch (err) {
2167
2423
  const message = log.extractMessage(err);
2424
+ fileLoggerStorage.getStore()?.error(`spec error: ${message}${err instanceof Error && err.stack ? `
2425
+ ${err.stack}` : ""}`);
2168
2426
  return {
2169
2427
  data: null,
2170
2428
  success: false,
@@ -2400,6 +2658,7 @@ function buildInlineTextSpecPrompt(text, cwd, outputPath) {
2400
2658
  // src/orchestrator/spec-pipeline.ts
2401
2659
  init_cleanup();
2402
2660
  init_logger();
2661
+ init_file_logger();
2403
2662
  import chalk5 from "chalk";
2404
2663
 
2405
2664
  // src/helpers/format.ts
@@ -2452,11 +2711,11 @@ async function withRetry(fn, maxRetries, options) {
2452
2711
  // src/orchestrator/spec-pipeline.ts
2453
2712
  init_timeout();
2454
2713
  var FETCH_TIMEOUT_MS = 3e4;
2455
- async function resolveDatasource(issues, issueSource, specCwd, org, project, workItemType) {
2714
+ async function resolveDatasource(issues, issueSource, specCwd, org, project, workItemType, iteration, area) {
2456
2715
  const source = await resolveSource(issues, issueSource, specCwd);
2457
2716
  if (!source) return null;
2458
2717
  const datasource4 = getDatasource(source);
2459
- const fetchOpts = { cwd: specCwd, org, project, workItemType };
2718
+ const fetchOpts = { cwd: specCwd, org, project, workItemType, iteration, area };
2460
2719
  return { source, datasource: datasource4, fetchOpts };
2461
2720
  }
2462
2721
  async function fetchTrackerItems(issues, datasource4, fetchOpts, concurrency, source) {
@@ -2497,7 +2756,7 @@ function buildInlineTextItem(issues, outputDir) {
2497
2756
  const title = text.length > 80 ? text.slice(0, 80).trimEnd() + "\u2026" : text;
2498
2757
  const slug = slugify(text, MAX_SLUG_LENGTH);
2499
2758
  const filename = `${slug}.md`;
2500
- const filepath = join6(outputDir, filename);
2759
+ const filepath = join7(outputDir, filename);
2501
2760
  const details = {
2502
2761
  number: filepath,
2503
2762
  title,
@@ -2559,7 +2818,7 @@ function previewDryRun(validItems, items, isTrackerMode, isInlineText, outputDir
2559
2818
  let filepath;
2560
2819
  if (isTrackerMode) {
2561
2820
  const slug = slugify(details.title, 60);
2562
- filepath = join6(outputDir, `${id}-${slug}.md`);
2821
+ filepath = join7(outputDir, `${id}-${slug}.md`);
2563
2822
  } else {
2564
2823
  filepath = id;
2565
2824
  }
@@ -2617,72 +2876,92 @@ async function generateSpecsBatch(validItems, items, specAgent, instance, isTrac
2617
2876
  log.error(`Skipping item ${id}: missing issue details`);
2618
2877
  return null;
2619
2878
  }
2620
- let filepath;
2621
- if (isTrackerMode) {
2622
- const slug = slugify(details.title, MAX_SLUG_LENGTH);
2623
- const filename = `${id}-${slug}.md`;
2624
- filepath = join6(outputDir, filename);
2625
- } else if (isInlineText) {
2626
- filepath = id;
2627
- } else {
2628
- filepath = id;
2629
- }
2630
- try {
2631
- log.info(`Generating spec for ${isTrackerMode ? `#${id}` : filepath}: ${details.title}...`);
2632
- const result = await withRetry(
2633
- () => specAgent.generate({
2634
- issue: isTrackerMode ? details : void 0,
2635
- filePath: isTrackerMode ? void 0 : id,
2636
- fileContent: isTrackerMode ? void 0 : details.body,
2637
- cwd: specCwd,
2638
- outputPath: filepath
2639
- }),
2640
- retries,
2641
- { label: `specAgent.generate(${isTrackerMode ? `#${id}` : filepath})` }
2642
- );
2643
- if (!result.success) {
2644
- throw new Error(result.error ?? "Spec generation failed");
2645
- }
2646
- if (isTrackerMode || isInlineText) {
2647
- const h1Title = extractTitle(result.data.content, filepath);
2648
- const h1Slug = slugify(h1Title, MAX_SLUG_LENGTH);
2649
- const finalFilename = isTrackerMode ? `${id}-${h1Slug}.md` : `${h1Slug}.md`;
2650
- const finalFilepath = join6(outputDir, finalFilename);
2651
- if (finalFilepath !== filepath) {
2652
- await rename2(filepath, finalFilepath);
2653
- filepath = finalFilepath;
2654
- }
2879
+ const itemBody = async () => {
2880
+ let filepath;
2881
+ if (isTrackerMode) {
2882
+ const slug = slugify(details.title, MAX_SLUG_LENGTH);
2883
+ const filename = `${id}-${slug}.md`;
2884
+ filepath = join7(outputDir, filename);
2885
+ } else if (isInlineText) {
2886
+ filepath = id;
2887
+ } else {
2888
+ filepath = id;
2655
2889
  }
2656
- const specDuration = Date.now() - specStart;
2657
- fileDurationsMs[filepath] = specDuration;
2658
- log.success(`Spec written: ${filepath} (${elapsed(specDuration)})`);
2659
- let identifier = filepath;
2890
+ fileLoggerStorage.getStore()?.info(`Output path: ${filepath}`);
2660
2891
  try {
2661
- if (isTrackerMode) {
2662
- await datasource4.update(id, details.title, result.data.content, fetchOpts);
2663
- log.success(`Updated issue #${id} with spec content`);
2664
- await unlink2(filepath);
2665
- log.success(`Deleted local spec ${filepath} (now tracked as issue #${id})`);
2666
- identifier = id;
2667
- issueNumbers.push(id);
2668
- } else if (datasource4.name !== "md") {
2669
- const created = await datasource4.create(details.title, result.data.content, fetchOpts);
2670
- log.success(`Created issue #${created.number} from ${filepath}`);
2671
- await unlink2(filepath);
2672
- log.success(`Deleted local spec ${filepath} (now tracked as issue #${created.number})`);
2673
- identifier = created.number;
2674
- issueNumbers.push(created.number);
2892
+ fileLoggerStorage.getStore()?.info(`Starting spec generation for ${isTrackerMode ? `#${id}` : filepath}`);
2893
+ log.info(`Generating spec for ${isTrackerMode ? `#${id}` : filepath}: ${details.title}...`);
2894
+ const result = await withRetry(
2895
+ () => specAgent.generate({
2896
+ issue: isTrackerMode ? details : void 0,
2897
+ filePath: isTrackerMode ? void 0 : id,
2898
+ fileContent: isTrackerMode ? void 0 : details.body,
2899
+ cwd: specCwd,
2900
+ outputPath: filepath
2901
+ }),
2902
+ retries,
2903
+ { label: `specAgent.generate(${isTrackerMode ? `#${id}` : filepath})` }
2904
+ );
2905
+ if (!result.success) {
2906
+ throw new Error(result.error ?? "Spec generation failed");
2907
+ }
2908
+ fileLoggerStorage.getStore()?.info(`Spec generated successfully`);
2909
+ if (isTrackerMode || isInlineText) {
2910
+ const h1Title = extractTitle(result.data.content, filepath);
2911
+ const h1Slug = slugify(h1Title, MAX_SLUG_LENGTH);
2912
+ const finalFilename = isTrackerMode ? `${id}-${h1Slug}.md` : `${h1Slug}.md`;
2913
+ const finalFilepath = join7(outputDir, finalFilename);
2914
+ if (finalFilepath !== filepath) {
2915
+ await rename2(filepath, finalFilepath);
2916
+ filepath = finalFilepath;
2917
+ }
2675
2918
  }
2919
+ const specDuration = Date.now() - specStart;
2920
+ fileDurationsMs[filepath] = specDuration;
2921
+ log.success(`Spec written: ${filepath} (${elapsed(specDuration)})`);
2922
+ let identifier = filepath;
2923
+ fileLoggerStorage.getStore()?.phase("Datasource sync");
2924
+ try {
2925
+ if (isTrackerMode) {
2926
+ await datasource4.update(id, details.title, result.data.content, fetchOpts);
2927
+ log.success(`Updated issue #${id} with spec content`);
2928
+ await unlink2(filepath);
2929
+ log.success(`Deleted local spec ${filepath} (now tracked as issue #${id})`);
2930
+ identifier = id;
2931
+ issueNumbers.push(id);
2932
+ } else if (datasource4.name !== "md") {
2933
+ const created = await datasource4.create(details.title, result.data.content, fetchOpts);
2934
+ log.success(`Created issue #${created.number} from ${filepath}`);
2935
+ await unlink2(filepath);
2936
+ log.success(`Deleted local spec ${filepath} (now tracked as issue #${created.number})`);
2937
+ identifier = created.number;
2938
+ issueNumbers.push(created.number);
2939
+ }
2940
+ } catch (err) {
2941
+ const label = isTrackerMode ? `issue #${id}` : filepath;
2942
+ log.warn(`Could not sync ${label} to datasource: ${log.formatErrorChain(err)}`);
2943
+ }
2944
+ return { filepath, identifier };
2676
2945
  } catch (err) {
2677
- const label = isTrackerMode ? `issue #${id}` : filepath;
2678
- log.warn(`Could not sync ${label} to datasource: ${log.formatErrorChain(err)}`);
2946
+ fileLoggerStorage.getStore()?.error(`Spec generation failed for ${id}: ${log.extractMessage(err)}${err instanceof Error && err.stack ? `
2947
+ ${err.stack}` : ""}`);
2948
+ log.error(`Failed to generate spec for ${isTrackerMode ? `#${id}` : filepath}: ${log.formatErrorChain(err)}`);
2949
+ log.debug(log.formatErrorChain(err));
2950
+ return null;
2679
2951
  }
2680
- return { filepath, identifier };
2681
- } catch (err) {
2682
- log.error(`Failed to generate spec for ${isTrackerMode ? `#${id}` : filepath}: ${log.formatErrorChain(err)}`);
2683
- log.debug(log.formatErrorChain(err));
2684
- return null;
2952
+ };
2953
+ const fileLogger = log.verbose ? new FileLogger(id, specCwd) : null;
2954
+ if (fileLogger) {
2955
+ return fileLoggerStorage.run(fileLogger, async () => {
2956
+ try {
2957
+ fileLogger.phase(`Spec generation: ${id}`);
2958
+ return await itemBody();
2959
+ } finally {
2960
+ fileLogger.close();
2961
+ }
2962
+ });
2685
2963
  }
2964
+ return itemBody();
2686
2965
  })
2687
2966
  );
2688
2967
  for (const result of batchResults) {
@@ -2736,16 +3015,18 @@ async function runSpecPipeline(opts) {
2736
3015
  model,
2737
3016
  serverUrl,
2738
3017
  cwd: specCwd,
2739
- outputDir = join6(specCwd, ".dispatch", "specs"),
3018
+ outputDir = join7(specCwd, ".dispatch", "specs"),
2740
3019
  org,
2741
3020
  project,
2742
3021
  workItemType,
3022
+ iteration,
3023
+ area,
2743
3024
  concurrency = defaultConcurrency(),
2744
3025
  dryRun,
2745
3026
  retries = 2
2746
3027
  } = opts;
2747
3028
  const pipelineStart = Date.now();
2748
- const resolved = await resolveDatasource(issues, opts.issueSource, specCwd, org, project, workItemType);
3029
+ const resolved = await resolveDatasource(issues, opts.issueSource, specCwd, org, project, workItemType, iteration, area);
2749
3030
  if (!resolved) {
2750
3031
  return { total: 0, generated: 0, failed: 0, files: [], issueNumbers: [], durationMs: Date.now() - pipelineStart, fileDurationsMs: {} };
2751
3032
  }
@@ -2865,7 +3146,9 @@ async function parseTaskFile(filePath) {
2865
3146
  }
2866
3147
  async function markTaskComplete(task) {
2867
3148
  const content = await readFile6(task.file, "utf-8");
2868
- const lines = content.split("\n");
3149
+ const eol = content.includes("\r\n") ? "\r\n" : "\n";
3150
+ const normalized = content.replace(/\r\n/g, "\n");
3151
+ const lines = normalized.split("\n");
2869
3152
  const lineIndex = task.line - 1;
2870
3153
  if (lineIndex < 0 || lineIndex >= lines.length) {
2871
3154
  throw new Error(
@@ -2880,7 +3163,7 @@ async function markTaskComplete(task) {
2880
3163
  );
2881
3164
  }
2882
3165
  lines[lineIndex] = updated;
2883
- await writeFile5(task.file, lines.join("\n"), "utf-8");
3166
+ await writeFile5(task.file, lines.join(eol), "utf-8");
2884
3167
  }
2885
3168
  function groupTasksByMode(tasks) {
2886
3169
  if (tasks.length === 0) return [];
@@ -2910,6 +3193,7 @@ function groupTasksByMode(tasks) {
2910
3193
 
2911
3194
  // src/agents/planner.ts
2912
3195
  init_logger();
3196
+ init_file_logger();
2913
3197
  async function boot6(opts) {
2914
3198
  const { provider, cwd } = opts;
2915
3199
  if (!provider) {
@@ -2922,13 +3206,18 @@ async function boot6(opts) {
2922
3206
  try {
2923
3207
  const sessionId = await provider.createSession();
2924
3208
  const prompt = buildPlannerPrompt(task, cwdOverride ?? cwd, fileContext, worktreeRoot);
3209
+ fileLoggerStorage.getStore()?.prompt("planner", prompt);
2925
3210
  const plan = await provider.prompt(sessionId, prompt);
3211
+ if (plan) fileLoggerStorage.getStore()?.response("planner", plan);
2926
3212
  if (!plan?.trim()) {
2927
3213
  return { data: null, success: false, error: "Planner returned empty plan", durationMs: Date.now() - startTime };
2928
3214
  }
3215
+ fileLoggerStorage.getStore()?.agentEvent("planner", "completed", `${Date.now() - startTime}ms`);
2929
3216
  return { data: { prompt: plan }, success: true, durationMs: Date.now() - startTime };
2930
3217
  } catch (err) {
2931
3218
  const message = log.extractMessage(err);
3219
+ fileLoggerStorage.getStore()?.error(`planner error: ${message}${err instanceof Error && err.stack ? `
3220
+ ${err.stack}` : ""}`);
2932
3221
  return { data: null, success: false, error: message, durationMs: Date.now() - startTime };
2933
3222
  }
2934
3223
  },
@@ -3010,22 +3299,28 @@ function buildPlannerPrompt(task, cwd, fileContext, worktreeRoot) {
3010
3299
 
3011
3300
  // src/dispatcher.ts
3012
3301
  init_logger();
3302
+ init_file_logger();
3013
3303
  async function dispatchTask(instance, task, cwd, plan, worktreeRoot) {
3014
3304
  try {
3015
3305
  log.debug(`Dispatching task: ${task.file}:${task.line} \u2014 ${task.text.slice(0, 80)}`);
3016
3306
  const sessionId = await instance.createSession();
3017
3307
  const prompt = plan ? buildPlannedPrompt(task, cwd, plan, worktreeRoot) : buildPrompt(task, cwd, worktreeRoot);
3018
3308
  log.debug(`Prompt built (${prompt.length} chars, ${plan ? "with plan" : "no plan"})`);
3309
+ fileLoggerStorage.getStore()?.prompt("dispatchTask", prompt);
3019
3310
  const response = await instance.prompt(sessionId, prompt);
3020
3311
  if (response === null) {
3021
3312
  log.debug("Task dispatch returned null response");
3313
+ fileLoggerStorage.getStore()?.warn("dispatchTask: null response");
3022
3314
  return { task, success: false, error: "No response from agent" };
3023
3315
  }
3024
3316
  log.debug(`Task dispatch completed (${response.length} chars response)`);
3317
+ fileLoggerStorage.getStore()?.response("dispatchTask", response);
3025
3318
  return { task, success: true };
3026
3319
  } catch (err) {
3027
3320
  const message = log.extractMessage(err);
3028
3321
  log.debug(`Task dispatch failed: ${log.formatErrorChain(err)}`);
3322
+ fileLoggerStorage.getStore()?.error(`dispatchTask error: ${message}${err instanceof Error && err.stack ? `
3323
+ ${err.stack}` : ""}`);
3029
3324
  return { task, success: false, error: message };
3030
3325
  }
3031
3326
  }
@@ -3092,6 +3387,7 @@ function buildWorktreeIsolation(worktreeRoot) {
3092
3387
 
3093
3388
  // src/agents/executor.ts
3094
3389
  init_logger();
3390
+ init_file_logger();
3095
3391
  async function boot7(opts) {
3096
3392
  const { provider } = opts;
3097
3393
  if (!provider) {
@@ -3099,18 +3395,23 @@ async function boot7(opts) {
3099
3395
  }
3100
3396
  return {
3101
3397
  name: "executor",
3102
- async execute(input2) {
3103
- const { task, cwd, plan, worktreeRoot } = input2;
3398
+ async execute(input3) {
3399
+ const { task, cwd, plan, worktreeRoot } = input3;
3104
3400
  const startTime = Date.now();
3105
3401
  try {
3402
+ fileLoggerStorage.getStore()?.agentEvent("executor", "started", task.text);
3106
3403
  const result = await dispatchTask(provider, task, cwd, plan ?? void 0, worktreeRoot);
3107
3404
  if (result.success) {
3108
3405
  await markTaskComplete(task);
3406
+ fileLoggerStorage.getStore()?.agentEvent("executor", "completed", `${Date.now() - startTime}ms`);
3109
3407
  return { data: { dispatchResult: result }, success: true, durationMs: Date.now() - startTime };
3110
3408
  }
3409
+ fileLoggerStorage.getStore()?.agentEvent("executor", "failed", result.error ?? "unknown error");
3111
3410
  return { data: null, success: false, error: result.error, durationMs: Date.now() - startTime };
3112
3411
  } catch (err) {
3113
3412
  const message = log.extractMessage(err);
3413
+ fileLoggerStorage.getStore()?.error(`executor error: ${message}${err instanceof Error && err.stack ? `
3414
+ ${err.stack}` : ""}`);
3114
3415
  return { data: null, success: false, error: message, durationMs: Date.now() - startTime };
3115
3416
  }
3116
3417
  },
@@ -3121,8 +3422,9 @@ async function boot7(opts) {
3121
3422
 
3122
3423
  // src/agents/commit.ts
3123
3424
  init_logger();
3425
+ init_file_logger();
3124
3426
  import { mkdir as mkdir5, writeFile as writeFile6 } from "fs/promises";
3125
- import { join as join7, resolve as resolve2 } from "path";
3427
+ import { join as join8, resolve as resolve2 } from "path";
3126
3428
  import { randomUUID as randomUUID4 } from "crypto";
3127
3429
  async function boot8(opts) {
3128
3430
  const { provider } = opts;
@@ -3136,14 +3438,16 @@ async function boot8(opts) {
3136
3438
  async generate(genOpts) {
3137
3439
  try {
3138
3440
  const resolvedCwd = resolve2(genOpts.cwd);
3139
- const tmpDir = join7(resolvedCwd, ".dispatch", "tmp");
3441
+ const tmpDir = join8(resolvedCwd, ".dispatch", "tmp");
3140
3442
  await mkdir5(tmpDir, { recursive: true });
3141
3443
  const tmpFilename = `commit-${randomUUID4()}.md`;
3142
- const tmpPath = join7(tmpDir, tmpFilename);
3444
+ const tmpPath = join8(tmpDir, tmpFilename);
3143
3445
  const prompt = buildCommitPrompt(genOpts);
3446
+ fileLoggerStorage.getStore()?.prompt("commit", prompt);
3144
3447
  const sessionId = await provider.createSession();
3145
3448
  log.debug(`Commit prompt built (${prompt.length} chars)`);
3146
3449
  const response = await provider.prompt(sessionId, prompt);
3450
+ if (response) fileLoggerStorage.getStore()?.response("commit", response);
3147
3451
  if (!response?.trim()) {
3148
3452
  return {
3149
3453
  commitMessage: "",
@@ -3167,12 +3471,15 @@ async function boot8(opts) {
3167
3471
  const outputContent = formatOutputFile(parsed);
3168
3472
  await writeFile6(tmpPath, outputContent, "utf-8");
3169
3473
  log.debug(`Wrote commit agent output to ${tmpPath}`);
3474
+ fileLoggerStorage.getStore()?.agentEvent("commit", "completed", `message: ${parsed.commitMessage.slice(0, 80)}`);
3170
3475
  return {
3171
3476
  ...parsed,
3172
3477
  success: true,
3173
3478
  outputPath: tmpPath
3174
3479
  };
3175
3480
  } catch (err) {
3481
+ fileLoggerStorage.getStore()?.error(`commit error: ${log.extractMessage(err)}${err instanceof Error && err.stack ? `
3482
+ ${err.stack}` : ""}`);
3176
3483
  const message = log.extractMessage(err);
3177
3484
  return {
3178
3485
  commitMessage: "",
@@ -3313,7 +3620,7 @@ init_logger();
3313
3620
  init_cleanup();
3314
3621
 
3315
3622
  // src/helpers/worktree.ts
3316
- import { join as join8, basename } from "path";
3623
+ import { join as join9, basename } from "path";
3317
3624
  import { execFile as execFile7 } from "child_process";
3318
3625
  import { promisify as promisify7 } from "util";
3319
3626
  import { randomUUID as randomUUID5 } from "crypto";
@@ -3332,7 +3639,7 @@ function worktreeName(issueFilename) {
3332
3639
  }
3333
3640
  async function createWorktree(repoRoot, issueFilename, branchName, startPoint) {
3334
3641
  const name = worktreeName(issueFilename);
3335
- const worktreePath = join8(repoRoot, WORKTREE_DIR, name);
3642
+ const worktreePath = join9(repoRoot, WORKTREE_DIR, name);
3336
3643
  try {
3337
3644
  const args = ["worktree", "add", worktreePath, "-b", branchName];
3338
3645
  if (startPoint) args.push(startPoint);
@@ -3351,7 +3658,7 @@ async function createWorktree(repoRoot, issueFilename, branchName, startPoint) {
3351
3658
  }
3352
3659
  async function removeWorktree(repoRoot, issueFilename) {
3353
3660
  const name = worktreeName(issueFilename);
3354
- const worktreePath = join8(repoRoot, WORKTREE_DIR, name);
3661
+ const worktreePath = join9(repoRoot, WORKTREE_DIR, name);
3355
3662
  try {
3356
3663
  await git2(["worktree", "remove", worktreePath], repoRoot);
3357
3664
  } catch {
@@ -3586,13 +3893,24 @@ function render(state) {
3586
3893
  return lines.join("\n");
3587
3894
  }
3588
3895
  function draw(state) {
3589
- if (lastLineCount > 0) {
3590
- process.stdout.write(`\x1B[${lastLineCount}A\x1B[0J`);
3591
- }
3592
3896
  const output = render(state);
3593
- process.stdout.write(output);
3594
3897
  const cols = process.stdout.columns || 80;
3595
- lastLineCount = countVisualRows(output, cols);
3898
+ const newLineCount = countVisualRows(output, cols);
3899
+ let buffer = "";
3900
+ if (lastLineCount > 0) {
3901
+ buffer += `\x1B[${lastLineCount}A`;
3902
+ }
3903
+ const lines = output.split("\n");
3904
+ buffer += lines.map((line) => line + "\x1B[K").join("\n");
3905
+ const leftover = lastLineCount - newLineCount;
3906
+ if (leftover > 0) {
3907
+ for (let i = 0; i < leftover; i++) {
3908
+ buffer += "\n\x1B[K";
3909
+ }
3910
+ buffer += `\x1B[${leftover}A`;
3911
+ }
3912
+ process.stdout.write(buffer);
3913
+ lastLineCount = newLineCount;
3596
3914
  }
3597
3915
  function createTui() {
3598
3916
  const state = {
@@ -3622,7 +3940,7 @@ init_providers();
3622
3940
 
3623
3941
  // src/orchestrator/datasource-helpers.ts
3624
3942
  init_logger();
3625
- import { basename as basename2, join as join9 } from "path";
3943
+ import { basename as basename2, join as join10 } from "path";
3626
3944
  import { mkdtemp, writeFile as writeFile7 } from "fs/promises";
3627
3945
  import { tmpdir } from "os";
3628
3946
  import { execFile as execFile8 } from "child_process";
@@ -3650,13 +3968,13 @@ async function fetchItemsById(issueIds, datasource4, fetchOpts) {
3650
3968
  return items;
3651
3969
  }
3652
3970
  async function writeItemsToTempDir(items) {
3653
- const tempDir = await mkdtemp(join9(tmpdir(), "dispatch-"));
3971
+ const tempDir = await mkdtemp(join10(tmpdir(), "dispatch-"));
3654
3972
  const files = [];
3655
3973
  const issueDetailsByFile = /* @__PURE__ */ new Map();
3656
3974
  for (const item of items) {
3657
3975
  const slug = slugify(item.title, MAX_SLUG_LENGTH);
3658
3976
  const filename = `${item.number}-${slug}.md`;
3659
- const filepath = join9(tempDir, filename);
3977
+ const filepath = join10(tempDir, filename);
3660
3978
  await writeFile7(filepath, item.body, "utf-8");
3661
3979
  files.push(filepath);
3662
3980
  issueDetailsByFile.set(filepath, item);
@@ -3669,34 +3987,6 @@ async function writeItemsToTempDir(items) {
3669
3987
  });
3670
3988
  return { files, issueDetailsByFile };
3671
3989
  }
3672
- async function closeCompletedSpecIssues(taskFiles, results, cwd, source, org, project, workItemType) {
3673
- let datasourceName = source;
3674
- if (!datasourceName) {
3675
- datasourceName = await detectDatasource(cwd) ?? void 0;
3676
- }
3677
- if (!datasourceName) return;
3678
- const datasource4 = getDatasource(datasourceName);
3679
- const succeededTasks = new Set(
3680
- results.filter((r) => r.success).map((r) => r.task)
3681
- );
3682
- const fetchOpts = { cwd, org, project, workItemType };
3683
- for (const taskFile of taskFiles) {
3684
- const fileTasks = taskFile.tasks;
3685
- if (fileTasks.length === 0) continue;
3686
- const allSucceeded = fileTasks.every((t) => succeededTasks.has(t));
3687
- if (!allSucceeded) continue;
3688
- const parsed = parseIssueFilename(taskFile.path);
3689
- if (!parsed) continue;
3690
- const { issueId } = parsed;
3691
- const filename = basename2(taskFile.path);
3692
- try {
3693
- await datasource4.close(issueId, fetchOpts);
3694
- log.success(`Closed issue #${issueId} (all tasks in ${filename} completed)`);
3695
- } catch (err) {
3696
- log.warn(`Could not close issue #${issueId}: ${log.formatErrorChain(err)}`);
3697
- }
3698
- }
3699
- }
3700
3990
  async function getCommitSummaries(defaultBranch, cwd) {
3701
3991
  try {
3702
3992
  const { stdout } = await exec8(
@@ -3823,6 +4113,7 @@ function buildFeaturePrBody(issues, tasks, results, datasourceName) {
3823
4113
  // src/orchestrator/dispatch-pipeline.ts
3824
4114
  init_timeout();
3825
4115
  import chalk7 from "chalk";
4116
+ init_file_logger();
3826
4117
  var exec9 = promisify9(execFile9);
3827
4118
  var DEFAULT_PLAN_TIMEOUT_MIN = 10;
3828
4119
  var DEFAULT_PLAN_RETRIES = 1;
@@ -3842,6 +4133,8 @@ async function runDispatchPipeline(opts, cwd) {
3842
4133
  org,
3843
4134
  project,
3844
4135
  workItemType,
4136
+ iteration,
4137
+ area,
3845
4138
  planTimeout,
3846
4139
  planRetries,
3847
4140
  retries
@@ -3850,7 +4143,7 @@ async function runDispatchPipeline(opts, cwd) {
3850
4143
  const maxPlanAttempts = (planRetries ?? retries ?? DEFAULT_PLAN_RETRIES) + 1;
3851
4144
  log.debug(`Plan timeout: ${planTimeout ?? DEFAULT_PLAN_TIMEOUT_MIN}m (${planTimeoutMs}ms), max attempts: ${maxPlanAttempts}`);
3852
4145
  if (dryRun) {
3853
- return dryRunMode(issueIds, cwd, source, org, project, workItemType);
4146
+ return dryRunMode(issueIds, cwd, source, org, project, workItemType, iteration, area);
3854
4147
  }
3855
4148
  const verbose = log.verbose;
3856
4149
  let tui;
@@ -3886,7 +4179,7 @@ async function runDispatchPipeline(opts, cwd) {
3886
4179
  return { total: 0, completed: 0, failed: 0, skipped: 0, results: [] };
3887
4180
  }
3888
4181
  const datasource4 = getDatasource(source);
3889
- const fetchOpts = { cwd, org, project, workItemType };
4182
+ const fetchOpts = { cwd, org, project, workItemType, iteration, area };
3890
4183
  const items = issueIds.length > 0 ? await fetchItemsById(issueIds, datasource4, fetchOpts) : await datasource4.list(fetchOpts);
3891
4184
  if (items.length === 0) {
3892
4185
  tui.state.phase = "done";
@@ -3988,332 +4281,370 @@ async function runDispatchPipeline(opts, cwd) {
3988
4281
  }
3989
4282
  const processIssueFile = async (file, fileTasks) => {
3990
4283
  const details = issueDetailsByFile.get(file);
3991
- let defaultBranch;
3992
- let branchName;
3993
- let worktreePath;
3994
- let issueCwd = cwd;
3995
- if (!noBranch && details) {
3996
- try {
3997
- defaultBranch = feature ? featureBranchName : await datasource4.getDefaultBranch(lifecycleOpts);
3998
- branchName = datasource4.buildBranchName(details.number, details.title, username);
3999
- if (useWorktrees) {
4000
- worktreePath = await createWorktree(cwd, file, branchName, ...feature && featureBranchName ? [featureBranchName] : []);
4001
- registerCleanup(async () => {
4002
- await removeWorktree(cwd, file);
4003
- });
4004
- issueCwd = worktreePath;
4005
- log.debug(`Created worktree for issue #${details.number} at ${worktreePath}`);
4006
- const wtName = worktreeName(file);
4284
+ const fileLogger = verbose && details ? new FileLogger(details.number, cwd) : null;
4285
+ const body = async () => {
4286
+ let defaultBranch;
4287
+ let branchName;
4288
+ let worktreePath;
4289
+ let issueCwd = cwd;
4290
+ if (!noBranch && details) {
4291
+ fileLogger?.phase("Branch/worktree setup");
4292
+ try {
4293
+ defaultBranch = feature ? featureBranchName : await datasource4.getDefaultBranch(lifecycleOpts);
4294
+ branchName = datasource4.buildBranchName(details.number, details.title, username);
4295
+ if (useWorktrees) {
4296
+ worktreePath = await createWorktree(cwd, file, branchName, ...feature && featureBranchName ? [featureBranchName] : []);
4297
+ registerCleanup(async () => {
4298
+ await removeWorktree(cwd, file);
4299
+ });
4300
+ issueCwd = worktreePath;
4301
+ log.debug(`Created worktree for issue #${details.number} at ${worktreePath}`);
4302
+ fileLogger?.info(`Worktree created at ${worktreePath}`);
4303
+ const wtName = worktreeName(file);
4304
+ for (const task of fileTasks) {
4305
+ const tuiTask = tui.state.tasks.find((t) => t.task === task);
4306
+ if (tuiTask) tuiTask.worktree = wtName;
4307
+ }
4308
+ } else if (datasource4.supportsGit()) {
4309
+ await datasource4.createAndSwitchBranch(branchName, lifecycleOpts);
4310
+ log.debug(`Switched to branch ${branchName}`);
4311
+ fileLogger?.info(`Switched to branch ${branchName}`);
4312
+ }
4313
+ } catch (err) {
4314
+ const errorMsg = `Branch creation failed for issue #${details.number}: ${log.extractMessage(err)}`;
4315
+ fileLogger?.error(`Branch creation failed: ${log.extractMessage(err)}${err instanceof Error && err.stack ? `
4316
+ ${err.stack}` : ""}`);
4317
+ log.error(errorMsg);
4007
4318
  for (const task of fileTasks) {
4008
4319
  const tuiTask = tui.state.tasks.find((t) => t.task === task);
4009
- if (tuiTask) tuiTask.worktree = wtName;
4010
- }
4011
- } else if (datasource4.supportsGit()) {
4012
- await datasource4.createAndSwitchBranch(branchName, lifecycleOpts);
4013
- log.debug(`Switched to branch ${branchName}`);
4014
- }
4015
- } catch (err) {
4016
- const errorMsg = `Branch creation failed for issue #${details.number}: ${log.extractMessage(err)}`;
4017
- log.error(errorMsg);
4018
- for (const task of fileTasks) {
4019
- const tuiTask = tui.state.tasks.find((t) => t.task === task);
4020
- if (tuiTask) {
4021
- tuiTask.status = "failed";
4022
- tuiTask.error = errorMsg;
4320
+ if (tuiTask) {
4321
+ tuiTask.status = "failed";
4322
+ tuiTask.error = errorMsg;
4323
+ }
4324
+ results.push({ task, success: false, error: errorMsg });
4023
4325
  }
4024
- results.push({ task, success: false, error: errorMsg });
4326
+ failed += fileTasks.length;
4327
+ return;
4025
4328
  }
4026
- failed += fileTasks.length;
4027
- return;
4028
4329
  }
4029
- }
4030
- const worktreeRoot = useWorktrees ? worktreePath : void 0;
4031
- const issueLifecycleOpts = { cwd: issueCwd };
4032
- let localInstance;
4033
- let localPlanner;
4034
- let localExecutor;
4035
- let localCommitAgent;
4036
- if (useWorktrees) {
4037
- localInstance = await bootProvider(provider, { url: serverUrl, cwd: issueCwd, model });
4038
- registerCleanup(() => localInstance.cleanup());
4039
- if (localInstance.model && !tui.state.model) {
4040
- tui.state.model = localInstance.model;
4330
+ const worktreeRoot = useWorktrees ? worktreePath : void 0;
4331
+ const issueLifecycleOpts = { cwd: issueCwd };
4332
+ fileLogger?.phase("Provider/agent boot");
4333
+ let localInstance;
4334
+ let localPlanner;
4335
+ let localExecutor;
4336
+ let localCommitAgent;
4337
+ if (useWorktrees) {
4338
+ localInstance = await bootProvider(provider, { url: serverUrl, cwd: issueCwd, model });
4339
+ registerCleanup(() => localInstance.cleanup());
4340
+ if (localInstance.model && !tui.state.model) {
4341
+ tui.state.model = localInstance.model;
4342
+ }
4343
+ if (verbose && localInstance.model) log.debug(`Model: ${localInstance.model}`);
4344
+ localPlanner = noPlan ? null : await boot6({ provider: localInstance, cwd: issueCwd });
4345
+ localExecutor = await boot7({ provider: localInstance, cwd: issueCwd });
4346
+ localCommitAgent = await boot8({ provider: localInstance, cwd: issueCwd });
4347
+ fileLogger?.info(`Provider booted: ${localInstance.model ?? provider}`);
4348
+ } else {
4349
+ localInstance = instance;
4350
+ localPlanner = planner;
4351
+ localExecutor = executor;
4352
+ localCommitAgent = commitAgent;
4041
4353
  }
4042
- if (verbose && localInstance.model) log.debug(`Model: ${localInstance.model}`);
4043
- localPlanner = noPlan ? null : await boot6({ provider: localInstance, cwd: issueCwd });
4044
- localExecutor = await boot7({ provider: localInstance, cwd: issueCwd });
4045
- localCommitAgent = await boot8({ provider: localInstance, cwd: issueCwd });
4046
- } else {
4047
- localInstance = instance;
4048
- localPlanner = planner;
4049
- localExecutor = executor;
4050
- localCommitAgent = commitAgent;
4051
- }
4052
- const groups = groupTasksByMode(fileTasks);
4053
- const issueResults = [];
4054
- for (const group of groups) {
4055
- const groupQueue = [...group];
4056
- while (groupQueue.length > 0) {
4057
- const batch = groupQueue.splice(0, concurrency);
4058
- const batchResults = await Promise.all(
4059
- batch.map(async (task) => {
4060
- const tuiTask = tui.state.tasks.find((t) => t.task === task);
4061
- const startTime = Date.now();
4062
- tuiTask.elapsed = startTime;
4063
- let plan;
4064
- if (localPlanner) {
4065
- tuiTask.status = "planning";
4066
- if (verbose) log.info(`Task #${tui.state.tasks.indexOf(tuiTask) + 1}: planning \u2014 "${task.text}"`);
4067
- const rawContent = fileContentMap.get(task.file);
4068
- const fileContext = rawContent ? buildTaskContext(rawContent, task) : void 0;
4069
- let planResult;
4070
- for (let attempt = 1; attempt <= maxPlanAttempts; attempt++) {
4071
- try {
4072
- planResult = await withTimeout(
4073
- localPlanner.plan(task, fileContext, issueCwd, worktreeRoot),
4074
- planTimeoutMs,
4075
- "planner.plan()"
4076
- );
4077
- break;
4078
- } catch (err) {
4079
- if (err instanceof TimeoutError) {
4080
- log.warn(
4081
- `Planning timed out for task "${task.text}" (attempt ${attempt}/${maxPlanAttempts})`
4354
+ const groups = groupTasksByMode(fileTasks);
4355
+ const issueResults = [];
4356
+ for (const group of groups) {
4357
+ const groupQueue = [...group];
4358
+ while (groupQueue.length > 0) {
4359
+ const batch = groupQueue.splice(0, concurrency);
4360
+ const batchResults = await Promise.all(
4361
+ batch.map(async (task) => {
4362
+ const tuiTask = tui.state.tasks.find((t) => t.task === task);
4363
+ const startTime = Date.now();
4364
+ tuiTask.elapsed = startTime;
4365
+ let plan;
4366
+ if (localPlanner) {
4367
+ tuiTask.status = "planning";
4368
+ fileLogger?.phase(`Planning task: ${task.text}`);
4369
+ if (verbose) log.info(`Task #${tui.state.tasks.indexOf(tuiTask) + 1}: planning \u2014 "${task.text}"`);
4370
+ const rawContent = fileContentMap.get(task.file);
4371
+ const fileContext = rawContent ? buildTaskContext(rawContent, task) : void 0;
4372
+ let planResult;
4373
+ for (let attempt = 1; attempt <= maxPlanAttempts; attempt++) {
4374
+ try {
4375
+ planResult = await withTimeout(
4376
+ localPlanner.plan(task, fileContext, issueCwd, worktreeRoot),
4377
+ planTimeoutMs,
4378
+ "planner.plan()"
4082
4379
  );
4083
- if (attempt < maxPlanAttempts) {
4084
- log.debug(`Retrying planning (attempt ${attempt + 1}/${maxPlanAttempts})`);
4085
- }
4086
- } else {
4087
- planResult = {
4088
- data: null,
4089
- success: false,
4090
- error: log.extractMessage(err),
4091
- durationMs: 0
4092
- };
4093
4380
  break;
4381
+ } catch (err) {
4382
+ if (err instanceof TimeoutError) {
4383
+ log.warn(
4384
+ `Planning timed out for task "${task.text}" (attempt ${attempt}/${maxPlanAttempts})`
4385
+ );
4386
+ fileLogger?.warn(`Planning timeout (attempt ${attempt}/${maxPlanAttempts})`);
4387
+ if (attempt < maxPlanAttempts) {
4388
+ log.debug(`Retrying planning (attempt ${attempt + 1}/${maxPlanAttempts})`);
4389
+ fileLogger?.info(`Retrying planning (attempt ${attempt + 1}/${maxPlanAttempts})`);
4390
+ }
4391
+ } else {
4392
+ planResult = {
4393
+ data: null,
4394
+ success: false,
4395
+ error: log.extractMessage(err),
4396
+ durationMs: 0
4397
+ };
4398
+ break;
4399
+ }
4094
4400
  }
4095
4401
  }
4402
+ if (!planResult) {
4403
+ const timeoutMin = planTimeout ?? 10;
4404
+ planResult = {
4405
+ data: null,
4406
+ success: false,
4407
+ error: `Planning timed out after ${timeoutMin}m (${maxPlanAttempts} attempts)`,
4408
+ durationMs: 0
4409
+ };
4410
+ }
4411
+ if (!planResult.success) {
4412
+ tuiTask.status = "failed";
4413
+ tuiTask.error = `Planning failed: ${planResult.error}`;
4414
+ fileLogger?.error(`Planning failed: ${planResult.error}`);
4415
+ tuiTask.elapsed = Date.now() - startTime;
4416
+ if (verbose) log.error(`Task #${tui.state.tasks.indexOf(tuiTask) + 1}: failed \u2014 ${tuiTask.error} (${elapsed(tuiTask.elapsed)})`);
4417
+ failed++;
4418
+ return { task, success: false, error: tuiTask.error };
4419
+ }
4420
+ plan = planResult.data.prompt;
4421
+ fileLogger?.info(`Planning completed (${planResult.durationMs ?? 0}ms)`);
4096
4422
  }
4097
- if (!planResult) {
4098
- const timeoutMin = planTimeout ?? 10;
4099
- planResult = {
4100
- data: null,
4101
- success: false,
4102
- error: `Planning timed out after ${timeoutMin}m (${maxPlanAttempts} attempts)`,
4103
- durationMs: 0
4104
- };
4105
- }
4106
- if (!planResult.success) {
4423
+ tuiTask.status = "running";
4424
+ fileLogger?.phase(`Executing task: ${task.text}`);
4425
+ if (verbose) log.info(`Task #${tui.state.tasks.indexOf(tuiTask) + 1}: executing \u2014 "${task.text}"`);
4426
+ const execRetries = 2;
4427
+ const execResult = await withRetry(
4428
+ async () => {
4429
+ const result = await localExecutor.execute({
4430
+ task,
4431
+ cwd: issueCwd,
4432
+ plan: plan ?? null,
4433
+ worktreeRoot
4434
+ });
4435
+ if (!result.success) {
4436
+ throw new Error(result.error ?? "Execution failed");
4437
+ }
4438
+ return result;
4439
+ },
4440
+ execRetries,
4441
+ { label: `executor "${task.text}"` }
4442
+ ).catch((err) => ({
4443
+ data: null,
4444
+ success: false,
4445
+ error: log.extractMessage(err),
4446
+ durationMs: 0
4447
+ }));
4448
+ if (execResult.success) {
4449
+ fileLogger?.info(`Execution completed successfully (${Date.now() - startTime}ms)`);
4450
+ try {
4451
+ const parsed = parseIssueFilename(task.file);
4452
+ if (parsed) {
4453
+ const updatedContent = await readFile7(task.file, "utf-8");
4454
+ const issueDetails = issueDetailsByFile.get(task.file);
4455
+ const title = issueDetails?.title ?? parsed.slug;
4456
+ await datasource4.update(parsed.issueId, title, updatedContent, fetchOpts);
4457
+ log.success(`Synced task completion to issue #${parsed.issueId}`);
4458
+ }
4459
+ } catch (err) {
4460
+ log.warn(`Could not sync task completion to datasource: ${log.formatErrorChain(err)}`);
4461
+ }
4462
+ tuiTask.status = "done";
4463
+ tuiTask.elapsed = Date.now() - startTime;
4464
+ if (verbose) log.success(`Task #${tui.state.tasks.indexOf(tuiTask) + 1}: done \u2014 "${task.text}" (${elapsed(tuiTask.elapsed)})`);
4465
+ completed++;
4466
+ } else {
4467
+ fileLogger?.error(`Execution failed: ${execResult.error}`);
4107
4468
  tuiTask.status = "failed";
4108
- tuiTask.error = `Planning failed: ${planResult.error}`;
4469
+ tuiTask.error = execResult.error;
4109
4470
  tuiTask.elapsed = Date.now() - startTime;
4110
- if (verbose) log.error(`Task #${tui.state.tasks.indexOf(tuiTask) + 1}: failed \u2014 ${tuiTask.error} (${elapsed(tuiTask.elapsed)})`);
4471
+ if (verbose) log.error(`Task #${tui.state.tasks.indexOf(tuiTask) + 1}: failed \u2014 "${task.text}" (${elapsed(tuiTask.elapsed)})${tuiTask.error ? `: ${tuiTask.error}` : ""}`);
4111
4472
  failed++;
4112
- return { task, success: false, error: tuiTask.error };
4113
4473
  }
4114
- plan = planResult.data.prompt;
4115
- }
4116
- tuiTask.status = "running";
4117
- if (verbose) log.info(`Task #${tui.state.tasks.indexOf(tuiTask) + 1}: executing \u2014 "${task.text}"`);
4118
- const execRetries = 2;
4119
- const execResult = await withRetry(
4120
- async () => {
4121
- const result = await localExecutor.execute({
4122
- task,
4123
- cwd: issueCwd,
4124
- plan: plan ?? null,
4125
- worktreeRoot
4126
- });
4127
- if (!result.success) {
4128
- throw new Error(result.error ?? "Execution failed");
4129
- }
4130
- return result;
4131
- },
4132
- execRetries,
4133
- { label: `executor "${task.text}"` }
4134
- ).catch((err) => ({
4135
- data: null,
4136
- success: false,
4137
- error: log.extractMessage(err),
4138
- durationMs: 0
4139
- }));
4140
- if (execResult.success) {
4474
+ const dispatchResult = execResult.success ? execResult.data.dispatchResult : {
4475
+ task,
4476
+ success: false,
4477
+ error: execResult.error ?? "Executor failed without returning a dispatch result."
4478
+ };
4479
+ return dispatchResult;
4480
+ })
4481
+ );
4482
+ issueResults.push(...batchResults);
4483
+ if (!tui.state.model && localInstance.model) {
4484
+ tui.state.model = localInstance.model;
4485
+ }
4486
+ }
4487
+ }
4488
+ results.push(...issueResults);
4489
+ if (!noBranch && branchName && defaultBranch && details && datasource4.supportsGit()) {
4490
+ try {
4491
+ await datasource4.commitAllChanges(
4492
+ `chore: stage uncommitted changes for issue #${details.number}`,
4493
+ issueLifecycleOpts
4494
+ );
4495
+ log.debug(`Staged uncommitted changes for issue #${details.number}`);
4496
+ } catch (err) {
4497
+ log.warn(`Could not commit uncommitted changes for issue #${details.number}: ${log.formatErrorChain(err)}`);
4498
+ }
4499
+ }
4500
+ fileLogger?.phase("Commit generation");
4501
+ let commitAgentResult;
4502
+ if (!noBranch && branchName && defaultBranch && details && datasource4.supportsGit()) {
4503
+ try {
4504
+ const branchDiff = await getBranchDiff(defaultBranch, issueCwd);
4505
+ if (branchDiff) {
4506
+ const result = await localCommitAgent.generate({
4507
+ branchDiff,
4508
+ issue: details,
4509
+ taskResults: issueResults,
4510
+ cwd: issueCwd,
4511
+ worktreeRoot
4512
+ });
4513
+ if (result.success) {
4514
+ commitAgentResult = result;
4515
+ fileLogger?.info(`Commit message generated for issue #${details.number}`);
4141
4516
  try {
4142
- const parsed = parseIssueFilename(task.file);
4143
- if (parsed) {
4144
- const updatedContent = await readFile7(task.file, "utf-8");
4145
- const issueDetails = issueDetailsByFile.get(task.file);
4146
- const title = issueDetails?.title ?? parsed.slug;
4147
- await datasource4.update(parsed.issueId, title, updatedContent, fetchOpts);
4148
- log.success(`Synced task completion to issue #${parsed.issueId}`);
4149
- }
4517
+ await squashBranchCommits(defaultBranch, result.commitMessage, issueCwd);
4518
+ log.debug(`Rewrote commit message for issue #${details.number}`);
4519
+ fileLogger?.info(`Rewrote commit history for issue #${details.number}`);
4150
4520
  } catch (err) {
4151
- log.warn(`Could not sync task completion to datasource: ${log.formatErrorChain(err)}`);
4521
+ log.warn(`Could not rewrite commit message for issue #${details.number}: ${log.formatErrorChain(err)}`);
4152
4522
  }
4153
- tuiTask.status = "done";
4154
- tuiTask.elapsed = Date.now() - startTime;
4155
- if (verbose) log.success(`Task #${tui.state.tasks.indexOf(tuiTask) + 1}: done \u2014 "${task.text}" (${elapsed(tuiTask.elapsed)})`);
4156
- completed++;
4157
4523
  } else {
4158
- tuiTask.status = "failed";
4159
- tuiTask.error = execResult.error;
4160
- tuiTask.elapsed = Date.now() - startTime;
4161
- if (verbose) log.error(`Task #${tui.state.tasks.indexOf(tuiTask) + 1}: failed \u2014 "${task.text}" (${elapsed(tuiTask.elapsed)})${tuiTask.error ? `: ${tuiTask.error}` : ""}`);
4162
- failed++;
4524
+ log.warn(`Commit agent failed for issue #${details.number}: ${result.error}`);
4525
+ fileLogger?.warn(`Commit agent failed: ${result.error}`);
4163
4526
  }
4164
- const dispatchResult = execResult.success ? execResult.data.dispatchResult : {
4165
- task,
4166
- success: false,
4167
- error: execResult.error ?? "Executor failed without returning a dispatch result."
4168
- };
4169
- return dispatchResult;
4170
- })
4171
- );
4172
- issueResults.push(...batchResults);
4173
- if (!tui.state.model && localInstance.model) {
4174
- tui.state.model = localInstance.model;
4527
+ }
4528
+ } catch (err) {
4529
+ log.warn(`Commit agent error for issue #${details.number}: ${log.formatErrorChain(err)}`);
4175
4530
  }
4176
4531
  }
4177
- }
4178
- results.push(...issueResults);
4179
- if (!noBranch && branchName && defaultBranch && details && datasource4.supportsGit()) {
4180
- try {
4181
- await datasource4.commitAllChanges(
4182
- `chore: stage uncommitted changes for issue #${details.number}`,
4183
- issueLifecycleOpts
4184
- );
4185
- log.debug(`Staged uncommitted changes for issue #${details.number}`);
4186
- } catch (err) {
4187
- log.warn(`Could not commit uncommitted changes for issue #${details.number}: ${log.formatErrorChain(err)}`);
4188
- }
4189
- }
4190
- let commitAgentResult;
4191
- if (!noBranch && branchName && defaultBranch && details && datasource4.supportsGit()) {
4192
- try {
4193
- const branchDiff = await getBranchDiff(defaultBranch, issueCwd);
4194
- if (branchDiff) {
4195
- const result = await localCommitAgent.generate({
4196
- branchDiff,
4197
- issue: details,
4198
- taskResults: issueResults,
4199
- cwd: issueCwd,
4200
- worktreeRoot
4201
- });
4202
- if (result.success) {
4203
- commitAgentResult = result;
4532
+ fileLogger?.phase("PR lifecycle");
4533
+ if (!noBranch && branchName && defaultBranch && details) {
4534
+ if (feature && featureBranchName) {
4535
+ if (worktreePath) {
4204
4536
  try {
4205
- await squashBranchCommits(defaultBranch, result.commitMessage, issueCwd);
4206
- log.debug(`Rewrote commit message for issue #${details.number}`);
4537
+ await removeWorktree(cwd, file);
4207
4538
  } catch (err) {
4208
- log.warn(`Could not rewrite commit message for issue #${details.number}: ${log.formatErrorChain(err)}`);
4539
+ log.warn(`Could not remove worktree for issue #${details.number}: ${log.formatErrorChain(err)}`);
4209
4540
  }
4210
- } else {
4211
- log.warn(`Commit agent failed for issue #${details.number}: ${result.error}`);
4212
4541
  }
4213
- }
4214
- } catch (err) {
4215
- log.warn(`Commit agent error for issue #${details.number}: ${log.formatErrorChain(err)}`);
4216
- }
4217
- }
4218
- if (!noBranch && branchName && defaultBranch && details) {
4219
- if (feature && featureBranchName) {
4220
- if (worktreePath) {
4221
4542
  try {
4222
- await removeWorktree(cwd, file);
4543
+ await datasource4.switchBranch(featureBranchName, lifecycleOpts);
4544
+ await exec9("git", ["merge", branchName, "--no-ff", "-m", `merge: issue #${details.number}`], { cwd });
4545
+ log.debug(`Merged ${branchName} into ${featureBranchName}`);
4223
4546
  } catch (err) {
4224
- log.warn(`Could not remove worktree for issue #${details.number}: ${log.formatErrorChain(err)}`);
4225
- }
4226
- }
4227
- try {
4228
- await datasource4.switchBranch(featureBranchName, lifecycleOpts);
4229
- await exec9("git", ["merge", branchName, "--no-ff", "-m", `merge: issue #${details.number}`], { cwd });
4230
- log.debug(`Merged ${branchName} into ${featureBranchName}`);
4231
- } catch (err) {
4232
- const mergeError = `Could not merge ${branchName} into feature branch: ${log.formatErrorChain(err)}`;
4233
- log.warn(mergeError);
4234
- try {
4235
- await exec9("git", ["merge", "--abort"], { cwd });
4236
- } catch {
4237
- }
4238
- for (const task of fileTasks) {
4239
- const tuiTask = tui.state.tasks.find((t) => t.task === task);
4240
- if (tuiTask) {
4241
- tuiTask.status = "failed";
4242
- tuiTask.error = mergeError;
4547
+ const mergeError = `Could not merge ${branchName} into feature branch: ${log.formatErrorChain(err)}`;
4548
+ log.warn(mergeError);
4549
+ try {
4550
+ await exec9("git", ["merge", "--abort"], { cwd });
4551
+ } catch {
4243
4552
  }
4244
- const existingResult = results.find((r) => r.task === task);
4245
- if (existingResult) {
4246
- existingResult.success = false;
4247
- existingResult.error = mergeError;
4553
+ for (const task of fileTasks) {
4554
+ const tuiTask = tui.state.tasks.find((t) => t.task === task);
4555
+ if (tuiTask) {
4556
+ tuiTask.status = "failed";
4557
+ tuiTask.error = mergeError;
4558
+ }
4559
+ const existingResult = results.find((r) => r.task === task);
4560
+ if (existingResult) {
4561
+ existingResult.success = false;
4562
+ existingResult.error = mergeError;
4563
+ }
4248
4564
  }
4565
+ return;
4249
4566
  }
4250
- return;
4251
- }
4252
- try {
4253
- await exec9("git", ["branch", "-d", branchName], { cwd });
4254
- log.debug(`Deleted local branch ${branchName}`);
4255
- } catch (err) {
4256
- log.warn(`Could not delete local branch ${branchName}: ${log.formatErrorChain(err)}`);
4257
- }
4258
- try {
4259
- await datasource4.switchBranch(featureDefaultBranch, lifecycleOpts);
4260
- } catch (err) {
4261
- log.warn(`Could not switch back to ${featureDefaultBranch}: ${log.formatErrorChain(err)}`);
4262
- }
4263
- } else {
4264
- if (datasource4.supportsGit()) {
4265
4567
  try {
4266
- await datasource4.pushBranch(branchName, issueLifecycleOpts);
4267
- log.debug(`Pushed branch ${branchName}`);
4568
+ await exec9("git", ["branch", "-d", branchName], { cwd });
4569
+ log.debug(`Deleted local branch ${branchName}`);
4268
4570
  } catch (err) {
4269
- log.warn(`Could not push branch ${branchName}: ${log.formatErrorChain(err)}`);
4571
+ log.warn(`Could not delete local branch ${branchName}: ${log.formatErrorChain(err)}`);
4270
4572
  }
4271
- }
4272
- if (datasource4.supportsGit()) {
4273
4573
  try {
4274
- const prTitle = commitAgentResult?.prTitle || await buildPrTitle(details.title, defaultBranch, issueLifecycleOpts.cwd);
4275
- const prBody = commitAgentResult?.prDescription || await buildPrBody(
4276
- details,
4277
- fileTasks,
4278
- issueResults,
4279
- defaultBranch,
4280
- datasource4.name,
4281
- issueLifecycleOpts.cwd
4282
- );
4283
- const prUrl = await datasource4.createPullRequest(
4284
- branchName,
4285
- details.number,
4286
- prTitle,
4287
- prBody,
4288
- issueLifecycleOpts
4289
- );
4290
- if (prUrl) {
4291
- log.success(`Created PR for issue #${details.number}: ${prUrl}`);
4292
- }
4574
+ await datasource4.switchBranch(featureDefaultBranch, lifecycleOpts);
4293
4575
  } catch (err) {
4294
- log.warn(`Could not create PR for issue #${details.number}: ${log.formatErrorChain(err)}`);
4576
+ log.warn(`Could not switch back to ${featureDefaultBranch}: ${log.formatErrorChain(err)}`);
4295
4577
  }
4296
- }
4297
- if (useWorktrees && worktreePath) {
4298
- try {
4299
- await removeWorktree(cwd, file);
4300
- } catch (err) {
4301
- log.warn(`Could not remove worktree for issue #${details.number}: ${log.formatErrorChain(err)}`);
4578
+ } else {
4579
+ if (datasource4.supportsGit()) {
4580
+ try {
4581
+ await datasource4.pushBranch(branchName, issueLifecycleOpts);
4582
+ log.debug(`Pushed branch ${branchName}`);
4583
+ fileLogger?.info(`Pushed branch ${branchName}`);
4584
+ } catch (err) {
4585
+ log.warn(`Could not push branch ${branchName}: ${log.formatErrorChain(err)}`);
4586
+ }
4302
4587
  }
4303
- } else if (!useWorktrees && datasource4.supportsGit()) {
4304
- try {
4305
- await datasource4.switchBranch(defaultBranch, lifecycleOpts);
4306
- log.debug(`Switched back to ${defaultBranch}`);
4307
- } catch (err) {
4308
- log.warn(`Could not switch back to ${defaultBranch}: ${log.formatErrorChain(err)}`);
4588
+ if (datasource4.supportsGit()) {
4589
+ try {
4590
+ const prTitle = commitAgentResult?.prTitle || await buildPrTitle(details.title, defaultBranch, issueLifecycleOpts.cwd);
4591
+ const prBody = commitAgentResult?.prDescription || await buildPrBody(
4592
+ details,
4593
+ fileTasks,
4594
+ issueResults,
4595
+ defaultBranch,
4596
+ datasource4.name,
4597
+ issueLifecycleOpts.cwd
4598
+ );
4599
+ const prUrl = await datasource4.createPullRequest(
4600
+ branchName,
4601
+ details.number,
4602
+ prTitle,
4603
+ prBody,
4604
+ issueLifecycleOpts
4605
+ );
4606
+ if (prUrl) {
4607
+ log.success(`Created PR for issue #${details.number}: ${prUrl}`);
4608
+ fileLogger?.info(`Created PR: ${prUrl}`);
4609
+ }
4610
+ } catch (err) {
4611
+ log.warn(`Could not create PR for issue #${details.number}: ${log.formatErrorChain(err)}`);
4612
+ fileLogger?.warn(`PR creation failed: ${log.extractMessage(err)}`);
4613
+ }
4614
+ }
4615
+ if (useWorktrees && worktreePath) {
4616
+ try {
4617
+ await removeWorktree(cwd, file);
4618
+ } catch (err) {
4619
+ log.warn(`Could not remove worktree for issue #${details.number}: ${log.formatErrorChain(err)}`);
4620
+ }
4621
+ } else if (!useWorktrees && datasource4.supportsGit()) {
4622
+ try {
4623
+ await datasource4.switchBranch(defaultBranch, lifecycleOpts);
4624
+ log.debug(`Switched back to ${defaultBranch}`);
4625
+ } catch (err) {
4626
+ log.warn(`Could not switch back to ${defaultBranch}: ${log.formatErrorChain(err)}`);
4627
+ }
4309
4628
  }
4310
4629
  }
4311
4630
  }
4312
- }
4313
- if (useWorktrees) {
4314
- await localExecutor.cleanup();
4315
- await localPlanner?.cleanup();
4316
- await localInstance.cleanup();
4631
+ fileLogger?.phase("Resource cleanup");
4632
+ if (useWorktrees) {
4633
+ await localExecutor.cleanup();
4634
+ await localPlanner?.cleanup();
4635
+ await localInstance.cleanup();
4636
+ }
4637
+ };
4638
+ if (fileLogger) {
4639
+ await fileLoggerStorage.run(fileLogger, async () => {
4640
+ try {
4641
+ await body();
4642
+ } finally {
4643
+ fileLogger.close();
4644
+ }
4645
+ });
4646
+ } else {
4647
+ await body();
4317
4648
  }
4318
4649
  };
4319
4650
  if (useWorktrees && !feature) {
@@ -4364,11 +4695,6 @@ async function runDispatchPipeline(opts, cwd) {
4364
4695
  log.warn(`Could not switch back to ${featureDefaultBranch}: ${log.formatErrorChain(err)}`);
4365
4696
  }
4366
4697
  }
4367
- try {
4368
- await closeCompletedSpecIssues(taskFiles, results, cwd, source, org, project, workItemType);
4369
- } catch (err) {
4370
- log.warn(`Could not close completed spec issues: ${log.formatErrorChain(err)}`);
4371
- }
4372
4698
  await commitAgent?.cleanup();
4373
4699
  await executor?.cleanup();
4374
4700
  await planner?.cleanup();
@@ -4382,13 +4708,13 @@ async function runDispatchPipeline(opts, cwd) {
4382
4708
  throw err;
4383
4709
  }
4384
4710
  }
4385
- async function dryRunMode(issueIds, cwd, source, org, project, workItemType) {
4711
+ async function dryRunMode(issueIds, cwd, source, org, project, workItemType, iteration, area) {
4386
4712
  if (!source) {
4387
4713
  log.error("No datasource configured. Use --source or run 'dispatch config' to set up defaults.");
4388
4714
  return { total: 0, completed: 0, failed: 0, skipped: 0, results: [] };
4389
4715
  }
4390
4716
  const datasource4 = getDatasource(source);
4391
- const fetchOpts = { cwd, org, project, workItemType };
4717
+ const fetchOpts = { cwd, org, project, workItemType, iteration, area };
4392
4718
  const lifecycleOpts = { cwd };
4393
4719
  let username = "";
4394
4720
  try {
@@ -4493,6 +4819,8 @@ async function boot9(opts) {
4493
4819
  org: m.org,
4494
4820
  project: m.project,
4495
4821
  workItemType: m.workItemType,
4822
+ iteration: m.iteration,
4823
+ area: m.area,
4496
4824
  concurrency: m.concurrency,
4497
4825
  dryRun: m.dryRun
4498
4826
  });
@@ -4507,7 +4835,7 @@ async function boot9(opts) {
4507
4835
  process.exit(1);
4508
4836
  }
4509
4837
  const datasource4 = getDatasource(source);
4510
- const existing = await datasource4.list({ cwd: m.cwd, org: m.org, project: m.project, workItemType: m.workItemType });
4838
+ const existing = await datasource4.list({ cwd: m.cwd, org: m.org, project: m.project, workItemType: m.workItemType, iteration: m.iteration, area: m.area });
4511
4839
  if (existing.length === 0) {
4512
4840
  log.error("No existing specs found to regenerate");
4513
4841
  process.exit(1);
@@ -4533,6 +4861,8 @@ async function boot9(opts) {
4533
4861
  org: m.org,
4534
4862
  project: m.project,
4535
4863
  workItemType: m.workItemType,
4864
+ iteration: m.iteration,
4865
+ area: m.area,
4536
4866
  concurrency: m.concurrency,
4537
4867
  dryRun: m.dryRun
4538
4868
  });
@@ -4551,6 +4881,8 @@ async function boot9(opts) {
4551
4881
  org: m.org,
4552
4882
  project: m.project,
4553
4883
  workItemType: m.workItemType,
4884
+ iteration: m.iteration,
4885
+ area: m.area,
4554
4886
  planTimeout: m.planTimeout,
4555
4887
  planRetries: m.planRetries,
4556
4888
  retries: m.retries,
@@ -4634,187 +4966,156 @@ var HELP = `
4634
4966
  dispatch config
4635
4967
  `.trimStart();
4636
4968
  function parseArgs(argv) {
4969
+ const program = new Command();
4970
+ program.exitOverride().configureOutput({
4971
+ writeOut: () => {
4972
+ },
4973
+ writeErr: () => {
4974
+ }
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(
4976
+ new Option("--provider <name>", "Agent backend").choices(PROVIDER_NAMES)
4977
+ ).addOption(
4978
+ new Option("--source <name>", "Issue source").choices(
4979
+ DATASOURCE_NAMES
4980
+ )
4981
+ ).option(
4982
+ "--concurrency <n>",
4983
+ "Max parallel dispatches",
4984
+ (val) => {
4985
+ const n = parseInt(val, 10);
4986
+ if (isNaN(n) || n < 1) throw new CommanderError(1, "commander.invalidArgument", "--concurrency must be a positive integer");
4987
+ if (n > MAX_CONCURRENCY) throw new CommanderError(1, "commander.invalidArgument", `--concurrency must not exceed ${MAX_CONCURRENCY}`);
4988
+ return n;
4989
+ }
4990
+ ).option(
4991
+ "--plan-timeout <min>",
4992
+ "Planning timeout in minutes",
4993
+ (val) => {
4994
+ const n = parseFloat(val);
4995
+ if (isNaN(n) || n < CONFIG_BOUNDS.planTimeout.min) throw new CommanderError(1, "commander.invalidArgument", "--plan-timeout must be a positive number (minutes)");
4996
+ if (n > CONFIG_BOUNDS.planTimeout.max) throw new CommanderError(1, "commander.invalidArgument", `--plan-timeout must not exceed ${CONFIG_BOUNDS.planTimeout.max}`);
4997
+ return n;
4998
+ }
4999
+ ).option(
5000
+ "--retries <n>",
5001
+ "Retry attempts",
5002
+ (val) => {
5003
+ const n = parseInt(val, 10);
5004
+ if (isNaN(n) || n < 0) throw new CommanderError(1, "commander.invalidArgument", "--retries must be a non-negative integer");
5005
+ return n;
5006
+ }
5007
+ ).option(
5008
+ "--plan-retries <n>",
5009
+ "Planner retry attempts",
5010
+ (val) => {
5011
+ const n = parseInt(val, 10);
5012
+ if (isNaN(n) || n < 0) throw new CommanderError(1, "commander.invalidArgument", "--plan-retries must be a non-negative integer");
5013
+ return n;
5014
+ }
5015
+ ).option(
5016
+ "--test-timeout <min>",
5017
+ "Test timeout in minutes",
5018
+ (val) => {
5019
+ const n = parseFloat(val);
5020
+ if (isNaN(n) || n <= 0) throw new CommanderError(1, "commander.invalidArgument", "--test-timeout must be a positive number (minutes)");
5021
+ return n;
5022
+ }
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");
5024
+ try {
5025
+ program.parse(argv, { from: "user" });
5026
+ } catch (err) {
5027
+ if (err instanceof CommanderError) {
5028
+ log.error(err.message);
5029
+ process.exit(1);
5030
+ }
5031
+ throw err;
5032
+ }
5033
+ const opts = program.opts();
4637
5034
  const args = {
4638
- issueIds: [],
4639
- dryRun: false,
4640
- noPlan: false,
4641
- noBranch: false,
4642
- noWorktree: false,
4643
- force: false,
4644
- provider: "opencode",
4645
- cwd: process.cwd(),
4646
- help: false,
4647
- version: false,
4648
- verbose: false
5035
+ issueIds: program.args,
5036
+ dryRun: opts.dryRun ?? false,
5037
+ noPlan: !opts.plan,
5038
+ noBranch: !opts.branch,
5039
+ noWorktree: !opts.worktree,
5040
+ force: opts.force ?? false,
5041
+ provider: opts.provider ?? "opencode",
5042
+ cwd: opts.cwd ?? process.cwd(),
5043
+ help: opts.help ?? false,
5044
+ version: opts.version ?? false,
5045
+ verbose: opts.verbose ?? false
4649
5046
  };
4650
- const explicitFlags = /* @__PURE__ */ new Set();
4651
- let i = 0;
4652
- while (i < argv.length) {
4653
- const arg = argv[i];
4654
- if (arg === "--help" || arg === "-h") {
4655
- args.help = true;
4656
- explicitFlags.add("help");
4657
- } else if (arg === "--version" || arg === "-v") {
4658
- args.version = true;
4659
- explicitFlags.add("version");
4660
- } else if (arg === "--dry-run") {
4661
- args.dryRun = true;
4662
- explicitFlags.add("dryRun");
4663
- } else if (arg === "--no-plan") {
4664
- args.noPlan = true;
4665
- explicitFlags.add("noPlan");
4666
- } else if (arg === "--no-branch") {
4667
- args.noBranch = true;
4668
- explicitFlags.add("noBranch");
4669
- } else if (arg === "--no-worktree") {
4670
- args.noWorktree = true;
4671
- explicitFlags.add("noWorktree");
4672
- } else if (arg === "--feature") {
4673
- args.feature = true;
4674
- explicitFlags.add("feature");
4675
- } else if (arg === "--force") {
4676
- args.force = true;
4677
- explicitFlags.add("force");
4678
- } else if (arg === "--verbose") {
4679
- args.verbose = true;
4680
- explicitFlags.add("verbose");
4681
- } else if (arg === "--spec") {
4682
- i++;
4683
- const specs = [];
4684
- while (i < argv.length && !argv[i].startsWith("--")) {
4685
- specs.push(argv[i]);
4686
- i++;
4687
- }
4688
- i--;
4689
- args.spec = specs.length === 1 ? specs[0] : specs;
4690
- explicitFlags.add("spec");
4691
- } else if (arg === "--respec") {
4692
- i++;
4693
- const respecs = [];
4694
- while (i < argv.length && !argv[i].startsWith("--")) {
4695
- respecs.push(argv[i]);
4696
- i++;
4697
- }
4698
- i--;
4699
- args.respec = respecs.length === 1 ? respecs[0] : respecs;
4700
- explicitFlags.add("respec");
4701
- } else if (arg === "--fix-tests") {
4702
- args.fixTests = true;
4703
- explicitFlags.add("fixTests");
4704
- } else if (arg === "--source") {
4705
- i++;
4706
- const val = argv[i];
4707
- if (!DATASOURCE_NAMES.includes(val)) {
4708
- log.error(
4709
- `Unknown source "${val}". Available: ${DATASOURCE_NAMES.join(", ")}`
4710
- );
4711
- process.exit(1);
4712
- }
4713
- args.issueSource = val;
4714
- explicitFlags.add("issueSource");
4715
- } else if (arg === "--org") {
4716
- i++;
4717
- args.org = argv[i];
4718
- explicitFlags.add("org");
4719
- } else if (arg === "--project") {
4720
- i++;
4721
- args.project = argv[i];
4722
- explicitFlags.add("project");
4723
- } else if (arg === "--output-dir") {
4724
- i++;
4725
- args.outputDir = resolve3(argv[i]);
4726
- explicitFlags.add("outputDir");
4727
- } else if (arg === "--concurrency") {
4728
- i++;
4729
- const val = parseInt(argv[i], 10);
4730
- if (isNaN(val) || val < 1) {
4731
- log.error("--concurrency must be a positive integer");
4732
- process.exit(1);
4733
- }
4734
- if (val > MAX_CONCURRENCY) {
4735
- log.error(`--concurrency must not exceed ${MAX_CONCURRENCY}`);
4736
- process.exit(1);
4737
- }
4738
- args.concurrency = val;
4739
- explicitFlags.add("concurrency");
4740
- } else if (arg === "--provider") {
4741
- i++;
4742
- const val = argv[i];
4743
- if (!PROVIDER_NAMES.includes(val)) {
4744
- log.error(`Unknown provider "${val}". Available: ${PROVIDER_NAMES.join(", ")}`);
4745
- process.exit(1);
4746
- }
4747
- args.provider = val;
4748
- explicitFlags.add("provider");
4749
- } else if (arg === "--server-url") {
4750
- i++;
4751
- args.serverUrl = argv[i];
4752
- explicitFlags.add("serverUrl");
4753
- } else if (arg === "--plan-timeout") {
4754
- i++;
4755
- const val = parseFloat(argv[i]);
4756
- if (isNaN(val) || val < CONFIG_BOUNDS.planTimeout.min) {
4757
- log.error("--plan-timeout must be a positive number (minutes)");
4758
- process.exit(1);
4759
- }
4760
- if (val > CONFIG_BOUNDS.planTimeout.max) {
4761
- log.error(`--plan-timeout must not exceed ${CONFIG_BOUNDS.planTimeout.max}`);
4762
- process.exit(1);
4763
- }
4764
- args.planTimeout = val;
4765
- explicitFlags.add("planTimeout");
4766
- } else if (arg === "--retries") {
4767
- i++;
4768
- const val = parseInt(argv[i], 10);
4769
- if (isNaN(val) || val < 0) {
4770
- log.error("--retries must be a non-negative integer");
4771
- process.exit(1);
4772
- }
4773
- args.retries = val;
4774
- explicitFlags.add("retries");
4775
- } else if (arg === "--plan-retries") {
4776
- i++;
4777
- const val = parseInt(argv[i], 10);
4778
- if (isNaN(val) || val < 0) {
4779
- log.error("--plan-retries must be a non-negative integer");
4780
- process.exit(1);
4781
- }
4782
- args.planRetries = val;
4783
- explicitFlags.add("planRetries");
4784
- } else if (arg === "--test-timeout") {
4785
- i++;
4786
- const val = parseFloat(argv[i]);
4787
- if (isNaN(val) || val <= 0) {
4788
- log.error("--test-timeout must be a positive number (minutes)");
4789
- process.exit(1);
4790
- }
4791
- args.testTimeout = val;
4792
- explicitFlags.add("testTimeout");
4793
- } else if (arg === "--cwd") {
4794
- i++;
4795
- args.cwd = resolve3(argv[i]);
4796
- explicitFlags.add("cwd");
4797
- } else if (!arg.startsWith("-")) {
4798
- args.issueIds.push(arg);
5047
+ if (opts.spec !== void 0) {
5048
+ args.spec = opts.spec.length === 1 ? opts.spec[0] : opts.spec;
5049
+ }
5050
+ if (opts.respec !== void 0) {
5051
+ if (opts.respec === true) {
5052
+ args.respec = [];
4799
5053
  } else {
4800
- log.error(`Unknown option: ${arg}`);
4801
- process.exit(1);
5054
+ args.respec = opts.respec.length === 1 ? opts.respec[0] : opts.respec;
5055
+ }
5056
+ }
5057
+ if (opts.fixTests) args.fixTests = true;
5058
+ if (opts.feature) args.feature = true;
5059
+ if (opts.source !== void 0) args.issueSource = opts.source;
5060
+ if (opts.concurrency !== void 0) args.concurrency = opts.concurrency;
5061
+ if (opts.serverUrl !== void 0) args.serverUrl = opts.serverUrl;
5062
+ if (opts.planTimeout !== void 0) args.planTimeout = opts.planTimeout;
5063
+ if (opts.retries !== void 0) args.retries = opts.retries;
5064
+ if (opts.planRetries !== void 0) args.planRetries = opts.planRetries;
5065
+ if (opts.testTimeout !== void 0) args.testTimeout = opts.testTimeout;
5066
+ if (opts.org !== void 0) args.org = opts.org;
5067
+ if (opts.project !== void 0) args.project = opts.project;
5068
+ if (opts.outputDir !== void 0) args.outputDir = opts.outputDir;
5069
+ const explicitFlags = /* @__PURE__ */ new Set();
5070
+ const SOURCE_MAP = {
5071
+ help: "help",
5072
+ version: "version",
5073
+ dryRun: "dryRun",
5074
+ plan: "noPlan",
5075
+ branch: "noBranch",
5076
+ worktree: "noWorktree",
5077
+ force: "force",
5078
+ verbose: "verbose",
5079
+ spec: "spec",
5080
+ respec: "respec",
5081
+ fixTests: "fixTests",
5082
+ feature: "feature",
5083
+ source: "issueSource",
5084
+ provider: "provider",
5085
+ concurrency: "concurrency",
5086
+ serverUrl: "serverUrl",
5087
+ planTimeout: "planTimeout",
5088
+ retries: "retries",
5089
+ planRetries: "planRetries",
5090
+ testTimeout: "testTimeout",
5091
+ cwd: "cwd",
5092
+ org: "org",
5093
+ project: "project",
5094
+ outputDir: "outputDir"
5095
+ };
5096
+ for (const [attr, flag] of Object.entries(SOURCE_MAP)) {
5097
+ if (program.getOptionValueSource(attr) === "cli") {
5098
+ explicitFlags.add(flag);
4802
5099
  }
4803
- i++;
4804
5100
  }
4805
5101
  return [args, explicitFlags];
4806
5102
  }
4807
5103
  async function main() {
4808
5104
  const rawArgv = process.argv.slice(2);
4809
5105
  if (rawArgv[0] === "config") {
4810
- let cwd = process.cwd();
4811
- for (let i = 1; i < rawArgv.length; i++) {
4812
- if (rawArgv[i] === "--cwd" && i + 1 < rawArgv.length) {
4813
- cwd = resolve3(rawArgv[i + 1]);
4814
- break;
5106
+ const configProgram = new Command("dispatch-config").exitOverride().configureOutput({ writeOut: () => {
5107
+ }, writeErr: () => {
5108
+ } }).helpOption(false).allowUnknownOption(true).allowExcessArguments(true).option("--cwd <dir>", "Working directory", (v) => resolve3(v));
5109
+ try {
5110
+ configProgram.parse(rawArgv.slice(1), { from: "user" });
5111
+ } catch (err) {
5112
+ if (err instanceof CommanderError) {
5113
+ log.error(err.message);
5114
+ process.exit(1);
4815
5115
  }
5116
+ throw err;
4816
5117
  }
4817
- const configDir = join11(cwd, ".dispatch");
5118
+ const configDir = join12(configProgram.opts().cwd ?? process.cwd(), ".dispatch");
4818
5119
  await handleConfigCommand(rawArgv.slice(1), configDir);
4819
5120
  process.exit(0);
4820
5121
  }
@@ -4835,7 +5136,7 @@ async function main() {
4835
5136
  process.exit(0);
4836
5137
  }
4837
5138
  if (args.version) {
4838
- console.log(`dispatch v${"1.2.1"}`);
5139
+ console.log(`dispatch v${"1.3.0"}`);
4839
5140
  process.exit(0);
4840
5141
  }
4841
5142
  const orchestrator = await boot9({ cwd: args.cwd });