faberun 0.3.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 (144) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +131 -0
  3. package/bin/faberun.mjs +25 -0
  4. package/integrations/claude-code/statusline-bench.sh +42 -0
  5. package/integrations/claude-code/statusline.sh +80 -0
  6. package/package.json +33 -0
  7. package/skills/faberun/SKILL.md +24 -0
  8. package/skills/faberun/references/contract.md +380 -0
  9. package/skills/faberun/references/engineering.md +29 -0
  10. package/skills/faberun/references/handoffs.md +26 -0
  11. package/skills/faberun/references/operations.md +184 -0
  12. package/skills/faberun/references/rules.md +35 -0
  13. package/skills/faberun/references/workflow.md +23 -0
  14. package/skills/init-agentkit/SKILL.md +108 -0
  15. package/skills/init-agentkit/scripts/install-agentkit.sh +127 -0
  16. package/skills/init-agentkit/templates/.claude/commands/create-adr.md +44 -0
  17. package/skills/init-agentkit/templates/.github/workflows/quality.yml +43 -0
  18. package/skills/init-agentkit/templates/.sentrux/baseline.json +9 -0
  19. package/skills/init-agentkit/templates/.sentrux/rules.toml +21 -0
  20. package/skills/init-agentkit/templates/AGENTS.md +110 -0
  21. package/skills/init-agentkit/templates/docs/ABSTRACTIONS.md +30 -0
  22. package/skills/init-agentkit/templates/docs/ARCHITECTURE.md +31 -0
  23. package/skills/init-agentkit/templates/docs/GETTING-STARTED.md +44 -0
  24. package/skills/init-agentkit/templates/docs/VISION.md +33 -0
  25. package/skills/init-agentkit/templates/docs/adr/0001-record-architecture-decisions.md +36 -0
  26. package/skills/init-agentkit/templates/docs/adr/0002-root-managed-ai-guidance.md +37 -0
  27. package/skills/init-agentkit/templates/docs/adr/0003-sentrux-structural-quality-gates.md +49 -0
  28. package/skills/init-agentkit/templates/docs/adr/README.md +52 -0
  29. package/skills/init-agentkit/templates/docs/sentrux.md +66 -0
  30. package/skills/init-agentkit/templates/githooks/commit-msg +22 -0
  31. package/skills/init-agentkit/templates/githooks/pre-commit +32 -0
  32. package/src/campaign/brief.mjs +394 -0
  33. package/src/campaign/chain.mjs +555 -0
  34. package/src/campaign/handoff.mjs +516 -0
  35. package/src/campaign/index.mjs +300 -0
  36. package/src/campaign/journal.mjs +347 -0
  37. package/src/campaign/layout.mjs +51 -0
  38. package/src/campaign/metrics-evals.mjs +25 -0
  39. package/src/campaign/metrics.mjs +517 -0
  40. package/src/campaign/projection.mjs +250 -0
  41. package/src/campaign/record.mjs +102 -0
  42. package/src/campaign/unpark.mjs +56 -0
  43. package/src/cli/brand.mjs +205 -0
  44. package/src/cli/campaign.mjs +730 -0
  45. package/src/cli/contract.mjs +67 -0
  46. package/src/cli/init.mjs +170 -0
  47. package/src/cli/launch.mjs +239 -0
  48. package/src/cli/seat.mjs +139 -0
  49. package/src/cli/setup.mjs +294 -0
  50. package/src/cli/skills.mjs +105 -0
  51. package/src/cli/update.mjs +216 -0
  52. package/src/cli.mjs +525 -0
  53. package/src/contract/articles.mjs +12 -0
  54. package/src/contract/assert.mjs +162 -0
  55. package/src/contract/definition-of-done.mjs +97 -0
  56. package/src/contract/final-verification.mjs +96 -0
  57. package/src/contract/index.mjs +641 -0
  58. package/src/contract/judge-envelope.mjs +25 -0
  59. package/src/contract/review-modes.mjs +151 -0
  60. package/src/contract/runtime.mjs +204 -0
  61. package/src/contract/schema-version.mjs +25 -0
  62. package/src/contract/scope-findings.mjs +77 -0
  63. package/src/contract/snapshot.mjs +639 -0
  64. package/src/contract/task-packet.mjs +495 -0
  65. package/src/contract/untrusted.mjs +75 -0
  66. package/src/contract/verification.mjs +185 -0
  67. package/src/contract/worker-result.mjs +138 -0
  68. package/src/engine/assignment.mjs +63 -0
  69. package/src/engine/backoff.mjs +492 -0
  70. package/src/engine/bulk-read.mjs +361 -0
  71. package/src/engine/cancel.mjs +177 -0
  72. package/src/engine/detach.mjs +101 -0
  73. package/src/engine/dispatch.mjs +752 -0
  74. package/src/engine/failover.mjs +192 -0
  75. package/src/engine/gate.mjs +183 -0
  76. package/src/engine/judge-gate.mjs +517 -0
  77. package/src/engine/lifecycle.mjs +772 -0
  78. package/src/engine/live-preflight.mjs +299 -0
  79. package/src/engine/mutation.mjs +146 -0
  80. package/src/engine/notify-queue.mjs +327 -0
  81. package/src/engine/process-identity.mjs +72 -0
  82. package/src/engine/process.mjs +774 -0
  83. package/src/engine/prompts.mjs +289 -0
  84. package/src/engine/recover.mjs +300 -0
  85. package/src/engine/result-file.mjs +222 -0
  86. package/src/engine/resume.mjs +635 -0
  87. package/src/engine/retry.mjs +334 -0
  88. package/src/engine/review.mjs +228 -0
  89. package/src/engine/run-command.mjs +287 -0
  90. package/src/engine/run-identity.mjs +411 -0
  91. package/src/engine/runtime-discovery.mjs +235 -0
  92. package/src/engine/scheduler.mjs +526 -0
  93. package/src/engine/scope.mjs +378 -0
  94. package/src/engine/settle.mjs +207 -0
  95. package/src/engine/state.mjs +148 -0
  96. package/src/engine/supervise.mjs +713 -0
  97. package/src/engine/verify.mjs +167 -0
  98. package/src/harnesses/agy/index.mjs +62 -0
  99. package/src/harnesses/catalogue.mjs +509 -0
  100. package/src/harnesses/claude/index.mjs +90 -0
  101. package/src/harnesses/codex/index.mjs +87 -0
  102. package/src/harnesses/dsh/closed-packet.patch.yml +42 -0
  103. package/src/harnesses/dsh/index.mjs +210 -0
  104. package/src/harnesses/dsh/runner.mjs +259 -0
  105. package/src/harnesses/exec-jsonl/index.mjs +788 -0
  106. package/src/harnesses/index.mjs +508 -0
  107. package/src/harnesses/protocol.mjs +531 -0
  108. package/src/harnesses/replay/bin.mjs +386 -0
  109. package/src/harnesses/replay/index.mjs +238 -0
  110. package/src/harnesses/zcode/index.mjs +276 -0
  111. package/src/host/config.mjs +87 -0
  112. package/src/host/home.mjs +149 -0
  113. package/src/host/package.mjs +23 -0
  114. package/src/host/preflight.mjs +520 -0
  115. package/src/host/tool-policy-decisions.mjs +341 -0
  116. package/src/host/tool-policy-hook.mjs +270 -0
  117. package/src/notify/index.mjs +359 -0
  118. package/src/notify/os-macos.mjs +81 -0
  119. package/src/repo/declared-paths.mjs +220 -0
  120. package/src/repo/integrate.mjs +546 -0
  121. package/src/repo/scope-closure.mjs +665 -0
  122. package/src/repo/signal-block.mjs +16 -0
  123. package/src/repo/signal.mjs +222 -0
  124. package/src/repo/source-identity.mjs +295 -0
  125. package/src/repo/workspace.mjs +557 -0
  126. package/src/repo/worktree.mjs +352 -0
  127. package/src/report/final.mjs +200 -0
  128. package/src/report/metrics-report.mjs +99 -0
  129. package/src/report/next.mjs +383 -0
  130. package/src/report/render.mjs +716 -0
  131. package/src/run/disk-gc.mjs +251 -0
  132. package/src/run/lock.mjs +329 -0
  133. package/src/run/node-store.mjs +62 -0
  134. package/src/run/operations.mjs +286 -0
  135. package/src/run/store.mjs +187 -0
  136. package/src/run/usage.mjs +337 -0
  137. package/src/seat/harnesses.mjs +83 -0
  138. package/src/seat/index.mjs +239 -0
  139. package/src/seat/tmux.mjs +208 -0
  140. package/src/util.mjs +0 -0
  141. package/src/web/api.mjs +371 -0
  142. package/src/web/boundary.mjs +88 -0
  143. package/src/web/index.html +299 -0
  144. package/src/web/server.mjs +552 -0
@@ -0,0 +1,286 @@
1
+ /**
2
+ * The exactly-once ledger: for every provider invocation, an intent written
3
+ * before the process is released and a settlement merged after it closes.
4
+ *
5
+ * The distinction the rest of the system depends on is `settled` versus
6
+ * `unknown_effect`. A settled operation has a known provider outcome. An
7
+ * unknown effect means the request may have run without leaving proof, and it is
8
+ * explicitly NOT permission to retry -- `replayPolicy` and a clean persisted
9
+ * scope decide that, elsewhere.
10
+ */
11
+ import { basename, join } from "node:path";
12
+ import { createHash } from "node:crypto";
13
+ import { existsSync } from "node:fs";
14
+
15
+ import { readBoundedTail } from "../engine/process.mjs";
16
+ import { readJson, writeJsonAtomic } from "./store.mjs";
17
+ import { runtimeSnapshot } from "../engine/failover.mjs";
18
+ import { normalizeProviderResult } from "../harnesses/index.mjs";
19
+
20
+ /** @typedef {import("../engine/process.mjs").Invocation} Invocation */
21
+ /** @typedef {import("../contract/index.mjs").NodeSnapshot} NodeSnapshot */
22
+ /** @typedef {import("../contract/index.mjs").Usage} Usage */
23
+ /** @typedef {import("../contract/index.mjs").ValidatedContract} ValidatedContract */
24
+
25
+ const OPERATIONS_SCHEMA_VERSION = 1;
26
+ /**
27
+ * @param {string} runDir
28
+ * @param {string} invocationId
29
+ * @returns {string}
30
+ */
31
+ function operationIntentPath(runDir, invocationId) {
32
+ return join(runDir, "operations", `${invocationId}.intent.json`);
33
+ }
34
+ /**
35
+ * @param {string} runDir
36
+ * @param {string} invocationId
37
+ * @returns {string}
38
+ */
39
+ function operationSettlementPath(runDir, invocationId) {
40
+ return join(runDir, "operations", `${invocationId}.settlement.json`);
41
+ }
42
+ /**
43
+ * Reserve the operation durably before the gate releases the provider process.
44
+ * The invocation identity is written before spawn and is the only identity
45
+ * later accepted for settlement or recovery.
46
+ *
47
+ * @param {string} runDir
48
+ * @param {Invocation} invocation
49
+ * @param {{nodeId: string, role: "worker"|"judge", attempt: number, runtimeFingerprint: string, prompt: string}} context
50
+ */
51
+ export function persistInvocationIntent(runDir, invocation, context) {
52
+ const promptFingerprint = createHash("sha256").update(context.prompt, "utf8").digest("hex");
53
+ writeJsonAtomic(operationIntentPath(runDir, invocation.id), {
54
+ schemaVersion: OPERATIONS_SCHEMA_VERSION,
55
+ operationId: invocation.id,
56
+ invocationId: invocation.id,
57
+ runId: invocation.runId ?? basename(runDir),
58
+ campaignId: invocation.campaignId ?? null,
59
+ nodeId: context.nodeId,
60
+ role: context.role,
61
+ phase: invocation.phase,
62
+ planPhase: invocation.planPhase ?? null,
63
+ attempt: context.attempt,
64
+ runtimeId: invocation.runtimeId ?? null,
65
+ runtimeFingerprint: context.runtimeFingerprint,
66
+ promptFingerprint,
67
+ promptHash: promptFingerprint,
68
+ scopeSnapshotPath: context.role === "worker" ? invocation.snapshotPath ?? null : null,
69
+ scopeSnapshotRef: context.role === "worker" ? invocation.snapshotPath ?? null : null,
70
+ startedAt: invocation.startedAt,
71
+ intentAt: new Date().toISOString(),
72
+ });
73
+ }
74
+ /**
75
+ * Read an operation record without allowing a malformed or mismatched record
76
+ * to become recovery evidence.
77
+ *
78
+ * @param {string} runDir
79
+ * @param {string} invocationId
80
+ * @returns {Record<string, unknown>|null}
81
+ */
82
+ export function readOperationSettlement(runDir, invocationId) {
83
+ try {
84
+ const record = readJson(operationSettlementPath(runDir, invocationId));
85
+ return record.operationId === invocationId ? record : null;
86
+ } catch {
87
+ return null;
88
+ }
89
+ }
90
+ /**
91
+ * @param {string} runDir
92
+ * @param {string} invocationId
93
+ * @returns {Record<string, unknown>|null}
94
+ */
95
+ function readOperationIntent(runDir, invocationId) {
96
+ try {
97
+ const record = readJson(operationIntentPath(runDir, invocationId));
98
+ return record.operationId === invocationId ? record : null;
99
+ } catch {
100
+ return null;
101
+ }
102
+ }
103
+ /**
104
+ * A preliminary close observation is not a terminal settlement. It can be
105
+ * replaced by the final envelope outcome, while a resolved outcome is never
106
+ * downgraded by a later controller pass.
107
+ */
108
+ const UNRESOLVED_OPERATION_STATUSES = new Set(["closed", "unknown_effect"]);
109
+ const RESOLVED_OPERATION_STATUSES = new Set(["done", "failed", "exhausted", "stalled", "canceled", "adopted", "rejudge", "restarted", "safe_replay", "reconciled"]);
110
+ /**
111
+ * @param {string} runDir
112
+ * @param {string} invocationId
113
+ * @returns {boolean}
114
+ */
115
+ export function operationNeedsRecovery(runDir, invocationId) {
116
+ const settlement = readOperationSettlement(runDir, invocationId);
117
+ return !settlement || UNRESOLVED_OPERATION_STATUSES.has(String(settlement.status));
118
+ }
119
+ /**
120
+ * @param {Invocation|undefined|string} invocationOrId
121
+ * @param {unknown} supplied
122
+ * @param {unknown[]} existing
123
+ * @returns {Record<string, string>[]}
124
+ */
125
+ function operationReceipts(invocationOrId, supplied, existing = []) {
126
+ /** @type {Record<string, string>[]} */
127
+ const receipts = [];
128
+ const seen = new Set();
129
+ /** @type {(kind: string, ref: unknown) => void} */
130
+ const add = (kind, ref) => {
131
+ if (typeof ref !== "string" || ref.length === 0) return;
132
+ const key = `${kind}\u0000${ref}`;
133
+ if (seen.has(key)) return;
134
+ seen.add(key);
135
+ receipts.push({ kind, ref });
136
+ };
137
+ for (const receipt of existing) {
138
+ if (receipt && typeof receipt === "object") {
139
+ const record = /** @type {Record<string, unknown>} */ (receipt);
140
+ add(String(record.kind ?? "operation"), String(record.ref ?? ""));
141
+ }
142
+ }
143
+ if (Array.isArray(supplied)) {
144
+ for (const receipt of supplied) {
145
+ if (typeof receipt === "string") add("provider", receipt);
146
+ else if (receipt && typeof receipt === "object") {
147
+ const record = /** @type {Record<string, unknown>} */ (receipt);
148
+ add(String(record.kind ?? "provider"), String(record.ref ?? ""));
149
+ }
150
+ }
151
+ }
152
+ const invocation = invocationOrId && typeof invocationOrId === "object" ? invocationOrId : null;
153
+ if (invocation) {
154
+ add("prompt", invocation.promptPath);
155
+ add("stdout", invocation.stdoutPath);
156
+ add("stderr", invocation.stderrPath);
157
+ if (invocation.phase === "worker") add("scope_snapshot", invocation.snapshotPath);
158
+ add("provider", invocation.continuationId);
159
+ }
160
+ return receipts;
161
+ }
162
+ /**
163
+ * Provider-side evidence from the close path. The continuation identity the
164
+ * provider returned (thread/session) is a durable receipt for the invocation's
165
+ * external effect; the first terminal settlement must persist it.
166
+ *
167
+ * @param {{continuationId?: string|null}|null|undefined} envelope
168
+ * @returns {Record<string, string>[]}
169
+ */
170
+ export function providerReceipts(envelope) {
171
+ const ref = envelope?.continuationId;
172
+ return typeof ref === "string" && ref.length > 0 ? [{ kind: "provider", ref }] : [];
173
+ }
174
+ /**
175
+ * Provider receipts still recoverable from an invocation's surviving stream
176
+ * tail. A controller-loss window's first terminal settlement must persist
177
+ * them so repeated settlement/recovery stays idempotent and exact-once.
178
+ *
179
+ * @param {ValidatedContract} contract
180
+ * @param {Invocation|undefined} invocation
181
+ * @returns {Record<string, string>[]}
182
+ */
183
+ export function providerReceiptsFromInvocationTail(contract, invocation) {
184
+ if (!invocation?.stdoutPath) return [];
185
+ try {
186
+ const runtime = typeof invocation.runtimeId === "string"
187
+ ? runtimeSnapshot(contract, invocation.runtimeId)
188
+ : null;
189
+ if (!runtime) return [];
190
+ const envelope = normalizeProviderResult(
191
+ runtime,
192
+ readBoundedTail(invocation.stdoutPath),
193
+ invocation.exitCode ?? null,
194
+ invocation.signal ?? null,
195
+ );
196
+ return providerReceipts(envelope);
197
+ } catch {
198
+ return [];
199
+ }
200
+ }
201
+ /** @param {unknown} value @returns {unknown|null} */
202
+ function boundedSettlementResult(value) {
203
+ if (value === undefined || value === null) return null;
204
+ try {
205
+ const serialized = typeof value === "string" ? value : JSON.stringify(value);
206
+ if (typeof serialized !== "string" || Buffer.byteLength(serialized, "utf8") > 64 * 1024) return null;
207
+ return value;
208
+ } catch {
209
+ return null;
210
+ }
211
+ }
212
+ /** @param {NodeSnapshot} state @returns {Record<string, unknown>} */
213
+ export function operationNextState(state) {
214
+ return {
215
+ status: state.status,
216
+ phase: state.phase,
217
+ attempt: state.attempt,
218
+ revisions: state.revisions,
219
+ };
220
+ }
221
+ /**
222
+ * Persist a settlement as an idempotent operation record. The operation keeps
223
+ * its first settledAt timestamp, merges receipts, and retains unknown-effect
224
+ * classification while the controller resolves it.
225
+ *
226
+ * @param {string} runDir
227
+ * @param {Invocation|string} invocationOrId
228
+ * @param {{status?: string, usage?: import("../contract/index.mjs").Usage|null, costUsd?: number|null, structuredResult?: boolean|null, result?: unknown, receipts?: unknown, nextState?: unknown, terminalOutcome?: unknown, unknownEffect?: boolean, classification?: string, reason?: string, error?: unknown}} settlement
229
+ */
230
+ export function settleInvocation(runDir, invocationOrId, settlement) {
231
+ const invocation = typeof invocationOrId === "object" ? invocationOrId : undefined;
232
+ const invocationId = typeof invocationOrId === "string" ? invocationOrId : invocation?.id;
233
+ if (!invocationId) throw new TypeError("settlement requires an invocation identity");
234
+ const previous = readOperationSettlement(runDir, invocationId) ?? {};
235
+ const intent = readOperationIntent(runDir, invocationId) ?? {};
236
+ const requestedStatus = settlement.status ?? String(previous.status ?? "unknown_effect");
237
+ const previousStatus = String(previous.status ?? "");
238
+ const status = RESOLVED_OPERATION_STATUSES.has(previousStatus)
239
+ && ["adopted", "rejudge", "restarted"].includes(requestedStatus)
240
+ ? previousStatus
241
+ : requestedStatus;
242
+ const receipts = operationReceipts(invocationOrId, settlement.receipts, Array.isArray(previous.receipts) ? previous.receipts : []);
243
+ const result = settlement.result !== undefined
244
+ ? boundedSettlementResult(settlement.result)
245
+ : previous.result ?? null;
246
+ const record = {
247
+ ...previous,
248
+ schemaVersion: OPERATIONS_SCHEMA_VERSION,
249
+ operationId: invocationId,
250
+ invocationId,
251
+ runId: invocation?.runId ?? intent.runId ?? basename(runDir),
252
+ campaignId: invocation?.campaignId ?? intent.campaignId ?? null,
253
+ nodeId: intent.nodeId ?? null,
254
+ role: invocation?.role ?? intent.role ?? null,
255
+ status,
256
+ terminalOutcome: settlement.terminalOutcome ?? previous.terminalOutcome ?? status,
257
+ usage: settlement.usage !== undefined ? settlement.usage : previous.usage ?? null,
258
+ costUsd: settlement.costUsd !== undefined ? settlement.costUsd : previous.costUsd ?? null,
259
+ receipts,
260
+ nextState: settlement.nextState !== undefined ? settlement.nextState : previous.nextState ?? null,
261
+ structuredResult: settlement.structuredResult !== undefined ? settlement.structuredResult : previous.structuredResult ?? null,
262
+ result,
263
+ unknownEffect: settlement.unknownEffect ?? previous.unknownEffect ?? status === "unknown_effect",
264
+ classification: settlement.classification ?? previous.classification ?? null,
265
+ reason: settlement.reason ?? previous.reason ?? null,
266
+ error: settlement.error ?? previous.error ?? null,
267
+ settledAt: previous.settledAt ?? new Date().toISOString(),
268
+ };
269
+ writeJsonAtomic(operationSettlementPath(runDir, invocationId), record);
270
+ }
271
+ /**
272
+ * @param {string} runDir
273
+ * @param {string} invocationId
274
+ * @returns {boolean}
275
+ */
276
+ export function hasOperationIntent(runDir, invocationId) {
277
+ return existsSync(operationIntentPath(runDir, invocationId));
278
+ }
279
+ /**
280
+ * @param {string} runDir
281
+ * @param {string} invocationId
282
+ * @returns {boolean}
283
+ */
284
+ export function hasOperationSettlement(runDir, invocationId) {
285
+ return existsSync(operationSettlementPath(runDir, invocationId));
286
+ }
@@ -0,0 +1,187 @@
1
+ import {
2
+ closeSync,
3
+ ftruncateSync,
4
+ fsyncSync,
5
+ mkdirSync,
6
+ openSync,
7
+ readdirSync,
8
+ readFileSync,
9
+ readSync,
10
+ renameSync,
11
+ statSync,
12
+ unlinkSync,
13
+ writeSync,
14
+ } from "node:fs";
15
+ import { randomUUID } from "node:crypto";
16
+ import { dirname, join } from "node:path";
17
+ import { errorCode } from "../util.mjs";
18
+
19
+ const BOOTSTRAP_FILE = "bootstrap.json";
20
+ const JSONL_RECOVERY_TAIL_BYTES = 64 * 1024;
21
+
22
+ /**
23
+ * @param {string} path
24
+ * @returns {Record<string, unknown>}
25
+ */
26
+ export function readJson(path) {
27
+ return /** @type {Record<string, unknown>} */ (JSON.parse(readFileSync(path, "utf8")));
28
+ }
29
+
30
+ /**
31
+ * @param {string} path
32
+ * @param {unknown} value
33
+ */
34
+ export function writeJsonAtomic(path, value) {
35
+ writeTextAtomic(path, `${JSON.stringify(value, null, 2)}\n`);
36
+ }
37
+
38
+ /**
39
+ * @param {string} path
40
+ * @param {string} text
41
+ */
42
+ export function writeTextAtomic(path, text) {
43
+ mkdirSync(dirname(path), { recursive: true });
44
+ const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`;
45
+ const fd = openSync(temporary, "wx", 0o600);
46
+ let committed = false;
47
+ try {
48
+ try {
49
+ writeSync(fd, text, 0, "utf8");
50
+ fsyncSync(fd);
51
+ } finally {
52
+ closeSync(fd);
53
+ }
54
+ renameSync(temporary, path);
55
+ fsyncDirectory(dirname(path));
56
+ committed = true;
57
+ } catch (error) {
58
+ try { unlinkSync(temporary); } catch (cleanupError) {
59
+ if (errorCode(cleanupError) !== "ENOENT") throw cleanupError;
60
+ }
61
+ throw error;
62
+ } finally {
63
+ if (!committed) {
64
+ try { unlinkSync(temporary); } catch (error) {
65
+ if (errorCode(error) !== "ENOENT") throw error;
66
+ }
67
+ }
68
+ }
69
+ }
70
+
71
+ /**
72
+ * @param {string} path
73
+ * @param {unknown} value
74
+ */
75
+ export function appendJsonl(path, value) {
76
+ mkdirSync(dirname(path), { recursive: true });
77
+ recoverPartialJsonl(path);
78
+ const fd = openSync(path, "a", 0o600);
79
+ try {
80
+ writeSync(fd, Buffer.from(`${JSON.stringify(value)}\n`, "utf8"));
81
+ fsyncSync(fd);
82
+ } finally {
83
+ closeSync(fd);
84
+ }
85
+ }
86
+
87
+ /**
88
+ * @param {string} path
89
+ */
90
+ function recoverPartialJsonl(path) {
91
+ let size;
92
+ try {
93
+ size = statSync(path).size;
94
+ } catch (error) {
95
+ if (errorCode(error) === "ENOENT") return;
96
+ throw error;
97
+ }
98
+ if (size === 0) return;
99
+ const window = Math.min(size, JSONL_RECOVERY_TAIL_BYTES);
100
+ const fd = openSync(path, "r+");
101
+ try {
102
+ const buffer = Buffer.alloc(window);
103
+ readSync(fd, buffer, 0, window, size - window);
104
+ const tail = buffer.toString("utf8");
105
+ if (tail.endsWith("\n")) return;
106
+ const newline = tail.lastIndexOf("\n");
107
+ const completeBytes = newline >= 0
108
+ ? size - window + newline + 1
109
+ : size <= JSONL_RECOVERY_TAIL_BYTES ? 0 : size - window;
110
+ ftruncateSync(fd, completeBytes);
111
+ fsyncSync(fd);
112
+ } finally {
113
+ closeSync(fd);
114
+ }
115
+ }
116
+
117
+ /**
118
+ * @param {string} path
119
+ */
120
+ export function fsyncDirectory(path) {
121
+ try {
122
+ const fd = openSync(path, "r");
123
+ try {
124
+ fsyncSync(fd);
125
+ } finally {
126
+ closeSync(fd);
127
+ }
128
+ } catch (error) {
129
+ if (!(["EINVAL", "EPERM", "EISDIR"].includes(/** @type {string} */ (errorCode(error))))) throw error;
130
+ }
131
+ }
132
+
133
+ /**
134
+ * @param {string} runDir
135
+ * @returns {string}
136
+ */
137
+ export function bootstrapPath(runDir) {
138
+ return join(runDir, BOOTSTRAP_FILE);
139
+ }
140
+
141
+ /**
142
+ * @param {string} runDir
143
+ * @param {string} nonce
144
+ * @returns {string}
145
+ */
146
+ export function bootstrapAttemptPath(runDir, nonce) {
147
+ return join(runDir, `${BOOTSTRAP_FILE}.${nonce}`);
148
+ }
149
+
150
+ /**
151
+ * @param {string} runDir
152
+ * @param {string} nonce
153
+ * @returns {string}
154
+ */
155
+ export function bootstrapAckPath(runDir, nonce) {
156
+ return join(runDir, `${BOOTSTRAP_FILE}.${nonce}.ack`);
157
+ }
158
+
159
+ /**
160
+ * @param {string} runDir
161
+ * @param {string|null} keepNonce
162
+ */
163
+ export function cleanupBootstrapAttempts(runDir, keepNonce = null) {
164
+ const prefix = `${BOOTSTRAP_FILE}.`;
165
+ let names;
166
+ try { names = readdirSync(runDir); } catch (error) {
167
+ if (errorCode(error) === "ENOENT") return;
168
+ throw error;
169
+ }
170
+ for (const name of names) {
171
+ if (!name.startsWith(prefix) || !isBootstrapAttemptName(name)) continue;
172
+ if (keepNonce && (name === `${prefix}${keepNonce}` || name === `${prefix}${keepNonce}.ack`)) continue;
173
+ try { unlinkSync(join(runDir, name)); } catch (error) {
174
+ if (errorCode(error) !== "ENOENT") throw error;
175
+ }
176
+ }
177
+ }
178
+
179
+ /**
180
+ * @param {string} name
181
+ * @returns {boolean}
182
+ */
183
+ function isBootstrapAttemptName(name) {
184
+ const rest = name.slice(`${BOOTSTRAP_FILE}.`.length);
185
+ return /^[A-Za-z0-9-]{16,64}$/u.test(rest) || /^[A-Za-z0-9-]{16,64}\.ack$/u.test(rest);
186
+ }
187
+