@wrongstack/sdd 1.0.28 → 1.0.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -90,15 +90,31 @@ var SpecParser = class {
90
90
  const requirements = [];
91
91
  let inRequirements = false;
92
92
  let idCounter = 0;
93
+ let fence;
93
94
  for (const line of lines) {
95
+ const marker = /^ {0,3}(`{3,}|~{3,})(.*)$/.exec(line);
96
+ const markerRun = marker?.[1];
97
+ if (fence) {
98
+ if (markerRun && markerRun[0] === fence[0] && markerRun.length >= fence.length && !marker?.[2]?.trim()) {
99
+ fence = void 0;
100
+ }
101
+ continue;
102
+ }
103
+ if (marker) {
104
+ fence = marker[1];
105
+ continue;
106
+ }
94
107
  if (/^##\s+Requirements/i.test(line.trim())) {
95
108
  inRequirements = true;
96
109
  continue;
97
110
  }
98
111
  if (inRequirements && /^##\s+/.test(line.trim())) break;
99
112
  if (inRequirements) {
100
- const req = this.parseRequirementLine(line, `REQ-${++idCounter}`);
101
- if (req) requirements.push(req);
113
+ const req = this.parseRequirementLine(line, `REQ-${idCounter + 1}`);
114
+ if (req) {
115
+ idCounter += 1;
116
+ requirements.push(req);
117
+ }
102
118
  }
103
119
  }
104
120
  return requirements;
@@ -119,18 +135,21 @@ var SpecParser = class {
119
135
  if (lower.includes(`[${t}]`)) type = t;
120
136
  }
121
137
  let priority = "medium";
122
- if (trimmed.includes("[critical]") || trimmed.includes("[prio:high]")) {
138
+ if (lower.includes("[critical]") || lower.includes("[prio:high]")) {
123
139
  priority = "critical";
124
- } else if (trimmed.includes("[high]")) {
140
+ } else if (lower.includes("[high]")) {
125
141
  priority = "high";
126
- } else if (trimmed.includes("[low]")) {
142
+ } else if (lower.includes("[low]")) {
127
143
  priority = "low";
128
144
  }
129
145
  return {
130
146
  id,
131
147
  type,
132
148
  priority,
133
- description: trimmed.replace(/\[[^\]]+\]/g, "").trim(),
149
+ description: trimmed.replace(
150
+ /\[(?:functional|non-functional|security|performance|ux|critical|high|medium|low|prio:high)\]/gi,
151
+ ""
152
+ ).trim(),
134
153
  acceptanceCriteria: []
135
154
  };
136
155
  }
@@ -522,12 +541,14 @@ function assertTaskGraphExecutionIntegrity(graph) {
522
541
  const outgoing = /* @__PURE__ */ new Map();
523
542
  for (const edge of dependencies) {
524
543
  indegree.set(edge.to, indegree.get(edge.to) + 1);
525
- outgoing.set(edge.from, [...outgoing.get(edge.from) ?? [], edge.to]);
544
+ const dependents = outgoing.get(edge.from);
545
+ if (dependents) dependents.push(edge.to);
546
+ else outgoing.set(edge.from, [edge.to]);
526
547
  }
527
548
  const ready = Array.from(indegree.entries()).filter(([, degree]) => degree === 0).map(([id]) => id);
528
549
  let visited = 0;
529
- while (ready.length > 0) {
530
- const id = ready.shift();
550
+ for (let cursor = 0; cursor < ready.length; cursor++) {
551
+ const id = ready[cursor];
531
552
  visited += 1;
532
553
  for (const dependent of outgoing.get(id) ?? []) {
533
554
  const next = indegree.get(dependent) - 1;
@@ -643,7 +664,7 @@ var TaskFlow = class {
643
664
  this.opts.tracker.updateNodeStatus(taskId, "completed", comment);
644
665
  this.emit("task.completed", { taskId });
645
666
  } else {
646
- this.opts.tracker.updateNodeStatus(taskId, "in_progress", comment ?? "Needs revision");
667
+ this.opts.tracker.updateNodeStatus(taskId, "pending", comment ?? "Needs revision");
647
668
  this.emit("task.review", { taskId });
648
669
  }
649
670
  }
@@ -742,21 +763,24 @@ var SpecDrivenDev = class {
742
763
  import { randomUUID } from "node:crypto";
743
764
  import * as fsp from "node:fs/promises";
744
765
  import * as path from "node:path";
745
- import { atomicWrite, ensureDir } from "@wrongstack/core/utils";
766
+ import { atomicWrite, ensureDir, withFileLock } from "@wrongstack/core/utils";
746
767
  var SpecStore = class {
747
768
  baseDir;
748
769
  indexPath;
770
+ writeChain = Promise.resolve();
749
771
  constructor(opts) {
750
772
  this.baseDir = opts.baseDir;
751
773
  this.indexPath = path.join(this.baseDir, "_index.json");
752
774
  }
753
775
  async save(spec) {
754
- await ensureDir(this.baseDir);
755
- const filePath = this.filePath(spec.id);
756
- await atomicWrite(filePath, JSON.stringify(spec, null, 2), { mode: 384 });
757
- await this.updateIndex(spec);
776
+ const snapshot = structuredClone(spec);
777
+ await this.enqueueWrite(() => this.writeSnapshot(snapshot));
758
778
  }
759
779
  async load(id) {
780
+ await this.writeChain;
781
+ return this.loadFile(id);
782
+ }
783
+ async loadFile(id) {
760
784
  const filePath = this.filePath(id);
761
785
  try {
762
786
  const raw = await fsp.readFile(filePath, "utf8");
@@ -766,20 +790,26 @@ var SpecStore = class {
766
790
  }
767
791
  }
768
792
  async list() {
793
+ await this.writeChain;
769
794
  const index = await this.readIndex();
770
795
  return index.entries.sort((a, b) => b.updatedAt - a.updatedAt);
771
796
  }
772
797
  async delete(id) {
773
798
  const filePath = this.filePath(id);
774
- try {
775
- await fsp.unlink(filePath);
799
+ return this.enqueueWrite(async () => {
800
+ let removed = false;
801
+ try {
802
+ await fsp.unlink(filePath);
803
+ removed = true;
804
+ } catch (error) {
805
+ if (error.code !== "ENOENT") throw error;
806
+ }
776
807
  await this.removeFromIndex(id);
777
- return true;
778
- } catch {
779
- return false;
780
- }
808
+ return removed;
809
+ });
781
810
  }
782
811
  async exists(id) {
812
+ await this.writeChain;
783
813
  try {
784
814
  await fsp.access(this.filePath(id));
785
815
  return true;
@@ -806,17 +836,36 @@ var SpecStore = class {
806
836
  }
807
837
  /** Update spec fields and persist. */
808
838
  async update(id, patch) {
809
- const spec = await this.load(id);
810
- if (!spec) return null;
811
- const updated = {
812
- ...spec,
813
- ...patch,
814
- id: spec.id,
815
- createdAt: spec.createdAt,
816
- updatedAt: Date.now()
817
- };
818
- await this.save(updated);
819
- return updated;
839
+ return this.enqueueWrite(async () => {
840
+ const spec = await this.loadFile(id);
841
+ if (!spec) return null;
842
+ const updated = {
843
+ ...spec,
844
+ ...patch,
845
+ id: spec.id,
846
+ createdAt: spec.createdAt,
847
+ updatedAt: Date.now()
848
+ };
849
+ await this.writeSnapshot(updated);
850
+ return updated;
851
+ });
852
+ }
853
+ /** Serialize every read-modify-write across this process and other store instances. */
854
+ enqueueWrite(work) {
855
+ const pending = this.writeChain.then(async () => {
856
+ await ensureDir(this.baseDir);
857
+ return withFileLock(this.indexPath, work);
858
+ });
859
+ this.writeChain = pending.then(
860
+ () => void 0,
861
+ () => void 0
862
+ );
863
+ return pending;
864
+ }
865
+ async writeSnapshot(snapshot) {
866
+ const filePath = this.filePath(snapshot.id);
867
+ await atomicWrite(filePath, JSON.stringify(snapshot, null, 2), { mode: 384 });
868
+ await this.updateIndex(snapshot);
820
869
  }
821
870
  /**
822
871
  * Resolve a spec id to its file, refusing anything that escapes `baseDir`.
@@ -827,6 +876,9 @@ var SpecStore = class {
827
876
  * the same containment `task-graph-store.ts:134-145` applies to its ids.
828
877
  */
829
878
  filePath(id) {
879
+ if (typeof id === "string" && id.toLowerCase() === "_index") {
880
+ throw new Error(`Invalid spec id: ${JSON.stringify(id)}`);
881
+ }
830
882
  if (typeof id !== "string" || id.length === 0 || id.length > 200 || /[\0/\\]/.test(id)) {
831
883
  throw new Error(`Invalid spec id: ${JSON.stringify(id)}`);
832
884
  }
@@ -839,13 +891,47 @@ var SpecStore = class {
839
891
  return resolved;
840
892
  }
841
893
  async readIndex() {
894
+ let raw;
895
+ try {
896
+ raw = await fsp.readFile(this.indexPath, "utf8");
897
+ } catch (error) {
898
+ if (error.code === "ENOENT") return this.rebuildIndex();
899
+ throw error;
900
+ }
842
901
  try {
843
- const raw = await fsp.readFile(this.indexPath, "utf8");
844
902
  const parsed = JSON.parse(raw);
845
- if (parsed?.version === 1) return parsed;
903
+ if (parsed?.version === 1 && Array.isArray(parsed.entries) && parsed.entries.every(
904
+ (entry) => entry && typeof entry.id === "string" && typeof entry.title === "string" && typeof entry.version === "string" && typeof entry.status === "string" && Number.isFinite(entry.updatedAt) && typeof entry.filePath === "string"
905
+ ))
906
+ return parsed;
846
907
  } catch {
847
908
  }
848
- return { version: 1, entries: [] };
909
+ return this.rebuildIndex();
910
+ }
911
+ async rebuildIndex() {
912
+ let files;
913
+ try {
914
+ files = await fsp.readdir(this.baseDir, { withFileTypes: true });
915
+ } catch (error) {
916
+ if (error.code === "ENOENT") return { version: 1, entries: [] };
917
+ throw error;
918
+ }
919
+ const entries = [];
920
+ for (const file of files) {
921
+ if (!file.isFile() || !file.name.endsWith(".json") || file.name === "_index.json") continue;
922
+ const id = file.name.slice(0, -".json".length);
923
+ const spec = await this.loadFile(id);
924
+ if (!spec || spec.id !== id) continue;
925
+ entries.push({
926
+ id,
927
+ title: spec.title,
928
+ version: spec.version,
929
+ status: spec.status,
930
+ updatedAt: spec.updatedAt,
931
+ filePath: this.filePath(id)
932
+ });
933
+ }
934
+ return { version: 1, entries };
849
935
  }
850
936
  async updateIndex(spec) {
851
937
  const index = await this.readIndex();
@@ -875,7 +961,7 @@ var SpecStore = class {
875
961
  // src/task-graph-store.ts
876
962
  import * as fsp2 from "node:fs/promises";
877
963
  import * as path2 from "node:path";
878
- import { atomicWrite as atomicWrite2, ensureDir as ensureDir2 } from "@wrongstack/core/utils";
964
+ import { atomicWrite as atomicWrite2, ensureDir as ensureDir2, withFileLock as withFileLock2 } from "@wrongstack/core/utils";
879
965
  function graphToJSON(graph) {
880
966
  const serialisable = {
881
967
  ...graph,
@@ -900,17 +986,17 @@ var TaskGraphStore = class {
900
986
  }
901
987
  async save(graph) {
902
988
  const snapshot = graphFromJSON(graphToJSON(graph));
903
- const pending = this.writeChain.then(async () => {
904
- await ensureDir2(this.baseDir);
989
+ await this.enqueueWrite(async () => {
905
990
  const filePath = this.filePath(snapshot.id);
906
991
  await atomicWrite2(filePath, graphToJSON(snapshot), { mode: 384 });
907
992
  await this.updateIndex(snapshot);
908
993
  });
909
- this.writeChain = pending.catch(() => void 0);
910
- await pending;
911
994
  }
912
995
  async load(id) {
913
996
  await this.writeChain;
997
+ return this.loadFile(id);
998
+ }
999
+ async loadFile(id) {
914
1000
  try {
915
1001
  const raw = await fsp2.readFile(this.filePath(id), "utf8");
916
1002
  return graphFromJSON(raw);
@@ -924,14 +1010,23 @@ var TaskGraphStore = class {
924
1010
  return index.entries.sort((a, b) => b.updatedAt - a.updatedAt);
925
1011
  }
926
1012
  async delete(id) {
927
- await this.writeChain;
1013
+ let filePath;
928
1014
  try {
929
- await fsp2.unlink(this.filePath(id));
930
- await this.removeFromIndex(id);
931
- return true;
1015
+ filePath = this.filePath(id);
932
1016
  } catch {
933
1017
  return false;
934
1018
  }
1019
+ return this.enqueueWrite(async () => {
1020
+ let removed = false;
1021
+ try {
1022
+ await fsp2.unlink(filePath);
1023
+ removed = true;
1024
+ } catch (error) {
1025
+ if (error.code !== "ENOENT") throw error;
1026
+ }
1027
+ await this.removeFromIndex(id);
1028
+ return removed;
1029
+ });
935
1030
  }
936
1031
  async exists(id) {
937
1032
  await this.writeChain;
@@ -964,6 +1059,9 @@ var TaskGraphStore = class {
964
1059
  * already applies to board ids.
965
1060
  */
966
1061
  filePath(id) {
1062
+ if (typeof id === "string" && id.toLowerCase() === "_index") {
1063
+ throw new Error(`Invalid task-graph id: ${JSON.stringify(id)}`);
1064
+ }
967
1065
  if (typeof id !== "string" || id.length === 0 || id.length > 200 || /[\0]/.test(id)) {
968
1066
  throw new Error(`Invalid task-graph id: ${JSON.stringify(id)}`);
969
1067
  }
@@ -976,28 +1074,67 @@ var TaskGraphStore = class {
976
1074
  return resolved;
977
1075
  }
978
1076
  async readIndex() {
1077
+ let raw;
1078
+ try {
1079
+ raw = await fsp2.readFile(this.indexPath, "utf8");
1080
+ } catch (error) {
1081
+ if (error.code === "ENOENT") return this.rebuildIndex();
1082
+ throw error;
1083
+ }
979
1084
  try {
980
- const raw = await fsp2.readFile(this.indexPath, "utf8");
981
1085
  const parsed = JSON.parse(raw);
982
- if (parsed?.version === 1) return parsed;
1086
+ if (parsed?.version === 1 && Array.isArray(parsed.entries) && parsed.entries.every(
1087
+ (entry) => entry && typeof entry.id === "string" && typeof entry.specId === "string" && typeof entry.title === "string" && Number.isSafeInteger(entry.nodeCount) && Number.isSafeInteger(entry.completedCount) && Number.isFinite(entry.updatedAt) && typeof entry.filePath === "string"
1088
+ ))
1089
+ return parsed;
983
1090
  } catch {
984
1091
  }
985
- return { version: 1, entries: [] };
1092
+ return this.rebuildIndex();
986
1093
  }
987
- async updateIndex(graph) {
988
- const index = await this.readIndex();
989
- const completedCount = Array.from(graph.nodes.values()).filter(
990
- (n) => n.status === "completed"
991
- ).length;
992
- const entry = {
1094
+ async rebuildIndex() {
1095
+ let files;
1096
+ try {
1097
+ files = await fsp2.readdir(this.baseDir, { withFileTypes: true });
1098
+ } catch (error) {
1099
+ if (error.code === "ENOENT") return { version: 1, entries: [] };
1100
+ throw error;
1101
+ }
1102
+ const entries = [];
1103
+ for (const file of files) {
1104
+ if (!file.isFile() || !file.name.endsWith(".json") || file.name === "_index.json") continue;
1105
+ const id = file.name.slice(0, -".json".length);
1106
+ const graph = await this.loadFile(id);
1107
+ if (!graph || graph.id !== id) continue;
1108
+ entries.push(this.indexEntry(graph));
1109
+ }
1110
+ return { version: 1, entries };
1111
+ }
1112
+ /** Serialize graph and index mutations across this process and other store instances. */
1113
+ enqueueWrite(work) {
1114
+ const pending = this.writeChain.then(async () => {
1115
+ await ensureDir2(this.baseDir);
1116
+ return withFileLock2(this.indexPath, work);
1117
+ });
1118
+ this.writeChain = pending.then(
1119
+ () => void 0,
1120
+ () => void 0
1121
+ );
1122
+ return pending;
1123
+ }
1124
+ indexEntry(graph) {
1125
+ return {
993
1126
  id: graph.id,
994
1127
  specId: graph.specId,
995
1128
  title: graph.title,
996
1129
  nodeCount: graph.nodes.size,
997
- completedCount,
1130
+ completedCount: Array.from(graph.nodes.values()).filter((n) => n.status === "completed").length,
998
1131
  updatedAt: graph.updatedAt,
999
1132
  filePath: this.filePath(graph.id)
1000
1133
  };
1134
+ }
1135
+ async updateIndex(graph) {
1136
+ const index = await this.readIndex();
1137
+ const entry = this.indexEntry(graph);
1001
1138
  const idx = index.entries.findIndex((e) => e.id === graph.id);
1002
1139
  if (idx >= 0) {
1003
1140
  index.entries[idx] = entry;
@@ -1108,7 +1245,7 @@ function buildBoardSnapshot(graph, run, now) {
1108
1245
  // src/sdd-board-store.ts
1109
1246
  import * as fsp3 from "node:fs/promises";
1110
1247
  import * as path3 from "node:path";
1111
- import { atomicWrite as atomicWrite3, ensureDir as ensureDir3, withFileLock } from "@wrongstack/core/utils";
1248
+ import { atomicWrite as atomicWrite3, ensureDir as ensureDir3, withFileLock as withFileLock3 } from "@wrongstack/core/utils";
1112
1249
  var DEFAULT_EVENT_MAX_BYTES = 16 * 1024 * 1024;
1113
1250
  var DEFAULT_EVENT_KEEP_BYTES = 8 * 1024 * 1024;
1114
1251
  var DEFAULT_EVENT_SIZE_CHECK_EVERY = 100;
@@ -1125,6 +1262,7 @@ var SddBoardStore = class {
1125
1262
  baseDirReady;
1126
1263
  cachedIndex;
1127
1264
  cachedIndexSignature = null;
1265
+ indexNeedsRebuild = false;
1128
1266
  constructor(opts) {
1129
1267
  this.baseDir = opts.baseDir;
1130
1268
  this.indexPath = path3.join(this.baseDir, "_index.json");
@@ -1193,7 +1331,7 @@ var SddBoardStore = class {
1193
1331
  async appendControl(runId, command) {
1194
1332
  await this.ensureBaseDir();
1195
1333
  const filePath = this.controlPath(runId);
1196
- await withFileLock(
1334
+ await withFileLock3(
1197
1335
  filePath,
1198
1336
  () => fsp3.appendFile(filePath, `${JSON.stringify(command)}
1199
1337
  `, { mode: 384 })
@@ -1282,7 +1420,7 @@ var SddBoardStore = class {
1282
1420
  } catch {
1283
1421
  return [];
1284
1422
  }
1285
- return withFileLock(filePath, async () => {
1423
+ return withFileLock3(filePath, async () => {
1286
1424
  let raw;
1287
1425
  try {
1288
1426
  const stat2 = await this.controlFileIO.stat(filePath);
@@ -1320,24 +1458,53 @@ var SddBoardStore = class {
1320
1458
  try {
1321
1459
  const raw = await fsp3.readFile(this.indexPath, "utf8");
1322
1460
  const parsed = JSON.parse(raw);
1323
- if (parsed?.version === 1) {
1461
+ if (parsed?.version === 1 && Array.isArray(parsed.entries)) {
1324
1462
  parsed.entries.sort((a, b) => b.updatedAt - a.updatedAt);
1325
1463
  this.cachedIndex = parsed;
1326
1464
  this.cachedIndexSignature = signature;
1465
+ this.indexNeedsRebuild = false;
1327
1466
  return parsed;
1328
1467
  }
1329
1468
  } catch {
1330
1469
  }
1331
1470
  this.cachedIndex = { version: 1, entries: [] };
1332
1471
  this.cachedIndexSignature = signature;
1472
+ this.indexNeedsRebuild = true;
1333
1473
  return this.cachedIndex;
1334
1474
  }
1475
+ async rebuildIndexEntries() {
1476
+ let files;
1477
+ try {
1478
+ files = await fsp3.readdir(this.baseDir, { withFileTypes: true });
1479
+ } catch (error) {
1480
+ if (error.code === "ENOENT") return [];
1481
+ throw error;
1482
+ }
1483
+ const entries = [];
1484
+ for (const file of files) {
1485
+ if (!file.isFile() || !file.name.endsWith(".json") || file.name === "_index.json") continue;
1486
+ const runId = file.name.slice(0, -".json".length);
1487
+ const snapshot = await this.load(runId);
1488
+ if (!snapshot || snapshot.runId !== runId || typeof snapshot.title !== "string" || typeof snapshot.status !== "string" || !Number.isFinite(snapshot.updatedAt) || !snapshot.progress || !Number.isFinite(snapshot.progress.total) || !Number.isFinite(snapshot.progress.completed))
1489
+ continue;
1490
+ entries.push({
1491
+ runId,
1492
+ specId: snapshot.specId,
1493
+ title: snapshot.title,
1494
+ status: snapshot.status,
1495
+ total: snapshot.progress.total,
1496
+ completed: snapshot.progress.completed,
1497
+ updatedAt: snapshot.updatedAt
1498
+ });
1499
+ }
1500
+ return entries.sort((a, b) => b.updatedAt - a.updatedAt);
1501
+ }
1335
1502
  async updateIndex(snapshot) {
1336
- await withFileLock(this.indexPath, async () => {
1503
+ await withFileLock3(this.indexPath, async () => {
1337
1504
  const current = await this.readIndex();
1338
1505
  const index = {
1339
1506
  version: 1,
1340
- entries: current.entries.map((entry2) => ({ ...entry2 }))
1507
+ entries: this.indexNeedsRebuild ? await this.rebuildIndexEntries() : current.entries.map((entry2) => ({ ...entry2 }))
1341
1508
  };
1342
1509
  const entry = {
1343
1510
  runId: snapshot.runId,
@@ -1355,18 +1522,20 @@ var SddBoardStore = class {
1355
1522
  await atomicWrite3(this.indexPath, JSON.stringify(index, null, 2), { mode: 384 });
1356
1523
  this.cachedIndex = index;
1357
1524
  this.cachedIndexSignature = await this.indexSignature();
1525
+ this.indexNeedsRebuild = false;
1358
1526
  });
1359
1527
  }
1360
1528
  async removeFromIndex(runId) {
1361
- await withFileLock(this.indexPath, async () => {
1529
+ await withFileLock3(this.indexPath, async () => {
1362
1530
  const current = await this.readIndex();
1363
1531
  const index = {
1364
1532
  version: 1,
1365
- entries: current.entries.filter((entry) => entry.runId !== runId).map((entry) => ({ ...entry }))
1533
+ entries: (this.indexNeedsRebuild ? await this.rebuildIndexEntries() : current.entries).filter((entry) => entry.runId !== runId).map((entry) => ({ ...entry }))
1366
1534
  };
1367
1535
  await atomicWrite3(this.indexPath, JSON.stringify(index, null, 2), { mode: 384 });
1368
1536
  this.cachedIndex = index;
1369
1537
  this.cachedIndexSignature = await this.indexSignature();
1538
+ this.indexNeedsRebuild = false;
1370
1539
  });
1371
1540
  }
1372
1541
  async indexSignature() {
@@ -2822,90 +2991,378 @@ import {
2822
2991
  writeKanbanWorkflowState
2823
2992
  } from "@wrongstack/kanban";
2824
2993
 
2825
- // src/sdd-task-recovery.ts
2826
- function computeDeadlockChains(host) {
2827
- const tracker = host.opts.tracker;
2828
- const chains = [];
2829
- for (const node of tracker.getAllNodes()) {
2830
- if (node.status === "completed" || node.status === "failed") continue;
2831
- const blockedBy = tracker.getBlockers(node.id).filter((id) => tracker.getNode(id)?.status !== "completed");
2832
- if (blockedBy.length > 0) chains.push({ blocked: node.id, blockedBy });
2994
+ // src/sdd-task-execution.ts
2995
+ import { randomUUID as randomUUID2 } from "node:crypto";
2996
+ import { expectDefined as expectDefined3 } from "@wrongstack/core/utils";
2997
+ import { assignNickname } from "@wrongstack/core/coordination";
2998
+ import { ERROR_CODES as ERROR_CODES3, SddError as SddError3 } from "@wrongstack/core/types";
2999
+ async function executeSddTask(params) {
3000
+ const { task, opts } = params;
3001
+ const taskId = task.id;
3002
+ let agentName = task.assignee;
3003
+ if (!agentName) {
3004
+ const nick = assignNickname("executor", params.usedNicknames);
3005
+ params.usedNicknames.add(nick.key);
3006
+ agentName = nick.display.replace(/\s*\([^)]*\)\s*$/, "");
3007
+ opts.tracker.updateNode(taskId, { assignee: agentName });
2833
3008
  }
2834
- return chains;
2835
- }
2836
- function recoverFailedBlockers(host) {
2837
- const tracker = host.opts.tracker;
2838
- let recovered = false;
2839
- for (const node of tracker.getAllNodes({ status: ["failed"] })) {
2840
- const blocksIncomplete = tracker.getDependents(node.id).some((d) => {
2841
- const s = tracker.getNode(d)?.status;
2842
- return s !== "completed" && s !== "failed";
3009
+ opts.tracker.updateNodeStatus(taskId, "in_progress");
3010
+ await params.allocateWorktrees([task]);
3011
+ if (!params.coordinator)
3012
+ throw new SddError3({
3013
+ message: "SDD parallel runner requires a coordinator",
3014
+ code: ERROR_CODES3.SDD_INVALID_STATE
2843
3015
  });
2844
- if (blocksIncomplete) {
2845
- host.retryMap.delete(node.id);
2846
- host.persistRetries(node.id, 0);
2847
- tracker.updateNodeStatus(node.id, "pending", "deadlock recovery");
2848
- recovered = true;
2849
- }
2850
- }
2851
- return recovered;
2852
- }
2853
- function requeueFailedTasks(host, reason = "retry failed sweep") {
2854
- const tracker = host.opts.tracker;
2855
- let n = 0;
2856
- for (const node of tracker.getAllNodes({ status: ["failed"] })) {
2857
- if (host.cancelledTasks.has(node.id) || node.metadata?.cancelled) continue;
2858
- host.retryMap.delete(node.id);
2859
- host.persistRetries(node.id, 0);
2860
- tracker.updateNodeStatus(node.id, "pending", reason);
2861
- host.emit("sdd.task.retrying", {
2862
- runId: host.runId,
2863
- taskId: node.id,
2864
- attempt: 0,
2865
- maxRetries: host.maxRetries
3016
+ const coordinator = params.coordinator;
3017
+ const subagentId = params.nextSubagentId();
3018
+ const correlationId = randomUUID2();
3019
+ const meta = task.metadata ?? {};
3020
+ const model = (typeof meta.model === "string" ? meta.model : void 0) ?? opts.defaultModel;
3021
+ const provider = (typeof meta.provider === "string" ? meta.provider : void 0) ?? opts.defaultProvider;
3022
+ const fallbackModels = Array.isArray(meta.fallbackModels) ? meta.fallbackModels : opts.fallbackModels;
3023
+ const spawnResult = await coordinator.spawn({
3024
+ id: subagentId,
3025
+ name: agentName,
3026
+ role: "executor",
3027
+ idleTimeoutMs: params.idleTimeoutMs,
3028
+ ...params.timeoutMs ? { timeoutMs: params.timeoutMs } : {},
3029
+ cwd: params.taskCwds.get(taskId),
3030
+ disabledTools: ["delegate"],
3031
+ ...model ? { model } : {},
3032
+ ...provider ? { provider } : {},
3033
+ ...fallbackModels?.length ? { fallbackModels } : {}
3034
+ });
3035
+ if (!spawnResult.subagentId) {
3036
+ throw new SddError3({
3037
+ message: "One or more subagent spawns failed",
3038
+ code: ERROR_CODES3.SDD_INVALID_STATE
2866
3039
  });
2867
- n++;
2868
3040
  }
2869
- return n;
2870
- }
2871
- function restoreRetryMap(host) {
2872
- host.retryMap.clear();
2873
- for (const node of host.opts.tracker.getAllNodes()) {
2874
- const r = node.metadata?.retries;
2875
- if (typeof r === "number" && r > 0) host.retryMap.set(node.id, r);
3041
+ params.taskSubagents.set(taskId, subagentId);
3042
+ params.emit("sdd.task.started", {
3043
+ runId: params.runId,
3044
+ taskId,
3045
+ subagentId,
3046
+ agentName,
3047
+ worktreeBranch: params.taskBranches.get(taskId)
3048
+ });
3049
+ await coordinator.assign({
3050
+ id: correlationId,
3051
+ description: buildTaskDirective(opts.graph.title, task),
3052
+ subagentId,
3053
+ ...params.timeoutMs ? { timeoutMs: params.timeoutMs } : {},
3054
+ context: {
3055
+ telemetryTaskId: taskId,
3056
+ telemetryRunId: params.runId,
3057
+ telemetryBoardId: opts.graph.id
3058
+ }
3059
+ });
3060
+ let result;
3061
+ try {
3062
+ const got = await coordinator.awaitTasks([correlationId]);
3063
+ result = expectDefined3(got[0]);
3064
+ } catch (err) {
3065
+ result = {
3066
+ subagentId,
3067
+ taskId: correlationId,
3068
+ status: "failed",
3069
+ error: { kind: "unknown", message: String(err), retryable: false },
3070
+ iterations: 0,
3071
+ toolCalls: 0,
3072
+ durationMs: 0
3073
+ };
2876
3074
  }
2877
- }
2878
- function resetOrphans(tracker) {
2879
- let n = 0;
2880
- for (const node of tracker.getAllNodes({ status: ["in_progress"] })) {
2881
- tracker.updateNodeStatus(node.id, "pending", "resume: orphaned in_progress");
2882
- n++;
3075
+ params.taskSubagents.delete(taskId);
3076
+ if (params.cancelledTasks.has(taskId)) {
3077
+ await params.resolveWorktrees([task]);
3078
+ return { taskId, success: false, result };
2883
3079
  }
2884
- return n;
2885
- }
2886
-
2887
- // src/sdd-parallel-run.ts
2888
- import { randomUUID as randomUUID3 } from "node:crypto";
2889
- import {
2890
- DefaultMultiAgentCoordinator,
2891
- makeAgentSubagentRunner,
2892
- withDisabledToolFiltering
2893
- } from "@wrongstack/core/coordination";
2894
- import { requireSessionId } from "@wrongstack/primitives";
2895
-
2896
- // src/graph-split.ts
2897
- function splitGraphNode(tracker, taskId, subtasks, options = {}) {
2898
- const node = tracker.getNode(taskId);
2899
- if (!node) return [];
2900
- if (node.status === "in_progress" || options.isRunning?.(taskId)) return [];
2901
- if (!subtasks.length) return [];
2902
- const blockers = tracker.getBlockers(taskId);
2903
- const dependents = tracker.getDependents(taskId);
2904
- const leafIds = subtasks.map((s) => {
2905
- const criterion = s.successCriterion?.trim();
2906
- const description = criterion ? `${s.description}
2907
-
2908
- **Acceptance Criteria:**
3080
+ const verificationFailReason = await verifyTaskResult(params, result);
3081
+ let success = false;
3082
+ if (result.status === "success" && !verificationFailReason) {
3083
+ const merged = await params.integrateWorktree(task, result);
3084
+ if (merged.ok) {
3085
+ success = true;
3086
+ opts.tracker.updateNodeStatus(taskId, "completed");
3087
+ await params.resolveWorktrees([task]);
3088
+ params.emit("sdd.task.completed", {
3089
+ runId: params.runId,
3090
+ taskId,
3091
+ subagentId,
3092
+ durationMs: result.durationMs
3093
+ });
3094
+ } else if (merged.reason) {
3095
+ params.emit("sdd.task.verification_failed", {
3096
+ runId: params.runId,
3097
+ taskId,
3098
+ reason: merged.reason
3099
+ });
3100
+ if (merged.fatal) {
3101
+ opts.tracker.updateNodeStatus(taskId, "failed", merged.reason);
3102
+ params.emit("sdd.task.failed", {
3103
+ runId: params.runId,
3104
+ taskId,
3105
+ subagentId,
3106
+ error: merged.reason
3107
+ });
3108
+ } else {
3109
+ await params.applyTaskFailure(taskId, subagentId, merged.reason);
3110
+ }
3111
+ } else {
3112
+ const conflictFiles = merged.conflictFiles ?? [];
3113
+ params.emit("sdd.task.conflict", { runId: params.runId, taskId, conflictFiles });
3114
+ const reason = `merge conflict${conflictFiles.length ? `: ${conflictFiles.join(", ")}` : ""}`;
3115
+ await params.applyTaskFailure(taskId, subagentId, reason);
3116
+ }
3117
+ } else {
3118
+ const errMsg = verificationFailReason ?? (result.error?.kind ? `${result.error.kind}: ${result.error.message}` : result.error?.message ?? "unknown error");
3119
+ await params.applyTaskFailure(taskId, subagentId, errMsg);
3120
+ await params.resolveWorktrees([task]);
3121
+ }
3122
+ return { taskId, success, result };
3123
+ }
3124
+ function buildTaskDirective(graphTitle, task) {
3125
+ const directivePreamble = [
3126
+ "\u2550\u2550\u2550 SDD PARALLEL EXECUTION \u2550\u2550\u2550",
3127
+ "",
3128
+ `Graph: ${graphTitle}`,
3129
+ "",
3130
+ "\u2500\u2500 EXECUTION PROTOCOL \u2500\u2500",
3131
+ "\u2022 Execute the assigned SDD task end-to-end using multiple tool calls.",
3132
+ "\u2022 Mark the task [done] in the tracker when complete.",
3133
+ "\u2022 Do not ask before routine in-project tool use; if a permission gate appears, wait for that flow.",
3134
+ "\u2022 Keep output concise \u2014 summarize changes, do not transcribe files."
3135
+ ].join("\n");
3136
+ return [
3137
+ directivePreamble,
3138
+ "",
3139
+ `\u2500\u2500 TASK \u2500\u2500`,
3140
+ `[${task.priority.toUpperCase()}] ${task.title}`,
3141
+ "",
3142
+ task.description
3143
+ ].join("\n");
3144
+ }
3145
+ async function verifyTaskResult(params, result) {
3146
+ const { task, opts, taskCwds } = params;
3147
+ if (result.status !== "success" || !opts.verifyTask) return void 0;
3148
+ const taskId = task.id;
3149
+ const cwd = taskCwds.get(taskId) ?? opts.projectRoot;
3150
+ let verificationFailReason;
3151
+ try {
3152
+ const verdict = await opts.verifyTask({ task, result, cwd });
3153
+ if (!verdict.ok) {
3154
+ verificationFailReason = `verification failed: ${verdict.reason ?? "acceptance criteria not met"}`;
3155
+ }
3156
+ } catch (err) {
3157
+ verificationFailReason = `verification error: ${String(err)}`;
3158
+ }
3159
+ const hadVerifiable = typeof task.metadata?.["verificationCommand"] === "string" || task.description.includes("**Acceptance Criteria:**");
3160
+ if (verificationFailReason) {
3161
+ opts.tracker.patchMetadata(taskId, {
3162
+ verificationState: "failed",
3163
+ verificationDetail: verificationFailReason
3164
+ });
3165
+ params.emit("sdd.task.verification_failed", {
3166
+ runId: params.runId,
3167
+ taskId,
3168
+ reason: verificationFailReason
3169
+ });
3170
+ } else if (hadVerifiable) {
3171
+ opts.tracker.patchMetadata(taskId, {
3172
+ verificationState: "passed",
3173
+ verificationDetail: void 0
3174
+ });
3175
+ }
3176
+ return verificationFailReason;
3177
+ }
3178
+
3179
+ // src/sdd-task-dispatch.ts
3180
+ async function executeWave(batch) {
3181
+ const waveStart = Date.now();
3182
+ const outcomes = await Promise.all(batch.tasks.map((task) => this.executeOne(task)));
3183
+ const results = outcomes.map((o) => o.result).filter((r) => Boolean(r));
3184
+ const successCount = outcomes.filter((o) => o.success).length;
3185
+ const failCount = outcomes.length - successCount;
3186
+ return {
3187
+ wave: batch.wave,
3188
+ batch,
3189
+ results,
3190
+ successCount,
3191
+ failCount,
3192
+ durationMs: Date.now() - waveStart,
3193
+ stopRequested: this.stopRequested
3194
+ };
3195
+ }
3196
+ async function executeOne(task) {
3197
+ const outcome = await executeSddTask({
3198
+ task,
3199
+ opts: this.opts,
3200
+ coordinator: this.coordinator,
3201
+ usedNicknames: this.usedNicknames,
3202
+ idleTimeoutMs: this.idleTimeoutMs,
3203
+ timeoutMs: this.timeoutMs,
3204
+ runId: this.runId,
3205
+ nextSubagentId: () => `sdd-d${this.dispatchSeq++}`,
3206
+ emit: (event, payload) => this.emit(event, payload),
3207
+ taskCwds: this.taskCwds,
3208
+ taskBranches: this.taskBranches,
3209
+ taskSubagents: this.taskSubagents,
3210
+ cancelledTasks: this.cancelledTasks,
3211
+ allocateWorktrees: (tasks) => this.allocateWorktrees(tasks),
3212
+ resolveWorktrees: (tasks) => this.resolveWorktrees(tasks),
3213
+ integrateWorktree: (taskNode, result) => this.integrateWorktree(taskNode, result),
3214
+ applyTaskFailure: (taskId, subagentId, errMsg) => this.applyTaskFailure(taskId, subagentId, errMsg)
3215
+ });
3216
+ if (outcome.success) {
3217
+ this.retryMap.delete(task.id);
3218
+ this.persistRetries(task.id, 0);
3219
+ }
3220
+ return outcome;
3221
+ }
3222
+ async function applyTaskFailure(taskId, subagentId, errMsg) {
3223
+ const currentRetries = this.retryMap.get(taskId) ?? 0;
3224
+ if (currentRetries < this.maxRetries) {
3225
+ this.retryMap.set(taskId, currentRetries + 1);
3226
+ this.persistRetries(taskId, currentRetries + 1);
3227
+ this.opts.tracker.updateNodeStatus(
3228
+ taskId,
3229
+ "pending",
3230
+ `Retry ${currentRetries + 1}/${this.maxRetries}: ${errMsg}`
3231
+ );
3232
+ this.emit("sdd.task.retrying", {
3233
+ runId: this.runId,
3234
+ taskId,
3235
+ attempt: currentRetries + 1,
3236
+ maxRetries: this.maxRetries
3237
+ });
3238
+ return;
3239
+ }
3240
+ if (await this.trySupervisorRescue(taskId, errMsg)) return;
3241
+ this.opts.tracker.updateNodeStatus(taskId, "failed", errMsg);
3242
+ this.emit("sdd.task.failed", { runId: this.runId, taskId, subagentId, error: errMsg });
3243
+ }
3244
+ async function trySupervisorRescue(taskId, errMsg) {
3245
+ const supervise = this.opts.superviseFailure;
3246
+ if (!supervise) return false;
3247
+ const used = this.supervisorEscalations.get(taskId) ?? 0;
3248
+ if (used >= this.maxSupervisorEscalations) return false;
3249
+ const node = this.opts.tracker.getNode(taskId);
3250
+ if (!node) return false;
3251
+ let verdict;
3252
+ try {
3253
+ verdict = await supervise({ task: node, error: errMsg, attempts: used });
3254
+ } catch {
3255
+ return false;
3256
+ }
3257
+ if (!verdict || verdict.action === "fail") return false;
3258
+ this.supervisorEscalations.set(taskId, used + 1);
3259
+ const requeue = (reason) => {
3260
+ this.retryMap.delete(taskId);
3261
+ this.persistRetries(taskId, 0);
3262
+ this.opts.tracker.updateNodeStatus(taskId, "pending", reason);
3263
+ };
3264
+ if (verdict.action === "reassign") {
3265
+ this.setTaskModel(taskId, verdict.model, verdict.provider);
3266
+ requeue(`supervisor reassign: ${verdict.model ?? "default"}`);
3267
+ this.emit("sdd.supervisor.decision", { runId: this.runId, taskId, action: "reassign" });
3268
+ return true;
3269
+ }
3270
+ if (verdict.action === "split") {
3271
+ const ids = this.splitTask(taskId, verdict.subtasks);
3272
+ if (ids.length === 0) return false;
3273
+ this.emit("sdd.supervisor.decision", { runId: this.runId, taskId, action: "split" });
3274
+ return true;
3275
+ }
3276
+ requeue("supervisor retry");
3277
+ this.emit("sdd.supervisor.decision", { runId: this.runId, taskId, action: "retry" });
3278
+ return true;
3279
+ }
3280
+
3281
+ // src/sdd-task-recovery.ts
3282
+ function computeDeadlockChains(host) {
3283
+ const tracker = host.opts.tracker;
3284
+ const chains = [];
3285
+ for (const node of tracker.getAllNodes()) {
3286
+ if (node.status === "completed" || node.status === "failed") continue;
3287
+ const blockedBy = tracker.getBlockers(node.id).filter((id) => tracker.getNode(id)?.status !== "completed");
3288
+ if (blockedBy.length > 0) chains.push({ blocked: node.id, blockedBy });
3289
+ }
3290
+ return chains;
3291
+ }
3292
+ function recoverFailedBlockers(host) {
3293
+ const tracker = host.opts.tracker;
3294
+ let recovered = false;
3295
+ for (const node of tracker.getAllNodes({ status: ["failed"] })) {
3296
+ if (host.cancelledTasks.has(node.id) || node.metadata?.cancelled) continue;
3297
+ const blocksIncomplete = tracker.getDependents(node.id).some((d) => {
3298
+ const s = tracker.getNode(d)?.status;
3299
+ return s !== "completed" && s !== "failed";
3300
+ });
3301
+ if (blocksIncomplete) {
3302
+ host.retryMap.delete(node.id);
3303
+ host.persistRetries(node.id, 0);
3304
+ tracker.updateNodeStatus(node.id, "pending", "deadlock recovery");
3305
+ recovered = true;
3306
+ }
3307
+ }
3308
+ return recovered;
3309
+ }
3310
+ function requeueFailedTasks(host, reason = "retry failed sweep") {
3311
+ const tracker = host.opts.tracker;
3312
+ let n = 0;
3313
+ for (const node of tracker.getAllNodes({ status: ["failed"] })) {
3314
+ if (host.cancelledTasks.has(node.id) || node.metadata?.cancelled) continue;
3315
+ host.retryMap.delete(node.id);
3316
+ host.persistRetries(node.id, 0);
3317
+ tracker.updateNodeStatus(node.id, "pending", reason);
3318
+ host.emit("sdd.task.retrying", {
3319
+ runId: host.runId,
3320
+ taskId: node.id,
3321
+ attempt: 0,
3322
+ maxRetries: host.maxRetries
3323
+ });
3324
+ n++;
3325
+ }
3326
+ return n;
3327
+ }
3328
+ function restoreRetryMap(host) {
3329
+ host.retryMap.clear();
3330
+ for (const node of host.opts.tracker.getAllNodes()) {
3331
+ const r = node.metadata?.retries;
3332
+ if (typeof r === "number" && r > 0) host.retryMap.set(node.id, r);
3333
+ }
3334
+ }
3335
+ function resetOrphans(tracker) {
3336
+ let n = 0;
3337
+ for (const node of tracker.getAllNodes({ status: ["in_progress"] })) {
3338
+ tracker.updateNodeStatus(node.id, "pending", "resume: orphaned in_progress");
3339
+ n++;
3340
+ }
3341
+ return n;
3342
+ }
3343
+
3344
+ // src/sdd-parallel-run.ts
3345
+ import { randomUUID as randomUUID3 } from "node:crypto";
3346
+ import {
3347
+ DefaultMultiAgentCoordinator,
3348
+ makeAgentSubagentRunner,
3349
+ withDisabledToolFiltering
3350
+ } from "@wrongstack/core/coordination";
3351
+ import { requireSessionId } from "@wrongstack/primitives";
3352
+
3353
+ // src/graph-split.ts
3354
+ function splitGraphNode(tracker, taskId, subtasks, options = {}) {
3355
+ const node = tracker.getNode(taskId);
3356
+ if (!node) return [];
3357
+ if (node.status === "in_progress" || options.isRunning?.(taskId)) return [];
3358
+ if (!subtasks.length) return [];
3359
+ const blockers = tracker.getBlockers(taskId);
3360
+ const dependents = tracker.getDependents(taskId);
3361
+ const leafIds = subtasks.map((s) => {
3362
+ const criterion = s.successCriterion?.trim();
3363
+ const description = criterion ? `${s.description}
3364
+
3365
+ **Acceptance Criteria:**
2909
3366
  - ${criterion}` : s.description;
2910
3367
  return tracker.addNode({
2911
3368
  title: s.title,
@@ -3021,199 +3478,17 @@ var SddTaskDecomposer = class {
3021
3478
  });
3022
3479
  return ready;
3023
3480
  }
3024
- /** True when at least one non-completed, non-failed task is blocked. */
3481
+ /** True when non-completed, non-failed tasks remain blocked and cannot make progress. */
3025
3482
  hasAnyBlockedTasks() {
3026
3483
  const nodes = this.tracker.getAllNodes({
3027
3484
  status: ["pending", "in_progress", "blocked"]
3028
3485
  });
3029
- return nodes.some((n) => n.status === "blocked");
3030
- }
3031
- };
3032
-
3033
- // src/sdd-task-execution.ts
3034
- import { randomUUID as randomUUID2 } from "node:crypto";
3035
- import { expectDefined as expectDefined3 } from "@wrongstack/core/utils";
3036
- import { assignNickname } from "@wrongstack/core/coordination";
3037
- import { ERROR_CODES as ERROR_CODES3, SddError as SddError3 } from "@wrongstack/core/types";
3038
- async function executeSddTask(params) {
3039
- const { task, opts } = params;
3040
- const taskId = task.id;
3041
- let agentName = task.assignee;
3042
- if (!agentName) {
3043
- const nick = assignNickname("executor", params.usedNicknames);
3044
- params.usedNicknames.add(nick.key);
3045
- agentName = nick.display.replace(/\s*\([^)]*\)\s*$/, "");
3046
- opts.tracker.updateNode(taskId, { assignee: agentName });
3047
- }
3048
- opts.tracker.updateNodeStatus(taskId, "in_progress");
3049
- await params.allocateWorktrees([task]);
3050
- if (!params.coordinator)
3051
- throw new SddError3({
3052
- message: "SDD parallel runner requires a coordinator",
3053
- code: ERROR_CODES3.SDD_INVALID_STATE
3054
- });
3055
- const coordinator = params.coordinator;
3056
- const subagentId = params.nextSubagentId();
3057
- const correlationId = randomUUID2();
3058
- const meta = task.metadata ?? {};
3059
- const model = (typeof meta.model === "string" ? meta.model : void 0) ?? opts.defaultModel;
3060
- const provider = (typeof meta.provider === "string" ? meta.provider : void 0) ?? opts.defaultProvider;
3061
- const fallbackModels = Array.isArray(meta.fallbackModels) ? meta.fallbackModels : opts.fallbackModels;
3062
- const spawnResult = await coordinator.spawn({
3063
- id: subagentId,
3064
- name: agentName,
3065
- role: "executor",
3066
- idleTimeoutMs: params.idleTimeoutMs,
3067
- ...params.timeoutMs ? { timeoutMs: params.timeoutMs } : {},
3068
- cwd: params.taskCwds.get(taskId),
3069
- disabledTools: ["delegate"],
3070
- ...model ? { model } : {},
3071
- ...provider ? { provider } : {},
3072
- ...fallbackModels?.length ? { fallbackModels } : {}
3073
- });
3074
- if (!spawnResult.subagentId) {
3075
- throw new SddError3({
3076
- message: "One or more subagent spawns failed",
3077
- code: ERROR_CODES3.SDD_INVALID_STATE
3078
- });
3079
- }
3080
- params.taskSubagents.set(taskId, subagentId);
3081
- params.emit("sdd.task.started", {
3082
- runId: params.runId,
3083
- taskId,
3084
- subagentId,
3085
- agentName,
3086
- worktreeBranch: params.taskBranches.get(taskId)
3087
- });
3088
- await coordinator.assign({
3089
- id: correlationId,
3090
- description: buildTaskDirective(opts.graph.title, task),
3091
- subagentId,
3092
- ...params.timeoutMs ? { timeoutMs: params.timeoutMs } : {},
3093
- context: {
3094
- telemetryTaskId: taskId,
3095
- telemetryRunId: params.runId,
3096
- telemetryBoardId: opts.graph.id
3097
- }
3098
- });
3099
- let result;
3100
- try {
3101
- const got = await coordinator.awaitTasks([correlationId]);
3102
- result = expectDefined3(got[0]);
3103
- } catch (err) {
3104
- result = {
3105
- subagentId,
3106
- taskId: correlationId,
3107
- status: "failed",
3108
- error: { kind: "unknown", message: String(err), retryable: false },
3109
- iterations: 0,
3110
- toolCalls: 0,
3111
- durationMs: 0
3112
- };
3113
- }
3114
- params.taskSubagents.delete(taskId);
3115
- if (params.cancelledTasks.has(taskId)) {
3116
- await params.resolveWorktrees([task]);
3117
- return { taskId, success: false, result };
3118
- }
3119
- const verificationFailReason = await verifyTaskResult(params, result);
3120
- let success = false;
3121
- if (result.status === "success" && !verificationFailReason) {
3122
- const merged = await params.integrateWorktree(task, result);
3123
- if (merged.ok) {
3124
- success = true;
3125
- opts.tracker.updateNodeStatus(taskId, "completed");
3126
- await params.resolveWorktrees([task]);
3127
- params.emit("sdd.task.completed", {
3128
- runId: params.runId,
3129
- taskId,
3130
- subagentId,
3131
- durationMs: result.durationMs
3132
- });
3133
- } else if (merged.reason) {
3134
- params.emit("sdd.task.verification_failed", {
3135
- runId: params.runId,
3136
- taskId,
3137
- reason: merged.reason
3138
- });
3139
- if (merged.fatal) {
3140
- opts.tracker.updateNodeStatus(taskId, "failed", merged.reason);
3141
- params.emit("sdd.task.failed", {
3142
- runId: params.runId,
3143
- taskId,
3144
- subagentId,
3145
- error: merged.reason
3146
- });
3147
- } else {
3148
- await params.applyTaskFailure(taskId, subagentId, merged.reason);
3149
- }
3150
- } else {
3151
- const conflictFiles = merged.conflictFiles ?? [];
3152
- params.emit("sdd.task.conflict", { runId: params.runId, taskId, conflictFiles });
3153
- const reason = `merge conflict${conflictFiles.length ? `: ${conflictFiles.join(", ")}` : ""}`;
3154
- await params.applyTaskFailure(taskId, subagentId, reason);
3155
- }
3156
- } else {
3157
- const errMsg = verificationFailReason ?? (result.error?.kind ? `${result.error.kind}: ${result.error.message}` : result.error?.message ?? "unknown error");
3158
- await params.applyTaskFailure(taskId, subagentId, errMsg);
3159
- await params.resolveWorktrees([task]);
3160
- }
3161
- return { taskId, success, result };
3162
- }
3163
- function buildTaskDirective(graphTitle, task) {
3164
- const directivePreamble = [
3165
- "\u2550\u2550\u2550 SDD PARALLEL EXECUTION \u2550\u2550\u2550",
3166
- "",
3167
- `Graph: ${graphTitle}`,
3168
- "",
3169
- "\u2500\u2500 EXECUTION PROTOCOL \u2500\u2500",
3170
- "\u2022 Execute the assigned SDD task end-to-end using multiple tool calls.",
3171
- "\u2022 Mark the task [done] in the tracker when complete.",
3172
- "\u2022 Do not ask before routine in-project tool use; if a permission gate appears, wait for that flow.",
3173
- "\u2022 Keep output concise \u2014 summarize changes, do not transcribe files."
3174
- ].join("\n");
3175
- return [
3176
- directivePreamble,
3177
- "",
3178
- `\u2500\u2500 TASK \u2500\u2500`,
3179
- `[${task.priority.toUpperCase()}] ${task.title}`,
3180
- "",
3181
- task.description
3182
- ].join("\n");
3183
- }
3184
- async function verifyTaskResult(params, result) {
3185
- const { task, opts, taskCwds } = params;
3186
- if (result.status !== "success" || !opts.verifyTask) return void 0;
3187
- const taskId = task.id;
3188
- const cwd = taskCwds.get(taskId) ?? opts.projectRoot;
3189
- let verificationFailReason;
3190
- try {
3191
- const verdict = await opts.verifyTask({ task, result, cwd });
3192
- if (!verdict.ok) {
3193
- verificationFailReason = `verification failed: ${verdict.reason ?? "acceptance criteria not met"}`;
3486
+ if (nodes.some((n) => n.status === "in_progress")) {
3487
+ return false;
3194
3488
  }
3195
- } catch (err) {
3196
- verificationFailReason = `verification error: ${String(err)}`;
3489
+ return nodes.some((n) => n.status === "blocked" || n.status === "pending");
3197
3490
  }
3198
- const hadVerifiable = typeof task.metadata?.["verificationCommand"] === "string" || task.description.includes("**Acceptance Criteria:**");
3199
- if (verificationFailReason) {
3200
- opts.tracker.patchMetadata(taskId, {
3201
- verificationState: "failed",
3202
- verificationDetail: verificationFailReason
3203
- });
3204
- params.emit("sdd.task.verification_failed", {
3205
- runId: params.runId,
3206
- taskId,
3207
- reason: verificationFailReason
3208
- });
3209
- } else if (hadVerifiable) {
3210
- opts.tracker.patchMetadata(taskId, {
3211
- verificationState: "passed",
3212
- verificationDetail: void 0
3213
- });
3214
- }
3215
- return verificationFailReason;
3216
- }
3491
+ };
3217
3492
 
3218
3493
  // src/sdd-worktree-integration.ts
3219
3494
  function forgetTaskWorktree(state, taskId, opts = {}) {
@@ -3677,14 +3952,20 @@ var SddParallelRun = class {
3677
3952
  running.set(task.id, tracked);
3678
3953
  };
3679
3954
  while (!this.stopRequested) {
3680
- if (totalDispatched >= this.maxTotalWaves) break;
3681
- if (this.maxWallClockMs && Date.now() - startTime >= this.maxWallClockMs) break;
3955
+ if (totalDispatched >= this.maxTotalWaves) {
3956
+ await Promise.allSettled(running.values());
3957
+ break;
3958
+ }
3959
+ if (this.maxWallClockMs && Date.now() - startTime >= this.maxWallClockMs) {
3960
+ this.stop();
3961
+ break;
3962
+ }
3682
3963
  await this.waitWhilePaused();
3683
3964
  if (this.stopRequested) break;
3684
3965
  let dispatchedThisRound = 0;
3685
3966
  const ready = this.decomposer.readyNodes().filter((t) => !running.has(t.id));
3686
3967
  for (const task of ready) {
3687
- if (running.size >= this.slots) break;
3968
+ if (running.size >= this.slots || totalDispatched >= this.maxTotalWaves) break;
3688
3969
  dispatch(task);
3689
3970
  dispatchedThisRound++;
3690
3971
  }
@@ -3861,20 +4142,7 @@ var SddParallelRun = class {
3861
4142
  * (surfaced from `executeOne`), preserving the original all-or-nothing contract.
3862
4143
  */
3863
4144
  async executeWave(batch) {
3864
- const waveStart = Date.now();
3865
- const outcomes = await Promise.all(batch.tasks.map((task) => this.executeOne(task)));
3866
- const results = outcomes.map((o) => o.result).filter((r) => Boolean(r));
3867
- const successCount = outcomes.filter((o) => o.success).length;
3868
- const failCount = outcomes.length - successCount;
3869
- return {
3870
- wave: batch.wave,
3871
- batch,
3872
- results,
3873
- successCount,
3874
- failCount,
3875
- durationMs: Date.now() - waveStart,
3876
- stopRequested: this.stopRequested
3877
- };
4145
+ return executeWave.call(this.sddTaskDispatchHost(), batch);
3878
4146
  }
3879
4147
  /**
3880
4148
  * Execute one task end-to-end: assign a worker identity, allocate its worktree,
@@ -3884,30 +4152,7 @@ var SddParallelRun = class {
3884
4152
  * missing coordinator or failed spawn so callers can enforce all-or-nothing.
3885
4153
  */
3886
4154
  async executeOne(task) {
3887
- const outcome = await executeSddTask({
3888
- task,
3889
- opts: this.opts,
3890
- coordinator: this.coordinator,
3891
- usedNicknames: this.usedNicknames,
3892
- idleTimeoutMs: this.idleTimeoutMs,
3893
- timeoutMs: this.timeoutMs,
3894
- runId: this.runId,
3895
- nextSubagentId: () => `sdd-d${this.dispatchSeq++}`,
3896
- emit: (event, payload) => this.emit(event, payload),
3897
- taskCwds: this.taskCwds,
3898
- taskBranches: this.taskBranches,
3899
- taskSubagents: this.taskSubagents,
3900
- cancelledTasks: this.cancelledTasks,
3901
- allocateWorktrees: (tasks) => this.allocateWorktrees(tasks),
3902
- resolveWorktrees: (tasks) => this.resolveWorktrees(tasks),
3903
- integrateWorktree: (taskNode, result) => this.integrateWorktree(taskNode, result),
3904
- applyTaskFailure: (taskId, subagentId, errMsg) => this.applyTaskFailure(taskId, subagentId, errMsg)
3905
- });
3906
- if (outcome.success) {
3907
- this.retryMap.delete(task.id);
3908
- this.persistRetries(task.id, 0);
3909
- }
3910
- return outcome;
4155
+ return executeOne.call(this.sddTaskDispatchHost(), task);
3911
4156
  }
3912
4157
  /**
3913
4158
  * Apply a task failure: retry (→ pending, bump retry count) while attempts
@@ -3917,26 +4162,7 @@ var SddParallelRun = class {
3917
4162
  * negotiate the same retry budget and emit the same events.
3918
4163
  */
3919
4164
  async applyTaskFailure(taskId, subagentId, errMsg) {
3920
- const currentRetries = this.retryMap.get(taskId) ?? 0;
3921
- if (currentRetries < this.maxRetries) {
3922
- this.retryMap.set(taskId, currentRetries + 1);
3923
- this.persistRetries(taskId, currentRetries + 1);
3924
- this.opts.tracker.updateNodeStatus(
3925
- taskId,
3926
- "pending",
3927
- `Retry ${currentRetries + 1}/${this.maxRetries}: ${errMsg}`
3928
- );
3929
- this.emit("sdd.task.retrying", {
3930
- runId: this.runId,
3931
- taskId,
3932
- attempt: currentRetries + 1,
3933
- maxRetries: this.maxRetries
3934
- });
3935
- return;
3936
- }
3937
- if (await this.trySupervisorRescue(taskId, errMsg)) return;
3938
- this.opts.tracker.updateNodeStatus(taskId, "failed", errMsg);
3939
- this.emit("sdd.task.failed", { runId: this.runId, taskId, subagentId, error: errMsg });
4165
+ return applyTaskFailure.call(this.sddTaskDispatchHost(), taskId, subagentId, errMsg);
3940
4166
  }
3941
4167
  /**
3942
4168
  * Consult `superviseFailure` for a task that has exhausted its retries.
@@ -3945,40 +4171,7 @@ var SddParallelRun = class {
3945
4171
  * `maxSupervisorEscalations` so an always-"retry" supervisor can't loop forever.
3946
4172
  */
3947
4173
  async trySupervisorRescue(taskId, errMsg) {
3948
- const supervise = this.opts.superviseFailure;
3949
- if (!supervise) return false;
3950
- const used = this.supervisorEscalations.get(taskId) ?? 0;
3951
- if (used >= this.maxSupervisorEscalations) return false;
3952
- const node = this.opts.tracker.getNode(taskId);
3953
- if (!node) return false;
3954
- let verdict;
3955
- try {
3956
- verdict = await supervise({ task: node, error: errMsg, attempts: used });
3957
- } catch {
3958
- return false;
3959
- }
3960
- if (!verdict || verdict.action === "fail") return false;
3961
- this.supervisorEscalations.set(taskId, used + 1);
3962
- const requeue = (reason) => {
3963
- this.retryMap.delete(taskId);
3964
- this.persistRetries(taskId, 0);
3965
- this.opts.tracker.updateNodeStatus(taskId, "pending", reason);
3966
- };
3967
- if (verdict.action === "reassign") {
3968
- this.setTaskModel(taskId, verdict.model, verdict.provider);
3969
- requeue(`supervisor reassign: ${verdict.model ?? "default"}`);
3970
- this.emit("sdd.supervisor.decision", { runId: this.runId, taskId, action: "reassign" });
3971
- return true;
3972
- }
3973
- if (verdict.action === "split") {
3974
- const ids = this.splitTask(taskId, verdict.subtasks);
3975
- if (ids.length === 0) return false;
3976
- this.emit("sdd.supervisor.decision", { runId: this.runId, taskId, action: "split" });
3977
- return true;
3978
- }
3979
- requeue("supervisor retry");
3980
- this.emit("sdd.supervisor.decision", { runId: this.runId, taskId, action: "retry" });
3981
- return true;
4174
+ return trySupervisorRescue.call(this.sddTaskDispatchHost(), taskId, errMsg);
3982
4175
  }
3983
4176
  /**
3984
4177
  * Integrate a verified-successful task's worktree into the base branch.
@@ -4062,6 +4255,35 @@ var SddParallelRun = class {
4062
4255
  maxRetries: this.maxRetries
4063
4256
  };
4064
4257
  }
4258
+ sddTaskDispatchHost() {
4259
+ void this.executeOne;
4260
+ void this.stopRequested;
4261
+ void this.opts;
4262
+ void this.coordinator;
4263
+ void this.usedNicknames;
4264
+ void this.idleTimeoutMs;
4265
+ void this.timeoutMs;
4266
+ void this.runId;
4267
+ void this.dispatchSeq;
4268
+ void this.emit;
4269
+ void this.taskCwds;
4270
+ void this.taskBranches;
4271
+ void this.taskSubagents;
4272
+ void this.cancelledTasks;
4273
+ void this.allocateWorktrees;
4274
+ void this.resolveWorktrees;
4275
+ void this.integrateWorktree;
4276
+ void this.applyTaskFailure;
4277
+ void this.retryMap;
4278
+ void this.persistRetries;
4279
+ void this.maxRetries;
4280
+ void this.trySupervisorRescue;
4281
+ void this.supervisorEscalations;
4282
+ void this.maxSupervisorEscalations;
4283
+ void this.setTaskModel;
4284
+ void this.splitTask;
4285
+ return this;
4286
+ }
4065
4287
  };
4066
4288
 
4067
4289
  // src/start-sdd-run.ts
@@ -4836,11 +5058,17 @@ function renderTaskGraph(graph, opts) {
4836
5058
  lines.push(renderProgress(progress));
4837
5059
  lines.push("");
4838
5060
  const childrenMap = /* @__PURE__ */ new Map();
5061
+ const dependentsMap = /* @__PURE__ */ new Map();
4839
5062
  for (const edge of graph.edges) {
4840
5063
  if (edge.type === "depends_on") {
4841
5064
  const deps = childrenMap.get(edge.from) ?? [];
4842
5065
  deps.push(edge.to);
4843
5066
  childrenMap.set(edge.from, deps);
5067
+ if (graph.nodes.has(edge.from)) {
5068
+ const dependents = dependentsMap.get(edge.to) ?? [];
5069
+ dependents.push(edge.from);
5070
+ dependentsMap.set(edge.to, dependents);
5071
+ }
4844
5072
  }
4845
5073
  }
4846
5074
  const rendered = /* @__PURE__ */ new Set();
@@ -4850,18 +5078,18 @@ function renderTaskGraph(graph, opts) {
4850
5078
  return !deps || deps.length === 0;
4851
5079
  });
4852
5080
  for (const rootId of startNodes) {
4853
- renderNode(graph, rootId, lines, rendered, childrenMap, compact, "");
5081
+ renderNode(graph, rootId, lines, rendered, childrenMap, dependentsMap, compact, "");
4854
5082
  }
4855
5083
  for (const [id] of graph.nodes) {
4856
5084
  if (!rendered.has(id)) {
4857
- renderNode(graph, id, lines, rendered, childrenMap, compact, "");
5085
+ renderNode(graph, id, lines, rendered, childrenMap, dependentsMap, compact, "");
4858
5086
  }
4859
5087
  }
4860
5088
  lines.push("");
4861
5089
  lines.push("Legend: \u25CF done \u25D0 in-progress \u25CB pending \u2297 blocked \u2717 failed \u25D2 review");
4862
5090
  return lines.join("\n");
4863
5091
  }
4864
- function renderNode(graph, nodeId, lines, rendered, childrenMap, compact, prefix) {
5092
+ function renderNode(graph, nodeId, lines, rendered, childrenMap, dependentsMap, compact, prefix) {
4865
5093
  if (rendered.has(nodeId)) return;
4866
5094
  rendered.add(nodeId);
4867
5095
  const node = expectDefined4(graph.nodes.get(nodeId));
@@ -4878,9 +5106,9 @@ function renderNode(graph, nodeId, lines, rendered, childrenMap, compact, prefix
4878
5106
  lines.push(`${prefix} \u2514 ${truncate(dl, 60)}`);
4879
5107
  }
4880
5108
  }
4881
- const dependents = graph.edges.filter((e) => e.type === "depends_on" && e.to === nodeId).map((e) => e.from).filter((id) => graph.nodes.has(id));
5109
+ const dependents = dependentsMap.get(nodeId) ?? [];
4882
5110
  for (const depId of dependents) {
4883
- renderNode(graph, depId, lines, rendered, childrenMap, compact, prefix + " ");
5111
+ renderNode(graph, depId, lines, rendered, childrenMap, dependentsMap, compact, prefix + " ");
4884
5112
  }
4885
5113
  }
4886
5114
  function renderProgress(progress) {
@@ -4991,24 +5219,28 @@ function analyzeCriticalPath(graph) {
4991
5219
  for (const node of nodes) {
4992
5220
  if (node.status === "completed") continue;
4993
5221
  const downstream = getTransitiveBlocked(graph, node.id, blocksMap);
4994
- if (downstream.size > 0) {
4995
- const blockedHours = Array.from(downstream).reduce((sum, id) => {
4996
- const n = graph.nodes.get(id);
4997
- return sum + (n?.estimateHours ?? 0);
4998
- }, 0);
5222
+ const activeDownstream = Array.from(downstream).filter((id) => {
5223
+ const downstreamNode = graph.nodes.get(id);
5224
+ return downstreamNode?.status !== "completed";
5225
+ });
5226
+ if (activeDownstream.length > 0) {
5227
+ const blockedHours = activeDownstream.reduce(
5228
+ (sum, id) => sum + taskEstimateHours(graph, id, 0),
5229
+ 0
5230
+ );
4999
5231
  bottlenecks.push({
5000
5232
  taskId: node.id,
5001
5233
  title: node.title,
5002
- blockedCount: downstream.size,
5234
+ blockedCount: activeDownstream.length,
5003
5235
  blockedHours,
5004
- severity: Math.min(100, Math.round(downstream.size / nodes.length * 100))
5236
+ severity: Math.min(100, Math.round(activeDownstream.length / nodes.length * 100))
5005
5237
  });
5006
5238
  }
5007
5239
  }
5008
5240
  bottlenecks.sort((a, b) => b.severity - a.severity);
5009
5241
  const criticalPath = computeCriticalPath(graph, topoOrder, blockedByMap);
5010
5242
  const totalHours = criticalPath.reduce((sum, id) => {
5011
- return sum + (graph.nodes.get(id).estimateHours ?? 0);
5243
+ return sum + taskEstimateHours(graph, id, 0);
5012
5244
  }, 0);
5013
5245
  const parallelGroups = computeParallelGroups(graph, blockedByMap);
5014
5246
  const executionOrder = topoOrder.filter((id) => {
@@ -5047,7 +5279,7 @@ function computeCriticalPath(graph, _topoOrder, blockedByMap) {
5047
5279
  const dist = /* @__PURE__ */ new Map();
5048
5280
  const prev = /* @__PURE__ */ new Map();
5049
5281
  for (const id of allIds) {
5050
- dist.set(id, graph.nodes.get(id)?.estimateHours ?? 1);
5282
+ dist.set(id, taskEstimateHours(graph, id, 1));
5051
5283
  prev.set(id, null);
5052
5284
  }
5053
5285
  const blocksMap = /* @__PURE__ */ new Map();
@@ -5064,7 +5296,7 @@ function computeCriticalPath(graph, _topoOrder, blockedByMap) {
5064
5296
  const blocked = blocksMap.get(id);
5065
5297
  if (!blocked) continue;
5066
5298
  for (const blockedId of blocked) {
5067
- const candidateDist = dist.get(id) + (graph.nodes.get(blockedId)?.estimateHours ?? 1);
5299
+ const candidateDist = dist.get(id) + taskEstimateHours(graph, blockedId, 1);
5068
5300
  if (candidateDist > (dist.get(blockedId) ?? 0)) {
5069
5301
  dist.set(blockedId, candidateDist);
5070
5302
  prev.set(blockedId, id);
@@ -5093,6 +5325,10 @@ function computeCriticalPath(graph, _topoOrder, blockedByMap) {
5093
5325
  }
5094
5326
  return path7;
5095
5327
  }
5328
+ function taskEstimateHours(graph, taskId, fallback) {
5329
+ const value = graph.nodes.get(taskId)?.estimateHours;
5330
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : fallback;
5331
+ }
5096
5332
  function computeParallelGroups(graph, blockedByMap) {
5097
5333
  const groups = [];
5098
5334
  const assigned = new Set(
@@ -5143,21 +5379,23 @@ var SpecVersioning = class {
5143
5379
  const history = this.versions.get(spec.id) ?? [];
5144
5380
  history.push(version);
5145
5381
  this.versions.set(spec.id, history);
5146
- return version;
5382
+ return structuredClone(version);
5147
5383
  }
5148
5384
  /** Get version history for a spec. */
5149
5385
  getHistory(specId) {
5150
- return this.versions.get(specId) ?? [];
5386
+ return structuredClone(this.versions.get(specId) ?? []);
5151
5387
  }
5152
5388
  /** Get a specific version of a spec. */
5153
5389
  getVersion(specId, version) {
5154
5390
  const history = this.versions.get(specId) ?? [];
5155
- return history.find((v) => v.version === version);
5391
+ const found = history.find((v) => v.version === version);
5392
+ return found ? structuredClone(found) : void 0;
5156
5393
  }
5157
5394
  /** Get the latest version of a spec. */
5158
5395
  getLatest(specId) {
5159
5396
  const history = this.versions.get(specId) ?? [];
5160
- return history[history.length - 1];
5397
+ const latest = history[history.length - 1];
5398
+ return latest ? structuredClone(latest) : void 0;
5161
5399
  }
5162
5400
  /** Compute diff between two versions of a spec. */
5163
5401
  diff(oldSpec, newSpec) {
@@ -5245,6 +5483,17 @@ var SpecVersioning = class {
5245
5483
  task.title = mod.requirement.description;
5246
5484
  task.description = this.buildTaskDescription(mod.requirement);
5247
5485
  task.priority = mod.requirement.priority;
5486
+ const metadataTags = /* @__PURE__ */ new Set([
5487
+ mod.previousVersion.type,
5488
+ mod.previousVersion.priority,
5489
+ mod.requirement.type,
5490
+ mod.requirement.priority
5491
+ ]);
5492
+ task.tags = [
5493
+ ...(task.tags ?? []).filter((tag) => !metadataTags.has(tag)),
5494
+ mod.requirement.type,
5495
+ mod.requirement.priority
5496
+ ];
5248
5497
  task.updatedAt = Date.now();
5249
5498
  changes.push(`Updated task: ${task.title} (${mod.changes.join(", ")})`);
5250
5499
  }
@@ -5267,6 +5516,9 @@ var SpecVersioning = class {
5267
5516
  graph.rootNodes.push(newTask.id);
5268
5517
  changes.push(`Added task: ${newTask.title}`);
5269
5518
  }
5519
+ graph.requiredRequirementIds = Array.from(
5520
+ new Set(newSpec.requirements.map((requirement) => requirement.id))
5521
+ );
5270
5522
  graph.updatedAt = Date.now();
5271
5523
  return { graph, changes };
5272
5524
  }
@@ -5930,8 +6182,10 @@ function resolveConflictText(text, side) {
5930
6182
  }
5931
6183
  if (isMarker(line, "|", size)) state = "base";
5932
6184
  else if (isMarker(line, "=", size)) state = "theirs";
5933
- else if (isMarker(line, ">", size)) state = "normal";
5934
- else if (state === "ours" && side === "base") out.push(line);
6185
+ else if (isMarker(line, ">", size)) {
6186
+ if (state !== "theirs") return text;
6187
+ state = "normal";
6188
+ } else if (state === "ours" && side === "base") out.push(line);
5935
6189
  else if (state === "theirs" && side === "incoming") out.push(line);
5936
6190
  }
5937
6191
  if (state !== "normal") return text;