opencode-swarm 7.167.2 → 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 (102) 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/cli/{coder-settlement-jy9rad28.js → coder-settlement-a35acvj1.js} +12 -12
  4. package/dist/cli/{config-doctor-p66vnpy3.js → config-doctor-9csx775x.js} +3 -3
  5. package/dist/cli/{core-tw0wtntz.js → core-agk5s7dm.js} +2 -2
  6. package/dist/cli/{curation-policy-ndp27v0c.js → curation-policy-mgwv8h2k.js} +5 -5
  7. package/dist/cli/{curator-5nzde80g.js → curator-2fcagjyk.js} +38 -38
  8. package/dist/cli/{curator-drift-gb651hcw.js → curator-drift-k0zrnm09.js} +5 -5
  9. package/dist/cli/curator-llm-factory-qaxmyy46.js +71 -0
  10. package/dist/cli/{evidence-summary-service-bzpfaypy.js → evidence-summary-service-4s6cje60.js} +17 -17
  11. package/dist/cli/{gate-evidence-wa8cj8h6.js → gate-evidence-saja4dan.js} +7 -7
  12. package/dist/cli/guardrail-explain-z41q6sf0.js +72 -0
  13. package/dist/cli/{guardrail-log-7dv73e0q.js → guardrail-log-0h24spq0.js} +8 -8
  14. package/dist/cli/{guardrail-reset-1kng6y8x.js → guardrail-reset-cwbe47k5.js} +38 -38
  15. package/dist/cli/{hive-promoter-87kk2pg5.js → hive-promoter-esxtcvr1.js} +38 -38
  16. package/dist/cli/{index-3r6k6qa7.js → index-00zx94vn.js} +4 -4
  17. package/dist/cli/{index-arqv4psy.js → index-21ree67h.js} +57 -101
  18. package/dist/cli/{index-dme7wvwg.js → index-30h1pk05.js} +4 -4
  19. package/dist/cli/{index-shrf0wt3.js → index-3dywbkyr.js} +4 -4
  20. package/dist/cli/{index-vgpedapd.js → index-4r5pmtvk.js} +2 -2
  21. package/dist/cli/{index-3ws11dq0.js → index-54w1564x.js} +2 -2
  22. package/dist/cli/{index-m7cwnt9r.js → index-5zqakgp4.js} +2 -2
  23. package/dist/cli/{index-f91fhch0.js → index-64phnth4.js} +4 -4
  24. package/dist/cli/{index-09gv90d7.js → index-8f0kscpg.js} +2 -2
  25. package/dist/cli/{index-0q839sg6.js → index-8ybjqg51.js} +1 -1
  26. package/dist/cli/{index-z6we1abx.js → index-a8xbvsse.js} +2 -2
  27. package/dist/cli/{index-jhd3pmd1.js → index-an8b5gda.js} +7 -7
  28. package/dist/cli/{index-gmjg65tm.js → index-bvqqwx84.js} +2 -2
  29. package/dist/cli/{index-rrne6g2m.js → index-cyenn3pf.js} +6 -2
  30. package/dist/cli/{index-65s0vgb1.js → index-ddr97ram.js} +40 -40
  31. package/dist/cli/{index-mtepgqan.js → index-dfyv3xkd.js} +7 -7
  32. package/dist/cli/{index-2b1hd1fx.js → index-exatbqwj.js} +13 -4
  33. package/dist/cli/{index-83r0v5tn.js → index-gp9yzr1v.js} +1 -1
  34. package/dist/cli/{index-79234evf.js → index-h0e2e3h1.js} +1 -1
  35. package/dist/cli/{index-962qme08.js → index-h439y187.js} +1 -1
  36. package/dist/cli/{index-18rfc7qv.js → index-j950hqmq.js} +7 -7
  37. package/dist/cli/{index-8vbahc4c.js → index-kear8gb7.js} +11 -6
  38. package/dist/cli/{index-syedr4e3.js → index-m35hybf0.js} +1 -1
  39. package/dist/cli/{index-ce8p9t28.js → index-mr0wn36f.js} +5 -5
  40. package/dist/cli/{index-sjkg2hc4.js → index-nprngf8p.js} +6 -6
  41. package/dist/cli/{index-m3z83nhs.js → index-nsyxj57e.js} +8 -8
  42. package/dist/cli/{index-f8cc92g7.js → index-nwwk3dej.js} +1 -1
  43. package/dist/cli/{index-g409c7ke.js → index-pyf517sz.js} +2 -2
  44. package/dist/cli/{index-k73ehxa1.js → index-qhey6vc3.js} +1 -1
  45. package/dist/cli/{index-gs12223x.js → index-qwsh0h2g.js} +2 -2
  46. package/dist/cli/{index-n4fzmqz8.js → index-rdm3px0g.js} +4 -2
  47. package/dist/cli/{index-25xeyjfx.js → index-rxcgrgkc.js} +38 -38
  48. package/dist/cli/{index-ehq2vtkq.js → index-s0d5xter.js} +4 -4
  49. package/dist/cli/{index-wjet7x0a.js → index-seetnhpx.js} +1 -1
  50. package/dist/cli/{index-g5m43paw.js → index-sxt7fgcw.js} +1 -0
  51. package/dist/cli/{index-x7pm61wk.js → index-v3xc4sde.js} +9 -8
  52. package/dist/cli/{index-pdcperq5.js → index-wnzxcr0z.js} +2 -2
  53. package/dist/cli/{index-hhkm64wh.js → index-wst6n478.js} +103 -3
  54. package/dist/cli/{index-s7a9dqtb.js → index-x7k3dps5.js} +4 -3
  55. package/dist/cli/{index-y1sj2e1n.js → index-yevn4jpe.js} +2094 -1686
  56. package/dist/cli/index-yv76j77m.js +2008 -0
  57. package/dist/cli/{index-tcj8v6my.js → index-z6qreakh.js} +6 -6
  58. package/dist/cli/{index-5sjq3shs.js → index-ze520xw1.js} +8 -8
  59. package/dist/cli/index.js +38 -38
  60. package/dist/cli/{knowledge-escalator-5e4zxan5.js → knowledge-escalator-b2tf3sd1.js} +13 -13
  61. package/dist/cli/{knowledge-events-abzm5vfh.js → knowledge-events-bgs2kxe0.js} +11 -11
  62. package/dist/cli/{knowledge-link-9ga6hyd5.js → knowledge-link-dr3xnwyd.js} +4 -4
  63. package/dist/cli/{knowledge-store-q4a92eq7.js → knowledge-store-stbmb1b9.js} +5 -5
  64. package/dist/cli/{knowledge-validator-pn3m5r41.js → knowledge-validator-zykgnsag.js} +7 -7
  65. package/dist/cli/{model-preflight-mw895vgt.js → model-preflight-1ycat9jz.js} +1 -1
  66. package/dist/cli/{pending-delegations-3z0xjppd.js → pending-delegations-n5qavj20.js} +12 -12
  67. package/dist/cli/{pr-subscriptions-h7mwnp62.js → pr-subscriptions-gnvmyrng.js} +12 -12
  68. package/dist/cli/{pr-workflow-gate-fec04fa0.js → pr-workflow-gate-cf3ybrws.js} +38 -38
  69. package/dist/cli/{runner-d9833azt.js → runner-k0ycgv8x.js} +5 -5
  70. package/dist/cli/{scan-cursor-ymfvzdkz.js → scan-cursor-b3mcrx23.js} +6 -6
  71. package/dist/cli/{schema-chc4aez6.js → schema-08049cjb.js} +4 -2
  72. package/dist/cli/{scope-persistence-3xnr59y8.js → scope-persistence-a493gv51.js} +16 -16
  73. package/dist/cli/{skill-generator-3mvaghxk.js → skill-generator-2qqzkdpv.js} +15 -15
  74. package/dist/cli/{snapshot-coordination-init-arnc77r9.js → snapshot-coordination-init-8yt8p7q0.js} +38 -38
  75. package/dist/cli/{sqlite-loader-xrnf6bq8.js → sqlite-loader-amp9z7ty.js} +1 -1
  76. package/dist/cli/{telemetry-g9pppt9e.js → telemetry-2wzapf11.js} +1 -1
  77. package/dist/cli/{workspace-snapshot-hvsjtepd.js → workspace-snapshot-rkgxhj9b.js} +3 -3
  78. package/dist/cli/{worktree-collision-ownership-vj8anvb7.js → worktree-collision-ownership-xttafc4h.js} +13 -13
  79. package/dist/cli/{worktree-isolation-mt7z75hz.js → worktree-isolation-e5q5rewk.js} +38 -38
  80. package/dist/commands/reset.d.ts +10 -0
  81. package/dist/commands/rollback.d.ts +9 -0
  82. package/dist/config/schema.d.ts +29 -0
  83. package/dist/db/project-db.d.ts +8 -0
  84. package/dist/db/sqlite-loader.d.ts +2 -1
  85. package/dist/dispatch/spawn-circuit.d.ts +121 -0
  86. package/dist/dispatch/token-bucket.d.ts +46 -0
  87. package/dist/hooks/delegation-gate/dead-lane-reclaim.d.ts +61 -0
  88. package/dist/hooks/delegation-gate/worktree-isolation.d.ts +22 -0
  89. package/dist/hooks/issue-trace.d.ts +3 -0
  90. package/dist/hooks/utils.d.ts +1 -1
  91. package/dist/index.js +657 -572
  92. package/dist/plan/ledger-sqlite.d.ts +226 -0
  93. package/dist/plan/ledger.d.ts +24 -0
  94. package/dist/plan/manager.d.ts +19 -0
  95. package/dist/retention/sweep.d.ts +2 -0
  96. package/dist/services/spec-drift-recovery.d.ts +2 -1
  97. package/dist/utils/ephemeral-session-teardown.d.ts +55 -2
  98. package/opencode-swarm.schema.json +41 -0
  99. package/package.json +1 -1
  100. package/dist/cli/curator-llm-factory-xy96x5x8.js +0 -71
  101. package/dist/cli/guardrail-explain-1ef5he6j.js +0 -72
  102. package/dist/cli/index-z7z3g5wt.js +0 -1017
@@ -0,0 +1,2008 @@
1
+ // @bun
2
+ import {
3
+ derivePlanId,
4
+ derivePlanIdentityHash
5
+ } from "./index-h439y187.js";
6
+ import {
7
+ getProjectDb,
8
+ projectDbExists,
9
+ withImmediateTransaction,
10
+ withProjectDbReadOnly
11
+ } from "./index-wst6n478.js";
12
+ import {
13
+ assertProjectRoot,
14
+ withEvidenceLock
15
+ } from "./index-4r5pmtvk.js";
16
+ import {
17
+ emit
18
+ } from "./index-z6qreakh.js";
19
+ import {
20
+ ExecutionProfileSchema,
21
+ PlanSchema,
22
+ TaskStatusSchema,
23
+ init_loader,
24
+ loadPluginConfig
25
+ } from "./index-seetnhpx.js";
26
+ import {
27
+ init_package,
28
+ package_default
29
+ } from "./index-exatbqwj.js";
30
+ import {
31
+ criticalWarn,
32
+ init_logger,
33
+ log
34
+ } from "./index-zgwm4ryv.js";
35
+
36
+ // src/plan/planning-profile.ts
37
+ init_loader();
38
+ function renderPlanningProfileDirective(resolution, context = "runtime") {
39
+ const header = context === "repository_default" ? `[PLANNING PROFILE DEFAULT \u2014 USE ONLY WHEN NO RUNTIME RESOLUTION EXISTS] effective=${resolution.effective} source=${resolution.source}` : `[PLANNING PROFILE \u2014 CURRENT RUNTIME AUTHORITY] effective=${resolution.effective} source=${resolution.source}`;
40
+ const precedence = context === "runtime" ? `
41
+ This runtime resolution supersedes any planning-profile default in the base prompt.` : "";
42
+ if (resolution.effective === "balanced") {
43
+ return `${header}${precedence}
44
+ BALANCED ceremony: use durable QA and execution defaults without pausing for the full questionnaire. Do not require a spec solely as ceremony. Ask the user only about unresolved material ambiguity, destructive or high-risk authorization, or another decision only they can make. save_plan exact-binds the default QA profile; persist planning_profile: "balanced" in the execution profile. Follow persisted QA gates after save.`;
45
+ }
46
+ return `${header}${precedence}
47
+ STRICT ceremony: require an effective spec, run the complete clarification funnel, present the unified QA/execution questionnaire, wait for the user's answers, exact-bind those choices, and persist planning_profile: "strict". A locked legacy profile with no field uses this ceremony without materializing a new hash field.`;
48
+ }
49
+ function resolveRepositoryDefaultPlanningProfile(config) {
50
+ return config.execution_mode === "strict" ? "strict" : "balanced";
51
+ }
52
+ function resolvePlanningProfile({
53
+ directory,
54
+ incomingExecutionProfile,
55
+ existingExecutionProfile,
56
+ resetStatuses,
57
+ config
58
+ }) {
59
+ if (incomingExecutionProfile?.planning_profile) {
60
+ return {
61
+ effective: incomingExecutionProfile.planning_profile,
62
+ persisted: incomingExecutionProfile.planning_profile,
63
+ source: "incoming"
64
+ };
65
+ }
66
+ const existingProfile = resetStatuses ? undefined : existingExecutionProfile;
67
+ if (existingProfile?.planning_profile) {
68
+ return {
69
+ effective: existingProfile.planning_profile,
70
+ persisted: existingProfile.planning_profile,
71
+ source: "persisted"
72
+ };
73
+ }
74
+ if (existingProfile?.locked === true) {
75
+ return {
76
+ effective: "strict",
77
+ persisted: undefined,
78
+ source: "legacy_locked_default"
79
+ };
80
+ }
81
+ const loadedConfig = config ?? loadPluginConfig(directory);
82
+ const repositoryDefault = resolveRepositoryDefaultPlanningProfile(loadedConfig);
83
+ return {
84
+ effective: repositoryDefault,
85
+ persisted: repositoryDefault,
86
+ source: "repository_default"
87
+ };
88
+ }
89
+ function normalizeExecutionProfileForHash(profile) {
90
+ if (!profile) {
91
+ return;
92
+ }
93
+ return {
94
+ ...profile,
95
+ commit_after_each_completed_task: profile.commit_after_each_completed_task === true ? true : undefined
96
+ };
97
+ }
98
+
99
+ // src/plan/ledger.ts
100
+ init_package();
101
+ import * as crypto from "crypto";
102
+ import * as fs from "fs";
103
+ import * as path2 from "path";
104
+ init_logger();
105
+
106
+ // src/plan/ledger-sqlite.ts
107
+ import { createHash } from "crypto";
108
+ import { existsSync } from "fs";
109
+ import path from "path";
110
+ class SqliteLedgerStaleWriterError extends Error {
111
+ constructor(message) {
112
+ super(message);
113
+ this.name = "SqliteLedgerStaleWriterError";
114
+ }
115
+ }
116
+
117
+ class SqliteLedgerImportError extends Error {
118
+ constructor(message) {
119
+ super(message);
120
+ this.name = "SqliteLedgerImportError";
121
+ }
122
+ }
123
+ function withDirectory(first, second) {
124
+ if (typeof first === "string") {
125
+ if (second === undefined)
126
+ throw new TypeError("SQLite ledger input is required");
127
+ return { directory: first, input: second };
128
+ }
129
+ const { directory, ...input } = first;
130
+ return { directory, input };
131
+ }
132
+ function bytesOf(value) {
133
+ if (typeof value === "string")
134
+ return new TextEncoder().encode(value);
135
+ if (value instanceof ArrayBuffer)
136
+ return new Uint8Array(value.slice(0));
137
+ return new Uint8Array(value);
138
+ }
139
+ function canonicalJson(value) {
140
+ return bytesOf(JSON.stringify(value));
141
+ }
142
+ function sha256(bytes) {
143
+ return createHash("sha256").update(bytes).digest("hex");
144
+ }
145
+ function parseCanonical(bytes) {
146
+ let text;
147
+ try {
148
+ text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
149
+ } catch (error) {
150
+ throw new SqliteLedgerImportError(`Canonical plan-ledger event is not valid UTF-8: ${String(error)}`);
151
+ }
152
+ let parsed;
153
+ try {
154
+ parsed = JSON.parse(text);
155
+ } catch (error) {
156
+ throw new SqliteLedgerImportError(`Canonical plan-ledger event is not valid JSON: ${String(error)}`);
157
+ }
158
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
159
+ throw new SqliteLedgerImportError("Canonical plan-ledger event must be a JSON object");
160
+ }
161
+ return parsed;
162
+ }
163
+ function stringOrNull(value) {
164
+ return typeof value === "string" ? value : value === null ? null : null;
165
+ }
166
+ function numberOrNull(value) {
167
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
168
+ }
169
+ function metadataFor(input, bytes, parsed, index) {
170
+ const provided = input.metadata ?? {};
171
+ const get = (_camel, snake) => parsed[snake];
172
+ const seqValue = get("seq", "seq") ?? (index === undefined ? undefined : index + 1);
173
+ const seq = typeof seqValue === "number" && Number.isInteger(seqValue) ? seqValue : NaN;
174
+ const timestamp = get("timestamp", "timestamp");
175
+ const planId = get("planId", "plan_id");
176
+ const eventType = get("eventType", "event_type");
177
+ const source = get("source", "source");
178
+ const planHashBefore = get("planHashBefore", "plan_hash_before");
179
+ const planHashAfter = get("planHashAfter", "plan_hash_after");
180
+ const schemaVersion = get("schemaVersion", "schema_version");
181
+ if (!Number.isInteger(seq) || seq < 1 || typeof timestamp !== "string" || typeof planId !== "string" || typeof eventType !== "string" || typeof source !== "string" || typeof planHashBefore !== "string" || typeof planHashAfter !== "string" || typeof schemaVersion !== "string") {
182
+ throw new SqliteLedgerImportError(`Canonical event at index ${index ?? seqValue ?? "?"} is missing required typed metadata`);
183
+ }
184
+ const payload = parsed.payload;
185
+ const payloadRecord = payload && typeof payload === "object" ? payload : null;
186
+ const metadata = {
187
+ seq,
188
+ timestamp,
189
+ planId,
190
+ eventType,
191
+ taskId: stringOrNull(get("taskId", "task_id")),
192
+ phaseId: numberOrNull(get("phaseId", "phase_id")),
193
+ fromStatus: stringOrNull(get("fromStatus", "from_status")),
194
+ toStatus: stringOrNull(get("toStatus", "to_status")),
195
+ source,
196
+ planHashBefore,
197
+ planHashAfter,
198
+ schemaVersion,
199
+ eventHash: sha256(bytes),
200
+ rootEventHash: stringOrNull(get("rootEventHash", "root_event_hash") ?? payloadRecord?.root_event_hash),
201
+ planEpoch: stringOrNull(get("planEpoch", "plan_epoch") ?? payloadRecord?.plan_epoch),
202
+ payloadHash: stringOrNull(get("payloadHash", "payload_hash") ?? payloadRecord?.payload_hash),
203
+ payload: payload ?? null
204
+ };
205
+ const typedInputs = [
206
+ ["seq", "seq", "seq"],
207
+ ["timestamp", "timestamp", "timestamp"],
208
+ ["planId", "plan_id", "planId"],
209
+ ["eventType", "event_type", "eventType"],
210
+ ["taskId", "task_id", "taskId"],
211
+ ["phaseId", "phase_id", "phaseId"],
212
+ ["fromStatus", "from_status", "fromStatus"],
213
+ ["toStatus", "to_status", "toStatus"],
214
+ ["source", "source", "source"],
215
+ ["planHashBefore", "plan_hash_before", "planHashBefore"],
216
+ ["planHashAfter", "plan_hash_after", "planHashAfter"],
217
+ ["schemaVersion", "schema_version", "schemaVersion"],
218
+ ["rootEventHash", "root_event_hash", "rootEventHash"],
219
+ ["planEpoch", "plan_epoch", "planEpoch"],
220
+ ["payloadHash", "payload_hash", "payloadHash"]
221
+ ];
222
+ for (const [camel, snake, key] of typedInputs) {
223
+ const supplied = provided[camel] ?? provided[snake] ?? input[snake];
224
+ if (supplied !== undefined && supplied !== metadata[key]) {
225
+ throw new SqliteLedgerImportError(`Supplied SQLite plan-ledger metadata ${snake} disagrees with canonical event bytes`);
226
+ }
227
+ }
228
+ return metadata;
229
+ }
230
+ function normalizeEvent(value, index) {
231
+ const input = typeof value === "string" || value instanceof Uint8Array || value instanceof ArrayBuffer ? { canonicalEvent: value } : value;
232
+ const raw = input.canonicalEvent ?? input.canonical_event ?? input.bytes;
233
+ const bytes = raw === undefined ? canonicalJson(input.event) : bytesOf(raw);
234
+ if (bytes.length === 0)
235
+ throw new SqliteLedgerImportError("Canonical plan-ledger event is empty");
236
+ const parsed = parseCanonical(bytes);
237
+ return { bytes, parsed, metadata: metadataFor(input, bytes, parsed, index) };
238
+ }
239
+ function stateValue(input, camel, snake) {
240
+ if (!input)
241
+ return;
242
+ return input[camel] ?? input[snake];
243
+ }
244
+ function projectionBytes(input) {
245
+ const value = stateValue(input, "terminalProjection", "terminal_projection");
246
+ if (value === undefined || value === null)
247
+ return value;
248
+ return bytesOf(value);
249
+ }
250
+ function jsonValue(value) {
251
+ if (value === undefined || value === null)
252
+ return null;
253
+ if (typeof value === "string")
254
+ return value;
255
+ return JSON.stringify(value);
256
+ }
257
+ function decodeBytes(value) {
258
+ if (value === null || value === undefined)
259
+ return null;
260
+ if (value instanceof Uint8Array)
261
+ return new Uint8Array(value);
262
+ if (value instanceof ArrayBuffer)
263
+ return new Uint8Array(value.slice(0));
264
+ return null;
265
+ }
266
+ function readStateFromRow(row) {
267
+ if (!row)
268
+ return null;
269
+ const authority = row.authority_mode;
270
+ const parity = row.parity_status;
271
+ if (authority !== "file_shadow" && authority !== "sqlite" || parity !== "pending" && parity !== "clean" && parity !== "diverged" && parity !== "failed") {
272
+ throw new Error("Invalid plan_ledger_state authority or parity value");
273
+ }
274
+ const projection = decodeBytes(row.terminal_projection);
275
+ let metadata = null;
276
+ if (typeof row.terminal_metadata === "string") {
277
+ try {
278
+ metadata = JSON.parse(row.terminal_metadata);
279
+ } catch {
280
+ metadata = row.terminal_metadata;
281
+ }
282
+ }
283
+ return {
284
+ id: 1,
285
+ authorityMode: authority,
286
+ shadowStartedVersion: stringOrNull(row.shadow_started_version),
287
+ parityStatus: parity,
288
+ fileReplayHash: stringOrNull(row.file_replay_hash),
289
+ sqliteReplayHash: stringOrNull(row.sqlite_replay_hash),
290
+ terminalProjectionHash: typeof row.terminal_projection_hash === "string" ? row.terminal_projection_hash : "",
291
+ lastSeq: typeof row.last_seq === "number" ? row.last_seq : Number(row.last_seq ?? 0),
292
+ lastEventHash: stringOrNull(row.last_event_hash),
293
+ rootEventHash: stringOrNull(row.root_event_hash),
294
+ planId: stringOrNull(row.plan_id),
295
+ planEpoch: stringOrNull(row.plan_epoch),
296
+ terminalPlanHash: stringOrNull(row.terminal_plan_hash),
297
+ terminalProjection: projection,
298
+ terminalProjectionJson: stringOrNull(row.terminal_projection_json),
299
+ terminalMetadata: metadata,
300
+ updatedAt: typeof row.updated_at === "string" ? row.updated_at : "",
301
+ authority_mode: authority,
302
+ shadow_started_version: stringOrNull(row.shadow_started_version),
303
+ parity_status: parity,
304
+ file_replay_hash: stringOrNull(row.file_replay_hash),
305
+ sqlite_replay_hash: stringOrNull(row.sqlite_replay_hash),
306
+ terminal_projection_hash: typeof row.terminal_projection_hash === "string" ? row.terminal_projection_hash : "",
307
+ last_seq: typeof row.last_seq === "number" ? row.last_seq : Number(row.last_seq ?? 0)
308
+ };
309
+ }
310
+ function readState(db) {
311
+ return readStateFromRow(db.query("SELECT * FROM plan_ledger_state WHERE id = 1").get());
312
+ }
313
+ function rowMetadata(row) {
314
+ return {
315
+ seq: Number(row.seq),
316
+ timestamp: String(row.timestamp),
317
+ planId: String(row.plan_id),
318
+ eventType: String(row.event_type),
319
+ taskId: stringOrNull(row.task_id),
320
+ phaseId: numberOrNull(row.phase_id),
321
+ fromStatus: stringOrNull(row.from_status),
322
+ toStatus: stringOrNull(row.to_status),
323
+ source: String(row.source),
324
+ planHashBefore: String(row.plan_hash_before),
325
+ planHashAfter: String(row.plan_hash_after),
326
+ schemaVersion: String(row.schema_version),
327
+ eventHash: String(row.event_hash),
328
+ rootEventHash: stringOrNull(row.root_event_hash),
329
+ planEpoch: stringOrNull(row.plan_epoch),
330
+ payloadHash: stringOrNull(row.payload_hash),
331
+ payload: typeof row.payload_json === "string" ? (() => {
332
+ try {
333
+ return JSON.parse(row.payload_json);
334
+ } catch {
335
+ return row.payload_json;
336
+ }
337
+ })() : null
338
+ };
339
+ }
340
+ function rowEvent(row) {
341
+ const canonical = decodeBytes(row.canonical_event);
342
+ if (!canonical)
343
+ throw new Error(`SQLite plan-ledger event ${String(row.seq)} has no BLOB bytes`);
344
+ const parsed = parseCanonical(canonical);
345
+ const metadata = rowMetadata(row);
346
+ const canonicalMetadata = metadataFor({ canonicalEvent: canonical }, canonical, parsed);
347
+ const checkedMetadata = [
348
+ "seq",
349
+ "timestamp",
350
+ "planId",
351
+ "eventType",
352
+ "taskId",
353
+ "phaseId",
354
+ "fromStatus",
355
+ "toStatus",
356
+ "source",
357
+ "planHashBefore",
358
+ "planHashAfter",
359
+ "schemaVersion",
360
+ "eventHash",
361
+ "rootEventHash",
362
+ "planEpoch",
363
+ "payloadHash"
364
+ ].map((key) => [key, metadata[key], canonicalMetadata[key]]);
365
+ const mismatch = checkedMetadata.find(([, actual, expected]) => actual !== expected);
366
+ const payloadMismatch = JSON.stringify(metadata.payload) !== JSON.stringify(canonicalMetadata.payload);
367
+ if (mismatch || payloadMismatch)
368
+ throw new Error(`SQLite plan-ledger event ${metadata.seq} typed metadata mismatch (${mismatch?.[0] ?? "payload"})`);
369
+ return {
370
+ ...metadata,
371
+ canonicalEvent: canonical,
372
+ canonical_event: canonical,
373
+ event: parsed,
374
+ parsed
375
+ };
376
+ }
377
+ function currentStateForWrite(db) {
378
+ return readState(db);
379
+ }
380
+ function upsertState(db, input, fallback) {
381
+ const existing = currentStateForWrite(db);
382
+ const authorityMode = stateValue(input, "authorityMode", "authority_mode") ?? stateValue(fallback, "authorityMode", "authority_mode") ?? existing?.authorityMode ?? "sqlite";
383
+ const shadowVersion = stateValue(input, "shadowStartedVersion", "shadow_started_version") ?? stateValue(fallback, "shadowStartedVersion", "shadow_started_version") ?? existing?.shadowStartedVersion ?? null;
384
+ const parityStatus = stateValue(input, "parityStatus", "parity_status") ?? stateValue(fallback, "parityStatus", "parity_status") ?? existing?.parityStatus ?? "pending";
385
+ const fileHash = stateValue(input, "fileReplayHash", "file_replay_hash") ?? stateValue(fallback, "fileReplayHash", "file_replay_hash") ?? existing?.fileReplayHash ?? null;
386
+ const sqliteHash = stateValue(input, "sqliteReplayHash", "sqlite_replay_hash") ?? stateValue(fallback, "sqliteReplayHash", "sqlite_replay_hash") ?? existing?.sqliteReplayHash ?? null;
387
+ const projection = projectionBytes(input) ?? projectionBytes(fallback) ?? existing?.terminalProjection ?? null;
388
+ const projectionHash = stateValue(input, "terminalProjectionHash", "terminal_projection_hash") ?? stateValue(fallback, "terminalProjectionHash", "terminal_projection_hash") ?? (projection ? sha256(projection) : existing?.terminalProjectionHash ?? "");
389
+ const lastSeq = stateValue(input, "lastSeq", "last_seq") ?? stateValue(fallback, "lastSeq", "last_seq") ?? existing?.lastSeq ?? 0;
390
+ const lastEventHash = stateValue(input, "lastEventHash", "last_event_hash") ?? stateValue(fallback, "lastEventHash", "last_event_hash") ?? existing?.lastEventHash ?? null;
391
+ const rootEventHash = stateValue(input, "rootEventHash", "root_event_hash") ?? stateValue(fallback, "rootEventHash", "root_event_hash") ?? existing?.rootEventHash ?? null;
392
+ const planId = stateValue(input, "planId", "plan_id") ?? stateValue(fallback, "planId", "plan_id") ?? existing?.planId ?? null;
393
+ const planEpoch = stateValue(input, "planEpoch", "plan_epoch") ?? stateValue(fallback, "planEpoch", "plan_epoch") ?? existing?.planEpoch ?? null;
394
+ const terminalPlanHash = stateValue(input, "terminalPlanHash", "terminal_plan_hash") ?? stateValue(fallback, "terminalPlanHash", "terminal_plan_hash") ?? existing?.terminalPlanHash ?? null;
395
+ const projectionJson = stateValue(input, "terminalProjectionJson", "terminal_projection_json") ?? stateValue(fallback, "terminalProjectionJson", "terminal_projection_json") ?? existing?.terminalProjectionJson ?? (projection ? new TextDecoder().decode(projection) : null);
396
+ const terminalMetadata = jsonValue(input?.terminalMetadata ?? input?.terminal_metadata) ?? jsonValue(fallback.terminalMetadata ?? fallback.terminal_metadata) ?? (existing?.terminalMetadata === null ? null : jsonValue(existing?.terminalMetadata));
397
+ if (authorityMode !== "file_shadow" && authorityMode !== "sqlite")
398
+ throw new Error(`Invalid plan-ledger authority mode: ${authorityMode}`);
399
+ if (!["pending", "clean", "diverged", "failed"].includes(parityStatus))
400
+ throw new Error(`Invalid plan-ledger parity status: ${parityStatus}`);
401
+ db.run(`INSERT INTO plan_ledger_state (
402
+ id, authority_mode, shadow_started_version, parity_status, file_replay_hash,
403
+ sqlite_replay_hash, terminal_projection_hash, last_seq, last_event_hash,
404
+ root_event_hash, plan_id, plan_epoch, terminal_plan_hash, terminal_projection,
405
+ terminal_projection_json, terminal_metadata, updated_at
406
+ ) VALUES (1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
407
+ ON CONFLICT(id) DO UPDATE SET
408
+ authority_mode = excluded.authority_mode,
409
+ shadow_started_version = excluded.shadow_started_version,
410
+ parity_status = excluded.parity_status,
411
+ file_replay_hash = excluded.file_replay_hash,
412
+ sqlite_replay_hash = excluded.sqlite_replay_hash,
413
+ terminal_projection_hash = excluded.terminal_projection_hash,
414
+ last_seq = excluded.last_seq,
415
+ last_event_hash = excluded.last_event_hash,
416
+ root_event_hash = excluded.root_event_hash,
417
+ plan_id = excluded.plan_id,
418
+ plan_epoch = excluded.plan_epoch,
419
+ terminal_plan_hash = excluded.terminal_plan_hash,
420
+ terminal_projection = excluded.terminal_projection,
421
+ terminal_projection_json = excluded.terminal_projection_json,
422
+ terminal_metadata = excluded.terminal_metadata,
423
+ updated_at = excluded.updated_at`, [
424
+ authorityMode,
425
+ shadowVersion,
426
+ parityStatus,
427
+ fileHash,
428
+ sqliteHash,
429
+ projectionHash,
430
+ lastSeq,
431
+ lastEventHash,
432
+ rootEventHash,
433
+ planId,
434
+ planEpoch,
435
+ terminalPlanHash,
436
+ projection,
437
+ projectionJson,
438
+ terminalMetadata,
439
+ new Date().toISOString()
440
+ ]);
441
+ }
442
+ function insertEvent(db, event) {
443
+ const m = event.metadata;
444
+ const existing = db.query("SELECT * FROM plan_ledger_event WHERE seq = ?").get(m.seq);
445
+ if (existing) {
446
+ const current = rowEvent(existing);
447
+ if (current.eventHash !== m.eventHash || !current.canonicalEvent.every((v, i) => v === event.bytes[i]))
448
+ throw new SqliteLedgerStaleWriterError(`SQLite plan-ledger sequence ${m.seq} already contains a different event`);
449
+ return current;
450
+ }
451
+ db.run(`INSERT INTO plan_ledger_event (
452
+ seq, canonical_event, event_hash, root_event_hash, plan_epoch, timestamp, plan_id,
453
+ event_type, task_id, phase_id, from_status, to_status, source, plan_hash_before,
454
+ plan_hash_after, schema_version, payload_hash, payload_json
455
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
456
+ m.seq,
457
+ event.bytes,
458
+ m.eventHash,
459
+ m.rootEventHash,
460
+ m.planEpoch,
461
+ m.timestamp,
462
+ m.planId,
463
+ m.eventType,
464
+ m.taskId,
465
+ m.phaseId,
466
+ m.fromStatus,
467
+ m.toStatus,
468
+ m.source,
469
+ m.planHashBefore,
470
+ m.planHashAfter,
471
+ m.schemaVersion,
472
+ m.payloadHash,
473
+ jsonValue(m.payload)
474
+ ]);
475
+ return {
476
+ ...m,
477
+ canonicalEvent: new Uint8Array(event.bytes),
478
+ canonical_event: new Uint8Array(event.bytes),
479
+ event: event.parsed,
480
+ parsed: event.parsed
481
+ };
482
+ }
483
+ function appendInTransaction(db, input) {
484
+ const normalized = normalizeEvent({
485
+ canonicalEvent: input.canonicalEvent,
486
+ metadata: input.metadata
487
+ });
488
+ const maxSeq = db.query("SELECT MAX(seq) AS max FROM plan_ledger_event").get()?.max ?? 0;
489
+ const expectedSeq = input.expectedSeq ?? input.expected_seq;
490
+ if (expectedSeq !== undefined && expectedSeq !== maxSeq)
491
+ throw new SqliteLedgerStaleWriterError(`Stale SQLite plan-ledger writer: expected seq ${expectedSeq} but found ${maxSeq}`);
492
+ const expectedHash = input.expectedHash ?? input.expected_hash;
493
+ if (expectedHash !== undefined) {
494
+ const actual = db.query("SELECT plan_hash_after FROM plan_ledger_event ORDER BY seq DESC LIMIT 1").get()?.plan_hash_after ?? null;
495
+ if (actual !== expectedHash)
496
+ throw new SqliteLedgerStaleWriterError(`Stale SQLite plan-ledger writer: expected hash ${expectedHash} but found ${actual ?? "<missing>"}`);
497
+ }
498
+ if (normalized.metadata.seq !== maxSeq + 1)
499
+ throw new SqliteLedgerStaleWriterError(`SQLite plan-ledger append expected seq ${maxSeq + 1} but received ${normalized.metadata.seq}`);
500
+ const row = insertEvent(db, normalized);
501
+ upsertState(db, input.state, {
502
+ lastSeq: row.seq,
503
+ lastEventHash: row.eventHash,
504
+ rootEventHash: row.rootEventHash,
505
+ planId: row.planId,
506
+ planEpoch: row.planEpoch,
507
+ terminalPlanHash: row.planHashAfter
508
+ });
509
+ return row;
510
+ }
511
+ function hasSqliteLedger(directory) {
512
+ if (!projectDbExists(directory))
513
+ return false;
514
+ const db = getProjectDb(directory);
515
+ return (db.query("SELECT COUNT(*) AS count FROM plan_ledger_event").get()?.count ?? 0) > 0;
516
+ }
517
+ function getPlanLedgerState(directory) {
518
+ if (!projectDbExists(directory))
519
+ return null;
520
+ return readState(getProjectDb(directory));
521
+ }
522
+ function validateStateAgainstRows(state, rows) {
523
+ if (rows.length === 0)
524
+ return;
525
+ if (!state)
526
+ throw new Error("SQLite plan ledger has events but no authority state");
527
+ const last = rows.at(-1);
528
+ const expectedPlanEpoch = [...rows].reverse().find((row) => row.planEpoch !== null)?.planEpoch ?? null;
529
+ const expectedRootEventHash = [...rows].reverse().find((row) => row.rootEventHash !== null)?.rootEventHash ?? rows[0].eventHash;
530
+ const projectionHash = state.terminalProjection ? sha256(state.terminalProjection) : state.terminalProjectionJson ? sha256(new TextEncoder().encode(state.terminalProjectionJson)) : "";
531
+ const stateChecks = [
532
+ ["last_seq", state.lastSeq, last.seq],
533
+ ["last_event_hash", state.lastEventHash, last.eventHash],
534
+ ["root_event_hash", state.rootEventHash, expectedRootEventHash],
535
+ ["plan_id", state.planId, last.planId],
536
+ ["plan_epoch", state.planEpoch, expectedPlanEpoch],
537
+ ["terminal_plan_hash", state.terminalPlanHash, last.planHashAfter]
538
+ ];
539
+ if (state.terminalProjectionHash !== "") {
540
+ stateChecks.push([
541
+ "terminal_projection_hash",
542
+ state.terminalProjectionHash,
543
+ projectionHash
544
+ ]);
545
+ }
546
+ const mismatch = stateChecks.find(([, actual, expected]) => actual !== expected);
547
+ if (mismatch) {
548
+ throw new Error(`SQLite plan-ledger state metadata mismatch (${mismatch[0]}: state=${String(mismatch[1])}, rows=${String(mismatch[2])})`);
549
+ }
550
+ }
551
+ function readSqliteLedgerEvents(directory) {
552
+ if (!projectDbExists(directory))
553
+ return { events: [], state: null, import: null };
554
+ const db = getProjectDb(directory);
555
+ const rows = db.query("SELECT * FROM plan_ledger_event ORDER BY seq ASC").all().map(rowEvent);
556
+ const importRow = db.query("SELECT * FROM plan_ledger_import ORDER BY imported_at DESC, source ASC LIMIT 1").get();
557
+ const state = readState(db);
558
+ validateStateAgainstRows(state, rows);
559
+ return {
560
+ events: rows,
561
+ state,
562
+ import: importRow ? {
563
+ source: String(importRow.source),
564
+ sourceHash: String(importRow.source_hash),
565
+ archivePath: stringOrNull(importRow.archive_path),
566
+ archiveHash: stringOrNull(importRow.archive_hash),
567
+ archiveSize: typeof importRow.archive_size === "number" ? importRow.archive_size : null,
568
+ archiveCreatedAt: stringOrNull(importRow.archive_created_at),
569
+ mode: String(importRow.mode),
570
+ version: stringOrNull(importRow.version),
571
+ rowCount: Number(importRow.row_count ?? 0),
572
+ importedAt: String(importRow.imported_at)
573
+ } : null
574
+ };
575
+ }
576
+ function readSqliteLedgerEventsReadOnly(directory) {
577
+ const resetMarker = path.join(directory, ".swarm", "plan-ledger.resetting");
578
+ if (existsSync(resetMarker))
579
+ return { events: [], state: null, import: null };
580
+ try {
581
+ const result = withProjectDbReadOnly(directory, (db) => {
582
+ const table = db.query("SELECT 1 AS present FROM sqlite_master WHERE type = 'table' AND name = 'plan_ledger_event'").get();
583
+ if (!table)
584
+ return { events: [], state: null, import: null };
585
+ const rows = db.query("SELECT * FROM plan_ledger_event ORDER BY seq ASC").all().map(rowEvent);
586
+ const state = readState(db);
587
+ validateStateAgainstRows(state, rows);
588
+ return { events: rows, state, import: null };
589
+ }) ?? { events: [], state: null, import: null };
590
+ return existsSync(resetMarker) ? { events: [], state: null, import: null } : result;
591
+ } catch (error) {
592
+ const message = error instanceof Error ? error.message : String(error);
593
+ if (/no such table|no such column/i.test(message)) {
594
+ return { events: [], state: null, import: null };
595
+ }
596
+ throw error;
597
+ }
598
+ }
599
+ function appendSqliteLedger(first, second) {
600
+ const { directory, input } = withDirectory(first, second);
601
+ return withImmediateTransaction(getProjectDb(directory), "full", () => appendInTransaction(getProjectDb(directory), input));
602
+ }
603
+ function importInTransaction(db, input, replace) {
604
+ const existingCount = db.query("SELECT COUNT(*) AS count FROM plan_ledger_event").get()?.count ?? 0;
605
+ if (existingCount > 0 && !replace) {
606
+ const existing = readSqliteRows(db);
607
+ const incoming = input.canonicalEvents.map((event, index) => normalizeEvent(event, index));
608
+ if (existing.length !== incoming.length || existing.some((row, index) => row.eventHash !== incoming[index]?.metadata.eventHash))
609
+ throw new SqliteLedgerImportError("SQLite plan-ledger is not empty and does not match the requested import");
610
+ } else {
611
+ if (replace) {
612
+ db.run("DELETE FROM plan_ledger_event");
613
+ db.run("DELETE FROM plan_ledger_import");
614
+ }
615
+ let previousSeq = 0;
616
+ for (const [index, raw] of input.canonicalEvents.entries()) {
617
+ const event = normalizeEvent(raw, index);
618
+ if (event.metadata.seq !== previousSeq + 1)
619
+ throw new SqliteLedgerImportError(`SQLite plan-ledger import sequence must be contiguous at ${event.metadata.seq}`);
620
+ insertEvent(db, event);
621
+ previousSeq = event.metadata.seq;
622
+ }
623
+ }
624
+ const events = readSqliteRows(db);
625
+ const last = events.at(-1);
626
+ upsertState(db, input.state, {
627
+ authorityMode: input.state?.authorityMode ?? input.state?.authority_mode ?? "file_shadow",
628
+ lastSeq: last?.seq ?? 0,
629
+ lastEventHash: last?.eventHash ?? null,
630
+ rootEventHash: [...events].reverse().find((event) => event.rootEventHash !== null)?.rootEventHash ?? events[0]?.eventHash ?? null,
631
+ planId: last?.planId ?? null,
632
+ planEpoch: [...events].reverse().find((event) => event.planEpoch !== null)?.planEpoch ?? null,
633
+ terminalPlanHash: last?.planHashAfter ?? null
634
+ });
635
+ const source = input.source ?? "plan-ledger.jsonl";
636
+ const sourceHash = input.sourceHash ?? input.source_hash ?? sha256(new TextEncoder().encode(events.map((e) => new TextDecoder().decode(e.canonicalEvent)).join(`
637
+ `)));
638
+ const archivePath = input.archivePath ?? input.archive_path ?? null;
639
+ const archiveHash = input.archiveHash ?? input.archive_hash ?? null;
640
+ const archiveSize = input.archiveSize ?? input.archive_size ?? null;
641
+ const archiveCreatedAt = input.archiveCreatedAt ?? input.archive_created_at ?? null;
642
+ const mode = input.mode ?? "file_shadow";
643
+ const version = input.version ?? null;
644
+ const priorImport = db.query("SELECT source_hash FROM plan_ledger_import WHERE source = ?").get(source);
645
+ if (priorImport && priorImport.source_hash !== sourceHash)
646
+ throw new SqliteLedgerImportError(`SQLite plan-ledger source ${source} was already imported with a different hash`);
647
+ db.run(`INSERT INTO plan_ledger_import (
648
+ source, source_hash, archive_path, archive_hash, archive_size, archive_created_at,
649
+ mode, version, row_count, imported_at
650
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
651
+ ON CONFLICT(source) DO UPDATE SET
652
+ source_hash = excluded.source_hash, archive_path = excluded.archive_path,
653
+ archive_hash = excluded.archive_hash, archive_size = excluded.archive_size,
654
+ archive_created_at = excluded.archive_created_at, mode = excluded.mode,
655
+ version = excluded.version, row_count = excluded.row_count,
656
+ imported_at = excluded.imported_at`, [
657
+ source,
658
+ sourceHash,
659
+ archivePath,
660
+ archiveHash,
661
+ archiveSize,
662
+ archiveCreatedAt,
663
+ mode,
664
+ version,
665
+ events.length,
666
+ new Date().toISOString()
667
+ ]);
668
+ return readSqliteLedgerEventsFromDb(db);
669
+ }
670
+ function readSqliteRows(db) {
671
+ return db.query("SELECT * FROM plan_ledger_event ORDER BY seq ASC").all().map(rowEvent);
672
+ }
673
+ function readSqliteLedgerEventsFromDb(db) {
674
+ const events = readSqliteRows(db);
675
+ const row = db.query("SELECT * FROM plan_ledger_import ORDER BY imported_at DESC, source ASC LIMIT 1").get();
676
+ return {
677
+ events,
678
+ state: readState(db),
679
+ import: row ? {
680
+ source: String(row.source),
681
+ sourceHash: String(row.source_hash),
682
+ archivePath: stringOrNull(row.archive_path),
683
+ archiveHash: stringOrNull(row.archive_hash),
684
+ archiveSize: typeof row.archive_size === "number" ? row.archive_size : null,
685
+ archiveCreatedAt: stringOrNull(row.archive_created_at),
686
+ mode: String(row.mode),
687
+ version: stringOrNull(row.version),
688
+ rowCount: Number(row.row_count ?? 0),
689
+ importedAt: String(row.imported_at)
690
+ } : null
691
+ };
692
+ }
693
+ function importSqliteLedger(first, second) {
694
+ const { directory, input } = withDirectory(first, second);
695
+ return withImmediateTransaction(getProjectDb(directory), "full", () => importInTransaction(getProjectDb(directory), input, false));
696
+ }
697
+ function replaceSqliteLedger(first, second) {
698
+ const { directory, input } = withDirectory(first, second);
699
+ return withImmediateTransaction(getProjectDb(directory), "full", () => importInTransaction(getProjectDb(directory), input, true));
700
+ }
701
+ function recordSqliteLedgerParity(first, second) {
702
+ const { directory, input } = withDirectory(first, second);
703
+ const db = getProjectDb(directory);
704
+ return withImmediateTransaction(db, "full", () => {
705
+ const fileHash = input.fileReplayHash ?? input.file_replay_hash;
706
+ const sqliteHash = input.sqliteReplayHash ?? input.sqlite_replay_hash;
707
+ if (fileHash === undefined || sqliteHash === undefined)
708
+ throw new Error("Parity requires both file_replay_hash and sqlite_replay_hash");
709
+ const status = input.parityStatus ?? input.parity_status ?? (fileHash === sqliteHash ? "clean" : "diverged");
710
+ upsertState(db, {
711
+ fileReplayHash: fileHash,
712
+ sqliteReplayHash: sqliteHash,
713
+ terminalProjectionHash: input.terminalProjectionHash ?? input.terminal_projection_hash,
714
+ parityStatus: status
715
+ }, {});
716
+ return readState(db);
717
+ });
718
+ }
719
+ function cutoverSqliteLedger(first, second) {
720
+ const { directory, input } = typeof first === "string" ? { directory: first, input: second } : { directory: first.directory, input: first };
721
+ const db = getProjectDb(directory);
722
+ return withImmediateTransaction(db, "full", () => {
723
+ const state = readState(db);
724
+ if (!state)
725
+ throw new Error("Cannot cut over an uninitialized SQLite plan ledger");
726
+ if (state.parityStatus !== "clean" || !state.fileReplayHash || state.fileReplayHash !== state.sqliteReplayHash)
727
+ throw new Error("Cannot cut over SQLite plan ledger before a clean parity record");
728
+ const expected = input?.expectedShadowStartedVersion ?? input?.expected_shadow_started_version;
729
+ if (expected !== undefined && state.shadowStartedVersion !== expected)
730
+ throw new SqliteLedgerStaleWriterError(`SQLite plan-ledger shadow version changed: expected ${expected}, found ${state.shadowStartedVersion ?? "<missing>"}`);
731
+ db.run("UPDATE plan_ledger_state SET authority_mode = 'sqlite', updated_at = ? WHERE id = 1", [new Date().toISOString()]);
732
+ return readState(db);
733
+ });
734
+ }
735
+ function clearSqliteLedger(directory) {
736
+ if (!projectDbExists(directory))
737
+ return;
738
+ const db = getProjectDb(directory);
739
+ withImmediateTransaction(db, "full", () => {
740
+ db.run("DELETE FROM plan_ledger_event");
741
+ db.run("DELETE FROM plan_ledger_import");
742
+ db.run("DELETE FROM plan_ledger_state");
743
+ });
744
+ }
745
+
746
+ // src/plan/ledger.ts
747
+ var LEDGER_SCHEMA_VERSION = "1.1.0";
748
+ function resolvePlanEpochIdentity(events, authoritativePlan) {
749
+ const planId = derivePlanId(authoritativePlan);
750
+ const planIdentityHash = derivePlanIdentityHash(authoritativePlan);
751
+ const payloadHash = computePlanLedgerHash(authoritativePlan);
752
+ const candidates = extractPlanEpochCandidates(events, planId);
753
+ const distinctCandidateKeys = new Set(candidates.map((candidate) => `${candidate.planEpoch}:${candidate.rootEventHash}`));
754
+ if (distinctCandidateKeys.size > 1) {
755
+ throw new Error(`Conflicting plan epoch metadata detected for ${planId}; refusing to continue.`);
756
+ }
757
+ if (candidates.length === 0) {
758
+ return null;
759
+ }
760
+ const existing = candidates[0];
761
+ return {
762
+ planId,
763
+ planIdentityHash,
764
+ planEpoch: existing.planEpoch,
765
+ rootEventHash: existing.rootEventHash,
766
+ payloadHash,
767
+ source: existing.source
768
+ };
769
+ }
770
+ var LEDGER_FILENAME = "plan-ledger.jsonl";
771
+ var LEDGER_LOCK_PATH = path2.join(".swarm", LEDGER_FILENAME);
772
+ var PLAN_JSON_FILENAME = "plan.json";
773
+
774
+ class LedgerStaleWriterError extends Error {
775
+ constructor(message) {
776
+ super(message);
777
+ this.name = "LedgerStaleWriterError";
778
+ }
779
+ }
780
+ function getLedgerPath(directory) {
781
+ return path2.join(directory, ".swarm", LEDGER_FILENAME);
782
+ }
783
+ function getPlanJsonPath(directory) {
784
+ return path2.join(directory, ".swarm", PLAN_JSON_FILENAME);
785
+ }
786
+ function compareCodeUnits(a, b) {
787
+ return a < b ? -1 : a > b ? 1 : 0;
788
+ }
789
+ function readLedgerDirectory(directory) {
790
+ return fs.readdirSync(directory);
791
+ }
792
+ function asRecord(value) {
793
+ if (value && typeof value === "object" && !Array.isArray(value)) {
794
+ return value;
795
+ }
796
+ return null;
797
+ }
798
+ function isSha256Hex(value) {
799
+ return /^[a-f0-9]{64}$/i.test(value);
800
+ }
801
+ function isUuid(value) {
802
+ 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);
803
+ }
804
+ function serializeLedgerEvent(event) {
805
+ return JSON.stringify(event);
806
+ }
807
+ function computeLedgerEventHash(event) {
808
+ return crypto.createHash("sha256").update(serializeLedgerEvent(event), "utf8").digest("hex");
809
+ }
810
+ function extractPlanEpochCandidates(events, expectedPlanId) {
811
+ if (events.length === 0) {
812
+ throw new Error("Plan ledger is empty; missing plan_created root.");
813
+ }
814
+ const rootEvent = events[0];
815
+ if (rootEvent.event_type !== "plan_created") {
816
+ throw new Error(`Plan ledger root must be plan_created; found ${rootEvent.event_type} at seq ${rootEvent.seq}.`);
817
+ }
818
+ if (rootEvent.plan_id !== expectedPlanId) {
819
+ throw new Error(`Plan ledger identity mismatch: expected ${expectedPlanId} but found ${rootEvent.plan_id}.`);
820
+ }
821
+ const rootEventHash = computeLedgerEventHash(rootEvent);
822
+ const candidates = [];
823
+ const rootPayload = asRecord(rootEvent.payload);
824
+ const rootPlanEpoch = rootPayload?.plan_epoch;
825
+ if (typeof rootPlanEpoch === "string" && rootPlanEpoch.length > 0) {
826
+ if (!isUuid(rootPlanEpoch)) {
827
+ throw new Error("Plan ledger root carries an invalid plan_epoch.");
828
+ }
829
+ candidates.push({
830
+ planEpoch: rootPlanEpoch,
831
+ rootEventHash,
832
+ source: "root"
833
+ });
834
+ }
835
+ for (const event of events) {
836
+ if (event.event_type !== "snapshot" || event.source !== "plan_epoch_adopted") {
837
+ continue;
838
+ }
839
+ if (event.plan_id !== expectedPlanId) {
840
+ throw new Error(`Plan epoch adoption snapshot at seq ${event.seq} belongs to ${event.plan_id}, expected ${expectedPlanId}.`);
841
+ }
842
+ const payload = asRecord(event.payload);
843
+ const planEpoch = payload?.plan_epoch;
844
+ const rootHash = payload?.root_event_hash;
845
+ const embeddedPlan = PlanSchema.safeParse(payload?.plan);
846
+ if (!embeddedPlan.success) {
847
+ throw new Error(`Plan epoch adoption snapshot at seq ${event.seq} carries an invalid embedded plan.`);
848
+ }
849
+ const payloadHash = payload?.payload_hash;
850
+ if (typeof planEpoch !== "string" || !isUuid(planEpoch)) {
851
+ throw new Error(`Plan epoch adoption snapshot at seq ${event.seq} has an invalid plan_epoch.`);
852
+ }
853
+ if (typeof rootHash !== "string" || !isSha256Hex(rootHash)) {
854
+ throw new Error(`Plan epoch adoption snapshot at seq ${event.seq} has an invalid root_event_hash.`);
855
+ }
856
+ if (rootHash !== rootEventHash) {
857
+ throw new Error(`Plan epoch adoption snapshot at seq ${event.seq} does not match the canonical ledger root.`);
858
+ }
859
+ if (typeof payloadHash !== "string") {
860
+ throw new Error(`Plan epoch adoption snapshot at seq ${event.seq} is missing payload_hash.`);
861
+ }
862
+ if (derivePlanId(embeddedPlan.data) !== expectedPlanId) {
863
+ throw new Error(`Plan epoch adoption snapshot at seq ${event.seq} embeds a different plan identity.`);
864
+ }
865
+ if (computePlanLedgerHash(embeddedPlan.data) !== payloadHash) {
866
+ throw new Error(`Plan epoch adoption snapshot at seq ${event.seq} has a mismatched payload_hash.`);
867
+ }
868
+ candidates.push({
869
+ planEpoch,
870
+ rootEventHash: rootHash,
871
+ source: "plan_epoch_adopted"
872
+ });
873
+ }
874
+ return candidates;
875
+ }
876
+ function writeFileFsyncedThenRename(tempPath, targetPath, data) {
877
+ const fd = fs.openSync(tempPath, "w");
878
+ try {
879
+ fs.writeFileSync(fd, data, "utf8");
880
+ fs.fsyncSync(fd);
881
+ } finally {
882
+ fs.closeSync(fd);
883
+ }
884
+ fs.renameSync(tempPath, targetPath);
885
+ }
886
+ function fsyncRecoveryDirectory(directory) {
887
+ let fd;
888
+ try {
889
+ fd = fs.openSync(directory, "r");
890
+ fs.fsyncSync(fd);
891
+ } catch (error) {
892
+ const code = error.code;
893
+ const unsupportedOnWindows = process.platform === "win32" && ["EPERM", "EACCES", "EINVAL", "EBADF", "EISDIR", "ENOTSUP"].includes(code ?? "");
894
+ if (unsupportedOnWindows) {
895
+ log(`[ledger] Directory fsync unavailable for recovery archive ${directory}: ${error instanceof Error ? error.message : String(error)}`);
896
+ return;
897
+ }
898
+ throw new Error(`Failed to fsync recovery archive directory before canonical replacement: ${error instanceof Error ? error.message : String(error)}`);
899
+ } finally {
900
+ if (fd !== undefined)
901
+ fs.closeSync(fd);
902
+ }
903
+ }
904
+ function findRawMalformedSuffix(content) {
905
+ const decoder = new TextDecoder("utf-8", { fatal: true });
906
+ let lineStart = 0;
907
+ for (let index = 0;index <= content.length; index++) {
908
+ if (index < content.length && content[index] !== 10)
909
+ continue;
910
+ let lineEnd = index;
911
+ if (lineEnd > lineStart && content[lineEnd - 1] === 13)
912
+ lineEnd--;
913
+ const line = content.subarray(lineStart, lineEnd);
914
+ if (line.length > 0) {
915
+ try {
916
+ JSON.parse(decoder.decode(line));
917
+ } catch {
918
+ return content.subarray(lineStart);
919
+ }
920
+ }
921
+ lineStart = index + 1;
922
+ }
923
+ return Buffer.alloc(0);
924
+ }
925
+ function isStructurallyValidLedgerEvent(value) {
926
+ const event = asRecord(value);
927
+ return event !== null && Number.isSafeInteger(event.seq) && event.seq > 0 && typeof event.timestamp === "string" && typeof event.plan_id === "string" && typeof event.event_type === "string" && typeof event.source === "string" && typeof event.plan_hash_before === "string" && typeof event.plan_hash_after === "string" && typeof event.schema_version === "string";
928
+ }
929
+ function readFileLedgerExact(directory) {
930
+ const ledgerPath = getLedgerPath(directory);
931
+ if (!fs.existsSync(ledgerPath)) {
932
+ return { events: [], lines: [], truncated: false, badSuffix: null };
933
+ }
934
+ const bytes = fs.readFileSync(ledgerPath);
935
+ const decoder = new TextDecoder("utf-8", { fatal: true });
936
+ const events = [];
937
+ const lenientEvents = [];
938
+ const lines = [];
939
+ let truncated = false;
940
+ let badSuffix = null;
941
+ let start = 0;
942
+ for (let index = 0;index <= bytes.length; index++) {
943
+ if (index < bytes.length && bytes[index] !== 10)
944
+ continue;
945
+ let end = index;
946
+ if (end > start && bytes[end - 1] === 13)
947
+ end--;
948
+ const line = bytes.subarray(start, end);
949
+ if (line.length > 0) {
950
+ try {
951
+ const parsed = JSON.parse(decoder.decode(line));
952
+ if (!isStructurallyValidLedgerEvent(parsed)) {
953
+ throw new Error("invalid ledger event shape");
954
+ }
955
+ lenientEvents.push(parsed);
956
+ if (!truncated)
957
+ events.push(parsed);
958
+ if (!truncated)
959
+ lines.push(new Uint8Array(line));
960
+ } catch {
961
+ if (!truncated) {
962
+ truncated = true;
963
+ badSuffix = bytes.subarray(start).toString("utf8");
964
+ }
965
+ }
966
+ }
967
+ start = index + 1;
968
+ }
969
+ return { events, lines, truncated, badSuffix, lenientEvents };
970
+ }
971
+ function sqliteEventsAsLedger(directory) {
972
+ const rows = readSqliteLedgerEvents(directory).events;
973
+ return {
974
+ events: rows.map((row) => row.event),
975
+ lines: rows.map((row) => row.canonicalEvent)
976
+ };
977
+ }
978
+ function exactPrefix(prefix, complete) {
979
+ return prefix.length <= complete.length && prefix.every((line, index) => Buffer.from(line).equals(Buffer.from(complete[index])));
980
+ }
981
+ function eventsProjection(directory, events) {
982
+ const plan = reconstructPlanFromEvents(directory, structuredClone(events));
983
+ const projection = new TextEncoder().encode(JSON.stringify(plan));
984
+ return {
985
+ replayHash: plan ? computePlanLedgerHash(plan) : crypto.createHash("sha256").update(projection).digest("hex"),
986
+ projectionHash: crypto.createHash("sha256").update(projection).digest("hex"),
987
+ projection
988
+ };
989
+ }
990
+ function stateForEvents(directory, events, authorityMode) {
991
+ const terminal = eventsProjection(directory, events);
992
+ const last = events.at(-1);
993
+ return {
994
+ authorityMode,
995
+ shadowStartedVersion: getPlanLedgerState(directory)?.shadowStartedVersion ?? package_default.version,
996
+ parityStatus: "pending",
997
+ terminalProjectionHash: terminal.projectionHash,
998
+ terminalProjection: terminal.projection,
999
+ lastSeq: last?.seq ?? 0,
1000
+ planId: last?.plan_id ?? null,
1001
+ terminalPlanHash: last?.plan_hash_after ?? null
1002
+ };
1003
+ }
1004
+ function writePortableLedger(directory, lines) {
1005
+ const ledgerPath = getLedgerPath(directory);
1006
+ fs.mkdirSync(path2.dirname(ledgerPath), { recursive: true });
1007
+ const content = Buffer.concat(lines.flatMap((line) => [Buffer.from(line), Buffer.from(`
1008
+ `)]));
1009
+ const tempPath = `${ledgerPath}.sqlite-export.${crypto.randomBytes(16).toString("hex")}.tmp`;
1010
+ try {
1011
+ _internals.writeFileFsyncedThenRename(tempPath, ledgerPath, content);
1012
+ } finally {
1013
+ try {
1014
+ fs.unlinkSync(tempPath);
1015
+ } catch {}
1016
+ }
1017
+ }
1018
+ function archiveLegacyLedger(directory, bytes) {
1019
+ const hash = crypto.createHash("sha256").update(bytes).digest("hex");
1020
+ const archivePath = path2.join(directory, ".swarm", `plan-ledger.legacy-archive.${hash}.jsonl`);
1021
+ if (!fs.existsSync(archivePath)) {
1022
+ const tempPath = `${archivePath}.tmp.${crypto.randomBytes(16).toString("hex")}`;
1023
+ try {
1024
+ _internals.writeFileFsyncedThenRename(tempPath, archivePath, bytes);
1025
+ } finally {
1026
+ try {
1027
+ fs.unlinkSync(tempPath);
1028
+ } catch {}
1029
+ }
1030
+ fsyncRecoveryDirectory(path2.dirname(archivePath));
1031
+ }
1032
+ return { path: archivePath, hash };
1033
+ }
1034
+ function importCanonicalIntoSqlite(directory, file, legacy) {
1035
+ if (file.truncated)
1036
+ return;
1037
+ const ledgerBytes = fs.readFileSync(getLedgerPath(directory));
1038
+ const archive = legacy ? archiveLegacyLedger(directory, ledgerBytes) : null;
1039
+ const terminal = eventsProjection(directory, file.events);
1040
+ importSqliteLedger(directory, {
1041
+ canonicalEvents: file.lines,
1042
+ state: {
1043
+ ...stateForEvents(directory, file.events, "file_shadow"),
1044
+ fileReplayHash: terminal.replayHash,
1045
+ sqliteReplayHash: terminal.replayHash,
1046
+ parityStatus: "clean"
1047
+ },
1048
+ source: "plan-ledger.jsonl",
1049
+ sourceHash: crypto.createHash("sha256").update(ledgerBytes).digest("hex"),
1050
+ archivePath: archive?.path ?? null,
1051
+ archiveHash: archive?.hash ?? null,
1052
+ archiveSize: archive ? ledgerBytes.length : null,
1053
+ archiveCreatedAt: archive ? new Date().toISOString() : null,
1054
+ mode: "file_shadow",
1055
+ version: package_default.version
1056
+ });
1057
+ }
1058
+ function coordinateLedger(directory) {
1059
+ if (fs.existsSync(path2.join(directory, ".swarm", "plan-ledger.resetting")))
1060
+ return { events: [], lines: [], truncated: false, badSuffix: null };
1061
+ const file = readFileLedgerExact(directory);
1062
+ if (!hasSqliteLedger(directory)) {
1063
+ if (file.events.length > 0 && !file.truncated) {
1064
+ try {
1065
+ importCanonicalIntoSqlite(directory, file, true);
1066
+ } catch (error) {
1067
+ log(`[ledger] SQLite shadow import deferred after file read: ${error instanceof Error ? error.message : String(error)}`);
1068
+ }
1069
+ }
1070
+ return file;
1071
+ }
1072
+ let sqlite = sqliteEventsAsLedger(directory);
1073
+ const state = getPlanLedgerState(directory);
1074
+ if (!state)
1075
+ throw new Error("SQLite plan ledger has events but no authority state");
1076
+ if (state.authorityMode === "file_shadow") {
1077
+ if (!fs.existsSync(getLedgerPath(directory))) {
1078
+ writePortableLedger(directory, sqlite.lines);
1079
+ return {
1080
+ events: sqlite.events,
1081
+ lines: sqlite.lines,
1082
+ truncated: false,
1083
+ badSuffix: null
1084
+ };
1085
+ }
1086
+ if (file.truncated) {
1087
+ if (!exactPrefix(file.lines, sqlite.lines)) {
1088
+ throw new Error("PLAN_LEDGER_DIVERGED: malformed file prefix differs from SQLite authority");
1089
+ }
1090
+ return file;
1091
+ }
1092
+ if (file.events[0]?.seq === 1 && file.events[0]?.event_type === "plan_created" && sqlite.events[0]?.plan_id !== file.events[0]?.plan_id) {
1093
+ try {
1094
+ replaceSqliteLedger(directory, {
1095
+ canonicalEvents: file.lines,
1096
+ state: stateForEvents(directory, file.events, "file_shadow"),
1097
+ source: "plan_identity_reinitialized",
1098
+ mode: "file_shadow",
1099
+ version: package_default.version
1100
+ });
1101
+ sqlite = sqliteEventsAsLedger(directory);
1102
+ } catch (error) {
1103
+ log(`[ledger] SQLite shadow re-root reconciliation deferred: ${error instanceof Error ? error.message : String(error)}`);
1104
+ return file;
1105
+ }
1106
+ }
1107
+ if (!exactPrefix(sqlite.lines, file.lines)) {
1108
+ throw new Error("PLAN_LEDGER_DIVERGED: JSONL and SQLite committed prefixes differ");
1109
+ }
1110
+ try {
1111
+ for (let index = sqlite.lines.length;index < file.lines.length; index++) {
1112
+ appendSqliteLedger(directory, {
1113
+ canonicalEvent: file.lines[index],
1114
+ expectedSeq: index,
1115
+ state: stateForEvents(directory, file.events.slice(0, index + 1), "file_shadow")
1116
+ });
1117
+ }
1118
+ } catch (error) {
1119
+ log(`[ledger] SQLite shadow suffix repair deferred: ${error instanceof Error ? error.message : String(error)}`);
1120
+ return file;
1121
+ }
1122
+ sqlite = sqliteEventsAsLedger(directory);
1123
+ const fileProjection = eventsProjection(directory, file.events);
1124
+ const current = getPlanLedgerState(directory);
1125
+ const sqliteReplayHash = sqlite.events.at(-1)?.plan_hash_after ?? "";
1126
+ const sqliteProjectionHash = current.terminalProjectionHash;
1127
+ const parityStatus = fileProjection.replayHash === sqliteReplayHash && fileProjection.projectionHash === sqliteProjectionHash ? "clean" : "diverged";
1128
+ if (current.parityStatus !== parityStatus || current.fileReplayHash !== fileProjection.replayHash || current.sqliteReplayHash !== sqliteReplayHash) {
1129
+ recordSqliteLedgerParity(directory, {
1130
+ fileReplayHash: fileProjection.replayHash,
1131
+ sqliteReplayHash,
1132
+ terminalProjectionHash: sqliteProjectionHash,
1133
+ parityStatus
1134
+ });
1135
+ }
1136
+ const refreshed = getPlanLedgerState(directory);
1137
+ if (refreshed.parityStatus === "clean" && refreshed.shadowStartedVersion !== null && refreshed.shadowStartedVersion !== package_default.version) {
1138
+ cutoverSqliteLedger(directory, {
1139
+ expectedShadowStartedVersion: refreshed.shadowStartedVersion
1140
+ });
1141
+ }
1142
+ return file;
1143
+ }
1144
+ if (file.truncated || file.lines.length !== sqlite.lines.length || !exactPrefix(file.lines, sqlite.lines)) {
1145
+ writePortableLedger(directory, sqlite.lines);
1146
+ }
1147
+ return {
1148
+ events: sqlite.events,
1149
+ lines: sqlite.lines,
1150
+ truncated: false,
1151
+ badSuffix: null
1152
+ };
1153
+ }
1154
+ function computePlanLedgerHash(plan) {
1155
+ const normalized = {
1156
+ schema_version: plan.schema_version,
1157
+ title: plan.title,
1158
+ swarm: plan.swarm,
1159
+ current_phase: plan.current_phase,
1160
+ migration_status: plan.migration_status,
1161
+ execution_profile: normalizeExecutionProfileForHash(plan.execution_profile),
1162
+ phases: plan.phases.map((phase) => ({
1163
+ id: phase.id,
1164
+ name: phase.name,
1165
+ status: phase.status,
1166
+ required_agents: phase.required_agents ? [...phase.required_agents].sort() : undefined,
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(),
1174
+ acceptance: task.acceptance,
1175
+ files_touched: [...task.files_touched].sort(),
1176
+ evidence_path: task.evidence_path,
1177
+ blocked_reason: task.blocked_reason
1178
+ }))
1179
+ }))
1180
+ };
1181
+ const jsonString = JSON.stringify(normalized);
1182
+ return crypto.createHash("sha256").update(jsonString, "utf8").digest("hex");
1183
+ }
1184
+ function computePlanStructureHash(plan) {
1185
+ const normalized = {
1186
+ schema_version: plan.schema_version,
1187
+ title: plan.title,
1188
+ swarm: plan.swarm,
1189
+ current_phase: plan.current_phase,
1190
+ migration_status: plan.migration_status,
1191
+ execution_profile: normalizeExecutionProfileForHash(plan.execution_profile),
1192
+ phases: plan.phases.map((phase) => ({
1193
+ id: phase.id,
1194
+ name: phase.name,
1195
+ required_agents: phase.required_agents ? [...phase.required_agents].sort() : undefined,
1196
+ tasks: phase.tasks.map((task) => ({
1197
+ id: task.id,
1198
+ phase: task.phase,
1199
+ size: task.size,
1200
+ description: task.description,
1201
+ depends: [...task.depends].sort(),
1202
+ acceptance: task.acceptance,
1203
+ files_touched: [...task.files_touched].sort(),
1204
+ evidence_path: task.evidence_path,
1205
+ blocked_reason: task.blocked_reason
1206
+ }))
1207
+ }))
1208
+ };
1209
+ const jsonString = JSON.stringify(normalized);
1210
+ return crypto.createHash("sha256").update(jsonString, "utf8").digest("hex");
1211
+ }
1212
+ function computeCurrentPlanHash(directory) {
1213
+ const planPath = getPlanJsonPath(directory);
1214
+ try {
1215
+ const content = fs.readFileSync(planPath, "utf8");
1216
+ const plan = JSON.parse(content);
1217
+ return computePlanLedgerHash(plan);
1218
+ } catch {
1219
+ return "";
1220
+ }
1221
+ }
1222
+ async function ledgerExists(directory) {
1223
+ const ledgerPath = getLedgerPath(directory);
1224
+ return fs.existsSync(ledgerPath) || hasSqliteLedger(directory);
1225
+ }
1226
+ async function getLatestLedgerSeq(directory) {
1227
+ const coordinated = coordinateLedger(directory);
1228
+ return coordinated.events.reduce((maximum, event) => Math.max(maximum, event.seq), 0);
1229
+ }
1230
+ async function readLedgerEvents(directory) {
1231
+ const result = coordinateLedger(directory);
1232
+ const events = result.truncated ? result.lenientEvents ?? result.events : result.events;
1233
+ return [...events].sort((a, b) => a.seq - b.seq);
1234
+ }
1235
+ async function initLedger(directory, planId, initialPlanHash, initialPlan) {
1236
+ assertProjectRoot(directory);
1237
+ const ledgerPath = getLedgerPath(directory);
1238
+ const planJsonPath = getPlanJsonPath(directory);
1239
+ if (fs.existsSync(ledgerPath)) {
1240
+ throw new Error("Ledger already initialized. Use appendLedgerEvent to add events.");
1241
+ }
1242
+ let planHashAfter = initialPlanHash ?? "";
1243
+ let embeddedPlan = initialPlan;
1244
+ if (!initialPlanHash) {
1245
+ try {
1246
+ if (fs.existsSync(planJsonPath)) {
1247
+ const content = fs.readFileSync(planJsonPath, "utf8");
1248
+ const plan = JSON.parse(content);
1249
+ planHashAfter = computePlanLedgerHash(plan);
1250
+ if (!embeddedPlan)
1251
+ embeddedPlan = plan;
1252
+ }
1253
+ } catch {}
1254
+ }
1255
+ const payload = {
1256
+ plan_epoch: crypto.randomUUID(),
1257
+ ...embeddedPlan ? { plan: embeddedPlan, payload_hash: planHashAfter } : {}
1258
+ };
1259
+ const event = {
1260
+ seq: 1,
1261
+ timestamp: new Date().toISOString(),
1262
+ plan_id: planId,
1263
+ event_type: "plan_created",
1264
+ source: "initLedger",
1265
+ plan_hash_before: "",
1266
+ plan_hash_after: planHashAfter,
1267
+ schema_version: LEDGER_SCHEMA_VERSION,
1268
+ ...payload ? { payload } : {}
1269
+ };
1270
+ fs.mkdirSync(path2.join(directory, ".swarm"), { recursive: true });
1271
+ const tempPath = `${ledgerPath}.tmp.${Date.now()}.${Math.floor(Math.random() * 1e9)}`;
1272
+ const line = `${JSON.stringify(event)}
1273
+ `;
1274
+ writeFileFsyncedThenRename(tempPath, ledgerPath, line);
1275
+ const initialized = readFileLedgerExact(directory);
1276
+ if (hasSqliteLedger(directory)) {
1277
+ const priorMode = getPlanLedgerState(directory)?.authorityMode ?? "file_shadow";
1278
+ try {
1279
+ replaceSqliteLedger(directory, {
1280
+ canonicalEvents: initialized.lines,
1281
+ state: stateForEvents(directory, initialized.events, priorMode),
1282
+ source: "plan_identity_reinitialized",
1283
+ mode: priorMode,
1284
+ version: package_default.version
1285
+ });
1286
+ } catch (error) {
1287
+ if (priorMode !== "file_shadow")
1288
+ throw error;
1289
+ log(`[ledger] SQLite shadow re-root deferred after file commit: ${error instanceof Error ? error.message : String(error)}`);
1290
+ }
1291
+ } else {
1292
+ importCanonicalIntoSqlite(directory, initialized, false);
1293
+ }
1294
+ }
1295
+ async function readLedgerEventsForEpoch(directory) {
1296
+ const integrity = await _internals.readLedgerEventsWithIntegrity(directory, {
1297
+ failClosedOnReadError: true
1298
+ });
1299
+ if (integrity.truncated) {
1300
+ 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.`);
1301
+ }
1302
+ return integrity.events;
1303
+ }
1304
+ async function readPlanEpochIdentity(directory, authoritativePlan) {
1305
+ assertProjectRoot(directory);
1306
+ if (!await ledgerExists(directory)) {
1307
+ return null;
1308
+ }
1309
+ const events = await readLedgerEventsForEpoch(directory);
1310
+ if (events.length === 0) {
1311
+ throw new Error("Plan ledger is empty; missing plan_created root.");
1312
+ }
1313
+ return resolvePlanEpochIdentity(events, authoritativePlan);
1314
+ }
1315
+ async function getOrAdoptPlanEpochUnderLock(directory, authoritativePlan) {
1316
+ assertProjectRoot(directory);
1317
+ const planId = derivePlanId(authoritativePlan);
1318
+ const planIdentityHash = derivePlanIdentityHash(authoritativePlan);
1319
+ const payloadHash = computePlanLedgerHash(authoritativePlan);
1320
+ let lastStaleWriterError;
1321
+ for (let attempt = 0;attempt < 4; attempt++) {
1322
+ if (!await ledgerExists(directory)) {
1323
+ try {
1324
+ await initLedger(directory, planId, payloadHash, authoritativePlan);
1325
+ } catch (error) {
1326
+ if (!(error instanceof Error && error.message.includes("Ledger already initialized"))) {
1327
+ throw error;
1328
+ }
1329
+ }
1330
+ }
1331
+ const events = await readLedgerEventsForEpoch(directory);
1332
+ const existing = resolvePlanEpochIdentity(events, authoritativePlan);
1333
+ if (existing) {
1334
+ return existing;
1335
+ }
1336
+ if (events.length === 0) {
1337
+ throw new Error("Plan ledger is empty; cannot adopt a plan epoch.");
1338
+ }
1339
+ const rootEvent = events[0];
1340
+ const rootEventHash = computeLedgerEventHash(rootEvent);
1341
+ const tail = events[events.length - 1];
1342
+ const planEpoch = _internals.randomUUID();
1343
+ const adoptionPayload = {
1344
+ plan: authoritativePlan,
1345
+ payload_hash: payloadHash,
1346
+ plan_epoch: planEpoch,
1347
+ root_event_hash: rootEventHash
1348
+ };
1349
+ try {
1350
+ const appended = await _internals.appendLedgerEvent(directory, {
1351
+ event_type: "snapshot",
1352
+ source: "plan_epoch_adopted",
1353
+ plan_id: planId,
1354
+ payload: adoptionPayload
1355
+ }, {
1356
+ expectedSeq: tail.seq,
1357
+ expectedLedgerHash: tail.plan_hash_after,
1358
+ planHashAfter: payloadHash
1359
+ });
1360
+ const verifiedEvents = await readLedgerEventsForEpoch(directory);
1361
+ const verified = verifiedEvents[verifiedEvents.length - 1];
1362
+ const verifiedPayload = asRecord(verified?.payload);
1363
+ 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) {
1364
+ throw new Error(`Plan epoch adoption read verification failed for ${planId}.`);
1365
+ }
1366
+ return {
1367
+ planId,
1368
+ planIdentityHash,
1369
+ planEpoch,
1370
+ rootEventHash,
1371
+ payloadHash,
1372
+ source: "plan_epoch_adopted"
1373
+ };
1374
+ } catch (error) {
1375
+ if (error instanceof LedgerStaleWriterError) {
1376
+ lastStaleWriterError = error;
1377
+ continue;
1378
+ }
1379
+ throw error;
1380
+ }
1381
+ }
1382
+ throw new Error(`Unable to settle plan epoch for ${planId} after repeated stale-writer retries.`, { cause: lastStaleWriterError });
1383
+ }
1384
+ async function appendLedgerEvent(directory, eventInput, options) {
1385
+ assertProjectRoot(directory);
1386
+ return withEvidenceLock(directory, LEDGER_LOCK_PATH, "plan-ledger", "append-ledger-event", async () => {
1387
+ const ledgerPath = getLedgerPath(directory);
1388
+ if (options?.expectedLedgerHash !== undefined && options.expectedSeq === undefined) {
1389
+ throw new Error("expectedLedgerHash requires expectedSeq so stale-projection recovery is bound to one exact ledger tail");
1390
+ }
1391
+ const latestSeq = await getLatestLedgerSeq(directory);
1392
+ const nextSeq = latestSeq + 1;
1393
+ let ledgerHashBefore;
1394
+ if (options?.expectedLedgerHash !== undefined) {
1395
+ const events = await readLedgerEvents(directory);
1396
+ const tail = events[events.length - 1];
1397
+ if (!tail || tail.plan_hash_after !== options.expectedLedgerHash) {
1398
+ throw new LedgerStaleWriterError(`Stale writer: expected ledger hash ${options.expectedLedgerHash} but found ${tail?.plan_hash_after ?? "<missing>"}`);
1399
+ }
1400
+ ledgerHashBefore = tail.plan_hash_after;
1401
+ }
1402
+ const planHashBefore = computeCurrentPlanHash(directory);
1403
+ if (options?.expectedSeq !== undefined && options.expectedSeq !== latestSeq) {
1404
+ throw new LedgerStaleWriterError(`Stale writer: expected seq ${options.expectedSeq} but found ${latestSeq}`);
1405
+ }
1406
+ if (options?.expectedHash !== undefined && options.expectedHash !== planHashBefore) {
1407
+ throw new LedgerStaleWriterError(`Stale writer: expected hash ${options.expectedHash} but found ${planHashBefore}`);
1408
+ }
1409
+ const planHashAfter = options?.planHashAfter ?? planHashBefore;
1410
+ const event = {
1411
+ ...eventInput,
1412
+ seq: nextSeq,
1413
+ timestamp: new Date().toISOString(),
1414
+ plan_hash_before: ledgerHashBefore ?? planHashBefore,
1415
+ plan_hash_after: planHashAfter,
1416
+ schema_version: LEDGER_SCHEMA_VERSION
1417
+ };
1418
+ fs.mkdirSync(path2.join(directory, ".swarm"), { recursive: true });
1419
+ const canonicalEvent = new TextEncoder().encode(serializeLedgerEvent(event));
1420
+ const state = getPlanLedgerState(directory);
1421
+ const authorityMode = state?.authorityMode ?? "file_shadow";
1422
+ if (authorityMode === "sqlite") {
1423
+ try {
1424
+ const existing = sqliteEventsAsLedger(directory);
1425
+ appendSqliteLedger(directory, {
1426
+ canonicalEvent,
1427
+ expectedSeq: latestSeq,
1428
+ state: stateForEvents(directory, [...existing.events, event], "sqlite")
1429
+ });
1430
+ } catch (error) {
1431
+ if (error instanceof SqliteLedgerStaleWriterError) {
1432
+ throw new LedgerStaleWriterError(error.message);
1433
+ }
1434
+ throw error;
1435
+ }
1436
+ try {
1437
+ writePortableLedger(directory, sqliteEventsAsLedger(directory).lines);
1438
+ } catch (error) {
1439
+ criticalWarn(`[ledger] SQLite committed event ${event.seq}, but refreshing the portable plan-ledger.jsonl export failed: ${error instanceof Error ? error.message : String(error)}. SQLite remains authoritative; a later read will retry the export.`);
1440
+ }
1441
+ } else {
1442
+ const tempPath = `${ledgerPath}.tmp.${Date.now()}.${Math.floor(Math.random() * 1e9)}`;
1443
+ if (!fs.existsSync(ledgerPath)) {
1444
+ throw new Error("Ledger not initialized. Call initLedger() first.");
1445
+ }
1446
+ const existingContent = fs.readFileSync(ledgerPath);
1447
+ writeFileFsyncedThenRename(tempPath, ledgerPath, Buffer.concat([
1448
+ existingContent,
1449
+ Buffer.from(canonicalEvent),
1450
+ Buffer.from(`
1451
+ `)
1452
+ ]));
1453
+ try {
1454
+ appendSqliteLedger(directory, {
1455
+ canonicalEvent,
1456
+ expectedSeq: latestSeq,
1457
+ state: {
1458
+ authorityMode: "file_shadow",
1459
+ parityStatus: "pending"
1460
+ }
1461
+ });
1462
+ } catch (error) {
1463
+ log(`[ledger] SQLite shadow append deferred after file commit: ${error instanceof Error ? error.message : String(error)}`);
1464
+ }
1465
+ }
1466
+ return event;
1467
+ });
1468
+ }
1469
+ async function appendLedgerEventWithRetry(directory, eventInput, options) {
1470
+ const maxRetries = options.maxRetries ?? 3;
1471
+ const backoffBase = options.backoffMs ?? 10;
1472
+ let currentExpected = options.expectedHash;
1473
+ let attempt = 0;
1474
+ while (true) {
1475
+ try {
1476
+ return await appendLedgerEvent(directory, eventInput, {
1477
+ expectedHash: currentExpected,
1478
+ planHashAfter: options.planHashAfter
1479
+ });
1480
+ } catch (error) {
1481
+ if (!(error instanceof LedgerStaleWriterError) || attempt >= maxRetries) {
1482
+ throw error;
1483
+ }
1484
+ attempt++;
1485
+ const delayMs = backoffBase * 2 ** (attempt - 1);
1486
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
1487
+ if (options.verifyValid) {
1488
+ const stillValid = await options.verifyValid();
1489
+ if (!stillValid) {
1490
+ return null;
1491
+ }
1492
+ }
1493
+ currentExpected = computeCurrentPlanHash(directory);
1494
+ }
1495
+ }
1496
+ }
1497
+ async function takeSnapshotWithRetry(directory, plan, options) {
1498
+ const MAX_RETRIES = 3;
1499
+ const TOTAL_ATTEMPTS = 1 + MAX_RETRIES;
1500
+ const telemetrySource = options?.source ?? "save_plan_tool";
1501
+ const snapshotOptions = { planHashAfter: options?.planHashAfter };
1502
+ let lastError;
1503
+ for (let attempt = 1;attempt <= TOTAL_ATTEMPTS; attempt++) {
1504
+ try {
1505
+ await takeSnapshotEvent(directory, plan, snapshotOptions);
1506
+ return;
1507
+ } catch (err) {
1508
+ lastError = err instanceof Error ? err : new Error(String(err));
1509
+ if (attempt < TOTAL_ATTEMPTS) {
1510
+ await new Promise((r) => setTimeout(r, 10 * 2 ** (attempt - 1)));
1511
+ }
1512
+ }
1513
+ }
1514
+ criticalWarn(`[takeSnapshotWithRetry] Snapshot failed after ${MAX_RETRIES} retries (${TOTAL_ATTEMPTS} attempts): ${lastError.message}`);
1515
+ try {
1516
+ emit("snapshot_failed", {
1517
+ error: lastError.message,
1518
+ retries: MAX_RETRIES,
1519
+ source: telemetrySource
1520
+ });
1521
+ } catch {}
1522
+ }
1523
+ async function takeSnapshotEvent(directory, plan, options) {
1524
+ const payloadHash = options?.payloadHashOverride ?? (options?.source === "critic_approved" ? computePlanStructureHash(plan) : computePlanLedgerHash(plan));
1525
+ const snapshotPayload = {
1526
+ plan,
1527
+ payload_hash: payloadHash
1528
+ };
1529
+ if (options?.approvalMetadata) {
1530
+ snapshotPayload.approval = options.approvalMetadata;
1531
+ }
1532
+ const planId = derivePlanId(plan);
1533
+ return appendLedgerEvent(directory, {
1534
+ event_type: "snapshot",
1535
+ source: options?.source ?? "takeSnapshotEvent",
1536
+ plan_id: planId,
1537
+ payload: snapshotPayload
1538
+ }, {
1539
+ planHashAfter: options?.planHashAfter,
1540
+ expectedSeq: options?.expectedSeq,
1541
+ expectedLedgerHash: options?.expectedLedgerHash
1542
+ });
1543
+ }
1544
+ async function replaceTruncatedLedgerWithRecoveryRoot(directory, plan, expected) {
1545
+ assertProjectRoot(directory);
1546
+ const validated = PlanSchema.parse(plan);
1547
+ return withEvidenceLock(directory, LEDGER_LOCK_PATH, "plan-ledger", "reconcile-truncated-ledger", async () => {
1548
+ const ledgerPath = getLedgerPath(directory);
1549
+ if (!fs.existsSync(ledgerPath)) {
1550
+ throw new LedgerStaleWriterError("Stale writer: canonical ledger disappeared before truncated-ledger recovery");
1551
+ }
1552
+ const events = await readLedgerEvents(directory);
1553
+ const tail = events[events.length - 1];
1554
+ if (!tail || tail.seq !== expected.seq || tail.plan_hash_after !== expected.ledgerHash || events[0]?.plan_id !== derivePlanId(validated)) {
1555
+ throw new LedgerStaleWriterError("Stale writer: ledger identity or tail changed before truncated-ledger recovery");
1556
+ }
1557
+ const integrity = await readLedgerEventsWithIntegrity(directory);
1558
+ if (!integrity.truncated || integrity.badSuffix === null) {
1559
+ throw new LedgerStaleWriterError("Stale writer: ledger is no longer truncated; retry against its current tail");
1560
+ }
1561
+ const originalBytes = fs.readFileSync(ledgerPath);
1562
+ const archiveSha256 = crypto.createHash("sha256").update(originalBytes).digest("hex");
1563
+ const swarmDir = path2.dirname(ledgerPath);
1564
+ const archiveSuffix = `.${archiveSha256.slice(0, 12)}.jsonl`;
1565
+ let archiveName = `plan-ledger.reconcile-archive.${Date.now()}.${Math.floor(Math.random() * 1e9)}${archiveSuffix}`;
1566
+ let archivePath = path2.join(swarmDir, archiveName);
1567
+ let archiveNeedsWrite = true;
1568
+ for (const candidate of _internals.readLedgerDirectory(swarmDir).sort(compareCodeUnits)) {
1569
+ if (!candidate.startsWith("plan-ledger.reconcile-archive.") || !candidate.endsWith(archiveSuffix)) {
1570
+ continue;
1571
+ }
1572
+ const candidatePath = path2.join(swarmDir, candidate);
1573
+ if (fs.readFileSync(candidatePath).equals(originalBytes)) {
1574
+ archiveName = candidate;
1575
+ archivePath = candidatePath;
1576
+ archiveNeedsWrite = false;
1577
+ break;
1578
+ }
1579
+ }
1580
+ const archiveTempPath = `${archivePath}.tmp.${crypto.randomBytes(16).toString("hex")}`;
1581
+ const canonicalTempPath = `${ledgerPath}.reconcile.${crypto.randomBytes(16).toString("hex")}.tmp`;
1582
+ const planHash = computePlanLedgerHash(validated);
1583
+ const rawBadSuffix = findRawMalformedSuffix(originalBytes);
1584
+ if (rawBadSuffix.length === 0) {
1585
+ throw new LedgerStaleWriterError("Stale writer: raw ledger bytes no longer contain the malformed suffix detected during replay");
1586
+ }
1587
+ const badSuffixSha256 = crypto.createHash("sha256").update(rawBadSuffix).digest("hex");
1588
+ const recoveryRoot = {
1589
+ seq: 1,
1590
+ timestamp: new Date().toISOString(),
1591
+ plan_id: derivePlanId(validated),
1592
+ event_type: "plan_created",
1593
+ source: "save_plan_truncated_ledger_reconcile",
1594
+ plan_hash_before: "",
1595
+ plan_hash_after: planHash,
1596
+ schema_version: LEDGER_SCHEMA_VERSION,
1597
+ payload: {
1598
+ plan: validated,
1599
+ payload_hash: planHash,
1600
+ recovery: {
1601
+ kind: "truncated_ledger_reconcile",
1602
+ archived_ledger: `.swarm/${archiveName}`,
1603
+ archived_sha256: archiveSha256,
1604
+ bad_suffix_sha256: badSuffixSha256,
1605
+ prior_tail_seq: expected.seq,
1606
+ prior_tail_hash: expected.ledgerHash
1607
+ }
1608
+ }
1609
+ };
1610
+ try {
1611
+ if (archiveNeedsWrite) {
1612
+ _internals.writeFileFsyncedThenRename(archiveTempPath, archivePath, originalBytes);
1613
+ }
1614
+ _internals.fsyncRecoveryDirectory(swarmDir);
1615
+ _internals.writeFileFsyncedThenRename(canonicalTempPath, ledgerPath, `${JSON.stringify(recoveryRoot)}
1616
+ `);
1617
+ } finally {
1618
+ for (const tempPath of [archiveTempPath, canonicalTempPath]) {
1619
+ try {
1620
+ fs.unlinkSync(tempPath);
1621
+ } catch {}
1622
+ }
1623
+ }
1624
+ return { archivePath, archiveSha256, recoveryRoot };
1625
+ });
1626
+ }
1627
+ async function clearPlanLedgerForReset(directory) {
1628
+ assertProjectRoot(directory);
1629
+ await withEvidenceLock(directory, LEDGER_LOCK_PATH, "plan-ledger", "reset-plan-ledger", async () => {
1630
+ const marker = path2.join(directory, ".swarm", "plan-ledger.resetting");
1631
+ fs.writeFileSync(marker, `resetting
1632
+ `, "utf8");
1633
+ try {
1634
+ clearSqliteLedger(directory);
1635
+ try {
1636
+ fs.unlinkSync(getLedgerPath(directory));
1637
+ } catch (error) {
1638
+ if (error?.code !== "ENOENT")
1639
+ throw error;
1640
+ }
1641
+ } finally {
1642
+ try {
1643
+ fs.unlinkSync(marker);
1644
+ } catch {}
1645
+ }
1646
+ });
1647
+ }
1648
+ async function replacePlanLedgerWithRoot(directory, plan, source) {
1649
+ assertProjectRoot(directory);
1650
+ const validated = PlanSchema.parse(plan);
1651
+ await withEvidenceLock(directory, LEDGER_LOCK_PATH, "plan-ledger", "replace-plan-ledger-root", async () => {
1652
+ const ledgerPath = getLedgerPath(directory);
1653
+ if (fs.existsSync(ledgerPath)) {
1654
+ archiveLegacyLedger(directory, fs.readFileSync(ledgerPath));
1655
+ }
1656
+ const planHash = computePlanLedgerHash(validated);
1657
+ const root = {
1658
+ seq: 1,
1659
+ timestamp: new Date().toISOString(),
1660
+ plan_id: derivePlanId(validated),
1661
+ event_type: "plan_created",
1662
+ source,
1663
+ plan_hash_before: "",
1664
+ plan_hash_after: planHash,
1665
+ schema_version: LEDGER_SCHEMA_VERSION,
1666
+ payload: {
1667
+ plan: validated,
1668
+ payload_hash: planHash,
1669
+ plan_epoch: crypto.randomUUID()
1670
+ }
1671
+ };
1672
+ const line = new TextEncoder().encode(serializeLedgerEvent(root));
1673
+ const priorMode = getPlanLedgerState(directory)?.authorityMode ?? "file_shadow";
1674
+ if (priorMode === "file_shadow") {
1675
+ writePortableLedger(directory, [line]);
1676
+ try {
1677
+ replaceSqliteLedger(directory, {
1678
+ canonicalEvents: [line],
1679
+ state: stateForEvents(directory, [root], priorMode),
1680
+ source: source === "savePlan_identity_migration" ? "plan_identity_reinitialized" : "checkpoint_rollback",
1681
+ mode: priorMode,
1682
+ version: package_default.version
1683
+ });
1684
+ } catch (error) {
1685
+ log(`[ledger] SQLite shadow re-root deferred after authoritative file commit: ${error instanceof Error ? error.message : String(error)}`);
1686
+ }
1687
+ return;
1688
+ }
1689
+ replaceSqliteLedger(directory, {
1690
+ canonicalEvents: [line],
1691
+ state: stateForEvents(directory, [root], priorMode),
1692
+ source: "checkpoint_rollback",
1693
+ mode: priorMode,
1694
+ version: package_default.version
1695
+ });
1696
+ try {
1697
+ writePortableLedger(directory, [line]);
1698
+ } catch (error) {
1699
+ criticalWarn(`[ledger] SQLite committed rollback root, but refreshing the portable plan-ledger.jsonl export failed: ${error instanceof Error ? error.message : String(error)}. SQLite remains authoritative; a later read will retry the export.`);
1700
+ }
1701
+ });
1702
+ }
1703
+ async function replayFromLedger(directory, options) {
1704
+ const { plan } = await replayFromLedgerWithStatus(directory, options);
1705
+ return plan;
1706
+ }
1707
+ async function peekPlanFromLedger(directory, _options) {
1708
+ if (fs.existsSync(path2.join(directory, ".swarm", "plan-ledger.resetting")))
1709
+ return { plan: null, truncated: false, badSuffix: null };
1710
+ const file = readFileLedgerExact(directory);
1711
+ let events = file.events;
1712
+ let truncated = file.truncated;
1713
+ let badSuffix = file.badSuffix;
1714
+ const sqliteReadOnly = readSqliteLedgerEventsReadOnly(directory);
1715
+ if (fs.existsSync(path2.join(directory, ".swarm", "plan-ledger.resetting")))
1716
+ return { plan: null, truncated: false, badSuffix: null };
1717
+ if (sqliteReadOnly.events.length > 0) {
1718
+ const sqlite = {
1719
+ events: sqliteReadOnly.events.map((row) => row.event)
1720
+ };
1721
+ const authority = sqliteReadOnly.state;
1722
+ if (authority?.authorityMode === "sqlite" || events.length === 0) {
1723
+ events = sqlite.events;
1724
+ truncated = false;
1725
+ badSuffix = null;
1726
+ }
1727
+ }
1728
+ if (events.length === 0) {
1729
+ return { plan: null, truncated, badSuffix };
1730
+ }
1731
+ return {
1732
+ plan: reconstructPlanFromEvents(directory, events),
1733
+ truncated,
1734
+ badSuffix
1735
+ };
1736
+ }
1737
+ async function replayFromLedgerWithStatus(directory, _options) {
1738
+ const { events, truncated, badSuffix } = await readLedgerEventsWithIntegrity(directory);
1739
+ if (events.length === 0) {
1740
+ return { plan: null, truncated, badSuffix };
1741
+ }
1742
+ if (truncated && badSuffix !== null) {
1743
+ await quarantineLedgerSuffix(directory, badSuffix);
1744
+ }
1745
+ const plan = reconstructPlanFromEvents(directory, events);
1746
+ return { plan, truncated, badSuffix };
1747
+ }
1748
+ function reconstructPlanFromEvents(directory, events) {
1749
+ const targetPlanId = events[0].plan_id;
1750
+ const relevantEvents = events.filter((e) => e.plan_id === targetPlanId);
1751
+ {
1752
+ const snapshotEvents = relevantEvents.filter((e) => e.event_type === "snapshot");
1753
+ if (snapshotEvents.length > 0) {
1754
+ const latestSnapshotEvent = snapshotEvents[snapshotEvents.length - 1];
1755
+ const snapshotPayload = latestSnapshotEvent.payload;
1756
+ let plan2 = snapshotPayload.plan;
1757
+ const eventsAfterSnapshot = relevantEvents.filter((e) => e.seq > latestSnapshotEvent.seq);
1758
+ for (const event of eventsAfterSnapshot) {
1759
+ plan2 = applyEventToPlan(plan2, event);
1760
+ if (plan2 === null) {
1761
+ return null;
1762
+ }
1763
+ }
1764
+ return plan2;
1765
+ }
1766
+ }
1767
+ const createdEvent = relevantEvents.find((e) => e.event_type === "plan_created");
1768
+ if (createdEvent?.payload && typeof createdEvent.payload === "object" && "plan" in createdEvent.payload) {
1769
+ const parseResult = PlanSchema.safeParse(createdEvent.payload.plan);
1770
+ if (parseResult.success) {
1771
+ let plan2 = parseResult.data;
1772
+ const eventsAfterCreated = relevantEvents.filter((e) => e.seq > createdEvent.seq);
1773
+ for (const event of eventsAfterCreated) {
1774
+ if (plan2 === null)
1775
+ return null;
1776
+ plan2 = applyEventToPlan(plan2, event);
1777
+ }
1778
+ return plan2;
1779
+ }
1780
+ }
1781
+ const planJsonPath = getPlanJsonPath(directory);
1782
+ if (!fs.existsSync(planJsonPath)) {
1783
+ return null;
1784
+ }
1785
+ let plan;
1786
+ try {
1787
+ const content = fs.readFileSync(planJsonPath, "utf8");
1788
+ plan = JSON.parse(content);
1789
+ } catch {
1790
+ return null;
1791
+ }
1792
+ for (const event of relevantEvents) {
1793
+ if (plan === null) {
1794
+ return null;
1795
+ }
1796
+ plan = applyEventToPlan(plan, event);
1797
+ }
1798
+ return plan;
1799
+ }
1800
+ function applyEventToPlan(plan, event) {
1801
+ switch (event.event_type) {
1802
+ case "plan_created":
1803
+ if (event.payload && typeof event.payload === "object" && "plan" in event.payload) {
1804
+ const parsed = PlanSchema.safeParse(event.payload.plan);
1805
+ if (parsed.success)
1806
+ return parsed.data;
1807
+ }
1808
+ return plan;
1809
+ case "task_status_changed":
1810
+ if (event.task_id && event.to_status) {
1811
+ const parseResult = TaskStatusSchema.safeParse(event.to_status);
1812
+ if (!parseResult.success) {
1813
+ return plan;
1814
+ }
1815
+ for (const phase of plan.phases) {
1816
+ const task = phase.tasks.find((t) => t.id === event.task_id);
1817
+ if (task) {
1818
+ task.status = parseResult.data;
1819
+ break;
1820
+ }
1821
+ }
1822
+ }
1823
+ return plan;
1824
+ case "phase_completed":
1825
+ if (event.phase_id) {
1826
+ const phase = plan.phases.find((p) => p.id === event.phase_id);
1827
+ if (phase) {
1828
+ phase.status = "complete";
1829
+ }
1830
+ }
1831
+ return plan;
1832
+ case "plan_exported":
1833
+ return plan;
1834
+ case "task_added":
1835
+ return plan;
1836
+ case "task_removed":
1837
+ if (event.task_id) {
1838
+ for (const phase of plan.phases) {
1839
+ const idx = phase.tasks.findIndex((t) => t.id === event.task_id);
1840
+ if (idx !== -1) {
1841
+ phase.tasks.splice(idx, 1);
1842
+ break;
1843
+ }
1844
+ }
1845
+ }
1846
+ return plan;
1847
+ case "task_updated":
1848
+ return plan;
1849
+ case "plan_rebuilt":
1850
+ return plan;
1851
+ case "task_reordered":
1852
+ return plan;
1853
+ case "snapshot":
1854
+ return plan;
1855
+ case "plan_reset":
1856
+ return null;
1857
+ case "execution_profile_set": {
1858
+ const rawProfile = event.payload?.execution_profile;
1859
+ if (rawProfile !== undefined) {
1860
+ const parsed = ExecutionProfileSchema.safeParse(rawProfile);
1861
+ if (parsed.success) {
1862
+ return { ...plan, execution_profile: parsed.data };
1863
+ }
1864
+ }
1865
+ return plan;
1866
+ }
1867
+ case "execution_profile_locked": {
1868
+ if (plan.execution_profile) {
1869
+ return {
1870
+ ...plan,
1871
+ execution_profile: { ...plan.execution_profile, locked: true }
1872
+ };
1873
+ }
1874
+ return plan;
1875
+ }
1876
+ default:
1877
+ throw new Error(`applyEventToPlan: unhandled event type "${event.event_type}" at seq ${event.seq}`);
1878
+ }
1879
+ }
1880
+ async function readLedgerEventsWithIntegrity(directory, options) {
1881
+ const ledgerPath = getLedgerPath(directory);
1882
+ if (!fs.existsSync(ledgerPath) && !hasSqliteLedger(directory)) {
1883
+ return { events: [], truncated: false, badSuffix: null };
1884
+ }
1885
+ try {
1886
+ const result = coordinateLedger(directory);
1887
+ return {
1888
+ events: [...result.events].sort((a, b) => a.seq - b.seq),
1889
+ truncated: result.truncated,
1890
+ badSuffix: result.badSuffix
1891
+ };
1892
+ } catch (error) {
1893
+ if (options?.failClosedOnReadError) {
1894
+ 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.`);
1895
+ }
1896
+ const code = error?.code;
1897
+ if (code && ["EACCES", "EIO", "EISDIR", "EPERM"].includes(code)) {
1898
+ return { events: [], truncated: false, badSuffix: null };
1899
+ }
1900
+ throw error;
1901
+ }
1902
+ }
1903
+ async function quarantineLedgerSuffix(directory, badSuffix) {
1904
+ let salvagedCount = 0;
1905
+ for (const line of badSuffix.split(`
1906
+ `)) {
1907
+ if (line.trim() === "")
1908
+ continue;
1909
+ try {
1910
+ JSON.parse(line);
1911
+ salvagedCount++;
1912
+ } catch {}
1913
+ }
1914
+ try {
1915
+ assertProjectRoot(directory);
1916
+ const hash = crypto.createHash("sha256").update(badSuffix, "utf8").digest("hex").slice(0, 12);
1917
+ const swarmDir = path2.join(directory, ".swarm");
1918
+ try {
1919
+ const existing = _internals.readLedgerDirectory(swarmDir).sort(compareCodeUnits).filter((name) => name.startsWith("plan-ledger.quarantine.") && name.endsWith(`.${hash}`));
1920
+ for (const name of existing) {
1921
+ const existingPath = path2.join(swarmDir, name);
1922
+ if (fs.readFileSync(existingPath, "utf8") === badSuffix) {
1923
+ return { path: existingPath, salvagedCount };
1924
+ }
1925
+ }
1926
+ } catch {}
1927
+ const quarantinePath = path2.join(swarmDir, `plan-ledger.quarantine.${Date.now()}.${hash}`);
1928
+ fs.writeFileSync(quarantinePath, badSuffix, "utf8");
1929
+ log(`[ledger] Corrupted suffix quarantined to ${path2.relative(directory, quarantinePath)} (salvageable events: ${salvagedCount})`);
1930
+ return { path: quarantinePath, salvagedCount };
1931
+ } catch {
1932
+ return { path: null, salvagedCount };
1933
+ }
1934
+ }
1935
+ async function loadLastApprovedPlan(directory, expectedPlanId) {
1936
+ const events = await readLedgerEvents(directory);
1937
+ return findLastApprovedSnapshot(events, expectedPlanId);
1938
+ }
1939
+ async function loadLastPlanCriticApprovedSnapshot(directory, expectedPlanId) {
1940
+ const events = await readLedgerEvents(directory);
1941
+ return findLastApprovedSnapshot(events, expectedPlanId, (payload) => payload.approval?.source === "plan_critic_gate");
1942
+ }
1943
+ function findLastApprovedSnapshot(events, expectedPlanId, extraFilter) {
1944
+ if (events.length === 0) {
1945
+ return null;
1946
+ }
1947
+ for (let i = events.length - 1;i >= 0; i--) {
1948
+ const event = events[i];
1949
+ if (event.event_type !== "snapshot")
1950
+ continue;
1951
+ if (event.source !== "critic_approved")
1952
+ continue;
1953
+ if (expectedPlanId !== undefined && event.plan_id !== expectedPlanId) {
1954
+ continue;
1955
+ }
1956
+ const payload = event.payload;
1957
+ if (!payload || typeof payload !== "object" || !payload.plan) {
1958
+ continue;
1959
+ }
1960
+ if (expectedPlanId !== undefined) {
1961
+ const payloadPlanId = derivePlanId(payload.plan);
1962
+ if (payloadPlanId !== expectedPlanId) {
1963
+ continue;
1964
+ }
1965
+ }
1966
+ if (extraFilter && !extraFilter(payload)) {
1967
+ continue;
1968
+ }
1969
+ return {
1970
+ plan: payload.plan,
1971
+ seq: event.seq,
1972
+ timestamp: event.timestamp,
1973
+ approval: payload.approval,
1974
+ payloadHash: payload.payload_hash
1975
+ };
1976
+ }
1977
+ return null;
1978
+ }
1979
+ var _internals = {
1980
+ computePlanLedgerHash,
1981
+ computePlanStructureHash,
1982
+ computeCurrentPlanHash,
1983
+ ledgerExists,
1984
+ getLatestLedgerSeq,
1985
+ readLedgerEvents,
1986
+ initLedger,
1987
+ appendLedgerEvent,
1988
+ appendLedgerEventWithRetry,
1989
+ takeSnapshotEvent,
1990
+ replayFromLedger,
1991
+ replayFromLedgerWithStatus,
1992
+ peekPlanFromLedger,
1993
+ applyEventToPlan,
1994
+ readLedgerEventsWithIntegrity,
1995
+ quarantineLedgerSuffix,
1996
+ loadLastApprovedPlan,
1997
+ loadLastPlanCriticApprovedSnapshot,
1998
+ getLedgerPath,
1999
+ getPlanJsonPath,
2000
+ archiveLegacyLedger,
2001
+ writePortableLedger,
2002
+ writeFileFsyncedThenRename,
2003
+ fsyncRecoveryDirectory,
2004
+ readLedgerDirectory,
2005
+ randomUUID: () => crypto.randomUUID()
2006
+ };
2007
+
2008
+ export { renderPlanningProfileDirective, resolvePlanningProfile, normalizeExecutionProfileForHash, LedgerStaleWriterError, computePlanLedgerHash, computePlanStructureHash, computeCurrentPlanHash, ledgerExists, getLatestLedgerSeq, readLedgerEvents, initLedger, readPlanEpochIdentity, getOrAdoptPlanEpochUnderLock, appendLedgerEvent, takeSnapshotWithRetry, takeSnapshotEvent, replaceTruncatedLedgerWithRecoveryRoot, clearPlanLedgerForReset, replacePlanLedgerWithRoot, replayFromLedger, peekPlanFromLedger, replayFromLedgerWithStatus, readLedgerEventsWithIntegrity, loadLastApprovedPlan };