opencode-swarm 7.167.1 → 7.168.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 (126) hide show
  1. package/binaries/win32-arm64/swarm-sandbox-runner.exe +0 -0
  2. package/binaries/win32-x64/swarm-sandbox-runner.exe +0 -0
  3. package/dist/agents/project-context.d.ts +12 -8
  4. package/dist/cli/{coder-settlement-2ye4s5xr.js → coder-settlement-a35acvj1.js} +16 -16
  5. package/dist/cli/{config-doctor-b0k8eyd6.js → config-doctor-9csx775x.js} +3 -3
  6. package/dist/cli/{core-xgzgjk8s.js → core-agk5s7dm.js} +4 -4
  7. package/dist/cli/{curation-policy-bvxf5pxv.js → curation-policy-mgwv8h2k.js} +7 -7
  8. package/dist/cli/{curator-jpvzrxcv.js → curator-2fcagjyk.js} +45 -45
  9. package/dist/cli/{curator-drift-20m2ab5w.js → curator-drift-k0zrnm09.js} +8 -8
  10. package/dist/cli/curator-llm-factory-qaxmyy46.js +71 -0
  11. package/dist/cli/{dispatch-tmb929w8.js → dispatch-8ks9dyk5.js} +244 -70
  12. package/dist/cli/{evidence-summary-service-74az43cb.js → evidence-summary-service-4s6cje60.js} +22 -22
  13. package/dist/cli/{explorer-zvk5kbcb.js → explorer-2cv64zfj.js} +1 -1
  14. package/dist/cli/{gate-evidence-f26h2gn3.js → gate-evidence-saja4dan.js} +11 -11
  15. package/dist/cli/guardrail-explain-z41q6sf0.js +72 -0
  16. package/dist/cli/{guardrail-log-pnye3xw8.js → guardrail-log-0h24spq0.js} +9 -9
  17. package/dist/cli/{guardrail-reset-0da7q8ap.js → guardrail-reset-cwbe47k5.js} +44 -44
  18. package/dist/cli/hive-promoter-esxtcvr1.js +86 -0
  19. package/dist/cli/{index-bv8ha57q.js → index-00zx94vn.js} +6 -6
  20. package/dist/cli/{index-9e6fdz2h.js → index-0mhftbpp.js} +1 -1
  21. package/dist/cli/{index-7qefgq4j.js → index-1xnse449.js} +11 -1
  22. package/dist/cli/{index-qyr17fbz.js → index-21ree67h.js} +62 -106
  23. package/dist/cli/{index-q265ytw6.js → index-30h1pk05.js} +4 -4
  24. package/dist/cli/{index-f8xk5q5j.js → index-3dywbkyr.js} +4 -4
  25. package/dist/cli/{index-2w0beg3s.js → index-4r5pmtvk.js} +5 -5
  26. package/dist/cli/{index-69y9p6dh.js → index-54w1564x.js} +2 -2
  27. package/dist/cli/{index-251vrmge.js → index-5zqakgp4.js} +4 -4
  28. package/dist/cli/{index-bzdc4b7t.js → index-64phnth4.js} +6 -6
  29. package/dist/cli/{index-fmzz6aw7.js → index-8f0kscpg.js} +4 -4
  30. package/dist/cli/{index-t8tw1bhs.js → index-8ybjqg51.js} +2 -2
  31. package/dist/cli/{index-9ss2m4rs.js → index-9br68j86.js} +1 -1
  32. package/dist/cli/{index-4rhhvd1a.js → index-a1w58b47.js} +1 -1
  33. package/dist/cli/{index-qd619x3r.js → index-a8xbvsse.js} +2 -2
  34. package/dist/cli/{index-mgc3c483.js → index-an8b5gda.js} +10 -10
  35. package/dist/cli/{index-9wv6a3jx.js → index-bvqqwx84.js} +2 -2
  36. package/dist/cli/{index-bf6zx8cq.js → index-cyenn3pf.js} +6 -2
  37. package/dist/cli/{index-8x4zwk0t.js → index-ddr97ram.js} +47 -47
  38. package/dist/cli/{index-3nm1b3t2.js → index-dfyv3xkd.js} +8 -8
  39. package/dist/cli/{index-87agfprq.js → index-exatbqwj.js} +13 -4
  40. package/dist/cli/{index-70ha9e09.js → index-fyfbpz4f.js} +1 -1
  41. package/dist/cli/{index-5bd84nxc.js → index-gp9yzr1v.js} +1 -1
  42. package/dist/cli/{index-es13aqdb.js → index-h0e2e3h1.js} +1 -1
  43. package/dist/cli/{index-6d4dc99f.js → index-h439y187.js} +1 -1
  44. package/dist/cli/{index-9bhzbwa4.js → index-j950hqmq.js} +9 -9
  45. package/dist/cli/{index-sahf5yz3.js → index-kear8gb7.js} +11 -6
  46. package/dist/cli/{index-qww0h314.js → index-m35hybf0.js} +1 -1
  47. package/dist/cli/{index-qtd3hrk8.js → index-mr0wn36f.js} +5 -5
  48. package/dist/cli/{index-zwsbegc1.js → index-nprngf8p.js} +7 -7
  49. package/dist/cli/{index-z5gdgdkp.js → index-nsyxj57e.js} +12 -12
  50. package/dist/cli/{index-1aex5zd9.js → index-nwwk3dej.js} +2 -2
  51. package/dist/cli/{index-75f9jj3h.js → index-pyf517sz.js} +2 -2
  52. package/dist/cli/{index-m3tx1q9v.js → index-qhey6vc3.js} +1 -1
  53. package/dist/cli/{index-qd726agw.js → index-qwsh0h2g.js} +3 -3
  54. package/dist/cli/{index-n4fzmqz8.js → index-rdm3px0g.js} +4 -2
  55. package/dist/cli/index-rxcgrgkc.js +116 -0
  56. package/dist/cli/{index-54vzqy47.js → index-s0d5xter.js} +6 -6
  57. package/dist/cli/{index-emwezg3y.js → index-seetnhpx.js} +3 -3
  58. package/dist/cli/{index-0tc0pcmh.js → index-sxt7fgcw.js} +38 -2
  59. package/dist/cli/{index-m21x1bkx.js → index-v3xc4sde.js} +11 -8
  60. package/dist/cli/{index-79wnzc8q.js → index-wnzxcr0z.js} +3 -3
  61. package/dist/cli/{index-rk10x4zr.js → index-wst6n478.js} +106 -6
  62. package/dist/cli/{index-hdsg7yf6.js → index-x7k3dps5.js} +4 -3
  63. package/dist/cli/{index-32bxwzw0.js → index-yevn4jpe.js} +92908 -90093
  64. package/dist/cli/index-yv76j77m.js +2008 -0
  65. package/dist/cli/{index-f6zgz02s.js → index-z6qreakh.js} +7 -7
  66. package/dist/cli/{index-eretcr6t.js → index-ze520xw1.js} +11 -11
  67. package/dist/cli/index.js +44 -44
  68. package/dist/cli/{knowledge-escalator-p2a8xh8x.js → knowledge-escalator-b2tf3sd1.js} +17 -17
  69. package/dist/cli/{knowledge-events-jmh28m8d.js → knowledge-events-bgs2kxe0.js} +15 -15
  70. package/dist/cli/{knowledge-link-w1fwqts4.js → knowledge-link-dr3xnwyd.js} +6 -6
  71. package/dist/cli/{knowledge-store-8gmabtsn.js → knowledge-store-stbmb1b9.js} +7 -7
  72. package/dist/cli/{knowledge-validator-yk579vs4.js → knowledge-validator-zykgnsag.js} +9 -9
  73. package/dist/cli/{model-preflight-b3b5pgsc.js → model-preflight-1ycat9jz.js} +1 -1
  74. package/dist/cli/{pending-delegations-nw7ph53z.js → pending-delegations-n5qavj20.js} +14 -14
  75. package/dist/cli/{pr-subscriptions-9815fxbc.js → pr-subscriptions-gnvmyrng.js} +14 -14
  76. package/dist/cli/{pr-workflow-gate-98a7b6py.js → pr-workflow-gate-cf3ybrws.js} +44 -44
  77. package/dist/cli/{runner-nmrjv7te.js → runner-k0ycgv8x.js} +7 -7
  78. package/dist/cli/{scan-cursor-6zcs0frg.js → scan-cursor-b3mcrx23.js} +10 -10
  79. package/dist/cli/{schema-warvhy9h.js → schema-08049cjb.js} +4 -2
  80. package/dist/cli/{scope-persistence-c2ck77cv.js → scope-persistence-a493gv51.js} +20 -20
  81. package/dist/cli/{skill-generator-jamm1cna.js → skill-generator-2qqzkdpv.js} +19 -19
  82. package/dist/cli/{snapshot-coordination-init-st0xsn3f.js → snapshot-coordination-init-8yt8p7q0.js} +44 -44
  83. package/dist/cli/{sqlite-loader-xrnf6bq8.js → sqlite-loader-amp9z7ty.js} +1 -1
  84. package/dist/cli/{telemetry-ce0tx41h.js → telemetry-2wzapf11.js} +2 -2
  85. package/dist/cli/{workspace-snapshot-0kxj2grt.js → workspace-snapshot-rkgxhj9b.js} +6 -6
  86. package/dist/cli/{worktree-collision-ownership-qra7r8pw.js → worktree-collision-ownership-xttafc4h.js} +17 -17
  87. package/dist/cli/{worktree-isolation-yr9gnf64.js → worktree-isolation-e5q5rewk.js} +45 -45
  88. package/dist/commands/close/align-stage.d.ts +10 -0
  89. package/dist/commands/close/archive-stage.d.ts +34 -0
  90. package/dist/commands/close/clean-stage.d.ts +2 -0
  91. package/dist/commands/close/constants.d.ts +122 -0
  92. package/dist/commands/close/context.d.ts +129 -0
  93. package/dist/commands/close/db-helpers.d.ts +10 -0
  94. package/dist/commands/close/dry-run.d.ts +12 -0
  95. package/dist/commands/close/finalize-stage.d.ts +44 -0
  96. package/dist/commands/close/fs-helpers.d.ts +17 -0
  97. package/dist/commands/close/internals.d.ts +55 -0
  98. package/dist/commands/close/orchestrator.d.ts +32 -0
  99. package/dist/commands/close/wiring.d.ts +2 -0
  100. package/dist/commands/close.d.ts +10 -402
  101. package/dist/commands/reset.d.ts +10 -0
  102. package/dist/commands/rollback.d.ts +9 -0
  103. package/dist/config/schema.d.ts +29 -0
  104. package/dist/db/project-db.d.ts +8 -0
  105. package/dist/db/sqlite-loader.d.ts +2 -1
  106. package/dist/dispatch/spawn-circuit.d.ts +121 -0
  107. package/dist/dispatch/token-bucket.d.ts +46 -0
  108. package/dist/hooks/delegation-gate/dead-lane-reclaim.d.ts +61 -0
  109. package/dist/hooks/delegation-gate/worktree-isolation.d.ts +22 -0
  110. package/dist/hooks/issue-trace.d.ts +3 -0
  111. package/dist/hooks/utils.d.ts +1 -1
  112. package/dist/index.js +811 -726
  113. package/dist/lang/dispatch.d.ts +6 -6
  114. package/dist/plan/ledger-sqlite.d.ts +226 -0
  115. package/dist/plan/ledger.d.ts +24 -0
  116. package/dist/plan/manager.d.ts +19 -0
  117. package/dist/retention/sweep.d.ts +2 -0
  118. package/dist/services/spec-drift-recovery.d.ts +2 -1
  119. package/dist/utils/canonical-root.d.ts +19 -0
  120. package/dist/utils/ephemeral-session-teardown.d.ts +55 -2
  121. package/opencode-swarm.schema.json +41 -0
  122. package/package.json +1 -1
  123. package/dist/cli/curator-llm-factory-zeb0pe2j.js +0 -71
  124. package/dist/cli/guardrail-explain-5vhveypz.js +0 -72
  125. package/dist/cli/hive-promoter-2vj54xc4.js +0 -86
  126. package/dist/cli/index-fenasyvr.js +0 -967
@@ -1,967 +0,0 @@
1
- // @bun
2
- import {
3
- derivePlanId,
4
- derivePlanIdentityHash
5
- } from "./index-6d4dc99f.js";
6
- import {
7
- assertProjectRoot,
8
- withEvidenceLock
9
- } from "./index-2w0beg3s.js";
10
- import {
11
- emit
12
- } from "./index-f6zgz02s.js";
13
- import {
14
- ExecutionProfileSchema,
15
- PlanSchema,
16
- TaskStatusSchema,
17
- init_loader
18
- } from "./index-emwezg3y.js";
19
- import {
20
- criticalWarn,
21
- init_logger,
22
- log
23
- } from "./index-zgwm4ryv.js";
24
-
25
- // src/plan/ledger.ts
26
- import * as crypto from "crypto";
27
- import * as fs from "fs";
28
- import * as path from "path";
29
- init_logger();
30
-
31
- // src/plan/planning-profile.ts
32
- init_loader();
33
- function normalizeExecutionProfileForHash(profile) {
34
- if (!profile) {
35
- return;
36
- }
37
- return {
38
- ...profile,
39
- commit_after_each_completed_task: profile.commit_after_each_completed_task === true ? true : undefined
40
- };
41
- }
42
-
43
- // src/plan/ledger.ts
44
- var LEDGER_SCHEMA_VERSION = "1.1.0";
45
- function resolvePlanEpochIdentity(events, authoritativePlan) {
46
- const planId = derivePlanId(authoritativePlan);
47
- const planIdentityHash = derivePlanIdentityHash(authoritativePlan);
48
- const payloadHash = computePlanLedgerHash(authoritativePlan);
49
- const candidates = extractPlanEpochCandidates(events, planId);
50
- const distinctCandidateKeys = new Set(candidates.map((candidate) => `${candidate.planEpoch}:${candidate.rootEventHash}`));
51
- if (distinctCandidateKeys.size > 1) {
52
- throw new Error(`Conflicting plan epoch metadata detected for ${planId}; refusing to continue.`);
53
- }
54
- if (candidates.length === 0) {
55
- return null;
56
- }
57
- const existing = candidates[0];
58
- return {
59
- planId,
60
- planIdentityHash,
61
- planEpoch: existing.planEpoch,
62
- rootEventHash: existing.rootEventHash,
63
- payloadHash,
64
- source: existing.source
65
- };
66
- }
67
- var LEDGER_FILENAME = "plan-ledger.jsonl";
68
- var LEDGER_LOCK_PATH = path.join(".swarm", LEDGER_FILENAME);
69
- var PLAN_JSON_FILENAME = "plan.json";
70
-
71
- class LedgerStaleWriterError extends Error {
72
- constructor(message) {
73
- super(message);
74
- this.name = "LedgerStaleWriterError";
75
- }
76
- }
77
- function getLedgerPath(directory) {
78
- return path.join(directory, ".swarm", LEDGER_FILENAME);
79
- }
80
- function getPlanJsonPath(directory) {
81
- return path.join(directory, ".swarm", PLAN_JSON_FILENAME);
82
- }
83
- function compareCodeUnits(a, b) {
84
- return a < b ? -1 : a > b ? 1 : 0;
85
- }
86
- function readLedgerDirectory(directory) {
87
- return fs.readdirSync(directory);
88
- }
89
- function asRecord(value) {
90
- if (value && typeof value === "object" && !Array.isArray(value)) {
91
- return value;
92
- }
93
- return null;
94
- }
95
- function isSha256Hex(value) {
96
- return /^[a-f0-9]{64}$/i.test(value);
97
- }
98
- function isUuid(value) {
99
- return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value);
100
- }
101
- function serializeLedgerEvent(event) {
102
- return JSON.stringify(event);
103
- }
104
- function computeLedgerEventHash(event) {
105
- return crypto.createHash("sha256").update(serializeLedgerEvent(event), "utf8").digest("hex");
106
- }
107
- function extractPlanEpochCandidates(events, expectedPlanId) {
108
- if (events.length === 0) {
109
- throw new Error("Plan ledger is empty; missing plan_created root.");
110
- }
111
- const rootEvent = events[0];
112
- if (rootEvent.event_type !== "plan_created") {
113
- throw new Error(`Plan ledger root must be plan_created; found ${rootEvent.event_type} at seq ${rootEvent.seq}.`);
114
- }
115
- if (rootEvent.plan_id !== expectedPlanId) {
116
- throw new Error(`Plan ledger identity mismatch: expected ${expectedPlanId} but found ${rootEvent.plan_id}.`);
117
- }
118
- const rootEventHash = computeLedgerEventHash(rootEvent);
119
- const candidates = [];
120
- const rootPayload = asRecord(rootEvent.payload);
121
- const rootPlanEpoch = rootPayload?.plan_epoch;
122
- if (typeof rootPlanEpoch === "string" && rootPlanEpoch.length > 0) {
123
- if (!isUuid(rootPlanEpoch)) {
124
- throw new Error("Plan ledger root carries an invalid plan_epoch.");
125
- }
126
- candidates.push({
127
- planEpoch: rootPlanEpoch,
128
- rootEventHash,
129
- source: "root"
130
- });
131
- }
132
- for (const event of events) {
133
- if (event.event_type !== "snapshot" || event.source !== "plan_epoch_adopted") {
134
- continue;
135
- }
136
- if (event.plan_id !== expectedPlanId) {
137
- throw new Error(`Plan epoch adoption snapshot at seq ${event.seq} belongs to ${event.plan_id}, expected ${expectedPlanId}.`);
138
- }
139
- const payload = asRecord(event.payload);
140
- const planEpoch = payload?.plan_epoch;
141
- const rootHash = payload?.root_event_hash;
142
- const embeddedPlan = PlanSchema.safeParse(payload?.plan);
143
- if (!embeddedPlan.success) {
144
- throw new Error(`Plan epoch adoption snapshot at seq ${event.seq} carries an invalid embedded plan.`);
145
- }
146
- const payloadHash = payload?.payload_hash;
147
- if (typeof planEpoch !== "string" || !isUuid(planEpoch)) {
148
- throw new Error(`Plan epoch adoption snapshot at seq ${event.seq} has an invalid plan_epoch.`);
149
- }
150
- if (typeof rootHash !== "string" || !isSha256Hex(rootHash)) {
151
- throw new Error(`Plan epoch adoption snapshot at seq ${event.seq} has an invalid root_event_hash.`);
152
- }
153
- if (rootHash !== rootEventHash) {
154
- throw new Error(`Plan epoch adoption snapshot at seq ${event.seq} does not match the canonical ledger root.`);
155
- }
156
- if (typeof payloadHash !== "string") {
157
- throw new Error(`Plan epoch adoption snapshot at seq ${event.seq} is missing payload_hash.`);
158
- }
159
- if (derivePlanId(embeddedPlan.data) !== expectedPlanId) {
160
- throw new Error(`Plan epoch adoption snapshot at seq ${event.seq} embeds a different plan identity.`);
161
- }
162
- if (computePlanLedgerHash(embeddedPlan.data) !== payloadHash) {
163
- throw new Error(`Plan epoch adoption snapshot at seq ${event.seq} has a mismatched payload_hash.`);
164
- }
165
- candidates.push({
166
- planEpoch,
167
- rootEventHash: rootHash,
168
- source: "plan_epoch_adopted"
169
- });
170
- }
171
- return candidates;
172
- }
173
- function writeFileFsyncedThenRename(tempPath, targetPath, data) {
174
- const fd = fs.openSync(tempPath, "w");
175
- try {
176
- fs.writeFileSync(fd, data, "utf8");
177
- fs.fsyncSync(fd);
178
- } finally {
179
- fs.closeSync(fd);
180
- }
181
- fs.renameSync(tempPath, targetPath);
182
- }
183
- function fsyncRecoveryDirectory(directory) {
184
- let fd;
185
- try {
186
- fd = fs.openSync(directory, "r");
187
- fs.fsyncSync(fd);
188
- } catch (error) {
189
- const code = error.code;
190
- const unsupportedOnWindows = process.platform === "win32" && ["EPERM", "EACCES", "EINVAL", "EBADF", "EISDIR", "ENOTSUP"].includes(code ?? "");
191
- if (unsupportedOnWindows) {
192
- log(`[ledger] Directory fsync unavailable for recovery archive ${directory}: ${error instanceof Error ? error.message : String(error)}`);
193
- return;
194
- }
195
- throw new Error(`Failed to fsync recovery archive directory before canonical replacement: ${error instanceof Error ? error.message : String(error)}`);
196
- } finally {
197
- if (fd !== undefined)
198
- fs.closeSync(fd);
199
- }
200
- }
201
- function findRawMalformedSuffix(content) {
202
- const decoder = new TextDecoder("utf-8", { fatal: true });
203
- let lineStart = 0;
204
- for (let index = 0;index <= content.length; index++) {
205
- if (index < content.length && content[index] !== 10)
206
- continue;
207
- let lineEnd = index;
208
- if (lineEnd > lineStart && content[lineEnd - 1] === 13)
209
- lineEnd--;
210
- const line = content.subarray(lineStart, lineEnd);
211
- if (line.length > 0) {
212
- try {
213
- JSON.parse(decoder.decode(line));
214
- } catch {
215
- return content.subarray(lineStart);
216
- }
217
- }
218
- lineStart = index + 1;
219
- }
220
- return Buffer.alloc(0);
221
- }
222
- function computePlanLedgerHash(plan) {
223
- const normalized = {
224
- schema_version: plan.schema_version,
225
- title: plan.title,
226
- swarm: plan.swarm,
227
- current_phase: plan.current_phase,
228
- migration_status: plan.migration_status,
229
- execution_profile: normalizeExecutionProfileForHash(plan.execution_profile),
230
- phases: plan.phases.map((phase) => ({
231
- id: phase.id,
232
- name: phase.name,
233
- status: phase.status,
234
- required_agents: phase.required_agents ? [...phase.required_agents].sort() : undefined,
235
- tasks: phase.tasks.map((task) => ({
236
- id: task.id,
237
- phase: task.phase,
238
- status: task.status,
239
- size: task.size,
240
- description: task.description,
241
- depends: [...task.depends].sort(),
242
- acceptance: task.acceptance,
243
- files_touched: [...task.files_touched].sort(),
244
- evidence_path: task.evidence_path,
245
- blocked_reason: task.blocked_reason
246
- }))
247
- }))
248
- };
249
- const jsonString = JSON.stringify(normalized);
250
- return crypto.createHash("sha256").update(jsonString, "utf8").digest("hex");
251
- }
252
- function computePlanStructureHash(plan) {
253
- const normalized = {
254
- schema_version: plan.schema_version,
255
- title: plan.title,
256
- swarm: plan.swarm,
257
- current_phase: plan.current_phase,
258
- migration_status: plan.migration_status,
259
- execution_profile: normalizeExecutionProfileForHash(plan.execution_profile),
260
- phases: plan.phases.map((phase) => ({
261
- id: phase.id,
262
- name: phase.name,
263
- required_agents: phase.required_agents ? [...phase.required_agents].sort() : undefined,
264
- tasks: phase.tasks.map((task) => ({
265
- id: task.id,
266
- phase: task.phase,
267
- size: task.size,
268
- description: task.description,
269
- depends: [...task.depends].sort(),
270
- acceptance: task.acceptance,
271
- files_touched: [...task.files_touched].sort(),
272
- evidence_path: task.evidence_path,
273
- blocked_reason: task.blocked_reason
274
- }))
275
- }))
276
- };
277
- const jsonString = JSON.stringify(normalized);
278
- return crypto.createHash("sha256").update(jsonString, "utf8").digest("hex");
279
- }
280
- function computeCurrentPlanHash(directory) {
281
- const planPath = getPlanJsonPath(directory);
282
- try {
283
- const content = fs.readFileSync(planPath, "utf8");
284
- const plan = JSON.parse(content);
285
- return computePlanLedgerHash(plan);
286
- } catch {
287
- return "";
288
- }
289
- }
290
- async function ledgerExists(directory) {
291
- const ledgerPath = getLedgerPath(directory);
292
- return fs.existsSync(ledgerPath);
293
- }
294
- async function getLatestLedgerSeq(directory) {
295
- const ledgerPath = getLedgerPath(directory);
296
- if (!fs.existsSync(ledgerPath)) {
297
- return 0;
298
- }
299
- try {
300
- const content = fs.readFileSync(ledgerPath, "utf8");
301
- const lines = content.trim().split(`
302
- `).filter((line) => line.trim() !== "");
303
- if (lines.length === 0) {
304
- return 0;
305
- }
306
- let maxSeq = 0;
307
- for (const line of lines) {
308
- try {
309
- const event = JSON.parse(line);
310
- if (event.seq > maxSeq) {
311
- maxSeq = event.seq;
312
- }
313
- } catch {}
314
- }
315
- return maxSeq;
316
- } catch {
317
- return 0;
318
- }
319
- }
320
- async function readLedgerEvents(directory) {
321
- const ledgerPath = getLedgerPath(directory);
322
- if (!fs.existsSync(ledgerPath)) {
323
- return [];
324
- }
325
- try {
326
- const content = fs.readFileSync(ledgerPath, "utf8");
327
- const lines = content.trim().split(`
328
- `).filter((line) => line.trim() !== "");
329
- const events = [];
330
- let skippedCount = 0;
331
- for (const line of lines) {
332
- try {
333
- const event = JSON.parse(line);
334
- events.push(event);
335
- } catch {
336
- skippedCount++;
337
- }
338
- }
339
- if (skippedCount > 0) {
340
- log(`[ledger] Skipped ${skippedCount} malformed line(s) in plan-ledger.jsonl`);
341
- }
342
- events.sort((a, b) => a.seq - b.seq);
343
- return events;
344
- } catch {
345
- return [];
346
- }
347
- }
348
- async function initLedger(directory, planId, initialPlanHash, initialPlan) {
349
- assertProjectRoot(directory);
350
- const ledgerPath = getLedgerPath(directory);
351
- const planJsonPath = getPlanJsonPath(directory);
352
- if (fs.existsSync(ledgerPath)) {
353
- throw new Error("Ledger already initialized. Use appendLedgerEvent to add events.");
354
- }
355
- let planHashAfter = initialPlanHash ?? "";
356
- let embeddedPlan = initialPlan;
357
- if (!initialPlanHash) {
358
- try {
359
- if (fs.existsSync(planJsonPath)) {
360
- const content = fs.readFileSync(planJsonPath, "utf8");
361
- const plan = JSON.parse(content);
362
- planHashAfter = computePlanLedgerHash(plan);
363
- if (!embeddedPlan)
364
- embeddedPlan = plan;
365
- }
366
- } catch {}
367
- }
368
- const payload = {
369
- plan_epoch: crypto.randomUUID(),
370
- ...embeddedPlan ? { plan: embeddedPlan, payload_hash: planHashAfter } : {}
371
- };
372
- const event = {
373
- seq: 1,
374
- timestamp: new Date().toISOString(),
375
- plan_id: planId,
376
- event_type: "plan_created",
377
- source: "initLedger",
378
- plan_hash_before: "",
379
- plan_hash_after: planHashAfter,
380
- schema_version: LEDGER_SCHEMA_VERSION,
381
- ...payload ? { payload } : {}
382
- };
383
- fs.mkdirSync(path.join(directory, ".swarm"), { recursive: true });
384
- const tempPath = `${ledgerPath}.tmp.${Date.now()}.${Math.floor(Math.random() * 1e9)}`;
385
- const line = `${JSON.stringify(event)}
386
- `;
387
- writeFileFsyncedThenRename(tempPath, ledgerPath, line);
388
- }
389
- async function readLedgerEventsForEpoch(directory) {
390
- const integrity = await _internals.readLedgerEventsWithIntegrity(directory, {
391
- failClosedOnReadError: true
392
- });
393
- if (integrity.truncated) {
394
- throw new Error(`PLAN_LEDGER_TRUNCATED: ${getLedgerPath(directory)} has a malformed line; plan epoch identity cannot be resolved. Preserve this file and quarantine the corrupted suffix before retrying.`);
395
- }
396
- return integrity.events;
397
- }
398
- async function readPlanEpochIdentity(directory, authoritativePlan) {
399
- assertProjectRoot(directory);
400
- if (!await ledgerExists(directory)) {
401
- return null;
402
- }
403
- const events = await readLedgerEventsForEpoch(directory);
404
- if (events.length === 0) {
405
- throw new Error("Plan ledger is empty; missing plan_created root.");
406
- }
407
- return resolvePlanEpochIdentity(events, authoritativePlan);
408
- }
409
- async function getOrAdoptPlanEpochUnderLock(directory, authoritativePlan) {
410
- assertProjectRoot(directory);
411
- const planId = derivePlanId(authoritativePlan);
412
- const planIdentityHash = derivePlanIdentityHash(authoritativePlan);
413
- const payloadHash = computePlanLedgerHash(authoritativePlan);
414
- let lastStaleWriterError;
415
- for (let attempt = 0;attempt < 4; attempt++) {
416
- if (!await ledgerExists(directory)) {
417
- try {
418
- await initLedger(directory, planId, payloadHash, authoritativePlan);
419
- } catch (error) {
420
- if (!(error instanceof Error && error.message.includes("Ledger already initialized"))) {
421
- throw error;
422
- }
423
- }
424
- }
425
- const events = await readLedgerEventsForEpoch(directory);
426
- const existing = resolvePlanEpochIdentity(events, authoritativePlan);
427
- if (existing) {
428
- return existing;
429
- }
430
- if (events.length === 0) {
431
- throw new Error("Plan ledger is empty; cannot adopt a plan epoch.");
432
- }
433
- const rootEvent = events[0];
434
- const rootEventHash = computeLedgerEventHash(rootEvent);
435
- const tail = events[events.length - 1];
436
- const planEpoch = _internals.randomUUID();
437
- const adoptionPayload = {
438
- plan: authoritativePlan,
439
- payload_hash: payloadHash,
440
- plan_epoch: planEpoch,
441
- root_event_hash: rootEventHash
442
- };
443
- try {
444
- const appended = await _internals.appendLedgerEvent(directory, {
445
- event_type: "snapshot",
446
- source: "plan_epoch_adopted",
447
- plan_id: planId,
448
- payload: adoptionPayload
449
- }, {
450
- expectedSeq: tail.seq,
451
- expectedLedgerHash: tail.plan_hash_after,
452
- planHashAfter: payloadHash
453
- });
454
- const verifiedEvents = await readLedgerEventsForEpoch(directory);
455
- const verified = verifiedEvents[verifiedEvents.length - 1];
456
- const verifiedPayload = asRecord(verified?.payload);
457
- if (!verified || verified.seq !== appended.seq || verified.event_type !== "snapshot" || verified.source !== "plan_epoch_adopted" || verified.plan_id !== planId || verifiedPayload?.plan_epoch !== planEpoch || verifiedPayload?.root_event_hash !== rootEventHash || verifiedPayload?.payload_hash !== payloadHash) {
458
- throw new Error(`Plan epoch adoption read verification failed for ${planId}.`);
459
- }
460
- return {
461
- planId,
462
- planIdentityHash,
463
- planEpoch,
464
- rootEventHash,
465
- payloadHash,
466
- source: "plan_epoch_adopted"
467
- };
468
- } catch (error) {
469
- if (error instanceof LedgerStaleWriterError) {
470
- lastStaleWriterError = error;
471
- continue;
472
- }
473
- throw error;
474
- }
475
- }
476
- throw new Error(`Unable to settle plan epoch for ${planId} after repeated stale-writer retries.`, { cause: lastStaleWriterError });
477
- }
478
- async function appendLedgerEvent(directory, eventInput, options) {
479
- assertProjectRoot(directory);
480
- return withEvidenceLock(directory, LEDGER_LOCK_PATH, "plan-ledger", "append-ledger-event", async () => {
481
- const ledgerPath = getLedgerPath(directory);
482
- if (options?.expectedLedgerHash !== undefined && options.expectedSeq === undefined) {
483
- throw new Error("expectedLedgerHash requires expectedSeq so stale-projection recovery is bound to one exact ledger tail");
484
- }
485
- const latestSeq = await getLatestLedgerSeq(directory);
486
- const nextSeq = latestSeq + 1;
487
- let ledgerHashBefore;
488
- if (options?.expectedLedgerHash !== undefined) {
489
- const events = await readLedgerEvents(directory);
490
- const tail = events[events.length - 1];
491
- if (!tail || tail.plan_hash_after !== options.expectedLedgerHash) {
492
- throw new LedgerStaleWriterError(`Stale writer: expected ledger hash ${options.expectedLedgerHash} but found ${tail?.plan_hash_after ?? "<missing>"}`);
493
- }
494
- ledgerHashBefore = tail.plan_hash_after;
495
- }
496
- const planHashBefore = computeCurrentPlanHash(directory);
497
- if (options?.expectedSeq !== undefined && options.expectedSeq !== latestSeq) {
498
- throw new LedgerStaleWriterError(`Stale writer: expected seq ${options.expectedSeq} but found ${latestSeq}`);
499
- }
500
- if (options?.expectedHash !== undefined && options.expectedHash !== planHashBefore) {
501
- throw new LedgerStaleWriterError(`Stale writer: expected hash ${options.expectedHash} but found ${planHashBefore}`);
502
- }
503
- const planHashAfter = options?.planHashAfter ?? planHashBefore;
504
- const event = {
505
- ...eventInput,
506
- seq: nextSeq,
507
- timestamp: new Date().toISOString(),
508
- plan_hash_before: ledgerHashBefore ?? planHashBefore,
509
- plan_hash_after: planHashAfter,
510
- schema_version: LEDGER_SCHEMA_VERSION
511
- };
512
- fs.mkdirSync(path.join(directory, ".swarm"), { recursive: true });
513
- const tempPath = `${ledgerPath}.tmp.${Date.now()}.${Math.floor(Math.random() * 1e9)}`;
514
- const line = `${JSON.stringify(event)}
515
- `;
516
- if (!fs.existsSync(ledgerPath)) {
517
- throw new Error("Ledger not initialized. Call initLedger() first.");
518
- }
519
- const existingContent = fs.readFileSync(ledgerPath, "utf8");
520
- writeFileFsyncedThenRename(tempPath, ledgerPath, existingContent + line);
521
- return event;
522
- });
523
- }
524
- async function appendLedgerEventWithRetry(directory, eventInput, options) {
525
- const maxRetries = options.maxRetries ?? 3;
526
- const backoffBase = options.backoffMs ?? 10;
527
- let currentExpected = options.expectedHash;
528
- let attempt = 0;
529
- while (true) {
530
- try {
531
- return await appendLedgerEvent(directory, eventInput, {
532
- expectedHash: currentExpected,
533
- planHashAfter: options.planHashAfter
534
- });
535
- } catch (error) {
536
- if (!(error instanceof LedgerStaleWriterError) || attempt >= maxRetries) {
537
- throw error;
538
- }
539
- attempt++;
540
- const delayMs = backoffBase * 2 ** (attempt - 1);
541
- await new Promise((resolve) => setTimeout(resolve, delayMs));
542
- if (options.verifyValid) {
543
- const stillValid = await options.verifyValid();
544
- if (!stillValid) {
545
- return null;
546
- }
547
- }
548
- currentExpected = computeCurrentPlanHash(directory);
549
- }
550
- }
551
- }
552
- async function takeSnapshotWithRetry(directory, plan, options) {
553
- const MAX_RETRIES = 3;
554
- const TOTAL_ATTEMPTS = 1 + MAX_RETRIES;
555
- const telemetrySource = options?.source ?? "save_plan_tool";
556
- const snapshotOptions = { planHashAfter: options?.planHashAfter };
557
- let lastError;
558
- for (let attempt = 1;attempt <= TOTAL_ATTEMPTS; attempt++) {
559
- try {
560
- await takeSnapshotEvent(directory, plan, snapshotOptions);
561
- return;
562
- } catch (err) {
563
- lastError = err instanceof Error ? err : new Error(String(err));
564
- if (attempt < TOTAL_ATTEMPTS) {
565
- await new Promise((r) => setTimeout(r, 10 * 2 ** (attempt - 1)));
566
- }
567
- }
568
- }
569
- criticalWarn(`[takeSnapshotWithRetry] Snapshot failed after ${MAX_RETRIES} retries (${TOTAL_ATTEMPTS} attempts): ${lastError.message}`);
570
- try {
571
- emit("snapshot_failed", {
572
- error: lastError.message,
573
- retries: MAX_RETRIES,
574
- source: telemetrySource
575
- });
576
- } catch {}
577
- }
578
- async function takeSnapshotEvent(directory, plan, options) {
579
- const payloadHash = options?.payloadHashOverride ?? (options?.source === "critic_approved" ? computePlanStructureHash(plan) : computePlanLedgerHash(plan));
580
- const snapshotPayload = {
581
- plan,
582
- payload_hash: payloadHash
583
- };
584
- if (options?.approvalMetadata) {
585
- snapshotPayload.approval = options.approvalMetadata;
586
- }
587
- const planId = derivePlanId(plan);
588
- return appendLedgerEvent(directory, {
589
- event_type: "snapshot",
590
- source: options?.source ?? "takeSnapshotEvent",
591
- plan_id: planId,
592
- payload: snapshotPayload
593
- }, {
594
- planHashAfter: options?.planHashAfter,
595
- expectedSeq: options?.expectedSeq,
596
- expectedLedgerHash: options?.expectedLedgerHash
597
- });
598
- }
599
- async function replaceTruncatedLedgerWithRecoveryRoot(directory, plan, expected) {
600
- assertProjectRoot(directory);
601
- const validated = PlanSchema.parse(plan);
602
- return withEvidenceLock(directory, LEDGER_LOCK_PATH, "plan-ledger", "reconcile-truncated-ledger", async () => {
603
- const ledgerPath = getLedgerPath(directory);
604
- if (!fs.existsSync(ledgerPath)) {
605
- throw new LedgerStaleWriterError("Stale writer: canonical ledger disappeared before truncated-ledger recovery");
606
- }
607
- const events = await readLedgerEvents(directory);
608
- const tail = events[events.length - 1];
609
- if (!tail || tail.seq !== expected.seq || tail.plan_hash_after !== expected.ledgerHash || events[0]?.plan_id !== derivePlanId(validated)) {
610
- throw new LedgerStaleWriterError("Stale writer: ledger identity or tail changed before truncated-ledger recovery");
611
- }
612
- const integrity = await readLedgerEventsWithIntegrity(directory);
613
- if (!integrity.truncated || integrity.badSuffix === null) {
614
- throw new LedgerStaleWriterError("Stale writer: ledger is no longer truncated; retry against its current tail");
615
- }
616
- const originalBytes = fs.readFileSync(ledgerPath);
617
- const archiveSha256 = crypto.createHash("sha256").update(originalBytes).digest("hex");
618
- const swarmDir = path.dirname(ledgerPath);
619
- const archiveSuffix = `.${archiveSha256.slice(0, 12)}.jsonl`;
620
- let archiveName = `plan-ledger.reconcile-archive.${Date.now()}.${Math.floor(Math.random() * 1e9)}${archiveSuffix}`;
621
- let archivePath = path.join(swarmDir, archiveName);
622
- let archiveNeedsWrite = true;
623
- for (const candidate of _internals.readLedgerDirectory(swarmDir).sort(compareCodeUnits)) {
624
- if (!candidate.startsWith("plan-ledger.reconcile-archive.") || !candidate.endsWith(archiveSuffix)) {
625
- continue;
626
- }
627
- const candidatePath = path.join(swarmDir, candidate);
628
- if (fs.readFileSync(candidatePath).equals(originalBytes)) {
629
- archiveName = candidate;
630
- archivePath = candidatePath;
631
- archiveNeedsWrite = false;
632
- break;
633
- }
634
- }
635
- const archiveTempPath = `${archivePath}.tmp`;
636
- const canonicalTempPath = `${ledgerPath}.reconcile.${Date.now()}.${Math.floor(Math.random() * 1e9)}.tmp`;
637
- const planHash = computePlanLedgerHash(validated);
638
- const rawBadSuffix = findRawMalformedSuffix(originalBytes);
639
- if (rawBadSuffix.length === 0) {
640
- throw new LedgerStaleWriterError("Stale writer: raw ledger bytes no longer contain the malformed suffix detected during replay");
641
- }
642
- const badSuffixSha256 = crypto.createHash("sha256").update(rawBadSuffix).digest("hex");
643
- const recoveryRoot = {
644
- seq: 1,
645
- timestamp: new Date().toISOString(),
646
- plan_id: derivePlanId(validated),
647
- event_type: "plan_created",
648
- source: "save_plan_truncated_ledger_reconcile",
649
- plan_hash_before: "",
650
- plan_hash_after: planHash,
651
- schema_version: LEDGER_SCHEMA_VERSION,
652
- payload: {
653
- plan: validated,
654
- payload_hash: planHash,
655
- recovery: {
656
- kind: "truncated_ledger_reconcile",
657
- archived_ledger: `.swarm/${archiveName}`,
658
- archived_sha256: archiveSha256,
659
- bad_suffix_sha256: badSuffixSha256,
660
- prior_tail_seq: expected.seq,
661
- prior_tail_hash: expected.ledgerHash
662
- }
663
- }
664
- };
665
- try {
666
- if (archiveNeedsWrite) {
667
- _internals.writeFileFsyncedThenRename(archiveTempPath, archivePath, originalBytes);
668
- }
669
- _internals.fsyncRecoveryDirectory(swarmDir);
670
- _internals.writeFileFsyncedThenRename(canonicalTempPath, ledgerPath, `${JSON.stringify(recoveryRoot)}
671
- `);
672
- } finally {
673
- for (const tempPath of [archiveTempPath, canonicalTempPath]) {
674
- try {
675
- fs.unlinkSync(tempPath);
676
- } catch {}
677
- }
678
- }
679
- return { archivePath, archiveSha256, recoveryRoot };
680
- });
681
- }
682
- async function replayFromLedger(directory, options) {
683
- const { plan } = await replayFromLedgerWithStatus(directory, options);
684
- return plan;
685
- }
686
- async function replayFromLedgerWithStatus(directory, _options) {
687
- const { events, truncated, badSuffix } = await readLedgerEventsWithIntegrity(directory);
688
- if (events.length === 0) {
689
- return { plan: null, truncated, badSuffix };
690
- }
691
- if (truncated && badSuffix !== null) {
692
- await quarantineLedgerSuffix(directory, badSuffix);
693
- }
694
- const plan = reconstructPlanFromEvents(directory, events);
695
- return { plan, truncated, badSuffix };
696
- }
697
- function reconstructPlanFromEvents(directory, events) {
698
- const targetPlanId = events[0].plan_id;
699
- const relevantEvents = events.filter((e) => e.plan_id === targetPlanId);
700
- {
701
- const snapshotEvents = relevantEvents.filter((e) => e.event_type === "snapshot");
702
- if (snapshotEvents.length > 0) {
703
- const latestSnapshotEvent = snapshotEvents[snapshotEvents.length - 1];
704
- const snapshotPayload = latestSnapshotEvent.payload;
705
- let plan2 = snapshotPayload.plan;
706
- const eventsAfterSnapshot = relevantEvents.filter((e) => e.seq > latestSnapshotEvent.seq);
707
- for (const event of eventsAfterSnapshot) {
708
- plan2 = applyEventToPlan(plan2, event);
709
- if (plan2 === null) {
710
- return null;
711
- }
712
- }
713
- return plan2;
714
- }
715
- }
716
- const createdEvent = relevantEvents.find((e) => e.event_type === "plan_created");
717
- if (createdEvent?.payload && typeof createdEvent.payload === "object" && "plan" in createdEvent.payload) {
718
- const parseResult = PlanSchema.safeParse(createdEvent.payload.plan);
719
- if (parseResult.success) {
720
- let plan2 = parseResult.data;
721
- const eventsAfterCreated = relevantEvents.filter((e) => e.seq > createdEvent.seq);
722
- for (const event of eventsAfterCreated) {
723
- if (plan2 === null)
724
- return null;
725
- plan2 = applyEventToPlan(plan2, event);
726
- }
727
- return plan2;
728
- }
729
- }
730
- const planJsonPath = getPlanJsonPath(directory);
731
- if (!fs.existsSync(planJsonPath)) {
732
- return null;
733
- }
734
- let plan;
735
- try {
736
- const content = fs.readFileSync(planJsonPath, "utf8");
737
- plan = JSON.parse(content);
738
- } catch {
739
- return null;
740
- }
741
- for (const event of relevantEvents) {
742
- if (plan === null) {
743
- return null;
744
- }
745
- plan = applyEventToPlan(plan, event);
746
- }
747
- return plan;
748
- }
749
- function applyEventToPlan(plan, event) {
750
- switch (event.event_type) {
751
- case "plan_created":
752
- if (event.payload && typeof event.payload === "object" && "plan" in event.payload) {
753
- const parsed = PlanSchema.safeParse(event.payload.plan);
754
- if (parsed.success)
755
- return parsed.data;
756
- }
757
- return plan;
758
- case "task_status_changed":
759
- if (event.task_id && event.to_status) {
760
- const parseResult = TaskStatusSchema.safeParse(event.to_status);
761
- if (!parseResult.success) {
762
- return plan;
763
- }
764
- for (const phase of plan.phases) {
765
- const task = phase.tasks.find((t) => t.id === event.task_id);
766
- if (task) {
767
- task.status = parseResult.data;
768
- break;
769
- }
770
- }
771
- }
772
- return plan;
773
- case "phase_completed":
774
- if (event.phase_id) {
775
- const phase = plan.phases.find((p) => p.id === event.phase_id);
776
- if (phase) {
777
- phase.status = "complete";
778
- }
779
- }
780
- return plan;
781
- case "plan_exported":
782
- return plan;
783
- case "task_added":
784
- return plan;
785
- case "task_removed":
786
- if (event.task_id) {
787
- for (const phase of plan.phases) {
788
- const idx = phase.tasks.findIndex((t) => t.id === event.task_id);
789
- if (idx !== -1) {
790
- phase.tasks.splice(idx, 1);
791
- break;
792
- }
793
- }
794
- }
795
- return plan;
796
- case "task_updated":
797
- return plan;
798
- case "plan_rebuilt":
799
- return plan;
800
- case "task_reordered":
801
- return plan;
802
- case "snapshot":
803
- return plan;
804
- case "plan_reset":
805
- return null;
806
- case "execution_profile_set": {
807
- const rawProfile = event.payload?.execution_profile;
808
- if (rawProfile !== undefined) {
809
- const parsed = ExecutionProfileSchema.safeParse(rawProfile);
810
- if (parsed.success) {
811
- return { ...plan, execution_profile: parsed.data };
812
- }
813
- }
814
- return plan;
815
- }
816
- case "execution_profile_locked": {
817
- if (plan.execution_profile) {
818
- return {
819
- ...plan,
820
- execution_profile: { ...plan.execution_profile, locked: true }
821
- };
822
- }
823
- return plan;
824
- }
825
- default:
826
- throw new Error(`applyEventToPlan: unhandled event type "${event.event_type}" at seq ${event.seq}`);
827
- }
828
- }
829
- async function readLedgerEventsWithIntegrity(directory, options) {
830
- const ledgerPath = getLedgerPath(directory);
831
- if (!fs.existsSync(ledgerPath)) {
832
- return { events: [], truncated: false, badSuffix: null };
833
- }
834
- try {
835
- const content = fs.readFileSync(ledgerPath, "utf8");
836
- const lines = content.split(`
837
- `);
838
- const events = [];
839
- let truncated = false;
840
- let badSuffix = null;
841
- for (let i = 0;i < lines.length; i++) {
842
- const line = lines[i];
843
- if (line.trim() === "") {
844
- continue;
845
- }
846
- try {
847
- const event = JSON.parse(line);
848
- events.push(event);
849
- } catch {
850
- truncated = true;
851
- badSuffix = lines.slice(i).join(`
852
- `);
853
- break;
854
- }
855
- }
856
- events.sort((a, b) => a.seq - b.seq);
857
- return { events, truncated, badSuffix };
858
- } catch (error) {
859
- if (options?.failClosedOnReadError) {
860
- throw new Error(`PLAN_LEDGER_UNREADABLE: ${ledgerPath} exists but could not be read (${error instanceof Error ? error.message : String(error)}); plan state cannot be resolved. This is NOT an absent ledger \u2014 preserve the file and check permissions/filesystem health before retrying.`);
861
- }
862
- return { events: [], truncated: false, badSuffix: null };
863
- }
864
- }
865
- async function quarantineLedgerSuffix(directory, badSuffix) {
866
- let salvagedCount = 0;
867
- for (const line of badSuffix.split(`
868
- `)) {
869
- if (line.trim() === "")
870
- continue;
871
- try {
872
- JSON.parse(line);
873
- salvagedCount++;
874
- } catch {}
875
- }
876
- try {
877
- assertProjectRoot(directory);
878
- const hash = crypto.createHash("sha256").update(badSuffix, "utf8").digest("hex").slice(0, 12);
879
- const swarmDir = path.join(directory, ".swarm");
880
- try {
881
- const existing = _internals.readLedgerDirectory(swarmDir).sort(compareCodeUnits).filter((name) => name.startsWith("plan-ledger.quarantine.") && name.endsWith(`.${hash}`));
882
- for (const name of existing) {
883
- const existingPath = path.join(swarmDir, name);
884
- if (fs.readFileSync(existingPath, "utf8") === badSuffix) {
885
- return { path: existingPath, salvagedCount };
886
- }
887
- }
888
- } catch {}
889
- const quarantinePath = path.join(swarmDir, `plan-ledger.quarantine.${Date.now()}.${hash}`);
890
- fs.writeFileSync(quarantinePath, badSuffix, "utf8");
891
- log(`[ledger] Corrupted suffix quarantined to ${path.relative(directory, quarantinePath)} (salvageable events: ${salvagedCount})`);
892
- return { path: quarantinePath, salvagedCount };
893
- } catch {
894
- return { path: null, salvagedCount };
895
- }
896
- }
897
- async function loadLastApprovedPlan(directory, expectedPlanId) {
898
- const events = await readLedgerEvents(directory);
899
- return findLastApprovedSnapshot(events, expectedPlanId);
900
- }
901
- async function loadLastPlanCriticApprovedSnapshot(directory, expectedPlanId) {
902
- const events = await readLedgerEvents(directory);
903
- return findLastApprovedSnapshot(events, expectedPlanId, (payload) => payload.approval?.source === "plan_critic_gate");
904
- }
905
- function findLastApprovedSnapshot(events, expectedPlanId, extraFilter) {
906
- if (events.length === 0) {
907
- return null;
908
- }
909
- for (let i = events.length - 1;i >= 0; i--) {
910
- const event = events[i];
911
- if (event.event_type !== "snapshot")
912
- continue;
913
- if (event.source !== "critic_approved")
914
- continue;
915
- if (expectedPlanId !== undefined && event.plan_id !== expectedPlanId) {
916
- continue;
917
- }
918
- const payload = event.payload;
919
- if (!payload || typeof payload !== "object" || !payload.plan) {
920
- continue;
921
- }
922
- if (expectedPlanId !== undefined) {
923
- const payloadPlanId = derivePlanId(payload.plan);
924
- if (payloadPlanId !== expectedPlanId) {
925
- continue;
926
- }
927
- }
928
- if (extraFilter && !extraFilter(payload)) {
929
- continue;
930
- }
931
- return {
932
- plan: payload.plan,
933
- seq: event.seq,
934
- timestamp: event.timestamp,
935
- approval: payload.approval,
936
- payloadHash: payload.payload_hash
937
- };
938
- }
939
- return null;
940
- }
941
- var _internals = {
942
- computePlanLedgerHash,
943
- computePlanStructureHash,
944
- computeCurrentPlanHash,
945
- ledgerExists,
946
- getLatestLedgerSeq,
947
- readLedgerEvents,
948
- initLedger,
949
- appendLedgerEvent,
950
- appendLedgerEventWithRetry,
951
- takeSnapshotEvent,
952
- replayFromLedger,
953
- replayFromLedgerWithStatus,
954
- applyEventToPlan,
955
- readLedgerEventsWithIntegrity,
956
- quarantineLedgerSuffix,
957
- loadLastApprovedPlan,
958
- loadLastPlanCriticApprovedSnapshot,
959
- getLedgerPath,
960
- getPlanJsonPath,
961
- writeFileFsyncedThenRename,
962
- fsyncRecoveryDirectory,
963
- readLedgerDirectory,
964
- randomUUID: () => crypto.randomUUID()
965
- };
966
-
967
- export { normalizeExecutionProfileForHash, LedgerStaleWriterError, computePlanLedgerHash, computePlanStructureHash, computeCurrentPlanHash, ledgerExists, getLatestLedgerSeq, readLedgerEvents, initLedger, readPlanEpochIdentity, getOrAdoptPlanEpochUnderLock, appendLedgerEvent, takeSnapshotWithRetry, takeSnapshotEvent, replaceTruncatedLedgerWithRecoveryRoot, replayFromLedger, replayFromLedgerWithStatus, readLedgerEventsWithIntegrity, loadLastApprovedPlan };