@pruddiman/dispatch 1.2.1 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -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,14 +1023,15 @@ 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
 
917
1031
  // src/datasources/github.ts
918
1032
  init_logger();
919
- var exec = promisify(execFile);
1033
+
1034
+ // src/helpers/branch-validation.ts
920
1035
  var InvalidBranchNameError = class extends Error {
921
1036
  constructor(branch, reason) {
922
1037
  const detail = reason ? ` (${reason})` : "";
@@ -924,14 +1039,6 @@ var InvalidBranchNameError = class extends Error {
924
1039
  this.name = "InvalidBranchNameError";
925
1040
  }
926
1041
  };
927
- async function git(args, cwd) {
928
- const { stdout } = await exec("git", args, { cwd });
929
- return stdout;
930
- }
931
- async function gh(args, cwd) {
932
- const { stdout } = await exec("gh", args, { cwd });
933
- return stdout;
934
- }
935
1042
  var VALID_BRANCH_NAME_RE = /^[a-zA-Z0-9._\-/]+$/;
936
1043
  function isValidBranchName(name) {
937
1044
  if (name.length === 0 || name.length > 255) return false;
@@ -943,6 +1050,17 @@ function isValidBranchName(name) {
943
1050
  if (name.includes("//")) return false;
944
1051
  return true;
945
1052
  }
1053
+
1054
+ // src/datasources/github.ts
1055
+ var exec = promisify(execFile);
1056
+ async function git(args, cwd) {
1057
+ const { stdout } = await exec("git", args, { cwd });
1058
+ return stdout;
1059
+ }
1060
+ async function gh(args, cwd) {
1061
+ const { stdout } = await exec("gh", args, { cwd });
1062
+ return stdout;
1063
+ }
946
1064
  function buildBranchName(issueNumber, title, username = "unknown") {
947
1065
  const slug = slugify(title, 50);
948
1066
  return `${username}/dispatch/${issueNumber}-${slug}`;
@@ -1153,6 +1271,25 @@ import { execFile as execFile2 } from "child_process";
1153
1271
  import { promisify as promisify2 } from "util";
1154
1272
  init_logger();
1155
1273
  var exec2 = promisify2(execFile2);
1274
+ function mapWorkItemToIssueDetails(item, id, comments, defaults) {
1275
+ const fields = item.fields ?? {};
1276
+ return {
1277
+ number: String(item.id ?? id),
1278
+ title: fields["System.Title"] ?? defaults?.title ?? "",
1279
+ body: fields["System.Description"] ?? defaults?.body ?? "",
1280
+ labels: (fields["System.Tags"] ?? "").split(";").map((t) => t.trim()).filter(Boolean),
1281
+ state: fields["System.State"] ?? defaults?.state ?? "",
1282
+ url: item._links?.html?.href ?? item.url ?? "",
1283
+ comments,
1284
+ acceptanceCriteria: fields["Microsoft.VSTS.Common.AcceptanceCriteria"] ?? "",
1285
+ iterationPath: fields["System.IterationPath"] || void 0,
1286
+ areaPath: fields["System.AreaPath"] || void 0,
1287
+ assignee: fields["System.AssignedTo"]?.displayName || void 0,
1288
+ priority: fields["Microsoft.VSTS.Common.Priority"] ?? void 0,
1289
+ storyPoints: fields["Microsoft.VSTS.Scheduling.StoryPoints"] ?? fields["Microsoft.VSTS.Scheduling.Effort"] ?? fields["Microsoft.VSTS.Scheduling.Size"] ?? void 0,
1290
+ workItemType: fields["System.WorkItemType"] || defaults?.workItemType || void 0
1291
+ };
1292
+ }
1156
1293
  async function detectWorkItemType(opts = {}) {
1157
1294
  try {
1158
1295
  const args = ["boards", "work-item", "type", "list", "--output", "json"];
@@ -1179,7 +1316,26 @@ var datasource2 = {
1179
1316
  return true;
1180
1317
  },
1181
1318
  async list(opts = {}) {
1182
- const wiql = "SELECT [System.Id] FROM workitems WHERE [System.State] <> 'Closed' AND [System.State] <> 'Removed' ORDER BY [System.CreatedDate] DESC";
1319
+ const conditions = [
1320
+ "[System.State] <> 'Closed'",
1321
+ "[System.State] <> 'Removed'"
1322
+ ];
1323
+ if (opts.iteration) {
1324
+ const iterValue = String(opts.iteration).trim();
1325
+ if (iterValue === "@CurrentIteration") {
1326
+ conditions.push(`[System.IterationPath] UNDER @CurrentIteration`);
1327
+ } else {
1328
+ const escaped = iterValue.replace(/'/g, "''");
1329
+ if (escaped) conditions.push(`[System.IterationPath] UNDER '${escaped}'`);
1330
+ }
1331
+ }
1332
+ if (opts.area) {
1333
+ const area = String(opts.area).trim().replace(/'/g, "''");
1334
+ if (area) {
1335
+ conditions.push(`[System.AreaPath] UNDER '${area}'`);
1336
+ }
1337
+ }
1338
+ const wiql = `SELECT [System.Id] FROM workitems WHERE ${conditions.join(" AND ")} ORDER BY [System.CreatedDate] DESC`;
1183
1339
  const args = ["boards", "query", "--wiql", wiql, "--output", "json"];
1184
1340
  if (opts.org) args.push("--org", opts.org);
1185
1341
  if (opts.project) args.push("--project", opts.project);
@@ -1192,17 +1348,50 @@ var datasource2 = {
1192
1348
  } catch {
1193
1349
  throw new Error(`Failed to parse Azure CLI output: ${stdout.slice(0, 200)}`);
1194
1350
  }
1195
- const items = [];
1196
- if (Array.isArray(data)) {
1197
- for (const row of data) {
1198
- const id = String(row.id ?? row.ID ?? "");
1199
- if (id) {
1200
- const detail = await datasource2.fetch(id, opts);
1201
- items.push(detail);
1202
- }
1351
+ if (!Array.isArray(data)) return [];
1352
+ const ids = data.map((row) => String(row.id ?? row.ID ?? "")).filter(Boolean);
1353
+ if (ids.length === 0) return [];
1354
+ try {
1355
+ const batchArgs = [
1356
+ "boards",
1357
+ "work-item",
1358
+ "show",
1359
+ "--id",
1360
+ ...ids,
1361
+ "--output",
1362
+ "json"
1363
+ ];
1364
+ if (opts.org) batchArgs.push("--org", opts.org);
1365
+ if (opts.project) batchArgs.push("--project", opts.project);
1366
+ const { stdout: batchStdout } = await exec2("az", batchArgs, {
1367
+ cwd: opts.cwd || process.cwd()
1368
+ });
1369
+ let batchItems;
1370
+ try {
1371
+ batchItems = JSON.parse(batchStdout);
1372
+ } catch {
1373
+ throw new Error(`Failed to parse Azure CLI output: ${batchStdout.slice(0, 200)}`);
1203
1374
  }
1375
+ const itemsArray = Array.isArray(batchItems) ? batchItems : [batchItems];
1376
+ const commentsArray = [];
1377
+ const CONCURRENCY = 5;
1378
+ for (let i = 0; i < itemsArray.length; i += CONCURRENCY) {
1379
+ const batch = itemsArray.slice(i, i + CONCURRENCY);
1380
+ const batchResults = await Promise.all(
1381
+ batch.map((item) => fetchComments(String(item.id), opts))
1382
+ );
1383
+ commentsArray.push(...batchResults);
1384
+ }
1385
+ return itemsArray.map(
1386
+ (item, i) => mapWorkItemToIssueDetails(item, String(item.id), commentsArray[i])
1387
+ );
1388
+ } catch (err) {
1389
+ log.debug(`Batch work-item show failed, falling back to individual fetches: ${log.extractMessage(err)}`);
1390
+ const results = await Promise.all(
1391
+ ids.map((id) => datasource2.fetch(id, opts))
1392
+ );
1393
+ return results;
1204
1394
  }
1205
- return items;
1206
1395
  },
1207
1396
  async fetch(issueId, opts = {}) {
1208
1397
  const args = [
@@ -1229,18 +1418,8 @@ var datasource2 = {
1229
1418
  } catch {
1230
1419
  throw new Error(`Failed to parse Azure CLI output: ${stdout.slice(0, 200)}`);
1231
1420
  }
1232
- const fields = item.fields ?? {};
1233
1421
  const comments = await fetchComments(issueId, opts);
1234
- return {
1235
- number: String(item.id ?? issueId),
1236
- title: fields["System.Title"] ?? "",
1237
- body: fields["System.Description"] ?? "",
1238
- labels: (fields["System.Tags"] ?? "").split(";").map((t) => t.trim()).filter(Boolean),
1239
- state: fields["System.State"] ?? "",
1240
- url: item._links?.html?.href ?? item.url ?? "",
1241
- comments,
1242
- acceptanceCriteria: fields["Microsoft.VSTS.Common.AcceptanceCriteria"] ?? ""
1243
- };
1422
+ return mapWorkItemToIssueDetails(item, issueId, comments);
1244
1423
  },
1245
1424
  async update(issueId, title, body, opts = {}) {
1246
1425
  const args = [
@@ -1303,24 +1482,26 @@ var datasource2 = {
1303
1482
  } catch {
1304
1483
  throw new Error(`Failed to parse Azure CLI output: ${stdout.slice(0, 200)}`);
1305
1484
  }
1306
- const fields = item.fields ?? {};
1307
- return {
1308
- number: String(item.id),
1309
- title: fields["System.Title"] ?? title,
1310
- body: fields["System.Description"] ?? body,
1311
- labels: (fields["System.Tags"] ?? "").split(";").map((t) => t.trim()).filter(Boolean),
1312
- state: fields["System.State"] ?? "New",
1313
- url: item._links?.html?.href ?? item.url ?? "",
1314
- comments: [],
1315
- acceptanceCriteria: fields["Microsoft.VSTS.Common.AcceptanceCriteria"] ?? ""
1316
- };
1485
+ return mapWorkItemToIssueDetails(item, String(item.id), [], {
1486
+ title,
1487
+ body,
1488
+ state: "New",
1489
+ workItemType
1490
+ });
1317
1491
  },
1318
1492
  async getDefaultBranch(opts) {
1319
1493
  try {
1320
1494
  const { stdout } = await exec2("git", ["symbolic-ref", "refs/remotes/origin/HEAD"], { cwd: opts.cwd });
1321
1495
  const parts = stdout.trim().split("/");
1322
- return parts[parts.length - 1];
1323
- } catch {
1496
+ const branch = parts[parts.length - 1];
1497
+ if (!isValidBranchName(branch)) {
1498
+ throw new InvalidBranchNameError(branch, "from symbolic-ref output");
1499
+ }
1500
+ return branch;
1501
+ } catch (err) {
1502
+ if (err instanceof InvalidBranchNameError) {
1503
+ throw err;
1504
+ }
1324
1505
  try {
1325
1506
  await exec2("git", ["rev-parse", "--verify", "main"], { cwd: opts.cwd });
1326
1507
  return "main";
@@ -1332,18 +1513,38 @@ var datasource2 = {
1332
1513
  async getUsername(opts) {
1333
1514
  try {
1334
1515
  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);
1516
+ const name = slugify(stdout.trim());
1517
+ if (name) return name;
1518
+ } catch {
1519
+ }
1520
+ try {
1521
+ const { stdout } = await exec2("az", ["account", "show", "--query", "user.name", "-o", "tsv"], { cwd: opts.cwd });
1522
+ const name = slugify(stdout.trim());
1523
+ if (name) return name;
1524
+ } catch {
1525
+ }
1526
+ try {
1527
+ const { stdout } = await exec2("az", ["account", "show", "--query", "user.principalName", "-o", "tsv"], { cwd: opts.cwd });
1528
+ const principal = stdout.trim();
1529
+ const prefix = principal.split("@")[0];
1530
+ const name = slugify(prefix);
1531
+ if (name) return name;
1338
1532
  } catch {
1339
- return "unknown";
1340
1533
  }
1534
+ return "unknown";
1341
1535
  },
1342
1536
  buildBranchName(issueNumber, title, username) {
1343
1537
  const slug = slugify(title, 50);
1344
- return `${username}/dispatch/${issueNumber}-${slug}`;
1538
+ const branch = `${username}/dispatch/${issueNumber}-${slug}`;
1539
+ if (!isValidBranchName(branch)) {
1540
+ throw new InvalidBranchNameError(branch);
1541
+ }
1542
+ return branch;
1345
1543
  },
1346
1544
  async createAndSwitchBranch(branchName, opts) {
1545
+ if (!isValidBranchName(branchName)) {
1546
+ throw new InvalidBranchNameError(branchName);
1547
+ }
1347
1548
  try {
1348
1549
  await exec2("git", ["checkout", "-b", branchName], { cwd: opts.cwd });
1349
1550
  } catch (err) {
@@ -1469,7 +1670,7 @@ async function fetchComments(workItemId, opts) {
1469
1670
  // src/datasources/md.ts
1470
1671
  import { execFile as execFile3 } from "child_process";
1471
1672
  import { readFile, writeFile, readdir, mkdir, rename } from "fs/promises";
1472
- import { join, parse as parsePath } from "path";
1673
+ import { join as join2, parse as parsePath } from "path";
1473
1674
  import { promisify as promisify3 } from "util";
1474
1675
 
1475
1676
  // src/helpers/errors.ts
@@ -1489,7 +1690,7 @@ var exec3 = promisify3(execFile3);
1489
1690
  var DEFAULT_DIR = ".dispatch/specs";
1490
1691
  function resolveDir(opts) {
1491
1692
  const cwd = opts?.cwd ?? process.cwd();
1492
- return join(cwd, DEFAULT_DIR);
1693
+ return join2(cwd, DEFAULT_DIR);
1493
1694
  }
1494
1695
  function extractTitle(content, filename) {
1495
1696
  const match = content.match(/^#\s+(.+)$/m);
@@ -1514,7 +1715,7 @@ function toIssueDetails(filename, content, dir) {
1514
1715
  body: content,
1515
1716
  labels: [],
1516
1717
  state: "open",
1517
- url: join(dir, filename),
1718
+ url: join2(dir, filename),
1518
1719
  comments: [],
1519
1720
  acceptanceCriteria: ""
1520
1721
  };
@@ -1535,7 +1736,7 @@ var datasource3 = {
1535
1736
  const mdFiles = entries.filter((f) => f.endsWith(".md")).sort();
1536
1737
  const results = [];
1537
1738
  for (const filename of mdFiles) {
1538
- const filePath = join(dir, filename);
1739
+ const filePath = join2(dir, filename);
1539
1740
  const content = await readFile(filePath, "utf-8");
1540
1741
  results.push(toIssueDetails(filename, content, dir));
1541
1742
  }
@@ -1544,29 +1745,29 @@ var datasource3 = {
1544
1745
  async fetch(issueId, opts) {
1545
1746
  const dir = resolveDir(opts);
1546
1747
  const filename = issueId.endsWith(".md") ? issueId : `${issueId}.md`;
1547
- const filePath = join(dir, filename);
1748
+ const filePath = join2(dir, filename);
1548
1749
  const content = await readFile(filePath, "utf-8");
1549
1750
  return toIssueDetails(filename, content, dir);
1550
1751
  },
1551
1752
  async update(issueId, _title, body, opts) {
1552
1753
  const dir = resolveDir(opts);
1553
1754
  const filename = issueId.endsWith(".md") ? issueId : `${issueId}.md`;
1554
- const filePath = join(dir, filename);
1755
+ const filePath = join2(dir, filename);
1555
1756
  await writeFile(filePath, body, "utf-8");
1556
1757
  },
1557
1758
  async close(issueId, opts) {
1558
1759
  const dir = resolveDir(opts);
1559
1760
  const filename = issueId.endsWith(".md") ? issueId : `${issueId}.md`;
1560
- const filePath = join(dir, filename);
1561
- const archiveDir = join(dir, "archive");
1761
+ const filePath = join2(dir, filename);
1762
+ const archiveDir = join2(dir, "archive");
1562
1763
  await mkdir(archiveDir, { recursive: true });
1563
- await rename(filePath, join(archiveDir, filename));
1764
+ await rename(filePath, join2(archiveDir, filename));
1564
1765
  },
1565
1766
  async create(title, body, opts) {
1566
1767
  const dir = resolveDir(opts);
1567
1768
  await mkdir(dir, { recursive: true });
1568
1769
  const filename = `${slugify(title)}.md`;
1569
- const filePath = join(dir, filename);
1770
+ const filePath = join2(dir, filename);
1570
1771
  await writeFile(filePath, body, "utf-8");
1571
1772
  return toIssueDetails(filename, body, dir);
1572
1773
  },
@@ -1646,6 +1847,36 @@ async function detectDatasource(cwd) {
1646
1847
  }
1647
1848
  return null;
1648
1849
  }
1850
+ function parseAzDevOpsRemoteUrl(url) {
1851
+ const httpsMatch = url.match(
1852
+ /^https?:\/\/(?:[^@]+@)?dev\.azure\.com\/([^/]+)\/([^/]+)\/_git\//i
1853
+ );
1854
+ if (httpsMatch) {
1855
+ return {
1856
+ orgUrl: `https://dev.azure.com/${decodeURIComponent(httpsMatch[1])}`,
1857
+ project: decodeURIComponent(httpsMatch[2])
1858
+ };
1859
+ }
1860
+ const sshMatch = url.match(
1861
+ /^git@ssh\.dev\.azure\.com:v3\/([^/]+)\/([^/]+)\//i
1862
+ );
1863
+ if (sshMatch) {
1864
+ return {
1865
+ orgUrl: `https://dev.azure.com/${decodeURIComponent(sshMatch[1])}`,
1866
+ project: decodeURIComponent(sshMatch[2])
1867
+ };
1868
+ }
1869
+ const legacyMatch = url.match(
1870
+ /^https?:\/\/([^.]+)\.visualstudio\.com\/(?:DefaultCollection\/)?([^/]+)\/_git\//i
1871
+ );
1872
+ if (legacyMatch) {
1873
+ return {
1874
+ orgUrl: `https://dev.azure.com/${decodeURIComponent(legacyMatch[1])}`,
1875
+ project: decodeURIComponent(legacyMatch[2])
1876
+ };
1877
+ }
1878
+ return null;
1879
+ }
1649
1880
 
1650
1881
  // src/spec-generator.ts
1651
1882
  init_logger();
@@ -1662,16 +1893,16 @@ var RECOGNIZED_H2 = /* @__PURE__ */ new Set([
1662
1893
  function defaultConcurrency() {
1663
1894
  return Math.max(1, Math.min(cpus().length, Math.floor(freemem() / 1024 / 1024 / MB_PER_CONCURRENT_TASK)));
1664
1895
  }
1665
- function isIssueNumbers(input2) {
1666
- if (Array.isArray(input2)) return false;
1667
- return /^\d+(,\s*\d+)*$/.test(input2);
1896
+ function isIssueNumbers(input3) {
1897
+ if (Array.isArray(input3)) return false;
1898
+ return /^\d+(,\s*\d+)*$/.test(input3);
1668
1899
  }
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;
1900
+ function isGlobOrFilePath(input3) {
1901
+ if (Array.isArray(input3)) return true;
1902
+ if (/[*?\[{]/.test(input3)) return true;
1903
+ if (/[/\\]/.test(input3)) return true;
1904
+ if (/^\.\.?[\/\\]/.test(input3)) return true;
1905
+ if (/\.(md|txt|yaml|yml|json|ts|js|tsx|jsx)$/i.test(input3)) return true;
1675
1906
  return false;
1676
1907
  }
1677
1908
  function extractSpecContent(raw) {
@@ -1797,7 +2028,7 @@ function semverGte(current, minimum) {
1797
2028
  async function checkPrereqs(context) {
1798
2029
  const failures = [];
1799
2030
  try {
1800
- await exec5("git", ["--version"]);
2031
+ await exec5("git", ["--version"], { shell: process.platform === "win32" });
1801
2032
  } catch {
1802
2033
  failures.push("git is required but was not found on PATH. Install it from https://git-scm.com");
1803
2034
  }
@@ -1809,7 +2040,7 @@ async function checkPrereqs(context) {
1809
2040
  }
1810
2041
  if (context?.datasource === "github") {
1811
2042
  try {
1812
- await exec5("gh", ["--version"]);
2043
+ await exec5("gh", ["--version"], { shell: process.platform === "win32" });
1813
2044
  } catch {
1814
2045
  failures.push(
1815
2046
  "gh (GitHub CLI) is required for the github datasource but was not found on PATH. Install it from https://cli.github.com/"
@@ -1818,7 +2049,7 @@ async function checkPrereqs(context) {
1818
2049
  }
1819
2050
  if (context?.datasource === "azdevops") {
1820
2051
  try {
1821
- await exec5("az", ["--version"]);
2052
+ await exec5("az", ["--version"], { shell: process.platform === "win32" });
1822
2053
  } catch {
1823
2054
  failures.push(
1824
2055
  "az (Azure CLI) is required for the azdevops datasource but was not found on PATH. Install it from https://learn.microsoft.com/en-us/cli/azure/"
@@ -1831,17 +2062,23 @@ async function checkPrereqs(context) {
1831
2062
  // src/helpers/gitignore.ts
1832
2063
  init_logger();
1833
2064
  import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
1834
- import { join as join2 } from "path";
2065
+ import { join as join3 } from "path";
1835
2066
  async function ensureGitignoreEntry(repoRoot, entry) {
1836
- const gitignorePath = join2(repoRoot, ".gitignore");
2067
+ const gitignorePath = join3(repoRoot, ".gitignore");
1837
2068
  let contents = "";
1838
2069
  try {
1839
2070
  contents = await readFile2(gitignorePath, "utf8");
1840
- } catch {
2071
+ } catch (err) {
2072
+ if (err instanceof Error && "code" in err && err.code === "ENOENT") {
2073
+ } else {
2074
+ log.warn(`Could not read .gitignore: ${String(err)}`);
2075
+ return;
2076
+ }
1841
2077
  }
1842
- const lines = contents.split("\n").map((l) => l.trim());
2078
+ const lines = contents.split(/\r?\n/);
1843
2079
  const bare = entry.replace(/\/$/, "");
1844
- if (lines.includes(entry) || lines.includes(bare)) {
2080
+ const withSlash = bare + "/";
2081
+ if (lines.includes(entry) || lines.includes(bare) || lines.includes(withSlash)) {
1845
2082
  return;
1846
2083
  }
1847
2084
  try {
@@ -1856,18 +2093,18 @@ async function ensureGitignoreEntry(repoRoot, entry) {
1856
2093
 
1857
2094
  // src/orchestrator/cli-config.ts
1858
2095
  init_logger();
1859
- import { join as join4 } from "path";
2096
+ import { join as join5 } from "path";
1860
2097
  import { access } from "fs/promises";
1861
2098
  import { constants } from "fs";
1862
2099
 
1863
2100
  // src/config.ts
1864
2101
  init_providers();
1865
2102
  import { readFile as readFile3, writeFile as writeFile3, mkdir as mkdir2 } from "fs/promises";
1866
- import { join as join3, dirname } from "path";
2103
+ import { join as join4, dirname as dirname2 } from "path";
1867
2104
 
1868
2105
  // src/config-prompts.ts
1869
2106
  init_logger();
1870
- import { select, confirm } from "@inquirer/prompts";
2107
+ import { select, confirm, input as input2 } from "@inquirer/prompts";
1871
2108
  import chalk3 from "chalk";
1872
2109
  init_providers();
1873
2110
  async function runInteractiveConfigWizard(configDir) {
@@ -1947,6 +2184,54 @@ async function runInteractiveConfigWizard(configDir) {
1947
2184
  default: datasourceDefault
1948
2185
  });
1949
2186
  const source = selectedSource === "auto" ? void 0 : selectedSource;
2187
+ let org;
2188
+ let project;
2189
+ let workItemType;
2190
+ let iteration;
2191
+ let area;
2192
+ const effectiveSource = source ?? detectedSource;
2193
+ if (effectiveSource === "azdevops") {
2194
+ let defaultOrg = existing.org ?? "";
2195
+ let defaultProject = existing.project ?? "";
2196
+ try {
2197
+ const remoteUrl = await getGitRemoteUrl(process.cwd());
2198
+ if (remoteUrl) {
2199
+ const parsed = parseAzDevOpsRemoteUrl(remoteUrl);
2200
+ if (parsed) {
2201
+ if (!defaultOrg) defaultOrg = parsed.orgUrl;
2202
+ if (!defaultProject) defaultProject = parsed.project;
2203
+ }
2204
+ }
2205
+ } catch {
2206
+ }
2207
+ console.log();
2208
+ log.info(chalk3.bold("Azure DevOps settings") + chalk3.dim(" (leave empty to skip):"));
2209
+ const orgInput = await input2({
2210
+ message: "Organization URL:",
2211
+ default: defaultOrg || void 0
2212
+ });
2213
+ if (orgInput.trim()) org = orgInput.trim();
2214
+ const projectInput = await input2({
2215
+ message: "Project name:",
2216
+ default: defaultProject || void 0
2217
+ });
2218
+ if (projectInput.trim()) project = projectInput.trim();
2219
+ const workItemTypeInput = await input2({
2220
+ message: "Work item type (e.g. User Story, Bug):",
2221
+ default: existing.workItemType ?? void 0
2222
+ });
2223
+ if (workItemTypeInput.trim()) workItemType = workItemTypeInput.trim();
2224
+ const iterationInput = await input2({
2225
+ message: "Iteration path (e.g. MyProject\\Sprint 1, or @CurrentIteration):",
2226
+ default: existing.iteration ?? void 0
2227
+ });
2228
+ if (iterationInput.trim()) iteration = iterationInput.trim();
2229
+ const areaInput = await input2({
2230
+ message: "Area path (e.g. MyProject\\Team A):",
2231
+ default: existing.area ?? void 0
2232
+ });
2233
+ if (areaInput.trim()) area = areaInput.trim();
2234
+ }
1950
2235
  const newConfig = {
1951
2236
  provider,
1952
2237
  source
@@ -1954,6 +2239,11 @@ async function runInteractiveConfigWizard(configDir) {
1954
2239
  if (selectedModel !== void 0) {
1955
2240
  newConfig.model = selectedModel;
1956
2241
  }
2242
+ if (org !== void 0) newConfig.org = org;
2243
+ if (project !== void 0) newConfig.project = project;
2244
+ if (workItemType !== void 0) newConfig.workItemType = workItemType;
2245
+ if (iteration !== void 0) newConfig.iteration = iteration;
2246
+ if (area !== void 0) newConfig.area = area;
1957
2247
  console.log();
1958
2248
  log.info(chalk3.bold("Configuration summary:"));
1959
2249
  for (const [key, value] of Object.entries(newConfig)) {
@@ -1985,10 +2275,10 @@ var CONFIG_BOUNDS = {
1985
2275
  planTimeout: { min: 1, max: 120 },
1986
2276
  concurrency: { min: 1, max: 64 }
1987
2277
  };
1988
- var CONFIG_KEYS = ["provider", "model", "source", "testTimeout", "planTimeout", "concurrency"];
2278
+ var CONFIG_KEYS = ["provider", "model", "source", "testTimeout", "planTimeout", "concurrency", "org", "project", "workItemType", "iteration", "area"];
1989
2279
  function getConfigPath(configDir) {
1990
- const dir = configDir ?? join3(process.cwd(), ".dispatch");
1991
- return join3(dir, "config.json");
2280
+ const dir = configDir ?? join4(process.cwd(), ".dispatch");
2281
+ return join4(dir, "config.json");
1992
2282
  }
1993
2283
  async function loadConfig(configDir) {
1994
2284
  const configPath = getConfigPath(configDir);
@@ -2001,7 +2291,7 @@ async function loadConfig(configDir) {
2001
2291
  }
2002
2292
  async function saveConfig(config, configDir) {
2003
2293
  const configPath = getConfigPath(configDir);
2004
- await mkdir2(dirname(configPath), { recursive: true });
2294
+ await mkdir2(dirname2(configPath), { recursive: true });
2005
2295
  await writeFile3(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
2006
2296
  }
2007
2297
  async function handleConfigCommand(_argv, configDir) {
@@ -2015,14 +2305,19 @@ var CONFIG_TO_CLI = {
2015
2305
  source: "issueSource",
2016
2306
  testTimeout: "testTimeout",
2017
2307
  planTimeout: "planTimeout",
2018
- concurrency: "concurrency"
2308
+ concurrency: "concurrency",
2309
+ org: "org",
2310
+ project: "project",
2311
+ workItemType: "workItemType",
2312
+ iteration: "iteration",
2313
+ area: "area"
2019
2314
  };
2020
2315
  function setCliField(target, key, value) {
2021
2316
  target[key] = value;
2022
2317
  }
2023
2318
  async function resolveCliConfig(args) {
2024
2319
  const { explicitFlags } = args;
2025
- const configDir = join4(args.cwd, ".dispatch");
2320
+ const configDir = join5(args.cwd, ".dispatch");
2026
2321
  const config = await loadConfig(configDir);
2027
2322
  const merged = { ...args };
2028
2323
  for (const configKey of CONFIG_KEYS) {
@@ -2069,16 +2364,17 @@ async function resolveCliConfig(args) {
2069
2364
  }
2070
2365
 
2071
2366
  // src/orchestrator/spec-pipeline.ts
2072
- import { join as join6 } from "path";
2367
+ import { join as join7 } from "path";
2073
2368
  import { mkdir as mkdir4, readFile as readFile5, rename as rename2, unlink as unlink2 } from "fs/promises";
2074
2369
  import { glob } from "glob";
2075
2370
  init_providers();
2076
2371
 
2077
2372
  // src/agents/spec.ts
2078
2373
  import { mkdir as mkdir3, readFile as readFile4, writeFile as writeFile4, unlink } from "fs/promises";
2079
- import { join as join5, resolve, sep } from "path";
2374
+ import { join as join6, resolve, sep } from "path";
2080
2375
  import { randomUUID as randomUUID3 } from "crypto";
2081
2376
  init_logger();
2377
+ init_file_logger();
2082
2378
  async function boot5(opts) {
2083
2379
  const { provider } = opts;
2084
2380
  if (!provider) {
@@ -2100,10 +2396,10 @@ async function boot5(opts) {
2100
2396
  durationMs: Date.now() - startTime
2101
2397
  };
2102
2398
  }
2103
- const tmpDir = join5(resolvedCwd, ".dispatch", "tmp");
2399
+ const tmpDir = join6(resolvedCwd, ".dispatch", "tmp");
2104
2400
  await mkdir3(tmpDir, { recursive: true });
2105
2401
  const tmpFilename = `spec-${randomUUID3()}.md`;
2106
- const tmpPath = join5(tmpDir, tmpFilename);
2402
+ const tmpPath = join6(tmpDir, tmpFilename);
2107
2403
  let prompt;
2108
2404
  if (issue) {
2109
2405
  prompt = buildSpecPrompt(issue, workingDir, tmpPath);
@@ -2119,6 +2415,7 @@ async function boot5(opts) {
2119
2415
  durationMs: Date.now() - startTime
2120
2416
  };
2121
2417
  }
2418
+ fileLoggerStorage.getStore()?.prompt("spec", prompt);
2122
2419
  const sessionId = await provider.createSession();
2123
2420
  log.debug(`Spec prompt built (${prompt.length} chars)`);
2124
2421
  const response = await provider.prompt(sessionId, prompt);
@@ -2131,6 +2428,7 @@ async function boot5(opts) {
2131
2428
  };
2132
2429
  }
2133
2430
  log.debug(`Spec agent response (${response.length} chars)`);
2431
+ fileLoggerStorage.getStore()?.response("spec", response);
2134
2432
  let rawContent;
2135
2433
  try {
2136
2434
  rawContent = await readFile4(tmpPath, "utf-8");
@@ -2154,6 +2452,7 @@ async function boot5(opts) {
2154
2452
  await unlink(tmpPath);
2155
2453
  } catch {
2156
2454
  }
2455
+ fileLoggerStorage.getStore()?.agentEvent("spec", "completed", `${Date.now() - startTime}ms`);
2157
2456
  return {
2158
2457
  data: {
2159
2458
  content: cleanedContent,
@@ -2165,6 +2464,8 @@ async function boot5(opts) {
2165
2464
  };
2166
2465
  } catch (err) {
2167
2466
  const message = log.extractMessage(err);
2467
+ fileLoggerStorage.getStore()?.error(`spec error: ${message}${err instanceof Error && err.stack ? `
2468
+ ${err.stack}` : ""}`);
2168
2469
  return {
2169
2470
  data: null,
2170
2471
  success: false,
@@ -2400,6 +2701,7 @@ function buildInlineTextSpecPrompt(text, cwd, outputPath) {
2400
2701
  // src/orchestrator/spec-pipeline.ts
2401
2702
  init_cleanup();
2402
2703
  init_logger();
2704
+ init_file_logger();
2403
2705
  import chalk5 from "chalk";
2404
2706
 
2405
2707
  // src/helpers/format.ts
@@ -2452,11 +2754,11 @@ async function withRetry(fn, maxRetries, options) {
2452
2754
  // src/orchestrator/spec-pipeline.ts
2453
2755
  init_timeout();
2454
2756
  var FETCH_TIMEOUT_MS = 3e4;
2455
- async function resolveDatasource(issues, issueSource, specCwd, org, project, workItemType) {
2757
+ async function resolveDatasource(issues, issueSource, specCwd, org, project, workItemType, iteration, area) {
2456
2758
  const source = await resolveSource(issues, issueSource, specCwd);
2457
2759
  if (!source) return null;
2458
2760
  const datasource4 = getDatasource(source);
2459
- const fetchOpts = { cwd: specCwd, org, project, workItemType };
2761
+ const fetchOpts = { cwd: specCwd, org, project, workItemType, iteration, area };
2460
2762
  return { source, datasource: datasource4, fetchOpts };
2461
2763
  }
2462
2764
  async function fetchTrackerItems(issues, datasource4, fetchOpts, concurrency, source) {
@@ -2497,7 +2799,7 @@ function buildInlineTextItem(issues, outputDir) {
2497
2799
  const title = text.length > 80 ? text.slice(0, 80).trimEnd() + "\u2026" : text;
2498
2800
  const slug = slugify(text, MAX_SLUG_LENGTH);
2499
2801
  const filename = `${slug}.md`;
2500
- const filepath = join6(outputDir, filename);
2802
+ const filepath = join7(outputDir, filename);
2501
2803
  const details = {
2502
2804
  number: filepath,
2503
2805
  title,
@@ -2559,7 +2861,7 @@ function previewDryRun(validItems, items, isTrackerMode, isInlineText, outputDir
2559
2861
  let filepath;
2560
2862
  if (isTrackerMode) {
2561
2863
  const slug = slugify(details.title, 60);
2562
- filepath = join6(outputDir, `${id}-${slug}.md`);
2864
+ filepath = join7(outputDir, `${id}-${slug}.md`);
2563
2865
  } else {
2564
2866
  filepath = id;
2565
2867
  }
@@ -2617,72 +2919,92 @@ async function generateSpecsBatch(validItems, items, specAgent, instance, isTrac
2617
2919
  log.error(`Skipping item ${id}: missing issue details`);
2618
2920
  return null;
2619
2921
  }
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
- }
2922
+ const itemBody = async () => {
2923
+ let filepath;
2924
+ if (isTrackerMode) {
2925
+ const slug = slugify(details.title, MAX_SLUG_LENGTH);
2926
+ const filename = `${id}-${slug}.md`;
2927
+ filepath = join7(outputDir, filename);
2928
+ } else if (isInlineText) {
2929
+ filepath = id;
2930
+ } else {
2931
+ filepath = id;
2655
2932
  }
2656
- const specDuration = Date.now() - specStart;
2657
- fileDurationsMs[filepath] = specDuration;
2658
- log.success(`Spec written: ${filepath} (${elapsed(specDuration)})`);
2659
- let identifier = filepath;
2933
+ fileLoggerStorage.getStore()?.info(`Output path: ${filepath}`);
2660
2934
  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);
2935
+ fileLoggerStorage.getStore()?.info(`Starting spec generation for ${isTrackerMode ? `#${id}` : filepath}`);
2936
+ log.info(`Generating spec for ${isTrackerMode ? `#${id}` : filepath}: ${details.title}...`);
2937
+ const result = await withRetry(
2938
+ () => specAgent.generate({
2939
+ issue: isTrackerMode ? details : void 0,
2940
+ filePath: isTrackerMode ? void 0 : id,
2941
+ fileContent: isTrackerMode ? void 0 : details.body,
2942
+ cwd: specCwd,
2943
+ outputPath: filepath
2944
+ }),
2945
+ retries,
2946
+ { label: `specAgent.generate(${isTrackerMode ? `#${id}` : filepath})` }
2947
+ );
2948
+ if (!result.success) {
2949
+ throw new Error(result.error ?? "Spec generation failed");
2950
+ }
2951
+ fileLoggerStorage.getStore()?.info(`Spec generated successfully`);
2952
+ if (isTrackerMode || isInlineText) {
2953
+ const h1Title = extractTitle(result.data.content, filepath);
2954
+ const h1Slug = slugify(h1Title, MAX_SLUG_LENGTH);
2955
+ const finalFilename = isTrackerMode ? `${id}-${h1Slug}.md` : `${h1Slug}.md`;
2956
+ const finalFilepath = join7(outputDir, finalFilename);
2957
+ if (finalFilepath !== filepath) {
2958
+ await rename2(filepath, finalFilepath);
2959
+ filepath = finalFilepath;
2960
+ }
2675
2961
  }
2962
+ const specDuration = Date.now() - specStart;
2963
+ fileDurationsMs[filepath] = specDuration;
2964
+ log.success(`Spec written: ${filepath} (${elapsed(specDuration)})`);
2965
+ let identifier = filepath;
2966
+ fileLoggerStorage.getStore()?.phase("Datasource sync");
2967
+ try {
2968
+ if (isTrackerMode) {
2969
+ await datasource4.update(id, details.title, result.data.content, fetchOpts);
2970
+ log.success(`Updated issue #${id} with spec content`);
2971
+ await unlink2(filepath);
2972
+ log.success(`Deleted local spec ${filepath} (now tracked as issue #${id})`);
2973
+ identifier = id;
2974
+ issueNumbers.push(id);
2975
+ } else if (datasource4.name !== "md") {
2976
+ const created = await datasource4.create(details.title, result.data.content, fetchOpts);
2977
+ log.success(`Created issue #${created.number} from ${filepath}`);
2978
+ await unlink2(filepath);
2979
+ log.success(`Deleted local spec ${filepath} (now tracked as issue #${created.number})`);
2980
+ identifier = created.number;
2981
+ issueNumbers.push(created.number);
2982
+ }
2983
+ } catch (err) {
2984
+ const label = isTrackerMode ? `issue #${id}` : filepath;
2985
+ log.warn(`Could not sync ${label} to datasource: ${log.formatErrorChain(err)}`);
2986
+ }
2987
+ return { filepath, identifier };
2676
2988
  } catch (err) {
2677
- const label = isTrackerMode ? `issue #${id}` : filepath;
2678
- log.warn(`Could not sync ${label} to datasource: ${log.formatErrorChain(err)}`);
2989
+ fileLoggerStorage.getStore()?.error(`Spec generation failed for ${id}: ${log.extractMessage(err)}${err instanceof Error && err.stack ? `
2990
+ ${err.stack}` : ""}`);
2991
+ log.error(`Failed to generate spec for ${isTrackerMode ? `#${id}` : filepath}: ${log.formatErrorChain(err)}`);
2992
+ log.debug(log.formatErrorChain(err));
2993
+ return null;
2679
2994
  }
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;
2995
+ };
2996
+ const fileLogger = log.verbose ? new FileLogger(id, specCwd) : null;
2997
+ if (fileLogger) {
2998
+ return fileLoggerStorage.run(fileLogger, async () => {
2999
+ try {
3000
+ fileLogger.phase(`Spec generation: ${id}`);
3001
+ return await itemBody();
3002
+ } finally {
3003
+ fileLogger.close();
3004
+ }
3005
+ });
2685
3006
  }
3007
+ return itemBody();
2686
3008
  })
2687
3009
  );
2688
3010
  for (const result of batchResults) {
@@ -2736,16 +3058,18 @@ async function runSpecPipeline(opts) {
2736
3058
  model,
2737
3059
  serverUrl,
2738
3060
  cwd: specCwd,
2739
- outputDir = join6(specCwd, ".dispatch", "specs"),
3061
+ outputDir = join7(specCwd, ".dispatch", "specs"),
2740
3062
  org,
2741
3063
  project,
2742
3064
  workItemType,
3065
+ iteration,
3066
+ area,
2743
3067
  concurrency = defaultConcurrency(),
2744
3068
  dryRun,
2745
3069
  retries = 2
2746
3070
  } = opts;
2747
3071
  const pipelineStart = Date.now();
2748
- const resolved = await resolveDatasource(issues, opts.issueSource, specCwd, org, project, workItemType);
3072
+ const resolved = await resolveDatasource(issues, opts.issueSource, specCwd, org, project, workItemType, iteration, area);
2749
3073
  if (!resolved) {
2750
3074
  return { total: 0, generated: 0, failed: 0, files: [], issueNumbers: [], durationMs: Date.now() - pipelineStart, fileDurationsMs: {} };
2751
3075
  }
@@ -2865,7 +3189,9 @@ async function parseTaskFile(filePath) {
2865
3189
  }
2866
3190
  async function markTaskComplete(task) {
2867
3191
  const content = await readFile6(task.file, "utf-8");
2868
- const lines = content.split("\n");
3192
+ const eol = content.includes("\r\n") ? "\r\n" : "\n";
3193
+ const normalized = content.replace(/\r\n/g, "\n");
3194
+ const lines = normalized.split("\n");
2869
3195
  const lineIndex = task.line - 1;
2870
3196
  if (lineIndex < 0 || lineIndex >= lines.length) {
2871
3197
  throw new Error(
@@ -2880,7 +3206,7 @@ async function markTaskComplete(task) {
2880
3206
  );
2881
3207
  }
2882
3208
  lines[lineIndex] = updated;
2883
- await writeFile5(task.file, lines.join("\n"), "utf-8");
3209
+ await writeFile5(task.file, lines.join(eol), "utf-8");
2884
3210
  }
2885
3211
  function groupTasksByMode(tasks) {
2886
3212
  if (tasks.length === 0) return [];
@@ -2910,6 +3236,7 @@ function groupTasksByMode(tasks) {
2910
3236
 
2911
3237
  // src/agents/planner.ts
2912
3238
  init_logger();
3239
+ init_file_logger();
2913
3240
  async function boot6(opts) {
2914
3241
  const { provider, cwd } = opts;
2915
3242
  if (!provider) {
@@ -2922,13 +3249,18 @@ async function boot6(opts) {
2922
3249
  try {
2923
3250
  const sessionId = await provider.createSession();
2924
3251
  const prompt = buildPlannerPrompt(task, cwdOverride ?? cwd, fileContext, worktreeRoot);
3252
+ fileLoggerStorage.getStore()?.prompt("planner", prompt);
2925
3253
  const plan = await provider.prompt(sessionId, prompt);
3254
+ if (plan) fileLoggerStorage.getStore()?.response("planner", plan);
2926
3255
  if (!plan?.trim()) {
2927
3256
  return { data: null, success: false, error: "Planner returned empty plan", durationMs: Date.now() - startTime };
2928
3257
  }
3258
+ fileLoggerStorage.getStore()?.agentEvent("planner", "completed", `${Date.now() - startTime}ms`);
2929
3259
  return { data: { prompt: plan }, success: true, durationMs: Date.now() - startTime };
2930
3260
  } catch (err) {
2931
3261
  const message = log.extractMessage(err);
3262
+ fileLoggerStorage.getStore()?.error(`planner error: ${message}${err instanceof Error && err.stack ? `
3263
+ ${err.stack}` : ""}`);
2932
3264
  return { data: null, success: false, error: message, durationMs: Date.now() - startTime };
2933
3265
  }
2934
3266
  },
@@ -3010,22 +3342,28 @@ function buildPlannerPrompt(task, cwd, fileContext, worktreeRoot) {
3010
3342
 
3011
3343
  // src/dispatcher.ts
3012
3344
  init_logger();
3345
+ init_file_logger();
3013
3346
  async function dispatchTask(instance, task, cwd, plan, worktreeRoot) {
3014
3347
  try {
3015
3348
  log.debug(`Dispatching task: ${task.file}:${task.line} \u2014 ${task.text.slice(0, 80)}`);
3016
3349
  const sessionId = await instance.createSession();
3017
3350
  const prompt = plan ? buildPlannedPrompt(task, cwd, plan, worktreeRoot) : buildPrompt(task, cwd, worktreeRoot);
3018
3351
  log.debug(`Prompt built (${prompt.length} chars, ${plan ? "with plan" : "no plan"})`);
3352
+ fileLoggerStorage.getStore()?.prompt("dispatchTask", prompt);
3019
3353
  const response = await instance.prompt(sessionId, prompt);
3020
3354
  if (response === null) {
3021
3355
  log.debug("Task dispatch returned null response");
3356
+ fileLoggerStorage.getStore()?.warn("dispatchTask: null response");
3022
3357
  return { task, success: false, error: "No response from agent" };
3023
3358
  }
3024
3359
  log.debug(`Task dispatch completed (${response.length} chars response)`);
3360
+ fileLoggerStorage.getStore()?.response("dispatchTask", response);
3025
3361
  return { task, success: true };
3026
3362
  } catch (err) {
3027
3363
  const message = log.extractMessage(err);
3028
3364
  log.debug(`Task dispatch failed: ${log.formatErrorChain(err)}`);
3365
+ fileLoggerStorage.getStore()?.error(`dispatchTask error: ${message}${err instanceof Error && err.stack ? `
3366
+ ${err.stack}` : ""}`);
3029
3367
  return { task, success: false, error: message };
3030
3368
  }
3031
3369
  }
@@ -3092,6 +3430,7 @@ function buildWorktreeIsolation(worktreeRoot) {
3092
3430
 
3093
3431
  // src/agents/executor.ts
3094
3432
  init_logger();
3433
+ init_file_logger();
3095
3434
  async function boot7(opts) {
3096
3435
  const { provider } = opts;
3097
3436
  if (!provider) {
@@ -3099,18 +3438,23 @@ async function boot7(opts) {
3099
3438
  }
3100
3439
  return {
3101
3440
  name: "executor",
3102
- async execute(input2) {
3103
- const { task, cwd, plan, worktreeRoot } = input2;
3441
+ async execute(input3) {
3442
+ const { task, cwd, plan, worktreeRoot } = input3;
3104
3443
  const startTime = Date.now();
3105
3444
  try {
3445
+ fileLoggerStorage.getStore()?.agentEvent("executor", "started", task.text);
3106
3446
  const result = await dispatchTask(provider, task, cwd, plan ?? void 0, worktreeRoot);
3107
3447
  if (result.success) {
3108
3448
  await markTaskComplete(task);
3449
+ fileLoggerStorage.getStore()?.agentEvent("executor", "completed", `${Date.now() - startTime}ms`);
3109
3450
  return { data: { dispatchResult: result }, success: true, durationMs: Date.now() - startTime };
3110
3451
  }
3452
+ fileLoggerStorage.getStore()?.agentEvent("executor", "failed", result.error ?? "unknown error");
3111
3453
  return { data: null, success: false, error: result.error, durationMs: Date.now() - startTime };
3112
3454
  } catch (err) {
3113
3455
  const message = log.extractMessage(err);
3456
+ fileLoggerStorage.getStore()?.error(`executor error: ${message}${err instanceof Error && err.stack ? `
3457
+ ${err.stack}` : ""}`);
3114
3458
  return { data: null, success: false, error: message, durationMs: Date.now() - startTime };
3115
3459
  }
3116
3460
  },
@@ -3121,8 +3465,9 @@ async function boot7(opts) {
3121
3465
 
3122
3466
  // src/agents/commit.ts
3123
3467
  init_logger();
3468
+ init_file_logger();
3124
3469
  import { mkdir as mkdir5, writeFile as writeFile6 } from "fs/promises";
3125
- import { join as join7, resolve as resolve2 } from "path";
3470
+ import { join as join8, resolve as resolve2 } from "path";
3126
3471
  import { randomUUID as randomUUID4 } from "crypto";
3127
3472
  async function boot8(opts) {
3128
3473
  const { provider } = opts;
@@ -3136,14 +3481,16 @@ async function boot8(opts) {
3136
3481
  async generate(genOpts) {
3137
3482
  try {
3138
3483
  const resolvedCwd = resolve2(genOpts.cwd);
3139
- const tmpDir = join7(resolvedCwd, ".dispatch", "tmp");
3484
+ const tmpDir = join8(resolvedCwd, ".dispatch", "tmp");
3140
3485
  await mkdir5(tmpDir, { recursive: true });
3141
3486
  const tmpFilename = `commit-${randomUUID4()}.md`;
3142
- const tmpPath = join7(tmpDir, tmpFilename);
3487
+ const tmpPath = join8(tmpDir, tmpFilename);
3143
3488
  const prompt = buildCommitPrompt(genOpts);
3489
+ fileLoggerStorage.getStore()?.prompt("commit", prompt);
3144
3490
  const sessionId = await provider.createSession();
3145
3491
  log.debug(`Commit prompt built (${prompt.length} chars)`);
3146
3492
  const response = await provider.prompt(sessionId, prompt);
3493
+ if (response) fileLoggerStorage.getStore()?.response("commit", response);
3147
3494
  if (!response?.trim()) {
3148
3495
  return {
3149
3496
  commitMessage: "",
@@ -3167,12 +3514,15 @@ async function boot8(opts) {
3167
3514
  const outputContent = formatOutputFile(parsed);
3168
3515
  await writeFile6(tmpPath, outputContent, "utf-8");
3169
3516
  log.debug(`Wrote commit agent output to ${tmpPath}`);
3517
+ fileLoggerStorage.getStore()?.agentEvent("commit", "completed", `message: ${parsed.commitMessage.slice(0, 80)}`);
3170
3518
  return {
3171
3519
  ...parsed,
3172
3520
  success: true,
3173
3521
  outputPath: tmpPath
3174
3522
  };
3175
3523
  } catch (err) {
3524
+ fileLoggerStorage.getStore()?.error(`commit error: ${log.extractMessage(err)}${err instanceof Error && err.stack ? `
3525
+ ${err.stack}` : ""}`);
3176
3526
  const message = log.extractMessage(err);
3177
3527
  return {
3178
3528
  commitMessage: "",
@@ -3313,7 +3663,7 @@ init_logger();
3313
3663
  init_cleanup();
3314
3664
 
3315
3665
  // src/helpers/worktree.ts
3316
- import { join as join8, basename } from "path";
3666
+ import { join as join9, basename } from "path";
3317
3667
  import { execFile as execFile7 } from "child_process";
3318
3668
  import { promisify as promisify7 } from "util";
3319
3669
  import { randomUUID as randomUUID5 } from "crypto";
@@ -3332,7 +3682,7 @@ function worktreeName(issueFilename) {
3332
3682
  }
3333
3683
  async function createWorktree(repoRoot, issueFilename, branchName, startPoint) {
3334
3684
  const name = worktreeName(issueFilename);
3335
- const worktreePath = join8(repoRoot, WORKTREE_DIR, name);
3685
+ const worktreePath = join9(repoRoot, WORKTREE_DIR, name);
3336
3686
  try {
3337
3687
  const args = ["worktree", "add", worktreePath, "-b", branchName];
3338
3688
  if (startPoint) args.push(startPoint);
@@ -3351,7 +3701,7 @@ async function createWorktree(repoRoot, issueFilename, branchName, startPoint) {
3351
3701
  }
3352
3702
  async function removeWorktree(repoRoot, issueFilename) {
3353
3703
  const name = worktreeName(issueFilename);
3354
- const worktreePath = join8(repoRoot, WORKTREE_DIR, name);
3704
+ const worktreePath = join9(repoRoot, WORKTREE_DIR, name);
3355
3705
  try {
3356
3706
  await git2(["worktree", "remove", worktreePath], repoRoot);
3357
3707
  } catch {
@@ -3586,13 +3936,24 @@ function render(state) {
3586
3936
  return lines.join("\n");
3587
3937
  }
3588
3938
  function draw(state) {
3589
- if (lastLineCount > 0) {
3590
- process.stdout.write(`\x1B[${lastLineCount}A\x1B[0J`);
3591
- }
3592
3939
  const output = render(state);
3593
- process.stdout.write(output);
3594
3940
  const cols = process.stdout.columns || 80;
3595
- lastLineCount = countVisualRows(output, cols);
3941
+ const newLineCount = countVisualRows(output, cols);
3942
+ let buffer = "";
3943
+ if (lastLineCount > 0) {
3944
+ buffer += `\x1B[${lastLineCount}A`;
3945
+ }
3946
+ const lines = output.split("\n");
3947
+ buffer += lines.map((line) => line + "\x1B[K").join("\n");
3948
+ const leftover = lastLineCount - newLineCount;
3949
+ if (leftover > 0) {
3950
+ for (let i = 0; i < leftover; i++) {
3951
+ buffer += "\n\x1B[K";
3952
+ }
3953
+ buffer += `\x1B[${leftover}A`;
3954
+ }
3955
+ process.stdout.write(buffer);
3956
+ lastLineCount = newLineCount;
3596
3957
  }
3597
3958
  function createTui() {
3598
3959
  const state = {
@@ -3622,7 +3983,7 @@ init_providers();
3622
3983
 
3623
3984
  // src/orchestrator/datasource-helpers.ts
3624
3985
  init_logger();
3625
- import { basename as basename2, join as join9 } from "path";
3986
+ import { basename as basename2, join as join10 } from "path";
3626
3987
  import { mkdtemp, writeFile as writeFile7 } from "fs/promises";
3627
3988
  import { tmpdir } from "os";
3628
3989
  import { execFile as execFile8 } from "child_process";
@@ -3650,13 +4011,13 @@ async function fetchItemsById(issueIds, datasource4, fetchOpts) {
3650
4011
  return items;
3651
4012
  }
3652
4013
  async function writeItemsToTempDir(items) {
3653
- const tempDir = await mkdtemp(join9(tmpdir(), "dispatch-"));
4014
+ const tempDir = await mkdtemp(join10(tmpdir(), "dispatch-"));
3654
4015
  const files = [];
3655
4016
  const issueDetailsByFile = /* @__PURE__ */ new Map();
3656
4017
  for (const item of items) {
3657
4018
  const slug = slugify(item.title, MAX_SLUG_LENGTH);
3658
4019
  const filename = `${item.number}-${slug}.md`;
3659
- const filepath = join9(tempDir, filename);
4020
+ const filepath = join10(tempDir, filename);
3660
4021
  await writeFile7(filepath, item.body, "utf-8");
3661
4022
  files.push(filepath);
3662
4023
  issueDetailsByFile.set(filepath, item);
@@ -3669,34 +4030,6 @@ async function writeItemsToTempDir(items) {
3669
4030
  });
3670
4031
  return { files, issueDetailsByFile };
3671
4032
  }
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
4033
  async function getCommitSummaries(defaultBranch, cwd) {
3701
4034
  try {
3702
4035
  const { stdout } = await exec8(
@@ -3823,6 +4156,7 @@ function buildFeaturePrBody(issues, tasks, results, datasourceName) {
3823
4156
  // src/orchestrator/dispatch-pipeline.ts
3824
4157
  init_timeout();
3825
4158
  import chalk7 from "chalk";
4159
+ init_file_logger();
3826
4160
  var exec9 = promisify9(execFile9);
3827
4161
  var DEFAULT_PLAN_TIMEOUT_MIN = 10;
3828
4162
  var DEFAULT_PLAN_RETRIES = 1;
@@ -3842,6 +4176,8 @@ async function runDispatchPipeline(opts, cwd) {
3842
4176
  org,
3843
4177
  project,
3844
4178
  workItemType,
4179
+ iteration,
4180
+ area,
3845
4181
  planTimeout,
3846
4182
  planRetries,
3847
4183
  retries
@@ -3850,7 +4186,7 @@ async function runDispatchPipeline(opts, cwd) {
3850
4186
  const maxPlanAttempts = (planRetries ?? retries ?? DEFAULT_PLAN_RETRIES) + 1;
3851
4187
  log.debug(`Plan timeout: ${planTimeout ?? DEFAULT_PLAN_TIMEOUT_MIN}m (${planTimeoutMs}ms), max attempts: ${maxPlanAttempts}`);
3852
4188
  if (dryRun) {
3853
- return dryRunMode(issueIds, cwd, source, org, project, workItemType);
4189
+ return dryRunMode(issueIds, cwd, source, org, project, workItemType, iteration, area);
3854
4190
  }
3855
4191
  const verbose = log.verbose;
3856
4192
  let tui;
@@ -3886,7 +4222,7 @@ async function runDispatchPipeline(opts, cwd) {
3886
4222
  return { total: 0, completed: 0, failed: 0, skipped: 0, results: [] };
3887
4223
  }
3888
4224
  const datasource4 = getDatasource(source);
3889
- const fetchOpts = { cwd, org, project, workItemType };
4225
+ const fetchOpts = { cwd, org, project, workItemType, iteration, area };
3890
4226
  const items = issueIds.length > 0 ? await fetchItemsById(issueIds, datasource4, fetchOpts) : await datasource4.list(fetchOpts);
3891
4227
  if (items.length === 0) {
3892
4228
  tui.state.phase = "done";
@@ -3988,332 +4324,370 @@ async function runDispatchPipeline(opts, cwd) {
3988
4324
  }
3989
4325
  const processIssueFile = async (file, fileTasks) => {
3990
4326
  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);
4327
+ const fileLogger = verbose && details ? new FileLogger(details.number, cwd) : null;
4328
+ const body = async () => {
4329
+ let defaultBranch;
4330
+ let branchName;
4331
+ let worktreePath;
4332
+ let issueCwd = cwd;
4333
+ if (!noBranch && details) {
4334
+ fileLogger?.phase("Branch/worktree setup");
4335
+ try {
4336
+ defaultBranch = feature ? featureBranchName : await datasource4.getDefaultBranch(lifecycleOpts);
4337
+ branchName = datasource4.buildBranchName(details.number, details.title, username);
4338
+ if (useWorktrees) {
4339
+ worktreePath = await createWorktree(cwd, file, branchName, ...feature && featureBranchName ? [featureBranchName] : []);
4340
+ registerCleanup(async () => {
4341
+ await removeWorktree(cwd, file);
4342
+ });
4343
+ issueCwd = worktreePath;
4344
+ log.debug(`Created worktree for issue #${details.number} at ${worktreePath}`);
4345
+ fileLogger?.info(`Worktree created at ${worktreePath}`);
4346
+ const wtName = worktreeName(file);
4347
+ for (const task of fileTasks) {
4348
+ const tuiTask = tui.state.tasks.find((t) => t.task === task);
4349
+ if (tuiTask) tuiTask.worktree = wtName;
4350
+ }
4351
+ } else if (datasource4.supportsGit()) {
4352
+ await datasource4.createAndSwitchBranch(branchName, lifecycleOpts);
4353
+ log.debug(`Switched to branch ${branchName}`);
4354
+ fileLogger?.info(`Switched to branch ${branchName}`);
4355
+ }
4356
+ } catch (err) {
4357
+ const errorMsg = `Branch creation failed for issue #${details.number}: ${log.extractMessage(err)}`;
4358
+ fileLogger?.error(`Branch creation failed: ${log.extractMessage(err)}${err instanceof Error && err.stack ? `
4359
+ ${err.stack}` : ""}`);
4360
+ log.error(errorMsg);
4007
4361
  for (const task of fileTasks) {
4008
4362
  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;
4363
+ if (tuiTask) {
4364
+ tuiTask.status = "failed";
4365
+ tuiTask.error = errorMsg;
4366
+ }
4367
+ results.push({ task, success: false, error: errorMsg });
4023
4368
  }
4024
- results.push({ task, success: false, error: errorMsg });
4369
+ failed += fileTasks.length;
4370
+ return;
4025
4371
  }
4026
- failed += fileTasks.length;
4027
- return;
4028
4372
  }
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;
4373
+ const worktreeRoot = useWorktrees ? worktreePath : void 0;
4374
+ const issueLifecycleOpts = { cwd: issueCwd };
4375
+ fileLogger?.phase("Provider/agent boot");
4376
+ let localInstance;
4377
+ let localPlanner;
4378
+ let localExecutor;
4379
+ let localCommitAgent;
4380
+ if (useWorktrees) {
4381
+ localInstance = await bootProvider(provider, { url: serverUrl, cwd: issueCwd, model });
4382
+ registerCleanup(() => localInstance.cleanup());
4383
+ if (localInstance.model && !tui.state.model) {
4384
+ tui.state.model = localInstance.model;
4385
+ }
4386
+ if (verbose && localInstance.model) log.debug(`Model: ${localInstance.model}`);
4387
+ localPlanner = noPlan ? null : await boot6({ provider: localInstance, cwd: issueCwd });
4388
+ localExecutor = await boot7({ provider: localInstance, cwd: issueCwd });
4389
+ localCommitAgent = await boot8({ provider: localInstance, cwd: issueCwd });
4390
+ fileLogger?.info(`Provider booted: ${localInstance.model ?? provider}`);
4391
+ } else {
4392
+ localInstance = instance;
4393
+ localPlanner = planner;
4394
+ localExecutor = executor;
4395
+ localCommitAgent = commitAgent;
4041
4396
  }
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})`
4397
+ const groups = groupTasksByMode(fileTasks);
4398
+ const issueResults = [];
4399
+ for (const group of groups) {
4400
+ const groupQueue = [...group];
4401
+ while (groupQueue.length > 0) {
4402
+ const batch = groupQueue.splice(0, concurrency);
4403
+ const batchResults = await Promise.all(
4404
+ batch.map(async (task) => {
4405
+ const tuiTask = tui.state.tasks.find((t) => t.task === task);
4406
+ const startTime = Date.now();
4407
+ tuiTask.elapsed = startTime;
4408
+ let plan;
4409
+ if (localPlanner) {
4410
+ tuiTask.status = "planning";
4411
+ fileLogger?.phase(`Planning task: ${task.text}`);
4412
+ if (verbose) log.info(`Task #${tui.state.tasks.indexOf(tuiTask) + 1}: planning \u2014 "${task.text}"`);
4413
+ const rawContent = fileContentMap.get(task.file);
4414
+ const fileContext = rawContent ? buildTaskContext(rawContent, task) : void 0;
4415
+ let planResult;
4416
+ for (let attempt = 1; attempt <= maxPlanAttempts; attempt++) {
4417
+ try {
4418
+ planResult = await withTimeout(
4419
+ localPlanner.plan(task, fileContext, issueCwd, worktreeRoot),
4420
+ planTimeoutMs,
4421
+ "planner.plan()"
4082
4422
  );
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
4423
  break;
4424
+ } catch (err) {
4425
+ if (err instanceof TimeoutError) {
4426
+ log.warn(
4427
+ `Planning timed out for task "${task.text}" (attempt ${attempt}/${maxPlanAttempts})`
4428
+ );
4429
+ fileLogger?.warn(`Planning timeout (attempt ${attempt}/${maxPlanAttempts})`);
4430
+ if (attempt < maxPlanAttempts) {
4431
+ log.debug(`Retrying planning (attempt ${attempt + 1}/${maxPlanAttempts})`);
4432
+ fileLogger?.info(`Retrying planning (attempt ${attempt + 1}/${maxPlanAttempts})`);
4433
+ }
4434
+ } else {
4435
+ planResult = {
4436
+ data: null,
4437
+ success: false,
4438
+ error: log.extractMessage(err),
4439
+ durationMs: 0
4440
+ };
4441
+ break;
4442
+ }
4094
4443
  }
4095
4444
  }
4445
+ if (!planResult) {
4446
+ const timeoutMin = planTimeout ?? 10;
4447
+ planResult = {
4448
+ data: null,
4449
+ success: false,
4450
+ error: `Planning timed out after ${timeoutMin}m (${maxPlanAttempts} attempts)`,
4451
+ durationMs: 0
4452
+ };
4453
+ }
4454
+ if (!planResult.success) {
4455
+ tuiTask.status = "failed";
4456
+ tuiTask.error = `Planning failed: ${planResult.error}`;
4457
+ fileLogger?.error(`Planning failed: ${planResult.error}`);
4458
+ tuiTask.elapsed = Date.now() - startTime;
4459
+ if (verbose) log.error(`Task #${tui.state.tasks.indexOf(tuiTask) + 1}: failed \u2014 ${tuiTask.error} (${elapsed(tuiTask.elapsed)})`);
4460
+ failed++;
4461
+ return { task, success: false, error: tuiTask.error };
4462
+ }
4463
+ plan = planResult.data.prompt;
4464
+ fileLogger?.info(`Planning completed (${planResult.durationMs ?? 0}ms)`);
4096
4465
  }
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) {
4466
+ tuiTask.status = "running";
4467
+ fileLogger?.phase(`Executing task: ${task.text}`);
4468
+ if (verbose) log.info(`Task #${tui.state.tasks.indexOf(tuiTask) + 1}: executing \u2014 "${task.text}"`);
4469
+ const execRetries = 2;
4470
+ const execResult = await withRetry(
4471
+ async () => {
4472
+ const result = await localExecutor.execute({
4473
+ task,
4474
+ cwd: issueCwd,
4475
+ plan: plan ?? null,
4476
+ worktreeRoot
4477
+ });
4478
+ if (!result.success) {
4479
+ throw new Error(result.error ?? "Execution failed");
4480
+ }
4481
+ return result;
4482
+ },
4483
+ execRetries,
4484
+ { label: `executor "${task.text}"` }
4485
+ ).catch((err) => ({
4486
+ data: null,
4487
+ success: false,
4488
+ error: log.extractMessage(err),
4489
+ durationMs: 0
4490
+ }));
4491
+ if (execResult.success) {
4492
+ fileLogger?.info(`Execution completed successfully (${Date.now() - startTime}ms)`);
4493
+ try {
4494
+ const parsed = parseIssueFilename(task.file);
4495
+ if (parsed) {
4496
+ const updatedContent = await readFile7(task.file, "utf-8");
4497
+ const issueDetails = issueDetailsByFile.get(task.file);
4498
+ const title = issueDetails?.title ?? parsed.slug;
4499
+ await datasource4.update(parsed.issueId, title, updatedContent, fetchOpts);
4500
+ log.success(`Synced task completion to issue #${parsed.issueId}`);
4501
+ }
4502
+ } catch (err) {
4503
+ log.warn(`Could not sync task completion to datasource: ${log.formatErrorChain(err)}`);
4504
+ }
4505
+ tuiTask.status = "done";
4506
+ tuiTask.elapsed = Date.now() - startTime;
4507
+ if (verbose) log.success(`Task #${tui.state.tasks.indexOf(tuiTask) + 1}: done \u2014 "${task.text}" (${elapsed(tuiTask.elapsed)})`);
4508
+ completed++;
4509
+ } else {
4510
+ fileLogger?.error(`Execution failed: ${execResult.error}`);
4107
4511
  tuiTask.status = "failed";
4108
- tuiTask.error = `Planning failed: ${planResult.error}`;
4512
+ tuiTask.error = execResult.error;
4109
4513
  tuiTask.elapsed = Date.now() - startTime;
4110
- if (verbose) log.error(`Task #${tui.state.tasks.indexOf(tuiTask) + 1}: failed \u2014 ${tuiTask.error} (${elapsed(tuiTask.elapsed)})`);
4514
+ if (verbose) log.error(`Task #${tui.state.tasks.indexOf(tuiTask) + 1}: failed \u2014 "${task.text}" (${elapsed(tuiTask.elapsed)})${tuiTask.error ? `: ${tuiTask.error}` : ""}`);
4111
4515
  failed++;
4112
- return { task, success: false, error: tuiTask.error };
4113
4516
  }
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) {
4517
+ const dispatchResult = execResult.success ? execResult.data.dispatchResult : {
4518
+ task,
4519
+ success: false,
4520
+ error: execResult.error ?? "Executor failed without returning a dispatch result."
4521
+ };
4522
+ return dispatchResult;
4523
+ })
4524
+ );
4525
+ issueResults.push(...batchResults);
4526
+ if (!tui.state.model && localInstance.model) {
4527
+ tui.state.model = localInstance.model;
4528
+ }
4529
+ }
4530
+ }
4531
+ results.push(...issueResults);
4532
+ if (!noBranch && branchName && defaultBranch && details && datasource4.supportsGit()) {
4533
+ try {
4534
+ await datasource4.commitAllChanges(
4535
+ `chore: stage uncommitted changes for issue #${details.number}`,
4536
+ issueLifecycleOpts
4537
+ );
4538
+ log.debug(`Staged uncommitted changes for issue #${details.number}`);
4539
+ } catch (err) {
4540
+ log.warn(`Could not commit uncommitted changes for issue #${details.number}: ${log.formatErrorChain(err)}`);
4541
+ }
4542
+ }
4543
+ fileLogger?.phase("Commit generation");
4544
+ let commitAgentResult;
4545
+ if (!noBranch && branchName && defaultBranch && details && datasource4.supportsGit()) {
4546
+ try {
4547
+ const branchDiff = await getBranchDiff(defaultBranch, issueCwd);
4548
+ if (branchDiff) {
4549
+ const result = await localCommitAgent.generate({
4550
+ branchDiff,
4551
+ issue: details,
4552
+ taskResults: issueResults,
4553
+ cwd: issueCwd,
4554
+ worktreeRoot
4555
+ });
4556
+ if (result.success) {
4557
+ commitAgentResult = result;
4558
+ fileLogger?.info(`Commit message generated for issue #${details.number}`);
4141
4559
  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
- }
4560
+ await squashBranchCommits(defaultBranch, result.commitMessage, issueCwd);
4561
+ log.debug(`Rewrote commit message for issue #${details.number}`);
4562
+ fileLogger?.info(`Rewrote commit history for issue #${details.number}`);
4150
4563
  } catch (err) {
4151
- log.warn(`Could not sync task completion to datasource: ${log.formatErrorChain(err)}`);
4564
+ log.warn(`Could not rewrite commit message for issue #${details.number}: ${log.formatErrorChain(err)}`);
4152
4565
  }
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
4566
  } 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++;
4567
+ log.warn(`Commit agent failed for issue #${details.number}: ${result.error}`);
4568
+ fileLogger?.warn(`Commit agent failed: ${result.error}`);
4163
4569
  }
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;
4570
+ }
4571
+ } catch (err) {
4572
+ log.warn(`Commit agent error for issue #${details.number}: ${log.formatErrorChain(err)}`);
4175
4573
  }
4176
4574
  }
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;
4575
+ fileLogger?.phase("PR lifecycle");
4576
+ if (!noBranch && branchName && defaultBranch && details) {
4577
+ if (feature && featureBranchName) {
4578
+ if (worktreePath) {
4204
4579
  try {
4205
- await squashBranchCommits(defaultBranch, result.commitMessage, issueCwd);
4206
- log.debug(`Rewrote commit message for issue #${details.number}`);
4580
+ await removeWorktree(cwd, file);
4207
4581
  } catch (err) {
4208
- log.warn(`Could not rewrite commit message for issue #${details.number}: ${log.formatErrorChain(err)}`);
4582
+ log.warn(`Could not remove worktree for issue #${details.number}: ${log.formatErrorChain(err)}`);
4209
4583
  }
4210
- } else {
4211
- log.warn(`Commit agent failed for issue #${details.number}: ${result.error}`);
4212
4584
  }
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
4585
  try {
4222
- await removeWorktree(cwd, file);
4586
+ await datasource4.switchBranch(featureBranchName, lifecycleOpts);
4587
+ await exec9("git", ["merge", branchName, "--no-ff", "-m", `merge: issue #${details.number}`], { cwd });
4588
+ log.debug(`Merged ${branchName} into ${featureBranchName}`);
4223
4589
  } 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;
4590
+ const mergeError = `Could not merge ${branchName} into feature branch: ${log.formatErrorChain(err)}`;
4591
+ log.warn(mergeError);
4592
+ try {
4593
+ await exec9("git", ["merge", "--abort"], { cwd });
4594
+ } catch {
4243
4595
  }
4244
- const existingResult = results.find((r) => r.task === task);
4245
- if (existingResult) {
4246
- existingResult.success = false;
4247
- existingResult.error = mergeError;
4596
+ for (const task of fileTasks) {
4597
+ const tuiTask = tui.state.tasks.find((t) => t.task === task);
4598
+ if (tuiTask) {
4599
+ tuiTask.status = "failed";
4600
+ tuiTask.error = mergeError;
4601
+ }
4602
+ const existingResult = results.find((r) => r.task === task);
4603
+ if (existingResult) {
4604
+ existingResult.success = false;
4605
+ existingResult.error = mergeError;
4606
+ }
4248
4607
  }
4608
+ return;
4249
4609
  }
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
4610
  try {
4266
- await datasource4.pushBranch(branchName, issueLifecycleOpts);
4267
- log.debug(`Pushed branch ${branchName}`);
4611
+ await exec9("git", ["branch", "-d", branchName], { cwd });
4612
+ log.debug(`Deleted local branch ${branchName}`);
4268
4613
  } catch (err) {
4269
- log.warn(`Could not push branch ${branchName}: ${log.formatErrorChain(err)}`);
4614
+ log.warn(`Could not delete local branch ${branchName}: ${log.formatErrorChain(err)}`);
4270
4615
  }
4271
- }
4272
- if (datasource4.supportsGit()) {
4273
4616
  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
- }
4617
+ await datasource4.switchBranch(featureDefaultBranch, lifecycleOpts);
4293
4618
  } catch (err) {
4294
- log.warn(`Could not create PR for issue #${details.number}: ${log.formatErrorChain(err)}`);
4619
+ log.warn(`Could not switch back to ${featureDefaultBranch}: ${log.formatErrorChain(err)}`);
4295
4620
  }
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)}`);
4621
+ } else {
4622
+ if (datasource4.supportsGit()) {
4623
+ try {
4624
+ await datasource4.pushBranch(branchName, issueLifecycleOpts);
4625
+ log.debug(`Pushed branch ${branchName}`);
4626
+ fileLogger?.info(`Pushed branch ${branchName}`);
4627
+ } catch (err) {
4628
+ log.warn(`Could not push branch ${branchName}: ${log.formatErrorChain(err)}`);
4629
+ }
4302
4630
  }
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)}`);
4631
+ if (datasource4.supportsGit()) {
4632
+ try {
4633
+ const prTitle = commitAgentResult?.prTitle || await buildPrTitle(details.title, defaultBranch, issueLifecycleOpts.cwd);
4634
+ const prBody = commitAgentResult?.prDescription || await buildPrBody(
4635
+ details,
4636
+ fileTasks,
4637
+ issueResults,
4638
+ defaultBranch,
4639
+ datasource4.name,
4640
+ issueLifecycleOpts.cwd
4641
+ );
4642
+ const prUrl = await datasource4.createPullRequest(
4643
+ branchName,
4644
+ details.number,
4645
+ prTitle,
4646
+ prBody,
4647
+ issueLifecycleOpts
4648
+ );
4649
+ if (prUrl) {
4650
+ log.success(`Created PR for issue #${details.number}: ${prUrl}`);
4651
+ fileLogger?.info(`Created PR: ${prUrl}`);
4652
+ }
4653
+ } catch (err) {
4654
+ log.warn(`Could not create PR for issue #${details.number}: ${log.formatErrorChain(err)}`);
4655
+ fileLogger?.warn(`PR creation failed: ${log.extractMessage(err)}`);
4656
+ }
4657
+ }
4658
+ if (useWorktrees && worktreePath) {
4659
+ try {
4660
+ await removeWorktree(cwd, file);
4661
+ } catch (err) {
4662
+ log.warn(`Could not remove worktree for issue #${details.number}: ${log.formatErrorChain(err)}`);
4663
+ }
4664
+ } else if (!useWorktrees && datasource4.supportsGit()) {
4665
+ try {
4666
+ await datasource4.switchBranch(defaultBranch, lifecycleOpts);
4667
+ log.debug(`Switched back to ${defaultBranch}`);
4668
+ } catch (err) {
4669
+ log.warn(`Could not switch back to ${defaultBranch}: ${log.formatErrorChain(err)}`);
4670
+ }
4309
4671
  }
4310
4672
  }
4311
4673
  }
4312
- }
4313
- if (useWorktrees) {
4314
- await localExecutor.cleanup();
4315
- await localPlanner?.cleanup();
4316
- await localInstance.cleanup();
4674
+ fileLogger?.phase("Resource cleanup");
4675
+ if (useWorktrees) {
4676
+ await localExecutor.cleanup();
4677
+ await localPlanner?.cleanup();
4678
+ await localInstance.cleanup();
4679
+ }
4680
+ };
4681
+ if (fileLogger) {
4682
+ await fileLoggerStorage.run(fileLogger, async () => {
4683
+ try {
4684
+ await body();
4685
+ } finally {
4686
+ fileLogger.close();
4687
+ }
4688
+ });
4689
+ } else {
4690
+ await body();
4317
4691
  }
4318
4692
  };
4319
4693
  if (useWorktrees && !feature) {
@@ -4364,11 +4738,6 @@ async function runDispatchPipeline(opts, cwd) {
4364
4738
  log.warn(`Could not switch back to ${featureDefaultBranch}: ${log.formatErrorChain(err)}`);
4365
4739
  }
4366
4740
  }
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
4741
  await commitAgent?.cleanup();
4373
4742
  await executor?.cleanup();
4374
4743
  await planner?.cleanup();
@@ -4382,13 +4751,13 @@ async function runDispatchPipeline(opts, cwd) {
4382
4751
  throw err;
4383
4752
  }
4384
4753
  }
4385
- async function dryRunMode(issueIds, cwd, source, org, project, workItemType) {
4754
+ async function dryRunMode(issueIds, cwd, source, org, project, workItemType, iteration, area) {
4386
4755
  if (!source) {
4387
4756
  log.error("No datasource configured. Use --source or run 'dispatch config' to set up defaults.");
4388
4757
  return { total: 0, completed: 0, failed: 0, skipped: 0, results: [] };
4389
4758
  }
4390
4759
  const datasource4 = getDatasource(source);
4391
- const fetchOpts = { cwd, org, project, workItemType };
4760
+ const fetchOpts = { cwd, org, project, workItemType, iteration, area };
4392
4761
  const lifecycleOpts = { cwd };
4393
4762
  let username = "";
4394
4763
  try {
@@ -4493,6 +4862,8 @@ async function boot9(opts) {
4493
4862
  org: m.org,
4494
4863
  project: m.project,
4495
4864
  workItemType: m.workItemType,
4865
+ iteration: m.iteration,
4866
+ area: m.area,
4496
4867
  concurrency: m.concurrency,
4497
4868
  dryRun: m.dryRun
4498
4869
  });
@@ -4507,7 +4878,7 @@ async function boot9(opts) {
4507
4878
  process.exit(1);
4508
4879
  }
4509
4880
  const datasource4 = getDatasource(source);
4510
- const existing = await datasource4.list({ cwd: m.cwd, org: m.org, project: m.project, workItemType: m.workItemType });
4881
+ const existing = await datasource4.list({ cwd: m.cwd, org: m.org, project: m.project, workItemType: m.workItemType, iteration: m.iteration, area: m.area });
4511
4882
  if (existing.length === 0) {
4512
4883
  log.error("No existing specs found to regenerate");
4513
4884
  process.exit(1);
@@ -4533,6 +4904,8 @@ async function boot9(opts) {
4533
4904
  org: m.org,
4534
4905
  project: m.project,
4535
4906
  workItemType: m.workItemType,
4907
+ iteration: m.iteration,
4908
+ area: m.area,
4536
4909
  concurrency: m.concurrency,
4537
4910
  dryRun: m.dryRun
4538
4911
  });
@@ -4551,6 +4924,8 @@ async function boot9(opts) {
4551
4924
  org: m.org,
4552
4925
  project: m.project,
4553
4926
  workItemType: m.workItemType,
4927
+ iteration: m.iteration,
4928
+ area: m.area,
4554
4929
  planTimeout: m.planTimeout,
4555
4930
  planRetries: m.planRetries,
4556
4931
  retries: m.retries,
@@ -4634,187 +5009,156 @@ var HELP = `
4634
5009
  dispatch config
4635
5010
  `.trimStart();
4636
5011
  function parseArgs(argv) {
5012
+ const program = new Command();
5013
+ program.exitOverride().configureOutput({
5014
+ writeOut: () => {
5015
+ },
5016
+ writeErr: () => {
5017
+ }
5018
+ }).helpOption(false).argument("[issueIds...]").option("-h, --help", "Show help").option("-v, --version", "Show version").option("--dry-run", "List tasks without dispatching").option("--no-plan", "Skip the planner agent").option("--no-branch", "Skip branch creation").option("--no-worktree", "Skip git worktree isolation").option("--feature", "Group issues into a single feature branch").option("--force", "Ignore prior run state").option("--verbose", "Show detailed debug output").option("--fix-tests", "Run tests and fix failures").option("--spec <values...>", "Spec mode: issue numbers, glob, or text").option("--respec [values...]", "Regenerate specs").addOption(
5019
+ new Option("--provider <name>", "Agent backend").choices(PROVIDER_NAMES)
5020
+ ).addOption(
5021
+ new Option("--source <name>", "Issue source").choices(
5022
+ DATASOURCE_NAMES
5023
+ )
5024
+ ).option(
5025
+ "--concurrency <n>",
5026
+ "Max parallel dispatches",
5027
+ (val) => {
5028
+ const n = parseInt(val, 10);
5029
+ if (isNaN(n) || n < 1) throw new CommanderError(1, "commander.invalidArgument", "--concurrency must be a positive integer");
5030
+ if (n > MAX_CONCURRENCY) throw new CommanderError(1, "commander.invalidArgument", `--concurrency must not exceed ${MAX_CONCURRENCY}`);
5031
+ return n;
5032
+ }
5033
+ ).option(
5034
+ "--plan-timeout <min>",
5035
+ "Planning timeout in minutes",
5036
+ (val) => {
5037
+ const n = parseFloat(val);
5038
+ if (isNaN(n) || n < CONFIG_BOUNDS.planTimeout.min) throw new CommanderError(1, "commander.invalidArgument", "--plan-timeout must be a positive number (minutes)");
5039
+ if (n > CONFIG_BOUNDS.planTimeout.max) throw new CommanderError(1, "commander.invalidArgument", `--plan-timeout must not exceed ${CONFIG_BOUNDS.planTimeout.max}`);
5040
+ return n;
5041
+ }
5042
+ ).option(
5043
+ "--retries <n>",
5044
+ "Retry attempts",
5045
+ (val) => {
5046
+ const n = parseInt(val, 10);
5047
+ if (isNaN(n) || n < 0) throw new CommanderError(1, "commander.invalidArgument", "--retries must be a non-negative integer");
5048
+ return n;
5049
+ }
5050
+ ).option(
5051
+ "--plan-retries <n>",
5052
+ "Planner retry attempts",
5053
+ (val) => {
5054
+ const n = parseInt(val, 10);
5055
+ if (isNaN(n) || n < 0) throw new CommanderError(1, "commander.invalidArgument", "--plan-retries must be a non-negative integer");
5056
+ return n;
5057
+ }
5058
+ ).option(
5059
+ "--test-timeout <min>",
5060
+ "Test timeout in minutes",
5061
+ (val) => {
5062
+ const n = parseFloat(val);
5063
+ if (isNaN(n) || n <= 0) throw new CommanderError(1, "commander.invalidArgument", "--test-timeout must be a positive number (minutes)");
5064
+ return n;
5065
+ }
5066
+ ).option("--cwd <dir>", "Working directory", (val) => resolve3(val)).option("--output-dir <dir>", "Output directory", (val) => resolve3(val)).option("--org <url>", "Azure DevOps organization URL").option("--project <name>", "Azure DevOps project name").option("--server-url <url>", "Provider server URL");
5067
+ try {
5068
+ program.parse(argv, { from: "user" });
5069
+ } catch (err) {
5070
+ if (err instanceof CommanderError) {
5071
+ log.error(err.message);
5072
+ process.exit(1);
5073
+ }
5074
+ throw err;
5075
+ }
5076
+ const opts = program.opts();
4637
5077
  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
5078
+ issueIds: program.args,
5079
+ dryRun: opts.dryRun ?? false,
5080
+ noPlan: !opts.plan,
5081
+ noBranch: !opts.branch,
5082
+ noWorktree: !opts.worktree,
5083
+ force: opts.force ?? false,
5084
+ provider: opts.provider ?? "opencode",
5085
+ cwd: opts.cwd ?? process.cwd(),
5086
+ help: opts.help ?? false,
5087
+ version: opts.version ?? false,
5088
+ verbose: opts.verbose ?? false
4649
5089
  };
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);
5090
+ if (opts.spec !== void 0) {
5091
+ args.spec = opts.spec.length === 1 ? opts.spec[0] : opts.spec;
5092
+ }
5093
+ if (opts.respec !== void 0) {
5094
+ if (opts.respec === true) {
5095
+ args.respec = [];
4799
5096
  } else {
4800
- log.error(`Unknown option: ${arg}`);
4801
- process.exit(1);
5097
+ args.respec = opts.respec.length === 1 ? opts.respec[0] : opts.respec;
5098
+ }
5099
+ }
5100
+ if (opts.fixTests) args.fixTests = true;
5101
+ if (opts.feature) args.feature = true;
5102
+ if (opts.source !== void 0) args.issueSource = opts.source;
5103
+ if (opts.concurrency !== void 0) args.concurrency = opts.concurrency;
5104
+ if (opts.serverUrl !== void 0) args.serverUrl = opts.serverUrl;
5105
+ if (opts.planTimeout !== void 0) args.planTimeout = opts.planTimeout;
5106
+ if (opts.retries !== void 0) args.retries = opts.retries;
5107
+ if (opts.planRetries !== void 0) args.planRetries = opts.planRetries;
5108
+ if (opts.testTimeout !== void 0) args.testTimeout = opts.testTimeout;
5109
+ if (opts.org !== void 0) args.org = opts.org;
5110
+ if (opts.project !== void 0) args.project = opts.project;
5111
+ if (opts.outputDir !== void 0) args.outputDir = opts.outputDir;
5112
+ const explicitFlags = /* @__PURE__ */ new Set();
5113
+ const SOURCE_MAP = {
5114
+ help: "help",
5115
+ version: "version",
5116
+ dryRun: "dryRun",
5117
+ plan: "noPlan",
5118
+ branch: "noBranch",
5119
+ worktree: "noWorktree",
5120
+ force: "force",
5121
+ verbose: "verbose",
5122
+ spec: "spec",
5123
+ respec: "respec",
5124
+ fixTests: "fixTests",
5125
+ feature: "feature",
5126
+ source: "issueSource",
5127
+ provider: "provider",
5128
+ concurrency: "concurrency",
5129
+ serverUrl: "serverUrl",
5130
+ planTimeout: "planTimeout",
5131
+ retries: "retries",
5132
+ planRetries: "planRetries",
5133
+ testTimeout: "testTimeout",
5134
+ cwd: "cwd",
5135
+ org: "org",
5136
+ project: "project",
5137
+ outputDir: "outputDir"
5138
+ };
5139
+ for (const [attr, flag] of Object.entries(SOURCE_MAP)) {
5140
+ if (program.getOptionValueSource(attr) === "cli") {
5141
+ explicitFlags.add(flag);
4802
5142
  }
4803
- i++;
4804
5143
  }
4805
5144
  return [args, explicitFlags];
4806
5145
  }
4807
5146
  async function main() {
4808
5147
  const rawArgv = process.argv.slice(2);
4809
5148
  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;
5149
+ const configProgram = new Command("dispatch-config").exitOverride().configureOutput({ writeOut: () => {
5150
+ }, writeErr: () => {
5151
+ } }).helpOption(false).allowUnknownOption(true).allowExcessArguments(true).option("--cwd <dir>", "Working directory", (v) => resolve3(v));
5152
+ try {
5153
+ configProgram.parse(rawArgv.slice(1), { from: "user" });
5154
+ } catch (err) {
5155
+ if (err instanceof CommanderError) {
5156
+ log.error(err.message);
5157
+ process.exit(1);
4815
5158
  }
5159
+ throw err;
4816
5160
  }
4817
- const configDir = join11(cwd, ".dispatch");
5161
+ const configDir = join12(configProgram.opts().cwd ?? process.cwd(), ".dispatch");
4818
5162
  await handleConfigCommand(rawArgv.slice(1), configDir);
4819
5163
  process.exit(0);
4820
5164
  }
@@ -4835,7 +5179,7 @@ async function main() {
4835
5179
  process.exit(0);
4836
5180
  }
4837
5181
  if (args.version) {
4838
- console.log(`dispatch v${"1.2.1"}`);
5182
+ console.log(`dispatch v${"1.3.1"}`);
4839
5183
  process.exit(0);
4840
5184
  }
4841
5185
  const orchestrator = await boot9({ cwd: args.cwd });