@wrongstack/core 0.4.1 → 0.5.0
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/{agent-bridge-DaOnnHNW.d.ts → agent-bridge-BkjOkSkD.d.ts} +1 -1
- package/dist/{compactor-CFky6JKM.d.ts → compactor-CTHjZAwf.d.ts} +1 -1
- package/dist/{config-RlhKLjme.d.ts → config-DO2eCKgq.d.ts} +1 -1
- package/dist/{context-B1kBj1lY.d.ts → context-P207fTpo.d.ts} +11 -0
- package/dist/coordination/index.d.ts +10 -891
- package/dist/coordination/index.js +407 -236
- package/dist/coordination/index.js.map +1 -1
- package/dist/defaults/index.d.ts +19 -18
- package/dist/defaults/index.js +2139 -284
- package/dist/defaults/index.js.map +1 -1
- package/dist/{events-BBaKeMfb.d.ts → events-DYAbU84e.d.ts} +41 -1
- package/dist/execution/index.d.ts +19 -13
- package/dist/execution/index.js +15 -6
- package/dist/execution/index.js.map +1 -1
- package/dist/extension/index.d.ts +6 -6
- package/dist/index-BHL8QDCL.d.ts +899 -0
- package/dist/{index-meJRuQtc.d.ts → index-BNzUUDVR.d.ts} +8 -6
- package/dist/index.d.ts +30 -27
- package/dist/index.js +3764 -1446
- package/dist/index.js.map +1 -1
- package/dist/infrastructure/index.d.ts +6 -6
- package/dist/infrastructure/index.js +67 -2
- package/dist/infrastructure/index.js.map +1 -1
- package/dist/kernel/index.d.ts +10 -9
- package/dist/kernel/index.js.map +1 -1
- package/dist/{mcp-servers-Cf4-bJnd.d.ts → mcp-servers-CLkhKdnF.d.ts} +18 -3
- package/dist/models/index.d.ts +2 -2
- package/dist/models/index.js +106 -0
- package/dist/models/index.js.map +1 -1
- package/dist/{multi-agent-D5m66hzB.d.ts → multi-agent-B1Nn9eC0.d.ts} +71 -16
- package/dist/observability/index.d.ts +2 -2
- package/dist/{path-resolver-Bg4OP5fi.d.ts → path-resolver-mPccVA0l.d.ts} +9 -2
- package/dist/{provider-runner-CU9gnU2E.d.ts → provider-runner-D7lHu07L.d.ts} +3 -3
- package/dist/{skill-DayhFUBb.d.ts → retry-policy-CnYVKGPv.d.ts} +2 -28
- package/dist/sdd/index.d.ts +419 -5
- package/dist/sdd/index.js +1580 -1
- package/dist/sdd/index.js.map +1 -1
- package/dist/{secret-scrubber-Dyh5LVYL.d.ts → secret-scrubber-BSgec8CU.d.ts} +1 -1
- package/dist/{secret-scrubber-DyUAWS09.d.ts → secret-scrubber-Buo9zLGs.d.ts} +1 -1
- package/dist/security/index.d.ts +8 -4
- package/dist/security/index.js +8 -0
- package/dist/security/index.js.map +1 -1
- package/dist/{selector-DBEiwXBk.d.ts → selector-B-CGOshv.d.ts} +1 -1
- package/dist/{session-reader-D-z0AgHs.d.ts → session-reader-DmOEmSkq.d.ts} +1 -1
- package/dist/skill-CxuWrsKK.d.ts +29 -0
- package/dist/skills/index.d.ts +136 -0
- package/dist/skills/index.js +478 -0
- package/dist/skills/index.js.map +1 -0
- package/dist/storage/index.d.ts +5 -5
- package/dist/{system-prompt-DNetCecu.d.ts → system-prompt-CzY1zrbC.d.ts} +1 -1
- package/dist/{tool-executor-B5bgmEgE.d.ts → tool-executor-CYe5cp5U.d.ts} +4 -4
- package/dist/types/index.d.ts +16 -15
- package/dist/types/index.js +116 -0
- package/dist/types/index.js.map +1 -1
- package/dist/utils/index.d.ts +35 -2
- package/dist/utils/index.js +49 -1
- package/dist/utils/index.js.map +1 -1
- package/package.json +5 -1
- package/skills/sdd/SKILL.md +93 -6
- package/skills/skill-creator/SKILL.md +98 -0
package/dist/sdd/index.js
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import * as fs from 'fs/promises';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { randomUUID, randomBytes } from 'crypto';
|
|
4
|
+
|
|
1
5
|
// src/sdd/spec-parser.ts
|
|
2
6
|
var SpecParser = class {
|
|
3
7
|
parse(content) {
|
|
@@ -460,6 +464,27 @@ function computeTaskProgress(graph) {
|
|
|
460
464
|
actualHours
|
|
461
465
|
};
|
|
462
466
|
}
|
|
467
|
+
function topologicalSort(graph) {
|
|
468
|
+
const visited = /* @__PURE__ */ new Set();
|
|
469
|
+
const inStack = /* @__PURE__ */ new Set();
|
|
470
|
+
const result = [];
|
|
471
|
+
function visit(id) {
|
|
472
|
+
if (inStack.has(id)) return;
|
|
473
|
+
if (visited.has(id)) return;
|
|
474
|
+
if (!graph.nodes.has(id)) return;
|
|
475
|
+
visited.add(id);
|
|
476
|
+
inStack.add(id);
|
|
477
|
+
for (const edge of graph.edges) {
|
|
478
|
+
if (edge.from === id) visit(edge.to);
|
|
479
|
+
}
|
|
480
|
+
inStack.delete(id);
|
|
481
|
+
result.push(id);
|
|
482
|
+
}
|
|
483
|
+
for (const rootId of graph.rootNodes) {
|
|
484
|
+
visit(rootId);
|
|
485
|
+
}
|
|
486
|
+
return result;
|
|
487
|
+
}
|
|
463
488
|
|
|
464
489
|
// src/sdd/task-tracker.ts
|
|
465
490
|
var TaskTracker = class {
|
|
@@ -858,7 +883,1561 @@ var SpecDrivenDev = class {
|
|
|
858
883
|
}));
|
|
859
884
|
}
|
|
860
885
|
};
|
|
886
|
+
async function atomicWrite(targetPath, content, opts = {}) {
|
|
887
|
+
const dir = path.dirname(targetPath);
|
|
888
|
+
await fs.mkdir(dir, { recursive: true });
|
|
889
|
+
const tmp = path.join(dir, `.${path.basename(targetPath)}.${randomBytes(6).toString("hex")}.tmp`);
|
|
890
|
+
try {
|
|
891
|
+
if (typeof content === "string") {
|
|
892
|
+
await fs.writeFile(tmp, content, { flag: "wx", encoding: opts.encoding ?? "utf8" });
|
|
893
|
+
} else {
|
|
894
|
+
await fs.writeFile(tmp, content, { flag: "wx" });
|
|
895
|
+
}
|
|
896
|
+
try {
|
|
897
|
+
const fh = await fs.open(tmp, "r+");
|
|
898
|
+
try {
|
|
899
|
+
await fh.sync();
|
|
900
|
+
} finally {
|
|
901
|
+
await fh.close();
|
|
902
|
+
}
|
|
903
|
+
} catch {
|
|
904
|
+
}
|
|
905
|
+
let mode;
|
|
906
|
+
try {
|
|
907
|
+
const stat2 = await fs.stat(targetPath);
|
|
908
|
+
mode = stat2.mode & 511;
|
|
909
|
+
} catch {
|
|
910
|
+
mode = opts.mode;
|
|
911
|
+
}
|
|
912
|
+
if (mode !== void 0) {
|
|
913
|
+
await fs.chmod(tmp, mode);
|
|
914
|
+
}
|
|
915
|
+
await renameWithRetry(tmp, targetPath);
|
|
916
|
+
} catch (err) {
|
|
917
|
+
try {
|
|
918
|
+
await fs.unlink(tmp);
|
|
919
|
+
} catch {
|
|
920
|
+
}
|
|
921
|
+
throw err;
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
async function ensureDir(dir) {
|
|
925
|
+
await fs.mkdir(dir, { recursive: true });
|
|
926
|
+
}
|
|
927
|
+
var TRANSIENT_RENAME_CODES = /* @__PURE__ */ new Set(["EPERM", "EBUSY", "EACCES", "ENOTEMPTY"]);
|
|
928
|
+
async function renameWithRetry(from, to) {
|
|
929
|
+
if (process.platform !== "win32") {
|
|
930
|
+
await fs.rename(from, to);
|
|
931
|
+
return;
|
|
932
|
+
}
|
|
933
|
+
const delays = [10, 25, 60, 120, 250];
|
|
934
|
+
let lastErr;
|
|
935
|
+
for (let i = 0; i <= delays.length; i++) {
|
|
936
|
+
try {
|
|
937
|
+
await fs.rename(from, to);
|
|
938
|
+
return;
|
|
939
|
+
} catch (err) {
|
|
940
|
+
lastErr = err;
|
|
941
|
+
const code = err?.code;
|
|
942
|
+
if (!code || !TRANSIENT_RENAME_CODES.has(code) || i === delays.length) {
|
|
943
|
+
throw err;
|
|
944
|
+
}
|
|
945
|
+
await new Promise((resolve) => setTimeout(resolve, delays[i]));
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
throw lastErr;
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
// src/sdd/spec-store.ts
|
|
952
|
+
var SpecStore = class {
|
|
953
|
+
baseDir;
|
|
954
|
+
indexPath;
|
|
955
|
+
constructor(opts) {
|
|
956
|
+
this.baseDir = opts.baseDir;
|
|
957
|
+
this.indexPath = path.join(this.baseDir, "_index.json");
|
|
958
|
+
}
|
|
959
|
+
async save(spec) {
|
|
960
|
+
await ensureDir(this.baseDir);
|
|
961
|
+
const filePath = this.filePath(spec.id);
|
|
962
|
+
await atomicWrite(filePath, JSON.stringify(spec, null, 2), { mode: 384 });
|
|
963
|
+
await this.updateIndex(spec);
|
|
964
|
+
}
|
|
965
|
+
async load(id) {
|
|
966
|
+
try {
|
|
967
|
+
const raw = await fs.readFile(this.filePath(id), "utf8");
|
|
968
|
+
return JSON.parse(raw);
|
|
969
|
+
} catch {
|
|
970
|
+
return null;
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
async list() {
|
|
974
|
+
const index = await this.readIndex();
|
|
975
|
+
return index.entries.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
976
|
+
}
|
|
977
|
+
async delete(id) {
|
|
978
|
+
try {
|
|
979
|
+
await fs.unlink(this.filePath(id));
|
|
980
|
+
await this.removeFromIndex(id);
|
|
981
|
+
return true;
|
|
982
|
+
} catch {
|
|
983
|
+
return false;
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
async exists(id) {
|
|
987
|
+
try {
|
|
988
|
+
await fs.access(this.filePath(id));
|
|
989
|
+
return true;
|
|
990
|
+
} catch {
|
|
991
|
+
return false;
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
/** Create a new spec with defaults, assign ID, and persist. */
|
|
995
|
+
async createDraft(title, overview) {
|
|
996
|
+
const now = Date.now();
|
|
997
|
+
const spec = {
|
|
998
|
+
id: randomUUID(),
|
|
999
|
+
title,
|
|
1000
|
+
version: "0.1.0",
|
|
1001
|
+
status: "draft",
|
|
1002
|
+
overview: overview ?? "",
|
|
1003
|
+
sections: [],
|
|
1004
|
+
requirements: [],
|
|
1005
|
+
createdAt: now,
|
|
1006
|
+
updatedAt: now
|
|
1007
|
+
};
|
|
1008
|
+
await this.save(spec);
|
|
1009
|
+
return spec;
|
|
1010
|
+
}
|
|
1011
|
+
/** Update spec fields and persist. */
|
|
1012
|
+
async update(id, patch) {
|
|
1013
|
+
const spec = await this.load(id);
|
|
1014
|
+
if (!spec) return null;
|
|
1015
|
+
const updated = {
|
|
1016
|
+
...spec,
|
|
1017
|
+
...patch,
|
|
1018
|
+
id: spec.id,
|
|
1019
|
+
createdAt: spec.createdAt,
|
|
1020
|
+
updatedAt: Date.now()
|
|
1021
|
+
};
|
|
1022
|
+
await this.save(updated);
|
|
1023
|
+
return updated;
|
|
1024
|
+
}
|
|
1025
|
+
filePath(id) {
|
|
1026
|
+
return path.join(this.baseDir, `${id}.json`);
|
|
1027
|
+
}
|
|
1028
|
+
async readIndex() {
|
|
1029
|
+
try {
|
|
1030
|
+
const raw = await fs.readFile(this.indexPath, "utf8");
|
|
1031
|
+
const parsed = JSON.parse(raw);
|
|
1032
|
+
if (parsed?.version === 1) return parsed;
|
|
1033
|
+
} catch {
|
|
1034
|
+
}
|
|
1035
|
+
return { version: 1, entries: [] };
|
|
1036
|
+
}
|
|
1037
|
+
async updateIndex(spec) {
|
|
1038
|
+
const index = await this.readIndex();
|
|
1039
|
+
const entry = {
|
|
1040
|
+
id: spec.id,
|
|
1041
|
+
title: spec.title,
|
|
1042
|
+
version: spec.version,
|
|
1043
|
+
status: spec.status,
|
|
1044
|
+
updatedAt: spec.updatedAt,
|
|
1045
|
+
filePath: this.filePath(spec.id)
|
|
1046
|
+
};
|
|
1047
|
+
const idx = index.entries.findIndex((e) => e.id === spec.id);
|
|
1048
|
+
if (idx >= 0) {
|
|
1049
|
+
index.entries[idx] = entry;
|
|
1050
|
+
} else {
|
|
1051
|
+
index.entries.push(entry);
|
|
1052
|
+
}
|
|
1053
|
+
await atomicWrite(this.indexPath, JSON.stringify(index, null, 2), { mode: 384 });
|
|
1054
|
+
}
|
|
1055
|
+
async removeFromIndex(id) {
|
|
1056
|
+
const index = await this.readIndex();
|
|
1057
|
+
index.entries = index.entries.filter((e) => e.id !== id);
|
|
1058
|
+
await atomicWrite(this.indexPath, JSON.stringify(index, null, 2), { mode: 384 });
|
|
1059
|
+
}
|
|
1060
|
+
};
|
|
1061
|
+
function graphToJSON(graph) {
|
|
1062
|
+
const serialisable = {
|
|
1063
|
+
...graph,
|
|
1064
|
+
nodes: Array.from(graph.nodes.entries())
|
|
1065
|
+
};
|
|
1066
|
+
return JSON.stringify(serialisable, null, 2);
|
|
1067
|
+
}
|
|
1068
|
+
function graphFromJSON(raw) {
|
|
1069
|
+
const parsed = JSON.parse(raw);
|
|
1070
|
+
return {
|
|
1071
|
+
...parsed,
|
|
1072
|
+
nodes: new Map(parsed.nodes)
|
|
1073
|
+
};
|
|
1074
|
+
}
|
|
1075
|
+
var TaskGraphStore = class {
|
|
1076
|
+
baseDir;
|
|
1077
|
+
indexPath;
|
|
1078
|
+
constructor(opts) {
|
|
1079
|
+
this.baseDir = opts.baseDir;
|
|
1080
|
+
this.indexPath = path.join(this.baseDir, "_index.json");
|
|
1081
|
+
}
|
|
1082
|
+
async save(graph) {
|
|
1083
|
+
await ensureDir(this.baseDir);
|
|
1084
|
+
const filePath = this.filePath(graph.id);
|
|
1085
|
+
await atomicWrite(filePath, graphToJSON(graph), { mode: 384 });
|
|
1086
|
+
await this.updateIndex(graph);
|
|
1087
|
+
}
|
|
1088
|
+
async load(id) {
|
|
1089
|
+
try {
|
|
1090
|
+
const raw = await fs.readFile(this.filePath(id), "utf8");
|
|
1091
|
+
return graphFromJSON(raw);
|
|
1092
|
+
} catch {
|
|
1093
|
+
return null;
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
async list() {
|
|
1097
|
+
const index = await this.readIndex();
|
|
1098
|
+
return index.entries.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
1099
|
+
}
|
|
1100
|
+
async delete(id) {
|
|
1101
|
+
try {
|
|
1102
|
+
await fs.unlink(this.filePath(id));
|
|
1103
|
+
await this.removeFromIndex(id);
|
|
1104
|
+
return true;
|
|
1105
|
+
} catch {
|
|
1106
|
+
return false;
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
async exists(id) {
|
|
1110
|
+
try {
|
|
1111
|
+
await fs.access(this.filePath(id));
|
|
1112
|
+
return true;
|
|
1113
|
+
} catch {
|
|
1114
|
+
return false;
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
filePath(id) {
|
|
1118
|
+
return path.join(this.baseDir, `${id}.json`);
|
|
1119
|
+
}
|
|
1120
|
+
async readIndex() {
|
|
1121
|
+
try {
|
|
1122
|
+
const raw = await fs.readFile(this.indexPath, "utf8");
|
|
1123
|
+
const parsed = JSON.parse(raw);
|
|
1124
|
+
if (parsed?.version === 1) return parsed;
|
|
1125
|
+
} catch {
|
|
1126
|
+
}
|
|
1127
|
+
return { version: 1, entries: [] };
|
|
1128
|
+
}
|
|
1129
|
+
async updateIndex(graph) {
|
|
1130
|
+
const index = await this.readIndex();
|
|
1131
|
+
const completedCount = Array.from(graph.nodes.values()).filter(
|
|
1132
|
+
(n) => n.status === "completed"
|
|
1133
|
+
).length;
|
|
1134
|
+
const entry = {
|
|
1135
|
+
id: graph.id,
|
|
1136
|
+
specId: graph.specId,
|
|
1137
|
+
title: graph.title,
|
|
1138
|
+
nodeCount: graph.nodes.size,
|
|
1139
|
+
completedCount,
|
|
1140
|
+
updatedAt: graph.updatedAt,
|
|
1141
|
+
filePath: this.filePath(graph.id)
|
|
1142
|
+
};
|
|
1143
|
+
const idx = index.entries.findIndex((e) => e.id === graph.id);
|
|
1144
|
+
if (idx >= 0) {
|
|
1145
|
+
index.entries[idx] = entry;
|
|
1146
|
+
} else {
|
|
1147
|
+
index.entries.push(entry);
|
|
1148
|
+
}
|
|
1149
|
+
await atomicWrite(this.indexPath, JSON.stringify(index, null, 2), { mode: 384 });
|
|
1150
|
+
}
|
|
1151
|
+
async removeFromIndex(id) {
|
|
1152
|
+
const index = await this.readIndex();
|
|
1153
|
+
index.entries = index.entries.filter((e) => e.id !== id);
|
|
1154
|
+
await atomicWrite(this.indexPath, JSON.stringify(index, null, 2), { mode: 384 });
|
|
1155
|
+
}
|
|
1156
|
+
};
|
|
1157
|
+
|
|
1158
|
+
// src/sdd/spec-builder.ts
|
|
1159
|
+
function buildQuestioningPrompt(session, min, max) {
|
|
1160
|
+
const answered = session.answers.length;
|
|
1161
|
+
const remaining = Math.max(0, min - answered);
|
|
1162
|
+
const budget = max - answered;
|
|
1163
|
+
const lines = [
|
|
1164
|
+
`\u2550\u2550\u2550 SDD Spec Builder \u2550\u2550\u2550`,
|
|
1165
|
+
`Feature: "${session.title}"`,
|
|
1166
|
+
session.userIntent ? `Intent: ${session.userIntent}` : "",
|
|
1167
|
+
`Phase: Questioning (${answered} answered, ${budget} remaining budget)`,
|
|
1168
|
+
"",
|
|
1169
|
+
"**Instructions for AI:**",
|
|
1170
|
+
"",
|
|
1171
|
+
"You are conducting a specification interview. Your job is to ask the user",
|
|
1172
|
+
"intelligent, contextual questions to understand what they want to build.",
|
|
1173
|
+
"",
|
|
1174
|
+
`You have asked ${answered} questions so far.`
|
|
1175
|
+
];
|
|
1176
|
+
if (remaining > 0) {
|
|
1177
|
+
lines.push(`You MUST ask at least ${remaining} more question(s) before generating the spec.`);
|
|
1178
|
+
} else if (budget <= 0) {
|
|
1179
|
+
lines.push("You have reached the maximum question budget. Generate the spec NOW.");
|
|
1180
|
+
} else {
|
|
1181
|
+
lines.push(
|
|
1182
|
+
"You may ask more questions if needed, or generate the spec if you have enough information.",
|
|
1183
|
+
"Ask a question ONLY if it reveals something you genuinely need to know."
|
|
1184
|
+
);
|
|
1185
|
+
}
|
|
1186
|
+
lines.push(
|
|
1187
|
+
"",
|
|
1188
|
+
"**Rules:**",
|
|
1189
|
+
"- Ask ONE question at a time",
|
|
1190
|
+
"- Questions must be specific and contextual \u2014 never generic",
|
|
1191
|
+
"- Adapt based on previous answers",
|
|
1192
|
+
"- Cover: scope, constraints, edge cases, integrations, security, performance as relevant",
|
|
1193
|
+
"- When you have enough info, respond with the full specification in JSON format",
|
|
1194
|
+
"",
|
|
1195
|
+
`**Question budget:** ${budget}/${max} remaining`,
|
|
1196
|
+
`**Minimum required:** ${remaining > 0 ? remaining : "met"}`
|
|
1197
|
+
);
|
|
1198
|
+
if (session.projectContext) {
|
|
1199
|
+
lines.push("", "**Project Context:**", "```", session.projectContext, "```");
|
|
1200
|
+
}
|
|
1201
|
+
if (answered > 0) {
|
|
1202
|
+
lines.push("", "**Conversation so far:**");
|
|
1203
|
+
for (let i = 0; i < answered; i++) {
|
|
1204
|
+
const a = session.answers[i];
|
|
1205
|
+
lines.push(``, `Q${i + 1}: ${a.question}`, `A${i + 1}: ${a.answer}`);
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
lines.push(
|
|
1209
|
+
"",
|
|
1210
|
+
"---",
|
|
1211
|
+
"Now either:",
|
|
1212
|
+
`1. Ask your next question (if you need more info)`,
|
|
1213
|
+
`2. Generate the complete specification as JSON (if ready)`,
|
|
1214
|
+
"",
|
|
1215
|
+
"If generating spec, output JSON inside ```json code block with this structure:",
|
|
1216
|
+
"```json",
|
|
1217
|
+
"{",
|
|
1218
|
+
' "title": "...",',
|
|
1219
|
+
' "overview": "...",',
|
|
1220
|
+
' "sections": [{ "type": "overview|requirements|architecture|api|data|security|acceptance", "title": "...", "content": "...", "level": 1 }],',
|
|
1221
|
+
' "requirements": [{ "id": "REQ-1", "type": "functional|non-functional|security|performance|ux", "priority": "critical|high|medium|low", "description": "...", "acceptanceCriteria": ["..."] }]',
|
|
1222
|
+
"}",
|
|
1223
|
+
"```"
|
|
1224
|
+
);
|
|
1225
|
+
return lines.filter(Boolean).join("\n");
|
|
1226
|
+
}
|
|
1227
|
+
function buildSpecReviewPrompt(session) {
|
|
1228
|
+
const spec = session.spec;
|
|
1229
|
+
if (!spec) return "No spec generated yet.";
|
|
1230
|
+
const reqSummary = spec.requirements.map((r) => ` [${r.priority}] ${r.description}`).join("\n");
|
|
1231
|
+
return [
|
|
1232
|
+
`\u2550\u2550\u2550 Spec Review \u2550\u2550\u2550`,
|
|
1233
|
+
`Feature: "${spec.title}"`,
|
|
1234
|
+
`Requirements: ${spec.requirements.length}`,
|
|
1235
|
+
"",
|
|
1236
|
+
"**Specification:**",
|
|
1237
|
+
spec.overview,
|
|
1238
|
+
"",
|
|
1239
|
+
"**Requirements:**",
|
|
1240
|
+
reqSummary,
|
|
1241
|
+
"",
|
|
1242
|
+
"---",
|
|
1243
|
+
"Approve this spec? The AI will then generate an implementation plan and tasks.",
|
|
1244
|
+
'Say "approve" to proceed, or describe what needs to change.'
|
|
1245
|
+
].join("\n");
|
|
1246
|
+
}
|
|
1247
|
+
function buildImplementationPrompt(session) {
|
|
1248
|
+
const spec = session.spec;
|
|
1249
|
+
if (!spec) return "No spec to implement.";
|
|
1250
|
+
const reqList = spec.requirements.map((r) => ` - [${r.priority}] ${r.description}`).join("\n");
|
|
1251
|
+
return [
|
|
1252
|
+
`\u2550\u2550\u2550 Implementation Planning \u2550\u2550\u2550`,
|
|
1253
|
+
`Feature: "${spec.title}"`,
|
|
1254
|
+
`Requirements: ${spec.requirements.length}`,
|
|
1255
|
+
"",
|
|
1256
|
+
"**Requirements to implement:**",
|
|
1257
|
+
reqList,
|
|
1258
|
+
"",
|
|
1259
|
+
"**Instructions for AI:**",
|
|
1260
|
+
"Generate a detailed implementation plan for this specification.",
|
|
1261
|
+
"Include:",
|
|
1262
|
+
"1. Architecture decisions",
|
|
1263
|
+
"2. File structure changes",
|
|
1264
|
+
"3. Key implementation details",
|
|
1265
|
+
"4. Dependency requirements",
|
|
1266
|
+
"5. Testing strategy",
|
|
1267
|
+
"",
|
|
1268
|
+
"**IMPORTANT:** After the plan, you MUST generate executable tasks as a JSON array.",
|
|
1269
|
+
"Each task should be a concrete, actionable step. Output the JSON inside a ```json code block:",
|
|
1270
|
+
"```json",
|
|
1271
|
+
"[",
|
|
1272
|
+
" {",
|
|
1273
|
+
' "title": "Create auth middleware",',
|
|
1274
|
+
' "description": "Implement JWT verification middleware for protected routes",',
|
|
1275
|
+
' "type": "feature",',
|
|
1276
|
+
' "priority": "critical",',
|
|
1277
|
+
' "estimateHours": 3,',
|
|
1278
|
+
' "tags": ["auth", "middleware"]',
|
|
1279
|
+
" },",
|
|
1280
|
+
" {",
|
|
1281
|
+
' "title": "Write auth tests",',
|
|
1282
|
+
' "description": "Unit and integration tests for authentication flow",',
|
|
1283
|
+
' "type": "test",',
|
|
1284
|
+
' "priority": "high",',
|
|
1285
|
+
' "estimateHours": 2,',
|
|
1286
|
+
' "tags": ["test", "auth"]',
|
|
1287
|
+
" }",
|
|
1288
|
+
"]",
|
|
1289
|
+
"```",
|
|
1290
|
+
"",
|
|
1291
|
+
"Rules:",
|
|
1292
|
+
"- Each task must be independently executable",
|
|
1293
|
+
"- Order tasks by dependency (things that block others come first)",
|
|
1294
|
+
'- Use type: "feature" for code, "test" for tests, "docs" for documentation, "chore" for config',
|
|
1295
|
+
'- Use priority: "critical" for blockers, "high" for core features, "medium" for nice-to-haves, "low" for polish'
|
|
1296
|
+
].join("\n");
|
|
1297
|
+
}
|
|
1298
|
+
function buildTaskReviewPrompt(session) {
|
|
1299
|
+
return [
|
|
1300
|
+
`\u2550\u2550\u2550 Task Review \u2550\u2550\u2550`,
|
|
1301
|
+
`Feature: "${session.spec?.title ?? session.title}"`,
|
|
1302
|
+
"",
|
|
1303
|
+
session.implementation ?? "No implementation plan yet.",
|
|
1304
|
+
"",
|
|
1305
|
+
"---",
|
|
1306
|
+
'Ready to execute these tasks? Say "execute" to begin, or describe changes needed.'
|
|
1307
|
+
].join("\n");
|
|
1308
|
+
}
|
|
1309
|
+
function buildExecutingPrompt(session) {
|
|
1310
|
+
return [
|
|
1311
|
+
`\u2550\u2550\u2550 Task Execution \u2550\u2550\u2550`,
|
|
1312
|
+
`Feature: "${session.spec?.title ?? session.title}"`,
|
|
1313
|
+
"",
|
|
1314
|
+
"**Instructions for AI:**",
|
|
1315
|
+
"Execute the tasks one by one in the order shown in the task list above.",
|
|
1316
|
+
"",
|
|
1317
|
+
"For each task:",
|
|
1318
|
+
"1. Implement the code (create/modify files)",
|
|
1319
|
+
"2. Write tests if applicable",
|
|
1320
|
+
"3. After completing a task, tell the user to run: /sdd done <task number or title>",
|
|
1321
|
+
"4. Then move to the next task",
|
|
1322
|
+
"",
|
|
1323
|
+
"**Important:**",
|
|
1324
|
+
"- Focus on ONE task at a time",
|
|
1325
|
+
"- After completing each task, explicitly state what you did",
|
|
1326
|
+
'- Tell the user: "Run /sdd done <N> to mark this task complete"',
|
|
1327
|
+
"- Then proceed to the next task automatically",
|
|
1328
|
+
"- When ALL tasks are done, provide a summary of everything implemented",
|
|
1329
|
+
"",
|
|
1330
|
+
"Start executing the first pending task now."
|
|
1331
|
+
].join("\n");
|
|
1332
|
+
}
|
|
1333
|
+
var AISpecBuilder = class {
|
|
1334
|
+
session;
|
|
1335
|
+
store;
|
|
1336
|
+
minQuestions;
|
|
1337
|
+
maxQuestions;
|
|
1338
|
+
sessionPath;
|
|
1339
|
+
constructor(opts) {
|
|
1340
|
+
this.store = opts.store;
|
|
1341
|
+
this.minQuestions = opts.minQuestions ?? 2;
|
|
1342
|
+
this.maxQuestions = opts.maxQuestions ?? 10;
|
|
1343
|
+
this.sessionPath = opts.sessionPath;
|
|
1344
|
+
this.session = {
|
|
1345
|
+
id: crypto.randomUUID(),
|
|
1346
|
+
phase: "questioning",
|
|
1347
|
+
title: "",
|
|
1348
|
+
userIntent: "",
|
|
1349
|
+
projectContext: opts.projectContext ?? "",
|
|
1350
|
+
answers: [],
|
|
1351
|
+
questionCount: 0,
|
|
1352
|
+
approved: false,
|
|
1353
|
+
createdAt: Date.now(),
|
|
1354
|
+
updatedAt: Date.now()
|
|
1355
|
+
};
|
|
1356
|
+
}
|
|
1357
|
+
// ── Session Persistence ──────────────────────────────────────────────────
|
|
1358
|
+
/** Save session state to disk. */
|
|
1359
|
+
async saveSession() {
|
|
1360
|
+
if (!this.sessionPath) return;
|
|
1361
|
+
try {
|
|
1362
|
+
const fsp3 = await import('fs/promises');
|
|
1363
|
+
const path4 = await import('path');
|
|
1364
|
+
await fsp3.mkdir(path4.dirname(this.sessionPath), { recursive: true });
|
|
1365
|
+
await fsp3.writeFile(this.sessionPath, JSON.stringify(this.session, null, 2), "utf8");
|
|
1366
|
+
} catch {
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
/** Load session state from disk. Returns true if a session was loaded. */
|
|
1370
|
+
async loadSession() {
|
|
1371
|
+
if (!this.sessionPath) return false;
|
|
1372
|
+
try {
|
|
1373
|
+
const fsp3 = await import('fs/promises');
|
|
1374
|
+
const raw = await fsp3.readFile(this.sessionPath, "utf8");
|
|
1375
|
+
const loaded = JSON.parse(raw);
|
|
1376
|
+
if (loaded?.id && loaded?.phase && loaded?.title) {
|
|
1377
|
+
this.session = loaded;
|
|
1378
|
+
return true;
|
|
1379
|
+
}
|
|
1380
|
+
} catch {
|
|
1381
|
+
}
|
|
1382
|
+
return false;
|
|
1383
|
+
}
|
|
1384
|
+
/** Delete saved session from disk. */
|
|
1385
|
+
async deleteSession() {
|
|
1386
|
+
if (!this.sessionPath) return;
|
|
1387
|
+
try {
|
|
1388
|
+
const fsp3 = await import('fs/promises');
|
|
1389
|
+
await fsp3.unlink(this.sessionPath);
|
|
1390
|
+
} catch {
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
/** Auto-save helper — calls saveSession() but never throws. */
|
|
1394
|
+
autoSave() {
|
|
1395
|
+
this.saveSession().catch(() => {
|
|
1396
|
+
});
|
|
1397
|
+
}
|
|
1398
|
+
// ── Session Lifecycle ─────────────────────────────────────────────────────
|
|
1399
|
+
/** Start a new session with a title and optional intent. */
|
|
1400
|
+
startSession(title, intent) {
|
|
1401
|
+
this.session.title = title;
|
|
1402
|
+
this.session.userIntent = intent ?? "";
|
|
1403
|
+
this.session.phase = "questioning";
|
|
1404
|
+
this.session.updatedAt = Date.now();
|
|
1405
|
+
this.autoSave();
|
|
1406
|
+
}
|
|
1407
|
+
/** Get current session state (readonly). */
|
|
1408
|
+
getSession() {
|
|
1409
|
+
return { ...this.session };
|
|
1410
|
+
}
|
|
1411
|
+
/** Get the current phase. */
|
|
1412
|
+
getPhase() {
|
|
1413
|
+
return this.session.phase;
|
|
1414
|
+
}
|
|
1415
|
+
// ── AI Prompt Generation ──────────────────────────────────────────────────
|
|
1416
|
+
/**
|
|
1417
|
+
* Get the AI prompt for the current phase.
|
|
1418
|
+
* This prompt is injected into the conversation so the AI agent knows
|
|
1419
|
+
* what to do next (ask a question, generate a spec, etc.).
|
|
1420
|
+
*/
|
|
1421
|
+
getAIPrompt() {
|
|
1422
|
+
switch (this.session.phase) {
|
|
1423
|
+
case "questioning":
|
|
1424
|
+
return buildQuestioningPrompt(this.session, this.minQuestions, this.maxQuestions);
|
|
1425
|
+
case "spec_review":
|
|
1426
|
+
return buildSpecReviewPrompt(this.session);
|
|
1427
|
+
case "implementation":
|
|
1428
|
+
return buildImplementationPrompt(this.session);
|
|
1429
|
+
case "task_review":
|
|
1430
|
+
return buildTaskReviewPrompt(this.session);
|
|
1431
|
+
case "executing":
|
|
1432
|
+
return buildExecutingPrompt(this.session);
|
|
1433
|
+
case "done":
|
|
1434
|
+
return "All tasks completed. Specification is fully implemented.";
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
// ── Answer Processing ─────────────────────────────────────────────────────
|
|
1438
|
+
/**
|
|
1439
|
+
* Record a question/answer pair from the AI conversation.
|
|
1440
|
+
* Call this when the AI asks a question and the user responds.
|
|
1441
|
+
*/
|
|
1442
|
+
addAnswer(question, answer) {
|
|
1443
|
+
this.session.answers.push({ question, answer, timestamp: Date.now() });
|
|
1444
|
+
this.session.questionCount++;
|
|
1445
|
+
this.session.updatedAt = Date.now();
|
|
1446
|
+
this.autoSave();
|
|
1447
|
+
}
|
|
1448
|
+
/**
|
|
1449
|
+
* Check if more questions should be asked.
|
|
1450
|
+
* Returns false if max reached or if the AI has signaled it has enough info.
|
|
1451
|
+
*/
|
|
1452
|
+
shouldContinueQuestioning() {
|
|
1453
|
+
return this.session.questionCount < this.maxQuestions;
|
|
1454
|
+
}
|
|
1455
|
+
/**
|
|
1456
|
+
* Check if minimum questions have been asked.
|
|
1457
|
+
*/
|
|
1458
|
+
hasMetMinimumQuestions() {
|
|
1459
|
+
return this.session.questionCount >= this.minQuestions;
|
|
1460
|
+
}
|
|
1461
|
+
// ── Phase Transitions ─────────────────────────────────────────────────────
|
|
1462
|
+
/**
|
|
1463
|
+
* Set the generated specification and move to spec_review phase.
|
|
1464
|
+
*/
|
|
1465
|
+
setSpec(spec) {
|
|
1466
|
+
this.session.spec = spec;
|
|
1467
|
+
this.session.phase = "spec_review";
|
|
1468
|
+
this.session.updatedAt = Date.now();
|
|
1469
|
+
this.autoSave();
|
|
1470
|
+
}
|
|
1471
|
+
/**
|
|
1472
|
+
* Approve the current phase and advance to the next.
|
|
1473
|
+
* questioning → spec_review (requires spec to be set)
|
|
1474
|
+
* spec_review → implementation
|
|
1475
|
+
* implementation → task_review (requires implementation to be set)
|
|
1476
|
+
* task_review → executing
|
|
1477
|
+
* executing → done
|
|
1478
|
+
*/
|
|
1479
|
+
approve() {
|
|
1480
|
+
switch (this.session.phase) {
|
|
1481
|
+
case "questioning":
|
|
1482
|
+
if (!this.session.spec) {
|
|
1483
|
+
throw new Error("Cannot approve: no spec generated yet.");
|
|
1484
|
+
}
|
|
1485
|
+
this.session.phase = "spec_review";
|
|
1486
|
+
break;
|
|
1487
|
+
case "spec_review":
|
|
1488
|
+
this.session.phase = "implementation";
|
|
1489
|
+
break;
|
|
1490
|
+
case "implementation":
|
|
1491
|
+
this.session.phase = "task_review";
|
|
1492
|
+
break;
|
|
1493
|
+
case "task_review":
|
|
1494
|
+
this.session.phase = "executing";
|
|
1495
|
+
break;
|
|
1496
|
+
case "executing":
|
|
1497
|
+
this.session.phase = "done";
|
|
1498
|
+
break;
|
|
1499
|
+
}
|
|
1500
|
+
this.session.approved = true;
|
|
1501
|
+
this.session.updatedAt = Date.now();
|
|
1502
|
+
this.autoSave();
|
|
1503
|
+
return this.session.phase;
|
|
1504
|
+
}
|
|
1505
|
+
/**
|
|
1506
|
+
* Set the implementation plan text.
|
|
1507
|
+
*/
|
|
1508
|
+
setImplementation(plan) {
|
|
1509
|
+
this.session.implementation = plan;
|
|
1510
|
+
this.session.phase = "task_review";
|
|
1511
|
+
this.session.updatedAt = Date.now();
|
|
1512
|
+
this.autoSave();
|
|
1513
|
+
}
|
|
1514
|
+
/**
|
|
1515
|
+
* Mark session as done.
|
|
1516
|
+
*/
|
|
1517
|
+
markDone() {
|
|
1518
|
+
this.session.phase = "done";
|
|
1519
|
+
this.session.updatedAt = Date.now();
|
|
1520
|
+
this.autoSave();
|
|
1521
|
+
}
|
|
1522
|
+
/**
|
|
1523
|
+
* Set the task graph ID for this session.
|
|
1524
|
+
*/
|
|
1525
|
+
setTaskGraphId(graphId) {
|
|
1526
|
+
this.session.taskGraphId = graphId;
|
|
1527
|
+
this.autoSave();
|
|
1528
|
+
}
|
|
1529
|
+
/**
|
|
1530
|
+
* Get the task graph ID for this session.
|
|
1531
|
+
*/
|
|
1532
|
+
getTaskGraphId() {
|
|
1533
|
+
return this.session.taskGraphId;
|
|
1534
|
+
}
|
|
1535
|
+
// ── Spec Persistence ──────────────────────────────────────────────────────
|
|
1536
|
+
/**
|
|
1537
|
+
* Save the current spec to the store.
|
|
1538
|
+
*/
|
|
1539
|
+
async saveSpec() {
|
|
1540
|
+
if (!this.session.spec) {
|
|
1541
|
+
throw new Error("No spec to save.");
|
|
1542
|
+
}
|
|
1543
|
+
await this.store.save(this.session.spec);
|
|
1544
|
+
return this.session.spec;
|
|
1545
|
+
}
|
|
1546
|
+
// ── Spec Generation Helpers ───────────────────────────────────────────────
|
|
1547
|
+
/**
|
|
1548
|
+
* Parse a spec from a JSON string (from AI output).
|
|
1549
|
+
* Validates and normalizes the structure.
|
|
1550
|
+
*/
|
|
1551
|
+
parseSpecFromJSON(jsonStr) {
|
|
1552
|
+
let parsed;
|
|
1553
|
+
try {
|
|
1554
|
+
parsed = JSON.parse(jsonStr);
|
|
1555
|
+
} catch (e) {
|
|
1556
|
+
throw new Error(`Invalid JSON for spec: ${e instanceof Error ? e.message : "parse error"}`);
|
|
1557
|
+
}
|
|
1558
|
+
if (!parsed || typeof parsed !== "object") {
|
|
1559
|
+
throw new Error("Spec JSON must be an object.");
|
|
1560
|
+
}
|
|
1561
|
+
const raw = parsed;
|
|
1562
|
+
const now = Date.now();
|
|
1563
|
+
const title = String(raw.title ?? this.session.title ?? "Untitled");
|
|
1564
|
+
const overview = String(raw.overview ?? "");
|
|
1565
|
+
if (!overview || overview === "undefined") {
|
|
1566
|
+
throw new Error("Spec must have an overview.");
|
|
1567
|
+
}
|
|
1568
|
+
const rawSections = Array.isArray(raw.sections) ? raw.sections : [];
|
|
1569
|
+
const sections = rawSections.filter((s) => s && typeof s === "object").map((s) => ({
|
|
1570
|
+
type: ["overview", "requirements", "architecture", "api", "data", "security", "acceptance"].includes(String(s.type)) ? String(s.type) : "overview",
|
|
1571
|
+
title: String(s.title ?? ""),
|
|
1572
|
+
content: String(s.content ?? ""),
|
|
1573
|
+
level: Number(s.level) || 1
|
|
1574
|
+
}));
|
|
1575
|
+
const rawReqs = Array.isArray(raw.requirements) ? raw.requirements : [];
|
|
1576
|
+
const requirements = rawReqs.filter((r) => r && typeof r === "object").map((r, i) => ({
|
|
1577
|
+
id: String(r.id ?? `REQ-${i + 1}`),
|
|
1578
|
+
type: ["functional", "non-functional", "security", "performance", "ux"].includes(String(r.type)) ? String(r.type) : "functional",
|
|
1579
|
+
priority: ["critical", "high", "medium", "low"].includes(String(r.priority)) ? String(r.priority) : "medium",
|
|
1580
|
+
description: String(r.description ?? ""),
|
|
1581
|
+
acceptanceCriteria: Array.isArray(r.acceptanceCriteria) ? r.acceptanceCriteria.map(String) : []
|
|
1582
|
+
}));
|
|
1583
|
+
const spec = {
|
|
1584
|
+
id: crypto.randomUUID(),
|
|
1585
|
+
title,
|
|
1586
|
+
version: "0.1.0",
|
|
1587
|
+
status: "draft",
|
|
1588
|
+
overview,
|
|
1589
|
+
sections,
|
|
1590
|
+
requirements,
|
|
1591
|
+
createdAt: now,
|
|
1592
|
+
updatedAt: now,
|
|
1593
|
+
metadata: {
|
|
1594
|
+
generatedBy: "AISpecBuilder",
|
|
1595
|
+
sessionId: this.session.id
|
|
1596
|
+
}
|
|
1597
|
+
};
|
|
1598
|
+
return spec;
|
|
1599
|
+
}
|
|
1600
|
+
/**
|
|
1601
|
+
* Extract JSON from AI output (handles ```json blocks and raw JSON).
|
|
1602
|
+
*/
|
|
1603
|
+
extractJSON(text) {
|
|
1604
|
+
const codeBlockMatch = text.match(/```json\s*([\s\S]*?)```/);
|
|
1605
|
+
if (codeBlockMatch?.[1]) {
|
|
1606
|
+
return codeBlockMatch[1].trim();
|
|
1607
|
+
}
|
|
1608
|
+
const genericBlockMatch = text.match(/```\s*([\s\S]*?)```/);
|
|
1609
|
+
if (genericBlockMatch?.[1]) {
|
|
1610
|
+
const trimmed = genericBlockMatch[1].trim();
|
|
1611
|
+
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
|
1612
|
+
return trimmed;
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
const jsonMatch = text.match(/(\{[\s\S]*\})/);
|
|
1616
|
+
if (jsonMatch?.[1]) {
|
|
1617
|
+
try {
|
|
1618
|
+
JSON.parse(jsonMatch[1]);
|
|
1619
|
+
return jsonMatch[1];
|
|
1620
|
+
} catch {
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
return null;
|
|
1624
|
+
}
|
|
1625
|
+
/**
|
|
1626
|
+
* Detect if AI output contains a spec (JSON block).
|
|
1627
|
+
*/
|
|
1628
|
+
hasSpecInOutput(text) {
|
|
1629
|
+
return this.extractJSON(text) !== null;
|
|
1630
|
+
}
|
|
1631
|
+
/**
|
|
1632
|
+
* Try to parse a spec from AI output text.
|
|
1633
|
+
* Returns null if no valid spec found.
|
|
1634
|
+
*/
|
|
1635
|
+
tryParseSpecFromOutput(text) {
|
|
1636
|
+
const json = this.extractJSON(text);
|
|
1637
|
+
if (!json) return null;
|
|
1638
|
+
try {
|
|
1639
|
+
return this.parseSpecFromJSON(json);
|
|
1640
|
+
} catch {
|
|
1641
|
+
return null;
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
// ── JSON Array Extraction (for tasks) ─────────────────────────────────────
|
|
1645
|
+
/**
|
|
1646
|
+
* Extract a JSON array from AI output (for task lists).
|
|
1647
|
+
*/
|
|
1648
|
+
extractJSONArray(text) {
|
|
1649
|
+
const codeBlockMatch = text.match(/```json\s*([\s\S]*?)```/);
|
|
1650
|
+
if (codeBlockMatch?.[1]) {
|
|
1651
|
+
const trimmed = codeBlockMatch[1].trim();
|
|
1652
|
+
if (trimmed.startsWith("[")) return trimmed;
|
|
1653
|
+
}
|
|
1654
|
+
const arrayMatch = text.match(/(\[[\s\S]*\])/);
|
|
1655
|
+
if (arrayMatch?.[1]) {
|
|
1656
|
+
try {
|
|
1657
|
+
const parsed = JSON.parse(arrayMatch[1]);
|
|
1658
|
+
if (Array.isArray(parsed)) return arrayMatch[1];
|
|
1659
|
+
} catch {
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
return null;
|
|
1663
|
+
}
|
|
1664
|
+
};
|
|
1665
|
+
|
|
1666
|
+
// src/sdd/spec-templates.ts
|
|
1667
|
+
var SPEC_TEMPLATES = [
|
|
1668
|
+
{
|
|
1669
|
+
id: "feature",
|
|
1670
|
+
name: "New Feature",
|
|
1671
|
+
description: "Template for new feature development",
|
|
1672
|
+
sections: [
|
|
1673
|
+
{ type: "overview", title: "Overview", level: 2 },
|
|
1674
|
+
{ type: "requirements", title: "Requirements", level: 2 },
|
|
1675
|
+
{ type: "architecture", title: "Architecture", level: 2 },
|
|
1676
|
+
{ type: "api", title: "API Design", level: 2 },
|
|
1677
|
+
{ type: "data", title: "Data Model", level: 2 },
|
|
1678
|
+
{ type: "security", title: "Security", level: 2 },
|
|
1679
|
+
{ type: "acceptance", title: "Acceptance Criteria", level: 2 }
|
|
1680
|
+
],
|
|
1681
|
+
defaultRequirements: [
|
|
1682
|
+
{ type: "functional", priority: "high", acceptanceCriteria: [], blockedBy: [], implements: [] },
|
|
1683
|
+
{ type: "non-functional", priority: "medium", acceptanceCriteria: [], blockedBy: [], implements: [] }
|
|
1684
|
+
]
|
|
1685
|
+
},
|
|
1686
|
+
{
|
|
1687
|
+
id: "bugfix",
|
|
1688
|
+
name: "Bug Fix",
|
|
1689
|
+
description: "Template for bug fix specifications",
|
|
1690
|
+
sections: [
|
|
1691
|
+
{ type: "overview", title: "Bug Description", level: 2 },
|
|
1692
|
+
{ type: "requirements", title: "Root Cause Analysis", level: 2 },
|
|
1693
|
+
{ type: "acceptance", title: "Fix Verification", level: 2 }
|
|
1694
|
+
],
|
|
1695
|
+
defaultRequirements: [
|
|
1696
|
+
{ type: "functional", priority: "critical", acceptanceCriteria: [], blockedBy: [], implements: [] }
|
|
1697
|
+
]
|
|
1698
|
+
},
|
|
1699
|
+
{
|
|
1700
|
+
id: "refactor",
|
|
1701
|
+
name: "Refactor",
|
|
1702
|
+
description: "Template for code refactoring",
|
|
1703
|
+
sections: [
|
|
1704
|
+
{ type: "overview", title: "Current State", level: 2 },
|
|
1705
|
+
{ type: "requirements", title: "Refactoring Goals", level: 2 },
|
|
1706
|
+
{ type: "architecture", title: "Target Architecture", level: 2 },
|
|
1707
|
+
{ type: "acceptance", title: "Verification", level: 2 }
|
|
1708
|
+
],
|
|
1709
|
+
defaultRequirements: [
|
|
1710
|
+
{ type: "non-functional", priority: "high", acceptanceCriteria: [], blockedBy: [], implements: [] }
|
|
1711
|
+
]
|
|
1712
|
+
},
|
|
1713
|
+
{
|
|
1714
|
+
id: "infra",
|
|
1715
|
+
name: "Infrastructure",
|
|
1716
|
+
description: "Template for infrastructure/tooling changes",
|
|
1717
|
+
sections: [
|
|
1718
|
+
{ type: "overview", title: "What and Why", level: 2 },
|
|
1719
|
+
{ type: "requirements", title: "Requirements", level: 2 },
|
|
1720
|
+
{ type: "architecture", title: "Design", level: 2 },
|
|
1721
|
+
{ type: "security", title: "Security Impact", level: 2 },
|
|
1722
|
+
{ type: "acceptance", title: "Rollout Plan", level: 2 }
|
|
1723
|
+
],
|
|
1724
|
+
defaultRequirements: [
|
|
1725
|
+
{ type: "functional", priority: "high", acceptanceCriteria: [], blockedBy: [], implements: [] },
|
|
1726
|
+
{ type: "security", priority: "high", acceptanceCriteria: [], blockedBy: [], implements: [] }
|
|
1727
|
+
]
|
|
1728
|
+
},
|
|
1729
|
+
{
|
|
1730
|
+
id: "integration",
|
|
1731
|
+
name: "Integration",
|
|
1732
|
+
description: "Template for integrating external services or APIs",
|
|
1733
|
+
sections: [
|
|
1734
|
+
{ type: "overview", title: "Integration Overview", level: 2 },
|
|
1735
|
+
{ type: "requirements", title: "Integration Requirements", level: 2 },
|
|
1736
|
+
{ type: "api", title: "API Contract", level: 2 },
|
|
1737
|
+
{ type: "architecture", title: "Architecture", level: 2 },
|
|
1738
|
+
{ type: "security", title: "Auth & Security", level: 2 },
|
|
1739
|
+
{ type: "acceptance", title: "Testing Strategy", level: 2 }
|
|
1740
|
+
],
|
|
1741
|
+
defaultRequirements: [
|
|
1742
|
+
{ type: "functional", priority: "high", acceptanceCriteria: [], blockedBy: [], implements: [] },
|
|
1743
|
+
{ type: "security", priority: "critical", acceptanceCriteria: [], blockedBy: [], implements: [] },
|
|
1744
|
+
{ type: "performance", priority: "medium", acceptanceCriteria: [], blockedBy: [], implements: [] }
|
|
1745
|
+
]
|
|
1746
|
+
},
|
|
1747
|
+
{
|
|
1748
|
+
id: "cli-command",
|
|
1749
|
+
name: "CLI Command",
|
|
1750
|
+
description: "Template for new CLI commands/slash commands",
|
|
1751
|
+
sections: [
|
|
1752
|
+
{ type: "overview", title: "Command Overview", level: 2 },
|
|
1753
|
+
{ type: "requirements", title: "Command Requirements", level: 2 },
|
|
1754
|
+
{ type: "api", title: "Command Interface", level: 2 },
|
|
1755
|
+
{ type: "acceptance", title: "Usage Examples", level: 2 }
|
|
1756
|
+
],
|
|
1757
|
+
defaultRequirements: [
|
|
1758
|
+
{ type: "ux", priority: "high", acceptanceCriteria: [], blockedBy: [], implements: [] },
|
|
1759
|
+
{ type: "functional", priority: "high", acceptanceCriteria: [], blockedBy: [], implements: [] }
|
|
1760
|
+
]
|
|
1761
|
+
}
|
|
1762
|
+
];
|
|
1763
|
+
function getTemplate(id) {
|
|
1764
|
+
return SPEC_TEMPLATES.find((t) => t.id === id);
|
|
1765
|
+
}
|
|
1766
|
+
function listTemplates() {
|
|
1767
|
+
return SPEC_TEMPLATES.map((t) => ({ id: t.id, name: t.name, description: t.description }));
|
|
1768
|
+
}
|
|
1769
|
+
function templateToMarkdown(template, title) {
|
|
1770
|
+
const lines = [];
|
|
1771
|
+
lines.push(`# ${title ?? "Untitled Specification"}`);
|
|
1772
|
+
lines.push("Version: 0.1.0");
|
|
1773
|
+
lines.push("");
|
|
1774
|
+
for (const section of template.sections) {
|
|
1775
|
+
lines.push(`${"#".repeat(section.level + 1)} ${section.title}`);
|
|
1776
|
+
lines.push(`_<!-- ${section.type} section content -->_`);
|
|
1777
|
+
lines.push("");
|
|
1778
|
+
}
|
|
1779
|
+
return lines.join("\n");
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1782
|
+
// src/sdd/task-visualizer.ts
|
|
1783
|
+
var STATUS_ICON = {
|
|
1784
|
+
pending: "\u25CB",
|
|
1785
|
+
in_progress: "\u25D0",
|
|
1786
|
+
blocked: "\u2298",
|
|
1787
|
+
failed: "\u2717",
|
|
1788
|
+
review: "\u25D1",
|
|
1789
|
+
completed: "\u25CF"
|
|
1790
|
+
};
|
|
1791
|
+
var PRIORITY_ICON = {
|
|
1792
|
+
critical: "\u{1F534}",
|
|
1793
|
+
high: "\u{1F7E0}",
|
|
1794
|
+
medium: "\u{1F7E1}",
|
|
1795
|
+
low: "\u{1F7E2}"
|
|
1796
|
+
};
|
|
1797
|
+
var TYPE_ICON = {
|
|
1798
|
+
feature: "\u26A1",
|
|
1799
|
+
bugfix: "\u{1F41B}",
|
|
1800
|
+
refactor: "\u267B\uFE0F",
|
|
1801
|
+
docs: "\u{1F4DD}",
|
|
1802
|
+
test: "\u{1F9EA}",
|
|
1803
|
+
chore: "\u{1F527}"
|
|
1804
|
+
};
|
|
1805
|
+
function renderTaskGraph(graph, opts) {
|
|
1806
|
+
const lines = [];
|
|
1807
|
+
const compact = opts?.compact ?? false;
|
|
1808
|
+
lines.push(`\u256D\u2500 Task Graph: ${graph.title} \u2500\u256E`);
|
|
1809
|
+
lines.push(`\u2502 Spec: ${graph.specId.slice(0, 8)}... \u2502 Nodes: ${graph.nodes.size} \u2502 Edges: ${graph.edges.length} \u2502`);
|
|
1810
|
+
lines.push("\u2570" + "\u2500".repeat(Math.max(50, graph.title.length + 30)) + "\u256F");
|
|
1811
|
+
lines.push("");
|
|
1812
|
+
const progress = computeTaskProgress(graph);
|
|
1813
|
+
lines.push(renderProgress(progress));
|
|
1814
|
+
lines.push("");
|
|
1815
|
+
const childrenMap = /* @__PURE__ */ new Map();
|
|
1816
|
+
for (const edge of graph.edges) {
|
|
1817
|
+
if (edge.type === "depends_on") {
|
|
1818
|
+
const deps = childrenMap.get(edge.from) ?? [];
|
|
1819
|
+
deps.push(edge.to);
|
|
1820
|
+
childrenMap.set(edge.from, deps);
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
const rendered = /* @__PURE__ */ new Set();
|
|
1824
|
+
const rootNodes = graph.rootNodes.filter((id) => graph.nodes.has(id));
|
|
1825
|
+
const startNodes = rootNodes.length > 0 ? rootNodes : Array.from(graph.nodes.keys()).filter((id) => {
|
|
1826
|
+
const deps = childrenMap.get(id);
|
|
1827
|
+
return !deps || deps.length === 0;
|
|
1828
|
+
});
|
|
1829
|
+
for (const rootId of startNodes) {
|
|
1830
|
+
renderNode(graph, rootId, lines, rendered, childrenMap, compact, "");
|
|
1831
|
+
}
|
|
1832
|
+
for (const [id] of graph.nodes) {
|
|
1833
|
+
if (!rendered.has(id)) {
|
|
1834
|
+
renderNode(graph, id, lines, rendered, childrenMap, compact, "");
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
lines.push("");
|
|
1838
|
+
lines.push("Legend: \u25CF done \u25D0 in-progress \u25CB pending \u2297 blocked \u2717 failed \u25D2 review");
|
|
1839
|
+
return lines.join("\n");
|
|
1840
|
+
}
|
|
1841
|
+
function renderNode(graph, nodeId, lines, rendered, childrenMap, compact, prefix) {
|
|
1842
|
+
if (rendered.has(nodeId)) return;
|
|
1843
|
+
rendered.add(nodeId);
|
|
1844
|
+
const node = graph.nodes.get(nodeId);
|
|
1845
|
+
if (!node) return;
|
|
1846
|
+
const icon = STATUS_ICON[node.status];
|
|
1847
|
+
const prioIcon = PRIORITY_ICON[node.priority];
|
|
1848
|
+
const typeIcon = TYPE_ICON[node.type];
|
|
1849
|
+
const title = compact ? truncate(node.title, 40) : node.title;
|
|
1850
|
+
const blockedBy = childrenMap.get(nodeId) ?? [];
|
|
1851
|
+
const depsStr = blockedBy.length > 0 ? ` \u2190 [${blockedBy.map((d) => graph.nodes.get(d)?.title?.slice(0, 12) ?? "?").join(", ")}]` : "";
|
|
1852
|
+
lines.push(`${prefix}${icon} ${typeIcon} ${prioIcon} ${title}${depsStr}`);
|
|
1853
|
+
if (!compact && node.description) {
|
|
1854
|
+
const descLines = node.description.split("\n").slice(0, 3);
|
|
1855
|
+
for (const dl of descLines) {
|
|
1856
|
+
lines.push(`${prefix} \u2514 ${truncate(dl, 60)}`);
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
const dependents = graph.edges.filter((e) => e.type === "depends_on" && e.to === nodeId).map((e) => e.from).filter((id) => graph.nodes.has(id));
|
|
1860
|
+
for (const depId of dependents) {
|
|
1861
|
+
renderNode(graph, depId, lines, rendered, childrenMap, compact, prefix + " ");
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
function renderProgress(progress) {
|
|
1865
|
+
const barWidth = 30;
|
|
1866
|
+
const filled = Math.round(progress.percentComplete / 100 * barWidth);
|
|
1867
|
+
const empty = barWidth - filled;
|
|
1868
|
+
const bar = "\u2588".repeat(filled) + "\u2591".repeat(empty);
|
|
1869
|
+
return [
|
|
1870
|
+
`Progress: [${bar}] ${progress.percentComplete}%`,
|
|
1871
|
+
` ${progress.completed} done \u2502 ${progress.inProgress} active \u2502 ${progress.pending} pending \u2502 ${progress.blocked} blocked \u2502 ${progress.failed} failed`
|
|
1872
|
+
].join("\n");
|
|
1873
|
+
}
|
|
1874
|
+
function renderTaskList(graph) {
|
|
1875
|
+
const lines = [];
|
|
1876
|
+
const nodes = Array.from(graph.nodes.values());
|
|
1877
|
+
const groups = {
|
|
1878
|
+
in_progress: [],
|
|
1879
|
+
pending: [],
|
|
1880
|
+
blocked: [],
|
|
1881
|
+
review: [],
|
|
1882
|
+
failed: [],
|
|
1883
|
+
completed: []
|
|
1884
|
+
};
|
|
1885
|
+
for (const node of nodes) {
|
|
1886
|
+
groups[node.status]?.push(node);
|
|
1887
|
+
}
|
|
1888
|
+
for (const [status, group] of Object.entries(groups)) {
|
|
1889
|
+
if (group.length === 0) continue;
|
|
1890
|
+
const icon = STATUS_ICON[status];
|
|
1891
|
+
lines.push(`${icon} ${status.toUpperCase()} (${group.length})`);
|
|
1892
|
+
for (const node of group) {
|
|
1893
|
+
const prio = PRIORITY_ICON[node.priority];
|
|
1894
|
+
const type = TYPE_ICON[node.type];
|
|
1895
|
+
lines.push(` ${type} ${prio} ${node.title}`);
|
|
1896
|
+
}
|
|
1897
|
+
lines.push("");
|
|
1898
|
+
}
|
|
1899
|
+
return lines.join("\n");
|
|
1900
|
+
}
|
|
1901
|
+
function renderSpecAnalysis(spec, analysis) {
|
|
1902
|
+
const lines = [];
|
|
1903
|
+
lines.push(`\u256D\u2500 Spec Analysis: ${spec.title} \u2500\u256E`);
|
|
1904
|
+
lines.push("");
|
|
1905
|
+
const barWidth = 20;
|
|
1906
|
+
const filled = Math.round(analysis.completeness / 100 * barWidth);
|
|
1907
|
+
const bar = "\u2588".repeat(filled) + "\u2591".repeat(barWidth - filled);
|
|
1908
|
+
lines.push(`Completeness: [${bar}] ${analysis.completeness}%`);
|
|
1909
|
+
lines.push("");
|
|
1910
|
+
if (analysis.gaps.length > 0) {
|
|
1911
|
+
lines.push("\u26A0 Gaps:");
|
|
1912
|
+
for (const gap of analysis.gaps) {
|
|
1913
|
+
lines.push(` \u2022 ${gap}`);
|
|
1914
|
+
}
|
|
1915
|
+
lines.push("");
|
|
1916
|
+
}
|
|
1917
|
+
if (analysis.risks.length > 0) {
|
|
1918
|
+
lines.push("\u{1F534} Risks:");
|
|
1919
|
+
for (const risk of analysis.risks) {
|
|
1920
|
+
lines.push(` \u2022 ${risk}`);
|
|
1921
|
+
}
|
|
1922
|
+
lines.push("");
|
|
1923
|
+
}
|
|
1924
|
+
if (analysis.suggestions.length > 0) {
|
|
1925
|
+
lines.push("\u{1F4A1} Suggestions:");
|
|
1926
|
+
for (const sug of analysis.suggestions) {
|
|
1927
|
+
lines.push(` \u2022 ${sug}`);
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
return lines.join("\n");
|
|
1931
|
+
}
|
|
1932
|
+
function truncate(str, maxLen) {
|
|
1933
|
+
if (str.length <= maxLen) return str;
|
|
1934
|
+
return str.slice(0, maxLen - 1) + "\u2026";
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1937
|
+
// src/sdd/critical-path.ts
|
|
1938
|
+
function analyzeCriticalPath(graph) {
|
|
1939
|
+
const nodes = Array.from(graph.nodes.values());
|
|
1940
|
+
const topoOrder = topologicalSort(graph);
|
|
1941
|
+
const blockedByMap = /* @__PURE__ */ new Map();
|
|
1942
|
+
const blocksMap = /* @__PURE__ */ new Map();
|
|
1943
|
+
for (const edge of graph.edges) {
|
|
1944
|
+
if (edge.type === "depends_on") {
|
|
1945
|
+
if (!blockedByMap.has(edge.from)) blockedByMap.set(edge.from, /* @__PURE__ */ new Set());
|
|
1946
|
+
blockedByMap.get(edge.from).add(edge.to);
|
|
1947
|
+
if (!blocksMap.has(edge.to)) blocksMap.set(edge.to, /* @__PURE__ */ new Set());
|
|
1948
|
+
blocksMap.get(edge.to).add(edge.from);
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1951
|
+
const readyTasks = [];
|
|
1952
|
+
const blockedTasks = [];
|
|
1953
|
+
for (const node of nodes) {
|
|
1954
|
+
if (node.status === "completed") continue;
|
|
1955
|
+
const blockers = blockedByMap.get(node.id);
|
|
1956
|
+
if (!blockers || blockers.size === 0) {
|
|
1957
|
+
readyTasks.push(node.id);
|
|
1958
|
+
} else {
|
|
1959
|
+
const allCompleted = Array.from(blockers).every((id) => {
|
|
1960
|
+
const n = graph.nodes.get(id);
|
|
1961
|
+
return n?.status === "completed";
|
|
1962
|
+
});
|
|
1963
|
+
if (allCompleted) {
|
|
1964
|
+
readyTasks.push(node.id);
|
|
1965
|
+
} else {
|
|
1966
|
+
blockedTasks.push(node.id);
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1970
|
+
const bottlenecks = [];
|
|
1971
|
+
for (const node of nodes) {
|
|
1972
|
+
if (node.status === "completed") continue;
|
|
1973
|
+
const downstream = getTransitiveBlocked(graph, node.id, blocksMap);
|
|
1974
|
+
if (downstream.size > 0) {
|
|
1975
|
+
const blockedHours = Array.from(downstream).reduce((sum, id) => {
|
|
1976
|
+
const n = graph.nodes.get(id);
|
|
1977
|
+
return sum + (n?.estimateHours ?? 0);
|
|
1978
|
+
}, 0);
|
|
1979
|
+
bottlenecks.push({
|
|
1980
|
+
taskId: node.id,
|
|
1981
|
+
title: node.title,
|
|
1982
|
+
blockedCount: downstream.size,
|
|
1983
|
+
blockedHours,
|
|
1984
|
+
severity: Math.min(100, Math.round(downstream.size / nodes.length * 100))
|
|
1985
|
+
});
|
|
1986
|
+
}
|
|
1987
|
+
}
|
|
1988
|
+
bottlenecks.sort((a, b) => b.severity - a.severity);
|
|
1989
|
+
const criticalPath = computeCriticalPath(graph, topoOrder, blockedByMap);
|
|
1990
|
+
const totalHours = criticalPath.reduce((sum, id) => {
|
|
1991
|
+
const n = graph.nodes.get(id);
|
|
1992
|
+
return sum + (n?.estimateHours ?? 0);
|
|
1993
|
+
}, 0);
|
|
1994
|
+
const parallelGroups = computeParallelGroups(graph, blockedByMap);
|
|
1995
|
+
const executionOrder = topoOrder.filter((id) => {
|
|
1996
|
+
const n = graph.nodes.get(id);
|
|
1997
|
+
return n && n.status !== "completed";
|
|
1998
|
+
});
|
|
1999
|
+
return {
|
|
2000
|
+
criticalPath,
|
|
2001
|
+
totalHours,
|
|
2002
|
+
bottlenecks,
|
|
2003
|
+
parallelGroups,
|
|
2004
|
+
executionOrder,
|
|
2005
|
+
readyTasks,
|
|
2006
|
+
blockedTasks
|
|
2007
|
+
};
|
|
2008
|
+
}
|
|
2009
|
+
function getTransitiveBlocked(graph, taskId, blocksMap) {
|
|
2010
|
+
const visited = /* @__PURE__ */ new Set();
|
|
2011
|
+
const queue = [taskId];
|
|
2012
|
+
while (queue.length > 0) {
|
|
2013
|
+
const current = queue.shift();
|
|
2014
|
+
const blocked = blocksMap.get(current);
|
|
2015
|
+
if (!blocked) continue;
|
|
2016
|
+
for (const id of blocked) {
|
|
2017
|
+
if (!visited.has(id) && id !== taskId) {
|
|
2018
|
+
visited.add(id);
|
|
2019
|
+
queue.push(id);
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
return visited;
|
|
2024
|
+
}
|
|
2025
|
+
function computeCriticalPath(graph, topoOrder, blockedByMap) {
|
|
2026
|
+
const allIds = Array.from(graph.nodes.keys());
|
|
2027
|
+
if (allIds.length === 0) return [];
|
|
2028
|
+
const dist = /* @__PURE__ */ new Map();
|
|
2029
|
+
const prev = /* @__PURE__ */ new Map();
|
|
2030
|
+
for (const id of allIds) {
|
|
2031
|
+
dist.set(id, graph.nodes.get(id)?.estimateHours ?? 1);
|
|
2032
|
+
prev.set(id, null);
|
|
2033
|
+
}
|
|
2034
|
+
const blocksMap = /* @__PURE__ */ new Map();
|
|
2035
|
+
for (const [taskId, blockers] of blockedByMap) {
|
|
2036
|
+
for (const blockerId of blockers) {
|
|
2037
|
+
if (!blocksMap.has(blockerId)) blocksMap.set(blockerId, /* @__PURE__ */ new Set());
|
|
2038
|
+
blocksMap.get(blockerId).add(taskId);
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
const n = allIds.length;
|
|
2042
|
+
for (let i = 0; i < n - 1; i++) {
|
|
2043
|
+
let changed = false;
|
|
2044
|
+
for (const id of allIds) {
|
|
2045
|
+
const blocked = blocksMap.get(id);
|
|
2046
|
+
if (!blocked) continue;
|
|
2047
|
+
for (const blockedId of blocked) {
|
|
2048
|
+
const candidateDist = (dist.get(id) ?? 0) + (graph.nodes.get(blockedId)?.estimateHours ?? 1);
|
|
2049
|
+
if (candidateDist > (dist.get(blockedId) ?? 0)) {
|
|
2050
|
+
dist.set(blockedId, candidateDist);
|
|
2051
|
+
prev.set(blockedId, id);
|
|
2052
|
+
changed = true;
|
|
2053
|
+
}
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
2056
|
+
if (!changed) break;
|
|
2057
|
+
}
|
|
2058
|
+
let maxDist = 0;
|
|
2059
|
+
let maxId = allIds[0];
|
|
2060
|
+
for (const id of allIds) {
|
|
2061
|
+
const d = dist.get(id) ?? 0;
|
|
2062
|
+
if (d > maxDist) {
|
|
2063
|
+
maxDist = d;
|
|
2064
|
+
maxId = id;
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
2067
|
+
const path4 = [];
|
|
2068
|
+
let current = maxId;
|
|
2069
|
+
const visited = /* @__PURE__ */ new Set();
|
|
2070
|
+
while (current && !visited.has(current)) {
|
|
2071
|
+
visited.add(current);
|
|
2072
|
+
path4.unshift(current);
|
|
2073
|
+
current = prev.get(current) ?? null;
|
|
2074
|
+
}
|
|
2075
|
+
return path4;
|
|
2076
|
+
}
|
|
2077
|
+
function computeParallelGroups(graph, blockedByMap) {
|
|
2078
|
+
const groups = [];
|
|
2079
|
+
const assigned = /* @__PURE__ */ new Set();
|
|
2080
|
+
const nodes = Array.from(graph.nodes.values()).filter((n) => n.status !== "completed");
|
|
2081
|
+
const remaining = new Set(nodes.map((n) => n.id));
|
|
2082
|
+
while (remaining.size > 0) {
|
|
2083
|
+
const group = [];
|
|
2084
|
+
for (const id of remaining) {
|
|
2085
|
+
const blockers = blockedByMap.get(id);
|
|
2086
|
+
if (!blockers || blockers.size === 0) {
|
|
2087
|
+
group.push(id);
|
|
2088
|
+
} else {
|
|
2089
|
+
const allAssigned = Array.from(blockers).every((b) => assigned.has(b));
|
|
2090
|
+
if (allAssigned) {
|
|
2091
|
+
group.push(id);
|
|
2092
|
+
}
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2095
|
+
if (group.length === 0) {
|
|
2096
|
+
const first = Array.from(remaining)[0];
|
|
2097
|
+
if (first) group.push(first);
|
|
2098
|
+
}
|
|
2099
|
+
for (const id of group) {
|
|
2100
|
+
assigned.add(id);
|
|
2101
|
+
remaining.delete(id);
|
|
2102
|
+
}
|
|
2103
|
+
groups.push(group);
|
|
2104
|
+
}
|
|
2105
|
+
return groups;
|
|
2106
|
+
}
|
|
2107
|
+
|
|
2108
|
+
// src/sdd/spec-versioning.ts
|
|
2109
|
+
var SpecVersioning = class {
|
|
2110
|
+
versions = /* @__PURE__ */ new Map();
|
|
2111
|
+
/** Record a new version of a spec. */
|
|
2112
|
+
recordVersion(spec, changeDescription) {
|
|
2113
|
+
const version = {
|
|
2114
|
+
version: spec.version,
|
|
2115
|
+
spec: { ...spec },
|
|
2116
|
+
timestamp: Date.now(),
|
|
2117
|
+
changeDescription
|
|
2118
|
+
};
|
|
2119
|
+
const history = this.versions.get(spec.id) ?? [];
|
|
2120
|
+
history.push(version);
|
|
2121
|
+
this.versions.set(spec.id, history);
|
|
2122
|
+
return version;
|
|
2123
|
+
}
|
|
2124
|
+
/** Get version history for a spec. */
|
|
2125
|
+
getHistory(specId) {
|
|
2126
|
+
return this.versions.get(specId) ?? [];
|
|
2127
|
+
}
|
|
2128
|
+
/** Get a specific version of a spec. */
|
|
2129
|
+
getVersion(specId, version) {
|
|
2130
|
+
const history = this.versions.get(specId) ?? [];
|
|
2131
|
+
return history.find((v) => v.version === version);
|
|
2132
|
+
}
|
|
2133
|
+
/** Get the latest version of a spec. */
|
|
2134
|
+
getLatest(specId) {
|
|
2135
|
+
const history = this.versions.get(specId) ?? [];
|
|
2136
|
+
return history[history.length - 1];
|
|
2137
|
+
}
|
|
2138
|
+
/** Compute diff between two versions of a spec. */
|
|
2139
|
+
diff(oldSpec, newSpec) {
|
|
2140
|
+
const oldReqs = new Map(oldSpec.requirements.map((r) => [r.id, r]));
|
|
2141
|
+
const newReqs = new Map(newSpec.requirements.map((r) => [r.id, r]));
|
|
2142
|
+
const added = [];
|
|
2143
|
+
const removed = [];
|
|
2144
|
+
const modified = [];
|
|
2145
|
+
for (const [id, newReq] of newReqs) {
|
|
2146
|
+
const oldReq = oldReqs.get(id);
|
|
2147
|
+
if (!oldReq) {
|
|
2148
|
+
added.push(newReq);
|
|
2149
|
+
} else {
|
|
2150
|
+
const changes = this.compareRequirements(oldReq, newReq);
|
|
2151
|
+
if (changes.length > 0) {
|
|
2152
|
+
modified.push({
|
|
2153
|
+
requirement: newReq,
|
|
2154
|
+
previousVersion: oldReq,
|
|
2155
|
+
changes
|
|
2156
|
+
});
|
|
2157
|
+
}
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
2160
|
+
for (const [id, oldReq] of oldReqs) {
|
|
2161
|
+
if (!newReqs.has(id)) {
|
|
2162
|
+
removed.push(oldReq);
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2165
|
+
const parts = [];
|
|
2166
|
+
if (added.length > 0) parts.push(`${added.length} added`);
|
|
2167
|
+
if (removed.length > 0) parts.push(`${removed.length} removed`);
|
|
2168
|
+
if (modified.length > 0) parts.push(`${modified.length} modified`);
|
|
2169
|
+
return {
|
|
2170
|
+
added,
|
|
2171
|
+
removed,
|
|
2172
|
+
modified,
|
|
2173
|
+
summary: parts.length > 0 ? parts.join(", ") : "No changes"
|
|
2174
|
+
};
|
|
2175
|
+
}
|
|
2176
|
+
/**
|
|
2177
|
+
* Update a task graph incrementally based on spec changes.
|
|
2178
|
+
* - Added requirements → new tasks
|
|
2179
|
+
* - Removed requirements → remove tasks
|
|
2180
|
+
* - Modified requirements → update task descriptions
|
|
2181
|
+
* Returns the updated graph and list of changes made.
|
|
2182
|
+
*/
|
|
2183
|
+
updateTaskGraph(graph, oldSpec, newSpec) {
|
|
2184
|
+
const specDiff = this.diff(oldSpec, newSpec);
|
|
2185
|
+
const changes = [];
|
|
2186
|
+
const reqToTask = /* @__PURE__ */ new Map();
|
|
2187
|
+
for (const node of graph.nodes.values()) {
|
|
2188
|
+
if (node.specRequirementId) {
|
|
2189
|
+
reqToTask.set(node.specRequirementId, node);
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
2192
|
+
for (const req of specDiff.removed) {
|
|
2193
|
+
const task = reqToTask.get(req.id);
|
|
2194
|
+
if (task) {
|
|
2195
|
+
graph.nodes.delete(task.id);
|
|
2196
|
+
graph.edges = graph.edges.filter((e) => e.from !== task.id && e.to !== task.id);
|
|
2197
|
+
changes.push(`Removed task: ${task.title}`);
|
|
2198
|
+
}
|
|
2199
|
+
}
|
|
2200
|
+
for (const mod of specDiff.modified) {
|
|
2201
|
+
const task = reqToTask.get(mod.requirement.id);
|
|
2202
|
+
if (task) {
|
|
2203
|
+
task.title = mod.requirement.description;
|
|
2204
|
+
task.description = this.buildTaskDescription(mod.requirement);
|
|
2205
|
+
task.priority = mod.requirement.priority;
|
|
2206
|
+
task.updatedAt = Date.now();
|
|
2207
|
+
changes.push(`Updated task: ${task.title} (${mod.changes.join(", ")})`);
|
|
2208
|
+
}
|
|
2209
|
+
}
|
|
2210
|
+
for (const req of specDiff.added) {
|
|
2211
|
+
const now = Date.now();
|
|
2212
|
+
const newTask = {
|
|
2213
|
+
id: crypto.randomUUID(),
|
|
2214
|
+
title: req.description,
|
|
2215
|
+
description: this.buildTaskDescription(req),
|
|
2216
|
+
type: this.mapReqType(req.type),
|
|
2217
|
+
priority: req.priority,
|
|
2218
|
+
status: "pending",
|
|
2219
|
+
specRequirementId: req.id,
|
|
2220
|
+
tags: [req.type, req.priority],
|
|
2221
|
+
createdAt: now,
|
|
2222
|
+
updatedAt: now
|
|
2223
|
+
};
|
|
2224
|
+
graph.nodes.set(newTask.id, newTask);
|
|
2225
|
+
graph.rootNodes.push(newTask.id);
|
|
2226
|
+
changes.push(`Added task: ${newTask.title}`);
|
|
2227
|
+
}
|
|
2228
|
+
graph.updatedAt = Date.now();
|
|
2229
|
+
return { graph, changes };
|
|
2230
|
+
}
|
|
2231
|
+
compareRequirements(old, current) {
|
|
2232
|
+
const changes = [];
|
|
2233
|
+
if (old.description !== current.description) changes.push("description");
|
|
2234
|
+
if (old.priority !== current.priority) changes.push("priority");
|
|
2235
|
+
if (old.type !== current.type) changes.push("type");
|
|
2236
|
+
if (JSON.stringify(old.acceptanceCriteria) !== JSON.stringify(current.acceptanceCriteria)) {
|
|
2237
|
+
changes.push("acceptance criteria");
|
|
2238
|
+
}
|
|
2239
|
+
if (JSON.stringify(old.blockedBy) !== JSON.stringify(current.blockedBy)) {
|
|
2240
|
+
changes.push("dependencies");
|
|
2241
|
+
}
|
|
2242
|
+
return changes;
|
|
2243
|
+
}
|
|
2244
|
+
buildTaskDescription(req) {
|
|
2245
|
+
const lines = [req.description, "", `**Type:** ${req.type}`, `**Priority:** ${req.priority}`];
|
|
2246
|
+
if (req.acceptanceCriteria.length > 0) {
|
|
2247
|
+
lines.push("", "**Acceptance Criteria:**");
|
|
2248
|
+
for (const ac of req.acceptanceCriteria) {
|
|
2249
|
+
lines.push(`- ${ac}`);
|
|
2250
|
+
}
|
|
2251
|
+
}
|
|
2252
|
+
return lines.join("\n");
|
|
2253
|
+
}
|
|
2254
|
+
mapReqType(type) {
|
|
2255
|
+
switch (type) {
|
|
2256
|
+
case "functional":
|
|
2257
|
+
return "feature";
|
|
2258
|
+
case "security":
|
|
2259
|
+
return "feature";
|
|
2260
|
+
case "performance":
|
|
2261
|
+
return "feature";
|
|
2262
|
+
case "ux":
|
|
2263
|
+
return "feature";
|
|
2264
|
+
default:
|
|
2265
|
+
return "feature";
|
|
2266
|
+
}
|
|
2267
|
+
}
|
|
2268
|
+
};
|
|
2269
|
+
|
|
2270
|
+
// src/sdd/auto-executor.ts
|
|
2271
|
+
var AutoExecutor = class {
|
|
2272
|
+
opts;
|
|
2273
|
+
stopped = false;
|
|
2274
|
+
retryMap = /* @__PURE__ */ new Map();
|
|
2275
|
+
constructor(opts) {
|
|
2276
|
+
this.opts = opts;
|
|
2277
|
+
}
|
|
2278
|
+
/**
|
|
2279
|
+
* Execute all tasks in the graph, respecting dependencies.
|
|
2280
|
+
*/
|
|
2281
|
+
async execute(graph, spec) {
|
|
2282
|
+
this.stopped = false;
|
|
2283
|
+
this.retryMap.clear();
|
|
2284
|
+
const startTime = Date.now();
|
|
2285
|
+
const critical = analyzeCriticalPath(graph);
|
|
2286
|
+
let completed = 0;
|
|
2287
|
+
let failed = 0;
|
|
2288
|
+
const skipped = 0;
|
|
2289
|
+
let retried = 0;
|
|
2290
|
+
while (!this.stopped) {
|
|
2291
|
+
const readyTasks = this.getReadyTasks(graph);
|
|
2292
|
+
if (readyTasks.length === 0) {
|
|
2293
|
+
const allDone = Array.from(graph.nodes.values()).every(
|
|
2294
|
+
(n) => n.status === "completed" || n.status === "failed"
|
|
2295
|
+
);
|
|
2296
|
+
if (allDone) break;
|
|
2297
|
+
const hasDeadlock = this.detectDeadlock(graph);
|
|
2298
|
+
if (hasDeadlock) break;
|
|
2299
|
+
break;
|
|
2300
|
+
}
|
|
2301
|
+
const batch = readyTasks.slice(0, this.opts.maxConcurrent ?? 1);
|
|
2302
|
+
const results = await Promise.allSettled(
|
|
2303
|
+
batch.map((task) => this.executeTaskWithRetry(task, graph, spec))
|
|
2304
|
+
);
|
|
2305
|
+
for (let i = 0; i < results.length; i++) {
|
|
2306
|
+
const result = results[i];
|
|
2307
|
+
const task = batch[i];
|
|
2308
|
+
if (!result || !task) continue;
|
|
2309
|
+
if (result.status === "fulfilled") {
|
|
2310
|
+
const { result: execResult, retries } = result.value;
|
|
2311
|
+
if (execResult.success) {
|
|
2312
|
+
this.opts.tracker.updateNodeStatus(task.id, "completed");
|
|
2313
|
+
completed++;
|
|
2314
|
+
if (retries > 0) retried++;
|
|
2315
|
+
this.opts.onTaskComplete?.(task, execResult);
|
|
2316
|
+
} else if (execResult.retry) {
|
|
2317
|
+
retried++;
|
|
2318
|
+
} else {
|
|
2319
|
+
this.opts.tracker.updateNodeStatus(task.id, "failed", execResult.error);
|
|
2320
|
+
failed++;
|
|
2321
|
+
}
|
|
2322
|
+
} else {
|
|
2323
|
+
this.opts.tracker.updateNodeStatus(task.id, "failed", String(result.reason));
|
|
2324
|
+
failed++;
|
|
2325
|
+
this.opts.onTaskFail?.(task, result.reason, 0);
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2329
|
+
const duration = Date.now() - startTime;
|
|
2330
|
+
const summary = {
|
|
2331
|
+
total: graph.nodes.size,
|
|
2332
|
+
completed,
|
|
2333
|
+
failed,
|
|
2334
|
+
skipped,
|
|
2335
|
+
retried,
|
|
2336
|
+
duration,
|
|
2337
|
+
criticalPath: critical.criticalPath
|
|
2338
|
+
};
|
|
2339
|
+
this.opts.onDone?.(summary);
|
|
2340
|
+
return summary;
|
|
2341
|
+
}
|
|
2342
|
+
/** Stop execution. */
|
|
2343
|
+
stop() {
|
|
2344
|
+
this.stopped = true;
|
|
2345
|
+
}
|
|
2346
|
+
/** Get tasks that are ready to execute (all dependencies completed). */
|
|
2347
|
+
getReadyTasks(graph) {
|
|
2348
|
+
const ready = [];
|
|
2349
|
+
for (const node of graph.nodes.values()) {
|
|
2350
|
+
if (node.status !== "pending") continue;
|
|
2351
|
+
const blockers = graph.edges.filter((e) => e.type === "depends_on" && e.from === node.id).map((e) => graph.nodes.get(e.to)).filter(Boolean);
|
|
2352
|
+
const allBlockersDone = blockers.every((b) => b.status === "completed");
|
|
2353
|
+
if (allBlockersDone) {
|
|
2354
|
+
ready.push(node);
|
|
2355
|
+
}
|
|
2356
|
+
}
|
|
2357
|
+
const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
2358
|
+
ready.sort((a, b) => (priorityOrder[a.priority] ?? 4) - (priorityOrder[b.priority] ?? 4));
|
|
2359
|
+
return ready;
|
|
2360
|
+
}
|
|
2361
|
+
/** Execute a single task with retry logic. */
|
|
2362
|
+
async executeTaskWithRetry(task, graph, spec) {
|
|
2363
|
+
const maxRetries = this.opts.maxRetries ?? 2;
|
|
2364
|
+
let retryCount = this.retryMap.get(task.id) ?? 0;
|
|
2365
|
+
while (retryCount <= maxRetries) {
|
|
2366
|
+
this.opts.tracker.updateNodeStatus(task.id, "in_progress");
|
|
2367
|
+
this.opts.onTaskStart?.(task);
|
|
2368
|
+
const dependencies = this.getTaskDependencies(task.id, graph);
|
|
2369
|
+
const dependents = this.getTaskDependents(task.id, graph);
|
|
2370
|
+
const context = {
|
|
2371
|
+
spec,
|
|
2372
|
+
graph,
|
|
2373
|
+
task,
|
|
2374
|
+
dependencies,
|
|
2375
|
+
dependents,
|
|
2376
|
+
retryCount
|
|
2377
|
+
};
|
|
2378
|
+
try {
|
|
2379
|
+
const result = await this.opts.executeTask(task, context);
|
|
2380
|
+
if (result.success) {
|
|
2381
|
+
const retriesForTask = this.retryMap.get(task.id) ?? 0;
|
|
2382
|
+
this.retryMap.delete(task.id);
|
|
2383
|
+
return { result, retries: retriesForTask };
|
|
2384
|
+
}
|
|
2385
|
+
if (result.retry && retryCount < maxRetries) {
|
|
2386
|
+
retryCount++;
|
|
2387
|
+
this.retryMap.set(task.id, retryCount);
|
|
2388
|
+
this.opts.tracker.updateNodeStatus(task.id, "pending");
|
|
2389
|
+
continue;
|
|
2390
|
+
}
|
|
2391
|
+
return { result, retries: retryCount };
|
|
2392
|
+
} catch (error) {
|
|
2393
|
+
if (retryCount < maxRetries) {
|
|
2394
|
+
retryCount++;
|
|
2395
|
+
this.retryMap.set(task.id, retryCount);
|
|
2396
|
+
this.opts.tracker.updateNodeStatus(task.id, "pending");
|
|
2397
|
+
this.opts.onTaskFail?.(task, error, retryCount);
|
|
2398
|
+
continue;
|
|
2399
|
+
}
|
|
2400
|
+
return {
|
|
2401
|
+
result: {
|
|
2402
|
+
success: false,
|
|
2403
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2404
|
+
},
|
|
2405
|
+
retries: retryCount
|
|
2406
|
+
};
|
|
2407
|
+
}
|
|
2408
|
+
}
|
|
2409
|
+
return { result: { success: false, error: "Max retries exceeded" }, retries: retryCount };
|
|
2410
|
+
}
|
|
2411
|
+
/** Get tasks that this task depends on. */
|
|
2412
|
+
getTaskDependencies(taskId, graph) {
|
|
2413
|
+
return graph.edges.filter((e) => e.type === "depends_on" && e.from === taskId).map((e) => graph.nodes.get(e.to)).filter(Boolean);
|
|
2414
|
+
}
|
|
2415
|
+
/** Get tasks that depend on this task. */
|
|
2416
|
+
getTaskDependents(taskId, graph) {
|
|
2417
|
+
return graph.edges.filter((e) => e.type === "depends_on" && e.to === taskId).map((e) => graph.nodes.get(e.from)).filter(Boolean);
|
|
2418
|
+
}
|
|
2419
|
+
/** Detect deadlock: all remaining tasks are blocked by failed tasks. */
|
|
2420
|
+
detectDeadlock(graph) {
|
|
2421
|
+
const remaining = Array.from(graph.nodes.values()).filter(
|
|
2422
|
+
(n) => n.status === "pending" || n.status === "blocked"
|
|
2423
|
+
);
|
|
2424
|
+
if (remaining.length === 0) return false;
|
|
2425
|
+
return remaining.every((node) => {
|
|
2426
|
+
const blockers = graph.edges.filter((e) => e.type === "depends_on" && e.from === node.id).map((e) => graph.nodes.get(e.to)).filter(Boolean);
|
|
2427
|
+
return blockers.some((b) => b.status === "failed");
|
|
2428
|
+
});
|
|
2429
|
+
}
|
|
2430
|
+
};
|
|
2431
|
+
function createAutoExecutor(opts) {
|
|
2432
|
+
return new AutoExecutor({
|
|
2433
|
+
tracker: opts.tracker,
|
|
2434
|
+
events: opts.events,
|
|
2435
|
+
executeTask: opts.executeTask,
|
|
2436
|
+
maxConcurrent: opts.maxConcurrent,
|
|
2437
|
+
maxRetries: opts.maxRetries
|
|
2438
|
+
});
|
|
2439
|
+
}
|
|
861
2440
|
|
|
862
|
-
export { DefaultTaskStore, SpecDrivenDev, SpecParser, TaskFlow, TaskGenerator, TaskTracker };
|
|
2441
|
+
export { AISpecBuilder, AutoExecutor, DefaultTaskStore, SPEC_TEMPLATES, SpecDrivenDev, SpecParser, SpecStore, SpecVersioning, TaskFlow, TaskGenerator, TaskGraphStore, TaskTracker, analyzeCriticalPath, createAutoExecutor, getTemplate, listTemplates, renderProgress, renderSpecAnalysis, renderTaskGraph, renderTaskList, templateToMarkdown };
|
|
863
2442
|
//# sourceMappingURL=index.js.map
|
|
864
2443
|
//# sourceMappingURL=index.js.map
|