contextos-agents 1.7.0 → 2.0.0-beta.3

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 (313) hide show
  1. package/.agents/AGENTS.md +16 -50
  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 +100 -104
  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 +537 -112
  26. package/.agents/customization-dx.js +282 -0
  27. package/.agents/doctor.js +855 -66
  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 +278 -64
  38. package/.agents/profiles.js +486 -51
  39. package/.agents/resolver/canonical-resolver.js +1348 -0
  40. package/.agents/resolver.js +50 -534
  41. package/.agents/rules/rule-catalog.js +525 -0
  42. package/.agents/schemas/attestation.review.v1.json +111 -0
  43. package/.agents/schemas/attestation.verification.v1.json +85 -0
  44. package/.agents/schemas/lockfile.v2.schema.json +134 -0
  45. package/.agents/schemas/profile.v2.schema.json +114 -0
  46. package/.agents/schemas/runtime.thread.v1.json +192 -0
  47. package/.agents/schemas/skill.manifest.v2.json +177 -0
  48. package/.agents/schemas/verification.spec.v1.json +39 -0
  49. package/.agents/schemas/workspace.graph.schema.json +106 -0
  50. package/.agents/skills-index.json +6 -166
  51. package/.agents/stats.js +9 -9
  52. package/.agents/transaction-core/event-store.js +288 -0
  53. package/.agents/transaction-core/idempotency.js +129 -0
  54. package/.agents/transaction-core/ipc-lock.js +311 -0
  55. package/.agents/transaction-core/plugin-supply-chain-bundle.js +436 -0
  56. package/.agents/validate.js +44 -1
  57. package/.agents/watch.js +354 -102
  58. package/.agents/workspace/workspace-graph.js +778 -0
  59. package/README.md +59 -387
  60. package/benchmarks/v2/analysis/statistics.js +140 -0
  61. package/benchmarks/v2/analysis/stats.js +69 -0
  62. package/benchmarks/v2/arms/arm-definitions.js +79 -0
  63. package/benchmarks/v2/dataset.schema.json +34 -0
  64. package/benchmarks/v2/evaluators/index.js +25 -0
  65. package/benchmarks/v2/evaluators/verified-success.js +116 -0
  66. package/benchmarks/v2/harness/runner.js +88 -0
  67. package/benchmarks/v2/pilot-tasks.json +392 -0
  68. package/bin/commands/recover.js +88 -0
  69. package/bin/commands/update.js +80 -17
  70. package/bin/commands.js +62 -25
  71. package/bin/index.js +138 -81
  72. package/bin/lib/lockfile.js +5 -3
  73. package/bin/lib/safe-writer.js +34 -3
  74. package/package.json +87 -72
  75. package/registry.json +2 -2
  76. package/registry.v2.schema.json +86 -0
  77. package/.agents/core/profiles/backend.yaml +0 -47
  78. package/.agents/core/profiles/enterprise.yaml +0 -46
  79. package/.agents/core/profiles/frontend.yaml +0 -46
  80. package/.agents/core/profiles/hackathon.yaml +0 -45
  81. package/.agents/core/profiles/mvp.yaml +0 -44
  82. package/.agents/core/profiles/startup.yaml +0 -48
  83. package/.agents/core/skills/adapters/EXAMPLES.md +0 -19
  84. package/.agents/core/skills/adapters/SKILL.md +0 -105
  85. package/.agents/core/skills/adapters/TROUBLESHOOTING.md +0 -7
  86. package/.agents/core/skills/adapters/VALIDATION.json +0 -12
  87. package/.agents/core/skills/adapters/skill.yaml +0 -16
  88. package/.agents/core/skills/architecture-diagrams/SKILL.md +0 -108
  89. package/.agents/core/skills/architecture-diagrams/VALIDATION.json +0 -12
  90. package/.agents/core/skills/architecture-diagrams/skill.yaml +0 -12
  91. package/.agents/core/skills/brutalist-design/SKILL.md +0 -150
  92. package/.agents/core/skills/brutalist-design/VALIDATION.json +0 -12
  93. package/.agents/core/skills/brutalist-design/skill.yaml +0 -12
  94. package/.agents/core/skills/database/EXAMPLES.md +0 -74
  95. package/.agents/core/skills/database/SKILL.md +0 -101
  96. package/.agents/core/skills/database/TROUBLESHOOTING.md +0 -18
  97. package/.agents/core/skills/database/VALIDATION.json +0 -11
  98. package/.agents/core/skills/database/skill.yaml +0 -31
  99. package/.agents/core/skills/ddd/EXAMPLES.md +0 -42
  100. package/.agents/core/skills/ddd/SKILL.md +0 -247
  101. package/.agents/core/skills/ddd/TROUBLESHOOTING.md +0 -19
  102. package/.agents/core/skills/ddd/VALIDATION.json +0 -12
  103. package/.agents/core/skills/ddd/ddd.md +0 -178
  104. package/.agents/core/skills/ddd/skill.yaml +0 -17
  105. package/.agents/core/skills/decisions/EXAMPLES.md +0 -35
  106. package/.agents/core/skills/decisions/SKILL.md +0 -90
  107. package/.agents/core/skills/decisions/TROUBLESHOOTING.md +0 -13
  108. package/.agents/core/skills/decisions/VALIDATION.json +0 -12
  109. package/.agents/core/skills/decisions/skill.yaml +0 -16
  110. package/.agents/core/skills/docker/EXAMPLES.md +0 -56
  111. package/.agents/core/skills/docker/SKILL.md +0 -63
  112. package/.agents/core/skills/docker/TROUBLESHOOTING.md +0 -18
  113. package/.agents/core/skills/docker/VALIDATION.json +0 -11
  114. package/.agents/core/skills/docker/skill.yaml +0 -29
  115. package/.agents/core/skills/fastapi/EXAMPLES.md +0 -36
  116. package/.agents/core/skills/fastapi/SKILL.md +0 -148
  117. package/.agents/core/skills/fastapi/TROUBLESHOOTING.md +0 -19
  118. package/.agents/core/skills/fastapi/VALIDATION.json +0 -12
  119. package/.agents/core/skills/fastapi/fastapi.md +0 -112
  120. package/.agents/core/skills/fastapi/skill.yaml +0 -17
  121. package/.agents/core/skills/generators/EXAMPLES.md +0 -19
  122. package/.agents/core/skills/generators/SKILL.md +0 -112
  123. package/.agents/core/skills/generators/TROUBLESHOOTING.md +0 -7
  124. package/.agents/core/skills/generators/VALIDATION.json +0 -12
  125. package/.agents/core/skills/generators/skill.yaml +0 -25
  126. package/.agents/core/skills/generators/templates/API.md +0 -77
  127. package/.agents/core/skills/generators/templates/ARCHITECTURE.md +0 -70
  128. package/.agents/core/skills/generators/templates/DATABASE.md +0 -42
  129. package/.agents/core/skills/generators/templates/DECISION.md +0 -46
  130. package/.agents/core/skills/generators/templates/PRD.md +0 -67
  131. package/.agents/core/skills/generators/templates/PROJECT_GRAPH.md +0 -56
  132. package/.agents/core/skills/generators/templates/ROADMAP.md +0 -51
  133. package/.agents/core/skills/generators/templates/TASKS.md +0 -43
  134. package/.agents/core/skills/generators/templates/UI.md +0 -73
  135. package/.agents/core/skills/graphify/EXAMPLES.md +0 -73
  136. package/.agents/core/skills/graphify/SKILL.md +0 -130
  137. package/.agents/core/skills/graphify/VALIDATION.json +0 -12
  138. package/.agents/core/skills/graphify/skill.yaml +0 -18
  139. package/.agents/core/skills/impeccable-design/EXAMPLES.md +0 -26
  140. package/.agents/core/skills/impeccable-design/SKILL.md +0 -201
  141. package/.agents/core/skills/impeccable-design/TROUBLESHOOTING.md +0 -19
  142. package/.agents/core/skills/impeccable-design/VALIDATION.json +0 -12
  143. package/.agents/core/skills/impeccable-design/skill.yaml +0 -20
  144. package/.agents/core/skills/interview-me/SKILL.md +0 -97
  145. package/.agents/core/skills/interview-me/VALIDATION.json +0 -12
  146. package/.agents/core/skills/interview-me/skill.yaml +0 -12
  147. package/.agents/core/skills/microservices/EXAMPLES.md +0 -38
  148. package/.agents/core/skills/microservices/SKILL.md +0 -164
  149. package/.agents/core/skills/microservices/TROUBLESHOOTING.md +0 -19
  150. package/.agents/core/skills/microservices/VALIDATION.json +0 -12
  151. package/.agents/core/skills/microservices/microservices.md +0 -119
  152. package/.agents/core/skills/microservices/skill.yaml +0 -17
  153. package/.agents/core/skills/minimalist-design/SKILL.md +0 -113
  154. package/.agents/core/skills/minimalist-design/VALIDATION.json +0 -12
  155. package/.agents/core/skills/minimalist-design/skill.yaml +0 -12
  156. package/.agents/core/skills/nestjs/EXAMPLES.md +0 -40
  157. package/.agents/core/skills/nestjs/SKILL.md +0 -139
  158. package/.agents/core/skills/nestjs/TROUBLESHOOTING.md +0 -19
  159. package/.agents/core/skills/nestjs/VALIDATION.json +0 -12
  160. package/.agents/core/skills/nestjs/nestjs.md +0 -103
  161. package/.agents/core/skills/nestjs/skill.yaml +0 -17
  162. package/.agents/core/skills/nextjs/EXAMPLES.md +0 -40
  163. package/.agents/core/skills/nextjs/SKILL.md +0 -163
  164. package/.agents/core/skills/nextjs/TROUBLESHOOTING.md +0 -19
  165. package/.agents/core/skills/nextjs/VALIDATION.json +0 -12
  166. package/.agents/core/skills/nextjs/nextjs.md +0 -67
  167. package/.agents/core/skills/nextjs/skill.yaml +0 -17
  168. package/.agents/core/skills/node/EXAMPLES.md +0 -80
  169. package/.agents/core/skills/node/SKILL.md +0 -128
  170. package/.agents/core/skills/node/TROUBLESHOOTING.md +0 -19
  171. package/.agents/core/skills/node/VALIDATION.json +0 -12
  172. package/.agents/core/skills/node/node.md +0 -87
  173. package/.agents/core/skills/node/skill.yaml +0 -17
  174. package/.agents/core/skills/performance/EXAMPLES.md +0 -30
  175. package/.agents/core/skills/performance/SKILL.md +0 -75
  176. package/.agents/core/skills/performance/TROUBLESHOOTING.md +0 -19
  177. package/.agents/core/skills/performance/VALIDATION.json +0 -12
  178. package/.agents/core/skills/performance/performance.md +0 -52
  179. package/.agents/core/skills/performance/skill.yaml +0 -17
  180. package/.agents/core/skills/react/EXAMPLES.md +0 -79
  181. package/.agents/core/skills/react/SKILL.md +0 -132
  182. package/.agents/core/skills/react/TROUBLESHOOTING.md +0 -19
  183. package/.agents/core/skills/react/VALIDATION.json +0 -12
  184. package/.agents/core/skills/react/react.md +0 -93
  185. package/.agents/core/skills/react/skill.yaml +0 -17
  186. package/.agents/core/skills/react-best-practices/SKILL.md +0 -155
  187. package/.agents/core/skills/react-best-practices/VALIDATION.json +0 -12
  188. package/.agents/core/skills/react-best-practices/skill.yaml +0 -14
  189. package/.agents/core/skills/redesign-audit/SKILL.md +0 -117
  190. package/.agents/core/skills/redesign-audit/VALIDATION.json +0 -12
  191. package/.agents/core/skills/redesign-audit/skill.yaml +0 -12
  192. package/.agents/core/skills/soft-design/SKILL.md +0 -108
  193. package/.agents/core/skills/soft-design/VALIDATION.json +0 -12
  194. package/.agents/core/skills/soft-design/skill.yaml +0 -12
  195. package/.agents/core/skills/state-management/EXAMPLES.md +0 -56
  196. package/.agents/core/skills/state-management/SKILL.md +0 -48
  197. package/.agents/core/skills/state-management/TROUBLESHOOTING.md +0 -18
  198. package/.agents/core/skills/state-management/VALIDATION.json +0 -11
  199. package/.agents/core/skills/state-management/skill.yaml +0 -28
  200. package/.agents/core/skills/subagent-orchestrator/SKILL.md +0 -117
  201. package/.agents/core/skills/subagent-orchestrator/VALIDATION.json +0 -12
  202. package/.agents/core/skills/subagent-orchestrator/skill.yaml +0 -12
  203. package/.agents/core/skills/system-design/EXAMPLES.md +0 -75
  204. package/.agents/core/skills/system-design/SKILL.md +0 -419
  205. package/.agents/core/skills/system-design/TROUBLESHOOTING.md +0 -19
  206. package/.agents/core/skills/system-design/VALIDATION.json +0 -12
  207. package/.agents/core/skills/system-design/skill.yaml +0 -20
  208. package/.agents/core/skills/system-design/system-design.md +0 -112
  209. package/.agents/core/skills/testing/EXAMPLES.md +0 -71
  210. package/.agents/core/skills/testing/SKILL.md +0 -70
  211. package/.agents/core/skills/testing/TROUBLESHOOTING.md +0 -18
  212. package/.agents/core/skills/testing/VALIDATION.json +0 -11
  213. package/.agents/core/skills/testing/skill.yaml +0 -32
  214. package/.agents/core/skills/typescript/EXAMPLES.md +0 -64
  215. package/.agents/core/skills/typescript/SKILL.md +0 -112
  216. package/.agents/core/skills/typescript/TROUBLESHOOTING.md +0 -19
  217. package/.agents/core/skills/typescript/VALIDATION.json +0 -12
  218. package/.agents/core/skills/typescript/skill.yaml +0 -17
  219. package/.agents/core/skills/typescript/typescript.md +0 -71
  220. package/.agents/core/skills/ui-design/EXAMPLES.md +0 -21
  221. package/.agents/core/skills/ui-design/SKILL.md +0 -124
  222. package/.agents/core/skills/ui-design/TROUBLESHOOTING.md +0 -19
  223. package/.agents/core/skills/ui-design/VALIDATION.json +0 -12
  224. package/.agents/core/skills/ui-design/skill.yaml +0 -17
  225. package/.agents/core/skills/ui-design/ui.md +0 -88
  226. package/.agents/core/skills/ui-ux-pro/EXAMPLES.md +0 -62
  227. package/.agents/core/skills/ui-ux-pro/SKILL.md +0 -375
  228. package/.agents/core/skills/ui-ux-pro/TROUBLESHOOTING.md +0 -19
  229. package/.agents/core/skills/ui-ux-pro/VALIDATION.json +0 -12
  230. package/.agents/core/skills/ui-ux-pro/skill.yaml +0 -19
  231. package/.agents/core/skills/ux-design/EXAMPLES.md +0 -36
  232. package/.agents/core/skills/ux-design/SKILL.md +0 -116
  233. package/.agents/core/skills/ux-design/TROUBLESHOOTING.md +0 -19
  234. package/.agents/core/skills/ux-design/VALIDATION.json +0 -12
  235. package/.agents/core/skills/ux-design/skill.yaml +0 -17
  236. package/.agents/core/skills/ux-design/ux.md +0 -80
  237. package/.agents/core/skills/vercel-optimize/SKILL.md +0 -83
  238. package/.agents/core/skills/vercel-optimize/VALIDATION.json +0 -12
  239. package/.agents/core/skills/vercel-optimize/scripts/collect-signals.mjs +0 -131
  240. package/.agents/core/skills/vercel-optimize/scripts/gate-investigations.mjs +0 -142
  241. package/.agents/core/skills/vercel-optimize/scripts/merge-signals.mjs +0 -143
  242. package/.agents/core/skills/vercel-optimize/scripts/scan-codebase.mjs +0 -174
  243. package/.agents/core/skills/vercel-optimize/skill.yaml +0 -18
  244. package/.agents/core/skills/web-accessibility/EXAMPLES.md +0 -39
  245. package/.agents/core/skills/web-accessibility/SKILL.md +0 -170
  246. package/.agents/core/skills/web-accessibility/TROUBLESHOOTING.md +0 -19
  247. package/.agents/core/skills/web-accessibility/VALIDATION.json +0 -12
  248. package/.agents/core/skills/web-accessibility/accessibility.md +0 -63
  249. package/.agents/core/skills/web-accessibility/skill.yaml +0 -17
  250. package/.agents/generated/claude/skills/adapters/SKILL.md +0 -126
  251. package/.agents/generated/claude/skills/architecture-diagrams/SKILL.md +0 -101
  252. package/.agents/generated/claude/skills/brutalist-design/SKILL.md +0 -145
  253. package/.agents/generated/claude/skills/database/SKILL.md +0 -191
  254. package/.agents/generated/claude/skills/ddd/SKILL.md +0 -305
  255. package/.agents/generated/claude/skills/decisions/SKILL.md +0 -134
  256. package/.agents/generated/claude/skills/docker/SKILL.md +0 -135
  257. package/.agents/generated/claude/skills/fastapi/SKILL.md +0 -200
  258. package/.agents/generated/claude/skills/generators/SKILL.md +0 -133
  259. package/.agents/generated/claude/skills/graphify/SKILL.md +0 -198
  260. package/.agents/generated/claude/skills/impeccable-design/SKILL.md +0 -241
  261. package/.agents/generated/claude/skills/interview-me/SKILL.md +0 -90
  262. package/.agents/generated/claude/skills/microservices/SKILL.md +0 -218
  263. package/.agents/generated/claude/skills/minimalist-design/SKILL.md +0 -108
  264. package/.agents/generated/claude/skills/nestjs/SKILL.md +0 -195
  265. package/.agents/generated/claude/skills/nextjs/SKILL.md +0 -219
  266. package/.agents/generated/claude/skills/node/SKILL.md +0 -224
  267. package/.agents/generated/claude/skills/performance/SKILL.md +0 -121
  268. package/.agents/generated/claude/skills/react/SKILL.md +0 -227
  269. package/.agents/generated/claude/skills/react-best-practices/SKILL.md +0 -146
  270. package/.agents/generated/claude/skills/redesign-audit/SKILL.md +0 -112
  271. package/.agents/generated/claude/skills/soft-design/SKILL.md +0 -103
  272. package/.agents/generated/claude/skills/state-management/SKILL.md +0 -120
  273. package/.agents/generated/claude/skills/subagent-orchestrator/SKILL.md +0 -110
  274. package/.agents/generated/claude/skills/system-design/SKILL.md +0 -507
  275. package/.agents/generated/claude/skills/testing/SKILL.md +0 -157
  276. package/.agents/generated/claude/skills/typescript/SKILL.md +0 -192
  277. package/.agents/generated/claude/skills/ui-design/SKILL.md +0 -161
  278. package/.agents/generated/claude/skills/ui-ux-pro/SKILL.md +0 -451
  279. package/.agents/generated/claude/skills/ux-design/SKILL.md +0 -168
  280. package/.agents/generated/claude/skills/vercel-optimize/SKILL.md +0 -76
  281. package/.agents/generated/claude/skills/web-accessibility/SKILL.md +0 -225
  282. package/.agents/generated/gemini/skills/adapters/SKILL.md +0 -135
  283. package/.agents/generated/gemini/skills/architecture-diagrams/SKILL.md +0 -107
  284. package/.agents/generated/gemini/skills/brutalist-design/SKILL.md +0 -151
  285. package/.agents/generated/gemini/skills/database/SKILL.md +0 -200
  286. package/.agents/generated/gemini/skills/ddd/SKILL.md +0 -314
  287. package/.agents/generated/gemini/skills/decisions/SKILL.md +0 -143
  288. package/.agents/generated/gemini/skills/docker/SKILL.md +0 -144
  289. package/.agents/generated/gemini/skills/fastapi/SKILL.md +0 -209
  290. package/.agents/generated/gemini/skills/generators/SKILL.md +0 -142
  291. package/.agents/generated/gemini/skills/graphify/SKILL.md +0 -205
  292. package/.agents/generated/gemini/skills/impeccable-design/SKILL.md +0 -250
  293. package/.agents/generated/gemini/skills/interview-me/SKILL.md +0 -96
  294. package/.agents/generated/gemini/skills/microservices/SKILL.md +0 -227
  295. package/.agents/generated/gemini/skills/minimalist-design/SKILL.md +0 -114
  296. package/.agents/generated/gemini/skills/nestjs/SKILL.md +0 -204
  297. package/.agents/generated/gemini/skills/nextjs/SKILL.md +0 -298
  298. package/.agents/generated/gemini/skills/node/SKILL.md +0 -323
  299. package/.agents/generated/gemini/skills/performance/SKILL.md +0 -185
  300. package/.agents/generated/gemini/skills/react/SKILL.md +0 -332
  301. package/.agents/generated/gemini/skills/react-best-practices/SKILL.md +0 -152
  302. package/.agents/generated/gemini/skills/redesign-audit/SKILL.md +0 -118
  303. package/.agents/generated/gemini/skills/soft-design/SKILL.md +0 -109
  304. package/.agents/generated/gemini/skills/state-management/SKILL.md +0 -129
  305. package/.agents/generated/gemini/skills/subagent-orchestrator/SKILL.md +0 -116
  306. package/.agents/generated/gemini/skills/system-design/SKILL.md +0 -631
  307. package/.agents/generated/gemini/skills/testing/SKILL.md +0 -166
  308. package/.agents/generated/gemini/skills/typescript/SKILL.md +0 -275
  309. package/.agents/generated/gemini/skills/ui-design/SKILL.md +0 -170
  310. package/.agents/generated/gemini/skills/ui-ux-pro/SKILL.md +0 -460
  311. package/.agents/generated/gemini/skills/ux-design/SKILL.md +0 -177
  312. package/.agents/generated/gemini/skills/vercel-optimize/SKILL.md +0 -82
  313. 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
+ };