opencode-swarm 7.82.2 → 7.84.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +3 -1
  2. package/dist/cli/capability-probe-jevmgwmf.js +18 -0
  3. package/dist/cli/config-doctor-4tcdd9vt.js +35 -0
  4. package/dist/cli/dispatch-k86d928w.js +477 -0
  5. package/dist/cli/evidence-summary-service-g2znnd33.js +320 -0
  6. package/dist/cli/explorer-gz70sm9b.js +16 -0
  7. package/dist/cli/gate-evidence-y8zn7fe2.js +29 -0
  8. package/dist/cli/guardrail-explain-tcamcdfy.js +30 -0
  9. package/dist/cli/guardrail-log-fd14n96q.js +15 -0
  10. package/dist/cli/index-293f68mj.js +13538 -0
  11. package/dist/cli/index-8ra2qpk8.js +29027 -0
  12. package/dist/cli/index-a76rekgs.js +67 -0
  13. package/dist/cli/index-a82d6d87.js +1241 -0
  14. package/dist/cli/index-b9v501fr.js +371 -0
  15. package/dist/cli/index-bcp79s17.js +1673 -0
  16. package/dist/cli/index-ckntc5gf.js +91 -0
  17. package/dist/cli/index-d9fbxaqd.js +2314 -0
  18. package/dist/cli/index-e7h9bb6v.js +233 -0
  19. package/dist/cli/index-e8pk68cc.js +540 -0
  20. package/dist/cli/index-eb85wtx9.js +242 -0
  21. package/dist/cli/index-f8r50m3h.js +14505 -0
  22. package/dist/cli/index-fjwwrwr5.js +37 -0
  23. package/dist/cli/index-hz59hg4h.js +452 -0
  24. package/dist/cli/index-j710h2ge.js +412 -0
  25. package/dist/cli/index-jfgr5gye.js +110 -0
  26. package/dist/cli/index-jtqkh8jf.js +119 -0
  27. package/dist/cli/index-p0arc26j.js +28 -0
  28. package/dist/cli/index-p0ye10nd.js +222 -0
  29. package/dist/cli/index-pv2xmc9k.js +2391 -0
  30. package/dist/cli/index-red8fm8p.js +2914 -0
  31. package/dist/cli/index-wg3r6acj.js +2042 -0
  32. package/dist/cli/index-xw0bcy0v.js +583 -0
  33. package/dist/cli/index-yhsmmv2z.js +339 -0
  34. package/dist/cli/index-yx44zd0p.js +40 -0
  35. package/dist/cli/index-zfsbaaqh.js +29 -0
  36. package/dist/cli/index.js +73 -69703
  37. package/dist/cli/knowledge-store-n4x6zyk7.js +73 -0
  38. package/dist/cli/pending-delegations-pz61mrsz.js +255 -0
  39. package/dist/cli/pr-subscriptions-y1nn36e5.js +33 -0
  40. package/dist/cli/schema-c2dbzhm8.js +168 -0
  41. package/dist/cli/skill-generator-a5ehggyg.js +55 -0
  42. package/dist/cli/task-envelope-qn0qtnh0.js +90 -0
  43. package/dist/cli/telemetry-9bbyxrvn.js +20 -0
  44. package/dist/cli/workspace-snapshot-w58jr2ga.js +90 -0
  45. package/dist/commands/guardrail-explain.d.ts +1 -0
  46. package/dist/commands/guardrail-log.d.ts +1 -0
  47. package/dist/commands/index.d.ts +2 -0
  48. package/dist/commands/registry.d.ts +14 -0
  49. package/dist/config/index.d.ts +2 -2
  50. package/dist/config/schema.d.ts +7 -0
  51. package/dist/hooks/guardrails/audit-log.d.ts +114 -0
  52. package/dist/hooks/repo-graph-builder.d.ts +4 -1
  53. package/dist/index.js +3615 -2378
  54. package/dist/services/diagnose-service.d.ts +5 -0
  55. package/dist/services/guardrail-explain-service.d.ts +42 -0
  56. package/dist/services/guardrail-log-service.d.ts +10 -0
  57. package/dist/tools/repo-graph/types.d.ts +6 -0
  58. package/package.json +2 -2
@@ -0,0 +1,2914 @@
1
+ // @bun
2
+ import {
3
+ isValidTaskId,
4
+ readTaskEvidence,
5
+ readTaskEvidenceRaw,
6
+ sanitizeTaskId
7
+ } from "./index-e7h9bb6v.js";
8
+ import {
9
+ readSwarmFileAsync,
10
+ validateSwarmPath
11
+ } from "./index-ckntc5gf.js";
12
+ import {
13
+ readCachedParsedFile
14
+ } from "./index-jtqkh8jf.js";
15
+ import {
16
+ warn
17
+ } from "./index-yx44zd0p.js";
18
+ import {
19
+ withEvidenceLock
20
+ } from "./index-bcp79s17.js";
21
+ import {
22
+ ZodError,
23
+ exports_external
24
+ } from "./index-293f68mj.js";
25
+ import {
26
+ bunHash,
27
+ bunWrite
28
+ } from "./index-b9v501fr.js";
29
+ import {
30
+ emit
31
+ } from "./index-p0ye10nd.js";
32
+
33
+ // src/plan/manager.ts
34
+ import {
35
+ copyFileSync,
36
+ existsSync as existsSync3,
37
+ readdirSync as readdirSync2,
38
+ renameSync as renameSync3,
39
+ unlinkSync
40
+ } from "fs";
41
+ import * as fsPromises from "fs/promises";
42
+ import * as path3 from "path";
43
+
44
+ // src/config/plan-schema.ts
45
+ var ExecutionProfileSchema = exports_external.object({
46
+ parallelization_enabled: exports_external.boolean().default(false),
47
+ max_concurrent_tasks: exports_external.number().int().min(1).max(64).default(10),
48
+ council_parallel: exports_external.boolean().default(true),
49
+ locked: exports_external.boolean().default(false),
50
+ auto_proceed: exports_external.boolean().default(false)
51
+ });
52
+ var TaskStatusSchema = exports_external.enum([
53
+ "pending",
54
+ "in_progress",
55
+ "completed",
56
+ "blocked",
57
+ "closed"
58
+ ]);
59
+ var TaskSizeSchema = exports_external.enum(["small", "medium", "large"]);
60
+ var PhaseStatusSchema = exports_external.enum([
61
+ "pending",
62
+ "in_progress",
63
+ "complete",
64
+ "completed",
65
+ "blocked",
66
+ "closed"
67
+ ]);
68
+ var MigrationStatusSchema = exports_external.enum([
69
+ "native",
70
+ "migrated",
71
+ "migration_failed"
72
+ ]);
73
+ var TaskSchema = exports_external.object({
74
+ id: exports_external.string(),
75
+ phase: exports_external.number().int().min(1),
76
+ status: TaskStatusSchema.default("pending"),
77
+ size: TaskSizeSchema.default("small"),
78
+ description: exports_external.string().min(1),
79
+ depends: exports_external.array(exports_external.string()).default([]),
80
+ acceptance: exports_external.string().optional(),
81
+ files_touched: exports_external.array(exports_external.string()).default([]),
82
+ evidence_path: exports_external.string().optional(),
83
+ blocked_reason: exports_external.string().optional()
84
+ });
85
+ var PhaseSchema = exports_external.object({
86
+ id: exports_external.number().int().min(1),
87
+ name: exports_external.string().min(1),
88
+ status: PhaseStatusSchema.default("pending"),
89
+ tasks: exports_external.array(TaskSchema).default([]),
90
+ type: exports_external.enum(["code", "non-code"]).optional(),
91
+ required_agents: exports_external.array(exports_external.string()).optional()
92
+ });
93
+ var PlanSchema = exports_external.object({
94
+ schema_version: exports_external.literal("1.0.0"),
95
+ title: exports_external.string().min(1),
96
+ swarm: exports_external.string().min(1),
97
+ current_phase: exports_external.number().int().min(1).optional(),
98
+ phases: exports_external.array(PhaseSchema).min(1),
99
+ migration_status: MigrationStatusSchema.optional(),
100
+ specMtime: exports_external.string().optional(),
101
+ specHash: exports_external.string().optional(),
102
+ execution_profile: ExecutionProfileSchema.optional()
103
+ });
104
+
105
+ // src/utils/spec-hash.ts
106
+ import { createHash as createHash2 } from "crypto";
107
+
108
+ // src/sdd/effective-spec.ts
109
+ import { createHash } from "crypto";
110
+ import * as fs from "fs";
111
+ import * as path from "path";
112
+
113
+ // src/config/spec-schema.ts
114
+ var ObligationSchema = exports_external.enum(["MUST", "SHALL", "SHOULD", "MAY"]);
115
+ var SpecRequirementSchema = exports_external.object({
116
+ id: exports_external.string().regex(/^FR-(?!000)\d{3}$/, "Requirement ID must match FR-### pattern (e.g., FR-001)"),
117
+ obligation: ObligationSchema,
118
+ text: exports_external.string().min(1)
119
+ });
120
+ var SpecScenarioSchema = exports_external.object({
121
+ name: exports_external.string().min(1),
122
+ given: exports_external.array(exports_external.string()).optional().default([]),
123
+ when: exports_external.array(exports_external.string()).min(1, 'Scenario must have at least one "when" clause'),
124
+ thenClauses: exports_external.array(exports_external.string()).min(1, 'Scenario must have at least one "then" clause')
125
+ });
126
+ var SpecSectionSchema = exports_external.object({
127
+ name: exports_external.string().min(1),
128
+ requirements: exports_external.array(SpecRequirementSchema).default([])
129
+ });
130
+ var SwarmSpecSchema = exports_external.object({
131
+ title: exports_external.string().min(1),
132
+ purpose: exports_external.string().min(1),
133
+ sections: exports_external.array(SpecSectionSchema).min(1, "Spec must have at least one section")
134
+ });
135
+ var SpecDeltaSchema = exports_external.object({
136
+ added: exports_external.array(SpecRequirementSchema).default([]),
137
+ modified: exports_external.array(SpecRequirementSchema).default([]),
138
+ removed: exports_external.array(SpecRequirementSchema).default([])
139
+ });
140
+ var DeltaSpecSchema = exports_external.union([
141
+ SwarmSpecSchema,
142
+ SpecDeltaSchema
143
+ ]);
144
+ var FENCED_BLOCK_PATTERN = /```[\s\S]*?```/g;
145
+ var INLINE_CODE_PATTERN = /`[^`]*`/g;
146
+ var FR_ID_PATTERN = /\bFR-\d{3}\b/g;
147
+ var OBLIGATION_PATTERN = /\b(MUST|SHALL|SHOULD|MAY)\b/g;
148
+ var SECTION_HEADER_PATTERN = /^##\s+.+$/gm;
149
+ function stripCodeBlocks(content) {
150
+ return content.replace(FENCED_BLOCK_PATTERN, "").replace(INLINE_CODE_PATTERN, "");
151
+ }
152
+ function getLineNumber(content, position) {
153
+ const prefix = content.substring(0, position);
154
+ return (prefix.match(/\n/g) || []).length + 1;
155
+ }
156
+ function validateSpecContent(content) {
157
+ const issues = [];
158
+ if (!content || content.trim().length === 0) {
159
+ return { valid: false, issues: [{ line: 1, message: "Content is empty" }] };
160
+ }
161
+ const strippedContent = stripCodeBlocks(content);
162
+ const frMatches = strippedContent.match(FR_ID_PATTERN);
163
+ if (!frMatches || frMatches.length === 0) {
164
+ issues.push({
165
+ line: 1,
166
+ message: "No FR-### requirement IDs found in spec content"
167
+ });
168
+ }
169
+ const obligationMatches = strippedContent.match(OBLIGATION_PATTERN);
170
+ if (!obligationMatches || obligationMatches.length === 0) {
171
+ issues.push({
172
+ line: 1,
173
+ message: "No obligation keywords (MUST, SHALL, SHOULD, MAY) found in spec content"
174
+ });
175
+ }
176
+ const sectionMatches = strippedContent.match(SECTION_HEADER_PATTERN);
177
+ if (!sectionMatches || sectionMatches.length === 0) {
178
+ issues.push({
179
+ line: 1,
180
+ message: "No section headers (## Section Name) found in spec content"
181
+ });
182
+ }
183
+ const idMatches = strippedContent.matchAll(/\bFR-(\d{3})\b/g);
184
+ for (const idMatch of idMatches) {
185
+ const num = parseInt(idMatch[1], 10);
186
+ if (num === 0) {
187
+ const pos = idMatch.index;
188
+ issues.push({
189
+ line: getLineNumber(strippedContent, pos),
190
+ message: `Invalid FR-ID "${idMatch[0]}" \u2014 number must be 001-999`
191
+ });
192
+ }
193
+ }
194
+ return {
195
+ valid: issues.length === 0,
196
+ issues
197
+ };
198
+ }
199
+
200
+ // src/sdd/effective-spec.ts
201
+ var SWARM_SPEC_REL = path.join(".swarm", "spec.md");
202
+ var OPENSPEC_ROOT = "openspec";
203
+ var MAX_SPEC_BYTES = 256 * 1024;
204
+ var MAX_SOURCE_BYTES = 512 * 1024;
205
+ var MAX_SPEC_FILES = 100;
206
+ var MAX_WALK_DEPTH = 10;
207
+ function toPosix(relPath) {
208
+ return relPath.split(path.sep).join("/");
209
+ }
210
+ function hash(content) {
211
+ return createHash("sha256").update(content, "utf-8").digest("hex");
212
+ }
213
+ function readTextBounded(absPath) {
214
+ const stat = fs.lstatSync(absPath);
215
+ if (!stat.isFile() || stat.size > MAX_SOURCE_BYTES) {
216
+ return null;
217
+ }
218
+ return fs.readFileSync(absPath, "utf-8");
219
+ }
220
+ function fileArtifact(root, absPath) {
221
+ try {
222
+ const stat = fs.lstatSync(absPath);
223
+ if (!stat.isFile() || stat.size > MAX_SOURCE_BYTES)
224
+ return null;
225
+ return {
226
+ relPath: toPosix(path.relative(root, absPath)),
227
+ bytes: stat.size,
228
+ mtimeMs: stat.mtimeMs
229
+ };
230
+ } catch {
231
+ return null;
232
+ }
233
+ }
234
+ function walkSpecFiles(root, startRel) {
235
+ const start = path.join(root, startRel);
236
+ if (!fs.existsSync(start))
237
+ return [];
238
+ const artifacts = [];
239
+ const stack = [
240
+ { abs: start, depth: 0 }
241
+ ];
242
+ while (stack.length > 0 && artifacts.length < MAX_SPEC_FILES) {
243
+ const item = stack.pop();
244
+ if (!item || item.depth > MAX_WALK_DEPTH)
245
+ continue;
246
+ let entries;
247
+ try {
248
+ entries = fs.readdirSync(item.abs, { withFileTypes: true });
249
+ } catch {
250
+ continue;
251
+ }
252
+ const dirents = entries.filter((entry) => typeof entry?.name === "string");
253
+ for (const entry of dirents.sort((a, b) => b.name.localeCompare(a.name))) {
254
+ const abs = path.join(item.abs, entry.name);
255
+ if (entry.isSymbolicLink())
256
+ continue;
257
+ if (entry.isDirectory()) {
258
+ stack.push({ abs, depth: item.depth + 1 });
259
+ } else if (entry.isFile() && entry.name === "spec.md") {
260
+ const artifact = fileArtifact(root, abs);
261
+ if (artifact)
262
+ artifacts.push(artifact);
263
+ if (artifacts.length >= MAX_SPEC_FILES)
264
+ break;
265
+ }
266
+ }
267
+ }
268
+ return artifacts.sort((a, b) => a.relPath.localeCompare(b.relPath));
269
+ }
270
+ function listOpenSpecChanges(root) {
271
+ const changesDir = path.join(root, OPENSPEC_ROOT, "changes");
272
+ if (!fs.existsSync(changesDir))
273
+ return [];
274
+ let entries;
275
+ try {
276
+ entries = fs.readdirSync(changesDir, { withFileTypes: true });
277
+ } catch {
278
+ return [];
279
+ }
280
+ return entries.filter((entry) => entry.isDirectory() && entry.name !== "archive").sort((a, b) => a.name.localeCompare(b.name)).map((entry) => {
281
+ const rel = path.join(OPENSPEC_ROOT, "changes", entry.name);
282
+ return {
283
+ id: entry.name,
284
+ proposal: fs.existsSync(path.join(root, rel, "proposal.md")),
285
+ design: fs.existsSync(path.join(root, rel, "design.md")),
286
+ tasks: fs.existsSync(path.join(root, rel, "tasks.md")),
287
+ specs: walkSpecFiles(root, path.join(rel, "specs"))
288
+ };
289
+ });
290
+ }
291
+ function detectKind(line, current) {
292
+ const upper = line.toUpperCase();
293
+ if (/^#{2,6}\s+ADDED\b/.test(upper))
294
+ return "ADDED";
295
+ if (/^#{2,6}\s+MODIFIED\b/.test(upper))
296
+ return "MODIFIED";
297
+ if (/^#{2,6}\s+REMOVED\b/.test(upper))
298
+ return "REMOVED";
299
+ return current;
300
+ }
301
+ function requirementTextFromBlock(title, block, kind) {
302
+ const joined = block.join(" ").replace(/\s+/g, " ").trim();
303
+ const obligation = joined.match(/\b(MUST|SHALL|SHOULD|MAY)\b/i)?.[1];
304
+ if (obligation)
305
+ return joined;
306
+ if (kind === "REMOVED") {
307
+ return `MAY remove or retire behavior for ${title}.`;
308
+ }
309
+ return `MUST satisfy OpenSpec requirement ${title}.`;
310
+ }
311
+ function parseRequirements(content, sourceRel, defaultKind) {
312
+ const requirements = [];
313
+ const lines = content.replace(/\r\n/g, `
314
+ `).split(`
315
+ `);
316
+ let kind = defaultKind;
317
+ for (let i = 0;i < lines.length; i++) {
318
+ const line = lines[i];
319
+ kind = detectKind(line, kind);
320
+ const explicit = line.match(/\b(FR-(?!000)\d{3})\b/);
321
+ if (explicit && /\b(MUST|SHALL|SHOULD|MAY)\b/i.test(line)) {
322
+ requirements.push({
323
+ id: explicit[1].toUpperCase(),
324
+ kind,
325
+ title: explicit[1].toUpperCase(),
326
+ text: line.trim().replace(/^\s*[-*]\s*/, ""),
327
+ sourceRel
328
+ });
329
+ continue;
330
+ }
331
+ const openSpecReq = line.match(/^#{3,4}\s+Requirement:\s*(.+)$/i);
332
+ if (!openSpecReq)
333
+ continue;
334
+ const title = openSpecReq[1].trim();
335
+ const block = [];
336
+ for (let j = i + 1;j < lines.length; j++) {
337
+ if (/^##\s+/.test(lines[j]))
338
+ break;
339
+ if (/^#{3,4}\s+Requirement:/i.test(lines[j]))
340
+ break;
341
+ const trimmed = lines[j].trim();
342
+ if (trimmed)
343
+ block.push(trimmed);
344
+ }
345
+ const id = block.join(`
346
+ `).match(/\b(FR-(?!000)\d{3})\b/)?.[1] ?? null;
347
+ requirements.push({
348
+ id: id?.toUpperCase() ?? null,
349
+ kind,
350
+ title,
351
+ text: requirementTextFromBlock(title, block, kind),
352
+ sourceRel
353
+ });
354
+ }
355
+ return requirements;
356
+ }
357
+ function nextFrId(used, warnings) {
358
+ for (let n = 1;n <= 999; n++) {
359
+ const id = `FR-${String(n).padStart(3, "0")}`;
360
+ if (!used.has(id)) {
361
+ used.add(id);
362
+ return id;
363
+ }
364
+ }
365
+ warnings.push("More than 999 FR identifiers are required; reusing FR-999.");
366
+ return "FR-999";
367
+ }
368
+ function renderRequirement(req, used, warnings) {
369
+ const id = req.id && !used.has(req.id) ? req.id : nextFrId(used, warnings);
370
+ if (req.id && req.id !== id) {
371
+ warnings.push(`Duplicate requirement id ${req.id} in ${req.sourceRel}; generated ${id}.`);
372
+ }
373
+ if (req.id === id)
374
+ used.add(id);
375
+ let text = req.text;
376
+ if (!text.includes(id)) {
377
+ text = `${id}: ${text}`;
378
+ }
379
+ return `- ${text} _(source: ${req.sourceRel})_`;
380
+ }
381
+ function loadSddStatusSync(directory) {
382
+ const root = path.resolve(directory);
383
+ const swSpecPath = path.join(root, SWARM_SPEC_REL);
384
+ const openSpecPath = path.join(root, OPENSPEC_ROOT);
385
+ const errors = [];
386
+ const warnings = [];
387
+ const swSpecExists = fs.existsSync(swSpecPath);
388
+ const openSpecExists = fs.existsSync(openSpecPath);
389
+ const currentSpecs = walkSpecFiles(root, path.join(OPENSPEC_ROOT, "specs"));
390
+ const changes = listOpenSpecChanges(root);
391
+ const effectiveSpec = readEffectiveSpecSync(root);
392
+ if (openSpecExists && currentSpecs.length === 0 && changes.length === 0) {
393
+ errors.push("openspec/ exists but contains no specs or active changes.");
394
+ }
395
+ for (const change of changes) {
396
+ if (!change.proposal) {
397
+ warnings.push(`Change ${change.id} is missing proposal.md.`);
398
+ }
399
+ if (!change.tasks) {
400
+ warnings.push(`Change ${change.id} is missing tasks.md; tasks remain proposal input, not plan state.`);
401
+ }
402
+ if (change.specs.length === 0) {
403
+ errors.push(`Change ${change.id} has no specs/**/spec.md delta files.`);
404
+ }
405
+ }
406
+ return {
407
+ provider: effectiveSpec?.source ?? "none",
408
+ swSpecExists,
409
+ openSpecExists,
410
+ currentSpecs,
411
+ changes,
412
+ effectiveSpec,
413
+ errors,
414
+ warnings
415
+ };
416
+ }
417
+ function buildOpenSpecProjectionSync(directory, options = {}) {
418
+ const root = path.resolve(directory);
419
+ const currentSpecs = walkSpecFiles(root, path.join(OPENSPEC_ROOT, "specs"));
420
+ const allChanges = listOpenSpecChanges(root);
421
+ const changes = options.changeId ? allChanges.filter((change) => change.id === options.changeId) : allChanges;
422
+ const sourcePaths = [];
423
+ const warnings = [];
424
+ const usedIds = new Set;
425
+ const currentRequirements = [];
426
+ const changeRequirements = new Map;
427
+ let parsedRequirementCount = 0;
428
+ let mtimeMs = 0;
429
+ if (options.changeId && changes.length === 0)
430
+ return null;
431
+ if (currentSpecs.length === 0 && changes.length === 0)
432
+ return null;
433
+ for (const artifact of currentSpecs) {
434
+ const abs = path.join(root, artifact.relPath);
435
+ const content2 = readTextBounded(abs);
436
+ if (content2 === null) {
437
+ warnings.push(`Skipped unreadable or oversized spec ${artifact.relPath}.`);
438
+ continue;
439
+ }
440
+ sourcePaths.push(artifact.relPath);
441
+ mtimeMs = Math.max(mtimeMs, artifact.mtimeMs);
442
+ const parsed = parseRequirements(content2, artifact.relPath, "CURRENT");
443
+ currentRequirements.push(...parsed);
444
+ parsedRequirementCount += parsed.length;
445
+ }
446
+ for (const change of changes) {
447
+ const reqs = [];
448
+ for (const artifact of change.specs) {
449
+ const abs = path.join(root, artifact.relPath);
450
+ const content2 = readTextBounded(abs);
451
+ if (content2 === null) {
452
+ warnings.push(`Skipped unreadable or oversized spec ${artifact.relPath}.`);
453
+ continue;
454
+ }
455
+ sourcePaths.push(artifact.relPath);
456
+ mtimeMs = Math.max(mtimeMs, artifact.mtimeMs);
457
+ const parsed = parseRequirements(content2, artifact.relPath, "ADDED");
458
+ reqs.push(...parsed);
459
+ parsedRequirementCount += parsed.length;
460
+ }
461
+ changeRequirements.set(change.id, reqs);
462
+ }
463
+ if (sourcePaths.length > 0 && parsedRequirementCount === 0) {
464
+ return null;
465
+ }
466
+ const lines = [
467
+ "# Specification: Effective SDD Projection",
468
+ "",
469
+ "Generated from OpenSpec-compatible artifacts. Update the source artifacts, then run `/swarm sdd project` to refresh this projection.",
470
+ "",
471
+ "## Source Artifacts",
472
+ ...sourcePaths.map((rel) => `- ${rel}`),
473
+ "",
474
+ "## Current Requirements"
475
+ ];
476
+ if (currentRequirements.length === 0) {
477
+ lines.push("- No current OpenSpec requirements found in source artifacts.");
478
+ warnings.push("No current requirements found; projection includes an advisory note only.");
479
+ } else {
480
+ for (const req of currentRequirements) {
481
+ lines.push(renderRequirement(req, usedIds, warnings));
482
+ }
483
+ }
484
+ for (const [changeId, reqs] of changeRequirements.entries()) {
485
+ lines.push("", `## Pending Change: ${changeId}`);
486
+ if (reqs.length === 0) {
487
+ lines.push(`- ${nextFrId(usedIds, warnings)} SHOULD add OpenSpec delta requirements for change ${changeId}.`);
488
+ warnings.push(`Change ${changeId} contains no parsable requirements.`);
489
+ continue;
490
+ }
491
+ for (const req of reqs) {
492
+ lines.push(renderRequirement(req, usedIds, warnings));
493
+ }
494
+ }
495
+ const content = `${lines.join(`
496
+ `)}
497
+ `;
498
+ if (content.length > MAX_SPEC_BYTES) {
499
+ warnings.push(`Projected spec exceeds ${MAX_SPEC_BYTES} bytes; refusing to use projection.`);
500
+ return null;
501
+ }
502
+ const validation = validateSpecContent(content);
503
+ if (!validation.valid) {
504
+ warnings.push(...validation.issues.map((issue) => `Projection line ${issue.line}: ${issue.message}`));
505
+ }
506
+ return {
507
+ source: "openspec_projection",
508
+ content,
509
+ hash: hash(content),
510
+ mtime: mtimeMs > 0 ? new Date(mtimeMs).toISOString() : null,
511
+ sourcePaths,
512
+ warnings
513
+ };
514
+ }
515
+ function readEffectiveSpecSync(directory) {
516
+ const root = path.resolve(directory);
517
+ const swSpecPath = path.join(root, SWARM_SPEC_REL);
518
+ try {
519
+ const stat = fs.lstatSync(swSpecPath);
520
+ if (stat.isFile() && stat.size <= MAX_SPEC_BYTES) {
521
+ const content = fs.readFileSync(swSpecPath, "utf-8");
522
+ return {
523
+ source: "swarm",
524
+ content,
525
+ hash: hash(content),
526
+ mtime: stat.mtime.toISOString(),
527
+ sourcePaths: [toPosix(SWARM_SPEC_REL)],
528
+ warnings: []
529
+ };
530
+ }
531
+ } catch (error) {
532
+ if (error.code !== "ENOENT") {
533
+ throw error;
534
+ }
535
+ }
536
+ return buildOpenSpecProjectionSync(root);
537
+ }
538
+ function writeProjectedSpecSync(directory, options = {}) {
539
+ const root = path.resolve(directory);
540
+ const projection = buildOpenSpecProjectionSync(root, {
541
+ changeId: options.changeId
542
+ });
543
+ const target = path.join(root, SWARM_SPEC_REL);
544
+ if (!projection || options.dryRun) {
545
+ return { written: false, projection, path: target };
546
+ }
547
+ fs.mkdirSync(path.dirname(target), { recursive: true });
548
+ let archivePath;
549
+ if (fs.existsSync(target)) {
550
+ const prior = fs.readFileSync(target, "utf-8");
551
+ if (prior !== projection.content) {
552
+ const archiveDir = path.join(root, ".swarm", "spec-archive");
553
+ fs.mkdirSync(archiveDir, { recursive: true });
554
+ const stamp = new Date().toISOString().replace(/[:.]/g, "-");
555
+ archivePath = path.join(archiveDir, `sdd-projection-${stamp}.md`);
556
+ fs.writeFileSync(archivePath, prior, "utf-8");
557
+ }
558
+ }
559
+ const tmp = `${target}.tmp-${process.pid}-${Date.now()}`;
560
+ fs.writeFileSync(tmp, projection.content, "utf-8");
561
+ fs.renameSync(tmp, target);
562
+ return { written: true, projection, archivePath, path: target };
563
+ }
564
+
565
+ // src/utils/spec-hash.ts
566
+ async function computeSpecHash(directory) {
567
+ const spec = _internals.readEffectiveSpecSync(directory);
568
+ if (!spec)
569
+ return null;
570
+ return createHash2("sha256").update(spec.content, "utf-8").digest("hex");
571
+ }
572
+ async function isSpecStale(directory, plan) {
573
+ const currentHash = await _internals.computeSpecHash(directory);
574
+ if (!plan.specHash) {
575
+ return { stale: false };
576
+ }
577
+ if (currentHash === null) {
578
+ return {
579
+ stale: true,
580
+ reason: "effective spec has been deleted",
581
+ currentHash: null
582
+ };
583
+ }
584
+ if (currentHash !== plan.specHash) {
585
+ return {
586
+ stale: true,
587
+ reason: "effective spec has been modified since plan was saved",
588
+ currentHash
589
+ };
590
+ }
591
+ return { stale: false };
592
+ }
593
+ var _internals = {
594
+ computeSpecHash,
595
+ isSpecStale,
596
+ readEffectiveSpecSync
597
+ };
598
+
599
+ // src/plan/ledger.ts
600
+ import * as crypto from "crypto";
601
+ import * as fs2 from "fs";
602
+ import * as path2 from "path";
603
+
604
+ // src/plan/utils.ts
605
+ function derivePlanId(plan) {
606
+ return `${plan.swarm}-${plan.title}`.replace(/[^a-zA-Z0-9-_]/g, "_");
607
+ }
608
+
609
+ // src/plan/ledger.ts
610
+ var LEDGER_SCHEMA_VERSION = "1.1.0";
611
+ var LEDGER_FILENAME = "plan-ledger.jsonl";
612
+ var PLAN_JSON_FILENAME = "plan.json";
613
+
614
+ class LedgerStaleWriterError extends Error {
615
+ constructor(message) {
616
+ super(message);
617
+ this.name = "LedgerStaleWriterError";
618
+ }
619
+ }
620
+ function getLedgerPath(directory) {
621
+ return path2.join(directory, ".swarm", LEDGER_FILENAME);
622
+ }
623
+ function getPlanJsonPath(directory) {
624
+ return path2.join(directory, ".swarm", PLAN_JSON_FILENAME);
625
+ }
626
+ function computePlanHash(plan) {
627
+ const normalized = {
628
+ schema_version: plan.schema_version,
629
+ title: plan.title,
630
+ swarm: plan.swarm,
631
+ current_phase: plan.current_phase,
632
+ migration_status: plan.migration_status,
633
+ execution_profile: plan.execution_profile,
634
+ phases: plan.phases.map((phase) => ({
635
+ id: phase.id,
636
+ name: phase.name,
637
+ status: phase.status,
638
+ required_agents: phase.required_agents ? [...phase.required_agents].sort() : undefined,
639
+ tasks: phase.tasks.map((task) => ({
640
+ id: task.id,
641
+ phase: task.phase,
642
+ status: task.status,
643
+ size: task.size,
644
+ description: task.description,
645
+ depends: [...task.depends].sort(),
646
+ acceptance: task.acceptance,
647
+ files_touched: [...task.files_touched].sort(),
648
+ evidence_path: task.evidence_path,
649
+ blocked_reason: task.blocked_reason
650
+ }))
651
+ }))
652
+ };
653
+ const jsonString = JSON.stringify(normalized);
654
+ return crypto.createHash("sha256").update(jsonString, "utf8").digest("hex");
655
+ }
656
+ function computeCurrentPlanHash(directory) {
657
+ const planPath = getPlanJsonPath(directory);
658
+ try {
659
+ const content = fs2.readFileSync(planPath, "utf8");
660
+ const plan = JSON.parse(content);
661
+ return computePlanHash(plan);
662
+ } catch {
663
+ return "";
664
+ }
665
+ }
666
+ async function ledgerExists(directory) {
667
+ const ledgerPath = getLedgerPath(directory);
668
+ return fs2.existsSync(ledgerPath);
669
+ }
670
+ async function getLatestLedgerSeq(directory) {
671
+ const ledgerPath = getLedgerPath(directory);
672
+ if (!fs2.existsSync(ledgerPath)) {
673
+ return 0;
674
+ }
675
+ try {
676
+ const content = fs2.readFileSync(ledgerPath, "utf8");
677
+ const lines = content.trim().split(`
678
+ `).filter((line) => line.trim() !== "");
679
+ if (lines.length === 0) {
680
+ return 0;
681
+ }
682
+ let maxSeq = 0;
683
+ for (const line of lines) {
684
+ try {
685
+ const event = JSON.parse(line);
686
+ if (event.seq > maxSeq) {
687
+ maxSeq = event.seq;
688
+ }
689
+ } catch {}
690
+ }
691
+ return maxSeq;
692
+ } catch {
693
+ return 0;
694
+ }
695
+ }
696
+ async function readLedgerEvents(directory) {
697
+ const ledgerPath = getLedgerPath(directory);
698
+ if (!fs2.existsSync(ledgerPath)) {
699
+ return [];
700
+ }
701
+ try {
702
+ const content = fs2.readFileSync(ledgerPath, "utf8");
703
+ const lines = content.trim().split(`
704
+ `).filter((line) => line.trim() !== "");
705
+ const events = [];
706
+ let skippedCount = 0;
707
+ for (const line of lines) {
708
+ try {
709
+ const event = JSON.parse(line);
710
+ events.push(event);
711
+ } catch {
712
+ skippedCount++;
713
+ }
714
+ }
715
+ if (skippedCount > 0) {
716
+ console.warn(`[ledger] Skipped ${skippedCount} malformed line(s) in plan-ledger.jsonl`);
717
+ }
718
+ events.sort((a, b) => a.seq - b.seq);
719
+ return events;
720
+ } catch {
721
+ return [];
722
+ }
723
+ }
724
+ async function initLedger(directory, planId, initialPlanHash, initialPlan) {
725
+ const ledgerPath = getLedgerPath(directory);
726
+ const planJsonPath = getPlanJsonPath(directory);
727
+ if (fs2.existsSync(ledgerPath)) {
728
+ throw new Error("Ledger already initialized. Use appendLedgerEvent to add events.");
729
+ }
730
+ let planHashAfter = initialPlanHash ?? "";
731
+ let embeddedPlan = initialPlan;
732
+ if (!initialPlanHash) {
733
+ try {
734
+ if (fs2.existsSync(planJsonPath)) {
735
+ const content = fs2.readFileSync(planJsonPath, "utf8");
736
+ const plan = JSON.parse(content);
737
+ planHashAfter = computePlanHash(plan);
738
+ if (!embeddedPlan)
739
+ embeddedPlan = plan;
740
+ }
741
+ } catch {}
742
+ }
743
+ const payload = embeddedPlan ? { plan: embeddedPlan, payload_hash: planHashAfter } : undefined;
744
+ const event = {
745
+ seq: 1,
746
+ timestamp: new Date().toISOString(),
747
+ plan_id: planId,
748
+ event_type: "plan_created",
749
+ source: "initLedger",
750
+ plan_hash_before: "",
751
+ plan_hash_after: planHashAfter,
752
+ schema_version: LEDGER_SCHEMA_VERSION,
753
+ ...payload ? { payload } : {}
754
+ };
755
+ fs2.mkdirSync(path2.join(directory, ".swarm"), { recursive: true });
756
+ const tempPath = `${ledgerPath}.tmp.${Date.now()}.${Math.floor(Math.random() * 1e9)}`;
757
+ const line = `${JSON.stringify(event)}
758
+ `;
759
+ fs2.writeFileSync(tempPath, line, "utf8");
760
+ fs2.renameSync(tempPath, ledgerPath);
761
+ }
762
+ async function appendLedgerEvent(directory, eventInput, options) {
763
+ const ledgerPath = getLedgerPath(directory);
764
+ const latestSeq = await getLatestLedgerSeq(directory);
765
+ const nextSeq = latestSeq + 1;
766
+ const planHashBefore = computeCurrentPlanHash(directory);
767
+ if (options?.expectedSeq !== undefined && options.expectedSeq !== latestSeq) {
768
+ throw new LedgerStaleWriterError(`Stale writer: expected seq ${options.expectedSeq} but found ${latestSeq}`);
769
+ }
770
+ if (options?.expectedHash !== undefined && options.expectedHash !== planHashBefore) {
771
+ throw new LedgerStaleWriterError(`Stale writer: expected hash ${options.expectedHash} but found ${planHashBefore}`);
772
+ }
773
+ const planHashAfter = options?.planHashAfter ?? planHashBefore;
774
+ const event = {
775
+ ...eventInput,
776
+ seq: nextSeq,
777
+ timestamp: new Date().toISOString(),
778
+ plan_hash_before: planHashBefore,
779
+ plan_hash_after: planHashAfter,
780
+ schema_version: LEDGER_SCHEMA_VERSION
781
+ };
782
+ fs2.mkdirSync(path2.join(directory, ".swarm"), { recursive: true });
783
+ const tempPath = `${ledgerPath}.tmp.${Date.now()}.${Math.floor(Math.random() * 1e9)}`;
784
+ const line = `${JSON.stringify(event)}
785
+ `;
786
+ if (fs2.existsSync(ledgerPath)) {
787
+ const existingContent = fs2.readFileSync(ledgerPath, "utf8");
788
+ fs2.writeFileSync(tempPath, existingContent + line, "utf8");
789
+ } else {
790
+ throw new Error("Ledger not initialized. Call initLedger() first.");
791
+ }
792
+ fs2.renameSync(tempPath, ledgerPath);
793
+ return event;
794
+ }
795
+ async function takeSnapshotWithRetry(directory, plan, options) {
796
+ const MAX_RETRIES = 3;
797
+ const TOTAL_ATTEMPTS = 1 + MAX_RETRIES;
798
+ const telemetrySource = options?.source ?? "save_plan_tool";
799
+ const snapshotOptions = { planHashAfter: options?.planHashAfter };
800
+ let lastError;
801
+ for (let attempt = 1;attempt <= TOTAL_ATTEMPTS; attempt++) {
802
+ try {
803
+ await takeSnapshotEvent(directory, plan, snapshotOptions);
804
+ return;
805
+ } catch (err) {
806
+ lastError = err instanceof Error ? err : new Error(String(err));
807
+ if (attempt < TOTAL_ATTEMPTS) {
808
+ await new Promise((r) => setTimeout(r, 10 * 2 ** (attempt - 1)));
809
+ }
810
+ }
811
+ }
812
+ console.warn(`[takeSnapshotWithRetry] Snapshot failed after ${MAX_RETRIES} retries (${TOTAL_ATTEMPTS} attempts): ${lastError.message}`);
813
+ try {
814
+ emit("snapshot_failed", {
815
+ error: lastError.message,
816
+ retries: MAX_RETRIES,
817
+ source: telemetrySource
818
+ });
819
+ } catch {}
820
+ }
821
+ async function takeSnapshotEvent(directory, plan, options) {
822
+ const payloadHash = computePlanHash(plan);
823
+ const snapshotPayload = {
824
+ plan,
825
+ payload_hash: payloadHash
826
+ };
827
+ if (options?.approvalMetadata) {
828
+ snapshotPayload.approval = options.approvalMetadata;
829
+ }
830
+ const planId = derivePlanId(plan);
831
+ return appendLedgerEvent(directory, {
832
+ event_type: "snapshot",
833
+ source: options?.source ?? "takeSnapshotEvent",
834
+ plan_id: planId,
835
+ payload: snapshotPayload
836
+ }, { planHashAfter: options?.planHashAfter });
837
+ }
838
+ async function replayFromLedger(directory, _options) {
839
+ const { events, truncated, badSuffix } = await readLedgerEventsWithIntegrity(directory);
840
+ if (events.length === 0) {
841
+ return null;
842
+ }
843
+ if (truncated && badSuffix !== null) {
844
+ await quarantineLedgerSuffix(directory, badSuffix);
845
+ }
846
+ const targetPlanId = events[0].plan_id;
847
+ const relevantEvents = events.filter((e) => e.plan_id === targetPlanId);
848
+ {
849
+ const snapshotEvents = relevantEvents.filter((e) => e.event_type === "snapshot");
850
+ if (snapshotEvents.length > 0) {
851
+ const latestSnapshotEvent = snapshotEvents[snapshotEvents.length - 1];
852
+ const snapshotPayload = latestSnapshotEvent.payload;
853
+ let plan2 = snapshotPayload.plan;
854
+ const eventsAfterSnapshot = relevantEvents.filter((e) => e.seq > latestSnapshotEvent.seq);
855
+ for (const event of eventsAfterSnapshot) {
856
+ plan2 = applyEventToPlan(plan2, event);
857
+ if (plan2 === null) {
858
+ return null;
859
+ }
860
+ }
861
+ return plan2;
862
+ }
863
+ }
864
+ const createdEvent = relevantEvents.find((e) => e.event_type === "plan_created");
865
+ if (createdEvent?.payload && typeof createdEvent.payload === "object" && "plan" in createdEvent.payload) {
866
+ const parseResult = PlanSchema.safeParse(createdEvent.payload.plan);
867
+ if (parseResult.success) {
868
+ let plan2 = parseResult.data;
869
+ const eventsAfterCreated = relevantEvents.filter((e) => e.seq > createdEvent.seq);
870
+ for (const event of eventsAfterCreated) {
871
+ if (plan2 === null)
872
+ return null;
873
+ plan2 = applyEventToPlan(plan2, event);
874
+ }
875
+ return plan2;
876
+ }
877
+ }
878
+ const planJsonPath = getPlanJsonPath(directory);
879
+ if (!fs2.existsSync(planJsonPath)) {
880
+ return null;
881
+ }
882
+ let plan;
883
+ try {
884
+ const content = fs2.readFileSync(planJsonPath, "utf8");
885
+ plan = JSON.parse(content);
886
+ } catch {
887
+ return null;
888
+ }
889
+ for (const event of relevantEvents) {
890
+ if (plan === null) {
891
+ return null;
892
+ }
893
+ plan = applyEventToPlan(plan, event);
894
+ }
895
+ return plan;
896
+ }
897
+ function applyEventToPlan(plan, event) {
898
+ switch (event.event_type) {
899
+ case "plan_created":
900
+ if (event.payload && typeof event.payload === "object" && "plan" in event.payload) {
901
+ const parsed = PlanSchema.safeParse(event.payload.plan);
902
+ if (parsed.success)
903
+ return parsed.data;
904
+ }
905
+ return plan;
906
+ case "task_status_changed":
907
+ if (event.task_id && event.to_status) {
908
+ const parseResult = TaskStatusSchema.safeParse(event.to_status);
909
+ if (!parseResult.success) {
910
+ return plan;
911
+ }
912
+ for (const phase of plan.phases) {
913
+ const task = phase.tasks.find((t) => t.id === event.task_id);
914
+ if (task) {
915
+ task.status = parseResult.data;
916
+ break;
917
+ }
918
+ }
919
+ }
920
+ return plan;
921
+ case "phase_completed":
922
+ if (event.phase_id) {
923
+ const phase = plan.phases.find((p) => p.id === event.phase_id);
924
+ if (phase) {
925
+ phase.status = "complete";
926
+ }
927
+ }
928
+ return plan;
929
+ case "plan_exported":
930
+ return plan;
931
+ case "task_added":
932
+ return plan;
933
+ case "task_removed":
934
+ if (event.task_id) {
935
+ for (const phase of plan.phases) {
936
+ const idx = phase.tasks.findIndex((t) => t.id === event.task_id);
937
+ if (idx !== -1) {
938
+ phase.tasks.splice(idx, 1);
939
+ break;
940
+ }
941
+ }
942
+ }
943
+ return plan;
944
+ case "task_updated":
945
+ return plan;
946
+ case "plan_rebuilt":
947
+ return plan;
948
+ case "task_reordered":
949
+ return plan;
950
+ case "snapshot":
951
+ return plan;
952
+ case "plan_reset":
953
+ return null;
954
+ case "execution_profile_set": {
955
+ const rawProfile = event.payload?.execution_profile;
956
+ if (rawProfile !== undefined) {
957
+ const parsed = ExecutionProfileSchema.safeParse(rawProfile);
958
+ if (parsed.success) {
959
+ return { ...plan, execution_profile: parsed.data };
960
+ }
961
+ }
962
+ return plan;
963
+ }
964
+ case "execution_profile_locked": {
965
+ if (plan.execution_profile) {
966
+ return {
967
+ ...plan,
968
+ execution_profile: { ...plan.execution_profile, locked: true }
969
+ };
970
+ }
971
+ return plan;
972
+ }
973
+ default:
974
+ throw new Error(`applyEventToPlan: unhandled event type "${event.event_type}" at seq ${event.seq}`);
975
+ }
976
+ }
977
+ async function readLedgerEventsWithIntegrity(directory) {
978
+ const ledgerPath = getLedgerPath(directory);
979
+ if (!fs2.existsSync(ledgerPath)) {
980
+ return { events: [], truncated: false, badSuffix: null };
981
+ }
982
+ try {
983
+ const content = fs2.readFileSync(ledgerPath, "utf8");
984
+ const lines = content.split(`
985
+ `);
986
+ const events = [];
987
+ let truncated = false;
988
+ let badSuffix = null;
989
+ for (let i = 0;i < lines.length; i++) {
990
+ const line = lines[i];
991
+ if (line.trim() === "") {
992
+ continue;
993
+ }
994
+ try {
995
+ const event = JSON.parse(line);
996
+ events.push(event);
997
+ } catch {
998
+ truncated = true;
999
+ badSuffix = lines.slice(i).join(`
1000
+ `);
1001
+ break;
1002
+ }
1003
+ }
1004
+ events.sort((a, b) => a.seq - b.seq);
1005
+ return { events, truncated, badSuffix };
1006
+ } catch {
1007
+ return { events: [], truncated: false, badSuffix: null };
1008
+ }
1009
+ }
1010
+ async function quarantineLedgerSuffix(directory, badSuffix) {
1011
+ try {
1012
+ const quarantinePath = path2.join(directory, ".swarm", "plan-ledger.quarantine");
1013
+ fs2.writeFileSync(quarantinePath, badSuffix, "utf8");
1014
+ console.warn(`[ledger] Corrupted suffix quarantined to ${path2.relative(directory, quarantinePath)}`);
1015
+ } catch {}
1016
+ }
1017
+ async function loadLastApprovedPlan(directory, expectedPlanId) {
1018
+ const events = await readLedgerEvents(directory);
1019
+ if (events.length === 0) {
1020
+ return null;
1021
+ }
1022
+ for (let i = events.length - 1;i >= 0; i--) {
1023
+ const event = events[i];
1024
+ if (event.event_type !== "snapshot")
1025
+ continue;
1026
+ if (event.source !== "critic_approved")
1027
+ continue;
1028
+ if (expectedPlanId !== undefined && event.plan_id !== expectedPlanId) {
1029
+ continue;
1030
+ }
1031
+ const payload = event.payload;
1032
+ if (!payload || typeof payload !== "object" || !payload.plan) {
1033
+ continue;
1034
+ }
1035
+ if (expectedPlanId !== undefined) {
1036
+ const payloadPlanId = derivePlanId(payload.plan);
1037
+ if (payloadPlanId !== expectedPlanId) {
1038
+ continue;
1039
+ }
1040
+ }
1041
+ return {
1042
+ plan: payload.plan,
1043
+ seq: event.seq,
1044
+ timestamp: event.timestamp,
1045
+ approval: payload.approval,
1046
+ payloadHash: payload.payload_hash
1047
+ };
1048
+ }
1049
+ return null;
1050
+ }
1051
+
1052
+ // src/plan/manager.ts
1053
+ class PlanConcurrentModificationError extends Error {
1054
+ constructor(message) {
1055
+ super(message);
1056
+ this.name = "PlanConcurrentModificationError";
1057
+ }
1058
+ }
1059
+
1060
+ class PlanTaskRemovalNotAcknowledgedError extends Error {
1061
+ missingTasks;
1062
+ constructor(missingTasks) {
1063
+ const idList = missingTasks.map((t) => `${t.id}(${t.status})`).join(", ");
1064
+ super(`PLAN_TASK_REMOVAL_NOT_ACKNOWLEDGED: the following tasks were present in the prior plan but missing from the new save: ${idList}. Pass acknowledged_removals.ids covering all missing task IDs with a non-empty reason to proceed.`);
1065
+ this.name = "PlanTaskRemovalNotAcknowledgedError";
1066
+ this.missingTasks = missingTasks;
1067
+ }
1068
+ }
1069
+ var startupLedgerCheckedWorkspaces = new Set;
1070
+ var recoveryMutexes = new Map;
1071
+ var PLAN_JSON_CACHE_NAMESPACE = "plan-json:validated:v1";
1072
+ var _internals2 = {
1073
+ loadPlan,
1074
+ loadPlanJsonOnly,
1075
+ regeneratePlanMarkdown
1076
+ };
1077
+ var CAS_BACKOFF_START_MS = 5;
1078
+ var CAS_BACKOFF_CAP_MS = 250;
1079
+ var CAS_BACKOFF_JITTER = 0.25;
1080
+ var CAS_MAX_RETRIES = 3;
1081
+ async function retryCasWithBackoff(directory, eventInput, options) {
1082
+ const maxRetries = options.maxRetries ?? CAS_MAX_RETRIES;
1083
+ let currentExpected = options.expectedHash;
1084
+ let attempt = 0;
1085
+ while (true) {
1086
+ try {
1087
+ return await appendLedgerEvent(directory, eventInput, {
1088
+ expectedHash: currentExpected,
1089
+ planHashAfter: options.planHashAfter
1090
+ });
1091
+ } catch (error) {
1092
+ if (!(error instanceof LedgerStaleWriterError) || attempt >= maxRetries) {
1093
+ throw error;
1094
+ }
1095
+ attempt++;
1096
+ const base = Math.min(CAS_BACKOFF_START_MS * 2 ** (attempt - 1), CAS_BACKOFF_CAP_MS);
1097
+ const jitter = base * CAS_BACKOFF_JITTER * (Math.random() * 2 - 1);
1098
+ const delayMs = Math.max(1, Math.round(base + jitter));
1099
+ emit("plan_ledger_cas_retry", {
1100
+ attempt,
1101
+ expectedHashPrefix: currentExpected.slice(0, 8),
1102
+ delayMs
1103
+ });
1104
+ await new Promise((resolve3) => setTimeout(resolve3, delayMs));
1105
+ if (options.verifyValid) {
1106
+ const stillValid = await options.verifyValid();
1107
+ if (!stillValid)
1108
+ return null;
1109
+ }
1110
+ currentExpected = computeCurrentPlanHash(directory);
1111
+ }
1112
+ }
1113
+ }
1114
+ async function loadPlanJsonOnly(directory) {
1115
+ try {
1116
+ return await parsePlanJsonCached(directory);
1117
+ } catch (error) {
1118
+ warn(`Plan validation failed for .swarm/plan.json: ${error instanceof Error ? error.message : String(error)}`);
1119
+ }
1120
+ return null;
1121
+ }
1122
+ function compareTaskIds(a, b) {
1123
+ const partsA = a.split(".").map((n) => parseInt(n, 10));
1124
+ const partsB = b.split(".").map((n) => parseInt(n, 10));
1125
+ const maxLen = Math.max(partsA.length, partsB.length);
1126
+ for (let i = 0;i < maxLen; i++) {
1127
+ const numA = partsA[i] ?? 0;
1128
+ const numB = partsB[i] ?? 0;
1129
+ if (numA !== numB) {
1130
+ return numA - numB;
1131
+ }
1132
+ }
1133
+ return 0;
1134
+ }
1135
+ async function getLatestLedgerHash(directory) {
1136
+ try {
1137
+ const events = await readLedgerEvents(directory);
1138
+ if (events.length === 0)
1139
+ return "";
1140
+ const lastEvent = events[events.length - 1];
1141
+ return lastEvent.plan_hash_after;
1142
+ } catch {
1143
+ return "";
1144
+ }
1145
+ }
1146
+ async function parsePlanJsonCached(directory) {
1147
+ const planJsonPath = path3.resolve(directory, ".swarm", "plan.json");
1148
+ return readCachedParsedFile(planJsonPath, PLAN_JSON_CACHE_NAMESPACE, () => readSwarmFileAsync(directory, "plan.json"), (planJsonContent) => {
1149
+ if (planJsonContent.includes("\x00") || planJsonContent.includes("\uFFFD")) {
1150
+ throw new Error("Plan rejected: .swarm/plan.json contains null bytes or invalid encoding");
1151
+ }
1152
+ const parsed = JSON.parse(planJsonContent);
1153
+ return PlanSchema.parse(parsed);
1154
+ });
1155
+ }
1156
+ function computePlanContentHash(plan) {
1157
+ const content = {
1158
+ schema_version: plan.schema_version,
1159
+ title: plan.title,
1160
+ swarm: plan.swarm,
1161
+ current_phase: plan.current_phase,
1162
+ migration_status: plan.migration_status,
1163
+ phases: plan.phases.map((phase) => ({
1164
+ id: phase.id,
1165
+ name: phase.name,
1166
+ status: phase.status,
1167
+ tasks: phase.tasks.map((task) => ({
1168
+ id: task.id,
1169
+ phase: task.phase,
1170
+ status: task.status,
1171
+ size: task.size,
1172
+ description: task.description,
1173
+ depends: [...task.depends].sort(compareTaskIds),
1174
+ acceptance: task.acceptance,
1175
+ files_touched: [...task.files_touched].sort(),
1176
+ evidence_path: task.evidence_path,
1177
+ blocked_reason: task.blocked_reason
1178
+ })).sort((a, b) => compareTaskIds(a.id, b.id))
1179
+ })).sort((a, b) => a.id - b.id)
1180
+ };
1181
+ const jsonString = JSON.stringify(content);
1182
+ return bunHash(jsonString).toString(36);
1183
+ }
1184
+ function extractPlanHashFromMarkdown(markdown) {
1185
+ const match = markdown.match(/<!--\s*PLAN_HASH:\s*(\S+)\s*-->/);
1186
+ return match ? match[1] : null;
1187
+ }
1188
+ async function isPlanMdInSync(directory, plan) {
1189
+ const planMdContent = await readSwarmFileAsync(directory, "plan.md");
1190
+ if (planMdContent === null) {
1191
+ return false;
1192
+ }
1193
+ const expectedHash = computePlanContentHash(plan);
1194
+ const existingHash = extractPlanHashFromMarkdown(planMdContent);
1195
+ if (existingHash === expectedHash) {
1196
+ return true;
1197
+ }
1198
+ const expectedMarkdown = derivePlanMarkdown(plan);
1199
+ const normalizedExpected = expectedMarkdown.trim();
1200
+ const normalizedActual = planMdContent.trim();
1201
+ if (normalizedActual === normalizedExpected) {
1202
+ return true;
1203
+ }
1204
+ return normalizedActual.includes(normalizedExpected) || normalizedExpected.includes(normalizedActual.replace(/^#.*$/gm, "").trim());
1205
+ }
1206
+ async function regeneratePlanMarkdown(directory, plan) {
1207
+ const swarmDir = path3.resolve(directory, ".swarm");
1208
+ const contentHash = computePlanContentHash(plan);
1209
+ const markdown = derivePlanMarkdown(plan);
1210
+ const markdownWithHash = `<!-- PLAN_HASH: ${contentHash} -->
1211
+ ${markdown}`;
1212
+ const mdPath = path3.join(swarmDir, "plan.md");
1213
+ const mdTempPath = path3.join(swarmDir, `plan.md.tmp.${Date.now()}.${Math.floor(Math.random() * 1e9)}`);
1214
+ try {
1215
+ await bunWrite(mdTempPath, markdownWithHash);
1216
+ renameSync3(mdTempPath, mdPath);
1217
+ } finally {
1218
+ try {
1219
+ unlinkSync(mdTempPath);
1220
+ } catch {}
1221
+ }
1222
+ }
1223
+ async function loadPlan(directory) {
1224
+ const planJsonContent = await readSwarmFileAsync(directory, "plan.json");
1225
+ if (planJsonContent !== null) {
1226
+ if (planJsonContent.includes("\x00") || planJsonContent.includes("\uFFFD")) {
1227
+ warn("Plan rejected: .swarm/plan.json contains null bytes or invalid encoding");
1228
+ } else {
1229
+ try {
1230
+ const validated = await parsePlanJsonCached(directory);
1231
+ if (validated === null) {
1232
+ warn("[loadPlan] plan.json disappeared during cached parse. Falling back to plan.md migration or ledger recovery.");
1233
+ } else {
1234
+ const inSync = await isPlanMdInSync(directory, validated);
1235
+ if (!inSync) {
1236
+ try {
1237
+ await _internals2.regeneratePlanMarkdown(directory, validated);
1238
+ } catch (regenError) {
1239
+ warn(`Failed to regenerate plan.md: ${regenError instanceof Error ? regenError.message : String(regenError)}. Proceeding with plan.json only.`);
1240
+ }
1241
+ }
1242
+ if (await ledgerExists(directory)) {
1243
+ const planHash = computePlanHash(validated);
1244
+ const ledgerHash = await getLatestLedgerHash(directory);
1245
+ const resolvedWorkspace = path3.resolve(directory);
1246
+ if (!startupLedgerCheckedWorkspaces.has(resolvedWorkspace)) {
1247
+ startupLedgerCheckedWorkspaces.add(resolvedWorkspace);
1248
+ if (ledgerHash !== "" && planHash !== ledgerHash) {
1249
+ const currentPlanId = derivePlanId(validated);
1250
+ const ledgerEvents = await readLedgerEvents(directory);
1251
+ const firstEvent = ledgerEvents.length > 0 ? ledgerEvents[0] : null;
1252
+ if (firstEvent && firstEvent.plan_id !== currentPlanId) {
1253
+ warn(`[loadPlan] Ledger identity mismatch (ledger: ${firstEvent.plan_id}, plan: ${currentPlanId}) \u2014 skipping ledger rebuild (migration detected). Use /swarm reset-session to reinitialize the ledger.`);
1254
+ } else {
1255
+ warn("[loadPlan] plan.json is stale (hash mismatch with ledger) \u2014 rebuilding from ledger. If this recurs, run /swarm reset-session to clear stale session state.");
1256
+ try {
1257
+ const rebuilt = await replayFromLedger(directory);
1258
+ if (rebuilt) {
1259
+ await rebuildPlan(directory, rebuilt, {
1260
+ reason: "ledger_hash_mismatch_recovery"
1261
+ });
1262
+ warn("[loadPlan] Rebuilt plan from ledger. Checkpoint available at .swarm/SWARM_PLAN.md if it exists.");
1263
+ return rebuilt;
1264
+ }
1265
+ } catch (replayError) {
1266
+ try {
1267
+ const approved = await loadLastApprovedPlan(directory, currentPlanId);
1268
+ if (approved) {
1269
+ await rebuildPlan(directory, approved.plan, {
1270
+ reason: "approved_snapshot_fallback"
1271
+ });
1272
+ try {
1273
+ await takeSnapshotEvent(directory, approved.plan, {
1274
+ source: "recovery_from_approved_snapshot",
1275
+ approvalMetadata: approved.approval
1276
+ });
1277
+ } catch (healError) {
1278
+ warn(`[loadPlan] Recovery-heal snapshot append failed: ${healError instanceof Error ? healError.message : String(healError)}. Next loadPlan may re-enter recovery path.`);
1279
+ }
1280
+ const approvedPhase = approved.approval && typeof approved.approval === "object" && "phase" in approved.approval ? approved.approval.phase : undefined;
1281
+ warn(`[loadPlan] Ledger replay failed (${replayError instanceof Error ? replayError.message : String(replayError)}) \u2014 recovered from critic-approved snapshot seq=${approved.seq} (approval phase=${approvedPhase ?? "unknown"}, timestamp=${approved.timestamp}). This may roll the plan back to an earlier phase \u2014 verify before continuing.`);
1282
+ return approved.plan;
1283
+ }
1284
+ } catch {}
1285
+ warn(`[loadPlan] Ledger replay failed during hash-mismatch rebuild: ${replayError instanceof Error ? replayError.message : String(replayError)}. Returning stale plan.json. To recover: check .swarm/SWARM_PLAN.md for a checkpoint, or run /swarm reset-session.`);
1286
+ }
1287
+ }
1288
+ }
1289
+ } else if (ledgerHash !== "" && planHash !== ledgerHash) {
1290
+ if (process.env.DEBUG_SWARM) {
1291
+ console.warn(`[loadPlan] Ledger hash mismatch during active session for ${resolvedWorkspace} \u2014 skipping rebuild (startup check already performed).`);
1292
+ }
1293
+ }
1294
+ }
1295
+ if (validated.specHash) {
1296
+ const staleResult = await isSpecStale(directory, validated);
1297
+ if (staleResult.stale) {
1298
+ const runtimePlan = validated;
1299
+ runtimePlan._specStale = true;
1300
+ runtimePlan._specStaleReason = staleResult.reason;
1301
+ try {
1302
+ const specStalenessPath = path3.join(directory, ".swarm", "spec-staleness.json");
1303
+ await fsPromises.writeFile(specStalenessPath, JSON.stringify({
1304
+ type: "spec_stale_detected",
1305
+ timestamp: new Date().toISOString(),
1306
+ phase: validated.current_phase ?? 1,
1307
+ specHash_plan: validated.specHash,
1308
+ specHash_current: staleResult.currentHash ?? null,
1309
+ reason: staleResult.reason,
1310
+ planTitle: validated.title
1311
+ }, null, 2), "utf-8");
1312
+ } catch {}
1313
+ try {
1314
+ const eventsPath = path3.join(directory, ".swarm", "events.jsonl");
1315
+ const event = {
1316
+ type: "spec_stale_detected",
1317
+ timestamp: new Date().toISOString(),
1318
+ phase: validated.current_phase ?? 1,
1319
+ specHash_plan: validated.specHash,
1320
+ specHash_current: staleResult.currentHash ?? null,
1321
+ reason: staleResult.reason ?? "unknown",
1322
+ planTitle: validated.title
1323
+ };
1324
+ await fsPromises.appendFile(eventsPath, `${JSON.stringify(event)}
1325
+ `, "utf-8");
1326
+ } catch {}
1327
+ }
1328
+ }
1329
+ return validated;
1330
+ }
1331
+ } catch (error) {
1332
+ warn(`[loadPlan] plan.json validation failed: ${error instanceof Error ? error.message : String(error)}. Attempting rebuild from ledger. If rebuild fails, check .swarm/SWARM_PLAN.md for a checkpoint.`);
1333
+ let rawPlanId = null;
1334
+ try {
1335
+ const rawParsed = JSON.parse(planJsonContent);
1336
+ if (typeof rawParsed?.swarm === "string" && typeof rawParsed?.title === "string") {
1337
+ rawPlanId = derivePlanId(rawParsed);
1338
+ }
1339
+ } catch {}
1340
+ if (await ledgerExists(directory)) {
1341
+ const ledgerEventsForCatch = await readLedgerEvents(directory);
1342
+ const catchFirstEvent = ledgerEventsForCatch.length > 0 ? ledgerEventsForCatch[0] : null;
1343
+ const identityMatch = rawPlanId === null || catchFirstEvent === null || catchFirstEvent.plan_id === rawPlanId;
1344
+ if (!identityMatch) {
1345
+ warn(`[loadPlan] Ledger identity mismatch in validation-failure path (ledger: ${catchFirstEvent?.plan_id}, plan: ${rawPlanId}) \u2014 skipping ledger rebuild (migration detected).`);
1346
+ } else if (catchFirstEvent !== null && rawPlanId !== null) {
1347
+ const rebuilt = await replayFromLedger(directory);
1348
+ if (rebuilt) {
1349
+ await rebuildPlan(directory, rebuilt, {
1350
+ reason: "validation_failure_recovery"
1351
+ });
1352
+ warn("[loadPlan] Rebuilt plan from ledger after validation failure. Projection was stale.");
1353
+ return rebuilt;
1354
+ }
1355
+ }
1356
+ }
1357
+ const planMdContent2 = await readSwarmFileAsync(directory, "plan.md");
1358
+ if (planMdContent2 !== null) {
1359
+ const migrated = migrateLegacyPlan(planMdContent2);
1360
+ const { removedCount } = await savePlanWithAutoAcknowledgedRemovals(directory, migrated, "load_plan_migration_from_md", "migrate legacy plan.md to plan.json");
1361
+ if (removedCount > 0) {
1362
+ migrated._midLoadRemovals = {
1363
+ count: removedCount,
1364
+ source: "load_plan_migration_from_md"
1365
+ };
1366
+ }
1367
+ return migrated;
1368
+ }
1369
+ }
1370
+ }
1371
+ }
1372
+ const planMdContent = await readSwarmFileAsync(directory, "plan.md");
1373
+ if (planMdContent !== null) {
1374
+ const migrated = migrateLegacyPlan(planMdContent);
1375
+ await savePlan(directory, migrated);
1376
+ return migrated;
1377
+ }
1378
+ if (await ledgerExists(directory)) {
1379
+ const resolvedDir = path3.resolve(directory);
1380
+ const existingMutex = recoveryMutexes.get(resolvedDir);
1381
+ if (existingMutex) {
1382
+ await existingMutex;
1383
+ const postRecoveryPlan = await _internals2.loadPlanJsonOnly(directory);
1384
+ if (postRecoveryPlan)
1385
+ return postRecoveryPlan;
1386
+ }
1387
+ let resolveRecovery;
1388
+ const mutex = new Promise((r) => {
1389
+ resolveRecovery = r;
1390
+ });
1391
+ recoveryMutexes.set(resolvedDir, mutex);
1392
+ try {
1393
+ const rebuilt = await replayFromLedger(directory);
1394
+ if (rebuilt) {
1395
+ const { removedCount } = await savePlanWithAutoAcknowledgedRemovals(directory, rebuilt, "load_plan_rebuild_from_ledger", "rebuild plan from ledger replay");
1396
+ if (removedCount > 0) {
1397
+ rebuilt._midLoadRemovals = {
1398
+ count: removedCount,
1399
+ source: "load_plan_rebuild_from_ledger"
1400
+ };
1401
+ }
1402
+ return rebuilt;
1403
+ }
1404
+ try {
1405
+ const anchorEvents = await readLedgerEvents(directory);
1406
+ if (anchorEvents.length === 0) {
1407
+ warn("[loadPlan] Ledger present but no events readable \u2014 refusing approved-snapshot recovery (cannot verify plan identity).");
1408
+ return null;
1409
+ }
1410
+ const expectedPlanId = anchorEvents[0].plan_id;
1411
+ const approved = await loadLastApprovedPlan(directory, expectedPlanId);
1412
+ if (approved) {
1413
+ const approvedPhase = approved.approval && typeof approved.approval === "object" && "phase" in approved.approval ? approved.approval.phase : undefined;
1414
+ warn(`[loadPlan] Ledger replay returned no plan \u2014 recovered from critic-approved snapshot seq=${approved.seq} timestamp=${approved.timestamp} (approval phase=${approvedPhase ?? "unknown"}). This may roll the plan back to an earlier phase \u2014 verify before continuing.`);
1415
+ const { removedCount: snapshotRemovedCount } = await savePlanWithAutoAcknowledgedRemovals(directory, approved.plan, "load_plan_recovery_from_approved_snapshot", "restore from critic-approved snapshot");
1416
+ if (snapshotRemovedCount > 0) {
1417
+ approved.plan._midLoadRemovals = {
1418
+ count: snapshotRemovedCount,
1419
+ source: "load_plan_recovery_from_approved_snapshot"
1420
+ };
1421
+ }
1422
+ try {
1423
+ await takeSnapshotEvent(directory, approved.plan, {
1424
+ source: "recovery_from_approved_snapshot",
1425
+ approvalMetadata: approved.approval
1426
+ });
1427
+ } catch (healError) {
1428
+ warn(`[loadPlan] Recovery-heal snapshot append failed: ${healError instanceof Error ? healError.message : String(healError)}. Next loadPlan may re-enter recovery path.`);
1429
+ }
1430
+ return approved.plan;
1431
+ }
1432
+ } catch (recoveryError) {
1433
+ warn(`[loadPlan] Approved-snapshot recovery failed: ${recoveryError instanceof Error ? recoveryError.message : String(recoveryError)}`);
1434
+ }
1435
+ } finally {
1436
+ resolveRecovery();
1437
+ recoveryMutexes.delete(resolvedDir);
1438
+ }
1439
+ }
1440
+ return null;
1441
+ }
1442
+ async function savePlanWithAutoAcknowledgedRemovals(directory, plan, source, reason, options) {
1443
+ const existing = await _internals2.loadPlanJsonOnly(directory);
1444
+ const newIds = new Set;
1445
+ for (const phase of plan.phases) {
1446
+ for (const task of phase.tasks)
1447
+ newIds.add(task.id);
1448
+ }
1449
+ const removedIds = [];
1450
+ if (existing) {
1451
+ for (const phase of existing.phases) {
1452
+ for (const task of phase.tasks) {
1453
+ if (!newIds.has(task.id))
1454
+ removedIds.push(task.id);
1455
+ }
1456
+ }
1457
+ }
1458
+ await savePlan(directory, plan, {
1459
+ ...options ?? {},
1460
+ acknowledged_removals: { ids: removedIds, reason, source }
1461
+ });
1462
+ return { removedCount: removedIds.length };
1463
+ }
1464
+ async function savePlan(directory, plan, options) {
1465
+ if (directory === null || directory === undefined || typeof directory !== "string" || directory.trim().length === 0) {
1466
+ throw new Error(`Invalid directory: directory must be a non-empty string`);
1467
+ }
1468
+ const validated = PlanSchema.parse(plan);
1469
+ if (options?.preserveCompletedStatuses !== false) {
1470
+ try {
1471
+ const currentPlan2 = await _internals2.loadPlanJsonOnly(directory);
1472
+ if (currentPlan2) {
1473
+ const completedTaskIds = new Set;
1474
+ for (const phase of currentPlan2.phases) {
1475
+ for (const task of phase.tasks) {
1476
+ if (task.status === "completed")
1477
+ completedTaskIds.add(task.id);
1478
+ }
1479
+ }
1480
+ if (completedTaskIds.size > 0) {
1481
+ for (const phase of validated.phases) {
1482
+ for (const task of phase.tasks) {
1483
+ if (completedTaskIds.has(task.id) && task.status !== "completed") {
1484
+ task.status = "completed";
1485
+ }
1486
+ }
1487
+ }
1488
+ }
1489
+ }
1490
+ } catch {}
1491
+ }
1492
+ for (const phase of validated.phases) {
1493
+ const tasks = phase.tasks;
1494
+ if (tasks.length > 0 && tasks.every((t) => t.status === "completed")) {
1495
+ phase.status = "complete";
1496
+ } else if (tasks.some((t) => t.status === "in_progress")) {
1497
+ phase.status = "in_progress";
1498
+ } else if (tasks.some((t) => t.status === "blocked")) {
1499
+ phase.status = "blocked";
1500
+ } else {
1501
+ phase.status = "pending";
1502
+ }
1503
+ }
1504
+ const currentPlan = await _internals2.loadPlanJsonOnly(directory);
1505
+ const planId = derivePlanId(validated);
1506
+ const planHashForInit = computePlanHash(validated);
1507
+ if (!await ledgerExists(directory)) {
1508
+ try {
1509
+ await initLedger(directory, planId, planHashForInit, validated);
1510
+ } catch (initErr) {
1511
+ const msg = initErr instanceof Error ? initErr.message : String(initErr);
1512
+ if (!/already initialized/i.test(msg)) {
1513
+ throw initErr;
1514
+ }
1515
+ }
1516
+ } else {
1517
+ const existingEvents = await readLedgerEvents(directory);
1518
+ if (existingEvents.length > 0 && existingEvents[0].plan_id !== planId) {
1519
+ const swarmDir2 = path3.resolve(directory, ".swarm");
1520
+ const oldLedgerPath = path3.join(swarmDir2, "plan-ledger.jsonl");
1521
+ const oldLedgerBackupPath = path3.join(swarmDir2, `plan-ledger.backup-${Date.now()}-${Math.floor(Math.random() * 1e9)}.jsonl`);
1522
+ let backupExists = false;
1523
+ if (existsSync3(oldLedgerPath)) {
1524
+ try {
1525
+ renameSync3(oldLedgerPath, oldLedgerBackupPath);
1526
+ backupExists = true;
1527
+ } catch (renameErr) {
1528
+ 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.`);
1529
+ }
1530
+ }
1531
+ let initSucceeded = false;
1532
+ if (backupExists) {
1533
+ try {
1534
+ await initLedger(directory, planId, planHashForInit, validated);
1535
+ initSucceeded = true;
1536
+ } catch (initErr) {
1537
+ const errorMessage = String(initErr);
1538
+ if (errorMessage.includes("already initialized")) {
1539
+ try {
1540
+ if (existsSync3(oldLedgerBackupPath))
1541
+ unlinkSync(oldLedgerBackupPath);
1542
+ } catch {}
1543
+ } else {
1544
+ if (existsSync3(oldLedgerBackupPath)) {
1545
+ try {
1546
+ renameSync3(oldLedgerBackupPath, oldLedgerPath);
1547
+ } catch {
1548
+ copyFileSync(oldLedgerBackupPath, oldLedgerPath);
1549
+ try {
1550
+ unlinkSync(oldLedgerBackupPath);
1551
+ } catch {}
1552
+ }
1553
+ }
1554
+ throw initErr;
1555
+ }
1556
+ }
1557
+ }
1558
+ if (initSucceeded && backupExists) {
1559
+ const archivePath = path3.join(swarmDir2, `plan-ledger.archived-${Date.now()}-${Math.floor(Math.random() * 1e9)}.jsonl`);
1560
+ try {
1561
+ renameSync3(oldLedgerBackupPath, archivePath);
1562
+ warn(`[savePlan] Ledger identity mismatch (was "${existingEvents[0].plan_id}", now "${planId}") \u2014 archived old ledger to ${archivePath} and reinitializing.`);
1563
+ } catch (renameErr) {
1564
+ warn(`[savePlan] Could not archive old ledger (rename failed: ${renameErr instanceof Error ? renameErr.message : String(renameErr)}). Old ledger may still exist at ${oldLedgerBackupPath}.`);
1565
+ try {
1566
+ if (existsSync3(oldLedgerBackupPath))
1567
+ unlinkSync(oldLedgerBackupPath);
1568
+ } catch {}
1569
+ }
1570
+ } else if (!initSucceeded && backupExists) {
1571
+ try {
1572
+ if (existsSync3(oldLedgerBackupPath))
1573
+ unlinkSync(oldLedgerBackupPath);
1574
+ } catch {}
1575
+ }
1576
+ const MAX_ARCHIVED_SIBLINGS = 5;
1577
+ try {
1578
+ const allFiles = readdirSync2(swarmDir2);
1579
+ const archivedSiblings = allFiles.filter((f) => f.startsWith("plan-ledger.archived-") && f.endsWith(".jsonl")).sort();
1580
+ if (archivedSiblings.length > MAX_ARCHIVED_SIBLINGS) {
1581
+ const toRemove = archivedSiblings.slice(0, archivedSiblings.length - MAX_ARCHIVED_SIBLINGS);
1582
+ for (const file of toRemove) {
1583
+ try {
1584
+ unlinkSync(path3.join(swarmDir2, file));
1585
+ } catch {}
1586
+ }
1587
+ }
1588
+ } catch {}
1589
+ }
1590
+ }
1591
+ const currentHash = computeCurrentPlanHash(directory);
1592
+ const hashAfter = computePlanHash(validated);
1593
+ if (currentPlan) {
1594
+ const oldTaskMap = new Map;
1595
+ for (const phase of currentPlan.phases) {
1596
+ for (const task of phase.tasks) {
1597
+ oldTaskMap.set(task.id, { phase: task.phase, status: task.status });
1598
+ }
1599
+ }
1600
+ const newTaskIds = new Set;
1601
+ for (const phase of validated.phases) {
1602
+ for (const task of phase.tasks)
1603
+ newTaskIds.add(task.id);
1604
+ }
1605
+ const missingTasks = [];
1606
+ for (const [id, info] of oldTaskMap.entries()) {
1607
+ if (!newTaskIds.has(id)) {
1608
+ missingTasks.push({ id, phase: info.phase, status: info.status });
1609
+ }
1610
+ }
1611
+ const ack = options?.acknowledged_removals;
1612
+ if (missingTasks.length > 0) {
1613
+ if (!ack) {
1614
+ throw new PlanTaskRemovalNotAcknowledgedError(missingTasks);
1615
+ }
1616
+ if (typeof ack.reason !== "string" || ack.reason.trim().length === 0) {
1617
+ throw new Error("PLAN_ACKNOWLEDGED_REMOVAL_INVALID: acknowledged_removals.reason must be a non-empty string.");
1618
+ }
1619
+ if (typeof ack.source !== "string" || ack.source.trim().length === 0) {
1620
+ throw new Error("PLAN_ACKNOWLEDGED_REMOVAL_INVALID: acknowledged_removals.source must be a non-empty string.");
1621
+ }
1622
+ const ackSet = new Set(ack.ids);
1623
+ const missingIdsSet = new Set(missingTasks.map((t) => t.id));
1624
+ const unacked = missingTasks.filter((t) => !ackSet.has(t.id));
1625
+ if (unacked.length > 0) {
1626
+ throw new PlanTaskRemovalNotAcknowledgedError(unacked);
1627
+ }
1628
+ for (const id of ack.ids) {
1629
+ if (!missingIdsSet.has(id)) {
1630
+ throw new Error(`PLAN_ACKNOWLEDGED_REMOVAL_INVALID: acknowledged_removals contains "${id}" but that task is not missing from the plan.`);
1631
+ }
1632
+ }
1633
+ try {
1634
+ for (const missing of missingTasks) {
1635
+ const eventInput = {
1636
+ plan_id: derivePlanId(validated),
1637
+ event_type: "task_removed",
1638
+ task_id: missing.id,
1639
+ phase_id: missing.phase,
1640
+ from_status: missing.status,
1641
+ source: ack.source,
1642
+ payload: { reason: ack.reason, source: ack.source }
1643
+ };
1644
+ const capturedTaskId = missing.id;
1645
+ await retryCasWithBackoff(directory, eventInput, {
1646
+ expectedHash: currentHash,
1647
+ planHashAfter: hashAfter,
1648
+ verifyValid: async () => {
1649
+ const onDisk = await _internals2.loadPlanJsonOnly(directory);
1650
+ if (!onDisk)
1651
+ return true;
1652
+ for (const p of onDisk.phases) {
1653
+ if (p.tasks.some((x) => x.id === capturedTaskId))
1654
+ return true;
1655
+ }
1656
+ return false;
1657
+ }
1658
+ });
1659
+ }
1660
+ } catch (error) {
1661
+ if (error instanceof LedgerStaleWriterError) {
1662
+ throw new PlanConcurrentModificationError(`Concurrent plan modification detected after retries: ${error.message}. Please retry the operation.`);
1663
+ }
1664
+ throw error;
1665
+ }
1666
+ }
1667
+ try {
1668
+ for (const phase of validated.phases) {
1669
+ for (const task of phase.tasks) {
1670
+ const oldTask = oldTaskMap.get(task.id);
1671
+ if (oldTask && oldTask.status !== task.status) {
1672
+ const eventInput = {
1673
+ plan_id: derivePlanId(validated),
1674
+ event_type: "task_status_changed",
1675
+ task_id: task.id,
1676
+ phase_id: phase.id,
1677
+ from_status: oldTask.status,
1678
+ to_status: task.status,
1679
+ source: "savePlan"
1680
+ };
1681
+ const capturedFromStatus = oldTask.status;
1682
+ const capturedTaskId = task.id;
1683
+ await retryCasWithBackoff(directory, eventInput, {
1684
+ expectedHash: currentHash,
1685
+ planHashAfter: hashAfter,
1686
+ verifyValid: async () => {
1687
+ const onDisk = await _internals2.loadPlanJsonOnly(directory);
1688
+ if (!onDisk)
1689
+ return true;
1690
+ for (const p of onDisk.phases) {
1691
+ const t = p.tasks.find((x) => x.id === capturedTaskId);
1692
+ if (t) {
1693
+ return t.status === capturedFromStatus;
1694
+ }
1695
+ }
1696
+ return false;
1697
+ }
1698
+ });
1699
+ }
1700
+ }
1701
+ }
1702
+ } catch (error) {
1703
+ if (error instanceof LedgerStaleWriterError) {
1704
+ throw new PlanConcurrentModificationError(`Concurrent plan modification detected after retries: ${error.message}. Please retry the operation.`);
1705
+ }
1706
+ throw error;
1707
+ }
1708
+ }
1709
+ const SNAPSHOT_INTERVAL = 50;
1710
+ const latestSeq = await getLatestLedgerSeq(directory);
1711
+ if (latestSeq > 0 && latestSeq % SNAPSHOT_INTERVAL === 0) {
1712
+ await takeSnapshotWithRetry(directory, validated, {
1713
+ planHashAfter: hashAfter,
1714
+ source: "savePlan_manager"
1715
+ });
1716
+ }
1717
+ const swarmDir = path3.resolve(directory, ".swarm");
1718
+ const planPath = path3.join(swarmDir, "plan.json");
1719
+ const tempPath = path3.join(swarmDir, `plan.json.tmp.${Date.now()}.${Math.floor(Math.random() * 1e9)}`);
1720
+ try {
1721
+ await bunWrite(tempPath, JSON.stringify(validated, null, 2));
1722
+ renameSync3(tempPath, planPath);
1723
+ } finally {
1724
+ try {
1725
+ unlinkSync(tempPath);
1726
+ } catch {}
1727
+ }
1728
+ try {
1729
+ const markerPath = path3.join(swarmDir, ".plan-write-marker");
1730
+ const inProgressMarker = JSON.stringify({
1731
+ source: "plan_manager",
1732
+ timestamp: new Date().toISOString(),
1733
+ phases_count: validated.phases.length,
1734
+ tasks_count: validated.phases.reduce((sum, p) => sum + p.tasks.length, 0),
1735
+ in_progress: true
1736
+ });
1737
+ await bunWrite(markerPath, inProgressMarker);
1738
+ } catch {}
1739
+ try {
1740
+ const contentHash = computePlanContentHash(validated);
1741
+ const markdown = derivePlanMarkdown(validated);
1742
+ const markdownWithHash = `<!-- PLAN_HASH: ${contentHash} -->
1743
+ ${markdown}`;
1744
+ const mdPath = path3.join(swarmDir, "plan.md");
1745
+ const mdTempPath = path3.join(swarmDir, `plan.md.tmp.${Date.now()}.${Math.floor(Math.random() * 1e9)}`);
1746
+ try {
1747
+ await bunWrite(mdTempPath, markdownWithHash);
1748
+ renameSync3(mdTempPath, mdPath);
1749
+ } finally {
1750
+ try {
1751
+ unlinkSync(mdTempPath);
1752
+ } catch {}
1753
+ }
1754
+ } catch (mdError) {
1755
+ const message = mdError instanceof Error ? mdError.message : String(mdError);
1756
+ warn(`[savePlan] plan.md write failed (non-fatal, plan.json is authoritative): ${message}`);
1757
+ try {
1758
+ emit("plan_md_write_failed", {
1759
+ directory,
1760
+ error: message,
1761
+ timestamp: new Date().toISOString()
1762
+ });
1763
+ } catch {}
1764
+ }
1765
+ try {
1766
+ const markerPath = path3.join(swarmDir, ".plan-write-marker");
1767
+ const tasksCount = validated.phases.reduce((sum, phase) => sum + phase.tasks.length, 0);
1768
+ const marker = JSON.stringify({
1769
+ source: "plan_manager",
1770
+ timestamp: new Date().toISOString(),
1771
+ phases_count: validated.phases.length,
1772
+ tasks_count: tasksCount,
1773
+ in_progress: false
1774
+ });
1775
+ await bunWrite(markerPath, marker);
1776
+ } catch {}
1777
+ }
1778
+ async function rebuildPlan(directory, plan, options) {
1779
+ const targetPlan = plan ?? await replayFromLedger(directory);
1780
+ if (!targetPlan)
1781
+ return null;
1782
+ const swarmDir = path3.join(directory, ".swarm");
1783
+ const planPath = path3.join(swarmDir, "plan.json");
1784
+ const mdPath = path3.join(swarmDir, "plan.md");
1785
+ const tempPlanPath = path3.join(swarmDir, `plan.json.rebuild.${Date.now()}.${Math.floor(Math.random() * 1e9)}`);
1786
+ await bunWrite(tempPlanPath, JSON.stringify(targetPlan, null, 2));
1787
+ renameSync3(tempPlanPath, planPath);
1788
+ try {
1789
+ const markerPath = path3.join(swarmDir, ".plan-write-marker");
1790
+ const inProgressMarker = JSON.stringify({
1791
+ source: "plan_manager",
1792
+ timestamp: new Date().toISOString(),
1793
+ phases_count: targetPlan.phases.length,
1794
+ tasks_count: targetPlan.phases.reduce((sum, phase) => sum + phase.tasks.length, 0),
1795
+ in_progress: true
1796
+ });
1797
+ await bunWrite(markerPath, inProgressMarker);
1798
+ } catch {}
1799
+ try {
1800
+ const contentHash = computePlanContentHash(targetPlan);
1801
+ const markdown = derivePlanMarkdown(targetPlan);
1802
+ const markdownWithHash = `<!-- PLAN_HASH: ${contentHash} -->
1803
+ ${markdown}`;
1804
+ const tempMdPath = path3.join(swarmDir, `plan.md.rebuild.${Date.now()}.${Math.floor(Math.random() * 1e9)}`);
1805
+ await bunWrite(tempMdPath, markdownWithHash);
1806
+ renameSync3(tempMdPath, mdPath);
1807
+ } finally {
1808
+ try {
1809
+ const markerPath = path3.join(swarmDir, ".plan-write-marker");
1810
+ const tasksCount = targetPlan.phases.reduce((sum, phase) => sum + phase.tasks.length, 0);
1811
+ const marker = JSON.stringify({
1812
+ source: "plan_manager",
1813
+ timestamp: new Date().toISOString(),
1814
+ phases_count: targetPlan.phases.length,
1815
+ tasks_count: tasksCount,
1816
+ in_progress: false
1817
+ });
1818
+ await bunWrite(markerPath, marker);
1819
+ } catch {}
1820
+ }
1821
+ try {
1822
+ const planId = derivePlanId(targetPlan);
1823
+ const planHashAfter = computePlanHash(targetPlan);
1824
+ await appendLedgerEvent(directory, {
1825
+ event_type: "plan_rebuilt",
1826
+ source: "rebuildPlan",
1827
+ plan_id: planId,
1828
+ payload: {
1829
+ reason: options?.reason ?? "ledger_replay_recovery",
1830
+ phases_count: targetPlan.phases.length,
1831
+ tasks_count: targetPlan.phases.reduce((sum, p) => sum + p.tasks.length, 0)
1832
+ }
1833
+ }, { planHashAfter });
1834
+ } catch {}
1835
+ return targetPlan;
1836
+ }
1837
+ async function closePlanTerminalState(directory, plan, options) {
1838
+ const planId = derivePlanId(plan);
1839
+ const validated = PlanSchema.parse(plan);
1840
+ const hashAfter = computePlanHash(validated);
1841
+ for (const taskId of options.closedTaskIds) {
1842
+ let taskPhaseId;
1843
+ for (const phase of validated.phases) {
1844
+ if (phase.tasks.some((t) => t.id === taskId)) {
1845
+ taskPhaseId = phase.id;
1846
+ break;
1847
+ }
1848
+ }
1849
+ const fromStatus = options.originalStatuses?.get(taskId) ?? "in_progress";
1850
+ await appendLedgerEvent(directory, {
1851
+ plan_id: planId,
1852
+ event_type: "task_status_changed",
1853
+ task_id: taskId,
1854
+ phase_id: taskPhaseId,
1855
+ from_status: fromStatus,
1856
+ to_status: "closed",
1857
+ source: "close_terminal"
1858
+ }, { planHashAfter: hashAfter });
1859
+ }
1860
+ for (const phaseId of options.closedPhaseIds) {
1861
+ await appendLedgerEvent(directory, {
1862
+ plan_id: planId,
1863
+ event_type: "phase_completed",
1864
+ phase_id: phaseId,
1865
+ source: "close_terminal"
1866
+ }, { planHashAfter: hashAfter });
1867
+ }
1868
+ await takeSnapshotEvent(directory, validated, {
1869
+ planHashAfter: hashAfter,
1870
+ source: "close_terminal"
1871
+ });
1872
+ const swarmDir = path3.join(directory, ".swarm");
1873
+ const planPath = path3.join(swarmDir, "plan.json");
1874
+ const tempPlanPath = path3.join(swarmDir, `plan.json.close.${Date.now()}.${Math.floor(Math.random() * 1e9)}`);
1875
+ await bunWrite(tempPlanPath, JSON.stringify(validated, null, 2));
1876
+ renameSync3(tempPlanPath, planPath);
1877
+ try {
1878
+ const markerPath = path3.join(swarmDir, ".plan-write-marker");
1879
+ const inProgressMarker = JSON.stringify({
1880
+ source: "plan_manager_close",
1881
+ timestamp: new Date().toISOString(),
1882
+ phases_count: validated.phases.length,
1883
+ tasks_count: validated.phases.reduce((sum, phase) => sum + phase.tasks.length, 0),
1884
+ in_progress: true
1885
+ });
1886
+ await bunWrite(markerPath, inProgressMarker);
1887
+ } catch {}
1888
+ try {
1889
+ const mdPath = path3.join(swarmDir, "plan.md");
1890
+ const contentHash = computePlanContentHash(validated);
1891
+ const markdown = derivePlanMarkdown(validated);
1892
+ const markdownWithHash = `<!-- PLAN_HASH: ${contentHash} -->
1893
+ ${markdown}`;
1894
+ const mdTempPath = path3.join(swarmDir, `plan.md.close.${Date.now()}.${Math.floor(Math.random() * 1e9)}`);
1895
+ await bunWrite(mdTempPath, markdownWithHash);
1896
+ renameSync3(mdTempPath, mdPath);
1897
+ } finally {
1898
+ try {
1899
+ const markerPath = path3.join(swarmDir, ".plan-write-marker");
1900
+ const tasksCount = validated.phases.reduce((sum, phase) => sum + phase.tasks.length, 0);
1901
+ const marker = JSON.stringify({
1902
+ source: "plan_manager_close",
1903
+ timestamp: new Date().toISOString(),
1904
+ phases_count: validated.phases.length,
1905
+ tasks_count: tasksCount,
1906
+ in_progress: false
1907
+ });
1908
+ await bunWrite(markerPath, marker);
1909
+ } catch {}
1910
+ }
1911
+ }
1912
+ function derivePlanMarkdown(plan) {
1913
+ const statusMap = {
1914
+ pending: "PENDING",
1915
+ in_progress: "IN PROGRESS",
1916
+ complete: "COMPLETE",
1917
+ completed: "COMPLETE",
1918
+ blocked: "BLOCKED",
1919
+ closed: "CLOSED"
1920
+ };
1921
+ const now = new Date().toISOString();
1922
+ const currentPhase = plan.current_phase ?? 1;
1923
+ const phaseStatus = statusMap[plan.phases[currentPhase - 1]?.status] || "PENDING";
1924
+ let markdown = `# ${plan.title}
1925
+ Swarm: ${plan.swarm}
1926
+ Phase: ${currentPhase} [${phaseStatus}] | Updated: ${now}
1927
+ `;
1928
+ const sortedPhases = [...plan.phases].sort((a, b) => a.id - b.id);
1929
+ for (const phase of sortedPhases) {
1930
+ const phaseStatusText = statusMap[phase.status] || "PENDING";
1931
+ markdown += `
1932
+ ## Phase ${phase.id}: ${phase.name} [${phaseStatusText}]
1933
+ `;
1934
+ const sortedTasks = [...phase.tasks].sort((a, b) => compareTaskIds(a.id, b.id));
1935
+ let currentTaskMarked = false;
1936
+ for (const task of sortedTasks) {
1937
+ let taskLine = "";
1938
+ let suffix = "";
1939
+ if (task.status === "completed") {
1940
+ taskLine = `- [x] ${task.id}: ${task.description}`;
1941
+ } else if (task.status === "blocked") {
1942
+ taskLine = `- [BLOCKED] ${task.id}: ${task.description}`;
1943
+ if (task.blocked_reason) {
1944
+ taskLine += ` - ${task.blocked_reason}`;
1945
+ }
1946
+ } else {
1947
+ taskLine = `- [ ] ${task.id}: ${task.description}`;
1948
+ }
1949
+ taskLine += ` [${task.size.toUpperCase()}]`;
1950
+ if (task.depends.length > 0) {
1951
+ const sortedDepends = [...task.depends].sort();
1952
+ suffix += ` (depends: ${sortedDepends.join(", ")})`;
1953
+ }
1954
+ if (phase.id === plan.current_phase && task.status === "in_progress" && !currentTaskMarked) {
1955
+ suffix += " \u2190 CURRENT";
1956
+ currentTaskMarked = true;
1957
+ }
1958
+ markdown += `${taskLine}${suffix}
1959
+ `;
1960
+ }
1961
+ }
1962
+ const phaseSections = markdown.split(`
1963
+ ## `);
1964
+ if (phaseSections.length > 1) {
1965
+ const header = phaseSections[0];
1966
+ const phases = phaseSections.slice(1).map((p) => `## ${p}`);
1967
+ markdown = `${header}
1968
+ ---
1969
+ ${phases.join(`
1970
+ ---
1971
+ `)}`;
1972
+ }
1973
+ return `${markdown.trim()}
1974
+ `;
1975
+ }
1976
+ function migrateLegacyPlan(planContent, swarmId) {
1977
+ const lines = planContent.split(`
1978
+ `);
1979
+ let title = "Untitled Plan";
1980
+ let swarm = swarmId || "default-swarm";
1981
+ let currentPhaseNum = 1;
1982
+ const phases = [];
1983
+ let currentPhase = null;
1984
+ for (const line of lines) {
1985
+ const trimmed = line.trim();
1986
+ if (trimmed.startsWith("# ") && title === "Untitled Plan") {
1987
+ title = trimmed.substring(2).trim();
1988
+ continue;
1989
+ }
1990
+ if (trimmed.startsWith("Swarm:")) {
1991
+ swarm = trimmed.substring(6).trim();
1992
+ continue;
1993
+ }
1994
+ if (trimmed.startsWith("Phase:")) {
1995
+ const match = trimmed.match(/Phase:\s*(\d+)/i);
1996
+ if (match) {
1997
+ currentPhaseNum = parseInt(match[1], 10);
1998
+ }
1999
+ continue;
2000
+ }
2001
+ const phaseMatch = trimmed.match(/^#{2,3}\s*Phase\s+(\d+)(?::\s*([^[]+))?\s*(?:\[([^\]]+)\])?/i);
2002
+ if (phaseMatch) {
2003
+ if (currentPhase !== null) {
2004
+ phases.push(currentPhase);
2005
+ }
2006
+ const phaseId = parseInt(phaseMatch[1], 10);
2007
+ const phaseName = phaseMatch[2]?.trim() || `Phase ${phaseId}`;
2008
+ const statusText = phaseMatch[3]?.toLowerCase() || "pending";
2009
+ const statusMap = {
2010
+ complete: "complete",
2011
+ completed: "complete",
2012
+ "in progress": "in_progress",
2013
+ in_progress: "in_progress",
2014
+ inprogress: "in_progress",
2015
+ pending: "pending",
2016
+ blocked: "blocked"
2017
+ };
2018
+ currentPhase = {
2019
+ id: phaseId,
2020
+ name: phaseName,
2021
+ status: statusMap[statusText] || "pending",
2022
+ tasks: []
2023
+ };
2024
+ continue;
2025
+ }
2026
+ const taskMatch = trimmed.match(/^-\s*\[([^\]]+)\]\s+(\d+\.\d+):\s*(.+?)(?:\s*\[(\w+)\])?(?:\s*-\s*(.+))?$/i);
2027
+ if (taskMatch && currentPhase !== null) {
2028
+ const checkbox = taskMatch[1].toLowerCase();
2029
+ const taskId = taskMatch[2];
2030
+ let description = taskMatch[3].trim();
2031
+ const sizeText = taskMatch[4]?.toLowerCase() || "small";
2032
+ let blockedReason;
2033
+ const dependsMatch = description.match(/\s*\(depends:\s*([^)]+)\)$/i);
2034
+ const depends = [];
2035
+ if (dependsMatch) {
2036
+ const depsText = dependsMatch[1];
2037
+ depends.push(...depsText.split(",").map((d) => d.trim()));
2038
+ description = description.substring(0, dependsMatch.index).trim();
2039
+ }
2040
+ let status = "pending";
2041
+ if (checkbox === "x") {
2042
+ status = "completed";
2043
+ } else if (checkbox === "blocked") {
2044
+ status = "blocked";
2045
+ const blockedReasonMatch = taskMatch[5];
2046
+ if (blockedReasonMatch) {
2047
+ blockedReason = blockedReasonMatch.trim();
2048
+ }
2049
+ }
2050
+ const sizeMap = {
2051
+ small: "small",
2052
+ medium: "medium",
2053
+ large: "large"
2054
+ };
2055
+ const task = {
2056
+ id: taskId,
2057
+ phase: currentPhase.id,
2058
+ status,
2059
+ size: sizeMap[sizeText] || "small",
2060
+ description,
2061
+ depends,
2062
+ acceptance: undefined,
2063
+ files_touched: [],
2064
+ evidence_path: undefined,
2065
+ blocked_reason: blockedReason
2066
+ };
2067
+ currentPhase.tasks.push(task);
2068
+ }
2069
+ const numberedTaskMatch = trimmed.match(/^(\d+)\.\s+(.+?)(?:\s*\[(\w+)\])?$/);
2070
+ if (numberedTaskMatch && currentPhase !== null) {
2071
+ const taskId = `${currentPhase.id}.${currentPhase.tasks.length + 1}`;
2072
+ let description = numberedTaskMatch[2].trim();
2073
+ const sizeText = numberedTaskMatch[3]?.toLowerCase() || "small";
2074
+ const dependsMatch = description.match(/\s*\(depends:\s*([^)]+)\)$/i);
2075
+ const depends = [];
2076
+ if (dependsMatch) {
2077
+ const depsText = dependsMatch[1];
2078
+ depends.push(...depsText.split(",").map((d) => d.trim()));
2079
+ description = description.substring(0, dependsMatch.index).trim();
2080
+ }
2081
+ const sizeMap = {
2082
+ small: "small",
2083
+ medium: "medium",
2084
+ large: "large"
2085
+ };
2086
+ const task = {
2087
+ id: taskId,
2088
+ phase: currentPhase.id,
2089
+ status: "pending",
2090
+ size: sizeMap[sizeText] || "small",
2091
+ description,
2092
+ depends,
2093
+ acceptance: undefined,
2094
+ files_touched: [],
2095
+ evidence_path: undefined,
2096
+ blocked_reason: undefined
2097
+ };
2098
+ currentPhase.tasks.push(task);
2099
+ }
2100
+ const noPrefixTaskMatch = trimmed.match(/^-\s*\[([^\]]+)\]\s+(?!\d+\.\d+:)(.+?)(?:\s*\[(\w+)\])?(?:\s*-\s*(.+))?$/i);
2101
+ if (noPrefixTaskMatch && currentPhase !== null) {
2102
+ const checkbox = noPrefixTaskMatch[1].toLowerCase();
2103
+ const taskId = `${currentPhase.id}.${currentPhase.tasks.length + 1}`;
2104
+ let description = noPrefixTaskMatch[2].trim();
2105
+ const sizeText = noPrefixTaskMatch[3]?.toLowerCase() || "small";
2106
+ let blockedReason;
2107
+ const dependsMatch = description.match(/\s*\(depends:\s*([^)]+)\)$/i);
2108
+ const depends = [];
2109
+ if (dependsMatch) {
2110
+ const depsText = dependsMatch[1];
2111
+ depends.push(...depsText.split(",").map((d) => d.trim()));
2112
+ description = description.substring(0, dependsMatch.index).trim();
2113
+ }
2114
+ let status = "pending";
2115
+ if (checkbox === "x") {
2116
+ status = "completed";
2117
+ } else if (checkbox === "blocked") {
2118
+ status = "blocked";
2119
+ const blockedReasonMatch = noPrefixTaskMatch[4];
2120
+ if (blockedReasonMatch) {
2121
+ blockedReason = blockedReasonMatch.trim();
2122
+ }
2123
+ }
2124
+ const sizeMap = {
2125
+ small: "small",
2126
+ medium: "medium",
2127
+ large: "large"
2128
+ };
2129
+ const task = {
2130
+ id: taskId,
2131
+ phase: currentPhase.id,
2132
+ status,
2133
+ size: sizeMap[sizeText] || "small",
2134
+ description,
2135
+ depends,
2136
+ acceptance: undefined,
2137
+ files_touched: [],
2138
+ evidence_path: undefined,
2139
+ blocked_reason: blockedReason
2140
+ };
2141
+ currentPhase.tasks.push(task);
2142
+ }
2143
+ }
2144
+ if (currentPhase !== null) {
2145
+ phases.push(currentPhase);
2146
+ }
2147
+ let migrationStatus = "migrated";
2148
+ if (phases.length === 0) {
2149
+ console.warn(`migrateLegacyPlan: 0 phases parsed from ${lines.length} lines. First 3 lines: ${lines.slice(0, 3).join(" | ")}`);
2150
+ migrationStatus = "migration_failed";
2151
+ phases.push({
2152
+ id: 1,
2153
+ name: "Migration Failed",
2154
+ status: "blocked",
2155
+ tasks: [
2156
+ {
2157
+ id: "1.1",
2158
+ phase: 1,
2159
+ status: "blocked",
2160
+ size: "large",
2161
+ description: "Review and restructure plan manually",
2162
+ depends: [],
2163
+ files_touched: [],
2164
+ blocked_reason: "Legacy plan could not be parsed automatically"
2165
+ }
2166
+ ]
2167
+ });
2168
+ }
2169
+ phases.sort((a, b) => a.id - b.id);
2170
+ const plan = {
2171
+ schema_version: "1.0.0",
2172
+ title,
2173
+ swarm,
2174
+ current_phase: currentPhaseNum,
2175
+ phases,
2176
+ migration_status: migrationStatus
2177
+ };
2178
+ return plan;
2179
+ }
2180
+
2181
+ // src/evidence/manager.ts
2182
+ import {
2183
+ mkdirSync as mkdirSync3,
2184
+ readdirSync as readdirSync3,
2185
+ realpathSync,
2186
+ rmSync,
2187
+ statSync
2188
+ } from "fs";
2189
+ import * as fs3 from "fs/promises";
2190
+ import * as path4 from "path";
2191
+
2192
+ // src/config/evidence-schema.ts
2193
+ var EVIDENCE_MAX_JSON_BYTES = 500 * 1024;
2194
+ var EVIDENCE_MAX_PATCH_BYTES = 5 * 1024 * 1024;
2195
+ var EVIDENCE_MAX_TASK_BYTES = 20 * 1024 * 1024;
2196
+ var EvidenceTypeSchema = exports_external.enum([
2197
+ "review",
2198
+ "test",
2199
+ "diff",
2200
+ "approval",
2201
+ "note",
2202
+ "retrospective",
2203
+ "syntax",
2204
+ "placeholder",
2205
+ "sast",
2206
+ "sbom",
2207
+ "build",
2208
+ "quality_budget",
2209
+ "secretscan",
2210
+ "mutation-gate",
2211
+ "drift-verification",
2212
+ "hallucination-verification"
2213
+ ]);
2214
+ var EvidenceVerdictSchema = exports_external.enum([
2215
+ "pass",
2216
+ "fail",
2217
+ "warn",
2218
+ "skip",
2219
+ "approved",
2220
+ "rejected",
2221
+ "info"
2222
+ ]);
2223
+ var BaseEvidenceSchema = exports_external.object({
2224
+ task_id: exports_external.string().min(1),
2225
+ type: EvidenceTypeSchema,
2226
+ timestamp: exports_external.string().datetime(),
2227
+ agent: exports_external.string().min(1),
2228
+ verdict: EvidenceVerdictSchema,
2229
+ summary: exports_external.string().min(1),
2230
+ metadata: exports_external.record(exports_external.string(), exports_external.unknown()).optional()
2231
+ });
2232
+ var ReviewEvidenceSchema = BaseEvidenceSchema.extend({
2233
+ type: exports_external.literal("review"),
2234
+ risk: exports_external.enum(["low", "medium", "high", "critical"]),
2235
+ issues: exports_external.array(exports_external.object({
2236
+ severity: exports_external.enum(["error", "warning", "info"]),
2237
+ message: exports_external.string().min(1),
2238
+ file: exports_external.string().optional(),
2239
+ line: exports_external.number().int().optional()
2240
+ })).default([])
2241
+ });
2242
+ var TestEvidenceSchema = BaseEvidenceSchema.extend({
2243
+ type: exports_external.literal("test"),
2244
+ tests_passed: exports_external.number().int().min(0),
2245
+ tests_failed: exports_external.number().int().min(0),
2246
+ test_file: exports_external.string().optional(),
2247
+ failures: exports_external.array(exports_external.object({
2248
+ name: exports_external.string().min(1),
2249
+ message: exports_external.string().min(1)
2250
+ })).default([])
2251
+ });
2252
+ var DiffEvidenceSchema = BaseEvidenceSchema.extend({
2253
+ type: exports_external.literal("diff"),
2254
+ files_changed: exports_external.array(exports_external.string()).default([]),
2255
+ additions: exports_external.number().int().min(0).default(0),
2256
+ deletions: exports_external.number().int().min(0).default(0),
2257
+ patch_path: exports_external.string().optional()
2258
+ });
2259
+ var ApprovalEvidenceSchema = BaseEvidenceSchema.extend({
2260
+ type: exports_external.literal("approval")
2261
+ });
2262
+ var NoteEvidenceSchema = BaseEvidenceSchema.extend({
2263
+ type: exports_external.literal("note")
2264
+ });
2265
+ var RetrospectiveEvidenceSchema = BaseEvidenceSchema.extend({
2266
+ type: exports_external.literal("retrospective"),
2267
+ phase_number: exports_external.number().int().min(1).max(99),
2268
+ total_tool_calls: exports_external.number().int().min(0).max(9999),
2269
+ coder_revisions: exports_external.number().int().min(0).max(999),
2270
+ reviewer_rejections: exports_external.number().int().min(0).max(999),
2271
+ loop_detections: exports_external.number().int().min(0).max(9999).optional(),
2272
+ circuit_breaker_trips: exports_external.number().int().min(0).max(9999).optional(),
2273
+ test_failures: exports_external.number().int().min(0).max(9999),
2274
+ security_findings: exports_external.number().int().min(0).max(999),
2275
+ integration_issues: exports_external.number().int().min(0).max(999),
2276
+ task_count: exports_external.number().int().min(1).max(9999),
2277
+ task_complexity: exports_external.enum(["trivial", "simple", "moderate", "complex"]),
2278
+ top_rejection_reasons: exports_external.array(exports_external.string()).default([]),
2279
+ lessons_learned: exports_external.array(exports_external.string()).max(5).default([]),
2280
+ user_directives: exports_external.array(exports_external.object({
2281
+ directive: exports_external.string().min(1),
2282
+ category: exports_external.enum([
2283
+ "tooling",
2284
+ "code_style",
2285
+ "architecture",
2286
+ "process",
2287
+ "other"
2288
+ ]),
2289
+ scope: exports_external.enum(["session", "project", "global"])
2290
+ })).default([]),
2291
+ approaches_tried: exports_external.array(exports_external.object({
2292
+ approach: exports_external.string().min(1),
2293
+ result: exports_external.enum(["success", "failure", "partial"]),
2294
+ abandoned_reason: exports_external.string().optional()
2295
+ })).max(10).default([]),
2296
+ error_taxonomy: exports_external.array(exports_external.enum([
2297
+ "planning_error",
2298
+ "interface_mismatch",
2299
+ "logic_error",
2300
+ "scope_creep",
2301
+ "gate_evasion"
2302
+ ])).default([])
2303
+ });
2304
+ var SyntaxEvidenceSchema = BaseEvidenceSchema.extend({
2305
+ type: exports_external.literal("syntax"),
2306
+ files_checked: exports_external.number().int(),
2307
+ files_failed: exports_external.number().int(),
2308
+ skipped_count: exports_external.number().int().default(0),
2309
+ files: exports_external.array(exports_external.object({
2310
+ path: exports_external.string(),
2311
+ language: exports_external.string(),
2312
+ ok: exports_external.boolean(),
2313
+ errors: exports_external.array(exports_external.object({
2314
+ line: exports_external.number().int(),
2315
+ column: exports_external.number().int(),
2316
+ message: exports_external.string()
2317
+ })).default([]),
2318
+ skipped_reason: exports_external.string().optional()
2319
+ })).default([])
2320
+ });
2321
+ var PlaceholderEvidenceSchema = BaseEvidenceSchema.extend({
2322
+ type: exports_external.literal("placeholder"),
2323
+ findings: exports_external.array(exports_external.object({
2324
+ path: exports_external.string(),
2325
+ line: exports_external.number().int(),
2326
+ kind: exports_external.enum(["comment", "string", "function_body", "other"]),
2327
+ excerpt: exports_external.string(),
2328
+ rule_id: exports_external.string()
2329
+ })).default([]),
2330
+ files_scanned: exports_external.number().int(),
2331
+ files_with_findings: exports_external.number().int(),
2332
+ findings_count: exports_external.number().int()
2333
+ });
2334
+ var SastFindingSchema = exports_external.object({
2335
+ rule_id: exports_external.string(),
2336
+ severity: exports_external.enum(["critical", "high", "medium", "low"]),
2337
+ message: exports_external.string(),
2338
+ location: exports_external.object({
2339
+ file: exports_external.string(),
2340
+ line: exports_external.number().int(),
2341
+ column: exports_external.number().int().optional()
2342
+ }),
2343
+ remediation: exports_external.string().optional()
2344
+ });
2345
+ var SastEvidenceSchema = BaseEvidenceSchema.extend({
2346
+ type: exports_external.literal("sast"),
2347
+ findings: exports_external.array(SastFindingSchema).default([]),
2348
+ engine: exports_external.enum(["tier_a", "tier_a+tier_b"]),
2349
+ files_scanned: exports_external.number().int(),
2350
+ findings_count: exports_external.number().int(),
2351
+ findings_by_severity: exports_external.object({
2352
+ critical: exports_external.number().int(),
2353
+ high: exports_external.number().int(),
2354
+ medium: exports_external.number().int(),
2355
+ low: exports_external.number().int()
2356
+ }),
2357
+ new_findings: exports_external.array(SastFindingSchema).optional(),
2358
+ pre_existing_findings: exports_external.array(SastFindingSchema).optional(),
2359
+ baseline_used: exports_external.boolean().optional()
2360
+ });
2361
+ var SbomEvidenceSchema = BaseEvidenceSchema.extend({
2362
+ type: exports_external.literal("sbom"),
2363
+ components: exports_external.array(exports_external.object({
2364
+ name: exports_external.string(),
2365
+ version: exports_external.string(),
2366
+ type: exports_external.enum(["library", "framework", "application"]),
2367
+ purl: exports_external.string().optional(),
2368
+ license: exports_external.string().optional()
2369
+ })).default([]),
2370
+ metadata: exports_external.object({
2371
+ timestamp: exports_external.string().datetime(),
2372
+ tool: exports_external.string(),
2373
+ tool_version: exports_external.string()
2374
+ }),
2375
+ files: exports_external.array(exports_external.string()),
2376
+ components_count: exports_external.number().int(),
2377
+ output_path: exports_external.string()
2378
+ });
2379
+ var BuildEvidenceSchema = BaseEvidenceSchema.extend({
2380
+ type: exports_external.literal("build"),
2381
+ runs: exports_external.array(exports_external.object({
2382
+ kind: exports_external.enum(["build", "typecheck", "test"]),
2383
+ command: exports_external.string(),
2384
+ cwd: exports_external.string(),
2385
+ exit_code: exports_external.number().int(),
2386
+ duration_ms: exports_external.number().int(),
2387
+ stdout_tail: exports_external.string(),
2388
+ stderr_tail: exports_external.string()
2389
+ })).default([]),
2390
+ files_scanned: exports_external.number().int(),
2391
+ runs_count: exports_external.number().int(),
2392
+ failed_count: exports_external.number().int(),
2393
+ skipped_reason: exports_external.string().optional()
2394
+ });
2395
+ var QualityBudgetEvidenceSchema = BaseEvidenceSchema.extend({
2396
+ type: exports_external.literal("quality_budget"),
2397
+ metrics: exports_external.object({
2398
+ complexity_delta: exports_external.number(),
2399
+ public_api_delta: exports_external.number(),
2400
+ duplication_ratio: exports_external.number(),
2401
+ test_to_code_ratio: exports_external.number()
2402
+ }),
2403
+ thresholds: exports_external.object({
2404
+ max_complexity_delta: exports_external.number(),
2405
+ max_public_api_delta: exports_external.number(),
2406
+ max_duplication_ratio: exports_external.number(),
2407
+ min_test_to_code_ratio: exports_external.number()
2408
+ }),
2409
+ violations: exports_external.array(exports_external.object({
2410
+ type: exports_external.enum(["complexity", "api", "duplication", "test_ratio"]),
2411
+ message: exports_external.string(),
2412
+ severity: exports_external.enum(["error", "warning"]),
2413
+ files: exports_external.array(exports_external.string())
2414
+ })).default([]),
2415
+ files_analyzed: exports_external.array(exports_external.string())
2416
+ });
2417
+ var SecretscanEvidenceSchema = BaseEvidenceSchema.extend({
2418
+ type: exports_external.literal("secretscan"),
2419
+ findings_count: exports_external.number().int().min(0).default(0),
2420
+ scan_directory: exports_external.string().optional(),
2421
+ files_scanned: exports_external.number().int().min(0).default(0),
2422
+ skipped_files: exports_external.number().int().min(0).default(0)
2423
+ });
2424
+ var GateEvidenceBaseSchema = exports_external.object({
2425
+ task_id: exports_external.string().optional(),
2426
+ agent: exports_external.string().optional(),
2427
+ timestamp: exports_external.string().datetime(),
2428
+ summary: exports_external.string().min(1),
2429
+ metadata: exports_external.record(exports_external.string(), exports_external.unknown()).optional()
2430
+ });
2431
+ var MutationGateEvidenceSchema = GateEvidenceBaseSchema.extend({
2432
+ type: exports_external.literal("mutation-gate"),
2433
+ verdict: exports_external.enum(["pass", "warn", "fail", "skip"]),
2434
+ killRate: exports_external.number().min(0).max(1).optional(),
2435
+ adjustedKillRate: exports_external.number().min(0).max(1).optional(),
2436
+ survivedMutants: exports_external.string().optional()
2437
+ });
2438
+ var DriftVerificationEvidenceSchema = GateEvidenceBaseSchema.extend({
2439
+ type: exports_external.literal("drift-verification"),
2440
+ verdict: exports_external.enum(["approved", "rejected"]),
2441
+ requirementCoverage: exports_external.string().optional()
2442
+ });
2443
+ var HallucinationVerificationEvidenceSchema = GateEvidenceBaseSchema.extend({
2444
+ type: exports_external.literal("hallucination-verification"),
2445
+ verdict: exports_external.enum(["approved", "rejected"]),
2446
+ findings: exports_external.string().optional()
2447
+ });
2448
+ var EvidenceSchema = exports_external.discriminatedUnion("type", [
2449
+ ReviewEvidenceSchema,
2450
+ TestEvidenceSchema,
2451
+ DiffEvidenceSchema,
2452
+ ApprovalEvidenceSchema,
2453
+ NoteEvidenceSchema,
2454
+ RetrospectiveEvidenceSchema,
2455
+ SyntaxEvidenceSchema,
2456
+ PlaceholderEvidenceSchema,
2457
+ SastEvidenceSchema,
2458
+ SbomEvidenceSchema,
2459
+ BuildEvidenceSchema,
2460
+ QualityBudgetEvidenceSchema,
2461
+ SecretscanEvidenceSchema,
2462
+ MutationGateEvidenceSchema,
2463
+ DriftVerificationEvidenceSchema,
2464
+ HallucinationVerificationEvidenceSchema
2465
+ ]);
2466
+ var EvidenceBundleSchema = exports_external.object({
2467
+ schema_version: exports_external.literal("1.0.0"),
2468
+ task_id: exports_external.string().min(1),
2469
+ entries: exports_external.array(EvidenceSchema).default([]),
2470
+ created_at: exports_external.string().datetime(),
2471
+ updated_at: exports_external.string().datetime()
2472
+ });
2473
+
2474
+ // src/evidence/manager.ts
2475
+ var VALID_EVIDENCE_TYPES = [
2476
+ "review",
2477
+ "test",
2478
+ "diff",
2479
+ "approval",
2480
+ "note",
2481
+ "retrospective",
2482
+ "syntax",
2483
+ "placeholder",
2484
+ "sast",
2485
+ "sbom",
2486
+ "build",
2487
+ "quality_budget",
2488
+ "secretscan"
2489
+ ];
2490
+ function isValidEvidenceType(type) {
2491
+ return VALID_EVIDENCE_TYPES.includes(type);
2492
+ }
2493
+ var sanitizeTaskId2 = sanitizeTaskId;
2494
+ var MAX_DEPTH = 20;
2495
+ var PROJECT_INDICATORS = [
2496
+ "package.json",
2497
+ ".git",
2498
+ ".opencode",
2499
+ "Cargo.toml",
2500
+ "go.mod",
2501
+ "pyproject.toml",
2502
+ "Gemfile",
2503
+ "composer.json",
2504
+ "pom.xml",
2505
+ "build.gradle",
2506
+ "CMakeLists.txt"
2507
+ ];
2508
+ function validateProjectRoot(directory) {
2509
+ let resolved;
2510
+ try {
2511
+ resolved = realpathSync(directory);
2512
+ } catch {
2513
+ warn(`[evidence] Cannot canonicalize directory "${directory}" \u2014 failing closed`);
2514
+ throw new Error(`Cannot verify project root for "${directory}" \u2014 directory may not exist or is inaccessible`);
2515
+ }
2516
+ let current = resolved;
2517
+ let depth = 0;
2518
+ while (true) {
2519
+ if (depth >= MAX_DEPTH)
2520
+ break;
2521
+ depth++;
2522
+ const parent = path4.dirname(current);
2523
+ if (parent === current)
2524
+ break;
2525
+ const parentSwarm = path4.join(parent, ".swarm");
2526
+ try {
2527
+ if (statSync(parentSwarm).isDirectory()) {
2528
+ let hasProjectIndicator = false;
2529
+ for (const indicator of PROJECT_INDICATORS) {
2530
+ try {
2531
+ const indicatorStat = statSync(path4.join(parent, indicator));
2532
+ if (indicatorStat.isFile() || indicatorStat.isDirectory()) {
2533
+ hasProjectIndicator = true;
2534
+ break;
2535
+ }
2536
+ } catch (error) {
2537
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {} else {
2538
+ hasProjectIndicator = true;
2539
+ break;
2540
+ }
2541
+ }
2542
+ }
2543
+ if (hasProjectIndicator) {
2544
+ warn(`[evidence] Rejecting write to subdirectory "${resolved}" \u2014 parent "${parent}" already contains .swarm/`);
2545
+ throw new Error(`Cannot write evidence in "${resolved}" \u2014 parent directory "${parent}" already contains a .swarm/ folder. Evidence must be written to the project root.`);
2546
+ }
2547
+ }
2548
+ } catch (error) {
2549
+ if (error instanceof Error && error.message.startsWith("Cannot write evidence")) {
2550
+ throw error;
2551
+ }
2552
+ }
2553
+ current = parent;
2554
+ }
2555
+ }
2556
+ async function saveEvidence(directory, taskId, evidence) {
2557
+ _internals3.validateProjectRoot(directory);
2558
+ const sanitizedTaskId = sanitizeTaskId2(taskId);
2559
+ const relativePath = path4.join("evidence", sanitizedTaskId, "evidence.json");
2560
+ validateSwarmPath(directory, relativePath);
2561
+ return withEvidenceLock(directory, relativePath, "evidence-manager", sanitizedTaskId, async () => {
2562
+ const evidencePath = validateSwarmPath(directory, relativePath);
2563
+ const evidenceDir = path4.dirname(evidencePath);
2564
+ let bundle;
2565
+ const existingContent = await readSwarmFileAsync(directory, relativePath);
2566
+ if (existingContent !== null) {
2567
+ try {
2568
+ const parsed = JSON.parse(existingContent);
2569
+ bundle = EvidenceBundleSchema.parse(parsed);
2570
+ } catch (error) {
2571
+ warn(`Existing evidence bundle invalid for task ${sanitizedTaskId}, creating new: ${error instanceof Error ? error.message : String(error)}`);
2572
+ const now = new Date().toISOString();
2573
+ bundle = {
2574
+ schema_version: "1.0.0",
2575
+ task_id: sanitizedTaskId,
2576
+ entries: [],
2577
+ created_at: now,
2578
+ updated_at: now
2579
+ };
2580
+ }
2581
+ } else {
2582
+ const now = new Date().toISOString();
2583
+ bundle = {
2584
+ schema_version: "1.0.0",
2585
+ task_id: sanitizedTaskId,
2586
+ entries: [],
2587
+ created_at: now,
2588
+ updated_at: now
2589
+ };
2590
+ }
2591
+ const MAX_BUNDLE_ENTRIES = 100;
2592
+ let entries = [...bundle.entries, evidence];
2593
+ if (entries.length > MAX_BUNDLE_ENTRIES) {
2594
+ entries = entries.slice(entries.length - MAX_BUNDLE_ENTRIES);
2595
+ }
2596
+ const updatedBundle = {
2597
+ ...bundle,
2598
+ entries,
2599
+ updated_at: new Date().toISOString()
2600
+ };
2601
+ const bundleJson = JSON.stringify(updatedBundle);
2602
+ if (bundleJson.length > EVIDENCE_MAX_JSON_BYTES) {
2603
+ throw new Error(`Evidence bundle size (${bundleJson.length} bytes) exceeds maximum (${EVIDENCE_MAX_JSON_BYTES} bytes)`);
2604
+ }
2605
+ mkdirSync3(evidenceDir, { recursive: true });
2606
+ const tempPath = path4.join(evidenceDir, `evidence.json.tmp.${Date.now()}.${process.pid}`);
2607
+ try {
2608
+ await bunWrite(tempPath, bundleJson);
2609
+ await fs3.rename(tempPath, evidencePath);
2610
+ } catch (error) {
2611
+ try {
2612
+ rmSync(tempPath, { force: true });
2613
+ } catch {}
2614
+ throw error;
2615
+ }
2616
+ return updatedBundle;
2617
+ });
2618
+ }
2619
+ function isFlatRetrospective(parsed) {
2620
+ return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) && parsed.type === "retrospective" && !parsed.schema_version;
2621
+ }
2622
+ var LEGACY_TASK_COMPLEXITY_MAP = {
2623
+ low: "simple",
2624
+ medium: "moderate",
2625
+ high: "complex"
2626
+ };
2627
+ function remapLegacyTaskComplexity(entry) {
2628
+ const taskComplexity = entry.task_complexity;
2629
+ if (typeof taskComplexity === "string" && taskComplexity in LEGACY_TASK_COMPLEXITY_MAP) {
2630
+ return {
2631
+ ...entry,
2632
+ task_complexity: LEGACY_TASK_COMPLEXITY_MAP[taskComplexity]
2633
+ };
2634
+ }
2635
+ return entry;
2636
+ }
2637
+ function wrapFlatRetrospective(flatEntry, taskId) {
2638
+ const now = new Date().toISOString();
2639
+ const remappedEntry = remapLegacyTaskComplexity(flatEntry);
2640
+ return {
2641
+ schema_version: "1.0.0",
2642
+ task_id: remappedEntry.task_id ?? taskId,
2643
+ created_at: remappedEntry.timestamp ?? now,
2644
+ updated_at: remappedEntry.timestamp ?? now,
2645
+ entries: [remappedEntry]
2646
+ };
2647
+ }
2648
+ async function loadEvidence(directory, taskId) {
2649
+ const sanitizedTaskId = sanitizeTaskId2(taskId);
2650
+ const relativePath = path4.join("evidence", sanitizedTaskId, "evidence.json");
2651
+ if (relativePath.length > 4096) {
2652
+ return { status: "not_found" };
2653
+ }
2654
+ const evidencePath = validateSwarmPath(directory, relativePath);
2655
+ const content = await readSwarmFileAsync(directory, relativePath);
2656
+ if (content === null) {
2657
+ return { status: "not_found" };
2658
+ }
2659
+ let parsed;
2660
+ try {
2661
+ parsed = JSON.parse(content);
2662
+ } catch {
2663
+ return { status: "invalid_schema", errors: ["Invalid JSON"] };
2664
+ }
2665
+ if (isFlatRetrospective(parsed)) {
2666
+ const wrappedBundle = _internals3.wrapFlatRetrospective(parsed, sanitizedTaskId);
2667
+ try {
2668
+ const validated = EvidenceBundleSchema.parse(wrappedBundle);
2669
+ try {
2670
+ await withEvidenceLock(directory, relativePath, "evidence-loader", sanitizedTaskId, async () => {
2671
+ const evidenceDir = path4.dirname(evidencePath);
2672
+ const bundleJson = JSON.stringify(validated);
2673
+ const tempPath = path4.join(evidenceDir, `evidence.json.tmp.${Date.now()}.${process.pid}`);
2674
+ try {
2675
+ await bunWrite(tempPath, bundleJson);
2676
+ await fs3.rename(tempPath, evidencePath);
2677
+ } catch (writeError) {
2678
+ try {
2679
+ rmSync(tempPath, { force: true });
2680
+ } catch {}
2681
+ warn(`Failed to persist repaired flat retrospective for task ${sanitizedTaskId}: ${writeError instanceof Error ? writeError.message : String(writeError)}`);
2682
+ }
2683
+ });
2684
+ } catch (lockErr) {
2685
+ warn(`Evidence lock failed during flat-retrospective write-back for task ${sanitizedTaskId}: ${lockErr instanceof Error ? lockErr.message : String(lockErr)}`);
2686
+ }
2687
+ return { status: "found", bundle: validated };
2688
+ } catch (error) {
2689
+ warn(`Wrapped flat retrospective failed validation for task ${sanitizedTaskId}: ${error instanceof Error ? error.message : String(error)}`);
2690
+ const errors = error instanceof ZodError ? error.issues.map((e) => `${e.path.join(".")}: ${e.message}`) : [error instanceof Error ? error.message : String(error)];
2691
+ return { status: "invalid_schema", errors };
2692
+ }
2693
+ }
2694
+ try {
2695
+ const validated = EvidenceBundleSchema.parse(parsed);
2696
+ return { status: "found", bundle: validated };
2697
+ } catch (error) {
2698
+ warn(`Evidence bundle validation failed for task ${sanitizedTaskId}: ${error instanceof Error ? error.message : String(error)}`);
2699
+ const errors = error instanceof ZodError ? error.issues.map((e) => `${e.path.join(".")}: ${e.message}`) : [error instanceof Error ? error.message : String(error)];
2700
+ return { status: "invalid_schema", errors };
2701
+ }
2702
+ }
2703
+ async function listEvidenceTaskIds(directory) {
2704
+ const evidenceBasePath = validateSwarmPath(directory, "evidence");
2705
+ try {
2706
+ statSync(evidenceBasePath);
2707
+ } catch {
2708
+ return [];
2709
+ }
2710
+ let entries;
2711
+ try {
2712
+ entries = readdirSync3(evidenceBasePath);
2713
+ } catch {
2714
+ return [];
2715
+ }
2716
+ const taskIds = [];
2717
+ for (const entry of entries) {
2718
+ const entryPath = path4.join(evidenceBasePath, entry);
2719
+ try {
2720
+ const stats = statSync(entryPath);
2721
+ if (!stats.isDirectory()) {
2722
+ continue;
2723
+ }
2724
+ sanitizeTaskId2(entry);
2725
+ taskIds.push(entry);
2726
+ } catch (error) {
2727
+ if (error instanceof Error && !error.message.startsWith("Invalid task ID")) {
2728
+ warn(`Error reading evidence entry '${entry}': ${error.message}`);
2729
+ }
2730
+ }
2731
+ }
2732
+ return taskIds.sort();
2733
+ }
2734
+ async function deleteEvidence(directory, taskId) {
2735
+ const sanitizedTaskId = sanitizeTaskId2(taskId);
2736
+ const relativePath = path4.join("evidence", sanitizedTaskId);
2737
+ const evidenceDir = validateSwarmPath(directory, relativePath);
2738
+ try {
2739
+ statSync(evidenceDir);
2740
+ } catch {
2741
+ return false;
2742
+ }
2743
+ try {
2744
+ rmSync(evidenceDir, { recursive: true, force: true });
2745
+ return true;
2746
+ } catch (error) {
2747
+ warn(`Failed to delete evidence for task ${sanitizedTaskId}: ${error instanceof Error ? error.message : String(error)}`);
2748
+ return false;
2749
+ }
2750
+ }
2751
+ async function checkRequirementCoverage(phase, directory) {
2752
+ const relativePath = path4.join("evidence", `req-coverage-phase-${phase}.json`);
2753
+ const absolutePath = path4.resolve(directory, ".swarm", relativePath);
2754
+ try {
2755
+ await fs3.access(absolutePath);
2756
+ return { exists: true, path: absolutePath };
2757
+ } catch {
2758
+ return { exists: false, path: absolutePath };
2759
+ }
2760
+ }
2761
+ async function archiveEvidence(directory, maxAgeDays, maxBundles) {
2762
+ const taskIds = await _internals3.listEvidenceTaskIds(directory);
2763
+ const cutoffDate = new Date;
2764
+ cutoffDate.setDate(cutoffDate.getDate() - maxAgeDays);
2765
+ const cutoffIso = cutoffDate.toISOString();
2766
+ const archived = [];
2767
+ const remainingBundles = [];
2768
+ for (const taskId of taskIds) {
2769
+ const result = await _internals3.loadEvidence(directory, taskId);
2770
+ if (result.status !== "found") {
2771
+ continue;
2772
+ }
2773
+ if (result.bundle.updated_at < cutoffIso) {
2774
+ const deleted = await deleteEvidence(directory, taskId);
2775
+ if (deleted) {
2776
+ archived.push(taskId);
2777
+ }
2778
+ } else {
2779
+ remainingBundles.push({
2780
+ taskId,
2781
+ updatedAt: result.bundle.updated_at
2782
+ });
2783
+ }
2784
+ }
2785
+ if (maxBundles !== undefined && remainingBundles.length > maxBundles) {
2786
+ remainingBundles.sort((a, b) => a.updatedAt.localeCompare(b.updatedAt));
2787
+ const toDelete = remainingBundles.length - maxBundles;
2788
+ for (let i = 0;i < toDelete; i++) {
2789
+ const deleted = await deleteEvidence(directory, remainingBundles[i].taskId);
2790
+ if (deleted) {
2791
+ archived.push(remainingBundles[i].taskId);
2792
+ }
2793
+ }
2794
+ }
2795
+ return archived;
2796
+ }
2797
+ var _internals3 = {
2798
+ wrapFlatRetrospective,
2799
+ loadEvidence,
2800
+ listEvidenceTaskIds,
2801
+ validateProjectRoot
2802
+ };
2803
+
2804
+ // src/evidence/gate-bridge.ts
2805
+ var GATE_EVIDENCE_TYPE_BY_GATE = {
2806
+ reviewer: "review",
2807
+ test_engineer: "test"
2808
+ };
2809
+ async function readDurableGateEvidence(directory, taskId) {
2810
+ try {
2811
+ return await readTaskEvidence(directory, taskId);
2812
+ } catch {
2813
+ return null;
2814
+ }
2815
+ }
2816
+ function getDurableGateEvidenceStatus(evidence) {
2817
+ if (!evidence?.gates || typeof evidence.gates !== "object") {
2818
+ return {
2819
+ isComplete: false,
2820
+ missingGates: [],
2821
+ evidenceExists: evidence != null,
2822
+ invalid: false
2823
+ };
2824
+ }
2825
+ if (!Array.isArray(evidence.required_gates) || evidence.required_gates.length === 0) {
2826
+ return {
2827
+ isComplete: false,
2828
+ missingGates: ["required_gates"],
2829
+ evidenceExists: true,
2830
+ invalid: false
2831
+ };
2832
+ }
2833
+ const missingGates = evidence.required_gates.filter((gate) => evidence.gates[gate] == null);
2834
+ return {
2835
+ isComplete: missingGates.length === 0,
2836
+ missingGates,
2837
+ evidenceExists: true,
2838
+ invalid: false
2839
+ };
2840
+ }
2841
+ async function getDurableGateEvidenceStatusForTask(directory, taskId) {
2842
+ if (!isValidTaskId(taskId)) {
2843
+ return {
2844
+ isComplete: false,
2845
+ missingGates: [],
2846
+ evidenceExists: false,
2847
+ invalid: false
2848
+ };
2849
+ }
2850
+ try {
2851
+ return getDurableGateEvidenceStatus(readTaskEvidenceRaw(directory, taskId));
2852
+ } catch {
2853
+ return {
2854
+ isComplete: false,
2855
+ missingGates: ["invalid_gate_evidence"],
2856
+ evidenceExists: true,
2857
+ invalid: true
2858
+ };
2859
+ }
2860
+ }
2861
+ function gateEvidenceToEntry(taskId, gate, type, evidence) {
2862
+ const gateRecord = evidence.gates[gate];
2863
+ if (!gateRecord) {
2864
+ return null;
2865
+ }
2866
+ const base = {
2867
+ task_id: taskId,
2868
+ timestamp: gateRecord.timestamp,
2869
+ agent: gateRecord.agent || gate,
2870
+ verdict: "pass",
2871
+ summary: `Gate evidence recorded by ${gate}`,
2872
+ metadata: { source: "durable_gate_evidence", gate }
2873
+ };
2874
+ if (type === "review") {
2875
+ return {
2876
+ ...base,
2877
+ type,
2878
+ risk: "low",
2879
+ issues: []
2880
+ };
2881
+ }
2882
+ if (type === "approval") {
2883
+ return {
2884
+ ...base,
2885
+ type
2886
+ };
2887
+ }
2888
+ return {
2889
+ ...base,
2890
+ type,
2891
+ tests_passed: 0,
2892
+ tests_failed: 0,
2893
+ failures: []
2894
+ };
2895
+ }
2896
+ function mergeDurableGateEntriesFromEvidence(taskId, entries, evidence) {
2897
+ if (!evidence?.gates) {
2898
+ return entries;
2899
+ }
2900
+ const merged = [...entries];
2901
+ for (const gate of Object.keys(evidence.gates).sort()) {
2902
+ const type = GATE_EVIDENCE_TYPE_BY_GATE[gate] ?? "approval";
2903
+ if ((type === "review" || type === "test") && merged.some((entry2) => entry2.type === type)) {
2904
+ continue;
2905
+ }
2906
+ const entry = gateEvidenceToEntry(taskId, gate, type, evidence);
2907
+ if (entry) {
2908
+ merged.push(entry);
2909
+ }
2910
+ }
2911
+ return merged;
2912
+ }
2913
+
2914
+ export { PlanSchema, loadSddStatusSync, buildOpenSpecProjectionSync, readEffectiveSpecSync, writeProjectedSpecSync, 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 };