opencode-swarm 7.106.0 → 7.107.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.opencode/skills/critic-gate/SKILL.md +19 -5
- package/.opencode/skills/plan/SKILL.md +18 -3
- package/.opencode/skills/resume/SKILL.md +4 -1
- package/.opencode/skills/writing-tests/SKILL.md +31 -0
- package/dist/agents/critic.d.ts +1 -1
- package/dist/agents/explorer.d.ts +2 -2
- package/dist/cli/curator-drift-6ad8mxxb.js +211 -0
- package/dist/cli/curator-llm-factory-gdhtm1hh.js +31 -0
- package/dist/cli/curator-np2ky29t.js +51 -0
- package/dist/cli/{evidence-summary-service-v9kmv783.js → evidence-summary-service-dh44gevz.js} +4 -3
- package/dist/cli/{explorer-rwfvbbx5.js → explorer-ksr3xq7n.js} +1 -1
- package/dist/cli/{guardrail-explain-fvjc90wa.js → guardrail-explain-414smfg4.js} +13 -11
- package/dist/cli/hive-promoter-y9r8d315.js +39 -0
- package/dist/cli/{index-ydgy7vg5.js → index-09xpycan.js} +31 -5
- package/dist/cli/index-357p0baj.js +719 -0
- package/dist/cli/{index-7dtekwvd.js → index-7v734syt.js} +1 -1
- package/dist/cli/{index-z4t3w4xf.js → index-87d4wsv9.js} +14 -12
- package/dist/cli/{index-vwyjkzgs.js → index-9wstcavp.js} +1 -1
- package/dist/cli/index-c8s9a3zh.js +73 -0
- package/dist/cli/{index-f4yjypa4.js → index-gt514nt1.js} +29028 -27099
- package/dist/cli/{index-k2rmw9sz.js → index-jjp5qrjv.js} +265 -843
- package/dist/cli/{index-e9n3xsk0.js → index-t4hbye3v.js} +1 -1
- package/dist/cli/index.js +12 -10
- package/dist/cli/{pending-delegations-dvprsdfy.js → pending-delegations-46xf2n2e.js} +2 -2
- package/dist/cli/{pr-subscriptions-zfxjtyzw.js → pr-subscriptions-3c34rnm1.js} +3 -3
- package/dist/cli/{skill-generator-99rrfwae.js → skill-generator-y6mmq8n5.js} +1 -1
- package/dist/commands/curate.d.ts +38 -1
- package/dist/commands/registry.d.ts +2 -2
- package/dist/commands/reset-session.d.ts +10 -0
- package/dist/hooks/curator-postmortem.d.ts +52 -3
- package/dist/hooks/curator.d.ts +2 -1
- package/dist/hooks/delegation-gate/worktree-isolation.d.ts +3 -1
- package/dist/hooks/guardrails/index.d.ts +3 -1
- package/dist/hooks/system-enhancer.d.ts +4 -0
- package/dist/index.js +458 -386
- package/dist/plan/manager.d.ts +15 -1
- package/dist/tools/update-task-status.d.ts +1 -0
- package/dist/utils/spec-hash.d.ts +36 -0
- package/package.json +1 -1
- package/dist/cli/{index-7ztmmqpt.js → index-mpe0yk9s.js} +3 -3
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
// @bun
|
|
2
|
+
import {
|
|
3
|
+
readEffectiveSpecSync
|
|
4
|
+
} from "./index-357p0baj.js";
|
|
2
5
|
import {
|
|
3
6
|
isValidTaskId,
|
|
4
7
|
readTaskEvidence,
|
|
@@ -8,7 +11,7 @@ import {
|
|
|
8
11
|
import {
|
|
9
12
|
readSwarmFileAsync,
|
|
10
13
|
validateSwarmPath
|
|
11
|
-
} from "./index-
|
|
14
|
+
} from "./index-mpe0yk9s.js";
|
|
12
15
|
import {
|
|
13
16
|
readCachedParsedFile
|
|
14
17
|
} from "./index-jtqkh8jf.js";
|
|
@@ -36,13 +39,13 @@ import {
|
|
|
36
39
|
// src/plan/manager.ts
|
|
37
40
|
import {
|
|
38
41
|
copyFileSync,
|
|
39
|
-
existsSync as
|
|
40
|
-
readdirSync
|
|
41
|
-
renameSync as
|
|
42
|
+
existsSync as existsSync5,
|
|
43
|
+
readdirSync,
|
|
44
|
+
renameSync as renameSync4,
|
|
42
45
|
unlinkSync as unlinkSync3
|
|
43
46
|
} from "fs";
|
|
44
47
|
import * as fsPromises from "fs/promises";
|
|
45
|
-
import * as
|
|
48
|
+
import * as path5 from "path";
|
|
46
49
|
|
|
47
50
|
// src/config/plan-schema.ts
|
|
48
51
|
var ExecutionProfileSchema = exports_external.object({
|
|
@@ -1084,759 +1087,172 @@ function readTaskScopes(directory, taskId) {
|
|
|
1084
1087
|
}
|
|
1085
1088
|
|
|
1086
1089
|
// src/utils/spec-hash.ts
|
|
1087
|
-
import { createHash as createHash2 } from "crypto";
|
|
1088
|
-
|
|
1089
|
-
// src/sdd/effective-spec.ts
|
|
1090
1090
|
import { createHash } from "crypto";
|
|
1091
|
-
import * as
|
|
1092
|
-
import
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
var SpecRequirementSchema = exports_external.object({
|
|
1097
|
-
id: exports_external.string().regex(/^FR-(?!000)\d{3}$/, "Requirement ID must match FR-### pattern (e.g., FR-001)"),
|
|
1098
|
-
obligation: ObligationSchema,
|
|
1099
|
-
text: exports_external.string().min(1)
|
|
1100
|
-
});
|
|
1101
|
-
var SpecScenarioSchema = exports_external.object({
|
|
1102
|
-
name: exports_external.string().min(1),
|
|
1103
|
-
given: exports_external.array(exports_external.string()).optional().default([]),
|
|
1104
|
-
when: exports_external.array(exports_external.string()).min(1, 'Scenario must have at least one "when" clause'),
|
|
1105
|
-
thenClauses: exports_external.array(exports_external.string()).min(1, 'Scenario must have at least one "then" clause')
|
|
1106
|
-
});
|
|
1107
|
-
var SpecSectionSchema = exports_external.object({
|
|
1108
|
-
name: exports_external.string().min(1),
|
|
1109
|
-
requirements: exports_external.array(SpecRequirementSchema).default([])
|
|
1110
|
-
});
|
|
1111
|
-
var SwarmSpecSchema = exports_external.object({
|
|
1112
|
-
title: exports_external.string().min(1),
|
|
1113
|
-
purpose: exports_external.string().min(1),
|
|
1114
|
-
sections: exports_external.array(SpecSectionSchema).min(1, "Spec must have at least one section")
|
|
1115
|
-
});
|
|
1116
|
-
var SpecDeltaSchema = exports_external.object({
|
|
1117
|
-
added: exports_external.array(SpecRequirementSchema).default([]),
|
|
1118
|
-
modified: exports_external.array(SpecRequirementSchema).default([]),
|
|
1119
|
-
removed: exports_external.array(SpecRequirementSchema).default([])
|
|
1120
|
-
});
|
|
1121
|
-
var DeltaSpecSchema = exports_external.union([
|
|
1122
|
-
SwarmSpecSchema,
|
|
1123
|
-
SpecDeltaSchema
|
|
1124
|
-
]);
|
|
1125
|
-
var FENCED_BLOCK_PATTERN = /```[\s\S]*?```/g;
|
|
1126
|
-
var INLINE_CODE_PATTERN = /`[^`]*`/g;
|
|
1127
|
-
var FR_ID_PATTERN = /\bFR-\d{3}\b/g;
|
|
1128
|
-
var OBLIGATION_PATTERN = /\b(MUST|SHALL|SHOULD|MAY)\b/g;
|
|
1129
|
-
var SECTION_HEADER_PATTERN = /^##\s+.+$/gm;
|
|
1130
|
-
function stripCodeBlocks(content) {
|
|
1131
|
-
return content.replace(FENCED_BLOCK_PATTERN, "").replace(INLINE_CODE_PATTERN, "");
|
|
1132
|
-
}
|
|
1133
|
-
function getLineNumber(content, position) {
|
|
1134
|
-
const prefix = content.substring(0, position);
|
|
1135
|
-
return (prefix.match(/\n/g) || []).length + 1;
|
|
1136
|
-
}
|
|
1137
|
-
function validateSpecContent(content) {
|
|
1138
|
-
const issues = [];
|
|
1139
|
-
if (!content || content.trim().length === 0) {
|
|
1140
|
-
return { valid: false, issues: [{ line: 1, message: "Content is empty" }] };
|
|
1141
|
-
}
|
|
1142
|
-
const strippedContent = stripCodeBlocks(content);
|
|
1143
|
-
const frMatches = strippedContent.match(FR_ID_PATTERN);
|
|
1144
|
-
if (!frMatches || frMatches.length === 0) {
|
|
1145
|
-
issues.push({
|
|
1146
|
-
line: 1,
|
|
1147
|
-
message: "No FR-### requirement IDs found in spec content"
|
|
1148
|
-
});
|
|
1149
|
-
}
|
|
1150
|
-
const obligationMatches = strippedContent.match(OBLIGATION_PATTERN);
|
|
1151
|
-
if (!obligationMatches || obligationMatches.length === 0) {
|
|
1152
|
-
issues.push({
|
|
1153
|
-
line: 1,
|
|
1154
|
-
message: "No obligation keywords (MUST, SHALL, SHOULD, MAY) found in spec content"
|
|
1155
|
-
});
|
|
1156
|
-
}
|
|
1157
|
-
const sectionMatches = strippedContent.match(SECTION_HEADER_PATTERN);
|
|
1158
|
-
if (!sectionMatches || sectionMatches.length === 0) {
|
|
1159
|
-
issues.push({
|
|
1160
|
-
line: 1,
|
|
1161
|
-
message: "No section headers (## Section Name) found in spec content"
|
|
1162
|
-
});
|
|
1163
|
-
}
|
|
1164
|
-
const idMatches = strippedContent.matchAll(/\bFR-(\d{3})\b/g);
|
|
1165
|
-
for (const idMatch of idMatches) {
|
|
1166
|
-
const num = parseInt(idMatch[1], 10);
|
|
1167
|
-
if (num === 0) {
|
|
1168
|
-
const pos = idMatch.index;
|
|
1169
|
-
issues.push({
|
|
1170
|
-
line: getLineNumber(strippedContent, pos),
|
|
1171
|
-
message: `Invalid FR-ID "${idMatch[0]}" \u2014 number must be 001-999`
|
|
1172
|
-
});
|
|
1173
|
-
}
|
|
1174
|
-
}
|
|
1175
|
-
return {
|
|
1176
|
-
valid: issues.length === 0,
|
|
1177
|
-
issues
|
|
1178
|
-
};
|
|
1179
|
-
}
|
|
1180
|
-
|
|
1181
|
-
// src/sdd/effective-spec.ts
|
|
1182
|
-
var SWARM_SPEC_REL = path4.join(".swarm", "spec.md");
|
|
1183
|
-
var OPENSPEC_ROOT = "openspec";
|
|
1184
|
-
var SPECKIT_MARKER = ".specify";
|
|
1185
|
-
var SPECKIT_SPECS_DIR = "specs";
|
|
1186
|
-
var MAX_SPEC_BYTES = 256 * 1024;
|
|
1187
|
-
var MAX_SOURCE_BYTES = 512 * 1024;
|
|
1188
|
-
var MAX_SPEC_FILES = 100;
|
|
1189
|
-
var MAX_WALK_DEPTH = 10;
|
|
1190
|
-
var SPECKIT_REQUIRED_SECTIONS = [
|
|
1191
|
-
"## Functional Requirements",
|
|
1192
|
-
"## Success Criteria"
|
|
1193
|
-
];
|
|
1194
|
-
function toPosix(relPath) {
|
|
1195
|
-
return relPath.split(path4.sep).join("/");
|
|
1196
|
-
}
|
|
1197
|
-
function hash(content) {
|
|
1198
|
-
return createHash("sha256").update(content, "utf-8").digest("hex");
|
|
1199
|
-
}
|
|
1200
|
-
function readTextBounded(absPath) {
|
|
1201
|
-
let stat;
|
|
1202
|
-
try {
|
|
1203
|
-
stat = fs4.lstatSync(absPath);
|
|
1204
|
-
} catch {
|
|
1205
|
-
return null;
|
|
1206
|
-
}
|
|
1207
|
-
if (!stat.isFile() || stat.size > MAX_SOURCE_BYTES) {
|
|
1208
|
-
return null;
|
|
1209
|
-
}
|
|
1210
|
-
try {
|
|
1211
|
-
return fs4.readFileSync(absPath, "utf-8");
|
|
1212
|
-
} catch {
|
|
1091
|
+
import * as fsSync from "fs";
|
|
1092
|
+
import { join as join3 } from "path";
|
|
1093
|
+
async function computeSpecHash(directory) {
|
|
1094
|
+
const spec = _internals3.readEffectiveSpecSync(directory);
|
|
1095
|
+
if (!spec)
|
|
1213
1096
|
return null;
|
|
1214
|
-
|
|
1097
|
+
return createHash("sha256").update(spec.content, "utf-8").digest("hex");
|
|
1215
1098
|
}
|
|
1216
|
-
function
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1099
|
+
async function isSpecStale(directory, plan) {
|
|
1100
|
+
const currentHash = await _internals3.computeSpecHash(directory);
|
|
1101
|
+
if (!plan.specHash) {
|
|
1102
|
+
return { stale: false };
|
|
1103
|
+
}
|
|
1104
|
+
if (currentHash === null) {
|
|
1221
1105
|
return {
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1106
|
+
stale: true,
|
|
1107
|
+
reason: "effective spec has been deleted",
|
|
1108
|
+
currentHash: null
|
|
1225
1109
|
};
|
|
1226
|
-
} catch {
|
|
1227
|
-
return null;
|
|
1228
|
-
}
|
|
1229
|
-
}
|
|
1230
|
-
function walkSpecFiles(root, startRel) {
|
|
1231
|
-
const start = path4.join(root, startRel);
|
|
1232
|
-
if (!fs4.existsSync(start))
|
|
1233
|
-
return [];
|
|
1234
|
-
const artifacts = [];
|
|
1235
|
-
const stack = [
|
|
1236
|
-
{ abs: start, depth: 0 }
|
|
1237
|
-
];
|
|
1238
|
-
while (stack.length > 0 && artifacts.length < MAX_SPEC_FILES) {
|
|
1239
|
-
const item = stack.pop();
|
|
1240
|
-
if (!item || item.depth > MAX_WALK_DEPTH)
|
|
1241
|
-
continue;
|
|
1242
|
-
let entries;
|
|
1243
|
-
try {
|
|
1244
|
-
entries = fs4.readdirSync(item.abs, { withFileTypes: true });
|
|
1245
|
-
} catch {
|
|
1246
|
-
continue;
|
|
1247
|
-
}
|
|
1248
|
-
const dirents = entries.filter((entry) => typeof entry?.name === "string");
|
|
1249
|
-
for (const entry of dirents.sort((a, b) => b.name.localeCompare(a.name))) {
|
|
1250
|
-
const abs = path4.join(item.abs, entry.name);
|
|
1251
|
-
if (entry.isSymbolicLink())
|
|
1252
|
-
continue;
|
|
1253
|
-
if (entry.isDirectory()) {
|
|
1254
|
-
stack.push({ abs, depth: item.depth + 1 });
|
|
1255
|
-
} else if (entry.isFile() && entry.name === "spec.md") {
|
|
1256
|
-
const artifact = fileArtifact(root, abs);
|
|
1257
|
-
if (artifact)
|
|
1258
|
-
artifacts.push(artifact);
|
|
1259
|
-
if (artifacts.length >= MAX_SPEC_FILES)
|
|
1260
|
-
break;
|
|
1261
|
-
}
|
|
1262
|
-
}
|
|
1263
1110
|
}
|
|
1264
|
-
|
|
1265
|
-
}
|
|
1266
|
-
function listOpenSpecChanges(root) {
|
|
1267
|
-
const changesDir = path4.join(root, OPENSPEC_ROOT, "changes");
|
|
1268
|
-
if (!fs4.existsSync(changesDir))
|
|
1269
|
-
return [];
|
|
1270
|
-
let entries;
|
|
1271
|
-
try {
|
|
1272
|
-
entries = fs4.readdirSync(changesDir, { withFileTypes: true });
|
|
1273
|
-
} catch {
|
|
1274
|
-
return [];
|
|
1275
|
-
}
|
|
1276
|
-
return entries.filter((entry) => entry.isDirectory() && entry.name !== "archive").sort((a, b) => a.name.localeCompare(b.name)).map((entry) => {
|
|
1277
|
-
const rel = path4.join(OPENSPEC_ROOT, "changes", entry.name);
|
|
1111
|
+
if (currentHash !== plan.specHash) {
|
|
1278
1112
|
return {
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
tasks: fs4.existsSync(path4.join(root, rel, "tasks.md")),
|
|
1283
|
-
specs: walkSpecFiles(root, path4.join(rel, "specs"))
|
|
1113
|
+
stale: true,
|
|
1114
|
+
reason: "effective spec has been modified since plan was saved",
|
|
1115
|
+
currentHash
|
|
1284
1116
|
};
|
|
1285
|
-
});
|
|
1286
|
-
}
|
|
1287
|
-
function detectKind(line, current) {
|
|
1288
|
-
const upper = line.toUpperCase();
|
|
1289
|
-
if (/^#{2,6}\s+ADDED\b/.test(upper))
|
|
1290
|
-
return "ADDED";
|
|
1291
|
-
if (/^#{2,6}\s+MODIFIED\b/.test(upper))
|
|
1292
|
-
return "MODIFIED";
|
|
1293
|
-
if (/^#{2,6}\s+REMOVED\b/.test(upper))
|
|
1294
|
-
return "REMOVED";
|
|
1295
|
-
return current;
|
|
1296
|
-
}
|
|
1297
|
-
function requirementTextFromBlock(title, block, kind) {
|
|
1298
|
-
const joined = block.join(" ").replace(/\s+/g, " ").trim();
|
|
1299
|
-
const obligation = joined.match(/\b(MUST|SHALL|SHOULD|MAY)\b/i)?.[1];
|
|
1300
|
-
if (obligation)
|
|
1301
|
-
return joined;
|
|
1302
|
-
if (kind === "REMOVED") {
|
|
1303
|
-
return `MAY remove or retire behavior for ${title}.`;
|
|
1304
|
-
}
|
|
1305
|
-
return `MUST satisfy OpenSpec requirement ${title}.`;
|
|
1306
|
-
}
|
|
1307
|
-
function parseRequirements(content, sourceRel, defaultKind) {
|
|
1308
|
-
const requirements = [];
|
|
1309
|
-
const lines = content.replace(/\r\n/g, `
|
|
1310
|
-
`).split(`
|
|
1311
|
-
`);
|
|
1312
|
-
let kind = defaultKind;
|
|
1313
|
-
for (let i = 0;i < lines.length; i++) {
|
|
1314
|
-
const line = lines[i];
|
|
1315
|
-
kind = detectKind(line, kind);
|
|
1316
|
-
const explicit = line.match(/\b(FR-(?!000)\d{3})\b/);
|
|
1317
|
-
if (explicit && /\b(MUST|SHALL|SHOULD|MAY)\b/i.test(line)) {
|
|
1318
|
-
requirements.push({
|
|
1319
|
-
id: explicit[1].toUpperCase(),
|
|
1320
|
-
kind,
|
|
1321
|
-
title: explicit[1].toUpperCase(),
|
|
1322
|
-
text: line.trim().replace(/^\s*[-*]\s*/, ""),
|
|
1323
|
-
sourceRel
|
|
1324
|
-
});
|
|
1325
|
-
continue;
|
|
1326
|
-
}
|
|
1327
|
-
const openSpecReq = line.match(/^#{3,4}\s+Requirement:\s*(.+)$/i);
|
|
1328
|
-
if (!openSpecReq)
|
|
1329
|
-
continue;
|
|
1330
|
-
const title = openSpecReq[1].trim();
|
|
1331
|
-
const block = [];
|
|
1332
|
-
for (let j = i + 1;j < lines.length; j++) {
|
|
1333
|
-
if (/^##\s+/.test(lines[j]))
|
|
1334
|
-
break;
|
|
1335
|
-
if (/^#{3,4}\s+Requirement:/i.test(lines[j]))
|
|
1336
|
-
break;
|
|
1337
|
-
const trimmed = lines[j].trim();
|
|
1338
|
-
if (trimmed)
|
|
1339
|
-
block.push(trimmed);
|
|
1340
|
-
}
|
|
1341
|
-
const id = block.join(`
|
|
1342
|
-
`).match(/\b(FR-(?!000)\d{3})\b/)?.[1] ?? null;
|
|
1343
|
-
requirements.push({
|
|
1344
|
-
id: id?.toUpperCase() ?? null,
|
|
1345
|
-
kind,
|
|
1346
|
-
title,
|
|
1347
|
-
text: requirementTextFromBlock(title, block, kind),
|
|
1348
|
-
sourceRel
|
|
1349
|
-
});
|
|
1350
|
-
}
|
|
1351
|
-
return requirements;
|
|
1352
|
-
}
|
|
1353
|
-
function nextFrId(used, warnings, reserved) {
|
|
1354
|
-
for (let n = 1;n <= 999; n++) {
|
|
1355
|
-
const id = `FR-${String(n).padStart(3, "0")}`;
|
|
1356
|
-
if (!used.has(id) && !reserved?.has(id)) {
|
|
1357
|
-
used.add(id);
|
|
1358
|
-
return id;
|
|
1359
|
-
}
|
|
1360
1117
|
}
|
|
1361
|
-
|
|
1362
|
-
return "FR-999";
|
|
1363
|
-
}
|
|
1364
|
-
function renderRequirement(req, used, warnings, reserved) {
|
|
1365
|
-
const id = req.id && !used.has(req.id) ? req.id : nextFrId(used, warnings, reserved);
|
|
1366
|
-
if (req.id && req.id !== id) {
|
|
1367
|
-
warnings.push(`Duplicate requirement id ${req.id} in ${req.sourceRel}; generated ${id}.`);
|
|
1368
|
-
}
|
|
1369
|
-
if (req.id === id)
|
|
1370
|
-
used.add(id);
|
|
1371
|
-
let text = req.text;
|
|
1372
|
-
if (!text.includes(id)) {
|
|
1373
|
-
text = `${id}: ${text}`;
|
|
1374
|
-
}
|
|
1375
|
-
return `- ${text} _(source: ${req.sourceRel})_`;
|
|
1118
|
+
return { stale: false };
|
|
1376
1119
|
}
|
|
1377
|
-
function
|
|
1378
|
-
const
|
|
1379
|
-
const markerPath = path4.join(root, SPECKIT_MARKER);
|
|
1380
|
-
const markerPresent = fs4.statSync(markerPath, { throwIfNoEntry: false })?.isDirectory() ?? false;
|
|
1381
|
-
if (!markerPresent) {
|
|
1382
|
-
return { markerPresent: false, features: [] };
|
|
1383
|
-
}
|
|
1384
|
-
const specsRoot = path4.join(root, SPECKIT_SPECS_DIR);
|
|
1385
|
-
if (!fs4.existsSync(specsRoot)) {
|
|
1386
|
-
return { markerPresent: true, features: [] };
|
|
1387
|
-
}
|
|
1388
|
-
let entries;
|
|
1120
|
+
function isObligationPreserving(directory) {
|
|
1121
|
+
const snapshotPath = join3(directory, ".swarm", "spec-snapshot.md");
|
|
1389
1122
|
try {
|
|
1390
|
-
|
|
1391
|
-
} catch {
|
|
1392
|
-
return { markerPresent: true, features: [] };
|
|
1393
|
-
}
|
|
1394
|
-
const featureDirs = entries.filter((entry) => typeof entry?.name === "string" && !entry.isSymbolicLink() && entry.isDirectory()).sort((a, b) => a.name.localeCompare(b.name));
|
|
1395
|
-
const features = [];
|
|
1396
|
-
for (const entry of featureDirs) {
|
|
1397
|
-
if (features.length >= MAX_SPEC_FILES)
|
|
1398
|
-
break;
|
|
1399
|
-
const specAbs = path4.join(specsRoot, entry.name, "spec.md");
|
|
1400
|
-
let stat;
|
|
1123
|
+
let recordedContent;
|
|
1401
1124
|
try {
|
|
1402
|
-
|
|
1125
|
+
recordedContent = fsSync.readFileSync(snapshotPath, "utf-8");
|
|
1403
1126
|
} catch {
|
|
1404
|
-
|
|
1127
|
+
return false;
|
|
1405
1128
|
}
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
}
|
|
1413
|
-
return { markerPresent: true, features };
|
|
1414
|
-
}
|
|
1415
|
-
function parseSpeckitRequirements(content, sourceRel) {
|
|
1416
|
-
const requirements = [];
|
|
1417
|
-
const lines = content.replace(/\r\n/g, `
|
|
1418
|
-
`).split(`
|
|
1129
|
+
const currentResult = _internals3.readEffectiveSpecSync(directory);
|
|
1130
|
+
if (!currentResult || !currentResult.content) {
|
|
1131
|
+
return false;
|
|
1132
|
+
}
|
|
1133
|
+
const currentContent = currentResult.content;
|
|
1134
|
+
const normalize = (text) => text.replace(/\r\n/g, `
|
|
1419
1135
|
`);
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1136
|
+
const recordedNorm = normalize(recordedContent);
|
|
1137
|
+
const currentNorm = normalize(currentContent);
|
|
1138
|
+
const splitParagraphs = (text) => text.split(/\n\s*\n/).map((p) => p.trim()).filter((p) => p.length > 0);
|
|
1139
|
+
const recordedParagraphs = splitParagraphs(recordedNorm);
|
|
1140
|
+
const currentParagraphs = splitParagraphs(currentNorm);
|
|
1141
|
+
const isObligationLine = (line) => /\bSC-\d+/.test(line) || /\bFR-\d+/.test(line) || /\bMUST\b/i.test(line) || /\bSHALL\b/i.test(line);
|
|
1142
|
+
const isObligationParagraph = (para) => para.split(`
|
|
1143
|
+
`).some(isObligationLine);
|
|
1144
|
+
const recordedObligationParas = recordedParagraphs.filter(isObligationParagraph).sort();
|
|
1145
|
+
const currentObligationParas = currentParagraphs.filter(isObligationParagraph).sort();
|
|
1146
|
+
if (recordedObligationParas.length !== currentObligationParas.length) {
|
|
1147
|
+
return false;
|
|
1425
1148
|
}
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
if (!/\b(MUST|SHALL|SHOULD|MAY)\b/i.test(line))
|
|
1431
|
-
continue;
|
|
1432
|
-
const text = line.trim().replace(/^\s*[-*]\s+/, "");
|
|
1433
|
-
const explicit = line.match(/\b(FR-(?!000)\d{3})\b/);
|
|
1434
|
-
if (explicit) {
|
|
1435
|
-
requirements.push({
|
|
1436
|
-
id: explicit[1].toUpperCase(),
|
|
1437
|
-
kind: "CURRENT",
|
|
1438
|
-
title: explicit[1].toUpperCase(),
|
|
1439
|
-
text,
|
|
1440
|
-
sourceRel
|
|
1441
|
-
});
|
|
1442
|
-
continue;
|
|
1149
|
+
for (let i = 0;i < recordedObligationParas.length; i++) {
|
|
1150
|
+
if (recordedObligationParas[i] !== currentObligationParas[i]) {
|
|
1151
|
+
return false;
|
|
1152
|
+
}
|
|
1443
1153
|
}
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
title: text,
|
|
1448
|
-
text,
|
|
1449
|
-
sourceRel
|
|
1450
|
-
});
|
|
1154
|
+
return true;
|
|
1155
|
+
} catch {
|
|
1156
|
+
return false;
|
|
1451
1157
|
}
|
|
1452
|
-
return requirements;
|
|
1453
1158
|
}
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
let
|
|
1464
|
-
if (options.feature) {
|
|
1465
|
-
const found = detection.features.find((f) => f.featureId === options.feature);
|
|
1466
|
-
if (!found) {
|
|
1467
|
-
return {
|
|
1468
|
-
kind: "unknown_feature",
|
|
1469
|
-
feature: options.feature,
|
|
1470
|
-
available: detection.features.map((f) => f.featureId)
|
|
1471
|
-
};
|
|
1472
|
-
}
|
|
1473
|
-
selectedFeature = found;
|
|
1474
|
-
} else if (detection.features.length === 1) {
|
|
1475
|
-
selectedFeature = detection.features[0];
|
|
1476
|
-
} else {
|
|
1477
|
-
return {
|
|
1478
|
-
kind: "ambiguous",
|
|
1479
|
-
features: detection.features.map((f) => f.featureId)
|
|
1480
|
-
};
|
|
1481
|
-
}
|
|
1482
|
-
const specAbs = path4.join(root, selectedFeature.specRelPath);
|
|
1483
|
-
const content = readTextBounded(specAbs);
|
|
1484
|
-
if (content === null) {
|
|
1485
|
-
return { kind: "zero_requirements", feature: selectedFeature.featureId };
|
|
1486
|
-
}
|
|
1487
|
-
const warnings = [];
|
|
1488
|
-
const usedIds = new Set;
|
|
1489
|
-
const requirements = parseSpeckitRequirements(content, selectedFeature.specRelPath);
|
|
1490
|
-
if (requirements.length === 0) {
|
|
1491
|
-
return { kind: "zero_requirements", feature: selectedFeature.featureId };
|
|
1492
|
-
}
|
|
1493
|
-
const reservedIds = new Set;
|
|
1494
|
-
for (const req of requirements) {
|
|
1495
|
-
if (req.id)
|
|
1496
|
-
reservedIds.add(req.id);
|
|
1497
|
-
}
|
|
1498
|
-
let mtimeMs = 0;
|
|
1159
|
+
var _internals3 = {
|
|
1160
|
+
computeSpecDiff,
|
|
1161
|
+
computeSpecHash,
|
|
1162
|
+
isObligationPreserving,
|
|
1163
|
+
isSpecStale,
|
|
1164
|
+
readEffectiveSpecSync
|
|
1165
|
+
};
|
|
1166
|
+
function computeSpecDiff(directory) {
|
|
1167
|
+
const snapshotPath = join3(directory, ".swarm", "spec-snapshot.md");
|
|
1168
|
+
let recordedContent;
|
|
1499
1169
|
try {
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
"",
|
|
1505
|
-
"Generated from Spec-Kit feature artifacts. Update the source artifacts, then run `/swarm sdd project` to refresh this projection.",
|
|
1506
|
-
"",
|
|
1507
|
-
"## Source Artifacts",
|
|
1508
|
-
`- ${selectedFeature.specRelPath}`,
|
|
1509
|
-
"",
|
|
1510
|
-
"## Functional Requirements"
|
|
1511
|
-
];
|
|
1512
|
-
for (const req of requirements) {
|
|
1513
|
-
lines.push(renderRequirement(req, usedIds, warnings, reservedIds));
|
|
1514
|
-
}
|
|
1515
|
-
const projected = `${lines.join(`
|
|
1516
|
-
`)}
|
|
1517
|
-
`;
|
|
1518
|
-
if (projected.length > MAX_SPEC_BYTES) {
|
|
1519
|
-
return {
|
|
1520
|
-
kind: "too_large",
|
|
1521
|
-
feature: selectedFeature.featureId,
|
|
1522
|
-
bytes: projected.length
|
|
1523
|
-
};
|
|
1524
|
-
}
|
|
1525
|
-
const validation = validateSpecContent(projected);
|
|
1526
|
-
if (!validation.valid) {
|
|
1527
|
-
warnings.push(...validation.issues.map((issue) => `Projection line ${issue.line}: ${issue.message}`));
|
|
1528
|
-
}
|
|
1529
|
-
const spec = {
|
|
1530
|
-
source: "speckit_projection",
|
|
1531
|
-
content: projected,
|
|
1532
|
-
hash: hash(projected),
|
|
1533
|
-
mtime: mtimeMs > 0 ? new Date(mtimeMs).toISOString() : null,
|
|
1534
|
-
sourcePaths: [selectedFeature.specRelPath],
|
|
1535
|
-
warnings
|
|
1536
|
-
};
|
|
1537
|
-
return { kind: "ok", spec, feature: selectedFeature.featureId };
|
|
1538
|
-
}
|
|
1539
|
-
function buildSpeckitProjectionSync(directory, options = {}) {
|
|
1540
|
-
const resolution = resolveSpeckitProjection(directory, options);
|
|
1541
|
-
return resolution.kind === "ok" ? resolution.spec : null;
|
|
1542
|
-
}
|
|
1543
|
-
function loadSddStatusSync(directory, opts) {
|
|
1544
|
-
const root = path4.resolve(directory);
|
|
1545
|
-
const swSpecPath = path4.join(root, SWARM_SPEC_REL);
|
|
1546
|
-
const openSpecPath = path4.join(root, OPENSPEC_ROOT);
|
|
1547
|
-
const errors = [];
|
|
1548
|
-
const warnings = [];
|
|
1549
|
-
const swSpecExists = fs4.existsSync(swSpecPath);
|
|
1550
|
-
const openSpecExists = fs4.existsSync(openSpecPath);
|
|
1551
|
-
const currentSpecs = walkSpecFiles(root, path4.join(OPENSPEC_ROOT, "specs"));
|
|
1552
|
-
const changes = listOpenSpecChanges(root);
|
|
1553
|
-
const effectiveSpec = readEffectiveSpecSync(root, opts);
|
|
1554
|
-
if (openSpecExists && currentSpecs.length === 0 && changes.length === 0) {
|
|
1555
|
-
errors.push("openspec/ exists but contains no specs or active changes.");
|
|
1556
|
-
}
|
|
1557
|
-
for (const change of changes) {
|
|
1558
|
-
if (!change.proposal) {
|
|
1559
|
-
warnings.push(`Change ${change.id} is missing proposal.md.`);
|
|
1560
|
-
}
|
|
1561
|
-
if (!change.tasks) {
|
|
1562
|
-
warnings.push(`Change ${change.id} is missing tasks.md; tasks remain proposal input, not plan state.`);
|
|
1563
|
-
}
|
|
1564
|
-
if (change.specs.length === 0) {
|
|
1565
|
-
errors.push(`Change ${change.id} has no specs/**/spec.md delta files.`);
|
|
1170
|
+
const snapshotSize = fsSync.statSync(snapshotPath).size;
|
|
1171
|
+
const MAX_SNAPSHOT_BYTES = 512 * 1024;
|
|
1172
|
+
if (snapshotSize > MAX_SNAPSHOT_BYTES) {
|
|
1173
|
+
return null;
|
|
1566
1174
|
}
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
provider: effectiveSpec?.source ?? "none",
|
|
1570
|
-
swSpecExists,
|
|
1571
|
-
openSpecExists,
|
|
1572
|
-
currentSpecs,
|
|
1573
|
-
changes,
|
|
1574
|
-
effectiveSpec,
|
|
1575
|
-
errors,
|
|
1576
|
-
warnings
|
|
1577
|
-
};
|
|
1578
|
-
}
|
|
1579
|
-
function buildOpenSpecProjectionSync(directory, options = {}) {
|
|
1580
|
-
const root = path4.resolve(directory);
|
|
1581
|
-
const currentSpecs = walkSpecFiles(root, path4.join(OPENSPEC_ROOT, "specs"));
|
|
1582
|
-
const allChanges = listOpenSpecChanges(root);
|
|
1583
|
-
const changes = options.changeId ? allChanges.filter((change) => change.id === options.changeId) : allChanges;
|
|
1584
|
-
const sourcePaths = [];
|
|
1585
|
-
const warnings = [];
|
|
1586
|
-
const usedIds = new Set;
|
|
1587
|
-
const currentRequirements = [];
|
|
1588
|
-
const changeRequirements = new Map;
|
|
1589
|
-
let parsedRequirementCount = 0;
|
|
1590
|
-
let mtimeMs = 0;
|
|
1591
|
-
if (options.changeId && changes.length === 0)
|
|
1592
|
-
return null;
|
|
1593
|
-
if (currentSpecs.length === 0 && changes.length === 0)
|
|
1175
|
+
recordedContent = fsSync.readFileSync(snapshotPath, "utf-8");
|
|
1176
|
+
} catch {
|
|
1594
1177
|
return null;
|
|
1595
|
-
for (const artifact of currentSpecs) {
|
|
1596
|
-
const abs = path4.join(root, artifact.relPath);
|
|
1597
|
-
const content2 = readTextBounded(abs);
|
|
1598
|
-
if (content2 === null) {
|
|
1599
|
-
warnings.push(`Skipped unreadable or oversized spec ${artifact.relPath}.`);
|
|
1600
|
-
continue;
|
|
1601
|
-
}
|
|
1602
|
-
sourcePaths.push(artifact.relPath);
|
|
1603
|
-
mtimeMs = Math.max(mtimeMs, artifact.mtimeMs);
|
|
1604
|
-
const parsed = parseRequirements(content2, artifact.relPath, "CURRENT");
|
|
1605
|
-
currentRequirements.push(...parsed);
|
|
1606
|
-
parsedRequirementCount += parsed.length;
|
|
1607
1178
|
}
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
for (const artifact of change.specs) {
|
|
1611
|
-
const abs = path4.join(root, artifact.relPath);
|
|
1612
|
-
const content2 = readTextBounded(abs);
|
|
1613
|
-
if (content2 === null) {
|
|
1614
|
-
warnings.push(`Skipped unreadable or oversized spec ${artifact.relPath}.`);
|
|
1615
|
-
continue;
|
|
1616
|
-
}
|
|
1617
|
-
sourcePaths.push(artifact.relPath);
|
|
1618
|
-
mtimeMs = Math.max(mtimeMs, artifact.mtimeMs);
|
|
1619
|
-
const parsed = parseRequirements(content2, artifact.relPath, "ADDED");
|
|
1620
|
-
reqs.push(...parsed);
|
|
1621
|
-
parsedRequirementCount += parsed.length;
|
|
1622
|
-
}
|
|
1623
|
-
changeRequirements.set(change.id, reqs);
|
|
1624
|
-
}
|
|
1625
|
-
if (sourcePaths.length > 0 && parsedRequirementCount === 0) {
|
|
1179
|
+
const spec = _internals3.readEffectiveSpecSync(directory);
|
|
1180
|
+
if (!spec) {
|
|
1626
1181
|
return null;
|
|
1627
1182
|
}
|
|
1628
|
-
const
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
lines.push("", `## Pending Change: ${changeId}`);
|
|
1648
|
-
if (reqs.length === 0) {
|
|
1649
|
-
lines.push(`- ${nextFrId(usedIds, warnings)} SHOULD add OpenSpec delta requirements for change ${changeId}.`);
|
|
1650
|
-
warnings.push(`Change ${changeId} contains no parsable requirements.`);
|
|
1651
|
-
continue;
|
|
1183
|
+
const currentContent = spec.content;
|
|
1184
|
+
const normalize = (text) => text.replace(/\r\n/g, `
|
|
1185
|
+
`);
|
|
1186
|
+
const recordedLines = normalize(recordedContent).split(`
|
|
1187
|
+
`);
|
|
1188
|
+
const currentLines = normalize(currentContent).split(`
|
|
1189
|
+
`);
|
|
1190
|
+
const diffLines = computeLcsDiff(recordedLines, currentLines);
|
|
1191
|
+
const MAX_DIFF_LINES = 300;
|
|
1192
|
+
const diffText = diffLines.length <= MAX_DIFF_LINES ? diffLines.join(`
|
|
1193
|
+
`) : diffLines.slice(0, MAX_DIFF_LINES).join(`
|
|
1194
|
+
`) + `
|
|
1195
|
+
... (diff truncated \u2014 ` + String(diffLines.length - MAX_DIFF_LINES) + " more lines)";
|
|
1196
|
+
const changedSectionsSet = new Set;
|
|
1197
|
+
let currentSection = "";
|
|
1198
|
+
for (const line of diffLines) {
|
|
1199
|
+
const headingMatch = line.slice(1).match(/^##\s+(.+)$/);
|
|
1200
|
+
if (headingMatch) {
|
|
1201
|
+
currentSection = headingMatch[1].trim();
|
|
1652
1202
|
}
|
|
1653
|
-
|
|
1654
|
-
|
|
1203
|
+
if (line.length > 0 && (line[0] === "+" || line[0] === "-")) {
|
|
1204
|
+
if (currentSection) {
|
|
1205
|
+
changedSectionsSet.add(currentSection);
|
|
1206
|
+
}
|
|
1655
1207
|
}
|
|
1656
1208
|
}
|
|
1657
|
-
const content = `${lines.join(`
|
|
1658
|
-
`)}
|
|
1659
|
-
`;
|
|
1660
|
-
if (content.length > MAX_SPEC_BYTES) {
|
|
1661
|
-
warnings.push(`Projected spec exceeds ${MAX_SPEC_BYTES} bytes; refusing to use projection.`);
|
|
1662
|
-
return null;
|
|
1663
|
-
}
|
|
1664
|
-
const validation = validateSpecContent(content);
|
|
1665
|
-
if (!validation.valid) {
|
|
1666
|
-
warnings.push(...validation.issues.map((issue) => `Projection line ${issue.line}: ${issue.message}`));
|
|
1667
|
-
}
|
|
1668
1209
|
return {
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
hash: hash(content),
|
|
1672
|
-
mtime: mtimeMs > 0 ? new Date(mtimeMs).toISOString() : null,
|
|
1673
|
-
sourcePaths,
|
|
1674
|
-
warnings
|
|
1210
|
+
diff: diffText,
|
|
1211
|
+
changedSections: Array.from(changedSectionsSet)
|
|
1675
1212
|
};
|
|
1676
1213
|
}
|
|
1677
|
-
function
|
|
1678
|
-
const
|
|
1679
|
-
const
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
hash: hash(content),
|
|
1688
|
-
mtime: stat.mtime.toISOString(),
|
|
1689
|
-
sourcePaths: [toPosix(SWARM_SPEC_REL)],
|
|
1690
|
-
warnings: []
|
|
1691
|
-
};
|
|
1692
|
-
}
|
|
1693
|
-
} catch (error2) {
|
|
1694
|
-
if (error2.code !== "ENOENT") {
|
|
1695
|
-
throw error2;
|
|
1696
|
-
}
|
|
1697
|
-
}
|
|
1698
|
-
if (opts?.source) {
|
|
1699
|
-
switch (opts.source) {
|
|
1700
|
-
case "openspec":
|
|
1701
|
-
return buildOpenSpecProjectionSync(root);
|
|
1702
|
-
case "speckit":
|
|
1703
|
-
return buildSpeckitProjectionSync(root, { feature: opts.feature });
|
|
1704
|
-
case "swarm":
|
|
1705
|
-
return null;
|
|
1706
|
-
}
|
|
1707
|
-
}
|
|
1708
|
-
const speckitDetection = detectSpeckit(root);
|
|
1709
|
-
if (!speckitDetection.markerPresent) {
|
|
1710
|
-
return buildOpenSpecProjectionSync(root);
|
|
1711
|
-
}
|
|
1712
|
-
const speckitPresent = speckitDetection.features.length > 0;
|
|
1713
|
-
if (!speckitPresent) {
|
|
1714
|
-
return buildOpenSpecProjectionSync(root);
|
|
1715
|
-
}
|
|
1716
|
-
const openspecProjection = buildOpenSpecProjectionSync(root);
|
|
1717
|
-
const openspecPresent = openspecProjection !== null;
|
|
1718
|
-
if (openspecPresent) {
|
|
1719
|
-
console.warn("[opencode-swarm] Multiple SDD sources detected (openspec and speckit). " + "Enforcement/projection is suppressed until you disambiguate. " + "Pass --source openspec or --source speckit to select a provider.");
|
|
1720
|
-
return null;
|
|
1721
|
-
}
|
|
1722
|
-
return buildSpeckitProjectionSync(root, { feature: opts?.feature });
|
|
1723
|
-
}
|
|
1724
|
-
function writeProjectedSpecSync(directory, options = {}) {
|
|
1725
|
-
const root = path4.resolve(directory);
|
|
1726
|
-
const projection = options.source === "speckit" ? buildSpeckitProjectionSync(root, { feature: options.feature }) : buildOpenSpecProjectionSync(root, { changeId: options.changeId });
|
|
1727
|
-
const target = path4.join(root, SWARM_SPEC_REL);
|
|
1728
|
-
if (!projection || options.dryRun) {
|
|
1729
|
-
return { written: false, projection, path: target };
|
|
1730
|
-
}
|
|
1731
|
-
fs4.mkdirSync(path4.dirname(target), { recursive: true });
|
|
1732
|
-
let archivePath;
|
|
1733
|
-
if (fs4.existsSync(target)) {
|
|
1734
|
-
const prior = fs4.readFileSync(target, "utf-8");
|
|
1735
|
-
if (prior !== projection.content) {
|
|
1736
|
-
const archiveDir = path4.join(root, ".swarm", "spec-archive");
|
|
1737
|
-
fs4.mkdirSync(archiveDir, { recursive: true });
|
|
1738
|
-
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
1739
|
-
archivePath = path4.join(archiveDir, `sdd-projection-${stamp}.md`);
|
|
1740
|
-
fs4.writeFileSync(archivePath, prior, "utf-8");
|
|
1741
|
-
}
|
|
1742
|
-
}
|
|
1743
|
-
const tmp = `${target}.tmp-${process.pid}-${Date.now()}`;
|
|
1744
|
-
fs4.writeFileSync(tmp, projection.content, "utf-8");
|
|
1745
|
-
fs4.renameSync(tmp, target);
|
|
1746
|
-
return { written: true, projection, archivePath, path: target };
|
|
1747
|
-
}
|
|
1748
|
-
function validateSpeckit(directory, options = {}) {
|
|
1749
|
-
const root = path4.resolve(directory);
|
|
1750
|
-
const resolution = resolveSpeckitProjection(root, options);
|
|
1751
|
-
const problems = [];
|
|
1752
|
-
if (resolution.kind !== "ok" && resolution.kind !== "zero_requirements") {
|
|
1753
|
-
return { resolution, problems };
|
|
1754
|
-
}
|
|
1755
|
-
if (resolution.kind === "zero_requirements") {
|
|
1756
|
-
problems.push(`Feature '${resolution.feature}' contains no parsable functional requirements.`);
|
|
1757
|
-
}
|
|
1758
|
-
const detection = detectSpeckit(root);
|
|
1759
|
-
const featureEntry = detection.features.find((f) => f.featureId === resolution.feature);
|
|
1760
|
-
if (!featureEntry) {
|
|
1761
|
-
return { resolution, problems };
|
|
1762
|
-
}
|
|
1763
|
-
const specAbs = path4.join(root, featureEntry.specRelPath);
|
|
1764
|
-
let specContent = null;
|
|
1765
|
-
try {
|
|
1766
|
-
specContent = readTextBounded(specAbs);
|
|
1767
|
-
} catch {}
|
|
1768
|
-
if (specContent !== null) {
|
|
1769
|
-
const specLines = specContent.replace(/\r\n/g, `
|
|
1770
|
-
`).split(`
|
|
1771
|
-
`);
|
|
1772
|
-
for (const requiredHeader of SPECKIT_REQUIRED_SECTIONS) {
|
|
1773
|
-
if (!specLines.some((line) => line.trimEnd() === requiredHeader)) {
|
|
1774
|
-
problems.push(`Missing required spec.md section: ${requiredHeader}`);
|
|
1214
|
+
function computeLcsDiff(oldLines, newLines) {
|
|
1215
|
+
const m = oldLines.length;
|
|
1216
|
+
const n = newLines.length;
|
|
1217
|
+
const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
|
|
1218
|
+
for (let i2 = 1;i2 <= m; i2++) {
|
|
1219
|
+
for (let j2 = 1;j2 <= n; j2++) {
|
|
1220
|
+
if (oldLines[i2 - 1] === newLines[j2 - 1]) {
|
|
1221
|
+
dp[i2][j2] = dp[i2 - 1][j2 - 1] + 1;
|
|
1222
|
+
} else {
|
|
1223
|
+
dp[i2][j2] = Math.max(dp[i2 - 1][j2], dp[i2][j2 - 1]);
|
|
1775
1224
|
}
|
|
1776
1225
|
}
|
|
1777
1226
|
}
|
|
1778
|
-
const
|
|
1779
|
-
let
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
const hasStoryRef = /\[US\d+\]/i.test(line);
|
|
1793
|
-
const hasReqRef = /\bFR-(?!000)\d{3}\b/i.test(line);
|
|
1794
|
-
if (!hasStoryRef && !hasReqRef) {
|
|
1795
|
-
problems.push(`Task ${taskId} has no spec/requirement reference.`);
|
|
1796
|
-
}
|
|
1227
|
+
const entries = [];
|
|
1228
|
+
let i = m;
|
|
1229
|
+
let j = n;
|
|
1230
|
+
while (i > 0 || j > 0) {
|
|
1231
|
+
if (i > 0 && j > 0 && oldLines[i - 1] === newLines[j - 1]) {
|
|
1232
|
+
entries.unshift({ type: "unchanged", line: oldLines[i - 1] });
|
|
1233
|
+
i--;
|
|
1234
|
+
j--;
|
|
1235
|
+
} else if (j > 0 && (i === 0 || dp[i][j - 1] >= dp[i - 1][j])) {
|
|
1236
|
+
entries.unshift({ type: "add", line: newLines[j - 1] });
|
|
1237
|
+
j--;
|
|
1238
|
+
} else {
|
|
1239
|
+
entries.unshift({ type: "remove", line: oldLines[i - 1] });
|
|
1240
|
+
i--;
|
|
1797
1241
|
}
|
|
1798
1242
|
}
|
|
1799
|
-
return
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
return null;
|
|
1807
|
-
return createHash2("sha256").update(spec.content, "utf-8").digest("hex");
|
|
1808
|
-
}
|
|
1809
|
-
async function isSpecStale(directory, plan) {
|
|
1810
|
-
const currentHash = await _internals3.computeSpecHash(directory);
|
|
1811
|
-
if (!plan.specHash) {
|
|
1812
|
-
return { stale: false };
|
|
1813
|
-
}
|
|
1814
|
-
if (currentHash === null) {
|
|
1815
|
-
return {
|
|
1816
|
-
stale: true,
|
|
1817
|
-
reason: "effective spec has been deleted",
|
|
1818
|
-
currentHash: null
|
|
1819
|
-
};
|
|
1820
|
-
}
|
|
1821
|
-
if (currentHash !== plan.specHash) {
|
|
1822
|
-
return {
|
|
1823
|
-
stale: true,
|
|
1824
|
-
reason: "effective spec has been modified since plan was saved",
|
|
1825
|
-
currentHash
|
|
1826
|
-
};
|
|
1827
|
-
}
|
|
1828
|
-
return { stale: false };
|
|
1243
|
+
return entries.map((entry) => {
|
|
1244
|
+
if (entry.type === "unchanged")
|
|
1245
|
+
return ` ${entry.line}`;
|
|
1246
|
+
if (entry.type === "add")
|
|
1247
|
+
return `+${entry.line}`;
|
|
1248
|
+
return `-${entry.line}`;
|
|
1249
|
+
});
|
|
1829
1250
|
}
|
|
1830
|
-
var _internals3 = {
|
|
1831
|
-
computeSpecHash,
|
|
1832
|
-
isSpecStale,
|
|
1833
|
-
readEffectiveSpecSync
|
|
1834
|
-
};
|
|
1835
1251
|
|
|
1836
1252
|
// src/plan/ledger.ts
|
|
1837
1253
|
import * as crypto from "crypto";
|
|
1838
|
-
import * as
|
|
1839
|
-
import * as
|
|
1254
|
+
import * as fs4 from "fs";
|
|
1255
|
+
import * as path4 from "path";
|
|
1840
1256
|
|
|
1841
1257
|
// src/plan/utils.ts
|
|
1842
1258
|
function derivePlanId(plan) {
|
|
@@ -1846,7 +1262,7 @@ function derivePlanId(plan) {
|
|
|
1846
1262
|
// src/plan/ledger.ts
|
|
1847
1263
|
var LEDGER_SCHEMA_VERSION = "1.1.0";
|
|
1848
1264
|
var LEDGER_FILENAME = "plan-ledger.jsonl";
|
|
1849
|
-
var LEDGER_LOCK_PATH =
|
|
1265
|
+
var LEDGER_LOCK_PATH = path4.join(".swarm", LEDGER_FILENAME);
|
|
1850
1266
|
var PLAN_JSON_FILENAME = "plan.json";
|
|
1851
1267
|
|
|
1852
1268
|
class LedgerStaleWriterError extends Error {
|
|
@@ -1856,10 +1272,10 @@ class LedgerStaleWriterError extends Error {
|
|
|
1856
1272
|
}
|
|
1857
1273
|
}
|
|
1858
1274
|
function getLedgerPath(directory) {
|
|
1859
|
-
return
|
|
1275
|
+
return path4.join(directory, ".swarm", LEDGER_FILENAME);
|
|
1860
1276
|
}
|
|
1861
1277
|
function getPlanJsonPath(directory) {
|
|
1862
|
-
return
|
|
1278
|
+
return path4.join(directory, ".swarm", PLAN_JSON_FILENAME);
|
|
1863
1279
|
}
|
|
1864
1280
|
function computePlanHash(plan) {
|
|
1865
1281
|
const normalized = {
|
|
@@ -1894,7 +1310,7 @@ function computePlanHash(plan) {
|
|
|
1894
1310
|
function computeCurrentPlanHash(directory) {
|
|
1895
1311
|
const planPath = getPlanJsonPath(directory);
|
|
1896
1312
|
try {
|
|
1897
|
-
const content =
|
|
1313
|
+
const content = fs4.readFileSync(planPath, "utf8");
|
|
1898
1314
|
const plan = JSON.parse(content);
|
|
1899
1315
|
return computePlanHash(plan);
|
|
1900
1316
|
} catch {
|
|
@@ -1903,15 +1319,15 @@ function computeCurrentPlanHash(directory) {
|
|
|
1903
1319
|
}
|
|
1904
1320
|
async function ledgerExists(directory) {
|
|
1905
1321
|
const ledgerPath = getLedgerPath(directory);
|
|
1906
|
-
return
|
|
1322
|
+
return fs4.existsSync(ledgerPath);
|
|
1907
1323
|
}
|
|
1908
1324
|
async function getLatestLedgerSeq(directory) {
|
|
1909
1325
|
const ledgerPath = getLedgerPath(directory);
|
|
1910
|
-
if (!
|
|
1326
|
+
if (!fs4.existsSync(ledgerPath)) {
|
|
1911
1327
|
return 0;
|
|
1912
1328
|
}
|
|
1913
1329
|
try {
|
|
1914
|
-
const content =
|
|
1330
|
+
const content = fs4.readFileSync(ledgerPath, "utf8");
|
|
1915
1331
|
const lines = content.trim().split(`
|
|
1916
1332
|
`).filter((line) => line.trim() !== "");
|
|
1917
1333
|
if (lines.length === 0) {
|
|
@@ -1933,11 +1349,11 @@ async function getLatestLedgerSeq(directory) {
|
|
|
1933
1349
|
}
|
|
1934
1350
|
async function readLedgerEvents(directory) {
|
|
1935
1351
|
const ledgerPath = getLedgerPath(directory);
|
|
1936
|
-
if (!
|
|
1352
|
+
if (!fs4.existsSync(ledgerPath)) {
|
|
1937
1353
|
return [];
|
|
1938
1354
|
}
|
|
1939
1355
|
try {
|
|
1940
|
-
const content =
|
|
1356
|
+
const content = fs4.readFileSync(ledgerPath, "utf8");
|
|
1941
1357
|
const lines = content.trim().split(`
|
|
1942
1358
|
`).filter((line) => line.trim() !== "");
|
|
1943
1359
|
const events = [];
|
|
@@ -1962,15 +1378,15 @@ async function readLedgerEvents(directory) {
|
|
|
1962
1378
|
async function initLedger(directory, planId, initialPlanHash, initialPlan) {
|
|
1963
1379
|
const ledgerPath = getLedgerPath(directory);
|
|
1964
1380
|
const planJsonPath = getPlanJsonPath(directory);
|
|
1965
|
-
if (
|
|
1381
|
+
if (fs4.existsSync(ledgerPath)) {
|
|
1966
1382
|
throw new Error("Ledger already initialized. Use appendLedgerEvent to add events.");
|
|
1967
1383
|
}
|
|
1968
1384
|
let planHashAfter = initialPlanHash ?? "";
|
|
1969
1385
|
let embeddedPlan = initialPlan;
|
|
1970
1386
|
if (!initialPlanHash) {
|
|
1971
1387
|
try {
|
|
1972
|
-
if (
|
|
1973
|
-
const content =
|
|
1388
|
+
if (fs4.existsSync(planJsonPath)) {
|
|
1389
|
+
const content = fs4.readFileSync(planJsonPath, "utf8");
|
|
1974
1390
|
const plan = JSON.parse(content);
|
|
1975
1391
|
planHashAfter = computePlanHash(plan);
|
|
1976
1392
|
if (!embeddedPlan)
|
|
@@ -1990,12 +1406,12 @@ async function initLedger(directory, planId, initialPlanHash, initialPlan) {
|
|
|
1990
1406
|
schema_version: LEDGER_SCHEMA_VERSION,
|
|
1991
1407
|
...payload ? { payload } : {}
|
|
1992
1408
|
};
|
|
1993
|
-
|
|
1409
|
+
fs4.mkdirSync(path4.join(directory, ".swarm"), { recursive: true });
|
|
1994
1410
|
const tempPath = `${ledgerPath}.tmp.${Date.now()}.${Math.floor(Math.random() * 1e9)}`;
|
|
1995
1411
|
const line = `${JSON.stringify(event)}
|
|
1996
1412
|
`;
|
|
1997
|
-
|
|
1998
|
-
|
|
1413
|
+
fs4.writeFileSync(tempPath, line, "utf8");
|
|
1414
|
+
fs4.renameSync(tempPath, ledgerPath);
|
|
1999
1415
|
}
|
|
2000
1416
|
async function appendLedgerEvent(directory, eventInput, options) {
|
|
2001
1417
|
return withEvidenceLock(directory, LEDGER_LOCK_PATH, "plan-ledger", "append-ledger-event", async () => {
|
|
@@ -2018,17 +1434,17 @@ async function appendLedgerEvent(directory, eventInput, options) {
|
|
|
2018
1434
|
plan_hash_after: planHashAfter,
|
|
2019
1435
|
schema_version: LEDGER_SCHEMA_VERSION
|
|
2020
1436
|
};
|
|
2021
|
-
|
|
1437
|
+
fs4.mkdirSync(path4.join(directory, ".swarm"), { recursive: true });
|
|
2022
1438
|
const tempPath = `${ledgerPath}.tmp.${Date.now()}.${Math.floor(Math.random() * 1e9)}`;
|
|
2023
1439
|
const line = `${JSON.stringify(event)}
|
|
2024
1440
|
`;
|
|
2025
|
-
if (
|
|
2026
|
-
const existingContent =
|
|
2027
|
-
|
|
1441
|
+
if (fs4.existsSync(ledgerPath)) {
|
|
1442
|
+
const existingContent = fs4.readFileSync(ledgerPath, "utf8");
|
|
1443
|
+
fs4.writeFileSync(tempPath, existingContent + line, "utf8");
|
|
2028
1444
|
} else {
|
|
2029
1445
|
throw new Error("Ledger not initialized. Call initLedger() first.");
|
|
2030
1446
|
}
|
|
2031
|
-
|
|
1447
|
+
fs4.renameSync(tempPath, ledgerPath);
|
|
2032
1448
|
return event;
|
|
2033
1449
|
});
|
|
2034
1450
|
}
|
|
@@ -2116,12 +1532,12 @@ async function replayFromLedger(directory, _options) {
|
|
|
2116
1532
|
}
|
|
2117
1533
|
}
|
|
2118
1534
|
const planJsonPath = getPlanJsonPath(directory);
|
|
2119
|
-
if (!
|
|
1535
|
+
if (!fs4.existsSync(planJsonPath)) {
|
|
2120
1536
|
return null;
|
|
2121
1537
|
}
|
|
2122
1538
|
let plan;
|
|
2123
1539
|
try {
|
|
2124
|
-
const content =
|
|
1540
|
+
const content = fs4.readFileSync(planJsonPath, "utf8");
|
|
2125
1541
|
plan = JSON.parse(content);
|
|
2126
1542
|
} catch {
|
|
2127
1543
|
return null;
|
|
@@ -2216,11 +1632,11 @@ function applyEventToPlan(plan, event) {
|
|
|
2216
1632
|
}
|
|
2217
1633
|
async function readLedgerEventsWithIntegrity(directory) {
|
|
2218
1634
|
const ledgerPath = getLedgerPath(directory);
|
|
2219
|
-
if (!
|
|
1635
|
+
if (!fs4.existsSync(ledgerPath)) {
|
|
2220
1636
|
return { events: [], truncated: false, badSuffix: null };
|
|
2221
1637
|
}
|
|
2222
1638
|
try {
|
|
2223
|
-
const content =
|
|
1639
|
+
const content = fs4.readFileSync(ledgerPath, "utf8");
|
|
2224
1640
|
const lines = content.split(`
|
|
2225
1641
|
`);
|
|
2226
1642
|
const events = [];
|
|
@@ -2249,9 +1665,9 @@ async function readLedgerEventsWithIntegrity(directory) {
|
|
|
2249
1665
|
}
|
|
2250
1666
|
async function quarantineLedgerSuffix(directory, badSuffix) {
|
|
2251
1667
|
try {
|
|
2252
|
-
const quarantinePath =
|
|
2253
|
-
|
|
2254
|
-
console.warn(`[ledger] Corrupted suffix quarantined to ${
|
|
1668
|
+
const quarantinePath = path4.join(directory, ".swarm", "plan-ledger.quarantine");
|
|
1669
|
+
fs4.writeFileSync(quarantinePath, badSuffix, "utf8");
|
|
1670
|
+
console.warn(`[ledger] Corrupted suffix quarantined to ${path4.relative(directory, quarantinePath)}`);
|
|
2255
1671
|
} catch {}
|
|
2256
1672
|
}
|
|
2257
1673
|
async function loadLastApprovedPlan(directory, expectedPlanId) {
|
|
@@ -2347,7 +1763,7 @@ async function retryCasWithBackoff(directory, eventInput, options) {
|
|
|
2347
1763
|
expectedHashPrefix: currentExpected.slice(0, 8),
|
|
2348
1764
|
delayMs
|
|
2349
1765
|
});
|
|
2350
|
-
await new Promise((
|
|
1766
|
+
await new Promise((resolve3) => setTimeout(resolve3, delayMs));
|
|
2351
1767
|
if (options.verifyValid) {
|
|
2352
1768
|
const stillValid = await options.verifyValid();
|
|
2353
1769
|
if (!stillValid)
|
|
@@ -2390,7 +1806,7 @@ async function getLatestLedgerHash(directory) {
|
|
|
2390
1806
|
}
|
|
2391
1807
|
}
|
|
2392
1808
|
async function surfaceLedgerStaleIfPersisted(directory, plan) {
|
|
2393
|
-
const resolvedWorkspace =
|
|
1809
|
+
const resolvedWorkspace = path5.resolve(directory);
|
|
2394
1810
|
if (!ledgerStaleWorkspaces.has(resolvedWorkspace)) {
|
|
2395
1811
|
return plan;
|
|
2396
1812
|
}
|
|
@@ -2409,7 +1825,7 @@ async function surfaceLedgerStaleIfPersisted(directory, plan) {
|
|
|
2409
1825
|
return plan;
|
|
2410
1826
|
}
|
|
2411
1827
|
async function parsePlanJsonCached(directory) {
|
|
2412
|
-
const planJsonPath =
|
|
1828
|
+
const planJsonPath = path5.resolve(directory, ".swarm", "plan.json");
|
|
2413
1829
|
return readCachedParsedFile(planJsonPath, PLAN_JSON_CACHE_NAMESPACE, () => readSwarmFileAsync(directory, "plan.json"), (planJsonContent) => {
|
|
2414
1830
|
if (planJsonContent.includes("\x00") || planJsonContent.includes("\uFFFD")) {
|
|
2415
1831
|
throw new Error("Plan rejected: .swarm/plan.json contains null bytes or invalid encoding");
|
|
@@ -2469,16 +1885,16 @@ async function isPlanMdInSync(directory, plan, cache) {
|
|
|
2469
1885
|
return false;
|
|
2470
1886
|
}
|
|
2471
1887
|
async function regeneratePlanMarkdown(directory, plan) {
|
|
2472
|
-
const swarmDir =
|
|
1888
|
+
const swarmDir = path5.resolve(directory, ".swarm");
|
|
2473
1889
|
const contentHash = computePlanContentHash(plan);
|
|
2474
1890
|
const markdown = derivePlanMarkdown(plan);
|
|
2475
1891
|
const markdownWithHash = `<!-- PLAN_HASH: ${contentHash} -->
|
|
2476
1892
|
${markdown}`;
|
|
2477
|
-
const mdPath =
|
|
2478
|
-
const mdTempPath =
|
|
1893
|
+
const mdPath = path5.join(swarmDir, "plan.md");
|
|
1894
|
+
const mdTempPath = path5.join(swarmDir, `plan.md.tmp.${Date.now()}.${Math.floor(Math.random() * 1e9)}`);
|
|
2479
1895
|
try {
|
|
2480
1896
|
await bunWrite(mdTempPath, markdownWithHash);
|
|
2481
|
-
|
|
1897
|
+
renameSync4(mdTempPath, mdPath);
|
|
2482
1898
|
} finally {
|
|
2483
1899
|
try {
|
|
2484
1900
|
unlinkSync3(mdTempPath);
|
|
@@ -2507,7 +1923,7 @@ async function loadPlan(directory, cache) {
|
|
|
2507
1923
|
if (await ledgerExists(directory)) {
|
|
2508
1924
|
const planHash = computePlanHash(validated);
|
|
2509
1925
|
const ledgerHash = await getLatestLedgerHash(directory);
|
|
2510
|
-
const resolvedWorkspace =
|
|
1926
|
+
const resolvedWorkspace = path5.resolve(directory);
|
|
2511
1927
|
if (!startupLedgerCheckedWorkspaces.has(resolvedWorkspace)) {
|
|
2512
1928
|
startupLedgerCheckedWorkspaces.add(resolvedWorkspace);
|
|
2513
1929
|
if (ledgerHash !== "" && planHash !== ledgerHash) {
|
|
@@ -2569,32 +1985,38 @@ async function loadPlan(directory, cache) {
|
|
|
2569
1985
|
const runtimePlan = validated;
|
|
2570
1986
|
runtimePlan._specStale = true;
|
|
2571
1987
|
runtimePlan._specStaleReason = staleResult.reason;
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
1988
|
+
const preserving = await isObligationPreserving(directory);
|
|
1989
|
+
if (!preserving) {
|
|
1990
|
+
try {
|
|
1991
|
+
const diffInfo = computeSpecDiff(directory);
|
|
1992
|
+
const specStalenessPath = path5.join(directory, ".swarm", "spec-staleness.json");
|
|
1993
|
+
await fsPromises.writeFile(specStalenessPath, JSON.stringify({
|
|
1994
|
+
type: "spec_stale_detected",
|
|
1995
|
+
timestamp: new Date().toISOString(),
|
|
1996
|
+
phase: validated.current_phase ?? 1,
|
|
1997
|
+
specHash_plan: validated.specHash,
|
|
1998
|
+
specHash_current: staleResult.currentHash ?? null,
|
|
1999
|
+
reason: staleResult.reason,
|
|
2000
|
+
planTitle: validated.title,
|
|
2001
|
+
diff: diffInfo?.diff ?? null,
|
|
2002
|
+
changedSections: diffInfo?.changedSections ?? []
|
|
2003
|
+
}, null, 2), "utf-8");
|
|
2004
|
+
} catch {}
|
|
2005
|
+
try {
|
|
2006
|
+
const eventsPath = path5.join(directory, ".swarm", "events.jsonl");
|
|
2007
|
+
const event = {
|
|
2008
|
+
type: "spec_stale_detected",
|
|
2009
|
+
timestamp: new Date().toISOString(),
|
|
2010
|
+
phase: validated.current_phase ?? 1,
|
|
2011
|
+
specHash_plan: validated.specHash,
|
|
2012
|
+
specHash_current: staleResult.currentHash ?? null,
|
|
2013
|
+
reason: staleResult.reason ?? "unknown",
|
|
2014
|
+
planTitle: validated.title
|
|
2015
|
+
};
|
|
2016
|
+
await fsPromises.appendFile(eventsPath, `${JSON.stringify(event)}
|
|
2596
2017
|
`, "utf-8");
|
|
2597
|
-
|
|
2018
|
+
} catch {}
|
|
2019
|
+
}
|
|
2598
2020
|
}
|
|
2599
2021
|
}
|
|
2600
2022
|
return await surfaceLedgerStaleIfPersisted(directory, validated);
|
|
@@ -2647,7 +2069,7 @@ async function loadPlan(directory, cache) {
|
|
|
2647
2069
|
return migrated;
|
|
2648
2070
|
}
|
|
2649
2071
|
if (await ledgerExists(directory)) {
|
|
2650
|
-
const resolvedDir =
|
|
2072
|
+
const resolvedDir = path5.resolve(directory);
|
|
2651
2073
|
const existingMutex = recoveryMutexes.get(resolvedDir);
|
|
2652
2074
|
if (existingMutex) {
|
|
2653
2075
|
await existingMutex;
|
|
@@ -2787,13 +2209,13 @@ async function savePlan(directory, plan, options) {
|
|
|
2787
2209
|
} else {
|
|
2788
2210
|
const existingEvents = await readLedgerEvents(directory);
|
|
2789
2211
|
if (existingEvents.length > 0 && existingEvents[0].plan_id !== planId) {
|
|
2790
|
-
const swarmDir2 =
|
|
2791
|
-
const oldLedgerPath =
|
|
2792
|
-
const oldLedgerBackupPath =
|
|
2212
|
+
const swarmDir2 = path5.resolve(directory, ".swarm");
|
|
2213
|
+
const oldLedgerPath = path5.join(swarmDir2, "plan-ledger.jsonl");
|
|
2214
|
+
const oldLedgerBackupPath = path5.join(swarmDir2, `plan-ledger.backup-${Date.now()}-${Math.floor(Math.random() * 1e9)}.jsonl`);
|
|
2793
2215
|
let backupExists = false;
|
|
2794
|
-
if (
|
|
2216
|
+
if (existsSync5(oldLedgerPath)) {
|
|
2795
2217
|
try {
|
|
2796
|
-
|
|
2218
|
+
renameSync4(oldLedgerPath, oldLedgerBackupPath);
|
|
2797
2219
|
backupExists = true;
|
|
2798
2220
|
} catch (renameErr) {
|
|
2799
2221
|
throw new Error(`[savePlan] Cannot reinitialize ledger: could not move old ledger aside (rename failed: ${renameErr instanceof Error ? renameErr.message : String(renameErr)}). The existing ledger has plan_id="${existingEvents[0].plan_id}" which does not match the current plan="${planId}". To proceed, close any programs that may have the ledger file open, or run /swarm reset-session to clear the ledger.`);
|
|
@@ -2808,13 +2230,13 @@ async function savePlan(directory, plan, options) {
|
|
|
2808
2230
|
const errorMessage2 = String(initErr);
|
|
2809
2231
|
if (errorMessage2.includes("already initialized")) {
|
|
2810
2232
|
try {
|
|
2811
|
-
if (
|
|
2233
|
+
if (existsSync5(oldLedgerBackupPath))
|
|
2812
2234
|
unlinkSync3(oldLedgerBackupPath);
|
|
2813
2235
|
} catch {}
|
|
2814
2236
|
} else {
|
|
2815
|
-
if (
|
|
2237
|
+
if (existsSync5(oldLedgerBackupPath)) {
|
|
2816
2238
|
try {
|
|
2817
|
-
|
|
2239
|
+
renameSync4(oldLedgerBackupPath, oldLedgerPath);
|
|
2818
2240
|
} catch {
|
|
2819
2241
|
copyFileSync(oldLedgerBackupPath, oldLedgerPath);
|
|
2820
2242
|
try {
|
|
@@ -2827,32 +2249,32 @@ async function savePlan(directory, plan, options) {
|
|
|
2827
2249
|
}
|
|
2828
2250
|
}
|
|
2829
2251
|
if (initSucceeded && backupExists) {
|
|
2830
|
-
const archivePath =
|
|
2252
|
+
const archivePath = path5.join(swarmDir2, `plan-ledger.archived-${Date.now()}-${Math.floor(Math.random() * 1e9)}.jsonl`);
|
|
2831
2253
|
try {
|
|
2832
|
-
|
|
2254
|
+
renameSync4(oldLedgerBackupPath, archivePath);
|
|
2833
2255
|
warn(`[savePlan] Ledger identity mismatch (was "${existingEvents[0].plan_id}", now "${planId}") \u2014 archived old ledger to ${archivePath} and reinitializing.`);
|
|
2834
2256
|
} catch (renameErr) {
|
|
2835
2257
|
warn(`[savePlan] Could not archive old ledger (rename failed: ${renameErr instanceof Error ? renameErr.message : String(renameErr)}). Old ledger may still exist at ${oldLedgerBackupPath}.`);
|
|
2836
2258
|
try {
|
|
2837
|
-
if (
|
|
2259
|
+
if (existsSync5(oldLedgerBackupPath))
|
|
2838
2260
|
unlinkSync3(oldLedgerBackupPath);
|
|
2839
2261
|
} catch {}
|
|
2840
2262
|
}
|
|
2841
2263
|
} else if (!initSucceeded && backupExists) {
|
|
2842
2264
|
try {
|
|
2843
|
-
if (
|
|
2265
|
+
if (existsSync5(oldLedgerBackupPath))
|
|
2844
2266
|
unlinkSync3(oldLedgerBackupPath);
|
|
2845
2267
|
} catch {}
|
|
2846
2268
|
}
|
|
2847
2269
|
const MAX_ARCHIVED_SIBLINGS = 5;
|
|
2848
2270
|
try {
|
|
2849
|
-
const allFiles =
|
|
2271
|
+
const allFiles = readdirSync(swarmDir2);
|
|
2850
2272
|
const archivedSiblings = allFiles.filter((f) => f.startsWith("plan-ledger.archived-") && f.endsWith(".jsonl")).sort();
|
|
2851
2273
|
if (archivedSiblings.length > MAX_ARCHIVED_SIBLINGS) {
|
|
2852
2274
|
const toRemove = archivedSiblings.slice(0, archivedSiblings.length - MAX_ARCHIVED_SIBLINGS);
|
|
2853
2275
|
for (const file of toRemove) {
|
|
2854
2276
|
try {
|
|
2855
|
-
unlinkSync3(
|
|
2277
|
+
unlinkSync3(path5.join(swarmDir2, file));
|
|
2856
2278
|
} catch {}
|
|
2857
2279
|
}
|
|
2858
2280
|
}
|
|
@@ -2985,19 +2407,19 @@ async function savePlan(directory, plan, options) {
|
|
|
2985
2407
|
source: "savePlan_manager"
|
|
2986
2408
|
});
|
|
2987
2409
|
}
|
|
2988
|
-
const swarmDir =
|
|
2989
|
-
const planPath =
|
|
2990
|
-
const tempPath =
|
|
2410
|
+
const swarmDir = path5.resolve(directory, ".swarm");
|
|
2411
|
+
const planPath = path5.join(swarmDir, "plan.json");
|
|
2412
|
+
const tempPath = path5.join(swarmDir, `plan.json.tmp.${Date.now()}.${Math.floor(Math.random() * 1e9)}`);
|
|
2991
2413
|
try {
|
|
2992
2414
|
await bunWrite(tempPath, JSON.stringify(validated, null, 2));
|
|
2993
|
-
|
|
2415
|
+
renameSync4(tempPath, planPath);
|
|
2994
2416
|
} finally {
|
|
2995
2417
|
try {
|
|
2996
2418
|
unlinkSync3(tempPath);
|
|
2997
2419
|
} catch {}
|
|
2998
2420
|
}
|
|
2999
2421
|
try {
|
|
3000
|
-
const markerPath =
|
|
2422
|
+
const markerPath = path5.join(swarmDir, ".plan-write-marker");
|
|
3001
2423
|
const inProgressMarker = JSON.stringify({
|
|
3002
2424
|
source: "plan_manager",
|
|
3003
2425
|
timestamp: new Date().toISOString(),
|
|
@@ -3012,11 +2434,11 @@ async function savePlan(directory, plan, options) {
|
|
|
3012
2434
|
const markdown = derivePlanMarkdown(validated);
|
|
3013
2435
|
const markdownWithHash = `<!-- PLAN_HASH: ${contentHash} -->
|
|
3014
2436
|
${markdown}`;
|
|
3015
|
-
const mdPath =
|
|
3016
|
-
const mdTempPath =
|
|
2437
|
+
const mdPath = path5.join(swarmDir, "plan.md");
|
|
2438
|
+
const mdTempPath = path5.join(swarmDir, `plan.md.tmp.${Date.now()}.${Math.floor(Math.random() * 1e9)}`);
|
|
3017
2439
|
try {
|
|
3018
2440
|
await bunWrite(mdTempPath, markdownWithHash);
|
|
3019
|
-
|
|
2441
|
+
renameSync4(mdTempPath, mdPath);
|
|
3020
2442
|
} finally {
|
|
3021
2443
|
try {
|
|
3022
2444
|
unlinkSync3(mdTempPath);
|
|
@@ -3034,7 +2456,7 @@ ${markdown}`;
|
|
|
3034
2456
|
} catch {}
|
|
3035
2457
|
}
|
|
3036
2458
|
try {
|
|
3037
|
-
const markerPath =
|
|
2459
|
+
const markerPath = path5.join(swarmDir, ".plan-write-marker");
|
|
3038
2460
|
const tasksCount = validated.phases.reduce((sum, phase) => sum + phase.tasks.length, 0);
|
|
3039
2461
|
const marker = JSON.stringify({
|
|
3040
2462
|
source: "plan_manager",
|
|
@@ -3050,14 +2472,14 @@ async function rebuildPlan(directory, plan, options) {
|
|
|
3050
2472
|
const targetPlan = plan ?? await replayFromLedger(directory);
|
|
3051
2473
|
if (!targetPlan)
|
|
3052
2474
|
return null;
|
|
3053
|
-
const swarmDir =
|
|
3054
|
-
const planPath =
|
|
3055
|
-
const mdPath =
|
|
3056
|
-
const tempPlanPath =
|
|
2475
|
+
const swarmDir = path5.join(directory, ".swarm");
|
|
2476
|
+
const planPath = path5.join(swarmDir, "plan.json");
|
|
2477
|
+
const mdPath = path5.join(swarmDir, "plan.md");
|
|
2478
|
+
const tempPlanPath = path5.join(swarmDir, `plan.json.rebuild.${Date.now()}.${Math.floor(Math.random() * 1e9)}`);
|
|
3057
2479
|
await bunWrite(tempPlanPath, JSON.stringify(targetPlan, null, 2));
|
|
3058
|
-
|
|
2480
|
+
renameSync4(tempPlanPath, planPath);
|
|
3059
2481
|
try {
|
|
3060
|
-
const markerPath =
|
|
2482
|
+
const markerPath = path5.join(swarmDir, ".plan-write-marker");
|
|
3061
2483
|
const inProgressMarker = JSON.stringify({
|
|
3062
2484
|
source: "plan_manager",
|
|
3063
2485
|
timestamp: new Date().toISOString(),
|
|
@@ -3072,12 +2494,12 @@ async function rebuildPlan(directory, plan, options) {
|
|
|
3072
2494
|
const markdown = derivePlanMarkdown(targetPlan);
|
|
3073
2495
|
const markdownWithHash = `<!-- PLAN_HASH: ${contentHash} -->
|
|
3074
2496
|
${markdown}`;
|
|
3075
|
-
const tempMdPath =
|
|
2497
|
+
const tempMdPath = path5.join(swarmDir, `plan.md.rebuild.${Date.now()}.${Math.floor(Math.random() * 1e9)}`);
|
|
3076
2498
|
await bunWrite(tempMdPath, markdownWithHash);
|
|
3077
|
-
|
|
2499
|
+
renameSync4(tempMdPath, mdPath);
|
|
3078
2500
|
} finally {
|
|
3079
2501
|
try {
|
|
3080
|
-
const markerPath =
|
|
2502
|
+
const markerPath = path5.join(swarmDir, ".plan-write-marker");
|
|
3081
2503
|
const tasksCount = targetPlan.phases.reduce((sum, phase) => sum + phase.tasks.length, 0);
|
|
3082
2504
|
const marker = JSON.stringify({
|
|
3083
2505
|
source: "plan_manager",
|
|
@@ -3140,13 +2562,13 @@ async function closePlanTerminalState(directory, plan, options) {
|
|
|
3140
2562
|
planHashAfter: hashAfter,
|
|
3141
2563
|
source: "close_terminal"
|
|
3142
2564
|
});
|
|
3143
|
-
const swarmDir =
|
|
3144
|
-
const planPath =
|
|
3145
|
-
const tempPlanPath =
|
|
2565
|
+
const swarmDir = path5.join(directory, ".swarm");
|
|
2566
|
+
const planPath = path5.join(swarmDir, "plan.json");
|
|
2567
|
+
const tempPlanPath = path5.join(swarmDir, `plan.json.close.${Date.now()}.${Math.floor(Math.random() * 1e9)}`);
|
|
3146
2568
|
await bunWrite(tempPlanPath, JSON.stringify(validated, null, 2));
|
|
3147
|
-
|
|
2569
|
+
renameSync4(tempPlanPath, planPath);
|
|
3148
2570
|
try {
|
|
3149
|
-
const markerPath =
|
|
2571
|
+
const markerPath = path5.join(swarmDir, ".plan-write-marker");
|
|
3150
2572
|
const inProgressMarker = JSON.stringify({
|
|
3151
2573
|
source: "plan_manager_close",
|
|
3152
2574
|
timestamp: new Date().toISOString(),
|
|
@@ -3157,17 +2579,17 @@ async function closePlanTerminalState(directory, plan, options) {
|
|
|
3157
2579
|
await bunWrite(markerPath, inProgressMarker);
|
|
3158
2580
|
} catch {}
|
|
3159
2581
|
try {
|
|
3160
|
-
const mdPath =
|
|
2582
|
+
const mdPath = path5.join(swarmDir, "plan.md");
|
|
3161
2583
|
const contentHash = computePlanContentHash(validated);
|
|
3162
2584
|
const markdown = derivePlanMarkdown(validated);
|
|
3163
2585
|
const markdownWithHash = `<!-- PLAN_HASH: ${contentHash} -->
|
|
3164
2586
|
${markdown}`;
|
|
3165
|
-
const mdTempPath =
|
|
2587
|
+
const mdTempPath = path5.join(swarmDir, `plan.md.close.${Date.now()}.${Math.floor(Math.random() * 1e9)}`);
|
|
3166
2588
|
await bunWrite(mdTempPath, markdownWithHash);
|
|
3167
|
-
|
|
2589
|
+
renameSync4(mdTempPath, mdPath);
|
|
3168
2590
|
} finally {
|
|
3169
2591
|
try {
|
|
3170
|
-
const markerPath =
|
|
2592
|
+
const markerPath = path5.join(swarmDir, ".plan-write-marker");
|
|
3171
2593
|
const tasksCount = validated.phases.reduce((sum, phase) => sum + phase.tasks.length, 0);
|
|
3172
2594
|
const marker = JSON.stringify({
|
|
3173
2595
|
source: "plan_manager_close",
|
|
@@ -3451,14 +2873,14 @@ function migrateLegacyPlan(planContent, swarmId) {
|
|
|
3451
2873
|
|
|
3452
2874
|
// src/evidence/manager.ts
|
|
3453
2875
|
import {
|
|
3454
|
-
mkdirSync as
|
|
3455
|
-
readdirSync as
|
|
2876
|
+
mkdirSync as mkdirSync4,
|
|
2877
|
+
readdirSync as readdirSync2,
|
|
3456
2878
|
realpathSync,
|
|
3457
2879
|
rmSync,
|
|
3458
2880
|
statSync as statSync2
|
|
3459
2881
|
} from "fs";
|
|
3460
|
-
import * as
|
|
3461
|
-
import * as
|
|
2882
|
+
import * as fs5 from "fs/promises";
|
|
2883
|
+
import * as path6 from "path";
|
|
3462
2884
|
|
|
3463
2885
|
// src/config/evidence-schema.ts
|
|
3464
2886
|
var EVIDENCE_MAX_JSON_BYTES = 500 * 1024;
|
|
@@ -3790,16 +3212,16 @@ function validateProjectRoot(directory) {
|
|
|
3790
3212
|
if (depth >= MAX_DEPTH)
|
|
3791
3213
|
break;
|
|
3792
3214
|
depth++;
|
|
3793
|
-
const parent =
|
|
3215
|
+
const parent = path6.dirname(current);
|
|
3794
3216
|
if (parent === current)
|
|
3795
3217
|
break;
|
|
3796
|
-
const parentSwarm =
|
|
3218
|
+
const parentSwarm = path6.join(parent, ".swarm");
|
|
3797
3219
|
try {
|
|
3798
3220
|
if (statSync2(parentSwarm).isDirectory()) {
|
|
3799
3221
|
let hasProjectIndicator = false;
|
|
3800
3222
|
for (const indicator of PROJECT_INDICATORS) {
|
|
3801
3223
|
try {
|
|
3802
|
-
const indicatorStat = statSync2(
|
|
3224
|
+
const indicatorStat = statSync2(path6.join(parent, indicator));
|
|
3803
3225
|
if (indicatorStat.isFile() || indicatorStat.isDirectory()) {
|
|
3804
3226
|
hasProjectIndicator = true;
|
|
3805
3227
|
break;
|
|
@@ -3827,12 +3249,12 @@ function validateProjectRoot(directory) {
|
|
|
3827
3249
|
async function saveEvidence(directory, taskId, evidence) {
|
|
3828
3250
|
_internals5.validateProjectRoot(directory);
|
|
3829
3251
|
const sanitizedTaskId = sanitizeTaskId2(taskId);
|
|
3830
|
-
const relativePath =
|
|
3252
|
+
const relativePath = path6.join("evidence", sanitizedTaskId, "evidence.json");
|
|
3831
3253
|
validateSwarmPath(directory, relativePath);
|
|
3832
3254
|
_internals5.validateEvidence(evidence);
|
|
3833
3255
|
return withEvidenceLock(directory, relativePath, "evidence-manager", sanitizedTaskId, async () => {
|
|
3834
3256
|
const evidencePath = validateSwarmPath(directory, relativePath);
|
|
3835
|
-
const evidenceDir =
|
|
3257
|
+
const evidenceDir = path6.dirname(evidencePath);
|
|
3836
3258
|
let bundle;
|
|
3837
3259
|
const existingContent = await readSwarmFileAsync(directory, relativePath);
|
|
3838
3260
|
if (existingContent !== null) {
|
|
@@ -3874,11 +3296,11 @@ async function saveEvidence(directory, taskId, evidence) {
|
|
|
3874
3296
|
if (bundleJson.length > EVIDENCE_MAX_JSON_BYTES) {
|
|
3875
3297
|
throw new Error(`Evidence bundle size (${bundleJson.length} bytes) exceeds maximum (${EVIDENCE_MAX_JSON_BYTES} bytes)`);
|
|
3876
3298
|
}
|
|
3877
|
-
|
|
3878
|
-
const tempPath =
|
|
3299
|
+
mkdirSync4(evidenceDir, { recursive: true });
|
|
3300
|
+
const tempPath = path6.join(evidenceDir, `evidence.json.tmp.${Date.now()}.${process.pid}`);
|
|
3879
3301
|
try {
|
|
3880
3302
|
await bunWrite(tempPath, bundleJson);
|
|
3881
|
-
await
|
|
3303
|
+
await fs5.rename(tempPath, evidencePath);
|
|
3882
3304
|
} catch (error2) {
|
|
3883
3305
|
try {
|
|
3884
3306
|
rmSync(tempPath, { force: true });
|
|
@@ -3919,7 +3341,7 @@ function wrapFlatRetrospective(flatEntry, taskId) {
|
|
|
3919
3341
|
}
|
|
3920
3342
|
async function loadEvidence(directory, taskId) {
|
|
3921
3343
|
const sanitizedTaskId = sanitizeTaskId2(taskId);
|
|
3922
|
-
const relativePath =
|
|
3344
|
+
const relativePath = path6.join("evidence", sanitizedTaskId, "evidence.json");
|
|
3923
3345
|
if (relativePath.length > 4096) {
|
|
3924
3346
|
return { status: "not_found" };
|
|
3925
3347
|
}
|
|
@@ -3940,12 +3362,12 @@ async function loadEvidence(directory, taskId) {
|
|
|
3940
3362
|
const validated = EvidenceBundleSchema.parse(wrappedBundle);
|
|
3941
3363
|
try {
|
|
3942
3364
|
await withEvidenceLock(directory, relativePath, "evidence-loader", sanitizedTaskId, async () => {
|
|
3943
|
-
const evidenceDir =
|
|
3365
|
+
const evidenceDir = path6.dirname(evidencePath);
|
|
3944
3366
|
const bundleJson = JSON.stringify(validated);
|
|
3945
|
-
const tempPath =
|
|
3367
|
+
const tempPath = path6.join(evidenceDir, `evidence.json.tmp.${Date.now()}.${process.pid}`);
|
|
3946
3368
|
try {
|
|
3947
3369
|
await bunWrite(tempPath, bundleJson);
|
|
3948
|
-
await
|
|
3370
|
+
await fs5.rename(tempPath, evidencePath);
|
|
3949
3371
|
} catch (writeError) {
|
|
3950
3372
|
try {
|
|
3951
3373
|
rmSync(tempPath, { force: true });
|
|
@@ -3981,13 +3403,13 @@ async function listEvidenceTaskIds(directory) {
|
|
|
3981
3403
|
}
|
|
3982
3404
|
let entries;
|
|
3983
3405
|
try {
|
|
3984
|
-
entries =
|
|
3406
|
+
entries = readdirSync2(evidenceBasePath);
|
|
3985
3407
|
} catch {
|
|
3986
3408
|
return [];
|
|
3987
3409
|
}
|
|
3988
3410
|
const taskIds = [];
|
|
3989
3411
|
for (const entry of entries) {
|
|
3990
|
-
const entryPath =
|
|
3412
|
+
const entryPath = path6.join(evidenceBasePath, entry);
|
|
3991
3413
|
try {
|
|
3992
3414
|
const stats = statSync2(entryPath);
|
|
3993
3415
|
if (!stats.isDirectory()) {
|
|
@@ -4005,7 +3427,7 @@ async function listEvidenceTaskIds(directory) {
|
|
|
4005
3427
|
}
|
|
4006
3428
|
async function deleteEvidence(directory, taskId) {
|
|
4007
3429
|
const sanitizedTaskId = sanitizeTaskId2(taskId);
|
|
4008
|
-
const relativePath =
|
|
3430
|
+
const relativePath = path6.join("evidence", sanitizedTaskId);
|
|
4009
3431
|
const evidenceDir = validateSwarmPath(directory, relativePath);
|
|
4010
3432
|
try {
|
|
4011
3433
|
statSync2(evidenceDir);
|
|
@@ -4021,10 +3443,10 @@ async function deleteEvidence(directory, taskId) {
|
|
|
4021
3443
|
}
|
|
4022
3444
|
}
|
|
4023
3445
|
async function checkRequirementCoverage(phase, directory) {
|
|
4024
|
-
const relativePath =
|
|
4025
|
-
const absolutePath =
|
|
3446
|
+
const relativePath = path6.join("evidence", `req-coverage-phase-${phase}.json`);
|
|
3447
|
+
const absolutePath = path6.resolve(directory, ".swarm", relativePath);
|
|
4026
3448
|
try {
|
|
4027
|
-
await
|
|
3449
|
+
await fs5.access(absolutePath);
|
|
4028
3450
|
return { exists: true, path: absolutePath };
|
|
4029
3451
|
} catch {
|
|
4030
3452
|
return { exists: false, path: absolutePath };
|
|
@@ -4188,4 +3610,4 @@ function mergeDurableGateEntriesFromEvidence(taskId, entries, evidence) {
|
|
|
4188
3610
|
return merged;
|
|
4189
3611
|
}
|
|
4190
3612
|
|
|
4191
|
-
export { PlanSchema, MAX_TRANSIENT_RETRIES, isTransientSpawnError, transientBackoff, getGitRepositoryStatus, isGitRepo, resetToRemoteBranch, resetToMainAfterMerge, isStateUnreadable, loadEpicSessionState, isEpicModeActive, enableEpicMode, disableEpicMode, normalizePath, pathsConflict, isGlobalFile, isProtectedPath, readTaskScopes,
|
|
3613
|
+
export { PlanSchema, MAX_TRANSIENT_RETRIES, isTransientSpawnError, transientBackoff, getGitRepositoryStatus, isGitRepo, resetToRemoteBranch, resetToMainAfterMerge, isStateUnreadable, loadEpicSessionState, isEpicModeActive, enableEpicMode, disableEpicMode, normalizePath, pathsConflict, isGlobalFile, isProtectedPath, readTaskScopes, computeSpecHash, derivePlanId, computePlanHash, initLedger, appendLedgerEvent, loadPlanJsonOnly, loadPlan, savePlan, closePlanTerminalState, derivePlanMarkdown, RetrospectiveEvidenceSchema, isValidEvidenceType, sanitizeTaskId2 as sanitizeTaskId, validateProjectRoot, saveEvidence, loadEvidence, listEvidenceTaskIds, checkRequirementCoverage, archiveEvidence, readDurableGateEvidence, getDurableGateEvidenceStatus, getDurableGateEvidenceStatusForTask, mergeDurableGateEntriesFromEvidence };
|