@wrongstack/sdd 1.0.32 → 1.0.35
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +800 -702
- package/dist/sdd-parallel-controls.d.ts +39 -0
- package/dist/sdd-parallel-run-state.d.ts +196 -0
- package/dist/sdd-parallel-run.d.ts +23 -140
- package/dist/spec-builder-parsing.d.ts +16 -0
- package/dist/spec-builder-prompts.d.ts +7 -0
- package/dist/spec-builder-session-file.d.ts +11 -0
- package/dist/task-generator.d.ts +1 -2
- package/package.json +5 -5
package/dist/index.js
CHANGED
|
@@ -1975,8 +1975,8 @@ var SddRunRegistry = class {
|
|
|
1975
1975
|
import { TaskTracker as TaskTracker2 } from "@wrongstack/core/tasking";
|
|
1976
1976
|
|
|
1977
1977
|
// src/spec-builder.ts
|
|
1978
|
-
import { ERROR_CODES as
|
|
1979
|
-
import { expectDefined as
|
|
1978
|
+
import { ERROR_CODES as ERROR_CODES3, SddError as SddError3 } from "@wrongstack/core/types";
|
|
1979
|
+
import { expectDefined as expectDefined3 } from "@wrongstack/core/utils";
|
|
1980
1980
|
|
|
1981
1981
|
// src/sdd-session-types.ts
|
|
1982
1982
|
function isAISpecSession(value) {
|
|
@@ -1985,7 +1985,122 @@ function isAISpecSession(value) {
|
|
|
1985
1985
|
return typeof session.id === "string" && typeof session.phase === "string" && typeof session.title === "string" && typeof session.userIntent === "string" && Array.isArray(session.answers) && typeof session.updatedAt === "number";
|
|
1986
1986
|
}
|
|
1987
1987
|
|
|
1988
|
-
// src/spec-builder.ts
|
|
1988
|
+
// src/spec-builder-parsing.ts
|
|
1989
|
+
import { ERROR_CODES as ERROR_CODES2, SddError as SddError2 } from "@wrongstack/core/types";
|
|
1990
|
+
import { toErrorMessage } from "@wrongstack/core/utils";
|
|
1991
|
+
function parseSpecificationJSON(jsonStr, fallbackTitle, sessionId) {
|
|
1992
|
+
let parsed;
|
|
1993
|
+
try {
|
|
1994
|
+
parsed = JSON.parse(jsonStr);
|
|
1995
|
+
} catch (e) {
|
|
1996
|
+
throw new SddError2({
|
|
1997
|
+
message: "Invalid JSON for spec",
|
|
1998
|
+
code: ERROR_CODES2.SDD_PARSE_FAILED,
|
|
1999
|
+
cause: e,
|
|
2000
|
+
context: { detail: toErrorMessage(e) }
|
|
2001
|
+
});
|
|
2002
|
+
}
|
|
2003
|
+
if (!parsed || typeof parsed !== "object") {
|
|
2004
|
+
throw new SddError2({
|
|
2005
|
+
message: "Spec JSON must be an object",
|
|
2006
|
+
code: ERROR_CODES2.SDD_VALIDATION_FAILED,
|
|
2007
|
+
context: { actualType: typeof parsed }
|
|
2008
|
+
});
|
|
2009
|
+
}
|
|
2010
|
+
const raw = parsed;
|
|
2011
|
+
const now = Date.now();
|
|
2012
|
+
const title = String(raw.title ?? fallbackTitle);
|
|
2013
|
+
const overview = String(raw.overview ?? "");
|
|
2014
|
+
if (!overview || overview === "undefined") {
|
|
2015
|
+
throw new SddError2({
|
|
2016
|
+
message: "Spec must have an overview",
|
|
2017
|
+
code: ERROR_CODES2.SDD_VALIDATION_FAILED,
|
|
2018
|
+
context: { field: "overview", title }
|
|
2019
|
+
});
|
|
2020
|
+
}
|
|
2021
|
+
const rawSections = Array.isArray(raw.sections) ? raw.sections : [];
|
|
2022
|
+
const sections = rawSections.filter((s) => s && typeof s === "object").map((s) => ({
|
|
2023
|
+
type: [
|
|
2024
|
+
"overview",
|
|
2025
|
+
"requirements",
|
|
2026
|
+
"architecture",
|
|
2027
|
+
"api",
|
|
2028
|
+
"data",
|
|
2029
|
+
"security",
|
|
2030
|
+
"acceptance"
|
|
2031
|
+
].includes(String(s.type)) ? String(s.type) : "overview",
|
|
2032
|
+
title: String(s.title ?? ""),
|
|
2033
|
+
content: String(s.content ?? ""),
|
|
2034
|
+
level: Number(s.level) || 1
|
|
2035
|
+
}));
|
|
2036
|
+
const rawReqs = Array.isArray(raw.requirements) ? raw.requirements : [];
|
|
2037
|
+
const requirements = rawReqs.filter((r) => r && typeof r === "object").map((r, i) => ({
|
|
2038
|
+
id: String(r.id ?? `REQ-${i + 1}`),
|
|
2039
|
+
type: ["functional", "non-functional", "security", "performance", "ux"].includes(
|
|
2040
|
+
String(r.type)
|
|
2041
|
+
) ? String(r.type) : "functional",
|
|
2042
|
+
priority: ["critical", "high", "medium", "low"].includes(String(r.priority)) ? String(r.priority) : "medium",
|
|
2043
|
+
description: String(r.description ?? ""),
|
|
2044
|
+
acceptanceCriteria: Array.isArray(r.acceptanceCriteria) ? r.acceptanceCriteria.map(String) : []
|
|
2045
|
+
}));
|
|
2046
|
+
const spec = {
|
|
2047
|
+
id: crypto.randomUUID(),
|
|
2048
|
+
title,
|
|
2049
|
+
version: "0.1.0",
|
|
2050
|
+
status: "draft",
|
|
2051
|
+
overview,
|
|
2052
|
+
sections,
|
|
2053
|
+
requirements,
|
|
2054
|
+
createdAt: now,
|
|
2055
|
+
updatedAt: now,
|
|
2056
|
+
metadata: {
|
|
2057
|
+
generatedBy: "AISpecBuilder",
|
|
2058
|
+
sessionId
|
|
2059
|
+
}
|
|
2060
|
+
};
|
|
2061
|
+
return spec;
|
|
2062
|
+
}
|
|
2063
|
+
function extractJSONFromText(text) {
|
|
2064
|
+
const codeBlockMatch = text.match(/```json\s*([\s\S]*?)```/);
|
|
2065
|
+
if (codeBlockMatch?.[1]) {
|
|
2066
|
+
return codeBlockMatch[1].trim();
|
|
2067
|
+
}
|
|
2068
|
+
const genericBlockMatch = text.match(/```\s*([\s\S]*?)```/);
|
|
2069
|
+
if (genericBlockMatch?.[1]) {
|
|
2070
|
+
const trimmed = genericBlockMatch[1].trim();
|
|
2071
|
+
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
|
2072
|
+
return trimmed;
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
const jsonMatch = text.match(/(\{[\s\S]*\})/);
|
|
2076
|
+
if (jsonMatch?.[1]) {
|
|
2077
|
+
try {
|
|
2078
|
+
JSON.parse(jsonMatch[1]);
|
|
2079
|
+
return jsonMatch[1];
|
|
2080
|
+
} catch {
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
return null;
|
|
2084
|
+
}
|
|
2085
|
+
function extractJSONArrayFromText(text) {
|
|
2086
|
+
const codeBlockMatch = text.match(/```json\s*([\s\S]*?)```/);
|
|
2087
|
+
if (codeBlockMatch?.[1]) {
|
|
2088
|
+
const trimmed = codeBlockMatch[1].trim();
|
|
2089
|
+
if (trimmed.startsWith("[")) return trimmed;
|
|
2090
|
+
}
|
|
2091
|
+
const arrayMatch = text.match(/(\[[\s\S]*\])/);
|
|
2092
|
+
if (arrayMatch?.[1]) {
|
|
2093
|
+
try {
|
|
2094
|
+
const parsed = JSON.parse(arrayMatch[1]);
|
|
2095
|
+
if (Array.isArray(parsed)) return arrayMatch[1];
|
|
2096
|
+
} catch {
|
|
2097
|
+
}
|
|
2098
|
+
}
|
|
2099
|
+
return null;
|
|
2100
|
+
}
|
|
2101
|
+
|
|
2102
|
+
// src/spec-builder-prompts.ts
|
|
2103
|
+
import { expectDefined as expectDefined2 } from "@wrongstack/core/utils";
|
|
1989
2104
|
function buildQuestioningPrompt(session, min, max) {
|
|
1990
2105
|
const answered = session.answers.length;
|
|
1991
2106
|
const remaining = Math.max(0, min - answered);
|
|
@@ -2177,6 +2292,34 @@ function buildExecutingPrompt(session) {
|
|
|
2177
2292
|
"Start executing the first pending task now."
|
|
2178
2293
|
].join("\n");
|
|
2179
2294
|
}
|
|
2295
|
+
|
|
2296
|
+
// src/spec-builder-session-file.ts
|
|
2297
|
+
async function writeAISpecSessionFile(sessionPath, snapshot) {
|
|
2298
|
+
const fsp7 = await import("node:fs/promises");
|
|
2299
|
+
const path7 = await import("node:path");
|
|
2300
|
+
const { atomicWrite: atomicWrite4 } = await import("@wrongstack/core/utils");
|
|
2301
|
+
await fsp7.mkdir(path7.dirname(sessionPath), { recursive: true });
|
|
2302
|
+
await atomicWrite4(sessionPath, JSON.stringify(snapshot, null, 2));
|
|
2303
|
+
}
|
|
2304
|
+
async function readAISpecSessionFile(sessionPath) {
|
|
2305
|
+
try {
|
|
2306
|
+
const fsp7 = await import("node:fs/promises");
|
|
2307
|
+
const raw = await fsp7.readFile(sessionPath, "utf8");
|
|
2308
|
+
const loaded = JSON.parse(raw);
|
|
2309
|
+
if (isAISpecSession(loaded)) return loaded;
|
|
2310
|
+
} catch {
|
|
2311
|
+
}
|
|
2312
|
+
return void 0;
|
|
2313
|
+
}
|
|
2314
|
+
async function deleteAISpecSessionFile(sessionPath) {
|
|
2315
|
+
try {
|
|
2316
|
+
const fsp7 = await import("node:fs/promises");
|
|
2317
|
+
await fsp7.unlink(sessionPath);
|
|
2318
|
+
} catch {
|
|
2319
|
+
}
|
|
2320
|
+
}
|
|
2321
|
+
|
|
2322
|
+
// src/spec-builder.ts
|
|
2180
2323
|
var AISpecBuilder = class {
|
|
2181
2324
|
session;
|
|
2182
2325
|
store;
|
|
@@ -2217,12 +2360,7 @@ var AISpecBuilder = class {
|
|
|
2217
2360
|
await this.sessionPersistence.save(snapshot);
|
|
2218
2361
|
return;
|
|
2219
2362
|
}
|
|
2220
|
-
|
|
2221
|
-
const path7 = await import("node:path");
|
|
2222
|
-
const { atomicWrite: atomicWrite4 } = await import("@wrongstack/core/utils");
|
|
2223
|
-
const sessionPath = expectDefined2(this.sessionPath);
|
|
2224
|
-
await fsp7.mkdir(path7.dirname(sessionPath), { recursive: true });
|
|
2225
|
-
await atomicWrite4(sessionPath, JSON.stringify(snapshot, null, 2));
|
|
2363
|
+
await writeAISpecSessionFile(expectDefined3(this.sessionPath), snapshot);
|
|
2226
2364
|
} catch (error) {
|
|
2227
2365
|
console.warn(
|
|
2228
2366
|
JSON.stringify({
|
|
@@ -2239,23 +2377,18 @@ var AISpecBuilder = class {
|
|
|
2239
2377
|
/** Load session state from the configured durable owner. */
|
|
2240
2378
|
async loadSession() {
|
|
2241
2379
|
if (this.sessionPersistence) {
|
|
2242
|
-
const
|
|
2243
|
-
if (isAISpecSession(
|
|
2244
|
-
this.session =
|
|
2380
|
+
const loaded2 = await this.sessionPersistence.load();
|
|
2381
|
+
if (isAISpecSession(loaded2)) {
|
|
2382
|
+
this.session = loaded2;
|
|
2245
2383
|
return true;
|
|
2246
2384
|
}
|
|
2247
2385
|
return false;
|
|
2248
2386
|
}
|
|
2249
2387
|
if (!this.sessionPath) return false;
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
if (isAISpecSession(loaded)) {
|
|
2255
|
-
this.session = loaded;
|
|
2256
|
-
return true;
|
|
2257
|
-
}
|
|
2258
|
-
} catch {
|
|
2388
|
+
const loaded = await readAISpecSessionFile(this.sessionPath);
|
|
2389
|
+
if (loaded) {
|
|
2390
|
+
this.session = loaded;
|
|
2391
|
+
return true;
|
|
2259
2392
|
}
|
|
2260
2393
|
return false;
|
|
2261
2394
|
}
|
|
@@ -2267,11 +2400,7 @@ var AISpecBuilder = class {
|
|
|
2267
2400
|
return;
|
|
2268
2401
|
}
|
|
2269
2402
|
if (!this.sessionPath) return;
|
|
2270
|
-
|
|
2271
|
-
const fsp7 = await import("node:fs/promises");
|
|
2272
|
-
await fsp7.unlink(this.sessionPath);
|
|
2273
|
-
} catch {
|
|
2274
|
-
}
|
|
2403
|
+
await deleteAISpecSessionFile(this.sessionPath);
|
|
2275
2404
|
}
|
|
2276
2405
|
/** Auto-save helper. saveSession() already handles best-effort persistence. */
|
|
2277
2406
|
autoSave() {
|
|
@@ -2363,9 +2492,9 @@ var AISpecBuilder = class {
|
|
|
2363
2492
|
switch (this.session.phase) {
|
|
2364
2493
|
case "questioning":
|
|
2365
2494
|
if (!this.session.spec) {
|
|
2366
|
-
throw new
|
|
2495
|
+
throw new SddError3({
|
|
2367
2496
|
message: "Cannot approve: no spec generated yet.",
|
|
2368
|
-
code:
|
|
2497
|
+
code: ERROR_CODES3.SDD_INVALID_STATE,
|
|
2369
2498
|
context: { phase: "questioning", sessionId: this.session.id }
|
|
2370
2499
|
});
|
|
2371
2500
|
}
|
|
@@ -2499,9 +2628,9 @@ var AISpecBuilder = class {
|
|
|
2499
2628
|
*/
|
|
2500
2629
|
async saveSpec() {
|
|
2501
2630
|
if (!this.session.spec) {
|
|
2502
|
-
throw new
|
|
2631
|
+
throw new SddError3({
|
|
2503
2632
|
message: "No spec to save.",
|
|
2504
|
-
code:
|
|
2633
|
+
code: ERROR_CODES3.SDD_NOT_READY,
|
|
2505
2634
|
context: { sessionId: this.session.id }
|
|
2506
2635
|
});
|
|
2507
2636
|
}
|
|
@@ -2514,101 +2643,13 @@ var AISpecBuilder = class {
|
|
|
2514
2643
|
* Validates and normalizes the structure.
|
|
2515
2644
|
*/
|
|
2516
2645
|
parseSpecFromJSON(jsonStr) {
|
|
2517
|
-
|
|
2518
|
-
try {
|
|
2519
|
-
parsed = JSON.parse(jsonStr);
|
|
2520
|
-
} catch (e) {
|
|
2521
|
-
throw new SddError2({
|
|
2522
|
-
message: "Invalid JSON for spec",
|
|
2523
|
-
code: ERROR_CODES2.SDD_PARSE_FAILED,
|
|
2524
|
-
cause: e,
|
|
2525
|
-
context: { detail: toErrorMessage(e) }
|
|
2526
|
-
});
|
|
2527
|
-
}
|
|
2528
|
-
if (!parsed || typeof parsed !== "object") {
|
|
2529
|
-
throw new SddError2({
|
|
2530
|
-
message: "Spec JSON must be an object",
|
|
2531
|
-
code: ERROR_CODES2.SDD_VALIDATION_FAILED,
|
|
2532
|
-
context: { actualType: typeof parsed }
|
|
2533
|
-
});
|
|
2534
|
-
}
|
|
2535
|
-
const raw = parsed;
|
|
2536
|
-
const now = Date.now();
|
|
2537
|
-
const title = String(raw.title ?? this.session.title);
|
|
2538
|
-
const overview = String(raw.overview ?? "");
|
|
2539
|
-
if (!overview || overview === "undefined") {
|
|
2540
|
-
throw new SddError2({
|
|
2541
|
-
message: "Spec must have an overview",
|
|
2542
|
-
code: ERROR_CODES2.SDD_VALIDATION_FAILED,
|
|
2543
|
-
context: { field: "overview", title }
|
|
2544
|
-
});
|
|
2545
|
-
}
|
|
2546
|
-
const rawSections = Array.isArray(raw.sections) ? raw.sections : [];
|
|
2547
|
-
const sections = rawSections.filter((s) => s && typeof s === "object").map((s) => ({
|
|
2548
|
-
type: [
|
|
2549
|
-
"overview",
|
|
2550
|
-
"requirements",
|
|
2551
|
-
"architecture",
|
|
2552
|
-
"api",
|
|
2553
|
-
"data",
|
|
2554
|
-
"security",
|
|
2555
|
-
"acceptance"
|
|
2556
|
-
].includes(String(s.type)) ? String(s.type) : "overview",
|
|
2557
|
-
title: String(s.title ?? ""),
|
|
2558
|
-
content: String(s.content ?? ""),
|
|
2559
|
-
level: Number(s.level) || 1
|
|
2560
|
-
}));
|
|
2561
|
-
const rawReqs = Array.isArray(raw.requirements) ? raw.requirements : [];
|
|
2562
|
-
const requirements = rawReqs.filter((r) => r && typeof r === "object").map((r, i) => ({
|
|
2563
|
-
id: String(r.id ?? `REQ-${i + 1}`),
|
|
2564
|
-
type: ["functional", "non-functional", "security", "performance", "ux"].includes(
|
|
2565
|
-
String(r.type)
|
|
2566
|
-
) ? String(r.type) : "functional",
|
|
2567
|
-
priority: ["critical", "high", "medium", "low"].includes(String(r.priority)) ? String(r.priority) : "medium",
|
|
2568
|
-
description: String(r.description ?? ""),
|
|
2569
|
-
acceptanceCriteria: Array.isArray(r.acceptanceCriteria) ? r.acceptanceCriteria.map(String) : []
|
|
2570
|
-
}));
|
|
2571
|
-
const spec = {
|
|
2572
|
-
id: crypto.randomUUID(),
|
|
2573
|
-
title,
|
|
2574
|
-
version: "0.1.0",
|
|
2575
|
-
status: "draft",
|
|
2576
|
-
overview,
|
|
2577
|
-
sections,
|
|
2578
|
-
requirements,
|
|
2579
|
-
createdAt: now,
|
|
2580
|
-
updatedAt: now,
|
|
2581
|
-
metadata: {
|
|
2582
|
-
generatedBy: "AISpecBuilder",
|
|
2583
|
-
sessionId: this.session.id
|
|
2584
|
-
}
|
|
2585
|
-
};
|
|
2586
|
-
return spec;
|
|
2646
|
+
return parseSpecificationJSON(jsonStr, this.session.title, this.session.id);
|
|
2587
2647
|
}
|
|
2588
2648
|
/**
|
|
2589
2649
|
* Extract JSON from AI output (handles ```json blocks and raw JSON).
|
|
2590
2650
|
*/
|
|
2591
2651
|
extractJSON(text) {
|
|
2592
|
-
|
|
2593
|
-
if (codeBlockMatch?.[1]) {
|
|
2594
|
-
return codeBlockMatch[1].trim();
|
|
2595
|
-
}
|
|
2596
|
-
const genericBlockMatch = text.match(/```\s*([\s\S]*?)```/);
|
|
2597
|
-
if (genericBlockMatch?.[1]) {
|
|
2598
|
-
const trimmed = genericBlockMatch[1].trim();
|
|
2599
|
-
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
|
2600
|
-
return trimmed;
|
|
2601
|
-
}
|
|
2602
|
-
}
|
|
2603
|
-
const jsonMatch = text.match(/(\{[\s\S]*\})/);
|
|
2604
|
-
if (jsonMatch?.[1]) {
|
|
2605
|
-
try {
|
|
2606
|
-
JSON.parse(jsonMatch[1]);
|
|
2607
|
-
return jsonMatch[1];
|
|
2608
|
-
} catch {
|
|
2609
|
-
}
|
|
2610
|
-
}
|
|
2611
|
-
return null;
|
|
2652
|
+
return extractJSONFromText(text);
|
|
2612
2653
|
}
|
|
2613
2654
|
/**
|
|
2614
2655
|
* Detect if AI output contains a spec (JSON block).
|
|
@@ -2634,20 +2675,7 @@ var AISpecBuilder = class {
|
|
|
2634
2675
|
* Extract a JSON array from AI output (for task lists).
|
|
2635
2676
|
*/
|
|
2636
2677
|
extractJSONArray(text) {
|
|
2637
|
-
|
|
2638
|
-
if (codeBlockMatch?.[1]) {
|
|
2639
|
-
const trimmed = codeBlockMatch[1].trim();
|
|
2640
|
-
if (trimmed.startsWith("[")) return trimmed;
|
|
2641
|
-
}
|
|
2642
|
-
const arrayMatch = text.match(/(\[[\s\S]*\])/);
|
|
2643
|
-
if (arrayMatch?.[1]) {
|
|
2644
|
-
try {
|
|
2645
|
-
const parsed = JSON.parse(arrayMatch[1]);
|
|
2646
|
-
if (Array.isArray(parsed)) return arrayMatch[1];
|
|
2647
|
-
} catch {
|
|
2648
|
-
}
|
|
2649
|
-
}
|
|
2650
|
-
return null;
|
|
2678
|
+
return extractJSONArrayFromText(text);
|
|
2651
2679
|
}
|
|
2652
2680
|
};
|
|
2653
2681
|
|
|
@@ -2991,95 +3019,341 @@ import {
|
|
|
2991
3019
|
writeKanbanWorkflowState
|
|
2992
3020
|
} from "@wrongstack/kanban";
|
|
2993
3021
|
|
|
2994
|
-
// src/
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
const
|
|
3001
|
-
const
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
const
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
});
|
|
3016
|
-
const coordinator = params.coordinator;
|
|
3017
|
-
const subagentId = params.nextSubagentId();
|
|
3018
|
-
const correlationId = randomUUID2();
|
|
3019
|
-
const meta = task.metadata ?? {};
|
|
3020
|
-
const model = (typeof meta.model === "string" ? meta.model : void 0) ?? opts.defaultModel;
|
|
3021
|
-
const provider = (typeof meta.provider === "string" ? meta.provider : void 0) ?? opts.defaultProvider;
|
|
3022
|
-
const fallbackModels = Array.isArray(meta.fallbackModels) ? meta.fallbackModels : opts.fallbackModels;
|
|
3023
|
-
const spawnResult = await coordinator.spawn({
|
|
3024
|
-
id: subagentId,
|
|
3025
|
-
name: agentName,
|
|
3026
|
-
role: "executor",
|
|
3027
|
-
idleTimeoutMs: params.idleTimeoutMs,
|
|
3028
|
-
...params.timeoutMs ? { timeoutMs: params.timeoutMs } : {},
|
|
3029
|
-
cwd: params.taskCwds.get(taskId),
|
|
3030
|
-
disabledTools: ["delegate"],
|
|
3031
|
-
...model ? { model } : {},
|
|
3032
|
-
...provider ? { provider } : {},
|
|
3033
|
-
...fallbackModels?.length ? { fallbackModels } : {}
|
|
3022
|
+
// src/graph-split.ts
|
|
3023
|
+
function splitGraphNode(tracker, taskId, subtasks, options = {}) {
|
|
3024
|
+
const node = tracker.getNode(taskId);
|
|
3025
|
+
if (!node) return [];
|
|
3026
|
+
if (node.status === "in_progress" || options.isRunning?.(taskId)) return [];
|
|
3027
|
+
if (!subtasks.length) return [];
|
|
3028
|
+
const blockers = tracker.getBlockers(taskId);
|
|
3029
|
+
const dependents = tracker.getDependents(taskId);
|
|
3030
|
+
const leafIds = subtasks.map((s) => {
|
|
3031
|
+
const criterion = s.successCriterion?.trim();
|
|
3032
|
+
const description = criterion ? `${s.description}
|
|
3033
|
+
|
|
3034
|
+
**Acceptance Criteria:**
|
|
3035
|
+
- ${criterion}` : s.description;
|
|
3036
|
+
return tracker.addNode({
|
|
3037
|
+
title: s.title,
|
|
3038
|
+
description,
|
|
3039
|
+
type: s.type ?? node.type,
|
|
3040
|
+
priority: s.priority ?? node.priority,
|
|
3041
|
+
status: "pending",
|
|
3042
|
+
parentId: taskId
|
|
3043
|
+
}).id;
|
|
3034
3044
|
});
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3045
|
+
for (const leaf of leafIds) {
|
|
3046
|
+
for (const b of blockers) tracker.addDependency(b, leaf);
|
|
3047
|
+
for (const dep of dependents) tracker.addDependency(leaf, dep);
|
|
3048
|
+
}
|
|
3049
|
+
tracker.updateNodeStatus(taskId, "completed", `split into ${leafIds.length} subtasks`);
|
|
3050
|
+
return leafIds;
|
|
3051
|
+
}
|
|
3052
|
+
|
|
3053
|
+
// src/sdd-parallel-controls.ts
|
|
3054
|
+
async function cleanupWorktrees(host) {
|
|
3055
|
+
if (host.isRunning()) return 0;
|
|
3056
|
+
const wt = host.opts.worktrees;
|
|
3057
|
+
if (!wt) return 0;
|
|
3058
|
+
for (const [taskId, handle] of [...host.taskWorktrees]) {
|
|
3059
|
+
await wt.release(handle, { keep: false }).catch(() => {
|
|
3039
3060
|
});
|
|
3061
|
+
host.forgetWorktree(taskId);
|
|
3040
3062
|
}
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3063
|
+
const { removed } = await wt.cleanupAllManaged();
|
|
3064
|
+
return removed;
|
|
3065
|
+
}
|
|
3066
|
+
async function rollback(host) {
|
|
3067
|
+
if (host.isRunning())
|
|
3068
|
+
return { ok: false, reverted: 0, reason: "run still active \u2014 stop it first" };
|
|
3069
|
+
const wt = host.opts.worktrees;
|
|
3070
|
+
if (!wt || !host.baseBranch) {
|
|
3071
|
+
return { ok: false, reverted: 0, reason: "no worktree run to roll back" };
|
|
3072
|
+
}
|
|
3073
|
+
return wt.revertCommits(
|
|
3074
|
+
host.baseBranch,
|
|
3075
|
+
host.mergedCommits.map((c) => c.sha)
|
|
3076
|
+
);
|
|
3077
|
+
}
|
|
3078
|
+
function retryTask(host, taskId) {
|
|
3079
|
+
if (!host.opts.tracker.getNode(taskId)) return false;
|
|
3080
|
+
host.retryMap.delete(taskId);
|
|
3081
|
+
host.persistRetries(taskId, 0);
|
|
3082
|
+
host.cancelledTasks.delete(taskId);
|
|
3083
|
+
host.opts.tracker.patchMetadata(taskId, { cancelled: void 0 });
|
|
3084
|
+
host.opts.tracker.updateNodeStatus(taskId, "pending", "manual retry");
|
|
3085
|
+
return true;
|
|
3086
|
+
}
|
|
3087
|
+
function reassignTask(host, taskId, agentName) {
|
|
3088
|
+
if (!host.opts.tracker.getNode(taskId)) return false;
|
|
3089
|
+
host.opts.tracker.updateNode(taskId, { assignee: agentName });
|
|
3090
|
+
return true;
|
|
3091
|
+
}
|
|
3092
|
+
function setTaskModel(host, taskId, model, provider) {
|
|
3093
|
+
if (!host.opts.tracker.getNode(taskId)) return false;
|
|
3094
|
+
host.opts.tracker.patchMetadata(taskId, {
|
|
3095
|
+
model,
|
|
3096
|
+
...provider !== void 0 ? { provider } : {}
|
|
3097
|
+
});
|
|
3098
|
+
return true;
|
|
3099
|
+
}
|
|
3100
|
+
function setTaskFallbacks(host, taskId, fallbackModels) {
|
|
3101
|
+
if (!host.opts.tracker.getNode(taskId)) return false;
|
|
3102
|
+
host.opts.tracker.patchMetadata(taskId, { fallbackModels });
|
|
3103
|
+
return true;
|
|
3104
|
+
}
|
|
3105
|
+
function setTaskVerification(host, taskId, verificationCommand) {
|
|
3106
|
+
if (!host.opts.tracker.getNode(taskId)) return false;
|
|
3107
|
+
const cmd = verificationCommand?.trim();
|
|
3108
|
+
host.opts.tracker.patchMetadata(taskId, { verificationCommand: cmd ? cmd : void 0 });
|
|
3109
|
+
return true;
|
|
3110
|
+
}
|
|
3111
|
+
async function cancelTask(host, taskId) {
|
|
3112
|
+
const node = host.opts.tracker.getNode(taskId);
|
|
3113
|
+
if (!node) return false;
|
|
3114
|
+
if (node.status === "completed") return false;
|
|
3115
|
+
host.cancelledTasks.add(taskId);
|
|
3116
|
+
host.opts.tracker.patchMetadata(taskId, { cancelled: true });
|
|
3117
|
+
host.opts.tracker.updateNodeStatus(taskId, "failed", "cancelled by user");
|
|
3118
|
+
host.emit("sdd.task.failed", {
|
|
3119
|
+
runId: host.runId,
|
|
3044
3120
|
taskId,
|
|
3045
|
-
subagentId,
|
|
3046
|
-
|
|
3047
|
-
worktreeBranch: params.taskBranches.get(taskId)
|
|
3121
|
+
subagentId: "",
|
|
3122
|
+
error: "cancelled by user"
|
|
3048
3123
|
});
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3124
|
+
const subagentId = host.taskSubagents.get(taskId);
|
|
3125
|
+
if (subagentId && host.coordinator) {
|
|
3126
|
+
await host.coordinator.stop(subagentId).catch(() => {
|
|
3127
|
+
});
|
|
3128
|
+
}
|
|
3129
|
+
return true;
|
|
3130
|
+
}
|
|
3131
|
+
function deleteTask(host, taskId) {
|
|
3132
|
+
const node = host.opts.tracker.getNode(taskId);
|
|
3133
|
+
if (!node) return false;
|
|
3134
|
+
if (node.status === "in_progress" || host.taskSubagents.has(taskId)) return false;
|
|
3135
|
+
host.cancelledTasks.delete(taskId);
|
|
3136
|
+
host.retryMap.delete(taskId);
|
|
3137
|
+
return host.opts.tracker.removeNode(taskId);
|
|
3138
|
+
}
|
|
3139
|
+
function splitTask(host, taskId, subtasks) {
|
|
3140
|
+
const leafIds = splitGraphNode(host.opts.tracker, taskId, subtasks, {
|
|
3141
|
+
isRunning: (id) => host.taskSubagents.has(id)
|
|
3059
3142
|
});
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3143
|
+
if (!leafIds.length) return [];
|
|
3144
|
+
host.retryMap.delete(taskId);
|
|
3145
|
+
host.persistRetries(taskId, 0);
|
|
3146
|
+
host.emit("sdd.task.split", { runId: host.runId, taskId, subtaskIds: leafIds });
|
|
3147
|
+
return leafIds;
|
|
3148
|
+
}
|
|
3149
|
+
|
|
3150
|
+
// src/sdd-parallel-run-state.ts
|
|
3151
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
3152
|
+
import {
|
|
3153
|
+
DefaultMultiAgentCoordinator,
|
|
3154
|
+
makeAgentSubagentRunner,
|
|
3155
|
+
withDisabledToolFiltering
|
|
3156
|
+
} from "@wrongstack/core/coordination";
|
|
3157
|
+
import { requireSessionId } from "@wrongstack/primitives";
|
|
3158
|
+
|
|
3159
|
+
// src/sdd-task-decomposer.ts
|
|
3160
|
+
var SddTaskDecomposer = class {
|
|
3161
|
+
constructor(tracker, _graph, opts = {}) {
|
|
3162
|
+
this.tracker = tracker;
|
|
3163
|
+
this.slots = Math.min(16, Math.max(1, opts.parallelSlots ?? 4));
|
|
3074
3164
|
}
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3165
|
+
tracker;
|
|
3166
|
+
slots;
|
|
3167
|
+
wave = 0;
|
|
3168
|
+
// -------------------------------------------------------------------
|
|
3169
|
+
// Public API
|
|
3170
|
+
// -------------------------------------------------------------------
|
|
3171
|
+
/**
|
|
3172
|
+
* Return the next batch of runnable tasks.
|
|
3173
|
+
* Returns `allDone: true` when every node is completed.
|
|
3174
|
+
* Returns `deadlocked: true` when no batch can be produced because
|
|
3175
|
+
* all remaining tasks are blocked by failed nodes.
|
|
3176
|
+
*/
|
|
3177
|
+
nextBatch() {
|
|
3178
|
+
if (this.isDone()) {
|
|
3179
|
+
return { tasks: [], wave: this.wave, allDone: true, deadlocked: false };
|
|
3180
|
+
}
|
|
3181
|
+
const pending = this.pendingReadyNodes();
|
|
3182
|
+
if (pending.length === 0) {
|
|
3183
|
+
const hasBlockedTasks = this.hasAnyBlockedTasks();
|
|
3184
|
+
return { tasks: [], wave: this.wave, allDone: false, deadlocked: hasBlockedTasks };
|
|
3185
|
+
}
|
|
3186
|
+
const batch = pending.slice(0, this.slots);
|
|
3187
|
+
return { tasks: batch, wave: this.wave, allDone: false, deadlocked: false };
|
|
3079
3188
|
}
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3189
|
+
/**
|
|
3190
|
+
* Advance the wave counter after a batch completes.
|
|
3191
|
+
* Call this once per `nextBatch()` result that was fan-out.
|
|
3192
|
+
*/
|
|
3193
|
+
acknowledgeBatch(_completedTaskIds) {
|
|
3194
|
+
this.wave++;
|
|
3195
|
+
}
|
|
3196
|
+
/**
|
|
3197
|
+
* True when every node in the graph is completed.
|
|
3198
|
+
* Use this to exit the fan-out loop after `isDone() || deadlocked`.
|
|
3199
|
+
*/
|
|
3200
|
+
isDone() {
|
|
3201
|
+
const progress = this.tracker.getProgress();
|
|
3202
|
+
return progress.total > 0 && progress.completed === progress.total;
|
|
3203
|
+
}
|
|
3204
|
+
/**
|
|
3205
|
+
* Total waves produced so far.
|
|
3206
|
+
*/
|
|
3207
|
+
getWaveCount() {
|
|
3208
|
+
return this.wave;
|
|
3209
|
+
}
|
|
3210
|
+
/**
|
|
3211
|
+
* All ready (dependency-satisfied) pending tasks, priority-sorted — UNSLICED.
|
|
3212
|
+
* The continuous scheduler fills its own free slots from this list, so unlike
|
|
3213
|
+
* `nextBatch()` it does not cap at `slots`.
|
|
3214
|
+
*/
|
|
3215
|
+
readyNodes() {
|
|
3216
|
+
return this.pendingReadyNodes();
|
|
3217
|
+
}
|
|
3218
|
+
/**
|
|
3219
|
+
* True when every node has reached a terminal state (completed or failed).
|
|
3220
|
+
* This — not `isDone()` (which requires ALL completed) — is the correct loop
|
|
3221
|
+
* exit for the continuous scheduler: a terminally-failed task must not keep
|
|
3222
|
+
* the run spinning to its backstop.
|
|
3223
|
+
*/
|
|
3224
|
+
isSettled() {
|
|
3225
|
+
const nodes = this.tracker.getAllNodes();
|
|
3226
|
+
return nodes.length > 0 && nodes.every((n) => n.status === "completed" || n.status === "failed");
|
|
3227
|
+
}
|
|
3228
|
+
// -------------------------------------------------------------------
|
|
3229
|
+
// Internal helpers
|
|
3230
|
+
// -------------------------------------------------------------------
|
|
3231
|
+
/**
|
|
3232
|
+
* Return pending nodes whose blockers are all completed.
|
|
3233
|
+
* Sorted by priority (critical first), then by creation time.
|
|
3234
|
+
*/
|
|
3235
|
+
pendingReadyNodes() {
|
|
3236
|
+
const allPending = this.tracker.getAllNodes({ status: ["pending"] });
|
|
3237
|
+
const ready = [];
|
|
3238
|
+
for (const node of allPending) {
|
|
3239
|
+
if (this.tracker.canStart(node.id)) {
|
|
3240
|
+
ready.push(node);
|
|
3241
|
+
}
|
|
3242
|
+
}
|
|
3243
|
+
const priorityRank = {
|
|
3244
|
+
critical: 0,
|
|
3245
|
+
high: 1,
|
|
3246
|
+
medium: 2,
|
|
3247
|
+
low: 3
|
|
3248
|
+
};
|
|
3249
|
+
ready.sort((a, b) => {
|
|
3250
|
+
const pr = priorityRank[a.priority] - priorityRank[b.priority];
|
|
3251
|
+
if (pr !== 0) return pr;
|
|
3252
|
+
return a.createdAt - b.createdAt;
|
|
3253
|
+
});
|
|
3254
|
+
return ready;
|
|
3255
|
+
}
|
|
3256
|
+
/** True when non-completed, non-failed tasks remain blocked and cannot make progress. */
|
|
3257
|
+
hasAnyBlockedTasks() {
|
|
3258
|
+
const nodes = this.tracker.getAllNodes({
|
|
3259
|
+
status: ["pending", "in_progress", "blocked"]
|
|
3260
|
+
});
|
|
3261
|
+
if (nodes.some((n) => n.status === "in_progress")) {
|
|
3262
|
+
return false;
|
|
3263
|
+
}
|
|
3264
|
+
return nodes.some((n) => n.status === "blocked" || n.status === "pending");
|
|
3265
|
+
}
|
|
3266
|
+
};
|
|
3267
|
+
|
|
3268
|
+
// src/sdd-task-execution.ts
|
|
3269
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
3270
|
+
import { expectDefined as expectDefined4 } from "@wrongstack/core/utils";
|
|
3271
|
+
import { assignNickname } from "@wrongstack/core/coordination";
|
|
3272
|
+
import { ERROR_CODES as ERROR_CODES4, SddError as SddError4 } from "@wrongstack/core/types";
|
|
3273
|
+
async function executeSddTask(params) {
|
|
3274
|
+
const { task, opts } = params;
|
|
3275
|
+
const taskId = task.id;
|
|
3276
|
+
let agentName = task.assignee;
|
|
3277
|
+
if (!agentName) {
|
|
3278
|
+
const nick = assignNickname("executor", params.usedNicknames);
|
|
3279
|
+
params.usedNicknames.add(nick.key);
|
|
3280
|
+
agentName = nick.display.replace(/\s*\([^)]*\)\s*$/, "");
|
|
3281
|
+
opts.tracker.updateNode(taskId, { assignee: agentName });
|
|
3282
|
+
}
|
|
3283
|
+
opts.tracker.updateNodeStatus(taskId, "in_progress");
|
|
3284
|
+
await params.allocateWorktrees([task]);
|
|
3285
|
+
if (!params.coordinator)
|
|
3286
|
+
throw new SddError4({
|
|
3287
|
+
message: "SDD parallel runner requires a coordinator",
|
|
3288
|
+
code: ERROR_CODES4.SDD_INVALID_STATE
|
|
3289
|
+
});
|
|
3290
|
+
const coordinator = params.coordinator;
|
|
3291
|
+
const subagentId = params.nextSubagentId();
|
|
3292
|
+
const correlationId = randomUUID2();
|
|
3293
|
+
const meta = task.metadata ?? {};
|
|
3294
|
+
const model = (typeof meta.model === "string" ? meta.model : void 0) ?? opts.defaultModel;
|
|
3295
|
+
const provider = (typeof meta.provider === "string" ? meta.provider : void 0) ?? opts.defaultProvider;
|
|
3296
|
+
const fallbackModels = Array.isArray(meta.fallbackModels) ? meta.fallbackModels : opts.fallbackModels;
|
|
3297
|
+
const spawnResult = await coordinator.spawn({
|
|
3298
|
+
id: subagentId,
|
|
3299
|
+
name: agentName,
|
|
3300
|
+
role: "executor",
|
|
3301
|
+
idleTimeoutMs: params.idleTimeoutMs,
|
|
3302
|
+
...params.timeoutMs ? { timeoutMs: params.timeoutMs } : {},
|
|
3303
|
+
cwd: params.taskCwds.get(taskId),
|
|
3304
|
+
disabledTools: ["delegate"],
|
|
3305
|
+
...model ? { model } : {},
|
|
3306
|
+
...provider ? { provider } : {},
|
|
3307
|
+
...fallbackModels?.length ? { fallbackModels } : {}
|
|
3308
|
+
});
|
|
3309
|
+
if (!spawnResult.subagentId) {
|
|
3310
|
+
throw new SddError4({
|
|
3311
|
+
message: "One or more subagent spawns failed",
|
|
3312
|
+
code: ERROR_CODES4.SDD_INVALID_STATE
|
|
3313
|
+
});
|
|
3314
|
+
}
|
|
3315
|
+
params.taskSubagents.set(taskId, subagentId);
|
|
3316
|
+
params.emit("sdd.task.started", {
|
|
3317
|
+
runId: params.runId,
|
|
3318
|
+
taskId,
|
|
3319
|
+
subagentId,
|
|
3320
|
+
agentName,
|
|
3321
|
+
worktreeBranch: params.taskBranches.get(taskId)
|
|
3322
|
+
});
|
|
3323
|
+
await coordinator.assign({
|
|
3324
|
+
id: correlationId,
|
|
3325
|
+
description: buildTaskDirective(opts.graph.title, task),
|
|
3326
|
+
subagentId,
|
|
3327
|
+
...params.timeoutMs ? { timeoutMs: params.timeoutMs } : {},
|
|
3328
|
+
context: {
|
|
3329
|
+
telemetryTaskId: taskId,
|
|
3330
|
+
telemetryRunId: params.runId,
|
|
3331
|
+
telemetryBoardId: opts.graph.id
|
|
3332
|
+
}
|
|
3333
|
+
});
|
|
3334
|
+
let result;
|
|
3335
|
+
try {
|
|
3336
|
+
const got = await coordinator.awaitTasks([correlationId]);
|
|
3337
|
+
result = expectDefined4(got[0]);
|
|
3338
|
+
} catch (err) {
|
|
3339
|
+
result = {
|
|
3340
|
+
subagentId,
|
|
3341
|
+
taskId: correlationId,
|
|
3342
|
+
status: "failed",
|
|
3343
|
+
error: { kind: "unknown", message: String(err), retryable: false },
|
|
3344
|
+
iterations: 0,
|
|
3345
|
+
toolCalls: 0,
|
|
3346
|
+
durationMs: 0
|
|
3347
|
+
};
|
|
3348
|
+
}
|
|
3349
|
+
params.taskSubagents.delete(taskId);
|
|
3350
|
+
if (params.cancelledTasks.has(taskId)) {
|
|
3351
|
+
await params.resolveWorktrees([task]);
|
|
3352
|
+
return { taskId, success: false, result };
|
|
3353
|
+
}
|
|
3354
|
+
const verificationFailReason = await verifyTaskResult(params, result);
|
|
3355
|
+
let success = false;
|
|
3356
|
+
if (result.status === "success" && !verificationFailReason) {
|
|
3083
3357
|
const merged = await params.integrateWorktree(task, result);
|
|
3084
3358
|
if (merged.ok) {
|
|
3085
3359
|
success = true;
|
|
@@ -3268,6 +3542,9 @@ async function trySupervisorRescue(taskId, errMsg) {
|
|
|
3268
3542
|
return true;
|
|
3269
3543
|
}
|
|
3270
3544
|
if (verdict.action === "split") {
|
|
3545
|
+
if (this.opts.tracker.getNode(taskId)?.status === "in_progress") {
|
|
3546
|
+
this.opts.tracker.updateNodeStatus(taskId, "pending", "supervisor split");
|
|
3547
|
+
}
|
|
3271
3548
|
const ids = this.splitTask(taskId, verdict.subtasks);
|
|
3272
3549
|
if (ids.length === 0) return false;
|
|
3273
3550
|
this.emit("sdd.supervisor.decision", { runId: this.runId, taskId, action: "split" });
|
|
@@ -3341,179 +3618,30 @@ function resetOrphans(tracker) {
|
|
|
3341
3618
|
return n;
|
|
3342
3619
|
}
|
|
3343
3620
|
|
|
3344
|
-
// src/sdd-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
withDisabledToolFiltering
|
|
3350
|
-
} from "@wrongstack/core/coordination";
|
|
3351
|
-
import { requireSessionId } from "@wrongstack/primitives";
|
|
3352
|
-
|
|
3353
|
-
// src/graph-split.ts
|
|
3354
|
-
function splitGraphNode(tracker, taskId, subtasks, options = {}) {
|
|
3355
|
-
const node = tracker.getNode(taskId);
|
|
3356
|
-
if (!node) return [];
|
|
3357
|
-
if (node.status === "in_progress" || options.isRunning?.(taskId)) return [];
|
|
3358
|
-
if (!subtasks.length) return [];
|
|
3359
|
-
const blockers = tracker.getBlockers(taskId);
|
|
3360
|
-
const dependents = tracker.getDependents(taskId);
|
|
3361
|
-
const leafIds = subtasks.map((s) => {
|
|
3362
|
-
const criterion = s.successCriterion?.trim();
|
|
3363
|
-
const description = criterion ? `${s.description}
|
|
3364
|
-
|
|
3365
|
-
**Acceptance Criteria:**
|
|
3366
|
-
- ${criterion}` : s.description;
|
|
3367
|
-
return tracker.addNode({
|
|
3368
|
-
title: s.title,
|
|
3369
|
-
description,
|
|
3370
|
-
type: s.type ?? node.type,
|
|
3371
|
-
priority: s.priority ?? node.priority,
|
|
3372
|
-
status: "pending",
|
|
3373
|
-
parentId: taskId
|
|
3374
|
-
}).id;
|
|
3375
|
-
});
|
|
3376
|
-
for (const leaf of leafIds) {
|
|
3377
|
-
for (const b of blockers) tracker.addDependency(b, leaf);
|
|
3378
|
-
for (const dep of dependents) tracker.addDependency(leaf, dep);
|
|
3379
|
-
}
|
|
3380
|
-
tracker.updateNodeStatus(taskId, "completed", `split into ${leafIds.length} subtasks`);
|
|
3381
|
-
return leafIds;
|
|
3621
|
+
// src/sdd-worktree-integration.ts
|
|
3622
|
+
function forgetTaskWorktree(state, taskId, opts = {}) {
|
|
3623
|
+
state.taskWorktrees.delete(taskId);
|
|
3624
|
+
state.taskCwds.delete(taskId);
|
|
3625
|
+
if (!opts.keepBranchLabel) state.taskBranches.delete(taskId);
|
|
3382
3626
|
}
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
*/
|
|
3402
|
-
nextBatch() {
|
|
3403
|
-
if (this.isDone()) {
|
|
3404
|
-
return { tasks: [], wave: this.wave, allDone: true, deadlocked: false };
|
|
3405
|
-
}
|
|
3406
|
-
const pending = this.pendingReadyNodes();
|
|
3407
|
-
if (pending.length === 0) {
|
|
3408
|
-
const hasBlockedTasks = this.hasAnyBlockedTasks();
|
|
3409
|
-
return { tasks: [], wave: this.wave, allDone: false, deadlocked: hasBlockedTasks };
|
|
3410
|
-
}
|
|
3411
|
-
const batch = pending.slice(0, this.slots);
|
|
3412
|
-
return { tasks: batch, wave: this.wave, allDone: false, deadlocked: false };
|
|
3413
|
-
}
|
|
3414
|
-
/**
|
|
3415
|
-
* Advance the wave counter after a batch completes.
|
|
3416
|
-
* Call this once per `nextBatch()` result that was fan-out.
|
|
3417
|
-
*/
|
|
3418
|
-
acknowledgeBatch(_completedTaskIds) {
|
|
3419
|
-
this.wave++;
|
|
3420
|
-
}
|
|
3421
|
-
/**
|
|
3422
|
-
* True when every node in the graph is completed.
|
|
3423
|
-
* Use this to exit the fan-out loop after `isDone() || deadlocked`.
|
|
3424
|
-
*/
|
|
3425
|
-
isDone() {
|
|
3426
|
-
const progress = this.tracker.getProgress();
|
|
3427
|
-
return progress.total > 0 && progress.completed === progress.total;
|
|
3428
|
-
}
|
|
3429
|
-
/**
|
|
3430
|
-
* Total waves produced so far.
|
|
3431
|
-
*/
|
|
3432
|
-
getWaveCount() {
|
|
3433
|
-
return this.wave;
|
|
3434
|
-
}
|
|
3435
|
-
/**
|
|
3436
|
-
* All ready (dependency-satisfied) pending tasks, priority-sorted — UNSLICED.
|
|
3437
|
-
* The continuous scheduler fills its own free slots from this list, so unlike
|
|
3438
|
-
* `nextBatch()` it does not cap at `slots`.
|
|
3439
|
-
*/
|
|
3440
|
-
readyNodes() {
|
|
3441
|
-
return this.pendingReadyNodes();
|
|
3442
|
-
}
|
|
3443
|
-
/**
|
|
3444
|
-
* True when every node has reached a terminal state (completed or failed).
|
|
3445
|
-
* This — not `isDone()` (which requires ALL completed) — is the correct loop
|
|
3446
|
-
* exit for the continuous scheduler: a terminally-failed task must not keep
|
|
3447
|
-
* the run spinning to its backstop.
|
|
3448
|
-
*/
|
|
3449
|
-
isSettled() {
|
|
3450
|
-
const nodes = this.tracker.getAllNodes();
|
|
3451
|
-
return nodes.length > 0 && nodes.every((n) => n.status === "completed" || n.status === "failed");
|
|
3452
|
-
}
|
|
3453
|
-
// -------------------------------------------------------------------
|
|
3454
|
-
// Internal helpers
|
|
3455
|
-
// -------------------------------------------------------------------
|
|
3456
|
-
/**
|
|
3457
|
-
* Return pending nodes whose blockers are all completed.
|
|
3458
|
-
* Sorted by priority (critical first), then by creation time.
|
|
3459
|
-
*/
|
|
3460
|
-
pendingReadyNodes() {
|
|
3461
|
-
const allPending = this.tracker.getAllNodes({ status: ["pending"] });
|
|
3462
|
-
const ready = [];
|
|
3463
|
-
for (const node of allPending) {
|
|
3464
|
-
if (this.tracker.canStart(node.id)) {
|
|
3465
|
-
ready.push(node);
|
|
3466
|
-
}
|
|
3467
|
-
}
|
|
3468
|
-
const priorityRank = {
|
|
3469
|
-
critical: 0,
|
|
3470
|
-
high: 1,
|
|
3471
|
-
medium: 2,
|
|
3472
|
-
low: 3
|
|
3473
|
-
};
|
|
3474
|
-
ready.sort((a, b) => {
|
|
3475
|
-
const pr = priorityRank[a.priority] - priorityRank[b.priority];
|
|
3476
|
-
if (pr !== 0) return pr;
|
|
3477
|
-
return a.createdAt - b.createdAt;
|
|
3478
|
-
});
|
|
3479
|
-
return ready;
|
|
3480
|
-
}
|
|
3481
|
-
/** True when non-completed, non-failed tasks remain blocked and cannot make progress. */
|
|
3482
|
-
hasAnyBlockedTasks() {
|
|
3483
|
-
const nodes = this.tracker.getAllNodes({
|
|
3484
|
-
status: ["pending", "in_progress", "blocked"]
|
|
3485
|
-
});
|
|
3486
|
-
if (nodes.some((n) => n.status === "in_progress")) {
|
|
3487
|
-
return false;
|
|
3488
|
-
}
|
|
3489
|
-
return nodes.some((n) => n.status === "blocked" || n.status === "pending");
|
|
3490
|
-
}
|
|
3491
|
-
};
|
|
3492
|
-
|
|
3493
|
-
// src/sdd-worktree-integration.ts
|
|
3494
|
-
function forgetTaskWorktree(state, taskId, opts = {}) {
|
|
3495
|
-
state.taskWorktrees.delete(taskId);
|
|
3496
|
-
state.taskCwds.delete(taskId);
|
|
3497
|
-
if (!opts.keepBranchLabel) state.taskBranches.delete(taskId);
|
|
3498
|
-
}
|
|
3499
|
-
async function allocateTaskWorktrees(opts, state, tasks) {
|
|
3500
|
-
const wt = opts.worktrees;
|
|
3501
|
-
if (!wt) return;
|
|
3502
|
-
for (const task of tasks) {
|
|
3503
|
-
if (state.taskWorktrees.has(task.id)) continue;
|
|
3504
|
-
try {
|
|
3505
|
-
const handle = await wt.allocate(`sdd-${task.id}`, {
|
|
3506
|
-
slugHint: task.title,
|
|
3507
|
-
ownerLabel: task.title
|
|
3508
|
-
});
|
|
3509
|
-
if (handle.status === "active") {
|
|
3510
|
-
state.taskWorktrees.set(task.id, handle);
|
|
3511
|
-
state.taskCwds.set(task.id, handle.dir);
|
|
3512
|
-
state.taskBranches.set(task.id, handle.branch);
|
|
3513
|
-
const node = opts.tracker.getNode(task.id);
|
|
3514
|
-
if (node) node.metadata = { ...node.metadata, worktreeBranch: handle.branch };
|
|
3515
|
-
}
|
|
3516
|
-
} catch {
|
|
3627
|
+
async function allocateTaskWorktrees(opts, state, tasks) {
|
|
3628
|
+
const wt = opts.worktrees;
|
|
3629
|
+
if (!wt) return;
|
|
3630
|
+
for (const task of tasks) {
|
|
3631
|
+
if (state.taskWorktrees.has(task.id)) continue;
|
|
3632
|
+
try {
|
|
3633
|
+
const handle = await wt.allocate(`sdd-${task.id}`, {
|
|
3634
|
+
slugHint: task.title,
|
|
3635
|
+
ownerLabel: task.title
|
|
3636
|
+
});
|
|
3637
|
+
if (handle.status === "active") {
|
|
3638
|
+
state.taskWorktrees.set(task.id, handle);
|
|
3639
|
+
state.taskCwds.set(task.id, handle.dir);
|
|
3640
|
+
state.taskBranches.set(task.id, handle.branch);
|
|
3641
|
+
const node = opts.tracker.getNode(task.id);
|
|
3642
|
+
if (node) node.metadata = { ...node.metadata, worktreeBranch: handle.branch };
|
|
3643
|
+
}
|
|
3644
|
+
} catch {
|
|
3517
3645
|
}
|
|
3518
3646
|
}
|
|
3519
3647
|
}
|
|
@@ -3610,15 +3738,19 @@ async function integrateTaskWorktree(params) {
|
|
|
3610
3738
|
await wt.release(handle, { keep: false }).catch(() => {
|
|
3611
3739
|
});
|
|
3612
3740
|
forgetTaskWorktree(state, task.id, { keepBranchLabel: true });
|
|
3613
|
-
return {
|
|
3741
|
+
return {
|
|
3742
|
+
ok: false,
|
|
3743
|
+
conflictFiles: res.conflictFiles ?? [],
|
|
3744
|
+
...res.conflict ? {} : { reason: res.stderr?.trim() || "worktree merge failed" }
|
|
3745
|
+
};
|
|
3614
3746
|
} catch {
|
|
3615
3747
|
forgetTaskWorktree(state, task.id);
|
|
3616
3748
|
return { ok: false, conflictFiles: [] };
|
|
3617
3749
|
}
|
|
3618
3750
|
}
|
|
3619
3751
|
|
|
3620
|
-
// src/sdd-parallel-run.ts
|
|
3621
|
-
var
|
|
3752
|
+
// src/sdd-parallel-run-state.ts
|
|
3753
|
+
var SddParallelRunState = class {
|
|
3622
3754
|
constructor(opts) {
|
|
3623
3755
|
this.opts = opts;
|
|
3624
3756
|
this.slots = Math.min(16, Math.max(1, opts.parallelSlots ?? 2));
|
|
@@ -3697,6 +3829,15 @@ var SddParallelRun = class {
|
|
|
3697
3829
|
/** Monotonic dispatch counter (unique subagent ids) + dispatch-round counter. */
|
|
3698
3830
|
dispatchSeq = 0;
|
|
3699
3831
|
round = 0;
|
|
3832
|
+
/**
|
|
3833
|
+
* True once `run()` has returned. A FINISHED run is not "running" even when
|
|
3834
|
+
* the graph is left unsettled (deadlock exit): `isRunning()` gates
|
|
3835
|
+
* `cleanupWorktrees()` / `rollback()` / the Ctrl+C ladders, and a returned
|
|
3836
|
+
* run kept reporting live, silently no-oping cleanup and refusing rollback
|
|
3837
|
+
* with "run still active" until the user called `stop()` on a run that had
|
|
3838
|
+
* already ended. Cleared again at the top of `run()` so a re-run re-arms.
|
|
3839
|
+
*/
|
|
3840
|
+
runReturned = false;
|
|
3700
3841
|
/** Type-safe emit on the optional EventBus (no-op when unwired). */
|
|
3701
3842
|
emit(event, payload) {
|
|
3702
3843
|
const sessionId = this.currentSessionId();
|
|
@@ -3709,16 +3850,259 @@ var SddParallelRun = class {
|
|
|
3709
3850
|
const value = typeof this.sessionIdSource === "function" ? this.sessionIdSource() : this.sessionIdSource;
|
|
3710
3851
|
return requireSessionId(value, "SDD session operation");
|
|
3711
3852
|
}
|
|
3853
|
+
paused = false;
|
|
3854
|
+
/** Resolvers for tasks parked in `waitWhilePaused`, woken on resume/stop. */
|
|
3855
|
+
pausedWaiters = /* @__PURE__ */ new Set();
|
|
3856
|
+
notifyPausedWaiters() {
|
|
3857
|
+
for (const resolve3 of this.pausedWaiters) resolve3();
|
|
3858
|
+
this.pausedWaiters.clear();
|
|
3859
|
+
}
|
|
3860
|
+
async waitWhilePaused() {
|
|
3861
|
+
while (this.paused && !this.stopRequested) {
|
|
3862
|
+
await new Promise((resolve3) => {
|
|
3863
|
+
this.pausedWaiters.add(resolve3);
|
|
3864
|
+
const safety = setTimeout(() => {
|
|
3865
|
+
if (this.pausedWaiters.delete(resolve3)) resolve3();
|
|
3866
|
+
}, 1e3);
|
|
3867
|
+
safety.unref?.();
|
|
3868
|
+
});
|
|
3869
|
+
}
|
|
3870
|
+
}
|
|
3871
|
+
/**
|
|
3872
|
+
* Compute the blocking chains for a deadlock: every still-incomplete task and
|
|
3873
|
+
* the blockers (by node id) that are NOT completed. Failed blockers are
|
|
3874
|
+
* included since they're the usual deadlock cause once retries are exhausted.
|
|
3875
|
+
*/
|
|
3876
|
+
computeDeadlockChains() {
|
|
3877
|
+
return computeDeadlockChains(this.sddTaskRecoveryHost());
|
|
3878
|
+
}
|
|
3879
|
+
/** Requeue failed tasks that block an incomplete dependent. Returns true if any. */
|
|
3880
|
+
recoverFailedBlockers() {
|
|
3881
|
+
return recoverFailedBlockers(this.sddTaskRecoveryHost());
|
|
3882
|
+
}
|
|
3883
|
+
/**
|
|
3884
|
+
* Requeue every terminal-failed task that the user did NOT cancel, giving each
|
|
3885
|
+
* a fresh `maxRetries` budget. Shared by the automatic end-of-run sweep and
|
|
3886
|
+
* the manual "retry all failed" control. Returns the number requeued.
|
|
3887
|
+
*/
|
|
3888
|
+
requeueFailedTasks(reason = "retry failed sweep") {
|
|
3889
|
+
return requeueFailedTasks(this.sddTaskRecoveryHost(), reason);
|
|
3890
|
+
}
|
|
3891
|
+
/** Restore per-task retry counts persisted in node metadata (resume support). */
|
|
3892
|
+
restoreRetryMap() {
|
|
3893
|
+
restoreRetryMap(this.sddTaskRecoveryHost());
|
|
3894
|
+
}
|
|
3895
|
+
/** Clean teardown after a stop: reset interrupted tasks + release worktrees. */
|
|
3896
|
+
async teardown() {
|
|
3897
|
+
for (const node of this.opts.tracker.getAllNodes({ status: ["in_progress"] })) {
|
|
3898
|
+
this.opts.tracker.updateNodeStatus(node.id, "pending", "run stopped");
|
|
3899
|
+
}
|
|
3900
|
+
const wt = this.opts.worktrees;
|
|
3901
|
+
if (wt) {
|
|
3902
|
+
for (const [taskId, handle] of [...this.taskWorktrees]) {
|
|
3903
|
+
await wt.release(handle, { keep: true }).catch(() => {
|
|
3904
|
+
});
|
|
3905
|
+
this.forgetWorktree(taskId);
|
|
3906
|
+
}
|
|
3907
|
+
}
|
|
3908
|
+
}
|
|
3909
|
+
/**
|
|
3910
|
+
* Hard-stop the run after an unrecoverable error: the base branch is in a
|
|
3911
|
+
* state no retry can fix (e.g. a known-invalid merge that could not be rolled
|
|
3912
|
+
* back), so continuing would contaminate every task forked after this point.
|
|
3913
|
+
* The reason is surfaced on `run()`'s result and the `sdd.run.finished` event.
|
|
3914
|
+
*/
|
|
3915
|
+
abortRun(reason) {
|
|
3916
|
+
this.fatalError = reason;
|
|
3917
|
+
this.stopRequested = true;
|
|
3918
|
+
this.coordinator?.stopAll();
|
|
3919
|
+
}
|
|
3920
|
+
// -------------------------------------------------------------------
|
|
3921
|
+
// Internal
|
|
3922
|
+
// -------------------------------------------------------------------
|
|
3923
|
+
buildCoordinator() {
|
|
3924
|
+
const config = {
|
|
3925
|
+
coordinatorId: `sdd-parallel-${randomUUID3().slice(0, 8)}`,
|
|
3926
|
+
maxConcurrent: this.slots,
|
|
3927
|
+
doneCondition: { type: "all_tasks_done" },
|
|
3928
|
+
// Default budget guard for every spawned worker: idle reaper (resets on
|
|
3929
|
+
// activity) plus the opt-in wall-clock cap when one was configured. This
|
|
3930
|
+
// ensures the reaper applies even if a per-spawn config path is bypassed.
|
|
3931
|
+
defaultBudget: {
|
|
3932
|
+
idleTimeoutMs: this.idleTimeoutMs,
|
|
3933
|
+
...this.timeoutMs ? { timeoutMs: this.timeoutMs } : {}
|
|
3934
|
+
}
|
|
3935
|
+
};
|
|
3936
|
+
this.coordinator = new DefaultMultiAgentCoordinator(config, {
|
|
3937
|
+
sessionId: () => this.currentSessionId()
|
|
3938
|
+
});
|
|
3939
|
+
const baseFactory = this.opts.subagentFactory ?? this.defaultFactory();
|
|
3940
|
+
const filteredFactory = withDisabledToolFiltering(baseFactory);
|
|
3941
|
+
const runner = makeAgentSubagentRunner({
|
|
3942
|
+
factory: filteredFactory,
|
|
3943
|
+
hostEvents: this.events
|
|
3944
|
+
});
|
|
3945
|
+
this.coordinator.setRunner?.(runner);
|
|
3946
|
+
}
|
|
3947
|
+
defaultFactory() {
|
|
3948
|
+
return async (_config) => ({
|
|
3949
|
+
agent: this.opts.agent,
|
|
3950
|
+
events: this.opts.agent.events
|
|
3951
|
+
});
|
|
3952
|
+
}
|
|
3953
|
+
/**
|
|
3954
|
+
* Apply a task failure: retry (→ pending, bump retry count) while attempts
|
|
3955
|
+
* remain, else consult the optional supervisor (which can rescue via
|
|
3956
|
+
* retry/reassign/split), else terminal-fail (→ failed). Shared by the
|
|
3957
|
+
* worker-failure, verification-gate, and merge-conflict paths so all three
|
|
3958
|
+
* negotiate the same retry budget and emit the same events.
|
|
3959
|
+
*/
|
|
3960
|
+
async applyTaskFailure(taskId, subagentId, errMsg) {
|
|
3961
|
+
return applyTaskFailure.call(this.sddTaskDispatchHost(), taskId, subagentId, errMsg);
|
|
3962
|
+
}
|
|
3963
|
+
/**
|
|
3964
|
+
* Consult `superviseFailure` for a task that has exhausted its retries.
|
|
3965
|
+
* Applies the verdict (retry / reassign+retry / split) and returns true when
|
|
3966
|
+
* the task was rescued (caller must NOT terminal-fail it). Bounded per task by
|
|
3967
|
+
* `maxSupervisorEscalations` so an always-"retry" supervisor can't loop forever.
|
|
3968
|
+
*/
|
|
3969
|
+
async trySupervisorRescue(taskId, errMsg) {
|
|
3970
|
+
return trySupervisorRescue.call(this.sddTaskDispatchHost(), taskId, errMsg);
|
|
3971
|
+
}
|
|
3972
|
+
/**
|
|
3973
|
+
* Integrate a verified-successful task's worktree into the base branch.
|
|
3974
|
+
* Commits, squash-merges (optionally running `conflictResolver` first), and on
|
|
3975
|
+
* success releases the worktree. On an UNRESOLVED conflict it returns
|
|
3976
|
+
* `{ok:false}` with the conflicting files so the caller routes the task into
|
|
3977
|
+
* the failure path (a retry forks a fresh worktree off the now-advanced base,
|
|
3978
|
+
* which usually clears the conflict). No-op `{ok:true}` when worktrees are
|
|
3979
|
+
* disabled or none was allocated for this task. Never throws — a merge hiccup
|
|
3980
|
+
* degrades to a (retryable) failure rather than wedging the run.
|
|
3981
|
+
*/
|
|
3982
|
+
get worktreeState() {
|
|
3983
|
+
return {
|
|
3984
|
+
taskCwds: this.taskCwds,
|
|
3985
|
+
taskBranches: this.taskBranches,
|
|
3986
|
+
taskWorktrees: this.taskWorktrees,
|
|
3987
|
+
mergedCommits: this.mergedCommits
|
|
3988
|
+
};
|
|
3989
|
+
}
|
|
3990
|
+
async integrateWorktree(task, result) {
|
|
3991
|
+
return integrateTaskWorktree({
|
|
3992
|
+
opts: this.opts,
|
|
3993
|
+
state: this.worktreeState,
|
|
3994
|
+
task,
|
|
3995
|
+
result,
|
|
3996
|
+
runId: this.runId,
|
|
3997
|
+
emit: this.emit.bind(this),
|
|
3998
|
+
abortRun: this.abortRun.bind(this)
|
|
3999
|
+
});
|
|
4000
|
+
}
|
|
4001
|
+
/** Allocate a fresh git worktree per task in the batch (no-op without a manager). */
|
|
4002
|
+
async allocateWorktrees(tasks) {
|
|
4003
|
+
await allocateTaskWorktrees(this.opts, this.worktreeState, tasks);
|
|
4004
|
+
}
|
|
4005
|
+
/**
|
|
4006
|
+
* Resolve each task's worktree after its result is known. Serialized merges
|
|
4007
|
+
* (one at a time) keep the base branch consistent; the wave structure already
|
|
4008
|
+
* guarantees dependency order (a task's blockers merged in an earlier wave).
|
|
4009
|
+
*/
|
|
4010
|
+
async resolveWorktrees(tasks) {
|
|
4011
|
+
await resolveTaskWorktrees(this.opts, this.worktreeState, tasks);
|
|
4012
|
+
}
|
|
4013
|
+
forgetWorktree(taskId, opts = {}) {
|
|
4014
|
+
forgetTaskWorktree(this.worktreeState, taskId, opts);
|
|
4015
|
+
}
|
|
4016
|
+
/** Persist a task's retry count into node metadata (survives crash → resume). */
|
|
4017
|
+
persistRetries(taskId, retries) {
|
|
4018
|
+
const node = this.opts.tracker.getNode(taskId);
|
|
4019
|
+
if (node) node.metadata = { ...node.metadata, retries };
|
|
4020
|
+
}
|
|
4021
|
+
buildProgress() {
|
|
4022
|
+
const gp = this.opts.tracker.getProgress();
|
|
4023
|
+
const isDeadlocked = !this.decomposer.isDone() && this.decomposer.nextBatch().deadlocked;
|
|
4024
|
+
return {
|
|
4025
|
+
wave: this.decomposer.getWaveCount(),
|
|
4026
|
+
total: gp.total,
|
|
4027
|
+
completed: gp.completed,
|
|
4028
|
+
inProgress: gp.inProgress,
|
|
4029
|
+
failed: gp.failed,
|
|
4030
|
+
blocked: gp.blocked,
|
|
4031
|
+
pending: gp.pending,
|
|
4032
|
+
percent: gp.percentComplete,
|
|
4033
|
+
deadlocked: isDeadlocked
|
|
4034
|
+
};
|
|
4035
|
+
}
|
|
4036
|
+
sddTaskRecoveryHost() {
|
|
4037
|
+
const self = this;
|
|
4038
|
+
return {
|
|
4039
|
+
opts: self.opts,
|
|
4040
|
+
get retryMap() {
|
|
4041
|
+
return self.retryMap;
|
|
4042
|
+
},
|
|
4043
|
+
persistRetries: (...args) => this.persistRetries(...args),
|
|
4044
|
+
get cancelledTasks() {
|
|
4045
|
+
return self.cancelledTasks;
|
|
4046
|
+
},
|
|
4047
|
+
emit: (...args) => this.emit(...args),
|
|
4048
|
+
runId: this.runId,
|
|
4049
|
+
maxRetries: this.maxRetries
|
|
4050
|
+
};
|
|
4051
|
+
}
|
|
4052
|
+
sddTaskDispatchHost() {
|
|
4053
|
+
void this.executeOne;
|
|
4054
|
+
void this.stopRequested;
|
|
4055
|
+
void this.opts;
|
|
4056
|
+
void this.coordinator;
|
|
4057
|
+
void this.usedNicknames;
|
|
4058
|
+
void this.idleTimeoutMs;
|
|
4059
|
+
void this.timeoutMs;
|
|
4060
|
+
void this.runId;
|
|
4061
|
+
void this.dispatchSeq;
|
|
4062
|
+
void this.emit;
|
|
4063
|
+
void this.taskCwds;
|
|
4064
|
+
void this.taskBranches;
|
|
4065
|
+
void this.taskSubagents;
|
|
4066
|
+
void this.cancelledTasks;
|
|
4067
|
+
void this.allocateWorktrees;
|
|
4068
|
+
void this.resolveWorktrees;
|
|
4069
|
+
void this.integrateWorktree;
|
|
4070
|
+
void this.applyTaskFailure;
|
|
4071
|
+
void this.retryMap;
|
|
4072
|
+
void this.persistRetries;
|
|
4073
|
+
void this.maxRetries;
|
|
4074
|
+
void this.trySupervisorRescue;
|
|
4075
|
+
void this.supervisorEscalations;
|
|
4076
|
+
void this.maxSupervisorEscalations;
|
|
4077
|
+
void this.setTaskModel;
|
|
4078
|
+
void this.splitTask;
|
|
4079
|
+
return this;
|
|
4080
|
+
}
|
|
4081
|
+
sddParallelControlsHost() {
|
|
4082
|
+
void {
|
|
4083
|
+
isRunning: this.isRunning,
|
|
4084
|
+
opts: this.opts,
|
|
4085
|
+
taskWorktrees: this.taskWorktrees,
|
|
4086
|
+
forgetWorktree: this.forgetWorktree,
|
|
4087
|
+
baseBranch: this.baseBranch,
|
|
4088
|
+
mergedCommits: this.mergedCommits,
|
|
4089
|
+
retryMap: this.retryMap,
|
|
4090
|
+
persistRetries: this.persistRetries,
|
|
4091
|
+
cancelledTasks: this.cancelledTasks,
|
|
4092
|
+
emit: this.emit,
|
|
4093
|
+
runId: this.runId,
|
|
4094
|
+
taskSubagents: this.taskSubagents,
|
|
4095
|
+
coordinator: this.coordinator
|
|
4096
|
+
};
|
|
4097
|
+
return this;
|
|
4098
|
+
}
|
|
4099
|
+
};
|
|
4100
|
+
|
|
4101
|
+
// src/sdd-parallel-run.ts
|
|
4102
|
+
var SddParallelRun = class extends SddParallelRunState {
|
|
3712
4103
|
// -------------------------------------------------------------------
|
|
3713
4104
|
// Public API
|
|
3714
4105
|
// -------------------------------------------------------------------
|
|
3715
|
-
paused = false;
|
|
3716
|
-
/** Resolvers for tasks parked in `waitWhilePaused`, woken on resume/stop. */
|
|
3717
|
-
pausedWaiters = /* @__PURE__ */ new Set();
|
|
3718
|
-
notifyPausedWaiters() {
|
|
3719
|
-
for (const resolve3 of this.pausedWaiters) resolve3();
|
|
3720
|
-
this.pausedWaiters.clear();
|
|
3721
|
-
}
|
|
3722
4106
|
/** Trigger stop — causes run() to abort after the current wave. */
|
|
3723
4107
|
stop() {
|
|
3724
4108
|
this.stopRequested = true;
|
|
@@ -3738,7 +4122,7 @@ var SddParallelRun = class {
|
|
|
3738
4122
|
return this.paused;
|
|
3739
4123
|
}
|
|
3740
4124
|
isRunning() {
|
|
3741
|
-
return !this.stopRequested && !this.decomposer.isSettled();
|
|
4125
|
+
return !this.stopRequested && !this.runReturned && !this.decomposer.isSettled();
|
|
3742
4126
|
}
|
|
3743
4127
|
/** Base branch the run's squash commits land on (undefined when worktrees off). */
|
|
3744
4128
|
getBaseBranch() {
|
|
@@ -3755,16 +4139,7 @@ var SddParallelRun = class {
|
|
|
3755
4139
|
* (0 when worktrees are disabled). Idempotent.
|
|
3756
4140
|
*/
|
|
3757
4141
|
async cleanupWorktrees() {
|
|
3758
|
-
|
|
3759
|
-
const wt = this.opts.worktrees;
|
|
3760
|
-
if (!wt) return 0;
|
|
3761
|
-
for (const [taskId, handle] of [...this.taskWorktrees]) {
|
|
3762
|
-
await wt.release(handle, { keep: false }).catch(() => {
|
|
3763
|
-
});
|
|
3764
|
-
this.forgetWorktree(taskId);
|
|
3765
|
-
}
|
|
3766
|
-
const { removed } = await wt.cleanupAllManaged();
|
|
3767
|
-
return removed;
|
|
4142
|
+
return cleanupWorktrees(this.sddParallelControlsHost());
|
|
3768
4143
|
}
|
|
3769
4144
|
/**
|
|
3770
4145
|
* Undo the run's merged commits by reverting each on the base branch (history
|
|
@@ -3772,32 +4147,15 @@ var SddParallelRun = class {
|
|
|
3772
4147
|
* revert outcome; a dirty tree or revert conflict surfaces as `ok:false`.
|
|
3773
4148
|
*/
|
|
3774
4149
|
async rollback() {
|
|
3775
|
-
|
|
3776
|
-
return { ok: false, reverted: 0, reason: "run still active \u2014 stop it first" };
|
|
3777
|
-
const wt = this.opts.worktrees;
|
|
3778
|
-
if (!wt || !this.baseBranch) {
|
|
3779
|
-
return { ok: false, reverted: 0, reason: "no worktree run to roll back" };
|
|
3780
|
-
}
|
|
3781
|
-
return wt.revertCommits(
|
|
3782
|
-
this.baseBranch,
|
|
3783
|
-
this.mergedCommits.map((c) => c.sha)
|
|
3784
|
-
);
|
|
4150
|
+
return rollback(this.sddParallelControlsHost());
|
|
3785
4151
|
}
|
|
3786
4152
|
/** Requeue a task to `pending` so the scheduler re-runs it (clears retries + cancel marker). */
|
|
3787
4153
|
retryTask(taskId) {
|
|
3788
|
-
|
|
3789
|
-
this.retryMap.delete(taskId);
|
|
3790
|
-
this.persistRetries(taskId, 0);
|
|
3791
|
-
this.cancelledTasks.delete(taskId);
|
|
3792
|
-
this.opts.tracker.patchMetadata(taskId, { cancelled: void 0 });
|
|
3793
|
-
this.opts.tracker.updateNodeStatus(taskId, "pending", "manual retry");
|
|
3794
|
-
return true;
|
|
4154
|
+
return retryTask(this.sddParallelControlsHost(), taskId);
|
|
3795
4155
|
}
|
|
3796
4156
|
/** Reassign a task to a specific agent name (reflected on the board). */
|
|
3797
4157
|
reassignTask(taskId, agentName) {
|
|
3798
|
-
|
|
3799
|
-
this.opts.tracker.updateNode(taskId, { assignee: agentName });
|
|
3800
|
-
return true;
|
|
4158
|
+
return reassignTask(this.sddParallelControlsHost(), taskId, agentName);
|
|
3801
4159
|
}
|
|
3802
4160
|
/**
|
|
3803
4161
|
* Set/override a task's worker model (and optionally provider) — applied on its
|
|
@@ -3805,18 +4163,11 @@ var SddParallelRun = class {
|
|
|
3805
4163
|
* assignment lives on node metadata so it survives crash → resume.
|
|
3806
4164
|
*/
|
|
3807
4165
|
setTaskModel(taskId, model, provider) {
|
|
3808
|
-
|
|
3809
|
-
this.opts.tracker.patchMetadata(taskId, {
|
|
3810
|
-
model,
|
|
3811
|
-
...provider !== void 0 ? { provider } : {}
|
|
3812
|
-
});
|
|
3813
|
-
return true;
|
|
4166
|
+
return setTaskModel(this.sddParallelControlsHost(), taskId, model, provider);
|
|
3814
4167
|
}
|
|
3815
4168
|
/** Set/override a task's fallback model chain (applied on its next dispatch). */
|
|
3816
4169
|
setTaskFallbacks(taskId, fallbackModels) {
|
|
3817
|
-
|
|
3818
|
-
this.opts.tracker.patchMetadata(taskId, { fallbackModels });
|
|
3819
|
-
return true;
|
|
4170
|
+
return setTaskFallbacks(this.sddParallelControlsHost(), taskId, fallbackModels);
|
|
3820
4171
|
}
|
|
3821
4172
|
/**
|
|
3822
4173
|
* Set/override a task's verification command (the completion gate runs it in
|
|
@@ -3824,10 +4175,7 @@ var SddParallelRun = class {
|
|
|
3824
4175
|
* clears it. Applied on the task's next verification — i.e. its next dispatch.
|
|
3825
4176
|
*/
|
|
3826
4177
|
setTaskVerification(taskId, verificationCommand) {
|
|
3827
|
-
|
|
3828
|
-
const cmd = verificationCommand?.trim();
|
|
3829
|
-
this.opts.tracker.patchMetadata(taskId, { verificationCommand: cmd ? cmd : void 0 });
|
|
3830
|
-
return true;
|
|
4178
|
+
return setTaskVerification(this.sddParallelControlsHost(), taskId, verificationCommand);
|
|
3831
4179
|
}
|
|
3832
4180
|
/**
|
|
3833
4181
|
* Cancel a task. If it is currently running, abort its subagent and mark the
|
|
@@ -3836,24 +4184,7 @@ var SddParallelRun = class {
|
|
|
3836
4184
|
* `retryTask` to bring a cancelled task back. Returns false for an unknown task.
|
|
3837
4185
|
*/
|
|
3838
4186
|
async cancelTask(taskId) {
|
|
3839
|
-
|
|
3840
|
-
if (!node) return false;
|
|
3841
|
-
if (node.status === "completed") return false;
|
|
3842
|
-
this.cancelledTasks.add(taskId);
|
|
3843
|
-
this.opts.tracker.patchMetadata(taskId, { cancelled: true });
|
|
3844
|
-
this.opts.tracker.updateNodeStatus(taskId, "failed", "cancelled by user");
|
|
3845
|
-
this.emit("sdd.task.failed", {
|
|
3846
|
-
runId: this.runId,
|
|
3847
|
-
taskId,
|
|
3848
|
-
subagentId: "",
|
|
3849
|
-
error: "cancelled by user"
|
|
3850
|
-
});
|
|
3851
|
-
const subagentId = this.taskSubagents.get(taskId);
|
|
3852
|
-
if (subagentId && this.coordinator) {
|
|
3853
|
-
await this.coordinator.stop(subagentId).catch(() => {
|
|
3854
|
-
});
|
|
3855
|
-
}
|
|
3856
|
-
return true;
|
|
4187
|
+
return cancelTask(this.sddParallelControlsHost(), taskId);
|
|
3857
4188
|
}
|
|
3858
4189
|
/**
|
|
3859
4190
|
* Delete a not-yet-started task from the graph (pending/blocked/failed only —
|
|
@@ -3861,12 +4192,7 @@ var SddParallelRun = class {
|
|
|
3861
4192
|
* touching it; dependents lose this blocker. Returns false if missing or running.
|
|
3862
4193
|
*/
|
|
3863
4194
|
deleteTask(taskId) {
|
|
3864
|
-
|
|
3865
|
-
if (!node) return false;
|
|
3866
|
-
if (node.status === "in_progress" || this.taskSubagents.has(taskId)) return false;
|
|
3867
|
-
this.cancelledTasks.delete(taskId);
|
|
3868
|
-
this.retryMap.delete(taskId);
|
|
3869
|
-
return this.opts.tracker.removeNode(taskId);
|
|
4195
|
+
return deleteTask(this.sddParallelControlsHost(), taskId);
|
|
3870
4196
|
}
|
|
3871
4197
|
/**
|
|
3872
4198
|
* Split a task into sub-tasks and delegate them to separate workers. The new
|
|
@@ -3878,25 +4204,7 @@ var SddParallelRun = class {
|
|
|
3878
4204
|
* The scheduler picks the new pending leaves up on its next dispatch pass.
|
|
3879
4205
|
*/
|
|
3880
4206
|
splitTask(taskId, subtasks) {
|
|
3881
|
-
|
|
3882
|
-
isRunning: (id) => this.taskSubagents.has(id)
|
|
3883
|
-
});
|
|
3884
|
-
if (!leafIds.length) return [];
|
|
3885
|
-
this.retryMap.delete(taskId);
|
|
3886
|
-
this.persistRetries(taskId, 0);
|
|
3887
|
-
this.emit("sdd.task.split", { runId: this.runId, taskId, subtaskIds: leafIds });
|
|
3888
|
-
return leafIds;
|
|
3889
|
-
}
|
|
3890
|
-
async waitWhilePaused() {
|
|
3891
|
-
while (this.paused && !this.stopRequested) {
|
|
3892
|
-
await new Promise((resolve3) => {
|
|
3893
|
-
this.pausedWaiters.add(resolve3);
|
|
3894
|
-
const safety = setTimeout(() => {
|
|
3895
|
-
if (this.pausedWaiters.delete(resolve3)) resolve3();
|
|
3896
|
-
}, 1e3);
|
|
3897
|
-
safety.unref?.();
|
|
3898
|
-
});
|
|
3899
|
-
}
|
|
4207
|
+
return splitTask(this.sddParallelControlsHost(), taskId, subtasks);
|
|
3900
4208
|
}
|
|
3901
4209
|
/**
|
|
3902
4210
|
* Continuous dependency-driven execution. Unlike a wave-barrier loop (where a
|
|
@@ -3913,6 +4221,7 @@ var SddParallelRun = class {
|
|
|
3913
4221
|
const startTime = Date.now();
|
|
3914
4222
|
this.round = 0;
|
|
3915
4223
|
this.dispatchSeq = 0;
|
|
4224
|
+
this.runReturned = false;
|
|
3916
4225
|
let totalDispatched = 0;
|
|
3917
4226
|
this.buildCoordinator();
|
|
3918
4227
|
if (this.opts.worktrees && !this.baseBranch) {
|
|
@@ -4018,6 +4327,7 @@ var SddParallelRun = class {
|
|
|
4018
4327
|
stopped: this.stopRequested,
|
|
4019
4328
|
...this.fatalError ? { fatalError: this.fatalError } : {}
|
|
4020
4329
|
});
|
|
4330
|
+
this.runReturned = true;
|
|
4021
4331
|
return {
|
|
4022
4332
|
totalWaves: this.round,
|
|
4023
4333
|
totalCompleted: finalProgress.completed,
|
|
@@ -4029,26 +4339,6 @@ var SddParallelRun = class {
|
|
|
4029
4339
|
finalProgress
|
|
4030
4340
|
};
|
|
4031
4341
|
}
|
|
4032
|
-
/**
|
|
4033
|
-
* Compute the blocking chains for a deadlock: every still-incomplete task and
|
|
4034
|
-
* the blockers (by node id) that are NOT completed. Failed blockers are
|
|
4035
|
-
* included since they're the usual deadlock cause once retries are exhausted.
|
|
4036
|
-
*/
|
|
4037
|
-
computeDeadlockChains() {
|
|
4038
|
-
return computeDeadlockChains(this.sddTaskRecoveryHost());
|
|
4039
|
-
}
|
|
4040
|
-
/** Requeue failed tasks that block an incomplete dependent. Returns true if any. */
|
|
4041
|
-
recoverFailedBlockers() {
|
|
4042
|
-
return recoverFailedBlockers(this.sddTaskRecoveryHost());
|
|
4043
|
-
}
|
|
4044
|
-
/**
|
|
4045
|
-
* Requeue every terminal-failed task that the user did NOT cancel, giving each
|
|
4046
|
-
* a fresh `maxRetries` budget. Shared by the automatic end-of-run sweep and
|
|
4047
|
-
* the manual "retry all failed" control. Returns the number requeued.
|
|
4048
|
-
*/
|
|
4049
|
-
requeueFailedTasks(reason = "retry failed sweep") {
|
|
4050
|
-
return requeueFailedTasks(this.sddTaskRecoveryHost(), reason);
|
|
4051
|
-
}
|
|
4052
4342
|
/**
|
|
4053
4343
|
* Manually requeue all failed tasks to `pending` (board "Retry all failed").
|
|
4054
4344
|
* Unlike the automatic sweep this also clears any `cancelled` marker, so a
|
|
@@ -4064,10 +4354,6 @@ var SddParallelRun = class {
|
|
|
4064
4354
|
}
|
|
4065
4355
|
return this.requeueFailedTasks("manual retry all");
|
|
4066
4356
|
}
|
|
4067
|
-
/** Restore per-task retry counts persisted in node metadata (resume support). */
|
|
4068
|
-
restoreRetryMap() {
|
|
4069
|
-
restoreRetryMap(this.sddTaskRecoveryHost());
|
|
4070
|
-
}
|
|
4071
4357
|
/**
|
|
4072
4358
|
* Reset orphaned `in_progress` tasks (no agent runs them after a crash) back
|
|
4073
4359
|
* to `pending` so a fresh run re-executes them. Call before constructing a run
|
|
@@ -4076,64 +4362,6 @@ var SddParallelRun = class {
|
|
|
4076
4362
|
static resetOrphans(tracker) {
|
|
4077
4363
|
return resetOrphans(tracker);
|
|
4078
4364
|
}
|
|
4079
|
-
/** Clean teardown after a stop: reset interrupted tasks + release worktrees. */
|
|
4080
|
-
async teardown() {
|
|
4081
|
-
for (const node of this.opts.tracker.getAllNodes({ status: ["in_progress"] })) {
|
|
4082
|
-
this.opts.tracker.updateNodeStatus(node.id, "pending", "run stopped");
|
|
4083
|
-
}
|
|
4084
|
-
const wt = this.opts.worktrees;
|
|
4085
|
-
if (wt) {
|
|
4086
|
-
for (const [taskId, handle] of [...this.taskWorktrees]) {
|
|
4087
|
-
await wt.release(handle, { keep: true }).catch(() => {
|
|
4088
|
-
});
|
|
4089
|
-
this.forgetWorktree(taskId);
|
|
4090
|
-
}
|
|
4091
|
-
}
|
|
4092
|
-
}
|
|
4093
|
-
/**
|
|
4094
|
-
* Hard-stop the run after an unrecoverable error: the base branch is in a
|
|
4095
|
-
* state no retry can fix (e.g. a known-invalid merge that could not be rolled
|
|
4096
|
-
* back), so continuing would contaminate every task forked after this point.
|
|
4097
|
-
* The reason is surfaced on `run()`'s result and the `sdd.run.finished` event.
|
|
4098
|
-
*/
|
|
4099
|
-
abortRun(reason) {
|
|
4100
|
-
this.fatalError = reason;
|
|
4101
|
-
this.stopRequested = true;
|
|
4102
|
-
this.coordinator?.stopAll();
|
|
4103
|
-
}
|
|
4104
|
-
// -------------------------------------------------------------------
|
|
4105
|
-
// Internal
|
|
4106
|
-
// -------------------------------------------------------------------
|
|
4107
|
-
buildCoordinator() {
|
|
4108
|
-
const config = {
|
|
4109
|
-
coordinatorId: `sdd-parallel-${randomUUID3().slice(0, 8)}`,
|
|
4110
|
-
maxConcurrent: this.slots,
|
|
4111
|
-
doneCondition: { type: "all_tasks_done" },
|
|
4112
|
-
// Default budget guard for every spawned worker: idle reaper (resets on
|
|
4113
|
-
// activity) plus the opt-in wall-clock cap when one was configured. This
|
|
4114
|
-
// ensures the reaper applies even if a per-spawn config path is bypassed.
|
|
4115
|
-
defaultBudget: {
|
|
4116
|
-
idleTimeoutMs: this.idleTimeoutMs,
|
|
4117
|
-
...this.timeoutMs ? { timeoutMs: this.timeoutMs } : {}
|
|
4118
|
-
}
|
|
4119
|
-
};
|
|
4120
|
-
this.coordinator = new DefaultMultiAgentCoordinator(config, {
|
|
4121
|
-
sessionId: () => this.currentSessionId()
|
|
4122
|
-
});
|
|
4123
|
-
const baseFactory = this.opts.subagentFactory ?? this.defaultFactory();
|
|
4124
|
-
const filteredFactory = withDisabledToolFiltering(baseFactory);
|
|
4125
|
-
const runner = makeAgentSubagentRunner({
|
|
4126
|
-
factory: filteredFactory,
|
|
4127
|
-
hostEvents: this.events
|
|
4128
|
-
});
|
|
4129
|
-
this.coordinator.setRunner?.(runner);
|
|
4130
|
-
}
|
|
4131
|
-
defaultFactory() {
|
|
4132
|
-
return async (_config) => ({
|
|
4133
|
-
agent: this.opts.agent,
|
|
4134
|
-
events: this.opts.agent.events
|
|
4135
|
-
});
|
|
4136
|
-
}
|
|
4137
4365
|
/**
|
|
4138
4366
|
* Execute a batch of tasks together. Retained as a thin wrapper over the
|
|
4139
4367
|
* single-task primitive `executeOne` so the wave-oriented tests and any
|
|
@@ -4154,136 +4382,6 @@ var SddParallelRun = class {
|
|
|
4154
4382
|
async executeOne(task) {
|
|
4155
4383
|
return executeOne.call(this.sddTaskDispatchHost(), task);
|
|
4156
4384
|
}
|
|
4157
|
-
/**
|
|
4158
|
-
* Apply a task failure: retry (→ pending, bump retry count) while attempts
|
|
4159
|
-
* remain, else consult the optional supervisor (which can rescue via
|
|
4160
|
-
* retry/reassign/split), else terminal-fail (→ failed). Shared by the
|
|
4161
|
-
* worker-failure, verification-gate, and merge-conflict paths so all three
|
|
4162
|
-
* negotiate the same retry budget and emit the same events.
|
|
4163
|
-
*/
|
|
4164
|
-
async applyTaskFailure(taskId, subagentId, errMsg) {
|
|
4165
|
-
return applyTaskFailure.call(this.sddTaskDispatchHost(), taskId, subagentId, errMsg);
|
|
4166
|
-
}
|
|
4167
|
-
/**
|
|
4168
|
-
* Consult `superviseFailure` for a task that has exhausted its retries.
|
|
4169
|
-
* Applies the verdict (retry / reassign+retry / split) and returns true when
|
|
4170
|
-
* the task was rescued (caller must NOT terminal-fail it). Bounded per task by
|
|
4171
|
-
* `maxSupervisorEscalations` so an always-"retry" supervisor can't loop forever.
|
|
4172
|
-
*/
|
|
4173
|
-
async trySupervisorRescue(taskId, errMsg) {
|
|
4174
|
-
return trySupervisorRescue.call(this.sddTaskDispatchHost(), taskId, errMsg);
|
|
4175
|
-
}
|
|
4176
|
-
/**
|
|
4177
|
-
* Integrate a verified-successful task's worktree into the base branch.
|
|
4178
|
-
* Commits, squash-merges (optionally running `conflictResolver` first), and on
|
|
4179
|
-
* success releases the worktree. On an UNRESOLVED conflict it returns
|
|
4180
|
-
* `{ok:false}` with the conflicting files so the caller routes the task into
|
|
4181
|
-
* the failure path (a retry forks a fresh worktree off the now-advanced base,
|
|
4182
|
-
* which usually clears the conflict). No-op `{ok:true}` when worktrees are
|
|
4183
|
-
* disabled or none was allocated for this task. Never throws — a merge hiccup
|
|
4184
|
-
* degrades to a (retryable) failure rather than wedging the run.
|
|
4185
|
-
*/
|
|
4186
|
-
get worktreeState() {
|
|
4187
|
-
return {
|
|
4188
|
-
taskCwds: this.taskCwds,
|
|
4189
|
-
taskBranches: this.taskBranches,
|
|
4190
|
-
taskWorktrees: this.taskWorktrees,
|
|
4191
|
-
mergedCommits: this.mergedCommits
|
|
4192
|
-
};
|
|
4193
|
-
}
|
|
4194
|
-
async integrateWorktree(task, result) {
|
|
4195
|
-
return integrateTaskWorktree({
|
|
4196
|
-
opts: this.opts,
|
|
4197
|
-
state: this.worktreeState,
|
|
4198
|
-
task,
|
|
4199
|
-
result,
|
|
4200
|
-
runId: this.runId,
|
|
4201
|
-
emit: this.emit.bind(this),
|
|
4202
|
-
abortRun: this.abortRun.bind(this)
|
|
4203
|
-
});
|
|
4204
|
-
}
|
|
4205
|
-
/** Allocate a fresh git worktree per task in the batch (no-op without a manager). */
|
|
4206
|
-
async allocateWorktrees(tasks) {
|
|
4207
|
-
await allocateTaskWorktrees(this.opts, this.worktreeState, tasks);
|
|
4208
|
-
}
|
|
4209
|
-
/**
|
|
4210
|
-
* Resolve each task's worktree after its result is known. Serialized merges
|
|
4211
|
-
* (one at a time) keep the base branch consistent; the wave structure already
|
|
4212
|
-
* guarantees dependency order (a task's blockers merged in an earlier wave).
|
|
4213
|
-
*/
|
|
4214
|
-
async resolveWorktrees(tasks) {
|
|
4215
|
-
await resolveTaskWorktrees(this.opts, this.worktreeState, tasks);
|
|
4216
|
-
}
|
|
4217
|
-
forgetWorktree(taskId, opts = {}) {
|
|
4218
|
-
forgetTaskWorktree(this.worktreeState, taskId, opts);
|
|
4219
|
-
}
|
|
4220
|
-
/** Persist a task's retry count into node metadata (survives crash → resume). */
|
|
4221
|
-
persistRetries(taskId, retries) {
|
|
4222
|
-
const node = this.opts.tracker.getNode(taskId);
|
|
4223
|
-
if (node) node.metadata = { ...node.metadata, retries };
|
|
4224
|
-
}
|
|
4225
|
-
buildProgress() {
|
|
4226
|
-
const gp = this.opts.tracker.getProgress();
|
|
4227
|
-
const isDeadlocked = !this.decomposer.isDone() && this.decomposer.nextBatch().deadlocked;
|
|
4228
|
-
return {
|
|
4229
|
-
wave: this.decomposer.getWaveCount(),
|
|
4230
|
-
total: gp.total,
|
|
4231
|
-
completed: gp.completed,
|
|
4232
|
-
inProgress: gp.inProgress,
|
|
4233
|
-
failed: gp.failed,
|
|
4234
|
-
blocked: gp.blocked,
|
|
4235
|
-
pending: gp.pending,
|
|
4236
|
-
percent: gp.percentComplete,
|
|
4237
|
-
deadlocked: isDeadlocked
|
|
4238
|
-
};
|
|
4239
|
-
}
|
|
4240
|
-
sddTaskRecoveryHost() {
|
|
4241
|
-
const self = this;
|
|
4242
|
-
return {
|
|
4243
|
-
get opts() {
|
|
4244
|
-
return self.opts;
|
|
4245
|
-
},
|
|
4246
|
-
get retryMap() {
|
|
4247
|
-
return self.retryMap;
|
|
4248
|
-
},
|
|
4249
|
-
persistRetries: (...args) => this.persistRetries(...args),
|
|
4250
|
-
get cancelledTasks() {
|
|
4251
|
-
return self.cancelledTasks;
|
|
4252
|
-
},
|
|
4253
|
-
emit: (...args) => this.emit(...args),
|
|
4254
|
-
runId: this.runId,
|
|
4255
|
-
maxRetries: this.maxRetries
|
|
4256
|
-
};
|
|
4257
|
-
}
|
|
4258
|
-
sddTaskDispatchHost() {
|
|
4259
|
-
void this.executeOne;
|
|
4260
|
-
void this.stopRequested;
|
|
4261
|
-
void this.opts;
|
|
4262
|
-
void this.coordinator;
|
|
4263
|
-
void this.usedNicknames;
|
|
4264
|
-
void this.idleTimeoutMs;
|
|
4265
|
-
void this.timeoutMs;
|
|
4266
|
-
void this.runId;
|
|
4267
|
-
void this.dispatchSeq;
|
|
4268
|
-
void this.emit;
|
|
4269
|
-
void this.taskCwds;
|
|
4270
|
-
void this.taskBranches;
|
|
4271
|
-
void this.taskSubagents;
|
|
4272
|
-
void this.cancelledTasks;
|
|
4273
|
-
void this.allocateWorktrees;
|
|
4274
|
-
void this.resolveWorktrees;
|
|
4275
|
-
void this.integrateWorktree;
|
|
4276
|
-
void this.applyTaskFailure;
|
|
4277
|
-
void this.retryMap;
|
|
4278
|
-
void this.persistRetries;
|
|
4279
|
-
void this.maxRetries;
|
|
4280
|
-
void this.trySupervisorRescue;
|
|
4281
|
-
void this.supervisorEscalations;
|
|
4282
|
-
void this.maxSupervisorEscalations;
|
|
4283
|
-
void this.setTaskModel;
|
|
4284
|
-
void this.splitTask;
|
|
4285
|
-
return this;
|
|
4286
|
-
}
|
|
4287
4385
|
};
|
|
4288
4386
|
|
|
4289
4387
|
// src/start-sdd-run.ts
|
|
@@ -5022,7 +5120,7 @@ function templateToMarkdown(template, title) {
|
|
|
5022
5120
|
|
|
5023
5121
|
// src/task-visualizer.ts
|
|
5024
5122
|
import { computeTaskProgress as computeTaskProgress2 } from "@wrongstack/core/tasking";
|
|
5025
|
-
import { expectDefined as
|
|
5123
|
+
import { expectDefined as expectDefined5, truncate } from "@wrongstack/core/utils";
|
|
5026
5124
|
var STATUS_ICON = {
|
|
5027
5125
|
pending: "\u25CB",
|
|
5028
5126
|
in_progress: "\u25D0",
|
|
@@ -5092,7 +5190,7 @@ function renderTaskGraph(graph, opts) {
|
|
|
5092
5190
|
function renderNode(graph, nodeId, lines, rendered, childrenMap, dependentsMap, compact, prefix) {
|
|
5093
5191
|
if (rendered.has(nodeId)) return;
|
|
5094
5192
|
rendered.add(nodeId);
|
|
5095
|
-
const node =
|
|
5193
|
+
const node = expectDefined5(graph.nodes.get(nodeId));
|
|
5096
5194
|
const icon = STATUS_ICON[node.status];
|
|
5097
5195
|
const prioIcon = PRIORITY_ICON[node.priority];
|
|
5098
5196
|
const typeIcon = TYPE_ICON[node.type];
|
|
@@ -5182,7 +5280,7 @@ function renderSpecAnalysis(spec, analysis) {
|
|
|
5182
5280
|
|
|
5183
5281
|
// src/critical-path.ts
|
|
5184
5282
|
import { topologicalSort } from "@wrongstack/core/tasking";
|
|
5185
|
-
import { expectDefined as
|
|
5283
|
+
import { expectDefined as expectDefined6 } from "@wrongstack/core/utils";
|
|
5186
5284
|
function analyzeCriticalPath(graph) {
|
|
5187
5285
|
const nodes = Array.from(graph.nodes.values());
|
|
5188
5286
|
const topoOrder = topologicalSort(graph);
|
|
@@ -5261,7 +5359,7 @@ function getTransitiveBlocked(_graph, taskId, blocksMap) {
|
|
|
5261
5359
|
const visited = /* @__PURE__ */ new Set();
|
|
5262
5360
|
const queue = [taskId];
|
|
5263
5361
|
while (queue.length > 0) {
|
|
5264
|
-
const current =
|
|
5362
|
+
const current = expectDefined6(queue.shift());
|
|
5265
5363
|
const blocked = blocksMap.get(current);
|
|
5266
5364
|
if (!blocked) continue;
|
|
5267
5365
|
for (const id of blocked) {
|
|
@@ -5307,7 +5405,7 @@ function computeCriticalPath(graph, _topoOrder, blockedByMap) {
|
|
|
5307
5405
|
if (!changed) break;
|
|
5308
5406
|
}
|
|
5309
5407
|
let maxDist = 0;
|
|
5310
|
-
let maxId =
|
|
5408
|
+
let maxId = expectDefined6(allIds[0]);
|
|
5311
5409
|
for (const id of allIds) {
|
|
5312
5410
|
const d = dist.get(id);
|
|
5313
5411
|
if (d > maxDist) {
|
|
@@ -5350,7 +5448,7 @@ function computeParallelGroups(graph, blockedByMap) {
|
|
|
5350
5448
|
}
|
|
5351
5449
|
}
|
|
5352
5450
|
if (group.length === 0) {
|
|
5353
|
-
group.push(
|
|
5451
|
+
group.push(expectDefined6(Array.from(remaining)[0]));
|
|
5354
5452
|
}
|
|
5355
5453
|
for (const id of group) {
|
|
5356
5454
|
assigned.add(id);
|