@pruddiman/dispatch 1.5.0-beta.82cfb11 → 1.5.0-beta.9b425f0-beta.9b425f0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +8924 -7380
- package/dist/cli.js.map +1 -1
- package/dist/mcp/dispatch-worker.js +2767 -1532
- package/dist/mcp/dispatch-worker.js.map +1 -1
- package/package.json +3 -2
- package/skills/dispatch/SKILL.md +144 -0
- package/skills/dispatch/references/ordering-reference.md +215 -0
- package/skills/dispatch/references/troubleshooting.md +34 -0
- package/skills/dispatch/references/workflow-recipes.md +105 -0
|
@@ -1,26 +1,946 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createRequire } from 'module';
|
|
3
3
|
const require = createRequire(import.meta.url);
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
6
|
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
5
7
|
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
6
8
|
}) : x)(function(x) {
|
|
7
9
|
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
8
10
|
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
9
11
|
});
|
|
12
|
+
var __esm = (fn, res) => function __init() {
|
|
13
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
14
|
+
};
|
|
15
|
+
var __export = (target, all) => {
|
|
16
|
+
for (var name in all)
|
|
17
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
18
|
+
};
|
|
10
19
|
|
|
11
|
-
// src/
|
|
12
|
-
import
|
|
20
|
+
// src/helpers/format.ts
|
|
21
|
+
import chalk2 from "chalk";
|
|
22
|
+
function elapsed(ms) {
|
|
23
|
+
const s = Math.floor(ms / 1e3);
|
|
24
|
+
const m = Math.floor(s / 60);
|
|
25
|
+
const sec = s % 60;
|
|
26
|
+
if (m > 0) return `${m}m ${sec}s`;
|
|
27
|
+
return `${sec}s`;
|
|
28
|
+
}
|
|
29
|
+
function renderHeaderLines(info) {
|
|
30
|
+
const lines = [];
|
|
31
|
+
lines.push(chalk2.bold.white(" \u26A1 dispatch") + chalk2.dim(` \u2014 AI task orchestration`));
|
|
32
|
+
if (info.provider) {
|
|
33
|
+
lines.push(chalk2.dim(` provider: ${info.provider}`));
|
|
34
|
+
}
|
|
35
|
+
if (info.model) {
|
|
36
|
+
lines.push(chalk2.dim(` model: ${info.model}`));
|
|
37
|
+
}
|
|
38
|
+
if (info.source) {
|
|
39
|
+
lines.push(chalk2.dim(` source: ${info.source}`));
|
|
40
|
+
}
|
|
41
|
+
return lines;
|
|
42
|
+
}
|
|
43
|
+
var PALETTE;
|
|
44
|
+
var init_format = __esm({
|
|
45
|
+
"src/helpers/format.ts"() {
|
|
46
|
+
"use strict";
|
|
47
|
+
PALETTE = {
|
|
48
|
+
brand: "#58A6FF",
|
|
49
|
+
subtitle: "#484F58",
|
|
50
|
+
chrome: "#30363D",
|
|
51
|
+
text: "#C9D1D9",
|
|
52
|
+
muted: "#484F58",
|
|
53
|
+
success: "#56D364",
|
|
54
|
+
error: "#F85149",
|
|
55
|
+
warn: "#D29922",
|
|
56
|
+
accent: "#79C0FF",
|
|
57
|
+
planning: "#D2A8FF"
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
});
|
|
13
61
|
|
|
14
|
-
// src/
|
|
15
|
-
|
|
16
|
-
|
|
62
|
+
// src/helpers/ink-prompts.tsx
|
|
63
|
+
var ink_prompts_exports = {};
|
|
64
|
+
__export(ink_prompts_exports, {
|
|
65
|
+
confirm: () => confirm,
|
|
66
|
+
input: () => input,
|
|
67
|
+
multiSelect: () => multiSelect,
|
|
68
|
+
multiSelectWithActions: () => multiSelectWithActions,
|
|
69
|
+
select: () => select
|
|
70
|
+
});
|
|
71
|
+
import { useState, useRef, useEffect } from "react";
|
|
72
|
+
import { render, Box, Text, useInput, useApp } from "ink";
|
|
73
|
+
import SelectInput from "ink-select-input";
|
|
74
|
+
import TextInput from "ink-text-input";
|
|
75
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
76
|
+
function select(opts) {
|
|
77
|
+
return new Promise((resolve3) => {
|
|
78
|
+
function SelectPrompt() {
|
|
79
|
+
const { exit } = useApp();
|
|
80
|
+
const items = opts.choices.map((c) => ({ label: c.name, value: c.value }));
|
|
81
|
+
const initialIndex = opts.default !== void 0 ? opts.choices.findIndex((c) => c.value === opts.default) : 0;
|
|
82
|
+
const visibleLimit = opts.limit ?? Math.max(5, (process.stdout.rows ?? 24) - 4);
|
|
83
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
84
|
+
/* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(Text, { color: PALETTE.accent, bold: true, children: opts.message }) }),
|
|
85
|
+
/* @__PURE__ */ jsx(
|
|
86
|
+
SelectInput,
|
|
87
|
+
{
|
|
88
|
+
items,
|
|
89
|
+
initialIndex: Math.max(0, initialIndex),
|
|
90
|
+
limit: visibleLimit,
|
|
91
|
+
onSelect: (item) => {
|
|
92
|
+
resolve3(item.value);
|
|
93
|
+
exit();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
)
|
|
97
|
+
] });
|
|
98
|
+
}
|
|
99
|
+
const instance = render(/* @__PURE__ */ jsx(SelectPrompt, {}));
|
|
100
|
+
instance.waitUntilExit().catch(() => {
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
function confirm(opts) {
|
|
105
|
+
const defaultVal = opts.default ?? true;
|
|
106
|
+
return new Promise((resolve3) => {
|
|
107
|
+
function ConfirmPrompt() {
|
|
108
|
+
const { exit } = useApp();
|
|
109
|
+
const hint = defaultVal ? "(Y/n)" : "(y/N)";
|
|
110
|
+
useInput((input2) => {
|
|
111
|
+
const lower = input2.toLowerCase();
|
|
112
|
+
if (lower === "y") {
|
|
113
|
+
resolve3(true);
|
|
114
|
+
exit();
|
|
115
|
+
} else if (lower === "n") {
|
|
116
|
+
resolve3(false);
|
|
117
|
+
exit();
|
|
118
|
+
} else if (input2 === "\r" || input2 === "\n" || input2 === "") {
|
|
119
|
+
resolve3(defaultVal);
|
|
120
|
+
exit();
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
return /* @__PURE__ */ jsxs(Box, { children: [
|
|
124
|
+
/* @__PURE__ */ jsx(Text, { color: PALETTE.accent, bold: true, children: "? " }),
|
|
125
|
+
/* @__PURE__ */ jsxs(Text, { children: [
|
|
126
|
+
opts.message,
|
|
127
|
+
" "
|
|
128
|
+
] }),
|
|
129
|
+
/* @__PURE__ */ jsxs(Text, { color: PALETTE.muted, children: [
|
|
130
|
+
hint,
|
|
131
|
+
" "
|
|
132
|
+
] })
|
|
133
|
+
] });
|
|
134
|
+
}
|
|
135
|
+
const instance = render(/* @__PURE__ */ jsx(ConfirmPrompt, {}));
|
|
136
|
+
instance.waitUntilExit().catch(() => {
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
function multiSelect(opts) {
|
|
141
|
+
return new Promise((resolve3) => {
|
|
142
|
+
function MultiSelectPrompt() {
|
|
143
|
+
const { exit } = useApp();
|
|
144
|
+
const [cursor, setCursor] = useState(0);
|
|
145
|
+
const [selected, setSelected] = useState(() => {
|
|
146
|
+
const initial = /* @__PURE__ */ new Set();
|
|
147
|
+
opts.choices.forEach((c, i) => {
|
|
148
|
+
if (c.default) initial.add(i);
|
|
149
|
+
});
|
|
150
|
+
return initial;
|
|
151
|
+
});
|
|
152
|
+
const selectedRef = useRef(selected);
|
|
153
|
+
useEffect(() => {
|
|
154
|
+
selectedRef.current = selected;
|
|
155
|
+
}, [selected]);
|
|
156
|
+
useInput((input2, key) => {
|
|
157
|
+
if (key.upArrow) {
|
|
158
|
+
setCursor((prev) => prev > 0 ? prev - 1 : opts.choices.length - 1);
|
|
159
|
+
} else if (key.downArrow) {
|
|
160
|
+
setCursor((prev) => prev < opts.choices.length - 1 ? prev + 1 : 0);
|
|
161
|
+
} else if (input2 === " ") {
|
|
162
|
+
setSelected((prev) => {
|
|
163
|
+
const next = new Set(prev);
|
|
164
|
+
if (next.has(cursor)) next.delete(cursor);
|
|
165
|
+
else next.add(cursor);
|
|
166
|
+
return next;
|
|
167
|
+
});
|
|
168
|
+
} else if (key.return) {
|
|
169
|
+
const result = opts.choices.filter((_, i) => selectedRef.current.has(i)).map((c) => c.value);
|
|
170
|
+
resolve3(result);
|
|
171
|
+
exit();
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
175
|
+
/* @__PURE__ */ jsxs(Box, { children: [
|
|
176
|
+
/* @__PURE__ */ jsx(Text, { color: PALETTE.accent, bold: true, children: "? " }),
|
|
177
|
+
/* @__PURE__ */ jsx(Text, { children: opts.message }),
|
|
178
|
+
/* @__PURE__ */ jsx(Text, { color: PALETTE.muted, children: " (space to toggle, enter to confirm)" })
|
|
179
|
+
] }),
|
|
180
|
+
opts.choices.map((choice, i) => {
|
|
181
|
+
const isSelected = selected.has(i);
|
|
182
|
+
const isCursor = i === cursor;
|
|
183
|
+
const checkbox = isSelected ? "\u25C9" : "\u25EF";
|
|
184
|
+
const pointer = isCursor ? "\u276F" : " ";
|
|
185
|
+
return /* @__PURE__ */ jsxs(Box, { children: [
|
|
186
|
+
/* @__PURE__ */ jsxs(Text, { color: isCursor ? PALETTE.accent : void 0, children: [
|
|
187
|
+
pointer,
|
|
188
|
+
" ",
|
|
189
|
+
checkbox,
|
|
190
|
+
" ",
|
|
191
|
+
choice.name
|
|
192
|
+
] }),
|
|
193
|
+
choice.description && isCursor && /* @__PURE__ */ jsxs(Text, { color: PALETTE.muted, children: [
|
|
194
|
+
" \u2014 ",
|
|
195
|
+
choice.description
|
|
196
|
+
] })
|
|
197
|
+
] }, choice.name);
|
|
198
|
+
})
|
|
199
|
+
] });
|
|
200
|
+
}
|
|
201
|
+
const instance = render(/* @__PURE__ */ jsx(MultiSelectPrompt, {}));
|
|
202
|
+
instance.waitUntilExit().catch(() => {
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
function multiSelectWithActions(opts) {
|
|
207
|
+
return new Promise((resolve3) => {
|
|
208
|
+
function MultiSelectActionsPrompt() {
|
|
209
|
+
const { exit } = useApp();
|
|
210
|
+
const [cursor, setCursor] = useState(0);
|
|
211
|
+
const [selected, setSelected] = useState(() => {
|
|
212
|
+
const initial = /* @__PURE__ */ new Set();
|
|
213
|
+
opts.choices.forEach((c, i) => {
|
|
214
|
+
if (c.default) initial.add(i);
|
|
215
|
+
});
|
|
216
|
+
return initial;
|
|
217
|
+
});
|
|
218
|
+
const [marked, setMarked] = useState(/* @__PURE__ */ new Set());
|
|
219
|
+
const selectedRef = useRef(selected);
|
|
220
|
+
const markedRef = useRef(marked);
|
|
221
|
+
useEffect(() => {
|
|
222
|
+
selectedRef.current = selected;
|
|
223
|
+
}, [selected]);
|
|
224
|
+
useEffect(() => {
|
|
225
|
+
markedRef.current = marked;
|
|
226
|
+
}, [marked]);
|
|
227
|
+
useInput((input2, key) => {
|
|
228
|
+
if (key.upArrow) {
|
|
229
|
+
setCursor((prev) => prev > 0 ? prev - 1 : opts.choices.length - 1);
|
|
230
|
+
} else if (key.downArrow) {
|
|
231
|
+
setCursor((prev) => prev < opts.choices.length - 1 ? prev + 1 : 0);
|
|
232
|
+
} else if (input2 === " ") {
|
|
233
|
+
setSelected((prev) => {
|
|
234
|
+
const next = new Set(prev);
|
|
235
|
+
if (next.has(cursor)) next.delete(cursor);
|
|
236
|
+
else next.add(cursor);
|
|
237
|
+
return next;
|
|
238
|
+
});
|
|
239
|
+
} else if (key.return) {
|
|
240
|
+
const values = opts.choices.filter((_, i) => selectedRef.current.has(i)).map((c) => c.value);
|
|
241
|
+
const markedValues = opts.choices.filter((_, i) => markedRef.current.has(i)).map((c) => c.value);
|
|
242
|
+
resolve3({ values, marked: markedValues });
|
|
243
|
+
exit();
|
|
244
|
+
} else {
|
|
245
|
+
const action = opts.actions.find((a) => a.key === input2.toLowerCase());
|
|
246
|
+
if (action) {
|
|
247
|
+
setMarked((prev) => {
|
|
248
|
+
const next = new Set(prev);
|
|
249
|
+
if (next.has(cursor)) next.delete(cursor);
|
|
250
|
+
else next.add(cursor);
|
|
251
|
+
return next;
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
const actionHints = opts.actions.map((a) => `${a.key} to ${a.label}`).join(", ");
|
|
257
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
258
|
+
/* @__PURE__ */ jsxs(Box, { children: [
|
|
259
|
+
/* @__PURE__ */ jsx(Text, { color: PALETTE.accent, bold: true, children: "? " }),
|
|
260
|
+
/* @__PURE__ */ jsx(Text, { children: opts.message }),
|
|
261
|
+
/* @__PURE__ */ jsxs(Text, { color: PALETTE.muted, children: [
|
|
262
|
+
" (space to toggle, ",
|
|
263
|
+
actionHints,
|
|
264
|
+
", enter to confirm)"
|
|
265
|
+
] })
|
|
266
|
+
] }),
|
|
267
|
+
opts.choices.map((choice, i) => {
|
|
268
|
+
const isSelected = selected.has(i);
|
|
269
|
+
const isMarked = marked.has(i);
|
|
270
|
+
const isCursor = i === cursor;
|
|
271
|
+
const checkbox = isSelected ? "\u25C9" : "\u25EF";
|
|
272
|
+
const pointer = isCursor ? "\u276F" : " ";
|
|
273
|
+
const markIndicator = isMarked ? " \u27F3" : "";
|
|
274
|
+
return /* @__PURE__ */ jsxs(Box, { children: [
|
|
275
|
+
/* @__PURE__ */ jsxs(Text, { color: isCursor ? PALETTE.accent : void 0, children: [
|
|
276
|
+
pointer,
|
|
277
|
+
" ",
|
|
278
|
+
checkbox,
|
|
279
|
+
" ",
|
|
280
|
+
choice.name
|
|
281
|
+
] }),
|
|
282
|
+
isMarked && /* @__PURE__ */ jsx(Text, { color: PALETTE.warn, children: markIndicator }),
|
|
283
|
+
choice.description && isCursor && /* @__PURE__ */ jsxs(Text, { color: PALETTE.muted, children: [
|
|
284
|
+
" \u2014 ",
|
|
285
|
+
choice.description
|
|
286
|
+
] })
|
|
287
|
+
] }, choice.name);
|
|
288
|
+
})
|
|
289
|
+
] });
|
|
290
|
+
}
|
|
291
|
+
const instance = render(/* @__PURE__ */ jsx(MultiSelectActionsPrompt, {}));
|
|
292
|
+
instance.waitUntilExit().catch(() => {
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
function input(opts) {
|
|
297
|
+
return new Promise((resolve3) => {
|
|
298
|
+
function InputPrompt() {
|
|
299
|
+
const { exit } = useApp();
|
|
300
|
+
const [value, setValue] = useState(opts.default ?? "");
|
|
301
|
+
return /* @__PURE__ */ jsxs(Box, { children: [
|
|
302
|
+
/* @__PURE__ */ jsx(Text, { color: PALETTE.accent, bold: true, children: "? " }),
|
|
303
|
+
/* @__PURE__ */ jsxs(Text, { children: [
|
|
304
|
+
opts.message,
|
|
305
|
+
" "
|
|
306
|
+
] }),
|
|
307
|
+
/* @__PURE__ */ jsx(
|
|
308
|
+
TextInput,
|
|
309
|
+
{
|
|
310
|
+
value,
|
|
311
|
+
onChange: setValue,
|
|
312
|
+
onSubmit: (val) => {
|
|
313
|
+
resolve3(val);
|
|
314
|
+
exit();
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
)
|
|
318
|
+
] });
|
|
319
|
+
}
|
|
320
|
+
const instance = render(/* @__PURE__ */ jsx(InputPrompt, {}));
|
|
321
|
+
instance.waitUntilExit().catch(() => {
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
var init_ink_prompts = __esm({
|
|
326
|
+
"src/helpers/ink-prompts.tsx"() {
|
|
327
|
+
"use strict";
|
|
328
|
+
init_format();
|
|
329
|
+
}
|
|
330
|
+
});
|
|
17
331
|
|
|
18
|
-
// src/
|
|
19
|
-
|
|
332
|
+
// src/mcp/state/database.ts
|
|
333
|
+
import Database from "better-sqlite3";
|
|
334
|
+
import { join as join8 } from "path";
|
|
335
|
+
import { mkdirSync as mkdirSync2 } from "fs";
|
|
336
|
+
function createSchema(db) {
|
|
337
|
+
db.exec(`
|
|
338
|
+
CREATE TABLE IF NOT EXISTS schema_version (
|
|
339
|
+
version INTEGER NOT NULL
|
|
340
|
+
);
|
|
20
341
|
|
|
21
|
-
|
|
342
|
+
CREATE TABLE IF NOT EXISTS runs (
|
|
343
|
+
run_id TEXT PRIMARY KEY,
|
|
344
|
+
cwd TEXT NOT NULL,
|
|
345
|
+
issue_ids TEXT NOT NULL DEFAULT '[]',
|
|
346
|
+
status TEXT NOT NULL DEFAULT 'running',
|
|
347
|
+
started_at INTEGER NOT NULL,
|
|
348
|
+
finished_at INTEGER,
|
|
349
|
+
total INTEGER NOT NULL DEFAULT 0,
|
|
350
|
+
completed INTEGER NOT NULL DEFAULT 0,
|
|
351
|
+
failed INTEGER NOT NULL DEFAULT 0,
|
|
352
|
+
error TEXT,
|
|
353
|
+
worker_message TEXT,
|
|
354
|
+
queued_at INTEGER,
|
|
355
|
+
session_id TEXT
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
CREATE TABLE IF NOT EXISTS tasks (
|
|
359
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
360
|
+
run_id TEXT NOT NULL,
|
|
361
|
+
task_id TEXT NOT NULL,
|
|
362
|
+
task_text TEXT NOT NULL DEFAULT '',
|
|
363
|
+
file TEXT NOT NULL DEFAULT '',
|
|
364
|
+
line INTEGER NOT NULL DEFAULT 0,
|
|
365
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
366
|
+
branch TEXT,
|
|
367
|
+
error TEXT,
|
|
368
|
+
started_at INTEGER,
|
|
369
|
+
finished_at INTEGER,
|
|
370
|
+
FOREIGN KEY (run_id) REFERENCES runs(run_id)
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
CREATE TABLE IF NOT EXISTS spec_runs (
|
|
374
|
+
run_id TEXT PRIMARY KEY,
|
|
375
|
+
cwd TEXT NOT NULL,
|
|
376
|
+
issues TEXT NOT NULL DEFAULT '',
|
|
377
|
+
status TEXT NOT NULL DEFAULT 'running',
|
|
378
|
+
started_at INTEGER NOT NULL,
|
|
379
|
+
finished_at INTEGER,
|
|
380
|
+
total INTEGER NOT NULL DEFAULT 0,
|
|
381
|
+
generated INTEGER NOT NULL DEFAULT 0,
|
|
382
|
+
failed INTEGER NOT NULL DEFAULT 0,
|
|
383
|
+
error TEXT,
|
|
384
|
+
worker_message TEXT,
|
|
385
|
+
queued_at INTEGER,
|
|
386
|
+
session_id TEXT
|
|
387
|
+
);
|
|
388
|
+
`);
|
|
389
|
+
const row = db.prepare("SELECT version FROM schema_version LIMIT 1").get();
|
|
390
|
+
if (!row) {
|
|
391
|
+
db.prepare("INSERT INTO schema_version (version) VALUES (?)").run(CURRENT_SCHEMA_VERSION);
|
|
392
|
+
} else {
|
|
393
|
+
if (row.version < 2) migrateToV2(db);
|
|
394
|
+
if (row.version < 3) migrateToV3(db);
|
|
395
|
+
if (row.version < CURRENT_SCHEMA_VERSION) {
|
|
396
|
+
db.prepare("UPDATE schema_version SET version = ?").run(CURRENT_SCHEMA_VERSION);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
db.exec(`
|
|
400
|
+
CREATE INDEX IF NOT EXISTS idx_runs_session_id ON runs(session_id);
|
|
401
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_run_id ON tasks(run_id);
|
|
402
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_task_id ON tasks(task_id);
|
|
403
|
+
CREATE INDEX IF NOT EXISTS idx_spec_runs_session_id ON spec_runs(session_id);
|
|
404
|
+
`);
|
|
405
|
+
}
|
|
406
|
+
function migrateToV2(db) {
|
|
407
|
+
const runCols = db.prepare("PRAGMA table_info(runs)").all();
|
|
408
|
+
const runColNames = new Set(runCols.map((c) => c.name));
|
|
409
|
+
if (!runColNames.has("worker_message")) {
|
|
410
|
+
db.exec("ALTER TABLE runs ADD COLUMN worker_message TEXT");
|
|
411
|
+
}
|
|
412
|
+
if (!runColNames.has("queued_at")) {
|
|
413
|
+
db.exec("ALTER TABLE runs ADD COLUMN queued_at INTEGER");
|
|
414
|
+
}
|
|
415
|
+
const specCols = db.prepare("PRAGMA table_info(spec_runs)").all();
|
|
416
|
+
const specColNames = new Set(specCols.map((c) => c.name));
|
|
417
|
+
if (!specColNames.has("worker_message")) {
|
|
418
|
+
db.exec("ALTER TABLE spec_runs ADD COLUMN worker_message TEXT");
|
|
419
|
+
}
|
|
420
|
+
if (!specColNames.has("queued_at")) {
|
|
421
|
+
db.exec("ALTER TABLE spec_runs ADD COLUMN queued_at INTEGER");
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
function migrateToV3(db) {
|
|
425
|
+
const runCols = db.prepare("PRAGMA table_info(runs)").all();
|
|
426
|
+
if (!runCols.some((c) => c.name === "session_id")) {
|
|
427
|
+
db.exec("ALTER TABLE runs ADD COLUMN session_id TEXT");
|
|
428
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_runs_session_id ON runs(session_id)");
|
|
429
|
+
}
|
|
430
|
+
const specCols = db.prepare("PRAGMA table_info(spec_runs)").all();
|
|
431
|
+
if (!specCols.some((c) => c.name === "session_id")) {
|
|
432
|
+
db.exec("ALTER TABLE spec_runs ADD COLUMN session_id TEXT");
|
|
433
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_spec_runs_session_id ON spec_runs(session_id)");
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
function openDatabase(cwd) {
|
|
437
|
+
if (_db) return _db;
|
|
438
|
+
const dispatchDir = join8(cwd, ".dispatch");
|
|
439
|
+
mkdirSync2(dispatchDir, { recursive: true });
|
|
440
|
+
const dbPath = join8(dispatchDir, "dispatch.db");
|
|
441
|
+
const db = new Database(dbPath);
|
|
442
|
+
db.pragma("journal_mode = WAL");
|
|
443
|
+
db.pragma("synchronous = NORMAL");
|
|
444
|
+
db.pragma("foreign_keys = ON");
|
|
445
|
+
createSchema(db);
|
|
446
|
+
_db = db;
|
|
447
|
+
return db;
|
|
448
|
+
}
|
|
449
|
+
function getDb() {
|
|
450
|
+
if (!_db) {
|
|
451
|
+
throw new Error("Database not open. Call openDatabase(cwd) first.");
|
|
452
|
+
}
|
|
453
|
+
return _db;
|
|
454
|
+
}
|
|
455
|
+
var RUN_STATUSES, TASK_STATUSES, SPEC_STATUSES, _db, CURRENT_SCHEMA_VERSION;
|
|
456
|
+
var init_database = __esm({
|
|
457
|
+
"src/mcp/state/database.ts"() {
|
|
458
|
+
"use strict";
|
|
459
|
+
RUN_STATUSES = ["queued", "running", "completed", "failed", "cancelled"];
|
|
460
|
+
TASK_STATUSES = ["pending", "running", "success", "failed", "skipped"];
|
|
461
|
+
SPEC_STATUSES = ["queued", "running", "completed", "failed"];
|
|
462
|
+
_db = null;
|
|
463
|
+
CURRENT_SCHEMA_VERSION = 3;
|
|
464
|
+
}
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
// src/mcp/state/manager.ts
|
|
468
|
+
var manager_exports = {};
|
|
469
|
+
__export(manager_exports, {
|
|
470
|
+
addCompletionCallback: () => addCompletionCallback,
|
|
471
|
+
addLogCallback: () => addLogCallback,
|
|
472
|
+
countActiveRuns: () => countActiveRuns,
|
|
473
|
+
createRun: () => createRun,
|
|
474
|
+
createSpecRun: () => createSpecRun,
|
|
475
|
+
createTask: () => createTask,
|
|
476
|
+
emitLog: () => emitLog,
|
|
477
|
+
failAllQueuedRuns: () => failAllQueuedRuns,
|
|
478
|
+
finishRun: () => finishRun,
|
|
479
|
+
finishSpecRun: () => finishSpecRun,
|
|
480
|
+
getNextQueued: () => getNextQueued,
|
|
481
|
+
getQueuePosition: () => getQueuePosition,
|
|
482
|
+
getRun: () => getRun,
|
|
483
|
+
getSpecRun: () => getSpecRun,
|
|
484
|
+
getTasksForRun: () => getTasksForRun,
|
|
485
|
+
isLiveRun: () => isLiveRun,
|
|
486
|
+
listResumableSessions: () => listResumableSessions,
|
|
487
|
+
listRuns: () => listRuns,
|
|
488
|
+
listRunsByStatus: () => listRunsByStatus,
|
|
489
|
+
listSpecRuns: () => listSpecRuns,
|
|
490
|
+
markOrphanedRunsFailed: () => markOrphanedRunsFailed,
|
|
491
|
+
markRunStarted: () => markRunStarted,
|
|
492
|
+
markSpecRunStarted: () => markSpecRunStarted,
|
|
493
|
+
registerLiveRun: () => registerLiveRun,
|
|
494
|
+
requeueSessionRuns: () => requeueSessionRuns,
|
|
495
|
+
unregisterLiveRun: () => unregisterLiveRun,
|
|
496
|
+
updateRunCounters: () => updateRunCounters,
|
|
497
|
+
updateTaskStatus: () => updateTaskStatus,
|
|
498
|
+
waitForRunCompletion: () => waitForRunCompletion
|
|
499
|
+
});
|
|
500
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
501
|
+
function registerLiveRun(runId) {
|
|
502
|
+
liveRuns.set(runId, { runId, callbacks: [], completionCallbacks: [] });
|
|
503
|
+
}
|
|
504
|
+
function unregisterLiveRun(runId) {
|
|
505
|
+
const run = liveRuns.get(runId);
|
|
506
|
+
if (run) {
|
|
507
|
+
for (const cb of run.completionCallbacks) {
|
|
508
|
+
try {
|
|
509
|
+
cb();
|
|
510
|
+
} catch {
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
liveRuns.delete(runId);
|
|
515
|
+
}
|
|
516
|
+
function addLogCallback(runId, cb) {
|
|
517
|
+
const run = liveRuns.get(runId);
|
|
518
|
+
if (run) {
|
|
519
|
+
run.callbacks.push(cb);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
function emitLog(runId, message, level = "info") {
|
|
523
|
+
const run = liveRuns.get(runId);
|
|
524
|
+
if (run) {
|
|
525
|
+
for (const cb of run.callbacks) {
|
|
526
|
+
try {
|
|
527
|
+
cb(message, level);
|
|
528
|
+
} catch (err) {
|
|
529
|
+
if (process.env["DEBUG"]) console.error("[dispatch-mcp] log callback error:", err);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
function isLiveRun(runId) {
|
|
535
|
+
return liveRuns.has(runId);
|
|
536
|
+
}
|
|
537
|
+
function addCompletionCallback(runId, cb) {
|
|
538
|
+
const run = liveRuns.get(runId);
|
|
539
|
+
if (run) {
|
|
540
|
+
run.completionCallbacks.push(cb);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
function waitForRunCompletion(runId, waitMs, getStatus) {
|
|
544
|
+
const effectiveWait = Math.min(Math.max(waitMs, 0), 12e4);
|
|
545
|
+
if (effectiveWait <= 0) return Promise.resolve(false);
|
|
546
|
+
const currentStatus = getStatus();
|
|
547
|
+
if (currentStatus !== null && currentStatus !== "running" && currentStatus !== "queued") {
|
|
548
|
+
return Promise.resolve(true);
|
|
549
|
+
}
|
|
550
|
+
return new Promise((resolve3) => {
|
|
551
|
+
let settled = false;
|
|
552
|
+
let pollTimer;
|
|
553
|
+
let timeoutTimer;
|
|
554
|
+
const cleanup = () => {
|
|
555
|
+
if (pollTimer) clearInterval(pollTimer);
|
|
556
|
+
if (timeoutTimer) clearTimeout(timeoutTimer);
|
|
557
|
+
};
|
|
558
|
+
const settle = (completed) => {
|
|
559
|
+
if (settled) return;
|
|
560
|
+
settled = true;
|
|
561
|
+
cleanup();
|
|
562
|
+
resolve3(completed);
|
|
563
|
+
};
|
|
564
|
+
if (isLiveRun(runId)) {
|
|
565
|
+
addCompletionCallback(runId, () => settle(true));
|
|
566
|
+
}
|
|
567
|
+
pollTimer = setInterval(() => {
|
|
568
|
+
const s = getStatus();
|
|
569
|
+
if (s !== null && s !== "running" && s !== "queued") {
|
|
570
|
+
settle(true);
|
|
571
|
+
}
|
|
572
|
+
}, 2e3);
|
|
573
|
+
timeoutTimer = setTimeout(() => settle(false), effectiveWait);
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
function assertRunStatus(value) {
|
|
577
|
+
if (RUN_STATUSES.includes(value)) return value;
|
|
578
|
+
throw new Error(`Invalid RunStatus from database: "${value}"`);
|
|
579
|
+
}
|
|
580
|
+
function assertTaskStatus(value) {
|
|
581
|
+
if (TASK_STATUSES.includes(value)) return value;
|
|
582
|
+
throw new Error(`Invalid TaskStatus from database: "${value}"`);
|
|
583
|
+
}
|
|
584
|
+
function assertSpecStatus(value) {
|
|
585
|
+
if (SPEC_STATUSES.includes(value)) return value;
|
|
586
|
+
throw new Error(`Invalid SpecStatus from database: "${value}"`);
|
|
587
|
+
}
|
|
588
|
+
function rowToRun(row) {
|
|
589
|
+
return {
|
|
590
|
+
runId: row.run_id,
|
|
591
|
+
cwd: row.cwd,
|
|
592
|
+
issueIds: row.issue_ids,
|
|
593
|
+
status: assertRunStatus(row.status),
|
|
594
|
+
startedAt: row.started_at,
|
|
595
|
+
finishedAt: row.finished_at,
|
|
596
|
+
total: row.total,
|
|
597
|
+
completed: row.completed,
|
|
598
|
+
failed: row.failed,
|
|
599
|
+
error: row.error,
|
|
600
|
+
workerMessage: row.worker_message,
|
|
601
|
+
queuedAt: row.queued_at,
|
|
602
|
+
sessionId: row.session_id
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
function rowToTask(row) {
|
|
606
|
+
return {
|
|
607
|
+
rowId: row.id,
|
|
608
|
+
runId: row.run_id,
|
|
609
|
+
taskId: row.task_id,
|
|
610
|
+
taskText: row.task_text,
|
|
611
|
+
file: row.file,
|
|
612
|
+
line: row.line,
|
|
613
|
+
status: assertTaskStatus(row.status),
|
|
614
|
+
branch: row.branch,
|
|
615
|
+
error: row.error,
|
|
616
|
+
startedAt: row.started_at,
|
|
617
|
+
finishedAt: row.finished_at
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
function rowToSpecRun(row) {
|
|
621
|
+
return {
|
|
622
|
+
runId: row.run_id,
|
|
623
|
+
cwd: row.cwd,
|
|
624
|
+
issues: row.issues,
|
|
625
|
+
status: assertSpecStatus(row.status),
|
|
626
|
+
startedAt: row.started_at,
|
|
627
|
+
finishedAt: row.finished_at,
|
|
628
|
+
total: row.total,
|
|
629
|
+
generated: row.generated,
|
|
630
|
+
failed: row.failed,
|
|
631
|
+
error: row.error,
|
|
632
|
+
workerMessage: row.worker_message,
|
|
633
|
+
queuedAt: row.queued_at,
|
|
634
|
+
sessionId: row.session_id
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
function createRun(opts) {
|
|
638
|
+
const runId = randomUUID3();
|
|
639
|
+
const db = getDb();
|
|
640
|
+
const now = Date.now();
|
|
641
|
+
const status = opts.status ?? "running";
|
|
642
|
+
db.prepare(`
|
|
643
|
+
INSERT INTO runs (run_id, cwd, issue_ids, status, started_at, worker_message, queued_at, session_id)
|
|
644
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
645
|
+
`).run(runId, opts.cwd, JSON.stringify(opts.issueIds), status, now, opts.workerMessage ?? null, status === "queued" ? now : null, opts.sessionId ?? null);
|
|
646
|
+
registerLiveRun(runId);
|
|
647
|
+
return runId;
|
|
648
|
+
}
|
|
649
|
+
function updateRunCounters(runId, total, completed, failed) {
|
|
650
|
+
getDb().prepare(`
|
|
651
|
+
UPDATE runs SET total = ?, completed = ?, failed = ? WHERE run_id = ?
|
|
652
|
+
`).run(total, completed, failed, runId);
|
|
653
|
+
}
|
|
654
|
+
function finishRun(runId, status, error) {
|
|
655
|
+
getDb().prepare(`
|
|
656
|
+
UPDATE runs SET status = ?, finished_at = ?, error = ? WHERE run_id = ?
|
|
657
|
+
`).run(status, Date.now(), error ?? null, runId);
|
|
658
|
+
unregisterLiveRun(runId);
|
|
659
|
+
}
|
|
660
|
+
function getRun(runId) {
|
|
661
|
+
const row = getDb().prepare("SELECT * FROM runs WHERE run_id = ?").get(runId);
|
|
662
|
+
return row ? rowToRun(row) : null;
|
|
663
|
+
}
|
|
664
|
+
function listRuns(limit = 50) {
|
|
665
|
+
const rows = getDb().prepare(
|
|
666
|
+
"SELECT * FROM runs ORDER BY started_at DESC LIMIT ?"
|
|
667
|
+
).all(limit);
|
|
668
|
+
return rows.map(rowToRun);
|
|
669
|
+
}
|
|
670
|
+
function listRunsByStatus(status, limit = 20) {
|
|
671
|
+
const rows = getDb().prepare(
|
|
672
|
+
"SELECT * FROM runs WHERE status = ? ORDER BY started_at DESC LIMIT ?"
|
|
673
|
+
).all(status, limit);
|
|
674
|
+
return rows.map(rowToRun);
|
|
675
|
+
}
|
|
676
|
+
function createTask(opts) {
|
|
677
|
+
getDb().prepare(`
|
|
678
|
+
INSERT INTO tasks (run_id, task_id, task_text, file, line, status)
|
|
679
|
+
VALUES (?, ?, ?, ?, ?, 'pending')
|
|
680
|
+
`).run(opts.runId, opts.taskId, opts.taskText, opts.file, opts.line);
|
|
681
|
+
}
|
|
682
|
+
function updateTaskStatus(runId, taskId, status, opts) {
|
|
683
|
+
const now = Date.now();
|
|
684
|
+
const isTerminal = status === "success" || status === "failed" || status === "skipped";
|
|
685
|
+
const isStart = status === "running";
|
|
686
|
+
if (isStart) {
|
|
687
|
+
getDb().prepare(`
|
|
688
|
+
UPDATE tasks SET status = ?, started_at = ?, error = NULL
|
|
689
|
+
WHERE run_id = ? AND task_id = ?
|
|
690
|
+
`).run(status, now, runId, taskId);
|
|
691
|
+
} else if (isTerminal) {
|
|
692
|
+
getDb().prepare(`
|
|
693
|
+
UPDATE tasks SET status = ?, finished_at = ?, error = ?, branch = ?
|
|
694
|
+
WHERE run_id = ? AND task_id = ?
|
|
695
|
+
`).run(status, now, opts?.error ?? null, opts?.branch ?? null, runId, taskId);
|
|
696
|
+
} else {
|
|
697
|
+
getDb().prepare(`
|
|
698
|
+
UPDATE tasks SET status = ?, error = ?
|
|
699
|
+
WHERE run_id = ? AND task_id = ?
|
|
700
|
+
`).run(status, opts?.error ?? null, runId, taskId);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
function getTasksForRun(runId) {
|
|
704
|
+
const rows = getDb().prepare(
|
|
705
|
+
"SELECT * FROM tasks WHERE run_id = ? ORDER BY id ASC"
|
|
706
|
+
).all(runId);
|
|
707
|
+
return rows.map(rowToTask);
|
|
708
|
+
}
|
|
709
|
+
function createSpecRun(opts) {
|
|
710
|
+
const runId = randomUUID3();
|
|
711
|
+
const db = getDb();
|
|
712
|
+
const now = Date.now();
|
|
713
|
+
const status = opts.status ?? "running";
|
|
714
|
+
db.prepare(`
|
|
715
|
+
INSERT INTO spec_runs (run_id, cwd, issues, status, started_at, worker_message, queued_at, session_id)
|
|
716
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
717
|
+
`).run(runId, opts.cwd, JSON.stringify(opts.issues), status, now, opts.workerMessage ?? null, status === "queued" ? now : null, opts.sessionId ?? null);
|
|
718
|
+
registerLiveRun(runId);
|
|
719
|
+
return runId;
|
|
720
|
+
}
|
|
721
|
+
function finishSpecRun(runId, status, counters, error) {
|
|
722
|
+
getDb().prepare(`
|
|
723
|
+
UPDATE spec_runs
|
|
724
|
+
SET status = ?, finished_at = ?, total = ?, generated = ?, failed = ?, error = ?
|
|
725
|
+
WHERE run_id = ?
|
|
726
|
+
`).run(status, Date.now(), counters.total, counters.generated, counters.failed, error ?? null, runId);
|
|
727
|
+
unregisterLiveRun(runId);
|
|
728
|
+
}
|
|
729
|
+
function listSpecRuns(limit = 50) {
|
|
730
|
+
const rows = getDb().prepare(
|
|
731
|
+
"SELECT * FROM spec_runs ORDER BY started_at DESC LIMIT ?"
|
|
732
|
+
).all(limit);
|
|
733
|
+
return rows.map(rowToSpecRun);
|
|
734
|
+
}
|
|
735
|
+
function getSpecRun(runId) {
|
|
736
|
+
const row = getDb().prepare("SELECT * FROM spec_runs WHERE run_id = ?").get(runId);
|
|
737
|
+
return row ? rowToSpecRun(row) : null;
|
|
738
|
+
}
|
|
739
|
+
function markRunStarted(runId) {
|
|
740
|
+
getDb().prepare(`
|
|
741
|
+
UPDATE runs SET status = 'running', started_at = ? WHERE run_id = ? AND status = 'queued'
|
|
742
|
+
`).run(Date.now(), runId);
|
|
743
|
+
}
|
|
744
|
+
function markSpecRunStarted(runId) {
|
|
745
|
+
getDb().prepare(`
|
|
746
|
+
UPDATE spec_runs SET status = 'running', started_at = ? WHERE run_id = ? AND status = 'queued'
|
|
747
|
+
`).run(Date.now(), runId);
|
|
748
|
+
}
|
|
749
|
+
function getNextQueued() {
|
|
750
|
+
const db = getDb();
|
|
751
|
+
const runRow = db.prepare(
|
|
752
|
+
"SELECT run_id, worker_message FROM runs WHERE status = 'queued' AND worker_message IS NOT NULL ORDER BY queued_at ASC LIMIT 1"
|
|
753
|
+
).get();
|
|
754
|
+
const specRow = db.prepare(
|
|
755
|
+
"SELECT run_id, worker_message FROM spec_runs WHERE status = 'queued' AND worker_message IS NOT NULL ORDER BY queued_at ASC LIMIT 1"
|
|
756
|
+
).get();
|
|
757
|
+
if (!runRow && !specRow) return null;
|
|
758
|
+
if (runRow && specRow) {
|
|
759
|
+
const runQueuedAt = db.prepare("SELECT queued_at FROM runs WHERE run_id = ?").get(runRow.run_id).queued_at;
|
|
760
|
+
const specQueuedAt = db.prepare("SELECT queued_at FROM spec_runs WHERE run_id = ?").get(specRow.run_id).queued_at;
|
|
761
|
+
if (specQueuedAt < runQueuedAt) {
|
|
762
|
+
return { runId: specRow.run_id, workerMessage: specRow.worker_message, table: "spec_runs" };
|
|
763
|
+
}
|
|
764
|
+
return { runId: runRow.run_id, workerMessage: runRow.worker_message, table: "runs" };
|
|
765
|
+
}
|
|
766
|
+
if (runRow) return { runId: runRow.run_id, workerMessage: runRow.worker_message, table: "runs" };
|
|
767
|
+
return { runId: specRow.run_id, workerMessage: specRow.worker_message, table: "spec_runs" };
|
|
768
|
+
}
|
|
769
|
+
function countActiveRuns() {
|
|
770
|
+
const db = getDb();
|
|
771
|
+
const r1 = db.prepare("SELECT COUNT(*) as cnt FROM runs WHERE status = 'running'").get();
|
|
772
|
+
const r2 = db.prepare("SELECT COUNT(*) as cnt FROM spec_runs WHERE status = 'running'").get();
|
|
773
|
+
return r1.cnt + r2.cnt;
|
|
774
|
+
}
|
|
775
|
+
function getQueuePosition(runId) {
|
|
776
|
+
const db = getDb();
|
|
777
|
+
const runRow = db.prepare("SELECT queued_at FROM runs WHERE run_id = ? AND status = 'queued'").get(runId);
|
|
778
|
+
if (runRow) {
|
|
779
|
+
const pos = db.prepare(
|
|
780
|
+
"SELECT COUNT(*) as cnt FROM runs WHERE status = 'queued' AND queued_at <= ?"
|
|
781
|
+
).get(runRow.queued_at);
|
|
782
|
+
const specBefore = db.prepare(
|
|
783
|
+
"SELECT COUNT(*) as cnt FROM spec_runs WHERE status = 'queued' AND queued_at < ?"
|
|
784
|
+
).get(runRow.queued_at);
|
|
785
|
+
return pos.cnt + specBefore.cnt;
|
|
786
|
+
}
|
|
787
|
+
const specRow = db.prepare("SELECT queued_at FROM spec_runs WHERE run_id = ? AND status = 'queued'").get(runId);
|
|
788
|
+
if (specRow) {
|
|
789
|
+
const pos = db.prepare(
|
|
790
|
+
"SELECT COUNT(*) as cnt FROM spec_runs WHERE status = 'queued' AND queued_at <= ?"
|
|
791
|
+
).get(specRow.queued_at);
|
|
792
|
+
const runsBefore = db.prepare(
|
|
793
|
+
"SELECT COUNT(*) as cnt FROM runs WHERE status = 'queued' AND queued_at < ?"
|
|
794
|
+
).get(specRow.queued_at);
|
|
795
|
+
return pos.cnt + runsBefore.cnt;
|
|
796
|
+
}
|
|
797
|
+
return null;
|
|
798
|
+
}
|
|
799
|
+
function markOrphanedRunsFailed(opts) {
|
|
800
|
+
const db = getDb();
|
|
801
|
+
const now = Date.now();
|
|
802
|
+
if (opts?.exceptSessionId) {
|
|
803
|
+
db.prepare(`
|
|
804
|
+
UPDATE runs SET status = 'failed', finished_at = ?, error = 'Server restarted'
|
|
805
|
+
WHERE status IN ('queued', 'running') AND (session_id IS NULL OR session_id != ?)
|
|
806
|
+
`).run(now, opts.exceptSessionId);
|
|
807
|
+
db.prepare(`
|
|
808
|
+
UPDATE spec_runs SET status = 'failed', finished_at = ?, error = 'Server restarted'
|
|
809
|
+
WHERE status IN ('queued', 'running') AND (session_id IS NULL OR session_id != ?)
|
|
810
|
+
`).run(now, opts.exceptSessionId);
|
|
811
|
+
} else {
|
|
812
|
+
db.prepare(`
|
|
813
|
+
UPDATE runs SET status = 'failed', finished_at = ?, error = 'Server restarted'
|
|
814
|
+
WHERE status IN ('queued', 'running')
|
|
815
|
+
`).run(now);
|
|
816
|
+
db.prepare(`
|
|
817
|
+
UPDATE spec_runs SET status = 'failed', finished_at = ?, error = 'Server restarted'
|
|
818
|
+
WHERE status IN ('queued', 'running')
|
|
819
|
+
`).run(now);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
function failAllQueuedRuns() {
|
|
823
|
+
const db = getDb();
|
|
824
|
+
const now = Date.now();
|
|
825
|
+
db.prepare(`
|
|
826
|
+
UPDATE runs SET status = 'failed', finished_at = ?, error = 'Server shutting down'
|
|
827
|
+
WHERE status = 'queued'
|
|
828
|
+
`).run(now);
|
|
829
|
+
db.prepare(`
|
|
830
|
+
UPDATE spec_runs SET status = 'failed', finished_at = ?, error = 'Server shutting down'
|
|
831
|
+
WHERE status = 'queued'
|
|
832
|
+
`).run(now);
|
|
833
|
+
}
|
|
834
|
+
function listResumableSessions(cwd) {
|
|
835
|
+
const db = getDb();
|
|
836
|
+
const rows = db.prepare(`
|
|
837
|
+
SELECT session_id, MIN(started_at) as started_at,
|
|
838
|
+
COUNT(*) as total_runs,
|
|
839
|
+
SUM(CASE WHEN status IN ('queued', 'failed') THEN 1 ELSE 0 END) as incomplete,
|
|
840
|
+
(SELECT issue_ids FROM runs r2 WHERE r2.session_id = runs.session_id ORDER BY started_at ASC LIMIT 1) as issue_ids
|
|
841
|
+
FROM runs
|
|
842
|
+
WHERE session_id IS NOT NULL AND cwd = ?
|
|
843
|
+
AND session_id IN (
|
|
844
|
+
SELECT DISTINCT session_id FROM runs
|
|
845
|
+
WHERE status IN ('queued', 'failed') AND session_id IS NOT NULL AND cwd = ?
|
|
846
|
+
)
|
|
847
|
+
GROUP BY session_id
|
|
848
|
+
ORDER BY MIN(started_at) DESC
|
|
849
|
+
`).all(cwd, cwd);
|
|
850
|
+
return rows.map((r) => ({
|
|
851
|
+
sessionId: r.session_id,
|
|
852
|
+
startedAt: r.started_at,
|
|
853
|
+
totalRuns: r.total_runs,
|
|
854
|
+
incompleteRuns: r.incomplete,
|
|
855
|
+
issueIds: r.issue_ids
|
|
856
|
+
}));
|
|
857
|
+
}
|
|
858
|
+
function requeueSessionRuns(sessionId) {
|
|
859
|
+
const db = getDb();
|
|
860
|
+
const now = Date.now();
|
|
861
|
+
const runRows = db.prepare(`
|
|
862
|
+
SELECT run_id FROM runs
|
|
863
|
+
WHERE session_id = ? AND status IN ('failed', 'queued')
|
|
864
|
+
AND worker_message IS NOT NULL
|
|
865
|
+
`).all(sessionId);
|
|
866
|
+
const specRows = db.prepare(`
|
|
867
|
+
SELECT run_id FROM spec_runs
|
|
868
|
+
WHERE session_id = ? AND status IN ('failed', 'queued')
|
|
869
|
+
AND worker_message IS NOT NULL
|
|
870
|
+
`).all(sessionId);
|
|
871
|
+
if (runRows.length > 0) {
|
|
872
|
+
db.prepare(`
|
|
873
|
+
UPDATE runs SET status = 'queued', queued_at = ?, finished_at = NULL, error = NULL
|
|
874
|
+
WHERE session_id = ? AND status IN ('failed', 'queued') AND worker_message IS NOT NULL
|
|
875
|
+
`).run(now, sessionId);
|
|
876
|
+
}
|
|
877
|
+
if (specRows.length > 0) {
|
|
878
|
+
db.prepare(`
|
|
879
|
+
UPDATE spec_runs SET status = 'queued', queued_at = ?, finished_at = NULL, error = NULL
|
|
880
|
+
WHERE session_id = ? AND status IN ('failed', 'queued') AND worker_message IS NOT NULL
|
|
881
|
+
`).run(now, sessionId);
|
|
882
|
+
}
|
|
883
|
+
const runIds = [...runRows.map((r) => r.run_id), ...specRows.map((r) => r.run_id)];
|
|
884
|
+
for (const runId of runIds) {
|
|
885
|
+
registerLiveRun(runId);
|
|
886
|
+
}
|
|
887
|
+
return runIds;
|
|
888
|
+
}
|
|
889
|
+
var liveRuns;
|
|
890
|
+
var init_manager = __esm({
|
|
891
|
+
"src/mcp/state/manager.ts"() {
|
|
892
|
+
"use strict";
|
|
893
|
+
init_database();
|
|
894
|
+
liveRuns = /* @__PURE__ */ new Map();
|
|
895
|
+
}
|
|
896
|
+
});
|
|
897
|
+
|
|
898
|
+
// src/orchestrator/runner.ts
|
|
899
|
+
import { randomUUID as randomUUID6 } from "crypto";
|
|
900
|
+
|
|
901
|
+
// src/config.ts
|
|
902
|
+
import { readFile as readFile5, writeFile as writeFile3, mkdir as mkdir3 } from "fs/promises";
|
|
903
|
+
import { join as join6, dirname as dirname4 } from "path";
|
|
904
|
+
|
|
905
|
+
// src/providers/interface.ts
|
|
906
|
+
var PROVIDER_NAMES = ["opencode", "copilot", "claude", "codex"];
|
|
907
|
+
|
|
908
|
+
// src/providers/opencode.ts
|
|
22
909
|
import { execFile } from "child_process";
|
|
23
910
|
import { promisify } from "util";
|
|
911
|
+
import {
|
|
912
|
+
createOpencode,
|
|
913
|
+
createOpencodeClient
|
|
914
|
+
} from "@opencode-ai/sdk";
|
|
915
|
+
|
|
916
|
+
// src/providers/progress.ts
|
|
917
|
+
var ANSI_PATTERN = /(?:\u001B\[[0-?]*[ -/]*[@-~]|\u009B[0-?]*[ -/]*[@-~]|\u001B\][^\u0007]*(?:\u0007|\u001B\\))/g;
|
|
918
|
+
var CONTROL_PATTERN = /[\u0000-\u0008\u000B-\u001F\u007F]/g;
|
|
919
|
+
function sanitizeProgressText(raw, maxLength = 120) {
|
|
920
|
+
const text = raw.replace(ANSI_PATTERN, "").replace(CONTROL_PATTERN, "").replace(/\s+/g, " ").trim();
|
|
921
|
+
if (!text) return "";
|
|
922
|
+
if (text.length <= maxLength) return text;
|
|
923
|
+
if (maxLength <= 1) return maxLength <= 0 ? "" : "\u2026";
|
|
924
|
+
return `${text.slice(0, maxLength - 1).trimEnd()}\u2026`;
|
|
925
|
+
}
|
|
926
|
+
function createProgressReporter(onProgress) {
|
|
927
|
+
let last;
|
|
928
|
+
return {
|
|
929
|
+
emit(raw) {
|
|
930
|
+
if (!onProgress) return;
|
|
931
|
+
const text = sanitizeProgressText(raw ?? "");
|
|
932
|
+
if (!text || text === last) return;
|
|
933
|
+
last = text;
|
|
934
|
+
try {
|
|
935
|
+
onProgress({ text });
|
|
936
|
+
} catch {
|
|
937
|
+
}
|
|
938
|
+
},
|
|
939
|
+
reset() {
|
|
940
|
+
last = void 0;
|
|
941
|
+
}
|
|
942
|
+
};
|
|
943
|
+
}
|
|
24
944
|
|
|
25
945
|
// src/helpers/logger.ts
|
|
26
946
|
import chalk from "chalk";
|
|
@@ -215,956 +1135,98 @@ Object.defineProperty(log, "verbose", {
|
|
|
215
1135
|
configurable: true
|
|
216
1136
|
});
|
|
217
1137
|
|
|
218
|
-
// src/helpers/
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
const detail = reason ? ` (${reason})` : "";
|
|
222
|
-
super(`Invalid branch name: "${branch}"${detail}`);
|
|
223
|
-
this.name = "InvalidBranchNameError";
|
|
224
|
-
}
|
|
225
|
-
};
|
|
226
|
-
var VALID_BRANCH_NAME_RE = /^[a-zA-Z0-9._\-/]+$/;
|
|
227
|
-
function isValidBranchName(name) {
|
|
228
|
-
if (name.length === 0 || name.length > 255) return false;
|
|
229
|
-
if (!VALID_BRANCH_NAME_RE.test(name)) return false;
|
|
230
|
-
if (name.startsWith("/") || name.endsWith("/")) return false;
|
|
231
|
-
if (name.includes("..")) return false;
|
|
232
|
-
if (name.endsWith(".lock")) return false;
|
|
233
|
-
if (name.includes("@{")) return false;
|
|
234
|
-
if (name.includes("//")) return false;
|
|
235
|
-
return true;
|
|
1138
|
+
// src/helpers/guards.ts
|
|
1139
|
+
function hasProperty(value, key) {
|
|
1140
|
+
return typeof value === "object" && value !== null && Object.prototype.hasOwnProperty.call(value, key);
|
|
236
1141
|
}
|
|
237
1142
|
|
|
238
|
-
// src/helpers/
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
// src/constants.ts
|
|
249
|
-
var GITHUB_CLIENT_ID = "Ov23liUMP1Oyg811IF58";
|
|
250
|
-
var AZURE_CLIENT_ID = "150a3098-01dd-4126-8b10-5e7f77492e5c";
|
|
251
|
-
var AZURE_TENANT_ID = "organizations";
|
|
252
|
-
var AZURE_DEVOPS_SCOPE = "499b84ac-1321-427f-aa17-267ca6975798/.default";
|
|
253
|
-
|
|
254
|
-
// src/helpers/auth.ts
|
|
255
|
-
var AUTH_PATH = join2(homedir(), ".dispatch", "auth.json");
|
|
256
|
-
var EXPIRY_BUFFER_MS = 5 * 60 * 1e3;
|
|
257
|
-
var authPromptHandler = null;
|
|
258
|
-
function setAuthPromptHandler(handler) {
|
|
259
|
-
authPromptHandler = handler;
|
|
260
|
-
}
|
|
261
|
-
async function loadAuthCache() {
|
|
262
|
-
try {
|
|
263
|
-
const raw = await readFile(AUTH_PATH, "utf-8");
|
|
264
|
-
return JSON.parse(raw);
|
|
265
|
-
} catch {
|
|
266
|
-
return {};
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
async function saveAuthCache(cache) {
|
|
270
|
-
await mkdir(dirname2(AUTH_PATH), { recursive: true });
|
|
271
|
-
await writeFile(AUTH_PATH, JSON.stringify(cache, null, 2) + "\n", "utf-8");
|
|
272
|
-
if (process.platform !== "win32") {
|
|
273
|
-
try {
|
|
274
|
-
await chmod(AUTH_PATH, 384);
|
|
275
|
-
} catch {
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
async function getGithubOctokit() {
|
|
280
|
-
const cache = await loadAuthCache();
|
|
281
|
-
if (cache.github?.token) {
|
|
282
|
-
return new Octokit({ auth: cache.github.token });
|
|
1143
|
+
// src/helpers/timeout.ts
|
|
1144
|
+
var TimeoutError = class extends Error {
|
|
1145
|
+
/** Optional label identifying the operation that timed out. */
|
|
1146
|
+
label;
|
|
1147
|
+
constructor(ms, label) {
|
|
1148
|
+
const suffix = label ? ` [${label}]` : "";
|
|
1149
|
+
super(`Timed out after ${ms}ms${suffix}`);
|
|
1150
|
+
this.name = "TimeoutError";
|
|
1151
|
+
this.label = label;
|
|
283
1152
|
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
if (
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
1153
|
+
};
|
|
1154
|
+
var DEFAULT_PLAN_TIMEOUT_MIN = 30;
|
|
1155
|
+
function withTimeout(promise, ms, label) {
|
|
1156
|
+
const p = new Promise((resolve3, reject) => {
|
|
1157
|
+
let settled = false;
|
|
1158
|
+
const timer = setTimeout(() => {
|
|
1159
|
+
if (settled) return;
|
|
1160
|
+
settled = true;
|
|
1161
|
+
reject(new TimeoutError(ms, label));
|
|
1162
|
+
}, ms);
|
|
1163
|
+
promise.then(
|
|
1164
|
+
(value) => {
|
|
1165
|
+
if (settled) return;
|
|
1166
|
+
settled = true;
|
|
1167
|
+
clearTimeout(timer);
|
|
1168
|
+
resolve3(value);
|
|
1169
|
+
},
|
|
1170
|
+
(err) => {
|
|
1171
|
+
if (settled) return;
|
|
1172
|
+
settled = true;
|
|
1173
|
+
clearTimeout(timer);
|
|
1174
|
+
reject(err);
|
|
294
1175
|
}
|
|
295
|
-
|
|
296
|
-
});
|
|
297
|
-
}
|
|
1176
|
+
);
|
|
298
1177
|
});
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
return new Octokit({ auth: authentication.token });
|
|
1178
|
+
p.catch(() => {
|
|
1179
|
+
});
|
|
1180
|
+
return p;
|
|
303
1181
|
}
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
1182
|
+
|
|
1183
|
+
// src/providers/opencode.ts
|
|
1184
|
+
var SESSION_READY_TIMEOUT_MS = 6e5;
|
|
1185
|
+
async function boot(opts) {
|
|
1186
|
+
let client;
|
|
1187
|
+
let stopServer;
|
|
1188
|
+
let cleaned = false;
|
|
1189
|
+
if (opts?.url) {
|
|
1190
|
+
log.debug(`Connecting to existing OpenCode server at ${opts.url}`);
|
|
1191
|
+
client = createOpencodeClient({ baseUrl: opts.url });
|
|
1192
|
+
} else {
|
|
1193
|
+
log.debug("No --server-url provided, spawning local OpenCode server...");
|
|
1194
|
+
if (opts?.cwd) {
|
|
1195
|
+
log.debug(`Requested cwd "${opts.cwd}" \u2014 OpenCode SDK does not support spawn-level cwd; relying on prompt-level cwd`);
|
|
313
1196
|
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
if (authPromptHandler) {
|
|
323
|
-
authPromptHandler(msg);
|
|
324
|
-
} else {
|
|
325
|
-
log.info(msg);
|
|
326
|
-
}
|
|
327
|
-
open(deviceCodeInfo.verificationUri).catch(() => {
|
|
328
|
-
});
|
|
1197
|
+
try {
|
|
1198
|
+
const oc = await createOpencode({ port: 0 });
|
|
1199
|
+
client = oc.client;
|
|
1200
|
+
stopServer = () => oc.server.close();
|
|
1201
|
+
log.debug("OpenCode server started successfully");
|
|
1202
|
+
} catch (err) {
|
|
1203
|
+
log.debug(`Failed to start OpenCode server: ${log.formatErrorChain(err)}`);
|
|
1204
|
+
throw err;
|
|
329
1205
|
}
|
|
330
|
-
});
|
|
331
|
-
const accessToken = await credential.getToken(AZURE_DEVOPS_SCOPE);
|
|
332
|
-
if (!accessToken) {
|
|
333
|
-
throw new Error(
|
|
334
|
-
"Azure device-code authentication did not return a token. Please try again."
|
|
335
|
-
);
|
|
336
1206
|
}
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
}
|
|
347
|
-
async function ensureAuthReady(source, cwd, org) {
|
|
348
|
-
if (source === "github") {
|
|
349
|
-
const remoteUrl = await getGitRemoteUrl(cwd);
|
|
350
|
-
if (remoteUrl && parseGitHubRemoteUrl(remoteUrl)) {
|
|
351
|
-
await getGithubOctokit();
|
|
352
|
-
} else if (!remoteUrl) {
|
|
353
|
-
log.warn("No git remote found \u2014 skipping GitHub pre-authentication");
|
|
1207
|
+
let modelOverride;
|
|
1208
|
+
if (opts?.model) {
|
|
1209
|
+
const slash = opts.model.indexOf("/");
|
|
1210
|
+
if (slash > 0) {
|
|
1211
|
+
modelOverride = {
|
|
1212
|
+
providerID: opts.model.slice(0, slash),
|
|
1213
|
+
modelID: opts.model.slice(slash + 1)
|
|
1214
|
+
};
|
|
1215
|
+
log.debug(`Model override: ${opts.model}`);
|
|
354
1216
|
} else {
|
|
355
|
-
log.
|
|
356
|
-
}
|
|
357
|
-
} else if (source === "azdevops") {
|
|
358
|
-
let orgUrl = org;
|
|
359
|
-
if (!orgUrl) {
|
|
360
|
-
const remoteUrl = await getGitRemoteUrl(cwd);
|
|
361
|
-
if (remoteUrl) {
|
|
362
|
-
const parsed = parseAzDevOpsRemoteUrl(remoteUrl);
|
|
363
|
-
if (parsed) orgUrl = parsed.orgUrl;
|
|
364
|
-
else log.warn("Remote URL is not an Azure DevOps repository \u2014 skipping Azure pre-authentication");
|
|
365
|
-
} else {
|
|
366
|
-
log.warn("No git remote found \u2014 skipping Azure DevOps pre-authentication");
|
|
367
|
-
}
|
|
1217
|
+
log.debug(`Ignoring model override "${opts.model}": must be in "provider/model" format`);
|
|
368
1218
|
}
|
|
369
|
-
if (orgUrl) await getAzureConnection(orgUrl);
|
|
370
1219
|
}
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
* Request options that lead to the error.
|
|
382
|
-
*/
|
|
383
|
-
request;
|
|
384
|
-
/**
|
|
385
|
-
* Response object if a response was received
|
|
386
|
-
*/
|
|
387
|
-
response;
|
|
388
|
-
constructor(message, statusCode, options) {
|
|
389
|
-
super(message);
|
|
390
|
-
this.name = "HttpError";
|
|
391
|
-
this.status = Number.parseInt(statusCode);
|
|
392
|
-
if (Number.isNaN(this.status)) {
|
|
393
|
-
this.status = 0;
|
|
394
|
-
}
|
|
395
|
-
if ("response" in options) {
|
|
396
|
-
this.response = options.response;
|
|
397
|
-
}
|
|
398
|
-
const requestCopy = Object.assign({}, options.request);
|
|
399
|
-
if (options.request.headers.authorization) {
|
|
400
|
-
requestCopy.headers = Object.assign({}, options.request.headers, {
|
|
401
|
-
authorization: options.request.headers.authorization.replace(
|
|
402
|
-
/(?<! ) .*$/,
|
|
403
|
-
" [REDACTED]"
|
|
404
|
-
)
|
|
405
|
-
});
|
|
406
|
-
}
|
|
407
|
-
requestCopy.url = requestCopy.url.replace(/\bclient_secret=\w+/g, "client_secret=[REDACTED]").replace(/\baccess_token=\w+/g, "access_token=[REDACTED]");
|
|
408
|
-
this.request = requestCopy;
|
|
409
|
-
}
|
|
410
|
-
};
|
|
411
|
-
|
|
412
|
-
// src/datasources/github.ts
|
|
413
|
-
var exec = promisify(execFile);
|
|
414
|
-
async function git(args, cwd) {
|
|
415
|
-
const { stdout } = await exec("git", args, { cwd, shell: process.platform === "win32" });
|
|
416
|
-
return stdout;
|
|
417
|
-
}
|
|
418
|
-
function redactUrl(url) {
|
|
419
|
-
return url.replace(/\/\/[^@/]+@/, "//***@");
|
|
420
|
-
}
|
|
421
|
-
var ownerRepoCache = /* @__PURE__ */ new Map();
|
|
422
|
-
async function getOwnerRepo(cwd) {
|
|
423
|
-
const cached = ownerRepoCache.get(cwd);
|
|
424
|
-
if (cached) return cached;
|
|
425
|
-
const remoteUrl = await getGitRemoteUrl(cwd);
|
|
426
|
-
if (!remoteUrl) {
|
|
427
|
-
throw new Error("Could not determine git remote URL. Is this a git repository with an origin remote?");
|
|
428
|
-
}
|
|
429
|
-
const parsed = parseGitHubRemoteUrl(remoteUrl);
|
|
430
|
-
if (!parsed) {
|
|
431
|
-
throw new Error(`Could not parse GitHub owner/repo from remote URL: ${redactUrl(remoteUrl)}`);
|
|
432
|
-
}
|
|
433
|
-
ownerRepoCache.set(cwd, parsed);
|
|
434
|
-
return parsed;
|
|
435
|
-
}
|
|
436
|
-
function buildBranchName(issueNumber, _title, username = "unknown") {
|
|
437
|
-
return `${username}/dispatch/issue-${issueNumber}`;
|
|
438
|
-
}
|
|
439
|
-
async function getDefaultBranch(cwd) {
|
|
440
|
-
const PREFIX = "refs/remotes/origin/";
|
|
441
|
-
try {
|
|
442
|
-
const ref = await git(["symbolic-ref", "refs/remotes/origin/HEAD"], cwd);
|
|
443
|
-
const trimmed = ref.trim();
|
|
444
|
-
const branch = trimmed.startsWith(PREFIX) ? trimmed.slice(PREFIX.length) : trimmed;
|
|
445
|
-
if (!isValidBranchName(branch)) {
|
|
446
|
-
throw new InvalidBranchNameError(branch, "from symbolic-ref output");
|
|
447
|
-
}
|
|
448
|
-
return branch;
|
|
449
|
-
} catch (err) {
|
|
450
|
-
if (err instanceof InvalidBranchNameError) {
|
|
451
|
-
throw err;
|
|
452
|
-
}
|
|
453
|
-
try {
|
|
454
|
-
await git(["rev-parse", "--verify", "main"], cwd);
|
|
455
|
-
return "main";
|
|
456
|
-
} catch {
|
|
457
|
-
return "master";
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
var datasource = {
|
|
462
|
-
name: "github",
|
|
463
|
-
supportsGit() {
|
|
464
|
-
return true;
|
|
465
|
-
},
|
|
466
|
-
async list(opts = {}) {
|
|
467
|
-
const cwd = opts.cwd || process.cwd();
|
|
468
|
-
const { owner, repo } = await getOwnerRepo(cwd);
|
|
469
|
-
const octokit = await getGithubOctokit();
|
|
470
|
-
const issues = await octokit.paginate(
|
|
471
|
-
octokit.rest.issues.listForRepo,
|
|
472
|
-
{
|
|
473
|
-
owner,
|
|
474
|
-
repo,
|
|
475
|
-
state: "open"
|
|
476
|
-
}
|
|
477
|
-
);
|
|
478
|
-
return issues.filter((issue) => !issue.pull_request).map((issue) => ({
|
|
479
|
-
number: String(issue.number),
|
|
480
|
-
title: issue.title ?? "",
|
|
481
|
-
body: issue.body ?? "",
|
|
482
|
-
labels: (issue.labels ?? []).map((l) => typeof l === "string" ? l : l.name ?? "").filter(Boolean),
|
|
483
|
-
state: issue.state ?? "open",
|
|
484
|
-
url: issue.html_url ?? "",
|
|
485
|
-
comments: [],
|
|
486
|
-
acceptanceCriteria: ""
|
|
487
|
-
}));
|
|
488
|
-
},
|
|
489
|
-
async fetch(issueId, opts = {}) {
|
|
490
|
-
const cwd = opts.cwd || process.cwd();
|
|
491
|
-
const { owner, repo } = await getOwnerRepo(cwd);
|
|
492
|
-
const octokit = await getGithubOctokit();
|
|
493
|
-
const { data: issue } = await octokit.rest.issues.get({
|
|
494
|
-
owner,
|
|
495
|
-
repo,
|
|
496
|
-
issue_number: Number(issueId)
|
|
497
|
-
});
|
|
498
|
-
const issueComments = await octokit.paginate(
|
|
499
|
-
octokit.rest.issues.listComments,
|
|
500
|
-
{
|
|
501
|
-
owner,
|
|
502
|
-
repo,
|
|
503
|
-
issue_number: Number(issueId)
|
|
504
|
-
}
|
|
505
|
-
);
|
|
506
|
-
const comments = issueComments.map(
|
|
507
|
-
(c) => `**${c.user?.login ?? "unknown"}:** ${c.body ?? ""}`
|
|
508
|
-
);
|
|
509
|
-
return {
|
|
510
|
-
number: String(issue.number),
|
|
511
|
-
title: issue.title ?? "",
|
|
512
|
-
body: issue.body ?? "",
|
|
513
|
-
labels: (issue.labels ?? []).map((l) => typeof l === "string" ? l : l.name ?? "").filter(Boolean),
|
|
514
|
-
state: issue.state ?? "open",
|
|
515
|
-
url: issue.html_url ?? "",
|
|
516
|
-
comments,
|
|
517
|
-
acceptanceCriteria: ""
|
|
518
|
-
};
|
|
519
|
-
},
|
|
520
|
-
async update(issueId, title, body, opts = {}) {
|
|
521
|
-
const cwd = opts.cwd || process.cwd();
|
|
522
|
-
const { owner, repo } = await getOwnerRepo(cwd);
|
|
523
|
-
const octokit = await getGithubOctokit();
|
|
524
|
-
await octokit.rest.issues.update({
|
|
525
|
-
owner,
|
|
526
|
-
repo,
|
|
527
|
-
issue_number: Number(issueId),
|
|
528
|
-
title,
|
|
529
|
-
body
|
|
530
|
-
});
|
|
531
|
-
},
|
|
532
|
-
async close(issueId, opts = {}) {
|
|
533
|
-
const cwd = opts.cwd || process.cwd();
|
|
534
|
-
const { owner, repo } = await getOwnerRepo(cwd);
|
|
535
|
-
const octokit = await getGithubOctokit();
|
|
536
|
-
await octokit.rest.issues.update({
|
|
537
|
-
owner,
|
|
538
|
-
repo,
|
|
539
|
-
issue_number: Number(issueId),
|
|
540
|
-
state: "closed"
|
|
541
|
-
});
|
|
542
|
-
},
|
|
543
|
-
async create(title, body, opts = {}) {
|
|
544
|
-
const cwd = opts.cwd || process.cwd();
|
|
545
|
-
const { owner, repo } = await getOwnerRepo(cwd);
|
|
546
|
-
const octokit = await getGithubOctokit();
|
|
547
|
-
const { data: issue } = await octokit.rest.issues.create({
|
|
548
|
-
owner,
|
|
549
|
-
repo,
|
|
550
|
-
title,
|
|
551
|
-
body
|
|
552
|
-
});
|
|
553
|
-
return {
|
|
554
|
-
number: String(issue.number),
|
|
555
|
-
title: issue.title ?? "",
|
|
556
|
-
body: issue.body ?? "",
|
|
557
|
-
labels: (issue.labels ?? []).map((l) => typeof l === "string" ? l : l.name ?? "").filter(Boolean),
|
|
558
|
-
state: issue.state ?? "open",
|
|
559
|
-
url: issue.html_url ?? "",
|
|
560
|
-
comments: [],
|
|
561
|
-
acceptanceCriteria: ""
|
|
562
|
-
};
|
|
563
|
-
},
|
|
564
|
-
async getUsername(opts) {
|
|
565
|
-
if (opts.username) return opts.username;
|
|
566
|
-
return deriveShortUsername(opts.cwd, "unknown");
|
|
567
|
-
},
|
|
568
|
-
getDefaultBranch(opts) {
|
|
569
|
-
return getDefaultBranch(opts.cwd);
|
|
570
|
-
},
|
|
571
|
-
async getCurrentBranch(opts) {
|
|
572
|
-
try {
|
|
573
|
-
const branch = (await git(["rev-parse", "--abbrev-ref", "HEAD"], opts.cwd)).trim();
|
|
574
|
-
if (branch && branch !== "HEAD") return branch;
|
|
575
|
-
} catch {
|
|
576
|
-
}
|
|
577
|
-
return this.getDefaultBranch(opts);
|
|
578
|
-
},
|
|
579
|
-
buildBranchName(issueNumber, title, username) {
|
|
580
|
-
return buildBranchName(issueNumber, title, username ?? "unknown");
|
|
581
|
-
},
|
|
582
|
-
async createAndSwitchBranch(branchName, opts) {
|
|
583
|
-
const cwd = opts.cwd;
|
|
584
|
-
try {
|
|
585
|
-
await git(["checkout", "-b", branchName], cwd);
|
|
586
|
-
} catch (err) {
|
|
587
|
-
const message = log.extractMessage(err);
|
|
588
|
-
if (message.includes("already exists")) {
|
|
589
|
-
try {
|
|
590
|
-
await git(["checkout", branchName], cwd);
|
|
591
|
-
} catch (checkoutErr) {
|
|
592
|
-
const checkoutMessage = log.extractMessage(checkoutErr);
|
|
593
|
-
if (checkoutMessage.includes("already used by worktree")) {
|
|
594
|
-
await git(["worktree", "prune"], cwd);
|
|
595
|
-
await git(["checkout", branchName], cwd);
|
|
596
|
-
} else {
|
|
597
|
-
throw checkoutErr;
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
} else {
|
|
601
|
-
throw err;
|
|
602
|
-
}
|
|
603
|
-
}
|
|
604
|
-
},
|
|
605
|
-
async switchBranch(branchName, opts) {
|
|
606
|
-
await git(["checkout", branchName], opts.cwd);
|
|
607
|
-
},
|
|
608
|
-
async pushBranch(branchName, opts) {
|
|
609
|
-
await git(["push", "--set-upstream", "origin", branchName], opts.cwd);
|
|
610
|
-
},
|
|
611
|
-
async commitAllChanges(message, opts) {
|
|
612
|
-
const cwd = opts.cwd;
|
|
613
|
-
await git(["add", "-A"], cwd);
|
|
614
|
-
const status = await git(["diff", "--cached", "--stat"], cwd);
|
|
615
|
-
if (!status.trim()) {
|
|
616
|
-
return;
|
|
617
|
-
}
|
|
618
|
-
await git(["commit", "-m", message], cwd);
|
|
619
|
-
},
|
|
620
|
-
async createPullRequest(branchName, issueNumber, title, body, opts, baseBranch) {
|
|
621
|
-
const cwd = opts.cwd;
|
|
622
|
-
const { owner, repo } = await getOwnerRepo(cwd);
|
|
623
|
-
const octokit = await getGithubOctokit();
|
|
624
|
-
const prBody = body || `Closes #${issueNumber}`;
|
|
625
|
-
try {
|
|
626
|
-
const target = baseBranch ?? await getDefaultBranch(cwd);
|
|
627
|
-
const { data: pr } = await octokit.rest.pulls.create({
|
|
628
|
-
owner,
|
|
629
|
-
repo,
|
|
630
|
-
title,
|
|
631
|
-
body: prBody,
|
|
632
|
-
head: branchName,
|
|
633
|
-
base: target
|
|
634
|
-
});
|
|
635
|
-
return pr.html_url;
|
|
636
|
-
} catch (err) {
|
|
637
|
-
const isValidationError = err instanceof RequestError && err.status === 422;
|
|
638
|
-
if (isValidationError) {
|
|
639
|
-
const { data: prs } = await octokit.rest.pulls.list({
|
|
640
|
-
owner,
|
|
641
|
-
repo,
|
|
642
|
-
head: `${owner}:${branchName}`,
|
|
643
|
-
state: "open"
|
|
644
|
-
});
|
|
645
|
-
if (prs.length > 0) {
|
|
646
|
-
return prs[0].html_url;
|
|
647
|
-
}
|
|
648
|
-
}
|
|
649
|
-
throw err;
|
|
650
|
-
}
|
|
651
|
-
}
|
|
652
|
-
};
|
|
653
|
-
|
|
654
|
-
// src/datasources/azdevops.ts
|
|
655
|
-
import { execFile as execFile2 } from "child_process";
|
|
656
|
-
import { promisify as promisify2 } from "util";
|
|
657
|
-
import { PullRequestStatus } from "azure-devops-node-api/interfaces/GitInterfaces.js";
|
|
658
|
-
var exec2 = promisify2(execFile2);
|
|
659
|
-
var doneStateCache = /* @__PURE__ */ new Map();
|
|
660
|
-
async function git2(args, cwd) {
|
|
661
|
-
const { stdout } = await exec2("git", args, { cwd, shell: process.platform === "win32" });
|
|
662
|
-
return stdout;
|
|
663
|
-
}
|
|
664
|
-
function redactUrl2(url) {
|
|
665
|
-
return url.replace(/\/\/[^@/]+@/, "//***@");
|
|
666
|
-
}
|
|
667
|
-
async function getOrgAndProject(opts = {}) {
|
|
668
|
-
let orgUrl = opts.org;
|
|
669
|
-
let project = opts.project;
|
|
670
|
-
if (!orgUrl || !project) {
|
|
671
|
-
const cwd = opts.cwd || process.cwd();
|
|
672
|
-
const remoteUrl = await getGitRemoteUrl(cwd);
|
|
673
|
-
if (!remoteUrl) {
|
|
674
|
-
throw new Error(
|
|
675
|
-
"Could not determine git remote URL. Is this a git repository with an origin remote?"
|
|
676
|
-
);
|
|
677
|
-
}
|
|
678
|
-
const parsed = parseAzDevOpsRemoteUrl(remoteUrl);
|
|
679
|
-
if (!parsed) {
|
|
680
|
-
throw new Error(
|
|
681
|
-
`Could not parse Azure DevOps org/project from remote URL: ${redactUrl2(remoteUrl)}`
|
|
682
|
-
);
|
|
683
|
-
}
|
|
684
|
-
orgUrl = orgUrl || parsed.orgUrl;
|
|
685
|
-
project = project || parsed.project;
|
|
686
|
-
}
|
|
687
|
-
const connection = await getAzureConnection(orgUrl);
|
|
688
|
-
return { orgUrl, project, connection };
|
|
689
|
-
}
|
|
690
|
-
function mapWorkItemToIssueDetails(item, id, comments, defaults) {
|
|
691
|
-
const fields = item.fields ?? {};
|
|
692
|
-
return {
|
|
693
|
-
number: String(item.id ?? id),
|
|
694
|
-
title: fields["System.Title"] ?? defaults?.title ?? "",
|
|
695
|
-
body: fields["System.Description"] ?? defaults?.body ?? "",
|
|
696
|
-
labels: (fields["System.Tags"] ?? "").split(";").map((t) => t.trim()).filter(Boolean),
|
|
697
|
-
state: fields["System.State"] ?? defaults?.state ?? "",
|
|
698
|
-
url: item._links?.html?.href ?? item.url ?? "",
|
|
699
|
-
comments,
|
|
700
|
-
acceptanceCriteria: fields["Microsoft.VSTS.Common.AcceptanceCriteria"] ?? "",
|
|
701
|
-
iterationPath: fields["System.IterationPath"] || void 0,
|
|
702
|
-
areaPath: fields["System.AreaPath"] || void 0,
|
|
703
|
-
assignee: fields["System.AssignedTo"]?.displayName || void 0,
|
|
704
|
-
priority: fields["Microsoft.VSTS.Common.Priority"] ?? void 0,
|
|
705
|
-
storyPoints: fields["Microsoft.VSTS.Scheduling.StoryPoints"] ?? fields["Microsoft.VSTS.Scheduling.Effort"] ?? fields["Microsoft.VSTS.Scheduling.Size"] ?? void 0,
|
|
706
|
-
workItemType: fields["System.WorkItemType"] || defaults?.workItemType || void 0
|
|
707
|
-
};
|
|
708
|
-
}
|
|
709
|
-
async function detectWorkItemType(opts = {}) {
|
|
710
|
-
try {
|
|
711
|
-
const { project, connection } = await getOrgAndProject(opts);
|
|
712
|
-
const witApi = await connection.getWorkItemTrackingApi();
|
|
713
|
-
const types = await witApi.getWorkItemTypes(project);
|
|
714
|
-
if (!Array.isArray(types) || types.length === 0) return null;
|
|
715
|
-
const names = types.map((t) => t.name).filter((n) => !!n);
|
|
716
|
-
const preferred = ["User Story", "Product Backlog Item", "Requirement", "Issue"];
|
|
717
|
-
for (const p of preferred) {
|
|
718
|
-
if (names.includes(p)) return p;
|
|
719
|
-
}
|
|
720
|
-
return names[0] ?? null;
|
|
721
|
-
} catch {
|
|
722
|
-
return null;
|
|
723
|
-
}
|
|
724
|
-
}
|
|
725
|
-
async function detectDoneState(workItemType, opts = {}) {
|
|
726
|
-
const { orgUrl, project, connection } = await getOrgAndProject(opts);
|
|
727
|
-
const cacheKey = `${orgUrl}|${project}|${workItemType}`;
|
|
728
|
-
const cached = doneStateCache.get(cacheKey);
|
|
729
|
-
if (cached) return cached;
|
|
730
|
-
try {
|
|
731
|
-
const witApi = await connection.getWorkItemTrackingApi();
|
|
732
|
-
const states = await witApi.getWorkItemTypeStates(project, workItemType);
|
|
733
|
-
if (Array.isArray(states)) {
|
|
734
|
-
const completed = states.find((s) => s.category === "Completed");
|
|
735
|
-
if (completed?.name) {
|
|
736
|
-
doneStateCache.set(cacheKey, completed.name);
|
|
737
|
-
return completed.name;
|
|
738
|
-
}
|
|
739
|
-
const names = states.map((s) => s.name).filter((n) => !!n);
|
|
740
|
-
const fallbacks = ["Done", "Closed", "Resolved", "Completed"];
|
|
741
|
-
for (const f of fallbacks) {
|
|
742
|
-
if (names.includes(f)) {
|
|
743
|
-
doneStateCache.set(cacheKey, f);
|
|
744
|
-
return f;
|
|
745
|
-
}
|
|
746
|
-
}
|
|
747
|
-
}
|
|
748
|
-
} catch {
|
|
749
|
-
}
|
|
750
|
-
return "Closed";
|
|
751
|
-
}
|
|
752
|
-
async function fetchComments(workItemId, project, connection) {
|
|
753
|
-
try {
|
|
754
|
-
const witApi = await connection.getWorkItemTrackingApi();
|
|
755
|
-
const commentList = await witApi.getComments(project, workItemId);
|
|
756
|
-
if (commentList.comments && Array.isArray(commentList.comments)) {
|
|
757
|
-
return commentList.comments.map((c) => {
|
|
758
|
-
const author = c.createdBy?.displayName ?? "unknown";
|
|
759
|
-
return `**${author}:** ${c.text ?? ""}`;
|
|
760
|
-
});
|
|
761
|
-
}
|
|
762
|
-
return [];
|
|
763
|
-
} catch {
|
|
764
|
-
return [];
|
|
765
|
-
}
|
|
766
|
-
}
|
|
767
|
-
var datasource2 = {
|
|
768
|
-
name: "azdevops",
|
|
769
|
-
supportsGit() {
|
|
770
|
-
return true;
|
|
771
|
-
},
|
|
772
|
-
async list(opts = {}) {
|
|
773
|
-
const { project, connection } = await getOrgAndProject(opts);
|
|
774
|
-
const witApi = await connection.getWorkItemTrackingApi();
|
|
775
|
-
const conditions = [
|
|
776
|
-
"[System.State] <> 'Closed'",
|
|
777
|
-
"[System.State] <> 'Done'",
|
|
778
|
-
"[System.State] <> 'Removed'"
|
|
779
|
-
];
|
|
780
|
-
if (opts.iteration) {
|
|
781
|
-
const iterValue = String(opts.iteration).trim();
|
|
782
|
-
if (iterValue === "@CurrentIteration") {
|
|
783
|
-
conditions.push(`[System.IterationPath] UNDER @CurrentIteration`);
|
|
784
|
-
} else {
|
|
785
|
-
const escaped = iterValue.replace(/'/g, "''");
|
|
786
|
-
if (escaped) conditions.push(`[System.IterationPath] UNDER '${escaped}'`);
|
|
787
|
-
}
|
|
788
|
-
}
|
|
789
|
-
if (opts.area) {
|
|
790
|
-
const area = String(opts.area).trim().replace(/'/g, "''");
|
|
791
|
-
if (area) {
|
|
792
|
-
conditions.push(`[System.AreaPath] UNDER '${area}'`);
|
|
793
|
-
}
|
|
794
|
-
}
|
|
795
|
-
const wiql = `SELECT [System.Id] FROM workitems WHERE ${conditions.join(" AND ")} ORDER BY [System.CreatedDate] DESC`;
|
|
796
|
-
const queryResult = await witApi.queryByWiql({ query: wiql }, { project });
|
|
797
|
-
const workItemRefs = queryResult.workItems ?? [];
|
|
798
|
-
if (workItemRefs.length === 0) return [];
|
|
799
|
-
const ids = workItemRefs.map((ref) => ref.id).filter((id) => id != null);
|
|
800
|
-
if (ids.length === 0) return [];
|
|
801
|
-
try {
|
|
802
|
-
const items = await witApi.getWorkItems(ids);
|
|
803
|
-
const itemsArray = Array.isArray(items) ? items : [items];
|
|
804
|
-
const commentsArray = [];
|
|
805
|
-
const CONCURRENCY = 5;
|
|
806
|
-
for (let i = 0; i < itemsArray.length; i += CONCURRENCY) {
|
|
807
|
-
const batch = itemsArray.slice(i, i + CONCURRENCY);
|
|
808
|
-
const batchResults = await Promise.all(
|
|
809
|
-
// item.id is guaranteed non-null here: the ids array was built by filtering
|
|
810
|
-
// out null ids from workItemRefs, and itemsArray came from getWorkItems(ids).
|
|
811
|
-
batch.map((item) => fetchComments(item.id, project, connection))
|
|
812
|
-
);
|
|
813
|
-
commentsArray.push(...batchResults);
|
|
814
|
-
}
|
|
815
|
-
return itemsArray.map(
|
|
816
|
-
(item, i) => mapWorkItemToIssueDetails(item, String(item.id), commentsArray[i])
|
|
817
|
-
);
|
|
818
|
-
} catch (err) {
|
|
819
|
-
log.debug(`Batch getWorkItems failed, falling back to individual fetches: ${log.extractMessage(err)}`);
|
|
820
|
-
const results = await Promise.all(
|
|
821
|
-
ids.map((id) => datasource2.fetch(String(id), opts))
|
|
822
|
-
);
|
|
823
|
-
return results;
|
|
824
|
-
}
|
|
825
|
-
},
|
|
826
|
-
async fetch(issueId, opts = {}) {
|
|
827
|
-
const { project, connection } = await getOrgAndProject(opts);
|
|
828
|
-
const witApi = await connection.getWorkItemTrackingApi();
|
|
829
|
-
const item = await witApi.getWorkItem(Number(issueId));
|
|
830
|
-
const comments = await fetchComments(Number(issueId), project, connection);
|
|
831
|
-
return mapWorkItemToIssueDetails(item, issueId, comments);
|
|
832
|
-
},
|
|
833
|
-
async update(issueId, title, body, opts = {}) {
|
|
834
|
-
const { connection } = await getOrgAndProject(opts);
|
|
835
|
-
const witApi = await connection.getWorkItemTrackingApi();
|
|
836
|
-
const document = [
|
|
837
|
-
{ op: "add", path: "/fields/System.Title", value: title },
|
|
838
|
-
{ op: "add", path: "/fields/System.Description", value: body }
|
|
839
|
-
];
|
|
840
|
-
await witApi.updateWorkItem(null, document, Number(issueId));
|
|
841
|
-
},
|
|
842
|
-
async close(issueId, opts = {}) {
|
|
843
|
-
const { connection } = await getOrgAndProject(opts);
|
|
844
|
-
const witApi = await connection.getWorkItemTrackingApi();
|
|
845
|
-
let workItemType = opts.workItemType;
|
|
846
|
-
if (!workItemType) {
|
|
847
|
-
const item = await witApi.getWorkItem(Number(issueId));
|
|
848
|
-
workItemType = item.fields?.["System.WorkItemType"] ?? void 0;
|
|
849
|
-
}
|
|
850
|
-
const state = workItemType ? await detectDoneState(workItemType, opts) : "Closed";
|
|
851
|
-
const document = [
|
|
852
|
-
{ op: "add", path: "/fields/System.State", value: state }
|
|
853
|
-
];
|
|
854
|
-
await witApi.updateWorkItem(null, document, Number(issueId));
|
|
855
|
-
},
|
|
856
|
-
async create(title, body, opts = {}) {
|
|
857
|
-
const workItemType = opts.workItemType ?? await detectWorkItemType(opts);
|
|
858
|
-
if (!workItemType) {
|
|
859
|
-
throw new Error(
|
|
860
|
-
"Could not determine work item type. Set workItemType in your config (for example via `dispatch config`)."
|
|
861
|
-
);
|
|
862
|
-
}
|
|
863
|
-
const { project, connection } = await getOrgAndProject(opts);
|
|
864
|
-
const witApi = await connection.getWorkItemTrackingApi();
|
|
865
|
-
const document = [
|
|
866
|
-
{ op: "add", path: "/fields/System.Title", value: title },
|
|
867
|
-
{ op: "add", path: "/fields/System.Description", value: body }
|
|
868
|
-
];
|
|
869
|
-
const item = await witApi.createWorkItem(
|
|
870
|
-
// null as any: SDK customHeaders param — passing null is the documented way to omit it.
|
|
871
|
-
null,
|
|
872
|
-
document,
|
|
873
|
-
project,
|
|
874
|
-
workItemType
|
|
875
|
-
);
|
|
876
|
-
return mapWorkItemToIssueDetails(item, String(item.id), [], {
|
|
877
|
-
title,
|
|
878
|
-
body,
|
|
879
|
-
state: "New",
|
|
880
|
-
workItemType
|
|
881
|
-
});
|
|
882
|
-
},
|
|
883
|
-
async getDefaultBranch(opts) {
|
|
884
|
-
const PREFIX = "refs/remotes/origin/";
|
|
885
|
-
try {
|
|
886
|
-
const ref = await git2(["symbolic-ref", "refs/remotes/origin/HEAD"], opts.cwd);
|
|
887
|
-
const trimmed = ref.trim();
|
|
888
|
-
const branch = trimmed.startsWith(PREFIX) ? trimmed.slice(PREFIX.length) : trimmed;
|
|
889
|
-
if (!isValidBranchName(branch)) {
|
|
890
|
-
throw new InvalidBranchNameError(branch, "from symbolic-ref output");
|
|
891
|
-
}
|
|
892
|
-
return branch;
|
|
893
|
-
} catch (err) {
|
|
894
|
-
if (err instanceof InvalidBranchNameError) {
|
|
895
|
-
throw err;
|
|
896
|
-
}
|
|
897
|
-
try {
|
|
898
|
-
await git2(["rev-parse", "--verify", "main"], opts.cwd);
|
|
899
|
-
return "main";
|
|
900
|
-
} catch {
|
|
901
|
-
return "master";
|
|
902
|
-
}
|
|
903
|
-
}
|
|
904
|
-
},
|
|
905
|
-
async getCurrentBranch(opts) {
|
|
906
|
-
try {
|
|
907
|
-
const branch = (await git2(["rev-parse", "--abbrev-ref", "HEAD"], opts.cwd)).trim();
|
|
908
|
-
if (branch && branch !== "HEAD") return branch;
|
|
909
|
-
} catch {
|
|
910
|
-
}
|
|
911
|
-
return this.getDefaultBranch(opts);
|
|
912
|
-
},
|
|
913
|
-
async getUsername(opts) {
|
|
914
|
-
if (opts.username) return opts.username;
|
|
915
|
-
return deriveShortUsername(opts.cwd, "unknown");
|
|
916
|
-
},
|
|
917
|
-
buildBranchName(issueNumber, _title, username) {
|
|
918
|
-
const branch = `${username}/dispatch/issue-${issueNumber}`;
|
|
919
|
-
if (!isValidBranchName(branch)) {
|
|
920
|
-
throw new InvalidBranchNameError(branch);
|
|
921
|
-
}
|
|
922
|
-
return branch;
|
|
923
|
-
},
|
|
924
|
-
async createAndSwitchBranch(branchName, opts) {
|
|
925
|
-
if (!isValidBranchName(branchName)) {
|
|
926
|
-
throw new InvalidBranchNameError(branchName);
|
|
927
|
-
}
|
|
928
|
-
try {
|
|
929
|
-
await git2(["checkout", "-b", branchName], opts.cwd);
|
|
930
|
-
} catch (err) {
|
|
931
|
-
const message = log.extractMessage(err);
|
|
932
|
-
if (message.includes("already exists")) {
|
|
933
|
-
try {
|
|
934
|
-
await git2(["checkout", branchName], opts.cwd);
|
|
935
|
-
} catch (checkoutErr) {
|
|
936
|
-
const checkoutMessage = log.extractMessage(checkoutErr);
|
|
937
|
-
if (checkoutMessage.includes("already used by worktree")) {
|
|
938
|
-
await git2(["worktree", "prune"], opts.cwd);
|
|
939
|
-
await git2(["checkout", branchName], opts.cwd);
|
|
940
|
-
} else {
|
|
941
|
-
throw checkoutErr;
|
|
942
|
-
}
|
|
943
|
-
}
|
|
944
|
-
} else {
|
|
945
|
-
throw err;
|
|
946
|
-
}
|
|
947
|
-
}
|
|
948
|
-
},
|
|
949
|
-
async switchBranch(branchName, opts) {
|
|
950
|
-
await git2(["checkout", branchName], opts.cwd);
|
|
951
|
-
},
|
|
952
|
-
async pushBranch(branchName, opts) {
|
|
953
|
-
await git2(["push", "--set-upstream", "origin", branchName], opts.cwd);
|
|
954
|
-
},
|
|
955
|
-
async commitAllChanges(message, opts) {
|
|
956
|
-
await git2(["add", "-A"], opts.cwd);
|
|
957
|
-
const status = await git2(["diff", "--cached", "--stat"], opts.cwd);
|
|
958
|
-
if (!status.trim()) {
|
|
959
|
-
return;
|
|
960
|
-
}
|
|
961
|
-
await git2(["commit", "-m", message], opts.cwd);
|
|
962
|
-
},
|
|
963
|
-
async createPullRequest(branchName, issueNumber, title, body, opts, baseBranch) {
|
|
964
|
-
const cwd = opts.cwd;
|
|
965
|
-
const { orgUrl, project, connection } = await getOrgAndProject(opts);
|
|
966
|
-
const gitApi = await connection.getGitApi();
|
|
967
|
-
const remoteUrl = await getGitRemoteUrl(cwd);
|
|
968
|
-
if (!remoteUrl) {
|
|
969
|
-
throw new Error("Could not determine git remote URL.");
|
|
970
|
-
}
|
|
971
|
-
const repos = await gitApi.getRepositories(project);
|
|
972
|
-
const normalizeUrl = (u) => u.replace(/\/\/[^@/]+@/, "//").replace(/\.git$/, "").replace(/\/$/, "").toLowerCase();
|
|
973
|
-
const normalizedRemote = normalizeUrl(remoteUrl);
|
|
974
|
-
const repo = repos.find(
|
|
975
|
-
(r) => r.remoteUrl && normalizeUrl(r.remoteUrl) === normalizedRemote || r.sshUrl && normalizeUrl(r.sshUrl) === normalizedRemote || r.webUrl && normalizeUrl(r.webUrl) === normalizedRemote
|
|
976
|
-
);
|
|
977
|
-
if (!repo || !repo.id) {
|
|
978
|
-
throw new Error(`Could not find Azure DevOps repository matching remote URL: ${redactUrl2(remoteUrl)}`);
|
|
979
|
-
}
|
|
980
|
-
const target = baseBranch ?? await this.getDefaultBranch(opts);
|
|
981
|
-
try {
|
|
982
|
-
const pr = await gitApi.createPullRequest(
|
|
983
|
-
{
|
|
984
|
-
sourceRefName: `refs/heads/${branchName}`,
|
|
985
|
-
targetRefName: `refs/heads/${target}`,
|
|
986
|
-
title,
|
|
987
|
-
description: body || `Resolves AB#${issueNumber}`,
|
|
988
|
-
workItemRefs: [{ id: issueNumber }]
|
|
989
|
-
},
|
|
990
|
-
repo.id,
|
|
991
|
-
project
|
|
992
|
-
);
|
|
993
|
-
const webUrl = repo.webUrl ? `${repo.webUrl}/pullrequest/${pr.pullRequestId}` : pr.url ?? "";
|
|
994
|
-
return webUrl;
|
|
995
|
-
} catch (err) {
|
|
996
|
-
const message = log.extractMessage(err);
|
|
997
|
-
if (message.includes("already exists")) {
|
|
998
|
-
const prs = await gitApi.getPullRequests(
|
|
999
|
-
repo.id,
|
|
1000
|
-
{
|
|
1001
|
-
sourceRefName: `refs/heads/${branchName}`,
|
|
1002
|
-
status: PullRequestStatus.Active
|
|
1003
|
-
},
|
|
1004
|
-
project
|
|
1005
|
-
);
|
|
1006
|
-
if (Array.isArray(prs) && prs.length > 0) {
|
|
1007
|
-
const existingPr = prs[0];
|
|
1008
|
-
const webUrl = repo.webUrl ? `${repo.webUrl}/pullrequest/${existingPr.pullRequestId}` : existingPr.url ?? "";
|
|
1009
|
-
return webUrl;
|
|
1010
|
-
}
|
|
1011
|
-
return "";
|
|
1012
|
-
}
|
|
1013
|
-
throw err;
|
|
1014
|
-
}
|
|
1015
|
-
}
|
|
1016
|
-
};
|
|
1017
|
-
|
|
1018
|
-
// src/datasources/md.ts
|
|
1019
|
-
import { execFile as execFile5 } from "child_process";
|
|
1020
|
-
import { readFile as readFile5, writeFile as writeFile3, readdir, mkdir as mkdir3, rename } from "fs/promises";
|
|
1021
|
-
import { basename, dirname as dirname4, isAbsolute, join as join6, parse as parsePath, resolve } from "path";
|
|
1022
|
-
import { promisify as promisify5 } from "util";
|
|
1023
|
-
import { glob } from "glob";
|
|
1024
|
-
|
|
1025
|
-
// src/helpers/slugify.ts
|
|
1026
|
-
var MAX_SLUG_LENGTH = 60;
|
|
1027
|
-
function slugify(input2, maxLength) {
|
|
1028
|
-
const slug = input2.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
1029
|
-
return maxLength != null ? slug.slice(0, maxLength) : slug;
|
|
1030
|
-
}
|
|
1031
|
-
|
|
1032
|
-
// src/config.ts
|
|
1033
|
-
import { readFile as readFile4, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
|
|
1034
|
-
import { join as join5, dirname as dirname3 } from "path";
|
|
1035
|
-
|
|
1036
|
-
// src/providers/interface.ts
|
|
1037
|
-
var PROVIDER_NAMES = ["opencode", "copilot", "claude", "codex"];
|
|
1038
|
-
|
|
1039
|
-
// src/providers/opencode.ts
|
|
1040
|
-
import { execFile as execFile3 } from "child_process";
|
|
1041
|
-
import { promisify as promisify3 } from "util";
|
|
1042
|
-
import {
|
|
1043
|
-
createOpencode,
|
|
1044
|
-
createOpencodeClient
|
|
1045
|
-
} from "@opencode-ai/sdk";
|
|
1046
|
-
|
|
1047
|
-
// src/providers/progress.ts
|
|
1048
|
-
var ANSI_PATTERN = /(?:\u001B\[[0-?]*[ -/]*[@-~]|\u009B[0-?]*[ -/]*[@-~]|\u001B\][^\u0007]*(?:\u0007|\u001B\\))/g;
|
|
1049
|
-
var CONTROL_PATTERN = /[\u0000-\u0008\u000B-\u001F\u007F]/g;
|
|
1050
|
-
function sanitizeProgressText(raw, maxLength = 120) {
|
|
1051
|
-
const text = raw.replace(ANSI_PATTERN, "").replace(CONTROL_PATTERN, "").replace(/\s+/g, " ").trim();
|
|
1052
|
-
if (!text) return "";
|
|
1053
|
-
if (text.length <= maxLength) return text;
|
|
1054
|
-
if (maxLength <= 1) return maxLength <= 0 ? "" : "\u2026";
|
|
1055
|
-
return `${text.slice(0, maxLength - 1).trimEnd()}\u2026`;
|
|
1056
|
-
}
|
|
1057
|
-
function createProgressReporter(onProgress) {
|
|
1058
|
-
let last;
|
|
1059
|
-
return {
|
|
1060
|
-
emit(raw) {
|
|
1061
|
-
if (!onProgress) return;
|
|
1062
|
-
const text = sanitizeProgressText(raw ?? "");
|
|
1063
|
-
if (!text || text === last) return;
|
|
1064
|
-
last = text;
|
|
1065
|
-
try {
|
|
1066
|
-
onProgress({ text });
|
|
1067
|
-
} catch {
|
|
1068
|
-
}
|
|
1069
|
-
},
|
|
1070
|
-
reset() {
|
|
1071
|
-
last = void 0;
|
|
1072
|
-
}
|
|
1073
|
-
};
|
|
1074
|
-
}
|
|
1075
|
-
|
|
1076
|
-
// src/helpers/guards.ts
|
|
1077
|
-
function hasProperty(value, key) {
|
|
1078
|
-
return typeof value === "object" && value !== null && Object.prototype.hasOwnProperty.call(value, key);
|
|
1079
|
-
}
|
|
1080
|
-
|
|
1081
|
-
// src/helpers/timeout.ts
|
|
1082
|
-
var TimeoutError = class extends Error {
|
|
1083
|
-
/** Optional label identifying the operation that timed out. */
|
|
1084
|
-
label;
|
|
1085
|
-
constructor(ms, label) {
|
|
1086
|
-
const suffix = label ? ` [${label}]` : "";
|
|
1087
|
-
super(`Timed out after ${ms}ms${suffix}`);
|
|
1088
|
-
this.name = "TimeoutError";
|
|
1089
|
-
this.label = label;
|
|
1090
|
-
}
|
|
1091
|
-
};
|
|
1092
|
-
var DEFAULT_PLAN_TIMEOUT_MIN = 30;
|
|
1093
|
-
function withTimeout(promise, ms, label) {
|
|
1094
|
-
const p = new Promise((resolve3, reject) => {
|
|
1095
|
-
let settled = false;
|
|
1096
|
-
const timer = setTimeout(() => {
|
|
1097
|
-
if (settled) return;
|
|
1098
|
-
settled = true;
|
|
1099
|
-
reject(new TimeoutError(ms, label));
|
|
1100
|
-
}, ms);
|
|
1101
|
-
promise.then(
|
|
1102
|
-
(value) => {
|
|
1103
|
-
if (settled) return;
|
|
1104
|
-
settled = true;
|
|
1105
|
-
clearTimeout(timer);
|
|
1106
|
-
resolve3(value);
|
|
1107
|
-
},
|
|
1108
|
-
(err) => {
|
|
1109
|
-
if (settled) return;
|
|
1110
|
-
settled = true;
|
|
1111
|
-
clearTimeout(timer);
|
|
1112
|
-
reject(err);
|
|
1113
|
-
}
|
|
1114
|
-
);
|
|
1115
|
-
});
|
|
1116
|
-
p.catch(() => {
|
|
1117
|
-
});
|
|
1118
|
-
return p;
|
|
1119
|
-
}
|
|
1120
|
-
|
|
1121
|
-
// src/providers/opencode.ts
|
|
1122
|
-
var SESSION_READY_TIMEOUT_MS = 6e5;
|
|
1123
|
-
async function boot(opts) {
|
|
1124
|
-
let client;
|
|
1125
|
-
let stopServer;
|
|
1126
|
-
let cleaned = false;
|
|
1127
|
-
if (opts?.url) {
|
|
1128
|
-
log.debug(`Connecting to existing OpenCode server at ${opts.url}`);
|
|
1129
|
-
client = createOpencodeClient({ baseUrl: opts.url });
|
|
1130
|
-
} else {
|
|
1131
|
-
log.debug("No --server-url provided, spawning local OpenCode server...");
|
|
1132
|
-
if (opts?.cwd) {
|
|
1133
|
-
log.debug(`Requested cwd "${opts.cwd}" \u2014 OpenCode SDK does not support spawn-level cwd; relying on prompt-level cwd`);
|
|
1134
|
-
}
|
|
1135
|
-
try {
|
|
1136
|
-
const oc = await createOpencode({ port: 0 });
|
|
1137
|
-
client = oc.client;
|
|
1138
|
-
stopServer = () => oc.server.close();
|
|
1139
|
-
log.debug("OpenCode server started successfully");
|
|
1140
|
-
} catch (err) {
|
|
1141
|
-
log.debug(`Failed to start OpenCode server: ${log.formatErrorChain(err)}`);
|
|
1142
|
-
throw err;
|
|
1143
|
-
}
|
|
1144
|
-
}
|
|
1145
|
-
let modelOverride;
|
|
1146
|
-
if (opts?.model) {
|
|
1147
|
-
const slash = opts.model.indexOf("/");
|
|
1148
|
-
if (slash > 0) {
|
|
1149
|
-
modelOverride = {
|
|
1150
|
-
providerID: opts.model.slice(0, slash),
|
|
1151
|
-
modelID: opts.model.slice(slash + 1)
|
|
1152
|
-
};
|
|
1153
|
-
log.debug(`Model override: ${opts.model}`);
|
|
1154
|
-
} else {
|
|
1155
|
-
log.debug(`Ignoring model override "${opts.model}": must be in "provider/model" format`);
|
|
1156
|
-
}
|
|
1157
|
-
}
|
|
1158
|
-
let model = opts?.model;
|
|
1159
|
-
if (!model) {
|
|
1160
|
-
try {
|
|
1161
|
-
const { data: config } = await client.config.get();
|
|
1162
|
-
if (config?.model) {
|
|
1163
|
-
model = config.model;
|
|
1164
|
-
log.debug(`Detected model: ${model}`);
|
|
1165
|
-
}
|
|
1166
|
-
} catch (err) {
|
|
1167
|
-
log.debug(`Failed to retrieve model from config: ${log.formatErrorChain(err)}`);
|
|
1220
|
+
let model = opts?.model;
|
|
1221
|
+
if (!model) {
|
|
1222
|
+
try {
|
|
1223
|
+
const { data: config } = await client.config.get();
|
|
1224
|
+
if (config?.model) {
|
|
1225
|
+
model = config.model;
|
|
1226
|
+
log.debug(`Detected model: ${model}`);
|
|
1227
|
+
}
|
|
1228
|
+
} catch (err) {
|
|
1229
|
+
log.debug(`Failed to retrieve model from config: ${log.formatErrorChain(err)}`);
|
|
1168
1230
|
}
|
|
1169
1231
|
}
|
|
1170
1232
|
return {
|
|
@@ -1440,9 +1502,9 @@ async function boot2(opts) {
|
|
|
1440
1502
|
|
|
1441
1503
|
// src/providers/claude.ts
|
|
1442
1504
|
import { randomUUID } from "crypto";
|
|
1443
|
-
import { readFile
|
|
1444
|
-
import { homedir
|
|
1445
|
-
import { join as
|
|
1505
|
+
import { readFile } from "fs/promises";
|
|
1506
|
+
import { homedir } from "os";
|
|
1507
|
+
import { join as join2 } from "path";
|
|
1446
1508
|
import { unstable_v2_createSession } from "@anthropic-ai/claude-agent-sdk";
|
|
1447
1509
|
var SESSION_READY_TIMEOUT_MS3 = 6e5;
|
|
1448
1510
|
async function boot3(opts) {
|
|
@@ -1495,14 +1557,130 @@ async function boot3(opts) {
|
|
|
1495
1557
|
}
|
|
1496
1558
|
}
|
|
1497
1559
|
}
|
|
1498
|
-
})(),
|
|
1499
|
-
SESSION_READY_TIMEOUT_MS3,
|
|
1500
|
-
"claude session stream"
|
|
1560
|
+
})(),
|
|
1561
|
+
SESSION_READY_TIMEOUT_MS3,
|
|
1562
|
+
"claude session stream"
|
|
1563
|
+
);
|
|
1564
|
+
if (!receivedAssistant) {
|
|
1565
|
+
throw new Error("Claude stream ended before receiving an assistant message");
|
|
1566
|
+
}
|
|
1567
|
+
const result = parts.join("") || null;
|
|
1568
|
+
log.debug(`Prompt response received (${result?.length ?? 0} chars)`);
|
|
1569
|
+
return result;
|
|
1570
|
+
} catch (err) {
|
|
1571
|
+
log.debug(`Prompt failed: ${log.formatErrorChain(err)}`);
|
|
1572
|
+
throw err;
|
|
1573
|
+
}
|
|
1574
|
+
},
|
|
1575
|
+
async send(sessionId, text) {
|
|
1576
|
+
const session = sessions.get(sessionId);
|
|
1577
|
+
if (!session) {
|
|
1578
|
+
throw new Error(`Claude session ${sessionId} not found`);
|
|
1579
|
+
}
|
|
1580
|
+
log.debug(`Sending follow-up to session ${sessionId} (${text.length} chars)...`);
|
|
1581
|
+
try {
|
|
1582
|
+
await session.send(text);
|
|
1583
|
+
} catch (err) {
|
|
1584
|
+
log.debug(`Follow-up send failed: ${log.formatErrorChain(err)}`);
|
|
1585
|
+
throw err;
|
|
1586
|
+
}
|
|
1587
|
+
},
|
|
1588
|
+
async cleanup() {
|
|
1589
|
+
log.debug("Cleaning up Claude provider...");
|
|
1590
|
+
for (const session of sessions.values()) {
|
|
1591
|
+
try {
|
|
1592
|
+
await Promise.resolve(session.close());
|
|
1593
|
+
} catch (err) {
|
|
1594
|
+
log.debug(`Failed to close Claude session: ${log.formatErrorChain(err)}`);
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
sessions.clear();
|
|
1598
|
+
}
|
|
1599
|
+
};
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
// src/providers/codex.ts
|
|
1603
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
1604
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
1605
|
+
import { homedir as homedir2 } from "os";
|
|
1606
|
+
import { join as join3 } from "path";
|
|
1607
|
+
var SESSION_READY_TIMEOUT_MS4 = 6e5;
|
|
1608
|
+
async function loadCodexSdk() {
|
|
1609
|
+
return import("@openai/codex-sdk");
|
|
1610
|
+
}
|
|
1611
|
+
async function boot4(opts) {
|
|
1612
|
+
const model = opts?.model ?? "o4-mini";
|
|
1613
|
+
log.debug(`Booting Codex provider with model ${model}`);
|
|
1614
|
+
const { Codex, Thread } = await loadCodexSdk();
|
|
1615
|
+
const codex = new Codex();
|
|
1616
|
+
const sessions = /* @__PURE__ */ new Map();
|
|
1617
|
+
return {
|
|
1618
|
+
name: "codex",
|
|
1619
|
+
model,
|
|
1620
|
+
async createSession() {
|
|
1621
|
+
log.debug("Creating Codex session...");
|
|
1622
|
+
try {
|
|
1623
|
+
const sessionId = randomUUID2();
|
|
1624
|
+
const thread = codex.startThread({
|
|
1625
|
+
model,
|
|
1626
|
+
approvalPolicy: "never",
|
|
1627
|
+
sandboxMode: "workspace-write",
|
|
1628
|
+
...opts?.cwd ? { workingDirectory: opts.cwd } : {}
|
|
1629
|
+
});
|
|
1630
|
+
sessions.set(sessionId, thread);
|
|
1631
|
+
log.debug(`Session created: ${sessionId}`);
|
|
1632
|
+
return sessionId;
|
|
1633
|
+
} catch (err) {
|
|
1634
|
+
log.debug(`Session creation failed: ${log.formatErrorChain(err)}`);
|
|
1635
|
+
throw err;
|
|
1636
|
+
}
|
|
1637
|
+
},
|
|
1638
|
+
async prompt(sessionId, text, options) {
|
|
1639
|
+
const thread = sessions.get(sessionId);
|
|
1640
|
+
if (!thread) {
|
|
1641
|
+
throw new Error(`Codex session ${sessionId} not found`);
|
|
1642
|
+
}
|
|
1643
|
+
log.debug(`Sending prompt to session ${sessionId} (${text.length} chars)...`);
|
|
1644
|
+
const reporter = createProgressReporter(options?.onProgress);
|
|
1645
|
+
try {
|
|
1646
|
+
reporter.emit("Waiting for Codex response");
|
|
1647
|
+
if (options?.onProgress) {
|
|
1648
|
+
const { events } = await withTimeout(
|
|
1649
|
+
thread.runStreamed(text),
|
|
1650
|
+
SESSION_READY_TIMEOUT_MS4,
|
|
1651
|
+
"codex thread runStreamed"
|
|
1652
|
+
);
|
|
1653
|
+
let lastAgentMessage = null;
|
|
1654
|
+
for await (const event of events) {
|
|
1655
|
+
if (event.type === "item.updated" || event.type === "item.completed") {
|
|
1656
|
+
if (event.item.type === "agent_message") {
|
|
1657
|
+
lastAgentMessage = event.item.text;
|
|
1658
|
+
reporter.emit(event.item.text);
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
if (event.type === "turn.failed") {
|
|
1662
|
+
throw new Error(`Codex turn failed: ${event.error.message}`);
|
|
1663
|
+
}
|
|
1664
|
+
if (event.type === "item.completed" && event.item.type === "error") {
|
|
1665
|
+
throw new Error(`Codex error: ${event.item.message}`);
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
reporter.emit("Finalizing response");
|
|
1669
|
+
log.debug(`Prompt response received (${lastAgentMessage?.length ?? 0} chars, streaming)`);
|
|
1670
|
+
return lastAgentMessage;
|
|
1671
|
+
}
|
|
1672
|
+
const turn = await withTimeout(
|
|
1673
|
+
thread.run(text),
|
|
1674
|
+
SESSION_READY_TIMEOUT_MS4,
|
|
1675
|
+
"codex thread run"
|
|
1501
1676
|
);
|
|
1502
|
-
|
|
1503
|
-
|
|
1677
|
+
for (const item of turn.items) {
|
|
1678
|
+
if (item.type === "error") {
|
|
1679
|
+
throw new Error(`Codex error: ${item.message}`);
|
|
1680
|
+
}
|
|
1504
1681
|
}
|
|
1505
|
-
|
|
1682
|
+
reporter.emit("Finalizing response");
|
|
1683
|
+
const result = turn.finalResponse || null;
|
|
1506
1684
|
log.debug(`Prompt response received (${result?.length ?? 0} chars)`);
|
|
1507
1685
|
return result;
|
|
1508
1686
|
} catch (err) {
|
|
@@ -1511,444 +1689,988 @@ async function boot3(opts) {
|
|
|
1511
1689
|
}
|
|
1512
1690
|
},
|
|
1513
1691
|
async send(sessionId, text) {
|
|
1514
|
-
const
|
|
1515
|
-
if (!
|
|
1516
|
-
throw new Error(`
|
|
1692
|
+
const thread = sessions.get(sessionId);
|
|
1693
|
+
if (!thread) {
|
|
1694
|
+
throw new Error(`Codex session ${sessionId} not found`);
|
|
1517
1695
|
}
|
|
1518
1696
|
log.debug(`Sending follow-up to session ${sessionId} (${text.length} chars)...`);
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
}
|
|
1522
|
-
log.debug(`Follow-up send failed: ${log.formatErrorChain(err)}`);
|
|
1523
|
-
throw err;
|
|
1524
|
-
}
|
|
1697
|
+
thread.run(text).catch((err) => {
|
|
1698
|
+
log.debug(`Follow-up turn failed: ${log.formatErrorChain(err)}`);
|
|
1699
|
+
});
|
|
1525
1700
|
},
|
|
1526
1701
|
async cleanup() {
|
|
1527
|
-
log.debug("Cleaning up
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1702
|
+
log.debug("Cleaning up Codex provider...");
|
|
1703
|
+
sessions.clear();
|
|
1704
|
+
}
|
|
1705
|
+
};
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
// src/providers/registry.ts
|
|
1709
|
+
import { execFile as execFile2 } from "child_process";
|
|
1710
|
+
import { promisify as promisify2 } from "util";
|
|
1711
|
+
var exec = promisify2(execFile2);
|
|
1712
|
+
var AUTH_PROBE_TIMEOUT_MS = 1e4;
|
|
1713
|
+
async function checkCopilotAuth() {
|
|
1714
|
+
if (process.env.COPILOT_GITHUB_TOKEN || process.env.GH_TOKEN || process.env.GITHUB_TOKEN) {
|
|
1715
|
+
return { status: "authenticated" };
|
|
1716
|
+
}
|
|
1717
|
+
try {
|
|
1718
|
+
await exec("copilot", ["auth", "status"], { timeout: AUTH_PROBE_TIMEOUT_MS });
|
|
1719
|
+
return { status: "authenticated" };
|
|
1720
|
+
} catch {
|
|
1721
|
+
try {
|
|
1722
|
+
await exec("gh", ["auth", "status"], { timeout: AUTH_PROBE_TIMEOUT_MS });
|
|
1723
|
+
return { status: "authenticated" };
|
|
1724
|
+
} catch {
|
|
1725
|
+
return {
|
|
1726
|
+
status: "not-configured",
|
|
1727
|
+
hint: "Run 'copilot login' or set GITHUB_TOKEN"
|
|
1728
|
+
};
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
async function checkClaudeAuth() {
|
|
1733
|
+
if (process.env.ANTHROPIC_API_KEY) {
|
|
1734
|
+
return { status: "authenticated" };
|
|
1735
|
+
}
|
|
1736
|
+
try {
|
|
1737
|
+
await exec("claude", ["auth", "status"], { timeout: AUTH_PROBE_TIMEOUT_MS });
|
|
1738
|
+
return { status: "authenticated" };
|
|
1739
|
+
} catch {
|
|
1740
|
+
return {
|
|
1741
|
+
status: "not-configured",
|
|
1742
|
+
hint: "Run 'claude auth login' or set ANTHROPIC_API_KEY"
|
|
1743
|
+
};
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
async function checkCodexAuth() {
|
|
1747
|
+
if (process.env.OPENAI_API_KEY) {
|
|
1748
|
+
return { status: "authenticated" };
|
|
1749
|
+
}
|
|
1750
|
+
try {
|
|
1751
|
+
await exec("codex", ["login", "status"], { timeout: AUTH_PROBE_TIMEOUT_MS });
|
|
1752
|
+
return { status: "authenticated" };
|
|
1753
|
+
} catch {
|
|
1754
|
+
return {
|
|
1755
|
+
status: "not-configured",
|
|
1756
|
+
hint: "Run 'codex login --device-auth' or set OPENAI_API_KEY"
|
|
1757
|
+
};
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
async function checkOpencodeAuth() {
|
|
1761
|
+
try {
|
|
1762
|
+
await exec("opencode", ["--version"], { timeout: AUTH_PROBE_TIMEOUT_MS });
|
|
1763
|
+
} catch {
|
|
1764
|
+
return {
|
|
1765
|
+
status: "not-configured",
|
|
1766
|
+
hint: "Install OpenCode (https://opencode.ai)"
|
|
1767
|
+
};
|
|
1768
|
+
}
|
|
1769
|
+
try {
|
|
1770
|
+
await exec("opencode", ["auth", "list"], { timeout: AUTH_PROBE_TIMEOUT_MS });
|
|
1771
|
+
return { status: "authenticated" };
|
|
1772
|
+
} catch {
|
|
1773
|
+
return {
|
|
1774
|
+
status: "not-configured",
|
|
1775
|
+
hint: "OpenCode is installed but no credentials configured. Run 'opencode auth login' or set provider API keys"
|
|
1776
|
+
};
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
var PROVIDER_REGISTRY = {
|
|
1780
|
+
copilot: {
|
|
1781
|
+
name: "copilot",
|
|
1782
|
+
displayName: "GitHub Copilot",
|
|
1783
|
+
tier: "free",
|
|
1784
|
+
defaultStrongModel: "claude-sonnet-4-5",
|
|
1785
|
+
defaultFastModel: "claude-haiku-4",
|
|
1786
|
+
costScore: { strong: 1, fast: 1 },
|
|
1787
|
+
checkAuth: checkCopilotAuth
|
|
1788
|
+
},
|
|
1789
|
+
claude: {
|
|
1790
|
+
name: "claude",
|
|
1791
|
+
displayName: "Claude Code",
|
|
1792
|
+
tier: "api-key",
|
|
1793
|
+
defaultStrongModel: "claude-sonnet-4",
|
|
1794
|
+
defaultFastModel: "claude-haiku-3-5",
|
|
1795
|
+
costScore: { strong: 5, fast: 2 },
|
|
1796
|
+
checkAuth: checkClaudeAuth
|
|
1797
|
+
},
|
|
1798
|
+
codex: {
|
|
1799
|
+
name: "codex",
|
|
1800
|
+
displayName: "OpenAI Codex",
|
|
1801
|
+
tier: "api-key",
|
|
1802
|
+
defaultStrongModel: "o4-mini",
|
|
1803
|
+
defaultFastModel: "codex-mini-latest",
|
|
1804
|
+
costScore: { strong: 4, fast: 1 },
|
|
1805
|
+
checkAuth: checkCodexAuth
|
|
1806
|
+
},
|
|
1807
|
+
opencode: {
|
|
1808
|
+
name: "opencode",
|
|
1809
|
+
displayName: "OpenCode",
|
|
1810
|
+
tier: "api-key",
|
|
1811
|
+
defaultStrongModel: "anthropic/claude-sonnet-4",
|
|
1812
|
+
defaultFastModel: "anthropic/claude-haiku-3-5",
|
|
1813
|
+
costScore: { strong: 5, fast: 3 },
|
|
1814
|
+
checkAuth: checkOpencodeAuth
|
|
1815
|
+
}
|
|
1816
|
+
};
|
|
1817
|
+
|
|
1818
|
+
// src/providers/detect.ts
|
|
1819
|
+
async function checkProviderAuthenticated(name) {
|
|
1820
|
+
const meta = PROVIDER_REGISTRY[name];
|
|
1821
|
+
const status = await meta.checkAuth();
|
|
1822
|
+
return status.status === "authenticated";
|
|
1823
|
+
}
|
|
1824
|
+
async function getAuthenticatedProviders(providers) {
|
|
1825
|
+
const results = await Promise.all(
|
|
1826
|
+
providers.map(async (name) => ({
|
|
1827
|
+
name,
|
|
1828
|
+
authenticated: await checkProviderAuthenticated(name)
|
|
1829
|
+
}))
|
|
1830
|
+
);
|
|
1831
|
+
return results.filter((r) => r.authenticated).map((r) => r.name);
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1834
|
+
// src/providers/index.ts
|
|
1835
|
+
var PROVIDERS = {
|
|
1836
|
+
opencode: boot,
|
|
1837
|
+
copilot: boot2,
|
|
1838
|
+
claude: boot3,
|
|
1839
|
+
codex: boot4
|
|
1840
|
+
};
|
|
1841
|
+
async function bootProvider(name, opts) {
|
|
1842
|
+
const bootFn = PROVIDERS[name];
|
|
1843
|
+
if (!bootFn) {
|
|
1844
|
+
throw new Error(
|
|
1845
|
+
`Unknown provider "${name}". Available: ${PROVIDER_NAMES.join(", ")}`
|
|
1846
|
+
);
|
|
1847
|
+
}
|
|
1848
|
+
return bootFn(opts);
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1851
|
+
// src/datasources/index.ts
|
|
1852
|
+
import { execFile as execFile6 } from "child_process";
|
|
1853
|
+
import { promisify as promisify6 } from "util";
|
|
1854
|
+
|
|
1855
|
+
// src/datasources/interface.ts
|
|
1856
|
+
var DATASOURCE_NAMES = ["github", "azdevops", "md"];
|
|
1857
|
+
|
|
1858
|
+
// src/datasources/github.ts
|
|
1859
|
+
import { execFile as execFile3 } from "child_process";
|
|
1860
|
+
import { promisify as promisify3 } from "util";
|
|
1861
|
+
|
|
1862
|
+
// src/helpers/branch-validation.ts
|
|
1863
|
+
var InvalidBranchNameError = class extends Error {
|
|
1864
|
+
constructor(branch, reason) {
|
|
1865
|
+
const detail = reason ? ` (${reason})` : "";
|
|
1866
|
+
super(`Invalid branch name: "${branch}"${detail}`);
|
|
1867
|
+
this.name = "InvalidBranchNameError";
|
|
1868
|
+
}
|
|
1869
|
+
};
|
|
1870
|
+
var VALID_BRANCH_NAME_RE = /^[a-zA-Z0-9._\-/]+$/;
|
|
1871
|
+
function isValidBranchName(name) {
|
|
1872
|
+
if (name.length === 0 || name.length > 255) return false;
|
|
1873
|
+
if (!VALID_BRANCH_NAME_RE.test(name)) return false;
|
|
1874
|
+
if (name.startsWith("/") || name.endsWith("/")) return false;
|
|
1875
|
+
if (name.includes("..")) return false;
|
|
1876
|
+
if (name.endsWith(".lock")) return false;
|
|
1877
|
+
if (name.includes("@{")) return false;
|
|
1878
|
+
if (name.includes("//")) return false;
|
|
1879
|
+
return true;
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
// src/helpers/auth.ts
|
|
1883
|
+
import { readFile as readFile3, writeFile, mkdir, chmod } from "fs/promises";
|
|
1884
|
+
import { join as join4, dirname as dirname2 } from "path";
|
|
1885
|
+
import { homedir as homedir3 } from "os";
|
|
1886
|
+
import { Octokit } from "@octokit/rest";
|
|
1887
|
+
import { createOAuthDeviceAuth } from "@octokit/auth-oauth-device";
|
|
1888
|
+
import { DeviceCodeCredential } from "@azure/identity";
|
|
1889
|
+
import * as azdev from "azure-devops-node-api";
|
|
1890
|
+
import open from "open";
|
|
1891
|
+
|
|
1892
|
+
// src/constants.ts
|
|
1893
|
+
var GITHUB_CLIENT_ID = "Ov23liUMP1Oyg811IF58";
|
|
1894
|
+
var AZURE_CLIENT_ID = "150a3098-01dd-4126-8b10-5e7f77492e5c";
|
|
1895
|
+
var AZURE_TENANT_ID = "organizations";
|
|
1896
|
+
var AZURE_DEVOPS_SCOPE = "499b84ac-1321-427f-aa17-267ca6975798/.default";
|
|
1897
|
+
|
|
1898
|
+
// src/helpers/auth.ts
|
|
1899
|
+
var AUTH_PATH = join4(homedir3(), ".dispatch", "auth.json");
|
|
1900
|
+
var EXPIRY_BUFFER_MS = 5 * 60 * 1e3;
|
|
1901
|
+
var authPromptHandler = null;
|
|
1902
|
+
function setAuthPromptHandler(handler) {
|
|
1903
|
+
authPromptHandler = handler;
|
|
1904
|
+
}
|
|
1905
|
+
async function loadAuthCache() {
|
|
1906
|
+
try {
|
|
1907
|
+
const raw = await readFile3(AUTH_PATH, "utf-8");
|
|
1908
|
+
return JSON.parse(raw);
|
|
1909
|
+
} catch {
|
|
1910
|
+
return {};
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1913
|
+
async function saveAuthCache(cache) {
|
|
1914
|
+
await mkdir(dirname2(AUTH_PATH), { recursive: true });
|
|
1915
|
+
await writeFile(AUTH_PATH, JSON.stringify(cache, null, 2) + "\n", "utf-8");
|
|
1916
|
+
if (process.platform !== "win32") {
|
|
1917
|
+
try {
|
|
1918
|
+
await chmod(AUTH_PATH, 384);
|
|
1919
|
+
} catch {
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
async function getGithubOctokit() {
|
|
1924
|
+
const cache = await loadAuthCache();
|
|
1925
|
+
if (cache.github?.token) {
|
|
1926
|
+
return new Octokit({ auth: cache.github.token });
|
|
1927
|
+
}
|
|
1928
|
+
const auth = createOAuthDeviceAuth({
|
|
1929
|
+
clientId: GITHUB_CLIENT_ID,
|
|
1930
|
+
clientType: "oauth-app",
|
|
1931
|
+
scopes: ["repo"],
|
|
1932
|
+
onVerification(verification) {
|
|
1933
|
+
const msg = `Enter code ${verification.user_code} at ${verification.verification_uri}`;
|
|
1934
|
+
if (authPromptHandler) {
|
|
1935
|
+
authPromptHandler(msg);
|
|
1936
|
+
} else {
|
|
1937
|
+
log.info(msg);
|
|
1534
1938
|
}
|
|
1535
|
-
|
|
1939
|
+
open(verification.verification_uri).catch(() => {
|
|
1940
|
+
});
|
|
1536
1941
|
}
|
|
1537
|
-
};
|
|
1538
|
-
}
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
import { readFile as readFile3 } from "fs/promises";
|
|
1543
|
-
import { homedir as homedir3 } from "os";
|
|
1544
|
-
import { join as join4 } from "path";
|
|
1545
|
-
var SESSION_READY_TIMEOUT_MS4 = 6e5;
|
|
1546
|
-
async function loadCodexSdk() {
|
|
1547
|
-
return import("@openai/codex-sdk");
|
|
1942
|
+
});
|
|
1943
|
+
const authentication = await auth({ type: "oauth" });
|
|
1944
|
+
cache.github = { token: authentication.token };
|
|
1945
|
+
await saveAuthCache(cache);
|
|
1946
|
+
return new Octokit({ auth: authentication.token });
|
|
1548
1947
|
}
|
|
1549
|
-
async function
|
|
1550
|
-
const
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
} catch (err) {
|
|
1572
|
-
log.debug(`Session creation failed: ${log.formatErrorChain(err)}`);
|
|
1573
|
-
throw err;
|
|
1574
|
-
}
|
|
1575
|
-
},
|
|
1576
|
-
async prompt(sessionId, text, options) {
|
|
1577
|
-
const thread = sessions.get(sessionId);
|
|
1578
|
-
if (!thread) {
|
|
1579
|
-
throw new Error(`Codex session ${sessionId} not found`);
|
|
1580
|
-
}
|
|
1581
|
-
log.debug(`Sending prompt to session ${sessionId} (${text.length} chars)...`);
|
|
1582
|
-
const reporter = createProgressReporter(options?.onProgress);
|
|
1583
|
-
try {
|
|
1584
|
-
reporter.emit("Waiting for Codex response");
|
|
1585
|
-
if (options?.onProgress) {
|
|
1586
|
-
const { events } = await withTimeout(
|
|
1587
|
-
thread.runStreamed(text),
|
|
1588
|
-
SESSION_READY_TIMEOUT_MS4,
|
|
1589
|
-
"codex thread runStreamed"
|
|
1590
|
-
);
|
|
1591
|
-
let lastAgentMessage = null;
|
|
1592
|
-
for await (const event of events) {
|
|
1593
|
-
if (event.type === "item.updated" || event.type === "item.completed") {
|
|
1594
|
-
if (event.item.type === "agent_message") {
|
|
1595
|
-
lastAgentMessage = event.item.text;
|
|
1596
|
-
reporter.emit(event.item.text);
|
|
1597
|
-
}
|
|
1598
|
-
}
|
|
1599
|
-
if (event.type === "turn.failed") {
|
|
1600
|
-
throw new Error(`Codex turn failed: ${event.error.message}`);
|
|
1601
|
-
}
|
|
1602
|
-
if (event.type === "item.completed" && event.item.type === "error") {
|
|
1603
|
-
throw new Error(`Codex error: ${event.item.message}`);
|
|
1604
|
-
}
|
|
1605
|
-
}
|
|
1606
|
-
reporter.emit("Finalizing response");
|
|
1607
|
-
log.debug(`Prompt response received (${lastAgentMessage?.length ?? 0} chars, streaming)`);
|
|
1608
|
-
return lastAgentMessage;
|
|
1609
|
-
}
|
|
1610
|
-
const turn = await withTimeout(
|
|
1611
|
-
thread.run(text),
|
|
1612
|
-
SESSION_READY_TIMEOUT_MS4,
|
|
1613
|
-
"codex thread run"
|
|
1614
|
-
);
|
|
1615
|
-
for (const item of turn.items) {
|
|
1616
|
-
if (item.type === "error") {
|
|
1617
|
-
throw new Error(`Codex error: ${item.message}`);
|
|
1618
|
-
}
|
|
1619
|
-
}
|
|
1620
|
-
reporter.emit("Finalizing response");
|
|
1621
|
-
const result = turn.finalResponse || null;
|
|
1622
|
-
log.debug(`Prompt response received (${result?.length ?? 0} chars)`);
|
|
1623
|
-
return result;
|
|
1624
|
-
} catch (err) {
|
|
1625
|
-
log.debug(`Prompt failed: ${log.formatErrorChain(err)}`);
|
|
1626
|
-
throw err;
|
|
1627
|
-
}
|
|
1628
|
-
},
|
|
1629
|
-
async send(sessionId, text) {
|
|
1630
|
-
const thread = sessions.get(sessionId);
|
|
1631
|
-
if (!thread) {
|
|
1632
|
-
throw new Error(`Codex session ${sessionId} not found`);
|
|
1948
|
+
async function getAzureConnection(orgUrl) {
|
|
1949
|
+
const cache = await loadAuthCache();
|
|
1950
|
+
if (cache.azure?.token && cache.azure.expiresAt) {
|
|
1951
|
+
const expiresAt = new Date(cache.azure.expiresAt).getTime();
|
|
1952
|
+
if (expiresAt - Date.now() > EXPIRY_BUFFER_MS) {
|
|
1953
|
+
return new azdev.WebApi(
|
|
1954
|
+
orgUrl,
|
|
1955
|
+
azdev.getBearerHandler(cache.azure.token)
|
|
1956
|
+
);
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
const credential = new DeviceCodeCredential({
|
|
1960
|
+
tenantId: AZURE_TENANT_ID,
|
|
1961
|
+
clientId: AZURE_CLIENT_ID,
|
|
1962
|
+
userPromptCallback(deviceCodeInfo) {
|
|
1963
|
+
const note = "Azure DevOps requires a work or school account (personal Microsoft accounts are not supported).";
|
|
1964
|
+
const msg = `${note}
|
|
1965
|
+
${deviceCodeInfo.message}`;
|
|
1966
|
+
if (authPromptHandler) {
|
|
1967
|
+
authPromptHandler(msg);
|
|
1968
|
+
} else {
|
|
1969
|
+
log.info(msg);
|
|
1633
1970
|
}
|
|
1634
|
-
|
|
1635
|
-
thread.run(text).catch((err) => {
|
|
1636
|
-
log.debug(`Follow-up turn failed: ${log.formatErrorChain(err)}`);
|
|
1971
|
+
open(deviceCodeInfo.verificationUri).catch(() => {
|
|
1637
1972
|
});
|
|
1638
|
-
},
|
|
1639
|
-
async cleanup() {
|
|
1640
|
-
log.debug("Cleaning up Codex provider...");
|
|
1641
|
-
sessions.clear();
|
|
1642
1973
|
}
|
|
1974
|
+
});
|
|
1975
|
+
const accessToken = await credential.getToken(AZURE_DEVOPS_SCOPE);
|
|
1976
|
+
if (!accessToken) {
|
|
1977
|
+
throw new Error(
|
|
1978
|
+
"Azure device-code authentication did not return a token. Please try again."
|
|
1979
|
+
);
|
|
1980
|
+
}
|
|
1981
|
+
cache.azure = {
|
|
1982
|
+
token: accessToken.token,
|
|
1983
|
+
expiresAt: new Date(accessToken.expiresOnTimestamp).toISOString()
|
|
1643
1984
|
};
|
|
1985
|
+
await saveAuthCache(cache);
|
|
1986
|
+
return new azdev.WebApi(
|
|
1987
|
+
orgUrl,
|
|
1988
|
+
azdev.getBearerHandler(accessToken.token)
|
|
1989
|
+
);
|
|
1644
1990
|
}
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1991
|
+
async function ensureAuthReady(source, cwd, org) {
|
|
1992
|
+
if (source === "github") {
|
|
1993
|
+
const remoteUrl = await getGitRemoteUrl(cwd);
|
|
1994
|
+
if (remoteUrl && parseGitHubRemoteUrl(remoteUrl)) {
|
|
1995
|
+
await getGithubOctokit();
|
|
1996
|
+
} else if (!remoteUrl) {
|
|
1997
|
+
log.warn("No git remote found \u2014 skipping GitHub pre-authentication");
|
|
1998
|
+
} else {
|
|
1999
|
+
log.warn("Remote URL is not a GitHub repository \u2014 skipping GitHub pre-authentication");
|
|
2000
|
+
}
|
|
2001
|
+
} else if (source === "azdevops") {
|
|
2002
|
+
let orgUrl = org;
|
|
2003
|
+
if (!orgUrl) {
|
|
2004
|
+
const remoteUrl = await getGitRemoteUrl(cwd);
|
|
2005
|
+
if (remoteUrl) {
|
|
2006
|
+
const parsed = parseAzDevOpsRemoteUrl(remoteUrl);
|
|
2007
|
+
if (parsed) orgUrl = parsed.orgUrl;
|
|
2008
|
+
else log.warn("Remote URL is not an Azure DevOps repository \u2014 skipping Azure pre-authentication");
|
|
2009
|
+
} else {
|
|
2010
|
+
log.warn("No git remote found \u2014 skipping Azure DevOps pre-authentication");
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
if (orgUrl) await getAzureConnection(orgUrl);
|
|
1654
2014
|
}
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
2015
|
+
}
|
|
2016
|
+
|
|
2017
|
+
// node_modules/@octokit/request-error/dist-src/index.js
|
|
2018
|
+
var RequestError = class extends Error {
|
|
2019
|
+
name;
|
|
2020
|
+
/**
|
|
2021
|
+
* http status code
|
|
2022
|
+
*/
|
|
2023
|
+
status;
|
|
2024
|
+
/**
|
|
2025
|
+
* Request options that lead to the error.
|
|
2026
|
+
*/
|
|
2027
|
+
request;
|
|
2028
|
+
/**
|
|
2029
|
+
* Response object if a response was received
|
|
2030
|
+
*/
|
|
2031
|
+
response;
|
|
2032
|
+
constructor(message, statusCode, options) {
|
|
2033
|
+
super(message);
|
|
2034
|
+
this.name = "HttpError";
|
|
2035
|
+
this.status = Number.parseInt(statusCode);
|
|
2036
|
+
if (Number.isNaN(this.status)) {
|
|
2037
|
+
this.status = 0;
|
|
2038
|
+
}
|
|
2039
|
+
if ("response" in options) {
|
|
2040
|
+
this.response = options.response;
|
|
2041
|
+
}
|
|
2042
|
+
const requestCopy = Object.assign({}, options.request);
|
|
2043
|
+
if (options.request.headers.authorization) {
|
|
2044
|
+
requestCopy.headers = Object.assign({}, options.request.headers, {
|
|
2045
|
+
authorization: options.request.headers.authorization.replace(
|
|
2046
|
+
/(?<! ) .*$/,
|
|
2047
|
+
" [REDACTED]"
|
|
2048
|
+
)
|
|
2049
|
+
});
|
|
1667
2050
|
}
|
|
2051
|
+
requestCopy.url = requestCopy.url.replace(/\bclient_secret=\w+/g, "client_secret=[REDACTED]").replace(/\baccess_token=\w+/g, "access_token=[REDACTED]");
|
|
2052
|
+
this.request = requestCopy;
|
|
1668
2053
|
}
|
|
2054
|
+
};
|
|
2055
|
+
|
|
2056
|
+
// src/datasources/github.ts
|
|
2057
|
+
var exec2 = promisify3(execFile3);
|
|
2058
|
+
async function git(args, cwd) {
|
|
2059
|
+
const { stdout } = await exec2("git", args, { cwd, shell: process.platform === "win32" });
|
|
2060
|
+
return stdout;
|
|
1669
2061
|
}
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
2062
|
+
function redactUrl(url) {
|
|
2063
|
+
return url.replace(/\/\/[^@/]+@/, "//***@");
|
|
2064
|
+
}
|
|
2065
|
+
var ownerRepoCache = /* @__PURE__ */ new Map();
|
|
2066
|
+
async function getOwnerRepo(cwd) {
|
|
2067
|
+
const cached = ownerRepoCache.get(cwd);
|
|
2068
|
+
if (cached) return cached;
|
|
2069
|
+
const remoteUrl = await getGitRemoteUrl(cwd);
|
|
2070
|
+
if (!remoteUrl) {
|
|
2071
|
+
throw new Error("Could not determine git remote URL. Is this a git repository with an origin remote?");
|
|
1673
2072
|
}
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
} catch {
|
|
1678
|
-
return {
|
|
1679
|
-
status: "not-configured",
|
|
1680
|
-
hint: "Run 'claude auth login' or set ANTHROPIC_API_KEY"
|
|
1681
|
-
};
|
|
2073
|
+
const parsed = parseGitHubRemoteUrl(remoteUrl);
|
|
2074
|
+
if (!parsed) {
|
|
2075
|
+
throw new Error(`Could not parse GitHub owner/repo from remote URL: ${redactUrl(remoteUrl)}`);
|
|
1682
2076
|
}
|
|
2077
|
+
ownerRepoCache.set(cwd, parsed);
|
|
2078
|
+
return parsed;
|
|
1683
2079
|
}
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
2080
|
+
function buildBranchName(issueNumber, _title, username = "unknown") {
|
|
2081
|
+
return `${username}/dispatch/issue-${issueNumber}`;
|
|
2082
|
+
}
|
|
2083
|
+
async function getDefaultBranch(cwd) {
|
|
2084
|
+
const PREFIX = "refs/remotes/origin/";
|
|
1688
2085
|
try {
|
|
1689
|
-
await
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
2086
|
+
const ref = await git(["symbolic-ref", "refs/remotes/origin/HEAD"], cwd);
|
|
2087
|
+
const trimmed = ref.trim();
|
|
2088
|
+
const branch = trimmed.startsWith(PREFIX) ? trimmed.slice(PREFIX.length) : trimmed;
|
|
2089
|
+
if (!isValidBranchName(branch)) {
|
|
2090
|
+
throw new InvalidBranchNameError(branch, "from symbolic-ref output");
|
|
2091
|
+
}
|
|
2092
|
+
return branch;
|
|
2093
|
+
} catch (err) {
|
|
2094
|
+
if (err instanceof InvalidBranchNameError) {
|
|
2095
|
+
throw err;
|
|
2096
|
+
}
|
|
2097
|
+
try {
|
|
2098
|
+
await git(["rev-parse", "--verify", "main"], cwd);
|
|
2099
|
+
return "main";
|
|
2100
|
+
} catch {
|
|
2101
|
+
return "master";
|
|
2102
|
+
}
|
|
1696
2103
|
}
|
|
1697
2104
|
}
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
2105
|
+
var datasource = {
|
|
2106
|
+
name: "github",
|
|
2107
|
+
supportsGit() {
|
|
2108
|
+
return true;
|
|
2109
|
+
},
|
|
2110
|
+
async list(opts = {}) {
|
|
2111
|
+
const cwd = opts.cwd || process.cwd();
|
|
2112
|
+
const { owner, repo } = await getOwnerRepo(cwd);
|
|
2113
|
+
const octokit = await getGithubOctokit();
|
|
2114
|
+
const issues = await octokit.paginate(
|
|
2115
|
+
octokit.rest.issues.listForRepo,
|
|
2116
|
+
{
|
|
2117
|
+
owner,
|
|
2118
|
+
repo,
|
|
2119
|
+
state: "open"
|
|
2120
|
+
}
|
|
2121
|
+
);
|
|
2122
|
+
return issues.filter((issue) => !issue.pull_request).map((issue) => ({
|
|
2123
|
+
number: String(issue.number),
|
|
2124
|
+
title: issue.title ?? "",
|
|
2125
|
+
body: issue.body ?? "",
|
|
2126
|
+
labels: (issue.labels ?? []).map((l) => typeof l === "string" ? l : l.name ?? "").filter(Boolean),
|
|
2127
|
+
state: issue.state ?? "open",
|
|
2128
|
+
url: issue.html_url ?? "",
|
|
2129
|
+
comments: [],
|
|
2130
|
+
acceptanceCriteria: ""
|
|
2131
|
+
}));
|
|
2132
|
+
},
|
|
2133
|
+
async fetch(issueId, opts = {}) {
|
|
2134
|
+
const cwd = opts.cwd || process.cwd();
|
|
2135
|
+
const { owner, repo } = await getOwnerRepo(cwd);
|
|
2136
|
+
const octokit = await getGithubOctokit();
|
|
2137
|
+
const { data: issue } = await octokit.rest.issues.get({
|
|
2138
|
+
owner,
|
|
2139
|
+
repo,
|
|
2140
|
+
issue_number: Number(issueId)
|
|
2141
|
+
});
|
|
2142
|
+
const issueComments = await octokit.paginate(
|
|
2143
|
+
octokit.rest.issues.listComments,
|
|
2144
|
+
{
|
|
2145
|
+
owner,
|
|
2146
|
+
repo,
|
|
2147
|
+
issue_number: Number(issueId)
|
|
2148
|
+
}
|
|
2149
|
+
);
|
|
2150
|
+
const comments = issueComments.map(
|
|
2151
|
+
(c) => `**${c.user?.login ?? "unknown"}:** ${c.body ?? ""}`
|
|
2152
|
+
);
|
|
1702
2153
|
return {
|
|
1703
|
-
|
|
1704
|
-
|
|
2154
|
+
number: String(issue.number),
|
|
2155
|
+
title: issue.title ?? "",
|
|
2156
|
+
body: issue.body ?? "",
|
|
2157
|
+
labels: (issue.labels ?? []).map((l) => typeof l === "string" ? l : l.name ?? "").filter(Boolean),
|
|
2158
|
+
state: issue.state ?? "open",
|
|
2159
|
+
url: issue.html_url ?? "",
|
|
2160
|
+
comments,
|
|
2161
|
+
acceptanceCriteria: ""
|
|
1705
2162
|
};
|
|
1706
|
-
}
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
2163
|
+
},
|
|
2164
|
+
async update(issueId, title, body, opts = {}) {
|
|
2165
|
+
const cwd = opts.cwd || process.cwd();
|
|
2166
|
+
const { owner, repo } = await getOwnerRepo(cwd);
|
|
2167
|
+
const octokit = await getGithubOctokit();
|
|
2168
|
+
await octokit.rest.issues.update({
|
|
2169
|
+
owner,
|
|
2170
|
+
repo,
|
|
2171
|
+
issue_number: Number(issueId),
|
|
2172
|
+
title,
|
|
2173
|
+
body
|
|
2174
|
+
});
|
|
2175
|
+
},
|
|
2176
|
+
async close(issueId, opts = {}) {
|
|
2177
|
+
const cwd = opts.cwd || process.cwd();
|
|
2178
|
+
const { owner, repo } = await getOwnerRepo(cwd);
|
|
2179
|
+
const octokit = await getGithubOctokit();
|
|
2180
|
+
await octokit.rest.issues.update({
|
|
2181
|
+
owner,
|
|
2182
|
+
repo,
|
|
2183
|
+
issue_number: Number(issueId),
|
|
2184
|
+
state: "closed"
|
|
2185
|
+
});
|
|
2186
|
+
},
|
|
2187
|
+
async create(title, body, opts = {}) {
|
|
2188
|
+
const cwd = opts.cwd || process.cwd();
|
|
2189
|
+
const { owner, repo } = await getOwnerRepo(cwd);
|
|
2190
|
+
const octokit = await getGithubOctokit();
|
|
2191
|
+
const { data: issue } = await octokit.rest.issues.create({
|
|
2192
|
+
owner,
|
|
2193
|
+
repo,
|
|
2194
|
+
title,
|
|
2195
|
+
body
|
|
2196
|
+
});
|
|
1711
2197
|
return {
|
|
1712
|
-
|
|
1713
|
-
|
|
2198
|
+
number: String(issue.number),
|
|
2199
|
+
title: issue.title ?? "",
|
|
2200
|
+
body: issue.body ?? "",
|
|
2201
|
+
labels: (issue.labels ?? []).map((l) => typeof l === "string" ? l : l.name ?? "").filter(Boolean),
|
|
2202
|
+
state: issue.state ?? "open",
|
|
2203
|
+
url: issue.html_url ?? "",
|
|
2204
|
+
comments: [],
|
|
2205
|
+
acceptanceCriteria: ""
|
|
1714
2206
|
};
|
|
1715
|
-
}
|
|
1716
|
-
}
|
|
1717
|
-
var PROVIDER_REGISTRY = {
|
|
1718
|
-
copilot: {
|
|
1719
|
-
name: "copilot",
|
|
1720
|
-
displayName: "GitHub Copilot",
|
|
1721
|
-
tier: "free",
|
|
1722
|
-
defaultStrongModel: "claude-sonnet-4-5",
|
|
1723
|
-
defaultFastModel: "claude-haiku-4",
|
|
1724
|
-
costScore: { strong: 1, fast: 1 },
|
|
1725
|
-
checkAuth: checkCopilotAuth
|
|
1726
2207
|
},
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
tier: "api-key",
|
|
1731
|
-
defaultStrongModel: "claude-sonnet-4",
|
|
1732
|
-
defaultFastModel: "claude-haiku-3-5",
|
|
1733
|
-
costScore: { strong: 5, fast: 2 },
|
|
1734
|
-
checkAuth: checkClaudeAuth
|
|
2208
|
+
async getUsername(opts) {
|
|
2209
|
+
if (opts.username) return opts.username;
|
|
2210
|
+
return deriveShortUsername(opts.cwd, "unknown");
|
|
1735
2211
|
},
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
displayName: "OpenAI Codex",
|
|
1739
|
-
tier: "api-key",
|
|
1740
|
-
defaultStrongModel: "o4-mini",
|
|
1741
|
-
defaultFastModel: "codex-mini-latest",
|
|
1742
|
-
costScore: { strong: 4, fast: 1 },
|
|
1743
|
-
checkAuth: checkCodexAuth
|
|
2212
|
+
getDefaultBranch(opts) {
|
|
2213
|
+
return getDefaultBranch(opts.cwd);
|
|
1744
2214
|
},
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
2215
|
+
async getCurrentBranch(opts) {
|
|
2216
|
+
try {
|
|
2217
|
+
const branch = (await git(["rev-parse", "--abbrev-ref", "HEAD"], opts.cwd)).trim();
|
|
2218
|
+
if (branch && branch !== "HEAD") return branch;
|
|
2219
|
+
} catch {
|
|
2220
|
+
}
|
|
2221
|
+
return this.getDefaultBranch(opts);
|
|
2222
|
+
},
|
|
2223
|
+
buildBranchName(issueNumber, title, username) {
|
|
2224
|
+
return buildBranchName(issueNumber, title, username ?? "unknown");
|
|
2225
|
+
},
|
|
2226
|
+
async createAndSwitchBranch(branchName, opts) {
|
|
2227
|
+
const cwd = opts.cwd;
|
|
2228
|
+
try {
|
|
2229
|
+
await git(["checkout", "-b", branchName], cwd);
|
|
2230
|
+
} catch (err) {
|
|
2231
|
+
const message = log.extractMessage(err);
|
|
2232
|
+
if (message.includes("already exists")) {
|
|
2233
|
+
try {
|
|
2234
|
+
await git(["checkout", branchName], cwd);
|
|
2235
|
+
} catch (checkoutErr) {
|
|
2236
|
+
const checkoutMessage = log.extractMessage(checkoutErr);
|
|
2237
|
+
if (checkoutMessage.includes("already used by worktree")) {
|
|
2238
|
+
await git(["worktree", "prune"], cwd);
|
|
2239
|
+
await git(["checkout", branchName], cwd);
|
|
2240
|
+
} else {
|
|
2241
|
+
throw checkoutErr;
|
|
2242
|
+
}
|
|
2243
|
+
}
|
|
2244
|
+
} else {
|
|
2245
|
+
throw err;
|
|
2246
|
+
}
|
|
2247
|
+
}
|
|
2248
|
+
},
|
|
2249
|
+
async switchBranch(branchName, opts) {
|
|
2250
|
+
await git(["checkout", branchName], opts.cwd);
|
|
2251
|
+
},
|
|
2252
|
+
async pushBranch(branchName, opts) {
|
|
2253
|
+
await git(["push", "--set-upstream", "origin", branchName], opts.cwd);
|
|
2254
|
+
},
|
|
2255
|
+
async commitAllChanges(message, opts) {
|
|
2256
|
+
const cwd = opts.cwd;
|
|
2257
|
+
await git(["add", "-A"], cwd);
|
|
2258
|
+
const status = await git(["diff", "--cached", "--stat"], cwd);
|
|
2259
|
+
if (!status.trim()) {
|
|
2260
|
+
return;
|
|
2261
|
+
}
|
|
2262
|
+
await git(["commit", "-m", message], cwd);
|
|
2263
|
+
},
|
|
2264
|
+
async createPullRequest(branchName, issueNumber, title, body, opts, baseBranch) {
|
|
2265
|
+
const cwd = opts.cwd;
|
|
2266
|
+
const { owner, repo } = await getOwnerRepo(cwd);
|
|
2267
|
+
const octokit = await getGithubOctokit();
|
|
2268
|
+
const prBody = body || `Closes #${issueNumber}`;
|
|
2269
|
+
try {
|
|
2270
|
+
const target = baseBranch ?? await getDefaultBranch(cwd);
|
|
2271
|
+
const { data: pr } = await octokit.rest.pulls.create({
|
|
2272
|
+
owner,
|
|
2273
|
+
repo,
|
|
2274
|
+
title,
|
|
2275
|
+
body: prBody,
|
|
2276
|
+
head: branchName,
|
|
2277
|
+
base: target
|
|
2278
|
+
});
|
|
2279
|
+
return pr.html_url;
|
|
2280
|
+
} catch (err) {
|
|
2281
|
+
const isValidationError = err instanceof RequestError && err.status === 422;
|
|
2282
|
+
if (isValidationError) {
|
|
2283
|
+
const { data: prs } = await octokit.rest.pulls.list({
|
|
2284
|
+
owner,
|
|
2285
|
+
repo,
|
|
2286
|
+
head: `${owner}:${branchName}`,
|
|
2287
|
+
state: "open"
|
|
2288
|
+
});
|
|
2289
|
+
if (prs.length > 0) {
|
|
2290
|
+
return prs[0].html_url;
|
|
2291
|
+
}
|
|
2292
|
+
}
|
|
2293
|
+
throw err;
|
|
2294
|
+
}
|
|
1753
2295
|
}
|
|
1754
2296
|
};
|
|
1755
2297
|
|
|
1756
|
-
// src/
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
2298
|
+
// src/datasources/azdevops.ts
|
|
2299
|
+
import { execFile as execFile4 } from "child_process";
|
|
2300
|
+
import { promisify as promisify4 } from "util";
|
|
2301
|
+
import { PullRequestStatus } from "azure-devops-node-api/interfaces/GitInterfaces.js";
|
|
2302
|
+
var exec3 = promisify4(execFile4);
|
|
2303
|
+
var doneStateCache = /* @__PURE__ */ new Map();
|
|
2304
|
+
async function git2(args, cwd) {
|
|
2305
|
+
const { stdout } = await exec3("git", args, { cwd, shell: process.platform === "win32" });
|
|
2306
|
+
return stdout;
|
|
1761
2307
|
}
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
providers.map(async (name) => ({
|
|
1765
|
-
name,
|
|
1766
|
-
authenticated: await checkProviderAuthenticated(name)
|
|
1767
|
-
}))
|
|
1768
|
-
);
|
|
1769
|
-
return results.filter((r) => r.authenticated).map((r) => r.name);
|
|
2308
|
+
function redactUrl2(url) {
|
|
2309
|
+
return url.replace(/\/\/[^@/]+@/, "//***@");
|
|
1770
2310
|
}
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
2311
|
+
async function getOrgAndProject(opts = {}) {
|
|
2312
|
+
let orgUrl = opts.org;
|
|
2313
|
+
let project = opts.project;
|
|
2314
|
+
if (!orgUrl || !project) {
|
|
2315
|
+
const cwd = opts.cwd || process.cwd();
|
|
2316
|
+
const remoteUrl = await getGitRemoteUrl(cwd);
|
|
2317
|
+
if (!remoteUrl) {
|
|
2318
|
+
throw new Error(
|
|
2319
|
+
"Could not determine git remote URL. Is this a git repository with an origin remote?"
|
|
2320
|
+
);
|
|
2321
|
+
}
|
|
2322
|
+
const parsed = parseAzDevOpsRemoteUrl(remoteUrl);
|
|
2323
|
+
if (!parsed) {
|
|
2324
|
+
throw new Error(
|
|
2325
|
+
`Could not parse Azure DevOps org/project from remote URL: ${redactUrl2(remoteUrl)}`
|
|
2326
|
+
);
|
|
2327
|
+
}
|
|
2328
|
+
orgUrl = orgUrl || parsed.orgUrl;
|
|
2329
|
+
project = project || parsed.project;
|
|
1785
2330
|
}
|
|
1786
|
-
|
|
2331
|
+
const connection = await getAzureConnection(orgUrl);
|
|
2332
|
+
return { orgUrl, project, connection };
|
|
1787
2333
|
}
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
success: "#56D364",
|
|
1807
|
-
error: "#F85149",
|
|
1808
|
-
warn: "#D29922",
|
|
1809
|
-
accent: "#79C0FF",
|
|
1810
|
-
planning: "#D2A8FF"
|
|
1811
|
-
};
|
|
1812
|
-
function elapsed(ms) {
|
|
1813
|
-
const s = Math.floor(ms / 1e3);
|
|
1814
|
-
const m = Math.floor(s / 60);
|
|
1815
|
-
const sec = s % 60;
|
|
1816
|
-
if (m > 0) return `${m}m ${sec}s`;
|
|
1817
|
-
return `${sec}s`;
|
|
2334
|
+
function mapWorkItemToIssueDetails(item, id, comments, defaults) {
|
|
2335
|
+
const fields = item.fields ?? {};
|
|
2336
|
+
return {
|
|
2337
|
+
number: String(item.id ?? id),
|
|
2338
|
+
title: fields["System.Title"] ?? defaults?.title ?? "",
|
|
2339
|
+
body: fields["System.Description"] ?? defaults?.body ?? "",
|
|
2340
|
+
labels: (fields["System.Tags"] ?? "").split(";").map((t) => t.trim()).filter(Boolean),
|
|
2341
|
+
state: fields["System.State"] ?? defaults?.state ?? "",
|
|
2342
|
+
url: item._links?.html?.href ?? item.url ?? "",
|
|
2343
|
+
comments,
|
|
2344
|
+
acceptanceCriteria: fields["Microsoft.VSTS.Common.AcceptanceCriteria"] ?? "",
|
|
2345
|
+
iterationPath: fields["System.IterationPath"] || void 0,
|
|
2346
|
+
areaPath: fields["System.AreaPath"] || void 0,
|
|
2347
|
+
assignee: fields["System.AssignedTo"]?.displayName || void 0,
|
|
2348
|
+
priority: fields["Microsoft.VSTS.Common.Priority"] ?? void 0,
|
|
2349
|
+
storyPoints: fields["Microsoft.VSTS.Scheduling.StoryPoints"] ?? fields["Microsoft.VSTS.Scheduling.Effort"] ?? fields["Microsoft.VSTS.Scheduling.Size"] ?? void 0,
|
|
2350
|
+
workItemType: fields["System.WorkItemType"] || defaults?.workItemType || void 0
|
|
2351
|
+
};
|
|
1818
2352
|
}
|
|
1819
|
-
function
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
2353
|
+
async function detectWorkItemType(opts = {}) {
|
|
2354
|
+
try {
|
|
2355
|
+
const { project, connection } = await getOrgAndProject(opts);
|
|
2356
|
+
const witApi = await connection.getWorkItemTrackingApi();
|
|
2357
|
+
const types = await witApi.getWorkItemTypes(project);
|
|
2358
|
+
if (!Array.isArray(types) || types.length === 0) return null;
|
|
2359
|
+
const names = types.map((t) => t.name).filter((n) => !!n);
|
|
2360
|
+
const preferred = ["User Story", "Product Backlog Item", "Requirement", "Issue"];
|
|
2361
|
+
for (const p of preferred) {
|
|
2362
|
+
if (names.includes(p)) return p;
|
|
2363
|
+
}
|
|
2364
|
+
return names[0] ?? null;
|
|
2365
|
+
} catch {
|
|
2366
|
+
return null;
|
|
1830
2367
|
}
|
|
1831
|
-
return lines;
|
|
1832
2368
|
}
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
}
|
|
1856
|
-
}
|
|
1857
|
-
)
|
|
1858
|
-
] });
|
|
2369
|
+
async function detectDoneState(workItemType, opts = {}) {
|
|
2370
|
+
const { orgUrl, project, connection } = await getOrgAndProject(opts);
|
|
2371
|
+
const cacheKey = `${orgUrl}|${project}|${workItemType}`;
|
|
2372
|
+
const cached = doneStateCache.get(cacheKey);
|
|
2373
|
+
if (cached) return cached;
|
|
2374
|
+
try {
|
|
2375
|
+
const witApi = await connection.getWorkItemTrackingApi();
|
|
2376
|
+
const states = await witApi.getWorkItemTypeStates(project, workItemType);
|
|
2377
|
+
if (Array.isArray(states)) {
|
|
2378
|
+
const completed = states.find((s) => s.category === "Completed");
|
|
2379
|
+
if (completed?.name) {
|
|
2380
|
+
doneStateCache.set(cacheKey, completed.name);
|
|
2381
|
+
return completed.name;
|
|
2382
|
+
}
|
|
2383
|
+
const names = states.map((s) => s.name).filter((n) => !!n);
|
|
2384
|
+
const fallbacks = ["Done", "Closed", "Resolved", "Completed"];
|
|
2385
|
+
for (const f of fallbacks) {
|
|
2386
|
+
if (names.includes(f)) {
|
|
2387
|
+
doneStateCache.set(cacheKey, f);
|
|
2388
|
+
return f;
|
|
2389
|
+
}
|
|
2390
|
+
}
|
|
1859
2391
|
}
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
});
|
|
1864
|
-
}
|
|
1865
|
-
|
|
1866
|
-
// src/providers/auth-setup.tsx
|
|
1867
|
-
import { spawn } from "child_process";
|
|
1868
|
-
|
|
1869
|
-
// src/config.ts
|
|
1870
|
-
var CONFIG_KEYS = ["enabledProviders", "providerModels", "source", "planTimeout", "specTimeout", "specWarnTimeout", "specKillTimeout", "concurrency", "org", "project", "workItemType", "iteration", "area", "username"];
|
|
1871
|
-
function getConfigPath(configDir) {
|
|
1872
|
-
const dir = configDir ?? join5(process.cwd(), ".dispatch");
|
|
1873
|
-
return join5(dir, "config.json");
|
|
2392
|
+
} catch {
|
|
2393
|
+
}
|
|
2394
|
+
return "Closed";
|
|
1874
2395
|
}
|
|
1875
|
-
async function
|
|
1876
|
-
const configPath = getConfigPath(configDir);
|
|
2396
|
+
async function fetchComments(workItemId, project, connection) {
|
|
1877
2397
|
try {
|
|
1878
|
-
const
|
|
1879
|
-
const
|
|
1880
|
-
|
|
2398
|
+
const witApi = await connection.getWorkItemTrackingApi();
|
|
2399
|
+
const commentList = await witApi.getComments(project, workItemId);
|
|
2400
|
+
if (commentList.comments && Array.isArray(commentList.comments)) {
|
|
2401
|
+
return commentList.comments.map((c) => {
|
|
2402
|
+
const author = c.createdBy?.displayName ?? "unknown";
|
|
2403
|
+
return `**${author}:** ${c.text ?? ""}`;
|
|
2404
|
+
});
|
|
2405
|
+
}
|
|
2406
|
+
return [];
|
|
1881
2407
|
} catch {
|
|
1882
|
-
return
|
|
2408
|
+
return [];
|
|
1883
2409
|
}
|
|
1884
2410
|
}
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
2411
|
+
var datasource2 = {
|
|
2412
|
+
name: "azdevops",
|
|
2413
|
+
supportsGit() {
|
|
2414
|
+
return true;
|
|
2415
|
+
},
|
|
2416
|
+
async list(opts = {}) {
|
|
2417
|
+
const { project, connection } = await getOrgAndProject(opts);
|
|
2418
|
+
const witApi = await connection.getWorkItemTrackingApi();
|
|
2419
|
+
const conditions = [
|
|
2420
|
+
"[System.State] <> 'Closed'",
|
|
2421
|
+
"[System.State] <> 'Done'",
|
|
2422
|
+
"[System.State] <> 'Removed'"
|
|
2423
|
+
];
|
|
2424
|
+
if (opts.iteration) {
|
|
2425
|
+
const iterValue = String(opts.iteration).trim();
|
|
2426
|
+
if (iterValue === "@CurrentIteration") {
|
|
2427
|
+
conditions.push(`[System.IterationPath] UNDER @CurrentIteration`);
|
|
2428
|
+
} else {
|
|
2429
|
+
const escaped = iterValue.replace(/'/g, "''");
|
|
2430
|
+
if (escaped) conditions.push(`[System.IterationPath] UNDER '${escaped}'`);
|
|
2431
|
+
}
|
|
1892
2432
|
}
|
|
1893
|
-
if (
|
|
1894
|
-
const
|
|
1895
|
-
if (
|
|
1896
|
-
|
|
2433
|
+
if (opts.area) {
|
|
2434
|
+
const area = String(opts.area).trim().replace(/'/g, "''");
|
|
2435
|
+
if (area) {
|
|
2436
|
+
conditions.push(`[System.AreaPath] UNDER '${area}'`);
|
|
1897
2437
|
}
|
|
1898
2438
|
}
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
2439
|
+
const wiql = `SELECT [System.Id] FROM workitems WHERE ${conditions.join(" AND ")} ORDER BY [System.CreatedDate] DESC`;
|
|
2440
|
+
const queryResult = await witApi.queryByWiql({ query: wiql }, { project });
|
|
2441
|
+
const workItemRefs = queryResult.workItems ?? [];
|
|
2442
|
+
if (workItemRefs.length === 0) return [];
|
|
2443
|
+
const ids = workItemRefs.map((ref) => ref.id).filter((id) => id != null);
|
|
2444
|
+
if (ids.length === 0) return [];
|
|
2445
|
+
try {
|
|
2446
|
+
const items = await witApi.getWorkItems(ids);
|
|
2447
|
+
const itemsArray = Array.isArray(items) ? items : [items];
|
|
2448
|
+
const commentsArray = [];
|
|
2449
|
+
const CONCURRENCY = 5;
|
|
2450
|
+
for (let i = 0; i < itemsArray.length; i += CONCURRENCY) {
|
|
2451
|
+
const batch = itemsArray.slice(i, i + CONCURRENCY);
|
|
2452
|
+
const batchResults = await Promise.all(
|
|
2453
|
+
// item.id is guaranteed non-null here: the ids array was built by filtering
|
|
2454
|
+
// out null ids from workItemRefs, and itemsArray came from getWorkItems(ids).
|
|
2455
|
+
batch.map((item) => fetchComments(item.id, project, connection))
|
|
2456
|
+
);
|
|
2457
|
+
commentsArray.push(...batchResults);
|
|
1904
2458
|
}
|
|
2459
|
+
return itemsArray.map(
|
|
2460
|
+
(item, i) => mapWorkItemToIssueDetails(item, String(item.id), commentsArray[i])
|
|
2461
|
+
);
|
|
2462
|
+
} catch (err) {
|
|
2463
|
+
log.debug(`Batch getWorkItems failed, falling back to individual fetches: ${log.extractMessage(err)}`);
|
|
2464
|
+
const results = await Promise.all(
|
|
2465
|
+
ids.map((id) => datasource2.fetch(String(id), opts))
|
|
2466
|
+
);
|
|
2467
|
+
return results;
|
|
1905
2468
|
}
|
|
1906
|
-
|
|
1907
|
-
|
|
2469
|
+
},
|
|
2470
|
+
async fetch(issueId, opts = {}) {
|
|
2471
|
+
const { project, connection } = await getOrgAndProject(opts);
|
|
2472
|
+
const witApi = await connection.getWorkItemTrackingApi();
|
|
2473
|
+
const item = await witApi.getWorkItem(Number(issueId));
|
|
2474
|
+
const comments = await fetchComments(Number(issueId), project, connection);
|
|
2475
|
+
return mapWorkItemToIssueDetails(item, issueId, comments);
|
|
2476
|
+
},
|
|
2477
|
+
async update(issueId, title, body, opts = {}) {
|
|
2478
|
+
const { connection } = await getOrgAndProject(opts);
|
|
2479
|
+
const witApi = await connection.getWorkItemTrackingApi();
|
|
2480
|
+
const document = [
|
|
2481
|
+
{ op: "add", path: "/fields/System.Title", value: title },
|
|
2482
|
+
{ op: "add", path: "/fields/System.Description", value: body }
|
|
2483
|
+
];
|
|
2484
|
+
await witApi.updateWorkItem(null, document, Number(issueId));
|
|
2485
|
+
},
|
|
2486
|
+
async close(issueId, opts = {}) {
|
|
2487
|
+
const { connection } = await getOrgAndProject(opts);
|
|
2488
|
+
const witApi = await connection.getWorkItemTrackingApi();
|
|
2489
|
+
let workItemType = opts.workItemType;
|
|
2490
|
+
if (!workItemType) {
|
|
2491
|
+
const item = await witApi.getWorkItem(Number(issueId));
|
|
2492
|
+
workItemType = item.fields?.["System.WorkItemType"] ?? void 0;
|
|
1908
2493
|
}
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
2494
|
+
const state = workItemType ? await detectDoneState(workItemType, opts) : "Closed";
|
|
2495
|
+
const document = [
|
|
2496
|
+
{ op: "add", path: "/fields/System.State", value: state }
|
|
2497
|
+
];
|
|
2498
|
+
await witApi.updateWorkItem(null, document, Number(issueId));
|
|
2499
|
+
},
|
|
2500
|
+
async create(title, body, opts = {}) {
|
|
2501
|
+
const workItemType = opts.workItemType ?? await detectWorkItemType(opts);
|
|
2502
|
+
if (!workItemType) {
|
|
2503
|
+
throw new Error(
|
|
2504
|
+
"Could not determine work item type. Set workItemType in your config (for example via `dispatch config`)."
|
|
2505
|
+
);
|
|
2506
|
+
}
|
|
2507
|
+
const { project, connection } = await getOrgAndProject(opts);
|
|
2508
|
+
const witApi = await connection.getWorkItemTrackingApi();
|
|
2509
|
+
const document = [
|
|
2510
|
+
{ op: "add", path: "/fields/System.Title", value: title },
|
|
2511
|
+
{ op: "add", path: "/fields/System.Description", value: body }
|
|
2512
|
+
];
|
|
2513
|
+
const item = await witApi.createWorkItem(
|
|
2514
|
+
// null as any: SDK customHeaders param — passing null is the documented way to omit it.
|
|
2515
|
+
null,
|
|
2516
|
+
document,
|
|
2517
|
+
project,
|
|
2518
|
+
workItemType
|
|
1912
2519
|
);
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
2520
|
+
return mapWorkItemToIssueDetails(item, String(item.id), [], {
|
|
2521
|
+
title,
|
|
2522
|
+
body,
|
|
2523
|
+
state: "New",
|
|
2524
|
+
workItemType
|
|
2525
|
+
});
|
|
2526
|
+
},
|
|
2527
|
+
async getDefaultBranch(opts) {
|
|
2528
|
+
const PREFIX = "refs/remotes/origin/";
|
|
2529
|
+
try {
|
|
2530
|
+
const ref = await git2(["symbolic-ref", "refs/remotes/origin/HEAD"], opts.cwd);
|
|
2531
|
+
const trimmed = ref.trim();
|
|
2532
|
+
const branch = trimmed.startsWith(PREFIX) ? trimmed.slice(PREFIX.length) : trimmed;
|
|
2533
|
+
if (!isValidBranchName(branch)) {
|
|
2534
|
+
throw new InvalidBranchNameError(branch, "from symbolic-ref output");
|
|
2535
|
+
}
|
|
2536
|
+
return branch;
|
|
2537
|
+
} catch (err) {
|
|
2538
|
+
if (err instanceof InvalidBranchNameError) {
|
|
2539
|
+
throw err;
|
|
2540
|
+
}
|
|
2541
|
+
try {
|
|
2542
|
+
await git2(["rev-parse", "--verify", "main"], opts.cwd);
|
|
2543
|
+
return "main";
|
|
2544
|
+
} catch {
|
|
2545
|
+
return "master";
|
|
1925
2546
|
}
|
|
1926
2547
|
}
|
|
1927
|
-
|
|
1928
|
-
|
|
2548
|
+
},
|
|
2549
|
+
async getCurrentBranch(opts) {
|
|
2550
|
+
try {
|
|
2551
|
+
const branch = (await git2(["rev-parse", "--abbrev-ref", "HEAD"], opts.cwd)).trim();
|
|
2552
|
+
if (branch && branch !== "HEAD") return branch;
|
|
2553
|
+
} catch {
|
|
1929
2554
|
}
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
2555
|
+
return this.getDefaultBranch(opts);
|
|
2556
|
+
},
|
|
2557
|
+
async getUsername(opts) {
|
|
2558
|
+
if (opts.username) return opts.username;
|
|
2559
|
+
return deriveShortUsername(opts.cwd, "unknown");
|
|
2560
|
+
},
|
|
2561
|
+
buildBranchName(issueNumber, _title, username) {
|
|
2562
|
+
const branch = `${username}/dispatch/issue-${issueNumber}`;
|
|
2563
|
+
if (!isValidBranchName(branch)) {
|
|
2564
|
+
throw new InvalidBranchNameError(branch);
|
|
2565
|
+
}
|
|
2566
|
+
return branch;
|
|
2567
|
+
},
|
|
2568
|
+
async createAndSwitchBranch(branchName, opts) {
|
|
2569
|
+
if (!isValidBranchName(branchName)) {
|
|
2570
|
+
throw new InvalidBranchNameError(branchName);
|
|
2571
|
+
}
|
|
2572
|
+
try {
|
|
2573
|
+
await git2(["checkout", "-b", branchName], opts.cwd);
|
|
2574
|
+
} catch (err) {
|
|
2575
|
+
const message = log.extractMessage(err);
|
|
2576
|
+
if (message.includes("already exists")) {
|
|
2577
|
+
try {
|
|
2578
|
+
await git2(["checkout", branchName], opts.cwd);
|
|
2579
|
+
} catch (checkoutErr) {
|
|
2580
|
+
const checkoutMessage = log.extractMessage(checkoutErr);
|
|
2581
|
+
if (checkoutMessage.includes("already used by worktree")) {
|
|
2582
|
+
await git2(["worktree", "prune"], opts.cwd);
|
|
2583
|
+
await git2(["checkout", branchName], opts.cwd);
|
|
2584
|
+
} else {
|
|
2585
|
+
throw checkoutErr;
|
|
2586
|
+
}
|
|
2587
|
+
}
|
|
2588
|
+
} else {
|
|
2589
|
+
throw err;
|
|
2590
|
+
}
|
|
2591
|
+
}
|
|
2592
|
+
},
|
|
2593
|
+
async switchBranch(branchName, opts) {
|
|
2594
|
+
await git2(["checkout", branchName], opts.cwd);
|
|
2595
|
+
},
|
|
2596
|
+
async pushBranch(branchName, opts) {
|
|
2597
|
+
await git2(["push", "--set-upstream", "origin", branchName], opts.cwd);
|
|
2598
|
+
},
|
|
2599
|
+
async commitAllChanges(message, opts) {
|
|
2600
|
+
await git2(["add", "-A"], opts.cwd);
|
|
2601
|
+
const status = await git2(["diff", "--cached", "--stat"], opts.cwd);
|
|
2602
|
+
if (!status.trim()) {
|
|
2603
|
+
return;
|
|
2604
|
+
}
|
|
2605
|
+
await git2(["commit", "-m", message], opts.cwd);
|
|
2606
|
+
},
|
|
2607
|
+
async createPullRequest(branchName, issueNumber, title, body, opts, baseBranch) {
|
|
2608
|
+
const cwd = opts.cwd;
|
|
2609
|
+
const { orgUrl, project, connection } = await getOrgAndProject(opts);
|
|
2610
|
+
const gitApi = await connection.getGitApi();
|
|
2611
|
+
const remoteUrl = await getGitRemoteUrl(cwd);
|
|
2612
|
+
if (!remoteUrl) {
|
|
2613
|
+
throw new Error("Could not determine git remote URL.");
|
|
2614
|
+
}
|
|
2615
|
+
const repos = await gitApi.getRepositories(project);
|
|
2616
|
+
const normalizeUrl = (u) => u.replace(/\/\/[^@/]+@/, "//").replace(/\.git$/, "").replace(/\/$/, "").toLowerCase();
|
|
2617
|
+
const normalizedRemote = normalizeUrl(remoteUrl);
|
|
2618
|
+
const repo = repos.find(
|
|
2619
|
+
(r) => r.remoteUrl && normalizeUrl(r.remoteUrl) === normalizedRemote || r.sshUrl && normalizeUrl(r.sshUrl) === normalizedRemote || r.webUrl && normalizeUrl(r.webUrl) === normalizedRemote
|
|
2620
|
+
);
|
|
2621
|
+
if (!repo || !repo.id) {
|
|
2622
|
+
throw new Error(`Could not find Azure DevOps repository matching remote URL: ${redactUrl2(remoteUrl)}`);
|
|
2623
|
+
}
|
|
2624
|
+
const target = baseBranch ?? await this.getDefaultBranch(opts);
|
|
2625
|
+
try {
|
|
2626
|
+
const pr = await gitApi.createPullRequest(
|
|
2627
|
+
{
|
|
2628
|
+
sourceRefName: `refs/heads/${branchName}`,
|
|
2629
|
+
targetRefName: `refs/heads/${target}`,
|
|
2630
|
+
title,
|
|
2631
|
+
description: body || `Resolves AB#${issueNumber}`,
|
|
2632
|
+
workItemRefs: [{ id: issueNumber }]
|
|
2633
|
+
},
|
|
2634
|
+
repo.id,
|
|
2635
|
+
project
|
|
2636
|
+
);
|
|
2637
|
+
const webUrl = repo.webUrl ? `${repo.webUrl}/pullrequest/${pr.pullRequestId}` : pr.url ?? "";
|
|
2638
|
+
return webUrl;
|
|
2639
|
+
} catch (err) {
|
|
2640
|
+
const message = log.extractMessage(err);
|
|
2641
|
+
if (message.includes("already exists")) {
|
|
2642
|
+
const prs = await gitApi.getPullRequests(
|
|
2643
|
+
repo.id,
|
|
2644
|
+
{
|
|
2645
|
+
sourceRefName: `refs/heads/${branchName}`,
|
|
2646
|
+
status: PullRequestStatus.Active
|
|
2647
|
+
},
|
|
2648
|
+
project
|
|
2649
|
+
);
|
|
2650
|
+
if (Array.isArray(prs) && prs.length > 0) {
|
|
2651
|
+
const existingPr = prs[0];
|
|
2652
|
+
const webUrl = repo.webUrl ? `${repo.webUrl}/pullrequest/${existingPr.pullRequestId}` : existingPr.url ?? "";
|
|
2653
|
+
return webUrl;
|
|
2654
|
+
}
|
|
2655
|
+
return "";
|
|
2656
|
+
}
|
|
2657
|
+
throw err;
|
|
2658
|
+
}
|
|
2659
|
+
}
|
|
2660
|
+
};
|
|
2661
|
+
|
|
2662
|
+
// src/datasources/md.ts
|
|
2663
|
+
import { execFile as execFile5 } from "child_process";
|
|
2664
|
+
import { readFile as readFile4, writeFile as writeFile2, readdir, mkdir as mkdir2, rename } from "fs/promises";
|
|
2665
|
+
import { basename, dirname as dirname3, isAbsolute, join as join5, parse as parsePath, resolve } from "path";
|
|
2666
|
+
import { promisify as promisify5 } from "util";
|
|
2667
|
+
import { glob } from "glob";
|
|
2668
|
+
|
|
2669
|
+
// src/helpers/slugify.ts
|
|
2670
|
+
var MAX_SLUG_LENGTH = 60;
|
|
2671
|
+
function slugify(input2, maxLength) {
|
|
2672
|
+
const slug = input2.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
2673
|
+
return maxLength != null ? slug.slice(0, maxLength) : slug;
|
|
1952
2674
|
}
|
|
1953
2675
|
|
|
1954
2676
|
// src/datasources/md.ts
|
|
@@ -1969,7 +2691,7 @@ async function git3(args, cwd) {
|
|
|
1969
2691
|
var DEFAULT_DIR = ".dispatch/specs";
|
|
1970
2692
|
function resolveDir(opts) {
|
|
1971
2693
|
const cwd = opts?.cwd ?? process.cwd();
|
|
1972
|
-
return
|
|
2694
|
+
return join5(cwd, DEFAULT_DIR);
|
|
1973
2695
|
}
|
|
1974
2696
|
function resolveFilePath(issueId, opts) {
|
|
1975
2697
|
const filename = issueId.endsWith(".md") ? issueId : `${issueId}.md`;
|
|
@@ -1978,7 +2700,7 @@ function resolveFilePath(issueId, opts) {
|
|
|
1978
2700
|
const cwd = opts?.cwd ?? process.cwd();
|
|
1979
2701
|
return resolve(cwd, filename);
|
|
1980
2702
|
}
|
|
1981
|
-
return
|
|
2703
|
+
return join5(resolveDir(opts), filename);
|
|
1982
2704
|
}
|
|
1983
2705
|
async function resolveNumericFilePath(issueId, opts) {
|
|
1984
2706
|
if (/^\d+$/.test(issueId)) {
|
|
@@ -1986,7 +2708,7 @@ async function resolveNumericFilePath(issueId, opts) {
|
|
|
1986
2708
|
const entries = await readdir(dir);
|
|
1987
2709
|
const match = entries.find((f) => f.startsWith(`${issueId}-`) && f.endsWith(".md"));
|
|
1988
2710
|
if (match) {
|
|
1989
|
-
return
|
|
2711
|
+
return join5(dir, match);
|
|
1990
2712
|
}
|
|
1991
2713
|
}
|
|
1992
2714
|
return resolveFilePath(issueId, opts);
|
|
@@ -2015,7 +2737,7 @@ function toIssueDetails(filename, content, dir) {
|
|
|
2015
2737
|
body: content,
|
|
2016
2738
|
labels: [],
|
|
2017
2739
|
state: "open",
|
|
2018
|
-
url:
|
|
2740
|
+
url: join5(dir, filename),
|
|
2019
2741
|
comments: [],
|
|
2020
2742
|
acceptanceCriteria: ""
|
|
2021
2743
|
};
|
|
@@ -2032,9 +2754,9 @@ var datasource3 = {
|
|
|
2032
2754
|
const mdFiles2 = files.filter((f) => f.endsWith(".md")).sort();
|
|
2033
2755
|
const results2 = [];
|
|
2034
2756
|
for (const filePath of mdFiles2) {
|
|
2035
|
-
const content = await
|
|
2757
|
+
const content = await readFile4(filePath, "utf-8");
|
|
2036
2758
|
const filename = basename(filePath);
|
|
2037
|
-
const dir2 =
|
|
2759
|
+
const dir2 = dirname3(filePath);
|
|
2038
2760
|
results2.push(toIssueDetails(filename, content, dir2));
|
|
2039
2761
|
}
|
|
2040
2762
|
return results2;
|
|
@@ -2049,8 +2771,8 @@ var datasource3 = {
|
|
|
2049
2771
|
const mdFiles = entries.filter((f) => f.endsWith(".md")).sort();
|
|
2050
2772
|
const results = [];
|
|
2051
2773
|
for (const filename of mdFiles) {
|
|
2052
|
-
const filePath =
|
|
2053
|
-
const content = await
|
|
2774
|
+
const filePath = join5(dir, filename);
|
|
2775
|
+
const content = await readFile4(filePath, "utf-8");
|
|
2054
2776
|
results.push(toIssueDetails(filename, content, dir));
|
|
2055
2777
|
}
|
|
2056
2778
|
return results;
|
|
@@ -2061,38 +2783,38 @@ var datasource3 = {
|
|
|
2061
2783
|
const entries = await readdir(dir2);
|
|
2062
2784
|
const match = entries.find((f) => f.startsWith(`${issueId}-`) && f.endsWith(".md"));
|
|
2063
2785
|
if (match) {
|
|
2064
|
-
const content2 = await
|
|
2786
|
+
const content2 = await readFile4(join5(dir2, match), "utf-8");
|
|
2065
2787
|
return toIssueDetails(match, content2, dir2);
|
|
2066
2788
|
}
|
|
2067
2789
|
}
|
|
2068
2790
|
const filePath = resolveFilePath(issueId, opts);
|
|
2069
|
-
const content = await
|
|
2791
|
+
const content = await readFile4(filePath, "utf-8");
|
|
2070
2792
|
const filename = basename(filePath);
|
|
2071
|
-
const dir =
|
|
2793
|
+
const dir = dirname3(filePath);
|
|
2072
2794
|
return toIssueDetails(filename, content, dir);
|
|
2073
2795
|
},
|
|
2074
2796
|
async update(issueId, _title, body, opts) {
|
|
2075
2797
|
const filePath = await resolveNumericFilePath(issueId, opts);
|
|
2076
|
-
await
|
|
2798
|
+
await writeFile2(filePath, body, "utf-8");
|
|
2077
2799
|
},
|
|
2078
2800
|
async close(issueId, opts) {
|
|
2079
2801
|
const filePath = await resolveNumericFilePath(issueId, opts);
|
|
2080
2802
|
const filename = basename(filePath);
|
|
2081
|
-
const archiveDir =
|
|
2082
|
-
await
|
|
2083
|
-
await rename(filePath,
|
|
2803
|
+
const archiveDir = join5(dirname3(filePath), "archive");
|
|
2804
|
+
await mkdir2(archiveDir, { recursive: true });
|
|
2805
|
+
await rename(filePath, join5(archiveDir, filename));
|
|
2084
2806
|
},
|
|
2085
2807
|
async create(title, body, opts) {
|
|
2086
2808
|
return withCreateLock(async () => {
|
|
2087
2809
|
const cwd = opts?.cwd ?? process.cwd();
|
|
2088
|
-
const configDir =
|
|
2810
|
+
const configDir = join5(cwd, ".dispatch");
|
|
2089
2811
|
const config = await loadConfig(configDir);
|
|
2090
2812
|
const id = config.nextIssueId ?? 1;
|
|
2091
2813
|
const dir = resolveDir(opts);
|
|
2092
|
-
await
|
|
2814
|
+
await mkdir2(dir, { recursive: true });
|
|
2093
2815
|
const filename = `${id}-${slugify(title)}.md`;
|
|
2094
|
-
const filePath =
|
|
2095
|
-
await
|
|
2816
|
+
const filePath = join5(dir, filename);
|
|
2817
|
+
await writeFile2(filePath, body, "utf-8");
|
|
2096
2818
|
config.nextIssueId = id + 1;
|
|
2097
2819
|
await saveConfig(config, configDir);
|
|
2098
2820
|
return {
|
|
@@ -2322,7 +3044,111 @@ function parseGitHubRemoteUrl(url) {
|
|
|
2322
3044
|
return null;
|
|
2323
3045
|
}
|
|
2324
3046
|
|
|
3047
|
+
// src/config-prompts.tsx
|
|
3048
|
+
init_ink_prompts();
|
|
3049
|
+
import chalk3 from "chalk";
|
|
3050
|
+
init_format();
|
|
3051
|
+
|
|
3052
|
+
// src/providers/auth-setup.tsx
|
|
3053
|
+
init_ink_prompts();
|
|
3054
|
+
import { spawn } from "child_process";
|
|
3055
|
+
|
|
3056
|
+
// src/config.ts
|
|
3057
|
+
var CONFIG_BOUNDS = {
|
|
3058
|
+
planTimeout: { min: 1, max: 120 },
|
|
3059
|
+
specTimeout: { min: 1, max: 120 },
|
|
3060
|
+
specWarnTimeout: { min: 1, max: 120 },
|
|
3061
|
+
specKillTimeout: { min: 1, max: 120 },
|
|
3062
|
+
concurrency: { min: 1, max: 64 },
|
|
3063
|
+
maxRuns: { min: 1, max: 32 }
|
|
3064
|
+
};
|
|
3065
|
+
var CONFIG_KEYS = ["enabledProviders", "providerModels", "source", "planTimeout", "specTimeout", "specWarnTimeout", "specKillTimeout", "concurrency", "maxRuns", "org", "project", "workItemType", "iteration", "area", "username", "orchestratorProvider"];
|
|
3066
|
+
function getConfigPath(configDir) {
|
|
3067
|
+
const dir = configDir ?? join6(process.cwd(), ".dispatch");
|
|
3068
|
+
return join6(dir, "config.json");
|
|
3069
|
+
}
|
|
3070
|
+
async function loadConfig(configDir) {
|
|
3071
|
+
const configPath = getConfigPath(configDir);
|
|
3072
|
+
try {
|
|
3073
|
+
const raw = await readFile5(configPath, "utf-8");
|
|
3074
|
+
const parsed = JSON.parse(raw);
|
|
3075
|
+
return migrateConfig(parsed);
|
|
3076
|
+
} catch {
|
|
3077
|
+
return {};
|
|
3078
|
+
}
|
|
3079
|
+
}
|
|
3080
|
+
function migrateConfig(raw) {
|
|
3081
|
+
const config = {};
|
|
3082
|
+
if (!raw.enabledProviders && raw.provider) {
|
|
3083
|
+
const providers = /* @__PURE__ */ new Set();
|
|
3084
|
+
const legacyProvider = raw.provider;
|
|
3085
|
+
if (PROVIDER_NAMES.includes(legacyProvider)) {
|
|
3086
|
+
providers.add(legacyProvider);
|
|
3087
|
+
}
|
|
3088
|
+
if (raw.fastProvider) {
|
|
3089
|
+
const fastProvider = raw.fastProvider;
|
|
3090
|
+
if (PROVIDER_NAMES.includes(fastProvider)) {
|
|
3091
|
+
providers.add(fastProvider);
|
|
3092
|
+
}
|
|
3093
|
+
}
|
|
3094
|
+
if (raw.agents && typeof raw.agents === "object") {
|
|
3095
|
+
for (const agentCfg of Object.values(raw.agents)) {
|
|
3096
|
+
if (agentCfg?.provider && PROVIDER_NAMES.includes(agentCfg.provider)) {
|
|
3097
|
+
providers.add(agentCfg.provider);
|
|
3098
|
+
}
|
|
3099
|
+
}
|
|
3100
|
+
}
|
|
3101
|
+
if (providers.size > 0) {
|
|
3102
|
+
config.enabledProviders = [...providers];
|
|
3103
|
+
}
|
|
3104
|
+
} else if (Array.isArray(raw.enabledProviders)) {
|
|
3105
|
+
config.enabledProviders = raw.enabledProviders.filter(
|
|
3106
|
+
(p) => PROVIDER_NAMES.includes(p)
|
|
3107
|
+
);
|
|
3108
|
+
}
|
|
3109
|
+
if (raw.providerModels !== void 0 && typeof raw.providerModels === "object" && raw.providerModels !== null) {
|
|
3110
|
+
const validated = {};
|
|
3111
|
+
for (const [providerName, modelCfg] of Object.entries(raw.providerModels)) {
|
|
3112
|
+
if (!PROVIDER_NAMES.includes(providerName)) continue;
|
|
3113
|
+
if (typeof modelCfg !== "object" || modelCfg === null) continue;
|
|
3114
|
+
const cfg = modelCfg;
|
|
3115
|
+
const entry = {};
|
|
3116
|
+
if (typeof cfg.strong === "string") entry.strong = cfg.strong;
|
|
3117
|
+
if (typeof cfg.fast === "string") entry.fast = cfg.fast;
|
|
3118
|
+
if (entry.strong !== void 0 || entry.fast !== void 0) {
|
|
3119
|
+
validated[providerName] = entry;
|
|
3120
|
+
}
|
|
3121
|
+
}
|
|
3122
|
+
if (Object.keys(validated).length > 0) {
|
|
3123
|
+
config.providerModels = validated;
|
|
3124
|
+
}
|
|
3125
|
+
}
|
|
3126
|
+
if (typeof raw.source === "string" && DATASOURCE_NAMES.includes(raw.source)) {
|
|
3127
|
+
config.source = raw.source;
|
|
3128
|
+
}
|
|
3129
|
+
if (typeof raw.planTimeout === "number") config.planTimeout = raw.planTimeout;
|
|
3130
|
+
if (typeof raw.specTimeout === "number") config.specTimeout = raw.specTimeout;
|
|
3131
|
+
if (typeof raw.specWarnTimeout === "number") config.specWarnTimeout = raw.specWarnTimeout;
|
|
3132
|
+
if (typeof raw.specKillTimeout === "number") config.specKillTimeout = raw.specKillTimeout;
|
|
3133
|
+
if (typeof raw.concurrency === "number") config.concurrency = raw.concurrency;
|
|
3134
|
+
if (typeof raw.maxRuns === "number") config.maxRuns = raw.maxRuns;
|
|
3135
|
+
if (typeof raw.org === "string") config.org = raw.org;
|
|
3136
|
+
if (typeof raw.project === "string") config.project = raw.project;
|
|
3137
|
+
if (typeof raw.workItemType === "string") config.workItemType = raw.workItemType;
|
|
3138
|
+
if (typeof raw.iteration === "string") config.iteration = raw.iteration;
|
|
3139
|
+
if (typeof raw.area === "string") config.area = raw.area;
|
|
3140
|
+
if (typeof raw.username === "string") config.username = raw.username;
|
|
3141
|
+
if (typeof raw.nextIssueId === "number") config.nextIssueId = raw.nextIssueId;
|
|
3142
|
+
return config;
|
|
3143
|
+
}
|
|
3144
|
+
async function saveConfig(config, configDir) {
|
|
3145
|
+
const configPath = getConfigPath(configDir);
|
|
3146
|
+
await mkdir3(dirname4(configPath), { recursive: true });
|
|
3147
|
+
await writeFile3(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
3148
|
+
}
|
|
3149
|
+
|
|
2325
3150
|
// src/spec-generator.ts
|
|
3151
|
+
import { cpus, freemem } from "os";
|
|
2326
3152
|
var MB_PER_CONCURRENT_TASK = 500;
|
|
2327
3153
|
var DEFAULT_SPEC_TIMEOUT_MIN = 10;
|
|
2328
3154
|
var DEFAULT_SPEC_WARN_MIN = 10;
|
|
@@ -2437,6 +3263,7 @@ async function resolveSource(issues, issueSource, cwd) {
|
|
|
2437
3263
|
}
|
|
2438
3264
|
|
|
2439
3265
|
// src/helpers/confirm-large-batch.tsx
|
|
3266
|
+
init_ink_prompts();
|
|
2440
3267
|
var LARGE_BATCH_THRESHOLD = 100;
|
|
2441
3268
|
async function confirmLargeBatch(count, threshold = LARGE_BATCH_THRESHOLD) {
|
|
2442
3269
|
if (count <= threshold) return true;
|
|
@@ -2512,8 +3339,224 @@ async function ensureGitignoreEntry(repoRoot, entry) {
|
|
|
2512
3339
|
}
|
|
2513
3340
|
}
|
|
2514
3341
|
|
|
3342
|
+
// src/orchestrator/runner.ts
|
|
3343
|
+
init_database();
|
|
3344
|
+
init_manager();
|
|
3345
|
+
|
|
3346
|
+
// src/queue/fork-run.ts
|
|
3347
|
+
init_manager();
|
|
3348
|
+
import { fork } from "child_process";
|
|
3349
|
+
import { existsSync } from "fs";
|
|
3350
|
+
import { fileURLToPath } from "url";
|
|
3351
|
+
import { dirname as dirname5, join as join9 } from "path";
|
|
3352
|
+
var __filename = fileURLToPath(import.meta.url);
|
|
3353
|
+
var __dirname = dirname5(__filename);
|
|
3354
|
+
var WORKER_PATH = [
|
|
3355
|
+
join9(__dirname, "mcp", "dispatch-worker.js"),
|
|
3356
|
+
// bundled: __dirname = dist/
|
|
3357
|
+
join9(__dirname, "..", "mcp", "dispatch-worker.js"),
|
|
3358
|
+
// unbundled: __dirname = dist/queue/
|
|
3359
|
+
join9(__dirname, "..", "dispatch-worker.js")
|
|
3360
|
+
// legacy unbundled: __dirname = dist/mcp/tools/
|
|
3361
|
+
].find((p) => existsSync(p)) ?? join9(__dirname, "mcp", "dispatch-worker.js");
|
|
3362
|
+
var HEARTBEAT_INTERVAL_MS = 3e4;
|
|
3363
|
+
function forkDispatchRun(runId, workerMessage, options) {
|
|
3364
|
+
if (!existsSync(WORKER_PATH)) {
|
|
3365
|
+
throw new Error(
|
|
3366
|
+
`Dispatch worker not found at ${WORKER_PATH}. Run 'npm run build' to compile the project before using MCP tools.`
|
|
3367
|
+
);
|
|
3368
|
+
}
|
|
3369
|
+
const worker = fork(WORKER_PATH, [], { stdio: ["pipe", "pipe", "pipe", "ipc"] });
|
|
3370
|
+
worker.send(workerMessage);
|
|
3371
|
+
const heartbeat = setInterval(() => {
|
|
3372
|
+
emitLog(runId, `Run ${runId} still in progress...`);
|
|
3373
|
+
}, HEARTBEAT_INTERVAL_MS);
|
|
3374
|
+
worker.on("message", (msg) => {
|
|
3375
|
+
const msgType = msg["type"];
|
|
3376
|
+
switch (msgType) {
|
|
3377
|
+
case "progress": {
|
|
3378
|
+
const event = msg["event"];
|
|
3379
|
+
const eventType = event["type"];
|
|
3380
|
+
switch (eventType) {
|
|
3381
|
+
case "task_start":
|
|
3382
|
+
createTask({
|
|
3383
|
+
runId,
|
|
3384
|
+
taskId: event["taskId"],
|
|
3385
|
+
taskText: event["taskText"],
|
|
3386
|
+
file: event["file"] ?? event["taskId"].split(":")[0] ?? "",
|
|
3387
|
+
line: event["line"] ?? parseInt(event["taskId"].split(":")[1] ?? "0", 10)
|
|
3388
|
+
});
|
|
3389
|
+
updateTaskStatus(runId, event["taskId"], "running");
|
|
3390
|
+
emitLog(runId, `Task started: ${event["taskText"]}`);
|
|
3391
|
+
break;
|
|
3392
|
+
case "task_done":
|
|
3393
|
+
updateTaskStatus(runId, event["taskId"], "success");
|
|
3394
|
+
emitLog(runId, `Task done: ${event["taskText"]}`);
|
|
3395
|
+
break;
|
|
3396
|
+
case "task_failed":
|
|
3397
|
+
updateTaskStatus(runId, event["taskId"], "failed", { error: event["error"] });
|
|
3398
|
+
emitLog(runId, `Task failed: ${event["taskText"]} \u2014 ${event["error"]}`, "error");
|
|
3399
|
+
break;
|
|
3400
|
+
case "phase_change":
|
|
3401
|
+
emitLog(runId, event["message"] ?? `Phase: ${event["phase"]}`);
|
|
3402
|
+
break;
|
|
3403
|
+
case "log":
|
|
3404
|
+
emitLog(runId, event["message"]);
|
|
3405
|
+
break;
|
|
3406
|
+
}
|
|
3407
|
+
break;
|
|
3408
|
+
}
|
|
3409
|
+
case "spec_progress": {
|
|
3410
|
+
const event = msg["event"];
|
|
3411
|
+
const eventType = event["type"];
|
|
3412
|
+
switch (eventType) {
|
|
3413
|
+
case "item_start":
|
|
3414
|
+
emitLog(runId, `Generating spec for: ${event["itemTitle"] ?? event["itemId"]}`);
|
|
3415
|
+
break;
|
|
3416
|
+
case "item_done":
|
|
3417
|
+
emitLog(runId, `Spec done: ${event["itemTitle"] ?? event["itemId"]}`);
|
|
3418
|
+
break;
|
|
3419
|
+
case "item_failed":
|
|
3420
|
+
emitLog(runId, `Spec failed: ${event["itemTitle"] ?? event["itemId"]} \u2014 ${event["error"]}`, "error");
|
|
3421
|
+
break;
|
|
3422
|
+
case "log":
|
|
3423
|
+
emitLog(runId, event["message"]);
|
|
3424
|
+
break;
|
|
3425
|
+
}
|
|
3426
|
+
break;
|
|
3427
|
+
}
|
|
3428
|
+
case "done": {
|
|
3429
|
+
const result = msg["result"];
|
|
3430
|
+
if (options?.onDone) {
|
|
3431
|
+
options.onDone(result);
|
|
3432
|
+
} else if ("completed" in result) {
|
|
3433
|
+
updateRunCounters(runId, result["total"], result["completed"], result["failed"]);
|
|
3434
|
+
finishRun(runId, result["failed"] > 0 ? "failed" : "completed");
|
|
3435
|
+
emitLog(runId, `Dispatch complete: ${result["completed"]}/${result["total"]} tasks succeeded`);
|
|
3436
|
+
} else if ("generated" in result) {
|
|
3437
|
+
const total = result["total"];
|
|
3438
|
+
const generated = result["generated"];
|
|
3439
|
+
const failed = result["failed"];
|
|
3440
|
+
finishSpecRun(runId, failed > 0 ? "failed" : "completed", { total, generated, failed });
|
|
3441
|
+
emitLog(runId, `Spec complete: ${generated}/${total} specs generated`);
|
|
3442
|
+
}
|
|
3443
|
+
break;
|
|
3444
|
+
}
|
|
3445
|
+
case "error": {
|
|
3446
|
+
const errorMsg = msg["message"];
|
|
3447
|
+
if (options?.runType === "spec_runs") {
|
|
3448
|
+
finishSpecRun(runId, "failed", { total: 0, generated: 0, failed: 0 }, errorMsg);
|
|
3449
|
+
} else {
|
|
3450
|
+
finishRun(runId, "failed", errorMsg);
|
|
3451
|
+
}
|
|
3452
|
+
emitLog(runId, `Run error: ${errorMsg}`, "error");
|
|
3453
|
+
break;
|
|
3454
|
+
}
|
|
3455
|
+
}
|
|
3456
|
+
});
|
|
3457
|
+
worker.on("exit", (code) => {
|
|
3458
|
+
clearInterval(heartbeat);
|
|
3459
|
+
if (code !== 0 && code !== null) {
|
|
3460
|
+
const exitError = `Worker process exited with code ${code}`;
|
|
3461
|
+
if (options?.runType === "spec_runs") {
|
|
3462
|
+
finishSpecRun(runId, "failed", { total: 0, generated: 0, failed: 0 }, exitError);
|
|
3463
|
+
} else {
|
|
3464
|
+
finishRun(runId, "failed", exitError);
|
|
3465
|
+
}
|
|
3466
|
+
emitLog(runId, `Worker process exited unexpectedly (code ${code})`, "error");
|
|
3467
|
+
}
|
|
3468
|
+
options?.onExit?.(code);
|
|
3469
|
+
});
|
|
3470
|
+
return worker;
|
|
3471
|
+
}
|
|
3472
|
+
|
|
3473
|
+
// src/queue/run-queue.ts
|
|
3474
|
+
init_manager();
|
|
3475
|
+
var RunQueue = class {
|
|
3476
|
+
limit;
|
|
3477
|
+
/** In-memory map of runId → context needed to fork (callbacks). */
|
|
3478
|
+
contexts = /* @__PURE__ */ new Map();
|
|
3479
|
+
constructor(limit) {
|
|
3480
|
+
this.limit = Math.max(1, limit);
|
|
3481
|
+
}
|
|
3482
|
+
/**
|
|
3483
|
+
* Enqueue a run for execution. The run must already exist in the DB with
|
|
3484
|
+
* status "queued" and a serialized workerMessage.
|
|
3485
|
+
*
|
|
3486
|
+
* If a slot is available the run is forked immediately; otherwise it waits
|
|
3487
|
+
* in the DB queue until a running child exits.
|
|
3488
|
+
*/
|
|
3489
|
+
enqueue(runId, logCallback, options) {
|
|
3490
|
+
this.contexts.set(runId, { logCallback, options });
|
|
3491
|
+
if (logCallback) {
|
|
3492
|
+
addLogCallback(runId, logCallback);
|
|
3493
|
+
}
|
|
3494
|
+
this.drain();
|
|
3495
|
+
}
|
|
3496
|
+
/**
|
|
3497
|
+
* Drain the queue: fork queued runs while slots are available.
|
|
3498
|
+
* Called on enqueue and whenever a child process exits.
|
|
3499
|
+
*/
|
|
3500
|
+
drain() {
|
|
3501
|
+
let active = countActiveRuns();
|
|
3502
|
+
while (active < this.limit) {
|
|
3503
|
+
const next = getNextQueued();
|
|
3504
|
+
if (!next) break;
|
|
3505
|
+
const { runId, workerMessage, table } = next;
|
|
3506
|
+
if (table === "runs") {
|
|
3507
|
+
markRunStarted(runId);
|
|
3508
|
+
} else {
|
|
3509
|
+
markSpecRunStarted(runId);
|
|
3510
|
+
}
|
|
3511
|
+
let parsedMessage;
|
|
3512
|
+
try {
|
|
3513
|
+
parsedMessage = JSON.parse(workerMessage);
|
|
3514
|
+
} catch {
|
|
3515
|
+
emitLog(runId, `Failed to parse queued worker message`, "error");
|
|
3516
|
+
continue;
|
|
3517
|
+
}
|
|
3518
|
+
const ctx = this.contexts.get(runId);
|
|
3519
|
+
if (!ctx) {
|
|
3520
|
+
continue;
|
|
3521
|
+
}
|
|
3522
|
+
this.contexts.delete(runId);
|
|
3523
|
+
emitLog(runId, `Run started (${active + 1}/${this.limit} slots used)`);
|
|
3524
|
+
const userOnExit = ctx.options?.onExit;
|
|
3525
|
+
forkDispatchRun(runId, parsedMessage, {
|
|
3526
|
+
...ctx.options,
|
|
3527
|
+
runType: table,
|
|
3528
|
+
logCallback: ctx.logCallback,
|
|
3529
|
+
onExit: (code) => {
|
|
3530
|
+
userOnExit?.(code);
|
|
3531
|
+
this.drain();
|
|
3532
|
+
}
|
|
3533
|
+
});
|
|
3534
|
+
active++;
|
|
3535
|
+
}
|
|
3536
|
+
}
|
|
3537
|
+
/** Mark all queued runs as failed (for graceful shutdown). */
|
|
3538
|
+
abort() {
|
|
3539
|
+
failAllQueuedRuns();
|
|
3540
|
+
this.contexts.clear();
|
|
3541
|
+
}
|
|
3542
|
+
/** Current concurrency limit. */
|
|
3543
|
+
get maxRuns() {
|
|
3544
|
+
return this.limit;
|
|
3545
|
+
}
|
|
3546
|
+
};
|
|
3547
|
+
var _queue = null;
|
|
3548
|
+
function initRunQueue(limit) {
|
|
3549
|
+
_queue = new RunQueue(limit);
|
|
3550
|
+
}
|
|
3551
|
+
function getRunQueue() {
|
|
3552
|
+
if (!_queue) {
|
|
3553
|
+
throw new Error("RunQueue not initialized. Call initRunQueue(limit) first.");
|
|
3554
|
+
}
|
|
3555
|
+
return _queue;
|
|
3556
|
+
}
|
|
3557
|
+
|
|
2515
3558
|
// src/orchestrator/cli-config.ts
|
|
2516
|
-
import { join as
|
|
3559
|
+
import { join as join10 } from "path";
|
|
2517
3560
|
import { access } from "fs/promises";
|
|
2518
3561
|
import { constants } from "fs";
|
|
2519
3562
|
var CONFIG_TO_CLI = {
|
|
@@ -2537,11 +3580,12 @@ function setCliField(target, key, value) {
|
|
|
2537
3580
|
}
|
|
2538
3581
|
async function resolveCliConfig(args) {
|
|
2539
3582
|
const { explicitFlags } = args;
|
|
2540
|
-
const configDir =
|
|
3583
|
+
const configDir = join10(args.cwd, ".dispatch");
|
|
2541
3584
|
const config = await loadConfig(configDir);
|
|
2542
3585
|
const merged = { ...args };
|
|
2543
3586
|
for (const configKey of CONFIG_KEYS) {
|
|
2544
3587
|
const cliField = CONFIG_TO_CLI[configKey];
|
|
3588
|
+
if (!cliField) continue;
|
|
2545
3589
|
const configValue = config[configKey];
|
|
2546
3590
|
if (configValue !== void 0 && !explicitFlags.has(cliField)) {
|
|
2547
3591
|
setCliField(merged, cliField, configValue);
|
|
@@ -2583,9 +3627,9 @@ async function resolveCliConfig(args) {
|
|
|
2583
3627
|
}
|
|
2584
3628
|
|
|
2585
3629
|
// src/orchestrator/spec-pipeline.ts
|
|
2586
|
-
import { join as
|
|
3630
|
+
import { join as join12, resolve as resolve2 } from "path";
|
|
2587
3631
|
import { mkdir as mkdir4, readFile as readFile8, rename as rename2, unlink as unlink2 } from "fs/promises";
|
|
2588
|
-
import { randomUUID as
|
|
3632
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
2589
3633
|
import { glob as glob2 } from "glob";
|
|
2590
3634
|
|
|
2591
3635
|
// src/providers/router.ts
|
|
@@ -3023,6 +4067,7 @@ function registerCleanup(fn) {
|
|
|
3023
4067
|
import chalk4 from "chalk";
|
|
3024
4068
|
|
|
3025
4069
|
// src/tui.tsx
|
|
4070
|
+
init_format();
|
|
3026
4071
|
import { useState as useState2, useEffect as useEffect2 } from "react";
|
|
3027
4072
|
import { render as render2, Box as Box2, Text as Text2, Spacer, useInput as useInput2 } from "ink";
|
|
3028
4073
|
import Spinner from "ink-spinner";
|
|
@@ -3806,6 +4851,9 @@ function createTui(options) {
|
|
|
3806
4851
|
return { state, update, stop, waitForRecoveryAction };
|
|
3807
4852
|
}
|
|
3808
4853
|
|
|
4854
|
+
// src/orchestrator/spec-pipeline.ts
|
|
4855
|
+
init_format();
|
|
4856
|
+
|
|
3809
4857
|
// src/helpers/retry.ts
|
|
3810
4858
|
var DEFAULT_RETRY_COUNT = 3;
|
|
3811
4859
|
async function withRetry(fn, maxRetries, options) {
|
|
@@ -3874,7 +4922,7 @@ async function runWithConcurrency(options) {
|
|
|
3874
4922
|
}
|
|
3875
4923
|
|
|
3876
4924
|
// src/orchestrator/datasource-helpers.ts
|
|
3877
|
-
import { basename as basename2, join as
|
|
4925
|
+
import { basename as basename2, join as join11 } from "path";
|
|
3878
4926
|
import { mkdtemp, writeFile as writeFile6 } from "fs/promises";
|
|
3879
4927
|
import { tmpdir } from "os";
|
|
3880
4928
|
import { execFile as execFile8 } from "child_process";
|
|
@@ -3903,14 +4951,14 @@ async function fetchItemsById(issueIds, datasource4, fetchOpts) {
|
|
|
3903
4951
|
return items;
|
|
3904
4952
|
}
|
|
3905
4953
|
async function writeItemsToTempDir(items) {
|
|
3906
|
-
const tempDir = await mkdtemp(
|
|
4954
|
+
const tempDir = await mkdtemp(join11(tmpdir(), "dispatch-"));
|
|
3907
4955
|
const files = [];
|
|
3908
4956
|
const issueDetailsByFile = /* @__PURE__ */ new Map();
|
|
3909
4957
|
for (const item of items) {
|
|
3910
4958
|
const slug = slugify(item.title, MAX_SLUG_LENGTH);
|
|
3911
4959
|
const id = item.number.includes("/") || item.number.includes("\\") ? basename2(item.number, ".md") : item.number;
|
|
3912
4960
|
const filename = `${id}-${slug}.md`;
|
|
3913
|
-
const filepath =
|
|
4961
|
+
const filepath = join11(tempDir, filename);
|
|
3914
4962
|
await writeFile6(filepath, item.body, "utf-8");
|
|
3915
4963
|
files.push(filepath);
|
|
3916
4964
|
issueDetailsByFile.set(filepath, item);
|
|
@@ -4093,7 +5141,7 @@ function buildInlineTextItem(issues, outputDir) {
|
|
|
4093
5141
|
const title = text.length > 80 ? text.slice(0, 80).trimEnd() + "\u2026" : text;
|
|
4094
5142
|
const slug = slugify(text, MAX_SLUG_LENGTH);
|
|
4095
5143
|
const filename = `${slug}.md`;
|
|
4096
|
-
const filepath =
|
|
5144
|
+
const filepath = join12(outputDir, filename);
|
|
4097
5145
|
const details = {
|
|
4098
5146
|
number: filepath,
|
|
4099
5147
|
title,
|
|
@@ -4155,7 +5203,7 @@ function previewDryRun(validItems, items, isTrackerMode, isInlineText, outputDir
|
|
|
4155
5203
|
let filepath;
|
|
4156
5204
|
if (isTrackerMode) {
|
|
4157
5205
|
const slug = slugify(details.title, 60);
|
|
4158
|
-
filepath =
|
|
5206
|
+
filepath = join12(outputDir, `${id}-${slug}.md`);
|
|
4159
5207
|
} else {
|
|
4160
5208
|
filepath = id;
|
|
4161
5209
|
}
|
|
@@ -4222,7 +5270,7 @@ async function generateSpecsBatch(validItems, items, provider, isTrackerMode, is
|
|
|
4222
5270
|
if (isTrackerMode) {
|
|
4223
5271
|
const slug = slugify(details.title, MAX_SLUG_LENGTH);
|
|
4224
5272
|
const filename = `${id}-${slug}.md`;
|
|
4225
|
-
filepath =
|
|
5273
|
+
filepath = join12(outputDir, filename);
|
|
4226
5274
|
} else if (isInlineText) {
|
|
4227
5275
|
filepath = id;
|
|
4228
5276
|
} else {
|
|
@@ -4233,9 +5281,9 @@ async function generateSpecsBatch(validItems, items, provider, isTrackerMode, is
|
|
|
4233
5281
|
fileLoggerStorage.getStore()?.info(`Starting spec generation for ${isTrackerMode ? `#${id}` : filepath}`);
|
|
4234
5282
|
if (!quiet) log.info(`Generating spec for ${isTrackerMode ? `#${id}` : filepath}: ${details.title}...`);
|
|
4235
5283
|
const generateLabel = `dispatch(spec, ${isTrackerMode ? `#${id}` : filepath})`;
|
|
4236
|
-
const tmpDir =
|
|
5284
|
+
const tmpDir = join12(resolve2(specCwd), ".dispatch", "tmp");
|
|
4237
5285
|
await mkdir4(tmpDir, { recursive: true });
|
|
4238
|
-
const tmpPath =
|
|
5286
|
+
const tmpPath = join12(tmpDir, `spec-${randomUUID4()}.md`);
|
|
4239
5287
|
const specInput = {
|
|
4240
5288
|
issue: isTrackerMode ? details : void 0,
|
|
4241
5289
|
filePath: isTrackerMode ? void 0 : id,
|
|
@@ -4270,7 +5318,7 @@ async function generateSpecsBatch(validItems, items, provider, isTrackerMode, is
|
|
|
4270
5318
|
const h1Title = extractTitle(result.data.content, filepath);
|
|
4271
5319
|
const h1Slug = slugify(h1Title, MAX_SLUG_LENGTH);
|
|
4272
5320
|
const finalFilename = isTrackerMode ? `${id}-${h1Slug}.md` : `${h1Slug}.md`;
|
|
4273
|
-
const finalFilepath =
|
|
5321
|
+
const finalFilepath = join12(outputDir, finalFilename);
|
|
4274
5322
|
if (finalFilepath !== filepath) {
|
|
4275
5323
|
await rename2(filepath, finalFilepath);
|
|
4276
5324
|
filepath = finalFilepath;
|
|
@@ -4418,7 +5466,7 @@ async function runSpecPipeline(opts) {
|
|
|
4418
5466
|
providerModels,
|
|
4419
5467
|
serverUrl,
|
|
4420
5468
|
cwd: specCwd,
|
|
4421
|
-
outputDir =
|
|
5469
|
+
outputDir = join12(specCwd, ".dispatch", "specs"),
|
|
4422
5470
|
org,
|
|
4423
5471
|
project,
|
|
4424
5472
|
workItemType,
|
|
@@ -4955,11 +6003,11 @@ function parseCommitResponse(response) {
|
|
|
4955
6003
|
}
|
|
4956
6004
|
|
|
4957
6005
|
// src/helpers/worktree.ts
|
|
4958
|
-
import { join as
|
|
6006
|
+
import { join as join13, basename as basename3 } from "path";
|
|
4959
6007
|
import { execFile as execFile9 } from "child_process";
|
|
4960
6008
|
import { promisify as promisify9 } from "util";
|
|
4961
|
-
import { randomUUID as
|
|
4962
|
-
import { existsSync } from "fs";
|
|
6009
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
6010
|
+
import { existsSync as existsSync2 } from "fs";
|
|
4963
6011
|
import { rm } from "fs/promises";
|
|
4964
6012
|
var exec8 = promisify9(execFile9);
|
|
4965
6013
|
var WORKTREE_DIR = ".dispatch/worktrees";
|
|
@@ -4975,8 +6023,8 @@ function worktreeName(issueFilename) {
|
|
|
4975
6023
|
}
|
|
4976
6024
|
async function createWorktree(repoRoot, issueFilename, branchName, startPoint) {
|
|
4977
6025
|
const name = worktreeName(issueFilename);
|
|
4978
|
-
const worktreePath =
|
|
4979
|
-
if (
|
|
6026
|
+
const worktreePath = join13(repoRoot, WORKTREE_DIR, name);
|
|
6027
|
+
if (existsSync2(worktreePath)) {
|
|
4980
6028
|
try {
|
|
4981
6029
|
const listOutput = await git4(["worktree", "list", "--porcelain"], repoRoot);
|
|
4982
6030
|
const isRegistered = listOutput.includes(`worktree ${worktreePath}
|
|
@@ -5057,7 +6105,7 @@ async function createWorktree(repoRoot, issueFilename, branchName, startPoint) {
|
|
|
5057
6105
|
}
|
|
5058
6106
|
async function removeWorktree(repoRoot, issueFilename) {
|
|
5059
6107
|
const name = worktreeName(issueFilename);
|
|
5060
|
-
const worktreePath =
|
|
6108
|
+
const worktreePath = join13(repoRoot, WORKTREE_DIR, name);
|
|
5061
6109
|
try {
|
|
5062
6110
|
await git4(["worktree", "remove", worktreePath], repoRoot);
|
|
5063
6111
|
} catch {
|
|
@@ -5075,7 +6123,7 @@ async function removeWorktree(repoRoot, issueFilename) {
|
|
|
5075
6123
|
}
|
|
5076
6124
|
}
|
|
5077
6125
|
function generateFeatureBranchName() {
|
|
5078
|
-
const uuid =
|
|
6126
|
+
const uuid = randomUUID5();
|
|
5079
6127
|
const octet = uuid.split("-")[0];
|
|
5080
6128
|
return `dispatch/feature-${octet}`;
|
|
5081
6129
|
}
|
|
@@ -5180,11 +6228,12 @@ var ProviderPool = class {
|
|
|
5180
6228
|
};
|
|
5181
6229
|
|
|
5182
6230
|
// src/orchestrator/dispatch-pipeline.ts
|
|
6231
|
+
init_format();
|
|
5183
6232
|
import chalk5 from "chalk";
|
|
5184
6233
|
|
|
5185
6234
|
// src/helpers/run-state.ts
|
|
5186
6235
|
import { readFile as readFile10, mkdir as mkdir5 } from "fs/promises";
|
|
5187
|
-
import { join as
|
|
6236
|
+
import { join as join14, basename as basename4 } from "path";
|
|
5188
6237
|
import { z } from "zod";
|
|
5189
6238
|
var RunStateTaskStatusSchema = z.enum(["pending", "running", "success", "failed"]);
|
|
5190
6239
|
var RunStateTaskSchema = z.object({
|
|
@@ -5766,6 +6815,7 @@ ${err.stack}` : ""}`);
|
|
|
5766
6815
|
}
|
|
5767
6816
|
if (stopAfterIssue) break;
|
|
5768
6817
|
}
|
|
6818
|
+
let commitSkillResult;
|
|
5769
6819
|
if (!preserveContext) {
|
|
5770
6820
|
if (!noBranch && branchName && defaultBranch && details && datasource4.supportsGit()) {
|
|
5771
6821
|
try {
|
|
@@ -5779,7 +6829,6 @@ ${err.stack}` : ""}`);
|
|
|
5779
6829
|
}
|
|
5780
6830
|
}
|
|
5781
6831
|
fileLogger?.phase("Commit generation");
|
|
5782
|
-
let commitSkillResult;
|
|
5783
6832
|
if (!noBranch && branchName && defaultBranch && details && datasource4.supportsGit()) {
|
|
5784
6833
|
try {
|
|
5785
6834
|
const branchDiff = await getBranchDiff(defaultBranch, issueCwd);
|
|
@@ -5810,99 +6859,99 @@ ${err.stack}` : ""}`);
|
|
|
5810
6859
|
log.warn(`Commit agent error for issue #${details.number}: ${log.formatErrorChain(err)}`);
|
|
5811
6860
|
}
|
|
5812
6861
|
}
|
|
5813
|
-
|
|
5814
|
-
|
|
5815
|
-
|
|
5816
|
-
|
|
5817
|
-
|
|
5818
|
-
await removeWorktree(cwd, file);
|
|
5819
|
-
} catch (err) {
|
|
5820
|
-
log.warn(`Could not remove worktree for issue #${details.number}: ${log.formatErrorChain(err)}`);
|
|
5821
|
-
}
|
|
5822
|
-
}
|
|
6862
|
+
}
|
|
6863
|
+
fileLogger?.phase("PR lifecycle");
|
|
6864
|
+
if (!noBranch && branchName && defaultBranch && details) {
|
|
6865
|
+
if (feature && featureBranchName && !preserveContext) {
|
|
6866
|
+
if (worktreePath) {
|
|
5823
6867
|
try {
|
|
5824
|
-
await
|
|
5825
|
-
await exec9("git", ["merge", branchName, "--no-ff", "-m", `merge: issue #${details.number}`], { cwd, shell: process.platform === "win32" });
|
|
5826
|
-
log.debug(`Merged ${branchName} into ${featureBranchName}`);
|
|
6868
|
+
await removeWorktree(cwd, file);
|
|
5827
6869
|
} catch (err) {
|
|
5828
|
-
|
|
5829
|
-
log.warn(mergeError);
|
|
5830
|
-
try {
|
|
5831
|
-
await exec9("git", ["merge", "--abort"], { cwd, shell: process.platform === "win32" });
|
|
5832
|
-
} catch {
|
|
5833
|
-
}
|
|
5834
|
-
for (const task of fileTasks) {
|
|
5835
|
-
const tuiTask = tui.state.tasks.find((t) => t.task === task);
|
|
5836
|
-
if (tuiTask) {
|
|
5837
|
-
tuiTask.status = "failed";
|
|
5838
|
-
tuiTask.error = mergeError;
|
|
5839
|
-
}
|
|
5840
|
-
upsertResult(results, { task, success: false, error: mergeError });
|
|
5841
|
-
}
|
|
5842
|
-
return { halted: false };
|
|
6870
|
+
log.warn(`Could not remove worktree for issue #${details.number}: ${log.formatErrorChain(err)}`);
|
|
5843
6871
|
}
|
|
6872
|
+
}
|
|
6873
|
+
try {
|
|
6874
|
+
await datasource4.switchBranch(featureBranchName, lifecycleOpts);
|
|
6875
|
+
await exec9("git", ["merge", branchName, "--no-ff", "-m", `merge: issue #${details.number}`], { cwd, shell: process.platform === "win32" });
|
|
6876
|
+
log.debug(`Merged ${branchName} into ${featureBranchName}`);
|
|
6877
|
+
} catch (err) {
|
|
6878
|
+
const mergeError = `Could not merge ${branchName} into feature branch: ${log.formatErrorChain(err)}`;
|
|
6879
|
+
log.warn(mergeError);
|
|
5844
6880
|
try {
|
|
5845
|
-
await exec9("git", ["
|
|
5846
|
-
|
|
5847
|
-
}
|
|
5848
|
-
|
|
6881
|
+
await exec9("git", ["merge", "--abort"], { cwd, shell: process.platform === "win32" });
|
|
6882
|
+
} catch {
|
|
6883
|
+
}
|
|
6884
|
+
for (const task of fileTasks) {
|
|
6885
|
+
const tuiTask = tui.state.tasks.find((t) => t.task === task);
|
|
6886
|
+
if (tuiTask) {
|
|
6887
|
+
tuiTask.status = "failed";
|
|
6888
|
+
tuiTask.error = mergeError;
|
|
6889
|
+
}
|
|
6890
|
+
upsertResult(results, { task, success: false, error: mergeError });
|
|
5849
6891
|
}
|
|
6892
|
+
return { halted: false };
|
|
6893
|
+
}
|
|
6894
|
+
try {
|
|
6895
|
+
await exec9("git", ["branch", "-d", branchName], { cwd, shell: process.platform === "win32" });
|
|
6896
|
+
log.debug(`Deleted local branch ${branchName}`);
|
|
6897
|
+
} catch (err) {
|
|
6898
|
+
log.warn(`Could not delete local branch ${branchName}: ${log.formatErrorChain(err)}`);
|
|
6899
|
+
}
|
|
6900
|
+
try {
|
|
6901
|
+
await datasource4.switchBranch(featureDefaultBranch, lifecycleOpts);
|
|
6902
|
+
} catch (err) {
|
|
6903
|
+
log.warn(`Could not switch back to ${featureDefaultBranch}: ${log.formatErrorChain(err)}`);
|
|
6904
|
+
}
|
|
6905
|
+
} else if (!feature) {
|
|
6906
|
+
if (datasource4.supportsGit()) {
|
|
5850
6907
|
try {
|
|
5851
|
-
await datasource4.
|
|
6908
|
+
await datasource4.pushBranch(branchName, issueLifecycleOpts);
|
|
6909
|
+
log.debug(`Pushed branch ${branchName}`);
|
|
6910
|
+
fileLogger?.info(`Pushed branch ${branchName}`);
|
|
5852
6911
|
} catch (err) {
|
|
5853
|
-
log.warn(`Could not
|
|
6912
|
+
log.warn(`Could not push branch ${branchName}: ${log.formatErrorChain(err)}`);
|
|
5854
6913
|
}
|
|
5855
|
-
}
|
|
5856
|
-
|
|
5857
|
-
|
|
5858
|
-
|
|
5859
|
-
|
|
5860
|
-
|
|
5861
|
-
|
|
5862
|
-
|
|
6914
|
+
}
|
|
6915
|
+
if (datasource4.supportsGit()) {
|
|
6916
|
+
try {
|
|
6917
|
+
const prTitle = commitSkillResult?.success && commitSkillResult.data.prTitle || await buildPrTitle(details.title, defaultBranch, issueLifecycleOpts.cwd);
|
|
6918
|
+
const prBody = commitSkillResult?.success && commitSkillResult.data.prDescription || await buildPrBody(
|
|
6919
|
+
details,
|
|
6920
|
+
fileTasks,
|
|
6921
|
+
issueResults,
|
|
6922
|
+
defaultBranch,
|
|
6923
|
+
datasource4.name,
|
|
6924
|
+
issueLifecycleOpts.cwd
|
|
6925
|
+
);
|
|
6926
|
+
const prUrl = await datasource4.createPullRequest(
|
|
6927
|
+
branchName,
|
|
6928
|
+
details.number,
|
|
6929
|
+
prTitle,
|
|
6930
|
+
prBody,
|
|
6931
|
+
issueLifecycleOpts,
|
|
6932
|
+
startingBranch
|
|
6933
|
+
);
|
|
6934
|
+
if (prUrl) {
|
|
6935
|
+
log.success(`Created PR for issue #${details.number}: ${prUrl}`);
|
|
6936
|
+
fileLogger?.info(`Created PR: ${prUrl}`);
|
|
5863
6937
|
}
|
|
6938
|
+
} catch (err) {
|
|
6939
|
+
log.warn(`Could not create PR for issue #${details.number}: ${log.formatErrorChain(err)}`);
|
|
6940
|
+
fileLogger?.warn(`PR creation failed: ${log.extractMessage(err)}`);
|
|
5864
6941
|
}
|
|
5865
|
-
|
|
5866
|
-
|
|
5867
|
-
|
|
5868
|
-
|
|
5869
|
-
|
|
5870
|
-
|
|
5871
|
-
issueResults,
|
|
5872
|
-
defaultBranch,
|
|
5873
|
-
datasource4.name,
|
|
5874
|
-
issueLifecycleOpts.cwd
|
|
5875
|
-
);
|
|
5876
|
-
const prUrl = await datasource4.createPullRequest(
|
|
5877
|
-
branchName,
|
|
5878
|
-
details.number,
|
|
5879
|
-
prTitle,
|
|
5880
|
-
prBody,
|
|
5881
|
-
issueLifecycleOpts,
|
|
5882
|
-
startingBranch
|
|
5883
|
-
);
|
|
5884
|
-
if (prUrl) {
|
|
5885
|
-
log.success(`Created PR for issue #${details.number}: ${prUrl}`);
|
|
5886
|
-
fileLogger?.info(`Created PR: ${prUrl}`);
|
|
5887
|
-
}
|
|
5888
|
-
} catch (err) {
|
|
5889
|
-
log.warn(`Could not create PR for issue #${details.number}: ${log.formatErrorChain(err)}`);
|
|
5890
|
-
fileLogger?.warn(`PR creation failed: ${log.extractMessage(err)}`);
|
|
5891
|
-
}
|
|
6942
|
+
}
|
|
6943
|
+
if (useWorktrees && worktreePath && !preserveContext) {
|
|
6944
|
+
try {
|
|
6945
|
+
await removeWorktree(cwd, file);
|
|
6946
|
+
} catch (err) {
|
|
6947
|
+
log.warn(`Could not remove worktree for issue #${details.number}: ${log.formatErrorChain(err)}`);
|
|
5892
6948
|
}
|
|
5893
|
-
|
|
5894
|
-
|
|
5895
|
-
|
|
5896
|
-
|
|
5897
|
-
|
|
5898
|
-
}
|
|
5899
|
-
} else if (!useWorktrees && datasource4.supportsGit()) {
|
|
5900
|
-
try {
|
|
5901
|
-
await datasource4.switchBranch(defaultBranch, lifecycleOpts);
|
|
5902
|
-
log.debug(`Switched back to ${defaultBranch}`);
|
|
5903
|
-
} catch (err) {
|
|
5904
|
-
log.warn(`Could not switch back to ${defaultBranch}: ${log.formatErrorChain(err)}`);
|
|
5905
|
-
}
|
|
6949
|
+
} else if (!useWorktrees && datasource4.supportsGit()) {
|
|
6950
|
+
try {
|
|
6951
|
+
await datasource4.switchBranch(defaultBranch, lifecycleOpts);
|
|
6952
|
+
log.debug(`Switched back to ${defaultBranch}`);
|
|
6953
|
+
} catch (err) {
|
|
6954
|
+
log.warn(`Could not switch back to ${defaultBranch}: ${log.formatErrorChain(err)}`);
|
|
5906
6955
|
}
|
|
5907
6956
|
}
|
|
5908
6957
|
}
|
|
@@ -6095,109 +7144,294 @@ async function boot5(opts) {
|
|
|
6095
7144
|
log.error("--feature and --no-branch are mutually exclusive");
|
|
6096
7145
|
process.exit(1);
|
|
6097
7146
|
}
|
|
6098
|
-
if (m.
|
|
6099
|
-
|
|
6100
|
-
issues
|
|
6101
|
-
|
|
7147
|
+
if (m.dryRun) {
|
|
7148
|
+
if (m.spec || m.respec) {
|
|
7149
|
+
let issues;
|
|
7150
|
+
if (m.spec) {
|
|
7151
|
+
issues = m.spec;
|
|
7152
|
+
} else {
|
|
7153
|
+
const respecArgs = m.respec;
|
|
7154
|
+
const isEmpty = Array.isArray(respecArgs) && respecArgs.length === 0;
|
|
7155
|
+
if (isEmpty) {
|
|
7156
|
+
const source = await resolveSource(respecArgs, m.issueSource, m.cwd);
|
|
7157
|
+
if (!source) process.exit(1);
|
|
7158
|
+
const datasource4 = getDatasource(source);
|
|
7159
|
+
const existing = await datasource4.list({ cwd: m.cwd, org: m.org, project: m.project, workItemType: m.workItemType, iteration: m.iteration, area: m.area });
|
|
7160
|
+
if (existing.length === 0) {
|
|
7161
|
+
log.error("No existing specs found to regenerate");
|
|
7162
|
+
process.exit(1);
|
|
7163
|
+
}
|
|
7164
|
+
const identifiers = existing.map((item) => item.number);
|
|
7165
|
+
const allNumeric = identifiers.every((id) => /^\d+$/.test(id));
|
|
7166
|
+
issues = allNumeric ? identifiers.join(",") : identifiers;
|
|
7167
|
+
const confirmed = await confirmLargeBatch(existing.length);
|
|
7168
|
+
if (!confirmed) process.exit(0);
|
|
7169
|
+
} else {
|
|
7170
|
+
issues = respecArgs;
|
|
7171
|
+
}
|
|
7172
|
+
}
|
|
7173
|
+
return this.generateSpecs({
|
|
7174
|
+
issues,
|
|
7175
|
+
issueSource: m.issueSource,
|
|
7176
|
+
provider: m.provider,
|
|
7177
|
+
enabledProviders: m.enabledProviders,
|
|
7178
|
+
providerModels: m.providerModels,
|
|
7179
|
+
serverUrl: m.serverUrl,
|
|
7180
|
+
cwd: m.cwd,
|
|
7181
|
+
outputDir: m.outputDir,
|
|
7182
|
+
org: m.org,
|
|
7183
|
+
project: m.project,
|
|
7184
|
+
workItemType: m.workItemType,
|
|
7185
|
+
iteration: m.iteration,
|
|
7186
|
+
area: m.area,
|
|
7187
|
+
concurrency: m.concurrency,
|
|
7188
|
+
dryRun: true,
|
|
7189
|
+
retries: m.retries,
|
|
7190
|
+
specTimeout: m.specTimeout ?? DEFAULT_SPEC_TIMEOUT_MIN,
|
|
7191
|
+
specWarnTimeout: m.specWarnTimeout,
|
|
7192
|
+
specKillTimeout: m.specKillTimeout
|
|
7193
|
+
});
|
|
7194
|
+
}
|
|
7195
|
+
return this.orchestrate({
|
|
7196
|
+
issueIds: m.issueIds,
|
|
7197
|
+
concurrency: m.concurrency ?? defaultConcurrency(),
|
|
7198
|
+
dryRun: true,
|
|
7199
|
+
noPlan: m.noPlan,
|
|
7200
|
+
noBranch: m.noBranch,
|
|
7201
|
+
noWorktree: m.noWorktree,
|
|
6102
7202
|
provider: m.provider,
|
|
6103
7203
|
enabledProviders: m.enabledProviders,
|
|
6104
7204
|
providerModels: m.providerModels,
|
|
6105
7205
|
serverUrl: m.serverUrl,
|
|
6106
|
-
|
|
6107
|
-
outputDir: m.outputDir,
|
|
7206
|
+
source: m.issueSource,
|
|
6108
7207
|
org: m.org,
|
|
6109
7208
|
project: m.project,
|
|
6110
7209
|
workItemType: m.workItemType,
|
|
6111
7210
|
iteration: m.iteration,
|
|
6112
7211
|
area: m.area,
|
|
6113
|
-
|
|
6114
|
-
|
|
7212
|
+
planTimeout: m.planTimeout,
|
|
7213
|
+
planRetries: m.planRetries,
|
|
6115
7214
|
retries: m.retries,
|
|
6116
|
-
|
|
6117
|
-
|
|
6118
|
-
|
|
7215
|
+
force: m.force,
|
|
7216
|
+
feature: m.feature,
|
|
7217
|
+
username: m.username
|
|
6119
7218
|
});
|
|
6120
7219
|
}
|
|
6121
|
-
|
|
6122
|
-
|
|
6123
|
-
|
|
7220
|
+
openDatabase(cwd);
|
|
7221
|
+
const concurrency = m.concurrency ?? defaultConcurrency();
|
|
7222
|
+
const maxRuns = Math.min(Math.max(4, defaultConcurrency() * 2), CONFIG_BOUNDS.maxRuns.max);
|
|
7223
|
+
if (m.resume !== void 0) {
|
|
7224
|
+
return runResumeFlow(m.resume, cwd, maxRuns);
|
|
7225
|
+
}
|
|
7226
|
+
const sessionId = randomUUID6();
|
|
7227
|
+
markOrphanedRunsFailed({ exceptSessionId: sessionId });
|
|
7228
|
+
initRunQueue(maxRuns);
|
|
7229
|
+
if (m.spec || m.respec) {
|
|
6124
7230
|
let issues;
|
|
6125
|
-
if (
|
|
6126
|
-
|
|
6127
|
-
if (!source) {
|
|
6128
|
-
process.exit(1);
|
|
6129
|
-
}
|
|
6130
|
-
const datasource4 = getDatasource(source);
|
|
6131
|
-
const existing = await datasource4.list({ cwd: m.cwd, org: m.org, project: m.project, workItemType: m.workItemType, iteration: m.iteration, area: m.area });
|
|
6132
|
-
if (existing.length === 0) {
|
|
6133
|
-
log.error("No existing specs found to regenerate");
|
|
6134
|
-
process.exit(1);
|
|
6135
|
-
}
|
|
6136
|
-
const identifiers = existing.map((item) => item.number);
|
|
6137
|
-
const allNumeric = identifiers.every((id) => /^\d+$/.test(id));
|
|
6138
|
-
issues = allNumeric ? identifiers.join(",") : identifiers;
|
|
6139
|
-
const confirmed = await confirmLargeBatch(existing.length);
|
|
6140
|
-
if (!confirmed) {
|
|
6141
|
-
process.exit(0);
|
|
6142
|
-
}
|
|
7231
|
+
if (m.spec) {
|
|
7232
|
+
issues = m.spec;
|
|
6143
7233
|
} else {
|
|
6144
|
-
|
|
7234
|
+
const respecArgs = m.respec;
|
|
7235
|
+
const isEmpty = Array.isArray(respecArgs) && respecArgs.length === 0;
|
|
7236
|
+
if (isEmpty) {
|
|
7237
|
+
const source = await resolveSource(respecArgs, m.issueSource, m.cwd);
|
|
7238
|
+
if (!source) process.exit(1);
|
|
7239
|
+
const datasource4 = getDatasource(source);
|
|
7240
|
+
const existing = await datasource4.list({ cwd: m.cwd, org: m.org, project: m.project, workItemType: m.workItemType, iteration: m.iteration, area: m.area });
|
|
7241
|
+
if (existing.length === 0) {
|
|
7242
|
+
log.error("No existing specs found to regenerate");
|
|
7243
|
+
process.exit(1);
|
|
7244
|
+
}
|
|
7245
|
+
const identifiers = existing.map((item) => item.number);
|
|
7246
|
+
const allNumeric = identifiers.every((id) => /^\d+$/.test(id));
|
|
7247
|
+
issues = allNumeric ? identifiers.join(",") : identifiers;
|
|
7248
|
+
const confirmed = await confirmLargeBatch(existing.length);
|
|
7249
|
+
if (!confirmed) process.exit(0);
|
|
7250
|
+
} else {
|
|
7251
|
+
issues = respecArgs;
|
|
7252
|
+
}
|
|
6145
7253
|
}
|
|
6146
|
-
|
|
7254
|
+
const workerMessage2 = {
|
|
7255
|
+
type: "spec",
|
|
7256
|
+
cwd,
|
|
7257
|
+
opts: {
|
|
7258
|
+
issues,
|
|
7259
|
+
enabledProviders: m.enabledProviders,
|
|
7260
|
+
providerModels: m.providerModels,
|
|
7261
|
+
issueSource: m.issueSource,
|
|
7262
|
+
org: m.org,
|
|
7263
|
+
project: m.project,
|
|
7264
|
+
workItemType: m.workItemType,
|
|
7265
|
+
iteration: m.iteration,
|
|
7266
|
+
area: m.area,
|
|
7267
|
+
concurrency,
|
|
7268
|
+
specTimeout: m.specTimeout ?? DEFAULT_SPEC_TIMEOUT_MIN,
|
|
7269
|
+
specWarnTimeout: m.specWarnTimeout,
|
|
7270
|
+
specKillTimeout: m.specKillTimeout,
|
|
7271
|
+
dryRun: false,
|
|
7272
|
+
cwd
|
|
7273
|
+
}
|
|
7274
|
+
};
|
|
7275
|
+
const runId2 = createSpecRun({
|
|
7276
|
+
cwd,
|
|
6147
7277
|
issues,
|
|
6148
|
-
|
|
7278
|
+
sessionId,
|
|
7279
|
+
status: "queued",
|
|
7280
|
+
workerMessage: JSON.stringify(workerMessage2)
|
|
7281
|
+
});
|
|
7282
|
+
return awaitQueuedRuns([runId2], sessionId, cwd, "spec");
|
|
7283
|
+
}
|
|
7284
|
+
const workerMessage = {
|
|
7285
|
+
type: "dispatch",
|
|
7286
|
+
cwd,
|
|
7287
|
+
opts: {
|
|
7288
|
+
issueIds: m.issueIds,
|
|
7289
|
+
dryRun: false,
|
|
6149
7290
|
provider: m.provider,
|
|
6150
7291
|
enabledProviders: m.enabledProviders,
|
|
6151
7292
|
providerModels: m.providerModels,
|
|
6152
|
-
|
|
6153
|
-
cwd: m.cwd,
|
|
6154
|
-
outputDir: m.outputDir,
|
|
7293
|
+
source: m.issueSource,
|
|
6155
7294
|
org: m.org,
|
|
6156
7295
|
project: m.project,
|
|
6157
7296
|
workItemType: m.workItemType,
|
|
6158
7297
|
iteration: m.iteration,
|
|
6159
7298
|
area: m.area,
|
|
6160
|
-
|
|
6161
|
-
|
|
7299
|
+
username: m.username,
|
|
7300
|
+
planTimeout: m.planTimeout,
|
|
7301
|
+
concurrency,
|
|
7302
|
+
noPlan: m.noPlan,
|
|
7303
|
+
noBranch: m.noBranch,
|
|
7304
|
+
noWorktree: m.noWorktree,
|
|
6162
7305
|
retries: m.retries,
|
|
6163
|
-
|
|
6164
|
-
|
|
6165
|
-
|
|
6166
|
-
}
|
|
6167
|
-
}
|
|
6168
|
-
|
|
7306
|
+
feature: m.feature,
|
|
7307
|
+
planRetries: m.planRetries,
|
|
7308
|
+
force: m.force
|
|
7309
|
+
}
|
|
7310
|
+
};
|
|
7311
|
+
const runId = createRun({
|
|
7312
|
+
cwd,
|
|
6169
7313
|
issueIds: m.issueIds,
|
|
6170
|
-
|
|
6171
|
-
|
|
6172
|
-
|
|
6173
|
-
noBranch: m.noBranch,
|
|
6174
|
-
noWorktree: m.noWorktree,
|
|
6175
|
-
provider: m.provider,
|
|
6176
|
-
enabledProviders: m.enabledProviders,
|
|
6177
|
-
providerModels: m.providerModels,
|
|
6178
|
-
serverUrl: m.serverUrl,
|
|
6179
|
-
source: m.issueSource,
|
|
6180
|
-
org: m.org,
|
|
6181
|
-
project: m.project,
|
|
6182
|
-
workItemType: m.workItemType,
|
|
6183
|
-
iteration: m.iteration,
|
|
6184
|
-
area: m.area,
|
|
6185
|
-
planTimeout: m.planTimeout,
|
|
6186
|
-
planRetries: m.planRetries,
|
|
6187
|
-
retries: m.retries,
|
|
6188
|
-
force: m.force,
|
|
6189
|
-
feature: m.feature,
|
|
6190
|
-
username: m.username
|
|
7314
|
+
sessionId,
|
|
7315
|
+
status: "queued",
|
|
7316
|
+
workerMessage: JSON.stringify(workerMessage)
|
|
6191
7317
|
});
|
|
7318
|
+
return awaitQueuedRuns([runId], sessionId, cwd, "dispatch");
|
|
6192
7319
|
}
|
|
6193
7320
|
};
|
|
6194
7321
|
return runner;
|
|
6195
7322
|
}
|
|
7323
|
+
function cliLogCallback(message, level) {
|
|
7324
|
+
if (level === "error") log.error(message);
|
|
7325
|
+
else if (level === "warn") log.warn(message);
|
|
7326
|
+
else log.info(message);
|
|
7327
|
+
}
|
|
7328
|
+
async function awaitQueuedRuns(runIds, sessionId, cwd, mode) {
|
|
7329
|
+
const queue = getRunQueue();
|
|
7330
|
+
for (const runId of runIds) {
|
|
7331
|
+
queue.enqueue(runId, cliLogCallback);
|
|
7332
|
+
}
|
|
7333
|
+
const getStatus = mode === "spec" ? (runId) => getSpecRun(runId)?.status ?? null : (runId) => getRun(runId)?.status ?? null;
|
|
7334
|
+
await Promise.all(runIds.map(
|
|
7335
|
+
(runId) => waitForRunCompletion(runId, 6e5, () => getStatus(runId))
|
|
7336
|
+
));
|
|
7337
|
+
if (mode === "spec") {
|
|
7338
|
+
let total2 = 0, generated = 0, failed2 = 0;
|
|
7339
|
+
for (const runId of runIds) {
|
|
7340
|
+
const { getSpecRun: getSpecRun2 } = await Promise.resolve().then(() => (init_manager(), manager_exports));
|
|
7341
|
+
const run = getSpecRun2(runId);
|
|
7342
|
+
if (run) {
|
|
7343
|
+
total2 += run.total;
|
|
7344
|
+
generated += run.generated;
|
|
7345
|
+
failed2 += run.failed;
|
|
7346
|
+
}
|
|
7347
|
+
}
|
|
7348
|
+
return { total: total2, generated, failed: failed2 };
|
|
7349
|
+
}
|
|
7350
|
+
let total = 0, completed = 0, failed = 0;
|
|
7351
|
+
const results = [];
|
|
7352
|
+
for (const runId of runIds) {
|
|
7353
|
+
const run = getRun(runId);
|
|
7354
|
+
if (run) {
|
|
7355
|
+
total += run.total;
|
|
7356
|
+
completed += run.completed;
|
|
7357
|
+
failed += run.failed;
|
|
7358
|
+
}
|
|
7359
|
+
const tasks = getTasksForRun(runId);
|
|
7360
|
+
for (const task of tasks) {
|
|
7361
|
+
results.push({
|
|
7362
|
+
task: { file: task.file, line: task.line, text: task.taskText },
|
|
7363
|
+
success: task.status === "success",
|
|
7364
|
+
error: task.error ?? void 0
|
|
7365
|
+
});
|
|
7366
|
+
}
|
|
7367
|
+
}
|
|
7368
|
+
return { total, completed, failed, skipped: 0, results };
|
|
7369
|
+
}
|
|
7370
|
+
async function runResumeFlow(resumeArg, cwd, maxRuns) {
|
|
7371
|
+
const sessions = listResumableSessions(cwd);
|
|
7372
|
+
if (sessions.length === 0) {
|
|
7373
|
+
log.info("No incomplete sessions found to resume.");
|
|
7374
|
+
process.exit(0);
|
|
7375
|
+
}
|
|
7376
|
+
let sessionId;
|
|
7377
|
+
if (typeof resumeArg === "string") {
|
|
7378
|
+
const match = sessions.find((s) => s.sessionId === resumeArg);
|
|
7379
|
+
if (!match) {
|
|
7380
|
+
log.error(`Session ${resumeArg} not found or has no incomplete runs.`);
|
|
7381
|
+
log.info("Available sessions:");
|
|
7382
|
+
for (const s of sessions) {
|
|
7383
|
+
const date = new Date(s.startedAt).toLocaleString();
|
|
7384
|
+
log.info(` ${s.sessionId} (${date}, ${s.incompleteRuns} incomplete)`);
|
|
7385
|
+
}
|
|
7386
|
+
process.exit(1);
|
|
7387
|
+
}
|
|
7388
|
+
sessionId = resumeArg;
|
|
7389
|
+
} else if (sessions.length === 1) {
|
|
7390
|
+
sessionId = sessions[0].sessionId;
|
|
7391
|
+
log.info(`Resuming session ${sessionId} (${sessions[0].incompleteRuns} incomplete run(s))`);
|
|
7392
|
+
} else {
|
|
7393
|
+
const { select: select2 } = await Promise.resolve().then(() => (init_ink_prompts(), ink_prompts_exports));
|
|
7394
|
+
sessionId = await select2({
|
|
7395
|
+
message: "Multiple incomplete sessions found. Which one would you like to resume?",
|
|
7396
|
+
choices: sessions.map((s) => {
|
|
7397
|
+
const date = new Date(s.startedAt).toLocaleString();
|
|
7398
|
+
let issueLabel = "";
|
|
7399
|
+
try {
|
|
7400
|
+
const ids = JSON.parse(s.issueIds);
|
|
7401
|
+
issueLabel = ids.length > 3 ? `issues ${ids.slice(0, 3).join(", ")}...` : `issues ${ids.join(", ")}`;
|
|
7402
|
+
} catch {
|
|
7403
|
+
issueLabel = "unknown issues";
|
|
7404
|
+
}
|
|
7405
|
+
return {
|
|
7406
|
+
name: `${s.sessionId.slice(0, 8)} ${date} ${issueLabel} (${s.incompleteRuns} incomplete)`,
|
|
7407
|
+
value: s.sessionId
|
|
7408
|
+
};
|
|
7409
|
+
})
|
|
7410
|
+
});
|
|
7411
|
+
}
|
|
7412
|
+
markOrphanedRunsFailed({ exceptSessionId: sessionId });
|
|
7413
|
+
initRunQueue(maxRuns);
|
|
7414
|
+
const runIds = requeueSessionRuns(sessionId);
|
|
7415
|
+
if (runIds.length === 0) {
|
|
7416
|
+
log.info("No runs to resume in this session.");
|
|
7417
|
+
process.exit(0);
|
|
7418
|
+
}
|
|
7419
|
+
log.info(`Resuming ${runIds.length} run(s) from session ${sessionId.slice(0, 8)}...`);
|
|
7420
|
+
return awaitQueuedRuns(runIds, sessionId, cwd, "dispatch");
|
|
7421
|
+
}
|
|
6196
7422
|
|
|
6197
7423
|
// src/mcp/dispatch-worker.ts
|
|
6198
7424
|
process.on("message", (msg) => {
|
|
6199
7425
|
void handleMessage(msg);
|
|
6200
7426
|
});
|
|
7427
|
+
function sendAndFlush(msg) {
|
|
7428
|
+
return new Promise((resolve3, reject) => {
|
|
7429
|
+
process.send(msg, (err) => {
|
|
7430
|
+
if (err) reject(err);
|
|
7431
|
+
else resolve3();
|
|
7432
|
+
});
|
|
7433
|
+
});
|
|
7434
|
+
}
|
|
6201
7435
|
async function handleMessage(msg) {
|
|
6202
7436
|
try {
|
|
6203
7437
|
if (msg.type === "dispatch") {
|
|
@@ -6208,7 +7442,7 @@ async function handleMessage(msg) {
|
|
|
6208
7442
|
process.send({ type: "progress", event });
|
|
6209
7443
|
}
|
|
6210
7444
|
});
|
|
6211
|
-
|
|
7445
|
+
await sendAndFlush({ type: "done", result });
|
|
6212
7446
|
} else if (msg.type === "spec") {
|
|
6213
7447
|
const result = await runSpecPipeline({
|
|
6214
7448
|
...msg.opts,
|
|
@@ -6216,10 +7450,11 @@ async function handleMessage(msg) {
|
|
|
6216
7450
|
process.send({ type: "spec_progress", event });
|
|
6217
7451
|
}
|
|
6218
7452
|
});
|
|
6219
|
-
|
|
7453
|
+
await sendAndFlush({ type: "done", result });
|
|
6220
7454
|
}
|
|
6221
7455
|
} catch (err) {
|
|
6222
|
-
|
|
7456
|
+
await sendAndFlush({ type: "error", message: err instanceof Error ? err.message : String(err) }).catch(() => {
|
|
7457
|
+
});
|
|
6223
7458
|
}
|
|
6224
7459
|
process.exit(0);
|
|
6225
7460
|
}
|