@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
package/src/state.ts CHANGED
@@ -2,7 +2,10 @@
2
2
  // Core tables: pr_state, rounds, bot_comments (registry), deliveries
3
3
  // (webhook dedupe), events (append log).
4
4
  import { findingRowFields } from "@quickchatai/pi-quickreview/src/finding-row.js";
5
- import { roundStageDeadlines } from "@quickchatai/pi-quickreview/src/round-stage-deadlines.js";
5
+ import {
6
+ discoveryRowsOf,
7
+ roundStageDeadlines,
8
+ } from "@quickchatai/pi-quickreview/src/round-stage-deadlines.js";
6
9
  import Database from "better-sqlite3";
7
10
  import { chmodSync, mkdirSync, renameSync, writeFileSync } from "node:fs";
8
11
  import { dirname, join } from "node:path";
@@ -550,8 +553,9 @@ export type DrainState =
550
553
  /** When the hold lapses; null = until lifted (operator only). */
551
554
  expires_at: string | null;
552
555
  /** Why a SYSTEM engaged it (`worker_failures`, the crash-loop breaker); null
553
- * for a hand-set drain. A drain with a reason is lifted only by an explicit
554
- * `drain off`, never by a boot's first reconcile pass. */
556
+ * for a hand-set drain. A drain with a reason is never lifted by a boot's
557
+ * first reconcile pass: the breaker's lapses at its `expires_at`, or an
558
+ * explicit `drain off` lifts it sooner. */
555
559
  reason: string | null;
556
560
  /** The operator's no-auto-lift HOLD (`qcr drain on --hold`): no
557
561
  * boot's first reconcile lifts it, only an explicit operator `drain off`.
@@ -1050,7 +1054,8 @@ CREATE TABLE IF NOT EXISTS drain_state (
1050
1054
  -- with parked_at set gets no worker until the operator's rerun-round or
1051
1055
  -- restart-session clears the row. worker_breaker_global.epoch ticks at every
1052
1056
  -- worker success (and at the lift of the breaker's drain); a failure counts
1053
- -- toward the global drain only while it carries the current epoch.
1057
+ -- toward the global drain only while it carries the current epoch. trips
1058
+ -- counts the breaker's drains since then: each waits twice as long to lapse.
1054
1059
  CREATE TABLE IF NOT EXISTS worker_breaker (
1055
1060
  pr INTEGER PRIMARY KEY,
1056
1061
  failures INTEGER NOT NULL DEFAULT 0,
@@ -1064,6 +1069,7 @@ CREATE TABLE IF NOT EXISTS worker_breaker (
1064
1069
  CREATE TABLE IF NOT EXISTS worker_breaker_global (
1065
1070
  id INTEGER PRIMARY KEY CHECK (id = 1),
1066
1071
  epoch INTEGER NOT NULL DEFAULT 0,
1072
+ trips INTEGER NOT NULL DEFAULT 0,
1067
1073
  updated_at TEXT NOT NULL
1068
1074
  );
1069
1075
  -- The host lease: who holds a PR's session while the operator works
@@ -1828,6 +1834,7 @@ export class StateStore {
1828
1834
  this.db.exec("CREATE INDEX IF NOT EXISTS bot_comments_id ON bot_comments(comment_id)");
1829
1835
  this.migrateRecheckReceipts();
1830
1836
  this.migrateDrainHold();
1837
+ this.migrateBreakerHeal();
1831
1838
  this.migrateSessionCommandCancelledStatus();
1832
1839
  this.runDataMigrations();
1833
1840
  }
@@ -1932,6 +1939,37 @@ export class StateStore {
1932
1939
  })();
1933
1940
  }
1934
1941
 
1942
+ /**
1943
+ * The breaker's drain lapses on its own (src/workerbreaker.ts). One an older
1944
+ * daemon set stands with no expiry, and only `drain off` would lift it: the
1945
+ * boot that adds the `trips` column gives it an expiry of now, so the first
1946
+ * sweep lifts it and the queue retries, with the failures that tripped it
1947
+ * spent. Only the breaker's drain is carried
1948
+ * (its reason, no hold); a hand-set drain stands as it was set.
1949
+ */
1950
+ private migrateBreakerHeal(): void {
1951
+ this.db.transaction(() => {
1952
+ if (
1953
+ !this.addColumn(
1954
+ "ALTER TABLE worker_breaker_global ADD COLUMN trips INTEGER NOT NULL DEFAULT 0",
1955
+ )
1956
+ )
1957
+ return;
1958
+ const info = this.db
1959
+ .prepare(
1960
+ `UPDATE drain_state SET expires_at = ?
1961
+ WHERE id = 1 AND draining = 1 AND reason = ? AND expires_at IS NULL AND hold = 0`,
1962
+ )
1963
+ .run(this.ts(), WORKER_FAILURES_DRAIN_REASON);
1964
+ if (info.changes > 0) {
1965
+ // The failures that tripped it are spent, as at a new trip: a retry
1966
+ // failing once more must not re-drain with them.
1967
+ this.resetWorkerFailureWindow();
1968
+ this.logEvent("breaker_drain_heal_carried", null, {});
1969
+ }
1970
+ })();
1971
+ }
1972
+
1935
1973
  /** Existing databases carry a CHECK constraint that predates operator
1936
1974
  * cancellation. Rebuild this one table in-place; every column and index is
1937
1975
  * copied, and the transaction makes the old or new shape visible atomically. */
@@ -6696,18 +6734,31 @@ export class StateStore {
6696
6734
  * One more consecutive worker failure for `pr`. The PR is parked when the count
6697
6735
  * reaches `parkAfter`; `parked` says whether it stands parked now and
6698
6736
  * `newlyParked` whether THIS failure parked it (the caller raises the anomaly
6699
- * once).
6737
+ * once). With `drain: false` it does not count toward the global drain: its
6738
+ * epoch is one no window ever carries, so `failedWorkerPrsSince` never lists
6739
+ * it. With `park: false` it counts but does not park the PR by itself.
6700
6740
  */
6701
6741
  recordWorkerFailure(
6702
6742
  pr: number,
6703
- o: { commandId: string; error: string; at: string; parkAfter: number },
6743
+ o: {
6744
+ commandId: string;
6745
+ error: string;
6746
+ at: string;
6747
+ parkAfter: number;
6748
+ drain?: boolean;
6749
+ park?: boolean;
6750
+ },
6704
6751
  ): { failures: number; parked: boolean; newlyParked: boolean } {
6705
6752
  return this.db
6706
6753
  .transaction(() => {
6707
6754
  const row = this.workerBreaker(pr);
6708
6755
  const failures = (row?.failures ?? 0) + 1;
6709
6756
  const wasParked = row?.parked_at != null;
6710
- const parkedAt = wasParked ? row.parked_at : failures >= o.parkAfter ? o.at : null;
6757
+ const parkedAt = wasParked
6758
+ ? row.parked_at
6759
+ : o.park !== false && failures >= o.parkAfter
6760
+ ? o.at
6761
+ : null;
6711
6762
  this.db
6712
6763
  .prepare(
6713
6764
  `INSERT INTO worker_breaker (pr, failures, parked_at, last_error, last_failure_at, last_command_id, failure_epoch, updated_at)
@@ -6722,7 +6773,7 @@ export class StateStore {
6722
6773
  error: o.error,
6723
6774
  at: o.at,
6724
6775
  command: o.commandId,
6725
- epoch: this.workerEpoch(),
6776
+ epoch: o.drain === false ? -1 : this.workerEpoch(),
6726
6777
  });
6727
6778
  return {
6728
6779
  failures,
@@ -6770,12 +6821,31 @@ export class StateStore {
6770
6821
  return row?.epoch ?? 0;
6771
6822
  }
6772
6823
 
6773
- /** Start the global failure window afresh: earlier failures stop counting. */
6824
+ /** Start the global failure window afresh: earlier failures stop counting,
6825
+ * and the breaker's next drain waits the shortest time again. */
6774
6826
  resetWorkerFailureWindow(): void {
6775
6827
  this.db
6776
6828
  .prepare(
6777
- `INSERT INTO worker_breaker_global (id, epoch, updated_at) VALUES (1, 1, @ts)
6778
- ON CONFLICT(id) DO UPDATE SET epoch = epoch + 1, updated_at = @ts`,
6829
+ `INSERT INTO worker_breaker_global (id, epoch, trips, updated_at) VALUES (1, 1, 0, @ts)
6830
+ ON CONFLICT(id) DO UPDATE SET epoch = epoch + 1, trips = 0, updated_at = @ts`,
6831
+ )
6832
+ .run({ ts: this.ts() });
6833
+ }
6834
+
6835
+ /** The breaker's drains since the last worker success or operator lift. */
6836
+ workerBreakerTrips(): number {
6837
+ const row = this.db.prepare("SELECT trips FROM worker_breaker_global WHERE id = 1").get() as
6838
+ { trips: number } | undefined;
6839
+ return row?.trips ?? 0;
6840
+ }
6841
+
6842
+ /** The breaker drained once more: the failures that tripped it are spent (a
6843
+ * new epoch), and its next drain waits longer. */
6844
+ recordWorkerBreakerTrip(): void {
6845
+ this.db
6846
+ .prepare(
6847
+ `INSERT INTO worker_breaker_global (id, epoch, trips, updated_at) VALUES (1, 1, 1, @ts)
6848
+ ON CONFLICT(id) DO UPDATE SET epoch = epoch + 1, trips = trips + 1, updated_at = @ts`,
6779
6849
  )
6780
6850
  .run({ ts: this.ts() });
6781
6851
  }
@@ -7005,6 +7075,25 @@ export class StateStore {
7005
7075
  return someMarkHasKey(rows, key);
7006
7076
  }
7007
7077
 
7078
+ /** Has a check after this (pr, key)'s spent nudge already been recorded? */
7079
+ ledgerRepeatSeen(pr: number, key: string): boolean {
7080
+ const rows = this.db
7081
+ .prepare("SELECT payload FROM events WHERE kind = 'ledger_stale_repeat' AND pr = ?")
7082
+ .all(pr) as Array<{ payload: string | null }>;
7083
+ return someMarkHasKey(rows, key);
7084
+ }
7085
+
7086
+ /** The heads a recheck upheld on `pr` at or after `since` (every one when null). */
7087
+ recheckUpheldHeads(pr: number, since: string | null): string[] {
7088
+ const rows = this.db
7089
+ .prepare("SELECT payload FROM events WHERE kind = 'recheck_upheld' AND pr = ? AND ts >= ?")
7090
+ .all(pr, since ?? "") as Array<{ payload: string | null }>;
7091
+ // An unreadable mark names no head.
7092
+ return rows
7093
+ .map((r) => readStringField(r.payload, "head_sha"))
7094
+ .filter((head): head is string => head !== undefined && head !== "");
7095
+ }
7096
+
7008
7097
  /**
7009
7098
  * What the event log says about the migration-collision COMMENT with this
7010
7099
  * fingerprint on this PR (the collision comment's dedupe). Scans every such event for the
@@ -7781,9 +7870,27 @@ export interface OpenPrsSnapshot {
7781
7870
  prs: OpenPrSummary[];
7782
7871
  }
7783
7872
 
7873
+ /** The authoritative reconcile poll's last pass (status.json `poll`): what
7874
+ * "auto-review is working" is judged on, beside the heartbeat (the home's
7875
+ * label, src/home/autoreview.ts). */
7876
+ export interface PollStatus {
7877
+ /** When the last pass ran, ISO; null before the boot's first pass has ended. */
7878
+ last_at: string | null;
7879
+ /** That pass ran with no step error; null before the first pass. */
7880
+ ok: boolean | null;
7881
+ /** The first error, while not ok. */
7882
+ error?: string;
7883
+ /** The daemon's `[daemon] poll_interval_s`: freshness is judged against
7884
+ * the daemon's own cadence, never a reader's guess. */
7885
+ interval_s: number;
7886
+ }
7887
+
7784
7888
  export interface StatusSnapshot {
7785
7889
  written_at: string;
7786
7890
  dry_run: boolean;
7891
+ /** The reconcile poll's last pass. Absent in snapshots from an older
7892
+ * daemon — readers say "unknown", never "working". */
7893
+ poll?: PollStatus;
7787
7894
  /** Where the daemon runs (src/runhost.ts). Absent: not reported (an older
7788
7895
  * daemon, or one assembled without its host facts). */
7789
7896
  run_host?: import("./runhost.js").RunHostStatus;
@@ -7874,6 +7981,11 @@ export interface StatusSnapshot {
7874
7981
  max_concurrent_rounds?: number;
7875
7982
  total_spend_usd: number;
7876
7983
  today_spend_usd?: number;
7984
+ /** The month's spend so far: every round started in the snapshot's UTC
7985
+ * month (the same rule as `today_spend_usd`, expired rounds included).
7986
+ * Absent in snapshots from an older daemon: the home's spend line then
7987
+ * reads unknown, never a zero. */
7988
+ month_spend_usd?: number;
7877
7989
  digest_headline?: string;
7878
7990
  /** Present while pi sessions are failing API calls with token-expired
7879
7991
  * errors (packages/pi-plugin/src/authwatch.ts, fresh < 30 min). The TUI raises an alert. */
@@ -8155,6 +8267,7 @@ export function buildStatusSnapshot(
8155
8267
  started,
8156
8268
  roundStageBounds,
8157
8269
  round.sweep_owed === 1,
8270
+ discoveryRowsOf(round),
8158
8271
  );
8159
8272
  return {
8160
8273
  ...identity,
@@ -8224,6 +8337,8 @@ export function buildStatusSnapshot(
8224
8337
  ...(maxConcurrentRounds !== undefined ? { max_concurrent_rounds: maxConcurrentRounds } : {}),
8225
8338
  total_spend_usd: store.totalSpend(),
8226
8339
  today_spend_usd: roundUsage(store.roundsOn(today)).spend_usd,
8340
+ // roundsOn matches by prefix: the month's `YYYY-MM` is every day of it
8341
+ month_spend_usd: roundUsage(store.roundsOn(today.slice(0, 7))).spend_usd,
8227
8342
  ...(digestHeadline ? { digest_headline: digestHeadline } : {}),
8228
8343
  ...(piAuth
8229
8344
  ? {
package/src/takeover.ts CHANGED
@@ -82,6 +82,9 @@ export interface TakeoverPlan {
82
82
  cwd: string;
83
83
  /** The login's owner dir (`pihome/agent`): read-only, borrowed from. */
84
84
  agent_dir: string;
85
+ /** The login's dir when it is not `agent_dir` (the single box's shared
86
+ * login): the login is borrowed from here, the rest from `agent_dir`. */
87
+ login_dir?: string;
85
88
  /** The PR's JSONL, which the pi resumes and appends to. */
86
89
  session_file: string;
87
90
  model: string;
@@ -597,6 +600,7 @@ function rmQuiet(file: string): void {
597
600
  export function takeoverPlanFor(o: {
598
601
  root: string;
599
602
  agentDir: string;
603
+ loginDir?: string;
600
604
  sessionsRoot: string;
601
605
  model: string;
602
606
  pr: number;
@@ -612,6 +616,7 @@ export function takeoverPlanFor(o: {
612
616
  root: o.root,
613
617
  cwd: o.root,
614
618
  agent_dir: o.agentDir,
619
+ ...(o.loginDir !== undefined && o.loginDir !== o.agentDir ? { login_dir: o.loginDir } : {}),
615
620
  session_file: file,
616
621
  model: o.model,
617
622
  ...(o.systemPromptFile ? { system_prompt_file: o.systemPromptFile } : {}),
package/src/takeoverpi.ts CHANGED
@@ -91,6 +91,7 @@ export function readTakeoverSpec(file: string): TakeoverSpec {
91
91
  raw.session !== `takeover-pr-${raw.pr}` ||
92
92
  typeof raw.session_file !== "string" ||
93
93
  typeof raw.agent_dir !== "string" ||
94
+ (raw.login_dir !== undefined && typeof raw.login_dir !== "string") ||
94
95
  typeof raw.borrow_dir !== "string" ||
95
96
  typeof raw.model !== "string" ||
96
97
  typeof raw.cwd !== "string" ||
@@ -119,11 +120,11 @@ export function readLogin(agentDir: string): string | null {
119
120
  * the owner's dir.
120
121
  */
121
122
  export function prepareBorrowedAgent(
122
- spec: Pick<TakeoverSpec, "agent_dir" | "borrow_dir" | "idle_ms">,
123
+ spec: Pick<TakeoverSpec, "agent_dir" | "login_dir" | "borrow_dir" | "idle_ms">,
123
124
  now?: () => number,
124
125
  ): { expiresAt: number | null } {
125
126
  const { expiresAt } = copyPiAuth({
126
- srcDir: spec.agent_dir,
127
+ srcDir: spec.login_dir ?? spec.agent_dir,
127
128
  dstDir: spec.borrow_dir,
128
129
  needMs: spec.idle_ms + PI_REFRESH_MARGIN_MS,
129
130
  words: ENGINE_AUTH_WORDS,
package/src/tui/alerts.ts CHANGED
@@ -30,6 +30,7 @@ import { zoneDateString } from "../eod.js";
30
30
  import { curationNeedsAttention } from "../learner/status.js";
31
31
  import { memoryStatusLine, type MemoryStatus } from "../memory/health.js";
32
32
  import type { StatusSnapshot } from "../state.js";
33
+ import { DEFAULT_BREAKER_LIMITS } from "../workerbreaker.js";
33
34
 
34
35
  export type AlertTier = "notice" | "alert";
35
36
 
@@ -616,7 +617,11 @@ export interface WorkerCrashAlertMemory {
616
617
  export function maybeWriteWorkerCrashAlerts(
617
618
  alertsDir: string,
618
619
  snapshot:
619
- Pick<StatusSnapshot, "prs" | "draining" | "draining_reason" | "draining_since"> | undefined,
620
+ | Pick<
621
+ StatusSnapshot,
622
+ "prs" | "draining" | "draining_reason" | "draining_since" | "draining_until"
623
+ >
624
+ | undefined,
620
625
  fresh: boolean,
621
626
  mem: WorkerCrashAlertMemory,
622
627
  now: () => Date = () => new Date(),
@@ -660,7 +665,9 @@ export function maybeWriteWorkerCrashAlerts(
660
665
  "worker-failures-drain",
661
666
  null,
662
667
  "workers failing on several PRs: the bot is draining",
663
- "Workers failed on three PRs within 15 minutes with no success, so the bot drains: no round, answer or debug worker starts. Fix the cause, then run drain off.",
668
+ snapshot.draining_until
669
+ ? `Workers failed on three PRs within 15 minutes with no success, so the bot drains: no round, answer or debug worker starts. It lifts itself at ${snapshot.draining_until} and the queue retries; if workers fail again it drains again, each time waiting twice as long (up to ${DEFAULT_BREAKER_LIMITS.healMaxMs / 3_600_000} h). Run drain off to retry now.`
670
+ : "Workers failed on three PRs within 15 minutes with no success, so the bot drains: no round, answer or debug worker starts. Fix the cause, then run drain off.",
664
671
  );
665
672
  return names;
666
673
  }
package/src/tui/board.ts CHANGED
@@ -522,7 +522,7 @@ function barSegments(view: BoardView, alerts: boolean): string[] {
522
522
  }
523
523
  // Short on the bar, where every segment competes for the width; the list
524
524
  // carries the full sentence with the next step.
525
- const setupSeg = view.setup ? `${WARN}▲ setup not finished — qcr onboard${DRESET}` : "";
525
+ const setupSeg = view.setup ? `${WARN}▲ setup not finished${DRESET}` : "";
526
526
  const s = view.snapshot;
527
527
  const rounds = s ? `rounds ${s.running_rounds}/${s.max_concurrent_rounds ?? "?"}` : "";
528
528
  const spend = s?.today_spend_usd !== undefined ? `today $${s.today_spend_usd.toFixed(2)}` : "";
@@ -534,7 +534,7 @@ function barSegments(view: BoardView, alerts: boolean): string[] {
534
534
  // guessed at. The operator's HOLD says that no boot lifts it; an
535
535
  // operator drain from a daemon that reports no hold marker says "hold ?".
536
536
  const drainSeg = s?.draining
537
- ? `${FG.anomaly}⛔ DRAINING — ${s.running_rounds} round${s.running_rounds === 1 ? "" : "s"} finishing · ${s.drain_failed_loud ?? 0} failed loud · ${s.drained_deliveries ?? 0} dropped · ${leverHolderLabel(s.draining_by, s.draining_until)}${drainHoldLabel(s)}${s.draining_reason === "worker_failures" ? " · workers keep failing, drain off lifts it" : ""}${RESET}`
537
+ ? `${FG.anomaly}⛔ DRAINING — ${s.running_rounds} round${s.running_rounds === 1 ? "" : "s"} finishing · ${s.drain_failed_loud ?? 0} failed loud · ${s.drained_deliveries ?? 0} dropped · ${leverHolderLabel(s.draining_by, s.draining_until)}${drainHoldLabel(s)}${s.draining_reason === "worker_failures" ? (s.draining_until ? " · workers kept failing, it retries when it lapses; drain off lifts it now" : " · workers keep failing, drain off lifts it") : ""}${RESET}`
538
538
  : "";
539
539
  const td = s?.thread_decisions;
540
540
  const breakerSeg =
@@ -45,6 +45,7 @@ import { COMPANION_IMAGE, type CompanionConfig } from "../config.js";
45
45
  import { writeAtomic } from "../channelfs.js";
46
46
  import { appStamp } from "../appstamp.js";
47
47
  import { TOKEN_FILE } from "./ghread.js";
48
+ import { MEMORY_PORT } from "./memoryurl.js";
48
49
 
49
50
  export interface DockerResult {
50
51
  ok: boolean;
@@ -274,7 +275,7 @@ export const MEMORY = {
274
275
  subnet: "198.51.100.0/24",
275
276
  gateway: "198.51.100.1",
276
277
  /** Same port the server listens on in the VM container ([openviking] port). */
277
- port: 1933,
278
+ port: MEMORY_PORT,
278
279
  /** The OpenViking user whose memory the companion owns — becomes the
279
280
  * `X-OpenViking-User` header on every request it makes. */
280
281
  user: "companion",
@@ -1,6 +1,6 @@
1
1
  // `[5] deploy` — "what would a deploy ship?" plus the deploy button.
2
- // Every deploy is a RELEASE: the tag, the two-arch build, the paid e2e and the
3
- // owner's approval already happened in CI, so the tab only chooses a version.
2
+ // Every deploy is a RELEASE: the tag and the two-arch build with its smoke
3
+ // already happened in CI, so the tab only chooses a version.
4
4
  // It shows the version and revision the VM runs (`qcr-update status --json`,
5
5
  // read over ssh) against the chosen version, the commits between the two
6
6
  // revisions (from the local checkout), and deploy.sh's own
package/src/tui/index.ts CHANGED
@@ -114,8 +114,8 @@ troubleshooting:
114
114
  project): every read is docker exec into the daemon
115
115
  container, no ssh. The board names an absent or stopped
116
116
  container with the docker compose command that starts it
117
- setup not finished the onboarding checklist is not complete: run qcr onboard
118
- (the board names the next step)
117
+ setup not finished the setup checklist is not complete: run qcr (the board
118
+ names the next step)
119
119
  ▲ DATA STALE ssh works but status.json stopped updating — the
120
120
  container is down or the daemon hung (qcr-side:
121
121
  ssh <host> sudo docker ps / docker logs)
@@ -454,11 +454,14 @@ export async function runTui(argv: string | string[] = [], deps: RunTuiDeps = {}
454
454
 
455
455
  const fileConfig = deps.config ?? loadConfig();
456
456
  // The route — which daemon the board reads, and how — from [tui] plus the
457
- // facts this checkout holds locally (the compose project's .env, the
458
- // onboarding checklist, the Docker socket's mode). Two small file reads and
459
- // one access(2): no docker, network or walk, so it may precede the frame.
457
+ // facts the stack holds locally (the compose project's .env, the
458
+ // onboarding checklist, the Docker socket's mode). The stack is the one the
459
+ // home sets up (src/homes.ts `stackRoot`), so an app installed by a package
460
+ // manager reads it from its state home. Two small file reads and one
461
+ // access(2): no docker, network or walk, so it may precede the frame.
460
462
  const { readRouteFacts, resolveTuiRoute, pinnedVersion } = await import("./route.js");
461
- const routeFacts = deps.routeFacts ?? readRouteFacts(sandboxRoot());
463
+ const { stackRoot } = await import("../homes.js");
464
+ const routeFacts = deps.routeFacts ?? readRouteFacts(stackRoot(process.env, sandboxRoot()));
462
465
  const route = resolveTuiRoute(fileConfig.tui, routeFacts);
463
466
  const loaded: Config = { ...fileConfig, tui: route.cfg };
464
467
  // In the containerised TUI the companion is a container the host wrapper
package/src/tui/memory.ts CHANGED
@@ -52,7 +52,7 @@ import {
52
52
  import { countOutcome, emptyRequestCounter, type RequestCounter } from "../memory/health.js";
53
53
  import { OpenVikingStore } from "../openviking/store.js";
54
54
  import { readRetiredUrisFile, writeRetiredUris } from "../memory/ovsession.js";
55
- import { keysFilePath, MEMORY } from "./companion.js";
55
+ import { keysFilePath } from "./companion.js";
56
56
 
57
57
  export { matchesQuery, normalizeTag, refOf } from "../memory/store.js";
58
58
  export type {
@@ -227,20 +227,8 @@ export function keysFileReader(
227
227
  };
228
228
  }
229
229
 
230
- /**
231
- * Where the TUI reaches OpenViking: the memory forward's loopback listener.
232
- *
233
- * `QCR_MEMORY_URL` overrides it wholesale; `QCR_MEMORY_PORT` matches the flag
234
- * the forward itself takes, so a non-default port only has to be spelled once.
235
- * `MEMORY.port` is the shared constant the forward and the companion already
236
- * agree on.
237
- */
238
- export function tuiMemoryBaseUrl(env: NodeJS.ProcessEnv = process.env): string {
239
- const explicit = (env["QCR_MEMORY_URL"] ?? env["QCR_MEMORY_ENDPOINT"] ?? "").trim();
240
- if (explicit !== "") return explicit.replace(/\/+$/, "");
241
- const port = Number(env["QCR_MEMORY_PORT"] ?? MEMORY.port) || MEMORY.port;
242
- return `http://127.0.0.1:${port}`;
243
- }
230
+ import { tuiMemoryBaseUrl } from "./memoryurl.js";
231
+ export { tuiMemoryBaseUrl };
244
232
 
245
233
  // ---- the store ---------------------------------------------------------------
246
234
 
@@ -0,0 +1,22 @@
1
+ // Where this host reaches OpenViking: the memory forward's loopback
2
+ // listener. A leaf module, so a reader of the URL (the wizard's memory step,
3
+ // the home) never imports the TUI's memory store or the companion.
4
+
5
+ /** The port the memory forward and the companion agree on, the server's own
6
+ * in the VM container (`[openviking] port`). */
7
+ export const MEMORY_PORT = 1933;
8
+
9
+ /**
10
+ * Where the TUI reaches OpenViking: the memory forward's loopback listener.
11
+ *
12
+ * `QCR_MEMORY_URL` overrides it wholesale; `QCR_MEMORY_PORT` matches the flag
13
+ * the forward itself takes, so a non-default port only has to be spelled once.
14
+ * `MEMORY_PORT` is the shared constant the forward and the companion already
15
+ * agree on.
16
+ */
17
+ export function tuiMemoryBaseUrl(env: NodeJS.ProcessEnv = process.env): string {
18
+ const explicit = (env["QCR_MEMORY_URL"] ?? env["QCR_MEMORY_ENDPOINT"] ?? "").trim();
19
+ if (explicit !== "") return explicit.replace(/\/+$/, "");
20
+ const port = Number(env["QCR_MEMORY_PORT"] ?? MEMORY_PORT) || MEMORY_PORT;
21
+ return `http://127.0.0.1:${port}`;
22
+ }
package/src/tui/remote.ts CHANGED
@@ -341,7 +341,7 @@ export function killInFlightSsh(): number {
341
341
  * die on "Could not resolve hostname" and every surface would print an ssh
342
342
  * remedy with no host in it. */
343
343
  export const NO_SSH_HOST_STDERR =
344
- '[tui] ssh_host is empty — run `qcr onboard`, or set [tui] target = "local"';
344
+ '[tui] ssh_host is empty — set it to the VM\'s ssh alias, or set [tui] target = "local"';
345
345
 
346
346
  export const realSshRunner: SshRunner = (host, argv, opts) =>
347
347
  host.trim() === ""
package/src/tui/route.ts CHANGED
@@ -109,7 +109,7 @@ export function resolveTuiRoute(file: TuiConfig, facts: RouteFacts): TuiRoute {
109
109
  };
110
110
  notes.push(
111
111
  `[tui] target is "ssh" with no ssh_host — reading the local compose stack in ${facts.projectDir} instead ` +
112
- '(run `qcr onboard`, or set [tui] target = "local", to make it explicit)',
112
+ '(set [tui] target = "local" to make it explicit)',
113
113
  );
114
114
  } else {
115
115
  return {
@@ -117,7 +117,7 @@ export function resolveTuiRoute(file: TuiConfig, facts: RouteFacts): TuiRoute {
117
117
  notes,
118
118
  unconfigured:
119
119
  topology === "vm"
120
- ? "[tui] ssh_host is empty — the VM route's ssh alias is written by onboarding: run `qcr onboard`"
120
+ ? "[tui] ssh_host is empty — set it to the VM's ssh alias in config.toml"
121
121
  : EMPTY_SSH_HOST,
122
122
  };
123
123
  }
@@ -1,25 +1,27 @@
1
- // Is onboarding finished? Read from the checklist file the wizard keeps
2
- // (src/onboard/engine.ts), so a TUI opened halfway through `qcr onboard` says
3
- // so on its first frame instead of troubleshooting a daemon nobody has started
4
- // yet.
1
+ // Is setup finished? Read from the checklist file switch-on and Advanced keep
2
+ // (src/onboard/engine.ts), so a TUI opened halfway through says so on its
3
+ // first frame instead of troubleshooting a daemon nobody has started yet.
5
4
  //
6
5
  // Only a checklist that EXISTS and is not complete counts: an install that
7
- // never ran the wizard (a hand-written config, the VM operator's own setup)
8
- // has no checklist and gets no banner. The next step is the first enabled step,
9
- // in the wizard's own order, that the file does not record as done — the same
10
- // predicates the engine applies, so the TUI cannot name a step the wizard would
11
- // skip.
6
+ // never switched auto-review on (a hand-written config, the VM operator's own
7
+ // setup) has no checklist and gets no banner. Setup is finished in the home
8
+ // (a bare `qcr`), so the steps judged are the home's: switch-on's and
9
+ // Advanced's registry (src/home/advanced.ts), with the engine's own
10
+ // predicates. The next step is the first enabled, required one the file does
11
+ // not record as done; when there is none, nothing is left that the home could
12
+ // finish, and there is no banner (the home never runs the wizard's finish, so
13
+ // its checklist need not carry the wizard's completion marker).
12
14
  import { type ChecklistFile } from "../onboard/engine.js";
13
15
 
14
16
  export interface SetupState {
15
- /** The title of the first pending step ("the final verification" when only
16
- * finish is left), or undefined when it could not be computed. */
17
+ /** The title of the first pending step, or undefined when it could not be
18
+ * computed. */
17
19
  next: string | undefined;
18
20
  }
19
21
 
20
22
  /** The one sentence every surface uses. */
21
23
  export function setupLine(s: SetupState): string {
22
- return `setup not finished — run \`qcr onboard\` to continue${s.next ? ` (next: ${s.next})` : ""}`;
24
+ return `setup not finished — run \`qcr\` to finish setting up${s.next ? ` (next: ${s.next})` : ""}`;
23
25
  }
24
26
 
25
27
  /** Complete now: finished once and not reopened since (engine.ts `completeNow`). */
@@ -27,22 +29,24 @@ function completeNow(file: ChecklistFile): boolean {
27
29
  return typeof file.completed_at === "string" && !file.reopened_at;
28
30
  }
29
31
 
30
- /** undefined = nothing to say (no checklist, or onboarding is complete). */
32
+ /** undefined = nothing to say (no checklist, setup is complete, or the home
33
+ * has no step left to finish). */
31
34
  export async function setupStateOf(
32
35
  checklist: ChecklistFile | null,
33
36
  ): Promise<SetupState | undefined> {
34
37
  if (checklist === null || completeNow(checklist)) return undefined;
35
38
  let next: string | undefined;
36
39
  try {
37
- const { defaultSteps } = await import("../onboard/steps/index.js");
38
- const steps = [...defaultSteps()].sort((a, b) => a.order - b.order);
40
+ const { advancedRegistry } = await import("../home/advanced.js");
41
+ const steps = [...advancedRegistry().steps()].sort((a, b) => a.order - b.order);
39
42
  const pending = steps.find(
40
43
  (s) =>
41
44
  !s.optional &&
42
45
  (s.enabled ? s.enabled(checklist.choices) : true) &&
43
46
  checklist.steps[s.id]?.status !== "done",
44
47
  );
45
- next = pending?.title ?? "the final verification";
48
+ if (pending === undefined) return undefined;
49
+ next = pending.title;
46
50
  } catch {
47
51
  // The step registry could not be loaded: the banner still says setup is
48
52
  // unfinished, without naming a step it could not compute.
@@ -584,7 +584,7 @@ export async function triageConnection(
584
584
  return {
585
585
  kind: "unconfigured",
586
586
  cause: deps.unconfigured ?? NO_SSH_HOST_STDERR,
587
- remedy: "qcr onboard",
587
+ remedy: "set [tui] ssh_host in config.toml",
588
588
  };
589
589
  }
590
590
  const exists = deps.exists ?? existsSync;
package/src/verdicts.ts CHANGED
@@ -80,6 +80,7 @@ import {
80
80
  import { estimateUsd, type ModelPricing } from "./usage.js";
81
81
  import { TranscriptIndex } from "./transcriptindex.js";
82
82
  import { withinBudget } from "@quickchatai/pi-quickreview/src/budget.js";
83
+ import { VALIDATE_PASS } from "@quickchatai/pi-quickreview/src/include.js";
83
84
  import type { GateResult } from "@quickchatai/pi-quickreview/src/gate.js";
84
85
  import { applyEvent, restoreStandingLifecycle } from "./statemachine.js";
85
86
  import type { VerdictAnnouncer } from "./slack/announce.js";
@@ -505,6 +506,12 @@ function plannedTierMismatches(passes: VerdictPassEntry[], rows: PlanRow[]): str
505
506
  return wrong;
506
507
  }
507
508
 
509
+ /** Every lens of the stage reported its candidate count, and every count is zero. */
510
+ function nothingToValidate(passes: VerdictPassEntry[]): boolean {
511
+ const lenses = passes.filter((p) => p.name !== VALIDATE_PASS);
512
+ return lenses.length > 0 && lenses.every((p) => p.findings === 0);
513
+ }
514
+
508
515
  function comparePlan(
509
516
  passes: VerdictPassEntry[],
510
517
  plan: PlanRow[] | null,
@@ -3003,6 +3010,12 @@ export class VerdictWatcher {
3003
3010
  const deltaPasses = followUp ? v.passes.filter((p) => p.phase !== "sweep") : v.passes;
3004
3011
  const sweepPasses = followUp ? v.passes.filter((p) => p.phase === "sweep") : [];
3005
3012
  const main = comparePlan(deltaPasses, plan, addable, legacyPlanned, true);
3013
+ // A follow-up's delta stage dispatches no validator when its lenses
3014
+ // found nothing (prompts/round-follow-up.md, "Nothing to validate ⇒ no
3015
+ // validator"): that absence keeps the rule. Only a count every lens
3016
+ // reported as zero says so; an unreported one still owes the validator.
3017
+ if (followUp && nothingToValidate(deltaPasses))
3018
+ main.missing = main.missing.filter((name) => name !== VALIDATE_PASS);
3006
3019
  let sweep: PlanComparison | null = null;
3007
3020
  if (sweepPasses.length > 0 || sweepStatus === "ran") {
3008
3021
  const sweepPlan = round.sweep_plan ? (JSON.parse(round.sweep_plan) as PlanRow[]) : [];