@cleocode/core 2026.9.25 → 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 (208) 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/build-provenance.json +2 -2
  8. package/dist/cloud/index.d.ts +1 -0
  9. package/dist/cloud/index.d.ts.map +1 -1
  10. package/dist/cloud/index.js +1 -0
  11. package/dist/cloud/index.js.map +1 -1
  12. package/dist/cloud/nexus-auth.d.ts +26 -1
  13. package/dist/cloud/nexus-auth.d.ts.map +1 -1
  14. package/dist/cloud/nexus-auth.js +61 -32
  15. package/dist/cloud/nexus-auth.js.map +1 -1
  16. package/dist/cloud/nexus-credentials.js +2 -2
  17. package/dist/cloud/nexus-credentials.js.map +1 -1
  18. package/dist/cloud/nexus-device.d.ts +809 -0
  19. package/dist/cloud/nexus-device.d.ts.map +1 -0
  20. package/dist/cloud/nexus-device.js +1680 -0
  21. package/dist/cloud/nexus-device.js.map +1 -0
  22. package/dist/cloud/nexus-enrol.d.ts +210 -0
  23. package/dist/cloud/nexus-enrol.d.ts.map +1 -0
  24. package/dist/cloud/nexus-enrol.js +1023 -0
  25. package/dist/cloud/nexus-enrol.js.map +1 -0
  26. package/dist/cloud/nexus-link.d.ts +3 -0
  27. package/dist/cloud/nexus-link.d.ts.map +1 -1
  28. package/dist/cloud/nexus-link.js +38 -4
  29. package/dist/cloud/nexus-link.js.map +1 -1
  30. package/dist/cloud/signing.d.ts +20 -0
  31. package/dist/cloud/signing.d.ts.map +1 -1
  32. package/dist/cloud/signing.js +14 -0
  33. package/dist/cloud/signing.js.map +1 -1
  34. package/dist/crypto/credentials.d.ts +61 -0
  35. package/dist/crypto/credentials.d.ts.map +1 -1
  36. package/dist/crypto/credentials.js +154 -58
  37. package/dist/crypto/credentials.js.map +1 -1
  38. package/dist/docs/export-document.js +830 -190
  39. package/dist/docs/export-document.js.map +3 -3
  40. package/dist/doctor/dependency-cycles.d.ts +41 -0
  41. package/dist/doctor/dependency-cycles.d.ts.map +1 -0
  42. package/dist/doctor/dependency-cycles.js +39 -0
  43. package/dist/doctor/dependency-cycles.js.map +1 -0
  44. package/dist/doctor/index.d.ts +2 -0
  45. package/dist/doctor/index.d.ts.map +1 -1
  46. package/dist/doctor/index.js +1 -0
  47. package/dist/doctor/index.js.map +1 -1
  48. package/dist/doctor/row-identity.d.ts +11 -0
  49. package/dist/doctor/row-identity.d.ts.map +1 -1
  50. package/dist/doctor/row-identity.js +67 -9
  51. package/dist/doctor/row-identity.js.map +1 -1
  52. package/dist/error-catalog.d.ts.map +1 -1
  53. package/dist/error-catalog.js +4 -0
  54. package/dist/error-catalog.js.map +1 -1
  55. package/dist/error-registry.d.ts.map +1 -1
  56. package/dist/error-registry.js +8 -0
  57. package/dist/error-registry.js.map +1 -1
  58. package/dist/llm/plugin-facade.js +857 -217
  59. package/dist/llm/plugin-facade.js.map +3 -3
  60. package/dist/nexus/workspace.d.ts.map +1 -1
  61. package/dist/nexus/workspace.js +54 -4
  62. package/dist/nexus/workspace.js.map +1 -1
  63. package/dist/orchestrate/lifecycle-ops.js +1 -1
  64. package/dist/orchestrate/lifecycle-ops.js.map +1 -1
  65. package/dist/orchestrate/pivot.d.ts.map +1 -1
  66. package/dist/orchestrate/pivot.js +4 -2
  67. package/dist/orchestrate/pivot.js.map +1 -1
  68. package/dist/orchestrate/query-ops.d.ts.map +1 -1
  69. package/dist/orchestrate/query-ops.js +3 -3
  70. package/dist/orchestrate/query-ops.js.map +1 -1
  71. package/dist/orchestrate/spawn-ops.js +1 -1
  72. package/dist/orchestrate/spawn-ops.js.map +1 -1
  73. package/dist/orchestration/index.js +1 -1
  74. package/dist/orchestration/index.js.map +1 -1
  75. package/dist/orchestration/lead-rollup.d.ts.map +1 -1
  76. package/dist/orchestration/lead-rollup.js +3 -3
  77. package/dist/orchestration/lead-rollup.js.map +1 -1
  78. package/dist/orchestration/parallel.js +1 -1
  79. package/dist/orchestration/parallel.js.map +1 -1
  80. package/dist/orchestration/wave-topic.d.ts +4 -2
  81. package/dist/orchestration/wave-topic.d.ts.map +1 -1
  82. package/dist/orchestration/wave-topic.js +6 -4
  83. package/dist/orchestration/wave-topic.js.map +1 -1
  84. package/dist/orchestration/waves.d.ts +2 -1
  85. package/dist/orchestration/waves.d.ts.map +1 -1
  86. package/dist/orchestration/waves.js +4 -3
  87. package/dist/orchestration/waves.js.map +1 -1
  88. package/dist/phases/index.d.ts.map +1 -1
  89. package/dist/phases/index.js +4 -2
  90. package/dist/phases/index.js.map +1 -1
  91. package/dist/selfimprove/fix-gen.js +856 -216
  92. package/dist/selfimprove/fix-gen.js.map +3 -3
  93. package/dist/session/engine-ops.d.ts.map +1 -1
  94. package/dist/session/engine-ops.js +77 -49
  95. package/dist/session/engine-ops.js.map +1 -1
  96. package/dist/sessions/focus-state-store.d.ts +69 -23
  97. package/dist/sessions/focus-state-store.d.ts.map +1 -1
  98. package/dist/sessions/focus-state-store.js +113 -27
  99. package/dist/sessions/focus-state-store.js.map +1 -1
  100. package/dist/sessions/handoff.js +1 -1
  101. package/dist/sessions/handoff.js.map +1 -1
  102. package/dist/sessions/index.d.ts.map +1 -1
  103. package/dist/sessions/index.js +24 -13
  104. package/dist/sessions/index.js.map +1 -1
  105. package/dist/sessions/terminal-identity.d.ts +165 -17
  106. package/dist/sessions/terminal-identity.d.ts.map +1 -1
  107. package/dist/sessions/terminal-identity.js +341 -32
  108. package/dist/sessions/terminal-identity.js.map +1 -1
  109. package/dist/skills/orchestrator/startup.js +2 -2
  110. package/dist/skills/orchestrator/startup.js.map +1 -1
  111. package/dist/snapshot/index.d.ts +4 -0
  112. package/dist/snapshot/index.d.ts.map +1 -1
  113. package/dist/snapshot/index.js +50 -3
  114. package/dist/snapshot/index.js.map +1 -1
  115. package/dist/store/db-helpers.d.ts +15 -2
  116. package/dist/store/db-helpers.d.ts.map +1 -1
  117. package/dist/store/db-helpers.js +84 -3
  118. package/dist/store/db-helpers.js.map +1 -1
  119. package/dist/store/dependency-cycles.d.ts +164 -0
  120. package/dist/store/dependency-cycles.d.ts.map +1 -0
  121. package/dist/store/dependency-cycles.js +360 -0
  122. package/dist/store/dependency-cycles.js.map +1 -0
  123. package/dist/store/display-id-alias.d.ts +93 -37
  124. package/dist/store/display-id-alias.d.ts.map +1 -1
  125. package/dist/store/display-id-alias.js +590 -115
  126. package/dist/store/display-id-alias.js.map +1 -1
  127. package/dist/store/dual-scope-db.d.ts.map +1 -1
  128. package/dist/store/dual-scope-db.js +34 -7
  129. package/dist/store/dual-scope-db.js.map +1 -1
  130. package/dist/store/exodus/migrate.d.ts.map +1 -1
  131. package/dist/store/exodus/migrate.js +8 -1
  132. package/dist/store/exodus/migrate.js.map +1 -1
  133. package/dist/store/global-salt.d.ts +35 -0
  134. package/dist/store/global-salt.d.ts.map +1 -1
  135. package/dist/store/global-salt.js +136 -16
  136. package/dist/store/global-salt.js.map +1 -1
  137. package/dist/store/migration-sqlite.d.ts.map +1 -1
  138. package/dist/store/migration-sqlite.js +19 -11
  139. package/dist/store/migration-sqlite.js.map +1 -1
  140. package/dist/store/portable-bundle-scan.d.ts.map +1 -1
  141. package/dist/store/portable-bundle-scan.js +6 -0
  142. package/dist/store/portable-bundle-scan.js.map +1 -1
  143. package/dist/store/row-identity-registry.d.ts.map +1 -1
  144. package/dist/store/row-identity-registry.js +11 -3
  145. package/dist/store/row-identity-registry.js.map +1 -1
  146. package/dist/store/row-identity.d.ts +91 -1
  147. package/dist/store/row-identity.d.ts.map +1 -1
  148. package/dist/store/row-identity.js +276 -7
  149. package/dist/store/row-identity.js.map +1 -1
  150. package/dist/store/safety-data-accessor.d.ts +3 -2
  151. package/dist/store/safety-data-accessor.d.ts.map +1 -1
  152. package/dist/store/safety-data-accessor.js +5 -2
  153. package/dist/store/safety-data-accessor.js.map +1 -1
  154. package/dist/store/session-store.d.ts +63 -3
  155. package/dist/store/session-store.d.ts.map +1 -1
  156. package/dist/store/session-store.js +121 -8
  157. package/dist/store/session-store.js.map +1 -1
  158. package/dist/store/split-brain-import.d.ts.map +1 -1
  159. package/dist/store/split-brain-import.js +36 -0
  160. package/dist/store/split-brain-import.js.map +1 -1
  161. package/dist/store/sqlite-data-accessor.d.ts +50 -3
  162. package/dist/store/sqlite-data-accessor.d.ts.map +1 -1
  163. package/dist/store/sqlite-data-accessor.js +246 -13
  164. package/dist/store/sqlite-data-accessor.js.map +1 -1
  165. package/dist/store/tasks-sqlite.d.ts.map +1 -1
  166. package/dist/store/tasks-sqlite.js +14 -2
  167. package/dist/store/tasks-sqlite.js.map +1 -1
  168. package/dist/store/umbrella-data-accessor.d.ts +3 -2
  169. package/dist/store/umbrella-data-accessor.d.ts.map +1 -1
  170. package/dist/store/umbrella-data-accessor.js +5 -2
  171. package/dist/store/umbrella-data-accessor.js.map +1 -1
  172. package/dist/system/health.d.ts +2 -0
  173. package/dist/system/health.d.ts.map +1 -1
  174. package/dist/system/health.js +54 -0
  175. package/dist/system/health.js.map +1 -1
  176. package/dist/task-work/index.d.ts.map +1 -1
  177. package/dist/task-work/index.js +9 -6
  178. package/dist/task-work/index.js.map +1 -1
  179. package/dist/tasks/analyze.d.ts.map +1 -1
  180. package/dist/tasks/analyze.js +3 -2
  181. package/dist/tasks/analyze.js.map +1 -1
  182. package/dist/tasks/evidence.d.ts +19 -1
  183. package/dist/tasks/evidence.d.ts.map +1 -1
  184. package/dist/tasks/evidence.js +79 -18
  185. package/dist/tasks/evidence.js.map +1 -1
  186. package/dist/tasks/infer-add-params.js +1 -1
  187. package/dist/tasks/infer-add-params.js.map +1 -1
  188. package/dist/tasks/task-context.d.ts.map +1 -1
  189. package/dist/tasks/task-context.js +3 -1
  190. package/dist/tasks/task-context.js.map +1 -1
  191. package/dist/tasks/task-import.d.ts.map +1 -1
  192. package/dist/tasks/task-import.js +11 -3
  193. package/dist/tasks/task-import.js.map +1 -1
  194. package/dist/tasks/task-next.d.ts +5 -1
  195. package/dist/tasks/task-next.d.ts.map +1 -1
  196. package/dist/tasks/task-next.js +7 -2
  197. package/dist/tasks/task-next.js.map +1 -1
  198. package/dist/validation/engine-ops.d.ts.map +1 -1
  199. package/dist/validation/engine-ops.js +3 -2
  200. package/dist/validation/engine-ops.js.map +1 -1
  201. package/dist/workgraph/scaffold-apply.d.ts.map +1 -1
  202. package/dist/workgraph/scaffold-apply.js +14 -5
  203. package/dist/workgraph/scaffold-apply.js.map +1 -1
  204. package/migrations/drizzle-cleo-project/20260930160000_t12886-dependency-cycle-guard/migration.sql +76 -0
  205. package/migrations/drizzle-cleo-project/20260930160000_t12886-dependency-cycle-guard/revert.sql +7 -0
  206. package/package.json +13 -13
  207. package/templates/CLEO-INJECTION.md +2 -2
  208. package/templates/CLEO-REFERENCE.md +1 -1
@@ -22,13 +22,15 @@ import { withBackgroundOpCommitBoundary } from './background-ops.js';
22
22
  import { archivedTaskToRow, rowToSession, rowToTask, taskToRow } from './converters.js';
23
23
  import { cleanupBrainRefsOnSessionDelete } from './cross-db-cleanup.js';
24
24
  import { batchUpdateDependencies, insertNewTask, loadDependenciesForTasks, loadRelationsForTasks, parseLabels, updateDependencies, updateTaskLabels, upsertSession, upsertTask, } from './db-helpers.js';
25
- import { registerRowIdentityWriters } from './row-identity.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';
26
28
  import { tasksAuditLog } from './schema/cleo-project/audit.js';
27
29
  import { resolveCurrentSession } from './session-store.js';
28
30
  import { closeDb, getDb, getNativeTasksDb } from './sqlite.js';
29
31
  import { TERMINAL_TASK_STATUSES } from './status-registry.js';
30
32
  import { claimAllows, claimColumnsOf, claimFromColumns, isClaimExpired, leaseExpiresAtFrom, taskClaimedError, taskClaimLeaseMs, } from './task-claim.js';
31
- import { nextTaskVersion, taskConflictError, taskVersion } from './task-version.js';
33
+ import { nextTaskVersion, taskConflictError, taskVersion, } from './task-version.js';
32
34
  import * as schema from './tasks-schema.js';
33
35
  import { assertTwinCollapseWritable, bareCounterOf, mirrorCounterToBare } from './twin-collapse.js';
34
36
  import { runHeartbeatWrite, withWriteRetry } from './with-retry.js';
@@ -234,6 +236,7 @@ export function renameTaskDisplayIdNative(nativeDb, uid, toId, refs, guard = {})
234
236
  if (Number(changed) !== 1) {
235
237
  throw taskConflictError(current.id, taskVersion(current), null);
236
238
  }
239
+ recordLocalRename(nativeDb, uid, current.id, toId, taskVersion(current));
237
240
  const rewritten = {};
238
241
  const q = (name) => `"${name.replaceAll('"', '""')}"`;
239
242
  for (const ref of refs) {
@@ -311,19 +314,42 @@ export function rederiveAcIdsNative(nativeDb, fromId, toId) {
311
314
  return count;
312
315
  }
313
316
  /**
314
- * Insert a display-id alias row (idempotent on its uid).
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).
315
323
  *
316
324
  * @param nativeDb - The project `cleo.db` handle.
317
325
  * @param row - The alias.
326
+ * @param precedes - Whether `incoming` ranks before the `stored` row of the same uid.
318
327
  * @task T12341
319
328
  */
320
- export function insertDisplayIdAliasNative(nativeDb, row) {
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;
321
347
  nativeDb
322
- .prepare(`INSERT OR IGNORE INTO tasks_display_id_aliases
323
- (uid, entity_table, display_id, entity_uid, reason, origin, displaced_hlc, created_at,
324
- entity_birth_fp)
325
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`)
326
- .run(row.uid, row.entityTable, row.displayId, row.entityUid, row.reason, row.origin, row.displacedHlc, row.createdAt, row.entityBirthFp);
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);
327
353
  }
328
354
  /**
329
355
  * Delete one display-id alias row (a local alias whose entity was re-keyed is
@@ -420,6 +446,103 @@ export function relinkAcUidNative(nativeDb, acId, uid, birthFp) {
420
446
  export function clearAcUidGraveyardNative(nativeDb) {
421
447
  nativeDb.exec('DELETE FROM tasks_ac_uid_graveyard');
422
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
+ }
423
546
  /** Quote an identifier for the dynamic row-identity writes below. */
424
547
  function quoteIdent(name) {
425
548
  return `"${name.replaceAll('"', '""')}"`;
@@ -668,6 +791,83 @@ function claimPredicate(guard, allowedHolders) {
668
791
  : undefined;
669
792
  return or(isNull(holder), mine, related, expired);
670
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
+ }
671
871
  /** Throw `E_TASK_CLAIMED` when the stored holder refuses this claim write. */
672
872
  function assertClaimAllowed(taskId, row, guard, allowedHolders) {
673
873
  const stored = claimFromColumns(row);
@@ -977,8 +1177,18 @@ async function createOwnedSqliteDataAccessor(cwd, accessorScope) {
977
1177
  async upsertSingleTask(task) {
978
1178
  await accessor.transaction((tx) => tx.upsertSingleTask(task));
979
1179
  },
980
- async insertNewTask(task) {
981
- 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();
982
1192
  },
983
1193
  async addRelation(taskId, relatedTo, relationType, reason) {
984
1194
  return accessor.transaction(async () => {
@@ -1556,6 +1766,11 @@ async function createOwnedSqliteDataAccessor(cwd, accessorScope) {
1556
1766
  .where(eq(schema.tasks.id, taskId))
1557
1767
  .limit(1)
1558
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
+ }
1559
1774
  if (!current)
1560
1775
  throw new Error(`Task not found: ${taskId}`);
1561
1776
  // T12502: refuse a claim write the stored holder does not allow before
@@ -1638,24 +1853,42 @@ async function createOwnedSqliteDataAccessor(cwd, accessorScope) {
1638
1853
  nativeDb.prepare(`SAVEPOINT ${spName}`).run();
1639
1854
  }
1640
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();
1641
1859
  const tx = {
1642
1860
  async upsertSingleTask(task) {
1643
1861
  scope.assertActive();
1644
1862
  return accessor.transaction(async () => {
1645
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
+ }
1646
1869
  const row = taskToRow(task);
1647
1870
  await upsertTask(db, row);
1648
1871
  await updateDependencies(db, task.id, task.depends ?? []);
1649
1872
  });
1650
1873
  },
1651
- async insertNewTask(task) {
1874
+ async insertNewTask(task, identity) {
1652
1875
  scope.assertActive();
1653
1876
  return accessor.transaction(async () => {
1654
1877
  scope.assertActive();
1655
- await insertNewTask(db, taskToRow(task));
1878
+ await insertNewTask(db, taskToRow(task), identity);
1656
1879
  await updateDependencies(db, task.id, task.depends ?? []);
1657
1880
  });
1658
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
+ },
1659
1892
  async archiveSingleTask(taskId, fields) {
1660
1893
  scope.assertActive();
1661
1894
  return accessor.transaction(async () => {