@wrongstack/sdd 1.0.33 → 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));
@@ -3718,16 +3850,259 @@ var SddParallelRun = class {
3718
3850
  const value = typeof this.sessionIdSource === "function" ? this.sessionIdSource() : this.sessionIdSource;
3719
3851
  return requireSessionId(value, "SDD session operation");
3720
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 {
3721
4103
  // -------------------------------------------------------------------
3722
4104
  // Public API
3723
4105
  // -------------------------------------------------------------------
3724
- paused = false;
3725
- /** Resolvers for tasks parked in `waitWhilePaused`, woken on resume/stop. */
3726
- pausedWaiters = /* @__PURE__ */ new Set();
3727
- notifyPausedWaiters() {
3728
- for (const resolve3 of this.pausedWaiters) resolve3();
3729
- this.pausedWaiters.clear();
3730
- }
3731
4106
  /** Trigger stop — causes run() to abort after the current wave. */
3732
4107
  stop() {
3733
4108
  this.stopRequested = true;
@@ -3764,16 +4139,7 @@ var SddParallelRun = class {
3764
4139
  * (0 when worktrees are disabled). Idempotent.
3765
4140
  */
3766
4141
  async cleanupWorktrees() {
3767
- if (this.isRunning()) return 0;
3768
- const wt = this.opts.worktrees;
3769
- if (!wt) return 0;
3770
- for (const [taskId, handle] of [...this.taskWorktrees]) {
3771
- await wt.release(handle, { keep: false }).catch(() => {
3772
- });
3773
- this.forgetWorktree(taskId);
3774
- }
3775
- const { removed } = await wt.cleanupAllManaged();
3776
- return removed;
4142
+ return cleanupWorktrees(this.sddParallelControlsHost());
3777
4143
  }
3778
4144
  /**
3779
4145
  * Undo the run's merged commits by reverting each on the base branch (history
@@ -3781,32 +4147,15 @@ var SddParallelRun = class {
3781
4147
  * revert outcome; a dirty tree or revert conflict surfaces as `ok:false`.
3782
4148
  */
3783
4149
  async rollback() {
3784
- if (this.isRunning())
3785
- return { ok: false, reverted: 0, reason: "run still active \u2014 stop it first" };
3786
- const wt = this.opts.worktrees;
3787
- if (!wt || !this.baseBranch) {
3788
- return { ok: false, reverted: 0, reason: "no worktree run to roll back" };
3789
- }
3790
- return wt.revertCommits(
3791
- this.baseBranch,
3792
- this.mergedCommits.map((c) => c.sha)
3793
- );
4150
+ return rollback(this.sddParallelControlsHost());
3794
4151
  }
3795
4152
  /** Requeue a task to `pending` so the scheduler re-runs it (clears retries + cancel marker). */
3796
4153
  retryTask(taskId) {
3797
- if (!this.opts.tracker.getNode(taskId)) return false;
3798
- this.retryMap.delete(taskId);
3799
- this.persistRetries(taskId, 0);
3800
- this.cancelledTasks.delete(taskId);
3801
- this.opts.tracker.patchMetadata(taskId, { cancelled: void 0 });
3802
- this.opts.tracker.updateNodeStatus(taskId, "pending", "manual retry");
3803
- return true;
4154
+ return retryTask(this.sddParallelControlsHost(), taskId);
3804
4155
  }
3805
4156
  /** Reassign a task to a specific agent name (reflected on the board). */
3806
4157
  reassignTask(taskId, agentName) {
3807
- if (!this.opts.tracker.getNode(taskId)) return false;
3808
- this.opts.tracker.updateNode(taskId, { assignee: agentName });
3809
- return true;
4158
+ return reassignTask(this.sddParallelControlsHost(), taskId, agentName);
3810
4159
  }
3811
4160
  /**
3812
4161
  * Set/override a task's worker model (and optionally provider) — applied on its
@@ -3814,18 +4163,11 @@ var SddParallelRun = class {
3814
4163
  * assignment lives on node metadata so it survives crash → resume.
3815
4164
  */
3816
4165
  setTaskModel(taskId, model, provider) {
3817
- if (!this.opts.tracker.getNode(taskId)) return false;
3818
- this.opts.tracker.patchMetadata(taskId, {
3819
- model,
3820
- ...provider !== void 0 ? { provider } : {}
3821
- });
3822
- return true;
4166
+ return setTaskModel(this.sddParallelControlsHost(), taskId, model, provider);
3823
4167
  }
3824
4168
  /** Set/override a task's fallback model chain (applied on its next dispatch). */
3825
4169
  setTaskFallbacks(taskId, fallbackModels) {
3826
- if (!this.opts.tracker.getNode(taskId)) return false;
3827
- this.opts.tracker.patchMetadata(taskId, { fallbackModels });
3828
- return true;
4170
+ return setTaskFallbacks(this.sddParallelControlsHost(), taskId, fallbackModels);
3829
4171
  }
3830
4172
  /**
3831
4173
  * Set/override a task's verification command (the completion gate runs it in
@@ -3833,10 +4175,7 @@ var SddParallelRun = class {
3833
4175
  * clears it. Applied on the task's next verification — i.e. its next dispatch.
3834
4176
  */
3835
4177
  setTaskVerification(taskId, verificationCommand) {
3836
- if (!this.opts.tracker.getNode(taskId)) return false;
3837
- const cmd = verificationCommand?.trim();
3838
- this.opts.tracker.patchMetadata(taskId, { verificationCommand: cmd ? cmd : void 0 });
3839
- return true;
4178
+ return setTaskVerification(this.sddParallelControlsHost(), taskId, verificationCommand);
3840
4179
  }
3841
4180
  /**
3842
4181
  * Cancel a task. If it is currently running, abort its subagent and mark the
@@ -3845,24 +4184,7 @@ var SddParallelRun = class {
3845
4184
  * `retryTask` to bring a cancelled task back. Returns false for an unknown task.
3846
4185
  */
3847
4186
  async cancelTask(taskId) {
3848
- const node = this.opts.tracker.getNode(taskId);
3849
- if (!node) return false;
3850
- if (node.status === "completed") return false;
3851
- this.cancelledTasks.add(taskId);
3852
- this.opts.tracker.patchMetadata(taskId, { cancelled: true });
3853
- this.opts.tracker.updateNodeStatus(taskId, "failed", "cancelled by user");
3854
- this.emit("sdd.task.failed", {
3855
- runId: this.runId,
3856
- taskId,
3857
- subagentId: "",
3858
- error: "cancelled by user"
3859
- });
3860
- const subagentId = this.taskSubagents.get(taskId);
3861
- if (subagentId && this.coordinator) {
3862
- await this.coordinator.stop(subagentId).catch(() => {
3863
- });
3864
- }
3865
- return true;
4187
+ return cancelTask(this.sddParallelControlsHost(), taskId);
3866
4188
  }
3867
4189
  /**
3868
4190
  * Delete a not-yet-started task from the graph (pending/blocked/failed only —
@@ -3870,12 +4192,7 @@ var SddParallelRun = class {
3870
4192
  * touching it; dependents lose this blocker. Returns false if missing or running.
3871
4193
  */
3872
4194
  deleteTask(taskId) {
3873
- const node = this.opts.tracker.getNode(taskId);
3874
- if (!node) return false;
3875
- if (node.status === "in_progress" || this.taskSubagents.has(taskId)) return false;
3876
- this.cancelledTasks.delete(taskId);
3877
- this.retryMap.delete(taskId);
3878
- return this.opts.tracker.removeNode(taskId);
4195
+ return deleteTask(this.sddParallelControlsHost(), taskId);
3879
4196
  }
3880
4197
  /**
3881
4198
  * Split a task into sub-tasks and delegate them to separate workers. The new
@@ -3887,25 +4204,7 @@ var SddParallelRun = class {
3887
4204
  * The scheduler picks the new pending leaves up on its next dispatch pass.
3888
4205
  */
3889
4206
  splitTask(taskId, subtasks) {
3890
- const leafIds = splitGraphNode(this.opts.tracker, taskId, subtasks, {
3891
- isRunning: (id) => this.taskSubagents.has(id)
3892
- });
3893
- if (!leafIds.length) return [];
3894
- this.retryMap.delete(taskId);
3895
- this.persistRetries(taskId, 0);
3896
- this.emit("sdd.task.split", { runId: this.runId, taskId, subtaskIds: leafIds });
3897
- return leafIds;
3898
- }
3899
- async waitWhilePaused() {
3900
- while (this.paused && !this.stopRequested) {
3901
- await new Promise((resolve3) => {
3902
- this.pausedWaiters.add(resolve3);
3903
- const safety = setTimeout(() => {
3904
- if (this.pausedWaiters.delete(resolve3)) resolve3();
3905
- }, 1e3);
3906
- safety.unref?.();
3907
- });
3908
- }
4207
+ return splitTask(this.sddParallelControlsHost(), taskId, subtasks);
3909
4208
  }
3910
4209
  /**
3911
4210
  * Continuous dependency-driven execution. Unlike a wave-barrier loop (where a
@@ -4040,26 +4339,6 @@ var SddParallelRun = class {
4040
4339
  finalProgress
4041
4340
  };
4042
4341
  }
4043
- /**
4044
- * Compute the blocking chains for a deadlock: every still-incomplete task and
4045
- * the blockers (by node id) that are NOT completed. Failed blockers are
4046
- * included since they're the usual deadlock cause once retries are exhausted.
4047
- */
4048
- computeDeadlockChains() {
4049
- return computeDeadlockChains(this.sddTaskRecoveryHost());
4050
- }
4051
- /** Requeue failed tasks that block an incomplete dependent. Returns true if any. */
4052
- recoverFailedBlockers() {
4053
- return recoverFailedBlockers(this.sddTaskRecoveryHost());
4054
- }
4055
- /**
4056
- * Requeue every terminal-failed task that the user did NOT cancel, giving each
4057
- * a fresh `maxRetries` budget. Shared by the automatic end-of-run sweep and
4058
- * the manual "retry all failed" control. Returns the number requeued.
4059
- */
4060
- requeueFailedTasks(reason = "retry failed sweep") {
4061
- return requeueFailedTasks(this.sddTaskRecoveryHost(), reason);
4062
- }
4063
4342
  /**
4064
4343
  * Manually requeue all failed tasks to `pending` (board "Retry all failed").
4065
4344
  * Unlike the automatic sweep this also clears any `cancelled` marker, so a
@@ -4075,10 +4354,6 @@ var SddParallelRun = class {
4075
4354
  }
4076
4355
  return this.requeueFailedTasks("manual retry all");
4077
4356
  }
4078
- /** Restore per-task retry counts persisted in node metadata (resume support). */
4079
- restoreRetryMap() {
4080
- restoreRetryMap(this.sddTaskRecoveryHost());
4081
- }
4082
4357
  /**
4083
4358
  * Reset orphaned `in_progress` tasks (no agent runs them after a crash) back
4084
4359
  * to `pending` so a fresh run re-executes them. Call before constructing a run
@@ -4087,64 +4362,6 @@ var SddParallelRun = class {
4087
4362
  static resetOrphans(tracker) {
4088
4363
  return resetOrphans(tracker);
4089
4364
  }
4090
- /** Clean teardown after a stop: reset interrupted tasks + release worktrees. */
4091
- async teardown() {
4092
- for (const node of this.opts.tracker.getAllNodes({ status: ["in_progress"] })) {
4093
- this.opts.tracker.updateNodeStatus(node.id, "pending", "run stopped");
4094
- }
4095
- const wt = this.opts.worktrees;
4096
- if (wt) {
4097
- for (const [taskId, handle] of [...this.taskWorktrees]) {
4098
- await wt.release(handle, { keep: true }).catch(() => {
4099
- });
4100
- this.forgetWorktree(taskId);
4101
- }
4102
- }
4103
- }
4104
- /**
4105
- * Hard-stop the run after an unrecoverable error: the base branch is in a
4106
- * state no retry can fix (e.g. a known-invalid merge that could not be rolled
4107
- * back), so continuing would contaminate every task forked after this point.
4108
- * The reason is surfaced on `run()`'s result and the `sdd.run.finished` event.
4109
- */
4110
- abortRun(reason) {
4111
- this.fatalError = reason;
4112
- this.stopRequested = true;
4113
- this.coordinator?.stopAll();
4114
- }
4115
- // -------------------------------------------------------------------
4116
- // Internal
4117
- // -------------------------------------------------------------------
4118
- buildCoordinator() {
4119
- const config = {
4120
- coordinatorId: `sdd-parallel-${randomUUID3().slice(0, 8)}`,
4121
- maxConcurrent: this.slots,
4122
- doneCondition: { type: "all_tasks_done" },
4123
- // Default budget guard for every spawned worker: idle reaper (resets on
4124
- // activity) plus the opt-in wall-clock cap when one was configured. This
4125
- // ensures the reaper applies even if a per-spawn config path is bypassed.
4126
- defaultBudget: {
4127
- idleTimeoutMs: this.idleTimeoutMs,
4128
- ...this.timeoutMs ? { timeoutMs: this.timeoutMs } : {}
4129
- }
4130
- };
4131
- this.coordinator = new DefaultMultiAgentCoordinator(config, {
4132
- sessionId: () => this.currentSessionId()
4133
- });
4134
- const baseFactory = this.opts.subagentFactory ?? this.defaultFactory();
4135
- const filteredFactory = withDisabledToolFiltering(baseFactory);
4136
- const runner = makeAgentSubagentRunner({
4137
- factory: filteredFactory,
4138
- hostEvents: this.events
4139
- });
4140
- this.coordinator.setRunner?.(runner);
4141
- }
4142
- defaultFactory() {
4143
- return async (_config) => ({
4144
- agent: this.opts.agent,
4145
- events: this.opts.agent.events
4146
- });
4147
- }
4148
4365
  /**
4149
4366
  * Execute a batch of tasks together. Retained as a thin wrapper over the
4150
4367
  * single-task primitive `executeOne` so the wave-oriented tests and any
@@ -4165,136 +4382,6 @@ var SddParallelRun = class {
4165
4382
  async executeOne(task) {
4166
4383
  return executeOne.call(this.sddTaskDispatchHost(), task);
4167
4384
  }
4168
- /**
4169
- * Apply a task failure: retry (→ pending, bump retry count) while attempts
4170
- * remain, else consult the optional supervisor (which can rescue via
4171
- * retry/reassign/split), else terminal-fail (→ failed). Shared by the
4172
- * worker-failure, verification-gate, and merge-conflict paths so all three
4173
- * negotiate the same retry budget and emit the same events.
4174
- */
4175
- async applyTaskFailure(taskId, subagentId, errMsg) {
4176
- return applyTaskFailure.call(this.sddTaskDispatchHost(), taskId, subagentId, errMsg);
4177
- }
4178
- /**
4179
- * Consult `superviseFailure` for a task that has exhausted its retries.
4180
- * Applies the verdict (retry / reassign+retry / split) and returns true when
4181
- * the task was rescued (caller must NOT terminal-fail it). Bounded per task by
4182
- * `maxSupervisorEscalations` so an always-"retry" supervisor can't loop forever.
4183
- */
4184
- async trySupervisorRescue(taskId, errMsg) {
4185
- return trySupervisorRescue.call(this.sddTaskDispatchHost(), taskId, errMsg);
4186
- }
4187
- /**
4188
- * Integrate a verified-successful task's worktree into the base branch.
4189
- * Commits, squash-merges (optionally running `conflictResolver` first), and on
4190
- * success releases the worktree. On an UNRESOLVED conflict it returns
4191
- * `{ok:false}` with the conflicting files so the caller routes the task into
4192
- * the failure path (a retry forks a fresh worktree off the now-advanced base,
4193
- * which usually clears the conflict). No-op `{ok:true}` when worktrees are
4194
- * disabled or none was allocated for this task. Never throws — a merge hiccup
4195
- * degrades to a (retryable) failure rather than wedging the run.
4196
- */
4197
- get worktreeState() {
4198
- return {
4199
- taskCwds: this.taskCwds,
4200
- taskBranches: this.taskBranches,
4201
- taskWorktrees: this.taskWorktrees,
4202
- mergedCommits: this.mergedCommits
4203
- };
4204
- }
4205
- async integrateWorktree(task, result) {
4206
- return integrateTaskWorktree({
4207
- opts: this.opts,
4208
- state: this.worktreeState,
4209
- task,
4210
- result,
4211
- runId: this.runId,
4212
- emit: this.emit.bind(this),
4213
- abortRun: this.abortRun.bind(this)
4214
- });
4215
- }
4216
- /** Allocate a fresh git worktree per task in the batch (no-op without a manager). */
4217
- async allocateWorktrees(tasks) {
4218
- await allocateTaskWorktrees(this.opts, this.worktreeState, tasks);
4219
- }
4220
- /**
4221
- * Resolve each task's worktree after its result is known. Serialized merges
4222
- * (one at a time) keep the base branch consistent; the wave structure already
4223
- * guarantees dependency order (a task's blockers merged in an earlier wave).
4224
- */
4225
- async resolveWorktrees(tasks) {
4226
- await resolveTaskWorktrees(this.opts, this.worktreeState, tasks);
4227
- }
4228
- forgetWorktree(taskId, opts = {}) {
4229
- forgetTaskWorktree(this.worktreeState, taskId, opts);
4230
- }
4231
- /** Persist a task's retry count into node metadata (survives crash → resume). */
4232
- persistRetries(taskId, retries) {
4233
- const node = this.opts.tracker.getNode(taskId);
4234
- if (node) node.metadata = { ...node.metadata, retries };
4235
- }
4236
- buildProgress() {
4237
- const gp = this.opts.tracker.getProgress();
4238
- const isDeadlocked = !this.decomposer.isDone() && this.decomposer.nextBatch().deadlocked;
4239
- return {
4240
- wave: this.decomposer.getWaveCount(),
4241
- total: gp.total,
4242
- completed: gp.completed,
4243
- inProgress: gp.inProgress,
4244
- failed: gp.failed,
4245
- blocked: gp.blocked,
4246
- pending: gp.pending,
4247
- percent: gp.percentComplete,
4248
- deadlocked: isDeadlocked
4249
- };
4250
- }
4251
- sddTaskRecoveryHost() {
4252
- const self = this;
4253
- return {
4254
- get opts() {
4255
- return self.opts;
4256
- },
4257
- get retryMap() {
4258
- return self.retryMap;
4259
- },
4260
- persistRetries: (...args) => this.persistRetries(...args),
4261
- get cancelledTasks() {
4262
- return self.cancelledTasks;
4263
- },
4264
- emit: (...args) => this.emit(...args),
4265
- runId: this.runId,
4266
- maxRetries: this.maxRetries
4267
- };
4268
- }
4269
- sddTaskDispatchHost() {
4270
- void this.executeOne;
4271
- void this.stopRequested;
4272
- void this.opts;
4273
- void this.coordinator;
4274
- void this.usedNicknames;
4275
- void this.idleTimeoutMs;
4276
- void this.timeoutMs;
4277
- void this.runId;
4278
- void this.dispatchSeq;
4279
- void this.emit;
4280
- void this.taskCwds;
4281
- void this.taskBranches;
4282
- void this.taskSubagents;
4283
- void this.cancelledTasks;
4284
- void this.allocateWorktrees;
4285
- void this.resolveWorktrees;
4286
- void this.integrateWorktree;
4287
- void this.applyTaskFailure;
4288
- void this.retryMap;
4289
- void this.persistRetries;
4290
- void this.maxRetries;
4291
- void this.trySupervisorRescue;
4292
- void this.supervisorEscalations;
4293
- void this.maxSupervisorEscalations;
4294
- void this.setTaskModel;
4295
- void this.splitTask;
4296
- return this;
4297
- }
4298
4385
  };
4299
4386
 
4300
4387
  // src/start-sdd-run.ts
@@ -5033,7 +5120,7 @@ function templateToMarkdown(template, title) {
5033
5120
 
5034
5121
  // src/task-visualizer.ts
5035
5122
  import { computeTaskProgress as computeTaskProgress2 } from "@wrongstack/core/tasking";
5036
- import { expectDefined as expectDefined4, truncate } from "@wrongstack/core/utils";
5123
+ import { expectDefined as expectDefined5, truncate } from "@wrongstack/core/utils";
5037
5124
  var STATUS_ICON = {
5038
5125
  pending: "\u25CB",
5039
5126
  in_progress: "\u25D0",
@@ -5103,7 +5190,7 @@ function renderTaskGraph(graph, opts) {
5103
5190
  function renderNode(graph, nodeId, lines, rendered, childrenMap, dependentsMap, compact, prefix) {
5104
5191
  if (rendered.has(nodeId)) return;
5105
5192
  rendered.add(nodeId);
5106
- const node = expectDefined4(graph.nodes.get(nodeId));
5193
+ const node = expectDefined5(graph.nodes.get(nodeId));
5107
5194
  const icon = STATUS_ICON[node.status];
5108
5195
  const prioIcon = PRIORITY_ICON[node.priority];
5109
5196
  const typeIcon = TYPE_ICON[node.type];
@@ -5193,7 +5280,7 @@ function renderSpecAnalysis(spec, analysis) {
5193
5280
 
5194
5281
  // src/critical-path.ts
5195
5282
  import { topologicalSort } from "@wrongstack/core/tasking";
5196
- import { expectDefined as expectDefined5 } from "@wrongstack/core/utils";
5283
+ import { expectDefined as expectDefined6 } from "@wrongstack/core/utils";
5197
5284
  function analyzeCriticalPath(graph) {
5198
5285
  const nodes = Array.from(graph.nodes.values());
5199
5286
  const topoOrder = topologicalSort(graph);
@@ -5272,7 +5359,7 @@ function getTransitiveBlocked(_graph, taskId, blocksMap) {
5272
5359
  const visited = /* @__PURE__ */ new Set();
5273
5360
  const queue = [taskId];
5274
5361
  while (queue.length > 0) {
5275
- const current = expectDefined5(queue.shift());
5362
+ const current = expectDefined6(queue.shift());
5276
5363
  const blocked = blocksMap.get(current);
5277
5364
  if (!blocked) continue;
5278
5365
  for (const id of blocked) {
@@ -5318,7 +5405,7 @@ function computeCriticalPath(graph, _topoOrder, blockedByMap) {
5318
5405
  if (!changed) break;
5319
5406
  }
5320
5407
  let maxDist = 0;
5321
- let maxId = expectDefined5(allIds[0]);
5408
+ let maxId = expectDefined6(allIds[0]);
5322
5409
  for (const id of allIds) {
5323
5410
  const d = dist.get(id);
5324
5411
  if (d > maxDist) {
@@ -5361,7 +5448,7 @@ function computeParallelGroups(graph, blockedByMap) {
5361
5448
  }
5362
5449
  }
5363
5450
  if (group.length === 0) {
5364
- group.push(expectDefined5(Array.from(remaining)[0]));
5451
+ group.push(expectDefined6(Array.from(remaining)[0]));
5365
5452
  }
5366
5453
  for (const id of group) {
5367
5454
  assigned.add(id);