@abelxiaoxing/cadence 1.5.3 → 1.7.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 (38) hide show
  1. package/README.md +61 -1
  2. package/config/plan-draft.example.json +3 -0
  3. package/config/plan-draft.multiple-tasks.example.json +30 -0
  4. package/package.json +1 -1
  5. package/prompts/abel-design.md +33 -3
  6. package/prompts/abel-implement.md +36 -4
  7. package/src/apply-transaction.ts +1 -9
  8. package/src/artifact-store.ts +1 -9
  9. package/src/change-verification.ts +758 -65
  10. package/src/contracts.ts +8 -0
  11. package/src/delivery-compiler.ts +452 -9
  12. package/src/design-control.ts +2 -2
  13. package/src/design-diagnostics.ts +6 -4
  14. package/src/directory-sync.ts +36 -0
  15. package/src/durable-contracts.ts +19 -3
  16. package/src/durable-workflow.ts +47 -38
  17. package/src/implement-continuation.ts +559 -0
  18. package/src/implement-graph.ts +114 -0
  19. package/src/implement-plan.ts +2 -0
  20. package/src/index.ts +179 -1
  21. package/src/operator-cli.mjs +12 -7
  22. package/src/package-state.ts +309 -0
  23. package/src/package-verification.ts +17 -1
  24. package/src/package-workflow.ts +219 -171
  25. package/src/phase-execution.ts +7 -1
  26. package/src/plan-draft-summary.ts +385 -7
  27. package/src/plan-draft.ts +9 -0
  28. package/src/run-store.ts +39 -15
  29. package/src/storage-schema.ts +14 -6
  30. package/src/subagent-activity.ts +3 -1
  31. package/src/verification-capability.ts +17 -5
  32. package/src/verification-environment.ts +20 -2
  33. package/src/workflow-policy.ts +296 -5
  34. package/src/workflow-recovery-policy.ts +2 -0
  35. package/src/workflow-state-machine.ts +400 -131
  36. package/src/workflow-status.ts +251 -59
  37. package/src/workspace-io-worker.mjs +48 -42
  38. package/src/workspace-store.ts +1 -9
@@ -8,10 +8,14 @@ import {
8
8
  type EngineRunRow,
9
9
  type EngineTaskRow,
10
10
  hash,
11
+ normalizeVerificationPrerequisite,
11
12
  normalizeWorkflowContextRequest,
12
13
  normalizeWorkflowVerificationStatus,
14
+ parentRecoveryStrategy,
13
15
  parseDeliveryDiagnostics,
14
16
  permitsPlanAmendment,
17
+ permitsPrerequisiteAmendment,
18
+ permitsRecoveryAmendment,
15
19
  type SafeAttemptDiagnostic,
16
20
  } from "./workflow-policy.ts";
17
21
 
@@ -60,6 +64,9 @@ export function projectWorkflowStatus({
60
64
  (right.queue_position ?? Number.MAX_SAFE_INTEGER) ||
61
65
  left.task_order - right.task_order,
62
66
  );
67
+ const activeRows = rows.filter((row) =>
68
+ ["approval-needed", "paused", "retryable"].includes(row.state),
69
+ );
63
70
  const firstPaused = firstPausedTask(rows);
64
71
  const contextRequest = firstPaused?.context_request_json
65
72
  ? normalizeWorkflowContextRequest(
@@ -71,6 +78,10 @@ export function projectWorkflowStatus({
71
78
  firstPaused?.state === "approval-needed"
72
79
  ? (firstPaused.pause_code ?? projection.pauseCode ?? "task-paused")
73
80
  : (projection.pauseCode ?? firstPaused?.pause_code ?? "task-paused");
81
+ const taskPauseSuperseded =
82
+ projection.state === "paused" &&
83
+ projection.pauseCode !== undefined &&
84
+ !activeRows.some((row) => row.pause_code === projection.pauseCode);
74
85
  const approval =
75
86
  projection.state === "approval-needed"
76
87
  ? approvalRequirement(pauseCode, contextRequest)
@@ -78,36 +89,75 @@ export function projectWorkflowStatus({
78
89
  const attemptDiagnostic = firstPaused
79
90
  ? attemptDiagnostics.get(firstPaused.task_id)
80
91
  : undefined;
81
- const recovery =
82
- rows
83
- .map((row) => attemptDiagnostics.get(row.task_id)?.recovery)
84
- .find((fact) => fact && fact.failures >= fact.feedback.maxAttempts) ??
85
- attemptDiagnostic?.recovery;
92
+ const recovery = taskPauseSuperseded
93
+ ? undefined
94
+ : attemptDiagnostic?.recovery;
86
95
  const recoveryExhausted =
87
96
  recovery && recovery.failures >= recovery.feedback.maxAttempts;
88
- const blockers = rows
89
- .filter((row) =>
90
- ["approval-needed", "paused", "retryable"].includes(row.state),
91
- )
92
- .map((row) => {
93
- const context = row.context_request_json
94
- ? normalizeWorkflowContextRequest(JSON.parse(row.context_request_json))
97
+ const selectedPrerequisite = normalizeVerificationPrerequisite(
98
+ taskPauseSuperseded ? undefined : attemptDiagnostic?.prerequisite,
99
+ );
100
+ const selectedRecoveryStrategy = parentRecoveryStrategy(
101
+ pauseCode,
102
+ taskPauseSuperseded ? undefined : attemptDiagnostic,
103
+ );
104
+ const selectedPauseActionable =
105
+ (projection.state === "approval-needed" && approval !== undefined) ||
106
+ (projection.state === "paused" &&
107
+ (permitsPlanAmendment(pauseCode) ||
108
+ selectedRecoveryStrategy !== undefined ||
109
+ permitsPrerequisiteAmendment(selectedPrerequisite)));
110
+ const blockers = activeRows.map((row) => {
111
+ const context = row.context_request_json
112
+ ? normalizeWorkflowContextRequest(JSON.parse(row.context_request_json))
113
+ : undefined;
114
+ const code = row.pause_code ?? "task-paused";
115
+ const diagnostic = attemptDiagnostics.get(row.task_id);
116
+ const prerequisite = normalizeVerificationPrerequisite(
117
+ diagnostic?.prerequisite,
118
+ );
119
+ const requirement =
120
+ row.state === "approval-needed"
121
+ ? approvalRequirement(code, context)
95
122
  : undefined;
96
- const code = row.pause_code ?? "task-paused";
97
- const requirement =
98
- row.state === "approval-needed"
99
- ? approvalRequirement(code, context)
100
- : undefined;
101
- return {
102
- id: hash(runId, row.task_id, code, canonicalJson(context ?? {})),
103
- taskId: row.task_id,
104
- phase: row.phase,
105
- code,
106
- kind: requirement ? "decision" : "execution",
107
- ...(requirement ?? {}),
108
- ...(context ? { contextRequest: context } : {}),
109
- };
110
- });
123
+ return {
124
+ id: hash(runId, row.task_id, code, canonicalJson(context ?? {})),
125
+ taskId: row.task_id,
126
+ phase: row.phase,
127
+ code,
128
+ kind: requirement ? "decision" : "execution",
129
+ ...(requirement ?? {}),
130
+ ...(context ? { contextRequest: context } : {}),
131
+ ...(prerequisite
132
+ ? {
133
+ prerequisite,
134
+ scope:
135
+ prerequisite.scope === "baseline-full-suite" ? "change" : "task",
136
+ minimumRecovery: permitsPrerequisiteAmendment(prerequisite)
137
+ ? "Revise the baseline contract to read safe inputs from the retained original revision, then recompile within the existing authority."
138
+ : prerequisite.cause === "capability"
139
+ ? "Restore the required runner or external capability; a changed environment identity permits a bounded check."
140
+ : "Restore trusted verification prerequisites; the retained failure does not authorize plan changes.",
141
+ }
142
+ : {}),
143
+ ...(diagnostic?.recovery
144
+ ? {
145
+ recovery: {
146
+ code: diagnostic.recovery.feedback.code,
147
+ attempts: diagnostic.recovery.failures,
148
+ maxAttempts: diagnostic.recovery.feedback.maxAttempts,
149
+ strategy: diagnostic.recovery.feedback.strategy,
150
+ ...(diagnostic.recovery.feedback.failureIdentities
151
+ ? {
152
+ failureIdentities:
153
+ diagnostic.recovery.feedback.failureIdentities,
154
+ }
155
+ : {}),
156
+ },
157
+ }
158
+ : {}),
159
+ };
160
+ });
111
161
  if (
112
162
  projection.state === "approval-needed" &&
113
163
  !blockers.some((item) => item.kind === "decision") &&
@@ -123,9 +173,19 @@ export function projectWorkflowStatus({
123
173
  });
124
174
  }
125
175
  const decisions = blockers.filter((item) => item.kind === "decision");
126
- const amendments = blockers.filter(
127
- (item) => item.kind === "decision" || permitsPlanAmendment(item.code),
128
- );
176
+ const amendments = !selectedPauseActionable
177
+ ? []
178
+ : blockers.filter(
179
+ (item) =>
180
+ item.kind === "decision" ||
181
+ permitsPlanAmendment(item.code) ||
182
+ permitsRecoveryAmendment(
183
+ item.code,
184
+ attemptDiagnostics.get(item.taskId),
185
+ ) ||
186
+ ("prerequisite" in item &&
187
+ permitsPrerequisiteAmendment(item.prerequisite)),
188
+ );
129
189
  if (
130
190
  projection.state === "paused" &&
131
191
  permitsPlanAmendment(pauseCode) &&
@@ -147,6 +207,16 @@ export function projectWorkflowStatus({
147
207
  remaining: 64 - amendmentUsed,
148
208
  exhausted: amendmentUsed >= 64,
149
209
  };
210
+ const budgetCode = [
211
+ "change-work-budget-exhausted",
212
+ "change-recovery-budget-exhausted",
213
+ ].includes(pauseCode)
214
+ ? pauseCode
215
+ : undefined;
216
+ const budgetExhausted = budgetCode !== undefined;
217
+ const hasWorkCapacity =
218
+ !budgetExhausted &&
219
+ (!resourceBudget || resourceBudget.used < resourceBudget.maximum);
150
220
  const decisionBatch =
151
221
  amendments.length &&
152
222
  pauseCode !== "operation-cancelled" &&
@@ -179,12 +249,139 @@ export function projectWorkflowStatus({
179
249
  ].sort(),
180
250
  }
181
251
  : undefined;
182
- const budgetCode = rows.find(
183
- (row) =>
184
- row.pause_code === "change-work-budget-exhausted" ||
185
- row.pause_code === "change-recovery-budget-exhausted",
186
- )?.pause_code;
187
- const budgetExhausted = budgetCode !== undefined;
252
+ const guidanceRow = taskPauseSuperseded ? undefined : firstPaused;
253
+ const guidanceDiagnostic = taskPauseSuperseded
254
+ ? undefined
255
+ : attemptDiagnostic;
256
+ const guidanceCode = pauseCode;
257
+ const guidanceStrategy = selectedRecoveryStrategy;
258
+ const failureSequence = recovery
259
+ ? failureSequences.get(recovery.key)
260
+ : undefined;
261
+ const recoveryGrant =
262
+ recoveryExhausted &&
263
+ guidanceStrategy !== undefined &&
264
+ projection.state === "paused" &&
265
+ hasWorkCapacity &&
266
+ resourceBudget &&
267
+ failureSequence !== undefined &&
268
+ Number.isSafeInteger(failureSequence) &&
269
+ failureSequence > 0
270
+ ? {
271
+ incidentKey: recovery.key,
272
+ failureSequence,
273
+ reason: "parent-directed-retry" as const,
274
+ }
275
+ : undefined;
276
+ const recoveryContinuation =
277
+ projection.state === "paused" &&
278
+ hasWorkCapacity &&
279
+ guidanceStrategy &&
280
+ (!recoveryExhausted || recoveryGrant)
281
+ ? {
282
+ owner: "parent" as const,
283
+ automatic: true,
284
+ kind: "inspect-recovery" as const,
285
+ reason: guidanceCode,
286
+ stage: projection.stage,
287
+ change: projection.change,
288
+ metadata: {
289
+ ...(guidanceRow
290
+ ? { taskId: guidanceRow.task_id, phase: guidanceRow.phase }
291
+ : {}),
292
+ diagnostic: {
293
+ code: guidanceCode,
294
+ strategy:
295
+ guidanceDiagnostic?.recovery?.feedback.strategy ??
296
+ guidanceStrategy,
297
+ ...(guidanceDiagnostic?.recovery
298
+ ? {
299
+ inspection: guidanceStrategy,
300
+ attempts: guidanceDiagnostic.recovery.failures,
301
+ maxAttempts:
302
+ guidanceDiagnostic.recovery.feedback.maxAttempts,
303
+ ...(guidanceDiagnostic.recovery.feedback.failureIdentities
304
+ ? {
305
+ failureIdentities:
306
+ guidanceDiagnostic.recovery.feedback
307
+ .failureIdentities,
308
+ }
309
+ : {}),
310
+ }
311
+ : {}),
312
+ ...(guidanceDiagnostic?.prerequisite
313
+ ? {
314
+ prerequisite: normalizeVerificationPrerequisite(
315
+ guidanceDiagnostic.prerequisite,
316
+ ),
317
+ }
318
+ : {}),
319
+ ...(guidanceRow?.route_id || engineRun?.route_id
320
+ ? {
321
+ route: {
322
+ routeId:
323
+ guidanceRow?.route_id ?? engineRun?.route_id ?? "",
324
+ ...((guidanceRow?.route_fingerprint ??
325
+ engineRun?.route_fingerprint)
326
+ ? {
327
+ routeFingerprint:
328
+ guidanceRow?.route_fingerprint ??
329
+ engineRun?.route_fingerprint,
330
+ }
331
+ : {}),
332
+ },
333
+ }
334
+ : {}),
335
+ },
336
+ ...(recoveryGrant
337
+ ? {
338
+ recommendation: {
339
+ kind: "bounded-additional-attempt" as const,
340
+ resume: { recovery: recoveryGrant },
341
+ },
342
+ }
343
+ : {}),
344
+ },
345
+ }
346
+ : undefined;
347
+ const amendmentContinuation =
348
+ decisionBatch && !amendmentBudget.exhausted && hasWorkCapacity
349
+ ? {
350
+ owner: "parent" as const,
351
+ automatic: true,
352
+ ...decisionBatch.continuation,
353
+ }
354
+ : undefined;
355
+ const interruptedContinuation =
356
+ projection.stage === "abel-implement" &&
357
+ projection.change &&
358
+ projection.pauseCode === "operation-interrupted" &&
359
+ legalControlCommands(projection.state).includes("resume") &&
360
+ (projection.state === "recovering" ||
361
+ (projection.state === "paused" && hasWorkCapacity))
362
+ ? {
363
+ owner: "parent" as const,
364
+ automatic: true,
365
+ command: "resume" as const,
366
+ kind:
367
+ projection.state === "recovering"
368
+ ? ("settle-apply-recovery" as const)
369
+ : ("resume-interrupted-operation" as const),
370
+ reason: "operation-interrupted" as const,
371
+ stage: projection.stage,
372
+ change: projection.change,
373
+ ...(guidanceRow?.pause_code === "operation-interrupted"
374
+ ? {
375
+ metadata: {
376
+ taskId: guidanceRow.task_id,
377
+ phase: guidanceRow.phase,
378
+ },
379
+ }
380
+ : {}),
381
+ }
382
+ : undefined;
383
+ const continuation =
384
+ interruptedContinuation ?? recoveryContinuation ?? amendmentContinuation;
188
385
  const currentRevision = projection.deliveryRevision ?? 0;
189
386
  return {
190
387
  runId: projection.runId,
@@ -220,17 +417,6 @@ export function projectWorkflowStatus({
220
417
  ? {
221
418
  decisionBatch,
222
419
  amendmentBudget,
223
- ...(!amendmentBudget.exhausted &&
224
- !budgetExhausted &&
225
- (!resourceBudget || resourceBudget.used < resourceBudget.maximum)
226
- ? {
227
- continuation: {
228
- owner: "parent",
229
- automatic: true,
230
- ...decisionBatch.continuation,
231
- },
232
- }
233
- : {}),
234
420
  }
235
421
  : {}),
236
422
  ...(recoveryExhausted
@@ -242,20 +428,11 @@ export function projectWorkflowStatus({
242
428
  attempts: recovery.failures,
243
429
  maxAttempts: recovery.feedback.maxAttempts,
244
430
  automaticRetryExhausted: true,
245
- ...(!budgetExhausted &&
246
- resourceBudget &&
247
- resourceBudget.used < resourceBudget.maximum
248
- ? {
249
- additionalAttempt: {
250
- incidentKey: recovery.key,
251
- failureSequence: failureSequences.get(recovery.key) ?? 0,
252
- reason: "parent-directed-retry",
253
- },
254
- }
255
- : {}),
431
+ ...(recoveryGrant ? { additionalAttempt: recoveryGrant } : {}),
256
432
  },
257
433
  }
258
434
  : {}),
435
+ ...(continuation ? { continuation } : {}),
259
436
  ...(projection.terminal ? { terminal: projection.terminal } : {}),
260
437
  ...(projection.pauseCode || firstPaused?.pause_code
261
438
  ? {
@@ -298,6 +475,10 @@ export function projectWorkflowStatus({
298
475
  receiptHash: "matching-ready-receipt",
299
476
  },
300
477
  },
478
+ }
479
+ : {}),
480
+ ...(approval && projection.change
481
+ ? {
301
482
  conditionalCommands: [
302
483
  {
303
484
  command: "resume",
@@ -310,7 +491,18 @@ export function projectWorkflowStatus({
310
491
  },
311
492
  ],
312
493
  }
313
- : {}),
494
+ : recoveryGrant && projection.change
495
+ ? {
496
+ conditionalCommands: [
497
+ {
498
+ command: "resume",
499
+ stage: "abel-implement",
500
+ change: projection.change,
501
+ requires: { recovery: recoveryGrant },
502
+ },
503
+ ],
504
+ }
505
+ : {}),
314
506
  tasks: rows.map((row) => ({
315
507
  taskId: row.task_id,
316
508
  state: row.state,
@@ -7,19 +7,41 @@ import { parentPort, workerData } from "node:worker_threads";
7
7
  import { createHash, randomUUID } from "node:crypto";
8
8
  import {
9
9
  chmodSync,
10
- closeSync,
11
- constants,
10
+ closeSync as closeSync2,
11
+ constants as constants2,
12
12
  copyFileSync,
13
- fsyncSync,
13
+ fsyncSync as fsyncSync2,
14
14
  lstatSync,
15
15
  mkdirSync,
16
- openSync,
16
+ openSync as openSync2,
17
17
  readFileSync,
18
18
  renameSync,
19
19
  unlinkSync,
20
20
  writeFileSync
21
21
  } from "node:fs";
22
22
  import path from "node:path";
23
+
24
+ // src/directory-sync.ts
25
+ import { closeSync, constants, fstatSync, fsyncSync, openSync } from "node:fs";
26
+ function syncDirectory(directory) {
27
+ const descriptor = openSync(directory, constants.O_RDONLY);
28
+ try {
29
+ try {
30
+ fsyncSync(descriptor);
31
+ return "synced";
32
+ } catch (error) {
33
+ const failure = error;
34
+ if (process.platform === "win32" && failure?.code === "EPERM" && failure.syscall === "fsync" && fstatSync(descriptor).isDirectory()) {
35
+ return "unsupported-directory-barrier";
36
+ }
37
+ throw error;
38
+ }
39
+ } finally {
40
+ closeSync(descriptor);
41
+ }
42
+ }
43
+
44
+ // src/artifact-store.ts
23
45
  var SHA256 = /^[a-f0-9]{64}$/u;
24
46
  function digest(bytes) {
25
47
  return createHash("sha256").update(bytes).digest("hex");
@@ -55,24 +77,16 @@ function ensureDirectory(directory) {
55
77
  }
56
78
  chmodSync(directory, 448);
57
79
  }
58
- function syncDirectory(directory) {
59
- const descriptor = openSync(directory, constants.O_RDONLY);
60
- try {
61
- fsyncSync(descriptor);
62
- } finally {
63
- closeSync(descriptor);
64
- }
65
- }
66
80
  function writeAtomic(target, bytes) {
67
81
  const directory = path.dirname(target);
68
82
  ensureDirectory(directory);
69
83
  const temporary = path.join(directory, `.${path.basename(target)}.${process.pid}.${randomUUID()}.tmp`);
70
- const descriptor = openSync(temporary, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY, 384);
84
+ const descriptor = openSync2(temporary, constants2.O_CREAT | constants2.O_EXCL | constants2.O_WRONLY, 384);
71
85
  try {
72
86
  writeFileSync(descriptor, bytes);
73
- fsyncSync(descriptor);
87
+ fsyncSync2(descriptor);
74
88
  } finally {
75
- closeSync(descriptor);
89
+ closeSync2(descriptor);
76
90
  }
77
91
  try {
78
92
  renameSync(temporary, target);
@@ -148,7 +162,7 @@ class ArtifactStore {
148
162
  const stat = lstatSync(source, { throwIfNoEntry: false });
149
163
  if (!stat?.isFile() || stat.isSymbolicLink())
150
164
  throw new Error("artifact-unavailable");
151
- copyFileSync(source, destination, constants.COPYFILE_EXCL | constants.COPYFILE_FICLONE);
165
+ copyFileSync(source, destination, constants2.COPYFILE_EXCL | constants2.COPYFILE_FICLONE);
152
166
  const bytes = readFileSync(destination);
153
167
  if (digest(bytes) !== hash)
154
168
  throw new Error("artifact-integrity-invalid");
@@ -199,7 +213,7 @@ class ArtifactStore {
199
213
 
200
214
  // src/verification-environment-io.ts
201
215
  import {
202
- constants as constants2,
216
+ constants as constants3,
203
217
  cpSync,
204
218
  lstatSync as lstatSync2,
205
219
  mkdirSync as mkdirSync2,
@@ -329,7 +343,7 @@ function prepareVerificationEnvironmentIo(root, dependencyOwner, runnerBindings,
329
343
  checkCancelled();
330
344
  return true;
331
345
  },
332
- mode: constants2.COPYFILE_FICLONE,
346
+ mode: constants3.COPYFILE_FICLONE,
333
347
  verbatimSymlinks: true
334
348
  });
335
349
  };
@@ -410,11 +424,11 @@ function removeEntry(file, checkCancelled) {
410
424
  // src/verification-identity.ts
411
425
  import { createHash as createHash2 } from "node:crypto";
412
426
  import {
413
- closeSync as closeSync2,
414
- constants as constants3,
415
- fstatSync,
427
+ closeSync as closeSync3,
428
+ constants as constants4,
429
+ fstatSync as fstatSync2,
416
430
  lstatSync as lstatSync3,
417
- openSync as openSync2,
431
+ openSync as openSync3,
418
432
  readdirSync as readdirSync2,
419
433
  readlinkSync,
420
434
  readSync
@@ -451,9 +465,9 @@ function verificationEnvironmentDigest(roots, checkCancelled = () => {}) {
451
465
  }
452
466
  if (!stat.isFile())
453
467
  throw new Error("verification-environment-unsafe");
454
- const fd = openSync2(file, constants3.O_RDONLY | constants3.O_NOFOLLOW);
468
+ const fd = openSync3(file, constants4.O_RDONLY | constants4.O_NOFOLLOW);
455
469
  try {
456
- const before = fstatSync(fd);
470
+ const before = fstatSync2(fd);
457
471
  if (before.ino !== stat.ino || before.dev !== stat.dev)
458
472
  throw new Error("verification-environment-changed");
459
473
  const content = createHash2("sha256");
@@ -467,12 +481,12 @@ function verificationEnvironmentDigest(roots, checkCancelled = () => {}) {
467
481
  throw new Error("verification-environment-limit");
468
482
  content.update(buffer.subarray(0, count));
469
483
  }
470
- const after = fstatSync(fd);
484
+ const after = fstatSync2(fd);
471
485
  if (before.size !== after.size || before.mtimeMs !== after.mtimeMs || before.ctimeMs !== after.ctimeMs)
472
486
  throw new Error("verification-environment-changed");
473
487
  digest2.update(JSON.stringify(["file", content.digest("hex")]));
474
488
  } finally {
475
- closeSync2(fd);
489
+ closeSync3(fd);
476
490
  }
477
491
  };
478
492
  for (const root of [...new Set(roots)].sort())
@@ -485,12 +499,12 @@ import { execFile, execFileSync } from "node:child_process";
485
499
  import { createHash as createHash3, randomUUID as randomUUID2 } from "node:crypto";
486
500
  import {
487
501
  chmodSync as chmodSync2,
488
- closeSync as closeSync3,
489
- constants as constants4,
490
- fsyncSync as fsyncSync2,
502
+ closeSync as closeSync4,
503
+ constants as constants5,
504
+ fsyncSync as fsyncSync3,
491
505
  lstatSync as lstatSync5,
492
506
  mkdirSync as mkdirSync3,
493
- openSync as openSync3,
507
+ openSync as openSync4,
494
508
  readFileSync as readFileSync2,
495
509
  renameSync as renameSync2,
496
510
  unlinkSync as unlinkSync2,
@@ -765,29 +779,21 @@ function ensureDirectory2(directory) {
765
779
  }
766
780
  chmodSync2(directory, 448);
767
781
  }
768
- function syncDirectory2(directory) {
769
- const descriptor = openSync3(directory, constants4.O_RDONLY);
770
- try {
771
- fsyncSync2(descriptor);
772
- } finally {
773
- closeSync3(descriptor);
774
- }
775
- }
776
782
  function writeAtomic2(target, bytes) {
777
783
  const directory = path5.dirname(target);
778
784
  ensureDirectory2(directory);
779
785
  const temporary = path5.join(directory, `.${path5.basename(target)}.${process.pid}.${randomUUID2()}.tmp`);
780
- const descriptor = openSync3(temporary, constants4.O_CREAT | constants4.O_EXCL | constants4.O_WRONLY, 384);
786
+ const descriptor = openSync4(temporary, constants5.O_CREAT | constants5.O_EXCL | constants5.O_WRONLY, 384);
781
787
  try {
782
788
  writeFileSync3(descriptor, bytes);
783
- fsyncSync2(descriptor);
789
+ fsyncSync3(descriptor);
784
790
  } finally {
785
- closeSync3(descriptor);
791
+ closeSync4(descriptor);
786
792
  }
787
793
  try {
788
794
  renameSync2(temporary, target);
789
795
  chmodSync2(target, 384);
790
- syncDirectory2(directory);
796
+ syncDirectory(directory);
791
797
  } catch (error) {
792
798
  try {
793
799
  unlinkSync2(temporary);
@@ -18,6 +18,7 @@ import { promisify } from "node:util";
18
18
  import type { ArtifactStore } from "./artifact-store.ts";
19
19
  import { compareCanonicalStrings } from "./canonical.ts";
20
20
  import { isValidRelativePath } from "./contracts.ts";
21
+ import { syncDirectory } from "./directory-sync.ts";
21
22
  import { observeSafePath } from "./safe-path.ts";
22
23
  import { runWorkspaceIo, type WorkspaceIoMetrics } from "./workspace-io.ts";
23
24
 
@@ -127,15 +128,6 @@ function ensureDirectory(directory: string): void {
127
128
  chmodSync(directory, 0o700);
128
129
  }
129
130
 
130
- function syncDirectory(directory: string): void {
131
- const descriptor = openSync(directory, constants.O_RDONLY);
132
- try {
133
- fsyncSync(descriptor);
134
- } finally {
135
- closeSync(descriptor);
136
- }
137
- }
138
-
139
131
  function writeAtomic(target: string, bytes: Uint8Array): void {
140
132
  const directory = path.dirname(target);
141
133
  ensureDirectory(directory);