@quickchatai/quickreview 1.2.0 → 1.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (350) hide show
  1. package/README.md +116 -53
  2. package/bin/qcr.js +1 -1
  3. package/config.example.toml +25 -7
  4. package/deploy/ansible/README.md +20 -12
  5. package/deploy/ansible/inventory.example.ini +1 -1
  6. package/deploy/collect-secrets.sh +1 -1
  7. package/deploy/qcr-update.sh +1 -1
  8. package/dist/extensions/cli/home.js +14 -0
  9. package/dist/extensions/cli/home.js.map +1 -0
  10. package/dist/extensions/cli/selfimprove.js +18 -0
  11. package/dist/extensions/cli/selfimprove.js.map +1 -0
  12. package/dist/extensions/home.js +350 -0
  13. package/dist/extensions/home.js.map +1 -0
  14. package/dist/extensions/login-route.js +1 -1
  15. package/dist/extensions/login-route.js.map +1 -1
  16. package/dist/extensions/onboarding.js +8 -7
  17. package/dist/extensions/onboarding.js.map +1 -1
  18. package/dist/extensions/selfimprove.js +49 -0
  19. package/dist/extensions/selfimprove.js.map +1 -0
  20. package/dist/extensions/setup-scan.js +23 -592
  21. package/dist/extensions/setup-scan.js.map +1 -1
  22. package/dist/extensions/setup.js +2 -47
  23. package/dist/extensions/setup.js.map +1 -1
  24. package/dist/extensions/vendor/openviking-pi/index.js +4 -0
  25. package/dist/extensions/vendor/openviking-pi/index.js.map +1 -1
  26. package/dist/extensions/vendor/openviking-pi/qcr-hooks.js +3 -1
  27. package/dist/extensions/vendor/openviking-pi/qcr-hooks.js.map +1 -1
  28. package/dist/extensions/vendor/openviking-pi/sync.js +81 -0
  29. package/dist/extensions/vendor/openviking-pi/sync.js.map +1 -1
  30. package/dist/src/archive/restore.js +18 -15
  31. package/dist/src/archive/restore.js.map +1 -1
  32. package/dist/src/botworker.js +13 -2
  33. package/dist/src/botworker.js.map +1 -1
  34. package/dist/src/channelfs.js +3 -26
  35. package/dist/src/channelfs.js.map +1 -1
  36. package/dist/src/cli.js +14 -50
  37. package/dist/src/cli.js.map +1 -1
  38. package/dist/src/config.js +16 -0
  39. package/dist/src/config.js.map +1 -1
  40. package/dist/src/daemon.js +33 -4
  41. package/dist/src/daemon.js.map +1 -1
  42. package/dist/src/debuground.js +1 -0
  43. package/dist/src/debuground.js.map +1 -1
  44. package/dist/src/debugroundpi.js +1 -0
  45. package/dist/src/debugroundpi.js.map +1 -1
  46. package/dist/src/engine/caserunner-sdk.js +15 -251
  47. package/dist/src/engine/caserunner-sdk.js.map +1 -1
  48. package/dist/src/home/adapters.js +349 -0
  49. package/dist/src/home/adapters.js.map +1 -0
  50. package/dist/src/home/advanced.js +318 -0
  51. package/dist/src/home/advanced.js.map +1 -0
  52. package/dist/src/home/advancedtools.js +57 -0
  53. package/dist/src/home/advancedtools.js.map +1 -0
  54. package/dist/src/home/autoreview.js +325 -0
  55. package/dist/src/home/autoreview.js.map +1 -0
  56. package/dist/src/home/daemonstatus.js +32 -0
  57. package/dist/src/home/daemonstatus.js.map +1 -0
  58. package/dist/src/home/devicelogin.js +61 -0
  59. package/dist/src/home/devicelogin.js.map +1 -0
  60. package/dist/src/home/launch.js +202 -0
  61. package/dist/src/home/launch.js.map +1 -0
  62. package/dist/src/home/loginowner.js +205 -0
  63. package/dist/src/home/loginowner.js.map +1 -0
  64. package/dist/src/home/memory.js +122 -0
  65. package/dist/src/home/memory.js.map +1 -0
  66. package/dist/src/home/model.js +811 -0
  67. package/dist/src/home/model.js.map +1 -0
  68. package/dist/src/home/panel.js +520 -0
  69. package/dist/src/home/panel.js.map +1 -0
  70. package/dist/src/home/ports.js +19 -0
  71. package/dist/src/home/ports.js.map +1 -0
  72. package/dist/src/home/review.js +163 -0
  73. package/dist/src/home/review.js.map +1 -0
  74. package/dist/src/home/reviews.js +478 -0
  75. package/dist/src/home/reviews.js.map +1 -0
  76. package/dist/src/home/setup.js +507 -0
  77. package/dist/src/home/setup.js.map +1 -0
  78. package/dist/src/home/setupadapters.js +214 -0
  79. package/dist/src/home/setupadapters.js.map +1 -0
  80. package/dist/src/home/setupcard.js +199 -0
  81. package/dist/src/home/setupcard.js.map +1 -0
  82. package/dist/src/home/setuptools.js +45 -0
  83. package/dist/src/home/setuptools.js.map +1 -0
  84. package/dist/src/home/spend.js +160 -0
  85. package/dist/src/home/spend.js.map +1 -0
  86. package/dist/src/home/switches.js +209 -0
  87. package/dist/src/home/switches.js.map +1 -0
  88. package/dist/src/home/switchon.js +439 -0
  89. package/dist/src/home/switchon.js.map +1 -0
  90. package/dist/src/home/toolnames.js +41 -0
  91. package/dist/src/home/toolnames.js.map +1 -0
  92. package/dist/src/home/tune.js +341 -0
  93. package/dist/src/home/tune.js.map +1 -0
  94. package/dist/src/home/words.js +7 -0
  95. package/dist/src/home/words.js.map +1 -0
  96. package/dist/src/homes.js +14 -0
  97. package/dist/src/homes.js.map +1 -1
  98. package/dist/src/install.js +1 -1
  99. package/dist/src/install.js.map +1 -1
  100. package/dist/src/ipc/workerspec.js +8 -0
  101. package/dist/src/ipc/workerspec.js.map +1 -1
  102. package/dist/src/ledgercheck.js +39 -10
  103. package/dist/src/ledgercheck.js.map +1 -1
  104. package/dist/src/live/publish.js +1 -1
  105. package/dist/src/live/publish.js.map +1 -1
  106. package/dist/src/livecli.js +1 -1
  107. package/dist/src/livecli.js.map +1 -1
  108. package/dist/src/loginrefresh/handover.js +121 -0
  109. package/dist/src/loginrefresh/handover.js.map +1 -0
  110. package/dist/src/loginrefresh/lock.js +54 -3
  111. package/dist/src/loginrefresh/lock.js.map +1 -1
  112. package/dist/src/loginrefresh/refresher.js +33 -5
  113. package/dist/src/loginrefresh/refresher.js.map +1 -1
  114. package/dist/src/loginrefresh/supervisor.js +18 -1
  115. package/dist/src/loginrefresh/supervisor.js.map +1 -1
  116. package/dist/src/npmverbs.js +42 -12
  117. package/dist/src/npmverbs.js.map +1 -1
  118. package/dist/src/onboard/choices.js +6 -3
  119. package/dist/src/onboard/choices.js.map +1 -1
  120. package/dist/src/onboard/engine.js +33 -292
  121. package/dist/src/onboard/engine.js.map +1 -1
  122. package/dist/src/onboard/steps/bringup.js +3 -2
  123. package/dist/src/onboard/steps/bringup.js.map +1 -1
  124. package/dist/src/onboard/steps/configfill.js +18 -7
  125. package/dist/src/onboard/steps/configfill.js.map +1 -1
  126. package/dist/src/onboard/steps/loginshare.js +213 -0
  127. package/dist/src/onboard/steps/loginshare.js.map +1 -0
  128. package/dist/src/onboard/steps/memory.js +14 -2
  129. package/dist/src/onboard/steps/memory.js.map +1 -1
  130. package/dist/src/onboard/steps/repoconfig.js +72 -0
  131. package/dist/src/onboard/steps/repoconfig.js.map +1 -0
  132. package/dist/src/onboard/toolnames.js +2 -2
  133. package/dist/src/onboard/toolnames.js.map +1 -1
  134. package/dist/src/piagentdir.js +13 -0
  135. package/dist/src/piagentdir.js.map +1 -0
  136. package/dist/src/pilogin.js +31 -16
  137. package/dist/src/pilogin.js.map +1 -1
  138. package/dist/src/qcrbin.js +16 -11
  139. package/dist/src/qcrbin.js.map +1 -1
  140. package/dist/src/reviewargs.js +122 -0
  141. package/dist/src/reviewargs.js.map +1 -0
  142. package/dist/src/reviewcli.js +7 -161
  143. package/dist/src/reviewcli.js.map +1 -1
  144. package/dist/src/reviewdetach.js +76 -11
  145. package/dist/src/reviewdetach.js.map +1 -1
  146. package/dist/src/scancli.js +39 -691
  147. package/dist/src/scancli.js.map +1 -1
  148. package/dist/src/selfimprove.js +66 -0
  149. package/dist/src/selfimprove.js.map +1 -0
  150. package/dist/src/session-command-supervisor.js +38 -9
  151. package/dist/src/session-command-supervisor.js.map +1 -1
  152. package/dist/src/setup/adapters.js +5 -223
  153. package/dist/src/setup/adapters.js.map +1 -1
  154. package/dist/src/setup/caserunner.js +15 -249
  155. package/dist/src/setup/caserunner.js.map +1 -1
  156. package/dist/src/setup/engine.js +32 -341
  157. package/dist/src/setup/engine.js.map +1 -1
  158. package/dist/src/setup/facts.js +3 -592
  159. package/dist/src/setup/facts.js.map +1 -1
  160. package/dist/src/setup/findings.js +3 -947
  161. package/dist/src/setup/findings.js.map +1 -1
  162. package/dist/src/setup/presentation.js +3 -8
  163. package/dist/src/setup/presentation.js.map +1 -1
  164. package/dist/src/setup/scan.js +3 -724
  165. package/dist/src/setup/scan.js.map +1 -1
  166. package/dist/src/setup/scanbuild.js +9 -92
  167. package/dist/src/setup/scanbuild.js.map +1 -1
  168. package/dist/src/setup/scanlease.js +3 -250
  169. package/dist/src/setup/scanlease.js.map +1 -1
  170. package/dist/src/setup/scanreport.js +3 -288
  171. package/dist/src/setup/scanreport.js.map +1 -1
  172. package/dist/src/setup/scanrun.js +3 -1732
  173. package/dist/src/setup/scanrun.js.map +1 -1
  174. package/dist/src/setup/scantree.js +3 -256
  175. package/dist/src/setup/scantree.js.map +1 -1
  176. package/dist/src/setup/stage2request.js +3 -258
  177. package/dist/src/setup/stage2request.js.map +1 -1
  178. package/dist/src/setup/unitcut.js +3 -436
  179. package/dist/src/setup/unitcut.js.map +1 -1
  180. package/dist/src/state.js +75 -9
  181. package/dist/src/state.js.map +1 -1
  182. package/dist/src/takeover.js +1 -0
  183. package/dist/src/takeover.js.map +1 -1
  184. package/dist/src/takeoverpi.js +2 -1
  185. package/dist/src/takeoverpi.js.map +1 -1
  186. package/dist/src/tui/alerts.js +4 -1
  187. package/dist/src/tui/alerts.js.map +1 -1
  188. package/dist/src/tui/board.js +2 -2
  189. package/dist/src/tui/board.js.map +1 -1
  190. package/dist/src/tui/companion.js +2 -1
  191. package/dist/src/tui/companion.js.map +1 -1
  192. package/dist/src/tui/deploytab.js +2 -2
  193. package/dist/src/tui/deploytab.js.map +1 -1
  194. package/dist/src/tui/index.js +9 -6
  195. package/dist/src/tui/index.js.map +1 -1
  196. package/dist/src/tui/memory.js +3 -16
  197. package/dist/src/tui/memory.js.map +1 -1
  198. package/dist/src/tui/memoryurl.js +22 -0
  199. package/dist/src/tui/memoryurl.js.map +1 -0
  200. package/dist/src/tui/remote.js +1 -1
  201. package/dist/src/tui/remote.js.map +1 -1
  202. package/dist/src/tui/route.js +2 -2
  203. package/dist/src/tui/route.js.map +1 -1
  204. package/dist/src/tui/setupstate.js +8 -5
  205. package/dist/src/tui/setupstate.js.map +1 -1
  206. package/dist/src/tui/sshtriage.js +1 -1
  207. package/dist/src/tui/sshtriage.js.map +1 -1
  208. package/dist/src/verdicts.js +12 -0
  209. package/dist/src/verdicts.js.map +1 -1
  210. package/dist/src/wire.js +9 -4
  211. package/dist/src/wire.js.map +1 -1
  212. package/dist/src/workerbreaker.js +32 -5
  213. package/dist/src/workerbreaker.js.map +1 -1
  214. package/dist/src/workerworld.js +4 -0
  215. package/dist/src/workerworld.js.map +1 -1
  216. package/docker/README.md +1 -1
  217. package/docker/entrypoint.sh +40 -6
  218. package/docker-compose.yml +16 -1
  219. package/extensions/cli/home.ts +14 -0
  220. package/extensions/cli/selfimprove.ts +18 -0
  221. package/extensions/home.ts +484 -0
  222. package/extensions/login-route.ts +1 -1
  223. package/extensions/onboarding.ts +8 -7
  224. package/extensions/selfimprove.ts +65 -0
  225. package/extensions/setup-scan.ts +79 -766
  226. package/extensions/setup.ts +14 -70
  227. package/extensions/vendor/openviking-pi/NOTICE +5 -0
  228. package/extensions/vendor/openviking-pi/README.md +39 -5
  229. package/extensions/vendor/openviking-pi/index.ts +4 -0
  230. package/extensions/vendor/openviking-pi/patches/0014-one-turn-sync-in-flight.patch +239 -0
  231. package/extensions/vendor/openviking-pi/qcr-hooks.ts +3 -1
  232. package/extensions/vendor/openviking-pi/sync.ts +80 -0
  233. package/extensions/vendor/openviking-pi/upstream.json +2 -1
  234. package/package.json +6 -4
  235. package/prompts/onboarding.md +8 -7
  236. package/schemas/review-result.v1.json +60 -0
  237. package/scripts/deploy.sh +8 -6
  238. package/scripts/qcr-host.sh +28 -5
  239. package/scripts/vm-ghcr-login.sh +7 -4
  240. package/src/archive/restore.ts +18 -21
  241. package/src/botworker.ts +13 -2
  242. package/src/channelfs.ts +3 -26
  243. package/src/cli.ts +13 -54
  244. package/src/config.ts +38 -0
  245. package/src/daemon.ts +34 -4
  246. package/src/debuground.ts +3 -0
  247. package/src/debugroundpi.ts +1 -0
  248. package/src/engine/caserunner-sdk.ts +29 -314
  249. package/src/home/adapters.ts +418 -0
  250. package/src/home/advanced.ts +421 -0
  251. package/src/home/advancedtools.ts +96 -0
  252. package/src/home/autoreview.ts +385 -0
  253. package/src/home/daemonstatus.ts +55 -0
  254. package/src/home/devicelogin.ts +107 -0
  255. package/src/home/launch.ts +257 -0
  256. package/src/home/loginowner.ts +248 -0
  257. package/src/home/memory.ts +176 -0
  258. package/src/home/model.ts +965 -0
  259. package/src/home/panel.ts +666 -0
  260. package/src/home/ports.ts +338 -0
  261. package/src/home/review.ts +206 -0
  262. package/src/home/reviews.ts +565 -0
  263. package/src/home/setup.ts +689 -0
  264. package/src/home/setupadapters.ts +217 -0
  265. package/src/home/setupcard.ts +288 -0
  266. package/src/home/setuptools.ts +57 -0
  267. package/src/home/spend.ts +194 -0
  268. package/src/home/switches.ts +293 -0
  269. package/src/home/switchon.ts +554 -0
  270. package/src/home/toolnames.ts +46 -0
  271. package/src/home/tune.ts +405 -0
  272. package/src/home/words.ts +8 -0
  273. package/src/homes.ts +17 -0
  274. package/src/install.ts +1 -1
  275. package/src/ipc/workerspec.ts +15 -0
  276. package/src/ledgercheck.ts +45 -9
  277. package/src/live/publish.ts +4 -1
  278. package/src/livecli.ts +6 -1
  279. package/src/loginrefresh/handover.ts +138 -0
  280. package/src/loginrefresh/lock.ts +72 -3
  281. package/src/loginrefresh/refresher.ts +34 -5
  282. package/src/loginrefresh/supervisor.ts +25 -2
  283. package/src/npmverbs.ts +51 -11
  284. package/src/onboard/choices.ts +6 -3
  285. package/src/onboard/engine.ts +64 -434
  286. package/src/onboard/steps/bringup.ts +3 -2
  287. package/src/onboard/steps/configfill.ts +23 -7
  288. package/src/onboard/steps/loginshare.ts +258 -0
  289. package/src/onboard/steps/memory.ts +18 -2
  290. package/src/onboard/steps/repoconfig.ts +85 -0
  291. package/src/onboard/toolnames.ts +2 -2
  292. package/src/piagentdir.ts +12 -0
  293. package/src/pilogin.ts +32 -16
  294. package/src/qcrbin.ts +16 -11
  295. package/src/reviewargs.ts +119 -0
  296. package/src/reviewcli.ts +6 -167
  297. package/src/reviewdetach.ts +107 -13
  298. package/src/scancli.ts +75 -826
  299. package/src/selfimprove.ts +90 -0
  300. package/src/session-command-supervisor.ts +41 -8
  301. package/src/setup/adapters.ts +13 -221
  302. package/src/setup/caserunner.ts +34 -273
  303. package/src/setup/engine.ts +62 -496
  304. package/src/setup/facts.ts +3 -627
  305. package/src/setup/findings.ts +3 -1264
  306. package/src/setup/presentation.ts +3 -18
  307. package/src/setup/scan.ts +3 -1033
  308. package/src/setup/scanbuild.ts +20 -114
  309. package/src/setup/scanlease.ts +3 -297
  310. package/src/setup/scanreport.ts +3 -373
  311. package/src/setup/scanrun.ts +3 -2268
  312. package/src/setup/scantree.ts +3 -259
  313. package/src/setup/stage2request.ts +3 -347
  314. package/src/setup/unitcut.ts +3 -523
  315. package/src/state.ts +126 -11
  316. package/src/takeover.ts +5 -0
  317. package/src/takeoverpi.ts +3 -2
  318. package/src/tui/alerts.ts +9 -2
  319. package/src/tui/board.ts +2 -2
  320. package/src/tui/companion.ts +2 -1
  321. package/src/tui/deploytab.ts +2 -2
  322. package/src/tui/index.ts +9 -6
  323. package/src/tui/memory.ts +3 -15
  324. package/src/tui/memoryurl.ts +22 -0
  325. package/src/tui/remote.ts +1 -1
  326. package/src/tui/route.ts +2 -2
  327. package/src/tui/setupstate.ts +20 -16
  328. package/src/tui/sshtriage.ts +1 -1
  329. package/src/verdicts.ts +13 -0
  330. package/src/wire.ts +9 -4
  331. package/src/workerbreaker.ts +41 -5
  332. package/src/workerworld.ts +9 -1
  333. package/dist/src/live/projection.js +0 -450
  334. package/dist/src/live/projection.js.map +0 -1
  335. package/dist/src/live/read.js +0 -60
  336. package/dist/src/live/read.js.map +0 -1
  337. package/dist/src/onboard/launch.js +0 -75
  338. package/dist/src/onboard/launch.js.map +0 -1
  339. package/dist/src/setup/ledger.js +0 -207
  340. package/dist/src/setup/ledger.js.map +0 -1
  341. package/dist/src/setup/stage2.js +0 -617
  342. package/dist/src/setup/stage2.js.map +0 -1
  343. package/dist/src/setup/textsniff.js +0 -89
  344. package/dist/src/setup/textsniff.js.map +0 -1
  345. package/src/live/projection.ts +0 -491
  346. package/src/live/read.ts +0 -86
  347. package/src/onboard/launch.ts +0 -103
  348. package/src/setup/ledger.ts +0 -324
  349. package/src/setup/stage2.ts +0 -926
  350. package/src/setup/textsniff.ts +0 -95
@@ -0,0 +1,565 @@
1
+ // The home's reviews port (src/home/ports.ts `ReviewsPort`) over the app's
2
+ // own reviewer:
3
+ //
4
+ // preview `qcr review <PR> --repo R --preview --json`, the app's own bin
5
+ // run as a child with the person's environment (their pi login):
6
+ // the plan, the price and the key.
7
+ // start the detached registry's `start` (src/reviewdetach.ts), called
8
+ // in this process: `<PR> --repo R --key K --to file --out <a
9
+ // report in the state home>`. Never `--to pr`: a run posts
10
+ // nothing by itself. The run's driver is the app's bin again,
11
+ // detached, so it outlives the home.
12
+ // status the registry's `status`, read in this process.
13
+ // stop the registry's `stop`.
14
+ // runs every run the registry holds for the repo: what the home
15
+ // re-attaches to when it opens.
16
+ // latest the person's own reviews of the repo's PRs: the registry's
17
+ // finished runs and the app's per-target review state (the
18
+ // review home's delta history, packages/pi-plugin/src/reviewstate.ts).
19
+ // autoRounds the PRs the daemon finished a review round of, from its
20
+ // status snapshot (src/home/daemonstatus.ts): a PR whose rounds
21
+ // are all still running or failed has no review yet.
22
+ // post a finished review the run kept for later
23
+ // (packages/pi-plugin/src/savedpost.ts `postSavedReview`): one
24
+ // comment-only review through the gate with the install's dry_run.
25
+ // dryRun the install's dry_run.
26
+ //
27
+ // Everything effectful is a dependency; `realReviewsPort` is the assembly.
28
+ import { execFile } from "node:child_process";
29
+ import { randomBytes } from "node:crypto";
30
+ import { existsSync, mkdirSync, readFileSync, readdirSync } from "node:fs";
31
+ import { join } from "node:path";
32
+ import { openFindings, type ReviewState } from "@quickchatai/pi-quickreview/src/reviewstate.js";
33
+ import {
34
+ detachedCommand,
35
+ identifyTarget,
36
+ launchDetached,
37
+ listRunViews,
38
+ runDetachedVerb,
39
+ runView,
40
+ type DetachDeps,
41
+ type RunView,
42
+ watchdogCommand,
43
+ } from "../reviewdetach.js";
44
+ import type { DaemonSnapshot } from "./daemonstatus.js";
45
+ import {
46
+ known,
47
+ unknown,
48
+ type Known,
49
+ type LiveLens,
50
+ type Outcome,
51
+ type PrReview,
52
+ type ReviewFinding,
53
+ type ReviewQuote,
54
+ type ReviewRunState,
55
+ type ReviewRunStatus,
56
+ type ReviewsPort,
57
+ } from "./ports.js";
58
+
59
+ type Env = Readonly<Record<string, string | undefined>>;
60
+
61
+ export interface QcrResult {
62
+ code: number;
63
+ stdout: string;
64
+ stderr: string;
65
+ }
66
+
67
+ export interface ReviewsPortDeps {
68
+ /** The detached-run registry: `<state home>/review/detached`. */
69
+ registry: string;
70
+ /** Where the home's review reports go: `<state home>/home/reviews`. */
71
+ reportsDir: string;
72
+ /** The review home: `<state home>/review`, one store per repo. */
73
+ reviewHome: string;
74
+ /** The daemon's status snapshot (src/home/daemonstatus.ts). */
75
+ snapshot: () => Promise<DaemonSnapshot>;
76
+ /** Run the app's own CLI with these arguments, as the person. */
77
+ qcr: (args: readonly string[], o: { signal?: AbortSignal }) => Promise<QcrResult>;
78
+ /** The registry's process seams: what `start` launches and how a run's
79
+ * liveness is told. */
80
+ detach: Omit<DetachDeps, "home" | "out">;
81
+ /** Post a saved review through the gate; `failed` with its line when it did not. */
82
+ post: (file: string) => Promise<{ failed: boolean; line: string }>;
83
+ /** The install's dry_run; throws when its config cannot be read. */
84
+ dryRun: () => boolean;
85
+ now: () => number;
86
+ /** Read a file (the review state). */
87
+ read?: (path: string) => string;
88
+ list?: (dir: string) => string[];
89
+ }
90
+
91
+ const message = (err: unknown): string => (err instanceof Error ? err.message : String(err));
92
+ const isMissing = (err: unknown): boolean =>
93
+ (err as NodeJS.ErrnoException | null)?.code === "ENOENT";
94
+
95
+ type Json = Record<string, unknown>;
96
+ const obj = (v: unknown): Json | undefined =>
97
+ typeof v === "object" && v !== null && !Array.isArray(v) ? (v as Json) : undefined;
98
+ const str = (v: unknown): string | undefined => (typeof v === "string" ? v : undefined);
99
+ const num = (v: unknown): number | undefined =>
100
+ typeof v === "number" && Number.isFinite(v) ? v : undefined;
101
+
102
+ /** The last JSON object a command printed. */
103
+ export function lastJson(stdout: string): Json | undefined {
104
+ const lines = stdout.trim().split("\n");
105
+ for (let i = lines.length - 1; i >= 0; i--) {
106
+ const l = lines[i]!.trim();
107
+ if (!l.startsWith("{")) continue;
108
+ try {
109
+ return obj(JSON.parse(l));
110
+ } catch {
111
+ // a stray line
112
+ }
113
+ }
114
+ return undefined;
115
+ }
116
+
117
+ const errorsOf = (o: Json | undefined, fallback: string): string => {
118
+ const e = o?.["errors"];
119
+ return Array.isArray(e) && e.length > 0 ? e.map(String).join("; ") : fallback;
120
+ };
121
+
122
+ /** `qcr review --preview --json`'s answer as a quote. */
123
+ export function parsePreview(o: Json | undefined): Outcome<{ quote: ReviewQuote }> {
124
+ const p = obj(o?.["preview"]);
125
+ const est = num(p?.["estimate_usd"]);
126
+ const key = str(p?.["key"]);
127
+ if (o?.["ok"] !== true || p === undefined || est === undefined || key === undefined)
128
+ return { ok: false, why: errorsOf(o, "the reviewer gave no price") };
129
+ const cap = p["cap_usd"];
130
+ const lenses = p["lenses"];
131
+ return {
132
+ ok: true,
133
+ quote: {
134
+ estimateUsd: est,
135
+ capUsd: num(cap) ?? null,
136
+ key,
137
+ lenses: Array.isArray(lenses) ? lenses.map(String) : [],
138
+ files: num(p["files"]) ?? 0,
139
+ headSha: str(p["head_sha"]) ?? null,
140
+ },
141
+ };
142
+ }
143
+
144
+ /** The findings of a result, grouped by severity, as one list worst first. */
145
+ export function parseFindings(groups: unknown): ReviewFinding[] {
146
+ const g = obj(groups);
147
+ if (g === undefined) return [];
148
+ return Object.entries(g).flatMap(([sev, list]) =>
149
+ Array.isArray(list)
150
+ ? list.flatMap((f: unknown) => {
151
+ const x = obj(f);
152
+ if (x === undefined) return [];
153
+ return [
154
+ {
155
+ severity: str(x["severity"]) ?? sev,
156
+ path: str(x["path"]) ?? null,
157
+ line: num(x["line"]) ?? null,
158
+ title: str(x["title"]) ?? "(untitled finding)",
159
+ lens: str(x["lens"]) ?? null,
160
+ },
161
+ ];
162
+ })
163
+ : [],
164
+ );
165
+ }
166
+
167
+ /** The PR a run reviews, from its target label (`PR owner/name#12`). */
168
+ export function prOfTarget(target: string | undefined): { repo: string; pr: number } | null {
169
+ const m = /^PR ([\w.-]+\/[\w.-]+)#(\d+)$/.exec(target ?? "");
170
+ return m ? { repo: m[1]!, pr: Number(m[2]) } : null;
171
+ }
172
+
173
+ const STATES: readonly ReviewRunState[] = ["running", "done", "failed", "stopped"];
174
+
175
+ /** One run as `qcr review status --json` reports it, as the home reads it. */
176
+ export function parseRunStatus(v: Json, argv: readonly string[] = []): ReviewRunStatus | null {
177
+ const runId = str(v["run_id"]);
178
+ const state = str(v["state"]) as ReviewRunState | undefined;
179
+ if (runId === undefined || state === undefined || !STATES.includes(state)) return null;
180
+ const target = prOfTarget(str(v["target"]));
181
+ const out = argv.indexOf("--out");
182
+ const reportArg = out >= 0 ? argv[out + 1] : undefined;
183
+ const result = obj(v["result"]);
184
+ const reportResult = str(result?.["report"]);
185
+ const live = obj(obj(v["progress"])?.["live"]);
186
+ const agents = live?.["agents"];
187
+ const errors = [
188
+ ...(Array.isArray(result?.["errors"]) ? (result["errors"] as unknown[]).map(String) : []),
189
+ ...(Array.isArray(v["errors"]) ? (v["errors"] as unknown[]).map(String) : []),
190
+ ];
191
+ const ended = num(v["ended_at"]);
192
+ const report = reportResult ?? reportArg;
193
+ return {
194
+ runId,
195
+ state,
196
+ repo: target?.repo ?? null,
197
+ pr: target?.pr ?? null,
198
+ startedAt: num(v["started_at"]) ?? 0,
199
+ ...(ended !== undefined ? { endedAt: ended } : {}),
200
+ ...(report !== undefined ? { report } : {}),
201
+ ...(live !== undefined
202
+ ? {
203
+ live: {
204
+ lastActivity: str(live["last_activity"]) ?? "",
205
+ lenses: Array.isArray(agents)
206
+ ? agents.flatMap((a: unknown): LiveLens[] => {
207
+ const x = obj(a);
208
+ const agent = str(x?.["agent"]);
209
+ if (x === undefined || agent === undefined) return [];
210
+ const tool = str(x["current_tool"]);
211
+ const on = str(x["current_target"]);
212
+ return [
213
+ {
214
+ agent,
215
+ status: str(x["status"]) ?? "pending",
216
+ toolCount: num(x["tool_count"]) ?? 0,
217
+ durationMs: num(x["duration_ms"]) ?? 0,
218
+ ...(tool !== undefined ? { currentTool: tool } : {}),
219
+ ...(on !== undefined ? { currentTarget: on } : {}),
220
+ line: str(x["line"]) ?? agent,
221
+ },
222
+ ];
223
+ })
224
+ : [],
225
+ },
226
+ }
227
+ : {}),
228
+ ...(state !== "running" && result !== undefined
229
+ ? {
230
+ result: {
231
+ findings: parseFindings(result["findings"]),
232
+ costUsd: num(result["cost_usd"]) ?? null,
233
+ ...(str(result["post_ready"]) !== undefined
234
+ ? { postFile: str(result["post_ready"])! }
235
+ : {}),
236
+ ...(obj(result["post"])?.["ok"] === true ? { posted: true } : {}),
237
+ },
238
+ }
239
+ : {}),
240
+ errors,
241
+ };
242
+ }
243
+
244
+ const high = (list: readonly ReviewFinding[]): number =>
245
+ list.filter((f) => /^P[01]$/i.test(f.severity)).length;
246
+
247
+ /** A finished run as the person's review of its PR. */
248
+ function runReview(s: ReviewRunStatus): PrReview | null {
249
+ if (s.state !== "done" || s.result === undefined) return null;
250
+ const list = s.result.findings;
251
+ return {
252
+ findings: list.length,
253
+ high: high(list),
254
+ by: "you",
255
+ at: s.endedAt ?? s.startedAt,
256
+ list,
257
+ ...(s.report !== undefined ? { report: s.report } : {}),
258
+ ...(s.endedAt !== undefined ? { tookMs: s.endedAt - s.startedAt } : {}),
259
+ costUsd: s.result.costUsd,
260
+ };
261
+ }
262
+
263
+ const later = (a: PrReview | undefined, b: PrReview): PrReview =>
264
+ a === undefined || b.at > a.at ? b : a;
265
+
266
+ export function reviewsPort(d: ReviewsPortDeps): ReviewsPort {
267
+ const read = d.read ?? ((p: string) => readFileSync(p, "utf8"));
268
+ const list = d.list ?? ((dir: string) => readdirSync(dir));
269
+ const liveness = {
270
+ alive: d.detach.alive,
271
+ now: d.now,
272
+ ...(d.detach.liveContainers ? { liveContainers: d.detach.liveContainers } : {}),
273
+ };
274
+ /** Run one registry verb in this process; its one JSON line. */
275
+ const verb = async (
276
+ v: "start" | "status" | "stop",
277
+ args: readonly string[],
278
+ ): Promise<{ code: number; json: Json | undefined }> => {
279
+ const lines: string[] = [];
280
+ const code = await runDetachedVerb(v, args, true, {
281
+ ...d.detach,
282
+ home: d.registry,
283
+ out: (l) => lines.push(l),
284
+ });
285
+ return { code, json: lastJson(lines.join("\n")) };
286
+ };
287
+ const views = (repo: string): Known<RunView[]> => {
288
+ try {
289
+ return known(
290
+ listRunViews(d.registry, liveness).filter(
291
+ (r) => prOfTarget(str(r.view["target"]))?.repo === repo,
292
+ ),
293
+ );
294
+ } catch (err) {
295
+ return unknown(`can't read the review runs in ${d.registry}: ${message(err)}`);
296
+ }
297
+ };
298
+
299
+ return {
300
+ preview: async (repo, pr, o = {}) => {
301
+ const r = await d.qcr(["review", String(pr), "--repo", repo, "--preview", "--json"], o);
302
+ const parsed = parsePreview(lastJson(r.stdout));
303
+ if (parsed.ok || lastJson(r.stdout) !== undefined) return parsed;
304
+ return {
305
+ ok: false,
306
+ why: (r.stderr.trim().split("\n").pop() ?? "").trim() || `qcr exited ${r.code}`,
307
+ };
308
+ },
309
+
310
+ start: async (repo, pr, key) => {
311
+ const stamp = new Date(d.now())
312
+ .toISOString()
313
+ .replace(/[^0-9]/g, "")
314
+ .slice(0, 14);
315
+ const dir = join(d.reportsDir, repo.replace("/", "-"));
316
+ try {
317
+ mkdirSync(dir, { recursive: true });
318
+ } catch (err) {
319
+ return { ok: false, why: `can't make ${dir}: ${message(err)}` };
320
+ }
321
+ const report = join(dir, `pr-${pr}-${stamp}.md`);
322
+ // a file in the state home, never the PR: the run posts nothing by itself
323
+ const args = [String(pr), "--repo", repo, "--key", key, "--to", "file", "--out", report];
324
+ const r = await verb("start", args);
325
+ const runId = str(r.json?.["run_id"]);
326
+ if (r.code !== 0 || runId === undefined)
327
+ return { ok: false, why: errorsOf(r.json, "the review did not start") };
328
+ return { ok: true, runId, report };
329
+ },
330
+
331
+ status: async (runId) => {
332
+ try {
333
+ const v = runView(d.registry, runId, liveness);
334
+ if (v === null) return unknown(`no review run ${runId}`);
335
+ const s = parseRunStatus(v.view, v.argv);
336
+ return s === null ? unknown(`run ${runId}'s status is not one the home reads`) : known(s);
337
+ } catch (err) {
338
+ return unknown(message(err));
339
+ }
340
+ },
341
+
342
+ stop: async (runId) => {
343
+ const r = await verb("stop", [runId]);
344
+ return r.code === 0 ? { ok: true } : { ok: false, why: errorsOf(r.json, "it did not stop") };
345
+ },
346
+
347
+ runs: async (repo) => {
348
+ const v = views(repo);
349
+ if (v.kind === "unknown") return v;
350
+ return known(
351
+ v.value.flatMap((r) => {
352
+ const s = parseRunStatus(r.view, r.argv);
353
+ return s === null ? [] : [s];
354
+ }),
355
+ );
356
+ },
357
+
358
+ latest: async (repo) => {
359
+ const v = views(repo);
360
+ if (v.kind === "unknown") return v;
361
+ const by = new Map<number, PrReview>();
362
+ for (const r of v.value) {
363
+ const s = parseRunStatus(r.view, r.argv);
364
+ const rv = s && s.pr !== null ? runReview(s) : null;
365
+ if (s && s.pr !== null && rv) by.set(s.pr, later(by.get(s.pr), rv));
366
+ }
367
+ // the per-target review state: a PR reviewed from a checkout
368
+ let stores: string[];
369
+ try {
370
+ stores = existsSync(d.reviewHome) ? list(d.reviewHome) : [];
371
+ } catch (err) {
372
+ return unknown(`can't read the review state in ${d.reviewHome}: ${message(err)}`);
373
+ }
374
+ const prefix = `pr:${repo}#`;
375
+ for (const store of stores) {
376
+ const dir = join(d.reviewHome, store, "state");
377
+ let files: string[];
378
+ try {
379
+ files = list(dir);
380
+ } catch (err) {
381
+ if (isMissing(err) || (err as NodeJS.ErrnoException).code === "ENOTDIR") continue;
382
+ return unknown(`can't read ${dir}: ${message(err)}`);
383
+ }
384
+ for (const f of files.filter((x) => x.endsWith(".json"))) {
385
+ let s: ReviewState;
386
+ try {
387
+ s = JSON.parse(read(join(dir, f))) as ReviewState;
388
+ } catch {
389
+ continue; // unreadable state is no state (reviewstate.ts)
390
+ }
391
+ if (typeof s.target !== "string" || !s.target.startsWith(prefix)) continue;
392
+ const pr = Number(s.target.slice(prefix.length));
393
+ const last = Array.isArray(s.rounds) ? s.rounds[s.rounds.length - 1] : undefined;
394
+ const at = last ? Date.parse(last.reviewed_at) : NaN;
395
+ if (!Number.isInteger(pr) || !Number.isFinite(at)) continue;
396
+ let open: ReviewFinding[];
397
+ try {
398
+ open = openFindings(s).findings.map((x) => ({
399
+ severity: x.severity ?? "P3",
400
+ path: x.path,
401
+ line: x.line,
402
+ title: x.title ?? "(untitled finding)",
403
+ lens: x.lens || null,
404
+ }));
405
+ } catch {
406
+ continue;
407
+ }
408
+ by.set(
409
+ pr,
410
+ later(by.get(pr), {
411
+ findings: open.length,
412
+ high: high(open),
413
+ by: "you",
414
+ at,
415
+ list: open,
416
+ costUsd: last?.cost_usd ?? null,
417
+ }),
418
+ );
419
+ }
420
+ }
421
+ return known(by);
422
+ },
423
+
424
+ autoRounds: async () => {
425
+ let snapshot: DaemonSnapshot;
426
+ try {
427
+ snapshot = await d.snapshot();
428
+ } catch (err) {
429
+ return unknown(`can't read auto-review's status: ${message(err)}`);
430
+ }
431
+ // auto-review recorded nothing here: it reviewed nothing
432
+ if (snapshot.kind === "none") return known(new Map());
433
+ if (snapshot.kind === "off")
434
+ return unknown("auto-review is off, and its reviews are read from it while it runs");
435
+ if (snapshot.kind === "unknown") return unknown(snapshot.why);
436
+ let snap: Json | undefined;
437
+ try {
438
+ snap = obj(JSON.parse(snapshot.text));
439
+ } catch (err) {
440
+ return unknown(`auto-review's status is not JSON: ${message(err)}`);
441
+ }
442
+ const prs = snap?.["prs"];
443
+ if (!Array.isArray(prs)) return unknown("auto-review's status lists no pull requests");
444
+ const written = Date.parse(str(snap?.["written_at"]) ?? "");
445
+ const by = new Map<number, PrReview>();
446
+ for (const p of prs) {
447
+ const x = obj(p);
448
+ const n = num(x?.["number"]);
449
+ // `rounds` counts every round, running and failed ones too; a verdict
450
+ // is only recorded when a round finishes (src/state.ts): that is a review
451
+ const verdict = str(x?.["last_verdict"]);
452
+ if (x === undefined || n === undefined || verdict === undefined || verdict === "") continue;
453
+ const at = Date.parse(str(x["updated_at"]) ?? "");
454
+ // the snapshot counts rounds, not findings: those are on the PR
455
+ by.set(n, {
456
+ findings: null,
457
+ high: null,
458
+ by: "auto",
459
+ at: Number.isFinite(at) ? at : Number.isFinite(written) ? written : 0,
460
+ });
461
+ }
462
+ return known(by);
463
+ },
464
+
465
+ post: async (file) => {
466
+ try {
467
+ const r = await d.post(file);
468
+ return r.failed
469
+ ? { ok: false, why: r.line.replace(/^Review not posted: /, "") }
470
+ : { ok: true, line: r.line };
471
+ } catch (err) {
472
+ return { ok: false, why: message(err) };
473
+ }
474
+ },
475
+
476
+ dryRun: async () => {
477
+ try {
478
+ return known(d.dryRun());
479
+ } catch (err) {
480
+ return unknown(message(err));
481
+ }
482
+ },
483
+ };
484
+ }
485
+
486
+ // ---- the real assembly -------------------------------------------------------------
487
+
488
+ /** The app's bin, run as a child of this node. */
489
+ export function qcrChild(o: {
490
+ execPath: string;
491
+ bin: string;
492
+ cwd: string;
493
+ env: Env;
494
+ timeoutMs?: number;
495
+ }): ReviewsPortDeps["qcr"] {
496
+ return (args, { signal } = {}) =>
497
+ new Promise((resolve) => {
498
+ execFile(
499
+ o.execPath,
500
+ [o.bin, ...args],
501
+ {
502
+ cwd: o.cwd,
503
+ env: { ...o.env },
504
+ timeout: o.timeoutMs ?? 180_000,
505
+ maxBuffer: 32 << 20,
506
+ ...(signal ? { signal } : {}),
507
+ },
508
+ (err, stdout, stderr) => {
509
+ const e = err as (NodeJS.ErrnoException & { code?: unknown }) | null;
510
+ resolve({
511
+ code: e === null ? 0 : typeof e.code === "number" ? e.code : 1,
512
+ stdout: String(stdout),
513
+ stderr: e && !stderr ? e.message : String(stderr),
514
+ });
515
+ },
516
+ );
517
+ });
518
+ }
519
+
520
+ /** The registry's process seams, as `qcr review start` has them, with the
521
+ * app's bin as the driver: a run is a process group of its own that outlives
522
+ * the home. */
523
+ export function realDetachSeams(o: {
524
+ execPath: string;
525
+ bin: string;
526
+ cwd: string;
527
+ env: Env;
528
+ }): Omit<DetachDeps, "home" | "out"> {
529
+ const entry = { entry: o.bin, execPath: o.execPath, execArgv: [] as string[] };
530
+ return {
531
+ now: Date.now,
532
+ sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
533
+ newId: () =>
534
+ `${new Date()
535
+ .toISOString()
536
+ .replace(/[^0-9]/g, "")
537
+ .slice(0, 14)}-${randomBytes(3).toString("hex")}`,
538
+ alive: (pid) => {
539
+ try {
540
+ process.kill(pid, 0);
541
+ return true;
542
+ } catch {
543
+ return false;
544
+ }
545
+ },
546
+ kill: (pid, sig) => process.kill(-pid, sig),
547
+ identify: (args) => identifyTarget(args, o.cwd),
548
+ launch: (l) =>
549
+ launchDetached({
550
+ ...detachedCommand(l, entry),
551
+ dir: l.dir,
552
+ env: { ...o.env },
553
+ cwd: o.cwd,
554
+ }),
555
+ watch: (w) => {
556
+ launchDetached({
557
+ ...watchdogCommand(w, entry),
558
+ dir: w.dir,
559
+ env: { ...o.env },
560
+ cwd: o.cwd,
561
+ log: "watchdog.log",
562
+ });
563
+ },
564
+ };
565
+ }