@wrongstack/sdd 0.293.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 (47) 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 +722 -268
  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 +9 -6
  19. package/dist/sdd-board-projector.d.ts.map +1 -1
  20. package/dist/sdd-board-store.d.ts +39 -1
  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 +11 -5
  26. package/dist/sdd-parallel-run.d.ts.map +1 -1
  27. package/dist/sdd-supervisor.d.ts +1 -1
  28. package/dist/sdd-supervisor.d.ts.map +1 -1
  29. package/dist/spec-builder.d.ts +1 -3
  30. package/dist/spec-builder.d.ts.map +1 -1
  31. package/dist/spec-parser.d.ts +1 -1
  32. package/dist/spec-parser.d.ts.map +1 -1
  33. package/dist/spec-versioning.d.ts +1 -2
  34. package/dist/spec-versioning.d.ts.map +1 -1
  35. package/dist/start-sdd-run.d.ts +11 -5
  36. package/dist/start-sdd-run.d.ts.map +1 -1
  37. package/dist/task-flow.d.ts +1 -3
  38. package/dist/task-flow.d.ts.map +1 -1
  39. package/dist/task-generator.d.ts +31 -2
  40. package/dist/task-generator.d.ts.map +1 -1
  41. package/dist/task-tracker.d.ts +1 -1
  42. package/dist/task-tracker.d.ts.map +1 -1
  43. package/dist/task-visualizer.d.ts +1 -2
  44. package/dist/task-visualizer.d.ts.map +1 -1
  45. package/dist/verify-task.d.ts +31 -2
  46. package/dist/verify-task.d.ts.map +1 -1
  47. 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";
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) {
@@ -549,7 +623,7 @@ var SpecDrivenDev = class {
549
623
  import * as fsp from "node:fs/promises";
550
624
  import * as path from "node:path";
551
625
  import { randomUUID } from "node:crypto";
552
- import { atomicWrite, ensureDir } from "@wrongstack/core";
626
+ import { atomicWrite, ensureDir } from "@wrongstack/core/utils";
553
627
  var SpecStore = class {
554
628
  baseDir;
555
629
  indexPath;
@@ -663,7 +737,7 @@ var SpecStore = class {
663
737
  // src/task-graph-store.ts
664
738
  import * as fsp2 from "node:fs/promises";
665
739
  import * as path2 from "node:path";
666
- import { atomicWrite as atomicWrite2, ensureDir as ensureDir2 } from "@wrongstack/core";
740
+ import { atomicWrite as atomicWrite2, ensureDir as ensureDir2 } from "@wrongstack/core/utils";
667
741
  function graphToJSON(graph) {
668
742
  const serialisable = {
669
743
  ...graph,
@@ -762,7 +836,7 @@ var TaskGraphStore = class {
762
836
  };
763
837
 
764
838
  // src/board-types.ts
765
- import { computeTaskProgress } from "@wrongstack/core/types";
839
+ import { computeTaskProgress } from "@wrongstack/core/tasking";
766
840
  function shortIdMap(graph) {
767
841
  const nodes = Array.from(graph.nodes.values()).sort((a, b) => a.createdAt - b.createdAt);
768
842
  const m = /* @__PURE__ */ new Map();
@@ -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) {
@@ -854,13 +930,36 @@ function buildBoardSnapshot(graph, run, now) {
854
930
  // src/sdd-board-store.ts
855
931
  import * as fsp3 from "node:fs/promises";
856
932
  import * as path3 from "node:path";
857
- import { atomicWrite as atomicWrite3, ensureDir as ensureDir3 } from "@wrongstack/core";
933
+ import { atomicWrite as atomicWrite3, ensureDir as ensureDir3, withFileLock } from "@wrongstack/core/utils";
934
+ var DEFAULT_EVENT_MAX_BYTES = 16 * 1024 * 1024;
935
+ var DEFAULT_EVENT_KEEP_BYTES = 8 * 1024 * 1024;
936
+ var DEFAULT_EVENT_SIZE_CHECK_EVERY = 100;
858
937
  var SddBoardStore = class {
859
938
  baseDir;
860
939
  indexPath;
940
+ eventMaxBytes;
941
+ eventKeepBytes;
942
+ eventSizeCheckEvery;
943
+ controlFileIO;
944
+ eventChains = /* @__PURE__ */ new Map();
945
+ eventWritesSinceCheck = /* @__PURE__ */ new Map();
946
+ controlDrains = /* @__PURE__ */ new Map();
947
+ baseDirReady;
948
+ cachedIndex;
949
+ cachedIndexSignature = null;
861
950
  constructor(opts) {
862
951
  this.baseDir = opts.baseDir;
863
952
  this.indexPath = path3.join(this.baseDir, "_index.json");
953
+ this.eventMaxBytes = Math.max(1024, Math.floor(opts.eventMaxBytes ?? DEFAULT_EVENT_MAX_BYTES));
954
+ this.eventKeepBytes = Math.min(
955
+ this.eventMaxBytes,
956
+ Math.max(0, Math.floor(opts.eventKeepBytes ?? DEFAULT_EVENT_KEEP_BYTES))
957
+ );
958
+ this.eventSizeCheckEvery = Math.max(
959
+ 1,
960
+ Math.floor(opts.eventSizeCheckEvery ?? DEFAULT_EVENT_SIZE_CHECK_EVERY)
961
+ );
962
+ this.controlFileIO = opts.controlFileIO ?? fsp3;
864
963
  }
865
964
  snapshotPath(runId) {
866
965
  return path3.join(this.baseDir, `${this.safe(runId)}.json`);
@@ -872,7 +971,7 @@ var SddBoardStore = class {
872
971
  return path3.join(this.baseDir, `${this.safe(runId)}.control.jsonl`);
873
972
  }
874
973
  async saveSnapshot(snapshot) {
875
- await ensureDir3(this.baseDir);
974
+ await this.ensureBaseDir();
876
975
  await atomicWrite3(this.snapshotPath(snapshot.runId), JSON.stringify(snapshot, null, 2), {
877
976
  mode: 384
878
977
  });
@@ -888,7 +987,12 @@ var SddBoardStore = class {
888
987
  }
889
988
  async list() {
890
989
  const index = await this.readIndex();
891
- return index.entries.sort((a, b) => b.updatedAt - a.updatedAt);
990
+ return index.entries.map((entry) => ({ ...entry }));
991
+ }
992
+ /** Latest board metadata without cloning/sorting the complete index. */
993
+ async latest() {
994
+ const entry = (await this.readIndex()).entries[0];
995
+ return entry ? { ...entry } : void 0;
892
996
  }
893
997
  async loadLatestForSpec(specId) {
894
998
  const entry = (await this.list()).find((e) => e.specId === specId);
@@ -896,63 +1000,157 @@ var SddBoardStore = class {
896
1000
  }
897
1001
  /** Append one line to the board's JSONL event log (best-effort, never throws). */
898
1002
  async appendEvent(runId, event) {
899
- try {
900
- await ensureDir3(this.baseDir);
901
- await fsp3.appendFile(this.eventsPath(runId), `${JSON.stringify(event)}
902
- `, { mode: 384 });
903
- } catch {
904
- }
1003
+ const filePath = this.eventsPath(runId);
1004
+ const previous = this.eventChains.get(filePath) ?? Promise.resolve();
1005
+ const write = previous.then(() => this.appendEventInternal(filePath, event)).catch(() => void 0);
1006
+ this.eventChains.set(filePath, write);
1007
+ await write;
1008
+ if (this.eventChains.get(filePath) === write) this.eventChains.delete(filePath);
905
1009
  }
906
1010
  /** Append a control command (used by readers to steer a CLI-owned run). */
907
1011
  async appendControl(runId, command) {
908
- await ensureDir3(this.baseDir);
909
- await fsp3.appendFile(this.controlPath(runId), `${JSON.stringify(command)}
910
- `, { mode: 384 });
1012
+ await this.ensureBaseDir();
1013
+ const filePath = this.controlPath(runId);
1014
+ await withFileLock(
1015
+ filePath,
1016
+ () => fsp3.appendFile(filePath, `${JSON.stringify(command)}
1017
+ `, { mode: 384 })
1018
+ );
911
1019
  }
912
1020
  /** Read + truncate the control queue (the run drains it). Returns parsed commands. */
913
1021
  async drainControl(runId) {
914
- const p = this.controlPath(runId);
915
- let raw;
916
- try {
917
- raw = await fsp3.readFile(p, "utf8");
918
- } catch {
1022
+ const filePath = this.controlPath(runId);
1023
+ const active = this.controlDrains.get(filePath);
1024
+ if (active) {
1025
+ await active;
919
1026
  return [];
920
1027
  }
1028
+ const drain = this.drainControlInternal(filePath);
1029
+ this.controlDrains.set(filePath, drain);
921
1030
  try {
922
- await fsp3.writeFile(p, "", { mode: 384 });
923
- } catch {
1031
+ return await drain;
1032
+ } finally {
1033
+ this.controlDrains.delete(filePath);
924
1034
  }
925
- return raw.split("\n").filter((l) => l.trim()).map((l) => {
926
- try {
927
- return JSON.parse(l);
928
- } catch {
929
- return null;
930
- }
931
- }).filter((c) => c !== null);
932
1035
  }
933
1036
  async delete(runId) {
1037
+ const eventPath = this.eventsPath(runId);
1038
+ await this.eventChains.get(eventPath);
1039
+ this.eventChains.delete(eventPath);
934
1040
  await Promise.allSettled([
935
1041
  fsp3.unlink(this.snapshotPath(runId)),
936
- fsp3.unlink(this.eventsPath(runId)),
1042
+ fsp3.unlink(eventPath),
937
1043
  fsp3.unlink(this.controlPath(runId))
938
1044
  ]);
1045
+ this.eventWritesSinceCheck.delete(eventPath);
939
1046
  await this.removeFromIndex(runId);
940
1047
  }
941
1048
  // ── internal ────────────────────────────────────────────────────────────
942
1049
  safe(runId) {
943
1050
  return runId.replace(/[^a-zA-Z0-9._-]/g, "_");
944
1051
  }
1052
+ async ensureBaseDir() {
1053
+ this.baseDirReady ??= ensureDir3(this.baseDir).catch((error) => {
1054
+ this.baseDirReady = void 0;
1055
+ throw error;
1056
+ });
1057
+ await this.baseDirReady;
1058
+ }
1059
+ async appendEventInternal(filePath, event) {
1060
+ await this.ensureBaseDir();
1061
+ await fsp3.appendFile(filePath, `${JSON.stringify(event)}
1062
+ `, { mode: 384 });
1063
+ const writes = (this.eventWritesSinceCheck.get(filePath) ?? 0) + 1;
1064
+ if (writes < this.eventSizeCheckEvery) {
1065
+ this.eventWritesSinceCheck.set(filePath, writes);
1066
+ return;
1067
+ }
1068
+ this.eventWritesSinceCheck.set(filePath, 0);
1069
+ const stat2 = await fsp3.stat(filePath);
1070
+ if (stat2.size <= this.eventMaxBytes) return;
1071
+ await this.compactEventTail(filePath, stat2.size);
1072
+ }
1073
+ async compactEventTail(filePath, size) {
1074
+ if (this.eventKeepBytes === 0) {
1075
+ await atomicWrite3(filePath, "", { mode: 384 });
1076
+ return;
1077
+ }
1078
+ const handle = await fsp3.open(filePath, "r");
1079
+ let retained;
1080
+ try {
1081
+ const length = Math.min(size, this.eventKeepBytes);
1082
+ const start = size - length;
1083
+ const buffer = Buffer.allocUnsafe(length);
1084
+ const { bytesRead } = await handle.read(buffer, 0, length, start);
1085
+ retained = buffer.subarray(0, bytesRead);
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);
1090
+ }
1091
+ } finally {
1092
+ await handle.close();
1093
+ }
1094
+ await atomicWrite3(filePath, retained, { mode: 384 });
1095
+ }
1096
+ async drainControlInternal(filePath) {
1097
+ try {
1098
+ const stat2 = await this.controlFileIO.stat(filePath);
1099
+ if (stat2.size === 0) return [];
1100
+ } catch {
1101
+ return [];
1102
+ }
1103
+ return withFileLock(filePath, async () => {
1104
+ let raw;
1105
+ try {
1106
+ const stat2 = await this.controlFileIO.stat(filePath);
1107
+ if (stat2.size === 0) return [];
1108
+ raw = await this.controlFileIO.readFile(filePath, "utf8");
1109
+ } catch {
1110
+ return [];
1111
+ }
1112
+ try {
1113
+ await this.controlFileIO.truncate(filePath, 0);
1114
+ } catch {
1115
+ return [];
1116
+ }
1117
+ return raw.split("\n").filter((line) => line.trim()).map((line) => {
1118
+ try {
1119
+ return JSON.parse(line);
1120
+ } catch {
1121
+ return null;
1122
+ }
1123
+ }).filter(
1124
+ (command) => command !== null
1125
+ );
1126
+ });
1127
+ }
945
1128
  async readIndex() {
1129
+ const signature = await this.indexSignature();
1130
+ if (this.cachedIndex && sameIndexSignature(signature, this.cachedIndexSignature)) {
1131
+ return this.cachedIndex;
1132
+ }
946
1133
  try {
947
1134
  const raw = await fsp3.readFile(this.indexPath, "utf8");
948
1135
  const parsed = JSON.parse(raw);
949
- if (parsed?.version === 1) return parsed;
1136
+ if (parsed?.version === 1) {
1137
+ parsed.entries.sort((a, b) => b.updatedAt - a.updatedAt);
1138
+ this.cachedIndex = parsed;
1139
+ this.cachedIndexSignature = signature;
1140
+ return parsed;
1141
+ }
950
1142
  } catch {
951
1143
  }
952
- return { version: 1, entries: [] };
1144
+ this.cachedIndex = { version: 1, entries: [] };
1145
+ this.cachedIndexSignature = signature;
1146
+ return this.cachedIndex;
953
1147
  }
954
1148
  async updateIndex(snapshot) {
955
- const index = await this.readIndex();
1149
+ const current = await this.readIndex();
1150
+ const index = {
1151
+ version: 1,
1152
+ entries: current.entries.map((entry2) => ({ ...entry2 }))
1153
+ };
956
1154
  const entry = {
957
1155
  runId: snapshot.runId,
958
1156
  specId: snapshot.specId,
@@ -965,17 +1163,37 @@ var SddBoardStore = class {
965
1163
  const idx = index.entries.findIndex((e) => e.runId === snapshot.runId);
966
1164
  if (idx >= 0) index.entries[idx] = entry;
967
1165
  else index.entries.push(entry);
1166
+ index.entries.sort((a, b) => b.updatedAt - a.updatedAt);
968
1167
  await atomicWrite3(this.indexPath, JSON.stringify(index, null, 2), { mode: 384 });
1168
+ this.cachedIndex = index;
1169
+ this.cachedIndexSignature = await this.indexSignature();
969
1170
  }
970
1171
  async removeFromIndex(runId) {
971
- const index = await this.readIndex();
972
- index.entries = index.entries.filter((e) => e.runId !== runId);
1172
+ const current = await this.readIndex();
1173
+ const index = {
1174
+ version: 1,
1175
+ entries: current.entries.filter((entry) => entry.runId !== runId).map((entry) => ({ ...entry }))
1176
+ };
973
1177
  await atomicWrite3(this.indexPath, JSON.stringify(index, null, 2), { mode: 384 });
1178
+ this.cachedIndex = index;
1179
+ this.cachedIndexSignature = await this.indexSignature();
1180
+ }
1181
+ async indexSignature() {
1182
+ try {
1183
+ const stat2 = await fsp3.stat(this.indexPath);
1184
+ return { size: stat2.size, mtimeMs: stat2.mtimeMs, ctimeMs: stat2.ctimeMs };
1185
+ } catch {
1186
+ return null;
1187
+ }
974
1188
  }
975
1189
  };
1190
+ function sameIndexSignature(a, b) {
1191
+ if (a === null || b === null) return a === b;
1192
+ return a.size === b.size && a.mtimeMs === b.mtimeMs && a.ctimeMs === b.ctimeMs;
1193
+ }
976
1194
 
977
1195
  // src/sdd-board-projector.ts
978
- import { DefaultSecretScrubber } from "@wrongstack/core";
1196
+ import { DefaultSecretScrubber } from "@wrongstack/core/security";
979
1197
  function summarizeToolInput(input, scrubber) {
980
1198
  if (!input || typeof input !== "object" || Array.isArray(input)) return void 0;
981
1199
  const record = input;
@@ -1017,11 +1235,12 @@ var SddBoardProjector = class _SddBoardProjector {
1017
1235
  mergedCommits = [];
1018
1236
  /** Base branch reported by the run at start (overrides the constructor option). */
1019
1237
  runBaseBranch;
1020
- dirty = false;
1021
1238
  timer = null;
1022
1239
  unsubs = [];
1023
- /** Tail of in-flight persistence, so callers can await a settled state. */
1024
- lastSave = Promise.resolve();
1240
+ /** Latest snapshot waiting behind an in-flight disk write. */
1241
+ pendingSnapshot;
1242
+ /** At most one persistence loop runs; intermediate snapshots are coalesced. */
1243
+ saveLoop;
1025
1244
  constructor(opts) {
1026
1245
  this.o = opts;
1027
1246
  this.now = opts.now ?? Date.now;
@@ -1044,7 +1263,11 @@ var SddBoardProjector = class _SddBoardProjector {
1044
1263
  });
1045
1264
  this.onRun("sdd.wave", (e) => {
1046
1265
  this.wave = e.wave;
1047
- 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
+ });
1048
1271
  this.markDirty();
1049
1272
  });
1050
1273
  this.onRun("sdd.deadlock", (e) => {
@@ -1052,7 +1275,11 @@ var SddBoardProjector = class _SddBoardProjector {
1052
1275
  blocked: this.shortId.get(c.blocked) ?? c.blocked.slice(0, 6),
1053
1276
  blockedBy: c.blockedBy.map((b) => this.shortId.get(b) ?? b.slice(0, 6))
1054
1277
  }));
1055
- 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
+ });
1056
1283
  this.markDirty();
1057
1284
  });
1058
1285
  this.onRun("sdd.task.started", (e) => {
@@ -1232,7 +1459,8 @@ var SddBoardProjector = class _SddBoardProjector {
1232
1459
  }
1233
1460
  pushFeed(entry) {
1234
1461
  this.feed.unshift(entry);
1235
- 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;
1236
1464
  if (entry.taskId) {
1237
1465
  const taskFeed = this.taskEvents.get(entry.taskId) ?? [];
1238
1466
  taskFeed.unshift(entry);
@@ -1257,7 +1485,7 @@ var SddBoardProjector = class _SddBoardProjector {
1257
1485
  }
1258
1486
  /** Resolve once all in-flight snapshot persistence has settled. */
1259
1487
  async drain() {
1260
- await this.lastSave;
1488
+ while (this.saveLoop) await this.saveLoop;
1261
1489
  }
1262
1490
  /** Stop projecting and release subscriptions. */
1263
1491
  dispose() {
@@ -1324,15 +1552,13 @@ var SddBoardProjector = class _SddBoardProjector {
1324
1552
  return snap;
1325
1553
  }
1326
1554
  markDirty() {
1327
- this.dirty = true;
1328
1555
  if (this.timer || this.finished) return;
1329
1556
  this.timer = setTimeout(() => {
1330
1557
  this.timer = null;
1331
- if (this.dirty) this.flush();
1558
+ this.flush();
1332
1559
  }, this.throttleMs);
1333
1560
  }
1334
1561
  flush() {
1335
- this.dirty = false;
1336
1562
  if (this.timer) {
1337
1563
  clearTimeout(this.timer);
1338
1564
  this.timer = null;
@@ -1345,8 +1571,23 @@ var SddBoardProjector = class _SddBoardProjector {
1345
1571
  snapshot: snap
1346
1572
  });
1347
1573
  if (this.o.store) {
1348
- const store = this.o.store;
1349
- this.lastSave = this.lastSave.then(() => store.saveSnapshot(snap)).catch(() => {
1574
+ this.pendingSnapshot = snap;
1575
+ this.startSaveLoop(this.o.store);
1576
+ }
1577
+ }
1578
+ startSaveLoop(store) {
1579
+ if (this.saveLoop) return;
1580
+ const loop = this.persistPendingSnapshots(store);
1581
+ this.saveLoop = loop;
1582
+ void loop.finally(() => {
1583
+ this.saveLoop = void 0;
1584
+ });
1585
+ }
1586
+ async persistPendingSnapshots(store) {
1587
+ while (this.pendingSnapshot) {
1588
+ const snapshot = this.pendingSnapshot;
1589
+ this.pendingSnapshot = void 0;
1590
+ await store.saveSnapshot(snapshot).catch(() => {
1350
1591
  });
1351
1592
  }
1352
1593
  }
@@ -1370,10 +1611,12 @@ var SddRunRegistry = class {
1370
1611
  }
1371
1612
  };
1372
1613
 
1614
+ // src/sdd-interview-driver.ts
1615
+ import { DefaultTaskStore as DefaultTaskStore2, TaskTracker as TaskTracker2 } from "@wrongstack/core/tasking";
1616
+
1373
1617
  // src/spec-builder.ts
1374
- import { expectDefined } from "@wrongstack/core";
1375
- import { toErrorMessage } from "@wrongstack/core";
1376
- import { SddError as SddError2, ERROR_CODES as ERROR_CODES2 } from "@wrongstack/core";
1618
+ import { ERROR_CODES as ERROR_CODES2, SddError as SddError2 } from "@wrongstack/core/types";
1619
+ import { expectDefined as expectDefined2, toErrorMessage } from "@wrongstack/core/utils";
1377
1620
  function buildQuestioningPrompt(session, min, max) {
1378
1621
  const answered = session.answers.length;
1379
1622
  const remaining = Math.max(0, min - answered);
@@ -1422,7 +1665,7 @@ function buildQuestioningPrompt(session, min, max) {
1422
1665
  if (answered > 0) {
1423
1666
  lines.push("", "**Conversation so far:**");
1424
1667
  for (let i = 0; i < answered; i++) {
1425
- const a = expectDefined(session.answers[i]);
1668
+ const a = expectDefined2(session.answers[i]);
1426
1669
  lines.push(``, `Q${i + 1}: ${a.question}`, `A${i + 1}: ${a.answer}`);
1427
1670
  }
1428
1671
  }
@@ -1595,7 +1838,7 @@ var AISpecBuilder = class {
1595
1838
  try {
1596
1839
  const fsp5 = await import("node:fs/promises");
1597
1840
  const path4 = await import("node:path");
1598
- const { atomicWrite: atomicWrite4 } = await import("@wrongstack/core");
1841
+ const { atomicWrite: atomicWrite4 } = await import("@wrongstack/core/utils");
1599
1842
  await fsp5.mkdir(path4.dirname(this.sessionPath), { recursive: true });
1600
1843
  await atomicWrite4(this.sessionPath, JSON.stringify(this.session, null, 2));
1601
1844
  } catch {
@@ -1625,17 +1868,9 @@ var AISpecBuilder = class {
1625
1868
  } catch {
1626
1869
  }
1627
1870
  }
1628
- /** Auto-save helper — calls saveSession() but never throws.
1629
- * Failures are surfaced via process.emitWarning so a persistent
1630
- * ENOSPC / EACCES doesn't silently strand session edits in memory. */
1871
+ /** Auto-save helper. saveSession() already handles best-effort persistence. */
1631
1872
  autoSave() {
1632
- this.saveSession().catch((err) => {
1633
- const detail = toErrorMessage(err);
1634
- process.emitWarning(
1635
- `SpecBuilder autoSave failed: ${detail}`,
1636
- "SpecBuilderWarning"
1637
- );
1638
- });
1873
+ void this.saveSession();
1639
1874
  }
1640
1875
  // ── Session Lifecycle ─────────────────────────────────────────────────────
1641
1876
  /** Start a new session with a title and optional intent. */
@@ -1809,7 +2044,7 @@ var AISpecBuilder = class {
1809
2044
  message: "Invalid JSON for spec",
1810
2045
  code: ERROR_CODES2.SDD_PARSE_FAILED,
1811
2046
  cause: e,
1812
- context: { detail: e instanceof Error ? e.message : "parse error" }
2047
+ context: { detail: toErrorMessage(e) }
1813
2048
  });
1814
2049
  }
1815
2050
  if (!parsed || typeof parsed !== "object") {
@@ -1821,7 +2056,7 @@ var AISpecBuilder = class {
1821
2056
  }
1822
2057
  const raw = parsed;
1823
2058
  const now = Date.now();
1824
- const title = String(raw.title ?? this.session.title ?? "Untitled");
2059
+ const title = String(raw.title ?? this.session.title);
1825
2060
  const overview = String(raw.overview ?? "");
1826
2061
  if (!overview || overview === "undefined") {
1827
2062
  throw new SddError2({
@@ -1832,7 +2067,15 @@ var AISpecBuilder = class {
1832
2067
  }
1833
2068
  const rawSections = Array.isArray(raw.sections) ? raw.sections : [];
1834
2069
  const sections = rawSections.filter((s) => s && typeof s === "object").map((s) => ({
1835
- 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",
1836
2079
  title: String(s.title ?? ""),
1837
2080
  content: String(s.content ?? ""),
1838
2081
  level: Number(s.level) || 1
@@ -1840,7 +2083,9 @@ var AISpecBuilder = class {
1840
2083
  const rawReqs = Array.isArray(raw.requirements) ? raw.requirements : [];
1841
2084
  const requirements = rawReqs.filter((r) => r && typeof r === "object").map((r, i) => ({
1842
2085
  id: String(r.id ?? `REQ-${i + 1}`),
1843
- 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",
1844
2089
  priority: ["critical", "high", "medium", "low"].includes(String(r.priority)) ? String(r.priority) : "medium",
1845
2090
  description: String(r.description ?? ""),
1846
2091
  acceptanceCriteria: Array.isArray(r.acceptanceCriteria) ? r.acceptanceCriteria.map(String) : []
@@ -1929,7 +2174,6 @@ var AISpecBuilder = class {
1929
2174
  };
1930
2175
 
1931
2176
  // src/sdd-interview-driver.ts
1932
- import { TaskTracker as TaskTracker2, DefaultTaskStore as DefaultTaskStore2 } from "@wrongstack/core/tasking";
1933
2177
  var SddInterviewDriver = class {
1934
2178
  builder;
1935
2179
  o;
@@ -2142,16 +2386,17 @@ var SddInterviewDriver = class {
2142
2386
  );
2143
2387
  if (valid.length === 0) return void 0;
2144
2388
  const spec = this.builder.getSession().spec;
2145
- if (!spec) return void 0;
2146
- if (!this.tracker || !this.graph) {
2147
- const tracker = new TaskTracker2({ store: new DefaultTaskStore2() });
2148
- this.graph = await tracker.createGraph(spec.id, spec.title);
2149
- 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;
2150
2393
  }
2394
+ const tracker = this.tracker;
2395
+ const graph = this.graph;
2151
2396
  const refMap = /* @__PURE__ */ new Map();
2152
2397
  const created = [];
2153
2398
  valid.forEach((task, i) => {
2154
- const node = addTaskToTracker(this.tracker, task);
2399
+ const node = addTaskToTracker(tracker, task);
2155
2400
  created.push({ nodeId: node.id, task });
2156
2401
  if (typeof task.id === "string" && task.id.trim()) {
2157
2402
  refMap.set(task.id.trim().toLowerCase(), node.id);
@@ -2164,13 +2409,13 @@ var SddInterviewDriver = class {
2164
2409
  const deps = Array.isArray(task.dependsOn) ? task.dependsOn : [];
2165
2410
  for (const ref of deps) {
2166
2411
  const depId = refMap.get(normalizeTaskRef(String(ref)));
2167
- if (depId && depId !== nodeId) this.tracker.addDependency(depId, nodeId);
2412
+ if (depId && depId !== nodeId) tracker.addDependency(depId, nodeId);
2168
2413
  }
2169
2414
  }
2170
- await this.persistGraph(this.graph);
2171
- this.builder.setTaskGraphId(this.graph.id);
2415
+ await this.persistGraph(graph);
2416
+ this.builder.setTaskGraphId(graph.id);
2172
2417
  await this.builder.saveSession();
2173
- return this.graph.id;
2418
+ return graph.id;
2174
2419
  }
2175
2420
  };
2176
2421
  var TASK_TYPES = ["feature", "bugfix", "refactor", "docs", "test", "chore"];
@@ -2195,14 +2440,51 @@ function isExplanatoryText(text) {
2195
2440
  }
2196
2441
 
2197
2442
  // src/start-sdd-run.ts
2198
- import { TOKENS } from "@wrongstack/core";
2443
+ import { TOKENS } from "@wrongstack/core/kernel";
2199
2444
 
2200
2445
  // src/sdd-parallel-run.ts
2201
- import { expectDefined as expectDefined2 } from "@wrongstack/core";
2446
+ import { expectDefined as expectDefined3 } from "@wrongstack/core/utils";
2202
2447
  import { randomUUID as randomUUID2 } from "node:crypto";
2203
- import { makeAgentSubagentRunner, withDisabledToolFiltering, DefaultMultiAgentCoordinator } from "@wrongstack/core/coordination";
2204
- import { assignNickname } from "@wrongstack/core";
2205
- import { SddError as SddError3, ERROR_CODES as ERROR_CODES3 } from "@wrongstack/core";
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
+ }
2206
2488
 
2207
2489
  // src/sdd-task-decomposer.ts
2208
2490
  var SddTaskDecomposer = class {
@@ -2326,7 +2608,9 @@ var SddParallelRun = class {
2326
2608
  this.maxTotalWaves = opts.maxTotalWaves ?? opts.graph.nodes.size * (this.maxRetries + 2) + 10;
2327
2609
  this.maxWallClockMs = opts.maxWallClockMs;
2328
2610
  this.maxRecoveryRounds = Math.max(0, opts.maxRecoveryRounds ?? 0);
2329
- 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
+ });
2330
2614
  }
2331
2615
  opts;
2332
2616
  slots;
@@ -2449,7 +2733,8 @@ var SddParallelRun = class {
2449
2733
  * revert outcome; a dirty tree or revert conflict surfaces as `ok:false`.
2450
2734
  */
2451
2735
  async rollback() {
2452
- 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" };
2453
2738
  const wt = this.opts.worktrees;
2454
2739
  if (!wt || !this.baseBranch) {
2455
2740
  return { ok: false, reverted: 0, reason: "no worktree run to roll back" };
@@ -2482,7 +2767,10 @@ var SddParallelRun = class {
2482
2767
  */
2483
2768
  setTaskModel(taskId, model, provider) {
2484
2769
  if (!this.opts.tracker.getNode(taskId)) return false;
2485
- this.opts.tracker.patchMetadata(taskId, { model, ...provider !== void 0 ? { provider } : {} });
2770
+ this.opts.tracker.patchMetadata(taskId, {
2771
+ model,
2772
+ ...provider !== void 0 ? { provider } : {}
2773
+ });
2486
2774
  return true;
2487
2775
  }
2488
2776
  /** Set/override a task's fallback model chain (applied on its next dispatch). */
@@ -2514,7 +2802,12 @@ var SddParallelRun = class {
2514
2802
  this.cancelledTasks.add(taskId);
2515
2803
  this.opts.tracker.patchMetadata(taskId, { cancelled: true });
2516
2804
  this.opts.tracker.updateNodeStatus(taskId, "failed", "cancelled by user");
2517
- 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
+ });
2518
2811
  const subagentId = this.taskSubagents.get(taskId);
2519
2812
  if (subagentId && this.coordinator) {
2520
2813
  await this.coordinator.stop(subagentId).catch(() => {
@@ -2545,30 +2838,12 @@ var SddParallelRun = class {
2545
2838
  * The scheduler picks the new pending leaves up on its next dispatch pass.
2546
2839
  */
2547
2840
  splitTask(taskId, subtasks) {
2548
- const tracker = this.opts.tracker;
2549
- const node = tracker.getNode(taskId);
2550
- if (!node) return [];
2551
- if (node.status === "in_progress" || this.taskSubagents.has(taskId)) return [];
2552
- if (!subtasks.length) return [];
2553
- const blockers = tracker.getBlockers(taskId);
2554
- const dependents = tracker.getDependents(taskId);
2555
- const leafIds = subtasks.map(
2556
- (s) => tracker.addNode({
2557
- title: s.title,
2558
- description: s.description,
2559
- type: s.type ?? node.type,
2560
- priority: s.priority ?? node.priority,
2561
- status: "pending",
2562
- parentId: taskId
2563
- }).id
2564
- );
2565
- for (const leaf of leafIds) {
2566
- for (const b of blockers) tracker.addDependency(b, leaf);
2567
- for (const dep of dependents) tracker.addDependency(leaf, dep);
2568
- }
2841
+ const leafIds = splitGraphNode(this.opts.tracker, taskId, subtasks, {
2842
+ isRunning: (id) => this.taskSubagents.has(id)
2843
+ });
2844
+ if (!leafIds.length) return [];
2569
2845
  this.retryMap.delete(taskId);
2570
2846
  this.persistRetries(taskId, 0);
2571
- tracker.updateNodeStatus(taskId, "completed", `split into ${leafIds.length} subtasks`);
2572
2847
  this.emit("sdd.task.split", { runId: this.runId, taskId, subtaskIds: leafIds });
2573
2848
  return leafIds;
2574
2849
  }
@@ -2616,7 +2891,12 @@ var SddParallelRun = class {
2616
2891
  return await this.executeOne(task);
2617
2892
  } catch (err) {
2618
2893
  this.opts.tracker.updateNodeStatus(task.id, "failed", `dispatch error: ${String(err)}`);
2619
- 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
+ });
2620
2900
  return { taskId: task.id, success: false };
2621
2901
  } finally {
2622
2902
  running.delete(task.id);
@@ -2630,16 +2910,18 @@ var SddParallelRun = class {
2630
2910
  await this.waitWhilePaused();
2631
2911
  if (this.stopRequested) break;
2632
2912
  let dispatchedThisRound = 0;
2633
- if (running.size < this.slots) {
2634
- const ready = this.decomposer.readyNodes().filter((t) => !running.has(t.id));
2635
- for (const task of ready) {
2636
- if (running.size >= this.slots) break;
2637
- dispatch(task);
2638
- dispatchedThisRound++;
2639
- }
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++;
2640
2918
  }
2641
2919
  if (dispatchedThisRound > 0) {
2642
- 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
+ });
2643
2925
  this.round++;
2644
2926
  }
2645
2927
  if (running.size === 0) {
@@ -2670,7 +2952,6 @@ var SddParallelRun = class {
2670
2952
  this.opts.onProgress?.(this.buildProgress());
2671
2953
  }
2672
2954
  }
2673
- if (running.size > 0) await Promise.allSettled(running.values());
2674
2955
  if (this.stopRequested) await this.teardown();
2675
2956
  const finalProgress = this.opts.tracker.getProgress();
2676
2957
  this.emit("sdd.run.finished", {
@@ -2868,10 +3149,11 @@ var SddParallelRun = class {
2868
3149
  }
2869
3150
  this.opts.tracker.updateNodeStatus(taskId, "in_progress");
2870
3151
  await this.allocateWorktrees([task]);
2871
- if (!this.coordinator) throw new SddError3({
2872
- message: "SDD parallel runner requires a coordinator",
2873
- code: ERROR_CODES3.SDD_INVALID_STATE
2874
- });
3152
+ if (!this.coordinator)
3153
+ throw new SddError3({
3154
+ message: "SDD parallel runner requires a coordinator",
3155
+ code: ERROR_CODES3.SDD_INVALID_STATE
3156
+ });
2875
3157
  const coordinator = this.coordinator;
2876
3158
  const subagentId = `sdd-d${this.dispatchSeq++}`;
2877
3159
  const correlationId = randomUUID2();
@@ -2881,7 +3163,7 @@ var SddParallelRun = class {
2881
3163
  const fallbackModels = Array.isArray(meta.fallbackModels) ? meta.fallbackModels : this.opts.fallbackModels;
2882
3164
  const spawnResult = await coordinator.spawn({
2883
3165
  id: subagentId,
2884
- name: agentName ?? subagentId,
3166
+ name: agentName,
2885
3167
  role: "executor",
2886
3168
  // Idle reaper is always on; the hard wall-clock cap only when opted in.
2887
3169
  idleTimeoutMs: this.idleTimeoutMs,
@@ -2903,7 +3185,7 @@ var SddParallelRun = class {
2903
3185
  runId: this.runId,
2904
3186
  taskId,
2905
3187
  subagentId,
2906
- agentName: agentName ?? "",
3188
+ agentName,
2907
3189
  worktreeBranch: this.taskBranches.get(taskId)
2908
3190
  });
2909
3191
  const directivePreamble = [
@@ -2938,7 +3220,7 @@ var SddParallelRun = class {
2938
3220
  let result;
2939
3221
  try {
2940
3222
  const got = await coordinator.awaitTasks([correlationId]);
2941
- result = expectDefined2(got[0]);
3223
+ result = expectDefined3(got[0]);
2942
3224
  } catch (err) {
2943
3225
  result = {
2944
3226
  subagentId,
@@ -2966,12 +3248,22 @@ var SddParallelRun = class {
2966
3248
  } catch (err) {
2967
3249
  verificationFailReason = `verification error: ${String(err)}`;
2968
3250
  }
3251
+ const hadVerifiable = typeof task.metadata?.["verificationCommand"] === "string" || task.description.includes("**Acceptance Criteria:**");
2969
3252
  if (verificationFailReason) {
3253
+ this.opts.tracker.patchMetadata(taskId, {
3254
+ verificationState: "failed",
3255
+ verificationDetail: verificationFailReason
3256
+ });
2970
3257
  this.emit("sdd.task.verification_failed", {
2971
3258
  runId: this.runId,
2972
3259
  taskId,
2973
3260
  reason: verificationFailReason
2974
3261
  });
3262
+ } else if (hadVerifiable) {
3263
+ this.opts.tracker.patchMetadata(taskId, {
3264
+ verificationState: "passed",
3265
+ verificationDetail: void 0
3266
+ });
2975
3267
  }
2976
3268
  }
2977
3269
  let success = false;
@@ -2996,12 +3288,13 @@ var SddParallelRun = class {
2996
3288
  });
2997
3289
  await this.applyTaskFailure(taskId, subagentId, merged.reason);
2998
3290
  } else {
3291
+ const conflictFiles = merged.conflictFiles ?? [];
2999
3292
  this.emit("sdd.task.conflict", {
3000
3293
  runId: this.runId,
3001
3294
  taskId,
3002
- conflictFiles: merged.conflictFiles ?? []
3295
+ conflictFiles
3003
3296
  });
3004
- const reason = `merge conflict${merged.conflictFiles?.length ? `: ${merged.conflictFiles.join(", ")}` : ""}`;
3297
+ const reason = `merge conflict${conflictFiles.length ? `: ${conflictFiles.join(", ")}` : ""}`;
3005
3298
  await this.applyTaskFailure(taskId, subagentId, reason);
3006
3299
  }
3007
3300
  } else {
@@ -3104,7 +3397,11 @@ var SddParallelRun = class {
3104
3397
  const res = await wt.merge(handle, {
3105
3398
  squash: true,
3106
3399
  ...this.opts.conflictResolver ? {
3107
- 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
+ })
3108
3405
  } : {}
3109
3406
  });
3110
3407
  if (res.ok) {
@@ -3116,7 +3413,8 @@ var SddParallelRun = class {
3116
3413
  result: result ?? {},
3117
3414
  cwd: this.opts.projectRoot
3118
3415
  });
3119
- if (!verdict.ok) regressed = verdict.reason ?? "verification failed after conflict resolution";
3416
+ if (!verdict.ok)
3417
+ regressed = verdict.reason ?? "verification failed after conflict resolution";
3120
3418
  } catch (err) {
3121
3419
  regressed = `verification error after conflict resolution: ${String(err)}`;
3122
3420
  }
@@ -3232,6 +3530,32 @@ var SddParallelRun = class {
3232
3530
  };
3233
3531
 
3234
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
+ }
3235
3559
  function startSddRun(opts) {
3236
3560
  SddParallelRun.resetOrphans(opts.tracker);
3237
3561
  const run = new SddParallelRun({
@@ -3295,25 +3619,7 @@ function startSddRun(opts) {
3295
3619
  const controlTimer = setInterval(() => {
3296
3620
  void opts.boardStore.drainControl(run.runId).then((cmds) => {
3297
3621
  for (const c of cmds) {
3298
- const p = c.payload ?? {};
3299
- if (c.type === "pause") run.pause();
3300
- else if (c.type === "resume") run.resume();
3301
- else if (c.type === "stop") run.stop();
3302
- else if (c.type === "retry" && p.taskId) run.retryTask(p.taskId);
3303
- else if (c.type === "retry_all_failed") run.retryAllFailed();
3304
- else if (c.type === "reassign" && p.taskId) run.reassignTask(p.taskId, p.agentName ?? "");
3305
- else if (c.type === "set_task_model" && p.taskId) run.setTaskModel(p.taskId, p.model, p.provider);
3306
- else if (c.type === "set_task_fallbacks" && p.taskId) run.setTaskFallbacks(p.taskId, p.fallbackModels);
3307
- else if (c.type === "set_task_verification" && p.taskId)
3308
- run.setTaskVerification(p.taskId, p.verificationCommand);
3309
- else if (c.type === "cancel_task" && p.taskId) void run.cancelTask(p.taskId).catch(() => {
3310
- });
3311
- else if (c.type === "delete_task" && p.taskId) run.deleteTask(p.taskId);
3312
- else if (c.type === "split_task" && p.taskId && p.subtasks?.length) run.splitTask(p.taskId, p.subtasks);
3313
- else if (c.type === "cleanup_worktrees") void run.cleanupWorktrees().catch(() => {
3314
- });
3315
- else if (c.type === "rollback") void run.rollback().catch(() => {
3316
- });
3622
+ applySddControlCommand(run, c);
3317
3623
  }
3318
3624
  }).catch(() => {
3319
3625
  });
@@ -3341,7 +3647,8 @@ function startSddRun(opts) {
3341
3647
 
3342
3648
  // src/sdd-lifecycle.ts
3343
3649
  import * as fsp4 from "node:fs/promises";
3344
- import { WorktreeManager } from "@wrongstack/core";
3650
+ import { toErrorMessage as toErrorMessage2 } from "@wrongstack/core/utils";
3651
+ import { WorktreeManager } from "@wrongstack/core/worktree";
3345
3652
  async function cleanupSddWorktrees(projectRoot) {
3346
3653
  const wt = new WorktreeManager({ projectRoot });
3347
3654
  return wt.cleanupAllManaged();
@@ -3352,19 +3659,21 @@ async function cleanupStaleWorktrees(projectRoot) {
3352
3659
  }
3353
3660
  async function cleanupStaleSddWorktrees(opts) {
3354
3661
  const now = opts.now?.() ?? Date.now();
3355
- try {
3356
- const store = new SddBoardStore({ baseDir: opts.boardsDir });
3357
- const latest = (await store.list())[0];
3358
- if (latest) {
3359
- const age = now - latest.updatedAt;
3360
- if (latest.status === "running" && age < (opts.runningLiveMs ?? 12e4)) {
3361
- return { swept: false, removed: 0, detected: 0, skippedReason: "a run appears live (running)" };
3362
- }
3363
- if (latest.status === "paused" && age < (opts.pausedLiveMs ?? 18e5)) {
3364
- return { swept: false, removed: 0, detected: 0, skippedReason: "a run is paused" };
3365
- }
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" };
3366
3676
  }
3367
- } catch {
3368
3677
  }
3369
3678
  try {
3370
3679
  const wt = new WorktreeManager({ projectRoot: opts.projectRoot });
@@ -3381,7 +3690,11 @@ async function rollbackSddRunFromDisk(opts) {
3381
3690
  const snap = await store.load(runId);
3382
3691
  if (!snap) return { ok: false, reverted: 0, reason: `board "${runId}" not found` };
3383
3692
  if (!snap.baseBranch) {
3384
- 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
+ };
3385
3698
  }
3386
3699
  const shas = (snap.mergedCommits ?? []).map((c) => c.sha);
3387
3700
  if (shas.length === 0) {
@@ -3399,7 +3712,7 @@ async function destroySddProject(opts) {
3399
3712
  projectRoot: opts.projectRoot,
3400
3713
  boardsDir: opts.paths.projectSddBoards,
3401
3714
  runId: opts.runId
3402
- }).catch((err) => ({ ok: false, reverted: 0, reason: toReason(err) }));
3715
+ }).catch((err) => ({ ok: false, reverted: 0, reason: toErrorMessage2(err) }));
3403
3716
  reverted = r.reverted;
3404
3717
  revertOk = r.ok;
3405
3718
  revertReason = r.reason;
@@ -3426,9 +3739,6 @@ async function destroySddProject(opts) {
3426
3739
  await rmDir(opts.paths.projectSddBoards, "boards");
3427
3740
  return { worktreesRemoved: removed, deleted, reverted, revertOk, revertReason };
3428
3741
  }
3429
- function toReason(err) {
3430
- return err instanceof Error ? err.message : String(err);
3431
- }
3432
3742
  async function applySddLifecycle(op, opts) {
3433
3743
  try {
3434
3744
  if (op === "cleanup_worktrees") {
@@ -3460,7 +3770,7 @@ async function applySddLifecycle(op, opts) {
3460
3770
  reason: r.revertOk === false ? r.revertReason : void 0
3461
3771
  };
3462
3772
  } catch (err) {
3463
- return { op, ok: false, reason: toReason(err) };
3773
+ return { op, ok: false, reason: toErrorMessage2(err) };
3464
3774
  }
3465
3775
  }
3466
3776
 
@@ -3581,8 +3891,8 @@ function templateToMarkdown(template, title) {
3581
3891
  }
3582
3892
 
3583
3893
  // src/task-visualizer.ts
3584
- import { computeTaskProgress as computeTaskProgress2 } from "@wrongstack/core/types";
3585
- import { truncate } from "@wrongstack/core";
3894
+ import { computeTaskProgress as computeTaskProgress2 } from "@wrongstack/core/tasking";
3895
+ import { expectDefined as expectDefined4, truncate } from "@wrongstack/core/utils";
3586
3896
  var STATUS_ICON = {
3587
3897
  pending: "\u25CB",
3588
3898
  in_progress: "\u25D0",
@@ -3609,7 +3919,9 @@ function renderTaskGraph(graph, opts) {
3609
3919
  const lines = [];
3610
3920
  const compact = opts?.compact ?? false;
3611
3921
  lines.push(`\u256D\u2500 Task Graph: ${graph.title} \u2500\u256E`);
3612
- 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
+ );
3613
3925
  lines.push("\u2570" + "\u2500".repeat(Math.max(50, graph.title.length + 30)) + "\u256F");
3614
3926
  lines.push("");
3615
3927
  const progress = computeTaskProgress2(graph);
@@ -3644,8 +3956,7 @@ function renderTaskGraph(graph, opts) {
3644
3956
  function renderNode(graph, nodeId, lines, rendered, childrenMap, compact, prefix) {
3645
3957
  if (rendered.has(nodeId)) return;
3646
3958
  rendered.add(nodeId);
3647
- const node = graph.nodes.get(nodeId);
3648
- if (!node) return;
3959
+ const node = expectDefined4(graph.nodes.get(nodeId));
3649
3960
  const icon = STATUS_ICON[node.status];
3650
3961
  const prioIcon = PRIORITY_ICON[node.priority];
3651
3962
  const typeIcon = TYPE_ICON[node.type];
@@ -3686,7 +3997,7 @@ function renderTaskList(graph) {
3686
3997
  completed: []
3687
3998
  };
3688
3999
  for (const node of nodes) {
3689
- groups[node.status]?.push(node);
4000
+ groups[node.status].push(node);
3690
4001
  }
3691
4002
  for (const [status, group] of Object.entries(groups)) {
3692
4003
  if (group.length === 0) continue;
@@ -3734,8 +4045,8 @@ function renderSpecAnalysis(spec, analysis) {
3734
4045
  }
3735
4046
 
3736
4047
  // src/critical-path.ts
3737
- import { expectDefined as expectDefined3 } from "@wrongstack/core";
3738
- import { topologicalSort } from "@wrongstack/core/types";
4048
+ import { topologicalSort } from "@wrongstack/core/tasking";
4049
+ import { expectDefined as expectDefined5 } from "@wrongstack/core/utils";
3739
4050
  function analyzeCriticalPath(graph) {
3740
4051
  const nodes = Array.from(graph.nodes.values());
3741
4052
  const topoOrder = topologicalSort(graph);
@@ -3789,8 +4100,7 @@ function analyzeCriticalPath(graph) {
3789
4100
  bottlenecks.sort((a, b) => b.severity - a.severity);
3790
4101
  const criticalPath = computeCriticalPath(graph, topoOrder, blockedByMap);
3791
4102
  const totalHours = criticalPath.reduce((sum, id) => {
3792
- const n = graph.nodes.get(id);
3793
- return sum + (n?.estimateHours ?? 0);
4103
+ return sum + (graph.nodes.get(id).estimateHours ?? 0);
3794
4104
  }, 0);
3795
4105
  const parallelGroups = computeParallelGroups(graph, blockedByMap);
3796
4106
  const executionOrder = topoOrder.filter((id) => {
@@ -3811,7 +4121,7 @@ function getTransitiveBlocked(_graph, taskId, blocksMap) {
3811
4121
  const visited = /* @__PURE__ */ new Set();
3812
4122
  const queue = [taskId];
3813
4123
  while (queue.length > 0) {
3814
- const current = expectDefined3(queue.shift());
4124
+ const current = expectDefined5(queue.shift());
3815
4125
  const blocked = blocksMap.get(current);
3816
4126
  if (!blocked) continue;
3817
4127
  for (const id of blocked) {
@@ -3846,7 +4156,7 @@ function computeCriticalPath(graph, _topoOrder, blockedByMap) {
3846
4156
  const blocked = blocksMap.get(id);
3847
4157
  if (!blocked) continue;
3848
4158
  for (const blockedId of blocked) {
3849
- const candidateDist = (dist.get(id) ?? 0) + (graph.nodes.get(blockedId)?.estimateHours ?? 1);
4159
+ const candidateDist = dist.get(id) + (graph.nodes.get(blockedId)?.estimateHours ?? 1);
3850
4160
  if (candidateDist > (dist.get(blockedId) ?? 0)) {
3851
4161
  dist.set(blockedId, candidateDist);
3852
4162
  prev.set(blockedId, id);
@@ -3857,9 +4167,9 @@ function computeCriticalPath(graph, _topoOrder, blockedByMap) {
3857
4167
  if (!changed) break;
3858
4168
  }
3859
4169
  let maxDist = 0;
3860
- let maxId = expectDefined3(allIds[0]);
4170
+ let maxId = expectDefined5(allIds[0]);
3861
4171
  for (const id of allIds) {
3862
- const d = dist.get(id) ?? 0;
4172
+ const d = dist.get(id);
3863
4173
  if (d > maxDist) {
3864
4174
  maxDist = d;
3865
4175
  maxId = id;
@@ -3894,8 +4204,7 @@ function computeParallelGroups(graph, blockedByMap) {
3894
4204
  }
3895
4205
  }
3896
4206
  if (group.length === 0) {
3897
- const first = Array.from(remaining)[0];
3898
- if (first) group.push(first);
4207
+ group.push(expectDefined5(Array.from(remaining)[0]));
3899
4208
  }
3900
4209
  for (const id of group) {
3901
4210
  assigned.add(id);
@@ -3907,7 +4216,7 @@ function computeParallelGroups(graph, blockedByMap) {
3907
4216
  }
3908
4217
 
3909
4218
  // src/spec-versioning.ts
3910
- import { assertNever } from "@wrongstack/core";
4219
+ import { assertNever } from "@wrongstack/core/utils";
3911
4220
  var SpecVersioning = class {
3912
4221
  versions = /* @__PURE__ */ new Map();
3913
4222
  /** Record a new version of a spec. */
@@ -4109,7 +4418,6 @@ var AutoExecutor = class {
4109
4418
  for (let i = 0; i < results.length; i++) {
4110
4419
  const result = results[i];
4111
4420
  const task = batch[i];
4112
- if (!result || !task) continue;
4113
4421
  if (result.status === "fulfilled") {
4114
4422
  const { result: execResult, retries } = result.value;
4115
4423
  if (execResult.success) {
@@ -4159,14 +4467,14 @@ var AutoExecutor = class {
4159
4467
  }
4160
4468
  }
4161
4469
  const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
4162
- ready.sort((a, b) => (priorityOrder[a.priority] ?? 4) - (priorityOrder[b.priority] ?? 4));
4470
+ ready.sort((a, b) => priorityOrder[a.priority] - priorityOrder[b.priority]);
4163
4471
  return ready;
4164
4472
  }
4165
4473
  /** Execute a single task with retry logic. */
4166
4474
  async executeTaskWithRetry(task, graph, spec) {
4167
4475
  const maxRetries = this.opts.maxRetries ?? 2;
4168
4476
  let retryCount = this.retryMap.get(task.id) ?? 0;
4169
- while (retryCount <= maxRetries) {
4477
+ while (true) {
4170
4478
  this.opts.tracker.updateNodeStatus(task.id, "in_progress");
4171
4479
  this.opts.onTaskStart?.(task);
4172
4480
  const dependencies = this.getTaskDependencies(task.id, graph);
@@ -4210,7 +4518,6 @@ var AutoExecutor = class {
4210
4518
  };
4211
4519
  }
4212
4520
  }
4213
- return { result: { success: false, error: "Max retries exceeded" }, retries: retryCount };
4214
4521
  }
4215
4522
  /** Get tasks that this task depends on. */
4216
4523
  getTaskDependencies(taskId, graph) {
@@ -4243,7 +4550,7 @@ function createAutoExecutor(opts) {
4243
4550
  }
4244
4551
 
4245
4552
  // src/sdd-supervisor.ts
4246
- import { parseModelRef } from "@wrongstack/core";
4553
+ import { parseModelRef } from "@wrongstack/core/agent";
4247
4554
  var SddSupervisor = class {
4248
4555
  constructor(opts) {
4249
4556
  this.opts = opts;
@@ -4297,6 +4604,54 @@ Supervisor rescues already used: ${attempts}`,
4297
4604
 
4298
4605
  // src/verify-task.ts
4299
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
+ }
4300
4655
  function makeCommandVerifier(options = {}) {
4301
4656
  const metadataKey = options.metadataKey ?? "verificationCommand";
4302
4657
  const timeoutMs = options.timeoutMs ?? 18e4;
@@ -4304,8 +4659,7 @@ function makeCommandVerifier(options = {}) {
4304
4659
  const cmd = info.task.metadata?.[metadataKey];
4305
4660
  if (typeof cmd !== "string" || !cmd.trim()) return { ok: true };
4306
4661
  return await new Promise((resolve) => {
4307
- const isWindows = process.platform === "win32";
4308
- const [shell, ...shellArgs] = isWindows ? ["cmd", "/d", "/c"] : ["sh", "-c"];
4662
+ const [shell, ...shellArgs] = verificationShell(process.platform);
4309
4663
  const child = spawn(shell, [...shellArgs, cmd], {
4310
4664
  cwd: info.cwd,
4311
4665
  shell: false,
@@ -4327,7 +4681,6 @@ function makeCommandVerifier(options = {}) {
4327
4681
  });
4328
4682
  child.on("error", (err) => {
4329
4683
  clearTimeout(timer);
4330
- if (timedOut) return;
4331
4684
  resolve({ ok: false, reason: `verification spawn error: ${String(err)}` });
4332
4685
  });
4333
4686
  });
@@ -4335,10 +4688,7 @@ function makeCommandVerifier(options = {}) {
4335
4688
  }
4336
4689
 
4337
4690
  // src/decompose-task.ts
4338
- import {
4339
- readBundledInstructionText,
4340
- renderInstructionTemplate
4341
- } from "@wrongstack/core";
4691
+ import { readBundledInstructionText, renderInstructionTemplate } from "@wrongstack/core/utils";
4342
4692
  var TASK_TYPES2 = /* @__PURE__ */ new Set(["feature", "bugfix", "refactor", "docs", "test", "chore"]);
4343
4693
  var PRIORITIES = /* @__PURE__ */ new Set(["critical", "high", "medium", "low"]);
4344
4694
  function extractJsonArray(text) {
@@ -4362,48 +4712,144 @@ function buildPrompt(task, error, min, max) {
4362
4712
  error: error || "(none recorded)"
4363
4713
  });
4364
4714
  }
4365
- 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) {
4366
4741
  const min = Math.max(2, opts.minSubtasks ?? 2);
4367
- const max = Math.max(min, opts.maxSubtasks ?? 4);
4368
- return async function generateSubtasks(info) {
4742
+ const max = Math.max(min, opts.maxSubtasks ?? 5);
4743
+ return async function decompose(info) {
4369
4744
  let text;
4370
4745
  try {
4371
- 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
+ );
4372
4755
  } catch {
4373
4756
  return [];
4374
4757
  }
4375
- const json = extractJsonArray(text ?? "");
4376
- if (!json) return [];
4377
- 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;
4378
4766
  try {
4379
- raw = JSON.parse(json);
4767
+ text = await opts.run(buildPrompt(info.task, info.error, min, max));
4380
4768
  } catch {
4381
4769
  return [];
4382
4770
  }
4383
- if (!Array.isArray(raw)) return [];
4384
- const specs = [];
4385
- for (const item of raw) {
4386
- if (!item || typeof item !== "object") continue;
4387
- const r = item;
4388
- const title = typeof r["title"] === "string" ? r["title"].trim() : "";
4389
- const description = typeof r["description"] === "string" ? r["description"].trim() : "";
4390
- if (!title || !description) continue;
4391
- const type = TASK_TYPES2.has(r["type"]) ? r["type"] : void 0;
4392
- const priority = PRIORITIES.has(r["priority"]) ? r["priority"] : void 0;
4393
- specs.push({ title, description, type, priority });
4394
- if (specs.length >= max) break;
4395
- }
4396
- return specs.length >= min ? specs : [];
4771
+ return parseSubtaskSpecs(text, min, max);
4397
4772
  };
4398
4773
  }
4399
4774
 
4400
- // src/conflict-resolver.ts
4401
- import { readFile as readFile4, writeFile as writeFile2 } from "node:fs/promises";
4402
- import { join as join4, isAbsolute } from "node:path";
4775
+ // src/plan-decompose.ts
4403
4776
  import {
4404
- readBundledInstructionText as readBundledInstructionText2,
4405
- renderInstructionTemplate as renderInstructionTemplate2
4406
- } from "@wrongstack/core";
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
+
4843
+ // src/conflict-resolver.ts
4844
+ import { readFile as readFile4, writeFile } from "node:fs/promises";
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
+ };
4407
4853
  var START = "<<<<<<<";
4408
4854
  var BASE = "|||||||";
4409
4855
  var SEP = "=======";
@@ -4441,21 +4887,21 @@ function hasConflictMarkers(text) {
4441
4887
  return m === START || m === SEP || m === END || m === BASE;
4442
4888
  });
4443
4889
  }
4444
- function makePreferSideConflictResolver(side) {
4890
+ function makePreferSideConflictResolver(side, io = defaultFileIO) {
4445
4891
  return async function conflictResolver(info) {
4446
4892
  if (info.conflictFiles.length === 0) return false;
4447
4893
  for (const rel of info.conflictFiles) {
4448
4894
  const abs = isAbsolute(rel) ? rel : join4(info.cwd, rel);
4449
4895
  let content;
4450
4896
  try {
4451
- content = await readFile4(abs, "utf8");
4897
+ content = await io.read(abs);
4452
4898
  } catch {
4453
4899
  return false;
4454
4900
  }
4455
4901
  const resolved = resolveConflictText(content, side);
4456
4902
  if (hasConflictMarkers(resolved)) return false;
4457
4903
  try {
4458
- await writeFile2(abs, resolved, "utf8");
4904
+ await io.write(abs, resolved);
4459
4905
  } catch {
4460
4906
  return false;
4461
4907
  }
@@ -4475,13 +4921,14 @@ function nonMarkerLineCount(text) {
4475
4921
  }
4476
4922
  function makeLlmConflictResolver(opts) {
4477
4923
  const minFraction = opts.minRetainedFraction ?? 0.5;
4924
+ const io = opts.io ?? defaultFileIO;
4478
4925
  return async function conflictResolver(info) {
4479
4926
  if (info.conflictFiles.length === 0) return false;
4480
4927
  for (const rel of info.conflictFiles) {
4481
4928
  const abs = isAbsolute(rel) ? rel : join4(info.cwd, rel);
4482
4929
  let content;
4483
4930
  try {
4484
- content = await readFile4(abs, "utf8");
4931
+ content = await io.read(abs);
4485
4932
  } catch {
4486
4933
  return false;
4487
4934
  }
@@ -4505,7 +4952,7 @@ function makeLlmConflictResolver(opts) {
4505
4952
  return false;
4506
4953
  }
4507
4954
  try {
4508
- await writeFile2(abs, resolved, "utf8");
4955
+ await io.write(abs, resolved);
4509
4956
  } catch {
4510
4957
  return false;
4511
4958
  }
@@ -4535,21 +4982,27 @@ export {
4535
4982
  TaskTracker3 as TaskTracker,
4536
4983
  analyzeCriticalPath,
4537
4984
  applySddLifecycle,
4985
+ assessGeneratedTaskAtomicity,
4986
+ assessTaskNodeAtomicity,
4538
4987
  buildBoardSnapshot,
4539
4988
  buildBoardTasks,
4540
4989
  cleanupSddWorktrees,
4541
4990
  cleanupStaleSddWorktrees,
4542
4991
  cleanupStaleWorktrees,
4543
4992
  createAutoExecutor,
4993
+ decomposeNonAtomicTasks,
4544
4994
  destroySddProject,
4545
4995
  extractVerificationCommand,
4546
4996
  getTemplate,
4547
4997
  hasConflictMarkers,
4548
4998
  isExplanatoryText,
4549
4999
  listTemplates,
5000
+ makeAcceptanceCriteriaVerifier,
4550
5001
  makeCommandVerifier,
5002
+ makeCompositeVerifier,
4551
5003
  makeLlmConflictResolver,
4552
5004
  makeLlmSubtaskGenerator,
5005
+ makePlanningDecomposer,
4553
5006
  makePreferSideConflictResolver,
4554
5007
  renderProgress,
4555
5008
  renderSpecAnalysis,
@@ -4558,6 +5011,7 @@ export {
4558
5011
  resolveConflictText,
4559
5012
  rollbackSddRunFromDisk,
4560
5013
  shortIdMap,
5014
+ splitGraphNode,
4561
5015
  startSddRun,
4562
5016
  templateToMarkdown
4563
5017
  };