@pruddiman/dispatch 0.0.1 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +180 -225
- package/dist/cli.js +1666 -1122
- package/dist/cli.js.map +1 -1
- package/package.json +11 -4
package/dist/cli.js
CHANGED
|
@@ -9,42 +9,156 @@ 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() {
|
|
93
|
+
const envLevel = process.env.LOG_LEVEL?.toLowerCase();
|
|
94
|
+
if (envLevel && Object.hasOwn(LOG_LEVEL_SEVERITY, envLevel)) {
|
|
95
|
+
return envLevel;
|
|
96
|
+
}
|
|
97
|
+
if (process.env.DEBUG) {
|
|
98
|
+
return "debug";
|
|
99
|
+
}
|
|
100
|
+
return "info";
|
|
101
|
+
}
|
|
102
|
+
function shouldLog(level) {
|
|
103
|
+
return LOG_LEVEL_SEVERITY[level] >= LOG_LEVEL_SEVERITY[currentLevel];
|
|
104
|
+
}
|
|
105
|
+
function stripAnsi(str) {
|
|
106
|
+
return str.replace(/\x1B\[[0-9;]*m/g, "");
|
|
107
|
+
}
|
|
108
|
+
var LOG_LEVEL_SEVERITY, currentLevel, MAX_CAUSE_CHAIN_DEPTH, log;
|
|
15
109
|
var init_logger = __esm({
|
|
16
110
|
"src/helpers/logger.ts"() {
|
|
17
111
|
"use strict";
|
|
112
|
+
init_file_logger();
|
|
113
|
+
LOG_LEVEL_SEVERITY = {
|
|
114
|
+
debug: 0,
|
|
115
|
+
info: 1,
|
|
116
|
+
warn: 2,
|
|
117
|
+
error: 3
|
|
118
|
+
};
|
|
119
|
+
currentLevel = resolveLogLevel();
|
|
18
120
|
MAX_CAUSE_CHAIN_DEPTH = 5;
|
|
19
121
|
log = {
|
|
20
|
-
/** When true, `debug()` messages are printed. Set by `--verbose`. */
|
|
21
122
|
verbose: false,
|
|
22
123
|
info(msg) {
|
|
124
|
+
if (!shouldLog("info")) return;
|
|
23
125
|
console.log(chalk.blue("\u2139"), msg);
|
|
126
|
+
fileLoggerStorage.getStore()?.info(stripAnsi(msg));
|
|
24
127
|
},
|
|
25
128
|
success(msg) {
|
|
129
|
+
if (!shouldLog("info")) return;
|
|
26
130
|
console.log(chalk.green("\u2714"), msg);
|
|
131
|
+
fileLoggerStorage.getStore()?.success(stripAnsi(msg));
|
|
27
132
|
},
|
|
28
133
|
warn(msg) {
|
|
29
|
-
|
|
134
|
+
if (!shouldLog("warn")) return;
|
|
135
|
+
console.error(chalk.yellow("\u26A0"), msg);
|
|
136
|
+
fileLoggerStorage.getStore()?.warn(stripAnsi(msg));
|
|
30
137
|
},
|
|
31
138
|
error(msg) {
|
|
139
|
+
if (!shouldLog("error")) return;
|
|
32
140
|
console.error(chalk.red("\u2716"), msg);
|
|
141
|
+
fileLoggerStorage.getStore()?.error(stripAnsi(msg));
|
|
33
142
|
},
|
|
34
143
|
task(index, total, msg) {
|
|
144
|
+
if (!shouldLog("info")) return;
|
|
35
145
|
console.log(chalk.cyan(`[${index + 1}/${total}]`), msg);
|
|
146
|
+
fileLoggerStorage.getStore()?.task(stripAnsi(`[${index + 1}/${total}] ${msg}`));
|
|
36
147
|
},
|
|
37
148
|
dim(msg) {
|
|
149
|
+
if (!shouldLog("info")) return;
|
|
38
150
|
console.log(chalk.dim(msg));
|
|
151
|
+
fileLoggerStorage.getStore()?.dim(stripAnsi(msg));
|
|
39
152
|
},
|
|
40
153
|
/**
|
|
41
|
-
* Print a debug/verbose message. Only visible when
|
|
42
|
-
* Messages are prefixed with a dim arrow to visually nest
|
|
43
|
-
* preceding info/error line.
|
|
154
|
+
* Print a debug/verbose message. Only visible when the log level is
|
|
155
|
+
* `"debug"`. Messages are prefixed with a dim arrow to visually nest
|
|
156
|
+
* them under the preceding info/error line.
|
|
44
157
|
*/
|
|
45
158
|
debug(msg) {
|
|
46
|
-
if (!
|
|
159
|
+
if (!shouldLog("debug")) return;
|
|
47
160
|
console.log(chalk.dim(` \u2937 ${msg}`));
|
|
161
|
+
fileLoggerStorage.getStore()?.debug(stripAnsi(msg));
|
|
48
162
|
},
|
|
49
163
|
/**
|
|
50
164
|
* Extract and format the full error cause chain. Node.js network errors
|
|
@@ -83,6 +197,26 @@ var init_logger = __esm({
|
|
|
83
197
|
return "";
|
|
84
198
|
}
|
|
85
199
|
};
|
|
200
|
+
Object.defineProperty(log, "verbose", {
|
|
201
|
+
get() {
|
|
202
|
+
return currentLevel === "debug";
|
|
203
|
+
},
|
|
204
|
+
set(value) {
|
|
205
|
+
currentLevel = value ? "debug" : "info";
|
|
206
|
+
},
|
|
207
|
+
enumerable: true,
|
|
208
|
+
configurable: true
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// src/helpers/guards.ts
|
|
214
|
+
function hasProperty(value, key) {
|
|
215
|
+
return typeof value === "object" && value !== null && Object.prototype.hasOwnProperty.call(value, key);
|
|
216
|
+
}
|
|
217
|
+
var init_guards = __esm({
|
|
218
|
+
"src/helpers/guards.ts"() {
|
|
219
|
+
"use strict";
|
|
86
220
|
}
|
|
87
221
|
});
|
|
88
222
|
|
|
@@ -183,6 +317,7 @@ async function boot(opts) {
|
|
|
183
317
|
},
|
|
184
318
|
async prompt(sessionId, text) {
|
|
185
319
|
log.debug(`Sending async prompt to session ${sessionId} (${text.length} chars)...`);
|
|
320
|
+
let controller;
|
|
186
321
|
try {
|
|
187
322
|
const { error: promptError } = await client.session.promptAsync({
|
|
188
323
|
path: { id: sessionId },
|
|
@@ -195,11 +330,11 @@ async function boot(opts) {
|
|
|
195
330
|
throw new Error(`OpenCode promptAsync failed: ${JSON.stringify(promptError)}`);
|
|
196
331
|
}
|
|
197
332
|
log.debug("Async prompt accepted, subscribing to events...");
|
|
198
|
-
|
|
199
|
-
const { stream } = await client.event.subscribe({
|
|
200
|
-
signal: controller.signal
|
|
201
|
-
});
|
|
333
|
+
controller = new AbortController();
|
|
202
334
|
try {
|
|
335
|
+
const { stream } = await client.event.subscribe({
|
|
336
|
+
signal: controller.signal
|
|
337
|
+
});
|
|
203
338
|
for await (const event of stream) {
|
|
204
339
|
if (!isSessionEvent(event, sessionId)) continue;
|
|
205
340
|
if (event.type === "message.part.updated" && event.properties.part.type === "text") {
|
|
@@ -221,7 +356,7 @@ async function boot(opts) {
|
|
|
221
356
|
}
|
|
222
357
|
}
|
|
223
358
|
} finally {
|
|
224
|
-
controller.abort();
|
|
359
|
+
if (controller && !controller.signal.aborted) controller.abort();
|
|
225
360
|
}
|
|
226
361
|
const { data: messages } = await client.session.messages({
|
|
227
362
|
path: { id: sessionId }
|
|
@@ -235,7 +370,7 @@ async function boot(opts) {
|
|
|
235
370
|
log.debug("No assistant message found in session");
|
|
236
371
|
return null;
|
|
237
372
|
}
|
|
238
|
-
if (lastAssistant.info
|
|
373
|
+
if (hasProperty(lastAssistant.info, "error") && lastAssistant.info.error) {
|
|
239
374
|
throw new Error(
|
|
240
375
|
`OpenCode assistant error: ${JSON.stringify(lastAssistant.info.error)}`
|
|
241
376
|
);
|
|
@@ -265,11 +400,14 @@ async function boot(opts) {
|
|
|
265
400
|
}
|
|
266
401
|
function isSessionEvent(event, sessionId) {
|
|
267
402
|
const props = event.properties;
|
|
268
|
-
if (props
|
|
269
|
-
|
|
403
|
+
if (!hasProperty(props, "sessionID") && !hasProperty(props, "info") && !hasProperty(props, "part")) {
|
|
404
|
+
return false;
|
|
405
|
+
}
|
|
406
|
+
if (hasProperty(props, "sessionID") && props.sessionID === sessionId) return true;
|
|
407
|
+
if (hasProperty(props, "info") && hasProperty(props.info, "sessionID") && props.info.sessionID === sessionId) {
|
|
270
408
|
return true;
|
|
271
409
|
}
|
|
272
|
-
if (props
|
|
410
|
+
if (hasProperty(props, "part") && hasProperty(props.part, "sessionID") && props.part.sessionID === sessionId) {
|
|
273
411
|
return true;
|
|
274
412
|
}
|
|
275
413
|
return false;
|
|
@@ -278,6 +416,52 @@ var init_opencode = __esm({
|
|
|
278
416
|
"src/providers/opencode.ts"() {
|
|
279
417
|
"use strict";
|
|
280
418
|
init_logger();
|
|
419
|
+
init_guards();
|
|
420
|
+
}
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
// src/helpers/timeout.ts
|
|
424
|
+
function withTimeout(promise, ms, label) {
|
|
425
|
+
const p = new Promise((resolve4, reject) => {
|
|
426
|
+
let settled = false;
|
|
427
|
+
const timer = setTimeout(() => {
|
|
428
|
+
if (settled) return;
|
|
429
|
+
settled = true;
|
|
430
|
+
reject(new TimeoutError(ms, label));
|
|
431
|
+
}, ms);
|
|
432
|
+
promise.then(
|
|
433
|
+
(value) => {
|
|
434
|
+
if (settled) return;
|
|
435
|
+
settled = true;
|
|
436
|
+
clearTimeout(timer);
|
|
437
|
+
resolve4(value);
|
|
438
|
+
},
|
|
439
|
+
(err) => {
|
|
440
|
+
if (settled) return;
|
|
441
|
+
settled = true;
|
|
442
|
+
clearTimeout(timer);
|
|
443
|
+
reject(err);
|
|
444
|
+
}
|
|
445
|
+
);
|
|
446
|
+
});
|
|
447
|
+
p.catch(() => {
|
|
448
|
+
});
|
|
449
|
+
return p;
|
|
450
|
+
}
|
|
451
|
+
var TimeoutError;
|
|
452
|
+
var init_timeout = __esm({
|
|
453
|
+
"src/helpers/timeout.ts"() {
|
|
454
|
+
"use strict";
|
|
455
|
+
TimeoutError = class extends Error {
|
|
456
|
+
/** Optional label identifying the operation that timed out. */
|
|
457
|
+
label;
|
|
458
|
+
constructor(ms, label) {
|
|
459
|
+
const suffix = label ? ` [${label}]` : "";
|
|
460
|
+
super(`Timed out after ${ms}ms${suffix}`);
|
|
461
|
+
this.name = "TimeoutError";
|
|
462
|
+
this.label = label;
|
|
463
|
+
}
|
|
464
|
+
};
|
|
281
465
|
}
|
|
282
466
|
});
|
|
283
467
|
|
|
@@ -354,18 +538,25 @@ async function boot2(opts) {
|
|
|
354
538
|
try {
|
|
355
539
|
await session.send({ prompt: text });
|
|
356
540
|
log.debug("Async prompt accepted, waiting for session to become idle...");
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
541
|
+
let unsubIdle;
|
|
542
|
+
let unsubErr;
|
|
543
|
+
try {
|
|
544
|
+
await withTimeout(
|
|
545
|
+
new Promise((resolve4, reject) => {
|
|
546
|
+
unsubIdle = session.on("session.idle", () => {
|
|
547
|
+
resolve4();
|
|
548
|
+
});
|
|
549
|
+
unsubErr = session.on("session.error", (event) => {
|
|
550
|
+
reject(new Error(`Copilot session error: ${event.data.message}`));
|
|
551
|
+
});
|
|
552
|
+
}),
|
|
553
|
+
3e5,
|
|
554
|
+
"copilot session ready"
|
|
555
|
+
);
|
|
556
|
+
} finally {
|
|
557
|
+
unsubIdle?.();
|
|
558
|
+
unsubErr?.();
|
|
559
|
+
}
|
|
369
560
|
log.debug("Session went idle, fetching result...");
|
|
370
561
|
const events = await session.getMessages();
|
|
371
562
|
const last = [...events].reverse().find((e) => e.type === "assistant.message");
|
|
@@ -396,6 +587,7 @@ var init_copilot = __esm({
|
|
|
396
587
|
"src/providers/copilot.ts"() {
|
|
397
588
|
"use strict";
|
|
398
589
|
init_logger();
|
|
590
|
+
init_timeout();
|
|
399
591
|
}
|
|
400
592
|
});
|
|
401
593
|
|
|
@@ -502,7 +694,8 @@ async function boot4(opts) {
|
|
|
502
694
|
model,
|
|
503
695
|
config: { model, instructions: "" },
|
|
504
696
|
approvalPolicy: "full-auto",
|
|
505
|
-
|
|
697
|
+
...opts?.cwd ? { rootDir: opts.cwd } : {},
|
|
698
|
+
additionalWritableRoots: [],
|
|
506
699
|
getCommandConfirmation: async () => ({ approved: true }),
|
|
507
700
|
onItem: () => {
|
|
508
701
|
},
|
|
@@ -567,7 +760,9 @@ import { execFile as execFile6 } from "child_process";
|
|
|
567
760
|
import { promisify as promisify6 } from "util";
|
|
568
761
|
async function checkProviderInstalled(name) {
|
|
569
762
|
try {
|
|
570
|
-
await exec6(PROVIDER_BINARIES[name], ["--version"]
|
|
763
|
+
await exec6(PROVIDER_BINARIES[name], ["--version"], {
|
|
764
|
+
shell: process.platform === "win32"
|
|
765
|
+
});
|
|
571
766
|
return true;
|
|
572
767
|
} catch {
|
|
573
768
|
return false;
|
|
@@ -661,11 +856,11 @@ __export(fix_tests_pipeline_exports, {
|
|
|
661
856
|
runTestCommand: () => runTestCommand
|
|
662
857
|
});
|
|
663
858
|
import { readFile as readFile8 } from "fs/promises";
|
|
664
|
-
import { join as
|
|
859
|
+
import { join as join11 } from "path";
|
|
665
860
|
import { execFile as execFileCb } from "child_process";
|
|
666
861
|
async function detectTestCommand(cwd) {
|
|
667
862
|
try {
|
|
668
|
-
const raw = await readFile8(
|
|
863
|
+
const raw = await readFile8(join11(cwd, "package.json"), "utf-8");
|
|
669
864
|
let pkg;
|
|
670
865
|
try {
|
|
671
866
|
pkg = JSON.parse(raw);
|
|
@@ -685,7 +880,7 @@ async function detectTestCommand(cwd) {
|
|
|
685
880
|
}
|
|
686
881
|
}
|
|
687
882
|
function runTestCommand(command, cwd) {
|
|
688
|
-
return new Promise((
|
|
883
|
+
return new Promise((resolve4) => {
|
|
689
884
|
const [cmd, ...args] = command.split(" ");
|
|
690
885
|
execFileCb(
|
|
691
886
|
cmd,
|
|
@@ -693,7 +888,7 @@ function runTestCommand(command, cwd) {
|
|
|
693
888
|
{ cwd, maxBuffer: 10 * 1024 * 1024 },
|
|
694
889
|
(error, stdout, stderr) => {
|
|
695
890
|
const exitCode = error && "code" in error ? error.code ?? 1 : error ? 1 : 0;
|
|
696
|
-
|
|
891
|
+
resolve4({ exitCode, stdout, stderr, command });
|
|
697
892
|
}
|
|
698
893
|
);
|
|
699
894
|
});
|
|
@@ -740,46 +935,66 @@ async function runFixTestsPipeline(opts) {
|
|
|
740
935
|
log.dim(` Working directory: ${cwd}`);
|
|
741
936
|
return { mode: "fix-tests", success: false };
|
|
742
937
|
}
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
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);
|
|
762
970
|
await instance.cleanup();
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
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 };
|
|
772
986
|
}
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
)
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
987
|
+
};
|
|
988
|
+
if (fileLogger) {
|
|
989
|
+
return fileLoggerStorage.run(fileLogger, async () => {
|
|
990
|
+
try {
|
|
991
|
+
return await pipelineBody();
|
|
992
|
+
} finally {
|
|
993
|
+
fileLogger.close();
|
|
994
|
+
}
|
|
995
|
+
});
|
|
782
996
|
}
|
|
997
|
+
return pipelineBody();
|
|
783
998
|
}
|
|
784
999
|
var init_fix_tests_pipeline = __esm({
|
|
785
1000
|
"src/orchestrator/fix-tests-pipeline.ts"() {
|
|
@@ -787,11 +1002,13 @@ var init_fix_tests_pipeline = __esm({
|
|
|
787
1002
|
init_providers();
|
|
788
1003
|
init_cleanup();
|
|
789
1004
|
init_logger();
|
|
1005
|
+
init_file_logger();
|
|
790
1006
|
}
|
|
791
1007
|
});
|
|
792
1008
|
|
|
793
1009
|
// src/cli.ts
|
|
794
|
-
import { resolve, join as
|
|
1010
|
+
import { resolve as resolve3, join as join12 } from "path";
|
|
1011
|
+
import { Command, Option, CommanderError } from "commander";
|
|
795
1012
|
|
|
796
1013
|
// src/spec-generator.ts
|
|
797
1014
|
import { cpus, freemem } from "os";
|
|
@@ -806,14 +1023,21 @@ import { promisify } from "util";
|
|
|
806
1023
|
|
|
807
1024
|
// src/helpers/slugify.ts
|
|
808
1025
|
var MAX_SLUG_LENGTH = 60;
|
|
809
|
-
function slugify(
|
|
810
|
-
const slug =
|
|
1026
|
+
function slugify(input3, maxLength) {
|
|
1027
|
+
const slug = input3.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
811
1028
|
return maxLength != null ? slug.slice(0, maxLength) : slug;
|
|
812
1029
|
}
|
|
813
1030
|
|
|
814
1031
|
// src/datasources/github.ts
|
|
815
1032
|
init_logger();
|
|
816
1033
|
var exec = promisify(execFile);
|
|
1034
|
+
var InvalidBranchNameError = class extends Error {
|
|
1035
|
+
constructor(branch, reason) {
|
|
1036
|
+
const detail = reason ? ` (${reason})` : "";
|
|
1037
|
+
super(`Invalid branch name: "${branch}"${detail}`);
|
|
1038
|
+
this.name = "InvalidBranchNameError";
|
|
1039
|
+
}
|
|
1040
|
+
};
|
|
817
1041
|
async function git(args, cwd) {
|
|
818
1042
|
const { stdout } = await exec("git", args, { cwd });
|
|
819
1043
|
return stdout;
|
|
@@ -822,16 +1046,35 @@ async function gh(args, cwd) {
|
|
|
822
1046
|
const { stdout } = await exec("gh", args, { cwd });
|
|
823
1047
|
return stdout;
|
|
824
1048
|
}
|
|
1049
|
+
var VALID_BRANCH_NAME_RE = /^[a-zA-Z0-9._\-/]+$/;
|
|
1050
|
+
function isValidBranchName(name) {
|
|
1051
|
+
if (name.length === 0 || name.length > 255) return false;
|
|
1052
|
+
if (!VALID_BRANCH_NAME_RE.test(name)) return false;
|
|
1053
|
+
if (name.startsWith("/") || name.endsWith("/")) return false;
|
|
1054
|
+
if (name.includes("..")) return false;
|
|
1055
|
+
if (name.endsWith(".lock")) return false;
|
|
1056
|
+
if (name.includes("@{")) return false;
|
|
1057
|
+
if (name.includes("//")) return false;
|
|
1058
|
+
return true;
|
|
1059
|
+
}
|
|
825
1060
|
function buildBranchName(issueNumber, title, username = "unknown") {
|
|
826
1061
|
const slug = slugify(title, 50);
|
|
827
1062
|
return `${username}/dispatch/${issueNumber}-${slug}`;
|
|
828
1063
|
}
|
|
829
1064
|
async function getDefaultBranch(cwd) {
|
|
1065
|
+
const PREFIX = "refs/remotes/origin/";
|
|
830
1066
|
try {
|
|
831
1067
|
const ref = await git(["symbolic-ref", "refs/remotes/origin/HEAD"], cwd);
|
|
832
|
-
const
|
|
833
|
-
|
|
834
|
-
|
|
1068
|
+
const trimmed = ref.trim();
|
|
1069
|
+
const branch = trimmed.startsWith(PREFIX) ? trimmed.slice(PREFIX.length) : trimmed;
|
|
1070
|
+
if (!isValidBranchName(branch)) {
|
|
1071
|
+
throw new InvalidBranchNameError(branch, "from symbolic-ref output");
|
|
1072
|
+
}
|
|
1073
|
+
return branch;
|
|
1074
|
+
} catch (err) {
|
|
1075
|
+
if (err instanceof InvalidBranchNameError) {
|
|
1076
|
+
throw err;
|
|
1077
|
+
}
|
|
835
1078
|
try {
|
|
836
1079
|
await git(["rev-parse", "--verify", "main"], cwd);
|
|
837
1080
|
return "main";
|
|
@@ -842,6 +1085,9 @@ async function getDefaultBranch(cwd) {
|
|
|
842
1085
|
}
|
|
843
1086
|
var datasource = {
|
|
844
1087
|
name: "github",
|
|
1088
|
+
supportsGit() {
|
|
1089
|
+
return true;
|
|
1090
|
+
},
|
|
845
1091
|
async list(opts = {}) {
|
|
846
1092
|
const cwd = opts.cwd || process.cwd();
|
|
847
1093
|
const { stdout } = await exec(
|
|
@@ -1043,8 +1289,30 @@ async function detectWorkItemType(opts = {}) {
|
|
|
1043
1289
|
}
|
|
1044
1290
|
var datasource2 = {
|
|
1045
1291
|
name: "azdevops",
|
|
1292
|
+
supportsGit() {
|
|
1293
|
+
return true;
|
|
1294
|
+
},
|
|
1046
1295
|
async list(opts = {}) {
|
|
1047
|
-
const
|
|
1296
|
+
const conditions = [
|
|
1297
|
+
"[System.State] <> 'Closed'",
|
|
1298
|
+
"[System.State] <> 'Removed'"
|
|
1299
|
+
];
|
|
1300
|
+
if (opts.iteration) {
|
|
1301
|
+
const iterValue = String(opts.iteration).trim();
|
|
1302
|
+
if (iterValue === "@CurrentIteration") {
|
|
1303
|
+
conditions.push(`[System.IterationPath] UNDER @CurrentIteration`);
|
|
1304
|
+
} else {
|
|
1305
|
+
const escaped = iterValue.replace(/'/g, "''");
|
|
1306
|
+
if (escaped) conditions.push(`[System.IterationPath] UNDER '${escaped}'`);
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
if (opts.area) {
|
|
1310
|
+
const area = String(opts.area).trim().replace(/'/g, "''");
|
|
1311
|
+
if (area) {
|
|
1312
|
+
conditions.push(`[System.AreaPath] UNDER '${area}'`);
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
const wiql = `SELECT [System.Id] FROM workitems WHERE ${conditions.join(" AND ")} ORDER BY [System.CreatedDate] DESC`;
|
|
1048
1316
|
const args = ["boards", "query", "--wiql", wiql, "--output", "json"];
|
|
1049
1317
|
if (opts.org) args.push("--org", opts.org);
|
|
1050
1318
|
if (opts.project) args.push("--project", opts.project);
|
|
@@ -1104,7 +1372,13 @@ var datasource2 = {
|
|
|
1104
1372
|
state: fields["System.State"] ?? "",
|
|
1105
1373
|
url: item._links?.html?.href ?? item.url ?? "",
|
|
1106
1374
|
comments,
|
|
1107
|
-
acceptanceCriteria: fields["Microsoft.VSTS.Common.AcceptanceCriteria"] ?? ""
|
|
1375
|
+
acceptanceCriteria: fields["Microsoft.VSTS.Common.AcceptanceCriteria"] ?? "",
|
|
1376
|
+
iterationPath: fields["System.IterationPath"] || void 0,
|
|
1377
|
+
areaPath: fields["System.AreaPath"] || void 0,
|
|
1378
|
+
assignee: fields["System.AssignedTo"]?.displayName || void 0,
|
|
1379
|
+
priority: fields["Microsoft.VSTS.Common.Priority"] ?? void 0,
|
|
1380
|
+
storyPoints: fields["Microsoft.VSTS.Scheduling.StoryPoints"] ?? fields["Microsoft.VSTS.Scheduling.Effort"] ?? fields["Microsoft.VSTS.Scheduling.Size"] ?? void 0,
|
|
1381
|
+
workItemType: fields["System.WorkItemType"] || void 0
|
|
1108
1382
|
};
|
|
1109
1383
|
},
|
|
1110
1384
|
async update(issueId, title, body, opts = {}) {
|
|
@@ -1177,7 +1451,13 @@ var datasource2 = {
|
|
|
1177
1451
|
state: fields["System.State"] ?? "New",
|
|
1178
1452
|
url: item._links?.html?.href ?? item.url ?? "",
|
|
1179
1453
|
comments: [],
|
|
1180
|
-
acceptanceCriteria: fields["Microsoft.VSTS.Common.AcceptanceCriteria"] ?? ""
|
|
1454
|
+
acceptanceCriteria: fields["Microsoft.VSTS.Common.AcceptanceCriteria"] ?? "",
|
|
1455
|
+
iterationPath: fields["System.IterationPath"] || void 0,
|
|
1456
|
+
areaPath: fields["System.AreaPath"] || void 0,
|
|
1457
|
+
assignee: fields["System.AssignedTo"]?.displayName || void 0,
|
|
1458
|
+
priority: fields["Microsoft.VSTS.Common.Priority"] ?? void 0,
|
|
1459
|
+
storyPoints: fields["Microsoft.VSTS.Scheduling.StoryPoints"] ?? fields["Microsoft.VSTS.Scheduling.Effort"] ?? fields["Microsoft.VSTS.Scheduling.Size"] ?? void 0,
|
|
1460
|
+
workItemType: fields["System.WorkItemType"] || workItemType
|
|
1181
1461
|
};
|
|
1182
1462
|
},
|
|
1183
1463
|
async getDefaultBranch(opts) {
|
|
@@ -1197,12 +1477,25 @@ var datasource2 = {
|
|
|
1197
1477
|
async getUsername(opts) {
|
|
1198
1478
|
try {
|
|
1199
1479
|
const { stdout } = await exec2("git", ["config", "user.name"], { cwd: opts.cwd });
|
|
1200
|
-
const name = stdout.trim();
|
|
1201
|
-
if (
|
|
1202
|
-
|
|
1480
|
+
const name = slugify(stdout.trim());
|
|
1481
|
+
if (name) return name;
|
|
1482
|
+
} catch {
|
|
1483
|
+
}
|
|
1484
|
+
try {
|
|
1485
|
+
const { stdout } = await exec2("az", ["account", "show", "--query", "user.name", "-o", "tsv"], { cwd: opts.cwd });
|
|
1486
|
+
const name = slugify(stdout.trim());
|
|
1487
|
+
if (name) return name;
|
|
1488
|
+
} catch {
|
|
1489
|
+
}
|
|
1490
|
+
try {
|
|
1491
|
+
const { stdout } = await exec2("az", ["account", "show", "--query", "user.principalName", "-o", "tsv"], { cwd: opts.cwd });
|
|
1492
|
+
const principal = stdout.trim();
|
|
1493
|
+
const prefix = principal.split("@")[0];
|
|
1494
|
+
const name = slugify(prefix);
|
|
1495
|
+
if (name) return name;
|
|
1203
1496
|
} catch {
|
|
1204
|
-
return "unknown";
|
|
1205
1497
|
}
|
|
1498
|
+
return "unknown";
|
|
1206
1499
|
},
|
|
1207
1500
|
buildBranchName(issueNumber, title, username) {
|
|
1208
1501
|
const slug = slugify(title, 50);
|
|
@@ -1334,13 +1627,27 @@ async function fetchComments(workItemId, opts) {
|
|
|
1334
1627
|
// src/datasources/md.ts
|
|
1335
1628
|
import { execFile as execFile3 } from "child_process";
|
|
1336
1629
|
import { readFile, writeFile, readdir, mkdir, rename } from "fs/promises";
|
|
1337
|
-
import { join, parse as parsePath } from "path";
|
|
1630
|
+
import { join as join2, parse as parsePath } from "path";
|
|
1338
1631
|
import { promisify as promisify3 } from "util";
|
|
1632
|
+
|
|
1633
|
+
// src/helpers/errors.ts
|
|
1634
|
+
var UnsupportedOperationError = class extends Error {
|
|
1635
|
+
/** The name of the operation that is not supported. */
|
|
1636
|
+
operation;
|
|
1637
|
+
constructor(operation, message) {
|
|
1638
|
+
const msg = message ?? `Operation not supported: ${operation}`;
|
|
1639
|
+
super(msg);
|
|
1640
|
+
this.name = "UnsupportedOperationError";
|
|
1641
|
+
this.operation = operation;
|
|
1642
|
+
}
|
|
1643
|
+
};
|
|
1644
|
+
|
|
1645
|
+
// src/datasources/md.ts
|
|
1339
1646
|
var exec3 = promisify3(execFile3);
|
|
1340
1647
|
var DEFAULT_DIR = ".dispatch/specs";
|
|
1341
1648
|
function resolveDir(opts) {
|
|
1342
1649
|
const cwd = opts?.cwd ?? process.cwd();
|
|
1343
|
-
return
|
|
1650
|
+
return join2(cwd, DEFAULT_DIR);
|
|
1344
1651
|
}
|
|
1345
1652
|
function extractTitle(content, filename) {
|
|
1346
1653
|
const match = content.match(/^#\s+(.+)$/m);
|
|
@@ -1365,13 +1672,16 @@ function toIssueDetails(filename, content, dir) {
|
|
|
1365
1672
|
body: content,
|
|
1366
1673
|
labels: [],
|
|
1367
1674
|
state: "open",
|
|
1368
|
-
url:
|
|
1675
|
+
url: join2(dir, filename),
|
|
1369
1676
|
comments: [],
|
|
1370
1677
|
acceptanceCriteria: ""
|
|
1371
1678
|
};
|
|
1372
1679
|
}
|
|
1373
1680
|
var datasource3 = {
|
|
1374
1681
|
name: "md",
|
|
1682
|
+
supportsGit() {
|
|
1683
|
+
return false;
|
|
1684
|
+
},
|
|
1375
1685
|
async list(opts) {
|
|
1376
1686
|
const dir = resolveDir(opts);
|
|
1377
1687
|
let entries;
|
|
@@ -1383,7 +1693,7 @@ var datasource3 = {
|
|
|
1383
1693
|
const mdFiles = entries.filter((f) => f.endsWith(".md")).sort();
|
|
1384
1694
|
const results = [];
|
|
1385
1695
|
for (const filename of mdFiles) {
|
|
1386
|
-
const filePath =
|
|
1696
|
+
const filePath = join2(dir, filename);
|
|
1387
1697
|
const content = await readFile(filePath, "utf-8");
|
|
1388
1698
|
results.push(toIssueDetails(filename, content, dir));
|
|
1389
1699
|
}
|
|
@@ -1392,29 +1702,29 @@ var datasource3 = {
|
|
|
1392
1702
|
async fetch(issueId, opts) {
|
|
1393
1703
|
const dir = resolveDir(opts);
|
|
1394
1704
|
const filename = issueId.endsWith(".md") ? issueId : `${issueId}.md`;
|
|
1395
|
-
const filePath =
|
|
1705
|
+
const filePath = join2(dir, filename);
|
|
1396
1706
|
const content = await readFile(filePath, "utf-8");
|
|
1397
1707
|
return toIssueDetails(filename, content, dir);
|
|
1398
1708
|
},
|
|
1399
1709
|
async update(issueId, _title, body, opts) {
|
|
1400
1710
|
const dir = resolveDir(opts);
|
|
1401
1711
|
const filename = issueId.endsWith(".md") ? issueId : `${issueId}.md`;
|
|
1402
|
-
const filePath =
|
|
1712
|
+
const filePath = join2(dir, filename);
|
|
1403
1713
|
await writeFile(filePath, body, "utf-8");
|
|
1404
1714
|
},
|
|
1405
1715
|
async close(issueId, opts) {
|
|
1406
1716
|
const dir = resolveDir(opts);
|
|
1407
1717
|
const filename = issueId.endsWith(".md") ? issueId : `${issueId}.md`;
|
|
1408
|
-
const filePath =
|
|
1409
|
-
const archiveDir =
|
|
1718
|
+
const filePath = join2(dir, filename);
|
|
1719
|
+
const archiveDir = join2(dir, "archive");
|
|
1410
1720
|
await mkdir(archiveDir, { recursive: true });
|
|
1411
|
-
await rename(filePath,
|
|
1721
|
+
await rename(filePath, join2(archiveDir, filename));
|
|
1412
1722
|
},
|
|
1413
1723
|
async create(title, body, opts) {
|
|
1414
1724
|
const dir = resolveDir(opts);
|
|
1415
1725
|
await mkdir(dir, { recursive: true });
|
|
1416
1726
|
const filename = `${slugify(title)}.md`;
|
|
1417
|
-
const filePath =
|
|
1727
|
+
const filePath = join2(dir, filename);
|
|
1418
1728
|
await writeFile(filePath, body, "utf-8");
|
|
1419
1729
|
return toIssueDetails(filename, body, dir);
|
|
1420
1730
|
},
|
|
@@ -1436,15 +1746,19 @@ var datasource3 = {
|
|
|
1436
1746
|
return `${username}/dispatch/${issueNumber}-${slug}`;
|
|
1437
1747
|
},
|
|
1438
1748
|
async createAndSwitchBranch(_branchName, _opts) {
|
|
1749
|
+
throw new UnsupportedOperationError("createAndSwitchBranch");
|
|
1439
1750
|
},
|
|
1440
1751
|
async switchBranch(_branchName, _opts) {
|
|
1752
|
+
throw new UnsupportedOperationError("switchBranch");
|
|
1441
1753
|
},
|
|
1442
1754
|
async pushBranch(_branchName, _opts) {
|
|
1755
|
+
throw new UnsupportedOperationError("pushBranch");
|
|
1443
1756
|
},
|
|
1444
1757
|
async commitAllChanges(_message, _opts) {
|
|
1758
|
+
throw new UnsupportedOperationError("commitAllChanges");
|
|
1445
1759
|
},
|
|
1446
1760
|
async createPullRequest(_branchName, _issueNumber, _title, _body, _opts) {
|
|
1447
|
-
|
|
1761
|
+
throw new UnsupportedOperationError("createPullRequest");
|
|
1448
1762
|
}
|
|
1449
1763
|
};
|
|
1450
1764
|
|
|
@@ -1490,6 +1804,36 @@ async function detectDatasource(cwd) {
|
|
|
1490
1804
|
}
|
|
1491
1805
|
return null;
|
|
1492
1806
|
}
|
|
1807
|
+
function parseAzDevOpsRemoteUrl(url) {
|
|
1808
|
+
const httpsMatch = url.match(
|
|
1809
|
+
/^https?:\/\/(?:[^@]+@)?dev\.azure\.com\/([^/]+)\/([^/]+)\/_git\//i
|
|
1810
|
+
);
|
|
1811
|
+
if (httpsMatch) {
|
|
1812
|
+
return {
|
|
1813
|
+
orgUrl: `https://dev.azure.com/${decodeURIComponent(httpsMatch[1])}`,
|
|
1814
|
+
project: decodeURIComponent(httpsMatch[2])
|
|
1815
|
+
};
|
|
1816
|
+
}
|
|
1817
|
+
const sshMatch = url.match(
|
|
1818
|
+
/^git@ssh\.dev\.azure\.com:v3\/([^/]+)\/([^/]+)\//i
|
|
1819
|
+
);
|
|
1820
|
+
if (sshMatch) {
|
|
1821
|
+
return {
|
|
1822
|
+
orgUrl: `https://dev.azure.com/${decodeURIComponent(sshMatch[1])}`,
|
|
1823
|
+
project: decodeURIComponent(sshMatch[2])
|
|
1824
|
+
};
|
|
1825
|
+
}
|
|
1826
|
+
const legacyMatch = url.match(
|
|
1827
|
+
/^https?:\/\/([^.]+)\.visualstudio\.com\/(?:DefaultCollection\/)?([^/]+)\/_git\//i
|
|
1828
|
+
);
|
|
1829
|
+
if (legacyMatch) {
|
|
1830
|
+
return {
|
|
1831
|
+
orgUrl: `https://dev.azure.com/${decodeURIComponent(legacyMatch[1])}`,
|
|
1832
|
+
project: decodeURIComponent(legacyMatch[2])
|
|
1833
|
+
};
|
|
1834
|
+
}
|
|
1835
|
+
return null;
|
|
1836
|
+
}
|
|
1493
1837
|
|
|
1494
1838
|
// src/spec-generator.ts
|
|
1495
1839
|
init_logger();
|
|
@@ -1506,16 +1850,16 @@ var RECOGNIZED_H2 = /* @__PURE__ */ new Set([
|
|
|
1506
1850
|
function defaultConcurrency() {
|
|
1507
1851
|
return Math.max(1, Math.min(cpus().length, Math.floor(freemem() / 1024 / 1024 / MB_PER_CONCURRENT_TASK)));
|
|
1508
1852
|
}
|
|
1509
|
-
function isIssueNumbers(
|
|
1510
|
-
if (Array.isArray(
|
|
1511
|
-
return /^\d+(,\s*\d+)*$/.test(
|
|
1853
|
+
function isIssueNumbers(input3) {
|
|
1854
|
+
if (Array.isArray(input3)) return false;
|
|
1855
|
+
return /^\d+(,\s*\d+)*$/.test(input3);
|
|
1512
1856
|
}
|
|
1513
|
-
function isGlobOrFilePath(
|
|
1514
|
-
if (Array.isArray(
|
|
1515
|
-
if (/[*?\[{]/.test(
|
|
1516
|
-
if (/[/\\]/.test(
|
|
1517
|
-
if (
|
|
1518
|
-
if (/\.(md|txt|yaml|yml|json|ts|js|tsx|jsx)$/i.test(
|
|
1857
|
+
function isGlobOrFilePath(input3) {
|
|
1858
|
+
if (Array.isArray(input3)) return true;
|
|
1859
|
+
if (/[*?\[{]/.test(input3)) return true;
|
|
1860
|
+
if (/[/\\]/.test(input3)) return true;
|
|
1861
|
+
if (/^\.\.?[\/\\]/.test(input3)) return true;
|
|
1862
|
+
if (/\.(md|txt|yaml|yml|json|ts|js|tsx|jsx)$/i.test(input3)) return true;
|
|
1519
1863
|
return false;
|
|
1520
1864
|
}
|
|
1521
1865
|
function extractSpecContent(raw) {
|
|
@@ -1641,7 +1985,7 @@ function semverGte(current, minimum) {
|
|
|
1641
1985
|
async function checkPrereqs(context) {
|
|
1642
1986
|
const failures = [];
|
|
1643
1987
|
try {
|
|
1644
|
-
await exec5("git", ["--version"]);
|
|
1988
|
+
await exec5("git", ["--version"], { shell: process.platform === "win32" });
|
|
1645
1989
|
} catch {
|
|
1646
1990
|
failures.push("git is required but was not found on PATH. Install it from https://git-scm.com");
|
|
1647
1991
|
}
|
|
@@ -1653,7 +1997,7 @@ async function checkPrereqs(context) {
|
|
|
1653
1997
|
}
|
|
1654
1998
|
if (context?.datasource === "github") {
|
|
1655
1999
|
try {
|
|
1656
|
-
await exec5("gh", ["--version"]);
|
|
2000
|
+
await exec5("gh", ["--version"], { shell: process.platform === "win32" });
|
|
1657
2001
|
} catch {
|
|
1658
2002
|
failures.push(
|
|
1659
2003
|
"gh (GitHub CLI) is required for the github datasource but was not found on PATH. Install it from https://cli.github.com/"
|
|
@@ -1662,7 +2006,7 @@ async function checkPrereqs(context) {
|
|
|
1662
2006
|
}
|
|
1663
2007
|
if (context?.datasource === "azdevops") {
|
|
1664
2008
|
try {
|
|
1665
|
-
await exec5("az", ["--version"]);
|
|
2009
|
+
await exec5("az", ["--version"], { shell: process.platform === "win32" });
|
|
1666
2010
|
} catch {
|
|
1667
2011
|
failures.push(
|
|
1668
2012
|
"az (Azure CLI) is required for the azdevops datasource but was not found on PATH. Install it from https://learn.microsoft.com/en-us/cli/azure/"
|
|
@@ -1675,17 +2019,23 @@ async function checkPrereqs(context) {
|
|
|
1675
2019
|
// src/helpers/gitignore.ts
|
|
1676
2020
|
init_logger();
|
|
1677
2021
|
import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
|
|
1678
|
-
import { join as
|
|
2022
|
+
import { join as join3 } from "path";
|
|
1679
2023
|
async function ensureGitignoreEntry(repoRoot, entry) {
|
|
1680
|
-
const gitignorePath =
|
|
2024
|
+
const gitignorePath = join3(repoRoot, ".gitignore");
|
|
1681
2025
|
let contents = "";
|
|
1682
2026
|
try {
|
|
1683
2027
|
contents = await readFile2(gitignorePath, "utf8");
|
|
1684
|
-
} catch {
|
|
2028
|
+
} catch (err) {
|
|
2029
|
+
if (err instanceof Error && "code" in err && err.code === "ENOENT") {
|
|
2030
|
+
} else {
|
|
2031
|
+
log.warn(`Could not read .gitignore: ${String(err)}`);
|
|
2032
|
+
return;
|
|
2033
|
+
}
|
|
1685
2034
|
}
|
|
1686
|
-
const lines = contents.split(
|
|
2035
|
+
const lines = contents.split(/\r?\n/);
|
|
1687
2036
|
const bare = entry.replace(/\/$/, "");
|
|
1688
|
-
|
|
2037
|
+
const withSlash = bare + "/";
|
|
2038
|
+
if (lines.includes(entry) || lines.includes(bare) || lines.includes(withSlash)) {
|
|
1689
2039
|
return;
|
|
1690
2040
|
}
|
|
1691
2041
|
try {
|
|
@@ -1700,18 +2050,18 @@ async function ensureGitignoreEntry(repoRoot, entry) {
|
|
|
1700
2050
|
|
|
1701
2051
|
// src/orchestrator/cli-config.ts
|
|
1702
2052
|
init_logger();
|
|
1703
|
-
import { join as
|
|
2053
|
+
import { join as join5 } from "path";
|
|
1704
2054
|
import { access } from "fs/promises";
|
|
1705
2055
|
import { constants } from "fs";
|
|
1706
2056
|
|
|
1707
2057
|
// src/config.ts
|
|
1708
2058
|
init_providers();
|
|
1709
2059
|
import { readFile as readFile3, writeFile as writeFile3, mkdir as mkdir2 } from "fs/promises";
|
|
1710
|
-
import { join as
|
|
2060
|
+
import { join as join4, dirname as dirname2 } from "path";
|
|
1711
2061
|
|
|
1712
2062
|
// src/config-prompts.ts
|
|
1713
2063
|
init_logger();
|
|
1714
|
-
import { select, confirm } from "@inquirer/prompts";
|
|
2064
|
+
import { select, confirm, input as input2 } from "@inquirer/prompts";
|
|
1715
2065
|
import chalk3 from "chalk";
|
|
1716
2066
|
init_providers();
|
|
1717
2067
|
async function runInteractiveConfigWizard(configDir) {
|
|
@@ -1791,6 +2141,54 @@ async function runInteractiveConfigWizard(configDir) {
|
|
|
1791
2141
|
default: datasourceDefault
|
|
1792
2142
|
});
|
|
1793
2143
|
const source = selectedSource === "auto" ? void 0 : selectedSource;
|
|
2144
|
+
let org;
|
|
2145
|
+
let project;
|
|
2146
|
+
let workItemType;
|
|
2147
|
+
let iteration;
|
|
2148
|
+
let area;
|
|
2149
|
+
const effectiveSource = source ?? detectedSource;
|
|
2150
|
+
if (effectiveSource === "azdevops") {
|
|
2151
|
+
let defaultOrg = existing.org ?? "";
|
|
2152
|
+
let defaultProject = existing.project ?? "";
|
|
2153
|
+
try {
|
|
2154
|
+
const remoteUrl = await getGitRemoteUrl(process.cwd());
|
|
2155
|
+
if (remoteUrl) {
|
|
2156
|
+
const parsed = parseAzDevOpsRemoteUrl(remoteUrl);
|
|
2157
|
+
if (parsed) {
|
|
2158
|
+
if (!defaultOrg) defaultOrg = parsed.orgUrl;
|
|
2159
|
+
if (!defaultProject) defaultProject = parsed.project;
|
|
2160
|
+
}
|
|
2161
|
+
}
|
|
2162
|
+
} catch {
|
|
2163
|
+
}
|
|
2164
|
+
console.log();
|
|
2165
|
+
log.info(chalk3.bold("Azure DevOps settings") + chalk3.dim(" (leave empty to skip):"));
|
|
2166
|
+
const orgInput = await input2({
|
|
2167
|
+
message: "Organization URL:",
|
|
2168
|
+
default: defaultOrg || void 0
|
|
2169
|
+
});
|
|
2170
|
+
if (orgInput.trim()) org = orgInput.trim();
|
|
2171
|
+
const projectInput = await input2({
|
|
2172
|
+
message: "Project name:",
|
|
2173
|
+
default: defaultProject || void 0
|
|
2174
|
+
});
|
|
2175
|
+
if (projectInput.trim()) project = projectInput.trim();
|
|
2176
|
+
const workItemTypeInput = await input2({
|
|
2177
|
+
message: "Work item type (e.g. User Story, Bug):",
|
|
2178
|
+
default: existing.workItemType ?? void 0
|
|
2179
|
+
});
|
|
2180
|
+
if (workItemTypeInput.trim()) workItemType = workItemTypeInput.trim();
|
|
2181
|
+
const iterationInput = await input2({
|
|
2182
|
+
message: "Iteration path (e.g. MyProject\\Sprint 1, or @CurrentIteration):",
|
|
2183
|
+
default: existing.iteration ?? void 0
|
|
2184
|
+
});
|
|
2185
|
+
if (iterationInput.trim()) iteration = iterationInput.trim();
|
|
2186
|
+
const areaInput = await input2({
|
|
2187
|
+
message: "Area path (e.g. MyProject\\Team A):",
|
|
2188
|
+
default: existing.area ?? void 0
|
|
2189
|
+
});
|
|
2190
|
+
if (areaInput.trim()) area = areaInput.trim();
|
|
2191
|
+
}
|
|
1794
2192
|
const newConfig = {
|
|
1795
2193
|
provider,
|
|
1796
2194
|
source
|
|
@@ -1798,6 +2196,11 @@ async function runInteractiveConfigWizard(configDir) {
|
|
|
1798
2196
|
if (selectedModel !== void 0) {
|
|
1799
2197
|
newConfig.model = selectedModel;
|
|
1800
2198
|
}
|
|
2199
|
+
if (org !== void 0) newConfig.org = org;
|
|
2200
|
+
if (project !== void 0) newConfig.project = project;
|
|
2201
|
+
if (workItemType !== void 0) newConfig.workItemType = workItemType;
|
|
2202
|
+
if (iteration !== void 0) newConfig.iteration = iteration;
|
|
2203
|
+
if (area !== void 0) newConfig.area = area;
|
|
1801
2204
|
console.log();
|
|
1802
2205
|
log.info(chalk3.bold("Configuration summary:"));
|
|
1803
2206
|
for (const [key, value] of Object.entries(newConfig)) {
|
|
@@ -1824,10 +2227,15 @@ async function runInteractiveConfigWizard(configDir) {
|
|
|
1824
2227
|
}
|
|
1825
2228
|
|
|
1826
2229
|
// src/config.ts
|
|
1827
|
-
var
|
|
2230
|
+
var CONFIG_BOUNDS = {
|
|
2231
|
+
testTimeout: { min: 1, max: 120 },
|
|
2232
|
+
planTimeout: { min: 1, max: 120 },
|
|
2233
|
+
concurrency: { min: 1, max: 64 }
|
|
2234
|
+
};
|
|
2235
|
+
var CONFIG_KEYS = ["provider", "model", "source", "testTimeout", "planTimeout", "concurrency", "org", "project", "workItemType", "iteration", "area"];
|
|
1828
2236
|
function getConfigPath(configDir) {
|
|
1829
|
-
const dir = configDir ??
|
|
1830
|
-
return
|
|
2237
|
+
const dir = configDir ?? join4(process.cwd(), ".dispatch");
|
|
2238
|
+
return join4(dir, "config.json");
|
|
1831
2239
|
}
|
|
1832
2240
|
async function loadConfig(configDir) {
|
|
1833
2241
|
const configPath = getConfigPath(configDir);
|
|
@@ -1840,7 +2248,7 @@ async function loadConfig(configDir) {
|
|
|
1840
2248
|
}
|
|
1841
2249
|
async function saveConfig(config, configDir) {
|
|
1842
2250
|
const configPath = getConfigPath(configDir);
|
|
1843
|
-
await mkdir2(
|
|
2251
|
+
await mkdir2(dirname2(configPath), { recursive: true });
|
|
1844
2252
|
await writeFile3(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
1845
2253
|
}
|
|
1846
2254
|
async function handleConfigCommand(_argv, configDir) {
|
|
@@ -1852,14 +2260,21 @@ var CONFIG_TO_CLI = {
|
|
|
1852
2260
|
provider: "provider",
|
|
1853
2261
|
model: "model",
|
|
1854
2262
|
source: "issueSource",
|
|
1855
|
-
testTimeout: "testTimeout"
|
|
2263
|
+
testTimeout: "testTimeout",
|
|
2264
|
+
planTimeout: "planTimeout",
|
|
2265
|
+
concurrency: "concurrency",
|
|
2266
|
+
org: "org",
|
|
2267
|
+
project: "project",
|
|
2268
|
+
workItemType: "workItemType",
|
|
2269
|
+
iteration: "iteration",
|
|
2270
|
+
area: "area"
|
|
1856
2271
|
};
|
|
1857
2272
|
function setCliField(target, key, value) {
|
|
1858
2273
|
target[key] = value;
|
|
1859
2274
|
}
|
|
1860
2275
|
async function resolveCliConfig(args) {
|
|
1861
2276
|
const { explicitFlags } = args;
|
|
1862
|
-
const configDir =
|
|
2277
|
+
const configDir = join5(args.cwd, ".dispatch");
|
|
1863
2278
|
const config = await loadConfig(configDir);
|
|
1864
2279
|
const merged = { ...args };
|
|
1865
2280
|
for (const configKey of CONFIG_KEYS) {
|
|
@@ -1906,16 +2321,17 @@ async function resolveCliConfig(args) {
|
|
|
1906
2321
|
}
|
|
1907
2322
|
|
|
1908
2323
|
// src/orchestrator/spec-pipeline.ts
|
|
1909
|
-
import { join as
|
|
2324
|
+
import { join as join7 } from "path";
|
|
1910
2325
|
import { mkdir as mkdir4, readFile as readFile5, rename as rename2, unlink as unlink2 } from "fs/promises";
|
|
1911
2326
|
import { glob } from "glob";
|
|
1912
2327
|
init_providers();
|
|
1913
2328
|
|
|
1914
2329
|
// src/agents/spec.ts
|
|
1915
2330
|
import { mkdir as mkdir3, readFile as readFile4, writeFile as writeFile4, unlink } from "fs/promises";
|
|
1916
|
-
import { join as
|
|
2331
|
+
import { join as join6, resolve, sep } from "path";
|
|
1917
2332
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
1918
2333
|
init_logger();
|
|
2334
|
+
init_file_logger();
|
|
1919
2335
|
async function boot5(opts) {
|
|
1920
2336
|
const { provider } = opts;
|
|
1921
2337
|
if (!provider) {
|
|
@@ -1925,11 +2341,22 @@ async function boot5(opts) {
|
|
|
1925
2341
|
name: "spec",
|
|
1926
2342
|
async generate(genOpts) {
|
|
1927
2343
|
const { issue, filePath, fileContent, inlineText, cwd: workingDir, outputPath } = genOpts;
|
|
2344
|
+
const startTime = Date.now();
|
|
1928
2345
|
try {
|
|
1929
|
-
const
|
|
2346
|
+
const resolvedCwd = resolve(workingDir);
|
|
2347
|
+
const resolvedOutput = resolve(outputPath);
|
|
2348
|
+
if (resolvedOutput !== resolvedCwd && !resolvedOutput.startsWith(resolvedCwd + sep)) {
|
|
2349
|
+
return {
|
|
2350
|
+
data: null,
|
|
2351
|
+
success: false,
|
|
2352
|
+
error: `Output path "${outputPath}" escapes the working directory "${workingDir}"`,
|
|
2353
|
+
durationMs: Date.now() - startTime
|
|
2354
|
+
};
|
|
2355
|
+
}
|
|
2356
|
+
const tmpDir = join6(resolvedCwd, ".dispatch", "tmp");
|
|
1930
2357
|
await mkdir3(tmpDir, { recursive: true });
|
|
1931
2358
|
const tmpFilename = `spec-${randomUUID3()}.md`;
|
|
1932
|
-
const tmpPath =
|
|
2359
|
+
const tmpPath = join6(tmpDir, tmpFilename);
|
|
1933
2360
|
let prompt;
|
|
1934
2361
|
if (issue) {
|
|
1935
2362
|
prompt = buildSpecPrompt(issue, workingDir, tmpPath);
|
|
@@ -1939,33 +2366,35 @@ async function boot5(opts) {
|
|
|
1939
2366
|
prompt = buildFileSpecPrompt(filePath, fileContent, workingDir, tmpPath);
|
|
1940
2367
|
} else {
|
|
1941
2368
|
return {
|
|
1942
|
-
|
|
2369
|
+
data: null,
|
|
1943
2370
|
success: false,
|
|
1944
2371
|
error: "Either issue, inlineText, or filePath+fileContent must be provided",
|
|
1945
|
-
|
|
2372
|
+
durationMs: Date.now() - startTime
|
|
1946
2373
|
};
|
|
1947
2374
|
}
|
|
2375
|
+
fileLoggerStorage.getStore()?.prompt("spec", prompt);
|
|
1948
2376
|
const sessionId = await provider.createSession();
|
|
1949
2377
|
log.debug(`Spec prompt built (${prompt.length} chars)`);
|
|
1950
2378
|
const response = await provider.prompt(sessionId, prompt);
|
|
1951
2379
|
if (response === null) {
|
|
1952
2380
|
return {
|
|
1953
|
-
|
|
2381
|
+
data: null,
|
|
1954
2382
|
success: false,
|
|
1955
2383
|
error: "AI agent returned no response",
|
|
1956
|
-
|
|
2384
|
+
durationMs: Date.now() - startTime
|
|
1957
2385
|
};
|
|
1958
2386
|
}
|
|
1959
2387
|
log.debug(`Spec agent response (${response.length} chars)`);
|
|
2388
|
+
fileLoggerStorage.getStore()?.response("spec", response);
|
|
1960
2389
|
let rawContent;
|
|
1961
2390
|
try {
|
|
1962
2391
|
rawContent = await readFile4(tmpPath, "utf-8");
|
|
1963
2392
|
} catch {
|
|
1964
2393
|
return {
|
|
1965
|
-
|
|
2394
|
+
data: null,
|
|
1966
2395
|
success: false,
|
|
1967
2396
|
error: `Spec agent did not write the file to ${tmpPath}. Agent response: ${response.slice(0, 300)}`,
|
|
1968
|
-
|
|
2397
|
+
durationMs: Date.now() - startTime
|
|
1969
2398
|
};
|
|
1970
2399
|
}
|
|
1971
2400
|
const cleanedContent = extractSpecContent(rawContent);
|
|
@@ -1974,25 +2403,31 @@ async function boot5(opts) {
|
|
|
1974
2403
|
if (!validation.valid) {
|
|
1975
2404
|
log.warn(`Spec validation warning for ${outputPath}: ${validation.reason}`);
|
|
1976
2405
|
}
|
|
1977
|
-
await writeFile4(
|
|
1978
|
-
log.debug(`Wrote cleaned spec to ${
|
|
2406
|
+
await writeFile4(resolvedOutput, cleanedContent, "utf-8");
|
|
2407
|
+
log.debug(`Wrote cleaned spec to ${resolvedOutput}`);
|
|
1979
2408
|
try {
|
|
1980
2409
|
await unlink(tmpPath);
|
|
1981
2410
|
} catch {
|
|
1982
2411
|
}
|
|
2412
|
+
fileLoggerStorage.getStore()?.agentEvent("spec", "completed", `${Date.now() - startTime}ms`);
|
|
1983
2413
|
return {
|
|
1984
|
-
|
|
2414
|
+
data: {
|
|
2415
|
+
content: cleanedContent,
|
|
2416
|
+
valid: validation.valid,
|
|
2417
|
+
validationReason: validation.reason
|
|
2418
|
+
},
|
|
1985
2419
|
success: true,
|
|
1986
|
-
|
|
1987
|
-
validationReason: validation.reason
|
|
2420
|
+
durationMs: Date.now() - startTime
|
|
1988
2421
|
};
|
|
1989
2422
|
} catch (err) {
|
|
1990
2423
|
const message = log.extractMessage(err);
|
|
2424
|
+
fileLoggerStorage.getStore()?.error(`spec error: ${message}${err instanceof Error && err.stack ? `
|
|
2425
|
+
${err.stack}` : ""}`);
|
|
1991
2426
|
return {
|
|
1992
|
-
|
|
2427
|
+
data: null,
|
|
1993
2428
|
success: false,
|
|
1994
2429
|
error: message,
|
|
1995
|
-
|
|
2430
|
+
durationMs: Date.now() - startTime
|
|
1996
2431
|
};
|
|
1997
2432
|
}
|
|
1998
2433
|
},
|
|
@@ -2000,24 +2435,8 @@ async function boot5(opts) {
|
|
|
2000
2435
|
}
|
|
2001
2436
|
};
|
|
2002
2437
|
}
|
|
2003
|
-
function
|
|
2004
|
-
const
|
|
2005
|
-
`You are a **spec agent**. Your job is to explore the codebase, understand the issue below, and write a high-level **markdown spec file** to disk that will drive an automated implementation pipeline.`,
|
|
2006
|
-
``,
|
|
2007
|
-
`**Important:** This file will be consumed by a two-stage pipeline:`,
|
|
2008
|
-
`1. A **planner agent** reads each task together with the prose context in this file, then explores the codebase to produce a detailed, line-level implementation plan.`,
|
|
2009
|
-
`2. A **coder agent** follows that detailed plan to make the actual code changes.`,
|
|
2010
|
-
``,
|
|
2011
|
-
`Because the planner agent handles low-level details, your spec must stay **high-level and strategic**. Focus on the WHAT, WHY, and HOW \u2014 not exact code or line numbers.`,
|
|
2012
|
-
``,
|
|
2013
|
-
`**CRITICAL \u2014 Output constraints (read carefully):**`,
|
|
2014
|
-
`The file you write must contain ONLY the structured spec content described below. You MUST NOT include:`,
|
|
2015
|
-
`- **No preamble:** Do not add any text before the H1 heading (e.g., "Here's the spec:", "I've written the spec file to...")`,
|
|
2016
|
-
`- **No postamble:** Do not add any text after the last spec section (e.g., "Let me know if you'd like changes", "Here's a summary of...")`,
|
|
2017
|
-
`- **No summaries:** Do not append a summary or recap of what you wrote`,
|
|
2018
|
-
`- **No code fences:** Do not wrap the spec content in \`\`\`markdown ... \`\`\` or any other code fence`,
|
|
2019
|
-
`- **No conversational text:** Do not include any explanations, commentary, or dialogue \u2014 the file is consumed by an automated pipeline, not a human`,
|
|
2020
|
-
`The file content must start with \`# \` (the H1 heading) and contain nothing before or after the structured spec sections.`,
|
|
2438
|
+
function buildIssueSourceSection(issue) {
|
|
2439
|
+
const lines = [
|
|
2021
2440
|
``,
|
|
2022
2441
|
`## Issue Details`,
|
|
2023
2442
|
``,
|
|
@@ -2027,21 +2446,76 @@ function buildSpecPrompt(issue, cwd, outputPath) {
|
|
|
2027
2446
|
`- **URL:** ${issue.url}`
|
|
2028
2447
|
];
|
|
2029
2448
|
if (issue.labels.length > 0) {
|
|
2030
|
-
|
|
2449
|
+
lines.push(`- **Labels:** ${issue.labels.join(", ")}`);
|
|
2031
2450
|
}
|
|
2032
2451
|
if (issue.body) {
|
|
2033
|
-
|
|
2452
|
+
lines.push(``, `### Description`, ``, issue.body);
|
|
2034
2453
|
}
|
|
2035
2454
|
if (issue.acceptanceCriteria) {
|
|
2036
|
-
|
|
2455
|
+
lines.push(``, `### Acceptance Criteria`, ``, issue.acceptanceCriteria);
|
|
2037
2456
|
}
|
|
2038
2457
|
if (issue.comments.length > 0) {
|
|
2039
|
-
|
|
2458
|
+
lines.push(``, `### Discussion`, ``);
|
|
2040
2459
|
for (const comment of issue.comments) {
|
|
2041
|
-
|
|
2460
|
+
lines.push(comment, ``);
|
|
2042
2461
|
}
|
|
2043
2462
|
}
|
|
2044
|
-
|
|
2463
|
+
return lines;
|
|
2464
|
+
}
|
|
2465
|
+
function buildFileSourceSection(filePath, content, title) {
|
|
2466
|
+
const lines = [
|
|
2467
|
+
``,
|
|
2468
|
+
`## File Details`,
|
|
2469
|
+
``,
|
|
2470
|
+
`- **Title:** ${title}`,
|
|
2471
|
+
`- **Source file:** ${filePath}`
|
|
2472
|
+
];
|
|
2473
|
+
if (content) {
|
|
2474
|
+
lines.push(``, `### Content`, ``, content);
|
|
2475
|
+
}
|
|
2476
|
+
return lines;
|
|
2477
|
+
}
|
|
2478
|
+
function buildInlineTextSourceSection(title, text) {
|
|
2479
|
+
return [
|
|
2480
|
+
``,
|
|
2481
|
+
`## Inline Text`,
|
|
2482
|
+
``,
|
|
2483
|
+
`- **Title:** ${title}`,
|
|
2484
|
+
``,
|
|
2485
|
+
`### Description`,
|
|
2486
|
+
``,
|
|
2487
|
+
text
|
|
2488
|
+
];
|
|
2489
|
+
}
|
|
2490
|
+
function buildCommonSpecInstructions(params) {
|
|
2491
|
+
const {
|
|
2492
|
+
subject,
|
|
2493
|
+
sourceSection,
|
|
2494
|
+
cwd,
|
|
2495
|
+
outputPath,
|
|
2496
|
+
understandStep,
|
|
2497
|
+
titleTemplate,
|
|
2498
|
+
summaryTemplate,
|
|
2499
|
+
whyLines
|
|
2500
|
+
} = params;
|
|
2501
|
+
return [
|
|
2502
|
+
`You are a **spec agent**. Your job is to explore the codebase, understand ${subject}, and write a high-level **markdown spec file** to disk that will drive an automated implementation pipeline.`,
|
|
2503
|
+
``,
|
|
2504
|
+
`**Important:** This file will be consumed by a two-stage pipeline:`,
|
|
2505
|
+
`1. A **planner agent** reads each task together with the prose context in this file, then explores the codebase to produce a detailed, line-level implementation plan.`,
|
|
2506
|
+
`2. A **coder agent** follows that detailed plan to make the actual code changes.`,
|
|
2507
|
+
``,
|
|
2508
|
+
`Because the planner agent handles low-level details, your spec must stay **high-level and strategic**. Focus on the WHAT, WHY, and HOW \u2014 not exact code or line numbers.`,
|
|
2509
|
+
``,
|
|
2510
|
+
`**CRITICAL \u2014 Output constraints (read carefully):**`,
|
|
2511
|
+
`The file you write must contain ONLY the structured spec content described below. You MUST NOT include:`,
|
|
2512
|
+
`- **No preamble:** Do not add any text before the H1 heading (e.g., "Here's the spec:", "I've written the spec file to...")`,
|
|
2513
|
+
`- **No postamble:** Do not add any text after the last spec section (e.g., "Let me know if you'd like changes", "Here's a summary of...")`,
|
|
2514
|
+
`- **No summaries:** Do not append a summary or recap of what you wrote`,
|
|
2515
|
+
`- **No code fences:** Do not wrap the spec content in \`\`\`markdown ... \`\`\` or any other code fence`,
|
|
2516
|
+
`- **No conversational text:** Do not include any explanations, commentary, or dialogue \u2014 the file is consumed by an automated pipeline, not a human`,
|
|
2517
|
+
`The file content must start with \`# \` (the H1 heading) and contain nothing before or after the structured spec sections.`,
|
|
2518
|
+
...sourceSection,
|
|
2045
2519
|
``,
|
|
2046
2520
|
`## Working Directory`,
|
|
2047
2521
|
``,
|
|
@@ -2051,7 +2525,7 @@ function buildSpecPrompt(issue, cwd, outputPath) {
|
|
|
2051
2525
|
``,
|
|
2052
2526
|
`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.`,
|
|
2053
2527
|
``,
|
|
2054
|
-
|
|
2528
|
+
understandStep,
|
|
2055
2529
|
``,
|
|
2056
2530
|
`3. **Research the approach** \u2014 look up relevant documentation, libraries, and patterns. Consider how the change integrates with the existing architecture, standards, and technologies already in use. For example, if the project is TypeScript, do not propose a Python solution; if it uses Vitest, do not suggest Jest.`,
|
|
2057
2531
|
``,
|
|
@@ -2067,9 +2541,9 @@ function buildSpecPrompt(issue, cwd, outputPath) {
|
|
|
2067
2541
|
``,
|
|
2068
2542
|
`Use your Write tool to save the file. The file content MUST begin with the H1 heading \u2014 no preamble, no code fences, no conversational text before it. Do not add any text after the final spec section \u2014 no postamble, no summary, no commentary. The file must follow this structure exactly:`,
|
|
2069
2543
|
``,
|
|
2070
|
-
|
|
2544
|
+
titleTemplate,
|
|
2071
2545
|
``,
|
|
2072
|
-
|
|
2546
|
+
summaryTemplate,
|
|
2073
2547
|
``,
|
|
2074
2548
|
`## Context`,
|
|
2075
2549
|
``,
|
|
@@ -2081,8 +2555,7 @@ function buildSpecPrompt(issue, cwd, outputPath) {
|
|
|
2081
2555
|
`## Why`,
|
|
2082
2556
|
``,
|
|
2083
2557
|
`<Explain the motivation \u2014 why this change is needed, what problem it solves,`,
|
|
2084
|
-
|
|
2085
|
-
`acceptance criteria, and discussion.>`,
|
|
2558
|
+
...whyLines,
|
|
2086
2559
|
``,
|
|
2087
2560
|
`## Approach`,
|
|
2088
2561
|
``,
|
|
@@ -2133,258 +2606,59 @@ function buildSpecPrompt(issue, cwd, outputPath) {
|
|
|
2133
2606
|
`- **Tag every task with \`(P)\`, \`(S)\`, or \`(I)\`.** Default to \`(P)\` (parallel) unless the task depends on a prior task's output. Use \`(I)\` for validation/barrier tasks. Group related serial dependencies together and prefer parallelism to maximize throughput.`,
|
|
2134
2607
|
`- **Embed commit instructions within task descriptions.** You control when commits happen. Instead of creating standalone commit tasks (which would fail \u2014 each task runs in an isolated agent session), include commit instructions at the end of implementation task descriptions at logical boundaries. For example: "Implement the validation helper and commit with a conventional commit message." Group related changes into a single commit where it makes logical sense, and use the project's conventional commit types: \`feat\`, \`fix\`, \`docs\`, \`refactor\`, \`test\`, \`chore\`, \`style\`, \`perf\`, \`ci\`. Not every task needs a commit instruction \u2014 use your judgment to place them at logical boundaries.`,
|
|
2135
2608
|
`- **Keep the markdown clean** \u2014 it will be parsed by an automated tool.`
|
|
2136
|
-
|
|
2137
|
-
|
|
2609
|
+
];
|
|
2610
|
+
}
|
|
2611
|
+
function buildSpecPrompt(issue, cwd, outputPath) {
|
|
2612
|
+
return buildCommonSpecInstructions({
|
|
2613
|
+
subject: "the issue below",
|
|
2614
|
+
sourceSection: buildIssueSourceSection(issue),
|
|
2615
|
+
cwd,
|
|
2616
|
+
outputPath,
|
|
2617
|
+
understandStep: `2. **Understand the issue** \u2014 analyze the issue description, acceptance criteria, and discussion comments to fully understand what needs to be done and why.`,
|
|
2618
|
+
titleTemplate: `# <Issue title> (#<number>)`,
|
|
2619
|
+
summaryTemplate: `> <One-line summary: what this issue achieves and why it matters>`,
|
|
2620
|
+
whyLines: [
|
|
2621
|
+
`what user or system benefit it provides. Pull from the issue description,`,
|
|
2622
|
+
`acceptance criteria, and discussion.>`
|
|
2623
|
+
]
|
|
2624
|
+
}).join("\n");
|
|
2138
2625
|
}
|
|
2139
2626
|
function buildFileSpecPrompt(filePath, content, cwd, outputPath) {
|
|
2140
2627
|
const title = extractTitle(content, filePath);
|
|
2141
2628
|
const writePath = outputPath ?? filePath;
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
`2.
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
`- **No postamble:** Do not add any text after the last spec section (e.g., "Let me know if you'd like changes", "Here's a summary of...")`,
|
|
2155
|
-
`- **No summaries:** Do not append a summary or recap of what you wrote`,
|
|
2156
|
-
`- **No code fences:** Do not wrap the spec content in \`\`\`markdown ... \`\`\` or any other code fence`,
|
|
2157
|
-
`- **No conversational text:** Do not include any explanations, commentary, or dialogue \u2014 the file is consumed by an automated pipeline, not a human`,
|
|
2158
|
-
`The file content must start with \`# \` (the H1 heading) and contain nothing before or after the structured spec sections.`,
|
|
2159
|
-
``,
|
|
2160
|
-
`## File Details`,
|
|
2161
|
-
``,
|
|
2162
|
-
`- **Title:** ${title}`,
|
|
2163
|
-
`- **Source file:** ${filePath}`
|
|
2164
|
-
];
|
|
2165
|
-
if (content) {
|
|
2166
|
-
sections.push(``, `### Content`, ``, content);
|
|
2167
|
-
}
|
|
2168
|
-
sections.push(
|
|
2169
|
-
``,
|
|
2170
|
-
`## Working Directory`,
|
|
2171
|
-
``,
|
|
2172
|
-
`\`${cwd}\``,
|
|
2173
|
-
``,
|
|
2174
|
-
`## Instructions`,
|
|
2175
|
-
``,
|
|
2176
|
-
`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.`,
|
|
2177
|
-
``,
|
|
2178
|
-
`2. **Understand the content** \u2014 analyze the file content to fully understand what needs to be done and why.`,
|
|
2179
|
-
``,
|
|
2180
|
-
`3. **Research the approach** \u2014 look up relevant documentation, libraries, and patterns. Consider how the change integrates with the existing architecture, standards, and technologies already in use. For example, if the project is TypeScript, do not propose a Python solution; if it uses Vitest, do not suggest Jest.`,
|
|
2181
|
-
``,
|
|
2182
|
-
`4. **Identify integration points** \u2014 determine which existing modules, interfaces, patterns, and conventions the implementation must align with. Note the key files and modules involved, but do NOT prescribe exact code changes \u2014 the planner agent will handle that.`,
|
|
2183
|
-
``,
|
|
2184
|
-
`5. **DO NOT make any code changes** \u2014 you are only producing a spec, not implementing.`,
|
|
2185
|
-
``,
|
|
2186
|
-
`## Output`,
|
|
2187
|
-
``,
|
|
2188
|
-
`Write the complete spec as a markdown file to this exact path:`,
|
|
2189
|
-
``,
|
|
2190
|
-
`\`${writePath}\``,
|
|
2191
|
-
``,
|
|
2192
|
-
`Use your Write tool to save the file. The file content MUST begin with the H1 heading \u2014 no preamble, no code fences, no conversational text before it. Do not add any text after the final spec section \u2014 no postamble, no summary, no commentary. The file must follow this structure exactly:`,
|
|
2193
|
-
``,
|
|
2194
|
-
`# <Title>`,
|
|
2195
|
-
``,
|
|
2196
|
-
`> <One-line summary: what this achieves and why it matters>`,
|
|
2197
|
-
``,
|
|
2198
|
-
`## Context`,
|
|
2199
|
-
``,
|
|
2200
|
-
`<Describe the relevant parts of the codebase: key modules, directory structure,`,
|
|
2201
|
-
`language/framework, and architectural patterns. Name specific files and modules`,
|
|
2202
|
-
`that are involved so the planner agent knows where to look, but do not include`,
|
|
2203
|
-
`code snippets or line-level details.>`,
|
|
2204
|
-
``,
|
|
2205
|
-
`## Why`,
|
|
2206
|
-
``,
|
|
2207
|
-
`<Explain the motivation \u2014 why this change is needed, what problem it solves,`,
|
|
2208
|
-
`what user or system benefit it provides. Pull from the file content.>`,
|
|
2209
|
-
``,
|
|
2210
|
-
`## Approach`,
|
|
2211
|
-
``,
|
|
2212
|
-
`<High-level description of the implementation strategy. Explain the overall`,
|
|
2213
|
-
`approach, which patterns to follow, what to extend vs. create new, and how`,
|
|
2214
|
-
`the change fits into the existing architecture. Mention relevant standards,`,
|
|
2215
|
-
`technologies, and conventions the implementation MUST align with.>`,
|
|
2216
|
-
``,
|
|
2217
|
-
`## Integration Points`,
|
|
2218
|
-
``,
|
|
2219
|
-
`<List the specific modules, interfaces, configurations, and conventions that`,
|
|
2220
|
-
`the implementation must integrate with. For example: existing provider`,
|
|
2221
|
-
`interfaces to implement, CLI argument patterns to follow, test framework`,
|
|
2222
|
-
`and conventions to match, build system requirements, etc.>`,
|
|
2223
|
-
``,
|
|
2224
|
-
`## Tasks`,
|
|
2225
|
-
``,
|
|
2226
|
-
`Each task MUST be prefixed with an execution-mode tag:`,
|
|
2227
|
-
``,
|
|
2228
|
-
`- \`(P)\` \u2014 **Parallel-safe.** This task has no dependency on the output of a prior task and can run concurrently with other \`(P)\` tasks.`,
|
|
2229
|
-
`- \`(S)\` \u2014 **Serial / dependent.** This task depends on a prior task's output or modifies shared state that conflicts with concurrent work. It acts as a barrier: all preceding tasks complete before it starts, and it completes before subsequent tasks begin.`,
|
|
2230
|
-
`- \`(I)\` \u2014 **Isolated / barrier.** This task must run alone after all preceding tasks complete and before any subsequent tasks begin. Use for validation tasks like running tests, linting, or builds that read the output of prior tasks.`,
|
|
2231
|
-
``,
|
|
2232
|
-
`**Default to \`(P)\`.** Most tasks are independent (e.g., adding a function in one module, writing tests in another). Only use \`(S)\` when a task genuinely depends on the result of a prior task (e.g., "refactor module X" followed by "update callers of module X"). Use \`(I)\` for validation or barrier tasks that must run alone after all prior work completes (e.g., "run tests", "run linting", "build the project").`,
|
|
2233
|
-
``,
|
|
2234
|
-
`If a task has no \`(P)\`, \`(S)\`, or \`(I)\` prefix, the system treats it as serial, so always tag explicitly.`,
|
|
2235
|
-
``,
|
|
2236
|
-
`Example:`,
|
|
2237
|
-
``,
|
|
2238
|
-
`- [ ] (P) Add validation helper to the form utils module`,
|
|
2239
|
-
`- [ ] (P) Add unit tests for the new validation helper`,
|
|
2240
|
-
`- [ ] (S) Refactor the form component to use the new validation helper`,
|
|
2241
|
-
`- [ ] (P) Update documentation for the form utils module`,
|
|
2242
|
-
`- [ ] (I) Run the full test suite to verify all changes pass`,
|
|
2243
|
-
``,
|
|
2244
|
-
``,
|
|
2245
|
-
`## References`,
|
|
2246
|
-
``,
|
|
2247
|
-
`- <Links to relevant docs, related issues, or external resources>`,
|
|
2248
|
-
``,
|
|
2249
|
-
`## Key Guidelines`,
|
|
2250
|
-
``,
|
|
2251
|
-
`- **Stay high-level.** Do NOT include code snippets, exact line numbers, diffs, or step-by-step coding instructions. A dedicated planner agent will produce those details for each task at execution time.`,
|
|
2252
|
-
`- **Respect the project's stack.** Your spec must align with the languages, frameworks, libraries, test tools, and conventions already in use. Never suggest technologies that conflict with the existing project.`,
|
|
2253
|
-
`- **Explain WHAT, WHY, and HOW (strategically).** Each task should say what needs to happen, why it's needed, and which part of the codebase it touches \u2014 but leave the tactical "how" to the planner agent.`,
|
|
2254
|
-
`- **Detail integration points.** The prose sections (Context, Approach, Integration Points) are critical \u2014 they tell the planner agent where to look and what constraints to respect.`,
|
|
2255
|
-
`- **Keep tasks atomic and ordered.** Each \`- [ ]\` task must be a single, clear unit of work. Order them so dependencies come first.`,
|
|
2256
|
-
`- **Tag every task with \`(P)\`, \`(S)\`, or \`(I)\`.** Default to \`(P)\` (parallel) unless the task depends on a prior task's output. Use \`(I)\` for validation/barrier tasks. Group related serial dependencies together and prefer parallelism to maximize throughput.`,
|
|
2257
|
-
`- **Embed commit instructions within task descriptions.** You control when commits happen. Instead of creating standalone commit tasks (which would fail \u2014 each task runs in an isolated agent session), include commit instructions at the end of implementation task descriptions at logical boundaries. For example: "Implement the validation helper and commit with a conventional commit message." Group related changes into a single commit where it makes logical sense, and use the project's conventional commit types: \`feat\`, \`fix\`, \`docs\`, \`refactor\`, \`test\`, \`chore\`, \`style\`, \`perf\`, \`ci\`. Not every task needs a commit instruction \u2014 use your judgment to place them at logical boundaries.`,
|
|
2258
|
-
`- **Keep the markdown clean** \u2014 it will be parsed by an automated tool.`
|
|
2259
|
-
);
|
|
2260
|
-
return sections.join("\n");
|
|
2629
|
+
return buildCommonSpecInstructions({
|
|
2630
|
+
subject: "the content below",
|
|
2631
|
+
sourceSection: buildFileSourceSection(filePath, content, title),
|
|
2632
|
+
cwd,
|
|
2633
|
+
outputPath: writePath,
|
|
2634
|
+
understandStep: `2. **Understand the content** \u2014 analyze the file content to fully understand what needs to be done and why.`,
|
|
2635
|
+
titleTemplate: `# <Title>`,
|
|
2636
|
+
summaryTemplate: `> <One-line summary: what this achieves and why it matters>`,
|
|
2637
|
+
whyLines: [
|
|
2638
|
+
`what user or system benefit it provides. Pull from the file content.>`
|
|
2639
|
+
]
|
|
2640
|
+
}).join("\n");
|
|
2261
2641
|
}
|
|
2262
2642
|
function buildInlineTextSpecPrompt(text, cwd, outputPath) {
|
|
2263
2643
|
const title = text.length > 80 ? text.slice(0, 80).trimEnd() + "\u2026" : text;
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
`2.
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
`- **No postamble:** Do not add any text after the last spec section (e.g., "Let me know if you'd like changes", "Here's a summary of...")`,
|
|
2277
|
-
`- **No summaries:** Do not append a summary or recap of what you wrote`,
|
|
2278
|
-
`- **No code fences:** Do not wrap the spec content in \`\`\`markdown ... \`\`\` or any other code fence`,
|
|
2279
|
-
`- **No conversational text:** Do not include any explanations, commentary, or dialogue \u2014 the file is consumed by an automated pipeline, not a human`,
|
|
2280
|
-
`The file content must start with \`# \` (the H1 heading) and contain nothing before or after the structured spec sections.`,
|
|
2281
|
-
``,
|
|
2282
|
-
`## Inline Text`,
|
|
2283
|
-
``,
|
|
2284
|
-
`- **Title:** ${title}`,
|
|
2285
|
-
``,
|
|
2286
|
-
`### Description`,
|
|
2287
|
-
``,
|
|
2288
|
-
text
|
|
2289
|
-
];
|
|
2290
|
-
sections.push(
|
|
2291
|
-
``,
|
|
2292
|
-
`## Working Directory`,
|
|
2293
|
-
``,
|
|
2294
|
-
`\`${cwd}\``,
|
|
2295
|
-
``,
|
|
2296
|
-
`## Instructions`,
|
|
2297
|
-
``,
|
|
2298
|
-
`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.`,
|
|
2299
|
-
``,
|
|
2300
|
-
`2. **Understand the request** \u2014 analyze the inline text to fully understand what needs to be done and why. Since this is a brief description rather than a detailed issue or document, you may need to infer details from the codebase.`,
|
|
2301
|
-
``,
|
|
2302
|
-
`3. **Research the approach** \u2014 look up relevant documentation, libraries, and patterns. Consider how the change integrates with the existing architecture, standards, and technologies already in use. For example, if the project is TypeScript, do not propose a Python solution; if it uses Vitest, do not suggest Jest.`,
|
|
2303
|
-
``,
|
|
2304
|
-
`4. **Identify integration points** \u2014 determine which existing modules, interfaces, patterns, and conventions the implementation must align with. Note the key files and modules involved, but do NOT prescribe exact code changes \u2014 the planner agent will handle that.`,
|
|
2305
|
-
``,
|
|
2306
|
-
`5. **DO NOT make any code changes** \u2014 you are only producing a spec, not implementing.`,
|
|
2307
|
-
``,
|
|
2308
|
-
`## Output`,
|
|
2309
|
-
``,
|
|
2310
|
-
`Write the complete spec as a markdown file to this exact path:`,
|
|
2311
|
-
``,
|
|
2312
|
-
`\`${outputPath}\``,
|
|
2313
|
-
``,
|
|
2314
|
-
`Use your Write tool to save the file. The file content MUST begin with the H1 heading \u2014 no preamble, no code fences, no conversational text before it. Do not add any text after the final spec section \u2014 no postamble, no summary, no commentary. The file must follow this structure exactly:`,
|
|
2315
|
-
``,
|
|
2316
|
-
`# <Title>`,
|
|
2317
|
-
``,
|
|
2318
|
-
`> <One-line summary: what this achieves and why it matters>`,
|
|
2319
|
-
``,
|
|
2320
|
-
`## Context`,
|
|
2321
|
-
``,
|
|
2322
|
-
`<Describe the relevant parts of the codebase: key modules, directory structure,`,
|
|
2323
|
-
`language/framework, and architectural patterns. Name specific files and modules`,
|
|
2324
|
-
`that are involved so the planner agent knows where to look, but do not include`,
|
|
2325
|
-
`code snippets or line-level details.>`,
|
|
2326
|
-
``,
|
|
2327
|
-
`## Why`,
|
|
2328
|
-
``,
|
|
2329
|
-
`<Explain the motivation \u2014 why this change is needed, what problem it solves,`,
|
|
2330
|
-
`what user or system benefit it provides. Pull from the inline text description.>`,
|
|
2331
|
-
``,
|
|
2332
|
-
`## Approach`,
|
|
2333
|
-
``,
|
|
2334
|
-
`<High-level description of the implementation strategy. Explain the overall`,
|
|
2335
|
-
`approach, which patterns to follow, what to extend vs. create new, and how`,
|
|
2336
|
-
`the change fits into the existing architecture. Mention relevant standards,`,
|
|
2337
|
-
`technologies, and conventions the implementation MUST align with.>`,
|
|
2338
|
-
``,
|
|
2339
|
-
`## Integration Points`,
|
|
2340
|
-
``,
|
|
2341
|
-
`<List the specific modules, interfaces, configurations, and conventions that`,
|
|
2342
|
-
`the implementation must integrate with. For example: existing provider`,
|
|
2343
|
-
`interfaces to implement, CLI argument patterns to follow, test framework`,
|
|
2344
|
-
`and conventions to match, build system requirements, etc.>`,
|
|
2345
|
-
``,
|
|
2346
|
-
`## Tasks`,
|
|
2347
|
-
``,
|
|
2348
|
-
`Each task MUST be prefixed with an execution-mode tag:`,
|
|
2349
|
-
``,
|
|
2350
|
-
`- \`(P)\` \u2014 **Parallel-safe.** This task has no dependency on the output of a prior task and can run concurrently with other \`(P)\` tasks.`,
|
|
2351
|
-
`- \`(S)\` \u2014 **Serial / dependent.** This task depends on a prior task's output or modifies shared state that conflicts with concurrent work. It acts as a barrier: all preceding tasks complete before it starts, and it completes before subsequent tasks begin.`,
|
|
2352
|
-
`- \`(I)\` \u2014 **Isolated / barrier.** This task must run alone after all preceding tasks complete and before any subsequent tasks begin. Use for validation tasks like running tests, linting, or builds that read the output of prior tasks.`,
|
|
2353
|
-
``,
|
|
2354
|
-
`**Default to \`(P)\`.** Most tasks are independent (e.g., adding a function in one module, writing tests in another). Only use \`(S)\` when a task genuinely depends on the result of a prior task (e.g., "refactor module X" followed by "update callers of module X"). Use \`(I)\` for validation or barrier tasks that must run alone after all prior work completes (e.g., "run tests", "run linting", "build the project").`,
|
|
2355
|
-
``,
|
|
2356
|
-
`If a task has no \`(P)\`, \`(S)\`, or \`(I)\` prefix, the system treats it as serial, so always tag explicitly.`,
|
|
2357
|
-
``,
|
|
2358
|
-
`Example:`,
|
|
2359
|
-
``,
|
|
2360
|
-
`- [ ] (P) Add validation helper to the form utils module`,
|
|
2361
|
-
`- [ ] (P) Add unit tests for the new validation helper`,
|
|
2362
|
-
`- [ ] (S) Refactor the form component to use the new validation helper`,
|
|
2363
|
-
`- [ ] (P) Update documentation for the form utils module`,
|
|
2364
|
-
`- [ ] (I) Run the full test suite to verify all changes pass`,
|
|
2365
|
-
``,
|
|
2366
|
-
``,
|
|
2367
|
-
`## References`,
|
|
2368
|
-
``,
|
|
2369
|
-
`- <Links to relevant docs, related issues, or external resources>`,
|
|
2370
|
-
``,
|
|
2371
|
-
`## Key Guidelines`,
|
|
2372
|
-
``,
|
|
2373
|
-
`- **Stay high-level.** Do NOT include code snippets, exact line numbers, diffs, or step-by-step coding instructions. A dedicated planner agent will produce those details for each task at execution time.`,
|
|
2374
|
-
`- **Respect the project's stack.** Your spec must align with the languages, frameworks, libraries, test tools, and conventions already in use. Never suggest technologies that conflict with the existing project.`,
|
|
2375
|
-
`- **Explain WHAT, WHY, and HOW (strategically).** Each task should say what needs to happen, why it's needed, and which part of the codebase it touches \u2014 but leave the tactical "how" to the planner agent.`,
|
|
2376
|
-
`- **Detail integration points.** The prose sections (Context, Approach, Integration Points) are critical \u2014 they tell the planner agent where to look and what constraints to respect.`,
|
|
2377
|
-
`- **Keep tasks atomic and ordered.** Each \`- [ ]\` task must be a single, clear unit of work. Order them so dependencies come first.`,
|
|
2378
|
-
`- **Tag every task with \`(P)\`, \`(S)\`, or \`(I)\`.** Default to \`(P)\` (parallel) unless the task depends on a prior task's output. Use \`(I)\` for validation/barrier tasks. Group related serial dependencies together and prefer parallelism to maximize throughput.`,
|
|
2379
|
-
`- **Embed commit instructions within task descriptions.** You control when commits happen. Instead of creating standalone commit tasks (which would fail \u2014 each task runs in an isolated agent session), include commit instructions at the end of implementation task descriptions at logical boundaries. For example: "Implement the validation helper and commit with a conventional commit message." Group related changes into a single commit where it makes logical sense, and use the project's conventional commit types: \`feat\`, \`fix\`, \`docs\`, \`refactor\`, \`test\`, \`chore\`, \`style\`, \`perf\`, \`ci\`. Not every task needs a commit instruction \u2014 use your judgment to place them at logical boundaries.`,
|
|
2380
|
-
`- **Keep the markdown clean** \u2014 it will be parsed by an automated tool.`
|
|
2381
|
-
);
|
|
2382
|
-
return sections.join("\n");
|
|
2644
|
+
return buildCommonSpecInstructions({
|
|
2645
|
+
subject: "the request below",
|
|
2646
|
+
sourceSection: buildInlineTextSourceSection(title, text),
|
|
2647
|
+
cwd,
|
|
2648
|
+
outputPath,
|
|
2649
|
+
understandStep: `2. **Understand the request** \u2014 analyze the inline text to fully understand what needs to be done and why. Since this is a brief description rather than a detailed issue or document, you may need to infer details from the codebase.`,
|
|
2650
|
+
titleTemplate: `# <Title>`,
|
|
2651
|
+
summaryTemplate: `> <One-line summary: what this achieves and why it matters>`,
|
|
2652
|
+
whyLines: [
|
|
2653
|
+
`what user or system benefit it provides. Pull from the inline text description.>`
|
|
2654
|
+
]
|
|
2655
|
+
}).join("\n");
|
|
2383
2656
|
}
|
|
2384
2657
|
|
|
2385
2658
|
// src/orchestrator/spec-pipeline.ts
|
|
2386
2659
|
init_cleanup();
|
|
2387
2660
|
init_logger();
|
|
2661
|
+
init_file_logger();
|
|
2388
2662
|
import chalk5 from "chalk";
|
|
2389
2663
|
|
|
2390
2664
|
// src/helpers/format.ts
|
|
@@ -2435,146 +2709,134 @@ async function withRetry(fn, maxRetries, options) {
|
|
|
2435
2709
|
}
|
|
2436
2710
|
|
|
2437
2711
|
// src/orchestrator/spec-pipeline.ts
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
serverUrl,
|
|
2444
|
-
cwd: specCwd,
|
|
2445
|
-
outputDir = join6(specCwd, ".dispatch", "specs"),
|
|
2446
|
-
org,
|
|
2447
|
-
project,
|
|
2448
|
-
workItemType,
|
|
2449
|
-
concurrency = defaultConcurrency(),
|
|
2450
|
-
dryRun,
|
|
2451
|
-
retries = 2
|
|
2452
|
-
} = opts;
|
|
2453
|
-
const pipelineStart = Date.now();
|
|
2454
|
-
const source = await resolveSource(issues, opts.issueSource, specCwd);
|
|
2455
|
-
if (!source) {
|
|
2456
|
-
return { total: 0, generated: 0, failed: 0, files: [], issueNumbers: [], durationMs: Date.now() - pipelineStart, fileDurationsMs: {} };
|
|
2457
|
-
}
|
|
2712
|
+
init_timeout();
|
|
2713
|
+
var FETCH_TIMEOUT_MS = 3e4;
|
|
2714
|
+
async function resolveDatasource(issues, issueSource, specCwd, org, project, workItemType, iteration, area) {
|
|
2715
|
+
const source = await resolveSource(issues, issueSource, specCwd);
|
|
2716
|
+
if (!source) return null;
|
|
2458
2717
|
const datasource4 = getDatasource(source);
|
|
2459
|
-
const fetchOpts = { cwd: specCwd, org, project, workItemType };
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
}
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
}
|
|
2536
|
-
|
|
2537
|
-
|
|
2718
|
+
const fetchOpts = { cwd: specCwd, org, project, workItemType, iteration, area };
|
|
2719
|
+
return { source, datasource: datasource4, fetchOpts };
|
|
2720
|
+
}
|
|
2721
|
+
async function fetchTrackerItems(issues, datasource4, fetchOpts, concurrency, source) {
|
|
2722
|
+
const issueNumbers = issues.split(",").map((s) => s.trim()).filter(Boolean);
|
|
2723
|
+
if (issueNumbers.length === 0) {
|
|
2724
|
+
log.error("No issue numbers provided. Use --spec 1,2,3");
|
|
2725
|
+
return [];
|
|
2726
|
+
}
|
|
2727
|
+
const fetchStart = Date.now();
|
|
2728
|
+
log.info(`Fetching ${issueNumbers.length} issue(s) from ${source} (concurrency: ${concurrency})...`);
|
|
2729
|
+
const items = [];
|
|
2730
|
+
const fetchQueue = [...issueNumbers];
|
|
2731
|
+
while (fetchQueue.length > 0) {
|
|
2732
|
+
const batch = fetchQueue.splice(0, concurrency);
|
|
2733
|
+
log.debug(`Fetching batch of ${batch.length}: #${batch.join(", #")}`);
|
|
2734
|
+
const batchResults = await Promise.all(
|
|
2735
|
+
batch.map(async (id) => {
|
|
2736
|
+
try {
|
|
2737
|
+
const details = await withTimeout(datasource4.fetch(id, fetchOpts), FETCH_TIMEOUT_MS, "datasource fetch");
|
|
2738
|
+
log.success(`Fetched #${id}: ${details.title}`);
|
|
2739
|
+
log.debug(`Body: ${details.body?.length ?? 0} chars, Labels: ${details.labels.length}, Comments: ${details.comments.length}`);
|
|
2740
|
+
return { id, details };
|
|
2741
|
+
} catch (err) {
|
|
2742
|
+
const message = log.extractMessage(err);
|
|
2743
|
+
log.error(`Failed to fetch #${id}: ${log.formatErrorChain(err)}`);
|
|
2744
|
+
log.debug(log.formatErrorChain(err));
|
|
2745
|
+
return { id, details: null, error: message };
|
|
2746
|
+
}
|
|
2747
|
+
})
|
|
2748
|
+
);
|
|
2749
|
+
items.push(...batchResults);
|
|
2750
|
+
}
|
|
2751
|
+
log.debug(`Issue fetching completed in ${elapsed(Date.now() - fetchStart)}`);
|
|
2752
|
+
return items;
|
|
2753
|
+
}
|
|
2754
|
+
function buildInlineTextItem(issues, outputDir) {
|
|
2755
|
+
const text = Array.isArray(issues) ? issues.join(" ") : issues;
|
|
2756
|
+
const title = text.length > 80 ? text.slice(0, 80).trimEnd() + "\u2026" : text;
|
|
2757
|
+
const slug = slugify(text, MAX_SLUG_LENGTH);
|
|
2758
|
+
const filename = `${slug}.md`;
|
|
2759
|
+
const filepath = join7(outputDir, filename);
|
|
2760
|
+
const details = {
|
|
2761
|
+
number: filepath,
|
|
2762
|
+
title,
|
|
2763
|
+
body: text,
|
|
2764
|
+
labels: [],
|
|
2765
|
+
state: "open",
|
|
2766
|
+
url: filepath,
|
|
2767
|
+
comments: [],
|
|
2768
|
+
acceptanceCriteria: ""
|
|
2769
|
+
};
|
|
2770
|
+
log.info(`Inline text spec: "${title}"`);
|
|
2771
|
+
return [{ id: filepath, details }];
|
|
2772
|
+
}
|
|
2773
|
+
async function resolveFileItems(issues, specCwd, concurrency) {
|
|
2774
|
+
const files = await glob(issues, { cwd: specCwd, absolute: true });
|
|
2775
|
+
if (files.length === 0) {
|
|
2776
|
+
log.error(`No files matched the pattern "${Array.isArray(issues) ? issues.join(", ") : issues}".`);
|
|
2777
|
+
return null;
|
|
2778
|
+
}
|
|
2779
|
+
log.info(`Matched ${files.length} file(s) for spec generation (concurrency: ${concurrency})...`);
|
|
2780
|
+
const items = [];
|
|
2781
|
+
for (const filePath of files) {
|
|
2782
|
+
try {
|
|
2783
|
+
const content = await readFile5(filePath, "utf-8");
|
|
2784
|
+
const title = extractTitle(content, filePath);
|
|
2785
|
+
const details = {
|
|
2786
|
+
number: filePath,
|
|
2787
|
+
title,
|
|
2788
|
+
body: content,
|
|
2789
|
+
labels: [],
|
|
2790
|
+
state: "open",
|
|
2791
|
+
url: filePath,
|
|
2792
|
+
comments: [],
|
|
2793
|
+
acceptanceCriteria: ""
|
|
2794
|
+
};
|
|
2795
|
+
items.push({ id: filePath, details });
|
|
2796
|
+
} catch (err) {
|
|
2797
|
+
items.push({ id: filePath, details: null, error: log.extractMessage(err) });
|
|
2538
2798
|
}
|
|
2539
2799
|
}
|
|
2800
|
+
return items;
|
|
2801
|
+
}
|
|
2802
|
+
function filterValidItems(items, isTrackerMode, isInlineText) {
|
|
2540
2803
|
const validItems = items.filter(
|
|
2541
2804
|
(i) => i.details !== null
|
|
2542
2805
|
);
|
|
2543
2806
|
if (validItems.length === 0) {
|
|
2544
2807
|
const noun = isTrackerMode ? "issues" : isInlineText ? "inline specs" : "files";
|
|
2545
2808
|
log.error(`No ${noun} could be loaded. Aborting spec generation.`);
|
|
2546
|
-
return
|
|
2809
|
+
return null;
|
|
2547
2810
|
}
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2811
|
+
return validItems;
|
|
2812
|
+
}
|
|
2813
|
+
function previewDryRun(validItems, items, isTrackerMode, isInlineText, outputDir, pipelineStart) {
|
|
2814
|
+
const mode = isTrackerMode ? "tracker" : isInlineText ? "inline" : "file";
|
|
2815
|
+
log.info(`[DRY RUN] Would generate ${validItems.length} spec(s) (mode: ${mode}):
|
|
2551
2816
|
`);
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
}
|
|
2560
|
-
const label = isTrackerMode ? `#${id}` : filepath;
|
|
2561
|
-
log.info(`[DRY RUN] Would generate spec for ${label}: "${details.title}"`);
|
|
2562
|
-
log.dim(` \u2192 ${filepath}`);
|
|
2817
|
+
for (const { id, details } of validItems) {
|
|
2818
|
+
let filepath;
|
|
2819
|
+
if (isTrackerMode) {
|
|
2820
|
+
const slug = slugify(details.title, 60);
|
|
2821
|
+
filepath = join7(outputDir, `${id}-${slug}.md`);
|
|
2822
|
+
} else {
|
|
2823
|
+
filepath = id;
|
|
2563
2824
|
}
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
failed: items.filter((i) => i.details === null).length,
|
|
2568
|
-
files: [],
|
|
2569
|
-
issueNumbers: [],
|
|
2570
|
-
durationMs: Date.now() - pipelineStart,
|
|
2571
|
-
fileDurationsMs: {}
|
|
2572
|
-
};
|
|
2573
|
-
}
|
|
2574
|
-
const confirmed = await confirmLargeBatch(validItems.length);
|
|
2575
|
-
if (!confirmed) {
|
|
2576
|
-
return { total: 0, generated: 0, failed: 0, files: [], issueNumbers: [], durationMs: Date.now() - pipelineStart, fileDurationsMs: {} };
|
|
2825
|
+
const label = isTrackerMode ? `#${id}` : filepath;
|
|
2826
|
+
log.info(`[DRY RUN] Would generate spec for ${label}: "${details.title}"`);
|
|
2827
|
+
log.dim(` \u2192 ${filepath}`);
|
|
2577
2828
|
}
|
|
2829
|
+
return {
|
|
2830
|
+
total: items.length,
|
|
2831
|
+
generated: 0,
|
|
2832
|
+
failed: items.filter((i) => i.details === null).length,
|
|
2833
|
+
files: [],
|
|
2834
|
+
issueNumbers: [],
|
|
2835
|
+
durationMs: Date.now() - pipelineStart,
|
|
2836
|
+
fileDurationsMs: {}
|
|
2837
|
+
};
|
|
2838
|
+
}
|
|
2839
|
+
async function bootPipeline(provider, serverUrl, specCwd, model, source) {
|
|
2578
2840
|
const bootStart = Date.now();
|
|
2579
2841
|
log.info(`Booting ${provider} provider...`);
|
|
2580
2842
|
log.debug(serverUrl ? `Using server URL: ${serverUrl}` : "No --server-url, will spawn local server");
|
|
@@ -2593,6 +2855,9 @@ async function runSpecPipeline(opts) {
|
|
|
2593
2855
|
console.log(chalk5.dim(" \u2500".repeat(24)));
|
|
2594
2856
|
console.log("");
|
|
2595
2857
|
const specAgent = await boot5({ provider: instance, cwd: specCwd });
|
|
2858
|
+
return { specAgent, instance };
|
|
2859
|
+
}
|
|
2860
|
+
async function generateSpecsBatch(validItems, items, specAgent, instance, isTrackerMode, isInlineText, datasource4, fetchOpts, outputDir, specCwd, concurrency, retries) {
|
|
2596
2861
|
await mkdir4(outputDir, { recursive: true });
|
|
2597
2862
|
const generatedFiles = [];
|
|
2598
2863
|
const issueNumbers = [];
|
|
@@ -2611,72 +2876,92 @@ async function runSpecPipeline(opts) {
|
|
|
2611
2876
|
log.error(`Skipping item ${id}: missing issue details`);
|
|
2612
2877
|
return null;
|
|
2613
2878
|
}
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
try {
|
|
2625
|
-
log.info(`Generating spec for ${isTrackerMode ? `#${id}` : filepath}: ${details.title}...`);
|
|
2626
|
-
const result = await withRetry(
|
|
2627
|
-
() => specAgent.generate({
|
|
2628
|
-
issue: isTrackerMode ? details : void 0,
|
|
2629
|
-
filePath: isTrackerMode ? void 0 : id,
|
|
2630
|
-
fileContent: isTrackerMode ? void 0 : details.body,
|
|
2631
|
-
cwd: specCwd,
|
|
2632
|
-
outputPath: filepath
|
|
2633
|
-
}),
|
|
2634
|
-
retries,
|
|
2635
|
-
{ label: `specAgent.generate(${isTrackerMode ? `#${id}` : filepath})` }
|
|
2636
|
-
);
|
|
2637
|
-
if (!result.success) {
|
|
2638
|
-
throw new Error(result.error ?? "Spec generation failed");
|
|
2639
|
-
}
|
|
2640
|
-
if (isTrackerMode || isInlineText) {
|
|
2641
|
-
const h1Title = extractTitle(result.content, filepath);
|
|
2642
|
-
const h1Slug = slugify(h1Title, MAX_SLUG_LENGTH);
|
|
2643
|
-
const finalFilename = isTrackerMode ? `${id}-${h1Slug}.md` : `${h1Slug}.md`;
|
|
2644
|
-
const finalFilepath = join6(outputDir, finalFilename);
|
|
2645
|
-
if (finalFilepath !== filepath) {
|
|
2646
|
-
await rename2(filepath, finalFilepath);
|
|
2647
|
-
filepath = finalFilepath;
|
|
2648
|
-
}
|
|
2879
|
+
const itemBody = async () => {
|
|
2880
|
+
let filepath;
|
|
2881
|
+
if (isTrackerMode) {
|
|
2882
|
+
const slug = slugify(details.title, MAX_SLUG_LENGTH);
|
|
2883
|
+
const filename = `${id}-${slug}.md`;
|
|
2884
|
+
filepath = join7(outputDir, filename);
|
|
2885
|
+
} else if (isInlineText) {
|
|
2886
|
+
filepath = id;
|
|
2887
|
+
} else {
|
|
2888
|
+
filepath = id;
|
|
2649
2889
|
}
|
|
2650
|
-
|
|
2651
|
-
fileDurationsMs[filepath] = specDuration;
|
|
2652
|
-
log.success(`Spec written: ${filepath} (${elapsed(specDuration)})`);
|
|
2653
|
-
let identifier = filepath;
|
|
2890
|
+
fileLoggerStorage.getStore()?.info(`Output path: ${filepath}`);
|
|
2654
2891
|
try {
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2892
|
+
fileLoggerStorage.getStore()?.info(`Starting spec generation for ${isTrackerMode ? `#${id}` : filepath}`);
|
|
2893
|
+
log.info(`Generating spec for ${isTrackerMode ? `#${id}` : filepath}: ${details.title}...`);
|
|
2894
|
+
const result = await withRetry(
|
|
2895
|
+
() => specAgent.generate({
|
|
2896
|
+
issue: isTrackerMode ? details : void 0,
|
|
2897
|
+
filePath: isTrackerMode ? void 0 : id,
|
|
2898
|
+
fileContent: isTrackerMode ? void 0 : details.body,
|
|
2899
|
+
cwd: specCwd,
|
|
2900
|
+
outputPath: filepath
|
|
2901
|
+
}),
|
|
2902
|
+
retries,
|
|
2903
|
+
{ label: `specAgent.generate(${isTrackerMode ? `#${id}` : filepath})` }
|
|
2904
|
+
);
|
|
2905
|
+
if (!result.success) {
|
|
2906
|
+
throw new Error(result.error ?? "Spec generation failed");
|
|
2907
|
+
}
|
|
2908
|
+
fileLoggerStorage.getStore()?.info(`Spec generated successfully`);
|
|
2909
|
+
if (isTrackerMode || isInlineText) {
|
|
2910
|
+
const h1Title = extractTitle(result.data.content, filepath);
|
|
2911
|
+
const h1Slug = slugify(h1Title, MAX_SLUG_LENGTH);
|
|
2912
|
+
const finalFilename = isTrackerMode ? `${id}-${h1Slug}.md` : `${h1Slug}.md`;
|
|
2913
|
+
const finalFilepath = join7(outputDir, finalFilename);
|
|
2914
|
+
if (finalFilepath !== filepath) {
|
|
2915
|
+
await rename2(filepath, finalFilepath);
|
|
2916
|
+
filepath = finalFilepath;
|
|
2917
|
+
}
|
|
2918
|
+
}
|
|
2919
|
+
const specDuration = Date.now() - specStart;
|
|
2920
|
+
fileDurationsMs[filepath] = specDuration;
|
|
2921
|
+
log.success(`Spec written: ${filepath} (${elapsed(specDuration)})`);
|
|
2922
|
+
let identifier = filepath;
|
|
2923
|
+
fileLoggerStorage.getStore()?.phase("Datasource sync");
|
|
2924
|
+
try {
|
|
2925
|
+
if (isTrackerMode) {
|
|
2926
|
+
await datasource4.update(id, details.title, result.data.content, fetchOpts);
|
|
2927
|
+
log.success(`Updated issue #${id} with spec content`);
|
|
2928
|
+
await unlink2(filepath);
|
|
2929
|
+
log.success(`Deleted local spec ${filepath} (now tracked as issue #${id})`);
|
|
2930
|
+
identifier = id;
|
|
2931
|
+
issueNumbers.push(id);
|
|
2932
|
+
} else if (datasource4.name !== "md") {
|
|
2933
|
+
const created = await datasource4.create(details.title, result.data.content, fetchOpts);
|
|
2934
|
+
log.success(`Created issue #${created.number} from ${filepath}`);
|
|
2935
|
+
await unlink2(filepath);
|
|
2936
|
+
log.success(`Deleted local spec ${filepath} (now tracked as issue #${created.number})`);
|
|
2937
|
+
identifier = created.number;
|
|
2938
|
+
issueNumbers.push(created.number);
|
|
2939
|
+
}
|
|
2940
|
+
} catch (err) {
|
|
2941
|
+
const label = isTrackerMode ? `issue #${id}` : filepath;
|
|
2942
|
+
log.warn(`Could not sync ${label} to datasource: ${log.formatErrorChain(err)}`);
|
|
2669
2943
|
}
|
|
2944
|
+
return { filepath, identifier };
|
|
2670
2945
|
} catch (err) {
|
|
2671
|
-
|
|
2672
|
-
|
|
2946
|
+
fileLoggerStorage.getStore()?.error(`Spec generation failed for ${id}: ${log.extractMessage(err)}${err instanceof Error && err.stack ? `
|
|
2947
|
+
${err.stack}` : ""}`);
|
|
2948
|
+
log.error(`Failed to generate spec for ${isTrackerMode ? `#${id}` : filepath}: ${log.formatErrorChain(err)}`);
|
|
2949
|
+
log.debug(log.formatErrorChain(err));
|
|
2950
|
+
return null;
|
|
2673
2951
|
}
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2952
|
+
};
|
|
2953
|
+
const fileLogger = log.verbose ? new FileLogger(id, specCwd) : null;
|
|
2954
|
+
if (fileLogger) {
|
|
2955
|
+
return fileLoggerStorage.run(fileLogger, async () => {
|
|
2956
|
+
try {
|
|
2957
|
+
fileLogger.phase(`Spec generation: ${id}`);
|
|
2958
|
+
return await itemBody();
|
|
2959
|
+
} finally {
|
|
2960
|
+
fileLogger.close();
|
|
2961
|
+
}
|
|
2962
|
+
});
|
|
2679
2963
|
}
|
|
2964
|
+
return itemBody();
|
|
2680
2965
|
})
|
|
2681
2966
|
);
|
|
2682
2967
|
for (const result of batchResults) {
|
|
@@ -2692,6 +2977,9 @@ async function runSpecPipeline(opts) {
|
|
|
2692
2977
|
modelLoggedInBanner = true;
|
|
2693
2978
|
}
|
|
2694
2979
|
}
|
|
2980
|
+
return { generatedFiles, issueNumbers, dispatchIdentifiers, failed, fileDurationsMs };
|
|
2981
|
+
}
|
|
2982
|
+
async function cleanupPipeline(specAgent, instance) {
|
|
2695
2983
|
try {
|
|
2696
2984
|
await specAgent.cleanup();
|
|
2697
2985
|
} catch (err) {
|
|
@@ -2702,7 +2990,8 @@ async function runSpecPipeline(opts) {
|
|
|
2702
2990
|
} catch (err) {
|
|
2703
2991
|
log.warn(`Provider cleanup failed: ${log.formatErrorChain(err)}`);
|
|
2704
2992
|
}
|
|
2705
|
-
|
|
2993
|
+
}
|
|
2994
|
+
function logSummary(generatedFiles, dispatchIdentifiers, failed, totalDuration) {
|
|
2706
2995
|
log.info(
|
|
2707
2996
|
`Spec generation complete: ${generatedFiles.length} generated, ${failed} failed in ${elapsed(totalDuration)}`
|
|
2708
2997
|
);
|
|
@@ -2718,19 +3007,91 @@ async function runSpecPipeline(opts) {
|
|
|
2718
3007
|
`);
|
|
2719
3008
|
}
|
|
2720
3009
|
}
|
|
3010
|
+
}
|
|
3011
|
+
async function runSpecPipeline(opts) {
|
|
3012
|
+
const {
|
|
3013
|
+
issues,
|
|
3014
|
+
provider,
|
|
3015
|
+
model,
|
|
3016
|
+
serverUrl,
|
|
3017
|
+
cwd: specCwd,
|
|
3018
|
+
outputDir = join7(specCwd, ".dispatch", "specs"),
|
|
3019
|
+
org,
|
|
3020
|
+
project,
|
|
3021
|
+
workItemType,
|
|
3022
|
+
iteration,
|
|
3023
|
+
area,
|
|
3024
|
+
concurrency = defaultConcurrency(),
|
|
3025
|
+
dryRun,
|
|
3026
|
+
retries = 2
|
|
3027
|
+
} = opts;
|
|
3028
|
+
const pipelineStart = Date.now();
|
|
3029
|
+
const resolved = await resolveDatasource(issues, opts.issueSource, specCwd, org, project, workItemType, iteration, area);
|
|
3030
|
+
if (!resolved) {
|
|
3031
|
+
return { total: 0, generated: 0, failed: 0, files: [], issueNumbers: [], durationMs: Date.now() - pipelineStart, fileDurationsMs: {} };
|
|
3032
|
+
}
|
|
3033
|
+
const { source, datasource: datasource4, fetchOpts } = resolved;
|
|
3034
|
+
const isTrackerMode = isIssueNumbers(issues);
|
|
3035
|
+
const isInlineText = !isTrackerMode && !isGlobOrFilePath(issues);
|
|
3036
|
+
let items;
|
|
3037
|
+
if (isTrackerMode) {
|
|
3038
|
+
items = await fetchTrackerItems(issues, datasource4, fetchOpts, concurrency, source);
|
|
3039
|
+
if (items.length === 0) {
|
|
3040
|
+
return { total: 0, generated: 0, failed: 0, files: [], issueNumbers: [], durationMs: Date.now() - pipelineStart, fileDurationsMs: {} };
|
|
3041
|
+
}
|
|
3042
|
+
} else if (isInlineText) {
|
|
3043
|
+
items = buildInlineTextItem(issues, outputDir);
|
|
3044
|
+
} else {
|
|
3045
|
+
const fileItems = await resolveFileItems(issues, specCwd, concurrency);
|
|
3046
|
+
if (!fileItems) {
|
|
3047
|
+
return { total: 0, generated: 0, failed: 0, files: [], issueNumbers: [], durationMs: Date.now() - pipelineStart, fileDurationsMs: {} };
|
|
3048
|
+
}
|
|
3049
|
+
items = fileItems;
|
|
3050
|
+
}
|
|
3051
|
+
const validItems = filterValidItems(items, isTrackerMode, isInlineText);
|
|
3052
|
+
if (!validItems) {
|
|
3053
|
+
return { total: items.length, generated: 0, failed: items.length, files: [], issueNumbers: [], durationMs: Date.now() - pipelineStart, fileDurationsMs: {} };
|
|
3054
|
+
}
|
|
3055
|
+
if (dryRun) {
|
|
3056
|
+
return previewDryRun(validItems, items, isTrackerMode, isInlineText, outputDir, pipelineStart);
|
|
3057
|
+
}
|
|
3058
|
+
const confirmed = await confirmLargeBatch(validItems.length);
|
|
3059
|
+
if (!confirmed) {
|
|
3060
|
+
return { total: 0, generated: 0, failed: 0, files: [], issueNumbers: [], durationMs: Date.now() - pipelineStart, fileDurationsMs: {} };
|
|
3061
|
+
}
|
|
3062
|
+
const { specAgent, instance } = await bootPipeline(provider, serverUrl, specCwd, model, source);
|
|
3063
|
+
const results = await generateSpecsBatch(
|
|
3064
|
+
validItems,
|
|
3065
|
+
items,
|
|
3066
|
+
specAgent,
|
|
3067
|
+
instance,
|
|
3068
|
+
isTrackerMode,
|
|
3069
|
+
isInlineText,
|
|
3070
|
+
datasource4,
|
|
3071
|
+
fetchOpts,
|
|
3072
|
+
outputDir,
|
|
3073
|
+
specCwd,
|
|
3074
|
+
concurrency,
|
|
3075
|
+
retries
|
|
3076
|
+
);
|
|
3077
|
+
await cleanupPipeline(specAgent, instance);
|
|
3078
|
+
const totalDuration = Date.now() - pipelineStart;
|
|
3079
|
+
logSummary(results.generatedFiles, results.dispatchIdentifiers, results.failed, totalDuration);
|
|
2721
3080
|
return {
|
|
2722
3081
|
total: items.length,
|
|
2723
|
-
generated: generatedFiles.length,
|
|
2724
|
-
failed,
|
|
2725
|
-
files: generatedFiles,
|
|
2726
|
-
issueNumbers,
|
|
2727
|
-
identifiers: dispatchIdentifiers,
|
|
3082
|
+
generated: results.generatedFiles.length,
|
|
3083
|
+
failed: results.failed,
|
|
3084
|
+
files: results.generatedFiles,
|
|
3085
|
+
issueNumbers: results.issueNumbers,
|
|
3086
|
+
identifiers: results.dispatchIdentifiers,
|
|
2728
3087
|
durationMs: totalDuration,
|
|
2729
|
-
fileDurationsMs
|
|
3088
|
+
fileDurationsMs: results.fileDurationsMs
|
|
2730
3089
|
};
|
|
2731
3090
|
}
|
|
2732
3091
|
|
|
2733
3092
|
// src/orchestrator/dispatch-pipeline.ts
|
|
3093
|
+
import { execFile as execFile9 } from "child_process";
|
|
3094
|
+
import { promisify as promisify9 } from "util";
|
|
2734
3095
|
import { readFile as readFile7 } from "fs/promises";
|
|
2735
3096
|
|
|
2736
3097
|
// src/parser.ts
|
|
@@ -2785,7 +3146,9 @@ async function parseTaskFile(filePath) {
|
|
|
2785
3146
|
}
|
|
2786
3147
|
async function markTaskComplete(task) {
|
|
2787
3148
|
const content = await readFile6(task.file, "utf-8");
|
|
2788
|
-
const
|
|
3149
|
+
const eol = content.includes("\r\n") ? "\r\n" : "\n";
|
|
3150
|
+
const normalized = content.replace(/\r\n/g, "\n");
|
|
3151
|
+
const lines = normalized.split("\n");
|
|
2789
3152
|
const lineIndex = task.line - 1;
|
|
2790
3153
|
if (lineIndex < 0 || lineIndex >= lines.length) {
|
|
2791
3154
|
throw new Error(
|
|
@@ -2800,7 +3163,7 @@ async function markTaskComplete(task) {
|
|
|
2800
3163
|
);
|
|
2801
3164
|
}
|
|
2802
3165
|
lines[lineIndex] = updated;
|
|
2803
|
-
await writeFile5(task.file, lines.join(
|
|
3166
|
+
await writeFile5(task.file, lines.join(eol), "utf-8");
|
|
2804
3167
|
}
|
|
2805
3168
|
function groupTasksByMode(tasks) {
|
|
2806
3169
|
if (tasks.length === 0) return [];
|
|
@@ -2830,6 +3193,7 @@ function groupTasksByMode(tasks) {
|
|
|
2830
3193
|
|
|
2831
3194
|
// src/agents/planner.ts
|
|
2832
3195
|
init_logger();
|
|
3196
|
+
init_file_logger();
|
|
2833
3197
|
async function boot6(opts) {
|
|
2834
3198
|
const { provider, cwd } = opts;
|
|
2835
3199
|
if (!provider) {
|
|
@@ -2837,25 +3201,31 @@ async function boot6(opts) {
|
|
|
2837
3201
|
}
|
|
2838
3202
|
return {
|
|
2839
3203
|
name: "planner",
|
|
2840
|
-
async plan(task, fileContext, cwdOverride) {
|
|
3204
|
+
async plan(task, fileContext, cwdOverride, worktreeRoot) {
|
|
3205
|
+
const startTime = Date.now();
|
|
2841
3206
|
try {
|
|
2842
3207
|
const sessionId = await provider.createSession();
|
|
2843
|
-
const prompt = buildPlannerPrompt(task, cwdOverride ?? cwd, fileContext);
|
|
3208
|
+
const prompt = buildPlannerPrompt(task, cwdOverride ?? cwd, fileContext, worktreeRoot);
|
|
3209
|
+
fileLoggerStorage.getStore()?.prompt("planner", prompt);
|
|
2844
3210
|
const plan = await provider.prompt(sessionId, prompt);
|
|
3211
|
+
if (plan) fileLoggerStorage.getStore()?.response("planner", plan);
|
|
2845
3212
|
if (!plan?.trim()) {
|
|
2846
|
-
return {
|
|
3213
|
+
return { data: null, success: false, error: "Planner returned empty plan", durationMs: Date.now() - startTime };
|
|
2847
3214
|
}
|
|
2848
|
-
|
|
3215
|
+
fileLoggerStorage.getStore()?.agentEvent("planner", "completed", `${Date.now() - startTime}ms`);
|
|
3216
|
+
return { data: { prompt: plan }, success: true, durationMs: Date.now() - startTime };
|
|
2849
3217
|
} catch (err) {
|
|
2850
3218
|
const message = log.extractMessage(err);
|
|
2851
|
-
|
|
3219
|
+
fileLoggerStorage.getStore()?.error(`planner error: ${message}${err instanceof Error && err.stack ? `
|
|
3220
|
+
${err.stack}` : ""}`);
|
|
3221
|
+
return { data: null, success: false, error: message, durationMs: Date.now() - startTime };
|
|
2852
3222
|
}
|
|
2853
3223
|
},
|
|
2854
3224
|
async cleanup() {
|
|
2855
3225
|
}
|
|
2856
3226
|
};
|
|
2857
3227
|
}
|
|
2858
|
-
function buildPlannerPrompt(task, cwd, fileContext) {
|
|
3228
|
+
function buildPlannerPrompt(task, cwd, fileContext, worktreeRoot) {
|
|
2859
3229
|
const sections = [
|
|
2860
3230
|
`You are a **planning agent**. Your job is to explore the codebase, understand the task below, and produce a detailed execution prompt that another agent will follow to implement the changes.`,
|
|
2861
3231
|
``,
|
|
@@ -2879,6 +3249,21 @@ function buildPlannerPrompt(task, cwd, fileContext) {
|
|
|
2879
3249
|
`\`\`\``
|
|
2880
3250
|
);
|
|
2881
3251
|
}
|
|
3252
|
+
if (worktreeRoot) {
|
|
3253
|
+
sections.push(
|
|
3254
|
+
``,
|
|
3255
|
+
`## Worktree Isolation`,
|
|
3256
|
+
``,
|
|
3257
|
+
`You are operating inside a git worktree. All file operations MUST be confined`,
|
|
3258
|
+
`to the following directory tree:`,
|
|
3259
|
+
``,
|
|
3260
|
+
` ${worktreeRoot}`,
|
|
3261
|
+
``,
|
|
3262
|
+
`- Do NOT read, write, or execute commands that access files outside this directory.`,
|
|
3263
|
+
`- Do NOT reference or modify files in the main repository working tree or other worktrees.`,
|
|
3264
|
+
`- All relative paths must resolve within the worktree root above.`
|
|
3265
|
+
);
|
|
3266
|
+
}
|
|
2882
3267
|
sections.push(
|
|
2883
3268
|
``,
|
|
2884
3269
|
`## Instructions`,
|
|
@@ -2914,26 +3299,32 @@ function buildPlannerPrompt(task, cwd, fileContext) {
|
|
|
2914
3299
|
|
|
2915
3300
|
// src/dispatcher.ts
|
|
2916
3301
|
init_logger();
|
|
2917
|
-
|
|
3302
|
+
init_file_logger();
|
|
3303
|
+
async function dispatchTask(instance, task, cwd, plan, worktreeRoot) {
|
|
2918
3304
|
try {
|
|
2919
3305
|
log.debug(`Dispatching task: ${task.file}:${task.line} \u2014 ${task.text.slice(0, 80)}`);
|
|
2920
3306
|
const sessionId = await instance.createSession();
|
|
2921
|
-
const prompt = plan ? buildPlannedPrompt(task, cwd, plan) : buildPrompt(task, cwd);
|
|
3307
|
+
const prompt = plan ? buildPlannedPrompt(task, cwd, plan, worktreeRoot) : buildPrompt(task, cwd, worktreeRoot);
|
|
2922
3308
|
log.debug(`Prompt built (${prompt.length} chars, ${plan ? "with plan" : "no plan"})`);
|
|
3309
|
+
fileLoggerStorage.getStore()?.prompt("dispatchTask", prompt);
|
|
2923
3310
|
const response = await instance.prompt(sessionId, prompt);
|
|
2924
3311
|
if (response === null) {
|
|
2925
3312
|
log.debug("Task dispatch returned null response");
|
|
3313
|
+
fileLoggerStorage.getStore()?.warn("dispatchTask: null response");
|
|
2926
3314
|
return { task, success: false, error: "No response from agent" };
|
|
2927
3315
|
}
|
|
2928
3316
|
log.debug(`Task dispatch completed (${response.length} chars response)`);
|
|
3317
|
+
fileLoggerStorage.getStore()?.response("dispatchTask", response);
|
|
2929
3318
|
return { task, success: true };
|
|
2930
3319
|
} catch (err) {
|
|
2931
3320
|
const message = log.extractMessage(err);
|
|
2932
3321
|
log.debug(`Task dispatch failed: ${log.formatErrorChain(err)}`);
|
|
3322
|
+
fileLoggerStorage.getStore()?.error(`dispatchTask error: ${message}${err instanceof Error && err.stack ? `
|
|
3323
|
+
${err.stack}` : ""}`);
|
|
2933
3324
|
return { task, success: false, error: message };
|
|
2934
3325
|
}
|
|
2935
3326
|
}
|
|
2936
|
-
function buildPrompt(task, cwd) {
|
|
3327
|
+
function buildPrompt(task, cwd, worktreeRoot) {
|
|
2937
3328
|
return [
|
|
2938
3329
|
`You are completing a task from a markdown task file.`,
|
|
2939
3330
|
``,
|
|
@@ -2945,10 +3336,11 @@ function buildPrompt(task, cwd) {
|
|
|
2945
3336
|
`- Complete ONLY this specific task \u2014 do not work on other tasks.`,
|
|
2946
3337
|
`- Make the minimal, correct changes needed.`,
|
|
2947
3338
|
buildCommitInstruction(task.text),
|
|
3339
|
+
...buildWorktreeIsolation(worktreeRoot),
|
|
2948
3340
|
`- When finished, confirm by saying "Task complete."`
|
|
2949
3341
|
].join("\n");
|
|
2950
3342
|
}
|
|
2951
|
-
function buildPlannedPrompt(task, cwd, plan) {
|
|
3343
|
+
function buildPlannedPrompt(task, cwd, plan, worktreeRoot) {
|
|
2952
3344
|
return [
|
|
2953
3345
|
`You are an **executor agent** completing a task that has been pre-planned by a planner agent.`,
|
|
2954
3346
|
`The planner has already explored the codebase and produced detailed instructions below.`,
|
|
@@ -2973,6 +3365,7 @@ function buildPlannedPrompt(task, cwd, plan) {
|
|
|
2973
3365
|
`- Do NOT re-plan, question, or revise the plan. Trust it as given and execute it faithfully.`,
|
|
2974
3366
|
`- Do NOT search for additional context using grep, find, or similar tools unless the plan explicitly instructs you to.`,
|
|
2975
3367
|
buildCommitInstruction(task.text),
|
|
3368
|
+
...buildWorktreeIsolation(worktreeRoot),
|
|
2976
3369
|
`- When finished, confirm by saying "Task complete."`
|
|
2977
3370
|
].join("\n");
|
|
2978
3371
|
}
|
|
@@ -2985,9 +3378,16 @@ function buildCommitInstruction(taskText) {
|
|
|
2985
3378
|
}
|
|
2986
3379
|
return `- Do NOT commit changes \u2014 the orchestrator handles commits.`;
|
|
2987
3380
|
}
|
|
3381
|
+
function buildWorktreeIsolation(worktreeRoot) {
|
|
3382
|
+
if (!worktreeRoot) return [];
|
|
3383
|
+
return [
|
|
3384
|
+
`- **Worktree isolation:** You are operating inside a git worktree at \`${worktreeRoot}\`. You MUST NOT read, write, or execute commands that access files outside this directory. All file paths must resolve within \`${worktreeRoot}\`.`
|
|
3385
|
+
];
|
|
3386
|
+
}
|
|
2988
3387
|
|
|
2989
3388
|
// src/agents/executor.ts
|
|
2990
3389
|
init_logger();
|
|
3390
|
+
init_file_logger();
|
|
2991
3391
|
async function boot7(opts) {
|
|
2992
3392
|
const { provider } = opts;
|
|
2993
3393
|
if (!provider) {
|
|
@@ -2995,28 +3395,24 @@ async function boot7(opts) {
|
|
|
2995
3395
|
}
|
|
2996
3396
|
return {
|
|
2997
3397
|
name: "executor",
|
|
2998
|
-
async execute(
|
|
2999
|
-
const { task, cwd, plan } =
|
|
3398
|
+
async execute(input3) {
|
|
3399
|
+
const { task, cwd, plan, worktreeRoot } = input3;
|
|
3000
3400
|
const startTime = Date.now();
|
|
3001
3401
|
try {
|
|
3002
|
-
|
|
3402
|
+
fileLoggerStorage.getStore()?.agentEvent("executor", "started", task.text);
|
|
3403
|
+
const result = await dispatchTask(provider, task, cwd, plan ?? void 0, worktreeRoot);
|
|
3003
3404
|
if (result.success) {
|
|
3004
3405
|
await markTaskComplete(task);
|
|
3406
|
+
fileLoggerStorage.getStore()?.agentEvent("executor", "completed", `${Date.now() - startTime}ms`);
|
|
3407
|
+
return { data: { dispatchResult: result }, success: true, durationMs: Date.now() - startTime };
|
|
3005
3408
|
}
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
success: result.success,
|
|
3009
|
-
error: result.error,
|
|
3010
|
-
elapsedMs: Date.now() - startTime
|
|
3011
|
-
};
|
|
3409
|
+
fileLoggerStorage.getStore()?.agentEvent("executor", "failed", result.error ?? "unknown error");
|
|
3410
|
+
return { data: null, success: false, error: result.error, durationMs: Date.now() - startTime };
|
|
3012
3411
|
} catch (err) {
|
|
3013
3412
|
const message = log.extractMessage(err);
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
error: message,
|
|
3018
|
-
elapsedMs: Date.now() - startTime
|
|
3019
|
-
};
|
|
3413
|
+
fileLoggerStorage.getStore()?.error(`executor error: ${message}${err instanceof Error && err.stack ? `
|
|
3414
|
+
${err.stack}` : ""}`);
|
|
3415
|
+
return { data: null, success: false, error: message, durationMs: Date.now() - startTime };
|
|
3020
3416
|
}
|
|
3021
3417
|
},
|
|
3022
3418
|
async cleanup() {
|
|
@@ -3026,8 +3422,9 @@ async function boot7(opts) {
|
|
|
3026
3422
|
|
|
3027
3423
|
// src/agents/commit.ts
|
|
3028
3424
|
init_logger();
|
|
3425
|
+
init_file_logger();
|
|
3029
3426
|
import { mkdir as mkdir5, writeFile as writeFile6 } from "fs/promises";
|
|
3030
|
-
import { join as
|
|
3427
|
+
import { join as join8, resolve as resolve2 } from "path";
|
|
3031
3428
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
3032
3429
|
async function boot8(opts) {
|
|
3033
3430
|
const { provider } = opts;
|
|
@@ -3040,14 +3437,17 @@ async function boot8(opts) {
|
|
|
3040
3437
|
name: "commit",
|
|
3041
3438
|
async generate(genOpts) {
|
|
3042
3439
|
try {
|
|
3043
|
-
const
|
|
3440
|
+
const resolvedCwd = resolve2(genOpts.cwd);
|
|
3441
|
+
const tmpDir = join8(resolvedCwd, ".dispatch", "tmp");
|
|
3044
3442
|
await mkdir5(tmpDir, { recursive: true });
|
|
3045
3443
|
const tmpFilename = `commit-${randomUUID4()}.md`;
|
|
3046
|
-
const tmpPath =
|
|
3444
|
+
const tmpPath = join8(tmpDir, tmpFilename);
|
|
3047
3445
|
const prompt = buildCommitPrompt(genOpts);
|
|
3446
|
+
fileLoggerStorage.getStore()?.prompt("commit", prompt);
|
|
3048
3447
|
const sessionId = await provider.createSession();
|
|
3049
3448
|
log.debug(`Commit prompt built (${prompt.length} chars)`);
|
|
3050
3449
|
const response = await provider.prompt(sessionId, prompt);
|
|
3450
|
+
if (response) fileLoggerStorage.getStore()?.response("commit", response);
|
|
3051
3451
|
if (!response?.trim()) {
|
|
3052
3452
|
return {
|
|
3053
3453
|
commitMessage: "",
|
|
@@ -3071,12 +3471,15 @@ async function boot8(opts) {
|
|
|
3071
3471
|
const outputContent = formatOutputFile(parsed);
|
|
3072
3472
|
await writeFile6(tmpPath, outputContent, "utf-8");
|
|
3073
3473
|
log.debug(`Wrote commit agent output to ${tmpPath}`);
|
|
3474
|
+
fileLoggerStorage.getStore()?.agentEvent("commit", "completed", `message: ${parsed.commitMessage.slice(0, 80)}`);
|
|
3074
3475
|
return {
|
|
3075
3476
|
...parsed,
|
|
3076
3477
|
success: true,
|
|
3077
3478
|
outputPath: tmpPath
|
|
3078
3479
|
};
|
|
3079
3480
|
} catch (err) {
|
|
3481
|
+
fileLoggerStorage.getStore()?.error(`commit error: ${log.extractMessage(err)}${err instanceof Error && err.stack ? `
|
|
3482
|
+
${err.stack}` : ""}`);
|
|
3080
3483
|
const message = log.extractMessage(err);
|
|
3081
3484
|
return {
|
|
3082
3485
|
commitMessage: "",
|
|
@@ -3217,9 +3620,10 @@ init_logger();
|
|
|
3217
3620
|
init_cleanup();
|
|
3218
3621
|
|
|
3219
3622
|
// src/helpers/worktree.ts
|
|
3220
|
-
import { join as
|
|
3623
|
+
import { join as join9, basename } from "path";
|
|
3221
3624
|
import { execFile as execFile7 } from "child_process";
|
|
3222
3625
|
import { promisify as promisify7 } from "util";
|
|
3626
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
3223
3627
|
init_logger();
|
|
3224
3628
|
var exec7 = promisify7(execFile7);
|
|
3225
3629
|
var WORKTREE_DIR = ".dispatch/worktrees";
|
|
@@ -3230,13 +3634,16 @@ async function git2(args, cwd) {
|
|
|
3230
3634
|
function worktreeName(issueFilename) {
|
|
3231
3635
|
const base = basename(issueFilename);
|
|
3232
3636
|
const withoutExt = base.replace(/\.md$/i, "");
|
|
3233
|
-
|
|
3637
|
+
const match = withoutExt.match(/^(\d+)/);
|
|
3638
|
+
return match ? `issue-${match[1]}` : slugify(withoutExt);
|
|
3234
3639
|
}
|
|
3235
|
-
async function createWorktree(repoRoot, issueFilename, branchName) {
|
|
3640
|
+
async function createWorktree(repoRoot, issueFilename, branchName, startPoint) {
|
|
3236
3641
|
const name = worktreeName(issueFilename);
|
|
3237
|
-
const worktreePath =
|
|
3642
|
+
const worktreePath = join9(repoRoot, WORKTREE_DIR, name);
|
|
3238
3643
|
try {
|
|
3239
|
-
|
|
3644
|
+
const args = ["worktree", "add", worktreePath, "-b", branchName];
|
|
3645
|
+
if (startPoint) args.push(startPoint);
|
|
3646
|
+
await git2(args, repoRoot);
|
|
3240
3647
|
log.debug(`Created worktree at ${worktreePath} on branch ${branchName}`);
|
|
3241
3648
|
} catch (err) {
|
|
3242
3649
|
const message = log.extractMessage(err);
|
|
@@ -3251,7 +3658,7 @@ async function createWorktree(repoRoot, issueFilename, branchName) {
|
|
|
3251
3658
|
}
|
|
3252
3659
|
async function removeWorktree(repoRoot, issueFilename) {
|
|
3253
3660
|
const name = worktreeName(issueFilename);
|
|
3254
|
-
const worktreePath =
|
|
3661
|
+
const worktreePath = join9(repoRoot, WORKTREE_DIR, name);
|
|
3255
3662
|
try {
|
|
3256
3663
|
await git2(["worktree", "remove", worktreePath], repoRoot);
|
|
3257
3664
|
} catch {
|
|
@@ -3268,6 +3675,11 @@ async function removeWorktree(repoRoot, issueFilename) {
|
|
|
3268
3675
|
log.warn(`Could not prune worktrees: ${log.formatErrorChain(err)}`);
|
|
3269
3676
|
}
|
|
3270
3677
|
}
|
|
3678
|
+
function generateFeatureBranchName() {
|
|
3679
|
+
const uuid = randomUUID5();
|
|
3680
|
+
const octet = uuid.split("-")[0];
|
|
3681
|
+
return `dispatch/feature-${octet}`;
|
|
3682
|
+
}
|
|
3271
3683
|
|
|
3272
3684
|
// src/tui.ts
|
|
3273
3685
|
import chalk6 from "chalk";
|
|
@@ -3481,13 +3893,24 @@ function render(state) {
|
|
|
3481
3893
|
return lines.join("\n");
|
|
3482
3894
|
}
|
|
3483
3895
|
function draw(state) {
|
|
3484
|
-
if (lastLineCount > 0) {
|
|
3485
|
-
process.stdout.write(`\x1B[${lastLineCount}A\x1B[0J`);
|
|
3486
|
-
}
|
|
3487
3896
|
const output = render(state);
|
|
3488
|
-
process.stdout.write(output);
|
|
3489
3897
|
const cols = process.stdout.columns || 80;
|
|
3490
|
-
|
|
3898
|
+
const newLineCount = countVisualRows(output, cols);
|
|
3899
|
+
let buffer = "";
|
|
3900
|
+
if (lastLineCount > 0) {
|
|
3901
|
+
buffer += `\x1B[${lastLineCount}A`;
|
|
3902
|
+
}
|
|
3903
|
+
const lines = output.split("\n");
|
|
3904
|
+
buffer += lines.map((line) => line + "\x1B[K").join("\n");
|
|
3905
|
+
const leftover = lastLineCount - newLineCount;
|
|
3906
|
+
if (leftover > 0) {
|
|
3907
|
+
for (let i = 0; i < leftover; i++) {
|
|
3908
|
+
buffer += "\n\x1B[K";
|
|
3909
|
+
}
|
|
3910
|
+
buffer += `\x1B[${leftover}A`;
|
|
3911
|
+
}
|
|
3912
|
+
process.stdout.write(buffer);
|
|
3913
|
+
lastLineCount = newLineCount;
|
|
3491
3914
|
}
|
|
3492
3915
|
function createTui() {
|
|
3493
3916
|
const state = {
|
|
@@ -3517,7 +3940,7 @@ init_providers();
|
|
|
3517
3940
|
|
|
3518
3941
|
// src/orchestrator/datasource-helpers.ts
|
|
3519
3942
|
init_logger();
|
|
3520
|
-
import { basename as basename2, join as
|
|
3943
|
+
import { basename as basename2, join as join10 } from "path";
|
|
3521
3944
|
import { mkdtemp, writeFile as writeFile7 } from "fs/promises";
|
|
3522
3945
|
import { tmpdir } from "os";
|
|
3523
3946
|
import { execFile as execFile8 } from "child_process";
|
|
@@ -3545,13 +3968,13 @@ async function fetchItemsById(issueIds, datasource4, fetchOpts) {
|
|
|
3545
3968
|
return items;
|
|
3546
3969
|
}
|
|
3547
3970
|
async function writeItemsToTempDir(items) {
|
|
3548
|
-
const tempDir = await mkdtemp(
|
|
3971
|
+
const tempDir = await mkdtemp(join10(tmpdir(), "dispatch-"));
|
|
3549
3972
|
const files = [];
|
|
3550
3973
|
const issueDetailsByFile = /* @__PURE__ */ new Map();
|
|
3551
3974
|
for (const item of items) {
|
|
3552
3975
|
const slug = slugify(item.title, MAX_SLUG_LENGTH);
|
|
3553
3976
|
const filename = `${item.number}-${slug}.md`;
|
|
3554
|
-
const filepath =
|
|
3977
|
+
const filepath = join10(tempDir, filename);
|
|
3555
3978
|
await writeFile7(filepath, item.body, "utf-8");
|
|
3556
3979
|
files.push(filepath);
|
|
3557
3980
|
issueDetailsByFile.set(filepath, item);
|
|
@@ -3564,34 +3987,6 @@ async function writeItemsToTempDir(items) {
|
|
|
3564
3987
|
});
|
|
3565
3988
|
return { files, issueDetailsByFile };
|
|
3566
3989
|
}
|
|
3567
|
-
async function closeCompletedSpecIssues(taskFiles, results, cwd, source, org, project, workItemType) {
|
|
3568
|
-
let datasourceName = source;
|
|
3569
|
-
if (!datasourceName) {
|
|
3570
|
-
datasourceName = await detectDatasource(cwd) ?? void 0;
|
|
3571
|
-
}
|
|
3572
|
-
if (!datasourceName) return;
|
|
3573
|
-
const datasource4 = getDatasource(datasourceName);
|
|
3574
|
-
const succeededTasks = new Set(
|
|
3575
|
-
results.filter((r) => r.success).map((r) => r.task)
|
|
3576
|
-
);
|
|
3577
|
-
const fetchOpts = { cwd, org, project, workItemType };
|
|
3578
|
-
for (const taskFile of taskFiles) {
|
|
3579
|
-
const fileTasks = taskFile.tasks;
|
|
3580
|
-
if (fileTasks.length === 0) continue;
|
|
3581
|
-
const allSucceeded = fileTasks.every((t) => succeededTasks.has(t));
|
|
3582
|
-
if (!allSucceeded) continue;
|
|
3583
|
-
const parsed = parseIssueFilename(taskFile.path);
|
|
3584
|
-
if (!parsed) continue;
|
|
3585
|
-
const { issueId } = parsed;
|
|
3586
|
-
const filename = basename2(taskFile.path);
|
|
3587
|
-
try {
|
|
3588
|
-
await datasource4.close(issueId, fetchOpts);
|
|
3589
|
-
log.success(`Closed issue #${issueId} (all tasks in ${filename} completed)`);
|
|
3590
|
-
} catch (err) {
|
|
3591
|
-
log.warn(`Could not close issue #${issueId}: ${log.formatErrorChain(err)}`);
|
|
3592
|
-
}
|
|
3593
|
-
}
|
|
3594
|
-
}
|
|
3595
3990
|
async function getCommitSummaries(defaultBranch, cwd) {
|
|
3596
3991
|
try {
|
|
3597
3992
|
const { stdout } = await exec8(
|
|
@@ -3675,48 +4070,51 @@ async function buildPrTitle(issueTitle, defaultBranch, cwd) {
|
|
|
3675
4070
|
}
|
|
3676
4071
|
return `${commits[commits.length - 1]} (+${commits.length - 1} more)`;
|
|
3677
4072
|
}
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
/** Optional label identifying the operation that timed out. */
|
|
3682
|
-
label;
|
|
3683
|
-
constructor(ms, label) {
|
|
3684
|
-
const suffix = label ? ` [${label}]` : "";
|
|
3685
|
-
super(`Timed out after ${ms}ms${suffix}`);
|
|
3686
|
-
this.name = "TimeoutError";
|
|
3687
|
-
this.label = label;
|
|
4073
|
+
function buildFeaturePrTitle(featureBranchName, issues) {
|
|
4074
|
+
if (issues.length === 1) {
|
|
4075
|
+
return issues[0].title;
|
|
3688
4076
|
}
|
|
3689
|
-
};
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
},
|
|
3705
|
-
(err) => {
|
|
3706
|
-
if (settled) return;
|
|
3707
|
-
settled = true;
|
|
3708
|
-
clearTimeout(timer);
|
|
3709
|
-
reject(err);
|
|
3710
|
-
}
|
|
3711
|
-
);
|
|
3712
|
-
});
|
|
3713
|
-
p.catch(() => {
|
|
4077
|
+
const issueRefs = issues.map((d) => `#${d.number}`).join(", ");
|
|
4078
|
+
return `feat: ${featureBranchName} (${issueRefs})`;
|
|
4079
|
+
}
|
|
4080
|
+
function buildFeaturePrBody(issues, tasks, results, datasourceName) {
|
|
4081
|
+
const sections = [];
|
|
4082
|
+
sections.push("## Issues\n");
|
|
4083
|
+
for (const issue of issues) {
|
|
4084
|
+
sections.push(`- #${issue.number}: ${issue.title}`);
|
|
4085
|
+
}
|
|
4086
|
+
sections.push("");
|
|
4087
|
+
const taskResults = new Map(results.map((r) => [r.task, r]));
|
|
4088
|
+
const completedTasks = tasks.filter((t) => taskResults.get(t)?.success);
|
|
4089
|
+
const failedTasks = tasks.filter((t) => {
|
|
4090
|
+
const r = taskResults.get(t);
|
|
4091
|
+
return r && !r.success;
|
|
3714
4092
|
});
|
|
3715
|
-
|
|
4093
|
+
if (completedTasks.length > 0 || failedTasks.length > 0) {
|
|
4094
|
+
sections.push("## Tasks\n");
|
|
4095
|
+
for (const task of completedTasks) {
|
|
4096
|
+
sections.push(`- [x] ${task.text}`);
|
|
4097
|
+
}
|
|
4098
|
+
for (const task of failedTasks) {
|
|
4099
|
+
sections.push(`- [ ] ${task.text}`);
|
|
4100
|
+
}
|
|
4101
|
+
sections.push("");
|
|
4102
|
+
}
|
|
4103
|
+
for (const issue of issues) {
|
|
4104
|
+
if (datasourceName === "github") {
|
|
4105
|
+
sections.push(`Closes #${issue.number}`);
|
|
4106
|
+
} else if (datasourceName === "azdevops") {
|
|
4107
|
+
sections.push(`Resolves AB#${issue.number}`);
|
|
4108
|
+
}
|
|
4109
|
+
}
|
|
4110
|
+
return sections.join("\n");
|
|
3716
4111
|
}
|
|
3717
4112
|
|
|
3718
4113
|
// src/orchestrator/dispatch-pipeline.ts
|
|
4114
|
+
init_timeout();
|
|
3719
4115
|
import chalk7 from "chalk";
|
|
4116
|
+
init_file_logger();
|
|
4117
|
+
var exec9 = promisify9(execFile9);
|
|
3720
4118
|
var DEFAULT_PLAN_TIMEOUT_MIN = 10;
|
|
3721
4119
|
var DEFAULT_PLAN_RETRIES = 1;
|
|
3722
4120
|
async function runDispatchPipeline(opts, cwd) {
|
|
@@ -3728,12 +4126,15 @@ async function runDispatchPipeline(opts, cwd) {
|
|
|
3728
4126
|
noPlan,
|
|
3729
4127
|
noBranch,
|
|
3730
4128
|
noWorktree,
|
|
4129
|
+
feature,
|
|
3731
4130
|
provider = "opencode",
|
|
3732
4131
|
model,
|
|
3733
4132
|
source,
|
|
3734
4133
|
org,
|
|
3735
4134
|
project,
|
|
3736
4135
|
workItemType,
|
|
4136
|
+
iteration,
|
|
4137
|
+
area,
|
|
3737
4138
|
planTimeout,
|
|
3738
4139
|
planRetries,
|
|
3739
4140
|
retries
|
|
@@ -3742,7 +4143,7 @@ async function runDispatchPipeline(opts, cwd) {
|
|
|
3742
4143
|
const maxPlanAttempts = (planRetries ?? retries ?? DEFAULT_PLAN_RETRIES) + 1;
|
|
3743
4144
|
log.debug(`Plan timeout: ${planTimeout ?? DEFAULT_PLAN_TIMEOUT_MIN}m (${planTimeoutMs}ms), max attempts: ${maxPlanAttempts}`);
|
|
3744
4145
|
if (dryRun) {
|
|
3745
|
-
return dryRunMode(issueIds, cwd, source, org, project, workItemType);
|
|
4146
|
+
return dryRunMode(issueIds, cwd, source, org, project, workItemType, iteration, area);
|
|
3746
4147
|
}
|
|
3747
4148
|
const verbose = log.verbose;
|
|
3748
4149
|
let tui;
|
|
@@ -3778,7 +4179,7 @@ async function runDispatchPipeline(opts, cwd) {
|
|
|
3778
4179
|
return { total: 0, completed: 0, failed: 0, skipped: 0, results: [] };
|
|
3779
4180
|
}
|
|
3780
4181
|
const datasource4 = getDatasource(source);
|
|
3781
|
-
const fetchOpts = { cwd, org, project, workItemType };
|
|
4182
|
+
const fetchOpts = { cwd, org, project, workItemType, iteration, area };
|
|
3782
4183
|
const items = issueIds.length > 0 ? await fetchItemsById(issueIds, datasource4, fetchOpts) : await datasource4.list(fetchOpts);
|
|
3783
4184
|
if (items.length === 0) {
|
|
3784
4185
|
tui.state.phase = "done";
|
|
@@ -3820,7 +4221,7 @@ async function runDispatchPipeline(opts, cwd) {
|
|
|
3820
4221
|
list.push(task);
|
|
3821
4222
|
tasksByFile.set(task.file, list);
|
|
3822
4223
|
}
|
|
3823
|
-
const useWorktrees = !noWorktree && !noBranch && tasksByFile.size > 1;
|
|
4224
|
+
const useWorktrees = !noWorktree && (feature || !noBranch && tasksByFile.size > 1);
|
|
3824
4225
|
tui.state.phase = "booting";
|
|
3825
4226
|
if (verbose) log.info(`Booting ${provider} provider...`);
|
|
3826
4227
|
if (serverUrl) {
|
|
@@ -3848,6 +4249,30 @@ async function runDispatchPipeline(opts, cwd) {
|
|
|
3848
4249
|
let completed = 0;
|
|
3849
4250
|
let failed = 0;
|
|
3850
4251
|
const lifecycleOpts = { cwd };
|
|
4252
|
+
let featureBranchName;
|
|
4253
|
+
let featureDefaultBranch;
|
|
4254
|
+
if (feature) {
|
|
4255
|
+
try {
|
|
4256
|
+
featureDefaultBranch = await datasource4.getDefaultBranch(lifecycleOpts);
|
|
4257
|
+
await datasource4.switchBranch(featureDefaultBranch, lifecycleOpts);
|
|
4258
|
+
featureBranchName = generateFeatureBranchName();
|
|
4259
|
+
await datasource4.createAndSwitchBranch(featureBranchName, lifecycleOpts);
|
|
4260
|
+
log.debug(`Created feature branch ${featureBranchName} from ${featureDefaultBranch}`);
|
|
4261
|
+
registerCleanup(async () => {
|
|
4262
|
+
try {
|
|
4263
|
+
await datasource4.switchBranch(featureDefaultBranch, lifecycleOpts);
|
|
4264
|
+
} catch {
|
|
4265
|
+
}
|
|
4266
|
+
});
|
|
4267
|
+
await datasource4.switchBranch(featureDefaultBranch, lifecycleOpts);
|
|
4268
|
+
log.debug(`Switched back to ${featureDefaultBranch} for worktree creation`);
|
|
4269
|
+
} catch (err) {
|
|
4270
|
+
log.error(`Feature branch creation failed: ${log.extractMessage(err)}`);
|
|
4271
|
+
tui.state.phase = "done";
|
|
4272
|
+
tui.stop();
|
|
4273
|
+
return { total: allTasks.length, completed: 0, failed: allTasks.length, skipped: 0, results: [] };
|
|
4274
|
+
}
|
|
4275
|
+
}
|
|
3851
4276
|
let username = "";
|
|
3852
4277
|
try {
|
|
3853
4278
|
username = await datasource4.getUsername(lifecycleOpts);
|
|
@@ -3856,275 +4281,373 @@ async function runDispatchPipeline(opts, cwd) {
|
|
|
3856
4281
|
}
|
|
3857
4282
|
const processIssueFile = async (file, fileTasks) => {
|
|
3858
4283
|
const details = issueDetailsByFile.get(file);
|
|
3859
|
-
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
|
|
3863
|
-
|
|
3864
|
-
|
|
3865
|
-
|
|
3866
|
-
|
|
3867
|
-
|
|
3868
|
-
|
|
3869
|
-
|
|
3870
|
-
|
|
3871
|
-
|
|
3872
|
-
|
|
3873
|
-
|
|
3874
|
-
|
|
4284
|
+
const fileLogger = verbose && details ? new FileLogger(details.number, cwd) : null;
|
|
4285
|
+
const body = async () => {
|
|
4286
|
+
let defaultBranch;
|
|
4287
|
+
let branchName;
|
|
4288
|
+
let worktreePath;
|
|
4289
|
+
let issueCwd = cwd;
|
|
4290
|
+
if (!noBranch && details) {
|
|
4291
|
+
fileLogger?.phase("Branch/worktree setup");
|
|
4292
|
+
try {
|
|
4293
|
+
defaultBranch = feature ? featureBranchName : await datasource4.getDefaultBranch(lifecycleOpts);
|
|
4294
|
+
branchName = datasource4.buildBranchName(details.number, details.title, username);
|
|
4295
|
+
if (useWorktrees) {
|
|
4296
|
+
worktreePath = await createWorktree(cwd, file, branchName, ...feature && featureBranchName ? [featureBranchName] : []);
|
|
4297
|
+
registerCleanup(async () => {
|
|
4298
|
+
await removeWorktree(cwd, file);
|
|
4299
|
+
});
|
|
4300
|
+
issueCwd = worktreePath;
|
|
4301
|
+
log.debug(`Created worktree for issue #${details.number} at ${worktreePath}`);
|
|
4302
|
+
fileLogger?.info(`Worktree created at ${worktreePath}`);
|
|
4303
|
+
const wtName = worktreeName(file);
|
|
4304
|
+
for (const task of fileTasks) {
|
|
4305
|
+
const tuiTask = tui.state.tasks.find((t) => t.task === task);
|
|
4306
|
+
if (tuiTask) tuiTask.worktree = wtName;
|
|
4307
|
+
}
|
|
4308
|
+
} else if (datasource4.supportsGit()) {
|
|
4309
|
+
await datasource4.createAndSwitchBranch(branchName, lifecycleOpts);
|
|
4310
|
+
log.debug(`Switched to branch ${branchName}`);
|
|
4311
|
+
fileLogger?.info(`Switched to branch ${branchName}`);
|
|
4312
|
+
}
|
|
4313
|
+
} catch (err) {
|
|
4314
|
+
const errorMsg = `Branch creation failed for issue #${details.number}: ${log.extractMessage(err)}`;
|
|
4315
|
+
fileLogger?.error(`Branch creation failed: ${log.extractMessage(err)}${err instanceof Error && err.stack ? `
|
|
4316
|
+
${err.stack}` : ""}`);
|
|
4317
|
+
log.error(errorMsg);
|
|
3875
4318
|
for (const task of fileTasks) {
|
|
3876
4319
|
const tuiTask = tui.state.tasks.find((t) => t.task === task);
|
|
3877
|
-
if (tuiTask)
|
|
3878
|
-
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
|
|
3882
|
-
}
|
|
3883
|
-
} catch (err) {
|
|
3884
|
-
const errorMsg = `Branch creation failed for issue #${details.number}: ${log.extractMessage(err)}`;
|
|
3885
|
-
log.error(errorMsg);
|
|
3886
|
-
for (const task of fileTasks) {
|
|
3887
|
-
const tuiTask = tui.state.tasks.find((t) => t.task === task);
|
|
3888
|
-
if (tuiTask) {
|
|
3889
|
-
tuiTask.status = "failed";
|
|
3890
|
-
tuiTask.error = errorMsg;
|
|
4320
|
+
if (tuiTask) {
|
|
4321
|
+
tuiTask.status = "failed";
|
|
4322
|
+
tuiTask.error = errorMsg;
|
|
4323
|
+
}
|
|
4324
|
+
results.push({ task, success: false, error: errorMsg });
|
|
3891
4325
|
}
|
|
3892
|
-
|
|
4326
|
+
failed += fileTasks.length;
|
|
4327
|
+
return;
|
|
3893
4328
|
}
|
|
3894
|
-
failed += fileTasks.length;
|
|
3895
|
-
return;
|
|
3896
4329
|
}
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
tui.state.model
|
|
4330
|
+
const worktreeRoot = useWorktrees ? worktreePath : void 0;
|
|
4331
|
+
const issueLifecycleOpts = { cwd: issueCwd };
|
|
4332
|
+
fileLogger?.phase("Provider/agent boot");
|
|
4333
|
+
let localInstance;
|
|
4334
|
+
let localPlanner;
|
|
4335
|
+
let localExecutor;
|
|
4336
|
+
let localCommitAgent;
|
|
4337
|
+
if (useWorktrees) {
|
|
4338
|
+
localInstance = await bootProvider(provider, { url: serverUrl, cwd: issueCwd, model });
|
|
4339
|
+
registerCleanup(() => localInstance.cleanup());
|
|
4340
|
+
if (localInstance.model && !tui.state.model) {
|
|
4341
|
+
tui.state.model = localInstance.model;
|
|
4342
|
+
}
|
|
4343
|
+
if (verbose && localInstance.model) log.debug(`Model: ${localInstance.model}`);
|
|
4344
|
+
localPlanner = noPlan ? null : await boot6({ provider: localInstance, cwd: issueCwd });
|
|
4345
|
+
localExecutor = await boot7({ provider: localInstance, cwd: issueCwd });
|
|
4346
|
+
localCommitAgent = await boot8({ provider: localInstance, cwd: issueCwd });
|
|
4347
|
+
fileLogger?.info(`Provider booted: ${localInstance.model ?? provider}`);
|
|
4348
|
+
} else {
|
|
4349
|
+
localInstance = instance;
|
|
4350
|
+
localPlanner = planner;
|
|
4351
|
+
localExecutor = executor;
|
|
4352
|
+
localCommitAgent = commitAgent;
|
|
3908
4353
|
}
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
|
|
3919
|
-
|
|
3920
|
-
|
|
3921
|
-
|
|
3922
|
-
|
|
3923
|
-
|
|
3924
|
-
|
|
3925
|
-
|
|
3926
|
-
|
|
3927
|
-
|
|
3928
|
-
|
|
3929
|
-
|
|
3930
|
-
|
|
3931
|
-
|
|
3932
|
-
|
|
3933
|
-
|
|
3934
|
-
const rawContent = fileContentMap.get(task.file);
|
|
3935
|
-
const fileContext = rawContent ? buildTaskContext(rawContent, task) : void 0;
|
|
3936
|
-
let planResult;
|
|
3937
|
-
for (let attempt = 1; attempt <= maxPlanAttempts; attempt++) {
|
|
3938
|
-
try {
|
|
3939
|
-
planResult = await withTimeout(
|
|
3940
|
-
localPlanner.plan(task, fileContext, issueCwd),
|
|
3941
|
-
planTimeoutMs,
|
|
3942
|
-
"planner.plan()"
|
|
3943
|
-
);
|
|
3944
|
-
break;
|
|
3945
|
-
} catch (err) {
|
|
3946
|
-
if (err instanceof TimeoutError) {
|
|
3947
|
-
log.warn(
|
|
3948
|
-
`Planning timed out for task "${task.text}" (attempt ${attempt}/${maxPlanAttempts})`
|
|
4354
|
+
const groups = groupTasksByMode(fileTasks);
|
|
4355
|
+
const issueResults = [];
|
|
4356
|
+
for (const group of groups) {
|
|
4357
|
+
const groupQueue = [...group];
|
|
4358
|
+
while (groupQueue.length > 0) {
|
|
4359
|
+
const batch = groupQueue.splice(0, concurrency);
|
|
4360
|
+
const batchResults = await Promise.all(
|
|
4361
|
+
batch.map(async (task) => {
|
|
4362
|
+
const tuiTask = tui.state.tasks.find((t) => t.task === task);
|
|
4363
|
+
const startTime = Date.now();
|
|
4364
|
+
tuiTask.elapsed = startTime;
|
|
4365
|
+
let plan;
|
|
4366
|
+
if (localPlanner) {
|
|
4367
|
+
tuiTask.status = "planning";
|
|
4368
|
+
fileLogger?.phase(`Planning task: ${task.text}`);
|
|
4369
|
+
if (verbose) log.info(`Task #${tui.state.tasks.indexOf(tuiTask) + 1}: planning \u2014 "${task.text}"`);
|
|
4370
|
+
const rawContent = fileContentMap.get(task.file);
|
|
4371
|
+
const fileContext = rawContent ? buildTaskContext(rawContent, task) : void 0;
|
|
4372
|
+
let planResult;
|
|
4373
|
+
for (let attempt = 1; attempt <= maxPlanAttempts; attempt++) {
|
|
4374
|
+
try {
|
|
4375
|
+
planResult = await withTimeout(
|
|
4376
|
+
localPlanner.plan(task, fileContext, issueCwd, worktreeRoot),
|
|
4377
|
+
planTimeoutMs,
|
|
4378
|
+
"planner.plan()"
|
|
3949
4379
|
);
|
|
3950
|
-
if (attempt < maxPlanAttempts) {
|
|
3951
|
-
log.debug(`Retrying planning (attempt ${attempt + 1}/${maxPlanAttempts})`);
|
|
3952
|
-
}
|
|
3953
|
-
} else {
|
|
3954
|
-
planResult = {
|
|
3955
|
-
prompt: "",
|
|
3956
|
-
success: false,
|
|
3957
|
-
error: log.extractMessage(err)
|
|
3958
|
-
};
|
|
3959
4380
|
break;
|
|
4381
|
+
} catch (err) {
|
|
4382
|
+
if (err instanceof TimeoutError) {
|
|
4383
|
+
log.warn(
|
|
4384
|
+
`Planning timed out for task "${task.text}" (attempt ${attempt}/${maxPlanAttempts})`
|
|
4385
|
+
);
|
|
4386
|
+
fileLogger?.warn(`Planning timeout (attempt ${attempt}/${maxPlanAttempts})`);
|
|
4387
|
+
if (attempt < maxPlanAttempts) {
|
|
4388
|
+
log.debug(`Retrying planning (attempt ${attempt + 1}/${maxPlanAttempts})`);
|
|
4389
|
+
fileLogger?.info(`Retrying planning (attempt ${attempt + 1}/${maxPlanAttempts})`);
|
|
4390
|
+
}
|
|
4391
|
+
} else {
|
|
4392
|
+
planResult = {
|
|
4393
|
+
data: null,
|
|
4394
|
+
success: false,
|
|
4395
|
+
error: log.extractMessage(err),
|
|
4396
|
+
durationMs: 0
|
|
4397
|
+
};
|
|
4398
|
+
break;
|
|
4399
|
+
}
|
|
3960
4400
|
}
|
|
3961
4401
|
}
|
|
4402
|
+
if (!planResult) {
|
|
4403
|
+
const timeoutMin = planTimeout ?? 10;
|
|
4404
|
+
planResult = {
|
|
4405
|
+
data: null,
|
|
4406
|
+
success: false,
|
|
4407
|
+
error: `Planning timed out after ${timeoutMin}m (${maxPlanAttempts} attempts)`,
|
|
4408
|
+
durationMs: 0
|
|
4409
|
+
};
|
|
4410
|
+
}
|
|
4411
|
+
if (!planResult.success) {
|
|
4412
|
+
tuiTask.status = "failed";
|
|
4413
|
+
tuiTask.error = `Planning failed: ${planResult.error}`;
|
|
4414
|
+
fileLogger?.error(`Planning failed: ${planResult.error}`);
|
|
4415
|
+
tuiTask.elapsed = Date.now() - startTime;
|
|
4416
|
+
if (verbose) log.error(`Task #${tui.state.tasks.indexOf(tuiTask) + 1}: failed \u2014 ${tuiTask.error} (${elapsed(tuiTask.elapsed)})`);
|
|
4417
|
+
failed++;
|
|
4418
|
+
return { task, success: false, error: tuiTask.error };
|
|
4419
|
+
}
|
|
4420
|
+
plan = planResult.data.prompt;
|
|
4421
|
+
fileLogger?.info(`Planning completed (${planResult.durationMs ?? 0}ms)`);
|
|
3962
4422
|
}
|
|
3963
|
-
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
|
|
3968
|
-
|
|
3969
|
-
|
|
3970
|
-
|
|
3971
|
-
|
|
4423
|
+
tuiTask.status = "running";
|
|
4424
|
+
fileLogger?.phase(`Executing task: ${task.text}`);
|
|
4425
|
+
if (verbose) log.info(`Task #${tui.state.tasks.indexOf(tuiTask) + 1}: executing \u2014 "${task.text}"`);
|
|
4426
|
+
const execRetries = 2;
|
|
4427
|
+
const execResult = await withRetry(
|
|
4428
|
+
async () => {
|
|
4429
|
+
const result = await localExecutor.execute({
|
|
4430
|
+
task,
|
|
4431
|
+
cwd: issueCwd,
|
|
4432
|
+
plan: plan ?? null,
|
|
4433
|
+
worktreeRoot
|
|
4434
|
+
});
|
|
4435
|
+
if (!result.success) {
|
|
4436
|
+
throw new Error(result.error ?? "Execution failed");
|
|
4437
|
+
}
|
|
4438
|
+
return result;
|
|
4439
|
+
},
|
|
4440
|
+
execRetries,
|
|
4441
|
+
{ label: `executor "${task.text}"` }
|
|
4442
|
+
).catch((err) => ({
|
|
4443
|
+
data: null,
|
|
4444
|
+
success: false,
|
|
4445
|
+
error: log.extractMessage(err),
|
|
4446
|
+
durationMs: 0
|
|
4447
|
+
}));
|
|
4448
|
+
if (execResult.success) {
|
|
4449
|
+
fileLogger?.info(`Execution completed successfully (${Date.now() - startTime}ms)`);
|
|
4450
|
+
try {
|
|
4451
|
+
const parsed = parseIssueFilename(task.file);
|
|
4452
|
+
if (parsed) {
|
|
4453
|
+
const updatedContent = await readFile7(task.file, "utf-8");
|
|
4454
|
+
const issueDetails = issueDetailsByFile.get(task.file);
|
|
4455
|
+
const title = issueDetails?.title ?? parsed.slug;
|
|
4456
|
+
await datasource4.update(parsed.issueId, title, updatedContent, fetchOpts);
|
|
4457
|
+
log.success(`Synced task completion to issue #${parsed.issueId}`);
|
|
4458
|
+
}
|
|
4459
|
+
} catch (err) {
|
|
4460
|
+
log.warn(`Could not sync task completion to datasource: ${log.formatErrorChain(err)}`);
|
|
4461
|
+
}
|
|
4462
|
+
tuiTask.status = "done";
|
|
4463
|
+
tuiTask.elapsed = Date.now() - startTime;
|
|
4464
|
+
if (verbose) log.success(`Task #${tui.state.tasks.indexOf(tuiTask) + 1}: done \u2014 "${task.text}" (${elapsed(tuiTask.elapsed)})`);
|
|
4465
|
+
completed++;
|
|
4466
|
+
} else {
|
|
4467
|
+
fileLogger?.error(`Execution failed: ${execResult.error}`);
|
|
3972
4468
|
tuiTask.status = "failed";
|
|
3973
|
-
tuiTask.error =
|
|
4469
|
+
tuiTask.error = execResult.error;
|
|
3974
4470
|
tuiTask.elapsed = Date.now() - startTime;
|
|
3975
|
-
if (verbose) log.error(`Task #${tui.state.tasks.indexOf(tuiTask) + 1}: failed \u2014 ${
|
|
4471
|
+
if (verbose) log.error(`Task #${tui.state.tasks.indexOf(tuiTask) + 1}: failed \u2014 "${task.text}" (${elapsed(tuiTask.elapsed)})${tuiTask.error ? `: ${tuiTask.error}` : ""}`);
|
|
3976
4472
|
failed++;
|
|
3977
|
-
return { task, success: false, error: tuiTask.error };
|
|
3978
4473
|
}
|
|
3979
|
-
|
|
3980
|
-
|
|
3981
|
-
|
|
3982
|
-
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
|
|
3986
|
-
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
|
|
3998
|
-
|
|
3999
|
-
|
|
4000
|
-
|
|
4001
|
-
|
|
4002
|
-
|
|
4003
|
-
|
|
4004
|
-
|
|
4474
|
+
const dispatchResult = execResult.success ? execResult.data.dispatchResult : {
|
|
4475
|
+
task,
|
|
4476
|
+
success: false,
|
|
4477
|
+
error: execResult.error ?? "Executor failed without returning a dispatch result."
|
|
4478
|
+
};
|
|
4479
|
+
return dispatchResult;
|
|
4480
|
+
})
|
|
4481
|
+
);
|
|
4482
|
+
issueResults.push(...batchResults);
|
|
4483
|
+
if (!tui.state.model && localInstance.model) {
|
|
4484
|
+
tui.state.model = localInstance.model;
|
|
4485
|
+
}
|
|
4486
|
+
}
|
|
4487
|
+
}
|
|
4488
|
+
results.push(...issueResults);
|
|
4489
|
+
if (!noBranch && branchName && defaultBranch && details && datasource4.supportsGit()) {
|
|
4490
|
+
try {
|
|
4491
|
+
await datasource4.commitAllChanges(
|
|
4492
|
+
`chore: stage uncommitted changes for issue #${details.number}`,
|
|
4493
|
+
issueLifecycleOpts
|
|
4494
|
+
);
|
|
4495
|
+
log.debug(`Staged uncommitted changes for issue #${details.number}`);
|
|
4496
|
+
} catch (err) {
|
|
4497
|
+
log.warn(`Could not commit uncommitted changes for issue #${details.number}: ${log.formatErrorChain(err)}`);
|
|
4498
|
+
}
|
|
4499
|
+
}
|
|
4500
|
+
fileLogger?.phase("Commit generation");
|
|
4501
|
+
let commitAgentResult;
|
|
4502
|
+
if (!noBranch && branchName && defaultBranch && details && datasource4.supportsGit()) {
|
|
4503
|
+
try {
|
|
4504
|
+
const branchDiff = await getBranchDiff(defaultBranch, issueCwd);
|
|
4505
|
+
if (branchDiff) {
|
|
4506
|
+
const result = await localCommitAgent.generate({
|
|
4507
|
+
branchDiff,
|
|
4508
|
+
issue: details,
|
|
4509
|
+
taskResults: issueResults,
|
|
4510
|
+
cwd: issueCwd,
|
|
4511
|
+
worktreeRoot
|
|
4512
|
+
});
|
|
4513
|
+
if (result.success) {
|
|
4514
|
+
commitAgentResult = result;
|
|
4515
|
+
fileLogger?.info(`Commit message generated for issue #${details.number}`);
|
|
4005
4516
|
try {
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
const issueDetails = issueDetailsByFile.get(task.file);
|
|
4010
|
-
const title = issueDetails?.title ?? parsed.slug;
|
|
4011
|
-
await datasource4.update(parsed.issueId, title, updatedContent, fetchOpts);
|
|
4012
|
-
log.success(`Synced task completion to issue #${parsed.issueId}`);
|
|
4013
|
-
}
|
|
4517
|
+
await squashBranchCommits(defaultBranch, result.commitMessage, issueCwd);
|
|
4518
|
+
log.debug(`Rewrote commit message for issue #${details.number}`);
|
|
4519
|
+
fileLogger?.info(`Rewrote commit history for issue #${details.number}`);
|
|
4014
4520
|
} catch (err) {
|
|
4015
|
-
log.warn(`Could not
|
|
4521
|
+
log.warn(`Could not rewrite commit message for issue #${details.number}: ${log.formatErrorChain(err)}`);
|
|
4016
4522
|
}
|
|
4017
|
-
tuiTask.status = "done";
|
|
4018
|
-
tuiTask.elapsed = Date.now() - startTime;
|
|
4019
|
-
if (verbose) log.success(`Task #${tui.state.tasks.indexOf(tuiTask) + 1}: done \u2014 "${task.text}" (${elapsed(tuiTask.elapsed)})`);
|
|
4020
|
-
completed++;
|
|
4021
4523
|
} else {
|
|
4022
|
-
|
|
4023
|
-
|
|
4024
|
-
tuiTask.elapsed = Date.now() - startTime;
|
|
4025
|
-
if (verbose) log.error(`Task #${tui.state.tasks.indexOf(tuiTask) + 1}: failed \u2014 "${task.text}" (${elapsed(tuiTask.elapsed)})${tuiTask.error ? `: ${tuiTask.error}` : ""}`);
|
|
4026
|
-
failed++;
|
|
4524
|
+
log.warn(`Commit agent failed for issue #${details.number}: ${result.error}`);
|
|
4525
|
+
fileLogger?.warn(`Commit agent failed: ${result.error}`);
|
|
4027
4526
|
}
|
|
4028
|
-
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
issueResults.push(...batchResults);
|
|
4032
|
-
if (!tui.state.model && localInstance.model) {
|
|
4033
|
-
tui.state.model = localInstance.model;
|
|
4527
|
+
}
|
|
4528
|
+
} catch (err) {
|
|
4529
|
+
log.warn(`Commit agent error for issue #${details.number}: ${log.formatErrorChain(err)}`);
|
|
4034
4530
|
}
|
|
4035
4531
|
}
|
|
4036
|
-
|
|
4037
|
-
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
await datasource4.commitAllChanges(
|
|
4041
|
-
`chore: stage uncommitted changes for issue #${details.number}`,
|
|
4042
|
-
issueLifecycleOpts
|
|
4043
|
-
);
|
|
4044
|
-
log.debug(`Staged uncommitted changes for issue #${details.number}`);
|
|
4045
|
-
} catch (err) {
|
|
4046
|
-
log.warn(`Could not commit uncommitted changes for issue #${details.number}: ${log.formatErrorChain(err)}`);
|
|
4047
|
-
}
|
|
4048
|
-
}
|
|
4049
|
-
let commitAgentResult;
|
|
4050
|
-
if (!noBranch && branchName && defaultBranch && details) {
|
|
4051
|
-
try {
|
|
4052
|
-
const branchDiff = await getBranchDiff(defaultBranch, issueCwd);
|
|
4053
|
-
if (branchDiff) {
|
|
4054
|
-
const result = await localCommitAgent.generate({
|
|
4055
|
-
branchDiff,
|
|
4056
|
-
issue: details,
|
|
4057
|
-
taskResults: issueResults,
|
|
4058
|
-
cwd: issueCwd
|
|
4059
|
-
});
|
|
4060
|
-
if (result.success) {
|
|
4061
|
-
commitAgentResult = result;
|
|
4532
|
+
fileLogger?.phase("PR lifecycle");
|
|
4533
|
+
if (!noBranch && branchName && defaultBranch && details) {
|
|
4534
|
+
if (feature && featureBranchName) {
|
|
4535
|
+
if (worktreePath) {
|
|
4062
4536
|
try {
|
|
4063
|
-
await
|
|
4064
|
-
log.debug(`Rewrote commit message for issue #${details.number}`);
|
|
4537
|
+
await removeWorktree(cwd, file);
|
|
4065
4538
|
} catch (err) {
|
|
4066
|
-
log.warn(`Could not
|
|
4539
|
+
log.warn(`Could not remove worktree for issue #${details.number}: ${log.formatErrorChain(err)}`);
|
|
4540
|
+
}
|
|
4541
|
+
}
|
|
4542
|
+
try {
|
|
4543
|
+
await datasource4.switchBranch(featureBranchName, lifecycleOpts);
|
|
4544
|
+
await exec9("git", ["merge", branchName, "--no-ff", "-m", `merge: issue #${details.number}`], { cwd });
|
|
4545
|
+
log.debug(`Merged ${branchName} into ${featureBranchName}`);
|
|
4546
|
+
} catch (err) {
|
|
4547
|
+
const mergeError = `Could not merge ${branchName} into feature branch: ${log.formatErrorChain(err)}`;
|
|
4548
|
+
log.warn(mergeError);
|
|
4549
|
+
try {
|
|
4550
|
+
await exec9("git", ["merge", "--abort"], { cwd });
|
|
4551
|
+
} catch {
|
|
4552
|
+
}
|
|
4553
|
+
for (const task of fileTasks) {
|
|
4554
|
+
const tuiTask = tui.state.tasks.find((t) => t.task === task);
|
|
4555
|
+
if (tuiTask) {
|
|
4556
|
+
tuiTask.status = "failed";
|
|
4557
|
+
tuiTask.error = mergeError;
|
|
4558
|
+
}
|
|
4559
|
+
const existingResult = results.find((r) => r.task === task);
|
|
4560
|
+
if (existingResult) {
|
|
4561
|
+
existingResult.success = false;
|
|
4562
|
+
existingResult.error = mergeError;
|
|
4563
|
+
}
|
|
4564
|
+
}
|
|
4565
|
+
return;
|
|
4566
|
+
}
|
|
4567
|
+
try {
|
|
4568
|
+
await exec9("git", ["branch", "-d", branchName], { cwd });
|
|
4569
|
+
log.debug(`Deleted local branch ${branchName}`);
|
|
4570
|
+
} catch (err) {
|
|
4571
|
+
log.warn(`Could not delete local branch ${branchName}: ${log.formatErrorChain(err)}`);
|
|
4572
|
+
}
|
|
4573
|
+
try {
|
|
4574
|
+
await datasource4.switchBranch(featureDefaultBranch, lifecycleOpts);
|
|
4575
|
+
} catch (err) {
|
|
4576
|
+
log.warn(`Could not switch back to ${featureDefaultBranch}: ${log.formatErrorChain(err)}`);
|
|
4577
|
+
}
|
|
4578
|
+
} else {
|
|
4579
|
+
if (datasource4.supportsGit()) {
|
|
4580
|
+
try {
|
|
4581
|
+
await datasource4.pushBranch(branchName, issueLifecycleOpts);
|
|
4582
|
+
log.debug(`Pushed branch ${branchName}`);
|
|
4583
|
+
fileLogger?.info(`Pushed branch ${branchName}`);
|
|
4584
|
+
} catch (err) {
|
|
4585
|
+
log.warn(`Could not push branch ${branchName}: ${log.formatErrorChain(err)}`);
|
|
4586
|
+
}
|
|
4587
|
+
}
|
|
4588
|
+
if (datasource4.supportsGit()) {
|
|
4589
|
+
try {
|
|
4590
|
+
const prTitle = commitAgentResult?.prTitle || await buildPrTitle(details.title, defaultBranch, issueLifecycleOpts.cwd);
|
|
4591
|
+
const prBody = commitAgentResult?.prDescription || await buildPrBody(
|
|
4592
|
+
details,
|
|
4593
|
+
fileTasks,
|
|
4594
|
+
issueResults,
|
|
4595
|
+
defaultBranch,
|
|
4596
|
+
datasource4.name,
|
|
4597
|
+
issueLifecycleOpts.cwd
|
|
4598
|
+
);
|
|
4599
|
+
const prUrl = await datasource4.createPullRequest(
|
|
4600
|
+
branchName,
|
|
4601
|
+
details.number,
|
|
4602
|
+
prTitle,
|
|
4603
|
+
prBody,
|
|
4604
|
+
issueLifecycleOpts
|
|
4605
|
+
);
|
|
4606
|
+
if (prUrl) {
|
|
4607
|
+
log.success(`Created PR for issue #${details.number}: ${prUrl}`);
|
|
4608
|
+
fileLogger?.info(`Created PR: ${prUrl}`);
|
|
4609
|
+
}
|
|
4610
|
+
} catch (err) {
|
|
4611
|
+
log.warn(`Could not create PR for issue #${details.number}: ${log.formatErrorChain(err)}`);
|
|
4612
|
+
fileLogger?.warn(`PR creation failed: ${log.extractMessage(err)}`);
|
|
4613
|
+
}
|
|
4614
|
+
}
|
|
4615
|
+
if (useWorktrees && worktreePath) {
|
|
4616
|
+
try {
|
|
4617
|
+
await removeWorktree(cwd, file);
|
|
4618
|
+
} catch (err) {
|
|
4619
|
+
log.warn(`Could not remove worktree for issue #${details.number}: ${log.formatErrorChain(err)}`);
|
|
4620
|
+
}
|
|
4621
|
+
} else if (!useWorktrees && datasource4.supportsGit()) {
|
|
4622
|
+
try {
|
|
4623
|
+
await datasource4.switchBranch(defaultBranch, lifecycleOpts);
|
|
4624
|
+
log.debug(`Switched back to ${defaultBranch}`);
|
|
4625
|
+
} catch (err) {
|
|
4626
|
+
log.warn(`Could not switch back to ${defaultBranch}: ${log.formatErrorChain(err)}`);
|
|
4067
4627
|
}
|
|
4068
|
-
} else {
|
|
4069
|
-
log.warn(`Commit agent failed for issue #${details.number}: ${result.error}`);
|
|
4070
4628
|
}
|
|
4071
4629
|
}
|
|
4072
|
-
} catch (err) {
|
|
4073
|
-
log.warn(`Commit agent error for issue #${details.number}: ${log.formatErrorChain(err)}`);
|
|
4074
|
-
}
|
|
4075
|
-
}
|
|
4076
|
-
if (!noBranch && branchName && defaultBranch && details) {
|
|
4077
|
-
try {
|
|
4078
|
-
await datasource4.pushBranch(branchName, issueLifecycleOpts);
|
|
4079
|
-
log.debug(`Pushed branch ${branchName}`);
|
|
4080
|
-
} catch (err) {
|
|
4081
|
-
log.warn(`Could not push branch ${branchName}: ${log.formatErrorChain(err)}`);
|
|
4082
4630
|
}
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
issueResults,
|
|
4089
|
-
defaultBranch,
|
|
4090
|
-
datasource4.name,
|
|
4091
|
-
issueLifecycleOpts.cwd
|
|
4092
|
-
);
|
|
4093
|
-
const prUrl = await datasource4.createPullRequest(
|
|
4094
|
-
branchName,
|
|
4095
|
-
details.number,
|
|
4096
|
-
prTitle,
|
|
4097
|
-
prBody,
|
|
4098
|
-
issueLifecycleOpts
|
|
4099
|
-
);
|
|
4100
|
-
if (prUrl) {
|
|
4101
|
-
log.success(`Created PR for issue #${details.number}: ${prUrl}`);
|
|
4102
|
-
}
|
|
4103
|
-
} catch (err) {
|
|
4104
|
-
log.warn(`Could not create PR for issue #${details.number}: ${log.formatErrorChain(err)}`);
|
|
4631
|
+
fileLogger?.phase("Resource cleanup");
|
|
4632
|
+
if (useWorktrees) {
|
|
4633
|
+
await localExecutor.cleanup();
|
|
4634
|
+
await localPlanner?.cleanup();
|
|
4635
|
+
await localInstance.cleanup();
|
|
4105
4636
|
}
|
|
4106
|
-
|
|
4107
|
-
|
|
4108
|
-
|
|
4109
|
-
} catch (err) {
|
|
4110
|
-
log.warn(`Could not remove worktree for issue #${details.number}: ${log.formatErrorChain(err)}`);
|
|
4111
|
-
}
|
|
4112
|
-
} else if (!useWorktrees) {
|
|
4637
|
+
};
|
|
4638
|
+
if (fileLogger) {
|
|
4639
|
+
await fileLoggerStorage.run(fileLogger, async () => {
|
|
4113
4640
|
try {
|
|
4114
|
-
await
|
|
4115
|
-
|
|
4116
|
-
|
|
4117
|
-
log.warn(`Could not switch back to ${defaultBranch}: ${log.formatErrorChain(err)}`);
|
|
4641
|
+
await body();
|
|
4642
|
+
} finally {
|
|
4643
|
+
fileLogger.close();
|
|
4118
4644
|
}
|
|
4119
|
-
}
|
|
4120
|
-
}
|
|
4121
|
-
|
|
4122
|
-
await localExecutor.cleanup();
|
|
4123
|
-
await localPlanner?.cleanup();
|
|
4124
|
-
await localInstance.cleanup();
|
|
4645
|
+
});
|
|
4646
|
+
} else {
|
|
4647
|
+
await body();
|
|
4125
4648
|
}
|
|
4126
4649
|
};
|
|
4127
|
-
if (useWorktrees) {
|
|
4650
|
+
if (useWorktrees && !feature) {
|
|
4128
4651
|
await Promise.all(
|
|
4129
4652
|
Array.from(tasksByFile).map(
|
|
4130
4653
|
([file, fileTasks]) => processIssueFile(file, fileTasks)
|
|
@@ -4135,10 +4658,42 @@ async function runDispatchPipeline(opts, cwd) {
|
|
|
4135
4658
|
await processIssueFile(file, fileTasks);
|
|
4136
4659
|
}
|
|
4137
4660
|
}
|
|
4138
|
-
|
|
4139
|
-
|
|
4140
|
-
|
|
4141
|
-
|
|
4661
|
+
if (feature && featureBranchName && featureDefaultBranch) {
|
|
4662
|
+
try {
|
|
4663
|
+
await datasource4.switchBranch(featureBranchName, lifecycleOpts);
|
|
4664
|
+
log.debug(`Switched to feature branch ${featureBranchName}`);
|
|
4665
|
+
} catch (err) {
|
|
4666
|
+
log.warn(`Could not switch to feature branch: ${log.formatErrorChain(err)}`);
|
|
4667
|
+
}
|
|
4668
|
+
try {
|
|
4669
|
+
await datasource4.pushBranch(featureBranchName, lifecycleOpts);
|
|
4670
|
+
log.debug(`Pushed feature branch ${featureBranchName}`);
|
|
4671
|
+
} catch (err) {
|
|
4672
|
+
log.warn(`Could not push feature branch: ${log.formatErrorChain(err)}`);
|
|
4673
|
+
}
|
|
4674
|
+
try {
|
|
4675
|
+
const allIssueDetails = Array.from(issueDetailsByFile.values());
|
|
4676
|
+
const prTitle = buildFeaturePrTitle(featureBranchName, allIssueDetails);
|
|
4677
|
+
const prBody = buildFeaturePrBody(allIssueDetails, allTasks, results, source);
|
|
4678
|
+
const primaryIssue = allIssueDetails[0]?.number ?? "";
|
|
4679
|
+
const prUrl = await datasource4.createPullRequest(
|
|
4680
|
+
featureBranchName,
|
|
4681
|
+
primaryIssue,
|
|
4682
|
+
prTitle,
|
|
4683
|
+
prBody,
|
|
4684
|
+
lifecycleOpts
|
|
4685
|
+
);
|
|
4686
|
+
if (prUrl) {
|
|
4687
|
+
log.success(`Created feature PR: ${prUrl}`);
|
|
4688
|
+
}
|
|
4689
|
+
} catch (err) {
|
|
4690
|
+
log.warn(`Could not create feature PR: ${log.formatErrorChain(err)}`);
|
|
4691
|
+
}
|
|
4692
|
+
try {
|
|
4693
|
+
await datasource4.switchBranch(featureDefaultBranch, lifecycleOpts);
|
|
4694
|
+
} catch (err) {
|
|
4695
|
+
log.warn(`Could not switch back to ${featureDefaultBranch}: ${log.formatErrorChain(err)}`);
|
|
4696
|
+
}
|
|
4142
4697
|
}
|
|
4143
4698
|
await commitAgent?.cleanup();
|
|
4144
4699
|
await executor?.cleanup();
|
|
@@ -4153,13 +4708,13 @@ async function runDispatchPipeline(opts, cwd) {
|
|
|
4153
4708
|
throw err;
|
|
4154
4709
|
}
|
|
4155
4710
|
}
|
|
4156
|
-
async function dryRunMode(issueIds, cwd, source, org, project, workItemType) {
|
|
4711
|
+
async function dryRunMode(issueIds, cwd, source, org, project, workItemType, iteration, area) {
|
|
4157
4712
|
if (!source) {
|
|
4158
4713
|
log.error("No datasource configured. Use --source or run 'dispatch config' to set up defaults.");
|
|
4159
4714
|
return { total: 0, completed: 0, failed: 0, skipped: 0, results: [] };
|
|
4160
4715
|
}
|
|
4161
4716
|
const datasource4 = getDatasource(source);
|
|
4162
|
-
const fetchOpts = { cwd, org, project, workItemType };
|
|
4717
|
+
const fetchOpts = { cwd, org, project, workItemType, iteration, area };
|
|
4163
4718
|
const lifecycleOpts = { cwd };
|
|
4164
4719
|
let username = "";
|
|
4165
4720
|
try {
|
|
@@ -4233,12 +4788,17 @@ async function boot9(opts) {
|
|
|
4233
4788
|
const modeFlags = [
|
|
4234
4789
|
m.spec !== void 0 && "--spec",
|
|
4235
4790
|
m.respec !== void 0 && "--respec",
|
|
4236
|
-
m.fixTests && "--fix-tests"
|
|
4791
|
+
m.fixTests && "--fix-tests",
|
|
4792
|
+
m.feature && "--feature"
|
|
4237
4793
|
].filter(Boolean);
|
|
4238
4794
|
if (modeFlags.length > 1) {
|
|
4239
4795
|
log.error(`${modeFlags.join(" and ")} are mutually exclusive`);
|
|
4240
4796
|
process.exit(1);
|
|
4241
4797
|
}
|
|
4798
|
+
if (m.feature && m.noBranch) {
|
|
4799
|
+
log.error("--feature and --no-branch are mutually exclusive");
|
|
4800
|
+
process.exit(1);
|
|
4801
|
+
}
|
|
4242
4802
|
if (m.fixTests && m.issueIds.length > 0) {
|
|
4243
4803
|
log.error("--fix-tests cannot be combined with issue IDs");
|
|
4244
4804
|
process.exit(1);
|
|
@@ -4259,6 +4819,8 @@ async function boot9(opts) {
|
|
|
4259
4819
|
org: m.org,
|
|
4260
4820
|
project: m.project,
|
|
4261
4821
|
workItemType: m.workItemType,
|
|
4822
|
+
iteration: m.iteration,
|
|
4823
|
+
area: m.area,
|
|
4262
4824
|
concurrency: m.concurrency,
|
|
4263
4825
|
dryRun: m.dryRun
|
|
4264
4826
|
});
|
|
@@ -4273,7 +4835,7 @@ async function boot9(opts) {
|
|
|
4273
4835
|
process.exit(1);
|
|
4274
4836
|
}
|
|
4275
4837
|
const datasource4 = getDatasource(source);
|
|
4276
|
-
const existing = await datasource4.list({ cwd: m.cwd, org: m.org, project: m.project, workItemType: m.workItemType });
|
|
4838
|
+
const existing = await datasource4.list({ cwd: m.cwd, org: m.org, project: m.project, workItemType: m.workItemType, iteration: m.iteration, area: m.area });
|
|
4277
4839
|
if (existing.length === 0) {
|
|
4278
4840
|
log.error("No existing specs found to regenerate");
|
|
4279
4841
|
process.exit(1);
|
|
@@ -4299,6 +4861,8 @@ async function boot9(opts) {
|
|
|
4299
4861
|
org: m.org,
|
|
4300
4862
|
project: m.project,
|
|
4301
4863
|
workItemType: m.workItemType,
|
|
4864
|
+
iteration: m.iteration,
|
|
4865
|
+
area: m.area,
|
|
4302
4866
|
concurrency: m.concurrency,
|
|
4303
4867
|
dryRun: m.dryRun
|
|
4304
4868
|
});
|
|
@@ -4317,10 +4881,13 @@ async function boot9(opts) {
|
|
|
4317
4881
|
org: m.org,
|
|
4318
4882
|
project: m.project,
|
|
4319
4883
|
workItemType: m.workItemType,
|
|
4884
|
+
iteration: m.iteration,
|
|
4885
|
+
area: m.area,
|
|
4320
4886
|
planTimeout: m.planTimeout,
|
|
4321
4887
|
planRetries: m.planRetries,
|
|
4322
4888
|
retries: m.retries,
|
|
4323
|
-
force: m.force
|
|
4889
|
+
force: m.force,
|
|
4890
|
+
feature: m.feature
|
|
4324
4891
|
});
|
|
4325
4892
|
}
|
|
4326
4893
|
};
|
|
@@ -4331,7 +4898,7 @@ async function boot9(opts) {
|
|
|
4331
4898
|
init_logger();
|
|
4332
4899
|
init_cleanup();
|
|
4333
4900
|
init_providers();
|
|
4334
|
-
var MAX_CONCURRENCY =
|
|
4901
|
+
var MAX_CONCURRENCY = CONFIG_BOUNDS.concurrency.max;
|
|
4335
4902
|
var HELP = `
|
|
4336
4903
|
dispatch \u2014 AI agent orchestration CLI
|
|
4337
4904
|
|
|
@@ -4350,8 +4917,9 @@ var HELP = `
|
|
|
4350
4917
|
--no-plan Skip the planner agent, dispatch directly
|
|
4351
4918
|
--no-branch Skip branch creation, push, and PR lifecycle
|
|
4352
4919
|
--no-worktree Skip git worktree isolation for parallel issues
|
|
4920
|
+
--feature Group issues into a single feature branch and PR
|
|
4353
4921
|
--force Ignore prior run state and re-run all tasks
|
|
4354
|
-
--concurrency <n> Max parallel dispatches (default: min(cpus, freeMB/500), max:
|
|
4922
|
+
--concurrency <n> Max parallel dispatches (default: min(cpus, freeMB/500), max: ${MAX_CONCURRENCY})
|
|
4355
4923
|
--provider <name> Agent backend: ${PROVIDER_NAMES.join(", ")} (default: opencode)
|
|
4356
4924
|
--server-url <url> URL of a running provider server
|
|
4357
4925
|
--plan-timeout <min> Planning timeout in minutes (default: 10)
|
|
@@ -4398,180 +4966,156 @@ var HELP = `
|
|
|
4398
4966
|
dispatch config
|
|
4399
4967
|
`.trimStart();
|
|
4400
4968
|
function parseArgs(argv) {
|
|
4969
|
+
const program = new Command();
|
|
4970
|
+
program.exitOverride().configureOutput({
|
|
4971
|
+
writeOut: () => {
|
|
4972
|
+
},
|
|
4973
|
+
writeErr: () => {
|
|
4974
|
+
}
|
|
4975
|
+
}).helpOption(false).argument("[issueIds...]").option("-h, --help", "Show help").option("-v, --version", "Show version").option("--dry-run", "List tasks without dispatching").option("--no-plan", "Skip the planner agent").option("--no-branch", "Skip branch creation").option("--no-worktree", "Skip git worktree isolation").option("--feature", "Group issues into a single feature branch").option("--force", "Ignore prior run state").option("--verbose", "Show detailed debug output").option("--fix-tests", "Run tests and fix failures").option("--spec <values...>", "Spec mode: issue numbers, glob, or text").option("--respec [values...]", "Regenerate specs").addOption(
|
|
4976
|
+
new Option("--provider <name>", "Agent backend").choices(PROVIDER_NAMES)
|
|
4977
|
+
).addOption(
|
|
4978
|
+
new Option("--source <name>", "Issue source").choices(
|
|
4979
|
+
DATASOURCE_NAMES
|
|
4980
|
+
)
|
|
4981
|
+
).option(
|
|
4982
|
+
"--concurrency <n>",
|
|
4983
|
+
"Max parallel dispatches",
|
|
4984
|
+
(val) => {
|
|
4985
|
+
const n = parseInt(val, 10);
|
|
4986
|
+
if (isNaN(n) || n < 1) throw new CommanderError(1, "commander.invalidArgument", "--concurrency must be a positive integer");
|
|
4987
|
+
if (n > MAX_CONCURRENCY) throw new CommanderError(1, "commander.invalidArgument", `--concurrency must not exceed ${MAX_CONCURRENCY}`);
|
|
4988
|
+
return n;
|
|
4989
|
+
}
|
|
4990
|
+
).option(
|
|
4991
|
+
"--plan-timeout <min>",
|
|
4992
|
+
"Planning timeout in minutes",
|
|
4993
|
+
(val) => {
|
|
4994
|
+
const n = parseFloat(val);
|
|
4995
|
+
if (isNaN(n) || n < CONFIG_BOUNDS.planTimeout.min) throw new CommanderError(1, "commander.invalidArgument", "--plan-timeout must be a positive number (minutes)");
|
|
4996
|
+
if (n > CONFIG_BOUNDS.planTimeout.max) throw new CommanderError(1, "commander.invalidArgument", `--plan-timeout must not exceed ${CONFIG_BOUNDS.planTimeout.max}`);
|
|
4997
|
+
return n;
|
|
4998
|
+
}
|
|
4999
|
+
).option(
|
|
5000
|
+
"--retries <n>",
|
|
5001
|
+
"Retry attempts",
|
|
5002
|
+
(val) => {
|
|
5003
|
+
const n = parseInt(val, 10);
|
|
5004
|
+
if (isNaN(n) || n < 0) throw new CommanderError(1, "commander.invalidArgument", "--retries must be a non-negative integer");
|
|
5005
|
+
return n;
|
|
5006
|
+
}
|
|
5007
|
+
).option(
|
|
5008
|
+
"--plan-retries <n>",
|
|
5009
|
+
"Planner retry attempts",
|
|
5010
|
+
(val) => {
|
|
5011
|
+
const n = parseInt(val, 10);
|
|
5012
|
+
if (isNaN(n) || n < 0) throw new CommanderError(1, "commander.invalidArgument", "--plan-retries must be a non-negative integer");
|
|
5013
|
+
return n;
|
|
5014
|
+
}
|
|
5015
|
+
).option(
|
|
5016
|
+
"--test-timeout <min>",
|
|
5017
|
+
"Test timeout in minutes",
|
|
5018
|
+
(val) => {
|
|
5019
|
+
const n = parseFloat(val);
|
|
5020
|
+
if (isNaN(n) || n <= 0) throw new CommanderError(1, "commander.invalidArgument", "--test-timeout must be a positive number (minutes)");
|
|
5021
|
+
return n;
|
|
5022
|
+
}
|
|
5023
|
+
).option("--cwd <dir>", "Working directory", (val) => resolve3(val)).option("--output-dir <dir>", "Output directory", (val) => resolve3(val)).option("--org <url>", "Azure DevOps organization URL").option("--project <name>", "Azure DevOps project name").option("--server-url <url>", "Provider server URL");
|
|
5024
|
+
try {
|
|
5025
|
+
program.parse(argv, { from: "user" });
|
|
5026
|
+
} catch (err) {
|
|
5027
|
+
if (err instanceof CommanderError) {
|
|
5028
|
+
log.error(err.message);
|
|
5029
|
+
process.exit(1);
|
|
5030
|
+
}
|
|
5031
|
+
throw err;
|
|
5032
|
+
}
|
|
5033
|
+
const opts = program.opts();
|
|
4401
5034
|
const args = {
|
|
4402
|
-
issueIds:
|
|
4403
|
-
dryRun: false,
|
|
4404
|
-
noPlan:
|
|
4405
|
-
noBranch:
|
|
4406
|
-
noWorktree:
|
|
4407
|
-
force: false,
|
|
4408
|
-
provider: "opencode",
|
|
4409
|
-
cwd: process.cwd(),
|
|
4410
|
-
help: false,
|
|
4411
|
-
version: false,
|
|
4412
|
-
verbose: false
|
|
5035
|
+
issueIds: program.args,
|
|
5036
|
+
dryRun: opts.dryRun ?? false,
|
|
5037
|
+
noPlan: !opts.plan,
|
|
5038
|
+
noBranch: !opts.branch,
|
|
5039
|
+
noWorktree: !opts.worktree,
|
|
5040
|
+
force: opts.force ?? false,
|
|
5041
|
+
provider: opts.provider ?? "opencode",
|
|
5042
|
+
cwd: opts.cwd ?? process.cwd(),
|
|
5043
|
+
help: opts.help ?? false,
|
|
5044
|
+
version: opts.version ?? false,
|
|
5045
|
+
verbose: opts.verbose ?? false
|
|
4413
5046
|
};
|
|
4414
|
-
|
|
4415
|
-
|
|
4416
|
-
|
|
4417
|
-
|
|
4418
|
-
if (
|
|
4419
|
-
args.
|
|
4420
|
-
explicitFlags.add("help");
|
|
4421
|
-
} else if (arg === "--version" || arg === "-v") {
|
|
4422
|
-
args.version = true;
|
|
4423
|
-
explicitFlags.add("version");
|
|
4424
|
-
} else if (arg === "--dry-run") {
|
|
4425
|
-
args.dryRun = true;
|
|
4426
|
-
explicitFlags.add("dryRun");
|
|
4427
|
-
} else if (arg === "--no-plan") {
|
|
4428
|
-
args.noPlan = true;
|
|
4429
|
-
explicitFlags.add("noPlan");
|
|
4430
|
-
} else if (arg === "--no-branch") {
|
|
4431
|
-
args.noBranch = true;
|
|
4432
|
-
explicitFlags.add("noBranch");
|
|
4433
|
-
} else if (arg === "--no-worktree") {
|
|
4434
|
-
args.noWorktree = true;
|
|
4435
|
-
explicitFlags.add("noWorktree");
|
|
4436
|
-
} else if (arg === "--force") {
|
|
4437
|
-
args.force = true;
|
|
4438
|
-
explicitFlags.add("force");
|
|
4439
|
-
} else if (arg === "--verbose") {
|
|
4440
|
-
args.verbose = true;
|
|
4441
|
-
explicitFlags.add("verbose");
|
|
4442
|
-
} else if (arg === "--spec") {
|
|
4443
|
-
i++;
|
|
4444
|
-
const specs = [];
|
|
4445
|
-
while (i < argv.length && !argv[i].startsWith("--")) {
|
|
4446
|
-
specs.push(argv[i]);
|
|
4447
|
-
i++;
|
|
4448
|
-
}
|
|
4449
|
-
i--;
|
|
4450
|
-
args.spec = specs.length === 1 ? specs[0] : specs;
|
|
4451
|
-
explicitFlags.add("spec");
|
|
4452
|
-
} else if (arg === "--respec") {
|
|
4453
|
-
i++;
|
|
4454
|
-
const respecs = [];
|
|
4455
|
-
while (i < argv.length && !argv[i].startsWith("--")) {
|
|
4456
|
-
respecs.push(argv[i]);
|
|
4457
|
-
i++;
|
|
4458
|
-
}
|
|
4459
|
-
i--;
|
|
4460
|
-
args.respec = respecs.length === 1 ? respecs[0] : respecs;
|
|
4461
|
-
explicitFlags.add("respec");
|
|
4462
|
-
} else if (arg === "--fix-tests") {
|
|
4463
|
-
args.fixTests = true;
|
|
4464
|
-
explicitFlags.add("fixTests");
|
|
4465
|
-
} else if (arg === "--source") {
|
|
4466
|
-
i++;
|
|
4467
|
-
const val = argv[i];
|
|
4468
|
-
if (!DATASOURCE_NAMES.includes(val)) {
|
|
4469
|
-
log.error(
|
|
4470
|
-
`Unknown source "${val}". Available: ${DATASOURCE_NAMES.join(", ")}`
|
|
4471
|
-
);
|
|
4472
|
-
process.exit(1);
|
|
4473
|
-
}
|
|
4474
|
-
args.issueSource = val;
|
|
4475
|
-
explicitFlags.add("issueSource");
|
|
4476
|
-
} else if (arg === "--org") {
|
|
4477
|
-
i++;
|
|
4478
|
-
args.org = argv[i];
|
|
4479
|
-
explicitFlags.add("org");
|
|
4480
|
-
} else if (arg === "--project") {
|
|
4481
|
-
i++;
|
|
4482
|
-
args.project = argv[i];
|
|
4483
|
-
explicitFlags.add("project");
|
|
4484
|
-
} else if (arg === "--output-dir") {
|
|
4485
|
-
i++;
|
|
4486
|
-
args.outputDir = resolve(argv[i]);
|
|
4487
|
-
explicitFlags.add("outputDir");
|
|
4488
|
-
} else if (arg === "--concurrency") {
|
|
4489
|
-
i++;
|
|
4490
|
-
const val = parseInt(argv[i], 10);
|
|
4491
|
-
if (isNaN(val) || val < 1) {
|
|
4492
|
-
log.error("--concurrency must be a positive integer");
|
|
4493
|
-
process.exit(1);
|
|
4494
|
-
}
|
|
4495
|
-
if (val > MAX_CONCURRENCY) {
|
|
4496
|
-
log.error(`--concurrency must not exceed ${MAX_CONCURRENCY}`);
|
|
4497
|
-
process.exit(1);
|
|
4498
|
-
}
|
|
4499
|
-
args.concurrency = val;
|
|
4500
|
-
explicitFlags.add("concurrency");
|
|
4501
|
-
} else if (arg === "--provider") {
|
|
4502
|
-
i++;
|
|
4503
|
-
const val = argv[i];
|
|
4504
|
-
if (!PROVIDER_NAMES.includes(val)) {
|
|
4505
|
-
log.error(`Unknown provider "${val}". Available: ${PROVIDER_NAMES.join(", ")}`);
|
|
4506
|
-
process.exit(1);
|
|
4507
|
-
}
|
|
4508
|
-
args.provider = val;
|
|
4509
|
-
explicitFlags.add("provider");
|
|
4510
|
-
} else if (arg === "--server-url") {
|
|
4511
|
-
i++;
|
|
4512
|
-
args.serverUrl = argv[i];
|
|
4513
|
-
explicitFlags.add("serverUrl");
|
|
4514
|
-
} else if (arg === "--plan-timeout") {
|
|
4515
|
-
i++;
|
|
4516
|
-
const val = parseFloat(argv[i]);
|
|
4517
|
-
if (isNaN(val) || val <= 0) {
|
|
4518
|
-
log.error("--plan-timeout must be a positive number (minutes)");
|
|
4519
|
-
process.exit(1);
|
|
4520
|
-
}
|
|
4521
|
-
args.planTimeout = val;
|
|
4522
|
-
explicitFlags.add("planTimeout");
|
|
4523
|
-
} else if (arg === "--retries") {
|
|
4524
|
-
i++;
|
|
4525
|
-
const val = parseInt(argv[i], 10);
|
|
4526
|
-
if (isNaN(val) || val < 0) {
|
|
4527
|
-
log.error("--retries must be a non-negative integer");
|
|
4528
|
-
process.exit(1);
|
|
4529
|
-
}
|
|
4530
|
-
args.retries = val;
|
|
4531
|
-
explicitFlags.add("retries");
|
|
4532
|
-
} else if (arg === "--plan-retries") {
|
|
4533
|
-
i++;
|
|
4534
|
-
const val = parseInt(argv[i], 10);
|
|
4535
|
-
if (isNaN(val) || val < 0) {
|
|
4536
|
-
log.error("--plan-retries must be a non-negative integer");
|
|
4537
|
-
process.exit(1);
|
|
4538
|
-
}
|
|
4539
|
-
args.planRetries = val;
|
|
4540
|
-
explicitFlags.add("planRetries");
|
|
4541
|
-
} else if (arg === "--test-timeout") {
|
|
4542
|
-
i++;
|
|
4543
|
-
const val = parseFloat(argv[i]);
|
|
4544
|
-
if (isNaN(val) || val <= 0) {
|
|
4545
|
-
log.error("--test-timeout must be a positive number (minutes)");
|
|
4546
|
-
process.exit(1);
|
|
4547
|
-
}
|
|
4548
|
-
args.testTimeout = val;
|
|
4549
|
-
explicitFlags.add("testTimeout");
|
|
4550
|
-
} else if (arg === "--cwd") {
|
|
4551
|
-
i++;
|
|
4552
|
-
args.cwd = resolve(argv[i]);
|
|
4553
|
-
explicitFlags.add("cwd");
|
|
4554
|
-
} else if (!arg.startsWith("-")) {
|
|
4555
|
-
args.issueIds.push(arg);
|
|
5047
|
+
if (opts.spec !== void 0) {
|
|
5048
|
+
args.spec = opts.spec.length === 1 ? opts.spec[0] : opts.spec;
|
|
5049
|
+
}
|
|
5050
|
+
if (opts.respec !== void 0) {
|
|
5051
|
+
if (opts.respec === true) {
|
|
5052
|
+
args.respec = [];
|
|
4556
5053
|
} else {
|
|
4557
|
-
|
|
4558
|
-
|
|
5054
|
+
args.respec = opts.respec.length === 1 ? opts.respec[0] : opts.respec;
|
|
5055
|
+
}
|
|
5056
|
+
}
|
|
5057
|
+
if (opts.fixTests) args.fixTests = true;
|
|
5058
|
+
if (opts.feature) args.feature = true;
|
|
5059
|
+
if (opts.source !== void 0) args.issueSource = opts.source;
|
|
5060
|
+
if (opts.concurrency !== void 0) args.concurrency = opts.concurrency;
|
|
5061
|
+
if (opts.serverUrl !== void 0) args.serverUrl = opts.serverUrl;
|
|
5062
|
+
if (opts.planTimeout !== void 0) args.planTimeout = opts.planTimeout;
|
|
5063
|
+
if (opts.retries !== void 0) args.retries = opts.retries;
|
|
5064
|
+
if (opts.planRetries !== void 0) args.planRetries = opts.planRetries;
|
|
5065
|
+
if (opts.testTimeout !== void 0) args.testTimeout = opts.testTimeout;
|
|
5066
|
+
if (opts.org !== void 0) args.org = opts.org;
|
|
5067
|
+
if (opts.project !== void 0) args.project = opts.project;
|
|
5068
|
+
if (opts.outputDir !== void 0) args.outputDir = opts.outputDir;
|
|
5069
|
+
const explicitFlags = /* @__PURE__ */ new Set();
|
|
5070
|
+
const SOURCE_MAP = {
|
|
5071
|
+
help: "help",
|
|
5072
|
+
version: "version",
|
|
5073
|
+
dryRun: "dryRun",
|
|
5074
|
+
plan: "noPlan",
|
|
5075
|
+
branch: "noBranch",
|
|
5076
|
+
worktree: "noWorktree",
|
|
5077
|
+
force: "force",
|
|
5078
|
+
verbose: "verbose",
|
|
5079
|
+
spec: "spec",
|
|
5080
|
+
respec: "respec",
|
|
5081
|
+
fixTests: "fixTests",
|
|
5082
|
+
feature: "feature",
|
|
5083
|
+
source: "issueSource",
|
|
5084
|
+
provider: "provider",
|
|
5085
|
+
concurrency: "concurrency",
|
|
5086
|
+
serverUrl: "serverUrl",
|
|
5087
|
+
planTimeout: "planTimeout",
|
|
5088
|
+
retries: "retries",
|
|
5089
|
+
planRetries: "planRetries",
|
|
5090
|
+
testTimeout: "testTimeout",
|
|
5091
|
+
cwd: "cwd",
|
|
5092
|
+
org: "org",
|
|
5093
|
+
project: "project",
|
|
5094
|
+
outputDir: "outputDir"
|
|
5095
|
+
};
|
|
5096
|
+
for (const [attr, flag] of Object.entries(SOURCE_MAP)) {
|
|
5097
|
+
if (program.getOptionValueSource(attr) === "cli") {
|
|
5098
|
+
explicitFlags.add(flag);
|
|
4559
5099
|
}
|
|
4560
|
-
i++;
|
|
4561
5100
|
}
|
|
4562
5101
|
return [args, explicitFlags];
|
|
4563
5102
|
}
|
|
4564
5103
|
async function main() {
|
|
4565
5104
|
const rawArgv = process.argv.slice(2);
|
|
4566
5105
|
if (rawArgv[0] === "config") {
|
|
4567
|
-
|
|
4568
|
-
|
|
4569
|
-
|
|
4570
|
-
|
|
4571
|
-
|
|
5106
|
+
const configProgram = new Command("dispatch-config").exitOverride().configureOutput({ writeOut: () => {
|
|
5107
|
+
}, writeErr: () => {
|
|
5108
|
+
} }).helpOption(false).allowUnknownOption(true).allowExcessArguments(true).option("--cwd <dir>", "Working directory", (v) => resolve3(v));
|
|
5109
|
+
try {
|
|
5110
|
+
configProgram.parse(rawArgv.slice(1), { from: "user" });
|
|
5111
|
+
} catch (err) {
|
|
5112
|
+
if (err instanceof CommanderError) {
|
|
5113
|
+
log.error(err.message);
|
|
5114
|
+
process.exit(1);
|
|
4572
5115
|
}
|
|
5116
|
+
throw err;
|
|
4573
5117
|
}
|
|
4574
|
-
const configDir =
|
|
5118
|
+
const configDir = join12(configProgram.opts().cwd ?? process.cwd(), ".dispatch");
|
|
4575
5119
|
await handleConfigCommand(rawArgv.slice(1), configDir);
|
|
4576
5120
|
process.exit(0);
|
|
4577
5121
|
}
|
|
@@ -4592,7 +5136,7 @@ async function main() {
|
|
|
4592
5136
|
process.exit(0);
|
|
4593
5137
|
}
|
|
4594
5138
|
if (args.version) {
|
|
4595
|
-
console.log(`dispatch v${"
|
|
5139
|
+
console.log(`dispatch v${"1.3.0"}`);
|
|
4596
5140
|
process.exit(0);
|
|
4597
5141
|
}
|
|
4598
5142
|
const orchestrator = await boot9({ cwd: args.cwd });
|