opencode-swarm 7.122.0 → 7.123.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 (74) hide show
  1. package/.opencode/skills/issue-tracer/SKILL.md +341 -0
  2. package/.opencode/skills/issue-tracer/assets/pr-template.md +59 -0
  3. package/.opencode/skills/issue-tracer/references/critic-gate.md +245 -0
  4. package/.opencode/skills/issue-tracer/references/evidence-artifacts.md +267 -0
  5. package/.opencode/skills/issue-tracer/references/install.md +63 -0
  6. package/.opencode/skills/issue-tracer/references/localization-playbook.md +95 -0
  7. package/.opencode/skills/issue-tracer/references/method-provenance.md +13 -0
  8. package/.opencode/skills/issue-tracer/references/untrusted-content.md +22 -0
  9. package/.opencode/skills/issue-tracer/scripts/scan-deferred.sh +103 -0
  10. package/.opencode/skills/issue-tracer/scripts/trace-init.sh +119 -0
  11. package/dist/cli/{config-doctor-404c8mf8.js → config-doctor-3ksh1534.js} +2 -2
  12. package/dist/cli/{core-xnwasfpc.js → core-cwgb7ev8.js} +1 -1
  13. package/dist/cli/{curation-policy-cj1tpzr5.js → curation-policy-4j4qce5a.js} +5 -5
  14. package/dist/cli/{curator-llm-factory-8rg0f77t.js → curator-llm-factory-mfvd4sqx.js} +22 -20
  15. package/dist/cli/{curator-3rze3pd4.js → curator-tkyn9bzz.js} +22 -20
  16. package/dist/cli/{evidence-summary-service-k14ykm03.js → evidence-summary-service-96ndczbk.js} +7 -5
  17. package/dist/cli/{gate-evidence-tbpezk28.js → gate-evidence-xqks3kk2.js} +3 -3
  18. package/dist/cli/{guardrail-explain-hdsxa2c5.js → guardrail-explain-4vytbchb.js} +23 -21
  19. package/dist/cli/{guardrail-log-dhpp0kmn.js → guardrail-log-z291m4gw.js} +3 -3
  20. package/dist/cli/{hive-promoter-dh38qd9m.js → hive-promoter-y5wsztkc.js} +22 -20
  21. package/dist/cli/{index-zvae4m8g.js → index-17eybc3z.js} +1 -1
  22. package/dist/cli/{index-kyvg2cmp.js → index-19zn67q6.js} +1 -1
  23. package/dist/cli/{index-rbfq77sz.js → index-2j25br0p.js} +12 -10
  24. package/dist/cli/{index-9xpgytj9.js → index-3rr0ese8.js} +1 -1
  25. package/dist/cli/{index-68aggz0v.js → index-6sgqnvpw.js} +2 -2
  26. package/dist/cli/{index-qg11yf8q.js → index-6xnxz6z0.js} +1 -1
  27. package/dist/cli/{index-emkb0bbe.js → index-7cspygmb.js} +277 -673
  28. package/dist/cli/{index-jzpv74mq.js → index-7g91saha.js} +24 -22
  29. package/dist/cli/{index-3r1w7xky.js → index-cqsak7bf.js} +2 -2
  30. package/dist/cli/index-ctpgsj84.js +547 -0
  31. package/dist/cli/index-d6h5hq91.js +564 -0
  32. package/dist/cli/{index-vqcgmy4y.js → index-det5jwfk.js} +1 -1
  33. package/dist/cli/{index-3sx27bxt.js → index-dywekeen.js} +3 -3
  34. package/dist/cli/{index-wtn8wf7h.js → index-gw3m5fw0.js} +1 -1
  35. package/dist/cli/{index-rkd6qwjh.js → index-hns7xjm7.js} +2 -2
  36. package/dist/cli/{index-xdsj4qtd.js → index-j9kxwshw.js} +5 -5
  37. package/dist/cli/{index-ahxrne50.js → index-jyrhy4ww.js} +161 -58
  38. package/dist/cli/{index-j33zb3cx.js → index-m0bmh3w0.js} +4 -4
  39. package/dist/cli/index-m1abzg93.js +68 -0
  40. package/dist/cli/{index-ny13nr0x.js → index-n2712byd.js} +6 -6
  41. package/dist/cli/{index-gr7ssnj9.js → index-q00e4s1s.js} +5 -4
  42. package/dist/cli/{index-3jvqw5hy.js → index-q27bajqb.js} +1 -1
  43. package/dist/cli/{index-qt2yaghg.js → index-vcgfqd8f.js} +1 -63
  44. package/dist/cli/{index-tftv9xh9.js → index-wr8qgpz5.js} +1 -1
  45. package/dist/cli/index.js +22 -20
  46. package/dist/cli/{knowledge-escalator-sya06p12.js → knowledge-escalator-dyxsm78k.js} +6 -6
  47. package/dist/cli/{knowledge-events-xzps61a9.js → knowledge-events-36q6nvae.js} +4 -4
  48. package/dist/cli/{knowledge-link-sy85fw3w.js → knowledge-link-4yd2z8ev.js} +3 -3
  49. package/dist/cli/{knowledge-store-qg66a93n.js → knowledge-store-e07b4s54.js} +4 -4
  50. package/dist/cli/{knowledge-validator-gar6wv4f.js → knowledge-validator-sfyf7wn2.js} +7 -7
  51. package/dist/cli/pr-subscriptions-x653151h.js +1 -1
  52. package/dist/cli/{scan-cursor-nx5y5k7r.js → scan-cursor-hm0aqqae.js} +5 -5
  53. package/dist/cli/{schema-vwxpsk6j.js → schema-13bnng2n.js} +1 -1
  54. package/dist/cli/scope-persistence-d9erwnvb.js +36 -0
  55. package/dist/cli/{skill-generator-nt5qxafq.js → skill-generator-63r20a9k.js} +8 -8
  56. package/dist/config/bundled-skills.d.ts +1 -1
  57. package/dist/config/constants.d.ts +3 -2
  58. package/dist/hooks/delegation-gate.d.ts +19 -1
  59. package/dist/hooks/guardrails/nontransient-circuit.d.ts +47 -0
  60. package/dist/hooks/scope-guard.d.ts +9 -2
  61. package/dist/hooks/write-target-resolver.d.ts +30 -0
  62. package/dist/index.js +406 -370
  63. package/dist/sandbox/win32/restricted-environment-executor.d.ts +3 -2
  64. package/dist/scope/scope-binding.d.ts +89 -0
  65. package/dist/scope/scope-persistence.d.ts +53 -12
  66. package/dist/state.d.ts +25 -1
  67. package/dist/tools/declare-scope.d.ts +5 -1
  68. package/dist/tools/file-extractor-planner.d.ts +31 -0
  69. package/dist/tools/file-extractor.d.ts +2 -7
  70. package/package.json +2 -1
  71. package/dist/cli/index-51gg9947.js +0 -254
  72. package/dist/cli/scope-persistence-mfr36yc9.js +0 -20
  73. package/dist/cli/{index-h07wn84h.js → index-7mkpsw6g.js} +3 -3
  74. package/dist/cli/{pending-delegations-0z3znsec.js → pending-delegations-68cbaqv2.js} +1 -1
@@ -1,10 +1,10 @@
1
1
  // @bun
2
2
  import {
3
3
  handleGuardrailExplain
4
- } from "./index-68aggz0v.js";
4
+ } from "./index-6sgqnvpw.js";
5
5
  import {
6
6
  handleGuardrailLog
7
- } from "./index-zvae4m8g.js";
7
+ } from "./index-17eybc3z.js";
8
8
  import {
9
9
  COMMAND_REGISTRY,
10
10
  SWARM_COMMAND_TOOL_ALLOWLIST,
@@ -83,49 +83,51 @@ import {
83
83
  handleWriteRetroCommand,
84
84
  normalizeSwarmCommandInput,
85
85
  resolveCommand
86
- } from "./index-ahxrne50.js";
87
- import"./index-kyvg2cmp.js";
88
- import"./index-vqcgmy4y.js";
89
- import"./index-emkb0bbe.js";
86
+ } from "./index-jyrhy4ww.js";
87
+ import"./index-19zn67q6.js";
88
+ import"./index-det5jwfk.js";
89
+ import"./index-7cspygmb.js";
90
90
  import"./index-4905hd2m.js";
91
91
  import"./index-nprawcc0.js";
92
- import"./index-gr7ssnj9.js";
93
- import"./index-xdsj4qtd.js";
94
- import"./index-rkd6qwjh.js";
92
+ import"./index-q00e4s1s.js";
93
+ import"./index-j9kxwshw.js";
94
+ import"./index-hns7xjm7.js";
95
95
  import"./index-vxv732ex.js";
96
96
  import"./index-c8s9a3zh.js";
97
97
  import"./index-816rkp4y.js";
98
98
  import"./index-22e127jw.js";
99
- import"./index-j33zb3cx.js";
100
- import"./index-3r1w7xky.js";
99
+ import"./index-m0bmh3w0.js";
100
+ import"./index-cqsak7bf.js";
101
101
  import"./index-rtry5xyf.js";
102
- import"./index-ny13nr0x.js";
103
- import"./index-3sx27bxt.js";
102
+ import"./index-n2712byd.js";
103
+ import"./index-dywekeen.js";
104
104
  import"./index-ae75rja9.js";
105
- import"./index-3jvqw5hy.js";
106
- import"./index-9xpgytj9.js";
107
- import"./index-qt2yaghg.js";
105
+ import"./index-q27bajqb.js";
106
+ import"./index-3rr0ese8.js";
107
+ import"./index-vcgfqd8f.js";
108
108
  import"./index-gnp9x6se.js";
109
109
  import {
110
110
  AGENT_TOOL_MAP,
111
111
  ORCHESTRATOR_NAME,
112
112
  stripKnownSwarmPrefix
113
- } from "./index-tftv9xh9.js";
113
+ } from "./index-wr8qgpz5.js";
114
114
  import"./index-4rhcth7a.js";
115
115
  import"./index-8fwhhayc.js";
116
116
  import"./index-q1exe2b3.js";
117
- import"./index-51gg9947.js";
118
- import"./index-wtn8wf7h.js";
117
+ import"./index-d6h5hq91.js";
118
+ import"./index-ctpgsj84.js";
119
+ import"./index-m1abzg93.js";
120
+ import"./index-gw3m5fw0.js";
119
121
  import"./index-ye3fndsw.js";
120
122
  import"./index-kzj3fp7c.js";
121
123
  import"./index-5e4e2hvv.js";
122
124
  import"./index-p0arc26j.js";
123
125
  import"./index-zgwm4ryv.js";
124
126
  import"./index-dqh3zhhc.js";
125
- import"./index-qg11yf8q.js";
126
- import"./index-h07wn84h.js";
127
- import"./index-76kvm9z5.js";
127
+ import"./index-6xnxz6z0.js";
128
+ import"./index-7mkpsw6g.js";
128
129
  import"./index-fsrp8wp3.js";
130
+ import"./index-76kvm9z5.js";
129
131
  import"./index-q2rrv0cy.js";
130
132
  import"./index-293f68mj.js";
131
133
  import"./index-7g4c7s5r.js";
@@ -4,13 +4,13 @@ import {
4
4
  } from "./index-rtry5xyf.js";
5
5
  import {
6
6
  readKnowledgeEvents
7
- } from "./index-3sx27bxt.js";
7
+ } from "./index-dywekeen.js";
8
8
  import {
9
9
  isLinked,
10
10
  readLinkPointer,
11
11
  resolveKnowledgeStoreDir,
12
12
  resolveLinkDir
13
- } from "./index-3jvqw5hy.js";
13
+ } from "./index-q27bajqb.js";
14
14
 
15
15
  // src/knowledge/curation-policy.ts
16
16
  import { appendFile, mkdir as mkdir2, readFile as readFile2 } from "fs/promises";
@@ -0,0 +1,547 @@
1
+ // @bun
2
+ import {
3
+ ExecutionProfileSchema,
4
+ PlanSchema,
5
+ TaskStatusSchema
6
+ } from "./index-m1abzg93.js";
7
+ import {
8
+ criticalWarn,
9
+ init_logger,
10
+ log
11
+ } from "./index-zgwm4ryv.js";
12
+ import {
13
+ withEvidenceLock
14
+ } from "./index-76kvm9z5.js";
15
+ import {
16
+ emit
17
+ } from "./index-q2rrv0cy.js";
18
+
19
+ // src/plan/utils.ts
20
+ function derivePlanId(plan) {
21
+ return `${plan.swarm}-${plan.title}`.replace(/[^a-zA-Z0-9-_]/g, "_");
22
+ }
23
+
24
+ // src/plan/ledger.ts
25
+ import * as crypto from "crypto";
26
+ import * as fs from "fs";
27
+ import * as path from "path";
28
+ init_logger();
29
+ var LEDGER_SCHEMA_VERSION = "1.1.0";
30
+ var LEDGER_FILENAME = "plan-ledger.jsonl";
31
+ var LEDGER_LOCK_PATH = path.join(".swarm", LEDGER_FILENAME);
32
+ var PLAN_JSON_FILENAME = "plan.json";
33
+
34
+ class LedgerStaleWriterError extends Error {
35
+ constructor(message) {
36
+ super(message);
37
+ this.name = "LedgerStaleWriterError";
38
+ }
39
+ }
40
+ function getLedgerPath(directory) {
41
+ return path.join(directory, ".swarm", LEDGER_FILENAME);
42
+ }
43
+ function getPlanJsonPath(directory) {
44
+ return path.join(directory, ".swarm", PLAN_JSON_FILENAME);
45
+ }
46
+ function writeFileFsyncedThenRename(tempPath, targetPath, data) {
47
+ const fd = fs.openSync(tempPath, "w");
48
+ try {
49
+ fs.writeFileSync(fd, data, "utf8");
50
+ fs.fsyncSync(fd);
51
+ } finally {
52
+ fs.closeSync(fd);
53
+ }
54
+ fs.renameSync(tempPath, targetPath);
55
+ }
56
+ function computePlanHash(plan) {
57
+ const normalized = {
58
+ schema_version: plan.schema_version,
59
+ title: plan.title,
60
+ swarm: plan.swarm,
61
+ current_phase: plan.current_phase,
62
+ migration_status: plan.migration_status,
63
+ execution_profile: plan.execution_profile,
64
+ phases: plan.phases.map((phase) => ({
65
+ id: phase.id,
66
+ name: phase.name,
67
+ status: phase.status,
68
+ required_agents: phase.required_agents ? [...phase.required_agents].sort() : undefined,
69
+ tasks: phase.tasks.map((task) => ({
70
+ id: task.id,
71
+ phase: task.phase,
72
+ status: task.status,
73
+ size: task.size,
74
+ description: task.description,
75
+ depends: [...task.depends].sort(),
76
+ acceptance: task.acceptance,
77
+ files_touched: [...task.files_touched].sort(),
78
+ evidence_path: task.evidence_path,
79
+ blocked_reason: task.blocked_reason
80
+ }))
81
+ }))
82
+ };
83
+ const jsonString = JSON.stringify(normalized);
84
+ return crypto.createHash("sha256").update(jsonString, "utf8").digest("hex");
85
+ }
86
+ function computePlanStructureHash(plan) {
87
+ const normalized = {
88
+ schema_version: plan.schema_version,
89
+ title: plan.title,
90
+ swarm: plan.swarm,
91
+ current_phase: plan.current_phase,
92
+ migration_status: plan.migration_status,
93
+ execution_profile: plan.execution_profile,
94
+ phases: plan.phases.map((phase) => ({
95
+ id: phase.id,
96
+ name: phase.name,
97
+ required_agents: phase.required_agents ? [...phase.required_agents].sort() : undefined,
98
+ tasks: phase.tasks.map((task) => ({
99
+ id: task.id,
100
+ phase: task.phase,
101
+ size: task.size,
102
+ description: task.description,
103
+ depends: [...task.depends].sort(),
104
+ acceptance: task.acceptance,
105
+ files_touched: [...task.files_touched].sort(),
106
+ evidence_path: task.evidence_path,
107
+ blocked_reason: task.blocked_reason
108
+ }))
109
+ }))
110
+ };
111
+ const jsonString = JSON.stringify(normalized);
112
+ return crypto.createHash("sha256").update(jsonString, "utf8").digest("hex");
113
+ }
114
+ function computeCurrentPlanHash(directory) {
115
+ const planPath = getPlanJsonPath(directory);
116
+ try {
117
+ const content = fs.readFileSync(planPath, "utf8");
118
+ const plan = JSON.parse(content);
119
+ return computePlanHash(plan);
120
+ } catch {
121
+ return "";
122
+ }
123
+ }
124
+ async function ledgerExists(directory) {
125
+ const ledgerPath = getLedgerPath(directory);
126
+ return fs.existsSync(ledgerPath);
127
+ }
128
+ async function getLatestLedgerSeq(directory) {
129
+ const ledgerPath = getLedgerPath(directory);
130
+ if (!fs.existsSync(ledgerPath)) {
131
+ return 0;
132
+ }
133
+ try {
134
+ const content = fs.readFileSync(ledgerPath, "utf8");
135
+ const lines = content.trim().split(`
136
+ `).filter((line) => line.trim() !== "");
137
+ if (lines.length === 0) {
138
+ return 0;
139
+ }
140
+ let maxSeq = 0;
141
+ for (const line of lines) {
142
+ try {
143
+ const event = JSON.parse(line);
144
+ if (event.seq > maxSeq) {
145
+ maxSeq = event.seq;
146
+ }
147
+ } catch {}
148
+ }
149
+ return maxSeq;
150
+ } catch {
151
+ return 0;
152
+ }
153
+ }
154
+ async function readLedgerEvents(directory) {
155
+ const ledgerPath = getLedgerPath(directory);
156
+ if (!fs.existsSync(ledgerPath)) {
157
+ return [];
158
+ }
159
+ try {
160
+ const content = fs.readFileSync(ledgerPath, "utf8");
161
+ const lines = content.trim().split(`
162
+ `).filter((line) => line.trim() !== "");
163
+ const events = [];
164
+ let skippedCount = 0;
165
+ for (const line of lines) {
166
+ try {
167
+ const event = JSON.parse(line);
168
+ events.push(event);
169
+ } catch {
170
+ skippedCount++;
171
+ }
172
+ }
173
+ if (skippedCount > 0) {
174
+ log(`[ledger] Skipped ${skippedCount} malformed line(s) in plan-ledger.jsonl`);
175
+ }
176
+ events.sort((a, b) => a.seq - b.seq);
177
+ return events;
178
+ } catch {
179
+ return [];
180
+ }
181
+ }
182
+ async function initLedger(directory, planId, initialPlanHash, initialPlan) {
183
+ const ledgerPath = getLedgerPath(directory);
184
+ const planJsonPath = getPlanJsonPath(directory);
185
+ if (fs.existsSync(ledgerPath)) {
186
+ throw new Error("Ledger already initialized. Use appendLedgerEvent to add events.");
187
+ }
188
+ let planHashAfter = initialPlanHash ?? "";
189
+ let embeddedPlan = initialPlan;
190
+ if (!initialPlanHash) {
191
+ try {
192
+ if (fs.existsSync(planJsonPath)) {
193
+ const content = fs.readFileSync(planJsonPath, "utf8");
194
+ const plan = JSON.parse(content);
195
+ planHashAfter = computePlanHash(plan);
196
+ if (!embeddedPlan)
197
+ embeddedPlan = plan;
198
+ }
199
+ } catch {}
200
+ }
201
+ const payload = embeddedPlan ? { plan: embeddedPlan, payload_hash: planHashAfter } : undefined;
202
+ const event = {
203
+ seq: 1,
204
+ timestamp: new Date().toISOString(),
205
+ plan_id: planId,
206
+ event_type: "plan_created",
207
+ source: "initLedger",
208
+ plan_hash_before: "",
209
+ plan_hash_after: planHashAfter,
210
+ schema_version: LEDGER_SCHEMA_VERSION,
211
+ ...payload ? { payload } : {}
212
+ };
213
+ fs.mkdirSync(path.join(directory, ".swarm"), { recursive: true });
214
+ const tempPath = `${ledgerPath}.tmp.${Date.now()}.${Math.floor(Math.random() * 1e9)}`;
215
+ const line = `${JSON.stringify(event)}
216
+ `;
217
+ writeFileFsyncedThenRename(tempPath, ledgerPath, line);
218
+ }
219
+ async function appendLedgerEvent(directory, eventInput, options) {
220
+ return withEvidenceLock(directory, LEDGER_LOCK_PATH, "plan-ledger", "append-ledger-event", async () => {
221
+ const ledgerPath = getLedgerPath(directory);
222
+ const latestSeq = await getLatestLedgerSeq(directory);
223
+ const nextSeq = latestSeq + 1;
224
+ const planHashBefore = computeCurrentPlanHash(directory);
225
+ if (options?.expectedSeq !== undefined && options.expectedSeq !== latestSeq) {
226
+ throw new LedgerStaleWriterError(`Stale writer: expected seq ${options.expectedSeq} but found ${latestSeq}`);
227
+ }
228
+ if (options?.expectedHash !== undefined && options.expectedHash !== planHashBefore) {
229
+ throw new LedgerStaleWriterError(`Stale writer: expected hash ${options.expectedHash} but found ${planHashBefore}`);
230
+ }
231
+ const planHashAfter = options?.planHashAfter ?? planHashBefore;
232
+ const event = {
233
+ ...eventInput,
234
+ seq: nextSeq,
235
+ timestamp: new Date().toISOString(),
236
+ plan_hash_before: planHashBefore,
237
+ plan_hash_after: planHashAfter,
238
+ schema_version: LEDGER_SCHEMA_VERSION
239
+ };
240
+ fs.mkdirSync(path.join(directory, ".swarm"), { recursive: true });
241
+ const tempPath = `${ledgerPath}.tmp.${Date.now()}.${Math.floor(Math.random() * 1e9)}`;
242
+ const line = `${JSON.stringify(event)}
243
+ `;
244
+ if (!fs.existsSync(ledgerPath)) {
245
+ throw new Error("Ledger not initialized. Call initLedger() first.");
246
+ }
247
+ const existingContent = fs.readFileSync(ledgerPath, "utf8");
248
+ writeFileFsyncedThenRename(tempPath, ledgerPath, existingContent + line);
249
+ return event;
250
+ });
251
+ }
252
+ async function takeSnapshotWithRetry(directory, plan, options) {
253
+ const MAX_RETRIES = 3;
254
+ const TOTAL_ATTEMPTS = 1 + MAX_RETRIES;
255
+ const telemetrySource = options?.source ?? "save_plan_tool";
256
+ const snapshotOptions = { planHashAfter: options?.planHashAfter };
257
+ let lastError;
258
+ for (let attempt = 1;attempt <= TOTAL_ATTEMPTS; attempt++) {
259
+ try {
260
+ await takeSnapshotEvent(directory, plan, snapshotOptions);
261
+ return;
262
+ } catch (err) {
263
+ lastError = err instanceof Error ? err : new Error(String(err));
264
+ if (attempt < TOTAL_ATTEMPTS) {
265
+ await new Promise((r) => setTimeout(r, 10 * 2 ** (attempt - 1)));
266
+ }
267
+ }
268
+ }
269
+ criticalWarn(`[takeSnapshotWithRetry] Snapshot failed after ${MAX_RETRIES} retries (${TOTAL_ATTEMPTS} attempts): ${lastError.message}`);
270
+ try {
271
+ emit("snapshot_failed", {
272
+ error: lastError.message,
273
+ retries: MAX_RETRIES,
274
+ source: telemetrySource
275
+ });
276
+ } catch {}
277
+ }
278
+ async function takeSnapshotEvent(directory, plan, options) {
279
+ const payloadHash = options?.payloadHashOverride ?? computePlanHash(plan);
280
+ const snapshotPayload = {
281
+ plan,
282
+ payload_hash: payloadHash
283
+ };
284
+ if (options?.approvalMetadata) {
285
+ snapshotPayload.approval = options.approvalMetadata;
286
+ }
287
+ const planId = derivePlanId(plan);
288
+ return appendLedgerEvent(directory, {
289
+ event_type: "snapshot",
290
+ source: options?.source ?? "takeSnapshotEvent",
291
+ plan_id: planId,
292
+ payload: snapshotPayload
293
+ }, { planHashAfter: options?.planHashAfter });
294
+ }
295
+ async function replayFromLedger(directory, options) {
296
+ const { plan } = await replayFromLedgerWithStatus(directory, options);
297
+ return plan;
298
+ }
299
+ async function replayFromLedgerWithStatus(directory, _options) {
300
+ const { events, truncated, badSuffix } = await readLedgerEventsWithIntegrity(directory);
301
+ if (events.length === 0) {
302
+ return { plan: null, truncated, badSuffix };
303
+ }
304
+ if (truncated && badSuffix !== null) {
305
+ await quarantineLedgerSuffix(directory, badSuffix);
306
+ }
307
+ const plan = reconstructPlanFromEvents(directory, events);
308
+ return { plan, truncated, badSuffix };
309
+ }
310
+ function reconstructPlanFromEvents(directory, events) {
311
+ const targetPlanId = events[0].plan_id;
312
+ const relevantEvents = events.filter((e) => e.plan_id === targetPlanId);
313
+ {
314
+ const snapshotEvents = relevantEvents.filter((e) => e.event_type === "snapshot");
315
+ if (snapshotEvents.length > 0) {
316
+ const latestSnapshotEvent = snapshotEvents[snapshotEvents.length - 1];
317
+ const snapshotPayload = latestSnapshotEvent.payload;
318
+ let plan2 = snapshotPayload.plan;
319
+ const eventsAfterSnapshot = relevantEvents.filter((e) => e.seq > latestSnapshotEvent.seq);
320
+ for (const event of eventsAfterSnapshot) {
321
+ plan2 = applyEventToPlan(plan2, event);
322
+ if (plan2 === null) {
323
+ return null;
324
+ }
325
+ }
326
+ return plan2;
327
+ }
328
+ }
329
+ const createdEvent = relevantEvents.find((e) => e.event_type === "plan_created");
330
+ if (createdEvent?.payload && typeof createdEvent.payload === "object" && "plan" in createdEvent.payload) {
331
+ const parseResult = PlanSchema.safeParse(createdEvent.payload.plan);
332
+ if (parseResult.success) {
333
+ let plan2 = parseResult.data;
334
+ const eventsAfterCreated = relevantEvents.filter((e) => e.seq > createdEvent.seq);
335
+ for (const event of eventsAfterCreated) {
336
+ if (plan2 === null)
337
+ return null;
338
+ plan2 = applyEventToPlan(plan2, event);
339
+ }
340
+ return plan2;
341
+ }
342
+ }
343
+ const planJsonPath = getPlanJsonPath(directory);
344
+ if (!fs.existsSync(planJsonPath)) {
345
+ return null;
346
+ }
347
+ let plan;
348
+ try {
349
+ const content = fs.readFileSync(planJsonPath, "utf8");
350
+ plan = JSON.parse(content);
351
+ } catch {
352
+ return null;
353
+ }
354
+ for (const event of relevantEvents) {
355
+ if (plan === null) {
356
+ return null;
357
+ }
358
+ plan = applyEventToPlan(plan, event);
359
+ }
360
+ return plan;
361
+ }
362
+ function applyEventToPlan(plan, event) {
363
+ switch (event.event_type) {
364
+ case "plan_created":
365
+ if (event.payload && typeof event.payload === "object" && "plan" in event.payload) {
366
+ const parsed = PlanSchema.safeParse(event.payload.plan);
367
+ if (parsed.success)
368
+ return parsed.data;
369
+ }
370
+ return plan;
371
+ case "task_status_changed":
372
+ if (event.task_id && event.to_status) {
373
+ const parseResult = TaskStatusSchema.safeParse(event.to_status);
374
+ if (!parseResult.success) {
375
+ return plan;
376
+ }
377
+ for (const phase of plan.phases) {
378
+ const task = phase.tasks.find((t) => t.id === event.task_id);
379
+ if (task) {
380
+ task.status = parseResult.data;
381
+ break;
382
+ }
383
+ }
384
+ }
385
+ return plan;
386
+ case "phase_completed":
387
+ if (event.phase_id) {
388
+ const phase = plan.phases.find((p) => p.id === event.phase_id);
389
+ if (phase) {
390
+ phase.status = "complete";
391
+ }
392
+ }
393
+ return plan;
394
+ case "plan_exported":
395
+ return plan;
396
+ case "task_added":
397
+ return plan;
398
+ case "task_removed":
399
+ if (event.task_id) {
400
+ for (const phase of plan.phases) {
401
+ const idx = phase.tasks.findIndex((t) => t.id === event.task_id);
402
+ if (idx !== -1) {
403
+ phase.tasks.splice(idx, 1);
404
+ break;
405
+ }
406
+ }
407
+ }
408
+ return plan;
409
+ case "task_updated":
410
+ return plan;
411
+ case "plan_rebuilt":
412
+ return plan;
413
+ case "task_reordered":
414
+ return plan;
415
+ case "snapshot":
416
+ return plan;
417
+ case "plan_reset":
418
+ return null;
419
+ case "execution_profile_set": {
420
+ const rawProfile = event.payload?.execution_profile;
421
+ if (rawProfile !== undefined) {
422
+ const parsed = ExecutionProfileSchema.safeParse(rawProfile);
423
+ if (parsed.success) {
424
+ return { ...plan, execution_profile: parsed.data };
425
+ }
426
+ }
427
+ return plan;
428
+ }
429
+ case "execution_profile_locked": {
430
+ if (plan.execution_profile) {
431
+ return {
432
+ ...plan,
433
+ execution_profile: { ...plan.execution_profile, locked: true }
434
+ };
435
+ }
436
+ return plan;
437
+ }
438
+ default:
439
+ throw new Error(`applyEventToPlan: unhandled event type "${event.event_type}" at seq ${event.seq}`);
440
+ }
441
+ }
442
+ async function readLedgerEventsWithIntegrity(directory) {
443
+ const ledgerPath = getLedgerPath(directory);
444
+ if (!fs.existsSync(ledgerPath)) {
445
+ return { events: [], truncated: false, badSuffix: null };
446
+ }
447
+ try {
448
+ const content = fs.readFileSync(ledgerPath, "utf8");
449
+ const lines = content.split(`
450
+ `);
451
+ const events = [];
452
+ let truncated = false;
453
+ let badSuffix = null;
454
+ for (let i = 0;i < lines.length; i++) {
455
+ const line = lines[i];
456
+ if (line.trim() === "") {
457
+ continue;
458
+ }
459
+ try {
460
+ const event = JSON.parse(line);
461
+ events.push(event);
462
+ } catch {
463
+ truncated = true;
464
+ badSuffix = lines.slice(i).join(`
465
+ `);
466
+ break;
467
+ }
468
+ }
469
+ events.sort((a, b) => a.seq - b.seq);
470
+ return { events, truncated, badSuffix };
471
+ } catch {
472
+ return { events: [], truncated: false, badSuffix: null };
473
+ }
474
+ }
475
+ async function quarantineLedgerSuffix(directory, badSuffix) {
476
+ let salvagedCount = 0;
477
+ for (const line of badSuffix.split(`
478
+ `)) {
479
+ if (line.trim() === "")
480
+ continue;
481
+ try {
482
+ JSON.parse(line);
483
+ salvagedCount++;
484
+ } catch {}
485
+ }
486
+ try {
487
+ const hash = crypto.createHash("sha256").update(badSuffix, "utf8").digest("hex").slice(0, 12);
488
+ const swarmDir = path.join(directory, ".swarm");
489
+ try {
490
+ const existing = fs.readdirSync(swarmDir).filter((name) => name.startsWith("plan-ledger.quarantine.") && name.endsWith(`.${hash}`));
491
+ for (const name of existing) {
492
+ const existingPath = path.join(swarmDir, name);
493
+ if (fs.readFileSync(existingPath, "utf8") === badSuffix) {
494
+ return { path: existingPath, salvagedCount };
495
+ }
496
+ }
497
+ } catch {}
498
+ const quarantinePath = path.join(swarmDir, `plan-ledger.quarantine.${Date.now()}.${hash}`);
499
+ fs.writeFileSync(quarantinePath, badSuffix, "utf8");
500
+ log(`[ledger] Corrupted suffix quarantined to ${path.relative(directory, quarantinePath)} (salvageable events: ${salvagedCount})`);
501
+ return { path: quarantinePath, salvagedCount };
502
+ } catch {
503
+ return { path: null, salvagedCount };
504
+ }
505
+ }
506
+ async function loadLastApprovedPlan(directory, expectedPlanId) {
507
+ const events = await readLedgerEvents(directory);
508
+ return findLastApprovedSnapshot(events, expectedPlanId);
509
+ }
510
+ function findLastApprovedSnapshot(events, expectedPlanId, extraFilter) {
511
+ if (events.length === 0) {
512
+ return null;
513
+ }
514
+ for (let i = events.length - 1;i >= 0; i--) {
515
+ const event = events[i];
516
+ if (event.event_type !== "snapshot")
517
+ continue;
518
+ if (event.source !== "critic_approved")
519
+ continue;
520
+ if (expectedPlanId !== undefined && event.plan_id !== expectedPlanId) {
521
+ continue;
522
+ }
523
+ const payload = event.payload;
524
+ if (!payload || typeof payload !== "object" || !payload.plan) {
525
+ continue;
526
+ }
527
+ if (expectedPlanId !== undefined) {
528
+ const payloadPlanId = derivePlanId(payload.plan);
529
+ if (payloadPlanId !== expectedPlanId) {
530
+ continue;
531
+ }
532
+ }
533
+ if (extraFilter && !extraFilter(payload)) {
534
+ continue;
535
+ }
536
+ return {
537
+ plan: payload.plan,
538
+ seq: event.seq,
539
+ timestamp: event.timestamp,
540
+ approval: payload.approval,
541
+ payloadHash: payload.payload_hash
542
+ };
543
+ }
544
+ return null;
545
+ }
546
+
547
+ export { derivePlanId, LedgerStaleWriterError, computePlanHash, computePlanStructureHash, computeCurrentPlanHash, ledgerExists, getLatestLedgerSeq, readLedgerEvents, initLedger, appendLedgerEvent, takeSnapshotWithRetry, takeSnapshotEvent, replayFromLedger, replayFromLedgerWithStatus, loadLastApprovedPlan };