harnery 0.45.0 → 0.45.1

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 (128) hide show
  1. package/dist/commander.js +0 -1
  2. package/dist/commands/files.d.ts +2 -2
  3. package/dist/commands/files.d.ts.map +1 -1
  4. package/dist/commands/files.js +4 -7
  5. package/dist/commands/servers.d.ts.map +1 -1
  6. package/dist/commands/servers.js +17 -11
  7. package/dist/commands/tunnel.d.ts +0 -15
  8. package/dist/commands/tunnel.d.ts.map +1 -1
  9. package/dist/commands/tunnel.js +153 -453
  10. package/dist/core/agents/live-authority-v3.d.ts +0 -1
  11. package/dist/core/agents/live-authority-v3.d.ts.map +1 -1
  12. package/dist/core/agents/live-authority-v3.js +0 -11
  13. package/dist/core/agents/live-lifecycle-v3.d.ts +2 -4
  14. package/dist/core/agents/live-lifecycle-v3.d.ts.map +1 -1
  15. package/dist/core/agents/live-lifecycle-v3.js +7 -28
  16. package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
  17. package/dist/core/agents/state/heartbeat-writer.js +14 -25
  18. package/dist/core/agents/state/live-coordination-writer.d.ts.map +1 -1
  19. package/dist/core/agents/state/live-coordination-writer.js +0 -5
  20. package/dist/core/agents/state/pidmap.d.ts.map +1 -1
  21. package/dist/core/agents/state/pidmap.js +0 -5
  22. package/dist/core/agents/state/stale-sweep.d.ts.map +1 -1
  23. package/dist/core/agents/state/stale-sweep.js +70 -180
  24. package/dist/core/artifacts/activity.d.ts +4 -1
  25. package/dist/core/artifacts/activity.d.ts.map +1 -1
  26. package/dist/core/artifacts/activity.js +8 -2
  27. package/dist/core/artifacts/delivery-card.d.ts.map +1 -1
  28. package/dist/core/artifacts/delivery-card.js +1 -2
  29. package/dist/core/artifacts/index.d.ts.map +1 -1
  30. package/dist/core/artifacts/index.js +38 -12
  31. package/dist/core/events/v3/authority-outbox.d.ts.map +1 -1
  32. package/dist/core/events/v3/authority-outbox.js +0 -29
  33. package/dist/core/events/v3/producers/coordination-recorder.d.ts +0 -1
  34. package/dist/core/events/v3/producers/coordination-recorder.d.ts.map +1 -1
  35. package/dist/core/events/v3/producers/coordination-recorder.js +0 -7
  36. package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
  37. package/dist/core/events/v3/producers/recorder.js +64 -77
  38. package/dist/core/hooks/cli.js +78 -48
  39. package/dist/core/servers/index.d.ts +13 -10
  40. package/dist/core/servers/index.d.ts.map +1 -1
  41. package/dist/core/servers/index.js +97 -192
  42. package/dist/core/servers/tunnels.d.ts +3 -5
  43. package/dist/core/servers/tunnels.d.ts.map +1 -1
  44. package/dist/core/servers/tunnels.js +2 -45
  45. package/dist/core/storage/durable-history.js +56 -22
  46. package/dist/core/storage/segments.d.ts.map +1 -1
  47. package/dist/core/storage/segments.js +73 -17
  48. package/dist/core/work/state.js +7 -7
  49. package/dist/core/workflow/engine.d.ts.map +1 -1
  50. package/dist/core/workflow/engine.js +21 -65
  51. package/dist/core/workflow/live-session-v3.d.ts +0 -6
  52. package/dist/core/workflow/live-session-v3.d.ts.map +1 -1
  53. package/dist/core/workflow/live-session-v3.js +5 -126
  54. package/dist/core/workflow/proof.d.ts +4 -4
  55. package/dist/core/workflow/proof.d.ts.map +1 -1
  56. package/dist/core/workflow/proof.js +4 -12
  57. package/dist/core/workflow/spawn-claude.d.ts.map +1 -1
  58. package/dist/core/workflow/spawn-claude.js +0 -19
  59. package/dist/core/workflow/spawn-codex.d.ts.map +1 -1
  60. package/dist/core/workflow/spawn-codex.js +0 -13
  61. package/dist/core/workflow/spawn-cursor.d.ts.map +1 -1
  62. package/dist/core/workflow/spawn-cursor.js +0 -19
  63. package/dist/core/workflow/spawn-opencode.d.ts.map +1 -1
  64. package/dist/core/workflow/spawn-opencode.js +0 -14
  65. package/dist/core/workflow/types.d.ts +11 -54
  66. package/dist/core/workflow/types.d.ts.map +1 -1
  67. package/dist/lib/identities/assume.d.ts.map +1 -1
  68. package/dist/lib/identities/assume.js +0 -4
  69. package/dist/lib/tunnel/state.d.ts +6 -13
  70. package/dist/lib/tunnel/state.d.ts.map +1 -1
  71. package/dist/lib/tunnel/state.js +12 -58
  72. package/package.json +1 -6
  73. package/src/commander.ts +0 -10
  74. package/src/commands/files.ts +5 -13
  75. package/src/commands/servers.ts +23 -12
  76. package/src/commands/tunnel.ts +166 -533
  77. package/src/core/agents/live-authority-v3.ts +0 -17
  78. package/src/core/agents/live-lifecycle-v3.ts +14 -52
  79. package/src/core/agents/state/heartbeat-writer.ts +12 -29
  80. package/src/core/agents/state/live-coordination-writer.ts +0 -29
  81. package/src/core/agents/state/pidmap.ts +0 -13
  82. package/src/core/agents/state/stale-sweep.ts +82 -247
  83. package/src/core/artifacts/activity.ts +9 -2
  84. package/src/core/artifacts/delivery-card.ts +1 -7
  85. package/src/core/artifacts/index.ts +45 -12
  86. package/src/core/events/v3/authority-outbox.ts +0 -56
  87. package/src/core/events/v3/producers/coordination-recorder.ts +0 -15
  88. package/src/core/events/v3/producers/recorder.ts +68 -94
  89. package/src/core/hooks/cli.ts +85 -47
  90. package/src/core/servers/index.ts +106 -228
  91. package/src/core/servers/tunnels.ts +3 -55
  92. package/src/core/storage/durable-history.ts +52 -20
  93. package/src/core/storage/segments.ts +79 -17
  94. package/src/core/work/state.ts +7 -7
  95. package/src/core/workflow/engine.ts +21 -73
  96. package/src/core/workflow/live-session-v3.ts +5 -146
  97. package/src/core/workflow/proof.ts +5 -21
  98. package/src/core/workflow/spawn-claude.ts +0 -18
  99. package/src/core/workflow/spawn-codex.ts +0 -13
  100. package/src/core/workflow/spawn-cursor.ts +0 -18
  101. package/src/core/workflow/spawn-opencode.ts +0 -14
  102. package/src/core/workflow/types.ts +11 -41
  103. package/src/lib/identities/assume.ts +0 -4
  104. package/src/lib/tunnel/state.ts +15 -70
  105. package/dist/commands/turn-rollback.d.ts +0 -5
  106. package/dist/commands/turn-rollback.d.ts.map +0 -1
  107. package/dist/commands/turn-rollback.js +0 -76
  108. package/dist/core/agents/state/unchanged-file.d.ts +0 -44
  109. package/dist/core/agents/state/unchanged-file.d.ts.map +0 -1
  110. package/dist/core/agents/state/unchanged-file.js +0 -390
  111. package/dist/core/events/v3/generation-custody.d.ts +0 -5
  112. package/dist/core/events/v3/generation-custody.d.ts.map +0 -1
  113. package/dist/core/events/v3/generation-custody.js +0 -50
  114. package/dist/core/turn-rollback/content.d.ts +0 -53
  115. package/dist/core/turn-rollback/content.d.ts.map +0 -1
  116. package/dist/core/turn-rollback/content.js +0 -396
  117. package/dist/core/turn-rollback/index.d.ts +0 -72
  118. package/dist/core/turn-rollback/index.d.ts.map +0 -1
  119. package/dist/core/turn-rollback/index.js +0 -443
  120. package/dist/core/workflow/failure-evidence.d.ts +0 -16
  121. package/dist/core/workflow/failure-evidence.d.ts.map +0 -1
  122. package/dist/core/workflow/failure-evidence.js +0 -114
  123. package/src/commands/turn-rollback.ts +0 -111
  124. package/src/core/agents/state/unchanged-file.ts +0 -456
  125. package/src/core/events/v3/generation-custody.ts +0 -59
  126. package/src/core/turn-rollback/content.ts +0 -499
  127. package/src/core/turn-rollback/index.ts +0 -580
  128. package/src/core/workflow/failure-evidence.ts +0 -141
@@ -1,443 +0,0 @@
1
- import { existsSync, lstatSync, readdirSync, readFileSync } from "node:fs";
2
- import { join, resolve } from "node:path";
3
- import { readJsonRecord, stableDigest, writeImmutableJson } from "../workflow/durable-record.js";
4
- import { acquireNoClobberLease } from "../workflow/workspaces/leases.js";
5
- import { createLocalGitWorktreeProvider } from "../workflow/workspaces/local-git.js";
6
- import { claimOverlapsWorkspace, reserveWorkspaceReclaim, } from "../workflow/workspaces/ownership.js";
7
- import { descriptorBackedPathsSupported } from "../workflow/workspaces/paths.js";
8
- import { readWorkflowLiveness } from "../workflow/workspaces/recovery.js";
9
- import { appendWorkspaceEvent, readWorkspaceClaim, readWorkspaceClaims, workspaceLockPath, } from "../workflow/workspaces/state.js";
10
- import { currentMatches, ensureDirectory, git, lockIndex, observe, persistSnapshot, restoreFile, safePaths, stageBefore, verifySnapshot, } from "./content.js";
11
- const PROVIDER = "local-git-worktree";
12
- const CAPTURE = /^turn-[a-f0-9]{32}$/;
13
- const TOTAL_LIMIT = 64 * 1024 * 1024;
14
- function statePath(coordRoot, binding) {
15
- if (binding?.provider?.id !== PROVIDER ||
16
- !/^[A-Za-z0-9][A-Za-z0-9._-]{0,199}$/.test(binding.binding_id))
17
- throw new Error("exclusive local provider binding is required");
18
- return join(resolve(coordRoot), ".harnery", "turn-rollback", PROVIDER, binding.binding_id);
19
- }
20
- function capturePath(scope, id, name) {
21
- if (!CAPTURE.test(id))
22
- throw new Error("invalid capture id");
23
- return join(scope.state, "captures", id, name);
24
- }
25
- function record(path) {
26
- if (!lstatSync(path).isFile() || lstatSync(path).isSymbolicLink())
27
- throw new Error("rollback record is unsafe");
28
- return readJsonRecord(path, "turn rollback record");
29
- }
30
- function load(scope, id) {
31
- if (!existsSync(capturePath(scope, id, "begin.json")))
32
- throw new Error("capture outcome is unknown; incomplete pending capture blocks further turns");
33
- const capture = record(capturePath(scope, id, "begin.json"));
34
- const { sha256, end: invalidEnd, ...basis } = capture;
35
- if (invalidEnd ||
36
- sha256 !== stableDigest(basis) ||
37
- capture.schema_version !== 1 ||
38
- capture.capture_id !== id ||
39
- capture.binding_sha256 !== stableDigest(scope.binding) ||
40
- capture.binding_generation !== scope.binding.generation ||
41
- !Number.isSafeInteger(capture.turn?.generation) ||
42
- capture.turn.generation < 1 ||
43
- !/^[A-Za-z0-9][A-Za-z0-9._-]{0,199}$/.test(capture.turn?.turn_id) ||
44
- stableDigest(safePaths(scope.binding, capture.paths)) !== stableDigest(capture.paths) ||
45
- stableDigest(capture.before.entries.map((entry) => entry.path)) !== stableDigest(capture.paths))
46
- throw new Error("capture authority changed");
47
- const sealed = capturePath(scope, id, "end.json");
48
- if (existsSync(sealed)) {
49
- const end = record(sealed);
50
- if (end.capture_sha256 !== sha256 ||
51
- stableDigest(end.snapshot.entries.map((entry) => entry.path)) !== stableDigest(capture.paths))
52
- throw new Error("sealed capture authority changed");
53
- return { ...capture, end: end.snapshot };
54
- }
55
- if (existsSync(capturePath(scope, id, "seal-pending.json")))
56
- throw new Error("seal outcome is unknown; incomplete pending snapshot blocks further turns");
57
- return capture;
58
- }
59
- function allCaptures(scope) {
60
- const root = join(scope.state, "captures");
61
- if (!existsSync(root))
62
- return [];
63
- const captures = readdirSync(root)
64
- .map((id) => load(scope, id))
65
- .sort((a, b) => a.turn.generation - b.turn.generation);
66
- if (captures.length > 64 ||
67
- new Set(captures.map((c) => c.turn.generation)).size !== captures.length)
68
- throw new Error("capture generation or retention limit is invalid");
69
- return captures;
70
- }
71
- function latest(scope, id) {
72
- const capture = allCaptures(scope).at(-1);
73
- if (!capture || capture.capture_id !== id)
74
- throw new Error("restore requires the latest turn capture");
75
- return capture;
76
- }
77
- function usage(captures) {
78
- return captures.reduce((sum, c) => sum + c.before.total_bytes + (c.end?.total_bytes ?? 0), 0);
79
- }
80
- async function withScope(coordRoot, binding, operation) {
81
- const state = statePath(coordRoot, binding);
82
- if (!descriptorBackedPathsSupported())
83
- throw new Error("descriptor-backed containment is unsupported");
84
- const provider = createLocalGitWorktreeProvider({ coordRoot });
85
- await provider.readBinding(binding);
86
- const claim = readWorkspaceClaim(coordRoot, PROVIDER, binding.binding_id);
87
- const authority = stableDigest({
88
- binding_id: claim.binding_id,
89
- request_sha256: claim.request_sha256,
90
- scope: "binding",
91
- });
92
- const path = workspaceLockPath(coordRoot, PROVIDER, binding.binding_id);
93
- const lease = acquireNoClobberLease({
94
- path,
95
- scope: "binding",
96
- authoritySha256: authority,
97
- staleAfterMs: 300_000,
98
- metadata: { binding_id: claim.binding_id, request_sha256: claim.request_sha256 },
99
- validateStaleOwner: (owner) => owner.authority_sha256 === authority &&
100
- owner.metadata?.binding_id === claim.binding_id &&
101
- owner.metadata?.request_sha256 === claim.request_sha256,
102
- });
103
- try {
104
- const reservation = reserveWorkspaceReclaim(coordRoot, claim);
105
- try {
106
- const guard = async () => {
107
- reservation.assertCurrent();
108
- const ownerPath = join(path, lease.owner.owner_file);
109
- const current = join(path, "current");
110
- const a = lstatSync(current, { bigint: true });
111
- const b = lstatSync(ownerPath, { bigint: true });
112
- if (!a.isFile() ||
113
- a.dev !== b.dev ||
114
- a.ino !== b.ino ||
115
- readFileSync(current, "utf8") !== readFileSync(ownerPath, "utf8") ||
116
- stableDigest(record(current)) !== stableDigest(lease.owner))
117
- throw new Error("binding lease changed");
118
- await provider.readBinding(binding);
119
- const attestation = await provider.attest(binding);
120
- if (attestation.status !== "ok" ||
121
- attestation.resource_state !== "active" ||
122
- attestation.repository?.conflicts.length ||
123
- attestation.repository?.operation_in_progress.length)
124
- throw new Error("workspace ownership is unproven");
125
- for (const peer of readWorkspaceClaims(coordRoot, PROVIDER)) {
126
- if (peer.binding_id !== binding.binding_id &&
127
- claimOverlapsWorkspace(peer, claim.workspace_root))
128
- throw new Error(`retained peer ${peer.binding_id} overlaps rollback workspace`);
129
- }
130
- };
131
- await guard();
132
- ensureDirectory(state);
133
- const store = join(state, "content.git");
134
- if (!existsSync(store))
135
- git(state, ["init", "--bare", "-q", store]);
136
- ensureDirectory(store);
137
- return await operation({
138
- binding,
139
- state,
140
- store,
141
- guard,
142
- audit: (event, data) => {
143
- appendWorkspaceEvent(coordRoot, claim, `turn_rollback.${event}`, data);
144
- },
145
- });
146
- }
147
- finally {
148
- reservation.release();
149
- }
150
- }
151
- finally {
152
- lease.release();
153
- }
154
- }
155
- /** Capture the exact named files before a turn, without staging anything. */
156
- export async function beginTurnRollback(coordRoot, input) {
157
- return withScope(coordRoot, input.binding, async (scope) => {
158
- if (!input.turn ||
159
- !/^[A-Za-z0-9][A-Za-z0-9._-]{0,199}$/.test(input.turn.turn_id) ||
160
- !Number.isSafeInteger(input.turn.generation) ||
161
- input.turn.generation < 1)
162
- throw new Error("invalid turn identity");
163
- const paths = safePaths(input.binding, input.paths);
164
- const prior = allCaptures(scope);
165
- const last = prior.at(-1);
166
- if (last &&
167
- (!last.end ||
168
- input.turn.generation <= last.turn.generation ||
169
- inspectTurnRollback(coordRoot, { binding: input.binding, capture_id: last.capture_id })
170
- .status === "unknown"))
171
- throw new Error("latest turn is open, unknown, or newer");
172
- if (prior.length >= 64)
173
- throw new Error("capture count limit exceeded");
174
- const index = lockIndex(input.binding, scope.state);
175
- try {
176
- const observed = observe(input.binding, scope.store, paths);
177
- if (usage(prior) + observed.total > TOTAL_LIMIT)
178
- throw new Error("binding retention byte limit exceeded");
179
- await scope.guard();
180
- const binding_sha256 = stableDigest(input.binding);
181
- const capture_id = `turn-${stableDigest({ binding_sha256, turn: input.turn, paths }).slice(0, 32)}`;
182
- ensureDirectory(join(scope.state, "captures", capture_id));
183
- writeImmutableJson(capturePath(scope, capture_id, "pending.json"), {
184
- schema_version: 1,
185
- capture_id,
186
- binding_sha256,
187
- turn: input.turn,
188
- paths,
189
- phase: "before",
190
- total_bytes: observed.total,
191
- });
192
- const before = persistSnapshot(scope.store, observed, `${capture_id}/before`);
193
- input.onSnapshotStep?.("content_persisted", capture_id);
194
- const basis = {
195
- schema_version: 1,
196
- capture_id,
197
- binding_sha256,
198
- binding_generation: input.binding.generation,
199
- turn: { ...input.turn },
200
- paths,
201
- before,
202
- };
203
- const capture = { ...basis, sha256: stableDigest(basis) };
204
- writeImmutableJson(capturePath(scope, capture_id, "begin.json"), capture);
205
- scope.audit("captured", {
206
- capture_id,
207
- capture_sha256: capture.sha256,
208
- turn: capture.turn,
209
- paths,
210
- });
211
- return capture;
212
- }
213
- finally {
214
- index.close();
215
- }
216
- });
217
- }
218
- /** Seal the end boundary of the latest captured turn. */
219
- export async function sealTurnRollback(coordRoot, input) {
220
- return withScope(coordRoot, input.binding, async (scope) => {
221
- const capture = latest(scope, input.capture_id);
222
- verifySnapshot(scope.store, capture.before);
223
- const index = lockIndex(input.binding, scope.state);
224
- try {
225
- if (capture.end) {
226
- verifySnapshot(scope.store, capture.end);
227
- if (!currentMatches(input.binding, scope.store, capture.end))
228
- throw new Error("sealed content changed");
229
- return capture;
230
- }
231
- const observed = observe(input.binding, scope.store, capture.paths);
232
- if (observed.head !== capture.before.head)
233
- throw new Error("HEAD changed during turn");
234
- if (usage(allCaptures(scope)) + observed.total > TOTAL_LIMIT)
235
- throw new Error("binding retention byte limit exceeded");
236
- await scope.guard();
237
- writeImmutableJson(capturePath(scope, capture.capture_id, "seal-pending.json"), {
238
- schema_version: 1,
239
- capture_id: capture.capture_id,
240
- binding_sha256: stableDigest(input.binding),
241
- turn: capture.turn,
242
- paths: capture.paths,
243
- phase: "end",
244
- total_bytes: observed.total,
245
- });
246
- const end = persistSnapshot(scope.store, observed, `${capture.capture_id}/end`);
247
- input.onSnapshotStep?.("content_persisted", capture.capture_id);
248
- writeImmutableJson(capturePath(scope, capture.capture_id, "end.json"), {
249
- capture_sha256: capture.sha256,
250
- snapshot: end,
251
- });
252
- scope.audit("sealed", {
253
- capture_id: capture.capture_id,
254
- capture_sha256: capture.sha256,
255
- end_sha256: end.sha256,
256
- });
257
- return { ...capture, end };
258
- }
259
- finally {
260
- index.close();
261
- }
262
- });
263
- }
264
- function planFor(capture) {
265
- if (!capture.end)
266
- throw new Error("turn capture is not sealed");
267
- const basis = {
268
- schema_version: 1,
269
- capture_id: capture.capture_id,
270
- binding_sha256: capture.binding_sha256,
271
- turn: capture.turn,
272
- before_sha256: capture.before.sha256,
273
- end_sha256: capture.end.sha256,
274
- paths: capture.paths.map((path, index) => ({
275
- path,
276
- before: capture.before.entries[index],
277
- end: capture.end.entries[index],
278
- })),
279
- };
280
- return { ...basis, plan_sha256: stableDigest(basis) };
281
- }
282
- /** Produce a dry-run exact-path proposal bound to both captured boundaries. */
283
- export async function prepareTurnRollback(coordRoot, input) {
284
- return withScope(coordRoot, input.binding, async (scope) => {
285
- const capture = latest(scope, input.capture_id);
286
- const plan = planFor(capture);
287
- verifySnapshot(scope.store, capture.before);
288
- verifySnapshot(scope.store, capture.end);
289
- const index = lockIndex(input.binding, scope.state);
290
- try {
291
- if (!currentMatches(input.binding, scope.store, capture.end))
292
- throw new Error("selected content or HEAD changed since seal");
293
- return plan;
294
- }
295
- finally {
296
- index.close();
297
- }
298
- });
299
- }
300
- export async function applyTurnRollback(coordRoot, input) {
301
- if (inspectTurnRollback(coordRoot, { binding: input.binding, capture_id: input.plan.capture_id })
302
- .status === "unknown")
303
- throw new Error("rollback outcome is unknown; automatic retry is refused");
304
- return withScope(coordRoot, input.binding, async (scope) => {
305
- const capture = latest(scope, input.plan.capture_id);
306
- const expected = planFor(capture);
307
- if (stableDigest(input.plan) !== stableDigest(expected))
308
- throw new Error("rollback proposal digest changed");
309
- verifySnapshot(scope.store, capture.before);
310
- verifySnapshot(scope.store, capture.end);
311
- const result = {
312
- schema_version: 1,
313
- capture_id: capture.capture_id,
314
- plan_sha256: expected.plan_sha256,
315
- };
316
- const receiptPath = capturePath(scope, capture.capture_id, "receipt.json");
317
- const startedPath = capturePath(scope, capture.capture_id, "started.json");
318
- const index = lockIndex(input.binding, scope.state);
319
- try {
320
- if (existsSync(receiptPath)) {
321
- const receipt = record(receiptPath);
322
- if (receipt.schema_version !== 1 ||
323
- receipt.status !== "applied" ||
324
- receipt.plan_sha256 !== expected.plan_sha256 ||
325
- receipt.capture_id !== capture.capture_id ||
326
- !currentMatches(input.binding, scope.store, capture.before))
327
- throw new Error("applied receipt or restored content changed");
328
- return { ...result, status: "already_applied" };
329
- }
330
- if (!currentMatches(input.binding, scope.store, capture.end))
331
- throw new Error("selected content or HEAD changed since proposal");
332
- if (!input.yes)
333
- return { ...result, status: "preview" };
334
- if (!input.actor?.trim() ||
335
- !input.reason?.trim() ||
336
- input.actor.length > 200 ||
337
- input.reason.length > 1000)
338
- throw new Error("actor and reason are required and bounded");
339
- if (readWorkflowLiveness(coordRoot, input.binding.run_id) !== "inactive")
340
- throw new Error("workflow must be proven inactive before rollback");
341
- await scope.guard();
342
- scope.audit("apply_requested", {
343
- ...result,
344
- turn: capture.turn,
345
- actor: input.actor,
346
- reason: input.reason,
347
- paths: capture.paths,
348
- });
349
- writeImmutableJson(startedPath, {
350
- ...result,
351
- turn: capture.turn,
352
- actor: input.actor,
353
- reason: input.reason,
354
- });
355
- try {
356
- input.onStep?.("started");
357
- // Each published path changes the expected worktree for subsequent guards.
358
- const progress = structuredClone(capture.end);
359
- for (const [position, entry] of capture.before.entries.entries()) {
360
- await scope.guard();
361
- if (readWorkflowLiveness(coordRoot, input.binding.run_id) !== "inactive" ||
362
- !currentMatches(input.binding, scope.store, progress))
363
- throw new Error("content or workflow changed during restore");
364
- restoreFile(input.binding, scope.store, entry);
365
- progress.entries[position].worktree = entry.worktree;
366
- progress.entries[position].untracked =
367
- !progress.entries[position].index && entry.worktree !== null;
368
- input.onStep?.("worktree_published");
369
- }
370
- await scope.guard();
371
- if (readWorkflowLiveness(coordRoot, input.binding.run_id) !== "inactive" ||
372
- !currentMatches(input.binding, scope.store, progress))
373
- throw new Error("content or workflow changed before index publication");
374
- stageBefore(input.binding, scope.store, capture.before, index);
375
- await scope.guard();
376
- if (readWorkflowLiveness(coordRoot, input.binding.run_id) !== "inactive" ||
377
- !currentMatches(input.binding, scope.store, progress))
378
- throw new Error("content changed before index publication");
379
- index.publish();
380
- input.onStep?.("index_published");
381
- await scope.guard();
382
- if (readWorkflowLiveness(coordRoot, input.binding.run_id) !== "inactive" ||
383
- !currentMatches(input.binding, scope.store, capture.before))
384
- throw new Error("restored content verification failed");
385
- const receipt = { ...result, status: "applied" };
386
- scope.audit("applied", { ...result, turn: capture.turn, paths: capture.paths });
387
- writeImmutableJson(receiptPath, receipt);
388
- return receipt;
389
- }
390
- catch (error) {
391
- scope.audit("unknown", { ...result, reason: String(error).slice(0, 1000) });
392
- return { ...result, status: "unknown", reason: String(error) };
393
- }
394
- }
395
- finally {
396
- index.close();
397
- }
398
- });
399
- }
400
- /** Read recovery evidence without attempting to continue an interrupted restore. */
401
- export function inspectTurnRollback(coordRoot, input) {
402
- const state = statePath(coordRoot, input.binding);
403
- const scope = { state, binding: input.binding };
404
- const beginPath = capturePath(scope, input.capture_id, "begin.json");
405
- if (!existsSync(beginPath)) {
406
- const pending = capturePath(scope, input.capture_id, "pending.json");
407
- if (!existsSync(pending))
408
- return { status: "missing", capture_id: input.capture_id };
409
- const reservation = record(pending);
410
- if (reservation.schema_version !== 1 ||
411
- reservation.capture_id !== input.capture_id ||
412
- reservation.binding_sha256 !== stableDigest(input.binding))
413
- throw new Error("pending capture authority changed");
414
- return { status: "unknown", capture_id: input.capture_id };
415
- }
416
- if (existsSync(capturePath(scope, input.capture_id, "seal-pending.json")) &&
417
- !existsSync(capturePath(scope, input.capture_id, "end.json")))
418
- return { status: "unknown", capture_id: input.capture_id };
419
- const capture = load(scope, input.capture_id);
420
- const started = existsSync(capturePath(scope, input.capture_id, "started.json"));
421
- const receipt = existsSync(capturePath(scope, input.capture_id, "receipt.json"));
422
- if (started) {
423
- const pending = record(capturePath(scope, input.capture_id, "started.json"));
424
- if (pending.schema_version !== 1 ||
425
- pending.capture_id !== capture.capture_id ||
426
- pending.plan_sha256 !== planFor(capture).plan_sha256 ||
427
- stableDigest(pending.turn) !== stableDigest(capture.turn))
428
- throw new Error("started restore authority changed");
429
- }
430
- if (receipt) {
431
- const value = record(capturePath(scope, input.capture_id, "receipt.json"));
432
- if (!started ||
433
- value.schema_version !== 1 ||
434
- value.status !== "applied" ||
435
- value.capture_id !== capture.capture_id ||
436
- value.plan_sha256 !== planFor(capture).plan_sha256)
437
- throw new Error("restore receipt is unproven");
438
- }
439
- return {
440
- status: started ? (receipt ? "applied" : "unknown") : capture.end ? "sealed" : "open",
441
- capture_id: input.capture_id,
442
- };
443
- }
@@ -1,16 +0,0 @@
1
- import type { AdapterRawResult } from "../adapters/types.js";
2
- import type { SpawnFailureEvidence, SpawnOutputSource, SpawnResult } from "./types.js";
3
- export declare function unknownFailureEvidence(): SpawnFailureEvidence;
4
- /** Built-in subprocess adapters observe output and process-start failures.
5
- * Their current vendor envelopes do not prove effects or native refusal.
6
- * In particular, blank output and 429/5xx statuses leave both unknown. */
7
- export declare function observeSpawnFailure(raw: Pick<AdapterRawResult, "spawnErrno" | "timedOut"> & Partial<Pick<AdapterRawResult, "stdout" | "resultFileText">>, text: string, source: SpawnOutputSource): SpawnFailureEvidence;
8
- export declare function isSpawnFailureEvidence(value: unknown): value is SpawnFailureEvidence;
9
- /** Missing observations are unknown; malformed positive claims are rejected.
10
- * This is also used when reading persisted workflow proof. */
11
- export declare function resolveFailureEvidence(value: unknown): SpawnFailureEvidence;
12
- export declare function failureEvidenceAllowsUncharged(value: unknown): boolean;
13
- /** A provider retry requires positive no-effect evidence. A missing binary is
14
- * known not to have started, but retrying that unchanged environment cannot help. */
15
- export declare function canRetryFailedSpawn(result: SpawnResult): boolean;
16
- //# sourceMappingURL=failure-evidence.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"failure-evidence.d.ts","sourceRoot":"","sources":["../../../src/core/workflow/failure-evidence.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAC7D,OAAO,KAAK,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAEvF,wBAAgB,sBAAsB,IAAI,oBAAoB,CAM7D;AAED;;0EAE0E;AAC1E,wBAAgB,mBAAmB,CACjC,GAAG,EAAE,IAAI,CAAC,gBAAgB,EAAE,YAAY,GAAG,UAAU,CAAC,GACpD,OAAO,CAAC,IAAI,CAAC,gBAAgB,EAAE,QAAQ,GAAG,gBAAgB,CAAC,CAAC,EAC9D,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,iBAAiB,GACxB,oBAAoB,CAsBtB;AA+DD,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,oBAAoB,CAQpF;AAED;8DAC8D;AAC9D,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,OAAO,GAAG,oBAAoB,CAI3E;AAED,wBAAgB,8BAA8B,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAOtE;AAED;qFACqF;AACrF,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAMhE"}
@@ -1,114 +0,0 @@
1
- import { createHash } from "node:crypto";
2
- export function unknownFailureEvidence() {
3
- return {
4
- effect: { state: "unknown" },
5
- refusal: { state: "unknown" },
6
- partial_output: { state: "unknown" },
7
- };
8
- }
9
- /** Built-in subprocess adapters observe output and process-start failures.
10
- * Their current vendor envelopes do not prove effects or native refusal.
11
- * In particular, blank output and 429/5xx statuses leave both unknown. */
12
- export function observeSpawnFailure(raw, text, source) {
13
- const bytes = Buffer.byteLength(text);
14
- return {
15
- effect: raw.spawnErrno === "ENOENT" &&
16
- !raw.timedOut &&
17
- bytes === 0 &&
18
- !raw.stdout?.trim() &&
19
- !raw.resultFileText?.trim()
20
- ? { state: "none", source: "spawn_errno", reference: "ENOENT" }
21
- : { state: "unknown" },
22
- refusal: { state: "unknown" },
23
- partial_output: bytes > 0
24
- ? {
25
- state: "present",
26
- source,
27
- bytes,
28
- sha256: createHash("sha256").update(text).digest("hex"),
29
- }
30
- : { state: "absent", source },
31
- };
32
- }
33
- function record(value) {
34
- return typeof value === "object" && value !== null && !Array.isArray(value);
35
- }
36
- function keys(value, expected) {
37
- return (Object.keys(value).length === expected.length &&
38
- expected.every((key) => Object.hasOwn(value, key)));
39
- }
40
- function reference(value) {
41
- return typeof value === "string" && value.trim().length > 0 && value.length <= 512;
42
- }
43
- function validEffect(value) {
44
- if (!record(value))
45
- return false;
46
- if (value.state === "unknown")
47
- return keys(value, ["state"]);
48
- if (!keys(value, ["state", "source", "reference"]))
49
- return false;
50
- if (value.state === "none" && value.source === "spawn_errno") {
51
- return value.reference === "ENOENT";
52
- }
53
- return ((value.state === "none" || value.state === "observed") &&
54
- value.source === "adapter_evidence" &&
55
- reference(value.reference));
56
- }
57
- function validRefusal(value) {
58
- if (!record(value))
59
- return false;
60
- if (value.state === "unknown")
61
- return keys(value, ["state"]);
62
- return (keys(value, ["state", "source", "reference"]) &&
63
- value.state === "refused" &&
64
- value.source === "vendor_envelope" &&
65
- reference(value.reference));
66
- }
67
- function validOutput(value) {
68
- if (!record(value))
69
- return false;
70
- if (value.state === "unknown")
71
- return keys(value, ["state"]);
72
- if (value.source !== "final_message" &&
73
- value.source !== "vendor_envelope" &&
74
- value.source !== "stdout") {
75
- return false;
76
- }
77
- if (value.state === "absent")
78
- return keys(value, ["state", "source"]);
79
- return (value.state === "present" &&
80
- keys(value, ["state", "source", "bytes", "sha256"]) &&
81
- Number.isSafeInteger(value.bytes) &&
82
- value.bytes > 0 &&
83
- typeof value.sha256 === "string" &&
84
- /^[a-f0-9]{64}$/.test(value.sha256));
85
- }
86
- export function isSpawnFailureEvidence(value) {
87
- return (record(value) &&
88
- keys(value, ["effect", "refusal", "partial_output"]) &&
89
- validEffect(value.effect) &&
90
- validRefusal(value.refusal) &&
91
- validOutput(value.partial_output));
92
- }
93
- /** Missing observations are unknown; malformed positive claims are rejected.
94
- * This is also used when reading persisted workflow proof. */
95
- export function resolveFailureEvidence(value) {
96
- if (value === undefined)
97
- return unknownFailureEvidence();
98
- if (!isSpawnFailureEvidence(value))
99
- throw new Error("invalid spawn failure evidence");
100
- return value;
101
- }
102
- export function failureEvidenceAllowsUncharged(value) {
103
- const evidence = resolveFailureEvidence(value);
104
- return (evidence.effect.state === "none" &&
105
- evidence.refusal.state !== "refused" &&
106
- evidence.partial_output.state === "absent");
107
- }
108
- /** A provider retry requires positive no-effect evidence. A missing binary is
109
- * known not to have started, but retrying that unchanged environment cannot help. */
110
- export function canRetryFailedSpawn(result) {
111
- return (!result.ok &&
112
- result.class !== "environment" &&
113
- failureEvidenceAllowsUncharged(result.failureEvidence));
114
- }