@quickchatai/quickreview 1.2.0 → 1.3.1

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 (322) hide show
  1. package/README.md +114 -50
  2. package/bin/qcr.js +1 -1
  3. package/config.example.toml +20 -6
  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/src/archive/restore.js +18 -15
  25. package/dist/src/archive/restore.js.map +1 -1
  26. package/dist/src/botworker.js +13 -2
  27. package/dist/src/botworker.js.map +1 -1
  28. package/dist/src/channelfs.js +3 -26
  29. package/dist/src/channelfs.js.map +1 -1
  30. package/dist/src/cli.js +12 -50
  31. package/dist/src/cli.js.map +1 -1
  32. package/dist/src/config.js +16 -0
  33. package/dist/src/config.js.map +1 -1
  34. package/dist/src/daemon.js +26 -0
  35. package/dist/src/daemon.js.map +1 -1
  36. package/dist/src/debuground.js +1 -0
  37. package/dist/src/debuground.js.map +1 -1
  38. package/dist/src/debugroundpi.js +1 -0
  39. package/dist/src/debugroundpi.js.map +1 -1
  40. package/dist/src/engine/caserunner-sdk.js +15 -251
  41. package/dist/src/engine/caserunner-sdk.js.map +1 -1
  42. package/dist/src/home/adapters.js +349 -0
  43. package/dist/src/home/adapters.js.map +1 -0
  44. package/dist/src/home/advanced.js +318 -0
  45. package/dist/src/home/advanced.js.map +1 -0
  46. package/dist/src/home/advancedtools.js +57 -0
  47. package/dist/src/home/advancedtools.js.map +1 -0
  48. package/dist/src/home/autoreview.js +325 -0
  49. package/dist/src/home/autoreview.js.map +1 -0
  50. package/dist/src/home/daemonstatus.js +32 -0
  51. package/dist/src/home/daemonstatus.js.map +1 -0
  52. package/dist/src/home/devicelogin.js +61 -0
  53. package/dist/src/home/devicelogin.js.map +1 -0
  54. package/dist/src/home/launch.js +202 -0
  55. package/dist/src/home/launch.js.map +1 -0
  56. package/dist/src/home/loginowner.js +205 -0
  57. package/dist/src/home/loginowner.js.map +1 -0
  58. package/dist/src/home/memory.js +122 -0
  59. package/dist/src/home/memory.js.map +1 -0
  60. package/dist/src/home/model.js +811 -0
  61. package/dist/src/home/model.js.map +1 -0
  62. package/dist/src/home/panel.js +520 -0
  63. package/dist/src/home/panel.js.map +1 -0
  64. package/dist/src/home/ports.js +19 -0
  65. package/dist/src/home/ports.js.map +1 -0
  66. package/dist/src/home/review.js +163 -0
  67. package/dist/src/home/review.js.map +1 -0
  68. package/dist/src/home/reviews.js +478 -0
  69. package/dist/src/home/reviews.js.map +1 -0
  70. package/dist/src/home/setup.js +507 -0
  71. package/dist/src/home/setup.js.map +1 -0
  72. package/dist/src/home/setupadapters.js +214 -0
  73. package/dist/src/home/setupadapters.js.map +1 -0
  74. package/dist/src/home/setupcard.js +199 -0
  75. package/dist/src/home/setupcard.js.map +1 -0
  76. package/dist/src/home/setuptools.js +45 -0
  77. package/dist/src/home/setuptools.js.map +1 -0
  78. package/dist/src/home/spend.js +160 -0
  79. package/dist/src/home/spend.js.map +1 -0
  80. package/dist/src/home/switches.js +209 -0
  81. package/dist/src/home/switches.js.map +1 -0
  82. package/dist/src/home/switchon.js +439 -0
  83. package/dist/src/home/switchon.js.map +1 -0
  84. package/dist/src/home/toolnames.js +41 -0
  85. package/dist/src/home/toolnames.js.map +1 -0
  86. package/dist/src/home/tune.js +341 -0
  87. package/dist/src/home/tune.js.map +1 -0
  88. package/dist/src/home/words.js +7 -0
  89. package/dist/src/home/words.js.map +1 -0
  90. package/dist/src/homes.js +14 -0
  91. package/dist/src/homes.js.map +1 -1
  92. package/dist/src/install.js +1 -1
  93. package/dist/src/install.js.map +1 -1
  94. package/dist/src/ipc/workerspec.js +8 -0
  95. package/dist/src/ipc/workerspec.js.map +1 -1
  96. package/dist/src/live/publish.js +1 -1
  97. package/dist/src/live/publish.js.map +1 -1
  98. package/dist/src/livecli.js +1 -1
  99. package/dist/src/livecli.js.map +1 -1
  100. package/dist/src/loginrefresh/handover.js +121 -0
  101. package/dist/src/loginrefresh/handover.js.map +1 -0
  102. package/dist/src/loginrefresh/lock.js +54 -3
  103. package/dist/src/loginrefresh/lock.js.map +1 -1
  104. package/dist/src/loginrefresh/refresher.js +33 -5
  105. package/dist/src/loginrefresh/refresher.js.map +1 -1
  106. package/dist/src/loginrefresh/supervisor.js +18 -1
  107. package/dist/src/loginrefresh/supervisor.js.map +1 -1
  108. package/dist/src/npmverbs.js +42 -12
  109. package/dist/src/npmverbs.js.map +1 -1
  110. package/dist/src/onboard/choices.js +6 -3
  111. package/dist/src/onboard/choices.js.map +1 -1
  112. package/dist/src/onboard/engine.js +33 -292
  113. package/dist/src/onboard/engine.js.map +1 -1
  114. package/dist/src/onboard/steps/bringup.js +3 -2
  115. package/dist/src/onboard/steps/bringup.js.map +1 -1
  116. package/dist/src/onboard/steps/configfill.js +18 -7
  117. package/dist/src/onboard/steps/configfill.js.map +1 -1
  118. package/dist/src/onboard/steps/loginshare.js +213 -0
  119. package/dist/src/onboard/steps/loginshare.js.map +1 -0
  120. package/dist/src/onboard/steps/memory.js +14 -2
  121. package/dist/src/onboard/steps/memory.js.map +1 -1
  122. package/dist/src/onboard/steps/repoconfig.js +72 -0
  123. package/dist/src/onboard/steps/repoconfig.js.map +1 -0
  124. package/dist/src/onboard/toolnames.js +2 -2
  125. package/dist/src/onboard/toolnames.js.map +1 -1
  126. package/dist/src/piagentdir.js +13 -0
  127. package/dist/src/piagentdir.js.map +1 -0
  128. package/dist/src/pilogin.js +31 -16
  129. package/dist/src/pilogin.js.map +1 -1
  130. package/dist/src/qcrbin.js +16 -11
  131. package/dist/src/qcrbin.js.map +1 -1
  132. package/dist/src/reviewargs.js +122 -0
  133. package/dist/src/reviewargs.js.map +1 -0
  134. package/dist/src/reviewcli.js +7 -161
  135. package/dist/src/reviewcli.js.map +1 -1
  136. package/dist/src/reviewdetach.js +76 -11
  137. package/dist/src/reviewdetach.js.map +1 -1
  138. package/dist/src/scancli.js +39 -691
  139. package/dist/src/scancli.js.map +1 -1
  140. package/dist/src/selfimprove.js +66 -0
  141. package/dist/src/selfimprove.js.map +1 -0
  142. package/dist/src/setup/adapters.js +5 -223
  143. package/dist/src/setup/adapters.js.map +1 -1
  144. package/dist/src/setup/caserunner.js +15 -249
  145. package/dist/src/setup/caserunner.js.map +1 -1
  146. package/dist/src/setup/engine.js +32 -341
  147. package/dist/src/setup/engine.js.map +1 -1
  148. package/dist/src/setup/facts.js +3 -592
  149. package/dist/src/setup/facts.js.map +1 -1
  150. package/dist/src/setup/findings.js +3 -947
  151. package/dist/src/setup/findings.js.map +1 -1
  152. package/dist/src/setup/presentation.js +3 -8
  153. package/dist/src/setup/presentation.js.map +1 -1
  154. package/dist/src/setup/scan.js +3 -724
  155. package/dist/src/setup/scan.js.map +1 -1
  156. package/dist/src/setup/scanbuild.js +9 -92
  157. package/dist/src/setup/scanbuild.js.map +1 -1
  158. package/dist/src/setup/scanlease.js +3 -250
  159. package/dist/src/setup/scanlease.js.map +1 -1
  160. package/dist/src/setup/scanreport.js +3 -288
  161. package/dist/src/setup/scanreport.js.map +1 -1
  162. package/dist/src/setup/scanrun.js +3 -1732
  163. package/dist/src/setup/scanrun.js.map +1 -1
  164. package/dist/src/setup/scantree.js +3 -256
  165. package/dist/src/setup/scantree.js.map +1 -1
  166. package/dist/src/setup/stage2request.js +3 -258
  167. package/dist/src/setup/stage2request.js.map +1 -1
  168. package/dist/src/setup/unitcut.js +3 -436
  169. package/dist/src/setup/unitcut.js.map +1 -1
  170. package/dist/src/state.js +2 -0
  171. package/dist/src/state.js.map +1 -1
  172. package/dist/src/takeover.js +1 -0
  173. package/dist/src/takeover.js.map +1 -1
  174. package/dist/src/takeoverpi.js +2 -1
  175. package/dist/src/takeoverpi.js.map +1 -1
  176. package/dist/src/tui/board.js +1 -1
  177. package/dist/src/tui/board.js.map +1 -1
  178. package/dist/src/tui/companion.js +2 -1
  179. package/dist/src/tui/companion.js.map +1 -1
  180. package/dist/src/tui/deploytab.js +2 -2
  181. package/dist/src/tui/deploytab.js.map +1 -1
  182. package/dist/src/tui/index.js +9 -6
  183. package/dist/src/tui/index.js.map +1 -1
  184. package/dist/src/tui/memory.js +3 -16
  185. package/dist/src/tui/memory.js.map +1 -1
  186. package/dist/src/tui/memoryurl.js +22 -0
  187. package/dist/src/tui/memoryurl.js.map +1 -0
  188. package/dist/src/tui/remote.js +1 -1
  189. package/dist/src/tui/remote.js.map +1 -1
  190. package/dist/src/tui/route.js +2 -2
  191. package/dist/src/tui/route.js.map +1 -1
  192. package/dist/src/tui/setupstate.js +8 -5
  193. package/dist/src/tui/setupstate.js.map +1 -1
  194. package/dist/src/tui/sshtriage.js +1 -1
  195. package/dist/src/tui/sshtriage.js.map +1 -1
  196. package/dist/src/wire.js +6 -2
  197. package/dist/src/wire.js.map +1 -1
  198. package/dist/src/workerworld.js +4 -0
  199. package/dist/src/workerworld.js.map +1 -1
  200. package/docker/README.md +1 -1
  201. package/docker/entrypoint.sh +40 -6
  202. package/docker-compose.yml +16 -1
  203. package/extensions/cli/home.ts +14 -0
  204. package/extensions/cli/selfimprove.ts +18 -0
  205. package/extensions/home.ts +484 -0
  206. package/extensions/login-route.ts +1 -1
  207. package/extensions/onboarding.ts +8 -7
  208. package/extensions/selfimprove.ts +65 -0
  209. package/extensions/setup-scan.ts +79 -766
  210. package/extensions/setup.ts +14 -70
  211. package/package.json +6 -4
  212. package/prompts/onboarding.md +8 -7
  213. package/schemas/review-result.v1.json +60 -0
  214. package/scripts/deploy.sh +8 -6
  215. package/scripts/qcr-host.sh +28 -5
  216. package/scripts/vm-ghcr-login.sh +7 -4
  217. package/src/archive/restore.ts +18 -21
  218. package/src/botworker.ts +13 -2
  219. package/src/channelfs.ts +3 -26
  220. package/src/cli.ts +11 -54
  221. package/src/config.ts +38 -0
  222. package/src/daemon.ts +26 -0
  223. package/src/debuground.ts +3 -0
  224. package/src/debugroundpi.ts +1 -0
  225. package/src/engine/caserunner-sdk.ts +29 -314
  226. package/src/home/adapters.ts +418 -0
  227. package/src/home/advanced.ts +421 -0
  228. package/src/home/advancedtools.ts +96 -0
  229. package/src/home/autoreview.ts +385 -0
  230. package/src/home/daemonstatus.ts +55 -0
  231. package/src/home/devicelogin.ts +107 -0
  232. package/src/home/launch.ts +257 -0
  233. package/src/home/loginowner.ts +248 -0
  234. package/src/home/memory.ts +176 -0
  235. package/src/home/model.ts +965 -0
  236. package/src/home/panel.ts +666 -0
  237. package/src/home/ports.ts +338 -0
  238. package/src/home/review.ts +206 -0
  239. package/src/home/reviews.ts +565 -0
  240. package/src/home/setup.ts +689 -0
  241. package/src/home/setupadapters.ts +217 -0
  242. package/src/home/setupcard.ts +288 -0
  243. package/src/home/setuptools.ts +57 -0
  244. package/src/home/spend.ts +194 -0
  245. package/src/home/switches.ts +293 -0
  246. package/src/home/switchon.ts +554 -0
  247. package/src/home/toolnames.ts +46 -0
  248. package/src/home/tune.ts +405 -0
  249. package/src/home/words.ts +8 -0
  250. package/src/homes.ts +17 -0
  251. package/src/install.ts +1 -1
  252. package/src/ipc/workerspec.ts +15 -0
  253. package/src/live/publish.ts +4 -1
  254. package/src/livecli.ts +6 -1
  255. package/src/loginrefresh/handover.ts +138 -0
  256. package/src/loginrefresh/lock.ts +72 -3
  257. package/src/loginrefresh/refresher.ts +34 -5
  258. package/src/loginrefresh/supervisor.ts +25 -2
  259. package/src/npmverbs.ts +51 -11
  260. package/src/onboard/choices.ts +6 -3
  261. package/src/onboard/engine.ts +64 -434
  262. package/src/onboard/steps/bringup.ts +3 -2
  263. package/src/onboard/steps/configfill.ts +23 -7
  264. package/src/onboard/steps/loginshare.ts +258 -0
  265. package/src/onboard/steps/memory.ts +18 -2
  266. package/src/onboard/steps/repoconfig.ts +85 -0
  267. package/src/onboard/toolnames.ts +2 -2
  268. package/src/piagentdir.ts +12 -0
  269. package/src/pilogin.ts +32 -16
  270. package/src/qcrbin.ts +16 -11
  271. package/src/reviewargs.ts +119 -0
  272. package/src/reviewcli.ts +6 -167
  273. package/src/reviewdetach.ts +107 -13
  274. package/src/scancli.ts +75 -826
  275. package/src/selfimprove.ts +90 -0
  276. package/src/setup/adapters.ts +13 -221
  277. package/src/setup/caserunner.ts +34 -273
  278. package/src/setup/engine.ts +62 -496
  279. package/src/setup/facts.ts +3 -627
  280. package/src/setup/findings.ts +3 -1264
  281. package/src/setup/presentation.ts +3 -18
  282. package/src/setup/scan.ts +3 -1033
  283. package/src/setup/scanbuild.ts +20 -114
  284. package/src/setup/scanlease.ts +3 -297
  285. package/src/setup/scanreport.ts +3 -373
  286. package/src/setup/scanrun.ts +3 -2268
  287. package/src/setup/scantree.ts +3 -259
  288. package/src/setup/stage2request.ts +3 -347
  289. package/src/setup/unitcut.ts +3 -523
  290. package/src/state.ts +25 -0
  291. package/src/takeover.ts +5 -0
  292. package/src/takeoverpi.ts +3 -2
  293. package/src/tui/board.ts +1 -1
  294. package/src/tui/companion.ts +2 -1
  295. package/src/tui/deploytab.ts +2 -2
  296. package/src/tui/index.ts +9 -6
  297. package/src/tui/memory.ts +3 -15
  298. package/src/tui/memoryurl.ts +22 -0
  299. package/src/tui/remote.ts +1 -1
  300. package/src/tui/route.ts +2 -2
  301. package/src/tui/setupstate.ts +20 -16
  302. package/src/tui/sshtriage.ts +1 -1
  303. package/src/wire.ts +6 -2
  304. package/src/workerworld.ts +9 -1
  305. package/dist/src/live/projection.js +0 -450
  306. package/dist/src/live/projection.js.map +0 -1
  307. package/dist/src/live/read.js +0 -60
  308. package/dist/src/live/read.js.map +0 -1
  309. package/dist/src/onboard/launch.js +0 -75
  310. package/dist/src/onboard/launch.js.map +0 -1
  311. package/dist/src/setup/ledger.js +0 -207
  312. package/dist/src/setup/ledger.js.map +0 -1
  313. package/dist/src/setup/stage2.js +0 -617
  314. package/dist/src/setup/stage2.js.map +0 -1
  315. package/dist/src/setup/textsniff.js +0 -89
  316. package/dist/src/setup/textsniff.js.map +0 -1
  317. package/src/live/projection.ts +0 -491
  318. package/src/live/read.ts +0 -86
  319. package/src/onboard/launch.ts +0 -103
  320. package/src/setup/ledger.ts +0 -324
  321. package/src/setup/stage2.ts +0 -926
  322. package/src/setup/textsniff.ts +0 -95
@@ -1,523 +1,3 @@
1
- // The unit cut (setup & scan spec "Unit"): the
2
- // partition of a repo into stack-aware units — a Django app, a package, a
3
- // workspace — that the survey reads, the scan reviews and every report groups
4
- // by. The COMPANION proposes it, the OPERATOR adjusts it, and once saved it is
5
- // reused by every later survey and scan; the companion re-cuts only on request.
6
- //
7
- // A unit OWNS path prefixes (`roots`). A file belongs to the unit whose root is
8
- // the LONGEST prefix of it, so units nest naturally (`backend` and
9
- // `backend/payments` are two units, and `backend/payments/x.py` is the
10
- // second's); an `ignored` prefix is the same kind of claim held by nobody, so
11
- // `backend/vendor` can be carved out of `backend` and a unit inside an ignored
12
- // tree takes its files back. The root `""` claims everything, which is how a
13
- // catch-all "rest" unit is spelt. A SAVED cut is a partition: every file is in
14
- // exactly one unit or deliberately ignored — a file nobody claims refuses the
15
- // save, because a scan that silently skips it would read as "covered".
16
- //
17
- // Pure: the cut is a value, validation and the adjust ops are functions over
18
- // it and a file listing. The engine persists it (src/setup/engine.ts).
19
- import { SCAN_DEFAULTS } from "../config.js";
20
- import { displayRoot, UNIT_KINDS, UNIT_NAME_MAX, UNIT_NAME_RE, type UnitKind } from "./facts.js";
21
-
22
- export interface Unit {
23
- name: string;
24
- roots: string[];
25
- kind: UnitKind;
26
- /** Why this unit exists — the marker that proposed it, or the operator's
27
- * reason. Shown beside the unit; never parsed. */
28
- note: string;
29
- }
30
-
31
- export interface UnitCut {
32
- units: Unit[];
33
- /** Prefixes nobody reviews (vendored, generated, fixtures the operator
34
- * chose to leave out). */
35
- ignored: string[];
36
- }
37
-
38
- // ---- normalisation -----------------------------------------------------------
39
-
40
- /** A root as stored: repo-relative, no leading `./` or `/`, no trailing `/`.
41
- * Null for anything that could leave the repo (`..`, an absolute path after
42
- * stripping is fine — it is relative by construction — but `..` is not). */
43
- export function normalizeRoot(raw: string): string | null {
44
- let r = raw.trim().replace(/\\/g, "/");
45
- while (r.startsWith("./")) r = r.slice(2);
46
- r = r
47
- .replace(/^\/+/, "")
48
- .replace(/\/+$/, "")
49
- .replace(/\/{2,}/g, "/");
50
- if (r === ".") r = "";
51
- if (r.split("/").some((s) => s === ".." || s === ".")) return null;
52
- return r;
53
- }
54
-
55
- function covers(root: string, path: string): boolean {
56
- return root === "" || path === root || path.startsWith(`${root}/`);
57
- }
58
-
59
- // ---- assignment --------------------------------------------------------------
60
-
61
- export interface Assignment {
62
- /** unit name → its files, in listing order. Every unit has an entry. */
63
- byUnit: Map<string, string[]>;
64
- ignored: string[];
65
- unassigned: string[];
66
- }
67
-
68
- /** Longest-prefix assignment of every listed file. */
69
- export function assign(cut: UnitCut, files: readonly string[]): Assignment {
70
- const claims: Array<{ root: string; unit: string | null }> = [
71
- ...cut.units.flatMap((u) => u.roots.map((root) => ({ root, unit: u.name }))),
72
- ...cut.ignored.map((root) => ({ root, unit: null })),
73
- ].sort((a, b) => b.root.length - a.root.length);
74
- const byUnit = new Map<string, string[]>(cut.units.map((u) => [u.name, []]));
75
- const ignored: string[] = [];
76
- const unassigned: string[] = [];
77
- for (const f of files) {
78
- const hit = claims.find((c) => covers(c.root, f));
79
- if (!hit) unassigned.push(f);
80
- else if (hit.unit === null) ignored.push(f);
81
- else byUnit.get(hit.unit)!.push(f);
82
- }
83
- return { byUnit, ignored, unassigned };
84
- }
85
-
86
- // ---- validation --------------------------------------------------------------
87
-
88
- export interface CutProblems {
89
- /** Refusals: the cut cannot be stored at all. */
90
- errors: string[];
91
- /** Fine for a draft, refused by save (an empty unit, unclaimed files). */
92
- blocking_save: string[];
93
- }
94
-
95
- const SAMPLE = 8;
96
-
97
- /** Refused both when a proposal is read and when a cut is checked. */
98
- const ROOT_IGNORED = "the repo root cannot be ignored — that would leave nothing to review";
99
-
100
- function sample(paths: readonly string[]): string {
101
- const head = paths.slice(0, SAMPLE).join(", ");
102
- return paths.length > SAMPLE ? `${head}, … (+${paths.length - SAMPLE} more)` : head;
103
- }
104
-
105
- /**
106
- * Structural refusals (bad names, duplicate names or roots, a root that could
107
- * leave the repo or matches no file, an ignored repo root) and the two
108
- * save-only conditions (a unit that ends up with no files, files no unit
109
- * claims). The cut is expected normalised (see `normalizeCut`).
110
- */
111
- export function checkCut(cut: UnitCut, files: readonly string[]): CutProblems {
112
- const errors: string[] = [];
113
- const blocking: string[] = [];
114
- if (cut.units.length === 0) errors.push("a unit cut needs at least one unit");
115
- const names = new Set<string>();
116
- const roots = new Map<string, string>();
117
- for (const u of cut.units) {
118
- if (!UNIT_NAME_RE.test(u.name) || u.name.includes("..")) {
119
- errors.push(
120
- `unit name ${JSON.stringify(u.name)} is not a plain name — letters, digits, '.', '_' and '-', at most ${UNIT_NAME_MAX}`,
121
- );
122
- }
123
- if (names.has(u.name)) errors.push(`unit name "${u.name}" is used twice`);
124
- names.add(u.name);
125
- if (u.roots.length === 0) errors.push(`unit "${u.name}" owns no root`);
126
- for (const r of u.roots) {
127
- const owner = roots.get(r);
128
- if (owner !== undefined)
129
- errors.push(`root "${displayRoot(r)}" is claimed by both "${owner}" and "${u.name}"`);
130
- roots.set(r, u.name);
131
- }
132
- }
133
- for (const r of cut.ignored) {
134
- if (r === "") errors.push(ROOT_IGNORED);
135
- const owner = roots.get(r);
136
- if (owner !== undefined) errors.push(`root "${r}" is both ignored and owned by "${owner}"`);
137
- roots.set(r, "(ignored)");
138
- }
139
- for (const [r, owner] of roots) {
140
- if (r !== "" && !files.some((f) => covers(r, f))) {
141
- errors.push(`root "${r}" (${owner}) matches no file in the repo`);
142
- }
143
- }
144
- if (errors.length === 0) {
145
- const a = assign(cut, files);
146
- for (const [name, owned] of a.byUnit) {
147
- if (owned.length === 0) {
148
- blocking.push(
149
- `unit "${name}" ends up with no files — every file under its roots belongs to a deeper unit or is ignored`,
150
- );
151
- }
152
- }
153
- if (a.unassigned.length > 0) {
154
- blocking.push(
155
- `${a.unassigned.length} file(s) belong to no unit: ${sample(a.unassigned)} — add them to a unit, add a catch-all unit with root "", or ignore them`,
156
- );
157
- }
158
- }
159
- return { errors, blocking_save: blocking };
160
- }
161
-
162
- /** Raw input (a tool call's JSON) → a normalised cut, or the reasons it is not
163
- * one. Normalisation never guesses: an unspellable root is an error, not a
164
- * silently-dropped entry. */
165
- export function normalizeCut(
166
- raw: unknown,
167
- ): { ok: true; cut: UnitCut } | { ok: false; errors: string[] } {
168
- const errors: string[] = [];
169
- const obj = (raw ?? {}) as { units?: unknown; ignored?: unknown };
170
- if (!Array.isArray(obj.units))
171
- return { ok: false, errors: ["`units` must be an array of {name, roots, kind?, note?}"] };
172
- const units: Unit[] = [];
173
- for (const [i, u] of obj.units.entries()) {
174
- const unit = (u ?? {}) as { name?: unknown; roots?: unknown; kind?: unknown; note?: unknown };
175
- if (typeof unit.name !== "string") {
176
- errors.push(`units[${i}]: name must be a string`);
177
- continue;
178
- }
179
- const rawRoots = typeof unit.roots === "string" ? [unit.roots] : unit.roots;
180
- if (!Array.isArray(rawRoots) || rawRoots.some((r) => typeof r !== "string")) {
181
- errors.push(`unit "${unit.name}": roots must be an array of repo-relative paths`);
182
- continue;
183
- }
184
- const roots = normalizeRoots(rawRoots as string[], `unit "${unit.name}"`, errors);
185
- units.push({
186
- name: unit.name.trim(),
187
- roots,
188
- kind: unitKind(unit.kind),
189
- note: typeof unit.note === "string" ? unit.note.trim() : "",
190
- });
191
- }
192
- let ignored: string[] = [];
193
- if (obj.ignored !== undefined) {
194
- if (!Array.isArray(obj.ignored) || obj.ignored.some((r) => typeof r !== "string")) {
195
- errors.push("`ignored` must be an array of repo-relative paths");
196
- } else {
197
- ignored = normalizeRoots(obj.ignored as string[], "ignored", errors);
198
- if (ignored.includes("")) errors.push(ROOT_IGNORED);
199
- }
200
- }
201
- return errors.length > 0 ? { ok: false, errors } : { ok: true, cut: { units, ignored } };
202
- }
203
-
204
- /** A supplied kind, or `custom` for anything that is not one of `UNIT_KINDS`. */
205
- function unitKind(raw: unknown): UnitKind {
206
- return typeof raw === "string" && new Set<string>(UNIT_KINDS).has(raw)
207
- ? (raw as UnitKind)
208
- : "custom";
209
- }
210
-
211
- function normalizeRoots(raw: readonly string[], owner: string, errors: string[]): string[] {
212
- const out: string[] = [];
213
- for (const r of raw) {
214
- const n = normalizeRoot(r);
215
- if (n === null) errors.push(`${owner}: root ${JSON.stringify(r)} leaves the repo`);
216
- else if (!out.includes(n)) out.push(n);
217
- }
218
- return out;
219
- }
220
-
221
- // ---- adjust ops --------------------------------------------------------------
222
-
223
- /** The operator's corrections, as the companion relays them. Applied to a
224
- * draft all-or-nothing: one refused op leaves the draft untouched. */
225
- export type AdjustOp =
226
- | { op: "rename"; unit: string; to: string }
227
- | { op: "add"; name: string; roots: string[]; kind?: string; note?: string }
228
- | { op: "remove"; unit: string }
229
- | { op: "merge"; units: string[]; into: string }
230
- | { op: "move"; root: string; to: string }
231
- | { op: "set_roots"; unit: string; roots: string[] }
232
- | { op: "ignore"; roots: string[] }
233
- | { op: "unignore"; roots: string[] }
234
- | { op: "note"; unit: string; note: string };
235
-
236
- export const ADJUST_OPS = [
237
- "rename",
238
- "add",
239
- "remove",
240
- "merge",
241
- "move",
242
- "set_roots",
243
- "ignore",
244
- "unignore",
245
- "note",
246
- ] as const;
247
-
248
- function clone(cut: UnitCut): UnitCut {
249
- return {
250
- units: cut.units.map((u) => ({ ...u, roots: [...u.roots] })),
251
- ignored: [...cut.ignored],
252
- };
253
- }
254
-
255
- function strings(v: unknown): string[] | null {
256
- if (typeof v === "string") return [v];
257
- return Array.isArray(v) && v.every((x) => typeof x === "string") ? v : null;
258
- }
259
-
260
- /**
261
- * Apply `ops` in order to a copy of `cut`. Returns the new cut, or every
262
- * reason it could not (unknown unit, unknown op, a root that leaves the repo).
263
- * The structural check of the RESULT is the caller's (`checkCut`) — an op
264
- * sequence may pass through an invalid intermediate state (swap two names).
265
- */
266
- export function applyOps(
267
- cut: UnitCut,
268
- ops: readonly unknown[],
269
- ): { ok: true; cut: UnitCut } | { ok: false; errors: string[] } {
270
- const next = clone(cut);
271
- const errors: string[] = [];
272
- const unit = (name: unknown, i: number): Unit | null => {
273
- const u = typeof name === "string" ? next.units.find((x) => x.name === name) : undefined;
274
- if (!u)
275
- errors.push(
276
- `ops[${i}]: no unit named ${JSON.stringify(name)} — units: ${next.units.map((x) => x.name).join(", ")}`,
277
- );
278
- return u ?? null;
279
- };
280
- const rootsOf = (v: unknown, i: number): string[] | null => {
281
- const raw = strings(v);
282
- if (raw === null) {
283
- errors.push(`ops[${i}]: roots must be a path or an array of paths`);
284
- return null;
285
- }
286
- return normalizeRoots(raw, `ops[${i}]`, errors);
287
- };
288
- const release = (roots: readonly string[]): void => {
289
- for (const u of next.units) u.roots = u.roots.filter((r) => !roots.includes(r));
290
- next.ignored = next.ignored.filter((r) => !roots.includes(r));
291
- };
292
- for (const [i, raw] of ops.entries()) {
293
- const o = (raw ?? {}) as Record<string, unknown>;
294
- switch (o.op) {
295
- case "rename": {
296
- const u = unit(o.unit, i);
297
- if (u && typeof o.to === "string") u.name = o.to.trim();
298
- else if (u) errors.push(`ops[${i}]: rename needs \`to\``);
299
- break;
300
- }
301
- case "add": {
302
- const roots = rootsOf(o.roots, i);
303
- if (typeof o.name !== "string") errors.push(`ops[${i}]: add needs \`name\``);
304
- else if (roots) {
305
- release(roots);
306
- next.units.push({
307
- name: o.name.trim(),
308
- roots,
309
- kind: unitKind(o.kind),
310
- note: typeof o.note === "string" ? o.note.trim() : "added by the operator",
311
- });
312
- }
313
- break;
314
- }
315
- case "remove": {
316
- const u = unit(o.unit, i);
317
- if (u) next.units = next.units.filter((x) => x !== u);
318
- break;
319
- }
320
- case "merge": {
321
- const names = strings(o.units);
322
- if (!names || names.length < 2 || typeof o.into !== "string") {
323
- errors.push(`ops[${i}]: merge needs \`units\` (two or more) and \`into\``);
324
- break;
325
- }
326
- const members = names.map((n) => unit(n, i));
327
- if (members.some((m) => m === null)) break;
328
- const into = o.into.trim();
329
- const keep = members.find((m) => m!.name === into) ?? members[0]!;
330
- keep.roots = [...new Set(members.flatMap((m) => m!.roots))];
331
- keep.name = into;
332
- keep.note = keep.note
333
- ? `${keep.note}; merged from ${names.join(", ")}`
334
- : `merged from ${names.join(", ")}`;
335
- next.units = next.units.filter((x) => x === keep || !members.includes(x));
336
- break;
337
- }
338
- case "move": {
339
- const roots = rootsOf(o.root, i);
340
- const u = unit(o.to, i);
341
- if (roots && u) {
342
- release(roots);
343
- u.roots.push(...roots);
344
- }
345
- break;
346
- }
347
- case "set_roots": {
348
- const u = unit(o.unit, i);
349
- const roots = rootsOf(o.roots, i);
350
- if (u && roots) {
351
- release(roots);
352
- u.roots = roots;
353
- }
354
- break;
355
- }
356
- case "ignore": {
357
- const roots = rootsOf(o.roots, i);
358
- if (roots) {
359
- release(roots);
360
- next.ignored.push(...roots);
361
- }
362
- break;
363
- }
364
- case "unignore": {
365
- const roots = rootsOf(o.roots, i);
366
- if (roots) {
367
- const missing = roots.filter((r) => !next.ignored.includes(r));
368
- if (missing.length > 0) errors.push(`ops[${i}]: not ignored: ${missing.join(", ")}`);
369
- next.ignored = next.ignored.filter((r) => !roots.includes(r));
370
- }
371
- break;
372
- }
373
- case "note": {
374
- const u = unit(o.unit, i);
375
- if (u && typeof o.note === "string") u.note = o.note.trim();
376
- else if (u) errors.push(`ops[${i}]: note needs \`note\``);
377
- break;
378
- }
379
- default:
380
- errors.push(
381
- `ops[${i}]: unknown op ${JSON.stringify(o.op)} — one of ${ADJUST_OPS.join(", ")}`,
382
- );
383
- }
384
- }
385
- return errors.length > 0 ? { ok: false, errors } : { ok: true, cut: next };
386
- }
387
-
388
- // ---- cases: an oversized unit split to fit, its name kept --------------------
389
-
390
- /** How much one case may read. The default is one PR round's sweep capacity
391
- * (`sweep_max_groups` × a sweep group, 6 × 5 files / 500 lines): a case is one
392
- * main pi fanning out leaf lenses, so it holds what a round holds. The scan's
393
- * `[scan]` config is where a repo tunes it. */
394
- export interface CaseBudget {
395
- maxFiles: number;
396
- maxLines: number;
397
- }
398
-
399
- export const DEFAULT_CASE_BUDGET: CaseBudget = {
400
- maxFiles: SCAN_DEFAULTS.case_max_files,
401
- maxLines: SCAN_DEFAULTS.case_max_lines,
402
- };
403
-
404
- export interface CasePlan {
405
- /** The UNIT's name — never the part's. Reports group by this. */
406
- unit: string;
407
- /** `<unit>` when the unit fits, `<unit>#<i>` (1-based) when it was split. */
408
- id: string;
409
- part: number;
410
- parts: number;
411
- files: string[];
412
- lines: number;
413
- /** One file alone over the budget: it is its own case here — this split
414
- * never cuts a file. The SCAN then reads such a file in line ranges, one
415
- * case per range under the unit's name (scan.ts cutRanges). */
416
- over_budget: boolean;
417
- }
418
-
419
- interface Piece {
420
- files: string[];
421
- lines: number;
422
- }
423
-
424
- /**
425
- * Split one unit's files into cases that fit `budget`: subdirectory first
426
- * (a directory that fits stays whole), then file chunks in path order; small
427
- * neighbouring pieces are packed back together so a split never produces a
428
- * case per tiny directory. Every case keeps the unit's name. Deterministic:
429
- * the same files, sizes and budget always give the same cases. No files, no
430
- * case.
431
- */
432
- export function splitUnit(
433
- unit: string,
434
- files: readonly string[],
435
- linesOf: (path: string) => number,
436
- budget: CaseBudget = DEFAULT_CASE_BUDGET,
437
- ): CasePlan[] {
438
- if (files.length === 0) return [];
439
- const sorted = [...files].sort();
440
- const size = new Map(sorted.map((f) => [f, Math.max(0, linesOf(f))]));
441
- const fits = (n: number, lines: number): boolean =>
442
- n <= budget.maxFiles && lines <= budget.maxLines;
443
- const total = (fs: readonly string[]): number => fs.reduce((n, f) => n + size.get(f)!, 0);
444
-
445
- const pieces = (fs: string[], depth: number): Piece[] => {
446
- const lines = total(fs);
447
- if (fits(fs.length, lines)) return [{ files: fs, lines }];
448
- // By the next path segment below the common prefix of this depth.
449
- const groups = new Map<string, string[]>();
450
- const loose: string[] = [];
451
- for (const f of fs) {
452
- const segs = f.split("/");
453
- if (segs.length - 1 > depth) {
454
- const key = segs.slice(0, depth + 1).join("/");
455
- if (!groups.has(key)) groups.set(key, []);
456
- groups.get(key)!.push(f);
457
- } else {
458
- loose.push(f);
459
- }
460
- }
461
- // One directory holding everything: descend without spending a split.
462
- if (loose.length === 0 && groups.size === 1) return pieces(fs, depth + 1);
463
- const out: Piece[] = [];
464
- for (const key of [...groups.keys()].sort()) out.push(...pieces(groups.get(key)!, depth + 1));
465
- out.push(...chunk(loose));
466
- return out;
467
- };
468
-
469
- const chunk = (fs: readonly string[]): Piece[] => {
470
- const out: Piece[] = [];
471
- let cur: Piece = { files: [], lines: 0 };
472
- for (const f of fs) {
473
- const n = size.get(f)!;
474
- if (cur.files.length > 0 && !fits(cur.files.length + 1, cur.lines + n)) {
475
- out.push(cur);
476
- cur = { files: [], lines: 0 };
477
- }
478
- cur.files.push(f);
479
- cur.lines += n;
480
- }
481
- if (cur.files.length > 0) out.push(cur);
482
- return out;
483
- };
484
-
485
- // Pack neighbours: a directory that fit on its own may share a case with
486
- // the next one when both still fit together.
487
- const packed: Piece[] = [];
488
- for (const p of pieces(sorted, commonDepth(sorted))) {
489
- const last = packed[packed.length - 1];
490
- if (last && fits(last.files.length + p.files.length, last.lines + p.lines)) {
491
- last.files.push(...p.files);
492
- last.lines += p.lines;
493
- } else {
494
- packed.push({ files: [...p.files], lines: p.lines });
495
- }
496
- }
497
- const parts = packed.length;
498
- return packed.map((p, i) => ({
499
- unit,
500
- id: parts === 1 ? unit : `${unit}#${i + 1}`,
501
- part: i + 1,
502
- parts,
503
- files: p.files,
504
- lines: p.lines,
505
- over_budget: !fits(p.files.length, p.lines),
506
- }));
507
- }
508
-
509
- /** Depth of the directory every file shares, so a unit rooted at
510
- * `backend/payments` splits by what is BELOW it rather than re-grouping on
511
- * `backend`. */
512
- function commonDepth(files: readonly string[]): number {
513
- if (files.length === 0) return 0;
514
- const first = files[0]!.split("/").slice(0, -1);
515
- let depth = first.length;
516
- for (const f of files) {
517
- const segs = f.split("/").slice(0, -1);
518
- let d = 0;
519
- while (d < depth && d < segs.length && segs[d] === first[d]) d++;
520
- depth = d;
521
- }
522
- return depth;
523
- }
1
+ // Moved to the plugin (packages/pi-plugin/src/scan/unitcut.ts), the scan engine's
2
+ // home: this name stays for the app's importers.
3
+ export * from "@quickchatai/pi-quickreview/src/scan/unitcut.js";
package/src/state.ts CHANGED
@@ -7781,9 +7781,27 @@ export interface OpenPrsSnapshot {
7781
7781
  prs: OpenPrSummary[];
7782
7782
  }
7783
7783
 
7784
+ /** The authoritative reconcile poll's last pass (status.json `poll`): what
7785
+ * "auto-review is working" is judged on, beside the heartbeat (the home's
7786
+ * label, src/home/autoreview.ts). */
7787
+ export interface PollStatus {
7788
+ /** When the last pass ran, ISO; null before the boot's first pass has ended. */
7789
+ last_at: string | null;
7790
+ /** That pass ran with no step error; null before the first pass. */
7791
+ ok: boolean | null;
7792
+ /** The first error, while not ok. */
7793
+ error?: string;
7794
+ /** The daemon's `[daemon] poll_interval_s`: freshness is judged against
7795
+ * the daemon's own cadence, never a reader's guess. */
7796
+ interval_s: number;
7797
+ }
7798
+
7784
7799
  export interface StatusSnapshot {
7785
7800
  written_at: string;
7786
7801
  dry_run: boolean;
7802
+ /** The reconcile poll's last pass. Absent in snapshots from an older
7803
+ * daemon — readers say "unknown", never "working". */
7804
+ poll?: PollStatus;
7787
7805
  /** Where the daemon runs (src/runhost.ts). Absent: not reported (an older
7788
7806
  * daemon, or one assembled without its host facts). */
7789
7807
  run_host?: import("./runhost.js").RunHostStatus;
@@ -7874,6 +7892,11 @@ export interface StatusSnapshot {
7874
7892
  max_concurrent_rounds?: number;
7875
7893
  total_spend_usd: number;
7876
7894
  today_spend_usd?: number;
7895
+ /** The month's spend so far: every round started in the snapshot's UTC
7896
+ * month (the same rule as `today_spend_usd`, expired rounds included).
7897
+ * Absent in snapshots from an older daemon: the home's spend line then
7898
+ * reads unknown, never a zero. */
7899
+ month_spend_usd?: number;
7877
7900
  digest_headline?: string;
7878
7901
  /** Present while pi sessions are failing API calls with token-expired
7879
7902
  * errors (packages/pi-plugin/src/authwatch.ts, fresh < 30 min). The TUI raises an alert. */
@@ -8224,6 +8247,8 @@ export function buildStatusSnapshot(
8224
8247
  ...(maxConcurrentRounds !== undefined ? { max_concurrent_rounds: maxConcurrentRounds } : {}),
8225
8248
  total_spend_usd: store.totalSpend(),
8226
8249
  today_spend_usd: roundUsage(store.roundsOn(today)).spend_usd,
8250
+ // roundsOn matches by prefix: the month's `YYYY-MM` is every day of it
8251
+ month_spend_usd: roundUsage(store.roundsOn(today.slice(0, 7))).spend_usd,
8227
8252
  ...(digestHeadline ? { digest_headline: digestHeadline } : {}),
8228
8253
  ...(piAuth
8229
8254
  ? {
package/src/takeover.ts CHANGED
@@ -82,6 +82,9 @@ export interface TakeoverPlan {
82
82
  cwd: string;
83
83
  /** The login's owner dir (`pihome/agent`): read-only, borrowed from. */
84
84
  agent_dir: string;
85
+ /** The login's dir when it is not `agent_dir` (the single box's shared
86
+ * login): the login is borrowed from here, the rest from `agent_dir`. */
87
+ login_dir?: string;
85
88
  /** The PR's JSONL, which the pi resumes and appends to. */
86
89
  session_file: string;
87
90
  model: string;
@@ -597,6 +600,7 @@ function rmQuiet(file: string): void {
597
600
  export function takeoverPlanFor(o: {
598
601
  root: string;
599
602
  agentDir: string;
603
+ loginDir?: string;
600
604
  sessionsRoot: string;
601
605
  model: string;
602
606
  pr: number;
@@ -612,6 +616,7 @@ export function takeoverPlanFor(o: {
612
616
  root: o.root,
613
617
  cwd: o.root,
614
618
  agent_dir: o.agentDir,
619
+ ...(o.loginDir !== undefined && o.loginDir !== o.agentDir ? { login_dir: o.loginDir } : {}),
615
620
  session_file: file,
616
621
  model: o.model,
617
622
  ...(o.systemPromptFile ? { system_prompt_file: o.systemPromptFile } : {}),
package/src/takeoverpi.ts CHANGED
@@ -91,6 +91,7 @@ export function readTakeoverSpec(file: string): TakeoverSpec {
91
91
  raw.session !== `takeover-pr-${raw.pr}` ||
92
92
  typeof raw.session_file !== "string" ||
93
93
  typeof raw.agent_dir !== "string" ||
94
+ (raw.login_dir !== undefined && typeof raw.login_dir !== "string") ||
94
95
  typeof raw.borrow_dir !== "string" ||
95
96
  typeof raw.model !== "string" ||
96
97
  typeof raw.cwd !== "string" ||
@@ -119,11 +120,11 @@ export function readLogin(agentDir: string): string | null {
119
120
  * the owner's dir.
120
121
  */
121
122
  export function prepareBorrowedAgent(
122
- spec: Pick<TakeoverSpec, "agent_dir" | "borrow_dir" | "idle_ms">,
123
+ spec: Pick<TakeoverSpec, "agent_dir" | "login_dir" | "borrow_dir" | "idle_ms">,
123
124
  now?: () => number,
124
125
  ): { expiresAt: number | null } {
125
126
  const { expiresAt } = copyPiAuth({
126
- srcDir: spec.agent_dir,
127
+ srcDir: spec.login_dir ?? spec.agent_dir,
127
128
  dstDir: spec.borrow_dir,
128
129
  needMs: spec.idle_ms + PI_REFRESH_MARGIN_MS,
129
130
  words: ENGINE_AUTH_WORDS,
package/src/tui/board.ts CHANGED
@@ -522,7 +522,7 @@ function barSegments(view: BoardView, alerts: boolean): string[] {
522
522
  }
523
523
  // Short on the bar, where every segment competes for the width; the list
524
524
  // carries the full sentence with the next step.
525
- const setupSeg = view.setup ? `${WARN}▲ setup not finished — qcr onboard${DRESET}` : "";
525
+ const setupSeg = view.setup ? `${WARN}▲ setup not finished${DRESET}` : "";
526
526
  const s = view.snapshot;
527
527
  const rounds = s ? `rounds ${s.running_rounds}/${s.max_concurrent_rounds ?? "?"}` : "";
528
528
  const spend = s?.today_spend_usd !== undefined ? `today $${s.today_spend_usd.toFixed(2)}` : "";
@@ -45,6 +45,7 @@ import { COMPANION_IMAGE, type CompanionConfig } from "../config.js";
45
45
  import { writeAtomic } from "../channelfs.js";
46
46
  import { appStamp } from "../appstamp.js";
47
47
  import { TOKEN_FILE } from "./ghread.js";
48
+ import { MEMORY_PORT } from "./memoryurl.js";
48
49
 
49
50
  export interface DockerResult {
50
51
  ok: boolean;
@@ -274,7 +275,7 @@ export const MEMORY = {
274
275
  subnet: "198.51.100.0/24",
275
276
  gateway: "198.51.100.1",
276
277
  /** Same port the server listens on in the VM container ([openviking] port). */
277
- port: 1933,
278
+ port: MEMORY_PORT,
278
279
  /** The OpenViking user whose memory the companion owns — becomes the
279
280
  * `X-OpenViking-User` header on every request it makes. */
280
281
  user: "companion",
@@ -1,6 +1,6 @@
1
1
  // `[5] deploy` — "what would a deploy ship?" plus the deploy button.
2
- // Every deploy is a RELEASE: the tag, the two-arch build, the paid e2e and the
3
- // owner's approval already happened in CI, so the tab only chooses a version.
2
+ // Every deploy is a RELEASE: the tag and the two-arch build with its smoke
3
+ // already happened in CI, so the tab only chooses a version.
4
4
  // It shows the version and revision the VM runs (`qcr-update status --json`,
5
5
  // read over ssh) against the chosen version, the commits between the two
6
6
  // revisions (from the local checkout), and deploy.sh's own