@allocator-one/rcl 4.4.19

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 (663) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1415 -0
  3. package/dist/ci.d.ts +16 -0
  4. package/dist/ci.js +42 -0
  5. package/dist/ci.js.map +1 -0
  6. package/dist/config/data-dir.d.ts +6 -0
  7. package/dist/config/data-dir.js +12 -0
  8. package/dist/config/data-dir.js.map +1 -0
  9. package/dist/config/defaults.d.ts +82 -0
  10. package/dist/config/defaults.js +89 -0
  11. package/dist/config/defaults.js.map +1 -0
  12. package/dist/config/harness.d.ts +40 -0
  13. package/dist/config/harness.js +151 -0
  14. package/dist/config/harness.js.map +1 -0
  15. package/dist/config/loader.d.ts +13 -0
  16. package/dist/config/loader.js +131 -0
  17. package/dist/config/loader.js.map +1 -0
  18. package/dist/config/provider-concurrency.d.ts +4 -0
  19. package/dist/config/provider-concurrency.js +6 -0
  20. package/dist/config/provider-concurrency.js.map +1 -0
  21. package/dist/config/providers.d.ts +3 -0
  22. package/dist/config/providers.js +9 -0
  23. package/dist/config/providers.js.map +1 -0
  24. package/dist/config/schema.d.ts +179 -0
  25. package/dist/config/schema.js +115 -0
  26. package/dist/config/schema.js.map +1 -0
  27. package/dist/consensus/claim-contract.d.ts +9 -0
  28. package/dist/consensus/claim-contract.js +307 -0
  29. package/dist/consensus/claim-contract.js.map +1 -0
  30. package/dist/consensus/claim-identity.d.ts +5 -0
  31. package/dist/consensus/claim-identity.js +39 -0
  32. package/dist/consensus/claim-identity.js.map +1 -0
  33. package/dist/consensus/deduper.d.ts +43 -0
  34. package/dist/consensus/deduper.js +1052 -0
  35. package/dist/consensus/deduper.js.map +1 -0
  36. package/dist/consensus/finding-identity.d.ts +20 -0
  37. package/dist/consensus/finding-identity.js +49 -0
  38. package/dist/consensus/finding-identity.js.map +1 -0
  39. package/dist/consensus/gating.d.ts +215 -0
  40. package/dist/consensus/gating.js +699 -0
  41. package/dist/consensus/gating.js.map +1 -0
  42. package/dist/consensus/parser.d.ts +22 -0
  43. package/dist/consensus/parser.js +255 -0
  44. package/dist/consensus/parser.js.map +1 -0
  45. package/dist/consensus/semantic-deduper.d.ts +10 -0
  46. package/dist/consensus/semantic-deduper.js +70 -0
  47. package/dist/consensus/semantic-deduper.js.map +1 -0
  48. package/dist/consensus/types.d.ts +196 -0
  49. package/dist/consensus/types.js +2 -0
  50. package/dist/consensus/types.js.map +1 -0
  51. package/dist/consensus/verification-contract.d.ts +8 -0
  52. package/dist/consensus/verification-contract.js +95 -0
  53. package/dist/consensus/verification-contract.js.map +1 -0
  54. package/dist/consensus/voter.d.ts +29 -0
  55. package/dist/consensus/voter.js +394 -0
  56. package/dist/consensus/voter.js.map +1 -0
  57. package/dist/converge/attempt-budget.d.ts +88 -0
  58. package/dist/converge/attempt-budget.js +717 -0
  59. package/dist/converge/attempt-budget.js.map +1 -0
  60. package/dist/converge/bound-fix-recovery-source.d.ts +24 -0
  61. package/dist/converge/bound-fix-recovery-source.js +25 -0
  62. package/dist/converge/bound-fix-recovery-source.js.map +1 -0
  63. package/dist/converge/bound-fix-recovery.d.ts +15 -0
  64. package/dist/converge/bound-fix-recovery.js +65 -0
  65. package/dist/converge/bound-fix-recovery.js.map +1 -0
  66. package/dist/converge/correction-anchors.d.ts +1 -0
  67. package/dist/converge/correction-anchors.js +2 -0
  68. package/dist/converge/correction-anchors.js.map +1 -0
  69. package/dist/converge/cycle-remote.d.ts +6 -0
  70. package/dist/converge/cycle-remote.js +41 -0
  71. package/dist/converge/cycle-remote.js.map +1 -0
  72. package/dist/converge/delivery-reconciliation.d.ts +17 -0
  73. package/dist/converge/delivery-reconciliation.js +53 -0
  74. package/dist/converge/delivery-reconciliation.js.map +1 -0
  75. package/dist/converge/finding-identity.d.ts +6 -0
  76. package/dist/converge/finding-identity.js +7 -0
  77. package/dist/converge/finding-identity.js.map +1 -0
  78. package/dist/converge/fresh-review.d.ts +37 -0
  79. package/dist/converge/fresh-review.js +351 -0
  80. package/dist/converge/fresh-review.js.map +1 -0
  81. package/dist/converge/launch-guard.d.ts +55 -0
  82. package/dist/converge/launch-guard.js +363 -0
  83. package/dist/converge/launch-guard.js.map +1 -0
  84. package/dist/converge/launch-preflight.d.ts +5 -0
  85. package/dist/converge/launch-preflight.js +41 -0
  86. package/dist/converge/launch-preflight.js.map +1 -0
  87. package/dist/converge/launch-record.d.ts +136 -0
  88. package/dist/converge/launch-record.js +86 -0
  89. package/dist/converge/launch-record.js.map +1 -0
  90. package/dist/converge/legacy-launch-health.d.ts +311 -0
  91. package/dist/converge/legacy-launch-health.js +159 -0
  92. package/dist/converge/legacy-launch-health.js.map +1 -0
  93. package/dist/converge/legacy-roster.d.ts +29 -0
  94. package/dist/converge/legacy-roster.js +268 -0
  95. package/dist/converge/legacy-roster.js.map +1 -0
  96. package/dist/converge/native-lock.d.ts +18 -0
  97. package/dist/converge/native-lock.js +122 -0
  98. package/dist/converge/native-lock.js.map +1 -0
  99. package/dist/converge/ordinary-pending-export.d.ts +67 -0
  100. package/dist/converge/ordinary-pending-export.js +175 -0
  101. package/dist/converge/ordinary-pending-export.js.map +1 -0
  102. package/dist/converge/ordinary-pending-package.d.ts +28 -0
  103. package/dist/converge/ordinary-pending-package.js +36 -0
  104. package/dist/converge/ordinary-pending-package.js.map +1 -0
  105. package/dist/converge/pending-legacy-resume.d.ts +119 -0
  106. package/dist/converge/pending-legacy-resume.js +720 -0
  107. package/dist/converge/pending-legacy-resume.js.map +1 -0
  108. package/dist/converge/pending-recovery-source.d.ts +131 -0
  109. package/dist/converge/pending-recovery-source.js +71 -0
  110. package/dist/converge/pending-recovery-source.js.map +1 -0
  111. package/dist/converge/recovery-launch.d.ts +66 -0
  112. package/dist/converge/recovery-launch.js +518 -0
  113. package/dist/converge/recovery-launch.js.map +1 -0
  114. package/dist/converge/retained-report.d.ts +29 -0
  115. package/dist/converge/retained-report.js +218 -0
  116. package/dist/converge/retained-report.js.map +1 -0
  117. package/dist/converge/retry-source.d.ts +25 -0
  118. package/dist/converge/retry-source.js +13 -0
  119. package/dist/converge/retry-source.js.map +1 -0
  120. package/dist/converge/review-cycle.d.ts +49 -0
  121. package/dist/converge/review-cycle.js +26 -0
  122. package/dist/converge/review-cycle.js.map +1 -0
  123. package/dist/converge/round-gap-schema.d.ts +66 -0
  124. package/dist/converge/round-gap-schema.js +51 -0
  125. package/dist/converge/round-gap-schema.js.map +1 -0
  126. package/dist/converge/round-gap.d.ts +38 -0
  127. package/dist/converge/round-gap.js +334 -0
  128. package/dist/converge/round-gap.js.map +1 -0
  129. package/dist/converge/run-state.d.ts +224 -0
  130. package/dist/converge/run-state.js +564 -0
  131. package/dist/converge/run-state.js.map +1 -0
  132. package/dist/converge/stale-report-schema.d.ts +42 -0
  133. package/dist/converge/stale-report-schema.js +79 -0
  134. package/dist/converge/stale-report-schema.js.map +1 -0
  135. package/dist/converge/stale-report-storage.d.ts +47 -0
  136. package/dist/converge/stale-report-storage.js +172 -0
  137. package/dist/converge/stale-report-storage.js.map +1 -0
  138. package/dist/converge/stale-report.d.ts +14 -0
  139. package/dist/converge/stale-report.js +213 -0
  140. package/dist/converge/stale-report.js.map +1 -0
  141. package/dist/converge/target-ownership.d.ts +29 -0
  142. package/dist/converge/target-ownership.js +95 -0
  143. package/dist/converge/target-ownership.js.map +1 -0
  144. package/dist/converge/terminal-rejection-schema.d.ts +42 -0
  145. package/dist/converge/terminal-rejection-schema.js +39 -0
  146. package/dist/converge/terminal-rejection-schema.js.map +1 -0
  147. package/dist/converge/terminal-rejection.d.ts +13 -0
  148. package/dist/converge/terminal-rejection.js +275 -0
  149. package/dist/converge/terminal-rejection.js.map +1 -0
  150. package/dist/coordination/registry-lock.d.ts +82 -0
  151. package/dist/coordination/registry-lock.js +498 -0
  152. package/dist/coordination/registry-lock.js.map +1 -0
  153. package/dist/discuss.d.ts +53 -0
  154. package/dist/discuss.js +112 -0
  155. package/dist/discuss.js.map +1 -0
  156. package/dist/dispatch/adapter.d.ts +32 -0
  157. package/dist/dispatch/adapter.js +2 -0
  158. package/dist/dispatch/adapter.js.map +1 -0
  159. package/dist/dispatch/anthropic.d.ts +10 -0
  160. package/dist/dispatch/anthropic.js +235 -0
  161. package/dist/dispatch/anthropic.js.map +1 -0
  162. package/dist/dispatch/async-lane.d.ts +127 -0
  163. package/dist/dispatch/async-lane.js +414 -0
  164. package/dist/dispatch/async-lane.js.map +1 -0
  165. package/dist/dispatch/capture-council.d.ts +63 -0
  166. package/dist/dispatch/capture-council.js +137 -0
  167. package/dist/dispatch/capture-council.js.map +1 -0
  168. package/dist/dispatch/captured-async.d.ts +67 -0
  169. package/dist/dispatch/captured-async.js +40 -0
  170. package/dist/dispatch/captured-async.js.map +1 -0
  171. package/dist/dispatch/captured-inputs.d.ts +50 -0
  172. package/dist/dispatch/captured-inputs.js +190 -0
  173. package/dist/dispatch/captured-inputs.js.map +1 -0
  174. package/dist/dispatch/checkpoint-async-context.d.ts +27 -0
  175. package/dist/dispatch/checkpoint-async-context.js +51 -0
  176. package/dist/dispatch/checkpoint-async-context.js.map +1 -0
  177. package/dist/dispatch/checkpoint-async-execution.d.ts +22 -0
  178. package/dist/dispatch/checkpoint-async-execution.js +130 -0
  179. package/dist/dispatch/checkpoint-async-execution.js.map +1 -0
  180. package/dist/dispatch/checkpoint-async-store.d.ts +134 -0
  181. package/dist/dispatch/checkpoint-async-store.js +534 -0
  182. package/dist/dispatch/checkpoint-async-store.js.map +1 -0
  183. package/dist/dispatch/checkpoint-async-unknown.d.ts +19 -0
  184. package/dist/dispatch/checkpoint-async-unknown.js +46 -0
  185. package/dist/dispatch/checkpoint-async-unknown.js.map +1 -0
  186. package/dist/dispatch/checkpoint-async.d.ts +252 -0
  187. package/dist/dispatch/checkpoint-async.js +270 -0
  188. package/dist/dispatch/checkpoint-async.js.map +1 -0
  189. package/dist/dispatch/checkpoint-gating-execution.d.ts +26 -0
  190. package/dist/dispatch/checkpoint-gating-execution.js +84 -0
  191. package/dist/dispatch/checkpoint-gating-execution.js.map +1 -0
  192. package/dist/dispatch/checkpoint-phase-limits.d.ts +5 -0
  193. package/dist/dispatch/checkpoint-phase-limits.js +11 -0
  194. package/dist/dispatch/checkpoint-phase-limits.js.map +1 -0
  195. package/dist/dispatch/checkpoint-verification-context.d.ts +15 -0
  196. package/dist/dispatch/checkpoint-verification-context.js +48 -0
  197. package/dist/dispatch/checkpoint-verification-context.js.map +1 -0
  198. package/dist/dispatch/checkpoint-verification.d.ts +213 -0
  199. package/dist/dispatch/checkpoint-verification.js +381 -0
  200. package/dist/dispatch/checkpoint-verification.js.map +1 -0
  201. package/dist/dispatch/checkpoint.d.ts +329 -0
  202. package/dist/dispatch/checkpoint.js +1418 -0
  203. package/dist/dispatch/checkpoint.js.map +1 -0
  204. package/dist/dispatch/google.d.ts +10 -0
  205. package/dist/dispatch/google.js +158 -0
  206. package/dist/dispatch/google.js.map +1 -0
  207. package/dist/dispatch/late-audit.d.ts +23 -0
  208. package/dist/dispatch/late-audit.js +133 -0
  209. package/dist/dispatch/late-audit.js.map +1 -0
  210. package/dist/dispatch/merge.d.ts +29 -0
  211. package/dist/dispatch/merge.js +112 -0
  212. package/dist/dispatch/merge.js.map +1 -0
  213. package/dist/dispatch/openai-compat.d.ts +30 -0
  214. package/dist/dispatch/openai-compat.js +166 -0
  215. package/dist/dispatch/openai-compat.js.map +1 -0
  216. package/dist/dispatch/openai.d.ts +10 -0
  217. package/dist/dispatch/openai.js +159 -0
  218. package/dist/dispatch/openai.js.map +1 -0
  219. package/dist/dispatch/original-execution.d.ts +30 -0
  220. package/dist/dispatch/original-execution.js +91 -0
  221. package/dist/dispatch/original-execution.js.map +1 -0
  222. package/dist/dispatch/original-launch.d.ts +37 -0
  223. package/dist/dispatch/original-launch.js +117 -0
  224. package/dist/dispatch/original-launch.js.map +1 -0
  225. package/dist/dispatch/quorum.d.ts +15 -0
  226. package/dist/dispatch/quorum.js +33 -0
  227. package/dist/dispatch/quorum.js.map +1 -0
  228. package/dist/dispatch/recovery-operation.d.ts +44 -0
  229. package/dist/dispatch/recovery-operation.js +146 -0
  230. package/dist/dispatch/recovery-operation.js.map +1 -0
  231. package/dist/dispatch/recovery-policy.d.ts +54 -0
  232. package/dist/dispatch/recovery-policy.js +157 -0
  233. package/dist/dispatch/recovery-policy.js.map +1 -0
  234. package/dist/dispatch/recovery.d.ts +78 -0
  235. package/dist/dispatch/recovery.js +299 -0
  236. package/dist/dispatch/recovery.js.map +1 -0
  237. package/dist/dispatch/retained-async.d.ts +13 -0
  238. package/dist/dispatch/retained-async.js +101 -0
  239. package/dist/dispatch/retained-async.js.map +1 -0
  240. package/dist/dispatch/reviewer-identity.d.ts +10 -0
  241. package/dist/dispatch/reviewer-identity.js +19 -0
  242. package/dist/dispatch/reviewer-identity.js.map +1 -0
  243. package/dist/dispatch/runner.d.ts +76 -0
  244. package/dist/dispatch/runner.js +415 -0
  245. package/dist/dispatch/runner.js.map +1 -0
  246. package/dist/dispatch/utils.d.ts +159 -0
  247. package/dist/dispatch/utils.js +381 -0
  248. package/dist/dispatch/utils.js.map +1 -0
  249. package/dist/dispatch/verification-execution.d.ts +48 -0
  250. package/dist/dispatch/verification-execution.js +252 -0
  251. package/dist/dispatch/verification-execution.js.map +1 -0
  252. package/dist/dispatch/verification-late-audit.d.ts +17 -0
  253. package/dist/dispatch/verification-late-audit.js +117 -0
  254. package/dist/dispatch/verification-late-audit.js.map +1 -0
  255. package/dist/evidence/claim-recovery/authenticated-carrier-projection.d.ts +30 -0
  256. package/dist/evidence/claim-recovery/authenticated-carrier-projection.js +100 -0
  257. package/dist/evidence/claim-recovery/authenticated-carrier-projection.js.map +1 -0
  258. package/dist/evidence/claim-recovery/authenticated-receipts.d.ts +46 -0
  259. package/dist/evidence/claim-recovery/authenticated-receipts.js +120 -0
  260. package/dist/evidence/claim-recovery/authenticated-receipts.js.map +1 -0
  261. package/dist/evidence/claim-recovery/carrier-inventory.d.ts +49 -0
  262. package/dist/evidence/claim-recovery/carrier-inventory.js +340 -0
  263. package/dist/evidence/claim-recovery/carrier-inventory.js.map +1 -0
  264. package/dist/evidence/claim-recovery/claim-index.d.ts +12 -0
  265. package/dist/evidence/claim-recovery/claim-index.js +56 -0
  266. package/dist/evidence/claim-recovery/claim-index.js.map +1 -0
  267. package/dist/evidence/claim-recovery/confirmation.d.ts +53 -0
  268. package/dist/evidence/claim-recovery/confirmation.js +151 -0
  269. package/dist/evidence/claim-recovery/confirmation.js.map +1 -0
  270. package/dist/evidence/claim-recovery/context.d.ts +15 -0
  271. package/dist/evidence/claim-recovery/context.js +40 -0
  272. package/dist/evidence/claim-recovery/context.js.map +1 -0
  273. package/dist/evidence/claim-recovery/proof-storage.d.ts +6 -0
  274. package/dist/evidence/claim-recovery/proof-storage.js +97 -0
  275. package/dist/evidence/claim-recovery/proof-storage.js.map +1 -0
  276. package/dist/evidence/claim-recovery/validation/anchors.d.ts +20 -0
  277. package/dist/evidence/claim-recovery/validation/anchors.js +22 -0
  278. package/dist/evidence/claim-recovery/validation/anchors.js.map +1 -0
  279. package/dist/evidence/claim-recovery/validation/carrier-projection.d.ts +9 -0
  280. package/dist/evidence/claim-recovery/validation/carrier-projection.js +364 -0
  281. package/dist/evidence/claim-recovery/validation/carrier-projection.js.map +1 -0
  282. package/dist/evidence/claim-recovery/validation/carrier-types.d.ts +83 -0
  283. package/dist/evidence/claim-recovery/validation/carrier-types.js +2 -0
  284. package/dist/evidence/claim-recovery/validation/carrier-types.js.map +1 -0
  285. package/dist/evidence/claim-recovery/validation/claim-split.d.ts +57 -0
  286. package/dist/evidence/claim-recovery/validation/claim-split.js +209 -0
  287. package/dist/evidence/claim-recovery/validation/claim-split.js.map +1 -0
  288. package/dist/evidence/claim-recovery/validation/claims.d.ts +20 -0
  289. package/dist/evidence/claim-recovery/validation/claims.js +108 -0
  290. package/dist/evidence/claim-recovery/validation/claims.js.map +1 -0
  291. package/dist/evidence/claim-recovery/validation/current-projection.d.ts +36 -0
  292. package/dist/evidence/claim-recovery/validation/current-projection.js +424 -0
  293. package/dist/evidence/claim-recovery/validation/current-projection.js.map +1 -0
  294. package/dist/evidence/claim-recovery/validation/lexical.d.ts +18 -0
  295. package/dist/evidence/claim-recovery/validation/lexical.js +91 -0
  296. package/dist/evidence/claim-recovery/validation/lexical.js.map +1 -0
  297. package/dist/evidence/claim-recovery/validation/materials.d.ts +19 -0
  298. package/dist/evidence/claim-recovery/validation/materials.js +160 -0
  299. package/dist/evidence/claim-recovery/validation/materials.js.map +1 -0
  300. package/dist/evidence/claim-recovery/validation/native-material.d.ts +27 -0
  301. package/dist/evidence/claim-recovery/validation/native-material.js +35 -0
  302. package/dist/evidence/claim-recovery/validation/native-material.js.map +1 -0
  303. package/dist/evidence/claim-recovery/validation/native-occurrences.d.ts +79 -0
  304. package/dist/evidence/claim-recovery/validation/native-occurrences.js +187 -0
  305. package/dist/evidence/claim-recovery/validation/native-occurrences.js.map +1 -0
  306. package/dist/evidence/claim-recovery/validation/native-state.d.ts +45 -0
  307. package/dist/evidence/claim-recovery/validation/native-state.js +385 -0
  308. package/dist/evidence/claim-recovery/validation/native-state.js.map +1 -0
  309. package/dist/evidence/claim-recovery/validation/obligations.d.ts +15 -0
  310. package/dist/evidence/claim-recovery/validation/obligations.js +65 -0
  311. package/dist/evidence/claim-recovery/validation/obligations.js.map +1 -0
  312. package/dist/evidence/claim-recovery/validation/occurrence-source.d.ts +45 -0
  313. package/dist/evidence/claim-recovery/validation/occurrence-source.js +208 -0
  314. package/dist/evidence/claim-recovery/validation/occurrence-source.js.map +1 -0
  315. package/dist/evidence/claim-recovery/validation/occurrence-types.d.ts +101 -0
  316. package/dist/evidence/claim-recovery/validation/occurrence-types.js +2 -0
  317. package/dist/evidence/claim-recovery/validation/occurrence-types.js.map +1 -0
  318. package/dist/evidence/claim-recovery/validation/occurrence.d.ts +30 -0
  319. package/dist/evidence/claim-recovery/validation/occurrence.js +227 -0
  320. package/dist/evidence/claim-recovery/validation/occurrence.js.map +1 -0
  321. package/dist/evidence/claim-recovery/validation/primitives.d.ts +11 -0
  322. package/dist/evidence/claim-recovery/validation/primitives.js +41 -0
  323. package/dist/evidence/claim-recovery/validation/primitives.js.map +1 -0
  324. package/dist/evidence/claim-recovery/validation/receipts.d.ts +37 -0
  325. package/dist/evidence/claim-recovery/validation/receipts.js +83 -0
  326. package/dist/evidence/claim-recovery/validation/receipts.js.map +1 -0
  327. package/dist/evidence/claim-recovery/validation/recovery-json.d.ts +8 -0
  328. package/dist/evidence/claim-recovery/validation/recovery-json.js +42 -0
  329. package/dist/evidence/claim-recovery/validation/recovery-json.js.map +1 -0
  330. package/dist/evidence/claim-recovery/validation/semantic-cache.d.ts +3 -0
  331. package/dist/evidence/claim-recovery/validation/semantic-cache.js +22 -0
  332. package/dist/evidence/claim-recovery/validation/semantic-cache.js.map +1 -0
  333. package/dist/evidence/claim-recovery/validation/semantic-validation.d.ts +9 -0
  334. package/dist/evidence/claim-recovery/validation/semantic-validation.js +347 -0
  335. package/dist/evidence/claim-recovery/validation/semantic-validation.js.map +1 -0
  336. package/dist/evidence/claim-recovery/validation/sighting.d.ts +2 -0
  337. package/dist/evidence/claim-recovery/validation/sighting.js +15 -0
  338. package/dist/evidence/claim-recovery/validation/sighting.js.map +1 -0
  339. package/dist/evidence/claim-recovery/validation/sources.d.ts +15 -0
  340. package/dist/evidence/claim-recovery/validation/sources.js +2 -0
  341. package/dist/evidence/claim-recovery/validation/sources.js.map +1 -0
  342. package/dist/evidence/claim-recovery/validation/types.d.ts +126 -0
  343. package/dist/evidence/claim-recovery/validation/types.js +2 -0
  344. package/dist/evidence/claim-recovery/validation/types.js.map +1 -0
  345. package/dist/evidence/claim-split.d.ts +1 -0
  346. package/dist/evidence/claim-split.js +2 -0
  347. package/dist/evidence/claim-split.js.map +1 -0
  348. package/dist/evidence/event-receipts.d.ts +16 -0
  349. package/dist/evidence/event-receipts.js +41 -0
  350. package/dist/evidence/event-receipts.js.map +1 -0
  351. package/dist/evidence/finding-recovery.d.ts +23 -0
  352. package/dist/evidence/finding-recovery.js +74 -0
  353. package/dist/evidence/finding-recovery.js.map +1 -0
  354. package/dist/evidence/finding-retriage.d.ts +14 -0
  355. package/dist/evidence/finding-retriage.js +57 -0
  356. package/dist/evidence/finding-retriage.js.map +1 -0
  357. package/dist/evidence/format.d.ts +18 -0
  358. package/dist/evidence/format.js +152 -0
  359. package/dist/evidence/format.js.map +1 -0
  360. package/dist/evidence/original-run/decode.d.ts +30 -0
  361. package/dist/evidence/original-run/decode.js +208 -0
  362. package/dist/evidence/original-run/decode.js.map +1 -0
  363. package/dist/evidence/original-run/journal.d.ts +31 -0
  364. package/dist/evidence/original-run/journal.js +157 -0
  365. package/dist/evidence/original-run/journal.js.map +1 -0
  366. package/dist/evidence/original-run/lock-path.d.ts +17 -0
  367. package/dist/evidence/original-run/lock-path.js +116 -0
  368. package/dist/evidence/original-run/lock-path.js.map +1 -0
  369. package/dist/evidence/original-run/lock-scope.d.ts +21 -0
  370. package/dist/evidence/original-run/lock-scope.js +61 -0
  371. package/dist/evidence/original-run/lock-scope.js.map +1 -0
  372. package/dist/evidence/original-run/lock.d.ts +33 -0
  373. package/dist/evidence/original-run/lock.js +259 -0
  374. package/dist/evidence/original-run/lock.js.map +1 -0
  375. package/dist/evidence/original-run/remote.d.ts +23 -0
  376. package/dist/evidence/original-run/remote.js +110 -0
  377. package/dist/evidence/original-run/remote.js.map +1 -0
  378. package/dist/evidence/original-run/source.d.ts +61 -0
  379. package/dist/evidence/original-run/source.js +267 -0
  380. package/dist/evidence/original-run/source.js.map +1 -0
  381. package/dist/evidence/reads.d.ts +11 -0
  382. package/dist/evidence/reads.js +16 -0
  383. package/dist/evidence/reads.js.map +1 -0
  384. package/dist/evidence/recover-finding.d.ts +17 -0
  385. package/dist/evidence/recover-finding.js +65 -0
  386. package/dist/evidence/recover-finding.js.map +1 -0
  387. package/dist/evidence/recover-run.d.ts +26 -0
  388. package/dist/evidence/recover-run.js +180 -0
  389. package/dist/evidence/recover-run.js.map +1 -0
  390. package/dist/evidence/retained-time-budget.d.ts +9 -0
  391. package/dist/evidence/retained-time-budget.js +14 -0
  392. package/dist/evidence/retained-time-budget.js.map +1 -0
  393. package/dist/evidence/retriage-finding.d.ts +12 -0
  394. package/dist/evidence/retriage-finding.js +67 -0
  395. package/dist/evidence/retriage-finding.js.map +1 -0
  396. package/dist/evidence/reviewer-lineage.d.ts +35 -0
  397. package/dist/evidence/reviewer-lineage.js +293 -0
  398. package/dist/evidence/reviewer-lineage.js.map +1 -0
  399. package/dist/evidence/reviewer-original.d.ts +67 -0
  400. package/dist/evidence/reviewer-original.js +148 -0
  401. package/dist/evidence/reviewer-original.js.map +1 -0
  402. package/dist/evidence/reviewer-recovery.d.ts +74 -0
  403. package/dist/evidence/reviewer-recovery.js +179 -0
  404. package/dist/evidence/reviewer-recovery.js.map +1 -0
  405. package/dist/evidence/reviewer-status.d.ts +145 -0
  406. package/dist/evidence/reviewer-status.js +302 -0
  407. package/dist/evidence/reviewer-status.js.map +1 -0
  408. package/dist/evidence/show.d.ts +10 -0
  409. package/dist/evidence/show.js +41 -0
  410. package/dist/evidence/show.js.map +1 -0
  411. package/dist/evidence/status.d.ts +38 -0
  412. package/dist/evidence/status.js +73 -0
  413. package/dist/evidence/status.js.map +1 -0
  414. package/dist/evidence/target.d.ts +17 -0
  415. package/dist/evidence/target.js +74 -0
  416. package/dist/evidence/target.js.map +1 -0
  417. package/dist/evidence/types.d.ts +176 -0
  418. package/dist/evidence/types.js +109 -0
  419. package/dist/evidence/types.js.map +1 -0
  420. package/dist/index.d.ts +2 -0
  421. package/dist/index.js +2688 -0
  422. package/dist/index.js.map +1 -0
  423. package/dist/models/seed.d.ts +44 -0
  424. package/dist/models/seed.js +175 -0
  425. package/dist/models/seed.js.map +1 -0
  426. package/dist/models/server-stats.d.ts +80 -0
  427. package/dist/models/server-stats.js +109 -0
  428. package/dist/models/server-stats.js.map +1 -0
  429. package/dist/models/stats-store.d.ts +84 -0
  430. package/dist/models/stats-store.js +572 -0
  431. package/dist/models/stats-store.js.map +1 -0
  432. package/dist/output/artifacts.d.ts +16 -0
  433. package/dist/output/artifacts.js +29 -0
  434. package/dist/output/artifacts.js.map +1 -0
  435. package/dist/output/assembly-refusal.d.ts +8 -0
  436. package/dist/output/assembly-refusal.js +14 -0
  437. package/dist/output/assembly-refusal.js.map +1 -0
  438. package/dist/output/github.d.ts +12 -0
  439. package/dist/output/github.js +214 -0
  440. package/dist/output/github.js.map +1 -0
  441. package/dist/output/json.d.ts +3 -0
  442. package/dist/output/json.js +8 -0
  443. package/dist/output/json.js.map +1 -0
  444. package/dist/output/markdown.d.ts +3 -0
  445. package/dist/output/markdown.js +281 -0
  446. package/dist/output/markdown.js.map +1 -0
  447. package/dist/output/progress.d.ts +55 -0
  448. package/dist/output/progress.js +180 -0
  449. package/dist/output/progress.js.map +1 -0
  450. package/dist/output/sanitize.d.ts +18 -0
  451. package/dist/output/sanitize.js +46 -0
  452. package/dist/output/sanitize.js.map +1 -0
  453. package/dist/output/terminal.d.ts +2 -0
  454. package/dist/output/terminal.js +172 -0
  455. package/dist/output/terminal.js.map +1 -0
  456. package/dist/prepare/chunker.d.ts +18 -0
  457. package/dist/prepare/chunker.js +325 -0
  458. package/dist/prepare/chunker.js.map +1 -0
  459. package/dist/prepare/language.d.ts +1 -0
  460. package/dist/prepare/language.js +99 -0
  461. package/dist/prepare/language.js.map +1 -0
  462. package/dist/prepare/prompt-builder.d.ts +44 -0
  463. package/dist/prepare/prompt-builder.js +102 -0
  464. package/dist/prepare/prompt-builder.js.map +1 -0
  465. package/dist/prepare/unified-diff.d.ts +40 -0
  466. package/dist/prepare/unified-diff.js +135 -0
  467. package/dist/prepare/unified-diff.js.map +1 -0
  468. package/dist/prompts/base.d.ts +3 -0
  469. package/dist/prompts/base.js +53 -0
  470. package/dist/prompts/base.js.map +1 -0
  471. package/dist/prompts/hardening.d.ts +19 -0
  472. package/dist/prompts/hardening.js +67 -0
  473. package/dist/prompts/hardening.js.map +1 -0
  474. package/dist/prompts/languages/elixir.d.ts +1 -0
  475. package/dist/prompts/languages/elixir.js +13 -0
  476. package/dist/prompts/languages/elixir.js.map +1 -0
  477. package/dist/prompts/languages/generic.d.ts +1 -0
  478. package/dist/prompts/languages/generic.js +11 -0
  479. package/dist/prompts/languages/generic.js.map +1 -0
  480. package/dist/prompts/languages/python.d.ts +1 -0
  481. package/dist/prompts/languages/python.js +14 -0
  482. package/dist/prompts/languages/python.js.map +1 -0
  483. package/dist/prompts/languages/typescript.d.ts +1 -0
  484. package/dist/prompts/languages/typescript.js +13 -0
  485. package/dist/prompts/languages/typescript.js.map +1 -0
  486. package/dist/prompts/plan.d.ts +15 -0
  487. package/dist/prompts/plan.js +55 -0
  488. package/dist/prompts/plan.js.map +1 -0
  489. package/dist/report/aggregation-inputs.d.ts +52 -0
  490. package/dist/report/aggregation-inputs.js +114 -0
  491. package/dist/report/aggregation-inputs.js.map +1 -0
  492. package/dist/report/assembly.d.ts +43 -0
  493. package/dist/report/assembly.js +133 -0
  494. package/dist/report/assembly.js.map +1 -0
  495. package/dist/report/blocking-health.d.ts +64 -0
  496. package/dist/report/blocking-health.js +156 -0
  497. package/dist/report/blocking-health.js.map +1 -0
  498. package/dist/report/checkpoint-assembly.d.ts +10 -0
  499. package/dist/report/checkpoint-assembly.js +17 -0
  500. package/dist/report/checkpoint-assembly.js.map +1 -0
  501. package/dist/report/checkpoint-consensus.d.ts +59 -0
  502. package/dist/report/checkpoint-consensus.js +197 -0
  503. package/dist/report/checkpoint-consensus.js.map +1 -0
  504. package/dist/report/checkpoint-gating.d.ts +36 -0
  505. package/dist/report/checkpoint-gating.js +110 -0
  506. package/dist/report/checkpoint-gating.js.map +1 -0
  507. package/dist/report/checkpoint-projection.d.ts +76 -0
  508. package/dist/report/checkpoint-projection.js +122 -0
  509. package/dist/report/checkpoint-projection.js.map +1 -0
  510. package/dist/report/consensus-assembly.d.ts +37 -0
  511. package/dist/report/consensus-assembly.js +33 -0
  512. package/dist/report/consensus-assembly.js.map +1 -0
  513. package/dist/report/recovery-source.d.ts +7 -0
  514. package/dist/report/recovery-source.js +7 -0
  515. package/dist/report/recovery-source.js.map +1 -0
  516. package/dist/report/reviewer-artifact.d.ts +118 -0
  517. package/dist/report/reviewer-artifact.js +381 -0
  518. package/dist/report/reviewer-artifact.js.map +1 -0
  519. package/dist/report/reviewer-evidence-schema.d.ts +42 -0
  520. package/dist/report/reviewer-evidence-schema.js +28 -0
  521. package/dist/report/reviewer-evidence-schema.js.map +1 -0
  522. package/dist/report/reviewer-evidence.d.ts +54 -0
  523. package/dist/report/reviewer-evidence.js +229 -0
  524. package/dist/report/reviewer-evidence.js.map +1 -0
  525. package/dist/report/reviewer-health.d.ts +62 -0
  526. package/dist/report/reviewer-health.js +105 -0
  527. package/dist/report/reviewer-health.js.map +1 -0
  528. package/dist/report/run-header.d.ts +219 -0
  529. package/dist/report/run-header.js +307 -0
  530. package/dist/report/run-header.js.map +1 -0
  531. package/dist/report/supplemental-async.d.ts +19 -0
  532. package/dist/report/supplemental-async.js +85 -0
  533. package/dist/report/supplemental-async.js.map +1 -0
  534. package/dist/report/uuid.d.ts +16 -0
  535. package/dist/report/uuid.js +41 -0
  536. package/dist/report/uuid.js.map +1 -0
  537. package/dist/resolver/git.d.ts +16 -0
  538. package/dist/resolver/git.js +77 -0
  539. package/dist/resolver/git.js.map +1 -0
  540. package/dist/resolver/github-client.d.ts +82 -0
  541. package/dist/resolver/github-client.js +27 -0
  542. package/dist/resolver/github-client.js.map +1 -0
  543. package/dist/resolver/github.d.ts +29 -0
  544. package/dist/resolver/github.js +166 -0
  545. package/dist/resolver/github.js.map +1 -0
  546. package/dist/resolver/local.d.ts +3 -0
  547. package/dist/resolver/local.js +75 -0
  548. package/dist/resolver/local.js.map +1 -0
  549. package/dist/resolver/plan.d.ts +8 -0
  550. package/dist/resolver/plan.js +44 -0
  551. package/dist/resolver/plan.js.map +1 -0
  552. package/dist/resolver/target.d.ts +35 -0
  553. package/dist/resolver/target.js +112 -0
  554. package/dist/resolver/target.js.map +1 -0
  555. package/dist/resolver/types.d.ts +43 -0
  556. package/dist/resolver/types.js +2 -0
  557. package/dist/resolver/types.js.map +1 -0
  558. package/dist/roles/builtin.d.ts +4 -0
  559. package/dist/roles/builtin.js +282 -0
  560. package/dist/roles/builtin.js.map +1 -0
  561. package/dist/roles/dispatcher.d.ts +26 -0
  562. package/dist/roles/dispatcher.js +96 -0
  563. package/dist/roles/dispatcher.js.map +1 -0
  564. package/dist/roles/legacy-4.1.10.d.ts +2 -0
  565. package/dist/roles/legacy-4.1.10.js +320 -0
  566. package/dist/roles/legacy-4.1.10.js.map +1 -0
  567. package/dist/roles/legacy-4.4.9.d.ts +4 -0
  568. package/dist/roles/legacy-4.4.9.js +282 -0
  569. package/dist/roles/legacy-4.4.9.js.map +1 -0
  570. package/dist/roles/loader.d.ts +10 -0
  571. package/dist/roles/loader.js +104 -0
  572. package/dist/roles/loader.js.map +1 -0
  573. package/dist/roles/types.d.ts +13 -0
  574. package/dist/roles/types.js +2 -0
  575. package/dist/roles/types.js.map +1 -0
  576. package/dist/telemetry/abort-signal.d.ts +13 -0
  577. package/dist/telemetry/abort-signal.js +40 -0
  578. package/dist/telemetry/abort-signal.js.map +1 -0
  579. package/dist/telemetry/attest.d.ts +98 -0
  580. package/dist/telemetry/attest.js +331 -0
  581. package/dist/telemetry/attest.js.map +1 -0
  582. package/dist/telemetry/attested-retry.d.ts +79 -0
  583. package/dist/telemetry/attested-retry.js +195 -0
  584. package/dist/telemetry/attested-retry.js.map +1 -0
  585. package/dist/telemetry/attested-reviewer-delivery.d.ts +58 -0
  586. package/dist/telemetry/attested-reviewer-delivery.js +238 -0
  587. package/dist/telemetry/attested-reviewer-delivery.js.map +1 -0
  588. package/dist/telemetry/backfill.d.ts +95 -0
  589. package/dist/telemetry/backfill.js +460 -0
  590. package/dist/telemetry/backfill.js.map +1 -0
  591. package/dist/telemetry/credentials.d.ts +44 -0
  592. package/dist/telemetry/credentials.js +87 -0
  593. package/dist/telemetry/credentials.js.map +1 -0
  594. package/dist/telemetry/deliver.d.ts +152 -0
  595. package/dist/telemetry/deliver.js +662 -0
  596. package/dist/telemetry/deliver.js.map +1 -0
  597. package/dist/telemetry/envelope-validation.d.ts +13 -0
  598. package/dist/telemetry/envelope-validation.js +196 -0
  599. package/dist/telemetry/envelope-validation.js.map +1 -0
  600. package/dist/telemetry/envelope.d.ts +159 -0
  601. package/dist/telemetry/envelope.js +288 -0
  602. package/dist/telemetry/envelope.js.map +1 -0
  603. package/dist/telemetry/events.d.ts +68 -0
  604. package/dist/telemetry/events.js +79 -0
  605. package/dist/telemetry/events.js.map +1 -0
  606. package/dist/telemetry/notice.d.ts +15 -0
  607. package/dist/telemetry/notice.js +67 -0
  608. package/dist/telemetry/notice.js.map +1 -0
  609. package/dist/telemetry/outbox.d.ts +166 -0
  610. package/dist/telemetry/outbox.js +741 -0
  611. package/dist/telemetry/outbox.js.map +1 -0
  612. package/dist/telemetry/quarantine.d.ts +105 -0
  613. package/dist/telemetry/quarantine.js +224 -0
  614. package/dist/telemetry/quarantine.js.map +1 -0
  615. package/dist/telemetry/read-sink.d.ts +31 -0
  616. package/dist/telemetry/read-sink.js +25 -0
  617. package/dist/telemetry/read-sink.js.map +1 -0
  618. package/dist/telemetry/recovery/command.d.ts +16 -0
  619. package/dist/telemetry/recovery/command.js +70 -0
  620. package/dist/telemetry/recovery/command.js.map +1 -0
  621. package/dist/telemetry/recovery/discovery.d.ts +8 -0
  622. package/dist/telemetry/recovery/discovery.js +277 -0
  623. package/dist/telemetry/recovery/discovery.js.map +1 -0
  624. package/dist/telemetry/recovery/files.d.ts +20 -0
  625. package/dist/telemetry/recovery/files.js +123 -0
  626. package/dist/telemetry/recovery/files.js.map +1 -0
  627. package/dist/telemetry/recovery/plan.d.ts +10 -0
  628. package/dist/telemetry/recovery/plan.js +325 -0
  629. package/dist/telemetry/recovery/plan.js.map +1 -0
  630. package/dist/telemetry/recovery/source.d.ts +328 -0
  631. package/dist/telemetry/recovery/source.js +253 -0
  632. package/dist/telemetry/recovery/source.js.map +1 -0
  633. package/dist/telemetry/recovery/types.d.ts +100 -0
  634. package/dist/telemetry/recovery/types.js +2 -0
  635. package/dist/telemetry/recovery/types.js.map +1 -0
  636. package/dist/telemetry/recovery-request-budget.d.ts +39 -0
  637. package/dist/telemetry/recovery-request-budget.js +102 -0
  638. package/dist/telemetry/recovery-request-budget.js.map +1 -0
  639. package/dist/telemetry/report-consistency.d.ts +11 -0
  640. package/dist/telemetry/report-consistency.js +111 -0
  641. package/dist/telemetry/report-consistency.js.map +1 -0
  642. package/dist/telemetry/reviewer-delivery.d.ts +39 -0
  643. package/dist/telemetry/reviewer-delivery.js +279 -0
  644. package/dist/telemetry/reviewer-delivery.js.map +1 -0
  645. package/dist/telemetry/reviewer-preflight.d.ts +13 -0
  646. package/dist/telemetry/reviewer-preflight.js +56 -0
  647. package/dist/telemetry/reviewer-preflight.js.map +1 -0
  648. package/dist/telemetry/scrub.d.ts +53 -0
  649. package/dist/telemetry/scrub.js +205 -0
  650. package/dist/telemetry/scrub.js.map +1 -0
  651. package/dist/telemetry/sink.d.ts +212 -0
  652. package/dist/telemetry/sink.js +813 -0
  653. package/dist/telemetry/sink.js.map +1 -0
  654. package/dist/telemetry/terminal-reviewer-delivery.d.ts +18 -0
  655. package/dist/telemetry/terminal-reviewer-delivery.js +27 -0
  656. package/dist/telemetry/terminal-reviewer-delivery.js.map +1 -0
  657. package/dist/telemetry/transient-input.d.ts +2 -0
  658. package/dist/telemetry/transient-input.js +37 -0
  659. package/dist/telemetry/transient-input.js.map +1 -0
  660. package/dist/telemetry/verification.d.ts +9 -0
  661. package/dist/telemetry/verification.js +12 -0
  662. package/dist/telemetry/verification.js.map +1 -0
  663. package/package.json +73 -0
package/dist/index.js ADDED
@@ -0,0 +1,2688 @@
1
+ #!/usr/bin/env node
2
+ import { previewTerminalRejection, applyTerminalRejection } from './converge/terminal-rejection.js';
3
+ import { guardedDeliveryState } from './telemetry/deliver.js';
4
+ import { AmbiguousReviewerIdentityError, assertUnambiguousReviewerIdentities } from './dispatch/reviewer-identity.js';
5
+ import { retainAssemblyRefusal } from './output/assembly-refusal.js';
6
+ import { createReviewCycleRemote } from './converge/cycle-remote.js';
7
+ import { assertNoPendingFreshReview, freshReviewOutputPaths } from './converge/fresh-review.js';
8
+ import { previewStaleReport, applyStaleReport } from './converge/stale-report.js';
9
+ import { Command, InvalidArgumentError } from 'commander';
10
+ import ora from 'ora';
11
+ import chalk from 'chalk';
12
+ import { readdir, readFile } from 'fs/promises';
13
+ import { hostname } from 'os';
14
+ import { fileURLToPath } from 'url';
15
+ import { dirname, join, resolve } from 'path';
16
+ import { loadConfig } from './config/loader.js';
17
+ import { applyHarnessModelKeys } from './config/harness.js';
18
+ import { DEFAULT_MODELS, DEFAULT_TIMEOUT_MS, DEFAULT_ASYNC_TIMEOUT_MS, DEFAULT_QUORUM_FRACTION, DEFAULT_MAX_RETRIES, DEFAULT_CONCURRENCY, DEFAULT_REASONING_EFFORT, DEFAULT_THRESHOLDS, } from './config/defaults.js';
19
+ import { resolveProviderConcurrency } from './config/provider-concurrency.js';
20
+ import { parseGitHubTarget, fetchPRDiff, isGitHubTarget } from './resolver/github.js';
21
+ import { loadLocalDiff } from './resolver/local.js';
22
+ import { loadGitDiff, resolveGitHeads } from './resolver/git.js';
23
+ import { loadPlanAsDiff } from './resolver/plan.js';
24
+ import { isPlanFocus, PLAN_FOCUS_MODES } from './prompts/plan.js';
25
+ import { chunkDiff } from './prepare/chunker.js';
26
+ import { buildPrompt, loadContextDocs as loadPromptContextDocs } from './prepare/prompt-builder.js';
27
+ import { BUILTIN_ROLES, getRoleByName } from './roles/builtin.js';
28
+ import { resolveRoles, findProjectRulesFile } from './roles/loader.js';
29
+ import { historicalExecutionPlan, legacyPendingClaimRoles } from './converge/legacy-roster.js';
30
+ import { decodeOriginalReport } from './evidence/original-run/decode.js';
31
+ import { originalRunReportSchema } from './telemetry/recovery/source.js';
32
+ import { buildAssignments, detectProvider } from './roles/dispatcher.js';
33
+ import { defaultAdapterFactory, runReviews } from './dispatch/runner.js';
34
+ import { mergeChunkReviews } from './dispatch/merge.js';
35
+ import { partitionAsyncAssignments, asyncTargetKey, resolveAsyncStoreDir, resolveExistingAsyncStoreDir, spoolAsyncCalls, launchAsyncWorkers, runAsyncWorker, collectAsyncResults, currentBranchLabel, MAX_ASYNC_CALLS_PER_ROUND, snapshotAsyncResults, consumeBoundAsyncResults, } from './dispatch/async-lane.js';
36
+ import { evaluateCiGate } from './ci.js';
37
+ import { resolveQuorumPolicy } from './dispatch/quorum.js';
38
+ import { assertAdmissibleReportHealth, deriveBlockingHealth, describeBlockingHealth, ReportHealthError } from './report/blocking-health.js';
39
+ import { resolveGatingConfig } from './consensus/gating.js';
40
+ import { printReviewSummary } from './output/terminal.js';
41
+ import { postGitHubReview } from './output/github.js';
42
+ import { renderReportArtifacts, writeReportArtifacts } from './output/artifacts.js';
43
+ import { assembleCompletedReview } from './report/assembly.js';
44
+ import { assertReviewWorkWithinLimit, buildCouncilRunPlan, CouncilProgressReporter, formatCouncilRunPlan, } from './output/progress.js';
45
+ import { resolveFinding, buildDiscussPrompts, runDiscussion, loadContextDocs, } from './discuss.js';
46
+ import { DEFAULT_CONVERGE_ATTEMPT_CAP, claimConvergeAttempt, ConvergeAttemptBudgetExceededError, convergeAttemptErrorExitCode, ConvergeAttemptStateError, resolveGitCommonDir, } from './converge/attempt-budget.js';
47
+ import { DEFAULT_CONVERGE_ROUND_CAP, HARD_CONVERGE_ROUND_CAP, processRoundReport, recordVerdicts, findingGatingReason, ConvergeRoundCapError, ConvergeRunStateError, } from './converge/run-state.js';
48
+ import { applyRoundGap, previewRoundGap } from './converge/round-gap.js';
49
+ import { guardReviewLaunch, ReviewLaunchRefused } from './converge/launch-guard.js';
50
+ import { createBoundFixRecovery } from './converge/bound-fix-recovery.js';
51
+ import { openReadSink } from './telemetry/read-sink.js';
52
+ import { reconcileFlushedRun } from './converge/delivery-reconciliation.js';
53
+ import { validateLaunchOutputs, validateLaunchProviders } from './converge/launch-preflight.js';
54
+ import { writeExclusive, serializeRecoveryDocument } from './evidence/original-run/journal.js';
55
+ import { readStable, sha256 } from './telemetry/recovery/files.js';
56
+ import { appendCalls, appendOutcomes, loadModelStats, resolveDataDir, DEFAULT_WINDOW_DAYS, MIN_OUTCOMES_FOR_WEIGHT, } from './models/stats-store.js';
57
+ import { buildSeedRecords } from './models/seed.js';
58
+ import { assignmentLane, buildRoster, configDigest, diffDigest, describeRunTarget, detectRunner, parseSpecSource, resolveConvergeContext, guardedInputSha256, sha256Hex, stableStringify, validateSha, } from './report/run-header.js';
59
+ import { assertExpectedHead, resolveReviewTarget } from './resolver/target.js';
60
+ import { createTelemetryRuntime, deliverRun, evidenceRequirementConflict, emitConvergeEvents, flushOutbox, flushOutboxAtStart, loadHarnessSettings, resolveTelemetryLevel, } from './telemetry/deliver.js';
61
+ import { normalizeGeneratedReport, sanitizeForDelivery } from './telemetry/envelope.js';
62
+ import { Quarantine, QUARANTINE_DIR } from './telemetry/quarantine.js';
63
+ import { scrubText } from './telemetry/scrub.js';
64
+ import { validateEnvelopeTimeoutMs } from './telemetry/sink.js';
65
+ import { buildEvent, roundIdentities } from './telemetry/events.js';
66
+ import { credentialHost } from './telemetry/credentials.js';
67
+ import { attestRun, renewAttestation } from './telemetry/attest.js';
68
+ import { uuidv7 } from './report/uuid.js';
69
+ import { runEvidenceStatus } from './evidence/status.js';
70
+ import { runEvidenceShow } from './evidence/show.js';
71
+ import { runFindingRecovery } from './evidence/recover-finding.js';
72
+ import { runOriginalRecovery } from './evidence/recover-run.js';
73
+ import { runFindingRetriage } from './evidence/retriage-finding.js';
74
+ import { fetchServerModelStats, loadMergedWeights, mergeWeights } from './models/server-stats.js';
75
+ import { runBackfill } from './telemetry/backfill.js';
76
+ import { runRefutationRecovery } from './telemetry/recovery/command.js';
77
+ import { parseRepoName } from './evidence/target.js';
78
+ import { text } from './evidence/format.js';
79
+ import { loadConvergeRunState } from './converge/run-state.js';
80
+ import { finalizeOrdinaryPendingLaunch, previewOrdinaryPendingLaunch, resumePendingLegacyLaunch } from './converge/pending-legacy-resume.js';
81
+ import { exportOrdinaryPendingPackage, retainOrdinaryLaunchInputs } from './converge/ordinary-pending-export.js';
82
+ import { capturePreparedCouncil } from './dispatch/capture-council.js';
83
+ import { executeCapturedOriginal } from './dispatch/original-execution.js';
84
+ import { resolveFinalizedAsyncExecution } from './dispatch/checkpoint-async-store.js';
85
+ import { exportCheckpointProof } from './dispatch/checkpoint.js';
86
+ import { captureAggregationInputs, AGGREGATION_ALGORITHM } from './report/aggregation-inputs.js';
87
+ import { decodeSupplementalAsync } from './report/supplemental-async.js';
88
+ import { projectCheckpointReport } from './report/checkpoint-projection.js';
89
+ import { executeCheckpointGating } from './dispatch/checkpoint-gating-execution.js';
90
+ import { assembleCheckpointReview } from './report/checkpoint-assembly.js';
91
+ import { describeReviewerEvidence } from './report/reviewer-evidence.js';
92
+ import { serializeReviewerArtifact } from './report/reviewer-artifact.js';
93
+ import { deliverTerminalReviewerRun } from './telemetry/terminal-reviewer-delivery.js';
94
+ const RCL_VERSION = JSON.parse(await readFile(join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json'), 'utf-8')).version;
95
+ const program = new Command();
96
+ program
97
+ .name('rcl')
98
+ .description('Review Council — multi-model AI code review')
99
+ .version(RCL_VERSION);
100
+ // Commands that perform work first deliver what an earlier one could not, bounded to
101
+ // five seconds so an offline machine never stalls (IO-12475 section 8.4).
102
+ // The telemetry commands manage the outbox themselves; the detached async
103
+ // worker is not a user command.
104
+ program.hook('preAction', async (_thisCommand, actionCommand) => {
105
+ const name = actionCommand.name();
106
+ if (name === 'review')
107
+ return; // Review flushes after detecting explicit/pending cycles.
108
+ // Reads and explicit repairs must not flush unrelated evidence, even in preview.
109
+ if (actionCommand.parent?.name() === 'evidence' && (name === 'show' || name === 'status'))
110
+ return;
111
+ if (name === 'converge-rejected' || name === 'converge-stale' || name === 'converge-gap' || name === 'recover-run' || name === 'recover-finding' || name === 'retriage-finding' || name === 'telemetry' || actionCommand.parent?.name() === 'telemetry' || name.includes('worker'))
112
+ return;
113
+ const flags = actionCommand.opts();
114
+ if (flags.telemetry === false || (process.env['RCL_TELEMETRY'] ?? '').trim().toLowerCase() === 'off')
115
+ return;
116
+ try {
117
+ // One cheap readdir before any credential or config work: most commands
118
+ // find an empty outbox and pay nothing.
119
+ const entries = await readdir(join(resolveDataDir(), 'outbox')).catch(() => []);
120
+ if (entries.length === 0)
121
+ return;
122
+ await flushOutboxAtStart(await createTelemetryRuntime({ rclVersion: RCL_VERSION }));
123
+ }
124
+ catch {
125
+ // Never let the outbox stop the command the user asked for.
126
+ }
127
+ });
128
+ const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
129
+ /**
130
+ * `--evidence-required` with telemetry switched off cannot be honored; say so
131
+ * before spending the council. The flag and the environment are known up
132
+ * front; the project config's `harness.telemetry: off` is checked again once
133
+ * the config is loaded, still before any reviewer call.
134
+ */
135
+ function assertEvidenceCanBeRequired(opts, config) {
136
+ const conflict = evidenceRequirementConflict(opts, config, process.env);
137
+ if (conflict !== undefined)
138
+ throw new Error(conflict);
139
+ }
140
+ /**
141
+ * With `--evidence-required`, resolve where the evidence would go before any
142
+ * reviewer is paid: an unmanaged repository or a missing credential makes
143
+ * delivery impossible, and that is worth knowing at once.
144
+ */
145
+ async function assertEvidenceDeliverable(opts, config, credential) {
146
+ if (!opts.evidenceRequired)
147
+ return;
148
+ const runtime = await createTelemetryRuntime({
149
+ rclVersion: RCL_VERSION,
150
+ config,
151
+ noTelemetry: opts.telemetry === false,
152
+ ...(credential ? { credential } : {}),
153
+ });
154
+ if (!runtime.repoManaged) {
155
+ throw new Error('--evidence-required needs a Harness-managed repository (one carrying .harness-cli/config.json); this one is not.');
156
+ }
157
+ if (runtime.level === 'off')
158
+ throw new Error('--evidence-required contradicts the resolved telemetry level off.');
159
+ if (!runtime.sink)
160
+ throw new Error(`--evidence-required needs a Harness credential: ${runtime.note ?? 'none available'}.`);
161
+ }
162
+ /** `--attest` records the full report or nothing: the resolved telemetry level must be `full`. */
163
+ function attestLevelMessage(level) {
164
+ return `--attest needs the telemetry level full (resolved: ${level}): an attested run carries its full report as evidence. Check RCL_TELEMETRY and harness.telemetry in the project config.`;
165
+ }
166
+ /**
167
+ * `--attest` (RCL-40), before any key, config or reviewer work: the target
168
+ * must be a pull request, the telemetry level must resolve to `full` from the
169
+ * environment and the project config (the file `--config` names included),
170
+ * and only then is the job's OIDC token requested and exchanged. An attested
171
+ * review is recorded or it does not run; the caller treats evidence as
172
+ * required from here on.
173
+ */
174
+ async function attestBeforeReview(spinner, opts, target, gitMode) {
175
+ if (gitMode || target === undefined || !isGitHubTarget(target)) {
176
+ throw new Error('--attest applies to a pull request target (owner/repo#N or a GitHub PR URL): Harness attests a run of the pull request it re-reads, not a local diff or a patch file.');
177
+ }
178
+ if (opts.telemetry === false) {
179
+ throw new Error('--attest contradicts --no-telemetry: an attested review is recorded or it does not run.');
180
+ }
181
+ const level = resolveTelemetryLevel(await loadHarnessSettings(process.cwd(), opts.config), { noTelemetry: false }, process.env);
182
+ if (level !== 'full')
183
+ throw new Error(attestLevelMessage(level));
184
+ spinner.text = 'Attesting to Harness as this GitHub Actions run...';
185
+ const attestation = await attestRun({ runId: uuidv7(), rclVersion: RCL_VERSION });
186
+ spinner.info(`Attested: run-bound credential from ${credentialHost(attestation.credential)} for run ${attestation.runId} (expires ${attestation.expiresAt})`);
187
+ spinner.start('Loading configuration...');
188
+ return attestation;
189
+ }
190
+ /** Converge commands report their events fail-soft; nothing they do depends on it. */
191
+ async function reportConvergeEvents(events) {
192
+ try {
193
+ await emitConvergeEvents(await createTelemetryRuntime({ rclVersion: RCL_VERSION }), events);
194
+ }
195
+ catch {
196
+ // Evidence of the loop is advisory next to the loop's own durable state.
197
+ }
198
+ }
199
+ // review command
200
+ program
201
+ .command('review [target]')
202
+ .description('Review a PR, local diff, or uncommitted work. Target: owner/repo#N, GitHub PR URL, or path to .patch file; or use --staged/--working-tree')
203
+ .option('--staged', 'Review staged changes (git diff --cached) instead of a target')
204
+ .option('--working-tree', 'Review all uncommitted changes (git diff HEAD) instead of a target')
205
+ .option('--role <name>', 'Use a single named role')
206
+ .option('--roles <names>', 'Comma-separated list of roles')
207
+ .option('--reviewer <pair>', 'Explicit model:role pair (repeatable)', (val, prev) => {
208
+ prev.push(val);
209
+ return prev;
210
+ }, [])
211
+ .option('--context <path>', 'Context file or directory to include (repeatable)', (val, prev) => {
212
+ prev.push(val);
213
+ return prev;
214
+ }, [])
215
+ .option('--spec <path>', 'Specification file for spec-compliance role')
216
+ .option('--models <models>', 'Comma-separated list of primary (SOTA) models')
217
+ .option('--secondary-models <models>', 'Comma-separated list of secondary models (specialized roles only)')
218
+ .option('--async-models <models>', 'Comma-separated list of async (non-blocking) bonus reviewers')
219
+ .option('--focus <areas>', 'Comma-separated focus areas')
220
+ .option('--post', 'Post review as GitHub PR comment')
221
+ .option('--json', 'Output JSON to stdout')
222
+ .option('--json-file <path>', 'Write JSON output to file')
223
+ .option('--markdown <path>', 'Write Markdown report to file')
224
+ .option('--ci', 'CI mode: exit non-zero if critical/important findings')
225
+ .option('--head-sha <sha>', 'Exact head commit a patch file was taken from (patch files only)')
226
+ .option('--base-sha <sha>', 'Exact base commit a patch file was taken from (patch files only)')
227
+ .option('--expect-head-sha <sha>', 'Fail fast unless the resolved head commit equals this SHA')
228
+ .option('--spec-source <source>', 'Where --spec came from: flag | repo_file | harness_issue:<ID>')
229
+ .option('--converge-target <key>', 'Converge target this round belongs to (or RCL_CONVERGE_TARGET)')
230
+ .option('--for-pr <owner/repo#N>', 'The pull request a patch-file review is evidence for (or RCL_FOR_PR): Harness verifies its head against that pull request')
231
+ .option('--round <n>', 'Converge round number (or RCL_CONVERGE_ROUND)')
232
+ .option('--attempt <n>', 'Converge attempt number (or RCL_CONVERGE_ATTEMPT)')
233
+ .option('--start-over', 'Start an explicitly requested fresh review with a new normal budget; retain all prior evidence and spending')
234
+ .option('--guarded-converge', 'Validate and claim inside this review process; derive the round from native state')
235
+ .option('--bound-fix-recovery <run-id>', 'Review unchanged inputs once live Harness evidence proves a retained bound fix obligation after this native dismissal-only run')
236
+ .option('--launch-intent <intent>', 'Guarded intent: review, stop-upstream, stop-review, or retry-delivery')
237
+ .option('--retry-report <path>', 'Original legacy report proving an inconclusive launch; the new inputs may differ; requires --retry-reason')
238
+ .option('--resume-pending', 'Finalize an unobservable pending legacy dispatch, then claim one fresh checkpointed retry under its target lock')
239
+ .option('--finalize-pending-only', 'Finalize one authenticated ordinary pending launch without claiming or dispatching its successor')
240
+ .option('--pending-native-sha256 <digest>', 'Finalize-only: exact native state digest returned by --preview-pending')
241
+ .option('--pending-attempt-sha256 <digest>', 'Finalize-only: exact attempt state digest returned by --preview-pending')
242
+ .option('--export-pending-package <path>', 'Export authenticated ordinary pending inputs to an exclusive private file without provider calls or native writes')
243
+ .option('--expect-base-sha <sha>', 'Pending package export: require the resolved current base to equal this SHA')
244
+ .option('--ordinary-pending-package <path>', 'Immutable ordinary pending-launch package; requires --resume-pending or --finalize-pending-only and replaces --retry-report')
245
+ .option('--preview-pending', 'Authenticate pending recovery or preview a pending package export without provider calls')
246
+ .option('--resume-async-sha256 <hashes>', 'Comma-separated SHA-256 bindings for every legacy async result retained as historical evidence')
247
+ .option('--retry-reason <reason>', 'Explicit bounded recovery decision for a previous failed or unknown launch; never resets caps')
248
+ .option('--max-attempts <n>', 'Guarded convergence: explicitly authorized attempt cap (omitting preserves the cap)')
249
+ .option('--max-rounds <n>', 'Guarded convergence: explicitly authorized round cap (2–99; omitting preserves the cap)')
250
+ .option('--no-telemetry', 'Do not deliver this review as evidence to Harness')
251
+ .option('--evidence-required', 'Exit 4 unless Harness acknowledged the evidence (spools first; retry with rcl telemetry flush)')
252
+ .option('--config <path>', 'Path to config file')
253
+ .option('--attest', 'GitHub Actions gate workflow only: exchange the job OIDC token for a run-bound Harness credential and record this review as attested (needs id-token: write and HARNESS_API_URL; implies --evidence-required; no fallback to another credential)')
254
+ .action(async (target, opts) => {
255
+ await runReview(target, opts);
256
+ });
257
+ // review-plan command
258
+ program
259
+ .command('review-plan <file>')
260
+ .description('Council-review an implementation plan document before code exists')
261
+ .option('--focus <mode>', `Focus the review: ${PLAN_FOCUS_MODES.join(' | ')} (default: comprehensive)`)
262
+ .option('--role <name>', 'Use a single named role')
263
+ .option('--roles <names>', 'Comma-separated list of roles')
264
+ .option('--reviewer <pair>', 'Explicit model:role pair (repeatable)', (val, prev) => {
265
+ prev.push(val);
266
+ return prev;
267
+ }, [])
268
+ .option('--context <path>', 'Context file or directory to include (repeatable)', (val, prev) => {
269
+ prev.push(val);
270
+ return prev;
271
+ }, [])
272
+ .option('--spec <path>', 'Specification the plan should satisfy (enables spec-compliance role)')
273
+ .option('--models <models>', 'Comma-separated list of primary (SOTA) models')
274
+ .option('--secondary-models <models>', 'Comma-separated list of secondary models (specialized roles only)')
275
+ .option('--async-models <models>', 'Comma-separated list of async (non-blocking) bonus reviewers')
276
+ .option('--json', 'Output JSON to stdout')
277
+ .option('--json-file <path>', 'Write JSON output to file')
278
+ .option('--markdown <path>', 'Write Markdown report to file')
279
+ .option('--spec-source <source>', 'Where --spec came from: flag | repo_file | harness_issue:<ID>')
280
+ .option('--converge-target <key>', 'Converge target this round belongs to (or RCL_CONVERGE_TARGET)')
281
+ .option('--for-pr <owner/repo#N>', 'The pull request a patch-file review is evidence for (or RCL_FOR_PR): Harness verifies its head against that pull request')
282
+ .option('--round <n>', 'Converge round number (or RCL_CONVERGE_ROUND)')
283
+ .option('--attempt <n>', 'Converge attempt number (or RCL_CONVERGE_ATTEMPT)')
284
+ .option('--no-telemetry', 'Do not deliver this review as evidence to Harness')
285
+ .option('--evidence-required', 'Exit 4 unless Harness acknowledged the evidence (spools first; retry with rcl telemetry flush)')
286
+ .option('--config <path>', 'Path to config file')
287
+ .action(async (file, opts) => {
288
+ await runPlanReview(file, opts);
289
+ });
290
+ // discuss command
291
+ program
292
+ .command('discuss <question>')
293
+ .description('Ask the models that flagged a finding a follow-up question (one round, from a saved report)')
294
+ .requiredOption('--report <path>', 'Report JSON from a previous review (--json-file)')
295
+ .requiredOption('--finding <id>', 'Finding id from the report; use <id>:<n> if the id is ambiguous')
296
+ .option('--models <models>', 'Override which models answer (comma-separated)')
297
+ .option('--context <path>', 'Code or doc file to attach as context (repeatable)', (val, prev) => {
298
+ prev.push(val);
299
+ return prev;
300
+ }, [])
301
+ .option('--json', 'Output JSON to stdout')
302
+ .option('--config <path>', 'Path to config file')
303
+ .action(async (question, opts) => {
304
+ await runDiscuss(question, opts);
305
+ });
306
+ // Machine-enforced cost/safety guard used by the rcl-converge workflow.
307
+ program
308
+ .command('converge-attempt')
309
+ .description('Atomically consume one persisted rcl-converge attempt before starting a review')
310
+ // Optional-value syntax is deliberate: Commander otherwise exits before
311
+ // the action, preventing --json callers from receiving structured errors
312
+ // for a missing value. The action enforces both values as required.
313
+ .option('--target [key]', 'Required stable repository-and-PR/branch convergence target key')
314
+ .option('--max-attempts [n]', `Explicit per-target cap override (default ${DEFAULT_CONVERGE_ATTEMPT_CAP} for a new target)`)
315
+ .option('--json', 'Output the claim as JSON')
316
+ .action(async (opts) => {
317
+ try {
318
+ if (typeof opts.target !== 'string' || opts.target.trim() === '') {
319
+ throw new ConvergeAttemptStateError('--target is required.');
320
+ }
321
+ let maxAttempts;
322
+ if (opts.maxAttempts !== undefined) {
323
+ maxAttempts = typeof opts.maxAttempts === 'string' ? Number(opts.maxAttempts) : NaN;
324
+ if (!Number.isSafeInteger(maxAttempts) || maxAttempts < 1) {
325
+ throw new ConvergeAttemptStateError('maxAttempts (--max-attempts) must be a positive safe integer.');
326
+ }
327
+ }
328
+ const claim = await claimConvergeAttempt({
329
+ gitCommonDir: await resolveGitCommonDir(),
330
+ target: opts.target,
331
+ maxAttempts,
332
+ });
333
+ await reportConvergeEvents([
334
+ buildEvent({
335
+ kind: 'attempt_claimed',
336
+ convergeTarget: claim.target,
337
+ attempt: claim.attempt,
338
+ // The claim's local state path and process id stay on this machine.
339
+ payload: { attempt: claim.attempt, cap: claim.cap, ...(claim.cycle ? { cycle_id: claim.cycle.id } : {}) },
340
+ }),
341
+ // An explicit --max-attempts is consent evidence, whatever it was before.
342
+ ...(maxAttempts !== undefined
343
+ ? [buildEvent({ kind: 'cap_changed', convergeTarget: claim.target, attempt: claim.attempt, payload: { kind: 'attempts', to: claim.cap } })]
344
+ : []),
345
+ ]);
346
+ if (opts.json) {
347
+ console.log(JSON.stringify(claim));
348
+ }
349
+ else {
350
+ console.log(`Convergence attempt ${claim.attempt}/${claim.cap} claimed for ${claim.target}. ` +
351
+ `State: ${claim.stateFile}`);
352
+ if (claim.warning)
353
+ console.error(chalk.yellow(claim.warning));
354
+ }
355
+ }
356
+ catch (err) {
357
+ const message = err instanceof Error ? err.message : String(err);
358
+ const code = err instanceof ConvergeAttemptBudgetExceededError
359
+ ? err.code
360
+ : err instanceof ConvergeAttemptStateError
361
+ ? err.code
362
+ : 'RCL_CONVERGE_ATTEMPT_ERROR';
363
+ if (opts.json) {
364
+ console.error(JSON.stringify({
365
+ error: {
366
+ code,
367
+ message,
368
+ ...(err instanceof ConvergeAttemptBudgetExceededError
369
+ ? { attemptsUsed: err.attemptsUsed, cap: err.cap, target: err.target }
370
+ : {}),
371
+ },
372
+ }));
373
+ }
374
+ else {
375
+ console.error(chalk.red(message));
376
+ }
377
+ // Exit 2 is the expected consent boundary. Exit 3 means accounting or
378
+ // infrastructure failed and raising the cap is not the remediation.
379
+ process.exitCode = convergeAttemptErrorExitCode(err);
380
+ }
381
+ });
382
+ program
383
+ .command('converge-rejected')
384
+ .description('Preview or apply an evidenced terminal local rejection; preserves the original report and spent attempts')
385
+ .option('--preview').option('--apply')
386
+ .requiredOption('--manifest <path>', 'Exclusive preview output or exact reviewed manifest')
387
+ .option('--manifest-sha256 <sha256>')
388
+ .option('--target <target>').option('--run <uuid>')
389
+ .option('--report <path>').option('--report-sha256 <sha256>')
390
+ .option('--reason <text>').option('--json')
391
+ .action(async (opts) => {
392
+ try {
393
+ if ([opts.preview, opts.apply].filter(Boolean).length !== 1)
394
+ throw new Error('choose_exactly_one_rejection_mode');
395
+ const common = await resolveGitCommonDir(), dataDir = resolveDataDir();
396
+ let result;
397
+ if (opts.preview) {
398
+ if (opts.manifestSha256 !== undefined)
399
+ throw new Error('preview_does_not_accept_manifest_digest');
400
+ const manifest = await previewTerminalRejection({ target: opts.target, runId: opts.run,
401
+ reportPath: opts.report, reportSha256: opts.reportSha256, reason: opts.reason }, common, dataDir);
402
+ await writeExclusive(opts.manifest, manifest, 16384);
403
+ result = { mode: 'preview', manifest, manifestSha256: sha256(serializeRecoveryDocument(manifest)) };
404
+ }
405
+ else {
406
+ if ([opts.target, opts.run, opts.report, opts.reportSha256, opts.reason].some(v => v !== undefined))
407
+ throw new Error('apply_uses_only_pinned_manifest');
408
+ result = { mode: 'apply', result: await applyTerminalRejection({ manifest: opts.manifest,
409
+ manifestSha256: opts.manifestSha256 }, common, dataDir) };
410
+ }
411
+ console.log(JSON.stringify({ ...result, accounting: 'unchanged',
412
+ scope: 'local terminal disposition only; no admission, upload, provider calls or approval' }));
413
+ }
414
+ catch (error) {
415
+ console.error(JSON.stringify({ error: { code: 'RCL_CONVERGE_REJECTED', message: error instanceof Error ? error.message : String(error) } }));
416
+ process.exitCode = 3;
417
+ }
418
+ });
419
+ // Cross-round finding identity + machine-enforced round cap (RCL-24).
420
+ program
421
+ .command('converge-stale')
422
+ .description('Preview/apply/resume an audited stale unadmitted report disposition; preserves reports, attempts and findings')
423
+ .option('--preview').option('--apply').option('--resume')
424
+ .requiredOption('--manifest <path>', 'Exclusive preview output; reviewed manifest for apply/resume')
425
+ .option('--manifest-sha256 <sha256>', 'Exact reviewed manifest digest for apply/resume')
426
+ .option('--target <target>')
427
+ .option('--head <sha>', 'Current exact head from the guarded launch refusal')
428
+ .option('--input-sha256 <sha256>', 'Current effective input digest from the guarded launch refusal; rechecked on continuation')
429
+ .option('--report <path>', 'Original completed, healthy, delivered report JSON')
430
+ .option('--report-sha256 <sha256>', 'Original immutable report digest')
431
+ .option('--reason <text>', 'Source-backed reason this report became materially stale, at most 500 characters')
432
+ .option('--json')
433
+ .action(async (opts) => {
434
+ try {
435
+ if ([opts.preview, opts.apply, opts.resume].filter(Boolean).length !== 1)
436
+ throw new Error('choose_exactly_one_stale_report_mode');
437
+ const common = await resolveGitCommonDir();
438
+ let result;
439
+ if (opts.preview) {
440
+ if (opts.manifestSha256 !== undefined)
441
+ throw new Error('preview_does_not_accept_manifest_digest');
442
+ if (![opts.target, opts.head, opts.inputSha256, opts.report, opts.reportSha256, opts.reason].every(v => typeof v === 'string'))
443
+ throw new Error('stale_report_preview_arguments_required');
444
+ const manifest = await previewStaleReport({ target: opts.target, headSha: opts.head,
445
+ inputSha256: opts.inputSha256, reportPath: opts.report, reportSha256: opts.reportSha256, reason: opts.reason }, common);
446
+ await writeExclusive(opts.manifest, manifest, 16384);
447
+ result = { mode: 'preview', manifest, manifestSha256: sha256(serializeRecoveryDocument(manifest)) };
448
+ }
449
+ else {
450
+ if ([opts.target, opts.head, opts.inputSha256, opts.report, opts.reportSha256, opts.reason].some(v => v !== undefined))
451
+ throw new Error('apply_uses_only_pinned_manifest');
452
+ result = { mode: opts.apply ? 'apply' : 'resume', result: await applyStaleReport({ manifest: opts.manifest,
453
+ manifestSha256: opts.manifestSha256, mode: opts.apply ? 'apply' : 'resume' }, common) };
454
+ }
455
+ console.log(JSON.stringify({ ...result, accounting: 'unchanged', scope: 'local stale disposition only; no admission, provider calls or approval' }));
456
+ }
457
+ catch (error) {
458
+ console.error(JSON.stringify({ error: { code: 'RCL_CONVERGE_STALE', message: error instanceof Error ? error.message : String(error) } }));
459
+ process.exitCode = 3;
460
+ }
461
+ });
462
+ program
463
+ .command('converge-gap')
464
+ .description('Preview, apply or resume one evidenced missing-terminal-report audit gap; never creates a round, finding, verdict or attempt')
465
+ .option('--preview')
466
+ .option('--apply')
467
+ .option('--resume')
468
+ .requiredOption('--manifest <path>', 'Exclusive preview manifest; existing reviewed manifest for apply/resume')
469
+ .option('--target <target>')
470
+ .option('--gap-round <number>')
471
+ .option('--admitting-round <number>')
472
+ .option('--attempt <number>')
473
+ .option('--run <uuid>')
474
+ .option('--report <path>', 'Exact original report JSON')
475
+ .option('--report-sha256 <sha256>')
476
+ .option('--incomplete <path>', 'Available incomplete-run evidence; does not prove global absence')
477
+ .option('--incomplete-sha256 <sha256>')
478
+ .option('--evidence <path>', 'Optional JSON array of additional {path, sha256} selections')
479
+ .option('--manifest-sha256 <sha256>', 'Exact reviewed manifest bytes; required for apply/resume')
480
+ .option('--json')
481
+ .action(async (opts) => {
482
+ try {
483
+ if ([opts.preview, opts.apply, opts.resume].filter(Boolean).length !== 1)
484
+ throw new Error('choose_exactly_one_round_gap_mode');
485
+ const gitCommonDir = await resolveGitCommonDir();
486
+ let result;
487
+ if (opts.preview) {
488
+ if (opts.manifestSha256 !== undefined)
489
+ throw new Error('preview_does_not_accept_manifest_digest');
490
+ if (![opts.target, opts.gapRound, opts.admittingRound, opts.attempt, opts.run, opts.report, opts.reportSha256, opts.incomplete, opts.incompleteSha256].every(value => typeof value === 'string'))
491
+ throw new Error('round_gap_preview_arguments_required');
492
+ const evidence = typeof opts.evidence === 'string' ? JSON.parse((await readStable(opts.evidence, 1024 * 1024)).text) : undefined;
493
+ const manifest = await previewRoundGap({ target: opts.target, gapRound: Number(opts.gapRound), admittingRound: Number(opts.admittingRound), attempt: Number(opts.attempt), runId: opts.run,
494
+ reportPath: opts.report, reportSha256: opts.reportSha256,
495
+ incompletePath: opts.incomplete, incompleteSha256: opts.incompleteSha256, ...(evidence !== undefined ? { evidence } : {}) }, gitCommonDir);
496
+ await writeExclusive(opts.manifest, manifest, 1024 * 1024);
497
+ result = { mode: 'preview', manifest, manifestSha256: sha256(serializeRecoveryDocument(manifest)), accounting: 'unchanged', scope: 'local audit only; no admission or approval' };
498
+ }
499
+ else {
500
+ if ([opts.target, opts.gapRound, opts.admittingRound, opts.attempt, opts.run, opts.report, opts.reportSha256, opts.incomplete, opts.incompleteSha256, opts.evidence].some(v => v !== undefined))
501
+ throw new Error('apply_uses_only_pinned_manifest');
502
+ result = { mode: opts.apply ? 'apply' : 'resume', result: await applyRoundGap({ manifest: opts.manifest, manifestSha256: opts.manifestSha256, mode: opts.apply ? 'apply' : 'resume' }, gitCommonDir), accounting: 'unchanged', scope: 'local audit only; no admission or approval' };
503
+ }
504
+ console.log(JSON.stringify(result));
505
+ }
506
+ catch (error) {
507
+ console.error(JSON.stringify({ error: { code: 'RCL_CONVERGE_GAP', message: error instanceof Error ? error.message : String(error) } }));
508
+ process.exitCode = 3;
509
+ }
510
+ });
511
+ function blockingHealthJson(health) {
512
+ return {
513
+ conclusive: health.conclusive,
514
+ blockingSeats: health.policy.seatCount,
515
+ successfulBlockingSeats: health.successfulSeats.length,
516
+ requiredSuccessfulSeats: health.policy.minimumSuccessful,
517
+ quorumFraction: health.policy.fraction,
518
+ incompleteSeats: health.unsuccessfulSeats,
519
+ excludedSuccesses: health.excludedSuccesses,
520
+ };
521
+ }
522
+ program
523
+ .command('converge-report')
524
+ .description('Dedupe a round report against the converge run state, enforce the round cap, and classify findings as new/repeat/suppressed/regating. ' +
525
+ 'Refuses a report whose blocking reviewer health is inconclusive or cannot be derived from its reviews and roster (exit 4). ' +
526
+ 'Exit codes: 0 admitted, 2 round cap, 3 state failure, 4 reviewer health.')
527
+ .option('--target [key]', 'Stable convergence target key (same key as converge-attempt)')
528
+ .option('--report [path]', 'Round report JSON (a --json-file output)')
529
+ .option('--round [n]', 'Evidence round number (1-based)')
530
+ .option('--max-rounds [n]', `Round cap override (default ${DEFAULT_CONVERGE_ROUND_CAP}, hard maximum ${HARD_CONVERGE_ROUND_CAP})`)
531
+ .option('--json', 'Output JSON')
532
+ .action(async (opts) => {
533
+ try {
534
+ if (typeof opts.target !== 'string' || opts.target.trim() === '') {
535
+ throw new ConvergeRunStateError('--target is required.');
536
+ }
537
+ if (typeof opts.report !== 'string' || opts.report.trim() === '') {
538
+ throw new ConvergeRunStateError('--report is required.');
539
+ }
540
+ const round = typeof opts.round === 'string' ? Number(opts.round) : NaN;
541
+ if (!Number.isSafeInteger(round) || round < 1) {
542
+ throw new ConvergeRunStateError('--round must be a positive integer.');
543
+ }
544
+ let maxRounds;
545
+ if (opts.maxRounds !== undefined) {
546
+ maxRounds = typeof opts.maxRounds === 'string' ? Number(opts.maxRounds) : NaN;
547
+ if (!Number.isSafeInteger(maxRounds)) {
548
+ throw new ConvergeRunStateError(`--max-rounds must be an integer between 2 and ${HARD_CONVERGE_ROUND_CAP}.`);
549
+ }
550
+ }
551
+ let report;
552
+ let reportSha256;
553
+ try {
554
+ const source = await readFile(opts.report);
555
+ report = JSON.parse(source.toString('utf8'));
556
+ reportSha256 = sha256(source);
557
+ }
558
+ catch (err) {
559
+ throw new ConvergeRunStateError(`Could not read report JSON: ${opts.report}`, {
560
+ cause: err,
561
+ });
562
+ }
563
+ if (!Array.isArray(report.findings)) {
564
+ throw new ConvergeRunStateError(`Not an rcl report (no findings array): ${opts.report}`);
565
+ }
566
+ // Reviewer health gates admission before any native state is read or
567
+ // written: an inconclusive report is never triaged (RCL-136).
568
+ const health = assertAdmissibleReportHealth(report);
569
+ // The round remembers the report's run id only when it is a UUID and
570
+ // the report was produced for this converge target (a report copied
571
+ // from another target must not bind its run to this loop).
572
+ const reportRunId = typeof report.run?.id === 'string' && UUID_PATTERN.test(report.run.id) ? report.run.id : undefined;
573
+ // A report without a converge target (a plain `rcl review`, or one
574
+ // copied in) is not this loop's evidence either.
575
+ const reportTarget = report.run?.converge?.target;
576
+ const runId = reportRunId !== undefined && typeof reportTarget === 'string' && reportTarget.trim() === opts.target.trim()
577
+ ? reportRunId
578
+ : undefined;
579
+ if (reportRunId !== undefined && runId === undefined) {
580
+ console.error(chalk.yellow(`Report run ${reportRunId} ${reportTarget === undefined ? 'carries no converge target' : `belongs to converge target ${reportTarget}`}, not ${opts.target}; the round keeps no run id.`));
581
+ }
582
+ const result = await processRoundReport({
583
+ gitCommonDir: await resolveGitCommonDir(),
584
+ target: opts.target,
585
+ round,
586
+ findings: report.findings,
587
+ reportSha256,
588
+ cycleId: report.run?.cycle_id,
589
+ ...(maxRounds !== undefined ? { maxRounds } : {}),
590
+ ...(runId !== undefined ? { runId } : {}),
591
+ });
592
+ const classified = result.findings.map((f) => ({
593
+ identity: f.identity,
594
+ status: f.status,
595
+ gating: findingGatingReason(f.finding),
596
+ severity: f.finding.severity,
597
+ file: f.finding.file,
598
+ startLine: f.finding.startLine,
599
+ endLine: f.finding.endLine,
600
+ title: f.finding.title,
601
+ ...(f.suppressReason ? { suppressReason: f.suppressReason } : {}),
602
+ }));
603
+ const actionable = classified.filter((f) => (f.status === 'new' || f.status === 'regating') && f.gating !== 'none');
604
+ await reportConvergeEvents([
605
+ buildEvent({
606
+ kind: 'round_processed',
607
+ convergeTarget: opts.target,
608
+ round,
609
+ ...(runId !== undefined ? { runId } : {}),
610
+ payload: {
611
+ round,
612
+ round_cap: result.roundCap,
613
+ counts: result.counts,
614
+ actionable_gating: actionable.length,
615
+ // Which identity each sighting was matched to, so the server
616
+ // can apply standing verdicts to keys that moved (IO-12601).
617
+ identities: roundIdentities(result.findings),
618
+ },
619
+ }),
620
+ ...(maxRounds !== undefined
621
+ ? [buildEvent({ kind: 'cap_changed', convergeTarget: opts.target, round, payload: { kind: 'rounds', to: result.roundCap } })]
622
+ : []),
623
+ ]);
624
+ if (opts.json) {
625
+ console.log(JSON.stringify({
626
+ target: opts.target,
627
+ round,
628
+ roundCap: result.roundCap,
629
+ reviewerHealth: blockingHealthJson(health),
630
+ counts: result.counts,
631
+ actionableGating: actionable.length,
632
+ findings: classified,
633
+ }, null, 2));
634
+ return;
635
+ }
636
+ console.log(chalk.dim(describeBlockingHealth(health)));
637
+ console.log(`Round ${round}/${result.roundCap} for ${opts.target}: ` +
638
+ `${result.counts.new} new, ${result.counts.repeat} repeat, ` +
639
+ `${result.counts.suppressed} suppressed, ${result.counts.regating} regating · ` +
640
+ `${actionable.length} actionable gating finding(s)`);
641
+ for (const f of actionable) {
642
+ console.log(` [${f.status}] ${f.identity} ${f.file}:${f.startLine} — ${f.title}`);
643
+ }
644
+ for (const f of classified.filter((c) => c.status === 'suppressed')) {
645
+ console.log(chalk.dim(` [suppressed] ${f.identity} ${f.file}:${f.startLine} — ${f.suppressReason}`));
646
+ }
647
+ }
648
+ catch (err) {
649
+ const message = err instanceof Error ? err.message : String(err);
650
+ if (opts.json) {
651
+ const code = err instanceof ConvergeRunStateError && err.message === 'report_health_inconclusive'
652
+ ? 'report_health_inconclusive'
653
+ : err instanceof ConvergeRoundCapError || err instanceof ConvergeRunStateError
654
+ ? err.code
655
+ : err instanceof ReportHealthError
656
+ ? err.code
657
+ : 'RCL_CONVERGE_REPORT_ERROR';
658
+ const health = err instanceof ReportHealthError && err.health ? { reviewerHealth: blockingHealthJson(err.health) } : {};
659
+ console.error(JSON.stringify({ error: { code, message, ...health } }));
660
+ }
661
+ else {
662
+ console.error(chalk.red(message));
663
+ }
664
+ // Exit 2 = round-cap consent boundary (mirrors converge-attempt);
665
+ // exit 3 = state/infrastructure failure; exit 4 = reviewer health
666
+ // is inconclusive or unverifiable, so nothing was admitted.
667
+ const healthRefusal = err instanceof ReportHealthError ||
668
+ (err instanceof ConvergeRunStateError && err.message === 'report_health_inconclusive');
669
+ process.exitCode = err instanceof ConvergeRoundCapError ? 2 : healthRefusal ? 4 : 3;
670
+ }
671
+ });
672
+ // Record triage outcomes for finding identities (RCL-24; the precision
673
+ // history these verdicts build feeds RCL-27's model weighting).
674
+ program
675
+ .command('converge-verdict')
676
+ .description('Record fixed/dismissed triage verdicts for finding identities in the converge run state')
677
+ .option('--run-id <uuid>', 'Report run UUID (required for fresh review cycles)')
678
+ .option('--target [key]', 'Stable convergence target key')
679
+ .option('--round [n]', 'Evidence round the triage belongs to')
680
+ .option('--fixed <key>', 'Finding identity verified and fixed (repeatable)', (val, prev) => {
681
+ prev.push(val);
682
+ return prev;
683
+ }, [])
684
+ .option('--fixed-reason <key=reason>', 'Current fix explanation for an identity also passed to --fixed (repeatable)', (val, prev) => {
685
+ prev.push(val);
686
+ return prev;
687
+ }, [])
688
+ .option('--dismissed <key=reason>', 'Finding identity dismissed, with reason (repeatable)', (val, prev) => {
689
+ prev.push(val);
690
+ return prev;
691
+ }, [])
692
+ .option('--json', 'Output JSON')
693
+ .action(async (opts) => {
694
+ try {
695
+ if (typeof opts.target !== 'string' || opts.target.trim() === '') {
696
+ throw new ConvergeRunStateError('--target is required.');
697
+ }
698
+ const round = typeof opts.round === 'string' ? Number(opts.round) : NaN;
699
+ if (!Number.isSafeInteger(round) || round < 1) {
700
+ throw new ConvergeRunStateError('--round must be a positive integer.');
701
+ }
702
+ const fixedReasons = new Map();
703
+ for (const assignment of opts.fixedReason) {
704
+ const eq = assignment.indexOf('=');
705
+ const key = assignment.slice(0, eq);
706
+ const reason = assignment.slice(eq + 1);
707
+ if (eq < 1 || reason.trim() === '' || !opts.fixed.includes(key)) {
708
+ throw new ConvergeRunStateError('--fixed-reason requires key=reason for an identity also passed to --fixed.');
709
+ }
710
+ if (fixedReasons.has(key)) {
711
+ throw new ConvergeRunStateError(`Duplicate --fixed-reason for finding "${key}".`);
712
+ }
713
+ fixedReasons.set(key, reason);
714
+ }
715
+ const verdicts = [
716
+ ...opts.fixed.map((key) => ({ key, verdict: 'fixed', reason: fixedReasons.get(key) })),
717
+ ...opts.dismissed.map((entry) => {
718
+ const eq = entry.indexOf('=');
719
+ return eq === -1
720
+ ? { key: entry, verdict: 'dismissed' }
721
+ : {
722
+ key: entry.slice(0, eq),
723
+ verdict: 'dismissed',
724
+ reason: entry.slice(eq + 1),
725
+ };
726
+ }),
727
+ ];
728
+ if (verdicts.length === 0) {
729
+ throw new ConvergeRunStateError('Nothing to record: pass --fixed and/or --dismissed.');
730
+ }
731
+ if (new Set(verdicts.map(({ key }) => key)).size !== verdicts.length) {
732
+ throw new ConvergeRunStateError('Pass each finding identity only once, as either fixed or dismissed.');
733
+ }
734
+ const { entries: updated, resolution, runId: roundRun } = await recordVerdicts({
735
+ gitCommonDir: await resolveGitCommonDir(),
736
+ target: opts.target,
737
+ round,
738
+ verdicts,
739
+ runId: opts.runId,
740
+ });
741
+ // Feed the cross-run precision history (RCL-27) — fail-soft, the
742
+ // verdicts above are already durably recorded.
743
+ try {
744
+ const ts = new Date().toISOString();
745
+ await appendOutcomes(updated
746
+ .filter((e) => e.verdict !== undefined && e.models.length > 0)
747
+ .map((e) => ({
748
+ ts,
749
+ verdict: e.verdict,
750
+ models: e.models,
751
+ severity: e.verdictSeverity ?? e.severity,
752
+ target: opts.target,
753
+ findingKey: e.key,
754
+ source: 'live',
755
+ })));
756
+ }
757
+ catch (err) {
758
+ // Advisory history; verdict recording must not fail over it —
759
+ // but say so, or a broken store silently stops learning.
760
+ console.warn(`Model-stats store unavailable (outcomes not recorded): ${String(err)}`);
761
+ }
762
+ await reportConvergeEvents([
763
+ buildEvent({
764
+ kind: 'verdicts_recorded',
765
+ convergeTarget: opts.target,
766
+ round,
767
+ ...(roundRun !== undefined ? { runId: roundRun } : {}),
768
+ payload: {
769
+ verdicts: updated.map((e) => ({
770
+ identity_key: e.key,
771
+ verdict: e.verdict,
772
+ // Verdict reasons are user-authored prose: scrubbed like every other free text that leaves the machine.
773
+ ...(e.verdictReason !== undefined ? { reason: scrubText(e.verdictReason, 500) } : {}),
774
+ severity: e.verdictSeverity ?? e.severity,
775
+ models: e.models,
776
+ })),
777
+ },
778
+ }),
779
+ ...(resolution
780
+ ? [
781
+ buildEvent({
782
+ kind: 'resolution',
783
+ convergeTarget: opts.target,
784
+ round,
785
+ ...(roundRun !== undefined ? { runId: roundRun } : {}),
786
+ payload: {
787
+ status: resolution.status,
788
+ actionable: resolution.actionable,
789
+ unresolved: resolution.unresolved.length,
790
+ fixed_this_round: resolution.fixedThisRound,
791
+ },
792
+ }),
793
+ ]
794
+ : []),
795
+ ]);
796
+ if (opts.json) {
797
+ console.log(JSON.stringify({
798
+ target: opts.target,
799
+ round,
800
+ recorded: verdicts.length,
801
+ ...(resolution ? { resolution } : {}),
802
+ }));
803
+ }
804
+ else {
805
+ console.log(`Recorded ${verdicts.length} verdict(s) for ${opts.target} round ${round}.`);
806
+ if (resolution) {
807
+ switch (resolution.status) {
808
+ case 'converged-dismissal-only':
809
+ console.log(`Round ${round} resolution: all ${resolution.actionable} gating finding(s) dismissed, ` +
810
+ 'nothing fixed — the reviewed patch is unchanged, so this round CONVERGES. ' +
811
+ 'No confirmation round is required (RCL-30).');
812
+ break;
813
+ case 'fixes-pending-fresh-round':
814
+ console.log(`Round ${round} resolution: ${resolution.fixedThisRound} fix(es) recorded — ` +
815
+ 'the patch changes; commit, push, and run a fresh exact-head round.');
816
+ break;
817
+ case 'unresolved':
818
+ console.log(`Round ${round} resolution: ${resolution.unresolved.length} gating identity(ies) ` +
819
+ `still untriaged: ${resolution.unresolved.join(', ')}`);
820
+ break;
821
+ }
822
+ }
823
+ }
824
+ }
825
+ catch (err) {
826
+ const message = err instanceof Error ? err.message : String(err);
827
+ if (opts.json) {
828
+ console.error(JSON.stringify({ error: { code: 'RCL_CONVERGE_VERDICT', message } }));
829
+ }
830
+ else {
831
+ console.error(chalk.red(message));
832
+ }
833
+ process.exitCode = 3;
834
+ }
835
+ });
836
+ // Evidence delivery operations (IO-12475 section 8.10).
837
+ const telemetry = program
838
+ .command('telemetry')
839
+ .description('Evidence delivery to Harness: credentials, queued deliveries and retained rejected reports');
840
+ telemetry
841
+ .command('recover-reviewer')
842
+ .description('Deliver one exact retained terminal reviewer report without restarting reviewers or verification')
843
+ .requiredOption('--target <target>', 'Exact guarded convergence target')
844
+ .requiredOption('--run <id>', 'Exact retained terminal run UUID')
845
+ .option('--json', 'Output JSON')
846
+ .action(async (opts) => {
847
+ try {
848
+ const runtime = await createTelemetryRuntime({ rclVersion: RCL_VERSION });
849
+ const result = await deliverTerminalReviewerRun(runtime, { target: opts.target, runId: opts.run });
850
+ if (opts.json)
851
+ console.log(JSON.stringify(result, null, 2));
852
+ else
853
+ console.log(`${result.outcome.line}\nReport ${result.reportSha256}; reviewer artifact ${result.reviewerArtifactSha256}`);
854
+ process.exitCode = result.outcome.exitCode;
855
+ }
856
+ catch (error) {
857
+ const message = scrubText(error instanceof Error ? error.message : String(error), 300);
858
+ if (opts.json)
859
+ console.error(JSON.stringify({ error: { code: 'RCL_TERMINAL_REVIEWER_DELIVERY', message } }));
860
+ else
861
+ console.error(chalk.red(`Cannot recover terminal reviewer delivery: ${message}`));
862
+ process.exitCode = 4;
863
+ }
864
+ });
865
+ telemetry
866
+ .command('rejected')
867
+ .description('Inspect immutable retained evidence without delivering or retrying it')
868
+ .option('--run <id>', 'Select one retained original run')
869
+ .option('--json', 'Output JSON')
870
+ .action(async (opts) => {
871
+ const store = new Quarantine(join(resolveDataDir(), QUARANTINE_DIR));
872
+ try {
873
+ const selected = opts.run ? await store.inspect(opts.run) : undefined;
874
+ const entries = opts.run ? (selected ? [selected] : []) : await store.list();
875
+ if (opts.json)
876
+ console.log(JSON.stringify({ dir: store.dir, entries }, null, 2));
877
+ else {
878
+ console.log(`Retained evidence: ${entries.length} entr${entries.length === 1 ? 'y' : 'ies'} — no automatic retry`);
879
+ for (const entry of entries)
880
+ console.log(` ${entry.runId} ${entry.status} ${entry.path}${entry.error ? ` (${entry.error})` : ''}`);
881
+ }
882
+ if ((opts.run && entries.length === 0) || entries.some((e) => e.status !== 'complete'))
883
+ process.exitCode = 1;
884
+ }
885
+ catch (error) {
886
+ console.error(`Cannot inspect retained evidence: ${scrubText(error instanceof Error ? error.message : String(error), 200)}`);
887
+ process.exitCode = 1;
888
+ }
889
+ });
890
+ telemetry
891
+ .command('status')
892
+ .description('Show the telemetry level, the credential source and every spooled delivery')
893
+ .option('--json', 'Output JSON')
894
+ .action(async (opts) => {
895
+ const runtime = await createTelemetryRuntime({ rclVersion: RCL_VERSION, requireRepo: false });
896
+ const entries = await runtime.outbox.list();
897
+ const loss = await runtime.outbox.pendingLoss();
898
+ const refused = await runtime.outbox.refusedLoss();
899
+ const status = {
900
+ level: runtime.level,
901
+ repoManaged: runtime.repoManaged,
902
+ credential: runtime.credential
903
+ ? { source: runtime.credential.source, host: credentialHost(runtime.credential) }
904
+ : null,
905
+ note: runtime.note ?? null,
906
+ outbox: { dir: runtime.outbox.dir, entries, pendingLoss: loss, refusedLoss: refused },
907
+ };
908
+ if (opts.json) {
909
+ console.log(JSON.stringify(status, null, 2));
910
+ return;
911
+ }
912
+ console.log(`Telemetry level: ${status.level}`);
913
+ console.log(status.repoManaged
914
+ ? 'Repository: Harness-managed (.harness-cli/config.json found) — reviews here are delivered'
915
+ : 'Repository: not Harness-managed — reviews here are not delivered; the outbox still flushes');
916
+ console.log(status.credential
917
+ ? `Credential: ${status.credential.source} → ${status.credential.host}`
918
+ : `Credential: none${status.note ? ` (${status.note})` : ''}`);
919
+ console.log(`Outbox: ${runtime.outbox.dir} — ${entries.length} entr${entries.length === 1 ? 'y' : 'ies'}`);
920
+ for (const entry of entries) {
921
+ console.log(` ${entry.id} ${entry.meta.kind} spooled ${entry.meta.spooled_at} attempts ${entry.meta.attempts} ` +
922
+ `${entry.bytes} bytes artifacts [${entry.artifacts.join(', ')}] events ${entry.events}` +
923
+ (entry.meta.envelope_delivered ? ' (envelope delivered)' : '') +
924
+ (entry.failed ? chalk.red(` FAILED: ${entry.failed.reason}`) : ''));
925
+ }
926
+ if (loss.length > 0) {
927
+ console.log(chalk.yellow(`Artifacts not spooled (outbox over its cap) for ${loss.length} run(s); reported on the next flush.`));
928
+ }
929
+ if (refused.length > 0) {
930
+ console.log(chalk.yellow(`${refused.length} loss report(s) the server refused are kept under ${runtime.outbox.dir}/loss (*.refused); they are not retried.`));
931
+ }
932
+ });
933
+ telemetry
934
+ .command('flush')
935
+ .description('Deliver every spooled envelope, artifact and event batch (runs to completion)')
936
+ .option('--run <id>', 'Flush one spooled run only')
937
+ .option('--envelope-timeout-ms <ms>', 'Envelope POST timeout in milliseconds (1–120000; default: 10000)', value => {
938
+ try {
939
+ return validateEnvelopeTimeoutMs(/^\d+$/.test(value) ? Number(value) : NaN);
940
+ }
941
+ catch (error) {
942
+ throw new InvalidArgumentError(error.message);
943
+ }
944
+ })
945
+ .option('--json', 'Output JSON')
946
+ .action(async (opts) => {
947
+ const runtime = await createTelemetryRuntime({ rclVersion: RCL_VERSION, requireRepo: false,
948
+ ...(opts.envelopeTimeoutMs !== undefined ? { envelopeTimeoutMs: opts.envelopeTimeoutMs } : {}) });
949
+ if (!runtime.sink) {
950
+ const reason = runtime.level === 'off' ? 'telemetry is off' : runtime.note ?? 'no Harness credential';
951
+ console.error(chalk.red(`Cannot flush: ${reason}.`));
952
+ process.exitCode = 1;
953
+ return;
954
+ }
955
+ const summary = await flushOutbox(runtime, opts.run ? { runId: opts.run } : {});
956
+ const reconciled = await reconcileFlushedRun(opts.run, summary, runtime.sink, {
957
+ cwd: process.cwd(),
958
+ onError: error => process.stderr.write(chalk.dim(`Run reconciliation skipped: ${String(error)}\n`)),
959
+ });
960
+ if (reconciled === 'reconciled')
961
+ process.stderr.write(chalk.dim('Reconciled delivered run ' + opts.run + ' with its guarded launch state.\n'));
962
+ if (opts.json) {
963
+ console.log(JSON.stringify(summary, null, 2));
964
+ }
965
+ else {
966
+ console.log(`Delivered ${summary.delivered.length}, remaining ${summary.remaining.length}, failed ${summary.failed.length}, dropped ${summary.dropped.length}` +
967
+ (summary.stopped ? ` (stopped: ${summary.stopped})` : '') +
968
+ (summary.lossPending ? `; ${summary.lossPending} loss report(s) still pending` : ''));
969
+ for (const id of summary.delivered)
970
+ console.log(` delivered ${id}`);
971
+ for (const f of summary.failed)
972
+ console.log(chalk.red(` failed ${f.id}: ${f.reason}`));
973
+ for (const d of summary.dropped)
974
+ console.log(chalk.dim(` dropped ${d.id}: ${d.reason}`));
975
+ for (const id of summary.remaining)
976
+ console.log(chalk.yellow(` remaining ${id}`));
977
+ }
978
+ if (summary.remaining.length > 0 || summary.failed.length > 0)
979
+ process.exitCode = 1;
980
+ });
981
+ // Evidence reads (RCL-41): what Harness holds about a pull request's gate
982
+ // and about one run. The skills gate on `evidence status`'s exit code: 0 only
983
+ // when the judged projection is converged, 1 for any other status, 2 when the
984
+ // pull request cannot be named, 3 when the read could not be answered.
985
+ const evidenceCmd = program
986
+ .command('evidence')
987
+ .description('Read Review Council evidence on Harness: a pull request’s gate status, one run’s record');
988
+ function evidenceDeps() {
989
+ return {
990
+ rclVersion: RCL_VERSION,
991
+ stdout: (line) => console.log(line),
992
+ stderr: (line) => console.error(chalk.red(line)),
993
+ };
994
+ }
995
+ evidenceCmd
996
+ .command('status [pr]')
997
+ .description('Gate status of a pull request — N or #N against the current remote, owner/repo#N, or a URL; exit 0 only when the judged projection is converged')
998
+ .option('--enforced', 'Judge the enforced projection instead of the advisory one')
999
+ .option('--json', 'Print the API status object')
1000
+ .action(async (pr, opts) => {
1001
+ process.exitCode = await runEvidenceStatus(pr, opts, evidenceDeps());
1002
+ });
1003
+ evidenceCmd
1004
+ .command('show [run-id]')
1005
+ .description('One recorded run: header, reviewer health, artifacts, findings with identity, gating reason and verdict')
1006
+ .option('--json', 'Print the API run object')
1007
+ .action(async (runId, opts) => {
1008
+ process.exitCode = await runEvidenceShow(runId ?? '', opts, evidenceDeps());
1009
+ });
1010
+ evidenceCmd
1011
+ .command('recover-run')
1012
+ .description('Preview, apply or resume delivery of one immutable original asserted run; no review, events or native accounting')
1013
+ .option('--preview', 'Validate sources and scoped remote reads; write only a new exclusive manifest')
1014
+ .option('--apply', 'Start the explicitly pinned manifest operation')
1015
+ .option('--resume', 'Resume the same journal through fresh exact run/artifact reads')
1016
+ .requiredOption('--manifest <path>', 'Exclusive preview manifest; existing manifest for apply/resume')
1017
+ .option('--manifest-sha256 <sha256>', 'Required exact reviewed manifest digest for apply/resume')
1018
+ .option('--run <uuid>', 'Original run UUID (preview)')
1019
+ .option('--for-pr <owner/repo#N>', 'Original repository and pull request (preview)')
1020
+ .option('--head <sha>', 'Original full reviewed head SHA (preview)')
1021
+ .option('--report-json <path>', 'Original bounded regular UTF-8 JSON file (preview)')
1022
+ .option('--report-sha256 <sha256>', 'Exact original JSON digest (preview)')
1023
+ .option('--report-md <path>', 'Optional original Markdown file (preview)')
1024
+ .option('--markdown-sha256 <sha256>', 'Required digest when selecting original Markdown')
1025
+ .option('--original-mode <mode>', 'Required operator assertion: asserted; CI/attested originals unsupported')
1026
+ .option('--original-prose <mode>', 'Explicit original finding-prose representation: control-code-units-v1')
1027
+ .option('--json', 'Print machine-readable operation status and diagnostics')
1028
+ .action(async (opts) => {
1029
+ process.exitCode = await runOriginalRecovery(opts, evidenceDeps());
1030
+ });
1031
+ evidenceCmd
1032
+ .command('recover-finding')
1033
+ .description('Preview an unpaid, attributed correction of one recorded finding identity; never reruns review or changes native history')
1034
+ .requiredOption('--target <target>', 'The retained native convergence target')
1035
+ .requiredOption('--run <uuid>', 'The immutable Harness run ID')
1036
+ .requiredOption('--report-sha256 <sha256>', 'The original report JSON digest')
1037
+ .requiredOption('--finding-ref <ref>', 'The exact finding ref within the run')
1038
+ .requiredOption('--identity <key>', 'The native canonical identity from retained state')
1039
+ .requiredOption('--for-pr <owner/repo#N>', 'The expected pull request, explicitly named')
1040
+ .option('--submit', 'Submit the correction; without this flag only read and preview')
1041
+ .action(async (opts) => {
1042
+ process.exitCode = await runFindingRecovery(opts, evidenceDeps());
1043
+ });
1044
+ evidenceCmd
1045
+ .command('retriage-finding')
1046
+ .description('Preview a fresh dismissal of one recorded finding at its actual severity; no rematching or native history changes')
1047
+ .requiredOption('--target <target>', 'The recorded convergence target, or an event label for a standalone attested run')
1048
+ .requiredOption('--run <uuid>', 'The immutable Harness run ID')
1049
+ .requiredOption('--report-sha256 <sha256>', 'The original stored report JSON digest')
1050
+ .requiredOption('--finding-ref <ref>', 'The exact finding ref within the run')
1051
+ .requiredOption('--for-pr <owner/repo#N>', 'The expected pull request, explicitly named')
1052
+ .requiredOption('--reason-file <path>', 'UTF-8 file containing the explicit source-backed reason for this new judgment')
1053
+ .option('--submit', 'Submit the fresh verdict; without this flag only read and preview')
1054
+ .action(async (opts) => {
1055
+ process.exitCode = await runFindingRetriage(opts, evidenceDeps());
1056
+ });
1057
+ telemetry
1058
+ .command('recover-refutations')
1059
+ .description('Discover original refutations and write a reviewed recovery manifest; dry-run by default')
1060
+ .requiredOption('--manifest <path>', 'New private manifest path, or the reviewed manifest with --apply')
1061
+ .option('--root <path>', 'Discovery root; repeat to replace the default roots', (value, previous = []) => [...previous, value])
1062
+ .option('--exclude-sha256 <digest>', 'Explicit synthetic report digest; repeat as needed', (value, previous = []) => [...previous, value])
1063
+ .option('--inventory-only', 'Offline discovery artifact without a destination; cannot be applied')
1064
+ .option('--apply', 'Revalidate and apply only the reviewed manifest selections')
1065
+ .option('--output <path>', 'New outcome path for apply; defaults to a unique file beside the manifest')
1066
+ .action(async (opts) => {
1067
+ process.exitCode = await runRefutationRecovery(opts, evidenceDeps());
1068
+ });
1069
+ telemetry
1070
+ .command('backfill')
1071
+ .description('Post recovered pre-3.0 reports and converge ledgers as backfill evidence (deterministic ids: running twice adds nothing)')
1072
+ .requiredOption('--from <dir>', 'Directory of rcl-report-*.json reports and rcl-converge-*-ledger.md ledgers')
1073
+ .requiredOption('--repo <owner/repo>', 'The GitHub repository the reports reviewed')
1074
+ .option('--dry-run', 'Build the runs and report counts without posting')
1075
+ .option('--json', 'Output JSON')
1076
+ .action(async (opts) => {
1077
+ const repo = parseRepoName(opts.repo);
1078
+ if (repo === null) {
1079
+ console.error(chalk.red('--repo must be a GitHub owner/repo.'));
1080
+ process.exitCode = 2;
1081
+ return;
1082
+ }
1083
+ // A dry run builds and counts; it needs no credential and sends nothing.
1084
+ // The ids still depend on the host, so a credential is used for them when
1085
+ // there is one and a placeholder named otherwise.
1086
+ const runtime = await createTelemetryRuntime({ rclVersion: RCL_VERSION, requireRepo: false });
1087
+ let host;
1088
+ if (runtime.sink && runtime.credential) {
1089
+ host = credentialHost(runtime.credential);
1090
+ }
1091
+ else if (opts.dryRun) {
1092
+ host = 'no-credential';
1093
+ }
1094
+ else {
1095
+ const reason = runtime.level === 'off' ? 'telemetry is off' : runtime.note ?? 'no Harness credential';
1096
+ console.error(chalk.red(`Cannot backfill: ${reason}.`));
1097
+ process.exitCode = 1;
1098
+ return;
1099
+ }
1100
+ let summary;
1101
+ try {
1102
+ summary = await runBackfill({ dir: opts.from, repo: `${repo.owner}/${repo.repo}`, rclVersion: RCL_VERSION, ...(opts.dryRun ? { dryRun: true } : {}) }, {
1103
+ ...(runtime.sink ? { sink: runtime.sink } : {}),
1104
+ host,
1105
+ placeholderHost: host === 'no-credential',
1106
+ ...(opts.json ? {} : { progress: (line) => console.log(chalk.dim(text(line, 400))) }),
1107
+ });
1108
+ }
1109
+ catch (err) {
1110
+ console.error(chalk.red(`Cannot backfill: ${scrubText(err instanceof Error ? err.message : String(err), 300)}`));
1111
+ process.exitCode = 1;
1112
+ return;
1113
+ }
1114
+ if (opts.json) {
1115
+ console.log(JSON.stringify(summary, null, 2));
1116
+ }
1117
+ else if (summary.dryRun) {
1118
+ console.log(`Would post ${summary.runs} run(s) with ${summary.planned.artifacts} artifact(s) and ${summary.planned.events} verdict event(s) ` +
1119
+ `(${summary.planned.verdicts} verdicts)${host === 'no-credential' ? ' — ids shown for a placeholder host; log in for the real ones' : ''}; ` +
1120
+ `${summary.skipped} file(s) skipped; ledgers ${summary.ledgersScanned}, bullets matched ${summary.bulletsMatched}, unmatched ${summary.bulletsUnmatched}.`);
1121
+ for (const f of summary.skippedFiles)
1122
+ console.log(chalk.dim(` skipped ${text(f.file, 200)}: ${text(f.reason, 300)}`));
1123
+ }
1124
+ else {
1125
+ console.log(`Posted ${summary.runs} run(s): ${summary.created} new, ${summary.existing} already recorded; ` +
1126
+ `${summary.artifacts} artifact(s) uploaded for the new runs; verdict events ${summary.events.inserted} new, ${summary.events.duplicates} already recorded; ` +
1127
+ `${summary.skipped} file(s) skipped; ledgers ${summary.ledgersScanned}, bullets matched ${summary.bulletsMatched} (at build), unmatched ${summary.bulletsUnmatched}.`);
1128
+ for (const f of summary.skippedFiles)
1129
+ console.log(chalk.dim(` skipped ${text(f.file, 200)}: ${text(f.reason, 300)}`));
1130
+ for (const f of summary.failed)
1131
+ console.log(chalk.red(` failed ${text(f.file, 200)}: ${text(f.reason, 300)}`));
1132
+ }
1133
+ if (summary.failed.length > 0)
1134
+ process.exitCode = 1;
1135
+ });
1136
+ // Detached async-lane worker (RCL-25) — launched by the review process for
1137
+ // each async (non-blocking) reviewer call; not for interactive use.
1138
+ program
1139
+ .command('async-worker', { hidden: true })
1140
+ .requiredOption('--spool <path>', 'Spool file written by the launching review')
1141
+ .action(async (opts) => {
1142
+ try {
1143
+ await runAsyncWorker(opts.spool);
1144
+ }
1145
+ catch {
1146
+ // Nothing is awaiting this process; a failed worker simply leaves no
1147
+ // result to merge. Exit non-zero for post-mortem visibility only.
1148
+ process.exitCode = 1;
1149
+ }
1150
+ });
1151
+ // Per-model triage history (RCL-27): trailing precision, volume, latency,
1152
+ // dead-call rate, and the consensus weight each model earns from them.
1153
+ const modelsCmd = program
1154
+ .command('models')
1155
+ .description('Per-model trailing precision, volume, latency, dead-call rate, and consensus weight');
1156
+ modelsCmd
1157
+ .command('show', { isDefault: true })
1158
+ .description(`Print per-model stats over the trailing window (default ${DEFAULT_WINDOW_DAYS} days)`)
1159
+ .option('--window <days>', 'Trailing window in days', String(DEFAULT_WINDOW_DAYS))
1160
+ .option('--local', 'This machine’s store only; do not ask Harness for the org-wide window')
1161
+ .option('--json', 'Output JSON')
1162
+ .action(async (opts) => {
1163
+ const windowDays = Number(opts.window);
1164
+ if (!Number.isFinite(windowDays) || windowDays <= 0) {
1165
+ console.error(chalk.red('--window must be a positive number of days.'));
1166
+ process.exitCode = 1;
1167
+ return;
1168
+ }
1169
+ const stats = await loadModelStats({ windowDays });
1170
+ // The org-wide window (RCL-38): Harness's `model-stats` over every run the
1171
+ // organization recorded. Its weight wins for a model it has the outcome
1172
+ // floor for; this machine's store decides below that. The same window is
1173
+ // asked of both; the server answers up to 366 days.
1174
+ const server = opts.local
1175
+ ? { kind: 'none', reason: '--local' }
1176
+ : !Number.isInteger(windowDays) || windowDays > 366
1177
+ ? { kind: 'none', reason: `the server window is whole days up to 366 (asked for ${windowDays})` }
1178
+ : await fetchServerModelStats({ rclVersion: RCL_VERSION, windowDays });
1179
+ const merged = mergeWeights(stats, server.kind === 'ok' ? server.value : undefined);
1180
+ if (opts.json) {
1181
+ console.log(JSON.stringify({
1182
+ windowDays,
1183
+ dataDir: resolveDataDir(),
1184
+ models: stats,
1185
+ server: server.kind === 'ok' ? { host: server.host, ...server.value } : null,
1186
+ serverNote: server.kind === 'ok' ? null : server.reason,
1187
+ weights: merged.map(({ model, weight, source, serverOutcomes, localOutcomes }) => ({
1188
+ model,
1189
+ weight,
1190
+ source,
1191
+ ...(serverOutcomes !== undefined ? { serverOutcomes } : {}),
1192
+ ...(localOutcomes !== undefined ? { localOutcomes } : {}),
1193
+ })),
1194
+ }, null, 2));
1195
+ return;
1196
+ }
1197
+ if (merged.length === 0) {
1198
+ console.log(`No model history in ${resolveDataDir()} yet` +
1199
+ (server.kind === 'ok' ? ` and none on ${server.host} for this window.` : ` (org-wide: ${server.reason}).`) +
1200
+ ' Converge runs record it automatically; seed from recovered artifacts with `rcl models seed --from <dir>`,' +
1201
+ ' or backfill the organization with `rcl telemetry backfill --from <dir> --repo <owner/repo>`.');
1202
+ return;
1203
+ }
1204
+ console.log('\n' + chalk.bold(`Model history — trailing ${windowDays} days`) + '\n');
1205
+ console.log(chalk.dim(server.kind === 'ok'
1206
+ ? `Org-wide from ${server.host}: ${server.value.models.length} model(s), window ${server.value.window_days} days, computed ${server.value.computed_at}.`
1207
+ : `Org-wide window not used (${server.reason}); weights are this machine’s.`));
1208
+ const pct = (v) => (v === undefined || v === null ? '—' : `${(v * 100).toFixed(0)}%`);
1209
+ console.log(chalk.dim('model'.padEnd(46) +
1210
+ 'precision (n)'.padEnd(16) +
1211
+ 'calls'.padEnd(8) +
1212
+ 'dead'.padEnd(7) +
1213
+ 'p50'.padEnd(8) +
1214
+ 'weight'.padEnd(8) +
1215
+ 'source'));
1216
+ for (const row of merged) {
1217
+ // Every column of a row comes from one record: the server's when the
1218
+ // server weighs it, else this machine's, else the server's thin row.
1219
+ const fromServer = row.source === 'server' || (row.local === undefined && row.server !== undefined);
1220
+ const outcomes = fromServer ? (row.server?.outcomes ?? 0) : (row.local?.outcomes ?? 0);
1221
+ const precision = fromServer ? pct(row.server?.precision) : pct(row.local?.precision);
1222
+ const calls = fromServer ? (row.server?.calls ?? 0) : (row.local?.calls ?? 0);
1223
+ const dead = fromServer ? pct(row.server?.dead_rate) : pct(row.local?.deadRate);
1224
+ const p50 = fromServer ? (row.server?.p50_ms ?? null) : (row.local?.p50Ms ?? null);
1225
+ console.log(text(row.model, 46).padEnd(46) +
1226
+ `${outcomes > 0 ? precision : '—'} (${outcomes})`.padEnd(16) +
1227
+ String(calls).padEnd(8) +
1228
+ dead.padEnd(7) +
1229
+ (typeof p50 === 'number' ? `${(p50 / 1000).toFixed(0)}s` : '—').padEnd(8) +
1230
+ row.weight.toFixed(2).padEnd(8) +
1231
+ row.source);
1232
+ }
1233
+ console.log(chalk.dim(`\nWeights (0.5 + precision, clamped to [0.5, 1.5]; neutral 1 under ${MIN_OUTCOMES_FOR_WEIGHT} outcomes) ` +
1234
+ 'scale each model’s consensus vote in reviews and gating. Source: server = the organization’s window on Harness, ' +
1235
+ 'local = this machine’s store, neutral = neither holds enough.\n'));
1236
+ });
1237
+ modelsCmd
1238
+ .command('seed')
1239
+ .description('Backfill the model-stats store from a directory of rcl reports and converge ledgers')
1240
+ .requiredOption('--from <dir>', 'Directory holding rcl-report-*.json and rcl-converge-*-ledger.md files')
1241
+ .option('--json', 'Output JSON')
1242
+ .action(async (opts) => {
1243
+ try {
1244
+ const { calls, outcomes, ...summary } = await buildSeedRecords(opts.from);
1245
+ await appendCalls(calls);
1246
+ await appendOutcomes(outcomes);
1247
+ if (opts.json) {
1248
+ console.log(JSON.stringify({ ...summary, dataDir: resolveDataDir() }, null, 2));
1249
+ }
1250
+ else {
1251
+ console.log(`Seeded ${summary.callsSeeded} call record(s) from ${summary.reportsScanned} report(s) and ` +
1252
+ `${summary.outcomesSeeded} outcome record(s) from ${summary.ledgersScanned} ledger(s) ` +
1253
+ `(${summary.unmatchedBullets}/${summary.bullets} ledger bullets could not be matched) → ${resolveDataDir()}`);
1254
+ }
1255
+ }
1256
+ catch (err) {
1257
+ console.error(chalk.red(`Seed failed: ${err instanceof Error ? err.message : String(err)}`));
1258
+ process.exitCode = 1;
1259
+ }
1260
+ });
1261
+ // roles subcommand
1262
+ const rolesCmd = program.command('roles').description('Manage and inspect roles');
1263
+ rolesCmd
1264
+ .command('list')
1265
+ .description('List all built-in roles')
1266
+ .action(() => {
1267
+ console.log('\n' + chalk.bold('Built-in Roles:') + '\n');
1268
+ for (const role of BUILTIN_ROLES) {
1269
+ const tag = role.isSpecialized ? chalk.dim('[specialized]') : chalk.blue('[general]');
1270
+ console.log(` ${chalk.cyan(role.name.padEnd(22))} ${tag} ${chalk.dim(role.description)}`);
1271
+ }
1272
+ console.log('');
1273
+ });
1274
+ rolesCmd
1275
+ .command('show <name>')
1276
+ .description('Show details for a specific role')
1277
+ .action((name) => {
1278
+ const role = getRoleByName(name);
1279
+ if (!role) {
1280
+ console.error(chalk.red(`Role "${name}" not found.`));
1281
+ console.log('Run `rcl roles list` to see available roles.');
1282
+ process.exit(1);
1283
+ }
1284
+ console.log('\n' + chalk.bold(`Role: ${role.name}`) + '\n');
1285
+ console.log(chalk.dim('Description:'), role.description);
1286
+ console.log(chalk.dim('Type:'), role.isSpecialized ? 'specialized' : 'general');
1287
+ console.log(chalk.dim('Focus:'), role.focus.join(', '));
1288
+ if (role.severityBias) {
1289
+ console.log(chalk.dim('Severity bias:'), JSON.stringify(role.severityBias));
1290
+ }
1291
+ console.log('\n' + chalk.dim('System Prompt:'));
1292
+ console.log(role.systemPrompt);
1293
+ console.log('');
1294
+ });
1295
+ /**
1296
+ * Shared front half of every council command: config, role resolution,
1297
+ * assignments. `fallbackRoles` is used only when neither CLI flags nor
1298
+ * config request roles (plan review defaults to a plan-suited subset).
1299
+ */
1300
+ /**
1301
+ * Harness key distribution runs BEFORE loadConfig: the loader's default-fleet
1302
+ * degradation (dropping openrouter models without OPENROUTER_API_KEY) must
1303
+ * see any injected keys.
1304
+ */
1305
+ async function fetchHarnessKeys(spinner, credential) {
1306
+ const { note } = await applyHarnessModelKeys(credential ? { credential } : {});
1307
+ if (note) {
1308
+ spinner.info(note);
1309
+ spinner.start('Loading configuration...');
1310
+ }
1311
+ }
1312
+ async function prepareCouncil(spinner, opts, fallbackRoles, attestation) {
1313
+ const startedAt = new Date();
1314
+ // Validate the converge context first: a bad --round must fail before any
1315
+ // model time is spent, not after the council has run.
1316
+ const converge = resolveConvergeContext({ convergeTarget: opts.convergeTarget, round: opts.round, attempt: opts.attempt }, reviewConvergeEnvironment(opts));
1317
+ if (!opts.exportPendingPackage)
1318
+ await fetchHarnessKeys(spinner, attestation?.credential);
1319
+ const config = await loadConfig(opts.config, undefined, { preserveDefaultRoster: opts.guardedConverge });
1320
+ // Validate mutually exclusive role options
1321
+ const roleOptionCount = [opts.role, opts.roles, opts.reviewer?.length].filter(Boolean).length;
1322
+ if (roleOptionCount > 1) {
1323
+ spinner.fail('--role, --roles, and --reviewer are mutually exclusive');
1324
+ process.exit(1);
1325
+ }
1326
+ // Override models from CLI
1327
+ if (opts.models) {
1328
+ config.models = opts.models.split(',').map((s) => s.trim()).filter(Boolean);
1329
+ // Clear secondary and async models unless explicitly provided — don't
1330
+ // leak code to default providers the user overrode away from.
1331
+ if (opts.secondaryModels === undefined) {
1332
+ config.secondaryModels = [];
1333
+ }
1334
+ if (opts.asyncModels === undefined) {
1335
+ config.asyncModels = [];
1336
+ }
1337
+ }
1338
+ if (opts.secondaryModels !== undefined) {
1339
+ config.secondaryModels = opts.secondaryModels.split(',').map((s) => s.trim()).filter(Boolean);
1340
+ }
1341
+ if (opts.asyncModels !== undefined) {
1342
+ config.asyncModels = opts.asyncModels.split(',').map((s) => s.trim()).filter(Boolean);
1343
+ }
1344
+ // Determine roles to use
1345
+ let requestedRoles;
1346
+ let explicitReviewers;
1347
+ if (opts.role) {
1348
+ requestedRoles = [opts.role];
1349
+ }
1350
+ else if (opts.roles) {
1351
+ requestedRoles = opts.roles.split(',').map((s) => s.trim());
1352
+ }
1353
+ else if (opts.reviewer && opts.reviewer.length > 0) {
1354
+ explicitReviewers = opts.reviewer.map((pair) => {
1355
+ const colonIdx = pair.indexOf(':');
1356
+ if (colonIdx < 0) {
1357
+ throw new InvalidArgumentError(`Invalid reviewer pair "${pair}". Use model:role format.`);
1358
+ }
1359
+ return {
1360
+ model: pair.slice(0, colonIdx),
1361
+ role: pair.slice(colonIdx + 1),
1362
+ };
1363
+ });
1364
+ }
1365
+ else if (fallbackRoles && !config.roles?.length) {
1366
+ requestedRoles = fallbackRoles;
1367
+ }
1368
+ // Load spec file
1369
+ let specContent;
1370
+ let spec;
1371
+ const specPath = opts.spec ?? config.spec;
1372
+ // Validated before any file is read so a typo fails fast, spec or not.
1373
+ const specSource = opts.specSource !== undefined
1374
+ ? parseSpecSource(opts.specSource)
1375
+ : specPath
1376
+ ? opts.spec
1377
+ ? 'flag'
1378
+ : 'repo_file'
1379
+ : undefined;
1380
+ if (specPath) {
1381
+ try {
1382
+ specContent = await readFile(specPath, 'utf-8');
1383
+ spec = { source: specSource ?? 'flag', sha256: sha256Hex(specContent) };
1384
+ }
1385
+ catch {
1386
+ if (opts.guardedConverge)
1387
+ throw new ReviewLaunchRefused('unreadable_spec', `Could not read required spec: ${specPath}`);
1388
+ spinner.warn(`Could not read spec file: ${specPath}`);
1389
+ }
1390
+ }
1391
+ // Provenance was claimed explicitly: refuse to run with nothing to attach
1392
+ // it to, rather than dropping the claim from the header without a word.
1393
+ if (opts.specSource !== undefined && spec === undefined) {
1394
+ throw new Error(specPath
1395
+ ? `--spec-source was given but the spec file could not be read: ${specPath}`
1396
+ : '--spec-source was given without a spec; pass --spec <path> (or set spec in the config).');
1397
+ }
1398
+ // A resolved spec makes the spec-compliance role useful for plan review
1399
+ // too — the plan gets checked against the higher-level spec.
1400
+ if (requestedRoles === fallbackRoles && requestedRoles && specContent) {
1401
+ requestedRoles = [...requestedRoles, 'spec-compliance'];
1402
+ }
1403
+ // Repository rules remain shared review context without a dedicated seat.
1404
+ const projectRulesFile = await findProjectRulesFile();
1405
+ let historicalPlan;
1406
+ let pendingClaimRoles;
1407
+ if (opts.retryReport) {
1408
+ let source;
1409
+ try {
1410
+ source = originalRunReportSchema.parse(decodeOriginalReport(await readFile(opts.retryReport, 'utf8')).value);
1411
+ }
1412
+ catch {
1413
+ throw new ReviewLaunchRefused('retry_report_plan_invalid', 'The retry report cannot reconstruct a supported historical execution plan.');
1414
+ }
1415
+ const projectRulesContent = projectRulesFile
1416
+ ? await readFile(projectRulesFile, 'utf8').catch(() => undefined)
1417
+ : undefined;
1418
+ if ((source.run.spec?.sha256 ?? undefined) !== (spec?.sha256 ?? undefined)) {
1419
+ throw new ReviewLaunchRefused('retry_report_plan_invalid', 'The retry report is bound to different specification content.');
1420
+ }
1421
+ historicalPlan = historicalExecutionPlan(source.run, config, projectRulesContent, specContent);
1422
+ if (opts.resumePending && historicalPlan) {
1423
+ pendingClaimRoles = legacyPendingClaimRoles(config, specContent);
1424
+ if (!pendingClaimRoles) {
1425
+ throw new ReviewLaunchRefused('retry_report_plan_invalid', 'The pending claim cannot reconstruct its original 4.4.9 dispatch plan.');
1426
+ }
1427
+ }
1428
+ }
1429
+ // Resolve roles
1430
+ const roles = historicalPlan?.roles ?? await resolveRoles(config, requestedRoles, specContent);
1431
+ if (roles.length === 0) {
1432
+ spinner.fail('No roles resolved. Check your --role/--roles flags.');
1433
+ process.exit(1);
1434
+ }
1435
+ // Build role map for voter
1436
+ const roleMap = new Map();
1437
+ for (const role of roles) {
1438
+ roleMap.set(role.name, role);
1439
+ }
1440
+ const models = config.models ?? [...DEFAULT_MODELS];
1441
+ const secondaryModels = config.secondaryModels ?? [];
1442
+ const built = buildAssignments({
1443
+ models,
1444
+ roles,
1445
+ secondaryModels,
1446
+ explicitReviewers,
1447
+ roleMap,
1448
+ deterministic: opts.guardedConverge,
1449
+ });
1450
+ if (opts.guardedConverge && explicitReviewers && built.length !== explicitReviewers.length) {
1451
+ throw new ReviewLaunchRefused('invalid_reviewers', 'Guarded review refuses unknown reviewer roles rather than removing them.');
1452
+ }
1453
+ // Async lane (RCL-25): async models run the general role(s) only.
1454
+ // Membership in `models` wins over `asyncModels` — an explicit blocking
1455
+ // seat is an explicit choice, so the model stays blocking and gets no
1456
+ // duplicate async seat. Async models appearing only in `secondaryModels`
1457
+ // are partitioned OUT of the blocking path below. Explicit --reviewer
1458
+ // pairs mean exact manual control: every pair runs as given — even a
1459
+ // model that is usually async — and no bonus seats are added, so the
1460
+ // async roster must not partition pairs away.
1461
+ const asyncModels = explicitReviewers
1462
+ ? []
1463
+ : (config.asyncModels ?? []).filter((m) => !models.includes(m));
1464
+ const { blocking: assignments } = partitionAsyncAssignments(built, asyncModels);
1465
+ const generalRoles = roles.filter((r) => !r.isSpecialized);
1466
+ const asyncAssignments = explicitReviewers || asyncModels.length === 0 || generalRoles.length === 0
1467
+ ? []
1468
+ : buildAssignments({ models: asyncModels, roles: generalRoles, roleMap, deterministic: opts.guardedConverge });
1469
+ let pendingClaimPlan;
1470
+ if (pendingClaimRoles) {
1471
+ const pendingRoleMap = new Map(pendingClaimRoles.map(role => [role.name, role]));
1472
+ const pendingBuilt = buildAssignments({ models, roles: pendingClaimRoles, secondaryModels,
1473
+ roleMap: pendingRoleMap, deterministic: true });
1474
+ const { blocking: pendingAssignments } = partitionAsyncAssignments(pendingBuilt, asyncModels);
1475
+ const pendingGeneral = pendingClaimRoles.filter(role => !role.isSpecialized);
1476
+ const pendingAsyncAssignments = asyncModels.length === 0 || pendingGeneral.length === 0 ? []
1477
+ : buildAssignments({ models: asyncModels, roles: pendingGeneral, roleMap: pendingRoleMap, deterministic: true });
1478
+ pendingClaimPlan = {
1479
+ assignments: pendingAssignments,
1480
+ asyncAssignments: pendingAsyncAssignments,
1481
+ gatingConfig: resolveGatingConfig(config.gating, [...models, ...secondaryModels, ...(config.asyncModels ?? [])]),
1482
+ contextFiles: [...(opts.context ?? []), ...(config.context ?? []), ...(projectRulesFile ? [projectRulesFile] : [])],
1483
+ };
1484
+ }
1485
+ assertUnambiguousReviewerIdentities([...assignments, ...asyncAssignments].map(({ model, role }) => ({ model, role: role.name })));
1486
+ const contextFiles = [...(opts.context ?? []), ...(config.context ?? [])];
1487
+ if (projectRulesFile && !historicalPlan?.embedsProjectRules && !contextFiles.some(path => resolve(path) === projectRulesFile)) {
1488
+ contextFiles.push(projectRulesFile);
1489
+ }
1490
+ // Resolve gating now: a config error (e.g. an aggregator-routed verifier)
1491
+ // must fail before any model time is spent, and the verifier is chosen
1492
+ // under roster containment — never a provider outside the configured fleet.
1493
+ const gatingConfig = historicalPlan?.gating ?? resolveGatingConfig(config.gating, [
1494
+ ...models,
1495
+ ...secondaryModels,
1496
+ ...(config.asyncModels ?? []),
1497
+ ]);
1498
+ return {
1499
+ config,
1500
+ roleMap,
1501
+ assignments,
1502
+ asyncAssignments,
1503
+ gatingConfig,
1504
+ contextFiles,
1505
+ ...(spec ? { spec, specContent } : {}),
1506
+ ...(historicalPlan ? { historicalRetryPlan: true } : {}),
1507
+ ...(pendingClaimPlan ? { pendingClaimPlan } : {}),
1508
+ explicit: explicitReviewers !== undefined,
1509
+ coreModels: models,
1510
+ ...(converge ? { converge } : {}),
1511
+ startedAt,
1512
+ };
1513
+ }
1514
+ function reviewConvergeEnvironment(opts) {
1515
+ return opts.startOver || opts.cycleReview
1516
+ ? { ...process.env, RCL_CONVERGE_ROUND: undefined, RCL_CONVERGE_ATTEMPT: undefined }
1517
+ : process.env;
1518
+ }
1519
+ async function discoverCycleReview(target, opts) {
1520
+ const prRef = target && isGitHubTarget(target) ? target : opts.forPr ?? process.env['RCL_FOR_PR'];
1521
+ if (opts.startOver && (!prRef || opts.staged || opts.workingTree))
1522
+ throw new Error('--start-over needs a PR or a captured patch with --for-pr and --head-sha');
1523
+ if (!prRef)
1524
+ return opts;
1525
+ const pr = parseGitHubTarget(prRef);
1526
+ const targetKey = opts.convergeTarget?.trim() || process.env['RCL_CONVERGE_TARGET']?.trim() || `${pr.repo.toLowerCase()}-${pr.number}`;
1527
+ let common;
1528
+ try {
1529
+ common = await resolveGitCommonDir();
1530
+ }
1531
+ catch (error) {
1532
+ if (opts.startOver)
1533
+ throw error;
1534
+ return opts;
1535
+ }
1536
+ if (!opts.startOver)
1537
+ await assertNoPendingFreshReview(common, targetKey);
1538
+ const active = opts.startOver ? undefined : await loadConvergeRunState(common, targetKey);
1539
+ if (!opts.startOver && !active?.cycle)
1540
+ return opts;
1541
+ if (opts.attempt !== undefined || (opts.startOver && opts.round !== undefined) || opts.attest)
1542
+ throw new Error('A fresh review assigns its own ordinals and needs an ordinary actor credential');
1543
+ if (opts.telemetry === false)
1544
+ throw new Error('Fresh review cycles require Harness evidence');
1545
+ const paths = opts.jsonFile ? {} : await freshReviewOutputPaths(common);
1546
+ return { ...opts, ...paths, ...(opts.markdown ? { markdown: opts.markdown } : {}),
1547
+ convergeTarget: targetKey, guardedConverge: true, cycleReview: true, evidenceRequired: true };
1548
+ }
1549
+ async function runReview(target, opts) {
1550
+ const spinner = ora('Loading configuration...').start();
1551
+ try {
1552
+ if (opts.exportPendingPackage !== undefined && (!opts.exportPendingPackage.trim() || !opts.guardedConverge ||
1553
+ !opts.expectBaseSha || opts.startOver || opts.boundFixRecovery || opts.attest || opts.resumePending ||
1554
+ opts.finalizePendingOnly || opts.ordinaryPendingPackage || opts.retryReport || opts.retryReason ||
1555
+ opts.resumeAsyncSha256 || opts.pendingNativeSha256 || opts.pendingAttemptSha256 || opts.launchIntent ||
1556
+ opts.maxAttempts !== undefined || opts.maxRounds !== undefined || opts.attempt !== undefined ||
1557
+ opts.post || opts.json || opts.jsonFile !== undefined || opts.markdown !== undefined || opts.ci ||
1558
+ opts.evidenceRequired || opts.staged || opts.workingTree || !target)) {
1559
+ throw new ReviewLaunchRefused('pending_export_incompatible', '--export-pending-package requires an ordinary guarded PR or patch target, original preparation flags and --expect-base-sha; do not combine it with launch, recovery, output or evidence flags.');
1560
+ }
1561
+ if (opts.expectBaseSha !== undefined && !opts.exportPendingPackage) {
1562
+ throw new ReviewLaunchRefused('pending_export_incompatible', '--expect-base-sha requires --export-pending-package.');
1563
+ }
1564
+ if (opts.expectBaseSha)
1565
+ validateSha(opts.expectBaseSha, '--expect-base-sha');
1566
+ if (opts.boundFixRecovery !== undefined) {
1567
+ if (!opts.guardedConverge || !opts.evidenceRequired || opts.telemetry === false ||
1568
+ opts.startOver || opts.attest || opts.retryReport !== undefined || opts.retryReason !== undefined ||
1569
+ (opts.launchIntent !== undefined && opts.launchIntent !== 'review') ||
1570
+ opts.staged || opts.workingTree || !(opts.forPr || (target && isGitHubTarget(target)))) {
1571
+ throw new ReviewLaunchRefused('bound_fix_recovery_incompatible', '--bound-fix-recovery requires --guarded-converge, --evidence-required and a PR target or explicit --for-pr; do not combine it with start-over, attestation or another retry mode.');
1572
+ }
1573
+ if (!/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/.test(opts.boundFixRecovery)) {
1574
+ throw new ReviewLaunchRefused('bound_fix_recovery_invalid', '--bound-fix-recovery must name an exact native run UUID.');
1575
+ }
1576
+ }
1577
+ if (opts.resumePending && (!opts.guardedConverge || opts.finalizePendingOnly || (!opts.retryReport && !opts.ordinaryPendingPackage) ||
1578
+ (opts.retryReport && opts.ordinaryPendingPackage) || !opts.retryReason ||
1579
+ !opts.resumeAsyncSha256 || !opts.evidenceRequired || opts.telemetry === false ||
1580
+ opts.maxAttempts === undefined || opts.startOver || opts.boundFixRecovery || opts.attest)) {
1581
+ throw new ReviewLaunchRefused('pending_resume_incompatible', '--resume-pending requires guarded convergence, evidence delivery, exactly one legacy retry report or ordinary package, retained retry reason, an explicit attempt cap and retained async result digests.');
1582
+ }
1583
+ const finalizeOnlyIgnoredReviewFlags = opts.retryReason !== undefined || opts.maxRounds !== undefined ||
1584
+ opts.round !== undefined || opts.attempt !== undefined || opts.post || opts.json ||
1585
+ opts.jsonFile !== undefined || opts.markdown !== undefined || opts.ci || opts.evidenceRequired ||
1586
+ opts.forPr !== undefined || opts.role !== undefined || opts.roles !== undefined ||
1587
+ opts.models !== undefined || opts.secondaryModels !== undefined || opts.asyncModels !== undefined ||
1588
+ (opts.reviewer?.length ?? 0) > 0 || (opts.context?.length ?? 0) > 0 ||
1589
+ opts.spec !== undefined || opts.focus !== undefined;
1590
+ const finalizeOnlySourceFlags = opts.staged || opts.workingTree || opts.specSource !== undefined;
1591
+ if (opts.finalizePendingOnly && (!opts.guardedConverge || opts.resumePending || !opts.ordinaryPendingPackage ||
1592
+ opts.retryReport !== undefined || !opts.resumeAsyncSha256 || opts.maxAttempts === undefined ||
1593
+ (!opts.previewPending && (!opts.pendingNativeSha256 || !opts.pendingAttemptSha256)) ||
1594
+ (opts.previewPending && (opts.pendingNativeSha256 !== undefined || opts.pendingAttemptSha256 !== undefined)) ||
1595
+ opts.startOver || opts.boundFixRecovery || opts.attest || opts.launchIntent !== undefined ||
1596
+ finalizeOnlyIgnoredReviewFlags || finalizeOnlySourceFlags)) {
1597
+ throw new ReviewLaunchRefused('pending_finalize_incompatible', '--finalize-pending-only accepts only guarded target/head/base/config bindings, one ordinary pending package, an explicit unchanged attempt cap, retained async result digests and, for apply, both exact state digests from preview; do not combine it with review, output, evidence or retry flags.');
1598
+ }
1599
+ if (!opts.finalizePendingOnly &&
1600
+ (opts.pendingNativeSha256 !== undefined || opts.pendingAttemptSha256 !== undefined)) {
1601
+ throw new ReviewLaunchRefused('pending_finalize_incompatible', 'Pending native and attempt state digests apply only to --finalize-pending-only.');
1602
+ }
1603
+ if (!opts.resumePending && !opts.finalizePendingOnly &&
1604
+ (opts.resumeAsyncSha256 !== undefined || opts.ordinaryPendingPackage !== undefined)) {
1605
+ throw new ReviewLaunchRefused('pending_resume_incompatible', '--resume-async-sha256 and --ordinary-pending-package require --resume-pending or --finalize-pending-only.');
1606
+ }
1607
+ if (opts.previewPending && !opts.exportPendingPackage && ((!opts.resumePending && !opts.finalizePendingOnly) ||
1608
+ !opts.ordinaryPendingPackage || opts.retryReport)) {
1609
+ throw new ReviewLaunchRefused('pending_preview_incompatible', '--preview-pending requires one pending recovery mode and --ordinary-pending-package, without --retry-report.');
1610
+ }
1611
+ if (!opts.exportPendingPackage)
1612
+ opts = { ...opts, ...await discoverCycleReview(target, opts) };
1613
+ // Finalize-only is a recovery-state operation even though it is exposed by
1614
+ // `review`; preview and apply must not flush unrelated review evidence.
1615
+ if (!opts.exportPendingPackage && !opts.finalizePendingOnly && !opts.cycleReview && opts.telemetry !== false &&
1616
+ (process.env['RCL_TELEMETRY'] ?? '').trim().toLowerCase() !== 'off') {
1617
+ try {
1618
+ if ((await readdir(join(resolveDataDir(), 'outbox')).catch(() => [])).length > 0) {
1619
+ await flushOutboxAtStart(await createTelemetryRuntime({ rclVersion: RCL_VERSION }));
1620
+ }
1621
+ }
1622
+ catch { /* Startup delivery remains best-effort for legacy reviews. */ }
1623
+ }
1624
+ if (!opts.guardedConverge && (opts.launchIntent !== undefined || opts.retryReason !== undefined || opts.retryReport !== undefined ||
1625
+ opts.maxAttempts !== undefined || opts.maxRounds !== undefined)) {
1626
+ throw new ReviewLaunchRefused('guard_required', 'Launch intent, retry reason and launch caps require --guarded-converge.');
1627
+ }
1628
+ if (opts.guardedConverge) {
1629
+ const converge = resolveConvergeContext(opts, reviewConvergeEnvironment(opts));
1630
+ if (!converge)
1631
+ throw new ReviewLaunchRefused('target_required', 'A guarded launch requires --converge-target.');
1632
+ if (converge.attempt !== undefined || opts.attest) {
1633
+ throw new ReviewLaunchRefused('incompatible_launch', 'Guarded review claims its own attempt; do not preclaim, pass --attempt, or combine it with --attest.');
1634
+ }
1635
+ }
1636
+ // Exactly one review source: a positional target, --staged, or --working-tree
1637
+ const sourceCount = [target, opts.staged, opts.workingTree].filter(Boolean).length;
1638
+ if (sourceCount === 0) {
1639
+ spinner.fail('Missing review target. Provide owner/repo#N, a patch file, --staged, or --working-tree.');
1640
+ process.exit(1);
1641
+ }
1642
+ if (sourceCount > 1) {
1643
+ spinner.fail('A positional target, --staged, and --working-tree are mutually exclusive');
1644
+ process.exit(1);
1645
+ }
1646
+ const gitMode = opts.staged ? 'staged' : opts.workingTree ? 'working-tree' : undefined;
1647
+ // Classify the positional target ONCE by shape: a GitHub PR reference is
1648
+ // a PR, anything else is a local patch file (whatever its extension or
1649
+ // path form). Both the flag gate below and the diff resolution use this
1650
+ // single decision, so they cannot diverge.
1651
+ const patchTarget = !gitMode && target !== undefined && !isGitHubTarget(target);
1652
+ // Validate the head-binding flags BEFORE any config, key, network, or
1653
+ // git work: a bad flag must fail before anything is spent or fetched.
1654
+ if ((opts.headSha !== undefined || opts.baseSha !== undefined) && !patchTarget) {
1655
+ throw new Error(`--head-sha and --base-sha apply to patch files only; ${gitMode ? `--${gitMode} resolves HEAD itself` : 'a PR target resolves its heads from GitHub'}.`);
1656
+ }
1657
+ if (opts.headSha !== undefined)
1658
+ validateSha(opts.headSha, '--head-sha');
1659
+ if (opts.baseSha !== undefined)
1660
+ validateSha(opts.baseSha, '--base-sha');
1661
+ if (opts.expectHeadSha !== undefined)
1662
+ validateSha(opts.expectHeadSha, '--expect-head-sha');
1663
+ if (opts.evidenceRequired && patchTarget && opts.headSha === undefined) {
1664
+ throw new Error('--evidence-required needs --head-sha for a patch file: evidence must bind to the commit it reviewed.');
1665
+ }
1666
+ // --attest (RCL-40) runs before any key, config or reviewer work; an
1667
+ // attested review is recorded or it does not run, so evidence is required.
1668
+ let attestation;
1669
+ if (opts.attest) {
1670
+ attestation = await attestBeforeReview(spinner, opts, target, gitMode);
1671
+ opts = { ...opts, evidenceRequired: true };
1672
+ }
1673
+ assertEvidenceCanBeRequired(opts);
1674
+ const prepared = await prepareCouncil(spinner, opts, undefined, attestation);
1675
+ const { config } = prepared;
1676
+ if (attestation) {
1677
+ // The loaded config is the authority; the pre-exchange check read the same file.
1678
+ const level = resolveTelemetryLevel(config, { noTelemetry: opts.telemetry === false }, process.env);
1679
+ if (level !== 'full')
1680
+ throw new Error(attestLevelMessage(level));
1681
+ }
1682
+ assertEvidenceCanBeRequired(opts, config);
1683
+ await assertEvidenceDeliverable(opts, config, attestation?.credential);
1684
+ spinner.text = `Resolving diff for: ${target ?? `--${gitMode}`}`;
1685
+ // Resolve diff. Git modes bracket the read with two HEAD resolutions: a
1686
+ // commit landing between them would bind the diff to a commit it was
1687
+ // not taken against, and --expect-head-sha would then vouch for it.
1688
+ let diff;
1689
+ let gitHeads;
1690
+ if (gitMode) {
1691
+ gitHeads = await resolveGitHeads();
1692
+ diff = await loadGitDiff(gitMode);
1693
+ // Both ends of the binding must hold still: HEAD (what the diff is
1694
+ // relative to) and the merge-base (what the header records as base).
1695
+ // Index or worktree edits during the read are not guarded — the
1696
+ // diff_sha256 describes exactly the bytes that were read.
1697
+ const after = await resolveGitHeads();
1698
+ if (after.headSha !== gitHeads.headSha || after.baseSha !== gitHeads.baseSha) {
1699
+ throw new Error(`HEAD or its merge-base moved (${gitHeads.headSha ?? 'unknown'}/${gitHeads.baseSha ?? 'unknown'} → ${after.headSha ?? 'unknown'}/${after.baseSha ?? 'unknown'}) while the diff was being read — refusing to review; rerun once the tree is quiet.`);
1700
+ }
1701
+ }
1702
+ else if (patchTarget) {
1703
+ try {
1704
+ diff = await loadLocalDiff(target);
1705
+ }
1706
+ catch (err) {
1707
+ // A mistyped PR reference lands here too; say what a target can be
1708
+ // instead of a bare ENOENT.
1709
+ if (err.code === 'ENOENT') {
1710
+ throw new Error(`No such patch file: ${target}. A review target is a local patch file, owner/repo#N, or a GitHub PR URL.`);
1711
+ }
1712
+ throw err;
1713
+ }
1714
+ }
1715
+ else {
1716
+ const prTarget = parseGitHubTarget(target);
1717
+ diff = await fetchPRDiff(prTarget, config.githubToken);
1718
+ }
1719
+ // Resolve the head BEFORE the empty-diff exit so --expect-head-sha is
1720
+ // honored even when there is nothing to review: a moved target must
1721
+ // never read as a clean round.
1722
+ const runTarget = await resolveReviewTarget(diff, gitMode, {
1723
+ ...opts,
1724
+ convergeTarget: opts.convergeTarget ?? process.env['RCL_CONVERGE_TARGET'],
1725
+ // The flag is the user's word and is refused off a patch file; a value
1726
+ // left in the environment only ever attributes a patch file.
1727
+ ...(opts.forPr !== undefined
1728
+ ? { forPr: opts.forPr }
1729
+ : !diff.metadata && !gitMode && (process.env['RCL_FOR_PR'] ?? '').trim() !== ''
1730
+ ? { forPr: process.env['RCL_FOR_PR'] }
1731
+ : {}),
1732
+ }, { gitHeads });
1733
+ if (opts.expectHeadSha !== undefined) {
1734
+ assertExpectedHead(runTarget, opts.expectHeadSha);
1735
+ }
1736
+ if (diff.files.length === 0 && opts.exportPendingPackage) {
1737
+ throw new ReviewLaunchRefused('pending_export_input_mismatch', 'The pending launch cannot be authenticated with an empty diff.');
1738
+ }
1739
+ if (diff.files.length === 0) {
1740
+ spinner.warn(gitMode === 'staged'
1741
+ ? 'No staged changes to review.'
1742
+ : gitMode === 'working-tree'
1743
+ ? 'No uncommitted changes to review.'
1744
+ : 'No files found in diff. Nothing to review.');
1745
+ process.exit(0);
1746
+ }
1747
+ // PR and Git-mode labels identify the review target directly. Patch
1748
+ // paths identify the capture; executeCouncil scopes converging patches
1749
+ // by their native convergence target instead.
1750
+ const asyncTargetLabel = diff.metadata
1751
+ ? `${diff.metadata.owner}/${diff.metadata.repo}#${diff.metadata.number}`
1752
+ : (target ?? `git-${gitMode}-${await currentBranchLabel()}`);
1753
+ await executeCouncil(spinner, prepared, diff, opts, {
1754
+ command: 'review',
1755
+ target: runTarget,
1756
+ asyncTargetLabel,
1757
+ ...(attestation ? { attestation } : {}),
1758
+ });
1759
+ }
1760
+ catch (err) {
1761
+ spinner.fail(String(err));
1762
+ if (process.env['RCL_DEBUG']) {
1763
+ console.error(err);
1764
+ }
1765
+ process.exitCode = opts.guardedConverge && (err instanceof ConvergeAttemptBudgetExceededError || err instanceof ConvergeRoundCapError)
1766
+ ? 2 : opts.guardedConverge && (err instanceof ConvergeAttemptStateError || err instanceof ConvergeRunStateError) ? 3 : 1;
1767
+ }
1768
+ }
1769
+ /**
1770
+ * Shared back half of every council command: chunking, prompt building,
1771
+ * dispatch, consensus, and every output surface.
1772
+ */
1773
+ async function prepareCouncilWork(spinner, prepared, diff, opts, focus) {
1774
+ const { assignments, contextFiles } = prepared;
1775
+ const chunks = chunkDiff(diff.files);
1776
+ assertReviewWorkWithinLimit(chunks.length, assignments.length);
1777
+ spinner.text = `Building prompts (${chunks.length} chunk(s), ${assignments.length} reviewer(s))...`;
1778
+ const chunkAssignments = chunks.flatMap(chunk => assignments.map(assignment => ({ assignment, chunk })));
1779
+ const { docs: contextDocs, skipped } = await loadPromptContextDocs(contextFiles);
1780
+ if (opts.guardedConverge && skipped.length > 0) {
1781
+ throw new ReviewLaunchRefused('unreadable_context', `Required context could not be read: ${skipped.join(', ')}`);
1782
+ }
1783
+ for (const path of skipped)
1784
+ console.warn(`Context file not readable, not included in the review: ${path}`);
1785
+ const prompts = await Promise.all(chunkAssignments.map(({ assignment, chunk }) => buildPrompt(chunk, assignment.role, {
1786
+ contextDocs,
1787
+ plan: focus === undefined ? undefined : { focus },
1788
+ })));
1789
+ return { chunks, chunkAssignments, contextDocs, prompts };
1790
+ }
1791
+ async function executeCouncil(spinner, prepared, diff, opts, extra, preparedWork) {
1792
+ const { config, roleMap, assignments, asyncAssignments } = prepared;
1793
+ const planContext = extra.focus !== undefined ? { focus: extra.focus } : undefined;
1794
+ const work = preparedWork ?? await prepareCouncilWork(spinner, prepared, diff, opts, extra.focus);
1795
+ const { chunks, chunkAssignments, contextDocs, prompts } = work;
1796
+ if (opts.guardedConverge) {
1797
+ const roster = buildRoster({ assignments, asyncAssignments, coreModels: prepared.coreModels,
1798
+ explicit: prepared.explicit, gating: prepared.gatingConfig });
1799
+ const guardedInput = {
1800
+ head: extra.target.headSha, kind: extra.target.kind, repo: extra.target.repo, pr: extra.target.prNumber,
1801
+ diff: diffDigest(diff.files), config: configDigest(config), roster, prompts,
1802
+ asyncRoles: asyncAssignments.map(assignment => assignment.role), spec: prepared.spec,
1803
+ };
1804
+ const ordinaryAsyncDescriptors = chunks.flatMap(() => asyncAssignments.map(assignment => ({
1805
+ model: assignment.model, role: assignment.role.name, provider: assignment.provider,
1806
+ }))).slice(0, MAX_ASYNC_CALLS_PER_ROUND);
1807
+ let completion;
1808
+ const common = await resolveGitCommonDir();
1809
+ if (opts.exportPendingPackage) {
1810
+ const receipt = await exportOrdinaryPendingPackage({ gitCommonDir: common,
1811
+ target: prepared.converge.target, headSha: extra.target.headSha ?? '', baseSha: extra.target.baseSha ?? '',
1812
+ expectedBaseSha: opts.expectBaseSha, expectedRound: prepared.converge.round,
1813
+ guardedInput, path: opts.exportPendingPackage,
1814
+ preview: opts.previewPending === true, asyncStoreDir: await resolveExistingAsyncStoreDir(),
1815
+ asyncTargetKey: asyncTargetKey(extra.asyncTargetLabel ?? prepared.converge.target, extra.target.kind === 'patch' ? prepared.converge.target : undefined),
1816
+ asyncDescriptors: ordinaryAsyncDescriptors });
1817
+ spinner.stop();
1818
+ console.log(JSON.stringify(receipt));
1819
+ return undefined;
1820
+ }
1821
+ const native = opts.startOver ? undefined : await loadConvergeRunState(common, prepared.converge.target);
1822
+ let cycleRemote;
1823
+ let boundFixRecovery;
1824
+ if (opts.boundFixRecovery !== undefined) {
1825
+ if (!extra.target.repo || !extra.target.prNumber) {
1826
+ throw new ReviewLaunchRefused('bound_fix_recovery_requires_pr', 'Bound fix recovery requires evidence bound to a pull request.');
1827
+ }
1828
+ const opened = await openReadSink({ rclVersion: RCL_VERSION });
1829
+ if (!opened.sink)
1830
+ throw new ReviewLaunchRefused('bound_fix_recovery_unanswered', 'Cannot read live Harness recovery evidence without an authenticated evidence credential.');
1831
+ boundFixRecovery = createBoundFixRecovery(opened.sink, extra.target.repo, extra.target.prNumber, opts.boundFixRecovery);
1832
+ }
1833
+ if (opts.startOver || native?.cycle) {
1834
+ if (!extra.target.repo || !extra.target.prNumber)
1835
+ throw new Error('fresh_review_requires_pr');
1836
+ const runtime = await createTelemetryRuntime({ rclVersion: RCL_VERSION, config, noTelemetry: opts.telemetry === false });
1837
+ if (!runtime.sink || runtime.level !== 'full')
1838
+ throw new Error('Fresh review cycles require full Harness evidence and an actor credential');
1839
+ cycleRemote = createReviewCycleRemote(runtime.sink, extra.target.repo, extra.target.prNumber, extra.target.headSha ?? '');
1840
+ }
1841
+ if (opts.resumePending || opts.finalizePendingOnly) {
1842
+ const expectedAsyncSha256 = opts.resumeAsyncSha256.split(',').map(value => value.trim());
1843
+ if (expectedAsyncSha256.length === 0 || expectedAsyncSha256.some(value => !/^[a-f0-9]{64}$/.test(value)) ||
1844
+ new Set(expectedAsyncSha256).size !== expectedAsyncSha256.length) {
1845
+ throw new ReviewLaunchRefused('pending_async_binding_invalid', 'Retained async SHA-256 bindings must be unique lowercase 64-character digests.');
1846
+ }
1847
+ const migrationPackage = opts.ordinaryPendingPackage === undefined ? undefined : JSON.parse((await readStable(opts.ordinaryPendingPackage)).text);
1848
+ const loadMigrationRetainedAsync = async () => {
1849
+ if (!migrationPackage)
1850
+ throw new ReviewLaunchRefused('pending_async_missing', 'An ordinary package is required.');
1851
+ const store = await resolveExistingAsyncStoreDir();
1852
+ const wanted = new Map(migrationPackage.retainedAsync.map(item => [item.sha256, item]));
1853
+ const found = new Map();
1854
+ for (const name of await readdir(store)) {
1855
+ const path = join(store, name);
1856
+ const bytes = await readStable(path).catch(() => undefined);
1857
+ if (!bytes || !wanted.has(bytes.sha256))
1858
+ continue;
1859
+ let row;
1860
+ try {
1861
+ row = JSON.parse(bytes.text);
1862
+ }
1863
+ catch {
1864
+ throw new ReviewLaunchRefused('pending_async_corrupt', 'A package-bound retained async artifact is not valid JSON.');
1865
+ }
1866
+ const descriptor = wanted.get(bytes.sha256);
1867
+ if (row.model !== descriptor.model || row.role !== descriptor.role || row.provider !== descriptor.provider) {
1868
+ throw new ReviewLaunchRefused('pending_async_identity_mismatch', 'A package-bound retained async artifact has a different reviewer identity.');
1869
+ }
1870
+ if (found.has(bytes.sha256))
1871
+ throw new ReviewLaunchRefused('pending_async_duplicate', 'Multiple retained async artifacts match one authenticated digest.');
1872
+ found.set(bytes.sha256, { path, sha256: bytes.sha256, bytesBase64: bytes.raw.toString('base64') });
1873
+ }
1874
+ if (found.size !== wanted.size)
1875
+ throw new ReviewLaunchRefused('pending_async_missing', 'A package-bound retained async artifact is unavailable.');
1876
+ return [...found.values()].sort((a, b) => a.sha256.localeCompare(b.sha256));
1877
+ };
1878
+ if (opts.previewPending && migrationPackage) {
1879
+ const inputSha256 = guardedInputSha256(migrationPackage.guardedInput);
1880
+ const preview = await previewOrdinaryPendingLaunch({ gitCommonDir: common,
1881
+ target: prepared.converge.target, headSha: extra.target.headSha ?? '', baseSha: extra.target.baseSha ?? '',
1882
+ pendingInputSha256: inputSha256, recoveryInputSha256: inputSha256,
1883
+ retryReason: opts.retryReason ?? 'Finalize-only authentication preview.',
1884
+ captured: {}, retainedAsyncSha256: expectedAsyncSha256,
1885
+ migrationPackage, maxAttempts: Number(opts.maxAttempts), maxPhysicalCalls: 1, maxAttemptsPerCell: 1,
1886
+ maxDurationMs: 1, validate: async () => { }, loadRetainedAsync: loadMigrationRetainedAsync,
1887
+ run: async () => { throw new Error('preview_must_not_run'); }, preview: true,
1888
+ previewMode: opts.finalizePendingOnly ? 'finalize-only' : 'combined' });
1889
+ spinner.stop();
1890
+ console.log(JSON.stringify({
1891
+ mode: opts.finalizePendingOnly ? 'finalize-only-preview' : 'preview', ...preview,
1892
+ }));
1893
+ return undefined;
1894
+ }
1895
+ if (opts.finalizePendingOnly && migrationPackage) {
1896
+ const inputSha256 = guardedInputSha256(migrationPackage.guardedInput);
1897
+ const result = await finalizeOrdinaryPendingLaunch({ gitCommonDir: common,
1898
+ target: prepared.converge.target, headSha: extra.target.headSha ?? '',
1899
+ baseSha: extra.target.baseSha ?? '', pendingInputSha256: inputSha256,
1900
+ nativeStateSha256: opts.pendingNativeSha256, attemptStateSha256: opts.pendingAttemptSha256,
1901
+ retainedAsyncSha256: expectedAsyncSha256, migrationPackage,
1902
+ maxAttempts: Number(opts.maxAttempts), loadRetainedAsync: loadMigrationRetainedAsync });
1903
+ spinner.stop();
1904
+ console.log(JSON.stringify({ mode: 'finalize-only', ...result }));
1905
+ return undefined;
1906
+ }
1907
+ // An ordinary migration package authenticates the old launch itself. The
1908
+ // current prepared plan is used only for its fresh successor; it must not
1909
+ // be substituted for the interrupted launch's input.
1910
+ const pendingClaimPlan = migrationPackage ? {
1911
+ assignments, asyncAssignments, gatingConfig: prepared.gatingConfig,
1912
+ contextFiles: prepared.contextFiles,
1913
+ } : prepared.pendingClaimPlan;
1914
+ if (!pendingClaimPlan) {
1915
+ throw new ReviewLaunchRefused('pending_claim_plan_missing', 'The pending launch has no supported original planner reconstruction.');
1916
+ }
1917
+ const pendingClaimRoster = buildRoster({ assignments: pendingClaimPlan.assignments,
1918
+ asyncAssignments: pendingClaimPlan.asyncAssignments, coreModels: prepared.coreModels,
1919
+ explicit: false, gating: pendingClaimPlan.gatingConfig });
1920
+ const pendingClaimChunks = chunks.flatMap(chunk => pendingClaimPlan.assignments.map(assignment => ({ assignment, chunk })));
1921
+ const { docs: pendingClaimContext, skipped: pendingClaimSkipped } = await loadPromptContextDocs([...new Set(pendingClaimPlan.contextFiles)]);
1922
+ if (pendingClaimSkipped.length > 0) {
1923
+ throw new ReviewLaunchRefused('unreadable_context', `Pending claim context could not be read: ${pendingClaimSkipped.join(', ')}`);
1924
+ }
1925
+ const pendingClaimPrompts = await Promise.all(pendingClaimChunks.map(({ assignment, chunk }) => buildPrompt(chunk, assignment.role, { contextDocs: pendingClaimContext, plan: planContext })));
1926
+ const pendingClaimAsyncChunks = chunks.flatMap(chunk => pendingClaimPlan.asyncAssignments.map(assignment => ({ assignment, chunk })))
1927
+ .slice(0, MAX_ASYNC_CALLS_PER_ROUND);
1928
+ const pendingClaimAsyncPrompts = await Promise.all(pendingClaimAsyncChunks.map(({ assignment, chunk }) => buildPrompt(chunk, assignment.role, { contextDocs: pendingClaimContext, plan: planContext })));
1929
+ const claimedInputSha256 = migrationPackage
1930
+ ? guardedInputSha256(migrationPackage.guardedInput)
1931
+ : guardedInputSha256({
1932
+ head: extra.target.headSha, kind: extra.target.kind, repo: extra.target.repo, pr: extra.target.prNumber,
1933
+ diff: diffDigest(diff.files), config: configDigest(config), roster: pendingClaimRoster,
1934
+ prompts: pendingClaimPrompts,
1935
+ asyncRoles: pendingClaimPlan.asyncAssignments.map(assignment => assignment.role), spec: prepared.spec,
1936
+ });
1937
+ if (process.env['RCL_DEBUG'])
1938
+ process.stderr.write(`Pending claim input: ${claimedInputSha256}\n`);
1939
+ const thresholds = {
1940
+ minConsensusScore: config.thresholds?.minConsensusScore ?? DEFAULT_THRESHOLDS.minConsensusScore,
1941
+ minConfidence: config.thresholds?.minConfidence ?? DEFAULT_THRESHOLDS.minConfidence,
1942
+ dedupeLineWindow: config.thresholds?.dedupeLineWindow ?? DEFAULT_THRESHOLDS.dedupeLineWindow,
1943
+ jaccardThreshold: config.thresholds?.jaccardThreshold ?? DEFAULT_THRESHOLDS.jaccardThreshold,
1944
+ };
1945
+ const aggregationInputs = captureAggregationInputs({ algorithm: AGGREGATION_ALGORITHM,
1946
+ diffSha256: diffDigest(diff.files), roleMap, thresholds, gating: prepared.gatingConfig,
1947
+ belowThresholdAppendix: config.output?.belowThresholdAppendix ?? true });
1948
+ const captured = capturePreparedCouncil({ target: prepared.converge.target,
1949
+ headSha: extra.target.headSha ?? '', mergeBaseSha: extra.target.baseSha ?? '', diff,
1950
+ assignments,
1951
+ lanes: assignments.map(item => assignmentLane(item.model, prepared.coreModels, prepared.explicit)),
1952
+ chunks, prompts, config, specBytes: prepared.specContent ?? '', contextDocs,
1953
+ compatibility: { parser: { name: 'findings-json', version: 1 }, aggregation: AGGREGATION_ALGORITHM },
1954
+ aggregationInputs,
1955
+ ...(pendingClaimAsyncChunks.length ? { async: {
1956
+ assignments: pendingClaimPlan.asyncAssignments, prompts: pendingClaimAsyncPrompts,
1957
+ timeoutMs: config.asyncTimeout ?? DEFAULT_ASYNC_TIMEOUT_MS,
1958
+ maxAttemptsPerCall: (config.maxRetries ?? DEFAULT_MAX_RETRIES) + 1,
1959
+ maxPhysicalCalls: pendingClaimAsyncChunks.length * ((config.maxRetries ?? DEFAULT_MAX_RETRIES) + 1),
1960
+ } } : {}), });
1961
+ const asyncStoreDir = await resolveAsyncStoreDir();
1962
+ const asyncKey = asyncTargetKey(extra.asyncTargetLabel ?? prepared.converge.target, extra.target.kind === 'patch' ? prepared.converge.target : undefined, prepared.converge.cycleId);
1963
+ const recoveryInputSha256 = sha256Hex(stableStringify({
1964
+ head: extra.target.headSha, kind: extra.target.kind, repo: extra.target.repo,
1965
+ pr: extra.target.prNumber, diff: diffDigest(diff.files), config: configDigest(config),
1966
+ roster, prompts, asyncRoles: asyncAssignments.map(assignment => assignment.role), spec: prepared.spec,
1967
+ }));
1968
+ const pendingResumeOptions = { gitCommonDir: common,
1969
+ target: prepared.converge.target, headSha: extra.target.headSha ?? '',
1970
+ baseSha: extra.target.baseSha ?? '',
1971
+ pendingInputSha256: claimedInputSha256, recoveryInputSha256,
1972
+ retryReason: opts.retryReason,
1973
+ ...(opts.retryReport ? { legacyRetry: { reportPath: opts.retryReport, config, roster,
1974
+ historicalPlan: prepared.historicalRetryPlan === true } } : {}),
1975
+ ...(migrationPackage ? { migrationPackage } : {}), captured,
1976
+ retainedAsyncSha256: expectedAsyncSha256,
1977
+ maxAttempts: Number(opts.maxAttempts),
1978
+ maxPhysicalCalls: captured.plan.cells.length * ((config.maxRetries ?? DEFAULT_MAX_RETRIES) + 1),
1979
+ maxAttemptsPerCell: (config.maxRetries ?? DEFAULT_MAX_RETRIES) + 1,
1980
+ maxDurationMs: Math.min(2_147_483_647, (config.timeout ?? DEFAULT_TIMEOUT_MS) * Math.max(1, Math.ceil(captured.plan.cells.length / (config.concurrency ?? DEFAULT_CONCURRENCY))) *
1981
+ ((config.maxRetries ?? DEFAULT_MAX_RETRIES) + 1) +
1982
+ prepared.gatingConfig.verificationTimeoutMs),
1983
+ validate: async () => { validateLaunchProviders(roster.map(entry => entry.provider)); await validateLaunchOutputs(opts); },
1984
+ loadRetainedAsync: migrationPackage ? loadMigrationRetainedAsync : async () => {
1985
+ const snapshot = await snapshotAsyncResults(asyncStoreDir, asyncKey, pendingClaimPlan.assignments.map(assignment => ({
1986
+ model: assignment.model, role: assignment.role.name,
1987
+ })), expectedAsyncSha256);
1988
+ if (snapshot.reviews.length !== pendingClaimAsyncChunks.length) {
1989
+ throw new ReviewLaunchRefused('pending_async_incomplete', `Expected ${pendingClaimAsyncChunks.length} retained async results, found ${snapshot.reviews.length}.`);
1990
+ }
1991
+ const identity = (value) => `${value.model}\u0000${value.role}\u0000${value.provider ?? ''}`;
1992
+ const expected = pendingClaimAsyncChunks.map(({ assignment }) => identity({
1993
+ model: assignment.model, role: assignment.role.name, provider: assignment.provider,
1994
+ })).sort();
1995
+ const actual = snapshot.reviews.map(review => identity(review)).sort();
1996
+ if (expected.some((value, index) => value !== actual[index])) {
1997
+ throw new ReviewLaunchRefused('pending_async_identity_mismatch', 'Retained async results do not match the interrupted launch plan.');
1998
+ }
1999
+ return snapshot.artifacts;
2000
+ },
2001
+ run: async (execution) => {
2002
+ const completion = await executeCouncil(spinner, { ...prepared, converge: execution.context }, diff, { ...opts, guardedConverge: false, exclusiveOutputs: true }, { ...extra, pendingResume: execution }, work);
2003
+ if (!completion)
2004
+ throw new Error('pending_resume_missing_completion');
2005
+ return completion;
2006
+ }, };
2007
+ if (opts.previewPending) {
2008
+ const preview = await resumePendingLegacyLaunch({ ...pendingResumeOptions, preview: true });
2009
+ spinner.stop();
2010
+ console.log(JSON.stringify({ mode: 'preview', ...preview }));
2011
+ return undefined;
2012
+ }
2013
+ const result = await resumePendingLegacyLaunch({ ...pendingResumeOptions, preview: false });
2014
+ try {
2015
+ if (!migrationPackage)
2016
+ await consumeBoundAsyncResults(asyncStoreDir, asyncKey, expectedAsyncSha256, { allowAlreadyConsumed: result.reusedCompletion });
2017
+ }
2018
+ catch (error) {
2019
+ console.warn(`Retained async results could not be cleaned after terminal recovery: ${String(error)}`);
2020
+ }
2021
+ if (result.reusedCompletion) {
2022
+ process.exitCode = result.completion.exitCode ??
2023
+ (result.completion.deliveryPending ? 4 : 0);
2024
+ }
2025
+ return result.completion;
2026
+ }
2027
+ const claim = await guardReviewLaunch({
2028
+ gitCommonDir: await resolveGitCommonDir(),
2029
+ target: prepared.converge.target,
2030
+ startOver: opts.startOver, cycleRemote,
2031
+ boundFixRecovery,
2032
+ headSha: extra.target.headSha ?? '',
2033
+ inputSha256: guardedInputSha256(guardedInput),
2034
+ round: prepared.converge.round,
2035
+ intent: opts.launchIntent,
2036
+ retryReason: opts.retryReason,
2037
+ ...(opts.retryReport ? { legacyRetry: { reportPath: opts.retryReport, config, roster,
2038
+ historicalPlan: prepared.historicalRetryPlan === true } } : {}),
2039
+ maxAttempts: opts.maxAttempts === undefined ? undefined : Number(opts.maxAttempts),
2040
+ maxRounds: opts.maxRounds === undefined ? undefined : Number(opts.maxRounds),
2041
+ validate: async () => {
2042
+ validateLaunchProviders(roster.map(entry => entry.provider));
2043
+ if (assignments.length < 2) {
2044
+ throw new ReviewLaunchRefused('insufficient_reviewers', 'Convergence needs at least two reviewer assignments for a conclusive round.');
2045
+ }
2046
+ await validateLaunchOutputs(opts);
2047
+ },
2048
+ beforeClaim: async (context) => {
2049
+ const retained = await retainOrdinaryLaunchInputs({ gitCommonDir: common, target: context.target,
2050
+ attempt: context.attempt, round: context.round, ...(context.cycleId ? { cycleId: context.cycleId } : {}),
2051
+ headSha: extra.target.headSha ?? '', baseSha: extra.target.baseSha ?? null,
2052
+ guardedInput, asyncDescriptors: ordinaryAsyncDescriptors });
2053
+ return { ordinaryInputs: { version: 1, packetSha256: retained.sha256,
2054
+ baseSha: retained.baseSha } };
2055
+ },
2056
+ onClaim: async (claim) => {
2057
+ if (opts.telemetry !== false)
2058
+ await reportConvergeEvents([buildEvent({
2059
+ kind: 'attempt_claimed', convergeTarget: claim.target, attempt: claim.attempt,
2060
+ payload: { attempt: claim.attempt, cap: claim.cap, ...(claim.cycle ? { cycle_id: claim.cycle.id } : {}),
2061
+ ...(boundFixRecovery ? { bound_fix_recovery: { run_id: boundFixRecovery.runId,
2062
+ repo: boundFixRecovery.repo, pr_number: boundFixRecovery.prNumber, head_sha: extra.target.headSha } } : {}) },
2063
+ })]);
2064
+ if (claim.cycle)
2065
+ process.stderr.write(`Review cycle ${claim.cycle.id}. Prior local history: ${claim.cycle.history.attempts} attempts, ${claim.cycle.history.rounds} admitted rounds${claim.cycle.history.incomplete ? ' (known history only)' : ''}.\n`);
2066
+ process.stderr.write(`Convergence attempt ${claim.attempt}/${claim.cap} claimed for ${claim.target}.\n`);
2067
+ if (boundFixRecovery)
2068
+ process.stderr.write(`Bound fix recovery verified from run ${boundFixRecovery.runId}; attempt ${claim.attempt} retains the existing review history.\n`);
2069
+ },
2070
+ run: async (converge) => {
2071
+ const guardedCompletion = await executeCouncil(spinner, { ...prepared, converge }, diff, { ...opts, guardedConverge: false, exclusiveOutputs: true }, extra, work);
2072
+ if (!guardedCompletion)
2073
+ throw new Error('guarded_launch_missing_completion');
2074
+ completion = guardedCompletion;
2075
+ return guardedCompletion;
2076
+ },
2077
+ });
2078
+ if (claim.warning)
2079
+ process.stderr.write(`${claim.warning}\n`);
2080
+ if (claim.resumedCompletion) {
2081
+ process.stderr.write(`Fresh review already completed as run ${claim.resumedCompletion.runId}; no reviewers restarted.\n`);
2082
+ if (claim.resumedCompletion.reportPath)
2083
+ process.stderr.write(`Retained report: ${claim.resumedCompletion.reportPath}\n`);
2084
+ process.exitCode = claim.resumedCompletion.exitCode ?? (claim.resumedCompletion.deliveryPending ? 4 : 0);
2085
+ }
2086
+ return claim.resumedCompletion ?? completion;
2087
+ }
2088
+ // Async lane (RCL-25): fire the async reviewers with the round, never
2089
+ // await them; collect whatever arrived from earlier rounds after the
2090
+ // blocking council returns. Best-effort by design — a broken lane must
2091
+ // never fail or slow the blocking round.
2092
+ const asyncTargetLabel = extra.asyncTargetLabel;
2093
+ let asyncStoreDir;
2094
+ let asyncKey;
2095
+ let asyncLaunched = 0;
2096
+ if (!extra.pendingResume &&
2097
+ asyncTargetLabel !== undefined &&
2098
+ (asyncAssignments.length > 0 || (config.asyncModels?.length ?? 0) > 0)) {
2099
+ try {
2100
+ asyncStoreDir = await resolveAsyncStoreDir();
2101
+ asyncKey = asyncTargetKey(asyncTargetLabel, extra.target.kind === 'patch' ? prepared.converge?.target : undefined, prepared.converge?.cycleId);
2102
+ let asyncChunkAssignments = chunks.flatMap((chunk) => asyncAssignments.map((assignment) => ({ assignment, chunk })));
2103
+ if (asyncChunkAssignments.length > MAX_ASYNC_CALLS_PER_ROUND) {
2104
+ console.warn(`Async lane: capping ${asyncChunkAssignments.length} async calls at ` +
2105
+ `${MAX_ASYNC_CALLS_PER_ROUND} (one detached process each); the rest are dropped.`);
2106
+ asyncChunkAssignments = asyncChunkAssignments.slice(0, MAX_ASYNC_CALLS_PER_ROUND);
2107
+ }
2108
+ if (asyncChunkAssignments.length > 0) {
2109
+ const asyncPrompts = await Promise.all(asyncChunkAssignments.map(({ assignment, chunk }) => buildPrompt(chunk, assignment.role, {
2110
+ contextDocs,
2111
+ plan: planContext,
2112
+ })));
2113
+ const spools = await spoolAsyncCalls(asyncChunkAssignments.map(({ assignment }, i) => ({
2114
+ model: assignment.model,
2115
+ role: assignment.role.name,
2116
+ provider: assignment.provider,
2117
+ systemPrompt: asyncPrompts[i].systemPrompt,
2118
+ userPrompt: asyncPrompts[i].userPrompt,
2119
+ })), {
2120
+ storeDir: asyncStoreDir,
2121
+ targetKey: asyncKey,
2122
+ timeoutMs: config.asyncTimeout ?? DEFAULT_ASYNC_TIMEOUT_MS,
2123
+ maxRetries: config.maxRetries ?? DEFAULT_MAX_RETRIES,
2124
+ reasoningEffort: config.reasoningEffort ?? DEFAULT_REASONING_EFFORT,
2125
+ });
2126
+ launchAsyncWorkers(spools);
2127
+ asyncLaunched = spools.length;
2128
+ }
2129
+ }
2130
+ catch (err) {
2131
+ console.warn(`Async reviewer lane unavailable: ${String(err)}`);
2132
+ asyncStoreDir = undefined;
2133
+ }
2134
+ }
2135
+ const startTime = Date.now();
2136
+ const totalCalls = chunkAssignments.length;
2137
+ const timeoutMs = config.timeout ?? DEFAULT_TIMEOUT_MS;
2138
+ const concurrency = config.concurrency ?? DEFAULT_CONCURRENCY;
2139
+ const providerConcurrency = resolveProviderConcurrency(config.providerConcurrency);
2140
+ const runPlan = buildCouncilRunPlan({
2141
+ totalCalls,
2142
+ reviewers: assignments.length,
2143
+ chunks: chunks.length,
2144
+ concurrency,
2145
+ timeoutMs,
2146
+ providers: chunkAssignments.map(({ assignment }) => assignment.provider),
2147
+ providerConcurrency,
2148
+ });
2149
+ const planText = formatCouncilRunPlan(runPlan);
2150
+ const interactive = process.stderr.isTTY === true;
2151
+ const postReviewStage = (stage) => {
2152
+ const line = `Post-review stage: ${stage}`;
2153
+ if (interactive && spinner.isSpinning) {
2154
+ spinner.text = line;
2155
+ }
2156
+ else {
2157
+ process.stderr.write(`${line}\n`);
2158
+ }
2159
+ };
2160
+ if (interactive) {
2161
+ spinner.text = planText;
2162
+ spinner.start();
2163
+ }
2164
+ else {
2165
+ spinner.stop();
2166
+ process.stderr.write(`${planText}\n`);
2167
+ }
2168
+ const progress = new CouncilProgressReporter({
2169
+ totalCalls,
2170
+ interactive,
2171
+ updateInteractive: (text) => {
2172
+ spinner.text = text;
2173
+ },
2174
+ writeLine: (text) => {
2175
+ process.stderr.write(`${text}\n`);
2176
+ },
2177
+ });
2178
+ progress.start();
2179
+ let chunkReviews;
2180
+ let pendingProjection;
2181
+ let pendingAsyncExecution;
2182
+ try {
2183
+ if (extra.pendingResume) {
2184
+ if (!(await extra.pendingResume.journal.read()).finalized) {
2185
+ await executeCapturedOriginal({ commonDir: await resolveGitCommonDir(),
2186
+ ownership: extra.pendingResume.ownership, journal: extra.pendingResume.journal,
2187
+ expectedPlan: extra.pendingResume.journal.getPlan(), launch: extra.pendingResume.launch,
2188
+ onPhysicalReviewComplete: review => { progress.complete(review); },
2189
+ onLateAuditError: error => {
2190
+ console.warn(`Late reviewer result could not be retained: ${scrubText(String(error), 300)}`);
2191
+ } });
2192
+ await extra.pendingResume.journal.finalize(extra.pendingResume.ownership);
2193
+ }
2194
+ pendingAsyncExecution = await resolveFinalizedAsyncExecution({ commonDir: await resolveGitCommonDir(),
2195
+ namespace: extra.pendingResume.launch.runId, plan: extra.pendingResume.journal.getPlan(),
2196
+ ownership: extra.pendingResume.ownership });
2197
+ const proof = await exportCheckpointProof(extra.pendingResume.journal);
2198
+ pendingProjection = projectCheckpointReport({ sources: [],
2199
+ successor: { runId: extra.pendingResume.launch.runId, proof },
2200
+ policy: { version: 1, fraction: config.quorumFraction ?? DEFAULT_QUORUM_FRACTION } });
2201
+ chunkReviews = pendingProjection.seatReviews.map(seat => structuredClone(seat.review));
2202
+ }
2203
+ else
2204
+ chunkReviews = await runReviews(chunkAssignments.map((ca) => ca.assignment), prompts, {
2205
+ // Fall back to the shared constants, never to inline literals:
2206
+ // duplicated defaults drift (this read 120_000 after the default
2207
+ // moved to 600_000).
2208
+ timeoutMs,
2209
+ maxRetries: config.maxRetries ?? DEFAULT_MAX_RETRIES,
2210
+ concurrency,
2211
+ // Keep the default version-owned rather than materializing it into
2212
+ // legacy config digests. Explicit caps remain source-bound inputs.
2213
+ providerConcurrency,
2214
+ reasoningEffort: config.reasoningEffort ?? DEFAULT_REASONING_EFFORT,
2215
+ // Quorum closure (RCL-26, RCL-136): the round stops waiting once the
2216
+ // configured fraction of blocking seats completed every chunk — the
2217
+ // same seats and policy as the report's blocking health and the
2218
+ // server's gate. Secondary seats never count toward that quorum.
2219
+ quorum: {
2220
+ fraction: config.quorumFraction ?? DEFAULT_QUORUM_FRACTION,
2221
+ coreModels: config.models ?? DEFAULT_MODELS,
2222
+ blocking: chunkAssignments.map((ca) => assignmentLane(ca.assignment.model, prepared.coreModels, prepared.explicit) === 'blocking'),
2223
+ },
2224
+ onReviewComplete: (review) => progress.complete(review),
2225
+ });
2226
+ }
2227
+ finally {
2228
+ progress.stop();
2229
+ }
2230
+ postReviewStage('collecting and merging reviewer outputs');
2231
+ const preserveRefusedReviews = async (error, asyncArtifacts = []) => {
2232
+ try {
2233
+ const path = await retainAssemblyRefusal(resolveDataDir(), {
2234
+ run: { ...(extra.attestation ? { id: extra.attestation.runId } : {}), command: extra.command, target: extra.target,
2235
+ startedAt: prepared.startedAt.toISOString(), ...(prepared.converge ? { converge: prepared.converge } : {}) },
2236
+ chunkReviews, asyncArtifacts,
2237
+ });
2238
+ process.stderr.write(`Assembly refused; observed reviewer results retained privately at ${path}\n`);
2239
+ }
2240
+ catch (retentionError) {
2241
+ error.message += ` Observed reviewer results could not be retained: ${String(retentionError)}`;
2242
+ throw error;
2243
+ }
2244
+ };
2245
+ try {
2246
+ assertUnambiguousReviewerIdentities(chunkReviews);
2247
+ }
2248
+ catch (error) {
2249
+ if (error instanceof AmbiguousReviewerIdentityError)
2250
+ await preserveRefusedReviews(error);
2251
+ throw error;
2252
+ }
2253
+ // Collect async results from earlier rounds of this target (marked async).
2254
+ // Report assembly collapses the completed calls to one (model, role) review.
2255
+ let arrivedAsync = [];
2256
+ if (extra.pendingResume) {
2257
+ const bytes = (await extra.pendingResume.journal.readBindings())['supplemental-async'];
2258
+ if (bytes === undefined)
2259
+ throw new Error('pending_legacy_resume_async_binding_missing');
2260
+ const supplemental = decodeSupplementalAsync(bytes);
2261
+ arrivedAsync = supplemental.reviews.map(review => structuredClone(review));
2262
+ asyncLaunched = supplemental.asyncLaunched;
2263
+ }
2264
+ else if (asyncStoreDir && asyncKey) {
2265
+ try {
2266
+ arrivedAsync = await collectAsyncResults(asyncStoreDir, asyncKey, { blockingReviews: chunkReviews, onIdentityRefused: preserveRefusedReviews });
2267
+ }
2268
+ catch (err) {
2269
+ if (err instanceof AmbiguousReviewerIdentityError)
2270
+ throw err;
2271
+ console.warn(`Could not collect async reviewer results: ${String(err)}`);
2272
+ }
2273
+ }
2274
+ // RCL-27: every call feeds the cross-run model history (fail-soft — the
2275
+ // stats store must never break a review).
2276
+ try {
2277
+ const ts = new Date().toISOString();
2278
+ await appendCalls([...chunkReviews, ...arrivedAsync].map((r) => ({
2279
+ ts,
2280
+ model: r.model,
2281
+ role: r.role,
2282
+ durationMs: r.durationMs,
2283
+ status: r.status,
2284
+ source: 'live',
2285
+ })));
2286
+ }
2287
+ catch (err) {
2288
+ // Stats are advisory; reviews must not fail over them — but a broken
2289
+ // store should not be invisible either.
2290
+ console.warn(`Model-stats store unavailable (call history not recorded): ${String(err)}`);
2291
+ }
2292
+ // Trailing-precision weights scale each model's consensus vote. An empty
2293
+ // history means no weighting (and no weight noise in the report).
2294
+ // Org-wide history from Harness outranks this machine's store for a model
2295
+ // the org has enough outcomes for (RCL-38); the server is asked with a
2296
+ // short bound and the local store stands in when it cannot answer.
2297
+ // Telemetry off means nothing leaves the machine for weights either. The
2298
+ // server side is bounded to three seconds; the settings file and the local
2299
+ // store are read as local files.
2300
+ // An attested review may outlast its credential: from here on — the
2301
+ // weights read, then delivery — the credential is renewed for the same run
2302
+ // id when little of it remains (RCL-40).
2303
+ let attestation = extra.attestation;
2304
+ if (attestation) {
2305
+ const renewal = await renewAttestation(attestation, { rclVersion: RCL_VERSION });
2306
+ attestation = renewal.attestation;
2307
+ if (renewal.renewed) {
2308
+ process.stderr.write(chalk.dim(`Attestation renewed for run ${attestation.runId} (expires ${attestation.expiresAt})`) + '\n');
2309
+ }
2310
+ else if (renewal.failure !== undefined) {
2311
+ process.stderr.write(chalk.dim(`Attestation not renewed (${renewal.failure}); continuing with the credential in hand`) + '\n');
2312
+ }
2313
+ }
2314
+ let modelWeights;
2315
+ try {
2316
+ if (extra.pendingResume)
2317
+ throw new Error('pending resume uses its frozen unweighted aggregation');
2318
+ const level = resolveTelemetryLevel(await loadHarnessSettings(process.cwd(), opts.config), { noTelemetry: opts.telemetry === false }, process.env);
2319
+ const loaded = await loadMergedWeights({
2320
+ rclVersion: RCL_VERSION,
2321
+ timeoutMs: 3_000,
2322
+ serverEnabled: level !== 'off',
2323
+ ...(attestation ? { credential: attestation.credential } : {}),
2324
+ });
2325
+ if (loaded.size > 0)
2326
+ modelWeights = loaded;
2327
+ }
2328
+ catch (err) {
2329
+ // Weights are advisory; the review runs unweighted, but not silently.
2330
+ if (!extra.pendingResume)
2331
+ console.warn(`Model weights unavailable (consensus unweighted): ${scrubText(err instanceof Error ? err.message : String(err), 300)}`);
2332
+ modelWeights = undefined;
2333
+ }
2334
+ const runInput = {
2335
+ id: extra.pendingResume?.launch.runId ?? extra.attestation?.runId,
2336
+ rclVersion: RCL_VERSION,
2337
+ command: extra.command,
2338
+ target: extra.target,
2339
+ roster: buildRoster({
2340
+ assignments,
2341
+ asyncAssignments,
2342
+ coreModels: prepared.coreModels,
2343
+ explicit: prepared.explicit,
2344
+ gating: prepared.gatingConfig,
2345
+ }),
2346
+ ...(prepared.spec ? { spec: prepared.spec } : {}),
2347
+ contextFiles: contextDocs.map((d) => ({ path: d.label, sha256: d.sha256 })),
2348
+ // Record the effective focus that shaped the plan prompts.
2349
+ ...(extra.command === 'review-plan' ? { plan: { focus: extra.focus ?? 'comprehensive' } } : {}),
2350
+ runner: detectRunner(process.env, hostname()),
2351
+ startedAt: extra.pendingResume
2352
+ ? new Date(extra.pendingResume.launch.startedAtMs)
2353
+ : prepared.startedAt,
2354
+ ...(prepared.converge ? { converge: { target: prepared.converge.target, round: prepared.converge.round, attempt: prepared.converge.attempt }, cycleId: prepared.converge.cycleId } : {}),
2355
+ };
2356
+ const assemblyDependencies = {
2357
+ onStage: postReviewStage,
2358
+ onVerificationStart: () => { spinner.text = 'Verifying single-model findings...'; },
2359
+ onVerificationProgress: (event) => {
2360
+ const line = `Verification ${event.completedBatches}/${event.totalBatches} batches ` +
2361
+ `(${event.completedCandidates}/${event.totalCandidates} findings)`;
2362
+ if (interactive) {
2363
+ spinner.text = line;
2364
+ }
2365
+ else {
2366
+ const stride = Math.max(1, Math.ceil(event.totalBatches / 20));
2367
+ if (event.completedBatches === 0 ||
2368
+ event.completedBatches === event.totalBatches ||
2369
+ event.completedBatches % stride === 0) {
2370
+ process.stderr.write(`${line}\n`);
2371
+ }
2372
+ }
2373
+ },
2374
+ };
2375
+ let result;
2376
+ let pendingEvidence;
2377
+ if (extra.pendingResume) {
2378
+ const proof = await exportCheckpointProof(extra.pendingResume.journal);
2379
+ const supplementalBytes = (await extra.pendingResume.journal.readBindings())['supplemental-async'];
2380
+ if (supplementalBytes === undefined)
2381
+ throw new Error('pending_legacy_resume_async_binding_missing');
2382
+ const supplementalAsync = decodeSupplementalAsync(supplementalBytes);
2383
+ const checkpointAssembly = {
2384
+ projection: pendingProjection,
2385
+ supplementalAsync,
2386
+ ...(pendingAsyncExecution === undefined ? {} : { asyncExecution: pendingAsyncExecution }),
2387
+ diff,
2388
+ startTime: extra.pendingResume.launch.startedAtMs,
2389
+ run: runInput,
2390
+ };
2391
+ const gated = await executeCheckpointGating({ assembly: checkpointAssembly,
2392
+ commonDir: await resolveGitCommonDir(), ownership: extra.pendingResume.ownership,
2393
+ journal: extra.pendingResume.journal, beforeLaunch: async () => { },
2394
+ askFactory: model => {
2395
+ const adapter = defaultAdapterFactory(detectProvider(model));
2396
+ return (selected, system, user, request) => adapter.ask(selected, system, user, request);
2397
+ },
2398
+ onLateAuditError: error => {
2399
+ console.warn(`Late verifier result could not be retained: ${scrubText(String(error), 300)}`);
2400
+ },
2401
+ });
2402
+ result = (await assembleCheckpointReview(checkpointAssembly, {
2403
+ ...assemblyDependencies, verificationProof: gated.verificationProof,
2404
+ })).report;
2405
+ result.run.reviewer_evidence = describeReviewerEvidence(proof, supplementalAsync);
2406
+ pendingEvidence = { proof, assembly: checkpointAssembly, verificationProof: gated.verificationProof };
2407
+ }
2408
+ else
2409
+ result = await assembleCompletedReview({
2410
+ chunkReviews, arrivedAsync, asyncLaunched, startTime, roleMap, config, diff,
2411
+ gatingConfig: prepared.gatingConfig, modelWeights, run: runInput,
2412
+ }, assemblyDependencies);
2413
+ const { run } = result;
2414
+ // The same derivation converge-report and the server apply to this report.
2415
+ // A report it cannot judge is recorded as inconclusive, never as healthy.
2416
+ let blockingHealth;
2417
+ let blockingHealthError;
2418
+ try {
2419
+ blockingHealth = deriveBlockingHealth({ roster: run.roster, reviews: result.reviews,
2420
+ fraction: config.quorumFraction ?? DEFAULT_QUORUM_FRACTION });
2421
+ }
2422
+ catch (error) {
2423
+ blockingHealthError = error instanceof Error ? error.message : String(error);
2424
+ }
2425
+ spinner.succeed('Review complete');
2426
+ process.stderr.write(chalk.dim(`Reviewed ${describeRunTarget(run.target)} · run ${run.id}` +
2427
+ (run.converge ? ` · converge ${run.converge.target} round ${run.converge.round ?? '?'}` : '')) + '\n');
2428
+ // Status lines go to stderr: stdout may be a machine-read JSON stream
2429
+ // (`--json | jq`), which a stray status line would corrupt.
2430
+ if (asyncLaunched > 0) {
2431
+ process.stderr.write(chalk.dim(extra.pendingResume
2432
+ ? `Reused ${asyncLaunched} retained async reviewer call(s) from this same interrupted attempt.`
2433
+ : `Fired ${asyncLaunched} async reviewer call(s) — results merge into the next round of this target.`) + '\n');
2434
+ }
2435
+ if (arrivedAsync.length > 0) {
2436
+ process.stderr.write(chalk.dim(`Merged ${mergeChunkReviews(arrivedAsync).length} async reviewer result(s) from an earlier round.`) + '\n');
2437
+ }
2438
+ process.stderr.write(blockingHealth
2439
+ ? (blockingHealth.conclusive ? chalk.dim : chalk.yellow)(describeBlockingHealth(blockingHealth)) + '\n'
2440
+ : chalk.yellow(`Blocking reviewer health could not be derived (${blockingHealthError}); the report will not be admitted.`) + '\n');
2441
+ // Evidence delivery (IO-12475 section 8) is fail-soft: nothing in it may
2442
+ // turn a finished review into a failure unless --evidence-required asks.
2443
+ let runtime;
2444
+ let runtimeError;
2445
+ try {
2446
+ runtime = await createTelemetryRuntime({
2447
+ rclVersion: RCL_VERSION,
2448
+ config,
2449
+ noTelemetry: opts.telemetry === false,
2450
+ ...(attestation ? { credential: attestation.credential, attestedExpiresAt: attestation.expiresAt } : {}),
2451
+ });
2452
+ }
2453
+ catch (err) {
2454
+ // Kept for the --evidence-required verdict below, which names the cause.
2455
+ runtimeError = scrubText(String(err), 200);
2456
+ process.stderr.write(chalk.dim(`Evidence delivery unavailable: ${runtimeError}`) + '\n');
2457
+ }
2458
+ // The report as it may leave the machine — free text scrubbed, a parse
2459
+ // failure reduced to the parser message unless harness.parseFailures opts
2460
+ // in. --json-file and --markdown are written from the same view, so the
2461
+ // declared digests match the files and nothing raw travels. With
2462
+ // telemetry off only fresh prose normalization applies. This is the fresh
2463
+ // producer boundary; retained reports and envelope builders never rewrite it.
2464
+ const produced = normalizeGeneratedReport(result);
2465
+ const delivered = runtime && runtime.level !== 'off' ? sanitizeForDelivery(produced, { parseFailures: runtime.parseFailures }) : produced;
2466
+ postReviewStage('rendering report artifacts');
2467
+ const artifacts = renderReportArtifacts(delivered);
2468
+ let pendingReviewerArtifact;
2469
+ if (extra.pendingResume && pendingEvidence) {
2470
+ pendingReviewerArtifact = serializeReviewerArtifact({
2471
+ assembly: pendingEvidence.assembly,
2472
+ reportBytes: artifacts.report_json,
2473
+ representation: { version: 1, parseFailures: runtime?.parseFailures ?? false },
2474
+ ...(pendingEvidence.verificationProof ? { verificationProof: pendingEvidence.verificationProof } : {}),
2475
+ });
2476
+ await extra.pendingResume.journal.retainTerminalReport({
2477
+ reportBytes: artifacts.report_json,
2478
+ reviewerArtifactBytes: pendingReviewerArtifact.bytes,
2479
+ }, extra.pendingResume.ownership);
2480
+ }
2481
+ // Output
2482
+ if (opts.json) {
2483
+ console.log(artifacts.report_json);
2484
+ }
2485
+ else {
2486
+ printReviewSummary(result);
2487
+ }
2488
+ const outputDiagnostics = await writeReportArtifacts(artifacts, { ...opts, exclusive: opts.guardedConverge || opts.exclusiveOutputs }, {
2489
+ onWritten: (label, path) => console.log(chalk.dim(`${label} written to: ${path}`)),
2490
+ onError: (message) => process.stderr.write(chalk.red(message) + '\n'),
2491
+ });
2492
+ if (opts.post && !diff.metadata) {
2493
+ console.log(chalk.yellow('--post ignored: no PR to post to for a local diff.'));
2494
+ }
2495
+ if (opts.post && diff.metadata) {
2496
+ const postSpinner = ora('Posting review to GitHub...').start();
2497
+ try {
2498
+ await postGitHubReview(result, diff.metadata, config.githubToken, diff.files);
2499
+ postSpinner.succeed('Review posted to GitHub');
2500
+ }
2501
+ catch (err) {
2502
+ postSpinner.fail(`Failed to post to GitHub: ${String(err)}`);
2503
+ }
2504
+ }
2505
+ // A failed requested output must not prevent delivery or immutable recovery
2506
+ // retention of the rendered originals. Try both files before any exit code.
2507
+ const evidenceRequired = opts.evidenceRequired === true;
2508
+ const delivery = runtime
2509
+ ? await deliverRun(runtime, { result: delivered, artifacts, evidenceRequired, outputDiagnostics,
2510
+ ...(pendingReviewerArtifact ? { reviewerArtifact: pendingReviewerArtifact } : {}) }).catch((err) => ({
2511
+ status: 'error',
2512
+ line: `Evidence delivery failed: ${scrubText(String(err), 300)}`,
2513
+ exitCode: evidenceRequired ? 4 : 0,
2514
+ spooled: false,
2515
+ }))
2516
+ : {
2517
+ status: 'off',
2518
+ line: evidenceRequired ? `Evidence not sent: telemetry could not be set up (${runtimeError ?? 'unknown cause'})` : '',
2519
+ exitCode: evidenceRequired ? 4 : 0,
2520
+ spooled: false,
2521
+ };
2522
+ if (delivery.line !== '')
2523
+ process.stderr.write(chalk.dim(delivery.line) + '\n');
2524
+ // The flush hint is honest only when something was spooled to flush.
2525
+ const evidenceFailure = [
2526
+ `Evidence was not recorded (--evidence-required): ${delivery.line || delivery.status}.`,
2527
+ delivery.spooled
2528
+ ? `Retry delivery with \`rcl telemetry flush --run ${delivery.runId}\` rather than re-running the review.`
2529
+ : undefined,
2530
+ delivery.retention?.status === 'complete'
2531
+ ? `Original reports are retained; inspect them with \`rcl telemetry rejected --run ${delivery.runId}\` before supported recovery.`
2532
+ : delivery.retention?.status === 'failed'
2533
+ ? `Original report retention failed: ${delivery.retention.error ?? 'unknown error'}.`
2534
+ : undefined,
2535
+ ].filter((part) => part !== undefined).join(' ');
2536
+ if (opts.exclusiveOutputs && outputDiagnostics.some(diagnostic => diagnostic.path === 'output.report_json') &&
2537
+ !delivery.spooled && delivery.status !== 'recorded' && delivery.retention?.status !== 'complete') {
2538
+ throw new ReviewLaunchRefused('report_write_failed', 'The JSON report could not be retained. This attempt remains spent; correct the output path before an explicit bounded retry.');
2539
+ }
2540
+ const completion = {
2541
+ runId: run.id,
2542
+ reportJsonSha256: sha256Hex(artifacts.report_json),
2543
+ successfulReviews: result.stats.successfulReviews,
2544
+ totalReviews: result.stats.totalReviews,
2545
+ // Aggregate counters stay the report's own stats; launch health is blocking-only.
2546
+ reviewerHealth: blockingHealth
2547
+ ? { version: 1, policy: blockingHealth.policy, successfulSeats: blockingHealth.successfulSeats.length }
2548
+ : { version: 1, policy: resolveQuorumPolicy(0), successfulSeats: 0 },
2549
+ ...guardedDeliveryState(delivery),
2550
+ ...(prepared.converge?.cycleId ? {
2551
+ exitCode: opts.ci && run.ci_exit_code !== 0 ? run.ci_exit_code : delivery.exitCode || (outputDiagnostics.length > 0 ? 1 : 0),
2552
+ ...(opts.jsonFile ? { reportPath: resolve(opts.jsonFile) } : {}),
2553
+ } : {}),
2554
+ hardFailure: chunkReviews.some(review => review.status === 'error' || review.status === 'parse_failed'),
2555
+ };
2556
+ // CI mode: fail on a fully-failed run or on blocking findings. The gate
2557
+ // verdict keeps its exit code — pipelines branch on it — and an evidence
2558
+ // failure is reported beside it.
2559
+ if (opts.ci) {
2560
+ const verdict = evaluateCiGate(result);
2561
+ if (verdict.exitCode !== 0) {
2562
+ console.error(chalk.red(`\n${verdict.message}`));
2563
+ if (delivery.exitCode !== 0)
2564
+ console.error(chalk.red(evidenceFailure));
2565
+ process.exitCode = verdict.exitCode;
2566
+ return completion;
2567
+ }
2568
+ }
2569
+ if (delivery.exitCode !== 0) {
2570
+ console.error(chalk.red(evidenceFailure));
2571
+ process.exitCode = delivery.exitCode;
2572
+ return completion;
2573
+ }
2574
+ if (outputDiagnostics.length > 0)
2575
+ process.exitCode = 1;
2576
+ return completion;
2577
+ }
2578
+ async function runDiscuss(question, opts) {
2579
+ const spinner = ora('Loading report...').start();
2580
+ try {
2581
+ await fetchHarnessKeys(spinner);
2582
+ const config = await loadConfig(opts.config);
2583
+ let result;
2584
+ try {
2585
+ result = JSON.parse(await readFile(opts.report, 'utf-8'));
2586
+ }
2587
+ catch {
2588
+ throw new Error(`Could not read report JSON: ${opts.report}`);
2589
+ }
2590
+ if (!Array.isArray(result.findings)) {
2591
+ throw new Error(`Not an rcl report (no findings array): ${opts.report}`);
2592
+ }
2593
+ const finding = resolveFinding(result, opts.finding);
2594
+ const models = opts.models
2595
+ ? opts.models.split(',').map((s) => s.trim()).filter(Boolean)
2596
+ : finding.consensus.models;
2597
+ if (models.length === 0) {
2598
+ throw new Error('No models to ask: the finding lists none and --models was not given.');
2599
+ }
2600
+ const contextDocs = await loadContextDocs(opts.context ?? []);
2601
+ const prompts = buildDiscussPrompts({ finding, question, contextDocs });
2602
+ spinner.text = `Asking ${models.length} model(s) about "${finding.title.slice(0, 60)}"...`;
2603
+ const answers = await runDiscussion(models, prompts, {
2604
+ timeoutMs: config.timeout ?? DEFAULT_TIMEOUT_MS,
2605
+ maxRetries: config.maxRetries ?? DEFAULT_MAX_RETRIES,
2606
+ reasoningEffort: config.reasoningEffort ?? DEFAULT_REASONING_EFFORT,
2607
+ });
2608
+ spinner.succeed(`Discussion complete (${answers.filter((a) => a.status === 'success').length}/${answers.length} answered)`);
2609
+ if (opts.json) {
2610
+ console.log(JSON.stringify({ finding: { id: finding.id, file: finding.file, title: finding.title }, question, answers }, null, 2));
2611
+ return;
2612
+ }
2613
+ console.log('');
2614
+ console.log(chalk.bold(`Finding: `) + `${finding.title}`);
2615
+ console.log(chalk.dim(`${finding.file}:${finding.startLine}–${finding.endLine} · ${finding.severity} · ${finding.consensus.disputed ? 'disputed' : finding.consensus.tier}`));
2616
+ console.log(chalk.bold(`Question: `) + question);
2617
+ for (const answer of answers) {
2618
+ console.log('');
2619
+ console.log(chalk.dim('─'.repeat(80)));
2620
+ if (answer.status === 'success') {
2621
+ console.log(chalk.cyan.bold(answer.model) + chalk.dim(` (${(answer.durationMs / 1000).toFixed(1)}s)`));
2622
+ console.log('');
2623
+ console.log(answer.text);
2624
+ }
2625
+ else {
2626
+ console.log(chalk.cyan.bold(answer.model) +
2627
+ ' ' +
2628
+ chalk.red(answer.status === 'timeout' ? '⏱ timed out' : `✗ ${answer.error ?? 'error'}`));
2629
+ }
2630
+ }
2631
+ console.log('');
2632
+ }
2633
+ catch (err) {
2634
+ spinner.fail(String(err));
2635
+ if (process.env['RCL_DEBUG']) {
2636
+ console.error(err);
2637
+ }
2638
+ process.exit(1);
2639
+ }
2640
+ }
2641
+ /**
2642
+ * Roles whose instincts transfer to reviewing a design document. Used only
2643
+ * when neither CLI flags nor config request roles; spec-compliance joins
2644
+ * when a spec is resolved (see prepareCouncil).
2645
+ */
2646
+ const PLAN_DEFAULT_ROLES = ['general', 'architecture', 'edge-case-hunter'];
2647
+ async function runPlanReview(file, opts) {
2648
+ const spinner = ora('Loading configuration...').start();
2649
+ try {
2650
+ // A plan is not a commit: the review-plan command declares none of the
2651
+ // head-binding flags, and this guard keeps any future path that shares
2652
+ // the option type from carrying one that would then be silently ignored.
2653
+ if (opts.headSha !== undefined || opts.baseSha !== undefined || opts.expectHeadSha !== undefined) {
2654
+ throw new Error('--head-sha, --base-sha and --expect-head-sha do not apply to plan reviews; a plan is bound by its content digest.');
2655
+ }
2656
+ assertEvidenceCanBeRequired(opts);
2657
+ let focus;
2658
+ if (opts.focus) {
2659
+ if (!isPlanFocus(opts.focus)) {
2660
+ spinner.fail(`Invalid --focus "${opts.focus}". Use one of: ${PLAN_FOCUS_MODES.join(', ')}.`);
2661
+ process.exit(1);
2662
+ }
2663
+ focus = opts.focus;
2664
+ }
2665
+ const prepared = await prepareCouncil(spinner, opts, PLAN_DEFAULT_ROLES);
2666
+ assertEvidenceCanBeRequired(opts, prepared.config);
2667
+ await assertEvidenceDeliverable(opts, prepared.config);
2668
+ spinner.text = `Loading plan: ${file}`;
2669
+ const diff = await loadPlanAsDiff(file);
2670
+ // Plan reviews get an async lane too: re-reviewing the same plan file
2671
+ // collects what the previous run fired.
2672
+ await executeCouncil(spinner, prepared, diff, opts, {
2673
+ command: 'review-plan',
2674
+ target: { kind: 'plan' },
2675
+ focus,
2676
+ asyncTargetLabel: `plan:${file}`,
2677
+ });
2678
+ }
2679
+ catch (err) {
2680
+ spinner.fail(String(err));
2681
+ if (process.env['RCL_DEBUG']) {
2682
+ console.error(err);
2683
+ }
2684
+ process.exit(1);
2685
+ }
2686
+ }
2687
+ program.parse();
2688
+ //# sourceMappingURL=index.js.map