agentera 3.0.0-dev.32 → 3.0.0-dev.39

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 (196) hide show
  1. package/bundle/CHANGELOG.md +75 -5
  2. package/bundle/references/adapters/package-publication.json +43 -0
  3. package/bundle/references/adapters/package-surface-characterization.md +12 -0
  4. package/bundle/references/analysis/evidence-tier-authority.yaml +23 -21
  5. package/bundle/references/analysis/verification-policy.yaml +1 -0
  6. package/bundle/references/artifacts/artifact-registry-interface-model.yaml +11 -2
  7. package/bundle/references/artifacts/glossary-entry-contract.yaml +1241 -0
  8. package/bundle/references/artifacts/state-storage-authority.yaml +149 -80
  9. package/bundle/references/cli/agent-ready-state-contract.yaml +84 -2
  10. package/bundle/references/cli/audience-namespace-cli-migration.yaml +12 -1
  11. package/bundle/references/cli/prime-consumer-compatibility.yaml +7 -1
  12. package/bundle/skills/agentera/SKILL.md +26 -26
  13. package/bundle/skills/agentera/capabilities/audit/schemas/validation.yaml +26 -8
  14. package/bundle/skills/agentera/capabilities/build/schemas/artifacts.yaml +21 -4
  15. package/bundle/skills/agentera/capabilities/build/schemas/exit.yaml +5 -4
  16. package/bundle/skills/agentera/capabilities/build/schemas/validation.yaml +67 -3
  17. package/bundle/skills/agentera/capabilities/discuss/schemas/artifacts.yaml +3 -2
  18. package/bundle/skills/agentera/capabilities/discuss/schemas/validation.yaml +64 -0
  19. package/bundle/skills/agentera/capabilities/plan/schemas/artifacts.yaml +4 -1
  20. package/bundle/skills/agentera/capabilities/plan/schemas/validation.yaml +30 -0
  21. package/bundle/skills/agentera/capabilities/profile/schemas/artifacts.yaml +5 -2
  22. package/bundle/skills/agentera/capabilities/profile/schemas/exit.yaml +3 -2
  23. package/bundle/skills/agentera/capabilities/profile/schemas/validation.yaml +30 -0
  24. package/bundle/skills/agentera/schemas/artifacts/glossary.yaml +82 -0
  25. package/bundle/skills/agentera/schemas/artifacts/plan.yaml +19 -4
  26. package/bundle/skills/agentera/schemas/artifacts/progress.yaml +92 -0
  27. package/dist/analytics/extractCorpus/evidenceTiers.js +46 -3
  28. package/dist/analytics/extractCorpus/evidenceTiers.js.map +1 -1
  29. package/dist/analytics/extractCorpus/tierReader.js +1 -1
  30. package/dist/analytics/extractCorpus/tierReader.js.map +1 -1
  31. package/dist/analytics/glossaryAdviceResolution.js +167 -0
  32. package/dist/analytics/glossaryAdviceResolution.js.map +1 -0
  33. package/dist/analytics/glossaryInputAcquisition.js +320 -0
  34. package/dist/analytics/glossaryInputAcquisition.js.map +1 -0
  35. package/dist/analytics/personalGlossaryAdmission.js +134 -0
  36. package/dist/analytics/personalGlossaryAdmission.js.map +1 -0
  37. package/dist/analytics/personalGlossaryProfile.js +219 -0
  38. package/dist/analytics/personalGlossaryProfile.js.map +1 -0
  39. package/dist/audit/terminologyDrift.js +282 -0
  40. package/dist/audit/terminologyDrift.js.map +1 -0
  41. package/dist/capabilities/audit/instructions.js +2 -1
  42. package/dist/capabilities/audit/instructions.js.map +1 -1
  43. package/dist/capabilities/build/instructions.js +41 -2
  44. package/dist/capabilities/build/instructions.js.map +1 -1
  45. package/dist/capabilities/discuss/instructions.js +14 -1
  46. package/dist/capabilities/discuss/instructions.js.map +1 -1
  47. package/dist/capabilities/index.js +1 -1
  48. package/dist/capabilities/index.js.map +1 -1
  49. package/dist/capabilities/orchestrate/instructions.js +2 -2
  50. package/dist/capabilities/plan/instructions.js +19 -2
  51. package/dist/capabilities/plan/instructions.js.map +1 -1
  52. package/dist/capabilities/profile/instructions.js +67 -1
  53. package/dist/capabilities/profile/instructions.js.map +1 -1
  54. package/dist/cli/capabilityContext/bespoke.js +4 -3
  55. package/dist/cli/capabilityContext/bespoke.js.map +1 -1
  56. package/dist/cli/capabilityContext/build.js +77 -24
  57. package/dist/cli/capabilityContext/build.js.map +1 -1
  58. package/dist/cli/capabilityContext/closeout.js +2 -2
  59. package/dist/cli/capabilityContext/closeout.js.map +1 -1
  60. package/dist/cli/capabilityContext/evidence.js +2 -6
  61. package/dist/cli/capabilityContext/evidence.js.map +1 -1
  62. package/dist/cli/capabilityContext/planState.js +59 -78
  63. package/dist/cli/capabilityContext/planState.js.map +1 -1
  64. package/dist/cli/capabilityContext/projectVerification.js +410 -0
  65. package/dist/cli/capabilityContext/projectVerification.js.map +1 -0
  66. package/dist/cli/capabilityContext/startup.js +21 -4
  67. package/dist/cli/capabilityContext/startup.js.map +1 -1
  68. package/dist/cli/commands/glossaryAdvice.js +139 -0
  69. package/dist/cli/commands/glossaryAdvice.js.map +1 -0
  70. package/dist/cli/commands/personalGlossary.js +177 -0
  71. package/dist/cli/commands/personalGlossary.js.map +1 -0
  72. package/dist/cli/commands/prime/buildExecutionRequest.js +165 -0
  73. package/dist/cli/commands/prime/buildExecutionRequest.js.map +1 -0
  74. package/dist/cli/commands/prime/collectEntityOrientation.js +18 -2
  75. package/dist/cli/commands/prime/collectEntityOrientation.js.map +1 -1
  76. package/dist/cli/commands/prime/collectOrientationState.js +7 -1
  77. package/dist/cli/commands/prime/collectOrientationState.js.map +1 -1
  78. package/dist/cli/commands/prime/orientationOutput.js +12 -2
  79. package/dist/cli/commands/prime/orientationOutput.js.map +1 -1
  80. package/dist/cli/commands/prime.js +37 -2
  81. package/dist/cli/commands/prime.js.map +1 -1
  82. package/dist/cli/commands/profileGrounding.js +77 -0
  83. package/dist/cli/commands/profileGrounding.js.map +1 -0
  84. package/dist/cli/commands/query.js +14 -4
  85. package/dist/cli/commands/query.js.map +1 -1
  86. package/dist/cli/commands/schema.js +10 -0
  87. package/dist/cli/commands/schema.js.map +1 -1
  88. package/dist/cli/commands/state/write.js +4 -1
  89. package/dist/cli/commands/state/write.js.map +1 -1
  90. package/dist/cli/dispatch/lifecycle.js +45 -0
  91. package/dist/cli/dispatch/lifecycle.js.map +1 -1
  92. package/dist/cli/dispatch/prime.js +22 -1
  93. package/dist/cli/dispatch/prime.js.map +1 -1
  94. package/dist/cli/dispatch/shared.js +1 -1
  95. package/dist/cli/dispatch/shared.js.map +1 -1
  96. package/dist/cli/help.js +23 -1
  97. package/dist/cli/help.js.map +1 -1
  98. package/dist/cli/migrationRequired.js +2 -0
  99. package/dist/cli/migrationRequired.js.map +1 -1
  100. package/dist/cli/orientation/attention.js +3 -1
  101. package/dist/cli/orientation/attention.js.map +1 -1
  102. package/dist/cli/orientation.js +2 -2
  103. package/dist/cli/orientation.js.map +1 -1
  104. package/dist/registries/artifactProtocolIds.js +2 -0
  105. package/dist/registries/artifactProtocolIds.js.map +1 -1
  106. package/dist/registries/artifactRegistry.js +2 -0
  107. package/dist/registries/artifactRegistry.js.map +1 -1
  108. package/dist/registries/evidenceTierContract.js +26 -16
  109. package/dist/registries/evidenceTierContract.js.map +1 -1
  110. package/dist/registries/glossaryAdviceContract.js +79 -0
  111. package/dist/registries/glossaryAdviceContract.js.map +1 -0
  112. package/dist/registries/glossaryCaveatContract.js +68 -0
  113. package/dist/registries/glossaryCaveatContract.js.map +1 -0
  114. package/dist/registries/glossaryConsumerContractValidation.js +792 -0
  115. package/dist/registries/glossaryConsumerContractValidation.js.map +1 -0
  116. package/dist/registries/glossaryEntryContract.js +747 -0
  117. package/dist/registries/glossaryEntryContract.js.map +1 -0
  118. package/dist/registries/glossaryTermIdentity.js +18 -0
  119. package/dist/registries/glossaryTermIdentity.js.map +1 -0
  120. package/dist/registries/glossaryTermOccurrence.js +21 -0
  121. package/dist/registries/glossaryTermOccurrence.js.map +1 -0
  122. package/dist/state/changelog.js +222 -0
  123. package/dist/state/changelog.js.map +1 -0
  124. package/dist/state/decisionEntities.js +18 -11
  125. package/dist/state/decisionEntities.js.map +1 -1
  126. package/dist/state/decisionMigrationProvenance.js +84 -1
  127. package/dist/state/decisionMigrationProvenance.js.map +1 -1
  128. package/dist/state/decisionOverlay.js +9 -32
  129. package/dist/state/decisionOverlay.js.map +1 -1
  130. package/dist/state/decisionRevision.js +69 -444
  131. package/dist/state/decisionRevision.js.map +1 -1
  132. package/dist/state/decisionRevisionMigration.js +90 -0
  133. package/dist/state/decisionRevisionMigration.js.map +1 -0
  134. package/dist/state/directRetrieval.js +3 -52
  135. package/dist/state/directRetrieval.js.map +1 -1
  136. package/dist/state/entityCutover.js +6 -6
  137. package/dist/state/entityCutover.js.map +1 -1
  138. package/dist/state/entityGlossaryAuthority.js +14 -0
  139. package/dist/state/entityGlossaryAuthority.js.map +1 -0
  140. package/dist/state/entityMigrationIdentity.js +6 -0
  141. package/dist/state/entityMigrationIdentity.js.map +1 -0
  142. package/dist/state/entityMigrationPreview.js +14 -23
  143. package/dist/state/entityMigrationPreview.js.map +1 -1
  144. package/dist/state/entityPublicationContext.js +374 -71
  145. package/dist/state/entityPublicationContext.js.map +1 -1
  146. package/dist/state/entityStorage.js +45 -29
  147. package/dist/state/entityStorage.js.map +1 -1
  148. package/dist/state/healthEntities.js +2 -4
  149. package/dist/state/healthEntities.js.map +1 -1
  150. package/dist/state/listRetrieval.js +7 -38
  151. package/dist/state/listRetrieval.js.map +1 -1
  152. package/dist/state/migrationSourceBinding.js +1 -1
  153. package/dist/state/migrationSourceBinding.js.map +1 -1
  154. package/dist/state/objectiveExperimentEntities.js +9 -9
  155. package/dist/state/objectiveExperimentEntities.js.map +1 -1
  156. package/dist/state/planEntities.js +57 -16
  157. package/dist/state/planEntities.js.map +1 -1
  158. package/dist/state/progressEntities.js +177 -39
  159. package/dist/state/progressEntities.js.map +1 -1
  160. package/dist/state/progressGlossaryCaveat.js +169 -0
  161. package/dist/state/progressGlossaryCaveat.js.map +1 -0
  162. package/dist/state/progressPublicationOrder.js +20 -0
  163. package/dist/state/progressPublicationOrder.js.map +1 -0
  164. package/dist/state/safeProjectFile.js +4 -1
  165. package/dist/state/safeProjectFile.js.map +1 -1
  166. package/dist/state/startupProjection.js +2 -70
  167. package/dist/state/startupProjection.js.map +1 -1
  168. package/dist/state/stateStorageAuthority.js +48 -0
  169. package/dist/state/stateStorageAuthority.js.map +1 -0
  170. package/dist/state/summarySourceRowAuthority.js +1 -1
  171. package/dist/state/summarySourceRowAuthority.js.map +1 -1
  172. package/dist/state/todoDocsEntities.js +8 -9
  173. package/dist/state/todoDocsEntities.js.map +1 -1
  174. package/dist/state/write/explain.js +101 -92
  175. package/dist/state/write/explain.js.map +1 -1
  176. package/dist/state/write/glossaryPublication.js +264 -0
  177. package/dist/state/write/glossaryPublication.js.map +1 -0
  178. package/dist/state/write/index.js +1 -0
  179. package/dist/state/write/index.js.map +1 -1
  180. package/dist/state/write/operations.js +25 -27
  181. package/dist/state/write/operations.js.map +1 -1
  182. package/dist/state/write/planEvaluation.js +6 -2
  183. package/dist/state/write/planEvaluation.js.map +1 -1
  184. package/dist/state/write/transaction.js +7 -4
  185. package/dist/state/write/transaction.js.map +1 -1
  186. package/dist/upgrade/upgradeOrchestrator.js +15 -10
  187. package/dist/upgrade/upgradeOrchestrator.js.map +1 -1
  188. package/dist/validate/crossCapability.js +3 -0
  189. package/dist/validate/crossCapability.js.map +1 -1
  190. package/dist/validate/v1LegacyCruft.js +141 -0
  191. package/dist/validate/v1LegacyCruft.js.map +1 -0
  192. package/dist/validate/vocabularyAuthority.js +2 -0
  193. package/dist/validate/vocabularyAuthority.js.map +1 -1
  194. package/package.json +4 -3
  195. package/dist/state/decisionRevisionPublication.js +0 -532
  196. package/dist/state/decisionRevisionPublication.js.map +0 -1
@@ -7,6 +7,7 @@ const DIRECTORY_FLAGS = fs.constants.O_RDONLY
7
7
  | (fs.constants.O_DIRECTORY ?? 0)
8
8
  | (fs.constants.O_NOFOLLOW ?? 0);
9
9
  const FILE_FLAGS = fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0);
10
+ const RECOVERY_IGNORE_BYTES = "*\n!.gitignore\n";
10
11
  function samePublished(left, right) {
11
12
  return left.dev === right.dev
12
13
  && left.ino === right.ino
@@ -75,6 +76,9 @@ function matchesPublished(fd, expected) {
75
76
  return false;
76
77
  return createHash("sha256").update(readDescriptor(fd)).digest("hex") === expected.sha256;
77
78
  }
79
+ function matchesExpected(fd, expected) {
80
+ return Buffer.isBuffer(expected) ? readDescriptor(fd).equals(expected) : matchesPublished(fd, expected);
81
+ }
78
82
  function openMatches(parentFd, name, expectedFd, flags) {
79
83
  let observed;
80
84
  try {
@@ -89,6 +93,34 @@ function openMatches(parentFd, name, expectedFd, flags) {
89
93
  fs.closeSync(observed);
90
94
  }
91
95
  }
96
+ function entryExists(parentFd, name) {
97
+ let fd;
98
+ try {
99
+ fd = fs.openSync(fdPath(parentFd, name), FILE_FLAGS);
100
+ return true;
101
+ }
102
+ catch (error) {
103
+ return error.code !== "ENOENT";
104
+ }
105
+ finally {
106
+ if (fd !== undefined)
107
+ fs.closeSync(fd);
108
+ }
109
+ }
110
+ function pathBytesEqual(parentFd, name, expected) {
111
+ let fd;
112
+ try {
113
+ fd = fs.openSync(fdPath(parentFd, name), FILE_FLAGS);
114
+ return readDescriptor(fd).equals(expected);
115
+ }
116
+ catch {
117
+ return false;
118
+ }
119
+ finally {
120
+ if (fd !== undefined)
121
+ fs.closeSync(fd);
122
+ }
123
+ }
92
124
  function syncDirectory(fd) {
93
125
  fs.fsyncSync(fd);
94
126
  }
@@ -495,26 +527,29 @@ export class EntityPublicationContext {
495
527
  }
496
528
  }
497
529
  }
498
- replaceExisting(relativeTarget, expectedBytes, bytes) {
499
- return this.replaceOwned(relativeTarget, expectedBytes, bytes);
530
+ replaceExisting(relativeTarget, expectedBytes, bytes, maxEntityBytes) {
531
+ return this.replaceOwned(relativeTarget, expectedBytes, bytes, maxEntityBytes);
500
532
  }
501
- restoreExact(relativeTarget, expected, bytes) {
533
+ restoreExact(relativeTarget, expected, bytes, maxEntityBytes) {
502
534
  // Recovery stays bound to the pinned root and exact archived target even when context invalidation caused the primary failure.
503
- return this.replaceOwned(relativeTarget, expected, bytes, false);
535
+ return this.replaceOwned(relativeTarget, expected, bytes, maxEntityBytes, false);
504
536
  }
505
- replaceOwned(relativeTarget, expected, bytes, verifyContext = true) {
537
+ replaceOwned(relativeTarget, expected, bytes, maxEntityBytes, verifyContext = true) {
506
538
  const segments = relativeTarget.split(path.sep).filter(Boolean);
507
539
  if (segments.length < 2 || path.isAbsolute(relativeTarget) || segments.includes("..") || segments.includes("."))
508
540
  throw new Error(`unsafe entity replacement target '${relativeTarget}'`);
509
541
  const directories = [];
510
542
  let targetFd;
511
543
  let stageFd;
544
+ let backupFd;
512
545
  let stageName;
513
546
  let backupName;
514
547
  let displacedName;
515
548
  let displacedFd;
516
- let displacedOwned = false;
517
- let replacementVisible = false;
549
+ let displacementActive = false;
550
+ let committedResult;
551
+ let recovery;
552
+ let baselineBytes;
518
553
  try {
519
554
  let parentFd = this.rootFd;
520
555
  for (const name of segments.slice(0, -1)) {
@@ -527,93 +562,360 @@ export class EntityPublicationContext {
527
562
  targetFd = fs.openSync(fdPath(directoryFd, targetName), FILE_FLAGS);
528
563
  this.assertBoundary(directories, undefined, { name: targetName, fd: targetFd }, verifyContext);
529
564
  const previousBytes = readDescriptor(targetFd);
530
- if (typeof expected === "string" ? !previousBytes.equals(Buffer.from(expected)) : !matchesPublished(targetFd, expected))
565
+ baselineBytes = previousBytes;
566
+ if (!matchesExpected(targetFd, expected))
531
567
  throw new Error(`entity '${relativeTarget}' ownership changed before replacement; preserve both changes and retry explicitly`);
532
- backupName = `.${targetName}.${process.pid}.${randomUUID()}.previous`;
533
- fs.linkSync(fdPath(directoryFd, targetName), fdPath(directoryFd, backupName));
534
- if (!openMatches(directoryFd, backupName, targetFd, FILE_FLAGS))
535
- throw new Error(`entity '${relativeTarget}' prior value could not be pinned for replacement`);
536
- stageName = `.${targetName}.${process.pid}.${randomUUID()}.tmp`;
537
- stageFd = fs.openSync(fdPath(directoryFd, stageName), fs.constants.O_RDWR | fs.constants.O_CREAT | fs.constants.O_EXCL | (fs.constants.O_NOFOLLOW ?? 0), 0o600);
568
+ if (!Number.isSafeInteger(maxEntityBytes) || maxEntityBytes < 1 || previousBytes.length > maxEntityBytes)
569
+ throw new Error(`entity '${relativeTarget}' exact ${previousBytes.length}-byte baseline exceeds the authority-owned ${maxEntityBytes}-byte recovery limit`);
570
+ recovery = this.createRecoveryAttempt(directoryFd);
571
+ backupName = "original.previous";
572
+ backupFd = fs.openSync(fdPath(recovery.attemptFd, backupName), fs.constants.O_RDWR | fs.constants.O_CREAT | fs.constants.O_EXCL | (fs.constants.O_NOFOLLOW ?? 0), 0o600);
573
+ fs.writeFileSync(backupFd, previousBytes);
574
+ fs.fsyncSync(backupFd);
575
+ const backupStat = fs.fstatSync(backupFd, { bigint: true });
576
+ const attemptStat = fs.fstatSync(recovery.attemptFd, { bigint: true });
577
+ if (!backupStat.isFile() || backupStat.dev !== attemptStat.dev || backupStat.uid !== attemptStat.uid || backupStat.nlink !== 1n || (backupStat.mode & 63n) !== 0n || !openMatches(recovery.attemptFd, backupName, backupFd, FILE_FLAGS) || !readDescriptor(backupFd).equals(previousBytes))
578
+ throw new Error(`entity '${relativeTarget}' exact prior-byte snapshot could not be verified before replacement`);
579
+ stageName = "replacement.tmp";
580
+ stageFd = fs.openSync(fdPath(recovery.attemptFd, stageName), fs.constants.O_RDWR | fs.constants.O_CREAT | fs.constants.O_EXCL | (fs.constants.O_NOFOLLOW ?? 0), 0o600);
538
581
  fs.writeFileSync(stageFd, bytes, "utf8");
539
582
  fs.fsyncSync(stageFd);
540
- this.assertBoundary(directories, { name: stageName, fd: stageFd }, { name: targetName, fd: targetFd }, verifyContext);
541
- if (verifyContext) {
542
- fs.renameSync(fdPath(directoryFd, stageName), fdPath(directoryFd, targetName));
543
- replacementVisible = true;
544
- stageName = undefined;
583
+ this.assertBoundary(directories, undefined, { name: targetName, fd: targetFd }, verifyContext);
584
+ if (!openMatches(recovery.attemptFd, stageName, stageFd, FILE_FLAGS) || !openMatches(recovery.attemptFd, backupName, backupFd, FILE_FLAGS) || !readDescriptor(backupFd).equals(previousBytes))
585
+ throw new Error(`entity '${relativeTarget}' private recovery attempt changed before displacement`);
586
+ displacedName = "original.displaced";
587
+ fs.renameSync(fdPath(directoryFd, targetName), fdPath(recovery.attemptFd, displacedName));
588
+ displacementActive = true;
589
+ displacedFd = fs.openSync(fdPath(recovery.attemptFd, displacedName), FILE_FLAGS);
590
+ if (!openMatches(recovery.attemptFd, displacedName, targetFd, FILE_FLAGS) || !matchesExpected(displacedFd, expected))
591
+ throw new Error(`entity '${relativeTarget}' ownership changed after staging; competing bytes were preserved at the target`);
592
+ try {
593
+ fs.linkSync(fdPath(recovery.attemptFd, stageName), fdPath(directoryFd, targetName));
545
594
  }
546
- else {
547
- displacedName = `.${targetName}.${process.pid}.${randomUUID()}.displaced`;
548
- fs.renameSync(fdPath(directoryFd, targetName), fdPath(directoryFd, displacedName));
549
- displacedFd = fs.openSync(fdPath(directoryFd, displacedName), FILE_FLAGS);
550
- if (typeof expected === "string" || !matchesPublished(displacedFd, expected)) {
551
- try {
552
- fs.linkSync(fdPath(directoryFd, displacedName), fdPath(directoryFd, targetName));
553
- if (!openMatches(directoryFd, targetName, displacedFd, FILE_FLAGS))
554
- throw new Error("competing entity could not be returned to its pathname");
555
- syncDirectory(directoryFd);
556
- }
557
- catch (restoreError) {
558
- throw new Error(`entity '${relativeTarget}' ownership changed immediately before restoration; competing bytes remain at '${displacedName}'`, { cause: restoreError });
559
- }
560
- throw new Error(`entity '${relativeTarget}' ownership changed immediately before restoration; competing bytes were preserved at the target and '${displacedName}'`);
561
- }
562
- displacedOwned = true;
563
- try {
564
- fs.linkSync(fdPath(directoryFd, stageName), fdPath(directoryFd, targetName));
565
- }
566
- catch (publishError) {
567
- if (publishError.code === "EEXIST")
568
- throw new Error(`entity '${relativeTarget}' ownership changed during no-clobber restoration publication; competing bytes were preserved`, { cause: publishError });
569
- throw publishError;
570
- }
571
- replacementVisible = true;
572
- this.removeOwnedFile(directoryFd, stageName, stageFd);
573
- stageName = undefined;
574
- this.removeOwnedFile(directoryFd, displacedName, targetFd);
575
- displacedName = undefined;
595
+ catch (publishError) {
596
+ if (publishError.code === "EEXIST")
597
+ throw new Error(`entity '${relativeTarget}' ownership changed during no-clobber publication; competing target bytes were preserved`, { cause: publishError });
598
+ throw publishError;
576
599
  }
600
+ if (!this.removeOwnedFile(recovery.attemptFd, stageName, stageFd))
601
+ throw new Error(`entity '${relativeTarget}' replacement stage changed before cleanup`);
602
+ stageName = undefined;
577
603
  syncDirectory(directoryFd);
604
+ syncDirectory(recovery.attemptFd);
578
605
  this.assertBoundary(directories, undefined, { name: targetName, fd: stageFd }, verifyContext);
579
- this.removeOwnedFile(directoryFd, backupName, targetFd);
580
- backupName = undefined;
581
- return { previousBytes: previousBytes.toString("utf8"), publishedIdentity: publishedIdentity(stageFd) };
606
+ if (!openMatches(recovery.attemptFd, displacedName, targetFd, FILE_FLAGS) || !matchesExpected(displacedFd, expected))
607
+ throw new Error(`entity '${relativeTarget}' ownership changed through the publication boundary; competing bytes remain recoverable`);
608
+ committedResult = { previousBytes: previousBytes.toString("utf8"), publishedIdentity: publishedIdentity(stageFd) };
609
+ this.cleanupCommittedReplacement(recovery.attemptFd, [[displacedName, targetFd], [backupName, backupFd]]);
610
+ displacementActive = false;
611
+ return committedResult;
582
612
  }
583
613
  catch (error) {
614
+ if (committedResult)
615
+ return committedResult;
584
616
  const directoryFd = directories.at(-1)?.fd;
585
- if (verifyContext && replacementVisible && directoryFd !== undefined && stageFd !== undefined && targetFd !== undefined) {
586
- const ownsReplacement = openMatches(directoryFd, segments.at(-1), stageFd, FILE_FLAGS);
587
- const ownsPrior = backupName !== undefined && openMatches(directoryFd, backupName, targetFd, FILE_FLAGS);
588
- if (ownsReplacement && ownsPrior) {
589
- fs.renameSync(fdPath(directoryFd, backupName), fdPath(directoryFd, segments.at(-1)));
590
- backupName = undefined;
591
- syncDirectory(directoryFd);
592
- }
593
- else if (ownsReplacement) {
594
- throw new Error(`entity '${relativeTarget}' replacement failed and its descriptor-pinned prior value could not be restored`, { cause: error });
595
- }
617
+ if (displacementActive && directoryFd !== undefined && targetFd !== undefined && recovery) {
618
+ throw this.recoverDisplacedReplacement({ directoryFd, recoveryFd: recovery.attemptFd, recoveryPath: recovery.relativePath, relativeTarget, baselineBytes: baselineBytes, backupFd, stageFd, stageName, backupName, displacedFd, displacedName, retainStage: !verifyContext, primary: error });
596
619
  }
620
+ const cleanupFailures = [];
621
+ for (const [name, fd, label] of [[stageName, stageFd, "stage"], [backupName, backupFd, "backup"]])
622
+ if (name && fd !== undefined) {
623
+ try {
624
+ if (!recovery || !this.removeOwnedFile(recovery.attemptFd, name, fd))
625
+ cleanupFailures.push(`${label} '${recovery ? path.join(recovery.relativePath, name) : name}' changed`);
626
+ }
627
+ catch (cleanupError) {
628
+ cleanupFailures.push(`${label} '${recovery ? path.join(recovery.relativePath, name) : name}': ${cleanupError.message}`);
629
+ }
630
+ }
631
+ if (cleanupFailures.length)
632
+ throw new Error(`${error.message}; cleanup retained attempt state: ${cleanupFailures.join("; ")}`, { cause: error });
597
633
  throw error;
598
634
  }
599
635
  finally {
600
- const directoryFd = directories.at(-1)?.fd;
601
- if (directoryFd !== undefined && stageFd !== undefined && stageName !== undefined)
602
- this.removeOwnedFile(directoryFd, stageName, stageFd);
603
- if (directoryFd !== undefined && targetFd !== undefined && backupName !== undefined)
604
- this.removeOwnedFile(directoryFd, backupName, targetFd);
605
- if (directoryFd !== undefined && targetFd !== undefined && displacedName !== undefined && displacedOwned)
606
- this.removeOwnedFile(directoryFd, displacedName, targetFd);
607
636
  if (displacedFd !== undefined)
608
637
  fs.closeSync(displacedFd);
638
+ if (backupFd !== undefined)
639
+ fs.closeSync(backupFd);
609
640
  if (stageFd !== undefined)
610
641
  fs.closeSync(stageFd);
611
642
  if (targetFd !== undefined)
612
643
  fs.closeSync(targetFd);
613
644
  for (const entry of directories.reverse())
614
645
  fs.closeSync(entry.fd);
646
+ if (recovery)
647
+ this.closeRecoveryAttempt(recovery);
615
648
  }
616
649
  }
650
+ createRecoveryAttempt(targetDirectoryFd) {
651
+ const rootName = ".entity-recovery";
652
+ const agenteraFd = fs.openSync(fdPath(this.rootFd, ".agentera"), DIRECTORY_FLAGS);
653
+ let rootFd;
654
+ let ignoreFd;
655
+ let ignoreCreated = false;
656
+ let attemptFd;
657
+ let attemptName;
658
+ let rootCreated = false;
659
+ try {
660
+ try {
661
+ rootFd = fs.openSync(fdPath(agenteraFd, rootName), DIRECTORY_FLAGS);
662
+ }
663
+ catch (error) {
664
+ if (error.code !== "ENOENT")
665
+ throw new Error(`private entity recovery root '.agentera/${rootName}' cannot be opened safely: ${error.message}`, { cause: error });
666
+ fs.mkdirSync(fdPath(agenteraFd, rootName), 0o700);
667
+ rootCreated = true;
668
+ syncDirectory(agenteraFd);
669
+ rootFd = fs.openSync(fdPath(agenteraFd, rootName), DIRECTORY_FLAGS);
670
+ }
671
+ const rootStat = fs.fstatSync(rootFd, { bigint: true });
672
+ if ((rootStat.mode & 18n) !== 0n || (typeof process.getuid === "function" && rootStat.uid !== BigInt(process.getuid())))
673
+ throw new Error(`private entity recovery root '.agentera/${rootName}' must be owned by the current user and not group/world-writable`);
674
+ if (rootStat.dev !== fs.fstatSync(targetDirectoryFd, { bigint: true }).dev)
675
+ throw new Error("private entity recovery root must share the canonical entity target filesystem");
676
+ try {
677
+ ignoreFd = fs.openSync(fdPath(rootFd, ".gitignore"), FILE_FLAGS);
678
+ }
679
+ catch (error) {
680
+ if (error.code !== "ENOENT")
681
+ throw new Error(`private entity recovery ignore marker cannot be opened safely: ${error.message}`, { cause: error });
682
+ ignoreFd = fs.openSync(fdPath(rootFd, ".gitignore"), fs.constants.O_RDWR | fs.constants.O_CREAT | fs.constants.O_EXCL | (fs.constants.O_NOFOLLOW ?? 0), 0o600);
683
+ ignoreCreated = true;
684
+ fs.writeFileSync(ignoreFd, RECOVERY_IGNORE_BYTES, "utf8");
685
+ fs.fsyncSync(ignoreFd);
686
+ }
687
+ const ignoreStat = fs.fstatSync(ignoreFd, { bigint: true });
688
+ if (!ignoreStat.isFile() || (ignoreStat.mode & 18n) !== 0n || (typeof process.getuid === "function" && ignoreStat.uid !== BigInt(process.getuid())) || !readDescriptor(ignoreFd).equals(Buffer.from(RECOVERY_IGNORE_BYTES)))
689
+ throw new Error("private entity recovery ignore marker must be an owner-controlled regular file with the authoritative rules");
690
+ syncDirectory(rootFd);
691
+ for (let attempt = 0; attempt < 8; attempt += 1) {
692
+ const candidate = `entity-${process.pid}-${randomUUID()}`;
693
+ try {
694
+ fs.mkdirSync(fdPath(rootFd, candidate), 0o700);
695
+ attemptName = candidate;
696
+ break;
697
+ }
698
+ catch (error) {
699
+ if (error.code !== "EEXIST")
700
+ throw error;
701
+ }
702
+ }
703
+ if (!attemptName)
704
+ throw new Error("could not allocate a unique private entity recovery attempt after 8 tries");
705
+ attemptFd = fs.openSync(fdPath(rootFd, attemptName), DIRECTORY_FLAGS);
706
+ const attemptStat = fs.fstatSync(attemptFd, { bigint: true });
707
+ if ((attemptStat.mode & 63n) !== 0n || attemptStat.uid !== rootStat.uid || attemptStat.dev !== rootStat.dev)
708
+ throw new Error("private entity recovery attempt must be owner-only, owner-controlled, and on the recovery-root filesystem");
709
+ syncDirectory(rootFd);
710
+ return { agenteraFd, rootFd, attemptFd, ignoreFd, attemptName, relativePath: `.agentera/${rootName}/${attemptName}` };
711
+ }
712
+ catch (error) {
713
+ if (attemptFd !== undefined)
714
+ fs.closeSync(attemptFd);
715
+ if (rootFd !== undefined && attemptName) {
716
+ try {
717
+ fs.rmdirSync(fdPath(rootFd, attemptName));
718
+ }
719
+ catch { /* preserve unexpected contents */ }
720
+ }
721
+ if (rootFd !== undefined && ignoreFd !== undefined && ignoreCreated) {
722
+ try {
723
+ this.removeOwnedFile(rootFd, ".gitignore", ignoreFd);
724
+ }
725
+ catch { /* preserve changed marker */ }
726
+ }
727
+ if (ignoreFd !== undefined)
728
+ fs.closeSync(ignoreFd);
729
+ if (rootFd !== undefined)
730
+ fs.closeSync(rootFd);
731
+ if (rootCreated) {
732
+ try {
733
+ fs.rmdirSync(fdPath(agenteraFd, rootName));
734
+ }
735
+ catch { /* preserve non-empty or changed root */ }
736
+ }
737
+ fs.closeSync(agenteraFd);
738
+ throw error;
739
+ }
740
+ }
741
+ closeRecoveryAttempt(recovery) {
742
+ try {
743
+ if (openMatches(recovery.rootFd, recovery.attemptName, recovery.attemptFd, DIRECTORY_FLAGS)) {
744
+ try {
745
+ fs.rmdirSync(fdPath(recovery.rootFd, recovery.attemptName));
746
+ syncDirectory(recovery.rootFd);
747
+ }
748
+ catch { /* best-effort private cleanup retains non-empty or changed attempt */ }
749
+ }
750
+ const remaining = fs.readdirSync(fdPath(recovery.rootFd));
751
+ if (remaining.length === 1 && remaining[0] === ".gitignore" && openMatches(recovery.rootFd, ".gitignore", recovery.ignoreFd, FILE_FLAGS)) {
752
+ try {
753
+ this.removeOwnedFile(recovery.rootFd, ".gitignore", recovery.ignoreFd);
754
+ syncDirectory(recovery.rootFd);
755
+ }
756
+ catch { /* preserve marker on cleanup failure */ }
757
+ }
758
+ if (openMatches(recovery.agenteraFd, ".entity-recovery", recovery.rootFd, DIRECTORY_FLAGS)) {
759
+ try {
760
+ fs.rmdirSync(fdPath(recovery.agenteraFd, ".entity-recovery"));
761
+ syncDirectory(recovery.agenteraFd);
762
+ }
763
+ catch { /* best-effort private cleanup retains non-empty or changed root */ }
764
+ }
765
+ }
766
+ catch {
767
+ /* committed or already-recovered state must not be reclassified by private cleanup */
768
+ }
769
+ finally {
770
+ for (const fd of [recovery.attemptFd, recovery.ignoreFd, recovery.rootFd, recovery.agenteraFd]) {
771
+ try {
772
+ fs.closeSync(fd);
773
+ }
774
+ catch { /* best-effort descriptor close */ }
775
+ }
776
+ }
777
+ }
778
+ cleanupCommittedReplacement(directoryFd, links) {
779
+ for (const [name, fd] of links) {
780
+ for (let attempt = 0; attempt < 2; attempt += 1) {
781
+ try {
782
+ this.removeOwnedFile(directoryFd, name, fd);
783
+ break;
784
+ }
785
+ catch { /* committed publication remains authoritative; retry once and otherwise retain recovery link */ }
786
+ }
787
+ }
788
+ for (let attempt = 0; attempt < 2; attempt += 1) {
789
+ try {
790
+ syncDirectory(directoryFd);
791
+ break;
792
+ }
793
+ catch { /* cleanup durability is best-effort after the durable commit boundary */ }
794
+ }
795
+ }
796
+ recoverDisplacedReplacement(state) {
797
+ const { directoryFd, recoveryFd, recoveryPath, relativeTarget, baselineBytes, backupFd, stageFd, displacedFd, retainStage, primary } = state;
798
+ const targetName = path.basename(relativeTarget);
799
+ const failures = [];
800
+ const retained = new Set();
801
+ let { stageName, backupName, displacedName } = state;
802
+ const publicPath = (name) => path.join(recoveryPath, name).split(path.sep).join("/");
803
+ const exactBaseline = () => Boolean(backupName && backupFd !== undefined && openMatches(recoveryFd, backupName, backupFd, FILE_FLAGS) && readDescriptor(backupFd).equals(baselineBytes));
804
+ const exactDisplaced = () => Boolean(displacedName && displacedFd !== undefined && openMatches(recoveryFd, displacedName, displacedFd, FILE_FLAGS));
805
+ const exactStage = () => Boolean(stageName && stageFd !== undefined && openMatches(recoveryFd, stageName, stageFd, FILE_FLAGS));
806
+ const remove = (name, fd, label) => {
807
+ try {
808
+ if (this.removeOwnedFile(recoveryFd, name, fd))
809
+ return true;
810
+ failures.push(`${label} '${publicPath(name)}' changed before cleanup`);
811
+ }
812
+ catch (error) {
813
+ failures.push(`${label} '${publicPath(name)}' cleanup failed: ${error.message}`);
814
+ }
815
+ retained.add(publicPath(name));
816
+ return false;
817
+ };
818
+ const sync = (label) => { try {
819
+ syncDirectory(directoryFd);
820
+ syncDirectory(recoveryFd);
821
+ return true;
822
+ }
823
+ catch (error) {
824
+ failures.push(`${label} directory fsync failed: ${error.message}`);
825
+ return false;
826
+ } };
827
+ const displacedBytes = exactDisplaced() ? readDescriptor(displacedFd) : undefined;
828
+ const displacedCompetitor = Boolean(displacedBytes && !displacedBytes.equals(baselineBytes));
829
+ const source = displacedCompetitor ? displacedName : exactBaseline() ? backupName : exactDisplaced() && displacedBytes?.equals(baselineBytes) ? displacedName : undefined;
830
+ const sourceFd = source === backupName ? backupFd : source === displacedName ? displacedFd : undefined;
831
+ if (stageFd !== undefined && openMatches(directoryFd, targetName, stageFd, FILE_FLAGS) && source && sourceFd !== undefined) {
832
+ try {
833
+ if (this.removeOwnedFile(directoryFd, targetName, stageFd))
834
+ sync("replacement removal");
835
+ else
836
+ failures.push(`published replacement '${relativeTarget}' changed before cleanup`);
837
+ }
838
+ catch (error) {
839
+ failures.push(`published replacement '${relativeTarget}' cleanup failed: ${error.message}`);
840
+ }
841
+ }
842
+ let restoredBaseline = pathBytesEqual(directoryFd, targetName, baselineBytes);
843
+ let restoredCompetitor = Boolean(displacedCompetitor && displacedFd !== undefined && openMatches(directoryFd, targetName, displacedFd, FILE_FLAGS));
844
+ let occupied = !restoredBaseline && !restoredCompetitor && entryExists(directoryFd, targetName);
845
+ if (!restoredBaseline && !restoredCompetitor && !occupied && source && sourceFd !== undefined) {
846
+ try {
847
+ fs.linkSync(fdPath(recoveryFd, source), fdPath(directoryFd, targetName));
848
+ }
849
+ catch (error) {
850
+ if (error.code !== "EEXIST")
851
+ failures.push(`restoration from '${publicPath(source)}' failed: ${error.message}`);
852
+ }
853
+ restoredBaseline = pathBytesEqual(directoryFd, targetName, baselineBytes);
854
+ restoredCompetitor = Boolean(displacedCompetitor && displacedFd !== undefined && openMatches(directoryFd, targetName, displacedFd, FILE_FLAGS));
855
+ occupied = !restoredBaseline && !restoredCompetitor && entryExists(directoryFd, targetName);
856
+ }
857
+ if (restoredBaseline || restoredCompetitor) {
858
+ const durable = sync(restoredBaseline ? "exact baseline restoration" : "competitor restoration");
859
+ if (durable) {
860
+ if (retainStage && exactStage())
861
+ retained.add(publicPath(stageName));
862
+ else if (exactStage() && remove(stageName, stageFd, "replacement stage"))
863
+ stageName = undefined;
864
+ if (restoredCompetitor) {
865
+ if (exactBaseline())
866
+ retained.add(publicPath(backupName));
867
+ else
868
+ failures.push("exact baseline snapshot is unavailable");
869
+ if (exactDisplaced() && remove(displacedName, displacedFd, "canonicalized competitor duplicate"))
870
+ displacedName = undefined;
871
+ }
872
+ else {
873
+ if (displacedCompetitor && exactDisplaced())
874
+ retained.add(publicPath(displacedName));
875
+ else if (exactDisplaced() && remove(displacedName, displacedFd, "displaced baseline duplicate"))
876
+ displacedName = undefined;
877
+ if (exactBaseline() && remove(backupName, backupFd, "baseline snapshot duplicate"))
878
+ backupName = undefined;
879
+ }
880
+ sync("restoration cleanup");
881
+ }
882
+ for (const name of [backupName, displacedName, stageName])
883
+ if (name && entryExists(recoveryFd, name))
884
+ retained.add(publicPath(name));
885
+ const outcome = restoredCompetitor
886
+ ? `recovery restored concurrent competitor bytes at '${relativeTarget}' and retained the exact discovered baseline snapshot at ${exactBaseline() ? `'${publicPath(backupName)}'` : "an unavailable path"}`
887
+ : `recovery restored byte-verified exact prior bytes at '${relativeTarget}'`;
888
+ return new Error(`${primary.message}; ${outcome}${retained.size ? `; retained recovery files: ${[...retained].join(", ")}` : ""}${failures.length ? `; recovery issues: ${failures.join("; ")}` : ""}`, { cause: primary });
889
+ }
890
+ if (occupied) {
891
+ if (exactBaseline())
892
+ retained.add(publicPath(backupName));
893
+ else
894
+ failures.push("exact baseline snapshot is unavailable");
895
+ if (displacedCompetitor && exactDisplaced())
896
+ retained.add(publicPath(displacedName));
897
+ if (sync("competing-target retention")) {
898
+ if (retainStage && exactStage())
899
+ retained.add(publicPath(stageName));
900
+ else if (exactStage() && remove(stageName, stageFd, "replacement stage"))
901
+ stageName = undefined;
902
+ if (!displacedCompetitor && exactDisplaced() && remove(displacedName, displacedFd, "displaced baseline duplicate"))
903
+ displacedName = undefined;
904
+ sync("retention cleanup");
905
+ }
906
+ for (const name of [backupName, displacedName, stageName])
907
+ if (name && entryExists(recoveryFd, name))
908
+ retained.add(publicPath(name));
909
+ return new Error(`${primary.message}; recovery preserved the competing canonical target and retained the exact discovered baseline snapshot and any displaced competitor at ${[...retained].join(", ") || "unavailable paths"}; preserve each role and resolve ownership explicitly${failures.length ? `; recovery issues: ${failures.join("; ")}` : ""}`, { cause: primary });
910
+ }
911
+ if (exactBaseline())
912
+ retained.add(publicPath(backupName));
913
+ if (exactDisplaced())
914
+ retained.add(publicPath(displacedName));
915
+ if (exactStage())
916
+ retained.add(publicPath(stageName));
917
+ return new Error(`${primary.message}; recovery could not restore the absent canonical target; retained role-labeled recovery files: ${[...retained].join(", ") || "none"}; restore the exact baseline or competitor explicitly before retrying${failures.length ? `; recovery issues: ${failures.join("; ")}` : ""}`, { cause: primary });
918
+ }
617
919
  close() {
618
920
  if (this.closed)
619
921
  return;
@@ -641,8 +943,9 @@ export class EntityPublicationContext {
641
943
  }
642
944
  removeOwnedFile(parentFd, name, expectedFd) {
643
945
  if (!openMatches(parentFd, name, expectedFd, FILE_FLAGS))
644
- return;
946
+ return false;
645
947
  fs.unlinkSync(fdPath(parentFd, name));
948
+ return true;
646
949
  }
647
950
  removeCreatedDirectories(directories) {
648
951
  for (const entry of [...directories].reverse()) {