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