@quickchatai/quickreview 1.1.0 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (471) hide show
  1. package/README.md +137 -57
  2. package/bin/qcr.js +1 -1
  3. package/config.example.toml +60 -14
  4. package/deploy/ansible/README.md +20 -12
  5. package/deploy/ansible/inventory.example.ini +1 -1
  6. package/deploy/collect-secrets.sh +1 -1
  7. package/deploy/qcr-update.sh +1 -1
  8. package/dist/extensions/cli/home.js +14 -0
  9. package/dist/extensions/cli/home.js.map +1 -0
  10. package/dist/extensions/cli/login-route.js +5 -0
  11. package/dist/extensions/cli/login-route.js.map +1 -0
  12. package/dist/extensions/cli/selfimprove.js +18 -0
  13. package/dist/extensions/cli/selfimprove.js.map +1 -0
  14. package/dist/extensions/home.js +350 -0
  15. package/dist/extensions/home.js.map +1 -0
  16. package/dist/extensions/login-route.js +180 -0
  17. package/dist/extensions/login-route.js.map +1 -0
  18. package/dist/extensions/onboarding.js +35 -11
  19. package/dist/extensions/onboarding.js.map +1 -1
  20. package/dist/extensions/selfimprove.js +49 -0
  21. package/dist/extensions/selfimprove.js.map +1 -0
  22. package/dist/extensions/setup-scan.js +23 -592
  23. package/dist/extensions/setup-scan.js.map +1 -1
  24. package/dist/extensions/setup.js +2 -47
  25. package/dist/extensions/setup.js.map +1 -1
  26. package/dist/src/archive/restore.js +18 -15
  27. package/dist/src/archive/restore.js.map +1 -1
  28. package/dist/src/botworker.js +13 -2
  29. package/dist/src/botworker.js.map +1 -1
  30. package/dist/src/channelfs.js +3 -26
  31. package/dist/src/channelfs.js.map +1 -1
  32. package/dist/src/ciretry.js +2 -1
  33. package/dist/src/ciretry.js.map +1 -1
  34. package/dist/src/cli.js +54 -61
  35. package/dist/src/cli.js.map +1 -1
  36. package/dist/src/cliops.js +3 -2
  37. package/dist/src/cliops.js.map +1 -1
  38. package/dist/src/commands.js +25 -2
  39. package/dist/src/commands.js.map +1 -1
  40. package/dist/src/config.js +32 -2
  41. package/dist/src/config.js.map +1 -1
  42. package/dist/src/daemon.js +50 -8
  43. package/dist/src/daemon.js.map +1 -1
  44. package/dist/src/debuground.js +1 -0
  45. package/dist/src/debuground.js.map +1 -1
  46. package/dist/src/debugroundpi.js +6 -1
  47. package/dist/src/debugroundpi.js.map +1 -1
  48. package/dist/src/dispatch.js +5 -4
  49. package/dist/src/dispatch.js.map +1 -1
  50. package/dist/src/engine/caserunner-sdk.js +15 -251
  51. package/dist/src/engine/caserunner-sdk.js.map +1 -1
  52. package/dist/src/engine/debugpi.js +17 -5
  53. package/dist/src/engine/debugpi.js.map +1 -1
  54. package/dist/src/execboundary.js +10 -3
  55. package/dist/src/execboundary.js.map +1 -1
  56. package/dist/src/github.js +51 -16
  57. package/dist/src/github.js.map +1 -1
  58. package/dist/src/githubtoken.js +346 -0
  59. package/dist/src/githubtoken.js.map +1 -0
  60. package/dist/src/home/adapters.js +349 -0
  61. package/dist/src/home/adapters.js.map +1 -0
  62. package/dist/src/home/advanced.js +318 -0
  63. package/dist/src/home/advanced.js.map +1 -0
  64. package/dist/src/home/advancedtools.js +57 -0
  65. package/dist/src/home/advancedtools.js.map +1 -0
  66. package/dist/src/home/autoreview.js +325 -0
  67. package/dist/src/home/autoreview.js.map +1 -0
  68. package/dist/src/home/daemonstatus.js +32 -0
  69. package/dist/src/home/daemonstatus.js.map +1 -0
  70. package/dist/src/home/devicelogin.js +61 -0
  71. package/dist/src/home/devicelogin.js.map +1 -0
  72. package/dist/src/home/launch.js +202 -0
  73. package/dist/src/home/launch.js.map +1 -0
  74. package/dist/src/home/loginowner.js +205 -0
  75. package/dist/src/home/loginowner.js.map +1 -0
  76. package/dist/src/home/memory.js +122 -0
  77. package/dist/src/home/memory.js.map +1 -0
  78. package/dist/src/home/model.js +811 -0
  79. package/dist/src/home/model.js.map +1 -0
  80. package/dist/src/home/panel.js +520 -0
  81. package/dist/src/home/panel.js.map +1 -0
  82. package/dist/src/home/ports.js +19 -0
  83. package/dist/src/home/ports.js.map +1 -0
  84. package/dist/src/home/review.js +163 -0
  85. package/dist/src/home/review.js.map +1 -0
  86. package/dist/src/home/reviews.js +478 -0
  87. package/dist/src/home/reviews.js.map +1 -0
  88. package/dist/src/home/setup.js +507 -0
  89. package/dist/src/home/setup.js.map +1 -0
  90. package/dist/src/home/setupadapters.js +214 -0
  91. package/dist/src/home/setupadapters.js.map +1 -0
  92. package/dist/src/home/setupcard.js +199 -0
  93. package/dist/src/home/setupcard.js.map +1 -0
  94. package/dist/src/home/setuptools.js +45 -0
  95. package/dist/src/home/setuptools.js.map +1 -0
  96. package/dist/src/home/spend.js +160 -0
  97. package/dist/src/home/spend.js.map +1 -0
  98. package/dist/src/home/switches.js +209 -0
  99. package/dist/src/home/switches.js.map +1 -0
  100. package/dist/src/home/switchon.js +439 -0
  101. package/dist/src/home/switchon.js.map +1 -0
  102. package/dist/src/home/toolnames.js +41 -0
  103. package/dist/src/home/toolnames.js.map +1 -0
  104. package/dist/src/home/tune.js +341 -0
  105. package/dist/src/home/tune.js.map +1 -0
  106. package/dist/src/home/words.js +7 -0
  107. package/dist/src/home/words.js.map +1 -0
  108. package/dist/src/homes.js +14 -0
  109. package/dist/src/homes.js.map +1 -1
  110. package/dist/src/install.js +1 -1
  111. package/dist/src/install.js.map +1 -1
  112. package/dist/src/ipc/workerspec.js +8 -0
  113. package/dist/src/ipc/workerspec.js.map +1 -1
  114. package/dist/src/live/publish.js +1 -1
  115. package/dist/src/live/publish.js.map +1 -1
  116. package/dist/src/livecli.js +1 -1
  117. package/dist/src/livecli.js.map +1 -1
  118. package/dist/src/loginhint.js +8 -3
  119. package/dist/src/loginhint.js.map +1 -1
  120. package/dist/src/loginrefresh/handover.js +121 -0
  121. package/dist/src/loginrefresh/handover.js.map +1 -0
  122. package/dist/src/loginrefresh/lock.js +54 -3
  123. package/dist/src/loginrefresh/lock.js.map +1 -1
  124. package/dist/src/loginrefresh/pirefresh.js +3 -0
  125. package/dist/src/loginrefresh/pirefresh.js.map +1 -1
  126. package/dist/src/loginrefresh/refresher.js +33 -5
  127. package/dist/src/loginrefresh/refresher.js.map +1 -1
  128. package/dist/src/loginrefresh/supervisor.js +18 -1
  129. package/dist/src/loginrefresh/supervisor.js.map +1 -1
  130. package/dist/src/npmverbs.js +46 -14
  131. package/dist/src/npmverbs.js.map +1 -1
  132. package/dist/src/onboard/choices.js +35 -4
  133. package/dist/src/onboard/choices.js.map +1 -1
  134. package/dist/src/onboard/engine.js +32 -306
  135. package/dist/src/onboard/engine.js.map +1 -1
  136. package/dist/src/onboard/pipin.js +22 -0
  137. package/dist/src/onboard/pipin.js.map +1 -0
  138. package/dist/src/onboard/stepforward.js +41 -0
  139. package/dist/src/onboard/stepforward.js.map +1 -0
  140. package/dist/src/onboard/steps/appslug.js +4 -1
  141. package/dist/src/onboard/steps/appslug.js.map +1 -1
  142. package/dist/src/onboard/steps/bringup.js +3 -2
  143. package/dist/src/onboard/steps/bringup.js.map +1 -1
  144. package/dist/src/onboard/steps/compose.js +29 -0
  145. package/dist/src/onboard/steps/compose.js.map +1 -1
  146. package/dist/src/onboard/steps/configfill.js +120 -39
  147. package/dist/src/onboard/steps/configfill.js.map +1 -1
  148. package/dist/src/onboard/steps/githubaccount.js +595 -0
  149. package/dist/src/onboard/steps/githubaccount.js.map +1 -0
  150. package/dist/src/onboard/steps/githubapp.js +65 -7
  151. package/dist/src/onboard/steps/githubapp.js.map +1 -1
  152. package/dist/src/onboard/steps/golive.js +2 -0
  153. package/dist/src/onboard/steps/golive.js.map +1 -1
  154. package/dist/src/onboard/steps/index.js +11 -0
  155. package/dist/src/onboard/steps/index.js.map +1 -1
  156. package/dist/src/onboard/steps/ingress.js +80 -25
  157. package/dist/src/onboard/steps/ingress.js.map +1 -1
  158. package/dist/src/onboard/steps/llmauth.js +103 -30
  159. package/dist/src/onboard/steps/llmauth.js.map +1 -1
  160. package/dist/src/onboard/steps/loginshare.js +213 -0
  161. package/dist/src/onboard/steps/loginshare.js.map +1 -0
  162. package/dist/src/onboard/steps/memory.js +67 -3
  163. package/dist/src/onboard/steps/memory.js.map +1 -1
  164. package/dist/src/onboard/steps/proof.js +366 -56
  165. package/dist/src/onboard/steps/proof.js.map +1 -1
  166. package/dist/src/onboard/steps/repoconfig.js +72 -0
  167. package/dist/src/onboard/steps/repoconfig.js.map +1 -0
  168. package/dist/src/onboard/steps/topology.js +6 -6
  169. package/dist/src/onboard/steps/topology.js.map +1 -1
  170. package/dist/src/onboard/steps/transport.js +9 -1
  171. package/dist/src/onboard/steps/transport.js.map +1 -1
  172. package/dist/src/onboard/steps/tuiroute.js +102 -0
  173. package/dist/src/onboard/steps/tuiroute.js.map +1 -0
  174. package/dist/src/onboard/steps/verifyup.js +17 -1
  175. package/dist/src/onboard/steps/verifyup.js.map +1 -1
  176. package/dist/src/onboard/toolnames.js +30 -0
  177. package/dist/src/onboard/toolnames.js.map +1 -0
  178. package/dist/src/operatorwrite.js +23 -0
  179. package/dist/src/operatorwrite.js.map +1 -0
  180. package/dist/src/outcomes.js +4 -1
  181. package/dist/src/outcomes.js.map +1 -1
  182. package/dist/src/piagentdir.js +13 -0
  183. package/dist/src/piagentdir.js.map +1 -0
  184. package/dist/src/pilogin.js +31 -16
  185. package/dist/src/pilogin.js.map +1 -1
  186. package/dist/src/posting-journal.js +2 -1
  187. package/dist/src/posting-journal.js.map +1 -1
  188. package/dist/src/posting-outbox.js +6 -2
  189. package/dist/src/posting-outbox.js.map +1 -1
  190. package/dist/src/posting-receipt.js +14 -1
  191. package/dist/src/posting-receipt.js.map +1 -1
  192. package/dist/src/qcrbin.js +16 -11
  193. package/dist/src/qcrbin.js.map +1 -1
  194. package/dist/src/recheck.js +2 -1
  195. package/dist/src/recheck.js.map +1 -1
  196. package/dist/src/reconcile.js +22 -2
  197. package/dist/src/reconcile.js.map +1 -1
  198. package/dist/src/reviewargs.js +122 -0
  199. package/dist/src/reviewargs.js.map +1 -0
  200. package/dist/src/reviewcli.js +7 -161
  201. package/dist/src/reviewcli.js.map +1 -1
  202. package/dist/src/reviewdetach.js +76 -11
  203. package/dist/src/reviewdetach.js.map +1 -1
  204. package/dist/src/reviewhistory.js +32 -6
  205. package/dist/src/reviewhistory.js.map +1 -1
  206. package/dist/src/reviewnow.js +45 -0
  207. package/dist/src/reviewnow.js.map +1 -0
  208. package/dist/src/scancli.js +39 -691
  209. package/dist/src/scancli.js.map +1 -1
  210. package/dist/src/selfimprove.js +66 -0
  211. package/dist/src/selfimprove.js.map +1 -0
  212. package/dist/src/setup/adapters.js +5 -223
  213. package/dist/src/setup/adapters.js.map +1 -1
  214. package/dist/src/setup/caserunner.js +15 -249
  215. package/dist/src/setup/caserunner.js.map +1 -1
  216. package/dist/src/setup/engine.js +32 -341
  217. package/dist/src/setup/engine.js.map +1 -1
  218. package/dist/src/setup/facts.js +3 -592
  219. package/dist/src/setup/facts.js.map +1 -1
  220. package/dist/src/setup/findings.js +3 -947
  221. package/dist/src/setup/findings.js.map +1 -1
  222. package/dist/src/setup/presentation.js +3 -8
  223. package/dist/src/setup/presentation.js.map +1 -1
  224. package/dist/src/setup/scan.js +3 -724
  225. package/dist/src/setup/scan.js.map +1 -1
  226. package/dist/src/setup/scanbuild.js +9 -92
  227. package/dist/src/setup/scanbuild.js.map +1 -1
  228. package/dist/src/setup/scanlease.js +3 -250
  229. package/dist/src/setup/scanlease.js.map +1 -1
  230. package/dist/src/setup/scanreport.js +3 -288
  231. package/dist/src/setup/scanreport.js.map +1 -1
  232. package/dist/src/setup/scanrun.js +3 -1732
  233. package/dist/src/setup/scanrun.js.map +1 -1
  234. package/dist/src/setup/scantree.js +3 -256
  235. package/dist/src/setup/scantree.js.map +1 -1
  236. package/dist/src/setup/stage2request.js +3 -258
  237. package/dist/src/setup/stage2request.js.map +1 -1
  238. package/dist/src/setup/unitcut.js +3 -436
  239. package/dist/src/setup/unitcut.js.map +1 -1
  240. package/dist/src/state.js +2 -0
  241. package/dist/src/state.js.map +1 -1
  242. package/dist/src/statemachine.js +4 -1
  243. package/dist/src/statemachine.js.map +1 -1
  244. package/dist/src/takeover.js +1 -0
  245. package/dist/src/takeover.js.map +1 -1
  246. package/dist/src/takeoverpi.js +15 -1
  247. package/dist/src/takeoverpi.js.map +1 -1
  248. package/dist/src/tui/app.js +49 -0
  249. package/dist/src/tui/app.js.map +1 -1
  250. package/dist/src/tui/board.js +88 -12
  251. package/dist/src/tui/board.js.map +1 -1
  252. package/dist/src/tui/cmdbar.js +4 -1
  253. package/dist/src/tui/cmdbar.js.map +1 -1
  254. package/dist/src/tui/companion.js +76 -11
  255. package/dist/src/tui/companion.js.map +1 -1
  256. package/dist/src/tui/connect.js +7 -1
  257. package/dist/src/tui/connect.js.map +1 -1
  258. package/dist/src/tui/deploytab.js +2 -2
  259. package/dist/src/tui/deploytab.js.map +1 -1
  260. package/dist/src/tui/filters.js +5 -2
  261. package/dist/src/tui/filters.js.map +1 -1
  262. package/dist/src/tui/ghread.js +61 -5
  263. package/dist/src/tui/ghread.js.map +1 -1
  264. package/dist/src/tui/index.js +109 -20
  265. package/dist/src/tui/index.js.map +1 -1
  266. package/dist/src/tui/memforward.js +39 -2
  267. package/dist/src/tui/memforward.js.map +1 -1
  268. package/dist/src/tui/memory.js +3 -16
  269. package/dist/src/tui/memory.js.map +1 -1
  270. package/dist/src/tui/memoryurl.js +22 -0
  271. package/dist/src/tui/memoryurl.js.map +1 -0
  272. package/dist/src/tui/model.js +25 -0
  273. package/dist/src/tui/model.js.map +1 -1
  274. package/dist/src/tui/prpause.js +7 -0
  275. package/dist/src/tui/prpause.js.map +1 -1
  276. package/dist/src/tui/remote.js +7 -1
  277. package/dist/src/tui/remote.js.map +1 -1
  278. package/dist/src/tui/route.js +143 -0
  279. package/dist/src/tui/route.js.map +1 -0
  280. package/dist/src/tui/setupstate.js +32 -0
  281. package/dist/src/tui/setupstate.js.map +1 -0
  282. package/dist/src/tui/sshtriage.js +102 -5
  283. package/dist/src/tui/sshtriage.js.map +1 -1
  284. package/dist/src/tui/theme.js +2 -1
  285. package/dist/src/tui/theme.js.map +1 -1
  286. package/dist/src/tui/unwedge.js +2 -12
  287. package/dist/src/tui/unwedge.js.map +1 -1
  288. package/dist/src/verdicts.js +17 -5
  289. package/dist/src/verdicts.js.map +1 -1
  290. package/dist/src/version.js +1 -1
  291. package/dist/src/version.js.map +1 -1
  292. package/dist/src/webhook.js +12 -1
  293. package/dist/src/webhook.js.map +1 -1
  294. package/dist/src/wire.js +34 -8
  295. package/dist/src/wire.js.map +1 -1
  296. package/dist/src/workerworld.js +11 -1
  297. package/dist/src/workerworld.js.map +1 -1
  298. package/docker/README.md +1 -1
  299. package/docker/companion/entrypoint.sh +20 -1
  300. package/docker/entrypoint.sh +57 -6
  301. package/docker-compose.yml +16 -1
  302. package/extensions/cli/home.ts +14 -0
  303. package/extensions/cli/login-route.ts +10 -0
  304. package/extensions/cli/selfimprove.ts +18 -0
  305. package/extensions/home.ts +484 -0
  306. package/extensions/login-route.ts +220 -0
  307. package/extensions/onboarding.ts +41 -11
  308. package/extensions/selfimprove.ts +65 -0
  309. package/extensions/setup-scan.ts +79 -766
  310. package/extensions/setup.ts +14 -70
  311. package/package.json +6 -4
  312. package/prompts/onboarding.md +58 -18
  313. package/schemas/review-result.v1.json +60 -0
  314. package/scripts/deploy.sh +8 -6
  315. package/scripts/mint-token.sh +25 -4
  316. package/scripts/qcr-host.sh +28 -5
  317. package/scripts/vm-ghcr-login.sh +7 -4
  318. package/src/archive/restore.ts +18 -21
  319. package/src/botworker.ts +13 -2
  320. package/src/channelfs.ts +3 -26
  321. package/src/ciretry.ts +2 -1
  322. package/src/cli.ts +59 -67
  323. package/src/cliops.ts +3 -2
  324. package/src/commands.ts +28 -2
  325. package/src/config.ts +62 -4
  326. package/src/daemon.ts +58 -7
  327. package/src/debuground.ts +3 -0
  328. package/src/debugroundpi.ts +12 -1
  329. package/src/dispatch.ts +5 -4
  330. package/src/engine/caserunner-sdk.ts +29 -314
  331. package/src/engine/debugpi.ts +17 -5
  332. package/src/execboundary.ts +16 -4
  333. package/src/github.ts +57 -15
  334. package/src/githubtoken.ts +435 -0
  335. package/src/home/adapters.ts +418 -0
  336. package/src/home/advanced.ts +421 -0
  337. package/src/home/advancedtools.ts +96 -0
  338. package/src/home/autoreview.ts +385 -0
  339. package/src/home/daemonstatus.ts +55 -0
  340. package/src/home/devicelogin.ts +107 -0
  341. package/src/home/launch.ts +257 -0
  342. package/src/home/loginowner.ts +248 -0
  343. package/src/home/memory.ts +176 -0
  344. package/src/home/model.ts +965 -0
  345. package/src/home/panel.ts +666 -0
  346. package/src/home/ports.ts +338 -0
  347. package/src/home/review.ts +206 -0
  348. package/src/home/reviews.ts +565 -0
  349. package/src/home/setup.ts +689 -0
  350. package/src/home/setupadapters.ts +217 -0
  351. package/src/home/setupcard.ts +288 -0
  352. package/src/home/setuptools.ts +57 -0
  353. package/src/home/spend.ts +194 -0
  354. package/src/home/switches.ts +293 -0
  355. package/src/home/switchon.ts +554 -0
  356. package/src/home/toolnames.ts +46 -0
  357. package/src/home/tune.ts +405 -0
  358. package/src/home/words.ts +8 -0
  359. package/src/homes.ts +17 -0
  360. package/src/install.ts +1 -1
  361. package/src/ipc/workerspec.ts +15 -0
  362. package/src/live/publish.ts +4 -1
  363. package/src/livecli.ts +6 -1
  364. package/src/loginhint.ts +8 -3
  365. package/src/loginrefresh/handover.ts +138 -0
  366. package/src/loginrefresh/lock.ts +72 -3
  367. package/src/loginrefresh/pirefresh.ts +3 -0
  368. package/src/loginrefresh/refresher.ts +34 -5
  369. package/src/loginrefresh/supervisor.ts +25 -2
  370. package/src/npmverbs.ts +55 -13
  371. package/src/onboard/choices.ts +40 -4
  372. package/src/onboard/engine.ts +70 -421
  373. package/src/onboard/pipin.ts +31 -0
  374. package/src/onboard/stepforward.ts +61 -0
  375. package/src/onboard/steps/appslug.ts +4 -1
  376. package/src/onboard/steps/bringup.ts +3 -2
  377. package/src/onboard/steps/compose.ts +31 -0
  378. package/src/onboard/steps/configfill.ts +144 -40
  379. package/src/onboard/steps/githubaccount.ts +716 -0
  380. package/src/onboard/steps/githubapp.ts +92 -8
  381. package/src/onboard/steps/golive.ts +2 -0
  382. package/src/onboard/steps/index.ts +11 -0
  383. package/src/onboard/steps/ingress.ts +98 -31
  384. package/src/onboard/steps/llmauth.ts +114 -29
  385. package/src/onboard/steps/loginshare.ts +258 -0
  386. package/src/onboard/steps/memory.ts +76 -3
  387. package/src/onboard/steps/proof.ts +473 -55
  388. package/src/onboard/steps/repoconfig.ts +85 -0
  389. package/src/onboard/steps/topology.ts +7 -6
  390. package/src/onboard/steps/transport.ts +15 -0
  391. package/src/onboard/steps/tuiroute.ts +119 -0
  392. package/src/onboard/steps/verifyup.ts +20 -1
  393. package/src/onboard/toolnames.ts +34 -0
  394. package/src/operatorwrite.ts +25 -0
  395. package/src/outcomes.ts +8 -1
  396. package/src/piagentdir.ts +12 -0
  397. package/src/pilogin.ts +32 -16
  398. package/src/posting-journal.ts +6 -1
  399. package/src/posting-outbox.ts +10 -2
  400. package/src/posting-receipt.ts +17 -0
  401. package/src/qcrbin.ts +16 -11
  402. package/src/recheck.ts +2 -1
  403. package/src/reconcile.ts +29 -3
  404. package/src/reviewargs.ts +119 -0
  405. package/src/reviewcli.ts +6 -167
  406. package/src/reviewdetach.ts +107 -13
  407. package/src/reviewhistory.ts +20 -6
  408. package/src/reviewnow.ts +76 -0
  409. package/src/scancli.ts +75 -826
  410. package/src/selfimprove.ts +90 -0
  411. package/src/setup/adapters.ts +13 -221
  412. package/src/setup/caserunner.ts +34 -273
  413. package/src/setup/engine.ts +62 -496
  414. package/src/setup/facts.ts +3 -627
  415. package/src/setup/findings.ts +3 -1264
  416. package/src/setup/presentation.ts +3 -18
  417. package/src/setup/scan.ts +3 -1033
  418. package/src/setup/scanbuild.ts +20 -114
  419. package/src/setup/scanlease.ts +3 -297
  420. package/src/setup/scanreport.ts +3 -373
  421. package/src/setup/scanrun.ts +3 -2268
  422. package/src/setup/scantree.ts +3 -259
  423. package/src/setup/stage2request.ts +3 -347
  424. package/src/setup/unitcut.ts +3 -523
  425. package/src/state.ts +43 -0
  426. package/src/statemachine.ts +4 -1
  427. package/src/takeover.ts +5 -0
  428. package/src/takeoverpi.ts +16 -2
  429. package/src/tui/app.ts +47 -0
  430. package/src/tui/board.ts +111 -14
  431. package/src/tui/cmdbar.ts +4 -1
  432. package/src/tui/companion.ts +80 -11
  433. package/src/tui/connect.ts +7 -1
  434. package/src/tui/deploytab.ts +2 -2
  435. package/src/tui/filters.ts +7 -3
  436. package/src/tui/ghread.ts +74 -7
  437. package/src/tui/index.ts +117 -23
  438. package/src/tui/memforward.ts +52 -3
  439. package/src/tui/memory.ts +3 -15
  440. package/src/tui/memoryurl.ts +22 -0
  441. package/src/tui/model.ts +36 -2
  442. package/src/tui/prpause.ts +11 -2
  443. package/src/tui/remote.ts +9 -1
  444. package/src/tui/route.ts +173 -0
  445. package/src/tui/setupstate.ts +56 -0
  446. package/src/tui/sshtriage.ts +127 -6
  447. package/src/tui/theme.ts +2 -1
  448. package/src/tui/unwedge.ts +2 -14
  449. package/src/verdicts.ts +23 -5
  450. package/src/version.ts +1 -1
  451. package/src/webhook.ts +10 -1
  452. package/src/wire.ts +42 -9
  453. package/src/workerworld.ts +16 -2
  454. package/dist/src/live/projection.js +0 -450
  455. package/dist/src/live/projection.js.map +0 -1
  456. package/dist/src/live/read.js +0 -60
  457. package/dist/src/live/read.js.map +0 -1
  458. package/dist/src/onboard/launch.js +0 -83
  459. package/dist/src/onboard/launch.js.map +0 -1
  460. package/dist/src/setup/ledger.js +0 -207
  461. package/dist/src/setup/ledger.js.map +0 -1
  462. package/dist/src/setup/stage2.js +0 -617
  463. package/dist/src/setup/stage2.js.map +0 -1
  464. package/dist/src/setup/textsniff.js +0 -89
  465. package/dist/src/setup/textsniff.js.map +0 -1
  466. package/src/live/projection.ts +0 -491
  467. package/src/live/read.ts +0 -86
  468. package/src/onboard/launch.ts +0 -115
  469. package/src/setup/ledger.ts +0 -324
  470. package/src/setup/stage2.ts +0 -926
  471. package/src/setup/textsniff.ts +0 -95
@@ -0,0 +1,405 @@
1
+ // "Tune to this repo": the home runs Setup for the repository's profile when
2
+ // the person chooses it (CONTEXT.md: Setup is the survey, the review history
3
+ // and the interview, and its result is the repo profile). Nothing waits on
4
+ // it: a review before it, the first one included, runs on the default
5
+ // profile, and the review button never asks for it.
6
+ //
7
+ // Setup is a conversation, so it runs with the agent the person is already
8
+ // talking to: the home hands the chat the plugin's own Setup brief
9
+ // (`profileSetupBrief`, the one `/qcr-setup profile` sends), over a checkout
10
+ // of the repository on this machine. The agent surveys the checkout with its
11
+ // read tools, reads the review history when it can, interviews the person in
12
+ // the chat, and writes the profile, `.qcr/` at the checkout's root, through
13
+ // `home_tune_write`: the one write the home lends it, which writes only a
14
+ // profile file, only while a tune runs, and only on the person's yes.
15
+ //
16
+ // A profile is several files (profile.toml and one addendum per house rule),
17
+ // so the tune stays open through all of them: it ends when the agent marks a
18
+ // write as the profile's last file (`last`), or with the session. Until the
19
+ // agent writes through the tool, the home also watches the profile (the
20
+ // plugin's own detect: `.qcr/profile.toml` in the checkout) and says when one
21
+ // is written another way. A review reads the profile from the base commit,
22
+ // so it applies once the person commits it there.
23
+ //
24
+ // The checkout is untrusted: its contributors choose what is in it, links
25
+ // included. A write creates its temporary file exclusively, never through a
26
+ // link, and refuses a `.qcr/` or `addenda/` that is a link or resolves
27
+ // outside the checkout, so a yes writes only the file it names.
28
+ //
29
+ // tune a key (t, alt+t) and the `home_tune` tool
30
+ // write the `home_tune_write` tool: the agent's draft, the person's yes
31
+ import { randomBytes } from "node:crypto";
32
+ import {
33
+ closeSync,
34
+ constants,
35
+ existsSync,
36
+ lstatSync,
37
+ mkdirSync,
38
+ openSync,
39
+ readFileSync,
40
+ realpathSync,
41
+ renameSync,
42
+ statSync,
43
+ unlinkSync,
44
+ writeSync,
45
+ type Stats,
46
+ } from "node:fs";
47
+ import { basename, dirname, join, relative, sep } from "node:path";
48
+ import { profileSetupBrief } from "@quickchatai/pi-quickreview/src/plugin/setup.js";
49
+ import { REPO_PROFILE_DIR } from "@quickchatai/pi-quickreview/src/profileoverrides.js";
50
+ import { parse } from "smol-toml";
51
+ import type { ProcessRunner } from "../onboard/engine.js";
52
+ import { known, unknown, type Known, type Outcome } from "./ports.js";
53
+ import { parseRepoInput } from "./setup.js";
54
+ import type { Ask } from "./switches.js";
55
+
56
+ /** Where the repository's Setup stands. */
57
+ export interface TuneStatus {
58
+ /** It has a profile of its own: Setup has run. */
59
+ tuned: boolean;
60
+ /** As the person reads it. */
61
+ detail: string;
62
+ /** When its profile was last written, ms since the epoch; null without one. */
63
+ at: number | null;
64
+ }
65
+
66
+ /** Setup, ready to hand the agent. */
67
+ export interface TuneStart {
68
+ /** The checkout Setup reads and writes the profile into. */
69
+ checkout: string;
70
+ /** The Setup brief. */
71
+ brief: string;
72
+ }
73
+
74
+ export interface TunePort {
75
+ status(repo: string): Promise<Known<TuneStatus>>;
76
+ /** Find the checkout Setup runs on, and its brief. */
77
+ start(repo: string): Promise<Outcome<TuneStart>>;
78
+ /** Write one file of the profile, `file` relative to `.qcr/`. */
79
+ write(checkout: string, file: string, text: string): Promise<Outcome<{ path: string }>>;
80
+ }
81
+
82
+ /** How often the home looks for the profile while a tune runs. */
83
+ export const TUNE_POLL_MS = 5000;
84
+
85
+ /** The files of a repo profile Setup may write: profile.toml, and one
86
+ * addendum per house rule. Null: not one of them. */
87
+ export function profileFile(file: string): string | null {
88
+ const f = file.trim().replace(/^\.qcr\//, "");
89
+ if (f === "profile.toml") return f;
90
+ return /^addenda\/[A-Za-z0-9][A-Za-z0-9._-]*\.md$/.test(f) && !f.includes("..") ? f : null;
91
+ }
92
+
93
+ /** What the home adds to the plugin's brief: the write it lends, and what
94
+ * the reviewer-only home's agent cannot do. */
95
+ export function homeBriefNote(checkout: string): string {
96
+ return [
97
+ "",
98
+ `In this home, the checkout is ${checkout}: read it with your read tools. Write each file of the profile with the \`home_tune_write\` tool (\`file\` is relative to ${REPO_PROFILE_DIR}/: \`profile.toml\` or \`addenda/<nn>-<rule-id>.md\`); it shows me the file and writes it only on my yes. Pass \`last: true\` with the profile's final file: the tune ends with it. If you cannot reach \`gh\` from here, skip the review history and say so.`,
99
+ ].join("\n");
100
+ }
101
+
102
+ export const WRITE_NO = "No, don't write it";
103
+ export const WRITE_YES = "Yes, write it";
104
+
105
+ const message = (err: unknown): string => (err instanceof Error ? err.message : String(err));
106
+
107
+ export interface HomeTuneOptions {
108
+ port?: TunePort;
109
+ /** The repository the home reviews, or null. */
110
+ repo: () => string | null;
111
+ say: (text: string, kind?: "info" | "ok" | "warn") => void;
112
+ /** Run `fn` once after `ms`; the returned function cancels it. */
113
+ after: (ms: number, fn: () => void) => () => void;
114
+ onChange?: () => void;
115
+ }
116
+
117
+ interface Tuning {
118
+ repo: string;
119
+ checkout: string;
120
+ before: TuneStatus | null;
121
+ /** The files written through the tool; once there is one, only `last` ends it. */
122
+ wrote: string[];
123
+ }
124
+
125
+ /** The tune: one at a time, from the person's choice to the profile's last file. */
126
+ export class HomeTune {
127
+ private tuning: Tuning | null = null;
128
+ private cancel: (() => void) | null = null;
129
+ private disposed = false;
130
+
131
+ constructor(private readonly o: HomeTuneOptions) {}
132
+
133
+ /** Is Setup under way? */
134
+ running(): boolean {
135
+ return this.tuning !== null;
136
+ }
137
+
138
+ dispose(): void {
139
+ this.disposed = true;
140
+ this.cancel?.();
141
+ this.cancel = null;
142
+ }
143
+
144
+ /** Run Setup for the repository: hand `tellAgent` the brief, then watch for
145
+ * the profile. Without an agent to tell (outside a session), nothing starts. */
146
+ async tune(tellAgent: ((text: string) => void) | undefined): Promise<string> {
147
+ const port = this.o.port;
148
+ if (!port) return "Setup can't be run from this home";
149
+ const repo = this.o.repo();
150
+ if (repo === null) return "no repository is chosen, so there is nothing to tune to";
151
+ if (this.tuning)
152
+ return `Setup of ${this.tuning.repo} is under way with your agent in the chat (${this.tuning.checkout}); it ends when your agent writes the profile's last file`;
153
+ if (!tellAgent) return "Setup is a conversation with the chat's agent, and there is none here";
154
+ let started: Outcome<TuneStart>;
155
+ try {
156
+ started = await port.start(repo);
157
+ } catch (err) {
158
+ started = { ok: false, why: message(err) };
159
+ }
160
+ if (!started.ok) {
161
+ this.o.say(`Can't tune to ${repo} yet: ${started.why}`, "warn");
162
+ return `Setup did not start: ${started.why}`;
163
+ }
164
+ const before = await this.read(repo);
165
+ this.tuning = {
166
+ repo,
167
+ checkout: started.checkout,
168
+ before: before.kind === "known" ? before.value : null,
169
+ wrote: [],
170
+ };
171
+ tellAgent(`${started.brief}\n${homeBriefNote(started.checkout)}`);
172
+ this.o.say(
173
+ `Tuning to ${repo}: your agent surveys ${started.checkout}, reads its review history and asks you about it, here in the chat. Reviews run on the default profile until it is done.`,
174
+ );
175
+ this.o.onChange?.();
176
+ this.watch();
177
+ return `Setup of ${repo} started with the chat's agent, over ${started.checkout}; the home says when the profile is written`;
178
+ }
179
+
180
+ /** The agent's draft of one profile file, written on the person's yes.
181
+ * `last`: it is the profile's final file, and the tune ends with it. */
182
+ async write(file: string, text: string, ask: Ask | undefined, last = false): Promise<string> {
183
+ const t = this.tuning;
184
+ const port = this.o.port;
185
+ if (!t || !port) return "no tune is running: start one first (home_tune)";
186
+ const f = profileFile(file);
187
+ if (f === null)
188
+ return `${JSON.stringify(file)} is not a profile file: write profile.toml or addenda/<nn>-<rule-id>.md`;
189
+ if (!ask) return "the person answers whether to write it, and no one can answer here";
190
+ const where = join(t.checkout, REPO_PROFILE_DIR, f);
191
+ let yes: string | undefined;
192
+ try {
193
+ const lines = text.split("\n").filter((l) => l.trim() !== "").length;
194
+ yes = await ask.select(`Write ${where} (${lines} lines, as your agent showed it)?`, [
195
+ WRITE_NO,
196
+ WRITE_YES,
197
+ ]);
198
+ } catch {
199
+ yes = undefined;
200
+ }
201
+ if (yes !== WRITE_YES) return `the person said no: ${REPO_PROFILE_DIR}/${f} was not written`;
202
+ let r: Outcome<{ path: string }>;
203
+ try {
204
+ r = await port.write(t.checkout, f, text);
205
+ } catch (err) {
206
+ r = { ok: false, why: message(err) };
207
+ }
208
+ if (!r.ok) return `${REPO_PROFILE_DIR}/${f} was not written: ${r.why}`;
209
+ if (this.tuning !== t) return `wrote ${r.path}`;
210
+ t.wrote.push(f);
211
+ if (!last)
212
+ return `wrote ${r.path}; the tune stays open: write the profile's other files, and pass last: true with the final one`;
213
+ const n = t.wrote.length === 1 ? "1 file" : `${t.wrote.length} files`;
214
+ this.finish(t, `${n} of ${REPO_PROFILE_DIR}/ in ${t.checkout}`);
215
+ return `wrote ${r.path}; Setup is done`;
216
+ }
217
+
218
+ private async read(repo: string): Promise<Known<TuneStatus>> {
219
+ try {
220
+ return await this.o.port!.status(repo);
221
+ } catch (err) {
222
+ return unknown(message(err));
223
+ }
224
+ }
225
+
226
+ private watch(): void {
227
+ if (this.disposed || !this.tuning) return;
228
+ this.cancel = this.o.after(TUNE_POLL_MS, () => {
229
+ this.cancel = null;
230
+ void this.check().then((done) => {
231
+ if (!done) this.watch();
232
+ });
233
+ });
234
+ }
235
+
236
+ /** Has the profile been written another way since the tune started? Says
237
+ * so once. Once the agent writes through the tool, only `last` ends it: a
238
+ * profile.toml is the first file of several, not the end. */
239
+ private async check(): Promise<boolean> {
240
+ const t = this.tuning;
241
+ if (!t || t.wrote.length > 0) return true;
242
+ const s = await this.read(t.repo);
243
+ if (this.tuning !== t || t.wrote.length > 0) return true;
244
+ if (s.kind === "unknown" || !s.value.tuned) return false;
245
+ const b = t.before;
246
+ const fresh = !b?.tuned || (s.value.at ?? 0) > (b.at ?? 0);
247
+ if (!fresh) return false;
248
+ this.finish(t, s.value.detail);
249
+ return true;
250
+ }
251
+
252
+ private finish(t: Tuning, detail: string): void {
253
+ this.tuning = null;
254
+ this.cancel?.();
255
+ this.cancel = null;
256
+ this.o.say(
257
+ `Tuned to ${t.repo}: ${detail}. Reviews use it once it is committed to the base branch.`,
258
+ "ok",
259
+ );
260
+ this.o.onChange?.();
261
+ }
262
+ }
263
+
264
+ // ---- the real port: a checkout on this machine ---------------------------
265
+
266
+ type Env = Readonly<Record<string, string | undefined>>;
267
+
268
+ export interface CheckoutTuneOptions {
269
+ env: Env;
270
+ /** The asset root: the shipped profiles and lenses the brief points at. */
271
+ root: () => string;
272
+ /** The app config, for `[companion] reviewed_repo_dir`. */
273
+ configPath: () => string;
274
+ runner: ProcessRunner;
275
+ read?: (p: string) => string;
276
+ }
277
+
278
+ /** The checkout of `repo` on this machine: QCR_REVIEWED_REPO, the config's
279
+ * `[companion] reviewed_repo_dir`, then the directory the home was opened
280
+ * from, each only when git says its origin is `repo`. */
281
+ export async function findCheckout(
282
+ repo: string,
283
+ o: Pick<CheckoutTuneOptions, "env" | "configPath" | "runner" | "read">,
284
+ ): Promise<string | null> {
285
+ const candidates: string[] = [];
286
+ const add = (v: unknown): void => {
287
+ if (typeof v === "string" && v.trim() !== "") candidates.push(v.trim());
288
+ };
289
+ add(o.env["QCR_REVIEWED_REPO"]);
290
+ try {
291
+ const read = o.read ?? ((p: string) => readFileSync(p, "utf8"));
292
+ const cfg = parse(read(o.configPath())) as Record<string, unknown>;
293
+ const companion = cfg["companion"];
294
+ if (typeof companion === "object" && companion !== null)
295
+ add((companion as Record<string, unknown>)["reviewed_repo_dir"]);
296
+ } catch {
297
+ // no config to read: the other places still count
298
+ }
299
+ add(o.env["PWD"]);
300
+ for (const dir of candidates) {
301
+ const top = await o.runner.run("git", ["-C", dir, "rev-parse", "--show-toplevel"]);
302
+ if (top.code !== 0) continue;
303
+ const root = top.stdout.trim();
304
+ const origin = await o.runner.run("git", ["-C", root, "remote", "get-url", "origin"]);
305
+ const named = origin.code === 0 ? parseRepoInput(origin.stdout.trim()) : null;
306
+ if (named !== null && named.toLowerCase() === repo.toLowerCase()) return root;
307
+ }
308
+ return null;
309
+ }
310
+
311
+ /** The tune over a checkout of the repository on this machine. */
312
+ export function checkoutTune(o: CheckoutTuneOptions): TunePort {
313
+ const profileAt = (top: string): string => join(top, REPO_PROFILE_DIR, "profile.toml");
314
+ const missing = (repo: string): string =>
315
+ `Setup reads a checkout of ${repo}, and there is none here: open the home from inside one (cd into it, then run qcr), or set QCR_REVIEWED_REPO to it`;
316
+ return {
317
+ status: async (repo) => {
318
+ const top = await findCheckout(repo, o);
319
+ if (top === null) return unknown(`no checkout of ${repo} on this machine`);
320
+ const file = profileAt(top);
321
+ if (!existsSync(file))
322
+ return known({ tuned: false, detail: "reviews run on the default profile", at: null });
323
+ let at: number | null = null;
324
+ try {
325
+ at = statSync(file).mtimeMs;
326
+ } catch {
327
+ // it is there: when it was written is all that is not known
328
+ }
329
+ return known({ tuned: true, detail: `${REPO_PROFILE_DIR}/profile.toml in ${top}`, at });
330
+ },
331
+ start: async (repo) => {
332
+ const top = await findCheckout(repo, o);
333
+ if (top === null) return { ok: false, why: missing(repo) };
334
+ return { ok: true, checkout: top, brief: profileSetupBrief({ top, root: o.root() }) };
335
+ },
336
+ write: async (checkout, file, text) => {
337
+ const f = profileFile(file);
338
+ if (f === null) return { ok: false, why: `${file} is not a profile file` };
339
+ const path = join(checkout, REPO_PROFILE_DIR, f);
340
+ try {
341
+ const body = text.endsWith("\n") ? text : `${text}\n`;
342
+ writeInCheckout(checkout, `${REPO_PROFILE_DIR}/${f}`, body);
343
+ } catch (err) {
344
+ return { ok: false, why: message(err) };
345
+ }
346
+ return { ok: true, path };
347
+ },
348
+ };
349
+ }
350
+
351
+ /** Write `rel` (slash-separated, below `checkout`) into an untrusted checkout.
352
+ * Every directory on the way is a real directory, made here when missing,
353
+ * never a link, and resolves inside the checkout; the file itself, when there,
354
+ * is a regular file. The text goes to a fresh temporary file, created
355
+ * exclusively and never through a link, then renamed over the target, so no
356
+ * name a contributor planted is opened for writing. */
357
+ export function writeInCheckout(checkout: string, rel: string, text: string): void {
358
+ const top = realpathSync(checkout);
359
+ const parts = rel.split("/");
360
+ const name = parts.pop()!;
361
+ let dir = top;
362
+ for (const part of parts) {
363
+ dir = join(dir, part);
364
+ let st: Stats;
365
+ try {
366
+ st = lstatSync(dir);
367
+ } catch {
368
+ mkdirSync(dir);
369
+ st = lstatSync(dir);
370
+ }
371
+ if (st.isSymbolicLink()) throw new Error(`${relative(top, dir)} is a link, not a directory`);
372
+ if (!st.isDirectory()) throw new Error(`${relative(top, dir)} is not a directory`);
373
+ }
374
+ const real = realpathSync(dir);
375
+ if (real !== top && !real.startsWith(top + sep))
376
+ throw new Error(`${relative(top, dir)} resolves outside the checkout`);
377
+ const path = join(real, name);
378
+ try {
379
+ const st = lstatSync(path);
380
+ if (!st.isFile()) throw new Error(`${rel} is there and is not a plain file`);
381
+ } catch (err) {
382
+ if ((err as NodeJS.ErrnoException).code !== "ENOENT") throw err;
383
+ }
384
+ const tmp = join(dirname(path), `.${basename(path)}.${randomBytes(6).toString("hex")}.tmp`);
385
+ const fd = openSync(
386
+ tmp,
387
+ constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW,
388
+ 0o644,
389
+ );
390
+ try {
391
+ try {
392
+ writeSync(fd, text);
393
+ } finally {
394
+ closeSync(fd);
395
+ }
396
+ renameSync(tmp, path);
397
+ } catch (err) {
398
+ try {
399
+ unlinkSync(tmp);
400
+ } catch {
401
+ // already renamed, or never there: nothing to clean up
402
+ }
403
+ throw err;
404
+ }
405
+ }
@@ -0,0 +1,8 @@
1
+ // The home's words for an amount: one money and one plural, so the panel, the
2
+ // model's notices and the review message say a price or a count alike.
3
+
4
+ /** "$1.33". */
5
+ export const money = (n: number): string => `$${n.toFixed(2)}`;
6
+
7
+ /** "1 finding", "3 findings". */
8
+ export const plural = (n: number, word: string): string => `${n} ${word}${n === 1 ? "" : "s"}`;
package/src/homes.ts CHANGED
@@ -104,6 +104,23 @@ export function operatorStateDir(
104
104
  return kind === "global" || kind === "local" ? stateHome(env) : join(root, "state");
105
105
  }
106
106
 
107
+ /**
108
+ * The compose project the home's switches and the admin TUI share, with the
109
+ * setup's state under it (`<root>/state`, `<root>/.env`): `QCR_ROOT` when
110
+ * set; a checkout or the image is its own; an app installed by a package
111
+ * manager gets one in its state home, never inside the package — for the
112
+ * same reasons as `operatorStateDir`.
113
+ */
114
+ export function stackRoot(
115
+ env: Readonly<Record<string, string | undefined>>,
116
+ pkgRoot: string,
117
+ ): string {
118
+ const explicit = (env["QCR_ROOT"] ?? "").trim();
119
+ if (explicit !== "") return explicit;
120
+ const kind = detectInstall(pkgRoot, env).kind;
121
+ return kind === "global" || kind === "local" ? join(stateHome(env), "stack") : pkgRoot;
122
+ }
123
+
107
124
  export function locateConfig(opts: LocateOptions = {}): LocatedConfig {
108
125
  return core.locateConfig(optEnv(opts));
109
126
  }
package/src/install.ts CHANGED
@@ -190,7 +190,7 @@ export function migrateReport(outcome: MigrateOutcome): { code: number; lines: s
190
190
  return {
191
191
  code: 0,
192
192
  lines: [
193
- `no config.toml in the checkout or at ${outcome.to} — nothing to move (\`qcr onboard\` writes a new one there)`,
193
+ `no config.toml in the checkout or at ${outcome.to} — nothing to move (a bare \`qcr\` writes one there when you choose a repository)`,
194
194
  ],
195
195
  };
196
196
  case "override":
@@ -43,6 +43,14 @@ export interface WorkerSpec {
43
43
  * in memory, never written back), and where models.json and the built lens
44
44
  * agents are read from. */
45
45
  agent_dir: string;
46
+ /** Where the login is borrowed from instead, when it is not in `agent_dir`:
47
+ * the single box's shared login (src/loginrefresh/supervisor.ts
48
+ * `loginDirOf`). Absent: `agent_dir`. */
49
+ login_dir?: string;
50
+ /** The providers the review's models name (the config's, plugin/login.ts
51
+ * `reviewProviders`): the login's admission checks only theirs. Omitted:
52
+ * every login in it. */
53
+ providers?: string[];
46
54
  /** Where the PR's main JSONL lives (`state/pi-sessions/pr-<n>`). The worker
47
55
  * resumes the newest one under it, or starts a session there at birth. */
48
56
  session_dir: string;
@@ -169,6 +177,13 @@ export function parseWorkerSpec(raw: unknown): WorkerSpec {
169
177
  spec.cap_usd = cap;
170
178
  }
171
179
  if (raw["lens_dir"] !== undefined) spec.lens_dir = abs(raw, "lens_dir");
180
+ if (raw["login_dir"] !== undefined) spec.login_dir = abs(raw, "login_dir");
181
+ if (raw["providers"] !== undefined) {
182
+ const p = raw["providers"];
183
+ if (!Array.isArray(p) || p.some((x) => typeof x !== "string" || x === ""))
184
+ return bad("`providers` must be a list of provider names");
185
+ spec.providers = p as string[];
186
+ }
172
187
  if (raw["compact"] !== undefined) {
173
188
  if (typeof raw["compact"] !== "boolean") return bad("`compact` must be a boolean");
174
189
  spec.compact = raw["compact"];
@@ -17,7 +17,10 @@ import {
17
17
  progressLogPath,
18
18
  } from "@quickchatai/pi-quickreview/src/round-progress.js";
19
19
  import { readLiveEvents } from "@quickchatai/pi-quickreview/src/live/events.js";
20
- import { RoundProgressProjection, type ProjectionIdentity } from "./projection.js";
20
+ import {
21
+ RoundProgressProjection,
22
+ type ProjectionIdentity,
23
+ } from "@quickchatai/pi-quickreview/src/live/projection.js";
21
24
 
22
25
  /** `<root>/state/round-progress/pr-<n>.json`, where the supervisor reads it. */
23
26
  export function roundProgressPath(root: string, pr: number): string {
package/src/livecli.ts CHANGED
@@ -15,7 +15,12 @@ import {
15
15
  readLiveEvents,
16
16
  type LiveEvent,
17
17
  } from "@quickchatai/pi-quickreview/src/live/events.js";
18
- import { latestLiveRound, lensLogIds, listLiveRounds, type LiveRoundRef } from "./live/read.js";
18
+ import {
19
+ latestLiveRound,
20
+ lensLogIds,
21
+ listLiveRounds,
22
+ type LiveRoundRef,
23
+ } from "@quickchatai/pi-quickreview/src/live/read.js";
19
24
 
20
25
  export const LIVE_USAGE =
21
26
  "usage: qcr live [RUN_ID | DIR] [--lens ID] [--round N] [--follow] [--json]";
package/src/loginhint.ts CHANGED
@@ -2,8 +2,11 @@
2
2
  // provider the review's models run on? A login in pi's agent dir counts, and
3
3
  // so does the provider's own API key in the environment. An API key for some
4
4
  // OTHER provider never does: the review runs on the models its config names,
5
- // or not at all, and it never switches models to use a key. The refusal names
6
- // `qcr login`, the way to get the login.
5
+ // or not at all, and it never switches models to use a key. The one exception
6
+ // is the OpenAI family: an `openai-codex/` model runs on an `openai` login or
7
+ // OPENAI_API_KEY (and back), the same model id on either
8
+ // (packages/pi-plugin/src/modelroute.ts), so either login counts for it. The
9
+ // refusal names `qcr login`, the way to get the login.
7
10
  import { readFileSync } from "node:fs";
8
11
  import { join } from "node:path";
9
12
  import {
@@ -11,6 +14,7 @@ import {
11
14
  RUNTIME_KEY_ENVS,
12
15
  type LoginSummary,
13
16
  } from "@quickchatai/pi-quickreview/src/engine/auth.js";
17
+ import { routeProvider } from "@quickchatai/pi-quickreview/src/modelroute.js";
14
18
 
15
19
  /** pi's stored logins in `agentDir`; empty when it holds none. */
16
20
  export function readLogins(
@@ -37,8 +41,9 @@ export function reviewLoginRefusal(o: {
37
41
  const name = RUNTIME_KEY_ENVS[p];
38
42
  return name !== undefined && (o.env[name] ?? "").trim() !== "" ? name : null;
39
43
  };
44
+ const usable = (p: string): boolean => logins[p] !== undefined || keyOf(p) !== null;
40
45
  for (const p of o.providers) {
41
- if (logins[p] !== undefined || keyOf(p) !== null) continue;
46
+ if (usable(routeProvider(p, "", { hasCredential: usable }))) continue;
42
47
  const otherKeys = Object.keys(RUNTIME_KEY_ENVS)
43
48
  .filter((q) => q !== p)
44
49
  .map(keyOf)
@@ -0,0 +1,138 @@
1
+ // The login handover, in Node: what docker/entrypoint.sh's login mode does
2
+ // around pi's /login (login_handover_begin / login_handover_end), for a login
3
+ // made outside the container, by the home's re-login (src/home/loginowner.ts).
4
+ //
5
+ // On a single box the daemon's refresher owns the host pi home's auth.json
6
+ // while auto-review runs (one login per box). A login that rewrites
7
+ // that file beside it would race its refresh: whichever wrote last wins, and a
8
+ // refresh landing after the login would put the old grant back. So a login
9
+ // first asks the refresher to pause: it writes `auth.json.pause` and waits for
10
+ // `"paused": true` in the refresher's status file. A paused refresher neither
11
+ // refreshes nor writes, and KEEPS its lock, so no second refresher can start
12
+ // during the login. Releasing removes the pause file; the refresher re-reads
13
+ // the new login at once and resumes. A pause file left behind (the home killed
14
+ // mid-login) is lifted by the refresher after 15 minutes; while the hold
15
+ // lasts, the file's time is renewed so a slow device login is never cut off.
16
+ //
17
+ // Where no refresher holds the lock (auto-review off) nothing is paused: the
18
+ // home's own pi owns the login then, and its /login runs under pi's own lock.
19
+ // Nothing here reads or writes auth.json.
20
+ import { readFileSync, unlinkSync, utimesSync, writeFileSync } from "node:fs";
21
+ import { join } from "node:path";
22
+ import { loginLockHeld } from "./lock.js";
23
+ import { LOCK_FILE, PAUSE_FILE, STATUS_FILE } from "./refresher.js";
24
+
25
+ /** How long a login waits for the refresher to pause (the entrypoint's 60 s). */
26
+ export const PAUSE_WAIT_MS = 60_000;
27
+ /** How often the status file is read while waiting (the refresher looks at the
28
+ * pause file every 2 s). */
29
+ export const PAUSE_POLL_MS = 500;
30
+ /** How often a held pause file's time is renewed: well inside the
31
+ * refresher's 15 minutes. */
32
+ export const PAUSE_RENEW_MS = 5 * 60_000;
33
+
34
+ export type RefresherHold =
35
+ /** `held`: a refresher was running and is paused now; false: none runs. */
36
+ { ok: true; held: boolean; release(): void } | { ok: false; why: string };
37
+
38
+ export interface HoldOptions {
39
+ flockBin?: string;
40
+ /** When the lock cannot be probed (no flock here): does a refresher run?
41
+ * null: unknown. */
42
+ refresherRuns?: () => Promise<boolean | null>;
43
+ waitMs?: number;
44
+ pollMs?: number;
45
+ renewMs?: number;
46
+ sleep?: (ms: number) => Promise<void>;
47
+ now?: () => number;
48
+ }
49
+
50
+ const realSleep = (ms: number): Promise<void> => new Promise((r) => setTimeout(r, ms));
51
+
52
+ /** Does a refresher hold the login in `agentDir` now? null: unknown. */
53
+ export async function refresherRuns(
54
+ agentDir: string,
55
+ o: Pick<HoldOptions, "flockBin" | "refresherRuns"> = {},
56
+ ): Promise<boolean | null> {
57
+ const held = loginLockHeld(join(agentDir, LOCK_FILE), o.flockBin ? { flockBin: o.flockBin } : {});
58
+ if (held !== null) return held;
59
+ return o.refresherRuns ? o.refresherRuns() : null;
60
+ }
61
+
62
+ /** The refresher's status file says it is paused. */
63
+ function pausedNow(statusPath: string): boolean {
64
+ try {
65
+ const v = JSON.parse(readFileSync(statusPath, "utf8")) as { paused?: unknown } | null;
66
+ return v?.paused === true;
67
+ } catch {
68
+ return false;
69
+ }
70
+ }
71
+
72
+ function removeQuietly(path: string): void {
73
+ try {
74
+ unlinkSync(path);
75
+ } catch {
76
+ /* lifted already */
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Pause the refresher of the login in `agentDir` for a login, when one runs.
82
+ * Answers once it is paused (or none runs), with the release; refuses when it
83
+ * does not pause in time (the pause file is then removed again) or when
84
+ * whether one runs cannot be told.
85
+ */
86
+ export async function holdRefresher(agentDir: string, o: HoldOptions = {}): Promise<RefresherHold> {
87
+ const runs = await refresherRuns(agentDir, o);
88
+ if (runs === null)
89
+ return {
90
+ ok: false,
91
+ why: "whether auto-review's login refresher runs can't be told on this machine",
92
+ };
93
+ if (!runs) return { ok: true, held: false, release: () => {} };
94
+ const pause = join(agentDir, PAUSE_FILE);
95
+ const status = join(agentDir, STATUS_FILE);
96
+ try {
97
+ writeFileSync(pause, "", { mode: 0o660 });
98
+ } catch (err) {
99
+ return {
100
+ ok: false,
101
+ why: `can't write ${pause}: ${err instanceof Error ? err.message : String(err)}`,
102
+ };
103
+ }
104
+ const sleep = o.sleep ?? realSleep;
105
+ const now = o.now ?? Date.now;
106
+ const waitMs = o.waitMs ?? PAUSE_WAIT_MS;
107
+ const until = now() + waitMs;
108
+ while (!pausedNow(status)) {
109
+ if (now() >= until) {
110
+ removeQuietly(pause);
111
+ return {
112
+ ok: false,
113
+ why: `auto-review's login refresher did not pause within ${Math.round(waitMs / 1000)} s (${status})`,
114
+ };
115
+ }
116
+ await sleep(o.pollMs ?? PAUSE_POLL_MS);
117
+ }
118
+ const renew = setInterval(() => {
119
+ try {
120
+ const t = new Date();
121
+ utimesSync(pause, t, t);
122
+ } catch {
123
+ /* lifted meanwhile */
124
+ }
125
+ }, o.renewMs ?? PAUSE_RENEW_MS);
126
+ renew.unref?.();
127
+ let released = false;
128
+ return {
129
+ ok: true,
130
+ held: true,
131
+ release: () => {
132
+ if (released) return;
133
+ released = true;
134
+ clearInterval(renew);
135
+ removeQuietly(pause);
136
+ },
137
+ };
138
+ }