@cleocode/core 2026.9.24 → 2026.9.26

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 (481) hide show
  1. package/dist/admin/import-tasks.d.ts.map +1 -1
  2. package/dist/admin/import-tasks.js +5 -2
  3. package/dist/admin/import-tasks.js.map +1 -1
  4. package/dist/admin/import.d.ts.map +1 -1
  5. package/dist/admin/import.js +7 -2
  6. package/dist/admin/import.js.map +1 -1
  7. package/dist/audit-prune.d.ts +32 -0
  8. package/dist/audit-prune.d.ts.map +1 -1
  9. package/dist/audit-prune.js +37 -3
  10. package/dist/audit-prune.js.map +1 -1
  11. package/dist/build-provenance.json +2 -2
  12. package/dist/cloud/crypto.d.ts +1 -2
  13. package/dist/cloud/crypto.d.ts.map +1 -1
  14. package/dist/cloud/crypto.js +1 -11
  15. package/dist/cloud/crypto.js.map +1 -1
  16. package/dist/cloud/index.d.ts +1 -0
  17. package/dist/cloud/index.d.ts.map +1 -1
  18. package/dist/cloud/index.js +1 -0
  19. package/dist/cloud/index.js.map +1 -1
  20. package/dist/cloud/nexus-auth.d.ts +26 -1
  21. package/dist/cloud/nexus-auth.d.ts.map +1 -1
  22. package/dist/cloud/nexus-auth.js +61 -32
  23. package/dist/cloud/nexus-auth.js.map +1 -1
  24. package/dist/cloud/nexus-credentials.js +2 -2
  25. package/dist/cloud/nexus-credentials.js.map +1 -1
  26. package/dist/cloud/nexus-device.d.ts +809 -0
  27. package/dist/cloud/nexus-device.d.ts.map +1 -0
  28. package/dist/cloud/nexus-device.js +1680 -0
  29. package/dist/cloud/nexus-device.js.map +1 -0
  30. package/dist/cloud/nexus-enrol.d.ts +210 -0
  31. package/dist/cloud/nexus-enrol.d.ts.map +1 -0
  32. package/dist/cloud/nexus-enrol.js +1023 -0
  33. package/dist/cloud/nexus-enrol.js.map +1 -0
  34. package/dist/cloud/nexus-link.d.ts +3 -0
  35. package/dist/cloud/nexus-link.d.ts.map +1 -1
  36. package/dist/cloud/nexus-link.js +38 -4
  37. package/dist/cloud/nexus-link.js.map +1 -1
  38. package/dist/cloud/signing.d.ts +20 -0
  39. package/dist/cloud/signing.d.ts.map +1 -1
  40. package/dist/cloud/signing.js +14 -0
  41. package/dist/cloud/signing.js.map +1 -1
  42. package/dist/cloud/uuidv7.d.ts +9 -0
  43. package/dist/cloud/uuidv7.d.ts.map +1 -0
  44. package/dist/cloud/uuidv7.js +19 -0
  45. package/dist/cloud/uuidv7.js.map +1 -0
  46. package/dist/crypto/credentials.d.ts +61 -0
  47. package/dist/crypto/credentials.d.ts.map +1 -1
  48. package/dist/crypto/credentials.js +154 -58
  49. package/dist/crypto/credentials.js.map +1 -1
  50. package/dist/decide/bench/index.d.ts +1 -1
  51. package/dist/decide/bench/index.d.ts.map +1 -1
  52. package/dist/decide/bench/index.js +1 -1
  53. package/dist/decide/bench/index.js.map +1 -1
  54. package/dist/decide/bench/operation.d.ts +1 -1
  55. package/dist/decide/bench/operation.d.ts.map +1 -1
  56. package/dist/decide/bench/operation.js +1 -1
  57. package/dist/decide/bench/operation.js.map +1 -1
  58. package/dist/decide/bench/profiles.d.ts +50 -22
  59. package/dist/decide/bench/profiles.d.ts.map +1 -1
  60. package/dist/decide/bench/profiles.js +103 -59
  61. package/dist/decide/bench/profiles.js.map +1 -1
  62. package/dist/decide/bench/report.d.ts.map +1 -1
  63. package/dist/decide/bench/report.js +6 -1
  64. package/dist/decide/bench/report.js.map +1 -1
  65. package/dist/decide/bench/runner.d.ts +6 -1
  66. package/dist/decide/bench/runner.d.ts.map +1 -1
  67. package/dist/decide/bench/runner.js +1 -0
  68. package/dist/decide/bench/runner.js.map +1 -1
  69. package/dist/decide/client.d.ts.map +1 -1
  70. package/dist/decide/client.js +11 -5
  71. package/dist/decide/client.js.map +1 -1
  72. package/dist/decide/jev-wire.d.ts +31 -1
  73. package/dist/decide/jev-wire.d.ts.map +1 -1
  74. package/dist/decide/jev-wire.js +61 -7
  75. package/dist/decide/jev-wire.js.map +1 -1
  76. package/dist/docs/export-document.js +12907 -10622
  77. package/dist/docs/export-document.js.map +3 -3
  78. package/dist/doctor/dependency-cycles.d.ts +41 -0
  79. package/dist/doctor/dependency-cycles.d.ts.map +1 -0
  80. package/dist/doctor/dependency-cycles.js +39 -0
  81. package/dist/doctor/dependency-cycles.js.map +1 -0
  82. package/dist/doctor/index.d.ts +2 -0
  83. package/dist/doctor/index.d.ts.map +1 -1
  84. package/dist/doctor/index.js +1 -0
  85. package/dist/doctor/index.js.map +1 -1
  86. package/dist/doctor/orphan-ac-bindings.d.ts +64 -0
  87. package/dist/doctor/orphan-ac-bindings.d.ts.map +1 -0
  88. package/dist/doctor/orphan-ac-bindings.js +59 -0
  89. package/dist/doctor/orphan-ac-bindings.js.map +1 -0
  90. package/dist/doctor/projects.d.ts.map +1 -1
  91. package/dist/doctor/projects.js +43 -29
  92. package/dist/doctor/projects.js.map +1 -1
  93. package/dist/doctor/row-identity.d.ts +42 -0
  94. package/dist/doctor/row-identity.d.ts.map +1 -0
  95. package/dist/doctor/row-identity.js +151 -0
  96. package/dist/doctor/row-identity.js.map +1 -0
  97. package/dist/doctor/twin-collapse.d.ts +58 -2
  98. package/dist/doctor/twin-collapse.d.ts.map +1 -1
  99. package/dist/doctor/twin-collapse.js +212 -14
  100. package/dist/doctor/twin-collapse.js.map +1 -1
  101. package/dist/error-catalog.d.ts.map +1 -1
  102. package/dist/error-catalog.js +4 -0
  103. package/dist/error-catalog.js.map +1 -1
  104. package/dist/error-registry.d.ts.map +1 -1
  105. package/dist/error-registry.js +8 -0
  106. package/dist/error-registry.js.map +1 -1
  107. package/dist/lifecycle/verification/satisfies-validator.d.ts.map +1 -1
  108. package/dist/lifecycle/verification/satisfies-validator.js +10 -0
  109. package/dist/lifecycle/verification/satisfies-validator.js.map +1 -1
  110. package/dist/llm/plugin-facade.js +17767 -15482
  111. package/dist/llm/plugin-facade.js.map +3 -3
  112. package/dist/memory/index.d.ts.map +1 -1
  113. package/dist/memory/index.js +19 -10
  114. package/dist/memory/index.js.map +1 -1
  115. package/dist/memory/manifest-identity.d.ts +171 -0
  116. package/dist/memory/manifest-identity.d.ts.map +1 -0
  117. package/dist/memory/manifest-identity.js +345 -0
  118. package/dist/memory/manifest-identity.js.map +1 -0
  119. package/dist/memory/mental-model-queue.d.ts +14 -4
  120. package/dist/memory/mental-model-queue.d.ts.map +1 -1
  121. package/dist/memory/mental-model-queue.js +64 -35
  122. package/dist/memory/mental-model-queue.js.map +1 -1
  123. package/dist/memory/pipeline-manifest-sqlite.d.ts +47 -5
  124. package/dist/memory/pipeline-manifest-sqlite.d.ts.map +1 -1
  125. package/dist/memory/pipeline-manifest-sqlite.js +130 -14
  126. package/dist/memory/pipeline-manifest-sqlite.js.map +1 -1
  127. package/dist/memory/retrieval/observe.d.ts.map +1 -1
  128. package/dist/memory/retrieval/observe.js +2 -0
  129. package/dist/memory/retrieval/observe.js.map +1 -1
  130. package/dist/memory/transcript-scanner.js +1 -1
  131. package/dist/nexus/deps.d.ts.map +1 -1
  132. package/dist/nexus/deps.js +7 -14
  133. package/dist/nexus/deps.js.map +1 -1
  134. package/dist/nexus/discover.d.ts.map +1 -1
  135. package/dist/nexus/discover.js +6 -9
  136. package/dist/nexus/discover.js.map +1 -1
  137. package/dist/nexus/git-state.d.ts.map +1 -1
  138. package/dist/nexus/git-state.js +10 -1
  139. package/dist/nexus/git-state.js.map +1 -1
  140. package/dist/nexus/projects-clean.d.ts.map +1 -1
  141. package/dist/nexus/projects-clean.js +22 -11
  142. package/dist/nexus/projects-clean.js.map +1 -1
  143. package/dist/nexus/projects-scan.d.ts.map +1 -1
  144. package/dist/nexus/projects-scan.js +8 -13
  145. package/dist/nexus/projects-scan.js.map +1 -1
  146. package/dist/nexus/query.d.ts.map +1 -1
  147. package/dist/nexus/query.js +4 -12
  148. package/dist/nexus/query.js.map +1 -1
  149. package/dist/nexus/registry-roots.d.ts +5 -2
  150. package/dist/nexus/registry-roots.d.ts.map +1 -1
  151. package/dist/nexus/registry-roots.js +8 -10
  152. package/dist/nexus/registry-roots.js.map +1 -1
  153. package/dist/nexus/registry.d.ts +18 -0
  154. package/dist/nexus/registry.d.ts.map +1 -1
  155. package/dist/nexus/registry.js +37 -16
  156. package/dist/nexus/registry.js.map +1 -1
  157. package/dist/nexus/workspace.d.ts.map +1 -1
  158. package/dist/nexus/workspace.js +54 -4
  159. package/dist/nexus/workspace.js.map +1 -1
  160. package/dist/orchestrate/lifecycle-ops.js +1 -1
  161. package/dist/orchestrate/lifecycle-ops.js.map +1 -1
  162. package/dist/orchestrate/pivot.d.ts.map +1 -1
  163. package/dist/orchestrate/pivot.js +4 -2
  164. package/dist/orchestrate/pivot.js.map +1 -1
  165. package/dist/orchestrate/query-ops.d.ts.map +1 -1
  166. package/dist/orchestrate/query-ops.js +3 -3
  167. package/dist/orchestrate/query-ops.js.map +1 -1
  168. package/dist/orchestrate/spawn-ops.js +1 -1
  169. package/dist/orchestrate/spawn-ops.js.map +1 -1
  170. package/dist/orchestrate/worktree-complete.d.ts.map +1 -1
  171. package/dist/orchestrate/worktree-complete.js +40 -2
  172. package/dist/orchestrate/worktree-complete.js.map +1 -1
  173. package/dist/orchestration/index.js +1 -1
  174. package/dist/orchestration/index.js.map +1 -1
  175. package/dist/orchestration/lead-rollup.d.ts.map +1 -1
  176. package/dist/orchestration/lead-rollup.js +3 -3
  177. package/dist/orchestration/lead-rollup.js.map +1 -1
  178. package/dist/orchestration/parallel.js +1 -1
  179. package/dist/orchestration/parallel.js.map +1 -1
  180. package/dist/orchestration/spawn-prompt.d.ts +26 -0
  181. package/dist/orchestration/spawn-prompt.d.ts.map +1 -1
  182. package/dist/orchestration/spawn-prompt.js +62 -91
  183. package/dist/orchestration/spawn-prompt.js.map +1 -1
  184. package/dist/orchestration/wave-topic.d.ts +4 -2
  185. package/dist/orchestration/wave-topic.d.ts.map +1 -1
  186. package/dist/orchestration/wave-topic.js +6 -4
  187. package/dist/orchestration/wave-topic.js.map +1 -1
  188. package/dist/orchestration/waves.d.ts +2 -1
  189. package/dist/orchestration/waves.d.ts.map +1 -1
  190. package/dist/orchestration/waves.js +4 -3
  191. package/dist/orchestration/waves.js.map +1 -1
  192. package/dist/phases/index.d.ts.map +1 -1
  193. package/dist/phases/index.js +4 -2
  194. package/dist/phases/index.js.map +1 -1
  195. package/dist/release/ci-evidence.d.ts +190 -0
  196. package/dist/release/ci-evidence.d.ts.map +1 -1
  197. package/dist/release/ci-evidence.js +436 -4
  198. package/dist/release/ci-evidence.js.map +1 -1
  199. package/dist/sdk/validator-ac-pull.d.ts.map +1 -1
  200. package/dist/sdk/validator-ac-pull.js +2 -1
  201. package/dist/sdk/validator-ac-pull.js.map +1 -1
  202. package/dist/selfimprove/fix-gen.js +12807 -10522
  203. package/dist/selfimprove/fix-gen.js.map +3 -3
  204. package/dist/sentient/cross-project-hygiene.d.ts +5 -0
  205. package/dist/sentient/cross-project-hygiene.d.ts.map +1 -1
  206. package/dist/sentient/cross-project-hygiene.js +7 -1
  207. package/dist/sentient/cross-project-hygiene.js.map +1 -1
  208. package/dist/sentient/propose-tick.d.ts.map +1 -1
  209. package/dist/sentient/propose-tick.js +2 -3
  210. package/dist/sentient/propose-tick.js.map +1 -1
  211. package/dist/sentient/stage-drift-tick.d.ts.map +1 -1
  212. package/dist/sentient/stage-drift-tick.js +2 -3
  213. package/dist/sentient/stage-drift-tick.js.map +1 -1
  214. package/dist/session/engine-ops.d.ts.map +1 -1
  215. package/dist/session/engine-ops.js +77 -49
  216. package/dist/session/engine-ops.js.map +1 -1
  217. package/dist/sessions/focus-state-store.d.ts +69 -23
  218. package/dist/sessions/focus-state-store.d.ts.map +1 -1
  219. package/dist/sessions/focus-state-store.js +113 -27
  220. package/dist/sessions/focus-state-store.js.map +1 -1
  221. package/dist/sessions/handoff.js +1 -1
  222. package/dist/sessions/handoff.js.map +1 -1
  223. package/dist/sessions/index.d.ts.map +1 -1
  224. package/dist/sessions/index.js +24 -13
  225. package/dist/sessions/index.js.map +1 -1
  226. package/dist/sessions/terminal-identity.d.ts +165 -17
  227. package/dist/sessions/terminal-identity.d.ts.map +1 -1
  228. package/dist/sessions/terminal-identity.js +341 -32
  229. package/dist/sessions/terminal-identity.js.map +1 -1
  230. package/dist/skills/manifests/research.js +2 -0
  231. package/dist/skills/manifests/research.js.map +1 -1
  232. package/dist/skills/orchestrator/startup.js +2 -2
  233. package/dist/skills/orchestrator/startup.js.map +1 -1
  234. package/dist/skills/orchestrator/validator.d.ts.map +1 -1
  235. package/dist/skills/orchestrator/validator.js +9 -2
  236. package/dist/skills/orchestrator/validator.js.map +1 -1
  237. package/dist/skills/validation.d.ts +4 -1
  238. package/dist/skills/validation.d.ts.map +1 -1
  239. package/dist/skills/validation.js +11 -3
  240. package/dist/skills/validation.js.map +1 -1
  241. package/dist/snapshot/index.d.ts +4 -0
  242. package/dist/snapshot/index.d.ts.map +1 -1
  243. package/dist/snapshot/index.js +50 -3
  244. package/dist/snapshot/index.js.map +1 -1
  245. package/dist/spawn/branch-lock.d.ts +111 -7
  246. package/dist/spawn/branch-lock.d.ts.map +1 -1
  247. package/dist/spawn/branch-lock.js +296 -8
  248. package/dist/spawn/branch-lock.js.map +1 -1
  249. package/dist/store/ac-binding-prune.d.ts +144 -0
  250. package/dist/store/ac-binding-prune.d.ts.map +1 -0
  251. package/dist/store/ac-binding-prune.js +293 -0
  252. package/dist/store/ac-binding-prune.js.map +1 -0
  253. package/dist/store/agent-registry-store.d.ts.map +1 -1
  254. package/dist/store/agent-registry-store.js +3 -2
  255. package/dist/store/agent-registry-store.js.map +1 -1
  256. package/dist/store/backup-sidecar.d.ts +85 -2
  257. package/dist/store/backup-sidecar.d.ts.map +1 -1
  258. package/dist/store/backup-sidecar.js +173 -1
  259. package/dist/store/backup-sidecar.js.map +1 -1
  260. package/dist/store/conduit-sqlite.d.ts.map +1 -1
  261. package/dist/store/conduit-sqlite.js +3 -2
  262. package/dist/store/conduit-sqlite.js.map +1 -1
  263. package/dist/store/credential-transfer.d.ts.map +1 -1
  264. package/dist/store/credential-transfer.js +3 -0
  265. package/dist/store/credential-transfer.js.map +1 -1
  266. package/dist/store/db-helpers.d.ts +32 -13
  267. package/dist/store/db-helpers.d.ts.map +1 -1
  268. package/dist/store/db-helpers.js +144 -40
  269. package/dist/store/db-helpers.js.map +1 -1
  270. package/dist/store/dependency-cycles.d.ts +164 -0
  271. package/dist/store/dependency-cycles.d.ts.map +1 -0
  272. package/dist/store/dependency-cycles.js +360 -0
  273. package/dist/store/dependency-cycles.js.map +1 -0
  274. package/dist/store/display-id-alias.d.ts +585 -0
  275. package/dist/store/display-id-alias.d.ts.map +1 -0
  276. package/dist/store/display-id-alias.js +1538 -0
  277. package/dist/store/display-id-alias.js.map +1 -0
  278. package/dist/store/dual-scope-db.d.ts.map +1 -1
  279. package/dist/store/dual-scope-db.js +48 -0
  280. package/dist/store/dual-scope-db.js.map +1 -1
  281. package/dist/store/exodus/migrate.d.ts.map +1 -1
  282. package/dist/store/exodus/migrate.js +8 -1
  283. package/dist/store/exodus/migrate.js.map +1 -1
  284. package/dist/store/exodus/reconcile.d.ts.map +1 -1
  285. package/dist/store/exodus/reconcile.js +7 -2
  286. package/dist/store/exodus/reconcile.js.map +1 -1
  287. package/dist/store/exodus/recovery.d.ts.map +1 -1
  288. package/dist/store/exodus/recovery.js +20 -1
  289. package/dist/store/exodus/recovery.js.map +1 -1
  290. package/dist/store/global-salt.d.ts +35 -0
  291. package/dist/store/global-salt.d.ts.map +1 -1
  292. package/dist/store/global-salt.js +136 -16
  293. package/dist/store/global-salt.js.map +1 -1
  294. package/dist/store/legacy-tasks-lineage.d.ts.map +1 -1
  295. package/dist/store/legacy-tasks-lineage.js +19 -2
  296. package/dist/store/legacy-tasks-lineage.js.map +1 -1
  297. package/dist/store/migration-sqlite.d.ts.map +1 -1
  298. package/dist/store/migration-sqlite.js +19 -11
  299. package/dist/store/migration-sqlite.js.map +1 -1
  300. package/dist/store/portable-bundle-scan.d.ts.map +1 -1
  301. package/dist/store/portable-bundle-scan.js +6 -0
  302. package/dist/store/portable-bundle-scan.js.map +1 -1
  303. package/dist/store/row-identity-flag.d.ts +26 -0
  304. package/dist/store/row-identity-flag.d.ts.map +1 -0
  305. package/dist/store/row-identity-flag.js +31 -0
  306. package/dist/store/row-identity-flag.js.map +1 -0
  307. package/dist/store/row-identity-registry.d.ts +61 -0
  308. package/dist/store/row-identity-registry.d.ts.map +1 -0
  309. package/dist/store/row-identity-registry.js +206 -0
  310. package/dist/store/row-identity-registry.js.map +1 -0
  311. package/dist/store/row-identity.d.ts +480 -0
  312. package/dist/store/row-identity.d.ts.map +1 -0
  313. package/dist/store/row-identity.js +1509 -0
  314. package/dist/store/row-identity.js.map +1 -0
  315. package/dist/store/safety-data-accessor.d.ts +5 -2
  316. package/dist/store/safety-data-accessor.d.ts.map +1 -1
  317. package/dist/store/safety-data-accessor.js +9 -2
  318. package/dist/store/safety-data-accessor.js.map +1 -1
  319. package/dist/store/schema/attachments.d.ts +1 -1
  320. package/dist/store/schema/audit.d.ts +4 -4
  321. package/dist/store/schema/background-jobs.d.ts +1 -1
  322. package/dist/store/schema/cleo-global/nexus.d.ts +1 -1
  323. package/dist/store/schema/cleo-global/skills.d.ts +2 -2
  324. package/dist/store/schema/cleo-project/audit.d.ts +4 -4
  325. package/dist/store/schema/cleo-project/docs.d.ts +1 -1
  326. package/dist/store/schema/cleo-project/lifecycle.d.ts +4 -4
  327. package/dist/store/schema/cleo-project/nexus-graph.d.ts +3 -3
  328. package/dist/store/schema/cleo-project/provenance-commits.d.ts +2 -2
  329. package/dist/store/schema/cleo-project/provenance-rest.d.ts +3 -3
  330. package/dist/store/schema/cleo-project/runtime.d.ts +3 -3
  331. package/dist/store/schema/cleo-project/tasks-core-batch2.d.ts +87 -1
  332. package/dist/store/schema/cleo-project/tasks-core-batch2.d.ts.map +1 -1
  333. package/dist/store/schema/cleo-project/tasks-core-batch2.js +35 -0
  334. package/dist/store/schema/cleo-project/tasks-core-batch2.js.map +1 -1
  335. package/dist/store/schema/cleo-project/tasks-core.d.ts +183 -7
  336. package/dist/store/schema/cleo-project/tasks-core.d.ts.map +1 -1
  337. package/dist/store/schema/cleo-project/tasks-core.js +52 -1
  338. package/dist/store/schema/cleo-project/tasks-core.js.map +1 -1
  339. package/dist/store/schema/cleo-shared/brain.d.ts +6 -6
  340. package/dist/store/schema/lifecycle.d.ts +4 -4
  341. package/dist/store/schema/manifest.d.ts +1 -1
  342. package/dist/store/schema/memory-schema.d.ts +5 -5
  343. package/dist/store/schema/nexus-schema.d.ts +3 -3
  344. package/dist/store/schema/provenance/releases.d.ts +1 -1
  345. package/dist/store/schema/skills-schema.d.ts +2 -2
  346. package/dist/store/schema/tasks.d.ts +6 -6
  347. package/dist/store/session-store.d.ts +63 -3
  348. package/dist/store/session-store.d.ts.map +1 -1
  349. package/dist/store/session-store.js +121 -8
  350. package/dist/store/session-store.js.map +1 -1
  351. package/dist/store/split-brain-import.d.ts +2 -14
  352. package/dist/store/split-brain-import.d.ts.map +1 -1
  353. package/dist/store/split-brain-import.js +40 -19
  354. package/dist/store/split-brain-import.js.map +1 -1
  355. package/dist/store/sqlite-data-accessor.d.ts +326 -2
  356. package/dist/store/sqlite-data-accessor.d.ts.map +1 -1
  357. package/dist/store/sqlite-data-accessor.js +782 -49
  358. package/dist/store/sqlite-data-accessor.js.map +1 -1
  359. package/dist/store/sync/clock-store.d.ts +67 -0
  360. package/dist/store/sync/clock-store.d.ts.map +1 -0
  361. package/dist/store/sync/clock-store.js +117 -0
  362. package/dist/store/sync/clock-store.js.map +1 -0
  363. package/dist/store/sync/flags.d.ts +51 -0
  364. package/dist/store/sync/flags.d.ts.map +1 -0
  365. package/dist/store/sync/flags.js +81 -0
  366. package/dist/store/sync/flags.js.map +1 -0
  367. package/dist/store/sync/hlc.d.ts +115 -0
  368. package/dist/store/sync/hlc.d.ts.map +1 -0
  369. package/dist/store/sync/hlc.js +172 -0
  370. package/dist/store/sync/hlc.js.map +1 -0
  371. package/dist/store/sync/index.d.ts +20 -0
  372. package/dist/store/sync/index.d.ts.map +1 -0
  373. package/dist/store/sync/index.js +20 -0
  374. package/dist/store/sync/index.js.map +1 -0
  375. package/dist/store/sync/replica-registry.d.ts +85 -0
  376. package/dist/store/sync/replica-registry.d.ts.map +1 -0
  377. package/dist/store/sync/replica-registry.js +147 -0
  378. package/dist/store/sync/replica-registry.js.map +1 -0
  379. package/dist/store/sync/replica.d.ts +181 -0
  380. package/dist/store/sync/replica.d.ts.map +1 -0
  381. package/dist/store/sync/replica.js +305 -0
  382. package/dist/store/sync/replica.js.map +1 -0
  383. package/dist/store/sync/schema.d.ts +68 -0
  384. package/dist/store/sync/schema.d.ts.map +1 -0
  385. package/dist/store/sync/schema.js +130 -0
  386. package/dist/store/sync/schema.js.map +1 -0
  387. package/dist/store/sync/skew-hold.d.ts +71 -0
  388. package/dist/store/sync/skew-hold.d.ts.map +1 -0
  389. package/dist/store/sync/skew-hold.js +89 -0
  390. package/dist/store/sync/skew-hold.js.map +1 -0
  391. package/dist/store/table-classification.d.ts +10 -8
  392. package/dist/store/table-classification.d.ts.map +1 -1
  393. package/dist/store/table-classification.js +93 -25
  394. package/dist/store/table-classification.js.map +1 -1
  395. package/dist/store/tasks-sqlite.d.ts.map +1 -1
  396. package/dist/store/tasks-sqlite.js +23 -3
  397. package/dist/store/tasks-sqlite.js.map +1 -1
  398. package/dist/store/twin-collapse.d.ts +113 -2
  399. package/dist/store/twin-collapse.d.ts.map +1 -1
  400. package/dist/store/twin-collapse.js +247 -19
  401. package/dist/store/twin-collapse.js.map +1 -1
  402. package/dist/store/umbrella-data-accessor.d.ts +5 -2
  403. package/dist/store/umbrella-data-accessor.d.ts.map +1 -1
  404. package/dist/store/umbrella-data-accessor.js +9 -2
  405. package/dist/store/umbrella-data-accessor.js.map +1 -1
  406. package/dist/store/validation-schemas.d.ts +241 -49
  407. package/dist/store/validation-schemas.d.ts.map +1 -1
  408. package/dist/system/health.d.ts +2 -0
  409. package/dist/system/health.d.ts.map +1 -1
  410. package/dist/system/health.js +57 -0
  411. package/dist/system/health.js.map +1 -1
  412. package/dist/system/project-health.d.ts +9 -2
  413. package/dist/system/project-health.d.ts.map +1 -1
  414. package/dist/system/project-health.js +8 -3
  415. package/dist/system/project-health.js.map +1 -1
  416. package/dist/task-work/index.d.ts.map +1 -1
  417. package/dist/task-work/index.js +9 -6
  418. package/dist/task-work/index.js.map +1 -1
  419. package/dist/tasks/ac-coverage-gate.d.ts.map +1 -1
  420. package/dist/tasks/ac-coverage-gate.js +3 -0
  421. package/dist/tasks/ac-coverage-gate.js.map +1 -1
  422. package/dist/tasks/ac-table.d.ts +35 -7
  423. package/dist/tasks/ac-table.d.ts.map +1 -1
  424. package/dist/tasks/ac-table.js +119 -14
  425. package/dist/tasks/ac-table.js.map +1 -1
  426. package/dist/tasks/add.d.ts.map +1 -1
  427. package/dist/tasks/add.js +48 -2
  428. package/dist/tasks/add.js.map +1 -1
  429. package/dist/tasks/analyze.d.ts.map +1 -1
  430. package/dist/tasks/analyze.js +3 -2
  431. package/dist/tasks/analyze.js.map +1 -1
  432. package/dist/tasks/backfill-child-projections.js +47 -6
  433. package/dist/tasks/backfill-child-projections.js.map +1 -1
  434. package/dist/tasks/complete.d.ts.map +1 -1
  435. package/dist/tasks/complete.js +6 -1
  436. package/dist/tasks/complete.js.map +1 -1
  437. package/dist/tasks/completion-evaluation.d.ts.map +1 -1
  438. package/dist/tasks/completion-evaluation.js +3 -0
  439. package/dist/tasks/completion-evaluation.js.map +1 -1
  440. package/dist/tasks/evidence.d.ts +19 -1
  441. package/dist/tasks/evidence.d.ts.map +1 -1
  442. package/dist/tasks/evidence.js +93 -21
  443. package/dist/tasks/evidence.js.map +1 -1
  444. package/dist/tasks/infer-add-params.js +1 -1
  445. package/dist/tasks/infer-add-params.js.map +1 -1
  446. package/dist/tasks/task-context.d.ts.map +1 -1
  447. package/dist/tasks/task-context.js +3 -1
  448. package/dist/tasks/task-context.js.map +1 -1
  449. package/dist/tasks/task-import.d.ts.map +1 -1
  450. package/dist/tasks/task-import.js +11 -3
  451. package/dist/tasks/task-import.js.map +1 -1
  452. package/dist/tasks/task-next.d.ts +5 -1
  453. package/dist/tasks/task-next.d.ts.map +1 -1
  454. package/dist/tasks/task-next.js +7 -2
  455. package/dist/tasks/task-next.js.map +1 -1
  456. package/dist/validation/compliance.d.ts +5 -1
  457. package/dist/validation/compliance.d.ts.map +1 -1
  458. package/dist/validation/compliance.js +24 -3
  459. package/dist/validation/compliance.js.map +1 -1
  460. package/dist/validation/engine-ops.d.ts.map +1 -1
  461. package/dist/validation/engine-ops.js +3 -2
  462. package/dist/validation/engine-ops.js.map +1 -1
  463. package/dist/validation/index.d.ts +1 -1
  464. package/dist/validation/index.d.ts.map +1 -1
  465. package/dist/validation/index.js +1 -1
  466. package/dist/validation/index.js.map +1 -1
  467. package/dist/validation/protocol-common.d.ts +47 -2
  468. package/dist/validation/protocol-common.d.ts.map +1 -1
  469. package/dist/validation/protocol-common.js +86 -21
  470. package/dist/validation/protocol-common.js.map +1 -1
  471. package/dist/workgraph/scaffold-apply.d.ts.map +1 -1
  472. package/dist/workgraph/scaffold-apply.js +14 -5
  473. package/dist/workgraph/scaffold-apply.js.map +1 -1
  474. package/migrations/README.md +12 -0
  475. package/migrations/drizzle-cleo-project/20260928120000_t12341-row-uids/migration.sql +169 -0
  476. package/migrations/drizzle-cleo-project/20260930160000_t12886-dependency-cycle-guard/migration.sql +76 -0
  477. package/migrations/drizzle-cleo-project/20260930160000_t12886-dependency-cycle-guard/revert.sql +7 -0
  478. package/migrations/sync-journal/20260929140000_t12342-sync-clock/migration.sql +42 -0
  479. package/package.json +13 -13
  480. package/templates/CLEO-INJECTION.md +2 -2
  481. package/templates/CLEO-REFERENCE.md +5 -1
@@ -16,16 +16,21 @@ import { ARCHIVE_REASON_TOMBSTONE, ExitCode, } from '@cleocode/contracts';
16
16
  import { and, eq, getTableColumns, inArray, isNull, like, lte, ne, notInArray, or, sql, } from 'drizzle-orm';
17
17
  import { CleoError } from '../errors.js';
18
18
  import { captureProjectScope as captureTaskAccessorScope, getProjectRoot, worktreeScope, } from '../project-scope.js';
19
+ import { acTextHash, buildAcRowId } from '../tasks/ac-table.js';
20
+ import { generateAuditLogId, pruneAcBindingsForAcIds, pruneAcBindingsForTask, pruneOrphanAcBindings, selectOrphanAcBindings, } from './ac-binding-prune.js';
19
21
  import { withBackgroundOpCommitBoundary } from './background-ops.js';
20
22
  import { archivedTaskToRow, rowToSession, rowToTask, taskToRow } from './converters.js';
21
23
  import { cleanupBrainRefsOnSessionDelete } from './cross-db-cleanup.js';
22
24
  import { batchUpdateDependencies, insertNewTask, loadDependenciesForTasks, loadRelationsForTasks, parseLabels, updateDependencies, updateTaskLabels, upsertSession, upsertTask, } from './db-helpers.js';
25
+ import { fillRowUids, parseStoreTimestamp, registerRowIdentityWriters, rowIdentityShared, } from './row-identity.js';
26
+ import { rowUidFillEnabled } from './row-identity-flag.js';
27
+ import { ROW_IDENTITY } from './row-identity-registry.js';
23
28
  import { tasksAuditLog } from './schema/cleo-project/audit.js';
24
29
  import { resolveCurrentSession } from './session-store.js';
25
30
  import { closeDb, getDb, getNativeTasksDb } from './sqlite.js';
26
31
  import { TERMINAL_TASK_STATUSES } from './status-registry.js';
27
32
  import { claimAllows, claimColumnsOf, claimFromColumns, isClaimExpired, leaseExpiresAtFrom, taskClaimedError, taskClaimLeaseMs, } from './task-claim.js';
28
- import { nextTaskVersion, taskConflictError, taskVersion } from './task-version.js';
33
+ import { nextTaskVersion, taskConflictError, taskVersion, } from './task-version.js';
29
34
  import * as schema from './tasks-schema.js';
30
35
  import { assertTwinCollapseWritable, bareCounterOf, mirrorCounterToBare } from './twin-collapse.js';
31
36
  import { runHeartbeatWrite, withWriteRetry } from './with-retry.js';
@@ -37,13 +42,67 @@ import { runHeartbeatWrite, withWriteRetry } from './with-retry.js';
37
42
  */
38
43
  let _txSavepointCounter = 0;
39
44
  /**
40
- * Generate a unique audit log entry ID.
41
- * @task T4837
45
+ * Evidence bindings for a set of CURRENT AC ids (T12341).
46
+ *
47
+ * A binding is matched through the criterion's uid (`ac_uid`), which survives
48
+ * an AC edit, and falls back to `ac_id` for a binding whose uid is not filled.
49
+ * A binding recorded against an older text of a criterion is reported under
50
+ * the criterion's current id, so callers keyed by AC id keep working, and is
51
+ * marked `stale` when the text hash it recorded (`ac_text_hash`) differs from
52
+ * the criterion's current text: evidence for different text never satisfies a
53
+ * gate (spec §8.2).
42
54
  */
43
- function generateAuditLogId() {
44
- const epoch = Math.floor(Date.now() / 1000);
45
- const rand = Math.random().toString(36).slice(2, 8);
46
- return `log-${epoch}-${rand}`;
55
+ async function selectAcBindings(db, acIds) {
56
+ if (acIds.length === 0)
57
+ return [];
58
+ const b = schema.evidenceAcBindings;
59
+ const ac = schema.taskAcceptanceCriteria;
60
+ const ids = acIds;
61
+ const columns = {
62
+ id: b.id,
63
+ evidenceAtomId: b.evidenceAtomId,
64
+ acId: b.acId,
65
+ currentAcId: ac.id,
66
+ currentAcText: ac.text,
67
+ recordedTextHash: b.acTextHash,
68
+ bindingType: b.bindingType,
69
+ createdAt: b.createdAt,
70
+ };
71
+ // Two indexed lookups rather than one OR across the join: by the current
72
+ // criterion's uid, then by the recorded ac_id.
73
+ const byUid = await db
74
+ .select(columns)
75
+ .from(b)
76
+ .innerJoin(ac, eq(ac.uid, b.acUid))
77
+ .where(inArray(ac.id, ids))
78
+ .all();
79
+ const byId = await db
80
+ .select(columns)
81
+ .from(b)
82
+ .leftJoin(ac, eq(ac.uid, b.acUid))
83
+ .where(inArray(b.acId, ids))
84
+ .all();
85
+ const rows = [...byUid, ...byId];
86
+ const wanted = new Set(ids);
87
+ const seen = new Set();
88
+ const out = [];
89
+ for (const r of rows) {
90
+ if (seen.has(r.id))
91
+ continue;
92
+ seen.add(r.id);
93
+ const stale = r.recordedTextHash !== null &&
94
+ r.currentAcText !== null &&
95
+ acTextHash(r.currentAcText) !== r.recordedTextHash;
96
+ out.push({
97
+ id: r.id,
98
+ evidenceAtomId: r.evidenceAtomId,
99
+ acId: r.currentAcId && wanted.has(r.currentAcId) ? r.currentAcId : r.acId,
100
+ bindingType: r.bindingType,
101
+ createdAt: r.createdAt,
102
+ ...(stale ? { stale: true } : {}),
103
+ });
104
+ }
105
+ return out;
47
106
  }
48
107
  // ---- Schema meta helpers ----
49
108
  /** Read a JSON blob from the schema_meta table by key. */
@@ -132,6 +191,519 @@ export function advanceTaskIdSequence(nativeDb, floor) {
132
191
  .get();
133
192
  return row?.counter;
134
193
  }
194
+ /**
195
+ * Give the task with `uid` the display id `toId`: a compare-and-set on the
196
+ * stored version AND claim lease that advances the version (so a stale
197
+ * `--if-match` writer fails, #1698), with the claim columns carried unchanged
198
+ * (#1701), then every local reference rewritten. Runs in the caller's
199
+ * transaction with foreign keys deferred to its commit.
200
+ *
201
+ * A collision re-mint is a system write: it passes no claim guard and never
202
+ * takes or drops a lease, but the version bump makes the lease holder's next
203
+ * guarded write re-read. A caller acting for a session (a manual rename)
204
+ * passes `guard`: `expectedUpdatedAt` fails with `E_CONFLICT` when the task
205
+ * moved on, `claim` fails with `E_TASK_CLAIMED` when another session holds it.
206
+ *
207
+ * @param nativeDb - The project `cleo.db` handle holding the transaction.
208
+ * @param uid - Uid of the task to rename.
209
+ * @param toId - Its new display id.
210
+ * @param refs - Columns that hold task display ids.
211
+ * @param guard - Optional version / claim guard.
212
+ * @returns The id it had and the rows rewritten per `table.column`.
213
+ * @task T12341
214
+ * @task T12748
215
+ */
216
+ export function renameTaskDisplayIdNative(nativeDb, uid, toId, refs, guard = {}) {
217
+ const current = nativeDb
218
+ .prepare(`SELECT id, updated_at AS updatedAt, created_at AS createdAt,
219
+ claimed_by_session AS claimedBySession, claimed_by_agent AS claimedByAgent,
220
+ claimed_at AS claimedAt, lease_expires_at AS leaseExpiresAt
221
+ FROM tasks_tasks WHERE uid = ?`)
222
+ .get(uid);
223
+ if (!current)
224
+ throw new Error(`No task with uid ${uid}`);
225
+ if (guard.expectedUpdatedAt !== undefined && guard.expectedUpdatedAt !== taskVersion(current)) {
226
+ throw taskConflictError(current.id, guard.expectedUpdatedAt, current);
227
+ }
228
+ if (guard.claim)
229
+ assertClaimAllowed(current.id, current, guard.claim, []);
230
+ nativeDb.exec('PRAGMA defer_foreign_keys = ON');
231
+ const changed = nativeDb
232
+ .prepare(`UPDATE tasks_tasks SET id = ?, updated_at = ?
233
+ WHERE uid = ? AND id = ? AND updated_at IS ? AND claimed_by_session IS ?
234
+ AND lease_expires_at IS ?`)
235
+ .run(toId, nextTaskVersion(current), uid, current.id, current.updatedAt, current.claimedBySession, current.leaseExpiresAt).changes;
236
+ if (Number(changed) !== 1) {
237
+ throw taskConflictError(current.id, taskVersion(current), null);
238
+ }
239
+ recordLocalRename(nativeDb, uid, current.id, toId, taskVersion(current));
240
+ const rewritten = {};
241
+ const q = (name) => `"${name.replaceAll('"', '""')}"`;
242
+ for (const ref of refs) {
243
+ const table = `main.${q(ref.table)}`;
244
+ const col = q(ref.column);
245
+ const n = ref.jsonArray
246
+ ? nativeDb
247
+ .prepare(`UPDATE ${table} SET ${col} = (
248
+ SELECT json_group_array(CASE WHEN j.value = ? THEN ? ELSE j.value END)
249
+ FROM json_each(${table}.${col}) j)
250
+ WHERE json_valid(${col}) AND EXISTS (
251
+ SELECT 1 FROM json_each(${table}.${col}) j WHERE j.value = ?)`)
252
+ .run(current.id, toId, current.id).changes
253
+ : nativeDb.prepare(`UPDATE ${table} SET ${col} = ? WHERE ${col} = ?`).run(toId, current.id)
254
+ .changes;
255
+ if (Number(n) > 0)
256
+ rewritten[`${ref.table}.${ref.column}`] = Number(n);
257
+ }
258
+ for (const [key, n] of Object.entries(rederiveAcIdsNative(nativeDb, current.id, toId))) {
259
+ if (n > 0)
260
+ rewritten[key] = n;
261
+ }
262
+ return { fromId: current.id, rewritten };
263
+ }
264
+ /**
265
+ * After a task's display id changed from `fromId` to `toId`, re-derive the
266
+ * ids of the acceptance criteria derived from it (`buildAcRowId(taskId,
267
+ * identity)`): the task's own criteria, and a parent's child-projection
268
+ * criteria (`child:<id>`, whose source key is rewritten too). The binding and
269
+ * history rows that point at a re-derived id follow. A criterion whose id was
270
+ * not derived from the old id (an explicit id) keeps it. Without this, two
271
+ * tasks that once shared a display id keep identical ids for same-text
272
+ * criteria ("tests pass") and collide on every merge (T12799).
273
+ *
274
+ * @param nativeDb - The project `cleo.db` handle, inside the rename's transaction.
275
+ * @param fromId - The task's old display id.
276
+ * @param toId - Its new display id (the task row already carries it).
277
+ * @returns Rows rewritten per `table.column`.
278
+ * @task T12799
279
+ */
280
+ export function rederiveAcIdsNative(nativeDb, fromId, toId) {
281
+ const count = {};
282
+ const bump = (key, n) => {
283
+ count[key] = (count[key] ?? 0) + Number(n);
284
+ };
285
+ const has = (table) => nativeDb
286
+ .prepare("SELECT 1 AS x FROM sqlite_master WHERE type = 'table' AND name = ?")
287
+ .get(table) !== undefined;
288
+ if (!has('tasks_task_acceptance_criteria'))
289
+ return count;
290
+ const oldChild = `child:${fromId}`;
291
+ const rows = nativeDb
292
+ .prepare(`SELECT id, task_id AS taskId, kind, text, source_key AS sourceKey
293
+ FROM tasks_task_acceptance_criteria WHERE task_id = ? OR source_key = ?`)
294
+ .all(toId, oldChild);
295
+ const followers = ['tasks_evidence_ac_bindings', 'tasks_task_acceptance_criteria_history'].filter(has);
296
+ for (const row of rows) {
297
+ const ownerBefore = row.taskId === toId ? fromId : row.taskId;
298
+ const sourceAfter = row.sourceKey === oldChild ? `child:${toId}` : row.sourceKey;
299
+ const identity = (source) => (row.kind === 'text' ? row.text : (source ?? ''));
300
+ const derived = row.id === buildAcRowId(ownerBefore, identity(row.sourceKey));
301
+ const newId = derived ? buildAcRowId(row.taskId, identity(sourceAfter)) : row.id;
302
+ if (newId === row.id && sourceAfter === row.sourceKey)
303
+ continue;
304
+ bump('tasks_task_acceptance_criteria.id', nativeDb
305
+ .prepare('UPDATE tasks_task_acceptance_criteria SET id = ?, source_key = ? WHERE id = ?')
306
+ .run(newId, sourceAfter, row.id).changes);
307
+ if (newId === row.id)
308
+ continue;
309
+ for (const table of followers) {
310
+ bump(`${table}.ac_id`, nativeDb.prepare(`UPDATE ${table} SET ac_id = ? WHERE ac_id = ?`).run(newId, row.id)
311
+ .changes);
312
+ }
313
+ }
314
+ return count;
315
+ }
316
+ /**
317
+ * Insert a display-id alias row. Idempotent on its uid, and order-independent
318
+ * (T12800): when two displacements write one alias uid, the stored row is
319
+ * replaced only when `precedes(incoming, stored)` says the incoming one ranks
320
+ * first, so every replica keeps the same row whatever order the ops arrived
321
+ * in. The caller ranks by HLC order (`compareHlc`), never by text: stored
322
+ * values may be in either HLC format (T12802).
323
+ *
324
+ * @param nativeDb - The project `cleo.db` handle.
325
+ * @param row - The alias.
326
+ * @param precedes - Whether `incoming` ranks before the `stored` row of the same uid.
327
+ * @task T12341
328
+ */
329
+ export function insertDisplayIdAliasNative(nativeDb, row, precedes) {
330
+ const stored = nativeDb
331
+ .prepare(`SELECT uid, entity_table AS entityTable, display_id AS displayId, entity_uid AS entityUid,
332
+ reason, origin, displaced_hlc AS displacedHlc, created_at AS createdAt,
333
+ entity_birth_fp AS entityBirthFp
334
+ FROM tasks_display_id_aliases WHERE uid = ?`)
335
+ .get(row.uid);
336
+ if (!stored) {
337
+ nativeDb
338
+ .prepare(`INSERT INTO tasks_display_id_aliases
339
+ (uid, entity_table, display_id, entity_uid, reason, origin, displaced_hlc, created_at,
340
+ entity_birth_fp)
341
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`)
342
+ .run(row.uid, row.entityTable, row.displayId, row.entityUid, row.reason, row.origin, row.displacedHlc, row.createdAt, row.entityBirthFp);
343
+ return;
344
+ }
345
+ if (!precedes(row, stored))
346
+ return;
347
+ nativeDb
348
+ .prepare(`UPDATE tasks_display_id_aliases
349
+ SET origin = ?, displaced_hlc = ?, created_at = ?,
350
+ entity_birth_fp = coalesce(entity_birth_fp, ?)
351
+ WHERE uid = ?`)
352
+ .run(row.origin, row.displacedHlc, row.createdAt, row.entityBirthFp, row.uid);
353
+ }
354
+ /**
355
+ * Delete one display-id alias row (a local alias whose entity was re-keyed is
356
+ * re-inserted under its new entity uid by the caller).
357
+ *
358
+ * @param nativeDb - The project `cleo.db` handle.
359
+ * @param uid - The alias row's uid.
360
+ * @task T12341
361
+ */
362
+ export function deleteDisplayIdAliasNative(nativeDb, uid) {
363
+ nativeDb.prepare('DELETE FROM tasks_display_id_aliases WHERE uid = ?').run(uid);
364
+ }
365
+ /**
366
+ * Insert a uid re-key alias row (idempotent on its uid).
367
+ *
368
+ * @param nativeDb - The project `cleo.db` handle.
369
+ * @param row - The alias.
370
+ * @task T12341
371
+ */
372
+ export function insertUidAliasNative(nativeDb, row) {
373
+ nativeDb
374
+ .prepare(`INSERT OR IGNORE INTO tasks_uid_aliases
375
+ (uid, entity_table, old_uid, old_birth_fp, new_uid, origin, displaced_hlc, created_at)
376
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`)
377
+ .run(row.uid, row.entityTable, row.oldUid, row.oldBirthFp, row.newUid, row.origin, row.displacedHlc, row.createdAt);
378
+ }
379
+ /**
380
+ * Hold an incoming row the merge cannot place yet (replacing an older hold of
381
+ * the same row).
382
+ *
383
+ * @param nativeDb - The project `cleo.db` handle.
384
+ * @param row - The held row.
385
+ * @task T12341
386
+ */
387
+ export function insertQuarantineNative(nativeDb, row) {
388
+ nativeDb
389
+ .prepare(`INSERT INTO tasks_identity_quarantine
390
+ (entity_table, uid, birth_fp, reason, contested_id, row_json, received_hlc, created_at)
391
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
392
+ ON CONFLICT(entity_table, uid, birth_fp) DO UPDATE SET
393
+ reason = excluded.reason, contested_id = excluded.contested_id,
394
+ row_json = excluded.row_json, received_hlc = excluded.received_hlc,
395
+ created_at = excluded.created_at`)
396
+ .run(row.entityTable, row.uid, row.birthFp, row.reason, row.contestedId, row.rowJson, row.receivedHlc, row.createdAt);
397
+ }
398
+ /**
399
+ * Drop a held row (released or superseded).
400
+ *
401
+ * @param nativeDb - The project `cleo.db` handle.
402
+ * @param key - The held row's table, uid and birth fingerprint.
403
+ * @task T12341
404
+ */
405
+ export function deleteQuarantineNative(nativeDb, key) {
406
+ nativeDb
407
+ .prepare('DELETE FROM tasks_identity_quarantine WHERE entity_table = ? AND uid = ? AND birth_fp = ?')
408
+ .run(key.entityTable, key.uid, key.birthFp);
409
+ }
410
+ /**
411
+ * Write a row-identity meta value (the recipe marker, a re-mint record).
412
+ *
413
+ * @param nativeDb - The project `cleo.db` handle.
414
+ * @param key - Meta key.
415
+ * @param value - Meta value.
416
+ * @task T12341
417
+ */
418
+ export function writeRowIdentityMetaNative(nativeDb, key, value) {
419
+ nativeDb
420
+ .prepare(`INSERT INTO tasks_row_identity_meta (key, value) VALUES (?, ?)
421
+ ON CONFLICT(key) DO UPDATE SET value = excluded.value`)
422
+ .run(key, value);
423
+ }
424
+ /**
425
+ * Hand a criterion back the uid its deleted predecessor carried (AC uid
426
+ * graveyard re-link, spec §6.5), with that predecessor's birth fingerprint.
427
+ *
428
+ * @param nativeDb - The project `cleo.db` handle.
429
+ * @param acId - The criterion to re-link (its uid is NULL).
430
+ * @param uid - The uid to hand back.
431
+ * @param birthFp - The fingerprint that goes with it, when recorded.
432
+ * @returns Rows changed (0 or 1).
433
+ * @task T12341
434
+ */
435
+ export function relinkAcUidNative(nativeDb, acId, uid, birthFp) {
436
+ return Number(nativeDb
437
+ .prepare('UPDATE tasks_task_acceptance_criteria SET uid = ?, birth_fp = COALESCE(?, birth_fp) WHERE id = ? AND uid IS NULL')
438
+ .run(uid, birthFp, acId).changes);
439
+ }
440
+ /**
441
+ * Empty the AC uid graveyard (after the open pass consumed it).
442
+ *
443
+ * @param nativeDb - The project `cleo.db` handle.
444
+ * @task T12341
445
+ */
446
+ export function clearAcUidGraveyardNative(nativeDb) {
447
+ nativeDb.exec('DELETE FROM tasks_ac_uid_graveyard');
448
+ }
449
+ /**
450
+ * Read the birth of task `taskId` before an overwrite replaces it.
451
+ *
452
+ * @param nativeDb - The project `cleo.db` handle.
453
+ * @param taskId - The task about to be overwritten.
454
+ * @returns Its birth, or `undefined` when there is no such row.
455
+ * @task T12806
456
+ */
457
+ export function readReplacedTaskBirthNative(nativeDb, taskId) {
458
+ return nativeDb
459
+ .prepare('SELECT created_at AS createdAt, uid FROM tasks_tasks WHERE id = ?')
460
+ .get(taskId);
461
+ }
462
+ /**
463
+ * After an overwrite import replaced task `taskId`, keep or re-key its
464
+ * identity (T12806 review):
465
+ *
466
+ * - the SAME task (same id, same birth instant as before the overwrite) keeps
467
+ * its identity. Sameness is decided on the immutable birth facts, never on
468
+ * the title or type: a task retitled here and re-imported unchanged stays
469
+ * the same row;
470
+ * - refused once identity is shared: the overwrite would re-point a uid other
471
+ * devices hold at different work;
472
+ * - otherwise the task, the natural rows keyed by it (dependencies,
473
+ * relations, labels) and its criteria are re-derived from the new row, now,
474
+ * and every AC history row and binding that pointed at one of those
475
+ * criteria is RE-KEYED to the criterion's new uid, found by the criterion's
476
+ * old uid as well as by its current id. A binding whose `ac_id` is stale
477
+ * (#1731) keeps resolving: its evidence is kept, never dropped.
478
+ *
479
+ * @param nativeDb - The project `cleo.db` handle holding the transaction.
480
+ * @param taskId - The overwritten task.
481
+ * @param before - Its birth before the overwrite ({@link readReplacedTaskBirthNative}).
482
+ * @task T12806
483
+ */
484
+ export function clearReplacedTaskIdentityNative(nativeDb, taskId, before) {
485
+ const row = nativeDb
486
+ .prepare('SELECT uid, birth_fp AS fp, created_at AS createdAt FROM tasks_tasks WHERE id = ?')
487
+ .get(taskId);
488
+ if (!row || (row.uid === null && row.fp === null))
489
+ return;
490
+ if (before) {
491
+ const was = parseStoreTimestamp(before.createdAt);
492
+ if (was !== null && was === parseStoreTimestamp(row.createdAt)) {
493
+ return; // the same task, re-imported: its identity stands
494
+ }
495
+ }
496
+ if (rowIdentityShared(nativeDb)) {
497
+ throw new CleoError(ExitCode.VALIDATION_ERROR, `Task ${taskId} cannot be overwritten by an import: its identity is shared with other devices`, {
498
+ fix: 'Import it under a new id (the default duplicate strategy), or edit the task instead of overwriting it.',
499
+ details: { field: 'taskId', actual: taskId },
500
+ });
501
+ }
502
+ const has = (table) => nativeDb
503
+ .prepare("SELECT 1 AS x FROM sqlite_master WHERE type = 'table' AND name = ?")
504
+ .get(table) !== undefined;
505
+ nativeDb.prepare('UPDATE tasks_tasks SET uid = NULL, birth_fp = NULL WHERE id = ?').run(taskId);
506
+ for (const spec of ROW_IDENTITY.project) {
507
+ if (spec.kind !== 'natural' || !has(spec.table))
508
+ continue;
509
+ const cols = (spec.keyRefs ?? []).filter((r) => r.table === 'tasks_tasks').map((r) => r.column);
510
+ if (cols.length === 0)
511
+ continue;
512
+ nativeDb
513
+ .prepare(`UPDATE main.${quoteIdent(spec.table)} SET uid = NULL WHERE ${cols.map((c) => `${quoteIdent(c)} = ?`).join(' OR ')}`)
514
+ .run(...cols.map(() => taskId));
515
+ }
516
+ const children = ['tasks_task_acceptance_criteria_history', 'tasks_evidence_ac_bindings'].filter(has);
517
+ const criteria = has('tasks_task_acceptance_criteria')
518
+ ? nativeDb
519
+ .prepare('SELECT id, uid FROM tasks_task_acceptance_criteria WHERE task_id = ?')
520
+ .all(taskId)
521
+ : [];
522
+ if (criteria.length > 0) {
523
+ nativeDb
524
+ .prepare('UPDATE tasks_task_acceptance_criteria SET uid = NULL, birth_fp = NULL WHERE task_id = ?')
525
+ .run(taskId);
526
+ }
527
+ // Re-derive the task, its natural rows and its criteria now, inside the
528
+ // import transaction, so the children can be re-keyed to the new uids.
529
+ fillRowUids(nativeDb, 'project');
530
+ const newUid = nativeDb.prepare('SELECT uid FROM tasks_task_acceptance_criteria WHERE id = ?');
531
+ let rekeyed = 0;
532
+ for (const ac of criteria) {
533
+ const now = newUid.get(ac.id)?.uid ?? null;
534
+ for (const table of children) {
535
+ // By the criterion's old uid (a stale ac_id keeps it) or its current id.
536
+ rekeyed += Number(nativeDb
537
+ .prepare(`UPDATE main.${quoteIdent(table)} SET ac_uid = ?, birth_fp = NULL
538
+ WHERE ac_id = ? OR (? IS NOT NULL AND ac_uid = ?)`)
539
+ .run(now, ac.id, ac.uid, ac.uid).changes);
540
+ }
541
+ }
542
+ // Their fingerprints hash the criterion's: re-derive them against the new one.
543
+ if (rekeyed > 0)
544
+ fillRowUids(nativeDb, 'project');
545
+ }
546
+ /** Quote an identifier for the dynamic row-identity writes below. */
547
+ function quoteIdent(name) {
548
+ return `"${name.replaceAll('"', '""')}"`;
549
+ }
550
+ /**
551
+ * Fill a NULL identity column (`uid`, `birth_fp`, a stored reference uid) of a
552
+ * declared table from its recipe SQL (the row-uid open pass, spec §6.1).
553
+ *
554
+ * @param nativeDb - The `cleo.db` handle holding the cold-open lease.
555
+ * @param table - Declared table.
556
+ * @param column - Identity column.
557
+ * @param valueSql - Recipe expression over `main."<table>"` columns.
558
+ * @param rowid - One row only (the clash fallback); all NULL rows otherwise.
559
+ * @returns Rows written.
560
+ * @task T12341
561
+ */
562
+ export function fillIdentityColumnNative(nativeDb, table, column, valueSql, rowid) {
563
+ const col = quoteIdent(column);
564
+ const sql = `UPDATE main.${quoteIdent(table)} SET ${col} = ${valueSql} WHERE ${col} IS NULL`;
565
+ const changes = rowid === undefined
566
+ ? nativeDb.prepare(sql).run().changes
567
+ : nativeDb.prepare(`${sql} AND rowid = ?`).run(rowid).changes;
568
+ return Number(changes);
569
+ }
570
+ /**
571
+ * Clear one pre-release birth fingerprint so the fill re-derives it (spec §12.1).
572
+ *
573
+ * @param nativeDb - The `cleo.db` handle holding the cold-open lease.
574
+ * @param table - Declared minted table.
575
+ * @param rowid - The row.
576
+ * @task T12341
577
+ */
578
+ export function clearBirthFpNative(nativeDb, table, rowid) {
579
+ nativeDb
580
+ .prepare(`UPDATE main.${quoteIdent(table)} SET birth_fp = NULL WHERE rowid = ?`)
581
+ .run(rowid);
582
+ }
583
+ /**
584
+ * Insert one row into a declared identity table, with the columns the row
585
+ * carries that the table has (a row released from the identity quarantine).
586
+ *
587
+ * @param nativeDb - The project `cleo.db` handle.
588
+ * @param table - Declared table.
589
+ * @param row - Column → value.
590
+ * @task T12341
591
+ */
592
+ export function insertIdentityRowNative(nativeDb, table, row) {
593
+ const have = new Set(nativeDb.prepare('SELECT name FROM pragma_table_info(?)').all(table).map((c) => c.name));
594
+ const cols = Object.keys(row).filter((c) => have.has(c));
595
+ nativeDb
596
+ .prepare(`INSERT INTO main.${quoteIdent(table)} (${cols.map(quoteIdent).join(', ')}) VALUES (${cols
597
+ .map(() => '?')
598
+ .join(', ')})`)
599
+ .run(...cols.map((c) => row[c] ?? null));
600
+ }
601
+ /**
602
+ * Give the row of `table` identified by (`oldUid`, `birthFp`) the uid
603
+ * `newUid` (a uid re-key; the birth fingerprint never changes).
604
+ *
605
+ * @param nativeDb - The project `cleo.db` handle.
606
+ * @param table - Declared minted table.
607
+ * @param oldUid - The colliding uid.
608
+ * @param birthFp - The row's birth fingerprint.
609
+ * @param newUid - Its new uid.
610
+ * @returns Rows changed (0 or 1).
611
+ * @task T12341
612
+ */
613
+ export function setRowUidNative(nativeDb, table, oldUid, birthFp, newUid) {
614
+ return Number(nativeDb
615
+ .prepare(`UPDATE main.${quoteIdent(table)} SET uid = ? WHERE uid = ? AND birth_fp = ?`)
616
+ .run(newUid, oldUid, birthFp).changes);
617
+ }
618
+ /**
619
+ * Point a stored reference uid column (`ac_uid`) of the rows that reference
620
+ * the re-keyed row by key at its new uid. Scoped by the key column, so a row
621
+ * referencing the OTHER holder of a colliding uid is never touched.
622
+ *
623
+ * @param nativeDb - The project `cleo.db` handle.
624
+ * @param table - Table holding the column.
625
+ * @param column - The stored reference uid column.
626
+ * @param fromColumn - The column that references the row by key (`ac_id`).
627
+ * @param key - The re-keyed row's local key.
628
+ * @param oldUid - Old referenced uid.
629
+ * @param newUid - New referenced uid.
630
+ * @returns Rows changed.
631
+ * @task T12341
632
+ */
633
+ export function rewriteStoredRefUidNative(nativeDb, table, column, fromColumn, key, oldUid, newUid) {
634
+ const col = quoteIdent(column);
635
+ return Number(nativeDb
636
+ .prepare(`UPDATE main.${quoteIdent(table)} SET ${col} = ? WHERE ${col} = ? AND ${quoteIdent(fromColumn)} = ?`)
637
+ .run(newUid, oldUid, key).changes);
638
+ }
639
+ /**
640
+ * Clear one row's uid so the natural recipe re-derives it (a uid re-key
641
+ * changed an endpoint's uid).
642
+ *
643
+ * @param nativeDb - The project `cleo.db` handle.
644
+ * @param table - Declared natural table.
645
+ * @param rowid - The row.
646
+ * @task T12341
647
+ */
648
+ export function clearRowUidNative(nativeDb, table, rowid) {
649
+ nativeDb.prepare(`UPDATE main.${quoteIdent(table)} SET uid = NULL WHERE rowid = ?`).run(rowid);
650
+ }
651
+ /**
652
+ * Delete a row-identity meta value (a re-mint record that moved with a re-key).
653
+ *
654
+ * @param nativeDb - The project `cleo.db` handle.
655
+ * @param key - Meta key.
656
+ * @task T12341
657
+ */
658
+ export function deleteRowIdentityMetaNative(nativeDb, key) {
659
+ nativeDb.prepare('DELETE FROM tasks_row_identity_meta WHERE key = ?').run(key);
660
+ }
661
+ /**
662
+ * Set a natural row's uid by its current uid (a re-key re-derives the natural
663
+ * uids keyed by the re-keyed row; a receiver applies the published values).
664
+ *
665
+ * @param nativeDb - The project `cleo.db` handle.
666
+ * @param table - Declared natural table.
667
+ * @param oldUid - Current uid.
668
+ * @param newUid - New uid.
669
+ * @returns Rows changed.
670
+ * @task T12341
671
+ */
672
+ export function setNaturalUidNative(nativeDb, table, oldUid, newUid) {
673
+ return Number(nativeDb
674
+ .prepare(`UPDATE main.${quoteIdent(table)} SET uid = ? WHERE uid = ?`)
675
+ .run(newUid, oldUid).changes);
676
+ }
677
+ /**
678
+ * Rewrite a held row (identity quarantine) under a new uid: a uid re-key of a
679
+ * row this replica holds but has not placed.
680
+ *
681
+ * @param nativeDb - The project `cleo.db` handle.
682
+ * @param key - The held row's table, uid and birth fingerprint.
683
+ * @param newUid - Its new uid.
684
+ * @param rowJson - Its wire form with the new uid.
685
+ * @task T12341
686
+ */
687
+ export function rekeyQuarantineNative(nativeDb, key, newUid, rowJson) {
688
+ nativeDb
689
+ .prepare(`UPDATE tasks_identity_quarantine SET uid = ?, row_json = ?
690
+ WHERE entity_table = ? AND uid = ? AND birth_fp = ?`)
691
+ .run(newUid, rowJson, key.entityTable, key.uid, key.birthFp);
692
+ }
693
+ /**
694
+ * Add labels to a task's label junction (idempotent), for writers that
695
+ * insert a task with raw SQL (the sentient proposal ticks).
696
+ *
697
+ * @param nativeDb - The project `cleo.db` handle.
698
+ * @param taskId - The task.
699
+ * @param labels - Labels to add.
700
+ * @task T12341
701
+ */
702
+ export function addTaskLabelsNative(nativeDb, taskId, labels) {
703
+ const stmt = nativeDb.prepare('INSERT OR IGNORE INTO tasks_task_labels (task_id, label) VALUES (?, ?)');
704
+ for (const label of labels)
705
+ stmt.run(taskId, label);
706
+ }
135
707
  // One queue per shared native handle, independent of accessor identity. These
136
708
  // hold coordination state only; the ProjectStore remains the owner of handles.
137
709
  const taskTransactionQueue = new WeakMap();
@@ -219,6 +791,83 @@ function claimPredicate(guard, allowedHolders) {
219
791
  : undefined;
220
792
  return or(isNull(holder), mine, related, expired);
221
793
  }
794
+ /** How long after a local rename a write to the old id is still read as meant for the renamed task. */
795
+ export const RENAMED_RECENT_MS = 24 * 60 * 60 * 1000;
796
+ function renameRecordKey(fromId) {
797
+ return `renamed_from:tasks_tasks:${fromId}`;
798
+ }
799
+ function hasIdentityMeta(nativeDb) {
800
+ return (nativeDb
801
+ .prepare("SELECT 1 AS x FROM sqlite_master WHERE type = 'table' AND name = 'tasks_row_identity_meta'")
802
+ .get() !== undefined);
803
+ }
804
+ /**
805
+ * Remember, locally, that `fromId` named the task `uid` at version
806
+ * `fromVersion` until now (T12800). Local-only (`tasks_row_identity_meta`):
807
+ * it answers "what did a caller of THIS store mean by `fromId`", which only
808
+ * this store's history can.
809
+ */
810
+ function recordLocalRename(nativeDb, uid, fromId, toId, fromVersion) {
811
+ if (!hasIdentityMeta(nativeDb))
812
+ return;
813
+ const record = { uid, fromVersion, toId, at: new Date().toISOString() };
814
+ writeRowIdentityMetaNative(nativeDb, renameRecordKey(fromId), JSON.stringify(record));
815
+ }
816
+ /**
817
+ * Refuse a guarded write that names a display id its task no longer carries
818
+ * (T12800), when the caller can only mean the renamed task:
819
+ *
820
+ * - it expects EXACTLY the version the renamed task had when it left the id
821
+ * (the version the caller read under the old id), and that is not the
822
+ * current holder's version: a stale write on the current holder stays a
823
+ * plain `E_CONFLICT`;
824
+ * - or it renews or releases a claim the caller's session holds on the
825
+ * renamed task (acquiring or taking over the new holder is legitimate: a
826
+ * pivot claims its target before it releases its source);
827
+ * - or no row holds the old id any more.
828
+ *
829
+ * Only a RECENT rename counts ({@link RENAMED_RECENT_MS}), and only one this
830
+ * store performed (the local rename record). `E_TASK_RENAMED` carries the new
831
+ * id in its details.
832
+ */
833
+ function assertNotRenamed(nativeDb, taskId, guard, current) {
834
+ if (!nativeDb || !hasIdentityMeta(nativeDb))
835
+ return;
836
+ const raw = nativeDb
837
+ .prepare('SELECT value FROM tasks_row_identity_meta WHERE key = ?')
838
+ .get(renameRecordKey(taskId));
839
+ if (!raw)
840
+ return;
841
+ let record;
842
+ try {
843
+ record = JSON.parse(raw.value);
844
+ }
845
+ catch {
846
+ return;
847
+ }
848
+ if (Date.now() - Date.parse(record.at) > RENAMED_RECENT_MS)
849
+ return;
850
+ const renamed = nativeDb
851
+ .prepare('SELECT id, claimed_by_session AS claimedBySession FROM tasks_tasks WHERE uid = ?')
852
+ .get(record.uid);
853
+ if (!renamed || renamed.id === taskId)
854
+ return;
855
+ const expected = guard.expectedUpdatedAt;
856
+ const session = guard.claim?.sessionId ?? null;
857
+ const holdsRenamedClaim = session !== null &&
858
+ (guard.claim?.mode === 'renew' || guard.claim?.mode === 'release') &&
859
+ renamed.claimedBySession === session &&
860
+ current?.claimedBySession !== session;
861
+ const readRenamed = expected !== undefined &&
862
+ expected === record.fromVersion &&
863
+ (current === undefined || taskVersion(current) !== expected);
864
+ if (!(current === undefined || holdsRenamedClaim || readRenamed))
865
+ return;
866
+ throw new CleoError(ExitCode.TASK_RENAMED, `Task ${taskId} is now ${renamed.id}: sync re-numbered it after a display-id collision, and ${taskId} ${current ? 'names another task' : 'names no task'}; nothing was written`, {
867
+ fix: `Re-read it with 'cleo show ${renamed.id}' and retry against ${renamed.id}.`,
868
+ details: { field: 'taskId', actual: taskId, expected: renamed.id },
869
+ });
870
+ }
222
871
  /** Throw `E_TASK_CLAIMED` when the stored holder refuses this claim write. */
223
872
  function assertClaimAllowed(taskId, row, guard, allowedHolders) {
224
873
  const stored = claimFromColumns(row);
@@ -528,8 +1177,18 @@ async function createOwnedSqliteDataAccessor(cwd, accessorScope) {
528
1177
  async upsertSingleTask(task) {
529
1178
  await accessor.transaction((tx) => tx.upsertSingleTask(task));
530
1179
  },
531
- async insertNewTask(task) {
532
- await accessor.transaction((tx) => tx.insertNewTask(task));
1180
+ async insertNewTask(task, identity) {
1181
+ await accessor.transaction((tx) => tx.insertNewTask(task, identity));
1182
+ },
1183
+ async getTaskIdentities(taskIds) {
1184
+ if (taskIds.length === 0)
1185
+ return [];
1186
+ const db = await getDb(cwd);
1187
+ return db
1188
+ .select({ id: schema.tasks.id, uid: schema.tasks.uid, birthFp: schema.tasks.birthFp })
1189
+ .from(schema.tasks)
1190
+ .where(inArray(schema.tasks.id, taskIds))
1191
+ .all();
533
1192
  },
534
1193
  async addRelation(taskId, relatedTo, relationType, reason) {
535
1194
  return accessor.transaction(async () => {
@@ -607,6 +1266,8 @@ async function createOwnedSqliteDataAccessor(cwd, accessorScope) {
607
1266
  createdAt: schema.taskAcceptanceCriteria.createdAt,
608
1267
  updatedAt: schema.taskAcceptanceCriteria.updatedAt,
609
1268
  contentHash: schema.taskAcceptanceCriteria.contentHash,
1269
+ uid: schema.taskAcceptanceCriteria.uid,
1270
+ birthFp: schema.taskAcceptanceCriteria.birthFp,
610
1271
  })
611
1272
  .from(schema.taskAcceptanceCriteria)
612
1273
  .where(eq(schema.taskAcceptanceCriteria.taskId, taskId))
@@ -624,31 +1285,20 @@ async function createOwnedSqliteDataAccessor(cwd, accessorScope) {
624
1285
  createdAt: r.createdAt,
625
1286
  updatedAt: r.updatedAt ?? null,
626
1287
  contentHash: r.contentHash ?? null,
1288
+ uid: r.uid ?? null,
1289
+ birthFp: r.birthFp ?? null,
627
1290
  }));
628
1291
  },
629
1292
  // ---- AC bindings (T10509 — AC-coverage gate) ----
630
1293
  async getAcBindings(acIds) {
631
1294
  if (acIds.length === 0)
632
1295
  return [];
1296
+ return selectAcBindings(await getDb(cwd), acIds);
1297
+ },
1298
+ // ---- AC bindings — orphan scan (T12790) ----
1299
+ async findOrphanAcBindings() {
633
1300
  const db = await getDb(cwd);
634
- const rows = await db
635
- .select({
636
- id: schema.evidenceAcBindings.id,
637
- evidenceAtomId: schema.evidenceAcBindings.evidenceAtomId,
638
- acId: schema.evidenceAcBindings.acId,
639
- bindingType: schema.evidenceAcBindings.bindingType,
640
- createdAt: schema.evidenceAcBindings.createdAt,
641
- })
642
- .from(schema.evidenceAcBindings)
643
- .where(inArray(schema.evidenceAcBindings.acId, acIds))
644
- .all();
645
- return rows.map((r) => ({
646
- id: r.id,
647
- evidenceAtomId: r.evidenceAtomId,
648
- acId: r.acId,
649
- bindingType: r.bindingType,
650
- createdAt: r.createdAt,
651
- }));
1301
+ return selectOrphanAcBindings(db);
652
1302
  },
653
1303
  async archiveSingleTask(taskId, fields) {
654
1304
  return accessor.transaction(async () => {
@@ -692,6 +1342,9 @@ async function createOwnedSqliteDataAccessor(cwd, accessorScope) {
692
1342
  .delete(schema.taskDependencies)
693
1343
  .where(eq(schema.taskDependencies.dependsOn, taskId))
694
1344
  .run();
1345
+ // T12790: the bindings table has no FK to the AC rows that cascade
1346
+ // from tasks_tasks — remove (and audit) them explicitly.
1347
+ await pruneAcBindingsForTask(db, taskId);
695
1348
  // Delete the task itself
696
1349
  await db.delete(schema.tasks).where(eq(schema.tasks.id, taskId)).run();
697
1350
  });
@@ -1113,6 +1766,11 @@ async function createOwnedSqliteDataAccessor(cwd, accessorScope) {
1113
1766
  .where(eq(schema.tasks.id, taskId))
1114
1767
  .limit(1)
1115
1768
  .all();
1769
+ // T12800: a guarded write aimed at a task that sync re-numbered must not
1770
+ // land on whatever row holds the old id now.
1771
+ if (guard?.claim || guard?.expectedUpdatedAt !== undefined) {
1772
+ assertNotRenamed(getNativeTasksDb(cwd), taskId, guard, current);
1773
+ }
1116
1774
  if (!current)
1117
1775
  throw new Error(`Task not found: ${taskId}`);
1118
1776
  // T12502: refuse a claim write the stored holder does not allow before
@@ -1195,24 +1853,42 @@ async function createOwnedSqliteDataAccessor(cwd, accessorScope) {
1195
1853
  nativeDb.prepare(`SAVEPOINT ${spName}`).run();
1196
1854
  }
1197
1855
  try {
1856
+ // The birth of each task an upsert replaced, for clearTaskIdentity
1857
+ // (T12806): sameness is decided on the birth before the overwrite.
1858
+ const replacedBirths = new Map();
1198
1859
  const tx = {
1199
1860
  async upsertSingleTask(task) {
1200
1861
  scope.assertActive();
1201
1862
  return accessor.transaction(async () => {
1202
1863
  scope.assertActive();
1864
+ if (rowUidFillEnabled() && !replacedBirths.has(task.id)) {
1865
+ const before = readReplacedTaskBirthNative(nativeDb, task.id);
1866
+ if (before)
1867
+ replacedBirths.set(task.id, before);
1868
+ }
1203
1869
  const row = taskToRow(task);
1204
1870
  await upsertTask(db, row);
1205
1871
  await updateDependencies(db, task.id, task.depends ?? []);
1206
1872
  });
1207
1873
  },
1208
- async insertNewTask(task) {
1874
+ async insertNewTask(task, identity) {
1209
1875
  scope.assertActive();
1210
1876
  return accessor.transaction(async () => {
1211
1877
  scope.assertActive();
1212
- await insertNewTask(db, taskToRow(task));
1878
+ await insertNewTask(db, taskToRow(task), identity);
1213
1879
  await updateDependencies(db, task.id, task.depends ?? []);
1214
1880
  });
1215
1881
  },
1882
+ async clearTaskIdentity(taskId) {
1883
+ scope.assertActive();
1884
+ // Row uids off: an overwrite changes no identity value (T12806 review).
1885
+ if (!rowUidFillEnabled())
1886
+ return;
1887
+ return accessor.transaction(async () => {
1888
+ scope.assertActive();
1889
+ clearReplacedTaskIdentityNative(nativeDb, taskId, replacedBirths.get(taskId));
1890
+ });
1891
+ },
1216
1892
  async archiveSingleTask(taskId, fields) {
1217
1893
  scope.assertActive();
1218
1894
  return accessor.transaction(async () => {
@@ -1242,6 +1918,8 @@ async function createOwnedSqliteDataAccessor(cwd, accessorScope) {
1242
1918
  .delete(schema.taskDependencies)
1243
1919
  .where(eq(schema.taskDependencies.dependsOn, taskId))
1244
1920
  .run();
1921
+ // T12790: bindings of the cascading AC rows (no FK).
1922
+ await pruneAcBindingsForTask(db, taskId);
1245
1923
  await db.delete(schema.tasks).where(eq(schema.tasks.id, taskId)).run();
1246
1924
  });
1247
1925
  },
@@ -1321,6 +1999,9 @@ async function createOwnedSqliteDataAccessor(cwd, accessorScope) {
1321
1999
  projection: r.projection ?? 'legacy',
1322
2000
  text: r.text,
1323
2001
  contentHash: r.contentHash ?? null,
2002
+ // A kept criterion keeps its uid (T12341); else one is minted.
2003
+ ...(r.uid ? { uid: r.uid } : {}),
2004
+ ...(r.uid && r.birthFp ? { birthFp: r.birthFp } : {}),
1324
2005
  })))
1325
2006
  .run();
1326
2007
  });
@@ -1340,6 +2021,8 @@ async function createOwnedSqliteDataAccessor(cwd, accessorScope) {
1340
2021
  createdAt: schema.taskAcceptanceCriteria.createdAt,
1341
2022
  updatedAt: schema.taskAcceptanceCriteria.updatedAt,
1342
2023
  contentHash: schema.taskAcceptanceCriteria.contentHash,
2024
+ uid: schema.taskAcceptanceCriteria.uid,
2025
+ birthFp: schema.taskAcceptanceCriteria.birthFp,
1343
2026
  })
1344
2027
  .from(schema.taskAcceptanceCriteria)
1345
2028
  .where(eq(schema.taskAcceptanceCriteria.taskId, taskId))
@@ -1357,18 +2040,70 @@ async function createOwnedSqliteDataAccessor(cwd, accessorScope) {
1357
2040
  createdAt: r.createdAt,
1358
2041
  updatedAt: r.updatedAt ?? null,
1359
2042
  contentHash: r.contentHash ?? null,
2043
+ uid: r.uid ?? null,
2044
+ birthFp: r.birthFp ?? null,
1360
2045
  }));
1361
2046
  },
1362
2047
  async deleteAcRowsForTask(taskId) {
1363
2048
  scope.assertActive();
1364
2049
  return accessor.transaction(async () => {
1365
2050
  scope.assertActive();
2051
+ // T12790: remove (and audit) the departing rows' bindings.
2052
+ await pruneAcBindingsForTask(db, taskId, 'ac-removed');
1366
2053
  await db
1367
2054
  .delete(schema.taskAcceptanceCriteria)
1368
2055
  .where(eq(schema.taskAcceptanceCriteria.taskId, taskId))
1369
2056
  .run();
1370
2057
  });
1371
2058
  },
2059
+ async deleteAcRowsByIds(taskId, ids, keepBindingsForUids = []) {
2060
+ scope.assertActive();
2061
+ if (ids.length === 0)
2062
+ return;
2063
+ return accessor.transaction(async () => {
2064
+ scope.assertActive();
2065
+ // T12790: only ids this task actually owns leave the set —
2066
+ // prune (and audit) exactly their bindings.
2067
+ const owned = await db
2068
+ .select({ id: schema.taskAcceptanceCriteria.id })
2069
+ .from(schema.taskAcceptanceCriteria)
2070
+ .where(and(eq(schema.taskAcceptanceCriteria.taskId, taskId), inArray(schema.taskAcceptanceCriteria.id, ids)))
2071
+ .all();
2072
+ await pruneAcBindingsForAcIds(db, taskId, owned.map((r) => r.id), 'ac-removed', keepBindingsForUids);
2073
+ await db
2074
+ .delete(schema.taskAcceptanceCriteria)
2075
+ .where(and(eq(schema.taskAcceptanceCriteria.taskId, taskId), inArray(schema.taskAcceptanceCriteria.id, ids)))
2076
+ .run();
2077
+ });
2078
+ },
2079
+ async updateAcRows(rows) {
2080
+ scope.assertActive();
2081
+ if (rows.length === 0)
2082
+ return;
2083
+ return accessor.transaction(async () => {
2084
+ scope.assertActive();
2085
+ const updatedAt = new Date().toISOString();
2086
+ for (const r of rows) {
2087
+ const result = await db
2088
+ .update(schema.taskAcceptanceCriteria)
2089
+ .set({
2090
+ ordinal: r.ordinal,
2091
+ kind: r.kind ?? 'text',
2092
+ sourceKey: r.sourceKey ?? `text:${r.ordinal}`,
2093
+ targetTaskId: r.targetTaskId ?? null,
2094
+ projection: r.projection ?? 'legacy',
2095
+ text: r.text,
2096
+ contentHash: r.contentHash ?? null,
2097
+ updatedAt,
2098
+ })
2099
+ .where(and(eq(schema.taskAcceptanceCriteria.id, r.id), eq(schema.taskAcceptanceCriteria.taskId, r.taskId)))
2100
+ .run();
2101
+ if (Number(result.changes) === 0) {
2102
+ throw new CleoError(ExitCode.VALIDATION_ERROR, `Acceptance criterion ${r.id} does not exist on task ${r.taskId}; refusing to update it`);
2103
+ }
2104
+ }
2105
+ });
2106
+ },
1372
2107
  async appendAcHistory(rows) {
1373
2108
  scope.assertActive();
1374
2109
  return accessor.transaction(async () => {
@@ -1381,6 +2116,7 @@ async function createOwnedSqliteDataAccessor(cwd, accessorScope) {
1381
2116
  acId: r.acId,
1382
2117
  previousText: r.previousText,
1383
2118
  reason: r.reason,
2119
+ ...(r.acUid ? { acUid: r.acUid } : {}),
1384
2120
  })))
1385
2121
  .run();
1386
2122
  });
@@ -1388,26 +2124,7 @@ async function createOwnedSqliteDataAccessor(cwd, accessorScope) {
1388
2124
  // ---- AC bindings (T10509 — AC-coverage gate) ----
1389
2125
  async getAcBindings(acIds) {
1390
2126
  scope.assertActive();
1391
- if (acIds.length === 0)
1392
- return [];
1393
- const out = await db
1394
- .select({
1395
- id: schema.evidenceAcBindings.id,
1396
- evidenceAtomId: schema.evidenceAcBindings.evidenceAtomId,
1397
- acId: schema.evidenceAcBindings.acId,
1398
- bindingType: schema.evidenceAcBindings.bindingType,
1399
- createdAt: schema.evidenceAcBindings.createdAt,
1400
- })
1401
- .from(schema.evidenceAcBindings)
1402
- .where(inArray(schema.evidenceAcBindings.acId, acIds))
1403
- .all();
1404
- return out.map((r) => ({
1405
- id: r.id,
1406
- evidenceAtomId: r.evidenceAtomId,
1407
- acId: r.acId,
1408
- bindingType: r.bindingType,
1409
- createdAt: r.createdAt,
1410
- }));
2127
+ return selectAcBindings(db, acIds);
1411
2128
  },
1412
2129
  // ---- AC bindings — writer (T10511, Validator SDK tools) ----
1413
2130
  async insertAcBindings(rows) {
@@ -1436,6 +2153,14 @@ async function createOwnedSqliteDataAccessor(cwd, accessorScope) {
1436
2153
  .run();
1437
2154
  });
1438
2155
  },
2156
+ // ---- AC bindings — orphan repair (T12790) ----
2157
+ async pruneOrphanAcBindings() {
2158
+ scope.assertActive();
2159
+ return accessor.transaction(async () => {
2160
+ scope.assertActive();
2161
+ return pruneOrphanAcBindings(db);
2162
+ });
2163
+ },
1439
2164
  };
1440
2165
  const result = await fn(bindTaskAccessorScope(tx, accessorScope));
1441
2166
  await scope.pending;
@@ -1624,4 +2349,12 @@ async function createOwnedSqliteDataAccessor(cwd, accessorScope) {
1624
2349
  };
1625
2350
  return accessor;
1626
2351
  }
2352
+ // T12341: the row-identity open pass writes through these chokepoint helpers.
2353
+ registerRowIdentityWriters({
2354
+ relinkAcUidNative,
2355
+ clearAcUidGraveyardNative,
2356
+ writeRowIdentityMetaNative,
2357
+ fillIdentityColumnNative,
2358
+ clearBirthFpNative,
2359
+ });
1627
2360
  //# sourceMappingURL=sqlite-data-accessor.js.map