@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 +709 -444
- package/dist/sdd-board-store.d.ts +2 -0
- package/dist/sdd-parallel-run.d.ts +10 -0
- package/dist/sdd-task-decomposer.d.ts +1 -1
- package/dist/sdd-task-dispatch.d.ts +41 -0
- package/dist/spec-store.d.ts +6 -0
- package/dist/task-graph-store.d.ts +5 -0
- package/package.json +5 -5
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-${
|
|
101
|
-
if (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 (
|
|
138
|
+
if (lower.includes("[critical]") || lower.includes("[prio:high]")) {
|
|
123
139
|
priority = "critical";
|
|
124
|
-
} else if (
|
|
140
|
+
} else if (lower.includes("[high]")) {
|
|
125
141
|
priority = "high";
|
|
126
|
-
} else if (
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
530
|
-
const id = ready
|
|
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, "
|
|
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
|
-
|
|
755
|
-
|
|
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
|
-
|
|
775
|
-
|
|
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
|
|
778
|
-
}
|
|
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
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
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)
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1013
|
+
let filePath;
|
|
928
1014
|
try {
|
|
929
|
-
|
|
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)
|
|
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
|
|
1092
|
+
return this.rebuildIndex();
|
|
986
1093
|
}
|
|
987
|
-
async
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
)
|
|
992
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
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
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
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
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
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
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
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
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
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
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
3681
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
4995
|
-
const
|
|
4996
|
-
|
|
4997
|
-
|
|
4998
|
-
|
|
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:
|
|
5245
|
+
blockedCount: activeDownstream.length,
|
|
5003
5246
|
blockedHours,
|
|
5004
|
-
severity: Math.min(100, Math.round(
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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))
|
|
5934
|
-
|
|
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;
|