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