@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/ghread.ts CHANGED
@@ -23,6 +23,14 @@
23
23
  // `companion-guard` additionally vetoes write-shaped calls against the API
24
24
  // (belt); the token's own scope is the brace.
25
25
  //
26
+ // Under `[github] auth = "user"` (the person's own credential, SECURITY.md)
27
+ // there is no App to mint from. The companion then gets a COPY of
28
+ // the person's token file (`<secrets dir>/github_token`), re-copied whenever
29
+ // the host refresher rewrites it, in the same env file and the same
30
+ // read-only mount. That token is the person's and is NOT down-scoped: in this
31
+ // mode `companion-guard`'s veto of write-shaped calls is the only fence, and
32
+ // the env file's header says so.
33
+ //
26
34
  // Everything here is total at the edge the TUI calls: `refresh()` answers an
27
35
  // outcome, never throws — a companion without GitHub is under-equipped, never
28
36
  // a companion that does not start.
@@ -160,7 +168,11 @@ const TOKEN_VALUE_RE = /^[A-Za-z0-9_.-]+$/;
160
168
  /** The env file's text. Every value is checked against a character set that
161
169
  * is inert in both a docker env file and a `source`d shell file. A token
162
170
  * outside that set is refused, and the refusal never echoes it. */
163
- export function renderTokenEnv(tok: ReadToken, repo: string): string {
171
+ export function renderTokenEnv(
172
+ tok: ReadToken,
173
+ repo: string,
174
+ source: "app" | "user" = "app",
175
+ ): string {
164
176
  if (!TOKEN_VALUE_RE.test(tok.token))
165
177
  throw new Error(
166
178
  "the minted token has a character an env file cannot carry safely (only A-Z a-z 0-9 _ . - are allowed)",
@@ -169,11 +181,16 @@ export function renderTokenEnv(tok: ReadToken, repo: string): string {
169
181
  throw new Error("the token expiry is not a timestamp");
170
182
  if (!REPO_RE.test(repo))
171
183
  throw new Error(`[github] repo ${JSON.stringify(repo)} is not owner/name`);
184
+ const header =
185
+ source === "user"
186
+ ? '# Copied on the host by qcr from the person\'s own GitHub token ([github] auth = "user").\n' +
187
+ "# It is NOT down-scoped: it can do what that person can. The companion uses it for reads\n" +
188
+ "# only, and companion-guard vetoes write-shaped calls. Re-copied when the host refreshes it.\n"
189
+ : "# Minted on the host by qcr: a READ-ONLY GitHub App installation token\n" +
190
+ "# (contents / issues / pull requests: read) for one repository. Re-minted before\n" +
191
+ "# it expires. It cannot write to GitHub.\n";
172
192
  return (
173
- "# Minted on the host by qcr: a READ-ONLY GitHub App installation token\n" +
174
- "# (contents / issues / pull requests: read) for one repository. Re-minted before\n" +
175
- "# it expires. It cannot write to GitHub.\n" +
176
- `${TOKEN_VAR}=${tok.token}\n${EXPIRES_VAR}=${tok.expires_at}\n${REPO_VAR}=${repo}\n`
193
+ header + `${TOKEN_VAR}=${tok.token}\n${EXPIRES_VAR}=${tok.expires_at}\n${REPO_VAR}=${repo}\n`
177
194
  );
178
195
  }
179
196
 
@@ -227,10 +244,22 @@ export function tokenExpiry(dir: string, repo?: string): number | undefined {
227
244
  return Number.isFinite(ms) ? ms : undefined;
228
245
  }
229
246
 
247
+ /** The token the env file holds, or undefined. */
248
+ function heldToken(dir: string): string | undefined {
249
+ try {
250
+ const text = readFileSync(join(dir, TOKEN_FILE), "utf8");
251
+ return new RegExp(`^${TOKEN_VAR}=(.*)$`, "m").exec(text)?.[1]?.trim();
252
+ } catch {
253
+ return undefined;
254
+ }
255
+ }
256
+
230
257
  export type TokenOutcome =
231
258
  /** The file holds a token with more than the margin left: nothing minted. */
232
259
  | { kind: "fresh"; expiresAt: string }
233
260
  | { kind: "minted"; expiresAt: string }
261
+ /** `auth = "user"`: the person's token file was copied in (it changed). */
262
+ | { kind: "copied"; expiresAt: string }
234
263
  /** No App credential on this host: the companion simply has no GitHub. */
235
264
  | { kind: "unavailable"; reason: string }
236
265
  | { kind: "failed"; reason: string };
@@ -242,6 +271,8 @@ export function tokenOutcomeLine(o: TokenOutcome): string {
242
271
  return `read-only GitHub token valid until ${o.expiresAt}`;
243
272
  case "minted":
244
273
  return `read-only GitHub token minted (valid until ${o.expiresAt})`;
274
+ case "copied":
275
+ return "GitHub token copied from the person's own token file (auth = user; used read-only)";
245
276
  case "unavailable":
246
277
  return `no read-only GitHub token — ${o.reason}`;
247
278
  case "failed":
@@ -254,9 +285,12 @@ export interface CompanionGithubTokenOptions {
254
285
  dir: string;
255
286
  /** `[github] repo`, owner/name. */
256
287
  repo: string;
257
- /** The resolved App secrets dir (`app_id`, `app.pem`). Read here, on the
258
- * host; nothing from it is ever written into `dir`. */
288
+ /** The resolved secrets dir (`app_id`, `app.pem`; or `github_token` under
289
+ * `auth = "user"`). Read here, on the host; under "app" nothing from it is
290
+ * ever written into `dir`. */
259
291
  secretsDir: string;
292
+ /** `[github] auth`. Absent ⇒ "app". */
293
+ auth?: "app" | "user";
260
294
  fetchFn?: FetchFn;
261
295
  apiBase?: string;
262
296
  now?: () => number;
@@ -276,6 +310,7 @@ export class CompanionGithubToken {
276
310
  }
277
311
 
278
312
  private async doRefresh(): Promise<TokenOutcome> {
313
+ if (this.opts.auth === "user") return this.copyUserToken();
279
314
  const now = (this.opts.now ?? Date.now)();
280
315
  const current = tokenExpiry(this.opts.dir, this.opts.repo);
281
316
  if (current !== undefined && current - now > REFRESH_MARGIN_MS)
@@ -307,6 +342,38 @@ export class CompanionGithubToken {
307
342
  }
308
343
  }
309
344
 
345
+ /** `auth = "user"`: copy the person's token file in when it differs from
346
+ * what the env file holds (or the repo changed). The expiry the file
347
+ * carries is a re-check time, not GitHub's: a user token's own expiry is
348
+ * not known here. */
349
+ private copyUserToken(): TokenOutcome {
350
+ const source = join(this.opts.secretsDir, "github_token");
351
+ let token: string;
352
+ try {
353
+ token = readFileSync(source, "utf8").trim();
354
+ } catch {
355
+ return {
356
+ kind: "unavailable",
357
+ reason: `no user token at ${source} (run \`qcr github-token sync\` on the host)`,
358
+ };
359
+ }
360
+ if (token === "")
361
+ return { kind: "unavailable", reason: `the user token file ${source} is empty` };
362
+ const expiresAt = new Date((this.opts.now ?? Date.now)() + REFRESH_MARGIN_MS * 2).toISOString();
363
+ const heldUntil = tokenExpiry(this.opts.dir, this.opts.repo);
364
+ if (heldUntil !== undefined && heldToken(this.opts.dir) === token)
365
+ return { kind: "fresh", expiresAt: new Date(heldUntil).toISOString() };
366
+ try {
367
+ writeTokenEnv(
368
+ this.opts.dir,
369
+ renderTokenEnv({ token, expires_at: expiresAt, permissions: {} }, this.opts.repo, "user"),
370
+ );
371
+ return { kind: "copied", expiresAt };
372
+ } catch (err) {
373
+ return { kind: "failed", reason: err instanceof Error ? err.message : String(err) };
374
+ }
375
+ }
376
+
310
377
  /** Check every `everyMs`; each outcome goes to `onOutcome`. Idempotent. */
311
378
  start(onOutcome: (o: TokenOutcome) => void, everyMs: number = CHECK_EVERY_MS): void {
312
379
  if (this.timer) return;
package/src/tui/index.ts CHANGED
@@ -34,7 +34,10 @@ import {
34
34
  RELOAD_EXIT_CODE,
35
35
  resumeStatePath,
36
36
  } from "./reload.js";
37
- import { killInFlightSsh, runnerFor, transportOf } from "./remote.js";
37
+ import { NO_SSH_HOST_STDERR, killInFlightSsh, runnerFor, transportOf } from "./remote.js";
38
+ import { diagnoseLocalMemory } from "./memforward.js";
39
+ import { realProbeRunner } from "./sshtriage.js";
40
+ import { setupLine } from "./setupstate.js";
38
41
  import { chainOnPoll } from "./poll-hooks.js";
39
42
  import { deliverMessage } from "./msg.js";
40
43
  import type { ConnectionDiagnosis } from "./sshtriage.js";
@@ -60,7 +63,7 @@ export function parseTarget(
60
63
  return { kind: "bad", reason: `unknown target '${arg}' — expected a PR number` };
61
64
  }
62
65
 
63
- export const TUI_HELP = `qcr tui — live board and operator console for the review daemon (over ssh)
66
+ export const TUI_HELP = `qcr tui — live board and operator console for the review daemon (over ssh, or local docker)
64
67
 
65
68
  usage: qcr tui [<pr>] [--no-companion] [--no-reload]
66
69
 
@@ -95,7 +98,8 @@ keys: 1…8 the eight surfaces — board · memory · digest · tickets · deplo
95
98
  board: j/k select · ⏎ focus the PR's pi pane · m quick-message ·
96
99
  g github · u unwedge · e events overlay · I set up the repo ·
97
100
  Ctrl-q release focus · Ctrl-c ×3 hard-kill the TUI ·
98
- s reconnect ssh (GCP passphrase, once) · r refresh ·
101
+ s re-check the connection (VM: ssh, the one passphrase prompt;
102
+ local: the daemon container) · r refresh ·
99
103
  ? full key map · q quit
100
104
 
101
105
  troubleshooting:
@@ -106,6 +110,12 @@ troubleshooting:
106
110
  not answering. Nothing is run for you; s re-checks and
107
111
  reports, and offers the one GCP passphrase prompt when a
108
112
  key is what is missing
113
+ local route [tui] target = "local" (or an empty ssh_host on a compose
114
+ project): every read is docker exec into the daemon
115
+ container, no ssh. The board names an absent or stopped
116
+ container with the docker compose command that starts it
117
+ setup not finished the setup checklist is not complete: run qcr (the board
118
+ names the next step)
109
119
  ▲ DATA STALE ssh works but status.json stopped updating — the
110
120
  container is down or the daemon hung (qcr-side:
111
121
  ssh <host> sudo docker ps / docker logs)
@@ -355,6 +365,8 @@ export interface RunTuiDeps {
355
365
  input?: NodeJS.ReadStream;
356
366
  config?: Config;
357
367
  exit?: (code: number) => void;
368
+ /** The route facts (src/tui/route.ts); default: read from this checkout. */
369
+ routeFacts?: import("./route.js").RouteFacts;
358
370
  }
359
371
 
360
372
  /** First slice of the quit-time wait for an in-flight bring-up. */
@@ -440,7 +452,18 @@ export async function runTui(argv: string | string[] = [], deps: RunTuiDeps = {}
440
452
 
441
453
  // ---- phase 0: local only, ending in the first frame -----------------------
442
454
 
443
- const loaded = deps.config ?? loadConfig();
455
+ const fileConfig = deps.config ?? loadConfig();
456
+ // The route — which daemon the board reads, and how — from [tui] plus the
457
+ // facts the stack holds locally (the compose project's .env, the
458
+ // onboarding checklist, the Docker socket's mode). The stack is the one the
459
+ // home sets up (src/homes.ts `stackRoot`), so an app installed by a package
460
+ // manager reads it from its state home. Two small file reads and one
461
+ // access(2): no docker, network or walk, so it may precede the frame.
462
+ const { readRouteFacts, resolveTuiRoute, pinnedVersion } = await import("./route.js");
463
+ const { stackRoot } = await import("../homes.js");
464
+ const routeFacts = deps.routeFacts ?? readRouteFacts(stackRoot(process.env, sandboxRoot()));
465
+ const route = resolveTuiRoute(fileConfig.tui, routeFacts);
466
+ const loaded: Config = { ...fileConfig, tui: route.cfg };
444
467
  // In the containerised TUI the companion is a container the host wrapper
445
468
  // runs, reached through a shared tmux socket. Its channel dirs are the
446
469
  // host's, named by the wrapper in env; nothing here can run docker.
@@ -454,6 +477,11 @@ export async function runTui(argv: string | string[] = [], deps: RunTuiDeps = {}
454
477
  const runner = runnerFor(cfg);
455
478
 
456
479
  const app = new App(cfg, runner, out, input);
480
+ // A half-finished onboarding is stated on the first frame: the checklist is
481
+ // the file read above, and the step titles are the wizard's own registry.
482
+ const { setupStateOf } = await import("./setupstate.js");
483
+ app.view.setup = await setupStateOf(routeFacts.checklist);
484
+ for (const line of route.notes) app.notice(line);
457
485
 
458
486
  // The terminal must come back whatever kills us — raw mode + alt screen
459
487
  // surviving a crash is exactly the "only closing the terminal works" trap.
@@ -560,6 +588,17 @@ export async function runTui(argv: string | string[] = [], deps: RunTuiDeps = {}
560
588
  // (nativecompanion.ts). The containerised TUI keeps its wrapper-run
561
589
  // container: it has no Docker of its own to ask.
562
590
  const containerLifecycle = new CompanionLifecycle(full.companion, realDockerRunner);
591
+ // On a compose box the release image is already pulled (QCR_VERSION in the
592
+ // project .env): a first start uses it rather than building one from this
593
+ // checkout. An explicit [companion] image, or a local build already
594
+ // present, still wins (CompanionLifecycle.ensureUp).
595
+ {
596
+ const version = pinnedVersion(routeFacts.projectEnv);
597
+ if (version !== "") {
598
+ const { releaseImage } = await import("../onboard/steps/compose.js");
599
+ containerLifecycle.preferredImage = releaseImage(version);
600
+ }
601
+ }
563
602
  const selecting = socketMode
564
603
  ? undefined
565
604
  : new SelectingCompanion(containerLifecycle, realDockerRunner, {
@@ -593,6 +632,7 @@ export async function runTui(argv: string | string[] = [], deps: RunTuiDeps = {}
593
632
  dir: companionGithubDir(socketMode ? laptopInstallEnv(process.env) : undefined),
594
633
  repo: full.github.repo,
595
634
  secretsDir: full.github.secrets_dir,
635
+ auth: full.github.auth ?? "app",
596
636
  });
597
637
  let configMirrorTimer: NodeJS.Timeout | undefined;
598
638
  const CONFIG_MIRROR_EVERY_MS = 30_000;
@@ -620,8 +660,17 @@ export async function runTui(argv: string | string[] = [], deps: RunTuiDeps = {}
620
660
  // never written to the normal screen, where the alt screen's clear would
621
661
  // erase them (a visible flicker). With the companion off the same column
622
662
  // carries the skipped steps, and stays as the place that says why it is off.
623
- const { StartupProgress } = await import("./startup.js");
624
- const progress = new StartupProgress(undefined, {
663
+ const { StartupProgress, STARTUP_STEPS } = await import("./startup.js");
664
+ // The connection step is named for the route it checks: ssh to the VM, or
665
+ // the daemon container on this machine.
666
+ const startupSteps = STARTUP_STEPS.map((step) =>
667
+ step.key === "ssh" && transportOf(cfg) === "local"
668
+ ? { ...step, label: "daemon container" }
669
+ : step.key === "ssh" && route.unconfigured !== undefined
670
+ ? { ...step, label: "connection" }
671
+ : step,
672
+ );
673
+ const progress = new StartupProgress(startupSteps, {
625
674
  onUpdate: (p) => {
626
675
  // Kept after the bring-up settles on purpose: a `✗ companion container ·
627
676
  // docker is not running` is the most useful thing the column can show,
@@ -714,12 +763,20 @@ export async function runTui(argv: string | string[] = [], deps: RunTuiDeps = {}
714
763
  // `[openviking] endpoint = "remote"`, else the daemon container.
715
764
  container: tuiMemoryContainer(cfg),
716
765
  // A local engine runs in Docker; with none here, memory is off — said
717
- // once as the forward's state, never retried as a failure.
718
- ...(!socketMode && transportOf(cfg) === "local"
719
- ? {
720
- offReason: async () => ((await dockerUp()) ? undefined : MEMORY_OFF_NO_DOCKER),
721
- }
722
- : {}),
766
+ // once as the forward's state, never retried as a failure. With no ssh
767
+ // host there is nowhere to forward to: off, with the one remedy, rather
768
+ // than a relay dialing the script's default host.
769
+ ...(transportOf(cfg) === "ssh" && cfg.ssh_host.trim() === ""
770
+ ? { offReason: () => route.unconfigured ?? NO_SSH_HOST_STDERR }
771
+ : !socketMode && transportOf(cfg) === "local"
772
+ ? {
773
+ offReason: async () => ((await dockerUp()) ? undefined : MEMORY_OFF_NO_DOCKER),
774
+ // A forward that binds but reaches nothing: say whether the memory
775
+ // container is there at all, and how it is started.
776
+ diagnoseUpstream: () =>
777
+ diagnoseLocalMemory(cfg, routeFacts.projectDir, realProbeRunner),
778
+ }
779
+ : {}),
723
780
  log: (line) => app.notice(line),
724
781
  });
725
782
  const { buildIssueSink, ticketProjection } = await import("./issuesink.js");
@@ -1005,12 +1062,26 @@ export async function runTui(argv: string | string[] = [], deps: RunTuiDeps = {}
1005
1062
  // gating the first paint and, on a bad day, an interactive prompt. Nothing
1006
1063
  // here is self-healing by design; the TUI reports the cause and the exact
1007
1064
  // command, and `s` re-runs the check.
1008
- const { diagnosisLine, triageConnection } = await import("./sshtriage.js");
1065
+ const { connectionUsable, diagnosisLine, triageConnection } = await import("./sshtriage.js");
1066
+ const triageDeps = {
1067
+ projectDir: routeFacts.projectDir,
1068
+ ...(route.unconfigured !== undefined ? { unconfigured: route.unconfigured } : {}),
1069
+ };
1009
1070
  const applyDiagnosis = (d: ConnectionDiagnosis): void => {
1010
1071
  app.view.connection = d;
1011
- if (d.kind === "ok" || d.kind === "local") progress.done("ssh", d.cause);
1072
+ const usable = connectionUsable(d);
1073
+ // Under an unfinished onboarding a stack that is not up yet is expected,
1074
+ // not a fault: the step is skipped with the setup line, never failed.
1075
+ if (usable) progress.done("ssh", d.cause);
1076
+ else if (app.view.setup) progress.skip("ssh", setupLine(app.view.setup));
1012
1077
  else progress.fail("ssh", diagnosisLine(d));
1013
- app.notice(d.kind === "ok" || d.kind === "local" ? `✓ ${d.cause}` : `✖ ${diagnosisLine(d)}`);
1078
+ app.notice(
1079
+ usable
1080
+ ? `✓ ${d.cause}`
1081
+ : app.view.setup
1082
+ ? setupLine(app.view.setup)
1083
+ : `✖ ${diagnosisLine(d)}`,
1084
+ );
1014
1085
  app.repaint();
1015
1086
  };
1016
1087
  // `s` starts its own async chain, independent of the bring-up. Tracked for
@@ -1018,9 +1089,13 @@ export async function runTui(argv: string | string[] = [], deps: RunTuiDeps = {}
1018
1089
  let pendingReconnect: Promise<void> = Promise.resolve();
1019
1090
  app.hooks.reconnect = () => {
1020
1091
  progress.begin("ssh", "re-checking");
1021
- app.notice("checking the connection to the VM…");
1092
+ app.notice(
1093
+ transportOf(cfg) === "local"
1094
+ ? "checking the daemon container…"
1095
+ : "checking the connection to the VM…",
1096
+ );
1022
1097
  pendingReconnect = (async () => {
1023
- let d = await triageConnection(cfg);
1098
+ let d = await triageConnection(cfg, triageDeps);
1024
1099
  // A missing key is the one class the operator can clear from here: `s`
1025
1100
  // is them asking for the single native passphrase prompt. A wedged
1026
1101
  // master or a dead host is reported with its command instead — prompting
@@ -1029,7 +1104,7 @@ export async function runTui(argv: string | string[] = [], deps: RunTuiDeps = {}
1029
1104
  onRealTerminal(() => {
1030
1105
  ensureSshMaster(cfg);
1031
1106
  });
1032
- d = await triageConnection(cfg);
1107
+ d = await triageConnection(cfg, triageDeps);
1033
1108
  }
1034
1109
  applyDiagnosis(d);
1035
1110
  // Awaited, not fired and forgotten: it is part of "re-run the check and
@@ -1397,7 +1472,7 @@ export async function runTui(argv: string | string[] = [], deps: RunTuiDeps = {}
1397
1472
  // says "nobody has reached this yet", which would be a lie the moment the
1398
1473
  // probes start.
1399
1474
  progress.begin("ssh");
1400
- const triaged = triageConnection(cfg)
1475
+ const triaged = triageConnection(cfg, triageDeps)
1401
1476
  .then(applyDiagnosis)
1402
1477
  .catch((err: unknown) =>
1403
1478
  progress.fail("ssh", `probe failed: ${String(err instanceof Error ? err.message : err)}`),
@@ -1439,12 +1514,31 @@ export async function runTui(argv: string | string[] = [], deps: RunTuiDeps = {}
1439
1514
  return;
1440
1515
  }
1441
1516
  if (step?.state === "running" && a.poller.state.consecutiveFailures >= 2) {
1442
- progress.fail(
1443
- "poll",
1444
- app.view.connection ? diagnosisLine(app.view.connection) : "status.json unreadable",
1445
- );
1517
+ if (app.view.setup) progress.skip("poll", setupLine(app.view.setup));
1518
+ else
1519
+ progress.fail(
1520
+ "poll",
1521
+ app.view.connection ? diagnosisLine(app.view.connection) : "status.json unreadable",
1522
+ );
1446
1523
  }
1447
1524
  });
1525
+ // The checklist moves while the wizard runs in another terminal: re-read it
1526
+ // each poll (one small file), and recompute the next step only when it
1527
+ // changed. Finishing onboarding clears the banner on its own.
1528
+ {
1529
+ const { readChecklist } = await import("../onboard/engine.js");
1530
+ let seen = routeFacts.checklist?.updated_at ?? null;
1531
+ chainOnPoll(app, () => {
1532
+ const now = readChecklist(join(routeFacts.projectDir, "state"));
1533
+ const stamp = now?.updated_at ?? null;
1534
+ if (stamp === seen) return;
1535
+ seen = stamp;
1536
+ void setupStateOf(now).then((next) => {
1537
+ app.view.setup = next;
1538
+ app.repaint();
1539
+ });
1540
+ });
1541
+ }
1448
1542
 
1449
1543
  // Restore a hot-reload snapshot before run(), so the first paint is already
1450
1544
  // on the right tab with the right selection.
@@ -40,6 +40,9 @@
40
40
  // status` reads them) rather than painted as zero.
41
41
  import { pathToFileURL } from "node:url";
42
42
  import { join } from "node:path";
43
+ import type { TuiConfig } from "../config.js";
44
+ import { shellQuote } from "./remote.js";
45
+ import type { ProbeRunner } from "./sshtriage.js";
43
46
 
44
47
  /** The container OpenViking runs in, as the TUI and the memory forward reach
45
48
  * it: the memory sidecar when `[tui] memory_container` names one, else the
@@ -50,6 +53,33 @@ export function tuiMemoryContainer(tui: { container: string; memory_container?:
50
53
  return named !== "" ? named : tui.container;
51
54
  }
52
55
 
56
+ /**
57
+ * The local route's account of the memory container, for a forward that
58
+ * binds but reaches nothing: absent, stopped, or running but not answering
59
+ * yet. One `docker inspect`; undefined when docker itself gave no usable
60
+ * answer (the generic line stands).
61
+ */
62
+ export async function diagnoseLocalMemory(
63
+ cfg: Pick<TuiConfig, "container" | "memory_container" | "sudo_docker">,
64
+ projectDir: string,
65
+ run: ProbeRunner,
66
+ ): Promise<string | undefined> {
67
+ const container = tuiMemoryContainer(cfg);
68
+ const inspect = ["inspect", "-f", "{{.State.Status}}", container];
69
+ const res = cfg.sudo_docker
70
+ ? await run("sudo", ["-n", "docker", ...inspect], 5_000)
71
+ : await run("docker", inspect, 5_000);
72
+ const up = `docker compose --project-directory ${shellQuote(projectDir)} up -d memory`;
73
+ const state = res.stdout.trim();
74
+ if (res.status === 0 && state === "running")
75
+ return `the memory container ${container} is running but OpenViking is not answering yet (it takes a minute after a start) — r retries`;
76
+ if (res.status === 0)
77
+ return `the memory container ${container} is ${state || "not running"} — start it: ${up}`;
78
+ if (/no such (object|container)/i.test(`${res.stderr}\n${res.stdout}`))
79
+ return `the memory container ${container} is not running — it starts at onboarding's bring-up step, or: ${up}`;
80
+ return undefined;
81
+ }
82
+
53
83
  /** What the forward's `stats()` reports — the fields painted. */
54
84
  export interface ForwardStats {
55
85
  maxRelays: number;
@@ -166,6 +196,10 @@ export interface MemForwardDeps {
166
196
  * forward `off` with it as the detail — no probe, no bind, no retry, and
167
197
  * nothing that reads as a failure. */
168
198
  offReason?: () => string | undefined | Promise<string | undefined>;
199
+ /** Asked when the forward listens but nothing answers through it: a precise
200
+ * account of the upstream (the memory container absent, stopped, still
201
+ * starting), or undefined to keep the generic line. Never throws. */
202
+ diagnoseUpstream?: () => Promise<string | undefined>;
169
203
  }
170
204
 
171
205
  const DEFAULT_PROBE_TIMEOUT_MS = 1_500;
@@ -195,12 +229,13 @@ async function httpProbe(baseUrl: string, timeoutMs: number): Promise<boolean> {
195
229
  */
196
230
  export class MemoryForward {
197
231
  private readonly deps: Required<
198
- Omit<MemForwardDeps, "env" | "sshHost" | "container" | "offReason">
232
+ Omit<MemForwardDeps, "env" | "sshHost" | "container" | "offReason" | "diagnoseUpstream">
199
233
  > & {
200
234
  env: NodeJS.ProcessEnv;
201
235
  sshHost?: string;
202
236
  container?: string;
203
237
  offReason?: MemForwardDeps["offReason"];
238
+ diagnoseUpstream?: MemForwardDeps["diagnoseUpstream"];
204
239
  };
205
240
  private handle: ForwarderHandle | null = null;
206
241
  /** How many of its addresses our forward bound, kept with the handle so
@@ -228,6 +263,7 @@ export class MemoryForward {
228
263
  ...(deps.sshHost ? { sshHost: deps.sshHost } : {}),
229
264
  ...(deps.container ? { container: deps.container } : {}),
230
265
  ...(deps.offReason ? { offReason: deps.offReason } : {}),
266
+ ...(deps.diagnoseUpstream ? { diagnoseUpstream: deps.diagnoseUpstream } : {}),
231
267
  };
232
268
  this.status = {
233
269
  state: "off",
@@ -295,7 +331,8 @@ export class MemoryForward {
295
331
  alive ? "owned" : "failed",
296
332
  alive
297
333
  ? this.ownedDetail(this.binds?.bound, this.binds?.wanted)
298
- : "the forward we started stopped answering — r retries",
334
+ : ((await this.upstreamDetail()) ??
335
+ "the forward we started stopped answering — r retries"),
299
336
  );
300
337
  }
301
338
  if (await this.deps.probe(this.deps.baseUrl, this.deps.probeTimeoutMs)) {
@@ -367,7 +404,10 @@ export class MemoryForward {
367
404
  if (!(await this.deps.probe(this.deps.baseUrl, this.deps.probeTimeoutMs))) {
368
405
  return this.settle(
369
406
  "failed",
370
- "the forward we started is listening but upstream is not answering (ssh, VM container or OpenViking) — r retries",
407
+ (await this.upstreamDetail()) ??
408
+ (this.deps.local
409
+ ? "the forward we started is listening but upstream is not answering (the memory container or OpenViking) — r retries"
410
+ : "the forward we started is listening but upstream is not answering (ssh, VM container or OpenViking) — r retries"),
371
411
  );
372
412
  }
373
413
  return this.settle("owned", this.ownedDetail(bound, cfg.binds.length));
@@ -400,6 +440,15 @@ export class MemoryForward {
400
440
  return this.inFlight;
401
441
  }
402
442
 
443
+ /** The upstream's own account, when one was wired; total. */
444
+ private async upstreamDetail(): Promise<string | undefined> {
445
+ try {
446
+ return await this.deps.diagnoseUpstream?.();
447
+ } catch {
448
+ return undefined;
449
+ }
450
+ }
451
+
403
452
  /** A partial bind is a working state, not a failure: loopback alone serves
404
453
  * the TUI, and the docker-bridge bind only matters when the companion
405
454
  * container is up. Say which, rather than reporting a bare "ok". */
package/src/tui/memory.ts CHANGED
@@ -52,7 +52,7 @@ import {
52
52
  import { countOutcome, emptyRequestCounter, type RequestCounter } from "../memory/health.js";
53
53
  import { OpenVikingStore } from "../openviking/store.js";
54
54
  import { readRetiredUrisFile, writeRetiredUris } from "../memory/ovsession.js";
55
- import { keysFilePath, MEMORY } from "./companion.js";
55
+ import { keysFilePath } from "./companion.js";
56
56
 
57
57
  export { matchesQuery, normalizeTag, refOf } from "../memory/store.js";
58
58
  export type {
@@ -227,20 +227,8 @@ export function keysFileReader(
227
227
  };
228
228
  }
229
229
 
230
- /**
231
- * Where the TUI reaches OpenViking: the memory forward's loopback listener.
232
- *
233
- * `QCR_MEMORY_URL` overrides it wholesale; `QCR_MEMORY_PORT` matches the flag
234
- * the forward itself takes, so a non-default port only has to be spelled once.
235
- * `MEMORY.port` is the shared constant the forward and the companion already
236
- * agree on.
237
- */
238
- export function tuiMemoryBaseUrl(env: NodeJS.ProcessEnv = process.env): string {
239
- const explicit = (env["QCR_MEMORY_URL"] ?? env["QCR_MEMORY_ENDPOINT"] ?? "").trim();
240
- if (explicit !== "") return explicit.replace(/\/+$/, "");
241
- const port = Number(env["QCR_MEMORY_PORT"] ?? MEMORY.port) || MEMORY.port;
242
- return `http://127.0.0.1:${port}`;
243
- }
230
+ import { tuiMemoryBaseUrl } from "./memoryurl.js";
231
+ export { tuiMemoryBaseUrl };
244
232
 
245
233
  // ---- the store ---------------------------------------------------------------
246
234
 
@@ -0,0 +1,22 @@
1
+ // Where this host reaches OpenViking: the memory forward's loopback
2
+ // listener. A leaf module, so a reader of the URL (the wizard's memory step,
3
+ // the home) never imports the TUI's memory store or the companion.
4
+
5
+ /** The port the memory forward and the companion agree on, the server's own
6
+ * in the VM container (`[openviking] port`). */
7
+ export const MEMORY_PORT = 1933;
8
+
9
+ /**
10
+ * Where the TUI reaches OpenViking: the memory forward's loopback listener.
11
+ *
12
+ * `QCR_MEMORY_URL` overrides it wholesale; `QCR_MEMORY_PORT` matches the flag
13
+ * the forward itself takes, so a non-default port only has to be spelled once.
14
+ * `MEMORY_PORT` is the shared constant the forward and the companion already
15
+ * agree on.
16
+ */
17
+ export function tuiMemoryBaseUrl(env: NodeJS.ProcessEnv = process.env): string {
18
+ const explicit = (env["QCR_MEMORY_URL"] ?? env["QCR_MEMORY_ENDPOINT"] ?? "").trim();
19
+ if (explicit !== "") return explicit.replace(/\/+$/, "");
20
+ const port = Number(env["QCR_MEMORY_PORT"] ?? MEMORY_PORT) || MEMORY_PORT;
21
+ return `http://127.0.0.1:${port}`;
22
+ }
package/src/tui/model.ts CHANGED
@@ -3,7 +3,16 @@
3
3
  // selection movement.
4
4
  import type { PrLifecycleState, StatusSnapshot } from "../state.js";
5
5
 
6
- export type SnapshotPr = StatusSnapshot["prs"][number];
6
+ export type SnapshotPr = StatusSnapshot["prs"][number] & {
7
+ /** A board-only row: an open PR of the repo the daemon does not track yet
8
+ * (status.json `open_prs`). It is offered for review (`R`); nothing about it
9
+ * is known beyond what the PR list says, so no PR verb but `R` and `g`
10
+ * acts on it. */
11
+ offered?: true;
12
+ };
13
+
14
+ /** The glyph of an offered row: open on GitHub, not reviewed yet. */
15
+ export const OFFERED_GLYPH = "○";
7
16
 
8
17
  export const STATE_GLYPHS: Record<PrLifecycleState, string> = {
9
18
  queued: "●",
@@ -14,7 +23,8 @@ export const STATE_GLYPHS: Record<PrLifecycleState, string> = {
14
23
  };
15
24
 
16
25
  /** `↻` prefixes the state glyph when a stale recheck is pending. */
17
- export function glyphFor(pr: Pick<SnapshotPr, "state" | "stale_recheck">): string {
26
+ export function glyphFor(pr: Pick<SnapshotPr, "state" | "stale_recheck" | "offered">): string {
27
+ if (pr.offered) return OFFERED_GLYPH;
18
28
  const base = STATE_GLYPHS[pr.state] ?? "?";
19
29
  return pr.stale_recheck ? `↻${base}` : base;
20
30
  }
@@ -79,6 +89,30 @@ export function sortPrs(prs: SnapshotPr[]): SnapshotPr[] {
79
89
  .sort((a, b) => rank(a.state) - rank(b.state) || a.number - b.number);
80
90
  }
81
91
 
92
+ /** The repo's open PRs the daemon does not track (status.json `open_prs`),
93
+ * newest activity first, as offered board rows. A tracked PR is never offered
94
+ * twice: its own row is the one the verbs act on. */
95
+ export function offeredPrs(
96
+ snapshot: Pick<StatusSnapshot, "prs" | "open_prs"> | undefined,
97
+ ): SnapshotPr[] {
98
+ const open = snapshot?.open_prs?.prs ?? [];
99
+ const tracked = new Set((snapshot?.prs ?? []).filter(onBoard).map((p) => p.number));
100
+ return open
101
+ .filter((p) => !tracked.has(p.number))
102
+ .slice()
103
+ .sort((a, b) => b.updated_at.localeCompare(a.updated_at) || b.number - a.number)
104
+ .map((p) => ({
105
+ number: p.number,
106
+ state: "tracked" as const,
107
+ title: p.draft ? `[draft] ${p.title}` : p.title,
108
+ head_sha: "",
109
+ stale_recheck: 0,
110
+ author: p.author,
111
+ updated_at: p.updated_at,
112
+ offered: true as const,
113
+ }));
114
+ }
115
+
82
116
  export interface EventLine {
83
117
  ts: string; // HH:MM:SS
84
118
  text: string;
@@ -20,8 +20,10 @@ import type { SshRunner } from "./remote.js";
20
20
  import { commandResultArgv, commandWriteArgv, resultReason } from "./unwedge.js";
21
21
  import { resolveLayout } from "./layout.js";
22
22
 
23
- /** The two payloads, read from the daemon's own `Command`. */
24
- export type PrLeverCommand = Extract<Command, { command: "pause-pr" | "resume-pr" }>;
23
+ /** The payloads, read from the daemon's own `Command`: the pause pair, and
24
+ * `review-now` (one PR queued for a human-asked round), which rides the same
25
+ * sealed channel and the same receipt. */
26
+ export type PrLeverCommand = Extract<Command, { command: "pause-pr" | "resume-pr" | "review-now" }>;
25
27
 
26
28
  export interface PrLeverOutcome {
27
29
  ok: boolean;
@@ -132,6 +134,13 @@ export function renderPrLever(
132
134
  return { lines, code: 1 };
133
135
  }
134
136
  if (cmd.command === "resume-pr") return { lines: [`✓ ${pr} resumed`], code: 0 };
137
+ if (cmd.command === "review-now")
138
+ return {
139
+ lines: [
140
+ `✓ ${pr} queued for review${out.outcome ? ` (${out.outcome})` : ""} — follow it with \`qcr why ${cmd.pr}\` or on the board`,
141
+ ],
142
+ code: 0,
143
+ };
135
144
  if (out.issuer !== "operator") {
136
145
  return {
137
146
  lines: [