@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
@@ -0,0 +1,85 @@
1
+ // The repository the home's setup checklist picks, written into config.toml:
2
+ // the config fill's writer (configfill.ts) over the template or the
3
+ // install's config, never touching dry_run.
4
+ import { parse } from "smol-toml";
5
+ import { assertNoDryRunEdit, changedEdits, patchToml, tomlString } from "./configfill.js";
6
+
7
+ const message = (err: unknown): string => (err instanceof Error ? err.message : String(err));
8
+
9
+ /** The repository pick, as the config takes it. */
10
+ export interface RepoConfigChoice {
11
+ repo: string;
12
+ baseBranch: string | null;
13
+ timezone: string | null;
14
+ ownerName: string | null;
15
+ ownerLogin: string | null;
16
+ }
17
+
18
+ /** A config value as text, "" when absent or not a string. */
19
+ function valueIn(doc: Record<string, unknown> | null, section: string, key: string): string {
20
+ const table = doc?.[section];
21
+ if (typeof table !== "object" || table === null) return "";
22
+ const v = (table as Record<string, unknown>)[key];
23
+ return typeof v === "string" ? v.trim() : "";
24
+ }
25
+
26
+ /** The config text with the choice in it, from `existing` (null: none yet,
27
+ * so the template). What changed is named; a config that is not TOML is
28
+ * refused, since what it holds cannot be seen. */
29
+ export function renderRepoConfig(
30
+ existing: string | null,
31
+ template: string,
32
+ choice: RepoConfigChoice,
33
+ ): { ok: true; text: string; wrote: string[] } | { ok: false; why: string } {
34
+ const fresh = existing === null;
35
+ const source = existing ?? template;
36
+ let doc: Record<string, unknown> | null = null;
37
+ try {
38
+ doc = parse(source);
39
+ } catch (err) {
40
+ return {
41
+ ok: false,
42
+ why: `${fresh ? "the shipped template" : "the config"} is not valid TOML (${message(err)}); nothing was written`,
43
+ };
44
+ }
45
+ const fill = (section: string, key: string): boolean =>
46
+ fresh || valueIn(doc, section, key) === "";
47
+ const wanted = [
48
+ { section: "github", key: "repo", value: tomlString(choice.repo) },
49
+ ...(choice.baseBranch
50
+ ? [{ section: "github", key: "base_branch", value: tomlString(choice.baseBranch) }]
51
+ : []),
52
+ ...(choice.timezone && fill("ops", "timezone")
53
+ ? [{ section: "ops", key: "timezone", value: tomlString(choice.timezone) }]
54
+ : []),
55
+ ...(choice.ownerName && fill("owner", "name")
56
+ ? [{ section: "owner", key: "name", value: tomlString(choice.ownerName) }]
57
+ : []),
58
+ ...(choice.ownerLogin && fill("owner", "github_login")
59
+ ? [{ section: "owner", key: "github_login", value: tomlString(choice.ownerLogin) }]
60
+ : []),
61
+ ];
62
+ assertNoDryRunEdit(wanted);
63
+ const edits = changedEdits(source, wanted);
64
+ const text = edits.length > 0 ? patchToml(source, edits) : source;
65
+ let after: Record<string, unknown>;
66
+ try {
67
+ after = parse(text);
68
+ } catch (err) {
69
+ return {
70
+ ok: false,
71
+ why: `the patched config would not parse (${message(err)}); nothing was written`,
72
+ };
73
+ }
74
+ if (valueIn(after, "github", "repo") !== choice.repo)
75
+ return {
76
+ ok: false,
77
+ why: "the patched config does not carry the repository; nothing was written",
78
+ };
79
+ if (fresh && after["dry_run"] !== true)
80
+ return {
81
+ ok: false,
82
+ why: "the shipped template no longer ships dry_run = true; nothing was written (invariant 6)",
83
+ };
84
+ return { ok: true, text, wrote: edits.map((e) => `[${e.section}] ${e.key}`) };
85
+ }
@@ -81,7 +81,7 @@ export interface TopologyOption {
81
81
  const TOPOLOGY_OPTION_METADATA = {
82
82
  vm: {
83
83
  headline:
84
- "A VM or server over ssh — RECOMMENDED, because a reviewer that is asleep reviews nothing",
84
+ "A VM or server over ssh — RECOMMENDED when this computer sleeps (a laptop), because a reviewer that is asleep reviews nothing",
85
85
  tradeoffs: [
86
86
  "it is on 24/7: pull requests opened while you sleep, while your laptop is shut, or while you are on a plane still get reviewed",
87
87
  "you bring the box (any ssh-reachable Linux machine — a cheap cloud VM is plenty); the wizard does not create one for you",
@@ -91,12 +91,13 @@ const TOPOLOGY_OPTION_METADATA = {
91
91
  ],
92
92
  },
93
93
  local: {
94
- headline: "This computer — everything in Docker on the machine you are sitting at",
94
+ headline:
95
+ "This computer — everything in Docker on the machine this wizard runs on; THE pick when it is itself an always-on server (a cloud VM you are logged into)",
95
96
  tradeoffs: [
96
- "nothing else to own: `docker compose up` and the whole system is here — daemon, memory engine, tunnel",
97
- "it reviews only while this machine is awake and the containers are running; close the lid and the reviewer stops",
98
- "zero third-party signup on the default path: the quick tunnel needs no account, and the daemon re-points its own webhook URL when that hostname rotates",
99
- "the honest way to try QuickReview, and a perfectly good permanent setup for one person on a desktop that stays on",
97
+ "nothing else to own: `docker compose up` and the whole system is here — daemon and memory engine, and the TUI reads them locally with no ssh",
98
+ "it reviews only while this machine is on and the containers are running: on a server that is always; on a laptop, close the lid and the reviewer stops",
99
+ "nothing to expose on the default path: the daemon polls GitHub for new pull requests every few minutes, with no webhook, tunnel or public URL",
100
+ "the honest way to try QuickReview, and a perfectly good permanent setup on a desktop that stays on or a cloud VM",
100
101
  "you can switch to a server later by re-running this step — the answers you gave (repo, App, keys) are kept",
101
102
  ],
102
103
  },
@@ -37,6 +37,7 @@ import {
37
37
  composePs,
38
38
  containerStateCheck,
39
39
  daemonExecArgs,
40
+ daemonRootExecArgs,
40
41
  healthUrl,
41
42
  memoryOn,
42
43
  isUp,
@@ -87,6 +88,10 @@ export interface StackTransport {
87
88
  daemonState(): Promise<DaemonState>;
88
89
  /** Run an argv INSIDE the daemon container. */
89
90
  exec(argv: readonly string[]): Promise<ProcessResult>;
91
+ /** The same, as uid 0: the one way to write the root-sealed operator
92
+ * command directory (`commands/operator/`), as `qcr drain` and the TUI's
93
+ * levers do. Used for the operator's own commands only. */
94
+ execAsRoot(argv: readonly string[]): Promise<ProcessResult>;
90
95
  /** Run an argv INSIDE the container that owns the memory engine: the memory
91
96
  * sidecar on the compose stack, `[tui] memory_container` (else the daemon's
92
97
  * container) on the server route. */
@@ -155,6 +160,10 @@ function composeTransport(ctx: StepContext, opts: TransportOptions): StackTransp
155
160
  return ctx.adapters.runner.run("docker", daemonExecArgs(root, argv));
156
161
  },
157
162
 
163
+ async execAsRoot(argv) {
164
+ return ctx.adapters.runner.run("docker", daemonRootExecArgs(root, argv));
165
+ },
166
+
158
167
  async execMemory(argv) {
159
168
  return ctx.adapters.runner.run(
160
169
  "docker",
@@ -465,6 +474,12 @@ function sshTransport(ctx: StepContext, opts: TransportOptions): StackTransport
465
474
  return remote([...dockerPrefix, "exec", facts.container, ...argv]);
466
475
  },
467
476
 
477
+ async execAsRoot(argv) {
478
+ if (facts.alias === "")
479
+ return { code: 1, stdout: "", stderr: "no ssh alias recorded for the server route" };
480
+ return remote([...dockerPrefix, "exec", "-u", "0", facts.container, ...argv]);
481
+ },
482
+
468
483
  async execMemory(argv) {
469
484
  if (facts.alias === "")
470
485
  return { code: 1, stdout: "", stderr: "no ssh alias recorded for the server route" };
@@ -0,0 +1,119 @@
1
+ // The TUI's own route, checked by the layered verification.
2
+ //
3
+ // The containers being up and the daemon answering /health prove the stack;
4
+ // they do not prove that `qcr tui`, opened right after the wizard, can READ
5
+ // it. The TUI reads status.json its own way — the [tui] block, resolved by
6
+ // src/tui/route.ts, through `runnerFor` (local docker, or ssh to the VM) — so
7
+ // this check runs exactly that: the same resolution, the same argv
8
+ // (`statusArgv`), the same runner, and it needs a status.json that parses.
9
+ // A red check here is the board that would open on "✖ unreachable".
10
+ import { loadConfig, type TuiConfig } from "../../config.js";
11
+ import { runnerFor, statusArgv, transportOf, type SshRunner } from "../../tui/remote.js";
12
+ import { readRouteFacts, resolveTuiRoute, type RouteFacts } from "../../tui/route.js";
13
+ import { sandboxRoot, type CheckResult, type StepContext } from "../engine.js";
14
+
15
+ export const TUI_ROUTE_CHECK = "tui-route";
16
+
17
+ export interface TuiRouteCheckOptions {
18
+ /** The runner for a resolved route; default `runnerFor` (spawns docker/ssh). */
19
+ runner?: (cfg: TuiConfig) => SshRunner;
20
+ /** Route facts that override what is read from the sandbox root. */
21
+ facts?: Partial<RouteFacts>;
22
+ /** Attempts while status.json is not written yet (a daemon just booted). */
23
+ attempts?: number;
24
+ intervalMs?: number;
25
+ sleep?: (ms: number) => Promise<void>;
26
+ }
27
+
28
+ function firstLine(text: string): string {
29
+ return (
30
+ text
31
+ .split("\n")
32
+ .map((l) => l.trim())
33
+ .find((l) => l !== "") ?? ""
34
+ );
35
+ }
36
+
37
+ /** Total: every outcome is a check, never a throw. */
38
+ export async function tuiRouteCheck(
39
+ ctx: StepContext,
40
+ opts: TuiRouteCheckOptions = {},
41
+ ): Promise<CheckResult> {
42
+ if (ctx.adapters.host.inContainer) {
43
+ return {
44
+ name: TUI_ROUTE_CHECK,
45
+ ok: true,
46
+ detail:
47
+ "not checked here — this wizard runs inside the image, and the TUI runs on the host: open `qcr tui` there",
48
+ };
49
+ }
50
+ let tui: TuiConfig;
51
+ try {
52
+ tui = loadConfig(ctx.adapters.paths.configPath).tui;
53
+ } catch (err) {
54
+ return {
55
+ name: TUI_ROUTE_CHECK,
56
+ ok: false,
57
+ detail: `config.toml does not load, so the TUI could not start either: ${String(err instanceof Error ? err.message : err)}`,
58
+ };
59
+ }
60
+ const route = resolveTuiRoute(tui, {
61
+ ...readRouteFacts(sandboxRoot(ctx.adapters.paths)),
62
+ ...opts.facts,
63
+ });
64
+ if (route.unconfigured !== undefined)
65
+ return {
66
+ name: TUI_ROUTE_CHECK,
67
+ ok: false,
68
+ detail: `${route.unconfigured} — Fix: run the config_fill step again`,
69
+ };
70
+ const cfg = route.cfg;
71
+ const run = (opts.runner ?? runnerFor)(cfg);
72
+ const argv = statusArgv(cfg);
73
+ const where =
74
+ transportOf(cfg) === "local"
75
+ ? `local docker, container ${cfg.container}`
76
+ : `ssh ${cfg.ssh_host}`;
77
+ const attempts = Math.max(1, opts.attempts ?? 4);
78
+ const sleep = opts.sleep ?? ((ms: number) => new Promise<void>((r) => setTimeout(r, ms)));
79
+ let last = "";
80
+ for (let i = 1; i <= attempts; i++) {
81
+ let res;
82
+ try {
83
+ res = await run(cfg.ssh_host, argv, { timeoutMs: 15_000 });
84
+ } catch (err) {
85
+ res = { ok: false, stdout: "", stderr: String(err), code: null };
86
+ }
87
+ if (res.ok) {
88
+ try {
89
+ const parsed = JSON.parse(res.stdout) as unknown;
90
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
91
+ const prs = (parsed as { prs?: unknown }).prs;
92
+ return {
93
+ name: TUI_ROUTE_CHECK,
94
+ ok: true,
95
+ detail:
96
+ `the TUI's own read works (${where}: \`${argv.join(" ")}\`) — status.json parses` +
97
+ `${Array.isArray(prs) ? `, ${prs.length} PR${prs.length === 1 ? "" : "s"} on the board` : ""}`,
98
+ };
99
+ }
100
+ last = "status.json is not a JSON object";
101
+ } catch {
102
+ last = "status.json is not valid JSON";
103
+ }
104
+ } else {
105
+ last = firstLine(res.stderr) || `exit ${String(res.code)}`;
106
+ }
107
+ if (i < attempts) await sleep(opts.intervalMs ?? 5_000);
108
+ }
109
+ return {
110
+ name: TUI_ROUTE_CHECK,
111
+ ok: false,
112
+ detail:
113
+ `the TUI could not read status.json the way it will (${where}: \`${argv.join(" ")}\`): ${last} — Fix: ` +
114
+ (transportOf(cfg) === "local"
115
+ ? "the [tui] container must be the running daemon container and remote_state_dir its state dir " +
116
+ "(run the config_fill step again to rewrite both)"
117
+ : `\`ssh ${cfg.ssh_host} true\` must work without a prompt, and remote_state_dir must be the VM's state dir`),
118
+ };
119
+ }
@@ -34,7 +34,7 @@
34
34
  // server, a curl on the box). Layer 3 needs no transport at all: it reads
35
35
  // GitHub's OWN delivery log, which is the same record wherever the daemon
36
36
  // runs, and that is exactly why it is the layer that proves the path.
37
- import { CHOICE_KEYS } from "../choices.js";
37
+ import { CHOICE_KEYS, userAuthRecorded } from "../choices.js";
38
38
  import { mintAppJwt } from "@quickchatai/pi-quickreview/src/githubAuth.js";
39
39
  import type { CheckResult, OnboardingStep, StepContext, StepReport } from "../engine.js";
40
40
  import { ingressOptionOf, secretsDirFor, readConfigText } from "./compose.js";
@@ -42,6 +42,7 @@ import { api as githubApi, readCredentials } from "./githubapp.js";
42
42
  import { readTomlKey, WEBHOOK_PATH } from "./ingress.js";
43
43
  import { sshCommandLine } from "./ssh.js";
44
44
  import { sshAliasOf, topologyOf } from "./topology.js";
45
+ import { tuiRouteCheck, type TuiRouteCheckOptions } from "./tuiroute.js";
45
46
  import {
46
47
  DEFAULT_HEALTH_TIMEOUT_MS,
47
48
  stackTransportFor,
@@ -67,6 +68,8 @@ export interface VerifyStackStepOptions {
67
68
  * WHICH transport is never an option: `stackTransportFor` decides from the
68
69
  * recorded topology, and this step never learns which one it got. */
69
70
  transport?: TransportOptions;
71
+ /** The TUI-route check's seams (tuiroute.ts): its runner and route facts. */
72
+ tuiRoute?: TuiRouteCheckOptions;
70
73
  }
71
74
 
72
75
  // ---- GitHub's delivery log ---------------------------------------------------
@@ -415,6 +418,22 @@ export function verifyStackStep(opts: VerifyStackStepOptions = {}): OnboardingSt
415
418
  });
416
419
  return { ok: false, checks };
417
420
  }
421
+ // The operator's own window onto the stack: can `qcr tui` read it the way
422
+ // it will? Independent of webhook delivery, so it never stops layer 3.
423
+ checks.push(await tuiRouteCheck(ctx, opts.tuiRoute));
424
+ // Layer 3 reads the App's own hook config and delivery log — App-JWT
425
+ // calls. The own-account route has no App, so there is no hook to read
426
+ // and nothing to deliver: the layer is skipped, and the reason said.
427
+ if (userAuthRecorded(ctx.choices)) {
428
+ checks.push({
429
+ name: "ping-delivery",
430
+ ok: true,
431
+ detail:
432
+ 'not checked — this install runs on your own GitHub account ([github] auth = "user"), which has no GitHub App, ' +
433
+ "so there is no webhook and no App delivery log to read; the reconcile poll is the only trigger path",
434
+ });
435
+ return { ok: checks.every((c) => c.ok), checks };
436
+ }
418
437
  // Layer 3 exists only where GitHub can deliver. The polling topology
419
438
  // deliberately exposes nothing, so a ping can never land there — requiring
420
439
  // one would keep this step (and so onboarding's finish) red forever.
@@ -0,0 +1,34 @@
1
+ // The onboarding session's tool names: one source for the extension that
2
+ // registers them (extensions/onboarding.ts) and the `--tools` allowlist that
3
+ // activates them (`onboardingToolAllowlist`).
4
+ //
5
+ // pi's `--tools <list>` is an allowlist over EVERY tool, extension tools
6
+ // included (pi 1.4.0: createAgentSession({ tools }) leaves an extension tool
7
+ // that is not on the list inactive). So the argv must name each wizard tool,
8
+ // or the guide runs with no tools at all. The built-in belt stays read-only:
9
+ // bash, write and edit are never on the list.
10
+
11
+ /** pi's built-in tools the wizard may use: reading only. */
12
+ export const ONBOARD_BUILTIN_TOOLS: readonly string[] = ["read", "ls", "grep"];
13
+
14
+ /** The checklist tool. */
15
+ export const ONBOARD_STATUS_TOOL = "onboard_status";
16
+ /** The verify-everything-and-exit tool. */
17
+ export const ONBOARD_FINISH_TOOL = "finish_and_exit";
18
+
19
+ /** A step's tool name. */
20
+ export function onboardStepToolName(stepId: string): string {
21
+ return `onboard_${stepId}`;
22
+ }
23
+
24
+ /** Every tool the onboarding extension registers over these step ids, in
25
+ * registration order. */
26
+ export function onboardingExtensionToolNames(stepIds: readonly string[]): string[] {
27
+ return [...stepIds.map(onboardStepToolName), ONBOARD_STATUS_TOOL, ONBOARD_FINISH_TOOL];
28
+ }
29
+
30
+ /** The whole `--tools` allowlist for the onboarding pi: the read-only belt,
31
+ * then every wizard tool. */
32
+ export function onboardingToolAllowlist(stepIds: readonly string[]): string[] {
33
+ return [...ONBOARD_BUILTIN_TOOLS, ...onboardingExtensionToolNames(stepIds)];
34
+ }
@@ -0,0 +1,25 @@
1
+ // The one in-container shell that writes an operator command file as root
2
+ // into the root-sealed `<commands>/operator/` — its full rationale is on
3
+ // `operatorCommandWriteScript` in src/tui/unwedge.ts, the TUI's caller. A
4
+ // leaf module (no imports) so the onboarding wizard's proof round, which knows
5
+ // the release image's command root rather than a TUI config, uses the same
6
+ // script without reaching the TUI's module graph.
7
+
8
+ /** `operatorCommandWriteScript` for a known command root (`<root>/commands`
9
+ * inside the container) — the onboarding wizard's caller, which knows the
10
+ * release image's layout rather than a TUI config. */
11
+ export function operatorCommandWriteScriptAt(root: string, name: string): string {
12
+ const dir = `${root}/operator`;
13
+ const tmp = `.${name}.tmp`;
14
+ return (
15
+ `[ -d '${root}' ] || { echo 'daemon commands dir ${root} missing - is the daemon running?' >&2; exit 1; }; ` +
16
+ `mkdir -p '${dir}' || exit 1; ` +
17
+ `[ ! -L '${dir}' ] || { echo '${dir} is a symlink - refusing to write the operator command' >&2; exit 1; }; ` +
18
+ `R=$(cd -P '${root}' && pwd -P) || exit 1; ` +
19
+ `cd -P '${dir}' || exit 1; ` +
20
+ `[ "$(pwd -P)" = "$R/operator" ] || { echo '${dir} was replaced while writing - refusing to write the operator command' >&2; exit 1; }; ` +
21
+ `chown -h 0:0 . && chmod 0755 . || exit 1; ` +
22
+ `find . -maxdepth 1 -name '*.json' -mmin +1440 -delete 2>/dev/null; ` +
23
+ `rm -f '${tmp}' '${name}' && cat > '${tmp}' && chmod 0644 '${tmp}' && mv '${tmp}' '${name}'`
24
+ );
25
+ }
package/src/outcomes.ts CHANGED
@@ -97,6 +97,7 @@ import { prActivity, type PrActivity } from "./pr-activity.js";
97
97
  // read-only and memoised; a commit the clone does not hold is UNKNOWN, never a
98
98
  // change and never no-change. Every failure is the sweep's own — reconcile's
99
99
  // health (which lifts a deploy drain) never waits on it.
100
+ import { carriesOwnMarker } from "@quickchatai/pi-quickreview/src/identity.js";
100
101
  import {
101
102
  carriedToRef,
102
103
  isPostedRow,
@@ -519,6 +520,10 @@ export interface OutcomeCaptureDeps {
519
520
  store: StateStore;
520
521
  /** `[github] bot_login` — whose root comments are findings. */
521
522
  botLogin: string;
523
+ /** `[github] auth = "user"`: the login is the person's, so a root comment is
524
+ * a finding only when it carries the reviewer's marker, and the person's
525
+ * own replies are a human's. */
526
+ personMode?: boolean;
522
527
  /** Absent ⇒ every "did the code change" question is unknown. */
523
528
  git?: AnchorGit;
524
529
  now?: () => Date;
@@ -585,6 +590,7 @@ export class OutcomeCapture {
585
590
  this.store.db.transaction(() => {
586
591
  for (const c of comments) {
587
592
  if (c.user !== this.deps.botLogin || c.in_reply_to_id !== null) continue;
593
+ if (this.deps.personMode && !carriesOwnMarker(c.body)) continue;
588
594
  const marker = parseFindingMarker(c.body);
589
595
  const head = commentHead(c.body);
590
596
  const info = stmt.run({
@@ -823,7 +829,8 @@ export class OutcomeCapture {
823
829
  const replies = new Map<number, PrCommentRow[]>();
824
830
  for (const c of this.store.prComments(pr)) {
825
831
  if (c.source !== "review_comment" || c.in_reply_to_id === null) continue;
826
- if (c.author === this.deps.botLogin) continue;
832
+ if (c.author === this.deps.botLogin && (!this.deps.personMode || carriesOwnMarker(c.body)))
833
+ continue;
827
834
  replies.set(c.in_reply_to_id, [...(replies.get(c.in_reply_to_id) ?? []), c]);
828
835
  }
829
836
  const events = this.store.prEvents(pr, [
@@ -0,0 +1,12 @@
1
+ // pi's agent dir, the way pi itself finds it: a leaf, so the npm verbs
2
+ // (src/npmverbs.ts) and the home's login reader (src/home/adapters.ts) read
3
+ // one rule without importing each other.
4
+ import { homedir } from "node:os";
5
+ import { join } from "node:path";
6
+
7
+ /** pi's agent dir: `$PI_CODING_AGENT_DIR`, else `~/.pi/agent`, as pi reads it. */
8
+ export function piAgentDir(env: Readonly<Record<string, string | undefined>>): string {
9
+ const set = (env["PI_CODING_AGENT_DIR"] ?? "").trim();
10
+ if (set !== "") return set;
11
+ return join((env["HOME"] ?? "").trim() || homedir(), ".pi", "agent");
12
+ }
package/src/pilogin.ts CHANGED
@@ -4,15 +4,24 @@
4
4
  // (src/scanbox.ts) share it without one reaching the other.
5
5
 
6
6
  /** pi itself, with `/login` (or `/login <provider>`) typed and sent for the
7
- * user: pi runs in a fresh tmux session; once its input box is drawn (two
8
- * editor borders on screen) the keys go in, and again every 0.3 s until pi's
9
- * login menu shows — an Enter sent while pi still starts is lost, and a
10
- * `/login` left in the editor then only needs the Enter. After 30 s the
11
- * terminal attaches regardless, so every login pi offers is there and
12
- * quitting pi ends it. `pi` is the pi to start: the companion image's CLI on
13
- * its PATH by default; the release image's entrypoint passes its one pi,
14
- * node_modules/.bin/pi (src/picli.ts `piCliIn`), and `qcr login` the pinned
15
- * package's own bin (src/picli.ts `piCliPath`). */
7
+ * user: pi runs in a fresh tmux session, and qcr reads pi's input box, the
8
+ * lines between the last two editor borders on screen, every 0.3 s (a second
9
+ * after a key, which counts as three polls toward the 30 s). Empty, it gets the keys and an Enter; still holding exactly
10
+ * the keys, one more Enter (an Enter sent while pi still starts is lost, and
11
+ * the first one may only accept the autocomplete). Anything else in the box is
12
+ * one of pi's menus (the provider list, a provider's own login methods), and
13
+ * no key goes in again. pi draws every selection in that box, so this holds
14
+ * whatever title pi or a provider gives the menu, where an Enter would pick
15
+ * its default: a browser login, opened again on every poll. pi starts with
16
+ * `--no-approve`: a login needs nothing from the folder qcr was run in, so a
17
+ * project folder neither stops it at pi's trust prompt nor gets trusted. A pi
18
+ * that has already exited ends the wait at once. After 30 s the terminal
19
+ * attaches regardless, so every login pi offers is there and quitting pi ends
20
+ * it; a failed attach (no terminal) ends the session instead of leaving pi
21
+ * running where nobody can reach it. `pi` is the pi to start: the
22
+ * companion image's CLI on its PATH by default; the release image's entrypoint
23
+ * passes its one pi, node_modules/.bin/pi (src/picli.ts `piCliIn`), and
24
+ * `qcr login` the pinned package's own bin (src/picli.ts `piCliPath`). */
16
25
  export function loginInPi(provider?: string, pi: string = "pi"): string[] {
17
26
  if (provider !== undefined && !/^[A-Za-z0-9._-]+$/.test(provider))
18
27
  throw new Error(
@@ -22,16 +31,23 @@ export function loginInPi(provider?: string, pi: string = "pi"): string[] {
22
31
  if (!/^[A-Za-z0-9._/@+-]+$/.test(pi))
23
32
  throw new Error(`scan login: pi ${JSON.stringify(pi)} is not a plain path`);
24
33
  const keys = provider ? `/login ${provider}` : "/login";
34
+ // The input box, its side padding (pi's editorPaddingX) trimmed and blank
35
+ // lines dropped; awk fails while fewer than two borders are drawn (pi still
36
+ // starting), and the loop then only waits.
37
+ const box =
38
+ `tmux capture-pane -p -t "$s" 2>/dev/null | awk '/──────────/ { b[++n] = NR } { l[NR] = $0 } ` +
39
+ `END { if (n < 2) exit 1; for (i = b[n - 1] + 1; i < b[n]; i++) ` +
40
+ `{ sub(/^ +/, "", l[i]); sub(/ +$/, "", l[i]); if (l[i] != "") print l[i] } }'`;
25
41
  return [
26
42
  "sh",
27
43
  "-c",
28
- `s=qcr-login-$$; tmux new-session -d -s "$s" ${pi} || exit 1; i=0; ` +
29
- `while [ $i -lt 100 ]; do p=$(tmux capture-pane -p -t "$s"); ` +
30
- `case "$p" in *"Select authentication"*|*"Select provider"*) break;; esac; ` +
31
- `if [ "$(printf '%s\\n' "$p" | grep -cF '──────────')" -ge 2 ]; then ` +
32
- `if printf '%s\\n' "$p" | grep -qxF '${keys}'; then tmux send-keys -t "$s" Enter; ` +
33
- `else tmux send-keys -t "$s" -l '${keys}'; tmux send-keys -t "$s" Enter; fi; fi; ` +
44
+ `s=qcr-login-$$; tmux new-session -d -s "$s" ${pi} --no-approve || exit 1; i=0; ` +
45
+ `while [ $i -lt 100 ]; do tmux has-session -t "$s" 2>/dev/null || break; ` +
46
+ `if e=$(${box}); then ` +
47
+ `if [ -z "$e" ]; then tmux send-keys -t "$s" -l '${keys}'; tmux send-keys -t "$s" Enter; sleep 1; i=$((i+3)); ` +
48
+ `elif [ "$e" = '${keys}' ]; then tmux send-keys -t "$s" Enter; sleep 1; i=$((i+3)); ` +
49
+ `else break; fi; fi; ` +
34
50
  `sleep 0.3; i=$((i+1)); done; ` +
35
- `exec tmux attach -t "$s"`,
51
+ `tmux attach -t "$s" || { tmux kill-session -t "$s" 2>/dev/null; exit 1; }`,
36
52
  ];
37
53
  }
@@ -106,6 +106,10 @@ export interface JournalOptions {
106
106
  /** `<state>/posting-receipts`, the dir the verdict watcher and the outbox read. */
107
107
  dir: string;
108
108
  repo: string;
109
+ /** `[github] auth = "user"`: every receipt says `person_mode`, the posting
110
+ * guard posts every event as a COMMENT, and an APPROVE posted as a COMMENT
111
+ * is NOT the withheld-approval shape (posting-receipt.ts). */
112
+ personMode?: boolean;
109
113
  now?: () => Date;
110
114
  }
111
115
 
@@ -357,7 +361,7 @@ export class PostingJournal {
357
361
  // An APPROVE that GitHub holds as a COMMENT was softened by guard 4 on
358
362
  // the gate after the CI preflight: the withheld-approval shape, which
359
363
  // the outbox's confirmation stamps the same way.
360
- ...(o.intended === "APPROVE" && actual === "COMMENT"
364
+ ...(o.intended === "APPROVE" && actual === "COMMENT" && !this.o.personMode
361
365
  ? { delivered_by: "daemon_retry" as const }
362
366
  : {}),
363
367
  }),
@@ -519,6 +523,7 @@ export class PostingJournal {
519
523
  actual_event: actual,
520
524
  payload_sha256: payloadHash(payload),
521
525
  prepared_at: base.prepared_at ?? this.iso(),
526
+ ...(this.o.personMode ? { person_mode: true } : {}),
522
527
  ...extra,
523
528
  };
524
529
  }
@@ -285,6 +285,10 @@ export class PostingOutbox {
285
285
  repo: string;
286
286
  dryRun: boolean;
287
287
  botLogins: string[];
288
+ /** `[github] auth = "user"`: receipts say `person_mode`, and an APPROVE
289
+ * delivered as a COMMENT is the person-mode rule, not a guard-4
290
+ * softening (posting-receipt.ts). */
291
+ personMode?: boolean;
288
292
  adapter: PostingDeliveryAdapter;
289
293
  current: (job: PostingJob) => boolean;
290
294
  now?: () => Date;
@@ -432,10 +436,13 @@ export class PostingOutbox {
432
436
  gh_state: state,
433
437
  // An APPROVE that GitHub holds as a COMMENT was softened by guard 4 on
434
438
  // the gate, which only the daemon's delivery passes through (the shell
435
- // route refuses a red-CI approval): the withheld-approval shape.
436
- ...(deliveredBy || (j.intended_event === "APPROVE" && event === "COMMENT")
439
+ // route refuses a red-CI approval): the withheld-approval shape. Not in
440
+ // person mode, where every review is a COMMENT by rule.
441
+ ...(deliveredBy ||
442
+ (j.intended_event === "APPROVE" && event === "COMMENT" && !this.deps.personMode)
437
443
  ? { delivered_by: "daemon_retry" as const }
438
444
  : {}),
445
+ ...(this.deps.personMode ? { person_mode: true as const } : {}),
439
446
  } satisfies PostingReceipt);
440
447
  j.status = "done";
441
448
  writePostingFile(path, j);
@@ -762,6 +769,7 @@ export class PostingOutbox {
762
769
  status: "prepared",
763
770
  payload_sha256: j.payload_sha256,
764
771
  prepared_at: j.attempted_at,
772
+ ...(this.deps.personMode ? { person_mode: true as const } : {}),
765
773
  } satisfies PostingReceipt);
766
774
  try {
767
775
  const result = await this.deps.adapter.createReview(j.pr, j.payload);
@@ -52,6 +52,11 @@ export interface PostingReceipt {
52
52
  * (APPROVE posted as a COMMENT, approval withheld for red CI) is something
53
53
  * the verdict file cannot know: see `retrySoftenedApproval`. */
54
54
  delivered_by?: "daemon_retry";
55
+ /** Written by a person-mode install (`[github] auth = "user"`): the posting
56
+ * guard posts EVERY review as a COMMENT there, so `actual_event` COMMENT
57
+ * proves nothing about the decision — `intended_event` carries it, and an
58
+ * APPROVE posted as a COMMENT is not a withheld approval. */
59
+ person_mode?: true;
55
60
  /** In memory only, never written by the posting scripts: the
56
61
  * daemon SYNTHESISED this receipt from a bot review carrying the round's
57
62
  * marker, because the shell never persisted its own. `intended_event` is
@@ -209,6 +214,7 @@ export function parsePostingReceipt(raw: string): PostingOutcome | undefined {
209
214
  value.status !== "deferred") ||
210
215
  (value.status === "deferred" && !isoDate(value.deferred_at)) ||
211
216
  (value.delivered_by !== undefined && value.delivered_by !== "daemon_retry") ||
217
+ (value.person_mode !== undefined && value.person_mode !== true) ||
212
218
  typeof value.payload_sha256 !== "string" ||
213
219
  !/^[a-f0-9]{64}$/.test(value.payload_sha256) ||
214
220
  !isoDate(value.prepared_at)
@@ -245,6 +251,7 @@ export function parsePostingReceipt(raw: string): PostingOutcome | undefined {
245
251
  * the receipt is the evidence that the approval was withheld. */
246
252
  export function retrySoftenedApproval(receipt: PostingReceipt): boolean {
247
253
  return (
254
+ receipt.person_mode !== true &&
248
255
  receipt.delivered_by === "daemon_retry" &&
249
256
  receipt.status === "confirmed" &&
250
257
  receipt.intended_event === "APPROVE" &&
@@ -259,6 +266,16 @@ export function receiptMatchesVerdict(receipt: PostingReceipt, verdict: VerdictF
259
266
  receipt.head_sha.toLowerCase() !== verdict.head_sha.toLowerCase()
260
267
  )
261
268
  return false;
269
+ // Person mode posts every review as a COMMENT. A receipt synthesised from
270
+ // GitHub knows nothing more than that; any other one is matched on what
271
+ // the session asked for, as if it had been posted as asked.
272
+ if (receipt.person_mode === true && receipt.actual_event === "COMMENT") {
273
+ if (receipt.reconciled === true) return true;
274
+ const asAsked: PostingReceipt = { ...receipt, actual_event: receipt.intended_event };
275
+ delete asAsked.person_mode;
276
+ delete asAsked.delivered_by;
277
+ return receiptMatchesVerdict(asAsked, verdict);
278
+ }
262
279
  if (verdict.verdict === "REQUEST_CHANGES")
263
280
  return receipt.actual_event === "REQUEST_CHANGES" || receipt.actual_event === "COMMENT";
264
281
  if (verdict.verdict === "APPROVE" && retrySoftenedApproval(receipt)) return true;