@quickchatai/quickreview 1.1.0 → 1.3.1

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 (471) hide show
  1. package/README.md +137 -57
  2. package/bin/qcr.js +1 -1
  3. package/config.example.toml +60 -14
  4. package/deploy/ansible/README.md +20 -12
  5. package/deploy/ansible/inventory.example.ini +1 -1
  6. package/deploy/collect-secrets.sh +1 -1
  7. package/deploy/qcr-update.sh +1 -1
  8. package/dist/extensions/cli/home.js +14 -0
  9. package/dist/extensions/cli/home.js.map +1 -0
  10. package/dist/extensions/cli/login-route.js +5 -0
  11. package/dist/extensions/cli/login-route.js.map +1 -0
  12. package/dist/extensions/cli/selfimprove.js +18 -0
  13. package/dist/extensions/cli/selfimprove.js.map +1 -0
  14. package/dist/extensions/home.js +350 -0
  15. package/dist/extensions/home.js.map +1 -0
  16. package/dist/extensions/login-route.js +180 -0
  17. package/dist/extensions/login-route.js.map +1 -0
  18. package/dist/extensions/onboarding.js +35 -11
  19. package/dist/extensions/onboarding.js.map +1 -1
  20. package/dist/extensions/selfimprove.js +49 -0
  21. package/dist/extensions/selfimprove.js.map +1 -0
  22. package/dist/extensions/setup-scan.js +23 -592
  23. package/dist/extensions/setup-scan.js.map +1 -1
  24. package/dist/extensions/setup.js +2 -47
  25. package/dist/extensions/setup.js.map +1 -1
  26. package/dist/src/archive/restore.js +18 -15
  27. package/dist/src/archive/restore.js.map +1 -1
  28. package/dist/src/botworker.js +13 -2
  29. package/dist/src/botworker.js.map +1 -1
  30. package/dist/src/channelfs.js +3 -26
  31. package/dist/src/channelfs.js.map +1 -1
  32. package/dist/src/ciretry.js +2 -1
  33. package/dist/src/ciretry.js.map +1 -1
  34. package/dist/src/cli.js +54 -61
  35. package/dist/src/cli.js.map +1 -1
  36. package/dist/src/cliops.js +3 -2
  37. package/dist/src/cliops.js.map +1 -1
  38. package/dist/src/commands.js +25 -2
  39. package/dist/src/commands.js.map +1 -1
  40. package/dist/src/config.js +32 -2
  41. package/dist/src/config.js.map +1 -1
  42. package/dist/src/daemon.js +50 -8
  43. package/dist/src/daemon.js.map +1 -1
  44. package/dist/src/debuground.js +1 -0
  45. package/dist/src/debuground.js.map +1 -1
  46. package/dist/src/debugroundpi.js +6 -1
  47. package/dist/src/debugroundpi.js.map +1 -1
  48. package/dist/src/dispatch.js +5 -4
  49. package/dist/src/dispatch.js.map +1 -1
  50. package/dist/src/engine/caserunner-sdk.js +15 -251
  51. package/dist/src/engine/caserunner-sdk.js.map +1 -1
  52. package/dist/src/engine/debugpi.js +17 -5
  53. package/dist/src/engine/debugpi.js.map +1 -1
  54. package/dist/src/execboundary.js +10 -3
  55. package/dist/src/execboundary.js.map +1 -1
  56. package/dist/src/github.js +51 -16
  57. package/dist/src/github.js.map +1 -1
  58. package/dist/src/githubtoken.js +346 -0
  59. package/dist/src/githubtoken.js.map +1 -0
  60. package/dist/src/home/adapters.js +349 -0
  61. package/dist/src/home/adapters.js.map +1 -0
  62. package/dist/src/home/advanced.js +318 -0
  63. package/dist/src/home/advanced.js.map +1 -0
  64. package/dist/src/home/advancedtools.js +57 -0
  65. package/dist/src/home/advancedtools.js.map +1 -0
  66. package/dist/src/home/autoreview.js +325 -0
  67. package/dist/src/home/autoreview.js.map +1 -0
  68. package/dist/src/home/daemonstatus.js +32 -0
  69. package/dist/src/home/daemonstatus.js.map +1 -0
  70. package/dist/src/home/devicelogin.js +61 -0
  71. package/dist/src/home/devicelogin.js.map +1 -0
  72. package/dist/src/home/launch.js +202 -0
  73. package/dist/src/home/launch.js.map +1 -0
  74. package/dist/src/home/loginowner.js +205 -0
  75. package/dist/src/home/loginowner.js.map +1 -0
  76. package/dist/src/home/memory.js +122 -0
  77. package/dist/src/home/memory.js.map +1 -0
  78. package/dist/src/home/model.js +811 -0
  79. package/dist/src/home/model.js.map +1 -0
  80. package/dist/src/home/panel.js +520 -0
  81. package/dist/src/home/panel.js.map +1 -0
  82. package/dist/src/home/ports.js +19 -0
  83. package/dist/src/home/ports.js.map +1 -0
  84. package/dist/src/home/review.js +163 -0
  85. package/dist/src/home/review.js.map +1 -0
  86. package/dist/src/home/reviews.js +478 -0
  87. package/dist/src/home/reviews.js.map +1 -0
  88. package/dist/src/home/setup.js +507 -0
  89. package/dist/src/home/setup.js.map +1 -0
  90. package/dist/src/home/setupadapters.js +214 -0
  91. package/dist/src/home/setupadapters.js.map +1 -0
  92. package/dist/src/home/setupcard.js +199 -0
  93. package/dist/src/home/setupcard.js.map +1 -0
  94. package/dist/src/home/setuptools.js +45 -0
  95. package/dist/src/home/setuptools.js.map +1 -0
  96. package/dist/src/home/spend.js +160 -0
  97. package/dist/src/home/spend.js.map +1 -0
  98. package/dist/src/home/switches.js +209 -0
  99. package/dist/src/home/switches.js.map +1 -0
  100. package/dist/src/home/switchon.js +439 -0
  101. package/dist/src/home/switchon.js.map +1 -0
  102. package/dist/src/home/toolnames.js +41 -0
  103. package/dist/src/home/toolnames.js.map +1 -0
  104. package/dist/src/home/tune.js +341 -0
  105. package/dist/src/home/tune.js.map +1 -0
  106. package/dist/src/home/words.js +7 -0
  107. package/dist/src/home/words.js.map +1 -0
  108. package/dist/src/homes.js +14 -0
  109. package/dist/src/homes.js.map +1 -1
  110. package/dist/src/install.js +1 -1
  111. package/dist/src/install.js.map +1 -1
  112. package/dist/src/ipc/workerspec.js +8 -0
  113. package/dist/src/ipc/workerspec.js.map +1 -1
  114. package/dist/src/live/publish.js +1 -1
  115. package/dist/src/live/publish.js.map +1 -1
  116. package/dist/src/livecli.js +1 -1
  117. package/dist/src/livecli.js.map +1 -1
  118. package/dist/src/loginhint.js +8 -3
  119. package/dist/src/loginhint.js.map +1 -1
  120. package/dist/src/loginrefresh/handover.js +121 -0
  121. package/dist/src/loginrefresh/handover.js.map +1 -0
  122. package/dist/src/loginrefresh/lock.js +54 -3
  123. package/dist/src/loginrefresh/lock.js.map +1 -1
  124. package/dist/src/loginrefresh/pirefresh.js +3 -0
  125. package/dist/src/loginrefresh/pirefresh.js.map +1 -1
  126. package/dist/src/loginrefresh/refresher.js +33 -5
  127. package/dist/src/loginrefresh/refresher.js.map +1 -1
  128. package/dist/src/loginrefresh/supervisor.js +18 -1
  129. package/dist/src/loginrefresh/supervisor.js.map +1 -1
  130. package/dist/src/npmverbs.js +46 -14
  131. package/dist/src/npmverbs.js.map +1 -1
  132. package/dist/src/onboard/choices.js +35 -4
  133. package/dist/src/onboard/choices.js.map +1 -1
  134. package/dist/src/onboard/engine.js +32 -306
  135. package/dist/src/onboard/engine.js.map +1 -1
  136. package/dist/src/onboard/pipin.js +22 -0
  137. package/dist/src/onboard/pipin.js.map +1 -0
  138. package/dist/src/onboard/stepforward.js +41 -0
  139. package/dist/src/onboard/stepforward.js.map +1 -0
  140. package/dist/src/onboard/steps/appslug.js +4 -1
  141. package/dist/src/onboard/steps/appslug.js.map +1 -1
  142. package/dist/src/onboard/steps/bringup.js +3 -2
  143. package/dist/src/onboard/steps/bringup.js.map +1 -1
  144. package/dist/src/onboard/steps/compose.js +29 -0
  145. package/dist/src/onboard/steps/compose.js.map +1 -1
  146. package/dist/src/onboard/steps/configfill.js +120 -39
  147. package/dist/src/onboard/steps/configfill.js.map +1 -1
  148. package/dist/src/onboard/steps/githubaccount.js +595 -0
  149. package/dist/src/onboard/steps/githubaccount.js.map +1 -0
  150. package/dist/src/onboard/steps/githubapp.js +65 -7
  151. package/dist/src/onboard/steps/githubapp.js.map +1 -1
  152. package/dist/src/onboard/steps/golive.js +2 -0
  153. package/dist/src/onboard/steps/golive.js.map +1 -1
  154. package/dist/src/onboard/steps/index.js +11 -0
  155. package/dist/src/onboard/steps/index.js.map +1 -1
  156. package/dist/src/onboard/steps/ingress.js +80 -25
  157. package/dist/src/onboard/steps/ingress.js.map +1 -1
  158. package/dist/src/onboard/steps/llmauth.js +103 -30
  159. package/dist/src/onboard/steps/llmauth.js.map +1 -1
  160. package/dist/src/onboard/steps/loginshare.js +213 -0
  161. package/dist/src/onboard/steps/loginshare.js.map +1 -0
  162. package/dist/src/onboard/steps/memory.js +67 -3
  163. package/dist/src/onboard/steps/memory.js.map +1 -1
  164. package/dist/src/onboard/steps/proof.js +366 -56
  165. package/dist/src/onboard/steps/proof.js.map +1 -1
  166. package/dist/src/onboard/steps/repoconfig.js +72 -0
  167. package/dist/src/onboard/steps/repoconfig.js.map +1 -0
  168. package/dist/src/onboard/steps/topology.js +6 -6
  169. package/dist/src/onboard/steps/topology.js.map +1 -1
  170. package/dist/src/onboard/steps/transport.js +9 -1
  171. package/dist/src/onboard/steps/transport.js.map +1 -1
  172. package/dist/src/onboard/steps/tuiroute.js +102 -0
  173. package/dist/src/onboard/steps/tuiroute.js.map +1 -0
  174. package/dist/src/onboard/steps/verifyup.js +17 -1
  175. package/dist/src/onboard/steps/verifyup.js.map +1 -1
  176. package/dist/src/onboard/toolnames.js +30 -0
  177. package/dist/src/onboard/toolnames.js.map +1 -0
  178. package/dist/src/operatorwrite.js +23 -0
  179. package/dist/src/operatorwrite.js.map +1 -0
  180. package/dist/src/outcomes.js +4 -1
  181. package/dist/src/outcomes.js.map +1 -1
  182. package/dist/src/piagentdir.js +13 -0
  183. package/dist/src/piagentdir.js.map +1 -0
  184. package/dist/src/pilogin.js +31 -16
  185. package/dist/src/pilogin.js.map +1 -1
  186. package/dist/src/posting-journal.js +2 -1
  187. package/dist/src/posting-journal.js.map +1 -1
  188. package/dist/src/posting-outbox.js +6 -2
  189. package/dist/src/posting-outbox.js.map +1 -1
  190. package/dist/src/posting-receipt.js +14 -1
  191. package/dist/src/posting-receipt.js.map +1 -1
  192. package/dist/src/qcrbin.js +16 -11
  193. package/dist/src/qcrbin.js.map +1 -1
  194. package/dist/src/recheck.js +2 -1
  195. package/dist/src/recheck.js.map +1 -1
  196. package/dist/src/reconcile.js +22 -2
  197. package/dist/src/reconcile.js.map +1 -1
  198. package/dist/src/reviewargs.js +122 -0
  199. package/dist/src/reviewargs.js.map +1 -0
  200. package/dist/src/reviewcli.js +7 -161
  201. package/dist/src/reviewcli.js.map +1 -1
  202. package/dist/src/reviewdetach.js +76 -11
  203. package/dist/src/reviewdetach.js.map +1 -1
  204. package/dist/src/reviewhistory.js +32 -6
  205. package/dist/src/reviewhistory.js.map +1 -1
  206. package/dist/src/reviewnow.js +45 -0
  207. package/dist/src/reviewnow.js.map +1 -0
  208. package/dist/src/scancli.js +39 -691
  209. package/dist/src/scancli.js.map +1 -1
  210. package/dist/src/selfimprove.js +66 -0
  211. package/dist/src/selfimprove.js.map +1 -0
  212. package/dist/src/setup/adapters.js +5 -223
  213. package/dist/src/setup/adapters.js.map +1 -1
  214. package/dist/src/setup/caserunner.js +15 -249
  215. package/dist/src/setup/caserunner.js.map +1 -1
  216. package/dist/src/setup/engine.js +32 -341
  217. package/dist/src/setup/engine.js.map +1 -1
  218. package/dist/src/setup/facts.js +3 -592
  219. package/dist/src/setup/facts.js.map +1 -1
  220. package/dist/src/setup/findings.js +3 -947
  221. package/dist/src/setup/findings.js.map +1 -1
  222. package/dist/src/setup/presentation.js +3 -8
  223. package/dist/src/setup/presentation.js.map +1 -1
  224. package/dist/src/setup/scan.js +3 -724
  225. package/dist/src/setup/scan.js.map +1 -1
  226. package/dist/src/setup/scanbuild.js +9 -92
  227. package/dist/src/setup/scanbuild.js.map +1 -1
  228. package/dist/src/setup/scanlease.js +3 -250
  229. package/dist/src/setup/scanlease.js.map +1 -1
  230. package/dist/src/setup/scanreport.js +3 -288
  231. package/dist/src/setup/scanreport.js.map +1 -1
  232. package/dist/src/setup/scanrun.js +3 -1732
  233. package/dist/src/setup/scanrun.js.map +1 -1
  234. package/dist/src/setup/scantree.js +3 -256
  235. package/dist/src/setup/scantree.js.map +1 -1
  236. package/dist/src/setup/stage2request.js +3 -258
  237. package/dist/src/setup/stage2request.js.map +1 -1
  238. package/dist/src/setup/unitcut.js +3 -436
  239. package/dist/src/setup/unitcut.js.map +1 -1
  240. package/dist/src/state.js +2 -0
  241. package/dist/src/state.js.map +1 -1
  242. package/dist/src/statemachine.js +4 -1
  243. package/dist/src/statemachine.js.map +1 -1
  244. package/dist/src/takeover.js +1 -0
  245. package/dist/src/takeover.js.map +1 -1
  246. package/dist/src/takeoverpi.js +15 -1
  247. package/dist/src/takeoverpi.js.map +1 -1
  248. package/dist/src/tui/app.js +49 -0
  249. package/dist/src/tui/app.js.map +1 -1
  250. package/dist/src/tui/board.js +88 -12
  251. package/dist/src/tui/board.js.map +1 -1
  252. package/dist/src/tui/cmdbar.js +4 -1
  253. package/dist/src/tui/cmdbar.js.map +1 -1
  254. package/dist/src/tui/companion.js +76 -11
  255. package/dist/src/tui/companion.js.map +1 -1
  256. package/dist/src/tui/connect.js +7 -1
  257. package/dist/src/tui/connect.js.map +1 -1
  258. package/dist/src/tui/deploytab.js +2 -2
  259. package/dist/src/tui/deploytab.js.map +1 -1
  260. package/dist/src/tui/filters.js +5 -2
  261. package/dist/src/tui/filters.js.map +1 -1
  262. package/dist/src/tui/ghread.js +61 -5
  263. package/dist/src/tui/ghread.js.map +1 -1
  264. package/dist/src/tui/index.js +109 -20
  265. package/dist/src/tui/index.js.map +1 -1
  266. package/dist/src/tui/memforward.js +39 -2
  267. package/dist/src/tui/memforward.js.map +1 -1
  268. package/dist/src/tui/memory.js +3 -16
  269. package/dist/src/tui/memory.js.map +1 -1
  270. package/dist/src/tui/memoryurl.js +22 -0
  271. package/dist/src/tui/memoryurl.js.map +1 -0
  272. package/dist/src/tui/model.js +25 -0
  273. package/dist/src/tui/model.js.map +1 -1
  274. package/dist/src/tui/prpause.js +7 -0
  275. package/dist/src/tui/prpause.js.map +1 -1
  276. package/dist/src/tui/remote.js +7 -1
  277. package/dist/src/tui/remote.js.map +1 -1
  278. package/dist/src/tui/route.js +143 -0
  279. package/dist/src/tui/route.js.map +1 -0
  280. package/dist/src/tui/setupstate.js +32 -0
  281. package/dist/src/tui/setupstate.js.map +1 -0
  282. package/dist/src/tui/sshtriage.js +102 -5
  283. package/dist/src/tui/sshtriage.js.map +1 -1
  284. package/dist/src/tui/theme.js +2 -1
  285. package/dist/src/tui/theme.js.map +1 -1
  286. package/dist/src/tui/unwedge.js +2 -12
  287. package/dist/src/tui/unwedge.js.map +1 -1
  288. package/dist/src/verdicts.js +17 -5
  289. package/dist/src/verdicts.js.map +1 -1
  290. package/dist/src/version.js +1 -1
  291. package/dist/src/version.js.map +1 -1
  292. package/dist/src/webhook.js +12 -1
  293. package/dist/src/webhook.js.map +1 -1
  294. package/dist/src/wire.js +34 -8
  295. package/dist/src/wire.js.map +1 -1
  296. package/dist/src/workerworld.js +11 -1
  297. package/dist/src/workerworld.js.map +1 -1
  298. package/docker/README.md +1 -1
  299. package/docker/companion/entrypoint.sh +20 -1
  300. package/docker/entrypoint.sh +57 -6
  301. package/docker-compose.yml +16 -1
  302. package/extensions/cli/home.ts +14 -0
  303. package/extensions/cli/login-route.ts +10 -0
  304. package/extensions/cli/selfimprove.ts +18 -0
  305. package/extensions/home.ts +484 -0
  306. package/extensions/login-route.ts +220 -0
  307. package/extensions/onboarding.ts +41 -11
  308. package/extensions/selfimprove.ts +65 -0
  309. package/extensions/setup-scan.ts +79 -766
  310. package/extensions/setup.ts +14 -70
  311. package/package.json +6 -4
  312. package/prompts/onboarding.md +58 -18
  313. package/schemas/review-result.v1.json +60 -0
  314. package/scripts/deploy.sh +8 -6
  315. package/scripts/mint-token.sh +25 -4
  316. package/scripts/qcr-host.sh +28 -5
  317. package/scripts/vm-ghcr-login.sh +7 -4
  318. package/src/archive/restore.ts +18 -21
  319. package/src/botworker.ts +13 -2
  320. package/src/channelfs.ts +3 -26
  321. package/src/ciretry.ts +2 -1
  322. package/src/cli.ts +59 -67
  323. package/src/cliops.ts +3 -2
  324. package/src/commands.ts +28 -2
  325. package/src/config.ts +62 -4
  326. package/src/daemon.ts +58 -7
  327. package/src/debuground.ts +3 -0
  328. package/src/debugroundpi.ts +12 -1
  329. package/src/dispatch.ts +5 -4
  330. package/src/engine/caserunner-sdk.ts +29 -314
  331. package/src/engine/debugpi.ts +17 -5
  332. package/src/execboundary.ts +16 -4
  333. package/src/github.ts +57 -15
  334. package/src/githubtoken.ts +435 -0
  335. package/src/home/adapters.ts +418 -0
  336. package/src/home/advanced.ts +421 -0
  337. package/src/home/advancedtools.ts +96 -0
  338. package/src/home/autoreview.ts +385 -0
  339. package/src/home/daemonstatus.ts +55 -0
  340. package/src/home/devicelogin.ts +107 -0
  341. package/src/home/launch.ts +257 -0
  342. package/src/home/loginowner.ts +248 -0
  343. package/src/home/memory.ts +176 -0
  344. package/src/home/model.ts +965 -0
  345. package/src/home/panel.ts +666 -0
  346. package/src/home/ports.ts +338 -0
  347. package/src/home/review.ts +206 -0
  348. package/src/home/reviews.ts +565 -0
  349. package/src/home/setup.ts +689 -0
  350. package/src/home/setupadapters.ts +217 -0
  351. package/src/home/setupcard.ts +288 -0
  352. package/src/home/setuptools.ts +57 -0
  353. package/src/home/spend.ts +194 -0
  354. package/src/home/switches.ts +293 -0
  355. package/src/home/switchon.ts +554 -0
  356. package/src/home/toolnames.ts +46 -0
  357. package/src/home/tune.ts +405 -0
  358. package/src/home/words.ts +8 -0
  359. package/src/homes.ts +17 -0
  360. package/src/install.ts +1 -1
  361. package/src/ipc/workerspec.ts +15 -0
  362. package/src/live/publish.ts +4 -1
  363. package/src/livecli.ts +6 -1
  364. package/src/loginhint.ts +8 -3
  365. package/src/loginrefresh/handover.ts +138 -0
  366. package/src/loginrefresh/lock.ts +72 -3
  367. package/src/loginrefresh/pirefresh.ts +3 -0
  368. package/src/loginrefresh/refresher.ts +34 -5
  369. package/src/loginrefresh/supervisor.ts +25 -2
  370. package/src/npmverbs.ts +55 -13
  371. package/src/onboard/choices.ts +40 -4
  372. package/src/onboard/engine.ts +70 -421
  373. package/src/onboard/pipin.ts +31 -0
  374. package/src/onboard/stepforward.ts +61 -0
  375. package/src/onboard/steps/appslug.ts +4 -1
  376. package/src/onboard/steps/bringup.ts +3 -2
  377. package/src/onboard/steps/compose.ts +31 -0
  378. package/src/onboard/steps/configfill.ts +144 -40
  379. package/src/onboard/steps/githubaccount.ts +716 -0
  380. package/src/onboard/steps/githubapp.ts +92 -8
  381. package/src/onboard/steps/golive.ts +2 -0
  382. package/src/onboard/steps/index.ts +11 -0
  383. package/src/onboard/steps/ingress.ts +98 -31
  384. package/src/onboard/steps/llmauth.ts +114 -29
  385. package/src/onboard/steps/loginshare.ts +258 -0
  386. package/src/onboard/steps/memory.ts +76 -3
  387. package/src/onboard/steps/proof.ts +473 -55
  388. package/src/onboard/steps/repoconfig.ts +85 -0
  389. package/src/onboard/steps/topology.ts +7 -6
  390. package/src/onboard/steps/transport.ts +15 -0
  391. package/src/onboard/steps/tuiroute.ts +119 -0
  392. package/src/onboard/steps/verifyup.ts +20 -1
  393. package/src/onboard/toolnames.ts +34 -0
  394. package/src/operatorwrite.ts +25 -0
  395. package/src/outcomes.ts +8 -1
  396. package/src/piagentdir.ts +12 -0
  397. package/src/pilogin.ts +32 -16
  398. package/src/posting-journal.ts +6 -1
  399. package/src/posting-outbox.ts +10 -2
  400. package/src/posting-receipt.ts +17 -0
  401. package/src/qcrbin.ts +16 -11
  402. package/src/recheck.ts +2 -1
  403. package/src/reconcile.ts +29 -3
  404. package/src/reviewargs.ts +119 -0
  405. package/src/reviewcli.ts +6 -167
  406. package/src/reviewdetach.ts +107 -13
  407. package/src/reviewhistory.ts +20 -6
  408. package/src/reviewnow.ts +76 -0
  409. package/src/scancli.ts +75 -826
  410. package/src/selfimprove.ts +90 -0
  411. package/src/setup/adapters.ts +13 -221
  412. package/src/setup/caserunner.ts +34 -273
  413. package/src/setup/engine.ts +62 -496
  414. package/src/setup/facts.ts +3 -627
  415. package/src/setup/findings.ts +3 -1264
  416. package/src/setup/presentation.ts +3 -18
  417. package/src/setup/scan.ts +3 -1033
  418. package/src/setup/scanbuild.ts +20 -114
  419. package/src/setup/scanlease.ts +3 -297
  420. package/src/setup/scanreport.ts +3 -373
  421. package/src/setup/scanrun.ts +3 -2268
  422. package/src/setup/scantree.ts +3 -259
  423. package/src/setup/stage2request.ts +3 -347
  424. package/src/setup/unitcut.ts +3 -523
  425. package/src/state.ts +43 -0
  426. package/src/statemachine.ts +4 -1
  427. package/src/takeover.ts +5 -0
  428. package/src/takeoverpi.ts +16 -2
  429. package/src/tui/app.ts +47 -0
  430. package/src/tui/board.ts +111 -14
  431. package/src/tui/cmdbar.ts +4 -1
  432. package/src/tui/companion.ts +80 -11
  433. package/src/tui/connect.ts +7 -1
  434. package/src/tui/deploytab.ts +2 -2
  435. package/src/tui/filters.ts +7 -3
  436. package/src/tui/ghread.ts +74 -7
  437. package/src/tui/index.ts +117 -23
  438. package/src/tui/memforward.ts +52 -3
  439. package/src/tui/memory.ts +3 -15
  440. package/src/tui/memoryurl.ts +22 -0
  441. package/src/tui/model.ts +36 -2
  442. package/src/tui/prpause.ts +11 -2
  443. package/src/tui/remote.ts +9 -1
  444. package/src/tui/route.ts +173 -0
  445. package/src/tui/setupstate.ts +56 -0
  446. package/src/tui/sshtriage.ts +127 -6
  447. package/src/tui/theme.ts +2 -1
  448. package/src/tui/unwedge.ts +2 -14
  449. package/src/verdicts.ts +23 -5
  450. package/src/version.ts +1 -1
  451. package/src/webhook.ts +10 -1
  452. package/src/wire.ts +42 -9
  453. package/src/workerworld.ts +16 -2
  454. package/dist/src/live/projection.js +0 -450
  455. package/dist/src/live/projection.js.map +0 -1
  456. package/dist/src/live/read.js +0 -60
  457. package/dist/src/live/read.js.map +0 -1
  458. package/dist/src/onboard/launch.js +0 -83
  459. package/dist/src/onboard/launch.js.map +0 -1
  460. package/dist/src/setup/ledger.js +0 -207
  461. package/dist/src/setup/ledger.js.map +0 -1
  462. package/dist/src/setup/stage2.js +0 -617
  463. package/dist/src/setup/stage2.js.map +0 -1
  464. package/dist/src/setup/textsniff.js +0 -89
  465. package/dist/src/setup/textsniff.js.map +0 -1
  466. package/src/live/projection.ts +0 -491
  467. package/src/live/read.ts +0 -86
  468. package/src/onboard/launch.ts +0 -115
  469. package/src/setup/ledger.ts +0 -324
  470. package/src/setup/stage2.ts +0 -926
  471. package/src/setup/textsniff.ts +0 -95
@@ -4,14 +4,23 @@
4
4
  // the review it produced before anything could ever reach GitHub.
5
5
  //
6
6
  // The shape:
7
- // 1. ask for a real PR of theirs with the trigger label on it (the wizard
8
- // never creates a demo PR);
9
- // 2. follow the round to completion THROUGH THE STATE STORE — the daemon's
7
+ // 1. ask for one real, open PR (a number or its URL; the five most recently
8
+ // updated open PRs of the repo are offered, the person's own included —
9
+ // on the own-account route those are the main target). The wizard never
10
+ // creates a demo PR;
11
+ // 2. start its review IMMEDIATELY: one `review-now` command into the
12
+ // daemon's root-sealed operator channel (src/commands.ts, the channel
13
+ // `qcr drain` and the TUI's levers use), which queues the PR exactly as a
14
+ // fresh trigger label would — with no GitHub write and no label for the
15
+ // person to add. A daemon that cannot take the command (an older image)
16
+ // falls back to the label, said plainly;
17
+ // 3. follow the round to completion THROUGH THE STATE STORE — the daemon's
10
18
  // own record, read with the daemon's own `qcr why --json` inside the
11
19
  // container, so the wizard is reading exactly what the operator would;
12
- // 3. SHOW the finished review, read out of the dry-run record — the file the
13
- // mutation gate wrote INSTEAD of calling GitHub. The operator judges the
14
- // output while it is still impossible for it to have posted.
20
+ // 4. SHOW the finished review, read out of the dry-run record — the file the
21
+ // mutation gate wrote INSTEAD of calling GitHub: the verdict, how many
22
+ // findings, the top ones, and where the full text is. The operator judges
23
+ // the output while it is still impossible for it to have posted.
15
24
  //
16
25
  // Two paths get real guidance rather than a spinner, because they are the two
17
26
  // that actually happen: the round never starts (label spelling, webhook not
@@ -19,11 +28,16 @@
19
28
  // is cancelled (the daemon's own event log says why). Neither ever hangs: the
20
29
  // poll is bounded and every attempt logs where it got to.
21
30
  //
22
- // Nothing here writes anything anywhere. It reads the state store, reads the
23
- // dry-run directory, and records what it saw on the checklist — the record the
24
- // go-live step later demands before it will even offer the flip.
31
+ // The one thing it writes is that operator command file. Otherwise it reads the
32
+ // state store, the dry-run directory and (to offer PRs) GitHub, and records
33
+ // what it saw on the checklist — the record the go-live step later demands
34
+ // before it will even offer the flip.
35
+ import { join } from "node:path";
36
+ import { mintAppJwt, USER_TOKEN_FILE } from "@quickchatai/pi-quickreview/src/githubAuth.js";
25
37
  import { loadConfig } from "../../config.js";
26
- import { CHOICE_KEYS } from "../choices.js";
38
+ import { operatorCommandWriteScriptAt } from "../../operatorwrite.js";
39
+ import { CHOICE_KEYS, userAuthRecorded } from "../choices.js";
40
+ import { readTextOrNull } from "../engine.js";
27
41
  import type {
28
42
  CheckResult,
29
43
  OnboardAdapters,
@@ -37,12 +51,30 @@ import {
37
51
  type DryrunRecord,
38
52
  type ReviewSnapshot,
39
53
  } from "../../compare.js";
40
- import { CONTAINER_DRYRUN_DIR, configPath, readConfigText } from "./compose.js";
41
- import { readTomlKey } from "./ingress.js";
42
- import { DEFAULT_LABELS } from "./githubapp.js";
54
+ import {
55
+ CONTAINER_COMMANDS_DIR,
56
+ CONTAINER_DRYRUN_DIR,
57
+ configPath,
58
+ readConfigText,
59
+ } from "./compose.js";
60
+ import { readTomlKey, secretsDirFor } from "./ingress.js";
61
+ import {
62
+ DEFAULT_LABELS,
63
+ api,
64
+ findInstallation,
65
+ installationToken,
66
+ parseRepoSlug,
67
+ readCredentials,
68
+ type RepoSlug,
69
+ } from "./githubapp.js";
43
70
  import { stackTransportFor, type StackTransport, type TransportOptions } from "./transport.js";
44
71
 
45
72
  export const PROOF_STEP_ID = "proof_round";
73
+
74
+ /** A string input or choice, else "" — never an object's stringification. */
75
+ function textOf(v: unknown): string {
76
+ return typeof v === "string" ? v : "";
77
+ }
46
78
  /** The completed-proof record. The go-live step will not offer the flip
47
79
  * without it AND a state-store re-verification of it. */
48
80
  export const PROOF_CHOICE_KEY = CHOICE_KEYS.proof;
@@ -238,14 +270,58 @@ export function recordForPr(r: DryrunRecord, pr: number): boolean {
238
270
  return r.pr === pr || new RegExp(`/(pulls|issues)/${pr}(/|$)`).test(r.path);
239
271
  }
240
272
 
273
+ /** How many findings the summary lists before "and N more". */
274
+ export const TOP_FINDINGS = 5;
275
+
276
+ export interface ReviewRenderExtras {
277
+ /** The round's verdict on the merits (the review EVENT can be COMMENT on
278
+ * the own-account route, where the verdict is stated in the body). */
279
+ verdict?: string | null;
280
+ /** Where the full record is, and the command that prints it. */
281
+ file?: string;
282
+ readCommand?: string;
283
+ }
284
+
285
+ /** The first line of a comment, trimmed to one readable line. */
286
+ function headline(body: string): string {
287
+ const line =
288
+ body
289
+ .trim()
290
+ .split("\n")
291
+ .find((l) => l.trim() !== "") ?? "";
292
+ const flat = line.trim();
293
+ return flat.length > 160 ? `${flat.slice(0, 157)}…` : flat;
294
+ }
295
+
241
296
  /** The would-be review, rendered for a human to judge. */
242
- export function renderReview(review: ReviewSnapshot, pr: number): string {
297
+ export function renderReview(
298
+ review: ReviewSnapshot,
299
+ pr: number,
300
+ extras: ReviewRenderExtras = {},
301
+ ): string {
302
+ const top = review.comments
303
+ .slice(0, TOP_FINDINGS)
304
+ .map((c, i) => ` ${i + 1}. ${c.path}:${c.line} — ${headline(c.body)}`);
305
+ const more = review.comments.length - top.length;
243
306
  const head = [
244
307
  `This is the review your reviewer produced for #${pr}. It was NOT posted: dry_run is on, so the`,
245
308
  `mutation gate wrote it to state/dryrun/ instead of calling GitHub.`,
246
309
  "",
247
310
  `Verdict it would have submitted: ${review.state}`,
311
+ ...(extras.verdict && extras.verdict !== review.state
312
+ ? [
313
+ `Verdict on the merits: ${extras.verdict} (stated in the review body; GitHub gets it as a ${review.state} review)`,
314
+ ]
315
+ : []),
248
316
  `Inline comments: ${review.comments.length}`,
317
+ ...(top.length > 0
318
+ ? ["Top findings:", ...top, ...(more > 0 ? [` … and ${more} more, below`] : [])]
319
+ : []),
320
+ ...(extras.file
321
+ ? [
322
+ `Full text: state/dryrun/${extras.file} in the daemon's state${extras.readCommand ? ` — \`${extras.readCommand}\`` : ""}`,
323
+ ]
324
+ : []),
249
325
  "",
250
326
  "--- review body ---",
251
327
  review.body.trim() === "" ? "(empty body)" : review.body.trim(),
@@ -268,8 +344,18 @@ export interface ProofStepOptions {
268
344
  * `stackTransportFor` decides it from the recorded topology, so the proof
269
345
  * round is the same act on a laptop and on a server. */
270
346
  transport?: TransportOptions;
347
+ /** GitHub's API base (the open-PR offer). */
348
+ apiBase?: string;
349
+ /** Secrets dir for the offer's read token; default: the one resolution. */
350
+ secretsDir?: string;
351
+ /** How many times the review-now result file is read before giving up. */
352
+ resultAttempts?: number;
353
+ resultIntervalMs?: number;
271
354
  }
272
355
 
356
+ /** How many open PRs the offer lists. */
357
+ export const PR_OFFER_SIZE = 5;
358
+
273
359
  function triggerLabelOf(adapters: OnboardAdapters): string {
274
360
  const text = readConfigText(adapters);
275
361
  const configured = text ? readTomlKey(text, "github", "trigger_label") : null;
@@ -283,22 +369,126 @@ function repoOf(choices: Readonly<Record<string, unknown>>, adapters: OnboardAda
283
369
  return (text ? readTomlKey(text, "github", "repo") : null) ?? "your repository";
284
370
  }
285
371
 
372
+ /** A PR answer: 17, "17", "#17", or the PR's URL (github.com/o/r/pull/17).
373
+ * A URL names its repo too, which the caller checks against the reviewed one. */
374
+ export function parsePrAnswer(raw: unknown): { pr: number; repo: string | null } | null {
375
+ if (typeof raw === "number")
376
+ return Number.isInteger(raw) && raw > 0 ? { pr: raw, repo: null } : null;
377
+ if (typeof raw !== "string") return null;
378
+ const v = raw.trim();
379
+ const url =
380
+ /^(?:https?:\/\/)?(?:www\.)?github\.com\/([A-Za-z0-9._-]+\/[A-Za-z0-9._-]+)\/pull\/(\d+)(?:[/?#].*)?$/.exec(
381
+ v,
382
+ );
383
+ if (url) return { pr: Number(url[2]), repo: url[1]! };
384
+ const n = /^#?(\d+)$/.exec(v);
385
+ return n && Number(n[1]) > 0 ? { pr: Number(n[1]), repo: null } : null;
386
+ }
387
+
286
388
  function prFrom(
287
389
  input: Readonly<Record<string, unknown>>,
288
390
  choices: Readonly<Record<string, unknown>>,
289
- ): number | null {
290
- for (const raw of [input.pr, parseProofRecord(choices[PROOF_CHOICE_KEY])?.pr]) {
291
- const n = typeof raw === "string" ? Number(raw.trim()) : raw;
292
- if (typeof n === "number" && Number.isInteger(n) && n > 0) return n;
391
+ ): { pr: number; repo: string | null } | null {
392
+ for (const raw of [input.pr, input.url, parseProofRecord(choices[PROOF_CHOICE_KEY])?.pr]) {
393
+ const parsed = parsePrAnswer(raw);
394
+ if (parsed) return parsed;
293
395
  }
294
396
  return null;
295
397
  }
296
398
 
399
+ /** A command file name the daemon's watcher serves (`*.json`), unique per call. */
400
+ export function reviewNowFileName(now: Date, pr: number): string {
401
+ return `${now.toISOString().replace(/[:.]/g, "-")}-onboard-reviewnow-${pr}.json`;
402
+ }
403
+
404
+ /** The argv that writes one `review-now` command into the root-sealed
405
+ * operator directory, through the same script the TUI's levers use. The
406
+ * payload is built here from a number, so it can carry no quote. */
407
+ export function reviewNowWriteArgv(name: string, pr: number): string[] {
408
+ if (!Number.isInteger(pr) || pr <= 0) throw new Error(`not a PR number: ${pr}`);
409
+ const payload = JSON.stringify({ command: "review-now", pr });
410
+ return [
411
+ "sh",
412
+ "-c",
413
+ `printf '%s\\n' '${payload}' | ( ${operatorCommandWriteScriptAt(CONTAINER_COMMANDS_DIR, name)} )`,
414
+ ];
415
+ }
416
+
417
+ /** The argv that prints the daemon's result for that command, or `pending`. */
418
+ export function reviewNowResultArgv(name: string): string[] {
419
+ const file = `${CONTAINER_COMMANDS_DIR}/results/${name.replace(/\.json$/, "")}.result.json`;
420
+ return ["sh", "-c", `if [ -f '${file}' ]; then cat '${file}'; else echo pending; fi`];
421
+ }
422
+
423
+ export interface OpenPr {
424
+ number: number;
425
+ title: string;
426
+ author: string;
427
+ updated_at: string;
428
+ draft: boolean;
429
+ }
430
+
431
+ /** A token that can read the reviewed repo: the person's token file on the
432
+ * own-account route, an App installation token on the App route; null when
433
+ * neither is to hand (the offer is then skipped, never guessed). */
434
+ async function readToken(
435
+ ctx: StepContext,
436
+ repo: RepoSlug,
437
+ apiBase: string,
438
+ secretsDir: string,
439
+ ): Promise<string | null> {
440
+ if (userAuthRecorded(ctx.choices)) {
441
+ const token = (readTextOrNull(join(secretsDir, USER_TOKEN_FILE)) ?? "").trim();
442
+ return token === "" ? null : token;
443
+ }
444
+ const creds = readCredentials(secretsDir);
445
+ if (!creds.complete) return null;
446
+ try {
447
+ const jwt = mintAppJwt(creds.appId!, creds.pem!, () => ctx.adapters.clock().getTime());
448
+ const found = await findInstallation(ctx.adapters.fetch, apiBase, jwt, repo.owner);
449
+ if (found.status !== "found") return null;
450
+ const minted = await installationToken(ctx.adapters.fetch, apiBase, jwt, found.id);
451
+ return "token" in minted ? minted.token : null;
452
+ } catch {
453
+ return null;
454
+ }
455
+ }
456
+
457
+ /** The repo's most recently updated open PRs (any author — the person's own
458
+ * included). Empty when GitHub cannot be read. */
459
+ export async function listOpenPrs(
460
+ fetchFn: typeof fetch,
461
+ apiBase: string,
462
+ token: string,
463
+ repo: RepoSlug,
464
+ limit = PR_OFFER_SIZE,
465
+ ): Promise<OpenPr[]> {
466
+ const r = await api(
467
+ fetchFn,
468
+ `${apiBase}/repos/${repo.owner}/${repo.name}/pulls?state=open&sort=updated&direction=desc&per_page=${limit}`,
469
+ { headers: { authorization: `Bearer ${token}` } },
470
+ );
471
+ if (r.status !== 200 || !Array.isArray(r.body)) return [];
472
+ return (r.body as Array<Record<string, unknown>>)
473
+ .filter((p) => typeof p.number === "number")
474
+ .slice(0, limit)
475
+ .map((p) => ({
476
+ number: p.number as number,
477
+ title: typeof p.title === "string" ? p.title : "",
478
+ author: textOf((p.user as { login?: unknown } | null)?.login),
479
+ updated_at: typeof p.updated_at === "string" ? p.updated_at : "",
480
+ draft: p.draft === true,
481
+ }));
482
+ }
483
+
297
484
  export function proofRoundStep(opts: ProofStepOptions = {}): OnboardingStep {
298
485
  const pollAttempts = opts.pollAttempts ?? 60;
299
486
  const pollIntervalMs = opts.pollIntervalMs ?? 30_000;
300
487
  const sleep = opts.sleep ?? ((ms: number) => new Promise<void>((r) => setTimeout(r, ms)));
301
488
  const dryrunLimit = opts.dryrunLimit ?? 40;
489
+ const apiBase = opts.apiBase ?? "https://api.github.com";
490
+ const resultAttempts = opts.resultAttempts ?? 15;
491
+ const resultIntervalMs = opts.resultIntervalMs ?? 2_000;
302
492
 
303
493
  function log(ctx: StepContext, line: string): void {
304
494
  ctx.adapters.log?.(`${PROOF_STEP_ID}: ${line}`);
@@ -322,23 +512,132 @@ export function proofRoundStep(opts: ProofStepOptions = {}): OnboardingStep {
322
512
  : { obs: null, detail: `\`qcr why\` did not answer JSON: ${r.stdout.slice(0, 200)}` };
323
513
  }
324
514
 
515
+ /** Send `review-now` for `pr` and wait for the daemon's receipt. `refused`
516
+ * means the daemon read the command and said no (its reason is the answer);
517
+ * otherwise a failure is the channel itself (write or no receipt). */
518
+ async function requestReview(
519
+ ctx: StepContext,
520
+ transport: StackTransport,
521
+ pr: number,
522
+ ): Promise<{ ok: boolean; refused: boolean; detail: string }> {
523
+ const name = reviewNowFileName(ctx.adapters.clock(), pr);
524
+ const write = await transport.execAsRoot(reviewNowWriteArgv(name, pr));
525
+ if (write.code !== 0)
526
+ return {
527
+ ok: false,
528
+ refused: false,
529
+ detail: `the command write failed: ${(write.stderr || write.stdout).trim().slice(0, 200) || `exit ${write.code}`}`,
530
+ };
531
+ for (let attempt = 1; attempt <= resultAttempts; attempt += 1) {
532
+ await sleep(resultIntervalMs);
533
+ const read = await transport.exec(reviewNowResultArgv(name));
534
+ const body = read.code === 0 ? read.stdout.trim() : "";
535
+ if (body === "" || body === "pending") continue;
536
+ let v: Record<string, unknown>;
537
+ try {
538
+ v = JSON.parse(body) as Record<string, unknown>;
539
+ } catch {
540
+ continue; // the daemon may still be writing its receipt
541
+ }
542
+ if (v.ok === true)
543
+ return {
544
+ ok: true,
545
+ refused: false,
546
+ detail: `queued${typeof v.state === "string" ? `, state ${v.state}` : ""}`,
547
+ };
548
+ return {
549
+ ok: false,
550
+ refused: true,
551
+ detail:
552
+ typeof v.reason === "string"
553
+ ? v.reason.replace(/\s+/g, " ").slice(0, 300)
554
+ : "no reason given",
555
+ };
556
+ }
557
+ return {
558
+ ok: false,
559
+ refused: false,
560
+ detail: `no answer to ${name} after ${resultAttempts} reads — the daemon may still be starting`,
561
+ };
562
+ }
563
+
564
+ /** No PR yet: offer the repo's open PRs, and say what happens next. */
565
+ async function askCheck(
566
+ ctx: StepContext,
567
+ repo: string,
568
+ label: string,
569
+ posture: string | null,
570
+ typed: boolean,
571
+ ): Promise<CheckResult> {
572
+ const slug = parseRepoSlug(repo);
573
+ let offer: OpenPr[] = [];
574
+ if (slug) {
575
+ const token = await readToken(
576
+ ctx,
577
+ slug,
578
+ apiBase,
579
+ opts.secretsDir ?? secretsDirFor(ctx.adapters, ctx.choices),
580
+ );
581
+ if (token) offer = await listOpenPrs(ctx.adapters.fetch, apiBase, token, slug);
582
+ }
583
+ const me = textOf(ctx.choices[CHOICE_KEYS.accountLogin]);
584
+ const rows = offer.map(
585
+ (p) =>
586
+ ` #${p.number} ${p.title}${p.draft ? " (draft)" : ""} — by ${p.author}${me !== "" && p.author === me ? " (yours)" : ""}, updated ${p.updated_at}`,
587
+ );
588
+ const assurance =
589
+ posture === "true"
590
+ ? "Nothing will be posted to GitHub: dry_run is on, so every would-be comment and review is written to a file instead, and you will read it here before deciding anything."
591
+ : posture === "false"
592
+ ? "This installation is LIVE (dry_run = false): the review will be posted to GitHub. The flag belongs to you; these instructions do not change it."
593
+ : "The dry_run posture could not be read, so posting is unknown. Check the live config before reviewing a PR.";
594
+ return {
595
+ name: "ask",
596
+ ok: false,
597
+ detail:
598
+ (typed ? "That is not a pull request number or link.\n" : "") +
599
+ "Read this out, then call this tool again with the PR number (pr) or its link (url).\n\n" +
600
+ (rows.length > 0
601
+ ? `The ${rows.length} most recently updated open pull requests in ${repo}:\n${rows.join("\n")}\n` +
602
+ "Pick one of these — or any other open PR in it — one you would actually want reviewed.\n"
603
+ : `Pick a real, open pull request in ${repo} — one you would actually want reviewed (your own is fine).\n`) +
604
+ "Its review starts as soon as you name it: nothing to add to the PR. (For later reviews, the trigger is the label " +
605
+ `"${label}" — the reviewer never reviews anything on its own initiative.)\n` +
606
+ assurance,
607
+ };
608
+ }
609
+
325
610
  return {
326
611
  id: PROOF_STEP_ID,
327
612
  title: "The proof: watch a real PR get reviewed",
328
613
  explain:
329
- "The wizard does not call itself done until you have seen the reviewer work. Ask the operator to open (or pick) one of " +
330
- "their own pull requests and put the trigger label on it, then call this tool with that PR number. It follows the round " +
331
- "through the daemon's own state until a verdict lands, and then SHOWS them the review it produced — read out of the " +
332
- "dry-run record, so they judge the output while it is still impossible for it to have posted to GitHub. If nothing " +
333
- "happens, or the round fails, it says what to check rather than waiting forever.",
614
+ "The wizard does not call itself done until you have seen the reviewer work, on one real, open pull request. Call this " +
615
+ "tool with no arguments to get the repo's five most recently updated open PRs (the operator's own included) to pick " +
616
+ "from, then call it again with pr (a number) or url (the PR's link). The review starts IMMEDIATELY — no label to add: " +
617
+ "the wizard asks the daemon directly. It follows the round through the daemon's own state until a verdict lands, then " +
618
+ "SHOWS the review — the verdict, how many findings, the top ones, and where the full text is — read out of the " +
619
+ "dry-run record, so they judge it while it is still impossible for it to have posted to GitHub. If nothing happens, " +
620
+ "or the round fails, it says what to check rather than waiting forever.",
334
621
  order: 76,
622
+ // After setup: the board offers it; finish never waits on it.
623
+ optional: true,
335
624
  parameters: {
336
625
  type: "object",
337
626
  properties: {
338
627
  pr: {
339
628
  type: "integer",
340
629
  description:
341
- "The pull request number the operator labelled. Call with no arguments first to get the exact instructions to read out.",
630
+ "The pull request number to review now. Call with no arguments first to get the open PRs to choose from.",
631
+ },
632
+ url: {
633
+ type: "string",
634
+ description:
635
+ "The pull request's URL, instead of its number (github.com/<owner>/<repo>/pull/<n>).",
636
+ },
637
+ confirm_live: {
638
+ type: "string",
639
+ description:
640
+ 'Only when the installation is LIVE (dry_run = false): "yes" means the operator agreed that this review posts to GitHub.',
342
641
  },
343
642
  },
344
643
  },
@@ -347,20 +646,19 @@ export function proofRoundStep(opts: ProofStepOptions = {}): OnboardingStep {
347
646
  const transport = stackTransportFor(ctx, opts.transport);
348
647
  const repo = repoOf(ctx.choices, ctx.adapters);
349
648
  const label = triggerLabelOf(ctx.adapters);
350
- const pr = prFrom(ctx.input, ctx.choices);
351
- if (pr === null) {
352
- let posture: string | null = null;
353
- try {
354
- posture = String(loadConfig(configPath(ctx.adapters)).dry_run);
355
- } catch {
356
- /* Unknown posture stays explicit. */
357
- }
358
- const assurance =
359
- posture === "true"
360
- ? "Nothing will be posted to GitHub: dry_run is on, so every would-be comment and review is written to a file instead, and you will read it here before deciding anything."
361
- : posture === "false"
362
- ? "This installation is LIVE (dry_run = false): adding the label can post the review to GitHub. The flag belongs to you; these instructions do not change it."
363
- : "The dry_run posture could not be read, so posting is unknown. Check the live config before adding the trigger label.";
649
+ let posture: string | null = null;
650
+ try {
651
+ posture = String(loadConfig(configPath(ctx.adapters)).dry_run);
652
+ } catch {
653
+ /* Unknown posture stays explicit. */
654
+ }
655
+ const answer = prFrom(ctx.input, ctx.choices);
656
+ const typed = ctx.input.pr !== undefined || ctx.input.url !== undefined;
657
+ if (answer === null || (typed && parsePrAnswer(ctx.input.pr ?? ctx.input.url) === null)) {
658
+ return { ok: false, checks: [await askCheck(ctx, repo, label, posture, typed)] };
659
+ }
660
+ const pr = answer.pr;
661
+ if (answer.repo !== null && answer.repo.toLowerCase() !== repo.toLowerCase()) {
364
662
  return {
365
663
  ok: false,
366
664
  checks: [
@@ -368,10 +666,25 @@ export function proofRoundStep(opts: ProofStepOptions = {}): OnboardingStep {
368
666
  name: "ask",
369
667
  ok: false,
370
668
  detail:
371
- `Read this out, then call this tool again with the PR number.\n\n` +
372
- `Pick a real, open pull request of yours in ${repo} — one you would actually want reviewed. Add the label ` +
373
- `"${label}" to it. That label IS the trigger: the reviewer never reviews anything on its own initiative.\n` +
374
- assurance,
669
+ `that link is a pull request in ${answer.repo}, but this reviewer reviews ${repo}. Pick an open PR in ${repo} ` +
670
+ "(or change the reviewed repo in the earlier GitHub step)",
671
+ },
672
+ ],
673
+ };
674
+ }
675
+ if (posture !== "true" && textOf(ctx.input.confirm_live).trim().toLowerCase() !== "yes") {
676
+ return {
677
+ ok: false,
678
+ checks: [
679
+ {
680
+ name: "live",
681
+ ok: false,
682
+ detail:
683
+ posture === "false"
684
+ ? `This installation is LIVE (dry_run = false): reviewing #${pr} now POSTS the review to GitHub. Ask the operator; ` +
685
+ 'only on their yes, call this step again with confirm_live: "yes". The flag is theirs; this step never changes it.'
686
+ : `The dry_run posture could not be read (${configPath(ctx.adapters)}), so whether this review would post is unknown. ` +
687
+ 'Fix the config first — or, on the operator\'s explicit yes, call again with confirm_live: "yes".',
375
688
  },
376
689
  ],
377
690
  };
@@ -391,28 +704,73 @@ export function proofRoundStep(opts: ProofStepOptions = {}): OnboardingStep {
391
704
  };
392
705
  }
393
706
 
707
+ // Where the PR stands before anything is asked of the daemon.
708
+ const first = await observe(transport, pr);
709
+ if (first.obs === null)
710
+ return { ok: false, checks: [stateStoreCheck(first.detail, transport)] };
711
+ const before = first.obs.rounds.at(-1);
712
+ const recorded = parseProofRecord(ctx.choices[PROOF_CHOICE_KEY]);
713
+ const resumable =
714
+ recorded !== null &&
715
+ recorded.pr === pr &&
716
+ before !== undefined &&
717
+ before.round_no === recorded.round_no &&
718
+ completedRound(before);
719
+ const inFlight =
720
+ before?.status === "running" ||
721
+ first.obs.state === "queued" ||
722
+ first.obs.state === "reviewing";
723
+ const initial = classifyRounds(first.obs);
394
724
  log(
395
725
  ctx,
396
- `following #${pr} — checking the daemon's state every ${Math.round(pollIntervalMs / 1000)}s, up to ${pollAttempts} times`,
726
+ initial.kind === "waiting"
727
+ ? initial.detail
728
+ : `#${pr} has ${first.obs.rounds.length} round${first.obs.rounds.length === 1 ? "" : "s"} on record (newest: round ${before?.round_no}, ${before?.status})`,
397
729
  );
398
- let last = "";
399
- for (let attempt = 1; attempt <= pollAttempts; attempt += 1) {
400
- const { obs, detail } = await observe(transport, pr);
401
- if (obs === null) {
730
+
731
+ // Ask for the review — unless one is already under way, or this is a
732
+ // re-run that only has to show the round it already recorded.
733
+ let requested = false;
734
+ let requestNote = "";
735
+ if (!resumable && !inFlight) {
736
+ const sent = await requestReview(ctx, transport, pr);
737
+ requested = sent.ok;
738
+ requestNote = sent.detail;
739
+ if (!sent.ok && sent.refused)
402
740
  return {
403
741
  ok: false,
404
742
  checks: [
405
743
  {
406
- name: "state-store",
744
+ name: "review-now",
407
745
  ok: false,
408
746
  detail:
409
- `could not read the daemon's state (${detail}) — Fix: \`${transport.logsCommand(200)}\`. The daemon ` +
410
- "may still be starting (the image is prebuilt; its first boot migrates state and reconciles), so give it a minute after bring-up and run this step again",
747
+ `the daemon refused to review #${pr} now: ${sent.detail}. ` +
748
+ `If this daemon predates "review this one PR now", add the label "${label}" to the PR instead and run this step again — ` +
749
+ "the label is the trigger the daemon has always understood.",
411
750
  },
412
751
  ],
413
752
  };
414
- }
415
- const outcome = classifyRounds(obs);
753
+ log(
754
+ ctx,
755
+ sent.ok
756
+ ? `asked the daemon to review #${pr} now (${sent.detail}) — no label needed`
757
+ : `could not ask the daemon directly (${sent.detail}); following #${pr} in case it is labelled "${label}"`,
758
+ );
759
+ }
760
+ // Only a round NEWER than the ones before the request answers it.
761
+ const floor = requested ? (before?.round_no ?? 0) : -Infinity;
762
+
763
+ log(
764
+ ctx,
765
+ `following #${pr} — checking the daemon's state every ${Math.round(pollIntervalMs / 1000)}s, up to ${pollAttempts} times`,
766
+ );
767
+ let last = "";
768
+ let settledPolls = 0;
769
+ for (let attempt = 1; attempt <= pollAttempts; attempt += 1) {
770
+ const { obs, detail } = await observe(transport, pr);
771
+ if (obs === null) return { ok: false, checks: [stateStoreCheck(detail, transport)] };
772
+ const fresh = { ...obs, rounds: obs.rounds.filter((r) => r.round_no > floor) };
773
+ const outcome = classifyRounds(fresh);
416
774
  if (outcome.kind === "done") {
417
775
  log(ctx, `round ${outcome.round.round_no} finished with ${outcome.round.verdict}`);
418
776
  return finish(ctx, transport, pr, outcome.round);
@@ -420,16 +778,47 @@ export function proofRoundStep(opts: ProofStepOptions = {}): OnboardingStep {
420
778
  if (outcome.kind === "failed") {
421
779
  return { ok: false, checks: [roundFailedCheck(outcome.round, pr, transport)] };
422
780
  }
781
+ // Asked, but the daemon settled the PR without a new round (nothing
782
+ // changed since a review of this same head): that review is the answer.
783
+ if (
784
+ requested &&
785
+ outcome.kind === "waiting" &&
786
+ obs.state !== "queued" &&
787
+ obs.state !== "reviewing"
788
+ ) {
789
+ settledPolls += 1;
790
+ const standing = obs.rounds.at(-1);
791
+ if (settledPolls >= 2 && completedRound(standing)) {
792
+ log(
793
+ ctx,
794
+ `the daemon reviewed nothing new — #${pr}'s head was already reviewed in round ${standing.round_no}; showing that review`,
795
+ );
796
+ return finish(ctx, transport, pr, standing);
797
+ }
798
+ } else settledPolls = 0;
423
799
  last =
424
800
  outcome.kind === "running"
425
801
  ? `round ${outcome.round.round_no} is running (started ${outcome.round.started_at})`
426
- : outcome.detail;
802
+ : requested && obs.state === "queued"
803
+ ? "queued — the round starts as soon as a review slot is free"
804
+ : outcome.detail;
427
805
  log(ctx, `${last} — attempt ${attempt}/${pollAttempts}`);
428
806
  if (attempt < pollAttempts) await sleep(pollIntervalMs);
429
807
  }
430
808
  return {
431
809
  ok: false,
432
- checks: [timeoutCheck(pr, repo, label, last, pollAttempts, pollIntervalMs, transport)],
810
+ checks: [
811
+ timeoutCheck(
812
+ pr,
813
+ repo,
814
+ label,
815
+ last,
816
+ pollAttempts,
817
+ pollIntervalMs,
818
+ transport,
819
+ requested ? requestNote : null,
820
+ ),
821
+ ],
433
822
  };
434
823
  },
435
824
 
@@ -537,7 +926,19 @@ export function proofRoundStep(opts: ProofStepOptions = {}): OnboardingStep {
537
926
  return { ok: false, checks };
538
927
  }
539
928
  const other = records.filter((r) => r.kind !== "create_review").length;
540
- checks.push({ name: "review", ok: true, detail: renderReview(review, pr) });
929
+ const file = records
930
+ .filter((r) => r.kind === "create_review" && r.reviewPurpose === undefined)
931
+ .at(-1)?.file;
932
+ checks.push({
933
+ name: "review",
934
+ ok: true,
935
+ detail: renderReview(review, pr, {
936
+ verdict: round.verdict,
937
+ ...(file
938
+ ? { file, readCommand: transport.execCommand(`cat ${CONTAINER_DRYRUN_DIR}/${file}`) }
939
+ : {}),
940
+ }),
941
+ });
541
942
  checks.push({
542
943
  name: "nothing-posted",
543
944
  ok: true,
@@ -596,6 +997,16 @@ function roundFailedCheck(
596
997
  };
597
998
  }
598
999
 
1000
+ function stateStoreCheck(detail: string, transport: StackTransport): CheckResult {
1001
+ return {
1002
+ name: "state-store",
1003
+ ok: false,
1004
+ detail:
1005
+ `could not read the daemon's state (${detail}) — Fix: \`${transport.logsCommand(200)}\`. The daemon ` +
1006
+ "may still be starting (the image is prebuilt; its first boot migrates state and reconciles), so give it a minute after bring-up and run this step again",
1007
+ };
1008
+ }
1009
+
599
1010
  function timeoutCheck(
600
1011
  pr: number,
601
1012
  repo: string,
@@ -604,6 +1015,8 @@ function timeoutCheck(
604
1015
  attempts: number,
605
1016
  intervalMs: number,
606
1017
  transport: StackTransport,
1018
+ /** Set when the review was asked for directly (review-now): its receipt. */
1019
+ requested: string | null = null,
607
1020
  ): CheckResult {
608
1021
  const minutes = Math.round((attempts * intervalMs) / 60_000);
609
1022
  return {
@@ -612,6 +1025,11 @@ function timeoutCheck(
612
1025
  detail:
613
1026
  `no finished round for #${pr} after ${minutes} minutes of watching (last seen: ${last || "nothing at all"}). ` +
614
1027
  "Nothing is broken by this — the wizard simply stopped waiting, and running this step again picks the watch back up.\n" +
1028
+ (requested !== null
1029
+ ? `The review WAS asked for directly (the daemon answered: ${requested}), so no label is needed. A first round on a ` +
1030
+ "big PR can take a while; `qcr tui " +
1031
+ `${pr}\` shows the round's own live log. If it never started:\n`
1032
+ : "") +
615
1033
  "What to check, in the order that actually finds it:\n" +
616
1034
  `① the label on #${pr} must be exactly "${label}" — the daemon matches the name from [github] trigger_label, and a ` +
617
1035
  "near-miss looks like nothing happening;\n" +