chati-dev 4.5.28 → 4.5.29

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 (172) hide show
  1. package/bin/chati.js +151 -123
  2. package/framework/agents/build/dev.md +4 -4
  3. package/framework/agents/deploy/devops.md +3 -3
  4. package/framework/agents/discover/brief.md +18 -6
  5. package/framework/agents/discover/brownfield-wu.md +3 -3
  6. package/framework/agents/discover/greenfield-wu.md +4 -4
  7. package/framework/agents/plan/architect.md +2 -2
  8. package/framework/agents/plan/detail.md +4 -4
  9. package/framework/agents/plan/phases.md +3 -3
  10. package/framework/agents/plan/tasks.md +47 -19
  11. package/framework/agents/plan/ux-brand-architect.md +1 -1
  12. package/framework/agents/plan/ux.md +26 -2
  13. package/framework/agents/quality/qa-implementation.md +8 -8
  14. package/framework/agents/quality/qa-planning.md +58 -4
  15. package/framework/agents/quality/qa-visual.md +4 -4
  16. package/framework/config.yaml +2 -2
  17. package/framework/constitution.md +12 -1
  18. package/framework/context/governance.md +2 -1
  19. package/framework/context/root.md +3 -1
  20. package/framework/domains/constitution.yaml +1 -1
  21. package/framework/domains/global.yaml +1 -1
  22. package/framework/domains/workflows/standard-flow.yaml +7 -8
  23. package/framework/hooks/advance-trigger.js +3 -2
  24. package/framework/hooks/brief-validator.js +1 -1
  25. package/framework/hooks/post-dev.js +1 -1
  26. package/framework/hooks/team-quality-gate.js +9 -2
  27. package/framework/i18n/en.yaml +1 -0
  28. package/framework/i18n/es.yaml +1 -0
  29. package/framework/i18n/fr.yaml +1 -0
  30. package/framework/i18n/pt.yaml +1 -0
  31. package/framework/manifest.json +83 -83
  32. package/framework/manifest.sig +1 -1
  33. package/framework/orchestrator/chati-router.js +14 -6
  34. package/framework/orchestrator/chati.md +61 -23
  35. package/framework/rules/human-writing-style.md +7 -0
  36. package/framework/schemas/session.schema.json +25 -2
  37. package/framework/schemas/task.schema.json +26 -0
  38. package/framework/tasks/orchestrator-handoff.md +10 -10
  39. package/framework/tasks/orchestrator-resume.md +3 -3
  40. package/framework/workflows/brownfield-fullstack.yaml +6 -3
  41. package/framework/workflows/brownfield-service.yaml +5 -3
  42. package/framework/workflows/brownfield-ui.yaml +5 -3
  43. package/framework/workflows/greenfield-fullstack.yaml +6 -3
  44. package/framework/workflows/standard-flow.yaml +8 -3
  45. package/node_modules/@chati/core/native/browser-proof-wire.h +29 -0
  46. package/node_modules/@chati/core/native/darwin-browser-listener.c +36 -0
  47. package/node_modules/@chati/core/native/darwin-browser-listener.h +32 -0
  48. package/node_modules/@chati/core/native/darwin-browser-proof.h +86 -0
  49. package/node_modules/@chati/core/native/darwin-code-identity.h +35 -0
  50. package/node_modules/@chati/core/native/darwin-launch-messages.h +37 -0
  51. package/node_modules/@chati/core/native/darwin-launcher.c +312 -0
  52. package/node_modules/@chati/core/native/descriptor-fs.c +407 -0
  53. package/node_modules/@chati/core/native/execution-admission.h +59 -0
  54. package/node_modules/@chati/core/native/execution-gate.c +41 -0
  55. package/node_modules/@chati/core/native/linux-process.c +41 -0
  56. package/node_modules/@chati/core/native/prebuilds/darwin-arm64/darwin-launcher +0 -0
  57. package/node_modules/@chati/core/native/prebuilds/darwin-arm64/descriptor-fs.node +0 -0
  58. package/node_modules/@chati/core/native/prebuilds/darwin-arm64/execution-gate +0 -0
  59. package/node_modules/@chati/core/native/prebuilds/linux-x64/descriptor-fs.node +0 -0
  60. package/node_modules/@chati/core/native/prebuilds/linux-x64/execution-gate +0 -0
  61. package/node_modules/@chati/core/native/process-info-pipe.c +91 -0
  62. package/node_modules/@chati/core/package.json +2 -1
  63. package/node_modules/@chati/core/scripts/build-native.js +36 -0
  64. package/node_modules/@chati/core/src/darwin-browser-proof.js +87 -0
  65. package/node_modules/@chati/core/src/darwin-host-boot.js +58 -0
  66. package/node_modules/@chati/core/src/darwin-process.js +79 -0
  67. package/node_modules/@chati/core/src/darwin-supervisor-channels.js +82 -0
  68. package/node_modules/@chati/core/src/darwin-terminal-receipt.js +111 -0
  69. package/node_modules/@chati/core/src/descriptor-fs.js +222 -0
  70. package/node_modules/@chati/core/src/execution-gate.js +8 -0
  71. package/node_modules/@chati/core/src/host-boot-marker.js +45 -0
  72. package/node_modules/@chati/core/src/index.js +18 -0
  73. package/node_modules/@chati/core/src/linux-host-boot.js +218 -0
  74. package/node_modules/@chati/core/src/linux-process.js +133 -0
  75. package/node_modules/@chati/planning/src/browser-verification-plan.js +71 -0
  76. package/node_modules/@chati/planning/src/index.js +33 -0
  77. package/node_modules/@chati/planning/src/planning-material.js +64 -0
  78. package/node_modules/@chati/planning/src/verification-plan.js +57 -0
  79. package/node_modules/@chati/rail/package.json +1 -0
  80. package/node_modules/@chati/rail/src/execution-restoration.js +92 -0
  81. package/node_modules/@chati/rail/src/index.js +812 -167
  82. package/node_modules/@chati/rail/src/provider-processing.js +118 -0
  83. package/node_modules/@chati/rail/src/terminal-containment.js +23 -0
  84. package/node_modules/@chati/release-lane/src/index.js +40 -43
  85. package/node_modules/@chati/review-council/src/index.js +223 -0
  86. package/node_modules/@chati/tracking-clickup/src/index.js +67 -58
  87. package/package-artifact-manifest.json +1 -1
  88. package/package-artifact-manifest.sig +1 -1
  89. package/package.json +3 -2
  90. package/scripts/verify-real-harness-e2e.js +20 -8
  91. package/src/installer/core.js +147 -107
  92. package/src/installer/package-artifact.js +7 -0
  93. package/src/installer/preflight.js +13 -0
  94. package/src/installer/provider-overlay.js +1 -2
  95. package/src/installer/templates.js +58 -0
  96. package/src/installer-v2/catalog-client.js +31 -1
  97. package/src/installer-v2/clickup-connection-storage.js +92 -0
  98. package/src/installer-v2/clickup-mcp-client.js +155 -0
  99. package/src/installer-v2/clickup-oauth-loopback.js +96 -0
  100. package/src/installer-v2/clickup-oauth.js +233 -0
  101. package/src/installer-v2/index.js +141 -15
  102. package/src/installer-v2/installation-authority.js +98 -4
  103. package/src/installer-v2/provider-executable.js +117 -12
  104. package/src/installer-v2/wizard-installation.js +11 -0
  105. package/src/orchestrator/agent-selector.js +39 -1
  106. package/src/orchestrator/browser-artifact-store.js +131 -0
  107. package/src/orchestrator/cli.js +762 -205
  108. package/src/orchestrator/clickup-mcp-observer.js +226 -0
  109. package/src/orchestrator/clickup-runtime.js +45 -24
  110. package/src/orchestrator/council-gate-runtime.js +903 -0
  111. package/src/orchestrator/evidence-authority-enrollment.js +75 -0
  112. package/src/orchestrator/evidence-authority-request.js +46 -0
  113. package/src/orchestrator/handoff-engine.js +4 -11
  114. package/src/orchestrator/pipeline-manager.js +42 -4
  115. package/src/orchestrator/planning-contract-revision.js +711 -0
  116. package/src/orchestrator/planning-reconciliation.js +302 -0
  117. package/src/orchestrator/planning-runtime.js +476 -29
  118. package/src/orchestrator/rail-adjudication-evidence.js +15 -24
  119. package/src/orchestrator/rail-human-effort-authority.js +46 -0
  120. package/src/orchestrator/rail-human-effort.js +149 -0
  121. package/src/orchestrator/rail-runtime.js +291 -47
  122. package/src/orchestrator/rail-verification.js +278 -0
  123. package/src/orchestrator/review-runtime.js +37 -2
  124. package/src/orchestrator/runtime-installation-v2.js +143 -24
  125. package/src/orchestrator/session-manager.js +1457 -29
  126. package/src/tasks/handoff.js +57 -33
  127. package/src/terminal/adapters/claude-adapter.js +3 -0
  128. package/src/terminal/adapters/codex-adapter.js +13 -0
  129. package/src/terminal/adapters/grok-adapter.js +1 -0
  130. package/src/terminal/agent-attempt-budget.js +35 -0
  131. package/src/terminal/browser-endpoint-proxy.js +174 -0
  132. package/src/terminal/browser-observer-driver.js +94 -0
  133. package/src/terminal/browser-observer-supervisor.js +73 -0
  134. package/src/terminal/browser-observer.js +95 -0
  135. package/src/terminal/browser-process-facts.js +75 -0
  136. package/src/terminal/browser-runtime.js +131 -0
  137. package/src/terminal/darwin-authority-policy.js +213 -0
  138. package/src/terminal/darwin-browser-application.js +116 -0
  139. package/src/terminal/darwin-browser-observer.js +57 -0
  140. package/src/terminal/darwin-execution.js +185 -0
  141. package/src/terminal/executable-snapshot.js +202 -0
  142. package/src/terminal/handoff-parser.js +48 -2
  143. package/src/terminal/isolation.js +14 -13
  144. package/src/terminal/prompt-builder.js +176 -15
  145. package/src/terminal/provider-preflight.js +47 -27
  146. package/src/terminal/rail-execution-worktree.js +22 -6
  147. package/src/terminal/rail-process-observer.js +10 -0
  148. package/src/terminal/rail-prompts.js +21 -3
  149. package/src/terminal/rail-readonly-workspace.js +54 -5
  150. package/src/terminal/rail-worktree-checkpoint.js +315 -0
  151. package/src/terminal/rail-worktree-restoration.js +485 -0
  152. package/src/terminal/run-agent.js +164 -42
  153. package/src/terminal/run-council-gate.js +338 -0
  154. package/src/terminal/run-planning-reconciliation.js +49 -0
  155. package/src/terminal/run-rail-adjudication.js +29 -7
  156. package/src/terminal/run-rail-recovery.js +97 -0
  157. package/src/terminal/run-rail-review.js +36 -15
  158. package/src/terminal/run-rail-rework.js +26 -15
  159. package/src/terminal/run-rail-task.js +22 -9
  160. package/src/terminal/runtime-native-helper.js +85 -0
  161. package/src/terminal/spawner.js +899 -142
  162. package/src/terminal/verification-process-facts.js +49 -0
  163. package/src/upgrade/entrypoints.js +68 -0
  164. package/src/upgrade/generation.js +194 -0
  165. package/src/upgrade/maintenance.js +79 -0
  166. package/src/upgrade/paths.js +81 -0
  167. package/src/upgrade/upgrade.js +267 -0
  168. package/src/utils/execution-role.js +15 -0
  169. package/src/utils/handoff-paths.js +59 -0
  170. package/src/wizard/i18n.js +1 -0
  171. package/src/wizard/index.js +17 -5
  172. package/src/wizard/questions.js +24 -12
@@ -1,12 +1,14 @@
1
1
  import {
2
- closeSync, constants, existsSync, fstatSync, fsyncSync, linkSync, lstatSync, mkdirSync,
3
- openSync, readSync, readdirSync, renameSync, unlinkSync, writeSync,
2
+ closeSync, constants, fstatSync, fsyncSync, openSync, readSync, writeSync,
4
3
  } from 'node:fs';
5
4
  import { randomUUID } from 'node:crypto';
6
5
  import { hostname } from 'node:os';
7
6
  import { basename, dirname, isAbsolute, join, parse, relative, resolve, sep } from 'node:path';
8
7
  import { performance } from 'node:perf_hooks';
9
- import { ContractError, canonicalize, sha256 } from '@chati/core';
8
+ import {
9
+ captureHostBootMarker, ContractError, canonicalize, directoryPathSync, hostBootMarkerChanged,
10
+ linkAtSync, lstatAtSync, mkdirAtSync, openAtSync, readDirectorySync, renameAtSync, sha256, unlinkAtSync,
11
+ } from '@chati/core';
10
12
 
11
13
  const RFC3339 = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/;
12
14
  const DELIVERY_STATES = new Set(['pending', 'sent_unconfirmed', 'confirmed', 'retryable_failure', 'permanent_failure']);
@@ -158,23 +160,22 @@ function readDescriptorBounded(descriptor, maxBytes) {
158
160
  }
159
161
 
160
162
  function openPinnedTrackingDirectory(path, { create = false } = {}) {
161
- if (process.platform !== 'linux') fail('UNSUPPORTED_TRACKING_STORAGE_PLATFORM', 'tracking authority storage requires Linux descriptor-relative filesystem operations');
163
+ if (!['linux', 'darwin'].includes(process.platform)) fail('UNSUPPORTED_TRACKING_STORAGE_PLATFORM', `tracking authority storage requires descriptor-relative operations; unsupported platform: ${process.platform}`);
162
164
  const absolute = resolve(path);
163
165
  const root = parse(absolute).root;
164
166
  let descriptor = openSync(root, constants.O_RDONLY | (constants.O_DIRECTORY || 0) | (constants.O_NOFOLLOW || 0));
165
167
  try {
166
168
  for (const segment of absolute.slice(root.length).split(sep).filter(Boolean)) {
167
- const childPath = join(`/proc/self/fd/${descriptor}`, segment);
168
169
  let child;
169
- try { child = openSync(childPath, constants.O_RDONLY | (constants.O_DIRECTORY || 0) | (constants.O_NOFOLLOW || 0)); }
170
+ try { child = openAtSync(descriptor, segment, constants.O_RDONLY | (constants.O_DIRECTORY || 0) | (constants.O_NOFOLLOW || 0)); }
170
171
  catch (error) {
171
172
  if (error?.code !== 'ENOENT' || !create) {
172
173
  if (['ELOOP', 'ENOTDIR'].includes(error?.code)) fail('UNSAFE_OUTBOX_DIRECTORY', 'tracking directory chain must contain only real directories');
173
174
  throw error;
174
175
  }
175
- try { mkdirSync(childPath, { mode: 0o700 }); }
176
+ try { mkdirAtSync(descriptor, segment, { mode: 0o700 }); }
176
177
  catch (mkdirError) { if (mkdirError?.code !== 'EEXIST') throw mkdirError; }
177
- child = openSync(childPath, constants.O_RDONLY | (constants.O_DIRECTORY || 0) | (constants.O_NOFOLLOW || 0));
178
+ child = openAtSync(descriptor, segment, constants.O_RDONLY | (constants.O_DIRECTORY || 0) | (constants.O_NOFOLLOW || 0));
178
179
  }
179
180
  const stat = fstatSync(child);
180
181
  if (!stat.isDirectory()) {
@@ -184,18 +185,13 @@ function openPinnedTrackingDirectory(path, { create = false } = {}) {
184
185
  closeSync(descriptor);
185
186
  descriptor = child;
186
187
  }
187
- return Object.freeze({ absolute, descriptor, identity: fstatSync(descriptor), reference: `/proc/self/fd/${descriptor}` });
188
+ return Object.freeze({ absolute, descriptor, identity: fstatSync(descriptor) });
188
189
  } catch (error) {
189
190
  closeSync(descriptor);
190
191
  throw error;
191
192
  }
192
193
  }
193
194
 
194
- function trackingEntry(directory, name) {
195
- if (basename(name) !== name || name === '.' || name === '..') fail('UNSAFE_OUTBOX_FILE', 'tracking entry must be one path segment');
196
- return join(directory.reference, name);
197
- }
198
-
199
195
  function assertPinnedTrackingDirectory(directory) {
200
196
  const pinned = fstatSync(directory.descriptor);
201
197
  if (!pinned.isDirectory() || !sameIdentity(pinned, directory.identity)) fail('UNSAFE_OUTBOX_DIRECTORY', 'pinned tracking directory changed');
@@ -217,10 +213,9 @@ function readRegular(path, { allowMissing = false, maxBytes = OUTBOX_MAX_BYTES,
217
213
  try { activeDirectory = openPinnedTrackingDirectory(dirname(path)); }
218
214
  catch (error) { if (allowMissing && error?.code === 'ENOENT') return null; throw error; }
219
215
  }
220
- const entry = trackingEntry(activeDirectory, basename(path));
221
216
  let descriptor;
222
217
  try {
223
- descriptor = openSync(entry, constants.O_RDONLY | (constants.O_NOFOLLOW || 0) | (constants.O_NONBLOCK || 0));
218
+ descriptor = openAtSync(activeDirectory.descriptor, basename(path), constants.O_RDONLY | (constants.O_NOFOLLOW || 0) | (constants.O_NONBLOCK || 0));
224
219
  } catch (error) {
225
220
  if (ownedDirectory) closeSync(activeDirectory.descriptor);
226
221
  if (allowMissing && error?.code === 'ENOENT') return null;
@@ -252,12 +247,12 @@ function writeAtomic(path, data, maxBytes = OUTBOX_MAX_BYTES, { directory = null
252
247
  if (bytes.length > maxBytes) fail('OUTBOX_SIZE_LIMIT', `tracking state exceeds the ${maxBytes}-byte safety limit`);
253
248
  const activeDirectory = directory ?? openPinnedTrackingDirectory(dirname(path), { create: true });
254
249
  const ownedDirectory = directory === null;
255
- const temporary = trackingEntry(activeDirectory, `${basename(path)}.${process.pid}.${randomUUID()}.tmp`);
256
- const destination = trackingEntry(activeDirectory, basename(path));
250
+ const temporaryName = `${basename(path)}.${process.pid}.${randomUUID()}.tmp`;
251
+ const name = basename(path);
257
252
  let temporaryDescriptor;
258
253
  try {
259
254
  const existing = readRegular(path, { allowMissing: true, maxBytes, directory: activeDirectory });
260
- temporaryDescriptor = openSync(temporary, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW || 0), 0o600);
255
+ temporaryDescriptor = openAtSync(activeDirectory.descriptor, temporaryName, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW || 0), 0o600);
261
256
  let offset = 0;
262
257
  while (offset < bytes.length) offset += writeSync(temporaryDescriptor, bytes, offset, bytes.length - offset);
263
258
  fsyncSync(temporaryDescriptor);
@@ -265,18 +260,18 @@ function writeAtomic(path, data, maxBytes = OUTBOX_MAX_BYTES, { directory = null
265
260
  temporaryDescriptor = undefined;
266
261
  assertPinnedTrackingDirectory(activeDirectory);
267
262
  let current = null;
268
- try { current = lstatSync(destination); } catch (error) { if (error?.code !== 'ENOENT') throw error; }
263
+ try { current = lstatAtSync(activeDirectory.descriptor, name); } catch (error) { if (error?.code !== 'ENOENT') throw error; }
269
264
  if ((existing === null && current !== null)
270
265
  || (existing !== null && (!current || current.isSymbolicLink() || !current.isFile()
271
266
  || current.nlink !== 1 || !sameIdentity(current, existing.stat)))) {
272
267
  fail('OUTBOX_STORAGE_CONFLICT', 'tracking state changed during atomic persistence');
273
268
  }
274
- renameSync(temporary, destination);
269
+ renameAtSync(activeDirectory.descriptor, temporaryName, activeDirectory.descriptor, name);
275
270
  fsyncSync(activeDirectory.descriptor);
276
271
  assertPinnedTrackingDirectory(activeDirectory);
277
272
  } finally {
278
273
  if (temporaryDescriptor !== undefined) closeSync(temporaryDescriptor);
279
- try { unlinkSync(temporary); } catch { /* leave an unverified temporary untouched */ }
274
+ try { unlinkAtSync(activeDirectory.descriptor, temporaryName); } catch { /* leave an unverified temporary untouched */ }
280
275
  if (ownedDirectory) closeSync(activeDirectory.descriptor);
281
276
  }
282
277
  }
@@ -320,8 +315,10 @@ export function persistImmutableClickUpDocument(path, data, { maxBytes = OUTBOX_
320
315
  const bytes = Buffer.from(content);
321
316
  if (bytes.length > maxBytes) fail('OUTBOX_SIZE_LIMIT', `tracking document exceeds the ${maxBytes}-byte safety limit`);
322
317
  const directory = openPinnedTrackingDirectory(dirname(path), { create: true });
323
- const temporary = trackingEntry(directory, `${basename(path)}.${process.pid}.${randomUUID()}.tmp`);
324
- const destination = trackingEntry(directory, basename(path));
318
+ const name = basename(path);
319
+ const temporaryName = `${name}.${process.pid}.${randomUUID()}.tmp`;
320
+ // Diagnostic/test-hook path only; mutations use the pinned descriptor.
321
+ const temporary = join(directoryPathSync(directory.descriptor), temporaryName);
325
322
  let temporaryDescriptor;
326
323
  try {
327
324
  const existing = readRegular(path, { allowMissing: true, maxBytes, directory });
@@ -330,7 +327,7 @@ export function persistImmutableClickUpDocument(path, data, { maxBytes = OUTBOX_
330
327
  return Object.freeze({ written: false, path });
331
328
  }
332
329
  runSecurityTestHook('beforeTrackingDocumentTempOpen', { path, temporary, directory: directory.absolute });
333
- temporaryDescriptor = openSync(temporary, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW || 0), 0o600);
330
+ temporaryDescriptor = openAtSync(directory.descriptor, temporaryName, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW || 0), 0o600);
334
331
  runSecurityTestHook('afterTrackingDocumentTempOpen', { path, temporary, directory: directory.absolute });
335
332
  let offset = 0;
336
333
  while (offset < bytes.length) offset += writeSync(temporaryDescriptor, bytes, offset, bytes.length - offset);
@@ -338,7 +335,7 @@ export function persistImmutableClickUpDocument(path, data, { maxBytes = OUTBOX_
338
335
  closeSync(temporaryDescriptor);
339
336
  temporaryDescriptor = undefined;
340
337
  assertPinnedTrackingDirectory(directory);
341
- try { linkSync(temporary, destination); }
338
+ try { linkAtSync(directory.descriptor, temporaryName, directory.descriptor, name); }
342
339
  catch (error) {
343
340
  if (error?.code !== 'EEXIST') throw error;
344
341
  const raced = readRegular(path, { maxBytes, directory });
@@ -350,7 +347,7 @@ export function persistImmutableClickUpDocument(path, data, { maxBytes = OUTBOX_
350
347
  return Object.freeze({ written: true, path });
351
348
  } finally {
352
349
  if (temporaryDescriptor !== undefined) closeSync(temporaryDescriptor);
353
- try { unlinkSync(temporary); } catch { /* leave an unverified temporary untouched */ }
350
+ try { unlinkAtSync(directory.descriptor, temporaryName); } catch { /* leave an unverified temporary untouched */ }
354
351
  closeSync(directory.descriptor);
355
352
  }
356
353
  }
@@ -358,7 +355,7 @@ export function persistImmutableClickUpDocument(path, data, { maxBytes = OUTBOX_
358
355
  export function listClickUpDocumentPaths(directoryPath) {
359
356
  const directory = openPinnedTrackingDirectory(directoryPath);
360
357
  try {
361
- const names = readdirSync(directory.reference).filter((name) => /^[A-Za-z0-9._-]+\.json$/.test(name)).sort();
358
+ const names = readDirectorySync(directory.descriptor).filter((name) => /^[A-Za-z0-9._-]+\.json$/.test(name)).sort();
362
359
  assertPinnedTrackingDirectory(directory);
363
360
  return names.map((name) => join(directory.absolute, name));
364
361
  } finally { closeSync(directory.descriptor); }
@@ -366,16 +363,16 @@ export function listClickUpDocumentPaths(directoryPath) {
366
363
 
367
364
  export function removeImmutableClickUpDocument(path, { maxBytes = OUTBOX_MAX_BYTES } = {}) {
368
365
  const directory = openPinnedTrackingDirectory(dirname(path));
369
- const destination = trackingEntry(directory, basename(path));
366
+ const name = basename(path);
370
367
  try {
371
368
  const existing = readRegular(path, { allowMissing: true, maxBytes, directory });
372
369
  if (existing === null) return false;
373
- const current = lstatSync(destination);
370
+ const current = lstatAtSync(directory.descriptor, name);
374
371
  if (!current.isFile() || current.isSymbolicLink() || current.nlink !== 1 || !sameIdentity(current, existing.stat)) {
375
372
  fail('OUTBOX_STORAGE_CONFLICT', 'tracking document changed before removal');
376
373
  }
377
374
  assertPinnedTrackingDirectory(directory);
378
- unlinkSync(destination);
375
+ unlinkAtSync(directory.descriptor, name);
379
376
  fsyncSync(directory.descriptor);
380
377
  assertPinnedTrackingDirectory(directory);
381
378
  return true;
@@ -392,10 +389,10 @@ function processIsAlive(pid) {
392
389
  try { process.kill(pid, 0); return true; } catch (error) { return error?.code === 'EPERM'; }
393
390
  }
394
391
 
395
- function readOutboxLock(path) {
392
+ function readOutboxLock(directory, name) {
396
393
  let descriptor;
397
394
  try {
398
- descriptor = openSync(path, constants.O_RDONLY | (constants.O_NOFOLLOW || 0) | (constants.O_NONBLOCK || 0));
395
+ descriptor = openAtSync(directory.descriptor, name, constants.O_RDONLY | (constants.O_NOFOLLOW || 0) | (constants.O_NONBLOCK || 0));
399
396
  const before = fstatSync(descriptor);
400
397
  if (!before.isFile() || before.nlink !== 1 || before.size > 16 * 1024) return null;
401
398
  const content = readDescriptorBounded(descriptor, 16 * 1024);
@@ -408,45 +405,54 @@ function readOutboxLock(path) {
408
405
  }
409
406
 
410
407
 
411
- function recoverStaleOutboxLock(path) {
408
+ function recoverStaleOutboxLock(directory, name) {
412
409
  let stat;
413
- try { stat = lstatSync(path); } catch (error) { return error?.code === 'ENOENT'; }
410
+ try { stat = lstatAtSync(directory.descriptor, name); } catch (error) { return error?.code === 'ENOENT'; }
414
411
  if (stat.isSymbolicLink() || !stat.isFile() || stat.nlink !== 1) return false;
415
- const owner = readOutboxLock(path);
416
- if (owner?.hostname === hostname() && processIsAlive(owner.pid)) return false;
412
+ const owner = readOutboxLock(directory, name);
413
+ // A rebooted host may recycle the owner's pid onto an unrelated live process.
414
+ // Prove the reboot from the boot marker before ever trusting PID liveness.
415
+ if (owner?.hostname === hostname() && !hostBootMarkerChanged(owner.host_boot_marker, captureHostBootMarker())
416
+ && processIsAlive(owner.pid)) return false;
417
417
  // A process on another host cannot be proven dead from this host. Keep the
418
418
  // lock until an operator explicitly reconciles it.
419
419
  if (owner?.hostname && owner.hostname !== hostname()) return false;
420
420
  if (!owner && Date.now() - stat.mtimeMs < OUTBOX_LOCK_STALE_MS) return false;
421
- const recoveryPath = `${path}.recovery`;
421
+ const recoveryName = `${name}.recovery`;
422
422
  let recoveryFd;
423
423
  try {
424
- recoveryFd = openSync(recoveryPath, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW || 0), 0o600);
425
- const bytes = Buffer.from(JSON.stringify({ pid: process.pid, hostname: hostname(), token: randomUUID() }));
424
+ recoveryFd = openAtSync(directory.descriptor, recoveryName, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW || 0), 0o600);
425
+ const recoveryMarker = captureHostBootMarker();
426
+ const bytes = Buffer.from(JSON.stringify({ pid: process.pid, hostname: hostname(), token: randomUUID(), ...(recoveryMarker ? { host_boot_marker: recoveryMarker } : {}) }));
426
427
  writeSync(recoveryFd, bytes, 0, bytes.length);
427
428
  fsyncSync(recoveryFd);
428
429
  } catch {
429
430
  if (recoveryFd !== undefined) closeSync(recoveryFd);
430
431
  return false;
431
432
  }
432
- const quarantine = `${path}.stale.${process.pid}.${randomUUID()}`;
433
+ const quarantine = `${name}.stale.${process.pid}.${randomUUID()}`;
433
434
  try {
434
- const current = lstatSync(path);
435
- const currentOwner = readOutboxLock(path);
435
+ const current = lstatAtSync(directory.descriptor, name);
436
+ const currentOwner = readOutboxLock(directory, name);
436
437
  if (!sameIdentity(stat, current) || currentOwner?.token !== owner?.token) return false;
437
- renameSync(path, quarantine);
438
- const quarantined = lstatSync(quarantine);
439
- const quarantinedOwner = readOutboxLock(quarantine);
438
+ renameAtSync(directory.descriptor, name, directory.descriptor, quarantine);
439
+ const quarantined = lstatAtSync(directory.descriptor, quarantine);
440
+ const quarantinedOwner = readOutboxLock(directory, quarantine);
440
441
  if (!sameIdentity(stat, quarantined) || quarantinedOwner?.token !== owner?.token) {
441
- try { if (!existsSync(path)) renameSync(quarantine, path); } catch { /* preserve quarantine fail-closed */ }
442
+ try {
443
+ let missing = false;
444
+ try { lstatAtSync(directory.descriptor, name); }
445
+ catch (error) { if (error.code !== 'ENOENT') throw error; missing = true; }
446
+ if (missing) renameAtSync(directory.descriptor, quarantine, directory.descriptor, name);
447
+ } catch { /* preserve quarantine fail-closed */ }
442
448
  return false;
443
449
  }
444
- unlinkSync(quarantine);
450
+ unlinkAtSync(directory.descriptor, quarantine);
445
451
  return true;
446
452
  } catch (error) { return error?.code === 'ENOENT'; }
447
453
  finally {
448
454
  if (recoveryFd !== undefined) closeSync(recoveryFd);
449
- try { unlinkSync(recoveryPath); } catch { /* fail closed if recovery ownership changed */ }
455
+ try { unlinkAtSync(directory.descriptor, recoveryName); } catch { /* fail closed if recovery ownership changed */ }
450
456
  }
451
457
  }
452
458
 
@@ -512,10 +518,12 @@ export class TrackingClickUp {
512
518
  if (this.#requireAuthenticatedState && (!this.#stateAuthority || !this.#authorityContext)) {
513
519
  fail('CLICKUP_STATE_AUTHORITY_REQUIRED', 'production ClickUp state requires a host authority and project context');
514
520
  }
515
- this.#loadState({ allowMissing: true });
516
521
  // Project Evolution is a derivative, never the only copy of evidence.
517
- // Rebuild it after an interrupted outbox/evolution write sequence.
518
- for (const projectId of new Set(this.#state.projections.map((item) => item.project_id))) this.#evolve(projectId);
522
+ // Rebuild under the same lock as enqueue: another constructor or writer may
523
+ // otherwise replace a snapshot during this read/atomic persistence sequence.
524
+ this.#exclusive(() => {
525
+ for (const projectId of new Set(this.#state.projections.map((item) => item.project_id))) this.#evolve(projectId);
526
+ });
519
527
  }
520
528
 
521
529
  get projections() { return Object.freeze(this.#state.projections.map((item) => Object.freeze(canonicalize(item)))); }
@@ -562,7 +570,7 @@ export class TrackingClickUp {
562
570
  #exclusive(action) {
563
571
  if (this.#lockDepth > 0) return action();
564
572
  const directory = openPinnedTrackingDirectory(dirname(this.#outboxPath), { create: true });
565
- const path = trackingEntry(directory, `${basename(this.#outboxPath)}.lock`);
573
+ const name = `${basename(this.#outboxPath)}.lock`;
566
574
  let descriptor;
567
575
  let token;
568
576
  try {
@@ -570,16 +578,17 @@ export class TrackingClickUp {
570
578
  while (performance.now() < lockDeadline) {
571
579
  let candidate;
572
580
  try {
573
- candidate = openSync(path, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW || 0), 0o600);
581
+ candidate = openAtSync(directory.descriptor, name, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW || 0), 0o600);
574
582
  } catch (error) {
575
583
  if (error?.code !== 'EEXIST') throw error;
576
- if (recoverStaleOutboxLock(path)) continue;
584
+ if (recoverStaleOutboxLock(directory, name)) continue;
577
585
  Atomics.wait(OUTBOX_LOCK_WAIT, 0, 0, 4);
578
586
  continue;
579
587
  }
580
588
  token = randomUUID();
581
589
  try {
582
- const bytes = Buffer.from(JSON.stringify({ schema_version: 1, pid: process.pid, hostname: hostname(), token, acquired_at: new Date().toISOString() }));
590
+ const ownerMarker = captureHostBootMarker();
591
+ const bytes = Buffer.from(JSON.stringify({ schema_version: 1, pid: process.pid, hostname: hostname(), token, acquired_at: new Date().toISOString(), ...(ownerMarker ? { host_boot_marker: ownerMarker } : {}) }));
583
592
  writeSync(candidate, bytes, 0, bytes.length);
584
593
  fsyncSync(candidate);
585
594
  fsyncSync(directory.descriptor);
@@ -587,7 +596,7 @@ export class TrackingClickUp {
587
596
  break;
588
597
  } catch (error) {
589
598
  closeSync(candidate);
590
- try { unlinkSync(path); } catch { /* acquisition failed closed */ }
599
+ try { unlinkAtSync(directory.descriptor, name); } catch { /* acquisition failed closed */ }
591
600
  throw error;
592
601
  }
593
602
  }
@@ -604,7 +613,7 @@ export class TrackingClickUp {
604
613
  if (descriptor !== undefined) {
605
614
  closeSync(descriptor);
606
615
  assertPinnedTrackingDirectory(directory);
607
- if (readOutboxLock(path)?.token === token) { unlinkSync(path); fsyncSync(directory.descriptor); }
616
+ if (readOutboxLock(directory, name)?.token === token) { unlinkAtSync(directory.descriptor, name); fsyncSync(directory.descriptor); }
608
617
  }
609
618
  } finally {
610
619
  closeSync(directory.descriptor);