@cleocode/core 2026.10.27 → 2026.11.1-canary.3

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 (520) hide show
  1. package/README.md +2 -0
  2. package/binaries/README.md +30 -10
  3. package/dist/.source-stamp.json +4 -0
  4. package/dist/audit/reconstruct.d.ts.map +1 -1
  5. package/dist/audit/reconstruct.js +3 -13
  6. package/dist/audit/reconstruct.js.map +1 -1
  7. package/dist/build-provenance.json +2 -2
  8. package/dist/cloud/nexus-cloud.d.ts.map +1 -1
  9. package/dist/cloud/nexus-cloud.js +3 -3
  10. package/dist/cloud/nexus-cloud.js.map +1 -1
  11. package/dist/cloud/nexus-device.d.ts +1 -1
  12. package/dist/cloud/nexus-link.d.ts +18 -2
  13. package/dist/cloud/nexus-link.d.ts.map +1 -1
  14. package/dist/cloud/nexus-link.js +26 -6
  15. package/dist/cloud/nexus-link.js.map +1 -1
  16. package/dist/cloud/nexus-vault.d.ts.map +1 -1
  17. package/dist/cloud/nexus-vault.js +3 -9
  18. package/dist/cloud/nexus-vault.js.map +1 -1
  19. package/dist/decide/sites/registry.d.ts +40 -0
  20. package/dist/decide/sites/registry.d.ts.map +1 -1
  21. package/dist/decide/sites/registry.js +38 -0
  22. package/dist/decide/sites/registry.js.map +1 -1
  23. package/dist/docs/adr-allocator.d.ts +3 -3
  24. package/dist/docs/adr-allocator.d.ts.map +1 -1
  25. package/dist/docs/adr-allocator.js +8 -5
  26. package/dist/docs/adr-allocator.js.map +1 -1
  27. package/dist/docs/blob-consolidation.d.ts.map +1 -1
  28. package/dist/docs/blob-consolidation.js +3 -2
  29. package/dist/docs/blob-consolidation.js.map +1 -1
  30. package/dist/docs/derive-links.d.ts +12 -0
  31. package/dist/docs/derive-links.d.ts.map +1 -1
  32. package/dist/docs/derive-links.js +19 -0
  33. package/dist/docs/derive-links.js.map +1 -1
  34. package/dist/docs/doc-name-rule.d.ts +105 -0
  35. package/dist/docs/doc-name-rule.d.ts.map +1 -0
  36. package/dist/docs/doc-name-rule.js +212 -0
  37. package/dist/docs/doc-name-rule.js.map +1 -0
  38. package/dist/docs/docs-inconsistency-detector.d.ts.map +1 -1
  39. package/dist/docs/docs-inconsistency-detector.js +4 -3
  40. package/dist/docs/docs-inconsistency-detector.js.map +1 -1
  41. package/dist/docs/docs-ops.d.ts +27 -5
  42. package/dist/docs/docs-ops.d.ts.map +1 -1
  43. package/dist/docs/docs-ops.js +149 -17
  44. package/dist/docs/docs-ops.js.map +1 -1
  45. package/dist/docs/docs-read-model.d.ts +45 -15
  46. package/dist/docs/docs-read-model.d.ts.map +1 -1
  47. package/dist/docs/docs-read-model.js +22 -102
  48. package/dist/docs/docs-read-model.js.map +1 -1
  49. package/dist/docs/docs-update.d.ts.map +1 -1
  50. package/dist/docs/docs-update.js +8 -2
  51. package/dist/docs/docs-update.js.map +1 -1
  52. package/dist/docs/doctor.d.ts +71 -4
  53. package/dist/docs/doctor.d.ts.map +1 -1
  54. package/dist/docs/doctor.js +324 -23
  55. package/dist/docs/doctor.js.map +1 -1
  56. package/dist/docs/duplicate-check.d.ts +138 -0
  57. package/dist/docs/duplicate-check.d.ts.map +1 -0
  58. package/dist/docs/duplicate-check.js +286 -0
  59. package/dist/docs/duplicate-check.js.map +1 -0
  60. package/dist/docs/fetch-result.d.ts.map +1 -1
  61. package/dist/docs/fetch-result.js +27 -0
  62. package/dist/docs/fetch-result.js.map +1 -1
  63. package/dist/docs/import/scanner.d.ts +2 -0
  64. package/dist/docs/import/scanner.d.ts.map +1 -1
  65. package/dist/docs/import/scanner.js +3 -1
  66. package/dist/docs/import/scanner.js.map +1 -1
  67. package/dist/docs/lineage.d.ts +66 -0
  68. package/dist/docs/lineage.d.ts.map +1 -0
  69. package/dist/docs/lineage.js +190 -0
  70. package/dist/docs/lineage.js.map +1 -0
  71. package/dist/docs/modernize/apply.d.ts +139 -0
  72. package/dist/docs/modernize/apply.d.ts.map +1 -0
  73. package/dist/docs/modernize/apply.js +760 -0
  74. package/dist/docs/modernize/apply.js.map +1 -0
  75. package/dist/docs/modernize/guard.d.ts +92 -0
  76. package/dist/docs/modernize/guard.d.ts.map +1 -0
  77. package/dist/docs/modernize/guard.js +214 -0
  78. package/dist/docs/modernize/guard.js.map +1 -0
  79. package/dist/docs/modernize/index.d.ts +68 -0
  80. package/dist/docs/modernize/index.d.ts.map +1 -0
  81. package/dist/docs/modernize/index.js +97 -0
  82. package/dist/docs/modernize/index.js.map +1 -0
  83. package/dist/docs/modernize/judge.d.ts +85 -0
  84. package/dist/docs/modernize/judge.d.ts.map +1 -0
  85. package/dist/docs/modernize/judge.js +186 -0
  86. package/dist/docs/modernize/judge.js.map +1 -0
  87. package/dist/docs/modernize/plan.d.ts +77 -0
  88. package/dist/docs/modernize/plan.d.ts.map +1 -0
  89. package/dist/docs/modernize/plan.js +510 -0
  90. package/dist/docs/modernize/plan.js.map +1 -0
  91. package/dist/docs/modernize/restore.d.ts +41 -0
  92. package/dist/docs/modernize/restore.d.ts.map +1 -0
  93. package/dist/docs/modernize/restore.js +103 -0
  94. package/dist/docs/modernize/restore.js.map +1 -0
  95. package/dist/docs/modernize/rules.d.ts +197 -0
  96. package/dist/docs/modernize/rules.d.ts.map +1 -0
  97. package/dist/docs/modernize/rules.js +510 -0
  98. package/dist/docs/modernize/rules.js.map +1 -0
  99. package/dist/docs/modernize/store.d.ts +64 -0
  100. package/dist/docs/modernize/store.d.ts.map +1 -0
  101. package/dist/docs/modernize/store.js +390 -0
  102. package/dist/docs/modernize/store.js.map +1 -0
  103. package/dist/docs/publications-store.d.ts +16 -2
  104. package/dist/docs/publications-store.d.ts.map +1 -1
  105. package/dist/docs/publications-store.js +23 -2
  106. package/dist/docs/publications-store.js.map +1 -1
  107. package/dist/docs/publish-pr.d.ts.map +1 -1
  108. package/dist/docs/publish-pr.js +12 -1
  109. package/dist/docs/publish-pr.js.map +1 -1
  110. package/dist/docs/rot.d.ts +94 -0
  111. package/dist/docs/rot.d.ts.map +1 -0
  112. package/dist/docs/rot.js +298 -0
  113. package/dist/docs/rot.js.map +1 -0
  114. package/dist/docs/similarity-check.d.ts +2 -0
  115. package/dist/docs/similarity-check.d.ts.map +1 -1
  116. package/dist/docs/similarity-check.js +6 -49
  117. package/dist/docs/similarity-check.js.map +1 -1
  118. package/dist/docs/slug-rename.d.ts +84 -0
  119. package/dist/docs/slug-rename.d.ts.map +1 -0
  120. package/dist/docs/slug-rename.js +166 -0
  121. package/dist/docs/slug-rename.js.map +1 -0
  122. package/dist/docs/slug-similarity.d.ts +24 -0
  123. package/dist/docs/slug-similarity.d.ts.map +1 -0
  124. package/dist/docs/slug-similarity.js +63 -0
  125. package/dist/docs/slug-similarity.js.map +1 -0
  126. package/dist/docs/wikilinks.d.ts +20 -4
  127. package/dist/docs/wikilinks.d.ts.map +1 -1
  128. package/dist/docs/wikilinks.js +52 -7
  129. package/dist/docs/wikilinks.js.map +1 -1
  130. package/dist/docs/writer-registry.d.ts.map +1 -1
  131. package/dist/docs/writer-registry.js +33 -0
  132. package/dist/docs/writer-registry.js.map +1 -1
  133. package/dist/doctor/type-matrix-repair.d.ts +194 -0
  134. package/dist/doctor/type-matrix-repair.d.ts.map +1 -0
  135. package/dist/doctor/type-matrix-repair.js +662 -0
  136. package/dist/doctor/type-matrix-repair.js.map +1 -0
  137. package/dist/gateway-client/generated/index.d.ts +2 -2
  138. package/dist/gateway-client/generated/index.d.ts.map +1 -1
  139. package/dist/gateway-client/generated/index.js +1 -1
  140. package/dist/gateway-client/generated/index.js.map +1 -1
  141. package/dist/gateway-client/generated/namespaces.gen.d.ts +5 -0
  142. package/dist/gateway-client/generated/namespaces.gen.d.ts.map +1 -1
  143. package/dist/gateway-client/generated/namespaces.gen.js +6 -1
  144. package/dist/gateway-client/generated/namespaces.gen.js.map +1 -1
  145. package/dist/gateway-client/generated/sdk.gen.d.ts +21 -1
  146. package/dist/gateway-client/generated/sdk.gen.d.ts.map +1 -1
  147. package/dist/gateway-client/generated/sdk.gen.js +55 -0
  148. package/dist/gateway-client/generated/sdk.gen.js.map +1 -1
  149. package/dist/gateway-client/generated/types.gen.d.ts +222 -0
  150. package/dist/gateway-client/generated/types.gen.d.ts.map +1 -1
  151. package/dist/gc/gc-subsystem.d.ts +4 -0
  152. package/dist/gc/gc-subsystem.d.ts.map +1 -1
  153. package/dist/gc/gc-subsystem.js +16 -0
  154. package/dist/gc/gc-subsystem.js.map +1 -1
  155. package/dist/git/work-tree.d.ts +48 -0
  156. package/dist/git/work-tree.d.ts.map +1 -1
  157. package/dist/git/work-tree.js +63 -1
  158. package/dist/git/work-tree.js.map +1 -1
  159. package/dist/hooks/handlers/session-hooks.d.ts +5 -4
  160. package/dist/hooks/handlers/session-hooks.d.ts.map +1 -1
  161. package/dist/hooks/handlers/session-hooks.js +5 -21
  162. package/dist/hooks/handlers/session-hooks.js.map +1 -1
  163. package/dist/internal.d.ts +5 -1
  164. package/dist/internal.d.ts.map +1 -1
  165. package/dist/internal.js +5 -1
  166. package/dist/internal.js.map +1 -1
  167. package/dist/memory/brain-doctor.d.ts.map +1 -1
  168. package/dist/memory/brain-doctor.js +6 -1
  169. package/dist/memory/brain-doctor.js.map +1 -1
  170. package/dist/memory/engine-compat.d.ts.map +1 -1
  171. package/dist/memory/engine-compat.js +6 -0
  172. package/dist/memory/engine-compat.js.map +1 -1
  173. package/dist/nexus/analyze-orchestrator.d.ts +24 -0
  174. package/dist/nexus/analyze-orchestrator.d.ts.map +1 -1
  175. package/dist/nexus/analyze-orchestrator.js +43 -5
  176. package/dist/nexus/analyze-orchestrator.js.map +1 -1
  177. package/dist/nexus/user-profile.d.ts +2 -1
  178. package/dist/nexus/user-profile.d.ts.map +1 -1
  179. package/dist/nexus/user-profile.js +13 -5
  180. package/dist/nexus/user-profile.js.map +1 -1
  181. package/dist/orchestrate/admission.d.ts +8 -0
  182. package/dist/orchestrate/admission.d.ts.map +1 -1
  183. package/dist/orchestrate/admission.js +3 -1
  184. package/dist/orchestrate/admission.js.map +1 -1
  185. package/dist/orchestrate/query-ops.d.ts.map +1 -1
  186. package/dist/orchestrate/query-ops.js +15 -4
  187. package/dist/orchestrate/query-ops.js.map +1 -1
  188. package/dist/orchestrate/spawn-ops.d.ts.map +1 -1
  189. package/dist/orchestrate/spawn-ops.js +43 -2
  190. package/dist/orchestrate/spawn-ops.js.map +1 -1
  191. package/dist/orchestration/index.d.ts +6 -0
  192. package/dist/orchestration/index.d.ts.map +1 -1
  193. package/dist/orchestration/index.js +3 -0
  194. package/dist/orchestration/index.js.map +1 -1
  195. package/dist/orchestration/spawn-prompt.d.ts.map +1 -1
  196. package/dist/orchestration/spawn-prompt.js +3 -1
  197. package/dist/orchestration/spawn-prompt.js.map +1 -1
  198. package/dist/paths.d.ts +43 -1
  199. package/dist/paths.d.ts.map +1 -1
  200. package/dist/paths.js +51 -1
  201. package/dist/paths.js.map +1 -1
  202. package/dist/project-lifecycle.d.ts.map +1 -1
  203. package/dist/project-lifecycle.js +31 -0
  204. package/dist/project-lifecycle.js.map +1 -1
  205. package/dist/release/authorization.d.ts +156 -0
  206. package/dist/release/authorization.d.ts.map +1 -0
  207. package/dist/release/authorization.js +460 -0
  208. package/dist/release/authorization.js.map +1 -0
  209. package/dist/release/ci-evidence.d.ts +55 -4
  210. package/dist/release/ci-evidence.d.ts.map +1 -1
  211. package/dist/release/ci-evidence.js +251 -22
  212. package/dist/release/ci-evidence.js.map +1 -1
  213. package/dist/release/ci-run-evidence.d.ts +107 -0
  214. package/dist/release/ci-run-evidence.d.ts.map +1 -0
  215. package/dist/release/ci-run-evidence.js +234 -0
  216. package/dist/release/ci-run-evidence.js.map +1 -0
  217. package/dist/release/cut.d.ts +231 -0
  218. package/dist/release/cut.d.ts.map +1 -0
  219. package/dist/release/cut.js +775 -0
  220. package/dist/release/cut.js.map +1 -0
  221. package/dist/release/deploy-evidence.d.ts +173 -0
  222. package/dist/release/deploy-evidence.d.ts.map +1 -0
  223. package/dist/release/deploy-evidence.js +381 -0
  224. package/dist/release/deploy-evidence.js.map +1 -0
  225. package/dist/release/engine-ops.d.ts.map +1 -1
  226. package/dist/release/engine-ops.js +10 -4
  227. package/dist/release/engine-ops.js.map +1 -1
  228. package/dist/release/index.d.ts +3 -0
  229. package/dist/release/index.d.ts.map +1 -1
  230. package/dist/release/index.js +2 -0
  231. package/dist/release/index.js.map +1 -1
  232. package/dist/release/invariants/archive-reason-invariant.d.ts.map +1 -1
  233. package/dist/release/invariants/archive-reason-invariant.js +4 -1
  234. package/dist/release/invariants/archive-reason-invariant.js.map +1 -1
  235. package/dist/release/open.d.ts.map +1 -1
  236. package/dist/release/open.js +2 -6
  237. package/dist/release/open.js.map +1 -1
  238. package/dist/release/plan.d.ts +34 -2
  239. package/dist/release/plan.d.ts.map +1 -1
  240. package/dist/release/plan.js +30 -6
  241. package/dist/release/plan.js.map +1 -1
  242. package/dist/release/preflight-skip.d.ts +4 -19
  243. package/dist/release/preflight-skip.d.ts.map +1 -1
  244. package/dist/release/preflight-skip.js +5 -110
  245. package/dist/release/preflight-skip.js.map +1 -1
  246. package/dist/release/reconcile-latest.d.ts +29 -0
  247. package/dist/release/reconcile-latest.d.ts.map +1 -0
  248. package/dist/release/reconcile-latest.js +71 -0
  249. package/dist/release/reconcile-latest.js.map +1 -0
  250. package/dist/release/reconcile.d.ts +3 -1
  251. package/dist/release/reconcile.d.ts.map +1 -1
  252. package/dist/release/reconcile.js +18 -6
  253. package/dist/release/reconcile.js.map +1 -1
  254. package/dist/release/release-published.d.ts +27 -0
  255. package/dist/release/release-published.d.ts.map +1 -0
  256. package/dist/release/release-published.js +35 -0
  257. package/dist/release/release-published.js.map +1 -0
  258. package/dist/release/tty-confirm.d.ts +23 -0
  259. package/dist/release/tty-confirm.d.ts.map +1 -0
  260. package/dist/release/tty-confirm.js +50 -0
  261. package/dist/release/tty-confirm.js.map +1 -0
  262. package/dist/remote/index.js +1 -1
  263. package/dist/remote/index.js.map +1 -1
  264. package/dist/resources/admission-ledger.d.ts +103 -1
  265. package/dist/resources/admission-ledger.d.ts.map +1 -1
  266. package/dist/resources/admission-ledger.js +200 -17
  267. package/dist/resources/admission-ledger.js.map +1 -1
  268. package/dist/resources/agent-lease.d.ts +82 -0
  269. package/dist/resources/agent-lease.d.ts.map +1 -0
  270. package/dist/resources/agent-lease.js +167 -0
  271. package/dist/resources/agent-lease.js.map +1 -0
  272. package/dist/resources/governor.d.ts +27 -1
  273. package/dist/resources/governor.d.ts.map +1 -1
  274. package/dist/resources/governor.js +88 -4
  275. package/dist/resources/governor.js.map +1 -1
  276. package/dist/resources/hook-footprint.d.ts +13 -1
  277. package/dist/resources/hook-footprint.d.ts.map +1 -1
  278. package/dist/resources/hook-footprint.js +33 -7
  279. package/dist/resources/hook-footprint.js.map +1 -1
  280. package/dist/resources/run-governed.d.ts +25 -0
  281. package/dist/resources/run-governed.d.ts.map +1 -1
  282. package/dist/resources/run-governed.js +44 -3
  283. package/dist/resources/run-governed.js.map +1 -1
  284. package/dist/sagas/reconcile.d.ts.map +1 -1
  285. package/dist/sagas/reconcile.js +11 -0
  286. package/dist/sagas/reconcile.js.map +1 -1
  287. package/dist/scaffold/ensure-dirs.d.ts.map +1 -1
  288. package/dist/scaffold/ensure-dirs.js +8 -6
  289. package/dist/scaffold/ensure-dirs.js.map +1 -1
  290. package/dist/security/override-cap-setting.d.ts +132 -0
  291. package/dist/security/override-cap-setting.d.ts.map +1 -1
  292. package/dist/security/override-cap-setting.js +88 -10
  293. package/dist/security/override-cap-setting.js.map +1 -1
  294. package/dist/security/owner-override-authority.d.ts.map +1 -1
  295. package/dist/security/owner-override-authority.js +5 -1
  296. package/dist/security/owner-override-authority.js.map +1 -1
  297. package/dist/selfimprove/draft-pr.d.ts.map +1 -1
  298. package/dist/selfimprove/draft-pr.js +4 -1
  299. package/dist/selfimprove/draft-pr.js.map +1 -1
  300. package/dist/session/engine-ops.d.ts.map +1 -1
  301. package/dist/session/engine-ops.js +4 -16
  302. package/dist/session/engine-ops.js.map +1 -1
  303. package/dist/sessions/briefing.d.ts +6 -0
  304. package/dist/sessions/briefing.d.ts.map +1 -1
  305. package/dist/sessions/briefing.js +34 -0
  306. package/dist/sessions/briefing.js.map +1 -1
  307. package/dist/sessions/session-end-snapshot-entry.js +2 -2
  308. package/dist/sessions/session-end-snapshot-entry.js.map +1 -1
  309. package/dist/sessions/session-end-snapshot.js +4 -4
  310. package/dist/sessions/session-end-snapshot.js.map +1 -1
  311. package/dist/skills/injection/token.d.ts.map +1 -1
  312. package/dist/skills/injection/token.js +3 -6
  313. package/dist/skills/injection/token.js.map +1 -1
  314. package/dist/skills/orchestrator/spawn.d.ts.map +1 -1
  315. package/dist/skills/orchestrator/spawn.js +3 -4
  316. package/dist/skills/orchestrator/spawn.js.map +1 -1
  317. package/dist/skills/propose-patch.d.ts.map +1 -1
  318. package/dist/skills/propose-patch.js +4 -1
  319. package/dist/skills/propose-patch.js.map +1 -1
  320. package/dist/spawn/branch-integration.d.ts.map +1 -1
  321. package/dist/spawn/branch-integration.js +5 -2
  322. package/dist/spawn/branch-integration.js.map +1 -1
  323. package/dist/store/attachment-repair.d.ts.map +1 -1
  324. package/dist/store/attachment-repair.js +5 -4
  325. package/dist/store/attachment-repair.js.map +1 -1
  326. package/dist/store/attachment-store.d.ts +6 -0
  327. package/dist/store/attachment-store.d.ts.map +1 -1
  328. package/dist/store/attachment-store.js +19 -2
  329. package/dist/store/attachment-store.js.map +1 -1
  330. package/dist/store/blob-ops.d.ts +1 -1
  331. package/dist/store/blob-ops.d.ts.map +1 -1
  332. package/dist/store/blob-ops.js +7 -3
  333. package/dist/store/blob-ops.js.map +1 -1
  334. package/dist/store/conduit-doc-history.d.ts +15 -1
  335. package/dist/store/conduit-doc-history.d.ts.map +1 -1
  336. package/dist/store/conduit-doc-history.js +16 -10
  337. package/dist/store/conduit-doc-history.js.map +1 -1
  338. package/dist/store/db-helpers.d.ts +29 -0
  339. package/dist/store/db-helpers.d.ts.map +1 -1
  340. package/dist/store/db-helpers.js +89 -1
  341. package/dist/store/db-helpers.js.map +1 -1
  342. package/dist/store/decision-adr-links.d.ts.map +1 -1
  343. package/dist/store/decision-adr-links.js +5 -2
  344. package/dist/store/decision-adr-links.js.map +1 -1
  345. package/dist/store/git-checkpoint.d.ts +11 -1
  346. package/dist/store/git-checkpoint.d.ts.map +1 -1
  347. package/dist/store/git-checkpoint.js +14 -3
  348. package/dist/store/git-checkpoint.js.map +1 -1
  349. package/dist/store/llmtxt-blob-adapter.d.ts.map +1 -1
  350. package/dist/store/llmtxt-blob-adapter.js +4 -1
  351. package/dist/store/llmtxt-blob-adapter.js.map +1 -1
  352. package/dist/store/schema/attachments.d.ts +7 -5
  353. package/dist/store/schema/attachments.d.ts.map +1 -1
  354. package/dist/store/schema/attachments.js +5 -2
  355. package/dist/store/schema/attachments.js.map +1 -1
  356. package/dist/store/session-store.d.ts.map +1 -1
  357. package/dist/store/session-store.js +6 -0
  358. package/dist/store/session-store.js.map +1 -1
  359. package/dist/store/sqlite-backup.d.ts +6 -1
  360. package/dist/store/sqlite-backup.d.ts.map +1 -1
  361. package/dist/store/sqlite-backup.js +9 -5
  362. package/dist/store/sqlite-backup.js.map +1 -1
  363. package/dist/store/sqlite-data-accessor.d.ts.map +1 -1
  364. package/dist/store/sqlite-data-accessor.js +63 -36
  365. package/dist/store/sqlite-data-accessor.js.map +1 -1
  366. package/dist/system/backup.d.ts +29 -0
  367. package/dist/system/backup.d.ts.map +1 -1
  368. package/dist/system/backup.js +240 -37
  369. package/dist/system/backup.js.map +1 -1
  370. package/dist/system/dependencies.d.ts +22 -0
  371. package/dist/system/dependencies.d.ts.map +1 -1
  372. package/dist/system/dependencies.js +49 -2
  373. package/dist/system/dependencies.js.map +1 -1
  374. package/dist/system/health.d.ts.map +1 -1
  375. package/dist/system/health.js +23 -1
  376. package/dist/system/health.js.map +1 -1
  377. package/dist/task-work/claims.d.ts.map +1 -1
  378. package/dist/task-work/claims.js +14 -0
  379. package/dist/task-work/claims.js.map +1 -1
  380. package/dist/task-work/index.d.ts.map +1 -1
  381. package/dist/task-work/index.js +22 -0
  382. package/dist/task-work/index.js.map +1 -1
  383. package/dist/tasks/ac-coverage-gate.d.ts +27 -0
  384. package/dist/tasks/ac-coverage-gate.d.ts.map +1 -1
  385. package/dist/tasks/ac-coverage-gate.js +63 -28
  386. package/dist/tasks/ac-coverage-gate.js.map +1 -1
  387. package/dist/tasks/affected-packages.d.ts +26 -30
  388. package/dist/tasks/affected-packages.d.ts.map +1 -1
  389. package/dist/tasks/affected-packages.js +27 -50
  390. package/dist/tasks/affected-packages.js.map +1 -1
  391. package/dist/tasks/affected-scope.d.ts +13 -3
  392. package/dist/tasks/affected-scope.d.ts.map +1 -1
  393. package/dist/tasks/affected-scope.js +29 -11
  394. package/dist/tasks/affected-scope.js.map +1 -1
  395. package/dist/tasks/change-set.d.ts.map +1 -1
  396. package/dist/tasks/change-set.js +39 -20
  397. package/dist/tasks/change-set.js.map +1 -1
  398. package/dist/tasks/command-gate-cmd.d.ts +33 -0
  399. package/dist/tasks/command-gate-cmd.d.ts.map +1 -0
  400. package/dist/tasks/command-gate-cmd.js +45 -0
  401. package/dist/tasks/command-gate-cmd.js.map +1 -0
  402. package/dist/tasks/complete-ci.d.ts +9 -0
  403. package/dist/tasks/complete-ci.d.ts.map +1 -1
  404. package/dist/tasks/complete-ci.js +21 -4
  405. package/dist/tasks/complete-ci.js.map +1 -1
  406. package/dist/tasks/complete.d.ts.map +1 -1
  407. package/dist/tasks/complete.js +14 -0
  408. package/dist/tasks/complete.js.map +1 -1
  409. package/dist/tasks/component-pr.d.ts +37 -0
  410. package/dist/tasks/component-pr.d.ts.map +1 -1
  411. package/dist/tasks/component-pr.js +106 -14
  412. package/dist/tasks/component-pr.js.map +1 -1
  413. package/dist/tasks/done-plan.d.ts +14 -1
  414. package/dist/tasks/done-plan.d.ts.map +1 -1
  415. package/dist/tasks/done-plan.js +125 -15
  416. package/dist/tasks/done-plan.js.map +1 -1
  417. package/dist/tasks/done-record.d.ts.map +1 -1
  418. package/dist/tasks/done-record.js +4 -0
  419. package/dist/tasks/done-record.js.map +1 -1
  420. package/dist/tasks/engine-converters.d.ts.map +1 -1
  421. package/dist/tasks/engine-converters.js +2 -0
  422. package/dist/tasks/engine-converters.js.map +1 -1
  423. package/dist/tasks/evidence-tree.d.ts +28 -1
  424. package/dist/tasks/evidence-tree.d.ts.map +1 -1
  425. package/dist/tasks/evidence-tree.js +66 -2
  426. package/dist/tasks/evidence-tree.js.map +1 -1
  427. package/dist/tasks/evidence.d.ts +39 -27
  428. package/dist/tasks/evidence.d.ts.map +1 -1
  429. package/dist/tasks/evidence.js +382 -188
  430. package/dist/tasks/evidence.js.map +1 -1
  431. package/dist/tasks/gate-notes.d.ts +17 -0
  432. package/dist/tasks/gate-notes.d.ts.map +1 -1
  433. package/dist/tasks/gate-notes.js.map +1 -1
  434. package/dist/tasks/gate-preview.d.ts +7 -1
  435. package/dist/tasks/gate-preview.d.ts.map +1 -1
  436. package/dist/tasks/gate-preview.js +30 -4
  437. package/dist/tasks/gate-preview.js.map +1 -1
  438. package/dist/tasks/gate-runner.d.ts +42 -0
  439. package/dist/tasks/gate-runner.d.ts.map +1 -1
  440. package/dist/tasks/gate-runner.js +234 -94
  441. package/dist/tasks/gate-runner.js.map +1 -1
  442. package/dist/tasks/manifest-scope.d.ts +115 -0
  443. package/dist/tasks/manifest-scope.d.ts.map +1 -0
  444. package/dist/tasks/manifest-scope.js +352 -0
  445. package/dist/tasks/manifest-scope.js.map +1 -0
  446. package/dist/tasks/merge-close.d.ts +376 -0
  447. package/dist/tasks/merge-close.d.ts.map +1 -0
  448. package/dist/tasks/merge-close.js +1497 -0
  449. package/dist/tasks/merge-close.js.map +1 -0
  450. package/dist/tasks/qa-run-binding.d.ts +5 -0
  451. package/dist/tasks/qa-run-binding.d.ts.map +1 -1
  452. package/dist/tasks/qa-run-binding.js +6 -1
  453. package/dist/tasks/qa-run-binding.js.map +1 -1
  454. package/dist/tasks/req.d.ts.map +1 -1
  455. package/dist/tasks/req.js +29 -1
  456. package/dist/tasks/req.js.map +1 -1
  457. package/dist/tasks/review-verdict.d.ts +159 -0
  458. package/dist/tasks/review-verdict.d.ts.map +1 -0
  459. package/dist/tasks/review-verdict.js +442 -0
  460. package/dist/tasks/review-verdict.js.map +1 -0
  461. package/dist/tasks/task-reparent.d.ts.map +1 -1
  462. package/dist/tasks/task-reparent.js +22 -3
  463. package/dist/tasks/task-reparent.js.map +1 -1
  464. package/dist/tasks/test-report-counts.d.ts +274 -0
  465. package/dist/tasks/test-report-counts.d.ts.map +1 -0
  466. package/dist/tasks/test-report-counts.js +746 -0
  467. package/dist/tasks/test-report-counts.js.map +1 -0
  468. package/dist/tasks/test-run-binding.d.ts +20 -45
  469. package/dist/tasks/test-run-binding.d.ts.map +1 -1
  470. package/dist/tasks/test-run-binding.js +34 -119
  471. package/dist/tasks/test-run-binding.js.map +1 -1
  472. package/dist/tasks/update.d.ts.map +1 -1
  473. package/dist/tasks/update.js +42 -0
  474. package/dist/tasks/update.js.map +1 -1
  475. package/dist/tasks/verification-policy.d.ts +12 -0
  476. package/dist/tasks/verification-policy.d.ts.map +1 -1
  477. package/dist/tasks/verification-policy.js +13 -0
  478. package/dist/tasks/verification-policy.js.map +1 -1
  479. package/dist/tasks/workspace-resolution.d.ts +225 -0
  480. package/dist/tasks/workspace-resolution.d.ts.map +1 -0
  481. package/dist/tasks/workspace-resolution.js +497 -0
  482. package/dist/tasks/workspace-resolution.js.map +1 -0
  483. package/dist/validation/doctor/checks.d.ts +20 -0
  484. package/dist/validation/doctor/checks.d.ts.map +1 -1
  485. package/dist/validation/doctor/checks.js +112 -0
  486. package/dist/validation/doctor/checks.js.map +1 -1
  487. package/dist/validation/engine-ops.d.ts.map +1 -1
  488. package/dist/validation/engine-ops.js +37 -3
  489. package/dist/validation/engine-ops.js.map +1 -1
  490. package/dist/validation/validate-ops.d.ts +4 -0
  491. package/dist/validation/validate-ops.d.ts.map +1 -1
  492. package/dist/validation/validate-ops.js +79 -2
  493. package/dist/validation/validate-ops.js.map +1 -1
  494. package/dist/worktree/list.d.ts +4 -21
  495. package/dist/worktree/list.d.ts.map +1 -1
  496. package/dist/worktree/list.js +14 -44
  497. package/dist/worktree/list.js.map +1 -1
  498. package/dist/worktree/merged-cleanup.d.ts +63 -0
  499. package/dist/worktree/merged-cleanup.d.ts.map +1 -0
  500. package/dist/worktree/merged-cleanup.js +571 -0
  501. package/dist/worktree/merged-cleanup.js.map +1 -0
  502. package/dist/worktree/prune.d.ts +64 -2
  503. package/dist/worktree/prune.d.ts.map +1 -1
  504. package/dist/worktree/prune.js +151 -6
  505. package/dist/worktree/prune.js.map +1 -1
  506. package/dist/worktree/sentinel-index.d.ts +14 -0
  507. package/dist/worktree/sentinel-index.d.ts.map +1 -1
  508. package/dist/worktree/sentinel-index.js +17 -0
  509. package/dist/worktree/sentinel-index.js.map +1 -1
  510. package/dist/worktree/worktree-adopt.d.ts +1 -13
  511. package/dist/worktree/worktree-adopt.d.ts.map +1 -1
  512. package/dist/worktree/worktree-adopt.js +2 -17
  513. package/dist/worktree/worktree-adopt.js.map +1 -1
  514. package/package.json +12 -12
  515. package/schemas/project-context.schema.json +33 -0
  516. package/templates/CLEO-INJECTION.md +6 -16
  517. package/templates/CLEO-REFERENCE.md +27 -4
  518. package/templates/git-hooks/legacy-hashes.json +2 -1
  519. package/templates/git-hooks/pre-commit +43 -11
  520. package/templates/workflows/release-prepare.yml.tmpl +9 -88
@@ -37,11 +37,12 @@ import { HEAVY_HEAP_ENV, HEAVY_WORKERS_ENV, isHeavyTool } from './heavy-tool-env
37
37
  import { DISABLE_ENV, describeMemoryLimit } from './heavy-tool-limit.js';
38
38
  import { qaRunUnionGapReason, typecheckCoverageGap } from './qa-run-binding.js';
39
39
  import { computeCommitRevalidationKey, readCommitRevalidationEntry, writeCommitRevalidationEntry, } from './revalidation-cache.js';
40
- import { bindTestRunReport, configSchemaCompositionGap, coveredTestFiles, playwrightTestRun, TEST_RUN_MAX_RECORDED_FILES, } from './test-run-binding.js';
40
+ import { present, readTestReport, TEST_RUN_SHAPE_HINT, } from './test-report-counts.js';
41
+ import { bindTestRunReport, configSchemaCompositionGap, coveredTestFiles, TEST_RUN_MAX_RECORDED_FILES, } from './test-run-binding.js';
41
42
  import { captureHead, captureTree, resolveSpawnTimeoutMs, runToolCached, } from './tool-cache.js';
42
43
  import { CANONICAL_TOOLS, canonicalToolName, listValidToolNames, resolveToolCommand, } from './tool-resolver.js';
43
44
  import { detectStaticVacuity, probeTscFileCount, VACUOUS_TOOL_FIX } from './tool-vacuity.js';
44
- import { classifyEvidenceTask, declaredEvidenceKind, gateProfileOf, implementedCodeEvidence, isDocumentArtifact, isDocumentEvidencePath, } from './verification-policy.js';
45
+ import { classifyEvidenceTask, declaredEvidenceKind, evidenceProfileOf, gateProfileOf, implementedCodeEvidence, isDocumentArtifact, isDocumentEvidencePath, } from './verification-policy.js';
45
46
  /**
46
47
  * Valid tool names recognised by the `tool:<name>` evidence atom.
47
48
  *
@@ -142,6 +143,47 @@ export function parseEvidence(raw) {
142
143
  throw err;
143
144
  }
144
145
  }
146
+ /**
147
+ * The two roots an evidence atom may be about, named so an arm cannot inherit
148
+ * the wrong one.
149
+ *
150
+ * gh#1365 was fixed once by threading a single `executionRoot` parameter, and
151
+ * the fix reached two of the switch's arms. The reason a single parameter could
152
+ * not close it is that **"root" is one word for two things**: the tree the
153
+ * evidence is ABOUT, and the store CLEO keeps its own records in. A parameter
154
+ * called `root` relocates that ambiguity into a signature rather than removing
155
+ * it, so the next arm silently picks whichever the author had in mind.
156
+ *
157
+ * With both fields named, every arm DECLARES what it is about by which field it
158
+ * reads. `decision:` reading {@link storeRoot} is self-documenting rather than
159
+ * an exception needing a comment; `pr:` reading both is obviously correct
160
+ * rather than obviously odd; and a new arm cannot inherit a default, because
161
+ * there is no default.
162
+ *
163
+ * The rule, stated once: **use the root that matches what the atom is about.**
164
+ * Evidence about the repository under test → {@link executionRoot}. Evidence
165
+ * about CLEO's own records → {@link storeRoot}.
166
+ *
167
+ * @task T12238 (gh#1365)
168
+ */
169
+ /**
170
+ * Repo paths a task's change touches, for judging a `ci-run:` test file's
171
+ * relevance (T14149): its declared files and the files its recorded
172
+ * `implemented` evidence names (`files:` paths and `pr:` changed paths).
173
+ */
174
+ function taskChangePaths(context) {
175
+ const implemented = context.task.verification?.evidence?.implemented?.atoms ?? [];
176
+ return [
177
+ ...new Set([
178
+ ...(context.task.files ?? []),
179
+ ...implemented.flatMap((atom) => atom.kind === 'files'
180
+ ? atom.files.map((file) => file.path)
181
+ : atom.kind === 'pr'
182
+ ? (atom.changedPaths ?? [])
183
+ : []),
184
+ ]),
185
+ ].map(normalizeEvidencePath);
186
+ }
145
187
  /**
146
188
  * Validate a single parsed atom against the filesystem / git / tools.
147
189
  *
@@ -280,6 +322,41 @@ siblingCommitSha, context) {
280
322
  ? { ok: true, atom: result.atom }
281
323
  : { ok: false, reason: result.reason, codeName: result.codeName };
282
324
  }
325
+ case 'ci-run': {
326
+ // T14149: a repeat-run.yml run, read from GitHub in the repository. Its
327
+ // test file must be one the task changed or one a linked criterion names.
328
+ const owner = taskId ?? context?.task.id;
329
+ if (owner === undefined || context === undefined)
330
+ return {
331
+ ok: false,
332
+ reason: 'ci-run evidence needs the task context to judge which test file is relevant',
333
+ codeName: 'E_EVIDENCE_INVALID',
334
+ };
335
+ const { resolveCiRunEvidenceAtom } = await import('../release/ci-run-evidence.js');
336
+ const result = await resolveCiRunEvidenceAtom(parsed.runId, roots, {
337
+ taskId: owner,
338
+ changedPaths: taskChangePaths(context),
339
+ criteriaTexts: (context.creditedCriteria ?? []).map((criterion) => criterion.text),
340
+ });
341
+ return result.ok
342
+ ? { ok: true, atom: result.atom }
343
+ : { ok: false, reason: result.reason, codeName: result.codeName };
344
+ }
345
+ case 'deploy': {
346
+ // T14083: the project context is CLEO's own record (storeRoot); the
347
+ // deployed commit is resolved in the repository (executionRoot).
348
+ const { resolveDeployEvidenceAtom } = await import('../release/deploy-evidence.js');
349
+ const { loadProjectContext } = await import('../agents/variable-substitution.js');
350
+ const ctx = loadProjectContext(roots.storeRoot);
351
+ const owner = taskId ?? context?.task.id;
352
+ const result = await resolveDeployEvidenceAtom(parsed, roots, {
353
+ projectContext: ctx.loaded ? ctx.context : null,
354
+ ...(owner !== undefined ? { taskId: owner } : {}),
355
+ });
356
+ return result.ok
357
+ ? { ok: true, atom: result.atom }
358
+ : { ok: false, reason: result.reason, codeName: result.codeName };
359
+ }
283
360
  case 'satisfies': {
284
361
  // ADR-079-r2: 5-check validator pipeline shipped by T10507.
285
362
  // Delegates to the dedicated validator module to keep the dispatch
@@ -811,8 +888,10 @@ export function extractTaskAcFilesWithProvenance(task) {
811
888
  /**
812
889
  * Return the list of file paths a commit touched (added, modified, or deleted).
813
890
  *
814
- * Uses `git diff-tree --no-commit-id -r --name-only -m --first-parent` which
815
- * correctly handles **merge commits** by diffing against the first parent only.
891
+ * Uses `git diff-tree --no-commit-id -r --name-only --diff-merges=first-parent`
892
+ * (git 2.31+), which diffs a **merge commit** against its first parent only.
893
+ * `-m --first-parent` listed the diff against EVERY parent, since
894
+ * `--first-parent` limits a revision walk, not diff-tree (T14166).
816
895
  * The legacy `git show --name-only` approach returned an empty list for merge
817
896
  * commits because show without `--first-parent` produces a combined diff that
818
897
  * `--name-only` cannot meaningfully summarise, causing the content-intersect
@@ -825,6 +904,7 @@ export function extractTaskAcFilesWithProvenance(task) {
825
904
  * @internal
826
905
  * @task T9245
827
906
  * @task T11959 (DHQ-083 companion b — merge-commit first-parent diff)
907
+ * @task T14166
828
908
  */
829
909
  async function gitShowFiles(sha, projectRoot) {
830
910
  return (await commitFilesOrNull(sha, projectRoot)) ?? [];
@@ -839,9 +919,9 @@ async function gitShowFiles(sha, projectRoot) {
839
919
  * @task T14001
840
920
  */
841
921
  async function commitFilesOrNull(sha, projectRoot) {
842
- // diff-tree with --first-parent gives a clean single-parent diff for both
922
+ // --diff-merges=first-parent gives a clean single-parent diff for both
843
923
  // regular commits and merge commits (compares merge result to first parent,
844
- // i.e. the branch tip before the PR was merged).
924
+ // i.e. the branch tip before the PR was merged; T14166).
845
925
  //
846
926
  // --root: Without this flag, git diff-tree returns EMPTY output for the
847
927
  // repository's initial (root) commit because there is no parent to diff
@@ -849,7 +929,15 @@ async function commitFilesOrNull(sha, projectRoot) {
849
929
  // against the empty tree, correctly listing all files it introduced.
850
930
  // This fixes the gate-verify-hint tests (and any real workflow) where the
851
931
  // evidence commit happens to be the very first commit in the repo (T11959).
852
- const r = await runCommand('git', ['diff-tree', '--root', '--no-commit-id', '-r', '--name-only', '-m', '--first-parent', sha], projectRoot);
932
+ const r = await runCommand('git', [
933
+ 'diff-tree',
934
+ '--root',
935
+ '--no-commit-id',
936
+ '-r',
937
+ '--name-only',
938
+ '--diff-merges=first-parent',
939
+ sha,
940
+ ], projectRoot);
853
941
  if (r.exitCode !== 0)
854
942
  return null;
855
943
  return r.stdout
@@ -1267,121 +1355,6 @@ async function validateFiles(paths, roots, taskId, commitShaFor, requireCommitAr
1267
1355
  }
1268
1356
  return { ok: true, atom: { kind: 'files', files } };
1269
1357
  }
1270
- /**
1271
- * The counter sets a `test-run:` report may carry (gh#1804), tried in order;
1272
- * the first whose total key is present is read. `failed` and `notRun` sum
1273
- * their keys, and an absent count is 0.
1274
- *
1275
- * | Shape | Total | Passed | Failed | Skipped / todo |
1276
- * |---|---|---|---|---|
1277
- * | vitest `--reporter=json`, jest `--json` | `numTotalTests` | `numPassedTests` | `numFailedTests` | `numPendingTests`, `numTodoTests` |
1278
- * | a runner's summary output (bun test, tsx --test) | `total` | `passed` | `failed` | `skipped`, `todo` |
1279
- * | node --test summary (`# tests`, `# pass`, ...) | `tests` | `pass` | `fail`, `cancelled` | `skipped`, `todo` |
1280
- *
1281
- * Integrity (T13136 review): every count is a non-negative integer;
1282
- * passed + failed + skipped/todo equals the total; a failure under ANY key
1283
- * ({@link TEST_RUN_FAILURE_KEYS}) refuses, whichever set supplied the total;
1284
- * a report's `exit` / `exitCode` must be a number, and a non-zero one is
1285
- * refused. A `test-run:` atom proves only what its file says; `tool:test` and
1286
- * `ci:<pr>` are what prove the run. `cleo verify --help` documents the same
1287
- * sets.
1288
- *
1289
- * @task T13136
1290
- */
1291
- const TEST_RUN_COUNTER_SHAPES = [
1292
- {
1293
- name: 'vitest/jest',
1294
- total: 'numTotalTests',
1295
- passed: 'numPassedTests',
1296
- failed: ['numFailedTests'],
1297
- notRun: ['numPendingTests', 'numTodoTests'],
1298
- },
1299
- {
1300
- name: 'summary',
1301
- total: 'total',
1302
- passed: 'passed',
1303
- failed: ['failed'],
1304
- notRun: ['skipped', 'todo'],
1305
- },
1306
- {
1307
- name: 'node --test',
1308
- total: 'tests',
1309
- passed: 'pass',
1310
- failed: ['fail', 'cancelled'],
1311
- notRun: ['skipped', 'todo'],
1312
- },
1313
- ];
1314
- /** The keys of each counter set, as the refusals name them. */
1315
- const TEST_RUN_SHAPE_HINT = TEST_RUN_COUNTER_SHAPES.map((s) => `${[s.total, s.passed, ...s.failed, ...s.notRun].join('/')} (${s.name})`).join(', ');
1316
- /**
1317
- * Keys that name failures in any counter set, plus jest/vitest suite failures
1318
- * and the mocha / JUnit spellings: a positive value under any of them refuses
1319
- * the report, whichever set supplied the total.
1320
- */
1321
- const TEST_RUN_FAILURE_KEYS = [
1322
- 'numFailedTests',
1323
- 'numFailedTestSuites',
1324
- 'numRuntimeErrorTestSuites',
1325
- 'failed',
1326
- 'fail',
1327
- 'cancelled',
1328
- 'failures',
1329
- 'errors',
1330
- ];
1331
- /** A present, non-null JSON value. */
1332
- const present = (value) => value !== undefined && value !== null;
1333
- /** A non-negative integer, or `undefined` when the value is anything else. */
1334
- const countOf = (value) => typeof value === 'number' && Number.isInteger(value) && value >= 0 ? value : undefined;
1335
- /**
1336
- * Read a report's counts from the first counter set it carries, enforcing
1337
- * their integrity; `null` when it carries none.
1338
- */
1339
- function testRunCounts(report) {
1340
- const shape = TEST_RUN_COUNTER_SHAPES.find((s) => present(report[s.total]));
1341
- if (shape === undefined)
1342
- return null;
1343
- const invalid = (key) => ({
1344
- reason: `test-run report's "${key}" is ${JSON.stringify(report[key])}; every count must be a non-negative integer`,
1345
- codeName: 'E_EVIDENCE_INVALID',
1346
- });
1347
- const read = (key) => {
1348
- if (!present(report[key]))
1349
- return 0;
1350
- return countOf(report[key]) ?? invalid(key);
1351
- };
1352
- const total = read(shape.total);
1353
- if (typeof total !== 'number')
1354
- return total;
1355
- const keys = [shape.passed, ...shape.failed, ...shape.notRun];
1356
- const values = [];
1357
- for (const key of keys) {
1358
- const v = read(key);
1359
- if (typeof v !== 'number')
1360
- return v;
1361
- values.push(v);
1362
- }
1363
- for (const key of TEST_RUN_FAILURE_KEYS) {
1364
- if (!present(report[key]))
1365
- continue;
1366
- const v = countOf(report[key]);
1367
- if (v === undefined)
1368
- return invalid(key);
1369
- if (v > 0) {
1370
- return { reason: `test-run reports ${key} = ${v}`, codeName: 'E_EVIDENCE_TESTS_FAILED' };
1371
- }
1372
- }
1373
- const [passed = 0, ...rest] = values;
1374
- const failed = rest.slice(0, shape.failed.length).reduce((a, b) => a + b, 0);
1375
- const notRun = rest.slice(shape.failed.length).reduce((a, b) => a + b, 0);
1376
- if (passed + failed + notRun !== total) {
1377
- return {
1378
- reason: `test-run report's counts do not add up: ${shape.total} is ${total}, but ` +
1379
- `${shape.passed} + ${[...shape.failed, ...shape.notRun].join(' + ')} = ${passed + failed + notRun}`,
1380
- codeName: 'E_EVIDENCE_INVALID',
1381
- };
1382
- }
1383
- return { shape, total, passed, failed, notRun };
1384
- }
1385
1358
  /**
1386
1359
  * `tool:test-affected` (D11150, T12635): run `testing.affectedCommand` over the
1387
1360
  * packages the branch diff touches plus their dependents, through the same
@@ -1403,6 +1376,9 @@ async function validateAffectedTests(roots, taskId) {
1403
1376
  */
1404
1377
  async function runAffectedTests(tool, run, roots, taskId) {
1405
1378
  const { storeRoot, executionRoot } = roots;
1379
+ const foreign = await workspaceResolutionRefusal(executionRoot);
1380
+ if (foreign !== null)
1381
+ return foreign;
1406
1382
  // T13132: status shows what holds the budget — the scope and the task.
1407
1383
  const result = await runToolCached(run.command, storeRoot, {
1408
1384
  executionRoot,
@@ -1437,6 +1413,7 @@ async function runAffectedTests(tool, run, roots, taskId) {
1437
1413
  ...(run.testFiles ? { affectedTestFiles: run.testFiles } : {}),
1438
1414
  ...(run.configPaths ? { configPaths: run.configPaths } : {}),
1439
1415
  ...(run.untested.length > 0 ? { untestedPackages: run.untested } : {}),
1416
+ ...(run.untrackedSetAside ? { untrackedSetAside: run.untrackedSetAside } : {}),
1440
1417
  ...(await toolRunAtomFields(result)),
1441
1418
  },
1442
1419
  };
@@ -1600,16 +1577,74 @@ async function readTestRunReport(path, roots) {
1600
1577
  codeName: 'E_EVIDENCE_INVALID',
1601
1578
  };
1602
1579
  }
1603
- try {
1604
- return { ok: true, abs, content, parsed: JSON.parse(content.toString('utf-8')) };
1605
- }
1606
- catch (err) {
1580
+ // T14124: one reader for every format, shared with typed gates' minCount.
1581
+ // T14161: a TAP report's file names resolve in the caller's tree.
1582
+ const text = content.toString('utf-8');
1583
+ const read = readTestReport(text, { cwd: executionRoot });
1584
+ if (read === null) {
1585
+ let parsed;
1586
+ try {
1587
+ parsed = JSON.parse(text);
1588
+ }
1589
+ catch (err) {
1590
+ return {
1591
+ ok: false,
1592
+ reason: 'test-run file is not valid JSON, JUnit XML or libtest JSON lines (or node:test TAP): ' +
1593
+ `${err instanceof Error ? err.message : String(err)}`,
1594
+ codeName: 'E_EVIDENCE_INVALID',
1595
+ };
1596
+ }
1607
1597
  return {
1608
1598
  ok: false,
1609
- reason: `test-run file is not valid JSON: ${err instanceof Error ? err.message : String(err)}`,
1599
+ reason: parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)
1600
+ ? `test-run file is not a JSON object. Expected test counts: ${TEST_RUN_SHAPE_HINT}`
1601
+ : `test-run report has no test counts. Expected one of: ${TEST_RUN_SHAPE_HINT}; ` +
1602
+ "e.g. vitest's --reporter=json --outputFile, Playwright's --reporter=json, or " +
1603
+ '{"total":43,"passed":42,"failed":0,"skipped":1}',
1610
1604
  codeName: 'E_EVIDENCE_INVALID',
1611
1605
  };
1612
1606
  }
1607
+ if (!read.ok)
1608
+ return { ok: false, reason: read.reason, codeName: read.codeName };
1609
+ return { ok: true, abs, content, parsed: read.report, counts: read.counts };
1610
+ }
1611
+ /**
1612
+ * T14097: refuse a test run in a checkout whose workspace packages resolve
1613
+ * into another checkout (its tests would run that checkout's build) or whose
1614
+ * dist was built from other source (T14123), and warn when a dependency's
1615
+ * build is borrowed from a checkout with identical source. Null when the run
1616
+ * may proceed.
1617
+ *
1618
+ * @param root - The tree the run is about (any directory inside the checkout).
1619
+ * @param testFiles - Test files the run names, to scope the check; all packages when empty.
1620
+ */
1621
+ async function workspaceResolutionRefusal(root, testFiles = []) {
1622
+ const { borrowedBuildWarning, checkoutRootOf, checkWorkspaceResolution, packagesNamedByTestArgs, UNSTAMPED_DIST_FIX, unstampedDistWarning, workspaceResolutionError, } = await import('./workspace-resolution.js');
1623
+ const checkout = checkoutRootOf(root) ?? root;
1624
+ const report = checkWorkspaceResolution(checkout, packagesNamedByTestArgs(checkout, checkout, testFiles));
1625
+ const refused = workspaceResolutionError(report);
1626
+ // @sync-invariant none:input-shape a test run against another checkout's build, or a dist built from other source, is refused before anything is written
1627
+ if (refused !== null)
1628
+ return { ok: false, reason: refused.message, codeName: refused.codeName };
1629
+ const borrowed = borrowedBuildWarning(report);
1630
+ if (borrowed !== null)
1631
+ pushWarning({
1632
+ // @sync-invariant none:input-shape a warning only; nothing is refused or written
1633
+ code: 'W_WORKSPACE_BORROWED_BUILD',
1634
+ severity: 'warn',
1635
+ message: borrowed,
1636
+ context: { root: checkout },
1637
+ });
1638
+ const unstamped = unstampedDistWarning(report);
1639
+ if (unstamped !== null)
1640
+ pushWarning({
1641
+ // @sync-invariant none:input-shape a warning only; nothing is refused or written
1642
+ code: 'W_WORKSPACE_UNSTAMPED_DIST',
1643
+ severity: 'warn',
1644
+ message: unstamped,
1645
+ context: { root: checkout, fix: UNSTAMPED_DIST_FIX },
1646
+ });
1647
+ return null;
1613
1648
  }
1614
1649
  /**
1615
1650
  * Validate a `test-run:` report: its counts, then bound to the change.
@@ -1623,44 +1658,14 @@ async function validateTestRun(path, roots, taskId, siblings = []) {
1623
1658
  const read = await readTestRunReport(path, roots);
1624
1659
  if (!read.ok)
1625
1660
  return read;
1626
- const { abs, content } = read;
1627
- let parsed = read.parsed;
1661
+ const { abs, content, parsed, counts } = read;
1628
1662
  const sha256 = createHash('sha256').update(content).digest('hex');
1629
- if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
1663
+ if (counts.failure !== undefined)
1630
1664
  return {
1631
1665
  ok: false,
1632
- reason: `test-run file is not a JSON object. Expected test counts: ${TEST_RUN_SHAPE_HINT}`,
1633
- codeName: 'E_EVIDENCE_INVALID',
1634
- };
1635
- }
1636
- // T14031: a Playwright JSON report is read in the vitest shape.
1637
- const playwright = playwrightTestRun(parsed);
1638
- if (playwright !== null && 'refusal' in playwright)
1639
- return { ok: false, reason: playwright.refusal, codeName: playwright.codeName };
1640
- if (playwright !== null)
1641
- parsed = playwright;
1642
- if (present(parsed.testResults) &&
1643
- (!Array.isArray(parsed.testResults) ||
1644
- parsed.testResults.some((tr) => tr === null || typeof tr !== 'object' || Array.isArray(tr)))) {
1645
- return {
1646
- ok: false,
1647
- reason: 'test-run report\'s "testResults" must be an array of objects',
1648
- codeName: 'E_EVIDENCE_INVALID',
1649
- };
1650
- }
1651
- const counts = testRunCounts(parsed);
1652
- if (counts !== null && !('shape' in counts)) {
1653
- return { ok: false, reason: counts.reason, codeName: counts.codeName };
1654
- }
1655
- if (counts === null) {
1656
- return {
1657
- ok: false,
1658
- reason: `test-run report has no test counts. Expected one of: ${TEST_RUN_SHAPE_HINT}; ` +
1659
- "e.g. vitest's --reporter=json --outputFile, Playwright's --reporter=json, or " +
1660
- '{"total":43,"passed":42,"failed":0,"skipped":1}',
1661
- codeName: 'E_EVIDENCE_INVALID',
1666
+ reason: `test-run reports ${counts.failure}`,
1667
+ codeName: 'E_EVIDENCE_TESTS_FAILED',
1662
1668
  };
1663
- }
1664
1669
  const { total, failed, passed, notRun: pending } = counts;
1665
1670
  // Both spellings are checked: a report carrying exit 0 and exitCode 1 failed
1666
1671
  // (T13136 review).
@@ -1736,18 +1741,19 @@ async function validateTestRun(path, roots, taskId, siblings = []) {
1736
1741
  if (broken)
1737
1742
  return { ok: false, reason: broken, codeName: E_EVIDENCE_GIT_ROOT };
1738
1743
  const testFiles = coveredTestFiles(parsed, bindRoot);
1744
+ // T14097: never record a receipt from a checkout testing another one's build.
1745
+ const foreign = await workspaceResolutionRefusal(bindRoot, testFiles);
1746
+ if (foreign !== null)
1747
+ return foreign;
1739
1748
  // T14031: the other reports of this write cover the change with this one.
1740
1749
  const siblingFiles = [];
1741
1750
  for (const sibling of siblings) {
1742
1751
  if (sibling === path)
1743
1752
  continue;
1744
1753
  const other = await readTestRunReport(sibling, roots);
1745
- if (!other.ok || other.parsed === null || typeof other.parsed !== 'object')
1754
+ if (!other.ok)
1746
1755
  continue;
1747
- const adapted = playwrightTestRun(other.parsed);
1748
- if (adapted !== null && 'refusal' in adapted)
1749
- continue;
1750
- siblingFiles.push(...coveredTestFiles(adapted ?? other.parsed, bindRoot));
1756
+ siblingFiles.push(...coveredTestFiles(other.parsed, bindRoot));
1751
1757
  }
1752
1758
  // The tree hash is the tool cache's (T12958), so a bound test-run and a
1753
1759
  // cached tool run of the same content agree.
@@ -1892,6 +1898,7 @@ async function validateQaRun(path, roots, taskId) {
1892
1898
  ...(gaps?.nonCodeChange ? { nonCodeChange: true } : {}),
1893
1899
  ...(gaps?.configPaths ? { configPaths: gaps.configPaths } : {}),
1894
1900
  ...(gaps?.untrackedSetAside ? { untrackedSetAside: gaps.untrackedSetAside } : {}),
1901
+ ...(gaps?.owedChecks ? { owedChecks: gaps.owedChecks } : {}),
1895
1902
  },
1896
1903
  };
1897
1904
  }
@@ -2109,7 +2116,8 @@ async function validateTool(tool, roots, context, siblingCommitSha) {
2109
2116
  ? (await import('./change-set.js')).resolveChangeSetRoot(roots.storeRoot, context.task.id)
2110
2117
  .root
2111
2118
  : roots.executionRoot;
2112
- const changed = scopedChangedPaths(root)?.paths ?? [];
2119
+ const scoped = scopedChangedPaths(root);
2120
+ const changed = scoped?.paths ?? [];
2113
2121
  const treeHash = isNonCodeOnlyChange(changed) ? (await captureTree(root)).tree : null;
2114
2122
  if (treeHash) {
2115
2123
  return {
@@ -2121,6 +2129,11 @@ async function validateTool(tool, roots, context, siblingCommitSha) {
2121
2129
  notApplicable: true,
2122
2130
  nonCodeChange: true,
2123
2131
  treeHash,
2132
+ // T14125: a stray untracked code file is not this change; once one
2133
+ // is committed, `cleo complete` refuses this result.
2134
+ ...(scoped && scoped.untrackedSetAside.length > 0
2135
+ ? { untrackedSetAside: scoped.untrackedSetAside }
2136
+ : {}),
2124
2137
  reason: `no test reads this change: it touches only non-code files (${changed.join(', ')})`,
2125
2138
  },
2126
2139
  };
@@ -2214,6 +2227,11 @@ async function validateTool(tool, roots, context, siblingCommitSha) {
2214
2227
  // T13132: a heavy tool here runs the whole project (the affected scope
2215
2228
  // returned above); status marks it scope=full with its task.
2216
2229
  const taskId = context?.task.id;
2230
+ if (resolution.command.canonical === 'test') {
2231
+ const foreign = await workspaceResolutionRefusal(executionRoot);
2232
+ if (foreign !== null)
2233
+ return foreign;
2234
+ }
2217
2235
  const result = await runToolCached(resolution.command, projectRoot, {
2218
2236
  executionRoot,
2219
2237
  semaphoreOptions: {
@@ -2714,13 +2732,19 @@ async function validateDecision(decisionId, roots) {
2714
2732
  * task in its title, body or head branch, or its diff intersects the task's
2715
2733
  * declared files — and when files are declared, the diff MUST intersect them.
2716
2734
  *
2735
+ * T14094: a task with no declared files may also credit a PR merged under
2736
+ * another task, through the criteria the same write names with
2737
+ * `satisfies:<task>#AC…` ({@link creditRefusal}).
2738
+ *
2717
2739
  * @param prNumber - The PR being linked.
2718
2740
  * @param pr - The verified PR (title, body, head branch, changed files).
2719
2741
  * @param context - The task the evidence is being recorded for.
2742
+ * @param mergedAt - When the PR merged, for the credited-criteria edit check.
2720
2743
  * @returns The refusal, or null when the PR is linked to the task.
2721
2744
  * @task T12634
2745
+ * @task T14094
2722
2746
  */
2723
- export function checkPrTaskLinkage(prNumber, pr, context) {
2747
+ export function checkPrTaskLinkage(prNumber, pr, context, mergedAt) {
2724
2748
  if (pr.changedFileCount !== pr.changedPaths.length || pr.changedPaths.length === 0) {
2725
2749
  return {
2726
2750
  ok: false,
@@ -2732,13 +2756,67 @@ export function checkPrTaskLinkage(prNumber, pr, context) {
2732
2756
  const taskMention = new RegExp(`(^|[^A-Za-z0-9])${context.task.id}([^A-Za-z0-9]|$)`);
2733
2757
  const explicitlyLinked = taskMention.test(`${pr.title}\n${pr.body}\n${pr.headRefName}`);
2734
2758
  const scopeIntersects = declaredFiles.length > 0 && diffIntersectsAc(pr.changedPaths, declaredFiles);
2735
- if ((!explicitlyLinked && !scopeIntersects) || (declaredFiles.length > 0 && !scopeIntersects)) {
2759
+ if (declaredFiles.length > 0 && !scopeIntersects) {
2736
2760
  return {
2737
2761
  ok: false,
2738
2762
  codeName: 'E_EVIDENCE_CONTENT_MISMATCH',
2739
- reason: `PR #${prNumber} does not establish a relationship to task ${context.task.id}. Changed artifacts: ${pr.changedPaths.join(', ')}. Declare task files or cite the exact task in the PR; declared scope must intersect the diff.`,
2763
+ reason: `PR #${prNumber} changes none of task ${context.task.id}'s declared files (${declaredFiles.join(', ')}). Changed artifacts: ${pr.changedPaths.join(', ')}.`,
2740
2764
  };
2741
2765
  }
2766
+ if (explicitlyLinked || scopeIntersects)
2767
+ return null;
2768
+ const refusal = creditRefusal(prNumber, pr.changedPaths, context, mergedAt);
2769
+ return refusal === null
2770
+ ? null
2771
+ : { ok: false, codeName: 'E_EVIDENCE_CONTENT_MISMATCH', reason: refusal };
2772
+ }
2773
+ /**
2774
+ * Why a PR that neither cites a task nor declares its files cannot be credited
2775
+ * to it, or null when it can (T14094: work shipped under another task). The
2776
+ * write must name criteria with `satisfies:<task>#AC…`; each must name at
2777
+ * least one repo path, the PR must have changed every one of them, and none
2778
+ * may have been edited after the PR merged, so a criterion cannot be made to
2779
+ * match after the fact. The refusal names that one route, and for a task the
2780
+ * other task fully covers, the duplicate close.
2781
+ *
2782
+ * @param prNumber - The PR being credited.
2783
+ * @param changedPaths - Its complete changed-file list.
2784
+ * @param context - The task, with the criteria the write credits.
2785
+ * @param mergedAt - When the PR merged; unknown refuses any credit.
2786
+ * @returns The refusal, or null.
2787
+ * @task T14094
2788
+ */
2789
+ function creditRefusal(prNumber, changedPaths, context, mergedAt) {
2790
+ const id = context.task.id;
2791
+ const credited = context.creditedCriteria ?? [];
2792
+ if (credited.length === 0)
2793
+ return (`PR #${prNumber} does not cite task ${id} and ${id} declares no files, so it is not ${id}'s work. ` +
2794
+ `To credit a PR merged under another task, name the criteria it delivers in the same write ` +
2795
+ `(satisfies:${id}#AC<n>); each must name the files the PR changed. If another task fully covers ${id}, ` +
2796
+ `close it with cleo cancel ${id} --reason "covered by T<n>" and cleo relates add ${id} T<n> duplicates "<reason>". ` +
2797
+ `Changed artifacts: ${changedPaths.join(', ')}.`);
2798
+ // Credit is for merged PRs only; without the merge time the edit check below cannot run.
2799
+ // Times here are zoned ISO-8601 (GitHub's mergedAt; criteria normalised to
2800
+ // UTC by the writer), so Date.parse reads them exactly.
2801
+ const mergedMs = mergedAt ? Date.parse(mergedAt) : Number.NaN;
2802
+ if (Number.isNaN(mergedMs))
2803
+ return `PR #${prNumber}'s merge time is unknown, so its credited criteria cannot be checked for later edits.`;
2804
+ const changed = new Set(changedPaths.map(normalizeEvidencePath));
2805
+ for (const criterion of credited) {
2806
+ // An edit gives a criterion a new row, so its creation time counts too.
2807
+ const written = criterion.updatedAt ?? criterion.createdAt;
2808
+ const writtenMs = written ? Date.parse(written) : Number.NaN;
2809
+ if (Number.isNaN(writtenMs))
2810
+ return `${id}#${criterion.alias} has no readable write time, so it cannot be shown to predate PR #${prNumber}'s merge.`;
2811
+ if (writtenMs > mergedMs)
2812
+ return `${id}#${criterion.alias} was written or edited at ${written}, after PR #${prNumber} merged at ${mergedAt}; a criterion changed after the merge cannot be credited by it.`;
2813
+ const paths = extractTaskAcFilesWithProvenance({ acceptance: [criterion.text] }).files ?? [];
2814
+ if (paths.length === 0)
2815
+ return `${id}#${criterion.alias} names no file, so PR #${prNumber} cannot be shown to deliver it; credit only criteria that name the files the PR changed.`;
2816
+ const missing = paths.filter((path) => !changed.has(normalizeEvidencePath(path)));
2817
+ if (missing.length > 0)
2818
+ return `PR #${prNumber} did not change ${missing.join(', ')}, which ${id}#${criterion.alias} names, so it cannot be credited with ${criterion.alias}.`;
2819
+ }
2742
2820
  return null;
2743
2821
  }
2744
2822
  /**
@@ -2789,10 +2867,10 @@ async function validatePrAtom(prNumber, roots, context, componentPrNumber) {
2789
2867
  // T12671: `pr:<component>@<integration>` — the integration PR is the
2790
2868
  // provenance (merge commit, required workflows); the component PR is the
2791
2869
  // task's link and its change. Both are verified, both are recorded.
2792
- const linked = await linkedPrChange(prNumber, result, roots, componentPrNumber);
2870
+ const linked = await linkedPrChange(prNumber, result, roots, componentPrNumber, undefined, context.task.id);
2793
2871
  if (!linked.ok)
2794
2872
  return linked;
2795
- const unlinked = checkPrTaskLinkage(linked.prNumber, linked.pr, context);
2873
+ const unlinked = checkPrTaskLinkage(linked.prNumber, linked.pr, context, result.mergedAt);
2796
2874
  if (unlinked)
2797
2875
  return unlinked;
2798
2876
  const changedPaths = linked.pr.changedPaths;
@@ -2848,14 +2926,40 @@ async function validatePrAtom(prNumber, roots, context, componentPrNumber) {
2848
2926
  * @param prNumber - The (integration) PR the atom names.
2849
2927
  * @param landing - Its verified resolution.
2850
2928
  * @param roots - Evidence roots (git runs in the execution root).
2929
+ * Without a component, a PR that does not name the task itself is linked
2930
+ * through a merge commit on it that names the task (T14164), carrying that
2931
+ * merge's own landed change only, never the whole PR's files.
2932
+ *
2851
2933
  * @param componentPrNumber - Component PR, when the atom names one.
2852
2934
  * @param view - Component PR reader (tests inject; defaults to `gh pr view`).
2935
+ * @param taskId - The task the evidence is for, to find a merge that names it.
2853
2936
  * @returns The linkage subject, or the refusal.
2854
2937
  * @task T12671
2938
+ * @task T14164
2855
2939
  */
2856
- export async function linkedPrChange(prNumber, landing, roots, componentPrNumber, view) {
2857
- if (componentPrNumber === undefined)
2858
- return { ok: true, prNumber, pr: landing };
2940
+ export async function linkedPrChange(prNumber, landing, roots, componentPrNumber, view, taskId) {
2941
+ if (componentPrNumber === undefined) {
2942
+ if (taskId === undefined ||
2943
+ new RegExp(`(^|[^A-Za-z0-9])${taskId}([^A-Za-z0-9]|$)`).test(`${landing.title}\n${landing.body}\n${landing.headRefName}`))
2944
+ return { ok: true, prNumber, pr: landing };
2945
+ const { taskMergesInLanding } = await import('./component-pr.js');
2946
+ const merged = taskMergesInLanding(roots.executionRoot, taskId, landing);
2947
+ if (merged === null)
2948
+ return { ok: true, prNumber, pr: landing };
2949
+ const changedPaths = [...merged.files, ...merged.deleted];
2950
+ return {
2951
+ ok: true,
2952
+ prNumber,
2953
+ pr: {
2954
+ title: merged.merges.map((m) => m.subject).join('\n'),
2955
+ body: '',
2956
+ headRefName: landing.headRefName,
2957
+ changedPaths,
2958
+ changedFileCount: changedPaths.length,
2959
+ ...(merged.deleted.length > 0 ? { deletedPaths: merged.deleted } : {}),
2960
+ },
2961
+ };
2962
+ }
2859
2963
  const { resolveComponentPr } = await import('./component-pr.js');
2860
2964
  const c = await resolveComponentPr(componentPrNumber, {
2861
2965
  prNumber,
@@ -3034,6 +3138,12 @@ export function checkTaskEvidenceContext(context, gate, atoms) {
3034
3138
  // T12634: a `ci:` result is bound to the task it was validated for, like `pr:`.
3035
3139
  if (atoms.some((atom) => atom.kind === 'ci' && atom.taskId !== context.task.id))
3036
3140
  return 'CI evidence lacks verified task linkage; re-verify with current task context.';
3141
+ // T14083: a probed deployment is bound to the task it was verified for.
3142
+ if (atoms.some((atom) => atom.kind === 'deploy' && atom.taskId !== context.task.id))
3143
+ return 'Deploy evidence lacks verified task linkage; re-verify with current task context.';
3144
+ // T14149: a repeat-run is judged relevant to the task it was verified for.
3145
+ if (atoms.some((atom) => atom.kind === 'ci-run' && atom.taskId !== context.task.id))
3146
+ return 'ci-run evidence lacks verified task linkage; re-verify with current task context.';
3037
3147
  // T12848: several PRs in one write are unambiguous — each `files:` path is
3038
3148
  // pinned to the merge of the PR that changed it (prMergeCommitForPath), and
3039
3149
  // every PR must be proven by an artifact it changed that was READ AT ITS OWN
@@ -3072,6 +3182,7 @@ export function checkTaskEvidenceContext(context, gate, atoms) {
3072
3182
  !atoms.some((atom) => atom.kind === 'test-run' ||
3073
3183
  atom.kind === 'qa-run' ||
3074
3184
  atom.kind === 'ci' ||
3185
+ (atom.kind === 'deploy' && atom.satisfiesGates.includes(gate)) ||
3075
3186
  (atom.kind === 'tool' && (!atom.notApplicable || atom.nonCodeChange === true)))) {
3076
3187
  return `Code task ${context.task.id} requires an actual verification result for ${gate}; absence of a toolchain is not a passing result.`;
3077
3188
  }
@@ -3096,11 +3207,31 @@ export function checkTaskEvidenceContext(context, gate, atoms) {
3096
3207
  !atoms.some((atom) => atom.kind === 'ci')) {
3097
3208
  // T13745: a non-code-only change has no TypeScript to typecheck.
3098
3209
  const nonCode = atoms.some((atom) => atom.kind === 'qa-run' && atom.nonCodeChange === true);
3099
- const missing = ['typecheck', 'lint'].filter((check) => !(nonCode && check === 'typecheck') &&
3100
- !atoms.some((atom) => (atom.kind === 'qa-run' && atom.check === check) ||
3101
- (atom.kind === 'tool' && canonicalToolName(atom.tool) === check)));
3102
- if (missing.length > 0)
3103
- return `qaPassed with qa-run receipts needs a typecheck and a lint result; missing ${missing.join(' and ')}: add qa-run:<${missing[0]}-receipt.json> or tool:${missing[0]}.`;
3210
+ // T14089: a check is owed only for the changed paths it reads, as the
3211
+ // receipts judged them at bind time. A receipt bound without that
3212
+ // judgement (older, or nothing judged) owes both, as before.
3213
+ const receipts = atoms.flatMap((atom) => (atom.kind === 'qa-run' ? [atom] : []));
3214
+ const owedPaths = (check) => receipts.some((r) => r.owedChecks === undefined)
3215
+ ? null
3216
+ : [...new Set(receipts.flatMap((r) => r.owedChecks?.[check] ?? []))];
3217
+ const missing = ['typecheck', 'lint'].filter((check) => {
3218
+ if (nonCode && check === 'typecheck')
3219
+ return false;
3220
+ const paths = owedPaths(check);
3221
+ if (paths !== null && paths.length === 0)
3222
+ return false;
3223
+ return !atoms.some((atom) => (atom.kind === 'qa-run' && atom.check === check) ||
3224
+ (atom.kind === 'tool' && canonicalToolName(atom.tool) === check));
3225
+ });
3226
+ if (missing.length > 0) {
3227
+ const named = missing.map((check) => {
3228
+ const paths = owedPaths(check);
3229
+ return paths === null
3230
+ ? check
3231
+ : `${check} (it reads ${paths.slice(0, 5).join(', ')}${paths.length > 5 ? `, … (${paths.length})` : ''})`;
3232
+ });
3233
+ return `qaPassed with qa-run receipts needs a result for each check that reads the changed paths; missing ${named.join(' and ')}: add qa-run:<${missing[0]}-receipt.json> or tool:${missing[0]}.`;
3234
+ }
3104
3235
  // T13580: receipts of one kind cover the change together; a check also
3105
3236
  // recorded as a whole-project tool: run needs no receipt coverage.
3106
3237
  const gap = qaRunUnionGapReason(atoms.filter((atom) => atom.kind === 'qa-run' &&
@@ -3240,18 +3371,43 @@ export function decisionOnlyRouteNote(gate, implemented) {
3240
3371
  // ---------------------------------------------------------------------------
3241
3372
  // Gate minimum evaluation
3242
3373
  // ---------------------------------------------------------------------------
3374
+ /**
3375
+ * Whether a probed `deploy:` atom stands for `gate` (T14083): only for the
3376
+ * gates the project listed in `evidence.deploySatisfies` when it was verified.
3377
+ * Static gate requirements never name `deploy`, so no gate takes one by default.
3378
+ */
3379
+ function deploySatisfiesGate(gate, atoms) {
3380
+ return atoms.some((atom) => atom.kind === 'deploy' && atom.satisfiesGates.includes(gate));
3381
+ }
3382
+ /**
3383
+ * The profile whose extra combinations apply to these atoms (T14187): the
3384
+ * task's, while every `files:` path is a document. A code path keeps the base
3385
+ * requirements, so a profiled change that touched code still owes a commit
3386
+ * or PR.
3387
+ */
3388
+ function profileForAtoms(profile, atoms) {
3389
+ if (!profile)
3390
+ return null;
3391
+ const paths = atoms.flatMap((atom) => atom.kind === 'files' ? atom.files.map((f) => f.path) : []);
3392
+ return paths.every(isDocumentEvidencePath) ? profile : null;
3393
+ }
3243
3394
  /**
3244
3395
  * Check whether a set of validated atoms satisfies the minimum evidence
3245
3396
  * required for a given gate.
3246
3397
  *
3247
3398
  * @param gate - The gate being verified
3248
3399
  * @param atoms - Validated atoms
3400
+ * @param profile - The task's gate profile ({@link evidenceProfileOf}); its
3401
+ * extra combinations apply while every `files:` path is a document (T14187).
3249
3402
  * @returns null when satisfied; otherwise the reason message
3250
3403
  *
3251
3404
  * @task T832
3405
+ * @task T14187
3252
3406
  * @adr ADR-051 §2.3
3253
3407
  */
3254
- export function checkGateEvidenceMinimum(gate, atoms) {
3408
+ export function checkGateEvidenceMinimum(gate, atoms, profile) {
3409
+ if (deploySatisfiesGate(gate, atoms))
3410
+ return null;
3255
3411
  // Delegate to the SSoT in @cleocode/contracts (T10337). validateEvidenceForGate
3256
3412
  // only inspects `.kind`; the post-validation EvidenceAtom union includes the
3257
3413
  // `override` kind which is never present here (override-evidence flows skip
@@ -3267,7 +3423,7 @@ export function checkGateEvidenceMinimum(gate, atoms) {
3267
3423
  const kind = atom.kind;
3268
3424
  projected.push({ kind });
3269
3425
  }
3270
- const result = validateEvidenceForGate(gate, projected);
3426
+ const result = validateEvidenceForGate(gate, projected, profileForAtoms(profile, atoms));
3271
3427
  return result.ok ? null : result.message;
3272
3428
  }
3273
3429
  /**
@@ -3281,12 +3437,16 @@ export function checkGateEvidenceMinimum(gate, atoms) {
3281
3437
  *
3282
3438
  * @param gate - The gate being verified
3283
3439
  * @param atoms - Validated atoms
3440
+ * @param profile - The task's gate profile, as for {@link checkGateEvidenceMinimum} (T14187).
3284
3441
  * @returns `null` when satisfied; `{ message, hint }` when not
3285
3442
  *
3286
3443
  * @task T9949
3444
+ * @task T14187
3287
3445
  * @adr ADR-051 §2.3
3288
3446
  */
3289
- export function checkGateEvidenceMinimumDetailed(gate, atoms) {
3447
+ export function checkGateEvidenceMinimumDetailed(gate, atoms, profile) {
3448
+ if (deploySatisfiesGate(gate, atoms))
3449
+ return null;
3290
3450
  const projected = [];
3291
3451
  for (const atom of atoms) {
3292
3452
  if (atom.kind === 'override')
@@ -3294,7 +3454,7 @@ export function checkGateEvidenceMinimumDetailed(gate, atoms) {
3294
3454
  const kind = atom.kind;
3295
3455
  projected.push({ kind });
3296
3456
  }
3297
- const result = validateEvidenceForGate(gate, projected);
3457
+ const result = validateEvidenceForGate(gate, projected, profileForAtoms(profile, atoms));
3298
3458
  return result.ok ? null : { message: result.message, hint: result.hint };
3299
3459
  }
3300
3460
  /**
@@ -3343,7 +3503,8 @@ export function composeGateEvidence(atoms, capturedBy, override, overrideReason,
3343
3503
  return atom.kind === 'tool' ||
3344
3504
  atom.kind === 'test-run' ||
3345
3505
  atom.kind === 'qa-run' ||
3346
- atom.kind === 'ci'
3506
+ atom.kind === 'ci' ||
3507
+ atom.kind === 'ci-run'
3347
3508
  ? [index]
3348
3509
  : [];
3349
3510
  });
@@ -3395,7 +3556,13 @@ export function composeGateEvidence(atoms, capturedBy, override, overrideReason,
3395
3556
  * @task T13003
3396
3557
  */
3397
3558
  export async function boundHeadGapReason(task, atoms, projectRoot) {
3398
- const runs = atoms.filter((a) => (a.kind === 'test-run' || a.kind === 'qa-run' || a.kind === 'tool') &&
3559
+ // T14083: a probed deployment is bound to the commit it serves.
3560
+ // T14149: a repeat-run is bound to the commit it tested.
3561
+ const runs = atoms.filter((a) => (a.kind === 'test-run' ||
3562
+ a.kind === 'qa-run' ||
3563
+ a.kind === 'tool' ||
3564
+ a.kind === 'deploy' ||
3565
+ a.kind === 'ci-run') &&
3399
3566
  typeof a.headSha === 'string');
3400
3567
  if (runs.length === 0)
3401
3568
  return null;
@@ -3468,7 +3635,11 @@ export async function boundHeadGapReason(task, atoms, projectRoot) {
3468
3635
  continue;
3469
3636
  const bound = run.kind === 'tool'
3470
3637
  ? `tool:${run.tool} ran at ${head.slice(0, 12)}`
3471
- : `${run.kind}:${run.path} was bound at ${head.slice(0, 12)}`;
3638
+ : run.kind === 'deploy'
3639
+ ? `deploy:${run.provider}/${run.deploymentId} serves ${head.slice(0, 12)}`
3640
+ : run.kind === 'ci-run'
3641
+ ? `ci-run:${run.runId} (${run.test} x${run.runs}) tested ${head.slice(0, 12)}`
3642
+ : `${run.kind}:${run.path} was bound at ${head.slice(0, 12)}`;
3472
3643
  if (missing !== null) {
3473
3644
  return (`${bound}, and git cannot read the commits that relate it to ${task.id}'s implemented ` +
3474
3645
  `commit ${shas} (missing object, or a shallow clone: ${missing}). ` +
@@ -3483,6 +3654,15 @@ export async function boundHeadGapReason(task, atoms, projectRoot) {
3483
3654
  'the branch was rebased, amended or squash-merged after implemented was recorded. ' +
3484
3655
  `Re-record implemented (cleo verify ${task.id} --gate implemented ...), then the run.`);
3485
3656
  }
3657
+ if (run.kind === 'ci-run') {
3658
+ return (`${bound}, which does not contain ${task.id}'s implemented commit ${shas}: dispatch ` +
3659
+ 'repeat-run.yml on the default branch after the change merged, and record that run.');
3660
+ }
3661
+ if (run.kind === 'deploy') {
3662
+ return (`${bound}, which does not contain ${task.id}'s implemented commit ${shas} (neither an ` +
3663
+ 'ancestor nor a patch-equivalent copy): that deployment predates or excludes the change. ' +
3664
+ 'Deploy a build that contains it and probe that deployment.');
3665
+ }
3486
3666
  return (`${bound}, which never saw ${task.id}'s implemented commit ${shas}: it tested other ` +
3487
3667
  "work (another task's checkout, or a commit before the change). Run the tests on this " +
3488
3668
  "task's checkout and record that report.");
@@ -3694,7 +3874,7 @@ export async function revalidateEvidence(evidence, projectRoot, gate, taskId, op
3694
3874
  !(classifyEvidenceTask(context) !== 'code' &&
3695
3875
  isDecisionOnlyImplementation(task.verification?.evidence?.implemented) &&
3696
3876
  DECISION_ONLY_RELAXED_GATES.includes(gate))) {
3697
- reason = checkGateEvidenceMinimum(gate, evidence.atoms);
3877
+ reason = checkGateEvidenceMinimum(gate, evidence.atoms, evidenceProfileOf(task));
3698
3878
  }
3699
3879
  if (!reason && evidence.scope) {
3700
3880
  if (evidence.scope.taskId !== taskId || evidence.scope.gate !== gate)
@@ -3913,6 +4093,20 @@ export async function revalidateEvidence(evidence, projectRoot, gate, taskId, op
3913
4093
  failed.push({ atom, reason: recheck.reason });
3914
4094
  break;
3915
4095
  }
4096
+ case 'ci-run': {
4097
+ // T14149: a re-run of the repeat-run that went red, or a run that moved
4098
+ // to another commit, no longer stands; containment is re-checked above.
4099
+ const { recheckCiRunAtom } = await import('../release/ci-run-evidence.js');
4100
+ const recheck = recheckCiRunAtom(atom, resolveEvidenceExecutionRoot(projectRoot));
4101
+ if (!recheck.ok)
4102
+ failed.push({ atom, reason: recheck.reason });
4103
+ break;
4104
+ }
4105
+ case 'deploy':
4106
+ // T14083: the probe is not re-run (the deployment may have moved on
4107
+ // since); the SHA containment it carries is re-checked above, through
4108
+ // boundHeadGapReason.
4109
+ break;
3916
4110
  case 'pr':
3917
4111
  // PR atoms capture (prNumber, mergedAt, mergeCommitSha) at verify
3918
4112
  // time. A merged PR's mergedAt is immutable, so the atom is trusted