@quickchatai/quickreview 1.2.0 → 1.3.2

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 (350) hide show
  1. package/README.md +116 -53
  2. package/bin/qcr.js +1 -1
  3. package/config.example.toml +25 -7
  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/selfimprove.js +18 -0
  11. package/dist/extensions/cli/selfimprove.js.map +1 -0
  12. package/dist/extensions/home.js +350 -0
  13. package/dist/extensions/home.js.map +1 -0
  14. package/dist/extensions/login-route.js +1 -1
  15. package/dist/extensions/login-route.js.map +1 -1
  16. package/dist/extensions/onboarding.js +8 -7
  17. package/dist/extensions/onboarding.js.map +1 -1
  18. package/dist/extensions/selfimprove.js +49 -0
  19. package/dist/extensions/selfimprove.js.map +1 -0
  20. package/dist/extensions/setup-scan.js +23 -592
  21. package/dist/extensions/setup-scan.js.map +1 -1
  22. package/dist/extensions/setup.js +2 -47
  23. package/dist/extensions/setup.js.map +1 -1
  24. package/dist/extensions/vendor/openviking-pi/index.js +4 -0
  25. package/dist/extensions/vendor/openviking-pi/index.js.map +1 -1
  26. package/dist/extensions/vendor/openviking-pi/qcr-hooks.js +3 -1
  27. package/dist/extensions/vendor/openviking-pi/qcr-hooks.js.map +1 -1
  28. package/dist/extensions/vendor/openviking-pi/sync.js +81 -0
  29. package/dist/extensions/vendor/openviking-pi/sync.js.map +1 -1
  30. package/dist/src/archive/restore.js +18 -15
  31. package/dist/src/archive/restore.js.map +1 -1
  32. package/dist/src/botworker.js +13 -2
  33. package/dist/src/botworker.js.map +1 -1
  34. package/dist/src/channelfs.js +3 -26
  35. package/dist/src/channelfs.js.map +1 -1
  36. package/dist/src/cli.js +14 -50
  37. package/dist/src/cli.js.map +1 -1
  38. package/dist/src/config.js +16 -0
  39. package/dist/src/config.js.map +1 -1
  40. package/dist/src/daemon.js +33 -4
  41. package/dist/src/daemon.js.map +1 -1
  42. package/dist/src/debuground.js +1 -0
  43. package/dist/src/debuground.js.map +1 -1
  44. package/dist/src/debugroundpi.js +1 -0
  45. package/dist/src/debugroundpi.js.map +1 -1
  46. package/dist/src/engine/caserunner-sdk.js +15 -251
  47. package/dist/src/engine/caserunner-sdk.js.map +1 -1
  48. package/dist/src/home/adapters.js +349 -0
  49. package/dist/src/home/adapters.js.map +1 -0
  50. package/dist/src/home/advanced.js +318 -0
  51. package/dist/src/home/advanced.js.map +1 -0
  52. package/dist/src/home/advancedtools.js +57 -0
  53. package/dist/src/home/advancedtools.js.map +1 -0
  54. package/dist/src/home/autoreview.js +325 -0
  55. package/dist/src/home/autoreview.js.map +1 -0
  56. package/dist/src/home/daemonstatus.js +32 -0
  57. package/dist/src/home/daemonstatus.js.map +1 -0
  58. package/dist/src/home/devicelogin.js +61 -0
  59. package/dist/src/home/devicelogin.js.map +1 -0
  60. package/dist/src/home/launch.js +202 -0
  61. package/dist/src/home/launch.js.map +1 -0
  62. package/dist/src/home/loginowner.js +205 -0
  63. package/dist/src/home/loginowner.js.map +1 -0
  64. package/dist/src/home/memory.js +122 -0
  65. package/dist/src/home/memory.js.map +1 -0
  66. package/dist/src/home/model.js +811 -0
  67. package/dist/src/home/model.js.map +1 -0
  68. package/dist/src/home/panel.js +520 -0
  69. package/dist/src/home/panel.js.map +1 -0
  70. package/dist/src/home/ports.js +19 -0
  71. package/dist/src/home/ports.js.map +1 -0
  72. package/dist/src/home/review.js +163 -0
  73. package/dist/src/home/review.js.map +1 -0
  74. package/dist/src/home/reviews.js +478 -0
  75. package/dist/src/home/reviews.js.map +1 -0
  76. package/dist/src/home/setup.js +507 -0
  77. package/dist/src/home/setup.js.map +1 -0
  78. package/dist/src/home/setupadapters.js +214 -0
  79. package/dist/src/home/setupadapters.js.map +1 -0
  80. package/dist/src/home/setupcard.js +199 -0
  81. package/dist/src/home/setupcard.js.map +1 -0
  82. package/dist/src/home/setuptools.js +45 -0
  83. package/dist/src/home/setuptools.js.map +1 -0
  84. package/dist/src/home/spend.js +160 -0
  85. package/dist/src/home/spend.js.map +1 -0
  86. package/dist/src/home/switches.js +209 -0
  87. package/dist/src/home/switches.js.map +1 -0
  88. package/dist/src/home/switchon.js +439 -0
  89. package/dist/src/home/switchon.js.map +1 -0
  90. package/dist/src/home/toolnames.js +41 -0
  91. package/dist/src/home/toolnames.js.map +1 -0
  92. package/dist/src/home/tune.js +341 -0
  93. package/dist/src/home/tune.js.map +1 -0
  94. package/dist/src/home/words.js +7 -0
  95. package/dist/src/home/words.js.map +1 -0
  96. package/dist/src/homes.js +14 -0
  97. package/dist/src/homes.js.map +1 -1
  98. package/dist/src/install.js +1 -1
  99. package/dist/src/install.js.map +1 -1
  100. package/dist/src/ipc/workerspec.js +8 -0
  101. package/dist/src/ipc/workerspec.js.map +1 -1
  102. package/dist/src/ledgercheck.js +39 -10
  103. package/dist/src/ledgercheck.js.map +1 -1
  104. package/dist/src/live/publish.js +1 -1
  105. package/dist/src/live/publish.js.map +1 -1
  106. package/dist/src/livecli.js +1 -1
  107. package/dist/src/livecli.js.map +1 -1
  108. package/dist/src/loginrefresh/handover.js +121 -0
  109. package/dist/src/loginrefresh/handover.js.map +1 -0
  110. package/dist/src/loginrefresh/lock.js +54 -3
  111. package/dist/src/loginrefresh/lock.js.map +1 -1
  112. package/dist/src/loginrefresh/refresher.js +33 -5
  113. package/dist/src/loginrefresh/refresher.js.map +1 -1
  114. package/dist/src/loginrefresh/supervisor.js +18 -1
  115. package/dist/src/loginrefresh/supervisor.js.map +1 -1
  116. package/dist/src/npmverbs.js +42 -12
  117. package/dist/src/npmverbs.js.map +1 -1
  118. package/dist/src/onboard/choices.js +6 -3
  119. package/dist/src/onboard/choices.js.map +1 -1
  120. package/dist/src/onboard/engine.js +33 -292
  121. package/dist/src/onboard/engine.js.map +1 -1
  122. package/dist/src/onboard/steps/bringup.js +3 -2
  123. package/dist/src/onboard/steps/bringup.js.map +1 -1
  124. package/dist/src/onboard/steps/configfill.js +18 -7
  125. package/dist/src/onboard/steps/configfill.js.map +1 -1
  126. package/dist/src/onboard/steps/loginshare.js +213 -0
  127. package/dist/src/onboard/steps/loginshare.js.map +1 -0
  128. package/dist/src/onboard/steps/memory.js +14 -2
  129. package/dist/src/onboard/steps/memory.js.map +1 -1
  130. package/dist/src/onboard/steps/repoconfig.js +72 -0
  131. package/dist/src/onboard/steps/repoconfig.js.map +1 -0
  132. package/dist/src/onboard/toolnames.js +2 -2
  133. package/dist/src/onboard/toolnames.js.map +1 -1
  134. package/dist/src/piagentdir.js +13 -0
  135. package/dist/src/piagentdir.js.map +1 -0
  136. package/dist/src/pilogin.js +31 -16
  137. package/dist/src/pilogin.js.map +1 -1
  138. package/dist/src/qcrbin.js +16 -11
  139. package/dist/src/qcrbin.js.map +1 -1
  140. package/dist/src/reviewargs.js +122 -0
  141. package/dist/src/reviewargs.js.map +1 -0
  142. package/dist/src/reviewcli.js +7 -161
  143. package/dist/src/reviewcli.js.map +1 -1
  144. package/dist/src/reviewdetach.js +76 -11
  145. package/dist/src/reviewdetach.js.map +1 -1
  146. package/dist/src/scancli.js +39 -691
  147. package/dist/src/scancli.js.map +1 -1
  148. package/dist/src/selfimprove.js +66 -0
  149. package/dist/src/selfimprove.js.map +1 -0
  150. package/dist/src/session-command-supervisor.js +38 -9
  151. package/dist/src/session-command-supervisor.js.map +1 -1
  152. package/dist/src/setup/adapters.js +5 -223
  153. package/dist/src/setup/adapters.js.map +1 -1
  154. package/dist/src/setup/caserunner.js +15 -249
  155. package/dist/src/setup/caserunner.js.map +1 -1
  156. package/dist/src/setup/engine.js +32 -341
  157. package/dist/src/setup/engine.js.map +1 -1
  158. package/dist/src/setup/facts.js +3 -592
  159. package/dist/src/setup/facts.js.map +1 -1
  160. package/dist/src/setup/findings.js +3 -947
  161. package/dist/src/setup/findings.js.map +1 -1
  162. package/dist/src/setup/presentation.js +3 -8
  163. package/dist/src/setup/presentation.js.map +1 -1
  164. package/dist/src/setup/scan.js +3 -724
  165. package/dist/src/setup/scan.js.map +1 -1
  166. package/dist/src/setup/scanbuild.js +9 -92
  167. package/dist/src/setup/scanbuild.js.map +1 -1
  168. package/dist/src/setup/scanlease.js +3 -250
  169. package/dist/src/setup/scanlease.js.map +1 -1
  170. package/dist/src/setup/scanreport.js +3 -288
  171. package/dist/src/setup/scanreport.js.map +1 -1
  172. package/dist/src/setup/scanrun.js +3 -1732
  173. package/dist/src/setup/scanrun.js.map +1 -1
  174. package/dist/src/setup/scantree.js +3 -256
  175. package/dist/src/setup/scantree.js.map +1 -1
  176. package/dist/src/setup/stage2request.js +3 -258
  177. package/dist/src/setup/stage2request.js.map +1 -1
  178. package/dist/src/setup/unitcut.js +3 -436
  179. package/dist/src/setup/unitcut.js.map +1 -1
  180. package/dist/src/state.js +75 -9
  181. package/dist/src/state.js.map +1 -1
  182. package/dist/src/takeover.js +1 -0
  183. package/dist/src/takeover.js.map +1 -1
  184. package/dist/src/takeoverpi.js +2 -1
  185. package/dist/src/takeoverpi.js.map +1 -1
  186. package/dist/src/tui/alerts.js +4 -1
  187. package/dist/src/tui/alerts.js.map +1 -1
  188. package/dist/src/tui/board.js +2 -2
  189. package/dist/src/tui/board.js.map +1 -1
  190. package/dist/src/tui/companion.js +2 -1
  191. package/dist/src/tui/companion.js.map +1 -1
  192. package/dist/src/tui/deploytab.js +2 -2
  193. package/dist/src/tui/deploytab.js.map +1 -1
  194. package/dist/src/tui/index.js +9 -6
  195. package/dist/src/tui/index.js.map +1 -1
  196. package/dist/src/tui/memory.js +3 -16
  197. package/dist/src/tui/memory.js.map +1 -1
  198. package/dist/src/tui/memoryurl.js +22 -0
  199. package/dist/src/tui/memoryurl.js.map +1 -0
  200. package/dist/src/tui/remote.js +1 -1
  201. package/dist/src/tui/remote.js.map +1 -1
  202. package/dist/src/tui/route.js +2 -2
  203. package/dist/src/tui/route.js.map +1 -1
  204. package/dist/src/tui/setupstate.js +8 -5
  205. package/dist/src/tui/setupstate.js.map +1 -1
  206. package/dist/src/tui/sshtriage.js +1 -1
  207. package/dist/src/tui/sshtriage.js.map +1 -1
  208. package/dist/src/verdicts.js +12 -0
  209. package/dist/src/verdicts.js.map +1 -1
  210. package/dist/src/wire.js +9 -4
  211. package/dist/src/wire.js.map +1 -1
  212. package/dist/src/workerbreaker.js +32 -5
  213. package/dist/src/workerbreaker.js.map +1 -1
  214. package/dist/src/workerworld.js +4 -0
  215. package/dist/src/workerworld.js.map +1 -1
  216. package/docker/README.md +1 -1
  217. package/docker/entrypoint.sh +40 -6
  218. package/docker-compose.yml +16 -1
  219. package/extensions/cli/home.ts +14 -0
  220. package/extensions/cli/selfimprove.ts +18 -0
  221. package/extensions/home.ts +484 -0
  222. package/extensions/login-route.ts +1 -1
  223. package/extensions/onboarding.ts +8 -7
  224. package/extensions/selfimprove.ts +65 -0
  225. package/extensions/setup-scan.ts +79 -766
  226. package/extensions/setup.ts +14 -70
  227. package/extensions/vendor/openviking-pi/NOTICE +5 -0
  228. package/extensions/vendor/openviking-pi/README.md +39 -5
  229. package/extensions/vendor/openviking-pi/index.ts +4 -0
  230. package/extensions/vendor/openviking-pi/patches/0014-one-turn-sync-in-flight.patch +239 -0
  231. package/extensions/vendor/openviking-pi/qcr-hooks.ts +3 -1
  232. package/extensions/vendor/openviking-pi/sync.ts +80 -0
  233. package/extensions/vendor/openviking-pi/upstream.json +2 -1
  234. package/package.json +6 -4
  235. package/prompts/onboarding.md +8 -7
  236. package/schemas/review-result.v1.json +60 -0
  237. package/scripts/deploy.sh +8 -6
  238. package/scripts/qcr-host.sh +28 -5
  239. package/scripts/vm-ghcr-login.sh +7 -4
  240. package/src/archive/restore.ts +18 -21
  241. package/src/botworker.ts +13 -2
  242. package/src/channelfs.ts +3 -26
  243. package/src/cli.ts +13 -54
  244. package/src/config.ts +38 -0
  245. package/src/daemon.ts +34 -4
  246. package/src/debuground.ts +3 -0
  247. package/src/debugroundpi.ts +1 -0
  248. package/src/engine/caserunner-sdk.ts +29 -314
  249. package/src/home/adapters.ts +418 -0
  250. package/src/home/advanced.ts +421 -0
  251. package/src/home/advancedtools.ts +96 -0
  252. package/src/home/autoreview.ts +385 -0
  253. package/src/home/daemonstatus.ts +55 -0
  254. package/src/home/devicelogin.ts +107 -0
  255. package/src/home/launch.ts +257 -0
  256. package/src/home/loginowner.ts +248 -0
  257. package/src/home/memory.ts +176 -0
  258. package/src/home/model.ts +965 -0
  259. package/src/home/panel.ts +666 -0
  260. package/src/home/ports.ts +338 -0
  261. package/src/home/review.ts +206 -0
  262. package/src/home/reviews.ts +565 -0
  263. package/src/home/setup.ts +689 -0
  264. package/src/home/setupadapters.ts +217 -0
  265. package/src/home/setupcard.ts +288 -0
  266. package/src/home/setuptools.ts +57 -0
  267. package/src/home/spend.ts +194 -0
  268. package/src/home/switches.ts +293 -0
  269. package/src/home/switchon.ts +554 -0
  270. package/src/home/toolnames.ts +46 -0
  271. package/src/home/tune.ts +405 -0
  272. package/src/home/words.ts +8 -0
  273. package/src/homes.ts +17 -0
  274. package/src/install.ts +1 -1
  275. package/src/ipc/workerspec.ts +15 -0
  276. package/src/ledgercheck.ts +45 -9
  277. package/src/live/publish.ts +4 -1
  278. package/src/livecli.ts +6 -1
  279. package/src/loginrefresh/handover.ts +138 -0
  280. package/src/loginrefresh/lock.ts +72 -3
  281. package/src/loginrefresh/refresher.ts +34 -5
  282. package/src/loginrefresh/supervisor.ts +25 -2
  283. package/src/npmverbs.ts +51 -11
  284. package/src/onboard/choices.ts +6 -3
  285. package/src/onboard/engine.ts +64 -434
  286. package/src/onboard/steps/bringup.ts +3 -2
  287. package/src/onboard/steps/configfill.ts +23 -7
  288. package/src/onboard/steps/loginshare.ts +258 -0
  289. package/src/onboard/steps/memory.ts +18 -2
  290. package/src/onboard/steps/repoconfig.ts +85 -0
  291. package/src/onboard/toolnames.ts +2 -2
  292. package/src/piagentdir.ts +12 -0
  293. package/src/pilogin.ts +32 -16
  294. package/src/qcrbin.ts +16 -11
  295. package/src/reviewargs.ts +119 -0
  296. package/src/reviewcli.ts +6 -167
  297. package/src/reviewdetach.ts +107 -13
  298. package/src/scancli.ts +75 -826
  299. package/src/selfimprove.ts +90 -0
  300. package/src/session-command-supervisor.ts +41 -8
  301. package/src/setup/adapters.ts +13 -221
  302. package/src/setup/caserunner.ts +34 -273
  303. package/src/setup/engine.ts +62 -496
  304. package/src/setup/facts.ts +3 -627
  305. package/src/setup/findings.ts +3 -1264
  306. package/src/setup/presentation.ts +3 -18
  307. package/src/setup/scan.ts +3 -1033
  308. package/src/setup/scanbuild.ts +20 -114
  309. package/src/setup/scanlease.ts +3 -297
  310. package/src/setup/scanreport.ts +3 -373
  311. package/src/setup/scanrun.ts +3 -2268
  312. package/src/setup/scantree.ts +3 -259
  313. package/src/setup/stage2request.ts +3 -347
  314. package/src/setup/unitcut.ts +3 -523
  315. package/src/state.ts +126 -11
  316. package/src/takeover.ts +5 -0
  317. package/src/takeoverpi.ts +3 -2
  318. package/src/tui/alerts.ts +9 -2
  319. package/src/tui/board.ts +2 -2
  320. package/src/tui/companion.ts +2 -1
  321. package/src/tui/deploytab.ts +2 -2
  322. package/src/tui/index.ts +9 -6
  323. package/src/tui/memory.ts +3 -15
  324. package/src/tui/memoryurl.ts +22 -0
  325. package/src/tui/remote.ts +1 -1
  326. package/src/tui/route.ts +2 -2
  327. package/src/tui/setupstate.ts +20 -16
  328. package/src/tui/sshtriage.ts +1 -1
  329. package/src/verdicts.ts +13 -0
  330. package/src/wire.ts +9 -4
  331. package/src/workerbreaker.ts +41 -5
  332. package/src/workerworld.ts +9 -1
  333. package/dist/src/live/projection.js +0 -450
  334. package/dist/src/live/projection.js.map +0 -1
  335. package/dist/src/live/read.js +0 -60
  336. package/dist/src/live/read.js.map +0 -1
  337. package/dist/src/onboard/launch.js +0 -75
  338. package/dist/src/onboard/launch.js.map +0 -1
  339. package/dist/src/setup/ledger.js +0 -207
  340. package/dist/src/setup/ledger.js.map +0 -1
  341. package/dist/src/setup/stage2.js +0 -617
  342. package/dist/src/setup/stage2.js.map +0 -1
  343. package/dist/src/setup/textsniff.js +0 -89
  344. package/dist/src/setup/textsniff.js.map +0 -1
  345. package/src/live/projection.ts +0 -491
  346. package/src/live/read.ts +0 -86
  347. package/src/onboard/launch.ts +0 -103
  348. package/src/setup/ledger.ts +0 -324
  349. package/src/setup/stage2.ts +0 -926
  350. package/src/setup/textsniff.ts +0 -95
@@ -0,0 +1,385 @@
1
+ // Auto-review in the home: its honest label, read from the daemon's
2
+ // container and its status snapshot, and the real switch over this machine's
3
+ // compose stack (src/home/switchon.ts runs the steps).
4
+ //
5
+ // The label:
6
+ //
7
+ // off the daemon's container is not running (switched off, or
8
+ // never brought up)
9
+ // starting it runs and its first poll has not ended yet, or the home
10
+ // is switching it on (the home holds that)
11
+ // on-live/-dry it runs, its heartbeat is within two poll intervals AND its
12
+ // last poll was clean; dry_run says which
13
+ // paused-login the login it reviews on expired (pi_auth_expired, or the
14
+ // login refresher's own record)
15
+ // broken it runs but is not working: the heartbeat or the poll is
16
+ // stale, the last poll failed, the container keeps
17
+ // restarting, or the pipeline or provider reports a cause
18
+ // unknown anything the home cannot read: Docker, the snapshot, or a
19
+ // snapshot from an older daemon with no `poll` record
20
+ // (invariant 9: never Off and never On)
21
+ import { readFileSync } from "node:fs";
22
+ import { parse } from "smol-toml";
23
+ import { snapshotHeartbeat } from "../state.js";
24
+ import { OnboardingEngine, type OnboardAdapters, type ProcessRunner } from "../onboard/engine.js";
25
+ import {
26
+ CONTAINER_STATE_DIR,
27
+ DAEMON_SERVICE,
28
+ MEMORY_SERVICE,
29
+ composeArgs,
30
+ composePs,
31
+ daemonExec,
32
+ serviceRow,
33
+ } from "../onboard/steps/compose.js";
34
+ import {
35
+ known,
36
+ unknown,
37
+ type AutoReviewControl,
38
+ type AutoReviewPort,
39
+ type AutoReviewStatus,
40
+ type Known,
41
+ type RunningRound,
42
+ type SwitchResult,
43
+ } from "./ports.js";
44
+ import {
45
+ MACHINE_CHECKS_STEP_ID,
46
+ recordedProof,
47
+ runGoLive,
48
+ runProofRound,
49
+ runSwitchOn,
50
+ switchOnRegistry,
51
+ type SwitchOnStepOptions,
52
+ } from "./switchon.js";
53
+
54
+ /** "On" needs a heartbeat within this many poll intervals. */
55
+ export const HEARTBEAT_INTERVALS = 2;
56
+ /** A poll older than this many intervals is not working: the daemon abandons
57
+ * a pass after three (RECONCILE_OVERDUE_INTERVALS) and starts the next. */
58
+ export const POLL_STALE_INTERVALS = 4;
59
+
60
+ /** The daemon container, as Docker says. */
61
+ export type DaemonContainer = "running" | "restarting" | "stopped";
62
+
63
+ export interface AutoReviewReading {
64
+ container: Known<DaemonContainer>;
65
+ /** status.json's text, read while the container runs; null when the
66
+ * daemon has written none. */
67
+ snapshot: Known<string | null>;
68
+ now: number;
69
+ }
70
+
71
+ const minutes = (ms: number): string => {
72
+ const m = Math.max(0, Math.round(ms / 60_000));
73
+ return m < 60 ? `${m}m` : `${Math.round(m / 60)}h`;
74
+ };
75
+
76
+ type Obj = Record<string, unknown>;
77
+ const isObj = (v: unknown): v is Obj => typeof v === "object" && v !== null && !Array.isArray(v);
78
+
79
+ /** status.json's text as an object; a reason when it is not one. */
80
+ function parseSnapshot(text: string): Obj | string {
81
+ try {
82
+ const v = JSON.parse(text) as unknown;
83
+ return isObj(v) ? v : "its status.json is not an object";
84
+ } catch {
85
+ return "its status.json is not readable JSON";
86
+ }
87
+ }
88
+
89
+ /** The refresher's record of the login, when the snapshot has one. */
90
+ function refresherLogin(s: Obj): Obj | null {
91
+ const lr = s["login_refresh"];
92
+ const login = isObj(lr) ? lr["login"] : undefined;
93
+ return isObj(login) ? login : null;
94
+ }
95
+
96
+ /** A re-login ended the daemon's last auth failure: the refresher's record,
97
+ * read after that failure, of a login that works now. It outweighs the
98
+ * failure's flag, which would otherwise stand for its own half hour. */
99
+ function loginRecovered(s: Obj, now: number): boolean {
100
+ const expired = s["pi_auth_expired"];
101
+ const login = refresherLogin(s);
102
+ return isObj(expired) && login !== null && worksSince(login, expired["last_at"], now);
103
+ }
104
+
105
+ /** Why the login it reviews on stops it, or null. A re-login ends the pause
106
+ * (loginRecovered). */
107
+ function loginPause(s: Obj, now: number): string | null {
108
+ const login = refresherLogin(s);
109
+ if (isObj(s["pi_auth_expired"]) && !loginRecovered(s, now)) return "login expired";
110
+ if (login === null) return null;
111
+ if (login["paused"] === true) return "a login is in progress";
112
+ const expires = typeof login["expires_at"] === "string" ? Date.parse(login["expires_at"]) : NaN;
113
+ if (Number.isFinite(expires) && expires <= now && typeof login["last_error"] === "string")
114
+ return "login expired";
115
+ return null;
116
+ }
117
+
118
+ /** The refresher looked after `since` and found a login that works: not
119
+ * paused, no error, a token not yet expired. */
120
+ function worksSince(login: Obj, since: unknown, now: number): boolean {
121
+ const at = typeof since === "string" ? Date.parse(since) : NaN;
122
+ const checked = typeof login["checked_at"] === "string" ? Date.parse(login["checked_at"]) : NaN;
123
+ const expires = typeof login["expires_at"] === "string" ? Date.parse(login["expires_at"]) : NaN;
124
+ return (
125
+ Number.isFinite(at) &&
126
+ Number.isFinite(checked) &&
127
+ checked > at &&
128
+ login["paused"] !== true &&
129
+ login["last_error"] === null &&
130
+ Number.isFinite(expires) &&
131
+ expires > now
132
+ );
133
+ }
134
+
135
+ /** The provider record the snapshot derives from that same auth failure
136
+ * (src/state.ts writes the two together, with one `last_at`). */
137
+ function authDerived(s: Obj, pv: Obj): boolean {
138
+ const expired = s["pi_auth_expired"];
139
+ return (
140
+ isObj(expired) && typeof pv["last_at"] === "string" && pv["last_at"] === expired["last_at"]
141
+ );
142
+ }
143
+
144
+ /** The pipeline's or the provider's cause, or null. A provider failure that
145
+ * is only the recovered login's own flag is not a cause; any other is. */
146
+ function brokenCause(s: Obj, now: number): string | null {
147
+ const p = s["pipeline"];
148
+ if (isObj(p) && p["healthy"] === false) {
149
+ const first = Array.isArray(p["causes"]) ? (p["causes"][0] as unknown) : undefined;
150
+ if (isObj(first)) {
151
+ const pr = typeof first["pr"] === "number" ? `#${first["pr"]}: ` : "";
152
+ const detail =
153
+ typeof first["detail"] === "string" ? first["detail"] : "the pipeline is unhealthy";
154
+ return `${pr}${detail}`;
155
+ }
156
+ return "the review pipeline reports itself unhealthy";
157
+ }
158
+ const pv = s["provider"];
159
+ if (isObj(pv) && pv["healthy"] === false && !(authDerived(s, pv) && loginRecovered(s, now)))
160
+ return typeof pv["detail"] === "string" ? pv["detail"] : "the model provider is failing";
161
+ return null;
162
+ }
163
+
164
+ /** The label, from what was read. Pure. */
165
+ export function autoReviewLabel(r: AutoReviewReading): Known<AutoReviewStatus> {
166
+ if (r.container.kind === "unknown")
167
+ return unknown(`can't ask Docker whether the daemon runs: ${r.container.why}`);
168
+ if (r.container.value === "stopped") return known({ state: "off" });
169
+ if (r.container.value === "restarting")
170
+ return known({
171
+ state: "broken",
172
+ cause: "the daemon keeps restarting (docker compose logs daemon says why)",
173
+ });
174
+ if (r.snapshot.kind === "unknown")
175
+ return unknown(`can't read the daemon's status: ${r.snapshot.why}`);
176
+ if (r.snapshot.value === null) return unknown("the daemon runs but has written no status yet");
177
+ const s = parseSnapshot(r.snapshot.value);
178
+ if (typeof s === "string") return unknown(s);
179
+ const beat = Date.parse(snapshotHeartbeat(s) ?? "");
180
+ if (!Number.isFinite(beat)) return unknown("its status has no heartbeat");
181
+ const poll = s["poll"];
182
+ if (!isObj(poll)) return unknown("its status has no poll record (an older daemon)");
183
+ const interval = poll["interval_s"];
184
+ if (typeof interval !== "number" || !(interval > 0))
185
+ return unknown("its poll record has no poll interval");
186
+ const step = interval * 1000;
187
+ if (r.now - beat > HEARTBEAT_INTERVALS * step)
188
+ return known({ state: "broken", cause: `no heartbeat for ${minutes(r.now - beat)}` });
189
+ const paused = loginPause(s, r.now);
190
+ if (paused) return known({ state: "paused-login", cause: paused });
191
+ const lastAt = typeof poll["last_at"] === "string" ? Date.parse(poll["last_at"]) : null;
192
+ if (poll["last_at"] === null) return known({ state: "starting" });
193
+ if (lastAt === null || !Number.isFinite(lastAt))
194
+ return unknown("its poll record has no readable time");
195
+ const checkedAt = lastAt;
196
+ if (poll["ok"] === false)
197
+ return known({
198
+ state: "broken",
199
+ cause: `the last poll failed: ${typeof poll["error"] === "string" ? poll["error"] : "no reason given"}`,
200
+ checkedAt,
201
+ });
202
+ if (poll["ok"] !== true) return unknown("its poll record does not say how the poll went");
203
+ if (r.now - lastAt > POLL_STALE_INTERVALS * step)
204
+ return known({
205
+ state: "broken",
206
+ cause: `no poll has finished for ${minutes(r.now - lastAt)}`,
207
+ checkedAt,
208
+ });
209
+ const cause = brokenCause(s, r.now);
210
+ if (cause) return known({ state: "broken", cause, checkedAt });
211
+ if (s["dry_run"] === true) return known({ state: "on-dry", checkedAt });
212
+ if (s["dry_run"] === false) return known({ state: "on-live", checkedAt });
213
+ return unknown("its status does not say whether it runs in dry-run");
214
+ }
215
+
216
+ /** The rounds running now, from status.json's text. */
217
+ export function runningRounds(text: string): Known<readonly RunningRound[]> {
218
+ const s = parseSnapshot(text);
219
+ if (typeof s === "string") return unknown(s);
220
+ if (!Array.isArray(s["prs"])) return unknown("its status lists no pull requests");
221
+ const out: RunningRound[] = [];
222
+ for (const p of s["prs"] as unknown[]) {
223
+ if (!isObj(p) || typeof p["number"] !== "number" || !isObj(p["current_round"])) continue;
224
+ const round = p["current_round"]["round_no"];
225
+ out.push({ pr: p["number"], round: typeof round === "number" ? round : 0 });
226
+ }
227
+ return known(out);
228
+ }
229
+
230
+ /** "a review of #12 is running and will be stopped", for one or more. */
231
+ export function runningWarning(rounds: Known<readonly RunningRound[]>): string {
232
+ if (rounds.kind === "unknown")
233
+ return `Whether a review is running is unknown (${rounds.why}); one that is will be stopped.`;
234
+ const n = rounds.value.map((r) => `#${r.pr}`);
235
+ if (n.length === 0) return "Nothing is running now.";
236
+ if (n.length === 1) return `A review of ${n[0]} is running and will be stopped.`;
237
+ return `Reviews of ${n.slice(0, -1).join(", ")} and ${n[n.length - 1]} are running and will be stopped.`;
238
+ }
239
+
240
+ // ---- the real adapter: this machine's compose stack ----------------------
241
+
242
+ /** A container's state as Docker reports it, folded. */
243
+ export function containerState(state: string | undefined): "running" | "restarting" | "stopped" {
244
+ if (state === undefined) return "stopped";
245
+ if (/restarting/i.test(state)) return "restarting";
246
+ return /running|up/i.test(state) ? "running" : "stopped";
247
+ }
248
+
249
+ /** One compose service's container, or unknown when Docker cannot answer. */
250
+ export async function serviceState(
251
+ runner: ProcessRunner,
252
+ root: string,
253
+ service: string,
254
+ ): Promise<Known<"running" | "restarting" | "stopped">> {
255
+ const ps = await composePs(runner, root);
256
+ if (!ps.ok) return unknown(ps.detail || "docker compose ps failed");
257
+ return known(containerState(serviceRow(ps.rows, service)?.state));
258
+ }
259
+
260
+ const firstLine = (s: string): string => s.trim().split("\n")[0]?.trim() || "no output";
261
+ const message = (err: unknown): string => (err instanceof Error ? err.message : String(err));
262
+
263
+ /** The config's `[github]` table and root `dry_run`, read again each time. */
264
+ function readConfig(path: string, read: (p: string) => string): Obj | string {
265
+ try {
266
+ const v = parse(read(path)) as unknown;
267
+ return isObj(v) ? v : `${path} is not a TOML table`;
268
+ } catch (err) {
269
+ return `can't read ${path}: ${message(err)}`;
270
+ }
271
+ }
272
+
273
+ export interface ComposeAutoReviewOptions extends SwitchOnStepOptions {
274
+ /** The app's root: the compose project directory, with `state/` under it. */
275
+ root: string;
276
+ configPath: () => string;
277
+ runner: ProcessRunner;
278
+ /** The wizard's adapters for the steps, with a line sink for progress. */
279
+ stepAdapters: (log: (line: string) => void) => OnboardAdapters;
280
+ now: () => number;
281
+ read?: (p: string) => string;
282
+ }
283
+
284
+ /** Auto-review on this machine: Docker for the container, the daemon's own
285
+ * status.json (inside its state volume) for the rest. */
286
+ export function composeAutoReview(o: ComposeAutoReviewOptions): AutoReviewPort {
287
+ const read = o.read ?? ((p: string) => readFileSync(p, "utf8"));
288
+ const statusText = async (): Promise<Known<string | null>> => {
289
+ const r = await daemonExec(o.runner, o.root, ["cat", `${CONTAINER_STATE_DIR}/status.json`]);
290
+ if (r.code === 0) return known(r.stdout);
291
+ if (/no such file/i.test(r.stderr)) return known(null);
292
+ return unknown(firstLine(r.stderr || r.stdout));
293
+ };
294
+ const config = (): Obj | string => readConfig(o.configPath(), read);
295
+ const engine = (log: (line: string) => void): OnboardingEngine =>
296
+ new OnboardingEngine(switchOnRegistry(o), o.stepAdapters(log));
297
+ const dryRunNow = (): boolean | null => {
298
+ const c = config();
299
+ return typeof c === "string" ? null : typeof c["dry_run"] === "boolean" ? c["dry_run"] : null;
300
+ };
301
+ const compose = async (args: string[], what: string): Promise<SwitchResult> => {
302
+ const r = await o.runner.run("docker", composeArgs(o.root, args));
303
+ return r.code === 0
304
+ ? { ok: true, detail: what }
305
+ : {
306
+ ok: false,
307
+ detail: `docker compose ${args.join(" ")} exited ${r.code}: ${firstLine(r.stderr || r.stdout)}`,
308
+ };
309
+ };
310
+
311
+ const control: AutoReviewControl = {
312
+ machineChecks: async () => {
313
+ const lines: string[] = [];
314
+ const r = await engine((l) => lines.push(l)).runStep(MACHINE_CHECKS_STEP_ID);
315
+ // the three the person is told about: Docker, disk, ports
316
+ const said = r.checks
317
+ .filter((c) => c.name === "docker" || c.name === "disk" || c.name === "ports")
318
+ .map((c) => c.detail)
319
+ .join(" · ");
320
+ return r.ok ? { ok: true, detail: said } : { ok: false, detail: r.failures.join("; ") };
321
+ },
322
+ dryRun: async () => {
323
+ const d = dryRunNow();
324
+ return d === null ? unknown(`can't read dry_run from ${o.configPath()}`) : known(d);
325
+ },
326
+ switchOn: async (answers, progress) => {
327
+ const c = config();
328
+ const github = typeof c === "string" ? undefined : c["github"];
329
+ const repo =
330
+ isObj(github) && typeof github["repo"] === "string" && github["repo"] !== ""
331
+ ? github["repo"]
332
+ : null;
333
+ return runSwitchOn({
334
+ checklist: engine((l) => progress?.(l)),
335
+ answers,
336
+ repo,
337
+ dryRun: dryRunNow,
338
+ stopMemory: () => compose(["stop", MEMORY_SERVICE], "memory stopped"),
339
+ startMemory: () => compose(["up", "-d", MEMORY_SERVICE], "the memory engine runs"),
340
+ ...(progress ? { progress } : {}),
341
+ });
342
+ },
343
+ proof: async () => {
344
+ const p = recordedProof(engine(() => {}));
345
+ return p ? { pr: p.pr, verdict: p.verdict } : null;
346
+ },
347
+ proofRound: (pr, progress) =>
348
+ runProofRound(
349
+ engine((l) => progress?.(l)),
350
+ pr,
351
+ ),
352
+ goLive: (answers) =>
353
+ runGoLive(
354
+ engine(() => {}),
355
+ answers,
356
+ ),
357
+ running: async () => {
358
+ const t = await statusText();
359
+ if (t.kind === "unknown") return t;
360
+ return t.value === null ? known([]) : runningRounds(t.value);
361
+ },
362
+ switchOff: () => compose(["stop", DAEMON_SERVICE], "the daemon is stopped; its data is kept"),
363
+ };
364
+
365
+ return {
366
+ status: async () => {
367
+ const container = await serviceState(o.runner, o.root, DAEMON_SERVICE);
368
+ const snapshot: Known<string | null> =
369
+ container.kind === "known" && container.value === "running"
370
+ ? await statusText()
371
+ : known(null);
372
+ return autoReviewLabel({ container, snapshot, now: o.now() });
373
+ },
374
+ trigger: async () => {
375
+ const c = config();
376
+ if (typeof c === "string") return unknown(c);
377
+ const github = c["github"];
378
+ const label = isObj(github) ? github["trigger_label"] : undefined;
379
+ return typeof label === "string" && label.trim() !== ""
380
+ ? known(label.trim())
381
+ : unknown(`${o.configPath()} has no [github] trigger_label`);
382
+ },
383
+ control,
384
+ };
385
+ }
@@ -0,0 +1,55 @@
1
+ // The daemon's status snapshot on this machine's compose stack, as the home
2
+ // reads it. The daemon writes status.json inside its state volume
3
+ // (docker-compose.yml `qcr-state`), never on a host path, so the home reads
4
+ // it through `docker compose exec` while the daemon runs. The spend line
5
+ // (src/home/spend.ts) and the Reviewed list's auto-review half
6
+ // (src/home/reviews.ts `autoRounds`) both read it here.
7
+ //
8
+ // A stack that can hold no snapshot recorded nothing: no Docker at all, a
9
+ // stack never brought up (no state volume), or a daemon that has not written
10
+ // one yet. A daemon that is off keeps its snapshot in its volume, where the
11
+ // home cannot read it: that is its own answer, never "nothing".
12
+ import type { ProcessRunner } from "../onboard/engine.js";
13
+ import {
14
+ COMPOSE_PROJECT,
15
+ CONTAINER_STATE_DIR,
16
+ DAEMON_SERVICE,
17
+ daemonExec,
18
+ } from "../onboard/steps/compose.js";
19
+ import { serviceState } from "./autoreview.js";
20
+
21
+ export type DaemonSnapshot =
22
+ | { kind: "text"; text: string }
23
+ /** Auto-review recorded nothing on this machine. */
24
+ | { kind: "none" }
25
+ /** The daemon is off; its snapshot is in its state volume. */
26
+ | { kind: "off" }
27
+ | { kind: "unknown"; why: string };
28
+
29
+ const firstLine = (s: string): string => s.trim().split("\n")[0]?.trim() || "no output";
30
+
31
+ /** The daemon's status.json on the compose stack at `root`. */
32
+ export async function daemonSnapshot(runner: ProcessRunner, root: string): Promise<DaemonSnapshot> {
33
+ const container = await serviceState(runner, root, DAEMON_SERVICE);
34
+ if (container.kind === "unknown") {
35
+ // no Docker at all: auto-review never ran on this machine
36
+ const v = await runner.run("docker", ["--version"]);
37
+ if (v.code !== 0 && /ENOENT|not found/i.test(`${v.stderr}${v.stdout}`)) return { kind: "none" };
38
+ return { kind: "unknown", why: `can't ask Docker about auto-review: ${container.why}` };
39
+ }
40
+ if (container.value === "running") {
41
+ const r = await daemonExec(runner, root, ["cat", `${CONTAINER_STATE_DIR}/status.json`]);
42
+ if (r.code === 0) return { kind: "text", text: r.stdout };
43
+ if (/no such file/i.test(r.stderr)) return { kind: "none" };
44
+ return { kind: "unknown", why: `can't read its status: ${firstLine(r.stderr || r.stdout)}` };
45
+ }
46
+ // off: did it ever run here? Its state volume is where its snapshot is.
47
+ const volume = `${COMPOSE_PROJECT}_qcr-state`;
48
+ const vol = await runner.run("docker", ["volume", "ls", "-q", "--filter", `name=^${volume}$`]);
49
+ if (vol.code !== 0)
50
+ return {
51
+ kind: "unknown",
52
+ why: `can't ask Docker for its state volume: ${firstLine(vol.stderr)}`,
53
+ };
54
+ return vol.stdout.trim() === "" ? { kind: "none" } : { kind: "off" };
55
+ }
@@ -0,0 +1,107 @@
1
+ // pi's own device-code login, for the home's setup checklist
2
+ // (src/home/setup.ts): the same login pi's /login runs, on the pi the chat
3
+ // runs in, so the credential lands where pi keeps it (its auth file, under
4
+ // its own lock) and the chat can use it at once, with no restart.
5
+ //
6
+ // pi 1.4.0 runs a login through its model runtime, `runtime.login(provider,
7
+ // "oauth", interaction)`, which drives the provider's own flow and then
8
+ // stores the credential. An extension reaches that runtime as
9
+ // `ctx.modelRegistry.runtime`: the registry is the facade pi hands
10
+ // extensions, and the plugin already reaches its runtime for a refresh
11
+ // (packages/pi-plugin/extensions/quickreview.ts hostAuth). VERIFY-ON-PIN: the
12
+ // field and the method are pi's internals, so a pi without them makes this
13
+ // port answer "unavailable", and the card points at pi's own /login instead.
14
+ //
15
+ // The openai-codex flow first asks which method: the interaction answers it
16
+ // with the device-code option, so the code appears with no prompt. Any other
17
+ // question pi's flow asks is refused, with /login named for it.
18
+ import type { DeviceCode, DeviceLoginPort, LoginFailureKind, LoginOutcome } from "./setup.js";
19
+
20
+ /** pi's login interaction, by the shape pi 1.4.0 hands a provider's flow. */
21
+ interface AuthPrompt {
22
+ type: string;
23
+ message?: string;
24
+ options?: ReadonlyArray<{ id: string; label?: string }>;
25
+ }
26
+ type AuthEvent =
27
+ | { type: "device_code"; userCode: string; verificationUri: string; expiresInSeconds?: number }
28
+ | { type: string; message?: string };
29
+
30
+ interface LoginRuntime {
31
+ login(
32
+ provider: string,
33
+ type: "oauth",
34
+ interaction: {
35
+ signal: AbortSignal;
36
+ prompt(p: AuthPrompt): Promise<string>;
37
+ notify(e: AuthEvent): void;
38
+ },
39
+ ): Promise<unknown>;
40
+ }
41
+
42
+ const message = (err: unknown): string => (err instanceof Error ? err.message : String(err));
43
+
44
+ /** A device login that ended in an error, as a person reads it. */
45
+ export function loginFailure(
46
+ err: unknown,
47
+ aborted: boolean,
48
+ ): {
49
+ kind: LoginFailureKind;
50
+ why: string;
51
+ } {
52
+ const why = message(err);
53
+ if (aborted || /login cancelled/i.test(why)) return { kind: "cancelled", why };
54
+ if (/timed out|expired|expir/i.test(why)) return { kind: "expired", why };
55
+ if (/not enabled/i.test(why)) return { kind: "not_enabled", why };
56
+ if (/denied|rejected|declined|access_denied/i.test(why)) return { kind: "denied", why };
57
+ return { kind: "failed", why };
58
+ }
59
+
60
+ /** The runtime behind pi's registry facade, when this pi has one. */
61
+ function runtimeOf(registry: unknown): LoginRuntime | null {
62
+ const runtime = (registry as { runtime?: { login?: unknown } } | null | undefined)?.runtime;
63
+ return typeof runtime?.login === "function" ? (runtime as LoginRuntime) : null;
64
+ }
65
+
66
+ /** The device login of the pi whose `ctx.modelRegistry` is `registry`. */
67
+ export function piDeviceLogin(registry: unknown): DeviceLoginPort {
68
+ return {
69
+ login: async ({ provider, signal, onCode }): Promise<LoginOutcome> => {
70
+ const runtime = runtimeOf(registry);
71
+ if (runtime === null)
72
+ return {
73
+ ok: false,
74
+ kind: "unavailable",
75
+ why: "this pi offers no device login to the home: type /login to sign in with pi's own menu",
76
+ };
77
+ try {
78
+ await runtime.login(provider, "oauth", {
79
+ signal,
80
+ prompt: async (p) => {
81
+ const device =
82
+ p.type === "select" ? p.options?.find((o) => /device/i.test(o.id)) : undefined;
83
+ if (device) return device.id;
84
+ throw new Error(
85
+ `pi's login asked "${p.message ?? p.type}", which the home does not answer: type /login to sign in with pi's own menu`,
86
+ );
87
+ },
88
+ notify: (e) => {
89
+ if (e.type === "device_code" && "userCode" in e) {
90
+ const code: DeviceCode = {
91
+ userCode: e.userCode,
92
+ verificationUri: e.verificationUri,
93
+ ...(e.expiresInSeconds !== undefined
94
+ ? { expiresInSeconds: e.expiresInSeconds }
95
+ : {}),
96
+ };
97
+ onCode(code);
98
+ }
99
+ },
100
+ });
101
+ return { ok: true };
102
+ } catch (err) {
103
+ return { ok: false, ...loginFailure(err, signal.aborted) };
104
+ }
105
+ },
106
+ };
107
+ }