@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
@@ -48,7 +48,7 @@ import { createServer, type IncomingMessage, type Server, type ServerResponse }
48
48
  import { isAbsolute, join } from "node:path";
49
49
  import type { Socket } from "node:net";
50
50
  import { mintAppJwt } from "@quickchatai/pi-quickreview/src/githubAuth.js";
51
- import { CHOICE_KEYS, REPO_SLUG_RE, botLoginFor } from "../choices.js";
51
+ import { CHOICE_KEYS, REPO_SLUG_RE, botLoginFor, userAuthRecorded } from "../choices.js";
52
52
  import { CONFIG_FILL_STEP_ID } from "./configfill.js";
53
53
  import { readTomlKey, secretsDirFor } from "./ingress.js";
54
54
  import {
@@ -416,9 +416,54 @@ export interface CallbackServerOptions {
416
416
  page: (bound: { url: string; redirectUrl: string }) => string;
417
417
  state: string;
418
418
  /** Preferred loopback port; a busy port falls back to an ephemeral one and
419
- * the manifest's redirect_url follows the port actually bound. */
419
+ * the manifest's redirect_url follows the port actually bound — except
420
+ * behind a `hosted` origin, where the port is strict. */
420
421
  port: number;
421
422
  timeoutMs: number;
423
+ /** A hosted setup (`QCR_ONBOARD_PUBLIC_ORIGIN`): a reverse proxy forwards
424
+ * this public https origin to the loopback listener on `port`. */
425
+ hosted?: HostedCallback;
426
+ }
427
+
428
+ /** The hosted callback's shape: GitHub redirects to `<origin>/manifest-callback`,
429
+ * the operator opens `pageUrl`, and the proxy's Host header is admitted. */
430
+ export interface HostedCallback {
431
+ /** `https://host[:port]`, no path. */
432
+ origin: string;
433
+ /** The page URL the operator is told to open (default: the origin). */
434
+ pageUrl: string;
435
+ }
436
+
437
+ /** The env knobs of a hosted setup. Total: an unusable origin is a refusal
438
+ * with its reason, never a silent fallback to loopback. */
439
+ export function hostedCallbackFromEnv(env: NodeJS.ProcessEnv): {
440
+ hosted?: HostedCallback;
441
+ problem?: string;
442
+ } {
443
+ const raw = (env["QCR_ONBOARD_PUBLIC_ORIGIN"] ?? "").trim();
444
+ if (raw === "") return {};
445
+ let u: URL;
446
+ try {
447
+ u = new URL(raw);
448
+ } catch {
449
+ return {
450
+ problem: `QCR_ONBOARD_PUBLIC_ORIGIN=${raw} is not a URL — set an https origin like https://<public host>`,
451
+ };
452
+ }
453
+ if (
454
+ u.protocol !== "https:" ||
455
+ u.username ||
456
+ u.password ||
457
+ (u.pathname !== "/" && u.pathname !== "") ||
458
+ u.search ||
459
+ u.hash
460
+ )
461
+ return {
462
+ problem: `QCR_ONBOARD_PUBLIC_ORIGIN=${raw} must be a bare https origin (https://host[:port], no path) — GitHub redirects to <origin>${CALLBACK_PATH}`,
463
+ };
464
+ const origin = u.origin;
465
+ const page = (env["QCR_ONBOARD_SETUP_URL"] ?? "").trim();
466
+ return { hosted: { origin, pageUrl: page !== "" ? page : `${origin}/` } };
422
467
  }
423
468
 
424
469
  export type CallbackServerFactory = (o: CallbackServerOptions) => Promise<CallbackListener>;
@@ -516,13 +561,30 @@ export const nodeCallbackServer: CallbackServerFactory = async (o) => {
516
561
  let port: number;
517
562
  try {
518
563
  port = await listen(o.port);
519
- } catch {
564
+ } catch (err) {
565
+ // Behind a hosted origin the proxy forwards to exactly this port, so
566
+ // another one would be a listener nothing reaches: refuse, and say so.
567
+ if (o.hosted)
568
+ throw new Error(
569
+ `port ${o.port} is busy, and the hosted callback (${o.hosted.origin} → 127.0.0.1:${o.port}) needs exactly ` +
570
+ `that port — stop whatever holds it and run the step again (${String(err instanceof Error ? err.message : err)})`,
571
+ { cause: err },
572
+ );
520
573
  port = await listen(0); // preferred port busy — any loopback port will do
521
574
  }
522
575
 
523
576
  allowedHosts = new Set([`127.0.0.1:${port}`, `localhost:${port}`]);
524
- const url = `http://127.0.0.1:${port}/`;
525
- const redirectUrl = `http://127.0.0.1:${port}${CALLBACK_PATH}`;
577
+ let url = `http://127.0.0.1:${port}/`;
578
+ let redirectUrl = `http://127.0.0.1:${port}${CALLBACK_PATH}`;
579
+ if (o.hosted) {
580
+ // The proxy's own Host header is the public one: admitted by name, with
581
+ // and without the scheme's default port, alongside loopback.
582
+ const pub = new URL(o.hosted.origin);
583
+ allowedHosts.add(pub.host.toLowerCase());
584
+ if (pub.port === "") allowedHosts.add(`${pub.hostname.toLowerCase()}:443`);
585
+ url = o.hosted.pageUrl;
586
+ redirectUrl = `${o.hosted.origin}${CALLBACK_PATH}`;
587
+ }
526
588
  html = o.page({ url, redirectUrl });
527
589
 
528
590
  const timer = setTimeout(
@@ -720,7 +782,7 @@ export async function findInstallation(
720
782
  };
721
783
  }
722
784
 
723
- async function installationToken(
785
+ export async function installationToken(
724
786
  fetchFn: typeof fetch,
725
787
  apiBase: string,
726
788
  jwt: string,
@@ -851,6 +913,9 @@ export interface GithubAppStepOptions {
851
913
  randomToken?: (bytes: number) => string;
852
914
  /** Try to open the operator's browser at the page (best effort, never fatal). */
853
915
  openBrowser?: boolean;
916
+ /** Env for the hosted-callback knobs (`QCR_ONBOARD_PUBLIC_ORIGIN`,
917
+ * `QCR_ONBOARD_SETUP_URL`); default the process env. */
918
+ env?: NodeJS.ProcessEnv;
854
919
  }
855
920
 
856
921
  interface Resolved {
@@ -863,8 +928,18 @@ interface Resolved {
863
928
  }
864
929
 
865
930
  function resolveLabels(ctx: StepContext): Resolved["labels"] {
866
- const config = readTextOrNull(ctx.adapters.paths.configPath);
867
- const recorded = (ctx.choices[CHOICE_KEYS.labels] ?? {}) as Partial<
931
+ return labelSpecs(readTextOrNull(ctx.adapters.paths.configPath), ctx.choices);
932
+ }
933
+
934
+ /** The three trigger/status labels this installation means — config.toml
935
+ * first, then the names the interview recorded, then the shipped defaults —
936
+ * with the colour and description the wizard creates them with. Shared with
937
+ * the own-account step, which creates the same labels with the person's token. */
938
+ export function labelSpecs(
939
+ config: string | null,
940
+ choices: Readonly<Record<string, unknown>>,
941
+ ): Array<{ name: string; color: string; description: string }> {
942
+ const recorded = (choices[CHOICE_KEYS.labels] ?? {}) as Partial<
868
943
  Record<keyof typeof DEFAULT_LABELS, unknown>
869
944
  >;
870
945
  return LABEL_META.map((meta) => {
@@ -1148,12 +1223,17 @@ export function githubAppStep(opts: GithubAppStepOptions = {}): OnboardingStep {
1148
1223
  // The page can only be rendered once the port is known (the manifest's
1149
1224
  // redirect_url must name the port actually bound), so the listener builds
1150
1225
  // it from its own bound address.
1226
+ const hostedEnv = hostedCallbackFromEnv(opts.env ?? process.env);
1227
+ if (hostedEnv.problem) {
1228
+ return [{ name: "callback listener", ok: false, detail: hostedEnv.problem }];
1229
+ }
1151
1230
  let listener: CallbackListener;
1152
1231
  try {
1153
1232
  listener = await serve({
1154
1233
  state,
1155
1234
  port: opts.callbackPort ?? DEFAULT_CALLBACK_PORT,
1156
1235
  timeoutMs: callbackTimeoutMs,
1236
+ ...(hostedEnv.hosted ? { hosted: hostedEnv.hosted } : {}),
1157
1237
  page: (bound) =>
1158
1238
  manifestPageHtml({
1159
1239
  postUrl: appCreationUrl(target, r.githubBase),
@@ -1319,6 +1399,10 @@ export function githubAppStep(opts: GithubAppStepOptions = {}): OnboardingStep {
1319
1399
  // ingress step (39), whose recorded webhook URL goes into the manifest;
1320
1400
  // before the bring-up steps.
1321
1401
  order: 40,
1402
+ // The App route only. The own-account route (github_account, order 33)
1403
+ // needs no App at all; an unanswered route keeps this step, so a
1404
+ // checklist from before the question existed still verifies its App.
1405
+ enabled: (choices) => !userAuthRecorded(choices),
1322
1406
  // The reviewed repo is NOT a parameter: it is the interview's answer, read
1323
1407
  // from the checklist (CHOICE_KEYS.repo). The only thing the operator may
1324
1408
  // supply here is what nothing else can derive — the App's display name.
@@ -267,6 +267,8 @@ export function goLiveStep(opts: GoLiveStepOptions = {}): OnboardingStep {
267
267
  'arguments to get the explanation to read out; then call it again with decision: "stay_dry_run", or with ' +
268
268
  `decision: "go_live" AND confirm: "${GO_LIVE_CONFIRMATION}" if — and only if — they said so explicitly.`,
269
269
  order: 78,
270
+ // After setup: the board offers it; finish never waits on it.
271
+ optional: true,
270
272
  parameters: {
271
273
  type: "object",
272
274
  properties: {
@@ -7,6 +7,7 @@ import { bringUpStep } from "./bringup.js";
7
7
  import { configFillStep } from "./configfill.js";
8
8
  import { embeddingsStep } from "./embeddings.js";
9
9
  import { escalationStep } from "./escalation.js";
10
+ import { githubAccountStep } from "./githubaccount.js";
10
11
  import { githubAppStep } from "./githubapp.js";
11
12
  import { goLiveStep } from "./golive.js";
12
13
  import { ingressStep } from "./ingress.js";
@@ -38,6 +39,12 @@ export function defaultSteps(): OnboardingStep[] {
38
39
  // ssh_preflight (22) VM only, and FIRST on that route: the alias, the
39
40
  // agent and the passphrase trap, before anything is
40
41
  // minted, written or deployed;
42
+ //
43
+ // The GitHub route split is the second question, asked by github_account
44
+ // (33): "user" (the person's own account — token file, polling, no App) or
45
+ // "app". Once answered, exactly one of github_account (33) / github_app (40)
46
+ // is enabled, and app_slug_reconcile (65) runs only on the App route.
47
+ //
41
48
  // vm_inventory (68) VM only: the deploy inventory, from the answers;
42
49
  // vm_deploy (71) VM only: `scripts/deploy.sh`, composed, with every
43
50
  // preflight it has — the peer of bringup (70), which is
@@ -51,6 +58,10 @@ export function defaultSteps(): OnboardingStep[] {
51
58
  topologyStep(),
52
59
  sshPreflightStep(),
53
60
  llmAuthStep(),
61
+ // github_account (33) before the interview (35): the interview asks the
62
+ // App questions only on the App route, and takes the repo and login the
63
+ // own-account route already settled.
64
+ githubAccountStep(),
54
65
  configFillStep(),
55
66
  escalationStep(),
56
67
  embeddingsStep(),
@@ -18,11 +18,18 @@
18
18
  // permanent hostname (the VM route's default). No rotation, so no
19
19
  // self-heal: `[ingress] shape = "none"` and `[server] hostname`
20
20
  // carries the name.
21
- // polling No tunnel at all. The reconcile poll becomes the ONLY trigger
22
- // path, at `[daemon] poll_interval_s`. What degrades is stated
23
- // plainly in the step's own output (author-reply routing and the
24
- // [feedback] loop are webhook-only), and the tunnel is offered as a
25
- // later upgrade — re-running this step is the upgrade.
21
+ // polling No tunnel at all — THE DEFAULT on every route, and the only
22
+ // shape the own-account route (`[github] auth = "user"`) allows.
23
+ // `[ingress] shape = "polling"`. The reconcile poll becomes the ONLY
24
+ // trigger path, at `[daemon] poll_interval_s`. What degrades is
25
+ // stated plainly in the step's own output (author-reply routing and
26
+ // the [feedback] loop are webhook-only), and the tunnel is offered
27
+ // as a later upgrade — re-running this step is the upgrade.
28
+ //
29
+ // Called with no choice and none recorded, the step applies the default
30
+ // (polling) and says so in one plain line, with the other shapes listed as
31
+ // the later upgrade: a stated default is not a guess, and nothing about it is
32
+ // irreversible.
26
33
  //
27
34
  // The step is deliberately self-contained: it owns the `[ingress]` section of
28
35
  // config.toml, the project `.env`'s COMPOSE_PROFILES line, and nothing else.
@@ -33,7 +40,7 @@ import { existsSync, readFileSync } from "node:fs";
33
40
  import { isAbsolute, join } from "node:path";
34
41
  import { INGRESS_DEFAULTS } from "../../config.js";
35
42
  import { locateSecrets } from "../../install.js";
36
- import { CHOICE_KEYS, INGRESS_CHOICES, type IngressChoice } from "../choices.js";
43
+ import { CHOICE_KEYS, INGRESS_CHOICES, userAuthRecorded, type IngressChoice } from "../choices.js";
37
44
  import { topologyOf } from "./topology.js";
38
45
  import {
39
46
  readTextOrNull,
@@ -67,7 +74,7 @@ export interface IngressOption {
67
74
  /** Honest trade-offs — the good and the bad, in the operator's words. */
68
75
  tradeoffs: string[];
69
76
  /** `[ingress] shape` this choice configures. */
70
- shape: "tunnel-file" | "none";
77
+ shape: "tunnel-file" | "none" | "polling";
71
78
  /** COMPOSE_PROFILES value ("" ⇒ no sidecar at all). */
72
79
  profile: string;
73
80
  /** Secrets-dir file this choice requires, if any. */
@@ -78,7 +85,7 @@ export interface IngressOption {
78
85
 
79
86
  const INGRESS_OPTION_METADATA = {
80
87
  quick: {
81
- headline: "Cloudflare quick tunnel (default) — no account, nothing to sign up for",
88
+ headline: "Cloudflare quick tunnel — no account, nothing to sign up for",
82
89
  tradeoffs: [
83
90
  "no account, no credential, no DNS: it works within seconds of `docker compose up`",
84
91
  "the *.trycloudflare.com hostname ROTATES on every tunnel restart — the daemon re-points the App's webhook URL itself, so this costs you nothing to operate",
@@ -117,14 +124,14 @@ const INGRESS_OPTION_METADATA = {
117
124
  service: "tunnel-named",
118
125
  },
119
126
  polling: {
120
- headline: "Polling only — no tunnel, nothing exposed",
127
+ headline: "Polling only (default) — no tunnel, nothing exposed",
121
128
  tradeoffs: [
122
129
  "nothing to install, nothing reachable from the internet: the daemon only makes outbound calls",
123
130
  "the reconcile poll becomes the ONLY trigger path, so a review starts up to one poll interval late ([daemon] poll_interval_s, 300s by default)",
124
131
  "what degrades, plainly: author replies do not route to the PR's session (the ping-pong path is webhook-driven), the [feedback] tuning loop never sees a comment, and the CI-completed retry of a withheld approval waits for the next sweep",
125
132
  "it is not a dead end — re-run this step later and pick a tunnel; nothing else has to change",
126
133
  ],
127
- shape: "none",
134
+ shape: "polling",
128
135
  profile: "",
129
136
  secret: null,
130
137
  service: null,
@@ -163,6 +170,16 @@ const INGRESS_ANCHOR = /^\[daemon\]/m;
163
170
 
164
171
  /** The section body (header line included) for a choice. */
165
172
  export function ingressBlock(option: IngressOption): string {
173
+ if (option.shape === "polling") {
174
+ return [
175
+ "[ingress]",
176
+ "# Written by the onboarding wizard.",
177
+ "# Chosen shape: polling — no webhook reaches this daemon; the reconcile poll",
178
+ "# ([daemon] poll_interval_s) is the only trigger path.",
179
+ 'shape = "polling"',
180
+ "",
181
+ ].join("\n");
182
+ }
166
183
  if (option.shape === "none") {
167
184
  return [
168
185
  "[ingress]",
@@ -425,35 +442,62 @@ function chosen(ctx: StepContext): IngressChoice | null {
425
442
  }
426
443
 
427
444
  /**
428
- * The recommended default differs by topology, and saying so is the honest
429
- * part. On a laptop the quick tunnel wins: no account, and the daemon
430
- * re-points its own rotating hostname. On a server the named tunnel wins: the
431
- * playbook installs cloudflared there from your token, and a permanent
432
- * installation deserves a permanent hostname. Every option stays selectable on
433
- * both — on the server route the ones the playbook does not install mean "you
434
- * front the daemon's port yourself", which the inventory step spells out.
445
+ * The recommended default is polling on every topology: nothing to install,
446
+ * nothing exposed, and the only shape the own-account route allows. A tunnel
447
+ * (quick on a laptop, named on a server) is the later upgrade for webhook
448
+ * latency and the reply-driven paths. Every option stays selectable on both
449
+ * topologies — on the server route the ones the playbook does not install mean
450
+ * "you front the daemon's port yourself", which the inventory step spells out.
435
451
  */
436
- export function recommendedIngressFor(topology: string): IngressChoice {
437
- return topology === "vm" ? "named" : "quick";
452
+ export function recommendedIngressFor(_topology: string): IngressChoice {
453
+ return "polling";
454
+ }
455
+
456
+ /** The reconcile poll's interval in whole minutes, as the config states it
457
+ * (the shipped default when it does not). */
458
+ export function pollMinutes(configText: string | null): number {
459
+ const raw = configText ? readTomlKey(configText, "daemon", "poll_interval_s") : null;
460
+ const seconds = raw !== null && /^\d+$/.test(raw.trim()) ? Number(raw.trim()) : 300;
461
+ return Math.max(1, Math.round(seconds / 60));
462
+ }
463
+
464
+ /** The one plain line the default is applied with. */
465
+ export function pollingDefaultLine(configText: string | null): string {
466
+ return (
467
+ `polling: the reviewer checks GitHub every ${pollMinutes(configText)} minutes for pull requests to review — ` +
468
+ "nothing to set up, nothing exposed to the internet. Webhooks (faster starts, author replies) are optional and " +
469
+ "can be added later by re-running this step with a tunnel choice."
470
+ );
438
471
  }
439
472
 
440
473
  function offerCheck(topology: string): CheckResult {
441
- const recommended = recommendedIngressFor(topology);
442
474
  return {
443
475
  name: "choice",
444
476
  ok: false,
445
477
  detail:
446
478
  `pick one of ${CHOICE_IDS.join(" | ")} and call this step again with it (choice: "<id>").\n` +
479
+ `The default is "${recommendedIngressFor(topology)}" — nothing to set up and nothing exposed; webhooks are a later upgrade.` +
447
480
  (topology === "vm"
448
- ? `On the server route the recommended answer is "${recommended}" — the deploy installs cloudflared on that machine from ` +
449
- "your tunnel token, and an always-on installation deserves a hostname that never rotates. The others stay available: " +
450
- "polling needs no ingress at all, and quick/ngrok mean you run that tunnel on the server yourself.\n"
451
- : `On this machine the recommended answer is "${recommended}" — no account, nothing to sign up for, and the daemon ` +
452
- "re-points its own webhook URL when the hostname rotates.\n") +
481
+ ? " On the server route the named tunnel is the upgrade worth having: the deploy installs cloudflared on that machine " +
482
+ "from your tunnel token, and a hostname that never rotates. quick/ngrok mean you run that tunnel on the server yourself.\n"
483
+ : " On this machine the quick tunnel is the no-account upgrade: the daemon re-points its own webhook URL when the " +
484
+ "hostname rotates.\n") +
453
485
  renderOptions(),
454
486
  };
455
487
  }
456
488
 
489
+ /** The own-account route has no webhook: no App exists to deliver one. */
490
+ function userRouteRefusal(choice: IngressChoice): CheckResult {
491
+ return {
492
+ name: "choice",
493
+ ok: false,
494
+ detail:
495
+ `"${choice}" needs webhooks, and the own-account GitHub route ([github] auth = "user") has none — webhooks are ` +
496
+ 'delivered to a GitHub App, and this install runs on your own account instead. It is polling only: run this step with choice: "polling" ' +
497
+ '(or with no choice). To use webhooks, switch to the App route first: the github_account step with github_auth: "app".',
498
+ };
499
+ }
500
+
457
501
  function credentialPresent(file: string): boolean {
458
502
  return existsSync(file) && readFileSync(file, "utf8").trim() !== "";
459
503
  }
@@ -464,7 +508,7 @@ export function ingressStep(opts: { stream?: StreamRunner } = {}): OnboardingSte
464
508
  title: "Webhook ingress: how GitHub reaches your daemon",
465
509
  explain:
466
510
  "GitHub delivers webhooks to an HTTPS URL, so your daemon needs one. Offer the four shapes with their real " +
467
- "trade-offs — quick tunnel (the default: no account, rotating hostname the daemon re-points itself), ngrok " +
511
+ "trade-offs — quick tunnel (no account, rotating hostname the daemon re-points itself), ngrok " +
468
512
  "(stable hostname, free-tier caps), a named Cloudflare tunnel (your own domain), or polling-only (no tunnel; " +
469
513
  "the reconcile poll triggers reviews, and author-reply routing plus the feedback loop go quiet). The recommended " +
470
514
  "answer depends on where the reviewer lives: the quick tunnel on this computer, the named tunnel on a server (the " +
@@ -493,13 +537,32 @@ export function ingressStep(opts: { stream?: StreamRunner } = {}): OnboardingSte
493
537
  },
494
538
 
495
539
  async run(ctx): Promise<StepReport> {
496
- const choice = chosen(ctx);
497
- if (choice === null)
540
+ const userRoute = userAuthRecorded(ctx.choices);
541
+ const typed = ctx.input.choice;
542
+ const typedAnswer = typeof typed === "string" && typed.trim() !== "";
543
+ let choice = chosen(ctx);
544
+ // An answer that names no shape is re-offered, never defaulted over.
545
+ if (choice === null && typedAnswer)
498
546
  return { ok: false, checks: [offerCheck(String(ctx.choices[CHOICE_KEYS.topology] ?? ""))] };
547
+ const defaulted = choice === null;
548
+ if (choice === null)
549
+ choice = recommendedIngressFor(String(ctx.choices[CHOICE_KEYS.topology] ?? ""));
550
+ if (userRoute && choice !== "polling") {
551
+ // A shape recorded before the route was switched is not an answer now.
552
+ if (!typedAnswer) choice = "polling";
553
+ else return { ok: false, checks: [userRouteRefusal(choice)] };
554
+ }
499
555
  const option = optionFor(choice)!;
500
556
  const root = sandboxRootOf(ctx.adapters);
501
557
  const checks: CheckResult[] = [
502
- { name: "choice", ok: true, detail: `${option.id} — ${option.headline}` },
558
+ {
559
+ name: "choice",
560
+ ok: true,
561
+ detail:
562
+ defaulted && option.id === "polling"
563
+ ? `${pollingDefaultLine(readTextOrNull(ctx.adapters.paths.configPath))}\nThe other shapes, for later: ${CHOICE_IDS.filter((id) => id !== "polling").join(", ")}.`
564
+ : `${option.id} — ${option.headline}`,
565
+ },
503
566
  { name: "trade-offs", ok: true, detail: option.tradeoffs.map((t) => `• ${t}`).join("\n") },
504
567
  ];
505
568
 
@@ -580,7 +643,9 @@ export function ingressStep(opts: { stream?: StreamRunner } = {}): OnboardingSte
580
643
  detail:
581
644
  option.shape === "tunnel-file"
582
645
  ? '[ingress] shape = "tunnel-file" — the daemon watches the hostname file and re-points the App webhook URL when the tunnel rotates'
583
- : '[ingress] shape = "none" — no webhook self-heal (this hostname does not rotate)',
646
+ : option.shape === "polling"
647
+ ? '[ingress] shape = "polling" — no webhook, the reconcile poll is the only trigger path'
648
+ : '[ingress] shape = "none" — no webhook self-heal (this hostname does not rotate)',
584
649
  });
585
650
 
586
651
  // 4. the compose profile — which sidecar (if any) comes up with the stack.
@@ -697,7 +762,9 @@ export function ingressStep(opts: { stream?: StreamRunner } = {}): OnboardingSte
697
762
  ok: false,
698
763
  detail: `${ctx.adapters.paths.configPath} is gone — re-run the ingress step after restoring it`,
699
764
  }
700
- : shape === option.shape
765
+ : shape === option.shape ||
766
+ // a polling install from before the shape had its own name
767
+ (option.id === "polling" && shape === "none" && !userAuthRecorded(ctx.choices))
701
768
  ? { name: "config", ok: true, detail: `[ingress] shape = "${shape}"` }
702
769
  : {
703
770
  name: "config",