@cleocode/core 2026.9.8 → 2026.9.10

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 (569) 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/compliance/protocol-rules.d.ts.map +1 -1
  9. package/dist/compliance/protocol-rules.js +3 -1
  10. package/dist/compliance/protocol-rules.js.map +1 -1
  11. package/dist/dispatch/mutate-projection.d.ts.map +1 -1
  12. package/dist/dispatch/mutate-projection.js +12 -0
  13. package/dist/dispatch/mutate-projection.js.map +1 -1
  14. package/dist/dispatch/mvi-projection.d.ts +29 -6
  15. package/dist/dispatch/mvi-projection.d.ts.map +1 -1
  16. package/dist/dispatch/mvi-projection.js +109 -139
  17. package/dist/dispatch/mvi-projection.js.map +1 -1
  18. package/dist/docs/docs-read-model.d.ts +3 -2
  19. package/dist/docs/docs-read-model.d.ts.map +1 -1
  20. package/dist/docs/docs-read-model.js +4 -3
  21. package/dist/docs/docs-read-model.js.map +1 -1
  22. package/dist/docs/export-document.js +5129 -2297
  23. package/dist/docs/export-document.js.map +3 -3
  24. package/dist/docs/projection.d.ts +67 -0
  25. package/dist/docs/projection.d.ts.map +1 -0
  26. package/dist/docs/projection.js +370 -0
  27. package/dist/docs/projection.js.map +1 -0
  28. package/dist/doctor/acceptance-drift.d.ts +6 -2
  29. package/dist/doctor/acceptance-drift.d.ts.map +1 -1
  30. package/dist/doctor/acceptance-drift.js +43 -5
  31. package/dist/doctor/acceptance-drift.js.map +1 -1
  32. package/dist/doctor/knowledge-summary.d.ts +41 -0
  33. package/dist/doctor/knowledge-summary.d.ts.map +1 -0
  34. package/dist/doctor/knowledge-summary.js +81 -0
  35. package/dist/doctor/knowledge-summary.js.map +1 -0
  36. package/dist/doctor/knowledge.d.ts +192 -0
  37. package/dist/doctor/knowledge.d.ts.map +1 -0
  38. package/dist/doctor/knowledge.js +1864 -0
  39. package/dist/doctor/knowledge.js.map +1 -0
  40. package/dist/git/work-tree.d.ts +169 -0
  41. package/dist/git/work-tree.d.ts.map +1 -0
  42. package/dist/git/work-tree.js +341 -0
  43. package/dist/git/work-tree.js.map +1 -0
  44. package/dist/hooks/handlers/task-hooks.d.ts +8 -5
  45. package/dist/hooks/handlers/task-hooks.d.ts.map +1 -1
  46. package/dist/hooks/handlers/task-hooks.js +15 -60
  47. package/dist/hooks/handlers/task-hooks.js.map +1 -1
  48. package/dist/identity/cleo-identity.d.ts +13 -0
  49. package/dist/identity/cleo-identity.d.ts.map +1 -1
  50. package/dist/identity/cleo-identity.js +20 -6
  51. package/dist/identity/cleo-identity.js.map +1 -1
  52. package/dist/injection.d.ts.map +1 -1
  53. package/dist/injection.js +29 -13
  54. package/dist/injection.js.map +1 -1
  55. package/dist/internal.d.ts +3 -0
  56. package/dist/internal.d.ts.map +1 -1
  57. package/dist/internal.js +4 -0
  58. package/dist/internal.js.map +1 -1
  59. package/dist/lifecycle/verification/satisfies-validator.d.ts.map +1 -1
  60. package/dist/lifecycle/verification/satisfies-validator.js +124 -120
  61. package/dist/lifecycle/verification/satisfies-validator.js.map +1 -1
  62. package/dist/llm/plugin-facade.js +4047 -1726
  63. package/dist/llm/plugin-facade.js.map +3 -3
  64. package/dist/memory/brain-backfill.d.ts +27 -12
  65. package/dist/memory/brain-backfill.d.ts.map +1 -1
  66. package/dist/memory/brain-backfill.js +325 -134
  67. package/dist/memory/brain-backfill.js.map +1 -1
  68. package/dist/memory/brain-doctor.d.ts +21 -0
  69. package/dist/memory/brain-doctor.d.ts.map +1 -1
  70. package/dist/memory/brain-doctor.js +111 -53
  71. package/dist/memory/brain-doctor.js.map +1 -1
  72. package/dist/memory/brain-links.d.ts.map +1 -1
  73. package/dist/memory/brain-links.js +31 -4
  74. package/dist/memory/brain-links.js.map +1 -1
  75. package/dist/memory/brain-reasoning.d.ts.map +1 -1
  76. package/dist/memory/brain-reasoning.js +28 -2
  77. package/dist/memory/brain-reasoning.js.map +1 -1
  78. package/dist/memory/brain-reconciler.d.ts +14 -19
  79. package/dist/memory/brain-reconciler.d.ts.map +1 -1
  80. package/dist/memory/brain-reconciler.js +70 -97
  81. package/dist/memory/brain-reconciler.js.map +1 -1
  82. package/dist/memory/brain-search.d.ts +2 -0
  83. package/dist/memory/brain-search.d.ts.map +1 -1
  84. package/dist/memory/brain-search.js +59 -29
  85. package/dist/memory/brain-search.js.map +1 -1
  86. package/dist/memory/brain-similarity.d.ts.map +1 -1
  87. package/dist/memory/brain-similarity.js +5 -4
  88. package/dist/memory/brain-similarity.js.map +1 -1
  89. package/dist/memory/brain-stub-prune.d.ts +31 -9
  90. package/dist/memory/brain-stub-prune.d.ts.map +1 -1
  91. package/dist/memory/brain-stub-prune.js +65 -18
  92. package/dist/memory/brain-stub-prune.js.map +1 -1
  93. package/dist/memory/brain-writer-handlers.d.ts +10 -1
  94. package/dist/memory/brain-writer-handlers.d.ts.map +1 -1
  95. package/dist/memory/brain-writer-handlers.js +23 -3
  96. package/dist/memory/brain-writer-handlers.js.map +1 -1
  97. package/dist/memory/brain-writer-thread.d.ts +11 -1
  98. package/dist/memory/brain-writer-thread.d.ts.map +1 -1
  99. package/dist/memory/brain-writer-thread.js +87 -20
  100. package/dist/memory/brain-writer-thread.js.map +1 -1
  101. package/dist/memory/brain-writer-worker.js +8 -1
  102. package/dist/memory/brain-writer-worker.js.map +1 -1
  103. package/dist/memory/decision-cross-link.d.ts +39 -0
  104. package/dist/memory/decision-cross-link.d.ts.map +1 -1
  105. package/dist/memory/decision-cross-link.js +316 -0
  106. package/dist/memory/decision-cross-link.js.map +1 -1
  107. package/dist/memory/decisions.d.ts +4 -1
  108. package/dist/memory/decisions.d.ts.map +1 -1
  109. package/dist/memory/decisions.js +7 -24
  110. package/dist/memory/decisions.js.map +1 -1
  111. package/dist/memory/dispatch-trace.d.ts +12 -5
  112. package/dist/memory/dispatch-trace.d.ts.map +1 -1
  113. package/dist/memory/dispatch-trace.js +17 -7
  114. package/dist/memory/dispatch-trace.js.map +1 -1
  115. package/dist/memory/eligibility.d.ts +51 -0
  116. package/dist/memory/eligibility.d.ts.map +1 -0
  117. package/dist/memory/eligibility.js +77 -0
  118. package/dist/memory/eligibility.js.map +1 -0
  119. package/dist/memory/engine-compat.d.ts +2 -0
  120. package/dist/memory/engine-compat.d.ts.map +1 -1
  121. package/dist/memory/engine-compat.js +10 -0
  122. package/dist/memory/engine-compat.js.map +1 -1
  123. package/dist/memory/extraction-gate.d.ts +21 -7
  124. package/dist/memory/extraction-gate.d.ts.map +1 -1
  125. package/dist/memory/extraction-gate.js +178 -216
  126. package/dist/memory/extraction-gate.js.map +1 -1
  127. package/dist/memory/graph-auto-populate.d.ts +38 -5
  128. package/dist/memory/graph-auto-populate.d.ts.map +1 -1
  129. package/dist/memory/graph-auto-populate.js +182 -60
  130. package/dist/memory/graph-auto-populate.js.map +1 -1
  131. package/dist/memory/graph-memory-bridge.d.ts +14 -1
  132. package/dist/memory/graph-memory-bridge.d.ts.map +1 -1
  133. package/dist/memory/graph-memory-bridge.js +66 -28
  134. package/dist/memory/graph-memory-bridge.js.map +1 -1
  135. package/dist/memory/index.d.ts +1 -19
  136. package/dist/memory/index.d.ts.map +1 -1
  137. package/dist/memory/index.js +1 -50
  138. package/dist/memory/index.js.map +1 -1
  139. package/dist/memory/learnings.d.ts.map +1 -1
  140. package/dist/memory/learnings.js +2 -23
  141. package/dist/memory/learnings.js.map +1 -1
  142. package/dist/memory/manifest-filter.d.ts +21 -0
  143. package/dist/memory/manifest-filter.d.ts.map +1 -0
  144. package/dist/memory/manifest-filter.js +51 -0
  145. package/dist/memory/manifest-filter.js.map +1 -0
  146. package/dist/memory/manifest-ingestion.d.ts +28 -30
  147. package/dist/memory/manifest-ingestion.d.ts.map +1 -1
  148. package/dist/memory/manifest-ingestion.js +104 -235
  149. package/dist/memory/manifest-ingestion.js.map +1 -1
  150. package/dist/memory/observer-reflector.d.ts +2 -0
  151. package/dist/memory/observer-reflector.d.ts.map +1 -1
  152. package/dist/memory/observer-reflector.js +6 -49
  153. package/dist/memory/observer-reflector.js.map +1 -1
  154. package/dist/memory/patterns.d.ts +10 -0
  155. package/dist/memory/patterns.d.ts.map +1 -1
  156. package/dist/memory/patterns.js +25 -33
  157. package/dist/memory/patterns.js.map +1 -1
  158. package/dist/memory/pipeline-manifest-sqlite.d.ts +72 -9
  159. package/dist/memory/pipeline-manifest-sqlite.d.ts.map +1 -1
  160. package/dist/memory/pipeline-manifest-sqlite.js +508 -412
  161. package/dist/memory/pipeline-manifest-sqlite.js.map +1 -1
  162. package/dist/memory/retrieval/build-retrieval-bundle.d.ts.map +1 -1
  163. package/dist/memory/retrieval/build-retrieval-bundle.js +16 -15
  164. package/dist/memory/retrieval/build-retrieval-bundle.js.map +1 -1
  165. package/dist/memory/retrieval/fetch.d.ts +7 -0
  166. package/dist/memory/retrieval/fetch.d.ts.map +1 -1
  167. package/dist/memory/retrieval/fetch.js +14 -11
  168. package/dist/memory/retrieval/fetch.js.map +1 -1
  169. package/dist/memory/retrieval/observe.d.ts +7 -1
  170. package/dist/memory/retrieval/observe.d.ts.map +1 -1
  171. package/dist/memory/retrieval/observe.js +58 -4
  172. package/dist/memory/retrieval/observe.js.map +1 -1
  173. package/dist/memory/retrieval/search.d.ts +14 -1
  174. package/dist/memory/retrieval/search.d.ts.map +1 -1
  175. package/dist/memory/retrieval/search.js +133 -69
  176. package/dist/memory/retrieval/search.js.map +1 -1
  177. package/dist/memory/retrieval/telemetry.d.ts +31 -0
  178. package/dist/memory/retrieval/telemetry.d.ts.map +1 -0
  179. package/dist/memory/retrieval/telemetry.js +53 -0
  180. package/dist/memory/retrieval/telemetry.js.map +1 -0
  181. package/dist/nexus/analyze-orchestrator.d.ts +41 -9
  182. package/dist/nexus/analyze-orchestrator.d.ts.map +1 -1
  183. package/dist/nexus/analyze-orchestrator.js +257 -58
  184. package/dist/nexus/analyze-orchestrator.js.map +1 -1
  185. package/dist/nexus/api-contracts.d.ts +2 -6
  186. package/dist/nexus/api-contracts.d.ts.map +1 -1
  187. package/dist/nexus/api-contracts.js +11 -2
  188. package/dist/nexus/api-contracts.js.map +1 -1
  189. package/dist/nexus/context.d.ts.map +1 -1
  190. package/dist/nexus/context.js +14 -37
  191. package/dist/nexus/context.js.map +1 -1
  192. package/dist/nexus/diff.d.ts.map +1 -1
  193. package/dist/nexus/diff.js +2 -1
  194. package/dist/nexus/diff.js.map +1 -1
  195. package/dist/nexus/identity.d.ts +29 -3
  196. package/dist/nexus/identity.d.ts.map +1 -1
  197. package/dist/nexus/identity.js +63 -12
  198. package/dist/nexus/identity.js.map +1 -1
  199. package/dist/nexus/impact.d.ts +21 -19
  200. package/dist/nexus/impact.d.ts.map +1 -1
  201. package/dist/nexus/impact.js +96 -192
  202. package/dist/nexus/impact.js.map +1 -1
  203. package/dist/nexus/knowledge.d.ts +80 -0
  204. package/dist/nexus/knowledge.d.ts.map +1 -0
  205. package/dist/nexus/knowledge.js +786 -0
  206. package/dist/nexus/knowledge.js.map +1 -0
  207. package/dist/nexus/living-brain.d.ts +8 -5
  208. package/dist/nexus/living-brain.d.ts.map +1 -1
  209. package/dist/nexus/living-brain.js +252 -188
  210. package/dist/nexus/living-brain.js.map +1 -1
  211. package/dist/nexus/registry.d.ts +11 -2
  212. package/dist/nexus/registry.d.ts.map +1 -1
  213. package/dist/nexus/registry.js +144 -128
  214. package/dist/nexus/registry.js.map +1 -1
  215. package/dist/nexus/source-roots.d.ts +16 -0
  216. package/dist/nexus/source-roots.d.ts.map +1 -0
  217. package/dist/nexus/source-roots.js +174 -0
  218. package/dist/nexus/source-roots.js.map +1 -0
  219. package/dist/nexus/task-evidence.d.ts +22 -0
  220. package/dist/nexus/task-evidence.d.ts.map +1 -0
  221. package/dist/nexus/task-evidence.js +210 -0
  222. package/dist/nexus/task-evidence.js.map +1 -0
  223. package/dist/nexus/tasks-bridge.d.ts +14 -7
  224. package/dist/nexus/tasks-bridge.d.ts.map +1 -1
  225. package/dist/nexus/tasks-bridge.js +81 -40
  226. package/dist/nexus/tasks-bridge.js.map +1 -1
  227. package/dist/orchestrate/lifecycle-ops.d.ts.map +1 -1
  228. package/dist/orchestrate/lifecycle-ops.js +170 -120
  229. package/dist/orchestrate/lifecycle-ops.js.map +1 -1
  230. package/dist/orchestrate/query-ops.d.ts +6 -7
  231. package/dist/orchestrate/query-ops.d.ts.map +1 -1
  232. package/dist/orchestrate/query-ops.js +509 -532
  233. package/dist/orchestrate/query-ops.js.map +1 -1
  234. package/dist/orchestration/bootstrap.d.ts +15 -1
  235. package/dist/orchestration/bootstrap.d.ts.map +1 -1
  236. package/dist/orchestration/bootstrap.js +21 -10
  237. package/dist/orchestration/bootstrap.js.map +1 -1
  238. package/dist/orchestration/index.d.ts +35 -3
  239. package/dist/orchestration/index.d.ts.map +1 -1
  240. package/dist/orchestration/index.js +64 -35
  241. package/dist/orchestration/index.js.map +1 -1
  242. package/dist/orchestration/lead-rollup.d.ts +24 -1
  243. package/dist/orchestration/lead-rollup.d.ts.map +1 -1
  244. package/dist/orchestration/lead-rollup.js +31 -11
  245. package/dist/orchestration/lead-rollup.js.map +1 -1
  246. package/dist/orchestration/spawn-prompt.d.ts.map +1 -1
  247. package/dist/orchestration/spawn-prompt.js +25 -45
  248. package/dist/orchestration/spawn-prompt.js.map +1 -1
  249. package/dist/orchestration/unblock.d.ts +15 -1
  250. package/dist/orchestration/unblock.d.ts.map +1 -1
  251. package/dist/orchestration/unblock.js +23 -12
  252. package/dist/orchestration/unblock.js.map +1 -1
  253. package/dist/orchestration/validate-spawn.d.ts.map +1 -1
  254. package/dist/orchestration/validate-spawn.js +3 -3
  255. package/dist/orchestration/validate-spawn.js.map +1 -1
  256. package/dist/orchestration/waves.d.ts +29 -13
  257. package/dist/orchestration/waves.d.ts.map +1 -1
  258. package/dist/orchestration/waves.js +43 -55
  259. package/dist/orchestration/waves.js.map +1 -1
  260. package/dist/paths.d.ts +17 -163
  261. package/dist/paths.d.ts.map +1 -1
  262. package/dist/paths.js +145 -496
  263. package/dist/paths.js.map +1 -1
  264. package/dist/project-info.d.ts +2 -11
  265. package/dist/project-info.d.ts.map +1 -1
  266. package/dist/project-info.js +3 -31
  267. package/dist/project-info.js.map +1 -1
  268. package/dist/project-scope.d.ts +239 -0
  269. package/dist/project-scope.d.ts.map +1 -0
  270. package/dist/project-scope.js +473 -0
  271. package/dist/project-scope.js.map +1 -0
  272. package/dist/release/pr-evidence.d.ts +23 -4
  273. package/dist/release/pr-evidence.d.ts.map +1 -1
  274. package/dist/release/pr-evidence.js +65 -9
  275. package/dist/release/pr-evidence.js.map +1 -1
  276. package/dist/render/nexus/graph/index.d.ts.map +1 -1
  277. package/dist/render/nexus/graph/index.js +4 -2
  278. package/dist/render/nexus/graph/index.js.map +1 -1
  279. package/dist/render/orchestration/audit-reconstruct.d.ts +11 -0
  280. package/dist/render/orchestration/audit-reconstruct.d.ts.map +1 -1
  281. package/dist/render/orchestration/audit-reconstruct.js +21 -6
  282. package/dist/render/orchestration/audit-reconstruct.js.map +1 -1
  283. package/dist/render/tasks/show.js +2 -1
  284. package/dist/render/tasks/show.js.map +1 -1
  285. package/dist/resources/spawn-wrapper.d.ts +82 -14
  286. package/dist/resources/spawn-wrapper.d.ts.map +1 -1
  287. package/dist/resources/spawn-wrapper.js +585 -27
  288. package/dist/resources/spawn-wrapper.js.map +1 -1
  289. package/dist/selfimprove/fix-gen.js +4055 -1734
  290. package/dist/selfimprove/fix-gen.js.map +3 -3
  291. package/dist/sequence/index.d.ts.map +1 -1
  292. package/dist/sequence/index.js +76 -68
  293. package/dist/sequence/index.js.map +1 -1
  294. package/dist/session/canon-lint.d.ts +24 -3
  295. package/dist/session/canon-lint.d.ts.map +1 -1
  296. package/dist/session/canon-lint.js +72 -31
  297. package/dist/session/canon-lint.js.map +1 -1
  298. package/dist/session/engine-ops.d.ts +1 -4
  299. package/dist/session/engine-ops.d.ts.map +1 -1
  300. package/dist/session/engine-ops.js +42 -6
  301. package/dist/session/engine-ops.js.map +1 -1
  302. package/dist/sessions/briefing.d.ts +11 -1
  303. package/dist/sessions/briefing.d.ts.map +1 -1
  304. package/dist/sessions/briefing.js +76 -8
  305. package/dist/sessions/briefing.js.map +1 -1
  306. package/dist/sessions/handoff.d.ts +3 -1
  307. package/dist/sessions/handoff.d.ts.map +1 -1
  308. package/dist/sessions/handoff.js +2 -0
  309. package/dist/sessions/handoff.js.map +1 -1
  310. package/dist/shutdown-deadline.d.ts +24 -33
  311. package/dist/shutdown-deadline.d.ts.map +1 -1
  312. package/dist/shutdown-deadline.js +71 -14
  313. package/dist/shutdown-deadline.js.map +1 -1
  314. package/dist/shutdown.d.ts +21 -10
  315. package/dist/shutdown.d.ts.map +1 -1
  316. package/dist/shutdown.js +89 -42
  317. package/dist/shutdown.js.map +1 -1
  318. package/dist/skills/index.d.ts +1 -1
  319. package/dist/skills/index.d.ts.map +1 -1
  320. package/dist/skills/index.js +1 -1
  321. package/dist/skills/index.js.map +1 -1
  322. package/dist/skills/orchestrator/validator.d.ts +87 -20
  323. package/dist/skills/orchestrator/validator.d.ts.map +1 -1
  324. package/dist/skills/orchestrator/validator.js +330 -241
  325. package/dist/skills/orchestrator/validator.js.map +1 -1
  326. package/dist/store/agent-resolver.d.ts.map +1 -1
  327. package/dist/store/agent-resolver.js +16 -9
  328. package/dist/store/agent-resolver.js.map +1 -1
  329. package/dist/store/attachment-store.d.ts +4 -1
  330. package/dist/store/attachment-store.d.ts.map +1 -1
  331. package/dist/store/attachment-store.js +20 -3
  332. package/dist/store/attachment-store.js.map +1 -1
  333. package/dist/store/background-jobs.d.ts +426 -105
  334. package/dist/store/background-jobs.d.ts.map +1 -1
  335. package/dist/store/background-jobs.js +1208 -252
  336. package/dist/store/background-jobs.js.map +1 -1
  337. package/dist/store/background-ops.d.ts +145 -8
  338. package/dist/store/background-ops.d.ts.map +1 -1
  339. package/dist/store/background-ops.js +459 -15
  340. package/dist/store/background-ops.js.map +1 -1
  341. package/dist/store/backup-inspect.d.ts +48 -4
  342. package/dist/store/backup-inspect.d.ts.map +1 -1
  343. package/dist/store/backup-inspect.js +384 -4
  344. package/dist/store/backup-inspect.js.map +1 -1
  345. package/dist/store/converters.d.ts.map +1 -1
  346. package/dist/store/converters.js +19 -2
  347. package/dist/store/converters.js.map +1 -1
  348. package/dist/store/data-accessor.d.ts +9 -1
  349. package/dist/store/data-accessor.d.ts.map +1 -1
  350. package/dist/store/data-accessor.js +22 -5
  351. package/dist/store/data-accessor.js.map +1 -1
  352. package/dist/store/data-safety-central.d.ts.map +1 -1
  353. package/dist/store/data-safety-central.js +272 -232
  354. package/dist/store/data-safety-central.js.map +1 -1
  355. package/dist/store/db-helpers.d.ts +19 -3
  356. package/dist/store/db-helpers.d.ts.map +1 -1
  357. package/dist/store/db-helpers.js +42 -58
  358. package/dist/store/db-helpers.js.map +1 -1
  359. package/dist/store/dual-scope-db.d.ts +63 -4
  360. package/dist/store/dual-scope-db.d.ts.map +1 -1
  361. package/dist/store/dual-scope-db.js +166 -19
  362. package/dist/store/dual-scope-db.js.map +1 -1
  363. package/dist/store/exodus/archive.d.ts +21 -7
  364. package/dist/store/exodus/archive.d.ts.map +1 -1
  365. package/dist/store/exodus/archive.js +60 -18
  366. package/dist/store/exodus/archive.js.map +1 -1
  367. package/dist/store/exodus/migrate.d.ts.map +1 -1
  368. package/dist/store/exodus/migrate.js +27 -23
  369. package/dist/store/exodus/migrate.js.map +1 -1
  370. package/dist/store/exodus/on-open.d.ts +16 -27
  371. package/dist/store/exodus/on-open.d.ts.map +1 -1
  372. package/dist/store/exodus/on-open.js +121 -169
  373. package/dist/store/exodus/on-open.js.map +1 -1
  374. package/dist/store/exodus/recovery.d.ts +90 -0
  375. package/dist/store/exodus/recovery.d.ts.map +1 -0
  376. package/dist/store/exodus/recovery.js +434 -0
  377. package/dist/store/exodus/recovery.js.map +1 -0
  378. package/dist/store/exodus/types.d.ts +15 -0
  379. package/dist/store/exodus/types.d.ts.map +1 -1
  380. package/dist/store/memory-accessor.d.ts +52 -3
  381. package/dist/store/memory-accessor.d.ts.map +1 -1
  382. package/dist/store/memory-accessor.js +122 -7
  383. package/dist/store/memory-accessor.js.map +1 -1
  384. package/dist/store/nexus-sqlite.d.ts +24 -6
  385. package/dist/store/nexus-sqlite.d.ts.map +1 -1
  386. package/dist/store/nexus-sqlite.js +39 -20
  387. package/dist/store/nexus-sqlite.js.map +1 -1
  388. package/dist/store/ports/domain-binding.d.ts.map +1 -1
  389. package/dist/store/ports/domain-binding.js +27 -5
  390. package/dist/store/ports/domain-binding.js.map +1 -1
  391. package/dist/store/schema/background-jobs.d.ts +181 -2
  392. package/dist/store/schema/background-jobs.d.ts.map +1 -1
  393. package/dist/store/schema/background-jobs.js +27 -3
  394. package/dist/store/schema/background-jobs.js.map +1 -1
  395. package/dist/store/schema/cleo-global/skills.d.ts +2 -2
  396. package/dist/store/schema/cleo-project/lifecycle.d.ts +2 -2
  397. package/dist/store/schema/cleo-project/runtime.d.ts +1 -1
  398. package/dist/store/schema/cleo-project/tasks-core.d.ts +1 -1
  399. package/dist/store/schema/lifecycle.d.ts +2 -2
  400. package/dist/store/schema/skills-schema.d.ts +2 -2
  401. package/dist/store/schema/tasks.d.ts +1 -1
  402. package/dist/store/session-store.d.ts +3 -1
  403. package/dist/store/session-store.d.ts.map +1 -1
  404. package/dist/store/session-store.js +252 -207
  405. package/dist/store/session-store.js.map +1 -1
  406. package/dist/store/sqlite-data-accessor.d.ts +39 -4
  407. package/dist/store/sqlite-data-accessor.d.ts.map +1 -1
  408. package/dist/store/sqlite-data-accessor.js +689 -527
  409. package/dist/store/sqlite-data-accessor.js.map +1 -1
  410. package/dist/store/tasks-sqlite.d.ts +3 -1
  411. package/dist/store/tasks-sqlite.d.ts.map +1 -1
  412. package/dist/store/tasks-sqlite.js +309 -279
  413. package/dist/store/tasks-sqlite.js.map +1 -1
  414. package/dist/store/validation-schemas.d.ts +8 -8
  415. package/dist/store/writer-lease.d.ts +80 -8
  416. package/dist/store/writer-lease.d.ts.map +1 -1
  417. package/dist/store/writer-lease.js +503 -134
  418. package/dist/store/writer-lease.js.map +1 -1
  419. package/dist/system/audit.d.ts +2 -1
  420. package/dist/system/audit.d.ts.map +1 -1
  421. package/dist/system/audit.js +118 -71
  422. package/dist/system/audit.js.map +1 -1
  423. package/dist/system/backup.d.ts.map +1 -1
  424. package/dist/system/backup.js +5 -5
  425. package/dist/system/backup.js.map +1 -1
  426. package/dist/system/inject-generate.js +1 -1
  427. package/dist/task-work/index.d.ts +3 -0
  428. package/dist/task-work/index.d.ts.map +1 -1
  429. package/dist/task-work/index.js +34 -37
  430. package/dist/task-work/index.js.map +1 -1
  431. package/dist/tasks/ac-coverage-gate.d.ts.map +1 -1
  432. package/dist/tasks/ac-coverage-gate.js +4 -0
  433. package/dist/tasks/ac-coverage-gate.js.map +1 -1
  434. package/dist/tasks/ac-immutability.d.ts +7 -4
  435. package/dist/tasks/ac-immutability.d.ts.map +1 -1
  436. package/dist/tasks/ac-immutability.js +19 -17
  437. package/dist/tasks/ac-immutability.js.map +1 -1
  438. package/dist/tasks/ac-table.d.ts +5 -5
  439. package/dist/tasks/ac-table.d.ts.map +1 -1
  440. package/dist/tasks/ac-table.js +26 -12
  441. package/dist/tasks/ac-table.js.map +1 -1
  442. package/dist/tasks/acceptance-input.d.ts +52 -0
  443. package/dist/tasks/acceptance-input.d.ts.map +1 -0
  444. package/dist/tasks/acceptance-input.js +259 -0
  445. package/dist/tasks/acceptance-input.js.map +1 -0
  446. package/dist/tasks/add.d.ts +67 -29
  447. package/dist/tasks/add.d.ts.map +1 -1
  448. package/dist/tasks/add.js +255 -97
  449. package/dist/tasks/add.js.map +1 -1
  450. package/dist/tasks/complete.d.ts +16 -1
  451. package/dist/tasks/complete.d.ts.map +1 -1
  452. package/dist/tasks/complete.js +152 -55
  453. package/dist/tasks/complete.js.map +1 -1
  454. package/dist/tasks/decompose.d.ts +103 -0
  455. package/dist/tasks/decompose.d.ts.map +1 -0
  456. package/dist/tasks/decompose.js +224 -0
  457. package/dist/tasks/decompose.js.map +1 -0
  458. package/dist/tasks/delete.d.ts +5 -8
  459. package/dist/tasks/delete.d.ts.map +1 -1
  460. package/dist/tasks/delete.js +4 -3
  461. package/dist/tasks/delete.js.map +1 -1
  462. package/dist/tasks/dependency-check.d.ts +59 -0
  463. package/dist/tasks/dependency-check.d.ts.map +1 -1
  464. package/dist/tasks/dependency-check.js +73 -0
  465. package/dist/tasks/dependency-check.js.map +1 -1
  466. package/dist/tasks/deps-ready.d.ts +13 -1
  467. package/dist/tasks/deps-ready.d.ts.map +1 -1
  468. package/dist/tasks/deps-ready.js +16 -5
  469. package/dist/tasks/deps-ready.js.map +1 -1
  470. package/dist/tasks/engine-converters.d.ts +7 -0
  471. package/dist/tasks/engine-converters.d.ts.map +1 -1
  472. package/dist/tasks/engine-converters.js +8 -1
  473. package/dist/tasks/engine-converters.js.map +1 -1
  474. package/dist/tasks/evidence.d.ts +88 -27
  475. package/dist/tasks/evidence.d.ts.map +1 -1
  476. package/dist/tasks/evidence.js +344 -43
  477. package/dist/tasks/evidence.js.map +1 -1
  478. package/dist/tasks/find.d.ts +16 -12
  479. package/dist/tasks/find.d.ts.map +1 -1
  480. package/dist/tasks/find.js +107 -93
  481. package/dist/tasks/find.js.map +1 -1
  482. package/dist/tasks/gate-preview.d.ts +73 -0
  483. package/dist/tasks/gate-preview.d.ts.map +1 -0
  484. package/dist/tasks/gate-preview.js +119 -0
  485. package/dist/tasks/gate-preview.js.map +1 -0
  486. package/dist/tasks/gate-runner.d.ts +88 -14
  487. package/dist/tasks/gate-runner.d.ts.map +1 -1
  488. package/dist/tasks/gate-runner.js +719 -250
  489. package/dist/tasks/gate-runner.js.map +1 -1
  490. package/dist/tasks/hierarchy-policy.d.ts +8 -0
  491. package/dist/tasks/hierarchy-policy.d.ts.map +1 -1
  492. package/dist/tasks/hierarchy-policy.js +9 -2
  493. package/dist/tasks/hierarchy-policy.js.map +1 -1
  494. package/dist/tasks/hierarchy.d.ts +67 -1
  495. package/dist/tasks/hierarchy.d.ts.map +1 -1
  496. package/dist/tasks/hierarchy.js +81 -1
  497. package/dist/tasks/hierarchy.js.map +1 -1
  498. package/dist/tasks/index.d.ts +3 -0
  499. package/dist/tasks/index.d.ts.map +1 -1
  500. package/dist/tasks/index.js +3 -0
  501. package/dist/tasks/index.js.map +1 -1
  502. package/dist/tasks/infer-add-params.d.ts +1 -20
  503. package/dist/tasks/infer-add-params.d.ts.map +1 -1
  504. package/dist/tasks/infer-add-params.js +2 -203
  505. package/dist/tasks/infer-add-params.js.map +1 -1
  506. package/dist/tasks/list.d.ts +7 -2
  507. package/dist/tasks/list.d.ts.map +1 -1
  508. package/dist/tasks/list.js +24 -36
  509. package/dist/tasks/list.js.map +1 -1
  510. package/dist/tasks/ops.d.ts +6 -61
  511. package/dist/tasks/ops.d.ts.map +1 -1
  512. package/dist/tasks/ops.js +5 -53
  513. package/dist/tasks/ops.js.map +1 -1
  514. package/dist/tasks/plan.d.ts +15 -1
  515. package/dist/tasks/plan.d.ts.map +1 -1
  516. package/dist/tasks/plan.js +23 -25
  517. package/dist/tasks/plan.js.map +1 -1
  518. package/dist/tasks/population.d.ts +46 -0
  519. package/dist/tasks/population.d.ts.map +1 -0
  520. package/dist/tasks/population.js +88 -0
  521. package/dist/tasks/population.js.map +1 -0
  522. package/dist/tasks/reconcile-scope.d.ts +157 -0
  523. package/dist/tasks/reconcile-scope.d.ts.map +1 -0
  524. package/dist/tasks/reconcile-scope.js +242 -0
  525. package/dist/tasks/reconcile-scope.js.map +1 -0
  526. package/dist/tasks/req.d.ts +51 -12
  527. package/dist/tasks/req.d.ts.map +1 -1
  528. package/dist/tasks/req.js +125 -69
  529. package/dist/tasks/req.js.map +1 -1
  530. package/dist/tasks/session-scope.d.ts +6 -31
  531. package/dist/tasks/session-scope.d.ts.map +1 -1
  532. package/dist/tasks/session-scope.js +3 -17
  533. package/dist/tasks/session-scope.js.map +1 -1
  534. package/dist/tasks/severity-attestation.d.ts +39 -10
  535. package/dist/tasks/severity-attestation.d.ts.map +1 -1
  536. package/dist/tasks/severity-attestation.js +75 -36
  537. package/dist/tasks/severity-attestation.js.map +1 -1
  538. package/dist/tasks/task-next.d.ts +5 -0
  539. package/dist/tasks/task-next.d.ts.map +1 -1
  540. package/dist/tasks/task-next.js +10 -9
  541. package/dist/tasks/task-next.js.map +1 -1
  542. package/dist/tasks/task-reparent.d.ts.map +1 -1
  543. package/dist/tasks/task-reparent.js +3 -13
  544. package/dist/tasks/task-reparent.js.map +1 -1
  545. package/dist/tasks/update.d.ts +20 -43
  546. package/dist/tasks/update.d.ts.map +1 -1
  547. package/dist/tasks/update.js +64 -49
  548. package/dist/tasks/update.js.map +1 -1
  549. package/dist/validation/engine-ops.d.ts +17 -1
  550. package/dist/validation/engine-ops.d.ts.map +1 -1
  551. package/dist/validation/engine-ops.js +149 -11
  552. package/dist/validation/engine-ops.js.map +1 -1
  553. package/dist/workgraph/reparent-preflight.d.ts +1 -1
  554. package/dist/workgraph/reparent-preflight.d.ts.map +1 -1
  555. package/dist/workgraph/reparent-preflight.js +6 -1
  556. package/dist/workgraph/reparent-preflight.js.map +1 -1
  557. package/dist/workgraph/scaffold-apply.d.ts.map +1 -1
  558. package/dist/workgraph/scaffold-apply.js +81 -76
  559. package/dist/workgraph/scaffold-apply.js.map +1 -1
  560. package/dist/workgraph/structure-validation.d.ts +1 -1
  561. package/dist/workgraph/structure-validation.d.ts.map +1 -1
  562. package/dist/workgraph/structure-validation.js +6 -1
  563. package/dist/workgraph/structure-validation.js.map +1 -1
  564. package/migrations/drizzle-tasks/20260919180000_t12263-job-ownership-fences/migration.sql +34 -0
  565. package/migrations/drizzle-tasks/20260919184500_t12265-durable-pending-proposals/migration.sql +4 -0
  566. package/package.json +18 -13
  567. package/schemas/config.schema.json +2 -2
  568. package/schemas/project-context.schema.json +12 -0
  569. package/templates/CLEO-INJECTION.md +114 -101
@@ -10,11 +10,16 @@
10
10
  *
11
11
  * @epic T4454
12
12
  */
13
+ import { AsyncLocalStorage } from 'node:async_hooks';
14
+ import { resolve } from 'node:path';
13
15
  import { ARCHIVE_REASON_TOMBSTONE, } from '@cleocode/contracts';
14
- import { and, eq, inArray, isNull, like, ne, notInArray, or, sql } from 'drizzle-orm';
16
+ import { and, eq, getTableColumns, inArray, isNull, like, ne, notInArray, or, sql, } from 'drizzle-orm';
17
+ import { captureProjectScope as captureTaskAccessorScope, getProjectRoot, worktreeScope, } from '../project-scope.js';
18
+ import { withBackgroundOpCommitBoundary } from './background-ops.js';
15
19
  import { archivedTaskToRow, rowToSession, rowToTask, taskToRow } from './converters.js';
16
20
  import { cleanupBrainRefsOnSessionDelete } from './cross-db-cleanup.js';
17
- import { batchUpdateDependencies, loadDependenciesForTasks, loadRelationsForTasks, updateDependencies, upsertSession, upsertTask, } from './db-helpers.js';
21
+ import { batchUpdateDependencies, loadDependenciesForTasks, loadRelationsForTasks, parseLabels, updateDependencies, updateTaskLabels, upsertSession, upsertTask, } from './db-helpers.js';
22
+ import { tasksAuditLog } from './schema/cleo-project/audit.js';
18
23
  import { resolveCurrentSession } from './session-store.js';
19
24
  import { closeDb, getDb, getNativeTasksDb } from './sqlite.js';
20
25
  import { TERMINAL_TASK_STATUSES } from './status-registry.js';
@@ -36,46 +41,6 @@ function generateAuditLogId() {
36
41
  const rand = Math.random().toString(36).slice(2, 8);
37
42
  return `log-${epoch}-${rand}`;
38
43
  }
39
- /**
40
- * Run `fn` inside an outermost `BEGIN IMMEDIATE` transaction, wrapped
41
- * in {@link withWriteRetry} so SQLITE_BUSY contention is absorbed at the
42
- * transaction boundary rather than mid-statement.
43
- *
44
- * - Acquires a RESERVED lock immediately so concurrent writers queue
45
- * politely (or fall through to retry after `busy_timeout` expires).
46
- * - Commits on success; rolls back on any thrown error before
47
- * propagating it. If the thrown error is SQLITE_BUSY,
48
- * {@link withWriteRetry} re-runs the WHOLE function (BEGIN included)
49
- * after backoff — better-sqlite3 / node:sqlite forbids nesting
50
- * `BEGIN IMMEDIATE` inside an open transaction, so retry MUST start
51
- * from this outer boundary.
52
- *
53
- * DO NOT call this from inside an already-open transaction. For nested
54
- * writes use `accessor.transaction()` which uses SAVEPOINTs (T9814).
55
- *
56
- * @bug gh-391 — SQLITE_BUSY contention on parallel writes.
57
- * @task T9839
58
- */
59
- async function runInImmediateTx(nativeDb, fn) {
60
- return withWriteRetry(async () => {
61
- nativeDb.prepare('BEGIN IMMEDIATE').run();
62
- try {
63
- const result = await fn();
64
- nativeDb.prepare('COMMIT').run();
65
- return result;
66
- }
67
- catch (err) {
68
- try {
69
- nativeDb.prepare('ROLLBACK').run();
70
- }
71
- catch {
72
- // Secondary rollback failures are non-fatal — propagate the original
73
- // error which captures the actual cause.
74
- }
75
- throw err;
76
- }
77
- });
78
- }
79
44
  // ---- Schema meta helpers ----
80
45
  /** Read a JSON blob from the schema_meta table by key. */
81
46
  async function getMetaValue(cwd, key) {
@@ -94,8 +59,13 @@ async function getMetaValue(cwd, key) {
94
59
  return null;
95
60
  }
96
61
  }
97
- /** Write a JSON blob to the schema_meta table by key. */
62
+ /** Write metadata through the same ownership queue as task mutations. */
98
63
  export async function setMetaValue(cwd, key, value) {
64
+ const accessor = await createSqliteDataAccessor(cwd);
65
+ await accessor.setMetaValue(key, value);
66
+ }
67
+ /** Write metadata while the caller owns the transaction. */
68
+ async function writeMetaValue(cwd, key, value) {
99
69
  const db = await getDb(cwd);
100
70
  const json = JSON.stringify(value);
101
71
  await db
@@ -107,17 +77,125 @@ export async function setMetaValue(cwd, key, value) {
107
77
  })
108
78
  .run();
109
79
  }
80
+ // One queue per shared native handle, independent of accessor identity. These
81
+ // hold coordination state only; the ProjectStore remains the owner of handles.
82
+ const taskTransactionQueue = new WeakMap();
83
+ /** One lexical transaction lifetime; its queue serializes sibling savepoints. */
84
+ class TaskTransactionScope {
85
+ native;
86
+ parent;
87
+ active = true;
88
+ pending = Promise.resolve();
89
+ constructor(native, parent) {
90
+ this.native = native;
91
+ this.parent = parent;
92
+ }
93
+ assertActive() {
94
+ if (!this.active)
95
+ throw new Error('Transaction scope has ended');
96
+ }
97
+ }
98
+ const taskTransactionContext = new AsyncLocalStorage();
99
+ function activeTransactionScope(native) {
100
+ let scope = taskTransactionContext.getStore();
101
+ while (scope) {
102
+ if (!scope.active)
103
+ return undefined;
104
+ if (scope.native === native)
105
+ return scope;
106
+ scope = scope.parent;
107
+ }
108
+ return undefined;
109
+ }
110
+ /** Serialize committed effects with foreground writers without opening a SQL transaction. */
111
+ async function scheduleTaskBackground(native, work) {
112
+ const previous = taskTransactionQueue.get(native) ?? Promise.resolve();
113
+ const released = Promise.withResolvers();
114
+ const tail = previous.then(() => released.promise);
115
+ taskTransactionQueue.set(native, tail);
116
+ await previous;
117
+ const scope = new TaskTransactionScope(native);
118
+ try {
119
+ return await taskTransactionContext.run(scope, work);
120
+ }
121
+ finally {
122
+ await scope.pending;
123
+ scope.active = false;
124
+ released.resolve();
125
+ if (taskTransactionQueue.get(native) === tail)
126
+ taskTransactionQueue.delete(native);
127
+ }
128
+ }
129
+ /**
130
+ * Compatible task-accessor name for the shared project ownership capture policy.
131
+ * @remarks This is the same function as `captureProjectScope`; identity validation
132
+ * and routing policy live in the dependency-leaf project scope module.
133
+ * @example
134
+ * ```ts
135
+ * const scope = captureTaskAccessorScope(getProjectRoot(), worktreeScope.getStore());
136
+ * ```
137
+ */
138
+ export { captureProjectScope as captureTaskAccessorScope } from '../project-scope.js';
139
+ /**
140
+ * Bind all asynchronous accessor methods, including transaction ports, to one project.
141
+ * @typeParam T - Concrete accessor contract retained by the scoped wrapper.
142
+ * @param accessor - Canonical, safety-wrapped, or transaction accessor to bind.
143
+ * @param scope - Immutable project scope captured at construction.
144
+ * @returns An accessor preserving method arguments, results and receiver semantics.
145
+ * @remarks The wrapper changes only worktree path scope. The existing transaction
146
+ * AsyncLocalStorage, savepoint queues and postcommit result semantics remain authoritative.
147
+ * A newly inherited execution cannot bypass a captured operation's cancellation or ownership.
148
+ * @example
149
+ * ```ts
150
+ * const bound = bindTaskAccessorScope(accessor, scope);
151
+ * await bound.upsertSingleTask(task);
152
+ * ```
153
+ */
154
+ export function bindTaskAccessorScope(accessor, scope) {
155
+ return new Proxy(accessor, {
156
+ get(target, property) {
157
+ const value = target[property];
158
+ if (typeof value !== 'function')
159
+ return value;
160
+ return new Proxy(value, {
161
+ async apply(method, _receiver, args) {
162
+ const activeExecution = worktreeScope.getStore()?.execution;
163
+ if (scope.execution && activeExecution && scope.execution !== activeExecution)
164
+ throw new Error('Task accessor cannot replace another captured execution authority.');
165
+ const invocationScope = captureTaskAccessorScope(scope.worktreeRoot, {
166
+ ...scope,
167
+ execution: scope.execution ?? activeExecution,
168
+ });
169
+ return worktreeScope.run(invocationScope, () => Reflect.apply(method, target, args));
170
+ },
171
+ });
172
+ },
173
+ });
174
+ }
110
175
  // ---- Accessor factory ----
111
176
  /**
112
177
  * Create a SQLite-backed DataAccessor.
113
178
  *
114
- * Opens (or creates) the SQLite database at `.cleo/tasks.db` and returns
179
+ * Opens (or creates) the consolidated SQLite database at `.cleo/cleo.db` and returns
115
180
  * a DataAccessor that materializes/dematerializes whole-file structures
116
181
  * from the relational tables.
117
182
  *
118
- * @param cwd - Working directory for path resolution (defaults to process.cwd())
183
+ * @param cwd - Explicit project root, or canonical root captured before initialization.
184
+ * @returns An accessor whose methods and transaction ports retain captured ownership.
185
+ * @remarks Later ambient scope changes cannot redirect this accessor's I/O. Active
186
+ * execution ownership is checked against persisted identity before method entry.
187
+ * @example
188
+ * ```ts
189
+ * const accessor = await createSqliteDataAccessor('/projects/example');
190
+ * const task = await accessor.loadSingleTask('T001');
191
+ * ```
119
192
  */
120
193
  export async function createSqliteDataAccessor(cwd) {
194
+ const scope = captureTaskAccessorScope(resolve(cwd ?? getProjectRoot()), worktreeScope.getStore());
195
+ return worktreeScope.run(scope, async () => bindTaskAccessorScope(await createOwnedSqliteDataAccessor(scope.worktreeRoot, scope), scope));
196
+ }
197
+ /** Construct the raw implementation inside its already captured project scope. */
198
+ async function createOwnedSqliteDataAccessor(cwd, accessorScope) {
121
199
  // Eagerly initialize the database to ensure tables exist
122
200
  await getDb(cwd);
123
201
  /** Load all task IDs for cross-task dependency validation. */
@@ -126,12 +204,13 @@ export async function createSqliteDataAccessor(cwd) {
126
204
  const rows = await db.select({ id: schema.tasks.id }).from(schema.tasks).all();
127
205
  return new Set(rows.map((r) => r.id));
128
206
  }
129
- // Per-accessor transaction nesting depth (T9814).
130
- // Tracks how many DataAccessor.transaction() calls are active on this
131
- // accessor instance. Used to choose BEGIN IMMEDIATE (outer, depth=0) vs
132
- // SAVEPOINT (nested, depth>0) so we preserve BEGIN IMMEDIATE locking
133
- // semantics at the outermost level while enabling batch-insert nesting.
134
- let _txDepth = 0;
207
+ async function requireNativeDb() {
208
+ await getDb(cwd);
209
+ const nativeDb = getNativeTasksDb(cwd);
210
+ if (!nativeDb)
211
+ throw new Error('Native database not initialized');
212
+ return nativeDb;
213
+ }
135
214
  const accessor = {
136
215
  engine: 'sqlite',
137
216
  // ---- loadArchive ----
@@ -180,22 +259,16 @@ export async function createSqliteDataAccessor(cwd) {
180
259
  },
181
260
  // ---- saveArchive ----
182
261
  async saveArchive(data) {
183
- const db = await getDb(cwd);
184
- // Pre-compute archive IDs + active task IDs for dependency validation
185
- const archiveIds = new Set(data.archivedTasks.map((t) => t.id));
186
- const activeRows = await db
187
- .select({ id: schema.tasks.id })
188
- .from(schema.tasks)
189
- .where(ne(schema.tasks.status, 'archived'))
190
- .all();
191
- const validDepIds = new Set([...archiveIds, ...activeRows.map((r) => r.id)]);
192
- // Wrap all upserts + dependency updates in a single transaction.
193
- // Retry on SQLITE_BUSY contention via runInImmediateTx (gh#391).
194
- const nativeDb = getNativeTasksDb();
195
- if (!nativeDb) {
196
- throw new Error('Native database not initialized');
197
- }
198
- await runInImmediateTx(nativeDb, async () => {
262
+ return accessor.transaction(async () => {
263
+ const db = await getDb(cwd);
264
+ // Pre-compute archive IDs + active task IDs for dependency validation
265
+ const archiveIds = new Set(data.archivedTasks.map((t) => t.id));
266
+ const activeRows = await db
267
+ .select({ id: schema.tasks.id })
268
+ .from(schema.tasks)
269
+ .where(ne(schema.tasks.status, 'archived'))
270
+ .all();
271
+ const validDepIds = new Set([...archiveIds, ...activeRows.map((r) => r.id)]);
199
272
  // Collect dependency data for batch update
200
273
  const depBatch = [];
201
274
  for (const task of data.archivedTasks) {
@@ -227,67 +300,74 @@ export async function createSqliteDataAccessor(cwd) {
227
300
  },
228
301
  // ---- saveSessions ----
229
302
  async saveSessions(sessions) {
230
- const db = await getDb(cwd);
231
- // Get existing session IDs
232
- const existingRows = await db.select({ id: schema.sessions.id }).from(schema.sessions).all();
233
- const existingIds = new Set(existingRows.map((r) => r.id));
234
- const incomingIds = new Set(sessions.map((s) => s.id));
235
- // Delete sessions that are no longer in the data
236
- for (const eid of existingIds) {
237
- if (!incomingIds.has(eid)) {
238
- await db.delete(schema.sessions).where(eq(schema.sessions.id, eid)).run();
303
+ return accessor.transaction(async () => {
304
+ const db = await getDb(cwd);
305
+ // Get existing session IDs
306
+ const existingRows = await db
307
+ .select({ id: schema.sessions.id })
308
+ .from(schema.sessions)
309
+ .all();
310
+ const existingIds = new Set(existingRows.map((r) => r.id));
311
+ const incomingIds = new Set(sessions.map((s) => s.id));
312
+ // Delete sessions that are no longer in the data
313
+ for (const eid of existingIds) {
314
+ if (!incomingIds.has(eid)) {
315
+ await db.delete(schema.sessions).where(eq(schema.sessions.id, eid)).run();
316
+ }
239
317
  }
240
- }
241
- // Upsert all sessions
242
- for (const session of sessions) {
243
- await upsertSession(db, session);
244
- }
318
+ // Upsert all sessions
319
+ for (const session of sessions) {
320
+ await upsertSession(db, session);
321
+ }
322
+ });
245
323
  },
246
324
  // ---- appendLog ----
247
325
  async appendLog(entry) {
248
- const db = await getDb(cwd);
249
- // gh#391: every task mutation appends one audit row; retry on contention.
250
- await withWriteRetry(() => db
251
- .insert(schema.auditLog)
252
- .values({
253
- id: entry.id ?? generateAuditLogId(),
254
- timestamp: entry.timestamp ?? new Date().toISOString(),
255
- action: entry.action ?? entry.operation ?? 'unknown',
256
- taskId: entry.taskId ?? 'unknown',
257
- actor: entry.actor ?? 'system',
258
- detailsJson: entry.details ? JSON.stringify(entry.details) : '{}',
259
- beforeJson: entry.before ? JSON.stringify(entry.before) : null,
260
- afterJson: entry.after ? JSON.stringify(entry.after) : null,
261
- })
262
- .run());
326
+ return accessor.transaction(async () => {
327
+ const db = await getDb(cwd);
328
+ // gh#391: every task mutation appends one audit row; retry on contention.
329
+ await withWriteRetry(() => db
330
+ .insert(tasksAuditLog)
331
+ .values({
332
+ id: entry.id ?? generateAuditLogId(),
333
+ timestamp: entry.timestamp ?? new Date().toISOString(),
334
+ action: entry.action ?? entry.operation ?? 'unknown',
335
+ taskId: entry.taskId ?? 'unknown',
336
+ actor: entry.actor ?? 'system',
337
+ detailsJson: entry.details ? JSON.stringify(entry.details) : '{}',
338
+ beforeJson: entry.before ? JSON.stringify(entry.before) : null,
339
+ afterJson: entry.after ? JSON.stringify(entry.after) : null,
340
+ })
341
+ .run());
342
+ });
263
343
  },
264
344
  async queryAuditLog(query) {
265
345
  const db = await getDb(cwd);
266
346
  const conditions = [];
267
347
  if (query.taskIds && query.taskIds.length > 0) {
268
- conditions.push(inArray(schema.auditLog.taskId, [...query.taskIds]));
348
+ conditions.push(inArray(tasksAuditLog.taskId, [...query.taskIds]));
269
349
  }
270
350
  if (query.actions && query.actions.length > 0) {
271
- conditions.push(inArray(schema.auditLog.action, [...query.actions]));
351
+ conditions.push(inArray(tasksAuditLog.action, [...query.actions]));
272
352
  }
273
353
  if (query.since) {
274
- conditions.push(sql `${schema.auditLog.timestamp} >= ${query.since}`);
354
+ conditions.push(sql `${tasksAuditLog.timestamp} >= ${query.since}`);
275
355
  }
276
356
  const limit = Math.max(1, Math.floor(query.limit ?? 100));
277
357
  const rows = await db
278
358
  .select({
279
- id: schema.auditLog.id,
280
- timestamp: schema.auditLog.timestamp,
281
- action: schema.auditLog.action,
282
- taskId: schema.auditLog.taskId,
283
- actor: schema.auditLog.actor,
284
- detailsJson: schema.auditLog.detailsJson,
285
- beforeJson: schema.auditLog.beforeJson,
286
- afterJson: schema.auditLog.afterJson,
359
+ id: tasksAuditLog.id,
360
+ timestamp: tasksAuditLog.timestamp,
361
+ action: tasksAuditLog.action,
362
+ taskId: tasksAuditLog.taskId,
363
+ actor: tasksAuditLog.actor,
364
+ detailsJson: tasksAuditLog.detailsJson,
365
+ beforeJson: tasksAuditLog.beforeJson,
366
+ afterJson: tasksAuditLog.afterJson,
287
367
  })
288
- .from(schema.auditLog)
368
+ .from(tasksAuditLog)
289
369
  .where(conditions.length > 0 ? and(...conditions) : undefined)
290
- .orderBy(sql `${schema.auditLog.timestamp} DESC`)
370
+ .orderBy(sql `${tasksAuditLog.timestamp} DESC`)
291
371
  .limit(limit)
292
372
  .all();
293
373
  return rows.map((row) => ({
@@ -303,51 +383,12 @@ export async function createSqliteDataAccessor(cwd) {
303
383
  },
304
384
  // ---- Fine-grained task operations (T5034) ----
305
385
  async upsertSingleTask(task) {
306
- const db = await getDb(cwd);
307
- const row = taskToRow(task);
308
- // gh#391: wrap multi-statement write (task row + dependency rows) in
309
- // a retry loop. SQLite implicitly auto-commits each statement, but
310
- // concurrent writers can still observe SQLITE_BUSY mid-sequence.
311
- await withWriteRetry(async () => {
312
- await upsertTask(db, row);
313
- await updateDependencies(db, task.id, task.depends ?? []);
314
- });
386
+ await accessor.transaction((tx) => tx.upsertSingleTask(task));
315
387
  },
316
388
  async addRelation(taskId, relatedTo, relationType, reason) {
317
- const db = await getDb(cwd);
318
- // Validate relation type - throw on invalid (T5168)
319
- const validTypes = [
320
- 'related',
321
- 'blocks',
322
- 'duplicates',
323
- 'absorbs',
324
- 'fixes',
325
- 'extends',
326
- 'supersedes',
327
- 'groups', // ADR-073: Saga member linkage
328
- ];
329
- if (!validTypes.includes(relationType)) {
330
- throw new Error(`Invalid relation type: ${relationType}. Valid types: ${validTypes.join(', ')}`);
331
- }
332
- // gh#391: retry on SQLITE_BUSY contention.
333
- await withWriteRetry(() => db
334
- .insert(schema.taskRelations)
335
- .values({
336
- taskId,
337
- relatedTo,
338
- relationType: relationType,
339
- reason: reason ?? null,
340
- })
341
- .onConflictDoNothing()
342
- .run());
343
- },
344
- async removeRelation(taskId, relatedTo, relationType) {
345
- const db = await getDb(cwd);
346
- const conditions = [
347
- eq(schema.taskRelations.taskId, taskId),
348
- eq(schema.taskRelations.relatedTo, relatedTo),
349
- ];
350
- if (relationType !== undefined) {
389
+ return accessor.transaction(async () => {
390
+ const db = await getDb(cwd);
391
+ // Validate relation type - throw on invalid (T5168)
351
392
  const validTypes = [
352
393
  'related',
353
394
  'blocks',
@@ -361,13 +402,48 @@ export async function createSqliteDataAccessor(cwd) {
361
402
  if (!validTypes.includes(relationType)) {
362
403
  throw new Error(`Invalid relation type: ${relationType}. Valid types: ${validTypes.join(', ')}`);
363
404
  }
364
- conditions.push(eq(schema.taskRelations.relationType, relationType));
365
- }
366
- // gh#391: retry on SQLITE_BUSY contention.
367
- await withWriteRetry(() => db
368
- .delete(schema.taskRelations)
369
- .where(and(...conditions))
370
- .run());
405
+ // gh#391: retry on SQLITE_BUSY contention.
406
+ await withWriteRetry(() => db
407
+ .insert(schema.taskRelations)
408
+ .values({
409
+ taskId,
410
+ relatedTo,
411
+ relationType: relationType,
412
+ reason: reason ?? null,
413
+ })
414
+ .onConflictDoNothing()
415
+ .run());
416
+ });
417
+ },
418
+ async removeRelation(taskId, relatedTo, relationType) {
419
+ return accessor.transaction(async () => {
420
+ const db = await getDb(cwd);
421
+ const conditions = [
422
+ eq(schema.taskRelations.taskId, taskId),
423
+ eq(schema.taskRelations.relatedTo, relatedTo),
424
+ ];
425
+ if (relationType !== undefined) {
426
+ const validTypes = [
427
+ 'related',
428
+ 'blocks',
429
+ 'duplicates',
430
+ 'absorbs',
431
+ 'fixes',
432
+ 'extends',
433
+ 'supersedes',
434
+ 'groups', // ADR-073: Saga member linkage
435
+ ];
436
+ if (!validTypes.includes(relationType)) {
437
+ throw new Error(`Invalid relation type: ${relationType}. Valid types: ${validTypes.join(', ')}`);
438
+ }
439
+ conditions.push(eq(schema.taskRelations.relationType, relationType));
440
+ }
441
+ // gh#391: retry on SQLITE_BUSY contention.
442
+ await withWriteRetry(() => db
443
+ .delete(schema.taskRelations)
444
+ .where(and(...conditions))
445
+ .run());
446
+ });
371
447
  },
372
448
  // ---- AC rows (T10508) ----
373
449
  async getAcRows(taskId) {
@@ -429,46 +505,50 @@ export async function createSqliteDataAccessor(cwd) {
429
505
  }));
430
506
  },
431
507
  async archiveSingleTask(taskId, fields) {
432
- const db = await getDb(cwd);
433
- // Verify the task exists before archiving
434
- const rows = await db
435
- .select({ id: schema.tasks.id })
436
- .from(schema.tasks)
437
- .where(eq(schema.tasks.id, taskId))
438
- .all();
439
- if (rows.length === 0)
440
- return;
441
- // gh#391: retry on SQLITE_BUSY contention.
442
- await withWriteRetry(() => db
443
- .update(schema.tasks)
444
- .set({
445
- status: 'archived',
446
- archivedAt: fields.archivedAt ?? new Date().toISOString(),
447
- archiveReason: fields.archiveReason ?? ARCHIVE_REASON_TOMBSTONE,
448
- cycleTimeDays: fields.cycleTimeDays ?? null,
449
- updatedAt: new Date().toISOString(),
450
- })
451
- .where(eq(schema.tasks.id, taskId))
452
- .run());
508
+ return accessor.transaction(async () => {
509
+ const db = await getDb(cwd);
510
+ // Verify the task exists before archiving
511
+ const rows = await db
512
+ .select({ id: schema.tasks.id })
513
+ .from(schema.tasks)
514
+ .where(eq(schema.tasks.id, taskId))
515
+ .all();
516
+ if (rows.length === 0)
517
+ return;
518
+ // gh#391: retry on SQLITE_BUSY contention.
519
+ await withWriteRetry(() => db
520
+ .update(schema.tasks)
521
+ .set({
522
+ status: 'archived',
523
+ archivedAt: fields.archivedAt ?? new Date().toISOString(),
524
+ archiveReason: fields.archiveReason ?? ARCHIVE_REASON_TOMBSTONE,
525
+ cycleTimeDays: fields.cycleTimeDays ?? null,
526
+ updatedAt: new Date().toISOString(),
527
+ })
528
+ .where(eq(schema.tasks.id, taskId))
529
+ .run());
530
+ });
453
531
  },
454
532
  async removeSingleTask(taskId) {
455
- const db = await getDb(cwd);
456
- // gh#391: wrap the three-statement delete sequence in one retry
457
- // boundary. SQLITE_BUSY mid-sequence would leak dangling task_dependencies
458
- // rows; retrying the whole sequence keeps the cleanup atomic at the
459
- // application level (foreign-key cascade catches edge cases regardless).
460
- await withWriteRetry(async () => {
461
- // Delete dependencies first (both directions)
462
- await db
463
- .delete(schema.taskDependencies)
464
- .where(eq(schema.taskDependencies.taskId, taskId))
465
- .run();
466
- await db
467
- .delete(schema.taskDependencies)
468
- .where(eq(schema.taskDependencies.dependsOn, taskId))
469
- .run();
470
- // Delete the task itself
471
- await db.delete(schema.tasks).where(eq(schema.tasks.id, taskId)).run();
533
+ return accessor.transaction(async () => {
534
+ const db = await getDb(cwd);
535
+ // gh#391: wrap the three-statement delete sequence in one retry
536
+ // boundary. SQLITE_BUSY mid-sequence would leak dangling task_dependencies
537
+ // rows; retrying the whole sequence keeps the cleanup atomic at the
538
+ // application level (foreign-key cascade catches edge cases regardless).
539
+ await withWriteRetry(async () => {
540
+ // Delete dependencies first (both directions)
541
+ await db
542
+ .delete(schema.taskDependencies)
543
+ .where(eq(schema.taskDependencies.taskId, taskId))
544
+ .run();
545
+ await db
546
+ .delete(schema.taskDependencies)
547
+ .where(eq(schema.taskDependencies.dependsOn, taskId))
548
+ .run();
549
+ // Delete the task itself
550
+ await db.delete(schema.tasks).where(eq(schema.tasks.id, taskId)).run();
551
+ });
472
552
  });
473
553
  },
474
554
  async loadSingleTask(taskId) {
@@ -509,14 +589,18 @@ export async function createSqliteDataAccessor(cwd) {
509
589
  return resolveCurrentSession(cwd);
510
590
  },
511
591
  async upsertSingleSession(session) {
512
- const db = await getDb(cwd);
513
- await upsertSession(db, session);
592
+ return accessor.transaction(async () => {
593
+ const db = await getDb(cwd);
594
+ await upsertSession(db, session);
595
+ });
514
596
  },
515
597
  async removeSingleSession(sessionId) {
516
- const db = await getDb(cwd);
517
- await db.delete(schema.sessions).where(eq(schema.sessions.id, sessionId)).run();
518
- // Best-effort cross-db cleanup: nullify brain.db references to this session
519
- void cleanupBrainRefsOnSessionDelete(sessionId, cwd);
598
+ return accessor.transaction(async () => {
599
+ const db = await getDb(cwd);
600
+ await db.delete(schema.sessions).where(eq(schema.sessions.id, sessionId)).run();
601
+ // Best-effort cross-db cleanup: nullify brain.db references to this session
602
+ void cleanupBrainRefsOnSessionDelete(sessionId, cwd);
603
+ });
520
604
  },
521
605
  // ---- Targeted query methods (Phase 2 modernization) ----
522
606
  async queryTasks(filters) {
@@ -685,16 +769,28 @@ export async function createSqliteDataAccessor(cwd) {
685
769
  return result?.count ?? 0;
686
770
  },
687
771
  async getAncestorChain(taskId) {
688
- const nativeDb = getNativeTasksDb();
689
- if (!nativeDb)
690
- return [];
772
+ const nativeDb = await requireNativeDb();
691
773
  const idRows = nativeDb
692
- .prepare(`WITH RECURSIVE ancestor_ids(id, depth) AS (
693
- SELECT parent_id, 0 FROM tasks_tasks WHERE id = ? AND parent_id IS NOT NULL
774
+ .prepare(
775
+ // T12307: the `path` column is a cycle guard, not decoration. `depth`
776
+ // increments on every iteration, so with UNION ALL — and even with a
777
+ // plain UNION — (id, depth) is distinct on every revisit and a
778
+ // self-parented or cyclic row recurses forever. SQLite materialises
779
+ // that in NATIVE memory, which `--max-old-space-size` does not bound,
780
+ // so the symptom is a silent VM death rather than a heap error: one
781
+ // seeded `parentId === id` fixture row killed five CI runs and OOM-froze
782
+ // a workstation three times before the mechanism was found. A depth cap
783
+ // would also terminate, but it silently truncates a legitimately deep
784
+ // chain — unacceptable in the subsystem whose whole job is depth.
785
+ `WITH RECURSIVE ancestor_ids(id, depth, path) AS (
786
+ SELECT parent_id, 0, '/' || id || '/' || parent_id || '/'
787
+ FROM tasks_tasks WHERE id = ? AND parent_id IS NOT NULL
694
788
  UNION ALL
695
- SELECT t.parent_id, a.depth + 1 FROM tasks_tasks t
696
- JOIN ancestor_ids a ON t.id = a.id
697
- WHERE t.parent_id IS NOT NULL
789
+ SELECT t.parent_id, a.depth + 1, a.path || t.parent_id || '/'
790
+ FROM tasks_tasks t
791
+ JOIN ancestor_ids a ON t.id = a.id
792
+ WHERE t.parent_id IS NOT NULL
793
+ AND a.path NOT LIKE '%/' || t.parent_id || '/%'
698
794
  )
699
795
  SELECT id FROM ancestor_ids ORDER BY depth DESC`)
700
796
  .all(taskId);
@@ -719,14 +815,17 @@ export async function createSqliteDataAccessor(cwd) {
719
815
  return tasks;
720
816
  },
721
817
  async getSubtree(rootId) {
722
- const nativeDb = getNativeTasksDb();
723
- if (!nativeDb)
724
- return [];
818
+ const nativeDb = await requireNativeDb();
725
819
  // Get IDs from the CTE, then load via Drizzle for proper conversion
726
820
  const idRows = nativeDb
727
- .prepare(`WITH RECURSIVE subtree AS (
821
+ .prepare(
822
+ // T12307: UNION, not UNION ALL. Every column here repeats verbatim on
823
+ // a revisit, so the dedupe terminates a self-parented or cyclic row;
824
+ // UNION ALL recurses forever in native memory. A parent edge is
825
+ // single-valued, so no legitimate row is ever deduped away.
826
+ `WITH RECURSIVE subtree AS (
728
827
  SELECT id FROM tasks_tasks WHERE id = ?
729
- UNION ALL
828
+ UNION
730
829
  SELECT t.id FROM tasks_tasks t
731
830
  JOIN subtree s ON t.parent_id = s.id
732
831
  )
@@ -770,9 +869,7 @@ export async function createSqliteDataAccessor(cwd) {
770
869
  return tasks;
771
870
  },
772
871
  async getDependencyChain(taskId) {
773
- const nativeDb = getNativeTasksDb();
774
- if (!nativeDb)
775
- return [];
872
+ const nativeDb = await requireNativeDb();
776
873
  const rows = nativeDb
777
874
  .prepare(`WITH RECURSIVE dep_chain(id) AS (
778
875
  SELECT depends_on FROM tasks_task_dependencies WHERE task_id = ?
@@ -813,7 +910,7 @@ export async function createSqliteDataAccessor(cwd) {
813
910
  },
814
911
  // ---- Position helpers (T024/T025) ----
815
912
  async getNextPosition(parentId) {
816
- const nativeDb = getNativeTasksDb();
913
+ const nativeDb = await requireNativeDb();
817
914
  if (!nativeDb) {
818
915
  throw new Error('Native database not initialized');
819
916
  }
@@ -827,66 +924,46 @@ export async function createSqliteDataAccessor(cwd) {
827
924
  return row?.next_pos ?? 1;
828
925
  },
829
926
  async shiftPositions(parentId, fromPosition, delta) {
830
- const nativeDb = getNativeTasksDb();
831
- if (!nativeDb) {
832
- throw new Error('Native database not initialized');
833
- }
834
- if (parentId === null) {
835
- nativeDb
836
- .prepare(`UPDATE tasks_tasks SET position = position + ?, position_version = position_version + 1, updated_at = ? WHERE parent_id IS NULL AND position >= ? AND status != 'archived'`)
837
- .run(delta, new Date().toISOString(), fromPosition);
838
- }
839
- else {
840
- nativeDb
841
- .prepare(`UPDATE tasks_tasks SET position = position + ?, position_version = position_version + 1, updated_at = ? WHERE parent_id = ? AND position >= ? AND status != 'archived'`)
842
- .run(delta, new Date().toISOString(), parentId, fromPosition);
843
- }
927
+ return accessor.transaction(async () => {
928
+ const nativeDb = await requireNativeDb();
929
+ if (!nativeDb) {
930
+ throw new Error('Native database not initialized');
931
+ }
932
+ if (parentId === null) {
933
+ nativeDb
934
+ .prepare(`UPDATE tasks_tasks SET position = position + ?, position_version = position_version + 1, updated_at = ? WHERE parent_id IS NULL AND position >= ? AND status != 'archived'`)
935
+ .run(delta, new Date().toISOString(), fromPosition);
936
+ }
937
+ else {
938
+ nativeDb
939
+ .prepare(`UPDATE tasks_tasks SET position = position + ?, position_version = position_version + 1, updated_at = ? WHERE parent_id = ? AND position >= ? AND status != 'archived'`)
940
+ .run(delta, new Date().toISOString(), parentId, fromPosition);
941
+ }
942
+ });
844
943
  },
845
944
  // ---- Targeted write methods ----
846
945
  async updateTaskFields(taskId, fields) {
847
- const db = await getDb(cwd);
848
- const updateRow = {
849
- updatedAt: fields.updatedAt ?? new Date().toISOString(),
850
- };
851
- // Copy only provided fields
852
- const fieldMap = [
853
- ['title', 'title'],
854
- ['description', 'description'],
855
- ['status', 'status'],
856
- ['priority', 'priority'],
857
- ['type', 'type'],
858
- ['parentId', 'parentId'],
859
- ['phase', 'phase'],
860
- ['size', 'size'],
861
- ['position', 'position'],
862
- ['positionVersion', 'positionVersion'],
863
- ['labelsJson', 'labelsJson'],
864
- ['notesJson', 'notesJson'],
865
- ['acceptanceJson', 'acceptanceJson'],
866
- ['filesJson', 'filesJson'],
867
- ['origin', 'origin'],
868
- ['blockedBy', 'blockedBy'],
869
- ['epicLifecycle', 'epicLifecycle'],
870
- ['noAutoComplete', 'noAutoComplete'],
871
- ['completedAt', 'completedAt'],
872
- ['cancelledAt', 'cancelledAt'],
873
- ['cancellationReason', 'cancellationReason'],
874
- ['verificationJson', 'verificationJson'],
875
- ['createdBy', 'createdBy'],
876
- ['modifiedBy', 'modifiedBy'],
877
- ['sessionId', 'sessionId'],
878
- ['assignee', 'assignee'],
879
- ['pipelineStage', 'pipelineStage'],
880
- ];
881
- for (const [key, col] of fieldMap) {
882
- if (fields[key] !== undefined) {
883
- updateRow[col] = fields[key];
946
+ return accessor.transaction(async () => {
947
+ const db = await getDb(cwd);
948
+ // TaskFieldUpdates uses the schema's field names. Reject unsupported runtime
949
+ // input before Drizzle can silently omit it; no second field map can drift.
950
+ const columns = getTableColumns(schema.tasks);
951
+ for (const key of Object.keys(fields)) {
952
+ if (!Object.hasOwn(columns, key) || key === 'id' || key === 'createdAt') {
953
+ throw new Error(`Unsupported task update field: ${key}`);
954
+ }
884
955
  }
885
- }
886
- // gh#391: this is the chokepoint for `cleo update <id> --add-labels`.
887
- // Parallel invocations from a single shell used to lose ~50% of writes
888
- // to SQLITE_BUSY; withWriteRetry recovers them.
889
- await withWriteRetry(() => db.update(schema.tasks).set(updateRow).where(eq(schema.tasks.id, taskId)).run());
956
+ const updateRow = { ...fields, updatedAt: fields.updatedAt ?? new Date().toISOString() };
957
+ // gh#391: this is the chokepoint for `cleo update <id> --add-labels`.
958
+ // Parallel invocations from a single shell used to lose ~50% of writes
959
+ // to SQLITE_BUSY; withWriteRetry recovers them.
960
+ const result = await withWriteRetry(() => db.update(schema.tasks).set(updateRow).where(eq(schema.tasks.id, taskId)).run());
961
+ if (Number(result.changes) !== 1)
962
+ throw new Error(`Task not found: ${taskId}`);
963
+ if (fields.labelsJson !== undefined) {
964
+ await updateTaskLabels(db, taskId, parseLabels(fields.labelsJson));
965
+ }
966
+ });
890
967
  },
891
968
  async transaction(fn) {
892
969
  // Await getDb first so _nativeDb is guaranteed to be the correct instance
@@ -898,225 +975,306 @@ export async function createSqliteDataAccessor(cwd) {
898
975
  if (!nativeDb) {
899
976
  throw new Error('Native database not initialized');
900
977
  }
901
- // Nested-transaction support (T9814 — batch insert):
902
- // - Outermost call (depth=0): use BEGIN IMMEDIATE — preserves the
903
- // RESERVED lock semantics that callers and tests depend on.
904
- // - Nested calls (depth>0): use SAVEPOINT — creates a logical
905
- // checkpoint inside the already-open transaction.
906
- const isOuter = _txDepth === 0;
907
- const spName = isOuter ? null : `_cleo_tx_${(_txSavepointCounter++).toString(36)}`;
908
- if (isOuter) {
909
- nativeDb.prepare('BEGIN IMMEDIATE').run();
910
- }
911
- else {
912
- nativeDb.prepare(`SAVEPOINT ${spName}`).run();
913
- }
914
- _txDepth++;
978
+ const context = activeTransactionScope(nativeDb);
979
+ const previous = context?.pending ?? taskTransactionQueue.get(nativeDb) ?? Promise.resolve();
980
+ const released = Promise.withResolvers();
981
+ const tail = previous.then(() => released.promise);
982
+ if (context)
983
+ context.pending = tail;
984
+ else
985
+ taskTransactionQueue.set(nativeDb, tail);
986
+ await previous;
915
987
  try {
916
- const tx = {
917
- async upsertSingleTask(task) {
918
- const row = taskToRow(task);
919
- await upsertTask(db, row);
920
- await updateDependencies(db, task.id, task.depends ?? []);
921
- },
922
- async archiveSingleTask(taskId, fields) {
923
- await db
924
- .update(schema.tasks)
925
- .set({
926
- status: 'archived',
927
- archivedAt: fields.archivedAt ?? new Date().toISOString(),
928
- archiveReason: fields.archiveReason ?? ARCHIVE_REASON_TOMBSTONE,
929
- cycleTimeDays: fields.cycleTimeDays ?? null,
930
- updatedAt: new Date().toISOString(),
931
- })
932
- .where(eq(schema.tasks.id, taskId))
933
- .run();
934
- },
935
- async removeSingleTask(taskId) {
936
- await db
937
- .delete(schema.taskDependencies)
938
- .where(eq(schema.taskDependencies.taskId, taskId))
939
- .run();
940
- await db
941
- .delete(schema.taskDependencies)
942
- .where(eq(schema.taskDependencies.dependsOn, taskId))
943
- .run();
944
- await db.delete(schema.tasks).where(eq(schema.tasks.id, taskId)).run();
945
- },
946
- async setMetaValue(key, value) {
947
- await setMetaValue(cwd, key, value);
948
- },
949
- async updateTaskFields(taskId, flds) {
950
- // Delegate to the outer accessor's implementation
951
- await accessor.updateTaskFields(taskId, flds);
952
- },
953
- async getChildren(parentId) {
954
- const rows = await db
955
- .select()
956
- .from(schema.tasks)
957
- .where(and(eq(schema.tasks.parentId, parentId), ne(schema.tasks.status, 'archived')))
958
- .orderBy(sql `${schema.tasks.position} ASC, ${schema.tasks.createdAt} ASC`)
959
- .all();
960
- return rows.map(rowToTask);
961
- },
962
- async appendLog(entry) {
963
- await accessor.appendLog(entry);
964
- },
965
- // T9514: persist relates mutations inside the transaction
966
- async addRelation(taskId, relatedTo, relationType, reason) {
967
- await accessor.addRelation(taskId, relatedTo, relationType, reason);
968
- },
969
- async removeRelation(taskId, relatedTo, relationType) {
970
- await accessor.removeRelation(taskId, relatedTo, relationType);
971
- },
972
- async clearRelations(taskId) {
973
- await db
974
- .delete(schema.taskRelations)
975
- .where(eq(schema.taskRelations.taskId, taskId))
976
- .run();
977
- },
978
- // ---- AC rows (T10508) ----
979
- async insertAcRows(rows) {
980
- if (rows.length === 0)
981
- return;
982
- await db
983
- .insert(schema.taskAcceptanceCriteria)
984
- .values(rows.map((r) => ({
985
- id: r.id,
986
- taskId: r.taskId,
987
- ordinal: r.ordinal,
988
- kind: r.kind ?? 'text',
989
- sourceKey: r.sourceKey ?? `text:${r.ordinal}`,
990
- targetTaskId: r.targetTaskId ?? null,
991
- projection: r.projection ?? 'legacy',
992
- text: r.text,
993
- contentHash: r.contentHash ?? null,
994
- })))
995
- .run();
996
- },
997
- async getAcRows(taskId) {
998
- const out = await db
999
- .select({
1000
- id: schema.taskAcceptanceCriteria.id,
1001
- taskId: schema.taskAcceptanceCriteria.taskId,
1002
- ordinal: schema.taskAcceptanceCriteria.ordinal,
1003
- kind: schema.taskAcceptanceCriteria.kind,
1004
- sourceKey: schema.taskAcceptanceCriteria.sourceKey,
1005
- targetTaskId: schema.taskAcceptanceCriteria.targetTaskId,
1006
- projection: schema.taskAcceptanceCriteria.projection,
1007
- text: schema.taskAcceptanceCriteria.text,
1008
- createdAt: schema.taskAcceptanceCriteria.createdAt,
1009
- updatedAt: schema.taskAcceptanceCriteria.updatedAt,
1010
- contentHash: schema.taskAcceptanceCriteria.contentHash,
1011
- })
1012
- .from(schema.taskAcceptanceCriteria)
1013
- .where(eq(schema.taskAcceptanceCriteria.taskId, taskId))
1014
- .orderBy(schema.taskAcceptanceCriteria.ordinal)
1015
- .all();
1016
- return out.map((r) => ({
1017
- id: r.id,
1018
- taskId: r.taskId,
1019
- ordinal: r.ordinal,
1020
- kind: r.kind,
1021
- sourceKey: r.sourceKey ?? `text:${r.ordinal}`,
1022
- targetTaskId: r.targetTaskId ?? null,
1023
- projection: r.projection,
1024
- text: r.text,
1025
- createdAt: r.createdAt,
1026
- updatedAt: r.updatedAt ?? null,
1027
- contentHash: r.contentHash ?? null,
1028
- }));
1029
- },
1030
- async deleteAcRowsForTask(taskId) {
1031
- await db
1032
- .delete(schema.taskAcceptanceCriteria)
1033
- .where(eq(schema.taskAcceptanceCriteria.taskId, taskId))
1034
- .run();
1035
- },
1036
- async appendAcHistory(rows) {
1037
- if (rows.length === 0)
1038
- return;
1039
- await db
1040
- .insert(schema.taskAcceptanceCriteriaHistory)
1041
- .values(rows.map((r) => ({
1042
- acId: r.acId,
1043
- previousText: r.previousText,
1044
- reason: r.reason,
1045
- })))
1046
- .run();
1047
- },
1048
- // ---- AC bindings (T10509 — AC-coverage gate) ----
1049
- async getAcBindings(acIds) {
1050
- if (acIds.length === 0)
1051
- return [];
1052
- const out = await db
1053
- .select({
1054
- id: schema.evidenceAcBindings.id,
1055
- evidenceAtomId: schema.evidenceAcBindings.evidenceAtomId,
1056
- acId: schema.evidenceAcBindings.acId,
1057
- bindingType: schema.evidenceAcBindings.bindingType,
1058
- createdAt: schema.evidenceAcBindings.createdAt,
1059
- })
1060
- .from(schema.evidenceAcBindings)
1061
- .where(inArray(schema.evidenceAcBindings.acId, acIds))
1062
- .all();
1063
- return out.map((r) => ({
1064
- id: r.id,
1065
- evidenceAtomId: r.evidenceAtomId,
1066
- acId: r.acId,
1067
- bindingType: r.bindingType,
1068
- createdAt: r.createdAt,
1069
- }));
1070
- },
1071
- // ---- AC bindings — writer (T10511, Validator SDK tools) ----
1072
- async insertAcBindings(rows) {
1073
- if (rows.length === 0)
1074
- return;
1075
- // Idempotent insert — UNIQUE (evidence_atom_id, ac_id, binding_type)
1076
- // index collapses re-inserts of the same triple via DO NOTHING.
1077
- await db
1078
- .insert(schema.evidenceAcBindings)
1079
- .values(rows.map((r) => ({
1080
- id: r.id,
1081
- evidenceAtomId: r.evidenceAtomId,
1082
- acId: r.acId,
1083
- bindingType: r.bindingType,
1084
- })))
1085
- .onConflictDoNothing({
1086
- target: [
1087
- schema.evidenceAcBindings.evidenceAtomId,
1088
- schema.evidenceAcBindings.acId,
1089
- schema.evidenceAcBindings.bindingType,
1090
- ],
1091
- })
1092
- .run();
1093
- },
988
+ if (context && !context.active)
989
+ return accessor.transaction(fn);
990
+ const execute = async () => {
991
+ if (!context && nativeDb.isTransaction) {
992
+ throw new Error('Cannot join a transaction without an active task-accessor owner');
993
+ }
994
+ const inherited = taskTransactionContext.getStore();
995
+ const scope = new TaskTransactionScope(nativeDb, inherited?.active ? inherited : undefined);
996
+ return taskTransactionContext.run(scope, async () => {
997
+ const isOuter = !nativeDb.isTransaction;
998
+ const spName = isOuter ? null : `_cleo_tx_${(_txSavepointCounter++).toString(36)}`;
999
+ if (isOuter) {
1000
+ nativeDb.prepare('BEGIN IMMEDIATE').run();
1001
+ }
1002
+ else {
1003
+ nativeDb.prepare(`SAVEPOINT ${spName}`).run();
1004
+ }
1005
+ try {
1006
+ const tx = {
1007
+ async upsertSingleTask(task) {
1008
+ scope.assertActive();
1009
+ return accessor.transaction(async () => {
1010
+ scope.assertActive();
1011
+ const row = taskToRow(task);
1012
+ await upsertTask(db, row);
1013
+ await updateDependencies(db, task.id, task.depends ?? []);
1014
+ });
1015
+ },
1016
+ async archiveSingleTask(taskId, fields) {
1017
+ scope.assertActive();
1018
+ return accessor.transaction(async () => {
1019
+ scope.assertActive();
1020
+ await db
1021
+ .update(schema.tasks)
1022
+ .set({
1023
+ status: 'archived',
1024
+ archivedAt: fields.archivedAt ?? new Date().toISOString(),
1025
+ archiveReason: fields.archiveReason ?? ARCHIVE_REASON_TOMBSTONE,
1026
+ cycleTimeDays: fields.cycleTimeDays ?? null,
1027
+ updatedAt: new Date().toISOString(),
1028
+ })
1029
+ .where(eq(schema.tasks.id, taskId))
1030
+ .run();
1031
+ });
1032
+ },
1033
+ async removeSingleTask(taskId) {
1034
+ scope.assertActive();
1035
+ return accessor.transaction(async () => {
1036
+ scope.assertActive();
1037
+ await db
1038
+ .delete(schema.taskDependencies)
1039
+ .where(eq(schema.taskDependencies.taskId, taskId))
1040
+ .run();
1041
+ await db
1042
+ .delete(schema.taskDependencies)
1043
+ .where(eq(schema.taskDependencies.dependsOn, taskId))
1044
+ .run();
1045
+ await db.delete(schema.tasks).where(eq(schema.tasks.id, taskId)).run();
1046
+ });
1047
+ },
1048
+ async setMetaValue(key, value) {
1049
+ scope.assertActive();
1050
+ return accessor.transaction(async () => {
1051
+ scope.assertActive();
1052
+ await writeMetaValue(cwd, key, value);
1053
+ });
1054
+ },
1055
+ async updateTaskFields(taskId, flds) {
1056
+ scope.assertActive();
1057
+ return accessor.transaction(async () => {
1058
+ scope.assertActive();
1059
+ // Delegate to the outer accessor's implementation
1060
+ await accessor.updateTaskFields(taskId, flds);
1061
+ });
1062
+ },
1063
+ async getChildren(parentId) {
1064
+ scope.assertActive();
1065
+ const rows = await db
1066
+ .select()
1067
+ .from(schema.tasks)
1068
+ .where(and(eq(schema.tasks.parentId, parentId), ne(schema.tasks.status, 'archived')))
1069
+ .orderBy(sql `${schema.tasks.position} ASC, ${schema.tasks.createdAt} ASC`)
1070
+ .all();
1071
+ return rows.map(rowToTask);
1072
+ },
1073
+ async appendLog(entry) {
1074
+ scope.assertActive();
1075
+ return accessor.transaction(async () => {
1076
+ scope.assertActive();
1077
+ await accessor.appendLog(entry);
1078
+ });
1079
+ },
1080
+ // T9514: persist relates mutations inside the transaction
1081
+ async addRelation(taskId, relatedTo, relationType, reason) {
1082
+ scope.assertActive();
1083
+ return accessor.transaction(async () => {
1084
+ scope.assertActive();
1085
+ await accessor.addRelation(taskId, relatedTo, relationType, reason);
1086
+ });
1087
+ },
1088
+ async removeRelation(taskId, relatedTo, relationType) {
1089
+ scope.assertActive();
1090
+ return accessor.transaction(async () => {
1091
+ scope.assertActive();
1092
+ await accessor.removeRelation(taskId, relatedTo, relationType);
1093
+ });
1094
+ },
1095
+ async clearRelations(taskId) {
1096
+ scope.assertActive();
1097
+ return accessor.transaction(async () => {
1098
+ scope.assertActive();
1099
+ await db
1100
+ .delete(schema.taskRelations)
1101
+ .where(eq(schema.taskRelations.taskId, taskId))
1102
+ .run();
1103
+ });
1104
+ },
1105
+ // ---- AC rows (T10508) ----
1106
+ async insertAcRows(rows) {
1107
+ scope.assertActive();
1108
+ return accessor.transaction(async () => {
1109
+ scope.assertActive();
1110
+ if (rows.length === 0)
1111
+ return;
1112
+ await db
1113
+ .insert(schema.taskAcceptanceCriteria)
1114
+ .values(rows.map((r) => ({
1115
+ id: r.id,
1116
+ taskId: r.taskId,
1117
+ ordinal: r.ordinal,
1118
+ kind: r.kind ?? 'text',
1119
+ sourceKey: r.sourceKey ?? `text:${r.ordinal}`,
1120
+ targetTaskId: r.targetTaskId ?? null,
1121
+ projection: r.projection ?? 'legacy',
1122
+ text: r.text,
1123
+ contentHash: r.contentHash ?? null,
1124
+ })))
1125
+ .run();
1126
+ });
1127
+ },
1128
+ async getAcRows(taskId) {
1129
+ scope.assertActive();
1130
+ const out = await db
1131
+ .select({
1132
+ id: schema.taskAcceptanceCriteria.id,
1133
+ taskId: schema.taskAcceptanceCriteria.taskId,
1134
+ ordinal: schema.taskAcceptanceCriteria.ordinal,
1135
+ kind: schema.taskAcceptanceCriteria.kind,
1136
+ sourceKey: schema.taskAcceptanceCriteria.sourceKey,
1137
+ targetTaskId: schema.taskAcceptanceCriteria.targetTaskId,
1138
+ projection: schema.taskAcceptanceCriteria.projection,
1139
+ text: schema.taskAcceptanceCriteria.text,
1140
+ createdAt: schema.taskAcceptanceCriteria.createdAt,
1141
+ updatedAt: schema.taskAcceptanceCriteria.updatedAt,
1142
+ contentHash: schema.taskAcceptanceCriteria.contentHash,
1143
+ })
1144
+ .from(schema.taskAcceptanceCriteria)
1145
+ .where(eq(schema.taskAcceptanceCriteria.taskId, taskId))
1146
+ .orderBy(schema.taskAcceptanceCriteria.ordinal)
1147
+ .all();
1148
+ return out.map((r) => ({
1149
+ id: r.id,
1150
+ taskId: r.taskId,
1151
+ ordinal: r.ordinal,
1152
+ kind: r.kind,
1153
+ sourceKey: r.sourceKey ?? `text:${r.ordinal}`,
1154
+ targetTaskId: r.targetTaskId ?? null,
1155
+ projection: r.projection,
1156
+ text: r.text,
1157
+ createdAt: r.createdAt,
1158
+ updatedAt: r.updatedAt ?? null,
1159
+ contentHash: r.contentHash ?? null,
1160
+ }));
1161
+ },
1162
+ async deleteAcRowsForTask(taskId) {
1163
+ scope.assertActive();
1164
+ return accessor.transaction(async () => {
1165
+ scope.assertActive();
1166
+ await db
1167
+ .delete(schema.taskAcceptanceCriteria)
1168
+ .where(eq(schema.taskAcceptanceCriteria.taskId, taskId))
1169
+ .run();
1170
+ });
1171
+ },
1172
+ async appendAcHistory(rows) {
1173
+ scope.assertActive();
1174
+ return accessor.transaction(async () => {
1175
+ scope.assertActive();
1176
+ if (rows.length === 0)
1177
+ return;
1178
+ await db
1179
+ .insert(schema.taskAcceptanceCriteriaHistory)
1180
+ .values(rows.map((r) => ({
1181
+ acId: r.acId,
1182
+ previousText: r.previousText,
1183
+ reason: r.reason,
1184
+ })))
1185
+ .run();
1186
+ });
1187
+ },
1188
+ // ---- AC bindings (T10509 — AC-coverage gate) ----
1189
+ async getAcBindings(acIds) {
1190
+ scope.assertActive();
1191
+ if (acIds.length === 0)
1192
+ return [];
1193
+ const out = await db
1194
+ .select({
1195
+ id: schema.evidenceAcBindings.id,
1196
+ evidenceAtomId: schema.evidenceAcBindings.evidenceAtomId,
1197
+ acId: schema.evidenceAcBindings.acId,
1198
+ bindingType: schema.evidenceAcBindings.bindingType,
1199
+ createdAt: schema.evidenceAcBindings.createdAt,
1200
+ })
1201
+ .from(schema.evidenceAcBindings)
1202
+ .where(inArray(schema.evidenceAcBindings.acId, acIds))
1203
+ .all();
1204
+ return out.map((r) => ({
1205
+ id: r.id,
1206
+ evidenceAtomId: r.evidenceAtomId,
1207
+ acId: r.acId,
1208
+ bindingType: r.bindingType,
1209
+ createdAt: r.createdAt,
1210
+ }));
1211
+ },
1212
+ // ---- AC bindings — writer (T10511, Validator SDK tools) ----
1213
+ async insertAcBindings(rows) {
1214
+ scope.assertActive();
1215
+ return accessor.transaction(async () => {
1216
+ scope.assertActive();
1217
+ if (rows.length === 0)
1218
+ return;
1219
+ // Idempotent insert — UNIQUE (evidence_atom_id, ac_id, binding_type)
1220
+ // index collapses re-inserts of the same triple via DO NOTHING.
1221
+ await db
1222
+ .insert(schema.evidenceAcBindings)
1223
+ .values(rows.map((r) => ({
1224
+ id: r.id,
1225
+ evidenceAtomId: r.evidenceAtomId,
1226
+ acId: r.acId,
1227
+ bindingType: r.bindingType,
1228
+ })))
1229
+ .onConflictDoNothing({
1230
+ target: [
1231
+ schema.evidenceAcBindings.evidenceAtomId,
1232
+ schema.evidenceAcBindings.acId,
1233
+ schema.evidenceAcBindings.bindingType,
1234
+ ],
1235
+ })
1236
+ .run();
1237
+ });
1238
+ },
1239
+ };
1240
+ const result = await fn(bindTaskAccessorScope(tx, accessorScope));
1241
+ await scope.pending;
1242
+ if (isOuter) {
1243
+ nativeDb.prepare('COMMIT').run();
1244
+ }
1245
+ else {
1246
+ nativeDb.prepare(`RELEASE SAVEPOINT ${spName}`).run();
1247
+ }
1248
+ return result;
1249
+ }
1250
+ catch (err) {
1251
+ await scope.pending;
1252
+ try {
1253
+ if (isOuter) {
1254
+ nativeDb.prepare('ROLLBACK').run();
1255
+ }
1256
+ else {
1257
+ nativeDb.prepare(`ROLLBACK TO SAVEPOINT ${spName}`).run();
1258
+ nativeDb.prepare(`RELEASE SAVEPOINT ${spName}`).run();
1259
+ }
1260
+ }
1261
+ catch {
1262
+ /* ignore secondary rollback errors */
1263
+ }
1264
+ throw err;
1265
+ }
1266
+ finally {
1267
+ scope.active = false;
1268
+ }
1269
+ });
1094
1270
  };
1095
- const result = await fn(tx);
1096
- _txDepth--;
1097
- if (isOuter) {
1098
- nativeDb.prepare('COMMIT').run();
1099
- }
1100
- else {
1101
- nativeDb.prepare(`RELEASE SAVEPOINT ${spName}`).run();
1102
- }
1103
- return result;
1271
+ const commit = () => withBackgroundOpCommitBoundary(nativeDb, execute, worktreeScope.getStore()?.execution, (work) => scheduleTaskBackground(nativeDb, work));
1272
+ return await (context ? commit() : withWriteRetry(commit));
1104
1273
  }
1105
- catch (err) {
1106
- _txDepth--;
1107
- try {
1108
- if (isOuter) {
1109
- nativeDb.prepare('ROLLBACK').run();
1110
- }
1111
- else {
1112
- nativeDb.prepare(`ROLLBACK TO SAVEPOINT ${spName}`).run();
1113
- nativeDb.prepare(`RELEASE SAVEPOINT ${spName}`).run();
1114
- }
1115
- }
1116
- catch {
1117
- /* ignore secondary rollback errors */
1118
- }
1119
- throw err;
1274
+ finally {
1275
+ released.resolve();
1276
+ if (!context && taskTransactionQueue.get(nativeDb) === tail)
1277
+ taskTransactionQueue.delete(nativeDb);
1120
1278
  }
1121
1279
  },
1122
1280
  // ---- close ----
@@ -1128,7 +1286,7 @@ export async function createSqliteDataAccessor(cwd) {
1128
1286
  return getMetaValue(cwd, key);
1129
1287
  },
1130
1288
  async setMetaValue(key, value) {
1131
- return setMetaValue(cwd, key, value);
1289
+ return accessor.transaction((tx) => tx.setMetaValue(key, value));
1132
1290
  },
1133
1291
  async getSchemaVersion() {
1134
1292
  const meta = await getMetaValue(cwd, 'file_meta');
@@ -1146,44 +1304,48 @@ export async function createSqliteDataAccessor(cwd) {
1146
1304
  },
1147
1305
  // ---- Agent task claiming ----
1148
1306
  async claimTask(taskId, agentId) {
1149
- const nativeDb = getNativeTasksDb();
1150
- if (!nativeDb) {
1151
- throw new Error('Native database not initialized');
1152
- }
1153
- // Verify the task exists first
1154
- const existsRow = nativeDb
1155
- .prepare('SELECT assignee FROM tasks_tasks WHERE id = ?')
1156
- .get(taskId);
1157
- if (!existsRow) {
1158
- throw new Error(`Task not found: ${taskId}`);
1159
- }
1160
- // Atomic claim: only succeeds if assignee IS NULL or already claimed by this agent.
1161
- // This prevents race conditions between concurrent agents.
1162
- const result = nativeDb
1163
- .prepare('UPDATE tasks_tasks SET assignee = ?, updated_at = ? WHERE id = ? AND (assignee IS NULL OR assignee = ?)')
1164
- .run(agentId, new Date().toISOString(), taskId, agentId);
1165
- if (result.changes === 0) {
1166
- // Row was not updated — task is claimed by a different agent
1167
- const currentRow = nativeDb
1307
+ return accessor.transaction(async () => {
1308
+ const nativeDb = await requireNativeDb();
1309
+ if (!nativeDb) {
1310
+ throw new Error('Native database not initialized');
1311
+ }
1312
+ // Verify the task exists first
1313
+ const existsRow = nativeDb
1168
1314
  .prepare('SELECT assignee FROM tasks_tasks WHERE id = ?')
1169
1315
  .get(taskId);
1170
- throw new Error(`Task ${taskId} is already claimed by agent: ${currentRow?.assignee ?? 'unknown'}`);
1171
- }
1316
+ if (!existsRow) {
1317
+ throw new Error(`Task not found: ${taskId}`);
1318
+ }
1319
+ // Atomic claim: only succeeds if assignee IS NULL or already claimed by this agent.
1320
+ // This prevents race conditions between concurrent agents.
1321
+ const result = nativeDb
1322
+ .prepare('UPDATE tasks_tasks SET assignee = ?, updated_at = ? WHERE id = ? AND (assignee IS NULL OR assignee = ?)')
1323
+ .run(agentId, new Date().toISOString(), taskId, agentId);
1324
+ if (result.changes === 0) {
1325
+ // Row was not updated — task is claimed by a different agent
1326
+ const currentRow = nativeDb
1327
+ .prepare('SELECT assignee FROM tasks_tasks WHERE id = ?')
1328
+ .get(taskId);
1329
+ throw new Error(`Task ${taskId} is already claimed by agent: ${currentRow?.assignee ?? 'unknown'}`);
1330
+ }
1331
+ });
1172
1332
  },
1173
1333
  async unclaimTask(taskId) {
1174
- const nativeDb = getNativeTasksDb();
1175
- if (!nativeDb) {
1176
- throw new Error('Native database not initialized');
1177
- }
1178
- // Verify the task exists
1179
- const existsRow = nativeDb.prepare('SELECT id FROM tasks_tasks WHERE id = ?').get(taskId);
1180
- if (!existsRow) {
1181
- throw new Error(`Task not found: ${taskId}`);
1182
- }
1183
- // Clear the assignee — no-op if already null
1184
- nativeDb
1185
- .prepare('UPDATE tasks_tasks SET assignee = NULL, updated_at = ? WHERE id = ?')
1186
- .run(new Date().toISOString(), taskId);
1334
+ return accessor.transaction(async () => {
1335
+ const nativeDb = await requireNativeDb();
1336
+ if (!nativeDb) {
1337
+ throw new Error('Native database not initialized');
1338
+ }
1339
+ // Verify the task exists
1340
+ const existsRow = nativeDb.prepare('SELECT id FROM tasks_tasks WHERE id = ?').get(taskId);
1341
+ if (!existsRow) {
1342
+ throw new Error(`Task not found: ${taskId}`);
1343
+ }
1344
+ // Clear the assignee — no-op if already null
1345
+ nativeDb
1346
+ .prepare('UPDATE tasks_tasks SET assignee = NULL, updated_at = ? WHERE id = ?')
1347
+ .run(new Date().toISOString(), taskId);
1348
+ });
1187
1349
  },
1188
1350
  };
1189
1351
  return accessor;