@wrongstack/sdd 0.295.0 → 0.296.2

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.
Files changed (49) hide show
  1. package/dist/auto-executor.d.ts +3 -4
  2. package/dist/auto-executor.d.ts.map +1 -1
  3. package/dist/board-types.d.ts +4 -0
  4. package/dist/board-types.d.ts.map +1 -1
  5. package/dist/conflict-resolver.d.ts +7 -1
  6. package/dist/conflict-resolver.d.ts.map +1 -1
  7. package/dist/critical-path.d.ts.map +1 -1
  8. package/dist/decompose-task.d.ts +21 -0
  9. package/dist/decompose-task.d.ts.map +1 -1
  10. package/dist/graph-split.d.ts +25 -0
  11. package/dist/graph-split.d.ts.map +1 -0
  12. package/dist/index.d.ts +6 -3
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +848 -394
  15. package/dist/index.js.map +4 -4
  16. package/dist/plan-decompose.d.ts +47 -0
  17. package/dist/plan-decompose.d.ts.map +1 -0
  18. package/dist/project-context.d.ts +6 -0
  19. package/dist/project-context.d.ts.map +1 -0
  20. package/dist/sdd-board-projector.d.ts +14 -1
  21. package/dist/sdd-board-projector.d.ts.map +1 -1
  22. package/dist/sdd-board-store.d.ts +16 -0
  23. package/dist/sdd-board-store.d.ts.map +1 -1
  24. package/dist/sdd-interview-driver.d.ts +17 -2
  25. package/dist/sdd-interview-driver.d.ts.map +1 -1
  26. package/dist/sdd-lifecycle.d.ts.map +1 -1
  27. package/dist/sdd-parallel-run-types.d.ts +200 -0
  28. package/dist/sdd-parallel-run-types.d.ts.map +1 -0
  29. package/dist/sdd-parallel-run.d.ts +22 -192
  30. package/dist/sdd-parallel-run.d.ts.map +1 -1
  31. package/dist/sdd-task-execution.d.ts +28 -0
  32. package/dist/sdd-task-execution.d.ts.map +1 -0
  33. package/dist/spec-builder.d.ts +21 -3
  34. package/dist/spec-builder.d.ts.map +1 -1
  35. package/dist/spec-parser.d.ts +1 -1
  36. package/dist/spec-parser.d.ts.map +1 -1
  37. package/dist/spec-versioning.d.ts +1 -2
  38. package/dist/spec-versioning.d.ts.map +1 -1
  39. package/dist/start-sdd-run.d.ts +8 -2
  40. package/dist/start-sdd-run.d.ts.map +1 -1
  41. package/dist/task-flow.d.ts +1 -3
  42. package/dist/task-flow.d.ts.map +1 -1
  43. package/dist/task-generator.d.ts +31 -2
  44. package/dist/task-generator.d.ts.map +1 -1
  45. package/dist/task-visualizer.d.ts +1 -2
  46. package/dist/task-visualizer.d.ts.map +1 -1
  47. package/dist/verify-task.d.ts +31 -2
  48. package/dist/verify-task.d.ts.map +1 -1
  49. package/package.json +3 -2
package/dist/index.js CHANGED
@@ -57,13 +57,13 @@ var SpecParser = class {
57
57
  if (h2) {
58
58
  if (currentSection && currentLines.length > 0) {
59
59
  sections.push({
60
- type: this.mapSectionType(currentSection.title ?? "unknown"),
61
- title: currentSection.title ?? "Unknown",
60
+ type: this.mapSectionType(currentSection.title),
61
+ title: currentSection.title,
62
62
  level: depth,
63
63
  content: currentLines.join("\n").trim()
64
64
  });
65
65
  }
66
- currentSection = { title: h2[1] ?? "Unknown" };
66
+ currentSection = { title: h2[1] };
67
67
  currentLines = [];
68
68
  depth = 2;
69
69
  continue;
@@ -78,8 +78,8 @@ var SpecParser = class {
78
78
  }
79
79
  if (currentSection && currentLines.length > 0) {
80
80
  sections.push({
81
- type: this.mapSectionType(currentSection.title ?? "unknown"),
82
- title: currentSection.title ?? "Unknown",
81
+ type: this.mapSectionType(currentSection.title),
82
+ title: currentSection.title,
83
83
  level: depth,
84
84
  content: currentLines.join("\n").trim()
85
85
  });
@@ -227,6 +227,39 @@ var SpecParser = class {
227
227
  };
228
228
 
229
229
  // src/task-generator.ts
230
+ import { assessAtomicity } from "@wrongstack/kanban";
231
+ var OVERVIEW_ESTIMATE_HOURS = 4;
232
+ var REQUIREMENT_ESTIMATE_HOURS = {
233
+ critical: 8,
234
+ high: 4,
235
+ medium: 2,
236
+ low: 1
237
+ };
238
+ var API_PARENT_ESTIMATE_HOURS = 0;
239
+ var API_BASE_ESTIMATE_HOURS = 2;
240
+ var TESTS_ESTIMATE_HOURS = 4;
241
+ var DOCS_ESTIMATE_HOURS = 2;
242
+ function assessGeneratedTaskAtomicity(task, config) {
243
+ const criteria = task.acceptanceCriteria ?? [];
244
+ const assessment = assessAtomicity(
245
+ {
246
+ title: task.title,
247
+ description: task.description,
248
+ estimatedHours: task.estimateHours,
249
+ // Graph edges are wired after generation; fan-in is unknown here.
250
+ dependencyCount: 0,
251
+ successCriteriaCount: criteria.length,
252
+ hasVerifiableOutput: extractVerificationCommand(criteria) !== void 0,
253
+ childCount: 0
254
+ },
255
+ config
256
+ );
257
+ return {
258
+ verdict: assessment.verdict,
259
+ score: assessment.score,
260
+ reasons: assessment.criteria.filter((c) => c.score < 1).map((c) => c.reason)
261
+ };
262
+ }
230
263
  function extractVerificationCommand(criteria) {
231
264
  const marker = /^\s*(?:\$\s+|(?:run|verify|cmd)\s*:\s*)(.+\S)\s*$/i;
232
265
  for (const c of criteria) {
@@ -240,17 +273,29 @@ var TaskGenerator = class {
240
273
  this.opts = opts;
241
274
  }
242
275
  opts;
276
+ /** metadata.atomicity payload when the option is enabled, else undefined. */
277
+ atomicityMetadata(task) {
278
+ if (!this.opts.atomicity) return void 0;
279
+ return { atomicity: assessGeneratedTaskAtomicity(task, this.opts.atomicity.config) };
280
+ }
243
281
  async generateFromSpec(spec) {
244
282
  const graph = await this.opts.taskTracker.createGraph(spec.id, spec.title);
245
283
  const overviewSection = spec.sections?.find((s) => s.type === "overview");
246
284
  if (overviewSection?.content) {
247
- this.opts.taskTracker.addNode({
285
+ const overview = {
248
286
  title: `Implement: ${spec.title}`,
249
287
  description: overviewSection.content,
288
+ estimateHours: OVERVIEW_ESTIMATE_HOURS
289
+ };
290
+ const metadata = this.atomicityMetadata(overview);
291
+ this.opts.taskTracker.addNode({
292
+ title: overview.title,
293
+ description: overview.description,
250
294
  type: "feature",
251
295
  priority: "high",
252
296
  status: "pending",
253
- estimateHours: 4
297
+ estimateHours: overview.estimateHours,
298
+ ...metadata ? { metadata } : {}
254
299
  });
255
300
  }
256
301
  const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
@@ -258,7 +303,7 @@ var TaskGenerator = class {
258
303
  (a, b) => (priorityOrder[a.priority] ?? 9) - (priorityOrder[b.priority] ?? 9)
259
304
  );
260
305
  for (const req of sorted) {
261
- const estimateHours = req.priority === "critical" ? 8 : req.priority === "high" ? 4 : req.priority === "medium" ? 2 : 1;
306
+ const estimateHours = REQUIREMENT_ESTIMATE_HOURS[req.priority] ?? 1;
262
307
  const tags = [req.type, req.priority];
263
308
  const acLines = (req.acceptanceCriteria ?? []).map((ac) => `- ${ac}`).join("\n");
264
309
  const blockedLine = req.blockedBy?.length ? `
@@ -270,7 +315,14 @@ var TaskGenerator = class {
270
315
 
271
316
  **Acceptance Criteria:**
272
317
  ${acLines}` : "") + blockedLine;
273
- const metadata = {};
318
+ const metadata = {
319
+ ...this.atomicityMetadata({
320
+ title: req.description,
321
+ description,
322
+ estimateHours,
323
+ acceptanceCriteria: req.acceptanceCriteria ?? []
324
+ })
325
+ };
274
326
  if (this.opts.verificationFromAcceptance) {
275
327
  const cmd = extractVerificationCommand(req.acceptanceCriteria ?? []);
276
328
  if (cmd) metadata.verificationCommand = cmd;
@@ -294,38 +346,58 @@ ${acLines}` : "") + blockedLine;
294
346
  type: "feature",
295
347
  priority: "high",
296
348
  status: "pending",
297
- estimateHours: 0
349
+ estimateHours: API_PARENT_ESTIMATE_HOURS
298
350
  });
299
351
  for (const ep of spec.apiEndpoints) {
300
- const baseHours = 2;
301
352
  const authHours = ep.auth ? 1 : 0;
302
353
  const reqHours = ep.request ? 1 : 0;
303
- this.opts.taskTracker.addNode({
354
+ const endpoint = {
304
355
  title: `${ep.method} ${ep.path} \u2014 ${ep.description}`,
305
356
  description: `${ep.method} ${ep.path}: ${ep.description}`,
357
+ estimateHours: API_BASE_ESTIMATE_HOURS + authHours + reqHours
358
+ };
359
+ const metadata = this.atomicityMetadata(endpoint);
360
+ this.opts.taskTracker.addNode({
361
+ title: endpoint.title,
362
+ description: endpoint.description,
306
363
  type: "feature",
307
364
  priority: "medium",
308
365
  status: "pending",
309
- estimateHours: baseHours + authHours + reqHours,
310
- parentId: apiParent.id
366
+ estimateHours: endpoint.estimateHours,
367
+ parentId: apiParent.id,
368
+ ...metadata ? { metadata } : {}
311
369
  });
312
370
  }
313
371
  }
314
- this.opts.taskTracker.addNode({
372
+ const testsTask = {
315
373
  title: "Write Tests",
316
374
  description: "Write comprehensive tests for the implemented features.",
375
+ estimateHours: TESTS_ESTIMATE_HOURS
376
+ };
377
+ const testsMetadata = this.atomicityMetadata(testsTask);
378
+ this.opts.taskTracker.addNode({
379
+ title: testsTask.title,
380
+ description: testsTask.description,
317
381
  type: "test",
318
382
  priority: "high",
319
383
  status: "pending",
320
- estimateHours: 4
384
+ estimateHours: testsTask.estimateHours,
385
+ ...testsMetadata ? { metadata: testsMetadata } : {}
321
386
  });
322
- this.opts.taskTracker.addNode({
387
+ const docsTask = {
323
388
  title: "Update Documentation",
324
389
  description: "Update project documentation to reflect the changes.",
390
+ estimateHours: DOCS_ESTIMATE_HOURS
391
+ };
392
+ const docsMetadata = this.atomicityMetadata(docsTask);
393
+ this.opts.taskTracker.addNode({
394
+ title: docsTask.title,
395
+ description: docsTask.description,
325
396
  type: "docs",
326
397
  priority: "low",
327
398
  status: "pending",
328
- estimateHours: 2
399
+ estimateHours: docsTask.estimateHours,
400
+ ...docsMetadata ? { metadata: docsMetadata } : {}
329
401
  });
330
402
  return graph;
331
403
  }
@@ -356,8 +428,9 @@ import {
356
428
  } from "@wrongstack/core/tasking";
357
429
 
358
430
  // src/task-flow.ts
359
- import { SddError, ERROR_CODES } from "@wrongstack/core/types";
360
431
  import { DefaultTaskStore, TaskTracker } from "@wrongstack/core/tasking";
432
+ import { ERROR_CODES, SddError } from "@wrongstack/core/types";
433
+ import { expectDefined } from "@wrongstack/core/utils";
361
434
  var TaskFlow = class {
362
435
  constructor(opts) {
363
436
  this.opts = opts;
@@ -397,11 +470,12 @@ var TaskFlow = class {
397
470
  return this.graph;
398
471
  }
399
472
  async execute(ctx) {
400
- if (!this.graph) throw new SddError({
401
- message: "No graph loaded. Call fromSpec first.",
402
- code: ERROR_CODES.SDD_INVALID_STATE,
403
- context: { phase: this.phase }
404
- });
473
+ if (!this.graph)
474
+ throw new SddError({
475
+ message: "No graph loaded. Call fromSpec first.",
476
+ code: ERROR_CODES.SDD_INVALID_STATE,
477
+ context: { phase: this.phase }
478
+ });
405
479
  this.setPhase("executing");
406
480
  this.stopped = false;
407
481
  const pendingTasks = this.getExecutableTasks();
@@ -412,9 +486,8 @@ var TaskFlow = class {
412
486
  batch.map((task) => this.executeSingleTask(task, ctx))
413
487
  );
414
488
  for (let i = 0; i < results.length; i++) {
415
- const result = results[i];
416
- const task = batch[i];
417
- if (!result || !task) continue;
489
+ const result = expectDefined(results[i]);
490
+ const task = expectDefined(batch[i]);
418
491
  if (result.status === "rejected") {
419
492
  const reason = result.reason;
420
493
  this.opts.tracker.updateNodeStatus(task.id, "failed", reason?.message);
@@ -441,11 +514,12 @@ var TaskFlow = class {
441
514
  }
442
515
  async reviewTask(taskId, approved, comment) {
443
516
  const task = this.opts.tracker.getNode(taskId);
444
- if (!task) throw new SddError({
445
- message: `Task ${taskId} not found`,
446
- code: ERROR_CODES.SDD_NOT_READY,
447
- context: { taskId }
448
- });
517
+ if (!task)
518
+ throw new SddError({
519
+ message: `Task ${taskId} not found`,
520
+ code: ERROR_CODES.SDD_NOT_READY,
521
+ context: { taskId }
522
+ });
449
523
  if (approved) {
450
524
  this.opts.tracker.updateNodeStatus(taskId, "completed", comment);
451
525
  this.emit("task.completed", { taskId });
@@ -474,7 +548,7 @@ var TaskFlow = class {
474
548
  getExecutableTasks() {
475
549
  return this.opts.tracker.getAllNodes({ status: ["pending", "blocked"] }).filter((n) => n.status === "pending" && this.opts.tracker.canStart(n.id)).sort((a, b) => {
476
550
  const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
477
- return (priorityOrder[a.priority] ?? 4) - (priorityOrder[b.priority] ?? 4);
551
+ return priorityOrder[a.priority] - priorityOrder[b.priority];
478
552
  });
479
553
  }
480
554
  async executeSingleTask(task, ctx) {
@@ -792,14 +866,14 @@ function buildBoardTasks(graph) {
792
866
  return d;
793
867
  };
794
868
  const toTask = (n) => {
795
- const deps = blockers.get(n.id) ?? [];
869
+ const deps = blockers.get(n.id);
796
870
  const allDepsDone = deps.every((b) => statusOf(b) === "completed");
797
871
  const meta = n.metadata ?? {};
798
872
  const cancelled = Boolean(meta["cancelled"]);
799
873
  const displayStatus = cancelled ? "cancelled" : n.status === "pending" && deps.length > 0 && allDepsDone ? "queued" : n.status;
800
874
  return {
801
875
  id: n.id,
802
- shortId: shortId.get(n.id) ?? n.id.slice(0, 6),
876
+ shortId: shortId.get(n.id),
803
877
  title: n.title,
804
878
  description: n.description,
805
879
  status: n.status,
@@ -815,7 +889,9 @@ function buildBoardTasks(graph) {
815
889
  model: typeof meta["model"] === "string" ? meta["model"] : void 0,
816
890
  provider: typeof meta["provider"] === "string" ? meta["provider"] : void 0,
817
891
  fallbackModels: Array.isArray(meta["fallbackModels"]) ? meta["fallbackModels"] : void 0,
818
- verificationCommand: typeof meta["verificationCommand"] === "string" ? meta["verificationCommand"] : void 0
892
+ verificationCommand: typeof meta["verificationCommand"] === "string" ? meta["verificationCommand"] : void 0,
893
+ verificationState: meta["verificationState"] === "passed" || meta["verificationState"] === "failed" ? meta["verificationState"] : void 0,
894
+ verificationDetail: typeof meta["verificationDetail"] === "string" ? meta["verificationDetail"] : void 0
819
895
  };
820
896
  };
821
897
  const tasks = nodes.map(toTask);
@@ -823,9 +899,9 @@ function buildBoardTasks(graph) {
823
899
  for (const n of nodes) {
824
900
  const d = depthOf(n.id);
825
901
  if (!byDepth.has(d)) byDepth.set(d, []);
826
- byDepth.get(d)?.push(shortId.get(n.id) ?? n.id.slice(0, 6));
902
+ byDepth.get(d).push(shortId.get(n.id));
827
903
  }
828
- const columns = [...byDepth.keys()].sort((a, b) => a - b).map((d) => ({ label: d === 0 ? "Start" : `Phase ${d}`, taskIds: byDepth.get(d) ?? [] }));
904
+ const columns = [...byDepth.keys()].sort((a, b) => a - b).map((d) => ({ label: d === 0 ? "Start" : `Phase ${d}`, taskIds: byDepth.get(d) }));
829
905
  return { tasks, columns };
830
906
  }
831
907
  function buildBoardSnapshot(graph, run, now) {
@@ -864,6 +940,7 @@ var SddBoardStore = class {
864
940
  eventMaxBytes;
865
941
  eventKeepBytes;
866
942
  eventSizeCheckEvery;
943
+ controlFileIO;
867
944
  eventChains = /* @__PURE__ */ new Map();
868
945
  eventWritesSinceCheck = /* @__PURE__ */ new Map();
869
946
  controlDrains = /* @__PURE__ */ new Map();
@@ -882,6 +959,7 @@ var SddBoardStore = class {
882
959
  1,
883
960
  Math.floor(opts.eventSizeCheckEvery ?? DEFAULT_EVENT_SIZE_CHECK_EVERY)
884
961
  );
962
+ this.controlFileIO = opts.controlFileIO ?? fsp3;
885
963
  }
886
964
  snapshotPath(runId) {
887
965
  return path3.join(this.baseDir, `${this.safe(runId)}.json`);
@@ -952,12 +1030,12 @@ var SddBoardStore = class {
952
1030
  try {
953
1031
  return await drain;
954
1032
  } finally {
955
- if (this.controlDrains.get(filePath) === drain) this.controlDrains.delete(filePath);
1033
+ this.controlDrains.delete(filePath);
956
1034
  }
957
1035
  }
958
1036
  async delete(runId) {
959
1037
  const eventPath = this.eventsPath(runId);
960
- await this.eventChains.get(eventPath)?.catch(() => void 0);
1038
+ await this.eventChains.get(eventPath);
961
1039
  this.eventChains.delete(eventPath);
962
1040
  await Promise.allSettled([
963
1041
  fsp3.unlink(this.snapshotPath(runId)),
@@ -1005,13 +1083,10 @@ var SddBoardStore = class {
1005
1083
  const buffer = Buffer.allocUnsafe(length);
1006
1084
  const { bytesRead } = await handle.read(buffer, 0, length, start);
1007
1085
  retained = buffer.subarray(0, bytesRead);
1008
- if (start > 0 && retained.length > 0) {
1009
- const previous = Buffer.allocUnsafe(1);
1010
- const preceding = await handle.read(previous, 0, 1, start - 1);
1011
- if (preceding.bytesRead !== 1 || previous[0] !== 10) {
1012
- const firstNewline = retained.indexOf(10);
1013
- retained = firstNewline >= 0 ? retained.subarray(firstNewline + 1) : retained.subarray(0, 0);
1014
- }
1086
+ const previous = Buffer.allocUnsafe(1);
1087
+ await handle.read(previous, 0, 1, start - 1);
1088
+ if (previous[0] !== 10) {
1089
+ retained = retained.subarray(retained.indexOf(10) + 1);
1015
1090
  }
1016
1091
  } finally {
1017
1092
  await handle.close();
@@ -1020,7 +1095,7 @@ var SddBoardStore = class {
1020
1095
  }
1021
1096
  async drainControlInternal(filePath) {
1022
1097
  try {
1023
- const stat2 = await fsp3.stat(filePath);
1098
+ const stat2 = await this.controlFileIO.stat(filePath);
1024
1099
  if (stat2.size === 0) return [];
1025
1100
  } catch {
1026
1101
  return [];
@@ -1028,14 +1103,14 @@ var SddBoardStore = class {
1028
1103
  return withFileLock(filePath, async () => {
1029
1104
  let raw;
1030
1105
  try {
1031
- const stat2 = await fsp3.stat(filePath);
1106
+ const stat2 = await this.controlFileIO.stat(filePath);
1032
1107
  if (stat2.size === 0) return [];
1033
- raw = await fsp3.readFile(filePath, "utf8");
1108
+ raw = await this.controlFileIO.readFile(filePath, "utf8");
1034
1109
  } catch {
1035
1110
  return [];
1036
1111
  }
1037
1112
  try {
1038
- await fsp3.truncate(filePath, 0);
1113
+ await this.controlFileIO.truncate(filePath, 0);
1039
1114
  } catch {
1040
1115
  return [];
1041
1116
  }
@@ -1160,7 +1235,6 @@ var SddBoardProjector = class _SddBoardProjector {
1160
1235
  mergedCommits = [];
1161
1236
  /** Base branch reported by the run at start (overrides the constructor option). */
1162
1237
  runBaseBranch;
1163
- dirty = false;
1164
1238
  timer = null;
1165
1239
  unsubs = [];
1166
1240
  /** Latest snapshot waiting behind an in-flight disk write. */
@@ -1189,7 +1263,11 @@ var SddBoardProjector = class _SddBoardProjector {
1189
1263
  });
1190
1264
  this.onRun("sdd.wave", (e) => {
1191
1265
  this.wave = e.wave;
1192
- this.pushFeed({ ts: this.now(), kind: "wave", text: `Wave ${e.wave + 1} started \xB7 ${e.batchSize} task(s) in parallel` });
1266
+ this.pushFeed({
1267
+ ts: this.now(),
1268
+ kind: "wave",
1269
+ text: `Wave ${e.wave + 1} started \xB7 ${e.batchSize} task(s) in parallel`
1270
+ });
1193
1271
  this.markDirty();
1194
1272
  });
1195
1273
  this.onRun("sdd.deadlock", (e) => {
@@ -1197,7 +1275,11 @@ var SddBoardProjector = class _SddBoardProjector {
1197
1275
  blocked: this.shortId.get(c.blocked) ?? c.blocked.slice(0, 6),
1198
1276
  blockedBy: c.blockedBy.map((b) => this.shortId.get(b) ?? b.slice(0, 6))
1199
1277
  }));
1200
- this.pushFeed({ ts: this.now(), kind: "deadlock", text: `Deadlock \u2014 ${e.chains.length} task(s) blocked by failed work` });
1278
+ this.pushFeed({
1279
+ ts: this.now(),
1280
+ kind: "deadlock",
1281
+ text: `Deadlock \u2014 ${e.chains.length} task(s) blocked by failed work`
1282
+ });
1201
1283
  this.markDirty();
1202
1284
  });
1203
1285
  this.onRun("sdd.task.started", (e) => {
@@ -1377,7 +1459,8 @@ var SddBoardProjector = class _SddBoardProjector {
1377
1459
  }
1378
1460
  pushFeed(entry) {
1379
1461
  this.feed.unshift(entry);
1380
- if (this.feed.length > _SddBoardProjector.FEED_CAP) this.feed.length = _SddBoardProjector.FEED_CAP;
1462
+ if (this.feed.length > _SddBoardProjector.FEED_CAP)
1463
+ this.feed.length = _SddBoardProjector.FEED_CAP;
1381
1464
  if (entry.taskId) {
1382
1465
  const taskFeed = this.taskEvents.get(entry.taskId) ?? [];
1383
1466
  taskFeed.unshift(entry);
@@ -1402,12 +1485,7 @@ var SddBoardProjector = class _SddBoardProjector {
1402
1485
  }
1403
1486
  /** Resolve once all in-flight snapshot persistence has settled. */
1404
1487
  async drain() {
1405
- for (; ; ) {
1406
- const observed = this.saveLoop;
1407
- if (!observed) return;
1408
- await observed;
1409
- if (observed === this.saveLoop && !this.pendingSnapshot) return;
1410
- }
1488
+ while (this.saveLoop) await this.saveLoop;
1411
1489
  }
1412
1490
  /** Stop projecting and release subscriptions. */
1413
1491
  dispose() {
@@ -1474,15 +1552,13 @@ var SddBoardProjector = class _SddBoardProjector {
1474
1552
  return snap;
1475
1553
  }
1476
1554
  markDirty() {
1477
- this.dirty = true;
1478
1555
  if (this.timer || this.finished) return;
1479
1556
  this.timer = setTimeout(() => {
1480
1557
  this.timer = null;
1481
- if (this.dirty) this.flush();
1558
+ this.flush();
1482
1559
  }, this.throttleMs);
1483
1560
  }
1484
1561
  flush() {
1485
- this.dirty = false;
1486
1562
  if (this.timer) {
1487
1563
  clearTimeout(this.timer);
1488
1564
  this.timer = null;
@@ -1504,9 +1580,7 @@ var SddBoardProjector = class _SddBoardProjector {
1504
1580
  const loop = this.persistPendingSnapshots(store);
1505
1581
  this.saveLoop = loop;
1506
1582
  void loop.finally(() => {
1507
- if (this.saveLoop !== loop) return;
1508
1583
  this.saveLoop = void 0;
1509
- if (this.pendingSnapshot) this.startSaveLoop(store);
1510
1584
  });
1511
1585
  }
1512
1586
  async persistPendingSnapshots(store) {
@@ -1537,9 +1611,12 @@ var SddRunRegistry = class {
1537
1611
  }
1538
1612
  };
1539
1613
 
1614
+ // src/sdd-interview-driver.ts
1615
+ import { DefaultTaskStore as DefaultTaskStore2, TaskTracker as TaskTracker2 } from "@wrongstack/core/tasking";
1616
+
1540
1617
  // src/spec-builder.ts
1541
- import { expectDefined, toErrorMessage } from "@wrongstack/core/utils";
1542
- import { SddError as SddError2, ERROR_CODES as ERROR_CODES2 } from "@wrongstack/core/types";
1618
+ import { ERROR_CODES as ERROR_CODES2, SddError as SddError2 } from "@wrongstack/core/types";
1619
+ import { expectDefined as expectDefined2, toErrorMessage } from "@wrongstack/core/utils";
1543
1620
  function buildQuestioningPrompt(session, min, max) {
1544
1621
  const answered = session.answers.length;
1545
1622
  const remaining = Math.max(0, min - answered);
@@ -1588,7 +1665,7 @@ function buildQuestioningPrompt(session, min, max) {
1588
1665
  if (answered > 0) {
1589
1666
  lines.push("", "**Conversation so far:**");
1590
1667
  for (let i = 0; i < answered; i++) {
1591
- const a = expectDefined(session.answers[i]);
1668
+ const a = expectDefined2(session.answers[i]);
1592
1669
  lines.push(``, `Q${i + 1}: ${a.question}`, `A${i + 1}: ${a.answer}`);
1593
1670
  }
1594
1671
  }
@@ -1759,10 +1836,10 @@ var AISpecBuilder = class {
1759
1836
  async saveSession() {
1760
1837
  if (!this.sessionPath) return;
1761
1838
  try {
1762
- const fsp5 = await import("node:fs/promises");
1763
- const path4 = await import("node:path");
1839
+ const fsp6 = await import("node:fs/promises");
1840
+ const path6 = await import("node:path");
1764
1841
  const { atomicWrite: atomicWrite4 } = await import("@wrongstack/core/utils");
1765
- await fsp5.mkdir(path4.dirname(this.sessionPath), { recursive: true });
1842
+ await fsp6.mkdir(path6.dirname(this.sessionPath), { recursive: true });
1766
1843
  await atomicWrite4(this.sessionPath, JSON.stringify(this.session, null, 2));
1767
1844
  } catch {
1768
1845
  }
@@ -1771,8 +1848,8 @@ var AISpecBuilder = class {
1771
1848
  async loadSession() {
1772
1849
  if (!this.sessionPath) return false;
1773
1850
  try {
1774
- const fsp5 = await import("node:fs/promises");
1775
- const raw = await fsp5.readFile(this.sessionPath, "utf8");
1851
+ const fsp6 = await import("node:fs/promises");
1852
+ const raw = await fsp6.readFile(this.sessionPath, "utf8");
1776
1853
  const loaded = JSON.parse(raw);
1777
1854
  if (loaded?.id && loaded?.phase && loaded?.title) {
1778
1855
  this.session = loaded;
@@ -1786,22 +1863,14 @@ var AISpecBuilder = class {
1786
1863
  async deleteSession() {
1787
1864
  if (!this.sessionPath) return;
1788
1865
  try {
1789
- const fsp5 = await import("node:fs/promises");
1790
- await fsp5.unlink(this.sessionPath);
1866
+ const fsp6 = await import("node:fs/promises");
1867
+ await fsp6.unlink(this.sessionPath);
1791
1868
  } catch {
1792
1869
  }
1793
1870
  }
1794
- /** Auto-save helper — calls saveSession() but never throws.
1795
- * Failures are surfaced via process.emitWarning so a persistent
1796
- * ENOSPC / EACCES doesn't silently strand session edits in memory. */
1871
+ /** Auto-save helper. saveSession() already handles best-effort persistence. */
1797
1872
  autoSave() {
1798
- this.saveSession().catch((err) => {
1799
- const detail = toErrorMessage(err);
1800
- process.emitWarning(
1801
- `SpecBuilder autoSave failed: ${detail}`,
1802
- "SpecBuilderWarning"
1803
- );
1804
- });
1873
+ void this.saveSession();
1805
1874
  }
1806
1875
  // ── Session Lifecycle ─────────────────────────────────────────────────────
1807
1876
  /** Start a new session with a title and optional intent. */
@@ -1946,6 +2015,43 @@ var AISpecBuilder = class {
1946
2015
  getTaskGraphId() {
1947
2016
  return this.session.taskGraphId;
1948
2017
  }
2018
+ /** Persist the last agent utterance so resume can rehydrate the UI + Q/A pairing. */
2019
+ setLastAgentText(text) {
2020
+ this.session.lastAgentText = text;
2021
+ this.session.updatedAt = Date.now();
2022
+ this.autoSave();
2023
+ }
2024
+ getLastAgentText() {
2025
+ return this.session.lastAgentText;
2026
+ }
2027
+ /** Record a run kicked off from this interview (board deep-link after restart). */
2028
+ setLastRunId(runId) {
2029
+ this.session.lastRunId = runId;
2030
+ this.session.updatedAt = Date.now();
2031
+ this.autoSave();
2032
+ }
2033
+ getLastRunId() {
2034
+ return this.session.lastRunId;
2035
+ }
2036
+ /**
2037
+ * Hard-reset in-memory session fields while keeping the same session id /
2038
+ * store binding. Used when the operator abandons a resumed interview and
2039
+ * starts a brand-new goal (the next save overwrites the session file).
2040
+ */
2041
+ resetForNewInterview() {
2042
+ this.session.phase = "questioning";
2043
+ this.session.title = "";
2044
+ this.session.userIntent = "";
2045
+ this.session.answers = [];
2046
+ this.session.questionCount = 0;
2047
+ this.session.spec = void 0;
2048
+ this.session.implementation = void 0;
2049
+ this.session.taskGraphId = void 0;
2050
+ this.session.lastAgentText = void 0;
2051
+ this.session.lastRunId = void 0;
2052
+ this.session.approved = false;
2053
+ this.session.updatedAt = Date.now();
2054
+ }
1949
2055
  // ── Spec Persistence ──────────────────────────────────────────────────────
1950
2056
  /**
1951
2057
  * Save the current spec to the store.
@@ -1975,7 +2081,7 @@ var AISpecBuilder = class {
1975
2081
  message: "Invalid JSON for spec",
1976
2082
  code: ERROR_CODES2.SDD_PARSE_FAILED,
1977
2083
  cause: e,
1978
- context: { detail: e instanceof Error ? e.message : "parse error" }
2084
+ context: { detail: toErrorMessage(e) }
1979
2085
  });
1980
2086
  }
1981
2087
  if (!parsed || typeof parsed !== "object") {
@@ -1987,7 +2093,7 @@ var AISpecBuilder = class {
1987
2093
  }
1988
2094
  const raw = parsed;
1989
2095
  const now = Date.now();
1990
- const title = String(raw.title ?? this.session.title ?? "Untitled");
2096
+ const title = String(raw.title ?? this.session.title);
1991
2097
  const overview = String(raw.overview ?? "");
1992
2098
  if (!overview || overview === "undefined") {
1993
2099
  throw new SddError2({
@@ -1998,7 +2104,15 @@ var AISpecBuilder = class {
1998
2104
  }
1999
2105
  const rawSections = Array.isArray(raw.sections) ? raw.sections : [];
2000
2106
  const sections = rawSections.filter((s) => s && typeof s === "object").map((s) => ({
2001
- type: ["overview", "requirements", "architecture", "api", "data", "security", "acceptance"].includes(String(s.type)) ? String(s.type) : "overview",
2107
+ type: [
2108
+ "overview",
2109
+ "requirements",
2110
+ "architecture",
2111
+ "api",
2112
+ "data",
2113
+ "security",
2114
+ "acceptance"
2115
+ ].includes(String(s.type)) ? String(s.type) : "overview",
2002
2116
  title: String(s.title ?? ""),
2003
2117
  content: String(s.content ?? ""),
2004
2118
  level: Number(s.level) || 1
@@ -2006,7 +2120,9 @@ var AISpecBuilder = class {
2006
2120
  const rawReqs = Array.isArray(raw.requirements) ? raw.requirements : [];
2007
2121
  const requirements = rawReqs.filter((r) => r && typeof r === "object").map((r, i) => ({
2008
2122
  id: String(r.id ?? `REQ-${i + 1}`),
2009
- type: ["functional", "non-functional", "security", "performance", "ux"].includes(String(r.type)) ? String(r.type) : "functional",
2123
+ type: ["functional", "non-functional", "security", "performance", "ux"].includes(
2124
+ String(r.type)
2125
+ ) ? String(r.type) : "functional",
2010
2126
  priority: ["critical", "high", "medium", "low"].includes(String(r.priority)) ? String(r.priority) : "medium",
2011
2127
  description: String(r.description ?? ""),
2012
2128
  acceptanceCriteria: Array.isArray(r.acceptanceCriteria) ? r.acceptanceCriteria.map(String) : []
@@ -2095,7 +2211,6 @@ var AISpecBuilder = class {
2095
2211
  };
2096
2212
 
2097
2213
  // src/sdd-interview-driver.ts
2098
- import { TaskTracker as TaskTracker2, DefaultTaskStore as DefaultTaskStore2 } from "@wrongstack/core/tasking";
2099
2214
  var SddInterviewDriver = class {
2100
2215
  builder;
2101
2216
  o;
@@ -2103,6 +2218,8 @@ var SddInterviewDriver = class {
2103
2218
  maxQuestions;
2104
2219
  tracker = null;
2105
2220
  graph = null;
2221
+ /** Set when {@link loadExisting} successfully rehydrated a session from disk. */
2222
+ resumedFromDisk = false;
2106
2223
  constructor(opts) {
2107
2224
  this.o = opts;
2108
2225
  this.minQuestions = opts.minQuestions ?? 2;
@@ -2117,9 +2234,11 @@ var SddInterviewDriver = class {
2117
2234
  }
2118
2235
  /** Begin a fresh interview. Returns the first AI prompt (a question kickoff). */
2119
2236
  start(title, intent) {
2237
+ this.builder.resetForNewInterview();
2120
2238
  this.builder.startSession(title, intent);
2121
2239
  this.tracker = null;
2122
2240
  this.graph = null;
2241
+ this.resumedFromDisk = false;
2123
2242
  return this.builder.getAIPrompt();
2124
2243
  }
2125
2244
  /**
@@ -2139,8 +2258,32 @@ var SddInterviewDriver = class {
2139
2258
  this.tracker = tracker;
2140
2259
  }
2141
2260
  }
2261
+ this.resumedFromDisk = true;
2142
2262
  return true;
2143
2263
  }
2264
+ /** Drop the on-disk session (if any) and clear in-memory interview state. */
2265
+ async discard() {
2266
+ await this.builder.deleteSession();
2267
+ this.builder.resetForNewInterview();
2268
+ this.tracker = null;
2269
+ this.graph = null;
2270
+ this.resumedFromDisk = false;
2271
+ }
2272
+ setLastAgentText(text) {
2273
+ this.builder.setLastAgentText(text);
2274
+ }
2275
+ getLastAgentText() {
2276
+ return this.builder.getLastAgentText();
2277
+ }
2278
+ setLastRunId(runId) {
2279
+ this.builder.setLastRunId(runId);
2280
+ }
2281
+ getLastRunId() {
2282
+ return this.builder.getLastRunId();
2283
+ }
2284
+ wasResumed() {
2285
+ return this.resumedFromDisk;
2286
+ }
2144
2287
  phase() {
2145
2288
  return this.builder.getPhase();
2146
2289
  }
@@ -2241,6 +2384,9 @@ var SddInterviewDriver = class {
2241
2384
  minQuestions: this.minQuestions,
2242
2385
  maxQuestions: this.maxQuestions,
2243
2386
  answers: s.answers.map((a) => ({ question: a.question, answer: a.answer })),
2387
+ lastAgentText: s.lastAgentText,
2388
+ lastRunId: s.lastRunId,
2389
+ resumed: this.resumedFromDisk || void 0,
2244
2390
  spec: spec ? {
2245
2391
  id: spec.id,
2246
2392
  title: spec.title,
@@ -2308,16 +2454,17 @@ var SddInterviewDriver = class {
2308
2454
  );
2309
2455
  if (valid.length === 0) return void 0;
2310
2456
  const spec = this.builder.getSession().spec;
2311
- if (!spec) return void 0;
2312
- if (!this.tracker || !this.graph) {
2313
- const tracker = new TaskTracker2({ store: new DefaultTaskStore2() });
2314
- this.graph = await tracker.createGraph(spec.id, spec.title);
2315
- this.tracker = tracker;
2457
+ if (!this.tracker) {
2458
+ const tracker2 = new TaskTracker2({ store: new DefaultTaskStore2() });
2459
+ this.graph = await tracker2.createGraph(spec.id, spec.title);
2460
+ this.tracker = tracker2;
2316
2461
  }
2462
+ const tracker = this.tracker;
2463
+ const graph = this.graph;
2317
2464
  const refMap = /* @__PURE__ */ new Map();
2318
2465
  const created = [];
2319
2466
  valid.forEach((task, i) => {
2320
- const node = addTaskToTracker(this.tracker, task);
2467
+ const node = addTaskToTracker(tracker, task);
2321
2468
  created.push({ nodeId: node.id, task });
2322
2469
  if (typeof task.id === "string" && task.id.trim()) {
2323
2470
  refMap.set(task.id.trim().toLowerCase(), node.id);
@@ -2330,13 +2477,13 @@ var SddInterviewDriver = class {
2330
2477
  const deps = Array.isArray(task.dependsOn) ? task.dependsOn : [];
2331
2478
  for (const ref of deps) {
2332
2479
  const depId = refMap.get(normalizeTaskRef(String(ref)));
2333
- if (depId && depId !== nodeId) this.tracker.addDependency(depId, nodeId);
2480
+ if (depId && depId !== nodeId) tracker.addDependency(depId, nodeId);
2334
2481
  }
2335
2482
  }
2336
- await this.persistGraph(this.graph);
2337
- this.builder.setTaskGraphId(this.graph.id);
2483
+ await this.persistGraph(graph);
2484
+ this.builder.setTaskGraphId(graph.id);
2338
2485
  await this.builder.saveSession();
2339
- return this.graph.id;
2486
+ return graph.id;
2340
2487
  }
2341
2488
  };
2342
2489
  var TASK_TYPES = ["feature", "bugfix", "refactor", "docs", "test", "chore"];
@@ -2364,10 +2511,219 @@ function isExplanatoryText(text) {
2364
2511
  import { TOKENS } from "@wrongstack/core/kernel";
2365
2512
 
2366
2513
  // src/sdd-parallel-run.ts
2367
- import { expectDefined as expectDefined2 } from "@wrongstack/core/utils";
2514
+ import { randomUUID as randomUUID3 } from "node:crypto";
2515
+ import {
2516
+ DefaultMultiAgentCoordinator,
2517
+ makeAgentSubagentRunner,
2518
+ withDisabledToolFiltering
2519
+ } from "@wrongstack/core/coordination";
2520
+
2521
+ // src/graph-split.ts
2522
+ function splitGraphNode(tracker, taskId, subtasks, options = {}) {
2523
+ const node = tracker.getNode(taskId);
2524
+ if (!node) return [];
2525
+ if (node.status === "in_progress" || options.isRunning?.(taskId)) return [];
2526
+ if (!subtasks.length) return [];
2527
+ const blockers = tracker.getBlockers(taskId);
2528
+ const dependents = tracker.getDependents(taskId);
2529
+ const leafIds = subtasks.map((s) => {
2530
+ const criterion = s.successCriterion?.trim();
2531
+ const verificationCommand = criterion ? extractVerificationCommand([criterion]) : void 0;
2532
+ const description = criterion && !verificationCommand ? `${s.description}
2533
+
2534
+ **Acceptance Criteria:**
2535
+ - ${criterion}` : s.description;
2536
+ return tracker.addNode({
2537
+ title: s.title,
2538
+ description,
2539
+ type: s.type ?? node.type,
2540
+ priority: s.priority ?? node.priority,
2541
+ status: "pending",
2542
+ parentId: taskId,
2543
+ ...verificationCommand ? { metadata: { verificationCommand } } : {}
2544
+ }).id;
2545
+ });
2546
+ for (const leaf of leafIds) {
2547
+ for (const b of blockers) tracker.addDependency(b, leaf);
2548
+ for (const dep of dependents) tracker.addDependency(leaf, dep);
2549
+ }
2550
+ tracker.updateNodeStatus(taskId, "completed", `split into ${leafIds.length} subtasks`);
2551
+ return leafIds;
2552
+ }
2553
+
2554
+ // src/sdd-task-execution.ts
2368
2555
  import { randomUUID as randomUUID2 } from "node:crypto";
2369
- import { assignNickname, makeAgentSubagentRunner, withDisabledToolFiltering, DefaultMultiAgentCoordinator } from "@wrongstack/core/coordination";
2370
- import { SddError as SddError3, ERROR_CODES as ERROR_CODES3 } from "@wrongstack/core/types";
2556
+ import { expectDefined as expectDefined3 } from "@wrongstack/core/utils";
2557
+ import { assignNickname } from "@wrongstack/core/coordination";
2558
+ import { ERROR_CODES as ERROR_CODES3, SddError as SddError3 } from "@wrongstack/core/types";
2559
+ async function executeSddTask(params) {
2560
+ const { task, opts } = params;
2561
+ const taskId = task.id;
2562
+ let agentName = task.assignee;
2563
+ if (!agentName) {
2564
+ const nick = assignNickname("executor", params.usedNicknames);
2565
+ params.usedNicknames.add(nick.key);
2566
+ agentName = nick.display.replace(/\s*\([^)]*\)\s*$/, "");
2567
+ opts.tracker.updateNode(taskId, { assignee: agentName });
2568
+ }
2569
+ opts.tracker.updateNodeStatus(taskId, "in_progress");
2570
+ await params.allocateWorktrees([task]);
2571
+ if (!params.coordinator)
2572
+ throw new SddError3({
2573
+ message: "SDD parallel runner requires a coordinator",
2574
+ code: ERROR_CODES3.SDD_INVALID_STATE
2575
+ });
2576
+ const coordinator = params.coordinator;
2577
+ const subagentId = params.nextSubagentId();
2578
+ const correlationId = randomUUID2();
2579
+ const meta = task.metadata ?? {};
2580
+ const model = (typeof meta.model === "string" ? meta.model : void 0) ?? opts.defaultModel;
2581
+ const provider = (typeof meta.provider === "string" ? meta.provider : void 0) ?? opts.defaultProvider;
2582
+ const fallbackModels = Array.isArray(meta.fallbackModels) ? meta.fallbackModels : opts.fallbackModels;
2583
+ const spawnResult = await coordinator.spawn({
2584
+ id: subagentId,
2585
+ name: agentName,
2586
+ role: "executor",
2587
+ idleTimeoutMs: params.idleTimeoutMs,
2588
+ ...params.timeoutMs ? { timeoutMs: params.timeoutMs } : {},
2589
+ cwd: params.taskCwds.get(taskId),
2590
+ disabledTools: ["delegate"],
2591
+ ...model ? { model } : {},
2592
+ ...provider ? { provider } : {},
2593
+ ...fallbackModels?.length ? { fallbackModels } : {}
2594
+ });
2595
+ if (!spawnResult.subagentId) {
2596
+ throw new SddError3({
2597
+ message: "One or more subagent spawns failed",
2598
+ code: ERROR_CODES3.SDD_INVALID_STATE
2599
+ });
2600
+ }
2601
+ params.taskSubagents.set(taskId, subagentId);
2602
+ params.emit("sdd.task.started", {
2603
+ runId: params.runId,
2604
+ taskId,
2605
+ subagentId,
2606
+ agentName,
2607
+ worktreeBranch: params.taskBranches.get(taskId)
2608
+ });
2609
+ await coordinator.assign({
2610
+ id: correlationId,
2611
+ description: buildTaskDirective(opts.graph.title, task),
2612
+ subagentId,
2613
+ ...params.timeoutMs ? { timeoutMs: params.timeoutMs } : {},
2614
+ context: {
2615
+ telemetryTaskId: taskId,
2616
+ telemetryRunId: params.runId,
2617
+ telemetryBoardId: opts.graph.id
2618
+ }
2619
+ });
2620
+ let result;
2621
+ try {
2622
+ const got = await coordinator.awaitTasks([correlationId]);
2623
+ result = expectDefined3(got[0]);
2624
+ } catch (err) {
2625
+ result = {
2626
+ subagentId,
2627
+ taskId: correlationId,
2628
+ status: "failed",
2629
+ error: { kind: "unknown", message: String(err), retryable: false },
2630
+ iterations: 0,
2631
+ toolCalls: 0,
2632
+ durationMs: 0
2633
+ };
2634
+ }
2635
+ params.taskSubagents.delete(taskId);
2636
+ if (params.cancelledTasks.has(taskId)) {
2637
+ await params.resolveWorktrees([task]);
2638
+ return { taskId, success: false, result };
2639
+ }
2640
+ const verificationFailReason = await verifyTaskResult(params, result);
2641
+ let success = false;
2642
+ if (result.status === "success" && !verificationFailReason) {
2643
+ const merged = await params.integrateWorktree(task, result);
2644
+ if (merged.ok) {
2645
+ success = true;
2646
+ opts.tracker.updateNodeStatus(taskId, "completed");
2647
+ params.emit("sdd.task.completed", {
2648
+ runId: params.runId,
2649
+ taskId,
2650
+ subagentId,
2651
+ durationMs: result.durationMs
2652
+ });
2653
+ } else if (merged.reason) {
2654
+ params.emit("sdd.task.verification_failed", {
2655
+ runId: params.runId,
2656
+ taskId,
2657
+ reason: merged.reason
2658
+ });
2659
+ await params.applyTaskFailure(taskId, subagentId, merged.reason);
2660
+ } else {
2661
+ const conflictFiles = merged.conflictFiles ?? [];
2662
+ params.emit("sdd.task.conflict", { runId: params.runId, taskId, conflictFiles });
2663
+ const reason = `merge conflict${conflictFiles.length ? `: ${conflictFiles.join(", ")}` : ""}`;
2664
+ await params.applyTaskFailure(taskId, subagentId, reason);
2665
+ }
2666
+ } else {
2667
+ const errMsg = verificationFailReason ?? (result.error?.kind ? `${result.error.kind}: ${result.error.message}` : result.error?.message ?? "unknown error");
2668
+ await params.applyTaskFailure(taskId, subagentId, errMsg);
2669
+ await params.resolveWorktrees([task]);
2670
+ }
2671
+ return { taskId, success, result };
2672
+ }
2673
+ function buildTaskDirective(graphTitle, task) {
2674
+ const directivePreamble = [
2675
+ "\u2550\u2550\u2550 SDD PARALLEL EXECUTION \u2550\u2550\u2550",
2676
+ "",
2677
+ `Graph: ${graphTitle}`,
2678
+ "",
2679
+ "\u2500\u2500 EXECUTION PROTOCOL \u2500\u2500",
2680
+ "\u2022 Execute the assigned SDD task end-to-end using multiple tool calls.",
2681
+ "\u2022 Mark the task [done] in the tracker when complete.",
2682
+ "\u2022 Do not ask before routine in-project tool use; if a permission gate appears, wait for that flow.",
2683
+ "\u2022 Keep output concise \u2014 summarize changes, do not transcribe files."
2684
+ ].join("\n");
2685
+ return [
2686
+ directivePreamble,
2687
+ "",
2688
+ `\u2500\u2500 TASK \u2500\u2500`,
2689
+ `[${task.priority.toUpperCase()}] ${task.title}`,
2690
+ "",
2691
+ task.description
2692
+ ].join("\n");
2693
+ }
2694
+ async function verifyTaskResult(params, result) {
2695
+ const { task, opts, taskCwds } = params;
2696
+ if (result.status !== "success" || !opts.verifyTask) return void 0;
2697
+ const taskId = task.id;
2698
+ const cwd = taskCwds.get(taskId) ?? opts.projectRoot;
2699
+ let verificationFailReason;
2700
+ try {
2701
+ const verdict = await opts.verifyTask({ task, result, cwd });
2702
+ if (!verdict.ok) {
2703
+ verificationFailReason = `verification failed: ${verdict.reason ?? "acceptance criteria not met"}`;
2704
+ }
2705
+ } catch (err) {
2706
+ verificationFailReason = `verification error: ${String(err)}`;
2707
+ }
2708
+ const hadVerifiable = typeof task.metadata?.["verificationCommand"] === "string" || task.description.includes("**Acceptance Criteria:**");
2709
+ if (verificationFailReason) {
2710
+ opts.tracker.patchMetadata(taskId, {
2711
+ verificationState: "failed",
2712
+ verificationDetail: verificationFailReason
2713
+ });
2714
+ params.emit("sdd.task.verification_failed", {
2715
+ runId: params.runId,
2716
+ taskId,
2717
+ reason: verificationFailReason
2718
+ });
2719
+ } else if (hadVerifiable) {
2720
+ opts.tracker.patchMetadata(taskId, {
2721
+ verificationState: "passed",
2722
+ verificationDetail: void 0
2723
+ });
2724
+ }
2725
+ return verificationFailReason;
2726
+ }
2371
2727
 
2372
2728
  // src/sdd-task-decomposer.ts
2373
2729
  var SddTaskDecomposer = class {
@@ -2485,13 +2841,15 @@ var SddParallelRun = class {
2485
2841
  this.maxRetries = Math.max(0, opts.maxRetries ?? 3);
2486
2842
  this.maxSupervisorEscalations = Math.max(0, opts.maxSupervisorEscalations ?? 2);
2487
2843
  this.maxFailedSweeps = Math.max(0, opts.maxFailedRetrySweeps ?? 2);
2488
- this.runId = opts.runId ?? `sdd-${randomUUID2().slice(0, 8)}`;
2844
+ this.runId = opts.runId ?? `sdd-${randomUUID3().slice(0, 8)}`;
2489
2845
  this.events = opts.events;
2490
2846
  this.sessionIdSource = opts.sessionId;
2491
2847
  this.maxTotalWaves = opts.maxTotalWaves ?? opts.graph.nodes.size * (this.maxRetries + 2) + 10;
2492
2848
  this.maxWallClockMs = opts.maxWallClockMs;
2493
2849
  this.maxRecoveryRounds = Math.max(0, opts.maxRecoveryRounds ?? 0);
2494
- this.decomposer = new SddTaskDecomposer(opts.tracker, opts.graph, { parallelSlots: this.slots });
2850
+ this.decomposer = new SddTaskDecomposer(opts.tracker, opts.graph, {
2851
+ parallelSlots: this.slots
2852
+ });
2495
2853
  }
2496
2854
  opts;
2497
2855
  slots;
@@ -2614,7 +2972,8 @@ var SddParallelRun = class {
2614
2972
  * revert outcome; a dirty tree or revert conflict surfaces as `ok:false`.
2615
2973
  */
2616
2974
  async rollback() {
2617
- if (this.isRunning()) return { ok: false, reverted: 0, reason: "run still active \u2014 stop it first" };
2975
+ if (this.isRunning())
2976
+ return { ok: false, reverted: 0, reason: "run still active \u2014 stop it first" };
2618
2977
  const wt = this.opts.worktrees;
2619
2978
  if (!wt || !this.baseBranch) {
2620
2979
  return { ok: false, reverted: 0, reason: "no worktree run to roll back" };
@@ -2647,7 +3006,10 @@ var SddParallelRun = class {
2647
3006
  */
2648
3007
  setTaskModel(taskId, model, provider) {
2649
3008
  if (!this.opts.tracker.getNode(taskId)) return false;
2650
- this.opts.tracker.patchMetadata(taskId, { model, ...provider !== void 0 ? { provider } : {} });
3009
+ this.opts.tracker.patchMetadata(taskId, {
3010
+ model,
3011
+ ...provider !== void 0 ? { provider } : {}
3012
+ });
2651
3013
  return true;
2652
3014
  }
2653
3015
  /** Set/override a task's fallback model chain (applied on its next dispatch). */
@@ -2679,7 +3041,12 @@ var SddParallelRun = class {
2679
3041
  this.cancelledTasks.add(taskId);
2680
3042
  this.opts.tracker.patchMetadata(taskId, { cancelled: true });
2681
3043
  this.opts.tracker.updateNodeStatus(taskId, "failed", "cancelled by user");
2682
- this.emit("sdd.task.failed", { runId: this.runId, taskId, subagentId: "", error: "cancelled by user" });
3044
+ this.emit("sdd.task.failed", {
3045
+ runId: this.runId,
3046
+ taskId,
3047
+ subagentId: "",
3048
+ error: "cancelled by user"
3049
+ });
2683
3050
  const subagentId = this.taskSubagents.get(taskId);
2684
3051
  if (subagentId && this.coordinator) {
2685
3052
  await this.coordinator.stop(subagentId).catch(() => {
@@ -2710,30 +3077,12 @@ var SddParallelRun = class {
2710
3077
  * The scheduler picks the new pending leaves up on its next dispatch pass.
2711
3078
  */
2712
3079
  splitTask(taskId, subtasks) {
2713
- const tracker = this.opts.tracker;
2714
- const node = tracker.getNode(taskId);
2715
- if (!node) return [];
2716
- if (node.status === "in_progress" || this.taskSubagents.has(taskId)) return [];
2717
- if (!subtasks.length) return [];
2718
- const blockers = tracker.getBlockers(taskId);
2719
- const dependents = tracker.getDependents(taskId);
2720
- const leafIds = subtasks.map(
2721
- (s) => tracker.addNode({
2722
- title: s.title,
2723
- description: s.description,
2724
- type: s.type ?? node.type,
2725
- priority: s.priority ?? node.priority,
2726
- status: "pending",
2727
- parentId: taskId
2728
- }).id
2729
- );
2730
- for (const leaf of leafIds) {
2731
- for (const b of blockers) tracker.addDependency(b, leaf);
2732
- for (const dep of dependents) tracker.addDependency(leaf, dep);
2733
- }
3080
+ const leafIds = splitGraphNode(this.opts.tracker, taskId, subtasks, {
3081
+ isRunning: (id) => this.taskSubagents.has(id)
3082
+ });
3083
+ if (!leafIds.length) return [];
2734
3084
  this.retryMap.delete(taskId);
2735
3085
  this.persistRetries(taskId, 0);
2736
- tracker.updateNodeStatus(taskId, "completed", `split into ${leafIds.length} subtasks`);
2737
3086
  this.emit("sdd.task.split", { runId: this.runId, taskId, subtaskIds: leafIds });
2738
3087
  return leafIds;
2739
3088
  }
@@ -2781,7 +3130,12 @@ var SddParallelRun = class {
2781
3130
  return await this.executeOne(task);
2782
3131
  } catch (err) {
2783
3132
  this.opts.tracker.updateNodeStatus(task.id, "failed", `dispatch error: ${String(err)}`);
2784
- this.emit("sdd.task.failed", { runId: this.runId, taskId: task.id, subagentId: "", error: String(err) });
3133
+ this.emit("sdd.task.failed", {
3134
+ runId: this.runId,
3135
+ taskId: task.id,
3136
+ subagentId: "",
3137
+ error: String(err)
3138
+ });
2785
3139
  return { taskId: task.id, success: false };
2786
3140
  } finally {
2787
3141
  running.delete(task.id);
@@ -2795,16 +3149,18 @@ var SddParallelRun = class {
2795
3149
  await this.waitWhilePaused();
2796
3150
  if (this.stopRequested) break;
2797
3151
  let dispatchedThisRound = 0;
2798
- if (running.size < this.slots) {
2799
- const ready = this.decomposer.readyNodes().filter((t) => !running.has(t.id));
2800
- for (const task of ready) {
2801
- if (running.size >= this.slots) break;
2802
- dispatch(task);
2803
- dispatchedThisRound++;
2804
- }
3152
+ const ready = this.decomposer.readyNodes().filter((t) => !running.has(t.id));
3153
+ for (const task of ready) {
3154
+ if (running.size >= this.slots) break;
3155
+ dispatch(task);
3156
+ dispatchedThisRound++;
2805
3157
  }
2806
3158
  if (dispatchedThisRound > 0) {
2807
- this.emit("sdd.wave", { runId: this.runId, wave: this.round, batchSize: dispatchedThisRound });
3159
+ this.emit("sdd.wave", {
3160
+ runId: this.runId,
3161
+ wave: this.round,
3162
+ batchSize: dispatchedThisRound
3163
+ });
2808
3164
  this.round++;
2809
3165
  }
2810
3166
  if (running.size === 0) {
@@ -2835,7 +3191,6 @@ var SddParallelRun = class {
2835
3191
  this.opts.onProgress?.(this.buildProgress());
2836
3192
  }
2837
3193
  }
2838
- if (running.size > 0) await Promise.allSettled(running.values());
2839
3194
  if (this.stopRequested) await this.teardown();
2840
3195
  const finalProgress = this.opts.tracker.getProgress();
2841
3196
  this.emit("sdd.run.finished", {
@@ -2966,7 +3321,7 @@ var SddParallelRun = class {
2966
3321
  // -------------------------------------------------------------------
2967
3322
  buildCoordinator() {
2968
3323
  const config = {
2969
- coordinatorId: `sdd-parallel-${randomUUID2().slice(0, 8)}`,
3324
+ coordinatorId: `sdd-parallel-${randomUUID3().slice(0, 8)}`,
2970
3325
  maxConcurrent: this.slots,
2971
3326
  doneCondition: { type: "all_tasks_done" },
2972
3327
  // Default budget guard for every spawned worker: idle reaper (resets on
@@ -3023,158 +3378,30 @@ var SddParallelRun = class {
3023
3378
  * missing coordinator or failed spawn so callers can enforce all-or-nothing.
3024
3379
  */
3025
3380
  async executeOne(task) {
3026
- const taskId = task.id;
3027
- let agentName = task.assignee;
3028
- if (!agentName) {
3029
- const nick = assignNickname("executor", this.usedNicknames);
3030
- this.usedNicknames.add(nick.key);
3031
- agentName = nick.display.replace(/\s*\([^)]*\)\s*$/, "");
3032
- this.opts.tracker.updateNode(taskId, { assignee: agentName });
3033
- }
3034
- this.opts.tracker.updateNodeStatus(taskId, "in_progress");
3035
- await this.allocateWorktrees([task]);
3036
- if (!this.coordinator) throw new SddError3({
3037
- message: "SDD parallel runner requires a coordinator",
3038
- code: ERROR_CODES3.SDD_INVALID_STATE
3039
- });
3040
- const coordinator = this.coordinator;
3041
- const subagentId = `sdd-d${this.dispatchSeq++}`;
3042
- const correlationId = randomUUID2();
3043
- const meta = task.metadata ?? {};
3044
- const model = (typeof meta.model === "string" ? meta.model : void 0) ?? this.opts.defaultModel;
3045
- const provider = (typeof meta.provider === "string" ? meta.provider : void 0) ?? this.opts.defaultProvider;
3046
- const fallbackModels = Array.isArray(meta.fallbackModels) ? meta.fallbackModels : this.opts.fallbackModels;
3047
- const spawnResult = await coordinator.spawn({
3048
- id: subagentId,
3049
- name: agentName ?? subagentId,
3050
- role: "executor",
3051
- // Idle reaper is always on; the hard wall-clock cap only when opted in.
3381
+ const outcome = await executeSddTask({
3382
+ task,
3383
+ opts: this.opts,
3384
+ coordinator: this.coordinator,
3385
+ usedNicknames: this.usedNicknames,
3052
3386
  idleTimeoutMs: this.idleTimeoutMs,
3053
- ...this.timeoutMs ? { timeoutMs: this.timeoutMs } : {},
3054
- cwd: this.taskCwds.get(taskId),
3055
- disabledTools: ["delegate"],
3056
- ...model ? { model } : {},
3057
- ...provider ? { provider } : {},
3058
- ...fallbackModels?.length ? { fallbackModels } : {}
3059
- });
3060
- if (!spawnResult.subagentId) {
3061
- throw new SddError3({
3062
- message: "One or more subagent spawns failed",
3063
- code: ERROR_CODES3.SDD_INVALID_STATE
3064
- });
3065
- }
3066
- this.taskSubagents.set(taskId, subagentId);
3067
- this.emit("sdd.task.started", {
3387
+ timeoutMs: this.timeoutMs,
3068
3388
  runId: this.runId,
3069
- taskId,
3070
- subagentId,
3071
- agentName: agentName ?? "",
3072
- worktreeBranch: this.taskBranches.get(taskId)
3389
+ nextSubagentId: () => `sdd-d${this.dispatchSeq++}`,
3390
+ emit: (event, payload) => this.emit(event, payload),
3391
+ taskCwds: this.taskCwds,
3392
+ taskBranches: this.taskBranches,
3393
+ taskSubagents: this.taskSubagents,
3394
+ cancelledTasks: this.cancelledTasks,
3395
+ allocateWorktrees: (tasks) => this.allocateWorktrees(tasks),
3396
+ resolveWorktrees: (tasks) => this.resolveWorktrees(tasks),
3397
+ integrateWorktree: (taskNode, result) => this.integrateWorktree(taskNode, result),
3398
+ applyTaskFailure: (taskId, subagentId, errMsg) => this.applyTaskFailure(taskId, subagentId, errMsg)
3073
3399
  });
3074
- const directivePreamble = [
3075
- "\u2550\u2550\u2550 SDD PARALLEL EXECUTION \u2550\u2550\u2550",
3076
- "",
3077
- `Graph: ${this.opts.graph.title}`,
3078
- "",
3079
- "\u2500\u2500 EXECUTION PROTOCOL \u2500\u2500",
3080
- "\u2022 Execute the assigned SDD task end-to-end using multiple tool calls.",
3081
- "\u2022 Mark the task [done] in the tracker when complete.",
3082
- "\u2022 Do not ask before routine in-project tool use; if a permission gate appears, wait for that flow.",
3083
- "\u2022 Keep output concise \u2014 summarize changes, do not transcribe files."
3084
- ].join("\n");
3085
- await coordinator.assign({
3086
- id: correlationId,
3087
- description: [
3088
- directivePreamble,
3089
- "",
3090
- `\u2500\u2500 TASK \u2500\u2500`,
3091
- `[${task.priority.toUpperCase()}] ${task.title}`,
3092
- "",
3093
- task.description
3094
- ].join("\n"),
3095
- subagentId,
3096
- ...this.timeoutMs ? { timeoutMs: this.timeoutMs } : {},
3097
- context: {
3098
- telemetryTaskId: taskId,
3099
- telemetryRunId: this.runId,
3100
- telemetryBoardId: this.opts.graph.id
3101
- }
3102
- });
3103
- let result;
3104
- try {
3105
- const got = await coordinator.awaitTasks([correlationId]);
3106
- result = expectDefined2(got[0]);
3107
- } catch (err) {
3108
- result = {
3109
- subagentId,
3110
- taskId: correlationId,
3111
- status: "failed",
3112
- error: { kind: "unknown", message: String(err), retryable: false },
3113
- iterations: 0,
3114
- toolCalls: 0,
3115
- durationMs: 0
3116
- };
3400
+ if (outcome.success) {
3401
+ this.retryMap.delete(task.id);
3402
+ this.persistRetries(task.id, 0);
3117
3403
  }
3118
- this.taskSubagents.delete(taskId);
3119
- if (this.cancelledTasks.has(taskId)) {
3120
- await this.resolveWorktrees([task]);
3121
- return { taskId, success: false, result };
3122
- }
3123
- let verificationFailReason;
3124
- if (result.status === "success" && this.opts.verifyTask) {
3125
- const cwd = this.taskCwds.get(taskId) ?? this.opts.projectRoot;
3126
- try {
3127
- const verdict = await this.opts.verifyTask({ task, result, cwd });
3128
- if (!verdict.ok) {
3129
- verificationFailReason = `verification failed: ${verdict.reason ?? "acceptance criteria not met"}`;
3130
- }
3131
- } catch (err) {
3132
- verificationFailReason = `verification error: ${String(err)}`;
3133
- }
3134
- if (verificationFailReason) {
3135
- this.emit("sdd.task.verification_failed", {
3136
- runId: this.runId,
3137
- taskId,
3138
- reason: verificationFailReason
3139
- });
3140
- }
3141
- }
3142
- let success = false;
3143
- if (result.status === "success" && !verificationFailReason) {
3144
- const merged = await this.integrateWorktree(task, result);
3145
- if (merged.ok) {
3146
- success = true;
3147
- this.opts.tracker.updateNodeStatus(taskId, "completed");
3148
- this.retryMap.delete(taskId);
3149
- this.persistRetries(taskId, 0);
3150
- this.emit("sdd.task.completed", {
3151
- runId: this.runId,
3152
- taskId,
3153
- subagentId,
3154
- durationMs: result.durationMs
3155
- });
3156
- } else if (merged.reason) {
3157
- this.emit("sdd.task.verification_failed", {
3158
- runId: this.runId,
3159
- taskId,
3160
- reason: merged.reason
3161
- });
3162
- await this.applyTaskFailure(taskId, subagentId, merged.reason);
3163
- } else {
3164
- this.emit("sdd.task.conflict", {
3165
- runId: this.runId,
3166
- taskId,
3167
- conflictFiles: merged.conflictFiles ?? []
3168
- });
3169
- const reason = `merge conflict${merged.conflictFiles?.length ? `: ${merged.conflictFiles.join(", ")}` : ""}`;
3170
- await this.applyTaskFailure(taskId, subagentId, reason);
3171
- }
3172
- } else {
3173
- const errMsg = verificationFailReason ?? (result.error?.kind ? `${result.error.kind}: ${result.error.message}` : result.error?.message ?? "unknown error");
3174
- await this.applyTaskFailure(taskId, subagentId, errMsg);
3175
- await this.resolveWorktrees([task]);
3176
- }
3177
- return { taskId, success, result };
3404
+ return outcome;
3178
3405
  }
3179
3406
  /**
3180
3407
  * Apply a task failure: retry (→ pending, bump retry count) while attempts
@@ -3269,7 +3496,11 @@ var SddParallelRun = class {
3269
3496
  const res = await wt.merge(handle, {
3270
3497
  squash: true,
3271
3498
  ...this.opts.conflictResolver ? {
3272
- resolve: (info) => this.opts.conflictResolver({ task, conflictFiles: info.conflictFiles, cwd: info.cwd })
3499
+ resolve: (info) => this.opts.conflictResolver({
3500
+ task,
3501
+ conflictFiles: info.conflictFiles,
3502
+ cwd: info.cwd
3503
+ })
3273
3504
  } : {}
3274
3505
  });
3275
3506
  if (res.ok) {
@@ -3281,7 +3512,8 @@ var SddParallelRun = class {
3281
3512
  result: result ?? {},
3282
3513
  cwd: this.opts.projectRoot
3283
3514
  });
3284
- if (!verdict.ok) regressed = verdict.reason ?? "verification failed after conflict resolution";
3515
+ if (!verdict.ok)
3516
+ regressed = verdict.reason ?? "verification failed after conflict resolution";
3285
3517
  } catch (err) {
3286
3518
  regressed = `verification error after conflict resolution: ${String(err)}`;
3287
3519
  }
@@ -3397,6 +3629,32 @@ var SddParallelRun = class {
3397
3629
  };
3398
3630
 
3399
3631
  // src/start-sdd-run.ts
3632
+ function applySddControlCommand(run, command) {
3633
+ const payload = command.payload ?? {};
3634
+ if (command.type === "pause") run.pause();
3635
+ else if (command.type === "resume") run.resume();
3636
+ else if (command.type === "stop") run.stop();
3637
+ else if (command.type === "retry" && payload.taskId) run.retryTask(payload.taskId);
3638
+ else if (command.type === "retry_all_failed") run.retryAllFailed();
3639
+ else if (command.type === "reassign" && payload.taskId)
3640
+ run.reassignTask(payload.taskId, payload.agentName ?? "");
3641
+ else if (command.type === "set_task_model" && payload.taskId)
3642
+ run.setTaskModel(payload.taskId, payload.model, payload.provider);
3643
+ else if (command.type === "set_task_fallbacks" && payload.taskId)
3644
+ run.setTaskFallbacks(payload.taskId, payload.fallbackModels);
3645
+ else if (command.type === "set_task_verification" && payload.taskId)
3646
+ run.setTaskVerification(payload.taskId, payload.verificationCommand);
3647
+ else if (command.type === "cancel_task" && payload.taskId)
3648
+ void run.cancelTask(payload.taskId).catch(() => {
3649
+ });
3650
+ else if (command.type === "delete_task" && payload.taskId) run.deleteTask(payload.taskId);
3651
+ else if (command.type === "split_task" && payload.taskId && payload.subtasks?.length)
3652
+ run.splitTask(payload.taskId, payload.subtasks);
3653
+ else if (command.type === "cleanup_worktrees") void run.cleanupWorktrees().catch(() => {
3654
+ });
3655
+ else if (command.type === "rollback") void run.rollback().catch(() => {
3656
+ });
3657
+ }
3400
3658
  function startSddRun(opts) {
3401
3659
  SddParallelRun.resetOrphans(opts.tracker);
3402
3660
  const run = new SddParallelRun({
@@ -3460,25 +3718,7 @@ function startSddRun(opts) {
3460
3718
  const controlTimer = setInterval(() => {
3461
3719
  void opts.boardStore.drainControl(run.runId).then((cmds) => {
3462
3720
  for (const c of cmds) {
3463
- const p = c.payload ?? {};
3464
- if (c.type === "pause") run.pause();
3465
- else if (c.type === "resume") run.resume();
3466
- else if (c.type === "stop") run.stop();
3467
- else if (c.type === "retry" && p.taskId) run.retryTask(p.taskId);
3468
- else if (c.type === "retry_all_failed") run.retryAllFailed();
3469
- else if (c.type === "reassign" && p.taskId) run.reassignTask(p.taskId, p.agentName ?? "");
3470
- else if (c.type === "set_task_model" && p.taskId) run.setTaskModel(p.taskId, p.model, p.provider);
3471
- else if (c.type === "set_task_fallbacks" && p.taskId) run.setTaskFallbacks(p.taskId, p.fallbackModels);
3472
- else if (c.type === "set_task_verification" && p.taskId)
3473
- run.setTaskVerification(p.taskId, p.verificationCommand);
3474
- else if (c.type === "cancel_task" && p.taskId) void run.cancelTask(p.taskId).catch(() => {
3475
- });
3476
- else if (c.type === "delete_task" && p.taskId) run.deleteTask(p.taskId);
3477
- else if (c.type === "split_task" && p.taskId && p.subtasks?.length) run.splitTask(p.taskId, p.subtasks);
3478
- else if (c.type === "cleanup_worktrees") void run.cleanupWorktrees().catch(() => {
3479
- });
3480
- else if (c.type === "rollback") void run.rollback().catch(() => {
3481
- });
3721
+ applySddControlCommand(run, c);
3482
3722
  }
3483
3723
  }).catch(() => {
3484
3724
  });
@@ -3506,7 +3746,10 @@ function startSddRun(opts) {
3506
3746
 
3507
3747
  // src/sdd-lifecycle.ts
3508
3748
  import * as fsp4 from "node:fs/promises";
3749
+ import * as path4 from "node:path";
3750
+ import { toErrorMessage as toErrorMessage2 } from "@wrongstack/core/utils";
3509
3751
  import { WorktreeManager } from "@wrongstack/core/worktree";
3752
+ import { listBoards, removeBoard } from "@wrongstack/kanban";
3510
3753
  async function cleanupSddWorktrees(projectRoot) {
3511
3754
  const wt = new WorktreeManager({ projectRoot });
3512
3755
  return wt.cleanupAllManaged();
@@ -3517,19 +3760,21 @@ async function cleanupStaleWorktrees(projectRoot) {
3517
3760
  }
3518
3761
  async function cleanupStaleSddWorktrees(opts) {
3519
3762
  const now = opts.now?.() ?? Date.now();
3520
- try {
3521
- const store = new SddBoardStore({ baseDir: opts.boardsDir });
3522
- const latest = (await store.list())[0];
3523
- if (latest) {
3524
- const age = now - latest.updatedAt;
3525
- if (latest.status === "running" && age < (opts.runningLiveMs ?? 12e4)) {
3526
- return { swept: false, removed: 0, detected: 0, skippedReason: "a run appears live (running)" };
3527
- }
3528
- if (latest.status === "paused" && age < (opts.pausedLiveMs ?? 18e5)) {
3529
- return { swept: false, removed: 0, detected: 0, skippedReason: "a run is paused" };
3530
- }
3763
+ const store = new SddBoardStore({ baseDir: opts.boardsDir });
3764
+ const latest = (await store.list())[0];
3765
+ if (latest) {
3766
+ const age = now - latest.updatedAt;
3767
+ if (latest.status === "running" && age < (opts.runningLiveMs ?? 12e4)) {
3768
+ return {
3769
+ swept: false,
3770
+ removed: 0,
3771
+ detected: 0,
3772
+ skippedReason: "a run appears live (running)"
3773
+ };
3774
+ }
3775
+ if (latest.status === "paused" && age < (opts.pausedLiveMs ?? 18e5)) {
3776
+ return { swept: false, removed: 0, detected: 0, skippedReason: "a run is paused" };
3531
3777
  }
3532
- } catch {
3533
3778
  }
3534
3779
  try {
3535
3780
  const wt = new WorktreeManager({ projectRoot: opts.projectRoot });
@@ -3546,7 +3791,11 @@ async function rollbackSddRunFromDisk(opts) {
3546
3791
  const snap = await store.load(runId);
3547
3792
  if (!snap) return { ok: false, reverted: 0, reason: `board "${runId}" not found` };
3548
3793
  if (!snap.baseBranch) {
3549
- return { ok: false, reverted: 0, reason: "this run did not record a base branch (no worktree run)" };
3794
+ return {
3795
+ ok: false,
3796
+ reverted: 0,
3797
+ reason: "this run did not record a base branch (no worktree run)"
3798
+ };
3550
3799
  }
3551
3800
  const shas = (snap.mergedCommits ?? []).map((c) => c.sha);
3552
3801
  if (shas.length === 0) {
@@ -3564,7 +3813,7 @@ async function destroySddProject(opts) {
3564
3813
  projectRoot: opts.projectRoot,
3565
3814
  boardsDir: opts.paths.projectSddBoards,
3566
3815
  runId: opts.runId
3567
- }).catch((err) => ({ ok: false, reverted: 0, reason: toReason(err) }));
3816
+ }).catch((err) => ({ ok: false, reverted: 0, reason: toErrorMessage2(err) }));
3568
3817
  reverted = r.reverted;
3569
3818
  revertOk = r.ok;
3570
3819
  revertReason = r.reason;
@@ -3586,14 +3835,24 @@ async function destroySddProject(opts) {
3586
3835
  }
3587
3836
  };
3588
3837
  await rmFile(opts.paths.projectSddSession, "session");
3838
+ await rmFile(
3839
+ path4.join(path4.dirname(opts.paths.projectSddSession), "sdd-wizard-session.json"),
3840
+ "wizard-session"
3841
+ );
3589
3842
  await rmDir(opts.paths.projectSpecs, "specs");
3590
3843
  await rmDir(opts.paths.projectTaskGraphs, "task-graphs");
3591
3844
  await rmDir(opts.paths.projectSddBoards, "boards");
3845
+ try {
3846
+ const mirrors = (await listBoards(opts.projectRoot)).filter((b) => b.tags?.includes("sdd"));
3847
+ let mirrorsRemoved = 0;
3848
+ for (const b of mirrors) {
3849
+ if (await removeBoard(opts.projectRoot, b.id)) mirrorsRemoved++;
3850
+ }
3851
+ if (mirrorsRemoved > 0) deleted.push(`kanban-mirrors(${mirrorsRemoved})`);
3852
+ } catch {
3853
+ }
3592
3854
  return { worktreesRemoved: removed, deleted, reverted, revertOk, revertReason };
3593
3855
  }
3594
- function toReason(err) {
3595
- return err instanceof Error ? err.message : String(err);
3596
- }
3597
3856
  async function applySddLifecycle(op, opts) {
3598
3857
  try {
3599
3858
  if (op === "cleanup_worktrees") {
@@ -3625,8 +3884,58 @@ async function applySddLifecycle(op, opts) {
3625
3884
  reason: r.revertOk === false ? r.revertReason : void 0
3626
3885
  };
3627
3886
  } catch (err) {
3628
- return { op, ok: false, reason: toReason(err) };
3887
+ return { op, ok: false, reason: toErrorMessage2(err) };
3888
+ }
3889
+ }
3890
+
3891
+ // src/project-context.ts
3892
+ import * as fsp5 from "node:fs/promises";
3893
+ import * as path5 from "node:path";
3894
+ async function gatherProjectContext(projectRoot) {
3895
+ const parts = [];
3896
+ const root = projectRoot.trim() || process.cwd();
3897
+ try {
3898
+ const pkgPath = path5.join(root, "package.json");
3899
+ const pkgRaw = await fsp5.readFile(pkgPath, "utf8");
3900
+ const pkg = JSON.parse(pkgRaw);
3901
+ parts.push(`Project: ${String(pkg.name ?? "unknown")}`);
3902
+ parts.push(`Description: ${String(pkg.description ?? "none")}`);
3903
+ if (pkg.dependencies && typeof pkg.dependencies === "object") {
3904
+ const deps = Object.keys(pkg.dependencies);
3905
+ parts.push(`Dependencies: ${deps.slice(0, 20).join(", ")}${deps.length > 20 ? "..." : ""}`);
3906
+ }
3907
+ if (pkg.devDependencies && typeof pkg.devDependencies === "object") {
3908
+ const devDeps = Object.keys(pkg.devDependencies);
3909
+ parts.push(
3910
+ `Dev Dependencies: ${devDeps.slice(0, 15).join(", ")}${devDeps.length > 15 ? "..." : ""}`
3911
+ );
3912
+ }
3913
+ } catch {
3914
+ }
3915
+ try {
3916
+ await fsp5.access(path5.join(root, "tsconfig.json"));
3917
+ parts.push("Language: TypeScript");
3918
+ } catch {
3919
+ }
3920
+ try {
3921
+ const srcDir = path5.join(root, "src");
3922
+ const entries = await fsp5.readdir(srcDir, { withFileTypes: true });
3923
+ const dirs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
3924
+ if (dirs.length > 0) parts.push(`Source structure: src/${dirs.join(", src/")}`);
3925
+ } catch {
3629
3926
  }
3927
+ try {
3928
+ const packagesDir = path5.join(root, "packages");
3929
+ const entries = await fsp5.readdir(packagesDir, { withFileTypes: true });
3930
+ const pkgs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
3931
+ if (pkgs.length > 0) {
3932
+ parts.push(
3933
+ `Packages: ${pkgs.slice(0, 25).join(", ")}${pkgs.length > 25 ? "..." : ""}`
3934
+ );
3935
+ }
3936
+ } catch {
3937
+ }
3938
+ return parts.join("\n");
3630
3939
  }
3631
3940
 
3632
3941
  // src/spec-templates.ts
@@ -3747,7 +4056,7 @@ function templateToMarkdown(template, title) {
3747
4056
 
3748
4057
  // src/task-visualizer.ts
3749
4058
  import { computeTaskProgress as computeTaskProgress2 } from "@wrongstack/core/tasking";
3750
- import { truncate as truncate2 } from "@wrongstack/core/utils";
4059
+ import { expectDefined as expectDefined4, truncate } from "@wrongstack/core/utils";
3751
4060
  var STATUS_ICON = {
3752
4061
  pending: "\u25CB",
3753
4062
  in_progress: "\u25D0",
@@ -3774,7 +4083,9 @@ function renderTaskGraph(graph, opts) {
3774
4083
  const lines = [];
3775
4084
  const compact = opts?.compact ?? false;
3776
4085
  lines.push(`\u256D\u2500 Task Graph: ${graph.title} \u2500\u256E`);
3777
- lines.push(`\u2502 Spec: ${graph.specId.slice(0, 8)}... \u2502 Nodes: ${graph.nodes.size} \u2502 Edges: ${graph.edges.length} \u2502`);
4086
+ lines.push(
4087
+ `\u2502 Spec: ${graph.specId.slice(0, 8)}... \u2502 Nodes: ${graph.nodes.size} \u2502 Edges: ${graph.edges.length} \u2502`
4088
+ );
3778
4089
  lines.push("\u2570" + "\u2500".repeat(Math.max(50, graph.title.length + 30)) + "\u256F");
3779
4090
  lines.push("");
3780
4091
  const progress = computeTaskProgress2(graph);
@@ -3809,19 +4120,18 @@ function renderTaskGraph(graph, opts) {
3809
4120
  function renderNode(graph, nodeId, lines, rendered, childrenMap, compact, prefix) {
3810
4121
  if (rendered.has(nodeId)) return;
3811
4122
  rendered.add(nodeId);
3812
- const node = graph.nodes.get(nodeId);
3813
- if (!node) return;
4123
+ const node = expectDefined4(graph.nodes.get(nodeId));
3814
4124
  const icon = STATUS_ICON[node.status];
3815
4125
  const prioIcon = PRIORITY_ICON[node.priority];
3816
4126
  const typeIcon = TYPE_ICON[node.type];
3817
- const title = compact ? truncate2(node.title, 40) : node.title;
4127
+ const title = compact ? truncate(node.title, 40) : node.title;
3818
4128
  const blockedBy = childrenMap.get(nodeId) ?? [];
3819
4129
  const depsStr = blockedBy.length > 0 ? ` \u2190 [${blockedBy.map((d) => graph.nodes.get(d)?.title?.slice(0, 12) ?? "?").join(", ")}]` : "";
3820
4130
  lines.push(`${prefix}${icon} ${typeIcon} ${prioIcon} ${title}${depsStr}`);
3821
4131
  if (!compact && node.description) {
3822
4132
  const descLines = node.description.split("\n").slice(0, 3);
3823
4133
  for (const dl of descLines) {
3824
- lines.push(`${prefix} \u2514 ${truncate2(dl, 60)}`);
4134
+ lines.push(`${prefix} \u2514 ${truncate(dl, 60)}`);
3825
4135
  }
3826
4136
  }
3827
4137
  const dependents = graph.edges.filter((e) => e.type === "depends_on" && e.to === nodeId).map((e) => e.from).filter((id) => graph.nodes.has(id));
@@ -3851,7 +4161,7 @@ function renderTaskList(graph) {
3851
4161
  completed: []
3852
4162
  };
3853
4163
  for (const node of nodes) {
3854
- groups[node.status]?.push(node);
4164
+ groups[node.status].push(node);
3855
4165
  }
3856
4166
  for (const [status, group] of Object.entries(groups)) {
3857
4167
  if (group.length === 0) continue;
@@ -3899,8 +4209,8 @@ function renderSpecAnalysis(spec, analysis) {
3899
4209
  }
3900
4210
 
3901
4211
  // src/critical-path.ts
3902
- import { expectDefined as expectDefined3 } from "@wrongstack/core/utils";
3903
4212
  import { topologicalSort } from "@wrongstack/core/tasking";
4213
+ import { expectDefined as expectDefined5 } from "@wrongstack/core/utils";
3904
4214
  function analyzeCriticalPath(graph) {
3905
4215
  const nodes = Array.from(graph.nodes.values());
3906
4216
  const topoOrder = topologicalSort(graph);
@@ -3954,8 +4264,7 @@ function analyzeCriticalPath(graph) {
3954
4264
  bottlenecks.sort((a, b) => b.severity - a.severity);
3955
4265
  const criticalPath = computeCriticalPath(graph, topoOrder, blockedByMap);
3956
4266
  const totalHours = criticalPath.reduce((sum, id) => {
3957
- const n = graph.nodes.get(id);
3958
- return sum + (n?.estimateHours ?? 0);
4267
+ return sum + (graph.nodes.get(id).estimateHours ?? 0);
3959
4268
  }, 0);
3960
4269
  const parallelGroups = computeParallelGroups(graph, blockedByMap);
3961
4270
  const executionOrder = topoOrder.filter((id) => {
@@ -3976,7 +4285,7 @@ function getTransitiveBlocked(_graph, taskId, blocksMap) {
3976
4285
  const visited = /* @__PURE__ */ new Set();
3977
4286
  const queue = [taskId];
3978
4287
  while (queue.length > 0) {
3979
- const current = expectDefined3(queue.shift());
4288
+ const current = expectDefined5(queue.shift());
3980
4289
  const blocked = blocksMap.get(current);
3981
4290
  if (!blocked) continue;
3982
4291
  for (const id of blocked) {
@@ -4011,7 +4320,7 @@ function computeCriticalPath(graph, _topoOrder, blockedByMap) {
4011
4320
  const blocked = blocksMap.get(id);
4012
4321
  if (!blocked) continue;
4013
4322
  for (const blockedId of blocked) {
4014
- const candidateDist = (dist.get(id) ?? 0) + (graph.nodes.get(blockedId)?.estimateHours ?? 1);
4323
+ const candidateDist = dist.get(id) + (graph.nodes.get(blockedId)?.estimateHours ?? 1);
4015
4324
  if (candidateDist > (dist.get(blockedId) ?? 0)) {
4016
4325
  dist.set(blockedId, candidateDist);
4017
4326
  prev.set(blockedId, id);
@@ -4022,23 +4331,23 @@ function computeCriticalPath(graph, _topoOrder, blockedByMap) {
4022
4331
  if (!changed) break;
4023
4332
  }
4024
4333
  let maxDist = 0;
4025
- let maxId = expectDefined3(allIds[0]);
4334
+ let maxId = expectDefined5(allIds[0]);
4026
4335
  for (const id of allIds) {
4027
- const d = dist.get(id) ?? 0;
4336
+ const d = dist.get(id);
4028
4337
  if (d > maxDist) {
4029
4338
  maxDist = d;
4030
4339
  maxId = id;
4031
4340
  }
4032
4341
  }
4033
- const path4 = [];
4342
+ const path6 = [];
4034
4343
  let current = maxId;
4035
4344
  const visited = /* @__PURE__ */ new Set();
4036
4345
  while (current && !visited.has(current)) {
4037
4346
  visited.add(current);
4038
- path4.unshift(current);
4347
+ path6.unshift(current);
4039
4348
  current = prev.get(current) ?? null;
4040
4349
  }
4041
- return path4;
4350
+ return path6;
4042
4351
  }
4043
4352
  function computeParallelGroups(graph, blockedByMap) {
4044
4353
  const groups = [];
@@ -4059,8 +4368,7 @@ function computeParallelGroups(graph, blockedByMap) {
4059
4368
  }
4060
4369
  }
4061
4370
  if (group.length === 0) {
4062
- const first = Array.from(remaining)[0];
4063
- if (first) group.push(first);
4371
+ group.push(expectDefined5(Array.from(remaining)[0]));
4064
4372
  }
4065
4373
  for (const id of group) {
4066
4374
  assigned.add(id);
@@ -4274,7 +4582,6 @@ var AutoExecutor = class {
4274
4582
  for (let i = 0; i < results.length; i++) {
4275
4583
  const result = results[i];
4276
4584
  const task = batch[i];
4277
- if (!result || !task) continue;
4278
4585
  if (result.status === "fulfilled") {
4279
4586
  const { result: execResult, retries } = result.value;
4280
4587
  if (execResult.success) {
@@ -4324,14 +4631,14 @@ var AutoExecutor = class {
4324
4631
  }
4325
4632
  }
4326
4633
  const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
4327
- ready.sort((a, b) => (priorityOrder[a.priority] ?? 4) - (priorityOrder[b.priority] ?? 4));
4634
+ ready.sort((a, b) => priorityOrder[a.priority] - priorityOrder[b.priority]);
4328
4635
  return ready;
4329
4636
  }
4330
4637
  /** Execute a single task with retry logic. */
4331
4638
  async executeTaskWithRetry(task, graph, spec) {
4332
4639
  const maxRetries = this.opts.maxRetries ?? 2;
4333
4640
  let retryCount = this.retryMap.get(task.id) ?? 0;
4334
- while (retryCount <= maxRetries) {
4641
+ while (true) {
4335
4642
  this.opts.tracker.updateNodeStatus(task.id, "in_progress");
4336
4643
  this.opts.onTaskStart?.(task);
4337
4644
  const dependencies = this.getTaskDependencies(task.id, graph);
@@ -4375,7 +4682,6 @@ var AutoExecutor = class {
4375
4682
  };
4376
4683
  }
4377
4684
  }
4378
- return { result: { success: false, error: "Max retries exceeded" }, retries: retryCount };
4379
4685
  }
4380
4686
  /** Get tasks that this task depends on. */
4381
4687
  getTaskDependencies(taskId, graph) {
@@ -4462,6 +4768,54 @@ Supervisor rescues already used: ${attempts}`,
4462
4768
 
4463
4769
  // src/verify-task.ts
4464
4770
  import { spawn } from "node:child_process";
4771
+ function verificationShell(platform) {
4772
+ return platform === "win32" ? ["cmd", "/d", "/c"] : ["sh", "-c"];
4773
+ }
4774
+ function makeCompositeVerifier(parts) {
4775
+ return async function verifyTask(info) {
4776
+ for (const part of parts) {
4777
+ const outcome = await part(info);
4778
+ if (!outcome.ok) return outcome;
4779
+ }
4780
+ return { ok: true };
4781
+ };
4782
+ }
4783
+ function makeAcceptanceCriteriaVerifier(options) {
4784
+ const maxResultChars = options.maxResultChars ?? 4e3;
4785
+ return async function verifyTask(info) {
4786
+ const description = info.task.description ?? "";
4787
+ const marker = description.indexOf("**Acceptance Criteria:**");
4788
+ if (marker === -1) return { ok: true };
4789
+ const criteria = description.slice(marker);
4790
+ const resultText = typeof info.result.result === "string" ? info.result.result.slice(0, maxResultChars) : JSON.stringify(info.result.result ?? "").slice(0, maxResultChars);
4791
+ let text;
4792
+ try {
4793
+ text = await options.run(
4794
+ [
4795
+ "You are a strict acceptance reviewer for one completed engineering task.",
4796
+ `Task: ${info.task.title}`,
4797
+ "",
4798
+ criteria,
4799
+ "",
4800
+ "Worker's reported result:",
4801
+ resultText || "(no result text)",
4802
+ "",
4803
+ "Does the reported result plausibly satisfy EVERY acceptance criterion?",
4804
+ 'Answer with exactly one line: "VERDICT: PASS" or "VERDICT: FAIL \u2014 <short reason>".'
4805
+ ].join("\n")
4806
+ );
4807
+ } catch {
4808
+ return { ok: true };
4809
+ }
4810
+ const match = text.match(/VERDICT:\s*(PASS|FAIL)(?:\s*[—-]\s*(.*))?/i);
4811
+ if (!match) return { ok: true };
4812
+ if (match[1].toUpperCase() === "PASS") return { ok: true };
4813
+ return {
4814
+ ok: false,
4815
+ reason: `acceptance criteria not met: ${match[2]?.trim() || "judge rejected the result"}`
4816
+ };
4817
+ };
4818
+ }
4465
4819
  function makeCommandVerifier(options = {}) {
4466
4820
  const metadataKey = options.metadataKey ?? "verificationCommand";
4467
4821
  const timeoutMs = options.timeoutMs ?? 18e4;
@@ -4469,8 +4823,7 @@ function makeCommandVerifier(options = {}) {
4469
4823
  const cmd = info.task.metadata?.[metadataKey];
4470
4824
  if (typeof cmd !== "string" || !cmd.trim()) return { ok: true };
4471
4825
  return await new Promise((resolve) => {
4472
- const isWindows = process.platform === "win32";
4473
- const [shell, ...shellArgs] = isWindows ? ["cmd", "/d", "/c"] : ["sh", "-c"];
4826
+ const [shell, ...shellArgs] = verificationShell(process.platform);
4474
4827
  const child = spawn(shell, [...shellArgs, cmd], {
4475
4828
  cwd: info.cwd,
4476
4829
  shell: false,
@@ -4492,7 +4845,6 @@ function makeCommandVerifier(options = {}) {
4492
4845
  });
4493
4846
  child.on("error", (err) => {
4494
4847
  clearTimeout(timer);
4495
- if (timedOut) return;
4496
4848
  resolve({ ok: false, reason: `verification spawn error: ${String(err)}` });
4497
4849
  });
4498
4850
  });
@@ -4500,10 +4852,7 @@ function makeCommandVerifier(options = {}) {
4500
4852
  }
4501
4853
 
4502
4854
  // src/decompose-task.ts
4503
- import {
4504
- readBundledInstructionText,
4505
- renderInstructionTemplate
4506
- } from "@wrongstack/core/utils";
4855
+ import { readBundledInstructionText, renderInstructionTemplate } from "@wrongstack/core/utils";
4507
4856
  var TASK_TYPES2 = /* @__PURE__ */ new Set(["feature", "bugfix", "refactor", "docs", "test", "chore"]);
4508
4857
  var PRIORITIES = /* @__PURE__ */ new Set(["critical", "high", "medium", "low"]);
4509
4858
  function extractJsonArray(text) {
@@ -4527,48 +4876,144 @@ function buildPrompt(task, error, min, max) {
4527
4876
  error: error || "(none recorded)"
4528
4877
  });
4529
4878
  }
4530
- function makeLlmSubtaskGenerator(opts) {
4879
+ function parseSubtaskSpecs(text, min, max, options = {}) {
4880
+ const json = extractJsonArray(text ?? "");
4881
+ if (!json) return [];
4882
+ let raw;
4883
+ try {
4884
+ raw = JSON.parse(json);
4885
+ } catch {
4886
+ return [];
4887
+ }
4888
+ const items = raw;
4889
+ const specs = [];
4890
+ for (const item of items) {
4891
+ if (!item || typeof item !== "object") continue;
4892
+ const r = item;
4893
+ const title = typeof r["title"] === "string" ? r["title"].trim() : "";
4894
+ const description = typeof r["description"] === "string" ? r["description"].trim() : "";
4895
+ if (!title || !description) continue;
4896
+ const type = TASK_TYPES2.has(r["type"]) ? r["type"] : void 0;
4897
+ const priority = PRIORITIES.has(r["priority"]) ? r["priority"] : void 0;
4898
+ const successCriterion = options.acceptSuccessCriterion && typeof r["successCriterion"] === "string" ? r["successCriterion"].trim() || void 0 : void 0;
4899
+ specs.push({ title, description, type, priority, successCriterion });
4900
+ if (specs.length >= max) break;
4901
+ }
4902
+ return specs.length >= min ? specs : [];
4903
+ }
4904
+ function makePlanningDecomposer(opts) {
4531
4905
  const min = Math.max(2, opts.minSubtasks ?? 2);
4532
- const max = Math.max(min, opts.maxSubtasks ?? 4);
4533
- return async function generateSubtasks(info) {
4906
+ const max = Math.max(min, opts.maxSubtasks ?? 5);
4907
+ return async function decompose(info) {
4534
4908
  let text;
4535
4909
  try {
4536
- text = await opts.run(buildPrompt(info.task, info.error, min, max));
4910
+ text = await opts.run(
4911
+ renderInstructionTemplate(readBundledInstructionText("sdd/decompose-task-planning.md"), {
4912
+ minSubtasks: String(min),
4913
+ maxSubtasks: String(max),
4914
+ title: info.title,
4915
+ description: info.description,
4916
+ reasons: info.reasons.length ? info.reasons.map((r) => `- ${r}`).join("\n") : "- (unspecified)"
4917
+ })
4918
+ );
4537
4919
  } catch {
4538
4920
  return [];
4539
4921
  }
4540
- const json = extractJsonArray(text ?? "");
4541
- if (!json) return [];
4542
- let raw;
4922
+ return parseSubtaskSpecs(text, min, max, { acceptSuccessCriterion: true });
4923
+ };
4924
+ }
4925
+ function makeLlmSubtaskGenerator(opts) {
4926
+ const min = Math.max(2, opts.minSubtasks ?? 2);
4927
+ const max = Math.max(min, opts.maxSubtasks ?? 4);
4928
+ return async function generateSubtasks(info) {
4929
+ let text;
4543
4930
  try {
4544
- raw = JSON.parse(json);
4931
+ text = await opts.run(buildPrompt(info.task, info.error, min, max));
4545
4932
  } catch {
4546
4933
  return [];
4547
4934
  }
4548
- if (!Array.isArray(raw)) return [];
4549
- const specs = [];
4550
- for (const item of raw) {
4551
- if (!item || typeof item !== "object") continue;
4552
- const r = item;
4553
- const title = typeof r["title"] === "string" ? r["title"].trim() : "";
4554
- const description = typeof r["description"] === "string" ? r["description"].trim() : "";
4555
- if (!title || !description) continue;
4556
- const type = TASK_TYPES2.has(r["type"]) ? r["type"] : void 0;
4557
- const priority = PRIORITIES.has(r["priority"]) ? r["priority"] : void 0;
4558
- specs.push({ title, description, type, priority });
4559
- if (specs.length >= max) break;
4560
- }
4561
- return specs.length >= min ? specs : [];
4935
+ return parseSubtaskSpecs(text, min, max);
4562
4936
  };
4563
4937
  }
4564
4938
 
4565
- // src/conflict-resolver.ts
4566
- import { readFile as readFile4, writeFile } from "node:fs/promises";
4567
- import { join as join4, isAbsolute } from "node:path";
4939
+ // src/plan-decompose.ts
4568
4940
  import {
4569
- readBundledInstructionText as readBundledInstructionText2,
4570
- renderInstructionTemplate as renderInstructionTemplate2
4571
- } from "@wrongstack/core/utils";
4941
+ assessAtomicity as assessAtomicity2
4942
+ } from "@wrongstack/kanban";
4943
+ function countAcceptanceCriteria(description) {
4944
+ const marker = description.indexOf("**Acceptance Criteria:**");
4945
+ if (marker === -1) return 0;
4946
+ const tail = description.slice(marker);
4947
+ return (tail.match(/^\s*-\s+\S/gm) ?? []).length;
4948
+ }
4949
+ function assessTaskNodeAtomicity(tracker, node, config) {
4950
+ const criteriaCount = countAcceptanceCriteria(node.description ?? "");
4951
+ const verificationCommand = node.metadata?.["verificationCommand"] ?? extractVerificationCommand([node.description ?? ""]);
4952
+ return assessAtomicity2(
4953
+ {
4954
+ title: node.title,
4955
+ description: node.description,
4956
+ estimatedHours: node.estimateHours,
4957
+ dependencyCount: tracker.getBlockers(node.id).length,
4958
+ successCriteriaCount: criteriaCount,
4959
+ hasVerifiableOutput: Boolean(verificationCommand),
4960
+ childCount: tracker.getAllNodes().filter((n) => n.parentId === node.id).length
4961
+ },
4962
+ config
4963
+ );
4964
+ }
4965
+ async function decomposeNonAtomicTasks(opts) {
4966
+ const maxDecompositions = Math.max(1, opts.maxDecompositions ?? 10);
4967
+ const result = { applied: [], proposals: [], flagged: [] };
4968
+ const nodes = opts.tracker.getAllNodes();
4969
+ const childCounts = /* @__PURE__ */ new Map();
4970
+ for (const node of nodes) {
4971
+ if (node.parentId) childCounts.set(node.parentId, (childCounts.get(node.parentId) ?? 0) + 1);
4972
+ }
4973
+ const candidates = nodes.filter(
4974
+ (node) => node.status === "pending" && !childCounts.get(node.id)
4975
+ );
4976
+ let spent = 0;
4977
+ for (const node of candidates) {
4978
+ if (spent >= maxDecompositions) break;
4979
+ const assessment = assessTaskNodeAtomicity(opts.tracker, node, opts.config);
4980
+ opts.tracker.patchMetadata(node.id, {
4981
+ atomicity: {
4982
+ verdict: assessment.verdict,
4983
+ score: assessment.score,
4984
+ reasons: assessment.criteria.filter((c) => c.score < 1).map((c) => c.reason)
4985
+ }
4986
+ });
4987
+ if (assessment.verdict !== "needs_decomposition") continue;
4988
+ result.flagged.push(node.id);
4989
+ spent += 1;
4990
+ const reasons = assessment.criteria.filter((c) => c.score < 1).map((c) => c.reason);
4991
+ const subtasks = await opts.decompose({
4992
+ title: node.title,
4993
+ description: node.description ?? "",
4994
+ reasons
4995
+ });
4996
+ if (!subtasks.length) continue;
4997
+ if (opts.mode === "auto") {
4998
+ const subtaskIds = splitGraphNode(opts.tracker, node.id, subtasks);
4999
+ if (subtaskIds.length) result.applied.push({ nodeId: node.id, subtaskIds });
5000
+ } else {
5001
+ result.proposals.push({ nodeId: node.id, title: node.title, reasons, subtasks });
5002
+ }
5003
+ }
5004
+ return result;
5005
+ }
5006
+
5007
+ // src/conflict-resolver.ts
5008
+ import { readFile as readFile5, writeFile } from "node:fs/promises";
5009
+ import { isAbsolute, join as join6 } from "node:path";
5010
+ import { readBundledInstructionText as readBundledInstructionText2, renderInstructionTemplate as renderInstructionTemplate2 } from "@wrongstack/core/utils";
5011
+ var defaultFileIO = {
5012
+ read: (path6) => readFile5(path6, "utf8"),
5013
+ write: async (path6, content) => {
5014
+ await writeFile(path6, content, "utf8");
5015
+ }
5016
+ };
4572
5017
  var START = "<<<<<<<";
4573
5018
  var BASE = "|||||||";
4574
5019
  var SEP = "=======";
@@ -4606,21 +5051,21 @@ function hasConflictMarkers(text) {
4606
5051
  return m === START || m === SEP || m === END || m === BASE;
4607
5052
  });
4608
5053
  }
4609
- function makePreferSideConflictResolver(side) {
5054
+ function makePreferSideConflictResolver(side, io = defaultFileIO) {
4610
5055
  return async function conflictResolver(info) {
4611
5056
  if (info.conflictFiles.length === 0) return false;
4612
5057
  for (const rel of info.conflictFiles) {
4613
- const abs = isAbsolute(rel) ? rel : join4(info.cwd, rel);
5058
+ const abs = isAbsolute(rel) ? rel : join6(info.cwd, rel);
4614
5059
  let content;
4615
5060
  try {
4616
- content = await readFile4(abs, "utf8");
5061
+ content = await io.read(abs);
4617
5062
  } catch {
4618
5063
  return false;
4619
5064
  }
4620
5065
  const resolved = resolveConflictText(content, side);
4621
5066
  if (hasConflictMarkers(resolved)) return false;
4622
5067
  try {
4623
- await writeFile(abs, resolved, "utf8");
5068
+ await io.write(abs, resolved);
4624
5069
  } catch {
4625
5070
  return false;
4626
5071
  }
@@ -4640,13 +5085,14 @@ function nonMarkerLineCount(text) {
4640
5085
  }
4641
5086
  function makeLlmConflictResolver(opts) {
4642
5087
  const minFraction = opts.minRetainedFraction ?? 0.5;
5088
+ const io = opts.io ?? defaultFileIO;
4643
5089
  return async function conflictResolver(info) {
4644
5090
  if (info.conflictFiles.length === 0) return false;
4645
5091
  for (const rel of info.conflictFiles) {
4646
- const abs = isAbsolute(rel) ? rel : join4(info.cwd, rel);
5092
+ const abs = isAbsolute(rel) ? rel : join6(info.cwd, rel);
4647
5093
  let content;
4648
5094
  try {
4649
- content = await readFile4(abs, "utf8");
5095
+ content = await io.read(abs);
4650
5096
  } catch {
4651
5097
  return false;
4652
5098
  }
@@ -4670,7 +5116,7 @@ function makeLlmConflictResolver(opts) {
4670
5116
  return false;
4671
5117
  }
4672
5118
  try {
4673
- await writeFile(abs, resolved, "utf8");
5119
+ await io.write(abs, resolved);
4674
5120
  } catch {
4675
5121
  return false;
4676
5122
  }
@@ -4700,21 +5146,28 @@ export {
4700
5146
  TaskTracker3 as TaskTracker,
4701
5147
  analyzeCriticalPath,
4702
5148
  applySddLifecycle,
5149
+ assessGeneratedTaskAtomicity,
5150
+ assessTaskNodeAtomicity,
4703
5151
  buildBoardSnapshot,
4704
5152
  buildBoardTasks,
4705
5153
  cleanupSddWorktrees,
4706
5154
  cleanupStaleSddWorktrees,
4707
5155
  cleanupStaleWorktrees,
4708
5156
  createAutoExecutor,
5157
+ decomposeNonAtomicTasks,
4709
5158
  destroySddProject,
4710
5159
  extractVerificationCommand,
5160
+ gatherProjectContext,
4711
5161
  getTemplate,
4712
5162
  hasConflictMarkers,
4713
5163
  isExplanatoryText,
4714
5164
  listTemplates,
5165
+ makeAcceptanceCriteriaVerifier,
4715
5166
  makeCommandVerifier,
5167
+ makeCompositeVerifier,
4716
5168
  makeLlmConflictResolver,
4717
5169
  makeLlmSubtaskGenerator,
5170
+ makePlanningDecomposer,
4718
5171
  makePreferSideConflictResolver,
4719
5172
  renderProgress,
4720
5173
  renderSpecAnalysis,
@@ -4723,6 +5176,7 @@ export {
4723
5176
  resolveConflictText,
4724
5177
  rollbackSddRunFromDisk,
4725
5178
  shortIdMap,
5179
+ splitGraphNode,
4726
5180
  startSddRun,
4727
5181
  templateToMarkdown
4728
5182
  };