@quickchatai/quickreview 0.0.0-stage → 1.1.0

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