@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
@@ -0,0 +1,2251 @@
1
+ // Deterministic dispatch. No LLM in this path: FIFO by trigger time, at most
2
+ // `daemon.max_concurrent_rounds` rounds in flight, triggers consumed at
3
+ // dispatch, lenses/gates derived from the files API by the heuristics.
4
+ //
5
+ // Before paying for a re-review round, a SYSTEM-provenance trigger (push / CI /
6
+ // staleness — `pr_state.trigger_provenance`, stamped by
7
+ // statemachine.applyEvent) is run through the recheck delta classifier; an
8
+ // empty/trivial delta since the reviewed head consumes the trigger and upholds
9
+ // the standing verdict instead of dispatching. A human ask (fresh label, fresh
10
+ // team re-review request, author reply) is never short-circuited — but like
11
+ // every trigger it runs no round at a head already reviewed with no author
12
+ // reply since (guard 2's daemon half): it settles on the standing verdict.
13
+ //
14
+ // The round order (types.ts `RoundOrder`) is the daemon↔session contract; the
15
+ // plan and the order are built by the review planner (packages/pi-plugin/src/planner.ts), and the
16
+ // prompt renderer (packages/pi-plugin/src/templates.ts) is injected so this module has no content
17
+ // dependency.
18
+ import { botAccountLogin } from "@quickchatai/pi-quickreview/src/identity.js";
19
+ import { existsSync, mkdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
20
+ import { join, resolve } from "node:path";
21
+ import type { Config } from "./config.js";
22
+ import { githubCloneDir, sandboxRoot } from "./config.js";
23
+ import {
24
+ fingerprintRoundIntegrity,
25
+ INTEGRITY_BUDGET_MS,
26
+ roundIntegrityOpts,
27
+ type IntegrityFingerprint,
28
+ type RoundIntegrityOpts,
29
+ } from "./execboundary.js";
30
+ import { withinBudget } from "@quickchatai/pi-quickreview/src/budget.js";
31
+ import type { GateResult } from "@quickchatai/pi-quickreview/src/gate.js";
32
+ import type { PrFileEntry } from "./github.js";
33
+ import { DJANGO_ECOSYSTEM, type Ecosystem } from "@quickchatai/pi-quickreview/src/ecosystem.js";
34
+ import { STYLE_PASS, LINT_CONFORMANCE_PASS } from "@quickchatai/pi-quickreview/src/heuristics.js";
35
+ import type { HeuristicsExtension } from "@quickchatai/pi-quickreview/src/heuristics.js";
36
+ import { lensEnabled } from "@quickchatai/pi-quickreview/src/lenstuning.js";
37
+ import type { MigrationCollision } from "@quickchatai/pi-quickreview/src/migrations.js";
38
+ import { VALIDATE_PASS } from "@quickchatai/pi-quickreview/src/include.js";
39
+ import { hasNewEvidence } from "@quickchatai/pi-quickreview/src/guards.js";
40
+ import {
41
+ addTierOf,
42
+ buildPassPlan,
43
+ buildRoundOrder,
44
+ decideFollowUp,
45
+ lensesOf,
46
+ optionalOrderOf,
47
+ renderDeltaPatch,
48
+ reviewedHeadOf,
49
+ roundDeltaOf,
50
+ sweepOrderOf,
51
+ type FollowUpDecision,
52
+ type FollowUpFullReason,
53
+ type PassPlan,
54
+ type PreparedTree,
55
+ } from "@quickchatai/pi-quickreview/src/planner.js";
56
+ import {
57
+ assembleRoundMemory,
58
+ describeGaps,
59
+ pinnedHeadState,
60
+ MEMORY_LESS,
61
+ roundContextQuery,
62
+ ROUND_CONTEXT_DEFAULTS,
63
+ type RoundMemory,
64
+ type RoundMemorySeed,
65
+ } from "./memory/context.js";
66
+ import {
67
+ carryWithheldMark,
68
+ reviewedOwnership,
69
+ classifyDelta,
70
+ ownershipSet,
71
+ recheckFingerprint,
72
+ verifyMerges,
73
+ type DeltaClass,
74
+ type DeltaList,
75
+ } from "./recheck.js";
76
+ import { postingReceiptPath, readPostingReceipt } from "./posting-receipt.js";
77
+ import type { RoundLessonHook, RoundSelectionResult } from "./lessonselect.js";
78
+ import {
79
+ buildRoundDelivery,
80
+ deliversAnything,
81
+ deliveryCounts,
82
+ onlyWritten,
83
+ type RoundLessonDelivery,
84
+ } from "./memory/delivery.js";
85
+ import { findStyleGuide } from "@quickchatai/pi-quickreview/src/styleguide.js";
86
+ import { findCiLinters } from "@quickchatai/pi-quickreview/src/lintconfig.js";
87
+ import { writeLessonFiles, writePassTasks } from "@quickchatai/pi-quickreview/src/passtasks.js";
88
+ import {
89
+ CONTEXT_PACK_FILE,
90
+ readWorktreeFileForPack,
91
+ renderContextPack,
92
+ } from "@quickchatai/pi-quickreview/src/contextpack.js";
93
+ import { renderGateNotes } from "@quickchatai/pi-quickreview/src/templates.js";
94
+ import { firstPartyDirectiveFiles } from "@quickchatai/pi-quickreview/src/severity.js";
95
+ import type { DispatchAnnouncer } from "./slack/announce.js";
96
+ import { applyEvent, restoreStandingLifecycle, setPrGates } from "./statemachine.js";
97
+ import type { ApprovalWithheldRow, PrStateRow, ReviewedHeadRow, StateStore } from "./state.js";
98
+ import { snapshotMark, standingReview } from "@quickchatai/pi-quickreview/src/standing-review.js";
99
+ import { roundPromptMarker } from "@quickchatai/pi-quickreview/src/types.js";
100
+ import type {
101
+ LedgerCarry,
102
+ PassPlanEntry,
103
+ PassTier,
104
+ RecordedAddablePass,
105
+ RoundDelta,
106
+ RoundOrder,
107
+ RoundRecoveryContext,
108
+ RoundType,
109
+ SessionHooks,
110
+ } from "@quickchatai/pi-quickreview/src/types.js";
111
+ import { preparePriorFindings } from "@quickchatai/pi-quickreview/src/priorfindings.js";
112
+ import { type PrOwnedDelta, prWorktreeRel } from "./worktrees.js";
113
+
114
+ /** The slice of GitHubAdapter dispatch needs — all mutations are gated inside
115
+ * the adapter, so this module never touches GitHub directly. */
116
+ export interface DispatchAdapter {
117
+ listReviewsFull?(pr: number): Promise<Array<{ user: string; body: string; commit_id: string }>>;
118
+ listPrFiles(n: number): Promise<PrFileEntry[] & { truncated?: boolean }>;
119
+ /** Read-only; dispatch needs `base_ref` to keep the base branch fresh, and
120
+ * `body` as pass-intent's artifact + substance-bar input. Absent body ⇒ the
121
+ * intent lens is never selected. */
122
+ getPr(n: number): Promise<{ base_ref: string; body?: string }>;
123
+ addLabel(n: number, label: string): Promise<GateResult>;
124
+ removeLabel(n: number, label: string): Promise<GateResult>;
125
+ deleteTeamReviewRequest(n: number): Promise<GateResult>;
126
+ /** COMMENT delivery for the migration-collision pre-check — the
127
+ * same gated `POST /pulls/<n>/reviews` the session's review uses. */
128
+ createReview(
129
+ n: number,
130
+ payload: {
131
+ commit_id: string;
132
+ event: "APPROVE" | "REQUEST_CHANGES" | "COMMENT";
133
+ body: string;
134
+ comments?: unknown[];
135
+ },
136
+ options?: { purpose?: "migration_precheck" },
137
+ ): Promise<GateResult>;
138
+ /** `GET /compare/<base>...<head>` — read-only, the trivial-delta
139
+ * short-circuit's delta source. Optional: an adapter without it (stub-wired
140
+ * core, test fakes) never short-circuits — every re-trigger pays a full
141
+ * round. */
142
+ compareFiles?(base: string, head: string): Promise<DeltaList>;
143
+ }
144
+
145
+ export interface DispatchDeps {
146
+ cfg: Config;
147
+ store: StateStore;
148
+ adapter: DispatchAdapter;
149
+ hooks: SessionHooks;
150
+ /**
151
+ * The profile-selected ecosystem seam: the tier-forcing migration gate, the
152
+ * round pre-check, and the prompt's gate prose all come off it. Absent ⇒
153
+ * Django; the production assembly (wire.ts) always passes the profile's
154
+ * selection explicitly, so a `none` profile really runs none of the
155
+ * machinery.
156
+ */
157
+ ecosystem?: Ecosystem;
158
+ /** The template renderer, injected. Default: the order as JSON. The second
159
+ * argument is the round's memory seed; a renderer may ignore it. */
160
+ renderPrompt?: (order: RoundOrder, memory: RoundMemory) => string;
161
+ /**
162
+ * Round-memory seeding: the assembler the one per-round context call goes
163
+ * through, plus its budget/deadline. Absent ⇒ every round renders
164
+ * memory-less — memory is a seasoning on the prompt, never a precondition
165
+ * for a round.
166
+ */
167
+ memory?: RoundMemorySeed;
168
+ /**
169
+ * The round's lesson selection (src/lessonselect.ts). Under
170
+ * `[memory] delivery = "shadow"` it is started once per round, after the
171
+ * round was handed to its session (seed, prompt and marker done), and
172
+ * never awaited — nothing the session sees depends on it, and starting it
173
+ * after the seed keeps its finds out of the seed's way on the server's
174
+ * one-thread embedder. Under `on` (`delivering()`) it is awaited
175
+ * beside the seed (`select`), no lesson find sent before the seed's search
176
+ * is, and the round's task files, lessons files and prompt are rendered
177
+ * from it; a round discarded before its handoff is `discard`ed from the
178
+ * ledger. Absent ⇒ nothing runs. Must not throw (a throw is logged and
179
+ * swallowed).
180
+ */
181
+ lessons?: RoundLessonHook;
182
+ /** SETTLED digest carried from the session ledger. When absent, the digest
183
+ * comes from `ledgerCarry`'s SETTLED items. */
184
+ settledDigest?: (pr: number) => string;
185
+ /**
186
+ * The PR's mirrored ledger, re-handed in every round order
187
+ * (packages/pi-plugin/src/ledgercarry.ts). Production reads `state/ledgers/pr-<n>.md`
188
+ * (daemon.ts). Total: undefined carry ⇒ the order has no `ledger_carry`.
189
+ * Absent ⇒ no carry and an empty digest.
190
+ */
191
+ ledgerCarry?: (pr: number) => { carry?: LedgerCarry; settled: string };
192
+ worktreeFor?: (pr: number) => string;
193
+ /**
194
+ * Fetch the PR head + its base branch and materialise the worktree, before
195
+ * the round order is built. Returns what is actually on disk. Optional: the
196
+ * stub-wired daemon core and unit tests have no git, and fall back to the PR
197
+ * row's sha with no base facts.
198
+ */
199
+ prepareWorktree?: (pr: number, baseRef: string, expectedHead: string) => Promise<PreparedTree>;
200
+ /**
201
+ * Every migration file present on the base branch at a commit. Read-only
202
+ * git in the base clone, which the worktree preparation has just
203
+ * re-fetched — so `base_sha` really is the branch's tip this round. Optional:
204
+ * absent (stub-wired core, unit tests without git) ⇒ the collision pre-check
205
+ * is skipped and logged, never fatal.
206
+ */
207
+ listBaseMigrations?: (baseSha: string) => Promise<string[]>;
208
+ /**
209
+ * The most recent human author of `paths` in the history reachable from
210
+ * `sha` (WorktreeManager.lastHumanAuthor). Read-only git in the
211
+ * base clone. Optional: absent (stub-wired core, unit tests without git) ⇒
212
+ * the escalation mention falls back to config
213
+ * `[review] escalation_mention_default`.
214
+ */
215
+ directiveAuthor?: (
216
+ sha: string,
217
+ paths: string[],
218
+ ) => Promise<{ login: string | null; name: string; email: string } | null>;
219
+ /** Hand-resolved merge files between two shas (conflict resolutions are
220
+ * never trivial), for the trivial-delta short-circuit's classification.
221
+ * Same seam RecheckDeps carries. Absent ⇒ merges classify by the ownership
222
+ * rule alone. */
223
+ resolvedFilesInMerges?: (
224
+ from: string,
225
+ to: string,
226
+ pr?: number,
227
+ ) => Promise<{ files: string[]; degraded: boolean }>;
228
+ /**
229
+ * The PR-OWNED change between the last reviewed head and this round's head
230
+ * (`WorktreeManager.prOwnedDelta`): the reviewed head replayed onto the
231
+ * round's merge base and diffed against the new head. Absent (stub-wired
232
+ * core, unit tests without git) ⇒ every later round runs in full —
233
+ * `followup_full_round {reason: "no-adapter"}`.
234
+ */
235
+ prOwnedDelta?: (q: {
236
+ pr: number;
237
+ from: string;
238
+ to: string;
239
+ mergeBase: string;
240
+ baseSha?: string;
241
+ /** The merge base the reviewed round recorded, when known. */
242
+ fromMergeBase?: string;
243
+ }) => Promise<PrOwnedDelta>;
244
+ /** Pin a head a round was dispatched to review against gc
245
+ * (`WorktreeManager.pinReviewedHead`), so the next follow-up round can
246
+ * replay it after a force-push. Best-effort; absent ⇒ nothing pinned. */
247
+ pinReviewedHead?: (pr: number, sha: string) => Promise<boolean>;
248
+ /** `<root>/state/findings` — where round findings records live, read by
249
+ * the follow-up round's prior-findings list. Default: under the
250
+ * sandbox root's state dir. */
251
+ findingsDir?: string;
252
+ /** `<root>/state/posting-receipts` — read to prove an interrupted
253
+ * approval retry posted nothing before its withheld mark is restored.
254
+ * Default: under the sandbox root's state dir. */
255
+ postingReceiptDir?: string;
256
+ /** The round's integrity baseline, taken before the session hears about
257
+ * the round. Default: `fingerprintRoundIntegrity` — real, async reads of the
258
+ * clone, worktree, pi home and sandbox policy surface. Injected only by a
259
+ * test whose fake clock cannot wait on real file reads. */
260
+ integrityFingerprint?: (opts: RoundIntegrityOpts) => Promise<IntegrityFingerprint>;
261
+ /** How long the baseline may take before the round goes unchecked.
262
+ * Default INTEGRITY_BUDGET_MS. */
263
+ integrityBudgetMs?: number;
264
+ /**
265
+ * Unpack the third-party sources this PR touches and return the rendered
266
+ * note for the order (packages/pi-plugin/src/depsources.ts; `[review] dependency_sources`).
267
+ * Must never throw or hang past its own budget. Absent ⇒ no note.
268
+ */
269
+ dependencySources?: (worktreePath: string, files: PrFileEntry[]) => Promise<string>;
270
+ /** Slack announcer. Optional: absent ⇒ no Slack at all, which
271
+ * is what the stub-wired daemon core and every non-Slack test see. */
272
+ announce?: DispatchAnnouncer;
273
+ /** Profile lens-heuristics extension: the selected repo profile's
274
+ * extra per-lens path/added-line predicates, loaded ONCE at assembly
275
+ * (wire.ts, `loadProfileHeuristics`). Absent ⇒ the compiled-in generic
276
+ * selection core alone. */
277
+ heuristics?: HeuristicsExtension;
278
+ now?: () => Date;
279
+ log?: (msg: string, extra?: unknown) => void;
280
+ }
281
+
282
+ export function defaultWorktree(pr: number): string {
283
+ return prWorktreeRel(pr);
284
+ }
285
+
286
+ /** PR size at the reviewed head, summed over the files API response. */
287
+ export function diffstatOf(files: PrFileEntry[]): {
288
+ additions: number;
289
+ deletions: number;
290
+ changed_files: number;
291
+ } {
292
+ return {
293
+ additions: files.reduce((n, f) => n + (f.additions ?? 0), 0),
294
+ deletions: files.reduce((n, f) => n + (f.deletions ?? 0), 0),
295
+ changed_files: files.length,
296
+ };
297
+ }
298
+
299
+ /** Round type: no prior round → "first"; a staleness recheck → "staleness";
300
+ * anything else → "re-review". `priorRounds` counts live verdicted rounds
301
+ * only: backfilled rounds and cancelled or verdict-less attempts are not
302
+ * counted, so such an attempt leaves the next round "first". */
303
+ export function roundTypeFor(row: PrStateRow, priorRounds: number): RoundType {
304
+ if (priorRounds === 0) return "first";
305
+ if (row.stale_recheck) return "staleness";
306
+ return "re-review";
307
+ }
308
+
309
+ /** Queued PRs in FIFO order by trigger time (nulls last, then PR number).
310
+ * Overseer-paused PRs never dispatch. */
311
+ export function queuedInOrder(store: StateStore): PrStateRow[] {
312
+ return store
313
+ .allPrs()
314
+ .filter((p) => p.state === "queued" && !p.paused)
315
+ .sort((a, b) => {
316
+ const at = a.trigger_ts ?? "";
317
+ const bt = b.trigger_ts ?? "";
318
+ if (at === bt) return a.number - b.number;
319
+ if (!at) return 1;
320
+ if (!bt) return -1;
321
+ return at < bt ? -1 : 1;
322
+ });
323
+ }
324
+
325
+ /** Review rounds in flight, for the case writer: rounds running
326
+ * (a provisional one included — its seed is about to read memory) and PRs
327
+ * QUEUED that the pump would dispatch — not paused, not held FAILED_LOUD
328
+ * (those wait for the operator or their retry time, not for a pump tick),
329
+ * not blocked by a FAILED session command (it waits for the supervisor's
330
+ * recovery — which puts it back to pending, counted again, or opens the
331
+ * FAILED_LOUD hold — or for the operator's restart-session, never for a pump
332
+ * tick), and none while a drain holds the pump. A row skipped for a PENDING
333
+ * or RUNNING session command or for held wakes still counts: it dispatches
334
+ * once those drain on their own. */
335
+ export interface ReviewsInFlight {
336
+ running: number;
337
+ queued: number;
338
+ }
339
+
340
+ export function reviewsInFlight(store: StateStore): ReviewsInFlight {
341
+ const running = store.runningRounds().length;
342
+ if (store.drainState().draining) return { running, queued: 0 };
343
+ const queued = queuedInOrder(store).filter(
344
+ (row) =>
345
+ !store.openReviewFailure(row.number) &&
346
+ // The pump skips a row while ANY blocking command stands (dispatchUnlocked's
347
+ // `blockingSessionCommand`); a failed one among them does not drain itself.
348
+ !store.blockingSessionCommandsForPr(row.number).some((c) => c.status === "failed"),
349
+ ).length;
350
+ return { running, queued };
351
+ }
352
+
353
+ export function capacity(store: StateStore, cfg: Config): number {
354
+ return Math.max(0, cfg.daemon.max_concurrent_rounds - store.runningRounds().length);
355
+ }
356
+
357
+ /** A consume target that is already gone (label removed in an earlier round,
358
+ * no pending team request) comes back as a 404 — that is consumption already
359
+ * done, not a failure. */
360
+ function alreadyConsumed(err: unknown): boolean {
361
+ return /→ 404\b|"status":\s*"404"/.test(String(err));
362
+ }
363
+
364
+ /**
365
+ * The round's ecosystem pre-check (behind the ecosystem seam), run once
366
+ * per round after the worktree is prepared and BEFORE the session is handed
367
+ * the order.
368
+ *
369
+ * The behavior is the ecosystem's (`packages/pi-plugin/src/ecosystem.ts`): Django detects
370
+ * migration-number collisions at the pinned base sha and posts the remedy as a
371
+ * gated **COMMENT** — never a blocker, never a verdict — while `none` is a
372
+ * first-class configured-off (`{ status: "off" }`): nothing runs, nothing is
373
+ * read, nothing is logged. What stays HERE is the plumbing the seam must not
374
+ * own: the store, the gate-backed adapter, and the mapping back onto the round
375
+ * order's `migration_collisions` / `migration_precheck_degraded` fields. The
376
+ * seam is total — every failure resolves to an outcome — so a round is never
377
+ * lost to the pre-check.
378
+ *
379
+ * A DEGRADED check (Django only: the check exists but could not run while the
380
+ * PR touches migrations) must never be quieter than no check at all ("more
381
+ * reliable, never quieter"): the caller renders an explicit
382
+ * "the pre-check did not run — check collisions yourself" note into the round
383
+ * prompt, which is what conditionally un-muzzles pass-data-safety's withdrawn
384
+ * finding class for exactly that round (see packages/pi-plugin/src/include.ts,
385
+ * MIGRATION_NUMBERING_ADDENDUM) — and raises the overseer anomaly. "off"
386
+ * takes none of those paths: configured-off is not degradation.
387
+ *
388
+ * A WITHHELD comment (the ledger holds a collision-comment row it cannot
389
+ * read, so a duplicate cannot be ruled out) is the other way a
390
+ * checked round gets quieter, and it gets the same operator surface: an
391
+ * overseer anomaly, raised ONCE per PR (`StateStore.anomalyRaised`) because
392
+ * the torn row is torn on every round until someone repairs it, and one wake
393
+ * per round would bury the anomalies that need reading. The event itself is
394
+ * logged on every withheld round — only the alert is deduplicated.
395
+ */
396
+ async function ecosystemPreCheck(
397
+ deps: DispatchDeps,
398
+ eco: Ecosystem,
399
+ args: {
400
+ round_id: number;
401
+ pr: number;
402
+ files: PrFileEntry[];
403
+ head_sha: string;
404
+ base_ref: string;
405
+ base_sha: string;
406
+ },
407
+ ): Promise<{
408
+ collisions: MigrationCollision[];
409
+ degraded: boolean;
410
+ commentWithheld: boolean;
411
+ commentUndelivered: boolean;
412
+ }> {
413
+ const outcome = await eco.preCheck(
414
+ {
415
+ logEvent: (kind, pr, payload) => deps.store.logEvent(kind, pr, payload),
416
+ isDryRun: deps.cfg.dry_run,
417
+ ...(deps.adapter.listReviewsFull
418
+ ? {
419
+ findCollisionComment: async (pr: number, marker: string, head: string) =>
420
+ (await deps.adapter.listReviewsFull!(pr)).some(
421
+ (review) =>
422
+ review.user === deps.cfg.github.bot_login &&
423
+ review.commit_id === head &&
424
+ review.body.split(/\r?\n/).includes(marker),
425
+ ),
426
+ }
427
+ : {}),
428
+ migrationCollisionCommented: (pr, fingerprint) =>
429
+ deps.store.migrationCollisionCommented(pr, fingerprint, { live: !deps.cfg.dry_run }),
430
+ createReview: async (pr, payload, options) => {
431
+ // A throw after transport begins is ambiguous. Persist that BEFORE the
432
+ // gated call, then narrow it only from the gate's returned fact. The
433
+ // pre-fanout auto-retry class excludes ambiguous/executed rounds.
434
+ deps.store.recordRoundPreflightPosting(args.round_id, "ambiguous");
435
+ const result = await deps.adapter.createReview(pr, payload, options);
436
+ deps.store.recordRoundPreflightPosting(
437
+ args.round_id,
438
+ result.executed ? "executed" : result.dryRunFile ? "dryrun" : "refused",
439
+ );
440
+ return result;
441
+ },
442
+ ...(deps.listBaseMigrations ? { listBaseMigrations: deps.listBaseMigrations } : {}),
443
+ ...(deps.log ? { log: deps.log } : {}),
444
+ },
445
+ args,
446
+ );
447
+ return {
448
+ collisions: outcome.status === "checked" ? outcome.collisions : [],
449
+ degraded: outcome.status === "degraded",
450
+ commentWithheld: outcome.status === "checked" && outcome.comment_withheld === true,
451
+ commentUndelivered: outcome.status === "checked" && outcome.comment_undelivered === true,
452
+ };
453
+ }
454
+
455
+ /**
456
+ * First-party directive detection + the deterministic escalation-mention pick,
457
+ * run per round after the worktree is prepared. Pure path predicate
458
+ * over the files API (`isFirstPartyDirectivePath`), plus one read-only git log
459
+ * at the round's MERGE BASE — pre-PR history, so the PR's own commits never
460
+ * elect the PR author as their own approver. The pick is logged
461
+ * (`escalation_mention_selected`) with its source; every failure degrades to
462
+ * the config fallback, never to a lost round.
463
+ */
464
+ async function directiveEscalationFor(
465
+ deps: DispatchDeps,
466
+ args: { pr: number; files: PrFileEntry[]; merge_base: string; base_sha: string },
467
+ ): Promise<{ files: string[]; mention: string } | undefined> {
468
+ const log = deps.log ?? (() => {});
469
+ // The vocabulary is the compiled-in default extended by [review.directives]
470
+ // — extension only, never a narrowing.
471
+ const directiveFiles = firstPartyDirectiveFiles(args.files, deps.cfg.review.directives);
472
+ if (directiveFiles.length === 0) return undefined;
473
+ // An unset [review] escalation_mention_default falls back to the owner's
474
+ // GitHub login — the config default itself stays "".
475
+ const fallback = deps.cfg.review.escalation_mention_default || deps.cfg.owner.github_login;
476
+ const sha = args.merge_base || args.base_sha;
477
+ let author: { login: string | null; name: string; email: string } | null = null;
478
+ if (deps.directiveAuthor && sha) {
479
+ try {
480
+ author = await deps.directiveAuthor(sha, directiveFiles);
481
+ } catch (err) {
482
+ log(`dispatch: directive-author lookup failed on #${args.pr}`, err);
483
+ }
484
+ }
485
+ const mention = author?.login ?? fallback;
486
+ deps.store.logEvent("escalation_mention_selected", args.pr, {
487
+ files: directiveFiles,
488
+ mention,
489
+ source: author?.login ? "git-log" : "config-default",
490
+ author_name: author?.name ?? null,
491
+ sha: sha || null,
492
+ });
493
+ return { files: directiveFiles, mention };
494
+ }
495
+
496
+ /**
497
+ * Seed one round's prompt with memory: the guaranteed pinned head
498
+ * plus EXACTLY ONE budgeted context block, queried from the PR's title and
499
+ * diff shape.
500
+ *
501
+ * This is the single context call per round — one call site, one
502
+ * `assembleRoundMemory`, which itself calls `contextBlock` once. It runs after
503
+ * the worktree is prepared and the order is built, so the context is seeded
504
+ * from the same point the passes read.
505
+ *
506
+ * It cannot fail a round. `assembleRoundMemory` is total (deadline + swallowed
507
+ * throws); the extra try/catch here is belt-and-braces for a client that
508
+ * breaks the contract, and the outcome is identical either way: log the gap,
509
+ * tell the memory health monitor (`seed.onSeeded`), dispatch memory-less.
510
+ *
511
+ * There is no per-round memory anomaly: the health monitor
512
+ * (src/memory/health.ts) owns telling a person, on state transitions rather
513
+ * than once per round. The `round_memory_seeded` event is the per-round
514
+ * record, and it is what the monitor is fed from.
515
+ */
516
+ async function seedRoundMemory(
517
+ deps: DispatchDeps,
518
+ order: RoundOrder,
519
+ files: PrFileEntry[],
520
+ /** Told once the round's context search was issued (the lesson finds
521
+ * under `delivery = "on"` wait for it). Optional. */
522
+ onContextSent?: () => void,
523
+ ): Promise<RoundMemory> {
524
+ const seed = deps.memory;
525
+ if (!seed) return MEMORY_LESS;
526
+ const log = deps.log ?? (() => {});
527
+ const query = roundContextQuery({ title: order.title, files });
528
+ // The session's own recall searches for the same thing.
529
+ try {
530
+ seed.publishQuery?.(order.pr, query);
531
+ } catch (err) {
532
+ log(`dispatch: #${order.pr} recall hint not published (ignored)`, err);
533
+ }
534
+ const timeoutMs = seed.timeoutMs ?? ROUND_CONTEXT_DEFAULTS.timeoutMs;
535
+ // The seed's search waits — inside its own budget, never past
536
+ // it — until the case writer's last batch is estimated done embedding, so
537
+ // it is not queued behind those embeddings on the one-thread embedder.
538
+ const activity = seed.activity;
539
+ const quiet =
540
+ activity && typeof activity.whenEmbedderQuiet === "function"
541
+ ? (maxWaitMs: number) => activity.whenEmbedderQuiet!(maxWaitMs)
542
+ : undefined;
543
+ let mem: RoundMemory;
544
+ try {
545
+ mem = await assembleRoundMemory(seed.assembler, query, {
546
+ ...(seed.maxTokens !== undefined ? { maxTokens: seed.maxTokens } : {}),
547
+ timeoutMs,
548
+ log,
549
+ ...(onContextSent ? { onContextSent } : {}),
550
+ ...(quiet ? { quiet } : {}),
551
+ ...(seed.searchReserveMs !== undefined ? { searchReserveMs: seed.searchReserveMs } : {}),
552
+ });
553
+ } catch (err) {
554
+ log(`dispatch: #${order.pr} memory seeding threw — round proceeds memory-less`, err);
555
+ mem = {
556
+ ...MEMORY_LESS,
557
+ asked: true,
558
+ query,
559
+ max_tokens: seed.maxTokens ?? ROUND_CONTEXT_DEFAULTS.maxTokens,
560
+ degraded: true,
561
+ gaps: [{ part: "head", reason: `error: ${String(err)}` }],
562
+ };
563
+ }
564
+ const headState = pinnedHeadState(mem);
565
+ // The baseline for the one transition that is silent by construction: a
566
+ // readable store whose pinned area went from populated to empty. Read
567
+ // BEFORE this round's row is written, and only when it could matter.
568
+ const previousHead = headState === "empty" ? deps.store.lastReadPinnedHead() : null;
569
+ deps.store.logEvent("round_memory_seeded", order.pr, {
570
+ round_id: order.round_id,
571
+ round_no: order.round_no,
572
+ query: mem.query,
573
+ max_tokens: mem.max_tokens,
574
+ head_state: headState,
575
+ head_chars: mem.head.length,
576
+ block_chars: mem.block.length,
577
+ clamped: mem.clamped,
578
+ degraded: mem.degraded,
579
+ gaps: mem.gaps,
580
+ ms: mem.ms,
581
+ // How long the search waited for the case writer's embedding tail,
582
+ // inside `ms` (absent when no tracker is wired).
583
+ ...(mem.quiet_wait_ms !== undefined ? { quiet_wait_ms: mem.quiet_wait_ms } : {}),
584
+ });
585
+ if (previousHead && previousHead.head_chars > 0) {
586
+ // Empty is not degraded: a genuinely empty pinned area is a healthy
587
+ // answer — right for a fresh install, wrong for a container recreated
588
+ // without its data volume. One anomaly on the transition itself carries
589
+ // that signal; the next round's baseline is this empty row, so it cannot
590
+ // repeat. An owner who unpinned their last standing instruction sees it
591
+ // once too, and knows why.
592
+ deps.store.logAnomaly(
593
+ "pinned_head_emptied",
594
+ order.pr,
595
+ order.round_no,
596
+ `the pinned area read as EMPTY with the store readable; the last seeded round (#${previousHead.pr ?? "?"} at ${previousHead.ts}) carried ${previousHead.head_chars} chars of standing instructions`,
597
+ [
598
+ `ANOMALY (memory) — PR #${order.pr} round ${order.round_no}`,
599
+ `The memory store answered, and the pinned area is EMPTY. At the last seeded round (#${previousHead.pr ?? "?"}, ${previousHead.ts}) it held ${previousHead.head_chars} chars of standing instructions.`,
600
+ "If nobody unpinned them on purpose, the store lost its data — a container recreated without its state volume looks exactly like this. " +
601
+ "Check the openviking data directory on the state volume and the TUI's memory tab. The round was dispatched normally; this fires once, on the transition.",
602
+ ].join("\n"),
603
+ );
604
+ }
605
+ try {
606
+ seed.onSeeded?.(mem);
607
+ } catch (err) {
608
+ log(`dispatch: #${order.pr} memory health observer threw (ignored)`, err);
609
+ }
610
+ if (mem.degraded) {
611
+ log(`dispatch: #${order.pr} round ${order.round_no} seeded memory-less (${describeGaps(mem)})`);
612
+ }
613
+ return mem;
614
+ }
615
+
616
+ /**
617
+ * Open the round's memory window (`RoundMemorySeed.activity`):
618
+ * from just before the seed until the round's lesson selection has taken
619
+ * over, the case writer starts no batch, so no case embedding is queued in
620
+ * front of the round's own reads on the server's one-thread embedder. Returns
621
+ * its idempotent `end`; total — a broken tracker is a no-op, never a throw.
622
+ */
623
+ function openMemoryWindow(
624
+ deps: DispatchDeps,
625
+ pr: number,
626
+ log: (msg: string, extra?: unknown) => void,
627
+ ): () => void {
628
+ const activity = deps.memory?.activity;
629
+ if (!activity) return () => {};
630
+ try {
631
+ const end = activity.begin(`round #${pr}`);
632
+ return () => {
633
+ try {
634
+ end();
635
+ } catch (err) {
636
+ log(`dispatch: #${pr} memory window not closed (ignored)`, err);
637
+ }
638
+ };
639
+ } catch (err) {
640
+ log(`dispatch: #${pr} memory window not opened (ignored)`, err);
641
+ return () => {};
642
+ }
643
+ }
644
+
645
+ /** `[memory] delivery = "on"` with a hook that can deliver. Total: a
646
+ * throwing hook is "not delivering" — the shadow path. */
647
+ function lessonsDelivering(
648
+ deps: DispatchDeps,
649
+ log: (msg: string, extra?: unknown) => void,
650
+ ): boolean {
651
+ const hook = deps.lessons;
652
+ if (!hook || typeof hook.select !== "function" || typeof hook.delivering !== "function")
653
+ return false;
654
+ try {
655
+ return hook.delivering() === true;
656
+ } catch (err) {
657
+ log("dispatch: lesson delivery switch unreadable — delivering nothing (ignored)", err);
658
+ return false;
659
+ }
660
+ }
661
+
662
+ /** The round's selection under `on`, awaited beside the seed. Total: the
663
+ * hook bounds it by its prefetch budget; a throw or a rejection is `null`
664
+ * and the round renders as if nothing were selected. */
665
+ async function selectRoundLessons(
666
+ deps: DispatchDeps,
667
+ order: RoundOrder,
668
+ files: PrFileEntry[],
669
+ after: Promise<void>,
670
+ ): Promise<RoundSelectionResult | null> {
671
+ try {
672
+ return (await deps.lessons?.select?.(order, files, { after })) ?? null;
673
+ } catch (err) {
674
+ (deps.log ?? (() => {}))(
675
+ `dispatch: #${order.pr} lesson selection failed — no lessons this round (ignored)`,
676
+ err,
677
+ );
678
+ return null;
679
+ }
680
+ }
681
+
682
+ /**
683
+ * Hand the round's delivered lessons to its prompt: write the
684
+ * lessons files (the validator's, and each lens the session may add) and
685
+ * return the seed carrying the delivery — or the seed untouched when nothing
686
+ * was selected, so the prompt keeps the standard bleed rule verbatim. Total: a
687
+ * lessons file that cannot be written is simply not named in the prompt.
688
+ */
689
+ function deliverRoundLessons(
690
+ deps: DispatchDeps,
691
+ input: {
692
+ seeded: RoundMemory;
693
+ delivery: RoundLessonDelivery | null;
694
+ dir: string;
695
+ },
696
+ ): RoundMemory {
697
+ const log = deps.log ?? (() => {});
698
+ let d = input.delivery;
699
+ if (!d || !deliversAnything(d)) return input.seeded;
700
+ try {
701
+ d = writeLessonFiles(input.dir, d);
702
+ } catch (err) {
703
+ log(`dispatch: #${d.pr} lessons files unavailable — the round names none`, err);
704
+ }
705
+ // A lessons file that was not written delivers nothing: neither the index
706
+ // nor the bleed rule may claim it did.
707
+ d = onlyWritten(d);
708
+ return deliversAnything(d) ? { ...input.seeded, lessons: d } : input.seeded;
709
+ }
710
+
711
+ /** The `lesson_delivered` record — written once the prompt carrying the
712
+ * delivery was handed to the session. */
713
+ function logLessonDelivery(store: StateStore, roundId: number, d: RoundLessonDelivery): void {
714
+ const counts = deliveryCounts(d);
715
+ store.logEvent("lesson_delivered", d.pr, {
716
+ round_id: roundId,
717
+ round_no: d.round_no,
718
+ lessons: counts.lessons,
719
+ cases: counts.cases,
720
+ rows: d.rows.filter((r) => r.lessons.length > 0).length,
721
+ validator: d.validator?.lessons.length ?? 0,
722
+ validator_file: d.validator?.file !== undefined,
723
+ addable_files: d.addable.filter((a) => a.file !== undefined).map((a) => a.lens),
724
+ });
725
+ }
726
+
727
+ /** The trivial-delta short-circuit's answer for one system-provenance re-trigger. */
728
+ type ShortCircuit =
729
+ | { skip: false }
730
+ | {
731
+ skip: true;
732
+ reason: "unchanged-head" | "empty-delta" | "trivial-delta";
733
+ class: DeltaClass;
734
+ files: string[];
735
+ };
736
+
737
+ /**
738
+ * Classify the delta (last reviewed head → current head) for a
739
+ * SYSTEM-provenance re-trigger, using the one recheck classifier
740
+ * (`recheck.classifyDelta`) — never a copy. Every uncertainty resolves to
741
+ * "run the full round": an unknown head, a missing compare seam, or a failed
742
+ * read must never quietly swallow a review.
743
+ */
744
+ async function classifySystemRetrigger(
745
+ deps: DispatchDeps,
746
+ row: PrStateRow,
747
+ mark: ReviewedHeadRow,
748
+ ): Promise<ShortCircuit> {
749
+ const log = deps.log ?? (() => {});
750
+ // Unknown head is always "new" — the same reading guards.hasNewEvidence takes.
751
+ if (!row.head_sha) return { skip: false };
752
+ if (row.head_sha === mark.head_sha) {
753
+ // Same head. An author reply pending on the mark is a person's ask (the
754
+ // rule guard 2 reads) → full round; anything else would be a full round
755
+ // whose output posting-time guard 2 throws away.
756
+ if (hasNewEvidence(mark, row.head_sha)) return { skip: false };
757
+ return { skip: true, reason: "unchanged-head", class: "empty", files: [] };
758
+ }
759
+ if (!deps.adapter.compareFiles) return { skip: false };
760
+ try {
761
+ const delta = await deps.adapter.compareFiles(mark.head_sha, row.head_sha);
762
+ // The union with the reviewed head's ownership: a push reverting a
763
+ // reviewed file is the PR's own change, not a base merge.
764
+ // A truncated listing leaves ownership unknown (undefined).
765
+ const prFiles = await reviewedOwnership(
766
+ deps.adapter,
767
+ row.number,
768
+ mark.head_sha,
769
+ delta,
770
+ ownershipSet(await deps.adapter.listPrFiles(row.number)),
771
+ );
772
+ // The recheck's own merge check: a degraded one keeps the delta-only
773
+ // classification but is never silently quieter.
774
+ const resolvedFiles = new Set(
775
+ await verifyMerges(
776
+ deps.store,
777
+ deps.resolvedFilesInMerges,
778
+ mark.head_sha,
779
+ row.head_sha,
780
+ row.number,
781
+ "dispatch",
782
+ ),
783
+ );
784
+ const cls = classifyDelta(delta, {
785
+ ...(prFiles ? { prFiles } : {}),
786
+ resolvedFiles,
787
+ });
788
+ if (cls.class === "significant") return { skip: false };
789
+ return {
790
+ skip: true,
791
+ reason: cls.class === "empty" ? "empty-delta" : "trivial-delta",
792
+ class: cls.class,
793
+ files: cls.files.map((f) => `${f.filename} (${f.reason})`),
794
+ };
795
+ } catch (err) {
796
+ log(`dispatch: #${row.number} delta classification failed — full round`, err);
797
+ deps.store.logEvent("dispatch_classifier_failed", row.number, { error: String(err) });
798
+ return { skip: false };
799
+ }
800
+ }
801
+
802
+ /** Consume both triggers at dispatch: the label and the team review request.
803
+ * Both go through the adapter (and therefore the dry-run gate); failures are
804
+ * logged, never fatal — the reconcile poll re-derives the truth. Returns the
805
+ * triggers really consumed: one the gate recorded instead of executing (dry
806
+ * run) or a guard refused is still on GitHub, and is logged as such. */
807
+ async function consumeTriggers(
808
+ deps: DispatchDeps,
809
+ pr: number,
810
+ /** The no-new-evidence skip consumes the triggers without starting
811
+ * a round, so it must not paint the PR "AI review in progress". */
812
+ opts: { markInProgress?: boolean } = {},
813
+ ): Promise<string[]> {
814
+ const consumed: string[] = [];
815
+ const log = deps.log ?? (() => {});
816
+ /** One trigger: consumed, already gone (consumed too), not executed, or
817
+ * failed — logged. */
818
+ const consume = async (
819
+ which: "label" | "team_request",
820
+ what: string,
821
+ op: () => Promise<GateResult | undefined>,
822
+ ): Promise<void> => {
823
+ try {
824
+ const result = await op();
825
+ if (result?.executed === false) {
826
+ deps.store.logEvent("trigger_consume_not_executed", pr, {
827
+ which,
828
+ ...(result.dryRunFile ? { dry_run: true } : {}),
829
+ ...(result.blocked ? { blocked: result.blocked } : {}),
830
+ });
831
+ return;
832
+ }
833
+ consumed.push(which);
834
+ } catch (err) {
835
+ if (alreadyConsumed(err)) {
836
+ consumed.push(which);
837
+ deps.store.logEvent("trigger_already_consumed", pr, { which });
838
+ } else {
839
+ log(`dispatch: ${what} failed on #${pr}`, err);
840
+ deps.store.logEvent("trigger_consume_failed", pr, { which, error: String(err) });
841
+ }
842
+ }
843
+ };
844
+ await consume("label", "label removal", () =>
845
+ deps.adapter.removeLabel(pr, deps.cfg.github.trigger_label),
846
+ );
847
+ await consume("team_request", "team-request DELETE", () =>
848
+ deps.adapter.deleteTeamReviewRequest(pr),
849
+ );
850
+ if (opts.markInProgress !== false) {
851
+ try {
852
+ await deps.adapter.addLabel(pr, deps.cfg.github.in_progress_label);
853
+ } catch (err) {
854
+ log(`dispatch: in-progress label failed on #${pr}`, err);
855
+ }
856
+ }
857
+ return consumed;
858
+ }
859
+
860
+ /**
861
+ * A withheld approval's retry round that ended without deciding anything —
862
+ * cancelled by a restart's orphan sweep, a self-requeue or an operator, or
863
+ * held FAILED_LOUD until the operator's `u` — still owes the approval:
864
+ * dispatch spent the mark at that round's handoff, and the round that would
865
+ * have re-decided it never did. The mark is re-recorded here from the spend's
866
+ * own record (`approval_retry_dispatched`), so the round that replaces it
867
+ * runs instead of being skipped as no new evidence. A round that decided (a
868
+ * verdict, a guard-2 no-op) or is still running owes nothing; neither does a
869
+ * spend at another head or one a later round has overtaken.
870
+ *
871
+ * Only a round that PROVABLY posted nothing is restored: a restored mark
872
+ * takes the replacement round past guard 2's daemon half, and the approval
873
+ * route has no guard 2 of its own, so a round whose APPROVE went out would
874
+ * post a second one. So the round has no posting receipt but a guard-2
875
+ * refusal (a `prepared` or `deferred` one is unknown, never "not posted"), its
876
+ * row records no posting, and the reviewed-head mark has not been stamped
877
+ * since it started.
878
+ */
879
+ function restoreInterruptedApprovalRetry(
880
+ store: DispatchDeps["store"],
881
+ row: Pick<PrStateRow, "number" | "head_sha">,
882
+ postingReceiptDir: string,
883
+ ): ApprovalWithheldRow | undefined {
884
+ const spent = store.db
885
+ .prepare(
886
+ "SELECT payload FROM events WHERE pr = ? AND kind = 'approval_retry_dispatched' ORDER BY id DESC LIMIT 1",
887
+ )
888
+ .get(row.number) as { payload: string } | undefined;
889
+ if (!spent) return undefined;
890
+ let record: { head_sha?: unknown; round_id?: unknown; ci_state?: unknown; reason?: unknown };
891
+ try {
892
+ record = JSON.parse(spent.payload) as typeof record;
893
+ } catch {
894
+ return undefined;
895
+ }
896
+ if (record.head_sha !== row.head_sha || typeof record.round_id !== "number") return undefined;
897
+ const round = store.roundsFor(row.number).find((r) => r.id === record.round_id);
898
+ if (!round || round.status === "running" || round.status === "done") return undefined;
899
+ if (round.status === "cancelled" && round.cancel_cause === "no_new_evidence") return undefined;
900
+ if (store.hasLaterRound(row.number, round.round_no)) return undefined;
901
+ // A receipt file that cannot be read is unknown too.
902
+ if (existsSync(postingReceiptPath(postingReceiptDir, row.number, round.round_no))) {
903
+ const receipt = readPostingReceipt(postingReceiptDir, row.number, round.round_no);
904
+ if (receipt?.status !== "skipped") return undefined;
905
+ }
906
+ if (round.posted_event || round.gh_review_id !== null) return undefined;
907
+ const mark = store.reviewedHead(row.number);
908
+ if (mark && mark.reviewed_at >= round.started_at) return undefined;
909
+ store.recordApprovalWithheld(row.number, {
910
+ head_sha: row.head_sha,
911
+ ci_state: typeof record.ci_state === "string" ? record.ci_state : null,
912
+ reason: typeof record.reason === "string" ? record.reason : "red-ci",
913
+ });
914
+ store.logEvent("approval_retry_restored", row.number, {
915
+ head_sha: row.head_sha,
916
+ round_id: round.id,
917
+ round_no: round.round_no,
918
+ round_status: round.status,
919
+ cancel_cause: round.cancel_cause ?? null,
920
+ });
921
+ return store.approvalWithheld(row.number);
922
+ }
923
+
924
+ /**
925
+ * Did a webhook move this PR while dispatch awaited GitHub? The skip paths
926
+ * decide from the row they read, then await trigger consumption (two GitHub
927
+ * round-trips) before settling the PR back onto its standing verdict. A push
928
+ * or a fresh review request applied in that window updates the QUEUED row —
929
+ * nothing records a successor for a row that is not `reviewing` — and
930
+ * settling it would erase that newer review debt: the stale standing verdict
931
+ * replayed, the new trigger cleared. So a moved row is left queued for the
932
+ * next pump, which decides again from what it now says. A row that left the
933
+ * queue (a close, the operator) is not settled either: it is no longer this
934
+ * dispatch's to decide. Returns the reason, or null when the row still says
935
+ * what the decision read.
936
+ */
937
+ function movedDuringConsume(
938
+ store: DispatchDeps["store"],
939
+ row: Pick<PrStateRow, "number" | "head_sha" | "trigger_ts" | "trigger_provenance">,
940
+ ): string | null {
941
+ const now = store.getPr(row.number);
942
+ if (!now) return null;
943
+ if (now.state !== "queued") return "left_queue";
944
+ if (now.head_sha !== row.head_sha) return "head_moved";
945
+ // A person's ask on a queued row keeps its trigger time and upgrades only
946
+ // the provenance — a re-request the system short-circuit must not settle.
947
+ if (now.trigger_ts !== row.trigger_ts || now.trigger_provenance !== row.trigger_provenance)
948
+ return "retriggered";
949
+ return null;
950
+ }
951
+
952
+ function logSupersededSettlement(
953
+ deps: DispatchDeps,
954
+ row: { number: number; head_sha: string; trigger_ts: string | null },
955
+ path: string,
956
+ reason: string,
957
+ ): void {
958
+ const now = deps.store.getPr(row.number);
959
+ deps.store.logEvent("dispatch_settlement_superseded", row.number, {
960
+ path,
961
+ reason,
962
+ decided_head_sha: row.head_sha,
963
+ decided_trigger_ts: row.trigger_ts,
964
+ head_sha: now?.head_sha ?? null,
965
+ trigger_ts: now?.trigger_ts ?? null,
966
+ });
967
+ deps.log?.(
968
+ `dispatch: #${row.number} ${path} not settled — the PR moved while its triggers were consumed (${reason}); left queued`,
969
+ );
970
+ }
971
+
972
+ export { ALL_PASSES } from "@quickchatai/pi-quickreview/src/passes.js";
973
+
974
+ // The review planner moved to packages/pi-plugin/src/planner.ts (the review core); re-exported so
975
+ // the app's importers keep their one module.
976
+ export {
977
+ addTierOf,
978
+ buildPassPlan,
979
+ buildRoundOrder,
980
+ decideFollowUp,
981
+ followUpSweepOwed,
982
+ LEAN_ADDABLE_PASSES,
983
+ leanFollowUpPlan,
984
+ lensesOf,
985
+ optionalOrderOf,
986
+ planFollowUp,
987
+ renderDeltaPatch,
988
+ reviewedHeadOf,
989
+ roundDeltaOf,
990
+ slowestFirst,
991
+ sweepOrderOf,
992
+ validatorTier,
993
+ type FollowUpDecision,
994
+ type FollowUpFullReason,
995
+ type FollowUpHistory,
996
+ type PassPlan,
997
+ type PreparedTree,
998
+ type PrOwnedDeltaReader,
999
+ } from "@quickchatai/pi-quickreview/src/planner.js";
1000
+
1001
+ /** What the round row records of a plan — the same projection on the
1002
+ * normal path and on the patch-write fallback, so the stored round and the
1003
+ * order always describe one plan. A follow-up round's optional rows ride
1004
+ * the addable record, so the executed-pass telemetry reads a run one as
1005
+ * offered work rather than an unknown addition. */
1006
+ function recordedPlan(
1007
+ cfg: Config,
1008
+ plan: PassPlan,
1009
+ optionalPlan: readonly PassPlanEntry[] = [],
1010
+ ): {
1011
+ plannedPasses: PassPlanEntry[];
1012
+ lenses: string[];
1013
+ addablePasses: RecordedAddablePass[];
1014
+ validateTier: PassTier | null;
1015
+ } {
1016
+ const plannedPasses = plan.pass_plan;
1017
+ const optional: RecordedAddablePass[] = optionalPlan.map((p) => ({
1018
+ lens: p.lens,
1019
+ tier: p.tier,
1020
+ optional: true,
1021
+ raisable_to: p.raisable_to,
1022
+ }));
1023
+ return {
1024
+ plannedPasses,
1025
+ lenses: lensesOf(plannedPasses),
1026
+ addablePasses: [
1027
+ ...plan.addable_lenses.map((lens) => ({ lens, tier: addTierOf(cfg, lens) })),
1028
+ ...optional,
1029
+ ],
1030
+ // Null when the profile switched the validator off: the round had no
1031
+ // validator, and recording the configured tier would say it did.
1032
+ validateTier: plannedPasses.find((p) => p.lens === VALIDATE_PASS)?.tier ?? null,
1033
+ };
1034
+ }
1035
+
1036
+ // A reconcile, webhook and periodic tick may all ask for a pump at once. The
1037
+ // store is the daemon-instance identity; joiners wait for the active pump and
1038
+ // return no duplicate orders. This closes the race where two pumps both saw a
1039
+ // queued PR/capacity slot and created two rounds for it.
1040
+ const activePumps = new WeakMap<StateStore, Promise<RoundOrder[]>>();
1041
+
1042
+ /** One serialized dispatch pump: drain the queue up to the concurrency cap. */
1043
+ export async function dispatchOnce(deps: DispatchDeps): Promise<RoundOrder[]> {
1044
+ const active = activePumps.get(deps.store);
1045
+ if (active) {
1046
+ await active;
1047
+ return [];
1048
+ }
1049
+ const pump = dispatchUnlocked(deps);
1050
+ activePumps.set(deps.store, pump);
1051
+ try {
1052
+ return await pump;
1053
+ } finally {
1054
+ if (activePumps.get(deps.store) === pump) activePumps.delete(deps.store);
1055
+ }
1056
+ }
1057
+
1058
+ async function dispatchUnlocked(deps: DispatchDeps): Promise<RoundOrder[]> {
1059
+ const { store, cfg } = deps;
1060
+ const eco = deps.ecosystem ?? DJANGO_ECOSYSTEM;
1061
+ const now = deps.now ?? (() => new Date());
1062
+ const render =
1063
+ deps.renderPrompt ?? ((o: RoundOrder, _m: RoundMemory) => JSON.stringify(o, null, 2));
1064
+ const worktreeFor = deps.worktreeFor ?? defaultWorktree;
1065
+ const ledgerCarry =
1066
+ deps.ledgerCarry ?? ((): { carry?: LedgerCarry; settled: string } => ({ settled: "" }));
1067
+ const log = deps.log ?? (() => {});
1068
+
1069
+ // Deploy-window drain: the pump starts NOTHING while draining.
1070
+ // In-flight rounds keep running to their natural end; queued rows simply
1071
+ // wait and dispatch when the drain lifts. The refusal stays quiet at the
1072
+ // 15s tick — the drain itself is loud (status.json, qcr status, the board
1073
+ // banner, the drain_on event).
1074
+ if (store.drainState().draining) return [];
1075
+
1076
+ const slots = capacity(store, cfg);
1077
+ if (slots <= 0) return [];
1078
+
1079
+ const dispatched: RoundOrder[] = [];
1080
+ /** Why this queued PR may not dispatch now, or null. */
1081
+ const heldBack = (row: PrStateRow): string | null => {
1082
+ // One PR = one worktree = at most one round in flight. A queued row with a
1083
+ // round still running (requeue raced the verdict) waits for the next pump —
1084
+ // two rounds sharing wt/pr-<n> fight over the git index.
1085
+ if (store.hasRunningRound(row.number)) return "skipped — a round is already running";
1086
+ const failure = store.openReviewFailure(row.number);
1087
+ if (failure) {
1088
+ const action =
1089
+ failure.next_action === "retry_wait" && failure.next_retry_at
1090
+ ? `automatic retry at ${failure.next_retry_at}`
1091
+ : "operator u required";
1092
+ return `held FAILED_LOUD (${failure.incident_id}); ${action}`;
1093
+ }
1094
+ const active = store.blockingSessionCommand(`pr-${row.number}`);
1095
+ if (active) return `skipped — session command ${active.id} is ${active.status}`;
1096
+ // The operator holds the session (src/takeover.ts): the round waits for the release.
1097
+ // A debug lease (src/debuground.ts) is armed for exactly this round: it goes out,
1098
+ // and the worker host hands it to the debug pi in place of a worker.
1099
+ const lease = store.hostLease(row.number);
1100
+ if (lease && lease.kind !== "debug")
1101
+ return `skipped — under a ${lease.kind} lease (${lease.holder})`;
1102
+ if (store.hasHeldWakes(row.number))
1103
+ return "skipped — held conversational wakes must drain first";
1104
+ return null;
1105
+ };
1106
+ const eligible = queuedInOrder(store).filter((row) => {
1107
+ const why = heldBack(row);
1108
+ if (why) log(`dispatch: #${row.number} ${why}`);
1109
+ return why === null;
1110
+ });
1111
+ // A slot is spent by a round that went running — handed off, or retained
1112
+ // behind its durable command — never by a PR this pump skipped, settled or
1113
+ // failed to prepare: one PR failing every tick must not starve the queue
1114
+ // behind it.
1115
+ let started = 0;
1116
+ for (const queued of eligible) {
1117
+ if (started >= slots) break;
1118
+ // Eligibility holds when the work starts, not only when the pump listed
1119
+ // the queue: every PR before this one awaited GitHub, and a drain, a
1120
+ // close, a pause or a fresh hold may have landed meanwhile. The row is
1121
+ // read again, so the round is decided from what it says now.
1122
+ if (store.drainState().draining) break;
1123
+ const row = store.getPr(queued.number);
1124
+ if (!row || row.state !== "queued" || row.paused) {
1125
+ log(`dispatch: #${queued.number} skipped — no longer queued (${row?.state ?? "gone"})`);
1126
+ continue;
1127
+ }
1128
+ const why = heldBack(row);
1129
+ if (why) {
1130
+ log(`dispatch: #${row.number} ${why}`);
1131
+ continue;
1132
+ }
1133
+ let provisionalRoundId: number | undefined;
1134
+ // The round's memory window (opened before the seed, below).
1135
+ let endMemoryWindow: () => void = () => {};
1136
+ // Spends the withheld-approval mark this round inherits (set below).
1137
+ let spendWithholding: (roundId: number) => void = () => {};
1138
+ // Whether this dispatch painted the PR "in progress" (trigger consumption
1139
+ // for a round, below): a failure before any round owns the review takes
1140
+ // the label off again.
1141
+ let markedInProgress = false;
1142
+ try {
1143
+ // Backfilled (imported) rounds are excluded: a historical review must
1144
+ // never flip a still-open PR's next live round to "re-review", whose
1145
+ // template references a settled digest that never existed.
1146
+ const priorRounds = store.priorRoundCount(row.number);
1147
+ const roundType = roundTypeFor(row, priorRounds);
1148
+
1149
+ // Guard 2, daemon half: a tick with nothing new to look at is a
1150
+ // no-op. The triggers are still consumed (leaving them would re-queue the
1151
+ // PR on the next reconcile, forever) and the PR returns to the state its
1152
+ // last verdict put it in — that verdict still stands.
1153
+ //
1154
+ // The mark is read through the one standing-review interpretation
1155
+ // (standing-review.ts): the shell stamps `posted_event` and the watcher
1156
+ // may never enrich `verdict`, so `verdict` alone can read null for a
1157
+ // standing REQUEST_CHANGES. Guard 2 keys on head + author reply and
1158
+ // nothing else, so THAT is the test here — provenance (a label, a team
1159
+ // re-request, a reconcile re-derivation) is a scheduling signal and
1160
+ // never widens the posting boundary. If the standing verdict cannot be
1161
+ // read off the mark (a COMMENT-only row), no round runs either — guard
1162
+ // 2 would still refuse it — but nothing is guessed: the PR lands in
1163
+ // `tracked` with a precise anomaly instead of an invented verdict.
1164
+ const mark = store.reviewedHead(row.number);
1165
+ const standingInfo = standingReview(mark);
1166
+ const standing = standingInfo.kind === "decided" ? standingInfo.verdict : null;
1167
+ // A withholding standing at THIS head is new evidence of its
1168
+ // own. The last round decided "no blockers, approval withheld for CI",
1169
+ // and something re-queued the PR — CI going green (cigate.ts) or a
1170
+ // person. Skipping here would leave that PR waiting for a verdict that is
1171
+ // never re-attempted, which is the stuck-PR failure the gate must not
1172
+ // create.
1173
+ const withheld =
1174
+ store.approvalWithheld(row.number) ??
1175
+ restoreInterruptedApprovalRetry(
1176
+ store,
1177
+ row,
1178
+ deps.postingReceiptDir ?? join(sandboxRoot(), "state", "posting-receipts"),
1179
+ );
1180
+ const withheldHere = withheld?.head_sha === row.head_sha;
1181
+ // The round about to run re-decides the verdict from scratch, so the
1182
+ // withholding it inherits is spent — once the round has a durable owner
1183
+ // (the session handoff), never before: a failure before the handoff
1184
+ // leaves the mark standing, so the next pump still owes the approval
1185
+ // retry and the reconcile backstop still sees it. Only the mark read
1186
+ // here is spent; a new one is recorded if this round's approval is
1187
+ // withheld again (guards.ts guard 4, or the verdict's
1188
+ // `approval_withheld`).
1189
+ let withholdingSpent = false;
1190
+ spendWithholding = (roundId) => {
1191
+ if (!withheld || withholdingSpent) return;
1192
+ store.db.transaction(() => {
1193
+ const current = store.approvalWithheld(row.number);
1194
+ if (
1195
+ current &&
1196
+ current.head_sha === withheld.head_sha &&
1197
+ current.created_at === withheld.created_at
1198
+ )
1199
+ store.clearApprovalWithheld(row.number);
1200
+ // The spend's own record: what `restoreInterruptedApprovalRetry`
1201
+ // re-records should this round end without deciding.
1202
+ if (withheldHere)
1203
+ store.logEvent("approval_retry_dispatched", row.number, {
1204
+ head_sha: row.head_sha,
1205
+ round_id: roundId,
1206
+ ci_state: withheld.ci_state,
1207
+ reason: withheld.reason,
1208
+ });
1209
+ })();
1210
+ withholdingSpent = true;
1211
+ };
1212
+ if (cfg.guards.skip_unchanged_head && !withheldHere && !hasNewEvidence(mark, row.head_sha)) {
1213
+ // Captured BEFORE the transition below clears them: current PR state
1214
+ // is not historical evidence (a later failure overwrites the
1215
+ // provenance).
1216
+ const decision = {
1217
+ stage: "dispatch",
1218
+ head_sha: row.head_sha,
1219
+ trigger_ts: row.trigger_ts,
1220
+ trigger_provenance: row.trigger_provenance,
1221
+ standing_verdict: standing,
1222
+ standing_source: standingInfo.kind === "decided" ? standingInfo.source : null,
1223
+ // Duplicate of standing_verdict, for readers of the older payload shape.
1224
+ last_verdict: standing,
1225
+ reviewed_at: mark!.reviewed_at,
1226
+ mark: snapshotMark(mark!),
1227
+ };
1228
+ await consumeTriggers(deps, row.number, { markInProgress: false });
1229
+ // An author reply landing during the consumption is new evidence the
1230
+ // skip did not see: the next pump runs the round it now owes.
1231
+ const movedSkip =
1232
+ movedDuringConsume(store, row) ??
1233
+ (hasNewEvidence(store.reviewedHead(row.number), row.head_sha) ? "new_evidence" : null);
1234
+ if (movedSkip) {
1235
+ logSupersededSettlement(deps, row, "no-new-evidence skip", movedSkip);
1236
+ continue;
1237
+ }
1238
+ store.logEvent("round_skipped_no_new_evidence", row.number, decision);
1239
+ // The head this skip settles is reviewed already: a successor owed at
1240
+ // it is answered, and is not promoted again on every reconcile pass.
1241
+ store.settleSuccessor(row.number, row.head_sha);
1242
+ if (standingInfo.kind === "unknown") {
1243
+ store.logAnomaly(
1244
+ "standing_review_unknown",
1245
+ row.number,
1246
+ null,
1247
+ `#${row.number} was re-triggered at ${row.head_sha.slice(0, 7)}, which is already reviewed with no author reply since, but the reviewed-head mark carries no verdict (posted_event=${standingInfo.posted_event ?? "null"}). No round was run — posting guard 2 would refuse its review — and no verdict was guessed; the PR is tracked until the author pushes or replies.`,
1248
+ );
1249
+ }
1250
+ const restored = restoreStandingLifecycle(store, row.number, standing, {
1251
+ botLogin: botAccountLogin(cfg.github),
1252
+ now,
1253
+ reason: standing ? "no-new-evidence" : "no-new-evidence:standing-unknown",
1254
+ });
1255
+ log(
1256
+ `dispatch: #${row.number} skipped — no new evidence at ${row.head_sha.slice(0, 7)}; standing ${standing ?? "review (verdict unknown)"} stands, PR ${restored}`,
1257
+ );
1258
+ continue;
1259
+ }
1260
+
1261
+ // A SYSTEM-provenance re-trigger (push / CI / staleness at an existing
1262
+ // trigger state — never a fresh human ask, which applyEvent stamps
1263
+ // "human") consults the recheck delta classifier before paying for a
1264
+ // full round. Empty/trivial delta since the reviewed head ⇒ consume the
1265
+ // trigger and leave state as an uphold would; significant ⇒ the full
1266
+ // round below runs. First reviews never short-circuit, and a withheld
1267
+ // approval at this head is its own new evidence: that round must run so
1268
+ // the approval can finally post.
1269
+ if (
1270
+ roundType !== "first" &&
1271
+ (standing === "APPROVE" || standing === "REQUEST_CHANGES") &&
1272
+ mark?.head_sha &&
1273
+ !withheldHere &&
1274
+ row.trigger_provenance === "system"
1275
+ ) {
1276
+ const sc = await classifySystemRetrigger(deps, row, mark);
1277
+ if (sc.skip) {
1278
+ await consumeTriggers(deps, row.number, { markInProgress: false });
1279
+ const movedShort = movedDuringConsume(store, row);
1280
+ if (movedShort) {
1281
+ logSupersededSettlement(deps, row, "system short-circuit", movedShort);
1282
+ continue;
1283
+ }
1284
+ if (row.head_sha !== mark.head_sha) {
1285
+ // Uphold semantics, daemon-side: a never-posted (withheld) approval
1286
+ // follows the head on a trivial/empty delta.
1287
+ carryWithheldMark(
1288
+ store,
1289
+ row.number,
1290
+ row.head_sha,
1291
+ `dispatch-short-circuit:${sc.reason}`,
1292
+ (m) => log(m),
1293
+ );
1294
+ if (standing === "APPROVE") {
1295
+ // This head's push question is answered (trivially). Claim the
1296
+ // same fingerprint the reconcile push-backstop checks so it is
1297
+ // not re-asked every sweep; INSERT OR IGNORE means a claim a
1298
+ // real recheck already holds is left untouched. No wake is sent,
1299
+ // so no receipt can exist: never the undelivered sweep's to
1300
+ // release.
1301
+ store.claimRecheck(
1302
+ row.number,
1303
+ recheckFingerprint({ trigger: "push" }, row.head_sha),
1304
+ {
1305
+ head_sha: row.head_sha,
1306
+ trigger: "push",
1307
+ reason: `dispatch-short-circuit:${sc.reason}`,
1308
+ nudge: false,
1309
+ },
1310
+ );
1311
+ }
1312
+ }
1313
+ // The trivial delta answers this head: a successor owed at it is
1314
+ // settled, so reconcile does not promote it again on every pass.
1315
+ store.settleSuccessor(row.number, row.head_sha);
1316
+ log(
1317
+ `dispatch: #${row.number} short-circuited (${sc.reason}) — system re-trigger, standing ${standing}`,
1318
+ );
1319
+ store.logEvent("dispatch_short_circuit", row.number, {
1320
+ reason: sc.reason,
1321
+ delta_class: sc.class,
1322
+ head_sha: row.head_sha,
1323
+ last_reviewed_sha: mark.head_sha,
1324
+ standing_verdict: standing,
1325
+ files: sc.files,
1326
+ });
1327
+ applyEvent(
1328
+ store,
1329
+ row.number,
1330
+ { type: "verdict", verdict: standing },
1331
+ {},
1332
+ {
1333
+ botLogin: botAccountLogin(cfg.github),
1334
+ now,
1335
+ reason: `dispatch-short-circuit:${sc.reason}`,
1336
+ },
1337
+ );
1338
+ continue;
1339
+ }
1340
+ }
1341
+
1342
+ markedInProgress = true;
1343
+ const consumed = await consumeTriggers(deps, row.number);
1344
+
1345
+ // The branch this PR targets (base freshness) and its body
1346
+ // (pass-intent's artifact + substance bar) come off one getPr read. A
1347
+ // failed read degrades to "no base facts, no intent lens", never a
1348
+ // failed round.
1349
+ let baseRef = "";
1350
+ let prBody: string | undefined;
1351
+ try {
1352
+ const ref = await deps.adapter.getPr(row.number);
1353
+ baseRef = ref.base_ref;
1354
+ prBody = ref.body;
1355
+ } catch (err) {
1356
+ log(`dispatch: base ref lookup failed on #${row.number}`, err);
1357
+ }
1358
+
1359
+ // Prepare the tree BEFORE building the order, so the prompt cites the sha
1360
+ // the passes will actually read. `row.head_sha` is a webhook/reconcile
1361
+ // snapshot; a push landing between it and the fetch would otherwise have
1362
+ // passes cite one sha while reading another tree, with anchors computed
1363
+ // against a diff the review is not about.
1364
+ //
1365
+ // And BEFORE the changed-files read: every file-derived fact of the round
1366
+ // — the migration gate, the pass plan and sweep focus, the collision
1367
+ // pre-check, the diffstat — must describe the head the tree holds (read
1368
+ // first, a migration a racing push added would get no gate and no
1369
+ // collision check). The files API is by PR number, so read after the
1370
+ // fetch it lists at least the prepared head.
1371
+ const tree = deps.prepareWorktree
1372
+ ? await deps.prepareWorktree(row.number, baseRef, row.head_sha)
1373
+ : undefined;
1374
+ // The head this round reviews — the prepared tree's, else the row's —
1375
+ // decided once, so the round row, the prior findings, the pre-check,
1376
+ // the recovery identity and the order all name the same one.
1377
+ const preparedHead = reviewedHeadOf(tree, row);
1378
+
1379
+ const files = await deps.adapter.listPrFiles(row.number);
1380
+ const gate = eco.migrationGate(files);
1381
+ // The repo's written style guide, if any: the style lens runs only on
1382
+ // one (`[review] style_lens`). Total — an unreadable tree is no guide.
1383
+ // Read off the prepared tree BEFORE the plan, so the plan is built once
1384
+ // and the round row records exactly the order's plan.
1385
+ const styleGuide = lensEnabled(cfg.lenses, STYLE_PASS, cfg.review.style_lens)
1386
+ ? findStyleGuide(resolve(sandboxRoot(), worktreeFor(row.number)))
1387
+ : [];
1388
+ // The linters a CI job of the repo runs: lint conformance runs only on
1389
+ // them (`[review] lint_conformance`). Total — an unreadable tree is none.
1390
+ const ciLinters = lensEnabled(cfg.lenses, LINT_CONFORMANCE_PASS, cfg.review.lint_conformance)
1391
+ ? findCiLinters(resolve(sandboxRoot(), worktreeFor(row.number)))
1392
+ : [];
1393
+ // The plan is pure and deterministic in `files` + the PR body + the
1394
+ // style guide + the linters: built ONCE here and handed to
1395
+ // buildRoundOrder, so the round row and the order never disagree.
1396
+ const fullPlan = buildPassPlan(
1397
+ cfg,
1398
+ files,
1399
+ { title: row.title, body: prBody },
1400
+ eco,
1401
+ deps.heuristics,
1402
+ styleGuide,
1403
+ ciLinters,
1404
+ );
1405
+ // Follow-up rounds (docs/follow-up-rounds.md): every round after the
1406
+ // first reviews the PR-owned change since the last reviewed head — or,
1407
+ // on any doubt, runs a full re-review. `files` stays the WHOLE
1408
+ // PR for every per-PR fact (the migration gate, the pre-check, the
1409
+ // directive escalation, the diffstat, dependency sources, memory and
1410
+ // lessons); only the plan and the coverage check read the delta.
1411
+ let followUp: FollowUpDecision | null = null;
1412
+ let fullRound: { reason: FollowUpFullReason; detail?: Record<string, unknown> } | null = null;
1413
+ if (roundType === "re-review") {
1414
+ const decided = await decideFollowUp(
1415
+ {
1416
+ cfg: deps.cfg,
1417
+ history: deps.store,
1418
+ ...(deps.prOwnedDelta ? { prOwnedDelta: deps.prOwnedDelta } : {}),
1419
+ ...(deps.heuristics ? { heuristics: deps.heuristics } : {}),
1420
+ },
1421
+ {
1422
+ pr: row.number,
1423
+ markHead: mark?.head_sha,
1424
+ tree,
1425
+ files,
1426
+ fullPlan,
1427
+ standing,
1428
+ // Read before this dispatch spends it: a withheld approval is
1429
+ // one GitHub never saw, so the sweep stays owed behind it.
1430
+ approvalWithheld: withheld !== undefined,
1431
+ ecosystem: eco,
1432
+ styleGuide,
1433
+ ciLinters,
1434
+ },
1435
+ );
1436
+ if (decided.kind === "delta") followUp = decided.decision;
1437
+ else
1438
+ fullRound = {
1439
+ reason: decided.reason,
1440
+ ...(decided.detail ? { detail: decided.detail } : {}),
1441
+ };
1442
+ }
1443
+ let passPolicy = followUp ? followUp.plan : fullPlan;
1444
+ let { plannedPasses, lenses, addablePasses, validateTier } = recordedPlan(
1445
+ cfg,
1446
+ passPolicy,
1447
+ followUp?.optionalPlan,
1448
+ );
1449
+
1450
+ // The PR must still be queued when its round row is written — read in
1451
+ // the same turn as `startRound`. Preparation awaited GitHub and git, and
1452
+ // a verdict settling an older round (a late, orphan-swept one), a close
1453
+ // or the operator may have moved the row meanwhile; then this round is
1454
+ // nobody's work and no row is written. The in-progress label the
1455
+ // trigger consumption put on comes off again (best effort).
1456
+ const live = store.getPr(row.number);
1457
+ if (live?.state !== "queued") {
1458
+ log(
1459
+ `dispatch: #${row.number} left the queue (${live?.state ?? "gone"}) while its round was prepared — no round started`,
1460
+ );
1461
+ store.logEvent("dispatch_abandoned", row.number, {
1462
+ state: live?.state ?? null,
1463
+ head_sha: preparedHead,
1464
+ });
1465
+ try {
1466
+ await deps.adapter.removeLabel(row.number, cfg.github.in_progress_label);
1467
+ } catch (err) {
1468
+ log(`dispatch: #${row.number} in-progress label not removed`, err);
1469
+ }
1470
+ continue;
1471
+ }
1472
+ // Diffstat comes free off the files list dispatch already fetched — no
1473
+ // extra GitHub call in the hot path. trigger_ts is copied off pr_state
1474
+ // here because the `dispatched` transition clears it moments later.
1475
+ // validate_tier is recorded so a later audit can tell which tier judged
1476
+ // which finding.
1477
+ const diffstat = diffstatOf(files);
1478
+ const roundId = store.startRound(
1479
+ row.number,
1480
+ roundType,
1481
+ lenses,
1482
+ cfg.daemon.round_deadline_min,
1483
+ {
1484
+ ...diffstat,
1485
+ trigger_ts: row.trigger_ts,
1486
+ validate_tier: validateTier,
1487
+ pass_plan: plannedPasses,
1488
+ addable_passes: addablePasses,
1489
+ head_sha: preparedHead,
1490
+ ...(tree?.merge_base ? { merge_base: tree.merge_base } : {}),
1491
+ ...(roundType === "re-review"
1492
+ ? followUp
1493
+ ? {
1494
+ scope: "delta" as const,
1495
+ delta_base_sha: followUp.fromSha,
1496
+ delta_lines: followUp.delta.lines,
1497
+ delta_files: followUp.delta.files.length,
1498
+ sweep_owed: followUp.sweepPlan !== undefined,
1499
+ ...(followUp.sweepPlan
1500
+ ? { sweep_plan: followUp.sweepPlan }
1501
+ : { sweep_status: "not_owed" as const }),
1502
+ }
1503
+ : { scope: "full" as const }
1504
+ : {}),
1505
+ },
1506
+ );
1507
+ provisionalRoundId = roundId;
1508
+ const roundNo = (
1509
+ store.db.prepare("SELECT round_no FROM rounds WHERE id = ?").get(roundId) as {
1510
+ round_no: number;
1511
+ }
1512
+ ).round_no;
1513
+
1514
+ // The delta as one patch file, beside the round's other state
1515
+ // files. A write that fails falls the round back to a full re-review —
1516
+ // the follow-up prompt names the file, and a missing one is never a
1517
+ // quieter round.
1518
+ let roundDelta: RoundDelta | undefined;
1519
+ if (followUp) {
1520
+ const patchFile = resolve(
1521
+ sandboxRoot(),
1522
+ "state",
1523
+ `pr-${row.number}-round-${roundNo}-delta.patch`,
1524
+ );
1525
+ try {
1526
+ mkdirSync(resolve(sandboxRoot(), "state"), { recursive: true });
1527
+ writeFileSync(`${patchFile}.tmp`, renderDeltaPatch(followUp.delta.files));
1528
+ renameSync(`${patchFile}.tmp`, patchFile);
1529
+ roundDelta = roundDeltaOf(followUp, patchFile);
1530
+ } catch (err) {
1531
+ log(`dispatch: #${row.number} delta patch not written — full round`, err);
1532
+ fullRound = {
1533
+ reason: "patch-write-failed",
1534
+ detail: { error: err instanceof Error ? err.message : String(err) },
1535
+ };
1536
+ followUp = null;
1537
+ passPolicy = fullPlan;
1538
+ ({ plannedPasses, lenses, addablePasses, validateTier } = recordedPlan(cfg, passPolicy));
1539
+ store.setRoundPlan(roundId, lenses, {
1540
+ validate_tier: validateTier,
1541
+ pass_plan: plannedPasses,
1542
+ addable_passes: addablePasses,
1543
+ });
1544
+ store.setRoundMeta(roundId, {
1545
+ scope: "full",
1546
+ delta_base_sha: null,
1547
+ delta_lines: null,
1548
+ delta_files: null,
1549
+ sweep_owed: null,
1550
+ sweep_plan: null,
1551
+ sweep_status: null,
1552
+ });
1553
+ }
1554
+ }
1555
+ if (fullRound) {
1556
+ store.logEvent("followup_full_round", row.number, {
1557
+ round_id: roundId,
1558
+ round_no: roundNo,
1559
+ reason: fullRound.reason,
1560
+ head_sha: preparedHead,
1561
+ last_reviewed_sha: mark?.head_sha ?? null,
1562
+ ...(fullRound.detail ?? {}),
1563
+ });
1564
+ log(`dispatch: #${row.number} round ${roundNo} runs in full (${fullRound.reason})`);
1565
+ }
1566
+ if (passPolicy.behaviour_unread)
1567
+ store.logEvent("behaviour_delta_unread", row.number, {
1568
+ round_id: roundId,
1569
+ round_no: roundNo,
1570
+ files: passPolicy.behaviour_unread,
1571
+ });
1572
+ // The prior findings this follow-up round verifies — listed in
1573
+ // prior_finding_checks and written to the round's sidecar. Total: a list
1574
+ // that cannot be assembled renders an explicit "verify from your ledger"
1575
+ // note, never an empty one.
1576
+ const priorFindings = followUp
1577
+ ? preparePriorFindings(
1578
+ {
1579
+ store,
1580
+ findingsDir: deps.findingsDir ?? resolve(sandboxRoot(), "state", "findings"),
1581
+ stateDir: resolve(sandboxRoot(), "state"),
1582
+ log,
1583
+ },
1584
+ row.number,
1585
+ roundNo,
1586
+ preparedHead,
1587
+ )
1588
+ : null;
1589
+ const priorFindingsNote = priorFindings?.note ?? "";
1590
+
1591
+ if (tree?.pull_ref_sha) {
1592
+ log(
1593
+ `dispatch: #${row.number} GitHub's refs/pull/${row.number}/head lags (${tree.pull_ref_sha}); reviewing the API head ${tree.head_sha}`,
1594
+ );
1595
+ store.logEvent("pull_ref_lagged", row.number, {
1596
+ round_id: roundId,
1597
+ pull_ref: tree.pull_ref_sha,
1598
+ head: tree.head_sha,
1599
+ });
1600
+ }
1601
+ if (tree && row.head_sha && tree.head_sha !== row.head_sha) {
1602
+ log(`dispatch: #${row.number} head drift ${row.head_sha} → ${tree.head_sha}`);
1603
+ store.logEvent("head_drift", row.number, {
1604
+ round_id: roundId,
1605
+ expected: row.head_sha,
1606
+ actual: tree.head_sha,
1607
+ source: "worktree_prepare",
1608
+ });
1609
+ }
1610
+
1611
+ // The integrity fingerprint — git's config/hook files and this system's
1612
+ // own policy surface under the sandbox root — taken HERE: after the
1613
+ // worktree is materialised (so the tree is the one the round will read)
1614
+ // and before the session hears about the round (so nothing it does is
1615
+ // inside the baseline). `verdicts.measureUsage` re-checks it when the
1616
+ // round ends. Best-effort in both directions: a fingerprint that cannot
1617
+ // be taken leaves the round unchecked — it never fails a round. It hashes
1618
+ // the whole policy surface (dist/, scripts/, …) asynchronously, a loop
1619
+ // turn after every file, and is BOUNDED, because this await sits inside
1620
+ // the one serialized dispatch pump: a read that never returns leaves
1621
+ // this round unchecked (no baseline), never every later PR undispatched.
1622
+ try {
1623
+ const budgetMs = Math.max(0, deps.integrityBudgetMs ?? INTEGRITY_BUDGET_MS);
1624
+ const baseline = await withinBudget(
1625
+ (deps.integrityFingerprint ?? fingerprintRoundIntegrity)(
1626
+ roundIntegrityOpts(githubCloneDir(cfg.github), sandboxRoot(), worktreeFor(row.number)),
1627
+ ),
1628
+ budgetMs,
1629
+ );
1630
+ if (baseline.kind === "done") {
1631
+ store.recordRoundGitConfig(roundId, baseline.value);
1632
+ } else {
1633
+ store.logEvent("round_integrity_unchecked", row.number, {
1634
+ round_id: roundId,
1635
+ stage: "baseline",
1636
+ reason: "budget",
1637
+ budget_ms: budgetMs,
1638
+ });
1639
+ log(
1640
+ `dispatch: #${row.number} integrity baseline not taken within ${budgetMs} ms — the round goes unchecked`,
1641
+ );
1642
+ }
1643
+ } catch (err) {
1644
+ log(`dispatch: #${row.number} git-config fingerprint unavailable`, err);
1645
+ }
1646
+
1647
+ // The ecosystem pre-check, before the session hears about the round.
1648
+ // Django delivers collisions as its
1649
+ // own COMMENT, so whatever the review concludes, the renumber never
1650
+ // rides on the verdict; a `none` ecosystem runs nothing at all.
1651
+ const precheck = await ecosystemPreCheck(deps, eco, {
1652
+ round_id: roundId,
1653
+ pr: row.number,
1654
+ files,
1655
+ head_sha: preparedHead,
1656
+ base_ref: tree?.base_ref ?? baseRef,
1657
+ base_sha: tree?.base_sha ?? "",
1658
+ });
1659
+ const migrationCollisions = precheck.collisions;
1660
+ // A degraded pre-check is an anomaly the overseer should see (same
1661
+ // channel verdicts.ts uses — an overseer inject, never a new path). The
1662
+ // round itself still runs: the prompt carries the "check collisions
1663
+ // yourself" note, so the class is covered either way. A configured-off
1664
+ // ecosystem ("none") can never reach this block — off is not degraded,
1665
+ // and the seam's outcome union keeps the two apart structurally.
1666
+ if (precheck.degraded) {
1667
+ // Durable anomaly record + the overseer wake (WAKE class) — one
1668
+ // logAnomaly call, the store's sink writes the inject.
1669
+ store.logAnomaly(
1670
+ "migration_precheck_degraded",
1671
+ row.number,
1672
+ roundNo,
1673
+ "migration pre-check could not run (no base facts, or the base read failed); the round prompt un-muzzles the lens",
1674
+ [
1675
+ `ANOMALY (migration-precheck) — PR #${row.number} round ${roundNo}`,
1676
+ "The migration-number pre-check could not run this round (no base facts, or the base read failed) — " +
1677
+ "see the migration_precheck_skipped/failed event on this PR.",
1678
+ "The round prompt tells the session to check base-branch migration-number collisions itself, so nothing is lost — " +
1679
+ "but a recurring degraded pre-check means the base clone or worktree preparation is broken and needs a look.",
1680
+ ].join("\n"),
1681
+ );
1682
+ }
1683
+ // A withheld collision COMMENT is an anomaly too: the round
1684
+ // still hears the collisions through the gate note, but every later
1685
+ // collision comment on this PR stays withheld until the torn ledger row
1686
+ // is repaired — a human has to know that, once.
1687
+ if (
1688
+ precheck.commentWithheld &&
1689
+ !store.anomalyRaised("migration_collision_comment_withheld", row.number)
1690
+ ) {
1691
+ store.logAnomaly(
1692
+ "migration_collision_comment_withheld",
1693
+ row.number,
1694
+ roundNo,
1695
+ "migration collision COMMENT withheld: the event log holds a migration_collision_comment row for this PR that cannot be read, " +
1696
+ "so a duplicate cannot be ruled out; the collisions still ride the round prompt",
1697
+ [
1698
+ `ANOMALY (migration-precheck) — PR #${row.number} round ${roundNo}`,
1699
+ "The migration-number pre-check found collisions but WITHHELD the GitHub comment: the event log holds a " +
1700
+ "migration_collision_comment row for this PR whose payload cannot be read, so posting could duplicate an earlier comment.",
1701
+ "The session still hears the collisions through the round prompt's gate note, so nothing is lost this round — " +
1702
+ "but EVERY collision comment on this PR stays withheld until that row is repaired or deleted " +
1703
+ `(events table, kind = 'migration_collision_comment', pr = ${row.number}; see the migration_collision_comment_withheld events).`,
1704
+ ].join("\n"),
1705
+ );
1706
+ }
1707
+
1708
+ // First-party directive edits: detect them and pick the ONE
1709
+ // teammate the escalation comment @-mentions, deterministically, before
1710
+ // the session hears about the round.
1711
+ const firstPartyDirectives = await directiveEscalationFor(deps, {
1712
+ pr: row.number,
1713
+ files,
1714
+ merge_base: tree?.merge_base ?? "",
1715
+ base_sha: tree?.base_sha ?? "",
1716
+ });
1717
+
1718
+ const rawRecoveryInstruction = store.roundRecoveryInstruction(row.number, roundNo);
1719
+ const recoveryInstruction = store.roundRecoveryInstructionForDispatch(
1720
+ row.number,
1721
+ roundNo,
1722
+ preparedHead,
1723
+ );
1724
+ if (rawRecoveryInstruction && !recoveryInstruction) {
1725
+ store.logEvent("round_recovery_instruction_not_applied", row.number, {
1726
+ target_round_no: roundNo,
1727
+ stored_head_sha: rawRecoveryInstruction.target_head_sha,
1728
+ prepared_head_sha: preparedHead,
1729
+ source_round_no: rawRecoveryInstruction.source_round_no,
1730
+ source_command_id: rawRecoveryInstruction.source_command_id,
1731
+ reason: "exact_successor_identity_mismatch",
1732
+ });
1733
+ }
1734
+ const recovery: RoundRecoveryContext | undefined = recoveryInstruction
1735
+ ? {
1736
+ stage: recoveryInstruction.stage,
1737
+ source_round_no: recoveryInstruction.source_round_no,
1738
+ source_command_id: recoveryInstruction.source_command_id,
1739
+ target_head_sha: recoveryInstruction.target_head_sha,
1740
+ immediate_attempt: recoveryInstruction.immediate_attempt,
1741
+ retry_epoch: recoveryInstruction.retry_epoch,
1742
+ ...(recoveryInstruction.source_incident_id
1743
+ ? { source_incident_id: recoveryInstruction.source_incident_id }
1744
+ : {}),
1745
+ }
1746
+ : undefined;
1747
+ // Third-party sources for the passes (`[review] dependency_sources`).
1748
+ // Total by contract — a dead registry yields an empty note, never a
1749
+ // failed or delayed-past-budget round.
1750
+ let dependencyNote = "";
1751
+ if (cfg.review.dependency_sources && deps.dependencySources) {
1752
+ try {
1753
+ dependencyNote = await deps.dependencySources(
1754
+ resolve(sandboxRoot(), worktreeFor(row.number)),
1755
+ files,
1756
+ );
1757
+ } catch (err) {
1758
+ log(`dispatch: #${row.number} dependency sources unavailable`, err);
1759
+ }
1760
+ }
1761
+ const built = buildRoundOrder({
1762
+ tree,
1763
+ plan: passPolicy,
1764
+ ...(followUp && roundDelta
1765
+ ? {
1766
+ followUp: {
1767
+ delta: roundDelta,
1768
+ deltaFiles: followUp.deltaFiles,
1769
+ priorFindingsNote,
1770
+ ...(priorFindings?.list ? { priorFindings: priorFindings.list } : {}),
1771
+ ...(followUp.optionalPlan ? { optionalPlan: followUp.optionalPlan } : {}),
1772
+ ...(followUp.sweepPlan ? { sweepPlan: followUp.sweepPlan } : {}),
1773
+ ...(followUp.sweepUnswept ? { sweepUnswept: followUp.sweepUnswept } : {}),
1774
+ ...(followUp.sweepBehaviourUnread
1775
+ ? { sweepBehaviourUnread: followUp.sweepBehaviourUnread }
1776
+ : {}),
1777
+ },
1778
+ }
1779
+ : {}),
1780
+ ecosystem: eco,
1781
+ ...(styleGuide.length ? { styleGuide } : {}),
1782
+ ...(ciLinters.length ? { ciLinters } : {}),
1783
+ ...(dependencyNote ? { dependencyNote } : {}),
1784
+ ...(recovery ? { recovery } : {}),
1785
+ migrationCollisions,
1786
+ ...(precheck.commentUndelivered ? { migrationCollisionsUndelivered: true } : {}),
1787
+ migrationPrecheckDegraded: precheck.degraded,
1788
+ ...(firstPartyDirectives !== undefined ? { firstPartyDirectives } : {}),
1789
+ cfg,
1790
+ row,
1791
+ files,
1792
+ roundId,
1793
+ roundNo,
1794
+ reviewNo: priorRounds + 1,
1795
+ roundType,
1796
+ worktree: worktreeFor(row.number),
1797
+ ...((): { settledDigest: string; ledgerCarry?: LedgerCarry } => {
1798
+ // One read of the ledger mirror per order; an explicit
1799
+ // settledDigest supplier still wins for the digest.
1800
+ const fromLedger = ledgerCarry(row.number);
1801
+ return {
1802
+ settledDigest: deps.settledDigest ? deps.settledDigest(row.number) : fromLedger.settled,
1803
+ ...(fromLedger.carry ? { ledgerCarry: fromLedger.carry } : {}),
1804
+ };
1805
+ })(),
1806
+ issuedAt: now().toISOString(),
1807
+ ...(prBody !== undefined ? { prBody } : {}),
1808
+ ...(deps.heuristics !== undefined ? { heuristics: deps.heuristics } : {}),
1809
+ // The host's lens tools, so the prompt and the task files name the ones its
1810
+ // sessions have (rule 9: `lens` / `lens_wave` on workers).
1811
+ ...(deps.hooks.lensRuntime ? { lensRuntime: deps.hooks.lensRuntime } : {}),
1812
+ });
1813
+ store.stampPostingKey(roundId, built.posting_key!);
1814
+ // The round row already holds the order's plan: the style guide and the
1815
+ // linters were read before the plan was built, and the plan was built
1816
+ // once.
1817
+
1818
+ // The ONE memory call this round WAITS for, placed after the
1819
+ // worktree and the order so the context is seeded from the same sha the
1820
+ // passes read. It is bounded and total: the worst case is this round's
1821
+ // prompt arriving memory-less a couple of seconds later.
1822
+ //
1823
+ // Under `[memory] delivery = "on"` the round's LESSON
1824
+ // SELECTION is awaited beside it — the prompt, the task files and the
1825
+ // lessons files are rendered from it. Its budget (≤ 2 s) runs from
1826
+ // here, inside the seed's deadline, so the round waits for the slower
1827
+ // of the two, never for their sum — but no lesson find is SENT before
1828
+ // the round's own context search was: the assembler says when it
1829
+ // issued the search, and a seed that never does (no assembler, a
1830
+ // failure before the search, an assembler that does not say) opens
1831
+ // the way when it settles. So the seed's search never queues behind a
1832
+ // lesson find on the server's one-thread embedder. Under
1833
+ // `shadow`/`off` the selection starts only after the handoff,
1834
+ // unawaited (below).
1835
+ const delivering = lessonsDelivering(deps, log);
1836
+ // No case batch starts while this round reads memory — the
1837
+ // window opens here, before the seed, and closes once the selection
1838
+ // has taken over: under `on` when the awaited selection is in; under
1839
+ // `shadow` once `start` has opened the selector's own window, after the
1840
+ // handoff (a batch started during the handoff would sit in front of the
1841
+ // selection's finds). The `finally` below is the belt.
1842
+ endMemoryWindow = openMemoryWindow(deps, row.number, log);
1843
+ let seeded: RoundMemory;
1844
+ let selection: RoundSelectionResult | null = null;
1845
+ if (delivering) {
1846
+ let searchSent!: () => void;
1847
+ const seedSearchSent = new Promise<void>((resolveSent) => (searchSent = resolveSent));
1848
+ const seeding = seedRoundMemory(deps, built, files, () => searchSent());
1849
+ void seeding.then(
1850
+ () => searchSent(),
1851
+ () => searchSent(),
1852
+ );
1853
+ [seeded, selection] = await Promise.all([
1854
+ seeding,
1855
+ selectRoundLessons(deps, built, files, seedSearchSent),
1856
+ ]);
1857
+ endMemoryWindow();
1858
+ } else {
1859
+ seeded = await seedRoundMemory(deps, built, files);
1860
+ }
1861
+ const delivery: RoundLessonDelivery | null = selection
1862
+ ? buildRoundDelivery({
1863
+ selections: selection.selections,
1864
+ texts: selection.texts,
1865
+ pr: row.number,
1866
+ round_no: roundNo,
1867
+ head_sha: built.head_sha,
1868
+ repo: cfg.github.repo,
1869
+ rowsInTaskFiles: cfg.review.task_files,
1870
+ })
1871
+ : null;
1872
+
1873
+ // `[review] task_files` (packages/pi-plugin/src/passtasks.ts): each discovery row's
1874
+ // complete lens task, written beside the round's pass outputs so the
1875
+ // session launches lenses with a pointer instead of generating every
1876
+ // task string. Total: a write that fails leaves the order without task
1877
+ // files, and the session writes the strings itself. Under `on` each
1878
+ // row's delivered lessons ride its task file.
1879
+ // `[review] context_pack = "pack-first"`: the round's CONTEXT
1880
+ // PACK, written before the task files that point at it, and only where
1881
+ // children share a cache key (`[engine] lens_cache_group`) — an unshared
1882
+ // pack costs every lens more, not less. Total: a pack that cannot be
1883
+ // written leaves the round exactly as without one; a stale pack from an
1884
+ // earlier attempt of this round number never survives into it.
1885
+ const passesDir = resolve(sandboxRoot(), "state", `pr-${row.number}-round-${roundNo}-passes`);
1886
+ const packPath = join(passesDir, CONTEXT_PACK_FILE);
1887
+ const removeStalePack = (): void => {
1888
+ try {
1889
+ rmSync(packPath, { force: true });
1890
+ } catch (err) {
1891
+ log(`dispatch: #${row.number} stale context pack not removed`, err);
1892
+ }
1893
+ };
1894
+ let packed: RoundOrder = built;
1895
+ if (
1896
+ cfg.review.task_files &&
1897
+ cfg.review.context_pack === "pack-first" &&
1898
+ cfg.engine.lens_cache_group > 0
1899
+ ) {
1900
+ try {
1901
+ const pack = renderContextPack({
1902
+ pr: row.number,
1903
+ headSha: built.head_sha,
1904
+ mergeBase: built.merge_base,
1905
+ files,
1906
+ ...(followUp ? { deltaPatch: renderDeltaPatch(followUp.delta.files) } : {}),
1907
+ readFile: (p) => readWorktreeFileForPack(resolve(sandboxRoot(), built.worktree), p),
1908
+ maxTokens: cfg.review.context_pack_max_tokens,
1909
+ });
1910
+ mkdirSync(passesDir, { recursive: true });
1911
+ writeFileSync(`${packPath}.tmp`, pack.text);
1912
+ renameSync(`${packPath}.tmp`, packPath);
1913
+ packed = { ...built, context_pack_file: packPath };
1914
+ store.logEvent("context_pack_written", row.number, {
1915
+ round_id: roundId,
1916
+ chars: pack.chars,
1917
+ diff_files: pack.diffFiles.length,
1918
+ diff_omitted: pack.diffOmitted.length,
1919
+ full_files: pack.fullFiles.length,
1920
+ });
1921
+ } catch (err) {
1922
+ log(`dispatch: #${row.number} context pack not written`, err);
1923
+ // A child finds the pack beside its task file, not through the
1924
+ // order — an earlier attempt's pack left here would be read as
1925
+ // this round's.
1926
+ removeStalePack();
1927
+ }
1928
+ } else {
1929
+ removeStalePack();
1930
+ }
1931
+
1932
+ let order = packed;
1933
+ let rowsDelivered = false;
1934
+ if (cfg.review.task_files) {
1935
+ try {
1936
+ const dir = join(passesDir, "tasks");
1937
+ const tasks = writePassTasks(
1938
+ dir,
1939
+ packed,
1940
+ renderGateNotes(packed, eco),
1941
+ delivery
1942
+ ? {
1943
+ rows: new Map(delivery.rows.map((r) => [r.row, r.lessons] as const)),
1944
+ repo: delivery.repo,
1945
+ }
1946
+ : undefined,
1947
+ );
1948
+ order = {
1949
+ ...packed,
1950
+ pass_task_files: tasks.map((t) => ({ row: t.row, agent: t.agent, path: t.path })),
1951
+ };
1952
+ rowsDelivered = true;
1953
+ } catch (err) {
1954
+ log(`dispatch: #${row.number} lens task files unavailable`, err);
1955
+ }
1956
+ // The owed pre-approval sweep's task files — its own
1957
+ // directory and row index (`row` indexes `sweep_plan`), the whole PR's
1958
+ // full plan rendered exactly as a full round's. No lesson rows ride
1959
+ // them: lesson selection covers `pass_plan` rows only. Total, as above.
1960
+ // With no sweep owed, an earlier attempt's sweep dir is removed, so
1961
+ // nothing stale can be launched from it.
1962
+ const sweepDir = join(passesDir, "tasks-sweep");
1963
+ if (!built.sweep_plan || built.sweep_plan.length === 0) {
1964
+ try {
1965
+ rmSync(sweepDir, { recursive: true, force: true });
1966
+ } catch (err) {
1967
+ log(`dispatch: #${row.number} stale sweep task dir not removed`, err);
1968
+ }
1969
+ }
1970
+ // The optional rows' task files — their own directory and
1971
+ // row index (`row` indexes `optional_plan`), rendered from the
1972
+ // follow-up order itself, so they carry the change since the last
1973
+ // review exactly as the core rows do. Total, as above.
1974
+ const optionalDir = join(passesDir, "tasks-optional");
1975
+ if (!built.optional_plan || built.optional_plan.length === 0) {
1976
+ try {
1977
+ rmSync(optionalDir, { recursive: true, force: true });
1978
+ } catch (err) {
1979
+ log(`dispatch: #${row.number} stale optional task dir not removed`, err);
1980
+ }
1981
+ } else {
1982
+ try {
1983
+ const optionalOrder = optionalOrderOf(packed, built.optional_plan);
1984
+ const optionalTasks = writePassTasks(
1985
+ optionalDir,
1986
+ optionalOrder,
1987
+ renderGateNotes(optionalOrder, eco),
1988
+ );
1989
+ order = {
1990
+ ...order,
1991
+ optional_task_files: optionalTasks.map((t) => ({
1992
+ row: t.row,
1993
+ agent: t.agent,
1994
+ path: t.path,
1995
+ })),
1996
+ };
1997
+ } catch (err) {
1998
+ log(`dispatch: #${row.number} optional task files unavailable`, err);
1999
+ }
2000
+ }
2001
+ if (built.sweep_plan && built.sweep_plan.length > 0) {
2002
+ try {
2003
+ const sweepOrder = sweepOrderOf(packed, built.sweep_plan);
2004
+ const sweepTasks = writePassTasks(
2005
+ sweepDir,
2006
+ sweepOrder,
2007
+ renderGateNotes(sweepOrder, eco),
2008
+ );
2009
+ order = {
2010
+ ...order,
2011
+ sweep_task_files: sweepTasks.map((t) => ({
2012
+ row: t.row,
2013
+ agent: t.agent,
2014
+ path: t.path,
2015
+ })),
2016
+ };
2017
+ } catch (err) {
2018
+ log(`dispatch: #${row.number} sweep task files unavailable`, err);
2019
+ }
2020
+ }
2021
+ }
2022
+ const memory = deliverRoundLessons(deps, {
2023
+ seeded,
2024
+ // A row's lessons travel only in its task file: none written, none
2025
+ // delivered — the index must not say otherwise.
2026
+ delivery: delivery && !rowsDelivered ? { ...delivery, rows: [] } : delivery,
2027
+ dir: join(passesDir, "lessons"),
2028
+ });
2029
+
2030
+ // A PR closed while its round was prepared gets no session and no
2031
+ // prompt: the provisional round is discarded like any failed
2032
+ // preparation (the close hook already archived the session).
2033
+ if (store.getPr(row.number)?.state === "closed")
2034
+ throw new Error(`#${row.number} closed while its round was prepared — not handed off`);
2035
+ // The round is provisional until the worktree/session/prompt handoff
2036
+ // completes; publishing reviewing/running state any earlier would leave
2037
+ // an orphan round whenever worktree preparation fails.
2038
+ const prompt = render(order, memory);
2039
+ // Rendering and optional preparation may have awaited or invoked callers
2040
+ // that changed the operator hold. Re-admit at the actual handoff seam.
2041
+ const admitted = store.getPr(row.number);
2042
+ if (!admitted || admitted.state === "closed" || admitted.paused)
2043
+ throw new Error(`#${row.number} closed or paused during preparation — not handed off`);
2044
+ await deps.hooks.dispatchRound(order, prompt);
2045
+ // Record the receipt token this round's prompt carries, so the
2046
+ // recovery paths can tell "delivered" from "never arrived" instead of
2047
+ // inferring it. Stamped only AFTER dispatchRound resolved: the marker
2048
+ // asserts that a prompt bearing it was written, and a dispatch that
2049
+ // threw may not have written it (one that persisted its durable
2050
+ // command first is retained by the catch below, unstamped).
2051
+ store.stampPromptMarker(roundId, roundPromptMarker(row.number, roundNo));
2052
+ spendWithholding(roundId);
2053
+ // The head this round reviews is the next follow-up round's
2054
+ // baseline — pin it against gc so a force-push cannot take it away.
2055
+ // Best-effort: an unpinned head is re-fetched, or the round runs in full.
2056
+ if (deps.pinReviewedHead) {
2057
+ try {
2058
+ const pinned = await deps.pinReviewedHead(row.number, order.head_sha);
2059
+ if (!pinned)
2060
+ log(`dispatch: #${row.number} reviewed head ${order.head_sha.slice(0, 7)} not pinned`);
2061
+ } catch (err) {
2062
+ log(`dispatch: #${row.number} reviewed head not pinned`, err);
2063
+ }
2064
+ }
2065
+ if (memory.lessons) logLessonDelivery(store, roundId, memory.lessons);
2066
+ // The round's lesson selection — the shadow ledger's
2067
+ // input. Started only now, with the round handed off, and never
2068
+ // awaited: under `shadow` it changes nothing in this round's prompt,
2069
+ // task files or agents, so nothing is gained by starting it sooner —
2070
+ // and starting it before the seed would queue its finds ahead of the
2071
+ // round's own memory search on the server's one-thread embedder. Under
2072
+ // `on` it already ran, awaited beside the seed (above).
2073
+ if (deps.lessons && !delivering) {
2074
+ try {
2075
+ deps.lessons.start(order, files);
2076
+ } catch (err) {
2077
+ log(`dispatch: #${row.number} lesson selection not started (ignored)`, err);
2078
+ }
2079
+ }
2080
+ // `start` opened the selector's own window synchronously (or there
2081
+ // is no selection) — the round's own window closes now.
2082
+ endMemoryWindow();
2083
+
2084
+ setPrGates(store, row.number, gate);
2085
+ applyEvent(
2086
+ store,
2087
+ row.number,
2088
+ { type: "dispatched" },
2089
+ {},
2090
+ { botLogin: botAccountLogin(cfg.github), now },
2091
+ );
2092
+ store.logEvent("round_dispatched", row.number, {
2093
+ round_id: roundId,
2094
+ round_no: roundNo,
2095
+ round_type: roundType,
2096
+ // What a later round reviewed (absent on a first round).
2097
+ ...(roundType === "re-review"
2098
+ ? {
2099
+ scope: order.scope === "delta" ? "delta" : "full",
2100
+ ...(order.delta
2101
+ ? {
2102
+ delta_lines: order.delta.lines,
2103
+ delta_files: order.delta.files.length,
2104
+ sweep_owed: (order.sweep_plan?.length ?? 0) > 0,
2105
+ }
2106
+ : {}),
2107
+ }
2108
+ : {}),
2109
+ lenses: order.lenses,
2110
+ validate_tier: validateTier,
2111
+ migration_gate: order.migration_gate,
2112
+ migration_collisions: migrationCollisions.length,
2113
+ migration_precheck_degraded: precheck.degraded,
2114
+ first_party_directive_files: firstPartyDirectives?.files.length ?? 0,
2115
+ head_sha: order.head_sha,
2116
+ base_ref: order.base_ref,
2117
+ base_sha: order.base_sha,
2118
+ triggers_consumed: consumed,
2119
+ // `row` is the pre-dispatch snapshot; `dispatched` has just
2120
+ // cleared these on the live row, and a late guard-2 settlement needs
2121
+ // to explain which trigger paid for the round.
2122
+ trigger_ts: row.trigger_ts,
2123
+ trigger_provenance: row.trigger_provenance,
2124
+ recovery: order.recovery ?? null,
2125
+ });
2126
+ // Slack: the PR's thread root on the first round we see, a
2127
+ // 🔄 line in-thread on later ones. Fire-and-forget: a slow or
2128
+ // blackholed Slack (bounded retries, capped Retry-After, one shared
2129
+ // post queue) must never stall the pump behind it — the announcer's
2130
+ // per-PR lane is claimed synchronously on the call, so thread order
2131
+ // still holds. The announcer swallows its own failures; the catches
2132
+ // here are belt-and-braces, and the round is recorded above already.
2133
+ if (deps.announce) {
2134
+ const pr = row.number;
2135
+ try {
2136
+ void deps.announce
2137
+ .roundStarted({
2138
+ pr: row.number,
2139
+ title: row.title,
2140
+ author: row.author,
2141
+ head_sha: order.head_sha,
2142
+ round_no: roundNo,
2143
+ round_type: roundType,
2144
+ lenses: order.lenses,
2145
+ additions: diffstat.additions,
2146
+ deletions: diffstat.deletions,
2147
+ changed_files: diffstat.changed_files,
2148
+ })
2149
+ .catch((err: unknown) => log(`dispatch: slack announcement failed on #${pr}`, err));
2150
+ } catch (err) {
2151
+ log(`dispatch: slack announcement failed on #${pr}`, err);
2152
+ }
2153
+ }
2154
+ provisionalRoundId = undefined;
2155
+ dispatched.push(order);
2156
+ started++;
2157
+ } catch (err) {
2158
+ // One bad PR must never stall the pump. Before the session handoff, the
2159
+ // provisional round is disposable. AFTER dispatchRound persisted a
2160
+ // durable command, deleting only the round would orphan a live command
2161
+ // that blocks every successor and that the supervisor cannot correlate.
2162
+ // Keep that round authoritative instead; best-effort publish reviewing
2163
+ // state and let the bounded command supervisor finish/recover it.
2164
+ let retainedCommand = false;
2165
+ if (provisionalRoundId !== undefined) {
2166
+ const provisional = store.runningRounds().find((round) => round.id === provisionalRoundId);
2167
+ const command = provisional
2168
+ ? store.sessionCommandForRound(row.number, provisional.round_no)
2169
+ : undefined;
2170
+ if (command) {
2171
+ retainedCommand = true;
2172
+ started++;
2173
+ // The retained round owns the review now: the withholding it
2174
+ // inherited is spent, exactly as on a clean handoff.
2175
+ try {
2176
+ spendWithholding(provisionalRoundId);
2177
+ } catch (spendError) {
2178
+ log(`dispatch: #${row.number} withheld-approval mark not spent`, spendError);
2179
+ }
2180
+ try {
2181
+ applyEvent(
2182
+ store,
2183
+ row.number,
2184
+ { type: "dispatched" },
2185
+ {},
2186
+ { botLogin: botAccountLogin(cfg.github), now },
2187
+ );
2188
+ } catch (publishError) {
2189
+ log(
2190
+ `dispatch: #${row.number} retained command but could not publish reviewing state`,
2191
+ publishError,
2192
+ );
2193
+ }
2194
+ try {
2195
+ store.logAnomaly(
2196
+ "dispatch_commit_failed",
2197
+ row.number,
2198
+ provisional!.round_no,
2199
+ `session command ${command.id} was persisted but dispatch bookkeeping failed: ${String(err)}; bounded supervision retains ownership`,
2200
+ );
2201
+ } catch (anomalyError) {
2202
+ log(`dispatch: #${row.number} retained-command anomaly write failed`, anomalyError);
2203
+ }
2204
+ } else {
2205
+ store.discardRunningRound(provisionalRoundId);
2206
+ // Under `on` the round's lesson selection ran — and was
2207
+ // recorded — before the handoff; a discarded round never ran, so
2208
+ // its ledger rows go too (and a selection still in flight records
2209
+ // nothing), or the shadow report would count the attempt.
2210
+ if (deps.lessons?.discard) {
2211
+ try {
2212
+ deps.lessons.discard(provisionalRoundId);
2213
+ } catch (discardError) {
2214
+ log(
2215
+ `dispatch: #${row.number} lesson record of the discarded round not dropped (ignored)`,
2216
+ discardError,
2217
+ );
2218
+ }
2219
+ }
2220
+ }
2221
+ }
2222
+ log(
2223
+ `dispatch: #${row.number} failed${retainedCommand ? " after durable handoff — round retained" : ""}`,
2224
+ err,
2225
+ );
2226
+ // No round owns the review: the PR stays queued for the next pump,
2227
+ // which paints it again when its round really starts.
2228
+ if (!retainedCommand && markedInProgress) {
2229
+ try {
2230
+ await deps.adapter.removeLabel(row.number, cfg.github.in_progress_label);
2231
+ } catch (labelError) {
2232
+ log(`dispatch: #${row.number} in-progress label not removed`, labelError);
2233
+ }
2234
+ }
2235
+ try {
2236
+ store.logEvent("dispatch_failed", row.number, {
2237
+ round_id: provisionalRoundId ?? null,
2238
+ retained_command: retainedCommand,
2239
+ error: String(err),
2240
+ });
2241
+ } catch (eventError) {
2242
+ log(`dispatch: #${row.number} failure event write failed`, eventError);
2243
+ }
2244
+ } finally {
2245
+ // A round that failed after its seed never leaves the case
2246
+ // writer held (idempotent: a closed window stays closed).
2247
+ endMemoryWindow();
2248
+ }
2249
+ }
2250
+ return dispatched;
2251
+ }