@wrongstack/sdd 1.0.29 → 1.0.33

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)}`;
3197
- }
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
- });
3489
+ return nodes.some((n) => n.status === "blocked" || n.status === "pending");
3214
3490
  }
3215
- return verificationFailReason;
3216
- }
3491
+ };
3217
3492
 
3218
3493
  // src/sdd-worktree-integration.ts
3219
3494
  function forgetTaskWorktree(state, taskId, opts = {}) {
@@ -3422,6 +3697,15 @@ var SddParallelRun = class {
3422
3697
  /** Monotonic dispatch counter (unique subagent ids) + dispatch-round counter. */
3423
3698
  dispatchSeq = 0;
3424
3699
  round = 0;
3700
+ /**
3701
+ * True once `run()` has returned. A FINISHED run is not "running" even when
3702
+ * the graph is left unsettled (deadlock exit): `isRunning()` gates
3703
+ * `cleanupWorktrees()` / `rollback()` / the Ctrl+C ladders, and a returned
3704
+ * run kept reporting live, silently no-oping cleanup and refusing rollback
3705
+ * with "run still active" until the user called `stop()` on a run that had
3706
+ * already ended. Cleared again at the top of `run()` so a re-run re-arms.
3707
+ */
3708
+ runReturned = false;
3425
3709
  /** Type-safe emit on the optional EventBus (no-op when unwired). */
3426
3710
  emit(event, payload) {
3427
3711
  const sessionId = this.currentSessionId();
@@ -3463,7 +3747,7 @@ var SddParallelRun = class {
3463
3747
  return this.paused;
3464
3748
  }
3465
3749
  isRunning() {
3466
- return !this.stopRequested && !this.decomposer.isSettled();
3750
+ return !this.stopRequested && !this.runReturned && !this.decomposer.isSettled();
3467
3751
  }
3468
3752
  /** Base branch the run's squash commits land on (undefined when worktrees off). */
3469
3753
  getBaseBranch() {
@@ -3638,6 +3922,7 @@ var SddParallelRun = class {
3638
3922
  const startTime = Date.now();
3639
3923
  this.round = 0;
3640
3924
  this.dispatchSeq = 0;
3925
+ this.runReturned = false;
3641
3926
  let totalDispatched = 0;
3642
3927
  this.buildCoordinator();
3643
3928
  if (this.opts.worktrees && !this.baseBranch) {
@@ -3677,14 +3962,20 @@ var SddParallelRun = class {
3677
3962
  running.set(task.id, tracked);
3678
3963
  };
3679
3964
  while (!this.stopRequested) {
3680
- if (totalDispatched >= this.maxTotalWaves) break;
3681
- if (this.maxWallClockMs && Date.now() - startTime >= this.maxWallClockMs) break;
3965
+ if (totalDispatched >= this.maxTotalWaves) {
3966
+ await Promise.allSettled(running.values());
3967
+ break;
3968
+ }
3969
+ if (this.maxWallClockMs && Date.now() - startTime >= this.maxWallClockMs) {
3970
+ this.stop();
3971
+ break;
3972
+ }
3682
3973
  await this.waitWhilePaused();
3683
3974
  if (this.stopRequested) break;
3684
3975
  let dispatchedThisRound = 0;
3685
3976
  const ready = this.decomposer.readyNodes().filter((t) => !running.has(t.id));
3686
3977
  for (const task of ready) {
3687
- if (running.size >= this.slots) break;
3978
+ if (running.size >= this.slots || totalDispatched >= this.maxTotalWaves) break;
3688
3979
  dispatch(task);
3689
3980
  dispatchedThisRound++;
3690
3981
  }
@@ -3737,6 +4028,7 @@ var SddParallelRun = class {
3737
4028
  stopped: this.stopRequested,
3738
4029
  ...this.fatalError ? { fatalError: this.fatalError } : {}
3739
4030
  });
4031
+ this.runReturned = true;
3740
4032
  return {
3741
4033
  totalWaves: this.round,
3742
4034
  totalCompleted: finalProgress.completed,
@@ -3861,20 +4153,7 @@ var SddParallelRun = class {
3861
4153
  * (surfaced from `executeOne`), preserving the original all-or-nothing contract.
3862
4154
  */
3863
4155
  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
- };
4156
+ return executeWave.call(this.sddTaskDispatchHost(), batch);
3878
4157
  }
3879
4158
  /**
3880
4159
  * Execute one task end-to-end: assign a worker identity, allocate its worktree,
@@ -3884,30 +4163,7 @@ var SddParallelRun = class {
3884
4163
  * missing coordinator or failed spawn so callers can enforce all-or-nothing.
3885
4164
  */
3886
4165
  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;
4166
+ return executeOne.call(this.sddTaskDispatchHost(), task);
3911
4167
  }
3912
4168
  /**
3913
4169
  * Apply a task failure: retry (→ pending, bump retry count) while attempts
@@ -3917,26 +4173,7 @@ var SddParallelRun = class {
3917
4173
  * negotiate the same retry budget and emit the same events.
3918
4174
  */
3919
4175
  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 });
4176
+ return applyTaskFailure.call(this.sddTaskDispatchHost(), taskId, subagentId, errMsg);
3940
4177
  }
3941
4178
  /**
3942
4179
  * Consult `superviseFailure` for a task that has exhausted its retries.
@@ -3945,40 +4182,7 @@ var SddParallelRun = class {
3945
4182
  * `maxSupervisorEscalations` so an always-"retry" supervisor can't loop forever.
3946
4183
  */
3947
4184
  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;
4185
+ return trySupervisorRescue.call(this.sddTaskDispatchHost(), taskId, errMsg);
3982
4186
  }
3983
4187
  /**
3984
4188
  * Integrate a verified-successful task's worktree into the base branch.
@@ -4062,6 +4266,35 @@ var SddParallelRun = class {
4062
4266
  maxRetries: this.maxRetries
4063
4267
  };
4064
4268
  }
4269
+ sddTaskDispatchHost() {
4270
+ void this.executeOne;
4271
+ void this.stopRequested;
4272
+ void this.opts;
4273
+ void this.coordinator;
4274
+ void this.usedNicknames;
4275
+ void this.idleTimeoutMs;
4276
+ void this.timeoutMs;
4277
+ void this.runId;
4278
+ void this.dispatchSeq;
4279
+ void this.emit;
4280
+ void this.taskCwds;
4281
+ void this.taskBranches;
4282
+ void this.taskSubagents;
4283
+ void this.cancelledTasks;
4284
+ void this.allocateWorktrees;
4285
+ void this.resolveWorktrees;
4286
+ void this.integrateWorktree;
4287
+ void this.applyTaskFailure;
4288
+ void this.retryMap;
4289
+ void this.persistRetries;
4290
+ void this.maxRetries;
4291
+ void this.trySupervisorRescue;
4292
+ void this.supervisorEscalations;
4293
+ void this.maxSupervisorEscalations;
4294
+ void this.setTaskModel;
4295
+ void this.splitTask;
4296
+ return this;
4297
+ }
4065
4298
  };
4066
4299
 
4067
4300
  // src/start-sdd-run.ts
@@ -4836,11 +5069,17 @@ function renderTaskGraph(graph, opts) {
4836
5069
  lines.push(renderProgress(progress));
4837
5070
  lines.push("");
4838
5071
  const childrenMap = /* @__PURE__ */ new Map();
5072
+ const dependentsMap = /* @__PURE__ */ new Map();
4839
5073
  for (const edge of graph.edges) {
4840
5074
  if (edge.type === "depends_on") {
4841
5075
  const deps = childrenMap.get(edge.from) ?? [];
4842
5076
  deps.push(edge.to);
4843
5077
  childrenMap.set(edge.from, deps);
5078
+ if (graph.nodes.has(edge.from)) {
5079
+ const dependents = dependentsMap.get(edge.to) ?? [];
5080
+ dependents.push(edge.from);
5081
+ dependentsMap.set(edge.to, dependents);
5082
+ }
4844
5083
  }
4845
5084
  }
4846
5085
  const rendered = /* @__PURE__ */ new Set();
@@ -4850,18 +5089,18 @@ function renderTaskGraph(graph, opts) {
4850
5089
  return !deps || deps.length === 0;
4851
5090
  });
4852
5091
  for (const rootId of startNodes) {
4853
- renderNode(graph, rootId, lines, rendered, childrenMap, compact, "");
5092
+ renderNode(graph, rootId, lines, rendered, childrenMap, dependentsMap, compact, "");
4854
5093
  }
4855
5094
  for (const [id] of graph.nodes) {
4856
5095
  if (!rendered.has(id)) {
4857
- renderNode(graph, id, lines, rendered, childrenMap, compact, "");
5096
+ renderNode(graph, id, lines, rendered, childrenMap, dependentsMap, compact, "");
4858
5097
  }
4859
5098
  }
4860
5099
  lines.push("");
4861
5100
  lines.push("Legend: \u25CF done \u25D0 in-progress \u25CB pending \u2297 blocked \u2717 failed \u25D2 review");
4862
5101
  return lines.join("\n");
4863
5102
  }
4864
- function renderNode(graph, nodeId, lines, rendered, childrenMap, compact, prefix) {
5103
+ function renderNode(graph, nodeId, lines, rendered, childrenMap, dependentsMap, compact, prefix) {
4865
5104
  if (rendered.has(nodeId)) return;
4866
5105
  rendered.add(nodeId);
4867
5106
  const node = expectDefined4(graph.nodes.get(nodeId));
@@ -4878,9 +5117,9 @@ function renderNode(graph, nodeId, lines, rendered, childrenMap, compact, prefix
4878
5117
  lines.push(`${prefix} \u2514 ${truncate(dl, 60)}`);
4879
5118
  }
4880
5119
  }
4881
- const dependents = graph.edges.filter((e) => e.type === "depends_on" && e.to === nodeId).map((e) => e.from).filter((id) => graph.nodes.has(id));
5120
+ const dependents = dependentsMap.get(nodeId) ?? [];
4882
5121
  for (const depId of dependents) {
4883
- renderNode(graph, depId, lines, rendered, childrenMap, compact, prefix + " ");
5122
+ renderNode(graph, depId, lines, rendered, childrenMap, dependentsMap, compact, prefix + " ");
4884
5123
  }
4885
5124
  }
4886
5125
  function renderProgress(progress) {
@@ -4991,24 +5230,28 @@ function analyzeCriticalPath(graph) {
4991
5230
  for (const node of nodes) {
4992
5231
  if (node.status === "completed") continue;
4993
5232
  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);
5233
+ const activeDownstream = Array.from(downstream).filter((id) => {
5234
+ const downstreamNode = graph.nodes.get(id);
5235
+ return downstreamNode?.status !== "completed";
5236
+ });
5237
+ if (activeDownstream.length > 0) {
5238
+ const blockedHours = activeDownstream.reduce(
5239
+ (sum, id) => sum + taskEstimateHours(graph, id, 0),
5240
+ 0
5241
+ );
4999
5242
  bottlenecks.push({
5000
5243
  taskId: node.id,
5001
5244
  title: node.title,
5002
- blockedCount: downstream.size,
5245
+ blockedCount: activeDownstream.length,
5003
5246
  blockedHours,
5004
- severity: Math.min(100, Math.round(downstream.size / nodes.length * 100))
5247
+ severity: Math.min(100, Math.round(activeDownstream.length / nodes.length * 100))
5005
5248
  });
5006
5249
  }
5007
5250
  }
5008
5251
  bottlenecks.sort((a, b) => b.severity - a.severity);
5009
5252
  const criticalPath = computeCriticalPath(graph, topoOrder, blockedByMap);
5010
5253
  const totalHours = criticalPath.reduce((sum, id) => {
5011
- return sum + (graph.nodes.get(id).estimateHours ?? 0);
5254
+ return sum + taskEstimateHours(graph, id, 0);
5012
5255
  }, 0);
5013
5256
  const parallelGroups = computeParallelGroups(graph, blockedByMap);
5014
5257
  const executionOrder = topoOrder.filter((id) => {
@@ -5047,7 +5290,7 @@ function computeCriticalPath(graph, _topoOrder, blockedByMap) {
5047
5290
  const dist = /* @__PURE__ */ new Map();
5048
5291
  const prev = /* @__PURE__ */ new Map();
5049
5292
  for (const id of allIds) {
5050
- dist.set(id, graph.nodes.get(id)?.estimateHours ?? 1);
5293
+ dist.set(id, taskEstimateHours(graph, id, 1));
5051
5294
  prev.set(id, null);
5052
5295
  }
5053
5296
  const blocksMap = /* @__PURE__ */ new Map();
@@ -5064,7 +5307,7 @@ function computeCriticalPath(graph, _topoOrder, blockedByMap) {
5064
5307
  const blocked = blocksMap.get(id);
5065
5308
  if (!blocked) continue;
5066
5309
  for (const blockedId of blocked) {
5067
- const candidateDist = dist.get(id) + (graph.nodes.get(blockedId)?.estimateHours ?? 1);
5310
+ const candidateDist = dist.get(id) + taskEstimateHours(graph, blockedId, 1);
5068
5311
  if (candidateDist > (dist.get(blockedId) ?? 0)) {
5069
5312
  dist.set(blockedId, candidateDist);
5070
5313
  prev.set(blockedId, id);
@@ -5093,6 +5336,10 @@ function computeCriticalPath(graph, _topoOrder, blockedByMap) {
5093
5336
  }
5094
5337
  return path7;
5095
5338
  }
5339
+ function taskEstimateHours(graph, taskId, fallback) {
5340
+ const value = graph.nodes.get(taskId)?.estimateHours;
5341
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : fallback;
5342
+ }
5096
5343
  function computeParallelGroups(graph, blockedByMap) {
5097
5344
  const groups = [];
5098
5345
  const assigned = new Set(
@@ -5143,21 +5390,23 @@ var SpecVersioning = class {
5143
5390
  const history = this.versions.get(spec.id) ?? [];
5144
5391
  history.push(version);
5145
5392
  this.versions.set(spec.id, history);
5146
- return version;
5393
+ return structuredClone(version);
5147
5394
  }
5148
5395
  /** Get version history for a spec. */
5149
5396
  getHistory(specId) {
5150
- return this.versions.get(specId) ?? [];
5397
+ return structuredClone(this.versions.get(specId) ?? []);
5151
5398
  }
5152
5399
  /** Get a specific version of a spec. */
5153
5400
  getVersion(specId, version) {
5154
5401
  const history = this.versions.get(specId) ?? [];
5155
- return history.find((v) => v.version === version);
5402
+ const found = history.find((v) => v.version === version);
5403
+ return found ? structuredClone(found) : void 0;
5156
5404
  }
5157
5405
  /** Get the latest version of a spec. */
5158
5406
  getLatest(specId) {
5159
5407
  const history = this.versions.get(specId) ?? [];
5160
- return history[history.length - 1];
5408
+ const latest = history[history.length - 1];
5409
+ return latest ? structuredClone(latest) : void 0;
5161
5410
  }
5162
5411
  /** Compute diff between two versions of a spec. */
5163
5412
  diff(oldSpec, newSpec) {
@@ -5245,6 +5494,17 @@ var SpecVersioning = class {
5245
5494
  task.title = mod.requirement.description;
5246
5495
  task.description = this.buildTaskDescription(mod.requirement);
5247
5496
  task.priority = mod.requirement.priority;
5497
+ const metadataTags = /* @__PURE__ */ new Set([
5498
+ mod.previousVersion.type,
5499
+ mod.previousVersion.priority,
5500
+ mod.requirement.type,
5501
+ mod.requirement.priority
5502
+ ]);
5503
+ task.tags = [
5504
+ ...(task.tags ?? []).filter((tag) => !metadataTags.has(tag)),
5505
+ mod.requirement.type,
5506
+ mod.requirement.priority
5507
+ ];
5248
5508
  task.updatedAt = Date.now();
5249
5509
  changes.push(`Updated task: ${task.title} (${mod.changes.join(", ")})`);
5250
5510
  }
@@ -5267,6 +5527,9 @@ var SpecVersioning = class {
5267
5527
  graph.rootNodes.push(newTask.id);
5268
5528
  changes.push(`Added task: ${newTask.title}`);
5269
5529
  }
5530
+ graph.requiredRequirementIds = Array.from(
5531
+ new Set(newSpec.requirements.map((requirement) => requirement.id))
5532
+ );
5270
5533
  graph.updatedAt = Date.now();
5271
5534
  return { graph, changes };
5272
5535
  }
@@ -5930,8 +6193,10 @@ function resolveConflictText(text, side) {
5930
6193
  }
5931
6194
  if (isMarker(line, "|", size)) state = "base";
5932
6195
  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);
6196
+ else if (isMarker(line, ">", size)) {
6197
+ if (state !== "theirs") return text;
6198
+ state = "normal";
6199
+ } else if (state === "ours" && side === "base") out.push(line);
5935
6200
  else if (state === "theirs" && side === "incoming") out.push(line);
5936
6201
  }
5937
6202
  if (state !== "normal") return text;