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