contextos-agents 1.7.0 → 2.0.0-beta.2

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 (309) hide show
  1. package/.agents/AGENTS.md +1 -1
  2. package/.agents/adapters/aider/export.js +117 -99
  3. package/.agents/adapters/claude/export.js +68 -26
  4. package/.agents/adapters/copilot/export.js +90 -53
  5. package/.agents/adapters/cursor/export.js +80 -101
  6. package/.agents/adapters/drift-detector.js +196 -0
  7. package/.agents/adapters/gemini/export.js +76 -45
  8. package/.agents/adapters/pure-compiler.js +443 -0
  9. package/.agents/adapters/zed/export.js +104 -96
  10. package/.agents/compiled/registry.v2.json +504 -0
  11. package/.agents/compiled/registry.v2.sha256 +1 -0
  12. package/.agents/compiler/manifest-compiler.js +963 -0
  13. package/.agents/compiler/vendor/yaml.LICENSE.txt +13 -0
  14. package/.agents/compiler/vendor/yaml.SBOM.json +6 -0
  15. package/.agents/compiler/vendor/yaml.js +139 -0
  16. package/.agents/core/profiles/init.yaml +25 -0
  17. package/.agents/core/skills/context-manager/skill.yaml +3 -5
  18. package/.agents/core/skills/context-os/SKILL.md +3 -6
  19. package/.agents/core/skills/context-os/skill.yaml +3 -8
  20. package/.agents/core/skills/engineering-workflow/skill.yaml +1 -7
  21. package/.agents/core/skills/gemini-precision/skill.yaml +1 -6
  22. package/.agents/core/skills/gstack-roles/skill.yaml +3 -6
  23. package/.agents/core/skills/ponytail-mindset/skill.yaml +1 -7
  24. package/.agents/core/skills/security/skill.yaml +15 -3
  25. package/.agents/ctx.js +574 -111
  26. package/.agents/customization-dx.js +282 -0
  27. package/.agents/doctor.js +719 -64
  28. package/.agents/filesystem/index.js +71 -0
  29. package/.agents/filesystem/journaled-transaction.js +451 -0
  30. package/.agents/filesystem/lockfile-v2.js +275 -0
  31. package/.agents/filesystem/platform-hardening.js +222 -0
  32. package/.agents/filesystem/project-lock.js +218 -0
  33. package/.agents/filesystem/safe-path.js +256 -0
  34. package/.agents/generated/claude/skills/context-os/SKILL.md +1 -1
  35. package/.agents/generated/gemini/skills/context-os/SKILL.md +2 -2
  36. package/.agents/plugins/contextos/plugin.json +1 -1
  37. package/.agents/plugins.js +259 -60
  38. package/.agents/profiles.js +486 -51
  39. package/.agents/resolver.js +50 -534
  40. package/.agents/schemas/attestation.review.v1.json +111 -0
  41. package/.agents/schemas/attestation.verification.v1.json +85 -0
  42. package/.agents/schemas/lockfile.v2.schema.json +134 -0
  43. package/.agents/schemas/profile.v2.schema.json +114 -0
  44. package/.agents/schemas/runtime.thread.v1.json +192 -0
  45. package/.agents/schemas/skill.manifest.v2.json +177 -0
  46. package/.agents/schemas/verification.spec.v1.json +39 -0
  47. package/.agents/schemas/workspace.graph.schema.json +106 -0
  48. package/.agents/transaction-core/event-store.js +288 -0
  49. package/.agents/transaction-core/idempotency.js +129 -0
  50. package/.agents/transaction-core/ipc-lock.js +311 -0
  51. package/.agents/transaction-core/plugin-supply-chain-bundle.js +436 -0
  52. package/.agents/validate.js +44 -1
  53. package/.agents/watch.js +354 -102
  54. package/.agents/workspace/workspace-graph.js +778 -0
  55. package/README.md +59 -387
  56. package/benchmarks/v2/analysis/statistics.js +140 -0
  57. package/benchmarks/v2/analysis/stats.js +69 -0
  58. package/benchmarks/v2/arms/arm-definitions.js +79 -0
  59. package/benchmarks/v2/dataset.schema.json +34 -0
  60. package/benchmarks/v2/evaluators/index.js +25 -0
  61. package/benchmarks/v2/evaluators/verified-success.js +116 -0
  62. package/benchmarks/v2/harness/runner.js +88 -0
  63. package/benchmarks/v2/pilot-tasks.json +392 -0
  64. package/bin/commands/recover.js +88 -0
  65. package/bin/commands/update.js +80 -17
  66. package/bin/commands.js +62 -25
  67. package/bin/index.js +138 -81
  68. package/bin/lib/lockfile.js +5 -3
  69. package/bin/lib/safe-writer.js +34 -3
  70. package/package.json +85 -72
  71. package/registry.json +2 -2
  72. package/registry.v2.schema.json +86 -0
  73. package/.agents/core/profiles/backend.yaml +0 -47
  74. package/.agents/core/profiles/enterprise.yaml +0 -46
  75. package/.agents/core/profiles/frontend.yaml +0 -46
  76. package/.agents/core/profiles/hackathon.yaml +0 -45
  77. package/.agents/core/profiles/mvp.yaml +0 -44
  78. package/.agents/core/profiles/startup.yaml +0 -48
  79. package/.agents/core/skills/adapters/EXAMPLES.md +0 -19
  80. package/.agents/core/skills/adapters/SKILL.md +0 -105
  81. package/.agents/core/skills/adapters/TROUBLESHOOTING.md +0 -7
  82. package/.agents/core/skills/adapters/VALIDATION.json +0 -12
  83. package/.agents/core/skills/adapters/skill.yaml +0 -16
  84. package/.agents/core/skills/architecture-diagrams/SKILL.md +0 -108
  85. package/.agents/core/skills/architecture-diagrams/VALIDATION.json +0 -12
  86. package/.agents/core/skills/architecture-diagrams/skill.yaml +0 -12
  87. package/.agents/core/skills/brutalist-design/SKILL.md +0 -150
  88. package/.agents/core/skills/brutalist-design/VALIDATION.json +0 -12
  89. package/.agents/core/skills/brutalist-design/skill.yaml +0 -12
  90. package/.agents/core/skills/database/EXAMPLES.md +0 -74
  91. package/.agents/core/skills/database/SKILL.md +0 -101
  92. package/.agents/core/skills/database/TROUBLESHOOTING.md +0 -18
  93. package/.agents/core/skills/database/VALIDATION.json +0 -11
  94. package/.agents/core/skills/database/skill.yaml +0 -31
  95. package/.agents/core/skills/ddd/EXAMPLES.md +0 -42
  96. package/.agents/core/skills/ddd/SKILL.md +0 -247
  97. package/.agents/core/skills/ddd/TROUBLESHOOTING.md +0 -19
  98. package/.agents/core/skills/ddd/VALIDATION.json +0 -12
  99. package/.agents/core/skills/ddd/ddd.md +0 -178
  100. package/.agents/core/skills/ddd/skill.yaml +0 -17
  101. package/.agents/core/skills/decisions/EXAMPLES.md +0 -35
  102. package/.agents/core/skills/decisions/SKILL.md +0 -90
  103. package/.agents/core/skills/decisions/TROUBLESHOOTING.md +0 -13
  104. package/.agents/core/skills/decisions/VALIDATION.json +0 -12
  105. package/.agents/core/skills/decisions/skill.yaml +0 -16
  106. package/.agents/core/skills/docker/EXAMPLES.md +0 -56
  107. package/.agents/core/skills/docker/SKILL.md +0 -63
  108. package/.agents/core/skills/docker/TROUBLESHOOTING.md +0 -18
  109. package/.agents/core/skills/docker/VALIDATION.json +0 -11
  110. package/.agents/core/skills/docker/skill.yaml +0 -29
  111. package/.agents/core/skills/fastapi/EXAMPLES.md +0 -36
  112. package/.agents/core/skills/fastapi/SKILL.md +0 -148
  113. package/.agents/core/skills/fastapi/TROUBLESHOOTING.md +0 -19
  114. package/.agents/core/skills/fastapi/VALIDATION.json +0 -12
  115. package/.agents/core/skills/fastapi/fastapi.md +0 -112
  116. package/.agents/core/skills/fastapi/skill.yaml +0 -17
  117. package/.agents/core/skills/generators/EXAMPLES.md +0 -19
  118. package/.agents/core/skills/generators/SKILL.md +0 -112
  119. package/.agents/core/skills/generators/TROUBLESHOOTING.md +0 -7
  120. package/.agents/core/skills/generators/VALIDATION.json +0 -12
  121. package/.agents/core/skills/generators/skill.yaml +0 -25
  122. package/.agents/core/skills/generators/templates/API.md +0 -77
  123. package/.agents/core/skills/generators/templates/ARCHITECTURE.md +0 -70
  124. package/.agents/core/skills/generators/templates/DATABASE.md +0 -42
  125. package/.agents/core/skills/generators/templates/DECISION.md +0 -46
  126. package/.agents/core/skills/generators/templates/PRD.md +0 -67
  127. package/.agents/core/skills/generators/templates/PROJECT_GRAPH.md +0 -56
  128. package/.agents/core/skills/generators/templates/ROADMAP.md +0 -51
  129. package/.agents/core/skills/generators/templates/TASKS.md +0 -43
  130. package/.agents/core/skills/generators/templates/UI.md +0 -73
  131. package/.agents/core/skills/graphify/EXAMPLES.md +0 -73
  132. package/.agents/core/skills/graphify/SKILL.md +0 -130
  133. package/.agents/core/skills/graphify/VALIDATION.json +0 -12
  134. package/.agents/core/skills/graphify/skill.yaml +0 -18
  135. package/.agents/core/skills/impeccable-design/EXAMPLES.md +0 -26
  136. package/.agents/core/skills/impeccable-design/SKILL.md +0 -201
  137. package/.agents/core/skills/impeccable-design/TROUBLESHOOTING.md +0 -19
  138. package/.agents/core/skills/impeccable-design/VALIDATION.json +0 -12
  139. package/.agents/core/skills/impeccable-design/skill.yaml +0 -20
  140. package/.agents/core/skills/interview-me/SKILL.md +0 -97
  141. package/.agents/core/skills/interview-me/VALIDATION.json +0 -12
  142. package/.agents/core/skills/interview-me/skill.yaml +0 -12
  143. package/.agents/core/skills/microservices/EXAMPLES.md +0 -38
  144. package/.agents/core/skills/microservices/SKILL.md +0 -164
  145. package/.agents/core/skills/microservices/TROUBLESHOOTING.md +0 -19
  146. package/.agents/core/skills/microservices/VALIDATION.json +0 -12
  147. package/.agents/core/skills/microservices/microservices.md +0 -119
  148. package/.agents/core/skills/microservices/skill.yaml +0 -17
  149. package/.agents/core/skills/minimalist-design/SKILL.md +0 -113
  150. package/.agents/core/skills/minimalist-design/VALIDATION.json +0 -12
  151. package/.agents/core/skills/minimalist-design/skill.yaml +0 -12
  152. package/.agents/core/skills/nestjs/EXAMPLES.md +0 -40
  153. package/.agents/core/skills/nestjs/SKILL.md +0 -139
  154. package/.agents/core/skills/nestjs/TROUBLESHOOTING.md +0 -19
  155. package/.agents/core/skills/nestjs/VALIDATION.json +0 -12
  156. package/.agents/core/skills/nestjs/nestjs.md +0 -103
  157. package/.agents/core/skills/nestjs/skill.yaml +0 -17
  158. package/.agents/core/skills/nextjs/EXAMPLES.md +0 -40
  159. package/.agents/core/skills/nextjs/SKILL.md +0 -163
  160. package/.agents/core/skills/nextjs/TROUBLESHOOTING.md +0 -19
  161. package/.agents/core/skills/nextjs/VALIDATION.json +0 -12
  162. package/.agents/core/skills/nextjs/nextjs.md +0 -67
  163. package/.agents/core/skills/nextjs/skill.yaml +0 -17
  164. package/.agents/core/skills/node/EXAMPLES.md +0 -80
  165. package/.agents/core/skills/node/SKILL.md +0 -128
  166. package/.agents/core/skills/node/TROUBLESHOOTING.md +0 -19
  167. package/.agents/core/skills/node/VALIDATION.json +0 -12
  168. package/.agents/core/skills/node/node.md +0 -87
  169. package/.agents/core/skills/node/skill.yaml +0 -17
  170. package/.agents/core/skills/performance/EXAMPLES.md +0 -30
  171. package/.agents/core/skills/performance/SKILL.md +0 -75
  172. package/.agents/core/skills/performance/TROUBLESHOOTING.md +0 -19
  173. package/.agents/core/skills/performance/VALIDATION.json +0 -12
  174. package/.agents/core/skills/performance/performance.md +0 -52
  175. package/.agents/core/skills/performance/skill.yaml +0 -17
  176. package/.agents/core/skills/react/EXAMPLES.md +0 -79
  177. package/.agents/core/skills/react/SKILL.md +0 -132
  178. package/.agents/core/skills/react/TROUBLESHOOTING.md +0 -19
  179. package/.agents/core/skills/react/VALIDATION.json +0 -12
  180. package/.agents/core/skills/react/react.md +0 -93
  181. package/.agents/core/skills/react/skill.yaml +0 -17
  182. package/.agents/core/skills/react-best-practices/SKILL.md +0 -155
  183. package/.agents/core/skills/react-best-practices/VALIDATION.json +0 -12
  184. package/.agents/core/skills/react-best-practices/skill.yaml +0 -14
  185. package/.agents/core/skills/redesign-audit/SKILL.md +0 -117
  186. package/.agents/core/skills/redesign-audit/VALIDATION.json +0 -12
  187. package/.agents/core/skills/redesign-audit/skill.yaml +0 -12
  188. package/.agents/core/skills/soft-design/SKILL.md +0 -108
  189. package/.agents/core/skills/soft-design/VALIDATION.json +0 -12
  190. package/.agents/core/skills/soft-design/skill.yaml +0 -12
  191. package/.agents/core/skills/state-management/EXAMPLES.md +0 -56
  192. package/.agents/core/skills/state-management/SKILL.md +0 -48
  193. package/.agents/core/skills/state-management/TROUBLESHOOTING.md +0 -18
  194. package/.agents/core/skills/state-management/VALIDATION.json +0 -11
  195. package/.agents/core/skills/state-management/skill.yaml +0 -28
  196. package/.agents/core/skills/subagent-orchestrator/SKILL.md +0 -117
  197. package/.agents/core/skills/subagent-orchestrator/VALIDATION.json +0 -12
  198. package/.agents/core/skills/subagent-orchestrator/skill.yaml +0 -12
  199. package/.agents/core/skills/system-design/EXAMPLES.md +0 -75
  200. package/.agents/core/skills/system-design/SKILL.md +0 -419
  201. package/.agents/core/skills/system-design/TROUBLESHOOTING.md +0 -19
  202. package/.agents/core/skills/system-design/VALIDATION.json +0 -12
  203. package/.agents/core/skills/system-design/skill.yaml +0 -20
  204. package/.agents/core/skills/system-design/system-design.md +0 -112
  205. package/.agents/core/skills/testing/EXAMPLES.md +0 -71
  206. package/.agents/core/skills/testing/SKILL.md +0 -70
  207. package/.agents/core/skills/testing/TROUBLESHOOTING.md +0 -18
  208. package/.agents/core/skills/testing/VALIDATION.json +0 -11
  209. package/.agents/core/skills/testing/skill.yaml +0 -32
  210. package/.agents/core/skills/typescript/EXAMPLES.md +0 -64
  211. package/.agents/core/skills/typescript/SKILL.md +0 -112
  212. package/.agents/core/skills/typescript/TROUBLESHOOTING.md +0 -19
  213. package/.agents/core/skills/typescript/VALIDATION.json +0 -12
  214. package/.agents/core/skills/typescript/skill.yaml +0 -17
  215. package/.agents/core/skills/typescript/typescript.md +0 -71
  216. package/.agents/core/skills/ui-design/EXAMPLES.md +0 -21
  217. package/.agents/core/skills/ui-design/SKILL.md +0 -124
  218. package/.agents/core/skills/ui-design/TROUBLESHOOTING.md +0 -19
  219. package/.agents/core/skills/ui-design/VALIDATION.json +0 -12
  220. package/.agents/core/skills/ui-design/skill.yaml +0 -17
  221. package/.agents/core/skills/ui-design/ui.md +0 -88
  222. package/.agents/core/skills/ui-ux-pro/EXAMPLES.md +0 -62
  223. package/.agents/core/skills/ui-ux-pro/SKILL.md +0 -375
  224. package/.agents/core/skills/ui-ux-pro/TROUBLESHOOTING.md +0 -19
  225. package/.agents/core/skills/ui-ux-pro/VALIDATION.json +0 -12
  226. package/.agents/core/skills/ui-ux-pro/skill.yaml +0 -19
  227. package/.agents/core/skills/ux-design/EXAMPLES.md +0 -36
  228. package/.agents/core/skills/ux-design/SKILL.md +0 -116
  229. package/.agents/core/skills/ux-design/TROUBLESHOOTING.md +0 -19
  230. package/.agents/core/skills/ux-design/VALIDATION.json +0 -12
  231. package/.agents/core/skills/ux-design/skill.yaml +0 -17
  232. package/.agents/core/skills/ux-design/ux.md +0 -80
  233. package/.agents/core/skills/vercel-optimize/SKILL.md +0 -83
  234. package/.agents/core/skills/vercel-optimize/VALIDATION.json +0 -12
  235. package/.agents/core/skills/vercel-optimize/scripts/collect-signals.mjs +0 -131
  236. package/.agents/core/skills/vercel-optimize/scripts/gate-investigations.mjs +0 -142
  237. package/.agents/core/skills/vercel-optimize/scripts/merge-signals.mjs +0 -143
  238. package/.agents/core/skills/vercel-optimize/scripts/scan-codebase.mjs +0 -174
  239. package/.agents/core/skills/vercel-optimize/skill.yaml +0 -18
  240. package/.agents/core/skills/web-accessibility/EXAMPLES.md +0 -39
  241. package/.agents/core/skills/web-accessibility/SKILL.md +0 -170
  242. package/.agents/core/skills/web-accessibility/TROUBLESHOOTING.md +0 -19
  243. package/.agents/core/skills/web-accessibility/VALIDATION.json +0 -12
  244. package/.agents/core/skills/web-accessibility/accessibility.md +0 -63
  245. package/.agents/core/skills/web-accessibility/skill.yaml +0 -17
  246. package/.agents/generated/claude/skills/adapters/SKILL.md +0 -126
  247. package/.agents/generated/claude/skills/architecture-diagrams/SKILL.md +0 -101
  248. package/.agents/generated/claude/skills/brutalist-design/SKILL.md +0 -145
  249. package/.agents/generated/claude/skills/database/SKILL.md +0 -191
  250. package/.agents/generated/claude/skills/ddd/SKILL.md +0 -305
  251. package/.agents/generated/claude/skills/decisions/SKILL.md +0 -134
  252. package/.agents/generated/claude/skills/docker/SKILL.md +0 -135
  253. package/.agents/generated/claude/skills/fastapi/SKILL.md +0 -200
  254. package/.agents/generated/claude/skills/generators/SKILL.md +0 -133
  255. package/.agents/generated/claude/skills/graphify/SKILL.md +0 -198
  256. package/.agents/generated/claude/skills/impeccable-design/SKILL.md +0 -241
  257. package/.agents/generated/claude/skills/interview-me/SKILL.md +0 -90
  258. package/.agents/generated/claude/skills/microservices/SKILL.md +0 -218
  259. package/.agents/generated/claude/skills/minimalist-design/SKILL.md +0 -108
  260. package/.agents/generated/claude/skills/nestjs/SKILL.md +0 -195
  261. package/.agents/generated/claude/skills/nextjs/SKILL.md +0 -219
  262. package/.agents/generated/claude/skills/node/SKILL.md +0 -224
  263. package/.agents/generated/claude/skills/performance/SKILL.md +0 -121
  264. package/.agents/generated/claude/skills/react/SKILL.md +0 -227
  265. package/.agents/generated/claude/skills/react-best-practices/SKILL.md +0 -146
  266. package/.agents/generated/claude/skills/redesign-audit/SKILL.md +0 -112
  267. package/.agents/generated/claude/skills/soft-design/SKILL.md +0 -103
  268. package/.agents/generated/claude/skills/state-management/SKILL.md +0 -120
  269. package/.agents/generated/claude/skills/subagent-orchestrator/SKILL.md +0 -110
  270. package/.agents/generated/claude/skills/system-design/SKILL.md +0 -507
  271. package/.agents/generated/claude/skills/testing/SKILL.md +0 -157
  272. package/.agents/generated/claude/skills/typescript/SKILL.md +0 -192
  273. package/.agents/generated/claude/skills/ui-design/SKILL.md +0 -161
  274. package/.agents/generated/claude/skills/ui-ux-pro/SKILL.md +0 -451
  275. package/.agents/generated/claude/skills/ux-design/SKILL.md +0 -168
  276. package/.agents/generated/claude/skills/vercel-optimize/SKILL.md +0 -76
  277. package/.agents/generated/claude/skills/web-accessibility/SKILL.md +0 -225
  278. package/.agents/generated/gemini/skills/adapters/SKILL.md +0 -135
  279. package/.agents/generated/gemini/skills/architecture-diagrams/SKILL.md +0 -107
  280. package/.agents/generated/gemini/skills/brutalist-design/SKILL.md +0 -151
  281. package/.agents/generated/gemini/skills/database/SKILL.md +0 -200
  282. package/.agents/generated/gemini/skills/ddd/SKILL.md +0 -314
  283. package/.agents/generated/gemini/skills/decisions/SKILL.md +0 -143
  284. package/.agents/generated/gemini/skills/docker/SKILL.md +0 -144
  285. package/.agents/generated/gemini/skills/fastapi/SKILL.md +0 -209
  286. package/.agents/generated/gemini/skills/generators/SKILL.md +0 -142
  287. package/.agents/generated/gemini/skills/graphify/SKILL.md +0 -205
  288. package/.agents/generated/gemini/skills/impeccable-design/SKILL.md +0 -250
  289. package/.agents/generated/gemini/skills/interview-me/SKILL.md +0 -96
  290. package/.agents/generated/gemini/skills/microservices/SKILL.md +0 -227
  291. package/.agents/generated/gemini/skills/minimalist-design/SKILL.md +0 -114
  292. package/.agents/generated/gemini/skills/nestjs/SKILL.md +0 -204
  293. package/.agents/generated/gemini/skills/nextjs/SKILL.md +0 -298
  294. package/.agents/generated/gemini/skills/node/SKILL.md +0 -323
  295. package/.agents/generated/gemini/skills/performance/SKILL.md +0 -185
  296. package/.agents/generated/gemini/skills/react/SKILL.md +0 -332
  297. package/.agents/generated/gemini/skills/react-best-practices/SKILL.md +0 -152
  298. package/.agents/generated/gemini/skills/redesign-audit/SKILL.md +0 -118
  299. package/.agents/generated/gemini/skills/soft-design/SKILL.md +0 -109
  300. package/.agents/generated/gemini/skills/state-management/SKILL.md +0 -129
  301. package/.agents/generated/gemini/skills/subagent-orchestrator/SKILL.md +0 -116
  302. package/.agents/generated/gemini/skills/system-design/SKILL.md +0 -631
  303. package/.agents/generated/gemini/skills/testing/SKILL.md +0 -166
  304. package/.agents/generated/gemini/skills/typescript/SKILL.md +0 -275
  305. package/.agents/generated/gemini/skills/ui-design/SKILL.md +0 -170
  306. package/.agents/generated/gemini/skills/ui-ux-pro/SKILL.md +0 -460
  307. package/.agents/generated/gemini/skills/ux-design/SKILL.md +0 -177
  308. package/.agents/generated/gemini/skills/vercel-optimize/SKILL.md +0 -82
  309. package/.agents/generated/gemini/skills/web-accessibility/SKILL.md +0 -300
@@ -0,0 +1,275 @@
1
+ /**
2
+ * .agents/filesystem/lockfile-v2.js
3
+ * ContextOS Lockfile v2 Schema Manager & CAS Revision Tracker
4
+ *
5
+ * Implements:
6
+ * - Dual-hashing: Exact byte SHA-256 + CRLF-normalized semantic text SHA-256
7
+ * - Monotonic revision tracking with Compare-And-Swap (CAS) concurrency guard
8
+ * - Schema v2 validation adhering to .agents/schemas/lockfile.v2.schema.json
9
+ * - Generator provenance and artifact metadata management
10
+ */
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+ const crypto = require('crypto');
15
+ const { safeRenameSync } = require('./safe-path.js');
16
+
17
+ const DEFAULT_LOCKFILE_SUBPATH = path.join('.agents', 'lockfile.v2.json');
18
+ const SCHEMA_PATH = path.join(__dirname, '..', 'schemas', 'lockfile.v2.schema.json');
19
+
20
+ const ERROR_CODES = {
21
+ INVALID_SCHEMA: 'CTX_LOCKFILE_INVALID_SCHEMA',
22
+ REVISION_CONFLICT: 'CTX_LOCKFILE_REVISION_CONFLICT',
23
+ CORRUPT: 'CTX_LOCKFILE_CORRUPT',
24
+ };
25
+
26
+ class LockfileV2Error extends Error {
27
+ constructor(code, message, details = {}) {
28
+ super(message);
29
+ this.name = 'LockfileV2Error';
30
+ this.code = code;
31
+ this.details = details;
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Computes exact SHA-256 hash of raw byte buffer or string.
37
+ * Format: sha256:<hex>
38
+ */
39
+ function computeExactHash(content) {
40
+ const buf = Buffer.isBuffer(content) ? content : Buffer.from(String(content), 'utf8');
41
+ const hash = crypto.createHash('sha256').update(buf).digest('hex');
42
+ return `sha256:${hash}`;
43
+ }
44
+
45
+ /**
46
+ * Computes semantic CRLF-normalized text SHA-256 hash to prevent false dirty states across Git checkouts.
47
+ * Format: sha256:<hex>
48
+ */
49
+ function computeSemanticHash(content) {
50
+ const str = Buffer.isBuffer(content) ? content.toString('utf8') : String(content);
51
+ // Normalize Windows CRLF to LF and trim trailing carriage returns
52
+ const normalized = str.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
53
+ const hash = crypto.createHash('sha256').update(normalized, 'utf8').digest('hex');
54
+ return `sha256:${hash}`;
55
+ }
56
+
57
+ class LockfileV2Manager {
58
+ constructor(projectRoot, options = {}) {
59
+ this.projectRoot = path.resolve(projectRoot);
60
+ this.lockfilePath = options.lockfilePath
61
+ ? path.resolve(this.projectRoot, options.lockfilePath)
62
+ : path.resolve(this.projectRoot, DEFAULT_LOCKFILE_SUBPATH);
63
+ this.schema = this.loadSchema();
64
+ }
65
+
66
+ loadSchema() {
67
+ try {
68
+ if (fs.existsSync(SCHEMA_PATH)) {
69
+ return JSON.parse(fs.readFileSync(SCHEMA_PATH, 'utf8'));
70
+ }
71
+ } catch {
72
+ // Best-effort schema load
73
+ }
74
+ return null;
75
+ }
76
+
77
+ /**
78
+ * Initializes a fresh Lockfile v2 data structure.
79
+ */
80
+ createEmpty(meta = {}) {
81
+ return {
82
+ schemaVersion: 2,
83
+ revision: 1,
84
+ package: {
85
+ name: meta.packageName || 'contextos-project',
86
+ version: meta.packageVersion || '1.0.0',
87
+ compilerVersion: meta.compilerVersion || '2.0.0',
88
+ },
89
+ profile: {
90
+ id: meta.profileId || 'default',
91
+ hash: meta.profileHash || 'sha256:0000000000000000000000000000000000000000000000000000000000000000',
92
+ },
93
+ sourceGraphHash: meta.sourceGraphHash || 'sha256:0000000000000000000000000000000000000000000000000000000000000000',
94
+ enabledAdapters: meta.enabledAdapters || [],
95
+ managedFiles: {},
96
+ };
97
+ }
98
+
99
+ /**
100
+ * Reads and parses Lockfile v2 from disk. Returns null if file does not exist.
101
+ */
102
+ read() {
103
+ if (!fs.existsSync(this.lockfilePath)) {
104
+ return null;
105
+ }
106
+
107
+ try {
108
+ const raw = fs.readFileSync(this.lockfilePath, 'utf8');
109
+ const parsed = JSON.parse(raw);
110
+ this.validate(parsed);
111
+ return parsed;
112
+ } catch (err) {
113
+ if (err instanceof LockfileV2Error) throw err;
114
+ throw new LockfileV2Error(
115
+ ERROR_CODES.CORRUPT,
116
+ `Lockfile at '${this.lockfilePath}' is corrupt or unparseable: ${err.message}`,
117
+ { error: err.message, lockfilePath: this.lockfilePath }
118
+ );
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Validates lockfile structure against LockfileV2 specification.
124
+ */
125
+ validate(data) {
126
+ if (!data || typeof data !== 'object') {
127
+ throw new LockfileV2Error(ERROR_CODES.INVALID_SCHEMA, 'Lockfile data must be a non-null object');
128
+ }
129
+
130
+ if (data.schemaVersion !== 2) {
131
+ throw new LockfileV2Error(
132
+ ERROR_CODES.INVALID_SCHEMA,
133
+ `Expected schemaVersion 2, got '${data.schemaVersion}'`
134
+ );
135
+ }
136
+
137
+ if (typeof data.revision !== 'number' || data.revision < 1) {
138
+ throw new LockfileV2Error(
139
+ ERROR_CODES.INVALID_SCHEMA,
140
+ `Lockfile revision must be a positive integer, got '${data.revision}'`
141
+ );
142
+ }
143
+
144
+ if (!data.package || !data.package.name || !data.package.version) {
145
+ throw new LockfileV2Error(
146
+ ERROR_CODES.INVALID_SCHEMA,
147
+ 'Lockfile missing required package identity (name, version)'
148
+ );
149
+ }
150
+
151
+ if (!data.profile || !data.profile.id || !data.profile.hash) {
152
+ throw new LockfileV2Error(
153
+ ERROR_CODES.INVALID_SCHEMA,
154
+ 'Lockfile missing required profile metadata (id, hash)'
155
+ );
156
+ }
157
+
158
+ if (!Array.isArray(data.enabledAdapters)) {
159
+ throw new LockfileV2Error(
160
+ ERROR_CODES.INVALID_SCHEMA,
161
+ 'Lockfile enabledAdapters must be an array'
162
+ );
163
+ }
164
+
165
+ if (!data.managedFiles || typeof data.managedFiles !== 'object') {
166
+ throw new LockfileV2Error(
167
+ ERROR_CODES.INVALID_SCHEMA,
168
+ 'Lockfile managedFiles must be an object map'
169
+ );
170
+ }
171
+
172
+ // Validate individual file records
173
+ for (const [filePath, record] of Object.entries(data.managedFiles)) {
174
+ if (!record.exactSha256 || !record.semanticTextSha256 || !record.kind || !record.generator) {
175
+ throw new LockfileV2Error(
176
+ ERROR_CODES.INVALID_SCHEMA,
177
+ `Managed file '${filePath}' record is missing required fields (exactSha256, semanticTextSha256, kind, generator)`,
178
+ { filePath, record }
179
+ );
180
+ }
181
+ }
182
+
183
+ return true;
184
+ }
185
+
186
+ /**
187
+ * Saves lockfile to disk with optimistic concurrency Compare-And-Swap (CAS).
188
+ *
189
+ * @param {object} data - Lockfile data to write
190
+ * @param {number} [expectedRevision] - Optional expected revision on disk before increment
191
+ * @returns {object} Updated lockfile data with incremented revision
192
+ */
193
+ write(data, expectedRevision = undefined) {
194
+ const currentOnDisk = this.read();
195
+
196
+ if (expectedRevision !== undefined && currentOnDisk !== null) {
197
+ if (currentOnDisk.revision !== expectedRevision) {
198
+ throw new LockfileV2Error(
199
+ ERROR_CODES.REVISION_CONFLICT,
200
+ `Compare-And-Swap failed: disk revision is ${currentOnDisk.revision}, expected ${expectedRevision}`,
201
+ { actualRevision: currentOnDisk.revision, expectedRevision }
202
+ );
203
+ }
204
+ }
205
+
206
+ // Increment revision
207
+ const nextRevision = currentOnDisk ? currentOnDisk.revision + 1 : (data.revision || 1);
208
+ const dataToWrite = {
209
+ ...data,
210
+ schemaVersion: 2,
211
+ revision: nextRevision,
212
+ };
213
+
214
+ this.validate(dataToWrite);
215
+
216
+ const dir = path.dirname(this.lockfilePath);
217
+ if (!fs.existsSync(dir)) {
218
+ fs.mkdirSync(dir, { recursive: true });
219
+ }
220
+
221
+ const tmpPath = `${this.lockfilePath}.tmp-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
222
+ fs.writeFileSync(tmpPath, JSON.stringify(dataToWrite, null, 2) + '\n', 'utf8');
223
+
224
+ try {
225
+ safeRenameSync(tmpPath, this.lockfilePath);
226
+ } catch (err) {
227
+ try {
228
+ if (fs.existsSync(tmpPath)) fs.unlinkSync(tmpPath);
229
+ } catch {
230
+ // Best-effort cleanup
231
+ }
232
+ throw err;
233
+ }
234
+
235
+ return dataToWrite;
236
+ }
237
+
238
+ /**
239
+ * Adds or updates a managed file entry in memory.
240
+ */
241
+ recordManagedFile(data, relativePosixPath, fileMeta) {
242
+ if (!data.managedFiles) data.managedFiles = {};
243
+
244
+ data.managedFiles[relativePosixPath] = {
245
+ exactSha256: fileMeta.exactSha256,
246
+ semanticTextSha256: fileMeta.semanticTextSha256,
247
+ kind: fileMeta.kind || 'generated-adapter',
248
+ generator: fileMeta.generator,
249
+ inputsHash: fileMeta.inputsHash || 'sha256:0000000000000000000000000000000000000000000000000000000000000000',
250
+ mode: fileMeta.mode || 420,
251
+ lastTransaction: fileMeta.lastTransaction || undefined,
252
+ };
253
+
254
+ return data;
255
+ }
256
+
257
+ /**
258
+ * Removes a managed file entry from memory.
259
+ */
260
+ removeManagedFile(data, relativePosixPath) {
261
+ if (data.managedFiles && data.managedFiles[relativePosixPath]) {
262
+ delete data.managedFiles[relativePosixPath];
263
+ }
264
+ return data;
265
+ }
266
+ }
267
+
268
+ module.exports = {
269
+ LockfileV2Manager,
270
+ computeExactHash,
271
+ computeSemanticHash,
272
+ LockfileV2Error,
273
+ ERROR_CODES,
274
+ DEFAULT_LOCKFILE_SUBPATH,
275
+ };
@@ -0,0 +1,222 @@
1
+ /**
2
+ * .agents/filesystem/platform-hardening.js
3
+ * ContextOS — Cross-Platform Hardening & Resilience Engine
4
+ *
5
+ * Implements Section 21 of CONTEXTOS_IMPLEMENTATION_PLAN.md:
6
+ * - 21.1: Platform support contract (Node 18 Core, Node 20 Runtime, filesystem checks)
7
+ * - 21.2: Windows resilience (EPERM/EBUSY rename/unlink retry wrappers, ADS, reserved names)
8
+ * - 21.4: macOS APFS Unicode normalization (NFC standard)
9
+ * - 21.5: UNC / Network filesystem rejection (distributed lock safety)
10
+ * - 21.6: Platform-aware repository fingerprint (no blanket lowercase, git common dir, persisted UUID)
11
+ */
12
+
13
+ 'use strict';
14
+
15
+ const fs = require('fs');
16
+ const path = require('path');
17
+ const os = require('os');
18
+ const crypto = require('crypto');
19
+
20
+ function sha256(str) {
21
+ return crypto.createHash('sha256').update(str || '').digest('hex');
22
+ }
23
+
24
+ /**
25
+ * Checks if a given path is an unsupported UNC or Network Share (Section 21.5).
26
+ *
27
+ * @param {string} targetPath
28
+ * @returns {boolean} True if path is UNC or network share
29
+ */
30
+ function isNetworkOrUNCPath(targetPath) {
31
+ if (typeof targetPath !== 'string') return false;
32
+ const normalized = targetPath.replace(/\\/g, '/');
33
+
34
+ // UNC paths on Windows: //server/share or \\server\share
35
+ if (normalized.startsWith('//')) {
36
+ return true;
37
+ }
38
+
39
+ // Windows extended-length UNC: \\?\UNC\server\share
40
+ if (/^\\\\[?.]\\UNC\\/i.test(targetPath)) {
41
+ return true;
42
+ }
43
+
44
+ return false;
45
+ }
46
+
47
+ /**
48
+ * Validates platform runtime compatibility (Section 21.1).
49
+ *
50
+ * @param {Object} [options]
51
+ * @param {'core'|'runtime'} [options.component='core']
52
+ * @returns {{ supported: boolean, nodeVersion: number, required: number, issues: string[] }}
53
+ */
54
+ function validatePlatformSupport(options = {}) {
55
+ const component = options.component || 'core';
56
+ const nodeMajor = parseInt(process.versions.node.split('.')[0], 10);
57
+ const requiredMajor = component === 'runtime' ? 20 : 18;
58
+ const issues = [];
59
+
60
+ if (nodeMajor < requiredMajor) {
61
+ issues.push(
62
+ `Node.js version ${process.versions.node} is below required minimum v${requiredMajor}.x for ContextOS ${component}.`
63
+ );
64
+ }
65
+
66
+ return {
67
+ supported: issues.length === 0,
68
+ nodeVersion: nodeMajor,
69
+ required: requiredMajor,
70
+ issues,
71
+ };
72
+ }
73
+
74
+ /**
75
+ * Computes platform-aware repository fingerprint (Section 21.6).
76
+ * Preserves Linux case while normalizing Windows/macOS drive letters and APFS.
77
+ * Incorporates persisted repository UUID and Git common directory identity.
78
+ *
79
+ * @param {string} repoRoot
80
+ * @returns {string} SHA-256 fingerprint
81
+ */
82
+ function calculateRepositoryFingerprint(repoRoot) {
83
+ const resolved = path.resolve(repoRoot);
84
+ let canonicalPath = resolved;
85
+
86
+ if (process.platform === 'win32') {
87
+ // Normalize drive letter uppercase (e.g. C:) and forward slashes
88
+ canonicalPath = canonicalPath.replace(/^([a-zA-Z]):/, (_, drive) => drive.toUpperCase() + ':');
89
+ canonicalPath = canonicalPath.toLowerCase(); // Case-insensitive on Windows
90
+ } else if (process.platform === 'darwin') {
91
+ // APFS is typically case-insensitive, normalize Unicode NFC
92
+ canonicalPath = canonicalPath.normalize('NFC').toLowerCase();
93
+ }
94
+
95
+ // Persisted repository UUID
96
+ const stateDir = path.join(resolved, '.agents', '.contextos');
97
+ const uuidFile = path.join(stateDir, 'repo-id');
98
+ let repoUuid = '';
99
+
100
+ if (fs.existsSync(uuidFile)) {
101
+ try {
102
+ repoUuid = fs.readFileSync(uuidFile, 'utf8').trim();
103
+ } catch {}
104
+ }
105
+
106
+ if (!repoUuid) {
107
+ repoUuid = crypto.randomUUID();
108
+ try {
109
+ fs.mkdirSync(stateDir, { recursive: true });
110
+ fs.writeFileSync(uuidFile, repoUuid, 'utf8');
111
+ } catch {}
112
+ }
113
+
114
+ // Check Git directory identity if present
115
+ let gitIdentity = 'nogit';
116
+ const gitDir = path.join(resolved, '.git');
117
+ if (fs.existsSync(gitDir)) {
118
+ try {
119
+ const gitStat = fs.statSync(gitDir);
120
+ gitIdentity = `${gitStat.ino || 0}:${gitStat.dev || 0}`;
121
+ } catch {}
122
+ }
123
+
124
+ const payload = `${canonicalPath}|${repoUuid}|${gitIdentity}`;
125
+ return sha256(payload);
126
+ }
127
+
128
+ /**
129
+ * Normalizes Unicode text to NFC standard (Section 21.4).
130
+ *
131
+ * @param {string} text
132
+ * @returns {string}
133
+ */
134
+ function normalizeUnicodeNFC(text) {
135
+ if (typeof text !== 'string') return text;
136
+ return text.normalize('NFC');
137
+ }
138
+
139
+ /**
140
+ * Synchronously renames a file with exponential backoff retry for Windows antivirus/indexer contention (Section 21.2).
141
+ *
142
+ * @param {string} source
143
+ * @param {string} destination
144
+ * @param {Object} [options]
145
+ * @param {number} [options.maxRetries=5]
146
+ * @param {number} [options.retryDelayMs=20]
147
+ */
148
+ function safeRenameSync(source, destination, options = {}) {
149
+ const maxRetries = options.maxRetries || 5;
150
+ const retryDelayMs = options.retryDelayMs || 20;
151
+
152
+ let lastErr = null;
153
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
154
+ try {
155
+ if (process.platform === 'win32' && fs.existsSync(destination)) {
156
+ try { fs.unlinkSync(destination); } catch {}
157
+ }
158
+ fs.renameSync(source, destination);
159
+ return true;
160
+ } catch (err) {
161
+ lastErr = err;
162
+ if (err.code === 'EPERM' || err.code === 'EBUSY' || err.code === 'EACCES') {
163
+ if (attempt < maxRetries) {
164
+ const sleepMs = Math.floor(retryDelayMs * Math.pow(1.5, attempt));
165
+ // Busy-wait briefly for sync operation
166
+ const start = Date.now();
167
+ while (Date.now() - start < sleepMs) {}
168
+ continue;
169
+ }
170
+ }
171
+ throw err;
172
+ }
173
+ }
174
+
175
+ throw lastErr;
176
+ }
177
+
178
+ /**
179
+ * Synchronously unlinks a file with retry for transient Windows locks (Section 21.2).
180
+ *
181
+ * @param {string} targetPath
182
+ * @param {Object} [options]
183
+ * @param {number} [options.maxRetries=5]
184
+ * @param {number} [options.retryDelayMs=20]
185
+ */
186
+ function safeUnlinkSync(targetPath, options = {}) {
187
+ if (!fs.existsSync(targetPath)) return true;
188
+
189
+ const maxRetries = options.maxRetries || 5;
190
+ const retryDelayMs = options.retryDelayMs || 20;
191
+
192
+ let lastErr = null;
193
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
194
+ try {
195
+ fs.unlinkSync(targetPath);
196
+ return true;
197
+ } catch (err) {
198
+ lastErr = err;
199
+ if (err.code === 'EPERM' || err.code === 'EBUSY' || err.code === 'EACCES') {
200
+ if (attempt < maxRetries) {
201
+ const sleepMs = Math.floor(retryDelayMs * Math.pow(1.5, attempt));
202
+ const start = Date.now();
203
+ while (Date.now() - start < sleepMs) {}
204
+ continue;
205
+ }
206
+ }
207
+ if (!fs.existsSync(targetPath)) return true;
208
+ throw err;
209
+ }
210
+ }
211
+
212
+ throw lastErr;
213
+ }
214
+
215
+ module.exports = {
216
+ isNetworkOrUNCPath,
217
+ validatePlatformSupport,
218
+ calculateRepositoryFingerprint,
219
+ normalizeUnicodeNFC,
220
+ safeRenameSync,
221
+ safeUnlinkSync,
222
+ };
@@ -0,0 +1,218 @@
1
+ /**
2
+ * .agents/filesystem/project-lock.js
3
+ * ContextOS Project-Wide Inter-Process Mutation Lock
4
+ *
5
+ * Enforces single-writer mutual exclusion during compilation, export, and migration.
6
+ * - Atomic acquisition via exclusive filesystem creation (O_CREAT | O_EXCL / 'wx')
7
+ * - Cryptographic UUID token prevents zombie/stale processes from releasing new locks
8
+ * - Stale process detection via PID liveness probe (kill(pid, 0))
9
+ * - Structured machine-readable diagnostic output (CTX_PROJECT_BUSY)
10
+ */
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+ const os = require('os');
15
+ const crypto = require('crypto');
16
+ const { isNetworkOrUNCPath } = require('./platform-hardening.js');
17
+
18
+ const LOCK_SUBPATH = path.join('.agents', '.contextos', 'locks', 'mutation.lock');
19
+ const ERROR_CODES = {
20
+ BUSY: 'CTX_PROJECT_BUSY',
21
+ INVALID_TOKEN: 'CTX_LOCK_INVALID_TOKEN',
22
+ NOT_LOCKED: 'CTX_LOCK_NOT_LOCKED',
23
+ UNSUPPORTED: 'UNSUPPORTED',
24
+ };
25
+
26
+ class ProjectLockError extends Error {
27
+ constructor(code, message, details = {}) {
28
+ super(message);
29
+ this.name = 'ProjectLockError';
30
+ this.code = code;
31
+ this.details = details;
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Checks if a PID is alive on the local system.
37
+ */
38
+ function isPidAlive(pid) {
39
+ if (typeof pid !== 'number' || pid <= 0) return false;
40
+ try {
41
+ process.kill(pid, 0);
42
+ return true;
43
+ } catch (err) {
44
+ return err.code === 'EPERM'; // Process exists but lacks permission to signal
45
+ }
46
+ }
47
+
48
+ class ProjectMutationLock {
49
+ constructor(projectRoot, options = {}) {
50
+ if (isNetworkOrUNCPath(projectRoot)) {
51
+ throw new ProjectLockError(
52
+ ERROR_CODES.UNSUPPORTED,
53
+ `UNC/network filesystem mutation is unsupported for projectRoot: ${projectRoot}`,
54
+ { projectRoot, status: 'UNSUPPORTED' }
55
+ );
56
+ }
57
+ this.projectRoot = path.resolve(projectRoot);
58
+ this.lockPath = options.lockPath
59
+ ? path.resolve(this.projectRoot, options.lockPath)
60
+ : path.resolve(this.projectRoot, LOCK_SUBPATH);
61
+ this.lockDir = path.dirname(this.lockPath);
62
+ this.currentToken = null;
63
+ }
64
+
65
+ /**
66
+ * Reads and parses current lock payload, or null if lock doesn't exist.
67
+ */
68
+ inspect() {
69
+ try {
70
+ if (!fs.existsSync(this.lockPath)) return null;
71
+ const raw = fs.readFileSync(this.lockPath, 'utf8');
72
+ return JSON.parse(raw);
73
+ } catch {
74
+ return null;
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Checks if lock is currently held.
80
+ */
81
+ isLocked() {
82
+ return this.inspect() !== null;
83
+ }
84
+
85
+ /**
86
+ * Attempts to acquire the mutation lock exclusively.
87
+ *
88
+ * @param {object} [meta] - Context metadata
89
+ * @param {string} [meta.command='mutation'] - Triggering command
90
+ * @param {string} [meta.projectFingerprint=''] - Project identity fingerprint
91
+ * @param {boolean} [meta.autoReapStale=true] - Automatically reclaim lock if holder PID is dead
92
+ * @returns {string} The secret lock token required for release
93
+ */
94
+ acquire(meta = {}) {
95
+ if (!fs.existsSync(this.lockDir)) {
96
+ fs.mkdirSync(this.lockDir, { recursive: true });
97
+ }
98
+
99
+ const {
100
+ command = 'mutation',
101
+ projectFingerprint = '',
102
+ autoReapStale = true,
103
+ } = meta;
104
+
105
+ // Check for existing lock and inspect holder
106
+ const existing = this.inspect();
107
+ if (existing) {
108
+ if (autoReapStale && existing.pid && !isPidAlive(existing.pid)) {
109
+ // Holder PID is dead, safe to reap stale lock
110
+ this.forceUnlock(`Auto-reaped stale lock from dead PID ${existing.pid}`);
111
+ } else {
112
+ throw new ProjectLockError(
113
+ ERROR_CODES.BUSY,
114
+ `Project is busy: lock is held by PID ${existing.pid} (${existing.command || 'unknown'}) since ${existing.acquiredAt}`,
115
+ { lockDetails: existing }
116
+ );
117
+ }
118
+ }
119
+
120
+ const token = crypto.randomUUID();
121
+ const payload = {
122
+ token,
123
+ pid: process.pid,
124
+ processStartedAt: new Date(Date.now() - Math.floor(process.uptime() * 1000)).toISOString(),
125
+ hostname: os.hostname(),
126
+ command,
127
+ projectFingerprint,
128
+ acquiredAt: new Date().toISOString(),
129
+ };
130
+
131
+ let fd = null;
132
+ try {
133
+ // 'wx' flag opens for writing, failing if path already exists (O_CREAT | O_EXCL)
134
+ fd = fs.openSync(this.lockPath, 'wx');
135
+ fs.writeFileSync(fd, JSON.stringify(payload, null, 2), 'utf8');
136
+ fs.fsyncSync(fd);
137
+ } catch (err) {
138
+ if (err.code === 'EEXIST') {
139
+ const currentHolder = this.inspect();
140
+ throw new ProjectLockError(
141
+ ERROR_CODES.BUSY,
142
+ `Project is busy: lock acquired concurrently by another process`,
143
+ { lockDetails: currentHolder }
144
+ );
145
+ }
146
+ throw err;
147
+ } finally {
148
+ if (fd !== null) {
149
+ try {
150
+ fs.closeSync(fd);
151
+ } catch {
152
+ // Best effort close
153
+ }
154
+ }
155
+ }
156
+
157
+ this.currentToken = token;
158
+ return token;
159
+ }
160
+
161
+ /**
162
+ * Releases lock verifying ownership token.
163
+ *
164
+ * @param {string} [token] - Release token (defaults to currently held token)
165
+ */
166
+ release(token = this.currentToken) {
167
+ if (!token) {
168
+ throw new ProjectLockError(
169
+ ERROR_CODES.INVALID_TOKEN,
170
+ 'Cannot release lock: no token provided and no token actively held in session'
171
+ );
172
+ }
173
+
174
+ const current = this.inspect();
175
+ if (!current) {
176
+ this.currentToken = null;
177
+ return; // Already unlocked
178
+ }
179
+
180
+ if (current.token !== token) {
181
+ throw new ProjectLockError(
182
+ ERROR_CODES.INVALID_TOKEN,
183
+ `Token mismatch: current lock belongs to PID ${current.pid} acquired at ${current.acquiredAt}`,
184
+ { expectedToken: current.token, attemptedToken: token }
185
+ );
186
+ }
187
+
188
+ try {
189
+ fs.unlinkSync(this.lockPath);
190
+ } catch (err) {
191
+ if (err.code !== 'ENOENT') throw err;
192
+ }
193
+
194
+ this.currentToken = null;
195
+ }
196
+
197
+ /**
198
+ * Administratively forces removal of the lock.
199
+ */
200
+ forceUnlock(reason = 'Administrative force unlock') {
201
+ try {
202
+ if (fs.existsSync(this.lockPath)) {
203
+ fs.unlinkSync(this.lockPath);
204
+ }
205
+ } catch (err) {
206
+ if (err.code !== 'ENOENT') throw err;
207
+ }
208
+ this.currentToken = null;
209
+ return { success: true, reason };
210
+ }
211
+ }
212
+
213
+ module.exports = {
214
+ ProjectMutationLock,
215
+ ProjectLockError,
216
+ ERROR_CODES,
217
+ isPidAlive,
218
+ };