opencode-swarm 7.107.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/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-zhw4rkjb.js → evidence-summary-service-dh44gevz.js} +4 -3
- package/dist/cli/{explorer-rwfvbbx5.js → explorer-ksr3xq7n.js} +1 -1
- package/dist/cli/{guardrail-explain-6xt0pb56.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-wa6at603.js → index-7v734syt.js} +1 -1
- package/dist/cli/{index-v90bnn0f.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-fhw0jm5c.js → index-gt514nt1.js} +23018 -21209
- package/dist/cli/{index-25pev4e7.js → index-jjp5qrjv.js} +122 -834
- 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/hooks/curator-postmortem.d.ts +52 -3
- package/dist/hooks/curator.d.ts +2 -1
- package/dist/index.js +349 -313
- 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,729 +1087,14 @@ function readTaskScopes(directory, taskId) {
|
|
|
1084
1087
|
}
|
|
1085
1088
|
|
|
1086
1089
|
// src/utils/spec-hash.ts
|
|
1087
|
-
import { createHash as createHash2 } from "crypto";
|
|
1088
|
-
import * as fsSync from "fs";
|
|
1089
|
-
import { join as join4 } from "path";
|
|
1090
|
-
|
|
1091
|
-
// src/sdd/effective-spec.ts
|
|
1092
1090
|
import { createHash } from "crypto";
|
|
1093
|
-
import * as
|
|
1094
|
-
import
|
|
1095
|
-
|
|
1096
|
-
// src/config/spec-schema.ts
|
|
1097
|
-
var ObligationSchema = exports_external.enum(["MUST", "SHALL", "SHOULD", "MAY"]);
|
|
1098
|
-
var SpecRequirementSchema = exports_external.object({
|
|
1099
|
-
id: exports_external.string().regex(/^FR-(?!000)\d{3}$/, "Requirement ID must match FR-### pattern (e.g., FR-001)"),
|
|
1100
|
-
obligation: ObligationSchema,
|
|
1101
|
-
text: exports_external.string().min(1)
|
|
1102
|
-
});
|
|
1103
|
-
var SpecScenarioSchema = exports_external.object({
|
|
1104
|
-
name: exports_external.string().min(1),
|
|
1105
|
-
given: exports_external.array(exports_external.string()).optional().default([]),
|
|
1106
|
-
when: exports_external.array(exports_external.string()).min(1, 'Scenario must have at least one "when" clause'),
|
|
1107
|
-
thenClauses: exports_external.array(exports_external.string()).min(1, 'Scenario must have at least one "then" clause')
|
|
1108
|
-
});
|
|
1109
|
-
var SpecSectionSchema = exports_external.object({
|
|
1110
|
-
name: exports_external.string().min(1),
|
|
1111
|
-
requirements: exports_external.array(SpecRequirementSchema).default([])
|
|
1112
|
-
});
|
|
1113
|
-
var SwarmSpecSchema = exports_external.object({
|
|
1114
|
-
title: exports_external.string().min(1),
|
|
1115
|
-
purpose: exports_external.string().min(1),
|
|
1116
|
-
sections: exports_external.array(SpecSectionSchema).min(1, "Spec must have at least one section")
|
|
1117
|
-
});
|
|
1118
|
-
var SpecDeltaSchema = exports_external.object({
|
|
1119
|
-
added: exports_external.array(SpecRequirementSchema).default([]),
|
|
1120
|
-
modified: exports_external.array(SpecRequirementSchema).default([]),
|
|
1121
|
-
removed: exports_external.array(SpecRequirementSchema).default([])
|
|
1122
|
-
});
|
|
1123
|
-
var DeltaSpecSchema = exports_external.union([
|
|
1124
|
-
SwarmSpecSchema,
|
|
1125
|
-
SpecDeltaSchema
|
|
1126
|
-
]);
|
|
1127
|
-
var FENCED_BLOCK_PATTERN = /```[\s\S]*?```/g;
|
|
1128
|
-
var INLINE_CODE_PATTERN = /`[^`]*`/g;
|
|
1129
|
-
var FR_ID_PATTERN = /\bFR-\d{3}\b/g;
|
|
1130
|
-
var OBLIGATION_PATTERN = /\b(MUST|SHALL|SHOULD|MAY)\b/g;
|
|
1131
|
-
var SECTION_HEADER_PATTERN = /^##\s+.+$/gm;
|
|
1132
|
-
function stripCodeBlocks(content) {
|
|
1133
|
-
return content.replace(FENCED_BLOCK_PATTERN, "").replace(INLINE_CODE_PATTERN, "");
|
|
1134
|
-
}
|
|
1135
|
-
function getLineNumber(content, position) {
|
|
1136
|
-
const prefix = content.substring(0, position);
|
|
1137
|
-
return (prefix.match(/\n/g) || []).length + 1;
|
|
1138
|
-
}
|
|
1139
|
-
function validateSpecContent(content) {
|
|
1140
|
-
const issues = [];
|
|
1141
|
-
if (!content || content.trim().length === 0) {
|
|
1142
|
-
return { valid: false, issues: [{ line: 1, message: "Content is empty" }] };
|
|
1143
|
-
}
|
|
1144
|
-
const strippedContent = stripCodeBlocks(content);
|
|
1145
|
-
const frMatches = strippedContent.match(FR_ID_PATTERN);
|
|
1146
|
-
if (!frMatches || frMatches.length === 0) {
|
|
1147
|
-
issues.push({
|
|
1148
|
-
line: 1,
|
|
1149
|
-
message: "No FR-### requirement IDs found in spec content"
|
|
1150
|
-
});
|
|
1151
|
-
}
|
|
1152
|
-
const obligationMatches = strippedContent.match(OBLIGATION_PATTERN);
|
|
1153
|
-
if (!obligationMatches || obligationMatches.length === 0) {
|
|
1154
|
-
issues.push({
|
|
1155
|
-
line: 1,
|
|
1156
|
-
message: "No obligation keywords (MUST, SHALL, SHOULD, MAY) found in spec content"
|
|
1157
|
-
});
|
|
1158
|
-
}
|
|
1159
|
-
const sectionMatches = strippedContent.match(SECTION_HEADER_PATTERN);
|
|
1160
|
-
if (!sectionMatches || sectionMatches.length === 0) {
|
|
1161
|
-
issues.push({
|
|
1162
|
-
line: 1,
|
|
1163
|
-
message: "No section headers (## Section Name) found in spec content"
|
|
1164
|
-
});
|
|
1165
|
-
}
|
|
1166
|
-
const idMatches = strippedContent.matchAll(/\bFR-(\d{3})\b/g);
|
|
1167
|
-
for (const idMatch of idMatches) {
|
|
1168
|
-
const num = parseInt(idMatch[1], 10);
|
|
1169
|
-
if (num === 0) {
|
|
1170
|
-
const pos = idMatch.index;
|
|
1171
|
-
issues.push({
|
|
1172
|
-
line: getLineNumber(strippedContent, pos),
|
|
1173
|
-
message: `Invalid FR-ID "${idMatch[0]}" \u2014 number must be 001-999`
|
|
1174
|
-
});
|
|
1175
|
-
}
|
|
1176
|
-
}
|
|
1177
|
-
return {
|
|
1178
|
-
valid: issues.length === 0,
|
|
1179
|
-
issues
|
|
1180
|
-
};
|
|
1181
|
-
}
|
|
1182
|
-
|
|
1183
|
-
// src/sdd/effective-spec.ts
|
|
1184
|
-
var SWARM_SPEC_REL = path4.join(".swarm", "spec.md");
|
|
1185
|
-
var OPENSPEC_ROOT = "openspec";
|
|
1186
|
-
var SPECKIT_MARKER = ".specify";
|
|
1187
|
-
var SPECKIT_SPECS_DIR = "specs";
|
|
1188
|
-
var MAX_SPEC_BYTES = 256 * 1024;
|
|
1189
|
-
var MAX_SOURCE_BYTES = 512 * 1024;
|
|
1190
|
-
var MAX_SPEC_FILES = 100;
|
|
1191
|
-
var MAX_WALK_DEPTH = 10;
|
|
1192
|
-
var SPECKIT_REQUIRED_SECTIONS = [
|
|
1193
|
-
"## Functional Requirements",
|
|
1194
|
-
"## Success Criteria"
|
|
1195
|
-
];
|
|
1196
|
-
function toPosix(relPath) {
|
|
1197
|
-
return relPath.split(path4.sep).join("/");
|
|
1198
|
-
}
|
|
1199
|
-
function hash(content) {
|
|
1200
|
-
return createHash("sha256").update(content, "utf-8").digest("hex");
|
|
1201
|
-
}
|
|
1202
|
-
function readTextBounded(absPath) {
|
|
1203
|
-
let stat;
|
|
1204
|
-
try {
|
|
1205
|
-
stat = fs4.lstatSync(absPath);
|
|
1206
|
-
} catch {
|
|
1207
|
-
return null;
|
|
1208
|
-
}
|
|
1209
|
-
if (!stat.isFile() || stat.size > MAX_SOURCE_BYTES) {
|
|
1210
|
-
return null;
|
|
1211
|
-
}
|
|
1212
|
-
try {
|
|
1213
|
-
return fs4.readFileSync(absPath, "utf-8");
|
|
1214
|
-
} catch {
|
|
1215
|
-
return null;
|
|
1216
|
-
}
|
|
1217
|
-
}
|
|
1218
|
-
function fileArtifact(root, absPath) {
|
|
1219
|
-
try {
|
|
1220
|
-
const stat = fs4.lstatSync(absPath);
|
|
1221
|
-
if (!stat.isFile() || stat.size > MAX_SOURCE_BYTES)
|
|
1222
|
-
return null;
|
|
1223
|
-
return {
|
|
1224
|
-
relPath: toPosix(path4.relative(root, absPath)),
|
|
1225
|
-
bytes: stat.size,
|
|
1226
|
-
mtimeMs: stat.mtimeMs
|
|
1227
|
-
};
|
|
1228
|
-
} catch {
|
|
1229
|
-
return null;
|
|
1230
|
-
}
|
|
1231
|
-
}
|
|
1232
|
-
function walkSpecFiles(root, startRel) {
|
|
1233
|
-
const start = path4.join(root, startRel);
|
|
1234
|
-
if (!fs4.existsSync(start))
|
|
1235
|
-
return [];
|
|
1236
|
-
const artifacts = [];
|
|
1237
|
-
const stack = [
|
|
1238
|
-
{ abs: start, depth: 0 }
|
|
1239
|
-
];
|
|
1240
|
-
while (stack.length > 0 && artifacts.length < MAX_SPEC_FILES) {
|
|
1241
|
-
const item = stack.pop();
|
|
1242
|
-
if (!item || item.depth > MAX_WALK_DEPTH)
|
|
1243
|
-
continue;
|
|
1244
|
-
let entries;
|
|
1245
|
-
try {
|
|
1246
|
-
entries = fs4.readdirSync(item.abs, { withFileTypes: true });
|
|
1247
|
-
} catch {
|
|
1248
|
-
continue;
|
|
1249
|
-
}
|
|
1250
|
-
const dirents = entries.filter((entry) => typeof entry?.name === "string");
|
|
1251
|
-
for (const entry of dirents.sort((a, b) => b.name.localeCompare(a.name))) {
|
|
1252
|
-
const abs = path4.join(item.abs, entry.name);
|
|
1253
|
-
if (entry.isSymbolicLink())
|
|
1254
|
-
continue;
|
|
1255
|
-
if (entry.isDirectory()) {
|
|
1256
|
-
stack.push({ abs, depth: item.depth + 1 });
|
|
1257
|
-
} else if (entry.isFile() && entry.name === "spec.md") {
|
|
1258
|
-
const artifact = fileArtifact(root, abs);
|
|
1259
|
-
if (artifact)
|
|
1260
|
-
artifacts.push(artifact);
|
|
1261
|
-
if (artifacts.length >= MAX_SPEC_FILES)
|
|
1262
|
-
break;
|
|
1263
|
-
}
|
|
1264
|
-
}
|
|
1265
|
-
}
|
|
1266
|
-
return artifacts.sort((a, b) => a.relPath.localeCompare(b.relPath));
|
|
1267
|
-
}
|
|
1268
|
-
function listOpenSpecChanges(root) {
|
|
1269
|
-
const changesDir = path4.join(root, OPENSPEC_ROOT, "changes");
|
|
1270
|
-
if (!fs4.existsSync(changesDir))
|
|
1271
|
-
return [];
|
|
1272
|
-
let entries;
|
|
1273
|
-
try {
|
|
1274
|
-
entries = fs4.readdirSync(changesDir, { withFileTypes: true });
|
|
1275
|
-
} catch {
|
|
1276
|
-
return [];
|
|
1277
|
-
}
|
|
1278
|
-
return entries.filter((entry) => entry.isDirectory() && entry.name !== "archive").sort((a, b) => a.name.localeCompare(b.name)).map((entry) => {
|
|
1279
|
-
const rel = path4.join(OPENSPEC_ROOT, "changes", entry.name);
|
|
1280
|
-
return {
|
|
1281
|
-
id: entry.name,
|
|
1282
|
-
proposal: fs4.existsSync(path4.join(root, rel, "proposal.md")),
|
|
1283
|
-
design: fs4.existsSync(path4.join(root, rel, "design.md")),
|
|
1284
|
-
tasks: fs4.existsSync(path4.join(root, rel, "tasks.md")),
|
|
1285
|
-
specs: walkSpecFiles(root, path4.join(rel, "specs"))
|
|
1286
|
-
};
|
|
1287
|
-
});
|
|
1288
|
-
}
|
|
1289
|
-
function detectKind(line, current) {
|
|
1290
|
-
const upper = line.toUpperCase();
|
|
1291
|
-
if (/^#{2,6}\s+ADDED\b/.test(upper))
|
|
1292
|
-
return "ADDED";
|
|
1293
|
-
if (/^#{2,6}\s+MODIFIED\b/.test(upper))
|
|
1294
|
-
return "MODIFIED";
|
|
1295
|
-
if (/^#{2,6}\s+REMOVED\b/.test(upper))
|
|
1296
|
-
return "REMOVED";
|
|
1297
|
-
return current;
|
|
1298
|
-
}
|
|
1299
|
-
function requirementTextFromBlock(title, block, kind) {
|
|
1300
|
-
const joined = block.join(" ").replace(/\s+/g, " ").trim();
|
|
1301
|
-
const obligation = joined.match(/\b(MUST|SHALL|SHOULD|MAY)\b/i)?.[1];
|
|
1302
|
-
if (obligation)
|
|
1303
|
-
return joined;
|
|
1304
|
-
if (kind === "REMOVED") {
|
|
1305
|
-
return `MAY remove or retire behavior for ${title}.`;
|
|
1306
|
-
}
|
|
1307
|
-
return `MUST satisfy OpenSpec requirement ${title}.`;
|
|
1308
|
-
}
|
|
1309
|
-
function parseRequirements(content, sourceRel, defaultKind) {
|
|
1310
|
-
const requirements = [];
|
|
1311
|
-
const lines = content.replace(/\r\n/g, `
|
|
1312
|
-
`).split(`
|
|
1313
|
-
`);
|
|
1314
|
-
let kind = defaultKind;
|
|
1315
|
-
for (let i = 0;i < lines.length; i++) {
|
|
1316
|
-
const line = lines[i];
|
|
1317
|
-
kind = detectKind(line, kind);
|
|
1318
|
-
const explicit = line.match(/\b(FR-(?!000)\d{3})\b/);
|
|
1319
|
-
if (explicit && /\b(MUST|SHALL|SHOULD|MAY)\b/i.test(line)) {
|
|
1320
|
-
requirements.push({
|
|
1321
|
-
id: explicit[1].toUpperCase(),
|
|
1322
|
-
kind,
|
|
1323
|
-
title: explicit[1].toUpperCase(),
|
|
1324
|
-
text: line.trim().replace(/^\s*[-*]\s*/, ""),
|
|
1325
|
-
sourceRel
|
|
1326
|
-
});
|
|
1327
|
-
continue;
|
|
1328
|
-
}
|
|
1329
|
-
const openSpecReq = line.match(/^#{3,4}\s+Requirement:\s*(.+)$/i);
|
|
1330
|
-
if (!openSpecReq)
|
|
1331
|
-
continue;
|
|
1332
|
-
const title = openSpecReq[1].trim();
|
|
1333
|
-
const block = [];
|
|
1334
|
-
for (let j = i + 1;j < lines.length; j++) {
|
|
1335
|
-
if (/^##\s+/.test(lines[j]))
|
|
1336
|
-
break;
|
|
1337
|
-
if (/^#{3,4}\s+Requirement:/i.test(lines[j]))
|
|
1338
|
-
break;
|
|
1339
|
-
const trimmed = lines[j].trim();
|
|
1340
|
-
if (trimmed)
|
|
1341
|
-
block.push(trimmed);
|
|
1342
|
-
}
|
|
1343
|
-
const id = block.join(`
|
|
1344
|
-
`).match(/\b(FR-(?!000)\d{3})\b/)?.[1] ?? null;
|
|
1345
|
-
requirements.push({
|
|
1346
|
-
id: id?.toUpperCase() ?? null,
|
|
1347
|
-
kind,
|
|
1348
|
-
title,
|
|
1349
|
-
text: requirementTextFromBlock(title, block, kind),
|
|
1350
|
-
sourceRel
|
|
1351
|
-
});
|
|
1352
|
-
}
|
|
1353
|
-
return requirements;
|
|
1354
|
-
}
|
|
1355
|
-
function nextFrId(used, warnings, reserved) {
|
|
1356
|
-
for (let n = 1;n <= 999; n++) {
|
|
1357
|
-
const id = `FR-${String(n).padStart(3, "0")}`;
|
|
1358
|
-
if (!used.has(id) && !reserved?.has(id)) {
|
|
1359
|
-
used.add(id);
|
|
1360
|
-
return id;
|
|
1361
|
-
}
|
|
1362
|
-
}
|
|
1363
|
-
warnings.push("More than 999 FR identifiers are required; reusing FR-999.");
|
|
1364
|
-
return "FR-999";
|
|
1365
|
-
}
|
|
1366
|
-
function renderRequirement(req, used, warnings, reserved) {
|
|
1367
|
-
const id = req.id && !used.has(req.id) ? req.id : nextFrId(used, warnings, reserved);
|
|
1368
|
-
if (req.id && req.id !== id) {
|
|
1369
|
-
warnings.push(`Duplicate requirement id ${req.id} in ${req.sourceRel}; generated ${id}.`);
|
|
1370
|
-
}
|
|
1371
|
-
if (req.id === id)
|
|
1372
|
-
used.add(id);
|
|
1373
|
-
let text = req.text;
|
|
1374
|
-
if (!text.includes(id)) {
|
|
1375
|
-
text = `${id}: ${text}`;
|
|
1376
|
-
}
|
|
1377
|
-
return `- ${text} _(source: ${req.sourceRel})_`;
|
|
1378
|
-
}
|
|
1379
|
-
function detectSpeckit(directory) {
|
|
1380
|
-
const root = path4.resolve(directory);
|
|
1381
|
-
const markerPath = path4.join(root, SPECKIT_MARKER);
|
|
1382
|
-
const markerPresent = fs4.statSync(markerPath, { throwIfNoEntry: false })?.isDirectory() ?? false;
|
|
1383
|
-
if (!markerPresent) {
|
|
1384
|
-
return { markerPresent: false, features: [] };
|
|
1385
|
-
}
|
|
1386
|
-
const specsRoot = path4.join(root, SPECKIT_SPECS_DIR);
|
|
1387
|
-
if (!fs4.existsSync(specsRoot)) {
|
|
1388
|
-
return { markerPresent: true, features: [] };
|
|
1389
|
-
}
|
|
1390
|
-
let entries;
|
|
1391
|
-
try {
|
|
1392
|
-
entries = fs4.readdirSync(specsRoot, { withFileTypes: true });
|
|
1393
|
-
} catch {
|
|
1394
|
-
return { markerPresent: true, features: [] };
|
|
1395
|
-
}
|
|
1396
|
-
const featureDirs = entries.filter((entry) => typeof entry?.name === "string" && !entry.isSymbolicLink() && entry.isDirectory()).sort((a, b) => a.name.localeCompare(b.name));
|
|
1397
|
-
const features = [];
|
|
1398
|
-
for (const entry of featureDirs) {
|
|
1399
|
-
if (features.length >= MAX_SPEC_FILES)
|
|
1400
|
-
break;
|
|
1401
|
-
const specAbs = path4.join(specsRoot, entry.name, "spec.md");
|
|
1402
|
-
let stat;
|
|
1403
|
-
try {
|
|
1404
|
-
stat = fs4.lstatSync(specAbs);
|
|
1405
|
-
} catch {
|
|
1406
|
-
continue;
|
|
1407
|
-
}
|
|
1408
|
-
if (!stat.isFile() || stat.size > MAX_SOURCE_BYTES)
|
|
1409
|
-
continue;
|
|
1410
|
-
features.push({
|
|
1411
|
-
featureId: entry.name,
|
|
1412
|
-
specRelPath: toPosix(path4.relative(root, specAbs))
|
|
1413
|
-
});
|
|
1414
|
-
}
|
|
1415
|
-
return { markerPresent: true, features };
|
|
1416
|
-
}
|
|
1417
|
-
function parseSpeckitRequirements(content, sourceRel) {
|
|
1418
|
-
const requirements = [];
|
|
1419
|
-
const lines = content.replace(/\r\n/g, `
|
|
1420
|
-
`).split(`
|
|
1421
|
-
`);
|
|
1422
|
-
let inFrSection = false;
|
|
1423
|
-
for (const line of lines) {
|
|
1424
|
-
if (/^##\s+/.test(line)) {
|
|
1425
|
-
inFrSection = /^##\s+Functional Requirements\s*$/i.test(line);
|
|
1426
|
-
continue;
|
|
1427
|
-
}
|
|
1428
|
-
if (!inFrSection)
|
|
1429
|
-
continue;
|
|
1430
|
-
if (!/^\s*[-*]\s+/.test(line))
|
|
1431
|
-
continue;
|
|
1432
|
-
if (!/\b(MUST|SHALL|SHOULD|MAY)\b/i.test(line))
|
|
1433
|
-
continue;
|
|
1434
|
-
const text = line.trim().replace(/^\s*[-*]\s+/, "");
|
|
1435
|
-
const explicit = line.match(/\b(FR-(?!000)\d{3})\b/);
|
|
1436
|
-
if (explicit) {
|
|
1437
|
-
requirements.push({
|
|
1438
|
-
id: explicit[1].toUpperCase(),
|
|
1439
|
-
kind: "CURRENT",
|
|
1440
|
-
title: explicit[1].toUpperCase(),
|
|
1441
|
-
text,
|
|
1442
|
-
sourceRel
|
|
1443
|
-
});
|
|
1444
|
-
continue;
|
|
1445
|
-
}
|
|
1446
|
-
requirements.push({
|
|
1447
|
-
id: null,
|
|
1448
|
-
kind: "CURRENT",
|
|
1449
|
-
title: text,
|
|
1450
|
-
text,
|
|
1451
|
-
sourceRel
|
|
1452
|
-
});
|
|
1453
|
-
}
|
|
1454
|
-
return requirements;
|
|
1455
|
-
}
|
|
1456
|
-
function resolveSpeckitProjection(directory, options = {}) {
|
|
1457
|
-
const root = path4.resolve(directory);
|
|
1458
|
-
const detection = detectSpeckit(root);
|
|
1459
|
-
if (!detection.markerPresent) {
|
|
1460
|
-
return { kind: "not_speckit" };
|
|
1461
|
-
}
|
|
1462
|
-
if (detection.features.length === 0) {
|
|
1463
|
-
return { kind: "empty" };
|
|
1464
|
-
}
|
|
1465
|
-
let selectedFeature;
|
|
1466
|
-
if (options.feature) {
|
|
1467
|
-
const found = detection.features.find((f) => f.featureId === options.feature);
|
|
1468
|
-
if (!found) {
|
|
1469
|
-
return {
|
|
1470
|
-
kind: "unknown_feature",
|
|
1471
|
-
feature: options.feature,
|
|
1472
|
-
available: detection.features.map((f) => f.featureId)
|
|
1473
|
-
};
|
|
1474
|
-
}
|
|
1475
|
-
selectedFeature = found;
|
|
1476
|
-
} else if (detection.features.length === 1) {
|
|
1477
|
-
selectedFeature = detection.features[0];
|
|
1478
|
-
} else {
|
|
1479
|
-
return {
|
|
1480
|
-
kind: "ambiguous",
|
|
1481
|
-
features: detection.features.map((f) => f.featureId)
|
|
1482
|
-
};
|
|
1483
|
-
}
|
|
1484
|
-
const specAbs = path4.join(root, selectedFeature.specRelPath);
|
|
1485
|
-
const content = readTextBounded(specAbs);
|
|
1486
|
-
if (content === null) {
|
|
1487
|
-
return { kind: "zero_requirements", feature: selectedFeature.featureId };
|
|
1488
|
-
}
|
|
1489
|
-
const warnings = [];
|
|
1490
|
-
const usedIds = new Set;
|
|
1491
|
-
const requirements = parseSpeckitRequirements(content, selectedFeature.specRelPath);
|
|
1492
|
-
if (requirements.length === 0) {
|
|
1493
|
-
return { kind: "zero_requirements", feature: selectedFeature.featureId };
|
|
1494
|
-
}
|
|
1495
|
-
const reservedIds = new Set;
|
|
1496
|
-
for (const req of requirements) {
|
|
1497
|
-
if (req.id)
|
|
1498
|
-
reservedIds.add(req.id);
|
|
1499
|
-
}
|
|
1500
|
-
let mtimeMs = 0;
|
|
1501
|
-
try {
|
|
1502
|
-
mtimeMs = fs4.lstatSync(specAbs).mtimeMs;
|
|
1503
|
-
} catch {}
|
|
1504
|
-
const lines = [
|
|
1505
|
-
"# Specification: Effective SDD Projection",
|
|
1506
|
-
"",
|
|
1507
|
-
"Generated from Spec-Kit feature artifacts. Update the source artifacts, then run `/swarm sdd project` to refresh this projection.",
|
|
1508
|
-
"",
|
|
1509
|
-
"## Source Artifacts",
|
|
1510
|
-
`- ${selectedFeature.specRelPath}`,
|
|
1511
|
-
"",
|
|
1512
|
-
"## Functional Requirements"
|
|
1513
|
-
];
|
|
1514
|
-
for (const req of requirements) {
|
|
1515
|
-
lines.push(renderRequirement(req, usedIds, warnings, reservedIds));
|
|
1516
|
-
}
|
|
1517
|
-
const projected = `${lines.join(`
|
|
1518
|
-
`)}
|
|
1519
|
-
`;
|
|
1520
|
-
if (projected.length > MAX_SPEC_BYTES) {
|
|
1521
|
-
return {
|
|
1522
|
-
kind: "too_large",
|
|
1523
|
-
feature: selectedFeature.featureId,
|
|
1524
|
-
bytes: projected.length
|
|
1525
|
-
};
|
|
1526
|
-
}
|
|
1527
|
-
const validation = validateSpecContent(projected);
|
|
1528
|
-
if (!validation.valid) {
|
|
1529
|
-
warnings.push(...validation.issues.map((issue) => `Projection line ${issue.line}: ${issue.message}`));
|
|
1530
|
-
}
|
|
1531
|
-
const spec = {
|
|
1532
|
-
source: "speckit_projection",
|
|
1533
|
-
content: projected,
|
|
1534
|
-
hash: hash(projected),
|
|
1535
|
-
mtime: mtimeMs > 0 ? new Date(mtimeMs).toISOString() : null,
|
|
1536
|
-
sourcePaths: [selectedFeature.specRelPath],
|
|
1537
|
-
warnings
|
|
1538
|
-
};
|
|
1539
|
-
return { kind: "ok", spec, feature: selectedFeature.featureId };
|
|
1540
|
-
}
|
|
1541
|
-
function buildSpeckitProjectionSync(directory, options = {}) {
|
|
1542
|
-
const resolution = resolveSpeckitProjection(directory, options);
|
|
1543
|
-
return resolution.kind === "ok" ? resolution.spec : null;
|
|
1544
|
-
}
|
|
1545
|
-
function loadSddStatusSync(directory, opts) {
|
|
1546
|
-
const root = path4.resolve(directory);
|
|
1547
|
-
const swSpecPath = path4.join(root, SWARM_SPEC_REL);
|
|
1548
|
-
const openSpecPath = path4.join(root, OPENSPEC_ROOT);
|
|
1549
|
-
const errors = [];
|
|
1550
|
-
const warnings = [];
|
|
1551
|
-
const swSpecExists = fs4.existsSync(swSpecPath);
|
|
1552
|
-
const openSpecExists = fs4.existsSync(openSpecPath);
|
|
1553
|
-
const currentSpecs = walkSpecFiles(root, path4.join(OPENSPEC_ROOT, "specs"));
|
|
1554
|
-
const changes = listOpenSpecChanges(root);
|
|
1555
|
-
const effectiveSpec = readEffectiveSpecSync(root, opts);
|
|
1556
|
-
if (openSpecExists && currentSpecs.length === 0 && changes.length === 0) {
|
|
1557
|
-
errors.push("openspec/ exists but contains no specs or active changes.");
|
|
1558
|
-
}
|
|
1559
|
-
for (const change of changes) {
|
|
1560
|
-
if (!change.proposal) {
|
|
1561
|
-
warnings.push(`Change ${change.id} is missing proposal.md.`);
|
|
1562
|
-
}
|
|
1563
|
-
if (!change.tasks) {
|
|
1564
|
-
warnings.push(`Change ${change.id} is missing tasks.md; tasks remain proposal input, not plan state.`);
|
|
1565
|
-
}
|
|
1566
|
-
if (change.specs.length === 0) {
|
|
1567
|
-
errors.push(`Change ${change.id} has no specs/**/spec.md delta files.`);
|
|
1568
|
-
}
|
|
1569
|
-
}
|
|
1570
|
-
return {
|
|
1571
|
-
provider: effectiveSpec?.source ?? "none",
|
|
1572
|
-
swSpecExists,
|
|
1573
|
-
openSpecExists,
|
|
1574
|
-
currentSpecs,
|
|
1575
|
-
changes,
|
|
1576
|
-
effectiveSpec,
|
|
1577
|
-
errors,
|
|
1578
|
-
warnings
|
|
1579
|
-
};
|
|
1580
|
-
}
|
|
1581
|
-
function buildOpenSpecProjectionSync(directory, options = {}) {
|
|
1582
|
-
const root = path4.resolve(directory);
|
|
1583
|
-
const currentSpecs = walkSpecFiles(root, path4.join(OPENSPEC_ROOT, "specs"));
|
|
1584
|
-
const allChanges = listOpenSpecChanges(root);
|
|
1585
|
-
const changes = options.changeId ? allChanges.filter((change) => change.id === options.changeId) : allChanges;
|
|
1586
|
-
const sourcePaths = [];
|
|
1587
|
-
const warnings = [];
|
|
1588
|
-
const usedIds = new Set;
|
|
1589
|
-
const currentRequirements = [];
|
|
1590
|
-
const changeRequirements = new Map;
|
|
1591
|
-
let parsedRequirementCount = 0;
|
|
1592
|
-
let mtimeMs = 0;
|
|
1593
|
-
if (options.changeId && changes.length === 0)
|
|
1594
|
-
return null;
|
|
1595
|
-
if (currentSpecs.length === 0 && changes.length === 0)
|
|
1596
|
-
return null;
|
|
1597
|
-
for (const artifact of currentSpecs) {
|
|
1598
|
-
const abs = path4.join(root, artifact.relPath);
|
|
1599
|
-
const content2 = readTextBounded(abs);
|
|
1600
|
-
if (content2 === null) {
|
|
1601
|
-
warnings.push(`Skipped unreadable or oversized spec ${artifact.relPath}.`);
|
|
1602
|
-
continue;
|
|
1603
|
-
}
|
|
1604
|
-
sourcePaths.push(artifact.relPath);
|
|
1605
|
-
mtimeMs = Math.max(mtimeMs, artifact.mtimeMs);
|
|
1606
|
-
const parsed = parseRequirements(content2, artifact.relPath, "CURRENT");
|
|
1607
|
-
currentRequirements.push(...parsed);
|
|
1608
|
-
parsedRequirementCount += parsed.length;
|
|
1609
|
-
}
|
|
1610
|
-
for (const change of changes) {
|
|
1611
|
-
const reqs = [];
|
|
1612
|
-
for (const artifact of change.specs) {
|
|
1613
|
-
const abs = path4.join(root, artifact.relPath);
|
|
1614
|
-
const content2 = readTextBounded(abs);
|
|
1615
|
-
if (content2 === null) {
|
|
1616
|
-
warnings.push(`Skipped unreadable or oversized spec ${artifact.relPath}.`);
|
|
1617
|
-
continue;
|
|
1618
|
-
}
|
|
1619
|
-
sourcePaths.push(artifact.relPath);
|
|
1620
|
-
mtimeMs = Math.max(mtimeMs, artifact.mtimeMs);
|
|
1621
|
-
const parsed = parseRequirements(content2, artifact.relPath, "ADDED");
|
|
1622
|
-
reqs.push(...parsed);
|
|
1623
|
-
parsedRequirementCount += parsed.length;
|
|
1624
|
-
}
|
|
1625
|
-
changeRequirements.set(change.id, reqs);
|
|
1626
|
-
}
|
|
1627
|
-
if (sourcePaths.length > 0 && parsedRequirementCount === 0) {
|
|
1628
|
-
return null;
|
|
1629
|
-
}
|
|
1630
|
-
const lines = [
|
|
1631
|
-
"# Specification: Effective SDD Projection",
|
|
1632
|
-
"",
|
|
1633
|
-
"Generated from OpenSpec-compatible artifacts. Update the source artifacts, then run `/swarm sdd project` to refresh this projection.",
|
|
1634
|
-
"",
|
|
1635
|
-
"## Source Artifacts",
|
|
1636
|
-
...sourcePaths.map((rel) => `- ${rel}`),
|
|
1637
|
-
"",
|
|
1638
|
-
"## Current Requirements"
|
|
1639
|
-
];
|
|
1640
|
-
if (currentRequirements.length === 0) {
|
|
1641
|
-
lines.push("- No current OpenSpec requirements found in source artifacts.");
|
|
1642
|
-
warnings.push("No current requirements found; projection includes an advisory note only.");
|
|
1643
|
-
} else {
|
|
1644
|
-
for (const req of currentRequirements) {
|
|
1645
|
-
lines.push(renderRequirement(req, usedIds, warnings));
|
|
1646
|
-
}
|
|
1647
|
-
}
|
|
1648
|
-
for (const [changeId, reqs] of changeRequirements.entries()) {
|
|
1649
|
-
lines.push("", `## Pending Change: ${changeId}`);
|
|
1650
|
-
if (reqs.length === 0) {
|
|
1651
|
-
lines.push(`- ${nextFrId(usedIds, warnings)} SHOULD add OpenSpec delta requirements for change ${changeId}.`);
|
|
1652
|
-
warnings.push(`Change ${changeId} contains no parsable requirements.`);
|
|
1653
|
-
continue;
|
|
1654
|
-
}
|
|
1655
|
-
for (const req of reqs) {
|
|
1656
|
-
lines.push(renderRequirement(req, usedIds, warnings));
|
|
1657
|
-
}
|
|
1658
|
-
}
|
|
1659
|
-
const content = `${lines.join(`
|
|
1660
|
-
`)}
|
|
1661
|
-
`;
|
|
1662
|
-
if (content.length > MAX_SPEC_BYTES) {
|
|
1663
|
-
warnings.push(`Projected spec exceeds ${MAX_SPEC_BYTES} bytes; refusing to use projection.`);
|
|
1664
|
-
return null;
|
|
1665
|
-
}
|
|
1666
|
-
const validation = validateSpecContent(content);
|
|
1667
|
-
if (!validation.valid) {
|
|
1668
|
-
warnings.push(...validation.issues.map((issue) => `Projection line ${issue.line}: ${issue.message}`));
|
|
1669
|
-
}
|
|
1670
|
-
return {
|
|
1671
|
-
source: "openspec_projection",
|
|
1672
|
-
content,
|
|
1673
|
-
hash: hash(content),
|
|
1674
|
-
mtime: mtimeMs > 0 ? new Date(mtimeMs).toISOString() : null,
|
|
1675
|
-
sourcePaths,
|
|
1676
|
-
warnings
|
|
1677
|
-
};
|
|
1678
|
-
}
|
|
1679
|
-
function readEffectiveSpecSync(directory, opts) {
|
|
1680
|
-
const root = path4.resolve(directory);
|
|
1681
|
-
const swSpecPath = path4.join(root, SWARM_SPEC_REL);
|
|
1682
|
-
try {
|
|
1683
|
-
const stat = fs4.lstatSync(swSpecPath);
|
|
1684
|
-
if (stat.isFile() && stat.size <= MAX_SPEC_BYTES) {
|
|
1685
|
-
const content = fs4.readFileSync(swSpecPath, "utf-8");
|
|
1686
|
-
return {
|
|
1687
|
-
source: "swarm",
|
|
1688
|
-
content,
|
|
1689
|
-
hash: hash(content),
|
|
1690
|
-
mtime: stat.mtime.toISOString(),
|
|
1691
|
-
sourcePaths: [toPosix(SWARM_SPEC_REL)],
|
|
1692
|
-
warnings: []
|
|
1693
|
-
};
|
|
1694
|
-
}
|
|
1695
|
-
} catch (error2) {
|
|
1696
|
-
if (error2.code !== "ENOENT") {
|
|
1697
|
-
throw error2;
|
|
1698
|
-
}
|
|
1699
|
-
}
|
|
1700
|
-
if (opts?.source) {
|
|
1701
|
-
switch (opts.source) {
|
|
1702
|
-
case "openspec":
|
|
1703
|
-
return buildOpenSpecProjectionSync(root);
|
|
1704
|
-
case "speckit":
|
|
1705
|
-
return buildSpeckitProjectionSync(root, { feature: opts.feature });
|
|
1706
|
-
case "swarm":
|
|
1707
|
-
return null;
|
|
1708
|
-
}
|
|
1709
|
-
}
|
|
1710
|
-
const speckitDetection = detectSpeckit(root);
|
|
1711
|
-
if (!speckitDetection.markerPresent) {
|
|
1712
|
-
return buildOpenSpecProjectionSync(root);
|
|
1713
|
-
}
|
|
1714
|
-
const speckitPresent = speckitDetection.features.length > 0;
|
|
1715
|
-
if (!speckitPresent) {
|
|
1716
|
-
return buildOpenSpecProjectionSync(root);
|
|
1717
|
-
}
|
|
1718
|
-
const openspecProjection = buildOpenSpecProjectionSync(root);
|
|
1719
|
-
const openspecPresent = openspecProjection !== null;
|
|
1720
|
-
if (openspecPresent) {
|
|
1721
|
-
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.");
|
|
1722
|
-
return null;
|
|
1723
|
-
}
|
|
1724
|
-
return buildSpeckitProjectionSync(root, { feature: opts?.feature });
|
|
1725
|
-
}
|
|
1726
|
-
function writeProjectedSpecSync(directory, options = {}) {
|
|
1727
|
-
const root = path4.resolve(directory);
|
|
1728
|
-
const projection = options.source === "speckit" ? buildSpeckitProjectionSync(root, { feature: options.feature }) : buildOpenSpecProjectionSync(root, { changeId: options.changeId });
|
|
1729
|
-
const target = path4.join(root, SWARM_SPEC_REL);
|
|
1730
|
-
if (!projection || options.dryRun) {
|
|
1731
|
-
return { written: false, projection, path: target };
|
|
1732
|
-
}
|
|
1733
|
-
fs4.mkdirSync(path4.dirname(target), { recursive: true });
|
|
1734
|
-
let archivePath;
|
|
1735
|
-
if (fs4.existsSync(target)) {
|
|
1736
|
-
const prior = fs4.readFileSync(target, "utf-8");
|
|
1737
|
-
if (prior !== projection.content) {
|
|
1738
|
-
const archiveDir = path4.join(root, ".swarm", "spec-archive");
|
|
1739
|
-
fs4.mkdirSync(archiveDir, { recursive: true });
|
|
1740
|
-
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
1741
|
-
archivePath = path4.join(archiveDir, `sdd-projection-${stamp}.md`);
|
|
1742
|
-
fs4.writeFileSync(archivePath, prior, "utf-8");
|
|
1743
|
-
}
|
|
1744
|
-
}
|
|
1745
|
-
const tmp = `${target}.tmp-${process.pid}-${Date.now()}`;
|
|
1746
|
-
fs4.writeFileSync(tmp, projection.content, "utf-8");
|
|
1747
|
-
fs4.renameSync(tmp, target);
|
|
1748
|
-
return { written: true, projection, archivePath, path: target };
|
|
1749
|
-
}
|
|
1750
|
-
function validateSpeckit(directory, options = {}) {
|
|
1751
|
-
const root = path4.resolve(directory);
|
|
1752
|
-
const resolution = resolveSpeckitProjection(root, options);
|
|
1753
|
-
const problems = [];
|
|
1754
|
-
if (resolution.kind !== "ok" && resolution.kind !== "zero_requirements") {
|
|
1755
|
-
return { resolution, problems };
|
|
1756
|
-
}
|
|
1757
|
-
if (resolution.kind === "zero_requirements") {
|
|
1758
|
-
problems.push(`Feature '${resolution.feature}' contains no parsable functional requirements.`);
|
|
1759
|
-
}
|
|
1760
|
-
const detection = detectSpeckit(root);
|
|
1761
|
-
const featureEntry = detection.features.find((f) => f.featureId === resolution.feature);
|
|
1762
|
-
if (!featureEntry) {
|
|
1763
|
-
return { resolution, problems };
|
|
1764
|
-
}
|
|
1765
|
-
const specAbs = path4.join(root, featureEntry.specRelPath);
|
|
1766
|
-
let specContent = null;
|
|
1767
|
-
try {
|
|
1768
|
-
specContent = readTextBounded(specAbs);
|
|
1769
|
-
} catch {}
|
|
1770
|
-
if (specContent !== null) {
|
|
1771
|
-
const specLines = specContent.replace(/\r\n/g, `
|
|
1772
|
-
`).split(`
|
|
1773
|
-
`);
|
|
1774
|
-
for (const requiredHeader of SPECKIT_REQUIRED_SECTIONS) {
|
|
1775
|
-
if (!specLines.some((line) => line.trimEnd() === requiredHeader)) {
|
|
1776
|
-
problems.push(`Missing required spec.md section: ${requiredHeader}`);
|
|
1777
|
-
}
|
|
1778
|
-
}
|
|
1779
|
-
}
|
|
1780
|
-
const tasksAbs = path4.join(path4.dirname(specAbs), "tasks.md");
|
|
1781
|
-
let tasksContent = null;
|
|
1782
|
-
try {
|
|
1783
|
-
tasksContent = readTextBounded(tasksAbs);
|
|
1784
|
-
} catch {}
|
|
1785
|
-
if (tasksContent !== null) {
|
|
1786
|
-
const tasksLines = tasksContent.replace(/\r\n/g, `
|
|
1787
|
-
`).split(`
|
|
1788
|
-
`);
|
|
1789
|
-
for (const line of tasksLines) {
|
|
1790
|
-
const taskMatch = line.match(/^\s*-\s+\[[ xX]\]\s+(T\d+)/);
|
|
1791
|
-
if (!taskMatch)
|
|
1792
|
-
continue;
|
|
1793
|
-
const taskId = taskMatch[1];
|
|
1794
|
-
const hasStoryRef = /\[US\d+\]/i.test(line);
|
|
1795
|
-
const hasReqRef = /\bFR-(?!000)\d{3}\b/i.test(line);
|
|
1796
|
-
if (!hasStoryRef && !hasReqRef) {
|
|
1797
|
-
problems.push(`Task ${taskId} has no spec/requirement reference.`);
|
|
1798
|
-
}
|
|
1799
|
-
}
|
|
1800
|
-
}
|
|
1801
|
-
return { resolution, problems };
|
|
1802
|
-
}
|
|
1803
|
-
|
|
1804
|
-
// src/utils/spec-hash.ts
|
|
1091
|
+
import * as fsSync from "fs";
|
|
1092
|
+
import { join as join3 } from "path";
|
|
1805
1093
|
async function computeSpecHash(directory) {
|
|
1806
1094
|
const spec = _internals3.readEffectiveSpecSync(directory);
|
|
1807
1095
|
if (!spec)
|
|
1808
1096
|
return null;
|
|
1809
|
-
return
|
|
1097
|
+
return createHash("sha256").update(spec.content, "utf-8").digest("hex");
|
|
1810
1098
|
}
|
|
1811
1099
|
async function isSpecStale(directory, plan) {
|
|
1812
1100
|
const currentHash = await _internals3.computeSpecHash(directory);
|
|
@@ -1830,7 +1118,7 @@ async function isSpecStale(directory, plan) {
|
|
|
1830
1118
|
return { stale: false };
|
|
1831
1119
|
}
|
|
1832
1120
|
function isObligationPreserving(directory) {
|
|
1833
|
-
const snapshotPath =
|
|
1121
|
+
const snapshotPath = join3(directory, ".swarm", "spec-snapshot.md");
|
|
1834
1122
|
try {
|
|
1835
1123
|
let recordedContent;
|
|
1836
1124
|
try {
|
|
@@ -1876,7 +1164,7 @@ var _internals3 = {
|
|
|
1876
1164
|
readEffectiveSpecSync
|
|
1877
1165
|
};
|
|
1878
1166
|
function computeSpecDiff(directory) {
|
|
1879
|
-
const snapshotPath =
|
|
1167
|
+
const snapshotPath = join3(directory, ".swarm", "spec-snapshot.md");
|
|
1880
1168
|
let recordedContent;
|
|
1881
1169
|
try {
|
|
1882
1170
|
const snapshotSize = fsSync.statSync(snapshotPath).size;
|
|
@@ -1963,8 +1251,8 @@ function computeLcsDiff(oldLines, newLines) {
|
|
|
1963
1251
|
|
|
1964
1252
|
// src/plan/ledger.ts
|
|
1965
1253
|
import * as crypto from "crypto";
|
|
1966
|
-
import * as
|
|
1967
|
-
import * as
|
|
1254
|
+
import * as fs4 from "fs";
|
|
1255
|
+
import * as path4 from "path";
|
|
1968
1256
|
|
|
1969
1257
|
// src/plan/utils.ts
|
|
1970
1258
|
function derivePlanId(plan) {
|
|
@@ -1974,7 +1262,7 @@ function derivePlanId(plan) {
|
|
|
1974
1262
|
// src/plan/ledger.ts
|
|
1975
1263
|
var LEDGER_SCHEMA_VERSION = "1.1.0";
|
|
1976
1264
|
var LEDGER_FILENAME = "plan-ledger.jsonl";
|
|
1977
|
-
var LEDGER_LOCK_PATH =
|
|
1265
|
+
var LEDGER_LOCK_PATH = path4.join(".swarm", LEDGER_FILENAME);
|
|
1978
1266
|
var PLAN_JSON_FILENAME = "plan.json";
|
|
1979
1267
|
|
|
1980
1268
|
class LedgerStaleWriterError extends Error {
|
|
@@ -1984,10 +1272,10 @@ class LedgerStaleWriterError extends Error {
|
|
|
1984
1272
|
}
|
|
1985
1273
|
}
|
|
1986
1274
|
function getLedgerPath(directory) {
|
|
1987
|
-
return
|
|
1275
|
+
return path4.join(directory, ".swarm", LEDGER_FILENAME);
|
|
1988
1276
|
}
|
|
1989
1277
|
function getPlanJsonPath(directory) {
|
|
1990
|
-
return
|
|
1278
|
+
return path4.join(directory, ".swarm", PLAN_JSON_FILENAME);
|
|
1991
1279
|
}
|
|
1992
1280
|
function computePlanHash(plan) {
|
|
1993
1281
|
const normalized = {
|
|
@@ -2022,7 +1310,7 @@ function computePlanHash(plan) {
|
|
|
2022
1310
|
function computeCurrentPlanHash(directory) {
|
|
2023
1311
|
const planPath = getPlanJsonPath(directory);
|
|
2024
1312
|
try {
|
|
2025
|
-
const content =
|
|
1313
|
+
const content = fs4.readFileSync(planPath, "utf8");
|
|
2026
1314
|
const plan = JSON.parse(content);
|
|
2027
1315
|
return computePlanHash(plan);
|
|
2028
1316
|
} catch {
|
|
@@ -2031,15 +1319,15 @@ function computeCurrentPlanHash(directory) {
|
|
|
2031
1319
|
}
|
|
2032
1320
|
async function ledgerExists(directory) {
|
|
2033
1321
|
const ledgerPath = getLedgerPath(directory);
|
|
2034
|
-
return
|
|
1322
|
+
return fs4.existsSync(ledgerPath);
|
|
2035
1323
|
}
|
|
2036
1324
|
async function getLatestLedgerSeq(directory) {
|
|
2037
1325
|
const ledgerPath = getLedgerPath(directory);
|
|
2038
|
-
if (!
|
|
1326
|
+
if (!fs4.existsSync(ledgerPath)) {
|
|
2039
1327
|
return 0;
|
|
2040
1328
|
}
|
|
2041
1329
|
try {
|
|
2042
|
-
const content =
|
|
1330
|
+
const content = fs4.readFileSync(ledgerPath, "utf8");
|
|
2043
1331
|
const lines = content.trim().split(`
|
|
2044
1332
|
`).filter((line) => line.trim() !== "");
|
|
2045
1333
|
if (lines.length === 0) {
|
|
@@ -2061,11 +1349,11 @@ async function getLatestLedgerSeq(directory) {
|
|
|
2061
1349
|
}
|
|
2062
1350
|
async function readLedgerEvents(directory) {
|
|
2063
1351
|
const ledgerPath = getLedgerPath(directory);
|
|
2064
|
-
if (!
|
|
1352
|
+
if (!fs4.existsSync(ledgerPath)) {
|
|
2065
1353
|
return [];
|
|
2066
1354
|
}
|
|
2067
1355
|
try {
|
|
2068
|
-
const content =
|
|
1356
|
+
const content = fs4.readFileSync(ledgerPath, "utf8");
|
|
2069
1357
|
const lines = content.trim().split(`
|
|
2070
1358
|
`).filter((line) => line.trim() !== "");
|
|
2071
1359
|
const events = [];
|
|
@@ -2090,15 +1378,15 @@ async function readLedgerEvents(directory) {
|
|
|
2090
1378
|
async function initLedger(directory, planId, initialPlanHash, initialPlan) {
|
|
2091
1379
|
const ledgerPath = getLedgerPath(directory);
|
|
2092
1380
|
const planJsonPath = getPlanJsonPath(directory);
|
|
2093
|
-
if (
|
|
1381
|
+
if (fs4.existsSync(ledgerPath)) {
|
|
2094
1382
|
throw new Error("Ledger already initialized. Use appendLedgerEvent to add events.");
|
|
2095
1383
|
}
|
|
2096
1384
|
let planHashAfter = initialPlanHash ?? "";
|
|
2097
1385
|
let embeddedPlan = initialPlan;
|
|
2098
1386
|
if (!initialPlanHash) {
|
|
2099
1387
|
try {
|
|
2100
|
-
if (
|
|
2101
|
-
const content =
|
|
1388
|
+
if (fs4.existsSync(planJsonPath)) {
|
|
1389
|
+
const content = fs4.readFileSync(planJsonPath, "utf8");
|
|
2102
1390
|
const plan = JSON.parse(content);
|
|
2103
1391
|
planHashAfter = computePlanHash(plan);
|
|
2104
1392
|
if (!embeddedPlan)
|
|
@@ -2118,12 +1406,12 @@ async function initLedger(directory, planId, initialPlanHash, initialPlan) {
|
|
|
2118
1406
|
schema_version: LEDGER_SCHEMA_VERSION,
|
|
2119
1407
|
...payload ? { payload } : {}
|
|
2120
1408
|
};
|
|
2121
|
-
|
|
1409
|
+
fs4.mkdirSync(path4.join(directory, ".swarm"), { recursive: true });
|
|
2122
1410
|
const tempPath = `${ledgerPath}.tmp.${Date.now()}.${Math.floor(Math.random() * 1e9)}`;
|
|
2123
1411
|
const line = `${JSON.stringify(event)}
|
|
2124
1412
|
`;
|
|
2125
|
-
|
|
2126
|
-
|
|
1413
|
+
fs4.writeFileSync(tempPath, line, "utf8");
|
|
1414
|
+
fs4.renameSync(tempPath, ledgerPath);
|
|
2127
1415
|
}
|
|
2128
1416
|
async function appendLedgerEvent(directory, eventInput, options) {
|
|
2129
1417
|
return withEvidenceLock(directory, LEDGER_LOCK_PATH, "plan-ledger", "append-ledger-event", async () => {
|
|
@@ -2146,17 +1434,17 @@ async function appendLedgerEvent(directory, eventInput, options) {
|
|
|
2146
1434
|
plan_hash_after: planHashAfter,
|
|
2147
1435
|
schema_version: LEDGER_SCHEMA_VERSION
|
|
2148
1436
|
};
|
|
2149
|
-
|
|
1437
|
+
fs4.mkdirSync(path4.join(directory, ".swarm"), { recursive: true });
|
|
2150
1438
|
const tempPath = `${ledgerPath}.tmp.${Date.now()}.${Math.floor(Math.random() * 1e9)}`;
|
|
2151
1439
|
const line = `${JSON.stringify(event)}
|
|
2152
1440
|
`;
|
|
2153
|
-
if (
|
|
2154
|
-
const existingContent =
|
|
2155
|
-
|
|
1441
|
+
if (fs4.existsSync(ledgerPath)) {
|
|
1442
|
+
const existingContent = fs4.readFileSync(ledgerPath, "utf8");
|
|
1443
|
+
fs4.writeFileSync(tempPath, existingContent + line, "utf8");
|
|
2156
1444
|
} else {
|
|
2157
1445
|
throw new Error("Ledger not initialized. Call initLedger() first.");
|
|
2158
1446
|
}
|
|
2159
|
-
|
|
1447
|
+
fs4.renameSync(tempPath, ledgerPath);
|
|
2160
1448
|
return event;
|
|
2161
1449
|
});
|
|
2162
1450
|
}
|
|
@@ -2244,12 +1532,12 @@ async function replayFromLedger(directory, _options) {
|
|
|
2244
1532
|
}
|
|
2245
1533
|
}
|
|
2246
1534
|
const planJsonPath = getPlanJsonPath(directory);
|
|
2247
|
-
if (!
|
|
1535
|
+
if (!fs4.existsSync(planJsonPath)) {
|
|
2248
1536
|
return null;
|
|
2249
1537
|
}
|
|
2250
1538
|
let plan;
|
|
2251
1539
|
try {
|
|
2252
|
-
const content =
|
|
1540
|
+
const content = fs4.readFileSync(planJsonPath, "utf8");
|
|
2253
1541
|
plan = JSON.parse(content);
|
|
2254
1542
|
} catch {
|
|
2255
1543
|
return null;
|
|
@@ -2344,11 +1632,11 @@ function applyEventToPlan(plan, event) {
|
|
|
2344
1632
|
}
|
|
2345
1633
|
async function readLedgerEventsWithIntegrity(directory) {
|
|
2346
1634
|
const ledgerPath = getLedgerPath(directory);
|
|
2347
|
-
if (!
|
|
1635
|
+
if (!fs4.existsSync(ledgerPath)) {
|
|
2348
1636
|
return { events: [], truncated: false, badSuffix: null };
|
|
2349
1637
|
}
|
|
2350
1638
|
try {
|
|
2351
|
-
const content =
|
|
1639
|
+
const content = fs4.readFileSync(ledgerPath, "utf8");
|
|
2352
1640
|
const lines = content.split(`
|
|
2353
1641
|
`);
|
|
2354
1642
|
const events = [];
|
|
@@ -2377,9 +1665,9 @@ async function readLedgerEventsWithIntegrity(directory) {
|
|
|
2377
1665
|
}
|
|
2378
1666
|
async function quarantineLedgerSuffix(directory, badSuffix) {
|
|
2379
1667
|
try {
|
|
2380
|
-
const quarantinePath =
|
|
2381
|
-
|
|
2382
|
-
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)}`);
|
|
2383
1671
|
} catch {}
|
|
2384
1672
|
}
|
|
2385
1673
|
async function loadLastApprovedPlan(directory, expectedPlanId) {
|
|
@@ -2475,7 +1763,7 @@ async function retryCasWithBackoff(directory, eventInput, options) {
|
|
|
2475
1763
|
expectedHashPrefix: currentExpected.slice(0, 8),
|
|
2476
1764
|
delayMs
|
|
2477
1765
|
});
|
|
2478
|
-
await new Promise((
|
|
1766
|
+
await new Promise((resolve3) => setTimeout(resolve3, delayMs));
|
|
2479
1767
|
if (options.verifyValid) {
|
|
2480
1768
|
const stillValid = await options.verifyValid();
|
|
2481
1769
|
if (!stillValid)
|
|
@@ -2518,7 +1806,7 @@ async function getLatestLedgerHash(directory) {
|
|
|
2518
1806
|
}
|
|
2519
1807
|
}
|
|
2520
1808
|
async function surfaceLedgerStaleIfPersisted(directory, plan) {
|
|
2521
|
-
const resolvedWorkspace =
|
|
1809
|
+
const resolvedWorkspace = path5.resolve(directory);
|
|
2522
1810
|
if (!ledgerStaleWorkspaces.has(resolvedWorkspace)) {
|
|
2523
1811
|
return plan;
|
|
2524
1812
|
}
|
|
@@ -2537,7 +1825,7 @@ async function surfaceLedgerStaleIfPersisted(directory, plan) {
|
|
|
2537
1825
|
return plan;
|
|
2538
1826
|
}
|
|
2539
1827
|
async function parsePlanJsonCached(directory) {
|
|
2540
|
-
const planJsonPath =
|
|
1828
|
+
const planJsonPath = path5.resolve(directory, ".swarm", "plan.json");
|
|
2541
1829
|
return readCachedParsedFile(planJsonPath, PLAN_JSON_CACHE_NAMESPACE, () => readSwarmFileAsync(directory, "plan.json"), (planJsonContent) => {
|
|
2542
1830
|
if (planJsonContent.includes("\x00") || planJsonContent.includes("\uFFFD")) {
|
|
2543
1831
|
throw new Error("Plan rejected: .swarm/plan.json contains null bytes or invalid encoding");
|
|
@@ -2597,16 +1885,16 @@ async function isPlanMdInSync(directory, plan, cache) {
|
|
|
2597
1885
|
return false;
|
|
2598
1886
|
}
|
|
2599
1887
|
async function regeneratePlanMarkdown(directory, plan) {
|
|
2600
|
-
const swarmDir =
|
|
1888
|
+
const swarmDir = path5.resolve(directory, ".swarm");
|
|
2601
1889
|
const contentHash = computePlanContentHash(plan);
|
|
2602
1890
|
const markdown = derivePlanMarkdown(plan);
|
|
2603
1891
|
const markdownWithHash = `<!-- PLAN_HASH: ${contentHash} -->
|
|
2604
1892
|
${markdown}`;
|
|
2605
|
-
const mdPath =
|
|
2606
|
-
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)}`);
|
|
2607
1895
|
try {
|
|
2608
1896
|
await bunWrite(mdTempPath, markdownWithHash);
|
|
2609
|
-
|
|
1897
|
+
renameSync4(mdTempPath, mdPath);
|
|
2610
1898
|
} finally {
|
|
2611
1899
|
try {
|
|
2612
1900
|
unlinkSync3(mdTempPath);
|
|
@@ -2635,7 +1923,7 @@ async function loadPlan(directory, cache) {
|
|
|
2635
1923
|
if (await ledgerExists(directory)) {
|
|
2636
1924
|
const planHash = computePlanHash(validated);
|
|
2637
1925
|
const ledgerHash = await getLatestLedgerHash(directory);
|
|
2638
|
-
const resolvedWorkspace =
|
|
1926
|
+
const resolvedWorkspace = path5.resolve(directory);
|
|
2639
1927
|
if (!startupLedgerCheckedWorkspaces.has(resolvedWorkspace)) {
|
|
2640
1928
|
startupLedgerCheckedWorkspaces.add(resolvedWorkspace);
|
|
2641
1929
|
if (ledgerHash !== "" && planHash !== ledgerHash) {
|
|
@@ -2701,7 +1989,7 @@ async function loadPlan(directory, cache) {
|
|
|
2701
1989
|
if (!preserving) {
|
|
2702
1990
|
try {
|
|
2703
1991
|
const diffInfo = computeSpecDiff(directory);
|
|
2704
|
-
const specStalenessPath =
|
|
1992
|
+
const specStalenessPath = path5.join(directory, ".swarm", "spec-staleness.json");
|
|
2705
1993
|
await fsPromises.writeFile(specStalenessPath, JSON.stringify({
|
|
2706
1994
|
type: "spec_stale_detected",
|
|
2707
1995
|
timestamp: new Date().toISOString(),
|
|
@@ -2715,7 +2003,7 @@ async function loadPlan(directory, cache) {
|
|
|
2715
2003
|
}, null, 2), "utf-8");
|
|
2716
2004
|
} catch {}
|
|
2717
2005
|
try {
|
|
2718
|
-
const eventsPath =
|
|
2006
|
+
const eventsPath = path5.join(directory, ".swarm", "events.jsonl");
|
|
2719
2007
|
const event = {
|
|
2720
2008
|
type: "spec_stale_detected",
|
|
2721
2009
|
timestamp: new Date().toISOString(),
|
|
@@ -2781,7 +2069,7 @@ async function loadPlan(directory, cache) {
|
|
|
2781
2069
|
return migrated;
|
|
2782
2070
|
}
|
|
2783
2071
|
if (await ledgerExists(directory)) {
|
|
2784
|
-
const resolvedDir =
|
|
2072
|
+
const resolvedDir = path5.resolve(directory);
|
|
2785
2073
|
const existingMutex = recoveryMutexes.get(resolvedDir);
|
|
2786
2074
|
if (existingMutex) {
|
|
2787
2075
|
await existingMutex;
|
|
@@ -2921,13 +2209,13 @@ async function savePlan(directory, plan, options) {
|
|
|
2921
2209
|
} else {
|
|
2922
2210
|
const existingEvents = await readLedgerEvents(directory);
|
|
2923
2211
|
if (existingEvents.length > 0 && existingEvents[0].plan_id !== planId) {
|
|
2924
|
-
const swarmDir2 =
|
|
2925
|
-
const oldLedgerPath =
|
|
2926
|
-
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`);
|
|
2927
2215
|
let backupExists = false;
|
|
2928
|
-
if (
|
|
2216
|
+
if (existsSync5(oldLedgerPath)) {
|
|
2929
2217
|
try {
|
|
2930
|
-
|
|
2218
|
+
renameSync4(oldLedgerPath, oldLedgerBackupPath);
|
|
2931
2219
|
backupExists = true;
|
|
2932
2220
|
} catch (renameErr) {
|
|
2933
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.`);
|
|
@@ -2942,13 +2230,13 @@ async function savePlan(directory, plan, options) {
|
|
|
2942
2230
|
const errorMessage2 = String(initErr);
|
|
2943
2231
|
if (errorMessage2.includes("already initialized")) {
|
|
2944
2232
|
try {
|
|
2945
|
-
if (
|
|
2233
|
+
if (existsSync5(oldLedgerBackupPath))
|
|
2946
2234
|
unlinkSync3(oldLedgerBackupPath);
|
|
2947
2235
|
} catch {}
|
|
2948
2236
|
} else {
|
|
2949
|
-
if (
|
|
2237
|
+
if (existsSync5(oldLedgerBackupPath)) {
|
|
2950
2238
|
try {
|
|
2951
|
-
|
|
2239
|
+
renameSync4(oldLedgerBackupPath, oldLedgerPath);
|
|
2952
2240
|
} catch {
|
|
2953
2241
|
copyFileSync(oldLedgerBackupPath, oldLedgerPath);
|
|
2954
2242
|
try {
|
|
@@ -2961,32 +2249,32 @@ async function savePlan(directory, plan, options) {
|
|
|
2961
2249
|
}
|
|
2962
2250
|
}
|
|
2963
2251
|
if (initSucceeded && backupExists) {
|
|
2964
|
-
const archivePath =
|
|
2252
|
+
const archivePath = path5.join(swarmDir2, `plan-ledger.archived-${Date.now()}-${Math.floor(Math.random() * 1e9)}.jsonl`);
|
|
2965
2253
|
try {
|
|
2966
|
-
|
|
2254
|
+
renameSync4(oldLedgerBackupPath, archivePath);
|
|
2967
2255
|
warn(`[savePlan] Ledger identity mismatch (was "${existingEvents[0].plan_id}", now "${planId}") \u2014 archived old ledger to ${archivePath} and reinitializing.`);
|
|
2968
2256
|
} catch (renameErr) {
|
|
2969
2257
|
warn(`[savePlan] Could not archive old ledger (rename failed: ${renameErr instanceof Error ? renameErr.message : String(renameErr)}). Old ledger may still exist at ${oldLedgerBackupPath}.`);
|
|
2970
2258
|
try {
|
|
2971
|
-
if (
|
|
2259
|
+
if (existsSync5(oldLedgerBackupPath))
|
|
2972
2260
|
unlinkSync3(oldLedgerBackupPath);
|
|
2973
2261
|
} catch {}
|
|
2974
2262
|
}
|
|
2975
2263
|
} else if (!initSucceeded && backupExists) {
|
|
2976
2264
|
try {
|
|
2977
|
-
if (
|
|
2265
|
+
if (existsSync5(oldLedgerBackupPath))
|
|
2978
2266
|
unlinkSync3(oldLedgerBackupPath);
|
|
2979
2267
|
} catch {}
|
|
2980
2268
|
}
|
|
2981
2269
|
const MAX_ARCHIVED_SIBLINGS = 5;
|
|
2982
2270
|
try {
|
|
2983
|
-
const allFiles =
|
|
2271
|
+
const allFiles = readdirSync(swarmDir2);
|
|
2984
2272
|
const archivedSiblings = allFiles.filter((f) => f.startsWith("plan-ledger.archived-") && f.endsWith(".jsonl")).sort();
|
|
2985
2273
|
if (archivedSiblings.length > MAX_ARCHIVED_SIBLINGS) {
|
|
2986
2274
|
const toRemove = archivedSiblings.slice(0, archivedSiblings.length - MAX_ARCHIVED_SIBLINGS);
|
|
2987
2275
|
for (const file of toRemove) {
|
|
2988
2276
|
try {
|
|
2989
|
-
unlinkSync3(
|
|
2277
|
+
unlinkSync3(path5.join(swarmDir2, file));
|
|
2990
2278
|
} catch {}
|
|
2991
2279
|
}
|
|
2992
2280
|
}
|
|
@@ -3119,19 +2407,19 @@ async function savePlan(directory, plan, options) {
|
|
|
3119
2407
|
source: "savePlan_manager"
|
|
3120
2408
|
});
|
|
3121
2409
|
}
|
|
3122
|
-
const swarmDir =
|
|
3123
|
-
const planPath =
|
|
3124
|
-
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)}`);
|
|
3125
2413
|
try {
|
|
3126
2414
|
await bunWrite(tempPath, JSON.stringify(validated, null, 2));
|
|
3127
|
-
|
|
2415
|
+
renameSync4(tempPath, planPath);
|
|
3128
2416
|
} finally {
|
|
3129
2417
|
try {
|
|
3130
2418
|
unlinkSync3(tempPath);
|
|
3131
2419
|
} catch {}
|
|
3132
2420
|
}
|
|
3133
2421
|
try {
|
|
3134
|
-
const markerPath =
|
|
2422
|
+
const markerPath = path5.join(swarmDir, ".plan-write-marker");
|
|
3135
2423
|
const inProgressMarker = JSON.stringify({
|
|
3136
2424
|
source: "plan_manager",
|
|
3137
2425
|
timestamp: new Date().toISOString(),
|
|
@@ -3146,11 +2434,11 @@ async function savePlan(directory, plan, options) {
|
|
|
3146
2434
|
const markdown = derivePlanMarkdown(validated);
|
|
3147
2435
|
const markdownWithHash = `<!-- PLAN_HASH: ${contentHash} -->
|
|
3148
2436
|
${markdown}`;
|
|
3149
|
-
const mdPath =
|
|
3150
|
-
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)}`);
|
|
3151
2439
|
try {
|
|
3152
2440
|
await bunWrite(mdTempPath, markdownWithHash);
|
|
3153
|
-
|
|
2441
|
+
renameSync4(mdTempPath, mdPath);
|
|
3154
2442
|
} finally {
|
|
3155
2443
|
try {
|
|
3156
2444
|
unlinkSync3(mdTempPath);
|
|
@@ -3168,7 +2456,7 @@ ${markdown}`;
|
|
|
3168
2456
|
} catch {}
|
|
3169
2457
|
}
|
|
3170
2458
|
try {
|
|
3171
|
-
const markerPath =
|
|
2459
|
+
const markerPath = path5.join(swarmDir, ".plan-write-marker");
|
|
3172
2460
|
const tasksCount = validated.phases.reduce((sum, phase) => sum + phase.tasks.length, 0);
|
|
3173
2461
|
const marker = JSON.stringify({
|
|
3174
2462
|
source: "plan_manager",
|
|
@@ -3184,14 +2472,14 @@ async function rebuildPlan(directory, plan, options) {
|
|
|
3184
2472
|
const targetPlan = plan ?? await replayFromLedger(directory);
|
|
3185
2473
|
if (!targetPlan)
|
|
3186
2474
|
return null;
|
|
3187
|
-
const swarmDir =
|
|
3188
|
-
const planPath =
|
|
3189
|
-
const mdPath =
|
|
3190
|
-
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)}`);
|
|
3191
2479
|
await bunWrite(tempPlanPath, JSON.stringify(targetPlan, null, 2));
|
|
3192
|
-
|
|
2480
|
+
renameSync4(tempPlanPath, planPath);
|
|
3193
2481
|
try {
|
|
3194
|
-
const markerPath =
|
|
2482
|
+
const markerPath = path5.join(swarmDir, ".plan-write-marker");
|
|
3195
2483
|
const inProgressMarker = JSON.stringify({
|
|
3196
2484
|
source: "plan_manager",
|
|
3197
2485
|
timestamp: new Date().toISOString(),
|
|
@@ -3206,12 +2494,12 @@ async function rebuildPlan(directory, plan, options) {
|
|
|
3206
2494
|
const markdown = derivePlanMarkdown(targetPlan);
|
|
3207
2495
|
const markdownWithHash = `<!-- PLAN_HASH: ${contentHash} -->
|
|
3208
2496
|
${markdown}`;
|
|
3209
|
-
const tempMdPath =
|
|
2497
|
+
const tempMdPath = path5.join(swarmDir, `plan.md.rebuild.${Date.now()}.${Math.floor(Math.random() * 1e9)}`);
|
|
3210
2498
|
await bunWrite(tempMdPath, markdownWithHash);
|
|
3211
|
-
|
|
2499
|
+
renameSync4(tempMdPath, mdPath);
|
|
3212
2500
|
} finally {
|
|
3213
2501
|
try {
|
|
3214
|
-
const markerPath =
|
|
2502
|
+
const markerPath = path5.join(swarmDir, ".plan-write-marker");
|
|
3215
2503
|
const tasksCount = targetPlan.phases.reduce((sum, phase) => sum + phase.tasks.length, 0);
|
|
3216
2504
|
const marker = JSON.stringify({
|
|
3217
2505
|
source: "plan_manager",
|
|
@@ -3274,13 +2562,13 @@ async function closePlanTerminalState(directory, plan, options) {
|
|
|
3274
2562
|
planHashAfter: hashAfter,
|
|
3275
2563
|
source: "close_terminal"
|
|
3276
2564
|
});
|
|
3277
|
-
const swarmDir =
|
|
3278
|
-
const planPath =
|
|
3279
|
-
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)}`);
|
|
3280
2568
|
await bunWrite(tempPlanPath, JSON.stringify(validated, null, 2));
|
|
3281
|
-
|
|
2569
|
+
renameSync4(tempPlanPath, planPath);
|
|
3282
2570
|
try {
|
|
3283
|
-
const markerPath =
|
|
2571
|
+
const markerPath = path5.join(swarmDir, ".plan-write-marker");
|
|
3284
2572
|
const inProgressMarker = JSON.stringify({
|
|
3285
2573
|
source: "plan_manager_close",
|
|
3286
2574
|
timestamp: new Date().toISOString(),
|
|
@@ -3291,17 +2579,17 @@ async function closePlanTerminalState(directory, plan, options) {
|
|
|
3291
2579
|
await bunWrite(markerPath, inProgressMarker);
|
|
3292
2580
|
} catch {}
|
|
3293
2581
|
try {
|
|
3294
|
-
const mdPath =
|
|
2582
|
+
const mdPath = path5.join(swarmDir, "plan.md");
|
|
3295
2583
|
const contentHash = computePlanContentHash(validated);
|
|
3296
2584
|
const markdown = derivePlanMarkdown(validated);
|
|
3297
2585
|
const markdownWithHash = `<!-- PLAN_HASH: ${contentHash} -->
|
|
3298
2586
|
${markdown}`;
|
|
3299
|
-
const mdTempPath =
|
|
2587
|
+
const mdTempPath = path5.join(swarmDir, `plan.md.close.${Date.now()}.${Math.floor(Math.random() * 1e9)}`);
|
|
3300
2588
|
await bunWrite(mdTempPath, markdownWithHash);
|
|
3301
|
-
|
|
2589
|
+
renameSync4(mdTempPath, mdPath);
|
|
3302
2590
|
} finally {
|
|
3303
2591
|
try {
|
|
3304
|
-
const markerPath =
|
|
2592
|
+
const markerPath = path5.join(swarmDir, ".plan-write-marker");
|
|
3305
2593
|
const tasksCount = validated.phases.reduce((sum, phase) => sum + phase.tasks.length, 0);
|
|
3306
2594
|
const marker = JSON.stringify({
|
|
3307
2595
|
source: "plan_manager_close",
|
|
@@ -3585,14 +2873,14 @@ function migrateLegacyPlan(planContent, swarmId) {
|
|
|
3585
2873
|
|
|
3586
2874
|
// src/evidence/manager.ts
|
|
3587
2875
|
import {
|
|
3588
|
-
mkdirSync as
|
|
3589
|
-
readdirSync as
|
|
2876
|
+
mkdirSync as mkdirSync4,
|
|
2877
|
+
readdirSync as readdirSync2,
|
|
3590
2878
|
realpathSync,
|
|
3591
2879
|
rmSync,
|
|
3592
|
-
statSync as
|
|
2880
|
+
statSync as statSync2
|
|
3593
2881
|
} from "fs";
|
|
3594
|
-
import * as
|
|
3595
|
-
import * as
|
|
2882
|
+
import * as fs5 from "fs/promises";
|
|
2883
|
+
import * as path6 from "path";
|
|
3596
2884
|
|
|
3597
2885
|
// src/config/evidence-schema.ts
|
|
3598
2886
|
var EVIDENCE_MAX_JSON_BYTES = 500 * 1024;
|
|
@@ -3924,16 +3212,16 @@ function validateProjectRoot(directory) {
|
|
|
3924
3212
|
if (depth >= MAX_DEPTH)
|
|
3925
3213
|
break;
|
|
3926
3214
|
depth++;
|
|
3927
|
-
const parent =
|
|
3215
|
+
const parent = path6.dirname(current);
|
|
3928
3216
|
if (parent === current)
|
|
3929
3217
|
break;
|
|
3930
|
-
const parentSwarm =
|
|
3218
|
+
const parentSwarm = path6.join(parent, ".swarm");
|
|
3931
3219
|
try {
|
|
3932
|
-
if (
|
|
3220
|
+
if (statSync2(parentSwarm).isDirectory()) {
|
|
3933
3221
|
let hasProjectIndicator = false;
|
|
3934
3222
|
for (const indicator of PROJECT_INDICATORS) {
|
|
3935
3223
|
try {
|
|
3936
|
-
const indicatorStat =
|
|
3224
|
+
const indicatorStat = statSync2(path6.join(parent, indicator));
|
|
3937
3225
|
if (indicatorStat.isFile() || indicatorStat.isDirectory()) {
|
|
3938
3226
|
hasProjectIndicator = true;
|
|
3939
3227
|
break;
|
|
@@ -3961,12 +3249,12 @@ function validateProjectRoot(directory) {
|
|
|
3961
3249
|
async function saveEvidence(directory, taskId, evidence) {
|
|
3962
3250
|
_internals5.validateProjectRoot(directory);
|
|
3963
3251
|
const sanitizedTaskId = sanitizeTaskId2(taskId);
|
|
3964
|
-
const relativePath =
|
|
3252
|
+
const relativePath = path6.join("evidence", sanitizedTaskId, "evidence.json");
|
|
3965
3253
|
validateSwarmPath(directory, relativePath);
|
|
3966
3254
|
_internals5.validateEvidence(evidence);
|
|
3967
3255
|
return withEvidenceLock(directory, relativePath, "evidence-manager", sanitizedTaskId, async () => {
|
|
3968
3256
|
const evidencePath = validateSwarmPath(directory, relativePath);
|
|
3969
|
-
const evidenceDir =
|
|
3257
|
+
const evidenceDir = path6.dirname(evidencePath);
|
|
3970
3258
|
let bundle;
|
|
3971
3259
|
const existingContent = await readSwarmFileAsync(directory, relativePath);
|
|
3972
3260
|
if (existingContent !== null) {
|
|
@@ -4008,11 +3296,11 @@ async function saveEvidence(directory, taskId, evidence) {
|
|
|
4008
3296
|
if (bundleJson.length > EVIDENCE_MAX_JSON_BYTES) {
|
|
4009
3297
|
throw new Error(`Evidence bundle size (${bundleJson.length} bytes) exceeds maximum (${EVIDENCE_MAX_JSON_BYTES} bytes)`);
|
|
4010
3298
|
}
|
|
4011
|
-
|
|
4012
|
-
const tempPath =
|
|
3299
|
+
mkdirSync4(evidenceDir, { recursive: true });
|
|
3300
|
+
const tempPath = path6.join(evidenceDir, `evidence.json.tmp.${Date.now()}.${process.pid}`);
|
|
4013
3301
|
try {
|
|
4014
3302
|
await bunWrite(tempPath, bundleJson);
|
|
4015
|
-
await
|
|
3303
|
+
await fs5.rename(tempPath, evidencePath);
|
|
4016
3304
|
} catch (error2) {
|
|
4017
3305
|
try {
|
|
4018
3306
|
rmSync(tempPath, { force: true });
|
|
@@ -4053,7 +3341,7 @@ function wrapFlatRetrospective(flatEntry, taskId) {
|
|
|
4053
3341
|
}
|
|
4054
3342
|
async function loadEvidence(directory, taskId) {
|
|
4055
3343
|
const sanitizedTaskId = sanitizeTaskId2(taskId);
|
|
4056
|
-
const relativePath =
|
|
3344
|
+
const relativePath = path6.join("evidence", sanitizedTaskId, "evidence.json");
|
|
4057
3345
|
if (relativePath.length > 4096) {
|
|
4058
3346
|
return { status: "not_found" };
|
|
4059
3347
|
}
|
|
@@ -4074,12 +3362,12 @@ async function loadEvidence(directory, taskId) {
|
|
|
4074
3362
|
const validated = EvidenceBundleSchema.parse(wrappedBundle);
|
|
4075
3363
|
try {
|
|
4076
3364
|
await withEvidenceLock(directory, relativePath, "evidence-loader", sanitizedTaskId, async () => {
|
|
4077
|
-
const evidenceDir =
|
|
3365
|
+
const evidenceDir = path6.dirname(evidencePath);
|
|
4078
3366
|
const bundleJson = JSON.stringify(validated);
|
|
4079
|
-
const tempPath =
|
|
3367
|
+
const tempPath = path6.join(evidenceDir, `evidence.json.tmp.${Date.now()}.${process.pid}`);
|
|
4080
3368
|
try {
|
|
4081
3369
|
await bunWrite(tempPath, bundleJson);
|
|
4082
|
-
await
|
|
3370
|
+
await fs5.rename(tempPath, evidencePath);
|
|
4083
3371
|
} catch (writeError) {
|
|
4084
3372
|
try {
|
|
4085
3373
|
rmSync(tempPath, { force: true });
|
|
@@ -4109,21 +3397,21 @@ async function loadEvidence(directory, taskId) {
|
|
|
4109
3397
|
async function listEvidenceTaskIds(directory) {
|
|
4110
3398
|
const evidenceBasePath = validateSwarmPath(directory, "evidence");
|
|
4111
3399
|
try {
|
|
4112
|
-
|
|
3400
|
+
statSync2(evidenceBasePath);
|
|
4113
3401
|
} catch {
|
|
4114
3402
|
return [];
|
|
4115
3403
|
}
|
|
4116
3404
|
let entries;
|
|
4117
3405
|
try {
|
|
4118
|
-
entries =
|
|
3406
|
+
entries = readdirSync2(evidenceBasePath);
|
|
4119
3407
|
} catch {
|
|
4120
3408
|
return [];
|
|
4121
3409
|
}
|
|
4122
3410
|
const taskIds = [];
|
|
4123
3411
|
for (const entry of entries) {
|
|
4124
|
-
const entryPath =
|
|
3412
|
+
const entryPath = path6.join(evidenceBasePath, entry);
|
|
4125
3413
|
try {
|
|
4126
|
-
const stats =
|
|
3414
|
+
const stats = statSync2(entryPath);
|
|
4127
3415
|
if (!stats.isDirectory()) {
|
|
4128
3416
|
continue;
|
|
4129
3417
|
}
|
|
@@ -4139,10 +3427,10 @@ async function listEvidenceTaskIds(directory) {
|
|
|
4139
3427
|
}
|
|
4140
3428
|
async function deleteEvidence(directory, taskId) {
|
|
4141
3429
|
const sanitizedTaskId = sanitizeTaskId2(taskId);
|
|
4142
|
-
const relativePath =
|
|
3430
|
+
const relativePath = path6.join("evidence", sanitizedTaskId);
|
|
4143
3431
|
const evidenceDir = validateSwarmPath(directory, relativePath);
|
|
4144
3432
|
try {
|
|
4145
|
-
|
|
3433
|
+
statSync2(evidenceDir);
|
|
4146
3434
|
} catch {
|
|
4147
3435
|
return false;
|
|
4148
3436
|
}
|
|
@@ -4155,10 +3443,10 @@ async function deleteEvidence(directory, taskId) {
|
|
|
4155
3443
|
}
|
|
4156
3444
|
}
|
|
4157
3445
|
async function checkRequirementCoverage(phase, directory) {
|
|
4158
|
-
const relativePath =
|
|
4159
|
-
const absolutePath =
|
|
3446
|
+
const relativePath = path6.join("evidence", `req-coverage-phase-${phase}.json`);
|
|
3447
|
+
const absolutePath = path6.resolve(directory, ".swarm", relativePath);
|
|
4160
3448
|
try {
|
|
4161
|
-
await
|
|
3449
|
+
await fs5.access(absolutePath);
|
|
4162
3450
|
return { exists: true, path: absolutePath };
|
|
4163
3451
|
} catch {
|
|
4164
3452
|
return { exists: false, path: absolutePath };
|
|
@@ -4322,4 +3610,4 @@ function mergeDurableGateEntriesFromEvidence(taskId, entries, evidence) {
|
|
|
4322
3610
|
return merged;
|
|
4323
3611
|
}
|
|
4324
3612
|
|
|
4325
|
-
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 };
|