@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 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 ERROR_CODES2, SddError as SddError2 } from "@wrongstack/core/types";
1979
- import { expectDefined as expectDefined2, toErrorMessage } from "@wrongstack/core/utils";
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
- const fsp7 = await import("node:fs/promises");
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 loaded = await this.sessionPersistence.load();
2243
- if (isAISpecSession(loaded)) {
2244
- this.session = loaded;
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
- try {
2251
- const fsp7 = await import("node:fs/promises");
2252
- const raw = await fsp7.readFile(this.sessionPath, "utf8");
2253
- const loaded = JSON.parse(raw);
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
- try {
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 SddError2({
2495
+ throw new SddError3({
2367
2496
  message: "Cannot approve: no spec generated yet.",
2368
- code: ERROR_CODES2.SDD_INVALID_STATE,
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 SddError2({
2631
+ throw new SddError3({
2503
2632
  message: "No spec to save.",
2504
- code: ERROR_CODES2.SDD_NOT_READY,
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
- let parsed;
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
- const codeBlockMatch = text.match(/```json\s*([\s\S]*?)```/);
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
- const codeBlockMatch = text.match(/```json\s*([\s\S]*?)```/);
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/sdd-task-execution.ts
2995
- import { randomUUID as randomUUID2 } from "node:crypto";
2996
- import { expectDefined as expectDefined3 } from "@wrongstack/core/utils";
2997
- import { assignNickname } from "@wrongstack/core/coordination";
2998
- import { ERROR_CODES as ERROR_CODES3, SddError as SddError3 } from "@wrongstack/core/types";
2999
- async function executeSddTask(params) {
3000
- const { task, opts } = params;
3001
- const taskId = task.id;
3002
- let agentName = task.assignee;
3003
- if (!agentName) {
3004
- const nick = assignNickname("executor", params.usedNicknames);
3005
- params.usedNicknames.add(nick.key);
3006
- agentName = nick.display.replace(/\s*\([^)]*\)\s*$/, "");
3007
- opts.tracker.updateNode(taskId, { assignee: agentName });
3008
- }
3009
- opts.tracker.updateNodeStatus(taskId, "in_progress");
3010
- await params.allocateWorktrees([task]);
3011
- if (!params.coordinator)
3012
- throw new SddError3({
3013
- message: "SDD parallel runner requires a coordinator",
3014
- code: ERROR_CODES3.SDD_INVALID_STATE
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
- if (!spawnResult.subagentId) {
3036
- throw new SddError3({
3037
- message: "One or more subagent spawns failed",
3038
- code: ERROR_CODES3.SDD_INVALID_STATE
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
- params.taskSubagents.set(taskId, subagentId);
3042
- params.emit("sdd.task.started", {
3043
- runId: params.runId,
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
- agentName,
3047
- worktreeBranch: params.taskBranches.get(taskId)
3121
+ subagentId: "",
3122
+ error: "cancelled by user"
3048
3123
  });
3049
- await coordinator.assign({
3050
- id: correlationId,
3051
- description: buildTaskDirective(opts.graph.title, task),
3052
- subagentId,
3053
- ...params.timeoutMs ? { timeoutMs: params.timeoutMs } : {},
3054
- context: {
3055
- telemetryTaskId: taskId,
3056
- telemetryRunId: params.runId,
3057
- telemetryBoardId: opts.graph.id
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
- let result;
3061
- try {
3062
- const got = await coordinator.awaitTasks([correlationId]);
3063
- result = expectDefined3(got[0]);
3064
- } catch (err) {
3065
- result = {
3066
- subagentId,
3067
- taskId: correlationId,
3068
- status: "failed",
3069
- error: { kind: "unknown", message: String(err), retryable: false },
3070
- iterations: 0,
3071
- toolCalls: 0,
3072
- durationMs: 0
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
- params.taskSubagents.delete(taskId);
3076
- if (params.cancelledTasks.has(taskId)) {
3077
- await params.resolveWorktrees([task]);
3078
- return { taskId, success: false, result };
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
- const verificationFailReason = await verifyTaskResult(params, result);
3081
- let success = false;
3082
- if (result.status === "success" && !verificationFailReason) {
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-parallel-run.ts
3345
- import { randomUUID as randomUUID3 } from "node:crypto";
3346
- import {
3347
- DefaultMultiAgentCoordinator,
3348
- makeAgentSubagentRunner,
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
- // src/sdd-task-decomposer.ts
3385
- var SddTaskDecomposer = class {
3386
- constructor(tracker, _graph, opts = {}) {
3387
- this.tracker = tracker;
3388
- this.slots = Math.min(16, Math.max(1, opts.parallelSlots ?? 4));
3389
- }
3390
- tracker;
3391
- slots;
3392
- wave = 0;
3393
- // -------------------------------------------------------------------
3394
- // Public API
3395
- // -------------------------------------------------------------------
3396
- /**
3397
- * Return the next batch of runnable tasks.
3398
- * Returns `allDone: true` when every node is completed.
3399
- * Returns `deadlocked: true` when no batch can be produced because
3400
- * all remaining tasks are blocked by failed nodes.
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 { ok: false, conflictFiles: res.conflictFiles ?? [] };
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 SddParallelRun = class {
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
- if (this.isRunning()) return 0;
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
- if (this.isRunning())
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
- if (!this.opts.tracker.getNode(taskId)) return false;
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
- if (!this.opts.tracker.getNode(taskId)) return false;
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
- if (!this.opts.tracker.getNode(taskId)) return false;
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
- if (!this.opts.tracker.getNode(taskId)) return false;
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
- if (!this.opts.tracker.getNode(taskId)) return false;
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
- const node = this.opts.tracker.getNode(taskId);
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
- const node = this.opts.tracker.getNode(taskId);
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
- const leafIds = splitGraphNode(this.opts.tracker, taskId, subtasks, {
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 expectDefined4, truncate } from "@wrongstack/core/utils";
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 = expectDefined4(graph.nodes.get(nodeId));
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 expectDefined5 } from "@wrongstack/core/utils";
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 = expectDefined5(queue.shift());
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 = expectDefined5(allIds[0]);
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(expectDefined5(Array.from(remaining)[0]));
5451
+ group.push(expectDefined6(Array.from(remaining)[0]));
5354
5452
  }
5355
5453
  for (const id of group) {
5356
5454
  assigned.add(id);