@cleocode/core 2026.10.21 → 2026.10.22-rc.ci.693

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 (420) hide show
  1. package/STABILITY.md +1 -1
  2. package/dist/build-provenance.json +2 -2
  3. package/dist/config.d.ts.map +1 -1
  4. package/dist/config.js +2 -0
  5. package/dist/config.js.map +1 -1
  6. package/dist/docs/document-edges.d.ts +38 -0
  7. package/dist/docs/document-edges.d.ts.map +1 -0
  8. package/dist/docs/document-edges.js +62 -0
  9. package/dist/docs/document-edges.js.map +1 -0
  10. package/dist/docs/projection.d.ts +3 -2
  11. package/dist/docs/projection.d.ts.map +1 -1
  12. package/dist/docs/projection.js +33 -36
  13. package/dist/docs/projection.js.map +1 -1
  14. package/dist/events/event-bus.d.ts +6 -5
  15. package/dist/events/event-bus.d.ts.map +1 -1
  16. package/dist/events/event-bus.js +9 -8
  17. package/dist/events/event-bus.js.map +1 -1
  18. package/dist/gateway-client/generated/index.d.ts +2 -2
  19. package/dist/gateway-client/generated/index.d.ts.map +1 -1
  20. package/dist/gateway-client/generated/index.js +1 -1
  21. package/dist/gateway-client/generated/index.js.map +1 -1
  22. package/dist/gateway-client/generated/namespaces.gen.d.ts +1 -0
  23. package/dist/gateway-client/generated/namespaces.gen.d.ts.map +1 -1
  24. package/dist/gateway-client/generated/namespaces.gen.js +2 -1
  25. package/dist/gateway-client/generated/namespaces.gen.js.map +1 -1
  26. package/dist/gateway-client/generated/sdk.gen.d.ts +5 -1
  27. package/dist/gateway-client/generated/sdk.gen.d.ts.map +1 -1
  28. package/dist/gateway-client/generated/sdk.gen.js +11 -0
  29. package/dist/gateway-client/generated/sdk.gen.js.map +1 -1
  30. package/dist/gateway-client/generated/types.gen.d.ts +34 -0
  31. package/dist/gateway-client/generated/types.gen.d.ts.map +1 -1
  32. package/dist/git/base-ref.d.ts +81 -0
  33. package/dist/git/base-ref.d.ts.map +1 -0
  34. package/dist/git/base-ref.js +135 -0
  35. package/dist/git/base-ref.js.map +1 -0
  36. package/dist/git/hooks-install.d.ts +1 -1
  37. package/dist/git/hooks-install.d.ts.map +1 -1
  38. package/dist/git/work-tree.d.ts +40 -0
  39. package/dist/git/work-tree.d.ts.map +1 -1
  40. package/dist/git/work-tree.js +62 -7
  41. package/dist/git/work-tree.js.map +1 -1
  42. package/dist/hooks/handlers/conduit-hooks.d.ts +2 -1
  43. package/dist/hooks/handlers/conduit-hooks.d.ts.map +1 -1
  44. package/dist/hooks/handlers/conduit-hooks.js +9 -23
  45. package/dist/hooks/handlers/conduit-hooks.js.map +1 -1
  46. package/dist/hooks/handlers/index.d.ts +0 -1
  47. package/dist/hooks/handlers/index.d.ts.map +1 -1
  48. package/dist/hooks/handlers/index.js +0 -2
  49. package/dist/hooks/handlers/index.js.map +1 -1
  50. package/dist/hooks.d.ts +1 -1
  51. package/dist/hooks.d.ts.map +1 -1
  52. package/dist/init.d.ts.map +1 -1
  53. package/dist/init.js +5 -12
  54. package/dist/init.js.map +1 -1
  55. package/dist/internal.d.ts +2 -3
  56. package/dist/internal.d.ts.map +1 -1
  57. package/dist/internal.js +1 -1
  58. package/dist/internal.js.map +1 -1
  59. package/dist/lifecycle/index.d.ts +15 -1
  60. package/dist/lifecycle/index.d.ts.map +1 -1
  61. package/dist/lifecycle/index.js +12 -6
  62. package/dist/lifecycle/index.js.map +1 -1
  63. package/dist/lifecycle/stage-guidance.js +1 -1
  64. package/dist/lifecycle/stage-guidance.js.map +1 -1
  65. package/dist/memory/auto-row-quarantine.d.ts +196 -0
  66. package/dist/memory/auto-row-quarantine.d.ts.map +1 -0
  67. package/dist/memory/auto-row-quarantine.js +363 -0
  68. package/dist/memory/auto-row-quarantine.js.map +1 -0
  69. package/dist/memory/auto-row-rules.d.ts +29 -0
  70. package/dist/memory/auto-row-rules.d.ts.map +1 -0
  71. package/dist/memory/auto-row-rules.js +55 -0
  72. package/dist/memory/auto-row-rules.js.map +1 -0
  73. package/dist/memory/brain-lifecycle.d.ts.map +1 -1
  74. package/dist/memory/brain-lifecycle.js +13 -8
  75. package/dist/memory/brain-lifecycle.js.map +1 -1
  76. package/dist/memory/brain-search.d.ts +49 -29
  77. package/dist/memory/brain-search.d.ts.map +1 -1
  78. package/dist/memory/brain-search.js +159 -156
  79. package/dist/memory/brain-search.js.map +1 -1
  80. package/dist/memory/brain-similarity.d.ts +27 -4
  81. package/dist/memory/brain-similarity.d.ts.map +1 -1
  82. package/dist/memory/brain-similarity.js +41 -20
  83. package/dist/memory/brain-similarity.js.map +1 -1
  84. package/dist/memory/brain-stub-prune.d.ts +13 -0
  85. package/dist/memory/brain-stub-prune.d.ts.map +1 -1
  86. package/dist/memory/brain-stub-prune.js +14 -3
  87. package/dist/memory/brain-stub-prune.js.map +1 -1
  88. package/dist/memory/brain-writer-handlers.d.ts +3 -7
  89. package/dist/memory/brain-writer-handlers.d.ts.map +1 -1
  90. package/dist/memory/brain-writer-handlers.js +5 -20
  91. package/dist/memory/brain-writer-handlers.js.map +1 -1
  92. package/dist/memory/brain-writer-thread.d.ts +2 -11
  93. package/dist/memory/brain-writer-thread.d.ts.map +1 -1
  94. package/dist/memory/brain-writer-thread.js +20 -78
  95. package/dist/memory/brain-writer-thread.js.map +1 -1
  96. package/dist/memory/brain-writer-worker.js +1 -8
  97. package/dist/memory/brain-writer-worker.js.map +1 -1
  98. package/dist/memory/dispatch-trace-format.d.ts +13 -14
  99. package/dist/memory/dispatch-trace-format.d.ts.map +1 -1
  100. package/dist/memory/dispatch-trace-format.js +13 -14
  101. package/dist/memory/dispatch-trace-format.js.map +1 -1
  102. package/dist/memory/eligibility.d.ts +22 -0
  103. package/dist/memory/eligibility.d.ts.map +1 -1
  104. package/dist/memory/eligibility.js +36 -0
  105. package/dist/memory/eligibility.js.map +1 -1
  106. package/dist/memory/engine-compat.d.ts.map +1 -1
  107. package/dist/memory/engine-compat.js +11 -1
  108. package/dist/memory/engine-compat.js.map +1 -1
  109. package/dist/memory/evidence-graph-links.d.ts +253 -0
  110. package/dist/memory/evidence-graph-links.d.ts.map +1 -0
  111. package/dist/memory/evidence-graph-links.js +554 -0
  112. package/dist/memory/evidence-graph-links.js.map +1 -0
  113. package/dist/memory/extraction-gate.d.ts +7 -0
  114. package/dist/memory/extraction-gate.d.ts.map +1 -1
  115. package/dist/memory/extraction-gate.js +20 -8
  116. package/dist/memory/extraction-gate.js.map +1 -1
  117. package/dist/memory/global-brain.d.ts +4 -6
  118. package/dist/memory/global-brain.d.ts.map +1 -1
  119. package/dist/memory/global-brain.js +15 -2
  120. package/dist/memory/global-brain.js.map +1 -1
  121. package/dist/memory/graph-auto-populate.d.ts +73 -1
  122. package/dist/memory/graph-auto-populate.d.ts.map +1 -1
  123. package/dist/memory/graph-auto-populate.js +111 -8
  124. package/dist/memory/graph-auto-populate.js.map +1 -1
  125. package/dist/memory/import-from-provider.d.ts +19 -6
  126. package/dist/memory/import-from-provider.d.ts.map +1 -1
  127. package/dist/memory/import-from-provider.js +28 -15
  128. package/dist/memory/import-from-provider.js.map +1 -1
  129. package/dist/memory/lessons-import.d.ts +175 -0
  130. package/dist/memory/lessons-import.d.ts.map +1 -0
  131. package/dist/memory/lessons-import.js +233 -0
  132. package/dist/memory/lessons-import.js.map +1 -0
  133. package/dist/memory/lessons.d.ts +67 -0
  134. package/dist/memory/lessons.d.ts.map +1 -0
  135. package/dist/memory/lessons.js +121 -0
  136. package/dist/memory/lessons.js.map +1 -0
  137. package/dist/memory/memory-bridge.d.ts.map +1 -1
  138. package/dist/memory/memory-bridge.js +3 -2
  139. package/dist/memory/memory-bridge.js.map +1 -1
  140. package/dist/memory/public-api.d.ts.map +1 -1
  141. package/dist/memory/public-api.js +7 -6
  142. package/dist/memory/public-api.js.map +1 -1
  143. package/dist/memory/recall-ranking.d.ts +88 -0
  144. package/dist/memory/recall-ranking.d.ts.map +1 -0
  145. package/dist/memory/recall-ranking.js +130 -0
  146. package/dist/memory/recall-ranking.js.map +1 -0
  147. package/dist/memory/retrieval/build-retrieval-bundle.js +5 -5
  148. package/dist/memory/retrieval/build-retrieval-bundle.js.map +1 -1
  149. package/dist/memory/retrieval/fetch.d.ts +3 -2
  150. package/dist/memory/retrieval/fetch.d.ts.map +1 -1
  151. package/dist/memory/retrieval/fetch.js +4 -3
  152. package/dist/memory/retrieval/fetch.js.map +1 -1
  153. package/dist/memory/retrieval/observe.d.ts +1 -7
  154. package/dist/memory/retrieval/observe.d.ts.map +1 -1
  155. package/dist/memory/retrieval/observe.js +9 -58
  156. package/dist/memory/retrieval/observe.js.map +1 -1
  157. package/dist/memory/retrieval/search.d.ts +5 -6
  158. package/dist/memory/retrieval/search.d.ts.map +1 -1
  159. package/dist/memory/retrieval/search.js +38 -102
  160. package/dist/memory/retrieval/search.js.map +1 -1
  161. package/dist/memory/retrieval/telemetry.d.ts +5 -16
  162. package/dist/memory/retrieval/telemetry.d.ts.map +1 -1
  163. package/dist/memory/retrieval/telemetry.js +5 -22
  164. package/dist/memory/retrieval/telemetry.js.map +1 -1
  165. package/dist/nexus/analyze-orchestrator.d.ts +15 -0
  166. package/dist/nexus/analyze-orchestrator.d.ts.map +1 -1
  167. package/dist/nexus/analyze-orchestrator.js +53 -6
  168. package/dist/nexus/analyze-orchestrator.js.map +1 -1
  169. package/dist/nexus/brief-map.d.ts +60 -0
  170. package/dist/nexus/brief-map.d.ts.map +1 -0
  171. package/dist/nexus/brief-map.js +234 -0
  172. package/dist/nexus/brief-map.js.map +1 -0
  173. package/dist/nexus/freshness.d.ts +128 -13
  174. package/dist/nexus/freshness.d.ts.map +1 -1
  175. package/dist/nexus/freshness.js +374 -86
  176. package/dist/nexus/freshness.js.map +1 -1
  177. package/dist/nexus/graph-manifest.d.ts +47 -0
  178. package/dist/nexus/graph-manifest.d.ts.map +1 -1
  179. package/dist/nexus/graph-manifest.js +69 -0
  180. package/dist/nexus/graph-manifest.js.map +1 -1
  181. package/dist/nexus/hooks-augment.d.ts.map +1 -1
  182. package/dist/nexus/hooks-augment.js +7 -3
  183. package/dist/nexus/hooks-augment.js.map +1 -1
  184. package/dist/nexus/index-health.d.ts +112 -0
  185. package/dist/nexus/index-health.d.ts.map +1 -0
  186. package/dist/nexus/index-health.js +199 -0
  187. package/dist/nexus/index-health.js.map +1 -0
  188. package/dist/nexus/living-brain.d.ts.map +1 -1
  189. package/dist/nexus/living-brain.js +2 -15
  190. package/dist/nexus/living-brain.js.map +1 -1
  191. package/dist/nexus/pid-lock.d.ts +50 -0
  192. package/dist/nexus/pid-lock.d.ts.map +1 -0
  193. package/dist/nexus/pid-lock.js +135 -0
  194. package/dist/nexus/pid-lock.js.map +1 -0
  195. package/dist/nexus/projects-clean.d.ts.map +1 -1
  196. package/dist/nexus/projects-clean.js +33 -2
  197. package/dist/nexus/projects-clean.js.map +1 -1
  198. package/dist/nexus/refresh-child.js +4 -0
  199. package/dist/nexus/refresh-child.js.map +1 -1
  200. package/dist/nexus/registry-host.d.ts +70 -0
  201. package/dist/nexus/registry-host.d.ts.map +1 -0
  202. package/dist/nexus/registry-host.js +79 -0
  203. package/dist/nexus/registry-host.js.map +1 -0
  204. package/dist/nexus/registry.d.ts +37 -0
  205. package/dist/nexus/registry.d.ts.map +1 -1
  206. package/dist/nexus/registry.js +49 -1
  207. package/dist/nexus/registry.js.map +1 -1
  208. package/dist/nexus/reindex-on-merge.d.ts +94 -0
  209. package/dist/nexus/reindex-on-merge.d.ts.map +1 -0
  210. package/dist/nexus/reindex-on-merge.js +192 -0
  211. package/dist/nexus/reindex-on-merge.js.map +1 -0
  212. package/dist/orchestrate/handoff-ops.d.ts.map +1 -1
  213. package/dist/orchestrate/handoff-ops.js +1 -13
  214. package/dist/orchestrate/handoff-ops.js.map +1 -1
  215. package/dist/orchestrate/index.d.ts +2 -3
  216. package/dist/orchestrate/index.d.ts.map +1 -1
  217. package/dist/orchestrate/index.js +2 -2
  218. package/dist/orchestrate/index.js.map +1 -1
  219. package/dist/orchestrate/lifecycle-ops.d.ts +3 -0
  220. package/dist/orchestrate/lifecycle-ops.d.ts.map +1 -1
  221. package/dist/orchestrate/lifecycle-ops.js +5 -5
  222. package/dist/orchestrate/lifecycle-ops.js.map +1 -1
  223. package/dist/orchestrate/spawn-ops.d.ts +8 -28
  224. package/dist/orchestrate/spawn-ops.d.ts.map +1 -1
  225. package/dist/orchestrate/spawn-ops.js +8 -67
  226. package/dist/orchestrate/spawn-ops.js.map +1 -1
  227. package/dist/orchestration/lead-rollup.d.ts +7 -0
  228. package/dist/orchestration/lead-rollup.d.ts.map +1 -1
  229. package/dist/orchestration/lead-rollup.js.map +1 -1
  230. package/dist/orchestration/spawn-prompt.d.ts +8 -29
  231. package/dist/orchestration/spawn-prompt.d.ts.map +1 -1
  232. package/dist/orchestration/spawn-prompt.js +44 -112
  233. package/dist/orchestration/spawn-prompt.js.map +1 -1
  234. package/dist/orchestration/spawn.d.ts +7 -14
  235. package/dist/orchestration/spawn.d.ts.map +1 -1
  236. package/dist/orchestration/spawn.js +2 -5
  237. package/dist/orchestration/spawn.js.map +1 -1
  238. package/dist/orchestration/wave-topic.d.ts +0 -13
  239. package/dist/orchestration/wave-topic.d.ts.map +1 -1
  240. package/dist/orchestration/wave-topic.js +0 -20
  241. package/dist/orchestration/wave-topic.js.map +1 -1
  242. package/dist/release/engine-ops.js +2 -2
  243. package/dist/release/engine-ops.js.map +1 -1
  244. package/dist/release/github-pr.d.ts +8 -5
  245. package/dist/release/github-pr.d.ts.map +1 -1
  246. package/dist/release/github-pr.js +11 -16
  247. package/dist/release/github-pr.js.map +1 -1
  248. package/dist/release/plan.d.ts +4 -2
  249. package/dist/release/plan.d.ts.map +1 -1
  250. package/dist/release/plan.js +11 -4
  251. package/dist/release/plan.js.map +1 -1
  252. package/dist/release/pr-evidence.d.ts +39 -11
  253. package/dist/release/pr-evidence.d.ts.map +1 -1
  254. package/dist/release/pr-evidence.js +122 -31
  255. package/dist/release/pr-evidence.js.map +1 -1
  256. package/dist/release/reconcile.d.ts +15 -0
  257. package/dist/release/reconcile.d.ts.map +1 -1
  258. package/dist/release/reconcile.js +36 -0
  259. package/dist/release/reconcile.js.map +1 -1
  260. package/dist/render/brain/index.d.ts +3 -2
  261. package/dist/render/brain/index.d.ts.map +1 -1
  262. package/dist/render/brain/index.js +4 -2
  263. package/dist/render/brain/index.js.map +1 -1
  264. package/dist/render/brain/migrate-repo-ids.d.ts +19 -0
  265. package/dist/render/brain/migrate-repo-ids.d.ts.map +1 -0
  266. package/dist/render/brain/migrate-repo-ids.js +41 -0
  267. package/dist/render/brain/migrate-repo-ids.js.map +1 -0
  268. package/dist/render/nexus/graph/index.d.ts.map +1 -1
  269. package/dist/render/nexus/graph/index.js +10 -1
  270. package/dist/render/nexus/graph/index.js.map +1 -1
  271. package/dist/resources/admission-ledger.d.ts +39 -4
  272. package/dist/resources/admission-ledger.d.ts.map +1 -1
  273. package/dist/resources/admission-ledger.js +147 -32
  274. package/dist/resources/admission-ledger.js.map +1 -1
  275. package/dist/resources/hook-footprint.d.ts +90 -0
  276. package/dist/resources/hook-footprint.d.ts.map +1 -0
  277. package/dist/resources/hook-footprint.js +245 -0
  278. package/dist/resources/hook-footprint.js.map +1 -0
  279. package/dist/resources/pressure-gate.d.ts +3 -2
  280. package/dist/resources/pressure-gate.d.ts.map +1 -1
  281. package/dist/resources/pressure-gate.js +8 -5
  282. package/dist/resources/pressure-gate.js.map +1 -1
  283. package/dist/resources/run-admission.d.ts +23 -3
  284. package/dist/resources/run-admission.d.ts.map +1 -1
  285. package/dist/resources/run-admission.js +70 -8
  286. package/dist/resources/run-admission.js.map +1 -1
  287. package/dist/resources/run-class.d.ts +1 -0
  288. package/dist/resources/run-class.d.ts.map +1 -1
  289. package/dist/resources/run-class.js +7 -0
  290. package/dist/resources/run-class.js.map +1 -1
  291. package/dist/resources/run-governed.d.ts +13 -1
  292. package/dist/resources/run-governed.d.ts.map +1 -1
  293. package/dist/resources/run-governed.js +29 -6
  294. package/dist/resources/run-governed.js.map +1 -1
  295. package/dist/resources/top-memory.d.ts +67 -0
  296. package/dist/resources/top-memory.d.ts.map +1 -0
  297. package/dist/resources/top-memory.js +143 -0
  298. package/dist/resources/top-memory.js.map +1 -0
  299. package/dist/scaffold/global-scaffold.d.ts +6 -1
  300. package/dist/scaffold/global-scaffold.d.ts.map +1 -1
  301. package/dist/scaffold/global-scaffold.js +11 -2
  302. package/dist/scaffold/global-scaffold.js.map +1 -1
  303. package/dist/scaffold/scaffold-global.d.ts +3 -2
  304. package/dist/scaffold/scaffold-global.d.ts.map +1 -1
  305. package/dist/scaffold/scaffold-global.js +3 -2
  306. package/dist/scaffold/scaffold-global.js.map +1 -1
  307. package/dist/sentient/ingesters/brain-ingester.d.ts.map +1 -1
  308. package/dist/sentient/ingesters/brain-ingester.js +2 -1
  309. package/dist/sentient/ingesters/brain-ingester.js.map +1 -1
  310. package/dist/session/engine-ops.js +1 -1
  311. package/dist/session/engine-ops.js.map +1 -1
  312. package/dist/sessions/briefing.d.ts +26 -0
  313. package/dist/sessions/briefing.d.ts.map +1 -1
  314. package/dist/sessions/briefing.js +46 -0
  315. package/dist/sessions/briefing.js.map +1 -1
  316. package/dist/sessions/session-end-snapshot.d.ts +1 -1
  317. package/dist/sessions/session-end-snapshot.js +1 -1
  318. package/dist/sessions/session-id.d.ts +2 -1
  319. package/dist/sessions/session-id.d.ts.map +1 -1
  320. package/dist/sessions/session-id.js +3 -2
  321. package/dist/sessions/session-id.js.map +1 -1
  322. package/dist/store/agent-resolver.d.ts +0 -6
  323. package/dist/store/agent-resolver.d.ts.map +1 -1
  324. package/dist/store/agent-resolver.js +0 -31
  325. package/dist/store/agent-resolver.js.map +1 -1
  326. package/dist/store/memory-accessor.d.ts +45 -2
  327. package/dist/store/memory-accessor.d.ts.map +1 -1
  328. package/dist/store/memory-accessor.js +81 -7
  329. package/dist/store/memory-accessor.js.map +1 -1
  330. package/dist/store/schema/cleo-shared/brain.d.ts +2 -2
  331. package/dist/store/schema/memory-schema.d.ts +3 -3
  332. package/dist/store/schema/memory-schema.d.ts.map +1 -1
  333. package/dist/store/schema/memory-schema.js +2 -0
  334. package/dist/store/schema/memory-schema.js.map +1 -1
  335. package/dist/store/session-store.d.ts +5 -2
  336. package/dist/store/session-store.d.ts.map +1 -1
  337. package/dist/store/session-store.js +10 -5
  338. package/dist/store/session-store.js.map +1 -1
  339. package/dist/store/sqlite-native.d.ts +5 -4
  340. package/dist/store/sqlite-native.d.ts.map +1 -1
  341. package/dist/store/sqlite-native.js +5 -4
  342. package/dist/store/sqlite-native.js.map +1 -1
  343. package/dist/store/sync/merge/rules.d.ts.map +1 -1
  344. package/dist/store/sync/merge/rules.js +4 -3
  345. package/dist/store/sync/merge/rules.js.map +1 -1
  346. package/dist/store/validation-schemas.d.ts +1 -0
  347. package/dist/store/validation-schemas.d.ts.map +1 -1
  348. package/dist/store/vault-manifest.js +1 -1
  349. package/dist/store/vault-manifest.js.map +1 -1
  350. package/dist/system/project-health.d.ts +7 -0
  351. package/dist/system/project-health.d.ts.map +1 -1
  352. package/dist/system/project-health.js +13 -0
  353. package/dist/system/project-health.js.map +1 -1
  354. package/dist/tasks/add.d.ts.map +1 -1
  355. package/dist/tasks/add.js +0 -15
  356. package/dist/tasks/add.js.map +1 -1
  357. package/dist/tasks/affected-packages.d.ts +26 -11
  358. package/dist/tasks/affected-packages.d.ts.map +1 -1
  359. package/dist/tasks/affected-packages.js +54 -17
  360. package/dist/tasks/affected-packages.js.map +1 -1
  361. package/dist/tasks/affected-scope.d.ts +24 -0
  362. package/dist/tasks/affected-scope.d.ts.map +1 -1
  363. package/dist/tasks/affected-scope.js +57 -0
  364. package/dist/tasks/affected-scope.js.map +1 -1
  365. package/dist/tasks/change-set.d.ts +72 -9
  366. package/dist/tasks/change-set.d.ts.map +1 -1
  367. package/dist/tasks/change-set.js +227 -20
  368. package/dist/tasks/change-set.js.map +1 -1
  369. package/dist/tasks/complete-ci.d.ts.map +1 -1
  370. package/dist/tasks/complete-ci.js +2 -0
  371. package/dist/tasks/complete-ci.js.map +1 -1
  372. package/dist/tasks/complete.d.ts +2 -2
  373. package/dist/tasks/complete.d.ts.map +1 -1
  374. package/dist/tasks/complete.js +38 -17
  375. package/dist/tasks/complete.js.map +1 -1
  376. package/dist/tasks/done-plan.d.ts.map +1 -1
  377. package/dist/tasks/done-plan.js +66 -18
  378. package/dist/tasks/done-plan.js.map +1 -1
  379. package/dist/tasks/epic-enforcement.d.ts +39 -1
  380. package/dist/tasks/epic-enforcement.d.ts.map +1 -1
  381. package/dist/tasks/epic-enforcement.js +46 -2
  382. package/dist/tasks/epic-enforcement.js.map +1 -1
  383. package/dist/tasks/evidence.d.ts +74 -22
  384. package/dist/tasks/evidence.d.ts.map +1 -1
  385. package/dist/tasks/evidence.js +221 -35
  386. package/dist/tasks/evidence.js.map +1 -1
  387. package/dist/tasks/gate-runner.d.ts.map +1 -1
  388. package/dist/tasks/gate-runner.js +2 -12
  389. package/dist/tasks/gate-runner.js.map +1 -1
  390. package/dist/tasks/qa-run-binding.d.ts +2 -0
  391. package/dist/tasks/qa-run-binding.d.ts.map +1 -1
  392. package/dist/tasks/qa-run-binding.js +6 -2
  393. package/dist/tasks/qa-run-binding.js.map +1 -1
  394. package/dist/tasks/test-run-binding.d.ts +2 -0
  395. package/dist/tasks/test-run-binding.d.ts.map +1 -1
  396. package/dist/tasks/test-run-binding.js +8 -3
  397. package/dist/tasks/test-run-binding.js.map +1 -1
  398. package/dist/tasks/verification-policy.d.ts +57 -1
  399. package/dist/tasks/verification-policy.d.ts.map +1 -1
  400. package/dist/tasks/verification-policy.js +94 -1
  401. package/dist/tasks/verification-policy.js.map +1 -1
  402. package/dist/upgrade.d.ts.map +1 -1
  403. package/dist/upgrade.js +2 -15
  404. package/dist/upgrade.js.map +1 -1
  405. package/dist/validation/engine-ops.d.ts.map +1 -1
  406. package/dist/validation/engine-ops.js +39 -19
  407. package/dist/validation/engine-ops.js.map +1 -1
  408. package/package.json +13 -13
  409. package/schemas/config.schema.json +69 -0
  410. package/templates/CLEO-INJECTION.md +4 -5
  411. package/templates/CLEO-REFERENCE.md +2 -4
  412. package/templates/git-hooks/post-merge +25 -0
  413. package/dist/memory/dispatch-trace.d.ts +0 -44
  414. package/dist/memory/dispatch-trace.d.ts.map +0 -1
  415. package/dist/memory/dispatch-trace.js +0 -65
  416. package/dist/memory/dispatch-trace.js.map +0 -1
  417. package/dist/memory/retrieval/increment-citation-counts.d.ts +0 -11
  418. package/dist/memory/retrieval/increment-citation-counts.d.ts.map +0 -1
  419. package/dist/memory/retrieval/increment-citation-counts.js +0 -43
  420. package/dist/memory/retrieval/increment-citation-counts.js.map +0 -1
@@ -13,25 +13,33 @@
13
13
  * @task T12316
14
14
  * @module nexus/freshness
15
15
  */
16
- import { existsSync } from 'node:fs';
16
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
17
+ import { join } from 'node:path';
17
18
  import { performance } from 'node:perf_hooks';
18
19
  import { fileURLToPath } from 'node:url';
19
20
  import { getHeapStatistics } from 'node:v8';
20
21
  import { PARSER_WORKER_HEAP_MAX_MB } from '@cleocode/contracts/code-symbol.js';
22
+ import { sql } from 'drizzle-orm';
21
23
  import { getConfigValue } from '../config/registry.js';
22
24
  import { readEnvOverride } from '../config.js';
23
25
  import { pushWarning } from '../output.js';
26
+ import { getCleoDirAbsolute } from '../paths.js';
24
27
  import { getNexusDb } from '../store/nexus-sqlite.js';
25
28
  import { runNexusAnalysis } from './analyze-orchestrator.js';
26
- import { readFileManifest, readRunCost } from './graph-manifest.js';
29
+ import { readActiveRefreshCooldown, readFileManifest, readRunCost, refreshCooldownApplies, writeRefreshCooldown, } from './graph-manifest.js';
30
+ import { envSwitch, measureAgainstBase, missingIndexedPaths, NEXUS_ALLOW_STALE_ENV, } from './index-health.js';
31
+ import { releasePidLock, takePidLock } from './pid-lock.js';
32
+ export { DEFAULT_MAX_COMMITS_BEHIND, measureAgainstBase, NEXUS_ALLOW_STALE_ENV, probeNexusIndexHealth, readNexusStalePolicy, } from './index-health.js';
27
33
  /** Command that refreshes the index; incremental by default since T12315. */
28
34
  export const NEXUS_REFRESH_COMMAND = 'cleo nexus analyze';
29
- /** Default number of stale files an index query may refresh inline. */
35
+ /**
36
+ * Retired file-count cap for an inline refresh (T14014): the size-scaled cost
37
+ * estimate against the budget decides. Kept as the reported default of the
38
+ * ignored `nexus.autoRefresh.maxFiles`.
39
+ */
30
40
  export const DEFAULT_AUTO_REFRESH_MAX_FILES = 25;
31
41
  /** Default wall-clock budget for an inline refresh. */
32
42
  export const DEFAULT_AUTO_REFRESH_BUDGET_MS = 60_000;
33
- /** Share of changed files above which analysis rebuilds fully (mirrors the pipeline). */
34
- const FULL_REBUILD_RATIO = 0.3;
35
43
  /**
36
44
  * Judge the queried symbol's own files against an assessment.
37
45
  * @param assessment - Assessment taken before the query ran.
@@ -49,25 +57,74 @@ export function judgeSymbolFiles(assessment, files) {
49
57
  symbolFile: stale ?? unique[0],
50
58
  };
51
59
  }
52
- /** Estimate a refresh from the last recorded run, or `null` when nothing is recorded. */
53
- function estimateRefreshMs(cost, staleFiles, fileCount) {
60
+ /** Largest factor a recorded run's whole-graph cost is scaled up by (T14014). */
61
+ const MAX_COST_SCALE = 4;
62
+ /**
63
+ * Estimate a refresh from the last recorded run, or `null` when nothing is
64
+ * recorded (T14014).
65
+ *
66
+ * Every refresh re-resolves, re-clusters and republishes the whole graph, so
67
+ * that cost is scaled from the last run's per-file rate to today's file count
68
+ * (at most {@link MAX_COST_SCALE} times, so one small sample cannot vouch for
69
+ * a far larger repository). Files without an exact cache entry for this build
70
+ * are parsed at the last run's parse rate, whatever their staleness: a wiped
71
+ * cache or an extractor upgrade means a full parse. Decoding reused
72
+ * extractions is recorded apart (`reuseDecode`) and never inflates that rate.
73
+ */
74
+ function estimateRefreshMs(cost, toParse, reusable, fileCount) {
54
75
  if (!cost)
55
76
  return null;
56
77
  const parseMs = cost.summary.phaseMs['parse'] ?? 0;
78
+ const decodeMs = cost.summary.phaseMs['reuseDecode'] ?? 0;
57
79
  const parsed = cost.summary.parsedFiles;
58
- const fixedMs = Math.max(0, cost.durationMs - parseMs);
80
+ const resolved = Math.max(1, cost.summary.resolvedFiles);
81
+ const wholeGraphMs = Math.max(0, cost.durationMs - parseMs - decodeMs);
82
+ const scale = Math.max(1, fileCount / resolved);
83
+ if (scale > MAX_COST_SCALE)
84
+ return null;
59
85
  const perFileMs = parsed > 0 ? parseMs / parsed : 0;
60
- const full = fileCount > 0 && staleFiles / fileCount > FULL_REBUILD_RATIO;
61
- const reparsed = full ? Math.max(parsed, cost.summary.resolvedFiles) : staleFiles;
62
- return Math.round(fixedMs + reparsed * perFileMs);
86
+ const perReusedMs = cost.summary.reusedFiles > 0 ? decodeMs / cost.summary.reusedFiles : 0;
87
+ return Math.round(wholeGraphMs * scale + toParse * perFileMs + reusable * perReusedMs);
88
+ }
89
+ /**
90
+ * Split the parseable files of the manifest (T14014) into those a refresh must
91
+ * parse (stale, or without an exact cache entry for this build) and those it
92
+ * can reuse (unchanged, same content hash, this build's extractor fingerprint).
93
+ * Files no extractor parses are neither.
94
+ */
95
+ async function planParse(db, manifestFiles, stale) {
96
+ const { computeExtractorFingerprint, isParseablePath, parseCacheFingerprint } = await import('@cleocode/nexus/pipeline');
97
+ // The inline refresh parses under the default limits (T14016).
98
+ const fingerprint = parseCacheFingerprint(computeExtractorFingerprint());
99
+ const cached = new Map();
100
+ for (const [path, contentHash, entryFingerprint] of db.values(sql `SELECT path, content_hash, fingerprint FROM main._nexus_parse_cache`)) {
101
+ if (entryFingerprint === fingerprint &&
102
+ typeof path === 'string' &&
103
+ typeof contentHash === 'string')
104
+ cached.set(path, contentHash);
105
+ }
106
+ let reusable = 0;
107
+ let toParse = 0;
108
+ for (const [path, , , contentHash] of manifestFiles) {
109
+ if (!isParseablePath(path))
110
+ continue;
111
+ if (!stale.has(path) && cached.get(path) === contentHash)
112
+ reusable++;
113
+ else
114
+ toParse++;
115
+ }
116
+ // A new parseable file is stale but not yet in the manifest.
117
+ const known = new Set(manifestFiles.map(([path]) => path));
118
+ for (const path of stale)
119
+ if (isParseablePath(path) && !known.has(path))
120
+ toParse++;
121
+ return { toParse, reusable };
63
122
  }
64
123
  /** Render an estimate for humans and agents alike. */
65
- function describeEstimate(ms, staleFiles, fileCount) {
124
+ function describeEstimate(ms, staleFiles, toParse) {
66
125
  if (staleFiles === 0)
67
126
  return 'none needed';
68
- const kind = fileCount > 0 && staleFiles / fileCount > FULL_REBUILD_RATIO
69
- ? 'full rebuild'
70
- : `incremental, re-parses ~${staleFiles} file(s)`;
127
+ const kind = `re-parses ~${toParse} file(s), re-resolves the whole graph`;
71
128
  if (ms === null)
72
129
  return `unknown duration (${kind}; no previous analysis cost recorded)`;
73
130
  return `~${Math.max(1, Math.round(ms / 1000))}s (${kind})`;
@@ -146,6 +203,7 @@ async function assessWithStaleSet(projectRoot, sampleLimit = 10) {
146
203
  // T12474: an unreachable source root is an unassessable index, never a crash.
147
204
  const freshness = {
148
205
  ...base,
206
+ ...(await measureAgainstBase(db, manifest.sourceRoot, manifest.assessedAt)),
149
207
  indexed: true,
150
208
  status: 'unknown',
151
209
  lastIndexedAt: manifest.assessedAt,
@@ -165,23 +223,97 @@ async function assessWithStaleSet(projectRoot, sampleLimit = 10) {
165
223
  if (known.get(file.path)?.contentHash !== file.contentHash)
166
224
  stale.add(file.path);
167
225
  }
168
- for (const path of known.keys())
169
- if (!seen.has(path))
170
- stale.add(path);
226
+ const unseen = [...known.keys()].filter((path) => !seen.has(path));
227
+ for (const path of unseen)
228
+ stale.add(path);
229
+ // T14021: unseen paths can be excluded files still on disk; only absent ones are missing.
230
+ const missing = missingIndexedPaths(manifest.sourceRoot, unseen);
171
231
  const fileCount = manifest.files.length;
172
- const estimate = estimateRefreshMs(readRunCost(db), stale.size, fileCount);
232
+ const { toParse, reusable } = stale.size > 0 ? await planParse(db, manifest.files, stale) : { toParse: 0, reusable: 0 };
233
+ const cost = readRunCost(db);
234
+ const estimate = estimateRefreshMs(cost, toParse, reusable, fileCount);
173
235
  const freshness = {
174
236
  ...base,
237
+ ...(await measureAgainstBase(db, manifest.sourceRoot, manifest.assessedAt)),
175
238
  indexed: true,
176
239
  status: stale.size === 0 ? 'fresh' : 'stale',
177
240
  lastIndexedAt: manifest.assessedAt,
178
241
  fileCount,
179
242
  staleFileCount: stale.size,
180
243
  stalePaths: [...stale].sort().slice(0, sampleLimit),
181
- refreshEstimate: describeEstimate(estimate, stale.size, fileCount),
244
+ missingFileCount: missing.length,
245
+ missingPaths: missing.slice(0, sampleLimit),
246
+ ...(cost?.summary.communitiesSkipped
247
+ ? { communitiesSkipped: cost.summary.communitiesSkipped }
248
+ : {}),
249
+ refreshEstimate: describeEstimate(estimate, stale.size, toParse),
182
250
  checkMs: Math.round(performance.now() - started),
183
251
  };
184
- return { freshness, staleFiles: stale };
252
+ return { freshness, staleFiles: stale, refreshPlan: { toParse, reusable, estimateMs: estimate } };
253
+ }
254
+ /**
255
+ * The freshness block every index-sensitive answer carries as `data.freshness`.
256
+ *
257
+ * @param freshness - The assessment behind the answer.
258
+ * @returns Revision, base, commits behind, age and verdict.
259
+ */
260
+ export function answerFreshness(freshness) {
261
+ return {
262
+ status: freshness.status,
263
+ indexedRevision: freshness.indexedRevision ?? null,
264
+ baseRef: freshness.baseRef ?? null,
265
+ commitsBehind: freshness.commitsBehind ?? null,
266
+ commitsBehindHead: freshness.commitsBehindHead ?? null,
267
+ changedFilesSinceIndex: freshness.changedFilesSinceIndex ?? null,
268
+ refreshCooldownUntil: freshness.refreshCooldownUntil ?? null,
269
+ ageDays: freshness.ageDays ?? null,
270
+ lastIndexedAt: freshness.lastIndexedAt,
271
+ missingFileCount: freshness.missingFileCount ?? null,
272
+ missingPaths: freshness.missingPaths ?? [],
273
+ communitiesSkipped: freshness.communitiesSkipped ?? null,
274
+ };
275
+ }
276
+ /**
277
+ * Whether a graph answer must be refused as stale: the index trails the
278
+ * checked-out HEAD by more than the policy allows, or the target is missing
279
+ * from an index that is stale for this checkout (commits behind HEAD or
280
+ * changed files), so it may exist in code the index never saw. Distance to the
281
+ * base branch (`commitsBehind`) is reported only: on a feature branch or
282
+ * worktree it grows as the base moves, and no reindex here could clear it.
283
+ *
284
+ * @param freshness - The assessment behind the answer.
285
+ * @param policy - The stale-answer policy.
286
+ * @param options - `targetMissing` when the query found no symbol; `projectRoot`
287
+ * names the checkout in the reindex command.
288
+ * @returns The refusal, or `null` when the answer may stand.
289
+ * @example
290
+ * ```ts
291
+ * const refusal = staleIndexRefusal(freshness, policy, { projectRoot: root });
292
+ * if (refusal) return refusalEnvelope(refusal);
293
+ * ```
294
+ */
295
+ export function staleIndexRefusal(freshness, policy, options = {}) {
296
+ if (policy.allowStale)
297
+ return null;
298
+ // Judged against what is checked out: a reindex here clears it on any branch
299
+ // or worktree. Distance to the base branch is reported, never refused on.
300
+ const behind = freshness.commitsBehindHead ?? null;
301
+ const tooFar = behind !== null && behind > policy.maxCommitsBehind;
302
+ const staleHere = freshness.status === 'stale' || (behind !== null && behind > 0);
303
+ if (!tooFar && !(options.targetMissing && staleHere))
304
+ return null;
305
+ const reason = tooFar
306
+ ? `The index is ${behind} commit(s) behind the checked-out HEAD (more than nexus.freshness.maxCommitsBehind = ${policy.maxCommitsBehind})`
307
+ : `The symbol is missing from an index that is stale for this checkout (${behind ?? 0} commit(s) behind HEAD, ${freshness.staleFileCount} changed file(s)); it may exist in code the index has not seen`;
308
+ const reindex = options.projectRoot
309
+ ? `${NEXUS_REFRESH_COMMAND} ${JSON.stringify(options.projectRoot)}`
310
+ : NEXUS_REFRESH_COMMAND;
311
+ return {
312
+ code: 'E_NEXUS_INDEX_STALE',
313
+ message: `${reason}; this answer would describe code that has changed.`,
314
+ fix: `${reindex} (then repeat the query); to accept the stale answer, pass --allow-stale (nexus impact, nexus context) or set ${NEXUS_ALLOW_STALE_ENV}=1`,
315
+ details: { ...answerFreshness(freshness), maxCommitsBehind: policy.maxCommitsBehind },
316
+ };
185
317
  }
186
318
  /**
187
319
  * Read the auto-refresh policy from `nexus.autoRefresh.*`, applying defaults.
@@ -210,15 +342,6 @@ export async function readNexusAutoRefreshPolicy(projectRoot) {
210
342
  return { ...policyFrom(false, maxFiles, budgetMs), disabledBy: 'CLEO_NEXUS_AUTO_REFRESH' };
211
343
  return policyFrom(enabled, maxFiles, budgetMs);
212
344
  }
213
- /** Read an on/off env value (`true`/`false`, `1`/`0`, `yes`/`no`, `on`/`off`); `undefined` otherwise. */
214
- function envSwitch(value) {
215
- const text = String(value).trim().toLowerCase();
216
- if (['true', '1', 'yes', 'on'].includes(text))
217
- return true;
218
- if (['false', '0', 'no', 'off'].includes(text))
219
- return false;
220
- return undefined;
221
- }
222
345
  /** Apply defaults to raw `nexus.autoRefresh.*` values. */
223
346
  function policyFrom(enabled, maxFiles, budgetMs) {
224
347
  return {
@@ -232,8 +355,8 @@ function policyFrom(enabled, maxFiles, budgetMs) {
232
355
  };
233
356
  }
234
357
  /**
235
- * Run a refresh in this thread, incrementally or not at all. Unbounded in
236
- * memory: {@link runRefreshInChild} runs it inside a heap-limited child process.
358
+ * Run a refresh in this thread, bounded by its budget. Unbounded in memory:
359
+ * {@link runRefreshInChild} runs it inside a heap-limited child process.
237
360
  *
238
361
  * @param input - The refresh to run.
239
362
  * @returns The refresh summary.
@@ -244,7 +367,8 @@ export async function runRefreshInProcess(input) {
244
367
  repoPath: input.repoPath,
245
368
  ...(input.includedRepositories ? { includedRepositories: input.includedRepositories } : {}),
246
369
  parserLimits: { signal: AbortSignal.timeout(input.budgetMs) },
247
- requireIncremental: true,
370
+ // T14016: the same budget, as a deadline uninterruptible phases can check.
371
+ deadlineAt: Date.now() + input.budgetMs,
248
372
  });
249
373
  return { mode: summary.mode, reason: summary.reason };
250
374
  }
@@ -311,89 +435,235 @@ export async function runRefreshInChild(input, options = {}) {
311
435
  await handle.stop();
312
436
  }
313
437
  }
438
+ /** Cooldown after an inline refresh fails or is aborted (T14014). */
439
+ export const INLINE_REFRESH_COOLDOWN_MS = 30 * 60_000;
440
+ /** A refresh parsing more than this many files (or 30% of them) needs `cleo run` admission. */
441
+ const ADMISSION_FREE_PARSE_FILES = 25;
442
+ /** Share of files above which a refresh needs `cleo run` admission. */
443
+ const ADMISSION_FREE_PARSE_RATIO = 0.3;
444
+ /**
445
+ * Indexed files above which every inline refresh needs `cleo run` admission
446
+ * (T14019): resolve, communities, flows and the republish run over the whole
447
+ * graph, so on a large repository they dominate memory even when one file is
448
+ * re-parsed.
449
+ */
450
+ export const ADMISSION_GRAPH_FILES = 2_000;
451
+ /** Admission footprint per indexed file of an inline refresh (T14019). */
452
+ const REFRESH_BYTES_PER_INDEXED_FILE = 256 * 1024;
453
+ /** Smallest admission footprint of an inline refresh (T14019). */
454
+ const REFRESH_MIN_FOOTPRINT_BYTES = 256 * 1024 * 1024;
455
+ /**
456
+ * The memory an inline refresh asks `cleo run` admission for (T14019): scaled
457
+ * to the graph it re-resolves (indexed files), at least
458
+ * {@link REFRESH_MIN_FOOTPRINT_BYTES}, at most the build class's footprint.
459
+ *
460
+ * @param indexedFiles - Files in the published generation.
461
+ * @param classBytes - Footprint of the `scoped-build` class on this machine.
462
+ * @returns Footprint in bytes.
463
+ * @example
464
+ * ```ts
465
+ * inlineRefreshFootprintBytes(10_000, footprintForClass('scoped-build'));
466
+ * ```
467
+ */
468
+ export function inlineRefreshFootprintBytes(indexedFiles, classBytes) {
469
+ const scaled = Math.max(0, indexedFiles) * REFRESH_BYTES_PER_INDEXED_FILE;
470
+ return Math.min(classBytes, Math.max(REFRESH_MIN_FOOTPRINT_BYTES, scaled));
471
+ }
472
+ /** Fallback cooldown file under `.cleo/` when `_nexus_meta` cannot be written (T14014). */
473
+ export const INLINE_REFRESH_COOLDOWN_FILE = 'nexus-refresh-cooldown.json';
474
+ /** Last resort when neither the store nor the file can hold a cooldown. */
475
+ let inProcessCooldown = null;
314
476
  /**
315
- * Assess freshness and, when only a few files are stale, refresh the index
477
+ * Record a cooldown: in `_nexus_meta`, else (SQLITE_BUSY while another
478
+ * analysis publishes, a read-only store) in a file under `.cleo/`, else in
479
+ * this process. Never throws: a failed write must not fail the query or lose
480
+ * the cooldown.
481
+ */
482
+ function recordCooldown(db, cleoDir, cooldown) {
483
+ try {
484
+ writeRefreshCooldown(db, cooldown);
485
+ return;
486
+ }
487
+ catch {
488
+ // fall through to the file
489
+ }
490
+ try {
491
+ mkdirSync(cleoDir, { recursive: true });
492
+ writeFileSync(join(cleoDir, INLINE_REFRESH_COOLDOWN_FILE), JSON.stringify(cooldown));
493
+ }
494
+ catch {
495
+ inProcessCooldown = cooldown;
496
+ }
497
+ }
498
+ /** The cooldown in force from the store, its fallback file, or this process. */
499
+ function activeCooldown(db, cleoDir) {
500
+ const applies = (cooldown) => {
501
+ try {
502
+ return cooldown && refreshCooldownApplies(db, cooldown) ? cooldown : null;
503
+ }
504
+ catch {
505
+ return null;
506
+ }
507
+ };
508
+ let stored = null;
509
+ try {
510
+ stored = readActiveRefreshCooldown(db);
511
+ }
512
+ catch {
513
+ stored = null;
514
+ }
515
+ if (stored)
516
+ return stored;
517
+ let filed = null;
518
+ try {
519
+ filed = JSON.parse(readFileSync(join(cleoDir, INLINE_REFRESH_COOLDOWN_FILE), 'utf-8'));
520
+ }
521
+ catch {
522
+ filed = null;
523
+ }
524
+ return applies(filed) ?? applies(inProcessCooldown);
525
+ }
526
+ /** Lock file under `.cleo/`: one inline refresh at a time per project. */
527
+ export const INLINE_REFRESH_LOCK_FILE = 'nexus-inline-refresh.lock';
528
+ /** The production admitter: one non-waiting `cleo run` admission pass, sized to the graph. */
529
+ const admitRefresh = async (projectRoot, indexedFiles) => {
530
+ const { admit, footprintForClass } = await import('../resources/admission-ledger.js');
531
+ const outcome = await admit({
532
+ label: 'nexus:inline-refresh',
533
+ footprintBytes: inlineRefreshFootprintBytes(indexedFiles, footprintForClass('scoped-build')),
534
+ cwd: projectRoot,
535
+ footprintReason: `inline NEXUS refresh re-resolving a ${indexedFiles}-file graph (T14014, T14019)`,
536
+ }, { wait: false, timeoutMs: 0 });
537
+ return outcome.admitted ? outcome.grant : { refused: outcome.refusal.reason };
538
+ };
539
+ /**
540
+ * Assess freshness and, when the refresh is affordable, refresh the index
316
541
  * inline before the caller answers from it.
317
542
  *
318
- * A refresh runs only when `0 < stale ≤ maxFiles` and the recorded cost
319
- * estimate fits `budgetMs`; the budget also cancels the run at its next phase
320
- * boundary, and publication is atomic, so an abandoned refresh leaves the
321
- * previous graph intact. Every decision — refreshed, skipped or failed — is
322
- * disclosed in `autoRefresh`, and the returned freshness describes the index
323
- * the caller is about to read.
543
+ * A refresh runs when the recorded cost, scaled to the repository and to the
544
+ * files it must parse (T14014), fits `budgetMs`. It is guarded so a stale
545
+ * index never turns every query into another heavy run:
546
+ * - one inline refresh at a time per project (a pid lock; a concurrent query
547
+ * answers from the stale index);
548
+ * - a refresh beyond a small incremental set needs a non-waiting `cleo run`
549
+ * admission, and is skipped when the machine is busy;
550
+ * - a refresh that fails or is aborted sets a cooldown: no inline refresh for
551
+ * {@link INLINE_REFRESH_COOLDOWN_MS}, or until an analysis publishes a new
552
+ * generation. The cooldown is reported as `refreshCooldownUntil`.
553
+ * The budget also cancels a run at its next check, and publication is atomic,
554
+ * so an abandoned refresh leaves the previous graph intact. Every decision is
555
+ * disclosed in `autoRefresh`.
324
556
  *
325
557
  * @param projectRoot - Project whose graph is about to be queried.
326
558
  * @param policy - Explicit policy; read from config when omitted.
327
559
  * @param runRefresh - Runs the refresh; a heap-bounded worker unless supplied (T13537).
560
+ * @param admitter - Admits a heavy refresh; `cleo run` admission unless supplied.
328
561
  * @returns Freshness of the index as it stands after any refresh, with its stale set.
329
562
  */
330
- export async function ensureNexusIndexFresh(projectRoot, policy, runRefresh = runRefreshInChild) {
563
+ export async function ensureNexusIndexFresh(projectRoot, policy, runRefresh = runRefreshInChild, admitter = admitRefresh) {
331
564
  const initial = await assessWithStaleSet(projectRoot);
332
565
  const before = initial.freshness;
333
566
  if (before.status !== 'stale')
334
567
  return initial;
335
568
  const effective = policy ?? (await readNexusAutoRefreshPolicy(projectRoot));
336
- const skip = (reason) => ({
569
+ const skip = (reason, cooldownUntil) => ({
337
570
  staleFiles: initial.staleFiles,
571
+ ...(initial.refreshPlan ? { refreshPlan: initial.refreshPlan } : {}),
338
572
  freshness: {
339
573
  ...before,
574
+ ...(cooldownUntil ? { refreshCooldownUntil: cooldownUntil } : {}),
340
575
  autoRefresh: { refreshed: false, staleFiles: before.staleFileCount, durationMs: 0, reason },
341
576
  },
342
577
  });
343
578
  if (!effective.enabled)
344
579
  return skip(`auto-refresh is disabled (${effective.disabledBy ?? 'nexus.autoRefresh.enabled = false'}); answered from the stale index`);
345
- if (before.staleFileCount > effective.maxFiles)
346
- return skip(`${before.staleFileCount} stale files exceed nexus.autoRefresh.maxFiles = ${effective.maxFiles}; ` +
347
- 'answered from the stale index');
348
- // T13523: a read query never runs a full rebuild inline; the pipeline also
349
- // refuses one it would fall back to (requireIncremental below).
350
- if (before.fileCount > 0 && before.staleFileCount / before.fileCount > FULL_REBUILD_RATIO)
351
- return skip(`${before.staleFileCount} of ${before.fileCount} files are stale, which needs a full rebuild; answered from the stale index (run ${NEXUS_REFRESH_COMMAND})`);
352
580
  const db = await getNexusDb(projectRoot);
353
- const estimate = estimateRefreshMs(readRunCost(db), before.staleFileCount, before.fileCount);
354
- // T13523: with no recorded cost there is no bound on the run (a full
355
- // analysis, Leiden included, can exhaust the CLI heap), so do not start one.
581
+ const cleoDir = getCleoDirAbsolute(projectRoot);
582
+ const cooldown = activeCooldown(db, cleoDir);
583
+ if (cooldown)
584
+ return skip(`a recent inline refresh did not finish (${cooldown.reason}); no inline refresh until ${cooldown.until} or the next ${NEXUS_REFRESH_COMMAND}; answered from the stale index`, cooldown.until);
585
+ const plan = initial.refreshPlan;
586
+ const estimate = plan?.estimateMs ?? null;
587
+ // T13523: with no comparable recorded cost there is no bound on the run, so
588
+ // do not start one.
356
589
  if (estimate === null)
357
- return skip(`no previous analysis cost recorded, so the refresh cannot be bounded; answered from the stale index (run ${NEXUS_REFRESH_COMMAND} to refresh and record one)`);
590
+ return skip(`no previous analysis cost recorded (or the repository has grown more than ${MAX_COST_SCALE}x since), so the refresh cannot be bounded; answered from the stale index (run ${NEXUS_REFRESH_COMMAND} to refresh and record one)`);
358
591
  if (estimate > effective.budgetMs)
359
592
  return skip(`estimated refresh ${Math.round(estimate / 1000)}s exceeds nexus.autoRefresh.budgetMs = ` +
360
593
  `${effective.budgetMs}; answered from the stale index`);
361
- const manifest = readFileManifest(db, projectRoot);
362
- const started = performance.now();
594
+ mkdirSync(cleoDir, { recursive: true });
595
+ const lockPath = join(cleoDir, INLINE_REFRESH_LOCK_FILE);
596
+ const holder = takePidLock(lockPath);
597
+ if (holder !== null)
598
+ return skip(`an inline refresh is already running${holder > 0 ? ` (pid ${holder})` : ''}; answered from the stale index`);
599
+ let grant = null;
363
600
  try {
364
- const summary = await runRefresh({
365
- projectRoot,
366
- repoPath: manifest?.sourceRoot ?? projectRoot,
367
- ...(manifest ? { includedRepositories: manifest.includedRepositories } : {}),
368
- budgetMs: effective.budgetMs,
369
- });
370
- const after = await assessWithStaleSet(projectRoot);
371
- return {
372
- staleFiles: after.staleFiles,
373
- freshness: {
374
- ...after.freshness,
375
- autoRefresh: {
376
- refreshed: summary.mode !== 'unchanged',
377
- staleFiles: before.staleFileCount,
378
- durationMs: Math.round(performance.now() - started),
379
- reason: `${summary.mode}: ${summary.reason}`,
601
+ const toParse = plan?.toParse ?? before.staleFileCount;
602
+ // T14019: the whole-graph phases make a large graph heavy even for one file.
603
+ const largeGraph = before.fileCount > (effective.admissionGraphFiles ?? ADMISSION_GRAPH_FILES);
604
+ const heavy = largeGraph ||
605
+ toParse > ADMISSION_FREE_PARSE_FILES ||
606
+ (before.fileCount > 0 && toParse / before.fileCount > ADMISSION_FREE_PARSE_RATIO);
607
+ if (heavy) {
608
+ const admitted = await admitter(projectRoot, before.fileCount);
609
+ if ('refused' in admitted)
610
+ return skip(`a refresh ${largeGraph ? `re-resolving a ${before.fileCount}-file graph` : `parsing ~${toParse} file(s)`} needs cleo run admission and the machine is busy (${admitted.refused}); answered from the stale index`);
611
+ grant = admitted;
612
+ }
613
+ const manifest = readFileManifest(db, projectRoot);
614
+ const started = performance.now();
615
+ const generation = db.values(sql `SELECT value FROM main._nexus_meta WHERE key = 'graph_generation'`)[0]?.[0];
616
+ try {
617
+ const summary = await runRefresh({
618
+ projectRoot,
619
+ repoPath: manifest?.sourceRoot ?? projectRoot,
620
+ ...(manifest ? { includedRepositories: manifest.includedRepositories } : {}),
621
+ budgetMs: effective.budgetMs,
622
+ });
623
+ const after = await assessWithStaleSet(projectRoot);
624
+ return {
625
+ staleFiles: after.staleFiles,
626
+ ...(after.refreshPlan ? { refreshPlan: after.refreshPlan } : {}),
627
+ freshness: {
628
+ ...after.freshness,
629
+ autoRefresh: {
630
+ refreshed: summary.mode !== 'unchanged',
631
+ staleFiles: before.staleFileCount,
632
+ durationMs: Math.round(performance.now() - started),
633
+ reason: `${summary.mode}: ${summary.reason}`,
634
+ },
380
635
  },
381
- },
382
- };
383
- }
384
- catch (error) {
385
- return {
386
- staleFiles: initial.staleFiles,
387
- freshness: {
388
- ...before,
389
- autoRefresh: {
390
- refreshed: false,
391
- staleFiles: before.staleFileCount,
392
- durationMs: Math.round(performance.now() - started),
393
- reason: `refresh failed, previous graph retained: ${error instanceof Error ? error.message : String(error)}`,
636
+ };
637
+ }
638
+ catch (error) {
639
+ const message = error instanceof Error ? error.message : String(error);
640
+ // T14014: an aborted or failed refresh records no cost, so without a
641
+ // cooldown every query would start another one.
642
+ const until = new Date(Date.now() + INLINE_REFRESH_COOLDOWN_MS).toISOString();
643
+ recordCooldown(db, cleoDir, {
644
+ until,
645
+ generation: typeof generation === 'string' ? generation : null,
646
+ reason: message.slice(0, 300),
647
+ });
648
+ return {
649
+ staleFiles: initial.staleFiles,
650
+ ...(initial.refreshPlan ? { refreshPlan: initial.refreshPlan } : {}),
651
+ freshness: {
652
+ ...before,
653
+ refreshCooldownUntil: until,
654
+ autoRefresh: {
655
+ refreshed: false,
656
+ staleFiles: before.staleFileCount,
657
+ durationMs: Math.round(performance.now() - started),
658
+ reason: `refresh failed, previous graph retained: ${message}; inline refresh paused until ${until}`,
659
+ },
394
660
  },
395
- },
396
- };
661
+ };
662
+ }
663
+ }
664
+ finally {
665
+ await grant?.release();
666
+ releasePidLock(lockPath);
397
667
  }
398
668
  }
399
669
  /**
@@ -497,6 +767,24 @@ export function discloseNexusFreshness(subject, freshness) {
497
767
  message: `Refreshed the index inline before answering (${refresh.staleFiles} stale file(s), ${refresh.durationMs}ms): ${refresh.reason}`,
498
768
  });
499
769
  }
770
+ const missing = freshness.missingFileCount ?? 0;
771
+ if (missing > 0) {
772
+ pushWarning({
773
+ code: 'W_NEXUS_INDEX_MISSING_PATHS',
774
+ severity: 'warn',
775
+ message: `${subject} answered from an index that still holds ${missing} file(s) no longer on disk ` +
776
+ `(e.g. ${(freshness.missingPaths ?? []).slice(0, 3).join(', ')}); results may cite deleted code. ` +
777
+ `Refresh: ${freshness.refreshCommand}.`,
778
+ });
779
+ }
780
+ if (freshness.communitiesSkipped) {
781
+ pushWarning({
782
+ code: 'W_NEXUS_COMMUNITIES_SKIPPED',
783
+ severity: 'warn',
784
+ message: `${subject} answered from an index with no communities: ${freshness.communitiesSkipped}. ` +
785
+ `Community-based results are missing until ${freshness.refreshCommand} runs without a budget.`,
786
+ });
787
+ }
500
788
  if (freshness.status === 'fresh')
501
789
  return;
502
790
  const symbol = freshness.symbolFileStale === true