@pruddiman/dispatch 1.3.1 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +749 -453
- package/dist/cli.js.map +1 -1
- package/package.json +2 -4
package/dist/cli.js
CHANGED
|
@@ -210,6 +210,27 @@ var init_logger = __esm({
|
|
|
210
210
|
}
|
|
211
211
|
});
|
|
212
212
|
|
|
213
|
+
// src/helpers/cleanup.ts
|
|
214
|
+
function registerCleanup(fn) {
|
|
215
|
+
cleanups.push(fn);
|
|
216
|
+
}
|
|
217
|
+
async function runCleanup() {
|
|
218
|
+
const fns = cleanups.splice(0);
|
|
219
|
+
for (const fn of fns) {
|
|
220
|
+
try {
|
|
221
|
+
await fn();
|
|
222
|
+
} catch {
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
var cleanups;
|
|
227
|
+
var init_cleanup = __esm({
|
|
228
|
+
"src/helpers/cleanup.ts"() {
|
|
229
|
+
"use strict";
|
|
230
|
+
cleanups = [];
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
|
|
213
234
|
// src/helpers/guards.ts
|
|
214
235
|
function hasProperty(value, key) {
|
|
215
236
|
return typeof value === "object" && value !== null && Object.prototype.hasOwnProperty.call(value, key);
|
|
@@ -422,7 +443,7 @@ var init_opencode = __esm({
|
|
|
422
443
|
|
|
423
444
|
// src/helpers/timeout.ts
|
|
424
445
|
function withTimeout(promise, ms, label) {
|
|
425
|
-
const p = new Promise((
|
|
446
|
+
const p = new Promise((resolve5, reject) => {
|
|
426
447
|
let settled = false;
|
|
427
448
|
const timer = setTimeout(() => {
|
|
428
449
|
if (settled) return;
|
|
@@ -434,7 +455,7 @@ function withTimeout(promise, ms, label) {
|
|
|
434
455
|
if (settled) return;
|
|
435
456
|
settled = true;
|
|
436
457
|
clearTimeout(timer);
|
|
437
|
-
|
|
458
|
+
resolve5(value);
|
|
438
459
|
},
|
|
439
460
|
(err) => {
|
|
440
461
|
if (settled) return;
|
|
@@ -542,9 +563,9 @@ async function boot2(opts) {
|
|
|
542
563
|
let unsubErr;
|
|
543
564
|
try {
|
|
544
565
|
await withTimeout(
|
|
545
|
-
new Promise((
|
|
566
|
+
new Promise((resolve5, reject) => {
|
|
546
567
|
unsubIdle = session.on("session.idle", () => {
|
|
547
|
-
|
|
568
|
+
resolve5();
|
|
548
569
|
});
|
|
549
570
|
unsubErr = session.on("session.error", (event) => {
|
|
550
571
|
reject(new Error(`Copilot session error: ${event.data.message}`));
|
|
@@ -592,7 +613,7 @@ var init_copilot = __esm({
|
|
|
592
613
|
});
|
|
593
614
|
|
|
594
615
|
// src/providers/claude.ts
|
|
595
|
-
import { randomUUID } from "crypto";
|
|
616
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
596
617
|
import { unstable_v2_createSession } from "@anthropic-ai/claude-agent-sdk";
|
|
597
618
|
async function listModels3(_opts) {
|
|
598
619
|
return [
|
|
@@ -615,7 +636,7 @@ async function boot3(opts) {
|
|
|
615
636
|
try {
|
|
616
637
|
const sessionOpts = { model, permissionMode: "acceptEdits", ...cwd ? { cwd } : {} };
|
|
617
638
|
const session = unstable_v2_createSession(sessionOpts);
|
|
618
|
-
const sessionId =
|
|
639
|
+
const sessionId = randomUUID2();
|
|
619
640
|
sessions.set(sessionId, session);
|
|
620
641
|
log.debug(`Session created: ${sessionId}`);
|
|
621
642
|
return sessionId;
|
|
@@ -667,7 +688,7 @@ var init_claude = __esm({
|
|
|
667
688
|
});
|
|
668
689
|
|
|
669
690
|
// src/providers/codex.ts
|
|
670
|
-
import { randomUUID as
|
|
691
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
671
692
|
async function loadAgentLoop() {
|
|
672
693
|
return import("@openai/codex");
|
|
673
694
|
}
|
|
@@ -689,7 +710,7 @@ async function boot4(opts) {
|
|
|
689
710
|
async createSession() {
|
|
690
711
|
log.debug("Creating Codex session...");
|
|
691
712
|
try {
|
|
692
|
-
const sessionId =
|
|
713
|
+
const sessionId = randomUUID3();
|
|
693
714
|
const agent = new AgentLoop({
|
|
694
715
|
model,
|
|
695
716
|
config: { model, instructions: "" },
|
|
@@ -756,23 +777,25 @@ var init_codex = __esm({
|
|
|
756
777
|
});
|
|
757
778
|
|
|
758
779
|
// src/providers/detect.ts
|
|
759
|
-
import { execFile as
|
|
760
|
-
import { promisify as
|
|
780
|
+
import { execFile as execFile8 } from "child_process";
|
|
781
|
+
import { promisify as promisify8 } from "util";
|
|
761
782
|
async function checkProviderInstalled(name) {
|
|
762
783
|
try {
|
|
763
|
-
await
|
|
764
|
-
shell: process.platform === "win32"
|
|
784
|
+
await exec8(PROVIDER_BINARIES[name], ["--version"], {
|
|
785
|
+
shell: process.platform === "win32",
|
|
786
|
+
timeout: DETECTION_TIMEOUT_MS
|
|
765
787
|
});
|
|
766
788
|
return true;
|
|
767
789
|
} catch {
|
|
768
790
|
return false;
|
|
769
791
|
}
|
|
770
792
|
}
|
|
771
|
-
var
|
|
793
|
+
var exec8, DETECTION_TIMEOUT_MS, PROVIDER_BINARIES;
|
|
772
794
|
var init_detect = __esm({
|
|
773
795
|
"src/providers/detect.ts"() {
|
|
774
796
|
"use strict";
|
|
775
|
-
|
|
797
|
+
exec8 = promisify8(execFile8);
|
|
798
|
+
DETECTION_TIMEOUT_MS = 5e3;
|
|
776
799
|
PROVIDER_BINARIES = {
|
|
777
800
|
opencode: "opencode",
|
|
778
801
|
copilot: "copilot",
|
|
@@ -826,24 +849,32 @@ var init_providers = __esm({
|
|
|
826
849
|
}
|
|
827
850
|
});
|
|
828
851
|
|
|
829
|
-
// src/helpers/
|
|
830
|
-
function
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
}
|
|
852
|
+
// src/helpers/environment.ts
|
|
853
|
+
function getEnvironmentInfo() {
|
|
854
|
+
const platform = process.platform;
|
|
855
|
+
switch (platform) {
|
|
856
|
+
case "win32":
|
|
857
|
+
return { platform, os: "Windows", shell: "cmd.exe/PowerShell" };
|
|
858
|
+
case "darwin":
|
|
859
|
+
return { platform, os: "macOS", shell: "zsh/bash" };
|
|
860
|
+
default:
|
|
861
|
+
return { platform, os: "Linux", shell: "bash" };
|
|
840
862
|
}
|
|
841
863
|
}
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
864
|
+
function formatEnvironmentPrompt() {
|
|
865
|
+
const env = getEnvironmentInfo();
|
|
866
|
+
return [
|
|
867
|
+
`## Environment`,
|
|
868
|
+
`- **Operating System:** ${env.os}`,
|
|
869
|
+
`- **Default Shell:** ${env.shell}`,
|
|
870
|
+
`- Always run commands directly in the shell. Do NOT write intermediate scripts (e.g. .bat, .ps1, .py files) unless the task explicitly requires creating a script.`
|
|
871
|
+
].join("\n");
|
|
872
|
+
}
|
|
873
|
+
var getEnvironmentBlock;
|
|
874
|
+
var init_environment = __esm({
|
|
875
|
+
"src/helpers/environment.ts"() {
|
|
845
876
|
"use strict";
|
|
846
|
-
|
|
877
|
+
getEnvironmentBlock = formatEnvironmentPrompt;
|
|
847
878
|
}
|
|
848
879
|
});
|
|
849
880
|
|
|
@@ -880,15 +911,15 @@ async function detectTestCommand(cwd) {
|
|
|
880
911
|
}
|
|
881
912
|
}
|
|
882
913
|
function runTestCommand(command, cwd) {
|
|
883
|
-
return new Promise((
|
|
914
|
+
return new Promise((resolve5) => {
|
|
884
915
|
const [cmd, ...args] = command.split(" ");
|
|
885
916
|
execFileCb(
|
|
886
917
|
cmd,
|
|
887
918
|
args,
|
|
888
|
-
{ cwd, maxBuffer: 10 * 1024 * 1024 },
|
|
919
|
+
{ cwd, maxBuffer: 10 * 1024 * 1024, shell: process.platform === "win32" },
|
|
889
920
|
(error, stdout, stderr) => {
|
|
890
921
|
const exitCode = error && "code" in error ? error.code ?? 1 : error ? 1 : 0;
|
|
891
|
-
|
|
922
|
+
resolve5({ exitCode, stdout, stderr, command });
|
|
892
923
|
}
|
|
893
924
|
);
|
|
894
925
|
});
|
|
@@ -902,6 +933,8 @@ function buildFixTestsPrompt(testResult, cwd) {
|
|
|
902
933
|
`**Test command:** ${testResult.command}`,
|
|
903
934
|
`**Exit code:** ${testResult.exitCode}`,
|
|
904
935
|
``,
|
|
936
|
+
formatEnvironmentPrompt(),
|
|
937
|
+
``,
|
|
905
938
|
`## Test Output`,
|
|
906
939
|
``,
|
|
907
940
|
"```",
|
|
@@ -1003,11 +1036,12 @@ var init_fix_tests_pipeline = __esm({
|
|
|
1003
1036
|
init_cleanup();
|
|
1004
1037
|
init_logger();
|
|
1005
1038
|
init_file_logger();
|
|
1039
|
+
init_environment();
|
|
1006
1040
|
}
|
|
1007
1041
|
});
|
|
1008
1042
|
|
|
1009
1043
|
// src/cli.ts
|
|
1010
|
-
import { resolve as
|
|
1044
|
+
import { resolve as resolve4, join as join12 } from "path";
|
|
1011
1045
|
import { Command, Option, CommanderError } from "commander";
|
|
1012
1046
|
|
|
1013
1047
|
// src/spec-generator.ts
|
|
@@ -1054,11 +1088,11 @@ function isValidBranchName(name) {
|
|
|
1054
1088
|
// src/datasources/github.ts
|
|
1055
1089
|
var exec = promisify(execFile);
|
|
1056
1090
|
async function git(args, cwd) {
|
|
1057
|
-
const { stdout } = await exec("git", args, { cwd });
|
|
1091
|
+
const { stdout } = await exec("git", args, { cwd, shell: process.platform === "win32" });
|
|
1058
1092
|
return stdout;
|
|
1059
1093
|
}
|
|
1060
1094
|
async function gh(args, cwd) {
|
|
1061
|
-
const { stdout } = await exec("gh", args, { cwd });
|
|
1095
|
+
const { stdout } = await exec("gh", args, { cwd, shell: process.platform === "win32" });
|
|
1062
1096
|
return stdout;
|
|
1063
1097
|
}
|
|
1064
1098
|
function buildBranchName(issueNumber, title, username = "unknown") {
|
|
@@ -1104,7 +1138,7 @@ var datasource = {
|
|
|
1104
1138
|
"--json",
|
|
1105
1139
|
"number,title,body,labels,state,url"
|
|
1106
1140
|
],
|
|
1107
|
-
{ cwd }
|
|
1141
|
+
{ cwd, shell: process.platform === "win32" }
|
|
1108
1142
|
);
|
|
1109
1143
|
let issues;
|
|
1110
1144
|
try {
|
|
@@ -1136,7 +1170,7 @@ var datasource = {
|
|
|
1136
1170
|
"--json",
|
|
1137
1171
|
"number,title,body,labels,state,url,comments"
|
|
1138
1172
|
],
|
|
1139
|
-
{ cwd }
|
|
1173
|
+
{ cwd, shell: process.platform === "win32" }
|
|
1140
1174
|
);
|
|
1141
1175
|
let issue;
|
|
1142
1176
|
try {
|
|
@@ -1164,18 +1198,18 @@ var datasource = {
|
|
|
1164
1198
|
},
|
|
1165
1199
|
async update(issueId, title, body, opts = {}) {
|
|
1166
1200
|
const cwd = opts.cwd || process.cwd();
|
|
1167
|
-
await exec("gh", ["issue", "edit", issueId, "--title", title, "--body", body], { cwd });
|
|
1201
|
+
await exec("gh", ["issue", "edit", issueId, "--title", title, "--body", body], { cwd, shell: process.platform === "win32" });
|
|
1168
1202
|
},
|
|
1169
1203
|
async close(issueId, opts = {}) {
|
|
1170
1204
|
const cwd = opts.cwd || process.cwd();
|
|
1171
|
-
await exec("gh", ["issue", "close", issueId], { cwd });
|
|
1205
|
+
await exec("gh", ["issue", "close", issueId], { cwd, shell: process.platform === "win32" });
|
|
1172
1206
|
},
|
|
1173
1207
|
async create(title, body, opts = {}) {
|
|
1174
1208
|
const cwd = opts.cwd || process.cwd();
|
|
1175
1209
|
const { stdout } = await exec(
|
|
1176
1210
|
"gh",
|
|
1177
1211
|
["issue", "create", "--title", title, "--body", body],
|
|
1178
|
-
{ cwd }
|
|
1212
|
+
{ cwd, shell: process.platform === "win32" }
|
|
1179
1213
|
);
|
|
1180
1214
|
const url = stdout.trim();
|
|
1181
1215
|
const match = url.match(/\/issues\/(\d+)$/);
|
|
@@ -1271,6 +1305,7 @@ import { execFile as execFile2 } from "child_process";
|
|
|
1271
1305
|
import { promisify as promisify2 } from "util";
|
|
1272
1306
|
init_logger();
|
|
1273
1307
|
var exec2 = promisify2(execFile2);
|
|
1308
|
+
var doneStateCache = /* @__PURE__ */ new Map();
|
|
1274
1309
|
function mapWorkItemToIssueDetails(item, id, comments, defaults) {
|
|
1275
1310
|
const fields = item.fields ?? {};
|
|
1276
1311
|
return {
|
|
@@ -1296,7 +1331,8 @@ async function detectWorkItemType(opts = {}) {
|
|
|
1296
1331
|
if (opts.project) args.push("--project", opts.project);
|
|
1297
1332
|
if (opts.org) args.push("--org", opts.org);
|
|
1298
1333
|
const { stdout } = await exec2("az", args, {
|
|
1299
|
-
cwd: opts.cwd || process.cwd()
|
|
1334
|
+
cwd: opts.cwd || process.cwd(),
|
|
1335
|
+
shell: process.platform === "win32"
|
|
1300
1336
|
});
|
|
1301
1337
|
const types = JSON.parse(stdout);
|
|
1302
1338
|
if (!Array.isArray(types) || types.length === 0) return null;
|
|
@@ -1310,6 +1346,48 @@ async function detectWorkItemType(opts = {}) {
|
|
|
1310
1346
|
return null;
|
|
1311
1347
|
}
|
|
1312
1348
|
}
|
|
1349
|
+
async function detectDoneState(workItemType, opts = {}) {
|
|
1350
|
+
const cacheKey = `${opts.org ?? ""}|${opts.project ?? ""}|${workItemType}`;
|
|
1351
|
+
const cached = doneStateCache.get(cacheKey);
|
|
1352
|
+
if (cached) return cached;
|
|
1353
|
+
try {
|
|
1354
|
+
const args = [
|
|
1355
|
+
"boards",
|
|
1356
|
+
"work-item",
|
|
1357
|
+
"type",
|
|
1358
|
+
"state",
|
|
1359
|
+
"list",
|
|
1360
|
+
"--type",
|
|
1361
|
+
workItemType,
|
|
1362
|
+
"--output",
|
|
1363
|
+
"json"
|
|
1364
|
+
];
|
|
1365
|
+
if (opts.project) args.push("--project", opts.project);
|
|
1366
|
+
if (opts.org) args.push("--org", opts.org);
|
|
1367
|
+
const { stdout } = await exec2("az", args, {
|
|
1368
|
+
cwd: opts.cwd || process.cwd(),
|
|
1369
|
+
shell: process.platform === "win32"
|
|
1370
|
+
});
|
|
1371
|
+
const states = JSON.parse(stdout);
|
|
1372
|
+
if (Array.isArray(states)) {
|
|
1373
|
+
const completed = states.find((s) => s.category === "Completed");
|
|
1374
|
+
if (completed) {
|
|
1375
|
+
doneStateCache.set(cacheKey, completed.name);
|
|
1376
|
+
return completed.name;
|
|
1377
|
+
}
|
|
1378
|
+
const names = states.map((s) => s.name);
|
|
1379
|
+
const fallbacks = ["Done", "Closed", "Resolved", "Completed"];
|
|
1380
|
+
for (const f of fallbacks) {
|
|
1381
|
+
if (names.includes(f)) {
|
|
1382
|
+
doneStateCache.set(cacheKey, f);
|
|
1383
|
+
return f;
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
} catch {
|
|
1388
|
+
}
|
|
1389
|
+
return "Closed";
|
|
1390
|
+
}
|
|
1313
1391
|
var datasource2 = {
|
|
1314
1392
|
name: "azdevops",
|
|
1315
1393
|
supportsGit() {
|
|
@@ -1318,6 +1396,7 @@ var datasource2 = {
|
|
|
1318
1396
|
async list(opts = {}) {
|
|
1319
1397
|
const conditions = [
|
|
1320
1398
|
"[System.State] <> 'Closed'",
|
|
1399
|
+
"[System.State] <> 'Done'",
|
|
1321
1400
|
"[System.State] <> 'Removed'"
|
|
1322
1401
|
];
|
|
1323
1402
|
if (opts.iteration) {
|
|
@@ -1340,7 +1419,8 @@ var datasource2 = {
|
|
|
1340
1419
|
if (opts.org) args.push("--org", opts.org);
|
|
1341
1420
|
if (opts.project) args.push("--project", opts.project);
|
|
1342
1421
|
const { stdout } = await exec2("az", args, {
|
|
1343
|
-
cwd: opts.cwd || process.cwd()
|
|
1422
|
+
cwd: opts.cwd || process.cwd(),
|
|
1423
|
+
shell: process.platform === "win32"
|
|
1344
1424
|
});
|
|
1345
1425
|
let data;
|
|
1346
1426
|
try {
|
|
@@ -1362,9 +1442,9 @@ var datasource2 = {
|
|
|
1362
1442
|
"json"
|
|
1363
1443
|
];
|
|
1364
1444
|
if (opts.org) batchArgs.push("--org", opts.org);
|
|
1365
|
-
if (opts.project) batchArgs.push("--project", opts.project);
|
|
1366
1445
|
const { stdout: batchStdout } = await exec2("az", batchArgs, {
|
|
1367
|
-
cwd: opts.cwd || process.cwd()
|
|
1446
|
+
cwd: opts.cwd || process.cwd(),
|
|
1447
|
+
shell: process.platform === "win32"
|
|
1368
1448
|
});
|
|
1369
1449
|
let batchItems;
|
|
1370
1450
|
try {
|
|
@@ -1406,11 +1486,9 @@ var datasource2 = {
|
|
|
1406
1486
|
if (opts.org) {
|
|
1407
1487
|
args.push("--org", opts.org);
|
|
1408
1488
|
}
|
|
1409
|
-
if (opts.project) {
|
|
1410
|
-
args.push("--project", opts.project);
|
|
1411
|
-
}
|
|
1412
1489
|
const { stdout } = await exec2("az", args, {
|
|
1413
|
-
cwd: opts.cwd || process.cwd()
|
|
1490
|
+
cwd: opts.cwd || process.cwd(),
|
|
1491
|
+
shell: process.platform === "win32"
|
|
1414
1492
|
});
|
|
1415
1493
|
let item;
|
|
1416
1494
|
try {
|
|
@@ -1434,10 +1512,33 @@ var datasource2 = {
|
|
|
1434
1512
|
body
|
|
1435
1513
|
];
|
|
1436
1514
|
if (opts.org) args.push("--org", opts.org);
|
|
1437
|
-
|
|
1438
|
-
await exec2("az", args, { cwd: opts.cwd || process.cwd() });
|
|
1515
|
+
await exec2("az", args, { cwd: opts.cwd || process.cwd(), shell: process.platform === "win32" });
|
|
1439
1516
|
},
|
|
1440
1517
|
async close(issueId, opts = {}) {
|
|
1518
|
+
let workItemType = opts.workItemType;
|
|
1519
|
+
if (!workItemType) {
|
|
1520
|
+
const showArgs = [
|
|
1521
|
+
"boards",
|
|
1522
|
+
"work-item",
|
|
1523
|
+
"show",
|
|
1524
|
+
"--id",
|
|
1525
|
+
issueId,
|
|
1526
|
+
"--output",
|
|
1527
|
+
"json"
|
|
1528
|
+
];
|
|
1529
|
+
if (opts.org) showArgs.push("--org", opts.org);
|
|
1530
|
+
const { stdout } = await exec2("az", showArgs, {
|
|
1531
|
+
cwd: opts.cwd || process.cwd(),
|
|
1532
|
+
shell: process.platform === "win32"
|
|
1533
|
+
});
|
|
1534
|
+
try {
|
|
1535
|
+
const item = JSON.parse(stdout);
|
|
1536
|
+
workItemType = item.fields?.["System.WorkItemType"] ?? void 0;
|
|
1537
|
+
} catch {
|
|
1538
|
+
workItemType = void 0;
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
const state = workItemType ? await detectDoneState(workItemType, opts) : "Closed";
|
|
1441
1542
|
const args = [
|
|
1442
1543
|
"boards",
|
|
1443
1544
|
"work-item",
|
|
@@ -1445,11 +1546,10 @@ var datasource2 = {
|
|
|
1445
1546
|
"--id",
|
|
1446
1547
|
issueId,
|
|
1447
1548
|
"--state",
|
|
1448
|
-
|
|
1549
|
+
state
|
|
1449
1550
|
];
|
|
1450
1551
|
if (opts.org) args.push("--org", opts.org);
|
|
1451
|
-
|
|
1452
|
-
await exec2("az", args, { cwd: opts.cwd || process.cwd() });
|
|
1552
|
+
await exec2("az", args, { cwd: opts.cwd || process.cwd(), shell: process.platform === "win32" });
|
|
1453
1553
|
},
|
|
1454
1554
|
async create(title, body, opts = {}) {
|
|
1455
1555
|
const workItemType = opts.workItemType ?? await detectWorkItemType(opts);
|
|
@@ -1474,7 +1574,8 @@ var datasource2 = {
|
|
|
1474
1574
|
if (opts.org) args.push("--org", opts.org);
|
|
1475
1575
|
if (opts.project) args.push("--project", opts.project);
|
|
1476
1576
|
const { stdout } = await exec2("az", args, {
|
|
1477
|
-
cwd: opts.cwd || process.cwd()
|
|
1577
|
+
cwd: opts.cwd || process.cwd(),
|
|
1578
|
+
shell: process.platform === "win32"
|
|
1478
1579
|
});
|
|
1479
1580
|
let item;
|
|
1480
1581
|
try {
|
|
@@ -1491,7 +1592,7 @@ var datasource2 = {
|
|
|
1491
1592
|
},
|
|
1492
1593
|
async getDefaultBranch(opts) {
|
|
1493
1594
|
try {
|
|
1494
|
-
const { stdout } = await exec2("git", ["symbolic-ref", "refs/remotes/origin/HEAD"], { cwd: opts.cwd });
|
|
1595
|
+
const { stdout } = await exec2("git", ["symbolic-ref", "refs/remotes/origin/HEAD"], { cwd: opts.cwd, shell: process.platform === "win32" });
|
|
1495
1596
|
const parts = stdout.trim().split("/");
|
|
1496
1597
|
const branch = parts[parts.length - 1];
|
|
1497
1598
|
if (!isValidBranchName(branch)) {
|
|
@@ -1503,7 +1604,7 @@ var datasource2 = {
|
|
|
1503
1604
|
throw err;
|
|
1504
1605
|
}
|
|
1505
1606
|
try {
|
|
1506
|
-
await exec2("git", ["rev-parse", "--verify", "main"], { cwd: opts.cwd });
|
|
1607
|
+
await exec2("git", ["rev-parse", "--verify", "main"], { cwd: opts.cwd, shell: process.platform === "win32" });
|
|
1507
1608
|
return "main";
|
|
1508
1609
|
} catch {
|
|
1509
1610
|
return "master";
|
|
@@ -1512,19 +1613,19 @@ var datasource2 = {
|
|
|
1512
1613
|
},
|
|
1513
1614
|
async getUsername(opts) {
|
|
1514
1615
|
try {
|
|
1515
|
-
const { stdout } = await exec2("git", ["config", "user.name"], { cwd: opts.cwd });
|
|
1616
|
+
const { stdout } = await exec2("git", ["config", "user.name"], { cwd: opts.cwd, shell: process.platform === "win32" });
|
|
1516
1617
|
const name = slugify(stdout.trim());
|
|
1517
1618
|
if (name) return name;
|
|
1518
1619
|
} catch {
|
|
1519
1620
|
}
|
|
1520
1621
|
try {
|
|
1521
|
-
const { stdout } = await exec2("az", ["account", "show", "--query", "user.name", "-o", "tsv"], { cwd: opts.cwd });
|
|
1622
|
+
const { stdout } = await exec2("az", ["account", "show", "--query", "user.name", "-o", "tsv"], { cwd: opts.cwd, shell: process.platform === "win32" });
|
|
1522
1623
|
const name = slugify(stdout.trim());
|
|
1523
1624
|
if (name) return name;
|
|
1524
1625
|
} catch {
|
|
1525
1626
|
}
|
|
1526
1627
|
try {
|
|
1527
|
-
const { stdout } = await exec2("az", ["account", "show", "--query", "user.principalName", "-o", "tsv"], { cwd: opts.cwd });
|
|
1628
|
+
const { stdout } = await exec2("az", ["account", "show", "--query", "user.principalName", "-o", "tsv"], { cwd: opts.cwd, shell: process.platform === "win32" });
|
|
1528
1629
|
const principal = stdout.trim();
|
|
1529
1630
|
const prefix = principal.split("@")[0];
|
|
1530
1631
|
const name = slugify(prefix);
|
|
@@ -1546,29 +1647,29 @@ var datasource2 = {
|
|
|
1546
1647
|
throw new InvalidBranchNameError(branchName);
|
|
1547
1648
|
}
|
|
1548
1649
|
try {
|
|
1549
|
-
await exec2("git", ["checkout", "-b", branchName], { cwd: opts.cwd });
|
|
1650
|
+
await exec2("git", ["checkout", "-b", branchName], { cwd: opts.cwd, shell: process.platform === "win32" });
|
|
1550
1651
|
} catch (err) {
|
|
1551
1652
|
const message = log.extractMessage(err);
|
|
1552
1653
|
if (message.includes("already exists")) {
|
|
1553
|
-
await exec2("git", ["checkout", branchName], { cwd: opts.cwd });
|
|
1654
|
+
await exec2("git", ["checkout", branchName], { cwd: opts.cwd, shell: process.platform === "win32" });
|
|
1554
1655
|
} else {
|
|
1555
1656
|
throw err;
|
|
1556
1657
|
}
|
|
1557
1658
|
}
|
|
1558
1659
|
},
|
|
1559
1660
|
async switchBranch(branchName, opts) {
|
|
1560
|
-
await exec2("git", ["checkout", branchName], { cwd: opts.cwd });
|
|
1661
|
+
await exec2("git", ["checkout", branchName], { cwd: opts.cwd, shell: process.platform === "win32" });
|
|
1561
1662
|
},
|
|
1562
1663
|
async pushBranch(branchName, opts) {
|
|
1563
|
-
await exec2("git", ["push", "--set-upstream", "origin", branchName], { cwd: opts.cwd });
|
|
1664
|
+
await exec2("git", ["push", "--set-upstream", "origin", branchName], { cwd: opts.cwd, shell: process.platform === "win32" });
|
|
1564
1665
|
},
|
|
1565
1666
|
async commitAllChanges(message, opts) {
|
|
1566
|
-
await exec2("git", ["add", "-A"], { cwd: opts.cwd });
|
|
1567
|
-
const { stdout } = await exec2("git", ["diff", "--cached", "--stat"], { cwd: opts.cwd });
|
|
1667
|
+
await exec2("git", ["add", "-A"], { cwd: opts.cwd, shell: process.platform === "win32" });
|
|
1668
|
+
const { stdout } = await exec2("git", ["diff", "--cached", "--stat"], { cwd: opts.cwd, shell: process.platform === "win32" });
|
|
1568
1669
|
if (!stdout.trim()) {
|
|
1569
1670
|
return;
|
|
1570
1671
|
}
|
|
1571
|
-
await exec2("git", ["commit", "-m", message], { cwd: opts.cwd });
|
|
1672
|
+
await exec2("git", ["commit", "-m", message], { cwd: opts.cwd, shell: process.platform === "win32" });
|
|
1572
1673
|
},
|
|
1573
1674
|
async createPullRequest(branchName, issueNumber, title, body, opts) {
|
|
1574
1675
|
try {
|
|
@@ -1589,7 +1690,7 @@ var datasource2 = {
|
|
|
1589
1690
|
"--output",
|
|
1590
1691
|
"json"
|
|
1591
1692
|
],
|
|
1592
|
-
{ cwd: opts.cwd }
|
|
1693
|
+
{ cwd: opts.cwd, shell: process.platform === "win32" }
|
|
1593
1694
|
);
|
|
1594
1695
|
let pr;
|
|
1595
1696
|
try {
|
|
@@ -1614,7 +1715,7 @@ var datasource2 = {
|
|
|
1614
1715
|
"--output",
|
|
1615
1716
|
"json"
|
|
1616
1717
|
],
|
|
1617
|
-
{ cwd: opts.cwd }
|
|
1718
|
+
{ cwd: opts.cwd, shell: process.platform === "win32" }
|
|
1618
1719
|
);
|
|
1619
1720
|
let prs;
|
|
1620
1721
|
try {
|
|
@@ -1646,11 +1747,9 @@ async function fetchComments(workItemId, opts) {
|
|
|
1646
1747
|
if (opts.org) {
|
|
1647
1748
|
args.push("--org", opts.org);
|
|
1648
1749
|
}
|
|
1649
|
-
if (opts.project) {
|
|
1650
|
-
args.push("--project", opts.project);
|
|
1651
|
-
}
|
|
1652
1750
|
const { stdout } = await exec2("az", args, {
|
|
1653
|
-
cwd: opts.cwd || process.cwd()
|
|
1751
|
+
cwd: opts.cwd || process.cwd(),
|
|
1752
|
+
shell: process.platform === "win32"
|
|
1654
1753
|
});
|
|
1655
1754
|
const data = JSON.parse(stdout);
|
|
1656
1755
|
if (data.comments && Array.isArray(data.comments)) {
|
|
@@ -1670,8 +1769,9 @@ async function fetchComments(workItemId, opts) {
|
|
|
1670
1769
|
// src/datasources/md.ts
|
|
1671
1770
|
import { execFile as execFile3 } from "child_process";
|
|
1672
1771
|
import { readFile, writeFile, readdir, mkdir, rename } from "fs/promises";
|
|
1673
|
-
import { join as join2, parse as parsePath } from "path";
|
|
1772
|
+
import { basename, dirname as dirname2, isAbsolute, join as join2, parse as parsePath, resolve } from "path";
|
|
1674
1773
|
import { promisify as promisify3 } from "util";
|
|
1774
|
+
import { glob } from "glob";
|
|
1675
1775
|
|
|
1676
1776
|
// src/helpers/errors.ts
|
|
1677
1777
|
var UnsupportedOperationError = class extends Error {
|
|
@@ -1692,6 +1792,15 @@ function resolveDir(opts) {
|
|
|
1692
1792
|
const cwd = opts?.cwd ?? process.cwd();
|
|
1693
1793
|
return join2(cwd, DEFAULT_DIR);
|
|
1694
1794
|
}
|
|
1795
|
+
function resolveFilePath(issueId, opts) {
|
|
1796
|
+
const filename = issueId.endsWith(".md") ? issueId : `${issueId}.md`;
|
|
1797
|
+
if (isAbsolute(filename)) return filename;
|
|
1798
|
+
if (/[/\\]/.test(filename)) {
|
|
1799
|
+
const cwd = opts?.cwd ?? process.cwd();
|
|
1800
|
+
return resolve(cwd, filename);
|
|
1801
|
+
}
|
|
1802
|
+
return join2(resolveDir(opts), filename);
|
|
1803
|
+
}
|
|
1695
1804
|
function extractTitle(content, filename) {
|
|
1696
1805
|
const match = content.match(/^#\s+(.+)$/m);
|
|
1697
1806
|
if (match) return match[1].trim();
|
|
@@ -1726,6 +1835,19 @@ var datasource3 = {
|
|
|
1726
1835
|
return false;
|
|
1727
1836
|
},
|
|
1728
1837
|
async list(opts) {
|
|
1838
|
+
if (opts?.pattern) {
|
|
1839
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
1840
|
+
const files = await glob(opts.pattern, { cwd, absolute: true });
|
|
1841
|
+
const mdFiles2 = files.filter((f) => f.endsWith(".md")).sort();
|
|
1842
|
+
const results2 = [];
|
|
1843
|
+
for (const filePath of mdFiles2) {
|
|
1844
|
+
const content = await readFile(filePath, "utf-8");
|
|
1845
|
+
const filename = basename(filePath);
|
|
1846
|
+
const dir2 = dirname2(filePath);
|
|
1847
|
+
results2.push(toIssueDetails(filename, content, dir2));
|
|
1848
|
+
}
|
|
1849
|
+
return results2;
|
|
1850
|
+
}
|
|
1729
1851
|
const dir = resolveDir(opts);
|
|
1730
1852
|
let entries;
|
|
1731
1853
|
try {
|
|
@@ -1743,23 +1865,20 @@ var datasource3 = {
|
|
|
1743
1865
|
return results;
|
|
1744
1866
|
},
|
|
1745
1867
|
async fetch(issueId, opts) {
|
|
1746
|
-
const
|
|
1747
|
-
const filename = issueId.endsWith(".md") ? issueId : `${issueId}.md`;
|
|
1748
|
-
const filePath = join2(dir, filename);
|
|
1868
|
+
const filePath = resolveFilePath(issueId, opts);
|
|
1749
1869
|
const content = await readFile(filePath, "utf-8");
|
|
1870
|
+
const filename = basename(filePath);
|
|
1871
|
+
const dir = dirname2(filePath);
|
|
1750
1872
|
return toIssueDetails(filename, content, dir);
|
|
1751
1873
|
},
|
|
1752
1874
|
async update(issueId, _title, body, opts) {
|
|
1753
|
-
const
|
|
1754
|
-
const filename = issueId.endsWith(".md") ? issueId : `${issueId}.md`;
|
|
1755
|
-
const filePath = join2(dir, filename);
|
|
1875
|
+
const filePath = resolveFilePath(issueId, opts);
|
|
1756
1876
|
await writeFile(filePath, body, "utf-8");
|
|
1757
1877
|
},
|
|
1758
1878
|
async close(issueId, opts) {
|
|
1759
|
-
const
|
|
1760
|
-
const filename =
|
|
1761
|
-
const
|
|
1762
|
-
const archiveDir = join2(dir, "archive");
|
|
1879
|
+
const filePath = resolveFilePath(issueId, opts);
|
|
1880
|
+
const filename = basename(filePath);
|
|
1881
|
+
const archiveDir = join2(dirname2(filePath), "archive");
|
|
1763
1882
|
await mkdir(archiveDir, { recursive: true });
|
|
1764
1883
|
await rename(filePath, join2(archiveDir, filename));
|
|
1765
1884
|
},
|
|
@@ -1776,7 +1895,7 @@ var datasource3 = {
|
|
|
1776
1895
|
},
|
|
1777
1896
|
async getUsername(opts) {
|
|
1778
1897
|
try {
|
|
1779
|
-
const { stdout } = await exec3("git", ["config", "user.name"], { cwd: opts.cwd });
|
|
1898
|
+
const { stdout } = await exec3("git", ["config", "user.name"], { cwd: opts.cwd, shell: process.platform === "win32" });
|
|
1780
1899
|
const name = stdout.trim();
|
|
1781
1900
|
if (!name) return "local";
|
|
1782
1901
|
return slugify(name);
|
|
@@ -1825,7 +1944,8 @@ function getDatasource(name) {
|
|
|
1825
1944
|
async function getGitRemoteUrl(cwd) {
|
|
1826
1945
|
try {
|
|
1827
1946
|
const { stdout } = await exec4("git", ["remote", "get-url", "origin"], {
|
|
1828
|
-
cwd
|
|
1947
|
+
cwd,
|
|
1948
|
+
shell: process.platform === "win32"
|
|
1829
1949
|
});
|
|
1830
1950
|
return stdout.trim() || null;
|
|
1831
1951
|
} catch {
|
|
@@ -1990,100 +2110,336 @@ async function resolveSource(issues, issueSource, cwd) {
|
|
|
1990
2110
|
return null;
|
|
1991
2111
|
}
|
|
1992
2112
|
|
|
1993
|
-
// src/orchestrator/
|
|
1994
|
-
init_logger();
|
|
1995
|
-
|
|
1996
|
-
// src/helpers/confirm-large-batch.ts
|
|
2113
|
+
// src/orchestrator/datasource-helpers.ts
|
|
1997
2114
|
init_logger();
|
|
1998
|
-
import {
|
|
1999
|
-
import
|
|
2000
|
-
|
|
2001
|
-
async function confirmLargeBatch(count, threshold = LARGE_BATCH_THRESHOLD) {
|
|
2002
|
-
if (count <= threshold) return true;
|
|
2003
|
-
log.warn(
|
|
2004
|
-
`This operation will process ${chalk2.bold(String(count))} specs, which exceeds the safety threshold of ${threshold}.`
|
|
2005
|
-
);
|
|
2006
|
-
const answer = await input({
|
|
2007
|
-
message: `Type ${chalk2.bold('"yes"')} to proceed:`
|
|
2008
|
-
});
|
|
2009
|
-
return answer.trim().toLowerCase() === "yes";
|
|
2010
|
-
}
|
|
2011
|
-
|
|
2012
|
-
// src/helpers/prereqs.ts
|
|
2115
|
+
import { basename as basename2, join as join3 } from "path";
|
|
2116
|
+
import { mkdtemp, writeFile as writeFile2 } from "fs/promises";
|
|
2117
|
+
import { tmpdir } from "os";
|
|
2013
2118
|
import { execFile as execFile5 } from "child_process";
|
|
2014
2119
|
import { promisify as promisify5 } from "util";
|
|
2015
2120
|
var exec5 = promisify5(execFile5);
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
const
|
|
2019
|
-
|
|
2121
|
+
function parseIssueFilename(filePath) {
|
|
2122
|
+
const filename = basename2(filePath);
|
|
2123
|
+
const match = /^(\d+)-(.+)\.md$/.exec(filename);
|
|
2124
|
+
if (!match) return null;
|
|
2125
|
+
return { issueId: match[1], slug: match[2] };
|
|
2020
2126
|
}
|
|
2021
|
-
function
|
|
2022
|
-
const
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2127
|
+
async function fetchItemsById(issueIds, datasource4, fetchOpts) {
|
|
2128
|
+
const ids = issueIds.flatMap(
|
|
2129
|
+
(id) => id.split(",").map((s) => s.trim()).filter(Boolean)
|
|
2130
|
+
);
|
|
2131
|
+
const items = [];
|
|
2132
|
+
for (const id of ids) {
|
|
2133
|
+
try {
|
|
2134
|
+
const item = await datasource4.fetch(id, fetchOpts);
|
|
2135
|
+
items.push(item);
|
|
2136
|
+
} catch (err) {
|
|
2137
|
+
const prefix = id.includes("/") || id.includes("\\") || id.endsWith(".md") ? "" : "#";
|
|
2138
|
+
log.warn(`Could not fetch issue ${prefix}${id}: ${log.formatErrorChain(err)}`);
|
|
2139
|
+
}
|
|
2140
|
+
}
|
|
2141
|
+
return items;
|
|
2027
2142
|
}
|
|
2028
|
-
async function
|
|
2029
|
-
const
|
|
2143
|
+
async function writeItemsToTempDir(items) {
|
|
2144
|
+
const tempDir = await mkdtemp(join3(tmpdir(), "dispatch-"));
|
|
2145
|
+
const files = [];
|
|
2146
|
+
const issueDetailsByFile = /* @__PURE__ */ new Map();
|
|
2147
|
+
for (const item of items) {
|
|
2148
|
+
const slug = slugify(item.title, MAX_SLUG_LENGTH);
|
|
2149
|
+
const filename = `${item.number}-${slug}.md`;
|
|
2150
|
+
const filepath = join3(tempDir, filename);
|
|
2151
|
+
await writeFile2(filepath, item.body, "utf-8");
|
|
2152
|
+
files.push(filepath);
|
|
2153
|
+
issueDetailsByFile.set(filepath, item);
|
|
2154
|
+
}
|
|
2155
|
+
files.sort((a, b) => {
|
|
2156
|
+
const numA = parseInt(basename2(a).match(/^(\d+)/)?.[1] ?? "0", 10);
|
|
2157
|
+
const numB = parseInt(basename2(b).match(/^(\d+)/)?.[1] ?? "0", 10);
|
|
2158
|
+
if (numA !== numB) return numA - numB;
|
|
2159
|
+
return a.localeCompare(b);
|
|
2160
|
+
});
|
|
2161
|
+
return { files, issueDetailsByFile };
|
|
2162
|
+
}
|
|
2163
|
+
async function getCommitSummaries(defaultBranch, cwd) {
|
|
2030
2164
|
try {
|
|
2031
|
-
|
|
2165
|
+
const { stdout } = await exec5(
|
|
2166
|
+
"git",
|
|
2167
|
+
["log", `${defaultBranch}..HEAD`, "--pretty=format:%s"],
|
|
2168
|
+
{ cwd, shell: process.platform === "win32" }
|
|
2169
|
+
);
|
|
2170
|
+
return stdout.trim().split("\n").filter(Boolean);
|
|
2032
2171
|
} catch {
|
|
2033
|
-
|
|
2172
|
+
return [];
|
|
2034
2173
|
}
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2174
|
+
}
|
|
2175
|
+
async function getBranchDiff(defaultBranch, cwd) {
|
|
2176
|
+
try {
|
|
2177
|
+
const { stdout } = await exec5(
|
|
2178
|
+
"git",
|
|
2179
|
+
["diff", `${defaultBranch}..HEAD`],
|
|
2180
|
+
{ cwd, maxBuffer: 10 * 1024 * 1024, shell: process.platform === "win32" }
|
|
2039
2181
|
);
|
|
2182
|
+
return stdout;
|
|
2183
|
+
} catch {
|
|
2184
|
+
return "";
|
|
2040
2185
|
}
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2186
|
+
}
|
|
2187
|
+
async function squashBranchCommits(defaultBranch, message, cwd) {
|
|
2188
|
+
const { stdout } = await exec5(
|
|
2189
|
+
"git",
|
|
2190
|
+
["merge-base", defaultBranch, "HEAD"],
|
|
2191
|
+
{ cwd, shell: process.platform === "win32" }
|
|
2192
|
+
);
|
|
2193
|
+
const mergeBase = stdout.trim();
|
|
2194
|
+
await exec5("git", ["reset", "--soft", mergeBase], { cwd, shell: process.platform === "win32" });
|
|
2195
|
+
await exec5("git", ["commit", "-m", message], { cwd, shell: process.platform === "win32" });
|
|
2196
|
+
}
|
|
2197
|
+
async function buildPrBody(details, tasks, results, defaultBranch, datasourceName, cwd) {
|
|
2198
|
+
const sections = [];
|
|
2199
|
+
const commits = await getCommitSummaries(defaultBranch, cwd);
|
|
2200
|
+
if (commits.length > 0) {
|
|
2201
|
+
sections.push("## Summary\n");
|
|
2202
|
+
for (const commit of commits) {
|
|
2203
|
+
sections.push(`- ${commit}`);
|
|
2048
2204
|
}
|
|
2205
|
+
sections.push("");
|
|
2049
2206
|
}
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2207
|
+
const taskResults = new Map(
|
|
2208
|
+
results.filter((r) => tasks.includes(r.task)).map((r) => [r.task, r])
|
|
2209
|
+
);
|
|
2210
|
+
const completedTasks = tasks.filter((t) => taskResults.get(t)?.success);
|
|
2211
|
+
const failedTasks = tasks.filter((t) => {
|
|
2212
|
+
const r = taskResults.get(t);
|
|
2213
|
+
return r && !r.success;
|
|
2214
|
+
});
|
|
2215
|
+
if (completedTasks.length > 0 || failedTasks.length > 0) {
|
|
2216
|
+
sections.push("## Tasks\n");
|
|
2217
|
+
for (const task of completedTasks) {
|
|
2218
|
+
sections.push(`- [x] ${task.text}`);
|
|
2057
2219
|
}
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
}
|
|
2061
|
-
|
|
2062
|
-
// src/helpers/gitignore.ts
|
|
2063
|
-
init_logger();
|
|
2064
|
-
import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
|
|
2065
|
-
import { join as join3 } from "path";
|
|
2066
|
-
async function ensureGitignoreEntry(repoRoot, entry) {
|
|
2067
|
-
const gitignorePath = join3(repoRoot, ".gitignore");
|
|
2068
|
-
let contents = "";
|
|
2069
|
-
try {
|
|
2070
|
-
contents = await readFile2(gitignorePath, "utf8");
|
|
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;
|
|
2220
|
+
for (const task of failedTasks) {
|
|
2221
|
+
sections.push(`- [ ] ${task.text}`);
|
|
2076
2222
|
}
|
|
2223
|
+
sections.push("");
|
|
2224
|
+
}
|
|
2225
|
+
if (details.labels.length > 0) {
|
|
2226
|
+
sections.push(`**Labels:** ${details.labels.join(", ")}
|
|
2227
|
+
`);
|
|
2228
|
+
}
|
|
2229
|
+
if (datasourceName === "github") {
|
|
2230
|
+
sections.push(`Closes #${details.number}`);
|
|
2231
|
+
} else if (datasourceName === "azdevops") {
|
|
2232
|
+
sections.push(`Resolves AB#${details.number}`);
|
|
2233
|
+
}
|
|
2234
|
+
return sections.join("\n");
|
|
2235
|
+
}
|
|
2236
|
+
async function buildPrTitle(issueTitle, defaultBranch, cwd) {
|
|
2237
|
+
const commits = await getCommitSummaries(defaultBranch, cwd);
|
|
2238
|
+
if (commits.length === 0) {
|
|
2239
|
+
return issueTitle;
|
|
2240
|
+
}
|
|
2241
|
+
if (commits.length === 1) {
|
|
2242
|
+
return commits[0];
|
|
2243
|
+
}
|
|
2244
|
+
return `${commits[commits.length - 1]} (+${commits.length - 1} more)`;
|
|
2245
|
+
}
|
|
2246
|
+
function buildFeaturePrTitle(featureBranchName, issues) {
|
|
2247
|
+
if (issues.length === 1) {
|
|
2248
|
+
return issues[0].title;
|
|
2249
|
+
}
|
|
2250
|
+
const issueRefs = issues.map((d) => `#${d.number}`).join(", ");
|
|
2251
|
+
return `feat: ${featureBranchName} (${issueRefs})`;
|
|
2252
|
+
}
|
|
2253
|
+
function buildFeaturePrBody(issues, tasks, results, datasourceName) {
|
|
2254
|
+
const sections = [];
|
|
2255
|
+
sections.push("## Issues\n");
|
|
2256
|
+
for (const issue of issues) {
|
|
2257
|
+
sections.push(`- #${issue.number}: ${issue.title}`);
|
|
2258
|
+
}
|
|
2259
|
+
sections.push("");
|
|
2260
|
+
const taskResults = new Map(results.map((r) => [r.task, r]));
|
|
2261
|
+
const completedTasks = tasks.filter((t) => taskResults.get(t)?.success);
|
|
2262
|
+
const failedTasks = tasks.filter((t) => {
|
|
2263
|
+
const r = taskResults.get(t);
|
|
2264
|
+
return r && !r.success;
|
|
2265
|
+
});
|
|
2266
|
+
if (completedTasks.length > 0 || failedTasks.length > 0) {
|
|
2267
|
+
sections.push("## Tasks\n");
|
|
2268
|
+
for (const task of completedTasks) {
|
|
2269
|
+
sections.push(`- [x] ${task.text}`);
|
|
2270
|
+
}
|
|
2271
|
+
for (const task of failedTasks) {
|
|
2272
|
+
sections.push(`- [ ] ${task.text}`);
|
|
2273
|
+
}
|
|
2274
|
+
sections.push("");
|
|
2275
|
+
}
|
|
2276
|
+
for (const issue of issues) {
|
|
2277
|
+
if (datasourceName === "github") {
|
|
2278
|
+
sections.push(`Closes #${issue.number}`);
|
|
2279
|
+
} else if (datasourceName === "azdevops") {
|
|
2280
|
+
sections.push(`Resolves AB#${issue.number}`);
|
|
2281
|
+
}
|
|
2282
|
+
}
|
|
2283
|
+
return sections.join("\n");
|
|
2284
|
+
}
|
|
2285
|
+
|
|
2286
|
+
// src/helpers/worktree.ts
|
|
2287
|
+
import { join as join4, basename as basename3 } from "path";
|
|
2288
|
+
import { execFile as execFile6 } from "child_process";
|
|
2289
|
+
import { promisify as promisify6 } from "util";
|
|
2290
|
+
import { randomUUID } from "crypto";
|
|
2291
|
+
init_logger();
|
|
2292
|
+
var exec6 = promisify6(execFile6);
|
|
2293
|
+
var WORKTREE_DIR = ".dispatch/worktrees";
|
|
2294
|
+
async function git2(args, cwd) {
|
|
2295
|
+
const { stdout } = await exec6("git", args, { cwd, shell: process.platform === "win32" });
|
|
2296
|
+
return stdout;
|
|
2297
|
+
}
|
|
2298
|
+
function worktreeName(issueFilename) {
|
|
2299
|
+
const base = basename3(issueFilename);
|
|
2300
|
+
const withoutExt = base.replace(/\.md$/i, "");
|
|
2301
|
+
const match = withoutExt.match(/^(\d+)/);
|
|
2302
|
+
return match ? `issue-${match[1]}` : slugify(withoutExt);
|
|
2303
|
+
}
|
|
2304
|
+
async function createWorktree(repoRoot, issueFilename, branchName, startPoint) {
|
|
2305
|
+
const name = worktreeName(issueFilename);
|
|
2306
|
+
const worktreePath = join4(repoRoot, WORKTREE_DIR, name);
|
|
2307
|
+
try {
|
|
2308
|
+
const args = ["worktree", "add", worktreePath, "-b", branchName];
|
|
2309
|
+
if (startPoint) args.push(startPoint);
|
|
2310
|
+
await git2(args, repoRoot);
|
|
2311
|
+
log.debug(`Created worktree at ${worktreePath} on branch ${branchName}`);
|
|
2312
|
+
} catch (err) {
|
|
2313
|
+
const message = log.extractMessage(err);
|
|
2314
|
+
if (message.includes("already exists")) {
|
|
2315
|
+
await git2(["worktree", "add", worktreePath, branchName], repoRoot);
|
|
2316
|
+
log.debug(`Created worktree at ${worktreePath} using existing branch ${branchName}`);
|
|
2317
|
+
} else {
|
|
2318
|
+
throw err;
|
|
2319
|
+
}
|
|
2320
|
+
}
|
|
2321
|
+
return worktreePath;
|
|
2322
|
+
}
|
|
2323
|
+
async function removeWorktree(repoRoot, issueFilename) {
|
|
2324
|
+
const name = worktreeName(issueFilename);
|
|
2325
|
+
const worktreePath = join4(repoRoot, WORKTREE_DIR, name);
|
|
2326
|
+
try {
|
|
2327
|
+
await git2(["worktree", "remove", worktreePath], repoRoot);
|
|
2328
|
+
} catch {
|
|
2329
|
+
try {
|
|
2330
|
+
await git2(["worktree", "remove", "--force", worktreePath], repoRoot);
|
|
2331
|
+
} catch (err) {
|
|
2332
|
+
log.warn(`Could not remove worktree ${name}: ${log.formatErrorChain(err)}`);
|
|
2333
|
+
return;
|
|
2334
|
+
}
|
|
2335
|
+
}
|
|
2336
|
+
try {
|
|
2337
|
+
await git2(["worktree", "prune"], repoRoot);
|
|
2338
|
+
} catch (err) {
|
|
2339
|
+
log.warn(`Could not prune worktrees: ${log.formatErrorChain(err)}`);
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
2342
|
+
function generateFeatureBranchName() {
|
|
2343
|
+
const uuid = randomUUID();
|
|
2344
|
+
const octet = uuid.split("-")[0];
|
|
2345
|
+
return `dispatch/feature-${octet}`;
|
|
2346
|
+
}
|
|
2347
|
+
|
|
2348
|
+
// src/orchestrator/runner.ts
|
|
2349
|
+
init_cleanup();
|
|
2350
|
+
init_logger();
|
|
2351
|
+
|
|
2352
|
+
// src/helpers/confirm-large-batch.ts
|
|
2353
|
+
init_logger();
|
|
2354
|
+
import { input } from "@inquirer/prompts";
|
|
2355
|
+
import chalk2 from "chalk";
|
|
2356
|
+
var LARGE_BATCH_THRESHOLD = 100;
|
|
2357
|
+
async function confirmLargeBatch(count, threshold = LARGE_BATCH_THRESHOLD) {
|
|
2358
|
+
if (count <= threshold) return true;
|
|
2359
|
+
log.warn(
|
|
2360
|
+
`This operation will process ${chalk2.bold(String(count))} specs, which exceeds the safety threshold of ${threshold}.`
|
|
2361
|
+
);
|
|
2362
|
+
const answer = await input({
|
|
2363
|
+
message: `Type ${chalk2.bold('"yes"')} to proceed:`
|
|
2364
|
+
});
|
|
2365
|
+
return answer.trim().toLowerCase() === "yes";
|
|
2366
|
+
}
|
|
2367
|
+
|
|
2368
|
+
// src/helpers/prereqs.ts
|
|
2369
|
+
import { execFile as execFile7 } from "child_process";
|
|
2370
|
+
import { promisify as promisify7 } from "util";
|
|
2371
|
+
var exec7 = promisify7(execFile7);
|
|
2372
|
+
var MIN_NODE_VERSION = "20.12.0";
|
|
2373
|
+
function parseSemver(version) {
|
|
2374
|
+
const [major, minor, patch] = version.split(".").map(Number);
|
|
2375
|
+
return [major ?? 0, minor ?? 0, patch ?? 0];
|
|
2376
|
+
}
|
|
2377
|
+
function semverGte(current, minimum) {
|
|
2378
|
+
const [cMaj, cMin, cPat] = parseSemver(current);
|
|
2379
|
+
const [mMaj, mMin, mPat] = parseSemver(minimum);
|
|
2380
|
+
if (cMaj !== mMaj) return cMaj > mMaj;
|
|
2381
|
+
if (cMin !== mMin) return cMin > mMin;
|
|
2382
|
+
return cPat >= mPat;
|
|
2383
|
+
}
|
|
2384
|
+
async function checkPrereqs(context) {
|
|
2385
|
+
const failures = [];
|
|
2386
|
+
try {
|
|
2387
|
+
await exec7("git", ["--version"], { shell: process.platform === "win32" });
|
|
2388
|
+
} catch {
|
|
2389
|
+
failures.push("git is required but was not found on PATH. Install it from https://git-scm.com");
|
|
2390
|
+
}
|
|
2391
|
+
const nodeVersion = process.versions.node;
|
|
2392
|
+
if (!semverGte(nodeVersion, MIN_NODE_VERSION)) {
|
|
2393
|
+
failures.push(
|
|
2394
|
+
`Node.js >= ${MIN_NODE_VERSION} is required but found ${nodeVersion}. Please upgrade Node.js`
|
|
2395
|
+
);
|
|
2396
|
+
}
|
|
2397
|
+
if (context?.datasource === "github") {
|
|
2398
|
+
try {
|
|
2399
|
+
await exec7("gh", ["--version"], { shell: process.platform === "win32" });
|
|
2400
|
+
} catch {
|
|
2401
|
+
failures.push(
|
|
2402
|
+
"gh (GitHub CLI) is required for the github datasource but was not found on PATH. Install it from https://cli.github.com/"
|
|
2403
|
+
);
|
|
2404
|
+
}
|
|
2405
|
+
}
|
|
2406
|
+
if (context?.datasource === "azdevops") {
|
|
2407
|
+
try {
|
|
2408
|
+
await exec7("az", ["--version"], { shell: process.platform === "win32" });
|
|
2409
|
+
} catch {
|
|
2410
|
+
failures.push(
|
|
2411
|
+
"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/"
|
|
2412
|
+
);
|
|
2413
|
+
}
|
|
2414
|
+
}
|
|
2415
|
+
return failures;
|
|
2416
|
+
}
|
|
2417
|
+
|
|
2418
|
+
// src/helpers/gitignore.ts
|
|
2419
|
+
init_logger();
|
|
2420
|
+
import { readFile as readFile2, writeFile as writeFile3 } from "fs/promises";
|
|
2421
|
+
import { join as join5 } from "path";
|
|
2422
|
+
async function ensureGitignoreEntry(repoRoot, entry) {
|
|
2423
|
+
const gitignorePath = join5(repoRoot, ".gitignore");
|
|
2424
|
+
let contents = "";
|
|
2425
|
+
try {
|
|
2426
|
+
contents = await readFile2(gitignorePath, "utf8");
|
|
2427
|
+
} catch (err) {
|
|
2428
|
+
if (err instanceof Error && "code" in err && err.code === "ENOENT") {
|
|
2429
|
+
} else {
|
|
2430
|
+
log.warn(`Could not read .gitignore: ${String(err)}`);
|
|
2431
|
+
return;
|
|
2432
|
+
}
|
|
2433
|
+
}
|
|
2434
|
+
const lines = contents.split(/\r?\n/);
|
|
2435
|
+
const bare = entry.replace(/\/$/, "");
|
|
2436
|
+
const withSlash = bare + "/";
|
|
2437
|
+
if (lines.includes(entry) || lines.includes(bare) || lines.includes(withSlash)) {
|
|
2438
|
+
return;
|
|
2077
2439
|
}
|
|
2078
|
-
const lines = contents.split(/\r?\n/);
|
|
2079
|
-
const bare = entry.replace(/\/$/, "");
|
|
2080
|
-
const withSlash = bare + "/";
|
|
2081
|
-
if (lines.includes(entry) || lines.includes(bare) || lines.includes(withSlash)) {
|
|
2082
|
-
return;
|
|
2083
|
-
}
|
|
2084
2440
|
try {
|
|
2085
2441
|
const separator = contents.length > 0 && !contents.endsWith("\n") ? "\n" : "";
|
|
2086
|
-
await
|
|
2442
|
+
await writeFile3(gitignorePath, `${contents}${separator}${entry}
|
|
2087
2443
|
`, "utf8");
|
|
2088
2444
|
log.debug(`Added '${entry}' to .gitignore`);
|
|
2089
2445
|
} catch (err) {
|
|
@@ -2093,14 +2449,14 @@ async function ensureGitignoreEntry(repoRoot, entry) {
|
|
|
2093
2449
|
|
|
2094
2450
|
// src/orchestrator/cli-config.ts
|
|
2095
2451
|
init_logger();
|
|
2096
|
-
import { join as
|
|
2452
|
+
import { join as join7 } from "path";
|
|
2097
2453
|
import { access } from "fs/promises";
|
|
2098
2454
|
import { constants } from "fs";
|
|
2099
2455
|
|
|
2100
2456
|
// src/config.ts
|
|
2101
2457
|
init_providers();
|
|
2102
|
-
import { readFile as readFile3, writeFile as
|
|
2103
|
-
import { join as
|
|
2458
|
+
import { readFile as readFile3, writeFile as writeFile4, mkdir as mkdir2 } from "fs/promises";
|
|
2459
|
+
import { join as join6, dirname as dirname3 } from "path";
|
|
2104
2460
|
|
|
2105
2461
|
// src/config-prompts.ts
|
|
2106
2462
|
init_logger();
|
|
@@ -2277,8 +2633,8 @@ var CONFIG_BOUNDS = {
|
|
|
2277
2633
|
};
|
|
2278
2634
|
var CONFIG_KEYS = ["provider", "model", "source", "testTimeout", "planTimeout", "concurrency", "org", "project", "workItemType", "iteration", "area"];
|
|
2279
2635
|
function getConfigPath(configDir) {
|
|
2280
|
-
const dir = configDir ??
|
|
2281
|
-
return
|
|
2636
|
+
const dir = configDir ?? join6(process.cwd(), ".dispatch");
|
|
2637
|
+
return join6(dir, "config.json");
|
|
2282
2638
|
}
|
|
2283
2639
|
async function loadConfig(configDir) {
|
|
2284
2640
|
const configPath = getConfigPath(configDir);
|
|
@@ -2291,8 +2647,8 @@ async function loadConfig(configDir) {
|
|
|
2291
2647
|
}
|
|
2292
2648
|
async function saveConfig(config, configDir) {
|
|
2293
2649
|
const configPath = getConfigPath(configDir);
|
|
2294
|
-
await mkdir2(
|
|
2295
|
-
await
|
|
2650
|
+
await mkdir2(dirname3(configPath), { recursive: true });
|
|
2651
|
+
await writeFile4(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
2296
2652
|
}
|
|
2297
2653
|
async function handleConfigCommand(_argv, configDir) {
|
|
2298
2654
|
await runInteractiveConfigWizard(configDir);
|
|
@@ -2317,7 +2673,7 @@ function setCliField(target, key, value) {
|
|
|
2317
2673
|
}
|
|
2318
2674
|
async function resolveCliConfig(args) {
|
|
2319
2675
|
const { explicitFlags } = args;
|
|
2320
|
-
const configDir =
|
|
2676
|
+
const configDir = join7(args.cwd, ".dispatch");
|
|
2321
2677
|
const config = await loadConfig(configDir);
|
|
2322
2678
|
const merged = { ...args };
|
|
2323
2679
|
for (const configKey of CONFIG_KEYS) {
|
|
@@ -2345,7 +2701,7 @@ async function resolveCliConfig(args) {
|
|
|
2345
2701
|
}
|
|
2346
2702
|
}
|
|
2347
2703
|
const sourceConfigured = explicitFlags.has("issueSource") || config.source !== void 0;
|
|
2348
|
-
const needsSource = !merged.fixTests && !merged.spec && !merged.respec;
|
|
2704
|
+
const needsSource = !(merged.fixTests && merged.issueIds.length === 0) && !merged.spec && !merged.respec;
|
|
2349
2705
|
if (needsSource && !sourceConfigured) {
|
|
2350
2706
|
const detected = await detectDatasource(merged.cwd);
|
|
2351
2707
|
if (detected) {
|
|
@@ -2364,17 +2720,18 @@ async function resolveCliConfig(args) {
|
|
|
2364
2720
|
}
|
|
2365
2721
|
|
|
2366
2722
|
// src/orchestrator/spec-pipeline.ts
|
|
2367
|
-
import { join as
|
|
2723
|
+
import { join as join9 } from "path";
|
|
2368
2724
|
import { mkdir as mkdir4, readFile as readFile5, rename as rename2, unlink as unlink2 } from "fs/promises";
|
|
2369
|
-
import { glob } from "glob";
|
|
2725
|
+
import { glob as glob2 } from "glob";
|
|
2370
2726
|
init_providers();
|
|
2371
2727
|
|
|
2372
2728
|
// src/agents/spec.ts
|
|
2373
|
-
import { mkdir as mkdir3, readFile as readFile4, writeFile as
|
|
2374
|
-
import { join as
|
|
2375
|
-
import { randomUUID as
|
|
2729
|
+
import { mkdir as mkdir3, readFile as readFile4, writeFile as writeFile5, unlink } from "fs/promises";
|
|
2730
|
+
import { join as join8, resolve as resolve2, sep } from "path";
|
|
2731
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
2376
2732
|
init_logger();
|
|
2377
2733
|
init_file_logger();
|
|
2734
|
+
init_environment();
|
|
2378
2735
|
async function boot5(opts) {
|
|
2379
2736
|
const { provider } = opts;
|
|
2380
2737
|
if (!provider) {
|
|
@@ -2386,8 +2743,8 @@ async function boot5(opts) {
|
|
|
2386
2743
|
const { issue, filePath, fileContent, inlineText, cwd: workingDir, outputPath } = genOpts;
|
|
2387
2744
|
const startTime = Date.now();
|
|
2388
2745
|
try {
|
|
2389
|
-
const resolvedCwd =
|
|
2390
|
-
const resolvedOutput =
|
|
2746
|
+
const resolvedCwd = resolve2(workingDir);
|
|
2747
|
+
const resolvedOutput = resolve2(outputPath);
|
|
2391
2748
|
if (resolvedOutput !== resolvedCwd && !resolvedOutput.startsWith(resolvedCwd + sep)) {
|
|
2392
2749
|
return {
|
|
2393
2750
|
data: null,
|
|
@@ -2396,10 +2753,10 @@ async function boot5(opts) {
|
|
|
2396
2753
|
durationMs: Date.now() - startTime
|
|
2397
2754
|
};
|
|
2398
2755
|
}
|
|
2399
|
-
const tmpDir =
|
|
2756
|
+
const tmpDir = join8(resolvedCwd, ".dispatch", "tmp");
|
|
2400
2757
|
await mkdir3(tmpDir, { recursive: true });
|
|
2401
|
-
const tmpFilename = `spec-${
|
|
2402
|
-
const tmpPath =
|
|
2758
|
+
const tmpFilename = `spec-${randomUUID4()}.md`;
|
|
2759
|
+
const tmpPath = join8(tmpDir, tmpFilename);
|
|
2403
2760
|
let prompt;
|
|
2404
2761
|
if (issue) {
|
|
2405
2762
|
prompt = buildSpecPrompt(issue, workingDir, tmpPath);
|
|
@@ -2446,7 +2803,7 @@ async function boot5(opts) {
|
|
|
2446
2803
|
if (!validation.valid) {
|
|
2447
2804
|
log.warn(`Spec validation warning for ${outputPath}: ${validation.reason}`);
|
|
2448
2805
|
}
|
|
2449
|
-
await
|
|
2806
|
+
await writeFile5(resolvedOutput, cleanedContent, "utf-8");
|
|
2450
2807
|
log.debug(`Wrote cleaned spec to ${resolvedOutput}`);
|
|
2451
2808
|
try {
|
|
2452
2809
|
await unlink(tmpPath);
|
|
@@ -2564,6 +2921,8 @@ function buildCommonSpecInstructions(params) {
|
|
|
2564
2921
|
``,
|
|
2565
2922
|
`\`${cwd}\``,
|
|
2566
2923
|
``,
|
|
2924
|
+
formatEnvironmentPrompt(),
|
|
2925
|
+
``,
|
|
2567
2926
|
`## Instructions`,
|
|
2568
2927
|
``,
|
|
2569
2928
|
`1. **Explore the codebase** \u2014 read relevant files, search for symbols, understand the project structure, language, frameworks, conventions, and patterns. Identify the tech stack (languages, package managers, frameworks, test runners) so your spec aligns with the project's actual standards.`,
|
|
@@ -2799,7 +3158,7 @@ function buildInlineTextItem(issues, outputDir) {
|
|
|
2799
3158
|
const title = text.length > 80 ? text.slice(0, 80).trimEnd() + "\u2026" : text;
|
|
2800
3159
|
const slug = slugify(text, MAX_SLUG_LENGTH);
|
|
2801
3160
|
const filename = `${slug}.md`;
|
|
2802
|
-
const filepath =
|
|
3161
|
+
const filepath = join9(outputDir, filename);
|
|
2803
3162
|
const details = {
|
|
2804
3163
|
number: filepath,
|
|
2805
3164
|
title,
|
|
@@ -2814,7 +3173,7 @@ function buildInlineTextItem(issues, outputDir) {
|
|
|
2814
3173
|
return [{ id: filepath, details }];
|
|
2815
3174
|
}
|
|
2816
3175
|
async function resolveFileItems(issues, specCwd, concurrency) {
|
|
2817
|
-
const files = await
|
|
3176
|
+
const files = await glob2(issues, { cwd: specCwd, absolute: true });
|
|
2818
3177
|
if (files.length === 0) {
|
|
2819
3178
|
log.error(`No files matched the pattern "${Array.isArray(issues) ? issues.join(", ") : issues}".`);
|
|
2820
3179
|
return null;
|
|
@@ -2861,7 +3220,7 @@ function previewDryRun(validItems, items, isTrackerMode, isInlineText, outputDir
|
|
|
2861
3220
|
let filepath;
|
|
2862
3221
|
if (isTrackerMode) {
|
|
2863
3222
|
const slug = slugify(details.title, 60);
|
|
2864
|
-
filepath =
|
|
3223
|
+
filepath = join9(outputDir, `${id}-${slug}.md`);
|
|
2865
3224
|
} else {
|
|
2866
3225
|
filepath = id;
|
|
2867
3226
|
}
|
|
@@ -2924,7 +3283,7 @@ async function generateSpecsBatch(validItems, items, specAgent, instance, isTrac
|
|
|
2924
3283
|
if (isTrackerMode) {
|
|
2925
3284
|
const slug = slugify(details.title, MAX_SLUG_LENGTH);
|
|
2926
3285
|
const filename = `${id}-${slug}.md`;
|
|
2927
|
-
filepath =
|
|
3286
|
+
filepath = join9(outputDir, filename);
|
|
2928
3287
|
} else if (isInlineText) {
|
|
2929
3288
|
filepath = id;
|
|
2930
3289
|
} else {
|
|
@@ -2953,7 +3312,7 @@ async function generateSpecsBatch(validItems, items, specAgent, instance, isTrac
|
|
|
2953
3312
|
const h1Title = extractTitle(result.data.content, filepath);
|
|
2954
3313
|
const h1Slug = slugify(h1Title, MAX_SLUG_LENGTH);
|
|
2955
3314
|
const finalFilename = isTrackerMode ? `${id}-${h1Slug}.md` : `${h1Slug}.md`;
|
|
2956
|
-
const finalFilepath =
|
|
3315
|
+
const finalFilepath = join9(outputDir, finalFilename);
|
|
2957
3316
|
if (finalFilepath !== filepath) {
|
|
2958
3317
|
await rename2(filepath, finalFilepath);
|
|
2959
3318
|
filepath = finalFilepath;
|
|
@@ -3058,7 +3417,7 @@ async function runSpecPipeline(opts) {
|
|
|
3058
3417
|
model,
|
|
3059
3418
|
serverUrl,
|
|
3060
3419
|
cwd: specCwd,
|
|
3061
|
-
outputDir =
|
|
3420
|
+
outputDir = join9(specCwd, ".dispatch", "specs"),
|
|
3062
3421
|
org,
|
|
3063
3422
|
project,
|
|
3064
3423
|
workItemType,
|
|
@@ -3136,9 +3495,10 @@ async function runSpecPipeline(opts) {
|
|
|
3136
3495
|
import { execFile as execFile9 } from "child_process";
|
|
3137
3496
|
import { promisify as promisify9 } from "util";
|
|
3138
3497
|
import { readFile as readFile7 } from "fs/promises";
|
|
3498
|
+
import { glob as glob3 } from "glob";
|
|
3139
3499
|
|
|
3140
3500
|
// src/parser.ts
|
|
3141
|
-
import { readFile as readFile6, writeFile as
|
|
3501
|
+
import { readFile as readFile6, writeFile as writeFile6 } from "fs/promises";
|
|
3142
3502
|
var UNCHECKED_RE = /^(\s*[-*]\s)\[ \]\s+(.+)$/;
|
|
3143
3503
|
var CHECKED_SUB = "$1[x] $2";
|
|
3144
3504
|
var MODE_PREFIX_RE = /^\(([PSI])\)\s+/;
|
|
@@ -3206,7 +3566,7 @@ async function markTaskComplete(task) {
|
|
|
3206
3566
|
);
|
|
3207
3567
|
}
|
|
3208
3568
|
lines[lineIndex] = updated;
|
|
3209
|
-
await
|
|
3569
|
+
await writeFile6(task.file, lines.join(eol), "utf-8");
|
|
3210
3570
|
}
|
|
3211
3571
|
function groupTasksByMode(tasks) {
|
|
3212
3572
|
if (tasks.length === 0) return [];
|
|
@@ -3237,6 +3597,7 @@ function groupTasksByMode(tasks) {
|
|
|
3237
3597
|
// src/agents/planner.ts
|
|
3238
3598
|
init_logger();
|
|
3239
3599
|
init_file_logger();
|
|
3600
|
+
init_environment();
|
|
3240
3601
|
async function boot6(opts) {
|
|
3241
3602
|
const { provider, cwd } = opts;
|
|
3242
3603
|
if (!provider) {
|
|
@@ -3307,6 +3668,10 @@ function buildPlannerPrompt(task, cwd, fileContext, worktreeRoot) {
|
|
|
3307
3668
|
`- All relative paths must resolve within the worktree root above.`
|
|
3308
3669
|
);
|
|
3309
3670
|
}
|
|
3671
|
+
sections.push(
|
|
3672
|
+
``,
|
|
3673
|
+
formatEnvironmentPrompt()
|
|
3674
|
+
);
|
|
3310
3675
|
sections.push(
|
|
3311
3676
|
``,
|
|
3312
3677
|
`## Instructions`,
|
|
@@ -3343,6 +3708,7 @@ function buildPlannerPrompt(task, cwd, fileContext, worktreeRoot) {
|
|
|
3343
3708
|
// src/dispatcher.ts
|
|
3344
3709
|
init_logger();
|
|
3345
3710
|
init_file_logger();
|
|
3711
|
+
init_environment();
|
|
3346
3712
|
async function dispatchTask(instance, task, cwd, plan, worktreeRoot) {
|
|
3347
3713
|
try {
|
|
3348
3714
|
log.debug(`Dispatching task: ${task.file}:${task.line} \u2014 ${task.text.slice(0, 80)}`);
|
|
@@ -3375,6 +3741,8 @@ function buildPrompt(task, cwd, worktreeRoot) {
|
|
|
3375
3741
|
`**Source file:** ${task.file}`,
|
|
3376
3742
|
`**Task (line ${task.line}):** ${task.text}`,
|
|
3377
3743
|
``,
|
|
3744
|
+
getEnvironmentBlock(),
|
|
3745
|
+
``,
|
|
3378
3746
|
`Instructions:`,
|
|
3379
3747
|
`- Complete ONLY this specific task \u2014 do not work on other tasks.`,
|
|
3380
3748
|
`- Make the minimal, correct changes needed.`,
|
|
@@ -3392,6 +3760,8 @@ function buildPlannedPrompt(task, cwd, plan, worktreeRoot) {
|
|
|
3392
3760
|
`**Source file:** ${task.file}`,
|
|
3393
3761
|
`**Task (line ${task.line}):** ${task.text}`,
|
|
3394
3762
|
``,
|
|
3763
|
+
getEnvironmentBlock(),
|
|
3764
|
+
``,
|
|
3395
3765
|
`---`,
|
|
3396
3766
|
``,
|
|
3397
3767
|
`## Execution Plan`,
|
|
@@ -3466,9 +3836,10 @@ ${err.stack}` : ""}`);
|
|
|
3466
3836
|
// src/agents/commit.ts
|
|
3467
3837
|
init_logger();
|
|
3468
3838
|
init_file_logger();
|
|
3469
|
-
|
|
3470
|
-
import {
|
|
3471
|
-
import {
|
|
3839
|
+
init_environment();
|
|
3840
|
+
import { mkdir as mkdir5, writeFile as writeFile7 } from "fs/promises";
|
|
3841
|
+
import { join as join10, resolve as resolve3 } from "path";
|
|
3842
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
3472
3843
|
async function boot8(opts) {
|
|
3473
3844
|
const { provider } = opts;
|
|
3474
3845
|
if (!provider) {
|
|
@@ -3480,11 +3851,11 @@ async function boot8(opts) {
|
|
|
3480
3851
|
name: "commit",
|
|
3481
3852
|
async generate(genOpts) {
|
|
3482
3853
|
try {
|
|
3483
|
-
const resolvedCwd =
|
|
3484
|
-
const tmpDir =
|
|
3854
|
+
const resolvedCwd = resolve3(genOpts.cwd);
|
|
3855
|
+
const tmpDir = join10(resolvedCwd, ".dispatch", "tmp");
|
|
3485
3856
|
await mkdir5(tmpDir, { recursive: true });
|
|
3486
|
-
const tmpFilename = `commit-${
|
|
3487
|
-
const tmpPath =
|
|
3857
|
+
const tmpFilename = `commit-${randomUUID5()}.md`;
|
|
3858
|
+
const tmpPath = join10(tmpDir, tmpFilename);
|
|
3488
3859
|
const prompt = buildCommitPrompt(genOpts);
|
|
3489
3860
|
fileLoggerStorage.getStore()?.prompt("commit", prompt);
|
|
3490
3861
|
const sessionId = await provider.createSession();
|
|
@@ -3512,7 +3883,7 @@ async function boot8(opts) {
|
|
|
3512
3883
|
};
|
|
3513
3884
|
}
|
|
3514
3885
|
const outputContent = formatOutputFile(parsed);
|
|
3515
|
-
await
|
|
3886
|
+
await writeFile7(tmpPath, outputContent, "utf-8");
|
|
3516
3887
|
log.debug(`Wrote commit agent output to ${tmpPath}`);
|
|
3517
3888
|
fileLoggerStorage.getStore()?.agentEvent("commit", "completed", `message: ${parsed.commitMessage.slice(0, 80)}`);
|
|
3518
3889
|
return {
|
|
@@ -3542,6 +3913,8 @@ function buildCommitPrompt(opts) {
|
|
|
3542
3913
|
const sections = [
|
|
3543
3914
|
`You are a **commit message agent**. Your job is to analyze the git diff below and generate a meaningful, conventional-commit-compliant commit message, a PR title, and a PR description.`,
|
|
3544
3915
|
``,
|
|
3916
|
+
formatEnvironmentPrompt(),
|
|
3917
|
+
``,
|
|
3545
3918
|
`## Conventional Commit Guidelines`,
|
|
3546
3919
|
``,
|
|
3547
3920
|
`Follow the Conventional Commits specification (https://www.conventionalcommits.org/):`,
|
|
@@ -3662,68 +4035,6 @@ function formatOutputFile(parsed) {
|
|
|
3662
4035
|
init_logger();
|
|
3663
4036
|
init_cleanup();
|
|
3664
4037
|
|
|
3665
|
-
// src/helpers/worktree.ts
|
|
3666
|
-
import { join as join9, basename } from "path";
|
|
3667
|
-
import { execFile as execFile7 } from "child_process";
|
|
3668
|
-
import { promisify as promisify7 } from "util";
|
|
3669
|
-
import { randomUUID as randomUUID5 } from "crypto";
|
|
3670
|
-
init_logger();
|
|
3671
|
-
var exec7 = promisify7(execFile7);
|
|
3672
|
-
var WORKTREE_DIR = ".dispatch/worktrees";
|
|
3673
|
-
async function git2(args, cwd) {
|
|
3674
|
-
const { stdout } = await exec7("git", args, { cwd });
|
|
3675
|
-
return stdout;
|
|
3676
|
-
}
|
|
3677
|
-
function worktreeName(issueFilename) {
|
|
3678
|
-
const base = basename(issueFilename);
|
|
3679
|
-
const withoutExt = base.replace(/\.md$/i, "");
|
|
3680
|
-
const match = withoutExt.match(/^(\d+)/);
|
|
3681
|
-
return match ? `issue-${match[1]}` : slugify(withoutExt);
|
|
3682
|
-
}
|
|
3683
|
-
async function createWorktree(repoRoot, issueFilename, branchName, startPoint) {
|
|
3684
|
-
const name = worktreeName(issueFilename);
|
|
3685
|
-
const worktreePath = join9(repoRoot, WORKTREE_DIR, name);
|
|
3686
|
-
try {
|
|
3687
|
-
const args = ["worktree", "add", worktreePath, "-b", branchName];
|
|
3688
|
-
if (startPoint) args.push(startPoint);
|
|
3689
|
-
await git2(args, repoRoot);
|
|
3690
|
-
log.debug(`Created worktree at ${worktreePath} on branch ${branchName}`);
|
|
3691
|
-
} catch (err) {
|
|
3692
|
-
const message = log.extractMessage(err);
|
|
3693
|
-
if (message.includes("already exists")) {
|
|
3694
|
-
await git2(["worktree", "add", worktreePath, branchName], repoRoot);
|
|
3695
|
-
log.debug(`Created worktree at ${worktreePath} using existing branch ${branchName}`);
|
|
3696
|
-
} else {
|
|
3697
|
-
throw err;
|
|
3698
|
-
}
|
|
3699
|
-
}
|
|
3700
|
-
return worktreePath;
|
|
3701
|
-
}
|
|
3702
|
-
async function removeWorktree(repoRoot, issueFilename) {
|
|
3703
|
-
const name = worktreeName(issueFilename);
|
|
3704
|
-
const worktreePath = join9(repoRoot, WORKTREE_DIR, name);
|
|
3705
|
-
try {
|
|
3706
|
-
await git2(["worktree", "remove", worktreePath], repoRoot);
|
|
3707
|
-
} catch {
|
|
3708
|
-
try {
|
|
3709
|
-
await git2(["worktree", "remove", "--force", worktreePath], repoRoot);
|
|
3710
|
-
} catch (err) {
|
|
3711
|
-
log.warn(`Could not remove worktree ${name}: ${log.formatErrorChain(err)}`);
|
|
3712
|
-
return;
|
|
3713
|
-
}
|
|
3714
|
-
}
|
|
3715
|
-
try {
|
|
3716
|
-
await git2(["worktree", "prune"], repoRoot);
|
|
3717
|
-
} catch (err) {
|
|
3718
|
-
log.warn(`Could not prune worktrees: ${log.formatErrorChain(err)}`);
|
|
3719
|
-
}
|
|
3720
|
-
}
|
|
3721
|
-
function generateFeatureBranchName() {
|
|
3722
|
-
const uuid = randomUUID5();
|
|
3723
|
-
const octet = uuid.split("-")[0];
|
|
3724
|
-
return `dispatch/feature-${octet}`;
|
|
3725
|
-
}
|
|
3726
|
-
|
|
3727
4038
|
// src/tui.ts
|
|
3728
4039
|
import chalk6 from "chalk";
|
|
3729
4040
|
var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
@@ -3980,184 +4291,38 @@ function createTui() {
|
|
|
3980
4291
|
|
|
3981
4292
|
// src/orchestrator/dispatch-pipeline.ts
|
|
3982
4293
|
init_providers();
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
|
|
3986
|
-
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
const match = /^(\d+)-(.+)\.md$/.exec(filename);
|
|
3995
|
-
if (!match) return null;
|
|
3996
|
-
return { issueId: match[1], slug: match[2] };
|
|
3997
|
-
}
|
|
3998
|
-
async function fetchItemsById(issueIds, datasource4, fetchOpts) {
|
|
3999
|
-
const ids = issueIds.flatMap(
|
|
4000
|
-
(id) => id.split(",").map((s) => s.trim()).filter(Boolean)
|
|
4001
|
-
);
|
|
4294
|
+
init_timeout();
|
|
4295
|
+
import chalk7 from "chalk";
|
|
4296
|
+
init_file_logger();
|
|
4297
|
+
var exec9 = promisify9(execFile9);
|
|
4298
|
+
async function resolveGlobItems(patterns, cwd) {
|
|
4299
|
+
const files = await glob3(patterns, { cwd, absolute: true });
|
|
4300
|
+
if (files.length === 0) {
|
|
4301
|
+
log.warn(`No files matched the pattern(s): ${patterns.join(", ")}`);
|
|
4302
|
+
return [];
|
|
4303
|
+
}
|
|
4304
|
+
log.info(`Matched ${files.length} file(s) from glob pattern(s)`);
|
|
4002
4305
|
const items = [];
|
|
4003
|
-
for (const
|
|
4306
|
+
for (const filePath of files) {
|
|
4004
4307
|
try {
|
|
4005
|
-
const
|
|
4006
|
-
|
|
4308
|
+
const content = await readFile7(filePath, "utf-8");
|
|
4309
|
+
const title = extractTitle(content, filePath);
|
|
4310
|
+
items.push({
|
|
4311
|
+
number: filePath,
|
|
4312
|
+
title,
|
|
4313
|
+
body: content,
|
|
4314
|
+
labels: [],
|
|
4315
|
+
state: "open",
|
|
4316
|
+
url: filePath,
|
|
4317
|
+
comments: [],
|
|
4318
|
+
acceptanceCriteria: ""
|
|
4319
|
+
});
|
|
4007
4320
|
} catch (err) {
|
|
4008
|
-
log.warn(`Could not
|
|
4321
|
+
log.warn(`Could not read file ${filePath}: ${log.formatErrorChain(err)}`);
|
|
4009
4322
|
}
|
|
4010
4323
|
}
|
|
4011
4324
|
return items;
|
|
4012
4325
|
}
|
|
4013
|
-
async function writeItemsToTempDir(items) {
|
|
4014
|
-
const tempDir = await mkdtemp(join10(tmpdir(), "dispatch-"));
|
|
4015
|
-
const files = [];
|
|
4016
|
-
const issueDetailsByFile = /* @__PURE__ */ new Map();
|
|
4017
|
-
for (const item of items) {
|
|
4018
|
-
const slug = slugify(item.title, MAX_SLUG_LENGTH);
|
|
4019
|
-
const filename = `${item.number}-${slug}.md`;
|
|
4020
|
-
const filepath = join10(tempDir, filename);
|
|
4021
|
-
await writeFile7(filepath, item.body, "utf-8");
|
|
4022
|
-
files.push(filepath);
|
|
4023
|
-
issueDetailsByFile.set(filepath, item);
|
|
4024
|
-
}
|
|
4025
|
-
files.sort((a, b) => {
|
|
4026
|
-
const numA = parseInt(basename2(a).match(/^(\d+)/)?.[1] ?? "0", 10);
|
|
4027
|
-
const numB = parseInt(basename2(b).match(/^(\d+)/)?.[1] ?? "0", 10);
|
|
4028
|
-
if (numA !== numB) return numA - numB;
|
|
4029
|
-
return a.localeCompare(b);
|
|
4030
|
-
});
|
|
4031
|
-
return { files, issueDetailsByFile };
|
|
4032
|
-
}
|
|
4033
|
-
async function getCommitSummaries(defaultBranch, cwd) {
|
|
4034
|
-
try {
|
|
4035
|
-
const { stdout } = await exec8(
|
|
4036
|
-
"git",
|
|
4037
|
-
["log", `${defaultBranch}..HEAD`, "--pretty=format:%s"],
|
|
4038
|
-
{ cwd }
|
|
4039
|
-
);
|
|
4040
|
-
return stdout.trim().split("\n").filter(Boolean);
|
|
4041
|
-
} catch {
|
|
4042
|
-
return [];
|
|
4043
|
-
}
|
|
4044
|
-
}
|
|
4045
|
-
async function getBranchDiff(defaultBranch, cwd) {
|
|
4046
|
-
try {
|
|
4047
|
-
const { stdout } = await exec8(
|
|
4048
|
-
"git",
|
|
4049
|
-
["diff", `${defaultBranch}..HEAD`],
|
|
4050
|
-
{ cwd, maxBuffer: 10 * 1024 * 1024 }
|
|
4051
|
-
);
|
|
4052
|
-
return stdout;
|
|
4053
|
-
} catch {
|
|
4054
|
-
return "";
|
|
4055
|
-
}
|
|
4056
|
-
}
|
|
4057
|
-
async function squashBranchCommits(defaultBranch, message, cwd) {
|
|
4058
|
-
const { stdout } = await exec8(
|
|
4059
|
-
"git",
|
|
4060
|
-
["merge-base", defaultBranch, "HEAD"],
|
|
4061
|
-
{ cwd }
|
|
4062
|
-
);
|
|
4063
|
-
const mergeBase = stdout.trim();
|
|
4064
|
-
await exec8("git", ["reset", "--soft", mergeBase], { cwd });
|
|
4065
|
-
await exec8("git", ["commit", "-m", message], { cwd });
|
|
4066
|
-
}
|
|
4067
|
-
async function buildPrBody(details, tasks, results, defaultBranch, datasourceName, cwd) {
|
|
4068
|
-
const sections = [];
|
|
4069
|
-
const commits = await getCommitSummaries(defaultBranch, cwd);
|
|
4070
|
-
if (commits.length > 0) {
|
|
4071
|
-
sections.push("## Summary\n");
|
|
4072
|
-
for (const commit of commits) {
|
|
4073
|
-
sections.push(`- ${commit}`);
|
|
4074
|
-
}
|
|
4075
|
-
sections.push("");
|
|
4076
|
-
}
|
|
4077
|
-
const taskResults = new Map(
|
|
4078
|
-
results.filter((r) => tasks.includes(r.task)).map((r) => [r.task, r])
|
|
4079
|
-
);
|
|
4080
|
-
const completedTasks = tasks.filter((t) => taskResults.get(t)?.success);
|
|
4081
|
-
const failedTasks = tasks.filter((t) => {
|
|
4082
|
-
const r = taskResults.get(t);
|
|
4083
|
-
return r && !r.success;
|
|
4084
|
-
});
|
|
4085
|
-
if (completedTasks.length > 0 || failedTasks.length > 0) {
|
|
4086
|
-
sections.push("## Tasks\n");
|
|
4087
|
-
for (const task of completedTasks) {
|
|
4088
|
-
sections.push(`- [x] ${task.text}`);
|
|
4089
|
-
}
|
|
4090
|
-
for (const task of failedTasks) {
|
|
4091
|
-
sections.push(`- [ ] ${task.text}`);
|
|
4092
|
-
}
|
|
4093
|
-
sections.push("");
|
|
4094
|
-
}
|
|
4095
|
-
if (details.labels.length > 0) {
|
|
4096
|
-
sections.push(`**Labels:** ${details.labels.join(", ")}
|
|
4097
|
-
`);
|
|
4098
|
-
}
|
|
4099
|
-
if (datasourceName === "github") {
|
|
4100
|
-
sections.push(`Closes #${details.number}`);
|
|
4101
|
-
} else if (datasourceName === "azdevops") {
|
|
4102
|
-
sections.push(`Resolves AB#${details.number}`);
|
|
4103
|
-
}
|
|
4104
|
-
return sections.join("\n");
|
|
4105
|
-
}
|
|
4106
|
-
async function buildPrTitle(issueTitle, defaultBranch, cwd) {
|
|
4107
|
-
const commits = await getCommitSummaries(defaultBranch, cwd);
|
|
4108
|
-
if (commits.length === 0) {
|
|
4109
|
-
return issueTitle;
|
|
4110
|
-
}
|
|
4111
|
-
if (commits.length === 1) {
|
|
4112
|
-
return commits[0];
|
|
4113
|
-
}
|
|
4114
|
-
return `${commits[commits.length - 1]} (+${commits.length - 1} more)`;
|
|
4115
|
-
}
|
|
4116
|
-
function buildFeaturePrTitle(featureBranchName, issues) {
|
|
4117
|
-
if (issues.length === 1) {
|
|
4118
|
-
return issues[0].title;
|
|
4119
|
-
}
|
|
4120
|
-
const issueRefs = issues.map((d) => `#${d.number}`).join(", ");
|
|
4121
|
-
return `feat: ${featureBranchName} (${issueRefs})`;
|
|
4122
|
-
}
|
|
4123
|
-
function buildFeaturePrBody(issues, tasks, results, datasourceName) {
|
|
4124
|
-
const sections = [];
|
|
4125
|
-
sections.push("## Issues\n");
|
|
4126
|
-
for (const issue of issues) {
|
|
4127
|
-
sections.push(`- #${issue.number}: ${issue.title}`);
|
|
4128
|
-
}
|
|
4129
|
-
sections.push("");
|
|
4130
|
-
const taskResults = new Map(results.map((r) => [r.task, r]));
|
|
4131
|
-
const completedTasks = tasks.filter((t) => taskResults.get(t)?.success);
|
|
4132
|
-
const failedTasks = tasks.filter((t) => {
|
|
4133
|
-
const r = taskResults.get(t);
|
|
4134
|
-
return r && !r.success;
|
|
4135
|
-
});
|
|
4136
|
-
if (completedTasks.length > 0 || failedTasks.length > 0) {
|
|
4137
|
-
sections.push("## Tasks\n");
|
|
4138
|
-
for (const task of completedTasks) {
|
|
4139
|
-
sections.push(`- [x] ${task.text}`);
|
|
4140
|
-
}
|
|
4141
|
-
for (const task of failedTasks) {
|
|
4142
|
-
sections.push(`- [ ] ${task.text}`);
|
|
4143
|
-
}
|
|
4144
|
-
sections.push("");
|
|
4145
|
-
}
|
|
4146
|
-
for (const issue of issues) {
|
|
4147
|
-
if (datasourceName === "github") {
|
|
4148
|
-
sections.push(`Closes #${issue.number}`);
|
|
4149
|
-
} else if (datasourceName === "azdevops") {
|
|
4150
|
-
sections.push(`Resolves AB#${issue.number}`);
|
|
4151
|
-
}
|
|
4152
|
-
}
|
|
4153
|
-
return sections.join("\n");
|
|
4154
|
-
}
|
|
4155
|
-
|
|
4156
|
-
// src/orchestrator/dispatch-pipeline.ts
|
|
4157
|
-
init_timeout();
|
|
4158
|
-
import chalk7 from "chalk";
|
|
4159
|
-
init_file_logger();
|
|
4160
|
-
var exec9 = promisify9(execFile9);
|
|
4161
4326
|
var DEFAULT_PLAN_TIMEOUT_MIN = 10;
|
|
4162
4327
|
var DEFAULT_PLAN_RETRIES = 1;
|
|
4163
4328
|
async function runDispatchPipeline(opts, cwd) {
|
|
@@ -4223,7 +4388,14 @@ async function runDispatchPipeline(opts, cwd) {
|
|
|
4223
4388
|
}
|
|
4224
4389
|
const datasource4 = getDatasource(source);
|
|
4225
4390
|
const fetchOpts = { cwd, org, project, workItemType, iteration, area };
|
|
4226
|
-
|
|
4391
|
+
let items;
|
|
4392
|
+
if (issueIds.length > 0 && source === "md" && issueIds.some((id) => isGlobOrFilePath(id))) {
|
|
4393
|
+
items = await resolveGlobItems(issueIds, cwd);
|
|
4394
|
+
} else if (issueIds.length > 0) {
|
|
4395
|
+
items = await fetchItemsById(issueIds, datasource4, fetchOpts);
|
|
4396
|
+
} else {
|
|
4397
|
+
items = await datasource4.list(fetchOpts);
|
|
4398
|
+
}
|
|
4227
4399
|
if (items.length === 0) {
|
|
4228
4400
|
tui.state.phase = "done";
|
|
4229
4401
|
tui.stop();
|
|
@@ -4295,12 +4467,32 @@ async function runDispatchPipeline(opts, cwd) {
|
|
|
4295
4467
|
let featureBranchName;
|
|
4296
4468
|
let featureDefaultBranch;
|
|
4297
4469
|
if (feature) {
|
|
4470
|
+
if (typeof feature === "string") {
|
|
4471
|
+
if (!isValidBranchName(feature)) {
|
|
4472
|
+
log.error(`Invalid feature branch name: "${feature}"`);
|
|
4473
|
+
tui.state.phase = "done";
|
|
4474
|
+
tui.stop();
|
|
4475
|
+
return { total: allTasks.length, completed: 0, failed: allTasks.length, skipped: 0, results: [] };
|
|
4476
|
+
}
|
|
4477
|
+
featureBranchName = feature.includes("/") ? feature : `dispatch/${feature}`;
|
|
4478
|
+
} else {
|
|
4479
|
+
featureBranchName = generateFeatureBranchName();
|
|
4480
|
+
}
|
|
4298
4481
|
try {
|
|
4299
4482
|
featureDefaultBranch = await datasource4.getDefaultBranch(lifecycleOpts);
|
|
4300
4483
|
await datasource4.switchBranch(featureDefaultBranch, lifecycleOpts);
|
|
4301
|
-
|
|
4302
|
-
|
|
4303
|
-
|
|
4484
|
+
try {
|
|
4485
|
+
await datasource4.createAndSwitchBranch(featureBranchName, lifecycleOpts);
|
|
4486
|
+
log.debug(`Created feature branch ${featureBranchName} from ${featureDefaultBranch}`);
|
|
4487
|
+
} catch (createErr) {
|
|
4488
|
+
const message = log.extractMessage(createErr);
|
|
4489
|
+
if (message.includes("already exists")) {
|
|
4490
|
+
await datasource4.switchBranch(featureBranchName, lifecycleOpts);
|
|
4491
|
+
log.debug(`Switched to existing feature branch ${featureBranchName}`);
|
|
4492
|
+
} else {
|
|
4493
|
+
throw createErr;
|
|
4494
|
+
}
|
|
4495
|
+
}
|
|
4304
4496
|
registerCleanup(async () => {
|
|
4305
4497
|
try {
|
|
4306
4498
|
await datasource4.switchBranch(featureDefaultBranch, lifecycleOpts);
|
|
@@ -4492,12 +4684,18 @@ ${err.stack}` : ""}`);
|
|
|
4492
4684
|
fileLogger?.info(`Execution completed successfully (${Date.now() - startTime}ms)`);
|
|
4493
4685
|
try {
|
|
4494
4686
|
const parsed = parseIssueFilename(task.file);
|
|
4687
|
+
const updatedContent = await readFile7(task.file, "utf-8");
|
|
4495
4688
|
if (parsed) {
|
|
4496
|
-
const updatedContent = await readFile7(task.file, "utf-8");
|
|
4497
4689
|
const issueDetails = issueDetailsByFile.get(task.file);
|
|
4498
4690
|
const title = issueDetails?.title ?? parsed.slug;
|
|
4499
4691
|
await datasource4.update(parsed.issueId, title, updatedContent, fetchOpts);
|
|
4500
4692
|
log.success(`Synced task completion to issue #${parsed.issueId}`);
|
|
4693
|
+
} else {
|
|
4694
|
+
const issueDetails = issueDetailsByFile.get(task.file);
|
|
4695
|
+
if (issueDetails) {
|
|
4696
|
+
await datasource4.update(issueDetails.number, issueDetails.title, updatedContent, fetchOpts);
|
|
4697
|
+
log.success(`Synced task completion to issue #${issueDetails.number}`);
|
|
4698
|
+
}
|
|
4501
4699
|
}
|
|
4502
4700
|
} catch (err) {
|
|
4503
4701
|
log.warn(`Could not sync task completion to datasource: ${log.formatErrorChain(err)}`);
|
|
@@ -4584,13 +4782,13 @@ ${err.stack}` : ""}`);
|
|
|
4584
4782
|
}
|
|
4585
4783
|
try {
|
|
4586
4784
|
await datasource4.switchBranch(featureBranchName, lifecycleOpts);
|
|
4587
|
-
await exec9("git", ["merge", branchName, "--no-ff", "-m", `merge: issue #${details.number}`], { cwd });
|
|
4785
|
+
await exec9("git", ["merge", branchName, "--no-ff", "-m", `merge: issue #${details.number}`], { cwd, shell: process.platform === "win32" });
|
|
4588
4786
|
log.debug(`Merged ${branchName} into ${featureBranchName}`);
|
|
4589
4787
|
} catch (err) {
|
|
4590
4788
|
const mergeError = `Could not merge ${branchName} into feature branch: ${log.formatErrorChain(err)}`;
|
|
4591
4789
|
log.warn(mergeError);
|
|
4592
4790
|
try {
|
|
4593
|
-
await exec9("git", ["merge", "--abort"], { cwd });
|
|
4791
|
+
await exec9("git", ["merge", "--abort"], { cwd, shell: process.platform === "win32" });
|
|
4594
4792
|
} catch {
|
|
4595
4793
|
}
|
|
4596
4794
|
for (const task of fileTasks) {
|
|
@@ -4608,7 +4806,7 @@ ${err.stack}` : ""}`);
|
|
|
4608
4806
|
return;
|
|
4609
4807
|
}
|
|
4610
4808
|
try {
|
|
4611
|
-
await exec9("git", ["branch", "-d", branchName], { cwd });
|
|
4809
|
+
await exec9("git", ["branch", "-d", branchName], { cwd, shell: process.platform === "win32" });
|
|
4612
4810
|
log.debug(`Deleted local branch ${branchName}`);
|
|
4613
4811
|
} catch (err) {
|
|
4614
4812
|
log.warn(`Could not delete local branch ${branchName}: ${log.formatErrorChain(err)}`);
|
|
@@ -4764,13 +4962,20 @@ async function dryRunMode(issueIds, cwd, source, org, project, workItemType, ite
|
|
|
4764
4962
|
username = await datasource4.getUsername(lifecycleOpts);
|
|
4765
4963
|
} catch {
|
|
4766
4964
|
}
|
|
4767
|
-
|
|
4965
|
+
let items;
|
|
4966
|
+
if (issueIds.length > 0 && source === "md" && issueIds.some((id) => isGlobOrFilePath(id))) {
|
|
4967
|
+
items = await resolveGlobItems(issueIds, cwd);
|
|
4968
|
+
} else if (issueIds.length > 0) {
|
|
4969
|
+
items = await fetchItemsById(issueIds, datasource4, fetchOpts);
|
|
4970
|
+
} else {
|
|
4971
|
+
items = await datasource4.list(fetchOpts);
|
|
4972
|
+
}
|
|
4768
4973
|
if (items.length === 0) {
|
|
4769
4974
|
const label = issueIds.length > 0 ? `issue(s) ${issueIds.join(", ")}` : `datasource: ${source}`;
|
|
4770
4975
|
log.warn("No work items found from " + label);
|
|
4771
4976
|
return { total: 0, completed: 0, failed: 0, skipped: 0, results: [] };
|
|
4772
4977
|
}
|
|
4773
|
-
const { files } = await writeItemsToTempDir(items);
|
|
4978
|
+
const { files, issueDetailsByFile } = await writeItemsToTempDir(items);
|
|
4774
4979
|
const taskFiles = [];
|
|
4775
4980
|
for (const file of files) {
|
|
4776
4981
|
const tf = await parseTaskFile(file);
|
|
@@ -4787,7 +4992,7 @@ async function dryRunMode(issueIds, cwd, source, org, project, workItemType, ite
|
|
|
4787
4992
|
`);
|
|
4788
4993
|
for (const task of allTasks) {
|
|
4789
4994
|
const parsed = parseIssueFilename(task.file);
|
|
4790
|
-
const details = parsed ? items.find((item) => item.number === parsed.issueId) :
|
|
4995
|
+
const details = parsed ? items.find((item) => item.number === parsed.issueId) : issueDetailsByFile.get(task.file);
|
|
4791
4996
|
const branchInfo = details ? ` [branch: ${datasource4.buildBranchName(details.number, details.title, username)}]` : "";
|
|
4792
4997
|
log.task(allTasks.indexOf(task), allTasks.length, `${task.file}:${task.line} \u2014 ${task.text}${branchInfo}`);
|
|
4793
4998
|
}
|
|
@@ -4801,6 +5006,58 @@ async function dryRunMode(issueIds, cwd, source, org, project, workItemType, ite
|
|
|
4801
5006
|
}
|
|
4802
5007
|
|
|
4803
5008
|
// src/orchestrator/runner.ts
|
|
5009
|
+
async function runMultiIssueFixTests(opts) {
|
|
5010
|
+
const { runFixTestsPipeline: runFixTestsPipeline2 } = await Promise.resolve().then(() => (init_fix_tests_pipeline(), fix_tests_pipeline_exports));
|
|
5011
|
+
const datasource4 = getDatasource(opts.source);
|
|
5012
|
+
const fetchOpts = { cwd: opts.cwd, org: opts.org, project: opts.project };
|
|
5013
|
+
const items = await fetchItemsById(opts.issueIds, datasource4, fetchOpts);
|
|
5014
|
+
if (items.length === 0) {
|
|
5015
|
+
log.warn("No issues found for the given IDs");
|
|
5016
|
+
return { mode: "fix-tests", success: false, error: "No issues found" };
|
|
5017
|
+
}
|
|
5018
|
+
let username = "";
|
|
5019
|
+
try {
|
|
5020
|
+
username = await datasource4.getUsername({ cwd: opts.cwd });
|
|
5021
|
+
} catch (err) {
|
|
5022
|
+
log.warn(`Could not resolve git username for branch naming: ${log.formatErrorChain(err)}`);
|
|
5023
|
+
}
|
|
5024
|
+
log.info(`Running fix-tests for ${items.length} issue(s) in worktrees`);
|
|
5025
|
+
const issueResults = [];
|
|
5026
|
+
for (const item of items) {
|
|
5027
|
+
const branchName = datasource4.buildBranchName(item.number, item.title, username);
|
|
5028
|
+
const issueFilename = `${item.number}-fix-tests.md`;
|
|
5029
|
+
let worktreePath;
|
|
5030
|
+
try {
|
|
5031
|
+
worktreePath = await createWorktree(opts.cwd, issueFilename, branchName);
|
|
5032
|
+
registerCleanup(async () => {
|
|
5033
|
+
await removeWorktree(opts.cwd, issueFilename);
|
|
5034
|
+
});
|
|
5035
|
+
log.info(`Created worktree for issue #${item.number} at ${worktreePath}`);
|
|
5036
|
+
const result = await runFixTestsPipeline2({
|
|
5037
|
+
cwd: worktreePath,
|
|
5038
|
+
provider: opts.provider,
|
|
5039
|
+
serverUrl: opts.serverUrl,
|
|
5040
|
+
verbose: opts.verbose,
|
|
5041
|
+
testTimeout: opts.testTimeout
|
|
5042
|
+
});
|
|
5043
|
+
issueResults.push({ issueId: item.number, branch: branchName, success: result.success, error: result.error });
|
|
5044
|
+
} catch (err) {
|
|
5045
|
+
const message = log.extractMessage(err);
|
|
5046
|
+
log.error(`Fix-tests failed for issue #${item.number}: ${message}`);
|
|
5047
|
+
issueResults.push({ issueId: item.number, branch: branchName, success: false, error: message });
|
|
5048
|
+
} finally {
|
|
5049
|
+
if (worktreePath) {
|
|
5050
|
+
try {
|
|
5051
|
+
await removeWorktree(opts.cwd, issueFilename);
|
|
5052
|
+
} catch (err) {
|
|
5053
|
+
log.warn(`Could not remove worktree for issue #${item.number}: ${log.formatErrorChain(err)}`);
|
|
5054
|
+
}
|
|
5055
|
+
}
|
|
5056
|
+
}
|
|
5057
|
+
}
|
|
5058
|
+
const allSuccess = issueResults.length > 0 && issueResults.every((r) => r.success);
|
|
5059
|
+
return { mode: "fix-tests", success: allSuccess, issueResults };
|
|
5060
|
+
}
|
|
4804
5061
|
async function boot9(opts) {
|
|
4805
5062
|
const { cwd } = opts;
|
|
4806
5063
|
const runner = {
|
|
@@ -4813,7 +5070,25 @@ async function boot9(opts) {
|
|
|
4813
5070
|
}
|
|
4814
5071
|
if (opts2.mode === "fix-tests") {
|
|
4815
5072
|
const { runFixTestsPipeline: runFixTestsPipeline2 } = await Promise.resolve().then(() => (init_fix_tests_pipeline(), fix_tests_pipeline_exports));
|
|
4816
|
-
|
|
5073
|
+
if (!opts2.issueIds || opts2.issueIds.length === 0) {
|
|
5074
|
+
return runFixTestsPipeline2({ cwd, provider: opts2.provider ?? "opencode", serverUrl: opts2.serverUrl, verbose: opts2.verbose ?? false, testTimeout: opts2.testTimeout });
|
|
5075
|
+
}
|
|
5076
|
+
const source = opts2.source;
|
|
5077
|
+
if (!source) {
|
|
5078
|
+
log.error("No datasource configured for multi-issue fix-tests.");
|
|
5079
|
+
return { mode: "fix-tests", success: false, error: "No datasource configured" };
|
|
5080
|
+
}
|
|
5081
|
+
return runMultiIssueFixTests({
|
|
5082
|
+
cwd,
|
|
5083
|
+
issueIds: opts2.issueIds,
|
|
5084
|
+
source,
|
|
5085
|
+
provider: opts2.provider ?? "opencode",
|
|
5086
|
+
serverUrl: opts2.serverUrl,
|
|
5087
|
+
verbose: opts2.verbose ?? false,
|
|
5088
|
+
testTimeout: opts2.testTimeout,
|
|
5089
|
+
org: opts2.org,
|
|
5090
|
+
project: opts2.project
|
|
5091
|
+
});
|
|
4817
5092
|
}
|
|
4818
5093
|
const { mode: _, ...rest } = opts2;
|
|
4819
5094
|
return runner.orchestrate(rest);
|
|
@@ -4842,13 +5117,27 @@ async function boot9(opts) {
|
|
|
4842
5117
|
log.error("--feature and --no-branch are mutually exclusive");
|
|
4843
5118
|
process.exit(1);
|
|
4844
5119
|
}
|
|
4845
|
-
if (m.fixTests && m.issueIds.length > 0) {
|
|
4846
|
-
log.error("--fix-tests cannot be combined with issue IDs");
|
|
4847
|
-
process.exit(1);
|
|
4848
|
-
}
|
|
4849
5120
|
if (m.fixTests) {
|
|
4850
5121
|
const { runFixTestsPipeline: runFixTestsPipeline2 } = await Promise.resolve().then(() => (init_fix_tests_pipeline(), fix_tests_pipeline_exports));
|
|
4851
|
-
|
|
5122
|
+
if (m.issueIds.length === 0) {
|
|
5123
|
+
return runFixTestsPipeline2({ cwd: m.cwd, provider: m.provider, serverUrl: m.serverUrl, verbose: m.verbose, testTimeout: m.testTimeout });
|
|
5124
|
+
}
|
|
5125
|
+
const source = m.issueSource;
|
|
5126
|
+
if (!source) {
|
|
5127
|
+
log.error("No datasource configured. Use --source or run 'dispatch config' to set up defaults.");
|
|
5128
|
+
process.exit(1);
|
|
5129
|
+
}
|
|
5130
|
+
return runMultiIssueFixTests({
|
|
5131
|
+
cwd: m.cwd,
|
|
5132
|
+
issueIds: m.issueIds,
|
|
5133
|
+
source,
|
|
5134
|
+
provider: m.provider,
|
|
5135
|
+
serverUrl: m.serverUrl,
|
|
5136
|
+
verbose: m.verbose,
|
|
5137
|
+
testTimeout: m.testTimeout,
|
|
5138
|
+
org: m.org,
|
|
5139
|
+
project: m.project
|
|
5140
|
+
});
|
|
4852
5141
|
}
|
|
4853
5142
|
if (m.spec) {
|
|
4854
5143
|
return this.generateSpecs({
|
|
@@ -4954,13 +5243,14 @@ var HELP = `
|
|
|
4954
5243
|
dispatch --respec <glob> Regenerate specs matching a glob pattern
|
|
4955
5244
|
dispatch --spec "description" Generate a spec from an inline text description
|
|
4956
5245
|
dispatch --fix-tests Run tests and fix failures via AI agent
|
|
5246
|
+
dispatch --fix-tests <ids> Fix tests on specific issue branches (in worktrees)
|
|
4957
5247
|
|
|
4958
5248
|
Dispatch options:
|
|
4959
5249
|
--dry-run List tasks without dispatching (also works with --spec)
|
|
4960
5250
|
--no-plan Skip the planner agent, dispatch directly
|
|
4961
5251
|
--no-branch Skip branch creation, push, and PR lifecycle
|
|
4962
5252
|
--no-worktree Skip git worktree isolation for parallel issues
|
|
4963
|
-
--feature
|
|
5253
|
+
--feature [name] Group issues into a single feature branch and PR
|
|
4964
5254
|
--force Ignore prior run state and re-run all tasks
|
|
4965
5255
|
--concurrency <n> Max parallel dispatches (default: min(cpus, freeMB/500), max: ${MAX_CONCURRENCY})
|
|
4966
5256
|
--provider <name> Agent backend: ${PROVIDER_NAMES.join(", ")} (default: opencode)
|
|
@@ -5006,6 +5296,12 @@ var HELP = `
|
|
|
5006
5296
|
dispatch --respec "specs/*.md"
|
|
5007
5297
|
dispatch --spec "add dark mode toggle to settings page"
|
|
5008
5298
|
dispatch --spec "feature A should do x" --provider copilot
|
|
5299
|
+
dispatch --feature
|
|
5300
|
+
dispatch --feature my-feature
|
|
5301
|
+
dispatch --fix-tests
|
|
5302
|
+
dispatch --fix-tests 14
|
|
5303
|
+
dispatch --fix-tests 14 15 16
|
|
5304
|
+
dispatch --fix-tests 14,15,16
|
|
5009
5305
|
dispatch config
|
|
5010
5306
|
`.trimStart();
|
|
5011
5307
|
function parseArgs(argv) {
|
|
@@ -5015,7 +5311,7 @@ function parseArgs(argv) {
|
|
|
5015
5311
|
},
|
|
5016
5312
|
writeErr: () => {
|
|
5017
5313
|
}
|
|
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(
|
|
5314
|
+
}).helpOption(false).argument("[issueIds...]").option("-h, --help", "Show help").option("-v, --version", "Show version").option("--dry-run", "List tasks without dispatching").option("--no-plan", "Skip the planner agent").option("--no-branch", "Skip branch creation").option("--no-worktree", "Skip git worktree isolation").option("--feature [name]", "Group issues into a single feature branch").option("--force", "Ignore prior run state").option("--verbose", "Show detailed debug output").option("--fix-tests", "Run tests and fix failures (optionally pass issue IDs to target specific branches)").option("--spec <values...>", "Spec mode: issue numbers, glob, or text").option("--respec [values...]", "Regenerate specs").addOption(
|
|
5019
5315
|
new Option("--provider <name>", "Agent backend").choices(PROVIDER_NAMES)
|
|
5020
5316
|
).addOption(
|
|
5021
5317
|
new Option("--source <name>", "Issue source").choices(
|
|
@@ -5063,7 +5359,7 @@ function parseArgs(argv) {
|
|
|
5063
5359
|
if (isNaN(n) || n <= 0) throw new CommanderError(1, "commander.invalidArgument", "--test-timeout must be a positive number (minutes)");
|
|
5064
5360
|
return n;
|
|
5065
5361
|
}
|
|
5066
|
-
).option("--cwd <dir>", "Working directory", (val) =>
|
|
5362
|
+
).option("--cwd <dir>", "Working directory", (val) => resolve4(val)).option("--output-dir <dir>", "Output directory", (val) => resolve4(val)).option("--org <url>", "Azure DevOps organization URL").option("--project <name>", "Azure DevOps project name").option("--server-url <url>", "Provider server URL");
|
|
5067
5363
|
try {
|
|
5068
5364
|
program.parse(argv, { from: "user" });
|
|
5069
5365
|
} catch (err) {
|
|
@@ -5098,7 +5394,7 @@ function parseArgs(argv) {
|
|
|
5098
5394
|
}
|
|
5099
5395
|
}
|
|
5100
5396
|
if (opts.fixTests) args.fixTests = true;
|
|
5101
|
-
if (opts.feature) args.feature =
|
|
5397
|
+
if (opts.feature) args.feature = opts.feature;
|
|
5102
5398
|
if (opts.source !== void 0) args.issueSource = opts.source;
|
|
5103
5399
|
if (opts.concurrency !== void 0) args.concurrency = opts.concurrency;
|
|
5104
5400
|
if (opts.serverUrl !== void 0) args.serverUrl = opts.serverUrl;
|
|
@@ -5148,7 +5444,7 @@ async function main() {
|
|
|
5148
5444
|
if (rawArgv[0] === "config") {
|
|
5149
5445
|
const configProgram = new Command("dispatch-config").exitOverride().configureOutput({ writeOut: () => {
|
|
5150
5446
|
}, writeErr: () => {
|
|
5151
|
-
} }).helpOption(false).allowUnknownOption(true).allowExcessArguments(true).option("--cwd <dir>", "Working directory", (v) =>
|
|
5447
|
+
} }).helpOption(false).allowUnknownOption(true).allowExcessArguments(true).option("--cwd <dir>", "Working directory", (v) => resolve4(v));
|
|
5152
5448
|
try {
|
|
5153
5449
|
configProgram.parse(rawArgv.slice(1), { from: "user" });
|
|
5154
5450
|
} catch (err) {
|
|
@@ -5179,7 +5475,7 @@ async function main() {
|
|
|
5179
5475
|
process.exit(0);
|
|
5180
5476
|
}
|
|
5181
5477
|
if (args.version) {
|
|
5182
|
-
console.log(`dispatch v${"1.
|
|
5478
|
+
console.log(`dispatch v${"1.4.1"}`);
|
|
5183
5479
|
process.exit(0);
|
|
5184
5480
|
}
|
|
5185
5481
|
const orchestrator = await boot9({ cwd: args.cwd });
|