@cleocode/core 2026.9.21 → 2026.9.22

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 (244) hide show
  1. package/dist/agents/scaffold.d.ts.map +1 -1
  2. package/dist/agents/scaffold.js +0 -1
  3. package/dist/agents/scaffold.js.map +1 -1
  4. package/dist/bootstrap.d.ts.map +1 -1
  5. package/dist/bootstrap.js +4 -0
  6. package/dist/bootstrap.js.map +1 -1
  7. package/dist/dispatch/mutate-projection.d.ts.map +1 -1
  8. package/dist/dispatch/mutate-projection.js +5 -0
  9. package/dist/dispatch/mutate-projection.js.map +1 -1
  10. package/dist/docs/docs-update.d.ts.map +1 -1
  11. package/dist/docs/docs-update.js +25 -9
  12. package/dist/docs/docs-update.js.map +1 -1
  13. package/dist/docs/export-document.js +323 -238
  14. package/dist/docs/export-document.js.map +3 -3
  15. package/dist/doctor/knowledge.d.ts +16 -1
  16. package/dist/doctor/knowledge.d.ts.map +1 -1
  17. package/dist/doctor/knowledge.js +53 -17
  18. package/dist/doctor/knowledge.js.map +1 -1
  19. package/dist/engine/engine-ops.d.ts +36 -6
  20. package/dist/engine/engine-ops.d.ts.map +1 -1
  21. package/dist/engine/engine-ops.js +115 -14
  22. package/dist/engine/engine-ops.js.map +1 -1
  23. package/dist/gateway-client/generated/types.gen.d.ts +1 -1
  24. package/dist/index.d.ts +2 -0
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +1 -0
  27. package/dist/index.js.map +1 -1
  28. package/dist/init.d.ts.map +1 -1
  29. package/dist/init.js +51 -11
  30. package/dist/init.js.map +1 -1
  31. package/dist/inject/index.d.ts.map +1 -1
  32. package/dist/inject/index.js +8 -3
  33. package/dist/inject/index.js.map +1 -1
  34. package/dist/internal.d.ts +2 -2
  35. package/dist/internal.d.ts.map +1 -1
  36. package/dist/internal.js +1 -1
  37. package/dist/internal.js.map +1 -1
  38. package/dist/llm/plugin-facade.js +323 -238
  39. package/dist/llm/plugin-facade.js.map +3 -3
  40. package/dist/memory/attention.d.ts.map +1 -1
  41. package/dist/memory/attention.js +2 -2
  42. package/dist/memory/attention.js.map +1 -1
  43. package/dist/nexus/analyze-orchestrator.d.ts +38 -0
  44. package/dist/nexus/analyze-orchestrator.d.ts.map +1 -1
  45. package/dist/nexus/analyze-orchestrator.js +96 -21
  46. package/dist/nexus/analyze-orchestrator.js.map +1 -1
  47. package/dist/nexus/knowledge.d.ts.map +1 -1
  48. package/dist/nexus/knowledge.js +15 -4
  49. package/dist/nexus/knowledge.js.map +1 -1
  50. package/dist/nexus/stored-roots.d.ts +65 -0
  51. package/dist/nexus/stored-roots.d.ts.map +1 -1
  52. package/dist/nexus/stored-roots.js +73 -0
  53. package/dist/nexus/stored-roots.js.map +1 -1
  54. package/dist/orchestrate/lifecycle-ops.d.ts.map +1 -1
  55. package/dist/orchestrate/lifecycle-ops.js +4 -1
  56. package/dist/orchestrate/lifecycle-ops.js.map +1 -1
  57. package/dist/orchestrate/plan.d.ts.map +1 -1
  58. package/dist/orchestrate/plan.js +5 -2
  59. package/dist/orchestrate/plan.js.map +1 -1
  60. package/dist/orchestrate/query-ops.d.ts +7 -2
  61. package/dist/orchestrate/query-ops.d.ts.map +1 -1
  62. package/dist/orchestrate/query-ops.js +17 -6
  63. package/dist/orchestrate/query-ops.js.map +1 -1
  64. package/dist/orchestrate/spawn-ops.d.ts.map +1 -1
  65. package/dist/orchestrate/spawn-ops.js +5 -14
  66. package/dist/orchestrate/spawn-ops.js.map +1 -1
  67. package/dist/orchestration/bootstrap.d.ts.map +1 -1
  68. package/dist/orchestration/bootstrap.js +4 -2
  69. package/dist/orchestration/bootstrap.js.map +1 -1
  70. package/dist/orchestration/index.d.ts +1 -0
  71. package/dist/orchestration/index.d.ts.map +1 -1
  72. package/dist/orchestration/index.js +4 -3
  73. package/dist/orchestration/index.js.map +1 -1
  74. package/dist/orchestration/lead-rollup.d.ts +8 -5
  75. package/dist/orchestration/lead-rollup.d.ts.map +1 -1
  76. package/dist/orchestration/lead-rollup.js +32 -15
  77. package/dist/orchestration/lead-rollup.js.map +1 -1
  78. package/dist/orchestration/parallel.d.ts.map +1 -1
  79. package/dist/orchestration/parallel.js +10 -17
  80. package/dist/orchestration/parallel.js.map +1 -1
  81. package/dist/orchestration/spawn-prompt.d.ts +4 -1
  82. package/dist/orchestration/spawn-prompt.d.ts.map +1 -1
  83. package/dist/orchestration/spawn-prompt.js +3 -2
  84. package/dist/orchestration/spawn-prompt.js.map +1 -1
  85. package/dist/orchestration/status.d.ts +8 -3
  86. package/dist/orchestration/status.d.ts.map +1 -1
  87. package/dist/orchestration/status.js +9 -6
  88. package/dist/orchestration/status.js.map +1 -1
  89. package/dist/orchestration/wave-topic.d.ts +54 -0
  90. package/dist/orchestration/wave-topic.d.ts.map +1 -0
  91. package/dist/orchestration/wave-topic.js +68 -0
  92. package/dist/orchestration/wave-topic.js.map +1 -0
  93. package/dist/orchestration/waves.d.ts +47 -8
  94. package/dist/orchestration/waves.d.ts.map +1 -1
  95. package/dist/orchestration/waves.js +152 -43
  96. package/dist/orchestration/waves.js.map +1 -1
  97. package/dist/phases/index.d.ts.map +1 -1
  98. package/dist/phases/index.js +6 -2
  99. package/dist/phases/index.js.map +1 -1
  100. package/dist/project-scope.d.ts +18 -0
  101. package/dist/project-scope.d.ts.map +1 -1
  102. package/dist/project-scope.js +37 -0
  103. package/dist/project-scope.js.map +1 -1
  104. package/dist/release/ci-evidence.d.ts +10 -0
  105. package/dist/release/ci-evidence.d.ts.map +1 -1
  106. package/dist/release/ci-evidence.js +23 -7
  107. package/dist/release/ci-evidence.js.map +1 -1
  108. package/dist/release/github-pr.d.ts +30 -0
  109. package/dist/release/github-pr.d.ts.map +1 -1
  110. package/dist/release/github-pr.js +40 -0
  111. package/dist/release/github-pr.js.map +1 -1
  112. package/dist/release/plan.d.ts.map +1 -1
  113. package/dist/release/plan.js +6 -3
  114. package/dist/release/plan.js.map +1 -1
  115. package/dist/release/pr-evidence.d.ts.map +1 -1
  116. package/dist/release/pr-evidence.js +12 -4
  117. package/dist/release/pr-evidence.js.map +1 -1
  118. package/dist/security/input-sanitization.d.ts.map +1 -1
  119. package/dist/security/input-sanitization.js +9 -2
  120. package/dist/security/input-sanitization.js.map +1 -1
  121. package/dist/selfimprove/fix-gen.js +323 -238
  122. package/dist/selfimprove/fix-gen.js.map +3 -3
  123. package/dist/session/engine-ops.d.ts +6 -0
  124. package/dist/session/engine-ops.d.ts.map +1 -1
  125. package/dist/session/engine-ops.js +36 -4
  126. package/dist/session/engine-ops.js.map +1 -1
  127. package/dist/sessions/briefing.d.ts +17 -0
  128. package/dist/sessions/briefing.d.ts.map +1 -1
  129. package/dist/sessions/briefing.js +47 -8
  130. package/dist/sessions/briefing.js.map +1 -1
  131. package/dist/sessions/drift-watchdog.d.ts.map +1 -1
  132. package/dist/sessions/drift-watchdog.js +4 -3
  133. package/dist/sessions/drift-watchdog.js.map +1 -1
  134. package/dist/sessions/focus-state-store.d.ts +89 -1
  135. package/dist/sessions/focus-state-store.d.ts.map +1 -1
  136. package/dist/sessions/focus-state-store.js +97 -1
  137. package/dist/sessions/focus-state-store.js.map +1 -1
  138. package/dist/sessions/index.d.ts +2 -2
  139. package/dist/sessions/index.d.ts.map +1 -1
  140. package/dist/sessions/index.js +1 -1
  141. package/dist/sessions/index.js.map +1 -1
  142. package/dist/sessions/session-drift.d.ts.map +1 -1
  143. package/dist/sessions/session-drift.js +4 -3
  144. package/dist/sessions/session-drift.js.map +1 -1
  145. package/dist/skills/discovery.d.ts +12 -1
  146. package/dist/skills/discovery.d.ts.map +1 -1
  147. package/dist/skills/discovery.js +27 -2
  148. package/dist/skills/discovery.js.map +1 -1
  149. package/dist/skills/orchestrator/startup.d.ts.map +1 -1
  150. package/dist/skills/orchestrator/startup.js +10 -5
  151. package/dist/skills/orchestrator/startup.js.map +1 -1
  152. package/dist/skills/prune-bundled.d.ts +267 -0
  153. package/dist/skills/prune-bundled.d.ts.map +1 -0
  154. package/dist/skills/prune-bundled.js +536 -0
  155. package/dist/skills/prune-bundled.js.map +1 -0
  156. package/dist/skills/types.d.ts +9 -1
  157. package/dist/skills/types.d.ts.map +1 -1
  158. package/dist/skills/types.js +17 -34
  159. package/dist/skills/types.js.map +1 -1
  160. package/dist/spawn/branch-lock.d.ts.map +1 -1
  161. package/dist/spawn/branch-lock.js +26 -2
  162. package/dist/spawn/branch-lock.js.map +1 -1
  163. package/dist/stats/index.d.ts.map +1 -1
  164. package/dist/stats/index.js +4 -2
  165. package/dist/stats/index.js.map +1 -1
  166. package/dist/store/skills-db.d.ts +17 -0
  167. package/dist/store/skills-db.d.ts.map +1 -1
  168. package/dist/store/skills-db.js +30 -0
  169. package/dist/store/skills-db.js.map +1 -1
  170. package/dist/store/snapshot-gate.d.ts.map +1 -1
  171. package/dist/store/snapshot-gate.js +3 -0
  172. package/dist/store/snapshot-gate.js.map +1 -1
  173. package/dist/store/sqlite-data-accessor.d.ts.map +1 -1
  174. package/dist/store/sqlite-data-accessor.js +12 -2
  175. package/dist/store/sqlite-data-accessor.js.map +1 -1
  176. package/dist/store/sqlite.d.ts +14 -0
  177. package/dist/store/sqlite.d.ts.map +1 -1
  178. package/dist/store/sqlite.js +26 -4
  179. package/dist/store/sqlite.js.map +1 -1
  180. package/dist/store/twin-collapse.d.ts +46 -1
  181. package/dist/store/twin-collapse.d.ts.map +1 -1
  182. package/dist/store/twin-collapse.js +131 -5
  183. package/dist/store/twin-collapse.js.map +1 -1
  184. package/dist/store/worktree-isolation-guard.d.ts.map +1 -1
  185. package/dist/store/worktree-isolation-guard.js +4 -2
  186. package/dist/store/worktree-isolation-guard.js.map +1 -1
  187. package/dist/system/inject-generate.d.ts.map +1 -1
  188. package/dist/system/inject-generate.js +4 -4
  189. package/dist/system/inject-generate.js.map +1 -1
  190. package/dist/task-work/index.d.ts +6 -0
  191. package/dist/task-work/index.d.ts.map +1 -1
  192. package/dist/task-work/index.js +6 -3
  193. package/dist/task-work/index.js.map +1 -1
  194. package/dist/tasks/affected-packages.d.ts +57 -12
  195. package/dist/tasks/affected-packages.d.ts.map +1 -1
  196. package/dist/tasks/affected-packages.js +138 -31
  197. package/dist/tasks/affected-packages.js.map +1 -1
  198. package/dist/tasks/affected-scope.d.ts +59 -0
  199. package/dist/tasks/affected-scope.d.ts.map +1 -0
  200. package/dist/tasks/affected-scope.js +81 -0
  201. package/dist/tasks/affected-scope.js.map +1 -0
  202. package/dist/tasks/analyze.d.ts.map +1 -1
  203. package/dist/tasks/analyze.js +7 -2
  204. package/dist/tasks/analyze.js.map +1 -1
  205. package/dist/tasks/change-set.d.ts +24 -0
  206. package/dist/tasks/change-set.d.ts.map +1 -1
  207. package/dist/tasks/change-set.js +130 -11
  208. package/dist/tasks/change-set.js.map +1 -1
  209. package/dist/tasks/complete.d.ts +10 -0
  210. package/dist/tasks/complete.d.ts.map +1 -1
  211. package/dist/tasks/complete.js +49 -0
  212. package/dist/tasks/complete.js.map +1 -1
  213. package/dist/tasks/component-pr.d.ts +106 -0
  214. package/dist/tasks/component-pr.d.ts.map +1 -0
  215. package/dist/tasks/component-pr.js +190 -0
  216. package/dist/tasks/component-pr.js.map +1 -0
  217. package/dist/tasks/done-plan.d.ts +21 -1
  218. package/dist/tasks/done-plan.d.ts.map +1 -1
  219. package/dist/tasks/done-plan.js +170 -15
  220. package/dist/tasks/done-plan.js.map +1 -1
  221. package/dist/tasks/done-record.d.ts.map +1 -1
  222. package/dist/tasks/done-record.js +9 -80
  223. package/dist/tasks/done-record.js.map +1 -1
  224. package/dist/tasks/evidence.d.ts +47 -2
  225. package/dist/tasks/evidence.d.ts.map +1 -1
  226. package/dist/tasks/evidence.js +57 -7
  227. package/dist/tasks/evidence.js.map +1 -1
  228. package/dist/tasks/task-next.d.ts +4 -0
  229. package/dist/tasks/task-next.d.ts.map +1 -1
  230. package/dist/tasks/task-next.js +32 -28
  231. package/dist/tasks/task-next.js.map +1 -1
  232. package/dist/validation/engine-ops.d.ts +9 -1
  233. package/dist/validation/engine-ops.d.ts.map +1 -1
  234. package/dist/validation/engine-ops.js +24 -1
  235. package/dist/validation/engine-ops.js.map +1 -1
  236. package/dist/validation/validate-ops.d.ts.map +1 -1
  237. package/dist/validation/validate-ops.js +9 -3
  238. package/dist/validation/validate-ops.js.map +1 -1
  239. package/dist/worktree/worktree-adopt.d.ts.map +1 -1
  240. package/dist/worktree/worktree-adopt.js +5 -2
  241. package/dist/worktree/worktree-adopt.js.map +1 -1
  242. package/package.json +13 -13
  243. package/templates/CLEO-INJECTION.md +1 -1
  244. package/templates/CLEO-REFERENCE.md +1 -1
@@ -10509,7 +10509,7 @@ var init_operations_registry = __esm({
10509
10509
  type: "string",
10510
10510
  required: false,
10511
10511
  enum: DOCS_LIFECYCLE_STATUSES,
10512
- description: `Override the new lifecycle status. Defaults to "draft" on every update. Valid: ${DOCS_LIFECYCLE_STATUSES.join("|")}.`
10512
+ description: `Override the new lifecycle status. Defaults to "draft" on every update. Alone (no file or content) it changes only the lifecycle status. Valid: ${DOCS_LIFECYCLE_STATUSES.join("|")}.`
10513
10513
  },
10514
10514
  {
10515
10515
  name: "dryRun",
@@ -12996,11 +12996,13 @@ var init_evidence_atom_schema = __esm({
12996
12996
  });
12997
12997
  prAtomSchema = z10.object({
12998
12998
  kind: z10.literal("pr"),
12999
- prNumber: z10.number().int().positive("pr atom requires a positive integer PR number")
12999
+ prNumber: z10.number().int().positive("pr atom requires a positive integer PR number"),
13000
+ componentPrNumber: z10.number().int().positive().optional()
13000
13001
  });
13001
13002
  ciAtomSchema = z10.object({
13002
13003
  kind: z10.literal("ci"),
13003
- prNumber: z10.number().int().positive("ci atom requires a positive integer PR number")
13004
+ prNumber: z10.number().int().positive("ci atom requires a positive integer PR number"),
13005
+ componentPrNumber: z10.number().int().positive().optional()
13004
13006
  });
13005
13007
  locDropAtomSchema = z10.object({
13006
13008
  kind: z10.literal("loc-drop"),
@@ -18640,6 +18642,22 @@ function linkedWorktreeMainRoot(dir) {
18640
18642
  if (basename2(commonDir) === ".git") return dirname2(commonDir);
18641
18643
  return readCoreWorktree(commonDir);
18642
18644
  }
18645
+ function describeWorktreeOwner(dir) {
18646
+ const mainRoot = linkedWorktreeMainRoot(dir);
18647
+ if (mainRoot === null) return "owning repository unknown (unreadable gitlink)";
18648
+ let projectId = null;
18649
+ try {
18650
+ const parsed = JSON.parse(
18651
+ readFileSync2(join3(mainRoot, ".cleo", "project-info.json"), "utf-8")
18652
+ );
18653
+ if (typeof parsed === "object" && parsed !== null && "projectId" in parsed) {
18654
+ const value = parsed.projectId;
18655
+ if (typeof value === "string" && value !== "") projectId = value;
18656
+ }
18657
+ } catch {
18658
+ }
18659
+ return projectId === null ? `owning repository ${mainRoot} (not an initialised CLEO project)` : `owning project ${mainRoot} (projectId ${projectId})`;
18660
+ }
18643
18661
  function readCoreWorktree(gitDir) {
18644
18662
  try {
18645
18663
  const config = readFileSync2(join3(gitDir, "config"), "utf-8");
@@ -25748,9 +25766,10 @@ function assertStorePathIsNotWorktreeResident(role, dbPath) {
25748
25766
  );
25749
25767
  return;
25750
25768
  }
25769
+ const owner = describeWorktreeOwner(projectRoot);
25751
25770
  throw new CleoError(
25752
25771
  8 /* CONFIG_ERROR */,
25753
- `E_WT_DB_ISOLATION_VIOLATION: refusing to open '${role}' DB at ${dbPath} \u2014 parent ${projectRoot} is a git worktree (gitlink). DBs must open against the canonical project root.`,
25772
+ `E_WT_DB_ISOLATION_VIOLATION: refusing to open '${role}' DB at ${dbPath} \u2014 parent ${projectRoot} is a git worktree (gitlink); ${owner}. DBs must open against the canonical project root.`,
25754
25773
  {
25755
25774
  fix: `Run from the canonical project root, or make sure the worktree's main repository is an initialised CLEO project so it resolves there. Inspect stranded worktree stores with \`cleo doctor worktree-stores\`. Emergency override (audited): CLEO_ALLOW_WORKTREE_DB_CREATE=1.`
25756
25775
  }
@@ -32337,216 +32356,6 @@ var init_memory_schema = __esm({
32337
32356
  }
32338
32357
  });
32339
32358
 
32340
- // packages/core/src/store/snapshot-gate.ts
32341
- import { join as join20 } from "node:path";
32342
- function readGateState(db) {
32343
- const row = db.prepare("SELECT value FROM tasks_schema_meta WHERE key = ?").get(SNAPSHOT_GATE_META_KEY);
32344
- const state = { generation: 0, prefixes: {} };
32345
- if (!row) return state;
32346
- try {
32347
- const parsed = JSON.parse(row.value);
32348
- if (parsed === null || typeof parsed !== "object") return state;
32349
- if (typeof parsed.generation === "number" && Number.isSafeInteger(parsed.generation)) {
32350
- state.generation = parsed.generation;
32351
- }
32352
- for (const [prefix, e] of Object.entries(parsed.prefixes ?? {})) {
32353
- if (e && typeof e.generation === "number" && typeof e.startedAt === "number" && typeof e.completedAt === "number") {
32354
- state.prefixes[prefix] = {
32355
- generation: e.generation,
32356
- startedAt: e.startedAt,
32357
- completedAt: e.completedAt
32358
- };
32359
- }
32360
- }
32361
- } catch {
32362
- }
32363
- return state;
32364
- }
32365
- function writeGateState(db, state) {
32366
- db.prepare(
32367
- "INSERT INTO tasks_schema_meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value"
32368
- ).run(SNAPSHOT_GATE_META_KEY, JSON.stringify(state));
32369
- }
32370
- function tryReadGateState(db) {
32371
- if (!db) return null;
32372
- try {
32373
- return readGateState(db);
32374
- } catch {
32375
- return null;
32376
- }
32377
- }
32378
- function needsSnapshot(entry, mode, nowMs, seenGeneration) {
32379
- if (!entry) return true;
32380
- if (mode === "required") return seenGeneration === null || entry.generation <= seenGeneration;
32381
- const elapsed = nowMs - entry.startedAt;
32382
- return elapsed < 0 || elapsed >= SNAPSHOT_DEBOUNCE_MS;
32383
- }
32384
- async function runGatedSnapshot(opts, snapshot) {
32385
- const now = opts.now ?? Date.now;
32386
- const mode = opts.mode ?? "routine";
32387
- const db = opts.stateDb;
32388
- const none = (skipped, error) => ({
32389
- snapshotted: [],
32390
- linked: [],
32391
- absent: [],
32392
- failed: [],
32393
- skipped,
32394
- ...error !== void 0 && { error }
32395
- });
32396
- const entryState = tryReadGateState(db);
32397
- const seenGeneration = mode === "required" ? opts.seenGeneration ?? entryState?.generation ?? null : null;
32398
- if (mode === "routine") {
32399
- if (!entryState) return none("state-unavailable");
32400
- const nowMs = now();
32401
- if (!opts.prefixes.some((p) => needsSnapshot(entryState.prefixes[p], mode, nowMs, null))) {
32402
- return none("debounced");
32403
- }
32404
- }
32405
- const lockPath2 = join20(opts.backupDir, GATE_LOCK_BASENAME);
32406
- let release;
32407
- try {
32408
- release = await acquireLock(lockPath2, {
32409
- retries: mode === "required" ? opts.lockWaitRetries ?? SNAPSHOT_LOCK_WAIT_RETRIES : 0,
32410
- stale: SNAPSHOT_LOCK_STALE_MS,
32411
- // A compromised lock (e.g. the backup dir was removed) must not throw
32412
- // from a timer and crash the host process.
32413
- onCompromised: () => {
32414
- }
32415
- });
32416
- } catch {
32417
- if (mode === "routine") return none("in-flight");
32418
- return none(
32419
- "lock-timeout",
32420
- `Snapshot lock ${lockPath2}.lock is still held after waiting. Another snapshot is running, or a crashed process left the lock (it expires after ${SNAPSHOT_LOCK_STALE_MS / 6e4} minutes). Remove it if no snapshot is running.`
32421
- );
32422
- }
32423
- try {
32424
- try {
32425
- opts.onLockAcquired?.();
32426
- } catch {
32427
- }
32428
- const state = tryReadGateState(db);
32429
- if (!state && mode === "routine") return none("state-unavailable");
32430
- const startMs = now();
32431
- const admitted = mode === "required" && opts.alwaysSnapshot === true ? [...opts.prefixes] : opts.prefixes.filter(
32432
- (p) => needsSnapshot(state?.prefixes[p], mode, startMs, seenGeneration)
32433
- );
32434
- if (admitted.length === 0) return none(mode === "routine" ? "debounced" : "covered");
32435
- const generation = (state?.generation ?? 0) + 1;
32436
- let claimed = false;
32437
- if (db && state) {
32438
- try {
32439
- writeGateState(db, { ...state, generation });
32440
- claimed = true;
32441
- } catch {
32442
- }
32443
- }
32444
- const snapshotted = [];
32445
- const linked = [];
32446
- const absent = [];
32447
- const failed = [];
32448
- for (const prefix of admitted) {
32449
- try {
32450
- const outcome = await snapshot(prefix);
32451
- if (outcome === "absent") absent.push(prefix);
32452
- else {
32453
- snapshotted.push(prefix);
32454
- if (outcome === "linked") linked.push(prefix);
32455
- }
32456
- } catch {
32457
- failed.push(prefix);
32458
- }
32459
- }
32460
- const satisfied = [...snapshotted, ...absent];
32461
- if (db && claimed && satisfied.length > 0) {
32462
- try {
32463
- const latest = readGateState(db);
32464
- const completedAt = now();
32465
- latest.generation = Math.max(latest.generation, generation);
32466
- for (const p of satisfied) {
32467
- latest.prefixes[p] = { generation, startedAt: startMs, completedAt };
32468
- }
32469
- writeGateState(db, latest);
32470
- } catch {
32471
- }
32472
- }
32473
- return { snapshotted, linked, absent, failed, skipped: null };
32474
- } finally {
32475
- await release().catch(() => {
32476
- });
32477
- }
32478
- }
32479
- function isValidStamp(ymd, hms) {
32480
- const y = Number(ymd.slice(0, 4));
32481
- const mo = Number(ymd.slice(4, 6));
32482
- const d = Number(ymd.slice(6, 8));
32483
- const h = Number(hms.slice(0, 2));
32484
- const mi = Number(hms.slice(2, 4));
32485
- const se = Number(hms.slice(4, 6));
32486
- if (mo < 1 || mo > 12 || d < 1 || h > 23 || mi > 59 || se > 59) return false;
32487
- const probe = new Date(Date.UTC(y, mo - 1, d));
32488
- return probe.getUTCFullYear() === y && probe.getUTCMonth() === mo - 1 && probe.getUTCDate() === d;
32489
- }
32490
- function selectSnapshotsToKeep(names, ctx = {}) {
32491
- const policy = ctx.policy ?? DEFAULT_SNAPSHOT_RETENTION;
32492
- const keep = /* @__PURE__ */ new Set();
32493
- const stamped = [];
32494
- for (const name2 of names) {
32495
- const m = STAMP_PATTERN.exec(name2);
32496
- const ymd = m?.[1];
32497
- const hms = m?.[2];
32498
- if (!ymd || !hms || !isValidStamp(ymd, hms)) {
32499
- keep.add(name2);
32500
- continue;
32501
- }
32502
- const stamp = `${ymd}-${hms}`;
32503
- if (ctx.nowStamp !== void 0 && stamp > ctx.nowStamp) {
32504
- keep.add(name2);
32505
- continue;
32506
- }
32507
- stamped.push({ name: name2, stamp });
32508
- }
32509
- if (ctx.pinned !== void 0 && names.includes(ctx.pinned)) keep.add(ctx.pinned);
32510
- stamped.sort((a, b) => a.stamp < b.stamp ? 1 : a.stamp > b.stamp ? -1 : 0);
32511
- for (const s of stamped.slice(0, policy.latest)) keep.add(s.name);
32512
- const keepNewestPerBucket = (bucketOf, slots) => {
32513
- const seen = /* @__PURE__ */ new Set();
32514
- for (const s of stamped) {
32515
- const bucket = bucketOf(s.stamp);
32516
- if (seen.has(bucket)) continue;
32517
- if (seen.size >= slots) break;
32518
- seen.add(bucket);
32519
- keep.add(s.name);
32520
- }
32521
- };
32522
- keepNewestPerBucket(
32523
- (stamp) => `${stamp.slice(0, 11)}q${Math.floor(Number(stamp.slice(11, 13)) / 15)}`,
32524
- policy.quarterHourly
32525
- );
32526
- keepNewestPerBucket((stamp) => stamp.slice(0, 11), policy.hourly);
32527
- keepNewestPerBucket((stamp) => stamp.slice(0, 8), policy.daily);
32528
- return keep;
32529
- }
32530
- var SNAPSHOT_DEBOUNCE_MS, SNAPSHOT_LOCK_STALE_MS, SNAPSHOT_LOCK_WAIT_RETRIES, SNAPSHOT_GATE_META_KEY, GATE_LOCK_BASENAME, DEFAULT_SNAPSHOT_RETENTION, STAMP_PATTERN;
32531
- var init_snapshot_gate = __esm({
32532
- "packages/core/src/store/snapshot-gate.ts"() {
32533
- "use strict";
32534
- init_lock();
32535
- SNAPSHOT_DEBOUNCE_MS = 5 * 6e4;
32536
- SNAPSHOT_LOCK_STALE_MS = 10 * 6e4;
32537
- SNAPSHOT_LOCK_WAIT_RETRIES = 90;
32538
- SNAPSHOT_GATE_META_KEY = "sqlite_snapshot_gate";
32539
- GATE_LOCK_BASENAME = ".snapshot-gate";
32540
- DEFAULT_SNAPSHOT_RETENTION = {
32541
- latest: 1,
32542
- quarterHourly: 3,
32543
- hourly: 3,
32544
- daily: 3
32545
- };
32546
- STAMP_PATTERN = /-(\d{8})-(\d{6})\.db$/;
32547
- }
32548
- });
32549
-
32550
32359
  // packages/core/src/store/twin-collapse.ts
32551
32360
  import { createHash as createHash3 } from "node:crypto";
32552
32361
  function hasMainTable(db, name2) {
@@ -32623,6 +32432,22 @@ function mergeSchemaMetaValue(key, bare, twin) {
32623
32432
  const field = SCHEMA_META_COUNTER_FIELDS[key];
32624
32433
  return field === void 0 ? bare : withMaxCounter(bare, twin, field);
32625
32434
  }
32435
+ function mirrorCounterToBare(db, key) {
32436
+ if (SCHEMA_META_COUNTER_FIELDS[key] === void 0) return;
32437
+ if (!hasMainTable(db, "schema_meta")) return;
32438
+ const twin = readKv(db, "tasks_schema_meta", key);
32439
+ if (twin === void 0) return;
32440
+ const bare = readKv(db, "schema_meta", key);
32441
+ const next = bare === void 0 ? twin : mergeSchemaMetaValue(key, bare, twin);
32442
+ if (next === null || next === bare) return;
32443
+ writeKv(db, "schema_meta", key, next);
32444
+ }
32445
+ function bareCounterOf(db, key) {
32446
+ const name2 = SCHEMA_META_COUNTER_FIELDS[key];
32447
+ if (name2 === void 0 || !hasMainTable(db, "schema_meta")) return 0;
32448
+ const bare = readKv(db, "schema_meta", key);
32449
+ return (bare === void 0 ? void 0 : counterOf(bare, name2)) ?? 0;
32450
+ }
32626
32451
  function kvRows(db, table, schema = "main") {
32627
32452
  const rows = db.prepare(`SELECT key, value FROM ${schema}.${table}`).all();
32628
32453
  return new Map(rows.map((r) => [r.key, r.value]));
@@ -32755,6 +32580,39 @@ function applyStickyRows(db, schema, plan) {
32755
32580
  }
32756
32581
  return { inserted, replaced: 0, deleted };
32757
32582
  }
32583
+ function tagsOf(tagsJson) {
32584
+ if (!tagsJson) return [];
32585
+ try {
32586
+ const parsed = JSON.parse(tagsJson);
32587
+ if (!Array.isArray(parsed)) return [];
32588
+ return [...new Set(parsed.filter((t) => typeof t === "string" && t.length > 0))];
32589
+ } catch {
32590
+ return [];
32591
+ }
32592
+ }
32593
+ function syncStickyJunction(db, schema) {
32594
+ const notes = db.prepare("SELECT id, tags_json FROM main.brain_sticky_notes").all();
32595
+ const have = /* @__PURE__ */ new Map();
32596
+ for (const r of db.prepare(`SELECT sticky_id, tag FROM ${schema}.brain_sticky_tags`).all()) {
32597
+ const set = have.get(r.sticky_id) ?? /* @__PURE__ */ new Set();
32598
+ set.add(r.tag);
32599
+ have.set(r.sticky_id, set);
32600
+ }
32601
+ const insert = db.prepare(
32602
+ `INSERT OR IGNORE INTO ${schema}.brain_sticky_tags (sticky_id, tag) VALUES (?, ?)`
32603
+ );
32604
+ const del = db.prepare(`DELETE FROM ${schema}.brain_sticky_tags WHERE sticky_id = ? AND tag = ?`);
32605
+ let inserted = 0;
32606
+ let deleted = 0;
32607
+ for (const note of notes) {
32608
+ const want = new Set(tagsOf(note.tags_json));
32609
+ const current = have.get(note.id) ?? /* @__PURE__ */ new Set();
32610
+ for (const tag of want)
32611
+ if (!current.has(tag)) inserted += Number(insert.run(note.id, tag).changes);
32612
+ for (const tag of current) if (!want.has(tag)) deleted += Number(del.run(note.id, tag).changes);
32613
+ }
32614
+ return { inserted, deleted };
32615
+ }
32758
32616
  function twinCollapseFailureOf(db) {
32759
32617
  return degraded.get(db);
32760
32618
  }
@@ -32964,7 +32822,7 @@ function collapseTwinTables(nativeDb, dbPath, options = {}) {
32964
32822
  }
32965
32823
  return inOrder();
32966
32824
  }
32967
- var log6, TWIN_COLLAPSE_MARKER_PREFIX, TWIN_COLLAPSE_FAILURE_PREFIX, SCHEMA_META_COUNTER_FIELDS, SCHEMA_META_NEVER_DELETED, MAX_CONFLICTS, sha, ABSENT, hashMap, emptyPlan, SCHEMA_META, hashSets, EMPTY_SET, STICKY_TAGS, PAIRS, degraded, SHADOW_WRITE_REFUSED, SHADOW_WRITE_OPS;
32825
+ var log6, TWIN_COLLAPSE_MARKER_PREFIX, TWIN_COLLAPSE_FAILURE_PREFIX, SNAPSHOT_GATE_KEY, SCHEMA_META_COUNTER_FIELDS, SCHEMA_META_NEVER_DELETED, MAX_CONFLICTS, sha, ABSENT, hashMap, emptyPlan, SCHEMA_META, hashSets, EMPTY_SET, STICKY_TAGS, PAIRS, degraded, SHADOW_WRITE_REFUSED, SHADOW_WRITE_OPS;
32968
32826
  var init_twin_collapse = __esm({
32969
32827
  "packages/core/src/store/twin-collapse.ts"() {
32970
32828
  "use strict";
@@ -32972,18 +32830,18 @@ var init_twin_collapse = __esm({
32972
32830
  init_errors2();
32973
32831
  init_logger2();
32974
32832
  init_pre_repair_snapshot();
32975
- init_snapshot_gate();
32976
32833
  log6 = getLogger("twin-collapse");
32977
32834
  TWIN_COLLAPSE_MARKER_PREFIX = "twin_collapse:";
32978
32835
  TWIN_COLLAPSE_FAILURE_PREFIX = "twin_collapse_failed:";
32836
+ SNAPSHOT_GATE_KEY = "sqlite_snapshot_gate";
32979
32837
  SCHEMA_META_COUNTER_FIELDS = {
32980
32838
  task_id_sequence: "counter",
32981
- [SNAPSHOT_GATE_META_KEY]: "generation",
32839
+ [SNAPSHOT_GATE_KEY]: "generation",
32982
32840
  file_meta: "generation"
32983
32841
  };
32984
32842
  SCHEMA_META_NEVER_DELETED = /* @__PURE__ */ new Set([
32985
32843
  "task_id_sequence",
32986
- SNAPSHOT_GATE_META_KEY
32844
+ SNAPSHOT_GATE_KEY
32987
32845
  ]);
32988
32846
  MAX_CONFLICTS = 50;
32989
32847
  sha = (value) => createHash3("sha256").update(value).digest("hex");
@@ -33048,7 +32906,12 @@ var init_twin_collapse = __esm({
33048
32906
  throw new Error(
33049
32907
  `sticky_tags collapse did not verify: ${missing} missing, ${lingering} not removed`
33050
32908
  );
33051
- return counts;
32909
+ const synced = syncStickyJunction(db, "main");
32910
+ return {
32911
+ inserted: counts.inserted + synced.inserted,
32912
+ replaced: 0,
32913
+ deleted: counts.deleted + synced.deleted
32914
+ };
33052
32915
  },
33053
32916
  shadow(db, plan) {
33054
32917
  db.exec(
@@ -33059,6 +32922,7 @@ var init_twin_collapse = __esm({
33059
32922
  "INSERT INTO temp.brain_sticky_tags (sticky_id, tag) SELECT sticky_id, tag FROM main.brain_sticky_tags"
33060
32923
  );
33061
32924
  applyStickyRows(db, "temp", plan);
32925
+ syncStickyJunction(db, "temp");
33062
32926
  }
33063
32927
  };
33064
32928
  PAIRS = [SCHEMA_META, STICKY_TAGS];
@@ -38533,9 +38397,12 @@ async function writeMetaValue(cwd, key, value) {
38533
38397
  target: tasksSchemaMeta.key,
38534
38398
  set: { value: json }
38535
38399
  }).run();
38400
+ const native = getNativeTasksDb(cwd);
38401
+ if (native) mirrorCounterToBare(native, key);
38536
38402
  }
38537
38403
  function advanceTaskIdSequence(nativeDb, floor) {
38538
38404
  assertTwinCollapseWritable(nativeDb);
38405
+ const lowest = Math.max(floor, bareCounterOf(nativeDb, "task_id_sequence"));
38539
38406
  nativeDb.prepare(`
38540
38407
  UPDATE tasks_schema_meta
38541
38408
  SET value = json_set(value,
@@ -38544,7 +38411,8 @@ function advanceTaskIdSequence(nativeDb, floor) {
38544
38411
  '$.checksum', 'alloc-' || strftime('%s','now')
38545
38412
  )
38546
38413
  WHERE key = 'task_id_sequence'
38547
- `).run(floor, floor);
38414
+ `).run(lowest, lowest);
38415
+ mirrorCounterToBare(nativeDb, "task_id_sequence");
38548
38416
  const row = nativeDb.prepare(`
38549
38417
  SELECT json_extract(value, '$.counter') AS counter
38550
38418
  FROM tasks_schema_meta WHERE key = 'task_id_sequence'
@@ -39588,12 +39456,12 @@ var init_sqlite_data_accessor = __esm({
39588
39456
 
39589
39457
  // packages/core/src/sequence/index.ts
39590
39458
  import { existsSync as existsSync17, readFileSync as readFileSync5, renameSync as renameSync5 } from "node:fs";
39591
- import { join as join21 } from "node:path";
39459
+ import { join as join20 } from "node:path";
39592
39460
  function getLegacySequenceJsonPath(cwd) {
39593
- return join21(resolveOrCwd(cwd), ".cleo", ".sequence.json");
39461
+ return join20(resolveOrCwd(cwd), ".cleo", ".sequence.json");
39594
39462
  }
39595
39463
  function getLegacySequencePath(cwd) {
39596
- return join21(resolveOrCwd(cwd), ".cleo", ".sequence");
39464
+ return join20(resolveOrCwd(cwd), ".cleo", ".sequence");
39597
39465
  }
39598
39466
  function isValidSequenceState(value) {
39599
39467
  if (!value || typeof value !== "object") return false;
@@ -39768,9 +39636,9 @@ var init_sequence = __esm({
39768
39636
  });
39769
39637
 
39770
39638
  // packages/core/src/store/checkpoint-git-dir.ts
39771
- import { join as join22 } from "node:path";
39639
+ import { join as join21 } from "node:path";
39772
39640
  function checkpointGitDir(cleoDir) {
39773
- return join22(cleoDir, CHECKPOINT_GIT_DIRNAME);
39641
+ return join21(cleoDir, CHECKPOINT_GIT_DIRNAME);
39774
39642
  }
39775
39643
  var CHECKPOINT_GIT_DIRNAME;
39776
39644
  var init_checkpoint_git_dir = __esm({
@@ -39784,7 +39652,7 @@ var init_checkpoint_git_dir = __esm({
39784
39652
  import { execFile as execFile2 } from "node:child_process";
39785
39653
  import { existsSync as existsSync18 } from "node:fs";
39786
39654
  import { readFile as readFile4, writeFile } from "node:fs/promises";
39787
- import { join as join23, resolve as resolve8 } from "node:path";
39655
+ import { join as join22, resolve as resolve8 } from "node:path";
39788
39656
  import { promisify as promisify2 } from "node:util";
39789
39657
  function makeCleoGitEnv(cleoDir) {
39790
39658
  const abs = resolve8(cleoDir);
@@ -39808,7 +39676,7 @@ async function cleoGitCommand(args, cleoDir) {
39808
39676
  }
39809
39677
  }
39810
39678
  function isCleoGitInitialized(cleoDir) {
39811
- return existsSync18(join23(checkpointGitDir(cleoDir), "HEAD"));
39679
+ return existsSync18(join22(checkpointGitDir(cleoDir), "HEAD"));
39812
39680
  }
39813
39681
  async function loadStateFileAllowlist(cwd) {
39814
39682
  try {
@@ -39852,25 +39720,25 @@ async function isCleoGitRepo(cleoDir) {
39852
39720
  return result.success && (result.stdout === "true" || isCleoGitInitialized(cleoDir));
39853
39721
  }
39854
39722
  function isMergeInProgress(cleoDir) {
39855
- return existsSync18(join23(checkpointGitDir(cleoDir), "MERGE_HEAD"));
39723
+ return existsSync18(join22(checkpointGitDir(cleoDir), "MERGE_HEAD"));
39856
39724
  }
39857
39725
  async function isDetachedHead(cleoDir) {
39858
39726
  const result = await cleoGitCommand(["symbolic-ref", "HEAD"], cleoDir);
39859
39727
  return !result.success;
39860
39728
  }
39861
39729
  function isRebaseInProgress(cleoDir) {
39862
- return existsSync18(join23(checkpointGitDir(cleoDir), "rebase-merge")) || existsSync18(join23(checkpointGitDir(cleoDir), "rebase-apply"));
39730
+ return existsSync18(join22(checkpointGitDir(cleoDir), "rebase-merge")) || existsSync18(join22(checkpointGitDir(cleoDir), "rebase-apply"));
39863
39731
  }
39864
39732
  async function recordCheckpointTime(cleoDir) {
39865
39733
  try {
39866
- const stateFile = join23(cleoDir, CHECKPOINT_STATE_FILE);
39734
+ const stateFile = join22(cleoDir, CHECKPOINT_STATE_FILE);
39867
39735
  await writeFile(stateFile, String(Math.floor(Date.now() / 1e3)));
39868
39736
  } catch {
39869
39737
  }
39870
39738
  }
39871
39739
  async function getLastCheckpointTime(cleoDir) {
39872
39740
  try {
39873
- const stateFile = join23(cleoDir, CHECKPOINT_STATE_FILE);
39741
+ const stateFile = join22(cleoDir, CHECKPOINT_STATE_FILE);
39874
39742
  const content = await readFile4(stateFile, "utf-8");
39875
39743
  const epoch = parseInt(content.trim(), 10);
39876
39744
  return Number.isNaN(epoch) ? 0 : epoch;
@@ -39882,7 +39750,7 @@ async function getChangedStateFiles(cleoDir, cwd) {
39882
39750
  const changed = [];
39883
39751
  const allStateFiles = await getAllStateFiles(cwd);
39884
39752
  for (const stateFile of allStateFiles) {
39885
- const fullPath = join23(cleoDir, stateFile);
39753
+ const fullPath = join22(cleoDir, stateFile);
39886
39754
  if (!existsSync18(fullPath)) continue;
39887
39755
  const diffResult = await cleoGitCommand(["diff", "--quiet", "--", stateFile], cleoDir);
39888
39756
  const cachedResult = await cleoGitCommand(
@@ -39983,6 +39851,218 @@ var init_git_checkpoint = __esm({
39983
39851
  }
39984
39852
  });
39985
39853
 
39854
+ // packages/core/src/store/snapshot-gate.ts
39855
+ import { join as join23 } from "node:path";
39856
+ function readGateState(db) {
39857
+ const row = db.prepare("SELECT value FROM tasks_schema_meta WHERE key = ?").get(SNAPSHOT_GATE_META_KEY);
39858
+ const state = { generation: 0, prefixes: {} };
39859
+ if (!row) return state;
39860
+ try {
39861
+ const parsed = JSON.parse(row.value);
39862
+ if (parsed === null || typeof parsed !== "object") return state;
39863
+ if (typeof parsed.generation === "number" && Number.isSafeInteger(parsed.generation)) {
39864
+ state.generation = parsed.generation;
39865
+ }
39866
+ for (const [prefix, e] of Object.entries(parsed.prefixes ?? {})) {
39867
+ if (e && typeof e.generation === "number" && typeof e.startedAt === "number" && typeof e.completedAt === "number") {
39868
+ state.prefixes[prefix] = {
39869
+ generation: e.generation,
39870
+ startedAt: e.startedAt,
39871
+ completedAt: e.completedAt
39872
+ };
39873
+ }
39874
+ }
39875
+ } catch {
39876
+ }
39877
+ return state;
39878
+ }
39879
+ function writeGateState(db, state) {
39880
+ db.prepare(
39881
+ "INSERT INTO tasks_schema_meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value"
39882
+ ).run(SNAPSHOT_GATE_META_KEY, JSON.stringify(state));
39883
+ mirrorCounterToBare(db, SNAPSHOT_GATE_META_KEY);
39884
+ }
39885
+ function tryReadGateState(db) {
39886
+ if (!db) return null;
39887
+ try {
39888
+ return readGateState(db);
39889
+ } catch {
39890
+ return null;
39891
+ }
39892
+ }
39893
+ function needsSnapshot(entry, mode, nowMs, seenGeneration) {
39894
+ if (!entry) return true;
39895
+ if (mode === "required") return seenGeneration === null || entry.generation <= seenGeneration;
39896
+ const elapsed = nowMs - entry.startedAt;
39897
+ return elapsed < 0 || elapsed >= SNAPSHOT_DEBOUNCE_MS;
39898
+ }
39899
+ async function runGatedSnapshot(opts, snapshot) {
39900
+ const now = opts.now ?? Date.now;
39901
+ const mode = opts.mode ?? "routine";
39902
+ const db = opts.stateDb;
39903
+ const none = (skipped, error) => ({
39904
+ snapshotted: [],
39905
+ linked: [],
39906
+ absent: [],
39907
+ failed: [],
39908
+ skipped,
39909
+ ...error !== void 0 && { error }
39910
+ });
39911
+ const entryState = tryReadGateState(db);
39912
+ const seenGeneration = mode === "required" ? opts.seenGeneration ?? entryState?.generation ?? null : null;
39913
+ if (mode === "routine") {
39914
+ if (!entryState) return none("state-unavailable");
39915
+ const nowMs = now();
39916
+ if (!opts.prefixes.some((p) => needsSnapshot(entryState.prefixes[p], mode, nowMs, null))) {
39917
+ return none("debounced");
39918
+ }
39919
+ }
39920
+ const lockPath2 = join23(opts.backupDir, GATE_LOCK_BASENAME);
39921
+ let release;
39922
+ try {
39923
+ release = await acquireLock(lockPath2, {
39924
+ retries: mode === "required" ? opts.lockWaitRetries ?? SNAPSHOT_LOCK_WAIT_RETRIES : 0,
39925
+ stale: SNAPSHOT_LOCK_STALE_MS,
39926
+ // A compromised lock (e.g. the backup dir was removed) must not throw
39927
+ // from a timer and crash the host process.
39928
+ onCompromised: () => {
39929
+ }
39930
+ });
39931
+ } catch {
39932
+ if (mode === "routine") return none("in-flight");
39933
+ return none(
39934
+ "lock-timeout",
39935
+ `Snapshot lock ${lockPath2}.lock is still held after waiting. Another snapshot is running, or a crashed process left the lock (it expires after ${SNAPSHOT_LOCK_STALE_MS / 6e4} minutes). Remove it if no snapshot is running.`
39936
+ );
39937
+ }
39938
+ try {
39939
+ try {
39940
+ opts.onLockAcquired?.();
39941
+ } catch {
39942
+ }
39943
+ const state = tryReadGateState(db);
39944
+ if (!state && mode === "routine") return none("state-unavailable");
39945
+ const startMs = now();
39946
+ const admitted = mode === "required" && opts.alwaysSnapshot === true ? [...opts.prefixes] : opts.prefixes.filter(
39947
+ (p) => needsSnapshot(state?.prefixes[p], mode, startMs, seenGeneration)
39948
+ );
39949
+ if (admitted.length === 0) return none(mode === "routine" ? "debounced" : "covered");
39950
+ const generation = (state?.generation ?? 0) + 1;
39951
+ let claimed = false;
39952
+ if (db && state) {
39953
+ try {
39954
+ writeGateState(db, { ...state, generation });
39955
+ claimed = true;
39956
+ } catch {
39957
+ }
39958
+ }
39959
+ const snapshotted = [];
39960
+ const linked = [];
39961
+ const absent = [];
39962
+ const failed = [];
39963
+ for (const prefix of admitted) {
39964
+ try {
39965
+ const outcome = await snapshot(prefix);
39966
+ if (outcome === "absent") absent.push(prefix);
39967
+ else {
39968
+ snapshotted.push(prefix);
39969
+ if (outcome === "linked") linked.push(prefix);
39970
+ }
39971
+ } catch {
39972
+ failed.push(prefix);
39973
+ }
39974
+ }
39975
+ const satisfied = [...snapshotted, ...absent];
39976
+ if (db && claimed && satisfied.length > 0) {
39977
+ try {
39978
+ const latest = readGateState(db);
39979
+ const completedAt = now();
39980
+ latest.generation = Math.max(latest.generation, generation);
39981
+ for (const p of satisfied) {
39982
+ latest.prefixes[p] = { generation, startedAt: startMs, completedAt };
39983
+ }
39984
+ writeGateState(db, latest);
39985
+ } catch {
39986
+ }
39987
+ }
39988
+ return { snapshotted, linked, absent, failed, skipped: null };
39989
+ } finally {
39990
+ await release().catch(() => {
39991
+ });
39992
+ }
39993
+ }
39994
+ function isValidStamp(ymd, hms) {
39995
+ const y = Number(ymd.slice(0, 4));
39996
+ const mo = Number(ymd.slice(4, 6));
39997
+ const d = Number(ymd.slice(6, 8));
39998
+ const h = Number(hms.slice(0, 2));
39999
+ const mi = Number(hms.slice(2, 4));
40000
+ const se = Number(hms.slice(4, 6));
40001
+ if (mo < 1 || mo > 12 || d < 1 || h > 23 || mi > 59 || se > 59) return false;
40002
+ const probe = new Date(Date.UTC(y, mo - 1, d));
40003
+ return probe.getUTCFullYear() === y && probe.getUTCMonth() === mo - 1 && probe.getUTCDate() === d;
40004
+ }
40005
+ function selectSnapshotsToKeep(names, ctx = {}) {
40006
+ const policy = ctx.policy ?? DEFAULT_SNAPSHOT_RETENTION;
40007
+ const keep = /* @__PURE__ */ new Set();
40008
+ const stamped = [];
40009
+ for (const name2 of names) {
40010
+ const m = STAMP_PATTERN.exec(name2);
40011
+ const ymd = m?.[1];
40012
+ const hms = m?.[2];
40013
+ if (!ymd || !hms || !isValidStamp(ymd, hms)) {
40014
+ keep.add(name2);
40015
+ continue;
40016
+ }
40017
+ const stamp = `${ymd}-${hms}`;
40018
+ if (ctx.nowStamp !== void 0 && stamp > ctx.nowStamp) {
40019
+ keep.add(name2);
40020
+ continue;
40021
+ }
40022
+ stamped.push({ name: name2, stamp });
40023
+ }
40024
+ if (ctx.pinned !== void 0 && names.includes(ctx.pinned)) keep.add(ctx.pinned);
40025
+ stamped.sort((a, b) => a.stamp < b.stamp ? 1 : a.stamp > b.stamp ? -1 : 0);
40026
+ for (const s of stamped.slice(0, policy.latest)) keep.add(s.name);
40027
+ const keepNewestPerBucket = (bucketOf, slots) => {
40028
+ const seen = /* @__PURE__ */ new Set();
40029
+ for (const s of stamped) {
40030
+ const bucket = bucketOf(s.stamp);
40031
+ if (seen.has(bucket)) continue;
40032
+ if (seen.size >= slots) break;
40033
+ seen.add(bucket);
40034
+ keep.add(s.name);
40035
+ }
40036
+ };
40037
+ keepNewestPerBucket(
40038
+ (stamp) => `${stamp.slice(0, 11)}q${Math.floor(Number(stamp.slice(11, 13)) / 15)}`,
40039
+ policy.quarterHourly
40040
+ );
40041
+ keepNewestPerBucket((stamp) => stamp.slice(0, 11), policy.hourly);
40042
+ keepNewestPerBucket((stamp) => stamp.slice(0, 8), policy.daily);
40043
+ return keep;
40044
+ }
40045
+ var SNAPSHOT_DEBOUNCE_MS, SNAPSHOT_LOCK_STALE_MS, SNAPSHOT_LOCK_WAIT_RETRIES, SNAPSHOT_GATE_META_KEY, GATE_LOCK_BASENAME, DEFAULT_SNAPSHOT_RETENTION, STAMP_PATTERN;
40046
+ var init_snapshot_gate = __esm({
40047
+ "packages/core/src/store/snapshot-gate.ts"() {
40048
+ "use strict";
40049
+ init_lock();
40050
+ init_twin_collapse();
40051
+ SNAPSHOT_DEBOUNCE_MS = 5 * 6e4;
40052
+ SNAPSHOT_LOCK_STALE_MS = 10 * 6e4;
40053
+ SNAPSHOT_LOCK_WAIT_RETRIES = 90;
40054
+ SNAPSHOT_GATE_META_KEY = "sqlite_snapshot_gate";
40055
+ GATE_LOCK_BASENAME = ".snapshot-gate";
40056
+ DEFAULT_SNAPSHOT_RETENTION = {
40057
+ latest: 1,
40058
+ quarterHourly: 3,
40059
+ hourly: 3,
40060
+ daily: 3
40061
+ };
40062
+ STAMP_PATTERN = /-(\d{8})-(\d{6})\.db$/;
40063
+ }
40064
+ });
40065
+
39986
40066
  // packages/core/src/store/data-safety-central.ts
39987
40067
  import { eq as eq7 } from "drizzle-orm";
39988
40068
  function inSafetyScope(cwd, operation) {
@@ -41284,7 +41364,8 @@ __export(sqlite_exports, {
41284
41364
  resetDbState: () => resetDbState,
41285
41365
  resolveMigrationsFolder: () => resolveMigrationsFolder,
41286
41366
  schema: () => tasks_schema_exports,
41287
- seedTasksMeta: () => seedTasksMeta
41367
+ seedTasksMeta: () => seedTasksMeta,
41368
+ worktreeRecoveryRefusal: () => worktreeRecoveryRefusal
41288
41369
  });
41289
41370
  import { copyFileSync as copyFileSync4, existsSync as existsSync20, realpathSync as realpathSync7, renameSync as renameSync7, unlinkSync as unlinkSync4 } from "node:fs";
41290
41371
  import { createRequire as createRequire7 } from "node:module";
@@ -41335,6 +41416,12 @@ function isSameDirectory(a, b) {
41335
41416
  return false;
41336
41417
  }
41337
41418
  }
41419
+ function worktreeRecoveryRefusal(dbPath) {
41420
+ const storeCleoDir = dirname14(dbPath);
41421
+ const root = dirname14(storeCleoDir);
41422
+ if (basename8(storeCleoDir) !== ".cleo" || !isGitLinkedCheckout(root)) return null;
41423
+ return `Auto-recovery refused: ${dbPath} lives inside the git worktree ${root}; ${describeWorktreeOwner(root)}. A worktree uses its owning project's store and never receives a restored copy (T12460, T12677).`;
41424
+ }
41338
41425
  async function autoRecoverFromBackup(nativeDb, dbPath, cwd) {
41339
41426
  const { openNativeDatabase: openNativeDatabase2 } = await Promise.resolve().then(() => (init_sqlite_native(), sqlite_native_exports));
41340
41427
  const log15 = getLogger("sqlite");
@@ -41344,11 +41431,9 @@ async function autoRecoverFromBackup(nativeDb, dbPath, cwd) {
41344
41431
  const taskCount = countResult?.cnt ?? 0;
41345
41432
  if (taskCount > 0) return false;
41346
41433
  const storeCleoDir = dirname14(dbPath);
41347
- if (basename8(storeCleoDir) === ".cleo" && isGitLinkedCheckout(dirname14(storeCleoDir))) {
41348
- log15.warn(
41349
- { dbPath },
41350
- "Auto-recovery refused: the empty store lives inside a git worktree (T12460). Worktrees must resolve the parent project store."
41351
- );
41434
+ const worktreeRefusal = worktreeRecoveryRefusal(dbPath);
41435
+ if (worktreeRefusal !== null) {
41436
+ log15.warn({ dbPath }, worktreeRefusal);
41352
41437
  return false;
41353
41438
  }
41354
41439
  const backups = listSqliteBackups(cwd);