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.
Files changed (40) hide show
  1. package/.opencode/skills/critic-gate/SKILL.md +19 -5
  2. package/.opencode/skills/plan/SKILL.md +18 -3
  3. package/.opencode/skills/resume/SKILL.md +4 -1
  4. package/.opencode/skills/writing-tests/SKILL.md +31 -0
  5. package/dist/agents/critic.d.ts +1 -1
  6. package/dist/agents/explorer.d.ts +2 -2
  7. package/dist/cli/curator-drift-6ad8mxxb.js +211 -0
  8. package/dist/cli/curator-llm-factory-gdhtm1hh.js +31 -0
  9. package/dist/cli/curator-np2ky29t.js +51 -0
  10. package/dist/cli/{evidence-summary-service-v9kmv783.js → evidence-summary-service-dh44gevz.js} +4 -3
  11. package/dist/cli/{explorer-rwfvbbx5.js → explorer-ksr3xq7n.js} +1 -1
  12. package/dist/cli/{guardrail-explain-fvjc90wa.js → guardrail-explain-414smfg4.js} +13 -11
  13. package/dist/cli/hive-promoter-y9r8d315.js +39 -0
  14. package/dist/cli/{index-ydgy7vg5.js → index-09xpycan.js} +31 -5
  15. package/dist/cli/index-357p0baj.js +719 -0
  16. package/dist/cli/{index-7dtekwvd.js → index-7v734syt.js} +1 -1
  17. package/dist/cli/{index-z4t3w4xf.js → index-87d4wsv9.js} +14 -12
  18. package/dist/cli/{index-vwyjkzgs.js → index-9wstcavp.js} +1 -1
  19. package/dist/cli/index-c8s9a3zh.js +73 -0
  20. package/dist/cli/{index-f4yjypa4.js → index-gt514nt1.js} +29028 -27099
  21. package/dist/cli/{index-k2rmw9sz.js → index-jjp5qrjv.js} +265 -843
  22. package/dist/cli/{index-e9n3xsk0.js → index-t4hbye3v.js} +1 -1
  23. package/dist/cli/index.js +12 -10
  24. package/dist/cli/{pending-delegations-dvprsdfy.js → pending-delegations-46xf2n2e.js} +2 -2
  25. package/dist/cli/{pr-subscriptions-zfxjtyzw.js → pr-subscriptions-3c34rnm1.js} +3 -3
  26. package/dist/cli/{skill-generator-99rrfwae.js → skill-generator-y6mmq8n5.js} +1 -1
  27. package/dist/commands/curate.d.ts +38 -1
  28. package/dist/commands/registry.d.ts +2 -2
  29. package/dist/commands/reset-session.d.ts +10 -0
  30. package/dist/hooks/curator-postmortem.d.ts +52 -3
  31. package/dist/hooks/curator.d.ts +2 -1
  32. package/dist/hooks/delegation-gate/worktree-isolation.d.ts +3 -1
  33. package/dist/hooks/guardrails/index.d.ts +3 -1
  34. package/dist/hooks/system-enhancer.d.ts +4 -0
  35. package/dist/index.js +458 -386
  36. package/dist/plan/manager.d.ts +15 -1
  37. package/dist/tools/update-task-status.d.ts +1 -0
  38. package/dist/utils/spec-hash.d.ts +36 -0
  39. package/package.json +1 -1
  40. 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-7ztmmqpt.js";
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 existsSync6,
40
- readdirSync as readdirSync2,
41
- renameSync as renameSync5,
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 path6 from "path";
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 fs4 from "fs";
1092
- import * as path4 from "path";
1093
-
1094
- // src/config/spec-schema.ts
1095
- var ObligationSchema = exports_external.enum(["MUST", "SHALL", "SHOULD", "MAY"]);
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 fileArtifact(root, absPath) {
1217
- try {
1218
- const stat = fs4.lstatSync(absPath);
1219
- if (!stat.isFile() || stat.size > MAX_SOURCE_BYTES)
1220
- return null;
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
- relPath: toPosix(path4.relative(root, absPath)),
1223
- bytes: stat.size,
1224
- mtimeMs: stat.mtimeMs
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
- return artifacts.sort((a, b) => a.relPath.localeCompare(b.relPath));
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
- id: entry.name,
1280
- proposal: fs4.existsSync(path4.join(root, rel, "proposal.md")),
1281
- design: fs4.existsSync(path4.join(root, rel, "design.md")),
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
- warnings.push("More than 999 FR identifiers are required; reusing FR-999.");
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 detectSpeckit(directory) {
1378
- const root = path4.resolve(directory);
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
- entries = fs4.readdirSync(specsRoot, { withFileTypes: true });
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
- stat = fs4.lstatSync(specAbs);
1125
+ recordedContent = fsSync.readFileSync(snapshotPath, "utf-8");
1403
1126
  } catch {
1404
- continue;
1127
+ return false;
1405
1128
  }
1406
- if (!stat.isFile() || stat.size > MAX_SOURCE_BYTES)
1407
- continue;
1408
- features.push({
1409
- featureId: entry.name,
1410
- specRelPath: toPosix(path4.relative(root, specAbs))
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
- let inFrSection = false;
1421
- for (const line of lines) {
1422
- if (/^##\s+/.test(line)) {
1423
- inFrSection = /^##\s+Functional Requirements\s*$/i.test(line);
1424
- continue;
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
- if (!inFrSection)
1427
- continue;
1428
- if (!/^\s*[-*]\s+/.test(line))
1429
- continue;
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
- requirements.push({
1445
- id: null,
1446
- kind: "CURRENT",
1447
- title: text,
1448
- text,
1449
- sourceRel
1450
- });
1154
+ return true;
1155
+ } catch {
1156
+ return false;
1451
1157
  }
1452
- return requirements;
1453
1158
  }
1454
- function resolveSpeckitProjection(directory, options = {}) {
1455
- const root = path4.resolve(directory);
1456
- const detection = detectSpeckit(root);
1457
- if (!detection.markerPresent) {
1458
- return { kind: "not_speckit" };
1459
- }
1460
- if (detection.features.length === 0) {
1461
- return { kind: "empty" };
1462
- }
1463
- let selectedFeature;
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
- mtimeMs = fs4.lstatSync(specAbs).mtimeMs;
1501
- } catch {}
1502
- const lines = [
1503
- "# Specification: Effective SDD Projection",
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
- return {
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
- for (const change of changes) {
1609
- const reqs = [];
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 lines = [
1629
- "# Specification: Effective SDD Projection",
1630
- "",
1631
- "Generated from OpenSpec-compatible artifacts. Update the source artifacts, then run `/swarm sdd project` to refresh this projection.",
1632
- "",
1633
- "## Source Artifacts",
1634
- ...sourcePaths.map((rel) => `- ${rel}`),
1635
- "",
1636
- "## Current Requirements"
1637
- ];
1638
- if (currentRequirements.length === 0) {
1639
- lines.push("- No current OpenSpec requirements found in source artifacts.");
1640
- warnings.push("No current requirements found; projection includes an advisory note only.");
1641
- } else {
1642
- for (const req of currentRequirements) {
1643
- lines.push(renderRequirement(req, usedIds, warnings));
1644
- }
1645
- }
1646
- for (const [changeId, reqs] of changeRequirements.entries()) {
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
- for (const req of reqs) {
1654
- lines.push(renderRequirement(req, usedIds, warnings));
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
- source: "openspec_projection",
1670
- content,
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 readEffectiveSpecSync(directory, opts) {
1678
- const root = path4.resolve(directory);
1679
- const swSpecPath = path4.join(root, SWARM_SPEC_REL);
1680
- try {
1681
- const stat = fs4.lstatSync(swSpecPath);
1682
- if (stat.isFile() && stat.size <= MAX_SPEC_BYTES) {
1683
- const content = fs4.readFileSync(swSpecPath, "utf-8");
1684
- return {
1685
- source: "swarm",
1686
- content,
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 tasksAbs = path4.join(path4.dirname(specAbs), "tasks.md");
1779
- let tasksContent = null;
1780
- try {
1781
- tasksContent = readTextBounded(tasksAbs);
1782
- } catch {}
1783
- if (tasksContent !== null) {
1784
- const tasksLines = tasksContent.replace(/\r\n/g, `
1785
- `).split(`
1786
- `);
1787
- for (const line of tasksLines) {
1788
- const taskMatch = line.match(/^\s*-\s+\[[ xX]\]\s+(T\d+)/);
1789
- if (!taskMatch)
1790
- continue;
1791
- const taskId = taskMatch[1];
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 { resolution, problems };
1800
- }
1801
-
1802
- // src/utils/spec-hash.ts
1803
- async function computeSpecHash(directory) {
1804
- const spec = _internals3.readEffectiveSpecSync(directory);
1805
- if (!spec)
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 fs5 from "fs";
1839
- import * as path5 from "path";
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 = path5.join(".swarm", LEDGER_FILENAME);
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 path5.join(directory, ".swarm", LEDGER_FILENAME);
1275
+ return path4.join(directory, ".swarm", LEDGER_FILENAME);
1860
1276
  }
1861
1277
  function getPlanJsonPath(directory) {
1862
- return path5.join(directory, ".swarm", PLAN_JSON_FILENAME);
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 = fs5.readFileSync(planPath, "utf8");
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 fs5.existsSync(ledgerPath);
1322
+ return fs4.existsSync(ledgerPath);
1907
1323
  }
1908
1324
  async function getLatestLedgerSeq(directory) {
1909
1325
  const ledgerPath = getLedgerPath(directory);
1910
- if (!fs5.existsSync(ledgerPath)) {
1326
+ if (!fs4.existsSync(ledgerPath)) {
1911
1327
  return 0;
1912
1328
  }
1913
1329
  try {
1914
- const content = fs5.readFileSync(ledgerPath, "utf8");
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 (!fs5.existsSync(ledgerPath)) {
1352
+ if (!fs4.existsSync(ledgerPath)) {
1937
1353
  return [];
1938
1354
  }
1939
1355
  try {
1940
- const content = fs5.readFileSync(ledgerPath, "utf8");
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 (fs5.existsSync(ledgerPath)) {
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 (fs5.existsSync(planJsonPath)) {
1973
- const content = fs5.readFileSync(planJsonPath, "utf8");
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
- fs5.mkdirSync(path5.join(directory, ".swarm"), { recursive: true });
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
- fs5.writeFileSync(tempPath, line, "utf8");
1998
- fs5.renameSync(tempPath, ledgerPath);
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
- fs5.mkdirSync(path5.join(directory, ".swarm"), { recursive: true });
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 (fs5.existsSync(ledgerPath)) {
2026
- const existingContent = fs5.readFileSync(ledgerPath, "utf8");
2027
- fs5.writeFileSync(tempPath, existingContent + line, "utf8");
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
- fs5.renameSync(tempPath, ledgerPath);
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 (!fs5.existsSync(planJsonPath)) {
1535
+ if (!fs4.existsSync(planJsonPath)) {
2120
1536
  return null;
2121
1537
  }
2122
1538
  let plan;
2123
1539
  try {
2124
- const content = fs5.readFileSync(planJsonPath, "utf8");
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 (!fs5.existsSync(ledgerPath)) {
1635
+ if (!fs4.existsSync(ledgerPath)) {
2220
1636
  return { events: [], truncated: false, badSuffix: null };
2221
1637
  }
2222
1638
  try {
2223
- const content = fs5.readFileSync(ledgerPath, "utf8");
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 = path5.join(directory, ".swarm", "plan-ledger.quarantine");
2253
- fs5.writeFileSync(quarantinePath, badSuffix, "utf8");
2254
- console.warn(`[ledger] Corrupted suffix quarantined to ${path5.relative(directory, quarantinePath)}`);
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((resolve4) => setTimeout(resolve4, delayMs));
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 = path6.resolve(directory);
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 = path6.resolve(directory, ".swarm", "plan.json");
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 = path6.resolve(directory, ".swarm");
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 = path6.join(swarmDir, "plan.md");
2478
- const mdTempPath = path6.join(swarmDir, `plan.md.tmp.${Date.now()}.${Math.floor(Math.random() * 1e9)}`);
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
- renameSync5(mdTempPath, mdPath);
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 = path6.resolve(directory);
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
- try {
2573
- const specStalenessPath = path6.join(directory, ".swarm", "spec-staleness.json");
2574
- await fsPromises.writeFile(specStalenessPath, JSON.stringify({
2575
- type: "spec_stale_detected",
2576
- timestamp: new Date().toISOString(),
2577
- phase: validated.current_phase ?? 1,
2578
- specHash_plan: validated.specHash,
2579
- specHash_current: staleResult.currentHash ?? null,
2580
- reason: staleResult.reason,
2581
- planTitle: validated.title
2582
- }, null, 2), "utf-8");
2583
- } catch {}
2584
- try {
2585
- const eventsPath = path6.join(directory, ".swarm", "events.jsonl");
2586
- const event = {
2587
- type: "spec_stale_detected",
2588
- timestamp: new Date().toISOString(),
2589
- phase: validated.current_phase ?? 1,
2590
- specHash_plan: validated.specHash,
2591
- specHash_current: staleResult.currentHash ?? null,
2592
- reason: staleResult.reason ?? "unknown",
2593
- planTitle: validated.title
2594
- };
2595
- await fsPromises.appendFile(eventsPath, `${JSON.stringify(event)}
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
- } catch {}
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 = path6.resolve(directory);
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 = path6.resolve(directory, ".swarm");
2791
- const oldLedgerPath = path6.join(swarmDir2, "plan-ledger.jsonl");
2792
- const oldLedgerBackupPath = path6.join(swarmDir2, `plan-ledger.backup-${Date.now()}-${Math.floor(Math.random() * 1e9)}.jsonl`);
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 (existsSync6(oldLedgerPath)) {
2216
+ if (existsSync5(oldLedgerPath)) {
2795
2217
  try {
2796
- renameSync5(oldLedgerPath, oldLedgerBackupPath);
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 (existsSync6(oldLedgerBackupPath))
2233
+ if (existsSync5(oldLedgerBackupPath))
2812
2234
  unlinkSync3(oldLedgerBackupPath);
2813
2235
  } catch {}
2814
2236
  } else {
2815
- if (existsSync6(oldLedgerBackupPath)) {
2237
+ if (existsSync5(oldLedgerBackupPath)) {
2816
2238
  try {
2817
- renameSync5(oldLedgerBackupPath, oldLedgerPath);
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 = path6.join(swarmDir2, `plan-ledger.archived-${Date.now()}-${Math.floor(Math.random() * 1e9)}.jsonl`);
2252
+ const archivePath = path5.join(swarmDir2, `plan-ledger.archived-${Date.now()}-${Math.floor(Math.random() * 1e9)}.jsonl`);
2831
2253
  try {
2832
- renameSync5(oldLedgerBackupPath, archivePath);
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 (existsSync6(oldLedgerBackupPath))
2259
+ if (existsSync5(oldLedgerBackupPath))
2838
2260
  unlinkSync3(oldLedgerBackupPath);
2839
2261
  } catch {}
2840
2262
  }
2841
2263
  } else if (!initSucceeded && backupExists) {
2842
2264
  try {
2843
- if (existsSync6(oldLedgerBackupPath))
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 = readdirSync2(swarmDir2);
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(path6.join(swarmDir2, file));
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 = path6.resolve(directory, ".swarm");
2989
- const planPath = path6.join(swarmDir, "plan.json");
2990
- const tempPath = path6.join(swarmDir, `plan.json.tmp.${Date.now()}.${Math.floor(Math.random() * 1e9)}`);
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
- renameSync5(tempPath, planPath);
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 = path6.join(swarmDir, ".plan-write-marker");
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 = path6.join(swarmDir, "plan.md");
3016
- const mdTempPath = path6.join(swarmDir, `plan.md.tmp.${Date.now()}.${Math.floor(Math.random() * 1e9)}`);
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
- renameSync5(mdTempPath, mdPath);
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 = path6.join(swarmDir, ".plan-write-marker");
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 = path6.join(directory, ".swarm");
3054
- const planPath = path6.join(swarmDir, "plan.json");
3055
- const mdPath = path6.join(swarmDir, "plan.md");
3056
- const tempPlanPath = path6.join(swarmDir, `plan.json.rebuild.${Date.now()}.${Math.floor(Math.random() * 1e9)}`);
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
- renameSync5(tempPlanPath, planPath);
2480
+ renameSync4(tempPlanPath, planPath);
3059
2481
  try {
3060
- const markerPath = path6.join(swarmDir, ".plan-write-marker");
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 = path6.join(swarmDir, `plan.md.rebuild.${Date.now()}.${Math.floor(Math.random() * 1e9)}`);
2497
+ const tempMdPath = path5.join(swarmDir, `plan.md.rebuild.${Date.now()}.${Math.floor(Math.random() * 1e9)}`);
3076
2498
  await bunWrite(tempMdPath, markdownWithHash);
3077
- renameSync5(tempMdPath, mdPath);
2499
+ renameSync4(tempMdPath, mdPath);
3078
2500
  } finally {
3079
2501
  try {
3080
- const markerPath = path6.join(swarmDir, ".plan-write-marker");
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 = path6.join(directory, ".swarm");
3144
- const planPath = path6.join(swarmDir, "plan.json");
3145
- const tempPlanPath = path6.join(swarmDir, `plan.json.close.${Date.now()}.${Math.floor(Math.random() * 1e9)}`);
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
- renameSync5(tempPlanPath, planPath);
2569
+ renameSync4(tempPlanPath, planPath);
3148
2570
  try {
3149
- const markerPath = path6.join(swarmDir, ".plan-write-marker");
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 = path6.join(swarmDir, "plan.md");
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 = path6.join(swarmDir, `plan.md.close.${Date.now()}.${Math.floor(Math.random() * 1e9)}`);
2587
+ const mdTempPath = path5.join(swarmDir, `plan.md.close.${Date.now()}.${Math.floor(Math.random() * 1e9)}`);
3166
2588
  await bunWrite(mdTempPath, markdownWithHash);
3167
- renameSync5(mdTempPath, mdPath);
2589
+ renameSync4(mdTempPath, mdPath);
3168
2590
  } finally {
3169
2591
  try {
3170
- const markerPath = path6.join(swarmDir, ".plan-write-marker");
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 mkdirSync5,
3455
- readdirSync as readdirSync3,
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 fs6 from "fs/promises";
3461
- import * as path7 from "path";
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 = path7.dirname(current);
3215
+ const parent = path6.dirname(current);
3794
3216
  if (parent === current)
3795
3217
  break;
3796
- const parentSwarm = path7.join(parent, ".swarm");
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(path7.join(parent, indicator));
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 = path7.join("evidence", sanitizedTaskId, "evidence.json");
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 = path7.dirname(evidencePath);
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
- mkdirSync5(evidenceDir, { recursive: true });
3878
- const tempPath = path7.join(evidenceDir, `evidence.json.tmp.${Date.now()}.${process.pid}`);
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 fs6.rename(tempPath, evidencePath);
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 = path7.join("evidence", sanitizedTaskId, "evidence.json");
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 = path7.dirname(evidencePath);
3365
+ const evidenceDir = path6.dirname(evidencePath);
3944
3366
  const bundleJson = JSON.stringify(validated);
3945
- const tempPath = path7.join(evidenceDir, `evidence.json.tmp.${Date.now()}.${process.pid}`);
3367
+ const tempPath = path6.join(evidenceDir, `evidence.json.tmp.${Date.now()}.${process.pid}`);
3946
3368
  try {
3947
3369
  await bunWrite(tempPath, bundleJson);
3948
- await fs6.rename(tempPath, evidencePath);
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 = readdirSync3(evidenceBasePath);
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 = path7.join(evidenceBasePath, entry);
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 = path7.join("evidence", sanitizedTaskId);
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 = path7.join("evidence", `req-coverage-phase-${phase}.json`);
4025
- const absolutePath = path7.resolve(directory, ".swarm", relativePath);
3446
+ const relativePath = path6.join("evidence", `req-coverage-phase-${phase}.json`);
3447
+ const absolutePath = path6.resolve(directory, ".swarm", relativePath);
4026
3448
  try {
4027
- await fs6.access(absolutePath);
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, detectSpeckit, resolveSpeckitProjection, loadSddStatusSync, buildOpenSpecProjectionSync, readEffectiveSpecSync, writeProjectedSpecSync, validateSpeckit, 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 };
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 };