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