@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
@@ -137,6 +137,10 @@
137
137
  "post": {
138
138
  "$ref": "#/$defs/post"
139
139
  },
140
+ "post_ready": {
141
+ "type": "string",
142
+ "description": "A finished PR review that was not posted: the file it is kept in, which a host that asks the person later posts through the gate exactly as `--to pr` would have."
143
+ },
140
144
  "run_id": {
141
145
  "type": "string",
142
146
  "description": "A detached run's id (start, status, stop)."
@@ -149,6 +153,14 @@
149
153
  "type": "string",
150
154
  "description": "What a detached run reviews."
151
155
  },
156
+ "started_at": {
157
+ "type": "number",
158
+ "description": "When a detached run was started, ms since the epoch."
159
+ },
160
+ "ended_at": {
161
+ "type": "number",
162
+ "description": "When a detached run ended (its exit or its stop), ms since the epoch; absent while it runs."
163
+ },
152
164
  "elapsed_s": {
153
165
  "type": "integer",
154
166
  "minimum": 0
@@ -165,6 +177,50 @@
165
177
  "items": {
166
178
  "type": "string"
167
179
  }
180
+ },
181
+ "live": {
182
+ "type": "object",
183
+ "description": "The running round's live log, projected: what the parent and each lens are doing now.",
184
+ "properties": {
185
+ "round": { "type": "integer", "minimum": 1 },
186
+ "parent_status": { "enum": ["active", "settled"] },
187
+ "last_activity": { "type": "string" },
188
+ "updated_at": { "type": "string" },
189
+ "active_tool": { "type": "string" },
190
+ "agents": {
191
+ "type": "array",
192
+ "items": {
193
+ "type": "object",
194
+ "required": ["agent", "status", "duration_ms", "tool_count"],
195
+ "properties": {
196
+ "agent": { "type": "string" },
197
+ "status": { "enum": ["running", "completed", "failed"] },
198
+ "duration_ms": {
199
+ "type": "number",
200
+ "minimum": 0,
201
+ "description": "The lens's time so far; a running lens's counts from its start between updates."
202
+ },
203
+ "tool_count": {
204
+ "type": "number",
205
+ "minimum": 0,
206
+ "description": "Tool calls the lens has started; a finished lens keeps its final count."
207
+ },
208
+ "current_tool": {
209
+ "type": "string",
210
+ "description": "The running (or last) tool, as `<tool>: <args>` or the bare name."
211
+ },
212
+ "current_target": {
213
+ "type": "string",
214
+ "description": "While the lens runs: the path its current tool works on, when it names one."
215
+ },
216
+ "line": {
217
+ "type": "string",
218
+ "description": "The lens as one line a person reads: `security · read src/x.ts · 7 tools · 0:41`."
219
+ }
220
+ }
221
+ }
222
+ }
223
+ }
168
224
  }
169
225
  }
170
226
  },
@@ -234,6 +290,10 @@
234
290
  "kind": {
235
291
  "enum": ["pr", "branch", "uncommitted", "diff"],
236
292
  "description": "What is reviewed: a pull request, the current branch against its base, the uncommitted working tree, or a unified diff handed in (`--diff`). Only a pr target can be posted."
293
+ },
294
+ "uncommitted": {
295
+ "const": true,
296
+ "description": "A branch target reviewed together with the working tree's uncommitted edits, as one review. Absent otherwise."
237
297
  }
238
298
  }
239
299
  },
package/scripts/deploy.sh CHANGED
@@ -46,9 +46,10 @@
46
46
  # cloudflared when the inventory enables it), the
47
47
  # containers, the image stamp, the pi settings and the
48
48
  # reviewer stub, and the state/status.json heartbeat the
49
- # status collector reads. It also warns 14 days before the
50
- # VM's ghcr pull token expires (the expiry that
51
- # scripts/vm-ghcr-login.sh recorded).
49
+ # status collector reads. It also warns 14 days before a
50
+ # private mirror's ghcr pull token expires (the expiry
51
+ # that scripts/vm-ghcr-login.sh recorded; the public
52
+ # release images need no login, so no record).
52
53
  #
53
54
  # `sync`, `dev` and `rebuild` are gone (the on-VM rsync and build are deleted):
54
55
  # they refuse with a pointer to `release`. A pre-release fix is a release.
@@ -1083,8 +1084,9 @@ release_problem() {
1083
1084
  fi
1084
1085
  }
1085
1086
 
1086
- # The VM's ghcr pull token expires (1 year, by ruling), and a lapsed one fails
1087
- # the updater's pull. scripts/vm-ghcr-login.sh records the expiry on this
1087
+ # The release images are public and pull with no login. A private mirror's
1088
+ # ghcr pull token expires (1 year, by ruling), and a lapsed one fails the
1089
+ # updater's pull. scripts/vm-ghcr-login.sh records the expiry on this
1088
1090
  # controller; release and verify read it back. Within GHCR_WARN_DAYS of it
1089
1091
  # (or past it) the plan line starts with WARNING. Warn only, never a refusal.
1090
1092
  GHCR_WARN_DAYS=14
@@ -1095,7 +1097,7 @@ ghcr_expiry_status() {
1095
1097
  local file iso epoch now days
1096
1098
  file="$(qcr_ghcr_expiry_file 2>/dev/null)" || file=""
1097
1099
  if [[ -z "$file" || ! -s "$file" ]]; then
1098
- GHCR_EXPIRY_LINE="no recorded expiry for the VM's ghcr pull token (scripts/vm-ghcr-login.sh records it when the owner logs the VM in)"
1100
+ GHCR_EXPIRY_LINE="no ghcr login recorded: the release images are public and pull without one (a private mirror logs the VM in with scripts/vm-ghcr-login.sh, which records its expiry)"
1099
1101
  return 0
1100
1102
  fi
1101
1103
  iso="$(head -1 "$file" | tr -d '[:space:]')"
@@ -12,7 +12,10 @@
12
12
  # `stop` find it by label. `stop` sends `docker stop -t
13
13
  # <grace>`, then `docker kill`: the container is the
14
14
  # hard-kill backstop for a driver whose loop is frozen.
15
- # qcr a bare qcr opens the operator TUI (tui mode)
15
+ # qcr a bare qcr (or `qcr home`) opens the home: the image's
16
+ # pi with the plugin and its panel (home mode)
17
+ # qcr tui the operator TUI (tui mode), the one verb that starts
18
+ # the companion
16
19
  # qcr login [provider] pi's own /login, stored in the qcr-login volume
17
20
  # qcr update [version] pull a newer image and repin this file
18
21
  #
@@ -21,8 +24,8 @@
21
24
  # a worktree's common git dir at its own path read-only
22
25
  # the qcr-state volume /opt/qcr/state read-write
23
26
  # the qcr-login volume /qcr-login read-only
24
- # (only `login` mounts it read-write,
25
- # at pi's agent dir; its files are in
27
+ # (only `login` and the home mount it
28
+ # read-write, at pi's agent dir; its files are in
26
29
  # group 4747, which every call joins,
27
30
  # so a uid change never loses the login)
28
31
  # ~/.config/qcr/profiles /qcr-config/profiles read-only
@@ -219,11 +222,20 @@ split=1
219
222
  drop=0
220
223
  gh=1
221
224
  case "$verb" in
225
+ # The home is the person's own pi, as `qcr` from npm opens it: pi's agent
226
+ # dir on the login volume, read-write (its device login lands there), its
227
+ # config and sessions on the state volume, and the GitHub token for its `gh`.
222
228
  '')
223
- mode=tui
229
+ mode=home
224
230
  tty=1
225
231
  split=0
226
232
  ;;
233
+ home)
234
+ mode=home
235
+ tty=1
236
+ split=0
237
+ drop=1
238
+ ;;
227
239
  tui)
228
240
  mode=tui
229
241
  tty=1
@@ -555,7 +567,8 @@ fi
555
567
  # ---- the GitHub token (GITHUB_TOKEN, GH_TOKEN, gh; no App on a laptop) -----
556
568
 
557
569
  # The claim call only reads git: it needs no token, and a token file is read once.
558
- if [ "$split" = 1 ] && [ "$gh" = 1 ] && [ "$phase" != claim ]; then
570
+ # The home is handed it too: its `gh` lists and opens the pull requests.
571
+ if { [ "$split" = 1 ] || [ "$mode" = home ]; } && [ "$gh" = 1 ] && [ "$phase" != claim ]; then
559
572
  token="${GITHUB_TOKEN:-}"
560
573
  [ -n "$token" ] || token="${GH_TOKEN:-}"
561
574
  if [ -z "$token" ] && command -v gh >/dev/null 2>&1; then
@@ -603,6 +616,16 @@ if [ "$mode" = login ]; then
603
616
  # pi's own /login, written to the login volume: the only read-write mount of
604
617
  # it. A login needs no repo, no state and no profiles.
605
618
  set -- "$@" --mount "type=volume,src=qcr-login,dst=/home/qcr/.pi/agent"
619
+ elif [ "$mode" = home ]; then
620
+ # The home works on a GitHub repository, not on the directory you stand in:
621
+ # no repo mount. pi's agent dir is the login volume, read-write, as login's
622
+ # is. Its state home, sessions and config.toml (the repository it picks,
623
+ # which it writes) live on the state volume, so they persist and stay
624
+ # writable; the install home is never mounted.
625
+ set -- "$@" --mount "type=volume,src=qcr-login,dst=/home/qcr/.pi/agent"
626
+ set -- "$@" --mount "type=volume,src=qcr-state,dst=/opt/qcr/state"
627
+ set -- "$@" -e QCR_STATE_HOME=/opt/qcr/state/home
628
+ set -- "$@" -e QCR_CONFIG=/opt/qcr/state/home/config.toml
606
629
  else
607
630
  # The repo, read-only; a worktree's common git dir at its own path.
608
631
  set -- "$@" --mount "type=bind,src=$root,dst=/repo,readonly" -w "/repo$rel"
@@ -1,7 +1,10 @@
1
1
  #!/usr/bin/env bash
2
- # vm-ghcr-login.sh — log the review VM in to ghcr.io, so it can pull the
3
- # release images. Run by the owner, on the controller. Idempotent: running it
4
- # again replaces the login and the recorded expiry.
2
+ # vm-ghcr-login.sh — log the review VM in to ghcr.io, so it can pull images
3
+ # that are not public: a private mirror named by QC_RELEASE_IMAGE and
4
+ # QC_RELEASE_MEMORY_IMAGE. The release images are public and pull with no
5
+ # login, so a VM on them needs none of this. Run by the owner, on the
6
+ # controller. Idempotent: running it again replaces the login and the recorded
7
+ # expiry.
5
8
  #
6
9
  # The credential is a CLASSIC personal access token, scoped read:packages and
7
10
  # nothing else (the one carve-out from the App-only credential rule, for the
@@ -26,7 +29,7 @@
26
29
  # scripts/vm-ghcr-login.sh --version X.Y.Z [--vm HOST] [--login USER] [--token-file FILE]
27
30
  # pbpaste | scripts/vm-ghcr-login.sh --version X.Y.Z # token on stdin
28
31
  #
29
- # --version a published release version; both images must exist at it
32
+ # --version a published version; both images must exist at it
30
33
  # --vm ssh target (default: $QC_VM_HOST, else the inventory's first host)
31
34
  # --login the GitHub login to store (default: the token's own; if given
32
35
  # it must be the token's)
@@ -32,13 +32,7 @@ import { readdir } from "node:fs/promises";
32
32
  import { join, relative } from "node:path";
33
33
  import type { StateStore } from "../state.js";
34
34
  import { yieldToLoop } from "@quickchatai/pi-quickreview/src/yieldloop.js";
35
- import {
36
- archiveStamp,
37
- bundlePassOf,
38
- freshPassId,
39
- identityFields,
40
- sameRecorded,
41
- } from "./archivewrite.js";
35
+ import { archiveStamp, bundlePassOf, freshPassId, identityFields } from "./archivewrite.js";
42
36
  import { readBundleRecords } from "./bundle.js";
43
37
  import { sameIdentity } from "./fsutil.js";
44
38
  import {
@@ -182,20 +176,23 @@ export async function restorePr(
182
176
  const steady = sameIdentity(live, after);
183
177
  if (steady && d.bytes === r.bytes && d.sha256 === r.sha256) {
184
178
  out.present += 1;
185
- // Its identity is recorded unless it is the one the archive record
186
- // already holds (a source that was never unlinked). A file an
187
- // earlier restore pass put back — one that died before its entry
188
- // was written — has a NEW inode: unrecorded, a re-archive after a
189
- // re-close would not know it and would compress the same bytes
190
- // again under `.v2`, instead of reusing the archived copy.
191
- if (!sameRecorded(r, live))
192
- restored.push({
193
- source: r.source,
194
- archive: r.archive,
195
- sha256: r.sha256,
196
- ...identityFields(live),
197
- present: true,
198
- });
179
+ // Its identity is recorded, always. A file an earlier restore pass
180
+ // put back — one that died before its entry was written — is
181
+ // unknown to the manifest otherwise, and a re-archive after a
182
+ // re-close would compress the same bytes again under `.v2` instead
183
+ // of reusing the archived copy. Its identity is no sign of whether
184
+ // it was ever unlinked: the filesystem hands a freed inode number
185
+ // back, and the restore gave the file its original size and mtime,
186
+ // so a file put back can carry exactly the identity the archive
187
+ // record holds. Recording a source that was never unlinked costs
188
+ // one `reused` entry at the re-archive, nothing more.
189
+ restored.push({
190
+ source: r.source,
191
+ archive: r.archive,
192
+ sha256: r.sha256,
193
+ ...identityFields(live),
194
+ present: true,
195
+ });
199
196
  } else out.liveDiffers.push(r.source);
200
197
  continue;
201
198
  }
package/src/botworker.ts CHANGED
@@ -439,7 +439,14 @@ export async function runBotWorker(args: WorkerArgs, deps: WorkerDeps): Promise<
439
439
  // The login: borrowed, blanked, and checked against this worker's own cap.
440
440
  let auth: EngineAuth;
441
441
  try {
442
- auth = engineCredentials({ srcDir: spec.agent_dir, needMs: spec.wall_clock_ms, now });
442
+ auth = engineCredentials({
443
+ srcDir: spec.login_dir ?? spec.agent_dir,
444
+ needMs: spec.wall_clock_ms,
445
+ now,
446
+ // a shared pi home holds the person's other logins too: only the
447
+ // review's providers are admitted
448
+ ...(spec.providers !== undefined ? { providers: spec.providers } : {}),
449
+ });
443
450
  } catch (e) {
444
451
  if (e instanceof AuthError) throw new Stop(EXIT.auth, e.message);
445
452
  throw e;
@@ -461,7 +468,11 @@ export async function runBotWorker(args: WorkerArgs, deps: WorkerDeps): Promise<
461
468
  ownsProcess: true,
462
469
  homeDir: homedir(),
463
470
  ...(ids !== null && !("refused" in ids) ? { toolUid: ids } : {}),
464
- fenceDirs: [...loginSourceDirs(env), ...credentialPaths(env, spec.env)],
471
+ fenceDirs: [
472
+ ...loginSourceDirs(env),
473
+ ...(spec.login_dir ? [spec.login_dir] : []),
474
+ ...credentialPaths(env, spec.env),
475
+ ],
465
476
  credentials: auth.credentials,
466
477
  settings: agentDirSettings(spec.agent_dir),
467
478
  });
package/src/channelfs.ts CHANGED
@@ -1,26 +1,3 @@
1
- // The file I/O of the scan's TUI↔companion records and channel files (the
2
- // run record, the findings store, the stop and stage-2 start requests and
3
- // their answers). A dependency-free leaf, so the two setup leaves the TUI
4
- // imports (scanreport.ts, stage2request.ts) share it without reaching each
5
- // other at runtime.
6
- import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
7
- import { dirname } from "node:path";
8
-
9
- /** A file's text, or null when it is ABSENT — any other failure throws, so a
10
- * reader can tell "not written yet" from "cannot be read". */
11
- export function readTextIfPresent(path: string): string | null {
12
- try {
13
- return readFileSync(path, "utf8");
14
- } catch (err) {
15
- if ((err as NodeJS.ErrnoException).code === "ENOENT") return null;
16
- throw err;
17
- }
18
- }
19
-
20
- /** tmp+rename: a reader never sees a half-written file. */
21
- export function writeAtomic(path: string, text: string): void {
22
- mkdirSync(dirname(path), { recursive: true });
23
- const tmp = `${path}.${process.pid}.tmp`;
24
- writeFileSync(tmp, text);
25
- renameSync(tmp, path);
26
- }
1
+ // Moved to the plugin (packages/pi-plugin/src/scan/channelfs.ts): this name
2
+ // stays for the app's importers.
3
+ export * from "@quickchatai/pi-quickreview/src/scan/channelfs.js";
package/src/cli.ts CHANGED
@@ -531,6 +531,8 @@ function drainHolderLine(snapshot: StatusSnapshot): string {
531
531
  /** What lifts the drain that stands. A snapshot with no hold marker comes from
532
532
  * a daemon that does not report holds: whether its boot lifts it is unknown. */
533
533
  function drainLiftLine(snapshot: StatusSnapshot): string {
534
+ if (snapshot.draining_reason && snapshot.draining_until && snapshot.draining_hold !== true)
535
+ return `lifts itself at ${snapshot.draining_until} and retries; qcr drain off to lift now`;
534
536
  if (snapshot.draining_hold === true || snapshot.draining_reason)
535
537
  return "only qcr drain off lifts it — no boot does";
536
538
  if (snapshot.draining_hold === undefined)
@@ -1857,55 +1859,6 @@ export async function cmdSetup(rest: string[]): Promise<number> {
1857
1859
  return 0;
1858
1860
  }
1859
1861
 
1860
- /** `qcr onboard` — the guided setup wizard: pi at the pinned
1861
- * version via npx (works from a bare clone; pi stays a non-dependency), the
1862
- * onboarding prompt + extension + a restricted tool set, then the TUI handoff
1863
- * when the checklist verifies complete. Resumable: run it again after a crash
1864
- * and completed steps are re-verified, not repeated. */
1865
- export async function cmdOnboard(): Promise<number> {
1866
- const { piPinFromPackageJson, onboardingSessionDir, runOnboard } =
1867
- await import("./onboard/launch.js");
1868
- const { checklistComplete } = await import("./onboard/engine.js");
1869
- const { spawn } = await import("node:child_process");
1870
- const root = sandboxRoot();
1871
- // Pin source: config.toml [pi] when it loads; package.json's "qcrPi" block on a
1872
- // bare clone (config.toml is written later, BY onboarding). Same pinned
1873
- // version by the rule-7 mirror discipline.
1874
- let pin: { package: string; version: string } | null = null;
1875
- try {
1876
- const { package: pkg, version } = loadConfig().pi;
1877
- pin = { package: pkg, version };
1878
- } catch {
1879
- pin = piPinFromPackageJson(root);
1880
- }
1881
- if (!pin)
1882
- throw new Error(
1883
- 'onboard: pi pin not found (need config.toml [pi] or the package.json "qcrPi" block)',
1884
- );
1885
- mkdirSync(onboardingSessionDir(root), { recursive: true });
1886
- const run = (cmd: string, args: string[]): Promise<number> =>
1887
- new Promise((resolvePromise) => {
1888
- const child = spawn(cmd, args, {
1889
- stdio: "inherit",
1890
- cwd: root,
1891
- env: { ...process.env, QCR_ROOT: root },
1892
- });
1893
- child.on("error", (err) => {
1894
- console.error(`onboard: ${cmd} failed to start: ${err.message}`);
1895
- resolvePromise(1);
1896
- });
1897
- child.on("close", (code) => resolvePromise(code ?? 1));
1898
- });
1899
- return runOnboard({
1900
- pin,
1901
- root,
1902
- spawn: run,
1903
- isComplete: () => checklistComplete(join(root, "state")),
1904
- log: (line) => console.log(line),
1905
- cliPath: join(root, "dist", "src", "cli.js"),
1906
- });
1907
- }
1908
-
1909
1862
  export const ARCHIVE_USAGE =
1910
1863
  "usage: qcr archive status [--plan] [--json] | qcr archive run | qcr archive restore <pr> | " +
1911
1864
  "qcr archive extract <pr> --to <dir>";
@@ -2154,6 +2107,16 @@ export async function main(rawArgv: string[]): Promise<void> {
2154
2107
  const { runTui } = await import("./tui/index.js");
2155
2108
  return runTui(rest);
2156
2109
  }
2110
+ case "home": {
2111
+ // the home (src/home/launch.ts): the bin routes a bare `qcr` here too
2112
+ // (src/qcrbin.ts); the image's home mode reaches it through this verb
2113
+ if (rest.length > 0) throw new Error("usage: qcr home");
2114
+ const { homeRunDeps, runHome } = await import("./home/launch.js");
2115
+ // the token taken at startup, when the wrapper handed one over
2116
+ const githubToken = takeHandoffToken();
2117
+ process.exitCode = runHome({ ...homeRunDeps(sandboxRoot()), githubToken: () => githubToken });
2118
+ return;
2119
+ }
2157
2120
  case "companion":
2158
2121
  if (rest.length !== 1) throw new Error("usage: qcr companion <up|down|status|rebuild|plan>");
2159
2122
  return cmdCompanion(rest[0]);
@@ -2161,10 +2124,6 @@ export async function main(rawArgv: string[]): Promise<void> {
2161
2124
  process.exitCode = await cmdMemory(rest);
2162
2125
  return;
2163
2126
  }
2164
- case "onboard": {
2165
- process.exitCode = await cmdOnboard();
2166
- return;
2167
- }
2168
2127
  case "setup": {
2169
2128
  process.exitCode = await cmdSetup(rest);
2170
2129
  return;
@@ -2273,7 +2232,7 @@ export async function main(rawArgv: string[]): Promise<void> {
2273
2232
  }
2274
2233
  default:
2275
2234
  console.log(
2276
- "usage: qcr <version|config check [--config-home <dir>]|onboard|setup [validate [<profile>]|commit]|status|spend|today|why <n>|metrics [--pr <n>]|events|takeover <n>|debug-round <n>|release <n>|tui [pr|overseer]|drain <on [--hold]|off>|pause-pr <pr> [--reason <text>]|resume-pr <pr>|review-now <pr>|github-token sync [--gh <path>] [--secrets-dir <dir>]|message <pr> [--may-post] <text...>|archive <status [--plan]|run|restore <pr>|extract <pr> --to <dir>>|install <where|migrate>|" +
2235
+ "usage: qcr <version|config check [--config-home <dir>]|home|setup [validate [<profile>]|commit]|status|spend|today|why <n>|metrics [--pr <n>]|events|takeover <n>|debug-round <n>|release <n>|tui [pr|overseer]|drain <on [--hold]|off>|pause-pr <pr> [--reason <text>]|resume-pr <pr>|review-now <pr>|github-token sync [--gh <path>] [--secrets-dir <dir>]|message <pr> [--may-post] <text...>|archive <status [--plan]|run|restore <pr>|extract <pr> --to <dir>>|install <where|migrate>|" +
2277
2236
  "companion <up|down|status|rebuild>|review [start|status|stop|branch|--uncommitted|--diff FILE|-|<PR number|URL>] [--repo OWNER/NAME] [--base REF] [--preview] [--key K] [--limit USD] [--json]|live [RUN_ID|DIR] [--lens ID] [--round N] [--follow] [--json]|open [RUN_ID|DIR|FILE.jsonl|scan[:RUN]|pr N [--round R]] [--lens ID] [--write] [--print]|scan <preview|start|status|stop|findings|matches|match|suggestions|stage2 propose|stage2 start|dismiss|units show|units propose|units adjust|units save|login|doctor|rebuild>|memory reembed [--user <name>]|memory relayout [--plan|--apply]|backfill [--dry-run] [--limit <n>]|history <digest|pull>|outcomes [--days <n>] [--capture [--read-github]]|lessons [mine|shadow-report [--days <n>]|off|on|suppress <id>|unsuppress <id>]|checklist|compare <pr...>|" +
2278
2237
  "regression [--set <path>] [--clone <path>] [--change <name>] [--limit <n>] [--out <path>]>",
2279
2238
  );
package/src/config.ts CHANGED
@@ -1158,6 +1158,43 @@ export interface Config extends CoreConfig {
1158
1158
  /** The bot login's single refresher (`src/loginrefresh/`). Optional
1159
1159
  * in the file; off unless the cutover's config turns it on. */
1160
1160
  login_refresh: LoginRefreshConfig;
1161
+ /** The home, the app's own pi a bare `qcr` opens (src/home/launch.ts).
1162
+ * Optional in the file and here, so hand-rolled configs keep compiling;
1163
+ * the parser fills every key. */
1164
+ home?: HomeConfig;
1165
+ }
1166
+
1167
+ /** Which tools the home's model may call: `reviewer` is the plugin's tools,
1168
+ * the home's own `home_*` tools and pi's read tools; `full` is all of pi's. */
1169
+ export type HomeToolSet = "reviewer" | "full";
1170
+
1171
+ /** `[home]`: the home a bare `qcr` opens (src/home/launch.ts reads it). */
1172
+ export interface HomeConfig {
1173
+ /** The chat's model, `provider/id`. The panel's login line counts only
1174
+ * this model's provider as signed in. */
1175
+ model: string;
1176
+ tools: HomeToolSet;
1177
+ }
1178
+
1179
+ export const HOME_DEFAULTS: Readonly<HomeConfig> = {
1180
+ model: "openai-codex/gpt-6.1-sol",
1181
+ tools: "reviewer",
1182
+ };
1183
+
1184
+ /** `[home]`, every key defaulted; a value of the wrong kind is refused. */
1185
+ export function parseHomeSection(raw: unknown): HomeConfig {
1186
+ const t = (raw ?? {}) as Record<string, unknown>;
1187
+ const model = t["model"] ?? HOME_DEFAULTS.model;
1188
+ if (typeof model !== "string" || !/^[^/\s]+\/\S+$/.test(model))
1189
+ throw new Error(
1190
+ `config.toml [home]: model must be "provider/id" (got ${JSON.stringify(model)})`,
1191
+ );
1192
+ const tools = t["tools"] ?? HOME_DEFAULTS.tools;
1193
+ if (tools !== "reviewer" && tools !== "full")
1194
+ throw new Error(
1195
+ `config.toml [home]: tools must be "reviewer" or "full" (got ${JSON.stringify(tools)})`,
1196
+ );
1197
+ return { model, tools };
1161
1198
  }
1162
1199
 
1163
1200
  /**
@@ -1822,6 +1859,7 @@ export function parseConfig(toml: string): Config {
1822
1859
  (raw.login_refresh ?? {}) as Record<string, unknown>,
1823
1860
  Number((req(raw, "daemon") as { round_deadline_min?: number }).round_deadline_min ?? 75),
1824
1861
  ),
1862
+ home: parseHomeSection(raw.home),
1825
1863
  };
1826
1864
  validateReviewSection(cfg);
1827
1865
  foldLegacyOwnerSlackId(raw, cfg.owner);
package/src/daemon.ts CHANGED
@@ -832,6 +832,8 @@ export function createDaemon(deps: DaemonDeps = {}): Daemon {
832
832
 
833
833
  /** The open PRs the last listing reconcile pass saw (status.json `open_prs`). */
834
834
  let openPrs: OpenPrsSnapshot | undefined;
835
+ /** The last reconcile pass (status.json `poll`): null until one has ended. */
836
+ let lastPoll: { at: string; ok: boolean; error?: string } | null = null;
835
837
  let firstReconcileSeen = false;
836
838
  let firstHealthyReconcileAt: Date | null = null;
837
839
  // Deploy-window drain auto-lift: only the boot's first reconcile pass may
@@ -888,6 +890,7 @@ export function createDaemon(deps: DaemonDeps = {}): Daemon {
888
890
  const bootFirstPass = !firstReconcileSeen;
889
891
  firstReconcileSeen = true;
890
892
  let completed = false;
893
+ let thrown: unknown = undefined;
891
894
  await guard("reconcile", async () => {
892
895
  const report = await reconcileOnce({
893
896
  cfg,
@@ -914,7 +917,15 @@ export function createDaemon(deps: DaemonDeps = {}): Daemon {
914
917
  // sweep reads session transcripts and is not needed for that, so it
915
918
  // starts on the next pass to keep the boot pass short.
916
919
  receiptSweep: !bootFirstPass,
920
+ }).catch((err: unknown) => {
921
+ thrown = err;
922
+ throw err;
917
923
  });
924
+ lastPoll = {
925
+ at: report.ran_at,
926
+ ok: report.errors.length === 0,
927
+ ...(report.errors[0] !== undefined ? { error: report.errors[0] } : {}),
928
+ };
918
929
  // The board's "review a PR" list: kept from the last pass that listed.
919
930
  if (report.open_list) openPrs = { listed_at: report.ran_at, prs: report.open_list };
920
931
  log(
@@ -943,8 +954,10 @@ export function createDaemon(deps: DaemonDeps = {}): Daemon {
943
954
  } else if (bootFirstPass && standing.draining && standing.reason !== null) {
944
955
  // A drain a system set for a reason (the worker crash-loop breaker,
945
956
  // `worker_failures`) is not a deploy window: a restart does not clear what
946
- // made it. Only an explicit `drain off` does.
947
- log(`drain stands — held for ${standing.reason}; only an explicit drain off lifts it`);
957
+ // made it. It lapses at its own expiry, or an explicit `drain off` lifts it.
958
+ log(
959
+ `drain stands — held for ${standing.reason}; ${standing.expires_at ? `it lifts itself at ${standing.expires_at}, or` : "only"} an explicit drain off lifts it`,
960
+ );
948
961
  } else if (bootFirstPass && report.errors.length === 0 && standing.draining) {
949
962
  // The operator's lift: this is the deploy flow's drain completing, so
950
963
  // it clears whatever hold stands — a session's included.
@@ -957,14 +970,25 @@ export function createDaemon(deps: DaemonDeps = {}): Daemon {
957
970
  }
958
971
  completed = true;
959
972
  });
973
+ if (thrown !== undefined)
974
+ lastPoll = {
975
+ at: (deps.now ? deps.now() : new Date()).toISOString(),
976
+ ok: false,
977
+ error: `the reconcile pass failed: ${thrown instanceof Error ? thrown.message : JSON.stringify(thrown)}`,
978
+ };
979
+ // The poll's outcome reaches status.json now, not at the next heartbeat:
980
+ // "working" is judged on it.
981
+ writeStatus();
960
982
  return completed;
961
983
  };
962
984
 
963
985
  const pump = async (): Promise<void> => {
964
986
  await guard("dispatch", async () => {
965
987
  // Session levers that lapsed since the last tick leave the ledger here;
966
- // the readers below already treat them as gone.
967
- sweepExpiredLevers(store, (line) => log(line));
988
+ // the readers below already treat them as gone. A drain that lapsed (a
989
+ // session's, or the worker breaker's healing itself) dropped thread
990
+ // replies as any drain does: sweep them as a lift would.
991
+ if (sweepExpiredLevers(store, (line) => log(line)).drain) threadDecisions.requestSweep();
968
992
  const orders = await dispatchOnce({
969
993
  cfg,
970
994
  store,
@@ -1122,6 +1146,12 @@ export function createDaemon(deps: DaemonDeps = {}): Daemon {
1122
1146
  };
1123
1147
  }
1124
1148
  if (openPrs) snapshot.open_prs = openPrs;
1149
+ snapshot.poll = {
1150
+ last_at: lastPoll?.at ?? null,
1151
+ ok: lastPoll?.ok ?? null,
1152
+ ...(lastPoll?.error !== undefined ? { error: lastPoll.error } : {}),
1153
+ interval_s: cfg.daemon.poll_interval_s,
1154
+ };
1125
1155
  if (deps.runHost) snapshot.run_host = deps.runHost;
1126
1156
  writeStatusJson(stateDir, snapshot);
1127
1157
  } catch (err) {
package/src/debuground.ts CHANGED
@@ -123,6 +123,8 @@ export interface DebugRoundSpec {
123
123
  cwd: string;
124
124
  /** The login's owner dir: read-only, borrowed from. */
125
125
  agent_dir: string;
126
+ /** The login's dir when it is not `agent_dir` (the worker spec's). */
127
+ login_dir?: string;
126
128
  /** Where the pi's own agent dir (the blanked login copy) is made. */
127
129
  borrow_dir: string;
128
130
  /** The window the borrowed login must outlast. */
@@ -603,6 +605,7 @@ export class DebugRoundManager {
603
605
  root: spec.root,
604
606
  cwd: spec.cwd,
605
607
  agent_dir: spec.agent_dir,
608
+ ...(spec.login_dir !== undefined ? { login_dir: spec.login_dir } : {}),
606
609
  borrow_dir: borrow,
607
610
  idle_ms: this.idleMs,
608
611
  ...(file !== undefined ? { session_file: file } : {}),
@@ -83,6 +83,7 @@ export function readDebugSpec(file: string): DebugRoundSpec {
83
83
  raw.session !== debugSession(raw.pr) ||
84
84
  typeof raw.command_id !== "string" ||
85
85
  typeof raw.agent_dir !== "string" ||
86
+ (raw.login_dir !== undefined && typeof raw.login_dir !== "string") ||
86
87
  typeof raw.borrow_dir !== "string" ||
87
88
  typeof raw.session_dir !== "string" ||
88
89
  typeof raw.model !== "string" ||