@quickchatai/quickreview 0.0.0-stage → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (1443) hide show
  1. package/LICENSE +23 -0
  2. package/README.md +385 -2
  3. package/THIRD-PARTY-NOTICES.md +134 -0
  4. package/agents/pass-abstraction.md +64 -0
  5. package/agents/pass-ask-human.md +134 -0
  6. package/agents/pass-backcompat.md +67 -0
  7. package/agents/pass-baseline.md +61 -0
  8. package/agents/pass-behaviour-delta.md +64 -0
  9. package/agents/pass-bounds.md +36 -0
  10. package/agents/pass-claims.md +31 -0
  11. package/agents/pass-concurrency.md +168 -0
  12. package/agents/pass-consistency.md +57 -0
  13. package/agents/pass-contracts.md +30 -0
  14. package/agents/pass-data-safety.md +54 -0
  15. package/agents/pass-deferred.md +35 -0
  16. package/agents/pass-delivery.md +37 -0
  17. package/agents/pass-duplication.md +60 -0
  18. package/agents/pass-intent.md +63 -0
  19. package/agents/pass-lint-conformance.md +46 -0
  20. package/agents/pass-maintainability.md +67 -0
  21. package/agents/pass-placement.md +68 -0
  22. package/agents/pass-prod-risk.md +24 -0
  23. package/agents/pass-rollout.md +32 -0
  24. package/agents/pass-roundtrip.md +46 -0
  25. package/agents/pass-security.md +31 -0
  26. package/agents/pass-simplicity.md +68 -0
  27. package/agents/pass-statelessness.md +27 -0
  28. package/agents/pass-style.md +37 -0
  29. package/agents/pass-sweep.md +57 -0
  30. package/agents/pass-tests.md +58 -0
  31. package/agents/pass-ui.md +23 -0
  32. package/agents/pass-validate.md +50 -0
  33. package/agents/pass-widened.md +46 -0
  34. package/agents/scan/pass-scan.md +86 -0
  35. package/bin/qcr.js +11 -0
  36. package/config.example.toml +1067 -0
  37. package/deploy/ansible/README.md +356 -0
  38. package/deploy/ansible/group_vars/all.yml +212 -0
  39. package/deploy/ansible/inventory.example.ini +25 -0
  40. package/deploy/ansible/playbook.yml +360 -0
  41. package/deploy/collect-secrets.sh +289 -0
  42. package/deploy/collect-slack-secrets.mjs +1190 -0
  43. package/deploy/github-app-manifest.json +31 -0
  44. package/deploy/manifest.schema.json +75 -0
  45. package/deploy/qcr-update.sh +803 -0
  46. package/deploy/reviewer/AGENTS.override.md +12 -0
  47. package/dist/extensions/cli/bash-guard-ambient.js +42 -0
  48. package/dist/extensions/cli/bash-guard-ambient.js.map +1 -0
  49. package/dist/extensions/cli/bash-guard.js +5 -0
  50. package/dist/extensions/cli/bash-guard.js.map +1 -0
  51. package/dist/extensions/cli/companion-guard.js +5 -0
  52. package/dist/extensions/cli/companion-guard.js.map +1 -0
  53. package/dist/extensions/cli/debug-round.js +8 -0
  54. package/dist/extensions/cli/debug-round.js.map +1 -0
  55. package/dist/extensions/cli/inject-bridge.js +5 -0
  56. package/dist/extensions/cli/inject-bridge.js.map +1 -0
  57. package/dist/extensions/cli/ledger-pin.js +5 -0
  58. package/dist/extensions/cli/ledger-pin.js.map +1 -0
  59. package/dist/extensions/cli/lens.js +13 -0
  60. package/dist/extensions/cli/lens.js.map +1 -0
  61. package/dist/extensions/cli/muted-theme.js +5 -0
  62. package/dist/extensions/cli/muted-theme.js.map +1 -0
  63. package/dist/extensions/cli/onboarding.js +5 -0
  64. package/dist/extensions/cli/onboarding.js.map +1 -0
  65. package/dist/extensions/cli/setup.js +5 -0
  66. package/dist/extensions/cli/setup.js.map +1 -0
  67. package/dist/extensions/cli/tui-view.js +5 -0
  68. package/dist/extensions/cli/tui-view.js.map +1 -0
  69. package/dist/extensions/companion-guard.js +361 -0
  70. package/dist/extensions/companion-guard.js.map +1 -0
  71. package/dist/extensions/debug-round.js +204 -0
  72. package/dist/extensions/debug-round.js.map +1 -0
  73. package/dist/extensions/inject-bridge.js +603 -0
  74. package/dist/extensions/inject-bridge.js.map +1 -0
  75. package/dist/extensions/muted-theme.js +12 -0
  76. package/dist/extensions/muted-theme.js.map +1 -0
  77. package/dist/extensions/onboarding.js +175 -0
  78. package/dist/extensions/onboarding.js.map +1 -0
  79. package/dist/extensions/setup-replay.js +269 -0
  80. package/dist/extensions/setup-replay.js.map +1 -0
  81. package/dist/extensions/setup-scan.js +998 -0
  82. package/dist/extensions/setup-scan.js.map +1 -0
  83. package/dist/extensions/setup-triage.js +175 -0
  84. package/dist/extensions/setup-triage.js.map +1 -0
  85. package/dist/extensions/setup.js +1160 -0
  86. package/dist/extensions/setup.js.map +1 -0
  87. package/dist/extensions/tui-view.js +389 -0
  88. package/dist/extensions/tui-view.js.map +1 -0
  89. package/dist/extensions/vendor/openviking-pi/client.js +376 -0
  90. package/dist/extensions/vendor/openviking-pi/client.js.map +1 -0
  91. package/dist/extensions/vendor/openviking-pi/config.js +189 -0
  92. package/dist/extensions/vendor/openviking-pi/config.js.map +1 -0
  93. package/dist/extensions/vendor/openviking-pi/index.js +421 -0
  94. package/dist/extensions/vendor/openviking-pi/index.js.map +1 -0
  95. package/dist/extensions/vendor/openviking-pi/lib/capture-adapter.mjs +86 -0
  96. package/dist/extensions/vendor/openviking-pi/lib/capture-adapter.mjs.map +1 -0
  97. package/dist/extensions/vendor/openviking-pi/lib/takeover-core.mjs +408 -0
  98. package/dist/extensions/vendor/openviking-pi/lib/takeover-core.mjs.map +1 -0
  99. package/dist/extensions/vendor/openviking-pi/lib/uri-guard-adapter.mjs +41 -0
  100. package/dist/extensions/vendor/openviking-pi/lib/uri-guard-adapter.mjs.map +1 -0
  101. package/dist/extensions/vendor/openviking-pi/qcr-hint.js +290 -0
  102. package/dist/extensions/vendor/openviking-pi/qcr-hint.js.map +1 -0
  103. package/dist/extensions/vendor/openviking-pi/qcr-honest.js +148 -0
  104. package/dist/extensions/vendor/openviking-pi/qcr-honest.js.map +1 -0
  105. package/dist/extensions/vendor/openviking-pi/qcr-hooks.js +68 -0
  106. package/dist/extensions/vendor/openviking-pi/qcr-hooks.js.map +1 -0
  107. package/dist/extensions/vendor/openviking-pi/qcr-leaf.js +77 -0
  108. package/dist/extensions/vendor/openviking-pi/qcr-leaf.js.map +1 -0
  109. package/dist/extensions/vendor/openviking-pi/qcr-lesson.js +216 -0
  110. package/dist/extensions/vendor/openviking-pi/qcr-lesson.js.map +1 -0
  111. package/dist/extensions/vendor/openviking-pi/qcr-namespaces.js +154 -0
  112. package/dist/extensions/vendor/openviking-pi/qcr-namespaces.js.map +1 -0
  113. package/dist/extensions/vendor/openviking-pi/qcr-recall.js +254 -0
  114. package/dist/extensions/vendor/openviking-pi/qcr-recall.js.map +1 -0
  115. package/dist/extensions/vendor/openviking-pi/qcr-shim.js +94 -0
  116. package/dist/extensions/vendor/openviking-pi/qcr-shim.js.map +1 -0
  117. package/dist/extensions/vendor/openviking-pi/qcr-write.js +333 -0
  118. package/dist/extensions/vendor/openviking-pi/qcr-write.js.map +1 -0
  119. package/dist/extensions/vendor/openviking-pi/recall.js +108 -0
  120. package/dist/extensions/vendor/openviking-pi/recall.js.map +1 -0
  121. package/dist/extensions/vendor/openviking-pi/shared/capture-utils.mjs +529 -0
  122. package/dist/extensions/vendor/openviking-pi/shared/capture-utils.mjs.map +1 -0
  123. package/dist/extensions/vendor/openviking-pi/shared/credentials.mjs +205 -0
  124. package/dist/extensions/vendor/openviking-pi/shared/credentials.mjs.map +1 -0
  125. package/dist/extensions/vendor/openviking-pi/shared/pending-queue.mjs +442 -0
  126. package/dist/extensions/vendor/openviking-pi/shared/pending-queue.mjs.map +1 -0
  127. package/dist/extensions/vendor/openviking-pi/shared/profile-inject.mjs +286 -0
  128. package/dist/extensions/vendor/openviking-pi/shared/profile-inject.mjs.map +1 -0
  129. package/dist/extensions/vendor/openviking-pi/shared/recall-compress-core.mjs +189 -0
  130. package/dist/extensions/vendor/openviking-pi/shared/recall-compress-core.mjs.map +1 -0
  131. package/dist/extensions/vendor/openviking-pi/shared/recall-core.mjs +580 -0
  132. package/dist/extensions/vendor/openviking-pi/shared/recall-core.mjs.map +1 -0
  133. package/dist/extensions/vendor/openviking-pi/shared/retryable.mjs +11 -0
  134. package/dist/extensions/vendor/openviking-pi/shared/retryable.mjs.map +1 -0
  135. package/dist/extensions/vendor/openviking-pi/shared/session-model.mjs +63 -0
  136. package/dist/extensions/vendor/openviking-pi/shared/session-model.mjs.map +1 -0
  137. package/dist/extensions/vendor/openviking-pi/shared/uri-guard.mjs +58 -0
  138. package/dist/extensions/vendor/openviking-pi/shared/uri-guard.mjs.map +1 -0
  139. package/dist/extensions/vendor/openviking-pi/shared/workspace-peer.mjs +16 -0
  140. package/dist/extensions/vendor/openviking-pi/shared/workspace-peer.mjs.map +1 -0
  141. package/dist/extensions/vendor/openviking-pi/sync.js +127 -0
  142. package/dist/extensions/vendor/openviking-pi/sync.js.map +1 -0
  143. package/dist/extensions/vendor/openviking-pi/takeover.js +25 -0
  144. package/dist/extensions/vendor/openviking-pi/takeover.js.map +1 -0
  145. package/dist/extensions/vendor/openviking-pi/tools.js +417 -0
  146. package/dist/extensions/vendor/openviking-pi/tools.js.map +1 -0
  147. package/dist/src/anomalies.js +483 -0
  148. package/dist/src/anomalies.js.map +1 -0
  149. package/dist/src/answerworker.js +180 -0
  150. package/dist/src/answerworker.js.map +1 -0
  151. package/dist/src/appstamp.js +68 -0
  152. package/dist/src/appstamp.js.map +1 -0
  153. package/dist/src/archive/admission.js +3 -0
  154. package/dist/src/archive/admission.js.map +1 -0
  155. package/dist/src/archive/archivedb.js +474 -0
  156. package/dist/src/archive/archivedb.js.map +1 -0
  157. package/dist/src/archive/archiver.js +1770 -0
  158. package/dist/src/archive/archiver.js.map +1 -0
  159. package/dist/src/archive/archivewrite.js +198 -0
  160. package/dist/src/archive/archivewrite.js.map +1 -0
  161. package/dist/src/archive/bundle.js +266 -0
  162. package/dist/src/archive/bundle.js.map +1 -0
  163. package/dist/src/archive/dbmove.js +638 -0
  164. package/dist/src/archive/dbmove.js.map +1 -0
  165. package/dist/src/archive/duplicatecounts.js +80 -0
  166. package/dist/src/archive/duplicatecounts.js.map +1 -0
  167. package/dist/src/archive/eligibility.js +113 -0
  168. package/dist/src/archive/eligibility.js.map +1 -0
  169. package/dist/src/archive/extract.js +288 -0
  170. package/dist/src/archive/extract.js.map +1 -0
  171. package/dist/src/archive/fsutil.js +330 -0
  172. package/dist/src/archive/fsutil.js.map +1 -0
  173. package/dist/src/archive/gzfile.js +277 -0
  174. package/dist/src/archive/gzfile.js.map +1 -0
  175. package/dist/src/archive/kinds.js +22 -0
  176. package/dist/src/archive/kinds.js.map +1 -0
  177. package/dist/src/archive/livecheckpoint.js +178 -0
  178. package/dist/src/archive/livecheckpoint.js.map +1 -0
  179. package/dist/src/archive/liveretention.js +145 -0
  180. package/dist/src/archive/liveretention.js.map +1 -0
  181. package/dist/src/archive/manifest.js +284 -0
  182. package/dist/src/archive/manifest.js.map +1 -0
  183. package/dist/src/archive/overseer.js +283 -0
  184. package/dist/src/archive/overseer.js.map +1 -0
  185. package/dist/src/archive/piglobal.js +129 -0
  186. package/dist/src/archive/piglobal.js.map +1 -0
  187. package/dist/src/archive/plan.js +398 -0
  188. package/dist/src/archive/plan.js.map +1 -0
  189. package/dist/src/archive/prarchive.js +831 -0
  190. package/dist/src/archive/prarchive.js.map +1 -0
  191. package/dist/src/archive/prfiles.js +303 -0
  192. package/dist/src/archive/prfiles.js.map +1 -0
  193. package/dist/src/archive/report.js +169 -0
  194. package/dist/src/archive/report.js.map +1 -0
  195. package/dist/src/archive/restore.js +276 -0
  196. package/dist/src/archive/restore.js.map +1 -0
  197. package/dist/src/archive/rows.js +106 -0
  198. package/dist/src/archive/rows.js.map +1 -0
  199. package/dist/src/archive/seal.js +480 -0
  200. package/dist/src/archive/seal.js.map +1 -0
  201. package/dist/src/archive/status.js +528 -0
  202. package/dist/src/archive/status.js.map +1 -0
  203. package/dist/src/archive/storepass.js +409 -0
  204. package/dist/src/archive/storepass.js.map +1 -0
  205. package/dist/src/backfill.js +217 -0
  206. package/dist/src/backfill.js.map +1 -0
  207. package/dist/src/bench/cost.js +87 -0
  208. package/dist/src/bench/exec.js +76 -0
  209. package/dist/src/bench/values.js +14 -0
  210. package/dist/src/botcontrol.js +127 -0
  211. package/dist/src/botcontrol.js.map +1 -0
  212. package/dist/src/botworker.js +805 -0
  213. package/dist/src/botworker.js.map +1 -0
  214. package/dist/src/capture.js +234 -0
  215. package/dist/src/capture.js.map +1 -0
  216. package/dist/src/cases.js +1491 -0
  217. package/dist/src/cases.js.map +1 -0
  218. package/dist/src/channelfs.js +27 -0
  219. package/dist/src/channelfs.js.map +1 -0
  220. package/dist/src/checklist.js +312 -0
  221. package/dist/src/checklist.js.map +1 -0
  222. package/dist/src/ciretry.js +91 -0
  223. package/dist/src/ciretry.js.map +1 -0
  224. package/dist/src/cli.js +1916 -0
  225. package/dist/src/cli.js.map +1 -0
  226. package/dist/src/cliops.js +333 -0
  227. package/dist/src/cliops.js.map +1 -0
  228. package/dist/src/clonedir.js +15 -0
  229. package/dist/src/clonedir.js.map +1 -0
  230. package/dist/src/commandeffects.js +43 -0
  231. package/dist/src/commandeffects.js.map +1 -0
  232. package/dist/src/commands.js +1312 -0
  233. package/dist/src/commands.js.map +1 -0
  234. package/dist/src/companioninbox.js +203 -0
  235. package/dist/src/companioninbox.js.map +1 -0
  236. package/dist/src/compare.js +673 -0
  237. package/dist/src/compare.js.map +1 -0
  238. package/dist/src/config.js +1027 -0
  239. package/dist/src/config.js.map +1 -0
  240. package/dist/src/configcheck.js +111 -0
  241. package/dist/src/configcheck.js.map +1 -0
  242. package/dist/src/coreext.js +43 -0
  243. package/dist/src/coreext.js.map +1 -0
  244. package/dist/src/daemon.js +929 -0
  245. package/dist/src/daemon.js.map +1 -0
  246. package/dist/src/debuground.js +731 -0
  247. package/dist/src/debuground.js.map +1 -0
  248. package/dist/src/debugroundpi.js +219 -0
  249. package/dist/src/debugroundpi.js.map +1 -0
  250. package/dist/src/diagnostics.js +14 -0
  251. package/dist/src/diagnostics.js.map +1 -0
  252. package/dist/src/digest.js +139 -0
  253. package/dist/src/digest.js.map +1 -0
  254. package/dist/src/dispatch.js +1726 -0
  255. package/dist/src/dispatch.js.map +1 -0
  256. package/dist/src/engine/answerguard.js +18 -0
  257. package/dist/src/engine/answerguard.js.map +1 -0
  258. package/dist/src/engine/botsdk.js +262 -0
  259. package/dist/src/engine/botsdk.js.map +1 -0
  260. package/dist/src/engine/caserunner-sdk.js +332 -0
  261. package/dist/src/engine/caserunner-sdk.js.map +1 -0
  262. package/dist/src/engine/debugpi.js +194 -0
  263. package/dist/src/engine/debugpi.js.map +1 -0
  264. package/dist/src/eod.js +473 -0
  265. package/dist/src/eod.js.map +1 -0
  266. package/dist/src/eodpinwatch.js +391 -0
  267. package/dist/src/eodpinwatch.js.map +1 -0
  268. package/dist/src/escalation.js +1009 -0
  269. package/dist/src/escalation.js.map +1 -0
  270. package/dist/src/eventloop.js +779 -0
  271. package/dist/src/eventloop.js.map +1 -0
  272. package/dist/src/execboundary.js +430 -0
  273. package/dist/src/execboundary.js.map +1 -0
  274. package/dist/src/feedback.js +915 -0
  275. package/dist/src/feedback.js.map +1 -0
  276. package/dist/src/findingscheck.js +200 -0
  277. package/dist/src/findingscheck.js.map +1 -0
  278. package/dist/src/fsdenied.js +26 -0
  279. package/dist/src/fsdenied.js.map +1 -0
  280. package/dist/src/ghread.js +181 -0
  281. package/dist/src/ghread.js.map +1 -0
  282. package/dist/src/github.js +1063 -0
  283. package/dist/src/github.js.map +1 -0
  284. package/dist/src/homes.js +103 -0
  285. package/dist/src/homes.js.map +1 -0
  286. package/dist/src/hostroute.js +24 -0
  287. package/dist/src/hostroute.js.map +1 -0
  288. package/dist/src/improvements.js +625 -0
  289. package/dist/src/improvements.js.map +1 -0
  290. package/dist/src/install.js +946 -0
  291. package/dist/src/install.js.map +1 -0
  292. package/dist/src/installkind.js +40 -0
  293. package/dist/src/installkind.js.map +1 -0
  294. package/dist/src/ipc/capability.js +286 -0
  295. package/dist/src/ipc/capability.js.map +1 -0
  296. package/dist/src/ipc/client.js +226 -0
  297. package/dist/src/ipc/client.js.map +1 -0
  298. package/dist/src/ipc/control.js +70 -0
  299. package/dist/src/ipc/control.js.map +1 -0
  300. package/dist/src/ipc/ghreadshim.js +77 -0
  301. package/dist/src/ipc/ghreadshim.js.map +1 -0
  302. package/dist/src/ipc/launcher.js +65 -0
  303. package/dist/src/ipc/launcher.js.map +1 -0
  304. package/dist/src/ipc/permits.js +293 -0
  305. package/dist/src/ipc/permits.js.map +1 -0
  306. package/dist/src/ipc/postshim.js +137 -0
  307. package/dist/src/ipc/postshim.js.map +1 -0
  308. package/dist/src/ipc/protocol.js +239 -0
  309. package/dist/src/ipc/protocol.js.map +1 -0
  310. package/dist/src/ipc/server.js +312 -0
  311. package/dist/src/ipc/server.js.map +1 -0
  312. package/dist/src/ipc/workerspec.js +148 -0
  313. package/dist/src/ipc/workerspec.js.map +1 -0
  314. package/dist/src/learner/learner.js +1272 -0
  315. package/dist/src/learner/learner.js.map +1 -0
  316. package/dist/src/learner/miner.js +948 -0
  317. package/dist/src/learner/miner.js.map +1 -0
  318. package/dist/src/learner/packet.js +216 -0
  319. package/dist/src/learner/packet.js.map +1 -0
  320. package/dist/src/learner/receipt.js +135 -0
  321. package/dist/src/learner/receipt.js.map +1 -0
  322. package/dist/src/learner/status.js +38 -0
  323. package/dist/src/learner/status.js.map +1 -0
  324. package/dist/src/ledgercheck.js +120 -0
  325. package/dist/src/ledgercheck.js.map +1 -0
  326. package/dist/src/lessonledger.js +451 -0
  327. package/dist/src/lessonledger.js.map +1 -0
  328. package/dist/src/lessonselect.js +639 -0
  329. package/dist/src/lessonselect.js.map +1 -0
  330. package/dist/src/lessonshadow.js +540 -0
  331. package/dist/src/lessonshadow.js.map +1 -0
  332. package/dist/src/levers.js +302 -0
  333. package/dist/src/levers.js.map +1 -0
  334. package/dist/src/live/projection.js +450 -0
  335. package/dist/src/live/projection.js.map +1 -0
  336. package/dist/src/live/publish.js +83 -0
  337. package/dist/src/live/publish.js.map +1 -0
  338. package/dist/src/live/read.js +60 -0
  339. package/dist/src/live/read.js.map +1 -0
  340. package/dist/src/livecli.js +288 -0
  341. package/dist/src/livecli.js.map +1 -0
  342. package/dist/src/loginhint.js +42 -0
  343. package/dist/src/loginhint.js.map +1 -0
  344. package/dist/src/loginrefresh/atomic.js +66 -0
  345. package/dist/src/loginrefresh/atomic.js.map +1 -0
  346. package/dist/src/loginrefresh/lock.js +93 -0
  347. package/dist/src/loginrefresh/lock.js.map +1 -0
  348. package/dist/src/loginrefresh/main.js +58 -0
  349. package/dist/src/loginrefresh/main.js.map +1 -0
  350. package/dist/src/loginrefresh/pirefresh.js +49 -0
  351. package/dist/src/loginrefresh/pirefresh.js.map +1 -0
  352. package/dist/src/loginrefresh/refresher.js +286 -0
  353. package/dist/src/loginrefresh/refresher.js.map +1 -0
  354. package/dist/src/loginrefresh/supervisor.js +253 -0
  355. package/dist/src/loginrefresh/supervisor.js.map +1 -0
  356. package/dist/src/loginrefresh/types.js +4 -0
  357. package/dist/src/loginrefresh/types.js.map +1 -0
  358. package/dist/src/memory/context.js +251 -0
  359. package/dist/src/memory/context.js.map +1 -0
  360. package/dist/src/memory/datafence.js +5 -0
  361. package/dist/src/memory/datafence.js.map +1 -0
  362. package/dist/src/memory/delivery.js +131 -0
  363. package/dist/src/memory/delivery.js.map +1 -0
  364. package/dist/src/memory/eventtags.js +81 -0
  365. package/dist/src/memory/eventtags.js.map +1 -0
  366. package/dist/src/memory/health.js +713 -0
  367. package/dist/src/memory/health.js.map +1 -0
  368. package/dist/src/memory/import.js +504 -0
  369. package/dist/src/memory/import.js.map +1 -0
  370. package/dist/src/memory/layout.js +252 -0
  371. package/dist/src/memory/layout.js.map +1 -0
  372. package/dist/src/memory/legacy-file.js +100 -0
  373. package/dist/src/memory/legacy-file.js.map +1 -0
  374. package/dist/src/memory/lessons.js +370 -0
  375. package/dist/src/memory/lessons.js.map +1 -0
  376. package/dist/src/memory/ovsession.js +449 -0
  377. package/dist/src/memory/ovsession.js.map +1 -0
  378. package/dist/src/memory/relayout.js +259 -0
  379. package/dist/src/memory/relayout.js.map +1 -0
  380. package/dist/src/memory/roundactivity.js +180 -0
  381. package/dist/src/memory/roundactivity.js.map +1 -0
  382. package/dist/src/memory/selection.js +690 -0
  383. package/dist/src/memory/selection.js.map +1 -0
  384. package/dist/src/memory/shadowstatus.js +69 -0
  385. package/dist/src/memory/shadowstatus.js.map +1 -0
  386. package/dist/src/memory/store.js +83 -0
  387. package/dist/src/memory/store.js.map +1 -0
  388. package/dist/src/memory/textidentity.js +8 -0
  389. package/dist/src/memory/textidentity.js.map +1 -0
  390. package/dist/src/metrics.js +773 -0
  391. package/dist/src/metrics.js.map +1 -0
  392. package/dist/src/missingconfig.js +8 -0
  393. package/dist/src/missingconfig.js.map +1 -0
  394. package/dist/src/npmverbs.js +226 -0
  395. package/dist/src/npmverbs.js.map +1 -0
  396. package/dist/src/onboard/choices.js +191 -0
  397. package/dist/src/onboard/choices.js.map +1 -0
  398. package/dist/src/onboard/engine.js +344 -0
  399. package/dist/src/onboard/engine.js.map +1 -0
  400. package/dist/src/onboard/launch.js +83 -0
  401. package/dist/src/onboard/launch.js.map +1 -0
  402. package/dist/src/onboard/steps/appslug.js +211 -0
  403. package/dist/src/onboard/steps/appslug.js.map +1 -0
  404. package/dist/src/onboard/steps/bringup.js +408 -0
  405. package/dist/src/onboard/steps/bringup.js.map +1 -0
  406. package/dist/src/onboard/steps/compose.js +343 -0
  407. package/dist/src/onboard/steps/compose.js.map +1 -0
  408. package/dist/src/onboard/steps/configdelivery.js +50 -0
  409. package/dist/src/onboard/steps/configdelivery.js.map +1 -0
  410. package/dist/src/onboard/steps/configfill.js +1148 -0
  411. package/dist/src/onboard/steps/configfill.js.map +1 -0
  412. package/dist/src/onboard/steps/deploysh.js +80 -0
  413. package/dist/src/onboard/steps/deploysh.js.map +1 -0
  414. package/dist/src/onboard/steps/embeddings.js +472 -0
  415. package/dist/src/onboard/steps/embeddings.js.map +1 -0
  416. package/dist/src/onboard/steps/escalation.js +571 -0
  417. package/dist/src/onboard/steps/escalation.js.map +1 -0
  418. package/dist/src/onboard/steps/githubapp.js +1092 -0
  419. package/dist/src/onboard/steps/githubapp.js.map +1 -0
  420. package/dist/src/onboard/steps/golive.js +506 -0
  421. package/dist/src/onboard/steps/golive.js.map +1 -0
  422. package/dist/src/onboard/steps/index.js +67 -0
  423. package/dist/src/onboard/steps/index.js.map +1 -0
  424. package/dist/src/onboard/steps/ingress.js +762 -0
  425. package/dist/src/onboard/steps/ingress.js.map +1 -0
  426. package/dist/src/onboard/steps/inventory.js +420 -0
  427. package/dist/src/onboard/steps/inventory.js.map +1 -0
  428. package/dist/src/onboard/steps/llmauth.js +893 -0
  429. package/dist/src/onboard/steps/llmauth.js.map +1 -0
  430. package/dist/src/onboard/steps/memory.js +1060 -0
  431. package/dist/src/onboard/steps/memory.js.map +1 -0
  432. package/dist/src/onboard/steps/memorykeys.js +43 -0
  433. package/dist/src/onboard/steps/memorykeys.js.map +1 -0
  434. package/dist/src/onboard/steps/preflight.js +172 -0
  435. package/dist/src/onboard/steps/preflight.js.map +1 -0
  436. package/dist/src/onboard/steps/proof.js +502 -0
  437. package/dist/src/onboard/steps/proof.js.map +1 -0
  438. package/dist/src/onboard/steps/ssh.js +300 -0
  439. package/dist/src/onboard/steps/ssh.js.map +1 -0
  440. package/dist/src/onboard/steps/topology.js +291 -0
  441. package/dist/src/onboard/steps/topology.js.map +1 -0
  442. package/dist/src/onboard/steps/transport.js +511 -0
  443. package/dist/src/onboard/steps/transport.js.map +1 -0
  444. package/dist/src/onboard/steps/verifyup.js +362 -0
  445. package/dist/src/onboard/steps/verifyup.js.map +1 -0
  446. package/dist/src/onboard/steps/vmdeploy.js +368 -0
  447. package/dist/src/onboard/steps/vmdeploy.js.map +1 -0
  448. package/dist/src/onboard/toml.js +53 -0
  449. package/dist/src/onboard/toml.js.map +1 -0
  450. package/dist/src/opencli.js +508 -0
  451. package/dist/src/opencli.js.map +1 -0
  452. package/dist/src/openpr.js +457 -0
  453. package/dist/src/openpr.js.map +1 -0
  454. package/dist/src/openviking/client.js +821 -0
  455. package/dist/src/openviking/client.js.map +1 -0
  456. package/dist/src/openviking/context.js +218 -0
  457. package/dist/src/openviking/context.js.map +1 -0
  458. package/dist/src/openviking/embedder.js +457 -0
  459. package/dist/src/openviking/embedder.js.map +1 -0
  460. package/dist/src/openviking/gapsink.js +38 -0
  461. package/dist/src/openviking/gapsink.js.map +1 -0
  462. package/dist/src/openviking/health.js +69 -0
  463. package/dist/src/openviking/health.js.map +1 -0
  464. package/dist/src/openviking/index.js +24 -0
  465. package/dist/src/openviking/index.js.map +1 -0
  466. package/dist/src/openviking/lessonfind.js +54 -0
  467. package/dist/src/openviking/lessonfind.js.map +1 -0
  468. package/dist/src/openviking/probe.js +89 -0
  469. package/dist/src/openviking/probe.js.map +1 -0
  470. package/dist/src/openviking/proc.js +95 -0
  471. package/dist/src/openviking/proc.js.map +1 -0
  472. package/dist/src/openviking/provisionreport.js +144 -0
  473. package/dist/src/openviking/provisionreport.js.map +1 -0
  474. package/dist/src/openviking/reembed.js +317 -0
  475. package/dist/src/openviking/reembed.js.map +1 -0
  476. package/dist/src/openviking/relayout.js +153 -0
  477. package/dist/src/openviking/relayout.js.map +1 -0
  478. package/dist/src/openviking/sidecar-main.js +39 -0
  479. package/dist/src/openviking/sidecar-main.js.map +1 -0
  480. package/dist/src/openviking/sidecar.js +79 -0
  481. package/dist/src/openviking/sidecar.js.map +1 -0
  482. package/dist/src/openviking/store.js +1758 -0
  483. package/dist/src/openviking/store.js.map +1 -0
  484. package/dist/src/openviking/supervisor.js +422 -0
  485. package/dist/src/openviking/supervisor.js.map +1 -0
  486. package/dist/src/openviking/types.js +5 -0
  487. package/dist/src/openviking/types.js.map +1 -0
  488. package/dist/src/operator-recovery.js +172 -0
  489. package/dist/src/operator-recovery.js.map +1 -0
  490. package/dist/src/operatormessage.js +44 -0
  491. package/dist/src/operatormessage.js.map +1 -0
  492. package/dist/src/outcomes.js +1361 -0
  493. package/dist/src/outcomes.js.map +1 -0
  494. package/dist/src/piauthcopy.js +5 -0
  495. package/dist/src/piauthcopy.js.map +1 -0
  496. package/dist/src/picli.js +45 -0
  497. package/dist/src/picli.js.map +1 -0
  498. package/dist/src/picwd.js +25 -0
  499. package/dist/src/picwd.js.map +1 -0
  500. package/dist/src/pilogin.js +35 -0
  501. package/dist/src/pilogin.js.map +1 -0
  502. package/dist/src/pingpong.js +1028 -0
  503. package/dist/src/pingpong.js.map +1 -0
  504. package/dist/src/posting-journal.js +441 -0
  505. package/dist/src/posting-journal.js.map +1 -0
  506. package/dist/src/posting-outbox.js +662 -0
  507. package/dist/src/posting-outbox.js.map +1 -0
  508. package/dist/src/posting-receipt.js +203 -0
  509. package/dist/src/posting-receipt.js.map +1 -0
  510. package/dist/src/posting.js +348 -0
  511. package/dist/src/posting.js.map +1 -0
  512. package/dist/src/postops.js +637 -0
  513. package/dist/src/postops.js.map +1 -0
  514. package/dist/src/pr-activity.js +11 -0
  515. package/dist/src/pr-activity.js.map +1 -0
  516. package/dist/src/pr-author.js +5 -0
  517. package/dist/src/pr-author.js.map +1 -0
  518. package/dist/src/pr-conversation.js +355 -0
  519. package/dist/src/pr-conversation.js.map +1 -0
  520. package/dist/src/pr-session-lifecycle.js +542 -0
  521. package/dist/src/pr-session-lifecycle.js.map +1 -0
  522. package/dist/src/preflight.js +341 -0
  523. package/dist/src/preflight.js.map +1 -0
  524. package/dist/src/priorresolve.js +275 -0
  525. package/dist/src/priorresolve.js.map +1 -0
  526. package/dist/src/qcrbin.js +180 -0
  527. package/dist/src/qcrbin.js.map +1 -0
  528. package/dist/src/recheck.js +1398 -0
  529. package/dist/src/recheck.js.map +1 -0
  530. package/dist/src/reconcile.js +820 -0
  531. package/dist/src/reconcile.js.map +1 -0
  532. package/dist/src/recordedfindings.js +15 -0
  533. package/dist/src/recordedfindings.js.map +1 -0
  534. package/dist/src/regression.js +546 -0
  535. package/dist/src/regression.js.map +1 -0
  536. package/dist/src/replay.js +598 -0
  537. package/dist/src/replay.js.map +1 -0
  538. package/dist/src/requiredpasses.js +113 -0
  539. package/dist/src/requiredpasses.js.map +1 -0
  540. package/dist/src/resumecheck.js +168 -0
  541. package/dist/src/resumecheck.js.map +1 -0
  542. package/dist/src/reviewcli.js +671 -0
  543. package/dist/src/reviewcli.js.map +1 -0
  544. package/dist/src/reviewcontext.js +22 -0
  545. package/dist/src/reviewcontext.js.map +1 -0
  546. package/dist/src/reviewdetach.js +574 -0
  547. package/dist/src/reviewdetach.js.map +1 -0
  548. package/dist/src/reviewhistory.js +284 -0
  549. package/dist/src/reviewhistory.js.map +1 -0
  550. package/dist/src/reviewhome.js +26 -0
  551. package/dist/src/reviewhome.js.map +1 -0
  552. package/dist/src/reviewmemory.js +110 -0
  553. package/dist/src/reviewmemory.js.map +1 -0
  554. package/dist/src/reviewrendercli.js +104 -0
  555. package/dist/src/reviewrendercli.js.map +1 -0
  556. package/dist/src/round-work.js +20 -0
  557. package/dist/src/round-work.js.map +1 -0
  558. package/dist/src/runhost.js +72 -0
  559. package/dist/src/runhost.js.map +1 -0
  560. package/dist/src/scanauth.js +25 -0
  561. package/dist/src/scanauth.js.map +1 -0
  562. package/dist/src/scanbox.js +846 -0
  563. package/dist/src/scanbox.js.map +1 -0
  564. package/dist/src/scancli.js +1074 -0
  565. package/dist/src/scancli.js.map +1 -0
  566. package/dist/src/scandriverlog.js +30 -0
  567. package/dist/src/scandriverlog.js.map +1 -0
  568. package/dist/src/session-bridge.js +75 -0
  569. package/dist/src/session-bridge.js.map +1 -0
  570. package/dist/src/session-command-payload.js +29 -0
  571. package/dist/src/session-command-payload.js.map +1 -0
  572. package/dist/src/session-command-supervisor.js +939 -0
  573. package/dist/src/session-command-supervisor.js.map +1 -0
  574. package/dist/src/sessionjsonl.js +85 -0
  575. package/dist/src/sessionjsonl.js.map +1 -0
  576. package/dist/src/sessions.js +541 -0
  577. package/dist/src/sessions.js.map +1 -0
  578. package/dist/src/setup/adapters.js +330 -0
  579. package/dist/src/setup/adapters.js.map +1 -0
  580. package/dist/src/setup/baselines.js +64 -0
  581. package/dist/src/setup/baselines.js.map +1 -0
  582. package/dist/src/setup/caserunner.js +554 -0
  583. package/dist/src/setup/caserunner.js.map +1 -0
  584. package/dist/src/setup/configcommit.js +358 -0
  585. package/dist/src/setup/configcommit.js.map +1 -0
  586. package/dist/src/setup/engine.js +672 -0
  587. package/dist/src/setup/engine.js.map +1 -0
  588. package/dist/src/setup/facts.js +593 -0
  589. package/dist/src/setup/facts.js.map +1 -0
  590. package/dist/src/setup/findings.js +948 -0
  591. package/dist/src/setup/findings.js.map +1 -0
  592. package/dist/src/setup/history.js +338 -0
  593. package/dist/src/setup/history.js.map +1 -0
  594. package/dist/src/setup/interview.js +185 -0
  595. package/dist/src/setup/interview.js.map +1 -0
  596. package/dist/src/setup/ledger.js +207 -0
  597. package/dist/src/setup/ledger.js.map +1 -0
  598. package/dist/src/setup/pins.js +43 -0
  599. package/dist/src/setup/pins.js.map +1 -0
  600. package/dist/src/setup/portrait.js +432 -0
  601. package/dist/src/setup/portrait.js.map +1 -0
  602. package/dist/src/setup/presentation.js +9 -0
  603. package/dist/src/setup/presentation.js.map +1 -0
  604. package/dist/src/setup/profile.js +417 -0
  605. package/dist/src/setup/profile.js.map +1 -0
  606. package/dist/src/setup/replay.js +240 -0
  607. package/dist/src/setup/replay.js.map +1 -0
  608. package/dist/src/setup/replaybuild.js +311 -0
  609. package/dist/src/setup/replaybuild.js.map +1 -0
  610. package/dist/src/setup/replayrun.js +806 -0
  611. package/dist/src/setup/replayrun.js.map +1 -0
  612. package/dist/src/setup/replaysource.js +169 -0
  613. package/dist/src/setup/replaysource.js.map +1 -0
  614. package/dist/src/setup/retune.js +333 -0
  615. package/dist/src/setup/retune.js.map +1 -0
  616. package/dist/src/setup/retunelog.js +51 -0
  617. package/dist/src/setup/retunelog.js.map +1 -0
  618. package/dist/src/setup/scan.js +725 -0
  619. package/dist/src/setup/scan.js.map +1 -0
  620. package/dist/src/setup/scanbuild.js +133 -0
  621. package/dist/src/setup/scanbuild.js.map +1 -0
  622. package/dist/src/setup/scanfacts.js +96 -0
  623. package/dist/src/setup/scanfacts.js.map +1 -0
  624. package/dist/src/setup/scanlease.js +251 -0
  625. package/dist/src/setup/scanlease.js.map +1 -0
  626. package/dist/src/setup/scanreport.js +289 -0
  627. package/dist/src/setup/scanreport.js.map +1 -0
  628. package/dist/src/setup/scanrun.js +1733 -0
  629. package/dist/src/setup/scanrun.js.map +1 -0
  630. package/dist/src/setup/scantree.js +257 -0
  631. package/dist/src/setup/scantree.js.map +1 -0
  632. package/dist/src/setup/stage2.js +617 -0
  633. package/dist/src/setup/stage2.js.map +1 -0
  634. package/dist/src/setup/stage2request.js +259 -0
  635. package/dist/src/setup/stage2request.js.map +1 -0
  636. package/dist/src/setup/survey.js +307 -0
  637. package/dist/src/setup/survey.js.map +1 -0
  638. package/dist/src/setup/surveyrun.js +549 -0
  639. package/dist/src/setup/surveyrun.js.map +1 -0
  640. package/dist/src/setup/textsniff.js +89 -0
  641. package/dist/src/setup/textsniff.js.map +1 -0
  642. package/dist/src/setup/trackersink.js +395 -0
  643. package/dist/src/setup/trackersink.js.map +1 -0
  644. package/dist/src/setup/triage.js +636 -0
  645. package/dist/src/setup/triage.js.map +1 -0
  646. package/dist/src/setup/tuning.js +585 -0
  647. package/dist/src/setup/tuning.js.map +1 -0
  648. package/dist/src/setup/unitcut.js +437 -0
  649. package/dist/src/setup/unitcut.js.map +1 -0
  650. package/dist/src/singleflight.js +72 -0
  651. package/dist/src/singleflight.js.map +1 -0
  652. package/dist/src/slack/announce.js +725 -0
  653. package/dist/src/slack/announce.js.map +1 -0
  654. package/dist/src/slack/api.js +227 -0
  655. package/dist/src/slack/api.js.map +1 -0
  656. package/dist/src/slack/messages.js +158 -0
  657. package/dist/src/slack/messages.js.map +1 -0
  658. package/dist/src/slack/origin.js +550 -0
  659. package/dist/src/slack/origin.js.map +1 -0
  660. package/dist/src/slack/policy.js +225 -0
  661. package/dist/src/slack/policy.js.map +1 -0
  662. package/dist/src/slack/reply.js +153 -0
  663. package/dist/src/slack/reply.js.map +1 -0
  664. package/dist/src/slack/router.js +189 -0
  665. package/dist/src/slack/router.js.map +1 -0
  666. package/dist/src/slack/socket.js +323 -0
  667. package/dist/src/slack/socket.js.map +1 -0
  668. package/dist/src/slack/types.js +11 -0
  669. package/dist/src/slack/types.js.map +1 -0
  670. package/dist/src/sqlitehint.js +30 -0
  671. package/dist/src/sqlitehint.js.map +1 -0
  672. package/dist/src/ssh-observation.js +25 -0
  673. package/dist/src/ssh-observation.js.map +1 -0
  674. package/dist/src/state.js +5869 -0
  675. package/dist/src/state.js.map +1 -0
  676. package/dist/src/statemachine.js +220 -0
  677. package/dist/src/statemachine.js.map +1 -0
  678. package/dist/src/stubs.js +52 -0
  679. package/dist/src/stubs.js.map +1 -0
  680. package/dist/src/takeover.js +506 -0
  681. package/dist/src/takeover.js.map +1 -0
  682. package/dist/src/takeoverpi.js +249 -0
  683. package/dist/src/takeoverpi.js.map +1 -0
  684. package/dist/src/terminal-failure.js +26 -0
  685. package/dist/src/terminal-failure.js.map +1 -0
  686. package/dist/src/thread-resolution-refusal.js +4 -0
  687. package/dist/src/thread-resolution-refusal.js.map +1 -0
  688. package/dist/src/tmux.js +126 -0
  689. package/dist/src/tmux.js.map +1 -0
  690. package/dist/src/trackerid.js +43 -0
  691. package/dist/src/trackerid.js.map +1 -0
  692. package/dist/src/transcriptindex.js +512 -0
  693. package/dist/src/transcriptindex.js.map +1 -0
  694. package/dist/src/tui/actions.js +404 -0
  695. package/dist/src/tui/actions.js.map +1 -0
  696. package/dist/src/tui/alerts.js +523 -0
  697. package/dist/src/tui/alerts.js.map +1 -0
  698. package/dist/src/tui/app.js +1835 -0
  699. package/dist/src/tui/app.js.map +1 -0
  700. package/dist/src/tui/archiveops.js +164 -0
  701. package/dist/src/tui/archiveops.js.map +1 -0
  702. package/dist/src/tui/autofile.js +191 -0
  703. package/dist/src/tui/autofile.js.map +1 -0
  704. package/dist/src/tui/board.js +1143 -0
  705. package/dist/src/tui/board.js.map +1 -0
  706. package/dist/src/tui/candidates.js +699 -0
  707. package/dist/src/tui/candidates.js.map +1 -0
  708. package/dist/src/tui/cellwidth.js +151 -0
  709. package/dist/src/tui/cellwidth.js.map +1 -0
  710. package/dist/src/tui/changelog-store.js +404 -0
  711. package/dist/src/tui/changelog-store.js.map +1 -0
  712. package/dist/src/tui/chord.js +26 -0
  713. package/dist/src/tui/chord.js.map +1 -0
  714. package/dist/src/tui/clipboard.js +81 -0
  715. package/dist/src/tui/clipboard.js.map +1 -0
  716. package/dist/src/tui/cmdbar.js +253 -0
  717. package/dist/src/tui/cmdbar.js.map +1 -0
  718. package/dist/src/tui/companion.js +1275 -0
  719. package/dist/src/tui/companion.js.map +1 -0
  720. package/dist/src/tui/companionplan.js +200 -0
  721. package/dist/src/tui/companionplan.js.map +1 -0
  722. package/dist/src/tui/companiontab.js +61 -0
  723. package/dist/src/tui/companiontab.js.map +1 -0
  724. package/dist/src/tui/confighome.js +105 -0
  725. package/dist/src/tui/confighome.js.map +1 -0
  726. package/dist/src/tui/connect.js +44 -0
  727. package/dist/src/tui/connect.js.map +1 -0
  728. package/dist/src/tui/deploy-ceremony.js +75 -0
  729. package/dist/src/tui/deploy-ceremony.js.map +1 -0
  730. package/dist/src/tui/deploy-drain.js +93 -0
  731. package/dist/src/tui/deploy-drain.js.map +1 -0
  732. package/dist/src/tui/deploy-process.js +234 -0
  733. package/dist/src/tui/deploy-process.js.map +1 -0
  734. package/dist/src/tui/deploytab.js +1263 -0
  735. package/dist/src/tui/deploytab.js.map +1 -0
  736. package/dist/src/tui/design.js +111 -0
  737. package/dist/src/tui/design.js.map +1 -0
  738. package/dist/src/tui/digesttab.js +148 -0
  739. package/dist/src/tui/digesttab.js.map +1 -0
  740. package/dist/src/tui/emptystate.js +32 -0
  741. package/dist/src/tui/emptystate.js.map +1 -0
  742. package/dist/src/tui/evidence.js +790 -0
  743. package/dist/src/tui/evidence.js.map +1 -0
  744. package/dist/src/tui/feed.js +415 -0
  745. package/dist/src/tui/feed.js.map +1 -0
  746. package/dist/src/tui/feedbacktab.js +907 -0
  747. package/dist/src/tui/feedbacktab.js.map +1 -0
  748. package/dist/src/tui/filters.js +72 -0
  749. package/dist/src/tui/filters.js.map +1 -0
  750. package/dist/src/tui/ghread.js +272 -0
  751. package/dist/src/tui/ghread.js.map +1 -0
  752. package/dist/src/tui/historypull.js +61 -0
  753. package/dist/src/tui/historypull.js.map +1 -0
  754. package/dist/src/tui/inboxpull.js +197 -0
  755. package/dist/src/tui/inboxpull.js.map +1 -0
  756. package/dist/src/tui/index.js +1425 -0
  757. package/dist/src/tui/index.js.map +1 -0
  758. package/dist/src/tui/issuesink.js +206 -0
  759. package/dist/src/tui/issuesink.js.map +1 -0
  760. package/dist/src/tui/jsonfile.js +32 -0
  761. package/dist/src/tui/jsonfile.js.map +1 -0
  762. package/dist/src/tui/keys.js +97 -0
  763. package/dist/src/tui/keys.js.map +1 -0
  764. package/dist/src/tui/layout.js +70 -0
  765. package/dist/src/tui/layout.js.map +1 -0
  766. package/dist/src/tui/learning.js +469 -0
  767. package/dist/src/tui/learning.js.map +1 -0
  768. package/dist/src/tui/lessonswitch.js +103 -0
  769. package/dist/src/tui/lessonswitch.js.map +1 -0
  770. package/dist/src/tui/livecontrol.js +167 -0
  771. package/dist/src/tui/livecontrol.js.map +1 -0
  772. package/dist/src/tui/memforward.js +310 -0
  773. package/dist/src/tui/memforward.js.map +1 -0
  774. package/dist/src/tui/memincident.js +287 -0
  775. package/dist/src/tui/memincident.js.map +1 -0
  776. package/dist/src/tui/memory.js +485 -0
  777. package/dist/src/tui/memory.js.map +1 -0
  778. package/dist/src/tui/memtab.js +513 -0
  779. package/dist/src/tui/memtab.js.map +1 -0
  780. package/dist/src/tui/metricstab.js +979 -0
  781. package/dist/src/tui/metricstab.js.map +1 -0
  782. package/dist/src/tui/model.js +104 -0
  783. package/dist/src/tui/model.js.map +1 -0
  784. package/dist/src/tui/mouse.js +83 -0
  785. package/dist/src/tui/mouse.js.map +1 -0
  786. package/dist/src/tui/msg.js +196 -0
  787. package/dist/src/tui/msg.js.map +1 -0
  788. package/dist/src/tui/nativecompanion.js +321 -0
  789. package/dist/src/tui/nativecompanion.js.map +1 -0
  790. package/dist/src/tui/openurl.js +17 -0
  791. package/dist/src/tui/openurl.js.map +1 -0
  792. package/dist/src/tui/paint.js +501 -0
  793. package/dist/src/tui/paint.js.map +1 -0
  794. package/dist/src/tui/panes.js +74 -0
  795. package/dist/src/tui/panes.js.map +1 -0
  796. package/dist/src/tui/pipane.js +166 -0
  797. package/dist/src/tui/pipane.js.map +1 -0
  798. package/dist/src/tui/poll-hooks.js +11 -0
  799. package/dist/src/tui/poll-hooks.js.map +1 -0
  800. package/dist/src/tui/poll.js +113 -0
  801. package/dist/src/tui/poll.js.map +1 -0
  802. package/dist/src/tui/proposals.js +715 -0
  803. package/dist/src/tui/proposals.js.map +1 -0
  804. package/dist/src/tui/prpause.js +132 -0
  805. package/dist/src/tui/prpause.js.map +1 -0
  806. package/dist/src/tui/reload.js +351 -0
  807. package/dist/src/tui/reload.js.map +1 -0
  808. package/dist/src/tui/remote.js +312 -0
  809. package/dist/src/tui/remote.js.map +1 -0
  810. package/dist/src/tui/rollover.js +767 -0
  811. package/dist/src/tui/rollover.js.map +1 -0
  812. package/dist/src/tui/scantab.js +1053 -0
  813. package/dist/src/tui/scantab.js.map +1 -0
  814. package/dist/src/tui/setupentry.js +81 -0
  815. package/dist/src/tui/setupentry.js.map +1 -0
  816. package/dist/src/tui/sidebar.js +602 -0
  817. package/dist/src/tui/sidebar.js.map +1 -0
  818. package/dist/src/tui/sshtriage.js +393 -0
  819. package/dist/src/tui/sshtriage.js.map +1 -0
  820. package/dist/src/tui/startup.js +172 -0
  821. package/dist/src/tui/startup.js.map +1 -0
  822. package/dist/src/tui/tabs.js +41 -0
  823. package/dist/src/tui/tabs.js.map +1 -0
  824. package/dist/src/tui/termmirror.js +217 -0
  825. package/dist/src/tui/termmirror.js.map +1 -0
  826. package/dist/src/tui/theme.js +95 -0
  827. package/dist/src/tui/theme.js.map +1 -0
  828. package/dist/src/tui/ticketstab.js +224 -0
  829. package/dist/src/tui/ticketstab.js.map +1 -0
  830. package/dist/src/tui/tmuxsocket.js +101 -0
  831. package/dist/src/tui/tmuxsocket.js.map +1 -0
  832. package/dist/src/tui/trackertickets.js +77 -0
  833. package/dist/src/tui/trackertickets.js.map +1 -0
  834. package/dist/src/tui/unwedge.js +245 -0
  835. package/dist/src/tui/unwedge.js.map +1 -0
  836. package/dist/src/tui/viewer.js +479 -0
  837. package/dist/src/tui/viewer.js.map +1 -0
  838. package/dist/src/tui/viewerinput.js +125 -0
  839. package/dist/src/tui/viewerinput.js.map +1 -0
  840. package/dist/src/updatenotice.js +143 -0
  841. package/dist/src/updatenotice.js.map +1 -0
  842. package/dist/src/usage.js +1007 -0
  843. package/dist/src/usage.js.map +1 -0
  844. package/dist/src/verdicts.js +3848 -0
  845. package/dist/src/verdicts.js.map +1 -0
  846. package/dist/src/version.js +58 -0
  847. package/dist/src/version.js.map +1 -0
  848. package/dist/src/wakes.js +411 -0
  849. package/dist/src/wakes.js.map +1 -0
  850. package/dist/src/webhook.js +725 -0
  851. package/dist/src/webhook.js.map +1 -0
  852. package/dist/src/webhookheal.js +238 -0
  853. package/dist/src/webhookheal.js.map +1 -0
  854. package/dist/src/wire.js +1801 -0
  855. package/dist/src/wire.js.map +1 -0
  856. package/dist/src/worker-session-host.js +531 -0
  857. package/dist/src/worker-session-host.js.map +1 -0
  858. package/dist/src/workerbreaker.js +112 -0
  859. package/dist/src/workerbreaker.js.map +1 -0
  860. package/dist/src/workercontrol.js +49 -0
  861. package/dist/src/workercontrol.js.map +1 -0
  862. package/dist/src/workerlease.js +351 -0
  863. package/dist/src/workerlease.js.map +1 -0
  864. package/dist/src/workerliveness.js +84 -0
  865. package/dist/src/workerliveness.js.map +1 -0
  866. package/dist/src/workerlog.js +65 -0
  867. package/dist/src/workerlog.js.map +1 -0
  868. package/dist/src/workerruntime.js +10 -0
  869. package/dist/src/workerruntime.js.map +1 -0
  870. package/dist/src/workersupervisor.js +1206 -0
  871. package/dist/src/workersupervisor.js.map +1 -0
  872. package/dist/src/workerusage.js +23 -0
  873. package/dist/src/workerusage.js.map +1 -0
  874. package/dist/src/workerworld.js +332 -0
  875. package/dist/src/workerworld.js.map +1 -0
  876. package/dist/src/worktrees.js +984 -0
  877. package/dist/src/worktrees.js.map +1 -0
  878. package/docker/Dockerfile +284 -0
  879. package/docker/Dockerfile.dockerignore +35 -0
  880. package/docker/README.md +221 -0
  881. package/docker/companion/Dockerfile +139 -0
  882. package/docker/companion/agents/evidence.md +34 -0
  883. package/docker/companion/agents/explore.md +26 -0
  884. package/docker/companion/agents/pass-survey.md +49 -0
  885. package/docker/companion/agents/study.md +26 -0
  886. package/docker/companion/entrypoint.sh +343 -0
  887. package/docker/companion/memory-health.mjs +269 -0
  888. package/docker/entrypoint.sh +782 -0
  889. package/docker/memory/Dockerfile +191 -0
  890. package/docker/memory/Dockerfile.dockerignore +20 -0
  891. package/docker/memory/README.md +60 -0
  892. package/docker/memory/entrypoint.sh +163 -0
  893. package/docker/setid/qcr-setid.c +109 -0
  894. package/docker/tunnel/Dockerfile.cloudflared +51 -0
  895. package/docker/tunnel/Dockerfile.ngrok +40 -0
  896. package/docker/tunnel/sidecar.mjs +411 -0
  897. package/docker/vm/tmux.conf +26 -0
  898. package/docker-compose.yml +348 -0
  899. package/extensions/cli/bash-guard-ambient.ts +71 -0
  900. package/extensions/cli/bash-guard.ts +12 -0
  901. package/extensions/cli/companion-guard.ts +8 -0
  902. package/extensions/cli/debug-round.ts +9 -0
  903. package/extensions/cli/inject-bridge.ts +8 -0
  904. package/extensions/cli/ledger-pin.ts +13 -0
  905. package/extensions/cli/lens.ts +22 -0
  906. package/extensions/cli/muted-theme.ts +9 -0
  907. package/extensions/cli/onboarding.ts +8 -0
  908. package/extensions/cli/setup.ts +8 -0
  909. package/extensions/cli/tui-view.ts +8 -0
  910. package/extensions/companion-guard.ts +411 -0
  911. package/extensions/debug-round.ts +281 -0
  912. package/extensions/inject-bridge.ts +630 -0
  913. package/extensions/inline-bash.ts +102 -0
  914. package/extensions/muted-theme.json +68 -0
  915. package/extensions/muted-theme.ts +23 -0
  916. package/extensions/onboarding.ts +233 -0
  917. package/extensions/setup-replay.ts +336 -0
  918. package/extensions/setup-scan.ts +1236 -0
  919. package/extensions/setup-triage.ts +190 -0
  920. package/extensions/setup.ts +1428 -0
  921. package/extensions/tui-view.ts +489 -0
  922. package/extensions/vendor/openviking-pi/LICENSE +201 -0
  923. package/extensions/vendor/openviking-pi/NOTICE +68 -0
  924. package/extensions/vendor/openviking-pi/README.md +550 -0
  925. package/extensions/vendor/openviking-pi/client.ts +588 -0
  926. package/extensions/vendor/openviking-pi/config.json +28 -0
  927. package/extensions/vendor/openviking-pi/config.ts +271 -0
  928. package/extensions/vendor/openviking-pi/index.ts +523 -0
  929. package/extensions/vendor/openviking-pi/lib/capture-adapter.d.mts +10 -0
  930. package/extensions/vendor/openviking-pi/lib/capture-adapter.mjs +88 -0
  931. package/extensions/vendor/openviking-pi/lib/takeover-core.d.mts +80 -0
  932. package/extensions/vendor/openviking-pi/lib/takeover-core.mjs +406 -0
  933. package/extensions/vendor/openviking-pi/lib/uri-guard-adapter.d.mts +1 -0
  934. package/extensions/vendor/openviking-pi/lib/uri-guard-adapter.mjs +42 -0
  935. package/extensions/vendor/openviking-pi/patches/0000-vendoring-and-build-conformance.patch +726 -0
  936. package/extensions/vendor/openviking-pi/patches/0001-remove-session-before-compact-handler.patch +101 -0
  937. package/extensions/vendor/openviking-pi/patches/0002-no-ambient-work-in-subagent-leaves.patch +202 -0
  938. package/extensions/vendor/openviking-pi/patches/0003-never-auto-create-openviking-sessions.patch +120 -0
  939. package/extensions/vendor/openviking-pi/patches/0004-viking-write-durable-batch-write.patch +521 -0
  940. package/extensions/vendor/openviking-pi/patches/0005-second-memory-namespace.patch +786 -0
  941. package/extensions/vendor/openviking-pi/patches/0006-writes-that-stick-and-honest-errors.patch +996 -0
  942. package/extensions/vendor/openviking-pi/patches/0007-bounded-recall-no-commit.patch +794 -0
  943. package/extensions/vendor/openviking-pi/patches/0008-pr-derived-recall.patch +379 -0
  944. package/extensions/vendor/openviking-pi/patches/0009-pr-session-tools-in-round-areas.patch +375 -0
  945. package/extensions/vendor/openviking-pi/patches/0010-typed-lessons.patch +448 -0
  946. package/extensions/vendor/openviking-pi/patches/0011-review-session-memory-boundary.patch +382 -0
  947. package/extensions/vendor/openviking-pi/patches/0012-typecheck-against-the-real-pi.patch +129 -0
  948. package/extensions/vendor/openviking-pi/patches/0013-createopenviking-factory.patch +1168 -0
  949. package/extensions/vendor/openviking-pi/qcr-hint.ts +313 -0
  950. package/extensions/vendor/openviking-pi/qcr-honest.ts +183 -0
  951. package/extensions/vendor/openviking-pi/qcr-hooks.ts +87 -0
  952. package/extensions/vendor/openviking-pi/qcr-leaf.ts +78 -0
  953. package/extensions/vendor/openviking-pi/qcr-lesson.ts +231 -0
  954. package/extensions/vendor/openviking-pi/qcr-namespaces.ts +198 -0
  955. package/extensions/vendor/openviking-pi/qcr-recall.ts +288 -0
  956. package/extensions/vendor/openviking-pi/qcr-shim.ts +142 -0
  957. package/extensions/vendor/openviking-pi/qcr-write.ts +427 -0
  958. package/extensions/vendor/openviking-pi/recall.ts +149 -0
  959. package/extensions/vendor/openviking-pi/shared/capture-utils.d.mts +9 -0
  960. package/extensions/vendor/openviking-pi/shared/capture-utils.mjs +515 -0
  961. package/extensions/vendor/openviking-pi/shared/credentials.d.mts +14 -0
  962. package/extensions/vendor/openviking-pi/shared/credentials.mjs +228 -0
  963. package/extensions/vendor/openviking-pi/shared/pending-queue.d.mts +19 -0
  964. package/extensions/vendor/openviking-pi/shared/pending-queue.mjs +458 -0
  965. package/extensions/vendor/openviking-pi/shared/profile-inject.d.mts +17 -0
  966. package/extensions/vendor/openviking-pi/shared/profile-inject.mjs +294 -0
  967. package/extensions/vendor/openviking-pi/shared/recall-compress-core.mjs +195 -0
  968. package/extensions/vendor/openviking-pi/shared/recall-core.d.mts +13 -0
  969. package/extensions/vendor/openviking-pi/shared/recall-core.mjs +609 -0
  970. package/extensions/vendor/openviking-pi/shared/retryable.mjs +9 -0
  971. package/extensions/vendor/openviking-pi/shared/session-model.d.mts +3 -0
  972. package/extensions/vendor/openviking-pi/shared/session-model.mjs +57 -0
  973. package/extensions/vendor/openviking-pi/shared/uri-guard.mjs +56 -0
  974. package/extensions/vendor/openviking-pi/shared/workspace-peer.d.mts +5 -0
  975. package/extensions/vendor/openviking-pi/shared/workspace-peer.mjs +16 -0
  976. package/extensions/vendor/openviking-pi/sync.ts +163 -0
  977. package/extensions/vendor/openviking-pi/takeover.ts +36 -0
  978. package/extensions/vendor/openviking-pi/tools.ts +479 -0
  979. package/extensions/vendor/openviking-pi/upstream.json +48 -0
  980. package/package.json +138 -4
  981. package/profiles/default/README.md +21 -0
  982. package/profiles/default/addenda/.gitkeep +0 -0
  983. package/profiles/default/profile.toml +17 -0
  984. package/profiles/example-django/README.md +21 -0
  985. package/profiles/example-django/addenda/10-migration-discipline.md +13 -0
  986. package/profiles/example-django/addenda/20-new-column-nullability.md +12 -0
  987. package/profiles/example-django/addenda/30-test-doctrine.md +12 -0
  988. package/profiles/example-django/addenda/40-orm-usage.md +19 -0
  989. package/profiles/example-django/addenda/50-declared-shape.md +10 -0
  990. package/profiles/example-django/addenda/60-config-placement.md +14 -0
  991. package/profiles/example-django/heuristics.toml +24 -0
  992. package/profiles/example-django/profile.toml +9 -0
  993. package/profiles/fast/README.md +24 -0
  994. package/profiles/fast/profile.toml +59 -0
  995. package/profiles/minimal/README.md +17 -0
  996. package/profiles/minimal/addenda/.gitkeep +0 -0
  997. package/profiles/minimal/profile.toml +8 -0
  998. package/profiles/snappy/README.md +24 -0
  999. package/profiles/snappy/addenda/10-race-as-question.md +6 -0
  1000. package/profiles/snappy/profile.toml +46 -0
  1001. package/prompts/companion-day-open.md +46 -0
  1002. package/prompts/companion-first-run.md +104 -0
  1003. package/prompts/companion.md +821 -0
  1004. package/prompts/conversation-system.md +117 -0
  1005. package/prompts/eod-sweep.md +120 -0
  1006. package/prompts/onboarding.md +74 -0
  1007. package/prompts/pass-common.md +52 -0
  1008. package/prompts/pass-quality-common.md +22 -0
  1009. package/prompts/round-first.md +874 -0
  1010. package/prompts/round-follow-up.md +991 -0
  1011. package/prompts/round-re-review.md +889 -0
  1012. package/prompts/round-recheck.md +196 -0
  1013. package/prompts/round-staleness.md +856 -0
  1014. package/schemas/review-result.v1.json +391 -0
  1015. package/schemas/scan-result.v1.json +421 -0
  1016. package/schemas/version.v1.json +57 -0
  1017. package/scripts/approve-pr.sh +16 -0
  1018. package/scripts/build-stamp.mjs +67 -0
  1019. package/scripts/commit-verdict.sh +155 -0
  1020. package/scripts/companion-e2e.sh +250 -0
  1021. package/scripts/companion-memory-forward.mjs +1966 -0
  1022. package/scripts/config-fingerprint.sh +28 -0
  1023. package/scripts/deploy-manifest.mjs +141 -0
  1024. package/scripts/deploy.sh +1435 -0
  1025. package/scripts/directory-lock.mjs +193 -0
  1026. package/scripts/heartbeat-fresh.mjs +54 -0
  1027. package/scripts/install-locator.sh +328 -0
  1028. package/scripts/install-wrapper.sh +76 -0
  1029. package/scripts/memory-search-check.mjs +98 -0
  1030. package/scripts/mint-token.sh +212 -0
  1031. package/scripts/openviking-embedder.py +392 -0
  1032. package/scripts/openviking-ov-conf.mjs +161 -0
  1033. package/scripts/openviking-provision.mjs +1122 -0
  1034. package/scripts/openviking-reembed.mjs +1154 -0
  1035. package/scripts/post-review.sh +16 -0
  1036. package/scripts/pr-checks.sh +256 -0
  1037. package/scripts/pr-context.py +174 -0
  1038. package/scripts/pr-context.sh +63 -0
  1039. package/scripts/pr-self.sh +144 -0
  1040. package/scripts/pr-threads.sh +145 -0
  1041. package/scripts/qcr +15 -0
  1042. package/scripts/qcr-host.sh +769 -0
  1043. package/scripts/render-review.sh +24 -0
  1044. package/scripts/reply-thread.sh +16 -0
  1045. package/scripts/required-passes.py +112 -0
  1046. package/scripts/smoke-companion.sh +211 -0
  1047. package/scripts/tui-deploy-runner.mjs +61 -0
  1048. package/scripts/verify-dist.mjs +201 -0
  1049. package/scripts/vm-ghcr-login.sh +188 -0
  1050. package/seeds/agents/pass-baseline.md +61 -0
  1051. package/seeds/agents/pass-contracts.md +30 -0
  1052. package/seeds/agents/pass-data-safety.md +54 -0
  1053. package/seeds/agents/pass-delivery.md +37 -0
  1054. package/seeds/agents/pass-prod-risk.md +24 -0
  1055. package/seeds/agents/pass-security.md +31 -0
  1056. package/seeds/agents/pass-statelessness.md +27 -0
  1057. package/seeds/agents/pass-ui.md +23 -0
  1058. package/seeds/agents/pass-validate.md +50 -0
  1059. package/seeds/prompts/conversation-system.md +117 -0
  1060. package/seeds/prompts/eod-sweep.md +120 -0
  1061. package/seeds/prompts/pass-common.md +52 -0
  1062. package/skills/companion/adjudicate/SKILL.md +247 -0
  1063. package/skills/companion/evidence/SKILL.md +150 -0
  1064. package/skills/companion/land/SKILL.md +200 -0
  1065. package/skills/companion/replay/SKILL.md +138 -0
  1066. package/skills/companion/setup/SKILL.md +167 -0
  1067. package/skills/companion/survey/SKILL.md +160 -0
  1068. package/skills/selfimprove/README.md +34 -0
  1069. package/skills/selfimprove/SKILL.md +127 -0
  1070. package/skills/selfimprove/agents/openai.yaml +2 -0
  1071. package/skills/selfimprove/commands/selfimprove.md +6 -0
  1072. package/skills/selfimprove/references/fix-loop.md +49 -0
  1073. package/skills/selfimprove/references/ledger.md +20 -0
  1074. package/skills/selfimprove/references/setup.md +40 -0
  1075. package/src/anomalies.ts +566 -0
  1076. package/src/answerworker.ts +251 -0
  1077. package/src/appstamp.ts +65 -0
  1078. package/src/archive/admission.ts +3 -0
  1079. package/src/archive/archivedb.ts +548 -0
  1080. package/src/archive/archiver.ts +1963 -0
  1081. package/src/archive/archivewrite.ts +279 -0
  1082. package/src/archive/bundle.ts +333 -0
  1083. package/src/archive/dbmove.ts +911 -0
  1084. package/src/archive/duplicatecounts.ts +98 -0
  1085. package/src/archive/eligibility.ts +181 -0
  1086. package/src/archive/extract.ts +342 -0
  1087. package/src/archive/fsutil.ts +415 -0
  1088. package/src/archive/gzfile.ts +370 -0
  1089. package/src/archive/kinds.ts +21 -0
  1090. package/src/archive/livecheckpoint.ts +217 -0
  1091. package/src/archive/liveretention.ts +180 -0
  1092. package/src/archive/manifest.ts +519 -0
  1093. package/src/archive/overseer.ts +336 -0
  1094. package/src/archive/piglobal.ts +174 -0
  1095. package/src/archive/plan.ts +612 -0
  1096. package/src/archive/prarchive.ts +984 -0
  1097. package/src/archive/prfiles.ts +406 -0
  1098. package/src/archive/report.ts +252 -0
  1099. package/src/archive/restore.ts +330 -0
  1100. package/src/archive/rows.ts +187 -0
  1101. package/src/archive/seal.ts +630 -0
  1102. package/src/archive/status.ts +719 -0
  1103. package/src/archive/storepass.ts +581 -0
  1104. package/src/backfill.ts +362 -0
  1105. package/src/bench/cost.ts +113 -0
  1106. package/src/bench/exec.ts +90 -0
  1107. package/src/bench/values.ts +13 -0
  1108. package/src/botcontrol.ts +176 -0
  1109. package/src/botworker.ts +953 -0
  1110. package/src/capture.ts +321 -0
  1111. package/src/cases.ts +1789 -0
  1112. package/src/channelfs.ts +26 -0
  1113. package/src/checklist.ts +506 -0
  1114. package/src/ciretry.ts +141 -0
  1115. package/src/cli.ts +2271 -0
  1116. package/src/cliops.ts +384 -0
  1117. package/src/clonedir.ts +15 -0
  1118. package/src/commandeffects.ts +57 -0
  1119. package/src/commands.ts +1678 -0
  1120. package/src/companioninbox.ts +239 -0
  1121. package/src/compare.ts +957 -0
  1122. package/src/config.ts +2050 -0
  1123. package/src/configcheck.ts +133 -0
  1124. package/src/coreext.ts +47 -0
  1125. package/src/daemon.ts +1302 -0
  1126. package/src/debuground.ts +927 -0
  1127. package/src/debugroundpi.ts +262 -0
  1128. package/src/diagnostics.ts +14 -0
  1129. package/src/digest.ts +183 -0
  1130. package/src/dispatch.ts +2250 -0
  1131. package/src/engine/answerguard.ts +27 -0
  1132. package/src/engine/botsdk.ts +336 -0
  1133. package/src/engine/caserunner-sdk.ts +423 -0
  1134. package/src/engine/debugpi.ts +235 -0
  1135. package/src/eod.ts +669 -0
  1136. package/src/eodpinwatch.ts +556 -0
  1137. package/src/escalation.ts +1253 -0
  1138. package/src/eventloop.ts +1045 -0
  1139. package/src/execboundary.ts +633 -0
  1140. package/src/feedback.ts +1162 -0
  1141. package/src/findingscheck.ts +298 -0
  1142. package/src/fsdenied.ts +24 -0
  1143. package/src/ghread.ts +242 -0
  1144. package/src/github.ts +1497 -0
  1145. package/src/homes.ts +175 -0
  1146. package/src/hostroute.ts +53 -0
  1147. package/src/improvements.ts +787 -0
  1148. package/src/install.ts +1103 -0
  1149. package/src/installkind.ts +50 -0
  1150. package/src/ipc/capability.ts +333 -0
  1151. package/src/ipc/client.ts +269 -0
  1152. package/src/ipc/control.ts +90 -0
  1153. package/src/ipc/ghreadshim.ts +94 -0
  1154. package/src/ipc/launcher.ts +65 -0
  1155. package/src/ipc/permits.ts +362 -0
  1156. package/src/ipc/postshim.ts +164 -0
  1157. package/src/ipc/protocol.ts +342 -0
  1158. package/src/ipc/server.ts +371 -0
  1159. package/src/ipc/workerspec.ts +218 -0
  1160. package/src/learner/learner.ts +1655 -0
  1161. package/src/learner/miner.ts +1290 -0
  1162. package/src/learner/packet.ts +340 -0
  1163. package/src/learner/receipt.ts +168 -0
  1164. package/src/learner/status.ts +107 -0
  1165. package/src/ledgercheck.ts +157 -0
  1166. package/src/lessonledger.ts +703 -0
  1167. package/src/lessonselect.ts +888 -0
  1168. package/src/lessonshadow.ts +810 -0
  1169. package/src/levers.ts +393 -0
  1170. package/src/live/projection.ts +491 -0
  1171. package/src/live/publish.ts +92 -0
  1172. package/src/live/read.ts +86 -0
  1173. package/src/livecli.ts +336 -0
  1174. package/src/loginhint.ts +56 -0
  1175. package/src/loginrefresh/atomic.ts +76 -0
  1176. package/src/loginrefresh/lock.ts +104 -0
  1177. package/src/loginrefresh/main.ts +59 -0
  1178. package/src/loginrefresh/pirefresh.ts +57 -0
  1179. package/src/loginrefresh/refresher.ts +326 -0
  1180. package/src/loginrefresh/supervisor.ts +273 -0
  1181. package/src/loginrefresh/types.ts +35 -0
  1182. package/src/memory/context.ts +385 -0
  1183. package/src/memory/datafence.ts +9 -0
  1184. package/src/memory/delivery.ts +171 -0
  1185. package/src/memory/eventtags.ts +80 -0
  1186. package/src/memory/health.ts +953 -0
  1187. package/src/memory/import.ts +664 -0
  1188. package/src/memory/layout.ts +261 -0
  1189. package/src/memory/legacy-file.ts +104 -0
  1190. package/src/memory/lessons.ts +447 -0
  1191. package/src/memory/ovsession.ts +601 -0
  1192. package/src/memory/relayout.ts +335 -0
  1193. package/src/memory/roundactivity.ts +209 -0
  1194. package/src/memory/selection.ts +989 -0
  1195. package/src/memory/shadowstatus.ts +125 -0
  1196. package/src/memory/store.ts +275 -0
  1197. package/src/memory/textidentity.ts +8 -0
  1198. package/src/metrics.ts +1056 -0
  1199. package/src/missingconfig.ts +7 -0
  1200. package/src/npmverbs.ts +264 -0
  1201. package/src/onboard/choices.ts +222 -0
  1202. package/src/onboard/engine.ts +518 -0
  1203. package/src/onboard/launch.ts +115 -0
  1204. package/src/onboard/steps/appslug.ts +265 -0
  1205. package/src/onboard/steps/bringup.ts +482 -0
  1206. package/src/onboard/steps/compose.ts +459 -0
  1207. package/src/onboard/steps/configdelivery.ts +58 -0
  1208. package/src/onboard/steps/configfill.ts +1352 -0
  1209. package/src/onboard/steps/deploysh.ts +105 -0
  1210. package/src/onboard/steps/embeddings.ts +550 -0
  1211. package/src/onboard/steps/escalation.ts +640 -0
  1212. package/src/onboard/steps/githubapp.ts +1414 -0
  1213. package/src/onboard/steps/golive.ts +577 -0
  1214. package/src/onboard/steps/index.ts +71 -0
  1215. package/src/onboard/steps/ingress.ts +907 -0
  1216. package/src/onboard/steps/inventory.ts +526 -0
  1217. package/src/onboard/steps/llmauth.ts +1095 -0
  1218. package/src/onboard/steps/memory.ts +1290 -0
  1219. package/src/onboard/steps/memorykeys.ts +85 -0
  1220. package/src/onboard/steps/preflight.ts +191 -0
  1221. package/src/onboard/steps/proof.ts +624 -0
  1222. package/src/onboard/steps/ssh.ts +377 -0
  1223. package/src/onboard/steps/topology.ts +337 -0
  1224. package/src/onboard/steps/transport.ts +665 -0
  1225. package/src/onboard/steps/verifyup.ts +448 -0
  1226. package/src/onboard/steps/vmdeploy.ts +442 -0
  1227. package/src/onboard/toml.ts +50 -0
  1228. package/src/opencli.ts +591 -0
  1229. package/src/openpr.ts +568 -0
  1230. package/src/openviking/client.ts +1131 -0
  1231. package/src/openviking/context.ts +263 -0
  1232. package/src/openviking/embedder.ts +516 -0
  1233. package/src/openviking/gapsink.ts +63 -0
  1234. package/src/openviking/health.ts +83 -0
  1235. package/src/openviking/index.ts +119 -0
  1236. package/src/openviking/lessonfind.ts +92 -0
  1237. package/src/openviking/probe.ts +129 -0
  1238. package/src/openviking/proc.ts +110 -0
  1239. package/src/openviking/provisionreport.ts +160 -0
  1240. package/src/openviking/reembed.ts +412 -0
  1241. package/src/openviking/relayout.ts +177 -0
  1242. package/src/openviking/sidecar-main.ts +38 -0
  1243. package/src/openviking/sidecar.ts +106 -0
  1244. package/src/openviking/store.ts +2025 -0
  1245. package/src/openviking/supervisor.ts +474 -0
  1246. package/src/openviking/types.ts +174 -0
  1247. package/src/operator-recovery.ts +237 -0
  1248. package/src/operatormessage.ts +52 -0
  1249. package/src/outcomes.ts +1670 -0
  1250. package/src/piauthcopy.ts +12 -0
  1251. package/src/picli.ts +49 -0
  1252. package/src/picwd.ts +25 -0
  1253. package/src/pilogin.ts +37 -0
  1254. package/src/pingpong.ts +1339 -0
  1255. package/src/posting-journal.ts +549 -0
  1256. package/src/posting-outbox.ts +802 -0
  1257. package/src/posting-receipt.ts +324 -0
  1258. package/src/posting.ts +603 -0
  1259. package/src/postops.ts +832 -0
  1260. package/src/pr-activity.ts +20 -0
  1261. package/src/pr-author.ts +6 -0
  1262. package/src/pr-conversation.ts +432 -0
  1263. package/src/pr-session-lifecycle.ts +631 -0
  1264. package/src/preflight.ts +391 -0
  1265. package/src/priorresolve.ts +343 -0
  1266. package/src/qcrbin.ts +186 -0
  1267. package/src/recheck.ts +1775 -0
  1268. package/src/reconcile.ts +1030 -0
  1269. package/src/recordedfindings.ts +27 -0
  1270. package/src/regression.ts +833 -0
  1271. package/src/replay.ts +816 -0
  1272. package/src/requiredpasses.ts +148 -0
  1273. package/src/resumecheck.ts +246 -0
  1274. package/src/reviewcli.ts +787 -0
  1275. package/src/reviewcontext.ts +35 -0
  1276. package/src/reviewdetach.ts +734 -0
  1277. package/src/reviewhistory.ts +427 -0
  1278. package/src/reviewhome.ts +36 -0
  1279. package/src/reviewmemory.ts +133 -0
  1280. package/src/reviewrendercli.ts +139 -0
  1281. package/src/round-work.ts +42 -0
  1282. package/src/runhost.ts +108 -0
  1283. package/src/scanauth.ts +36 -0
  1284. package/src/scanbox.ts +1081 -0
  1285. package/src/scancli.ts +1296 -0
  1286. package/src/scandriverlog.ts +32 -0
  1287. package/src/session-bridge.ts +139 -0
  1288. package/src/session-command-payload.ts +35 -0
  1289. package/src/session-command-supervisor.ts +1142 -0
  1290. package/src/sessionjsonl.ts +88 -0
  1291. package/src/sessions.ts +659 -0
  1292. package/src/setup/adapters.ts +335 -0
  1293. package/src/setup/baselines.ts +110 -0
  1294. package/src/setup/caserunner.ts +611 -0
  1295. package/src/setup/configcommit.ts +443 -0
  1296. package/src/setup/engine.ts +1001 -0
  1297. package/src/setup/facts.ts +627 -0
  1298. package/src/setup/findings.ts +1264 -0
  1299. package/src/setup/history.ts +443 -0
  1300. package/src/setup/interview.ts +243 -0
  1301. package/src/setup/ledger.ts +324 -0
  1302. package/src/setup/pins.ts +66 -0
  1303. package/src/setup/portrait.ts +635 -0
  1304. package/src/setup/presentation.ts +18 -0
  1305. package/src/setup/profile.ts +508 -0
  1306. package/src/setup/replay.ts +370 -0
  1307. package/src/setup/replaybuild.ts +392 -0
  1308. package/src/setup/replayrun.ts +1096 -0
  1309. package/src/setup/replaysource.ts +198 -0
  1310. package/src/setup/retune.ts +415 -0
  1311. package/src/setup/retunelog.ts +95 -0
  1312. package/src/setup/scan.ts +1033 -0
  1313. package/src/setup/scanbuild.ts +168 -0
  1314. package/src/setup/scanfacts.ts +107 -0
  1315. package/src/setup/scanlease.ts +297 -0
  1316. package/src/setup/scanreport.ts +373 -0
  1317. package/src/setup/scanrun.ts +2268 -0
  1318. package/src/setup/scantree.ts +259 -0
  1319. package/src/setup/stage2.ts +926 -0
  1320. package/src/setup/stage2request.ts +347 -0
  1321. package/src/setup/survey.ts +458 -0
  1322. package/src/setup/surveyrun.ts +758 -0
  1323. package/src/setup/textsniff.ts +95 -0
  1324. package/src/setup/trackersink.ts +433 -0
  1325. package/src/setup/triage.ts +768 -0
  1326. package/src/setup/tuning.ts +766 -0
  1327. package/src/setup/unitcut.ts +523 -0
  1328. package/src/singleflight.ts +86 -0
  1329. package/src/slack/announce.ts +911 -0
  1330. package/src/slack/api.ts +303 -0
  1331. package/src/slack/messages.ts +288 -0
  1332. package/src/slack/origin.ts +599 -0
  1333. package/src/slack/policy.ts +354 -0
  1334. package/src/slack/reply.ts +201 -0
  1335. package/src/slack/router.ts +285 -0
  1336. package/src/slack/socket.ts +384 -0
  1337. package/src/slack/types.ts +108 -0
  1338. package/src/sqlitehint.ts +32 -0
  1339. package/src/ssh-observation.ts +30 -0
  1340. package/src/state.ts +8243 -0
  1341. package/src/statemachine.ts +369 -0
  1342. package/src/stubs.ts +68 -0
  1343. package/src/takeover.ts +634 -0
  1344. package/src/takeoverpi.ts +293 -0
  1345. package/src/terminal-failure.ts +41 -0
  1346. package/src/thread-resolution-refusal.ts +2 -0
  1347. package/src/tmux.ts +153 -0
  1348. package/src/trackerid.ts +41 -0
  1349. package/src/transcriptindex.ts +665 -0
  1350. package/src/tui/actions.ts +484 -0
  1351. package/src/tui/alerts.ts +666 -0
  1352. package/src/tui/app.ts +1979 -0
  1353. package/src/tui/archiveops.ts +192 -0
  1354. package/src/tui/autofile.ts +233 -0
  1355. package/src/tui/board.ts +1432 -0
  1356. package/src/tui/candidates.ts +889 -0
  1357. package/src/tui/cellwidth.ts +148 -0
  1358. package/src/tui/changelog-store.ts +503 -0
  1359. package/src/tui/chord.ts +25 -0
  1360. package/src/tui/clipboard.ts +92 -0
  1361. package/src/tui/cmdbar.ts +296 -0
  1362. package/src/tui/companion.ts +1427 -0
  1363. package/src/tui/companionplan.ts +247 -0
  1364. package/src/tui/companiontab.ts +83 -0
  1365. package/src/tui/confighome.ts +155 -0
  1366. package/src/tui/connect.ts +68 -0
  1367. package/src/tui/deploy-ceremony.ts +104 -0
  1368. package/src/tui/deploy-drain.ts +129 -0
  1369. package/src/tui/deploy-process.ts +265 -0
  1370. package/src/tui/deploytab.ts +1523 -0
  1371. package/src/tui/design.ts +125 -0
  1372. package/src/tui/digesttab.ts +214 -0
  1373. package/src/tui/emptystate.ts +39 -0
  1374. package/src/tui/evidence.ts +999 -0
  1375. package/src/tui/feed.ts +604 -0
  1376. package/src/tui/feedbacktab.ts +1077 -0
  1377. package/src/tui/filters.ts +90 -0
  1378. package/src/tui/ghread.ts +321 -0
  1379. package/src/tui/historypull.ts +81 -0
  1380. package/src/tui/inboxpull.ts +236 -0
  1381. package/src/tui/index.ts +1565 -0
  1382. package/src/tui/issuesink.ts +238 -0
  1383. package/src/tui/jsonfile.ts +42 -0
  1384. package/src/tui/keys.ts +109 -0
  1385. package/src/tui/layout.ts +108 -0
  1386. package/src/tui/learning.ts +627 -0
  1387. package/src/tui/lessonswitch.ts +118 -0
  1388. package/src/tui/livecontrol.ts +228 -0
  1389. package/src/tui/memforward.ts +443 -0
  1390. package/src/tui/memincident.ts +363 -0
  1391. package/src/tui/memory.ts +620 -0
  1392. package/src/tui/memtab.ts +606 -0
  1393. package/src/tui/metricstab.ts +1177 -0
  1394. package/src/tui/model.ts +131 -0
  1395. package/src/tui/mouse.ts +98 -0
  1396. package/src/tui/msg.ts +268 -0
  1397. package/src/tui/nativecompanion.ts +417 -0
  1398. package/src/tui/openurl.ts +21 -0
  1399. package/src/tui/paint.ts +511 -0
  1400. package/src/tui/panes.ts +98 -0
  1401. package/src/tui/pipane.ts +219 -0
  1402. package/src/tui/poll-hooks.ts +12 -0
  1403. package/src/tui/poll.ts +147 -0
  1404. package/src/tui/proposals.ts +838 -0
  1405. package/src/tui/prpause.ts +150 -0
  1406. package/src/tui/reload.ts +436 -0
  1407. package/src/tui/remote.ts +413 -0
  1408. package/src/tui/rollover.ts +969 -0
  1409. package/src/tui/scantab.ts +1290 -0
  1410. package/src/tui/setupentry.ts +93 -0
  1411. package/src/tui/sidebar.ts +749 -0
  1412. package/src/tui/sshtriage.ts +510 -0
  1413. package/src/tui/startup.ts +231 -0
  1414. package/src/tui/tabs.ts +45 -0
  1415. package/src/tui/termmirror.ts +224 -0
  1416. package/src/tui/theme.ts +95 -0
  1417. package/src/tui/ticketstab.ts +310 -0
  1418. package/src/tui/tmuxsocket.ts +129 -0
  1419. package/src/tui/trackertickets.ts +84 -0
  1420. package/src/tui/unwedge.ts +286 -0
  1421. package/src/tui/viewer.ts +575 -0
  1422. package/src/tui/viewerinput.ts +173 -0
  1423. package/src/updatenotice.ts +178 -0
  1424. package/src/usage.ts +1154 -0
  1425. package/src/verdicts.ts +4689 -0
  1426. package/src/version.ts +80 -0
  1427. package/src/wakes.ts +476 -0
  1428. package/src/webhook.ts +942 -0
  1429. package/src/webhookheal.ts +276 -0
  1430. package/src/wire.ts +2153 -0
  1431. package/src/worker-session-host.ts +602 -0
  1432. package/src/workerbreaker.ts +156 -0
  1433. package/src/workercontrol.ts +104 -0
  1434. package/src/workerlease.ts +441 -0
  1435. package/src/workerliveness.ts +103 -0
  1436. package/src/workerlog.ts +79 -0
  1437. package/src/workerruntime.ts +30 -0
  1438. package/src/workersupervisor.ts +1538 -0
  1439. package/src/workerusage.ts +42 -0
  1440. package/src/workerworld.ts +427 -0
  1441. package/src/worktrees.ts +1138 -0
  1442. package/tests/fixtures/regression-cases.json +795 -0
  1443. package/tests/fixtures/seed-manifest.json +19 -0
package/src/recheck.ts ADDED
@@ -0,0 +1,1775 @@
1
+ // Post-approval delta recheck.
2
+ //
3
+ // Once a PR is approved, a later change can still regress it. A staleness
4
+ // check that re-reviews on every commit after the approval spends most of its
5
+ // output concluding nothing changed, so the delta is classified first, and
6
+ // the standing verdict is UPHELD BY DEFAULT.
7
+ //
8
+ // Shape of the mechanism:
9
+ //
10
+ // trigger ──► planRecheck() ──► no-op (uphold, nothing posted, nothing spent)
11
+ // └─► one nudge to the PR's OWN sticky session
12
+ //
13
+ // Four triggers: a push after an approve, CI completing at the head, the base
14
+ // branch advancing, and an author reply. The author reply is deliberately a
15
+ // no-op HERE: `src/pingpong.ts` already owns that path (≤3 exchanges, reply +
16
+ // optional thread resolve) and a second reply handler would duplicate it.
17
+ // With no code delta there is nothing to recheck; the value is the reply,
18
+ // which is already owned.
19
+ //
20
+ // Invariants this module keeps structurally, not by prompting:
21
+ //
22
+ // 1. **Only APPROVE → REQUEST_CHANGES.** A recheck is planned only when the
23
+ // standing verdict is APPROVE, and the decision file accepts exactly two
24
+ // outcomes: `uphold` and `request_changes`. Anything else — including a
25
+ // literal "APPROVE" or "NEEDS_HUMAN" — is coerced to `uphold` and logged.
26
+ // The path can therefore never re-approve and never park.
27
+ // 2. **A trivial or empty delta costs one classification and a no-op.** That
28
+ // is what makes the broad trigger set affordable. A base advance touching
29
+ // no PR-owned file is the loudest instance: every file it brings is a
30
+ // `base-merge` file, so it resolves to `trivial-delta`.
31
+ // 3. **An unchanged head with no new evidence is a no-op** — the same rule
32
+ // the gate's guard applies (`guards.hasNewEvidence`), read here off the
33
+ // same single `reviewed_heads` mark. There is no second copy.
34
+ // 4. **Single shepherd.** The recheck is a nudge to the PR's existing `pr-<n>`
35
+ // session — no new session, no lens fan-out, no round. It goes through
36
+ // `wakeOrHold`, so a nudge arriving mid-round is held and flushed at round
37
+ // end like any other wake.
38
+ // 5. **No GitHub write lives here.** The module reads (compare, files) and
39
+ // writes nothing; the session posts its review through the gated scripts.
40
+ // The MutationGate keeps its monopoly by this module having no mutation.
41
+ import { existsSync, mkdirSync, readdirSync, renameSync, watch, type FSWatcher } from "node:fs";
42
+ import { join } from "node:path";
43
+ import type { Config } from "./config.js";
44
+ import { readFindingsRecord } from "@quickchatai/pi-quickreview/src/findingsrecord.js";
45
+ import type { PrFileEntry } from "./github.js";
46
+ import { isMigrationPath } from "@quickchatai/pi-quickreview/src/heuristics.js";
47
+ import { parseHunkHeader } from "@quickchatai/pi-quickreview/src/hunkheader.js";
48
+ import { applyEvent } from "./statemachine.js";
49
+ import { recheckHead12, recheckKeyPrefix } from "@quickchatai/pi-quickreview/src/findingkey.js";
50
+ import { recheckWakeReason, type StateStore } from "./state.js";
51
+ import {
52
+ normalizeVerdict,
53
+ recheckPromptMarker,
54
+ stampPrompt,
55
+ } from "@quickchatai/pi-quickreview/src/types.js";
56
+ import { standingVerdict } from "@quickchatai/pi-quickreview/src/standing-review.js";
57
+ import { wakeOrHold, withRepoFacts } from "./wakes.js";
58
+ import { prWorktreeRel } from "./worktrees.js";
59
+ import {
60
+ readSettledJson,
61
+ resolvePartialWritePolicy,
62
+ type PartialWritePolicy,
63
+ } from "@quickchatai/pi-quickreview/src/settledread.js";
64
+ import type {
65
+ FileClass,
66
+ RecheckOrder,
67
+ RecheckTrigger,
68
+ TrivialReason,
69
+ } from "@quickchatai/pi-quickreview/src/recheckorder.js";
70
+ // The order the core renders is its own (packages/pi-plugin/src/recheckorder.ts).
71
+ export type { FileClass, RecheckOrder, RecheckTrigger, TrivialReason };
72
+
73
+ /** A changed file as the compare/files APIs report it. */
74
+ export type DeltaFile = PrFileEntry & { previous_filename?: string };
75
+
76
+ /** A file list as the adapter hands it over. `truncated` means GitHub's own
77
+ * cap cut it short (a compare's 300 files, a PR's 3,000): the list is a
78
+ * floor, and nothing may be concluded from a file's ABSENCE from it.
79
+ * `unread` means the read failed and the list is empty for that reason
80
+ * alone (`unreadDelta`). */
81
+ export type DeltaList = readonly DeltaFile[] & { truncated?: boolean; unread?: boolean };
82
+
83
+ /** The delta of a range GitHub could not list: no files, and significant by
84
+ * construction — nothing is known about what it holds. */
85
+ export function unreadDelta(): DeltaList {
86
+ return Object.assign([] as DeltaFile[], { unread: true });
87
+ }
88
+
89
+ // ---- the ONE trivial-delta classifier ------------------------------------
90
+
91
+ export type DeltaClass = "empty" | "trivial" | "significant";
92
+
93
+ export interface DeltaClassification {
94
+ class: DeltaClass;
95
+ files: FileClass[];
96
+ /** The subset the session is actually asked to look at. */
97
+ significant: FileClass[];
98
+ /** The delta was cut short by GitHub — significant whatever it shows. */
99
+ truncated?: true;
100
+ /** The delta could not be read at all — significant, with no files. */
101
+ unread?: true;
102
+ }
103
+
104
+ /** Dependency-manifest lockfiles: generated, never reviewed on their content. */
105
+ const LOCKFILES = new Set([
106
+ "package-lock.json",
107
+ "npm-shrinkwrap.json",
108
+ "yarn.lock",
109
+ "pnpm-lock.yaml",
110
+ "poetry.lock",
111
+ "uv.lock",
112
+ "Pipfile.lock",
113
+ "Cargo.lock",
114
+ "Gemfile.lock",
115
+ "composer.lock",
116
+ "go.sum",
117
+ ]);
118
+
119
+ /** Manifests whose version field moves on a release bump. */
120
+ const VERSION_MANIFESTS = new Set([
121
+ "package.json",
122
+ "pyproject.toml",
123
+ "Cargo.toml",
124
+ "setup.cfg",
125
+ "VERSION",
126
+ "version.txt",
127
+ ]);
128
+
129
+ function baseName(path: string): string {
130
+ return path.split("/").pop() ?? path;
131
+ }
132
+
133
+ /**
134
+ * Split a migration filename into its number and the rest: `0042_add_foo.py`
135
+ * → `{ num: "0042", rest: "add_foo.py" }`. Null when the file is not a
136
+ * numbered migration.
137
+ */
138
+ export function migrationName(path: string): { num: string; rest: string } | null {
139
+ const m = /^(\d+)[_-](.+)$/.exec(baseName(path));
140
+ if (!m || !isMigrationPath(path)) return null;
141
+ return { num: m[1] as string, rest: m[2] as string };
142
+ }
143
+
144
+ /** A rename that only moves a migration's number: `0042_x.py` → `0043_x.py`. */
145
+ export function isMigrationRenumber(from: string, to: string): boolean {
146
+ const a = migrationName(from);
147
+ const b = migrationName(to);
148
+ return a !== null && b !== null && a.rest === b.rest && a.num !== b.num;
149
+ }
150
+
151
+ /** One hunk of a unified diff: the text git prints after its `@@ … @@`
152
+ * header (often the enclosing function or table), the old-side line it starts
153
+ * at (null when the header gives none), and its lines in order. */
154
+ interface Hunk {
155
+ section: string;
156
+ oldStart: number | null;
157
+ lines: Array<{ kind: " " | "+" | "-"; text: string }>;
158
+ }
159
+
160
+ /**
161
+ * The hunks of a unified diff. File headers (`---`/`+++`) are recognised only
162
+ * before the first hunk — inside one, `--- x` is a removed line that reads
163
+ * `-- x`. A patch with no `@@` line is one hunk.
164
+ */
165
+ function parseHunks(patch: string): Hunk[] {
166
+ const hunks: Hunk[] = [];
167
+ let cur: Hunk | null = null;
168
+ for (const line of patch.split("\n")) {
169
+ if (line.startsWith("@@")) {
170
+ cur = {
171
+ section: /^@@[^@]*@@ ?(.*)$/.exec(line)?.[1] ?? "",
172
+ oldStart: parseHunkHeader(line)?.old_start ?? null,
173
+ lines: [],
174
+ };
175
+ hunks.push(cur);
176
+ continue;
177
+ }
178
+ if (cur === null && (line.startsWith("+++") || line.startsWith("---"))) continue;
179
+ const kind = line.charAt(0);
180
+ if (kind !== " " && kind !== "+" && kind !== "-") continue;
181
+ if (cur === null) {
182
+ cur = { section: "", oldStart: null, lines: [] };
183
+ hunks.push(cur);
184
+ }
185
+ cur.lines.push({ kind, text: line.slice(1) });
186
+ }
187
+ return hunks;
188
+ }
189
+
190
+ /** One change block: a maximal run of added/removed lines between context
191
+ * lines. A line that moved across unchanged code lands in two blocks, so
192
+ * comparing block by block keeps a reordering around unchanged statements
193
+ * from reading as formatting. */
194
+ interface ChangeBlock {
195
+ added: string[];
196
+ removed: string[];
197
+ }
198
+
199
+ function changeBlocks(hunk: Hunk): ChangeBlock[] {
200
+ const blocks: ChangeBlock[] = [];
201
+ let cur: ChangeBlock | null = null;
202
+ for (const line of hunk.lines) {
203
+ if (line.kind === " ") {
204
+ cur = null;
205
+ continue;
206
+ }
207
+ if (cur === null) {
208
+ cur = { added: [], removed: [] };
209
+ blocks.push(cur);
210
+ }
211
+ (line.kind === "+" ? cur.added : cur.removed).push(line.text);
212
+ }
213
+ return blocks;
214
+ }
215
+
216
+ /** Every line of one kind in a patch, prefix kept — a whole added or removed
217
+ * file's text. */
218
+ function changedLines(patch: string, kind: "+" | "-"): string[] {
219
+ return parseHunks(patch).flatMap((h) =>
220
+ h.lines.filter((l) => l.kind === kind).map((l) => `${kind}${l.text}`),
221
+ );
222
+ }
223
+
224
+ /** A quoted literal on one line. Whitespace inside one is content, never
225
+ * formatting: `"a b"` → `"ab"` changes the program. */
226
+ const QUOTED = /("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`(?:\\.|[^`\\])*`)/;
227
+
228
+ /** Drop whitespace OUTSIDE quoted literals; the literals stay verbatim. */
229
+ function stripOutsideQuotes(line: string): string {
230
+ return line
231
+ .split(QUOTED)
232
+ .map((part, i) => (i % 2 === 1 ? part : part.replace(/\s+/g, "")))
233
+ .join("");
234
+ }
235
+
236
+ /** Languages whose leading whitespace is syntax — a dedent in Python moves a
237
+ * line out of its block; in YAML it moves a key to another mapping; in the
238
+ * indented style-sheet syntaxes (Sass, Stylus) it moves a rule out of the
239
+ * selector it was nested under. */
240
+ function indentSignificant(path: string | undefined): boolean {
241
+ if (!path) return false;
242
+ const base = baseName(path);
243
+ return /\.(py|pyi|ya?ml|sass|styl)$/i.test(base) || base === "Makefile" || /\.mk$/i.test(base);
244
+ }
245
+
246
+ /** A shell script's basename (a Dockerfile counts). */
247
+ function isShellFile(base: string): boolean {
248
+ return /\.(sh|bash|zsh|ksh)$/i.test(base) || base === "Dockerfile";
249
+ }
250
+
251
+ /** A Ruby file's basename (a Gemfile counts). */
252
+ function isRubyFile(base: string): boolean {
253
+ return /\.rb$/i.test(base) || base === "Gemfile";
254
+ }
255
+
256
+ /** Languages where a line break ends a command or statement — in a shell
257
+ * script `a\n-b` runs two commands where `a -b` runs one; Ruby and Go end a
258
+ * statement at most line breaks. */
259
+ function lineTerminated(path: string | undefined): boolean {
260
+ if (!path) return false;
261
+ const base = baseName(path);
262
+ return isShellFile(base) || isRubyFile(base) || /\.go$/i.test(base);
263
+ }
264
+
265
+ /**
266
+ * The whitespace-insensitive form of a side of a patch. Whitespace between
267
+ * tokens is dropped (quoted literals kept verbatim) and the lines are
268
+ * concatenated, so a re-indent or a re-flow compares equal. For an
269
+ * indentation-significant file each non-blank line keeps its leading
270
+ * whitespace and stays its own line — a re-flow or re-indent there is not
271
+ * provably meaningless, so it is not formatting. For a file whose line
272
+ * breaks end statements each non-blank line stays its own line, so a re-flow
273
+ * there is not formatting while a re-indent still is.
274
+ */
275
+ function squash(lines: string[], path?: string): string {
276
+ if (lineTerminated(path))
277
+ return lines
278
+ .filter((l) => l.trim() !== "")
279
+ .map(stripOutsideQuotes)
280
+ .join("\n");
281
+ if (!indentSignificant(path)) return lines.map(stripOutsideQuotes).join("");
282
+ return lines
283
+ .filter((l) => l.trim() !== "")
284
+ .map((l) => (/^\s*/.exec(l)?.[0] ?? "") + stripOutsideQuotes(l))
285
+ .join("\n");
286
+ }
287
+
288
+ /**
289
+ * The slash-span form of a side of a patch: on each line, the raw text from
290
+ * its first `/` to its last is kept verbatim (delimited), and whitespace
291
+ * elsewhere is dropped. A regex literal never crosses a line, so it always
292
+ * lies inside its line's span; whitespace inside one is content (`/a b/` →
293
+ * `/ab/` changes the program). Telling a regex from a division, a comment or
294
+ * a markup closing tag is not attempted: any whitespace change between the
295
+ * first and last `/` of a line is held significant, so this form can only
296
+ * make a delta less trivial, never more.
297
+ */
298
+ function slashSpans(lines: string[]): string {
299
+ return lines
300
+ .map((l) => {
301
+ const i = l.indexOf("/");
302
+ const j = l.lastIndexOf("/");
303
+ if (i === j) return l.replace(/\s+/g, "");
304
+ return `${l.slice(0, i).replace(/\s+/g, "")}\0${l.slice(i, j + 1)}\0${l.slice(j + 1).replace(/\s+/g, "")}`;
305
+ })
306
+ .join("");
307
+ }
308
+
309
+ /** The class of a character for token joining: two characters of one class
310
+ * that whitespace separates would read as ONE token once it is dropped —
311
+ * `const n` → `constn`, `a + ++b` → `a+++b` (the same as `a++ + b`). Null for
312
+ * a character no neighbour joins with (brackets, separators, quotes). */
313
+ function joinClass(ch: string): "word" | "op" | null {
314
+ if (/[\w$]/.test(ch) || ch.charCodeAt(0) > 127) return "word";
315
+ if (/[+\-*/%&|^!~<>=?:.@#]/.test(ch)) return "op";
316
+ return null;
317
+ }
318
+
319
+ /** A word after which a line break ends the statement (JavaScript's
320
+ * restricted productions: `return\n(x)` returns undefined). Matched against
321
+ * the last nine characters, one more than the longest word. */
322
+ const RESTRICTED_WORD_END = /(?:^|[^\w$])(?:return|throw|break|continue|yield|async)$/;
323
+
324
+ /**
325
+ * The token-seam form of a side of a patch: whitespace outside quoted
326
+ * literals is dropped EXCEPT where it separates two characters of one join
327
+ * class, where it becomes a single space — or a line break, when the gap
328
+ * holds one. Dropping that whitespace would merge two tokens into one
329
+ * (`a + ++b` and `a++ + b` both squash to `a+++b`), so whether it is there is
330
+ * content. A line break is also kept wherever it can end a statement however
331
+ * the tokens join: after a restricted word (`return\n(x)`) and beside `++` or
332
+ * `--` (`a++\nb` against `a\n++b`). Every line break kept is a `\n` the form
333
+ * holds nowhere else, so compared beside `squash` this form can only make a
334
+ * delta less trivial, never more.
335
+ */
336
+ function tokenSeams(lines: string[]): string {
337
+ let out = "";
338
+ let gap = false;
339
+ let lineBreak = false;
340
+ // A line comment runs to the end of its line, so the break after a line
341
+ // that holds a comment marker outside a literal is content.
342
+ const LINE_COMMENT = /\/\/|#|--/;
343
+ const push = (text: string): void => {
344
+ if (text === "") return;
345
+ const prev = out.slice(-1);
346
+ if (gap && prev !== "") {
347
+ const cls = joinClass(prev);
348
+ const seam = cls !== null && cls === joinClass(text.charAt(0));
349
+ const ends =
350
+ lineBreak &&
351
+ (seam ||
352
+ RESTRICTED_WORD_END.test(out.slice(-9)) ||
353
+ /(?:\+\+|--)$/.test(out.slice(-2)) ||
354
+ /^(?:\+\+|--)/.test(text));
355
+ // A seam keeps its space and gains the break, so deleting every "\n"
356
+ // gives back the form without breaks exactly — before and after
357
+ // `digitsOnly` masks it — and the break form only ever refines it.
358
+ if (seam) out += " ";
359
+ if (ends) out += "\n";
360
+ }
361
+ out += text;
362
+ gap = false;
363
+ lineBreak = false;
364
+ };
365
+ for (const line of lines) {
366
+ gap = true;
367
+ lineBreak = true;
368
+ let commented = false;
369
+ line.split(QUOTED).forEach((part, i) => {
370
+ if (i % 2 === 1) return push(part);
371
+ if (LINE_COMMENT.test(part)) commented = true;
372
+ part.split(/(\s+)/).forEach((chunk, k) => {
373
+ if (k % 2 === 1) gap = true;
374
+ else push(chunk);
375
+ });
376
+ });
377
+ if (commented && out !== "") out += "\n";
378
+ }
379
+ return out;
380
+ }
381
+
382
+ /** Style sheets: whitespace between two selector tokens is a combinator
383
+ * (`a .child` is a descendant, `a.child` a compound), so no whitespace that
384
+ * separates two tokens there may vanish or appear, whatever their classes. */
385
+ function selectorWhitespace(path: string | undefined): boolean {
386
+ return path !== undefined && /\.(css|scss|sass|less|styl)$/i.test(baseName(path));
387
+ }
388
+
389
+ /** Every whitespace run inside a line collapsed to one space and the lines
390
+ * joined by one: a re-indent and a re-flow compare equal, a gap that opens or
391
+ * closes between two tokens does not. */
392
+ function collapsed(lines: string[]): string {
393
+ return lines
394
+ .map((l) => l.trim().replace(/\s+/g, " "))
395
+ .filter((l) => l !== "")
396
+ .join(" ");
397
+ }
398
+
399
+ /** A heredoc opener: `<<EOF`, `<<-'EOF'`, `<< "EOF"` (shell) and `<<~SQL`
400
+ * (Ruby, no space — `a << b` appends). */
401
+ const SHELL_HEREDOC = /<<-?\s*(['"]?)[A-Za-z_]\w*\1/;
402
+ const RUBY_HEREDOC = /<<[-~]?(['"]?)[A-Za-z_]\w*\1/;
403
+ /** A YAML block scalar indicator closing its line (`key: |`, `- >-`). */
404
+ const YAML_BLOCK_SCALAR = /(?:^|:|-)\s*[|>][-+1-9]{0,2}\s*(?:#.*)?$/;
405
+
406
+ /**
407
+ * May this hunk's changed lines sit inside a literal that spans lines — a
408
+ * template literal, a triple-quoted string, a heredoc, a YAML block scalar?
409
+ * Whitespace inside one is content, and the per-line `QUOTED` rule cannot see
410
+ * a literal that opens or closes on another line. Judged from what the hunk
411
+ * shows (its section text, its context and its changed lines): an unbalanced
412
+ * backtick, any triple quote, or the language's own opener. A literal whose
413
+ * delimiters both lie outside the hunk is beyond what the patch shows.
414
+ */
415
+ function mayTouchMultilineLiteral(hunk: Hunk, path: string | undefined): boolean {
416
+ const base = path === undefined ? "" : baseName(path);
417
+ const shell = isShellFile(base);
418
+ const ruby = isRubyFile(base);
419
+ const yaml = /\.ya?ml$/i.test(base);
420
+ return [hunk.section, ...hunk.lines.map((l) => l.text)].some(
421
+ (text) =>
422
+ text.includes('"""') ||
423
+ text.includes("'''") ||
424
+ (text.replace(/\\./g, "").split("`").length - 1) % 2 === 1 ||
425
+ (shell && SHELL_HEREDOC.test(text)) ||
426
+ (ruby && RUBY_HEREDOC.test(text)) ||
427
+ (yaml && YAML_BLOCK_SCALAR.test(text)),
428
+ );
429
+ }
430
+
431
+ /**
432
+ * The forms a side of a change block is compared in, every one of which must
433
+ * match for the change to be whitespace-only: `squash` (insignificant
434
+ * whitespace dropped — never inside a quoted literal, never leading
435
+ * indentation of a whitespace-significant language), `slashSpans` (no line's
436
+ * whitespace changed between its first and last `/` — a regex literal's
437
+ * whitespace is content), `tokenSeams` (no whitespace that separates two
438
+ * tokens appeared or vanished) and, in a style sheet, `collapsed`. The one
439
+ * list both `formattingOnly` and `digitsOnly` compare.
440
+ */
441
+ function whitespaceForms(lines: string[], path: string | undefined): string[] {
442
+ const forms = [squash(lines, path), slashSpans(lines), tokenSeams(lines)];
443
+ if (selectorWhitespace(path)) forms.push(collapsed(lines));
444
+ return forms;
445
+ }
446
+
447
+ /** The two sides of a block agree in every `whitespaceForms` form, each
448
+ * compared after `mask` (identity unless a caller masks something out). */
449
+ function sameForms(
450
+ block: ChangeBlock,
451
+ path: string | undefined,
452
+ mask: (form: string) => string = (form) => form,
453
+ ): boolean {
454
+ const removed = whitespaceForms(block.removed, path);
455
+ return whitespaceForms(block.added, path).every((form, i) => mask(form) === mask(removed[i]!));
456
+ }
457
+
458
+ /**
459
+ * Whitespace-only change, judged change block by change block (see
460
+ * `changeBlocks`): within a block, concatenation (not a per-line set) is the
461
+ * comparison, so a re-indent or a re-flow passes while a reordering of real
462
+ * lines does not — and a line moved across unchanged code is two blocks that
463
+ * each differ. A hunk that may touch a multi-line literal is never
464
+ * formatting.
465
+ */
466
+ export function formattingOnly(patch: string, path?: string): boolean {
467
+ const hunks = parseHunks(patch);
468
+ const blocks = hunks.flatMap(changeBlocks);
469
+ if (blocks.length === 0) return false;
470
+ if (hunks.some((h) => mayTouchMultilineLiteral(h, path))) return false;
471
+ return blocks.every((b) => sameForms(b, path));
472
+ }
473
+
474
+ /** A migration number: a digit run that starts a token and is followed by `_`
475
+ * and a letter — the `NNNN_name` shape a migration filename or `dependencies`
476
+ * entry carries. `1_000` (a numeric literal) is not one. */
477
+ const MIGRATION_NUMBER = /(?<![A-Za-z0-9_])\d+(?=_[A-Za-z])/g;
478
+
479
+ /** Same text once every MIGRATION-NUMBER run is masked — the renumber's
480
+ * content edit (`dependencies = [("app", "0042_x")]` → `"0043_x"`) and nothing
481
+ * else. Only the migration-number shape is masked (`MIGRATION_NUMBER`): a
482
+ * semantic numeric change inside a renamed migration — a batch size, a
483
+ * timeout, a default, `1_000` → `2_000` — does NOT mask away, so it
484
+ * classifies significant. Judged block by block like `formattingOnly`; in a
485
+ * hunk that may touch a multi-line literal the raw lines are compared, so only
486
+ * the numbers may differ there. */
487
+ export function digitsOnly(patch: string, path?: string): boolean {
488
+ const masked = (text: string): string => text.replace(MIGRATION_NUMBER, "#");
489
+ return parseHunks(patch).every((hunk) => {
490
+ const literal = mayTouchMultilineLiteral(hunk, path);
491
+ return changeBlocks(hunk).every((block) =>
492
+ literal
493
+ ? masked(block.added.join("\n")) === masked(block.removed.join("\n"))
494
+ : sameForms(block, path, masked),
495
+ );
496
+ });
497
+ }
498
+
499
+ /** One release-version line and nothing else on it: `"version": "1.2.3",`
500
+ * (package.json) or `version = "1.2.3"` (pyproject/Cargo). The value must
501
+ * look like a version — a digit first, no spaces — so the npm lifecycle
502
+ * script `"version": "npm run build"` does not pass, and a line that merely
503
+ * MENTIONS version (`serde = { version = "1", features = [...] }`, an inline
504
+ * dependency with a `git` source) is not the key itself. */
505
+ const RELEASE_VERSION_LINE =
506
+ /^\s*(?:"version"\s*:\s*"v?\d[^"\s]*"\s*,?|version\s*=\s*(?:"v?\d[^"\s]*"|'v?\d[^'\s]*'))\s*$/i;
507
+
508
+ /** A TOML table header line, its name with surrounding space dropped:
509
+ * `[package]` → `package`; an array table (`[[bin]]`) keeps its brackets. */
510
+ const TOML_TABLE = /^\s*(\[?\[[^\]]+\]\]?)\s*(?:#.*)?$/;
511
+
512
+ /** The TOML tables whose `version` key is the package's own release. */
513
+ const RELEASE_TABLES = new Set(["package", "workspace.package", "project", "tool.poetry"]);
514
+
515
+ function tomlTable(text: string): string | null {
516
+ const m = TOML_TABLE.exec(text);
517
+ if (!m) return null;
518
+ const header = m[1] as string;
519
+ return header.startsWith("[[") ? header : header.slice(1, -1).replace(/\s+/g, "");
520
+ }
521
+
522
+ /** How far a line of JSON opens (+) or closes (−) objects and arrays, its
523
+ * quoted strings left out. */
524
+ function jsonDepthDelta(text: string): number {
525
+ let delta = 0;
526
+ text.split(QUOTED).forEach((part, i) => {
527
+ if (i % 2 === 1) return;
528
+ for (const ch of part) {
529
+ if (ch === "{" || ch === "[") delta++;
530
+ else if (ch === "}" || ch === "]") delta--;
531
+ }
532
+ });
533
+ return delta;
534
+ }
535
+
536
+ /** Every changed line is the release-version key itself — the release-bump
537
+ * shape. The key must also be the package's own, proven from what the hunk
538
+ * shows: in a TOML manifest it sits in a release table (`RELEASE_TABLES`) —
539
+ * `version = "2"` under `[dependencies.serde]` is a dependency requirement —
540
+ * and in a JSON manifest it is a key of the top-level object, which the hunk
541
+ * proves only when it starts at the file's first line (`"version"` under
542
+ * `"config"` is configuration, not the release). A line whose place the patch
543
+ * does not show is not proven to be the release. */
544
+ export function versionOnly(patch: string, path?: string): boolean {
545
+ if (path !== undefined && ["VERSION", "version.txt"].includes(baseName(path))) {
546
+ const lines = parseHunks(patch).flatMap((h) => h.lines);
547
+ const version = /^v?\d+(?:\.\d+)*(?:[-+][0-9A-Za-z.-]+)?$/;
548
+ return (
549
+ lines.some((line) => line.kind !== " ") &&
550
+ lines.every((line) =>
551
+ line.text.trim() === "" ? line.kind === " " : version.test(line.text.trim()),
552
+ )
553
+ );
554
+ }
555
+ const toml = path !== undefined && /\.toml$/i.test(path);
556
+ const json = path !== undefined && /\.json$/i.test(path);
557
+ let changed = 0;
558
+ for (const hunk of parseHunks(patch)) {
559
+ let table = tomlTable(hunk.section);
560
+ // The old side's nesting depth before the line; unknown (null) unless
561
+ // the hunk starts at the top of the file.
562
+ let depth: number | null = hunk.oldStart === 1 ? 0 : null;
563
+ for (const line of hunk.lines) {
564
+ table = tomlTable(line.text) ?? table;
565
+ if (line.kind !== " ") {
566
+ changed++;
567
+ if (!RELEASE_VERSION_LINE.test(line.text)) return false;
568
+ if (toml && (table === null || !RELEASE_TABLES.has(table))) return false;
569
+ if (json && depth !== 1) return false;
570
+ }
571
+ if (depth !== null && line.kind !== "+") depth += jsonDepthDelta(line.text);
572
+ }
573
+ }
574
+ return changed > 0;
575
+ }
576
+
577
+ export interface ClassifyOpts {
578
+ /**
579
+ * Files this PR owns — its own diff against its base. A delta file outside
580
+ * that set arrived from the base branch (a master pull, a conflict
581
+ * resolution, a base advance) and is not this PR's to review: "later commits
582
+ * on the base are not yours". Omitted ⇒ ownership is not consulted.
583
+ */
584
+ prFiles?: ReadonlySet<string>;
585
+ /** Files hand-resolved inside a merge commit (their content differs from
586
+ * git's own auto-merge of the parents). A clean base merge is trivial; a
587
+ * conflict RESOLUTION is a human/agent decision and classifies significant
588
+ * even outside prFiles: a resolution can silently drop the base's change. */
589
+ resolvedFiles?: ReadonlySet<string>;
590
+ }
591
+
592
+ /** Does an ownership set hold this file — under its name, or, for a rename,
593
+ * under the name it had? The one ownership rule the classifier and the
594
+ * reviewed-head ownership read share. */
595
+ export function ownedBy(
596
+ file: { filename: string; previous_filename?: string | undefined },
597
+ owned: ReadonlySet<string>,
598
+ ): boolean {
599
+ return (
600
+ owned.has(file.filename) ||
601
+ (file.previous_filename !== undefined && owned.has(file.previous_filename))
602
+ );
603
+ }
604
+
605
+ /** The PR's own files as an ownership set — or undefined, "ownership not
606
+ * consulted", when the list was truncated: a PR-owned file past GitHub's cap
607
+ * would otherwise read as a base-merge file, which is trivial. */
608
+ export function ownershipSet(own: DeltaList): Set<string> | undefined {
609
+ if (own.truncated) return undefined;
610
+ return new Set(
611
+ own.flatMap((f) => (f.previous_filename ? [f.filename, f.previous_filename] : [f.filename])),
612
+ );
613
+ }
614
+
615
+ /** Classify one file. The rules are deliberately narrow: anything a rule does
616
+ * not positively recognise is significant. */
617
+ export function classifyFile(file: DeltaFile, opts: ClassifyOpts = {}): FileClass {
618
+ const name = file.filename;
619
+ const trivial = (reason: TrivialReason): FileClass => ({
620
+ filename: name,
621
+ class: "trivial",
622
+ reason,
623
+ });
624
+
625
+ // A hand-resolved merge conflict is never trivial, wherever the file lives.
626
+ if (opts.resolvedFiles?.has(name))
627
+ return { filename: name, class: "significant", reason: "content" };
628
+
629
+ if (opts.prFiles && !ownedBy(file, opts.prFiles)) return trivial("base-merge");
630
+
631
+ const patch = file.patch ?? "";
632
+ const churn = (file.additions ?? 0) + (file.deletions ?? 0);
633
+
634
+ if (file.previous_filename && isMigrationRenumber(file.previous_filename, name)) {
635
+ // A missing patch with churn (GitHub omits it on large files) proves
636
+ // nothing — the same reading classifyDelta's delete+add pairing takes.
637
+ if (churn === 0 || (patch && (digitsOnly(patch, name) || formattingOnly(patch, name))))
638
+ return trivial("migration-renumber");
639
+ return { filename: name, class: "significant", reason: "content" };
640
+ }
641
+ if (file.previous_filename && churn === 0) return trivial("pure-rename");
642
+
643
+ if (LOCKFILES.has(baseName(name))) return trivial("version-bump");
644
+ if (VERSION_MANIFESTS.has(baseName(name)) && patch && versionOnly(patch, name))
645
+ return trivial("version-bump");
646
+
647
+ if (patch && formattingOnly(patch, name)) return trivial("formatting");
648
+ if (!patch && churn === 0) return trivial("no-content-change");
649
+
650
+ return { filename: name, class: "significant", reason: "content" };
651
+ }
652
+
653
+ /**
654
+ * Classify the whole delta. Run before the per-file pass: GitHub reports a
655
+ * migration renumber as a rename only when it detects one — a large enough
656
+ * content edit comes back as a delete + an add. That pair is a renumber only
657
+ * when its content, too, differs by migration numbers alone (`digitsOnly`).
658
+ */
659
+ export function classifyDelta(files: DeltaList, opts: ClassifyOpts = {}): DeltaClassification {
660
+ const removedMigrations = new Map<string, DeltaFile>();
661
+ for (const f of files) {
662
+ if (f.status !== "removed") continue;
663
+ const m = migrationName(f.filename);
664
+ if (m) removedMigrations.set(m.rest, f);
665
+ }
666
+ const renumbered = new Set<string>();
667
+ for (const f of files) {
668
+ if (f.status !== "added") continue;
669
+ const m = migrationName(f.filename);
670
+ const partner = m ? removedMigrations.get(m.rest) : undefined;
671
+ if (!m || !partner) continue;
672
+ if (migrationName(partner.filename)?.num === m.num) continue;
673
+ // The names only say it MIGHT be a renumber; the content must say it is.
674
+ // GitHub reports a rename as delete + add exactly when similarity is low,
675
+ // so the pair is trivial only when the removed file's text and the added
676
+ // file's text differ by migration numbers alone. A missing patch (GitHub
677
+ // omits it on large files) proves nothing, so it is not a renumber.
678
+ if (!f.patch || !partner.patch) continue;
679
+ const gone = changedLines(partner.patch, "-");
680
+ const came = changedLines(f.patch, "+");
681
+ if (gone.length === 0 && came.length === 0) continue;
682
+ if (!digitsOnly(["@@", ...gone, ...came].join("\n"), f.filename)) continue;
683
+ renumbered.add(f.filename);
684
+ renumbered.add(partner.filename);
685
+ }
686
+
687
+ const classified = files.map((f): FileClass => {
688
+ // A hand-resolved conflict beats every trivial rule, renumber included:
689
+ // classifyFile alone decides it.
690
+ if (renumbered.has(f.filename) && !opts.resolvedFiles?.has(f.filename)) {
691
+ // Ownership still wins: a migration that arrived with the base is a
692
+ // base-merge file whatever its number does.
693
+ const owned = !opts.prFiles || opts.prFiles.has(f.filename);
694
+ return {
695
+ filename: f.filename,
696
+ class: "trivial",
697
+ reason: owned ? "migration-renumber" : "base-merge",
698
+ };
699
+ }
700
+ return classifyFile(f, opts);
701
+ });
702
+ const significant = classified.filter((f) => f.class === "significant");
703
+ // A truncated delta is significant by construction: the files GitHub did not
704
+ // return may be exactly the PR-owned edit, and "all visible files trivial"
705
+ // says nothing about them. An unread one, for the same reason.
706
+ if (files.unread) return { class: "significant", files: classified, significant, unread: true };
707
+ if (files.truncated)
708
+ return { class: "significant", files: classified, significant, truncated: true };
709
+ const cls: DeltaClass =
710
+ classified.length === 0 ? "empty" : significant.length > 0 ? "significant" : "trivial";
711
+ return { class: cls, files: classified, significant };
712
+ }
713
+
714
+ // ---- the plan -------------------------------------------------------------
715
+
716
+ /** Why a trigger did not produce a nudge. Every one of these upholds the
717
+ * standing verdict — that is the default, not an outcome. */
718
+ export type NoopReason =
719
+ | "disabled"
720
+ | "pingpong-owns-reply"
721
+ | "no-standing-approval"
722
+ | "stale-head"
723
+ | "pr-not-open"
724
+ | "unchanged-head"
725
+ | "empty-delta"
726
+ | "trivial-delta"
727
+ | "already-rechecked"
728
+ | "no-delta-source"
729
+ | "delivery-failed"
730
+ /** Deploy-window drain: no nudge is generated while draining. */
731
+ | "draining"
732
+ /** The PR is paused (`pause-pr`): no review work for it, a nudge included. */
733
+ | "paused";
734
+
735
+ export interface RecheckPlanInput {
736
+ trigger: RecheckTrigger;
737
+ /** The verdict standing at `last_reviewed_sha` (the reviewed-head mark). */
738
+ standing: string | null | undefined;
739
+ last_reviewed_sha: string;
740
+ head_sha: string;
741
+ delta: DeltaList;
742
+ /** The PR's own changed files (ownership, see ClassifyOpts.prFiles). */
743
+ prFiles?: readonly string[];
744
+ /** Hand-resolved merge files (see ClassifyOpts.resolvedFiles). */
745
+ resolvedFiles?: readonly string[];
746
+ /** CI conclusion for the `ci` trigger, as GitHub spells it. */
747
+ ci?: { conclusion: string | null };
748
+ }
749
+
750
+ export type RecheckPlan =
751
+ | { action: "noop"; reason: NoopReason; class: DeltaClass; classification?: DeltaClassification }
752
+ | { action: "recheck"; class: DeltaClass; evidence: string; classification: DeltaClassification };
753
+
754
+ /** CI conclusions that carry no bad news. Anything else at an approved head is
755
+ * new evidence (guard 2's sense): something the standing review did not see. */
756
+ const CI_BENIGN = new Set(["success", "neutral", "skipped", "cancelled", "stale"]);
757
+
758
+ export function ciIsAdverse(conclusion: string | null | undefined): boolean {
759
+ if (!conclusion) return false;
760
+ return !CI_BENIGN.has(conclusion.toLowerCase());
761
+ }
762
+
763
+ /**
764
+ * Guard 2's rule as the recheck reads it, off the same mark: an unchanged head
765
+ * with nothing new is a no-op. A base advance is exempt because its delta is on
766
+ * the base side, and an adverse CI conclusion IS the new evidence. The runner's
767
+ * free early refusal and the plan both ask this one predicate.
768
+ */
769
+ export function unchangedHeadNoop(
770
+ trigger: RecheckTrigger,
771
+ headSha: string,
772
+ lastReviewedSha: string,
773
+ ci: { conclusion?: string | null } | null | undefined,
774
+ ): boolean {
775
+ const adverseCi = trigger === "ci" && ciIsAdverse(ci?.conclusion);
776
+ return trigger !== "base_advance" && headSha === lastReviewedSha && !adverseCi;
777
+ }
778
+
779
+ /** Move a withheld-approval mark to a newer head after an uphold. This is NOT
780
+ * a re-approve — the approval was never posted; the CI-gate retry still
781
+ * demands green CI at the new head before anything posts. No mark, or a mark
782
+ * already at this head, is a silent no-op. */
783
+ export function carryWithheldMark(
784
+ store: StateStore,
785
+ pr: number,
786
+ newHead: string,
787
+ via: string,
788
+ log: (msg: string) => void = () => {},
789
+ ): void {
790
+ const withheld = store.approvalWithheld(pr);
791
+ if (!withheld || withheld.head_sha === newHead) return;
792
+ // Only ever carry TO the PR's current head. Overlapping rechecks finish in
793
+ // any order: an older push's handler finishing after the newer one's would
794
+ // otherwise carry the mark BACK, and the ci_completed retry at the real head
795
+ // then no-ops on "head-moved" with the push backstop already claimed —
796
+ // stranded. Read and write are one synchronous stretch, so nothing can
797
+ // interleave between them.
798
+ const current = store.getPr(pr)?.head_sha;
799
+ if (current && current !== newHead) {
800
+ store.logEvent("approval_withheld_carry_stale", pr, {
801
+ from: withheld.head_sha,
802
+ to: newHead,
803
+ current,
804
+ via,
805
+ });
806
+ log(
807
+ `recheck: #${pr} withheld approval NOT carried to ${newHead.slice(0, 7)} — the PR is at ${current.slice(0, 7)} (${via})`,
808
+ );
809
+ return;
810
+ }
811
+ // The classifier verdict that caused the withhold rides along with the
812
+ // mark — the carried head has no fresher CI read to offer, and the CI-gate
813
+ // retry re-classifies at release time anyway.
814
+ store.recordApprovalWithheld(pr, {
815
+ head_sha: newHead,
816
+ ci_state: withheld.ci_state ?? null,
817
+ reason: withheld.reason ?? "red-ci",
818
+ });
819
+ store.logEvent("approval_withheld_carried", pr, { from: withheld.head_sha, to: newHead, via });
820
+ log(
821
+ `recheck: #${pr} withheld approval carried ${withheld.head_sha.slice(0, 7)} → ${newHead.slice(0, 7)} (${via})`,
822
+ );
823
+ }
824
+
825
+ /**
826
+ * The whole decision, pure. Order matters: the cheap structural refusals come
827
+ * before anything that would cost a classification, and the classification
828
+ * comes before anything that would cost a session turn.
829
+ */
830
+ export function planRecheck(input: RecheckPlanInput): RecheckPlan {
831
+ const noop = (reason: NoopReason, classification?: DeltaClassification): RecheckPlan => ({
832
+ action: "noop",
833
+ reason,
834
+ class: classification?.class ?? "empty",
835
+ ...(classification ? { classification } : {}),
836
+ });
837
+
838
+ // The author-reply trigger is owned end to end by src/pingpong.ts. Nothing
839
+ // to recheck without a code delta, and a second reply handler would
840
+ // duplicate it.
841
+ if (input.trigger === "author_reply") return noop("pingpong-owns-reply");
842
+
843
+ // Only a standing APPROVE can be moved by this path, and only downwards.
844
+ // Everything else (REQUEST_CHANGES, NEEDS_HUMAN, never reviewed) already has
845
+ // a route: the ordinary trigger → round path.
846
+ if (normalizeVerdict(input.standing) !== "APPROVE") return noop("no-standing-approval");
847
+
848
+ if (unchangedHeadNoop(input.trigger, input.head_sha, input.last_reviewed_sha, input.ci))
849
+ return noop("unchanged-head");
850
+ const adverseCi = input.trigger === "ci" && ciIsAdverse(input.ci?.conclusion);
851
+
852
+ const classification = classifyDelta(input.delta, {
853
+ ...(input.prFiles ? { prFiles: new Set(input.prFiles) } : {}),
854
+ ...(input.resolvedFiles ? { resolvedFiles: new Set(input.resolvedFiles) } : {}),
855
+ });
856
+ if (adverseCi)
857
+ return {
858
+ action: "recheck",
859
+ class: classification.class,
860
+ evidence: classification.unread
861
+ ? `CI concluded ${input.ci?.conclusion}; the delta's files could not be read from GitHub — read the delta in the worktree yourself`
862
+ : `CI concluded ${input.ci?.conclusion}`,
863
+ classification,
864
+ };
865
+ if (classification.class === "empty") return noop("empty-delta", classification);
866
+ if (classification.class === "trivial") return noop("trivial-delta", classification);
867
+ return {
868
+ action: "recheck",
869
+ class: classification.class,
870
+ evidence: classification.truncated
871
+ ? `GitHub truncated the delta at ${classification.files.length} file(s) — the unseen remainder may hold changes this review has not seen`
872
+ : `${classification.significant.length} changed file(s) this review has not seen`,
873
+ classification,
874
+ };
875
+ }
876
+
877
+ // ---- the order handed to the session --------------------------------------
878
+
879
+ /** `pr-<n>-recheck-<head12>.json` — the decision file's name under `verdict/`,
880
+ * and the findings record's under `state/findings/`. */
881
+ export function recheckDecisionFile(pr: number, headSha: string): string {
882
+ return `pr-${pr}-recheck-${recheckHead12(headSha)}.json`;
883
+ }
884
+
885
+ export const RECHECK_FILE_RE = /^pr-(\d+)-recheck-([0-9a-f]{7,40})\.json$/i;
886
+
887
+ /** The runaway-guard identity of one recheck question: (trigger, head, ci
888
+ * conclusion, base target). Shared by runRecheck's claim and the reconcile
889
+ * backstop's has-this-been-asked check. */
890
+ export function recheckFingerprint(
891
+ req: {
892
+ trigger: RecheckTrigger;
893
+ head_sha?: string;
894
+ ci?: { conclusion?: string | null } | null;
895
+ base?: { after?: string } | null;
896
+ },
897
+ headOverride?: string,
898
+ ): string {
899
+ const head = headOverride ?? req.head_sha ?? "";
900
+ return [req.trigger, head, req.ci?.conclusion ?? "", req.base?.after ?? ""].join(":");
901
+ }
902
+
903
+ /** Fallback renderer (stub-wired daemon, unit tests): the order as JSON. */
904
+ export function defaultRecheckPrompt(order: RecheckOrder): string {
905
+ return JSON.stringify(order, null, 2);
906
+ }
907
+
908
+ // ---- the runner -----------------------------------------------------------
909
+
910
+ /** Read-only adapter slice. Every call here is a GET; this module never writes
911
+ * to GitHub, so no mutation can escape the gate through it. */
912
+ export interface RecheckAdapter {
913
+ getPr(n: number): Promise<{
914
+ base_ref: string;
915
+ head_sha: string;
916
+ state: string;
917
+ merged: boolean;
918
+ title?: string;
919
+ }>;
920
+ listPrFiles(n: number): Promise<DeltaList>;
921
+ /** `GET /compare/<base>...<head>` — the files a range changed. */
922
+ compareFiles(base: string, head: string): Promise<DeltaList>;
923
+ }
924
+
925
+ export interface RecheckDeps {
926
+ cfg: Config;
927
+ store: StateStore;
928
+ /** Partial: a daemon wired without the compare read simply never rechecks
929
+ * (logged). */
930
+ adapter: Partial<RecheckAdapter>;
931
+ /** SessionHooks.inject — the nudge goes to the PR's existing session. */
932
+ inject: (pr: number, message: string) => Promise<void>;
933
+ renderPrompt?: (order: RecheckOrder) => string;
934
+ /**
935
+ * The repo-fact lessons for the delta's files (src/lessonselect.ts
936
+ * `repoFacts`), rendered as a section APPENDED to the
937
+ * nudge — "" unless `[memory] delivery = "on"`, so the nudge is otherwise
938
+ * byte-identical. Bounded by the lesson prefetch's budget; a throw or a
939
+ * rejection is "" (the nudge goes out without it).
940
+ */
941
+ repoFacts?: (q: {
942
+ pr: number;
943
+ title: string;
944
+ paths: readonly string[];
945
+ head_sha: string;
946
+ purpose: "recheck";
947
+ }) => Promise<string>;
948
+ worktreeFor?: (pr: number) => string;
949
+ /** Hand-resolved merge files between two shas (WorktreeManager
950
+ * .resolvedFilesInMerges, wired by the daemon). Absent ⇒ merges classify by
951
+ * the ownership rule alone. */
952
+ resolvedFilesInMerges?: (
953
+ from: string,
954
+ to: string,
955
+ pr?: number,
956
+ ) => Promise<{ files: string[]; degraded: boolean }>;
957
+ now?: () => Date;
958
+ log?: (msg: string, extra?: unknown) => void;
959
+ }
960
+
961
+ export interface RecheckRequest {
962
+ pr: number;
963
+ trigger: RecheckTrigger;
964
+ /** The head the trigger is about; defaults to the PR row's head. */
965
+ head_sha?: string;
966
+ ci?: { conclusion: string | null };
967
+ /** The base advance: which branch moved, and between which shas. */
968
+ base?: { branch: string; before: string; after: string };
969
+ }
970
+
971
+ export type RecheckOutcome = RecheckPlan & {
972
+ delivered?: "injected" | "held";
973
+ order?: RecheckOrder;
974
+ };
975
+
976
+ /**
977
+ * The files the PR owns for delta classification: what it owns at the current
978
+ * head PLUS what it owned at the REVIEWED head. A push that
979
+ * restores a reviewed file to its base content drops it from the current diff,
980
+ * yet that is the PR undoing its own reviewed change, not a base merge. The
981
+ * second read happens only when a delta file falls outside today's ownership:
982
+ * `<base_ref>...<reviewed>` is the PR's own diff at the reviewed head. Throws
983
+ * on a read failure — every caller already treats that as "not trivial".
984
+ */
985
+ export async function reviewedOwnership(
986
+ adapter: {
987
+ getPr?(n: number): Promise<{ base_ref: string }>;
988
+ compareFiles?(base: string, head: string): Promise<DeltaList>;
989
+ },
990
+ pr: number,
991
+ reviewedHead: string,
992
+ delta: readonly DeltaFile[],
993
+ ownedNow: ReadonlySet<string> | undefined,
994
+ ): Promise<Set<string> | undefined> {
995
+ // Unknown ownership (a truncated listing) stays unknown.
996
+ if (!ownedNow) return undefined;
997
+ const unowned = delta.some((f) => !ownedBy(f, ownedNow));
998
+ if (!unowned || !adapter.getPr || !adapter.compareFiles) return new Set(ownedNow);
999
+ const { base_ref } = await adapter.getPr(pr);
1000
+ if (!base_ref) return new Set(ownedNow);
1001
+ const then = ownershipSet(await adapter.compareFiles(base_ref, reviewedHead));
1002
+ return then ? new Set([...ownedNow, ...then]) : undefined;
1003
+ }
1004
+
1005
+ /**
1006
+ * The files hand-resolved inside the merges of `from..to` — the one reading
1007
+ * of the merge check both the recheck and dispatch's system re-trigger
1008
+ * classifier make. No check wired ⇒ none. Degraded (git could not tell) keeps
1009
+ * the delta-only classification, but is recorded as an event and a durable
1010
+ * anomaly row — never silently quieter. `via` names a caller other than the
1011
+ * recheck.
1012
+ */
1013
+ export async function verifyMerges(
1014
+ store: StateStore,
1015
+ resolve: RecheckDeps["resolvedFilesInMerges"],
1016
+ from: string,
1017
+ to: string,
1018
+ pr: number,
1019
+ via?: string,
1020
+ ): Promise<string[]> {
1021
+ if (!resolve) return [];
1022
+ const res = await resolve(from, to, pr);
1023
+ if (res.degraded) {
1024
+ store.logEvent("recheck_merge_verify_degraded", pr, { from, to, ...(via ? { via } : {}) });
1025
+ store.logAnomaly(
1026
+ "recheck_merge_verify_degraded",
1027
+ pr,
1028
+ null,
1029
+ `merge-verify degraded ${from.slice(0, 7)}→${to.slice(0, 7)}${via ? ` at ${via}` : ""} — delta-only classification kept`,
1030
+ );
1031
+ }
1032
+ return res.files;
1033
+ }
1034
+
1035
+ /**
1036
+ * One trigger, start to finish. Returns the plan so callers (and tests) can see
1037
+ * exactly why a trigger cost nothing.
1038
+ */
1039
+ export async function runRecheck(deps: RecheckDeps, req: RecheckRequest): Promise<RecheckOutcome> {
1040
+ const { cfg, store } = deps;
1041
+ const log = deps.log ?? ((): void => {});
1042
+ const now = deps.now ?? ((): Date => new Date());
1043
+
1044
+ const done = (plan: RecheckPlan): RecheckOutcome => {
1045
+ if (plan.action === "noop")
1046
+ store.logEvent("recheck_noop", req.pr, {
1047
+ trigger: req.trigger,
1048
+ reason: plan.reason,
1049
+ delta_class: plan.class,
1050
+ files: plan.classification?.files.map((f) => `${f.filename} (${f.reason})`) ?? [],
1051
+ });
1052
+ return plan;
1053
+ };
1054
+
1055
+ // Deploy-window drain: generate no recheck nudge at all —
1056
+ // prefer not creating work over hold-then-flush, because the session the
1057
+ // nudge would wake is about to be killed by the deploy. The reconcile
1058
+ // push-backstop re-raises the trigger after the lift.
1059
+ if (store.drainState().draining)
1060
+ return done({ action: "noop", reason: "draining", class: "empty" });
1061
+
1062
+ if (!cfg.recheck.enabled || !triggerEnabled(cfg, req.trigger))
1063
+ return done({ action: "noop", reason: "disabled", class: "empty" });
1064
+ // Cheapest refusal first: ping-pong owns the author reply.
1065
+ if (req.trigger === "author_reply")
1066
+ return done(planRecheck({ ...emptyPlanInput(req), standing: "APPROVE" }));
1067
+
1068
+ const mark = store.reviewedHead(req.pr);
1069
+ if (standingVerdict(mark) !== "APPROVE")
1070
+ return done({ action: "noop", reason: "no-standing-approval", class: "empty" });
1071
+
1072
+ const row = store.getPr(req.pr);
1073
+ if (!row || row.state === "closed")
1074
+ return done({ action: "noop", reason: "pr-not-open", class: "empty" });
1075
+ // A pause stops this PR's review work, a nudge included — it defers the
1076
+ // question, never loses it. A push question is asked again by the reconcile
1077
+ // backstop once the pause lifts; a CI or base-advance question has no such
1078
+ // sweep, so it goes on to its claim and its nudge is held until the resume
1079
+ // (`deferWhilePaused` below).
1080
+ if (row.paused && req.trigger === "push")
1081
+ return done({ action: "noop", reason: "paused", class: "empty" });
1082
+
1083
+ const head = req.head_sha || row.head_sha;
1084
+ if (head.toLowerCase() !== row.head_sha.toLowerCase())
1085
+ return done({ action: "noop", reason: "stale-head", class: "empty" });
1086
+ const lastReviewed = mark!.head_sha;
1087
+ // Refuse before spending a single API call on a head we already decided.
1088
+ if (unchangedHeadNoop(req.trigger, head, lastReviewed, req.ci))
1089
+ return done({ action: "noop", reason: "unchanged-head", class: "empty" });
1090
+ // The range the delta is read over — and the one its merges are checked in.
1091
+ const range =
1092
+ req.trigger === "base_advance" && req.base
1093
+ ? { from: req.base.before, to: req.base.after }
1094
+ : { from: lastReviewed, to: head };
1095
+
1096
+ if (!deps.adapter.compareFiles || !deps.adapter.listPrFiles)
1097
+ return done({ action: "noop", reason: "no-delta-source", class: "empty" });
1098
+
1099
+ let delta: DeltaList;
1100
+ let prFiles: Set<string> | undefined;
1101
+ try {
1102
+ delta = await deps.adapter.compareFiles(range.from, range.to);
1103
+ prFiles = ownershipSet(await deps.adapter.listPrFiles(req.pr));
1104
+ // Ownership is the union of the reviewed head's and the current head's —
1105
+ // see reviewedOwnership. A base advance leaves the
1106
+ // head where it was reviewed, so there is nothing to revert. A truncated
1107
+ // listing leaves ownership unknown (undefined): every delta file is then
1108
+ // significant.
1109
+ if (req.trigger !== "base_advance")
1110
+ prFiles = await reviewedOwnership(deps.adapter, req.pr, lastReviewed, delta, prFiles);
1111
+ } catch (err) {
1112
+ // A read failure is not evidence of anything. A question the reconcile
1113
+ // push backstop asks again — a push, or a benign CI result at a new head,
1114
+ // whose push question is the same — upholds and is asked once the read
1115
+ // recovers. An adverse CI result is asked whatever its delta holds, so it
1116
+ // goes on with the delta UNREAD and the session is told so. A base advance
1117
+ // is NOT asked unread: almost every one touches no PR-owned file, and
1118
+ // waking a session on an advance nobody could classify is the delta-blind
1119
+ // staleness check the recheck exists to avoid. It upholds, with this
1120
+ // anomaly row as its record — nothing asks it again.
1121
+ const askUnread = ciIsAdverse(req.ci?.conclusion);
1122
+ log(`recheck: #${req.pr} delta read failed`, err);
1123
+ store.logEvent("recheck_delta_failed", req.pr, {
1124
+ trigger: req.trigger,
1125
+ error: String(err),
1126
+ asked_unread: askUnread,
1127
+ });
1128
+ // Durable anomaly row, quiet class: DB-only, reviewed at the daily
1129
+ // heartbeat.
1130
+ store.logAnomaly(
1131
+ "recheck_delta_failed",
1132
+ req.pr,
1133
+ null,
1134
+ `delta read failed (${req.trigger}): ${String(err)} — ${
1135
+ askUnread
1136
+ ? "asked with the delta unread"
1137
+ : req.trigger === "base_advance"
1138
+ ? "upheld unread; nothing asks this base advance again"
1139
+ : "the reconcile push backstop asks again"
1140
+ }`,
1141
+ );
1142
+ if (!askUnread) return done({ action: "noop", reason: "no-delta-source", class: "empty" });
1143
+ delta = unreadDelta();
1144
+ prFiles = undefined;
1145
+ }
1146
+
1147
+ // Conflict resolutions are never trivial: ask git which files in this
1148
+ // delta's merges were hand-resolved.
1149
+ const resolvedFiles = await verifyMerges(
1150
+ store,
1151
+ deps.resolvedFilesInMerges,
1152
+ range.from,
1153
+ range.to,
1154
+ req.pr,
1155
+ );
1156
+
1157
+ const plan = planRecheck({
1158
+ trigger: req.trigger,
1159
+ standing: standingVerdict(mark),
1160
+ last_reviewed_sha: lastReviewed,
1161
+ head_sha: head,
1162
+ delta,
1163
+ ...(prFiles ? { prFiles: [...prFiles] } : {}),
1164
+ resolvedFiles,
1165
+ ...(req.ci ? { ci: req.ci } : {}),
1166
+ });
1167
+ if (store.getPr(req.pr)?.head_sha.toLowerCase() !== head.toLowerCase())
1168
+ return done({ action: "noop", reason: "stale-head", class: "empty" });
1169
+ // Carry-forward: a withheld approval pinned to an old sha would be stranded
1170
+ // — the retry no-ops on "head-moved", and the recheck can never approve.
1171
+ // When the delta itself upholds (empty/trivial), the never-posted approval
1172
+ // moves with the head, so green CI at the NEW head releases it. A
1173
+ // significant delta carries only after the session's explicit uphold
1174
+ // (watcher below).
1175
+ if (plan.action === "noop") {
1176
+ if (
1177
+ (plan.reason === "empty-delta" || plan.reason === "trivial-delta") &&
1178
+ head !== lastReviewed
1179
+ ) {
1180
+ carryWithheldMark(store, req.pr, head, `recheck-noop:${plan.reason}`, log);
1181
+ // A trivial/empty answer is terminal for this head: claim it, so the
1182
+ // reconcile backstop (below in reconcile.ts) never re-classifies the
1183
+ // same question every sweep. No wake goes out, so no receipt token can
1184
+ // ever exist for it: `nudge: false` keeps the undelivered sweep from
1185
+ // reading its absence as a lost nudge and releasing it.
1186
+ store.claimRecheck(req.pr, recheckFingerprint(req, head), {
1187
+ head_sha: head,
1188
+ trigger: req.trigger,
1189
+ reason: `noop:${plan.reason}`,
1190
+ nudge: false,
1191
+ });
1192
+ }
1193
+ return done(plan);
1194
+ }
1195
+
1196
+ // The runaway guard: one nudge per (trigger, head, evidence). Without it a
1197
+ // reconcile replay or a redelivered webhook re-asks the same question and
1198
+ // pays for it every time.
1199
+ const fingerprint = recheckFingerprint(req, head);
1200
+ if (store.hasRecheck(req.pr, fingerprint))
1201
+ return done({
1202
+ action: "noop",
1203
+ reason: "already-rechecked",
1204
+ class: plan.class,
1205
+ classification: plan.classification,
1206
+ });
1207
+
1208
+ const order: RecheckOrder = {
1209
+ pr: req.pr,
1210
+ title: row.title,
1211
+ trigger: req.trigger,
1212
+ standing_verdict: "APPROVE",
1213
+ last_reviewed_sha: lastReviewed,
1214
+ head_sha: head,
1215
+ worktree: (deps.worktreeFor ?? prWorktreeRel)(req.pr),
1216
+ // A base advance's delta is the base branch's range, not anything since
1217
+ // the reviewed head: the evidence names the range that was compared.
1218
+ evidence:
1219
+ req.trigger === "base_advance" && req.base
1220
+ ? `the base branch \`${req.base.branch}\` advanced ${range.from.slice(0, 12)}→${range.to.slice(0, 12)}; ${plan.evidence}`
1221
+ : plan.evidence,
1222
+ files: plan.classification.files,
1223
+ resolved_files: resolvedFiles,
1224
+ decision_file: recheckDecisionFile(req.pr, head),
1225
+ issued_at: now().toISOString(),
1226
+ };
1227
+ const render = deps.renderPrompt ?? defaultRecheckPrompt;
1228
+ // The repo facts for the files the session must review
1229
+ // (the significant ones; every delta file when none is) — "" unless
1230
+ // lessons are delivered. Bounded and total: never a reason to hold or lose
1231
+ // the nudge.
1232
+ let facts = "";
1233
+ if (deps.repoFacts) {
1234
+ const touched = plan.classification.significant.length
1235
+ ? plan.classification.significant.map((f) => f.filename)
1236
+ : plan.classification.files.map((f) => f.filename);
1237
+ try {
1238
+ facts = await deps.repoFacts({
1239
+ pr: req.pr,
1240
+ title: row.title,
1241
+ paths: touched,
1242
+ head_sha: head,
1243
+ purpose: "recheck",
1244
+ });
1245
+ } catch (err) {
1246
+ log(`recheck: #${req.pr} repo facts unavailable (ignored)`, err);
1247
+ facts = "";
1248
+ }
1249
+ if (typeof facts !== "string") facts = "";
1250
+ }
1251
+ // Mid-round nudges are held and flushed at round end — the PR's one main
1252
+ // pi is never given two instructions at once.
1253
+ let delivered: "injected" | "held";
1254
+ let claimed = false;
1255
+ try {
1256
+ const payload = stampPrompt(
1257
+ withRepoFacts(render(order), facts),
1258
+ recheckPromptMarker(req.pr, fingerprint),
1259
+ );
1260
+ if (store.getPr(req.pr)?.head_sha.toLowerCase() !== head.toLowerCase())
1261
+ return done({ action: "noop", reason: "stale-head", class: "empty" });
1262
+ if (standingVerdict(store.reviewedHead(req.pr)) !== "APPROVE")
1263
+ return done({ action: "noop", reason: "no-standing-approval", class: "empty" });
1264
+ // A pause that landed while the reads ran: a push question is asked again
1265
+ // by the reconcile backstop once it lifts.
1266
+ if (req.trigger === "push" && store.getPr(req.pr)?.paused)
1267
+ return done({ action: "noop", reason: "paused", class: "empty" });
1268
+ claimed = store.db.transaction(() => {
1269
+ if (
1270
+ !store.claimRecheck(req.pr, fingerprint, {
1271
+ head_sha: head,
1272
+ trigger: req.trigger,
1273
+ reason: plan.evidence,
1274
+ ...(req.trigger !== "push"
1275
+ ? { delivery: { payload, reason: recheckWakeReason(req.trigger) } }
1276
+ : {}),
1277
+ })
1278
+ )
1279
+ return false;
1280
+ // A same-head question (an adverse CI conclusion, a base advance) is new
1281
+ // evidence at a head already reviewed. Recorded on the mark the posting
1282
+ // guard reads, so the change request it may lead to is not refused as a
1283
+ // duplicate of the review that stands (guard 2).
1284
+ if (head === lastReviewed) {
1285
+ store.noteNewEvidence(req.pr, now().toISOString());
1286
+ store.logEvent("recheck_evidence_noted", req.pr, {
1287
+ trigger: req.trigger,
1288
+ head_sha: head,
1289
+ evidence: plan.evidence,
1290
+ });
1291
+ }
1292
+
1293
+ return true;
1294
+ })();
1295
+ if (!claimed)
1296
+ return done({
1297
+ action: "noop",
1298
+ reason: "already-rechecked",
1299
+ class: plan.class,
1300
+ classification: plan.classification,
1301
+ });
1302
+ delivered = await wakeOrHold(
1303
+ { store, inject: deps.inject, log },
1304
+ req.pr,
1305
+ // The receipt token distinguishes confirmed delivery from owed work.
1306
+ // Pushes can be regenerated; CI/base questions retain these exact bytes.
1307
+ payload,
1308
+ recheckWakeReason(req.trigger),
1309
+ // A push question the reconcile backstop asks again; a CI or base-advance
1310
+ // question uses its durable payload, so its claim stays and the wake is held
1311
+ // for the flush paths rather than released on a failed injection — and
1312
+ // held, not delivered, while the PR is paused.
1313
+ { holdOnFailure: req.trigger !== "push", deferWhilePaused: req.trigger !== "push" },
1314
+ );
1315
+ } catch (err) {
1316
+ // Pushes have a backstop. Other triggers retain the claim and exact
1317
+ // payload so a failed injection cannot consume their only question.
1318
+ if (claimed && req.trigger === "push") store.releaseRecheck(req.pr, fingerprint);
1319
+ store.logEvent("recheck_delivery_failed", req.pr, {
1320
+ trigger: req.trigger,
1321
+ head_sha: head,
1322
+ error: String(err),
1323
+ });
1324
+ log(
1325
+ `recheck: #${req.pr} delivery failed — ${req.trigger === "push" ? "claim released for retry" : "durable question retained when claimed"}`,
1326
+ err,
1327
+ );
1328
+ return done({
1329
+ action: "noop",
1330
+ reason: "delivery-failed",
1331
+ class: plan.class,
1332
+ classification: plan.classification,
1333
+ });
1334
+ }
1335
+ store.logEvent("recheck_nudged", req.pr, {
1336
+ trigger: req.trigger,
1337
+ head_sha: head,
1338
+ last_reviewed_sha: lastReviewed,
1339
+ delta_class: plan.class,
1340
+ significant: plan.classification.significant.map((f) => f.filename),
1341
+ evidence: plan.evidence,
1342
+ delivered,
1343
+ });
1344
+ log(
1345
+ `recheck: #${req.pr} nudged (${req.trigger}, ${plan.classification.significant.length} file(s)) — ${delivered}`,
1346
+ );
1347
+ return { ...plan, delivered, order };
1348
+ }
1349
+
1350
+ function triggerEnabled(cfg: Config, trigger: RecheckTrigger): boolean {
1351
+ switch (trigger) {
1352
+ case "push":
1353
+ return cfg.recheck.on_push;
1354
+ case "ci":
1355
+ return cfg.recheck.on_ci;
1356
+ case "base_advance":
1357
+ return cfg.recheck.on_base_advance;
1358
+ case "author_reply":
1359
+ return true; // always reaches the ping-pong no-op
1360
+ }
1361
+ }
1362
+
1363
+ function emptyPlanInput(req: RecheckRequest): RecheckPlanInput {
1364
+ return {
1365
+ trigger: req.trigger,
1366
+ standing: null,
1367
+ last_reviewed_sha: "",
1368
+ head_sha: req.head_sha ?? "",
1369
+ delta: [],
1370
+ };
1371
+ }
1372
+
1373
+ /**
1374
+ * A base branch moved: fan the advance out to the PRs that target it and hold
1375
+ * a standing approval. Every other PR is skipped without an API call.
1376
+ */
1377
+ export async function runBaseAdvance(
1378
+ deps: RecheckDeps,
1379
+ advance: { branch: string; before: string; after: string },
1380
+ ): Promise<Map<number, RecheckOutcome>> {
1381
+ const out = new Map<number, RecheckOutcome>();
1382
+ const log = deps.log ?? ((): void => {});
1383
+ if (!deps.cfg.recheck.enabled || !deps.cfg.recheck.on_base_advance) return out;
1384
+ // Deploy-window drain: skip the whole fan-out before it spends per-PR API
1385
+ // calls (runRecheck would refuse each one anyway).
1386
+ if (deps.store.drainState().draining) return out;
1387
+ for (const row of deps.store.allPrs()) {
1388
+ if (row.state === "closed") continue;
1389
+ if (standingVerdict(deps.store.reviewedHead(row.number)) !== "APPROVE") continue;
1390
+ if (deps.adapter.getPr) {
1391
+ try {
1392
+ // Called on the adapter, never through a detached reference: the real
1393
+ // GitHubAdapter's methods are class methods and need their `this`.
1394
+ const ref = await deps.adapter.getPr(row.number);
1395
+ // GitHub already says closed: there is nothing left to audit, and the
1396
+ // reconcile closed-sync settles the row.
1397
+ if (ref.state === "closed" || ref.merged) continue;
1398
+ if (ref.base_ref && ref.base_ref !== advance.branch) continue;
1399
+ } catch (err) {
1400
+ log(`recheck: #${row.number} base ref lookup failed`, err);
1401
+ continue;
1402
+ }
1403
+ }
1404
+ out.set(
1405
+ row.number,
1406
+ await runRecheck(deps, { pr: row.number, trigger: "base_advance", base: advance }),
1407
+ );
1408
+ }
1409
+ return out;
1410
+ }
1411
+
1412
+ // ---- the decision file ----------------------------------------------------
1413
+
1414
+ /**
1415
+ * The session's answer. Two outcomes only — this is where invariant 1 lives.
1416
+ * `uphold` posts nothing at all; `request_changes` means the session has
1417
+ * posted its review through the gated scripts and the daemon moves the PR to
1418
+ * awaiting_author.
1419
+ */
1420
+ export interface RecheckDecision {
1421
+ pr: number;
1422
+ head_sha: string;
1423
+ outcome: "uphold" | "request_changes";
1424
+ reason?: string;
1425
+ }
1426
+
1427
+ export interface ParsedRecheckDecision {
1428
+ ok: boolean;
1429
+ decision?: RecheckDecision;
1430
+ error?: string;
1431
+ /** An outcome that was not one of the two, folded to `uphold`. */
1432
+ coerced?: string;
1433
+ }
1434
+
1435
+ export function validateRecheckDecision(raw: unknown): ParsedRecheckDecision {
1436
+ if (typeof raw !== "object" || raw === null) return { ok: false, error: "not an object" };
1437
+ const v = raw as Record<string, unknown>;
1438
+ if (typeof v.pr === "string" && /^\d+$/.test(v.pr)) v.pr = Number(v.pr);
1439
+ if (typeof v.pr !== "number" || !Number.isInteger(v.pr))
1440
+ return { ok: false, error: "pr must be an integer" };
1441
+ if (typeof v.head_sha !== "string" || v.head_sha.length === 0)
1442
+ return { ok: false, error: "head_sha must be a non-empty string" };
1443
+ const raw_outcome = typeof v.outcome === "string" ? v.outcome.trim().toLowerCase() : "";
1444
+ // NOT a rejection: an unrecognised outcome (including "approve" and
1445
+ // "needs_human", which a session may reach for out of round habit) becomes
1446
+ // the default — uphold. The recheck path has no other verdict to give.
1447
+ const coerced =
1448
+ raw_outcome === "uphold" || raw_outcome === "request_changes"
1449
+ ? undefined
1450
+ : String(v.outcome ?? "");
1451
+ const outcome: RecheckDecision["outcome"] =
1452
+ raw_outcome === "request_changes" ? "request_changes" : "uphold";
1453
+ return {
1454
+ ok: true,
1455
+ decision: {
1456
+ pr: v.pr,
1457
+ head_sha: v.head_sha,
1458
+ outcome,
1459
+ ...(typeof v.reason === "string" ? { reason: v.reason.trim().slice(0, 800) } : {}),
1460
+ },
1461
+ ...(coerced !== undefined ? { coerced } : {}),
1462
+ };
1463
+ }
1464
+
1465
+ export interface RecheckWatcherDeps {
1466
+ cfg: Config;
1467
+ store: StateStore;
1468
+ verdictDir: string;
1469
+ /** Ledger freshness enforcement, run after an uphold — the session just
1470
+ * examined a significant delta, so its ledger should name the upheld head.
1471
+ * Optional: absent (stubs, tests) ⇒ no check. Best-effort — never allowed
1472
+ * to disturb the decision (the caller wraps it). */
1473
+ checkLedger?: (pr: number, headSha: string) => Promise<void>;
1474
+ /**
1475
+ * Where the session writes reasoning records — `<root>/state/findings`. A
1476
+ * recheck that MOVES the verdict posts real findings, and "how often the
1477
+ * review backs down" counts those too, so its record is parsed into the
1478
+ * same rows.
1479
+ *
1480
+ * Deliberately weaker than the round path: read-if-present, never a nudge
1481
+ * and never an anomaly. A recheck has no round row and no deadline
1482
+ * underneath it, so a claim-then-nudge here would have nothing to bound it.
1483
+ * Absent ⇒ nothing happens.
1484
+ */
1485
+ findingsDir?: string;
1486
+ now?: () => Date;
1487
+ log?: (msg: string, extra?: unknown) => void;
1488
+ /** Partial-write tolerance (`packages/pi-plugin/src/settledread.ts`) — fs.watch
1489
+ * fires on creation, before the session's write completes; the same race,
1490
+ * and the same reader, as the VerdictWatcher's. `{ windowMs: 0 }` makes one
1491
+ * read decide (tests). */
1492
+ partialWrite?: Partial<PartialWritePolicy>;
1493
+ }
1494
+
1495
+ /**
1496
+ * Consumes `verdict/pr-<n>-recheck-<head12>.json`. It touches GitHub not at
1497
+ * all: on `uphold` there is nothing to say, and on `request_changes` the
1498
+ * session has already posted the review through the gated scripts — the daemon
1499
+ * only moves the lifecycle state.
1500
+ *
1501
+ * Neither outcome moves the reviewed-head mark to a new head. The mark
1502
+ * records what was really REVIEWED and posted; an upheld recheck posted
1503
+ * nothing, and stamping it would make the no-new-evidence guard swallow a
1504
+ * genuine review re-request at that head. A change request the session posted
1505
+ * was stamped by the posting route itself; the watcher only writes its
1506
+ * decided verdict onto that stamp (`settleChangeRequest`). The recheck's own
1507
+ * runaway guard is the `rechecks` claim table.
1508
+ */
1509
+ export class RecheckWatcher {
1510
+ private watcher: FSWatcher | null = null;
1511
+ private readonly inFlight = new Set<string>();
1512
+ private readonly processedDir: string;
1513
+
1514
+ constructor(private readonly deps: RecheckWatcherDeps) {
1515
+ this.processedDir = join(deps.verdictDir, "processed");
1516
+ }
1517
+
1518
+ start(): void {
1519
+ mkdirSync(this.deps.verdictDir, { recursive: true });
1520
+ void this.sweep();
1521
+ this.watcher = watch(this.deps.verdictDir, (_event, filename) => {
1522
+ const name = filename ? String(filename) : "";
1523
+ if (RECHECK_FILE_RE.test(name)) void this.handleFile(name);
1524
+ });
1525
+ }
1526
+
1527
+ stop(): void {
1528
+ this.watcher?.close();
1529
+ this.watcher = null;
1530
+ }
1531
+
1532
+ /** One PR's decisions in name order, different PRs side by side: a file
1533
+ * still being written is waited out for seconds and must not
1534
+ * hold another PR's decision behind it. */
1535
+ async sweep(): Promise<string[]> {
1536
+ if (!existsSync(this.deps.verdictDir)) return [];
1537
+ const byPr = new Map<string, string[]>();
1538
+ for (const name of readdirSync(this.deps.verdictDir).sort()) {
1539
+ const match = RECHECK_FILE_RE.exec(name);
1540
+ if (!match) continue;
1541
+ byPr.set(match[1]!, [...(byPr.get(match[1]!) ?? []), name]);
1542
+ }
1543
+ const done: string[] = [];
1544
+ await Promise.all(
1545
+ [...byPr.values()].map(async (names) => {
1546
+ for (const name of names) if (await this.handleFile(name)) done.push(name);
1547
+ }),
1548
+ );
1549
+ return done.sort();
1550
+ }
1551
+
1552
+ private archive(name: string): void {
1553
+ mkdirSync(this.processedDir, { recursive: true });
1554
+ const ts = (this.deps.now ?? ((): Date => new Date()))().toISOString().replace(/[:.]/g, "-");
1555
+ try {
1556
+ renameSync(join(this.deps.verdictDir, name), join(this.processedDir, `${ts}-${name}`));
1557
+ } catch (err) {
1558
+ this.deps.log?.(`recheck: could not archive ${name}`, err);
1559
+ }
1560
+ }
1561
+
1562
+ /**
1563
+ * Parse `state/findings/pr-<n>-recheck-<head12>.json` into `round_findings`.
1564
+ *
1565
+ * A recheck owns no round row, so its entries are attributed to the round
1566
+ * whose verdict it just moved — the PR's reviewed-head mark, falling back to
1567
+ * its highest round number. Keys are prefixed `recheck:<head12>:` so a
1568
+ * recheck's rows can never collide with that round's own.
1569
+ */
1570
+ private async recordRecheckFindings(pr: number, headSha: string): Promise<void> {
1571
+ const dir = this.deps.findingsDir;
1572
+ if (!dir) return;
1573
+ try {
1574
+ const path = join(dir, recheckDecisionFile(pr, headSha));
1575
+ const store = this.deps.store;
1576
+ const roundNo =
1577
+ store.reviewedHead(pr)?.round_no ??
1578
+ store.roundsFor(pr).reduce<number>((m, r) => (r.round_no > m ? r.round_no : m), 0);
1579
+ const parsed = readFindingsRecord(path, pr, roundNo);
1580
+ if (parsed.status !== "ok" || !parsed.record) {
1581
+ store.logEvent("recheck_findings_record_absent", pr, {
1582
+ head_sha: headSha,
1583
+ status: parsed.status,
1584
+ });
1585
+ this.deps.log?.(
1586
+ `recheck: #${pr} moved the verdict with no findings record (${parsed.status}) — decision stands`,
1587
+ );
1588
+ return;
1589
+ }
1590
+ const entries = parsed.record.findings.map((f) => ({
1591
+ ...f,
1592
+ key: `${recheckKeyPrefix(headSha)}${f.key}`,
1593
+ }));
1594
+ store.recordRoundFindings(
1595
+ { pr, round_id: null, round_no: roundNo, head_sha: headSha },
1596
+ entries,
1597
+ );
1598
+ store.logEvent("recheck_findings_recorded", pr, {
1599
+ head_sha: headSha,
1600
+ round_no: roundNo,
1601
+ entries: entries.length,
1602
+ });
1603
+ } catch (err) {
1604
+ this.deps.log?.(`recheck: #${pr} findings record read failed`, err);
1605
+ }
1606
+ }
1607
+
1608
+ /**
1609
+ * The lifecycle half of a change request the session posted. The review on
1610
+ * GitHub is the standing decision at its head: when the shell stamped it at
1611
+ * the head the mark already held, the mark keeps the verdict it had, so the
1612
+ * decided verdict is written here — whether it posted as REQUEST_CHANGES or
1613
+ * guard 1 softened it to a COMMENT (a person approved the head), the same
1614
+ * enrichment a round's verdict gets. A mark still reading APPROVE posted
1615
+ * holds no post of this decision and is left as it is. The lifecycle moves to awaiting_author
1616
+ * unless newer work owns the PR — a round running, or a person's queued
1617
+ * re-review request — which this answer must not settle or erase; that
1618
+ * work's own verdict decides. The PR's head is GitHub's and is never
1619
+ * rewound to the decision's.
1620
+ */
1621
+ private settleChangeRequest(pr: number, headSha: string, file: string): void {
1622
+ const { store } = this.deps;
1623
+ const mark = store.reviewedHead(pr);
1624
+ if (
1625
+ mark &&
1626
+ mark.head_sha.toLowerCase() === headSha.toLowerCase() &&
1627
+ (mark.posted_event === "REQUEST_CHANGES" || mark.posted_event === "COMMENT") &&
1628
+ standingVerdict(mark) !== "REQUEST_CHANGES"
1629
+ )
1630
+ store.recordReviewedVerdict(pr, mark.head_sha, "REQUEST_CHANGES");
1631
+ const row = store.getPr(pr);
1632
+ if (
1633
+ row?.state === "reviewing" ||
1634
+ (row?.state === "queued" && row.trigger_provenance === "human")
1635
+ ) {
1636
+ store.logEvent("recheck_lifecycle_deferred", pr, {
1637
+ file,
1638
+ head_sha: headSha,
1639
+ state: row.state,
1640
+ trigger_provenance: row.trigger_provenance ?? null,
1641
+ });
1642
+ return;
1643
+ }
1644
+ applyEvent(
1645
+ store,
1646
+ pr,
1647
+ { type: "verdict", verdict: "REQUEST_CHANGES" },
1648
+ {},
1649
+ {
1650
+ botLogin: this.deps.cfg.github.bot_login,
1651
+ ...(this.deps.now ? { now: this.deps.now } : {}),
1652
+ reason: "recheck:request-changes",
1653
+ },
1654
+ );
1655
+ }
1656
+
1657
+ private superseded(pr: number, head: string): boolean {
1658
+ const { store } = this.deps;
1659
+ const mark = store.reviewedHead(pr);
1660
+ if (!mark || mark.head_sha.toLowerCase() === head.toLowerCase()) return false;
1661
+ if (mark.head_sha.toLowerCase() === store.getPr(pr)?.head_sha.toLowerCase()) return true;
1662
+ const claims = store
1663
+ .rechecksFor(pr)
1664
+ .filter((claim) => claim.head_sha.toLowerCase() === head.toLowerCase());
1665
+ return claims.some((claim) => mark.reviewed_at > claim.created_at);
1666
+ }
1667
+
1668
+ async handleFile(name: string): Promise<boolean> {
1669
+ const match = RECHECK_FILE_RE.exec(name);
1670
+ if (!match || this.inFlight.has(name)) return false;
1671
+ this.inFlight.add(name);
1672
+ try {
1673
+ const path = join(this.deps.verdictDir, name);
1674
+ if (!existsSync(path)) return false;
1675
+ const pr = Number(match[1]);
1676
+ // fs.watch fires the moment the file appears, often before the
1677
+ // session's write completes, so a single eager read would discard valid
1678
+ // decisions as "unparseable". An unparseable file is waited out while
1679
+ // it is still moving; one that parses is judged at once.
1680
+ const read = await readSettledJson(path, resolvePartialWritePolicy(this.deps.partialWrite));
1681
+ if (read.kind === "missing") return false;
1682
+ const reads = read.reads;
1683
+ const parsed: ParsedRecheckDecision =
1684
+ read.kind === "parsed"
1685
+ ? validateRecheckDecision(read.value)
1686
+ : { ok: false, error: `unparseable: ${read.error}` };
1687
+ if (!parsed.ok || !parsed.decision) {
1688
+ // Malformed is the safe direction here — the standing verdict simply
1689
+ // stays standing. No park, no anomaly round, nothing posted. But say
1690
+ // so out loud: a silent drop would hide a partial-read race.
1691
+ this.deps.store.logEvent("recheck_decision_malformed", pr, {
1692
+ file: name,
1693
+ error: parsed.error ?? "unknown",
1694
+ });
1695
+ this.deps.log?.(
1696
+ `recheck: #${pr} decision ${name} malformed after ${reads} read(s) — standing verdict kept (${parsed.error ?? "unknown"})`,
1697
+ );
1698
+ this.archive(name);
1699
+ return true;
1700
+ }
1701
+ const d = parsed.decision;
1702
+ if (d.pr !== pr || recheckHead12(d.head_sha) !== (match[2] as string).toLowerCase()) {
1703
+ this.deps.store.logEvent("recheck_decision_rejected", pr, {
1704
+ file: name,
1705
+ reason: "pr/head do not match the filename",
1706
+ });
1707
+ this.archive(name);
1708
+ return true;
1709
+ }
1710
+ if (parsed.coerced !== undefined)
1711
+ this.deps.store.logEvent("recheck_outcome_coerced", pr, {
1712
+ file: name,
1713
+ given: parsed.coerced,
1714
+ applied: "uphold",
1715
+ });
1716
+
1717
+ if (this.superseded(pr, d.head_sha)) {
1718
+ this.deps.store.logEvent("recheck_decision_superseded", pr, {
1719
+ file: name,
1720
+ head_sha: d.head_sha,
1721
+ });
1722
+ this.archive(name);
1723
+ return true;
1724
+ }
1725
+ if (d.outcome === "request_changes") {
1726
+ // The reasoning behind a backed-down approval. Best-effort
1727
+ // and strictly additive: a missing record here is logged and nothing
1728
+ // else — the decision above is what moves the PR.
1729
+ await this.recordRecheckFindings(pr, d.head_sha);
1730
+ // Ingestion yields: a newer round may have settled in the meantime.
1731
+ if (this.superseded(pr, d.head_sha)) {
1732
+ this.archive(name);
1733
+ return true;
1734
+ }
1735
+ // Approval is off the table now — a lingering withheld mark would let
1736
+ // a later green CI re-queue "for the withheld approval" of a PR whose
1737
+ // standing verdict this very decision just flipped.
1738
+ this.deps.store.clearApprovalWithheld(pr);
1739
+ this.settleChangeRequest(pr, d.head_sha, name);
1740
+ }
1741
+ if (d.outcome === "uphold") {
1742
+ // Session-examined significant delta, verdict upheld: the withheld
1743
+ // approval (if any) follows the head.
1744
+ carryWithheldMark(this.deps.store, pr, d.head_sha, "recheck-uphold", (m) =>
1745
+ this.deps.log?.(m),
1746
+ );
1747
+ // Ledger freshness after the uphold (optional wiring).
1748
+ if (this.deps.checkLedger && this.deps.store.getPr(pr)?.state !== "closed") {
1749
+ try {
1750
+ await this.deps.checkLedger(pr, d.head_sha);
1751
+ } catch (err) {
1752
+ this.deps.log?.(`recheck: ledger check failed on #${pr}`, err);
1753
+ }
1754
+ }
1755
+ }
1756
+ this.deps.store.logEvent(
1757
+ d.outcome === "request_changes" ? "recheck_request_changes" : "recheck_upheld",
1758
+ pr,
1759
+ {
1760
+ file: name,
1761
+ head_sha: d.head_sha,
1762
+ reason: d.reason ?? null,
1763
+ },
1764
+ );
1765
+ this.deps.log?.(`recheck: #${pr} ${d.outcome} at ${d.head_sha.slice(0, 7)}`);
1766
+ this.archive(name);
1767
+ return true;
1768
+ } catch (err) {
1769
+ this.deps.log?.(`recheck: ${name} failed`, err);
1770
+ return false;
1771
+ } finally {
1772
+ this.inFlight.delete(name);
1773
+ }
1774
+ }
1775
+ }