clinkx 0.3.0 → 1.0.0

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 (555) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/LICENSE +10 -0
  3. package/README.md +82 -26
  4. package/clinkx-workflows/dist/allowed-roots.d.ts +1 -0
  5. package/clinkx-workflows/dist/allowed-roots.js +5 -0
  6. package/clinkx-workflows/dist/allowed-roots.js.map +1 -0
  7. package/clinkx-workflows/dist/artifacts.d.ts +20 -1
  8. package/clinkx-workflows/dist/artifacts.js +38 -9
  9. package/clinkx-workflows/dist/artifacts.js.map +1 -1
  10. package/clinkx-workflows/dist/backend.d.ts +4 -0
  11. package/clinkx-workflows/dist/citation-check.d.ts +40 -0
  12. package/clinkx-workflows/dist/citation-check.js +348 -0
  13. package/clinkx-workflows/dist/citation-check.js.map +1 -0
  14. package/clinkx-workflows/dist/clink-client.d.ts +2 -1
  15. package/clinkx-workflows/dist/clink-client.js +17 -4
  16. package/clinkx-workflows/dist/clink-client.js.map +1 -1
  17. package/clinkx-workflows/dist/config.d.ts +145 -16
  18. package/clinkx-workflows/dist/config.js +384 -26
  19. package/clinkx-workflows/dist/config.js.map +1 -1
  20. package/clinkx-workflows/dist/contained-file.d.ts +172 -0
  21. package/clinkx-workflows/dist/contained-file.js +483 -0
  22. package/clinkx-workflows/dist/contained-file.js.map +1 -0
  23. package/clinkx-workflows/dist/context-builder-lifecycle-contract.d.ts +416 -0
  24. package/clinkx-workflows/dist/context-builder-lifecycle-contract.js +328 -0
  25. package/clinkx-workflows/dist/context-builder-lifecycle-contract.js.map +1 -0
  26. package/clinkx-workflows/dist/context-builder-lifecycle-router.d.ts +24 -0
  27. package/clinkx-workflows/dist/context-builder-lifecycle-router.js +39 -0
  28. package/clinkx-workflows/dist/context-builder-lifecycle-router.js.map +1 -0
  29. package/clinkx-workflows/dist/context-cache-key.d.ts +53 -0
  30. package/clinkx-workflows/dist/context-cache-key.js +91 -0
  31. package/clinkx-workflows/dist/context-cache-key.js.map +1 -0
  32. package/clinkx-workflows/dist/context-mode-resolver.d.ts +27 -0
  33. package/clinkx-workflows/dist/context-mode-resolver.js +126 -0
  34. package/clinkx-workflows/dist/context-mode-resolver.js.map +1 -0
  35. package/clinkx-workflows/dist/context-preflight.d.ts +184 -0
  36. package/clinkx-workflows/dist/context-preflight.js +91 -0
  37. package/clinkx-workflows/dist/context-preflight.js.map +1 -0
  38. package/clinkx-workflows/dist/council-expander.d.ts +1 -1
  39. package/clinkx-workflows/dist/council-expander.js +2 -1
  40. package/clinkx-workflows/dist/council-expander.js.map +1 -1
  41. package/clinkx-workflows/dist/dashboard/activity-contract.d.ts +165 -0
  42. package/clinkx-workflows/dist/dashboard/activity-contract.js +105 -0
  43. package/clinkx-workflows/dist/dashboard/activity-contract.js.map +1 -0
  44. package/clinkx-workflows/dist/dashboard/activity-lease.d.ts +25 -0
  45. package/clinkx-workflows/dist/dashboard/activity-lease.js +34 -0
  46. package/clinkx-workflows/dist/dashboard/activity-lease.js.map +1 -0
  47. package/clinkx-workflows/dist/dashboard/activity-reader.d.ts +86 -0
  48. package/clinkx-workflows/dist/dashboard/activity-reader.js +480 -0
  49. package/clinkx-workflows/dist/dashboard/activity-reader.js.map +1 -0
  50. package/clinkx-workflows/dist/dashboard/api-routes.d.ts +36 -0
  51. package/clinkx-workflows/dist/dashboard/api-routes.js +1047 -0
  52. package/clinkx-workflows/dist/dashboard/api-routes.js.map +1 -0
  53. package/clinkx-workflows/dist/dashboard/artifact-index.d.ts +37 -0
  54. package/clinkx-workflows/dist/dashboard/artifact-index.js +145 -0
  55. package/clinkx-workflows/dist/dashboard/artifact-index.js.map +1 -0
  56. package/clinkx-workflows/dist/dashboard/auth/challenge-store.d.ts +31 -0
  57. package/clinkx-workflows/dist/dashboard/auth/challenge-store.js +65 -0
  58. package/clinkx-workflows/dist/dashboard/auth/challenge-store.js.map +1 -0
  59. package/clinkx-workflows/dist/dashboard/auth/controller.d.ts +26 -0
  60. package/clinkx-workflows/dist/dashboard/auth/controller.js +596 -0
  61. package/clinkx-workflows/dist/dashboard/auth/controller.js.map +1 -0
  62. package/clinkx-workflows/dist/dashboard/auth/credential-store.d.ts +60 -0
  63. package/clinkx-workflows/dist/dashboard/auth/credential-store.js +776 -0
  64. package/clinkx-workflows/dist/dashboard/auth/credential-store.js.map +1 -0
  65. package/clinkx-workflows/dist/dashboard/auth/enrollment-gate.d.ts +9 -0
  66. package/clinkx-workflows/dist/dashboard/auth/enrollment-gate.js +44 -0
  67. package/clinkx-workflows/dist/dashboard/auth/enrollment-gate.js.map +1 -0
  68. package/clinkx-workflows/dist/dashboard/auth/session-store.d.ts +17 -0
  69. package/clinkx-workflows/dist/dashboard/auth/session-store.js +77 -0
  70. package/clinkx-workflows/dist/dashboard/auth/session-store.js.map +1 -0
  71. package/clinkx-workflows/dist/dashboard/auth/webauthn-service.d.ts +25 -0
  72. package/clinkx-workflows/dist/dashboard/auth/webauthn-service.js +71 -0
  73. package/clinkx-workflows/dist/dashboard/auth/webauthn-service.js.map +1 -0
  74. package/clinkx-workflows/dist/dashboard/context-run-liveness.d.ts +37 -0
  75. package/clinkx-workflows/dist/dashboard/context-run-liveness.js +49 -0
  76. package/clinkx-workflows/dist/dashboard/context-run-liveness.js.map +1 -0
  77. package/clinkx-workflows/dist/dashboard/context-run-reader.d.ts +222 -0
  78. package/clinkx-workflows/dist/dashboard/context-run-reader.js +1178 -0
  79. package/clinkx-workflows/dist/dashboard/context-run-reader.js.map +1 -0
  80. package/clinkx-workflows/dist/dashboard/contracts.d.ts +8936 -0
  81. package/clinkx-workflows/dist/dashboard/contracts.js +1157 -0
  82. package/clinkx-workflows/dist/dashboard/contracts.js.map +1 -0
  83. package/clinkx-workflows/dist/dashboard/generation.d.ts +14 -0
  84. package/clinkx-workflows/dist/dashboard/generation.js +41 -0
  85. package/clinkx-workflows/dist/dashboard/generation.js.map +1 -0
  86. package/clinkx-workflows/dist/dashboard/index.d.ts +28 -0
  87. package/clinkx-workflows/dist/dashboard/index.js +16 -0
  88. package/clinkx-workflows/dist/dashboard/index.js.map +1 -0
  89. package/clinkx-workflows/dist/dashboard/lazy-content.d.ts +101 -0
  90. package/clinkx-workflows/dist/dashboard/lazy-content.js +748 -0
  91. package/clinkx-workflows/dist/dashboard/lazy-content.js.map +1 -0
  92. package/clinkx-workflows/dist/dashboard/progress-report-contract.d.ts +303 -0
  93. package/clinkx-workflows/dist/dashboard/progress-report-contract.js +221 -0
  94. package/clinkx-workflows/dist/dashboard/progress-report-contract.js.map +1 -0
  95. package/clinkx-workflows/dist/dashboard/progress-report-reader.d.ts +50 -0
  96. package/clinkx-workflows/dist/dashboard/progress-report-reader.js +581 -0
  97. package/clinkx-workflows/dist/dashboard/progress-report-reader.js.map +1 -0
  98. package/clinkx-workflows/dist/dashboard/progress-report-status-overlay.d.ts +89 -0
  99. package/clinkx-workflows/dist/dashboard/progress-report-status-overlay.js +71 -0
  100. package/clinkx-workflows/dist/dashboard/progress-report-status-overlay.js.map +1 -0
  101. package/clinkx-workflows/dist/dashboard/progress-report-status-store.d.ts +28 -0
  102. package/clinkx-workflows/dist/dashboard/progress-report-status-store.js +110 -0
  103. package/clinkx-workflows/dist/dashboard/progress-report-status-store.js.map +1 -0
  104. package/clinkx-workflows/dist/dashboard/read-model.d.ts +86 -0
  105. package/clinkx-workflows/dist/dashboard/read-model.js +440 -0
  106. package/clinkx-workflows/dist/dashboard/read-model.js.map +1 -0
  107. package/clinkx-workflows/dist/dashboard/request-admission.d.ts +31 -0
  108. package/clinkx-workflows/dist/dashboard/request-admission.js +126 -0
  109. package/clinkx-workflows/dist/dashboard/request-admission.js.map +1 -0
  110. package/clinkx-workflows/dist/dashboard/scanner.d.ts +75 -0
  111. package/clinkx-workflows/dist/dashboard/scanner.js +1110 -0
  112. package/clinkx-workflows/dist/dashboard/scanner.js.map +1 -0
  113. package/clinkx-workflows/dist/dashboard/server-factory.d.ts +50 -0
  114. package/clinkx-workflows/dist/dashboard/server-factory.js +431 -0
  115. package/clinkx-workflows/dist/dashboard/server-factory.js.map +1 -0
  116. package/clinkx-workflows/dist/dashboard/session-log-contract.d.ts +2611 -0
  117. package/clinkx-workflows/dist/dashboard/session-log-contract.js +480 -0
  118. package/clinkx-workflows/dist/dashboard/session-log-contract.js.map +1 -0
  119. package/clinkx-workflows/dist/dashboard/session-log-identity.d.ts +21 -0
  120. package/clinkx-workflows/dist/dashboard/session-log-identity.js +101 -0
  121. package/clinkx-workflows/dist/dashboard/session-log-identity.js.map +1 -0
  122. package/clinkx-workflows/dist/dashboard/session-log-journal.d.ts +43 -0
  123. package/clinkx-workflows/dist/dashboard/session-log-journal.js +522 -0
  124. package/clinkx-workflows/dist/dashboard/session-log-journal.js.map +1 -0
  125. package/clinkx-workflows/dist/dashboard/session-log-reader.d.ts +61 -0
  126. package/clinkx-workflows/dist/dashboard/session-log-reader.js +378 -0
  127. package/clinkx-workflows/dist/dashboard/session-log-reader.js.map +1 -0
  128. package/clinkx-workflows/dist/dashboard/session-log-redaction.d.ts +6 -0
  129. package/clinkx-workflows/dist/dashboard/session-log-redaction.js +367 -0
  130. package/clinkx-workflows/dist/dashboard/session-log-redaction.js.map +1 -0
  131. package/clinkx-workflows/dist/dashboard/session-log-retention.d.ts +23 -0
  132. package/clinkx-workflows/dist/dashboard/session-log-retention.js +212 -0
  133. package/clinkx-workflows/dist/dashboard/session-log-retention.js.map +1 -0
  134. package/clinkx-workflows/dist/dashboard/static-responses.d.ts +10 -0
  135. package/clinkx-workflows/dist/dashboard/static-responses.js +94 -0
  136. package/clinkx-workflows/dist/dashboard/static-responses.js.map +1 -0
  137. package/clinkx-workflows/dist/dashboard/worker-lock-observer.d.ts +28 -0
  138. package/clinkx-workflows/dist/dashboard/worker-lock-observer.js +241 -0
  139. package/clinkx-workflows/dist/dashboard/worker-lock-observer.js.map +1 -0
  140. package/clinkx-workflows/dist/dashboard-ui/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
  141. package/clinkx-workflows/dist/dashboard-ui/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
  142. package/clinkx-workflows/dist/dashboard-ui/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
  143. package/clinkx-workflows/dist/dashboard-ui/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
  144. package/clinkx-workflows/dist/dashboard-ui/assets/ibm-plex-sans-latin-400-normal-CDDApCn2.woff2 +0 -0
  145. package/clinkx-workflows/dist/dashboard-ui/assets/ibm-plex-sans-latin-400-normal-CYLoc0-x.woff +0 -0
  146. package/clinkx-workflows/dist/dashboard-ui/assets/ibm-plex-sans-latin-500-normal-6ng42L7E.woff2 +0 -0
  147. package/clinkx-workflows/dist/dashboard-ui/assets/ibm-plex-sans-latin-500-normal-BgVn5rGT.woff +0 -0
  148. package/clinkx-workflows/dist/dashboard-ui/assets/ibm-plex-sans-latin-600-normal-Cu4Hd6ag.woff +0 -0
  149. package/clinkx-workflows/dist/dashboard-ui/assets/ibm-plex-sans-latin-600-normal-CuJfVYMP.woff2 +0 -0
  150. package/clinkx-workflows/dist/dashboard-ui/assets/index-DOkMXTfB.js +25 -0
  151. package/clinkx-workflows/dist/dashboard-ui/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
  152. package/clinkx-workflows/dist/dashboard-ui/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
  153. package/clinkx-workflows/dist/dashboard-ui/assets/jetbrains-mono-latin-500-normal-BWZEU5yA.woff2 +0 -0
  154. package/clinkx-workflows/dist/dashboard-ui/assets/jetbrains-mono-latin-500-normal-CJOVTJB7.woff +0 -0
  155. package/clinkx-workflows/dist/dashboard-ui/assets/style-ChJsn0KD.css +1 -0
  156. package/clinkx-workflows/dist/dashboard-ui/contracts/council-plan-review-input-v1.json +158 -0
  157. package/clinkx-workflows/dist/dashboard-ui/index.html +15 -0
  158. package/clinkx-workflows/dist/dashboard-ui/licenses/IBM-Plex-Mono-OFL.txt +93 -0
  159. package/clinkx-workflows/dist/dashboard-ui/licenses/IBM-Plex-Sans-OFL.txt +93 -0
  160. package/clinkx-workflows/dist/dashboard-ui/licenses/JetBrains-Mono-OFL.txt +93 -0
  161. package/clinkx-workflows/dist/dashboard-ui/licenses/NOTICE.md +32 -0
  162. package/clinkx-workflows/dist/dashboard-ui/theme-init.js +8 -0
  163. package/clinkx-workflows/dist/definition-normalizer.d.ts +19 -0
  164. package/clinkx-workflows/dist/definition-normalizer.js.map +1 -1
  165. package/clinkx-workflows/dist/delegate-validate.d.ts +78 -0
  166. package/clinkx-workflows/dist/delegate-validate.js +97 -0
  167. package/clinkx-workflows/dist/delegate-validate.js.map +1 -0
  168. package/clinkx-workflows/dist/engine.d.ts +141 -0
  169. package/clinkx-workflows/dist/engine.js +882 -28
  170. package/clinkx-workflows/dist/engine.js.map +1 -1
  171. package/clinkx-workflows/dist/errors.d.ts +2 -0
  172. package/clinkx-workflows/dist/errors.js.map +1 -1
  173. package/clinkx-workflows/dist/fingerprint.d.ts +1 -1
  174. package/clinkx-workflows/dist/fingerprint.js +1 -1
  175. package/clinkx-workflows/dist/index.d.ts +19 -0
  176. package/clinkx-workflows/dist/index.js +15 -0
  177. package/clinkx-workflows/dist/index.js.map +1 -1
  178. package/clinkx-workflows/dist/loader.d.ts +9 -3
  179. package/clinkx-workflows/dist/loader.js +92 -21
  180. package/clinkx-workflows/dist/loader.js.map +1 -1
  181. package/clinkx-workflows/dist/plan-review/admission.d.ts +160 -0
  182. package/clinkx-workflows/dist/plan-review/admission.js +290 -0
  183. package/clinkx-workflows/dist/plan-review/admission.js.map +1 -0
  184. package/clinkx-workflows/dist/plan-review/context-policy.d.ts +55 -0
  185. package/clinkx-workflows/dist/plan-review/context-policy.js +146 -0
  186. package/clinkx-workflows/dist/plan-review/context-policy.js.map +1 -0
  187. package/clinkx-workflows/dist/plan-review/contract.d.ts +763 -0
  188. package/clinkx-workflows/dist/plan-review/contract.js +289 -0
  189. package/clinkx-workflows/dist/plan-review/contract.js.map +1 -0
  190. package/clinkx-workflows/dist/plan-review/digest.d.ts +68 -0
  191. package/clinkx-workflows/dist/plan-review/digest.js +241 -0
  192. package/clinkx-workflows/dist/plan-review/digest.js.map +1 -0
  193. package/clinkx-workflows/dist/plan-review/discovery.d.ts +45 -0
  194. package/clinkx-workflows/dist/plan-review/discovery.js +36 -0
  195. package/clinkx-workflows/dist/plan-review/discovery.js.map +1 -0
  196. package/clinkx-workflows/dist/plan-review/execution-contract.d.ts +298 -0
  197. package/clinkx-workflows/dist/plan-review/execution-contract.js +194 -0
  198. package/clinkx-workflows/dist/plan-review/execution-contract.js.map +1 -0
  199. package/clinkx-workflows/dist/plan-review/expander.d.ts +48 -0
  200. package/clinkx-workflows/dist/plan-review/expander.js +243 -0
  201. package/clinkx-workflows/dist/plan-review/expander.js.map +1 -0
  202. package/clinkx-workflows/dist/plan-review/projector.d.ts +126 -0
  203. package/clinkx-workflows/dist/plan-review/projector.js +437 -0
  204. package/clinkx-workflows/dist/plan-review/projector.js.map +1 -0
  205. package/clinkx-workflows/dist/plan-review/publication.d.ts +95 -0
  206. package/clinkx-workflows/dist/plan-review/publication.js +182 -0
  207. package/clinkx-workflows/dist/plan-review/publication.js.map +1 -0
  208. package/clinkx-workflows/dist/plan-review/ranking-reducer.d.ts +34 -0
  209. package/clinkx-workflows/dist/plan-review/ranking-reducer.js +139 -0
  210. package/clinkx-workflows/dist/plan-review/ranking-reducer.js.map +1 -0
  211. package/clinkx-workflows/dist/plan-review/resume.d.ts +38 -0
  212. package/clinkx-workflows/dist/plan-review/resume.js +122 -0
  213. package/clinkx-workflows/dist/plan-review/resume.js.map +1 -0
  214. package/clinkx-workflows/dist/plan-review/rubric.d.ts +7 -0
  215. package/clinkx-workflows/dist/plan-review/rubric.js +24 -0
  216. package/clinkx-workflows/dist/plan-review/rubric.js.map +1 -0
  217. package/clinkx-workflows/dist/plan-review/run-state.d.ts +398 -0
  218. package/clinkx-workflows/dist/plan-review/run-state.js +183 -0
  219. package/clinkx-workflows/dist/plan-review/run-state.js.map +1 -0
  220. package/clinkx-workflows/dist/plan-review/runtime-policy.d.ts +74 -0
  221. package/clinkx-workflows/dist/plan-review/runtime-policy.js +186 -0
  222. package/clinkx-workflows/dist/plan-review/runtime-policy.js.map +1 -0
  223. package/clinkx-workflows/dist/plan-review/snapshot.d.ts +81 -0
  224. package/clinkx-workflows/dist/plan-review/snapshot.js +170 -0
  225. package/clinkx-workflows/dist/plan-review/snapshot.js.map +1 -0
  226. package/clinkx-workflows/dist/plan-review/stage-error.d.ts +19 -0
  227. package/clinkx-workflows/dist/plan-review/stage-error.js +20 -0
  228. package/clinkx-workflows/dist/plan-review/stage-error.js.map +1 -0
  229. package/clinkx-workflows/dist/post-processors/index.d.ts +20 -0
  230. package/clinkx-workflows/dist/post-processors/index.js +21 -0
  231. package/clinkx-workflows/dist/post-processors/index.js.map +1 -0
  232. package/clinkx-workflows/dist/prompt-budget.d.ts +7 -0
  233. package/clinkx-workflows/dist/prompt-budget.js +7 -1
  234. package/clinkx-workflows/dist/prompt-budget.js.map +1 -1
  235. package/clinkx-workflows/dist/reducers/index.js +2 -0
  236. package/clinkx-workflows/dist/reducers/index.js.map +1 -1
  237. package/clinkx-workflows/dist/run-lock-heartbeat.d.ts +2 -0
  238. package/clinkx-workflows/dist/run-lock-heartbeat.js +3 -0
  239. package/clinkx-workflows/dist/run-lock-heartbeat.js.map +1 -0
  240. package/clinkx-workflows/dist/run-summary/cards/council-plan-review.d.ts +34 -0
  241. package/clinkx-workflows/dist/run-summary/cards/council-plan-review.js +74 -0
  242. package/clinkx-workflows/dist/run-summary/cards/council-plan-review.js.map +1 -0
  243. package/clinkx-workflows/dist/run-summary/cards/index.js +5 -0
  244. package/clinkx-workflows/dist/run-summary/cards/index.js.map +1 -1
  245. package/clinkx-workflows/dist/run-summary.d.ts +13 -0
  246. package/clinkx-workflows/dist/run-summary.js +29 -1
  247. package/clinkx-workflows/dist/run-summary.js.map +1 -1
  248. package/clinkx-workflows/dist/schema.d.ts +76 -21
  249. package/clinkx-workflows/dist/schema.js +10 -1
  250. package/clinkx-workflows/dist/schema.js.map +1 -1
  251. package/clinkx-workflows/dist/seed-terms.d.ts +30 -0
  252. package/clinkx-workflows/dist/seed-terms.js +176 -0
  253. package/clinkx-workflows/dist/seed-terms.js.map +1 -0
  254. package/clinkx-workflows/dist/server.d.ts +5 -2
  255. package/clinkx-workflows/dist/server.js +82 -52
  256. package/clinkx-workflows/dist/server.js.map +1 -1
  257. package/clinkx-workflows/dist/shutdown.d.ts +18 -20
  258. package/clinkx-workflows/dist/shutdown.js +43 -61
  259. package/clinkx-workflows/dist/shutdown.js.map +1 -1
  260. package/clinkx-workflows/dist/state-schema.d.ts +38 -0
  261. package/clinkx-workflows/dist/state.d.ts +170 -3
  262. package/clinkx-workflows/dist/state.js +1275 -28
  263. package/clinkx-workflows/dist/state.js.map +1 -1
  264. package/clinkx-workflows/dist/transport.d.ts +15 -0
  265. package/clinkx-workflows/dist/transport.js +19 -10
  266. package/clinkx-workflows/dist/transport.js.map +1 -1
  267. package/clinkx-workflows/dist/types.d.ts +14 -2
  268. package/clinkx-workflows/dist/validators/council.d.ts +318 -85
  269. package/clinkx-workflows/dist/validators/council.js +52 -13
  270. package/clinkx-workflows/dist/validators/council.js.map +1 -1
  271. package/clinkx-workflows/dist/validators/index.d.ts +1 -0
  272. package/clinkx-workflows/dist/validators/index.js +3 -0
  273. package/clinkx-workflows/dist/validators/index.js.map +1 -1
  274. package/clinkx-workflows/dist/validators/plan-review.d.ts +398 -0
  275. package/clinkx-workflows/dist/validators/plan-review.js +316 -0
  276. package/clinkx-workflows/dist/validators/plan-review.js.map +1 -0
  277. package/clinkx-workflows/dist/workflow-receipt.js +188 -4
  278. package/clinkx-workflows/dist/workflow-receipt.js.map +1 -1
  279. package/clinkx-workflows/dist/workflow-tools.d.ts +48 -3
  280. package/clinkx-workflows/dist/workflow-tools.js +1402 -59
  281. package/clinkx-workflows/dist/workflow-tools.js.map +1 -1
  282. package/clinkx-workflows/dist/workspace-signature.d.ts +16 -0
  283. package/clinkx-workflows/dist/workspace-signature.js +388 -0
  284. package/clinkx-workflows/dist/workspace-signature.js.map +1 -0
  285. package/clinkx-workflows/templates/council/answer-specialist.txt +2 -1
  286. package/clinkx-workflows/templates/council/code-review-chairman.txt +13 -0
  287. package/clinkx-workflows/templates/council/code-review-cross-reviewer.txt +4 -1
  288. package/clinkx-workflows/templates/council/code-review-reviewer.txt +19 -3
  289. package/clinkx-workflows/templates/council/debug-debugger.txt +2 -1
  290. package/clinkx-workflows/templates/council/discover-analyst.txt +2 -1
  291. package/clinkx-workflows/templates/council/member.txt +2 -1
  292. package/clinkx-workflows/templates/council/plan-review-chairman.txt +68 -0
  293. package/clinkx-workflows/templates/council/plan-review-cross-reviewer.txt +51 -0
  294. package/clinkx-workflows/templates/council/plan-review-reviewer.txt +60 -0
  295. package/clinkx-workflows/templates/plan-review/chairman.txt +48 -0
  296. package/clinkx-workflows/templates/plan-review/cross-review.txt +52 -0
  297. package/clinkx-workflows/templates/plan-review/ranking.txt +16 -0
  298. package/clinkx-workflows/templates/plan-review/reviewer.txt +57 -0
  299. package/clinkx-workflows/workflows/council-answer.yaml +97 -8
  300. package/clinkx-workflows/workflows/council-code-review.yaml +103 -8
  301. package/clinkx-workflows/workflows/council-debug.yaml +97 -8
  302. package/clinkx-workflows/workflows/council-default.yaml +97 -8
  303. package/clinkx-workflows/workflows/council-discover.yaml +97 -8
  304. package/clinkx-workflows/workflows/council-plan-review.yaml +250 -0
  305. package/clinkx-workflows/workflows/plan-review-receipts.yaml +230 -0
  306. package/conf/adapters/agy.json +49 -0
  307. package/conf/adapters/claude.json +5 -0
  308. package/conf/adapters/codex-app-server.json +44 -0
  309. package/conf/adapters/codex.json +7 -2
  310. package/conf/adapters/glm.json +5 -0
  311. package/conf/adapters/grok.json +60 -0
  312. package/conf/adapters/hapi/agy.json +52 -0
  313. package/conf/adapters/hapi/claude.json +5 -0
  314. package/conf/adapters/hapi/codex.json +7 -2
  315. package/conf/adapters/hapi/glm.json +5 -0
  316. package/dist/activity-journal.d.ts +42 -0
  317. package/dist/activity-journal.js +381 -0
  318. package/dist/activity-journal.js.map +1 -0
  319. package/dist/allowed-roots.d.ts +41 -0
  320. package/dist/allowed-roots.js +125 -3
  321. package/dist/allowed-roots.js.map +1 -1
  322. package/dist/codex/activity-session-log.d.ts +14 -0
  323. package/dist/codex/activity-session-log.js +206 -0
  324. package/dist/codex/activity-session-log.js.map +1 -0
  325. package/dist/codex/agent-tools.d.ts +23 -0
  326. package/dist/codex/agent-tools.js +1270 -0
  327. package/dist/codex/agent-tools.js.map +1 -0
  328. package/dist/codex/app-server-client.d.ts +76 -0
  329. package/dist/codex/app-server-client.js +582 -0
  330. package/dist/codex/app-server-client.js.map +1 -0
  331. package/dist/codex/app-server-registry.d.ts +389 -0
  332. package/dist/codex/app-server-registry.js +1407 -0
  333. package/dist/codex/app-server-registry.js.map +1 -0
  334. package/dist/codex/client-types.d.ts +109 -0
  335. package/dist/codex/client-types.js +2 -0
  336. package/dist/codex/client-types.js.map +1 -0
  337. package/dist/codex/protocol.d.ts +5375 -0
  338. package/dist/codex/protocol.js +614 -0
  339. package/dist/codex/protocol.js.map +1 -0
  340. package/dist/codex/session.d.ts +263 -0
  341. package/dist/codex/session.js +466 -0
  342. package/dist/codex/session.js.map +1 -0
  343. package/dist/config.d.ts +185 -19
  344. package/dist/config.js +82 -16
  345. package/dist/config.js.map +1 -1
  346. package/dist/context-builder/anchors.d.ts +1 -0
  347. package/dist/context-builder/anchors.js +3 -0
  348. package/dist/context-builder/anchors.js.map +1 -1
  349. package/dist/context-builder/bundle.d.ts +35 -2
  350. package/dist/context-builder/bundle.js +342 -2417
  351. package/dist/context-builder/bundle.js.map +1 -1
  352. package/dist/context-builder/codemap.d.ts +5 -0
  353. package/dist/context-builder/codemap.js +387 -13
  354. package/dist/context-builder/codemap.js.map +1 -1
  355. package/dist/context-builder/config.d.ts +1 -0
  356. package/dist/context-builder/config.js +12 -0
  357. package/dist/context-builder/config.js.map +1 -1
  358. package/dist/context-builder/context-run-catalog.d.ts +17 -0
  359. package/dist/context-builder/context-run-catalog.js +99 -0
  360. package/dist/context-builder/context-run-catalog.js.map +1 -0
  361. package/dist/context-builder/delegate-packet.d.ts +37 -0
  362. package/dist/context-builder/delegate-packet.js +359 -67
  363. package/dist/context-builder/delegate-packet.js.map +1 -1
  364. package/dist/context-builder/delegate-validation.d.ts +10 -1
  365. package/dist/context-builder/delegate-validation.js +153 -41
  366. package/dist/context-builder/delegate-validation.js.map +1 -1
  367. package/dist/context-builder/delegate.d.ts +2 -0
  368. package/dist/context-builder/delegate.js +1 -0
  369. package/dist/context-builder/delegate.js.map +1 -1
  370. package/dist/context-builder/diagnostics.d.ts +11 -2
  371. package/dist/context-builder/diagnostics.js +39 -6
  372. package/dist/context-builder/diagnostics.js.map +1 -1
  373. package/dist/context-builder/discovery.d.ts +29 -1
  374. package/dist/context-builder/discovery.js +475 -175
  375. package/dist/context-builder/discovery.js.map +1 -1
  376. package/dist/context-builder/globs.js +10 -10
  377. package/dist/context-builder/globs.js.map +1 -1
  378. package/dist/context-builder/handler.d.ts +26 -2
  379. package/dist/context-builder/handler.js +1158 -2416
  380. package/dist/context-builder/handler.js.map +1 -1
  381. package/dist/context-builder/lifecycle-contract.d.ts +424 -0
  382. package/dist/context-builder/lifecycle-contract.js +523 -0
  383. package/dist/context-builder/lifecycle-contract.js.map +1 -0
  384. package/dist/context-builder/lifecycle-journal.d.ts +62 -0
  385. package/dist/context-builder/lifecycle-journal.js +88 -0
  386. package/dist/context-builder/lifecycle-journal.js.map +1 -0
  387. package/dist/context-builder/lifecycle-observer.d.ts +102 -0
  388. package/dist/context-builder/lifecycle-observer.js +258 -0
  389. package/dist/context-builder/lifecycle-observer.js.map +1 -0
  390. package/dist/context-builder/lifecycle-reconcile.d.ts +32 -0
  391. package/dist/context-builder/lifecycle-reconcile.js +191 -0
  392. package/dist/context-builder/lifecycle-reconcile.js.map +1 -0
  393. package/dist/context-builder/lifecycle-recovery.d.ts +2 -0
  394. package/dist/context-builder/lifecycle-recovery.js +14 -0
  395. package/dist/context-builder/lifecycle-recovery.js.map +1 -0
  396. package/dist/context-builder/path-signals.js +9 -5
  397. package/dist/context-builder/path-signals.js.map +1 -1
  398. package/dist/context-builder/progress.d.ts +3 -1
  399. package/dist/context-builder/progress.js +23 -0
  400. package/dist/context-builder/progress.js.map +1 -1
  401. package/dist/context-builder/receipt.d.ts +6 -4
  402. package/dist/context-builder/receipt.js +12 -1
  403. package/dist/context-builder/receipt.js.map +1 -1
  404. package/dist/context-builder/refine-handler.d.ts +14 -0
  405. package/dist/context-builder/refine-handler.js +790 -0
  406. package/dist/context-builder/refine-handler.js.map +1 -0
  407. package/dist/context-builder/review-coverage.d.ts +84 -0
  408. package/dist/context-builder/review-coverage.js +437 -0
  409. package/dist/context-builder/review-coverage.js.map +1 -0
  410. package/dist/context-builder/schema.d.ts +239 -1
  411. package/dist/context-builder/schema.js +112 -6
  412. package/dist/context-builder/schema.js.map +1 -1
  413. package/dist/context-builder/secrets.d.ts +28 -0
  414. package/dist/context-builder/secrets.js +1473 -44
  415. package/dist/context-builder/secrets.js.map +1 -1
  416. package/dist/context-builder/selection.d.ts +7 -3
  417. package/dist/context-builder/selection.js +200 -738
  418. package/dist/context-builder/selection.js.map +1 -1
  419. package/dist/context-builder/session-log-identity.d.ts +1 -0
  420. package/dist/context-builder/session-log-identity.js +66 -0
  421. package/dist/context-builder/session-log-identity.js.map +1 -0
  422. package/dist/context-builder/slice-range-math.d.ts +16 -0
  423. package/dist/context-builder/slice-range-math.js +137 -0
  424. package/dist/context-builder/slice-range-math.js.map +1 -0
  425. package/dist/context-builder/status.d.ts +22 -1
  426. package/dist/context-builder/status.js +59 -4
  427. package/dist/context-builder/status.js.map +1 -1
  428. package/dist/context-builder/storage.d.ts +35 -2
  429. package/dist/context-builder/storage.js +271 -14
  430. package/dist/context-builder/storage.js.map +1 -1
  431. package/dist/context-builder/token-estimate.d.ts +13 -2
  432. package/dist/context-builder/token-estimate.js +77 -4
  433. package/dist/context-builder/token-estimate.js.map +1 -1
  434. package/dist/context-builder/versions.d.ts +29 -0
  435. package/dist/context-builder/versions.js +30 -0
  436. package/dist/context-builder/versions.js.map +1 -0
  437. package/dist/context-builder/workflow-runner.d.ts +69 -0
  438. package/dist/context-builder/workflow-runner.js +1051 -0
  439. package/dist/context-builder/workflow-runner.js.map +1 -0
  440. package/dist/context-builder/write-watchdog.d.ts +14 -0
  441. package/dist/context-builder/write-watchdog.js +38 -0
  442. package/dist/context-builder/write-watchdog.js.map +1 -0
  443. package/dist/dashboard-adapter-catalog.d.ts +9 -0
  444. package/dist/dashboard-adapter-catalog.js +46 -0
  445. package/dist/dashboard-adapter-catalog.js.map +1 -0
  446. package/dist/dashboard-cli.d.ts +79 -0
  447. package/dist/dashboard-cli.js +585 -0
  448. package/dist/dashboard-cli.js.map +1 -0
  449. package/dist/dashboard-companion/client.d.ts +36 -0
  450. package/dist/dashboard-companion/client.js +210 -0
  451. package/dist/dashboard-companion/client.js.map +1 -0
  452. package/dist/dashboard-companion/config.d.ts +8 -0
  453. package/dist/dashboard-companion/config.js +52 -0
  454. package/dist/dashboard-companion/config.js.map +1 -0
  455. package/dist/dashboard-companion/http-proxy.d.ts +10 -0
  456. package/dist/dashboard-companion/http-proxy.js +234 -0
  457. package/dist/dashboard-companion/http-proxy.js.map +1 -0
  458. package/dist/dashboard-companion/protocol.d.ts +80 -0
  459. package/dist/dashboard-companion/protocol.js +455 -0
  460. package/dist/dashboard-companion/protocol.js.map +1 -0
  461. package/dist/dashboard-companion/standalone-assets.d.ts +12 -0
  462. package/dist/dashboard-companion/standalone-assets.js +72 -0
  463. package/dist/dashboard-companion/standalone-assets.js.map +1 -0
  464. package/dist/dashboard-companion/standalone-entry.d.ts +1 -0
  465. package/dist/dashboard-companion/standalone-entry.js +17 -0
  466. package/dist/dashboard-companion/standalone-entry.js.map +1 -0
  467. package/dist/dashboard-gateway/config.d.ts +9 -0
  468. package/dist/dashboard-gateway/config.js +44 -0
  469. package/dist/dashboard-gateway/config.js.map +1 -0
  470. package/dist/dashboard-gateway/index.d.ts +11 -0
  471. package/dist/dashboard-gateway/index.js +755 -0
  472. package/dist/dashboard-gateway/index.js.map +1 -0
  473. package/dist/dashboard-gateway/main.d.ts +1 -0
  474. package/dist/dashboard-gateway/main.js +25 -0
  475. package/dist/dashboard-gateway/main.js.map +1 -0
  476. package/dist/dashboard-pairing-token.d.ts +1 -0
  477. package/dist/dashboard-pairing-token.js +5 -0
  478. package/dist/dashboard-pairing-token.js.map +1 -0
  479. package/dist/elaris/client.d.ts +73 -0
  480. package/dist/elaris/client.js +825 -0
  481. package/dist/elaris/client.js.map +1 -0
  482. package/dist/elaris/config.d.ts +18 -0
  483. package/dist/elaris/config.js +75 -0
  484. package/dist/elaris/config.js.map +1 -0
  485. package/dist/elaris/errors.d.ts +41 -0
  486. package/dist/elaris/errors.js +205 -0
  487. package/dist/elaris/errors.js.map +1 -0
  488. package/dist/elaris/github-repo.d.ts +18 -0
  489. package/dist/elaris/github-repo.js +89 -0
  490. package/dist/elaris/github-repo.js.map +1 -0
  491. package/dist/elaris/index.d.ts +10 -0
  492. package/dist/elaris/index.js +11 -0
  493. package/dist/elaris/index.js.map +1 -0
  494. package/dist/elaris/redact.d.ts +89 -0
  495. package/dist/elaris/redact.js +423 -0
  496. package/dist/elaris/redact.js.map +1 -0
  497. package/dist/elaris/run-id.d.ts +5 -0
  498. package/dist/elaris/run-id.js +20 -0
  499. package/dist/elaris/run-id.js.map +1 -0
  500. package/dist/elaris/status.d.ts +45 -0
  501. package/dist/elaris/status.js +122 -0
  502. package/dist/elaris/status.js.map +1 -0
  503. package/dist/elaris/tool-schemas.d.ts +314 -0
  504. package/dist/elaris/tool-schemas.js +208 -0
  505. package/dist/elaris/tool-schemas.js.map +1 -0
  506. package/dist/elaris/tools.d.ts +73 -0
  507. package/dist/elaris/tools.js +1065 -0
  508. package/dist/elaris/tools.js.map +1 -0
  509. package/dist/elaris/types.d.ts +1541 -0
  510. package/dist/elaris/types.js +252 -0
  511. package/dist/elaris/types.js.map +1 -0
  512. package/dist/errors.d.ts +16 -0
  513. package/dist/errors.js +19 -0
  514. package/dist/errors.js.map +1 -1
  515. package/dist/handler.d.ts +4 -0
  516. package/dist/handler.js +10 -1
  517. package/dist/handler.js.map +1 -1
  518. package/dist/index.d.ts +2 -1
  519. package/dist/index.js +13 -3
  520. package/dist/index.js.map +1 -1
  521. package/dist/local-clink-backend.d.ts +3 -0
  522. package/dist/local-clink-backend.js +4 -0
  523. package/dist/local-clink-backend.js.map +1 -1
  524. package/dist/package-info.d.ts +2 -0
  525. package/dist/package-info.js +18 -0
  526. package/dist/package-info.js.map +1 -0
  527. package/dist/parsers/codex-jsonl.js +48 -3
  528. package/dist/parsers/codex-jsonl.js.map +1 -1
  529. package/dist/parsers/extract.d.ts +10 -1
  530. package/dist/parsers/extract.js +11 -3
  531. package/dist/parsers/extract.js.map +1 -1
  532. package/dist/pipeline.d.ts +4 -0
  533. package/dist/pipeline.js +129 -8
  534. package/dist/pipeline.js.map +1 -1
  535. package/dist/run-dir.d.ts +24 -0
  536. package/dist/run-dir.js +201 -2
  537. package/dist/run-dir.js.map +1 -1
  538. package/dist/runner.d.ts +13 -0
  539. package/dist/runner.js +184 -15
  540. package/dist/runner.js.map +1 -1
  541. package/dist/safety.d.ts +4 -4
  542. package/dist/safety.js +4 -4
  543. package/dist/schema.d.ts +7 -7
  544. package/dist/server.d.ts +1 -0
  545. package/dist/server.js +359 -17
  546. package/dist/server.js.map +1 -1
  547. package/dist/session-log-stream.d.ts +18 -0
  548. package/dist/session-log-stream.js +319 -0
  549. package/dist/session-log-stream.js.map +1 -0
  550. package/package.json +46 -5
  551. package/conf/adapters/gemini.json +0 -44
  552. package/conf/adapters/hapi/gemini.json +0 -47
  553. package/dist/parsers/gemini-json.d.ts +0 -2
  554. package/dist/parsers/gemini-json.js +0 -72
  555. package/dist/parsers/gemini-json.js.map +0 -1
@@ -1,47 +1,95 @@
1
- import { isSamePathOrDescendant } from "../allowed-roots.js";
1
+ import { appendArtifactOnlyRootsToEnv, isArtifactOnlyAllowedRootEntry, isSamePathOrDescendant, parseAllowedRootsEnv, stripArtifactOnlyMarker, } from "../allowed-roots.js";
2
2
  import { CancellationError, ExecutionError, InvalidParamsError, } from "../errors.js";
3
- import { readFileSync, realpathSync } from "node:fs";
3
+ import { realpathSync } from "node:fs";
4
4
  import { realpath as fsRealpath } from "node:fs/promises";
5
5
  import path from "node:path";
6
- import { getCurrentAllowedRoots } from "../paths.js";
7
6
  import { AdapterRegistry as RuntimeAdapterRegistry } from "../registry.js";
7
+ import { logger } from "../logger.js";
8
8
  import { assertContextBuilderAnchorSufficiency } from "./anchors.js";
9
9
  import { writeContextBuilderFinalArtifacts, } from "./bundle.js";
10
- import { CONTEXT_BUILDER_INTERNAL_CAP_DEFAULTS, ContextBuilderSemaphore, getContextBuilderMaxConcurrent, resolveContextBuilderRunsDir, } from "./config.js";
11
- import { buildContextBuilderCuratorPacket, buildContextBuilderFinalizerPacket, CONTEXT_BUILDER_DELEGATE_PACKET_CAP_PROFILE_DEFAULTS, } from "./delegate-packet.js";
10
+ import { CONTEXT_BUILDER_INTERNAL_CAP_DEFAULTS, getSharedContextBuilderSemaphore, resolveContextBuilderRunsDir, } from "./config.js";
11
+ import { buildContextBuilderCuratorPacket, buildContextBuilderFinalizerPacket, CONTEXT_BUILDER_DELEGATE_PACKET_CAP_PROFILE_DEFAULTS, contextBuilderCandidateId, } from "./delegate-packet.js";
12
12
  import { reconstructContextBuilderDelegateSelection } from "./delegate-reconstruction.js";
13
13
  import { runContextBuilderDelegateCall, } from "./delegate.js";
14
- import { validateContextBuilderCuratorOutput, validateContextBuilderFinalizerOutput, } from "./delegate-validation.js";
15
- import { discoverContextBuilderCandidates, mergeLineRanges, splitTextLines, } from "./discovery.js";
16
- import { boundContextBuilderPathList, isRequestedEvidenceOmission, sanitizeDiagnosticText, } from "./diagnostics.js";
17
- import { validateContextBuilderGlobs } from "./globs.js";
14
+ import { validateContextBuilderCuratorOutput, validateContextBuilderDelegateCoverageSelection, validateContextBuilderFinalizerOutput, } from "./delegate-validation.js";
15
+ import { discoverContextBuilderCandidates, mergeLineRanges, } from "./discovery.js";
16
+ import { evaluateSearchCapMateriality } from "./status.js";
17
+ import { boundContextBuilderPathList, isRequestedEvidenceOmission, sanitizeDelegateFailureText, sanitizeDiagnosticText, } from "./diagnostics.js";
18
+ import { compileContextBuilderGlobs, CONTEXT_BUILDER_DEFAULT_EXCLUDE_GLOBS, validateContextBuilderGlobs, } from "./globs.js";
18
19
  import { resolveContextBuilderFocusPaths, resolveContextBuilderRoots, } from "./paths.js";
19
20
  import { createContextBuilderProgressReporter, } from "./progress.js";
21
+ import { ContextBuilderLifecycleController, ContextBuilderLifecycleObserverError, } from "./lifecycle-observer.js";
22
+ import { createContextBuilderLifecycleJournal } from "./lifecycle-journal.js";
23
+ import { recordContextRunCatalogMarker } from "./context-run-catalog.js";
24
+ import { appendLifecycleCompletionWarning } from "./lifecycle-recovery.js";
25
+ import { withContextBuilderWriteWatchdog } from "./write-watchdog.js";
20
26
  import { createContextBuilderReceiptTextResponse, } from "./receipt.js";
21
27
  import { generateContextBuilderRunId } from "./run-id.js";
22
28
  import { CONTEXT_BUILDER_INTENT_PROFILE_DEFAULTS, parseContextBuilderInput, } from "./schema.js";
23
- import { isContextBuilderTaskClosureAnchor, selectContextBuilderFinalContext, } from "./selection.js";
29
+ import { evaluateContextCoverage, resolveContextCoveragePolicy, } from "./review-coverage.js";
30
+ import { selectContextBuilderFinalContext, } from "./selection.js";
24
31
  import { cleanupContextBuilderIncompleteRun, createContextBuilderRun, } from "./storage.js";
32
+ import { admitJournalContextGeneration } from "./session-log-identity.js";
25
33
  import { estimateContextBuilderTokensFromUtf8Bytes } from "./token-estimate.js";
26
- let defaultSemaphore;
27
34
  let defaultAdapterRegistry;
28
35
  const artifactOnlyAllowedRoots = new Set();
29
- const CONTEXT_BUILDER_DELEGATE_PROMPT_VERSION = "cb-delegate-v2-packet-nonempty";
36
+ const CONTEXT_BUILDER_DELEGATE_PROMPT_VERSION = "cb-delegate-v3-scored-obligations";
30
37
  const MAX_DELEGATE_MINIMUM_OUTPUT_SELECTIONS = 24;
31
38
  export async function handleContextBuilder(args, options = {}) {
32
39
  const input = parseContextBuilderInput(args);
40
+ const coveragePolicy = resolveContextCoveragePolicy(input.profile);
33
41
  const semaphore = options.semaphore ?? getDefaultContextBuilderSemaphore();
34
- semaphore.acquire();
35
42
  const progress = createContextBuilderProgressReporter(options.progress);
43
+ const ownsLifecycle = options.lifecycleController == null;
44
+ semaphore.acquire();
45
+ let lifecycle;
36
46
  let storage;
37
47
  let committed = false;
38
48
  try {
39
- await progress?.emit("validating");
49
+ // Resolve and validate the runs dir up front so a broad/forbidden runs_dir
50
+ // is rejected with its canonical error BEFORE any lifecycle transition, and
51
+ // so the journal reuses this one already-validated path instead of
52
+ // re-resolving (a second resolution would wrap that rejection as an opaque
53
+ // observer failure).
54
+ const runsDir = await resolveContextBuilderRunsDir(options.runsDir);
55
+ // A standalone operation that owns its lifecycle (no injected observer)
56
+ // persists authoritatively through the .context-runs journal; the injected
57
+ // path — workflow preflight — supplies its own store instead.
58
+ const journal = ownsLifecycle && options.lifecycle == null
59
+ ? createContextBuilderLifecycleJournal({
60
+ runsDir,
61
+ resolveRunsDir: async () => runsDir,
62
+ })
63
+ : undefined;
64
+ lifecycle = options.lifecycleController ??
65
+ new ContextBuilderLifecycleController({
66
+ operationKind: "standalone",
67
+ execution: lifecycleExecutionConfig(input),
68
+ ...(options.lifecycle != null
69
+ ? {
70
+ observer: options.lifecycle.observer,
71
+ ...(options.lifecycle.origin == null
72
+ ? {}
73
+ : { origin: options.lifecycle.origin }),
74
+ }
75
+ : journal == null
76
+ ? {}
77
+ : { observer: journal.observe }),
78
+ ...(progress == null ? {} : { notificationObserver: progress.observe }),
79
+ ...(options.now == null ? {} : { now: options.now }),
80
+ });
81
+ if (ownsLifecycle) {
82
+ await lifecycle?.transition("resolving");
83
+ }
40
84
  const createdAt = (options.now?.() ?? new Date()).toISOString();
41
85
  const runId = input.run_id ?? generateContextBuilderRunId();
86
+ await lifecycle?.transition("validating", {
87
+ ...(ownsLifecycle ? { artifactRunId: runId } : {}),
88
+ });
42
89
  const internalCaps = options.internalCaps ?? CONTEXT_BUILDER_INTERNAL_CAP_DEFAULTS;
43
- const runsDir = await resolveContextBuilderRunsDir(options.runsDir);
44
- const currentAllowedRoots = getCurrentAllowedRoots();
90
+ const currentAllowedRoots = parseAllowedRootsEnv({
91
+ includeArtifactOnly: false,
92
+ });
45
93
  const explicitlyConfiguredArtifactRoots = getExactlyConfiguredArtifactRoots();
46
94
  const sourceAllowedRoots = currentAllowedRoots.filter((root) => !artifactOnlyAllowedRoots.has(root) ||
47
95
  explicitlyConfiguredArtifactRoots.has(root));
@@ -60,38 +108,77 @@ export async function handleContextBuilder(args, options = {}) {
60
108
  focus_paths: focusPaths,
61
109
  include_globs: input.include_globs,
62
110
  search_terms: input.search_terms,
111
+ allowRootWideDiscovery: options.allowRootWideDiscovery ?? false,
112
+ });
113
+ assertNoContextBuilderFocusGlobConflicts({
114
+ focusPaths,
115
+ includeGlobs: input.include_globs,
116
+ excludeGlobs: input.exclude_globs,
63
117
  });
64
118
  appendArtifactOnlyRootToEnv(runsDir);
65
119
  if (!sourceAllowedRoots.includes(runsDir)) {
66
120
  artifactOnlyAllowedRoots.add(runsDir);
67
121
  }
68
122
  const excludedArtifactRoots = [...artifactOnlyAllowedRoots].filter((root) => !explicitlyConfiguredArtifactRoots.has(root));
69
- storage = await createContextBuilderRun({
123
+ const writingStorage = await createContextBuilderRun({
70
124
  runsDir,
71
125
  runId,
72
126
  createdAt,
73
127
  });
74
- await progress?.emit("scanning");
128
+ storage = writingStorage;
129
+ let sessionLogContext;
130
+ if (journal !== undefined) {
131
+ try {
132
+ sessionLogContext = {
133
+ contextRunId: lifecycle.contextRunId,
134
+ contextGeneration: await admitJournalContextGeneration(runsDir, lifecycle.contextRunId),
135
+ };
136
+ }
137
+ catch {
138
+ logger.debug({ contextRunId: lifecycle.contextRunId }, "context session-log identity admission failed open");
139
+ }
140
+ }
141
+ else if (options.sessionLogContext !== undefined) {
142
+ // Workflow preflight path: the runner minted the operation id (via the
143
+ // controller) and created the run-dir generation sidecar. Use that pair
144
+ // instead of the journal (which the injected-controller path does not
145
+ // create). Both flow into the context-family session-log owner emitted for
146
+ // the curator/finalizer delegate calls.
147
+ const injected = options.sessionLogContext;
148
+ sessionLogContext = injected.contextRunId === lifecycle.contextRunId
149
+ ? injected
150
+ : { contextRunId: lifecycle.contextRunId, contextGeneration: injected.contextGeneration };
151
+ }
152
+ await lifecycle?.transition("scanning");
75
153
  const discovery = await discoverContextBuilderCandidates({
76
154
  roots: rootResolution.roots,
77
155
  task: input.task,
156
+ allowSecrets: input.allow_secrets,
78
157
  focus_paths: focusPaths,
79
158
  include_globs: input.include_globs,
80
159
  exclude_globs: input.exclude_globs,
81
160
  search_terms: input.search_terms,
161
+ coverage_policy: coveragePolicy,
82
162
  budget: input.budget,
83
163
  internal_caps: internalCaps,
84
164
  excluded_real_roots: excludedArtifactRoots,
85
165
  ...(options.signal == null ? {} : { signal: options.signal }),
86
166
  });
87
- await progress?.emit("searching");
88
- await progress?.emit("selecting");
167
+ await lifecycle?.transition("searching", {
168
+ counts: lifecycleCountsFromDiscovery(discovery),
169
+ });
170
+ await lifecycle?.transition("selecting");
89
171
  const selection = selectContextBuilderFinalContext({
90
172
  task: input.task,
91
173
  tree: discovery.tree,
92
174
  search: discovery.search,
93
175
  slices: discovery.slices,
94
176
  budget: input.budget,
177
+ coverage_policy: coveragePolicy,
178
+ evidence_classes_by_candidate: evidenceClassesByCandidate(discovery.content_candidates),
179
+ ...(discovery.review_obligations == null
180
+ ? {}
181
+ : { review_obligations: discovery.review_obligations }),
95
182
  });
96
183
  const effectiveDelegatePacketCaps = resolveEffectiveDelegatePacketCaps(input.profile, options.delegatePacketCaps);
97
184
  const delegateResult = input.delegate.enabled
@@ -103,55 +190,26 @@ export async function handleContextBuilder(args, options = {}) {
103
190
  baselineOmissions: rootResolution.omissions,
104
191
  sourceRoots: rootResolution.roots,
105
192
  registry: options.registry ?? getDefaultAdapterRegistry(),
193
+ coveragePolicy,
106
194
  ...(effectiveDelegatePacketCaps == null
107
195
  ? {}
108
196
  : { delegatePacketCaps: effectiveDelegatePacketCaps }),
109
- ...(progress == null ? {} : { progress }),
197
+ ...(lifecycle == null ? {} : { lifecycle }),
110
198
  ...(options.signal == null ? {} : { signal: options.signal }),
199
+ ...(sessionLogContext == null ? {} : { sessionLogContext }),
111
200
  })
112
201
  : undefined;
113
- let effectiveDelegateResult = delegateResult;
114
- let finalSelection = pruneTaskSpecificFinalSelection({
115
- selection: effectiveDelegateResult?.selection ?? selection,
116
- contentCandidates: discovery.content_candidates,
117
- sourceRoots: rootResolution.roots,
118
- budget: input.budget,
119
- task: input.task,
120
- });
121
- let packetQa = validateBenchmarkPacketQuality({
122
- task: input.task,
123
- selection: finalSelection,
124
- contentCandidates: discovery.content_candidates,
202
+ const effectiveDelegateResult = delegateResult;
203
+ const finalSelection = effectiveDelegateResult?.selection ?? selection;
204
+ const coverage = evaluateSelectionCoverage({
205
+ policy: coveragePolicy,
206
+ candidates: discovery.content_candidates,
207
+ selected: finalSelection.selected,
208
+ ...(discovery.review_obligations == null
209
+ ? {}
210
+ : { obligations: discovery.review_obligations }),
125
211
  });
126
- if (packetQa.materialFailureReason != null &&
127
- effectiveDelegateResult != null &&
128
- input.delegate.enabled) {
129
- effectiveDelegateResult = handleDelegateRuntimeFailure({
130
- delegate: input.delegate,
131
- reason: packetQa.materialFailureReason,
132
- deterministicSelection: selection,
133
- omissionCount: rootResolution.omissions.length +
134
- discovery.omissions.length +
135
- selection.omissions.length,
136
- candidateFallbackCount: selection.selected.length,
137
- });
138
- finalSelection = pruneTaskSpecificFinalSelection({
139
- selection: effectiveDelegateResult.selection,
140
- contentCandidates: discovery.content_candidates,
141
- sourceRoots: rootResolution.roots,
142
- budget: input.budget,
143
- task: input.task,
144
- });
145
- packetQa = validateBenchmarkPacketQuality({
146
- task: input.task,
147
- selection: finalSelection,
148
- contentCandidates: discovery.content_candidates,
149
- });
150
- }
151
- const rawWarnings = [
152
- ...(effectiveDelegateResult?.warnings ?? []),
153
- ...packetQa.warnings,
154
- ];
212
+ const rawWarnings = [...(effectiveDelegateResult?.warnings ?? [])];
155
213
  const openQuestions = effectiveDelegateResult?.openQuestions ?? [];
156
214
  const omissions = [
157
215
  ...rootResolution.omissions,
@@ -164,13 +222,21 @@ export async function handleContextBuilder(args, options = {}) {
164
222
  selection: finalSelection.metrics,
165
223
  omissionCount: omissions.length,
166
224
  });
167
- const warnings = reconcileContextBuilderWarnings({
225
+ const reconciledWarnings = reconcileContextBuilderWarnings({
168
226
  warnings: rawWarnings,
169
227
  selected: finalSelection.selected,
170
228
  omissions,
171
229
  metrics,
172
230
  delegate: effectiveDelegateResult?.delegate,
173
231
  });
232
+ // A material search cap must be explained in the receipt, not just
233
+ // classified: the warning carries the bounded coverage summary while the
234
+ // cap event (built from metrics + search_coverage in the status reducer)
235
+ // carries the partial classification.
236
+ const searchCapWarning = searchCapCoverageWarning(metrics.search_cap_hit, discovery.search_coverage);
237
+ const warnings = searchCapWarning == null
238
+ ? reconciledWarnings
239
+ : [...reconciledWarnings, searchCapWarning];
174
240
  const delegateMetadata = effectiveDelegateResult?.delegate == null
175
241
  ? undefined
176
242
  : {
@@ -178,10 +244,26 @@ export async function handleContextBuilder(args, options = {}) {
178
244
  selected_count: finalSelection.selected.length,
179
245
  warning_count: warnings.length,
180
246
  };
247
+ const delegatePacketEstimatedTokens = effectiveDelegateResult?.observability.delegate_packet_size ?? 0;
248
+ const delegatePacketCompaction = effectiveDelegateResult?.observability.delegate_packet_compaction;
249
+ const observability = {
250
+ curator_ms: effectiveDelegateResult?.observability.curator_ms ?? null,
251
+ finalizer_ms: effectiveDelegateResult?.observability.finalizer_ms ?? null,
252
+ candidate_pool_size: discovery.content_candidates.length,
253
+ // Deprecated alias (see ContextBuilderObservabilityMetrics): historical
254
+ // name for the packet ESTIMATED-TOKEN metric. Kept byte-identical for
255
+ // compatibility; new readers should use delegate_packet_estimated_tokens.
256
+ delegate_packet_size: delegatePacketEstimatedTokens,
257
+ delegate_packet_estimated_tokens: delegatePacketEstimatedTokens,
258
+ ...(delegatePacketCompaction == null
259
+ ? {}
260
+ : { delegate_packet_compaction: delegatePacketCompaction }),
261
+ };
181
262
  const renderInput = {
182
263
  run_id: runId,
183
264
  created_at: createdAt,
184
265
  task: input.task,
266
+ allowSecrets: input.allow_secrets,
185
267
  roots: rootResolution.roots.map((root) => ({
186
268
  root_index: root.root_index,
187
269
  input_path: root.input_path,
@@ -207,245 +289,226 @@ export async function handleContextBuilder(args, options = {}) {
207
289
  omissions,
208
290
  warnings,
209
291
  metrics,
292
+ search_coverage: discovery.search_coverage,
210
293
  open_questions: openQuestions,
211
294
  ...(delegateMetadata == null ? {} : { delegate: delegateMetadata }),
295
+ observability,
296
+ coverage,
297
+ status_evidence: { coverage },
212
298
  debug: input.debug,
213
299
  };
214
- await progress?.emit("writing");
215
- const receipt = await writeContextBuilderFinalArtifacts(storage, renderInput, {
216
- ...(options.signal == null ? {} : { signal: options.signal }),
217
- ...(storage.ownerNonce == null ? {} : { ownerNonce: storage.ownerNonce }),
300
+ await lifecycle?.transition("writing", {
301
+ ...delegateLifecyclePatch(effectiveDelegateResult, lifecycle),
302
+ counts: lifecycleCountsFromMetrics(metrics, discovery.search.length),
303
+ coverage: lifecycleCoverage(coverage),
304
+ ...(effectiveDelegateResult?.delegate.fallback_used === true
305
+ ? {
306
+ fallback: {
307
+ status: "used",
308
+ reason_code: "delegate_fallback",
309
+ },
310
+ reasonCodes: ["delegate_fallback"],
311
+ }
312
+ : {}),
218
313
  });
314
+ // Capture into a const so the beforeManifestCommit closure keeps the
315
+ // narrowed non-undefined type (the outer binding is a reassignable let).
316
+ const committingLifecycle = lifecycle;
317
+ const receipt = await withContextBuilderWriteWatchdog((signal) => writeContextBuilderFinalArtifacts(writingStorage, renderInput, {
318
+ signal,
319
+ ...(writingStorage.ownerNonce == null ? {} : { ownerNonce: writingStorage.ownerNonce }),
320
+ ...(!ownsLifecycle || committingLifecycle == null
321
+ ? {}
322
+ : { beforeManifestCommit: () => committingLifecycle.transition("committing") }),
323
+ }), options.signal == null ? {} : { signal: options.signal });
219
324
  committed = true;
220
- await progress?.emit("complete");
221
- return createContextBuilderReceiptTextResponse(receipt);
325
+ if (ownsLifecycle) {
326
+ await recordContextRunCatalogMarker({
327
+ runsDir,
328
+ kind: "artifact",
329
+ id: receipt.run_id,
330
+ createdAt,
331
+ }).catch(() => {
332
+ logger.warn({ contextRunId: receipt.run_id }, "context run catalog marker write failed");
333
+ });
334
+ }
335
+ let responseReceipt = receipt;
336
+ if (ownsLifecycle) {
337
+ try {
338
+ await lifecycle?.complete(receipt.status, {
339
+ artifactRunId: receipt.run_id,
340
+ counts: lifecycleCountsFromMetrics(receipt.metrics, discovery.search.length),
341
+ coverage: lifecycleCoverage(coverage),
342
+ });
343
+ }
344
+ catch (error) {
345
+ if (!(error instanceof ContextBuilderLifecycleObserverError))
346
+ throw error;
347
+ logger.warn({ contextRunId: lifecycle?.contextRunId }, "context run lifecycle completion unavailable after retry");
348
+ responseReceipt = appendLifecycleCompletionWarning(receipt, internalCaps.max_warnings);
349
+ }
350
+ }
351
+ return createContextBuilderReceiptTextResponse(responseReceipt);
222
352
  }
223
353
  catch (err) {
224
354
  if (storage != null && !committed && storage.ownerNonce != null) {
225
355
  await cleanupContextBuilderIncompleteRun(storage, storage.ownerNonce).catch(() => { });
226
356
  }
357
+ if (ownsLifecycle && lifecycle != null &&
358
+ !(err instanceof ContextBuilderLifecycleObserverError)) {
359
+ const cancelled = err instanceof CancellationError || options.signal?.aborted === true;
360
+ const delegateFailed = input.delegate.enabled && delegateExecutionActive(lifecycle);
361
+ await lifecycle.fail(cancelled ? "cancelled" : "failed", {
362
+ ...terminalExecutionPatch(lifecycle, cancelled),
363
+ reasonCodes: [
364
+ cancelled
365
+ ? "operation_cancelled"
366
+ : delegateFailed
367
+ ? "delegate_failed"
368
+ : "internal_failure",
369
+ ],
370
+ ...(!cancelled && delegateFailed
371
+ ? {
372
+ fallback: {
373
+ status: "failed",
374
+ reason_code: "delegate_failed",
375
+ },
376
+ }
377
+ : {}),
378
+ });
379
+ }
227
380
  throw err;
228
381
  }
229
382
  finally {
230
383
  semaphore.release();
231
384
  }
232
385
  }
233
- function pruneTaskSpecificFinalSelection(input) {
234
- const taskSpineRepaired = repairTaskSpineRanges({
235
- selection: input.selection,
236
- candidates: input.contentCandidates,
237
- budget: input.budget,
238
- task: input.task,
239
- sourceRoots: input.sourceRoots,
240
- });
241
- const closureExpanded = expandSelectedClosureFilesToFullCandidates(taskSpineRepaired, input.contentCandidates, input.task, input.budget);
242
- const discoveredRetained = retainDiscoveredTaskCriticalFiles(closureExpanded.selection, input.contentCandidates, input.task, input.budget);
243
- const closureRepaired = dropDegenerateSelectedSlices(discoveredRetained.selection, input.contentCandidates, input.task);
244
- const runtimePruned = pruneRuntimeSettingsDiscoverySearchNoise(closureRepaired.selection, input.task);
245
- const debugPruned = pruneDebugTraceSearchNoise(runtimePruned, input.contentCandidates, input.budget, input.task);
246
- const reviewPruned = pruneCodexRuntimeReviewSearchNoise(debugPruned, input.task);
247
- return rankTaskSpecificFinalSelection(reviewPruned, input.task);
248
- }
249
- function validateBenchmarkPacketQuality(input) {
250
- const axes = benchmarkPacketQaAxes(input.task);
251
- if (axes.length === 0) {
252
- return { warnings: [] };
253
- }
254
- const selected = input.selection.selected.filter((file) => file.content.trim().length > 0);
255
- const summaryFiles = input.selection.summary?.files ?? [];
256
- const gaps = [];
257
- for (const axis of axes) {
258
- if (selected.some((file) => axis.matches(file.path))) {
259
- continue;
260
- }
261
- const candidatePaths = input.contentCandidates
262
- .filter((candidate) => axis.matches(candidate.path))
263
- .map((candidate) => candidate.path);
264
- const summaryOnlyPaths = summaryFiles
265
- .filter((file) => axis.matches(file.path))
266
- .map((file) => file.path);
267
- if (candidatePaths.length === 0 && summaryOnlyPaths.length === 0) {
268
- continue;
269
- }
270
- gaps.push({
271
- axis,
272
- candidatePaths: boundContextBuilderPathList(candidatePaths),
273
- summaryOnlyPaths: boundContextBuilderPathList(summaryOnlyPaths),
274
- });
275
- }
276
- if (gaps.length === 0) {
277
- return { warnings: [] };
278
- }
279
- const warnings = gaps.map((gap) => delegateWarning("packet_qa", packetQaGapMessage(gap.axis.label, gap.candidatePaths, gap.summaryOnlyPaths)));
280
- const materialGaps = gaps.filter((gap) => gap.candidatePaths.length > 0);
281
- if (materialGaps.length === 0) {
282
- return { warnings };
386
+ function lifecycleExecutionConfig(input) {
387
+ if (!input.delegate.enabled) {
388
+ return {
389
+ curator: { configured: false, enabled: false },
390
+ finalizer: { configured: false, enabled: false },
391
+ };
283
392
  }
284
393
  return {
285
- warnings,
286
- materialFailureReason: `packet QA missing source evidence for task axis: ${materialGaps
287
- .map((gap) => `${gap.axis.label} (${gap.candidatePaths.join(", ")})`)
288
- .join("; ")}`,
394
+ curator: { configured: true, enabled: true },
395
+ finalizer: {
396
+ configured: true,
397
+ enabled: contextBuilderFinalizerEnabled(input),
398
+ },
289
399
  };
290
400
  }
291
- function packetQaGapMessage(axis, candidatePaths, summaryOnlyPaths) {
292
- const paths = candidatePaths.length > 0 ? candidatePaths : summaryOnlyPaths;
293
- const backing = candidatePaths.length > 0 && summaryOnlyPaths.length > 0
294
- ? "available source candidate only represented in selection summary"
295
- : candidatePaths.length > 0
296
- ? "available source candidate"
297
- : "selection summary";
298
- return `packet QA found ${backing} for task axis "${axis}" without source evidence in selected context: ${paths.join(", ")}`;
299
- }
300
- function benchmarkPacketQaAxes(task) {
301
- if (isCodexRuntimeReviewTask(task)) {
302
- return codexRuntimeReviewQaAxes(task);
303
- }
304
- if (isDevtoolsTraceDebugTask(task)) {
305
- return devtoolsTraceDebugQaAxes(task);
306
- }
307
- if (isRuntimeSettingsDiscoveryTask(task)) {
308
- return runtimeSettingsDiscoveryQaAxes(task);
309
- }
310
- return [];
311
- }
312
- function codexRuntimeReviewQaAxes(task) {
313
- const normalized = task.toLowerCase();
314
- const axes = [
315
- {
316
- label: "Codex runtime implementation",
317
- matches: (path) => {
318
- const normalizedPath = path.toLowerCase();
319
- return (normalizedPath.endsWith("/services/runtime/codex.py") ||
320
- normalizedPath.endsWith("/services/runtime/codex_stream_processor.py") ||
321
- normalizedPath.endsWith("/services/runtime/base.py") ||
322
- normalizedPath.endsWith("/services/runtime/resolver.py"));
323
- },
324
- },
325
- ];
326
- if (/\b(approval|permission|permissions?)\b/.test(normalized)) {
327
- axes.push({
328
- label: "Codex approval and permission flow",
329
- matches: (path) => {
330
- const normalizedPath = path.toLowerCase();
331
- return (normalizedPath.endsWith("/services/runtime/codex_approval.py") ||
332
- normalizedPath.endsWith("/services/permission_manager.py") ||
333
- normalizedPath.endsWith("/api/endpoints/permissions.py") ||
334
- normalizedPath.endsWith("/models/schemas/permissions.py") ||
335
- normalizedPath.endsWith("/models/schemas/permission_mode.py") ||
336
- normalizedPath.endsWith("/tests/test_permissions.py") ||
337
- normalizedPath.includes("/permission-mode-selector/"));
338
- },
339
- });
340
- }
341
- if (/\b(session|orchestrat|stream|dispatch)\b/.test(normalized)) {
342
- axes.push({
343
- label: "runtime session and orchestration flow",
344
- matches: (path) => {
345
- const normalizedPath = path.toLowerCase();
346
- return (normalizedPath.endsWith("/services/runtime_session_service.py") ||
347
- normalizedPath.endsWith("/models/db_models/runtime_session.py") ||
348
- normalizedPath.endsWith("/services/streaming/orchestrator.py") ||
349
- normalizedPath.endsWith("/api/endpoints/chat.py") ||
350
- normalizedPath.endsWith("/services/chat.py") ||
351
- normalizedPath.endsWith("/tests/services/test_runtime_session_service.py"));
352
- },
353
- });
354
- }
355
- if (/\b(frontend|consumer|attachment|upload|ui|settings?)\b/.test(normalized)) {
356
- axes.push({
357
- label: "frontend Codex consumer flow",
358
- matches: (path) => {
359
- const normalizedPath = path.toLowerCase();
360
- return (normalizedPath.endsWith("/hooks/usechatruntimesettings.ts") ||
361
- normalizedPath.endsWith("/hooks/usemessageactions.ts") ||
362
- normalizedPath.endsWith("/components/ui/fileuploaddialog.tsx") ||
363
- normalizedPath.endsWith("/components/chat/message-input/input.tsx") ||
364
- normalizedPath.endsWith("/components/chat/message-input/composerruntimecontrols.tsx") ||
365
- normalizedPath.endsWith("/utils/modelprovider.ts"));
366
- },
367
- });
401
+ function lifecycleCountsFromDiscovery(discovery) {
402
+ return {
403
+ files_seen: discovery.metrics.files_seen,
404
+ files_considered: discovery.metrics.files_considered,
405
+ files_selected: 0,
406
+ files_omitted: discovery.omissions.length,
407
+ search_results: discovery.search.length,
408
+ bytes_read: discovery.metrics.bytes_read,
409
+ bytes_written: 0,
410
+ };
411
+ }
412
+ function lifecycleCountsFromMetrics(metrics, searchResults) {
413
+ return {
414
+ files_seen: metrics.files_seen,
415
+ files_considered: metrics.files_considered,
416
+ files_selected: metrics.files_selected,
417
+ files_omitted: metrics.files_omitted,
418
+ search_results: searchResults,
419
+ bytes_read: metrics.bytes_read,
420
+ bytes_written: metrics.bytes_written,
421
+ };
422
+ }
423
+ function lifecycleCoverage(coverage) {
424
+ const required = coverage.required_when_available.length;
425
+ const missing = coverage.missing_required.length;
426
+ return {
427
+ policy: coverage.policy_id,
428
+ status: coverage.status,
429
+ required,
430
+ selected: Math.max(0, required - missing),
431
+ missing,
432
+ };
433
+ }
434
+ function delegateLifecyclePatch(result, lifecycle) {
435
+ if (result == null)
436
+ return {};
437
+ if (result.delegate.fallback_used) {
438
+ if (lifecycle?.snapshot?.phase === "finalizing") {
439
+ return { curatorStatus: "succeeded", finalizerStatus: "failed" };
440
+ }
441
+ return {
442
+ curatorStatus: "failed",
443
+ ...(lifecycle?.snapshot?.finalizer.effective_enabled === true
444
+ ? { finalizerStatus: "skipped" }
445
+ : {}),
446
+ };
368
447
  }
369
- return axes;
370
- }
371
- function devtoolsTraceDebugQaAxes(task) {
372
- const normalized = task.toLowerCase();
373
- const axes = [
374
- {
375
- label: "DevTools trace panel UI",
376
- matches: (path) => {
377
- const normalizedPath = path.toLowerCase();
378
- return (normalizedPath.endsWith("/components/right-panel/rightpaneldevtools.tsx") ||
379
- normalizedPath.endsWith("/store/devtoolsnavstore.ts") ||
380
- normalizedPath.endsWith("/services/devtoolsservice.ts") ||
381
- normalizedPath.endsWith("/hooks/queries/usedevtoolsqueries.ts"));
382
- },
383
- },
384
- {
385
- label: "trace persistence and API flow",
386
- matches: (path) => {
387
- const normalizedPath = path.toLowerCase();
388
- return (normalizedPath.endsWith("/api/endpoints/devtools.py") ||
389
- normalizedPath.endsWith("/services/session_trace_archive.py") ||
390
- normalizedPath.endsWith("/services/codex_trace.py") ||
391
- normalizedPath.endsWith("/models/schemas/devtools.py") ||
392
- normalizedPath.endsWith("/types/devtools.types.ts"));
393
- },
394
- },
395
- ];
396
- if (/\b(chat|message|autoscroll|tool)\b/.test(normalized)) {
397
- axes.push({
398
- label: "chat trace rendering flow",
399
- matches: (path) => {
400
- const normalizedPath = path.toLowerCase();
401
- return (normalizedPath.endsWith("/components/chat/chat-window/chat.tsx") ||
402
- normalizedPath.endsWith("/components/chat/message-bubble/messagerenderer.tsx") ||
403
- normalizedPath.endsWith("/components/chat/tools/common/toolcard.tsx") ||
404
- normalizedPath.endsWith("/hooks/usemessagecache.ts") ||
405
- normalizedPath.endsWith("/utils/messagecacheorder.ts"));
406
- },
407
- });
448
+ return {
449
+ curatorStatus: "succeeded",
450
+ ...(result.delegate.finalizer_score == null
451
+ ? {}
452
+ : {
453
+ finalizerStatus: "succeeded",
454
+ finalizerScore: result.delegate.finalizer_score,
455
+ }),
456
+ };
457
+ }
458
+ function terminalExecutionPatch(lifecycle, cancelled) {
459
+ const snapshot = lifecycle.snapshot;
460
+ if (snapshot == null)
461
+ return {};
462
+ const terminalStatus = cancelled ? "cancelled" : "failed";
463
+ return {
464
+ ...(snapshot.curator.execution_status === "running"
465
+ ? { curatorStatus: terminalStatus }
466
+ : snapshot.curator.execution_status === "pending"
467
+ ? { curatorStatus: "skipped" }
468
+ : {}),
469
+ ...(snapshot.finalizer.execution_status === "running"
470
+ ? { finalizerStatus: terminalStatus }
471
+ : snapshot.finalizer.execution_status === "pending"
472
+ ? { finalizerStatus: "skipped" }
473
+ : {}),
474
+ };
475
+ }
476
+ function delegateExecutionActive(lifecycle) {
477
+ const snapshot = lifecycle.snapshot;
478
+ return snapshot?.curator.execution_status === "running" ||
479
+ snapshot?.finalizer.execution_status === "running";
480
+ }
481
+ function evidenceClassesByCandidate(candidates) {
482
+ return new Map(candidates.map((candidate) => [
483
+ `${candidate.root_index}\u0000${candidate.path}`,
484
+ candidate.evidence_classes ?? [],
485
+ ]));
486
+ }
487
+ function evaluateSelectionCoverage(input) {
488
+ const classesByCandidate = evidenceClassesByCandidate(input.candidates);
489
+ const selectedClasses = input.selected.flatMap((file) => classesByCandidate.get(`${file.root_index}\u0000${file.path}`) ?? []);
490
+ return evaluateContextCoverage({
491
+ policy: input.policy,
492
+ available: input.candidates.flatMap((candidate) => candidate.evidence_classes ?? []),
493
+ selected: selectedClasses,
494
+ unclassifiable: input.policy.id == null
495
+ ? []
496
+ : ["public_entrypoint", "reverse_consumer"],
497
+ ...(input.obligations == null ? {} : { obligations: input.obligations }),
498
+ selectedRanges: input.selected,
499
+ });
500
+ }
501
+ function delegateCoverageSelectionFailure(selected, candidates, coveragePolicy) {
502
+ try {
503
+ validateContextBuilderDelegateCoverageSelection(selected.map((file) => ({
504
+ candidate_id: contextBuilderCandidateId(file.root_index, file.path),
505
+ ranges: file.ranges,
506
+ })), candidates, coveragePolicy);
507
+ return null;
408
508
  }
409
- return axes;
410
- }
411
- function runtimeSettingsDiscoveryQaAxes(task) {
412
- const normalized = task.toLowerCase();
413
- const axes = [
414
- {
415
- label: "runtime settings UI control flow",
416
- matches: (path) => {
417
- const normalizedPath = path.toLowerCase();
418
- return (normalizedPath.endsWith("/hooks/usechatruntimesettings.ts") ||
419
- normalizedPath.endsWith("/components/chat/message-input/composerruntimecontrols.tsx") ||
420
- normalizedPath.endsWith("/components/chat/message-input/input.tsx") ||
421
- normalizedPath.endsWith("/pages/chatpage.tsx"));
422
- },
423
- },
424
- {
425
- label: "runtime settings dispatch and persistence flow",
426
- matches: (path) => {
427
- const normalizedPath = path.toLowerCase();
428
- return (normalizedPath.endsWith("/services/chatservice.ts") ||
429
- normalizedPath.endsWith("/api/endpoints/chat.py") ||
430
- normalizedPath.endsWith("/services/chat.py") ||
431
- normalizedPath.endsWith("/store/messagequeuestore.ts"));
432
- },
433
- },
434
- ];
435
- if (/\b(provider|session|approval|permission|runtime)\b/.test(normalized)) {
436
- axes.push({
437
- label: "runtime provider/session support flow",
438
- matches: (path) => {
439
- const normalizedPath = path.toLowerCase();
440
- return (normalizedPath.endsWith("/services/runtime/codex.py") ||
441
- normalizedPath.endsWith("/services/runtime/claude.py") ||
442
- normalizedPath.endsWith("/services/runtime/codex_approval.py") ||
443
- normalizedPath.endsWith("/services/runtime_session_service.py") ||
444
- normalizedPath.endsWith("/models/db_models/runtime_session.py"));
445
- },
446
- });
509
+ catch (error) {
510
+ return error instanceof Error ? error.message : String(error);
447
511
  }
448
- return axes;
449
512
  }
450
513
  async function rejectArtifactOnlySourceRoots(inputRoots, input) {
451
514
  if (artifactOnlyAllowedRoots.size === 0) {
@@ -467,13 +530,70 @@ async function rejectArtifactOnlySourceRoots(inputRoots, input) {
467
530
  }
468
531
  }
469
532
  }
533
+ /**
534
+ * Fail-fast admission check: an explicitly-focused path that the caller's own
535
+ * (or the default) globs would prune is a contradictory request. Discovery
536
+ * only surfaces these later, one gate at a time, as fatal runtime omissions,
537
+ * so aggregate EVERY conflict up front into one InvalidParamsError before any
538
+ * scanning or delegate subprocess work begins.
539
+ *
540
+ * The check mirrors discovery's glob semantics exactly: the matcher is
541
+ * compiled from include_globs plus CONTEXT_BUILDER_DEFAULT_EXCLUDE_GLOBS +
542
+ * exclude_globs (discoverContextBuilderCandidates does the same), focus FILES
543
+ * are matched on their own root-relative path (processFile's includes/excludes
544
+ * gates), and focus DIRECTORIES are flagged only when the directory itself is
545
+ * exclude-pruned via the same `<dir>` / `<dir>/__dir__` probes traverseDirectory
546
+ * uses. Ancestor-directory exclusion and directory-vs-include pruning are
547
+ * deliberately NOT modeled here (they depend on traversal state), so anything
548
+ * not flagged falls through to discovery's existing runtime handling.
549
+ */
550
+ function assertNoContextBuilderFocusGlobConflicts(input) {
551
+ const matcher = compileContextBuilderGlobs({
552
+ include: [...input.includeGlobs],
553
+ exclude: [
554
+ ...CONTEXT_BUILDER_DEFAULT_EXCLUDE_GLOBS,
555
+ ...input.excludeGlobs,
556
+ ],
557
+ });
558
+ const conflicts = [];
559
+ const seen = new Set();
560
+ for (const focus of input.focusPaths) {
561
+ const relative = focus.root_relative_path;
562
+ if (relative === "") {
563
+ continue;
564
+ }
565
+ const key = `${focus.root_index}\u0000${relative}\u0000${focus.kind}`;
566
+ if (seen.has(key)) {
567
+ continue;
568
+ }
569
+ seen.add(key);
570
+ if (focus.kind === "directory") {
571
+ if (matcher.excludes(relative) ||
572
+ matcher.excludes(`${relative}/__dir__`)) {
573
+ conflicts.push(`focus directory "${relative}" (root_index ${focus.root_index}) is pruned by exclude_globs`);
574
+ }
575
+ continue;
576
+ }
577
+ if (matcher.excludes(relative)) {
578
+ conflicts.push(`focus path "${relative}" (root_index ${focus.root_index}) is omitted by exclude_globs`);
579
+ continue;
580
+ }
581
+ if (!matcher.includes(relative)) {
582
+ conflicts.push(`focus path "${relative}" (root_index ${focus.root_index}) is outside include_globs`);
583
+ }
584
+ }
585
+ if (conflicts.length > 0) {
586
+ throw new InvalidParamsError(`context_builder focus_paths conflict with the effective include/exclude globs (${conflicts.length} conflict${conflicts.length === 1 ? "" : "s"}): ${conflicts.join("; ")}`);
587
+ }
588
+ }
470
589
  function getExactlyConfiguredArtifactRoots() {
471
590
  const rawEntries = rawAllowedRootEntries();
472
591
  const configured = new Set();
473
592
  const cwd = path.resolve(process.cwd());
474
593
  for (const artifactRoot of artifactOnlyAllowedRoots) {
475
594
  for (const rawEntry of rawEntries) {
476
- if (rawEntry === artifactOnlyEnvEntry(artifactRoot)) {
595
+ if (isArtifactOnlyAllowedRootEntry(rawEntry) &&
596
+ realpathSyncOrUndefined(path.resolve(cwd, stripArtifactOnlyMarker(rawEntry))) === artifactRoot) {
477
597
  continue;
478
598
  }
479
599
  const resolvedEntry = realpathSyncOrUndefined(path.resolve(cwd, rawEntry));
@@ -486,37 +606,7 @@ function getExactlyConfiguredArtifactRoots() {
486
606
  return configured;
487
607
  }
488
608
  function appendArtifactOnlyRootToEnv(runsDir) {
489
- const raw = process.env["CLINKX_ALLOWED_ROOTS"];
490
- const cwd = path.resolve(process.cwd());
491
- const entries = raw == null || raw.trim() === ""
492
- ? [cwd]
493
- : rawAllowedRootEntries();
494
- let runsDirAlreadyAllowed = false;
495
- for (const entry of entries) {
496
- const resolved = tryResolveRawAllowedRootEntry(entry, cwd);
497
- if (resolved != null && resolved === runsDir) {
498
- runsDirAlreadyAllowed = true;
499
- }
500
- }
501
- process.env["CLINKX_ALLOWED_ROOTS"] = [
502
- ...entries,
503
- ...(runsDirAlreadyAllowed ? [] : [artifactOnlyEnvEntry(runsDir)]),
504
- ].join(",");
505
- }
506
- function tryResolveRawAllowedRootEntry(entry, cwd) {
507
- try {
508
- const resolved = realpathSync(path.resolve(cwd, entry));
509
- if (path.parse(resolved).root === resolved) {
510
- throw new InvalidParamsError(`Allowed root must not be a filesystem root: ${resolved}`);
511
- }
512
- return resolved;
513
- }
514
- catch (error) {
515
- if (error instanceof InvalidParamsError) {
516
- throw error;
517
- }
518
- return undefined;
519
- }
609
+ appendArtifactOnlyRootsToEnv([runsDir], { requireExisting: false });
520
610
  }
521
611
  function rawAllowedRootEntries() {
522
612
  const raw = process.env["CLINKX_ALLOWED_ROOTS"];
@@ -528,9 +618,6 @@ function rawAllowedRootEntries() {
528
618
  .map((root) => root.trim())
529
619
  .filter((root) => root.length > 0);
530
620
  }
531
- function artifactOnlyEnvEntry(root) {
532
- return root.endsWith(path.sep) ? `${root}.` : `${root}${path.sep}.`;
533
- }
534
621
  function realpathSyncOrUndefined(input) {
535
622
  try {
536
623
  return realpathSync(input);
@@ -540,12 +627,7 @@ function realpathSyncOrUndefined(input) {
540
627
  }
541
628
  }
542
629
  function getDefaultContextBuilderSemaphore() {
543
- const limit = getContextBuilderMaxConcurrent();
544
- if (defaultSemaphore == null ||
545
- defaultSemaphore.limit !== limit) {
546
- defaultSemaphore = new ContextBuilderSemaphore(limit);
547
- }
548
- return defaultSemaphore;
630
+ return getSharedContextBuilderSemaphore();
549
631
  }
550
632
  function getDefaultAdapterRegistry() {
551
633
  if (defaultAdapterRegistry == null) {
@@ -594,6 +676,7 @@ async function resolveDelegateSelection(input) {
594
676
  const finalizerEnabled = contextBuilderFinalizerEnabled(input.input);
595
677
  const delegatePacketInput = {
596
678
  task: input.input.task,
679
+ allowSecrets: input.input.allow_secrets,
597
680
  discovery: {
598
681
  tree: input.discovery.tree,
599
682
  content_candidates: input.discovery.content_candidates,
@@ -601,38 +684,78 @@ async function resolveDelegateSelection(input) {
601
684
  slices: input.discovery.slices,
602
685
  omissions: [...input.baselineOmissions, ...input.discovery.omissions],
603
686
  metrics: input.discovery.metrics,
687
+ ...(input.discovery.review_obligations == null
688
+ ? {}
689
+ : { review_obligations: input.discovery.review_obligations }),
604
690
  },
605
691
  selection: input.deterministicSelection,
606
692
  budget: input.input.budget,
607
693
  focus_paths: input.focusPaths,
694
+ caller_search_terms: input.input.search_terms,
695
+ coveragePolicy: input.coveragePolicy,
608
696
  ...(input.delegatePacketCaps == null
609
697
  ? {}
610
698
  : { caps: input.delegatePacketCaps }),
611
699
  };
612
700
  const candidateFallbackCount = input.deterministicSelection.selected.length;
701
+ let curatorMs = null;
702
+ let finalizerMs = null;
703
+ let delegatePacketSize = 0;
704
+ // Compaction counters from the last-built packet (finalizer overrides
705
+ // curator) and bounded T3 evidence warnings, surfaced additively.
706
+ let delegatePacketCompaction;
707
+ const packetEvidenceWarnings = [];
708
+ const recordPacketEvidenceWarning = (warning) => {
709
+ if (warning == null ||
710
+ packetEvidenceWarnings.some((existing) => existing.code === warning.code)) {
711
+ return;
712
+ }
713
+ packetEvidenceWarnings.push({
714
+ code: warning.code,
715
+ message: warning.message,
716
+ });
717
+ };
718
+ const currentDelegateObservability = () => ({
719
+ curator_ms: curatorMs,
720
+ finalizer_ms: finalizerMs,
721
+ delegate_packet_size: delegatePacketSize,
722
+ ...(delegatePacketCompaction == null
723
+ ? {}
724
+ : { delegate_packet_compaction: delegatePacketCompaction }),
725
+ });
726
+ const sessionLogOwner = (executor) => input.sessionLogContext == null
727
+ ? undefined
728
+ : {
729
+ family: "context",
730
+ contextRunId: input.sessionLogContext.contextRunId,
731
+ contextGeneration: input.sessionLogContext.contextGeneration,
732
+ executor,
733
+ executionId: `${executor}_${input.sessionLogContext.contextRunId.slice(-32)}`,
734
+ attempt: 1,
735
+ };
613
736
  const requestedFocusOmissions = input.discovery.omissions.filter(isRequestedEvidenceOmission);
614
737
  if (requestedFocusOmissions.length > 0) {
615
738
  return handleDelegateRuntimeFailure({
616
739
  delegate,
617
- reason: `requested local evidence omitted: ${requestedFocusOmissions
618
- .map(formatOmissionForDelegateFailure)
619
- .join("; ")}`,
740
+ reason: `requested local evidence omitted: ${formatOmissionsForDelegateFailure(requestedFocusOmissions)}`,
620
741
  deterministicSelection: input.deterministicSelection,
621
742
  omissionCount: input.baselineOmissions.length +
622
743
  input.discovery.omissions.length +
623
744
  input.deterministicSelection.omissions.length,
624
745
  candidateFallbackCount,
746
+ observability: currentDelegateObservability(),
625
747
  });
626
748
  }
627
- await input.progress?.emit("curating");
749
+ await input.lifecycle?.transition("curating", { curatorStatus: "running" });
628
750
  const curatorPacket = buildContextBuilderCuratorPacket(delegatePacketInput);
751
+ delegatePacketSize = curatorPacket.metrics.packet_estimated_tokens;
752
+ delegatePacketCompaction = curatorPacket.metrics.compaction;
753
+ recordPacketEvidenceWarning(curatorPacket.evidence_warning);
629
754
  const curatorPacketRequestedOmissions = curatorPacket.omissions.filter(isRequestedEvidenceOmission);
630
755
  if (curatorPacketRequestedOmissions.length > 0) {
631
756
  return handleDelegateRuntimeFailure({
632
757
  delegate,
633
- reason: `requested local evidence omitted: ${curatorPacketRequestedOmissions
634
- .map(formatOmissionForDelegateFailure)
635
- .join("; ")}`,
758
+ reason: `requested local evidence omitted: ${formatOmissionsForDelegateFailure(curatorPacketRequestedOmissions)}`,
636
759
  deterministicSelection: input.deterministicSelection,
637
760
  omissions: curatorPacketRequestedOmissions,
638
761
  omissionCount: input.baselineOmissions.length +
@@ -640,6 +763,7 @@ async function resolveDelegateSelection(input) {
640
763
  input.deterministicSelection.omissions.length +
641
764
  curatorPacketRequestedOmissions.length,
642
765
  candidateFallbackCount,
766
+ observability: currentDelegateObservability(),
643
767
  });
644
768
  }
645
769
  if (curatorPacket.candidates.length === 0) {
@@ -651,8 +775,11 @@ async function resolveDelegateSelection(input) {
651
775
  input.discovery.omissions.length +
652
776
  input.deterministicSelection.omissions.length,
653
777
  candidateFallbackCount,
778
+ observability: currentDelegateObservability(),
654
779
  });
655
780
  }
781
+ const curatorStartMs = Date.now();
782
+ const curatorSessionLogOwner = sessionLogOwner("curator");
656
783
  const curatorCall = await runContextBuilderDelegateCall({
657
784
  prompt: renderDelegatePrompt("curator", curatorPacket),
658
785
  provider: delegate.curator,
@@ -660,16 +787,22 @@ async function resolveDelegateSelection(input) {
660
787
  timeout_seconds: delegate.timeout_seconds,
661
788
  max_response_chars: delegate.max_response_chars,
662
789
  ...(input.signal == null ? {} : { signal: input.signal }),
790
+ ...(curatorSessionLogOwner == null
791
+ ? {}
792
+ : { sessionLogOwner: curatorSessionLogOwner }),
663
793
  });
794
+ curatorMs = Math.max(0, Date.now() - curatorStartMs);
664
795
  if (!curatorCall.ok) {
665
796
  return handleDelegateRuntimeFailure({
666
797
  delegate,
667
798
  reason: failureReasonFromCall("curator", curatorCall),
799
+ failureKind: curatorCall.reason,
668
800
  deterministicSelection: input.deterministicSelection,
669
801
  omissionCount: input.baselineOmissions.length +
670
802
  input.discovery.omissions.length +
671
803
  input.deterministicSelection.omissions.length,
672
804
  candidateFallbackCount,
805
+ observability: currentDelegateObservability(),
673
806
  });
674
807
  }
675
808
  let curatorProposal;
@@ -678,6 +811,8 @@ async function resolveDelegateSelection(input) {
678
811
  const validatedCuratorProposal = validateContextBuilderCuratorOutput(curatorCall.json, {
679
812
  candidates: curatorPacket.candidates,
680
813
  budget: input.input.budget,
814
+ allowSecrets: input.input.allow_secrets,
815
+ coveragePolicy: curatorPacket.coverage_policy,
681
816
  });
682
817
  curatorProposal = augmentCuratorProposalWithDeterministicBaseline(validatedCuratorProposal, curatorPacket, input.input.budget.max_files);
683
818
  curatorSelection = reconstructContextBuilderDelegateSelection({
@@ -727,6 +862,7 @@ async function resolveDelegateSelection(input) {
727
862
  input.discovery.omissions.length +
728
863
  input.deterministicSelection.omissions.length,
729
864
  candidateFallbackCount,
865
+ observability: currentDelegateObservability(),
730
866
  });
731
867
  }
732
868
  if (curatorSelection.selected.length === 0) {
@@ -738,24 +874,62 @@ async function resolveDelegateSelection(input) {
738
874
  input.discovery.omissions.length +
739
875
  input.deterministicSelection.omissions.length,
740
876
  candidateFallbackCount,
877
+ observability: currentDelegateObservability(),
741
878
  });
742
879
  }
743
880
  if (!finalizerEnabled) {
744
- const repairedSelection = repairDelegateSelectionForTask({
881
+ const repairedSelection = repairDelegateSelection({
745
882
  curatorSelection,
746
- deterministicSelection: input.deterministicSelection,
747
883
  contentCandidates: input.discovery.content_candidates,
748
884
  focusPaths: input.focusPaths,
749
885
  budget: input.input.budget,
750
- task: input.input.task,
751
- sourceRoots: input.sourceRoots,
886
+ expandSelectedFiles: true,
752
887
  });
753
888
  const warnings = curatorProposal.warnings.map((message) => delegateWarning("delegate_curator_warning", message));
889
+ warnings.push(...packetEvidenceWarnings);
754
890
  appendDelegateRepairWarnings(warnings, repairedSelection);
755
- const openQuestions = curatorProposal.open_questions.map(sanitizeDiagnosticText);
891
+ const openQuestions = curatorProposal.open_questions.map((question) => sanitizeDiagnosticText(question));
756
892
  const omissionCount = input.baselineOmissions.length +
757
893
  input.discovery.omissions.length +
758
894
  repairedSelection.selection.omissions.length;
895
+ const delegateEvidenceCoverageFailure = delegateCoverageSelectionFailure(restoreDroppedDegenerateReviewTargetsForValidation({
896
+ preRepairSelected: curatorSelection.selected,
897
+ postRepairSelected: repairedSelection.selection.selected,
898
+ candidates: curatorPacket.candidates,
899
+ coveragePolicy: curatorPacket.coverage_policy,
900
+ }), curatorPacket.candidates, curatorPacket.coverage_policy);
901
+ if (delegateEvidenceCoverageFailure != null) {
902
+ return handleDelegateRuntimeFailure({
903
+ delegate,
904
+ reason: `post-repair evidence coverage failed: ${delegateEvidenceCoverageFailure}`,
905
+ deterministicSelection: input.deterministicSelection,
906
+ omissionCount,
907
+ candidateFallbackCount,
908
+ observability: currentDelegateObservability(),
909
+ });
910
+ }
911
+ // Re-verify required-focus presence against the actual returned selection.
912
+ // The curator-only branch does not fold required-focus ranges, so it checks
913
+ // whole-file presence here; range coverage is re-verified only in the
914
+ // finalizer branch, which folds requested focus evidence first.
915
+ const curatorOnlyCoverageFailure = postRepairRequiredFocusEvictionFailure({
916
+ focusPaths: input.focusPaths,
917
+ candidates: curatorPacket.candidates,
918
+ preRepairSelected: curatorSelection.selected,
919
+ postRepairSelected: repairedSelection.selection.selected,
920
+ });
921
+ if (curatorOnlyCoverageFailure != null) {
922
+ return handleDelegateRuntimeFailure({
923
+ delegate,
924
+ reason: `post-repair required-focus coverage failed: ${curatorOnlyCoverageFailure}`,
925
+ deterministicSelection: input.deterministicSelection,
926
+ omissionCount: input.baselineOmissions.length +
927
+ input.discovery.omissions.length +
928
+ input.deterministicSelection.omissions.length,
929
+ candidateFallbackCount,
930
+ observability: currentDelegateObservability(),
931
+ });
932
+ }
759
933
  return {
760
934
  selection: repairedSelection.selection,
761
935
  warnings,
@@ -772,21 +946,26 @@ async function resolveDelegateSelection(input) {
772
946
  warning_count: warnings.length,
773
947
  omission_count: omissionCount,
774
948
  },
949
+ observability: currentDelegateObservability(),
775
950
  };
776
951
  }
777
- await input.progress?.emit("finalizing");
952
+ await input.lifecycle?.transition("finalizing", {
953
+ curatorStatus: "succeeded",
954
+ finalizerStatus: "running",
955
+ });
778
956
  const finalizerPacket = buildContextBuilderFinalizerPacket({
779
957
  ...delegatePacketInput,
780
958
  selection: curatorSelection,
781
959
  curator_proposal: curatorProposal,
782
960
  });
961
+ delegatePacketSize = finalizerPacket.metrics.packet_estimated_tokens;
962
+ delegatePacketCompaction = finalizerPacket.metrics.compaction;
963
+ recordPacketEvidenceWarning(finalizerPacket.evidence_warning);
783
964
  const finalizerPacketRequestedOmissions = finalizerPacket.omissions.filter(isRequestedEvidenceOmission);
784
965
  if (finalizerPacketRequestedOmissions.length > 0) {
785
966
  return handleDelegateRuntimeFailure({
786
967
  delegate,
787
- reason: `requested local evidence omitted: ${finalizerPacketRequestedOmissions
788
- .map(formatOmissionForDelegateFailure)
789
- .join("; ")}`,
968
+ reason: `requested local evidence omitted: ${formatOmissionsForDelegateFailure(finalizerPacketRequestedOmissions)}`,
790
969
  deterministicSelection: input.deterministicSelection,
791
970
  omissions: finalizerPacketRequestedOmissions,
792
971
  omissionCount: input.baselineOmissions.length +
@@ -794,6 +973,7 @@ async function resolveDelegateSelection(input) {
794
973
  input.deterministicSelection.omissions.length +
795
974
  finalizerPacketRequestedOmissions.length,
796
975
  candidateFallbackCount,
976
+ observability: currentDelegateObservability(),
797
977
  });
798
978
  }
799
979
  if ((finalizerPacket.curator_proposal?.selected.length ?? 0) === 0) {
@@ -805,25 +985,36 @@ async function resolveDelegateSelection(input) {
805
985
  input.discovery.omissions.length +
806
986
  input.deterministicSelection.omissions.length,
807
987
  candidateFallbackCount,
988
+ observability: currentDelegateObservability(),
808
989
  });
809
990
  }
991
+ const finalizerStartMs = Date.now();
992
+ const finalizerSessionLogOwner = sessionLogOwner("finalizer");
810
993
  const finalizerCall = await runContextBuilderDelegateCall({
811
- prompt: renderDelegatePrompt("finalizer", finalizerPacket),
994
+ prompt: renderDelegatePrompt("finalizer", finalizerPacket, {
995
+ minScore: delegate.finalizer.min_score,
996
+ }),
812
997
  provider: delegate.finalizer,
813
998
  registry: input.registry,
814
999
  timeout_seconds: delegate.timeout_seconds,
815
1000
  max_response_chars: delegate.max_response_chars,
816
1001
  ...(input.signal == null ? {} : { signal: input.signal }),
1002
+ ...(finalizerSessionLogOwner == null
1003
+ ? {}
1004
+ : { sessionLogOwner: finalizerSessionLogOwner }),
817
1005
  });
1006
+ finalizerMs = Math.max(0, Date.now() - finalizerStartMs);
818
1007
  if (!finalizerCall.ok) {
819
1008
  return handleDelegateRuntimeFailure({
820
1009
  delegate,
821
1010
  reason: failureReasonFromCall("finalizer", finalizerCall),
1011
+ failureKind: finalizerCall.reason,
822
1012
  deterministicSelection: input.deterministicSelection,
823
1013
  omissionCount: input.baselineOmissions.length +
824
1014
  input.discovery.omissions.length +
825
1015
  input.deterministicSelection.omissions.length,
826
1016
  candidateFallbackCount,
1017
+ observability: currentDelegateObservability(),
827
1018
  });
828
1019
  }
829
1020
  try {
@@ -831,6 +1022,8 @@ async function resolveDelegateSelection(input) {
831
1022
  candidates: finalizerPacket.candidates,
832
1023
  budget: input.input.budget,
833
1024
  minScore: delegate.finalizer.min_score,
1025
+ allowSecrets: input.input.allow_secrets,
1026
+ coveragePolicy: finalizerPacket.coverage_policy,
834
1027
  });
835
1028
  if (finalizerProposal.omissions.length > 0) {
836
1029
  return handleDelegateRuntimeFailure({
@@ -842,6 +1035,7 @@ async function resolveDelegateSelection(input) {
842
1035
  input.deterministicSelection.omissions.length +
843
1036
  finalizerProposal.omissions.length,
844
1037
  candidateFallbackCount,
1038
+ observability: currentDelegateObservability(),
845
1039
  });
846
1040
  }
847
1041
  const foldedFinalizerSelection = foldRequiredFocusEvidenceIntoSelection(finalizerProposal.selected, requiredFocusEvidenceSelections({
@@ -865,25 +1059,23 @@ async function resolveDelegateSelection(input) {
865
1059
  input.discovery.omissions.length +
866
1060
  input.deterministicSelection.omissions.length,
867
1061
  candidateFallbackCount,
1062
+ observability: currentDelegateObservability(),
868
1063
  });
869
1064
  }
870
- if (finalSelection.omissions.length > 0) {
871
- return handleDelegateRuntimeFailure({
872
- delegate,
873
- reason: `finalizer selection exceeded local reconstruction budget: ${finalSelection.omissions
874
- .map(formatOmissionForDelegateFailure)
875
- .join("; ")}`,
876
- deterministicSelection: input.deterministicSelection,
877
- omissionCount: input.baselineOmissions.length +
878
- input.discovery.omissions.length +
879
- finalSelection.omissions.length,
880
- candidateFallbackCount,
881
- });
882
- }
1065
+ // A per-file reconstruction-budget overrun no longer fails the entire
1066
+ // delegate selection. Over-budget candidates (e.g. large generated/i18n
1067
+ // catalogs the finalizer over-selected) are dropped while the rest of the
1068
+ // curated bundle is retained. Completeness is re-checked against the
1069
+ // MATERIALIZED (post-budget) selection, so a dropped REQUIRED-FOCUS file
1070
+ // still fails closed below, while non-required oversized files are pruned
1071
+ // and surfaced as a warning further down rather than collapsing the bundle.
1072
+ // Required-focus coverage is additionally re-verified against the
1073
+ // post-repair selection below, so eviction/compaction during repair also
1074
+ // fails closed.
883
1075
  const completenessFailure = delegateCompletenessFailure({
884
1076
  focusPaths: input.focusPaths,
885
1077
  candidates: finalizerPacket.candidates,
886
- finalizerSelected: foldedFinalizerSelection.selected,
1078
+ finalizerSelected: materializeFinalizerSelection(finalSelection.selected, finalizerPacket.candidates),
887
1079
  curatorWarnings: curatorProposal.warnings,
888
1080
  finalizerWarnings: finalizerProposal.warnings,
889
1081
  });
@@ -896,17 +1088,49 @@ async function resolveDelegateSelection(input) {
896
1088
  input.discovery.omissions.length +
897
1089
  input.deterministicSelection.omissions.length,
898
1090
  candidateFallbackCount,
1091
+ observability: currentDelegateObservability(),
899
1092
  });
900
1093
  }
901
- const repairedSelection = repairDelegateSelectionForTask({
1094
+ const repairedSelection = repairDelegateSelection({
902
1095
  curatorSelection: finalSelection,
903
- deterministicSelection: input.deterministicSelection,
904
1096
  contentCandidates: input.discovery.content_candidates,
905
1097
  focusPaths: input.focusPaths,
906
1098
  budget: input.input.budget,
907
- task: input.input.task,
908
- sourceRoots: input.sourceRoots,
1099
+ expandSelectedFiles: false,
1100
+ });
1101
+ const delegateEvidenceCoverageFailure = delegateCoverageSelectionFailure(repairedSelection.selection.selected, finalizerPacket.candidates, finalizerPacket.coverage_policy);
1102
+ if (delegateEvidenceCoverageFailure != null) {
1103
+ return handleDelegateRuntimeFailure({
1104
+ delegate,
1105
+ reason: `post-repair evidence coverage failed: ${delegateEvidenceCoverageFailure}`,
1106
+ deterministicSelection: input.deterministicSelection,
1107
+ omissionCount: input.baselineOmissions.length +
1108
+ input.discovery.omissions.length +
1109
+ repairedSelection.selection.omissions.length,
1110
+ candidateFallbackCount,
1111
+ observability: currentDelegateObservability(),
1112
+ });
1113
+ }
1114
+ // Re-verify required-focus completeness against the actual returned
1115
+ // selection. This keeps the fallback contract tied to the final materialized
1116
+ // packet rather than the delegate's requested selection.
1117
+ const postRepairCoverageFailure = requiredFocusCoverageFailure({
1118
+ focusPaths: input.focusPaths,
1119
+ candidates: finalizerPacket.candidates,
1120
+ finalizerSelected: materializeFinalizerSelection(repairedSelection.selection.selected, finalizerPacket.candidates),
909
1121
  });
1122
+ if (postRepairCoverageFailure != null) {
1123
+ return handleDelegateRuntimeFailure({
1124
+ delegate,
1125
+ reason: `post-repair required-focus coverage failed: ${postRepairCoverageFailure}`,
1126
+ deterministicSelection: input.deterministicSelection,
1127
+ omissionCount: input.baselineOmissions.length +
1128
+ input.discovery.omissions.length +
1129
+ input.deterministicSelection.omissions.length,
1130
+ candidateFallbackCount,
1131
+ observability: currentDelegateObservability(),
1132
+ });
1133
+ }
910
1134
  const warnings = [
911
1135
  ...curatorProposal.warnings.map((message) => delegateWarning("delegate_curator_warning", message)),
912
1136
  ...finalizerProposal.warnings.map((message) => delegateWarning("delegate_finalizer_warning", message)),
@@ -914,8 +1138,14 @@ async function resolveDelegateSelection(input) {
914
1138
  if (foldedFinalizerSelection.foldedPaths.length > 0) {
915
1139
  warnings.push(delegateWarning("delegate_finalizer_warning", `finalizer omitted requested local evidence; server folded it back into the selection: ${boundContextBuilderPathList(foldedFinalizerSelection.foldedPaths).join(", ")}`));
916
1140
  }
1141
+ if (finalSelection.omissions.length > 0) {
1142
+ warnings.push(delegateWarning("delegate_finalizer_warning", `finalizer selection pruned to fit the local reconstruction budget; omitted: ${finalSelection.omissions
1143
+ .map(formatOmissionForDelegateFailure)
1144
+ .join("; ")}`));
1145
+ }
1146
+ warnings.push(...packetEvidenceWarnings);
917
1147
  appendDelegateRepairWarnings(warnings, repairedSelection);
918
- const openQuestions = curatorProposal.open_questions.map(sanitizeDiagnosticText);
1148
+ const openQuestions = curatorProposal.open_questions.map((question) => sanitizeDiagnosticText(question));
919
1149
  const omissionCount = input.baselineOmissions.length +
920
1150
  input.discovery.omissions.length +
921
1151
  repairedSelection.selection.omissions.length;
@@ -936,6 +1166,7 @@ async function resolveDelegateSelection(input) {
936
1166
  warning_count: warnings.length,
937
1167
  omission_count: omissionCount,
938
1168
  },
1169
+ observability: currentDelegateObservability(),
939
1170
  };
940
1171
  }
941
1172
  catch (err) {
@@ -947,1193 +1178,74 @@ async function resolveDelegateSelection(input) {
947
1178
  }
948
1179
  return handleDelegateRuntimeFailure({
949
1180
  delegate,
950
- reason: failureReasonFromError("finalizer_validation", err, delegatePromptDiagnostic(finalizerPacket)),
1181
+ reason: failureReasonFromError("finalizer_validation", err, `${delegatePromptDiagnostic(finalizerPacket)}${finalizerScoreDiagnostic(finalizerCall.json, delegate.finalizer.min_score)}`),
951
1182
  deterministicSelection: input.deterministicSelection,
952
1183
  omissionCount: input.baselineOmissions.length +
953
1184
  input.discovery.omissions.length +
954
1185
  input.deterministicSelection.omissions.length,
955
1186
  candidateFallbackCount,
1187
+ observability: currentDelegateObservability(),
956
1188
  });
957
1189
  }
958
1190
  }
959
- function repairDelegateSelectionForTask(input) {
960
- const focusExpanded = expandSelectedFocusFilesToFullCandidates(input.curatorSelection, input.contentCandidates, input.focusPaths, input.budget, input.task);
961
- const closureExpanded = expandSelectedClosureFilesToFullCandidates(focusExpanded.selection, input.contentCandidates, input.task, input.budget);
962
- const degenerateDropped = dropDegenerateSelectedSlices(closureExpanded.selection, input.contentCandidates, input.task);
963
- const deterministicRetained = retainDeterministicClosureFiles(degenerateDropped.selection, input.deterministicSelection, input.budget, input.task);
964
- const discoveredRetained = retainDiscoveredTaskCriticalFiles(deterministicRetained.selection, input.contentCandidates, input.task, input.budget);
965
- const finalDegenerateDropped = dropDegenerateSelectedSlices(discoveredRetained.selection, input.contentCandidates, input.task);
966
- const taskSpineRepaired = repairTaskSpineRanges({
967
- selection: finalDegenerateDropped.selection,
968
- candidates: input.contentCandidates,
969
- budget: input.budget,
970
- task: input.task,
971
- sourceRoots: input.sourceRoots,
972
- });
973
- const runtimeNoisePruned = pruneRuntimeSettingsDiscoverySearchNoise(taskSpineRepaired, input.task);
974
- const debugNoisePruned = pruneDebugTraceSearchNoise(runtimeNoisePruned, input.contentCandidates, input.budget, input.task);
975
- const reviewNoisePruned = pruneCodexRuntimeReviewSearchNoise(debugNoisePruned, input.task);
976
- return {
977
- selection: reviewNoisePruned,
978
- expandedCount: focusExpanded.expandedCount + closureExpanded.expandedCount,
979
- deterministicRetainedCount: deterministicRetained.retainedCount,
980
- discoveredRetainedCount: discoveredRetained.retainedCount,
981
- evictedCount: discoveredRetained.evictedCount,
982
- droppedDegenerateCount: degenerateDropped.droppedCount + finalDegenerateDropped.droppedCount,
983
- };
984
- }
985
- function repairTaskSpineRanges(input) {
986
- const specs = taskSpineRangeSpecs(input.task);
987
- if (specs.length === 0) {
988
- return input.selection;
989
- }
990
- const candidatesByKey = new Map(input.candidates.map((candidate) => [selectedFileKey(candidate), candidate]));
991
- const repairBudget = taskSpineRepairBudget(input.budget);
992
- let selected = [...input.selection.selected];
993
- let selectedKeys = new Set(selected.map((file) => selectedFileKey(file)));
994
- let totalBytes = input.selection.metrics.bytes_selected;
995
- let totalTokens = input.selection.metrics.estimated_tokens;
996
- for (const spec of specs) {
997
- const selectedIndex = selected.findIndex((file) => spec.path.test(file.path.toLowerCase()));
998
- const existing = selectedIndex >= 0 ? selected[selectedIndex] : undefined;
999
- const candidate = existing == null
1000
- ? candidateForTaskSpineSpec(spec, input.candidates, input.sourceRoots)
1001
- : (localCandidateForTaskSpineSpec(spec, input.sourceRoots, existing.root_index) ??
1002
- candidatesByKey.get(selectedFileKey(existing)) ??
1003
- localCandidateFromSelectedFile(existing, input.sourceRoots));
1004
- if (candidate == null) {
1005
- continue;
1006
- }
1007
- const file = selectedFileFromCandidateRanges(candidate, spec.full
1008
- ? [{ start_line: 1, end_line: Math.max(1, candidate.line_count) }]
1009
- : spec.ranges, existing?.source ?? "search");
1010
- if (file == null) {
1011
- continue;
1012
- }
1013
- if (existing != null) {
1014
- const totalsWithoutFile = {
1015
- totalBytes: totalBytes - existing.bytes,
1016
- totalTokens: totalTokens - existing.estimated_tokens,
1017
- };
1018
- if (shouldReplaceWithTaskSpineRanges(existing.path, input.task) &&
1019
- file.bytes <= repairBudget.max_bytes_per_file &&
1020
- totalsWithoutFile.totalBytes + file.bytes <= repairBudget.max_total_bytes &&
1021
- totalsWithoutFile.totalTokens + file.estimated_tokens <=
1022
- repairBudget.target_estimated_tokens) {
1023
- selected[selectedIndex] = file;
1024
- totalBytes = totalsWithoutFile.totalBytes + file.bytes;
1025
- totalTokens = totalsWithoutFile.totalTokens + file.estimated_tokens;
1026
- continue;
1027
- }
1028
- const merged = selectedFileFromCandidateRanges(candidate, [...existing.ranges, ...file.ranges], existing.source);
1029
- if (merged == null || merged.bytes <= existing.bytes) {
1030
- continue;
1031
- }
1032
- if (merged.bytes > input.budget.max_bytes_per_file ||
1033
- totalsWithoutFile.totalBytes + merged.bytes > input.budget.max_total_bytes ||
1034
- totalsWithoutFile.totalTokens + merged.estimated_tokens >
1035
- repairBudget.target_estimated_tokens) {
1036
- if (shouldReplaceWithTaskSpineRanges(existing.path, input.task)) {
1037
- if (file.bytes <= repairBudget.max_bytes_per_file &&
1038
- totalsWithoutFile.totalBytes + file.bytes <=
1039
- repairBudget.max_total_bytes &&
1040
- totalsWithoutFile.totalTokens + file.estimated_tokens <=
1041
- repairBudget.target_estimated_tokens) {
1042
- selected[selectedIndex] = file;
1043
- totalBytes = totalsWithoutFile.totalBytes + file.bytes;
1044
- totalTokens = totalsWithoutFile.totalTokens + file.estimated_tokens;
1045
- continue;
1046
- }
1047
- }
1048
- const withoutExisting = selected.filter((item) => selectedFileKey(item) !== selectedFileKey(existing));
1049
- const keysWithoutExisting = new Set(withoutExisting.map((item) => selectedFileKey(item)));
1050
- const repaired = makeRoomForRequiredClosureFile({
1051
- selected: withoutExisting,
1052
- selectedKeys: keysWithoutExisting,
1053
- totalBytes: totalsWithoutFile.totalBytes,
1054
- totalTokens: totalsWithoutFile.totalTokens,
1055
- file: shouldReplaceWithTaskSpineRanges(existing.path, input.task)
1056
- ? file
1057
- : merged,
1058
- budget: repairBudget,
1059
- task: input.task,
1060
- });
1061
- if (repaired == null) {
1062
- continue;
1063
- }
1064
- const repairedFile = shouldReplaceWithTaskSpineRanges(existing.path, input.task)
1065
- ? file
1066
- : merged;
1067
- selected = [...repaired.selected, repairedFile];
1068
- selectedKeys = new Set(repaired.selectedKeys);
1069
- selectedKeys.add(selectedFileKey(repairedFile));
1070
- totalBytes = repaired.totalBytes + repairedFile.bytes;
1071
- totalTokens = repaired.totalTokens + repairedFile.estimated_tokens;
1072
- continue;
1073
- }
1074
- selected[selectedIndex] = merged;
1075
- totalBytes = totalsWithoutFile.totalBytes + merged.bytes;
1076
- totalTokens = totalsWithoutFile.totalTokens + merged.estimated_tokens;
1077
- continue;
1078
- }
1079
- if (!selectionFitsBudgetWithFile(selected, totalBytes, totalTokens, file, repairBudget)) {
1080
- const repaired = makeRoomForRequiredClosureFile({
1081
- selected,
1082
- selectedKeys,
1083
- totalBytes,
1084
- totalTokens,
1085
- file,
1086
- budget: repairBudget,
1087
- task: input.task,
1088
- });
1089
- if (repaired == null) {
1090
- continue;
1091
- }
1092
- selected = repaired.selected;
1093
- selectedKeys = repaired.selectedKeys;
1094
- totalBytes = repaired.totalBytes;
1095
- totalTokens = repaired.totalTokens;
1096
- }
1097
- selected.push(file);
1098
- selectedKeys.add(selectedFileKey(file));
1099
- totalBytes += file.bytes;
1100
- totalTokens += file.estimated_tokens;
1191
+ function restoreDroppedDegenerateReviewTargetsForValidation(input) {
1192
+ const targetKeys = new Set((input.coveragePolicy.obligations?.targets ?? []).flatMap((target) => {
1193
+ const candidate = input.candidates.find((item) => item.id === target.candidate_id);
1194
+ return candidate == null
1195
+ ? []
1196
+ : [`${candidate.root_index}\u0000${candidate.path}`];
1197
+ }));
1198
+ if (targetKeys.size === 0) {
1199
+ return [...input.postRepairSelected];
1101
1200
  }
1102
- return selectionWithSelectedFiles(input.selection, selected);
1103
- }
1104
- function taskSpineRepairBudget(budget) {
1105
- return {
1106
- ...budget,
1107
- target_estimated_tokens: budget.max_estimated_tokens,
1108
- };
1109
- }
1110
- function pruneDebugTraceSearchNoise(selection, candidates, budget, task) {
1111
- if (!isDevtoolsTraceDebugTask(task)) {
1112
- return selection;
1113
- }
1114
- const selected = selection.selected.filter((file) => file.source !== "search" || isDevtoolsTraceRelevantPath(file.path));
1115
- const pruned = selected.length === selection.selected.length
1116
- ? selection
1117
- : selectionWithSelectedFiles(selection, selected);
1118
- return expandSmallDevtoolsTraceFiles(pruned, candidates, budget);
1119
- }
1120
- function pruneRuntimeSettingsDiscoverySearchNoise(selection, task) {
1121
- if (!isRuntimeSettingsDiscoveryTask(task)) {
1122
- return selection;
1123
- }
1124
- const selected = selection.selected.filter((file) => file.source !== "search" ||
1125
- !isRuntimeSettingsDiscoverySearchNoisePath(file.path));
1126
- return selected.length === selection.selected.length
1127
- ? selection
1128
- : selectionWithSelectedFiles(selection, selected);
1129
- }
1130
- function pruneCodexRuntimeReviewSearchNoise(selection, task) {
1131
- if (!isCodexRuntimeReviewTask(task)) {
1132
- return selection;
1133
- }
1134
- const shouldPrunePath = (filePath) => isTangentialCodexRuntimeReviewUiPath(filePath) ||
1135
- isLowValueCodexRuntimeReviewNoisePath(filePath, task);
1136
- const selected = selection.selected.filter((file) => file.source === "focus" ||
1137
- !shouldPrunePath(file.path));
1138
- const search = selection.search.filter((record) => !shouldPrunePath(record.path));
1139
- const slices = selection.slices.filter((record) => record.source === "focus" || !shouldPrunePath(record.path));
1140
- const selectedKeys = new Set(selected.map(selectedFileKey));
1141
- const summaryFiles = selection.summary?.files.filter((file) => selectedKeys.has(selectedFileKey(file)) || !shouldPrunePath(file.path)) ?? [];
1142
- if (selected.length === selection.selected.length &&
1143
- search.length === selection.search.length &&
1144
- slices.length === selection.slices.length &&
1145
- summaryFiles.length === (selection.summary?.files.length ?? 0)) {
1146
- return selection;
1147
- }
1148
- const prunedSelection = selectionWithSelectedFiles(selection, selected);
1201
+ const postRepairKeys = new Set(input.postRepairSelected.map((file) => selectedFileKey(file)));
1202
+ const droppedDegenerateTargets = input.preRepairSelected.filter((file) => {
1203
+ const key = selectedFileKey(file);
1204
+ return (targetKeys.has(key) &&
1205
+ !postRepairKeys.has(key) &&
1206
+ file.content.trim().length === 0);
1207
+ });
1208
+ return [...input.postRepairSelected, ...droppedDegenerateTargets];
1209
+ }
1210
+ function repairDelegateSelection(input) {
1211
+ const focusExpanded = input.expandSelectedFiles
1212
+ ? expandSelectedFocusFilesToFullCandidates(input.curatorSelection, input.contentCandidates, input.focusPaths, input.budget)
1213
+ : { selection: input.curatorSelection, expandedCount: 0 };
1214
+ const smallSlicesExpanded = input.expandSelectedFiles
1215
+ ? expandSelectedSmallFilesToFullCandidates(focusExpanded.selection, input.contentCandidates, input.focusPaths, input.budget)
1216
+ : focusExpanded;
1217
+ const emptyDropped = dropEmptySelectedFiles(smallSlicesExpanded.selection);
1149
1218
  return {
1150
- ...prunedSelection,
1151
- search,
1152
- slices,
1153
- ...(selection.summary == null ? {} : { summary: { files: summaryFiles } }),
1219
+ selection: emptyDropped.selection,
1220
+ expandedCount: focusExpanded.expandedCount + smallSlicesExpanded.expandedCount,
1221
+ droppedDegenerateCount: emptyDropped.droppedCount,
1154
1222
  };
1155
1223
  }
1156
- function rankTaskSpecificFinalSelection(selection, task) {
1157
- if (!isRuntimeSettingsDiscoveryTask(task) && !isDevtoolsTraceDebugTask(task)) {
1158
- return selection;
1159
- }
1160
- const indexed = selection.selected.map((file, index) => ({ file, index }));
1161
- indexed.sort((left, right) => taskSpecificFinalSelectedRank(left.file.path, task) -
1162
- taskSpecificFinalSelectedRank(right.file.path, task) ||
1163
- left.index - right.index);
1164
- const selected = indexed.map((item) => item.file);
1165
- const orderChanged = selected.some((file, index) => selectedFileKey(file) !== selectedFileKey(selection.selected[index]));
1166
- return orderChanged ? selectionWithSelectedFiles(selection, selected) : selection;
1167
- }
1168
- function taskSpecificFinalSelectedRank(path, task) {
1169
- if (isRuntimeSettingsDiscoveryTask(task)) {
1170
- return runtimeSettingsDiscoveryFinalRank(path);
1171
- }
1172
- if (isDevtoolsTraceDebugTask(task)) {
1173
- return devtoolsTraceDebugFinalRank(path);
1174
- }
1175
- return 0;
1176
- }
1177
- function runtimeSettingsDiscoveryFinalRank(path) {
1178
- const normalized = path.toLowerCase();
1179
- const basename = normalized.split("/").at(-1) ?? normalized;
1180
- if (normalized.endsWith("/hooks/usechatruntimesettings.ts")) {
1181
- return 0;
1182
- }
1183
- if (normalized.endsWith("/components/chat/message-input/composerruntimecontrols.tsx")) {
1184
- return 1;
1185
- }
1186
- if (normalized.endsWith("/components/chat/message-input/input.tsx")) {
1187
- return 2;
1188
- }
1189
- if (normalized.endsWith("/pages/chatpage.tsx")) {
1190
- return 3;
1191
- }
1192
- if (normalized.endsWith("/services/chatservice.ts") ||
1193
- normalized.endsWith("/api/endpoints/chat.py") ||
1194
- normalized.endsWith("/services/chat.py")) {
1195
- return 4;
1196
- }
1197
- if (normalized.endsWith("/store/messagequeuestore.ts") ||
1198
- normalized.endsWith("/store/__tests__/messagequeuestore.test.ts") ||
1199
- normalized.endsWith("/services/queueservice.ts") ||
1200
- normalized.endsWith("/tests/services/test_queue.py") ||
1201
- normalized.endsWith("/tests/test_queue.py") ||
1202
- normalized.endsWith("/tests/services/test_pending_initial_prompt_service.py")) {
1203
- return 5;
1204
- }
1205
- if (normalized.endsWith("/services/runtime/codex.py") ||
1206
- normalized.endsWith("/services/runtime/claude.py") ||
1207
- normalized.endsWith("/services/runtime/codex_approval.py") ||
1208
- normalized.endsWith("/services/runtime/codex_stream_processor.py") ||
1209
- normalized.endsWith("/services/runtime_session_service.py") ||
1210
- normalized.endsWith("/services/streaming/orchestrator.py")) {
1211
- return 6;
1212
- }
1213
- if (normalized.endsWith("/hooks/__tests__/usechatruntimesettings.test.ts") ||
1214
- normalized.endsWith("/pages/landingpage.test.tsx") ||
1215
- normalized.endsWith("/tests/services/runtime/test_codex_runtime.py") ||
1216
- normalized.endsWith("/tests/test_codex_runtime_data_migration.py") ||
1217
- normalized.endsWith("/tests/test_codex_effort_translation.py") ||
1218
- normalized.endsWith("/tests/test_effort_level.py") ||
1219
- normalized.endsWith("/tests/test_permission_mode.py") ||
1220
- normalized.endsWith("/tests/services/test_claude_agent.py")) {
1221
- return 7;
1222
- }
1223
- if (basename === "effort.py" ||
1224
- basename === "permission_mode.py" ||
1225
- basename === "chat.types.ts" ||
1226
- basename === "queue.types.ts" ||
1227
- normalized.endsWith("/models/schemas/chat.py") ||
1228
- normalized.endsWith("/models/db_models/chat.py") ||
1229
- normalized.endsWith("/models/db_models/runtime_session.py") ||
1230
- basename.includes("permissionmodeselector") ||
1231
- basename.includes("thinkingmodeselector")) {
1232
- return 8;
1233
- }
1234
- return 12;
1235
- }
1236
- function devtoolsTraceDebugFinalRank(path) {
1237
- const normalized = path.toLowerCase();
1238
- const basename = normalized.split("/").at(-1) ?? normalized;
1239
- if (normalized.endsWith("/components/right-panel/rightpaneldevtools.tsx") ||
1240
- basename === "rightpaneldevtools.tsx") {
1241
- return 0;
1242
- }
1243
- if (normalized.endsWith("/components/right-panel/__tests__/right-panel-devtools.test.tsx") ||
1244
- normalized.endsWith("/components/chat/__tests__/chat.autoscroll.test.tsx") ||
1245
- normalized.endsWith("/store/devtoolsnavstore.ts") ||
1246
- normalized.endsWith("/store/__tests__/devtoolsnavstore.test.ts")) {
1247
- return 1;
1248
- }
1249
- if (normalized.endsWith("/components/chat/chat-window/chat.tsx") ||
1250
- normalized.endsWith("/components/chat/message-bubble/messagerenderer.tsx") ||
1251
- normalized.endsWith("/components/chat/tools/common/toolcard.tsx")) {
1252
- return 2;
1253
- }
1254
- if (normalized.endsWith("/hooks/usemessagecache.ts") ||
1255
- normalized.endsWith("/utils/messagecacheorder.ts") ||
1256
- normalized.endsWith("/components/chat/tools/common/toolexpandcontext.ts") ||
1257
- normalized.endsWith("/components/chat/tools/common/__tests__/toolexpand.test.tsx")) {
1258
- return 3;
1259
- }
1260
- if (normalized.endsWith("/components/right-panel/devtools-helpers.ts") ||
1261
- normalized.endsWith("/components/right-panel/__tests__/devtools-helpers.test.ts") ||
1262
- normalized.endsWith("/services/devtoolsservice.ts") ||
1263
- normalized.endsWith("/hooks/queries/usedevtoolsqueries.ts")) {
1264
- return 4;
1265
- }
1266
- if (normalized.endsWith("/api/endpoints/devtools.py") ||
1267
- normalized.endsWith("/services/session_trace_archive.py") ||
1268
- normalized.endsWith("/services/codex_trace.py") ||
1269
- normalized.endsWith("/models/db_models/session_trace_archive.py") ||
1270
- normalized.endsWith("/models/schemas/devtools.py") ||
1271
- normalized.endsWith("/types/devtools.types.ts")) {
1272
- return 7;
1273
- }
1274
- return 10;
1275
- }
1276
- function isLowValueCodexRuntimeReviewNoisePath(path, task) {
1277
- const normalized = path.toLowerCase();
1278
- const normalizedTask = task.toLowerCase();
1279
- if (normalized.endsWith("/core/config.py") &&
1280
- !/\bconfig(?:uration)?\b/.test(normalizedTask)) {
1281
- return true;
1224
+ function appendDelegateRepairWarnings(warnings, repair) {
1225
+ if (repair.expandedCount > 0) {
1226
+ warnings.push(delegateWarning("delegate_curator_warning", `server-expanded ${repair.expandedCount} selected file(s) to full context`));
1282
1227
  }
1283
- if (normalized.endsWith("/tests/services/streaming/test_context_usage_payloads.py") &&
1284
- !/\bcontext\s+usage|usage\s+payloads?\b/.test(normalizedTask)) {
1285
- return true;
1228
+ if (repair.droppedDegenerateCount > 0) {
1229
+ warnings.push(delegateWarning("delegate_curator_warning", `server-dropped ${repair.droppedDegenerateCount} degenerate selected slice(s)`));
1286
1230
  }
1287
- return false;
1288
1231
  }
1289
- function isRuntimeSettingsDiscoveryTask(task) {
1290
- const normalized = task.toLowerCase();
1291
- return (/\bdiscovery\b/.test(normalized) &&
1292
- normalized.includes("runtime") &&
1293
- /\bsettings?\b/.test(normalized));
1294
- }
1295
- function isCodexRuntimeReviewTask(task) {
1296
- const normalized = task.toLowerCase();
1297
- return (/\breview\b/.test(normalized) &&
1298
- normalized.includes("codex") &&
1299
- /\b(runtime|permission|effort|approval|session)\b/.test(normalized));
1300
- }
1301
- function isTangentialCodexRuntimeReviewUiPath(path) {
1302
- const normalized = path.toLowerCase();
1303
- const basename = normalized.split("/").at(-1) ?? normalized;
1304
- return (basename === "requestaccesspage.tsx" ||
1305
- basename === "providerauthmodal.tsx" ||
1306
- basename === "codexauthupload.tsx" ||
1307
- basename === "oauthcallbackpage.tsx" ||
1308
- basename === "subscriptionusagecard.tsx" ||
1309
- basename === "requestaccesspage.test.tsx" ||
1310
- basename === "oauthcallbackpage.test.tsx" ||
1311
- basename === "codexauthflow.test.tsx" ||
1312
- basename === "useauthbootstrap.test.ts" ||
1313
- basename === "providerauthservice.ratelimit.test.ts" ||
1314
- normalized.endsWith("docs/ops/nano-permissions-policy.md") ||
1315
- normalized.includes("/components/settings/inputs/") ||
1316
- normalized.includes("/components/settings/tabs/") ||
1317
- normalized.includes("/components/settings/__tests__/") ||
1318
- normalized.includes("/provider-auth/") ||
1319
- normalized.includes("/pages/requestaccess") ||
1320
- normalized.includes("/pages/oauthcallback") ||
1321
- normalized.includes("/services/checks/") ||
1322
- normalized.endsWith("/hooks/usestreamcallbacks.ts") ||
1323
- normalized.endsWith("/hooks/queries/querykeys.ts") ||
1324
- normalized.endsWith("/vite.config.ts") ||
1325
- normalized.endsWith("/api/endpoints/provider_auth.py") ||
1326
- normalized.endsWith("/api/endpoints/settings.py") ||
1327
- normalized.endsWith("/services/access_request.py") ||
1328
- normalized.endsWith("/models/schemas/access_request.py"));
1329
- }
1330
- function isRuntimeSettingsDiscoverySearchNoisePath(path) {
1331
- const normalized = path.toLowerCase();
1332
- const basename = normalized.split("/").at(-1) ?? normalized;
1333
- return (isTangentialRuntimeSettingsDiscoveryTestPath(normalized, basename) ||
1334
- normalized.includes("/tests/services/nightshift/") ||
1335
- normalized.includes("/tests/services/sandbox/") ||
1336
- normalized.endsWith("/test_codex_sdk_smoke.py") ||
1337
- normalized.endsWith("/test_codex_runtime_live.py") ||
1338
- normalized.endsWith("/test_transport_absence.py") ||
1339
- normalized.endsWith("/test_codex_approval_bridge.py") ||
1340
- normalized.endsWith("/test_codex_stream_processor.py") ||
1341
- normalized.endsWith("/test_codex_stream_processor_fixtures.py") ||
1342
- normalized.endsWith("/test_codex_runtime_invariants.py"));
1343
- }
1344
- function expandSmallDevtoolsTraceFiles(selection, candidates, budget) {
1232
+ function expandSelectedFocusFilesToFullCandidates(selection, candidates, focusPaths, budget) {
1233
+ const focusKeys = new Set(focusPaths
1234
+ .filter((focus) => focus.kind === "file")
1235
+ .map((focus) => `${focus.root_index}\u0000${focus.root_relative_path}`));
1236
+ if (focusKeys.size === 0) {
1237
+ return { selection, expandedCount: 0 };
1238
+ }
1345
1239
  const candidatesByKey = new Map(candidates.map((candidate) => [selectedFileKey(candidate), candidate]));
1346
1240
  let totalBytes = selection.metrics.bytes_selected;
1347
1241
  let totalTokens = selection.metrics.estimated_tokens;
1348
1242
  let expandedCount = 0;
1349
1243
  const selected = selection.selected.map((file) => {
1350
- if (!shouldExpandSmallDevtoolsTraceFile(file.path)) {
1244
+ const key = selectedFileKey(file);
1245
+ if (!focusKeys.has(key)) {
1351
1246
  return file;
1352
1247
  }
1353
- const candidate = candidatesByKey.get(selectedFileKey(file));
1354
- if (candidate == null || fileCoversCandidate(file, candidate)) {
1355
- return file;
1356
- }
1357
- const fullFile = selectedFileFromCandidate(candidate);
1358
- const totalsWithoutFile = {
1359
- totalBytes: totalBytes - file.bytes,
1360
- totalTokens: totalTokens - file.estimated_tokens,
1361
- };
1362
- if (fullFile.estimated_tokens > 8_000 ||
1363
- fullFile.bytes > budget.max_bytes_per_file ||
1364
- totalsWithoutFile.totalBytes + fullFile.bytes > budget.max_total_bytes ||
1365
- totalsWithoutFile.totalTokens + fullFile.estimated_tokens >
1366
- budget.target_estimated_tokens) {
1367
- return file;
1368
- }
1369
- totalBytes = totalsWithoutFile.totalBytes + fullFile.bytes;
1370
- totalTokens = totalsWithoutFile.totalTokens + fullFile.estimated_tokens;
1371
- expandedCount += 1;
1372
- return { ...fullFile, source: file.source };
1373
- });
1374
- if (expandedCount === 0) {
1375
- return selection;
1376
- }
1377
- return selectionWithSelectedFiles(selection, selected);
1378
- }
1379
- function taskSpineRangeSpecs(task) {
1380
- const normalized = task.toLowerCase();
1381
- const specs = [];
1382
- if ((/\bdiscovery\b/.test(normalized) && normalized.includes("runtime")) ||
1383
- isRuntimeSettingsPersistenceFlowTask(normalized)) {
1384
- specs.push({
1385
- path: /\/pages\/chatpage\.tsx$/,
1386
- exactPaths: ["frontend/src/pages/ChatPage.tsx"],
1387
- ranges: [
1388
- { start_line: 150, end_line: 292 },
1389
- { start_line: 640, end_line: 700 },
1390
- ],
1391
- }, {
1392
- path: /\/components\/chat\/message-input\/input\.tsx$/,
1393
- exactPaths: ["frontend/src/components/chat/message-input/Input.tsx"],
1394
- ranges: [
1395
- { start_line: 360, end_line: 630 },
1396
- { start_line: 1018, end_line: 1048 },
1397
- ],
1398
- }, {
1399
- path: /\/hooks\/usechatruntimesettings\.ts$/,
1400
- exactPaths: ["frontend/src/hooks/useChatRuntimeSettings.ts"],
1401
- ranges: [{ start_line: 1, end_line: Number.MAX_SAFE_INTEGER }],
1402
- full: true,
1403
- }, {
1404
- path: /\/services\/chatservice\.ts$/,
1405
- exactPaths: ["frontend/src/services/chatService.ts"],
1406
- ranges: [{ start_line: 1, end_line: Number.MAX_SAFE_INTEGER }],
1407
- full: true,
1408
- }, {
1409
- path: /\/services\/chat\.py$/,
1410
- exactPaths: ["backend/app/services/chat.py"],
1411
- ranges: [
1412
- { start_line: 160, end_line: 675 },
1413
- { start_line: 1033, end_line: 1163 },
1414
- { start_line: 1619, end_line: 1756 },
1415
- { start_line: 1758, end_line: 2179 },
1416
- { start_line: 2446, end_line: 2552 },
1417
- { start_line: 2563, end_line: 2610 },
1418
- ],
1419
- }, {
1420
- path: /\/api\/endpoints\/chat\.py$/,
1421
- exactPaths: ["backend/app/api/endpoints/chat.py"],
1422
- ranges: [
1423
- { start_line: 396, end_line: 468 },
1424
- { start_line: 528, end_line: 558 },
1425
- { start_line: 1298, end_line: 1383 },
1426
- ],
1427
- }, {
1428
- path: /\/store\/__tests__\/messagequeuestore\.test\.ts$/,
1429
- exactPaths: ["frontend/src/store/__tests__/messageQueueStore.test.ts"],
1430
- ranges: [
1431
- { start_line: 1, end_line: 121 },
1432
- { start_line: 123, end_line: 241 },
1433
- { start_line: 243, end_line: 320 },
1434
- ],
1435
- }, {
1436
- path: /\/store\/messagequeuestore\.ts$/,
1437
- exactPaths: ["frontend/src/store/messageQueueStore.ts"],
1438
- ranges: [
1439
- { start_line: 120, end_line: 155 },
1440
- { start_line: 208, end_line: 360 },
1441
- { start_line: 640, end_line: 760 },
1442
- ],
1443
- }, {
1444
- path: /\/tests\/.*test_pending_initial_prompt_service\.py$/,
1445
- exactPaths: ["backend/tests/services/test_pending_initial_prompt_service.py"],
1446
- ranges: [
1447
- { start_line: 1, end_line: 230 },
1448
- { start_line: 531, end_line: 592 },
1449
- { start_line: 1150, end_line: 1242 },
1450
- { start_line: 1245, end_line: 1400 },
1451
- ],
1452
- }, {
1453
- path: /\/services\/claude_agent\.py$/,
1454
- exactPaths: ["backend/app/services/claude_agent.py"],
1455
- ranges: [
1456
- { start_line: 44, end_line: 80 },
1457
- { start_line: 433, end_line: 466 },
1458
- { start_line: 786, end_line: 874 },
1459
- { start_line: 988, end_line: 1002 },
1460
- ],
1461
- }, {
1462
- path: /\/services\/runtime\/claude\.py$/,
1463
- exactPaths: ["backend/app/services/runtime/claude.py"],
1464
- ranges: [{ start_line: 1, end_line: Number.MAX_SAFE_INTEGER }],
1465
- full: true,
1466
- }, {
1467
- path: /\/services\/runtime\/codex_approval\.py$/,
1468
- exactPaths: ["backend/app/services/runtime/codex_approval.py"],
1469
- ranges: [
1470
- { start_line: 1, end_line: 135 },
1471
- { start_line: 193, end_line: 280 },
1472
- { start_line: 310, end_line: 386 },
1473
- ],
1474
- }, {
1475
- path: /\/services\/runtime\/codex\.py$/,
1476
- exactPaths: ["backend/app/services/runtime/codex.py"],
1477
- ranges: [
1478
- { start_line: 1, end_line: 82 },
1479
- { start_line: 87, end_line: 264 },
1480
- { start_line: 576, end_line: 621 },
1481
- ],
1482
- }, {
1483
- path: /\/services\/runtime_session_service\.py$/,
1484
- exactPaths: ["backend/app/services/runtime_session_service.py"],
1485
- ranges: [{ start_line: 1, end_line: Number.MAX_SAFE_INTEGER }],
1486
- full: true,
1487
- }, {
1488
- path: /\/models\/db_models\/runtime_session\.py$/,
1489
- exactPaths: ["backend/app/models/db_models/runtime_session.py"],
1490
- ranges: [{ start_line: 1, end_line: Number.MAX_SAFE_INTEGER }],
1491
- full: true,
1492
- }, {
1493
- path: /\/services\/permission_manager\.py$/,
1494
- exactPaths: ["backend/app/services/permission_manager.py"],
1495
- ranges: [
1496
- { start_line: 1, end_line: 160 },
1497
- { start_line: 220, end_line: 360 },
1498
- ],
1499
- }, {
1500
- path: /\/services\/runtime\/codex_stream_processor\.py$/,
1501
- exactPaths: ["backend/app/services/runtime/codex_stream_processor.py"],
1502
- ranges: [
1503
- { start_line: 94, end_line: 123 },
1504
- { start_line: 155, end_line: 227 },
1505
- { start_line: 263, end_line: 328 },
1506
- ],
1507
- }, {
1508
- path: /\/hooks\/__tests__\/usechatruntimesettings\.test\.ts$/,
1509
- exactPaths: ["frontend/src/hooks/__tests__/useChatRuntimeSettings.test.ts"],
1510
- ranges: [{ start_line: 1, end_line: Number.MAX_SAFE_INTEGER }],
1511
- full: true,
1512
- }, {
1513
- path: /\/tests\/services\/test_claude_agent\.py$/,
1514
- exactPaths: ["backend/tests/services/test_claude_agent.py"],
1515
- ranges: [
1516
- { start_line: 247, end_line: 325 },
1517
- { start_line: 325, end_line: 381 },
1518
- { start_line: 628, end_line: 705 },
1519
- ],
1520
- }, {
1521
- path: /\/models\/schemas\/chat\.py$/,
1522
- exactPaths: ["backend/app/models/schemas/chat.py"],
1523
- ranges: [{ start_line: 1, end_line: Number.MAX_SAFE_INTEGER }],
1524
- full: true,
1525
- }, {
1526
- path: /\/types\/chat\.types\.ts$/,
1527
- exactPaths: ["frontend/src/types/chat.types.ts"],
1528
- ranges: [{ start_line: 1, end_line: Number.MAX_SAFE_INTEGER }],
1529
- full: true,
1530
- }, {
1531
- path: /\/types\/queue\.types\.ts$/,
1532
- exactPaths: ["frontend/src/types/queue.types.ts"],
1533
- ranges: [{ start_line: 1, end_line: Number.MAX_SAFE_INTEGER }],
1534
- full: true,
1535
- }, {
1536
- path: /\/services\/streamservice\.ts$/,
1537
- exactPaths: ["frontend/src/services/streamService.ts"],
1538
- ranges: [
1539
- { start_line: 1, end_line: 140 },
1540
- { start_line: 260, end_line: 422 },
1541
- ],
1542
- }, {
1543
- path: /\/tests\/test_queue\.py$/,
1544
- exactPaths: ["backend/tests/test_queue.py"],
1545
- ranges: [
1546
- { start_line: 1, end_line: 120 },
1547
- { start_line: 430, end_line: 632 },
1548
- ],
1549
- }, {
1550
- path: /\/tests\/services\/test_queue\.py$/,
1551
- exactPaths: ["backend/tests/services/test_queue.py"],
1552
- ranges: [{ start_line: 1, end_line: Number.MAX_SAFE_INTEGER }],
1553
- full: true,
1554
- }, {
1555
- path: /\/tests\/test_effort_level\.py$/,
1556
- exactPaths: ["backend/tests/test_effort_level.py"],
1557
- ranges: [{ start_line: 1, end_line: Number.MAX_SAFE_INTEGER }],
1558
- full: true,
1559
- }, {
1560
- path: /\/tests\/test_permission_mode\.py$/,
1561
- exactPaths: ["backend/tests/test_permission_mode.py"],
1562
- ranges: [{ start_line: 1, end_line: Number.MAX_SAFE_INTEGER }],
1563
- full: true,
1564
- }, {
1565
- path: /\/tests\/services\/runtime\/test_codex_runtime\.py$/,
1566
- exactPaths: ["backend/tests/services/runtime/test_codex_runtime.py"],
1567
- ranges: [
1568
- { start_line: 1, end_line: 160 },
1569
- { start_line: 540, end_line: 776 },
1570
- ],
1571
- }, {
1572
- path: /\/tests\/test_codex_effort_translation\.py$/,
1573
- exactPaths: ["backend/tests/test_codex_effort_translation.py"],
1574
- ranges: [{ start_line: 1, end_line: Number.MAX_SAFE_INTEGER }],
1575
- full: true,
1576
- }, {
1577
- path: /\/hooks\/usepermissionrequest\.ts$/,
1578
- exactPaths: ["frontend/src/hooks/usePermissionRequest.ts"],
1579
- ranges: [{ start_line: 1, end_line: Number.MAX_SAFE_INTEGER }],
1580
- full: true,
1581
- }, {
1582
- path: /\/hooks\/queries\/usesettingsqueries\.ts$/,
1583
- exactPaths: ["frontend/src/hooks/queries/useSettingsQueries.ts"],
1584
- ranges: [{ start_line: 1, end_line: Number.MAX_SAFE_INTEGER }],
1585
- full: true,
1586
- });
1587
- }
1588
- if (/\breview\b/.test(normalized) && normalized.includes("codex")) {
1589
- specs.push({
1590
- path: /\/services\/runtime\/codex\.py$/,
1591
- exactPaths: ["backend/app/services/runtime/codex.py"],
1592
- ranges: [
1593
- { start_line: 130, end_line: 178 },
1594
- { start_line: 212, end_line: 392 },
1595
- { start_line: 394, end_line: 476 },
1596
- { start_line: 576, end_line: 621 },
1597
- { start_line: 698, end_line: 766 },
1598
- ],
1599
- }, {
1600
- path: /\/services\/runtime\/codex_stream_processor\.py$/,
1601
- exactPaths: ["backend/app/services/runtime/codex_stream_processor.py"],
1602
- ranges: [
1603
- { start_line: 94, end_line: 123 },
1604
- { start_line: 155, end_line: 227 },
1605
- { start_line: 263, end_line: 328 },
1606
- ],
1607
- }, {
1608
- path: /\/services\/runtime\/base\.py$/,
1609
- exactPaths: ["backend/app/services/runtime/base.py"],
1610
- ranges: [{ start_line: 1, end_line: Number.MAX_SAFE_INTEGER }],
1611
- full: true,
1612
- }, {
1613
- path: /\/services\/runtime\/claude\.py$/,
1614
- exactPaths: ["backend/app/services/runtime/claude.py"],
1615
- ranges: [{ start_line: 1, end_line: Number.MAX_SAFE_INTEGER }],
1616
- full: true,
1617
- }, {
1618
- path: /\/services\/runtime\/codex_approval\.py$/,
1619
- exactPaths: ["backend/app/services/runtime/codex_approval.py"],
1620
- ranges: [{ start_line: 1, end_line: Number.MAX_SAFE_INTEGER }],
1621
- full: true,
1622
- }, {
1623
- path: /\/services\/runtime\/resolver\.py$/,
1624
- exactPaths: ["backend/app/services/runtime/resolver.py"],
1625
- ranges: [{ start_line: 1, end_line: Number.MAX_SAFE_INTEGER }],
1626
- full: true,
1627
- }, {
1628
- path: /\/services\/runtime_session_service\.py$/,
1629
- exactPaths: ["backend/app/services/runtime_session_service.py"],
1630
- ranges: [{ start_line: 1, end_line: Number.MAX_SAFE_INTEGER }],
1631
- full: true,
1632
- }, {
1633
- path: /\/services\/streaming\/orchestrator\.py$/,
1634
- exactPaths: ["backend/app/services/streaming/orchestrator.py"],
1635
- ranges: [
1636
- { start_line: 979, end_line: 1173 },
1637
- { start_line: 1207, end_line: 1245 },
1638
- ],
1639
- }, {
1640
- path: /\/services\/sandbox\/provider\.py$/,
1641
- exactPaths: ["backend/app/services/sandbox/provider.py"],
1642
- ranges: [
1643
- { start_line: 1, end_line: 260 },
1644
- { start_line: 760, end_line: 920 },
1645
- ],
1646
- }, {
1647
- path: /\/services\/streaming\/context_usage\.py$/,
1648
- exactPaths: ["backend/app/services/streaming/context_usage.py"],
1649
- ranges: [{ start_line: 1, end_line: Number.MAX_SAFE_INTEGER }],
1650
- full: true,
1651
- }, {
1652
- path: /\/services\/providerauthservice\.ts$/,
1653
- exactPaths: ["frontend/src/services/providerAuthService.ts"],
1654
- ranges: [{ start_line: 1, end_line: Number.MAX_SAFE_INTEGER }],
1655
- full: true,
1656
- }, {
1657
- path: /\/services\/streamservice\.ts$/,
1658
- exactPaths: ["frontend/src/services/streamService.ts"],
1659
- ranges: [
1660
- { start_line: 1, end_line: 140 },
1661
- { start_line: 260, end_line: 422 },
1662
- ],
1663
- }, {
1664
- path: /\/hooks\/usechatruntimesettings\.ts$/,
1665
- exactPaths: ["frontend/src/hooks/useChatRuntimeSettings.ts"],
1666
- ranges: [{ start_line: 1, end_line: Number.MAX_SAFE_INTEGER }],
1667
- full: true,
1668
- }, {
1669
- path: /\/services\/permission_manager\.py$/,
1670
- exactPaths: ["backend/app/services/permission_manager.py"],
1671
- ranges: [{ start_line: 1, end_line: Number.MAX_SAFE_INTEGER }],
1672
- full: true,
1673
- }, {
1674
- path: /\/services\/model_provider_auth\.py$/,
1675
- exactPaths: ["backend/app/services/model_provider_auth.py"],
1676
- ranges: [{ start_line: 1, end_line: Number.MAX_SAFE_INTEGER }],
1677
- full: true,
1678
- }, {
1679
- path: /\/tests\/services\/test_model_provider_auth\.py$/,
1680
- exactPaths: ["backend/tests/services/test_model_provider_auth.py"],
1681
- ranges: [{ start_line: 1, end_line: Number.MAX_SAFE_INTEGER }],
1682
- full: true,
1683
- }, {
1684
- path: /\/tests\/services\/runtime\/test_codex_stream_processor\.py$/,
1685
- exactPaths: ["backend/tests/services/runtime/test_codex_stream_processor.py"],
1686
- ranges: [{ start_line: 1, end_line: Number.MAX_SAFE_INTEGER }],
1687
- full: true,
1688
- }, {
1689
- path: /\/tests\/services\/runtime\/test_codex_approval_bridge\.py$/,
1690
- exactPaths: ["backend/tests/services/runtime/test_codex_approval_bridge.py"],
1691
- ranges: [
1692
- { start_line: 1, end_line: 80 },
1693
- { start_line: 193, end_line: 280 },
1694
- { start_line: 310, end_line: 386 },
1695
- { start_line: 449, end_line: 565 },
1696
- { start_line: 953, end_line: 1068 },
1697
- { start_line: 1100, end_line: 1165 },
1698
- ],
1699
- }, {
1700
- path: /\/tests\/services\/runtime\/test_resolver\.py$/,
1701
- exactPaths: ["backend/tests/services/runtime/test_resolver.py"],
1702
- ranges: [{ start_line: 1, end_line: Number.MAX_SAFE_INTEGER }],
1703
- full: true,
1704
- }, {
1705
- path: /\/tests\/services\/test_runtime_session_service\.py$/,
1706
- exactPaths: ["backend/tests/services/test_runtime_session_service.py"],
1707
- ranges: [{ start_line: 1, end_line: Number.MAX_SAFE_INTEGER }],
1708
- full: true,
1709
- }, {
1710
- path: /\/api\/endpoints\/chat\.py$/,
1711
- exactPaths: ["backend/app/api/endpoints/chat.py"],
1712
- ranges: [
1713
- { start_line: 1, end_line: 120 },
1714
- { start_line: 390, end_line: 570 },
1715
- { start_line: 1215, end_line: 1400 },
1716
- ],
1717
- }, {
1718
- path: /\/api\/endpoints\/permissions\.py$/,
1719
- exactPaths: ["backend/app/api/endpoints/permissions.py"],
1720
- ranges: [{ start_line: 1, end_line: Number.MAX_SAFE_INTEGER }],
1721
- full: true,
1722
- }, {
1723
- path: /\/services\/chat\.py$/,
1724
- exactPaths: ["backend/app/services/chat.py"],
1725
- ranges: [
1726
- { start_line: 160, end_line: 675 },
1727
- { start_line: 777, end_line: 851 },
1728
- { start_line: 1131, end_line: 1177 },
1729
- { start_line: 1619, end_line: 1756 },
1730
- { start_line: 1878, end_line: 2170 },
1731
- { start_line: 2454, end_line: 2508 },
1732
- { start_line: 2570, end_line: 2594 },
1733
- ],
1734
- }, {
1735
- path: /\/services\/claude_agent\.py$/,
1736
- exactPaths: ["backend/app/services/claude_agent.py"],
1737
- ranges: [
1738
- { start_line: 44, end_line: 80 },
1739
- { start_line: 433, end_line: 466 },
1740
- { start_line: 786, end_line: 874 },
1741
- { start_line: 988, end_line: 1002 },
1742
- ],
1743
- }, {
1744
- path: /\/components\/chat\/message-input\/input\.tsx$/,
1745
- exactPaths: ["frontend/src/components/chat/message-input/Input.tsx"],
1746
- ranges: [
1747
- { start_line: 360, end_line: 630 },
1748
- { start_line: 1018, end_line: 1048 },
1749
- ],
1750
- }, {
1751
- path: /\/components\/chat\/message-input\/composerruntimecontrols\.tsx$/,
1752
- exactPaths: ["frontend/src/components/chat/message-input/ComposerRuntimeControls.tsx"],
1753
- ranges: [{ start_line: 1, end_line: Number.MAX_SAFE_INTEGER }],
1754
- full: true,
1755
- }, {
1756
- path: /\/hooks\/usemessageactions\.ts$/,
1757
- exactPaths: ["frontend/src/hooks/useMessageActions.ts"],
1758
- ranges: [{ start_line: 1, end_line: Number.MAX_SAFE_INTEGER }],
1759
- full: true,
1760
- }, {
1761
- path: /\/components\/ui\/fileuploaddialog\.tsx$/,
1762
- exactPaths: ["frontend/src/components/ui/FileUploadDialog.tsx"],
1763
- ranges: [{ start_line: 1, end_line: Number.MAX_SAFE_INTEGER }],
1764
- full: true,
1765
- }, {
1766
- path: /\/components\/ui\/fileuploaddialog\.codex\.test\.tsx$/,
1767
- exactPaths: ["frontend/src/components/ui/FileUploadDialog.codex.test.tsx"],
1768
- ranges: [{ start_line: 1, end_line: Number.MAX_SAFE_INTEGER }],
1769
- full: true,
1770
- }, {
1771
- path: /\/hooks\/usepermissionrequest\.ts$/,
1772
- exactPaths: ["frontend/src/hooks/usePermissionRequest.ts"],
1773
- ranges: [{ start_line: 1, end_line: Number.MAX_SAFE_INTEGER }],
1774
- full: true,
1775
- }, {
1776
- path: /\/utils\/modelprovider\.ts$/,
1777
- exactPaths: ["frontend/src/utils/modelProvider.ts"],
1778
- ranges: [{ start_line: 1, end_line: Number.MAX_SAFE_INTEGER }],
1779
- full: true,
1780
- }, {
1781
- path: /\/components\/chat\/message-input\/inputsettingspopover\.tsx$/,
1782
- exactPaths: ["frontend/src/components/chat/message-input/InputSettingsPopover.tsx"],
1783
- ranges: [{ start_line: 1, end_line: Number.MAX_SAFE_INTEGER }],
1784
- full: true,
1785
- }, {
1786
- path: /\/permissionmodeselector\.tsx$/,
1787
- exactPaths: ["frontend/src/components/chat/permission-mode-selector/PermissionModeSelector.tsx"],
1788
- ranges: [{ start_line: 1, end_line: Number.MAX_SAFE_INTEGER }],
1789
- full: true,
1790
- }, {
1791
- path: /\/thinkingmodeselector\.tsx$/,
1792
- exactPaths: ["frontend/src/components/chat/thinking-mode-selector/ThinkingModeSelector.tsx"],
1793
- ranges: [{ start_line: 1, end_line: Number.MAX_SAFE_INTEGER }],
1794
- full: true,
1795
- }, {
1796
- path: /\/models\/schemas\/chat\.py$/,
1797
- exactPaths: ["backend/app/models/schemas/chat.py"],
1798
- ranges: [{ start_line: 1, end_line: Number.MAX_SAFE_INTEGER }],
1799
- full: true,
1800
- }, {
1801
- path: /\/models\/schemas\/permission_mode\.py$/,
1802
- exactPaths: ["backend/app/models/schemas/permission_mode.py"],
1803
- ranges: [{ start_line: 1, end_line: Number.MAX_SAFE_INTEGER }],
1804
- full: true,
1805
- }, {
1806
- path: /\/models\/schemas\/permissions\.py$/,
1807
- exactPaths: ["backend/app/models/schemas/permissions.py"],
1808
- ranges: [{ start_line: 1, end_line: Number.MAX_SAFE_INTEGER }],
1809
- full: true,
1810
- }, {
1811
- path: /\/models\/db_models\/ai_model\.py$/,
1812
- exactPaths: ["backend/app/models/db_models/ai_model.py"],
1813
- ranges: [{ start_line: 1, end_line: Number.MAX_SAFE_INTEGER }],
1814
- full: true,
1815
- }, {
1816
- path: /\/api\/endpoints\/ai_models\.py$/,
1817
- exactPaths: ["backend/app/api/endpoints/ai_models.py"],
1818
- ranges: [{ start_line: 1, end_line: Number.MAX_SAFE_INTEGER }],
1819
- full: true,
1820
- }, {
1821
- path: /\/tests\/test_ai_models_codex_gate\.py$/,
1822
- exactPaths: ["backend/tests/test_ai_models_codex_gate.py"],
1823
- ranges: [{ start_line: 1, end_line: Number.MAX_SAFE_INTEGER }],
1824
- full: true,
1825
- }, {
1826
- path: /\/tests\/test_permissions\.py$/,
1827
- exactPaths: ["backend/tests/test_permissions.py"],
1828
- ranges: [
1829
- { start_line: 1, end_line: 230 },
1830
- { start_line: 306, end_line: 389 },
1831
- { start_line: 489, end_line: 555 },
1832
- ],
1833
- });
1834
- }
1835
- if (isDevtoolsTraceDebugTask(task)) {
1836
- specs.push({
1837
- path: /\/right-panel\/rightpaneldevtools\.tsx$/,
1838
- exactPaths: ["frontend/src/components/right-panel/RightPanelDevtools.tsx"],
1839
- ranges: [
1840
- { start_line: 1, end_line: 115 },
1841
- { start_line: 528, end_line: 745 },
1842
- { start_line: 840, end_line: 948 },
1843
- ],
1844
- }, {
1845
- path: /\/right-panel\/__tests__\/right-panel-devtools\.test\.tsx$/,
1846
- exactPaths: ["frontend/src/components/right-panel/__tests__/right-panel-devtools.test.tsx"],
1847
- ranges: [{ start_line: 1, end_line: Number.MAX_SAFE_INTEGER }],
1848
- full: true,
1849
- }, {
1850
- path: /\/right-panel\/devtools-helpers\.ts$/,
1851
- exactPaths: ["frontend/src/components/right-panel/devtools-helpers.ts"],
1852
- ranges: [{ start_line: 1, end_line: Number.MAX_SAFE_INTEGER }],
1853
- full: true,
1854
- }, {
1855
- path: /\/store\/devtoolsnavstore\.ts$/,
1856
- exactPaths: ["frontend/src/store/devtoolsNavStore.ts"],
1857
- ranges: [{ start_line: 1, end_line: Number.MAX_SAFE_INTEGER }],
1858
- full: true,
1859
- }, {
1860
- path: /\/services\/devtoolsservice\.ts$/,
1861
- exactPaths: ["frontend/src/services/devtoolsService.ts"],
1862
- ranges: [{ start_line: 1, end_line: Number.MAX_SAFE_INTEGER }],
1863
- full: true,
1864
- }, {
1865
- path: /\/hooks\/queries\/usedevtoolsqueries\.ts$/,
1866
- exactPaths: ["frontend/src/hooks/queries/useDevtoolsQueries.ts"],
1867
- ranges: [{ start_line: 1, end_line: Number.MAX_SAFE_INTEGER }],
1868
- full: true,
1869
- }, {
1870
- path: /\/components\/chat\/chat-window\/chat\.tsx$/,
1871
- exactPaths: ["frontend/src/components/chat/chat-window/Chat.tsx"],
1872
- ranges: [
1873
- { start_line: 450, end_line: 615 },
1874
- { start_line: 670, end_line: 750 },
1875
- ],
1876
- }, {
1877
- path: /\/components\/chat\/message-bubble\/message\.tsx$/,
1878
- exactPaths: ["frontend/src/components/chat/message-bubble/Message.tsx"],
1879
- ranges: [{ start_line: 1, end_line: Number.MAX_SAFE_INTEGER }],
1880
- full: true,
1881
- }, {
1882
- path: /\/pages\/chatpage\.tsx$/,
1883
- exactPaths: ["frontend/src/pages/ChatPage.tsx"],
1884
- ranges: [
1885
- { start_line: 150, end_line: 292 },
1886
- { start_line: 640, end_line: 700 },
1887
- ],
1888
- }, {
1889
- path: /\/components\/chat\/message-input\/input\.tsx$/,
1890
- exactPaths: ["frontend/src/components/chat/message-input/Input.tsx"],
1891
- ranges: [
1892
- { start_line: 360, end_line: 630 },
1893
- { start_line: 1018, end_line: 1048 },
1894
- ],
1895
- }, {
1896
- path: /\/api\/endpoints\/chat\.py$/,
1897
- exactPaths: ["backend/app/api/endpoints/chat.py"],
1898
- ranges: [
1899
- { start_line: 390, end_line: 570 },
1900
- { start_line: 1215, end_line: 1400 },
1901
- ],
1902
- }, {
1903
- path: /\/services\/claude_agent\.py$/,
1904
- exactPaths: ["backend/app/services/claude_agent.py"],
1905
- ranges: [
1906
- { start_line: 44, end_line: 80 },
1907
- { start_line: 433, end_line: 466 },
1908
- { start_line: 786, end_line: 874 },
1909
- { start_line: 988, end_line: 1002 },
1910
- ],
1911
- }, {
1912
- path: /\/store\/__tests__\/devtoolsnavstore\.test\.ts$/,
1913
- exactPaths: ["frontend/src/store/__tests__/devtoolsNavStore.test.ts"],
1914
- ranges: [{ start_line: 1, end_line: Number.MAX_SAFE_INTEGER }],
1915
- full: true,
1916
- }, {
1917
- path: /\/models\/schemas\/devtools\.py$/,
1918
- exactPaths: ["backend/app/models/schemas/devtools.py"],
1919
- ranges: [{ start_line: 1, end_line: Number.MAX_SAFE_INTEGER }],
1920
- full: true,
1921
- });
1922
- }
1923
- return specs;
1924
- }
1925
- function isRuntimeSettingsPersistenceFlowTask(normalizedTask) {
1926
- return (normalizedTask.includes("runtime") &&
1927
- /\bsettings?\b/.test(normalizedTask) &&
1928
- /\b(validat(?:e|es|ed|ing|ion)|dropp?ed|dropp?ing|drops?|persist(?:s|ed|ing|ence)?|dispatch(?:es|ed|ing)?)\b/.test(normalizedTask));
1929
- }
1930
- function isTangentialRuntimeSettingsDiscoveryTestPath(normalizedPath, basename) {
1931
- return (basename === "test_codex_e2e_live.py" ||
1932
- basename === "test_codex_sdk_smoke.py" ||
1933
- basename === "test_codex_runtime_live.py" ||
1934
- basename === "test_transport_absence.py" ||
1935
- basename === "test_resolver.py" ||
1936
- normalizedPath.includes("nightshift") ||
1937
- /\/tests?\/.*codex.*approval.*\.(py|ts|tsx)$/.test(normalizedPath));
1938
- }
1939
- function candidateForTaskSpineSpec(spec, candidates, sourceRoots) {
1940
- const local = localCandidateForTaskSpineSpec(spec, sourceRoots);
1941
- if (local != null) {
1942
- return local;
1943
- }
1944
- const discovered = candidates.find((item) => spec.path.test(item.path.toLowerCase()));
1945
- if (discovered != null) {
1946
- return discovered;
1947
- }
1948
- return undefined;
1949
- }
1950
- function localCandidateForTaskSpineSpec(spec, sourceRoots, preferredRootIndex) {
1951
- for (const exactPath of spec.exactPaths ?? []) {
1952
- const local = localCandidateFromExactPath(exactPath, sourceRoots, preferredRootIndex);
1953
- if (local != null) {
1954
- return local;
1955
- }
1956
- }
1957
- return undefined;
1958
- }
1959
- function localCandidateFromSelectedFile(file, sourceRoots) {
1960
- return localCandidateFromExactPath(file.path, sourceRoots, file.root_index);
1961
- }
1962
- function localCandidateFromExactPath(relativePath, sourceRoots, preferredRootIndex) {
1963
- const rootIndexes = preferredRootIndex == null
1964
- ? sourceRoots.map((_, index) => index)
1965
- : [preferredRootIndex];
1966
- for (const rootIndex of rootIndexes) {
1967
- const root = sourceRoots[rootIndex];
1968
- if (root == null) {
1969
- continue;
1970
- }
1971
- const absolutePath = path.join(root.path, relativePath);
1972
- try {
1973
- const content = readFileSync(absolutePath, "utf8");
1974
- return {
1975
- root_index: rootIndex,
1976
- path: relativePath,
1977
- absolute_path: absolutePath,
1978
- content,
1979
- byte_length: Buffer.byteLength(content, "utf8"),
1980
- line_count: splitTextLines(content).length,
1981
- has_final_newline: content.endsWith("\n"),
1982
- };
1983
- }
1984
- catch {
1985
- continue;
1986
- }
1987
- }
1988
- return undefined;
1989
- }
1990
- function shouldReplaceWithTaskSpineRanges(path, task) {
1991
- const normalized = path.toLowerCase();
1992
- const normalizedTask = task.toLowerCase();
1993
- if (/\bdiscovery\b/.test(normalizedTask) &&
1994
- normalizedTask.includes("runtime") &&
1995
- (normalized.endsWith("/services/chat.py") ||
1996
- normalized.endsWith("/services/claude_agent.py"))) {
1997
- return true;
1998
- }
1999
- if (isDevtoolsTraceDebugTask(task) &&
2000
- (normalized.endsWith("/components/right-panel/rightpaneldevtools.tsx") ||
2001
- normalized.endsWith("/components/chat/chat-window/chat.tsx") ||
2002
- normalized.endsWith("/pages/chatpage.tsx") ||
2003
- normalized.endsWith("/components/chat/message-input/input.tsx") ||
2004
- normalized.endsWith("/api/endpoints/chat.py") ||
2005
- normalized.endsWith("/services/claude_agent.py"))) {
2006
- return true;
2007
- }
2008
- if (isCodexRuntimeReviewTask(task) &&
2009
- (normalized.endsWith("/services/runtime/codex.py") ||
2010
- normalized.endsWith("/services/runtime/codex_approval.py") ||
2011
- normalized.endsWith("/services/runtime/codex_stream_processor.py") ||
2012
- normalized.endsWith("/services/streaming/orchestrator.py") ||
2013
- normalized.endsWith("/services/chat.py") ||
2014
- normalized.endsWith("/tests/services/runtime/test_codex_approval_bridge.py") ||
2015
- normalized.endsWith("/tests/test_permissions.py") ||
2016
- normalized.endsWith("/services/claude_agent.py"))) {
2017
- return true;
2018
- }
2019
- return false;
2020
- }
2021
- function selectedFileFromCandidateRanges(candidate, ranges, source) {
2022
- const taskSpineContent = readTaskSpineCandidateContent(candidate);
2023
- const sourceLines = splitTextLines(taskSpineContent);
2024
- if (sourceLines.length === 0) {
2025
- return null;
2026
- }
2027
- const lineCount = sourceLines.length;
2028
- const bounded = mergeLineRanges(ranges
2029
- .map((range) => ({
2030
- start_line: Math.max(1, range.start_line),
2031
- end_line: Math.min(Math.max(1, lineCount), range.end_line),
2032
- }))
2033
- .filter((range) => range.start_line <= range.end_line));
2034
- if (bounded.length === 0) {
2035
- return null;
2036
- }
2037
- const content = bounded
2038
- .flatMap((range) => sourceLines.slice(range.start_line - 1, range.end_line))
2039
- .join("\n");
2040
- const bytes = Buffer.byteLength(content, "utf8");
2041
- if (bytes <= 0) {
2042
- return null;
2043
- }
2044
- return {
2045
- root_index: candidate.root_index,
2046
- path: candidate.path,
2047
- ranges: bounded,
2048
- content,
2049
- bytes,
2050
- estimated_tokens: estimateContextBuilderTokensFromUtf8Bytes(bytes),
2051
- source,
2052
- };
2053
- }
2054
- function readTaskSpineCandidateContent(candidate) {
2055
- try {
2056
- return readFileSync(candidate.absolute_path, "utf8");
2057
- }
2058
- catch {
2059
- return candidate.content;
2060
- }
2061
- }
2062
- function appendDelegateRepairWarnings(warnings, repair) {
2063
- if (repair.expandedCount > 0) {
2064
- warnings.push(delegateWarning("delegate_curator_warning", `server-expanded ${repair.expandedCount} selected file(s) to full context`));
2065
- }
2066
- if (repair.droppedDegenerateCount > 0) {
2067
- warnings.push(delegateWarning("delegate_curator_warning", `server-dropped ${repair.droppedDegenerateCount} degenerate selected slice(s)`));
2068
- }
2069
- if (repair.deterministicRetainedCount > 0) {
2070
- warnings.push(delegateWarning("delegate_curator_warning", `server-retained ${repair.deterministicRetainedCount} compact deterministic closure file(s) after curator underselection`));
2071
- }
2072
- if (repair.discoveredRetainedCount > 0) {
2073
- warnings.push(delegateWarning("delegate_curator_warning", `server-retained ${repair.discoveredRetainedCount} task-critical discovered closure file(s) after curator underselection`));
2074
- }
2075
- if (repair.evictedCount > 0) {
2076
- warnings.push(delegateWarning("delegate_curator_warning", `server-evicted ${repair.evictedCount} lower-priority selected file(s) to retain required closure`));
2077
- }
2078
- }
2079
- function retainDeterministicClosureFiles(selection, deterministicSelection, budget, task) {
2080
- const selectedKeys = new Set(selection.selected.map((file) => selectedFileKey(file)));
2081
- let totalBytes = selection.metrics.bytes_selected;
2082
- let totalTokens = selection.metrics.estimated_tokens;
2083
- const selected = [...selection.selected];
2084
- let retainedCount = 0;
2085
- for (const file of deterministicSelection.selected) {
2086
- if (selectedKeys.has(selectedFileKey(file)) ||
2087
- !isRetainableClosureFile(file, task)) {
2088
- continue;
2089
- }
2090
- if (selected.length >= budget.max_files ||
2091
- file.bytes > budget.max_bytes_per_file ||
2092
- totalBytes + file.bytes > budget.max_total_bytes ||
2093
- totalTokens + file.estimated_tokens > budget.target_estimated_tokens) {
2094
- continue;
2095
- }
2096
- selected.push(file);
2097
- selectedKeys.add(selectedFileKey(file));
2098
- totalBytes += file.bytes;
2099
- totalTokens += file.estimated_tokens;
2100
- retainedCount += 1;
2101
- }
2102
- if (retainedCount === 0) {
2103
- return { selection, retainedCount };
2104
- }
2105
- return {
2106
- retainedCount,
2107
- selection: {
2108
- ...selection,
2109
- selected,
2110
- metrics: {
2111
- files_selected: selected.length,
2112
- files_omitted: selection.metrics.files_omitted,
2113
- bytes_selected: totalBytes,
2114
- estimated_tokens: totalTokens,
2115
- truncated: selection.metrics.truncated,
2116
- },
2117
- },
2118
- };
2119
- }
2120
- function expandSelectedFocusFilesToFullCandidates(selection, candidates, focusPaths, budget, task = "") {
2121
- if (!shouldExpandFocusFilesForTask(task)) {
2122
- return { selection, expandedCount: 0 };
2123
- }
2124
- const focusKeys = new Set(focusPaths
2125
- .filter((focus) => focus.kind === "file")
2126
- .map((focus) => `${focus.root_index}\u0000${focus.root_relative_path}`));
2127
- if (focusKeys.size === 0) {
2128
- return { selection, expandedCount: 0 };
2129
- }
2130
- const candidatesByKey = new Map(candidates.map((candidate) => [selectedFileKey(candidate), candidate]));
2131
- let totalBytes = selection.metrics.bytes_selected;
2132
- let totalTokens = selection.metrics.estimated_tokens;
2133
- let expandedCount = 0;
2134
- const selected = selection.selected.map((file) => {
2135
- const key = selectedFileKey(file);
2136
- if (!focusKeys.has(key)) {
1248
+ if (file.ranges.length !== 1) {
2137
1249
  return file;
2138
1250
  }
2139
1251
  const candidate = candidatesByKey.get(key);
@@ -2144,10 +1256,6 @@ function expandSelectedFocusFilesToFullCandidates(selection, candidates, focusPa
2144
1256
  if (fullFile.bytes <= file.bytes || fileCoversCandidate(file, candidate)) {
2145
1257
  return file;
2146
1258
  }
2147
- if (fullFile.estimated_tokens >
2148
- focusExpansionTokenLimit(candidate.path, task, budget)) {
2149
- return file;
2150
- }
2151
1259
  const totalsWithoutFile = {
2152
1260
  totalBytes: totalBytes - file.bytes,
2153
1261
  totalTokens: totalTokens - file.estimated_tokens,
@@ -2181,41 +1289,30 @@ function expandSelectedFocusFilesToFullCandidates(selection, candidates, focusPa
2181
1289
  },
2182
1290
  };
2183
1291
  }
2184
- function shouldExpandFocusFilesForTask(task) {
2185
- return /\b(debug(?:ging)?|review|discovery|implement(?:ation)?|fix|diagnose)\b/i.test(task);
2186
- }
2187
- function focusExpansionTokenLimit(path, task, budget) {
2188
- const normalized = path.toLowerCase();
2189
- const normalizedTask = task.toLowerCase();
2190
- if (/\b(debug|debugging|diagnose|diagnosis|diagnostic)\b/.test(normalizedTask) &&
2191
- normalized.endsWith("/rightpaneldevtools.tsx")) {
2192
- return 35_000;
2193
- }
2194
- if (/\b(review|discovery|provider|runtime|codex|claude)\b/.test(normalizedTask) &&
2195
- normalized.includes("/services/runtime/")) {
2196
- return Math.min(18_000, Math.floor(budget.target_estimated_tokens * 0.2));
2197
- }
2198
- return Math.min(12_000, Math.floor(budget.target_estimated_tokens * 0.18));
2199
- }
2200
- function expandSelectedClosureFilesToFullCandidates(selection, candidates, task, budget) {
1292
+ const SMALL_SELECTED_SLICE_FULL_EXPANSION_MAX_BYTES = 64_000;
1293
+ function expandSelectedSmallFilesToFullCandidates(selection, candidates, focusPaths, budget) {
2201
1294
  const candidatesByKey = new Map(candidates.map((candidate) => [selectedFileKey(candidate), candidate]));
1295
+ const focusKeys = new Set(focusPaths
1296
+ .filter((focus) => focus.kind === "file")
1297
+ .map((focus) => `${focus.root_index}\u0000${focus.root_relative_path}`));
2202
1298
  let totalBytes = selection.metrics.bytes_selected;
2203
1299
  let totalTokens = selection.metrics.estimated_tokens;
2204
1300
  let expandedCount = 0;
2205
1301
  const selected = selection.selected.map((file) => {
2206
- if (!isRetainableClosureFile(file, task) &&
2207
- !isContextBuilderTaskClosureAnchor(file.path, task) &&
2208
- !isTaskCriticalDiscoveredClosure(file.path, task)) {
1302
+ if (focusKeys.has(selectedFileKey(file)) || file.ranges.length !== 1) {
2209
1303
  return file;
2210
1304
  }
2211
1305
  const candidate = candidatesByKey.get(selectedFileKey(file));
2212
1306
  if (candidate == null || fileCoversCandidate(file, candidate)) {
2213
1307
  return file;
2214
1308
  }
2215
- if (candidate.byte_length > retainableClosureByteLimit(candidate.path, task)) {
1309
+ if (candidate.byte_length > SMALL_SELECTED_SLICE_FULL_EXPANSION_MAX_BYTES) {
2216
1310
  return file;
2217
1311
  }
2218
1312
  const fullFile = selectedFileFromCandidate(candidate);
1313
+ if (fullFile.bytes <= file.bytes) {
1314
+ return file;
1315
+ }
2219
1316
  const totalsWithoutFile = {
2220
1317
  totalBytes: totalBytes - file.bytes,
2221
1318
  totalTokens: totalTokens - file.estimated_tokens,
@@ -2249,413 +1346,17 @@ function expandSelectedClosureFilesToFullCandidates(selection, candidates, task,
2249
1346
  },
2250
1347
  };
2251
1348
  }
2252
- function retainDiscoveredTaskCriticalFiles(selection, candidates, task, budget) {
2253
- let selectedKeys = new Set(selection.selected.map((file) => selectedFileKey(file)));
2254
- let totalBytes = selection.metrics.bytes_selected;
2255
- let totalTokens = selection.metrics.estimated_tokens;
2256
- let selected = [...selection.selected];
2257
- let retainedCount = 0;
2258
- let evictedCount = 0;
2259
- for (const candidate of [...candidates]
2260
- .filter((item) => !isTangentialTaskClosureCandidate(item.path, task) &&
2261
- (isContextBuilderTaskClosureAnchor(item.path, task) ||
2262
- isTaskCriticalDiscoveredClosure(item.path, task)))
2263
- .sort((left, right) => compareTaskCriticalCandidates(left, right, task))) {
2264
- const key = selectedFileKey(candidate);
2265
- if (selectedKeys.has(key) ||
2266
- (candidate.byte_length > retainableClosureByteLimit(candidate.path, task) &&
2267
- compactCandidateForRequiredClosure(candidate, task) == null)) {
2268
- continue;
2269
- }
2270
- const file = candidate.byte_length > retainableClosureByteLimit(candidate.path, task)
2271
- ? compactCandidateForRequiredClosure(candidate, task)
2272
- : selectedFileFromCandidate(candidate);
2273
- if (file == null) {
2274
- continue;
2275
- }
2276
- if (!selectionFitsBudgetWithFile(selected, totalBytes, totalTokens, file, budget)) {
2277
- const repaired = makeRoomForRequiredClosureFile({
2278
- selected,
2279
- selectedKeys,
2280
- totalBytes,
2281
- totalTokens,
2282
- file,
2283
- budget,
2284
- task,
2285
- });
2286
- if (repaired == null) {
2287
- continue;
2288
- }
2289
- selected = repaired.selected;
2290
- selectedKeys = repaired.selectedKeys;
2291
- totalBytes = repaired.totalBytes;
2292
- totalTokens = repaired.totalTokens;
2293
- evictedCount += repaired.evictedCount;
2294
- }
2295
- selected.push(file);
2296
- selectedKeys.add(key);
2297
- totalBytes += file.bytes;
2298
- totalTokens += file.estimated_tokens;
2299
- retainedCount += 1;
2300
- }
2301
- if (retainedCount === 0 && evictedCount === 0) {
2302
- return { selection, retainedCount, evictedCount };
2303
- }
2304
- return {
2305
- retainedCount,
2306
- evictedCount,
2307
- selection: {
2308
- ...selection,
2309
- selected,
2310
- metrics: {
2311
- files_selected: selected.length,
2312
- files_omitted: selection.metrics.files_omitted,
2313
- bytes_selected: totalBytes,
2314
- estimated_tokens: totalTokens,
2315
- truncated: selection.metrics.truncated,
2316
- },
2317
- },
2318
- };
2319
- }
2320
- function dropDegenerateSelectedSlices(selection, candidates, task) {
2321
- const candidatesByKey = new Map(candidates.map((candidate) => [selectedFileKey(candidate), candidate]));
2322
- const selected = selection.selected.filter((file) => {
2323
- if (file.content.trim().length === 0) {
2324
- return false;
2325
- }
2326
- if (!isDegenerateSelectedSlice(file) || file.source === "focus") {
2327
- return true;
2328
- }
2329
- if (!isRequiredClosurePath(file.path, task)) {
2330
- return true;
2331
- }
2332
- const candidate = candidatesByKey.get(selectedFileKey(file));
2333
- return (candidate != null &&
2334
- fileCoversCandidate(file, candidate) &&
2335
- isRequiredClosurePath(candidate.path, task));
2336
- });
1349
+ function dropEmptySelectedFiles(selection) {
1350
+ const selected = selection.selected.filter((file) => file.content.trim().length > 0);
2337
1351
  const droppedCount = selection.selected.length - selected.length;
2338
1352
  if (droppedCount === 0) {
2339
1353
  return { selection, droppedCount };
2340
1354
  }
2341
1355
  return {
2342
- droppedCount,
2343
1356
  selection: selectionWithSelectedFiles(selection, selected),
1357
+ droppedCount,
2344
1358
  };
2345
1359
  }
2346
- function makeRoomForRequiredClosureFile(input) {
2347
- if (input.file.bytes > input.budget.max_bytes_per_file) {
2348
- return null;
2349
- }
2350
- const incomingRank = taskCriticalPathRank(input.file.path, input.task);
2351
- let selected = [...input.selected];
2352
- let selectedKeys = new Set(input.selectedKeys);
2353
- let totalBytes = input.totalBytes;
2354
- let totalTokens = input.totalTokens;
2355
- let evictedCount = 0;
2356
- if (!selectionFitsBudgetWithFile(selected, totalBytes, totalTokens, input.file, input.budget)) {
2357
- const compacted = compactSelectedFilesForRequiredClosure({
2358
- selected,
2359
- totalBytes,
2360
- totalTokens,
2361
- incomingFile: input.file,
2362
- task: input.task,
2363
- });
2364
- selected = compacted.selected;
2365
- totalBytes = compacted.totalBytes;
2366
- totalTokens = compacted.totalTokens;
2367
- }
2368
- const evictionOrder = selected
2369
- .map((file, index) => ({ file, index }))
2370
- .filter(({ file }) => canEvictForRequiredClosure(file, input.task, incomingRank))
2371
- .sort((left, right) => selectedEvictionRank(right.file, input.task, incomingRank) -
2372
- selectedEvictionRank(left.file, input.task, incomingRank) ||
2373
- right.index - left.index);
2374
- for (const { file } of evictionOrder) {
2375
- if (selectionFitsBudgetWithFile(selected, totalBytes, totalTokens, input.file, input.budget)) {
2376
- break;
2377
- }
2378
- const index = selected.findIndex((item) => selectedFileKey(item) === selectedFileKey(file));
2379
- if (index < 0) {
2380
- continue;
2381
- }
2382
- const [removed] = selected.splice(index, 1);
2383
- if (removed == null) {
2384
- continue;
2385
- }
2386
- selectedKeys.delete(selectedFileKey(removed));
2387
- totalBytes -= removed.bytes;
2388
- totalTokens -= removed.estimated_tokens;
2389
- evictedCount += 1;
2390
- }
2391
- if (!selectionFitsBudgetWithFile(selected, totalBytes, totalTokens, input.file, input.budget)) {
2392
- return null;
2393
- }
2394
- return { selected, selectedKeys, totalBytes, totalTokens, evictedCount };
2395
- }
2396
- function selectionFitsBudgetWithFile(selected, totalBytes, totalTokens, file, budget) {
2397
- return (selected.length < budget.max_files &&
2398
- file.bytes <= budget.max_bytes_per_file &&
2399
- totalBytes + file.bytes <= budget.max_total_bytes &&
2400
- totalTokens + file.estimated_tokens <= budget.target_estimated_tokens);
2401
- }
2402
- const REQUIRED_CLOSURE_COMPACT_MIN_TOKENS = 3_000;
2403
- const REQUIRED_CLOSURE_COMPACT_HEAD_LINES = 60;
2404
- const REQUIRED_CLOSURE_COMPACT_CONTEXT_LINES = 24;
2405
- const REQUIRED_CLOSURE_COMPACT_MAX_LINES = 260;
2406
- function compactSelectedFilesForRequiredClosure(input) {
2407
- let totalBytes = input.totalBytes;
2408
- let totalTokens = input.totalTokens;
2409
- const selected = input.selected.map((file) => {
2410
- const compacted = compactSelectedFileForRequiredClosure(file, input.incomingFile, input.task);
2411
- if (compacted == null) {
2412
- return file;
2413
- }
2414
- totalBytes = totalBytes - file.bytes + compacted.bytes;
2415
- totalTokens = totalTokens - file.estimated_tokens + compacted.estimated_tokens;
2416
- return compacted;
2417
- });
2418
- return { selected, totalBytes, totalTokens };
2419
- }
2420
- function compactSelectedFileForRequiredClosure(file, incomingFile, task) {
2421
- if (file.source !== "focus" ||
2422
- isProtectedReviewFocusPath(file.path, task) ||
2423
- file.estimated_tokens <= REQUIRED_CLOSURE_COMPACT_MIN_TOKENS ||
2424
- selectedRangeLineCount(file) <= REQUIRED_CLOSURE_COMPACT_HEAD_LINES) {
2425
- return null;
2426
- }
2427
- const sourceLines = splitTextLines(file.content);
2428
- if (sourceLines.length <= REQUIRED_CLOSURE_COMPACT_HEAD_LINES) {
2429
- return null;
2430
- }
2431
- const sourceLineNumbers = selectedFileContentLineNumbers(file, sourceLines.length);
2432
- if (sourceLineNumbers.length !== sourceLines.length) {
2433
- return null;
2434
- }
2435
- const tokens = new Set([
2436
- ...tokenizeRepairWords(task),
2437
- ...tokenizeRepairWords(incomingFile.path),
2438
- ...tokenizeRepairWords(file.path),
2439
- ]);
2440
- const selectedLineNumbers = new Set();
2441
- addOriginalLineWindow(selectedLineNumbers, sourceLineNumbers, sourceLineNumbers[0] ?? 1, (sourceLineNumbers[0] ?? 1) + REQUIRED_CLOSURE_COMPACT_HEAD_LINES - 1);
2442
- for (let index = 0; index < sourceLines.length; index++) {
2443
- if (selectedLineNumbers.size >= REQUIRED_CLOSURE_COMPACT_MAX_LINES) {
2444
- break;
2445
- }
2446
- const line = sourceLines[index] ?? "";
2447
- if (!lineMatchesRepairTokens(line, tokens)) {
2448
- continue;
2449
- }
2450
- const originalLine = sourceLineNumbers[index];
2451
- if (originalLine == null) {
2452
- continue;
2453
- }
2454
- addOriginalLineWindow(selectedLineNumbers, sourceLineNumbers, originalLine - REQUIRED_CLOSURE_COMPACT_CONTEXT_LINES, originalLine + REQUIRED_CLOSURE_COMPACT_CONTEXT_LINES);
2455
- }
2456
- if (selectedLineNumbers.size >= sourceLines.length) {
2457
- return null;
2458
- }
2459
- const ranges = mergeLineRanges([...selectedLineNumbers].map((line) => ({
2460
- start_line: line,
2461
- end_line: line,
2462
- })));
2463
- const linesByOriginalLine = new Map();
2464
- for (let index = 0; index < sourceLines.length; index++) {
2465
- const lineNumber = sourceLineNumbers[index];
2466
- if (lineNumber != null) {
2467
- linesByOriginalLine.set(lineNumber, sourceLines[index] ?? "");
2468
- }
2469
- }
2470
- const content = ranges
2471
- .flatMap((range) => {
2472
- const lines = [];
2473
- for (let line = range.start_line; line <= range.end_line; line++) {
2474
- const contentLine = linesByOriginalLine.get(line);
2475
- if (contentLine != null) {
2476
- lines.push(contentLine);
2477
- }
2478
- }
2479
- return lines;
2480
- })
2481
- .join("\n");
2482
- const bytes = Buffer.byteLength(content, "utf8");
2483
- if (bytes >= file.bytes) {
2484
- return null;
2485
- }
2486
- return {
2487
- ...file,
2488
- ranges,
2489
- content,
2490
- bytes,
2491
- estimated_tokens: estimateContextBuilderTokensFromUtf8Bytes(bytes),
2492
- };
2493
- }
2494
- function selectedFileContentLineNumbers(file, lineCount) {
2495
- const numbers = [];
2496
- for (const range of file.ranges) {
2497
- for (let line = range.start_line; line <= range.end_line; line++) {
2498
- numbers.push(line);
2499
- if (numbers.length >= lineCount) {
2500
- return numbers;
2501
- }
2502
- }
2503
- }
2504
- return numbers;
2505
- }
2506
- function addOriginalLineWindow(selectedLineNumbers, availableLineNumbers, startLine, endLine) {
2507
- const available = new Set(availableLineNumbers);
2508
- for (let line = startLine; line <= endLine; line++) {
2509
- if (selectedLineNumbers.size >= REQUIRED_CLOSURE_COMPACT_MAX_LINES) {
2510
- return;
2511
- }
2512
- if (available.has(line)) {
2513
- selectedLineNumbers.add(line);
2514
- }
2515
- }
2516
- }
2517
- function lineMatchesRepairTokens(line, tokens) {
2518
- for (const token of tokenizeRepairWords(line)) {
2519
- if (tokens.has(token)) {
2520
- return true;
2521
- }
2522
- }
2523
- return false;
2524
- }
2525
- function tokenizeRepairWords(input) {
2526
- return input
2527
- .toLowerCase()
2528
- .split(/[^a-z0-9]+/)
2529
- .filter((token) => token.length >= 3 && !REPAIR_STOP_WORDS.has(token));
2530
- }
2531
- const REPAIR_STOP_WORDS = new Set([
2532
- "app",
2533
- "src",
2534
- "the",
2535
- "and",
2536
- "for",
2537
- "with",
2538
- "from",
2539
- "this",
2540
- "that",
2541
- "file",
2542
- "test",
2543
- "tests",
2544
- ]);
2545
- function canEvictForRequiredClosure(file, task, incomingRank) {
2546
- const existingRank = taskCriticalPathRank(file.path, task);
2547
- if (file.source === "focus") {
2548
- return (incomingRank <= 0 &&
2549
- existingRank > incomingRank &&
2550
- !isProtectedReviewFocusPath(file.path, task));
2551
- }
2552
- if (isRequiredClosurePath(file.path, task) && existingRank <= incomingRank) {
2553
- return false;
2554
- }
2555
- return true;
2556
- }
2557
- function selectedEvictionRank(file, task, incomingRank) {
2558
- let rank = file.estimated_tokens;
2559
- if (isDegenerateSelectedSlice(file)) {
2560
- rank += 10_000;
2561
- }
2562
- if (!isRequiredClosurePath(file.path, task)) {
2563
- rank += 5_000;
2564
- }
2565
- rank += Math.max(0, taskCriticalPathRank(file.path, task) - incomingRank) * 500;
2566
- if (isGenericSelectedPath(file.path)) {
2567
- rank += 1_000;
2568
- }
2569
- return rank;
2570
- }
2571
- function isDegenerateSelectedSlice(file) {
2572
- return file.content.trim().length === 0 || selectedRangeLineCount(file) <= 1;
2573
- }
2574
- function selectedRangeLineCount(file) {
2575
- return file.ranges.reduce((total, range) => total + Math.max(0, range.end_line - range.start_line + 1), 0);
2576
- }
2577
- function isRequiredClosurePath(path, task) {
2578
- return (isContextBuilderTaskClosureAnchor(path, task) ||
2579
- isTaskCriticalDiscoveredClosure(path, task) ||
2580
- taskCriticalPathRank(path, task) <= 0);
2581
- }
2582
- function isGenericSelectedPath(path) {
2583
- const normalized = path.toLowerCase();
2584
- return (normalized.includes("/settings/") ||
2585
- normalized.includes("/components/ui/") ||
2586
- normalized.includes("/admin/") ||
2587
- normalized.includes("/models/db_models/__init__.py") ||
2588
- normalized.includes("/db/base.py"));
2589
- }
2590
- function isDevtoolsTraceDebugTask(task) {
2591
- const normalized = task.toLowerCase();
2592
- return (/\b(debug|debugging|diagnose|diagnosis|diagnostic)\b/.test(normalized) &&
2593
- normalized.includes("devtools") &&
2594
- normalized.includes("trace"));
2595
- }
2596
- function isDevtoolsTraceRelevantPath(path) {
2597
- if (isTangentialDevtoolsTraceDebugPath(path)) {
2598
- return false;
2599
- }
2600
- const normalized = path.toLowerCase();
2601
- return (normalized.includes("/right-panel/") ||
2602
- normalized.endsWith("/rightpaneldevtools.tsx") ||
2603
- normalized.includes("/store/devtoolsnavstore") ||
2604
- normalized.includes("/store/__tests__/devtoolsnavstore.test.ts") ||
2605
- normalized.includes("/services/devtoolsservice") ||
2606
- normalized.includes("/hooks/queries/usedevtoolsqueries") ||
2607
- normalized.includes("/hooks/queries/querykeys") ||
2608
- normalized.endsWith("/querykeys.ts") ||
2609
- normalized.includes("/types/devtools.types") ||
2610
- normalized.endsWith("/models/schemas/devtools.py") ||
2611
- normalized.includes("/api/endpoints/devtools.py") ||
2612
- normalized.includes("/session_trace_archive") ||
2613
- normalized.endsWith("/services/codex_trace.py") ||
2614
- normalized.includes("/chat-window/chat.tsx") ||
2615
- normalized.includes("/chat/__tests__/chat.test.tsx") ||
2616
- normalized.includes("/message-bubble/message.tsx") ||
2617
- normalized.includes("/message-bubble/messagerenderer.tsx") ||
2618
- normalized.includes("/tools/common/toolcard.tsx") ||
2619
- normalized.includes("/tools/common/toolexpandcontext.ts") ||
2620
- normalized.includes("/tools/common/__tests__/toolexpand.test.tsx") ||
2621
- normalized.includes("/hooks/usemessagecache.ts") ||
2622
- normalized.includes("/utils/messagecacheorder.ts"));
2623
- }
2624
- function shouldExpandSmallDevtoolsTraceFile(path) {
2625
- const normalized = path.toLowerCase();
2626
- return (normalized.endsWith("/rightpanel.tsx") ||
2627
- normalized.endsWith("/tools/common/toolcard.tsx") ||
2628
- normalized.endsWith("/tools/common/toolexpandcontext.ts"));
2629
- }
2630
- function isTangentialDevtoolsTraceDebugPath(path) {
2631
- const normalized = path.toLowerCase();
2632
- const basename = normalized.split("/").at(-1) ?? normalized;
2633
- return (basename === "advancedblocknoteeditor.tsx" ||
2634
- basename === "githubsettingspopover.tsx" ||
2635
- basename === "input.tsx" ||
2636
- basename === "inputsettingspopover.tsx" ||
2637
- basename === "mentionsuggestionspanel.tsx" ||
2638
- basename === "segmentbuilder.ts" ||
2639
- basename === "sendbutton.tsx" ||
2640
- basename === "slashcommandspanel.tsx" ||
2641
- normalized === "backend/tests/test_devtools_trace_archive.py" ||
2642
- normalized.endsWith("/backend/tests/test_devtools_trace_archive.py"));
2643
- }
2644
- function isTangentialTaskClosureCandidate(path, task) {
2645
- return ((isDevtoolsTraceDebugTask(task) && isTangentialDevtoolsTraceDebugPath(path)) ||
2646
- (isCodexRuntimeReviewTask(task) && isTangentialCodexRuntimeReviewUiPath(path)));
2647
- }
2648
- function isProtectedReviewFocusPath(path, task) {
2649
- if (!/\breview\b/i.test(task)) {
2650
- return false;
2651
- }
2652
- const normalized = path.toLowerCase();
2653
- return (normalized.includes("/services/runtime/") ||
2654
- normalized.endsWith("/services/runtime_session_service.py") ||
2655
- normalized.endsWith("/services/streaming/orchestrator.py") ||
2656
- normalized.includes("/tests/services/runtime/") ||
2657
- normalized.endsWith("/tests/services/test_runtime_session_service.py"));
2658
- }
2659
1360
  function selectionWithSelectedFiles(selection, selected) {
2660
1361
  const bytes = selected.reduce((total, file) => total + file.bytes, 0);
2661
1362
  const tokens = selected.reduce((total, file) => total + file.estimated_tokens, 0);
@@ -2674,54 +1375,6 @@ function selectionWithSelectedFiles(selection, selected) {
2674
1375
  function selectedFileKey(file) {
2675
1376
  return `${file.root_index}\u0000${file.path}`;
2676
1377
  }
2677
- const RETAINABLE_CLOSURE_MAX_BYTES = 24_000;
2678
- const RETAINABLE_PROVIDER_IMPL_MAX_BYTES = 48_000;
2679
- function isRetainableClosureFile(file, task) {
2680
- if (file.bytes > retainableClosureByteLimit(file.path, task)) {
2681
- return false;
2682
- }
2683
- const normalized = file.path.toLowerCase();
2684
- const basename = normalized.split("/").at(-1) ?? normalized;
2685
- return (normalized.includes("/types/") ||
2686
- normalized.includes("/schemas/") ||
2687
- normalized.includes("/constants.") ||
2688
- normalized.includes("/config.") ||
2689
- normalized.includes("/querykeys.") ||
2690
- normalized.includes("/messagerenderer.") ||
2691
- normalized.includes("/message-bubble/message.") ||
2692
- normalized.includes("/toolcard.") ||
2693
- normalized.includes("/toolexpandcontext.") ||
2694
- normalized.includes("/messagecacheorder.") ||
2695
- normalized.includes("/session_trace_archive.") ||
2696
- normalized.includes("/runtime/base.") ||
2697
- normalized.includes("/claude_agent.") ||
2698
- normalized.includes("/rightpanel.") ||
2699
- basename.endsWith(".types.ts") ||
2700
- basename === "api.types.ts" ||
2701
- basename === "types.ts" ||
2702
- basename === "schema.ts" ||
2703
- basename === "schemas.ts" ||
2704
- basename === "constants.ts" ||
2705
- basename === "constants.py" ||
2706
- basename === "config.ts" ||
2707
- normalized.endsWith("/core/config.py") ||
2708
- basename === "querykeys.ts" ||
2709
- basename === "permission_mode.py" ||
2710
- basename === "effort.py" ||
2711
- basename === "codex_trace.py");
2712
- }
2713
- function retainableClosureByteLimit(path, task) {
2714
- const normalized = path.toLowerCase();
2715
- if (normalized.endsWith("codex_trace.py")) {
2716
- return 12_000;
2717
- }
2718
- if (/\b(provider|runtime|codex|claude|review|discovery)\b/.test(task.toLowerCase()) &&
2719
- (normalized.endsWith("/claude_agent.py") ||
2720
- normalized.endsWith("/runtime/base.py"))) {
2721
- return RETAINABLE_PROVIDER_IMPL_MAX_BYTES;
2722
- }
2723
- return RETAINABLE_CLOSURE_MAX_BYTES;
2724
- }
2725
1378
  function selectedFileFromCandidate(candidate) {
2726
1379
  const bytes = Buffer.byteLength(candidate.content, "utf8");
2727
1380
  return {
@@ -2734,421 +1387,21 @@ function selectedFileFromCandidate(candidate) {
2734
1387
  source: "search",
2735
1388
  };
2736
1389
  }
2737
- function compactCandidateForRequiredClosure(candidate, task) {
2738
- const sourceLines = splitTextLines(candidate.content);
2739
- if (sourceLines.length === 0) {
2740
- return null;
2741
- }
2742
- const tokens = new Set([
2743
- ...tokenizeRepairWords(task),
2744
- ...tokenizeRepairWords(candidate.path),
2745
- ]);
2746
- const selectedLineNumbers = new Set();
2747
- const availableLineNumbers = sourceLines.map((_, index) => index + 1);
2748
- addOriginalLineWindow(selectedLineNumbers, availableLineNumbers, 1, REQUIRED_CLOSURE_COMPACT_HEAD_LINES);
2749
- for (let index = 0; index < sourceLines.length; index++) {
2750
- if (selectedLineNumbers.size >= REQUIRED_CLOSURE_COMPACT_MAX_LINES) {
2751
- break;
2752
- }
2753
- const line = sourceLines[index] ?? "";
2754
- if (!lineMatchesRepairTokens(line, tokens)) {
2755
- continue;
2756
- }
2757
- const originalLine = index + 1;
2758
- addOriginalLineWindow(selectedLineNumbers, availableLineNumbers, originalLine - REQUIRED_CLOSURE_COMPACT_CONTEXT_LINES, originalLine + REQUIRED_CLOSURE_COMPACT_CONTEXT_LINES);
2759
- }
2760
- if (selectedLineNumbers.size === 0 || selectedLineNumbers.size >= sourceLines.length) {
2761
- return null;
2762
- }
2763
- const ranges = mergeLineRanges([...selectedLineNumbers].map((line) => ({
2764
- start_line: line,
2765
- end_line: line,
2766
- })));
2767
- const content = ranges
2768
- .flatMap((range) => sourceLines.slice(range.start_line - 1, range.end_line))
2769
- .join("\n");
2770
- const bytes = Buffer.byteLength(content, "utf8");
2771
- if (bytes <= 0 || bytes >= candidate.byte_length) {
2772
- return null;
2773
- }
2774
- return {
2775
- root_index: candidate.root_index,
2776
- path: candidate.path,
2777
- ranges,
2778
- content,
2779
- bytes,
2780
- estimated_tokens: estimateContextBuilderTokensFromUtf8Bytes(bytes),
2781
- source: "search",
2782
- };
2783
- }
2784
- function isTaskCriticalDiscoveredClosure(path, task) {
2785
- const normalizedTask = task.toLowerCase();
2786
- const normalized = path.toLowerCase();
2787
- const basename = normalized.split("/").at(-1) ?? normalized;
2788
- const providerRuntimeTask = /\b(provider|provider-specific|runtime|codex|claude)\b/.test(normalizedTask);
2789
- if (/\bdebug(?:ging)?\b/.test(normalizedTask) &&
2790
- !isTangentialDevtoolsTraceDebugPath(path) &&
2791
- (basename === "codex_trace.py" ||
2792
- basename === "session_trace_archive.py" ||
2793
- basename === "toolcard.tsx" ||
2794
- basename === "devtools-helpers.ts" ||
2795
- basename === "devtoolsservice.ts" ||
2796
- basename === "usedevtoolsqueries.ts" ||
2797
- basename === "devtoolsnavstore.ts" ||
2798
- basename === "messagerenderer.tsx" ||
2799
- basename === "rightpaneldevtools.tsx" ||
2800
- basename === "rightpanel.tsx" ||
2801
- basename === "usemessagecache.ts" ||
2802
- basename === "chat.tsx" ||
2803
- basename === "right-panel-devtools.test.tsx" ||
2804
- normalized.endsWith("/api/endpoints/devtools.py") ||
2805
- normalized.endsWith("/models/db_models/session_trace_archive.py") ||
2806
- normalized.includes("devtools-helpers.test") ||
2807
- normalized.includes("toolexpand.test") ||
2808
- normalized.includes("devtoolsnavstore.test"))) {
2809
- return true;
2810
- }
2811
- if (/\breview\b/.test(normalizedTask) &&
2812
- !isTangentialCodexRuntimeReviewUiPath(path) &&
2813
- ((normalized.endsWith("/core/config.py") &&
2814
- /\bconfig(?:uration)?\b/.test(normalizedTask)) ||
2815
- basename === "constants.py" ||
2816
- basename === "permission_manager.py" ||
2817
- basename === "ai_model.py" ||
2818
- basename === "permissions.py" ||
2819
- basename === "permission_mode.py" ||
2820
- basename === "permissions.ts" ||
2821
- basename === "effort.py" ||
2822
- basename === "usepermissionrequest.ts" ||
2823
- basename === "usemessageactions.ts" ||
2824
- basename === "fileuploaddialog.tsx" ||
2825
- basename === "modelprovider.ts" ||
2826
- basename === "ai_models.py" ||
2827
- basename === "test_ai_models_codex_gate.py" ||
2828
- basename === "model_provider_auth.py" ||
2829
- basename === "chat.types.ts" ||
2830
- basename === "chatservice.ts" ||
2831
- normalized.endsWith("/api/endpoints/chat.py") ||
2832
- normalized.endsWith("/services/chat.py") ||
2833
- basename === "streamservice.ts" ||
2834
- basename === "fileuploaddialog.codex.test.tsx" ||
2835
- basename === "test_permissions.py" ||
2836
- basename === "test_model_provider_auth.py" ||
2837
- normalized.endsWith("/api/endpoints/permissions.py") ||
2838
- isTaskCriticalReviewDoc(normalized) ||
2839
- normalized.includes("resolver") && normalized.includes("test") ||
2840
- normalized.includes("modelprovider") && normalized.includes("test"))) {
2841
- return true;
2842
- }
2843
- if (/\bdiscovery\b/.test(normalizedTask) &&
2844
- (basename === "permission_mode.py" ||
2845
- normalized.endsWith("/services/chat.py") ||
2846
- normalized.endsWith("/components/chat/chat-window/chat.tsx") ||
2847
- normalized.endsWith("/components/chat/message-input/input.tsx") ||
2848
- normalized.endsWith("/components/chat/message-input/composerruntimecontrols.tsx") ||
2849
- normalized.endsWith("/components/chat/message-input/inputsettingspopover.tsx") ||
2850
- normalized.endsWith("/hooks/usechatruntimesettings.ts") ||
2851
- normalized.endsWith("/pages/chatpage.tsx") ||
2852
- normalized.endsWith("/api/endpoints/chat.py") ||
2853
- normalized.endsWith("/models/db_models/chat.py") ||
2854
- normalized.endsWith("/models/schemas/chat.py") ||
2855
- basename === "chatservice.ts" ||
2856
- basename === "messagequeuestore.ts" ||
2857
- basename === "streamservice.ts" ||
2858
- basename === "messagequeuestore.ts" ||
2859
- basename === "usepermissionrequest.ts" ||
2860
- basename === "usesettingsqueries.ts" ||
2861
- basename === "chat_processor.py" ||
2862
- basename === "landingpage.tsx" ||
2863
- basename === "usechatstreaming.ts" ||
2864
- basename === "usemessageactions.ts" ||
2865
- basename === "queue.py" ||
2866
- basename === "test_chat.py" ||
2867
- basename === "test_queue.py" ||
2868
- normalized.endsWith("/tests/services/test_queue.py") ||
2869
- basename === "test_effort_level.py" ||
2870
- basename === "test_permission_mode.py" ||
2871
- basename === "test_codex_runtime.py" ||
2872
- basename === "test_codex_effort_translation.py" ||
2873
- normalized.endsWith("/services/runtime/claude.py") ||
2874
- normalized.endsWith("/services/runtime/codex.py") ||
2875
- normalized.endsWith("/services/runtime/claude.py") ||
2876
- normalized.endsWith("/services/runtime/base.py") ||
2877
- normalized.endsWith("/services/runtime/claude.py") ||
2878
- normalized.endsWith("/services/runtime/codex_approval.py") ||
2879
- normalized.endsWith("/services/runtime_session_service.py") ||
2880
- basename === "test_pending_initial_prompt_service.py" ||
2881
- basename === "queue.types.ts" ||
2882
- basename === "chat.types.ts" ||
2883
- basename.includes("permissionmodeselector") ||
2884
- basename.includes("thinkingmodeselector"))) {
2885
- return true;
2886
- }
2887
- if (providerRuntimeTask &&
2888
- /\b(provider-specific|provider\s+semantics|providers?|claude|agent|symmetry)\b/.test(normalizedTask) &&
2889
- (basename === "claude_agent.py" || normalized.endsWith("/runtime/base.py"))) {
2890
- return true;
2891
- }
2892
- return false;
2893
- }
2894
1390
  function fileCoversCandidate(file, candidate) {
2895
1391
  return file.ranges.some((range) => range.start_line <= 1 &&
2896
1392
  range.end_line >= Math.max(1, candidate.line_count));
2897
1393
  }
2898
- function compareTaskCriticalCandidates(left, right, task) {
2899
- return (taskCriticalPathRank(left.path, task) - taskCriticalPathRank(right.path, task) ||
2900
- left.byte_length - right.byte_length ||
2901
- left.root_index - right.root_index ||
2902
- left.path.localeCompare(right.path));
2903
- }
2904
- function taskCriticalPathRank(path, task) {
2905
- const normalized = path.toLowerCase();
2906
- const basename = normalized.split("/").at(-1) ?? normalized;
2907
- const normalizedTask = task.toLowerCase();
2908
- if (/\bdiscovery\b/.test(normalizedTask)) {
2909
- if (isRuntimeSettingsDiscoveryTask(task) &&
2910
- isTangentialRuntimeSettingsDiscoveryTestPath(normalized, basename)) {
2911
- return 5;
2912
- }
2913
- if (isRuntimeSettingsDiscoveryTask(task)) {
2914
- const runtimeRank = runtimeSettingsDiscoveryTaskCriticalRank(normalized, basename);
2915
- if (runtimeRank != null) {
2916
- return runtimeRank;
2917
- }
2918
- }
2919
- if (normalized.endsWith("/services/chat.py") ||
2920
- normalized.endsWith("/components/chat/chat-window/chat.tsx") ||
2921
- normalized.endsWith("/components/chat/message-input/input.tsx") ||
2922
- normalized.endsWith("/components/chat/message-input/composerruntimecontrols.tsx") ||
2923
- normalized.endsWith("/components/chat/message-input/inputsettingspopover.tsx") ||
2924
- normalized.endsWith("/pages/chatpage.tsx") ||
2925
- normalized.endsWith("/models/db_models/chat.py") ||
2926
- normalized.endsWith("/models/schemas/chat.py") ||
2927
- basename === "chatservice.ts" ||
2928
- basename === "streamservice.ts" ||
2929
- basename === "usemessageactions.ts" ||
2930
- basename === "usechatstreaming.ts" ||
2931
- basename === "landingpage.tsx" ||
2932
- basename === "chat_processor.py" ||
2933
- basename === "test_queue.py" ||
2934
- normalized.endsWith("/tests/services/test_queue.py") ||
2935
- basename === "test_chat.py" ||
2936
- basename === "test_queue_service.py" ||
2937
- basename === "test_chat_processor.py" ||
2938
- normalized.endsWith("/services/runtime/claude.py") ||
2939
- normalized.endsWith("/services/runtime/codex.py") ||
2940
- normalized.endsWith("/services/runtime/codex_stream_processor.py") ||
2941
- normalized.endsWith("/services/runtime/base.py") ||
2942
- normalized.endsWith("/services/runtime/claude.py") ||
2943
- normalized.endsWith("/services/runtime/codex_approval.py") ||
2944
- normalized.endsWith("/services/runtime_session_service.py") ||
2945
- normalized.endsWith("/models/db_models/runtime_session.py") ||
2946
- normalized.endsWith("/services/permission_manager.py") ||
2947
- normalized.endsWith("/hooks/__tests__/usechatruntimesettings.test.ts") ||
2948
- normalized.endsWith("/pages/landingpage.test.tsx") ||
2949
- normalized.endsWith("/tests/test_codex_runtime_data_migration.py") ||
2950
- basename === "chat.types.ts" ||
2951
- basename === "queue.types.ts" ||
2952
- basename.includes("permissionmodeselector") ||
2953
- basename.includes("thinkingmodeselector")) {
2954
- return 0;
2955
- }
2956
- if (basename === "test_effort_level.py" ||
2957
- basename === "test_permission_mode.py" ||
2958
- basename === "test_codex_runtime.py" ||
2959
- basename === "test_codex_effort_translation.py") {
2960
- return 1;
2961
- }
2962
- if ((basename === "claude_agent.py" || basename === "test_claude_agent.py") &&
2963
- /\b(provider-specific|provider\s+semantics|providers?|claude|agent|symmetry)\b/.test(normalizedTask)) {
2964
- return 0;
2965
- }
2966
- if (basename === "test_codex_e2e_live.py" ||
2967
- basename === "test_codex_sdk_smoke.py" ||
2968
- basename === "test_transport_absence.py" ||
2969
- basename === "test_codex_runtime_live.py" ||
2970
- basename === "test_resolver.py") {
2971
- return 5;
2972
- }
2973
- }
2974
- if (/\breview\b/.test(normalizedTask)) {
2975
- if (normalized.endsWith("/api/endpoints/permissions.py") ||
2976
- (normalized.endsWith("/core/config.py") &&
2977
- /\bconfig(?:uration)?\b/.test(normalizedTask)) ||
2978
- normalized.endsWith("/services/runtime/codex.py") ||
2979
- normalized.endsWith("/services/runtime/codex_approval.py") ||
2980
- normalized.endsWith("/services/runtime/codex_stream_processor.py") ||
2981
- normalized.endsWith("/services/runtime/resolver.py") ||
2982
- normalized.endsWith("/services/runtime_session_service.py") ||
2983
- normalized.endsWith("/services/sandbox/provider.py") ||
2984
- normalized.endsWith("/services/streaming/context_usage.py") ||
2985
- normalized.endsWith("/services/model_provider_auth.py") ||
2986
- normalized.endsWith("/services/providerauthservice.ts") ||
2987
- normalized.endsWith("/services/streamservice.ts") ||
2988
- normalized.endsWith("/hooks/usechatruntimesettings.ts") ||
2989
- normalized.endsWith("/components/chat/permission-mode-selector/permissionmodeselector.tsx") ||
2990
- normalized.endsWith("/components/chat/thinking-mode-selector/thinkingmodeselector.tsx") ||
2991
- normalized.endsWith("/components/chat/message-input/composerruntimecontrols.tsx") ||
2992
- normalized.endsWith("/api/endpoints/ai_models.py") ||
2993
- normalized.endsWith("/tests/test_ai_models_codex_gate.py") ||
2994
- normalized.endsWith("/services/streaming/orchestrator.py") ||
2995
- (normalized.endsWith("/tests/services/streaming/test_context_usage_payloads.py") &&
2996
- /\bcontext\s+usage|usage\s+payloads?\b/.test(normalizedTask)) ||
2997
- normalized.includes("/tests/services/runtime/") ||
2998
- normalized.endsWith("/tests/services/test_model_provider_auth.py") ||
2999
- normalized.endsWith("/tests/services/test_runtime_session_service.py") ||
3000
- normalized.endsWith("/api/endpoints/chat.py") ||
3001
- normalized.endsWith("/services/chat.py") ||
3002
- normalized.endsWith("/models/db_models/ai_model.py") ||
3003
- basename === "constants.py" ||
3004
- basename === "usepermissionrequest.ts" ||
3005
- basename === "usemessageactions.ts" ||
3006
- basename === "fileuploaddialog.tsx" ||
3007
- basename === "modelprovider.ts" ||
3008
- basename === "test_permissions.py" ||
3009
- basename === "permission_manager.py" ||
3010
- basename === "chatservice.ts" ||
3011
- basename === "chat.types.ts") {
3012
- return 0;
3013
- }
3014
- if (isTaskCriticalReviewDoc(normalized)) {
3015
- return 2;
3016
- }
3017
- if (normalized.includes("/admin/config.py") ||
3018
- normalized.includes("/.ai/memory-bank/")) {
3019
- return 5;
3020
- }
3021
- }
3022
- if (isDevtoolsTraceDebugTask(task)) {
3023
- if (isTangentialDevtoolsTraceDebugPath(path)) {
3024
- return 5;
3025
- }
3026
- if (normalized.endsWith("/components/right-panel/rightpaneldevtools.tsx") ||
3027
- normalized.endsWith("/components/right-panel/__tests__/right-panel-devtools.test.tsx") ||
3028
- normalized.endsWith("/components/chat/__tests__/chat.autoscroll.test.tsx") ||
3029
- normalized.endsWith("/components/right-panel/devtools-helpers.ts") ||
3030
- normalized.endsWith("/store/devtoolsnavstore.ts") ||
3031
- normalized.endsWith("/store/__tests__/devtoolsnavstore.test.ts") ||
3032
- normalized.endsWith("/components/chat/chat-window/chat.tsx") ||
3033
- normalized.endsWith("/components/chat/message-bubble/messagerenderer.tsx") ||
3034
- normalized.endsWith("/components/chat/tools/common/toolcard.tsx") ||
3035
- normalized.endsWith("/components/chat/message-bubble/message.tsx")) {
3036
- return 0;
3037
- }
3038
- if (normalized.endsWith("/hooks/queries/usedevtoolsqueries.ts") ||
3039
- normalized.endsWith("/services/devtoolsservice.ts") ||
3040
- normalized.endsWith("/hooks/queries/querykeys.ts") ||
3041
- normalized.endsWith("/utils/messagecacheorder.ts") ||
3042
- normalized.endsWith("/hooks/usemessagecache.ts") ||
3043
- normalized.endsWith("/components/chat/tools/common/toolexpandcontext.ts") ||
3044
- normalized.endsWith("/components/chat/tools/common/__tests__/toolexpand.test.tsx")) {
3045
- return 1;
3046
- }
3047
- if (normalized.endsWith("/api/endpoints/devtools.py") ||
3048
- normalized.endsWith("/services/session_trace_archive.py")) {
3049
- return 3;
3050
- }
3051
- if (normalized.endsWith("/models/db_models/session_trace_archive.py") ||
3052
- normalized.endsWith("/models/schemas/devtools.py") ||
3053
- normalized.endsWith("/services/codex_trace.py") ||
3054
- normalized.endsWith("/types/devtools.types.ts")) {
3055
- return 4;
3056
- }
3057
- if (normalized.endsWith("/pages/chatpage.tsx") ||
3058
- normalized.endsWith("/components/chat/message-input/input.tsx") ||
3059
- normalized.endsWith("/api/endpoints/chat.py") ||
3060
- normalized.endsWith("/services/claude_agent.py") ||
3061
- normalized.endsWith("/services/chat.py") ||
3062
- normalized.endsWith("/services/chatservice.ts")) {
3063
- return 4;
3064
- }
3065
- }
3066
- if (basename === "permission_mode.py" ||
3067
- basename === "effort.py" ||
3068
- basename === "permissions.py" ||
3069
- basename === "constants.py" ||
3070
- basename === "config.py" ||
3071
- basename === "querykeys.ts" ||
3072
- basename.endsWith(".types.ts")) {
3073
- return 0;
3074
- }
3075
- if (basename.endsWith(".test.tsx") || basename.endsWith(".test.ts")) {
3076
- return 2;
3077
- }
3078
- return 1;
3079
- }
3080
- function runtimeSettingsDiscoveryTaskCriticalRank(normalizedPath, basename) {
3081
- if (normalizedPath.endsWith("/hooks/usechatruntimesettings.ts")) {
3082
- return 0;
3083
- }
3084
- if (normalizedPath.endsWith("/components/chat/message-input/composerruntimecontrols.tsx") ||
3085
- normalizedPath.endsWith("/components/chat/message-input/input.tsx") ||
3086
- normalizedPath.endsWith("/pages/chatpage.tsx")) {
3087
- return 0;
3088
- }
3089
- if (normalizedPath.endsWith("/services/chatservice.ts") ||
3090
- normalizedPath.endsWith("/api/endpoints/chat.py") ||
3091
- normalizedPath.endsWith("/services/chat.py")) {
3092
- return 0;
3093
- }
3094
- if (normalizedPath.endsWith("/store/messagequeuestore.ts") ||
3095
- normalizedPath.endsWith("/store/__tests__/messagequeuestore.test.ts") ||
3096
- normalizedPath.endsWith("/services/queueservice.ts") ||
3097
- normalizedPath.endsWith("/tests/services/test_queue.py") ||
3098
- normalizedPath.endsWith("/tests/test_queue.py") ||
3099
- normalizedPath.endsWith("/tests/services/test_pending_initial_prompt_service.py")) {
3100
- return 0;
3101
- }
3102
- if (normalizedPath.endsWith("/services/runtime/codex.py") ||
3103
- normalizedPath.endsWith("/services/runtime/claude.py") ||
3104
- normalizedPath.endsWith("/services/runtime/codex_approval.py") ||
3105
- normalizedPath.endsWith("/services/runtime/codex_stream_processor.py") ||
3106
- normalizedPath.endsWith("/services/runtime_session_service.py") ||
3107
- normalizedPath.endsWith("/services/streaming/orchestrator.py")) {
3108
- return 0;
3109
- }
3110
- if (normalizedPath.endsWith("/hooks/__tests__/usechatruntimesettings.test.ts") ||
3111
- normalizedPath.endsWith("/pages/landingpage.test.tsx") ||
3112
- normalizedPath.endsWith("/tests/services/runtime/test_codex_runtime.py") ||
3113
- normalizedPath.endsWith("/tests/test_codex_runtime_data_migration.py") ||
3114
- normalizedPath.endsWith("/tests/test_codex_effort_translation.py") ||
3115
- normalizedPath.endsWith("/tests/test_effort_level.py") ||
3116
- normalizedPath.endsWith("/tests/test_permission_mode.py") ||
3117
- normalizedPath.endsWith("/tests/services/test_claude_agent.py")) {
3118
- return 1;
3119
- }
3120
- if (basename === "effort.py" ||
3121
- basename === "permission_mode.py" ||
3122
- basename === "chat.types.ts" ||
3123
- basename === "queue.types.ts" ||
3124
- normalizedPath.endsWith("/models/schemas/chat.py") ||
3125
- normalizedPath.endsWith("/models/db_models/chat.py") ||
3126
- normalizedPath.endsWith("/models/db_models/runtime_session.py") ||
3127
- basename.includes("permissionmodeselector") ||
3128
- basename.includes("thinkingmodeselector")) {
3129
- return 2;
3130
- }
3131
- return null;
3132
- }
3133
- function isTaskCriticalReviewDoc(normalizedPath) {
3134
- if (!normalizedPath.endsWith(".md")) {
3135
- return false;
3136
- }
3137
- if (normalizedPath.startsWith(".ai/memory-bank/") || normalizedPath.includes("/.ai/memory-bank/")) {
3138
- return false;
3139
- }
3140
- if (normalizedPath.startsWith(".ai/") || normalizedPath.includes("/.ai/")) {
3141
- return false;
3142
- }
3143
- if (normalizedPath.endsWith("docs/ops/nano-permissions-policy.md")) {
3144
- return false;
3145
- }
3146
- return (/(^|\/)docs?\/.*(codex|approval|permission|session|runtime).*\.md$/.test(normalizedPath));
3147
- }
3148
1394
  function handleDelegateRuntimeFailure(input) {
3149
- const failureReason = sanitizeDiagnosticText(input.reason);
1395
+ // Delegate-failure reasons aggregate many per-entry details (each already
1396
+ // individually sanitized/redacted where assembled); the wider marked-
1397
+ // truncation bound keeps the tail from being silently dropped.
1398
+ const failureReason = sanitizeDelegateFailureText(input.reason);
3150
1399
  if (input.delegate.failure_policy === "fail") {
3151
- throw new ExecutionError(`context_builder delegate failed: ${failureReason}`);
1400
+ const error = new ExecutionError(`context_builder delegate failed: ${failureReason}`);
1401
+ if (input.failureKind === "timeout") {
1402
+ error.failureClass = "transient";
1403
+ }
1404
+ throw error;
3152
1405
  }
3153
1406
  const warnings = [
3154
1407
  delegateWarning("delegate_fallback", `Delegate curation failed; deterministic selection was used. ${failureReason}`),
@@ -3170,6 +1423,11 @@ function handleDelegateRuntimeFailure(input) {
3170
1423
  omission_count: input.omissionCount,
3171
1424
  failure_reason: failureReason,
3172
1425
  },
1426
+ observability: input.observability ?? {
1427
+ curator_ms: null,
1428
+ finalizer_ms: null,
1429
+ delegate_packet_size: 0,
1430
+ },
3173
1431
  };
3174
1432
  }
3175
1433
  function formatOmissionForDelegateFailure(omission) {
@@ -3178,6 +1436,34 @@ function formatOmissionForDelegateFailure(omission) {
3178
1436
  : boundContextBuilderPathList(omission.paths).join(", ");
3179
1437
  return `${omission.reason} (${paths}): ${sanitizeDiagnosticText(omission.message)}`;
3180
1438
  }
1439
+ // Bounds for the aggregated delegate-failure omission list. Entries past the
1440
+ // cap are dropped with an explicit `(+N more omissions)` marker and the total
1441
+ // count always leads the message, so truncation is never silent.
1442
+ const DELEGATE_FAILURE_MAX_OMISSION_ENTRIES = 6;
1443
+ const DELEGATE_FAILURE_MAX_ENTRY_TEXT_LENGTH = 240;
1444
+ const DELEGATE_FAILURE_MAX_ENTRY_PATHS = 5;
1445
+ function formatOmissionsForDelegateFailure(omissions) {
1446
+ const total = omissions.length;
1447
+ const shown = omissions
1448
+ .slice(0, DELEGATE_FAILURE_MAX_OMISSION_ENTRIES)
1449
+ .map(formatOmissionEntryForDelegateFailure);
1450
+ const droppedSuffix = total > shown.length ? ` (+${total - shown.length} more omissions)` : "";
1451
+ return `${total} omission${total === 1 ? "" : "s"}: ${shown.join("; ")}${droppedSuffix}`;
1452
+ }
1453
+ function formatOmissionEntryForDelegateFailure(omission) {
1454
+ // Redaction happens per entry, BEFORE assembly into the aggregate message:
1455
+ // paths through boundContextBuilderPathList (secret-path redaction) and free
1456
+ // text through the marked bounded sanitizer, so no entry can smuggle secret
1457
+ // material into the joined reason and no entry ends in an unmarked "...".
1458
+ const allPaths = omission.paths ?? [];
1459
+ const shownPaths = boundContextBuilderPathList(allPaths, DELEGATE_FAILURE_MAX_ENTRY_PATHS);
1460
+ const pathText = shownPaths.length === 0
1461
+ ? "unknown focused path"
1462
+ : allPaths.length > shownPaths.length
1463
+ ? `${shownPaths.join(", ")} (+${allPaths.length - shownPaths.length} more paths)`
1464
+ : shownPaths.join(", ");
1465
+ return `${sanitizeDelegateFailureText(omission.reason, DELEGATE_FAILURE_MAX_ENTRY_TEXT_LENGTH)} (${pathText}): ${sanitizeDelegateFailureText(omission.message, DELEGATE_FAILURE_MAX_ENTRY_TEXT_LENGTH)}`;
1466
+ }
3181
1467
  function rangeCoveredByRanges(target, ranges) {
3182
1468
  let nextLine = target.start_line;
3183
1469
  for (const range of [...ranges].sort((left, right) => left.start_line - right.start_line || left.end_line - right.end_line)) {
@@ -3239,6 +1525,9 @@ function delegateCompletenessFailure(input) {
3239
1525
  if (materialWarnings.length > 0) {
3240
1526
  return `delegate warnings reported material evidence gaps: ${materialWarnings.join("; ")}`;
3241
1527
  }
1528
+ return requiredFocusCoverageFailure(input);
1529
+ }
1530
+ function requiredFocusCoverageFailure(input) {
3242
1531
  const required = requiredFocusEvidenceSelections(input);
3243
1532
  if (required.length === 0) {
3244
1533
  return undefined;
@@ -3263,6 +1552,53 @@ function delegateCompletenessFailure(input) {
3263
1552
  .join(",")}`)
3264
1553
  .join("; ")}`;
3265
1554
  }
1555
+ // Curator-only post-repair guard. Unlike the finalizer branch, the curator-only
1556
+ // path does not fold required-focus ranges, so its baseline is the curator's
1557
+ // own pre-repair selection. Repair is allowed to:
1558
+ // - COMPACT a required-focus file's ranges to fit budget pressure (the file
1559
+ // stays in the bundle, just sliced — the documented focus-slicing behavior); and
1560
+ // - DROP a degenerate (whitespace-only) selected slice entirely (server-dropped).
1561
+ // The HIGH failure mode this guards is repair evicting a NON-degenerate
1562
+ // required-focus candidate ENTIRELY: a focus file with real content that the
1563
+ // curator selected and that survived reconstruction must not silently vanish
1564
+ // from the post-repair selection. Whole-file eviction of non-empty required
1565
+ // focus fails closed.
1566
+ function postRepairRequiredFocusEvictionFailure(input) {
1567
+ // Root-aware required-focus key set: a multi-root run can carry the same
1568
+ // relative path under two roots, only one of which is required focus. Keying
1569
+ // the required set by bare path would misclassify the non-required twin.
1570
+ const requiredKeys = new Set(input.candidates
1571
+ .filter((candidate) => candidate.focus_requested === true &&
1572
+ (candidate.required_focus_ranges?.length ?? 0) > 0 &&
1573
+ candidateMatchesFocusPath(candidate, input.focusPaths))
1574
+ .map((candidate) => `${candidate.root_index}:${candidate.path}`));
1575
+ if (requiredKeys.size === 0) {
1576
+ return undefined;
1577
+ }
1578
+ const selectedFileKey = (file) => `${file.root_index}:${file.path}`;
1579
+ // Pre-repair required-focus files that carried real (non-whitespace) content.
1580
+ // A whitespace-only focus slice is legitimately server-dropped by repair, so
1581
+ // exempt it — only its eviction is a no-op, never a hard failure.
1582
+ const preRepairRequired = new Map();
1583
+ for (const file of input.preRepairSelected) {
1584
+ if (requiredKeys.has(selectedFileKey(file)) &&
1585
+ file.ranges.length > 0 &&
1586
+ file.content.trim().length > 0) {
1587
+ preRepairRequired.set(selectedFileKey(file), file);
1588
+ }
1589
+ }
1590
+ const postRepairKeys = new Set(input.postRepairSelected
1591
+ .filter((file) => file.ranges.length > 0)
1592
+ .map(selectedFileKey));
1593
+ const evicted = [...preRepairRequired.values()]
1594
+ .filter((file) => !postRepairKeys.has(selectedFileKey(file)))
1595
+ .map((file) => `root ${file.root_index}: ${file.path}`);
1596
+ if (evicted.length === 0) {
1597
+ return undefined;
1598
+ }
1599
+ return `repair evicted required-focus evidence the curator selection carried: ${evicted.join("; ")}`;
1600
+ }
1601
+ export const postRepairRequiredFocusEvictionFailureForTest = postRepairRequiredFocusEvictionFailure;
3266
1602
  function warningReferencesSelectedDelegateCandidate(input) {
3267
1603
  const selectedCandidateIds = new Set(input.selected.map((selection) => selection.candidate_id));
3268
1604
  const selectedFiles = input.candidates
@@ -3281,6 +1617,37 @@ function warningReferencesSelectedDelegateCandidate(input) {
3281
1617
  }));
3282
1618
  return isStaleSelectedContextOmissionWarning(input.warning.toLowerCase(), selectedFiles);
3283
1619
  }
1620
+ /**
1621
+ * Re-express a materialized (post-budget reconstruction) selection as
1622
+ * candidate-id-keyed proposal selections so completeness can be evaluated
1623
+ * against what actually survived the local reconstruction budget — not what
1624
+ * the finalizer merely requested. A candidate dropped by the budget guard is
1625
+ * absent here, so any required-focus range it carried reads as uncovered and
1626
+ * fails closed in delegateCompletenessFailure.
1627
+ */
1628
+ function materializeFinalizerSelection(selected, candidates) {
1629
+ const candidateIdByKey = new Map();
1630
+ for (const candidate of candidates) {
1631
+ candidateIdByKey.set(`${candidate.root_index}:${candidate.path}`, candidate.id);
1632
+ }
1633
+ const materialized = [];
1634
+ for (const file of selected) {
1635
+ const candidateId = candidateIdByKey.get(`${file.root_index}:${file.path}`);
1636
+ if (candidateId == null) {
1637
+ continue;
1638
+ }
1639
+ materialized.push({
1640
+ candidate_id: candidateId,
1641
+ reason: "materialized post-budget selection",
1642
+ ranges: file.ranges.map((range) => ({
1643
+ start_line: range.start_line,
1644
+ end_line: range.end_line,
1645
+ reason: "materialized post-budget selection range",
1646
+ })),
1647
+ });
1648
+ }
1649
+ return materialized;
1650
+ }
3284
1651
  function requiredFocusEvidenceSelections(input) {
3285
1652
  if (input.focusPaths.length === 0) {
3286
1653
  return [];
@@ -3455,10 +1822,139 @@ function augmentCuratorProposalWithDeterministicBaseline(proposal, packet, maxSe
3455
1822
  function totalSelectedRangeLines(ranges) {
3456
1823
  return ranges.reduce((total, range) => total + Math.max(0, range.end_line - range.start_line + 1), 0);
3457
1824
  }
3458
- export function renderDelegatePrompt(stage, packet) {
1825
+ /**
1826
+ * Renders the server-enforced coverage obligations as explicit prompt rules.
1827
+ *
1828
+ * The obligations are already present in the serialized packet, but the packet
1829
+ * alone never told the delegate they were MANDATORY, so a schema-valid proposal
1830
+ * could miss one and be rejected by
1831
+ * {@link validateContextBuilderDelegateCoverageSelection}.
1832
+ *
1833
+ * Branch selection here tracks that validator's branch selection: it enters the
1834
+ * obligation branch on `obligations != null`, NOT on a non-empty target list.
1835
+ * Branching on `targets.length > 0` instead would render the satisfiable
1836
+ * evidence-class contract for a packet the validator rejects outright, i.e.
1837
+ * instruct the delegate to do something that provably cannot pass.
1838
+ */
1839
+ function packetCoverageObligationRules(packet) {
1840
+ if (packet == null || typeof packet !== "object") {
1841
+ return [];
1842
+ }
1843
+ const policy = packet["coverage_policy"];
1844
+ if (policy == null || typeof policy !== "object") {
1845
+ return [];
1846
+ }
1847
+ const policyRecord = policy;
1848
+ if (policyRecord["policy_id"] !== "review_deep_v1") {
1849
+ return [];
1850
+ }
1851
+ const requiredClasses = policyRecord["required_when_available"];
1852
+ if (!Array.isArray(requiredClasses) || requiredClasses.length === 0) {
1853
+ return [];
1854
+ }
1855
+ const obligations = policyRecord["obligations"];
1856
+ if (obligations != null && typeof obligations === "object") {
1857
+ const targets = obligations["targets"];
1858
+ if (!Array.isArray(targets) || targets.length === 0) {
1859
+ // The validator rejects a present-but-empty target list unconditionally,
1860
+ // so no instruction rendered here could be satisfied. Stay silent rather
1861
+ // than describe a contract the delegate cannot meet. (The server building
1862
+ // a packet it will always reject is a separate defect; do not paper over
1863
+ // it by rendering the evidence-class rules the validator will not apply.)
1864
+ return [];
1865
+ }
1866
+ return [
1867
+ "Coverage obligations (mandatory, server-enforced):",
1868
+ "- packet.coverage_policy.obligations.targets is required coverage, not a suggestion. Server validation rejects your output if any obligation below is unmet, even when every other rule is satisfied.",
1869
+ "- For EVERY target: selected MUST contain that target's candidate_id with at least one range that overlaps one of that target's ranges.",
1870
+ "- For EVERY target whose regression_links is non-empty: selected MUST also contain at least ONE of those links' candidate_id with an overlapping range. One linked candidate per target is sufficient; you do not need all of them.",
1871
+ "- Obligations hold even when the task reads narrower than the obligation set. Add the obligation candidates alongside your task-driven picks rather than dropping them.",
1872
+ ];
1873
+ }
1874
+ const classes = requiredClasses.filter((entry) => typeof entry === "string");
1875
+ if (classes.length === 0) {
1876
+ return [];
1877
+ }
1878
+ return [
1879
+ "Coverage obligations (mandatory, server-enforced):",
1880
+ `- packet.coverage_policy.required_when_available lists required evidence classes: ${classes.join(", ")}.`,
1881
+ "- For every listed class that ANY packet candidate carries in its evidence_classes, selected MUST include at least one candidate carrying that class. Server validation rejects your output otherwise.",
1882
+ ];
1883
+ }
1884
+ /**
1885
+ * Renders the finalizer scoring contract.
1886
+ *
1887
+ * `min_score` is a server-side gate
1888
+ * ({@link validateContextBuilderFinalizerOutput}); before this it was never
1889
+ * sent to the model, so the finalizer was asked to clear a bar it could not
1890
+ * see, on a scale that was never defined. The rubric below is deliberately
1891
+ * anchored so that an approving score lands at or above the threshold, and it
1892
+ * names the dominant real-world failure mode: penalizing the packet for
1893
+ * evidence the packet was never meant to contain.
1894
+ */
1895
+ function finalizerScoringRules(minScore) {
1896
+ if (minScore == null) {
1897
+ return [];
1898
+ }
1899
+ // A threshold of 0 cannot reject anything (the gate is `score < minScore`), so
1900
+ // the usual "below X, do not approve" band would render a range no score can
1901
+ // occupy and instruct the delegate to "honestly reach 0". State the degenerate
1902
+ // case plainly instead of rendering an impossible rule.
1903
+ const scoreBands = minScore <= 0
1904
+ ? [
1905
+ `- score does not gate this request (any score clears a minimum of ${minScore}); express insufficiency through approved:false and concrete omissions, never through a low score.`,
1906
+ ]
1907
+ : [
1908
+ "- score rates ONLY whether the `selected` you are returning is sufficient, correct local evidence for packet.task, judged against the evidence actually present in this packet:",
1909
+ // Bands are stated relative to the live threshold rather than as fixed
1910
+ // ranges. Fixed bands (9-10 / 6-8 / 0-5) left the shipped 8.5 default
1911
+ // in an undefined gap, and at a lower configured threshold they
1912
+ // labelled the passing score as deficient — telling the delegate to
1913
+ // approve while self-reporting a score whose own band calls the
1914
+ // selection inadequate.
1915
+ ` - ${minScore}-10: selected is sufficient for the task; no packet evidence the task requires was left out.`,
1916
+ ` - below ${minScore}: selected is wrong, or omits locally-available packet evidence the task needs. Do not approve.`,
1917
+ `- If you would approve, return a score at or above ${minScore}. If you cannot honestly reach ${minScore}, set approved:false and list concrete omissions rather than approving with a failing score.`,
1918
+ ];
1919
+ return [
1920
+ "Scoring (server-enforced):",
1921
+ `- score >= ${minScore} clears the score gate; below ${minScore} fails the whole request regardless of everything else. Clearing the score gate is necessary but NOT sufficient — approved, omissions, warnings, selections, coverage, and budget are each validated separately.`,
1922
+ ...scoreBands,
1923
+ "- Do NOT lower score for evidence that is absent from the packet, outside the bounded task, or that you merely suspect exists elsewhere in the repository. The packet is intentionally bounded and is the only source of truth for this call.",
1924
+ // The server scans warnings with a keyword-proximity matcher
1925
+ // (isMaterialEvidenceGapWarning) that has no negation handling, so an
1926
+ // affirmative all-clear is rejected exactly like a real gap report. Name the
1927
+ // exact fatal phrasings; a partial description reads as authoritative and is
1928
+ // worse than none.
1929
+ "- warnings carry speculative or out-of-scope notes ONLY, and the server scans them for evidence-gap phrasing. Do not mention required, requested, or focused evidence in warnings at all — not even to state that nothing is missing — and never phrase a warning as 'outside the selected range(s)' or as 'selected ... not present/not selectable/unselectable'. That scan is a keyword-proximity match with no negation handling, so an affirmative all-clear is rejected exactly like a real gap report.",
1930
+ "- If required evidence is genuinely missing, set approved:false with concrete omissions. Never route a real coverage gap into warnings.",
1931
+ ];
1932
+ }
1933
+ /**
1934
+ * Single source for every score exemplar the finalizer prompt shows.
1935
+ *
1936
+ * The shape example, the rubric bands, and the minimum-output example must all
1937
+ * derive from the live threshold: hardcoding them let the prompt display a
1938
+ * score that its own rubric did not cover, and (at `min_score` above the
1939
+ * hardcoded value — 9.25 is schema-legal) an example that would fail the very
1940
+ * gate it was illustrating.
1941
+ */
1942
+ function finalizerExampleScore(minScore) {
1943
+ // A non-gating threshold gets the neutral fallback rather than its own value:
1944
+ // "emit the lowest score that still clears the gate" is the right anti-inflation
1945
+ // rule wherever a gate exists, but at 0 it would render `{"score":0,"approved":true}`
1946
+ // and an unsure finalizer copying that reports a 0-scored approval into
1947
+ // `finalizer_score` observability. A higher exemplar always clears a lower gate.
1948
+ return minScore == null || minScore <= 0 ? 9 : minScore;
1949
+ }
1950
+ export function renderDelegatePrompt(stage, packet, options) {
1951
+ // Diagnostic-array exemplars are non-empty on purpose: empty arrays cannot
1952
+ // communicate element types, and a live finalizer failure returned
1953
+ // object-shaped `omissions`. The angle-bracket entries are placeholders that
1954
+ // show each element is one plain string.
3459
1955
  const outputShape = stage === "curator"
3460
- ? '{"selected":[{"candidate_id":"c:0:path","ranges":[{"start_line":1,"end_line":1,"reason":"why this range"}],"reason":"why this file"}],"warnings":[],"open_questions":[]}'
3461
- : '{"score":9,"approved":true,"selected":[{"candidate_id":"c:0:path","ranges":[{"start_line":1,"end_line":1,"reason":"why this range"}],"reason":"why this file"}],"warnings":[],"omissions":[]}';
1956
+ ? '{"selected":[{"candidate_id":"c:0:path","ranges":[{"start_line":1,"end_line":1,"reason":"why this range"}],"reason":"why this file"}],"warnings":["<short plain-string caveat>"],"open_questions":["<short plain-string follow-up question>"]}'
1957
+ : `{"score":${finalizerExampleScore(options?.minScore)},"approved":true,"selected":[{"candidate_id":"c:0:path","ranges":[{"start_line":1,"end_line":1,"reason":"why this range"}],"reason":"why this file"}],"warnings":["<short plain-string caveat>"],"omissions":["<short plain string describing an omitted file or range; must be [] when approving>"]}`;
3462
1958
  const stageRules = stage === "curator"
3463
1959
  ? [
3464
1960
  "Curator-specific rules:",
@@ -3468,6 +1964,10 @@ export function renderDelegatePrompt(stage, packet) {
3468
1964
  "- Use packet.deterministic_selection.selected as the safe starting point; keep relevant deterministic candidates unless the task clearly excludes them.",
3469
1965
  "- Treat the minimum valid JSON example as a floor, not a complete packet. Add task-critical candidates from packet.candidates when deterministic_selection is sparse or omissions show important local evidence was budget-pruned.",
3470
1966
  "- Do not return selected:[] to express uncertainty. Select the closest relevant local candidate and put caveats in warnings or open_questions.",
1967
+ // The validator requires string elements (SafeTextSchema) in every
1968
+ // diagnostic array; empty exemplars alone let models invent object
1969
+ // entries, which fail schema validation.
1970
+ "- warnings and open_questions are arrays of plain strings. Each element is one short plain string; objects inside these arrays are invalid and fail server validation.",
3471
1971
  ]
3472
1972
  : [
3473
1973
  "Finalizer-specific rules:",
@@ -3478,9 +1978,19 @@ export function renderDelegatePrompt(stage, packet) {
3478
1978
  "- Treat packet.deterministic_selection.selected as server-ranked required baseline evidence. If the curator underselected or warned about sparse evidence, add relevant deterministic candidates/ranges that are present in packet.candidates instead of lowering the score for evidence the packet already provides.",
3479
1979
  "- Do not reject solely because the curator warning says broader implementation may exist when deterministic_selection and packet.candidates contain concrete task-critical ranges for that concern.",
3480
1980
  "- If the selected packet evidence is insufficient for the bounded task, set approved:false and explain concrete omissions; the server will treat that as delegate failure according to failure_policy.",
3481
- "- If approving with score at or above the requested minimum, omissions MUST be []. Put speculative out-of-packet concerns or 'may contain more detail' caveats in warnings instead.",
1981
+ // "'may contain more detail' caveats" invited exactly the phrasing
1982
+ // isMaterialEvidenceGapWarning rejects outright
1983
+ // (/\boutside (?:the )?selected ranges?\b/), so obeying this rule got
1984
+ // the finalizer hard-failed. Steer the caveat at evidence absent from
1985
+ // the packet instead of at the selected ranges.
1986
+ "- If approving with score at or above the requested minimum, omissions MUST be []. Put speculative concerns about evidence absent from this packet in warnings instead.",
1987
+ // The observed live schema failure was object-shaped `omissions`;
1988
+ // state the element type explicitly instead of relying on exemplars.
1989
+ // (No open_questions here: the finalizer schema is strict and would
1990
+ // reject that key as unrecognized.)
1991
+ "- warnings and omissions are arrays of plain strings. Each element is one short plain string; objects inside these arrays are invalid and fail server validation.",
3482
1992
  ];
3483
- const minimumOutput = buildMinimumDelegateOutput(stage, packet);
1993
+ const minimumOutput = buildMinimumDelegateOutput(stage, packet, options?.minScore);
3484
1994
  return [
3485
1995
  `You are the context_builder ${stage}.`,
3486
1996
  `Prompt version: ${CONTEXT_BUILDER_DELEGATE_PROMPT_VERSION}.`,
@@ -3495,6 +2005,8 @@ export function renderDelegatePrompt(stage, packet) {
3495
2005
  ? "Use the excerpt, search_hits, deterministic_selection, omissions, and task fields as evidence; the packet intentionally omits full source content."
3496
2006
  : "Use the excerpt, search_hits, curator_proposal, deterministic_selection, omissions, and task fields as evidence; the packet intentionally omits full source content.",
3497
2007
  ...stageRules,
2008
+ ...(stage === "finalizer" ? finalizerScoringRules(options?.minScore) : []),
2009
+ ...packetCoverageObligationRules(packet),
3498
2010
  ...(minimumOutput == null
3499
2011
  ? []
3500
2012
  : [
@@ -3510,11 +2022,153 @@ export function renderDelegatePrompt(stage, packet) {
3510
2022
  "Return the JSON object now. If candidates exist, selected must be non-empty.",
3511
2023
  ].join("\n");
3512
2024
  }
3513
- function buildMinimumDelegateOutput(stage, packet) {
3514
- const minimumSelections = packetMinimumSelections(stage, packet);
2025
+ /**
2026
+ * True when `selections` would clear the packet's coverage obligations.
2027
+ *
2028
+ * Mirrors {@link validateContextBuilderDelegateCoverageSelection}: every target
2029
+ * needs an overlapping selected range, and a target with regression links needs
2030
+ * an overlapping range from at least ONE of them (the link check is disjunctive).
2031
+ */
2032
+ function selectionsSatisfyPacketObligations(selections, packet) {
2033
+ if (packet == null || typeof packet !== "object") {
2034
+ return true;
2035
+ }
2036
+ const policy = packet["coverage_policy"];
2037
+ if (policy == null || typeof policy !== "object") {
2038
+ return true;
2039
+ }
2040
+ const policyRecord = policy;
2041
+ if (policyRecord["policy_id"] !== "review_deep_v1") {
2042
+ return true;
2043
+ }
2044
+ const requiredClasses = policyRecord["required_when_available"];
2045
+ if (!Array.isArray(requiredClasses) || requiredClasses.length === 0) {
2046
+ return true;
2047
+ }
2048
+ const obligations = policyRecord["obligations"];
2049
+ if (obligations == null || typeof obligations !== "object") {
2050
+ // The validator does NOT concede this branch: it falls through to the
2051
+ // evidence-class check and still rejects a selection that omits an
2052
+ // available required class. Returning true here would advertise an example
2053
+ // the validator rejects. Unreachable through the current packet builder
2054
+ // (normalizeCoveragePolicy attaches obligations for every review_deep_v1
2055
+ // policy), but the two must not disagree — that divergence is the bug
2056
+ // generator this whole change exists to close.
2057
+ const candidates = packet["candidates"];
2058
+ if (!Array.isArray(candidates)) {
2059
+ return true;
2060
+ }
2061
+ const selectedIds = new Set(selections.map((selection) => selection.candidate_id));
2062
+ const evidenceClassesOf = (candidate) => {
2063
+ if (candidate == null || typeof candidate !== "object") {
2064
+ return [];
2065
+ }
2066
+ const classes = candidate["evidence_classes"];
2067
+ return Array.isArray(classes) ? classes : [];
2068
+ };
2069
+ return requiredClasses.every((evidenceClass) => {
2070
+ const available = candidates.some((candidate) => evidenceClassesOf(candidate).includes(evidenceClass));
2071
+ if (!available) {
2072
+ return true;
2073
+ }
2074
+ return candidates.some((candidate) => {
2075
+ const id = candidate != null && typeof candidate === "object"
2076
+ ? candidate["id"]
2077
+ : undefined;
2078
+ return (typeof id === "string" &&
2079
+ selectedIds.has(id) &&
2080
+ evidenceClassesOf(candidate).includes(evidenceClass));
2081
+ });
2082
+ });
2083
+ }
2084
+ const targets = obligations["targets"];
2085
+ if (!Array.isArray(targets)) {
2086
+ return true;
2087
+ }
2088
+ if (targets.length === 0) {
2089
+ // The validator rejects a present-but-empty target list unconditionally, so
2090
+ // no example can be valid.
2091
+ return false;
2092
+ }
2093
+ const selectedById = new Map(selections.map((selection) => [selection.candidate_id, selection.ranges]));
2094
+ const covers = (value) => {
2095
+ if (value == null || typeof value !== "object") {
2096
+ return false;
2097
+ }
2098
+ const record = value;
2099
+ const candidateId = record["candidate_id"];
2100
+ if (typeof candidateId !== "string") {
2101
+ return false;
2102
+ }
2103
+ const selectedRanges = selectedById.get(candidateId);
2104
+ if (selectedRanges == null || selectedRanges.length === 0) {
2105
+ return false;
2106
+ }
2107
+ const obligationRanges = record["ranges"];
2108
+ if (!Array.isArray(obligationRanges) || obligationRanges.length === 0) {
2109
+ return false;
2110
+ }
2111
+ return obligationRanges.some((range) => {
2112
+ if (range == null || typeof range !== "object") {
2113
+ return false;
2114
+ }
2115
+ const start = range["start_line"];
2116
+ const end = range["end_line"];
2117
+ if (typeof start !== "number" || typeof end !== "number") {
2118
+ return false;
2119
+ }
2120
+ return selectedRanges.some((selectedRange) => selectedRange.start_line <= end && start <= selectedRange.end_line);
2121
+ });
2122
+ };
2123
+ return targets.every((target) => {
2124
+ if (!covers(target)) {
2125
+ return false;
2126
+ }
2127
+ const links = target["regression_links"];
2128
+ if (!Array.isArray(links) || links.length === 0) {
2129
+ return true;
2130
+ }
2131
+ return links.some((link) => covers(link));
2132
+ });
2133
+ }
2134
+ /** The packet's `budget.max_files`, when it carries a usable one. */
2135
+ function packetBudgetMaxFiles(packet) {
2136
+ if (packet == null || typeof packet !== "object") {
2137
+ return null;
2138
+ }
2139
+ const budget = packet["budget"];
2140
+ if (budget == null || typeof budget !== "object") {
2141
+ return null;
2142
+ }
2143
+ const maxFiles = budget["max_files"];
2144
+ return typeof maxFiles === "number" && Number.isFinite(maxFiles) && maxFiles > 0
2145
+ ? maxFiles
2146
+ : null;
2147
+ }
2148
+ function buildMinimumDelegateOutput(stage, packet, minScore) {
2149
+ const allSelections = packetMinimumSelections(stage, packet);
2150
+ if (allSelections.length === 0) {
2151
+ return null;
2152
+ }
2153
+ // The example is advertised as "minimum valid" and a delegate may copy it
2154
+ // verbatim, so it must satisfy every gate the copy would face.
2155
+ //
2156
+ // packetMinimumSelections bounds by MAX_DELEGATE_MINIMUM_OUTPUT_SELECTIONS,
2157
+ // which is unrelated to the caller's budget: validateBudget rejects a
2158
+ // selection longer than budget.max_files, so an unbounded example was invalid
2159
+ // whenever max_files was the smaller limit. Apply the budget first, then judge
2160
+ // coverage on exactly what is advertised.
2161
+ const maxFiles = packetBudgetMaxFiles(packet);
2162
+ const minimumSelections = maxFiles == null ? allSelections : allSelections.slice(0, maxFiles);
3515
2163
  if (minimumSelections.length === 0) {
3516
2164
  return null;
3517
2165
  }
2166
+ // Neither bound ranks obligations first, so a required target can fall outside
2167
+ // the cap — offering an example that fails coverage validation. Withhold the
2168
+ // example rather than advertise an invalid one.
2169
+ if (!selectionsSatisfyPacketObligations(minimumSelections, packet)) {
2170
+ return null;
2171
+ }
3518
2172
  const selected = minimumSelections.map((selection) => ({
3519
2173
  candidate_id: selection.candidate_id,
3520
2174
  ranges: selection.ranges.map((range) => ({
@@ -3533,7 +2187,13 @@ function buildMinimumDelegateOutput(stage, packet) {
3533
2187
  open_questions: [],
3534
2188
  }
3535
2189
  : {
3536
- score: 10,
2190
+ // This example is offered to an UNSURE finalizer, and it carries a
2191
+ // deliberately minimal selection. Stamping it `score: 10` would pair
2192
+ // the top of the scale with the floor of the evidence and teach
2193
+ // "when uncertain, emit 10" — inflating the very self-score the gate
2194
+ // relies on. Emit the lowest score that still clears the gate, so the
2195
+ // minimum selection reads as the minimum passing answer.
2196
+ score: finalizerExampleScore(minScore),
3537
2197
  approved: true,
3538
2198
  selected,
3539
2199
  warnings: [
@@ -3591,7 +2251,18 @@ function mergePacketSelections(selections) {
3591
2251
  const merged = new Map();
3592
2252
  for (const selection of selections) {
3593
2253
  const ranges = merged.get(selection.candidate_id) ?? [];
3594
- ranges.push(...selection.ranges);
2254
+ // Deduplicate by bounds. Sources overlap by construction — a candidate in
2255
+ // deterministic_selection is normally also present in packet.candidates —
2256
+ // so concatenating produced the SAME range twice, and validateSelections
2257
+ // rejects a duplicate range outright. That made the advertised "minimum
2258
+ // valid JSON" example invalid in the ordinary case: a delegate copying it
2259
+ // was hard-failed by the server that offered it.
2260
+ for (const range of selection.ranges) {
2261
+ if (!ranges.some((existing) => existing.start_line === range.start_line &&
2262
+ existing.end_line === range.end_line)) {
2263
+ ranges.push(range);
2264
+ }
2265
+ }
3595
2266
  merged.set(selection.candidate_id, ranges);
3596
2267
  }
3597
2268
  return [...merged].map(([candidate_id, ranges]) => ({
@@ -3645,25 +2316,52 @@ function delegatePromptDiagnostic(packet) {
3645
2316
  const deterministicCount = Array.isArray(deterministicSelected)
3646
2317
  ? deterministicSelected.length
3647
2318
  : 0;
2319
+ // `curator_proposal` only exists on a finalizer packet (curator packets are
2320
+ // built before any proposal exists). Reporting `curator_selected=0` for a
2321
+ // curator packet described the packet's missing field, not the curator's
2322
+ // output, so every curator-stage diagnostic claimed an empty selection that
2323
+ // had in fact never been observed. Omit the field unless the packet carries a
2324
+ // proposal to count.
3648
2325
  const proposal = record["curator_proposal"];
3649
2326
  const proposalSelected = proposal != null && typeof proposal === "object"
3650
2327
  ? proposal["selected"]
3651
2328
  : undefined;
3652
- const curatorProposalCount = Array.isArray(proposalSelected)
3653
- ? proposalSelected.length
3654
- : 0;
3655
- const minimumSelection = packetMinimumSelections("curator", packet);
2329
+ // `min` must report what the prompt actually carried, not whether a minimum
2330
+ // selection could be computed: buildMinimumDelegateOutput also withholds the
2331
+ // example when it would fail budget or coverage, and a withheld floor example
2332
+ // is exactly the condition worth seeing next to a delegate failure. Derive it
2333
+ // from the same call the prompt makes, at the stage this packet belongs to.
2334
+ const stage = Array.isArray(proposalSelected)
2335
+ ? "finalizer"
2336
+ : "curator";
3656
2337
  return [
3657
2338
  `pv=${CONTEXT_BUILDER_DELEGATE_PROMPT_VERSION}`,
3658
2339
  `candidates=${candidateCount}`,
3659
2340
  `det=${deterministicCount}`,
3660
- `curator_selected=${curatorProposalCount}`,
3661
- `min=${minimumSelection.length > 0}`,
2341
+ ...(Array.isArray(proposalSelected)
2342
+ ? [`curator_selected=${proposalSelected.length}`]
2343
+ : []),
2344
+ `min=${buildMinimumDelegateOutput(stage, packet) != null}`,
3662
2345
  ].join("; ");
3663
2346
  }
3664
2347
  function failureReasonFromCall(stage, call) {
3665
2348
  return `${stage} ${call.reason}: ${call.message}`;
3666
2349
  }
2350
+ /**
2351
+ * Safe-scalar score context for finalizer rejections. A not-approved rejection
2352
+ * previously carried no score at all; surfacing the reported score next to the
2353
+ * configured min_score makes rejection and under-score failures diagnosable
2354
+ * from the failure reason alone. Only a finite numeric `score` read from the
2355
+ * finalizer's raw JSON is ever included — never free-form provider text.
2356
+ */
2357
+ function finalizerScoreDiagnostic(json, minScore) {
2358
+ const rawScore = json != null && typeof json === "object"
2359
+ ? json["score"]
2360
+ : undefined;
2361
+ return typeof rawScore === "number" && Number.isFinite(rawScore)
2362
+ ? `; finalizer_score=${rawScore}; min_score=${minScore}`
2363
+ : `; min_score=${minScore}`;
2364
+ }
3667
2365
  function failureReasonFromError(stage, err, diagnostic) {
3668
2366
  const message = err instanceof Error ? err.message : "unknown delegate error";
3669
2367
  return diagnostic == null
@@ -3676,6 +2374,50 @@ function delegateWarning(code, message) {
3676
2374
  message: sanitizeDiagnosticText(message),
3677
2375
  };
3678
2376
  }
2377
+ // Bounded per-term detail in the search-cap coverage warning. Full per-term
2378
+ // rows (up to the public 20-term cap) persist in manifest/summary
2379
+ // `search_coverage`; the warning message stays a compact summary.
2380
+ const MAX_SEARCH_CAP_WARNING_TERM_DETAILS = 6;
2381
+ /**
2382
+ * Builds the receipt-visible explanation for a MATERIAL search cap. Returns
2383
+ * undefined when the cap was not hit or coverage proves it non-material.
2384
+ * The warning `code` is the stable materiality reason string
2385
+ * (`search_cap_unresolved_coverage` / `search_cap_unsearched_candidates`);
2386
+ * classification itself rides on the cap event in the status reducer, so this
2387
+ * warning is explanatory and never double-counts toward `partial`.
2388
+ * Terms are referenced by `search_term_index`, never by value, so the warning
2389
+ * cannot leak caller term text; the full (sanitized) rows live in
2390
+ * `search_coverage`. The first unsearched candidate's rank/path is not
2391
+ * reported: T1's coverage counters are aggregate-only by design and the
2392
+ * caller-phase scheduler is term-major round-robin, so the searched set is not
2393
+ * an ordering prefix and no per-candidate identity is cheaply available.
2394
+ */
2395
+ function searchCapCoverageWarning(searchCapHit, coverage) {
2396
+ if (!searchCapHit) {
2397
+ return undefined;
2398
+ }
2399
+ const materiality = evaluateSearchCapMateriality(coverage);
2400
+ if (!materiality.material) {
2401
+ return undefined;
2402
+ }
2403
+ const incompleteTerms = coverage.caller_terms.filter((term) => !term.scan_complete);
2404
+ const termDetails = incompleteTerms
2405
+ .slice(0, MAX_SEARCH_CAP_WARNING_TERM_DETAILS)
2406
+ .map((term) => `#${term.search_term_index}(hits=${term.hit_count})`)
2407
+ .join(", ");
2408
+ const hiddenIncomplete = incompleteTerms.length - Math.min(incompleteTerms.length, MAX_SEARCH_CAP_WARNING_TERM_DETAILS);
2409
+ const callerSummary = coverage.caller_term_count === 0
2410
+ ? "caller terms 0"
2411
+ : `caller terms ${coverage.caller_term_count} (${incompleteTerms.length} incomplete${termDetails.length === 0 ? "" : `: ${termDetails}`}${hiddenIncomplete > 0 ? `, +${hiddenIncomplete} more` : ""}; ${coverage.caller_record_count} records)`;
2412
+ return {
2413
+ code: materiality.reason,
2414
+ message: `search cap ended discovery before coverage was proven: searched ` +
2415
+ `${coverage.searched_candidate_count}/${coverage.eligible_candidate_count} eligible candidates ` +
2416
+ `(${coverage.unsearched_candidate_count} unsearched); ${callerSummary}; ` +
2417
+ `derived terms ${coverage.derived_term_count} (${coverage.derived_record_count} records); ` +
2418
+ `full per-term coverage is in manifest.json search_coverage`,
2419
+ };
2420
+ }
3679
2421
  function reconcileContextBuilderWarnings(input) {
3680
2422
  if (input.warnings.length === 0) {
3681
2423
  return [];