@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
package/src/tui/remote.ts CHANGED
@@ -337,8 +337,16 @@ export function killInFlightSsh(): number {
337
337
  return killed;
338
338
  }
339
339
 
340
+ /** What the ssh runner answers for an empty host — spawned, `ssh "" …` would
341
+ * die on "Could not resolve hostname" and every surface would print an ssh
342
+ * remedy with no host in it. */
343
+ export const NO_SSH_HOST_STDERR =
344
+ '[tui] ssh_host is empty — set it to the VM\'s ssh alias, or set [tui] target = "local"';
345
+
340
346
  export const realSshRunner: SshRunner = (host, argv, opts) =>
341
- runChild("ssh", ["-o", "BatchMode=yes", host, argv.map(shellQuote).join(" ")], opts);
347
+ host.trim() === ""
348
+ ? Promise.resolve({ ok: false, stdout: "", stderr: NO_SSH_HOST_STDERR, code: null })
349
+ : runChild("ssh", ["-o", "BatchMode=yes", host, argv.map(shellQuote).join(" ")], opts);
342
350
 
343
351
  /**
344
352
  * The laptop topology's runner — the same argv every builder above
@@ -0,0 +1,173 @@
1
+ // The TUI's route — which daemon the board reads, and how — resolved ONCE,
2
+ // before the first frame, from config.toml's [tui] block plus three local
3
+ // facts that cost no docker, network or directory walk: the compose
4
+ // project's `.env`, the onboarding checklist, and whether this user can open
5
+ // the Docker socket without sudo.
6
+ //
7
+ // Why a resolution step at all. `[tui]` ships VM-shaped defaults (target
8
+ // "ssh", sudo docker, the VM host's state path), and on one box a fresh
9
+ // install opens the TUI before onboarding's config_fill has written its
10
+ // local values — or with a hand-written config that never names them. Every
11
+ // builder in remote.ts reads the config it is handed, and the transport is
12
+ // still decided by the explicit `target` knob alone (`transportOf`); this
13
+ // module only decides which config that is, and says so once when it departs
14
+ // from the file:
15
+ //
16
+ // * `target = "ssh"` with an EMPTY `ssh_host` cannot reach anything. When
17
+ // this checkout is a compose project (the checklist recorded the local
18
+ // topology, or none was recorded and the project `.env` pins a release)
19
+ // the route is the local stack, said once; otherwise the route is
20
+ // `unconfigured` and every surface shows the one precise remedy instead
21
+ // of an ssh command with no host in it.
22
+ // * On the local route the state dir is the release image's
23
+ // (`/opt/qcr/state`, the compose `qcr-state` volume) whenever the knob
24
+ // still holds the VM default, the daemon's `-daemon-N` container implies
25
+ // its `-memory-N` sidecar for the memory forward, and sudo is dropped
26
+ // when the socket answers this user without it (a non-interactive
27
+ // `sudo docker` behind the alt screen can only fail).
28
+ //
29
+ // An operator's explicit, non-default value always wins: nothing here
30
+ // overwrites a value the file states differently from the shipped default.
31
+ import { accessSync, constants, readFileSync } from "node:fs";
32
+ import { join } from "node:path";
33
+ import { TUI_DEFAULTS, type TuiConfig } from "../config.js";
34
+ import { readChecklist, type ChecklistFile } from "../onboard/engine.js";
35
+ import { CHOICE_KEYS } from "../onboard/choices.js";
36
+ import { NO_SSH_HOST_STDERR } from "./remote.js";
37
+
38
+ /** The release image's state dir inside the daemon container (the compose
39
+ * `qcr-state` volume; src/onboard/steps/ingress.ts CONTAINER_STATE_DIR). */
40
+ export const RELEASE_STATE_DIR = "/opt/qcr/state";
41
+
42
+ /** The bundled compose project's daemon and memory containers
43
+ * (docker-compose.yml `name: quickreview`). */
44
+ export const COMPOSE_DAEMON_CONTAINER = "quickreview-daemon-1";
45
+ export const COMPOSE_MEMORY_CONTAINER = "quickreview-memory-1";
46
+
47
+ /** The legacy daemon container (no `qcr.layout` label): the one container whose
48
+ * in-container state dir is still the VM-shaped default. */
49
+ const LEGACY_CONTAINER = "quickreview-sandbox";
50
+
51
+ export interface RouteFacts {
52
+ /** The compose project directory (this checkout): remedy text names it. */
53
+ projectDir: string;
54
+ /** `<project>/.env`, or null when there is none. */
55
+ projectEnv: string | null;
56
+ /** The onboarding checklist, or null when absent or unreadable. */
57
+ checklist: ChecklistFile | null;
58
+ /** Can this user open the Docker socket without sudo? undefined = no socket
59
+ * found to ask (no local Docker, or a remote DOCKER_HOST). */
60
+ dockerSocketAccess: boolean | undefined;
61
+ }
62
+
63
+ export interface TuiRoute {
64
+ /** The config every TUI remote op is built from. */
65
+ cfg: TuiConfig;
66
+ /** One-time lines for the notice line: where the route departs from the file. */
67
+ notes: string[];
68
+ /** Set when nothing can be reached: the precise reason and remedy, shown in
69
+ * place of any ssh diagnosis. */
70
+ unconfigured?: string;
71
+ }
72
+
73
+ /** The precise message for `target = "ssh"` with no host and no local stack —
74
+ * the same words the ssh runner answers an empty host with. */
75
+ export const EMPTY_SSH_HOST = NO_SSH_HOST_STDERR;
76
+
77
+ /** `QCR_VERSION=` from a project `.env` — "" when absent. */
78
+ export function pinnedVersion(envText: string | null): string {
79
+ if (envText === null) return "";
80
+ const m = /^QCR_VERSION=(.*)$/m.exec(envText);
81
+ return m ? (m[1] ?? "").trim().replace(/^["']|["']$/g, "") : "";
82
+ }
83
+
84
+ /** The recorded topology, or null when the checklist recorded none. */
85
+ function recordedTopology(checklist: ChecklistFile | null): "local" | "vm" | null {
86
+ const raw = checklist?.choices[CHOICE_KEYS.topology];
87
+ return raw === "local" || raw === "vm" ? raw : null;
88
+ }
89
+
90
+ /** The `-memory-N` sidecar a compose `-daemon-N` container implies, or "". */
91
+ export function composeMemoryFor(container: string): string {
92
+ const m = /^(.+)-daemon-(\d+)$/.exec(container);
93
+ return m ? `${m[1]}-memory-${m[2]}` : "";
94
+ }
95
+
96
+ /** Pure: the effective [tui] config for these facts. */
97
+ export function resolveTuiRoute(file: TuiConfig, facts: RouteFacts): TuiRoute {
98
+ const notes: string[] = [];
99
+ let cfg: TuiConfig = { ...file };
100
+ if (cfg.target === "ssh" && cfg.ssh_host.trim() === "") {
101
+ const topology = recordedTopology(facts.checklist);
102
+ const composeProject = pinnedVersion(facts.projectEnv) !== "";
103
+ if (topology === "local" || (topology === null && composeProject)) {
104
+ cfg = {
105
+ ...cfg,
106
+ target: "local",
107
+ container:
108
+ cfg.container === TUI_DEFAULTS.container ? COMPOSE_DAEMON_CONTAINER : cfg.container,
109
+ };
110
+ notes.push(
111
+ `[tui] target is "ssh" with no ssh_host — reading the local compose stack in ${facts.projectDir} instead ` +
112
+ '(set [tui] target = "local" to make it explicit)',
113
+ );
114
+ } else {
115
+ return {
116
+ cfg,
117
+ notes,
118
+ unconfigured:
119
+ topology === "vm"
120
+ ? "[tui] ssh_host is empty — set it to the VM's ssh alias in config.toml"
121
+ : EMPTY_SSH_HOST,
122
+ };
123
+ }
124
+ }
125
+ if (cfg.target !== "local") return { cfg, notes };
126
+ if (cfg.remote_state_dir === TUI_DEFAULTS.remote_state_dir && cfg.container !== LEGACY_CONTAINER)
127
+ cfg = { ...cfg, remote_state_dir: RELEASE_STATE_DIR };
128
+ if ((cfg.memory_container ?? "").trim() === "") {
129
+ const memory = composeMemoryFor(cfg.container);
130
+ if (memory !== "") cfg = { ...cfg, memory_container: memory };
131
+ }
132
+ if (cfg.sudo_docker && facts.dockerSocketAccess === true) cfg = { ...cfg, sudo_docker: false };
133
+ return { cfg, notes };
134
+ }
135
+
136
+ /** The Docker socket this process would talk to: `DOCKER_HOST` when it is a
137
+ * unix socket, the default path otherwise; undefined for a remote daemon. */
138
+ export function dockerSocketPath(env: NodeJS.ProcessEnv): string | undefined {
139
+ const host = (env["DOCKER_HOST"] ?? "").trim();
140
+ if (host === "") return "/var/run/docker.sock";
141
+ return host.startsWith("unix://") ? host.slice("unix://".length) : undefined;
142
+ }
143
+
144
+ /** One `access(2)` on the socket — no docker call, no walk. */
145
+ export function probeSocketAccess(
146
+ path: string | undefined,
147
+ access: (p: string, mode: number) => void = accessSync,
148
+ ): boolean | undefined {
149
+ if (path === undefined) return undefined;
150
+ try {
151
+ access(path, constants.R_OK | constants.W_OK);
152
+ return true;
153
+ } catch (err) {
154
+ const code = (err as NodeJS.ErrnoException).code;
155
+ return code === "EACCES" || code === "EPERM" ? false : undefined;
156
+ }
157
+ }
158
+
159
+ /** The facts, read from this checkout: two small files and one access(2). */
160
+ export function readRouteFacts(root: string, env: NodeJS.ProcessEnv = process.env): RouteFacts {
161
+ let projectEnv: string | null = null;
162
+ try {
163
+ projectEnv = readFileSync(join(root, ".env"), "utf8");
164
+ } catch {
165
+ projectEnv = null;
166
+ }
167
+ return {
168
+ projectDir: root,
169
+ projectEnv,
170
+ checklist: readChecklist(join(root, "state")),
171
+ dockerSocketAccess: probeSocketAccess(dockerSocketPath(env)),
172
+ };
173
+ }
@@ -0,0 +1,56 @@
1
+ // Is setup finished? Read from the checklist file switch-on and Advanced keep
2
+ // (src/onboard/engine.ts), so a TUI opened halfway through says so on its
3
+ // first frame instead of troubleshooting a daemon nobody has started yet.
4
+ //
5
+ // Only a checklist that EXISTS and is not complete counts: an install that
6
+ // never switched auto-review on (a hand-written config, the VM operator's own
7
+ // setup) has no checklist and gets no banner. Setup is finished in the home
8
+ // (a bare `qcr`), so the steps judged are the home's: switch-on's and
9
+ // Advanced's registry (src/home/advanced.ts), with the engine's own
10
+ // predicates. The next step is the first enabled, required one the file does
11
+ // not record as done; when there is none, nothing is left that the home could
12
+ // finish, and there is no banner (the home never runs the wizard's finish, so
13
+ // its checklist need not carry the wizard's completion marker).
14
+ import { type ChecklistFile } from "../onboard/engine.js";
15
+
16
+ export interface SetupState {
17
+ /** The title of the first pending step, or undefined when it could not be
18
+ * computed. */
19
+ next: string | undefined;
20
+ }
21
+
22
+ /** The one sentence every surface uses. */
23
+ export function setupLine(s: SetupState): string {
24
+ return `setup not finished — run \`qcr\` to finish setting up${s.next ? ` (next: ${s.next})` : ""}`;
25
+ }
26
+
27
+ /** Complete now: finished once and not reopened since (engine.ts `completeNow`). */
28
+ function completeNow(file: ChecklistFile): boolean {
29
+ return typeof file.completed_at === "string" && !file.reopened_at;
30
+ }
31
+
32
+ /** undefined = nothing to say (no checklist, setup is complete, or the home
33
+ * has no step left to finish). */
34
+ export async function setupStateOf(
35
+ checklist: ChecklistFile | null,
36
+ ): Promise<SetupState | undefined> {
37
+ if (checklist === null || completeNow(checklist)) return undefined;
38
+ let next: string | undefined;
39
+ try {
40
+ const { advancedRegistry } = await import("../home/advanced.js");
41
+ const steps = [...advancedRegistry().steps()].sort((a, b) => a.order - b.order);
42
+ const pending = steps.find(
43
+ (s) =>
44
+ !s.optional &&
45
+ (s.enabled ? s.enabled(checklist.choices) : true) &&
46
+ checklist.steps[s.id]?.status !== "done",
47
+ );
48
+ if (pending === undefined) return undefined;
49
+ next = pending.title;
50
+ } catch {
51
+ // The step registry could not be loaded: the banner still says setup is
52
+ // unfinished, without naming a step it could not compute.
53
+ next = undefined;
54
+ }
55
+ return { next };
56
+ }
@@ -25,13 +25,22 @@ import { existsSync } from "node:fs";
25
25
  import { homedir } from "node:os";
26
26
  import type { TuiConfig } from "../config.js";
27
27
  import { masterCheckArgv } from "./connect.js";
28
- import { shellQuote, transportOf } from "./remote.js";
28
+ import { NO_SSH_HOST_STDERR, shellQuote, transportOf } from "./remote.js";
29
29
 
30
30
  export type ConnectionClass =
31
31
  /** The VM answers — master alive, or a fresh connection succeeded. */
32
32
  | "ok"
33
- /** `[tui] target = "local"`: there is no ssh anywhere. */
33
+ /** `[tui] target = "local"`: there is no ssh anywhere, and the daemon
34
+ * container is running on this machine. */
34
35
  | "local"
36
+ /** Local route: Docker answers, but the daemon container is absent or not
37
+ * running (the stack is not up yet, or it stopped). */
38
+ | "local-down"
39
+ /** Local route: the local Docker itself did not answer this user. */
40
+ | "local-docker"
41
+ /** Nothing to connect to: `target = "ssh"` with no `ssh_host` (onboarding
42
+ * has not written the route yet). No ssh is ever spawned for it. */
43
+ | "unconfigured"
35
44
  /** The agent cannot offer the key ssh wants for this host. */
36
45
  | "no-agent-key"
37
46
  /** A ControlMaster socket is there and does not answer. */
@@ -413,6 +422,112 @@ export interface TriageDeps {
413
422
  run?: ProbeRunner;
414
423
  exists?: (path: string) => boolean;
415
424
  home?: string;
425
+ /** The compose project directory, for the local route's remedy line. */
426
+ projectDir?: string;
427
+ /** The route resolver's reason nothing can be reached (src/tui/route.ts);
428
+ * defaults to the empty-ssh_host sentence. */
429
+ unconfigured?: string;
430
+ }
431
+
432
+ /** Is this a local-route diagnosis (no ssh anywhere)? */
433
+ export function isLocalDiagnosis(d: Pick<ConnectionDiagnosis, "kind">): boolean {
434
+ return d.kind === "local" || d.kind === "local-down" || d.kind === "local-docker";
435
+ }
436
+
437
+ /** A diagnosis under which the daemon can answer: the board's "connected". */
438
+ export function connectionUsable(d: Pick<ConnectionDiagnosis, "kind">): boolean {
439
+ return d.kind === "ok" || d.kind === "local";
440
+ }
441
+
442
+ /** `docker inspect` of the daemon container's state, for the local route.
443
+ * `sudo -n` when sudo is configured: a password prompt behind the alt screen
444
+ * is a hang, so a sudo that would ask fails instead. */
445
+ export function localDaemonProbeArgv(cfg: TuiConfig): { cmd: string; argv: string[] } {
446
+ const inspect = ["inspect", "-f", "{{.State.Status}}", cfg.container];
447
+ return cfg.sudo_docker
448
+ ? { cmd: "sudo", argv: ["-n", "docker", ...inspect] }
449
+ : { cmd: "docker", argv: inspect };
450
+ }
451
+
452
+ /** How to bring the stack up from here. */
453
+ export function composeUpRemedy(projectDir: string | undefined, container: string): string {
454
+ return projectDir
455
+ ? `docker compose --project-directory ${shellQuote(projectDir)} up -d`
456
+ : `docker start ${shellQuote(container)}`;
457
+ }
458
+
459
+ /** Pure: the local route's diagnosis from one `docker inspect`. */
460
+ export function classifyLocal(
461
+ cfg: TuiConfig,
462
+ res: ProbeResult,
463
+ projectDir?: string,
464
+ ): ConnectionDiagnosis {
465
+ const c = cfg.container;
466
+ const out = res.stdout.trim();
467
+ const evidence = firstLine(res.stderr);
468
+ const detail = evidence ? { detail: evidence } : {};
469
+ if (res.status === 0 && out === "running") {
470
+ return {
471
+ kind: "local",
472
+ cause: `local docker — the daemon container ${c} is running on this machine (no ssh)`,
473
+ remedy: "",
474
+ };
475
+ }
476
+ if (res.status === 0) {
477
+ return {
478
+ kind: "local-down",
479
+ cause: `the daemon container ${c} is ${out || "not running"} — start it`,
480
+ remedy: composeUpRemedy(projectDir, c),
481
+ };
482
+ }
483
+ if (res.timedOut) {
484
+ return {
485
+ kind: "local-docker",
486
+ cause: "local Docker did not answer in time",
487
+ remedy: "docker info",
488
+ ...detail,
489
+ };
490
+ }
491
+ const text = `${res.stderr}
492
+ ${res.stdout}`;
493
+ if (/no such (object|container)/i.test(text)) {
494
+ return {
495
+ kind: "local-down",
496
+ cause: `no daemon container ${c} on this machine — the compose stack is not up (it starts at onboarding's bring-up step)`,
497
+ remedy: composeUpRemedy(projectDir, c),
498
+ };
499
+ }
500
+ if (/a password is required|a terminal is required/i.test(text)) {
501
+ return {
502
+ kind: "local-docker",
503
+ cause: "[tui] sudo_docker is on and sudo wants a password the TUI cannot type",
504
+ remedy:
505
+ 'sudo usermod -aG docker "$USER" # then log in again, and set [tui] sudo_docker = false',
506
+ ...detail,
507
+ };
508
+ }
509
+ if (/permission denied/i.test(text)) {
510
+ return {
511
+ kind: "local-docker",
512
+ cause: "this user cannot open the local Docker socket",
513
+ remedy: 'sudo usermod -aG docker "$USER" # then log in again',
514
+ ...detail,
515
+ };
516
+ }
517
+ if (/ENOENT|not found|no such file/i.test(text) && !/daemon/i.test(text)) {
518
+ return {
519
+ kind: "local-docker",
520
+ cause: "there is no docker command on this machine",
521
+ remedy: "docker --version",
522
+ ...detail,
523
+ };
524
+ }
525
+ return {
526
+ kind: "local-docker",
527
+ cause: `local Docker is not answering — ${evidence || "no error text"}`,
528
+ remedy: "docker info",
529
+ ...detail,
530
+ };
416
531
  }
417
532
 
418
533
  const CONFIG_TIMEOUT_MS = 5_000;
@@ -458,14 +573,20 @@ export async function triageConnection(
458
573
  cfg: TuiConfig,
459
574
  deps: TriageDeps = {},
460
575
  ): Promise<ConnectionDiagnosis> {
576
+ const run = deps.run ?? realProbeRunner;
461
577
  if (transportOf(cfg) === "local") {
578
+ // No ssh anywhere: the one probe is local docker's own answer about the
579
+ // daemon container.
580
+ const probe = localDaemonProbeArgv(cfg);
581
+ return classifyLocal(cfg, await run(probe.cmd, probe.argv, CONFIG_TIMEOUT_MS), deps.projectDir);
582
+ }
583
+ if (cfg.ssh_host.trim() === "") {
462
584
  return {
463
- kind: "local",
464
- cause: "local transport — the daemon runs on this machine, no ssh involved",
465
- remedy: "",
585
+ kind: "unconfigured",
586
+ cause: deps.unconfigured ?? NO_SSH_HOST_STDERR,
587
+ remedy: "set [tui] ssh_host in config.toml",
466
588
  };
467
589
  }
468
- const run = deps.run ?? realProbeRunner;
469
590
  const exists = deps.exists ?? existsSync;
470
591
  const home = deps.home ?? homedir();
471
592
  const host = cfg.ssh_host;
package/src/tui/theme.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  // legend — a glance at the color tells the state without reading. Verdicts
4
4
  // and anomalies get their own fixed colors.
5
5
  import { ACCENT, MUTED } from "./design.js";
6
- import { STATE_GLYPHS } from "./model.js";
6
+ import { OFFERED_GLYPH, STATE_GLYPHS } from "./model.js";
7
7
  import { ESC, rgb } from "./paint.js";
8
8
 
9
9
  export const FG = {
@@ -78,6 +78,7 @@ export function legendLine(): string {
78
78
  [STATE_GLYPHS.queued, "queued", FG.queued],
79
79
  [STATE_GLYPHS.awaiting_author, "author", FG.awaiting_author],
80
80
  [STATE_GLYPHS.tracked, "tracked", FG.tracked],
81
+ [OFFERED_GLYPH, "not reviewed", FG.dim],
81
82
  ["↻", "stale", FG.accent],
82
83
  ];
83
84
  return parts
@@ -26,6 +26,7 @@ import type { App } from "./app.js";
26
26
  import { sanitizeText } from "./paint.js";
27
27
  import type { SshRunner } from "./remote.js";
28
28
  import { remoteSandboxRoot, resolveLayout } from "./layout.js";
29
+ import { operatorCommandWriteScriptAt } from "../operatorwrite.js";
29
30
 
30
31
  /** The commands this channel may produce. Deliberately a closed set: the
31
32
  * daemon's whitelist is the authority, and this is the narrower subset that
@@ -94,20 +95,7 @@ export function commandFileName(now: Date, seq: number, client?: string): string
94
95
  * (`tests/levers.test.ts`).
95
96
  */
96
97
  export function operatorCommandWriteScript(cfg: TuiConfig, name: string): string {
97
- const root = commandDirFor(cfg);
98
- const dir = operatorCommandDirFor(cfg);
99
- const tmp = `.${name}.tmp`;
100
- return (
101
- `[ -d '${root}' ] || { echo 'daemon commands dir ${root} missing - is the daemon running?' >&2; exit 1; }; ` +
102
- `mkdir -p '${dir}' || exit 1; ` +
103
- `[ ! -L '${dir}' ] || { echo '${dir} is a symlink - refusing to write the operator command' >&2; exit 1; }; ` +
104
- `R=$(cd -P '${root}' && pwd -P) || exit 1; ` +
105
- `cd -P '${dir}' || exit 1; ` +
106
- `[ "$(pwd -P)" = "$R/operator" ] || { echo '${dir} was replaced while writing - refusing to write the operator command' >&2; exit 1; }; ` +
107
- `chown -h 0:0 . && chmod 0755 . || exit 1; ` +
108
- `find . -maxdepth 1 -name '*.json' -mmin +1440 -delete 2>/dev/null; ` +
109
- `rm -f '${tmp}' '${name}' && cat > '${tmp}' && chmod 0644 '${tmp}' && mv '${tmp}' '${name}'`
110
- );
98
+ return operatorCommandWriteScriptAt(commandDirFor(cfg), name);
111
99
  }
112
100
 
113
101
  /** argv for the atomic command write. The payload rides STDIN, never the
package/src/verdicts.ts CHANGED
@@ -15,6 +15,11 @@ import {
15
15
  // write the companion inbox. There is no path here that touches an existing review:
16
16
  // the invariant against retracting an approval is structural, not prompted
17
17
  // (AGENTS.md invariant 2).
18
+ import {
19
+ botAccountLogin,
20
+ isPersonMode,
21
+ ownLogins,
22
+ } from "@quickchatai/pi-quickreview/src/identity.js";
18
23
  import {
19
24
  normalizePassPhase,
20
25
  normalizeSweepClaim,
@@ -406,6 +411,15 @@ export function decidedVerdictFromReceipt(receipt: PostingReceipt): ReceiptDecis
406
411
  return { verdict: "REQUEST_CHANGES" };
407
412
  if (receipt.intended_event === "APPROVE" && receipt.actual_event === "APPROVE")
408
413
  return { verdict: "APPROVE" };
414
+ // Person mode posts every review as a COMMENT: the approval the session
415
+ // asked for is the decision, and it was not withheld.
416
+ if (
417
+ receipt.person_mode === true &&
418
+ receipt.intended_event === "APPROVE" &&
419
+ receipt.actual_event === "COMMENT" &&
420
+ receipt.reconciled !== true
421
+ )
422
+ return { verdict: "APPROVE" };
409
423
  if (
410
424
  receipt.intended_event === "COMMENT" &&
411
425
  receipt.actual_event === "COMMENT" &&
@@ -1537,12 +1551,11 @@ export class VerdictWatcher {
1537
1551
  if (!this.deps.cfg.dry_run && this.deps.adapter.listReviewsFull) {
1538
1552
  try {
1539
1553
  const marker = postingMarker(round.posting_key);
1540
- const bot = this.deps.cfg.github.bot_login;
1541
1554
  const reviews = await this.deps.adapter.listReviewsFull(round.pr);
1542
1555
  reconciled = true;
1543
1556
  const matches = reviews.filter(
1544
1557
  (review) =>
1545
- (review.user === bot || review.user === `${this.deps.cfg.github.app_slug}[bot]`) &&
1558
+ ownLogins(this.deps.cfg.github).includes(review.user) &&
1546
1559
  review.commit_id?.toLowerCase() === expectedHead.toLowerCase() &&
1547
1560
  review.body.includes(marker),
1548
1561
  );
@@ -1596,6 +1609,7 @@ export class VerdictWatcher {
1596
1609
  gh_commit_id: match.commit_id,
1597
1610
  gh_state: match.state,
1598
1611
  ...(verifiedIntent ? {} : { reconciled: true }),
1612
+ ...(isPersonMode(this.deps.cfg.github) ? { person_mode: true as const } : {}),
1599
1613
  };
1600
1614
  if (verdict && !receiptMatchesVerdict(synthetic, verdict)) {
1601
1615
  return {
@@ -1804,7 +1818,7 @@ export class VerdictWatcher {
1804
1818
  );
1805
1819
  }
1806
1820
  const restored = restoreStandingLifecycle(store, round.pr, standing, {
1807
- botLogin: cfg.github.bot_login,
1821
+ botLogin: botAccountLogin(cfg.github),
1808
1822
  now: () => this.now(),
1809
1823
  reason: standing
1810
1824
  ? `posting-guarded-noop:${standing}`
@@ -2339,7 +2353,11 @@ export class VerdictWatcher {
2339
2353
  v.pr,
2340
2354
  { type: "verdict", verdict: v.verdict },
2341
2355
  { head_sha: v.head_sha },
2342
- { botLogin: cfg.github.bot_login, now: () => this.now(), reason: `verdict:${v.verdict}` },
2356
+ {
2357
+ botLogin: botAccountLogin(cfg.github),
2358
+ now: () => this.now(),
2359
+ reason: `verdict:${v.verdict}`,
2360
+ },
2343
2361
  );
2344
2362
  store.logEvent("verdict_applied", v.pr, {
2345
2363
  file: name,
@@ -3486,7 +3504,7 @@ export class VerdictWatcher {
3486
3504
  pr,
3487
3505
  { type: "round_overdue", standing },
3488
3506
  {},
3489
- { botLogin: cfg.github.bot_login, now: () => this.now(), reason: `anomaly:${kind}` },
3507
+ { botLogin: botAccountLogin(cfg.github), now: () => this.now(), reason: `anomaly:${kind}` },
3490
3508
  );
3491
3509
  const command = round ? store.sessionCommandForRound(pr, round.round_no) : undefined;
3492
3510
  const successor = store.successorFor(pr);
package/src/version.ts CHANGED
@@ -8,7 +8,7 @@ import {
8
8
  SCAN_SCHEMA_VERSION,
9
9
  VERSION_SCHEMA_VERSION,
10
10
  } from "@quickchatai/pi-quickreview/src/contract.js";
11
- import { piPinFromPackageJson } from "./onboard/launch.js";
11
+ import { piPinFromPackageJson } from "./onboard/pipin.js";
12
12
 
13
13
  export interface VersionInfo {
14
14
  schema_version: number;
package/src/webhook.ts CHANGED
@@ -9,6 +9,7 @@ import { createHmac, timingSafeEqual } from "node:crypto";
9
9
  import { createServer, type IncomingMessage, type Server, type ServerResponse } from "node:http";
10
10
  import { captureFromEvent, recordCapturedComment } from "./capture.js";
11
11
  import { reviewTeamSlug, type Config } from "./config.js";
12
+ import { botAccountLogin } from "@quickchatai/pi-quickreview/src/identity.js";
12
13
  import { interpretReviewEvent, isFeedbackBody, ledgerFeedbackSignal } from "./feedback.js";
13
14
  import { applyEvent as applyEventFor, mergedOverBlock } from "./statemachine.js";
14
15
  import type { StateStore, StatusSnapshot } from "./state.js";
@@ -20,6 +21,8 @@ import { blockedPrAuthor } from "./pr-conversation.js";
20
21
  * header, a wrong prefix, or a length mismatch (timingSafeEqual throws on
21
22
  * unequal lengths — we must not let that leak as a 500). */
22
23
  export function verifySignature(raw: Buffer, header: string | undefined, secret: string): boolean {
24
+ // An empty secret signs nothing: anyone can compute that HMAC.
25
+ if (secret === "") return false;
23
26
  if (!header || !header.startsWith("sha256=")) return false;
24
27
  const expected = Buffer.from(signBody(raw, secret), "utf8");
25
28
  const given = Buffer.from(header, "utf8");
@@ -499,6 +502,10 @@ export function handleDelivery(
499
502
  raw: Buffer,
500
503
  ): WebhookHandleResult {
501
504
  const log = deps.log ?? (() => {});
505
+ // The polling shape (and so `[github] auth = "user"`) takes no webhooks:
506
+ // the daemon hands no secret, and every delivery is refused unread. The
507
+ // reconcile poll is that install's only trigger.
508
+ if (deps.secret === "") return { status: 404, reason: "webhooks-disabled" };
502
509
  if (!verifySignature(raw, headers.signature, deps.secret)) {
503
510
  logRejection(deps, log, headers);
504
511
  return { status: 403, reason: "bad-signature" };
@@ -810,7 +817,9 @@ export async function applyInterpretation(
810
817
  i: Interpretation,
811
818
  ): Promise<void> {
812
819
  const { store, cfg } = deps;
813
- const opts = { botLogin: cfg.github.bot_login };
820
+ // The bot ACCOUNT for PR authorship. Person mode has none; no webhook
821
+ // arrives there, but the state machine reads one rule everywhere.
822
+ const opts = { botLogin: botAccountLogin(cfg.github) };
814
823
  const local = <T>(run: () => T): T => store.webhookLocalOnce(deps.deliveryGuid, run);
815
824
  switch (i.kind) {
816
825
  case "trigger":