@wrongstack/sdd 0.295.0 → 0.295.1

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 (43) 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 +5 -3
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +529 -240
  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/sdd-board-projector.d.ts +14 -1
  19. package/dist/sdd-board-projector.d.ts.map +1 -1
  20. package/dist/sdd-board-store.d.ts +16 -0
  21. package/dist/sdd-board-store.d.ts.map +1 -1
  22. package/dist/sdd-interview-driver.d.ts +2 -2
  23. package/dist/sdd-interview-driver.d.ts.map +1 -1
  24. package/dist/sdd-lifecycle.d.ts.map +1 -1
  25. package/dist/sdd-parallel-run.d.ts +9 -3
  26. package/dist/sdd-parallel-run.d.ts.map +1 -1
  27. package/dist/spec-builder.d.ts +1 -3
  28. package/dist/spec-builder.d.ts.map +1 -1
  29. package/dist/spec-parser.d.ts +1 -1
  30. package/dist/spec-parser.d.ts.map +1 -1
  31. package/dist/spec-versioning.d.ts +1 -2
  32. package/dist/spec-versioning.d.ts.map +1 -1
  33. package/dist/start-sdd-run.d.ts +8 -2
  34. package/dist/start-sdd-run.d.ts.map +1 -1
  35. package/dist/task-flow.d.ts +1 -3
  36. package/dist/task-flow.d.ts.map +1 -1
  37. package/dist/task-generator.d.ts +31 -2
  38. package/dist/task-generator.d.ts.map +1 -1
  39. package/dist/task-visualizer.d.ts +1 -2
  40. package/dist/task-visualizer.d.ts.map +1 -1
  41. package/dist/verify-task.d.ts +31 -2
  42. package/dist/verify-task.d.ts.map +1 -1
  43. 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
  }
@@ -1791,17 +1868,9 @@ var AISpecBuilder = class {
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. */
@@ -1975,7 +2044,7 @@ var AISpecBuilder = class {
1975
2044
  message: "Invalid JSON for spec",
1976
2045
  code: ERROR_CODES2.SDD_PARSE_FAILED,
1977
2046
  cause: e,
1978
- context: { detail: e instanceof Error ? e.message : "parse error" }
2047
+ context: { detail: toErrorMessage(e) }
1979
2048
  });
1980
2049
  }
1981
2050
  if (!parsed || typeof parsed !== "object") {
@@ -1987,7 +2056,7 @@ var AISpecBuilder = class {
1987
2056
  }
1988
2057
  const raw = parsed;
1989
2058
  const now = Date.now();
1990
- const title = String(raw.title ?? this.session.title ?? "Untitled");
2059
+ const title = String(raw.title ?? this.session.title);
1991
2060
  const overview = String(raw.overview ?? "");
1992
2061
  if (!overview || overview === "undefined") {
1993
2062
  throw new SddError2({
@@ -1998,7 +2067,15 @@ var AISpecBuilder = class {
1998
2067
  }
1999
2068
  const rawSections = Array.isArray(raw.sections) ? raw.sections : [];
2000
2069
  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",
2070
+ type: [
2071
+ "overview",
2072
+ "requirements",
2073
+ "architecture",
2074
+ "api",
2075
+ "data",
2076
+ "security",
2077
+ "acceptance"
2078
+ ].includes(String(s.type)) ? String(s.type) : "overview",
2002
2079
  title: String(s.title ?? ""),
2003
2080
  content: String(s.content ?? ""),
2004
2081
  level: Number(s.level) || 1
@@ -2006,7 +2083,9 @@ var AISpecBuilder = class {
2006
2083
  const rawReqs = Array.isArray(raw.requirements) ? raw.requirements : [];
2007
2084
  const requirements = rawReqs.filter((r) => r && typeof r === "object").map((r, i) => ({
2008
2085
  id: String(r.id ?? `REQ-${i + 1}`),
2009
- type: ["functional", "non-functional", "security", "performance", "ux"].includes(String(r.type)) ? String(r.type) : "functional",
2086
+ type: ["functional", "non-functional", "security", "performance", "ux"].includes(
2087
+ String(r.type)
2088
+ ) ? String(r.type) : "functional",
2010
2089
  priority: ["critical", "high", "medium", "low"].includes(String(r.priority)) ? String(r.priority) : "medium",
2011
2090
  description: String(r.description ?? ""),
2012
2091
  acceptanceCriteria: Array.isArray(r.acceptanceCriteria) ? r.acceptanceCriteria.map(String) : []
@@ -2095,7 +2174,6 @@ var AISpecBuilder = class {
2095
2174
  };
2096
2175
 
2097
2176
  // src/sdd-interview-driver.ts
2098
- import { TaskTracker as TaskTracker2, DefaultTaskStore as DefaultTaskStore2 } from "@wrongstack/core/tasking";
2099
2177
  var SddInterviewDriver = class {
2100
2178
  builder;
2101
2179
  o;
@@ -2308,16 +2386,17 @@ var SddInterviewDriver = class {
2308
2386
  );
2309
2387
  if (valid.length === 0) return void 0;
2310
2388
  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;
2389
+ if (!this.tracker) {
2390
+ const tracker2 = new TaskTracker2({ store: new DefaultTaskStore2() });
2391
+ this.graph = await tracker2.createGraph(spec.id, spec.title);
2392
+ this.tracker = tracker2;
2316
2393
  }
2394
+ const tracker = this.tracker;
2395
+ const graph = this.graph;
2317
2396
  const refMap = /* @__PURE__ */ new Map();
2318
2397
  const created = [];
2319
2398
  valid.forEach((task, i) => {
2320
- const node = addTaskToTracker(this.tracker, task);
2399
+ const node = addTaskToTracker(tracker, task);
2321
2400
  created.push({ nodeId: node.id, task });
2322
2401
  if (typeof task.id === "string" && task.id.trim()) {
2323
2402
  refMap.set(task.id.trim().toLowerCase(), node.id);
@@ -2330,13 +2409,13 @@ var SddInterviewDriver = class {
2330
2409
  const deps = Array.isArray(task.dependsOn) ? task.dependsOn : [];
2331
2410
  for (const ref of deps) {
2332
2411
  const depId = refMap.get(normalizeTaskRef(String(ref)));
2333
- if (depId && depId !== nodeId) this.tracker.addDependency(depId, nodeId);
2412
+ if (depId && depId !== nodeId) tracker.addDependency(depId, nodeId);
2334
2413
  }
2335
2414
  }
2336
- await this.persistGraph(this.graph);
2337
- this.builder.setTaskGraphId(this.graph.id);
2415
+ await this.persistGraph(graph);
2416
+ this.builder.setTaskGraphId(graph.id);
2338
2417
  await this.builder.saveSession();
2339
- return this.graph.id;
2418
+ return graph.id;
2340
2419
  }
2341
2420
  };
2342
2421
  var TASK_TYPES = ["feature", "bugfix", "refactor", "docs", "test", "chore"];
@@ -2364,10 +2443,48 @@ function isExplanatoryText(text) {
2364
2443
  import { TOKENS } from "@wrongstack/core/kernel";
2365
2444
 
2366
2445
  // src/sdd-parallel-run.ts
2367
- import { expectDefined as expectDefined2 } from "@wrongstack/core/utils";
2446
+ import { expectDefined as expectDefined3 } from "@wrongstack/core/utils";
2368
2447
  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";
2448
+ import {
2449
+ assignNickname,
2450
+ DefaultMultiAgentCoordinator,
2451
+ makeAgentSubagentRunner,
2452
+ withDisabledToolFiltering
2453
+ } from "@wrongstack/core/coordination";
2454
+ import { ERROR_CODES as ERROR_CODES3, SddError as SddError3 } from "@wrongstack/core/types";
2455
+
2456
+ // src/graph-split.ts
2457
+ function splitGraphNode(tracker, taskId, subtasks, options = {}) {
2458
+ const node = tracker.getNode(taskId);
2459
+ if (!node) return [];
2460
+ if (node.status === "in_progress" || options.isRunning?.(taskId)) return [];
2461
+ if (!subtasks.length) return [];
2462
+ const blockers = tracker.getBlockers(taskId);
2463
+ const dependents = tracker.getDependents(taskId);
2464
+ const leafIds = subtasks.map((s) => {
2465
+ const criterion = s.successCriterion?.trim();
2466
+ const verificationCommand = criterion ? extractVerificationCommand([criterion]) : void 0;
2467
+ const description = criterion && !verificationCommand ? `${s.description}
2468
+
2469
+ **Acceptance Criteria:**
2470
+ - ${criterion}` : s.description;
2471
+ return tracker.addNode({
2472
+ title: s.title,
2473
+ description,
2474
+ type: s.type ?? node.type,
2475
+ priority: s.priority ?? node.priority,
2476
+ status: "pending",
2477
+ parentId: taskId,
2478
+ ...verificationCommand ? { metadata: { verificationCommand } } : {}
2479
+ }).id;
2480
+ });
2481
+ for (const leaf of leafIds) {
2482
+ for (const b of blockers) tracker.addDependency(b, leaf);
2483
+ for (const dep of dependents) tracker.addDependency(leaf, dep);
2484
+ }
2485
+ tracker.updateNodeStatus(taskId, "completed", `split into ${leafIds.length} subtasks`);
2486
+ return leafIds;
2487
+ }
2371
2488
 
2372
2489
  // src/sdd-task-decomposer.ts
2373
2490
  var SddTaskDecomposer = class {
@@ -2491,7 +2608,9 @@ var SddParallelRun = class {
2491
2608
  this.maxTotalWaves = opts.maxTotalWaves ?? opts.graph.nodes.size * (this.maxRetries + 2) + 10;
2492
2609
  this.maxWallClockMs = opts.maxWallClockMs;
2493
2610
  this.maxRecoveryRounds = Math.max(0, opts.maxRecoveryRounds ?? 0);
2494
- this.decomposer = new SddTaskDecomposer(opts.tracker, opts.graph, { parallelSlots: this.slots });
2611
+ this.decomposer = new SddTaskDecomposer(opts.tracker, opts.graph, {
2612
+ parallelSlots: this.slots
2613
+ });
2495
2614
  }
2496
2615
  opts;
2497
2616
  slots;
@@ -2614,7 +2733,8 @@ var SddParallelRun = class {
2614
2733
  * revert outcome; a dirty tree or revert conflict surfaces as `ok:false`.
2615
2734
  */
2616
2735
  async rollback() {
2617
- if (this.isRunning()) return { ok: false, reverted: 0, reason: "run still active \u2014 stop it first" };
2736
+ if (this.isRunning())
2737
+ return { ok: false, reverted: 0, reason: "run still active \u2014 stop it first" };
2618
2738
  const wt = this.opts.worktrees;
2619
2739
  if (!wt || !this.baseBranch) {
2620
2740
  return { ok: false, reverted: 0, reason: "no worktree run to roll back" };
@@ -2647,7 +2767,10 @@ var SddParallelRun = class {
2647
2767
  */
2648
2768
  setTaskModel(taskId, model, provider) {
2649
2769
  if (!this.opts.tracker.getNode(taskId)) return false;
2650
- this.opts.tracker.patchMetadata(taskId, { model, ...provider !== void 0 ? { provider } : {} });
2770
+ this.opts.tracker.patchMetadata(taskId, {
2771
+ model,
2772
+ ...provider !== void 0 ? { provider } : {}
2773
+ });
2651
2774
  return true;
2652
2775
  }
2653
2776
  /** Set/override a task's fallback model chain (applied on its next dispatch). */
@@ -2679,7 +2802,12 @@ var SddParallelRun = class {
2679
2802
  this.cancelledTasks.add(taskId);
2680
2803
  this.opts.tracker.patchMetadata(taskId, { cancelled: true });
2681
2804
  this.opts.tracker.updateNodeStatus(taskId, "failed", "cancelled by user");
2682
- this.emit("sdd.task.failed", { runId: this.runId, taskId, subagentId: "", error: "cancelled by user" });
2805
+ this.emit("sdd.task.failed", {
2806
+ runId: this.runId,
2807
+ taskId,
2808
+ subagentId: "",
2809
+ error: "cancelled by user"
2810
+ });
2683
2811
  const subagentId = this.taskSubagents.get(taskId);
2684
2812
  if (subagentId && this.coordinator) {
2685
2813
  await this.coordinator.stop(subagentId).catch(() => {
@@ -2710,30 +2838,12 @@ var SddParallelRun = class {
2710
2838
  * The scheduler picks the new pending leaves up on its next dispatch pass.
2711
2839
  */
2712
2840
  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
- }
2841
+ const leafIds = splitGraphNode(this.opts.tracker, taskId, subtasks, {
2842
+ isRunning: (id) => this.taskSubagents.has(id)
2843
+ });
2844
+ if (!leafIds.length) return [];
2734
2845
  this.retryMap.delete(taskId);
2735
2846
  this.persistRetries(taskId, 0);
2736
- tracker.updateNodeStatus(taskId, "completed", `split into ${leafIds.length} subtasks`);
2737
2847
  this.emit("sdd.task.split", { runId: this.runId, taskId, subtaskIds: leafIds });
2738
2848
  return leafIds;
2739
2849
  }
@@ -2781,7 +2891,12 @@ var SddParallelRun = class {
2781
2891
  return await this.executeOne(task);
2782
2892
  } catch (err) {
2783
2893
  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) });
2894
+ this.emit("sdd.task.failed", {
2895
+ runId: this.runId,
2896
+ taskId: task.id,
2897
+ subagentId: "",
2898
+ error: String(err)
2899
+ });
2785
2900
  return { taskId: task.id, success: false };
2786
2901
  } finally {
2787
2902
  running.delete(task.id);
@@ -2795,16 +2910,18 @@ var SddParallelRun = class {
2795
2910
  await this.waitWhilePaused();
2796
2911
  if (this.stopRequested) break;
2797
2912
  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
- }
2913
+ const ready = this.decomposer.readyNodes().filter((t) => !running.has(t.id));
2914
+ for (const task of ready) {
2915
+ if (running.size >= this.slots) break;
2916
+ dispatch(task);
2917
+ dispatchedThisRound++;
2805
2918
  }
2806
2919
  if (dispatchedThisRound > 0) {
2807
- this.emit("sdd.wave", { runId: this.runId, wave: this.round, batchSize: dispatchedThisRound });
2920
+ this.emit("sdd.wave", {
2921
+ runId: this.runId,
2922
+ wave: this.round,
2923
+ batchSize: dispatchedThisRound
2924
+ });
2808
2925
  this.round++;
2809
2926
  }
2810
2927
  if (running.size === 0) {
@@ -2835,7 +2952,6 @@ var SddParallelRun = class {
2835
2952
  this.opts.onProgress?.(this.buildProgress());
2836
2953
  }
2837
2954
  }
2838
- if (running.size > 0) await Promise.allSettled(running.values());
2839
2955
  if (this.stopRequested) await this.teardown();
2840
2956
  const finalProgress = this.opts.tracker.getProgress();
2841
2957
  this.emit("sdd.run.finished", {
@@ -3033,10 +3149,11 @@ var SddParallelRun = class {
3033
3149
  }
3034
3150
  this.opts.tracker.updateNodeStatus(taskId, "in_progress");
3035
3151
  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
- });
3152
+ if (!this.coordinator)
3153
+ throw new SddError3({
3154
+ message: "SDD parallel runner requires a coordinator",
3155
+ code: ERROR_CODES3.SDD_INVALID_STATE
3156
+ });
3040
3157
  const coordinator = this.coordinator;
3041
3158
  const subagentId = `sdd-d${this.dispatchSeq++}`;
3042
3159
  const correlationId = randomUUID2();
@@ -3046,7 +3163,7 @@ var SddParallelRun = class {
3046
3163
  const fallbackModels = Array.isArray(meta.fallbackModels) ? meta.fallbackModels : this.opts.fallbackModels;
3047
3164
  const spawnResult = await coordinator.spawn({
3048
3165
  id: subagentId,
3049
- name: agentName ?? subagentId,
3166
+ name: agentName,
3050
3167
  role: "executor",
3051
3168
  // Idle reaper is always on; the hard wall-clock cap only when opted in.
3052
3169
  idleTimeoutMs: this.idleTimeoutMs,
@@ -3068,7 +3185,7 @@ var SddParallelRun = class {
3068
3185
  runId: this.runId,
3069
3186
  taskId,
3070
3187
  subagentId,
3071
- agentName: agentName ?? "",
3188
+ agentName,
3072
3189
  worktreeBranch: this.taskBranches.get(taskId)
3073
3190
  });
3074
3191
  const directivePreamble = [
@@ -3103,7 +3220,7 @@ var SddParallelRun = class {
3103
3220
  let result;
3104
3221
  try {
3105
3222
  const got = await coordinator.awaitTasks([correlationId]);
3106
- result = expectDefined2(got[0]);
3223
+ result = expectDefined3(got[0]);
3107
3224
  } catch (err) {
3108
3225
  result = {
3109
3226
  subagentId,
@@ -3131,12 +3248,22 @@ var SddParallelRun = class {
3131
3248
  } catch (err) {
3132
3249
  verificationFailReason = `verification error: ${String(err)}`;
3133
3250
  }
3251
+ const hadVerifiable = typeof task.metadata?.["verificationCommand"] === "string" || task.description.includes("**Acceptance Criteria:**");
3134
3252
  if (verificationFailReason) {
3253
+ this.opts.tracker.patchMetadata(taskId, {
3254
+ verificationState: "failed",
3255
+ verificationDetail: verificationFailReason
3256
+ });
3135
3257
  this.emit("sdd.task.verification_failed", {
3136
3258
  runId: this.runId,
3137
3259
  taskId,
3138
3260
  reason: verificationFailReason
3139
3261
  });
3262
+ } else if (hadVerifiable) {
3263
+ this.opts.tracker.patchMetadata(taskId, {
3264
+ verificationState: "passed",
3265
+ verificationDetail: void 0
3266
+ });
3140
3267
  }
3141
3268
  }
3142
3269
  let success = false;
@@ -3161,12 +3288,13 @@ var SddParallelRun = class {
3161
3288
  });
3162
3289
  await this.applyTaskFailure(taskId, subagentId, merged.reason);
3163
3290
  } else {
3291
+ const conflictFiles = merged.conflictFiles ?? [];
3164
3292
  this.emit("sdd.task.conflict", {
3165
3293
  runId: this.runId,
3166
3294
  taskId,
3167
- conflictFiles: merged.conflictFiles ?? []
3295
+ conflictFiles
3168
3296
  });
3169
- const reason = `merge conflict${merged.conflictFiles?.length ? `: ${merged.conflictFiles.join(", ")}` : ""}`;
3297
+ const reason = `merge conflict${conflictFiles.length ? `: ${conflictFiles.join(", ")}` : ""}`;
3170
3298
  await this.applyTaskFailure(taskId, subagentId, reason);
3171
3299
  }
3172
3300
  } else {
@@ -3269,7 +3397,11 @@ var SddParallelRun = class {
3269
3397
  const res = await wt.merge(handle, {
3270
3398
  squash: true,
3271
3399
  ...this.opts.conflictResolver ? {
3272
- resolve: (info) => this.opts.conflictResolver({ task, conflictFiles: info.conflictFiles, cwd: info.cwd })
3400
+ resolve: (info) => this.opts.conflictResolver({
3401
+ task,
3402
+ conflictFiles: info.conflictFiles,
3403
+ cwd: info.cwd
3404
+ })
3273
3405
  } : {}
3274
3406
  });
3275
3407
  if (res.ok) {
@@ -3281,7 +3413,8 @@ var SddParallelRun = class {
3281
3413
  result: result ?? {},
3282
3414
  cwd: this.opts.projectRoot
3283
3415
  });
3284
- if (!verdict.ok) regressed = verdict.reason ?? "verification failed after conflict resolution";
3416
+ if (!verdict.ok)
3417
+ regressed = verdict.reason ?? "verification failed after conflict resolution";
3285
3418
  } catch (err) {
3286
3419
  regressed = `verification error after conflict resolution: ${String(err)}`;
3287
3420
  }
@@ -3397,6 +3530,32 @@ var SddParallelRun = class {
3397
3530
  };
3398
3531
 
3399
3532
  // src/start-sdd-run.ts
3533
+ function applySddControlCommand(run, command) {
3534
+ const payload = command.payload ?? {};
3535
+ if (command.type === "pause") run.pause();
3536
+ else if (command.type === "resume") run.resume();
3537
+ else if (command.type === "stop") run.stop();
3538
+ else if (command.type === "retry" && payload.taskId) run.retryTask(payload.taskId);
3539
+ else if (command.type === "retry_all_failed") run.retryAllFailed();
3540
+ else if (command.type === "reassign" && payload.taskId)
3541
+ run.reassignTask(payload.taskId, payload.agentName ?? "");
3542
+ else if (command.type === "set_task_model" && payload.taskId)
3543
+ run.setTaskModel(payload.taskId, payload.model, payload.provider);
3544
+ else if (command.type === "set_task_fallbacks" && payload.taskId)
3545
+ run.setTaskFallbacks(payload.taskId, payload.fallbackModels);
3546
+ else if (command.type === "set_task_verification" && payload.taskId)
3547
+ run.setTaskVerification(payload.taskId, payload.verificationCommand);
3548
+ else if (command.type === "cancel_task" && payload.taskId)
3549
+ void run.cancelTask(payload.taskId).catch(() => {
3550
+ });
3551
+ else if (command.type === "delete_task" && payload.taskId) run.deleteTask(payload.taskId);
3552
+ else if (command.type === "split_task" && payload.taskId && payload.subtasks?.length)
3553
+ run.splitTask(payload.taskId, payload.subtasks);
3554
+ else if (command.type === "cleanup_worktrees") void run.cleanupWorktrees().catch(() => {
3555
+ });
3556
+ else if (command.type === "rollback") void run.rollback().catch(() => {
3557
+ });
3558
+ }
3400
3559
  function startSddRun(opts) {
3401
3560
  SddParallelRun.resetOrphans(opts.tracker);
3402
3561
  const run = new SddParallelRun({
@@ -3460,25 +3619,7 @@ function startSddRun(opts) {
3460
3619
  const controlTimer = setInterval(() => {
3461
3620
  void opts.boardStore.drainControl(run.runId).then((cmds) => {
3462
3621
  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
- });
3622
+ applySddControlCommand(run, c);
3482
3623
  }
3483
3624
  }).catch(() => {
3484
3625
  });
@@ -3506,6 +3647,7 @@ function startSddRun(opts) {
3506
3647
 
3507
3648
  // src/sdd-lifecycle.ts
3508
3649
  import * as fsp4 from "node:fs/promises";
3650
+ import { toErrorMessage as toErrorMessage2 } from "@wrongstack/core/utils";
3509
3651
  import { WorktreeManager } from "@wrongstack/core/worktree";
3510
3652
  async function cleanupSddWorktrees(projectRoot) {
3511
3653
  const wt = new WorktreeManager({ projectRoot });
@@ -3517,19 +3659,21 @@ async function cleanupStaleWorktrees(projectRoot) {
3517
3659
  }
3518
3660
  async function cleanupStaleSddWorktrees(opts) {
3519
3661
  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
- }
3662
+ const store = new SddBoardStore({ baseDir: opts.boardsDir });
3663
+ const latest = (await store.list())[0];
3664
+ if (latest) {
3665
+ const age = now - latest.updatedAt;
3666
+ if (latest.status === "running" && age < (opts.runningLiveMs ?? 12e4)) {
3667
+ return {
3668
+ swept: false,
3669
+ removed: 0,
3670
+ detected: 0,
3671
+ skippedReason: "a run appears live (running)"
3672
+ };
3673
+ }
3674
+ if (latest.status === "paused" && age < (opts.pausedLiveMs ?? 18e5)) {
3675
+ return { swept: false, removed: 0, detected: 0, skippedReason: "a run is paused" };
3531
3676
  }
3532
- } catch {
3533
3677
  }
3534
3678
  try {
3535
3679
  const wt = new WorktreeManager({ projectRoot: opts.projectRoot });
@@ -3546,7 +3690,11 @@ async function rollbackSddRunFromDisk(opts) {
3546
3690
  const snap = await store.load(runId);
3547
3691
  if (!snap) return { ok: false, reverted: 0, reason: `board "${runId}" not found` };
3548
3692
  if (!snap.baseBranch) {
3549
- return { ok: false, reverted: 0, reason: "this run did not record a base branch (no worktree run)" };
3693
+ return {
3694
+ ok: false,
3695
+ reverted: 0,
3696
+ reason: "this run did not record a base branch (no worktree run)"
3697
+ };
3550
3698
  }
3551
3699
  const shas = (snap.mergedCommits ?? []).map((c) => c.sha);
3552
3700
  if (shas.length === 0) {
@@ -3564,7 +3712,7 @@ async function destroySddProject(opts) {
3564
3712
  projectRoot: opts.projectRoot,
3565
3713
  boardsDir: opts.paths.projectSddBoards,
3566
3714
  runId: opts.runId
3567
- }).catch((err) => ({ ok: false, reverted: 0, reason: toReason(err) }));
3715
+ }).catch((err) => ({ ok: false, reverted: 0, reason: toErrorMessage2(err) }));
3568
3716
  reverted = r.reverted;
3569
3717
  revertOk = r.ok;
3570
3718
  revertReason = r.reason;
@@ -3591,9 +3739,6 @@ async function destroySddProject(opts) {
3591
3739
  await rmDir(opts.paths.projectSddBoards, "boards");
3592
3740
  return { worktreesRemoved: removed, deleted, reverted, revertOk, revertReason };
3593
3741
  }
3594
- function toReason(err) {
3595
- return err instanceof Error ? err.message : String(err);
3596
- }
3597
3742
  async function applySddLifecycle(op, opts) {
3598
3743
  try {
3599
3744
  if (op === "cleanup_worktrees") {
@@ -3625,7 +3770,7 @@ async function applySddLifecycle(op, opts) {
3625
3770
  reason: r.revertOk === false ? r.revertReason : void 0
3626
3771
  };
3627
3772
  } catch (err) {
3628
- return { op, ok: false, reason: toReason(err) };
3773
+ return { op, ok: false, reason: toErrorMessage2(err) };
3629
3774
  }
3630
3775
  }
3631
3776
 
@@ -3747,7 +3892,7 @@ function templateToMarkdown(template, title) {
3747
3892
 
3748
3893
  // src/task-visualizer.ts
3749
3894
  import { computeTaskProgress as computeTaskProgress2 } from "@wrongstack/core/tasking";
3750
- import { truncate as truncate2 } from "@wrongstack/core/utils";
3895
+ import { expectDefined as expectDefined4, truncate } from "@wrongstack/core/utils";
3751
3896
  var STATUS_ICON = {
3752
3897
  pending: "\u25CB",
3753
3898
  in_progress: "\u25D0",
@@ -3774,7 +3919,9 @@ function renderTaskGraph(graph, opts) {
3774
3919
  const lines = [];
3775
3920
  const compact = opts?.compact ?? false;
3776
3921
  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`);
3922
+ lines.push(
3923
+ `\u2502 Spec: ${graph.specId.slice(0, 8)}... \u2502 Nodes: ${graph.nodes.size} \u2502 Edges: ${graph.edges.length} \u2502`
3924
+ );
3778
3925
  lines.push("\u2570" + "\u2500".repeat(Math.max(50, graph.title.length + 30)) + "\u256F");
3779
3926
  lines.push("");
3780
3927
  const progress = computeTaskProgress2(graph);
@@ -3809,19 +3956,18 @@ function renderTaskGraph(graph, opts) {
3809
3956
  function renderNode(graph, nodeId, lines, rendered, childrenMap, compact, prefix) {
3810
3957
  if (rendered.has(nodeId)) return;
3811
3958
  rendered.add(nodeId);
3812
- const node = graph.nodes.get(nodeId);
3813
- if (!node) return;
3959
+ const node = expectDefined4(graph.nodes.get(nodeId));
3814
3960
  const icon = STATUS_ICON[node.status];
3815
3961
  const prioIcon = PRIORITY_ICON[node.priority];
3816
3962
  const typeIcon = TYPE_ICON[node.type];
3817
- const title = compact ? truncate2(node.title, 40) : node.title;
3963
+ const title = compact ? truncate(node.title, 40) : node.title;
3818
3964
  const blockedBy = childrenMap.get(nodeId) ?? [];
3819
3965
  const depsStr = blockedBy.length > 0 ? ` \u2190 [${blockedBy.map((d) => graph.nodes.get(d)?.title?.slice(0, 12) ?? "?").join(", ")}]` : "";
3820
3966
  lines.push(`${prefix}${icon} ${typeIcon} ${prioIcon} ${title}${depsStr}`);
3821
3967
  if (!compact && node.description) {
3822
3968
  const descLines = node.description.split("\n").slice(0, 3);
3823
3969
  for (const dl of descLines) {
3824
- lines.push(`${prefix} \u2514 ${truncate2(dl, 60)}`);
3970
+ lines.push(`${prefix} \u2514 ${truncate(dl, 60)}`);
3825
3971
  }
3826
3972
  }
3827
3973
  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 +3997,7 @@ function renderTaskList(graph) {
3851
3997
  completed: []
3852
3998
  };
3853
3999
  for (const node of nodes) {
3854
- groups[node.status]?.push(node);
4000
+ groups[node.status].push(node);
3855
4001
  }
3856
4002
  for (const [status, group] of Object.entries(groups)) {
3857
4003
  if (group.length === 0) continue;
@@ -3899,8 +4045,8 @@ function renderSpecAnalysis(spec, analysis) {
3899
4045
  }
3900
4046
 
3901
4047
  // src/critical-path.ts
3902
- import { expectDefined as expectDefined3 } from "@wrongstack/core/utils";
3903
4048
  import { topologicalSort } from "@wrongstack/core/tasking";
4049
+ import { expectDefined as expectDefined5 } from "@wrongstack/core/utils";
3904
4050
  function analyzeCriticalPath(graph) {
3905
4051
  const nodes = Array.from(graph.nodes.values());
3906
4052
  const topoOrder = topologicalSort(graph);
@@ -3954,8 +4100,7 @@ function analyzeCriticalPath(graph) {
3954
4100
  bottlenecks.sort((a, b) => b.severity - a.severity);
3955
4101
  const criticalPath = computeCriticalPath(graph, topoOrder, blockedByMap);
3956
4102
  const totalHours = criticalPath.reduce((sum, id) => {
3957
- const n = graph.nodes.get(id);
3958
- return sum + (n?.estimateHours ?? 0);
4103
+ return sum + (graph.nodes.get(id).estimateHours ?? 0);
3959
4104
  }, 0);
3960
4105
  const parallelGroups = computeParallelGroups(graph, blockedByMap);
3961
4106
  const executionOrder = topoOrder.filter((id) => {
@@ -3976,7 +4121,7 @@ function getTransitiveBlocked(_graph, taskId, blocksMap) {
3976
4121
  const visited = /* @__PURE__ */ new Set();
3977
4122
  const queue = [taskId];
3978
4123
  while (queue.length > 0) {
3979
- const current = expectDefined3(queue.shift());
4124
+ const current = expectDefined5(queue.shift());
3980
4125
  const blocked = blocksMap.get(current);
3981
4126
  if (!blocked) continue;
3982
4127
  for (const id of blocked) {
@@ -4011,7 +4156,7 @@ function computeCriticalPath(graph, _topoOrder, blockedByMap) {
4011
4156
  const blocked = blocksMap.get(id);
4012
4157
  if (!blocked) continue;
4013
4158
  for (const blockedId of blocked) {
4014
- const candidateDist = (dist.get(id) ?? 0) + (graph.nodes.get(blockedId)?.estimateHours ?? 1);
4159
+ const candidateDist = dist.get(id) + (graph.nodes.get(blockedId)?.estimateHours ?? 1);
4015
4160
  if (candidateDist > (dist.get(blockedId) ?? 0)) {
4016
4161
  dist.set(blockedId, candidateDist);
4017
4162
  prev.set(blockedId, id);
@@ -4022,9 +4167,9 @@ function computeCriticalPath(graph, _topoOrder, blockedByMap) {
4022
4167
  if (!changed) break;
4023
4168
  }
4024
4169
  let maxDist = 0;
4025
- let maxId = expectDefined3(allIds[0]);
4170
+ let maxId = expectDefined5(allIds[0]);
4026
4171
  for (const id of allIds) {
4027
- const d = dist.get(id) ?? 0;
4172
+ const d = dist.get(id);
4028
4173
  if (d > maxDist) {
4029
4174
  maxDist = d;
4030
4175
  maxId = id;
@@ -4059,8 +4204,7 @@ function computeParallelGroups(graph, blockedByMap) {
4059
4204
  }
4060
4205
  }
4061
4206
  if (group.length === 0) {
4062
- const first = Array.from(remaining)[0];
4063
- if (first) group.push(first);
4207
+ group.push(expectDefined5(Array.from(remaining)[0]));
4064
4208
  }
4065
4209
  for (const id of group) {
4066
4210
  assigned.add(id);
@@ -4274,7 +4418,6 @@ var AutoExecutor = class {
4274
4418
  for (let i = 0; i < results.length; i++) {
4275
4419
  const result = results[i];
4276
4420
  const task = batch[i];
4277
- if (!result || !task) continue;
4278
4421
  if (result.status === "fulfilled") {
4279
4422
  const { result: execResult, retries } = result.value;
4280
4423
  if (execResult.success) {
@@ -4324,14 +4467,14 @@ var AutoExecutor = class {
4324
4467
  }
4325
4468
  }
4326
4469
  const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
4327
- ready.sort((a, b) => (priorityOrder[a.priority] ?? 4) - (priorityOrder[b.priority] ?? 4));
4470
+ ready.sort((a, b) => priorityOrder[a.priority] - priorityOrder[b.priority]);
4328
4471
  return ready;
4329
4472
  }
4330
4473
  /** Execute a single task with retry logic. */
4331
4474
  async executeTaskWithRetry(task, graph, spec) {
4332
4475
  const maxRetries = this.opts.maxRetries ?? 2;
4333
4476
  let retryCount = this.retryMap.get(task.id) ?? 0;
4334
- while (retryCount <= maxRetries) {
4477
+ while (true) {
4335
4478
  this.opts.tracker.updateNodeStatus(task.id, "in_progress");
4336
4479
  this.opts.onTaskStart?.(task);
4337
4480
  const dependencies = this.getTaskDependencies(task.id, graph);
@@ -4375,7 +4518,6 @@ var AutoExecutor = class {
4375
4518
  };
4376
4519
  }
4377
4520
  }
4378
- return { result: { success: false, error: "Max retries exceeded" }, retries: retryCount };
4379
4521
  }
4380
4522
  /** Get tasks that this task depends on. */
4381
4523
  getTaskDependencies(taskId, graph) {
@@ -4462,6 +4604,54 @@ Supervisor rescues already used: ${attempts}`,
4462
4604
 
4463
4605
  // src/verify-task.ts
4464
4606
  import { spawn } from "node:child_process";
4607
+ function verificationShell(platform) {
4608
+ return platform === "win32" ? ["cmd", "/d", "/c"] : ["sh", "-c"];
4609
+ }
4610
+ function makeCompositeVerifier(parts) {
4611
+ return async function verifyTask(info) {
4612
+ for (const part of parts) {
4613
+ const outcome = await part(info);
4614
+ if (!outcome.ok) return outcome;
4615
+ }
4616
+ return { ok: true };
4617
+ };
4618
+ }
4619
+ function makeAcceptanceCriteriaVerifier(options) {
4620
+ const maxResultChars = options.maxResultChars ?? 4e3;
4621
+ return async function verifyTask(info) {
4622
+ const description = info.task.description ?? "";
4623
+ const marker = description.indexOf("**Acceptance Criteria:**");
4624
+ if (marker === -1) return { ok: true };
4625
+ const criteria = description.slice(marker);
4626
+ const resultText = typeof info.result.result === "string" ? info.result.result.slice(0, maxResultChars) : JSON.stringify(info.result.result ?? "").slice(0, maxResultChars);
4627
+ let text;
4628
+ try {
4629
+ text = await options.run(
4630
+ [
4631
+ "You are a strict acceptance reviewer for one completed engineering task.",
4632
+ `Task: ${info.task.title}`,
4633
+ "",
4634
+ criteria,
4635
+ "",
4636
+ "Worker's reported result:",
4637
+ resultText || "(no result text)",
4638
+ "",
4639
+ "Does the reported result plausibly satisfy EVERY acceptance criterion?",
4640
+ 'Answer with exactly one line: "VERDICT: PASS" or "VERDICT: FAIL \u2014 <short reason>".'
4641
+ ].join("\n")
4642
+ );
4643
+ } catch {
4644
+ return { ok: true };
4645
+ }
4646
+ const match = text.match(/VERDICT:\s*(PASS|FAIL)(?:\s*[—-]\s*(.*))?/i);
4647
+ if (!match) return { ok: true };
4648
+ if (match[1].toUpperCase() === "PASS") return { ok: true };
4649
+ return {
4650
+ ok: false,
4651
+ reason: `acceptance criteria not met: ${match[2]?.trim() || "judge rejected the result"}`
4652
+ };
4653
+ };
4654
+ }
4465
4655
  function makeCommandVerifier(options = {}) {
4466
4656
  const metadataKey = options.metadataKey ?? "verificationCommand";
4467
4657
  const timeoutMs = options.timeoutMs ?? 18e4;
@@ -4469,8 +4659,7 @@ function makeCommandVerifier(options = {}) {
4469
4659
  const cmd = info.task.metadata?.[metadataKey];
4470
4660
  if (typeof cmd !== "string" || !cmd.trim()) return { ok: true };
4471
4661
  return await new Promise((resolve) => {
4472
- const isWindows = process.platform === "win32";
4473
- const [shell, ...shellArgs] = isWindows ? ["cmd", "/d", "/c"] : ["sh", "-c"];
4662
+ const [shell, ...shellArgs] = verificationShell(process.platform);
4474
4663
  const child = spawn(shell, [...shellArgs, cmd], {
4475
4664
  cwd: info.cwd,
4476
4665
  shell: false,
@@ -4492,7 +4681,6 @@ function makeCommandVerifier(options = {}) {
4492
4681
  });
4493
4682
  child.on("error", (err) => {
4494
4683
  clearTimeout(timer);
4495
- if (timedOut) return;
4496
4684
  resolve({ ok: false, reason: `verification spawn error: ${String(err)}` });
4497
4685
  });
4498
4686
  });
@@ -4500,10 +4688,7 @@ function makeCommandVerifier(options = {}) {
4500
4688
  }
4501
4689
 
4502
4690
  // src/decompose-task.ts
4503
- import {
4504
- readBundledInstructionText,
4505
- renderInstructionTemplate
4506
- } from "@wrongstack/core/utils";
4691
+ import { readBundledInstructionText, renderInstructionTemplate } from "@wrongstack/core/utils";
4507
4692
  var TASK_TYPES2 = /* @__PURE__ */ new Set(["feature", "bugfix", "refactor", "docs", "test", "chore"]);
4508
4693
  var PRIORITIES = /* @__PURE__ */ new Set(["critical", "high", "medium", "low"]);
4509
4694
  function extractJsonArray(text) {
@@ -4527,48 +4712,144 @@ function buildPrompt(task, error, min, max) {
4527
4712
  error: error || "(none recorded)"
4528
4713
  });
4529
4714
  }
4530
- function makeLlmSubtaskGenerator(opts) {
4715
+ function parseSubtaskSpecs(text, min, max, options = {}) {
4716
+ const json = extractJsonArray(text ?? "");
4717
+ if (!json) return [];
4718
+ let raw;
4719
+ try {
4720
+ raw = JSON.parse(json);
4721
+ } catch {
4722
+ return [];
4723
+ }
4724
+ const items = raw;
4725
+ const specs = [];
4726
+ for (const item of items) {
4727
+ if (!item || typeof item !== "object") continue;
4728
+ const r = item;
4729
+ const title = typeof r["title"] === "string" ? r["title"].trim() : "";
4730
+ const description = typeof r["description"] === "string" ? r["description"].trim() : "";
4731
+ if (!title || !description) continue;
4732
+ const type = TASK_TYPES2.has(r["type"]) ? r["type"] : void 0;
4733
+ const priority = PRIORITIES.has(r["priority"]) ? r["priority"] : void 0;
4734
+ const successCriterion = options.acceptSuccessCriterion && typeof r["successCriterion"] === "string" ? r["successCriterion"].trim() || void 0 : void 0;
4735
+ specs.push({ title, description, type, priority, successCriterion });
4736
+ if (specs.length >= max) break;
4737
+ }
4738
+ return specs.length >= min ? specs : [];
4739
+ }
4740
+ function makePlanningDecomposer(opts) {
4531
4741
  const min = Math.max(2, opts.minSubtasks ?? 2);
4532
- const max = Math.max(min, opts.maxSubtasks ?? 4);
4533
- return async function generateSubtasks(info) {
4742
+ const max = Math.max(min, opts.maxSubtasks ?? 5);
4743
+ return async function decompose(info) {
4534
4744
  let text;
4535
4745
  try {
4536
- text = await opts.run(buildPrompt(info.task, info.error, min, max));
4746
+ text = await opts.run(
4747
+ renderInstructionTemplate(readBundledInstructionText("sdd/decompose-task-planning.md"), {
4748
+ minSubtasks: String(min),
4749
+ maxSubtasks: String(max),
4750
+ title: info.title,
4751
+ description: info.description,
4752
+ reasons: info.reasons.length ? info.reasons.map((r) => `- ${r}`).join("\n") : "- (unspecified)"
4753
+ })
4754
+ );
4537
4755
  } catch {
4538
4756
  return [];
4539
4757
  }
4540
- const json = extractJsonArray(text ?? "");
4541
- if (!json) return [];
4542
- let raw;
4758
+ return parseSubtaskSpecs(text, min, max, { acceptSuccessCriterion: true });
4759
+ };
4760
+ }
4761
+ function makeLlmSubtaskGenerator(opts) {
4762
+ const min = Math.max(2, opts.minSubtasks ?? 2);
4763
+ const max = Math.max(min, opts.maxSubtasks ?? 4);
4764
+ return async function generateSubtasks(info) {
4765
+ let text;
4543
4766
  try {
4544
- raw = JSON.parse(json);
4767
+ text = await opts.run(buildPrompt(info.task, info.error, min, max));
4545
4768
  } catch {
4546
4769
  return [];
4547
4770
  }
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 : [];
4771
+ return parseSubtaskSpecs(text, min, max);
4562
4772
  };
4563
4773
  }
4564
4774
 
4775
+ // src/plan-decompose.ts
4776
+ import {
4777
+ assessAtomicity as assessAtomicity2
4778
+ } from "@wrongstack/kanban";
4779
+ function countAcceptanceCriteria(description) {
4780
+ const marker = description.indexOf("**Acceptance Criteria:**");
4781
+ if (marker === -1) return 0;
4782
+ const tail = description.slice(marker);
4783
+ return (tail.match(/^\s*-\s+\S/gm) ?? []).length;
4784
+ }
4785
+ function assessTaskNodeAtomicity(tracker, node, config) {
4786
+ const criteriaCount = countAcceptanceCriteria(node.description ?? "");
4787
+ const verificationCommand = node.metadata?.["verificationCommand"] ?? extractVerificationCommand([node.description ?? ""]);
4788
+ return assessAtomicity2(
4789
+ {
4790
+ title: node.title,
4791
+ description: node.description,
4792
+ estimatedHours: node.estimateHours,
4793
+ dependencyCount: tracker.getBlockers(node.id).length,
4794
+ successCriteriaCount: criteriaCount,
4795
+ hasVerifiableOutput: Boolean(verificationCommand),
4796
+ childCount: tracker.getAllNodes().filter((n) => n.parentId === node.id).length
4797
+ },
4798
+ config
4799
+ );
4800
+ }
4801
+ async function decomposeNonAtomicTasks(opts) {
4802
+ const maxDecompositions = Math.max(1, opts.maxDecompositions ?? 10);
4803
+ const result = { applied: [], proposals: [], flagged: [] };
4804
+ const nodes = opts.tracker.getAllNodes();
4805
+ const childCounts = /* @__PURE__ */ new Map();
4806
+ for (const node of nodes) {
4807
+ if (node.parentId) childCounts.set(node.parentId, (childCounts.get(node.parentId) ?? 0) + 1);
4808
+ }
4809
+ const candidates = nodes.filter(
4810
+ (node) => node.status === "pending" && !childCounts.get(node.id)
4811
+ );
4812
+ let spent = 0;
4813
+ for (const node of candidates) {
4814
+ if (spent >= maxDecompositions) break;
4815
+ const assessment = assessTaskNodeAtomicity(opts.tracker, node, opts.config);
4816
+ opts.tracker.patchMetadata(node.id, {
4817
+ atomicity: {
4818
+ verdict: assessment.verdict,
4819
+ score: assessment.score,
4820
+ reasons: assessment.criteria.filter((c) => c.score < 1).map((c) => c.reason)
4821
+ }
4822
+ });
4823
+ if (assessment.verdict !== "needs_decomposition") continue;
4824
+ result.flagged.push(node.id);
4825
+ spent += 1;
4826
+ const reasons = assessment.criteria.filter((c) => c.score < 1).map((c) => c.reason);
4827
+ const subtasks = await opts.decompose({
4828
+ title: node.title,
4829
+ description: node.description ?? "",
4830
+ reasons
4831
+ });
4832
+ if (!subtasks.length) continue;
4833
+ if (opts.mode === "auto") {
4834
+ const subtaskIds = splitGraphNode(opts.tracker, node.id, subtasks);
4835
+ if (subtaskIds.length) result.applied.push({ nodeId: node.id, subtaskIds });
4836
+ } else {
4837
+ result.proposals.push({ nodeId: node.id, title: node.title, reasons, subtasks });
4838
+ }
4839
+ }
4840
+ return result;
4841
+ }
4842
+
4565
4843
  // src/conflict-resolver.ts
4566
4844
  import { readFile as readFile4, writeFile } from "node:fs/promises";
4567
- import { join as join4, isAbsolute } from "node:path";
4568
- import {
4569
- readBundledInstructionText as readBundledInstructionText2,
4570
- renderInstructionTemplate as renderInstructionTemplate2
4571
- } from "@wrongstack/core/utils";
4845
+ import { isAbsolute, join as join4 } from "node:path";
4846
+ import { readBundledInstructionText as readBundledInstructionText2, renderInstructionTemplate as renderInstructionTemplate2 } from "@wrongstack/core/utils";
4847
+ var defaultFileIO = {
4848
+ read: (path4) => readFile4(path4, "utf8"),
4849
+ write: async (path4, content) => {
4850
+ await writeFile(path4, content, "utf8");
4851
+ }
4852
+ };
4572
4853
  var START = "<<<<<<<";
4573
4854
  var BASE = "|||||||";
4574
4855
  var SEP = "=======";
@@ -4606,21 +4887,21 @@ function hasConflictMarkers(text) {
4606
4887
  return m === START || m === SEP || m === END || m === BASE;
4607
4888
  });
4608
4889
  }
4609
- function makePreferSideConflictResolver(side) {
4890
+ function makePreferSideConflictResolver(side, io = defaultFileIO) {
4610
4891
  return async function conflictResolver(info) {
4611
4892
  if (info.conflictFiles.length === 0) return false;
4612
4893
  for (const rel of info.conflictFiles) {
4613
4894
  const abs = isAbsolute(rel) ? rel : join4(info.cwd, rel);
4614
4895
  let content;
4615
4896
  try {
4616
- content = await readFile4(abs, "utf8");
4897
+ content = await io.read(abs);
4617
4898
  } catch {
4618
4899
  return false;
4619
4900
  }
4620
4901
  const resolved = resolveConflictText(content, side);
4621
4902
  if (hasConflictMarkers(resolved)) return false;
4622
4903
  try {
4623
- await writeFile(abs, resolved, "utf8");
4904
+ await io.write(abs, resolved);
4624
4905
  } catch {
4625
4906
  return false;
4626
4907
  }
@@ -4640,13 +4921,14 @@ function nonMarkerLineCount(text) {
4640
4921
  }
4641
4922
  function makeLlmConflictResolver(opts) {
4642
4923
  const minFraction = opts.minRetainedFraction ?? 0.5;
4924
+ const io = opts.io ?? defaultFileIO;
4643
4925
  return async function conflictResolver(info) {
4644
4926
  if (info.conflictFiles.length === 0) return false;
4645
4927
  for (const rel of info.conflictFiles) {
4646
4928
  const abs = isAbsolute(rel) ? rel : join4(info.cwd, rel);
4647
4929
  let content;
4648
4930
  try {
4649
- content = await readFile4(abs, "utf8");
4931
+ content = await io.read(abs);
4650
4932
  } catch {
4651
4933
  return false;
4652
4934
  }
@@ -4670,7 +4952,7 @@ function makeLlmConflictResolver(opts) {
4670
4952
  return false;
4671
4953
  }
4672
4954
  try {
4673
- await writeFile(abs, resolved, "utf8");
4955
+ await io.write(abs, resolved);
4674
4956
  } catch {
4675
4957
  return false;
4676
4958
  }
@@ -4700,21 +4982,27 @@ export {
4700
4982
  TaskTracker3 as TaskTracker,
4701
4983
  analyzeCriticalPath,
4702
4984
  applySddLifecycle,
4985
+ assessGeneratedTaskAtomicity,
4986
+ assessTaskNodeAtomicity,
4703
4987
  buildBoardSnapshot,
4704
4988
  buildBoardTasks,
4705
4989
  cleanupSddWorktrees,
4706
4990
  cleanupStaleSddWorktrees,
4707
4991
  cleanupStaleWorktrees,
4708
4992
  createAutoExecutor,
4993
+ decomposeNonAtomicTasks,
4709
4994
  destroySddProject,
4710
4995
  extractVerificationCommand,
4711
4996
  getTemplate,
4712
4997
  hasConflictMarkers,
4713
4998
  isExplanatoryText,
4714
4999
  listTemplates,
5000
+ makeAcceptanceCriteriaVerifier,
4715
5001
  makeCommandVerifier,
5002
+ makeCompositeVerifier,
4716
5003
  makeLlmConflictResolver,
4717
5004
  makeLlmSubtaskGenerator,
5005
+ makePlanningDecomposer,
4718
5006
  makePreferSideConflictResolver,
4719
5007
  renderProgress,
4720
5008
  renderSpecAnalysis,
@@ -4723,6 +5011,7 @@ export {
4723
5011
  resolveConflictText,
4724
5012
  rollbackSddRunFromDisk,
4725
5013
  shortIdMap,
5014
+ splitGraphNode,
4726
5015
  startSddRun,
4727
5016
  templateToMarkdown
4728
5017
  };