@pruddiman/dispatch 1.4.3 → 1.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -4
- package/dist/cli.js +1557 -879
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -241,6 +241,41 @@ var init_logger = __esm({
|
|
|
241
241
|
}
|
|
242
242
|
});
|
|
243
243
|
|
|
244
|
+
// src/providers/progress.ts
|
|
245
|
+
function sanitizeProgressText(raw, maxLength = 120) {
|
|
246
|
+
const text = raw.replace(ANSI_PATTERN, "").replace(CONTROL_PATTERN, "").replace(/\s+/g, " ").trim();
|
|
247
|
+
if (!text) return "";
|
|
248
|
+
if (text.length <= maxLength) return text;
|
|
249
|
+
if (maxLength <= 1) return maxLength <= 0 ? "" : "\u2026";
|
|
250
|
+
return `${text.slice(0, maxLength - 1).trimEnd()}\u2026`;
|
|
251
|
+
}
|
|
252
|
+
function createProgressReporter(onProgress) {
|
|
253
|
+
let last;
|
|
254
|
+
return {
|
|
255
|
+
emit(raw) {
|
|
256
|
+
if (!onProgress) return;
|
|
257
|
+
const text = sanitizeProgressText(raw ?? "");
|
|
258
|
+
if (!text || text === last) return;
|
|
259
|
+
last = text;
|
|
260
|
+
try {
|
|
261
|
+
onProgress({ text });
|
|
262
|
+
} catch {
|
|
263
|
+
}
|
|
264
|
+
},
|
|
265
|
+
reset() {
|
|
266
|
+
last = void 0;
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
var ANSI_PATTERN, CONTROL_PATTERN;
|
|
271
|
+
var init_progress = __esm({
|
|
272
|
+
"src/providers/progress.ts"() {
|
|
273
|
+
"use strict";
|
|
274
|
+
ANSI_PATTERN = /(?:\u001B\[[0-?]*[ -/]*[@-~]|\u009B[0-?]*[ -/]*[@-~]|\u001B\][^\u0007]*(?:\u0007|\u001B\\))/g;
|
|
275
|
+
CONTROL_PATTERN = /[\u0000-\u0008\u000B-\u001F\u007F]/g;
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
|
|
244
279
|
// src/helpers/guards.ts
|
|
245
280
|
function hasProperty(value, key) {
|
|
246
281
|
return typeof value === "object" && value !== null && Object.prototype.hasOwnProperty.call(value, key);
|
|
@@ -251,6 +286,52 @@ var init_guards = __esm({
|
|
|
251
286
|
}
|
|
252
287
|
});
|
|
253
288
|
|
|
289
|
+
// src/helpers/timeout.ts
|
|
290
|
+
function withTimeout(promise, ms, label) {
|
|
291
|
+
const p = new Promise((resolve5, reject) => {
|
|
292
|
+
let settled = false;
|
|
293
|
+
const timer = setTimeout(() => {
|
|
294
|
+
if (settled) return;
|
|
295
|
+
settled = true;
|
|
296
|
+
reject(new TimeoutError(ms, label));
|
|
297
|
+
}, ms);
|
|
298
|
+
promise.then(
|
|
299
|
+
(value) => {
|
|
300
|
+
if (settled) return;
|
|
301
|
+
settled = true;
|
|
302
|
+
clearTimeout(timer);
|
|
303
|
+
resolve5(value);
|
|
304
|
+
},
|
|
305
|
+
(err) => {
|
|
306
|
+
if (settled) return;
|
|
307
|
+
settled = true;
|
|
308
|
+
clearTimeout(timer);
|
|
309
|
+
reject(err);
|
|
310
|
+
}
|
|
311
|
+
);
|
|
312
|
+
});
|
|
313
|
+
p.catch(() => {
|
|
314
|
+
});
|
|
315
|
+
return p;
|
|
316
|
+
}
|
|
317
|
+
var TimeoutError, DEFAULT_PLAN_TIMEOUT_MIN;
|
|
318
|
+
var init_timeout = __esm({
|
|
319
|
+
"src/helpers/timeout.ts"() {
|
|
320
|
+
"use strict";
|
|
321
|
+
TimeoutError = class extends Error {
|
|
322
|
+
/** Optional label identifying the operation that timed out. */
|
|
323
|
+
label;
|
|
324
|
+
constructor(ms, label) {
|
|
325
|
+
const suffix = label ? ` [${label}]` : "";
|
|
326
|
+
super(`Timed out after ${ms}ms${suffix}`);
|
|
327
|
+
this.name = "TimeoutError";
|
|
328
|
+
this.label = label;
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
DEFAULT_PLAN_TIMEOUT_MIN = 30;
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
|
|
254
335
|
// src/providers/opencode.ts
|
|
255
336
|
import {
|
|
256
337
|
createOpencode,
|
|
@@ -346,9 +427,10 @@ async function boot(opts) {
|
|
|
346
427
|
throw err;
|
|
347
428
|
}
|
|
348
429
|
},
|
|
349
|
-
async prompt(sessionId, text) {
|
|
430
|
+
async prompt(sessionId, text, options) {
|
|
350
431
|
log.debug(`Sending async prompt to session ${sessionId} (${text.length} chars)...`);
|
|
351
432
|
let controller;
|
|
433
|
+
const reporter = createProgressReporter(options?.onProgress);
|
|
352
434
|
try {
|
|
353
435
|
const { error: promptError } = await client.session.promptAsync({
|
|
354
436
|
path: { id: sessionId },
|
|
@@ -366,29 +448,15 @@ async function boot(opts) {
|
|
|
366
448
|
const { stream } = await client.event.subscribe({
|
|
367
449
|
signal: controller.signal
|
|
368
450
|
});
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
log.debug(`Streaming text (+${delta.length} chars)...`);
|
|
375
|
-
}
|
|
376
|
-
continue;
|
|
377
|
-
}
|
|
378
|
-
if (event.type === "session.error") {
|
|
379
|
-
const err = event.properties.error;
|
|
380
|
-
throw new Error(
|
|
381
|
-
`OpenCode session error: ${err ? JSON.stringify(err) : "unknown error"}`
|
|
382
|
-
);
|
|
383
|
-
}
|
|
384
|
-
if (event.type === "session.idle") {
|
|
385
|
-
log.debug("Session went idle, fetching result...");
|
|
386
|
-
break;
|
|
387
|
-
}
|
|
388
|
-
}
|
|
451
|
+
await withTimeout(
|
|
452
|
+
waitForSessionReady(stream, sessionId, reporter),
|
|
453
|
+
SESSION_READY_TIMEOUT_MS,
|
|
454
|
+
"opencode session ready"
|
|
455
|
+
);
|
|
389
456
|
} finally {
|
|
390
457
|
if (controller && !controller.signal.aborted) controller.abort();
|
|
391
458
|
}
|
|
459
|
+
log.debug("Session went idle, fetching result...");
|
|
392
460
|
const { data: messages } = await client.session.messages({
|
|
393
461
|
path: { id: sessionId }
|
|
394
462
|
});
|
|
@@ -417,6 +485,20 @@ async function boot(opts) {
|
|
|
417
485
|
throw err;
|
|
418
486
|
}
|
|
419
487
|
},
|
|
488
|
+
async send(sessionId, text) {
|
|
489
|
+
log.debug(`Sending follow-up message to session ${sessionId} (${text.length} chars)...`);
|
|
490
|
+
const { error } = await client.session.promptAsync({
|
|
491
|
+
path: { id: sessionId },
|
|
492
|
+
body: {
|
|
493
|
+
parts: [{ type: "text", text }],
|
|
494
|
+
...modelOverride ? { model: modelOverride } : {}
|
|
495
|
+
}
|
|
496
|
+
});
|
|
497
|
+
if (error) {
|
|
498
|
+
throw new Error(`OpenCode send failed: ${JSON.stringify(error)}`);
|
|
499
|
+
}
|
|
500
|
+
log.debug("Follow-up message sent successfully");
|
|
501
|
+
},
|
|
420
502
|
async cleanup() {
|
|
421
503
|
if (cleaned) return;
|
|
422
504
|
cleaned = true;
|
|
@@ -429,6 +511,29 @@ async function boot(opts) {
|
|
|
429
511
|
}
|
|
430
512
|
};
|
|
431
513
|
}
|
|
514
|
+
async function waitForSessionReady(stream, sessionId, reporter) {
|
|
515
|
+
for await (const event of stream) {
|
|
516
|
+
if (!isSessionEvent(event, sessionId)) continue;
|
|
517
|
+
if (event.type === "message.part.updated" && event.properties.part.type === "text") {
|
|
518
|
+
const delta = event.properties.delta;
|
|
519
|
+
if (delta) {
|
|
520
|
+
log.debug(`Streaming text (+${delta.length} chars)...`);
|
|
521
|
+
reporter?.emit(delta);
|
|
522
|
+
}
|
|
523
|
+
continue;
|
|
524
|
+
}
|
|
525
|
+
if (event.type === "session.error") {
|
|
526
|
+
const err = event.properties.error;
|
|
527
|
+
throw new Error(
|
|
528
|
+
`OpenCode session error: ${err ? JSON.stringify(err) : "unknown error"}`
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
if (event.type === "session.idle") {
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
throw new Error("OpenCode event stream ended before session became idle");
|
|
536
|
+
}
|
|
432
537
|
function isSessionEvent(event, sessionId) {
|
|
433
538
|
const props = event.properties;
|
|
434
539
|
if (!hasProperty(props, "sessionID") && !hasProperty(props, "info") && !hasProperty(props, "part")) {
|
|
@@ -443,56 +548,15 @@ function isSessionEvent(event, sessionId) {
|
|
|
443
548
|
}
|
|
444
549
|
return false;
|
|
445
550
|
}
|
|
551
|
+
var SESSION_READY_TIMEOUT_MS;
|
|
446
552
|
var init_opencode = __esm({
|
|
447
553
|
"src/providers/opencode.ts"() {
|
|
448
554
|
"use strict";
|
|
555
|
+
init_progress();
|
|
449
556
|
init_logger();
|
|
450
557
|
init_guards();
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
// src/helpers/timeout.ts
|
|
455
|
-
function withTimeout(promise, ms, label) {
|
|
456
|
-
const p = new Promise((resolve5, reject) => {
|
|
457
|
-
let settled = false;
|
|
458
|
-
const timer = setTimeout(() => {
|
|
459
|
-
if (settled) return;
|
|
460
|
-
settled = true;
|
|
461
|
-
reject(new TimeoutError(ms, label));
|
|
462
|
-
}, ms);
|
|
463
|
-
promise.then(
|
|
464
|
-
(value) => {
|
|
465
|
-
if (settled) return;
|
|
466
|
-
settled = true;
|
|
467
|
-
clearTimeout(timer);
|
|
468
|
-
resolve5(value);
|
|
469
|
-
},
|
|
470
|
-
(err) => {
|
|
471
|
-
if (settled) return;
|
|
472
|
-
settled = true;
|
|
473
|
-
clearTimeout(timer);
|
|
474
|
-
reject(err);
|
|
475
|
-
}
|
|
476
|
-
);
|
|
477
|
-
});
|
|
478
|
-
p.catch(() => {
|
|
479
|
-
});
|
|
480
|
-
return p;
|
|
481
|
-
}
|
|
482
|
-
var TimeoutError;
|
|
483
|
-
var init_timeout = __esm({
|
|
484
|
-
"src/helpers/timeout.ts"() {
|
|
485
|
-
"use strict";
|
|
486
|
-
TimeoutError = class extends Error {
|
|
487
|
-
/** Optional label identifying the operation that timed out. */
|
|
488
|
-
label;
|
|
489
|
-
constructor(ms, label) {
|
|
490
|
-
const suffix = label ? ` [${label}]` : "";
|
|
491
|
-
super(`Timed out after ${ms}ms${suffix}`);
|
|
492
|
-
this.name = "TimeoutError";
|
|
493
|
-
this.label = label;
|
|
494
|
-
}
|
|
495
|
-
};
|
|
558
|
+
init_timeout();
|
|
559
|
+
SESSION_READY_TIMEOUT_MS = 6e5;
|
|
496
560
|
}
|
|
497
561
|
});
|
|
498
562
|
|
|
@@ -5603,35 +5667,33 @@ async function boot2(opts) {
|
|
|
5603
5667
|
throw err;
|
|
5604
5668
|
}
|
|
5605
5669
|
},
|
|
5606
|
-
async prompt(sessionId, text) {
|
|
5670
|
+
async prompt(sessionId, text, options) {
|
|
5607
5671
|
const session = sessions.get(sessionId);
|
|
5608
5672
|
if (!session) {
|
|
5609
5673
|
throw new Error(`Copilot session ${sessionId} not found`);
|
|
5610
5674
|
}
|
|
5611
5675
|
log.debug(`Sending prompt to session ${sessionId} (${text.length} chars)...`);
|
|
5676
|
+
const reporter = createProgressReporter(options?.onProgress);
|
|
5677
|
+
let unsubIdle;
|
|
5678
|
+
let unsubErr;
|
|
5612
5679
|
try {
|
|
5613
5680
|
await session.send({ prompt: text });
|
|
5614
5681
|
log.debug("Async prompt accepted, waiting for session to become idle...");
|
|
5615
|
-
|
|
5616
|
-
|
|
5617
|
-
|
|
5618
|
-
|
|
5619
|
-
|
|
5620
|
-
|
|
5621
|
-
|
|
5622
|
-
});
|
|
5623
|
-
|
|
5624
|
-
|
|
5625
|
-
|
|
5626
|
-
|
|
5627
|
-
|
|
5628
|
-
"copilot session ready"
|
|
5629
|
-
);
|
|
5630
|
-
} finally {
|
|
5631
|
-
unsubIdle?.();
|
|
5632
|
-
unsubErr?.();
|
|
5633
|
-
}
|
|
5682
|
+
reporter.emit("Waiting for Copilot response");
|
|
5683
|
+
await withTimeout(
|
|
5684
|
+
new Promise((resolve5, reject) => {
|
|
5685
|
+
unsubIdle = session.on("session.idle", () => {
|
|
5686
|
+
resolve5();
|
|
5687
|
+
});
|
|
5688
|
+
unsubErr = session.on("session.error", (event) => {
|
|
5689
|
+
reject(new Error(`Copilot session error: ${event.data.message}`));
|
|
5690
|
+
});
|
|
5691
|
+
}),
|
|
5692
|
+
SESSION_READY_TIMEOUT_MS2,
|
|
5693
|
+
"copilot session ready"
|
|
5694
|
+
);
|
|
5634
5695
|
log.debug("Session went idle, fetching result...");
|
|
5696
|
+
reporter.emit("Finalizing response");
|
|
5635
5697
|
const events = await session.getMessages();
|
|
5636
5698
|
const last = [...events].reverse().find((e) => e.type === "assistant.message");
|
|
5637
5699
|
const result = last?.data?.content ?? null;
|
|
@@ -5640,6 +5702,23 @@ async function boot2(opts) {
|
|
|
5640
5702
|
} catch (err) {
|
|
5641
5703
|
log.debug(`Prompt failed: ${log.formatErrorChain(err)}`);
|
|
5642
5704
|
throw err;
|
|
5705
|
+
} finally {
|
|
5706
|
+
unsubIdle?.();
|
|
5707
|
+
unsubErr?.();
|
|
5708
|
+
}
|
|
5709
|
+
},
|
|
5710
|
+
async send(sessionId, text) {
|
|
5711
|
+
const session = sessions.get(sessionId);
|
|
5712
|
+
if (!session) {
|
|
5713
|
+
throw new Error(`Copilot session ${sessionId} not found`);
|
|
5714
|
+
}
|
|
5715
|
+
log.debug(`Sending follow-up to session ${sessionId} (${text.length} chars)...`);
|
|
5716
|
+
try {
|
|
5717
|
+
await session.send({ prompt: text });
|
|
5718
|
+
log.debug("Follow-up message sent");
|
|
5719
|
+
} catch (err) {
|
|
5720
|
+
log.debug(`Follow-up send failed: ${log.formatErrorChain(err)}`);
|
|
5721
|
+
throw err;
|
|
5643
5722
|
}
|
|
5644
5723
|
},
|
|
5645
5724
|
async cleanup() {
|
|
@@ -5657,13 +5736,14 @@ async function boot2(opts) {
|
|
|
5657
5736
|
}
|
|
5658
5737
|
};
|
|
5659
5738
|
}
|
|
5660
|
-
var
|
|
5739
|
+
var SESSION_READY_TIMEOUT_MS2;
|
|
5661
5740
|
var init_copilot = __esm({
|
|
5662
5741
|
"src/providers/copilot.ts"() {
|
|
5663
5742
|
"use strict";
|
|
5743
|
+
init_progress();
|
|
5664
5744
|
init_logger();
|
|
5665
5745
|
init_timeout();
|
|
5666
|
-
|
|
5746
|
+
SESSION_READY_TIMEOUT_MS2 = 6e5;
|
|
5667
5747
|
}
|
|
5668
5748
|
});
|
|
5669
5749
|
|
|
@@ -5689,7 +5769,12 @@ async function boot3(opts) {
|
|
|
5689
5769
|
async createSession() {
|
|
5690
5770
|
log.debug("Creating Claude session...");
|
|
5691
5771
|
try {
|
|
5692
|
-
const sessionOpts = {
|
|
5772
|
+
const sessionOpts = {
|
|
5773
|
+
model,
|
|
5774
|
+
permissionMode: "bypassPermissions",
|
|
5775
|
+
allowDangerouslySkipPermissions: true,
|
|
5776
|
+
...cwd ? { cwd } : {}
|
|
5777
|
+
};
|
|
5693
5778
|
const session = unstable_v2_createSession(sessionOpts);
|
|
5694
5779
|
const sessionId = randomUUID();
|
|
5695
5780
|
sessions.set(sessionId, session);
|
|
@@ -5700,19 +5785,23 @@ async function boot3(opts) {
|
|
|
5700
5785
|
throw err;
|
|
5701
5786
|
}
|
|
5702
5787
|
},
|
|
5703
|
-
async prompt(sessionId, text) {
|
|
5788
|
+
async prompt(sessionId, text, options) {
|
|
5704
5789
|
const session = sessions.get(sessionId);
|
|
5705
5790
|
if (!session) {
|
|
5706
5791
|
throw new Error(`Claude session ${sessionId} not found`);
|
|
5707
5792
|
}
|
|
5708
5793
|
log.debug(`Sending prompt to session ${sessionId} (${text.length} chars)...`);
|
|
5794
|
+
const reporter = createProgressReporter(options?.onProgress);
|
|
5709
5795
|
try {
|
|
5710
5796
|
await session.send(text);
|
|
5711
5797
|
const parts = [];
|
|
5712
5798
|
for await (const msg of session.stream()) {
|
|
5713
5799
|
if (msg.type === "assistant") {
|
|
5714
5800
|
const msgText = msg.message.content.filter((block) => block.type === "text").map((block) => block.text).join("");
|
|
5715
|
-
if (msgText)
|
|
5801
|
+
if (msgText) {
|
|
5802
|
+
reporter.emit(msgText);
|
|
5803
|
+
parts.push(msgText);
|
|
5804
|
+
}
|
|
5716
5805
|
}
|
|
5717
5806
|
}
|
|
5718
5807
|
const result = parts.join("") || null;
|
|
@@ -5723,6 +5812,19 @@ async function boot3(opts) {
|
|
|
5723
5812
|
throw err;
|
|
5724
5813
|
}
|
|
5725
5814
|
},
|
|
5815
|
+
async send(sessionId, text) {
|
|
5816
|
+
const session = sessions.get(sessionId);
|
|
5817
|
+
if (!session) {
|
|
5818
|
+
throw new Error(`Claude session ${sessionId} not found`);
|
|
5819
|
+
}
|
|
5820
|
+
log.debug(`Sending follow-up to session ${sessionId} (${text.length} chars)...`);
|
|
5821
|
+
try {
|
|
5822
|
+
await session.send(text);
|
|
5823
|
+
} catch (err) {
|
|
5824
|
+
log.debug(`Follow-up send failed: ${log.formatErrorChain(err)}`);
|
|
5825
|
+
throw err;
|
|
5826
|
+
}
|
|
5827
|
+
},
|
|
5726
5828
|
async cleanup() {
|
|
5727
5829
|
log.debug("Cleaning up Claude provider...");
|
|
5728
5830
|
for (const session of sessions.values()) {
|
|
@@ -5738,6 +5840,7 @@ async function boot3(opts) {
|
|
|
5738
5840
|
var init_claude = __esm({
|
|
5739
5841
|
"src/providers/claude.ts"() {
|
|
5740
5842
|
"use strict";
|
|
5843
|
+
init_progress();
|
|
5741
5844
|
init_logger();
|
|
5742
5845
|
}
|
|
5743
5846
|
});
|
|
@@ -5766,6 +5869,11 @@ async function boot4(opts) {
|
|
|
5766
5869
|
log.debug("Creating Codex session...");
|
|
5767
5870
|
try {
|
|
5768
5871
|
const sessionId = randomUUID2();
|
|
5872
|
+
const state = {
|
|
5873
|
+
agent: void 0,
|
|
5874
|
+
reporter: createProgressReporter(),
|
|
5875
|
+
loadingReported: false
|
|
5876
|
+
};
|
|
5769
5877
|
const agent = new AgentLoop({
|
|
5770
5878
|
model,
|
|
5771
5879
|
config: { model, instructions: "" },
|
|
@@ -5773,14 +5881,26 @@ async function boot4(opts) {
|
|
|
5773
5881
|
...opts?.cwd ? { rootDir: opts.cwd } : {},
|
|
5774
5882
|
additionalWritableRoots: [],
|
|
5775
5883
|
getCommandConfirmation: async () => ({ approved: true }),
|
|
5776
|
-
onItem: () => {
|
|
5884
|
+
onItem: (item) => {
|
|
5885
|
+
if (item && typeof item === "object" && "type" in item && item.type === "message" && "content" in item && Array.isArray(item.content)) {
|
|
5886
|
+
const itemText = item.content.filter(
|
|
5887
|
+
(block) => Boolean(block) && typeof block === "object" && "type" in block && block.type === "output_text"
|
|
5888
|
+
).map((block) => block.text ?? "").join("");
|
|
5889
|
+
if (itemText) {
|
|
5890
|
+
state.reporter.emit(itemText);
|
|
5891
|
+
}
|
|
5892
|
+
}
|
|
5777
5893
|
},
|
|
5778
5894
|
onLoading: () => {
|
|
5895
|
+
if (state.loadingReported) return;
|
|
5896
|
+
state.loadingReported = true;
|
|
5897
|
+
state.reporter.emit("thinking");
|
|
5779
5898
|
},
|
|
5780
5899
|
onLastResponseId: () => {
|
|
5781
5900
|
}
|
|
5782
5901
|
});
|
|
5783
|
-
|
|
5902
|
+
state.agent = agent;
|
|
5903
|
+
sessions.set(sessionId, state);
|
|
5784
5904
|
log.debug(`Session created: ${sessionId}`);
|
|
5785
5905
|
return sessionId;
|
|
5786
5906
|
} catch (err) {
|
|
@@ -5788,35 +5908,55 @@ async function boot4(opts) {
|
|
|
5788
5908
|
throw err;
|
|
5789
5909
|
}
|
|
5790
5910
|
},
|
|
5791
|
-
async prompt(sessionId, text) {
|
|
5792
|
-
const
|
|
5793
|
-
if (!
|
|
5911
|
+
async prompt(sessionId, text, options) {
|
|
5912
|
+
const state = sessions.get(sessionId);
|
|
5913
|
+
if (!state) {
|
|
5794
5914
|
throw new Error(`Codex session ${sessionId} not found`);
|
|
5795
5915
|
}
|
|
5796
5916
|
log.debug(`Sending prompt to session ${sessionId} (${text.length} chars)...`);
|
|
5917
|
+
state.onProgress = options?.onProgress;
|
|
5918
|
+
state.reporter = createProgressReporter(state.onProgress);
|
|
5919
|
+
state.loadingReported = false;
|
|
5797
5920
|
try {
|
|
5798
|
-
|
|
5921
|
+
state.reporter.emit("Waiting for Codex response");
|
|
5922
|
+
const items = await state.agent.run([text]);
|
|
5799
5923
|
const parts = [];
|
|
5800
5924
|
for (const item of items) {
|
|
5801
5925
|
if (item.type === "message" && "content" in item) {
|
|
5802
5926
|
const content = item.content;
|
|
5803
5927
|
const itemText = content.filter((block) => block.type === "output_text").map((block) => block.text ?? "").join("");
|
|
5804
|
-
if (itemText)
|
|
5928
|
+
if (itemText) {
|
|
5929
|
+
parts.push(itemText);
|
|
5930
|
+
}
|
|
5805
5931
|
}
|
|
5806
5932
|
}
|
|
5933
|
+
state.reporter.emit("Finalizing response");
|
|
5807
5934
|
const result = parts.join("") || null;
|
|
5808
5935
|
log.debug(`Prompt response received (${result?.length ?? 0} chars)`);
|
|
5809
5936
|
return result;
|
|
5810
5937
|
} catch (err) {
|
|
5811
5938
|
log.debug(`Prompt failed: ${log.formatErrorChain(err)}`);
|
|
5812
5939
|
throw err;
|
|
5940
|
+
} finally {
|
|
5941
|
+
state.onProgress = void 0;
|
|
5942
|
+
state.reporter = createProgressReporter();
|
|
5943
|
+
state.loadingReported = false;
|
|
5944
|
+
}
|
|
5945
|
+
},
|
|
5946
|
+
async send(sessionId, text) {
|
|
5947
|
+
const state = sessions.get(sessionId);
|
|
5948
|
+
if (!state) {
|
|
5949
|
+
throw new Error(`Codex session ${sessionId} not found`);
|
|
5813
5950
|
}
|
|
5951
|
+
log.debug(
|
|
5952
|
+
`Codex provider does not support non-blocking send \u2014 agent.run() is blocking. Ignoring follow-up for session ${sessionId} (${text.length} chars).`
|
|
5953
|
+
);
|
|
5814
5954
|
},
|
|
5815
5955
|
async cleanup() {
|
|
5816
5956
|
log.debug("Cleaning up Codex provider...");
|
|
5817
|
-
for (const
|
|
5957
|
+
for (const state of sessions.values()) {
|
|
5818
5958
|
try {
|
|
5819
|
-
agent.terminate();
|
|
5959
|
+
state.agent.terminate();
|
|
5820
5960
|
} catch {
|
|
5821
5961
|
}
|
|
5822
5962
|
}
|
|
@@ -5827,6 +5967,7 @@ async function boot4(opts) {
|
|
|
5827
5967
|
var init_codex = __esm({
|
|
5828
5968
|
"src/providers/codex.ts"() {
|
|
5829
5969
|
"use strict";
|
|
5970
|
+
init_progress();
|
|
5830
5971
|
init_logger();
|
|
5831
5972
|
}
|
|
5832
5973
|
});
|
|
@@ -6128,19 +6269,10 @@ import { execFile as execFile5 } from "child_process";
|
|
|
6128
6269
|
import { promisify as promisify5 } from "util";
|
|
6129
6270
|
|
|
6130
6271
|
// src/datasources/github.ts
|
|
6272
|
+
init_logger();
|
|
6131
6273
|
import { execFile } from "child_process";
|
|
6132
6274
|
import { promisify } from "util";
|
|
6133
6275
|
|
|
6134
|
-
// src/helpers/slugify.ts
|
|
6135
|
-
var MAX_SLUG_LENGTH = 60;
|
|
6136
|
-
function slugify(input3, maxLength) {
|
|
6137
|
-
const slug = input3.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
6138
|
-
return maxLength != null ? slug.slice(0, maxLength) : slug;
|
|
6139
|
-
}
|
|
6140
|
-
|
|
6141
|
-
// src/datasources/github.ts
|
|
6142
|
-
init_logger();
|
|
6143
|
-
|
|
6144
6276
|
// src/helpers/branch-validation.ts
|
|
6145
6277
|
var InvalidBranchNameError = class extends Error {
|
|
6146
6278
|
constructor(branch, reason) {
|
|
@@ -6271,6 +6403,31 @@ ${deviceCodeInfo.message}`;
|
|
|
6271
6403
|
azdev.getBearerHandler(accessToken.token)
|
|
6272
6404
|
);
|
|
6273
6405
|
}
|
|
6406
|
+
async function ensureAuthReady(source, cwd, org) {
|
|
6407
|
+
if (source === "github") {
|
|
6408
|
+
const remoteUrl = await getGitRemoteUrl(cwd);
|
|
6409
|
+
if (remoteUrl && parseGitHubRemoteUrl(remoteUrl)) {
|
|
6410
|
+
await getGithubOctokit();
|
|
6411
|
+
} else if (!remoteUrl) {
|
|
6412
|
+
log.warn("No git remote found \u2014 skipping GitHub pre-authentication");
|
|
6413
|
+
} else {
|
|
6414
|
+
log.warn("Remote URL is not a GitHub repository \u2014 skipping GitHub pre-authentication");
|
|
6415
|
+
}
|
|
6416
|
+
} else if (source === "azdevops") {
|
|
6417
|
+
let orgUrl = org;
|
|
6418
|
+
if (!orgUrl) {
|
|
6419
|
+
const remoteUrl = await getGitRemoteUrl(cwd);
|
|
6420
|
+
if (remoteUrl) {
|
|
6421
|
+
const parsed = parseAzDevOpsRemoteUrl(remoteUrl);
|
|
6422
|
+
if (parsed) orgUrl = parsed.orgUrl;
|
|
6423
|
+
else log.warn("Remote URL is not an Azure DevOps repository \u2014 skipping Azure pre-authentication");
|
|
6424
|
+
} else {
|
|
6425
|
+
log.warn("No git remote found \u2014 skipping Azure DevOps pre-authentication");
|
|
6426
|
+
}
|
|
6427
|
+
}
|
|
6428
|
+
if (orgUrl) await getAzureConnection(orgUrl);
|
|
6429
|
+
}
|
|
6430
|
+
}
|
|
6274
6431
|
|
|
6275
6432
|
// src/datasources/github.ts
|
|
6276
6433
|
var exec = promisify(execFile);
|
|
@@ -6292,9 +6449,31 @@ async function getOwnerRepo(cwd) {
|
|
|
6292
6449
|
}
|
|
6293
6450
|
return parsed;
|
|
6294
6451
|
}
|
|
6295
|
-
function buildBranchName(issueNumber,
|
|
6296
|
-
|
|
6297
|
-
|
|
6452
|
+
function buildBranchName(issueNumber, _title, username = "unknown") {
|
|
6453
|
+
return `${username}/dispatch/issue-${issueNumber}`;
|
|
6454
|
+
}
|
|
6455
|
+
async function deriveShortUsername(cwd, fallback) {
|
|
6456
|
+
try {
|
|
6457
|
+
const raw = (await git(["config", "user.name"], cwd)).trim();
|
|
6458
|
+
if (raw) {
|
|
6459
|
+
const parts = raw.toLowerCase().replace(/[^a-z\s]/g, "").trim().split(/\s+/);
|
|
6460
|
+
if (parts.length >= 2) {
|
|
6461
|
+
return parts[0].slice(0, 2) + parts[parts.length - 1].slice(0, 6) || fallback;
|
|
6462
|
+
}
|
|
6463
|
+
}
|
|
6464
|
+
} catch {
|
|
6465
|
+
}
|
|
6466
|
+
try {
|
|
6467
|
+
const raw = (await git(["config", "user.email"], cwd)).trim();
|
|
6468
|
+
if (raw) {
|
|
6469
|
+
const localPart = raw.split("@")[0].toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
6470
|
+
if (localPart) {
|
|
6471
|
+
return localPart.slice(0, 8);
|
|
6472
|
+
}
|
|
6473
|
+
}
|
|
6474
|
+
} catch {
|
|
6475
|
+
}
|
|
6476
|
+
return fallback;
|
|
6298
6477
|
}
|
|
6299
6478
|
async function getDefaultBranch(cwd) {
|
|
6300
6479
|
const PREFIX = "refs/remotes/origin/";
|
|
@@ -6422,13 +6601,8 @@ var datasource = {
|
|
|
6422
6601
|
};
|
|
6423
6602
|
},
|
|
6424
6603
|
async getUsername(opts) {
|
|
6425
|
-
|
|
6426
|
-
|
|
6427
|
-
const slug = slugify(name.trim());
|
|
6428
|
-
return slug || "unknown";
|
|
6429
|
-
} catch {
|
|
6430
|
-
return "unknown";
|
|
6431
|
-
}
|
|
6604
|
+
if (opts.username) return opts.username;
|
|
6605
|
+
return deriveShortUsername(opts.cwd, "unknown");
|
|
6432
6606
|
},
|
|
6433
6607
|
getDefaultBranch(opts) {
|
|
6434
6608
|
return getDefaultBranch(opts.cwd);
|
|
@@ -6517,9 +6691,9 @@ var datasource = {
|
|
|
6517
6691
|
};
|
|
6518
6692
|
|
|
6519
6693
|
// src/datasources/azdevops.ts
|
|
6694
|
+
init_logger();
|
|
6520
6695
|
import { execFile as execFile2 } from "child_process";
|
|
6521
6696
|
import { promisify as promisify2 } from "util";
|
|
6522
|
-
init_logger();
|
|
6523
6697
|
import { PullRequestStatus } from "azure-devops-node-api/interfaces/GitInterfaces.js";
|
|
6524
6698
|
var exec2 = promisify2(execFile2);
|
|
6525
6699
|
var doneStateCache = /* @__PURE__ */ new Map();
|
|
@@ -6630,6 +6804,29 @@ async function fetchComments(workItemId, project, connection) {
|
|
|
6630
6804
|
return [];
|
|
6631
6805
|
}
|
|
6632
6806
|
}
|
|
6807
|
+
async function deriveShortUsername2(cwd, fallback) {
|
|
6808
|
+
try {
|
|
6809
|
+
const raw = (await git2(["config", "user.name"], cwd)).trim();
|
|
6810
|
+
if (raw) {
|
|
6811
|
+
const parts = raw.toLowerCase().replace(/[^a-z\s]/g, "").trim().split(/\s+/);
|
|
6812
|
+
if (parts.length >= 2) {
|
|
6813
|
+
return parts[0].slice(0, 2) + parts[parts.length - 1].slice(0, 6) || fallback;
|
|
6814
|
+
}
|
|
6815
|
+
}
|
|
6816
|
+
} catch {
|
|
6817
|
+
}
|
|
6818
|
+
try {
|
|
6819
|
+
const raw = (await git2(["config", "user.email"], cwd)).trim();
|
|
6820
|
+
if (raw) {
|
|
6821
|
+
const localPart = raw.split("@")[0].toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
6822
|
+
if (localPart) {
|
|
6823
|
+
return localPart.slice(0, 8);
|
|
6824
|
+
}
|
|
6825
|
+
}
|
|
6826
|
+
} catch {
|
|
6827
|
+
}
|
|
6828
|
+
return fallback;
|
|
6829
|
+
}
|
|
6633
6830
|
var datasource2 = {
|
|
6634
6831
|
name: "azdevops",
|
|
6635
6832
|
supportsGit() {
|
|
@@ -6774,17 +6971,11 @@ var datasource2 = {
|
|
|
6774
6971
|
return this.getDefaultBranch(opts);
|
|
6775
6972
|
},
|
|
6776
6973
|
async getUsername(opts) {
|
|
6777
|
-
|
|
6778
|
-
|
|
6779
|
-
const slug = slugify(name.trim());
|
|
6780
|
-
if (slug) return slug;
|
|
6781
|
-
} catch {
|
|
6782
|
-
}
|
|
6783
|
-
return "unknown";
|
|
6974
|
+
if (opts.username) return opts.username;
|
|
6975
|
+
return deriveShortUsername2(opts.cwd, "unknown");
|
|
6784
6976
|
},
|
|
6785
|
-
buildBranchName(issueNumber,
|
|
6786
|
-
const
|
|
6787
|
-
const branch = `${username}/dispatch/${issueNumber}-${slug}`;
|
|
6977
|
+
buildBranchName(issueNumber, _title, username) {
|
|
6978
|
+
const branch = `${username}/dispatch/issue-${issueNumber}`;
|
|
6788
6979
|
if (!isValidBranchName(branch)) {
|
|
6789
6980
|
throw new InvalidBranchNameError(branch);
|
|
6790
6981
|
}
|
|
@@ -6891,6 +7082,13 @@ import { basename, dirname as dirname5, isAbsolute, join as join5, parse as pars
|
|
|
6891
7082
|
import { promisify as promisify4 } from "util";
|
|
6892
7083
|
import { glob } from "glob";
|
|
6893
7084
|
|
|
7085
|
+
// src/helpers/slugify.ts
|
|
7086
|
+
var MAX_SLUG_LENGTH = 60;
|
|
7087
|
+
function slugify(input3, maxLength) {
|
|
7088
|
+
const slug = input3.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
7089
|
+
return maxLength != null ? slug.slice(0, maxLength) : slug;
|
|
7090
|
+
}
|
|
7091
|
+
|
|
6894
7092
|
// src/config.ts
|
|
6895
7093
|
init_providers();
|
|
6896
7094
|
import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
|
|
@@ -7026,6 +7224,12 @@ async function runInteractiveConfigWizard(configDir) {
|
|
|
7026
7224
|
});
|
|
7027
7225
|
if (areaInput.trim()) area = areaInput.trim();
|
|
7028
7226
|
}
|
|
7227
|
+
try {
|
|
7228
|
+
await ensureAuthReady(effectiveSource ?? void 0, process.cwd(), org);
|
|
7229
|
+
} catch (err) {
|
|
7230
|
+
log.warn(`Authentication failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
7231
|
+
log.warn("You can re-run 'dispatch config' or authenticate later at runtime.");
|
|
7232
|
+
}
|
|
7029
7233
|
const newConfig = {
|
|
7030
7234
|
provider,
|
|
7031
7235
|
source
|
|
@@ -7067,9 +7271,12 @@ async function runInteractiveConfigWizard(configDir) {
|
|
|
7067
7271
|
var CONFIG_BOUNDS = {
|
|
7068
7272
|
testTimeout: { min: 1, max: 120 },
|
|
7069
7273
|
planTimeout: { min: 1, max: 120 },
|
|
7274
|
+
specTimeout: { min: 1, max: 120 },
|
|
7275
|
+
specWarnTimeout: { min: 1, max: 120 },
|
|
7276
|
+
specKillTimeout: { min: 1, max: 120 },
|
|
7070
7277
|
concurrency: { min: 1, max: 64 }
|
|
7071
7278
|
};
|
|
7072
|
-
var CONFIG_KEYS = ["provider", "model", "source", "testTimeout", "planTimeout", "concurrency", "org", "project", "workItemType", "iteration", "area"];
|
|
7279
|
+
var CONFIG_KEYS = ["provider", "model", "source", "testTimeout", "planTimeout", "specTimeout", "specWarnTimeout", "specKillTimeout", "concurrency", "org", "project", "workItemType", "iteration", "area", "username"];
|
|
7073
7280
|
function getConfigPath(configDir) {
|
|
7074
7281
|
const dir = configDir ?? join4(process.cwd(), ".dispatch");
|
|
7075
7282
|
return join4(dir, "config.json");
|
|
@@ -7153,10 +7360,33 @@ function toIssueDetails(filename, content, dir) {
|
|
|
7153
7360
|
acceptanceCriteria: ""
|
|
7154
7361
|
};
|
|
7155
7362
|
}
|
|
7156
|
-
|
|
7157
|
-
|
|
7158
|
-
|
|
7159
|
-
|
|
7363
|
+
async function deriveShortUsername3(cwd, fallback) {
|
|
7364
|
+
try {
|
|
7365
|
+
const raw = (await git3(["config", "user.name"], cwd)).trim();
|
|
7366
|
+
if (raw) {
|
|
7367
|
+
const parts = raw.toLowerCase().replace(/[^a-z\s]/g, "").trim().split(/\s+/);
|
|
7368
|
+
if (parts.length >= 2) {
|
|
7369
|
+
return parts[0].slice(0, 2) + parts[parts.length - 1].slice(0, 6) || fallback;
|
|
7370
|
+
}
|
|
7371
|
+
}
|
|
7372
|
+
} catch {
|
|
7373
|
+
}
|
|
7374
|
+
try {
|
|
7375
|
+
const raw = (await git3(["config", "user.email"], cwd)).trim();
|
|
7376
|
+
if (raw) {
|
|
7377
|
+
const localPart = raw.split("@")[0].toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
7378
|
+
if (localPart) {
|
|
7379
|
+
return localPart.slice(0, 8);
|
|
7380
|
+
}
|
|
7381
|
+
}
|
|
7382
|
+
} catch {
|
|
7383
|
+
}
|
|
7384
|
+
return fallback;
|
|
7385
|
+
}
|
|
7386
|
+
var datasource3 = {
|
|
7387
|
+
name: "md",
|
|
7388
|
+
supportsGit() {
|
|
7389
|
+
return true;
|
|
7160
7390
|
},
|
|
7161
7391
|
async list(opts) {
|
|
7162
7392
|
if (opts?.pattern) {
|
|
@@ -7267,29 +7497,22 @@ var datasource3 = {
|
|
|
7267
7497
|
return this.getDefaultBranch(opts);
|
|
7268
7498
|
},
|
|
7269
7499
|
async getUsername(opts) {
|
|
7270
|
-
|
|
7271
|
-
|
|
7272
|
-
const name = stdout.trim();
|
|
7273
|
-
if (!name) return "local";
|
|
7274
|
-
return slugify(name);
|
|
7275
|
-
} catch {
|
|
7276
|
-
return "local";
|
|
7277
|
-
}
|
|
7500
|
+
if (opts.username) return opts.username;
|
|
7501
|
+
return deriveShortUsername3(opts.cwd, "local");
|
|
7278
7502
|
},
|
|
7279
|
-
buildBranchName(issueNumber,
|
|
7280
|
-
const slug = slugify(title, 50);
|
|
7503
|
+
buildBranchName(issueNumber, _title, username) {
|
|
7281
7504
|
if (issueNumber.includes("/") || issueNumber.includes("\\")) {
|
|
7282
7505
|
const normalized = issueNumber.replaceAll("\\", "/");
|
|
7283
7506
|
const filename = basename(normalized);
|
|
7284
7507
|
const idMatch = /^(\d+)-(.+)\.md$/.exec(filename);
|
|
7285
7508
|
if (idMatch) {
|
|
7286
|
-
return `${username}/dispatch/
|
|
7509
|
+
return `${username}/dispatch/issue-${idMatch[1]}`;
|
|
7287
7510
|
}
|
|
7288
7511
|
const nameWithoutExt = parsePath(filename).name;
|
|
7289
7512
|
const slugifiedName = slugify(nameWithoutExt, 50);
|
|
7290
|
-
return `${username}/dispatch/file-${slugifiedName}
|
|
7513
|
+
return `${username}/dispatch/file-${slugifiedName}`;
|
|
7291
7514
|
}
|
|
7292
|
-
return `${username}/dispatch
|
|
7515
|
+
return `${username}/dispatch/issue-${issueNumber}`;
|
|
7293
7516
|
},
|
|
7294
7517
|
async createAndSwitchBranch(branchName, opts) {
|
|
7295
7518
|
try {
|
|
@@ -7439,6 +7662,9 @@ function parseGitHubRemoteUrl(url) {
|
|
|
7439
7662
|
// src/spec-generator.ts
|
|
7440
7663
|
init_logger();
|
|
7441
7664
|
var MB_PER_CONCURRENT_TASK = 500;
|
|
7665
|
+
var DEFAULT_SPEC_TIMEOUT_MIN = 10;
|
|
7666
|
+
var DEFAULT_SPEC_WARN_MIN = 10;
|
|
7667
|
+
var DEFAULT_SPEC_KILL_MIN = 10;
|
|
7442
7668
|
var RECOGNIZED_H2 = /* @__PURE__ */ new Set([
|
|
7443
7669
|
"## Context",
|
|
7444
7670
|
"## Why",
|
|
@@ -7900,12 +8126,16 @@ var CONFIG_TO_CLI = {
|
|
|
7900
8126
|
source: "issueSource",
|
|
7901
8127
|
testTimeout: "testTimeout",
|
|
7902
8128
|
planTimeout: "planTimeout",
|
|
8129
|
+
specTimeout: "specTimeout",
|
|
8130
|
+
specWarnTimeout: "specWarnTimeout",
|
|
8131
|
+
specKillTimeout: "specKillTimeout",
|
|
7903
8132
|
concurrency: "concurrency",
|
|
7904
8133
|
org: "org",
|
|
7905
8134
|
project: "project",
|
|
7906
8135
|
workItemType: "workItemType",
|
|
7907
8136
|
iteration: "iteration",
|
|
7908
|
-
area: "area"
|
|
8137
|
+
area: "area",
|
|
8138
|
+
username: "username"
|
|
7909
8139
|
};
|
|
7910
8140
|
function setCliField(target, key, value) {
|
|
7911
8141
|
target[key] = value;
|
|
@@ -7968,6 +8198,7 @@ init_providers();
|
|
|
7968
8198
|
import { mkdir as mkdir4, readFile as readFile5, writeFile as writeFile6, unlink } from "fs/promises";
|
|
7969
8199
|
import { join as join10, resolve as resolve2, sep } from "path";
|
|
7970
8200
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
8201
|
+
init_timeout();
|
|
7971
8202
|
init_logger();
|
|
7972
8203
|
init_file_logger();
|
|
7973
8204
|
init_environment();
|
|
@@ -7979,7 +8210,7 @@ async function boot5(opts) {
|
|
|
7979
8210
|
return {
|
|
7980
8211
|
name: "spec",
|
|
7981
8212
|
async generate(genOpts) {
|
|
7982
|
-
const { issue, filePath, fileContent, inlineText, cwd: workingDir, outputPath } = genOpts;
|
|
8213
|
+
const { issue, filePath, fileContent, inlineText, cwd: workingDir, outputPath, onProgress } = genOpts;
|
|
7983
8214
|
const startTime = Date.now();
|
|
7984
8215
|
try {
|
|
7985
8216
|
const resolvedCwd = resolve2(workingDir);
|
|
@@ -8014,7 +8245,50 @@ async function boot5(opts) {
|
|
|
8014
8245
|
fileLoggerStorage.getStore()?.prompt("spec", prompt);
|
|
8015
8246
|
const sessionId = await provider.createSession();
|
|
8016
8247
|
log.debug(`Spec prompt built (${prompt.length} chars)`);
|
|
8017
|
-
const
|
|
8248
|
+
const warnMs = genOpts.timeboxWarnMs ?? DEFAULT_SPEC_WARN_MIN * 6e4;
|
|
8249
|
+
const killMs = genOpts.timeboxKillMs ?? DEFAULT_SPEC_KILL_MIN * 6e4;
|
|
8250
|
+
const response = await new Promise((resolve5, reject) => {
|
|
8251
|
+
let settled = false;
|
|
8252
|
+
let warnTimer;
|
|
8253
|
+
let killTimer;
|
|
8254
|
+
const cleanup = () => {
|
|
8255
|
+
if (warnTimer) clearTimeout(warnTimer);
|
|
8256
|
+
if (killTimer) clearTimeout(killTimer);
|
|
8257
|
+
};
|
|
8258
|
+
warnTimer = setTimeout(() => {
|
|
8259
|
+
if (settled) return;
|
|
8260
|
+
const remainingSec = Math.round(killMs / 1e3);
|
|
8261
|
+
const warnMessage = `Your spec generation time is done. You have exceeded the ${Math.round(warnMs / 6e4)}-minute limit. You MUST write the spec file to "${outputPath}" immediately. If you do not comply within ${remainingSec} seconds, you will be terminated.`;
|
|
8262
|
+
log.warn(`Timebox warn fired for session ${sessionId} \u2014 sending wrap-up message`);
|
|
8263
|
+
if (provider.send) {
|
|
8264
|
+
provider.send(sessionId, warnMessage).catch((err) => {
|
|
8265
|
+
log.warn(`Failed to send timebox warning: ${log.extractMessage(err)}`);
|
|
8266
|
+
});
|
|
8267
|
+
} else {
|
|
8268
|
+
log.warn(`Provider does not support send() \u2014 cannot deliver timebox warning`);
|
|
8269
|
+
}
|
|
8270
|
+
killTimer = setTimeout(() => {
|
|
8271
|
+
if (settled) return;
|
|
8272
|
+
settled = true;
|
|
8273
|
+
cleanup();
|
|
8274
|
+
reject(new TimeoutError(warnMs + killMs, "spec timebox"));
|
|
8275
|
+
}, killMs);
|
|
8276
|
+
}, warnMs);
|
|
8277
|
+
provider.prompt(sessionId, prompt, { onProgress }).then(
|
|
8278
|
+
(value) => {
|
|
8279
|
+
if (settled) return;
|
|
8280
|
+
settled = true;
|
|
8281
|
+
cleanup();
|
|
8282
|
+
resolve5(value);
|
|
8283
|
+
},
|
|
8284
|
+
(err) => {
|
|
8285
|
+
if (settled) return;
|
|
8286
|
+
settled = true;
|
|
8287
|
+
cleanup();
|
|
8288
|
+
reject(err);
|
|
8289
|
+
}
|
|
8290
|
+
);
|
|
8291
|
+
});
|
|
8018
8292
|
if (response === null) {
|
|
8019
8293
|
return {
|
|
8020
8294
|
data: null,
|
|
@@ -8140,12 +8414,18 @@ function buildCommonSpecInstructions(params) {
|
|
|
8140
8414
|
return [
|
|
8141
8415
|
`You are a **spec agent**. Your job is to explore the codebase, understand ${subject}, and write a high-level **markdown spec file** to disk that will drive an automated implementation pipeline.`,
|
|
8142
8416
|
``,
|
|
8417
|
+
`**Time limit:** You have ${DEFAULT_SPEC_WARN_MIN} minutes to complete this spec. Work efficiently and focus on delivering a complete, well-structured spec within this window.`,
|
|
8418
|
+
``,
|
|
8143
8419
|
`**Important:** This file will be consumed by a two-stage pipeline:`,
|
|
8144
8420
|
`1. A **planner agent** reads each task together with the prose context in this file, then explores the codebase to produce a detailed, line-level implementation plan.`,
|
|
8145
8421
|
`2. A **coder agent** follows that detailed plan to make the actual code changes.`,
|
|
8146
8422
|
``,
|
|
8147
8423
|
`Because the planner agent handles low-level details, your spec must stay **high-level and strategic**. Focus on the WHAT, WHY, and HOW \u2014 not exact code or line numbers.`,
|
|
8148
8424
|
``,
|
|
8425
|
+
`**Scope:** Each invocation is scoped to exactly one source item. The source item for this invocation is the single passed issue, file, or inline request shown below.`,
|
|
8426
|
+
`Treat other repository materials \u2014 including existing spec files, sibling issues, and future work \u2014 as context only unless the passed source explicitly references them as required context.`,
|
|
8427
|
+
`Do not merge unrelated specs, issues, files, or requests into the generated output.`,
|
|
8428
|
+
``,
|
|
8149
8429
|
`**CRITICAL \u2014 Output constraints (read carefully):**`,
|
|
8150
8430
|
`The file you write must contain ONLY the structured spec content described below. You MUST NOT include:`,
|
|
8151
8431
|
`- **No preamble:** Do not add any text before the H1 heading (e.g., "Here's the spec:", "I've written the spec file to...")`,
|
|
@@ -8300,7 +8580,11 @@ function buildInlineTextSpecPrompt(text, cwd, outputPath) {
|
|
|
8300
8580
|
init_cleanup();
|
|
8301
8581
|
init_logger();
|
|
8302
8582
|
init_file_logger();
|
|
8583
|
+
import chalk6 from "chalk";
|
|
8584
|
+
|
|
8585
|
+
// src/tui.ts
|
|
8303
8586
|
import chalk5 from "chalk";
|
|
8587
|
+
import { emitKeypressEvents } from "readline";
|
|
8304
8588
|
|
|
8305
8589
|
// src/helpers/format.ts
|
|
8306
8590
|
import chalk4 from "chalk";
|
|
@@ -8326,8 +8610,402 @@ function renderHeaderLines(info) {
|
|
|
8326
8610
|
return lines;
|
|
8327
8611
|
}
|
|
8328
8612
|
|
|
8613
|
+
// src/tui.ts
|
|
8614
|
+
var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
8615
|
+
var BAR_WIDTH = 30;
|
|
8616
|
+
var spinnerIndex = 0;
|
|
8617
|
+
var interval = null;
|
|
8618
|
+
var lastLineCount = 0;
|
|
8619
|
+
function spinner() {
|
|
8620
|
+
return chalk5.cyan(SPINNER_FRAMES[spinnerIndex % SPINNER_FRAMES.length]);
|
|
8621
|
+
}
|
|
8622
|
+
function progressBar(done, total) {
|
|
8623
|
+
if (total === 0) return chalk5.dim("\u2591".repeat(BAR_WIDTH));
|
|
8624
|
+
const filled = Math.round(done / total * BAR_WIDTH);
|
|
8625
|
+
const empty = BAR_WIDTH - filled;
|
|
8626
|
+
const pct = Math.round(done / total * 100);
|
|
8627
|
+
return chalk5.green("\u2588".repeat(filled)) + chalk5.dim("\u2591".repeat(empty)) + chalk5.white(` ${pct}%`);
|
|
8628
|
+
}
|
|
8629
|
+
function statusIcon(status) {
|
|
8630
|
+
switch (status) {
|
|
8631
|
+
case "pending":
|
|
8632
|
+
return chalk5.dim("\u25CB");
|
|
8633
|
+
case "planning":
|
|
8634
|
+
return spinner();
|
|
8635
|
+
case "running":
|
|
8636
|
+
case "generating":
|
|
8637
|
+
case "syncing":
|
|
8638
|
+
return spinner();
|
|
8639
|
+
case "paused":
|
|
8640
|
+
return chalk5.yellow("\u25D0");
|
|
8641
|
+
case "done":
|
|
8642
|
+
return chalk5.green("\u25CF");
|
|
8643
|
+
case "failed":
|
|
8644
|
+
return chalk5.red("\u2716");
|
|
8645
|
+
}
|
|
8646
|
+
}
|
|
8647
|
+
function statusLabel(status) {
|
|
8648
|
+
switch (status) {
|
|
8649
|
+
case "pending":
|
|
8650
|
+
return chalk5.dim("pending");
|
|
8651
|
+
case "planning":
|
|
8652
|
+
return chalk5.magenta("planning");
|
|
8653
|
+
case "running":
|
|
8654
|
+
return chalk5.cyan("executing");
|
|
8655
|
+
case "generating":
|
|
8656
|
+
return chalk5.cyan("generating");
|
|
8657
|
+
case "syncing":
|
|
8658
|
+
return chalk5.cyan("syncing");
|
|
8659
|
+
case "paused":
|
|
8660
|
+
return chalk5.yellow("paused");
|
|
8661
|
+
case "done":
|
|
8662
|
+
return chalk5.green("done");
|
|
8663
|
+
case "failed":
|
|
8664
|
+
return chalk5.red("failed");
|
|
8665
|
+
}
|
|
8666
|
+
}
|
|
8667
|
+
function phaseLabel(phase, provider, mode = "dispatch") {
|
|
8668
|
+
switch (phase) {
|
|
8669
|
+
case "discovering":
|
|
8670
|
+
return `${spinner()} Discovering task files...`;
|
|
8671
|
+
case "parsing":
|
|
8672
|
+
return `${spinner()} Parsing tasks...`;
|
|
8673
|
+
case "booting": {
|
|
8674
|
+
const name = provider ?? "provider";
|
|
8675
|
+
return `${spinner()} Connecting to ${name}...`;
|
|
8676
|
+
}
|
|
8677
|
+
case "dispatching":
|
|
8678
|
+
return mode === "spec" ? `${spinner()} Generating specs...` : `${spinner()} Dispatching tasks...`;
|
|
8679
|
+
case "paused":
|
|
8680
|
+
return chalk5.yellow("\u25D0") + " Waiting for rerun...";
|
|
8681
|
+
case "done":
|
|
8682
|
+
return chalk5.green("\u2714") + " Complete";
|
|
8683
|
+
}
|
|
8684
|
+
}
|
|
8685
|
+
function isActiveStatus(status) {
|
|
8686
|
+
return status === "planning" || status === "running" || status === "generating" || status === "syncing";
|
|
8687
|
+
}
|
|
8688
|
+
function sanitizeSubordinateText(text) {
|
|
8689
|
+
return text.replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, "").replace(/[\r\n]+/g, " ").replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]+/g, "").replace(/\s+/g, " ").trim();
|
|
8690
|
+
}
|
|
8691
|
+
function truncateText(text, maxLen) {
|
|
8692
|
+
if (text.length <= maxLen) return text;
|
|
8693
|
+
return text.slice(0, Math.max(0, maxLen - 1)) + "\u2026";
|
|
8694
|
+
}
|
|
8695
|
+
function renderTaskError(error) {
|
|
8696
|
+
if (!error) return null;
|
|
8697
|
+
return chalk5.red(` \u2514\u2500 ${error}`);
|
|
8698
|
+
}
|
|
8699
|
+
function renderTaskFeedback(feedback, cols) {
|
|
8700
|
+
if (!feedback) return null;
|
|
8701
|
+
const sanitized = sanitizeSubordinateText(feedback);
|
|
8702
|
+
if (!sanitized) return null;
|
|
8703
|
+
const maxLen = Math.max(16, cols - 10);
|
|
8704
|
+
return chalk5.dim(` \u2514\u2500 ${truncateText(sanitized, maxLen)}`);
|
|
8705
|
+
}
|
|
8706
|
+
function countVisualRows(text, cols) {
|
|
8707
|
+
const stripped = text.replace(/\x1B\[[0-9;]*m/g, "");
|
|
8708
|
+
const safeCols = Math.max(1, cols);
|
|
8709
|
+
return stripped.split("\n").reduce((sum, line) => {
|
|
8710
|
+
return sum + Math.max(1, Math.ceil(line.length / safeCols));
|
|
8711
|
+
}, 0);
|
|
8712
|
+
}
|
|
8713
|
+
function toggleRecoveryAction(action) {
|
|
8714
|
+
return action === "rerun" ? "quit" : "rerun";
|
|
8715
|
+
}
|
|
8716
|
+
function renderRecoveryAction(action, selectedAction) {
|
|
8717
|
+
const selected = action === selectedAction;
|
|
8718
|
+
if (action === "rerun") {
|
|
8719
|
+
return selected ? chalk5.greenBright(`[\u25B6 rerun]`) : chalk5.dim("\u25B6 rerun");
|
|
8720
|
+
}
|
|
8721
|
+
return selected ? chalk5.redBright("[q quit]") : chalk5.dim("q quit");
|
|
8722
|
+
}
|
|
8723
|
+
function render(state, cols) {
|
|
8724
|
+
const lines = [];
|
|
8725
|
+
const now = Date.now();
|
|
8726
|
+
const totalElapsed = elapsed(now - state.startTime);
|
|
8727
|
+
const done = state.tasks.filter((t) => t.status === "done").length;
|
|
8728
|
+
const failed = state.tasks.filter((t) => t.status === "failed").length;
|
|
8729
|
+
const total = state.tasks.length;
|
|
8730
|
+
lines.push("");
|
|
8731
|
+
lines.push(
|
|
8732
|
+
...renderHeaderLines({
|
|
8733
|
+
provider: state.provider,
|
|
8734
|
+
model: state.model,
|
|
8735
|
+
source: state.source
|
|
8736
|
+
})
|
|
8737
|
+
);
|
|
8738
|
+
if (state.currentIssue) {
|
|
8739
|
+
lines.push(
|
|
8740
|
+
chalk5.dim(` issue: `) + chalk5.white(`#${state.currentIssue.number}`) + chalk5.dim(` \u2014 ${state.currentIssue.title}`)
|
|
8741
|
+
);
|
|
8742
|
+
}
|
|
8743
|
+
lines.push(chalk5.dim(" \u2500".repeat(24)));
|
|
8744
|
+
if (state.notification) {
|
|
8745
|
+
lines.push("");
|
|
8746
|
+
for (const notifLine of state.notification.split("\n")) {
|
|
8747
|
+
lines.push(" " + chalk5.yellowBright("\u26A0 ") + chalk5.yellow(notifLine));
|
|
8748
|
+
}
|
|
8749
|
+
}
|
|
8750
|
+
lines.push(` ${phaseLabel(state.phase, state.provider, state.mode)}` + chalk5.dim(` ${totalElapsed}`));
|
|
8751
|
+
if (state.phase === "dispatching" || state.phase === "paused" || state.phase === "done") {
|
|
8752
|
+
lines.push("");
|
|
8753
|
+
lines.push(` ${progressBar(done + failed, total)} ${chalk5.dim(`${done + failed}/${total} tasks`)}`);
|
|
8754
|
+
lines.push("");
|
|
8755
|
+
const activeWorktrees = new Set(
|
|
8756
|
+
state.tasks.map((t) => t.worktree).filter(Boolean)
|
|
8757
|
+
);
|
|
8758
|
+
const showWorktree = activeWorktrees.size > 1;
|
|
8759
|
+
const maxTextLen = cols - 30;
|
|
8760
|
+
const paused = state.tasks.filter((t) => t.status === "paused");
|
|
8761
|
+
const running = state.tasks.filter((t) => isActiveStatus(t.status));
|
|
8762
|
+
const completed = state.tasks.filter(
|
|
8763
|
+
(t) => t.status === "done" || t.status === "failed"
|
|
8764
|
+
);
|
|
8765
|
+
const pending = state.tasks.filter((t) => t.status === "pending");
|
|
8766
|
+
if (showWorktree) {
|
|
8767
|
+
const groups = /* @__PURE__ */ new Map();
|
|
8768
|
+
const ungrouped = [];
|
|
8769
|
+
for (const ts of state.tasks) {
|
|
8770
|
+
if (ts.worktree) {
|
|
8771
|
+
const arr = groups.get(ts.worktree) ?? [];
|
|
8772
|
+
arr.push(ts);
|
|
8773
|
+
groups.set(ts.worktree, arr);
|
|
8774
|
+
} else {
|
|
8775
|
+
ungrouped.push(ts);
|
|
8776
|
+
}
|
|
8777
|
+
}
|
|
8778
|
+
const doneGroups = [];
|
|
8779
|
+
const activeGroups = [];
|
|
8780
|
+
for (const [wt, tasks] of groups) {
|
|
8781
|
+
const allDone = tasks.every((t) => t.status === "done" || t.status === "failed");
|
|
8782
|
+
if (allDone) {
|
|
8783
|
+
doneGroups.push([wt, tasks]);
|
|
8784
|
+
} else {
|
|
8785
|
+
activeGroups.push([wt, tasks]);
|
|
8786
|
+
}
|
|
8787
|
+
}
|
|
8788
|
+
if (doneGroups.length > 3) {
|
|
8789
|
+
lines.push(chalk5.dim(` \xB7\xB7\xB7 ${doneGroups.length - 3} earlier issue(s) completed`));
|
|
8790
|
+
}
|
|
8791
|
+
for (const [wt, tasks] of doneGroups.slice(-3)) {
|
|
8792
|
+
const issueNum = wt.match(/^(\d+)/)?.[1] ?? wt.slice(0, 12);
|
|
8793
|
+
const anyFailed = tasks.some((t) => t.status === "failed");
|
|
8794
|
+
const icon = anyFailed ? chalk5.red("\u2716") : chalk5.green("\u25CF");
|
|
8795
|
+
const doneCount = tasks.filter((t) => t.status === "done").length;
|
|
8796
|
+
const maxElapsed = Math.max(...tasks.map((t) => t.elapsed ?? 0));
|
|
8797
|
+
lines.push(` ${icon} ${chalk5.dim(`#${issueNum}`)} ${chalk5.dim(`${doneCount}/${tasks.length} tasks`)} ${chalk5.dim(elapsed(maxElapsed))}`);
|
|
8798
|
+
}
|
|
8799
|
+
for (const [wt, tasks] of activeGroups) {
|
|
8800
|
+
const issueNum = wt.match(/^(\d+)/)?.[1] ?? wt.slice(0, 12);
|
|
8801
|
+
const activeTasks = tasks.filter((t) => isActiveStatus(t.status) || t.status === "paused");
|
|
8802
|
+
const firstActive = activeTasks[0];
|
|
8803
|
+
const displayStatus = firstActive?.status ?? "pending";
|
|
8804
|
+
const truncLen = Math.min(cols - 26, 60);
|
|
8805
|
+
let text = firstActive?.task.text ?? tasks[0]?.task.text ?? "";
|
|
8806
|
+
if (text.length > truncLen) {
|
|
8807
|
+
text = text.slice(0, truncLen - 1) + "\u2026";
|
|
8808
|
+
}
|
|
8809
|
+
const earliest = activeTasks.length > 0 ? Math.min(...activeTasks.map((t) => t.elapsed ?? now)) : now;
|
|
8810
|
+
const elapsedStr = elapsed(now - earliest);
|
|
8811
|
+
const countLabel = activeTasks.length > 0 ? `${activeTasks.length} active` : `${tasks.length} pending`;
|
|
8812
|
+
lines.push(` ${statusIcon(displayStatus)} ${chalk5.white(`#${issueNum}`)} ${countLabel} ${text} ${chalk5.dim(elapsedStr)}`);
|
|
8813
|
+
}
|
|
8814
|
+
for (const ts of ungrouped) {
|
|
8815
|
+
if (!isActiveStatus(ts.status) && ts.status !== "paused") continue;
|
|
8816
|
+
const icon = statusIcon(ts.status);
|
|
8817
|
+
const idx = chalk5.dim(`#${state.tasks.indexOf(ts) + 1}`);
|
|
8818
|
+
const text = truncateText(ts.task.text, maxTextLen);
|
|
8819
|
+
const elapsedStr = chalk5.dim(` ${elapsed(now - (ts.elapsed || now))}`);
|
|
8820
|
+
const label = statusLabel(ts.status);
|
|
8821
|
+
lines.push(` ${icon} ${idx} ${text} ${label}${elapsedStr}`);
|
|
8822
|
+
const feedbackLine = ts.status === "generating" ? renderTaskFeedback(ts.feedback, cols) : null;
|
|
8823
|
+
if (feedbackLine) {
|
|
8824
|
+
lines.push(feedbackLine);
|
|
8825
|
+
}
|
|
8826
|
+
const errorLine = renderTaskError(ts.error);
|
|
8827
|
+
if (errorLine) {
|
|
8828
|
+
lines.push(errorLine);
|
|
8829
|
+
}
|
|
8830
|
+
}
|
|
8831
|
+
} else {
|
|
8832
|
+
const visibleRunning = running.slice(0, 8);
|
|
8833
|
+
const visible = [
|
|
8834
|
+
...completed.slice(-3),
|
|
8835
|
+
...paused.slice(0, 3),
|
|
8836
|
+
...visibleRunning,
|
|
8837
|
+
...pending.slice(0, 3)
|
|
8838
|
+
];
|
|
8839
|
+
if (completed.length > 3) {
|
|
8840
|
+
lines.push(chalk5.dim(` \xB7\xB7\xB7 ${completed.length - 3} earlier task(s) completed`));
|
|
8841
|
+
}
|
|
8842
|
+
for (const ts of visible) {
|
|
8843
|
+
const icon = statusIcon(ts.status);
|
|
8844
|
+
const idx = chalk5.dim(`#${state.tasks.indexOf(ts) + 1}`);
|
|
8845
|
+
const text = truncateText(ts.task.text, maxTextLen);
|
|
8846
|
+
const elapsedStr = isActiveStatus(ts.status) ? chalk5.dim(` ${elapsed(now - (ts.elapsed || now))}`) : ts.status === "done" && ts.elapsed ? chalk5.dim(` ${elapsed(ts.elapsed)}`) : "";
|
|
8847
|
+
const label = statusLabel(ts.status);
|
|
8848
|
+
lines.push(` ${icon} ${idx} ${text} ${label}${elapsedStr}`);
|
|
8849
|
+
const feedbackLine = ts.status === "generating" ? renderTaskFeedback(ts.feedback, cols) : null;
|
|
8850
|
+
if (feedbackLine) {
|
|
8851
|
+
lines.push(feedbackLine);
|
|
8852
|
+
}
|
|
8853
|
+
const errorLine = renderTaskError(ts.error);
|
|
8854
|
+
if (errorLine) {
|
|
8855
|
+
lines.push(errorLine);
|
|
8856
|
+
}
|
|
8857
|
+
}
|
|
8858
|
+
if (running.length > 8) {
|
|
8859
|
+
lines.push(chalk5.dim(` \xB7\xB7\xB7 ${running.length - 8} more running`));
|
|
8860
|
+
}
|
|
8861
|
+
if (pending.length > 3) {
|
|
8862
|
+
lines.push(chalk5.dim(` \xB7\xB7\xB7 ${pending.length - 3} more task(s) pending`));
|
|
8863
|
+
}
|
|
8864
|
+
}
|
|
8865
|
+
if (state.phase === "paused" && state.recovery) {
|
|
8866
|
+
const selectedAction = state.recovery.selectedAction ?? "rerun";
|
|
8867
|
+
lines.push("");
|
|
8868
|
+
lines.push(` ${chalk5.yellow("Recovery")}: ${chalk5.white(`#${state.recovery.taskIndex + 1}`)} ${state.recovery.taskText}`);
|
|
8869
|
+
lines.push(` ${chalk5.red(state.recovery.error)}`);
|
|
8870
|
+
if (state.recovery.issue) {
|
|
8871
|
+
lines.push(` ${chalk5.dim(`Issue #${state.recovery.issue.number} - ${state.recovery.issue.title}`)}`);
|
|
8872
|
+
}
|
|
8873
|
+
if (state.recovery.worktree) {
|
|
8874
|
+
lines.push(` ${chalk5.dim(`Worktree: ${state.recovery.worktree}`)}`);
|
|
8875
|
+
}
|
|
8876
|
+
lines.push(` ${chalk5.red("\u2716")} ${renderRecoveryAction("rerun", selectedAction)} ${renderRecoveryAction("quit", selectedAction)}`);
|
|
8877
|
+
lines.push(` ${chalk5.dim("Tab/\u2190/\u2192 switch \xB7 Enter/Space runs selection \xB7 r reruns \xB7 q quits")}`);
|
|
8878
|
+
}
|
|
8879
|
+
lines.push("");
|
|
8880
|
+
const parts = [];
|
|
8881
|
+
if (done > 0) parts.push(chalk5.green(`${done} passed`));
|
|
8882
|
+
if (failed > 0) parts.push(chalk5.red(`${failed} failed`));
|
|
8883
|
+
if (total - done - failed > 0)
|
|
8884
|
+
parts.push(chalk5.dim(`${total - done - failed} remaining`));
|
|
8885
|
+
lines.push(` ${parts.join(chalk5.dim(" \xB7 "))}`);
|
|
8886
|
+
} else if (state.filesFound > 0) {
|
|
8887
|
+
lines.push(chalk5.dim(` Found ${state.filesFound} file(s)`));
|
|
8888
|
+
}
|
|
8889
|
+
lines.push("");
|
|
8890
|
+
return lines.join("\n");
|
|
8891
|
+
}
|
|
8892
|
+
function drawToOutput(state, output) {
|
|
8893
|
+
const cols = output.columns || 80;
|
|
8894
|
+
const rendered = render(state, cols);
|
|
8895
|
+
const newLineCount = countVisualRows(rendered, cols);
|
|
8896
|
+
let buffer = "";
|
|
8897
|
+
if (lastLineCount > 0) {
|
|
8898
|
+
buffer += `\x1B[${lastLineCount}A`;
|
|
8899
|
+
}
|
|
8900
|
+
const lines = rendered.split("\n");
|
|
8901
|
+
buffer += lines.map((line) => line + "\x1B[K").join("\n");
|
|
8902
|
+
const leftover = lastLineCount - newLineCount;
|
|
8903
|
+
if (leftover > 0) {
|
|
8904
|
+
for (let i = 0; i < leftover; i++) {
|
|
8905
|
+
buffer += "\n\x1B[K";
|
|
8906
|
+
}
|
|
8907
|
+
buffer += `\x1B[${leftover}A`;
|
|
8908
|
+
}
|
|
8909
|
+
output.write(buffer);
|
|
8910
|
+
lastLineCount = newLineCount;
|
|
8911
|
+
}
|
|
8912
|
+
function createTui(options) {
|
|
8913
|
+
const input3 = options?.input ?? process.stdin;
|
|
8914
|
+
const output = options?.output ?? process.stdout;
|
|
8915
|
+
const state = {
|
|
8916
|
+
tasks: [],
|
|
8917
|
+
phase: "discovering",
|
|
8918
|
+
mode: "dispatch",
|
|
8919
|
+
startTime: Date.now(),
|
|
8920
|
+
filesFound: 0
|
|
8921
|
+
};
|
|
8922
|
+
lastLineCount = 0;
|
|
8923
|
+
spinnerIndex = 0;
|
|
8924
|
+
let activeRecoveryPromise = null;
|
|
8925
|
+
let cleanupRecoveryPrompt = null;
|
|
8926
|
+
interval = setInterval(() => {
|
|
8927
|
+
spinnerIndex++;
|
|
8928
|
+
drawToOutput(state, output);
|
|
8929
|
+
}, 80);
|
|
8930
|
+
const update = () => drawToOutput(state, output);
|
|
8931
|
+
const waitForRecoveryAction = () => {
|
|
8932
|
+
if (activeRecoveryPromise) {
|
|
8933
|
+
return activeRecoveryPromise;
|
|
8934
|
+
}
|
|
8935
|
+
activeRecoveryPromise = new Promise((resolve5) => {
|
|
8936
|
+
const ttyInput = input3;
|
|
8937
|
+
const wasRaw = ttyInput.isRaw ?? false;
|
|
8938
|
+
const canToggleRawMode = ttyInput.isTTY === true && typeof ttyInput.setRawMode === "function";
|
|
8939
|
+
if (state.recovery) {
|
|
8940
|
+
state.recovery.selectedAction = state.recovery.selectedAction ?? "rerun";
|
|
8941
|
+
drawToOutput(state, output);
|
|
8942
|
+
}
|
|
8943
|
+
emitKeypressEvents(input3);
|
|
8944
|
+
if (canToggleRawMode) {
|
|
8945
|
+
ttyInput.setRawMode(true);
|
|
8946
|
+
}
|
|
8947
|
+
const finish = (action) => {
|
|
8948
|
+
cleanupRecoveryPrompt?.();
|
|
8949
|
+
resolve5(action);
|
|
8950
|
+
};
|
|
8951
|
+
const updateSelection = (nextAction) => {
|
|
8952
|
+
if (!state.recovery || state.recovery.selectedAction === nextAction) {
|
|
8953
|
+
return;
|
|
8954
|
+
}
|
|
8955
|
+
state.recovery.selectedAction = nextAction;
|
|
8956
|
+
drawToOutput(state, output);
|
|
8957
|
+
};
|
|
8958
|
+
const onKeypress = (str, key) => {
|
|
8959
|
+
const name = key?.name ?? str;
|
|
8960
|
+
if (key?.ctrl && name === "c") {
|
|
8961
|
+
finish("quit");
|
|
8962
|
+
return;
|
|
8963
|
+
}
|
|
8964
|
+
if (name === "r" || name === "R") {
|
|
8965
|
+
finish("rerun");
|
|
8966
|
+
return;
|
|
8967
|
+
}
|
|
8968
|
+
if (name === "q" || name === "Q") {
|
|
8969
|
+
finish("quit");
|
|
8970
|
+
return;
|
|
8971
|
+
}
|
|
8972
|
+
if (name === "tab" || name === "left" || name === "right") {
|
|
8973
|
+
updateSelection(toggleRecoveryAction(state.recovery?.selectedAction ?? "rerun"));
|
|
8974
|
+
return;
|
|
8975
|
+
}
|
|
8976
|
+
if (name === "return" || name === "enter" || name === "space" || str === " ") {
|
|
8977
|
+
finish(state.recovery?.selectedAction ?? "rerun");
|
|
8978
|
+
}
|
|
8979
|
+
};
|
|
8980
|
+
cleanupRecoveryPrompt = () => {
|
|
8981
|
+
input3.off("keypress", onKeypress);
|
|
8982
|
+
if (canToggleRawMode) {
|
|
8983
|
+
ttyInput.setRawMode(wasRaw);
|
|
8984
|
+
}
|
|
8985
|
+
cleanupRecoveryPrompt = null;
|
|
8986
|
+
activeRecoveryPromise = null;
|
|
8987
|
+
};
|
|
8988
|
+
input3.on("keypress", onKeypress);
|
|
8989
|
+
});
|
|
8990
|
+
return activeRecoveryPromise;
|
|
8991
|
+
};
|
|
8992
|
+
const stop = () => {
|
|
8993
|
+
if (interval) {
|
|
8994
|
+
clearInterval(interval);
|
|
8995
|
+
interval = null;
|
|
8996
|
+
}
|
|
8997
|
+
if (activeRecoveryPromise) {
|
|
8998
|
+
cleanupRecoveryPrompt?.();
|
|
8999
|
+
}
|
|
9000
|
+
drawToOutput(state, output);
|
|
9001
|
+
};
|
|
9002
|
+
drawToOutput(state, output);
|
|
9003
|
+
return { state, update, stop, waitForRecoveryAction };
|
|
9004
|
+
}
|
|
9005
|
+
|
|
8329
9006
|
// src/helpers/retry.ts
|
|
8330
9007
|
init_logger();
|
|
9008
|
+
var DEFAULT_RETRY_COUNT = 3;
|
|
8331
9009
|
async function withRetry(fn, maxRetries, options) {
|
|
8332
9010
|
const maxAttempts = maxRetries + 1;
|
|
8333
9011
|
const label = options?.label;
|
|
@@ -8351,6 +9029,48 @@ async function withRetry(fn, maxRetries, options) {
|
|
|
8351
9029
|
|
|
8352
9030
|
// src/orchestrator/spec-pipeline.ts
|
|
8353
9031
|
init_timeout();
|
|
9032
|
+
|
|
9033
|
+
// src/helpers/concurrency.ts
|
|
9034
|
+
async function runWithConcurrency(options) {
|
|
9035
|
+
const { items, concurrency, worker, shouldStop } = options;
|
|
9036
|
+
if (items.length === 0) return [];
|
|
9037
|
+
const limit = Math.max(1, concurrency);
|
|
9038
|
+
const results = new Array(items.length);
|
|
9039
|
+
let nextIndex = 0;
|
|
9040
|
+
return new Promise((resolve5) => {
|
|
9041
|
+
let active = 0;
|
|
9042
|
+
const launch = () => {
|
|
9043
|
+
while (active < limit && nextIndex < items.length) {
|
|
9044
|
+
if (shouldStop?.()) break;
|
|
9045
|
+
const idx = nextIndex++;
|
|
9046
|
+
active++;
|
|
9047
|
+
worker(items[idx], idx).then(
|
|
9048
|
+
(value) => {
|
|
9049
|
+
results[idx] = { status: "fulfilled", value };
|
|
9050
|
+
active--;
|
|
9051
|
+
launch();
|
|
9052
|
+
},
|
|
9053
|
+
(reason) => {
|
|
9054
|
+
results[idx] = { status: "rejected", reason };
|
|
9055
|
+
active--;
|
|
9056
|
+
launch();
|
|
9057
|
+
}
|
|
9058
|
+
);
|
|
9059
|
+
}
|
|
9060
|
+
if (active === 0) {
|
|
9061
|
+
for (let i = 0; i < results.length; i++) {
|
|
9062
|
+
if (!(i in results)) {
|
|
9063
|
+
results[i] = { status: "skipped" };
|
|
9064
|
+
}
|
|
9065
|
+
}
|
|
9066
|
+
resolve5(results);
|
|
9067
|
+
}
|
|
9068
|
+
};
|
|
9069
|
+
launch();
|
|
9070
|
+
});
|
|
9071
|
+
}
|
|
9072
|
+
|
|
9073
|
+
// src/orchestrator/spec-pipeline.ts
|
|
8354
9074
|
var FETCH_TIMEOUT_MS = 3e4;
|
|
8355
9075
|
async function resolveDatasource(issues, issueSource, specCwd, org, project, workItemType, iteration, area) {
|
|
8356
9076
|
const source = await resolveSource(issues, issueSource, specCwd);
|
|
@@ -8493,13 +9213,14 @@ async function bootPipeline(provider, serverUrl, specCwd, model, source) {
|
|
|
8493
9213
|
for (const line of headerLines) {
|
|
8494
9214
|
console.log(line);
|
|
8495
9215
|
}
|
|
8496
|
-
console.log(
|
|
9216
|
+
console.log(chalk6.dim(" \u2500".repeat(24)));
|
|
8497
9217
|
console.log("");
|
|
8498
9218
|
const specAgent = await boot5({ provider: instance, cwd: specCwd });
|
|
8499
9219
|
return { specAgent, instance };
|
|
8500
9220
|
}
|
|
8501
|
-
async function generateSpecsBatch(validItems, items, specAgent, instance, isTrackerMode, isInlineText, datasource4, fetchOpts, outputDir, specCwd, concurrency, retries) {
|
|
9221
|
+
async function generateSpecsBatch(validItems, items, specAgent, instance, isTrackerMode, isInlineText, datasource4, fetchOpts, outputDir, specCwd, concurrency, retries, specWarnMs, specKillMs, tuiState, tuiUpdate) {
|
|
8502
9222
|
await mkdir5(outputDir, { recursive: true });
|
|
9223
|
+
const quiet = !!tuiState && !log.verbose;
|
|
8503
9224
|
const generatedFiles = [];
|
|
8504
9225
|
const issueNumbers = [];
|
|
8505
9226
|
const dispatchIdentifiers = [];
|
|
@@ -8507,140 +9228,175 @@ async function generateSpecsBatch(validItems, items, specAgent, instance, isTrac
|
|
|
8507
9228
|
const fileDurationsMs = {};
|
|
8508
9229
|
const genQueue = [...validItems];
|
|
8509
9230
|
let modelLoggedInBanner = !!instance.model;
|
|
8510
|
-
|
|
8511
|
-
const
|
|
8512
|
-
|
|
8513
|
-
|
|
8514
|
-
|
|
8515
|
-
|
|
8516
|
-
|
|
8517
|
-
|
|
8518
|
-
|
|
9231
|
+
async function processItem({ id, details }) {
|
|
9232
|
+
const specStart = Date.now();
|
|
9233
|
+
const tuiTask = tuiState?.tasks.find((t) => t.task.file === id);
|
|
9234
|
+
if (tuiTask) {
|
|
9235
|
+
tuiTask.status = "generating";
|
|
9236
|
+
tuiTask.elapsed = specStart;
|
|
9237
|
+
tuiUpdate?.();
|
|
9238
|
+
}
|
|
9239
|
+
if (!details) {
|
|
9240
|
+
log.error(`Skipping item ${id}: missing issue details`);
|
|
9241
|
+
failed++;
|
|
9242
|
+
return;
|
|
9243
|
+
}
|
|
9244
|
+
const itemBody = async () => {
|
|
9245
|
+
let filepath;
|
|
9246
|
+
if (isTrackerMode) {
|
|
9247
|
+
const slug = slugify(details.title, MAX_SLUG_LENGTH);
|
|
9248
|
+
const filename = `${id}-${slug}.md`;
|
|
9249
|
+
filepath = join11(outputDir, filename);
|
|
9250
|
+
} else if (isInlineText) {
|
|
9251
|
+
filepath = id;
|
|
9252
|
+
} else {
|
|
9253
|
+
filepath = id;
|
|
9254
|
+
}
|
|
9255
|
+
fileLoggerStorage.getStore()?.info(`Output path: ${filepath}`);
|
|
9256
|
+
try {
|
|
9257
|
+
fileLoggerStorage.getStore()?.info(`Starting spec generation for ${isTrackerMode ? `#${id}` : filepath}`);
|
|
9258
|
+
if (!quiet) log.info(`Generating spec for ${isTrackerMode ? `#${id}` : filepath}: ${details.title}...`);
|
|
9259
|
+
const generateLabel = `specAgent.generate(${isTrackerMode ? `#${id}` : filepath})`;
|
|
9260
|
+
const result = await withRetry(
|
|
9261
|
+
() => withTimeout(
|
|
9262
|
+
specAgent.generate({
|
|
9263
|
+
issue: isTrackerMode ? details : void 0,
|
|
9264
|
+
filePath: isTrackerMode ? void 0 : id,
|
|
9265
|
+
fileContent: isTrackerMode ? void 0 : details.body,
|
|
9266
|
+
cwd: specCwd,
|
|
9267
|
+
outputPath: filepath,
|
|
9268
|
+
timeboxWarnMs: specWarnMs,
|
|
9269
|
+
timeboxKillMs: specKillMs,
|
|
9270
|
+
onProgress: tuiTask ? (snapshot) => {
|
|
9271
|
+
tuiTask.feedback = snapshot.text;
|
|
9272
|
+
tuiUpdate?.();
|
|
9273
|
+
} : void 0
|
|
9274
|
+
}),
|
|
9275
|
+
specWarnMs + specKillMs,
|
|
9276
|
+
generateLabel
|
|
9277
|
+
),
|
|
9278
|
+
retries,
|
|
9279
|
+
{ label: generateLabel }
|
|
9280
|
+
);
|
|
9281
|
+
if (!result.success) {
|
|
9282
|
+
throw new Error(result.error ?? "Spec generation failed");
|
|
9283
|
+
}
|
|
9284
|
+
fileLoggerStorage.getStore()?.info(`Spec generated successfully`);
|
|
9285
|
+
if (isTrackerMode || isInlineText) {
|
|
9286
|
+
const h1Title = extractTitle(result.data.content, filepath);
|
|
9287
|
+
const h1Slug = slugify(h1Title, MAX_SLUG_LENGTH);
|
|
9288
|
+
const finalFilename = isTrackerMode ? `${id}-${h1Slug}.md` : `${h1Slug}.md`;
|
|
9289
|
+
const finalFilepath = join11(outputDir, finalFilename);
|
|
9290
|
+
if (finalFilepath !== filepath) {
|
|
9291
|
+
await rename2(filepath, finalFilepath);
|
|
9292
|
+
filepath = finalFilepath;
|
|
9293
|
+
}
|
|
9294
|
+
}
|
|
9295
|
+
const specDuration = Date.now() - specStart;
|
|
9296
|
+
fileDurationsMs[filepath] = specDuration;
|
|
9297
|
+
if (!quiet) log.success(`Spec written: ${filepath} (${elapsed(specDuration)})`);
|
|
9298
|
+
if (tuiTask) {
|
|
9299
|
+
tuiTask.status = "done";
|
|
9300
|
+
tuiTask.elapsed = specDuration;
|
|
9301
|
+
tuiTask.feedback = void 0;
|
|
9302
|
+
tuiUpdate?.();
|
|
9303
|
+
}
|
|
9304
|
+
let identifier = filepath;
|
|
9305
|
+
fileLoggerStorage.getStore()?.phase("Datasource sync");
|
|
9306
|
+
if (tuiTask) {
|
|
9307
|
+
tuiTask.status = "syncing";
|
|
9308
|
+
tuiTask.feedback = void 0;
|
|
9309
|
+
tuiUpdate?.();
|
|
8519
9310
|
}
|
|
8520
|
-
|
|
8521
|
-
let filepath;
|
|
9311
|
+
try {
|
|
8522
9312
|
if (isTrackerMode) {
|
|
8523
|
-
|
|
8524
|
-
|
|
8525
|
-
|
|
8526
|
-
|
|
8527
|
-
|
|
8528
|
-
|
|
8529
|
-
|
|
8530
|
-
|
|
8531
|
-
|
|
8532
|
-
|
|
8533
|
-
|
|
8534
|
-
|
|
8535
|
-
|
|
8536
|
-
|
|
8537
|
-
|
|
8538
|
-
|
|
8539
|
-
|
|
8540
|
-
|
|
8541
|
-
|
|
8542
|
-
}),
|
|
8543
|
-
retries,
|
|
8544
|
-
{ label: `specAgent.generate(${isTrackerMode ? `#${id}` : filepath})` }
|
|
8545
|
-
);
|
|
8546
|
-
if (!result.success) {
|
|
8547
|
-
throw new Error(result.error ?? "Spec generation failed");
|
|
8548
|
-
}
|
|
8549
|
-
fileLoggerStorage.getStore()?.info(`Spec generated successfully`);
|
|
8550
|
-
if (isTrackerMode || isInlineText) {
|
|
8551
|
-
const h1Title = extractTitle(result.data.content, filepath);
|
|
8552
|
-
const h1Slug = slugify(h1Title, MAX_SLUG_LENGTH);
|
|
8553
|
-
const finalFilename = isTrackerMode ? `${id}-${h1Slug}.md` : `${h1Slug}.md`;
|
|
8554
|
-
const finalFilepath = join11(outputDir, finalFilename);
|
|
8555
|
-
if (finalFilepath !== filepath) {
|
|
8556
|
-
await rename2(filepath, finalFilepath);
|
|
8557
|
-
filepath = finalFilepath;
|
|
8558
|
-
}
|
|
8559
|
-
}
|
|
8560
|
-
const specDuration = Date.now() - specStart;
|
|
8561
|
-
fileDurationsMs[filepath] = specDuration;
|
|
8562
|
-
log.success(`Spec written: ${filepath} (${elapsed(specDuration)})`);
|
|
8563
|
-
let identifier = filepath;
|
|
8564
|
-
fileLoggerStorage.getStore()?.phase("Datasource sync");
|
|
8565
|
-
try {
|
|
8566
|
-
if (isTrackerMode) {
|
|
8567
|
-
await datasource4.update(id, details.title, result.data.content, fetchOpts);
|
|
8568
|
-
log.success(`Updated issue #${id} with spec content`);
|
|
8569
|
-
await unlink2(filepath);
|
|
8570
|
-
log.success(`Deleted local spec ${filepath} (now tracked as issue #${id})`);
|
|
8571
|
-
identifier = id;
|
|
8572
|
-
issueNumbers.push(id);
|
|
8573
|
-
} else if (datasource4.name === "md") {
|
|
8574
|
-
const parsed = parseIssueFilename(filepath);
|
|
8575
|
-
if (parsed) {
|
|
8576
|
-
await datasource4.update(parsed.issueId, details.title, result.data.content, fetchOpts);
|
|
8577
|
-
log.success(`Updated spec #${parsed.issueId} in-place`);
|
|
8578
|
-
identifier = parsed.issueId;
|
|
8579
|
-
issueNumbers.push(parsed.issueId);
|
|
8580
|
-
} else {
|
|
8581
|
-
const created = await datasource4.create(details.title, result.data.content, fetchOpts);
|
|
8582
|
-
log.success(`Created spec #${created.number} from ${filepath}`);
|
|
8583
|
-
identifier = created.number;
|
|
8584
|
-
issueNumbers.push(created.number);
|
|
8585
|
-
try {
|
|
8586
|
-
await unlink2(filepath);
|
|
8587
|
-
log.success(`Deleted local spec ${filepath} (now tracked as spec #${created.number})`);
|
|
8588
|
-
} catch (unlinkErr) {
|
|
8589
|
-
log.warn(`Could not delete local spec ${filepath}: ${log.formatErrorChain(unlinkErr)}`);
|
|
8590
|
-
}
|
|
8591
|
-
const oldDuration = fileDurationsMs[filepath];
|
|
8592
|
-
delete fileDurationsMs[filepath];
|
|
8593
|
-
filepath = created.url;
|
|
8594
|
-
fileDurationsMs[filepath] = oldDuration;
|
|
8595
|
-
}
|
|
8596
|
-
} else {
|
|
8597
|
-
const created = await datasource4.create(details.title, result.data.content, fetchOpts);
|
|
8598
|
-
log.success(`Created issue #${created.number} from ${filepath}`);
|
|
9313
|
+
await datasource4.update(id, details.title, result.data.content, fetchOpts);
|
|
9314
|
+
if (!quiet) log.success(`Updated issue #${id} with spec content`);
|
|
9315
|
+
await unlink2(filepath);
|
|
9316
|
+
if (!quiet) log.success(`Deleted local spec ${filepath} (now tracked as issue #${id})`);
|
|
9317
|
+
identifier = id;
|
|
9318
|
+
issueNumbers.push(id);
|
|
9319
|
+
} else if (datasource4.name === "md") {
|
|
9320
|
+
const parsed = parseIssueFilename(filepath);
|
|
9321
|
+
if (parsed) {
|
|
9322
|
+
await datasource4.update(parsed.issueId, details.title, result.data.content, fetchOpts);
|
|
9323
|
+
if (!quiet) log.success(`Updated spec #${parsed.issueId} in-place`);
|
|
9324
|
+
identifier = parsed.issueId;
|
|
9325
|
+
issueNumbers.push(parsed.issueId);
|
|
9326
|
+
} else {
|
|
9327
|
+
const created = await datasource4.create(details.title, result.data.content, fetchOpts);
|
|
9328
|
+
if (!quiet) log.success(`Created spec #${created.number} from ${filepath}`);
|
|
9329
|
+
identifier = created.number;
|
|
9330
|
+
issueNumbers.push(created.number);
|
|
9331
|
+
try {
|
|
8599
9332
|
await unlink2(filepath);
|
|
8600
|
-
log.success(`Deleted local spec ${filepath} (now tracked as
|
|
8601
|
-
|
|
8602
|
-
|
|
9333
|
+
if (!quiet) log.success(`Deleted local spec ${filepath} (now tracked as spec #${created.number})`);
|
|
9334
|
+
} catch (unlinkErr) {
|
|
9335
|
+
log.warn(`Could not delete local spec ${filepath}: ${log.formatErrorChain(unlinkErr)}`);
|
|
8603
9336
|
}
|
|
8604
|
-
|
|
8605
|
-
|
|
8606
|
-
|
|
9337
|
+
const oldDuration = fileDurationsMs[filepath];
|
|
9338
|
+
delete fileDurationsMs[filepath];
|
|
9339
|
+
filepath = created.url;
|
|
9340
|
+
fileDurationsMs[filepath] = oldDuration;
|
|
8607
9341
|
}
|
|
8608
|
-
|
|
8609
|
-
|
|
8610
|
-
|
|
8611
|
-
|
|
8612
|
-
log.
|
|
8613
|
-
|
|
8614
|
-
|
|
9342
|
+
} else {
|
|
9343
|
+
const created = await datasource4.create(details.title, result.data.content, fetchOpts);
|
|
9344
|
+
if (!quiet) log.success(`Created issue #${created.number} from ${filepath}`);
|
|
9345
|
+
await unlink2(filepath);
|
|
9346
|
+
if (!quiet) log.success(`Deleted local spec ${filepath} (now tracked as issue #${created.number})`);
|
|
9347
|
+
identifier = created.number;
|
|
9348
|
+
issueNumbers.push(created.number);
|
|
8615
9349
|
}
|
|
8616
|
-
}
|
|
8617
|
-
|
|
8618
|
-
|
|
8619
|
-
return fileLoggerStorage.run(fileLogger, async () => {
|
|
8620
|
-
try {
|
|
8621
|
-
fileLogger.phase(`Spec generation: ${id}`);
|
|
8622
|
-
return await itemBody();
|
|
8623
|
-
} finally {
|
|
8624
|
-
fileLogger.close();
|
|
8625
|
-
}
|
|
8626
|
-
});
|
|
9350
|
+
} catch (err) {
|
|
9351
|
+
const label = isTrackerMode ? `issue #${id}` : filepath;
|
|
9352
|
+
log.warn(`Could not sync ${label} to datasource: ${log.formatErrorChain(err)}`);
|
|
8627
9353
|
}
|
|
8628
|
-
return
|
|
8629
|
-
})
|
|
8630
|
-
|
|
8631
|
-
|
|
8632
|
-
|
|
8633
|
-
|
|
8634
|
-
|
|
8635
|
-
|
|
8636
|
-
|
|
9354
|
+
return { filepath, identifier };
|
|
9355
|
+
} catch (err) {
|
|
9356
|
+
fileLoggerStorage.getStore()?.error(`Spec generation failed for ${id}: ${log.extractMessage(err)}${err instanceof Error && err.stack ? `
|
|
9357
|
+
${err.stack}` : ""}`);
|
|
9358
|
+
log.error(`Failed to generate spec for ${isTrackerMode ? `#${id}` : filepath}: ${log.formatErrorChain(err)}`);
|
|
9359
|
+
log.debug(log.formatErrorChain(err));
|
|
9360
|
+
if (tuiTask) {
|
|
9361
|
+
tuiTask.status = "failed";
|
|
9362
|
+
tuiTask.elapsed = Date.now() - specStart;
|
|
9363
|
+
tuiTask.error = log.extractMessage(err);
|
|
9364
|
+
tuiTask.feedback = void 0;
|
|
9365
|
+
tuiUpdate?.();
|
|
9366
|
+
}
|
|
9367
|
+
return null;
|
|
8637
9368
|
}
|
|
9369
|
+
};
|
|
9370
|
+
let itemResult;
|
|
9371
|
+
const fileLogger = log.verbose ? new FileLogger(id, specCwd) : null;
|
|
9372
|
+
if (fileLogger) {
|
|
9373
|
+
itemResult = await fileLoggerStorage.run(fileLogger, async () => {
|
|
9374
|
+
try {
|
|
9375
|
+
fileLogger.phase(`Spec generation: ${id}`);
|
|
9376
|
+
return await itemBody();
|
|
9377
|
+
} finally {
|
|
9378
|
+
fileLogger.close();
|
|
9379
|
+
}
|
|
9380
|
+
});
|
|
9381
|
+
} else {
|
|
9382
|
+
itemResult = await itemBody();
|
|
9383
|
+
}
|
|
9384
|
+
if (itemResult !== null) {
|
|
9385
|
+
generatedFiles.push(itemResult.filepath);
|
|
9386
|
+
dispatchIdentifiers.push(itemResult.identifier);
|
|
9387
|
+
} else {
|
|
9388
|
+
failed++;
|
|
8638
9389
|
}
|
|
8639
9390
|
if (!modelLoggedInBanner && instance.model) {
|
|
8640
|
-
log.info(`Detected model: ${instance.model}`);
|
|
9391
|
+
if (!quiet) log.info(`Detected model: ${instance.model}`);
|
|
8641
9392
|
modelLoggedInBanner = true;
|
|
8642
9393
|
}
|
|
8643
9394
|
}
|
|
9395
|
+
await runWithConcurrency({
|
|
9396
|
+
items: genQueue,
|
|
9397
|
+
concurrency,
|
|
9398
|
+
worker: async (item) => processItem(item)
|
|
9399
|
+
});
|
|
8644
9400
|
return { generatedFiles, issueNumbers, dispatchIdentifiers, failed, fileDurationsMs };
|
|
8645
9401
|
}
|
|
8646
9402
|
async function cleanupPipeline(specAgent, instance) {
|
|
@@ -8687,14 +9443,18 @@ async function runSpecPipeline(opts) {
|
|
|
8687
9443
|
area,
|
|
8688
9444
|
concurrency = defaultConcurrency(),
|
|
8689
9445
|
dryRun,
|
|
8690
|
-
retries =
|
|
9446
|
+
retries = DEFAULT_RETRY_COUNT
|
|
8691
9447
|
} = opts;
|
|
8692
9448
|
const pipelineStart = Date.now();
|
|
9449
|
+
const specWarnMs = (opts.specWarnTimeout ?? DEFAULT_SPEC_WARN_MIN) * 6e4;
|
|
9450
|
+
const specKillMs = (opts.specKillTimeout ?? DEFAULT_SPEC_KILL_MIN) * 6e4;
|
|
9451
|
+
log.debug(`Spec timebox: warn=${opts.specWarnTimeout ?? DEFAULT_SPEC_WARN_MIN}m, kill=${opts.specKillTimeout ?? DEFAULT_SPEC_KILL_MIN}m, total=${specWarnMs + specKillMs}ms`);
|
|
8693
9452
|
const resolved = await resolveDatasource(issues, opts.issueSource, specCwd, org, project, workItemType, iteration, area);
|
|
8694
9453
|
if (!resolved) {
|
|
8695
9454
|
return { total: 0, generated: 0, failed: 0, files: [], issueNumbers: [], durationMs: Date.now() - pipelineStart, fileDurationsMs: {} };
|
|
8696
9455
|
}
|
|
8697
9456
|
const { source, datasource: datasource4, fetchOpts } = resolved;
|
|
9457
|
+
await ensureAuthReady(source, specCwd, org);
|
|
8698
9458
|
const isTrackerMode = isIssueNumbers(issues);
|
|
8699
9459
|
const isInlineText = !isTrackerMode && !isGlobOrFilePath(issues);
|
|
8700
9460
|
let items;
|
|
@@ -8724,33 +9484,85 @@ async function runSpecPipeline(opts) {
|
|
|
8724
9484
|
return { total: 0, generated: 0, failed: 0, files: [], issueNumbers: [], durationMs: Date.now() - pipelineStart, fileDurationsMs: {} };
|
|
8725
9485
|
}
|
|
8726
9486
|
const { specAgent, instance } = await bootPipeline(provider, serverUrl, specCwd, model, source);
|
|
8727
|
-
const
|
|
8728
|
-
|
|
8729
|
-
|
|
8730
|
-
|
|
8731
|
-
|
|
8732
|
-
|
|
8733
|
-
|
|
8734
|
-
|
|
8735
|
-
|
|
8736
|
-
|
|
8737
|
-
|
|
8738
|
-
|
|
8739
|
-
|
|
8740
|
-
|
|
8741
|
-
|
|
8742
|
-
|
|
8743
|
-
|
|
8744
|
-
|
|
8745
|
-
|
|
8746
|
-
|
|
8747
|
-
|
|
8748
|
-
|
|
8749
|
-
|
|
8750
|
-
|
|
8751
|
-
|
|
8752
|
-
|
|
8753
|
-
|
|
9487
|
+
const verbose = log.verbose;
|
|
9488
|
+
let tui;
|
|
9489
|
+
if (verbose) {
|
|
9490
|
+
const state = {
|
|
9491
|
+
tasks: [],
|
|
9492
|
+
phase: "booting",
|
|
9493
|
+
mode: "spec",
|
|
9494
|
+
startTime: Date.now(),
|
|
9495
|
+
filesFound: 0,
|
|
9496
|
+
provider,
|
|
9497
|
+
model: instance.model,
|
|
9498
|
+
source
|
|
9499
|
+
};
|
|
9500
|
+
tui = {
|
|
9501
|
+
state,
|
|
9502
|
+
update: () => {
|
|
9503
|
+
},
|
|
9504
|
+
stop: () => {
|
|
9505
|
+
},
|
|
9506
|
+
waitForRecoveryAction: async () => "quit"
|
|
9507
|
+
};
|
|
9508
|
+
} else {
|
|
9509
|
+
tui = createTui();
|
|
9510
|
+
tui.state.mode = "spec";
|
|
9511
|
+
tui.state.provider = provider;
|
|
9512
|
+
tui.state.model = instance.model;
|
|
9513
|
+
tui.state.source = source;
|
|
9514
|
+
}
|
|
9515
|
+
tui.state.tasks = validItems.map((item, index) => ({
|
|
9516
|
+
task: {
|
|
9517
|
+
index,
|
|
9518
|
+
text: item.details?.title ?? `Item ${item.id}`,
|
|
9519
|
+
line: 0,
|
|
9520
|
+
raw: item.details?.title ?? item.id,
|
|
9521
|
+
file: item.id
|
|
9522
|
+
},
|
|
9523
|
+
status: "pending"
|
|
9524
|
+
}));
|
|
9525
|
+
tui.state.phase = "dispatching";
|
|
9526
|
+
tui.update();
|
|
9527
|
+
try {
|
|
9528
|
+
const results = await generateSpecsBatch(
|
|
9529
|
+
validItems,
|
|
9530
|
+
items,
|
|
9531
|
+
specAgent,
|
|
9532
|
+
instance,
|
|
9533
|
+
isTrackerMode,
|
|
9534
|
+
isInlineText,
|
|
9535
|
+
datasource4,
|
|
9536
|
+
fetchOpts,
|
|
9537
|
+
outputDir,
|
|
9538
|
+
specCwd,
|
|
9539
|
+
concurrency,
|
|
9540
|
+
retries,
|
|
9541
|
+
specWarnMs,
|
|
9542
|
+
specKillMs,
|
|
9543
|
+
tui.state,
|
|
9544
|
+
tui.update
|
|
9545
|
+
);
|
|
9546
|
+
await cleanupPipeline(specAgent, instance);
|
|
9547
|
+
tui.state.phase = "done";
|
|
9548
|
+
tui.stop();
|
|
9549
|
+
const totalDuration = Date.now() - pipelineStart;
|
|
9550
|
+
logSummary(results.generatedFiles, results.dispatchIdentifiers, results.failed, totalDuration);
|
|
9551
|
+
return {
|
|
9552
|
+
total: items.length,
|
|
9553
|
+
generated: results.generatedFiles.length,
|
|
9554
|
+
failed: results.failed,
|
|
9555
|
+
files: results.generatedFiles,
|
|
9556
|
+
issueNumbers: results.issueNumbers,
|
|
9557
|
+
identifiers: results.dispatchIdentifiers,
|
|
9558
|
+
durationMs: totalDuration,
|
|
9559
|
+
fileDurationsMs: results.fileDurationsMs
|
|
9560
|
+
};
|
|
9561
|
+
} catch (err) {
|
|
9562
|
+
tui.state.phase = "done";
|
|
9563
|
+
tui.stop();
|
|
9564
|
+
throw err;
|
|
9565
|
+
}
|
|
8754
9566
|
}
|
|
8755
9567
|
|
|
8756
9568
|
// src/orchestrator/dispatch-pipeline.ts
|
|
@@ -9296,268 +10108,6 @@ function formatOutputFile(parsed) {
|
|
|
9296
10108
|
// src/orchestrator/dispatch-pipeline.ts
|
|
9297
10109
|
init_logger();
|
|
9298
10110
|
init_cleanup();
|
|
9299
|
-
|
|
9300
|
-
// src/tui.ts
|
|
9301
|
-
import chalk6 from "chalk";
|
|
9302
|
-
var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
9303
|
-
var BAR_WIDTH = 30;
|
|
9304
|
-
var spinnerIndex = 0;
|
|
9305
|
-
var interval = null;
|
|
9306
|
-
var lastLineCount = 0;
|
|
9307
|
-
function spinner() {
|
|
9308
|
-
return chalk6.cyan(SPINNER_FRAMES[spinnerIndex % SPINNER_FRAMES.length]);
|
|
9309
|
-
}
|
|
9310
|
-
function progressBar(done, total) {
|
|
9311
|
-
if (total === 0) return chalk6.dim("\u2591".repeat(BAR_WIDTH));
|
|
9312
|
-
const filled = Math.round(done / total * BAR_WIDTH);
|
|
9313
|
-
const empty = BAR_WIDTH - filled;
|
|
9314
|
-
const pct = Math.round(done / total * 100);
|
|
9315
|
-
return chalk6.green("\u2588".repeat(filled)) + chalk6.dim("\u2591".repeat(empty)) + chalk6.white(` ${pct}%`);
|
|
9316
|
-
}
|
|
9317
|
-
function statusIcon(status) {
|
|
9318
|
-
switch (status) {
|
|
9319
|
-
case "pending":
|
|
9320
|
-
return chalk6.dim("\u25CB");
|
|
9321
|
-
case "planning":
|
|
9322
|
-
return spinner();
|
|
9323
|
-
case "running":
|
|
9324
|
-
return spinner();
|
|
9325
|
-
case "done":
|
|
9326
|
-
return chalk6.green("\u25CF");
|
|
9327
|
-
case "failed":
|
|
9328
|
-
return chalk6.red("\u2716");
|
|
9329
|
-
}
|
|
9330
|
-
}
|
|
9331
|
-
function statusLabel(status) {
|
|
9332
|
-
switch (status) {
|
|
9333
|
-
case "pending":
|
|
9334
|
-
return chalk6.dim("pending");
|
|
9335
|
-
case "planning":
|
|
9336
|
-
return chalk6.magenta("planning");
|
|
9337
|
-
case "running":
|
|
9338
|
-
return chalk6.cyan("executing");
|
|
9339
|
-
case "done":
|
|
9340
|
-
return chalk6.green("done");
|
|
9341
|
-
case "failed":
|
|
9342
|
-
return chalk6.red("failed");
|
|
9343
|
-
}
|
|
9344
|
-
}
|
|
9345
|
-
function phaseLabel(phase, provider) {
|
|
9346
|
-
switch (phase) {
|
|
9347
|
-
case "discovering":
|
|
9348
|
-
return `${spinner()} Discovering task files...`;
|
|
9349
|
-
case "parsing":
|
|
9350
|
-
return `${spinner()} Parsing tasks...`;
|
|
9351
|
-
case "booting": {
|
|
9352
|
-
const name = provider ?? "provider";
|
|
9353
|
-
return `${spinner()} Connecting to ${name}...`;
|
|
9354
|
-
}
|
|
9355
|
-
case "dispatching":
|
|
9356
|
-
return `${spinner()} Dispatching tasks...`;
|
|
9357
|
-
case "done":
|
|
9358
|
-
return chalk6.green("\u2714") + " Complete";
|
|
9359
|
-
}
|
|
9360
|
-
}
|
|
9361
|
-
function countVisualRows(text, cols) {
|
|
9362
|
-
const stripped = text.replace(/\x1B\[[0-9;]*m/g, "");
|
|
9363
|
-
const safeCols = Math.max(1, cols);
|
|
9364
|
-
return stripped.split("\n").reduce((sum, line) => {
|
|
9365
|
-
return sum + Math.max(1, Math.ceil(line.length / safeCols));
|
|
9366
|
-
}, 0);
|
|
9367
|
-
}
|
|
9368
|
-
function render(state) {
|
|
9369
|
-
const lines = [];
|
|
9370
|
-
const now = Date.now();
|
|
9371
|
-
const totalElapsed = elapsed(now - state.startTime);
|
|
9372
|
-
const done = state.tasks.filter((t) => t.status === "done").length;
|
|
9373
|
-
const failed = state.tasks.filter((t) => t.status === "failed").length;
|
|
9374
|
-
const total = state.tasks.length;
|
|
9375
|
-
lines.push("");
|
|
9376
|
-
lines.push(
|
|
9377
|
-
...renderHeaderLines({
|
|
9378
|
-
provider: state.provider,
|
|
9379
|
-
model: state.model,
|
|
9380
|
-
source: state.source
|
|
9381
|
-
})
|
|
9382
|
-
);
|
|
9383
|
-
if (state.currentIssue) {
|
|
9384
|
-
lines.push(
|
|
9385
|
-
chalk6.dim(` issue: `) + chalk6.white(`#${state.currentIssue.number}`) + chalk6.dim(` \u2014 ${state.currentIssue.title}`)
|
|
9386
|
-
);
|
|
9387
|
-
}
|
|
9388
|
-
lines.push(chalk6.dim(" \u2500".repeat(24)));
|
|
9389
|
-
if (state.notification) {
|
|
9390
|
-
lines.push("");
|
|
9391
|
-
for (const notifLine of state.notification.split("\n")) {
|
|
9392
|
-
lines.push(" " + chalk6.yellowBright("\u26A0 ") + chalk6.yellow(notifLine));
|
|
9393
|
-
}
|
|
9394
|
-
}
|
|
9395
|
-
lines.push(` ${phaseLabel(state.phase, state.provider)}` + chalk6.dim(` ${totalElapsed}`));
|
|
9396
|
-
if (state.phase === "dispatching" || state.phase === "done") {
|
|
9397
|
-
lines.push("");
|
|
9398
|
-
lines.push(` ${progressBar(done + failed, total)} ${chalk6.dim(`${done + failed}/${total} tasks`)}`);
|
|
9399
|
-
lines.push("");
|
|
9400
|
-
const activeWorktrees = new Set(
|
|
9401
|
-
state.tasks.map((t) => t.worktree).filter(Boolean)
|
|
9402
|
-
);
|
|
9403
|
-
const showWorktree = activeWorktrees.size > 1;
|
|
9404
|
-
const cols = process.stdout.columns || 80;
|
|
9405
|
-
const maxTextLen = cols - 30;
|
|
9406
|
-
const running = state.tasks.filter((t) => t.status === "running" || t.status === "planning");
|
|
9407
|
-
const completed = state.tasks.filter(
|
|
9408
|
-
(t) => t.status === "done" || t.status === "failed"
|
|
9409
|
-
);
|
|
9410
|
-
const pending = state.tasks.filter((t) => t.status === "pending");
|
|
9411
|
-
if (showWorktree) {
|
|
9412
|
-
const groups = /* @__PURE__ */ new Map();
|
|
9413
|
-
const ungrouped = [];
|
|
9414
|
-
for (const ts of state.tasks) {
|
|
9415
|
-
if (ts.worktree) {
|
|
9416
|
-
const arr = groups.get(ts.worktree) ?? [];
|
|
9417
|
-
arr.push(ts);
|
|
9418
|
-
groups.set(ts.worktree, arr);
|
|
9419
|
-
} else {
|
|
9420
|
-
ungrouped.push(ts);
|
|
9421
|
-
}
|
|
9422
|
-
}
|
|
9423
|
-
const doneGroups = [];
|
|
9424
|
-
const activeGroups = [];
|
|
9425
|
-
for (const [wt, tasks] of groups) {
|
|
9426
|
-
const allDone = tasks.every((t) => t.status === "done" || t.status === "failed");
|
|
9427
|
-
if (allDone) {
|
|
9428
|
-
doneGroups.push([wt, tasks]);
|
|
9429
|
-
} else {
|
|
9430
|
-
activeGroups.push([wt, tasks]);
|
|
9431
|
-
}
|
|
9432
|
-
}
|
|
9433
|
-
if (doneGroups.length > 3) {
|
|
9434
|
-
lines.push(chalk6.dim(` \xB7\xB7\xB7 ${doneGroups.length - 3} earlier issue(s) completed`));
|
|
9435
|
-
}
|
|
9436
|
-
for (const [wt, tasks] of doneGroups.slice(-3)) {
|
|
9437
|
-
const issueNum = wt.match(/^(\d+)/)?.[1] ?? wt.slice(0, 12);
|
|
9438
|
-
const anyFailed = tasks.some((t) => t.status === "failed");
|
|
9439
|
-
const icon = anyFailed ? chalk6.red("\u2716") : chalk6.green("\u25CF");
|
|
9440
|
-
const doneCount = tasks.filter((t) => t.status === "done").length;
|
|
9441
|
-
const maxElapsed = Math.max(...tasks.map((t) => t.elapsed ?? 0));
|
|
9442
|
-
lines.push(` ${icon} ${chalk6.dim(`#${issueNum}`)} ${chalk6.dim(`${doneCount}/${tasks.length} tasks`)} ${chalk6.dim(elapsed(maxElapsed))}`);
|
|
9443
|
-
}
|
|
9444
|
-
for (const [wt, tasks] of activeGroups) {
|
|
9445
|
-
const issueNum = wt.match(/^(\d+)/)?.[1] ?? wt.slice(0, 12);
|
|
9446
|
-
const activeTasks = tasks.filter((t) => t.status === "running" || t.status === "planning");
|
|
9447
|
-
const activeCount = activeTasks.length;
|
|
9448
|
-
const firstActive = activeTasks[0];
|
|
9449
|
-
const truncLen = Math.min(cols - 26, 60);
|
|
9450
|
-
let text = firstActive?.task.text ?? "";
|
|
9451
|
-
if (text.length > truncLen) {
|
|
9452
|
-
text = text.slice(0, truncLen - 1) + "\u2026";
|
|
9453
|
-
}
|
|
9454
|
-
const earliest = Math.min(...activeTasks.map((t) => t.elapsed ?? now));
|
|
9455
|
-
const elapsedStr = elapsed(now - earliest);
|
|
9456
|
-
lines.push(` ${spinner()} ${chalk6.white(`#${issueNum}`)} ${activeCount} active ${text} ${chalk6.dim(elapsedStr)}`);
|
|
9457
|
-
}
|
|
9458
|
-
for (const ts of ungrouped) {
|
|
9459
|
-
if (ts.status !== "running" && ts.status !== "planning") continue;
|
|
9460
|
-
const icon = statusIcon(ts.status);
|
|
9461
|
-
const idx = chalk6.dim(`#${state.tasks.indexOf(ts) + 1}`);
|
|
9462
|
-
let text = ts.task.text;
|
|
9463
|
-
if (text.length > maxTextLen) {
|
|
9464
|
-
text = text.slice(0, maxTextLen - 1) + "\u2026";
|
|
9465
|
-
}
|
|
9466
|
-
const elapsedStr = chalk6.dim(` ${elapsed(now - (ts.elapsed || now))}`);
|
|
9467
|
-
const label = statusLabel(ts.status);
|
|
9468
|
-
lines.push(` ${icon} ${idx} ${text} ${label}${elapsedStr}`);
|
|
9469
|
-
if (ts.error) {
|
|
9470
|
-
lines.push(chalk6.red(` \u2514\u2500 ${ts.error}`));
|
|
9471
|
-
}
|
|
9472
|
-
}
|
|
9473
|
-
} else {
|
|
9474
|
-
const visibleRunning = running.slice(0, 8);
|
|
9475
|
-
const visible = [
|
|
9476
|
-
...completed.slice(-3),
|
|
9477
|
-
...visibleRunning,
|
|
9478
|
-
...pending.slice(0, 3)
|
|
9479
|
-
];
|
|
9480
|
-
if (completed.length > 3) {
|
|
9481
|
-
lines.push(chalk6.dim(` \xB7\xB7\xB7 ${completed.length - 3} earlier task(s) completed`));
|
|
9482
|
-
}
|
|
9483
|
-
for (const ts of visible) {
|
|
9484
|
-
const icon = statusIcon(ts.status);
|
|
9485
|
-
const idx = chalk6.dim(`#${state.tasks.indexOf(ts) + 1}`);
|
|
9486
|
-
let text = ts.task.text;
|
|
9487
|
-
if (text.length > maxTextLen) {
|
|
9488
|
-
text = text.slice(0, maxTextLen - 1) + "\u2026";
|
|
9489
|
-
}
|
|
9490
|
-
const elapsedStr = ts.status === "running" || ts.status === "planning" ? chalk6.dim(` ${elapsed(now - (ts.elapsed || now))}`) : ts.status === "done" && ts.elapsed ? chalk6.dim(` ${elapsed(ts.elapsed)}`) : "";
|
|
9491
|
-
const label = statusLabel(ts.status);
|
|
9492
|
-
lines.push(` ${icon} ${idx} ${text} ${label}${elapsedStr}`);
|
|
9493
|
-
if (ts.error) {
|
|
9494
|
-
lines.push(chalk6.red(` \u2514\u2500 ${ts.error}`));
|
|
9495
|
-
}
|
|
9496
|
-
}
|
|
9497
|
-
if (running.length > 8) {
|
|
9498
|
-
lines.push(chalk6.dim(` \xB7\xB7\xB7 ${running.length - 8} more running`));
|
|
9499
|
-
}
|
|
9500
|
-
if (pending.length > 3) {
|
|
9501
|
-
lines.push(chalk6.dim(` \xB7\xB7\xB7 ${pending.length - 3} more task(s) pending`));
|
|
9502
|
-
}
|
|
9503
|
-
}
|
|
9504
|
-
lines.push("");
|
|
9505
|
-
const parts = [];
|
|
9506
|
-
if (done > 0) parts.push(chalk6.green(`${done} passed`));
|
|
9507
|
-
if (failed > 0) parts.push(chalk6.red(`${failed} failed`));
|
|
9508
|
-
if (total - done - failed > 0)
|
|
9509
|
-
parts.push(chalk6.dim(`${total - done - failed} remaining`));
|
|
9510
|
-
lines.push(` ${parts.join(chalk6.dim(" \xB7 "))}`);
|
|
9511
|
-
} else if (state.filesFound > 0) {
|
|
9512
|
-
lines.push(chalk6.dim(` Found ${state.filesFound} file(s)`));
|
|
9513
|
-
}
|
|
9514
|
-
lines.push("");
|
|
9515
|
-
return lines.join("\n");
|
|
9516
|
-
}
|
|
9517
|
-
function draw(state) {
|
|
9518
|
-
const output = render(state);
|
|
9519
|
-
const cols = process.stdout.columns || 80;
|
|
9520
|
-
const newLineCount = countVisualRows(output, cols);
|
|
9521
|
-
let buffer = "";
|
|
9522
|
-
if (lastLineCount > 0) {
|
|
9523
|
-
buffer += `\x1B[${lastLineCount}A`;
|
|
9524
|
-
}
|
|
9525
|
-
const lines = output.split("\n");
|
|
9526
|
-
buffer += lines.map((line) => line + "\x1B[K").join("\n");
|
|
9527
|
-
const leftover = lastLineCount - newLineCount;
|
|
9528
|
-
if (leftover > 0) {
|
|
9529
|
-
for (let i = 0; i < leftover; i++) {
|
|
9530
|
-
buffer += "\n\x1B[K";
|
|
9531
|
-
}
|
|
9532
|
-
buffer += `\x1B[${leftover}A`;
|
|
9533
|
-
}
|
|
9534
|
-
process.stdout.write(buffer);
|
|
9535
|
-
lastLineCount = newLineCount;
|
|
9536
|
-
}
|
|
9537
|
-
function createTui() {
|
|
9538
|
-
const state = {
|
|
9539
|
-
tasks: [],
|
|
9540
|
-
phase: "discovering",
|
|
9541
|
-
startTime: Date.now(),
|
|
9542
|
-
filesFound: 0
|
|
9543
|
-
};
|
|
9544
|
-
interval = setInterval(() => {
|
|
9545
|
-
spinnerIndex++;
|
|
9546
|
-
draw(state);
|
|
9547
|
-
}, 80);
|
|
9548
|
-
const update = () => draw(state);
|
|
9549
|
-
const stop = () => {
|
|
9550
|
-
if (interval) {
|
|
9551
|
-
clearInterval(interval);
|
|
9552
|
-
interval = null;
|
|
9553
|
-
}
|
|
9554
|
-
draw(state);
|
|
9555
|
-
};
|
|
9556
|
-
draw(state);
|
|
9557
|
-
return { state, update, stop };
|
|
9558
|
-
}
|
|
9559
|
-
|
|
9560
|
-
// src/orchestrator/dispatch-pipeline.ts
|
|
9561
10111
|
init_providers();
|
|
9562
10112
|
init_timeout();
|
|
9563
10113
|
import chalk7 from "chalk";
|
|
@@ -9591,12 +10141,10 @@ async function resolveGlobItems(patterns, cwd) {
|
|
|
9591
10141
|
}
|
|
9592
10142
|
return items;
|
|
9593
10143
|
}
|
|
9594
|
-
var DEFAULT_PLAN_TIMEOUT_MIN = 10;
|
|
9595
|
-
var DEFAULT_PLAN_RETRIES = 1;
|
|
9596
10144
|
async function runDispatchPipeline(opts, cwd) {
|
|
9597
10145
|
const {
|
|
9598
10146
|
issueIds,
|
|
9599
|
-
concurrency,
|
|
10147
|
+
concurrency = 1,
|
|
9600
10148
|
dryRun,
|
|
9601
10149
|
serverUrl,
|
|
9602
10150
|
noPlan,
|
|
@@ -9613,36 +10161,22 @@ async function runDispatchPipeline(opts, cwd) {
|
|
|
9613
10161
|
area,
|
|
9614
10162
|
planTimeout,
|
|
9615
10163
|
planRetries,
|
|
9616
|
-
retries
|
|
10164
|
+
retries,
|
|
10165
|
+
username: usernameOverride
|
|
9617
10166
|
} = opts;
|
|
9618
10167
|
let noBranch = noBranchOpt;
|
|
9619
|
-
const
|
|
9620
|
-
const
|
|
9621
|
-
|
|
10168
|
+
const resolvedRetries = retries ?? DEFAULT_RETRY_COUNT;
|
|
10169
|
+
const resolvedPlanTimeoutMin = planTimeout ?? DEFAULT_PLAN_TIMEOUT_MIN;
|
|
10170
|
+
const planTimeoutMs = resolvedPlanTimeoutMin * 6e4;
|
|
10171
|
+
const resolvedPlanRetries = planRetries ?? resolvedRetries;
|
|
10172
|
+
const maxPlanAttempts = resolvedPlanRetries + 1;
|
|
10173
|
+
log.debug(`Plan timeout: ${resolvedPlanTimeoutMin}m (${planTimeoutMs}ms), max attempts: ${maxPlanAttempts}`);
|
|
9622
10174
|
if (dryRun) {
|
|
9623
|
-
return dryRunMode(issueIds, cwd, source, org, project, workItemType, iteration, area);
|
|
9624
|
-
}
|
|
9625
|
-
if (source === "github") {
|
|
9626
|
-
const remoteUrl = await getGitRemoteUrl(cwd);
|
|
9627
|
-
if (remoteUrl && parseGitHubRemoteUrl(remoteUrl)) {
|
|
9628
|
-
await getGithubOctokit();
|
|
9629
|
-
} else if (!remoteUrl) {
|
|
9630
|
-
log.warn("No git remote found \u2014 skipping GitHub pre-authentication");
|
|
9631
|
-
} else {
|
|
9632
|
-
log.warn("Remote URL is not a GitHub repository \u2014 skipping GitHub pre-authentication");
|
|
9633
|
-
}
|
|
9634
|
-
} else if (source === "azdevops") {
|
|
9635
|
-
let orgUrl = org;
|
|
9636
|
-
if (!orgUrl) {
|
|
9637
|
-
const remoteUrl = await getGitRemoteUrl(cwd);
|
|
9638
|
-
if (remoteUrl) {
|
|
9639
|
-
const parsed = parseAzDevOpsRemoteUrl(remoteUrl);
|
|
9640
|
-
if (parsed) orgUrl = parsed.orgUrl;
|
|
9641
|
-
}
|
|
9642
|
-
}
|
|
9643
|
-
if (orgUrl) await getAzureConnection(orgUrl);
|
|
10175
|
+
return dryRunMode(issueIds, cwd, source, org, project, workItemType, iteration, area, usernameOverride);
|
|
9644
10176
|
}
|
|
10177
|
+
await ensureAuthReady(source, cwd, org);
|
|
9645
10178
|
const verbose = log.verbose;
|
|
10179
|
+
const canRecoverInteractively = !verbose && process.stdin.isTTY === true && process.stdout.isTTY === true;
|
|
9646
10180
|
let tui;
|
|
9647
10181
|
if (verbose) {
|
|
9648
10182
|
const headerLines = renderHeaderLines({ provider, source });
|
|
@@ -9659,9 +10193,14 @@ async function runDispatchPipeline(opts, cwd) {
|
|
|
9659
10193
|
provider,
|
|
9660
10194
|
source
|
|
9661
10195
|
};
|
|
9662
|
-
tui = {
|
|
9663
|
-
|
|
9664
|
-
|
|
10196
|
+
tui = {
|
|
10197
|
+
state,
|
|
10198
|
+
update: () => {
|
|
10199
|
+
},
|
|
10200
|
+
stop: () => {
|
|
10201
|
+
},
|
|
10202
|
+
waitForRecoveryAction: async () => "quit"
|
|
10203
|
+
};
|
|
9665
10204
|
} else {
|
|
9666
10205
|
tui = createTui();
|
|
9667
10206
|
tui.state.provider = provider;
|
|
@@ -9702,7 +10241,6 @@ async function runDispatchPipeline(opts, cwd) {
|
|
|
9702
10241
|
setAuthPromptHandler(null);
|
|
9703
10242
|
if (items.length === 0) {
|
|
9704
10243
|
tui.state.phase = "done";
|
|
9705
|
-
setAuthPromptHandler(null);
|
|
9706
10244
|
tui.stop();
|
|
9707
10245
|
const label = issueIds.length > 0 ? `issue(s) ${issueIds.join(", ")}` : `datasource: ${source}`;
|
|
9708
10246
|
log.warn("No work items found from " + label);
|
|
@@ -9727,7 +10265,6 @@ async function runDispatchPipeline(opts, cwd) {
|
|
|
9727
10265
|
}
|
|
9728
10266
|
if (allTasks.length === 0) {
|
|
9729
10267
|
tui.state.phase = "done";
|
|
9730
|
-
setAuthPromptHandler(null);
|
|
9731
10268
|
tui.stop();
|
|
9732
10269
|
log.warn("No unchecked tasks found");
|
|
9733
10270
|
return { total: 0, completed: 0, failed: 0, skipped: 0, results: [] };
|
|
@@ -9767,9 +10304,8 @@ async function runDispatchPipeline(opts, cwd) {
|
|
|
9767
10304
|
tui.state.phase = "dispatching";
|
|
9768
10305
|
if (verbose) log.info(`Dispatching ${allTasks.length} task(s)...`);
|
|
9769
10306
|
const results = [];
|
|
9770
|
-
let
|
|
9771
|
-
|
|
9772
|
-
const lifecycleOpts = { cwd };
|
|
10307
|
+
let halted = false;
|
|
10308
|
+
const lifecycleOpts = { cwd, username: usernameOverride };
|
|
9773
10309
|
const startingBranch = await datasource4.getCurrentBranch(lifecycleOpts);
|
|
9774
10310
|
let featureBranchName;
|
|
9775
10311
|
let featureDefaultBranch;
|
|
@@ -9829,6 +10365,15 @@ async function runDispatchPipeline(opts, cwd) {
|
|
|
9829
10365
|
let branchName;
|
|
9830
10366
|
let worktreePath;
|
|
9831
10367
|
let issueCwd = cwd;
|
|
10368
|
+
let preserveContext = false;
|
|
10369
|
+
const upsertResult = (collection, result) => {
|
|
10370
|
+
const index = collection.findIndex((entry) => entry.task === result.task);
|
|
10371
|
+
if (index >= 0) {
|
|
10372
|
+
collection[index] = result;
|
|
10373
|
+
} else {
|
|
10374
|
+
collection.push(result);
|
|
10375
|
+
}
|
|
10376
|
+
};
|
|
9832
10377
|
if (!noBranch && details) {
|
|
9833
10378
|
fileLogger?.phase("Branch/worktree setup");
|
|
9834
10379
|
try {
|
|
@@ -9863,14 +10408,13 @@ ${err.stack}` : ""}`);
|
|
|
9863
10408
|
tuiTask.status = "failed";
|
|
9864
10409
|
tuiTask.error = errorMsg;
|
|
9865
10410
|
}
|
|
9866
|
-
results
|
|
10411
|
+
upsertResult(results, { task, success: false, error: errorMsg });
|
|
9867
10412
|
}
|
|
9868
|
-
|
|
9869
|
-
return;
|
|
10413
|
+
return { halted: false };
|
|
9870
10414
|
}
|
|
9871
10415
|
}
|
|
9872
10416
|
const worktreeRoot = useWorktrees ? worktreePath : void 0;
|
|
9873
|
-
const issueLifecycleOpts = { cwd: issueCwd };
|
|
10417
|
+
const issueLifecycleOpts = { cwd: issueCwd, username: usernameOverride };
|
|
9874
10418
|
fileLogger?.phase("Provider/agent boot");
|
|
9875
10419
|
let localInstance;
|
|
9876
10420
|
let localPlanner;
|
|
@@ -9893,286 +10437,345 @@ ${err.stack}` : ""}`);
|
|
|
9893
10437
|
localExecutor = executor;
|
|
9894
10438
|
localCommitAgent = commitAgent;
|
|
9895
10439
|
}
|
|
9896
|
-
const groups = groupTasksByMode(fileTasks);
|
|
9897
10440
|
const issueResults = [];
|
|
9898
|
-
|
|
9899
|
-
const
|
|
9900
|
-
|
|
9901
|
-
|
|
9902
|
-
|
|
9903
|
-
|
|
9904
|
-
|
|
9905
|
-
|
|
9906
|
-
|
|
9907
|
-
|
|
9908
|
-
|
|
9909
|
-
|
|
9910
|
-
|
|
9911
|
-
|
|
9912
|
-
|
|
9913
|
-
|
|
9914
|
-
|
|
9915
|
-
|
|
9916
|
-
|
|
9917
|
-
|
|
9918
|
-
|
|
9919
|
-
|
|
9920
|
-
|
|
9921
|
-
|
|
9922
|
-
|
|
9923
|
-
|
|
9924
|
-
|
|
9925
|
-
|
|
9926
|
-
|
|
9927
|
-
|
|
9928
|
-
|
|
9929
|
-
|
|
9930
|
-
|
|
9931
|
-
|
|
9932
|
-
|
|
9933
|
-
|
|
9934
|
-
|
|
9935
|
-
|
|
9936
|
-
|
|
9937
|
-
|
|
9938
|
-
|
|
9939
|
-
|
|
9940
|
-
|
|
9941
|
-
|
|
9942
|
-
|
|
9943
|
-
}
|
|
9944
|
-
if (
|
|
9945
|
-
|
|
9946
|
-
|
|
9947
|
-
data: null,
|
|
9948
|
-
success: false,
|
|
9949
|
-
error: `Planning timed out after ${timeoutMin}m (${maxPlanAttempts} attempts)`,
|
|
9950
|
-
durationMs: 0
|
|
9951
|
-
};
|
|
9952
|
-
}
|
|
9953
|
-
if (!planResult.success) {
|
|
9954
|
-
tuiTask.status = "failed";
|
|
9955
|
-
tuiTask.error = `Planning failed: ${planResult.error}`;
|
|
9956
|
-
fileLogger?.error(`Planning failed: ${planResult.error}`);
|
|
9957
|
-
tuiTask.elapsed = Date.now() - startTime;
|
|
9958
|
-
if (verbose) log.error(`Task #${tui.state.tasks.indexOf(tuiTask) + 1}: failed \u2014 ${tuiTask.error} (${elapsed(tuiTask.elapsed)})`);
|
|
9959
|
-
failed++;
|
|
9960
|
-
return { task, success: false, error: tuiTask.error };
|
|
9961
|
-
}
|
|
9962
|
-
plan = planResult.data.prompt;
|
|
9963
|
-
fileLogger?.info(`Planning completed (${planResult.durationMs ?? 0}ms)`);
|
|
9964
|
-
}
|
|
9965
|
-
tuiTask.status = "running";
|
|
9966
|
-
fileLogger?.phase(`Executing task: ${task.text}`);
|
|
9967
|
-
if (verbose) log.info(`Task #${tui.state.tasks.indexOf(tuiTask) + 1}: executing \u2014 "${task.text}"`);
|
|
9968
|
-
const execRetries = 2;
|
|
9969
|
-
const execResult = await withRetry(
|
|
9970
|
-
async () => {
|
|
9971
|
-
const result = await localExecutor.execute({
|
|
9972
|
-
task,
|
|
9973
|
-
cwd: issueCwd,
|
|
9974
|
-
plan: plan ?? null,
|
|
9975
|
-
worktreeRoot
|
|
9976
|
-
});
|
|
9977
|
-
if (!result.success) {
|
|
9978
|
-
throw new Error(result.error ?? "Execution failed");
|
|
9979
|
-
}
|
|
9980
|
-
return result;
|
|
9981
|
-
},
|
|
9982
|
-
execRetries,
|
|
9983
|
-
{ label: `executor "${task.text}"` }
|
|
9984
|
-
).catch((err) => ({
|
|
9985
|
-
data: null,
|
|
9986
|
-
success: false,
|
|
9987
|
-
error: log.extractMessage(err),
|
|
9988
|
-
durationMs: 0
|
|
9989
|
-
}));
|
|
9990
|
-
if (execResult.success) {
|
|
9991
|
-
fileLogger?.info(`Execution completed successfully (${Date.now() - startTime}ms)`);
|
|
9992
|
-
try {
|
|
9993
|
-
const parsed = parseIssueFilename(task.file);
|
|
9994
|
-
const updatedContent = await readFile8(task.file, "utf-8");
|
|
9995
|
-
if (parsed) {
|
|
9996
|
-
const issueDetails = issueDetailsByFile.get(task.file);
|
|
9997
|
-
const title = issueDetails?.title ?? parsed.slug;
|
|
9998
|
-
await datasource4.update(parsed.issueId, title, updatedContent, fetchOpts);
|
|
9999
|
-
log.success(`Synced task completion to issue #${parsed.issueId}`);
|
|
10000
|
-
} else {
|
|
10001
|
-
const issueDetails = issueDetailsByFile.get(task.file);
|
|
10002
|
-
if (issueDetails) {
|
|
10003
|
-
await datasource4.update(issueDetails.number, issueDetails.title, updatedContent, fetchOpts);
|
|
10004
|
-
log.success(`Synced task completion to issue #${issueDetails.number}`);
|
|
10005
|
-
}
|
|
10006
|
-
}
|
|
10007
|
-
} catch (err) {
|
|
10008
|
-
log.warn(`Could not sync task completion to datasource: ${log.formatErrorChain(err)}`);
|
|
10441
|
+
const pauseTask = (task, error) => {
|
|
10442
|
+
const tuiTask = tui.state.tasks.find((entry) => entry.task === task);
|
|
10443
|
+
tuiTask.status = "paused";
|
|
10444
|
+
tuiTask.error = error;
|
|
10445
|
+
tui.state.phase = "paused";
|
|
10446
|
+
tui.state.recovery = {
|
|
10447
|
+
taskIndex: tui.state.tasks.indexOf(tuiTask),
|
|
10448
|
+
taskText: task.text,
|
|
10449
|
+
error,
|
|
10450
|
+
issue: details ? { number: details.number, title: details.title } : void 0,
|
|
10451
|
+
worktree: tuiTask.worktree ?? worktreeRoot,
|
|
10452
|
+
selectedAction: "rerun"
|
|
10453
|
+
};
|
|
10454
|
+
tui.update();
|
|
10455
|
+
return tuiTask;
|
|
10456
|
+
};
|
|
10457
|
+
const clearRecovery = () => {
|
|
10458
|
+
tui.state.recovery = void 0;
|
|
10459
|
+
tui.state.phase = "dispatching";
|
|
10460
|
+
tui.update();
|
|
10461
|
+
};
|
|
10462
|
+
const runTaskLifecycle = async (task) => {
|
|
10463
|
+
const tuiTask = tui.state.tasks.find((entry) => entry.task === task);
|
|
10464
|
+
const startTime = Date.now();
|
|
10465
|
+
let plan;
|
|
10466
|
+
tuiTask.elapsed = startTime;
|
|
10467
|
+
tuiTask.error = void 0;
|
|
10468
|
+
if (localPlanner) {
|
|
10469
|
+
tuiTask.status = "planning";
|
|
10470
|
+
fileLogger?.phase(`Planning task: ${task.text}`);
|
|
10471
|
+
if (verbose) log.info(`Task #${tui.state.tasks.indexOf(tuiTask) + 1}: planning \u2014 "${task.text}"`);
|
|
10472
|
+
const rawContent = fileContentMap.get(task.file);
|
|
10473
|
+
const fileContext = rawContent ? buildTaskContext(rawContent, task) : void 0;
|
|
10474
|
+
let planResult;
|
|
10475
|
+
for (let attempt = 1; attempt <= maxPlanAttempts; attempt++) {
|
|
10476
|
+
try {
|
|
10477
|
+
planResult = await withTimeout(
|
|
10478
|
+
localPlanner.plan(task, fileContext, issueCwd, worktreeRoot),
|
|
10479
|
+
planTimeoutMs,
|
|
10480
|
+
"planner.plan()"
|
|
10481
|
+
);
|
|
10482
|
+
break;
|
|
10483
|
+
} catch (err) {
|
|
10484
|
+
if (err instanceof TimeoutError) {
|
|
10485
|
+
log.warn(`Planning timed out for task "${task.text}" (attempt ${attempt}/${maxPlanAttempts})`);
|
|
10486
|
+
fileLogger?.warn(`Planning timeout (attempt ${attempt}/${maxPlanAttempts})`);
|
|
10487
|
+
if (attempt < maxPlanAttempts) {
|
|
10488
|
+
log.debug(`Retrying planning (attempt ${attempt + 1}/${maxPlanAttempts})`);
|
|
10489
|
+
fileLogger?.info(`Retrying planning (attempt ${attempt + 1}/${maxPlanAttempts})`);
|
|
10009
10490
|
}
|
|
10010
|
-
tuiTask.status = "done";
|
|
10011
|
-
tuiTask.elapsed = Date.now() - startTime;
|
|
10012
|
-
if (verbose) log.success(`Task #${tui.state.tasks.indexOf(tuiTask) + 1}: done \u2014 "${task.text}" (${elapsed(tuiTask.elapsed)})`);
|
|
10013
|
-
completed++;
|
|
10014
10491
|
} else {
|
|
10015
|
-
|
|
10016
|
-
|
|
10017
|
-
|
|
10018
|
-
|
|
10019
|
-
|
|
10020
|
-
|
|
10492
|
+
planResult = {
|
|
10493
|
+
data: null,
|
|
10494
|
+
success: false,
|
|
10495
|
+
error: log.extractMessage(err),
|
|
10496
|
+
durationMs: 0
|
|
10497
|
+
};
|
|
10498
|
+
break;
|
|
10021
10499
|
}
|
|
10022
|
-
|
|
10023
|
-
task,
|
|
10024
|
-
success: false,
|
|
10025
|
-
error: execResult.error ?? "Executor failed without returning a dispatch result."
|
|
10026
|
-
};
|
|
10027
|
-
return dispatchResult;
|
|
10028
|
-
})
|
|
10029
|
-
);
|
|
10030
|
-
issueResults.push(...batchResults);
|
|
10031
|
-
if (!tui.state.model && localInstance.model) {
|
|
10032
|
-
tui.state.model = localInstance.model;
|
|
10500
|
+
}
|
|
10033
10501
|
}
|
|
10502
|
+
if (!planResult) {
|
|
10503
|
+
planResult = {
|
|
10504
|
+
data: null,
|
|
10505
|
+
success: false,
|
|
10506
|
+
error: `Planning timed out after ${resolvedPlanTimeoutMin}m (${maxPlanAttempts} attempts)`,
|
|
10507
|
+
durationMs: 0
|
|
10508
|
+
};
|
|
10509
|
+
}
|
|
10510
|
+
if (!planResult.success) {
|
|
10511
|
+
const error = `Planning failed: ${planResult.error}`;
|
|
10512
|
+
fileLogger?.error(error);
|
|
10513
|
+
tuiTask.elapsed = Date.now() - startTime;
|
|
10514
|
+
pauseTask(task, error);
|
|
10515
|
+
if (verbose) log.error(`Task #${tui.state.tasks.indexOf(tuiTask) + 1}: paused \u2014 ${error} (${elapsed(tuiTask.elapsed)})`);
|
|
10516
|
+
return { kind: "paused", error };
|
|
10517
|
+
}
|
|
10518
|
+
plan = planResult.data.prompt;
|
|
10519
|
+
fileLogger?.info(`Planning completed (${planResult.durationMs ?? 0}ms)`);
|
|
10034
10520
|
}
|
|
10035
|
-
|
|
10036
|
-
|
|
10037
|
-
|
|
10038
|
-
|
|
10039
|
-
|
|
10040
|
-
|
|
10041
|
-
|
|
10042
|
-
);
|
|
10043
|
-
log.debug(`Staged uncommitted changes for issue #${details.number}`);
|
|
10044
|
-
} catch (err) {
|
|
10045
|
-
log.warn(`Could not commit uncommitted changes for issue #${details.number}: ${log.formatErrorChain(err)}`);
|
|
10046
|
-
}
|
|
10047
|
-
}
|
|
10048
|
-
fileLogger?.phase("Commit generation");
|
|
10049
|
-
let commitAgentResult;
|
|
10050
|
-
if (!noBranch && branchName && defaultBranch && details && datasource4.supportsGit()) {
|
|
10051
|
-
try {
|
|
10052
|
-
const branchDiff = await getBranchDiff(defaultBranch, issueCwd);
|
|
10053
|
-
if (branchDiff) {
|
|
10054
|
-
const result = await localCommitAgent.generate({
|
|
10055
|
-
branchDiff,
|
|
10056
|
-
issue: details,
|
|
10057
|
-
taskResults: issueResults,
|
|
10521
|
+
tuiTask.status = "running";
|
|
10522
|
+
fileLogger?.phase(`Executing task: ${task.text}`);
|
|
10523
|
+
if (verbose) log.info(`Task #${tui.state.tasks.indexOf(tuiTask) + 1}: executing \u2014 "${task.text}"`);
|
|
10524
|
+
const execResult = await withRetry(
|
|
10525
|
+
async () => {
|
|
10526
|
+
const result = await localExecutor.execute({
|
|
10527
|
+
task,
|
|
10058
10528
|
cwd: issueCwd,
|
|
10529
|
+
plan: plan ?? null,
|
|
10059
10530
|
worktreeRoot
|
|
10060
10531
|
});
|
|
10061
|
-
if (result.success) {
|
|
10062
|
-
|
|
10063
|
-
|
|
10064
|
-
|
|
10065
|
-
|
|
10066
|
-
|
|
10067
|
-
|
|
10068
|
-
|
|
10069
|
-
|
|
10070
|
-
|
|
10071
|
-
|
|
10072
|
-
|
|
10073
|
-
|
|
10532
|
+
if (!result.success) {
|
|
10533
|
+
throw new Error(result.error ?? "Execution failed");
|
|
10534
|
+
}
|
|
10535
|
+
return result;
|
|
10536
|
+
},
|
|
10537
|
+
resolvedRetries,
|
|
10538
|
+
{ label: `executor "${task.text}"` }
|
|
10539
|
+
).catch((err) => ({
|
|
10540
|
+
data: null,
|
|
10541
|
+
success: false,
|
|
10542
|
+
error: log.extractMessage(err),
|
|
10543
|
+
durationMs: 0
|
|
10544
|
+
}));
|
|
10545
|
+
if (!execResult.success) {
|
|
10546
|
+
const error = execResult.error ?? "Executor failed without returning a dispatch result.";
|
|
10547
|
+
fileLogger?.error(`Execution failed: ${error}`);
|
|
10548
|
+
tuiTask.elapsed = Date.now() - startTime;
|
|
10549
|
+
pauseTask(task, error);
|
|
10550
|
+
if (verbose) log.error(`Task #${tui.state.tasks.indexOf(tuiTask) + 1}: paused \u2014 "${task.text}" (${elapsed(tuiTask.elapsed)})${error ? `: ${error}` : ""}`);
|
|
10551
|
+
return { kind: "paused", error };
|
|
10552
|
+
}
|
|
10553
|
+
fileLogger?.info(`Execution completed successfully (${Date.now() - startTime}ms)`);
|
|
10554
|
+
try {
|
|
10555
|
+
const parsed = parseIssueFilename(task.file);
|
|
10556
|
+
const updatedContent = await readFile8(task.file, "utf-8");
|
|
10557
|
+
if (parsed) {
|
|
10558
|
+
const issueDetails = issueDetailsByFile.get(task.file);
|
|
10559
|
+
const title = issueDetails?.title ?? parsed.slug;
|
|
10560
|
+
await datasource4.update(parsed.issueId, title, updatedContent, fetchOpts);
|
|
10561
|
+
log.success(`Synced task completion to issue #${parsed.issueId}`);
|
|
10562
|
+
} else {
|
|
10563
|
+
const issueDetails = issueDetailsByFile.get(task.file);
|
|
10564
|
+
if (issueDetails) {
|
|
10565
|
+
await datasource4.update(issueDetails.number, issueDetails.title, updatedContent, fetchOpts);
|
|
10566
|
+
log.success(`Synced task completion to issue #${issueDetails.number}`);
|
|
10074
10567
|
}
|
|
10075
10568
|
}
|
|
10076
10569
|
} catch (err) {
|
|
10077
|
-
log.warn(`
|
|
10570
|
+
log.warn(`Could not sync task completion to datasource: ${log.formatErrorChain(err)}`);
|
|
10078
10571
|
}
|
|
10079
|
-
|
|
10080
|
-
|
|
10081
|
-
|
|
10082
|
-
if (
|
|
10083
|
-
|
|
10084
|
-
|
|
10085
|
-
|
|
10086
|
-
|
|
10087
|
-
|
|
10088
|
-
|
|
10572
|
+
tuiTask.status = "done";
|
|
10573
|
+
tuiTask.error = void 0;
|
|
10574
|
+
tuiTask.elapsed = Date.now() - startTime;
|
|
10575
|
+
if (verbose) log.success(`Task #${tui.state.tasks.indexOf(tuiTask) + 1}: done \u2014 "${task.text}" (${elapsed(tuiTask.elapsed)})`);
|
|
10576
|
+
return { kind: "success", result: execResult.data.dispatchResult };
|
|
10577
|
+
};
|
|
10578
|
+
const recoverPausedTask = async (task, error) => {
|
|
10579
|
+
while (true) {
|
|
10580
|
+
const tuiTask = pauseTask(task, error);
|
|
10581
|
+
if (!canRecoverInteractively) {
|
|
10582
|
+
log.warn("Manual rerun requires an interactive terminal; verbose or non-TTY runs will not wait for input, and the current branch/worktree will be left intact.");
|
|
10583
|
+
tuiTask.status = "failed";
|
|
10584
|
+
clearRecovery();
|
|
10585
|
+
return { halted: true, result: { task, success: false, error } };
|
|
10089
10586
|
}
|
|
10090
|
-
|
|
10091
|
-
|
|
10092
|
-
|
|
10093
|
-
|
|
10094
|
-
|
|
10095
|
-
|
|
10096
|
-
|
|
10097
|
-
|
|
10098
|
-
|
|
10099
|
-
|
|
10100
|
-
|
|
10101
|
-
|
|
10102
|
-
|
|
10103
|
-
|
|
10104
|
-
|
|
10105
|
-
|
|
10106
|
-
|
|
10107
|
-
|
|
10108
|
-
|
|
10109
|
-
|
|
10110
|
-
|
|
10111
|
-
|
|
10112
|
-
|
|
10113
|
-
|
|
10587
|
+
const action = await tui.waitForRecoveryAction();
|
|
10588
|
+
if (action === "quit") {
|
|
10589
|
+
tuiTask.status = "failed";
|
|
10590
|
+
clearRecovery();
|
|
10591
|
+
return { halted: true, result: { task, success: false, error } };
|
|
10592
|
+
}
|
|
10593
|
+
clearRecovery();
|
|
10594
|
+
const rerun = await runTaskLifecycle(task);
|
|
10595
|
+
if (rerun.kind === "success") {
|
|
10596
|
+
return { halted: false, result: rerun.result };
|
|
10597
|
+
}
|
|
10598
|
+
error = rerun.error;
|
|
10599
|
+
}
|
|
10600
|
+
};
|
|
10601
|
+
const groups = groupTasksByMode(fileTasks);
|
|
10602
|
+
let stopAfterIssue = false;
|
|
10603
|
+
for (const group of groups) {
|
|
10604
|
+
const groupResults = await runWithConcurrency({
|
|
10605
|
+
items: group,
|
|
10606
|
+
concurrency,
|
|
10607
|
+
worker: async (task) => runTaskLifecycle(task),
|
|
10608
|
+
shouldStop: () => stopAfterIssue
|
|
10609
|
+
});
|
|
10610
|
+
const pausedTasks = [];
|
|
10611
|
+
for (let i = 0; i < group.length; i++) {
|
|
10612
|
+
const result = groupResults[i];
|
|
10613
|
+
if (result.status === "skipped") continue;
|
|
10614
|
+
if (result.status === "rejected") {
|
|
10615
|
+
pausedTasks.push({ task: group[i], error: String(result.reason) });
|
|
10616
|
+
continue;
|
|
10114
10617
|
}
|
|
10618
|
+
const outcome = result.value;
|
|
10619
|
+
if (outcome.kind === "success") {
|
|
10620
|
+
upsertResult(issueResults, outcome.result);
|
|
10621
|
+
upsertResult(results, outcome.result);
|
|
10622
|
+
} else {
|
|
10623
|
+
pausedTasks.push({ task: group[i], error: outcome.error });
|
|
10624
|
+
}
|
|
10625
|
+
}
|
|
10626
|
+
for (const pausedTask of pausedTasks) {
|
|
10627
|
+
const resolution = await recoverPausedTask(pausedTask.task, pausedTask.error);
|
|
10628
|
+
upsertResult(issueResults, resolution.result);
|
|
10629
|
+
upsertResult(results, resolution.result);
|
|
10630
|
+
if (resolution.halted) {
|
|
10631
|
+
preserveContext = true;
|
|
10632
|
+
stopAfterIssue = true;
|
|
10633
|
+
halted = true;
|
|
10634
|
+
break;
|
|
10635
|
+
}
|
|
10636
|
+
}
|
|
10637
|
+
if (!tui.state.model && localInstance.model) {
|
|
10638
|
+
tui.state.model = localInstance.model;
|
|
10639
|
+
}
|
|
10640
|
+
if (stopAfterIssue) break;
|
|
10641
|
+
}
|
|
10642
|
+
if (!preserveContext) {
|
|
10643
|
+
if (!noBranch && branchName && defaultBranch && details && datasource4.supportsGit()) {
|
|
10115
10644
|
try {
|
|
10116
|
-
await
|
|
10117
|
-
|
|
10645
|
+
await datasource4.commitAllChanges(
|
|
10646
|
+
`chore: stage uncommitted changes for issue #${details.number}`,
|
|
10647
|
+
issueLifecycleOpts
|
|
10648
|
+
);
|
|
10649
|
+
log.debug(`Staged uncommitted changes for issue #${details.number}`);
|
|
10118
10650
|
} catch (err) {
|
|
10119
|
-
log.warn(`Could not
|
|
10651
|
+
log.warn(`Could not commit uncommitted changes for issue #${details.number}: ${log.formatErrorChain(err)}`);
|
|
10120
10652
|
}
|
|
10653
|
+
}
|
|
10654
|
+
fileLogger?.phase("Commit generation");
|
|
10655
|
+
let commitAgentResult;
|
|
10656
|
+
if (!noBranch && branchName && defaultBranch && details && datasource4.supportsGit()) {
|
|
10121
10657
|
try {
|
|
10122
|
-
await
|
|
10658
|
+
const branchDiff = await getBranchDiff(defaultBranch, issueCwd);
|
|
10659
|
+
if (branchDiff) {
|
|
10660
|
+
const result = await localCommitAgent.generate({
|
|
10661
|
+
branchDiff,
|
|
10662
|
+
issue: details,
|
|
10663
|
+
taskResults: issueResults,
|
|
10664
|
+
cwd: issueCwd,
|
|
10665
|
+
worktreeRoot
|
|
10666
|
+
});
|
|
10667
|
+
if (result.success) {
|
|
10668
|
+
commitAgentResult = result;
|
|
10669
|
+
fileLogger?.info(`Commit message generated for issue #${details.number}`);
|
|
10670
|
+
try {
|
|
10671
|
+
await squashBranchCommits(defaultBranch, result.commitMessage, issueCwd);
|
|
10672
|
+
log.debug(`Rewrote commit message for issue #${details.number}`);
|
|
10673
|
+
fileLogger?.info(`Rewrote commit history for issue #${details.number}`);
|
|
10674
|
+
} catch (err) {
|
|
10675
|
+
log.warn(`Could not rewrite commit message for issue #${details.number}: ${log.formatErrorChain(err)}`);
|
|
10676
|
+
}
|
|
10677
|
+
} else {
|
|
10678
|
+
log.warn(`Commit agent failed for issue #${details.number}: ${result.error}`);
|
|
10679
|
+
fileLogger?.warn(`Commit agent failed: ${result.error}`);
|
|
10680
|
+
}
|
|
10681
|
+
}
|
|
10123
10682
|
} catch (err) {
|
|
10124
|
-
log.warn(`
|
|
10683
|
+
log.warn(`Commit agent error for issue #${details.number}: ${log.formatErrorChain(err)}`);
|
|
10125
10684
|
}
|
|
10126
|
-
}
|
|
10127
|
-
|
|
10128
|
-
|
|
10129
|
-
|
|
10130
|
-
|
|
10131
|
-
|
|
10132
|
-
|
|
10133
|
-
|
|
10685
|
+
}
|
|
10686
|
+
fileLogger?.phase("PR lifecycle");
|
|
10687
|
+
if (!noBranch && branchName && defaultBranch && details) {
|
|
10688
|
+
if (feature && featureBranchName) {
|
|
10689
|
+
if (worktreePath) {
|
|
10690
|
+
try {
|
|
10691
|
+
await removeWorktree(cwd, file);
|
|
10692
|
+
} catch (err) {
|
|
10693
|
+
log.warn(`Could not remove worktree for issue #${details.number}: ${log.formatErrorChain(err)}`);
|
|
10694
|
+
}
|
|
10134
10695
|
}
|
|
10135
|
-
}
|
|
10136
|
-
if (datasource4.supportsGit()) {
|
|
10137
10696
|
try {
|
|
10138
|
-
|
|
10139
|
-
|
|
10140
|
-
|
|
10141
|
-
fileTasks,
|
|
10142
|
-
issueResults,
|
|
10143
|
-
defaultBranch,
|
|
10144
|
-
datasource4.name,
|
|
10145
|
-
issueLifecycleOpts.cwd
|
|
10146
|
-
);
|
|
10147
|
-
const prUrl = await datasource4.createPullRequest(
|
|
10148
|
-
branchName,
|
|
10149
|
-
details.number,
|
|
10150
|
-
prTitle,
|
|
10151
|
-
prBody,
|
|
10152
|
-
issueLifecycleOpts,
|
|
10153
|
-
startingBranch
|
|
10154
|
-
);
|
|
10155
|
-
if (prUrl) {
|
|
10156
|
-
log.success(`Created PR for issue #${details.number}: ${prUrl}`);
|
|
10157
|
-
fileLogger?.info(`Created PR: ${prUrl}`);
|
|
10158
|
-
}
|
|
10697
|
+
await datasource4.switchBranch(featureBranchName, lifecycleOpts);
|
|
10698
|
+
await exec9("git", ["merge", branchName, "--no-ff", "-m", `merge: issue #${details.number}`], { cwd, shell: process.platform === "win32" });
|
|
10699
|
+
log.debug(`Merged ${branchName} into ${featureBranchName}`);
|
|
10159
10700
|
} catch (err) {
|
|
10160
|
-
|
|
10161
|
-
|
|
10701
|
+
const mergeError = `Could not merge ${branchName} into feature branch: ${log.formatErrorChain(err)}`;
|
|
10702
|
+
log.warn(mergeError);
|
|
10703
|
+
try {
|
|
10704
|
+
await exec9("git", ["merge", "--abort"], { cwd, shell: process.platform === "win32" });
|
|
10705
|
+
} catch {
|
|
10706
|
+
}
|
|
10707
|
+
for (const task of fileTasks) {
|
|
10708
|
+
const tuiTask = tui.state.tasks.find((t) => t.task === task);
|
|
10709
|
+
if (tuiTask) {
|
|
10710
|
+
tuiTask.status = "failed";
|
|
10711
|
+
tuiTask.error = mergeError;
|
|
10712
|
+
}
|
|
10713
|
+
upsertResult(results, { task, success: false, error: mergeError });
|
|
10714
|
+
}
|
|
10715
|
+
return { halted: false };
|
|
10162
10716
|
}
|
|
10163
|
-
}
|
|
10164
|
-
if (useWorktrees && worktreePath) {
|
|
10165
10717
|
try {
|
|
10166
|
-
await
|
|
10718
|
+
await exec9("git", ["branch", "-d", branchName], { cwd, shell: process.platform === "win32" });
|
|
10719
|
+
log.debug(`Deleted local branch ${branchName}`);
|
|
10167
10720
|
} catch (err) {
|
|
10168
|
-
log.warn(`Could not
|
|
10721
|
+
log.warn(`Could not delete local branch ${branchName}: ${log.formatErrorChain(err)}`);
|
|
10169
10722
|
}
|
|
10170
|
-
} else if (!useWorktrees && datasource4.supportsGit()) {
|
|
10171
10723
|
try {
|
|
10172
|
-
await datasource4.switchBranch(
|
|
10173
|
-
log.debug(`Switched back to ${defaultBranch}`);
|
|
10724
|
+
await datasource4.switchBranch(featureDefaultBranch, lifecycleOpts);
|
|
10174
10725
|
} catch (err) {
|
|
10175
|
-
log.warn(`Could not switch back to ${
|
|
10726
|
+
log.warn(`Could not switch back to ${featureDefaultBranch}: ${log.formatErrorChain(err)}`);
|
|
10727
|
+
}
|
|
10728
|
+
} else {
|
|
10729
|
+
if (datasource4.supportsGit()) {
|
|
10730
|
+
try {
|
|
10731
|
+
await datasource4.pushBranch(branchName, issueLifecycleOpts);
|
|
10732
|
+
log.debug(`Pushed branch ${branchName}`);
|
|
10733
|
+
fileLogger?.info(`Pushed branch ${branchName}`);
|
|
10734
|
+
} catch (err) {
|
|
10735
|
+
log.warn(`Could not push branch ${branchName}: ${log.formatErrorChain(err)}`);
|
|
10736
|
+
}
|
|
10737
|
+
}
|
|
10738
|
+
if (datasource4.supportsGit()) {
|
|
10739
|
+
try {
|
|
10740
|
+
const prTitle = commitAgentResult?.prTitle || await buildPrTitle(details.title, defaultBranch, issueLifecycleOpts.cwd);
|
|
10741
|
+
const prBody = commitAgentResult?.prDescription || await buildPrBody(
|
|
10742
|
+
details,
|
|
10743
|
+
fileTasks,
|
|
10744
|
+
issueResults,
|
|
10745
|
+
defaultBranch,
|
|
10746
|
+
datasource4.name,
|
|
10747
|
+
issueLifecycleOpts.cwd
|
|
10748
|
+
);
|
|
10749
|
+
const prUrl = await datasource4.createPullRequest(
|
|
10750
|
+
branchName,
|
|
10751
|
+
details.number,
|
|
10752
|
+
prTitle,
|
|
10753
|
+
prBody,
|
|
10754
|
+
issueLifecycleOpts,
|
|
10755
|
+
startingBranch
|
|
10756
|
+
);
|
|
10757
|
+
if (prUrl) {
|
|
10758
|
+
log.success(`Created PR for issue #${details.number}: ${prUrl}`);
|
|
10759
|
+
fileLogger?.info(`Created PR: ${prUrl}`);
|
|
10760
|
+
}
|
|
10761
|
+
} catch (err) {
|
|
10762
|
+
log.warn(`Could not create PR for issue #${details.number}: ${log.formatErrorChain(err)}`);
|
|
10763
|
+
fileLogger?.warn(`PR creation failed: ${log.extractMessage(err)}`);
|
|
10764
|
+
}
|
|
10765
|
+
}
|
|
10766
|
+
if (useWorktrees && worktreePath) {
|
|
10767
|
+
try {
|
|
10768
|
+
await removeWorktree(cwd, file);
|
|
10769
|
+
} catch (err) {
|
|
10770
|
+
log.warn(`Could not remove worktree for issue #${details.number}: ${log.formatErrorChain(err)}`);
|
|
10771
|
+
}
|
|
10772
|
+
} else if (!useWorktrees && datasource4.supportsGit()) {
|
|
10773
|
+
try {
|
|
10774
|
+
await datasource4.switchBranch(defaultBranch, lifecycleOpts);
|
|
10775
|
+
log.debug(`Switched back to ${defaultBranch}`);
|
|
10776
|
+
} catch (err) {
|
|
10777
|
+
log.warn(`Could not switch back to ${defaultBranch}: ${log.formatErrorChain(err)}`);
|
|
10778
|
+
}
|
|
10176
10779
|
}
|
|
10177
10780
|
}
|
|
10178
10781
|
}
|
|
@@ -10183,31 +10786,42 @@ ${err.stack}` : ""}`);
|
|
|
10183
10786
|
await localPlanner?.cleanup();
|
|
10184
10787
|
await localInstance.cleanup();
|
|
10185
10788
|
}
|
|
10789
|
+
return { halted: stopAfterIssue };
|
|
10186
10790
|
};
|
|
10187
10791
|
if (fileLogger) {
|
|
10188
|
-
|
|
10792
|
+
return fileLoggerStorage.run(fileLogger, async () => {
|
|
10189
10793
|
try {
|
|
10190
|
-
await body();
|
|
10794
|
+
return await body();
|
|
10191
10795
|
} finally {
|
|
10192
10796
|
fileLogger.close();
|
|
10193
10797
|
}
|
|
10194
10798
|
});
|
|
10195
|
-
} else {
|
|
10196
|
-
await body();
|
|
10197
10799
|
}
|
|
10800
|
+
return body();
|
|
10198
10801
|
};
|
|
10199
10802
|
if (useWorktrees && !feature) {
|
|
10200
|
-
|
|
10201
|
-
|
|
10202
|
-
|
|
10203
|
-
|
|
10204
|
-
|
|
10803
|
+
const issueEntries = Array.from(tasksByFile.entries());
|
|
10804
|
+
const concurrencyResults = await runWithConcurrency({
|
|
10805
|
+
items: issueEntries,
|
|
10806
|
+
concurrency,
|
|
10807
|
+
worker: async ([file, fileTasks]) => processIssueFile(file, fileTasks),
|
|
10808
|
+
shouldStop: () => halted
|
|
10809
|
+
});
|
|
10810
|
+
for (const result of concurrencyResults) {
|
|
10811
|
+
if (result.status === "fulfilled" && result.value?.halted) {
|
|
10812
|
+
halted = true;
|
|
10813
|
+
}
|
|
10814
|
+
}
|
|
10205
10815
|
} else {
|
|
10206
10816
|
for (const [file, fileTasks] of tasksByFile) {
|
|
10207
|
-
await processIssueFile(file, fileTasks);
|
|
10817
|
+
const issueResult = await processIssueFile(file, fileTasks);
|
|
10818
|
+
if (issueResult?.halted) {
|
|
10819
|
+
halted = true;
|
|
10820
|
+
break;
|
|
10821
|
+
}
|
|
10208
10822
|
}
|
|
10209
10823
|
}
|
|
10210
|
-
if (feature && featureBranchName && featureDefaultBranch) {
|
|
10824
|
+
if (!halted && feature && featureBranchName && featureDefaultBranch) {
|
|
10211
10825
|
try {
|
|
10212
10826
|
await datasource4.switchBranch(featureBranchName, lifecycleOpts);
|
|
10213
10827
|
log.debug(`Switched to feature branch ${featureBranchName}`);
|
|
@@ -10249,7 +10863,10 @@ ${err.stack}` : ""}`);
|
|
|
10249
10863
|
await executor?.cleanup();
|
|
10250
10864
|
await planner?.cleanup();
|
|
10251
10865
|
await instance?.cleanup();
|
|
10866
|
+
const completed = results.filter((result) => result.success).length;
|
|
10867
|
+
const failed = results.filter((result) => !result.success).length;
|
|
10252
10868
|
tui.state.phase = "done";
|
|
10869
|
+
setAuthPromptHandler(null);
|
|
10253
10870
|
tui.stop();
|
|
10254
10871
|
if (verbose) log.success(`Done \u2014 ${completed} completed, ${failed} failed (${elapsed(Date.now() - tui.state.startTime)})`);
|
|
10255
10872
|
return { total: allTasks.length, completed, failed, skipped: 0, results };
|
|
@@ -10259,17 +10876,17 @@ ${err.stack}` : ""}`);
|
|
|
10259
10876
|
throw err;
|
|
10260
10877
|
}
|
|
10261
10878
|
}
|
|
10262
|
-
async function dryRunMode(issueIds, cwd, source, org, project, workItemType, iteration, area) {
|
|
10879
|
+
async function dryRunMode(issueIds, cwd, source, org, project, workItemType, iteration, area, username) {
|
|
10263
10880
|
if (!source) {
|
|
10264
10881
|
log.error("No datasource configured. Use --source or run 'dispatch config' to set up defaults.");
|
|
10265
10882
|
return { total: 0, completed: 0, failed: 0, skipped: 0, results: [] };
|
|
10266
10883
|
}
|
|
10267
10884
|
const datasource4 = getDatasource(source);
|
|
10268
10885
|
const fetchOpts = { cwd, org, project, workItemType, iteration, area };
|
|
10269
|
-
const lifecycleOpts = { cwd };
|
|
10270
|
-
let
|
|
10886
|
+
const lifecycleOpts = { cwd, username };
|
|
10887
|
+
let resolvedUsername = "";
|
|
10271
10888
|
try {
|
|
10272
|
-
|
|
10889
|
+
resolvedUsername = await datasource4.getUsername(lifecycleOpts);
|
|
10273
10890
|
} catch {
|
|
10274
10891
|
}
|
|
10275
10892
|
let items;
|
|
@@ -10303,7 +10920,7 @@ async function dryRunMode(issueIds, cwd, source, org, project, workItemType, ite
|
|
|
10303
10920
|
for (const task of allTasks) {
|
|
10304
10921
|
const parsed = parseIssueFilename(task.file);
|
|
10305
10922
|
const details = parsed ? items.find((item) => item.number === parsed.issueId) : issueDetailsByFile.get(task.file);
|
|
10306
|
-
const branchInfo = details ? ` [branch: ${datasource4.buildBranchName(details.number, details.title,
|
|
10923
|
+
const branchInfo = details ? ` [branch: ${datasource4.buildBranchName(details.number, details.title, resolvedUsername)}]` : "";
|
|
10307
10924
|
log.task(allTasks.indexOf(task), allTasks.length, `${task.file}:${task.line} \u2014 ${task.text}${branchInfo}`);
|
|
10308
10925
|
}
|
|
10309
10926
|
return {
|
|
@@ -10327,7 +10944,7 @@ async function runMultiIssueFixTests(opts) {
|
|
|
10327
10944
|
}
|
|
10328
10945
|
let username = "";
|
|
10329
10946
|
try {
|
|
10330
|
-
username = await datasource4.getUsername({ cwd: opts.cwd });
|
|
10947
|
+
username = await datasource4.getUsername({ cwd: opts.cwd, username: opts.username });
|
|
10331
10948
|
} catch (err) {
|
|
10332
10949
|
log.warn(`Could not resolve git username for branch naming: ${log.formatErrorChain(err)}`);
|
|
10333
10950
|
}
|
|
@@ -10446,7 +11063,8 @@ async function boot9(opts) {
|
|
|
10446
11063
|
verbose: m.verbose,
|
|
10447
11064
|
testTimeout: m.testTimeout,
|
|
10448
11065
|
org: m.org,
|
|
10449
|
-
project: m.project
|
|
11066
|
+
project: m.project,
|
|
11067
|
+
username: m.username
|
|
10450
11068
|
});
|
|
10451
11069
|
}
|
|
10452
11070
|
if (m.spec) {
|
|
@@ -10464,7 +11082,11 @@ async function boot9(opts) {
|
|
|
10464
11082
|
iteration: m.iteration,
|
|
10465
11083
|
area: m.area,
|
|
10466
11084
|
concurrency: m.concurrency,
|
|
10467
|
-
dryRun: m.dryRun
|
|
11085
|
+
dryRun: m.dryRun,
|
|
11086
|
+
retries: m.retries,
|
|
11087
|
+
specTimeout: m.specTimeout ?? DEFAULT_SPEC_TIMEOUT_MIN,
|
|
11088
|
+
specWarnTimeout: m.specWarnTimeout,
|
|
11089
|
+
specKillTimeout: m.specKillTimeout
|
|
10468
11090
|
});
|
|
10469
11091
|
}
|
|
10470
11092
|
if (m.respec) {
|
|
@@ -10506,7 +11128,11 @@ async function boot9(opts) {
|
|
|
10506
11128
|
iteration: m.iteration,
|
|
10507
11129
|
area: m.area,
|
|
10508
11130
|
concurrency: m.concurrency,
|
|
10509
|
-
dryRun: m.dryRun
|
|
11131
|
+
dryRun: m.dryRun,
|
|
11132
|
+
retries: m.retries,
|
|
11133
|
+
specTimeout: m.specTimeout ?? DEFAULT_SPEC_TIMEOUT_MIN,
|
|
11134
|
+
specWarnTimeout: m.specWarnTimeout,
|
|
11135
|
+
specKillTimeout: m.specKillTimeout
|
|
10510
11136
|
});
|
|
10511
11137
|
}
|
|
10512
11138
|
return this.orchestrate({
|
|
@@ -10529,7 +11155,8 @@ async function boot9(opts) {
|
|
|
10529
11155
|
planRetries: m.planRetries,
|
|
10530
11156
|
retries: m.retries,
|
|
10531
11157
|
force: m.force,
|
|
10532
|
-
feature: m.feature
|
|
11158
|
+
feature: m.feature,
|
|
11159
|
+
username: m.username
|
|
10533
11160
|
});
|
|
10534
11161
|
}
|
|
10535
11162
|
};
|
|
@@ -10565,8 +11192,8 @@ var HELP = `
|
|
|
10565
11192
|
--provider <name> Agent backend: ${PROVIDER_NAMES.join(", ")} (default: opencode)
|
|
10566
11193
|
--source <name> Issue source: ${DATASOURCE_NAMES.join(", ")} (optional; auto-detected from git remote)
|
|
10567
11194
|
--server-url <url> URL of a running provider server
|
|
10568
|
-
--plan-timeout <min> Planning timeout in minutes (default:
|
|
10569
|
-
--retries <n> Retry attempts for all agents (default:
|
|
11195
|
+
--plan-timeout <min> Planning timeout in minutes (default: 30)
|
|
11196
|
+
--retries <n> Retry attempts for all agents (default: 3)
|
|
10570
11197
|
--plan-retries <n> Retry attempts after planning timeout (overrides --retries for planner)
|
|
10571
11198
|
--test-timeout <min> Test timeout in minutes (default: 5)
|
|
10572
11199
|
--cwd <dir> Working directory (default: cwd)
|
|
@@ -10574,6 +11201,9 @@ var HELP = `
|
|
|
10574
11201
|
Spec options:
|
|
10575
11202
|
--spec <value> Comma-separated issue numbers, glob pattern for .md files, or inline text description
|
|
10576
11203
|
--respec [value] Regenerate specs: issue numbers, glob, or omit to regenerate all existing specs
|
|
11204
|
+
--spec-timeout <min> Spec generation timeout in minutes (default: 10)
|
|
11205
|
+
--spec-warn-timeout <min> Spec warn-phase timeout in minutes (default: 10)
|
|
11206
|
+
--spec-kill-timeout <min> Spec kill-phase timeout in minutes (default: 10)
|
|
10577
11207
|
--output-dir <dir> Output directory for specs (default: .dispatch/specs)
|
|
10578
11208
|
|
|
10579
11209
|
Azure DevOps options:
|
|
@@ -10585,6 +11215,9 @@ var HELP = `
|
|
|
10585
11215
|
-h, --help Show this help
|
|
10586
11216
|
-v, --version Show version
|
|
10587
11217
|
|
|
11218
|
+
Interactive dispatch runs pause exhausted failed tasks so you can rerun them
|
|
11219
|
+
in place; verbose or non-TTY runs do not wait for input.
|
|
11220
|
+
|
|
10588
11221
|
Config:
|
|
10589
11222
|
dispatch config Launch interactive configuration wizard
|
|
10590
11223
|
|
|
@@ -10632,6 +11265,9 @@ var CLI_OPTIONS_MAP = {
|
|
|
10632
11265
|
concurrency: "concurrency",
|
|
10633
11266
|
serverUrl: "serverUrl",
|
|
10634
11267
|
planTimeout: "planTimeout",
|
|
11268
|
+
specTimeout: "specTimeout",
|
|
11269
|
+
specWarnTimeout: "specWarnTimeout",
|
|
11270
|
+
specKillTimeout: "specKillTimeout",
|
|
10635
11271
|
retries: "retries",
|
|
10636
11272
|
planRetries: "planRetries",
|
|
10637
11273
|
testTimeout: "testTimeout",
|
|
@@ -10671,6 +11307,45 @@ function parseArgs(argv) {
|
|
|
10671
11307
|
if (n > CONFIG_BOUNDS.planTimeout.max) throw new CommanderError(1, "commander.invalidArgument", `--plan-timeout must not exceed ${CONFIG_BOUNDS.planTimeout.max}`);
|
|
10672
11308
|
return n;
|
|
10673
11309
|
}
|
|
11310
|
+
).option(
|
|
11311
|
+
"--spec-timeout <min>",
|
|
11312
|
+
"Spec generation timeout in minutes",
|
|
11313
|
+
(val) => {
|
|
11314
|
+
const n = parseFloat(val);
|
|
11315
|
+
if (isNaN(n) || n < CONFIG_BOUNDS.specTimeout.min) {
|
|
11316
|
+
throw new CommanderError(1, "commander.invalidArgument", "--spec-timeout must be a positive number (minutes)");
|
|
11317
|
+
}
|
|
11318
|
+
if (n > CONFIG_BOUNDS.specTimeout.max) {
|
|
11319
|
+
throw new CommanderError(1, "commander.invalidArgument", `--spec-timeout must not exceed ${CONFIG_BOUNDS.specTimeout.max}`);
|
|
11320
|
+
}
|
|
11321
|
+
return n;
|
|
11322
|
+
}
|
|
11323
|
+
).option(
|
|
11324
|
+
"--spec-warn-timeout <min>",
|
|
11325
|
+
"Spec warn-phase timeout in minutes",
|
|
11326
|
+
(val) => {
|
|
11327
|
+
const n = parseFloat(val);
|
|
11328
|
+
if (isNaN(n) || n < CONFIG_BOUNDS.specWarnTimeout.min) {
|
|
11329
|
+
throw new CommanderError(1, "commander.invalidArgument", "--spec-warn-timeout must be a positive number (minutes)");
|
|
11330
|
+
}
|
|
11331
|
+
if (n > CONFIG_BOUNDS.specWarnTimeout.max) {
|
|
11332
|
+
throw new CommanderError(1, "commander.invalidArgument", `--spec-warn-timeout must not exceed ${CONFIG_BOUNDS.specWarnTimeout.max}`);
|
|
11333
|
+
}
|
|
11334
|
+
return n;
|
|
11335
|
+
}
|
|
11336
|
+
).option(
|
|
11337
|
+
"--spec-kill-timeout <min>",
|
|
11338
|
+
"Spec kill-phase timeout in minutes",
|
|
11339
|
+
(val) => {
|
|
11340
|
+
const n = parseFloat(val);
|
|
11341
|
+
if (isNaN(n) || n < CONFIG_BOUNDS.specKillTimeout.min) {
|
|
11342
|
+
throw new CommanderError(1, "commander.invalidArgument", "--spec-kill-timeout must be a positive number (minutes)");
|
|
11343
|
+
}
|
|
11344
|
+
if (n > CONFIG_BOUNDS.specKillTimeout.max) {
|
|
11345
|
+
throw new CommanderError(1, "commander.invalidArgument", `--spec-kill-timeout must not exceed ${CONFIG_BOUNDS.specKillTimeout.max}`);
|
|
11346
|
+
}
|
|
11347
|
+
return n;
|
|
11348
|
+
}
|
|
10674
11349
|
).option(
|
|
10675
11350
|
"--retries <n>",
|
|
10676
11351
|
"Retry attempts",
|
|
@@ -10735,6 +11410,9 @@ function parseArgs(argv) {
|
|
|
10735
11410
|
if (opts.concurrency !== void 0) args.concurrency = opts.concurrency;
|
|
10736
11411
|
if (opts.serverUrl !== void 0) args.serverUrl = opts.serverUrl;
|
|
10737
11412
|
if (opts.planTimeout !== void 0) args.planTimeout = opts.planTimeout;
|
|
11413
|
+
if (opts.specTimeout !== void 0) args.specTimeout = opts.specTimeout;
|
|
11414
|
+
if (opts.specWarnTimeout !== void 0) args.specWarnTimeout = opts.specWarnTimeout;
|
|
11415
|
+
if (opts.specKillTimeout !== void 0) args.specKillTimeout = opts.specKillTimeout;
|
|
10738
11416
|
if (opts.retries !== void 0) args.retries = opts.retries;
|
|
10739
11417
|
if (opts.planRetries !== void 0) args.planRetries = opts.planRetries;
|
|
10740
11418
|
if (opts.testTimeout !== void 0) args.testTimeout = opts.testTimeout;
|
|
@@ -10785,7 +11463,7 @@ async function main() {
|
|
|
10785
11463
|
process.exit(0);
|
|
10786
11464
|
}
|
|
10787
11465
|
if (args.version) {
|
|
10788
|
-
console.log(`dispatch v${"1.4.
|
|
11466
|
+
console.log(`dispatch v${"1.4.4"}`);
|
|
10789
11467
|
process.exit(0);
|
|
10790
11468
|
}
|
|
10791
11469
|
const orchestrator = await boot9({ cwd: args.cwd });
|