clinkx 1.0.0 → 1.1.1

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 (333) hide show
  1. package/CHANGELOG.md +95 -0
  2. package/README.md +184 -112
  3. package/clinkx-workflows/dist/chairman-knobs.d.ts +24 -0
  4. package/clinkx-workflows/dist/chairman-knobs.js +27 -0
  5. package/clinkx-workflows/dist/chairman-knobs.js.map +1 -0
  6. package/clinkx-workflows/dist/clink-client.d.ts +10 -0
  7. package/clinkx-workflows/dist/clink-client.js +39 -1
  8. package/clinkx-workflows/dist/clink-client.js.map +1 -1
  9. package/clinkx-workflows/dist/config.d.ts +29 -1
  10. package/clinkx-workflows/dist/config.js +35 -2
  11. package/clinkx-workflows/dist/config.js.map +1 -1
  12. package/clinkx-workflows/dist/context-preflight.d.ts +5 -0
  13. package/clinkx-workflows/dist/context-preflight.js.map +1 -1
  14. package/clinkx-workflows/dist/council-execution-policy.d.ts +68 -0
  15. package/clinkx-workflows/dist/council-execution-policy.js +110 -0
  16. package/clinkx-workflows/dist/council-execution-policy.js.map +1 -0
  17. package/clinkx-workflows/dist/council-expander.d.ts +9 -8
  18. package/clinkx-workflows/dist/council-expander.js +18 -9
  19. package/clinkx-workflows/dist/council-expander.js.map +1 -1
  20. package/clinkx-workflows/dist/dashboard/activity-contract.d.ts +5557 -4
  21. package/clinkx-workflows/dist/dashboard/activity-contract.js +55 -3
  22. package/clinkx-workflows/dist/dashboard/activity-contract.js.map +1 -1
  23. package/clinkx-workflows/dist/dashboard/activity-reader.d.ts +9 -5
  24. package/clinkx-workflows/dist/dashboard/activity-reader.js +15 -2
  25. package/clinkx-workflows/dist/dashboard/activity-reader.js.map +1 -1
  26. package/clinkx-workflows/dist/dashboard/api-routes.d.ts +4 -0
  27. package/clinkx-workflows/dist/dashboard/api-routes.js +133 -25
  28. package/clinkx-workflows/dist/dashboard/api-routes.js.map +1 -1
  29. package/clinkx-workflows/dist/dashboard/context-builder-invocation-contract.d.ts +3865 -0
  30. package/clinkx-workflows/dist/dashboard/context-builder-invocation-contract.js +69 -0
  31. package/clinkx-workflows/dist/dashboard/context-builder-invocation-contract.js.map +1 -0
  32. package/clinkx-workflows/dist/dashboard/context-run-reader.d.ts +36 -2
  33. package/clinkx-workflows/dist/dashboard/context-run-reader.js +142 -19
  34. package/clinkx-workflows/dist/dashboard/context-run-reader.js.map +1 -1
  35. package/clinkx-workflows/dist/dashboard/contracts.d.ts +20027 -480
  36. package/clinkx-workflows/dist/dashboard/contracts.js +133 -6
  37. package/clinkx-workflows/dist/dashboard/contracts.js.map +1 -1
  38. package/clinkx-workflows/dist/dashboard/index.d.ts +4 -0
  39. package/clinkx-workflows/dist/dashboard/index.js +4 -0
  40. package/clinkx-workflows/dist/dashboard/index.js.map +1 -1
  41. package/clinkx-workflows/dist/dashboard/invocation-contract.d.ts +786 -0
  42. package/clinkx-workflows/dist/dashboard/invocation-contract.js +201 -0
  43. package/clinkx-workflows/dist/dashboard/invocation-contract.js.map +1 -0
  44. package/clinkx-workflows/dist/dashboard/lazy-content.d.ts +14 -1
  45. package/clinkx-workflows/dist/dashboard/lazy-content.js +53 -2
  46. package/clinkx-workflows/dist/dashboard/lazy-content.js.map +1 -1
  47. package/clinkx-workflows/dist/dashboard/native-transcript/contract.d.ts +90 -0
  48. package/clinkx-workflows/dist/dashboard/native-transcript/contract.js +31 -0
  49. package/clinkx-workflows/dist/dashboard/native-transcript/contract.js.map +1 -0
  50. package/clinkx-workflows/dist/dashboard/native-transcript/projection.d.ts +12 -0
  51. package/clinkx-workflows/dist/dashboard/native-transcript/projection.js +286 -0
  52. package/clinkx-workflows/dist/dashboard/native-transcript/projection.js.map +1 -0
  53. package/clinkx-workflows/dist/dashboard/progress-report-contract.d.ts +147 -4
  54. package/clinkx-workflows/dist/dashboard/progress-report-contract.js +11 -11
  55. package/clinkx-workflows/dist/dashboard/progress-report-contract.js.map +1 -1
  56. package/clinkx-workflows/dist/dashboard/progress-report-reader.js +4 -56
  57. package/clinkx-workflows/dist/dashboard/progress-report-reader.js.map +1 -1
  58. package/clinkx-workflows/dist/dashboard/progress-report-status-store.d.ts +2 -1
  59. package/clinkx-workflows/dist/dashboard/progress-report-status-store.js +6 -5
  60. package/clinkx-workflows/dist/dashboard/progress-report-status-store.js.map +1 -1
  61. package/clinkx-workflows/dist/dashboard/prompt-builder-library-contract.d.ts +194 -0
  62. package/clinkx-workflows/dist/dashboard/prompt-builder-library-contract.js +89 -0
  63. package/clinkx-workflows/dist/dashboard/prompt-builder-library-contract.js.map +1 -0
  64. package/clinkx-workflows/dist/dashboard/prompt-builder-library-store.d.ts +26 -0
  65. package/clinkx-workflows/dist/dashboard/prompt-builder-library-store.js +102 -0
  66. package/clinkx-workflows/dist/dashboard/prompt-builder-library-store.js.map +1 -0
  67. package/clinkx-workflows/dist/dashboard/request-admission.js +4 -0
  68. package/clinkx-workflows/dist/dashboard/request-admission.js.map +1 -1
  69. package/clinkx-workflows/dist/dashboard/scanner.d.ts +3 -0
  70. package/clinkx-workflows/dist/dashboard/scanner.js +52 -8
  71. package/clinkx-workflows/dist/dashboard/scanner.js.map +1 -1
  72. package/clinkx-workflows/dist/dashboard/server-factory.d.ts +14 -1
  73. package/clinkx-workflows/dist/dashboard/server-factory.js +73 -21
  74. package/clinkx-workflows/dist/dashboard/server-factory.js.map +1 -1
  75. package/clinkx-workflows/dist/dashboard/session-log-contract.d.ts +223 -56
  76. package/clinkx-workflows/dist/dashboard/session-log-contract.js +26 -0
  77. package/clinkx-workflows/dist/dashboard/session-log-contract.js.map +1 -1
  78. package/clinkx-workflows/dist/dashboard/session-log-journal.js +10 -3
  79. package/clinkx-workflows/dist/dashboard/session-log-journal.js.map +1 -1
  80. package/clinkx-workflows/dist/dashboard/session-log-redaction.d.ts +9 -1
  81. package/clinkx-workflows/dist/dashboard/session-log-redaction.js +793 -59
  82. package/clinkx-workflows/dist/dashboard/session-log-redaction.js.map +1 -1
  83. package/clinkx-workflows/dist/dashboard-ui/assets/index-JPHQVtn9.js +106 -0
  84. package/clinkx-workflows/dist/dashboard-ui/assets/style-DOMHk-j6.css +1 -0
  85. package/clinkx-workflows/dist/dashboard-ui/index.html +2 -2
  86. package/clinkx-workflows/dist/delegate-finalizer-policy.d.ts +26 -0
  87. package/clinkx-workflows/dist/delegate-finalizer-policy.js +72 -0
  88. package/clinkx-workflows/dist/delegate-finalizer-policy.js.map +1 -0
  89. package/clinkx-workflows/dist/delegate-signature.d.ts +14 -0
  90. package/clinkx-workflows/dist/delegate-signature.js +14 -0
  91. package/clinkx-workflows/dist/delegate-signature.js.map +1 -0
  92. package/clinkx-workflows/dist/delegate-validate.d.ts +32 -2
  93. package/clinkx-workflows/dist/delegate-validate.js +51 -0
  94. package/clinkx-workflows/dist/delegate-validate.js.map +1 -1
  95. package/clinkx-workflows/dist/engine.d.ts +113 -3
  96. package/clinkx-workflows/dist/engine.js +476 -80
  97. package/clinkx-workflows/dist/engine.js.map +1 -1
  98. package/clinkx-workflows/dist/errors.d.ts +22 -2
  99. package/clinkx-workflows/dist/errors.js +30 -2
  100. package/clinkx-workflows/dist/errors.js.map +1 -1
  101. package/clinkx-workflows/dist/fidelity.d.ts +47 -1
  102. package/clinkx-workflows/dist/fidelity.js +54 -1
  103. package/clinkx-workflows/dist/fidelity.js.map +1 -1
  104. package/clinkx-workflows/dist/fingerprint.d.ts +24 -6
  105. package/clinkx-workflows/dist/fingerprint.js +32 -9
  106. package/clinkx-workflows/dist/fingerprint.js.map +1 -1
  107. package/clinkx-workflows/dist/index.js +53 -1
  108. package/clinkx-workflows/dist/index.js.map +1 -1
  109. package/clinkx-workflows/dist/logger.d.ts +1 -1
  110. package/clinkx-workflows/dist/logger.js +17 -3
  111. package/clinkx-workflows/dist/logger.js.map +1 -1
  112. package/clinkx-workflows/dist/monotonic-clock.d.ts +19 -0
  113. package/clinkx-workflows/dist/monotonic-clock.js +22 -0
  114. package/clinkx-workflows/dist/monotonic-clock.js.map +1 -0
  115. package/clinkx-workflows/dist/plan-review/contract.d.ts +52 -52
  116. package/clinkx-workflows/dist/plan-review/publication.d.ts +4 -4
  117. package/clinkx-workflows/dist/plan-review/run-state.d.ts +16 -16
  118. package/clinkx-workflows/dist/roster-contract.d.ts +889 -0
  119. package/clinkx-workflows/dist/roster-contract.js +180 -0
  120. package/clinkx-workflows/dist/roster-contract.js.map +1 -0
  121. package/clinkx-workflows/dist/rosters.d.ts +14 -0
  122. package/clinkx-workflows/dist/rosters.js +169 -0
  123. package/clinkx-workflows/dist/rosters.js.map +1 -0
  124. package/clinkx-workflows/dist/run-health.d.ts +7 -0
  125. package/clinkx-workflows/dist/run-health.js +25 -0
  126. package/clinkx-workflows/dist/run-health.js.map +1 -0
  127. package/clinkx-workflows/dist/schema.d.ts +64 -0
  128. package/clinkx-workflows/dist/schema.js +24 -0
  129. package/clinkx-workflows/dist/schema.js.map +1 -1
  130. package/clinkx-workflows/dist/server.js +2 -0
  131. package/clinkx-workflows/dist/server.js.map +1 -1
  132. package/clinkx-workflows/dist/shutdown.d.ts +15 -2
  133. package/clinkx-workflows/dist/shutdown.js +60 -16
  134. package/clinkx-workflows/dist/shutdown.js.map +1 -1
  135. package/clinkx-workflows/dist/state-schema.d.ts +23 -0
  136. package/clinkx-workflows/dist/state.d.ts +5 -0
  137. package/clinkx-workflows/dist/state.js +128 -21
  138. package/clinkx-workflows/dist/state.js.map +1 -1
  139. package/clinkx-workflows/dist/stdio-watchdog.d.ts +52 -0
  140. package/clinkx-workflows/dist/stdio-watchdog.js +212 -0
  141. package/clinkx-workflows/dist/stdio-watchdog.js.map +1 -0
  142. package/clinkx-workflows/dist/types.d.ts +6 -0
  143. package/clinkx-workflows/dist/validators/council.d.ts +50 -50
  144. package/clinkx-workflows/dist/validators/plan-review.d.ts +3 -3
  145. package/clinkx-workflows/dist/workflow-tools.d.ts +70 -0
  146. package/clinkx-workflows/dist/workflow-tools.js +760 -168
  147. package/clinkx-workflows/dist/workflow-tools.js.map +1 -1
  148. package/clinkx-workflows/templates/council/answer-chairman.txt +3 -0
  149. package/clinkx-workflows/templates/council/answer-cross-reviewer.txt +3 -0
  150. package/clinkx-workflows/templates/council/answer-specialist.txt +3 -0
  151. package/clinkx-workflows/templates/council/chairman.txt +3 -0
  152. package/clinkx-workflows/templates/council/code-review-chairman.txt +3 -0
  153. package/clinkx-workflows/templates/council/code-review-cross-reviewer.txt +3 -0
  154. package/clinkx-workflows/templates/council/code-review-reviewer.txt +3 -0
  155. package/clinkx-workflows/templates/council/debug-chairman.txt +3 -0
  156. package/clinkx-workflows/templates/council/debug-cross-reviewer.txt +3 -0
  157. package/clinkx-workflows/templates/council/debug-debugger.txt +3 -0
  158. package/clinkx-workflows/templates/council/discover-analyst.txt +3 -0
  159. package/clinkx-workflows/templates/council/discover-chairman.txt +3 -0
  160. package/clinkx-workflows/templates/council/discover-cross-reviewer.txt +3 -0
  161. package/clinkx-workflows/templates/council/member.txt +3 -0
  162. package/clinkx-workflows/templates/council/plan-review-chairman.txt +3 -0
  163. package/clinkx-workflows/templates/council/plan-review-cross-reviewer.txt +3 -0
  164. package/clinkx-workflows/templates/council/plan-review-reviewer.txt +3 -0
  165. package/clinkx-workflows/templates/council/reviewer.txt +3 -0
  166. package/clinkx-workflows/workflows/council-answer.yaml +3 -0
  167. package/clinkx-workflows/workflows/council-code-review.yaml +3 -0
  168. package/clinkx-workflows/workflows/council-debug.yaml +3 -0
  169. package/clinkx-workflows/workflows/council-default.yaml +7 -0
  170. package/clinkx-workflows/workflows/council-discover.yaml +3 -0
  171. package/clinkx-workflows/workflows/council-plan-review.yaml +3 -0
  172. package/conf/adapters/agy.json +9 -2
  173. package/conf/adapters/claude.json +7 -0
  174. package/conf/adapters/codex.json +7 -0
  175. package/conf/adapters/glm.json +19 -3
  176. package/conf/adapters/grok.json +9 -2
  177. package/conf/adapters/hapi/agy.json +9 -2
  178. package/conf/adapters/hapi/claude.json +7 -0
  179. package/conf/adapters/hapi/codex.json +7 -0
  180. package/conf/adapters/hapi/glm.json +20 -4
  181. package/conf/rosters/default-council.json +21 -0
  182. package/dist/activity-journal.d.ts +17 -3
  183. package/dist/activity-journal.js +404 -79
  184. package/dist/activity-journal.js.map +1 -1
  185. package/dist/codex/agent-tools.js +36 -0
  186. package/dist/codex/agent-tools.js.map +1 -1
  187. package/dist/codex/app-server-client.d.ts +3 -0
  188. package/dist/codex/app-server-client.js +11 -0
  189. package/dist/codex/app-server-client.js.map +1 -1
  190. package/dist/codex/app-server-registry.d.ts +18 -0
  191. package/dist/codex/app-server-registry.js +88 -2
  192. package/dist/codex/app-server-registry.js.map +1 -1
  193. package/dist/codex/client-types.d.ts +7 -1
  194. package/dist/codex/protocol.d.ts +4 -4
  195. package/dist/config.d.ts +220 -23
  196. package/dist/config.js +104 -1
  197. package/dist/config.js.map +1 -1
  198. package/dist/context-builder/bundle.d.ts +13 -0
  199. package/dist/context-builder/bundle.js +46 -0
  200. package/dist/context-builder/bundle.js.map +1 -1
  201. package/dist/context-builder/delegate-finalizer-effective.d.ts +33 -0
  202. package/dist/context-builder/delegate-finalizer-effective.js +82 -0
  203. package/dist/context-builder/delegate-finalizer-effective.js.map +1 -0
  204. package/dist/context-builder/delegate-packet.d.ts +18 -0
  205. package/dist/context-builder/delegate-packet.js +73 -9
  206. package/dist/context-builder/delegate-packet.js.map +1 -1
  207. package/dist/context-builder/delegate-reconstruction.d.ts +29 -0
  208. package/dist/context-builder/delegate-reconstruction.js +110 -41
  209. package/dist/context-builder/delegate-reconstruction.js.map +1 -1
  210. package/dist/context-builder/delegate-validation.d.ts +31 -0
  211. package/dist/context-builder/delegate-validation.js +87 -10
  212. package/dist/context-builder/delegate-validation.js.map +1 -1
  213. package/dist/context-builder/delegate.d.ts +9 -0
  214. package/dist/context-builder/delegate.js +210 -59
  215. package/dist/context-builder/delegate.js.map +1 -1
  216. package/dist/context-builder/diagnostics.d.ts +1 -1
  217. package/dist/context-builder/diagnostics.js.map +1 -1
  218. package/dist/context-builder/discovery-progress.d.ts +25 -0
  219. package/dist/context-builder/discovery-progress.js +64 -0
  220. package/dist/context-builder/discovery-progress.js.map +1 -0
  221. package/dist/context-builder/discovery-watchdog.d.ts +15 -0
  222. package/dist/context-builder/discovery-watchdog.js +39 -0
  223. package/dist/context-builder/discovery-watchdog.js.map +1 -0
  224. package/dist/context-builder/discovery.d.ts +7 -0
  225. package/dist/context-builder/discovery.js +119 -7
  226. package/dist/context-builder/discovery.js.map +1 -1
  227. package/dist/context-builder/external-gaps.d.ts +73 -0
  228. package/dist/context-builder/external-gaps.js +248 -0
  229. package/dist/context-builder/external-gaps.js.map +1 -0
  230. package/dist/context-builder/globs.d.ts +1 -0
  231. package/dist/context-builder/globs.js +15 -0
  232. package/dist/context-builder/globs.js.map +1 -1
  233. package/dist/context-builder/handler.d.ts +42 -1
  234. package/dist/context-builder/handler.js +690 -62
  235. package/dist/context-builder/handler.js.map +1 -1
  236. package/dist/context-builder/invocation-sidecar.d.ts +41 -0
  237. package/dist/context-builder/invocation-sidecar.js +189 -0
  238. package/dist/context-builder/invocation-sidecar.js.map +1 -0
  239. package/dist/context-builder/lifecycle-contract.d.ts +51 -51
  240. package/dist/context-builder/mutation-lock.d.ts +32 -0
  241. package/dist/context-builder/mutation-lock.js +321 -0
  242. package/dist/context-builder/mutation-lock.js.map +1 -0
  243. package/dist/context-builder/review-coverage.d.ts +32 -0
  244. package/dist/context-builder/review-coverage.js +130 -23
  245. package/dist/context-builder/review-coverage.js.map +1 -1
  246. package/dist/context-builder/schema.d.ts +76 -1
  247. package/dist/context-builder/schema.js +21 -1
  248. package/dist/context-builder/schema.js.map +1 -1
  249. package/dist/context-builder/secrets.d.ts +31 -0
  250. package/dist/context-builder/secrets.js +871 -84
  251. package/dist/context-builder/secrets.js.map +1 -1
  252. package/dist/context-builder/status.d.ts +1 -1
  253. package/dist/context-builder/status.js +1 -0
  254. package/dist/context-builder/status.js.map +1 -1
  255. package/dist/context-builder/storage.d.ts +29 -5
  256. package/dist/context-builder/storage.js +311 -236
  257. package/dist/context-builder/storage.js.map +1 -1
  258. package/dist/context-builder/versions.d.ts +19 -4
  259. package/dist/context-builder/versions.js +19 -4
  260. package/dist/context-builder/versions.js.map +1 -1
  261. package/dist/context-builder/workflow-runner.d.ts +6 -0
  262. package/dist/context-builder/workflow-runner.js +363 -173
  263. package/dist/context-builder/workflow-runner.js.map +1 -1
  264. package/dist/dashboard-adapter-catalog.js +7 -0
  265. package/dist/dashboard-adapter-catalog.js.map +1 -1
  266. package/dist/dashboard-cli.d.ts +2 -0
  267. package/dist/dashboard-cli.js +40 -2
  268. package/dist/dashboard-cli.js.map +1 -1
  269. package/dist/dashboard-companion/client.d.ts +1 -0
  270. package/dist/dashboard-companion/client.js +22 -3
  271. package/dist/dashboard-companion/client.js.map +1 -1
  272. package/dist/dashboard-companion/protocol.js +4 -1
  273. package/dist/dashboard-companion/protocol.js.map +1 -1
  274. package/dist/dashboard-gateway/index.d.ts +1 -0
  275. package/dist/dashboard-gateway/index.js +33 -2
  276. package/dist/dashboard-gateway/index.js.map +1 -1
  277. package/dist/handler.js +5 -1
  278. package/dist/handler.js.map +1 -1
  279. package/dist/index.d.ts +18 -1
  280. package/dist/index.js +145 -7
  281. package/dist/index.js.map +1 -1
  282. package/dist/logger.d.ts +1 -1
  283. package/dist/logger.js +18 -3
  284. package/dist/logger.js.map +1 -1
  285. package/dist/parsers/agy-stream-json.d.ts +14 -0
  286. package/dist/parsers/agy-stream-json.js +172 -0
  287. package/dist/parsers/agy-stream-json.js.map +1 -0
  288. package/dist/parsers/extract.js +5 -1
  289. package/dist/parsers/extract.js.map +1 -1
  290. package/dist/parsers/observed-invocation.d.ts +20 -0
  291. package/dist/parsers/observed-invocation.js +42 -0
  292. package/dist/parsers/observed-invocation.js.map +1 -0
  293. package/dist/pipeline.d.ts +13 -0
  294. package/dist/pipeline.js +60 -10
  295. package/dist/pipeline.js.map +1 -1
  296. package/dist/progress-report/lock.d.ts +74 -0
  297. package/dist/progress-report/lock.js +343 -0
  298. package/dist/progress-report/lock.js.map +1 -0
  299. package/dist/progress-report/schema.d.ts +808 -0
  300. package/dist/progress-report/schema.js +119 -0
  301. package/dist/progress-report/schema.js.map +1 -0
  302. package/dist/progress-report/store.d.ts +66 -0
  303. package/dist/progress-report/store.js +766 -0
  304. package/dist/progress-report/store.js.map +1 -0
  305. package/dist/progress-report/tools.d.ts +20 -0
  306. package/dist/progress-report/tools.js +141 -0
  307. package/dist/progress-report/tools.js.map +1 -0
  308. package/dist/prompt-mode.d.ts +1 -1
  309. package/dist/prompt-mode.js +7 -2
  310. package/dist/prompt-mode.js.map +1 -1
  311. package/dist/runner.d.ts +7 -0
  312. package/dist/runner.js +10 -3
  313. package/dist/runner.js.map +1 -1
  314. package/dist/safety.d.ts +50 -0
  315. package/dist/safety.js +182 -1
  316. package/dist/safety.js.map +1 -1
  317. package/dist/schema.d.ts +8 -0
  318. package/dist/schema.js +20 -0
  319. package/dist/schema.js.map +1 -1
  320. package/dist/server.d.ts +6 -1
  321. package/dist/server.js +94 -8
  322. package/dist/server.js.map +1 -1
  323. package/dist/session-log-stream.d.ts +1 -1
  324. package/dist/session-log-stream.js +280 -33
  325. package/dist/session-log-stream.js.map +1 -1
  326. package/dist/stdio-activity.d.ts +28 -0
  327. package/dist/stdio-activity.js +72 -0
  328. package/dist/stdio-activity.js.map +1 -0
  329. package/docs/configuration.md +426 -0
  330. package/docs/security.md +293 -0
  331. package/package.json +5 -5
  332. package/clinkx-workflows/dist/dashboard-ui/assets/index-DOkMXTfB.js +0 -25
  333. package/clinkx-workflows/dist/dashboard-ui/assets/style-ChJsn0KD.css +0 -1
@@ -1,18 +1,25 @@
1
1
  import { appendArtifactOnlyRootsToEnv, isArtifactOnlyAllowedRootEntry, isSamePathOrDescendant, parseAllowedRootsEnv, stripArtifactOnlyMarker, } from "../allowed-roots.js";
2
2
  import { CancellationError, ExecutionError, InvalidParamsError, } from "../errors.js";
3
+ import { deriveInvocationAudit, resolveInvocationOverrides } from "../safety.js";
3
4
  import { realpathSync } from "node:fs";
4
5
  import { realpath as fsRealpath } from "node:fs/promises";
5
6
  import path from "node:path";
6
7
  import { AdapterRegistry as RuntimeAdapterRegistry } from "../registry.js";
7
8
  import { logger } from "../logger.js";
9
+ import { invocationAuditV2PersistenceEnabled } from "../../clinkx-workflows/dist/config.js";
8
10
  import { assertContextBuilderAnchorSufficiency } from "./anchors.js";
9
11
  import { writeContextBuilderFinalArtifacts, } from "./bundle.js";
10
12
  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
- import { reconstructContextBuilderDelegateSelection } from "./delegate-reconstruction.js";
13
+ import { buildContextBuilderCuratorPacket, buildContextBuilderFinalizerPacket, CONTEXT_BUILDER_DELEGATE_PACKET_CAP_PROFILE_DEFAULTS, contextBuilderCandidateId, countDelegatePacketObligationTruncation, } from "./delegate-packet.js";
14
+ import { reconstructContextBuilderDelegateSelection, reconstructContextBuilderDelegateSelectionWithReport, } from "./delegate-reconstruction.js";
15
+ import { assertNoDisabledFinalizerInvocationOverrides, resolveContextBuilderFinalizerEffectiveEnabled, } from "./delegate-finalizer-effective.js";
13
16
  import { runContextBuilderDelegateCall, } from "./delegate.js";
14
- import { validateContextBuilderCuratorOutput, validateContextBuilderDelegateCoverageSelection, validateContextBuilderFinalizerOutput, } from "./delegate-validation.js";
17
+ import { recordContextInvocationAudit } from "./invocation-sidecar.js";
18
+ import { classifyFinalizerExternalGaps, } from "./external-gaps.js";
19
+ import { ContextBuilderFinalizerRejectionError, MAX_FINALIZER_EXTERNAL_GAPS, validateContextBuilderCuratorOutput, validateContextBuilderDelegateCoverageSelection, validateContextBuilderFinalizerOutput, } from "./delegate-validation.js";
15
20
  import { discoverContextBuilderCandidates, mergeLineRanges, } from "./discovery.js";
21
+ import { createScanProgressPublisher } from "./discovery-progress.js";
22
+ import { withContextBuilderDiscoveryWatchdog } from "./discovery-watchdog.js";
16
23
  import { evaluateSearchCapMateriality } from "./status.js";
17
24
  import { boundContextBuilderPathList, isRequestedEvidenceOmission, sanitizeDelegateFailureText, sanitizeDiagnosticText, } from "./diagnostics.js";
18
25
  import { compileContextBuilderGlobs, CONTEXT_BUILDER_DEFAULT_EXCLUDE_GLOBS, validateContextBuilderGlobs, } from "./globs.js";
@@ -25,18 +32,23 @@ import { appendLifecycleCompletionWarning } from "./lifecycle-recovery.js";
25
32
  import { withContextBuilderWriteWatchdog } from "./write-watchdog.js";
26
33
  import { createContextBuilderReceiptTextResponse, } from "./receipt.js";
27
34
  import { generateContextBuilderRunId } from "./run-id.js";
28
- import { CONTEXT_BUILDER_INTENT_PROFILE_DEFAULTS, parseContextBuilderInput, } from "./schema.js";
35
+ import { CONTEXT_BUILDER_PUBLIC_BUDGET_HARD_LIMITS, parseContextBuilderInput, } from "./schema.js";
29
36
  import { evaluateContextCoverage, resolveContextCoveragePolicy, } from "./review-coverage.js";
30
37
  import { selectContextBuilderFinalContext, } from "./selection.js";
31
- import { cleanupContextBuilderIncompleteRun, createContextBuilderRun, } from "./storage.js";
38
+ import { cleanupContextBuilderIncompleteRun, createContextBuilderRun, withContextBuilderRunMutationLease, } from "./storage.js";
32
39
  import { admitJournalContextGeneration } from "./session-log-identity.js";
33
40
  import { estimateContextBuilderTokensFromUtf8Bytes } from "./token-estimate.js";
34
41
  let defaultAdapterRegistry;
35
42
  const artifactOnlyAllowedRoots = new Set();
36
- const CONTEXT_BUILDER_DELEGATE_PROMPT_VERSION = "cb-delegate-v3-scored-obligations";
43
+ const CONTEXT_BUILDER_DELEGATE_PROMPT_VERSION = "cb-delegate-v6-gap-ancestors";
37
44
  const MAX_DELEGATE_MINIMUM_OUTPUT_SELECTIONS = 24;
38
45
  export async function handleContextBuilder(args, options = {}) {
39
46
  const input = parseContextBuilderInput(args);
47
+ if (input.delegate.enabled) {
48
+ validateDelegateInvocationOverrides(input, options.registry, {
49
+ minScoreExplicit: options.standaloneMinScoreExplicit === true,
50
+ });
51
+ }
40
52
  const coveragePolicy = resolveContextCoveragePolicy(input.profile);
41
53
  const semaphore = options.semaphore ?? getDefaultContextBuilderSemaphore();
42
54
  const progress = createContextBuilderProgressReporter(options.progress);
@@ -150,7 +162,8 @@ export async function handleContextBuilder(args, options = {}) {
150
162
  : { contextRunId: lifecycle.contextRunId, contextGeneration: injected.contextGeneration };
151
163
  }
152
164
  await lifecycle?.transition("scanning");
153
- const discovery = await discoverContextBuilderCandidates({
165
+ const scanProgressPublisher = createScanProgressPublisher(lifecycle);
166
+ const discovery = await withContextBuilderDiscoveryWatchdog((signal) => discoverContextBuilderCandidates({
154
167
  roots: rootResolution.roots,
155
168
  task: input.task,
156
169
  allowSecrets: input.allow_secrets,
@@ -162,8 +175,9 @@ export async function handleContextBuilder(args, options = {}) {
162
175
  budget: input.budget,
163
176
  internal_caps: internalCaps,
164
177
  excluded_real_roots: excludedArtifactRoots,
165
- ...(options.signal == null ? {} : { signal: options.signal }),
166
- });
178
+ ...(signal == null ? {} : { signal }),
179
+ onProgress: scanProgressPublisher,
180
+ }), { signal: options.signal });
167
181
  await lifecycle?.transition("searching", {
168
182
  counts: lifecycleCountsFromDiscovery(discovery),
169
183
  });
@@ -189,6 +203,8 @@ export async function handleContextBuilder(args, options = {}) {
189
203
  focusPaths,
190
204
  baselineOmissions: rootResolution.omissions,
191
205
  sourceRoots: rootResolution.roots,
206
+ excludedRealRoots: excludedArtifactRoots,
207
+ artifactRunDir: writingStorage.runDir,
192
208
  registry: options.registry ?? getDefaultAdapterRegistry(),
193
209
  coveragePolicy,
194
210
  ...(effectiveDelegatePacketCaps == null
@@ -234,9 +250,14 @@ export async function handleContextBuilder(args, options = {}) {
234
250
  // cap event (built from metrics + search_coverage in the status reducer)
235
251
  // carries the partial classification.
236
252
  const searchCapWarning = searchCapCoverageWarning(metrics.search_cap_hit, discovery.search_coverage);
237
- const warnings = searchCapWarning == null
238
- ? reconciledWarnings
239
- : [...reconciledWarnings, searchCapWarning];
253
+ const coverageAfterDelegateWarning = coverageDegradedAfterDelegateWarning(coverage, effectiveDelegateResult?.delegate);
254
+ const warnings = [
255
+ ...reconciledWarnings,
256
+ ...(searchCapWarning == null ? [] : [searchCapWarning]),
257
+ ...(coverageAfterDelegateWarning == null
258
+ ? []
259
+ : [coverageAfterDelegateWarning]),
260
+ ];
240
261
  const delegateMetadata = effectiveDelegateResult?.delegate == null
241
262
  ? undefined
242
263
  : {
@@ -291,6 +312,15 @@ export async function handleContextBuilder(args, options = {}) {
291
312
  metrics,
292
313
  search_coverage: discovery.search_coverage,
293
314
  open_questions: openQuestions,
315
+ ...(effectiveDelegateResult?.knownMissingEvidence == null
316
+ ? {}
317
+ : {
318
+ known_missing_evidence: effectiveDelegateResult.knownMissingEvidence.map((gap) => ({
319
+ path: gap.path,
320
+ provenance: gap.provenance,
321
+ reason: gap.reason,
322
+ })),
323
+ }),
294
324
  ...(delegateMetadata == null ? {} : { delegate: delegateMetadata }),
295
325
  observability,
296
326
  coverage,
@@ -314,14 +344,28 @@ export async function handleContextBuilder(args, options = {}) {
314
344
  // Capture into a const so the beforeManifestCommit closure keeps the
315
345
  // narrowed non-undefined type (the outer binding is a reassignable let).
316
346
  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
347
+ const writeCommittedRun = async (leasedOps) => {
348
+ const receipt = await withContextBuilderWriteWatchdog((signal) => writeContextBuilderFinalArtifacts(writingStorage, renderInput, {
349
+ signal,
350
+ ...(writingStorage.ownerNonce == null
351
+ ? {}
352
+ : { ownerNonce: writingStorage.ownerNonce }),
353
+ ...(leasedOps == null ? {} : { ops: leasedOps }),
354
+ ...(!ownsLifecycle || committingLifecycle == null
355
+ ? {}
356
+ : { beforeManifestCommit: () => committingLifecycle.transition("committing") }),
357
+ }), options.signal == null ? {} : { signal: options.signal });
358
+ committed = true;
359
+ if (options.retainCommittedRun !== undefined && leasedOps !== undefined) {
360
+ await options.retainCommittedRun(createContextBuilderReceiptTextResponse(receipt), writingStorage, leasedOps);
361
+ }
362
+ return receipt;
363
+ };
364
+ const receipt = options.retainCommittedRun == null
365
+ ? await writeCommittedRun()
366
+ : await withContextBuilderRunMutationLease(writingStorage, writeCommittedRun, undefined, writingStorage.ownerNonce == null
321
367
  ? {}
322
- : { beforeManifestCommit: () => committingLifecycle.transition("committing") }),
323
- }), options.signal == null ? {} : { signal: options.signal });
324
- committed = true;
368
+ : { ownerNonce: writingStorage.ownerNonce });
325
369
  if (ownsLifecycle) {
326
370
  await recordContextRunCatalogMarker({
327
371
  runsDir,
@@ -570,12 +614,19 @@ function assertNoContextBuilderFocusGlobConflicts(input) {
570
614
  if (focus.kind === "directory") {
571
615
  if (matcher.excludes(relative) ||
572
616
  matcher.excludes(`${relative}/__dir__`)) {
573
- conflicts.push(`focus directory "${relative}" (root_index ${focus.root_index}) is pruned by exclude_globs`);
617
+ const defaultGlob = matchingDefaultExcludeGlob(relative) ??
618
+ matchingDefaultExcludeGlob(`${relative}/__dir__`);
619
+ conflicts.push(defaultGlob == null
620
+ ? `focus directory "${relative}" (root_index ${focus.root_index}) is pruned by exclude_globs`
621
+ : `focus directory "${relative}" (root_index ${focus.root_index}) matched default exclude ${defaultGlob} (not overridable)`);
574
622
  }
575
623
  continue;
576
624
  }
577
625
  if (matcher.excludes(relative)) {
578
- conflicts.push(`focus path "${relative}" (root_index ${focus.root_index}) is omitted by exclude_globs`);
626
+ const defaultGlob = matchingDefaultExcludeGlob(relative);
627
+ conflicts.push(defaultGlob == null
628
+ ? `focus path "${relative}" (root_index ${focus.root_index}) is omitted by exclude_globs`
629
+ : `focus path "${relative}" (root_index ${focus.root_index}) matched default exclude ${defaultGlob} (not overridable)`);
579
630
  continue;
580
631
  }
581
632
  if (!matcher.includes(relative)) {
@@ -586,6 +637,17 @@ function assertNoContextBuilderFocusGlobConflicts(input) {
586
637
  throw new InvalidParamsError(`context_builder focus_paths conflict with the effective include/exclude globs (${conflicts.length} conflict${conflicts.length === 1 ? "" : "s"}): ${conflicts.join("; ")}`);
587
638
  }
588
639
  }
640
+ // Default excludes are always prepended and cannot be removed by callers, so a
641
+ // conflict against one needs to say so — "omitted by exclude_globs" sends the
642
+ // caller hunting through globs they never passed.
643
+ let defaultExcludeGlobMatchers;
644
+ function matchingDefaultExcludeGlob(relative) {
645
+ defaultExcludeGlobMatchers ??= CONTEXT_BUILDER_DEFAULT_EXCLUDE_GLOBS.map((glob) => ({
646
+ glob,
647
+ matcher: compileContextBuilderGlobs({ include: [], exclude: [glob] }),
648
+ }));
649
+ return defaultExcludeGlobMatchers.find(({ matcher }) => matcher.excludes(relative))?.glob;
650
+ }
589
651
  function getExactlyConfiguredArtifactRoots() {
590
652
  const rawEntries = rawAllowedRootEntries();
591
653
  const configured = new Set();
@@ -635,6 +697,38 @@ function getDefaultAdapterRegistry() {
635
697
  }
636
698
  return defaultAdapterRegistry;
637
699
  }
700
+ function validateDelegateInvocationOverrides(input, registry, overrides = {}) {
701
+ if (!input.delegate.enabled) {
702
+ return;
703
+ }
704
+ assertNoDisabledFinalizerInvocationOverrides(input.delegate.finalizer.enabled, input.profile, input.delegate.finalizer.model, input.delegate.finalizer.effort, overrides.minScoreExplicit === true
705
+ ? input.delegate.finalizer.min_score
706
+ : undefined);
707
+ const finalizerEnabled = contextBuilderFinalizerEnabled(input);
708
+ const curatorHasOverrides = delegateProviderHasInvocationOverrides(input.delegate.curator);
709
+ const finalizerHasOverrides = finalizerEnabled &&
710
+ delegateProviderHasInvocationOverrides(input.delegate.finalizer);
711
+ if (!curatorHasOverrides && !finalizerHasOverrides) {
712
+ return;
713
+ }
714
+ const resolvedRegistry = registry ?? getDefaultAdapterRegistry();
715
+ if (curatorHasOverrides) {
716
+ validateDelegateProviderInvocationOverrides(input.delegate.curator, resolvedRegistry);
717
+ }
718
+ if (finalizerHasOverrides) {
719
+ validateDelegateProviderInvocationOverrides(input.delegate.finalizer, resolvedRegistry);
720
+ }
721
+ }
722
+ function delegateProviderHasInvocationOverrides(provider) {
723
+ return provider.model !== undefined || provider.effort !== undefined;
724
+ }
725
+ function validateDelegateProviderInvocationOverrides(provider, registry) {
726
+ const adapter = registry.resolveAdapter(provider.cli_name);
727
+ resolveInvocationOverrides(adapter, adapter.args, {
728
+ ...(provider.model === undefined ? {} : { model: provider.model }),
729
+ ...(provider.effort === undefined ? {} : { effort: provider.effort }),
730
+ });
731
+ }
638
732
  function resolveEffectiveDelegatePacketCaps(profile, explicitCaps) {
639
733
  const profileCaps = profile == null
640
734
  ? undefined
@@ -651,15 +745,7 @@ function contextBuilderFinalizerEnabled(input) {
651
745
  if (!input.delegate.enabled) {
652
746
  return false;
653
747
  }
654
- const explicit = input.delegate.finalizer.enabled;
655
- if (explicit != null) {
656
- return explicit;
657
- }
658
- if (input.profile != null) {
659
- return CONTEXT_BUILDER_INTENT_PROFILE_DEFAULTS[input.profile]
660
- .finalizer_enabled;
661
- }
662
- return true;
748
+ return resolveContextBuilderFinalizerEffectiveEnabled(input.delegate.finalizer.enabled, input.profile);
663
749
  }
664
750
  function delegateFinalizerReceiptMetadata(finalizer) {
665
751
  return {
@@ -668,6 +754,41 @@ function delegateFinalizerReceiptMetadata(finalizer) {
668
754
  min_score: finalizer.min_score,
669
755
  };
670
756
  }
757
+ function delegateProviderReceiptMetadata(provider) {
758
+ return {
759
+ cli_name: provider.cli_name,
760
+ role: provider.role,
761
+ };
762
+ }
763
+ async function recordDelegateSeatInvocationAudit(artifactRunDir, seat, provider, registry) {
764
+ if (!invocationAuditV2PersistenceEnabled())
765
+ return;
766
+ try {
767
+ const adapter = registry.resolveAdapter(provider.cli_name);
768
+ const extraArgs = adapter.roles[provider.role]?.extra_args;
769
+ const mergedArgs = extraArgs == null
770
+ ? adapter.args
771
+ : [...adapter.args, ...extraArgs];
772
+ const audit = deriveInvocationAudit(adapter, mergedArgs, {
773
+ model: provider.model,
774
+ effort: provider.effort,
775
+ });
776
+ await recordContextInvocationAudit(artifactRunDir, seat, audit);
777
+ }
778
+ catch (err) {
779
+ // Adapter resolution failures are raised by the subsequent delegate
780
+ // call; do not recast them as sidecar problems.
781
+ if (err instanceof InvalidParamsError) {
782
+ return;
783
+ }
784
+ try {
785
+ logger.warn({ err, seat }, "failed to record context_builder invocation audit sidecar");
786
+ }
787
+ catch {
788
+ // Sidecar diagnostics must not fail the delegate.
789
+ }
790
+ }
791
+ }
671
792
  async function resolveDelegateSelection(input) {
672
793
  const delegate = input.input.delegate;
673
794
  if (!delegate.enabled) {
@@ -737,6 +858,7 @@ async function resolveDelegateSelection(input) {
737
858
  if (requestedFocusOmissions.length > 0) {
738
859
  return handleDelegateRuntimeFailure({
739
860
  delegate,
861
+ discoveryMetrics: input.discovery.metrics,
740
862
  reason: `requested local evidence omitted: ${formatOmissionsForDelegateFailure(requestedFocusOmissions)}`,
741
863
  deterministicSelection: input.deterministicSelection,
742
864
  omissionCount: input.baselineOmissions.length +
@@ -751,10 +873,12 @@ async function resolveDelegateSelection(input) {
751
873
  delegatePacketSize = curatorPacket.metrics.packet_estimated_tokens;
752
874
  delegatePacketCompaction = curatorPacket.metrics.compaction;
753
875
  recordPacketEvidenceWarning(curatorPacket.evidence_warning);
876
+ recordPacketEvidenceWarning(packetObligationTruncationWarning(input.discovery.review_obligations, curatorPacket.coverage_policy));
754
877
  const curatorPacketRequestedOmissions = curatorPacket.omissions.filter(isRequestedEvidenceOmission);
755
878
  if (curatorPacketRequestedOmissions.length > 0) {
756
879
  return handleDelegateRuntimeFailure({
757
880
  delegate,
881
+ discoveryMetrics: input.discovery.metrics,
758
882
  reason: `requested local evidence omitted: ${formatOmissionsForDelegateFailure(curatorPacketRequestedOmissions)}`,
759
883
  deterministicSelection: input.deterministicSelection,
760
884
  omissions: curatorPacketRequestedOmissions,
@@ -769,6 +893,7 @@ async function resolveDelegateSelection(input) {
769
893
  if (curatorPacket.candidates.length === 0) {
770
894
  return handleDelegateRuntimeFailure({
771
895
  delegate,
896
+ discoveryMetrics: input.discovery.metrics,
772
897
  reason: `curator packet contained no selectable candidates; ${delegatePromptDiagnostic(curatorPacket)}`,
773
898
  deterministicSelection: input.deterministicSelection,
774
899
  omissionCount: input.baselineOmissions.length +
@@ -778,8 +903,35 @@ async function resolveDelegateSelection(input) {
778
903
  observability: currentDelegateObservability(),
779
904
  });
780
905
  }
906
+ // Required focus is admitted first after the finalizer, so if it cannot fit
907
+ // the local reconstruction budget ON ITS OWN no curator/finalizer answer can
908
+ // rescue the run. Dry-run the same reconstruction now and fail in ~1s
909
+ // instead of after minutes of provider time. Finalizer mode only: curator-
910
+ // only mode may legitimately compact required ranges under budget pressure.
911
+ if (finalizerEnabled) {
912
+ const preDispatchFailure = requiredFocusPreDispatchFailure({
913
+ focusPaths: input.focusPaths,
914
+ candidates: curatorPacket.candidates,
915
+ discovery: input.discovery,
916
+ budget: input.input.budget,
917
+ });
918
+ if (preDispatchFailure != null) {
919
+ return handleDelegateRuntimeFailure({
920
+ delegate,
921
+ discoveryMetrics: input.discovery.metrics,
922
+ reason: preDispatchFailure,
923
+ deterministicSelection: input.deterministicSelection,
924
+ omissionCount: input.baselineOmissions.length +
925
+ input.discovery.omissions.length +
926
+ input.deterministicSelection.omissions.length,
927
+ candidateFallbackCount,
928
+ observability: currentDelegateObservability(),
929
+ });
930
+ }
931
+ }
781
932
  const curatorStartMs = Date.now();
782
933
  const curatorSessionLogOwner = sessionLogOwner("curator");
934
+ await recordDelegateSeatInvocationAudit(input.artifactRunDir, "curator", delegate.curator, input.registry);
783
935
  const curatorCall = await runContextBuilderDelegateCall({
784
936
  prompt: renderDelegatePrompt("curator", curatorPacket),
785
937
  provider: delegate.curator,
@@ -795,6 +947,7 @@ async function resolveDelegateSelection(input) {
795
947
  if (!curatorCall.ok) {
796
948
  return handleDelegateRuntimeFailure({
797
949
  delegate,
950
+ discoveryMetrics: input.discovery.metrics,
798
951
  reason: failureReasonFromCall("curator", curatorCall),
799
952
  failureKind: curatorCall.reason,
800
953
  deterministicSelection: input.deterministicSelection,
@@ -856,6 +1009,7 @@ async function resolveDelegateSelection(input) {
856
1009
  }
857
1010
  return handleDelegateRuntimeFailure({
858
1011
  delegate,
1012
+ discoveryMetrics: input.discovery.metrics,
859
1013
  reason: failureReasonFromError("curator_validation", err, delegatePromptDiagnostic(curatorPacket)),
860
1014
  deterministicSelection: input.deterministicSelection,
861
1015
  omissionCount: input.baselineOmissions.length +
@@ -868,6 +1022,7 @@ async function resolveDelegateSelection(input) {
868
1022
  if (curatorSelection.selected.length === 0) {
869
1023
  return handleDelegateRuntimeFailure({
870
1024
  delegate,
1025
+ discoveryMetrics: input.discovery.metrics,
871
1026
  reason: "curator produced empty local selection",
872
1027
  deterministicSelection: input.deterministicSelection,
873
1028
  omissionCount: input.baselineOmissions.length +
@@ -886,6 +1041,7 @@ async function resolveDelegateSelection(input) {
886
1041
  expandSelectedFiles: true,
887
1042
  });
888
1043
  const warnings = curatorProposal.warnings.map((message) => delegateWarning("delegate_curator_warning", message));
1044
+ warnings.push(...correctiveRetryWarnings("curator", curatorCall));
889
1045
  warnings.push(...packetEvidenceWarnings);
890
1046
  appendDelegateRepairWarnings(warnings, repairedSelection);
891
1047
  const openQuestions = curatorProposal.open_questions.map((question) => sanitizeDiagnosticText(question));
@@ -901,6 +1057,7 @@ async function resolveDelegateSelection(input) {
901
1057
  if (delegateEvidenceCoverageFailure != null) {
902
1058
  return handleDelegateRuntimeFailure({
903
1059
  delegate,
1060
+ discoveryMetrics: input.discovery.metrics,
904
1061
  reason: `post-repair evidence coverage failed: ${delegateEvidenceCoverageFailure}`,
905
1062
  deterministicSelection: input.deterministicSelection,
906
1063
  omissionCount,
@@ -921,6 +1078,7 @@ async function resolveDelegateSelection(input) {
921
1078
  if (curatorOnlyCoverageFailure != null) {
922
1079
  return handleDelegateRuntimeFailure({
923
1080
  delegate,
1081
+ discoveryMetrics: input.discovery.metrics,
924
1082
  reason: `post-repair required-focus coverage failed: ${curatorOnlyCoverageFailure}`,
925
1083
  deterministicSelection: input.deterministicSelection,
926
1084
  omissionCount: input.baselineOmissions.length +
@@ -940,7 +1098,7 @@ async function resolveDelegateSelection(input) {
940
1098
  status: "succeeded",
941
1099
  failure_policy: delegate.failure_policy,
942
1100
  fallback_used: false,
943
- curator: { ...delegate.curator },
1101
+ curator: delegateProviderReceiptMetadata(delegate.curator),
944
1102
  finalizer: delegateFinalizerReceiptMetadata(delegate.finalizer),
945
1103
  selected_count: repairedSelection.selection.selected.length,
946
1104
  warning_count: warnings.length,
@@ -961,10 +1119,12 @@ async function resolveDelegateSelection(input) {
961
1119
  delegatePacketSize = finalizerPacket.metrics.packet_estimated_tokens;
962
1120
  delegatePacketCompaction = finalizerPacket.metrics.compaction;
963
1121
  recordPacketEvidenceWarning(finalizerPacket.evidence_warning);
1122
+ recordPacketEvidenceWarning(packetObligationTruncationWarning(input.discovery.review_obligations, finalizerPacket.coverage_policy));
964
1123
  const finalizerPacketRequestedOmissions = finalizerPacket.omissions.filter(isRequestedEvidenceOmission);
965
1124
  if (finalizerPacketRequestedOmissions.length > 0) {
966
1125
  return handleDelegateRuntimeFailure({
967
1126
  delegate,
1127
+ discoveryMetrics: input.discovery.metrics,
968
1128
  reason: `requested local evidence omitted: ${formatOmissionsForDelegateFailure(finalizerPacketRequestedOmissions)}`,
969
1129
  deterministicSelection: input.deterministicSelection,
970
1130
  omissions: finalizerPacketRequestedOmissions,
@@ -979,6 +1139,7 @@ async function resolveDelegateSelection(input) {
979
1139
  if ((finalizerPacket.curator_proposal?.selected.length ?? 0) === 0) {
980
1140
  return handleDelegateRuntimeFailure({
981
1141
  delegate,
1142
+ discoveryMetrics: input.discovery.metrics,
982
1143
  reason: `finalizer packet lost curator proposal selection; ${delegatePromptDiagnostic(finalizerPacket)}`,
983
1144
  deterministicSelection: input.deterministicSelection,
984
1145
  omissionCount: input.baselineOmissions.length +
@@ -990,9 +1151,11 @@ async function resolveDelegateSelection(input) {
990
1151
  }
991
1152
  const finalizerStartMs = Date.now();
992
1153
  const finalizerSessionLogOwner = sessionLogOwner("finalizer");
1154
+ await recordDelegateSeatInvocationAudit(input.artifactRunDir, "finalizer", delegate.finalizer, input.registry);
993
1155
  const finalizerCall = await runContextBuilderDelegateCall({
994
1156
  prompt: renderDelegatePrompt("finalizer", finalizerPacket, {
995
1157
  minScore: delegate.finalizer.min_score,
1158
+ externalGapPolicy: delegate.external_gap_policy,
996
1159
  }),
997
1160
  provider: delegate.finalizer,
998
1161
  registry: input.registry,
@@ -1007,6 +1170,7 @@ async function resolveDelegateSelection(input) {
1007
1170
  if (!finalizerCall.ok) {
1008
1171
  return handleDelegateRuntimeFailure({
1009
1172
  delegate,
1173
+ discoveryMetrics: input.discovery.metrics,
1010
1174
  reason: failureReasonFromCall("finalizer", finalizerCall),
1011
1175
  failureKind: finalizerCall.reason,
1012
1176
  deterministicSelection: input.deterministicSelection,
@@ -1025,43 +1189,64 @@ async function resolveDelegateSelection(input) {
1025
1189
  allowSecrets: input.input.allow_secrets,
1026
1190
  coveragePolicy: finalizerPacket.coverage_policy,
1027
1191
  });
1028
- if (finalizerProposal.omissions.length > 0) {
1192
+ const externalGaps = classifyFinalizerExternalGaps({
1193
+ gaps: finalizerProposal.external_gaps,
1194
+ sourceRoots: input.sourceRoots,
1195
+ focusPaths: input.focusPaths,
1196
+ packetCandidates: finalizerPacket.candidates,
1197
+ discovery: input.discovery,
1198
+ includeGlobs: input.input.include_globs,
1199
+ excludeGlobs: input.input.exclude_globs,
1200
+ ...(input.excludedRealRoots == null
1201
+ ? {}
1202
+ : { excludedRealRoots: input.excludedRealRoots }),
1203
+ });
1204
+ // A rejected gap means the finalizer mis-described the packet (it named
1205
+ // in-packet or focused evidence, or a path that is not local at all);
1206
+ // that is invalid output under either gap policy.
1207
+ const externalGapFailure = externalGaps.rejected.length > 0
1208
+ ? formatExternalGapRejectionFailure(externalGaps.rejected)
1209
+ : externalGaps.external.length > 0 &&
1210
+ delegate.external_gap_policy === "fail"
1211
+ ? formatExternalGapPolicyFailure(externalGaps.external)
1212
+ : null;
1213
+ if (externalGapFailure != null) {
1029
1214
  return handleDelegateRuntimeFailure({
1030
1215
  delegate,
1031
- reason: `finalizer reported omissions: ${finalizerProposal.omissions.join("; ")}`,
1216
+ discoveryMetrics: input.discovery.metrics,
1217
+ reason: externalGapFailure,
1032
1218
  deterministicSelection: input.deterministicSelection,
1033
1219
  omissionCount: input.baselineOmissions.length +
1034
1220
  input.discovery.omissions.length +
1035
- input.deterministicSelection.omissions.length +
1036
- finalizerProposal.omissions.length,
1221
+ input.deterministicSelection.omissions.length,
1037
1222
  candidateFallbackCount,
1038
1223
  observability: currentDelegateObservability(),
1039
1224
  });
1040
1225
  }
1041
- const foldedFinalizerSelection = foldRequiredFocusEvidenceIntoSelection(finalizerProposal.selected, requiredFocusEvidenceSelections({
1226
+ const requiredFocus = requiredFocusEvidenceSelections({
1042
1227
  focusPaths: input.focusPaths,
1043
1228
  candidates: finalizerPacket.candidates,
1044
- }));
1045
- const finalSelection = reconstructContextBuilderDelegateSelection({
1229
+ });
1230
+ const foldedFinalizerSelection = foldRequiredFocusEvidenceIntoSelection(finalizerProposal.selected, requiredFocus);
1231
+ // Required-first admission: the fold appends finalizer-dropped required
1232
+ // focus at the END of selection order, so a preference-free first-fit
1233
+ // prune would evict exactly that evidence under cap pressure. Admitting
1234
+ // required candidates first means a required-focus file can only be
1235
+ // missing here when required focus ALONE exceeds the budget.
1236
+ const finalReconstruction = reconstructContextBuilderDelegateSelectionWithReport({
1046
1237
  selected: foldedFinalizerSelection.selected,
1047
1238
  candidates: finalizerPacket.candidates,
1048
1239
  slices: input.discovery.slices,
1049
1240
  tree: input.discovery.tree,
1050
1241
  search: input.discovery.search,
1051
1242
  budget: input.input.budget,
1243
+ ...(requiredFocus.length === 0
1244
+ ? {}
1245
+ : {
1246
+ requiredFocus: new Map(requiredFocus.map((required) => [required.candidate_id, required.ranges])),
1247
+ }),
1052
1248
  });
1053
- if (finalSelection.selected.length === 0) {
1054
- return handleDelegateRuntimeFailure({
1055
- delegate,
1056
- reason: "finalizer produced empty local selection",
1057
- deterministicSelection: input.deterministicSelection,
1058
- omissionCount: input.baselineOmissions.length +
1059
- input.discovery.omissions.length +
1060
- input.deterministicSelection.omissions.length,
1061
- candidateFallbackCount,
1062
- observability: currentDelegateObservability(),
1063
- });
1064
- }
1249
+ const finalSelection = finalReconstruction.selection;
1065
1250
  // A per-file reconstruction-budget overrun no longer fails the entire
1066
1251
  // delegate selection. Over-budget candidates (e.g. large generated/i18n
1067
1252
  // catalogs the finalizer over-selected) are dropped while the rest of the
@@ -1078,10 +1263,16 @@ async function resolveDelegateSelection(input) {
1078
1263
  finalizerSelected: materializeFinalizerSelection(finalSelection.selected, finalizerPacket.candidates),
1079
1264
  curatorWarnings: curatorProposal.warnings,
1080
1265
  finalizerWarnings: finalizerProposal.warnings,
1266
+ budgetDiagnostics: {
1267
+ omissions: finalSelection.omissions,
1268
+ foldedPaths: foldedFinalizerSelection.foldedPaths,
1269
+ budget: input.input.budget,
1270
+ },
1081
1271
  });
1082
1272
  if (completenessFailure != null) {
1083
1273
  return handleDelegateRuntimeFailure({
1084
1274
  delegate,
1275
+ discoveryMetrics: input.discovery.metrics,
1085
1276
  reason: completenessFailure,
1086
1277
  deterministicSelection: input.deterministicSelection,
1087
1278
  omissionCount: input.baselineOmissions.length +
@@ -1091,6 +1282,24 @@ async function resolveDelegateSelection(input) {
1091
1282
  observability: currentDelegateObservability(),
1092
1283
  });
1093
1284
  }
1285
+ // Ordered after the completeness gate: when a binding cap prunes every
1286
+ // candidate — including required focus — the lever-bearing diagnostic
1287
+ // above must win over this generic catch. An empty selection without
1288
+ // focus paths (or with only non-material delegate warnings) still lands
1289
+ // here as the terminal catch.
1290
+ if (finalSelection.selected.length === 0) {
1291
+ return handleDelegateRuntimeFailure({
1292
+ delegate,
1293
+ discoveryMetrics: input.discovery.metrics,
1294
+ reason: "finalizer produced empty local selection",
1295
+ deterministicSelection: input.deterministicSelection,
1296
+ omissionCount: input.baselineOmissions.length +
1297
+ input.discovery.omissions.length +
1298
+ input.deterministicSelection.omissions.length,
1299
+ candidateFallbackCount,
1300
+ observability: currentDelegateObservability(),
1301
+ });
1302
+ }
1094
1303
  const repairedSelection = repairDelegateSelection({
1095
1304
  curatorSelection: finalSelection,
1096
1305
  contentCandidates: input.discovery.content_candidates,
@@ -1102,6 +1311,7 @@ async function resolveDelegateSelection(input) {
1102
1311
  if (delegateEvidenceCoverageFailure != null) {
1103
1312
  return handleDelegateRuntimeFailure({
1104
1313
  delegate,
1314
+ discoveryMetrics: input.discovery.metrics,
1105
1315
  reason: `post-repair evidence coverage failed: ${delegateEvidenceCoverageFailure}`,
1106
1316
  deterministicSelection: input.deterministicSelection,
1107
1317
  omissionCount: input.baselineOmissions.length +
@@ -1111,6 +1321,29 @@ async function resolveDelegateSelection(input) {
1111
1321
  observability: currentDelegateObservability(),
1112
1322
  });
1113
1323
  }
1324
+ // D1 backstop: required-first reconstruction plus the completeness gate
1325
+ // above should make this unreachable. It fails closed if a server-side
1326
+ // change ever lets a non-degenerate required-focus file vanish while
1327
+ // non-required evidence stays selected.
1328
+ const finalizerEvictionFailure = finalizerRequiredFocusEvictionFailure({
1329
+ focusPaths: input.focusPaths,
1330
+ candidates: finalizerPacket.candidates,
1331
+ contentCandidates: input.discovery.content_candidates,
1332
+ selected: repairedSelection.selection.selected,
1333
+ });
1334
+ if (finalizerEvictionFailure != null) {
1335
+ return handleDelegateRuntimeFailure({
1336
+ delegate,
1337
+ discoveryMetrics: input.discovery.metrics,
1338
+ reason: `post-repair required-focus coverage failed: ${finalizerEvictionFailure}`,
1339
+ deterministicSelection: input.deterministicSelection,
1340
+ omissionCount: input.baselineOmissions.length +
1341
+ input.discovery.omissions.length +
1342
+ input.deterministicSelection.omissions.length,
1343
+ candidateFallbackCount,
1344
+ observability: currentDelegateObservability(),
1345
+ });
1346
+ }
1114
1347
  // Re-verify required-focus completeness against the actual returned
1115
1348
  // selection. This keeps the fallback contract tied to the final materialized
1116
1349
  // packet rather than the delegate's requested selection.
@@ -1122,6 +1355,7 @@ async function resolveDelegateSelection(input) {
1122
1355
  if (postRepairCoverageFailure != null) {
1123
1356
  return handleDelegateRuntimeFailure({
1124
1357
  delegate,
1358
+ discoveryMetrics: input.discovery.metrics,
1125
1359
  reason: `post-repair required-focus coverage failed: ${postRepairCoverageFailure}`,
1126
1360
  deterministicSelection: input.deterministicSelection,
1127
1361
  omissionCount: input.baselineOmissions.length +
@@ -1134,6 +1368,8 @@ async function resolveDelegateSelection(input) {
1134
1368
  const warnings = [
1135
1369
  ...curatorProposal.warnings.map((message) => delegateWarning("delegate_curator_warning", message)),
1136
1370
  ...finalizerProposal.warnings.map((message) => delegateWarning("delegate_finalizer_warning", message)),
1371
+ ...correctiveRetryWarnings("curator", curatorCall),
1372
+ ...correctiveRetryWarnings("finalizer", finalizerCall),
1137
1373
  ];
1138
1374
  if (foldedFinalizerSelection.foldedPaths.length > 0) {
1139
1375
  warnings.push(delegateWarning("delegate_finalizer_warning", `finalizer omitted requested local evidence; server folded it back into the selection: ${boundContextBuilderPathList(foldedFinalizerSelection.foldedPaths).join(", ")}`));
@@ -1143,8 +1379,15 @@ async function resolveDelegateSelection(input) {
1143
1379
  .map(formatOmissionForDelegateFailure)
1144
1380
  .join("; ")}`));
1145
1381
  }
1382
+ warnings.push(...finalizerDropProvenanceWarnings({
1383
+ finalizerSelected: finalizerProposal.selected,
1384
+ requiredFocus,
1385
+ foldedPaths: foldedFinalizerSelection.foldedPaths,
1386
+ reconstruction: finalReconstruction,
1387
+ }));
1146
1388
  warnings.push(...packetEvidenceWarnings);
1147
1389
  appendDelegateRepairWarnings(warnings, repairedSelection);
1390
+ warnings.push(...externalGapWarnings(externalGaps.external));
1148
1391
  const openQuestions = curatorProposal.open_questions.map((question) => sanitizeDiagnosticText(question));
1149
1392
  const omissionCount = input.baselineOmissions.length +
1150
1393
  input.discovery.omissions.length +
@@ -1154,12 +1397,18 @@ async function resolveDelegateSelection(input) {
1154
1397
  warnings,
1155
1398
  omissions: [],
1156
1399
  openQuestions,
1400
+ ...(externalGaps.external.length === 0
1401
+ ? {}
1402
+ : { knownMissingEvidence: externalGaps.external }),
1157
1403
  delegate: {
1158
1404
  enabled: true,
1159
- status: "succeeded",
1405
+ // partial WITHOUT fallback is the degrade signature: the curated
1406
+ // selection is used as-is, but conclusions resting on the reported
1407
+ // external gaps are conditional.
1408
+ status: externalGaps.external.length === 0 ? "succeeded" : "partial",
1160
1409
  failure_policy: delegate.failure_policy,
1161
1410
  fallback_used: false,
1162
- curator: { ...delegate.curator },
1411
+ curator: delegateProviderReceiptMetadata(delegate.curator),
1163
1412
  finalizer: delegateFinalizerReceiptMetadata(delegate.finalizer),
1164
1413
  finalizer_score: finalizerProposal.score,
1165
1414
  selected_count: repairedSelection.selection.selected.length,
@@ -1178,7 +1427,10 @@ async function resolveDelegateSelection(input) {
1178
1427
  }
1179
1428
  return handleDelegateRuntimeFailure({
1180
1429
  delegate,
1181
- reason: failureReasonFromError("finalizer_validation", err, `${delegatePromptDiagnostic(finalizerPacket)}${finalizerScoreDiagnostic(finalizerCall.json, delegate.finalizer.min_score)}`),
1430
+ discoveryMetrics: input.discovery.metrics,
1431
+ reason: err instanceof ContextBuilderFinalizerRejectionError
1432
+ ? formatFinalizerRejectionFailure(err, delegatePromptDiagnostic(finalizerPacket))
1433
+ : failureReasonFromError("finalizer_validation", err, `${delegatePromptDiagnostic(finalizerPacket)}${finalizerScoreDiagnostic(finalizerCall.json, delegate.finalizer.min_score)}`),
1182
1434
  deterministicSelection: input.deterministicSelection,
1183
1435
  omissionCount: input.baselineOmissions.length +
1184
1436
  input.discovery.omissions.length +
@@ -1391,11 +1643,14 @@ function fileCoversCandidate(file, candidate) {
1391
1643
  return file.ranges.some((range) => range.start_line <= 1 &&
1392
1644
  range.end_line >= Math.max(1, candidate.line_count));
1393
1645
  }
1646
+ const DISCOVERY_BYTE_CAP_FAILURE_SUFFIX = "; discovery_byte_cap_hit=true (discovery read cap reached; narrow roots/focus or add exclude_globs (workflow: cb_exclude_globs))";
1394
1647
  function handleDelegateRuntimeFailure(input) {
1395
1648
  // Delegate-failure reasons aggregate many per-entry details (each already
1396
1649
  // individually sanitized/redacted where assembled); the wider marked-
1397
1650
  // truncation bound keeps the tail from being silently dropped.
1398
- const failureReason = sanitizeDelegateFailureText(input.reason);
1651
+ const failureReason = sanitizeDelegateFailureText(input.discoveryMetrics?.discovery_byte_cap_hit === true
1652
+ ? `${input.reason}${DISCOVERY_BYTE_CAP_FAILURE_SUFFIX}`
1653
+ : input.reason);
1399
1654
  if (input.delegate.failure_policy === "fail") {
1400
1655
  const error = new ExecutionError(`context_builder delegate failed: ${failureReason}`);
1401
1656
  if (input.failureKind === "timeout") {
@@ -1416,7 +1671,7 @@ function handleDelegateRuntimeFailure(input) {
1416
1671
  status: "partial",
1417
1672
  failure_policy: input.delegate.failure_policy,
1418
1673
  fallback_used: true,
1419
- curator: { ...input.delegate.curator },
1674
+ curator: delegateProviderReceiptMetadata(input.delegate.curator),
1420
1675
  finalizer: delegateFinalizerReceiptMetadata(input.delegate.finalizer),
1421
1676
  selected_count: input.candidateFallbackCount,
1422
1677
  warning_count: warnings.length,
@@ -1527,6 +1782,45 @@ function delegateCompletenessFailure(input) {
1527
1782
  }
1528
1783
  return requiredFocusCoverageFailure(input);
1529
1784
  }
1785
+ // Feasibility dry-run with the exact post-finalizer reconstruction call (same
1786
+ // required-first admission and estimator), fed only the required ranges. Only
1787
+ // a BUDGET-explained miss fails here; anything else is left to the normal
1788
+ // post-finalizer gates so this check can never be stricter than they are.
1789
+ function requiredFocusPreDispatchFailure(input) {
1790
+ const requiredFocus = requiredFocusEvidenceSelections(input);
1791
+ if (requiredFocus.length === 0) {
1792
+ return undefined;
1793
+ }
1794
+ const dryRun = reconstructContextBuilderDelegateSelectionWithReport({
1795
+ selected: foldRequiredFocusEvidenceIntoSelection([], requiredFocus).selected,
1796
+ candidates: input.candidates,
1797
+ slices: input.discovery.slices,
1798
+ tree: input.discovery.tree,
1799
+ search: input.discovery.search,
1800
+ budget: input.budget,
1801
+ requiredFocus: new Map(requiredFocus.map((required) => [required.candidate_id, required.ranges])),
1802
+ });
1803
+ const selectedByCandidate = new Map();
1804
+ for (const selection of materializeFinalizerSelection(dryRun.selection.selected, input.candidates)) {
1805
+ selectedByCandidate.set(selection.candidate_id, [
1806
+ ...(selectedByCandidate.get(selection.candidate_id) ?? []),
1807
+ ...selection.ranges,
1808
+ ]);
1809
+ }
1810
+ const missing = requiredFocus.filter((required) => {
1811
+ const selectedRanges = selectedByCandidate.get(required.candidate_id) ?? [];
1812
+ return required.ranges.some((range) => !rangeCoveredByRanges(range, selectedRanges));
1813
+ });
1814
+ if (missing.length === 0) {
1815
+ return undefined;
1816
+ }
1817
+ return formatRequiredFocusBudgetFailure(missing, input.candidates, {
1818
+ omissions: dryRun.selection.omissions,
1819
+ foldedPaths: [],
1820
+ budget: input.budget,
1821
+ preDispatch: true,
1822
+ });
1823
+ }
1530
1824
  function requiredFocusCoverageFailure(input) {
1531
1825
  const required = requiredFocusEvidenceSelections(input);
1532
1826
  if (required.length === 0) {
@@ -1546,12 +1840,163 @@ function requiredFocusCoverageFailure(input) {
1546
1840
  if (missing.length === 0) {
1547
1841
  return undefined;
1548
1842
  }
1843
+ const budgetFailure = input.budgetDiagnostics == null
1844
+ ? undefined
1845
+ : formatRequiredFocusBudgetFailure(missing, input.candidates, input.budgetDiagnostics);
1846
+ if (budgetFailure != null) {
1847
+ return budgetFailure;
1848
+ }
1549
1849
  return `delegate finalizer omitted requested local evidence: ${missing
1550
1850
  .map((selection) => `${selection.path}#${selection.ranges
1551
1851
  .map((range) => `${range.start_line}-${range.end_line}`)
1552
1852
  .join(",")}`)
1553
1853
  .join("; ")}`;
1554
1854
  }
1855
+ // Public budget key a caller raises for each local reconstruction cap.
1856
+ // target_estimated_tokens is derived (0.9 x max_estimated_tokens), so its
1857
+ // lever is the max key.
1858
+ const REQUIRED_FOCUS_BUDGET_LEVER_KEYS = {
1859
+ max_files: "max_files",
1860
+ max_bytes_per_file: "max_bytes_per_file",
1861
+ max_total_bytes: "max_total_bytes",
1862
+ target_estimated_tokens: "max_estimated_tokens",
1863
+ };
1864
+ // Lever-first failure text for required focus that does not fit the local
1865
+ // reconstruction budget on its own. The grep-stable prefix leads, the binding
1866
+ // cap(s) and levers follow immediately (sanitizeDelegateFailureText keeps the
1867
+ // head), and the per-path causes trail with marked truncation. Returns
1868
+ // undefined when no missing path is explained by a budget omission, so the
1869
+ // caller keeps the legacy range-coverage message.
1870
+ function formatRequiredFocusBudgetFailure(missing, candidates, diagnostics) {
1871
+ const rootByCandidate = new Map(candidates.map((candidate) => [candidate.id, candidate.root_index]));
1872
+ const omissionReasonByKey = new Map();
1873
+ for (const omission of diagnostics.omissions) {
1874
+ for (const path of omission.paths ?? []) {
1875
+ const key = `${omission.root_index ?? 0}:${path}`;
1876
+ if (!omissionReasonByKey.has(key)) {
1877
+ omissionReasonByKey.set(key, omission.reason);
1878
+ }
1879
+ }
1880
+ }
1881
+ const folded = new Set(diagnostics.foldedPaths);
1882
+ const bindingCaps = [];
1883
+ const entries = missing.map((selection) => {
1884
+ const reason = omissionReasonByKey.get(`${rootByCandidate.get(selection.candidate_id) ?? 0}:${selection.path}`);
1885
+ if (reason != null && !bindingCaps.includes(reason)) {
1886
+ bindingCaps.push(reason);
1887
+ }
1888
+ const ranges = selection.ranges
1889
+ .map((range) => `${range.start_line}-${range.end_line}`)
1890
+ .join(",");
1891
+ const provenance = diagnostics.preDispatch === true
1892
+ ? "required focus"
1893
+ : folded.has(selection.path)
1894
+ ? "omitted by finalizer, server-folded"
1895
+ : "selected by finalizer";
1896
+ const cause = reason == null
1897
+ ? `${provenance}; required ranges uncovered`
1898
+ : `${provenance}; pruned by ${reason}`;
1899
+ return `${selection.path}#${ranges} (${cause})`;
1900
+ });
1901
+ if (bindingCaps.length === 0) {
1902
+ return undefined;
1903
+ }
1904
+ // Advising "raise" for a key already at its public hard maximum sends the
1905
+ // caller into a guaranteed -32602; only focus reduction can help there.
1906
+ const raisable = [];
1907
+ const atHardMax = [];
1908
+ for (const cap of bindingCaps) {
1909
+ const key = REQUIRED_FOCUS_BUDGET_LEVER_KEYS[cap] ?? cap;
1910
+ const current = budgetValue(diagnostics.budget, key);
1911
+ const hardMax = hardLimitValue(key);
1912
+ if (current != null && hardMax != null && current >= hardMax) {
1913
+ atHardMax.push(`budget.${key} is already at its hard maximum (${current})`);
1914
+ }
1915
+ else {
1916
+ // " / " would read as an absolute path to the diagnostic redactor.
1917
+ raisable.push(`budget.${key} (workflow: cb_budget_${key})`);
1918
+ }
1919
+ }
1920
+ const leverText = atHardMax.length === 0
1921
+ ? `raise ${raisable.join(" and ")} or remove the path from focus_paths (workflow: cb_focus_paths)`
1922
+ : `${raisable.length === 0 ? "" : `raise ${raisable.join(" and ")}; `}${atHardMax.join("; ")}; reduce or split focus_paths (workflow: cb_focus_paths)`;
1923
+ const shown = entries.slice(0, DELEGATE_FAILURE_MAX_OMISSION_ENTRIES);
1924
+ const droppedSuffix = entries.length > shown.length
1925
+ ? ` (+${entries.length - shown.length} more)`
1926
+ : "";
1927
+ return `delegate finalizer omitted requested local evidence: required focus alone exceeds the local reconstruction budget${diagnostics.preDispatch === true ? " (pre-dispatch)" : ""} (binding cap: ${bindingCaps.join(", ")}); ${leverText}. ${entries.length} required path(s) not retained: ${shown.join("; ")}${droppedSuffix}`;
1928
+ }
1929
+ function budgetValue(budget, key) {
1930
+ const value = budget?.[key];
1931
+ return typeof value === "number" ? value : undefined;
1932
+ }
1933
+ function hardLimitValue(key) {
1934
+ const value = CONTEXT_BUILDER_PUBLIC_BUDGET_HARD_LIMITS[key];
1935
+ return typeof value === "number" ? value : undefined;
1936
+ }
1937
+ // Bounded per-path provenance for the finalizer success path. Warnings are
1938
+ // capped at the diagnostic text length, so each path gets its own entry. The
1939
+ // wording deliberately avoids the omission/range vocabulary that
1940
+ // reconcileContextBuilderWarnings treats as stale for selected paths.
1941
+ const FINALIZER_DROP_PROVENANCE_MAX_WARNINGS = 12;
1942
+ function finalizerDropProvenanceWarnings(input) {
1943
+ const finalizerSelectedIds = new Set(input.finalizerSelected.map((selection) => selection.candidate_id));
1944
+ const folded = new Set(input.foldedPaths);
1945
+ const entries = [];
1946
+ for (const required of input.requiredFocus) {
1947
+ if (!folded.has(required.path)) {
1948
+ continue;
1949
+ }
1950
+ entries.push(`${required.path} = ${finalizerSelectedIds.has(required.candidate_id)
1951
+ ? "finalizer_range_narrowed"
1952
+ : "finalizer_unselected"} (server-folded)`);
1953
+ }
1954
+ for (const omission of input.reconstruction.selection.omissions) {
1955
+ for (const path of omission.paths ?? []) {
1956
+ entries.push(`${path} = budget:${omission.reason}`);
1957
+ }
1958
+ }
1959
+ for (const item of input.reconstruction.compacted) {
1960
+ entries.push(`${item.path} = compacted_to_required_ranges`);
1961
+ }
1962
+ const shown = entries.slice(0, FINALIZER_DROP_PROVENANCE_MAX_WARNINGS);
1963
+ const warnings = shown.map((entry) => delegateWarning("delegate_finalizer_warning", `finalizer drop provenance: ${entry}`));
1964
+ if (entries.length > shown.length) {
1965
+ warnings.push(delegateWarning("delegate_finalizer_warning", `finalizer drop provenance: +${entries.length - shown.length} more path(s) not itemized`));
1966
+ }
1967
+ return warnings;
1968
+ }
1969
+ // Finalizer-path D1 backstop, mirroring postRepairRequiredFocusEvictionFailure
1970
+ // for the curator-only branch: a non-degenerate required-focus file must never
1971
+ // be absent from the returned selection while any non-required candidate is
1972
+ // still selected. A whitespace-only required file is exempt (repair drops
1973
+ // degenerate slices; the range re-verify still reports it).
1974
+ function finalizerRequiredFocusEvictionFailure(input) {
1975
+ const requiredKeys = new Set(requiredFocusEvidenceSelections(input).flatMap((required) => {
1976
+ const candidate = input.candidates.find((item) => item.id === required.candidate_id);
1977
+ return candidate == null ? [] : [`${candidate.root_index}:${candidate.path}`];
1978
+ }));
1979
+ if (requiredKeys.size === 0) {
1980
+ return undefined;
1981
+ }
1982
+ const fileKey = (file) => `${file.root_index}:${file.path}`;
1983
+ const selectedKeys = new Set(input.selected.filter((file) => file.ranges.length > 0).map(fileKey));
1984
+ if (![...selectedKeys].some((key) => !requiredKeys.has(key))) {
1985
+ return undefined;
1986
+ }
1987
+ const contentByKey = new Map(input.contentCandidates.map((candidate) => [fileKey(candidate), candidate.content]));
1988
+ const evicted = [...requiredKeys].filter((key) => !selectedKeys.has(key) && (contentByKey.get(key) ?? "x").trim().length > 0);
1989
+ if (evicted.length === 0) {
1990
+ return undefined;
1991
+ }
1992
+ return `finalizer selection evicted required-focus evidence while non-required evidence remained selected: ${evicted
1993
+ .map((key) => {
1994
+ const separator = key.indexOf(":");
1995
+ return `root ${key.slice(0, separator)}: ${key.slice(separator + 1)}`;
1996
+ })
1997
+ .join("; ")}`;
1998
+ }
1999
+ export const finalizerRequiredFocusEvictionFailureForTest = finalizerRequiredFocusEvictionFailure;
1555
2000
  // Curator-only post-repair guard. Unlike the finalizer branch, the curator-only
1556
2001
  // path does not fold required-focus ranges, so its baseline is the curator's
1557
2002
  // own pre-repair selection. Repair is allowed to:
@@ -1722,6 +2167,9 @@ function isSameRootRelativePathOrDescendant(candidatePath, parentPath) {
1722
2167
  }
1723
2168
  return candidatePath === parentPath || candidatePath.startsWith(`${parentPath}/`);
1724
2169
  }
2170
+ export function isMaterialDelegateEvidenceWarningForTest(message) {
2171
+ return isMaterialDelegateEvidenceWarning(message);
2172
+ }
1725
2173
  function isMaterialDelegateEvidenceWarning(message) {
1726
2174
  const text = message.toLowerCase();
1727
2175
  if (isNonBlockingDelegateWarning(text)) {
@@ -1881,6 +2329,53 @@ function packetCoverageObligationRules(packet) {
1881
2329
  "- 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
2330
  ];
1883
2331
  }
2332
+ /**
2333
+ * Renders the caller-anchored required-focus contract.
2334
+ *
2335
+ * Rendered only when the packet carries a candidate with focus_requested:true
2336
+ * and non-empty required_focus_ranges, i.e. exactly the candidates
2337
+ * requiredFocusEvidenceSelections enforces. Without it the finalizer had no
2338
+ * signal that anchored files are mandatory and deduplicated near-duplicate
2339
+ * anchors as a quality judgment.
2340
+ *
2341
+ * Phrasing guard: the finalizer may echo these lines into `warnings`, which
2342
+ * isMaterialDelegateEvidenceWarning scans with a negation-blind proximity
2343
+ * match. Keep gap vocabulary (missing, omitted, unavailable, not selected, ...)
2344
+ * out of these lines; tests/context-builder/delegate-prompt.test.ts runs every
2345
+ * rendered line through that matcher.
2346
+ */
2347
+ export function packetRequiredFocusRules(stage, packet) {
2348
+ if (packet == null || typeof packet !== "object") {
2349
+ return [];
2350
+ }
2351
+ const candidates = packet["candidates"];
2352
+ const hasRequiredFocus = Array.isArray(candidates) &&
2353
+ candidates.some((candidate) => {
2354
+ if (candidate == null || typeof candidate !== "object") {
2355
+ return false;
2356
+ }
2357
+ const record = candidate;
2358
+ const ranges = record["required_focus_ranges"];
2359
+ return (record["focus_requested"] === true &&
2360
+ Array.isArray(ranges) &&
2361
+ ranges.length > 0);
2362
+ });
2363
+ if (!hasRequiredFocus) {
2364
+ return [];
2365
+ }
2366
+ if (stage === "curator") {
2367
+ return [
2368
+ "- Candidates with focus_requested:true and a non-empty required_focus_ranges are caller-anchored: include each one in selected with ranges covering its required_focus_ranges, and do not deduplicate them.",
2369
+ ];
2370
+ }
2371
+ return [
2372
+ "Caller-anchored focus (mandatory, server-enforced):",
2373
+ "- Every packet.candidates entry with focus_requested:true and a non-empty required_focus_ranges MUST appear in selected, with ranges that cover every one of its required_focus_ranges entries.",
2374
+ "- You may add ranges to those candidates. Never drop one of them and never narrow its ranges below its required_focus_ranges.",
2375
+ "- Do not deduplicate those candidates, even when two of them look like near-duplicates (for example the same document at two paths). Keep all of them.",
2376
+ "- The server folds a dropped anchored candidate back into selected and admits anchored candidates before any other candidate, so dropping one never removes it; it only displaces lower-priority evidence from the local budget.",
2377
+ ];
2378
+ }
1884
2379
  /**
1885
2380
  * Renders the finalizer scoring contract.
1886
2381
  *
@@ -1947,6 +2442,23 @@ function finalizerExampleScore(minScore) {
1947
2442
  // `finalizer_score` observability. A higher exemplar always clears a lower gate.
1948
2443
  return minScore == null || minScore <= 0 ? 9 : minScore;
1949
2444
  }
2445
+ // Role prompts (conf/prompts) tell a reviewer to inspect files and to reject
2446
+ // work lacking regression tests; a finalizer obeying that either stalls on
2447
+ // tool use or vetoes a bounded packet for evidence it was never meant to hold.
2448
+ // Rendered for the finalizer only — the curator has no veto to misuse.
2449
+ const FINALIZER_ROLE_OVERLAY = "These finalizer rules override any role instruction to read or inspect files or to reject packets missing regression tests: do not read files; the packet is the only evidence. Report task-relevant evidence absent from packet.candidates in external_gaps, not as grounds for rejection.";
2450
+ function finalizerExternalGapRules(policy) {
2451
+ return [
2452
+ "External gaps:",
2453
+ `- Evidence the task needs that is ABSENT from packet.candidates goes in external_gaps as [{"path":"<root-relative path>","reason":"<why the task needs it>"}] (at most ${MAX_FINALIZER_EXTERNAL_GAPS} entries). Never lower score for it.`,
2454
+ "- Never list a path that is in packet.candidates or under a focus path in external_gaps; the server verifies every entry and rejects the whole answer if one is wrong.",
2455
+ "- Never list a directory that contains a packet.candidates path or a focus path; name the specific missing file instead.",
2456
+ "- If evidence present in packet.candidates is insufficient, that is still approved:false with concrete omissions, not an external gap.",
2457
+ policy === "degrade"
2458
+ ? "- Absent evidence reported in external_gaps is NOT grounds for approved:false; the server delivers the selection with those gaps marked as conditional."
2459
+ : "- The server will fail this request if external_gaps is non-empty; report absent evidence there honestly anyway rather than hiding it.",
2460
+ ];
2461
+ }
1950
2462
  export function renderDelegatePrompt(stage, packet, options) {
1951
2463
  // Diagnostic-array exemplars are non-empty on purpose: empty arrays cannot
1952
2464
  // communicate element types, and a live finalizer failure returned
@@ -1954,7 +2466,7 @@ export function renderDelegatePrompt(stage, packet, options) {
1954
2466
  // show each element is one plain string.
1955
2467
  const outputShape = stage === "curator"
1956
2468
  ? '{"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>"]}`;
2469
+ : `{"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>"],"external_gaps":[]}`;
1958
2470
  const stageRules = stage === "curator"
1959
2471
  ? [
1960
2472
  "Curator-specific rules:",
@@ -1978,12 +2490,10 @@ export function renderDelegatePrompt(stage, packet, options) {
1978
2490
  "- 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.",
1979
2491
  "- 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.",
1980
2492
  "- 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.",
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.",
2493
+ // Evidence absent from the packet is routed to external_gaps (see
2494
+ // finalizerExternalGapRules), not warnings: warnings are scanned by
2495
+ // isMaterialEvidenceGapWarning and a gap phrased there hard-fails.
2496
+ "- If approving with score at or above the requested minimum, omissions MUST be [].",
1987
2497
  // The observed live schema failure was object-shaped `omissions`;
1988
2498
  // state the element type explicitly instead of relying on exemplars.
1989
2499
  // (No open_questions here: the finalizer schema is strict and would
@@ -1994,6 +2504,7 @@ export function renderDelegatePrompt(stage, packet, options) {
1994
2504
  return [
1995
2505
  `You are the context_builder ${stage}.`,
1996
2506
  `Prompt version: ${CONTEXT_BUILDER_DELEGATE_PROMPT_VERSION}.`,
2507
+ ...(stage === "finalizer" ? [FINALIZER_ROLE_OVERLAY] : []),
1997
2508
  "Return exactly one strict JSON object and no prose.",
1998
2509
  "For this tool call, the JSON packet below is the only source of truth.",
1999
2510
  stage === "curator"
@@ -2005,7 +2516,11 @@ export function renderDelegatePrompt(stage, packet, options) {
2005
2516
  ? "Use the excerpt, search_hits, deterministic_selection, omissions, and task fields as evidence; the packet intentionally omits full source content."
2006
2517
  : "Use the excerpt, search_hits, curator_proposal, deterministic_selection, omissions, and task fields as evidence; the packet intentionally omits full source content.",
2007
2518
  ...stageRules,
2519
+ ...packetRequiredFocusRules(stage, packet),
2008
2520
  ...(stage === "finalizer" ? finalizerScoringRules(options?.minScore) : []),
2521
+ ...(stage === "finalizer"
2522
+ ? finalizerExternalGapRules(options?.externalGapPolicy ?? "fail")
2523
+ : []),
2009
2524
  ...packetCoverageObligationRules(packet),
2010
2525
  ...(minimumOutput == null
2011
2526
  ? []
@@ -2200,6 +2715,7 @@ function buildMinimumDelegateOutput(stage, packet, minScore) {
2200
2715
  "minimum curator proposal selection used; review packet evidence for more context",
2201
2716
  ],
2202
2717
  omissions: [],
2718
+ external_gaps: [],
2203
2719
  };
2204
2720
  return JSON.stringify(output);
2205
2721
  }
@@ -2362,12 +2878,115 @@ function finalizerScoreDiagnostic(json, minScore) {
2362
2878
  ? `; finalizer_score=${rawScore}; min_score=${minScore}`
2363
2879
  : `; min_score=${minScore}`;
2364
2880
  }
2881
+ const FINALIZER_REJECTION_LEVERS = 'levers: add missing path(s) to focus_paths (workflow: cb_focus_paths); exclude vendor/generated dirs via exclude_globs (workflow: cb_exclude_globs); or set external_gap_policy:"degrade" (workflow: cb_delegate.external_gap_policy)';
2882
+ // Verdict-first text for a finalizer veto: what the finalizer said (approval,
2883
+ // its own omission list) leads, because that is what the caller acts on; the
2884
+ // score and packet diagnostics trail. Each omission is provider text, so it is
2885
+ // redacted on its own before joining — the whole-string redactor would
2886
+ // otherwise blank the entire reason when any single entry carries a secret.
2887
+ function formatFinalizerRejectionFailure(err, packetDiagnostic) {
2888
+ const lead = err.reasonKind === "score"
2889
+ ? `score=${err.score} below min_score=${err.minScore}`
2890
+ : `approved=${err.approved}`;
2891
+ const entries = err.reasonKind === "material_warning"
2892
+ ? { label: "material_warnings", values: err.materialWarnings }
2893
+ : { label: "omissions", values: err.omissions };
2894
+ const shown = entries.values
2895
+ .slice(0, DELEGATE_FAILURE_MAX_OMISSION_ENTRIES)
2896
+ .map((entry) => sanitizeDelegateFailureText(entry, DELEGATE_FAILURE_MAX_ENTRY_TEXT_LENGTH));
2897
+ const dropped = entries.values.length - shown.length;
2898
+ const entryText = shown.length === 0
2899
+ ? `${entries.label}=0`
2900
+ : `${entries.label}=${entries.values.length}: ${shown.join("; ")}${dropped > 0 ? ` (+${dropped} more omissions)` : ""}`;
2901
+ return [
2902
+ `finalizer_validation: ${lead}`,
2903
+ entryText,
2904
+ `finalizer_score=${err.score}`,
2905
+ `min_score=${err.minScore}`,
2906
+ FINALIZER_REJECTION_LEVERS,
2907
+ packetDiagnostic,
2908
+ err.message,
2909
+ ].join("; ");
2910
+ }
2911
+ const EXTERNAL_GAP_POLICY_LEVERS = 'levers: add path(s) to focus_paths (workflow: cb_focus_paths) or set external_gap_policy:"degrade" (workflow: cb_delegate.external_gap_policy)';
2912
+ function formatExternalGapEntries(entries) {
2913
+ const shown = entries
2914
+ .slice(0, DELEGATE_FAILURE_MAX_OMISSION_ENTRIES)
2915
+ .map((entry) => sanitizeDelegateFailureText(`${entry.path} (${entry.detail})`, DELEGATE_FAILURE_MAX_ENTRY_TEXT_LENGTH));
2916
+ const dropped = entries.length - shown.length;
2917
+ return `${entries.length}: ${shown.join("; ")}${dropped > 0 ? ` (+${dropped} more external gaps)` : ""}`;
2918
+ }
2919
+ function formatExternalGapRejectionFailure(rejected) {
2920
+ return [
2921
+ `finalizer_validation: external_gaps rejected=${formatExternalGapEntries(rejected.map((gap) => ({ path: gap.path, detail: gap.why })))}`,
2922
+ FINALIZER_REJECTION_LEVERS,
2923
+ ].join("; ");
2924
+ }
2925
+ function formatExternalGapPolicyFailure(external) {
2926
+ return [
2927
+ `finalizer_validation: external_gap_policy=fail; external_gaps=${formatExternalGapEntries(external.map((gap) => ({ path: gap.path, detail: gap.provenance })))}`,
2928
+ EXTERNAL_GAP_POLICY_LEVERS,
2929
+ ].join("; ");
2930
+ }
2931
+ // Wording deliberately avoids the truncation/omission vocabulary that
2932
+ // reconcileContextBuilderWarnings filters as stale; the codes are also
2933
+ // exempted there because the per-gap reason is provider prose.
2934
+ function externalGapWarnings(external) {
2935
+ if (external.length === 0) {
2936
+ return [];
2937
+ }
2938
+ return [
2939
+ delegateWarning("delegate_external_gaps", `finalizer reported ${external.length} evidence gap(s) outside the delegate packet; conclusions depending on them are conditional`),
2940
+ ...external.map((gap) => delegateWarning("delegate_external_gap", `external evidence gap: ${gap.path} (${gap.provenance}): ${gap.reason}`)),
2941
+ ];
2942
+ }
2365
2943
  function failureReasonFromError(stage, err, diagnostic) {
2366
2944
  const message = err instanceof Error ? err.message : "unknown delegate error";
2367
2945
  return diagnostic == null
2368
2946
  ? `${stage}: ${message}`
2369
2947
  : `${stage}: ${diagnostic}; ${message}`;
2370
2948
  }
2949
+ // P1b observability. The packet serializes a sliced obligation set while
2950
+ // terminal coverage evaluates the full one; name the gap so a succeeded
2951
+ // delegate next to degraded coverage is explainable. Wording avoids the
2952
+ // truncation/omission vocabulary reconcileContextBuilderWarnings filters.
2953
+ function packetObligationTruncationWarning(fullObligations, packetPolicy) {
2954
+ const counts = countDelegatePacketObligationTruncation(fullObligations, packetPolicy);
2955
+ if (counts == null) {
2956
+ return undefined;
2957
+ }
2958
+ return {
2959
+ code: "delegate_packet_obligations_truncated",
2960
+ message: `delegate packet carried ${counts.targets_included} of ${counts.targets_total} review obligation targets (${counts.ranges_included} of ${counts.ranges_total} ranges, ${counts.regression_links_included} of ${counts.regression_links_total} regression links); final coverage evaluates the full set`,
2961
+ };
2962
+ }
2963
+ export const packetObligationTruncationWarningForTest = packetObligationTruncationWarning;
2964
+ function coverageDegradedAfterDelegateWarning(coverage, delegate) {
2965
+ if (coverage.status !== "degraded" ||
2966
+ delegate?.status !== "succeeded" ||
2967
+ delegate.fallback_used) {
2968
+ return undefined;
2969
+ }
2970
+ const unclassifiableRegression = coverage.missing_required.includes("regression_test") &&
2971
+ coverage.unclassifiable.includes("regression_test");
2972
+ return {
2973
+ code: "coverage_degraded_after_delegate",
2974
+ message: `delegate succeeded but final coverage is degraded; unmet required classes: ${coverage.missing_required.join(", ") || "none"}; unclassifiable regression: ${unclassifiableRegression
2975
+ ? "yes (no confident regression link, so no selection could satisfy it)"
2976
+ : "no"}`,
2977
+ };
2978
+ }
2979
+ const DELEGATE_JSON_CORRECTIVE_RETRY_WARNING_CODE = "delegate_json_corrective_retry";
2980
+ // Informational only: the retried call produced valid JSON, so the run is not
2981
+ // degraded; the warning keeps the recovered formatting failure observable.
2982
+ function correctiveRetryWarnings(stage, call) {
2983
+ if (call.corrective_retry?.outcome !== "succeeded") {
2984
+ return [];
2985
+ }
2986
+ return [
2987
+ delegateWarning(DELEGATE_JSON_CORRECTIVE_RETRY_WARNING_CODE, `${stage} returned malformed JSON once; one corrective retry succeeded`),
2988
+ ];
2989
+ }
2371
2990
  function delegateWarning(code, message) {
2372
2991
  return {
2373
2992
  code: sanitizeDiagnosticText(code),
@@ -2423,6 +3042,15 @@ function reconcileContextBuilderWarnings(input) {
2423
3042
  return [];
2424
3043
  }
2425
3044
  return input.warnings.filter((warning) => {
3045
+ // External-gap warnings name paths the packet never carried, so they can
3046
+ // never be stale against the selection; their reason text is provider
3047
+ // prose and may happen to use omission vocabulary. The corrective-retry
3048
+ // notice names no evidence at all, so it is never stale either.
3049
+ if (warning.code === "delegate_external_gaps" ||
3050
+ warning.code === "delegate_external_gap" ||
3051
+ warning.code === DELEGATE_JSON_CORRECTIVE_RETRY_WARNING_CODE) {
3052
+ return true;
3053
+ }
2426
3054
  const text = warning.message.toLowerCase();
2427
3055
  if (isStaleTruncationWarning(text, input)) {
2428
3056
  return false;