@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/live/read.ts DELETED
@@ -1,86 +0,0 @@
1
- // Finding and reading a round's LiveEvent logs: what `qcr review status` and
2
- // `qcr live` do with a run directory. No writer and no engine here, so a
3
- // daemon-side or TUI reader can import it.
4
- import { readdirSync, statSync } from "node:fs";
5
- import { join } from "node:path";
6
- import {
7
- LIVE_MAIN_FILE,
8
- readLiveEvents,
9
- type LiveEvent,
10
- } from "@quickchatai/pi-quickreview/src/live/events.js";
11
- import { projectRoundProgress } from "./projection.js";
12
- import type { RoundProgressSnapshot } from "@quickchatai/pi-quickreview/src/round-progress.js";
13
-
14
- export interface LiveRoundRef {
15
- /** `state/live/<key>/r<round>` */
16
- dir: string;
17
- key: string;
18
- round: number;
19
- }
20
-
21
- const names = (dir: string): string[] => {
22
- try {
23
- return readdirSync(dir);
24
- } catch {
25
- return [];
26
- }
27
- };
28
-
29
- /** Every round log directory under `<root>/state/live/`, oldest round first. */
30
- export function listLiveRounds(root: string): LiveRoundRef[] {
31
- const base = join(root, "state", "live");
32
- const found: LiveRoundRef[] = [];
33
- for (const key of names(base)) {
34
- for (const r of names(join(base, key))) {
35
- const m = /^r(\d+)$/.exec(r);
36
- if (m) found.push({ dir: join(base, key, r), key, round: Number(m[1]) });
37
- }
38
- }
39
- return found.sort((a, b) => a.key.localeCompare(b.key) || a.round - b.round);
40
- }
41
-
42
- function mtime(path: string): number {
43
- try {
44
- return statSync(path).mtimeMs;
45
- } catch {
46
- return 0;
47
- }
48
- }
49
-
50
- /** The round that was written last. */
51
- export function latestLiveRound(root: string): LiveRoundRef | undefined {
52
- return listLiveRounds(root).reduce<LiveRoundRef | undefined>(
53
- (best, r) =>
54
- best === undefined ||
55
- mtime(join(r.dir, LIVE_MAIN_FILE)) >= mtime(join(best.dir, LIVE_MAIN_FILE))
56
- ? r
57
- : best,
58
- undefined,
59
- );
60
- }
61
-
62
- /** The ids of the lens sessions a round logged, in file-name order. */
63
- export function lensLogIds(dir: string): string[] {
64
- return names(dir)
65
- .flatMap((f) => /^lens-(.+)\.jsonl$/.exec(f)?.[1] ?? [])
66
- .sort();
67
- }
68
-
69
- export interface RoundView {
70
- ref: LiveRoundRef;
71
- events: readonly LiveEvent[];
72
- /** The round-progress v2 snapshot the main log projects to. */
73
- snapshot: RoundProgressSnapshot | undefined;
74
- lenses: string[];
75
- }
76
-
77
- /** Read one round's main log and project it. */
78
- export function viewRound(ref: LiveRoundRef): RoundView {
79
- const { events } = readLiveEvents(join(ref.dir, LIVE_MAIN_FILE));
80
- return {
81
- ref,
82
- events,
83
- snapshot: projectRoundProgress(events)?.snapshot,
84
- lenses: lensLogIds(ref.dir),
85
- };
86
- }
@@ -1,103 +0,0 @@
1
- // `qcr onboard` mechanics: launch pi at the pinned version via npx
2
- // (pi stays a non-dependency — this works from a bare clone with only Node and
3
- // the checkout), hand it the onboarding system prompt + exactly one onboarding
4
- // extension + a restricted built-in tool set, and on exit either exec the TUI
5
- // (checklist complete) or say how to resume.
6
- //
7
- // Everything effectful is injected (spawn, completion read, log), so tests
8
- // drive the whole flow with fakes; src/cli.ts cmdOnboard is the one real
9
- // assembly.
10
- import { join } from "node:path";
11
- import { defaultSteps } from "./steps/index.js";
12
- import { onboardingToolAllowlist } from "./toolnames.js";
13
-
14
- // The pin reader lives in a leaf module: src/version.ts (on the daemon's import
15
- // graph) needs it, and this module imports the whole step registry.
16
- export { piPinFromPackageJson, type PiPin } from "./pipin.js";
17
- import type { PiPin } from "./pipin.js";
18
-
19
- /** Dedicated onboarding session store — never the reviewer/companion dirs, so
20
- * onboarding cannot pollute (or be polluted by) any other pi session store. */
21
- export function onboardingSessionDir(root: string): string {
22
- return join(root, "state", "onboarding", "pi-sessions");
23
- }
24
-
25
- /**
26
- * The full npx argv for the onboarding pi session.
27
- *
28
- * `--approve` is the same never-block-on-input rule as buildPiCommand
29
- * (src/sessions.ts) and the companion's piArgv: the wizard must never stall
30
- * on pi's interactive "Trust project folder?" modal.
31
- *
32
- * VERIFY-ON-PIN: `--no-extensions`, `--no-skills`, `--no-context-files`,
33
- * `-e <path>` and `--tools <list>` against the pinned pi version's --help —
34
- * the intent (no ambient extensions, exactly one onboarding extension, a
35
- * read-only built-in belt) is the design decision; the exact flag spellings
36
- * are unverified statically and belong to the named onboarding smoke step.
37
- *
38
- * `--tools` is an allowlist over extension tools too (pi 1.4.0; pinned by
39
- * tests/onboard-tools-sdk.test.ts on the real SDK), so it names every wizard
40
- * tool, derived from the step registry: a step added there is active here
41
- * with no second edit. bash, write and edit stay off it.
42
- */
43
- export function onboardingPiArgv(
44
- pin: PiPin,
45
- root: string,
46
- stepIds: readonly string[] = defaultSteps().map((s) => s.id),
47
- ): string[] {
48
- return [
49
- "npx",
50
- "-y",
51
- `${pin.package}@${pin.version}`,
52
- "--approve",
53
- "--no-extensions",
54
- "--no-skills",
55
- "--no-context-files",
56
- "-e",
57
- join(root, "extensions", "cli", "onboarding.ts"),
58
- "--system-prompt",
59
- join(root, "prompts", "onboarding.md"),
60
- "--tools",
61
- onboardingToolAllowlist(stepIds).join(","),
62
- "--session-dir",
63
- onboardingSessionDir(root),
64
- ];
65
- }
66
-
67
- export interface OnboardRunDeps {
68
- pin: PiPin;
69
- root: string;
70
- /** Spawn with stdio inherited; resolves with the exit code (a spawn failure
71
- * is a nonzero code, never a throw). */
72
- spawn(cmd: string, args: string[]): Promise<number>;
73
- /** Does the checklist file say every step verified? (engine.checklistComplete
74
- * over state/ — the finish tool only sets the flag after a green re-verify.) */
75
- isComplete(): boolean;
76
- log(line: string): void;
77
- /** Compiled CLI path for the TUI handoff (dist/src/cli.js). */
78
- cliPath: string;
79
- /** Node binary for the handoff; defaults to the running one. */
80
- nodeBin?: string;
81
- }
82
-
83
- /**
84
- * Run the onboarding session and handle the handoff:
85
- * - checklist complete after pi exits ⇒ open the TUI (same stdio);
86
- * - otherwise print how to resume — `qcr onboard` again re-verifies
87
- * completed steps and continues at the first unfinished one.
88
- */
89
- export async function runOnboard(deps: OnboardRunDeps): Promise<number> {
90
- const argv = onboardingPiArgv(deps.pin, deps.root);
91
- deps.log(
92
- `onboarding: launching pi ${deps.pin.package}@${deps.pin.version} via npx (first run downloads it — give it a moment)`,
93
- );
94
- const code = await deps.spawn(argv[0]!, argv.slice(1));
95
- if (deps.isComplete()) {
96
- deps.log("onboarding: checklist verified complete — opening the TUI.");
97
- return deps.spawn(deps.nodeBin ?? process.execPath, [deps.cliPath, "tui"]);
98
- }
99
- deps.log(
100
- "onboarding: not finished yet. Run `qcr onboard` again to resume — completed steps are re-verified, not repeated.",
101
- );
102
- return code;
103
- }
@@ -1,324 +0,0 @@
1
- // The COVERAGE LEDGER (spec "Ledger"): what the
2
- // scan has read — one row per file + content hash + lens, with the tier,
3
- // effort, validator setting, scan sha and cost it was read at. Two jobs:
4
- //
5
- // * the RESUME GRAIN — stage 1 never re-reads a file whose NEWEST ledger
6
- // row for the lens is at the same content hash, so an interrupted scan (a TUI
7
- // quit, an expired login, a stop, the hard stop) continues where it
8
- // stopped and a finished one costs nothing to start again;
9
- // * the PRICE — after the first-scan calibration run, every cost preview is
10
- // measured from these rows' actual costs, never from a constant.
11
- //
12
- // A file too big for one case is read in LINE RANGES (scan.ts cutRanges):
13
- // one row per range, carrying it, so a file is covered only when the ranges
14
- // of its newest reading cover every line — a run cut short between ranges
15
- // leaves the rest to read, and changed content re-reads every range.
16
- //
17
- // A row records the DEPTH it was read at (`[scan] depth`; absent = standard,
18
- // so a ledger written before depths reads as standard). A deep reading is its
19
- // own coverage: a standard reading of a file never covers it for a deep run
20
- // (a deep run on a scanned repo would otherwise read nothing), while a deep
21
- // reading covers it for a standard one. A standard row and a deep row of the
22
- // same file, content and lens stand side by side.
23
- //
24
- // A row is written only when its case FINISHED — a case cut short leaves no
25
- // row, so it is read again in full. A case's cost is apportioned over its
26
- // files by lines; a case the runner could not price writes null costs, which
27
- // the rate ignores (it never reads as free).
28
- //
29
- // Persistence: `<state home>/scan/ledger.json`, tmp+rename atomic through the
30
- // state port. An unreadable ledger is REPORTED and refuses writes: it is the
31
- // record of money already spent, and starting over on top of it would pay
32
- // for the same files twice.
33
- import type { ScanDepth } from "../config.js";
34
- import type { StateFs } from "./engine.js";
35
- import { coverKey, rangesCover, type FileRange, type ScanRate } from "./scan.js";
36
-
37
- export const LEDGER_FILE = "scan/ledger.json";
38
- export const LEDGER_VERSION = 1;
39
-
40
- export interface LedgerEntry {
41
- path: string;
42
- /** The file's git blob hash at `sha`. */
43
- blob: string;
44
- lens: string;
45
- /** The line range this row read, when the file was cut into ranges;
46
- * absent (or null) = the whole file. Part of the row's identity. */
47
- range?: FileRange | null;
48
- tier: string;
49
- effort: string;
50
- /** Whether validation was enabled for this case; a lens returning NONE needs no validator run. */
51
- validator: boolean;
52
- /** `"deep"` for a deep scan's reading; absent = standard. */
53
- depth?: ScanDepth;
54
- sha: string;
55
- /** Lines read: the file's, or its range's. */
56
- lines: number;
57
- /** This file's share of its case's cost; null when the runner reported
58
- * none. */
59
- cost_usd: number | null;
60
- run: string;
61
- /** The case, within the run (`<unit>` or `<unit>#<i>`). */
62
- case: string;
63
- at: string;
64
- }
65
-
66
- /** What a price is measured over: the same lens at the same settings — at
67
- * one depth when `depth` is given, at any otherwise. */
68
- export interface RateKey {
69
- lens: string;
70
- tier: string;
71
- effort: string;
72
- validator: boolean;
73
- depth?: ScanDepth;
74
- }
75
-
76
- /** The depth a row was read at. */
77
- export const depthOf = (e: Pick<LedgerEntry, "depth">): ScanDepth =>
78
- e.depth === "deep" ? "deep" : "standard";
79
-
80
- export interface LedgerFile {
81
- version: number;
82
- entries: LedgerEntry[];
83
- }
84
-
85
- const entryKey = (e: Pick<LedgerEntry, "path" | "blob" | "lens" | "range" | "depth">): string =>
86
- `${e.lens}\0${coverKey(e.path, e.blob)}${e.range ? `\0${e.range.start}-${e.range.end}` : ""}${depthOf(e) === "deep" ? "\0deep" : ""}`;
87
-
88
- export class Ledger {
89
- constructor(private readonly state: StateFs) {}
90
-
91
- /** The ledger, or why it cannot be read (then `entries` is empty and
92
- * UNKNOWN, not absent). */
93
- load(): { entries: LedgerEntry[]; error: string | null } {
94
- let text: string | null;
95
- try {
96
- text = this.state.read(LEDGER_FILE);
97
- } catch (err) {
98
- return {
99
- entries: [],
100
- error: `${LEDGER_FILE} in ${this.state.location} cannot be read: ${String(err)}`,
101
- };
102
- }
103
- if (text === null) return { entries: [], error: null };
104
- try {
105
- const raw = JSON.parse(text) as Partial<LedgerFile> | null;
106
- if (
107
- typeof raw !== "object" ||
108
- raw === null ||
109
- raw.version !== LEDGER_VERSION ||
110
- !Array.isArray(raw.entries)
111
- ) {
112
- throw new Error("not a version-1 ledger");
113
- }
114
- for (const e of raw.entries) {
115
- if (
116
- typeof e?.path !== "string" ||
117
- typeof e.blob !== "string" ||
118
- typeof e.lens !== "string"
119
- ) {
120
- throw new Error("an entry has no path, blob or lens");
121
- }
122
- }
123
- return { entries: raw.entries, error: null };
124
- } catch (err) {
125
- return {
126
- entries: [],
127
- error:
128
- `${LEDGER_FILE} in ${this.state.location} is not a coverage ledger this version reads (${String(err)}) — ` +
129
- "nothing will overwrite it; move it aside to start the ledger over",
130
- };
131
- }
132
- }
133
-
134
- /** Add (or replace, by file + content hash + lens + range + depth)
135
- * finished rows. Throws when the stored ledger cannot be read — never
136
- * writes over it. */
137
- record(rows: readonly LedgerEntry[]): void {
138
- const { entries, error } = this.load();
139
- if (error !== null) throw new Error(error);
140
- const byKey = new Map(entries.map((e) => [entryKey(e), e]));
141
- for (const r of rows) byKey.set(entryKey(r), r);
142
- const file: LedgerFile = { version: LEDGER_VERSION, entries: [...byKey.values()] };
143
- this.state.write(LEDGER_FILE, `${JSON.stringify(file)}\n`);
144
- }
145
- }
146
-
147
- /** `path\0blob` of each file's NEWEST reading by `lens` — the same newest
148
- * row `FindingsStore.reconcile` settles findings against — complete at
149
- * `depth` (`readings`). An older reading a later one superseded covers
150
- * nothing: its findings may since have been settled `fixed`, so content that
151
- * comes back to it (a revert) must be read again for them to come back too. */
152
- export function coveredBy(
153
- entries: readonly LedgerEntry[],
154
- lens: string,
155
- depth: ScanDepth = "standard",
156
- ): Set<string> {
157
- const out = new Set<string>();
158
- for (const [path, r] of readings(entries, lens, depth))
159
- if (r.complete) out.add(coverKey(path, r.blob));
160
- return out;
161
- }
162
-
163
- /** A file's newest reading by one lens: the rows at its newest content,
164
- * back to the last row at other content (an older reading of the same
165
- * content, superseded since, is not part of it). */
166
- export interface Reading {
167
- blob: string;
168
- /** The newest row of the reading. */
169
- newest: LedgerEntry;
170
- /** Whole file, or ranges covering every line. */
171
- complete: boolean;
172
- /** The ranges read so far (empty when a whole-file row stands). */
173
- ranges: FileRange[];
174
- /** Every run that wrote a row of this reading — a ranged reading cut short
175
- * is finished by a later run, and both re-read the file. */
176
- runs: string[];
177
- }
178
-
179
- /** Every path's newest reading by `lens`, as `depth` counts it. The newest
180
- * content is settled by every row whatever its depth (a later reading of
181
- * other content supersedes a deep one too); within it, a `"standard"` reading
182
- * is made of every row — a deep reading is at least a standard one — and a
183
- * `"deep"` reading of the deep rows alone, so a path whose newest content was
184
- * read only at standard depth has no deep reading. */
185
- export function readings(
186
- entries: readonly LedgerEntry[],
187
- lens: string,
188
- depth: ScanDepth = "standard",
189
- ): Map<string, Reading> {
190
- const byPath = new Map<string, LedgerEntry[]>();
191
- for (const e of entries) {
192
- if (e.lens !== lens) continue;
193
- const rows = byPath.get(e.path);
194
- if (rows) rows.push(e);
195
- else byPath.set(e.path, [e]);
196
- }
197
- const out = new Map<string, Reading>();
198
- for (const [path, rows] of byPath) {
199
- // Stable by time: equal stamps keep ledger order, as `newestRead` does.
200
- const sorted = rows
201
- .map((e, i) => ({ e, i }))
202
- .sort((a, b) => compareAt(a.e.at, b.e.at) || a.i - b.i);
203
- const newest = sorted[sorted.length - 1]!.e;
204
- const all: LedgerEntry[] = [];
205
- for (let k = sorted.length - 1; k >= 0 && sorted[k]!.e.blob === newest.blob; k--)
206
- all.push(sorted[k]!.e);
207
- const streak = depth === "deep" ? all.filter((e) => depthOf(e) === "deep") : all;
208
- if (streak.length === 0) continue;
209
- const whole = streak.some((e) => !e.range);
210
- const ranges = streak.filter((e) => e.range).map((e) => e.range!);
211
- const total = ranges[0]?.total ?? 0;
212
- out.set(path, {
213
- blob: newest.blob,
214
- newest: streak[0]!,
215
- complete: whole || (ranges.length > 0 && rangesCover(ranges, 1, total)),
216
- ranges: whole ? [] : ranges,
217
- runs: [...new Set(streak.map((e) => e.run))],
218
- });
219
- }
220
- return out;
221
- }
222
-
223
- /** `path\0blob` → the ranges of each newest reading by `lens` at `depth` that
224
- * is not yet complete: the ranges a plan need not read again. */
225
- export function partialRanges(
226
- entries: readonly LedgerEntry[],
227
- lens: string,
228
- depth: ScanDepth = "standard",
229
- ): Map<string, FileRange[]> {
230
- const out = new Map<string, FileRange[]>();
231
- for (const [path, r] of readings(entries, lens, depth))
232
- if (!r.complete && r.ranges.length > 0) out.set(coverKey(path, r.blob), r.ranges);
233
- return out;
234
- }
235
-
236
- /** One case's identity across its rows: the run it ran in and its id there. */
237
- const caseId = (e: LedgerEntry): string => `${e.run}\0${e.case}`;
238
-
239
- /** Was `e` written at `key`'s settings — its depth too, when the key names one? */
240
- function atKey(e: LedgerEntry, key: RateKey): boolean {
241
- return (
242
- e.lens === key.lens &&
243
- e.tier === key.tier &&
244
- e.effort === key.effort &&
245
- e.validator === key.validator &&
246
- (key.depth === undefined || depthOf(e) === key.depth)
247
- );
248
- }
249
-
250
- /** Rows written for `key` at all, and how many of their cases were priced —
251
- * which is how a preview tells "never calibrated" from "calibrated, but the
252
- * runner reports no cost". */
253
- export function casesFor(
254
- entries: readonly LedgerEntry[],
255
- key: RateKey,
256
- ): { cases: number; priced: number } {
257
- const cases = new Map<string, boolean>();
258
- for (const e of entries) {
259
- if (!atKey(e, key)) continue;
260
- const id = caseId(e);
261
- cases.set(id, (cases.get(id) ?? false) || e.cost_usd !== null);
262
- }
263
- return { cases: cases.size, priced: [...cases.values()].filter(Boolean).length };
264
- }
265
-
266
- /** The measured price for `key`: every priced row's cost over its lines, or
267
- * null when no priced case has finished at these settings. */
268
- export function measuredRate(entries: readonly LedgerEntry[], key: RateKey): ScanRate | null {
269
- return rateWhere(entries, (e) => atKey(e, key));
270
- }
271
-
272
- /** The measured price over the priced rows `keep` selects (stage 2 borrows
273
- * another lens's price at the same settings this way), or null when none. */
274
- export function rateWhere(
275
- entries: readonly LedgerEntry[],
276
- keep: (e: LedgerEntry) => boolean,
277
- ): ScanRate | null {
278
- let cost = 0;
279
- let lines = 0;
280
- const cases = new Set<string>();
281
- for (const e of entries) {
282
- if (!keep(e) || e.cost_usd === null) continue;
283
- cost += e.cost_usd;
284
- // The file's REAL lines — the count the preview showed. An empty file
285
- // (an `__init__.py`) is 0 here as there; `Math.max(lines, 1)` belongs to
286
- // splitting a case's cost across its rows, never to the rate's basis.
287
- lines += e.lines;
288
- cases.add(caseId(e));
289
- }
290
- if (cases.size === 0 || lines === 0) return null;
291
- return { usd_per_kline: (cost / lines) * 1000, cases: cases.size, lines, cost_usd: cost };
292
- }
293
-
294
- // ---- the delta's basis ----
295
-
296
- /** Timestamp order, the one rule `readings` and `newestRead` both sort by. */
297
- function compareAt(a: string, b: string): number {
298
- return a < b ? -1 : a > b ? 1 : 0;
299
- }
300
-
301
- /** Each path's NEWEST content hash read by `lens` — the reading findings
302
- * are settled against. A path whose content is not that one has CHANGED
303
- * since it was scanned, even when an older, superseded reading saw exactly
304
- * this content (a revert): that reading's findings may since be `fixed`. */
305
- export function newestRead(entries: readonly LedgerEntry[], lens: string): Map<string, string> {
306
- const newest = new Map<string, LedgerEntry>();
307
- for (const e of entries) {
308
- if (e.lens !== lens) continue;
309
- const had = newest.get(e.path);
310
- if (!had || compareAt(e.at, had.at) >= 0) newest.set(e.path, e);
311
- }
312
- return new Map([...newest].map(([path, e]) => [path, e.blob]));
313
- }
314
-
315
- /** The sha of the LAST scan by `lens` — its newest row's — or null when the
316
- * ledger holds none: what a delta re-scan compares the tree against. */
317
- export function lastScanned(
318
- entries: readonly LedgerEntry[],
319
- lens: string,
320
- ): { sha: string; at: string; run: string } | null {
321
- let last: LedgerEntry | null = null;
322
- for (const e of entries) if (e.lens === lens && (last === null || e.at >= last.at)) last = e;
323
- return last === null ? null : { sha: last.sha, at: last.at, run: last.run };
324
- }