codex-workflow-v2 2.0.0-beta.2 → 2.0.0-beta.4

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/README.md +35 -2
  2. package/dist/src/alpha6/adoption.js +13 -3
  3. package/dist/src/alpha6/adoption.js.map +1 -1
  4. package/dist/src/alpha6/handoff.d.ts +21 -0
  5. package/dist/src/alpha6/handoff.js +100 -19
  6. package/dist/src/alpha6/handoff.js.map +1 -1
  7. package/dist/src/alpha6/milestone.d.ts +2 -0
  8. package/dist/src/alpha6/milestone.js +15 -0
  9. package/dist/src/alpha6/milestone.js.map +1 -1
  10. package/dist/src/alpha6/plan-risk.d.ts +1 -0
  11. package/dist/src/alpha6/plan-risk.js +6 -0
  12. package/dist/src/alpha6/plan-risk.js.map +1 -1
  13. package/dist/src/cli.js +188 -65
  14. package/dist/src/cli.js.map +1 -1
  15. package/dist/src/contracts.d.ts +111 -5
  16. package/dist/src/contracts.js +1 -1
  17. package/dist/src/credential-output.d.ts +10 -0
  18. package/dist/src/credential-output.js +73 -0
  19. package/dist/src/credential-output.js.map +1 -0
  20. package/dist/src/errors.d.ts +1 -1
  21. package/dist/src/errors.js.map +1 -1
  22. package/dist/src/index.d.ts +1 -1
  23. package/dist/src/index.js +1 -1
  24. package/dist/src/index.js.map +1 -1
  25. package/dist/src/lifecycle/canonical-hash.d.ts +6 -0
  26. package/dist/src/lifecycle/canonical-hash.js +24 -0
  27. package/dist/src/lifecycle/canonical-hash.js.map +1 -0
  28. package/dist/src/lifecycle/catalog.d.ts +26 -0
  29. package/dist/src/lifecycle/catalog.js +208 -0
  30. package/dist/src/lifecycle/catalog.js.map +1 -0
  31. package/dist/src/lifecycle/compiler-inspection.d.ts +48 -0
  32. package/dist/src/lifecycle/compiler-inspection.js +29 -0
  33. package/dist/src/lifecycle/compiler-inspection.js.map +1 -0
  34. package/dist/src/lifecycle/core-static-readiness.d.ts +6 -0
  35. package/dist/src/lifecycle/core-static-readiness.js +130 -0
  36. package/dist/src/lifecycle/core-static-readiness.js.map +1 -0
  37. package/dist/src/lifecycle/corrective-replan-authority.d.ts +53 -0
  38. package/dist/src/lifecycle/corrective-replan-authority.js +139 -0
  39. package/dist/src/lifecycle/corrective-replan-authority.js.map +1 -0
  40. package/dist/src/lifecycle/corrective-replan-binding-manifest.d.ts +21 -0
  41. package/dist/src/lifecycle/corrective-replan-binding-manifest.js +35 -0
  42. package/dist/src/lifecycle/corrective-replan-binding-manifest.js.map +1 -0
  43. package/dist/src/lifecycle/corrective-replan-credential-core.d.ts +86 -0
  44. package/dist/src/lifecycle/corrective-replan-credential-core.js +534 -0
  45. package/dist/src/lifecycle/corrective-replan-credential-core.js.map +1 -0
  46. package/dist/src/lifecycle/corrective-replan-credential-schema.d.ts +17 -0
  47. package/dist/src/lifecycle/corrective-replan-credential-schema.js +44 -0
  48. package/dist/src/lifecycle/corrective-replan-credential-schema.js.map +1 -0
  49. package/dist/src/lifecycle/corrective-replan-credentials.d.ts +47 -0
  50. package/dist/src/lifecycle/corrective-replan-credentials.js +103 -0
  51. package/dist/src/lifecycle/corrective-replan-credentials.js.map +1 -0
  52. package/dist/src/lifecycle/corrective-replan.d.ts +64 -0
  53. package/dist/src/lifecycle/corrective-replan.js +548 -0
  54. package/dist/src/lifecycle/corrective-replan.js.map +1 -0
  55. package/dist/src/lifecycle/evaluator.d.ts +2 -0
  56. package/dist/src/lifecycle/evaluator.js +35 -0
  57. package/dist/src/lifecycle/evaluator.js.map +1 -0
  58. package/dist/src/lifecycle/fingerprint.d.ts +31 -0
  59. package/dist/src/lifecycle/fingerprint.js +388 -0
  60. package/dist/src/lifecycle/fingerprint.js.map +1 -0
  61. package/dist/src/lifecycle/immutable.d.ts +1 -0
  62. package/dist/src/lifecycle/immutable.js +10 -0
  63. package/dist/src/lifecycle/immutable.js.map +1 -0
  64. package/dist/src/lifecycle/implementation-table.d.ts +29 -0
  65. package/dist/src/lifecycle/implementation-table.js +87 -0
  66. package/dist/src/lifecycle/implementation-table.js.map +1 -0
  67. package/dist/src/lifecycle/index.d.ts +4 -0
  68. package/dist/src/lifecycle/index.js +5 -0
  69. package/dist/src/lifecycle/index.js.map +1 -0
  70. package/dist/src/lifecycle/schema-artifact.d.ts +12 -0
  71. package/dist/src/lifecycle/schema-artifact.js +214 -0
  72. package/dist/src/lifecycle/schema-artifact.js.map +1 -0
  73. package/dist/src/lifecycle/semantic-registry.d.ts +6 -0
  74. package/dist/src/lifecycle/semantic-registry.js +489 -0
  75. package/dist/src/lifecycle/semantic-registry.js.map +1 -0
  76. package/dist/src/lifecycle/types.d.ts +718 -0
  77. package/dist/src/lifecycle/types.js +4 -0
  78. package/dist/src/lifecycle/types.js.map +1 -0
  79. package/dist/src/observation.d.ts +20 -0
  80. package/dist/src/observation.js +171 -0
  81. package/dist/src/observation.js.map +1 -0
  82. package/dist/src/state/corrective-replan-executor.d.ts +64 -0
  83. package/dist/src/state/corrective-replan-executor.js +598 -0
  84. package/dist/src/state/corrective-replan-executor.js.map +1 -0
  85. package/dist/src/state/corrective-replan-public-schema.d.ts +3 -0
  86. package/dist/src/state/corrective-replan-public-schema.js +43 -0
  87. package/dist/src/state/corrective-replan-public-schema.js.map +1 -0
  88. package/dist/src/state/corrective-replan-public.d.ts +95 -0
  89. package/dist/src/state/corrective-replan-public.js +618 -0
  90. package/dist/src/state/corrective-replan-public.js.map +1 -0
  91. package/dist/src/state/corrective-replan-transaction.d.ts +158 -0
  92. package/dist/src/state/corrective-replan-transaction.js +723 -0
  93. package/dist/src/state/corrective-replan-transaction.js.map +1 -0
  94. package/dist/src/state/corrective-yield-executor.d.ts +31 -0
  95. package/dist/src/state/corrective-yield-executor.js +236 -0
  96. package/dist/src/state/corrective-yield-executor.js.map +1 -0
  97. package/dist/src/state/corrective-yield-transaction.d.ts +97 -0
  98. package/dist/src/state/corrective-yield-transaction.js +591 -0
  99. package/dist/src/state/corrective-yield-transaction.js.map +1 -0
  100. package/dist/src/state/lock.d.ts +75 -3
  101. package/dist/src/state/lock.js +582 -30
  102. package/dist/src/state/lock.js.map +1 -1
  103. package/dist/src/state/store.d.ts +3 -0
  104. package/dist/src/state/store.js +65 -12
  105. package/dist/src/state/store.js.map +1 -1
  106. package/dist/src/transition-core.d.ts +67 -0
  107. package/dist/src/transition-core.js +318 -0
  108. package/dist/src/transition-core.js.map +1 -0
  109. package/dist/src/version.d.ts +1 -1
  110. package/dist/src/version.js +1 -1
  111. package/dist/src/workflow.d.ts +34 -22
  112. package/dist/src/workflow.js +486 -141
  113. package/dist/src/workflow.js.map +1 -1
  114. package/docs/lifecycle/state-machine-stabilization.md +641 -0
  115. package/docs/release.md +4 -2
  116. package/docs/updating-existing-project.md +9 -0
  117. package/package.json +4 -1
  118. package/schemas/adoption-posture-event.schema.json +1 -1
  119. package/schemas/context-envelope.schema.json +1 -1
  120. package/schemas/corrective-replan-credentials.private.schema.json +98 -0
  121. package/schemas/corrective-replan-execution-event.private.schema.json +53 -0
  122. package/schemas/corrective-replan-public.schema.json +142 -0
  123. package/schemas/corrective-replan-task-transaction.private.schema.json +151 -0
  124. package/schemas/corrective-yield-task-transaction.private.schema.json +98 -0
  125. package/schemas/remediation-event.schema.json +11 -1
  126. package/schemas/task-handoff-event.schema.json +1 -1
  127. package/schemas/transition-definition.schema.json +2609 -0
  128. package/schemas/transition-payloads.schema.json +140 -0
@@ -1,21 +1,30 @@
1
- import { closeSync, existsSync, openSync, readdirSync, unlinkSync, writeFileSync } from 'node:fs';
1
+ import { closeSync, constants, fstatSync, lstatSync, openSync, readFileSync, readdirSync, unlinkSync, writeFileSync, } from 'node:fs';
2
2
  import os from 'node:os';
3
3
  import path from 'node:path';
4
- import { randomUUID } from 'node:crypto';
5
- import { STATE_SCHEMA_VERSION } from '../contracts.js';
4
+ import { createHash, randomUUID } from 'node:crypto';
5
+ import { STATE_SCHEMA_VERSION, } from '../contracts.js';
6
6
  import { WorkflowError } from '../errors.js';
7
- import { ensureDirectory, readJson, writeJsonAtomic } from '../fs-utils.js';
7
+ import { ensureDirectory, writeJsonAtomic } from '../fs-utils.js';
8
8
  const DEFAULT_LEASE_MS = 30 * 60 * 1000;
9
+ const LOCK_ENTITY_PATTERN = /^(TASK|MS)-[0-9A-HJKMNP-TV-Z]{26}$/;
10
+ const LOCK_FILE_PATTERN = /^(TASK|MS)-[0-9A-HJKMNP-TV-Z]{26}\.json$/;
11
+ const coreHandles = new WeakMap();
12
+ const writerHandles = new WeakMap();
9
13
  export class WriterLockManager {
10
14
  locksRoot;
11
15
  now;
12
- constructor(locksRoot, now = () => new Date()) {
16
+ trustedRoot;
17
+ constructor(locksRoot, now = () => new Date(), trustedRoot) {
13
18
  this.locksRoot = locksRoot;
14
19
  this.now = now;
20
+ this.trustedRoot = trustedRoot;
21
+ assertLockRootPath(locksRoot, trustedRoot);
15
22
  ensureDirectory(locksRoot);
23
+ assertLockRootPath(locksRoot, trustedRoot);
16
24
  }
17
25
  acquire(entityId, owner, leaseMs = DEFAULT_LEASE_MS) {
18
26
  const file = this.file(entityId);
27
+ assertLockRootPath(this.locksRoot, this.trustedRoot);
19
28
  const timestamp = this.now();
20
29
  const lease = {
21
30
  schemaVersion: STATE_SCHEMA_VERSION,
@@ -28,79 +37,622 @@ export class WriterLockManager {
28
37
  heartbeatAt: timestamp.toISOString(),
29
38
  expiresAt: new Date(timestamp.getTime() + leaseMs).toISOString(),
30
39
  };
40
+ let descriptor = null;
31
41
  try {
32
- const descriptor = openSync(file, 'wx', 0o600);
42
+ descriptor = openSync(file, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | constants.O_NOFOLLOW, 0o600);
33
43
  writeFileSync(descriptor, `${JSON.stringify(lease, null, 2)}\n`, 'utf8');
34
44
  closeSync(descriptor);
45
+ descriptor = null;
35
46
  return lease;
36
47
  }
37
48
  catch (error) {
38
- if (existsSync(file)) {
39
- const current = readJson(file);
49
+ if (descriptor !== null)
50
+ closeSync(descriptor);
51
+ if (lstatIfPresent(file)) {
52
+ const snapshot = readLockSnapshot(this.locksRoot, entityId, this.trustedRoot);
53
+ if (isCoreTaskOperationMutex(snapshot.record)) {
54
+ throw new WorkflowError('LOCKED', `${entityId} already has a Core Task operation`, {
55
+ owner: snapshot.record.owner,
56
+ operationId: snapshot.record.operationId,
57
+ transitionId: snapshot.record.transitionId,
58
+ expiresAt: snapshot.record.expiresAt,
59
+ stale: lockExpiry(snapshot.record, file) <= timestamp.getTime(),
60
+ });
61
+ }
62
+ const current = snapshot.record;
40
63
  throw new WorkflowError('LOCKED', `${entityId} already has a writer`, {
41
64
  owner: current.owner,
42
65
  pid: current.pid,
43
66
  hostname: current.hostname,
44
67
  heartbeatAt: current.heartbeatAt,
45
68
  expiresAt: current.expiresAt,
46
- stale: Date.parse(current.expiresAt) <= timestamp.getTime(),
69
+ stale: leaseExpiry(current, file) <= timestamp.getTime(),
47
70
  });
48
71
  }
49
72
  throw error;
50
73
  }
51
74
  }
52
75
  heartbeat(entityId, token, leaseMs = DEFAULT_LEASE_MS) {
53
- const current = this.requireToken(entityId, token);
54
76
  const timestamp = this.now();
77
+ const current = this.requireConsumableToken(entityId, token, timestamp);
55
78
  const updated = {
56
- ...current,
79
+ ...current.lease,
57
80
  heartbeatAt: timestamp.toISOString(),
58
81
  expiresAt: new Date(timestamp.getTime() + leaseMs).toISOString(),
59
82
  };
83
+ assertSameLeaseIdentity(this.file(entityId), current.identity);
60
84
  writeJsonAtomic(this.file(entityId), updated);
61
85
  return updated;
62
86
  }
63
87
  release(entityId, token) {
64
- this.requireToken(entityId, token);
65
- unlinkSync(this.file(entityId));
88
+ const current = this.requireConsumableToken(entityId, token, this.now());
89
+ const file = this.file(entityId);
90
+ assertSameLeaseIdentity(file, current.identity);
91
+ unlinkSync(file);
92
+ }
93
+ prove(entityId, token) {
94
+ const snapshot = this.requireConsumableToken(entityId, token, this.now());
95
+ const handle = Object.freeze(Object.create(null));
96
+ writerHandles.set(handle, {
97
+ locksRoot: path.resolve(this.locksRoot),
98
+ ...(this.trustedRoot === undefined ? {} : { trustedRoot: path.resolve(this.trustedRoot) }),
99
+ snapshot,
100
+ });
101
+ return handle;
102
+ }
103
+ proofBinding(handle) {
104
+ const snapshot = assertWriterLeaseHandle(this.locksRoot, this.trustedRoot, handle, this.now());
105
+ return Object.freeze({
106
+ entityId: snapshot.lease.entityId,
107
+ owner: snapshot.lease.owner,
108
+ expiresAt: snapshot.lease.expiresAt,
109
+ tokenSha256: createHash('sha256').update(snapshot.lease.token).digest('hex'),
110
+ contentSha256: createHash('sha256').update(snapshot.content).digest('hex'),
111
+ });
66
112
  }
67
113
  inspect(entityId) {
68
- const file = this.file(entityId);
69
- return existsSync(file) ? readJson(file) : null;
114
+ return inspectWriterLease(this.locksRoot, entityId, this.trustedRoot);
70
115
  }
71
116
  list() {
72
- return readdirSync(this.locksRoot, { withFileTypes: true })
73
- .filter((entry) => entry.isFile() && /^(TASK|MS)-[0-9A-HJKMNP-TV-Z]{26}\.json$/.test(entry.name))
74
- .map((entry) => readJson(path.join(this.locksRoot, entry.name)))
75
- .sort((left, right) => left.entityId.localeCompare(right.entityId));
117
+ return listWriterLeases(this.locksRoot, this.trustedRoot);
76
118
  }
77
119
  repairStale(entityId) {
78
120
  const file = this.file(entityId);
79
- if (!existsSync(file))
121
+ const current = readLeaseSnapshotOrNull(this.locksRoot, entityId, this.trustedRoot);
122
+ if (!current)
80
123
  throw new WorkflowError('NOT_FOUND', `No writer lock for ${entityId}`);
81
- const current = readJson(file);
82
- if (Date.parse(current.expiresAt) > this.now().getTime()) {
124
+ const expiresAt = leaseExpiry(current.lease, file);
125
+ if (expiresAt > this.now().getTime()) {
83
126
  throw new WorkflowError('TRANSITION_BLOCKED', `Writer lock is not stale for ${entityId}`, {
84
- expiresAt: current.expiresAt,
127
+ expiresAt: current.lease.expiresAt,
85
128
  });
86
129
  }
130
+ assertSameLeaseIdentity(file, current.identity);
87
131
  unlinkSync(file);
88
- return current;
132
+ return current.lease;
89
133
  }
90
- requireToken(entityId, token) {
91
- const current = this.inspect(entityId);
134
+ requireConsumableToken(entityId, token, now) {
135
+ const current = readLeaseSnapshotOrNull(this.locksRoot, entityId, this.trustedRoot);
92
136
  if (!current)
93
137
  throw new WorkflowError('NOT_FOUND', `No writer lock for ${entityId}`);
94
- if (current.token !== token) {
138
+ if (current.lease.token !== token) {
95
139
  throw new WorkflowError('LOCK_TOKEN_INVALID', `Writer token does not own ${entityId}`);
96
140
  }
141
+ if (leaseExpiry(current.lease, this.file(entityId)) <= now.getTime()) {
142
+ throw new WorkflowError('TRANSITION_BLOCKED', `Writer lock has expired for ${entityId}; explicit stale-lock repair is required.`, {
143
+ entityId,
144
+ expiresAt: current.lease.expiresAt,
145
+ requiredAction: 'locks repair',
146
+ });
147
+ }
97
148
  return current;
98
149
  }
99
150
  file(entityId) {
100
- if (!/^(TASK|MS)-[0-9A-HJKMNP-TV-Z]{26}$/.test(entityId)) {
101
- throw new WorkflowError('INVALID_ARGUMENT', `Invalid lock entity: ${entityId}`);
102
- }
151
+ assertLockEntity(entityId);
103
152
  return path.join(this.locksRoot, `${entityId}.json`);
104
153
  }
105
154
  }
155
+ export function listWriterLeases(locksRoot, trustedRoot) {
156
+ if (!lstatIfPresent(locksRoot))
157
+ return [];
158
+ assertLockRootPath(locksRoot, trustedRoot);
159
+ return readdirSync(locksRoot, { withFileTypes: true })
160
+ .filter((entry) => LOCK_FILE_PATTERN.test(entry.name))
161
+ .flatMap((entry) => {
162
+ const snapshot = readLockSnapshot(locksRoot, entry.name.slice(0, -'.json'.length), trustedRoot);
163
+ return isCoreTaskOperationMutex(snapshot.record) ? [] : [snapshot.record];
164
+ })
165
+ .sort((left, right) => left.entityId.localeCompare(right.entityId));
166
+ }
167
+ export function inspectWriterLease(locksRoot, entityId, trustedRoot) {
168
+ assertLockEntity(entityId);
169
+ if (!lstatIfPresent(locksRoot))
170
+ return null;
171
+ const file = path.join(locksRoot, `${entityId}.json`);
172
+ if (!lstatIfPresent(file))
173
+ return null;
174
+ const record = readLockSnapshot(locksRoot, entityId, trustedRoot).record;
175
+ return isCoreTaskOperationMutex(record) ? null : record;
176
+ }
177
+ export function listCoreTaskOperationMutexes(locksRoot, trustedRoot) {
178
+ if (!lstatIfPresent(locksRoot))
179
+ return [];
180
+ assertLockRootPath(locksRoot, trustedRoot);
181
+ return readdirSync(locksRoot, { withFileTypes: true })
182
+ .filter((entry) => LOCK_FILE_PATTERN.test(entry.name))
183
+ .flatMap((entry) => {
184
+ const record = readLockSnapshot(locksRoot, entry.name.slice(0, -'.json'.length), trustedRoot).record;
185
+ return isCoreTaskOperationMutex(record) ? [record] : [];
186
+ })
187
+ .sort((left, right) => left.entityId.localeCompare(right.entityId));
188
+ }
189
+ export function toPublicWriterLease(lease, now) {
190
+ const expiresAt = leaseExpiry(lease, '<writer-lease>');
191
+ return {
192
+ schemaVersion: lease.schemaVersion,
193
+ entityId: lease.entityId,
194
+ owner: lease.owner,
195
+ pid: lease.pid,
196
+ hostname: lease.hostname,
197
+ acquiredAt: lease.acquiredAt,
198
+ heartbeatAt: lease.heartbeatAt,
199
+ expiresAt: lease.expiresAt,
200
+ stale: expiresAt <= now.getTime(),
201
+ };
202
+ }
203
+ function readLeaseSnapshotOrNull(locksRoot, entityId, trustedRoot) {
204
+ assertLockEntity(entityId);
205
+ if (!lstatIfPresent(locksRoot))
206
+ return null;
207
+ assertLockRootPath(locksRoot, trustedRoot);
208
+ const file = path.join(locksRoot, `${entityId}.json`);
209
+ if (!lstatIfPresent(file))
210
+ return null;
211
+ return readLeaseSnapshot(locksRoot, entityId, trustedRoot);
212
+ }
213
+ function readLeaseSnapshot(locksRoot, entityId, trustedRoot) {
214
+ const snapshot = readLockSnapshot(locksRoot, entityId, trustedRoot);
215
+ if (isCoreTaskOperationMutex(snapshot.record)) {
216
+ throw new WorkflowError('LOCK_TOKEN_INVALID', `Writer token does not own ${entityId}`);
217
+ }
218
+ return { lease: snapshot.record, identity: snapshot.identity, content: snapshot.content };
219
+ }
220
+ function readLockSnapshot(locksRoot, entityId, trustedRoot) {
221
+ assertLockEntity(entityId);
222
+ assertLockRootPath(locksRoot, trustedRoot);
223
+ const file = path.join(locksRoot, `${entityId}.json`);
224
+ const boundary = trustedBoundary(locksRoot, trustedRoot);
225
+ assertNoSymlinkBelowBoundary(file, boundary, 'Writer lease path');
226
+ const before = lstatIfPresent(file);
227
+ if (!before)
228
+ throw new WorkflowError('NOT_FOUND', `No writer lock for ${entityId}`);
229
+ if (before.isSymbolicLink() || !before.isFile()) {
230
+ throw new WorkflowError('STATE_CORRUPT', `Writer lease must be a regular file: ${file}`);
231
+ }
232
+ let descriptor = null;
233
+ try {
234
+ descriptor = openSync(file, constants.O_RDONLY | constants.O_NOFOLLOW);
235
+ const opened = fstatSync(descriptor);
236
+ if (!sameIdentity(before, opened) || !opened.isFile()) {
237
+ throw new WorkflowError('STATE_CONFLICT', `Writer lease changed while opening: ${file}`);
238
+ }
239
+ const content = readFileSync(descriptor, 'utf8');
240
+ let record;
241
+ try {
242
+ record = JSON.parse(content);
243
+ }
244
+ catch (error) {
245
+ throw new WorkflowError('STATE_CORRUPT', `Cannot read JSON state: ${file}`, {
246
+ cause: error instanceof Error ? error.message : String(error),
247
+ });
248
+ }
249
+ validateLockRecord(record, entityId, file);
250
+ const after = lstatIfPresent(file);
251
+ if (!after || !sameIdentity(opened, after)) {
252
+ throw new WorkflowError('STATE_CONFLICT', `Writer lease changed while reading: ${file}`);
253
+ }
254
+ return { record, identity: { dev: opened.dev, ino: opened.ino }, content };
255
+ }
256
+ finally {
257
+ if (descriptor !== null)
258
+ closeSync(descriptor);
259
+ }
260
+ }
261
+ export class CoreTaskOperationLockManager {
262
+ locksRoot;
263
+ now;
264
+ trustedRoot;
265
+ constructor(locksRoot, now = () => new Date(), trustedRoot) {
266
+ this.locksRoot = locksRoot;
267
+ this.now = now;
268
+ this.trustedRoot = trustedRoot;
269
+ assertLockRootPath(locksRoot, trustedRoot);
270
+ ensureDirectory(locksRoot);
271
+ assertLockRootPath(locksRoot, trustedRoot);
272
+ }
273
+ acquire(input) {
274
+ assertLockEntity(input.entityId);
275
+ if (!input.entityId.startsWith('TASK-')) {
276
+ throw new WorkflowError('INVALID_ARGUMENT', 'Core corrective-replan operations require a Task entity.');
277
+ }
278
+ if (!/^[a-f0-9]{64}$/.test(input.operationId)) {
279
+ throw new WorkflowError('INVALID_ARGUMENT', 'Core Task operation id must be lowercase SHA-256.');
280
+ }
281
+ if (!isSupportedCoreTaskTransition(input.transitionId)) {
282
+ throw new WorkflowError('INVALID_ARGUMENT', `Unsupported Core Task transition: ${input.transitionId}`);
283
+ }
284
+ const leaseMs = input.leaseMs ?? DEFAULT_LEASE_MS;
285
+ if (!Number.isSafeInteger(leaseMs) || leaseMs <= 0 || leaseMs > DEFAULT_LEASE_MS) {
286
+ throw new WorkflowError('INVALID_ARGUMENT', 'Core Task operation lease must be between 1 ms and 30 minutes.');
287
+ }
288
+ const timestamp = this.now();
289
+ const nonce = randomUUID();
290
+ const mutex = {
291
+ schemaVersion: STATE_SCHEMA_VERSION,
292
+ kind: 'core-task-operation-mutex',
293
+ entityId: input.entityId,
294
+ operationId: input.operationId,
295
+ transitionId: input.transitionId,
296
+ mutexId: randomUUID(),
297
+ nonceSha256: createHash('sha256').update(nonce).digest('hex'),
298
+ owner: 'workflow-core',
299
+ acquiredAt: timestamp.toISOString(),
300
+ expiresAt: new Date(timestamp.getTime() + leaseMs).toISOString(),
301
+ };
302
+ const file = path.join(this.locksRoot, `${input.entityId}.json`);
303
+ let descriptor = null;
304
+ try {
305
+ descriptor = openSync(file, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | constants.O_NOFOLLOW, 0o600);
306
+ writeFileSync(descriptor, `${JSON.stringify(mutex, null, 2)}\n`, 'utf8');
307
+ closeSync(descriptor);
308
+ descriptor = null;
309
+ }
310
+ catch (error) {
311
+ if (descriptor !== null)
312
+ closeSync(descriptor);
313
+ if (lstatIfPresent(file)) {
314
+ const current = readLockSnapshot(this.locksRoot, input.entityId, this.trustedRoot).record;
315
+ throw new WorkflowError('LOCKED', `${input.entityId} already has a Task operation owner`, {
316
+ owner: isCoreTaskOperationMutex(current) ? current.owner : current.owner,
317
+ ...(isCoreTaskOperationMutex(current) ? {
318
+ operationId: current.operationId,
319
+ transitionId: current.transitionId,
320
+ } : {}),
321
+ expiresAt: current.expiresAt,
322
+ stale: lockExpiry(current, file) <= timestamp.getTime(),
323
+ });
324
+ }
325
+ throw error;
326
+ }
327
+ return issueCoreHandle(this.locksRoot, this.trustedRoot, mutex);
328
+ }
329
+ acquireForRecovery(input) {
330
+ assertLockEntity(input.entityId);
331
+ if (!input.entityId.startsWith('TASK-')
332
+ || !/^[a-f0-9]{64}$/.test(input.operationId)
333
+ || !isSupportedCoreTaskTransition(input.transitionId)) {
334
+ throw new WorkflowError('INVALID_ARGUMENT', 'Invalid Core Task recovery operation identity.');
335
+ }
336
+ const leaseMs = input.leaseMs ?? DEFAULT_LEASE_MS;
337
+ if (!Number.isSafeInteger(leaseMs) || leaseMs <= 0 || leaseMs > DEFAULT_LEASE_MS) {
338
+ throw new WorkflowError('INVALID_ARGUMENT', 'Core Task recovery lease must be between 1 ms and 30 minutes.');
339
+ }
340
+ const file = path.join(this.locksRoot, `${input.entityId}.json`);
341
+ if (!lstatIfPresent(file))
342
+ return this.acquire(input);
343
+ const now = this.now();
344
+ const snapshot = readLockSnapshot(this.locksRoot, input.entityId, this.trustedRoot);
345
+ if (!isCoreTaskOperationMutex(snapshot.record)
346
+ || snapshot.record.operationId !== input.operationId
347
+ || snapshot.record.transitionId !== input.transitionId) {
348
+ throw new WorkflowError('LOCKED', `${input.entityId} is owned by another Task operation.`, {
349
+ expiresAt: snapshot.record.expiresAt,
350
+ });
351
+ }
352
+ if (lockExpiry(snapshot.record, file) > now.getTime()) {
353
+ throw new WorkflowError('LOCKED', `${input.entityId} still has an active Core Task operation.`, {
354
+ operationId: snapshot.record.operationId,
355
+ expiresAt: snapshot.record.expiresAt,
356
+ });
357
+ }
358
+ assertSameLeaseIdentity(file, snapshot.identity);
359
+ unlinkSync(file);
360
+ return this.acquire(input);
361
+ }
362
+ transferFromWriter(writerHandle, input) {
363
+ const writer = assertWriterLeaseHandle(this.locksRoot, this.trustedRoot, writerHandle, this.now());
364
+ if (writer.lease.entityId !== input.entityId) {
365
+ throw new WorkflowError('LOCK_TOKEN_INVALID', `Writer lease does not own ${input.entityId}`);
366
+ }
367
+ const mutex = this.materializeMutex(input);
368
+ const file = path.join(this.locksRoot, `${input.entityId}.json`);
369
+ assertSameLeaseIdentity(file, writer.identity);
370
+ writeJsonAtomic(file, mutex);
371
+ writerHandles.delete(writerHandle);
372
+ return issueCoreHandle(this.locksRoot, this.trustedRoot, mutex);
373
+ }
374
+ acquireYieldRecovery(input) {
375
+ assertHash64(input.expectedWriterContentSha256, 'Expected writer lease content hash');
376
+ assertHash64(input.expectedWriterTokenSha256, 'Expected writer lease token hash');
377
+ const file = path.join(this.locksRoot, `${input.entityId}.json`);
378
+ if (!lstatIfPresent(file))
379
+ return this.acquire(input);
380
+ const snapshot = readLockSnapshot(this.locksRoot, input.entityId, this.trustedRoot);
381
+ if (isCoreTaskOperationMutex(snapshot.record))
382
+ return this.acquireForRecovery(input);
383
+ const now = this.now();
384
+ if (leaseExpiry(snapshot.record, file) > now.getTime()) {
385
+ throw new WorkflowError('LOCKED', `${input.entityId} still has an active writer lease.`);
386
+ }
387
+ if (createHash('sha256').update(snapshot.content).digest('hex') !== input.expectedWriterContentSha256
388
+ || createHash('sha256').update(snapshot.record.token).digest('hex') !== input.expectedWriterTokenSha256) {
389
+ throw new WorkflowError('STATE_CONFLICT', 'Stale writer lease does not match the corrective-yield intent.');
390
+ }
391
+ const mutex = this.materializeMutex(input);
392
+ assertSameLeaseIdentity(file, snapshot.identity);
393
+ writeJsonAtomic(file, mutex);
394
+ return issueCoreHandle(this.locksRoot, this.trustedRoot, mutex);
395
+ }
396
+ release(handle) {
397
+ const record = assertCoreTaskOperationHandle(this.locksRoot, this.trustedRoot, handle, this.now());
398
+ const file = path.join(this.locksRoot, `${record.entityId}.json`);
399
+ const snapshot = readLockSnapshot(this.locksRoot, record.entityId, this.trustedRoot);
400
+ if (!isCoreTaskOperationMutex(snapshot.record) || snapshot.record.mutexId !== record.mutexId) {
401
+ throw new WorkflowError('LOCK_TOKEN_INVALID', `Core Task operation does not own ${record.entityId}`);
402
+ }
403
+ assertSameLeaseIdentity(file, snapshot.identity);
404
+ unlinkSync(file);
405
+ closeSync(coreHandles.get(handle).descriptor);
406
+ coreHandles.delete(handle);
407
+ }
408
+ materializeMutex(input) {
409
+ assertLockEntity(input.entityId);
410
+ if (!input.entityId.startsWith('TASK-') || !/^[a-f0-9]{64}$/.test(input.operationId)
411
+ || !isSupportedCoreTaskTransition(input.transitionId)) {
412
+ throw new WorkflowError('INVALID_ARGUMENT', 'Invalid Core Task operation identity.');
413
+ }
414
+ const leaseMs = input.leaseMs ?? DEFAULT_LEASE_MS;
415
+ if (!Number.isSafeInteger(leaseMs) || leaseMs <= 0 || leaseMs > DEFAULT_LEASE_MS) {
416
+ throw new WorkflowError('INVALID_ARGUMENT', 'Core Task operation lease must be between 1 ms and 30 minutes.');
417
+ }
418
+ const timestamp = this.now();
419
+ return {
420
+ schemaVersion: STATE_SCHEMA_VERSION,
421
+ kind: 'core-task-operation-mutex',
422
+ entityId: input.entityId,
423
+ operationId: input.operationId,
424
+ transitionId: input.transitionId,
425
+ mutexId: randomUUID(),
426
+ nonceSha256: createHash('sha256').update(randomUUID()).digest('hex'),
427
+ owner: 'workflow-core',
428
+ acquiredAt: timestamp.toISOString(),
429
+ expiresAt: new Date(timestamp.getTime() + leaseMs).toISOString(),
430
+ };
431
+ }
432
+ }
433
+ export function assertCoreTaskOperationHandle(locksRoot, trustedRoot, handle, now, expected) {
434
+ if (!handle || typeof handle !== 'object') {
435
+ throw new WorkflowError('LOCK_TOKEN_INVALID', 'A genuine Core Task operation handle is required.');
436
+ }
437
+ const issued = coreHandles.get(handle);
438
+ if (!issued || issued.locksRoot !== path.resolve(locksRoot)
439
+ || (issued.trustedRoot ?? undefined) !== (trustedRoot === undefined ? undefined : path.resolve(trustedRoot))) {
440
+ throw new WorkflowError('LOCK_TOKEN_INVALID', 'A genuine Core Task operation handle is required.');
441
+ }
442
+ if (expected?.entityId !== undefined && issued.mutex.entityId !== expected.entityId) {
443
+ throw new WorkflowError('LOCK_TOKEN_INVALID', `Core Task operation does not own ${expected.entityId}`);
444
+ }
445
+ if (expected?.operationId !== undefined && issued.mutex.operationId !== expected.operationId) {
446
+ throw new WorkflowError('LOCK_TOKEN_INVALID', 'Core Task operation id mismatch.');
447
+ }
448
+ if (expected?.transitionId !== undefined && issued.mutex.transitionId !== expected.transitionId) {
449
+ throw new WorkflowError('LOCK_TOKEN_INVALID', 'Core Task operation transition mismatch.');
450
+ }
451
+ const snapshot = readLockSnapshot(locksRoot, issued.mutex.entityId, trustedRoot);
452
+ const anchored = fstatSync(issued.descriptor);
453
+ if (!isCoreTaskOperationMutex(snapshot.record)
454
+ || snapshot.record.mutexId !== issued.mutex.mutexId
455
+ || snapshot.record.nonceSha256 !== issued.mutex.nonceSha256
456
+ || !sameIdentity(snapshot.identity, issued.identity)
457
+ || !sameIdentity(snapshot.identity, anchored)) {
458
+ closeSync(issued.descriptor);
459
+ coreHandles.delete(handle);
460
+ throw new WorkflowError('LOCK_TOKEN_INVALID', `Core Task operation does not own ${issued.mutex.entityId}`);
461
+ }
462
+ if (lockExpiry(snapshot.record, path.join(locksRoot, `${issued.mutex.entityId}.json`)) <= now.getTime()) {
463
+ throw new WorkflowError('TRANSITION_BLOCKED', `Core Task operation has expired for ${issued.mutex.entityId}.`);
464
+ }
465
+ return snapshot.record;
466
+ }
467
+ export function inspectCoreTaskOperationMutex(locksRoot, entityId, trustedRoot) {
468
+ assertLockEntity(entityId);
469
+ if (!lstatIfPresent(locksRoot))
470
+ return null;
471
+ const file = path.join(locksRoot, `${entityId}.json`);
472
+ if (!lstatIfPresent(file))
473
+ return null;
474
+ const record = readLockSnapshot(locksRoot, entityId, trustedRoot).record;
475
+ return isCoreTaskOperationMutex(record) ? record : null;
476
+ }
477
+ export function toPublicCoreTaskOperation(mutex, now) {
478
+ return {
479
+ schemaVersion: mutex.schemaVersion,
480
+ entityId: mutex.entityId,
481
+ operationId: mutex.operationId,
482
+ transitionId: mutex.transitionId,
483
+ owner: mutex.owner,
484
+ acquiredAt: mutex.acquiredAt,
485
+ expiresAt: mutex.expiresAt,
486
+ stale: lockExpiry(mutex, '<core-task-operation>') <= now.getTime(),
487
+ };
488
+ }
489
+ function assertLockRootPath(locksRoot, trustedRoot) {
490
+ const boundary = trustedBoundary(locksRoot, trustedRoot);
491
+ assertNoSymlinkBelowBoundary(locksRoot, boundary, 'Writer lock root');
492
+ const stat = lstatIfPresent(locksRoot);
493
+ if (stat && (stat.isSymbolicLink() || !stat.isDirectory())) {
494
+ throw new WorkflowError('STATE_CORRUPT', `Writer lock root must be a directory: ${locksRoot}`);
495
+ }
496
+ }
497
+ function assertNoSymlinkBelowBoundary(candidate, boundary, label) {
498
+ const resolvedCandidate = path.resolve(candidate);
499
+ if (resolvedCandidate !== boundary && !resolvedCandidate.startsWith(`${boundary}${path.sep}`)) {
500
+ throw new WorkflowError('INVALID_ARGUMENT', `${label} is outside of the trusted root: ${candidate}`, {
501
+ trustedRoot: boundary,
502
+ });
503
+ }
504
+ let current = boundary;
505
+ for (const segment of path.relative(boundary, resolvedCandidate).split(path.sep).filter(Boolean)) {
506
+ current = path.join(current, segment);
507
+ const stat = lstatIfPresent(current);
508
+ if (!stat)
509
+ return;
510
+ if (stat.isSymbolicLink()) {
511
+ throw new WorkflowError('STATE_CORRUPT', `${label} must not traverse a symlink: ${candidate}`, {
512
+ symlinkPath: current,
513
+ });
514
+ }
515
+ }
516
+ }
517
+ function trustedBoundary(locksRoot, trustedRoot) {
518
+ const boundary = path.resolve(trustedRoot ?? path.dirname(locksRoot));
519
+ const stat = lstatIfPresent(boundary);
520
+ if (!stat || stat.isSymbolicLink() || !stat.isDirectory()) {
521
+ throw new WorkflowError('STATE_CORRUPT', `Writer lock trusted root must be an existing directory: ${boundary}`);
522
+ }
523
+ return boundary;
524
+ }
525
+ function assertSameLeaseIdentity(file, expected) {
526
+ const current = lstatIfPresent(file);
527
+ if (!current || current.isSymbolicLink() || !current.isFile() || !sameIdentity(expected, current)) {
528
+ throw new WorkflowError('STATE_CONFLICT', `Writer lease changed before mutation: ${file}`);
529
+ }
530
+ }
531
+ function leaseExpiry(lease, file) {
532
+ if (typeof lease.expiresAt !== 'string') {
533
+ throw new WorkflowError('STATE_CORRUPT', `Writer lease expiresAt must be a finite ISO timestamp: ${file}`);
534
+ }
535
+ const expiresAt = Date.parse(lease.expiresAt);
536
+ if (!Number.isFinite(expiresAt) || new Date(expiresAt).toISOString() !== lease.expiresAt) {
537
+ throw new WorkflowError('STATE_CORRUPT', `Writer lease expiresAt must be a finite ISO timestamp: ${file}`, {
538
+ expiresAt: lease.expiresAt,
539
+ });
540
+ }
541
+ return expiresAt;
542
+ }
543
+ function lockExpiry(record, file) {
544
+ return leaseExpiry(record, file);
545
+ }
546
+ function isCoreTaskOperationMutex(record) {
547
+ return record.kind === 'core-task-operation-mutex';
548
+ }
549
+ function validateLockRecord(record, expectedEntityId, file) {
550
+ if (!record || typeof record !== 'object' || Array.isArray(record)) {
551
+ throw new WorkflowError('STATE_CORRUPT', `Task operation lock must be an object: ${file}`);
552
+ }
553
+ if (record.schemaVersion !== STATE_SCHEMA_VERSION || record.entityId !== expectedEntityId) {
554
+ throw new WorkflowError('STATE_CORRUPT', `Task operation lock identity mismatch: ${file}`);
555
+ }
556
+ if (isCoreTaskOperationMutex(record)) {
557
+ const keys = Object.keys(record).sort();
558
+ const expected = [
559
+ 'acquiredAt', 'entityId', 'expiresAt', 'kind', 'mutexId', 'nonceSha256', 'operationId',
560
+ 'owner', 'schemaVersion', 'transitionId',
561
+ ].sort();
562
+ if (keys.length !== expected.length || keys.some((key, index) => key !== expected[index])) {
563
+ throw new WorkflowError('STATE_CORRUPT', `Core Task operation lock has unknown or missing fields: ${file}`);
564
+ }
565
+ if (record.owner !== 'workflow-core' || !isSupportedCoreTaskTransition(record.transitionId)) {
566
+ throw new WorkflowError('STATE_CORRUPT', `Core Task operation lock semantic identity mismatch: ${file}`);
567
+ }
568
+ assertHash64(record.operationId, 'Core Task operation id');
569
+ assertHash64(record.nonceSha256, 'Core Task operation nonce hash');
570
+ if (typeof record.mutexId !== 'string' || !/^[0-9a-f-]{36}$/.test(record.mutexId)) {
571
+ throw new WorkflowError('STATE_CORRUPT', `Core Task operation mutexId is invalid: ${file}`);
572
+ }
573
+ const acquiredAt = typeof record.acquiredAt === 'string' ? Date.parse(record.acquiredAt) : Number.NaN;
574
+ if (!Number.isFinite(acquiredAt) || new Date(acquiredAt).toISOString() !== record.acquiredAt) {
575
+ throw new WorkflowError('STATE_CORRUPT', `Core Task operation acquiredAt is invalid: ${file}`);
576
+ }
577
+ }
578
+ lockExpiry(record, file);
579
+ }
580
+ function assertWriterLeaseHandle(locksRoot, trustedRoot, handle, now) {
581
+ if (!handle || typeof handle !== 'object') {
582
+ throw new WorkflowError('LOCK_TOKEN_INVALID', 'A genuine writer lease handle is required.');
583
+ }
584
+ const issued = writerHandles.get(handle);
585
+ if (!issued || issued.locksRoot !== path.resolve(locksRoot)
586
+ || (issued.trustedRoot ?? undefined) !== (trustedRoot === undefined ? undefined : path.resolve(trustedRoot))) {
587
+ throw new WorkflowError('LOCK_TOKEN_INVALID', 'A genuine writer lease handle is required.');
588
+ }
589
+ const current = readLeaseSnapshot(locksRoot, issued.snapshot.lease.entityId, trustedRoot);
590
+ if (!sameIdentity(current.identity, issued.snapshot.identity)
591
+ || current.lease.token !== issued.snapshot.lease.token
592
+ || current.content !== issued.snapshot.content) {
593
+ throw new WorkflowError('LOCK_TOKEN_INVALID', 'Writer lease changed after proof issuance.');
594
+ }
595
+ if (leaseExpiry(current.lease, path.join(locksRoot, `${current.lease.entityId}.json`)) <= now.getTime()) {
596
+ throw new WorkflowError('TRANSITION_BLOCKED', `Writer lease has expired for ${current.lease.entityId}.`);
597
+ }
598
+ return current;
599
+ }
600
+ function issueCoreHandle(locksRoot, trustedRoot, mutex) {
601
+ const issued = readLockSnapshot(locksRoot, mutex.entityId, trustedRoot);
602
+ if (!isCoreTaskOperationMutex(issued.record) || issued.record.mutexId !== mutex.mutexId) {
603
+ throw new WorkflowError('STATE_CONFLICT', 'Core Task operation changed during lock transfer.');
604
+ }
605
+ const file = path.join(locksRoot, `${mutex.entityId}.json`);
606
+ let descriptor = null;
607
+ try {
608
+ descriptor = openSync(file, constants.O_RDONLY | constants.O_NOFOLLOW);
609
+ const anchored = fstatSync(descriptor);
610
+ if (!anchored.isFile() || !sameIdentity(issued.identity, anchored)) {
611
+ throw new WorkflowError('STATE_CONFLICT', 'Core Task operation changed while anchoring its handle.');
612
+ }
613
+ }
614
+ catch (error) {
615
+ if (descriptor !== null)
616
+ closeSync(descriptor);
617
+ throw error;
618
+ }
619
+ const handle = Object.freeze(Object.create(null));
620
+ coreHandles.set(handle, {
621
+ locksRoot: path.resolve(locksRoot),
622
+ ...(trustedRoot === undefined ? {} : { trustedRoot: path.resolve(trustedRoot) }),
623
+ mutex,
624
+ identity: issued.identity,
625
+ descriptor,
626
+ });
627
+ return handle;
628
+ }
629
+ function isSupportedCoreTaskTransition(value) {
630
+ return value === 'task.plan.corrective-replan' || value === 'task.coordination.corrective-yield';
631
+ }
632
+ function assertHash64(value, label) {
633
+ if (typeof value !== 'string' || !/^[a-f0-9]{64}$/.test(value)) {
634
+ throw new WorkflowError('STATE_CORRUPT', `${label} must be a lowercase SHA-256 value.`);
635
+ }
636
+ }
637
+ function assertLockEntity(entityId) {
638
+ if (!LOCK_ENTITY_PATTERN.test(entityId)) {
639
+ throw new WorkflowError('INVALID_ARGUMENT', `Invalid lock entity: ${entityId}`);
640
+ }
641
+ }
642
+ function sameIdentity(left, right) {
643
+ return left.dev === right.dev && left.ino === right.ino;
644
+ }
645
+ function lstatIfPresent(candidate) {
646
+ try {
647
+ return lstatSync(candidate);
648
+ }
649
+ catch (error) {
650
+ if (isNodeError(error) && error.code === 'ENOENT')
651
+ return null;
652
+ throw error;
653
+ }
654
+ }
655
+ function isNodeError(error) {
656
+ return error instanceof Error && 'code' in error;
657
+ }
106
658
  //# sourceMappingURL=lock.js.map