@cleocode/core 2026.9.8 → 2026.9.9

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 (561) hide show
  1. package/dist/audit/reconstruct.d.ts +13 -43
  2. package/dist/audit/reconstruct.d.ts.map +1 -1
  3. package/dist/audit/reconstruct.js +295 -290
  4. package/dist/audit/reconstruct.js.map +1 -1
  5. package/dist/bootstrap.d.ts.map +1 -1
  6. package/dist/bootstrap.js +8 -2
  7. package/dist/bootstrap.js.map +1 -1
  8. package/dist/dispatch/mutate-projection.d.ts.map +1 -1
  9. package/dist/dispatch/mutate-projection.js +12 -0
  10. package/dist/dispatch/mutate-projection.js.map +1 -1
  11. package/dist/dispatch/mvi-projection.d.ts +29 -6
  12. package/dist/dispatch/mvi-projection.d.ts.map +1 -1
  13. package/dist/dispatch/mvi-projection.js +109 -139
  14. package/dist/dispatch/mvi-projection.js.map +1 -1
  15. package/dist/docs/docs-read-model.d.ts +3 -2
  16. package/dist/docs/docs-read-model.d.ts.map +1 -1
  17. package/dist/docs/docs-read-model.js +4 -3
  18. package/dist/docs/docs-read-model.js.map +1 -1
  19. package/dist/docs/export-document.js +5100 -2297
  20. package/dist/docs/export-document.js.map +3 -3
  21. package/dist/docs/projection.d.ts +67 -0
  22. package/dist/docs/projection.d.ts.map +1 -0
  23. package/dist/docs/projection.js +370 -0
  24. package/dist/docs/projection.js.map +1 -0
  25. package/dist/doctor/acceptance-drift.d.ts +6 -2
  26. package/dist/doctor/acceptance-drift.d.ts.map +1 -1
  27. package/dist/doctor/acceptance-drift.js +43 -5
  28. package/dist/doctor/acceptance-drift.js.map +1 -1
  29. package/dist/doctor/knowledge-summary.d.ts +41 -0
  30. package/dist/doctor/knowledge-summary.d.ts.map +1 -0
  31. package/dist/doctor/knowledge-summary.js +81 -0
  32. package/dist/doctor/knowledge-summary.js.map +1 -0
  33. package/dist/doctor/knowledge.d.ts +192 -0
  34. package/dist/doctor/knowledge.d.ts.map +1 -0
  35. package/dist/doctor/knowledge.js +1864 -0
  36. package/dist/doctor/knowledge.js.map +1 -0
  37. package/dist/git/work-tree.d.ts +64 -0
  38. package/dist/git/work-tree.d.ts.map +1 -0
  39. package/dist/git/work-tree.js +111 -0
  40. package/dist/git/work-tree.js.map +1 -0
  41. package/dist/hooks/handlers/task-hooks.d.ts +8 -5
  42. package/dist/hooks/handlers/task-hooks.d.ts.map +1 -1
  43. package/dist/hooks/handlers/task-hooks.js +15 -60
  44. package/dist/hooks/handlers/task-hooks.js.map +1 -1
  45. package/dist/identity/cleo-identity.d.ts +13 -0
  46. package/dist/identity/cleo-identity.d.ts.map +1 -1
  47. package/dist/identity/cleo-identity.js +20 -6
  48. package/dist/identity/cleo-identity.js.map +1 -1
  49. package/dist/injection.d.ts.map +1 -1
  50. package/dist/injection.js +29 -13
  51. package/dist/injection.js.map +1 -1
  52. package/dist/internal.d.ts +2 -0
  53. package/dist/internal.d.ts.map +1 -1
  54. package/dist/internal.js +2 -0
  55. package/dist/internal.js.map +1 -1
  56. package/dist/lifecycle/verification/satisfies-validator.d.ts.map +1 -1
  57. package/dist/lifecycle/verification/satisfies-validator.js +124 -120
  58. package/dist/lifecycle/verification/satisfies-validator.js.map +1 -1
  59. package/dist/llm/plugin-facade.js +4018 -1726
  60. package/dist/llm/plugin-facade.js.map +3 -3
  61. package/dist/memory/brain-backfill.d.ts +27 -12
  62. package/dist/memory/brain-backfill.d.ts.map +1 -1
  63. package/dist/memory/brain-backfill.js +325 -134
  64. package/dist/memory/brain-backfill.js.map +1 -1
  65. package/dist/memory/brain-doctor.d.ts +21 -0
  66. package/dist/memory/brain-doctor.d.ts.map +1 -1
  67. package/dist/memory/brain-doctor.js +111 -53
  68. package/dist/memory/brain-doctor.js.map +1 -1
  69. package/dist/memory/brain-links.d.ts.map +1 -1
  70. package/dist/memory/brain-links.js +31 -4
  71. package/dist/memory/brain-links.js.map +1 -1
  72. package/dist/memory/brain-reasoning.d.ts.map +1 -1
  73. package/dist/memory/brain-reasoning.js +28 -2
  74. package/dist/memory/brain-reasoning.js.map +1 -1
  75. package/dist/memory/brain-reconciler.d.ts +14 -19
  76. package/dist/memory/brain-reconciler.d.ts.map +1 -1
  77. package/dist/memory/brain-reconciler.js +70 -97
  78. package/dist/memory/brain-reconciler.js.map +1 -1
  79. package/dist/memory/brain-search.d.ts +2 -0
  80. package/dist/memory/brain-search.d.ts.map +1 -1
  81. package/dist/memory/brain-search.js +59 -29
  82. package/dist/memory/brain-search.js.map +1 -1
  83. package/dist/memory/brain-similarity.d.ts.map +1 -1
  84. package/dist/memory/brain-similarity.js +5 -4
  85. package/dist/memory/brain-similarity.js.map +1 -1
  86. package/dist/memory/brain-stub-prune.d.ts +31 -9
  87. package/dist/memory/brain-stub-prune.d.ts.map +1 -1
  88. package/dist/memory/brain-stub-prune.js +65 -18
  89. package/dist/memory/brain-stub-prune.js.map +1 -1
  90. package/dist/memory/brain-writer-handlers.d.ts +10 -1
  91. package/dist/memory/brain-writer-handlers.d.ts.map +1 -1
  92. package/dist/memory/brain-writer-handlers.js +23 -3
  93. package/dist/memory/brain-writer-handlers.js.map +1 -1
  94. package/dist/memory/brain-writer-thread.d.ts +11 -1
  95. package/dist/memory/brain-writer-thread.d.ts.map +1 -1
  96. package/dist/memory/brain-writer-thread.js +87 -20
  97. package/dist/memory/brain-writer-thread.js.map +1 -1
  98. package/dist/memory/brain-writer-worker.js +8 -1
  99. package/dist/memory/brain-writer-worker.js.map +1 -1
  100. package/dist/memory/decision-cross-link.d.ts +39 -0
  101. package/dist/memory/decision-cross-link.d.ts.map +1 -1
  102. package/dist/memory/decision-cross-link.js +316 -0
  103. package/dist/memory/decision-cross-link.js.map +1 -1
  104. package/dist/memory/decisions.d.ts +4 -1
  105. package/dist/memory/decisions.d.ts.map +1 -1
  106. package/dist/memory/decisions.js +7 -24
  107. package/dist/memory/decisions.js.map +1 -1
  108. package/dist/memory/dispatch-trace.d.ts +12 -5
  109. package/dist/memory/dispatch-trace.d.ts.map +1 -1
  110. package/dist/memory/dispatch-trace.js +17 -7
  111. package/dist/memory/dispatch-trace.js.map +1 -1
  112. package/dist/memory/eligibility.d.ts +51 -0
  113. package/dist/memory/eligibility.d.ts.map +1 -0
  114. package/dist/memory/eligibility.js +77 -0
  115. package/dist/memory/eligibility.js.map +1 -0
  116. package/dist/memory/engine-compat.d.ts +2 -0
  117. package/dist/memory/engine-compat.d.ts.map +1 -1
  118. package/dist/memory/engine-compat.js +10 -0
  119. package/dist/memory/engine-compat.js.map +1 -1
  120. package/dist/memory/extraction-gate.d.ts +21 -7
  121. package/dist/memory/extraction-gate.d.ts.map +1 -1
  122. package/dist/memory/extraction-gate.js +178 -216
  123. package/dist/memory/extraction-gate.js.map +1 -1
  124. package/dist/memory/graph-auto-populate.d.ts +38 -5
  125. package/dist/memory/graph-auto-populate.d.ts.map +1 -1
  126. package/dist/memory/graph-auto-populate.js +182 -60
  127. package/dist/memory/graph-auto-populate.js.map +1 -1
  128. package/dist/memory/graph-memory-bridge.d.ts +14 -1
  129. package/dist/memory/graph-memory-bridge.d.ts.map +1 -1
  130. package/dist/memory/graph-memory-bridge.js +66 -28
  131. package/dist/memory/graph-memory-bridge.js.map +1 -1
  132. package/dist/memory/index.d.ts +1 -19
  133. package/dist/memory/index.d.ts.map +1 -1
  134. package/dist/memory/index.js +1 -50
  135. package/dist/memory/index.js.map +1 -1
  136. package/dist/memory/learnings.d.ts.map +1 -1
  137. package/dist/memory/learnings.js +2 -23
  138. package/dist/memory/learnings.js.map +1 -1
  139. package/dist/memory/manifest-filter.d.ts +21 -0
  140. package/dist/memory/manifest-filter.d.ts.map +1 -0
  141. package/dist/memory/manifest-filter.js +51 -0
  142. package/dist/memory/manifest-filter.js.map +1 -0
  143. package/dist/memory/manifest-ingestion.d.ts +28 -30
  144. package/dist/memory/manifest-ingestion.d.ts.map +1 -1
  145. package/dist/memory/manifest-ingestion.js +104 -235
  146. package/dist/memory/manifest-ingestion.js.map +1 -1
  147. package/dist/memory/observer-reflector.d.ts +2 -0
  148. package/dist/memory/observer-reflector.d.ts.map +1 -1
  149. package/dist/memory/observer-reflector.js +6 -49
  150. package/dist/memory/observer-reflector.js.map +1 -1
  151. package/dist/memory/patterns.d.ts +10 -0
  152. package/dist/memory/patterns.d.ts.map +1 -1
  153. package/dist/memory/patterns.js +25 -33
  154. package/dist/memory/patterns.js.map +1 -1
  155. package/dist/memory/pipeline-manifest-sqlite.d.ts +72 -9
  156. package/dist/memory/pipeline-manifest-sqlite.d.ts.map +1 -1
  157. package/dist/memory/pipeline-manifest-sqlite.js +508 -412
  158. package/dist/memory/pipeline-manifest-sqlite.js.map +1 -1
  159. package/dist/memory/retrieval/build-retrieval-bundle.d.ts.map +1 -1
  160. package/dist/memory/retrieval/build-retrieval-bundle.js +16 -15
  161. package/dist/memory/retrieval/build-retrieval-bundle.js.map +1 -1
  162. package/dist/memory/retrieval/fetch.d.ts +7 -0
  163. package/dist/memory/retrieval/fetch.d.ts.map +1 -1
  164. package/dist/memory/retrieval/fetch.js +14 -11
  165. package/dist/memory/retrieval/fetch.js.map +1 -1
  166. package/dist/memory/retrieval/observe.d.ts +7 -1
  167. package/dist/memory/retrieval/observe.d.ts.map +1 -1
  168. package/dist/memory/retrieval/observe.js +58 -4
  169. package/dist/memory/retrieval/observe.js.map +1 -1
  170. package/dist/memory/retrieval/search.d.ts +14 -1
  171. package/dist/memory/retrieval/search.d.ts.map +1 -1
  172. package/dist/memory/retrieval/search.js +133 -69
  173. package/dist/memory/retrieval/search.js.map +1 -1
  174. package/dist/memory/retrieval/telemetry.d.ts +31 -0
  175. package/dist/memory/retrieval/telemetry.d.ts.map +1 -0
  176. package/dist/memory/retrieval/telemetry.js +53 -0
  177. package/dist/memory/retrieval/telemetry.js.map +1 -0
  178. package/dist/nexus/analyze-orchestrator.d.ts +41 -9
  179. package/dist/nexus/analyze-orchestrator.d.ts.map +1 -1
  180. package/dist/nexus/analyze-orchestrator.js +257 -58
  181. package/dist/nexus/analyze-orchestrator.js.map +1 -1
  182. package/dist/nexus/api-contracts.d.ts +2 -6
  183. package/dist/nexus/api-contracts.d.ts.map +1 -1
  184. package/dist/nexus/api-contracts.js +11 -2
  185. package/dist/nexus/api-contracts.js.map +1 -1
  186. package/dist/nexus/context.d.ts.map +1 -1
  187. package/dist/nexus/context.js +14 -37
  188. package/dist/nexus/context.js.map +1 -1
  189. package/dist/nexus/diff.d.ts.map +1 -1
  190. package/dist/nexus/diff.js +2 -1
  191. package/dist/nexus/diff.js.map +1 -1
  192. package/dist/nexus/identity.d.ts +29 -3
  193. package/dist/nexus/identity.d.ts.map +1 -1
  194. package/dist/nexus/identity.js +63 -12
  195. package/dist/nexus/identity.js.map +1 -1
  196. package/dist/nexus/impact.d.ts +21 -19
  197. package/dist/nexus/impact.d.ts.map +1 -1
  198. package/dist/nexus/impact.js +96 -192
  199. package/dist/nexus/impact.js.map +1 -1
  200. package/dist/nexus/knowledge.d.ts +80 -0
  201. package/dist/nexus/knowledge.d.ts.map +1 -0
  202. package/dist/nexus/knowledge.js +786 -0
  203. package/dist/nexus/knowledge.js.map +1 -0
  204. package/dist/nexus/living-brain.d.ts +8 -5
  205. package/dist/nexus/living-brain.d.ts.map +1 -1
  206. package/dist/nexus/living-brain.js +252 -188
  207. package/dist/nexus/living-brain.js.map +1 -1
  208. package/dist/nexus/registry.d.ts +11 -2
  209. package/dist/nexus/registry.d.ts.map +1 -1
  210. package/dist/nexus/registry.js +144 -128
  211. package/dist/nexus/registry.js.map +1 -1
  212. package/dist/nexus/source-roots.d.ts +16 -0
  213. package/dist/nexus/source-roots.d.ts.map +1 -0
  214. package/dist/nexus/source-roots.js +174 -0
  215. package/dist/nexus/source-roots.js.map +1 -0
  216. package/dist/nexus/task-evidence.d.ts +22 -0
  217. package/dist/nexus/task-evidence.d.ts.map +1 -0
  218. package/dist/nexus/task-evidence.js +210 -0
  219. package/dist/nexus/task-evidence.js.map +1 -0
  220. package/dist/nexus/tasks-bridge.d.ts +14 -7
  221. package/dist/nexus/tasks-bridge.d.ts.map +1 -1
  222. package/dist/nexus/tasks-bridge.js +81 -40
  223. package/dist/nexus/tasks-bridge.js.map +1 -1
  224. package/dist/orchestrate/lifecycle-ops.d.ts.map +1 -1
  225. package/dist/orchestrate/lifecycle-ops.js +170 -120
  226. package/dist/orchestrate/lifecycle-ops.js.map +1 -1
  227. package/dist/orchestrate/query-ops.d.ts +6 -7
  228. package/dist/orchestrate/query-ops.d.ts.map +1 -1
  229. package/dist/orchestrate/query-ops.js +509 -532
  230. package/dist/orchestrate/query-ops.js.map +1 -1
  231. package/dist/orchestration/bootstrap.d.ts +15 -1
  232. package/dist/orchestration/bootstrap.d.ts.map +1 -1
  233. package/dist/orchestration/bootstrap.js +21 -10
  234. package/dist/orchestration/bootstrap.js.map +1 -1
  235. package/dist/orchestration/index.d.ts +35 -3
  236. package/dist/orchestration/index.d.ts.map +1 -1
  237. package/dist/orchestration/index.js +64 -35
  238. package/dist/orchestration/index.js.map +1 -1
  239. package/dist/orchestration/lead-rollup.d.ts +24 -1
  240. package/dist/orchestration/lead-rollup.d.ts.map +1 -1
  241. package/dist/orchestration/lead-rollup.js +31 -11
  242. package/dist/orchestration/lead-rollup.js.map +1 -1
  243. package/dist/orchestration/spawn-prompt.d.ts.map +1 -1
  244. package/dist/orchestration/spawn-prompt.js +16 -41
  245. package/dist/orchestration/spawn-prompt.js.map +1 -1
  246. package/dist/orchestration/unblock.d.ts +15 -1
  247. package/dist/orchestration/unblock.d.ts.map +1 -1
  248. package/dist/orchestration/unblock.js +23 -12
  249. package/dist/orchestration/unblock.js.map +1 -1
  250. package/dist/orchestration/validate-spawn.d.ts.map +1 -1
  251. package/dist/orchestration/validate-spawn.js +3 -3
  252. package/dist/orchestration/validate-spawn.js.map +1 -1
  253. package/dist/orchestration/waves.d.ts +29 -13
  254. package/dist/orchestration/waves.d.ts.map +1 -1
  255. package/dist/orchestration/waves.js +43 -55
  256. package/dist/orchestration/waves.js.map +1 -1
  257. package/dist/paths.d.ts +17 -163
  258. package/dist/paths.d.ts.map +1 -1
  259. package/dist/paths.js +145 -496
  260. package/dist/paths.js.map +1 -1
  261. package/dist/project-info.d.ts +2 -11
  262. package/dist/project-info.d.ts.map +1 -1
  263. package/dist/project-info.js +3 -31
  264. package/dist/project-info.js.map +1 -1
  265. package/dist/project-scope.d.ts +239 -0
  266. package/dist/project-scope.d.ts.map +1 -0
  267. package/dist/project-scope.js +473 -0
  268. package/dist/project-scope.js.map +1 -0
  269. package/dist/release/pr-evidence.d.ts +16 -3
  270. package/dist/release/pr-evidence.d.ts.map +1 -1
  271. package/dist/release/pr-evidence.js +53 -7
  272. package/dist/release/pr-evidence.js.map +1 -1
  273. package/dist/render/nexus/graph/index.d.ts.map +1 -1
  274. package/dist/render/nexus/graph/index.js +4 -2
  275. package/dist/render/nexus/graph/index.js.map +1 -1
  276. package/dist/render/orchestration/audit-reconstruct.d.ts +11 -0
  277. package/dist/render/orchestration/audit-reconstruct.d.ts.map +1 -1
  278. package/dist/render/orchestration/audit-reconstruct.js +21 -6
  279. package/dist/render/orchestration/audit-reconstruct.js.map +1 -1
  280. package/dist/render/tasks/show.js +2 -1
  281. package/dist/render/tasks/show.js.map +1 -1
  282. package/dist/resources/spawn-wrapper.d.ts +82 -14
  283. package/dist/resources/spawn-wrapper.d.ts.map +1 -1
  284. package/dist/resources/spawn-wrapper.js +585 -27
  285. package/dist/resources/spawn-wrapper.js.map +1 -1
  286. package/dist/selfimprove/fix-gen.js +4026 -1734
  287. package/dist/selfimprove/fix-gen.js.map +3 -3
  288. package/dist/sequence/index.d.ts.map +1 -1
  289. package/dist/sequence/index.js +76 -68
  290. package/dist/sequence/index.js.map +1 -1
  291. package/dist/session/canon-lint.d.ts +24 -3
  292. package/dist/session/canon-lint.d.ts.map +1 -1
  293. package/dist/session/canon-lint.js +72 -31
  294. package/dist/session/canon-lint.js.map +1 -1
  295. package/dist/session/engine-ops.d.ts +1 -4
  296. package/dist/session/engine-ops.d.ts.map +1 -1
  297. package/dist/session/engine-ops.js +6 -2
  298. package/dist/session/engine-ops.js.map +1 -1
  299. package/dist/sessions/briefing.d.ts +11 -1
  300. package/dist/sessions/briefing.d.ts.map +1 -1
  301. package/dist/sessions/briefing.js +76 -8
  302. package/dist/sessions/briefing.js.map +1 -1
  303. package/dist/sessions/handoff.d.ts +3 -1
  304. package/dist/sessions/handoff.d.ts.map +1 -1
  305. package/dist/sessions/handoff.js +2 -0
  306. package/dist/sessions/handoff.js.map +1 -1
  307. package/dist/shutdown-deadline.d.ts +24 -33
  308. package/dist/shutdown-deadline.d.ts.map +1 -1
  309. package/dist/shutdown-deadline.js +71 -14
  310. package/dist/shutdown-deadline.js.map +1 -1
  311. package/dist/shutdown.d.ts +21 -10
  312. package/dist/shutdown.d.ts.map +1 -1
  313. package/dist/shutdown.js +89 -42
  314. package/dist/shutdown.js.map +1 -1
  315. package/dist/skills/index.d.ts +1 -1
  316. package/dist/skills/index.d.ts.map +1 -1
  317. package/dist/skills/index.js +1 -1
  318. package/dist/skills/index.js.map +1 -1
  319. package/dist/skills/orchestrator/validator.d.ts +87 -20
  320. package/dist/skills/orchestrator/validator.d.ts.map +1 -1
  321. package/dist/skills/orchestrator/validator.js +330 -241
  322. package/dist/skills/orchestrator/validator.js.map +1 -1
  323. package/dist/store/agent-resolver.d.ts.map +1 -1
  324. package/dist/store/agent-resolver.js +16 -9
  325. package/dist/store/agent-resolver.js.map +1 -1
  326. package/dist/store/attachment-store.d.ts +4 -1
  327. package/dist/store/attachment-store.d.ts.map +1 -1
  328. package/dist/store/attachment-store.js +20 -3
  329. package/dist/store/attachment-store.js.map +1 -1
  330. package/dist/store/background-jobs.d.ts +426 -105
  331. package/dist/store/background-jobs.d.ts.map +1 -1
  332. package/dist/store/background-jobs.js +1208 -252
  333. package/dist/store/background-jobs.js.map +1 -1
  334. package/dist/store/background-ops.d.ts +145 -8
  335. package/dist/store/background-ops.d.ts.map +1 -1
  336. package/dist/store/background-ops.js +459 -15
  337. package/dist/store/background-ops.js.map +1 -1
  338. package/dist/store/backup-inspect.d.ts +48 -4
  339. package/dist/store/backup-inspect.d.ts.map +1 -1
  340. package/dist/store/backup-inspect.js +384 -4
  341. package/dist/store/backup-inspect.js.map +1 -1
  342. package/dist/store/converters.d.ts.map +1 -1
  343. package/dist/store/converters.js +19 -2
  344. package/dist/store/converters.js.map +1 -1
  345. package/dist/store/data-accessor.d.ts +9 -1
  346. package/dist/store/data-accessor.d.ts.map +1 -1
  347. package/dist/store/data-accessor.js +22 -5
  348. package/dist/store/data-accessor.js.map +1 -1
  349. package/dist/store/data-safety-central.d.ts.map +1 -1
  350. package/dist/store/data-safety-central.js +272 -232
  351. package/dist/store/data-safety-central.js.map +1 -1
  352. package/dist/store/db-helpers.d.ts +19 -3
  353. package/dist/store/db-helpers.d.ts.map +1 -1
  354. package/dist/store/db-helpers.js +42 -58
  355. package/dist/store/db-helpers.js.map +1 -1
  356. package/dist/store/dual-scope-db.d.ts +63 -4
  357. package/dist/store/dual-scope-db.d.ts.map +1 -1
  358. package/dist/store/dual-scope-db.js +166 -19
  359. package/dist/store/dual-scope-db.js.map +1 -1
  360. package/dist/store/exodus/archive.d.ts +21 -7
  361. package/dist/store/exodus/archive.d.ts.map +1 -1
  362. package/dist/store/exodus/archive.js +60 -18
  363. package/dist/store/exodus/archive.js.map +1 -1
  364. package/dist/store/exodus/migrate.d.ts.map +1 -1
  365. package/dist/store/exodus/migrate.js +27 -23
  366. package/dist/store/exodus/migrate.js.map +1 -1
  367. package/dist/store/exodus/on-open.d.ts +16 -27
  368. package/dist/store/exodus/on-open.d.ts.map +1 -1
  369. package/dist/store/exodus/on-open.js +121 -169
  370. package/dist/store/exodus/on-open.js.map +1 -1
  371. package/dist/store/exodus/recovery.d.ts +90 -0
  372. package/dist/store/exodus/recovery.d.ts.map +1 -0
  373. package/dist/store/exodus/recovery.js +434 -0
  374. package/dist/store/exodus/recovery.js.map +1 -0
  375. package/dist/store/exodus/types.d.ts +15 -0
  376. package/dist/store/exodus/types.d.ts.map +1 -1
  377. package/dist/store/memory-accessor.d.ts +52 -3
  378. package/dist/store/memory-accessor.d.ts.map +1 -1
  379. package/dist/store/memory-accessor.js +122 -7
  380. package/dist/store/memory-accessor.js.map +1 -1
  381. package/dist/store/nexus-sqlite.d.ts +24 -6
  382. package/dist/store/nexus-sqlite.d.ts.map +1 -1
  383. package/dist/store/nexus-sqlite.js +39 -20
  384. package/dist/store/nexus-sqlite.js.map +1 -1
  385. package/dist/store/ports/domain-binding.d.ts.map +1 -1
  386. package/dist/store/ports/domain-binding.js +27 -5
  387. package/dist/store/ports/domain-binding.js.map +1 -1
  388. package/dist/store/schema/background-jobs.d.ts +181 -2
  389. package/dist/store/schema/background-jobs.d.ts.map +1 -1
  390. package/dist/store/schema/background-jobs.js +27 -3
  391. package/dist/store/schema/background-jobs.js.map +1 -1
  392. package/dist/store/schema/cleo-global/skills.d.ts +2 -2
  393. package/dist/store/schema/cleo-project/lifecycle.d.ts +2 -2
  394. package/dist/store/schema/cleo-project/runtime.d.ts +1 -1
  395. package/dist/store/schema/cleo-project/tasks-core.d.ts +1 -1
  396. package/dist/store/schema/lifecycle.d.ts +2 -2
  397. package/dist/store/schema/skills-schema.d.ts +2 -2
  398. package/dist/store/schema/tasks.d.ts +1 -1
  399. package/dist/store/session-store.d.ts +3 -1
  400. package/dist/store/session-store.d.ts.map +1 -1
  401. package/dist/store/session-store.js +252 -207
  402. package/dist/store/session-store.js.map +1 -1
  403. package/dist/store/sqlite-data-accessor.d.ts +39 -4
  404. package/dist/store/sqlite-data-accessor.d.ts.map +1 -1
  405. package/dist/store/sqlite-data-accessor.js +689 -527
  406. package/dist/store/sqlite-data-accessor.js.map +1 -1
  407. package/dist/store/tasks-sqlite.d.ts +3 -1
  408. package/dist/store/tasks-sqlite.d.ts.map +1 -1
  409. package/dist/store/tasks-sqlite.js +309 -279
  410. package/dist/store/tasks-sqlite.js.map +1 -1
  411. package/dist/store/validation-schemas.d.ts +8 -8
  412. package/dist/store/writer-lease.d.ts +80 -8
  413. package/dist/store/writer-lease.d.ts.map +1 -1
  414. package/dist/store/writer-lease.js +503 -134
  415. package/dist/store/writer-lease.js.map +1 -1
  416. package/dist/system/audit.d.ts +2 -1
  417. package/dist/system/audit.d.ts.map +1 -1
  418. package/dist/system/audit.js +118 -71
  419. package/dist/system/audit.js.map +1 -1
  420. package/dist/system/backup.d.ts.map +1 -1
  421. package/dist/system/backup.js +5 -5
  422. package/dist/system/backup.js.map +1 -1
  423. package/dist/system/inject-generate.js +1 -1
  424. package/dist/task-work/index.d.ts +3 -0
  425. package/dist/task-work/index.d.ts.map +1 -1
  426. package/dist/task-work/index.js +34 -37
  427. package/dist/task-work/index.js.map +1 -1
  428. package/dist/tasks/ac-coverage-gate.d.ts.map +1 -1
  429. package/dist/tasks/ac-coverage-gate.js +4 -0
  430. package/dist/tasks/ac-coverage-gate.js.map +1 -1
  431. package/dist/tasks/ac-immutability.d.ts +7 -4
  432. package/dist/tasks/ac-immutability.d.ts.map +1 -1
  433. package/dist/tasks/ac-immutability.js +19 -17
  434. package/dist/tasks/ac-immutability.js.map +1 -1
  435. package/dist/tasks/ac-table.d.ts +5 -5
  436. package/dist/tasks/ac-table.d.ts.map +1 -1
  437. package/dist/tasks/ac-table.js +26 -12
  438. package/dist/tasks/ac-table.js.map +1 -1
  439. package/dist/tasks/acceptance-input.d.ts +52 -0
  440. package/dist/tasks/acceptance-input.d.ts.map +1 -0
  441. package/dist/tasks/acceptance-input.js +259 -0
  442. package/dist/tasks/acceptance-input.js.map +1 -0
  443. package/dist/tasks/add.d.ts +67 -29
  444. package/dist/tasks/add.d.ts.map +1 -1
  445. package/dist/tasks/add.js +255 -97
  446. package/dist/tasks/add.js.map +1 -1
  447. package/dist/tasks/complete.d.ts +16 -1
  448. package/dist/tasks/complete.d.ts.map +1 -1
  449. package/dist/tasks/complete.js +152 -55
  450. package/dist/tasks/complete.js.map +1 -1
  451. package/dist/tasks/decompose.d.ts +103 -0
  452. package/dist/tasks/decompose.d.ts.map +1 -0
  453. package/dist/tasks/decompose.js +224 -0
  454. package/dist/tasks/decompose.js.map +1 -0
  455. package/dist/tasks/delete.d.ts +5 -8
  456. package/dist/tasks/delete.d.ts.map +1 -1
  457. package/dist/tasks/delete.js +4 -3
  458. package/dist/tasks/delete.js.map +1 -1
  459. package/dist/tasks/dependency-check.d.ts +59 -0
  460. package/dist/tasks/dependency-check.d.ts.map +1 -1
  461. package/dist/tasks/dependency-check.js +73 -0
  462. package/dist/tasks/dependency-check.js.map +1 -1
  463. package/dist/tasks/deps-ready.d.ts +13 -1
  464. package/dist/tasks/deps-ready.d.ts.map +1 -1
  465. package/dist/tasks/deps-ready.js +16 -5
  466. package/dist/tasks/deps-ready.js.map +1 -1
  467. package/dist/tasks/engine-converters.d.ts +7 -0
  468. package/dist/tasks/engine-converters.d.ts.map +1 -1
  469. package/dist/tasks/engine-converters.js +8 -1
  470. package/dist/tasks/engine-converters.js.map +1 -1
  471. package/dist/tasks/evidence.d.ts +54 -26
  472. package/dist/tasks/evidence.d.ts.map +1 -1
  473. package/dist/tasks/evidence.js +292 -40
  474. package/dist/tasks/evidence.js.map +1 -1
  475. package/dist/tasks/find.d.ts +16 -12
  476. package/dist/tasks/find.d.ts.map +1 -1
  477. package/dist/tasks/find.js +107 -93
  478. package/dist/tasks/find.js.map +1 -1
  479. package/dist/tasks/gate-runner.d.ts +88 -14
  480. package/dist/tasks/gate-runner.d.ts.map +1 -1
  481. package/dist/tasks/gate-runner.js +596 -253
  482. package/dist/tasks/gate-runner.js.map +1 -1
  483. package/dist/tasks/hierarchy-policy.d.ts +8 -0
  484. package/dist/tasks/hierarchy-policy.d.ts.map +1 -1
  485. package/dist/tasks/hierarchy-policy.js +9 -2
  486. package/dist/tasks/hierarchy-policy.js.map +1 -1
  487. package/dist/tasks/hierarchy.d.ts +67 -1
  488. package/dist/tasks/hierarchy.d.ts.map +1 -1
  489. package/dist/tasks/hierarchy.js +81 -1
  490. package/dist/tasks/hierarchy.js.map +1 -1
  491. package/dist/tasks/index.d.ts +3 -0
  492. package/dist/tasks/index.d.ts.map +1 -1
  493. package/dist/tasks/index.js +3 -0
  494. package/dist/tasks/index.js.map +1 -1
  495. package/dist/tasks/infer-add-params.d.ts +1 -20
  496. package/dist/tasks/infer-add-params.d.ts.map +1 -1
  497. package/dist/tasks/infer-add-params.js +2 -203
  498. package/dist/tasks/infer-add-params.js.map +1 -1
  499. package/dist/tasks/list.d.ts +7 -2
  500. package/dist/tasks/list.d.ts.map +1 -1
  501. package/dist/tasks/list.js +24 -36
  502. package/dist/tasks/list.js.map +1 -1
  503. package/dist/tasks/ops.d.ts +6 -61
  504. package/dist/tasks/ops.d.ts.map +1 -1
  505. package/dist/tasks/ops.js +5 -53
  506. package/dist/tasks/ops.js.map +1 -1
  507. package/dist/tasks/plan.d.ts +15 -1
  508. package/dist/tasks/plan.d.ts.map +1 -1
  509. package/dist/tasks/plan.js +23 -25
  510. package/dist/tasks/plan.js.map +1 -1
  511. package/dist/tasks/population.d.ts +46 -0
  512. package/dist/tasks/population.d.ts.map +1 -0
  513. package/dist/tasks/population.js +88 -0
  514. package/dist/tasks/population.js.map +1 -0
  515. package/dist/tasks/reconcile-scope.d.ts +157 -0
  516. package/dist/tasks/reconcile-scope.d.ts.map +1 -0
  517. package/dist/tasks/reconcile-scope.js +242 -0
  518. package/dist/tasks/reconcile-scope.js.map +1 -0
  519. package/dist/tasks/req.d.ts +51 -12
  520. package/dist/tasks/req.d.ts.map +1 -1
  521. package/dist/tasks/req.js +125 -69
  522. package/dist/tasks/req.js.map +1 -1
  523. package/dist/tasks/session-scope.d.ts +6 -31
  524. package/dist/tasks/session-scope.d.ts.map +1 -1
  525. package/dist/tasks/session-scope.js +3 -17
  526. package/dist/tasks/session-scope.js.map +1 -1
  527. package/dist/tasks/severity-attestation.d.ts +39 -10
  528. package/dist/tasks/severity-attestation.d.ts.map +1 -1
  529. package/dist/tasks/severity-attestation.js +75 -36
  530. package/dist/tasks/severity-attestation.js.map +1 -1
  531. package/dist/tasks/task-next.d.ts +5 -0
  532. package/dist/tasks/task-next.d.ts.map +1 -1
  533. package/dist/tasks/task-next.js +10 -9
  534. package/dist/tasks/task-next.js.map +1 -1
  535. package/dist/tasks/task-reparent.d.ts.map +1 -1
  536. package/dist/tasks/task-reparent.js +3 -13
  537. package/dist/tasks/task-reparent.js.map +1 -1
  538. package/dist/tasks/update.d.ts +20 -43
  539. package/dist/tasks/update.d.ts.map +1 -1
  540. package/dist/tasks/update.js +64 -49
  541. package/dist/tasks/update.js.map +1 -1
  542. package/dist/validation/engine-ops.d.ts +17 -1
  543. package/dist/validation/engine-ops.d.ts.map +1 -1
  544. package/dist/validation/engine-ops.js +149 -11
  545. package/dist/validation/engine-ops.js.map +1 -1
  546. package/dist/workgraph/reparent-preflight.d.ts +1 -1
  547. package/dist/workgraph/reparent-preflight.d.ts.map +1 -1
  548. package/dist/workgraph/reparent-preflight.js +6 -1
  549. package/dist/workgraph/reparent-preflight.js.map +1 -1
  550. package/dist/workgraph/scaffold-apply.d.ts.map +1 -1
  551. package/dist/workgraph/scaffold-apply.js +81 -76
  552. package/dist/workgraph/scaffold-apply.js.map +1 -1
  553. package/dist/workgraph/structure-validation.d.ts +1 -1
  554. package/dist/workgraph/structure-validation.d.ts.map +1 -1
  555. package/dist/workgraph/structure-validation.js +6 -1
  556. package/dist/workgraph/structure-validation.js.map +1 -1
  557. package/migrations/drizzle-tasks/20260919180000_t12263-job-ownership-fences/migration.sql +34 -0
  558. package/migrations/drizzle-tasks/20260919184500_t12265-durable-pending-proposals/migration.sql +4 -0
  559. package/package.json +18 -13
  560. package/schemas/config.schema.json +2 -2
  561. package/templates/CLEO-INJECTION.md +103 -101
@@ -62,10 +62,14 @@
62
62
  * @see ./writer-lease-schema.ts — the drizzle table decls + bootstrap index assertion
63
63
  * @see ./dual-scope-db.ts — the open chokepoint this engine routes through (Seams 0 & 1)
64
64
  */
65
+ import { AsyncLocalStorage } from 'node:async_hooks';
65
66
  import { randomUUID } from 'node:crypto';
67
+ import { realpathSync, statSync } from 'node:fs';
66
68
  import { resolve as resolvePath } from 'node:path';
67
69
  import { getLogger } from '../logger.js';
68
- import { openDualScopeDb, openDualScopeDbAtPath, resolveDualScopeDbPath } from './dual-scope-db.js';
70
+ import { observeOperation } from './background-ops.js';
71
+ import { getDualScopeNativeDb, openDualScopeDb, openDualScopeDbAtPath, resolveDualScopeDbPath, } from './dual-scope-db.js';
72
+ import { openNativeDatabase } from './sqlite-native.js';
69
73
  import { assertWriterLeaseActiveIndexPresent, WRITER_LEASES_ACTIVE_INDEX, WRITER_LEASES_TABLE, WRITER_QUEUE_TABLE, } from './writer-lease-schema.js';
70
74
  /** Default lease TTL — aligned to `busy_timeout=30000` (specs/sqlite-pragmas.json). */
71
75
  export const DEFAULT_TTL_MS = 30_000;
@@ -167,25 +171,36 @@ export function _resetWriterLeaseStateForTest() {
167
171
  * the cached path-aware opener is used so the lease row lands in THAT file (the
168
172
  * multi-project-in-one-process case — Finding 1).
169
173
  */
170
- const defaultNativeDbResolver = async (scope, dbPath) => {
171
- let handle;
172
- if (dbPath !== undefined && dbPath !== resolveDualScopeDbPath(scope)) {
173
- // Explicit non-canonical path → open (cached) at that exact file.
174
- handle =
175
- scope === 'project'
176
- ? await openDualScopeDbAtPath('project', dbPath)
177
- : await openDualScopeDbAtPath('global', dbPath);
178
- }
179
- else {
180
- handle =
181
- scope === 'project' ? await openDualScopeDb('project') : await openDualScopeDb('global');
182
- }
183
- const native = handle.db.$client;
184
- // `handle.dbPath` is the path this open resolved; fall back to the scope→path
185
- // resolver defensively (the handle always carries it in practice).
186
- const resolvedPath = handle.dbPath ?? dbPath ?? resolveDualScopeDbPath(scope);
187
- return { native, dbPath: resolvedPath };
174
+ const defaultNativeDbResolver = async (scope, dbPath, execution) => {
175
+ execution?.assertActive();
176
+ const explicit = dbPath !== undefined && (execution !== undefined || dbPath !== resolveDualScopeDbPath(scope));
177
+ // Scoped leases already carry the exact database resource. Forward their lifetime
178
+ // directly through the existing opener API, avoiding the paths routing module's
179
+ // nexus/store dependency chain. Legacy canonical opens retain their exodus hook.
180
+ const handle = explicit
181
+ ? scope === 'project'
182
+ ? await openDualScopeDbAtPath('project', dbPath, undefined, { execution })
183
+ : await openDualScopeDbAtPath('global', dbPath, undefined, { execution })
184
+ : scope === 'project'
185
+ ? await openDualScopeDb('project', undefined, { execution })
186
+ : await openDualScopeDb('global', undefined, { execution });
187
+ return { native: getDualScopeNativeDb(handle), dbPath: handle.dbPath };
188
188
  };
189
+ /** Observe native resolution within the shared budget without pretending its opener was preempted. */
190
+ async function resolveLeaseTarget(scope, dbPath, execution) {
191
+ execution?.assertActive();
192
+ const pending = _nativeDbResolver(scope, dbPath, execution);
193
+ if (!execution)
194
+ return pending;
195
+ const observed = await observeOperation(execution, pending);
196
+ if (!observed.settled) {
197
+ throw new Error('Writer native resolution remains unresolved after operation cancellation; the opener may still finish', { cause: observed.reason });
198
+ }
199
+ if (!observed.success)
200
+ throw observed.error;
201
+ execution.assertActive();
202
+ return observed.value;
203
+ }
189
204
  let _nativeDbResolver = defaultNativeDbResolver;
190
205
  /**
191
206
  * Override how the engine obtains a scope's native `cleo.db` handle + path. Tests
@@ -205,8 +220,8 @@ export function _setNativeDbResolverForTest(resolver) {
205
220
  _nativeDbResolver = defaultNativeDbResolver;
206
221
  return;
207
222
  }
208
- _nativeDbResolver = async (scope, dbPath) => {
209
- const out = await resolver(scope, dbPath);
223
+ _nativeDbResolver = async (scope, dbPath, execution) => {
224
+ const out = await resolver(scope, dbPath, execution);
210
225
  // Adapt a bare native handle to a LeaseTarget with a stable synthetic path.
211
226
  return 'native' in out ? out : { native: out, dbPath: dbPath ?? scopePathToken(scope) };
212
227
  };
@@ -295,12 +310,20 @@ function canonicalizeDbPath(dbPath) {
295
310
  * @param scope - The cleo.db scope.
296
311
  * @param dbPath - An absolute or relative path to the scope's cleo.db. Normalized
297
312
  * via `path.resolve()` before freezing.
298
- * @returns A frozen, normalized identity.
313
+ * @returns A frozen, normalized identity, with device/inode when the file already exists.
314
+ * @remarks Required schema ownership refuses identities without captured file provenance.
315
+ * Missing abstract paths remain valid for legacy unscoped consumers; other stat errors propagate.
299
316
  *
300
317
  * @task T12042 (E6-L12b)
301
318
  */
302
319
  export function makeWriterLeaseIdentity(scope, dbPath) {
303
- return Object.freeze({ scope, dbPath: resolvePath(dbPath) });
320
+ const path = resolvePath(dbPath);
321
+ const file = statSync(path, { bigint: true, throwIfNoEntry: false });
322
+ return Object.freeze({
323
+ scope,
324
+ dbPath: path,
325
+ ...(file ? { fileDevice: String(file.dev), fileInode: String(file.ino) } : {}),
326
+ });
304
327
  }
305
328
  // ── DB-identity registry (typed WeakMap — immutable, concurrency-safe) ──────
306
329
  /**
@@ -336,8 +359,13 @@ const _dbIdentityRegistry = new WeakMap();
336
359
  export function registerDbIdentity(db, identity) {
337
360
  const existing = _dbIdentityRegistry.get(db);
338
361
  if (existing) {
339
- if (existing.scope === identity.scope && existing.dbPath === identity.dbPath) {
340
- return; // idempotent — same canonical identity
362
+ if (existing.scope === identity.scope &&
363
+ existing.dbPath === identity.dbPath &&
364
+ (existing.fileDevice === undefined ||
365
+ identity.fileDevice === undefined ||
366
+ (existing.fileDevice === identity.fileDevice && existing.fileInode === identity.fileInode))) {
367
+ // Never upgrade a legacy path-only identity using a later filesystem observation.
368
+ return; // idempotent — same canonical identity; original provenance remains immutable
341
369
  }
342
370
  throw new Error('E_WRITER_LEASE_IDENTITY_MISMATCH: the same drizzle DB handle was ' +
343
371
  `previously registered as ${existing.scope}::${existing.dbPath} ` +
@@ -437,6 +465,10 @@ class InternalLeaseHandle {
437
465
  heartbeat() {
438
466
  if (this.released)
439
467
  return;
468
+ if (this.nativeDb.isOpen && this.nativeDb.isTransaction) {
469
+ log().debug({ scope: this.scope, lane: this.lane }, 'Lease heartbeat deferred while another caller owns the native transaction');
470
+ return;
471
+ }
440
472
  // The heartbeat fires from an unref'd timer; if the underlying native handle
441
473
  // was closed out from under a still-held lease (cache eviction / shutdown /
442
474
  // test teardown), `prepare()` throws `database is not open` INSIDE the timer
@@ -468,9 +500,10 @@ class InternalLeaseHandle {
468
500
  * @param nativeDb - Handle to use for the release update. A caller may retry
469
501
  * with a freshly resolved handle when the lease's original shared handle
470
502
  * was closed mid-operation.
503
+ * @param guardTransaction - Refuse to borrow another caller's native transaction.
471
504
  * @returns True when the release update executed successfully.
472
505
  */
473
- releaseRow(nativeDb = this.nativeDb) {
506
+ releaseRow(nativeDb = this.nativeDb, guardTransaction = false) {
474
507
  if (!this.released) {
475
508
  this.released = true;
476
509
  if (this.heartbeatTimer) {
@@ -478,18 +511,46 @@ class InternalLeaseHandle {
478
511
  this.heartbeatTimer = null;
479
512
  }
480
513
  }
514
+ let began = false;
481
515
  try {
516
+ if (guardTransaction) {
517
+ nativeDb.exec('BEGIN IMMEDIATE');
518
+ began = true;
519
+ }
482
520
  nativeDb
483
521
  .prepare(`UPDATE ${WRITER_LEASES_TABLE} SET active = 0, reentrancy_depth = 0 ` +
484
522
  `WHERE scope = ? AND lane = ? AND holder_id = ? AND epoch = ? AND active = 1`)
485
523
  .run(this.scope, this.lane, this.holderId, this.epoch);
524
+ if (began)
525
+ nativeDb.exec('COMMIT');
486
526
  return true;
487
527
  }
488
528
  catch (err) {
529
+ if (began)
530
+ nativeDb.exec('ROLLBACK');
489
531
  log().debug({ scope: this.scope, lane: this.lane, err: err instanceof Error ? err.message : err }, 'writer-lease releaseRow failed (native handle likely closed); fresh-handle retry required');
490
532
  return false;
491
533
  }
492
534
  }
535
+ /** Adjust a shared grant using its existing connection only; never reopen during cleanup. */
536
+ releaseScopedDepth(depth) {
537
+ let began = false;
538
+ try {
539
+ this.nativeDb.exec('BEGIN IMMEDIATE');
540
+ began = true;
541
+ this.nativeDb
542
+ .prepare(`UPDATE ${WRITER_LEASES_TABLE} SET reentrancy_depth = ? WHERE scope = ? AND lane = ? AND holder_id = ? AND epoch = ? AND active = 1`)
543
+ .run(depth, this.scope, this.lane, this.holderId, this.epoch);
544
+ this.nativeDb.exec('COMMIT');
545
+ return true;
546
+ }
547
+ catch (error) {
548
+ if (began)
549
+ this.nativeDb.exec('ROLLBACK');
550
+ log().debug({ scope: this.scope, lane: this.lane, error }, 'Scoped lease depth cleanup deferred; original handle unavailable or owned');
551
+ return false;
552
+ }
553
+ }
493
554
  async release() {
494
555
  await releaseGrant(this.scope, this.dbPath, this.lane);
495
556
  }
@@ -551,6 +612,30 @@ export function hasActiveGrant(scope, lane, dbPath) {
551
612
  const entry = _grantMemo.get(memoKey(scope, normalized, lane));
552
613
  return entry !== undefined && entry.refcount > 0;
553
614
  }
615
+ /** Increment only a live captured grant, refusing borrowed native transactions for scoped callers. */
616
+ function incrementGrantDepth(native, scope, lane, entry, execution) {
617
+ execution?.assertActive();
618
+ if (execution)
619
+ native.exec('BEGIN IMMEDIATE');
620
+ try {
621
+ execution?.assertActive();
622
+ const result = native
623
+ .prepare(`UPDATE ${WRITER_LEASES_TABLE} SET reentrancy_depth = reentrancy_depth + 1 ` +
624
+ `WHERE scope = ? AND lane = ? AND holder_id = ? AND epoch = ? AND active = 1`)
625
+ .run(scope, lane, entry.handle.holderId, entry.handle.epoch);
626
+ if (execution && Number(result.changes) !== 1) {
627
+ throw new LeaseUnavailableError(scope, lane, 'captured grant is no longer active');
628
+ }
629
+ execution?.assertActive();
630
+ if (execution)
631
+ native.exec('COMMIT');
632
+ }
633
+ catch (error) {
634
+ if (execution)
635
+ native.exec('ROLLBACK');
636
+ throw error;
637
+ }
638
+ }
554
639
  /**
555
640
  * AC4 guard. Assert this process holds the `(scope, lane)` writer lease before it
556
641
  * opens a dedicated WRITE handle. In `off` mode the assertion is a no-op (there is
@@ -577,10 +662,12 @@ export function assertWriterLeaseHeld(scope, lane, dbPath) {
577
662
  * epoch on success, or `null` if a live holder owns the row (caller backs off and
578
663
  * retries; the queue row was enqueued on the first miss).
579
664
  */
580
- function tryClaimOnce(nativeDb, scope, lane, holderId, ttlMs) {
665
+ function tryClaimOnce(nativeDb, scope, lane, holderId, ttlMs, execution) {
666
+ execution?.assertActive();
581
667
  const now = Date.now();
582
668
  nativeDb.exec('BEGIN IMMEDIATE TRANSACTION');
583
669
  try {
670
+ execution?.assertActive();
584
671
  const active = nativeDb
585
672
  .prepare(`SELECT id, holder_id, holder_pid, epoch, heartbeat_at, ttl_ms, reentrancy_depth ` +
586
673
  `FROM ${WRITER_LEASES_TABLE} WHERE scope = ? AND lane = ? AND active = 1`)
@@ -590,6 +677,7 @@ function tryClaimOnce(nativeDb, scope, lane, holderId, ttlMs) {
590
677
  const nextEpoch = nativeDb
591
678
  .prepare(`SELECT COALESCE(MAX(epoch), 0) + 1 AS e FROM ${WRITER_LEASES_TABLE} WHERE scope = ? AND lane = ?`)
592
679
  .get(scope, lane)?.e ?? 1;
680
+ execution?.assertActive();
593
681
  nativeDb
594
682
  .prepare(`INSERT INTO ${WRITER_LEASES_TABLE} ` +
595
683
  `(scope, lane, holder_id, holder_pid, epoch, acquired_at, heartbeat_at, ttl_ms, reentrancy_depth, active) ` +
@@ -601,6 +689,7 @@ function tryClaimOnce(nativeDb, scope, lane, holderId, ttlMs) {
601
689
  if (active.holder_id === holderId) {
602
690
  // Same holder (defensive — same-process re-entrancy is handled by the memo
603
691
  // before reaching here). Bump the durable depth and re-assert the epoch.
692
+ execution?.assertActive();
604
693
  nativeDb
605
694
  .prepare(`UPDATE ${WRITER_LEASES_TABLE} SET reentrancy_depth = reentrancy_depth + 1, heartbeat_at = ? ` +
606
695
  `WHERE id = ?`)
@@ -614,6 +703,7 @@ function tryClaimOnce(nativeDb, scope, lane, holderId, ttlMs) {
614
703
  const stale = now - active.heartbeat_at > active.ttl_ms && !isPidAlive(active.holder_pid);
615
704
  if (stale) {
616
705
  const reclaimedEpoch = active.epoch + 1;
706
+ execution?.assertActive();
617
707
  nativeDb
618
708
  .prepare(`UPDATE ${WRITER_LEASES_TABLE} ` +
619
709
  `SET holder_id = ?, holder_pid = ?, epoch = ?, acquired_at = ?, heartbeat_at = ?, ttl_ms = ?, reentrancy_depth = 1 ` +
@@ -650,10 +740,22 @@ function enqueueWaiter(nativeDb, scope, lane, holderId, priority, ttlMs) {
650
740
  .run(scope, lane, holderId, priority, now, now + ttlMs);
651
741
  }
652
742
  /** Remove this holder's waiter row once granted (or on give-up). */
653
- function dequeueWaiter(nativeDb, scope, lane, holderId) {
654
- nativeDb
655
- .prepare(`DELETE FROM ${WRITER_QUEUE_TABLE} WHERE scope = ? AND lane = ? AND holder_id = ?`)
656
- .run(scope, lane, holderId);
743
+ function dequeueWaiter(nativeDb, scope, lane, holderId, guardTransaction = false) {
744
+ // Cleanup may run after cancellation, but it must never join another owner's transaction.
745
+ if (guardTransaction)
746
+ nativeDb.exec('BEGIN IMMEDIATE');
747
+ try {
748
+ nativeDb
749
+ .prepare(`DELETE FROM ${WRITER_QUEUE_TABLE} WHERE scope = ? AND lane = ? AND holder_id = ?`)
750
+ .run(scope, lane, holderId);
751
+ if (guardTransaction)
752
+ nativeDb.exec('COMMIT');
753
+ }
754
+ catch (error) {
755
+ if (guardTransaction)
756
+ nativeDb.exec('ROLLBACK');
757
+ throw error;
758
+ }
657
759
  }
658
760
  // ── Acquire / release surface ─────────────────────────────────────────────────
659
761
  /**
@@ -668,6 +770,13 @@ function dequeueWaiter(nativeDb, scope, lane, holderId) {
668
770
  * - `require` mode → throws {@link LeaseUnavailableError} if the row cannot be
669
771
  * taken within the acquire window (`min(ACQUIRE_DEADLINE_MS, ttlMs)`).
670
772
  *
773
+ * @remarks A supplied execution context preserves its original deadline and requires
774
+ * an explicit database path. Cancellation never grants degraded write permission.
775
+ * Unsettled native resolution is reported explicitly; its opener is not preempted.
776
+ * @example
777
+ * ```ts
778
+ * const lease = await acquireWriterLease('project', 'brain', options);
779
+ * ```
671
780
  * @param scope - The cleo.db scope.
672
781
  * @param lane - The write lane within the scope.
673
782
  * @param opts - Priority / TTL / re-entrancy options.
@@ -676,6 +785,10 @@ function dequeueWaiter(nativeDb, scope, lane, holderId) {
676
785
  * @task T11627
677
786
  */
678
787
  export async function acquireWriterLease(scope, lane, opts) {
788
+ opts?.execution?.assertActive();
789
+ if (opts?.execution && !opts.dbPath) {
790
+ throw new TypeError('Scoped writer acquisition requires an explicit database path');
791
+ }
679
792
  const mode = effectiveMode();
680
793
  const reentrant = opts?.reentrant ?? true;
681
794
  // `off` mode — pure pass-through. No row written, no memo entry; busy_timeout on
@@ -696,11 +809,10 @@ export async function acquireWriterLease(scope, lane, opts) {
696
809
  if (existing) {
697
810
  existing.refcount += 1;
698
811
  try {
699
- const { native } = await _nativeDbResolver(scope, dbPath);
700
- native
701
- .prepare(`UPDATE ${WRITER_LEASES_TABLE} SET reentrancy_depth = reentrancy_depth + 1 ` +
702
- `WHERE scope = ? AND lane = ? AND holder_id = ? AND epoch = ? AND active = 1`)
703
- .run(scope, lane, existing.handle.holderId, existing.handle.epoch);
812
+ opts?.execution?.assertActive();
813
+ const { native } = await resolveLeaseTarget(scope, dbPath, opts?.execution);
814
+ opts?.execution?.assertActive();
815
+ incrementGrantDepth(native, scope, lane, existing, opts?.execution);
704
816
  }
705
817
  catch (err) {
706
818
  // Native resolver or depth-update failed — roll back the refcount
@@ -709,14 +821,15 @@ export async function acquireWriterLease(scope, lane, opts) {
709
821
  existing.refcount -= 1;
710
822
  throw err;
711
823
  }
712
- return existing.handle;
824
+ return scopeLeaseRelease(existing.handle, dbPath, opts?.execution);
713
825
  }
714
826
  // Single-flight FIRST acquisition (Finding 2): if another caller is already
715
827
  // mid-acquire for this key, await its in-flight promise and share the grant
716
828
  // (refcount++) instead of racing a second full claim that would spin the whole
717
829
  // acquire window and degrade to a lease-less write. Decided synchronously here
718
830
  // — no `await` has run since the memo check above, so the two checks are atomic.
719
- const inflight = _inflightAcquire.get(key);
831
+ // Scoped callers must not inherit another caller's deadline or cancellation.
832
+ const inflight = opts?.execution ? undefined : _inflightAcquire.get(key);
720
833
  if (inflight) {
721
834
  const shared = await inflight;
722
835
  // Re-check the memo: the in-flight acquire may have already released (e.g.
@@ -725,17 +838,16 @@ export async function acquireWriterLease(scope, lane, opts) {
725
838
  if (entry && entry.handle === shared) {
726
839
  entry.refcount += 1;
727
840
  try {
728
- const { native } = await _nativeDbResolver(scope, dbPath);
729
- native
730
- .prepare(`UPDATE ${WRITER_LEASES_TABLE} SET reentrancy_depth = reentrancy_depth + 1 ` +
731
- `WHERE scope = ? AND lane = ? AND holder_id = ? AND epoch = ? AND active = 1`)
732
- .run(scope, lane, entry.handle.holderId, entry.handle.epoch);
841
+ opts?.execution?.assertActive();
842
+ const { native } = await resolveLeaseTarget(scope, dbPath, opts?.execution);
843
+ opts?.execution?.assertActive();
844
+ incrementGrantDepth(native, scope, lane, entry, opts?.execution);
733
845
  }
734
846
  catch (err) {
735
847
  entry.refcount -= 1;
736
848
  throw err;
737
849
  }
738
- return entry.handle;
850
+ return scopeLeaseRelease(entry.handle, dbPath, opts?.execution);
739
851
  }
740
852
  // The shared acquire is no longer active — fall through to a fresh acquire.
741
853
  }
@@ -745,11 +857,11 @@ export async function acquireWriterLease(scope, lane, opts) {
745
857
  // it. Only the reentrant path participates; reentrant:false callers (test cross-
746
858
  // holder contention) deliberately bypass the memo + single-flight.
747
859
  const acquirePromise = performFirstAcquire(scope, dbPath, lane, mode, key, opts);
748
- if (reentrant) {
860
+ if (reentrant && !opts?.execution) {
749
861
  _inflightAcquire.set(key, acquirePromise);
750
862
  }
751
863
  try {
752
- return await acquirePromise;
864
+ return scopeLeaseRelease(await acquirePromise, dbPath, opts?.execution);
753
865
  }
754
866
  finally {
755
867
  // Always clear our in-flight entry. A degraded acquire resolves to a no-op
@@ -773,7 +885,9 @@ export async function acquireWriterLease(scope, lane, opts) {
773
885
  * reentrant followers re-check the memo against before sharing.
774
886
  */
775
887
  async function performFirstAcquire(scope, dbPath, lane, mode, key, opts) {
776
- const { native } = await _nativeDbResolver(scope, dbPath);
888
+ opts?.execution?.assertActive();
889
+ const { native } = await resolveLeaseTarget(scope, dbPath, opts?.execution);
890
+ opts?.execution?.assertActive();
777
891
  // Defensive bootstrap assert: the partial-unique active index MUST exist or AC1
778
892
  // is unenforced. Cheap single-row sqlite_master lookup, runs on first acquire.
779
893
  assertWriterLeaseActiveIndexPresent(native);
@@ -784,66 +898,116 @@ async function performFirstAcquire(scope, dbPath, lane, mode, key, opts) {
784
898
  // not block for a grant longer than the lease itself would live. busy_timeout on
785
899
  // the IMMEDIATE lock still backstops every individual claim attempt.
786
900
  const acquireWindowMs = Math.min(ACQUIRE_DEADLINE_MS, Math.max(1, ttlMs));
787
- const deadline = Date.now() + acquireWindowMs;
901
+ const deadline = Math.min(Date.now() + acquireWindowMs, opts?.execution?.deadlineAt ?? Infinity);
788
902
  let enqueued = false;
789
- for (;;) {
790
- const epoch = tryClaimOnce(native, scope, lane, holderId, ttlMs);
791
- if (epoch !== null) {
792
- if (enqueued)
793
- dequeueWaiter(native, scope, lane, holderId);
794
- const handle = new InternalLeaseHandle({
795
- scope,
796
- lane,
797
- epoch,
798
- holderId,
799
- dbPath,
800
- nativeDb: native,
801
- ttlMs,
802
- });
803
- _grantMemo.set(key, { handle, refcount: 1 });
804
- return handle;
805
- }
806
- // Contended by a live holder. Enqueue once for ordering/aging, then back off.
807
- if (!enqueued) {
808
- enqueueWaiter(native, scope, lane, holderId, priority, ttlMs);
809
- enqueued = true;
810
- }
811
- if (Date.now() >= deadline) {
812
- if (enqueued)
813
- dequeueWaiter(native, scope, lane, holderId);
814
- if (mode === 'require') {
815
- throw new LeaseUnavailableError(scope, lane, 'live holder did not release within deadline');
903
+ try {
904
+ for (;;) {
905
+ opts?.execution?.assertActive();
906
+ const epoch = tryClaimOnce(native, scope, lane, holderId, ttlMs, opts?.execution);
907
+ if (epoch !== null) {
908
+ if (enqueued)
909
+ dequeueWaiter(native, scope, lane, holderId, opts?.execution !== undefined);
910
+ const handle = new InternalLeaseHandle({
911
+ scope,
912
+ lane,
913
+ epoch,
914
+ holderId,
915
+ dbPath,
916
+ nativeDb: native,
917
+ ttlMs,
918
+ });
919
+ try {
920
+ opts?.execution?.assertActive();
921
+ }
922
+ catch (error) {
923
+ handle.releaseRow();
924
+ throw error;
925
+ }
926
+ _grantMemo.set(key, { handle, refcount: 1 });
927
+ return handle;
816
928
  }
817
- // local: degrade to today's behaviour — proceed without a lease. busy_timeout
818
- // on the connection still serializes the actual write. A no-op handle writes
819
- // no row, starts no heartbeat, and is never memoized — so single-flight
820
- // followers find no memo entry and acquire freshly.
821
- const active = native
822
- .prepare(`SELECT holder_id, holder_pid, epoch, heartbeat_at, ttl_ms ` +
823
- `FROM ${WRITER_LEASES_TABLE} WHERE scope = ? AND lane = ? AND active = 1`)
824
- .get(scope, lane);
825
- log().warn({
826
- scope,
827
- lane,
828
- dbPath,
829
- activeHolderId: active?.holder_id,
830
- activeHolderPid: active?.holder_pid,
831
- activeEpoch: active?.epoch,
832
- activeHeartbeatAgeMs: active === undefined ? undefined : Math.max(0, Date.now() - active.heartbeat_at),
833
- }, 'writer-lease acquire deadline exceeded; proceeding under busy_timeout fallback (degraded)');
834
- return makeNoopHandle(scope, lane);
929
+ // Contended by a live holder. Enqueue once for ordering/aging, then back off.
930
+ if (!enqueued) {
931
+ opts?.execution?.assertActive();
932
+ enqueueWaiter(native, scope, lane, holderId, priority, ttlMs);
933
+ enqueued = true;
934
+ }
935
+ if (Date.now() >= deadline) {
936
+ if (enqueued)
937
+ dequeueWaiter(native, scope, lane, holderId, opts?.execution !== undefined);
938
+ opts?.execution?.assertActive();
939
+ if (mode === 'require' || opts?.execution) {
940
+ throw new LeaseUnavailableError(scope, lane, 'live holder did not release within deadline');
941
+ }
942
+ // local: degrade to today's behaviour — proceed without a lease. busy_timeout
943
+ // on the connection still serializes the actual write. A no-op handle writes
944
+ // no row, starts no heartbeat, and is never memoized — so single-flight
945
+ // followers find no memo entry and acquire freshly.
946
+ const active = native
947
+ .prepare(`SELECT holder_id, holder_pid, epoch, heartbeat_at, ttl_ms ` +
948
+ `FROM ${WRITER_LEASES_TABLE} WHERE scope = ? AND lane = ? AND active = 1`)
949
+ .get(scope, lane);
950
+ log().warn({
951
+ scope,
952
+ lane,
953
+ dbPath,
954
+ activeHolderId: active?.holder_id,
955
+ activeHolderPid: active?.holder_pid,
956
+ activeEpoch: active?.epoch,
957
+ activeHeartbeatAgeMs: active === undefined ? undefined : Math.max(0, Date.now() - active.heartbeat_at),
958
+ }, 'writer-lease acquire deadline exceeded; proceeding under busy_timeout fallback (degraded)');
959
+ return makeNoopHandle(scope, lane);
960
+ }
961
+ await sleepAsync(Math.min(CLAIM_RETRY_DELAY_MS, Math.max(0, deadline - Date.now())));
835
962
  }
836
- await sleepAsync(CLAIM_RETRY_DELAY_MS);
837
963
  }
964
+ finally {
965
+ // Cancellation removes only this operation's waiter; never another holder's grant.
966
+ if (enqueued)
967
+ dequeueWaiter(native, scope, lane, holderId, opts?.execution !== undefined);
968
+ }
969
+ }
970
+ /** Bind cleanup to this caller's original grant without reopening after cancellation. */
971
+ function scopeLeaseRelease(handle, dbPath, execution) {
972
+ if (!execution)
973
+ return handle;
974
+ let released = false;
975
+ let cleanupPending = false;
976
+ return {
977
+ get cleanupPending() {
978
+ return cleanupPending;
979
+ },
980
+ scope: handle.scope,
981
+ lane: handle.lane,
982
+ epoch: handle.epoch,
983
+ async release() {
984
+ if (released)
985
+ return;
986
+ released = true;
987
+ cleanupPending = await releaseGrant(handle.scope, dbPath, handle.lane, handle);
988
+ },
989
+ heartbeat() {
990
+ execution.assertActive();
991
+ if (!released)
992
+ handle.heartbeat();
993
+ },
994
+ };
838
995
  }
839
996
  /** Decrement the memoized grant; free the row at depth 0. */
840
- async function releaseGrant(scope, dbPath, lane) {
997
+ async function releaseGrant(scope, dbPath, lane, scopedHandle) {
841
998
  const key = memoKey(scope, dbPath, lane);
842
999
  const entry = _grantMemo.get(key);
843
- if (!entry)
844
- return; // off-mode / no-op handle / already released
1000
+ if (!entry || (scopedHandle && entry.handle !== scopedHandle)) {
1001
+ // Stop a stale scoped timer without touching the successor's epoch or reopening.
1002
+ return scopedHandle instanceof InternalLeaseHandle
1003
+ ? !scopedHandle.releaseRow(undefined, true)
1004
+ : false;
1005
+ }
845
1006
  entry.refcount -= 1;
846
1007
  if (entry.refcount > 0) {
1008
+ if (scopedHandle) {
1009
+ return !entry.handle.releaseScopedDepth(entry.refcount);
1010
+ }
847
1011
  // Still re-entered above this frame — decrement the durable depth only. Pin the
848
1012
  // resolver at the grant's own dbPath so the depth-write lands in the SAME file
849
1013
  // the lease row lives in (matches the memo key, not the cwd-default).
@@ -852,22 +1016,27 @@ async function releaseGrant(scope, dbPath, lane) {
852
1016
  .prepare(`UPDATE ${WRITER_LEASES_TABLE} SET reentrancy_depth = reentrancy_depth - 1 ` +
853
1017
  `WHERE scope = ? AND lane = ? AND holder_id = ? AND epoch = ? AND active = 1`)
854
1018
  .run(scope, lane, entry.handle.holderId, entry.handle.epoch);
855
- return;
1019
+ return false;
856
1020
  }
857
1021
  // Depth 0 — free the row and evict the memo.
858
1022
  _grantMemo.delete(key);
859
- if (entry.handle.releaseRow())
860
- return;
1023
+ if (entry.handle.releaseRow(undefined, scopedHandle !== undefined))
1024
+ return false;
1025
+ if (scopedHandle) {
1026
+ log().debug({ scope, lane, dbPath }, 'Scoped lease release deferred to TTL recovery; no replacement handle opened');
1027
+ return true;
1028
+ }
861
1029
  // The domain can close and replace the shared dual-scope handle while a lease
862
1030
  // is still unwinding. Closing SQLite does not mutate the persisted lease row;
863
1031
  // retry through the path-aware resolver so the next writer does not wait for
864
1032
  // the full stale-holder TTL.
865
1033
  try {
866
1034
  const { native } = await _nativeDbResolver(scope, dbPath);
867
- entry.handle.releaseRow(native);
1035
+ return !entry.handle.releaseRow(native);
868
1036
  }
869
1037
  catch (err) {
870
1038
  log().debug({ scope, lane, dbPath, err: err instanceof Error ? err.message : err }, 'writer-lease fresh-handle release retry failed; row will expire by TTL');
1039
+ return true;
871
1040
  }
872
1041
  }
873
1042
  /**
@@ -887,6 +1056,24 @@ function makeNoopHandle(scope, lane) {
887
1056
  },
888
1057
  };
889
1058
  }
1059
+ /** Release only this cold-open claim, refusing another native transaction when scoped. */
1060
+ function releaseColdOpenClaim(nativeDb, scope, lane, holderId, epoch, guardTransaction) {
1061
+ if (guardTransaction)
1062
+ nativeDb.exec('BEGIN IMMEDIATE');
1063
+ try {
1064
+ nativeDb
1065
+ .prepare(`UPDATE ${WRITER_LEASES_TABLE} SET active = 0, reentrancy_depth = 0 ` +
1066
+ `WHERE scope = ? AND lane = ? AND holder_id = ? AND epoch = ? AND active = 1`)
1067
+ .run(scope, lane, holderId, epoch);
1068
+ if (guardTransaction)
1069
+ nativeDb.exec('COMMIT');
1070
+ }
1071
+ catch (error) {
1072
+ if (guardTransaction)
1073
+ nativeDb.exec('ROLLBACK');
1074
+ throw error;
1075
+ }
1076
+ }
890
1077
  /**
891
1078
  * Primary surface: acquire → run `fn` → release (always, even on throw).
892
1079
  *
@@ -897,6 +1084,14 @@ function makeNoopHandle(scope, lane) {
897
1084
  * - `require` mode → an unacquirable lease throws {@link LeaseUnavailableError}
898
1085
  * before `fn` runs.
899
1086
  *
1087
+ * @remarks A supplied execution context preserves its original deadline and requires
1088
+ * an explicit database path. Cancellation never grants degraded write permission.
1089
+ * Unsettled native resolution is reported explicitly; its opener is not preempted.
1090
+ * @example
1091
+ * ```ts
1092
+ * const value = await withWriterLease('project', 'brain', write, options);
1093
+ * ```
1094
+ * @typeParam T - Result of the accepted write callback.
900
1095
  * @param scope - The cleo.db scope.
901
1096
  * @param lane - The write lane.
902
1097
  * @param fn - The work to run while holding the lease; receives the handle.
@@ -908,6 +1103,7 @@ function makeNoopHandle(scope, lane) {
908
1103
  export async function withWriterLease(scope, lane, fn, opts) {
909
1104
  const handle = await acquireWriterLease(scope, lane, opts);
910
1105
  try {
1106
+ opts?.execution?.assertActive();
911
1107
  return await fn(handle);
912
1108
  }
913
1109
  finally {
@@ -985,6 +1181,139 @@ function ensureColdOpenLeaseTables(nativeDb) {
985
1181
  nativeDb.exec(stmt);
986
1182
  }
987
1183
  }
1184
+ /** Operation-local membership, not a process-wide grant or domain-handle cache. */
1185
+ const requiredColdOpenScope = new AsyncLocalStorage();
1186
+ /** Refuse replacement of the exact file protected by this operation. */
1187
+ function assertColdOpenFile(frame) {
1188
+ const current = statSync(frame.path, { bigint: true });
1189
+ if (!frame.active || current.dev !== frame.device || current.ino !== frame.inode) {
1190
+ throw new LeaseUnavailableError(frame.scope, 'tasks', 'required schema ownership expired or database file replaced');
1191
+ }
1192
+ }
1193
+ /** Hold schema ownership independently of a callback that can close its domain handle. */
1194
+ async function withRequiredColdOpenLease(scope, nativeDb, fn, opts) {
1195
+ opts.execution?.assertActive();
1196
+ const captured = opts.requiredIdentity;
1197
+ const path = realpathSync(captured.dbPath);
1198
+ const identity = statSync(path, { bigint: true });
1199
+ if (captured.scope !== scope ||
1200
+ captured.fileDevice !== String(identity.dev) ||
1201
+ captured.fileInode !== String(identity.ino)) {
1202
+ throw new LeaseUnavailableError(scope, 'tasks', 'required schema identity is missing or its database file was replaced');
1203
+ }
1204
+ const parent = requiredColdOpenScope.getStore();
1205
+ for (let frame = parent; frame; frame = frame.parent) {
1206
+ if (!frame.active)
1207
+ throw new LeaseUnavailableError(scope, 'tasks', 'expired lexical schema ownership');
1208
+ if (frame.path !== path)
1209
+ continue;
1210
+ assertColdOpenFile(frame);
1211
+ if (frame.scope !== scope)
1212
+ throw new LeaseUnavailableError(scope, 'tasks', 'nested schema scope mismatch');
1213
+ if (frame.execution && opts.execution && frame.execution !== opts.execution) {
1214
+ throw new LeaseUnavailableError(scope, 'tasks', 'nested schema ownership cannot replace execution authority');
1215
+ }
1216
+ frame.execution?.assertActive();
1217
+ frame.guard.assertHeld(nativeDb);
1218
+ const result = await fn(frame.guard);
1219
+ assertColdOpenFile(frame);
1220
+ frame.guard.assertHeld();
1221
+ return result;
1222
+ }
1223
+ const remaining = opts.execution
1224
+ ? Math.max(1, opts.execution.deadlineAt - Date.now())
1225
+ : ACQUIRE_DEADLINE_MS;
1226
+ const owned = openNativeDatabase(path, { timeout: Math.min(ACQUIRE_DEADLINE_MS, remaining) });
1227
+ try {
1228
+ opts.execution?.assertActive();
1229
+ const afterOpen = statSync(path, { bigint: true });
1230
+ if (afterOpen.dev !== identity.dev || afterOpen.ino !== identity.ino) {
1231
+ throw new LeaseUnavailableError(scope, 'tasks', 'database file changed during lease open');
1232
+ }
1233
+ return await runColdOpenLease(scope, owned, async (grant) => {
1234
+ const frame = {
1235
+ parent,
1236
+ scope,
1237
+ path,
1238
+ device: identity.dev,
1239
+ inode: identity.ino,
1240
+ get guard() {
1241
+ return guard;
1242
+ },
1243
+ execution: opts.execution,
1244
+ active: true,
1245
+ };
1246
+ const guard = {
1247
+ assertHeld(target) {
1248
+ assertColdOpenFile(frame);
1249
+ grant.assertHeld(target);
1250
+ },
1251
+ mutate(target, mutation) {
1252
+ assertColdOpenFile(frame);
1253
+ opts.execution?.assertActive();
1254
+ if (target !== nativeDb) {
1255
+ throw new LeaseUnavailableError(scope, 'tasks', 'schema mutation requires its captured native handle');
1256
+ }
1257
+ if (target.isTransaction) {
1258
+ throw new LeaseUnavailableError(scope, 'tasks', 'schema mutation cannot borrow an active transaction');
1259
+ }
1260
+ const previousTimeout = Number(target.prepare('PRAGMA busy_timeout').get()?.timeout ?? 0);
1261
+ if (opts.execution) {
1262
+ target.exec(`PRAGMA busy_timeout = ${Math.max(0, Math.min(previousTimeout, opts.execution.deadlineAt - Date.now()))}`);
1263
+ }
1264
+ try {
1265
+ target.exec('BEGIN IMMEDIATE');
1266
+ // Read on the actual writer after its lock, so a competing epoch change
1267
+ // cannot occur between ownership validation and these committed writes.
1268
+ const assertMutationOwnership = () => {
1269
+ guard.assertHeld(target);
1270
+ const lease = target
1271
+ .prepare(`SELECT heartbeat_at, ttl_ms FROM main.${WRITER_LEASES_TABLE} WHERE scope = ? AND lane = 'tasks' AND active = 1`)
1272
+ .get(scope);
1273
+ if (!lease || Number(lease.heartbeat_at) + Number(lease.ttl_ms) <= Date.now()) {
1274
+ throw new LeaseUnavailableError(scope, 'tasks', 'schema mutation ownership expired');
1275
+ }
1276
+ opts.execution?.assertActive();
1277
+ };
1278
+ assertMutationOwnership();
1279
+ const result = mutation();
1280
+ if (result !== null &&
1281
+ (typeof result === 'object' || typeof result === 'function') &&
1282
+ 'then' in result) {
1283
+ throw new TypeError('schema mutation callback must be synchronous');
1284
+ }
1285
+ assertMutationOwnership();
1286
+ target.exec('COMMIT');
1287
+ return result;
1288
+ }
1289
+ catch (error) {
1290
+ if (target.isTransaction)
1291
+ target.exec('ROLLBACK');
1292
+ throw error;
1293
+ }
1294
+ finally {
1295
+ if (target.isOpen && opts.execution)
1296
+ target.exec(`PRAGMA busy_timeout = ${previousTimeout}`);
1297
+ }
1298
+ },
1299
+ };
1300
+ // The random holder and epoch must be visible through the actual callback
1301
+ // handle. Matching path strings alone cannot establish file identity.
1302
+ guard.assertHeld(nativeDb);
1303
+ try {
1304
+ const result = await requiredColdOpenScope.run(frame, () => fn(guard));
1305
+ guard.assertHeld();
1306
+ return result;
1307
+ }
1308
+ finally {
1309
+ frame.active = false;
1310
+ }
1311
+ }, opts, true);
1312
+ }
1313
+ finally {
1314
+ owned.close();
1315
+ }
1316
+ }
988
1317
  /**
989
1318
  * Lease the dual-scope-db COLD-OPEN critical section (Seam 0 — the T5158 heal).
990
1319
  *
@@ -1012,6 +1341,17 @@ function ensureColdOpenLeaseTables(nativeDb) {
1012
1341
  * degrade to running `fn` (busy_timeout still serializes the migrate write-txn).
1013
1342
  * - `require` → throw {@link LeaseUnavailableError} if the row cannot be taken.
1014
1343
  *
1344
+ * @typeParam T - Actual result of the admitted migration callback.
1345
+ * @remarks Scoped callbacks receive one original deadline and must cooperate at
1346
+ * their own asynchronous boundaries. Required ownership pins the captured file generation,
1347
+ * uses an independently owned handle, and refuses off/degraded execution. Explicit
1348
+ * checkpoints and publication checks do not fence arbitrary callback writes.
1349
+ * A committed callback result survives later
1350
+ * cancellation; cancellation never grants degraded migration permission.
1351
+ * @example
1352
+ * ```ts
1353
+ * const result = await withColdOpenLease('project', native, migrate, options);
1354
+ * ```
1015
1355
  * @param scope - The cleo.db scope being cold-opened.
1016
1356
  * @param nativeDb - The native handle the cold-open just created (pragmas applied).
1017
1357
  * @param fn - The cold-open body to run while holding the lease.
@@ -1024,11 +1364,25 @@ function ensureColdOpenLeaseTables(nativeDb) {
1024
1364
  * @task T12042 (E6-L12b — no process-global side effect)
1025
1365
  */
1026
1366
  export async function withColdOpenLease(scope, nativeDb, fn, opts) {
1027
- const mode = effectiveMode();
1367
+ return opts?.requiredIdentity
1368
+ ? withRequiredColdOpenLease(scope, nativeDb, fn, opts)
1369
+ : runColdOpenLease(scope, nativeDb, fn, opts);
1370
+ }
1371
+ /** Shared row acquisition; required callers cannot take the compatibility fallback. */
1372
+ async function runColdOpenLease(scope, nativeDb, fn, opts, required = false) {
1373
+ opts?.execution?.assertActive();
1374
+ const mode = required ? 'require' : effectiveMode();
1375
+ const rejectMutation = () => {
1376
+ throw new LeaseUnavailableError(scope, 'tasks', 'schema mutation requires operation-owned authority');
1377
+ };
1378
+ const unleased = {
1379
+ assertHeld: () => opts?.execution?.assertActive(),
1380
+ mutate: rejectMutation,
1381
+ };
1028
1382
  // `off` mode — pure pass-through. busy_timeout=30000 on the connection still
1029
1383
  // serializes the migrate/reconcile write-txn exactly as before the lease.
1030
1384
  if (mode === 'off') {
1031
- return fn();
1385
+ return fn(unleased);
1032
1386
  }
1033
1387
  // The lease tables MUST exist before the claim txn — the migration that creates
1034
1388
  // them runs inside `fn`, so bootstrap them idempotently here first.
@@ -1042,43 +1396,58 @@ export async function withColdOpenLease(scope, nativeDb, fn, opts) {
1042
1396
  // Bounded acquire window — never wait longer than the lease TTL. busy_timeout on
1043
1397
  // the IMMEDIATE lock backstops every individual claim attempt.
1044
1398
  const acquireWindowMs = Math.min(ACQUIRE_DEADLINE_MS, Math.max(1, ttlMs));
1045
- const deadline = Date.now() + acquireWindowMs;
1399
+ const deadline = Math.min(Date.now() + acquireWindowMs, opts?.execution?.deadlineAt ?? Infinity);
1046
1400
  let enqueued = false;
1047
1401
  let epoch = null;
1048
- for (;;) {
1049
- epoch = tryClaimOnce(nativeDb, scope, lane, holderId, ttlMs);
1050
- if (epoch !== null) {
1051
- if (enqueued)
1052
- dequeueWaiter(nativeDb, scope, lane, holderId);
1053
- break;
1054
- }
1055
- if (!enqueued) {
1056
- enqueueWaiter(nativeDb, scope, lane, holderId, priority, ttlMs);
1057
- enqueued = true;
1058
- }
1059
- if (Date.now() >= deadline) {
1060
- if (enqueued)
1061
- dequeueWaiter(nativeDb, scope, lane, holderId);
1062
- if (mode === 'require') {
1063
- throw new LeaseUnavailableError(scope, lane, 'cold-open: live holder did not release within deadline');
1402
+ try {
1403
+ for (;;) {
1404
+ opts?.execution?.assertActive();
1405
+ epoch = tryClaimOnce(nativeDb, scope, lane, holderId, ttlMs, opts?.execution);
1406
+ if (epoch !== null) {
1407
+ if (enqueued)
1408
+ dequeueWaiter(nativeDb, scope, lane, holderId, opts?.execution !== undefined);
1409
+ break;
1410
+ }
1411
+ if (!enqueued) {
1412
+ opts?.execution?.assertActive();
1413
+ enqueueWaiter(nativeDb, scope, lane, holderId, priority, ttlMs);
1414
+ enqueued = true;
1415
+ }
1416
+ if (Date.now() >= deadline) {
1417
+ if (enqueued)
1418
+ dequeueWaiter(nativeDb, scope, lane, holderId, opts?.execution !== undefined);
1419
+ opts?.execution?.assertActive();
1420
+ if (mode === 'require' || opts?.execution) {
1421
+ throw new LeaseUnavailableError(scope, lane, 'cold-open: live holder did not release within deadline');
1422
+ }
1423
+ // local/supervisor: degrade to today's behaviour — run the cold-open under
1424
+ // busy_timeout, which still serializes the migrate write-txn.
1425
+ log().warn({ scope }, 'cold-open writer-lease acquire deadline exceeded; proceeding under ' +
1426
+ 'busy_timeout fallback (degraded)');
1427
+ return fn(unleased);
1064
1428
  }
1065
- // local/supervisor: degrade to today's behaviour — run the cold-open under
1066
- // busy_timeout, which still serializes the migrate write-txn.
1067
- log().warn({ scope }, 'cold-open writer-lease acquire deadline exceeded; proceeding under ' +
1068
- 'busy_timeout fallback (degraded)');
1069
- return fn();
1429
+ await sleepAsync(Math.min(CLAIM_RETRY_DELAY_MS, Math.max(0, deadline - Date.now())));
1070
1430
  }
1071
- await sleepAsync(CLAIM_RETRY_DELAY_MS);
1072
- }
1073
- // Held — run the cold-open body, then free the row (epoch-guarded) on the way out.
1074
- try {
1075
- return await fn();
1431
+ // Retain an actual committed callback result even if cancellation arrives afterwards.
1432
+ opts?.execution?.assertActive();
1433
+ return await fn({
1434
+ mutate: rejectMutation,
1435
+ assertHeld(target = nativeDb) {
1436
+ const row = target
1437
+ .prepare(`SELECT holder_id, epoch FROM main.${WRITER_LEASES_TABLE} WHERE scope = ? AND lane = ? AND active = 1`)
1438
+ .get(scope, lane);
1439
+ if (!row || row.holder_id !== holderId || row.epoch !== epoch) {
1440
+ throw new LeaseUnavailableError(scope, lane, 'required schema ownership is stale or belongs to another database');
1441
+ }
1442
+ },
1443
+ });
1076
1444
  }
1077
1445
  finally {
1078
- nativeDb
1079
- .prepare(`UPDATE ${WRITER_LEASES_TABLE} SET active = 0, reentrancy_depth = 0 ` +
1080
- `WHERE scope = ? AND lane = ? AND holder_id = ? AND epoch = ? AND active = 1`)
1081
- .run(scope, lane, holderId, epoch);
1446
+ if (enqueued)
1447
+ dequeueWaiter(nativeDb, scope, lane, holderId, opts?.execution !== undefined);
1448
+ if (epoch !== null) {
1449
+ releaseColdOpenClaim(nativeDb, scope, lane, holderId, epoch, opts?.execution !== undefined);
1450
+ }
1082
1451
  }
1083
1452
  }
1084
1453
  //# sourceMappingURL=writer-lease.js.map