@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
@@ -57,7 +57,12 @@ import {
57
57
  } from "@quickchatai/pi-quickreview/extensions/pi-tool.js";
58
58
  import { fileURLToPath } from "node:url";
59
59
  import { configHome, stateHome } from "../src/install.js";
60
- import { SetupEngine, type CutStatus, type CutView } from "../src/setup/engine.js";
60
+ import { SetupEngine } from "../src/setup/engine.js";
61
+ import {
62
+ renderCut,
63
+ renderDraft,
64
+ renderStatus,
65
+ } from "@quickchatai/pi-quickreview/src/scan/cutrender.js";
61
66
  import type { HistoryTheme, HistoryThemesFile } from "../src/setup/history.js";
62
67
  import {
63
68
  HISTORY_SECTIONS,
@@ -154,75 +159,14 @@ export function renderFacts(facts: RepoFacts, head: string | null): string {
154
159
  ].join("\n\n");
155
160
  }
156
161
 
157
- export function renderCut(title: string, v: CutView): string {
158
- const rows = v.units.map((u) => {
159
- const split =
160
- u.cases === null
161
- ? " — case coverage UNKNOWN"
162
- : u.cases.length > 1
163
- ? ` — split into ${u.cases.length} cases (${u.cases.map((c) => `${c.id}: ${c.files}f/${c.lines}l`).join(", ")})`
164
- : "";
165
- const over = u.cases?.some((c) => c.over_budget)
166
- ? " [a file alone exceeds one case — a scan reads it in line ranges]"
167
- : "";
168
- return ` - ${u.name} [${u.kind}] ${u.roots.map(displayRoot).join(", ")}: ${u.files ?? "UNKNOWN"} files, ${u.reviewable ?? "UNKNOWN"} reviewable, ${u.lines ?? "UNKNOWN"} lines${split}${over}${u.note ? ` — ${u.note}` : ""}`;
169
- });
170
- const lines = [`${title}:`, ...rows];
171
- if (v.ignored.length > 0)
172
- lines.push(` ignored: ${v.ignored.join(", ")} (${v.ignored_files ?? "UNKNOWN"} files)`);
173
- for (const e of v.errors) lines.push(` ✗ ${e}`);
174
- for (const b of v.blocking_save) lines.push(` ! ${b}`);
175
- return lines.join("\n");
176
- }
177
-
178
- /** How a rendering names the save that makes a draft the cut — the tool, or
179
- * the `qcr scan units` verb. */
180
- export interface CutVerbs {
181
- save: string;
182
- }
183
- export const CUT_TOOL_VERBS: CutVerbs = { save: "setup_unit_cut_save" };
184
- export const CUT_CLI_VERBS: CutVerbs = { save: "qcr scan units save" };
185
-
186
- /** A stored or adjusted draft, and that it is not in use until saved. */
187
- export function renderDraft(
188
- title: string,
189
- draft: CutView,
190
- verbs: CutVerbs = CUT_TOOL_VERBS,
191
- ): string {
192
- return `${renderCut(title, draft)}\n\nNot in use until ${verbs.save}.`;
193
- }
194
-
195
- export function renderStatus(s: CutStatus): string {
196
- const out: string[] = [];
197
- for (const p of s.problems) out.push(`✗ ${p}`);
198
- if (s.saved) {
199
- const stale =
200
- s.saved.blocking_save.length > 0 ? " — STALE against the repo now, see the ! lines" : "";
201
- out.push(
202
- renderCut(
203
- `Saved unit cut (saved ${s.saved.saved_at}${s.saved.head ? ` at ${s.saved.head.slice(0, 12)}` : ""})${stale}`,
204
- s.saved,
205
- ),
206
- );
207
- } else if (s.cut_file_unreadable) {
208
- out.push(
209
- "Saved unit cut: UNKNOWN — the cut file could not be read (see above); nothing will overwrite it.",
210
- );
211
- } else {
212
- out.push("No saved unit cut yet.");
213
- }
214
- if (s.draft)
215
- out.push(
216
- renderCut(
217
- `Draft (${s.draft.source}, updated ${s.draft.updated_at}) — not in use until saved`,
218
- s.draft,
219
- ),
220
- );
221
- if (s.has_previous)
222
- out.push("The cut before the saved one is kept as `previous` in the state home.");
223
- return out.join("\n\n");
224
- }
225
-
162
+ export {
163
+ CUT_CLI_VERBS,
164
+ CUT_TOOL_VERBS,
165
+ renderCut,
166
+ renderDraft,
167
+ renderStatus,
168
+ type CutVerbs,
169
+ } from "@quickchatai/pi-quickreview/src/scan/cutrender.js";
226
170
  export function renderThemes(themes: readonly HistoryTheme[]): string {
227
171
  return themes.length === 0
228
172
  ? " (none — an empty history has no themes)"
@@ -60,6 +60,11 @@ Changed by the quickreviewd maintainers, 2026-08-20:
60
60
  12. The extension is a factory over a configuration value; the process's
61
61
  environment, argv, working directory and home directory are read once, by
62
62
  a shim, and every hook runs under a deadline and a catch (2026-10-19).
63
+ 13. One turn sync runs at a time per session: a turn that ends while one
64
+ runs queues its branch, the running sync sends the queued branches in
65
+ order, each entry sent once by its pi entry id, and agent_end waits for
66
+ them under turn_end's deadline, so a hook deadline can no longer start a
67
+ second sync that re-sends the same messages (2026-10-29).
63
68
 
64
69
  Plus vendoring scaffolding: build-conformance fixes for this repo's stricter
65
70
  tsconfig (ambient type declarations while pi was not installed, retired once
@@ -1,7 +1,7 @@
1
1
  # openviking-pi — vendored, patched fork
2
2
 
3
3
  The OpenViking pi extension our sessions load. Vendored, not depended on:
4
- upstream ships it as an `examples/` directory, not a package, and we need twelve
4
+ upstream ships it as an `examples/` directory, not a package, and we need thirteen
5
5
  behaviour changes that upstream has no reason to make.
6
6
 
7
7
  | | |
@@ -20,7 +20,7 @@ fails on drift between the two.
20
20
 
21
21
  ## Why a fork rather than configuration
22
22
 
23
- Every one of the twelve patches was tried as configuration first and does not
23
+ Every one of the thirteen patches was tried as configuration first and does not
24
24
  exist there. The evidence is a spike against a **running v0.4.15 server plus
25
25
  the pinned pi 0.84.1**, with transcripts, kept with the project's design
26
26
  records. The short version: several of its findings contradict the OpenViking
@@ -29,12 +29,13 @@ anything here.
29
29
 
30
30
  ## The patch series
31
31
 
32
- Twelve numbered behaviour patches, applied in order over the vendored upstream
32
+ Thirteen numbered behaviour patches, applied in order over the vendored upstream
33
33
  files, plus two unnumbered build patches (0000 scaffolding, 0012 typing). They live in `patches/` as
34
34
  `git apply`-able diffs — verified by applying the first thirteen, in order, to a
35
35
  pristine `v0.4.15` extraction and diffing the result against this directory:
36
- byte-identical. (0013 is checked to apply on the tree the first thirteen
37
- produce; the full pristine-extraction replay has not been re-run with it.) (`patches/` and `upstream.json` are meta and are not themselves
36
+ byte-identical. (0013 and 0014 are checked to apply on the tree the patches
37
+ before them produce; the full pristine-extraction replay has not been re-run
38
+ with them.) (`patches/` and `upstream.json` are meta and are not themselves
38
39
  part of the reproduced tree.) Every hunk they add carries a greppable marker:
39
40
 
40
41
  ```sh
@@ -488,6 +489,39 @@ Touches: `index.ts`, `config.ts`, `sync.ts`, `qcr-leaf.ts`, `qcr-shim.ts`
488
489
  (new), `qcr-hooks.ts` (new), `shared/pending-queue.mjs` and `.d.mts`, `NOTICE`,
489
490
  this README.
490
491
 
492
+ ### 0014 — one turn sync in flight per session (`[QCR PATCH 13]`)
493
+
494
+ The `turn_end` hook runs under a deadline (`qcr-hooks.ts`), and a deadline
495
+ that passes returns without cancelling the hook's work. Upstream's
496
+ `syncBranch` then ran on in the background, and under a slow server the next
497
+ turn started a second sync from the same watermark — which only advances once
498
+ a whole sync has finished. One process sent the same messages to one OV
499
+ session several times at once; the server takes a tree lock per session
500
+ append, so they queued on it, timed out client-side (and were queued for
501
+ replay), and the health probes queued behind them. On the bot it was measured
502
+ at up to five appends in flight to one session from its one worker.
503
+
504
+ `SyncManager.syncBranch` now runs one sync at a time. A turn that ends while
505
+ one runs queues its branch, in memory and in order; the running sync then
506
+ sends each queued branch in turn, so the process sends nothing twice and
507
+ nothing at once, and a later turn (a branch switch included) never overwrites
508
+ an earlier one. Upstream's watermark is a count of entries, so a queued branch
509
+ as long as the one before it would read as sent; what was sent is now
510
+ remembered by pi's entry ids, and only entries without ids fall back to the
511
+ count. The offline queue on disk is
512
+ not used for this: it is shared by every worker, it collapses identical
513
+ payloads and replays by write time, all of which would reorder or drop turns.
514
+
515
+ A bot worker disposes its session without `session_shutdown`, so the queued
516
+ turns must go out before the prompt resolves: `agent_end` awaits
517
+ `SyncManager.drain()`, and its deadline in `qcr-hooks.ts` is raised from 1 s
518
+ to turn_end's 30 s. With nothing queued, `drain()` settles at once; with a
519
+ slow server the prompt can resolve up to 30 s later than before. Turns still
520
+ queued when that deadline passes are lost with the worker, as a running sync
521
+ was before this patch; the pi transcript keeps them.
522
+
523
+ Touches: `sync.ts`, `index.ts`, `qcr-hooks.ts`, `NOTICE`, this README.
524
+
491
525
  ## Rebasing onto a newer upstream
492
526
 
493
527
  ```sh
@@ -405,6 +405,10 @@ async function register(pi: ExtensionAPI, cfg: OpenVikingConfig): Promise<void>
405
405
  // --- agent_end ---
406
406
  pi.on("agent_end", guard("agent_end", async (_event, _ctx) => {
407
407
  recall.invalidate();
408
+ // [QCR PATCH 13] The turns queued behind a slow sync go out before the
409
+ // prompt resolves, under this hook's deadline: a worker disposes its
410
+ // session without session_shutdown, so this is the last chance.
411
+ await sync.drain();
408
412
  }));
409
413
 
410
414
  // ================================================================
@@ -0,0 +1,239 @@
1
+ diff --git a/extensions/vendor/openviking-pi/NOTICE b/extensions/vendor/openviking-pi/NOTICE
2
+ index 15992e4a..1afc0e01 100644
3
+ --- a/extensions/vendor/openviking-pi/NOTICE
4
+ +++ b/extensions/vendor/openviking-pi/NOTICE
5
+ @@ -60,6 +60,11 @@ Changed by the quickreviewd maintainers, 2026-08-20:
6
+ 12. The extension is a factory over a configuration value; the process's
7
+ environment, argv, working directory and home directory are read once, by
8
+ a shim, and every hook runs under a deadline and a catch (2026-10-19).
9
+ + 13. One turn sync runs at a time per session: a turn that ends while one
10
+ + runs queues its branch, the running sync sends the queued branches in
11
+ + order, each entry sent once by its pi entry id, and agent_end waits for
12
+ + them under turn_end's deadline, so a hook deadline can no longer start a
13
+ + second sync that re-sends the same messages (2026-10-29).
14
+
15
+ Plus vendoring scaffolding: build-conformance fixes for this repo's stricter
16
+ tsconfig (ambient type declarations while pi was not installed, retired once
17
+ diff --git a/extensions/vendor/openviking-pi/README.md b/extensions/vendor/openviking-pi/README.md
18
+ index 670e38b2..ca289c2d 100644
19
+ --- a/extensions/vendor/openviking-pi/README.md
20
+ +++ b/extensions/vendor/openviking-pi/README.md
21
+ @@ -1,7 +1,7 @@
22
+ # openviking-pi — vendored, patched fork
23
+
24
+ The OpenViking pi extension our sessions load. Vendored, not depended on:
25
+ -upstream ships it as an `examples/` directory, not a package, and we need twelve
26
+ +upstream ships it as an `examples/` directory, not a package, and we need thirteen
27
+ behaviour changes that upstream has no reason to make.
28
+
29
+ | | |
30
+ @@ -20,7 +20,7 @@ fails on drift between the two.
31
+
32
+ ## Why a fork rather than configuration
33
+
34
+ -Every one of the twelve patches was tried as configuration first and does not
35
+ +Every one of the thirteen patches was tried as configuration first and does not
36
+ exist there. The evidence is a spike against a **running v0.4.15 server plus
37
+ the pinned pi 0.84.1**, with transcripts, kept with the project's design
38
+ records. The short version: several of its findings contradict the OpenViking
39
+ @@ -29,12 +29,13 @@ anything here.
40
+
41
+ ## The patch series
42
+
43
+ -Twelve numbered behaviour patches, applied in order over the vendored upstream
44
+ +Thirteen numbered behaviour patches, applied in order over the vendored upstream
45
+ files, plus two unnumbered build patches (0000 scaffolding, 0012 typing). They live in `patches/` as
46
+ `git apply`-able diffs — verified by applying the first thirteen, in order, to a
47
+ pristine `v0.4.15` extraction and diffing the result against this directory:
48
+ -byte-identical. (0013 is checked to apply on the tree the first thirteen
49
+ -produce; the full pristine-extraction replay has not been re-run with it.) (`patches/` and `upstream.json` are meta and are not themselves
50
+ +byte-identical. (0013 and 0014 are checked to apply on the tree the patches
51
+ +before them produce; the full pristine-extraction replay has not been re-run
52
+ +with them.) (`patches/` and `upstream.json` are meta and are not themselves
53
+ part of the reproduced tree.) Every hunk they add carries a greppable marker:
54
+
55
+ ```sh
56
+ @@ -488,6 +489,39 @@ Touches: `index.ts`, `config.ts`, `sync.ts`, `qcr-leaf.ts`, `qcr-shim.ts`
57
+ (new), `qcr-hooks.ts` (new), `shared/pending-queue.mjs` and `.d.mts`, `NOTICE`,
58
+ this README.
59
+
60
+ +### 0014 — one turn sync in flight per session (`[QCR PATCH 13]`)
61
+ +
62
+ +The `turn_end` hook runs under a deadline (`qcr-hooks.ts`), and a deadline
63
+ +that passes returns without cancelling the hook's work. Upstream's
64
+ +`syncBranch` then ran on in the background, and under a slow server the next
65
+ +turn started a second sync from the same watermark — which only advances once
66
+ +a whole sync has finished. One process sent the same messages to one OV
67
+ +session several times at once; the server takes a tree lock per session
68
+ +append, so they queued on it, timed out client-side (and were queued for
69
+ +replay), and the health probes queued behind them. On the bot it was measured
70
+ +at up to five appends in flight to one session from its one worker.
71
+ +
72
+ +`SyncManager.syncBranch` now runs one sync at a time. A turn that ends while
73
+ +one runs queues its branch, in memory and in order; the running sync then
74
+ +sends each queued branch in turn, so the process sends nothing twice and
75
+ +nothing at once, and a later turn (a branch switch included) never overwrites
76
+ +an earlier one. Upstream's watermark is a count of entries, so a queued branch
77
+ +as long as the one before it would read as sent; what was sent is now
78
+ +remembered by pi's entry ids, and only entries without ids fall back to the
79
+ +count. The offline queue on disk is
80
+ +not used for this: it is shared by every worker, it collapses identical
81
+ +payloads and replays by write time, all of which would reorder or drop turns.
82
+ +
83
+ +A bot worker disposes its session without `session_shutdown`, so the queued
84
+ +turns must go out before the prompt resolves: `agent_end` awaits
85
+ +`SyncManager.drain()`, and its deadline in `qcr-hooks.ts` is raised from 1 s
86
+ +to turn_end's 30 s. With nothing queued, `drain()` settles at once; with a
87
+ +slow server the prompt can resolve up to 30 s later than before. Turns still
88
+ +queued when that deadline passes are lost with the worker, as a running sync
89
+ +was before this patch; the pi transcript keeps them.
90
+ +
91
+ +Touches: `sync.ts`, `index.ts`, `qcr-hooks.ts`, `NOTICE`, this README.
92
+ +
93
+ ## Rebasing onto a newer upstream
94
+
95
+ ```sh
96
+ diff --git a/extensions/vendor/openviking-pi/index.ts b/extensions/vendor/openviking-pi/index.ts
97
+ index 8bdc65d9..1b60eeee 100644
98
+ --- a/extensions/vendor/openviking-pi/index.ts
99
+ +++ b/extensions/vendor/openviking-pi/index.ts
100
+ @@ -405,6 +405,10 @@ async function register(pi: ExtensionAPI, cfg: OpenVikingConfig): Promise<void>
101
+ // --- agent_end ---
102
+ pi.on("agent_end", guard("agent_end", async (_event, _ctx) => {
103
+ recall.invalidate();
104
+ + // [QCR PATCH 13] The turns queued behind a slow sync go out before the
105
+ + // prompt resolves, under this hook's deadline: a worker disposes its
106
+ + // session without session_shutdown, so this is the last chance.
107
+ + await sync.drain();
108
+ }));
109
+
110
+ // ================================================================
111
+ diff --git a/extensions/vendor/openviking-pi/qcr-hooks.ts b/extensions/vendor/openviking-pi/qcr-hooks.ts
112
+ index 8979e729..89d8281c 100644
113
+ --- a/extensions/vendor/openviking-pi/qcr-hooks.ts
114
+ +++ b/extensions/vendor/openviking-pi/qcr-hooks.ts
115
+ @@ -32,7 +32,9 @@ export const HOOK_TIMEOUT_MS: Readonly<Record<HookName, number>> = {
116
+ tool_call: 2_000,
117
+ turn_end: 30_000,
118
+ session_shutdown: 5_000,
119
+ - agent_end: 1_000,
120
+ + // [QCR PATCH 13] agent_end drains the turn syncs queued behind a slow one,
121
+ + // so it gets turn_end's deadline; with nothing queued it returns at once.
122
+ + agent_end: 30_000,
123
+ command: 15_000,
124
+ };
125
+
126
+ diff --git a/extensions/vendor/openviking-pi/sync.ts b/extensions/vendor/openviking-pi/sync.ts
127
+ index 037d6306..ddfe651e 100644
128
+ --- a/extensions/vendor/openviking-pi/sync.ts
129
+ +++ b/extensions/vendor/openviking-pi/sync.ts
130
+ @@ -38,6 +38,14 @@ export class SyncManager {
131
+ private syncedEntryCount = 0;
132
+ private queue: PendingQueueConfig;
133
+ private debugLog: (message: string) => void;
134
+ + // [QCR PATCH 13] One turn sync in flight per session. A turn that ends
135
+ + // while one runs queues its branch here, in order; the running sync sends
136
+ + // each in turn. `idle` settles when the queue is empty again.
137
+ + private syncing = false;
138
+ + private waiting: any[][] = [];
139
+ + private idle: Promise<void> = Promise.resolve();
140
+ + // [QCR PATCH 13] The ids of the entries already sent (see syncOnce).
141
+ + private sentIds = new Set<string>();
142
+
143
+ constructor(client: OVClient, config: OVConfig, opts: SyncOptions = {}) {
144
+ this.client = client;
145
+ @@ -52,6 +60,7 @@ export class SyncManager {
146
+ restoreWatermark(n: number): void {
147
+ const next = Math.max(0, Math.floor(Number(n) || 0));
148
+ this.syncedEntryCount = next;
149
+ + this.sentIds.clear(); // [QCR PATCH 13] the next sync re-seeds from `next`
150
+ }
151
+
152
+ /**
153
+ @@ -110,8 +119,54 @@ export class SyncManager {
154
+ return countUndeliveredForSession(pending, this.ovSessionId) === 0;
155
+ }
156
+
157
+ + /**
158
+ + * [QCR PATCH 13] The turn_end hook's deadline returns without cancelling
159
+ + * this sync, so under a slow server the next turn used to start a second one
160
+ + * from the same watermark: the same messages went to one session several
161
+ + * times at once and queued on its lock. Now one sync runs at a time. A turn
162
+ + * that ends while it runs queues its branch, and the running sync sends the
163
+ + * queued branches one after another, each from the watermark the one before
164
+ + * it left: nothing is sent twice or at once, and no turn is overwritten by a
165
+ + * later one. `drain()` (awaited at agent_end) waits for the queue to empty.
166
+ + */
167
+ async syncBranch(branch: any[]): Promise<SyncBranchResult> {
168
+ if (!this.ovSessionId) return { added: 0, tokens: 0, allDelivered: true };
169
+ + if (this.syncing) {
170
+ + this.waiting.push(branch);
171
+ + return { added: 0, tokens: 0, allDelivered: false };
172
+ + }
173
+ + this.syncing = true;
174
+ + let done!: () => void;
175
+ + this.idle = new Promise<void>((resolve) => (done = resolve));
176
+ + try {
177
+ + const total = await this.syncOnce(branch);
178
+ + for (let next = this.waiting.shift(); next; next = this.waiting.shift()) {
179
+ + const r = await this.syncOnce(next);
180
+ + total.added += r.added;
181
+ + total.tokens += r.tokens;
182
+ + total.allDelivered = total.allDelivered && r.allDelivered;
183
+ + }
184
+ + return total;
185
+ + } finally {
186
+ + this.syncing = false;
187
+ + done();
188
+ + }
189
+ + }
190
+ +
191
+ + /** [QCR PATCH 13] Settles once no sync runs and no turn waits for one. */
192
+ + drain(): Promise<void> {
193
+ + return this.idle;
194
+ + }
195
+ +
196
+ + private async syncOnce(branch: any[]): Promise<SyncBranchResult> {
197
+ + // [QCR PATCH 13] A queued branch can be as long as the one sent before it
198
+ + // and share none of its entries (a branch switch), so a length watermark
199
+ + // would count its entries as sent. pi gives every entry an id: what was
200
+ + // sent is remembered by id. The watermark below stays for entries without.
201
+ + const ids = branch.map((entry) => entry?.id);
202
+ + if (branch.length > 0 && ids.every((id) => typeof id === "string")) {
203
+ + return this.syncById(branch, ids);
204
+ + }
205
+
206
+ const extracted = extractBranchCapturePayloads(branch, this.syncedEntryCount, this.config);
207
+ if (extracted.resetWatermark) this.syncedEntryCount = 0;
208
+ @@ -135,6 +190,31 @@ export class SyncManager {
209
+ return { added, tokens, allDelivered };
210
+ }
211
+
212
+ + /** [QCR PATCH 13] Sends each entry of `branch` not sent before, in order. */
213
+ + private async syncById(branch: any[], ids: string[]): Promise<SyncBranchResult> {
214
+ + // A restored watermark counts the branch's first entries as sent.
215
+ + if (this.sentIds.size === 0) {
216
+ + for (const id of ids.slice(0, this.syncedEntryCount)) this.sentIds.add(id);
217
+ + }
218
+ + let added = 0;
219
+ + let tokens = 0;
220
+ + let allDelivered = true;
221
+ + for (const [i, entry] of branch.entries()) {
222
+ + const id = ids[i]!;
223
+ + if (this.sentIds.has(id)) continue;
224
+ + for (const payload of extractBranchCapturePayloads([entry], 0, this.config).payloads) {
225
+ + const result = await this.addPayload(payload);
226
+ + if (!result.accepted) return { added, tokens, allDelivered };
227
+ + added++;
228
+ + tokens += estimatePayloadTokens(payload);
229
+ + allDelivered = allDelivered && result.delivered;
230
+ + }
231
+ + this.sentIds.add(id);
232
+ + }
233
+ + this.syncedEntryCount = branch.length;
234
+ + return { added, tokens, allDelivered };
235
+ + }
236
+ +
237
+ async addPayload(payload: any): Promise<AddPayloadResult> {
238
+ if (!this.ovSessionId) return { accepted: false, delivered: false };
239
+ const ok = await this.client.addMessagePayload(this.ovSessionId, payload);
@@ -32,7 +32,9 @@ export const HOOK_TIMEOUT_MS: Readonly<Record<HookName, number>> = {
32
32
  tool_call: 2_000,
33
33
  turn_end: 30_000,
34
34
  session_shutdown: 5_000,
35
- agent_end: 1_000,
35
+ // [QCR PATCH 13] agent_end drains the turn syncs queued behind a slow one,
36
+ // so it gets turn_end's deadline; with nothing queued it returns at once.
37
+ agent_end: 30_000,
36
38
  command: 15_000,
37
39
  };
38
40
 
@@ -38,6 +38,14 @@ export class SyncManager {
38
38
  private syncedEntryCount = 0;
39
39
  private queue: PendingQueueConfig;
40
40
  private debugLog: (message: string) => void;
41
+ // [QCR PATCH 13] One turn sync in flight per session. A turn that ends
42
+ // while one runs queues its branch here, in order; the running sync sends
43
+ // each in turn. `idle` settles when the queue is empty again.
44
+ private syncing = false;
45
+ private waiting: any[][] = [];
46
+ private idle: Promise<void> = Promise.resolve();
47
+ // [QCR PATCH 13] The ids of the entries already sent (see syncOnce).
48
+ private sentIds = new Set<string>();
41
49
 
42
50
  constructor(client: OVClient, config: OVConfig, opts: SyncOptions = {}) {
43
51
  this.client = client;
@@ -52,6 +60,7 @@ export class SyncManager {
52
60
  restoreWatermark(n: number): void {
53
61
  const next = Math.max(0, Math.floor(Number(n) || 0));
54
62
  this.syncedEntryCount = next;
63
+ this.sentIds.clear(); // [QCR PATCH 13] the next sync re-seeds from `next`
55
64
  }
56
65
 
57
66
  /**
@@ -110,8 +119,54 @@ export class SyncManager {
110
119
  return countUndeliveredForSession(pending, this.ovSessionId) === 0;
111
120
  }
112
121
 
122
+ /**
123
+ * [QCR PATCH 13] The turn_end hook's deadline returns without cancelling
124
+ * this sync, so under a slow server the next turn used to start a second one
125
+ * from the same watermark: the same messages went to one session several
126
+ * times at once and queued on its lock. Now one sync runs at a time. A turn
127
+ * that ends while it runs queues its branch, and the running sync sends the
128
+ * queued branches one after another, each from the watermark the one before
129
+ * it left: nothing is sent twice or at once, and no turn is overwritten by a
130
+ * later one. `drain()` (awaited at agent_end) waits for the queue to empty.
131
+ */
113
132
  async syncBranch(branch: any[]): Promise<SyncBranchResult> {
114
133
  if (!this.ovSessionId) return { added: 0, tokens: 0, allDelivered: true };
134
+ if (this.syncing) {
135
+ this.waiting.push(branch);
136
+ return { added: 0, tokens: 0, allDelivered: false };
137
+ }
138
+ this.syncing = true;
139
+ let done!: () => void;
140
+ this.idle = new Promise<void>((resolve) => (done = resolve));
141
+ try {
142
+ const total = await this.syncOnce(branch);
143
+ for (let next = this.waiting.shift(); next; next = this.waiting.shift()) {
144
+ const r = await this.syncOnce(next);
145
+ total.added += r.added;
146
+ total.tokens += r.tokens;
147
+ total.allDelivered = total.allDelivered && r.allDelivered;
148
+ }
149
+ return total;
150
+ } finally {
151
+ this.syncing = false;
152
+ done();
153
+ }
154
+ }
155
+
156
+ /** [QCR PATCH 13] Settles once no sync runs and no turn waits for one. */
157
+ drain(): Promise<void> {
158
+ return this.idle;
159
+ }
160
+
161
+ private async syncOnce(branch: any[]): Promise<SyncBranchResult> {
162
+ // [QCR PATCH 13] A queued branch can be as long as the one sent before it
163
+ // and share none of its entries (a branch switch), so a length watermark
164
+ // would count its entries as sent. pi gives every entry an id: what was
165
+ // sent is remembered by id. The watermark below stays for entries without.
166
+ const ids = branch.map((entry) => entry?.id);
167
+ if (branch.length > 0 && ids.every((id) => typeof id === "string")) {
168
+ return this.syncById(branch, ids);
169
+ }
115
170
 
116
171
  const extracted = extractBranchCapturePayloads(branch, this.syncedEntryCount, this.config);
117
172
  if (extracted.resetWatermark) this.syncedEntryCount = 0;
@@ -135,6 +190,31 @@ export class SyncManager {
135
190
  return { added, tokens, allDelivered };
136
191
  }
137
192
 
193
+ /** [QCR PATCH 13] Sends each entry of `branch` not sent before, in order. */
194
+ private async syncById(branch: any[], ids: string[]): Promise<SyncBranchResult> {
195
+ // A restored watermark counts the branch's first entries as sent.
196
+ if (this.sentIds.size === 0) {
197
+ for (const id of ids.slice(0, this.syncedEntryCount)) this.sentIds.add(id);
198
+ }
199
+ let added = 0;
200
+ let tokens = 0;
201
+ let allDelivered = true;
202
+ for (const [i, entry] of branch.entries()) {
203
+ const id = ids[i]!;
204
+ if (this.sentIds.has(id)) continue;
205
+ for (const payload of extractBranchCapturePayloads([entry], 0, this.config).payloads) {
206
+ const result = await this.addPayload(payload);
207
+ if (!result.accepted) return { added, tokens, allDelivered };
208
+ added++;
209
+ tokens += estimatePayloadTokens(payload);
210
+ allDelivered = allDelivered && result.delivered;
211
+ }
212
+ this.sentIds.add(id);
213
+ }
214
+ this.syncedEntryCount = branch.length;
215
+ return { added, tokens, allDelivered };
216
+ }
217
+
138
218
  async addPayload(payload: any): Promise<AddPayloadResult> {
139
219
  if (!this.ovSessionId) return { accepted: false, delivered: false };
140
220
  const ok = await this.client.addMessagePayload(this.ovSessionId, payload);
@@ -43,6 +43,7 @@
43
43
  "0010-typed-lessons.patch",
44
44
  "0011-review-session-memory-boundary.patch",
45
45
  "0012-typecheck-against-the-real-pi.patch",
46
- "0013-createopenviking-factory.patch"
46
+ "0013-createopenviking-factory.patch",
47
+ "0014-one-turn-sync-in-flight.patch"
47
48
  ]
48
49
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quickchatai/quickreview",
3
- "version": "1.2.0",
3
+ "version": "1.3.2",
4
4
  "private": false,
5
5
  "//private": "The root is the published `qcr` CLI: the whole app (TUI, daemon, companion, every verb), installed with npm, pnpm or bun. It depends on the pi plugin (packages/pi-plugin) at its exact version and on pi at the exact pins below. Publishing is a release step of its own, never part of a build.",
6
6
  "description": "A 24/7 AI code reviewer for GitHub: a deterministic daemon driving pi review sessions, with an operator TUI and a local qcr review",
@@ -94,11 +94,10 @@
94
94
  "prepack": "npm run build",
95
95
  "watch": "node scripts/tsc-watch.mjs -b --watch --preserveWatchOutput",
96
96
  "dev": "bash scripts/dev-tui.sh",
97
- "onboard": "bash scripts/qcr onboard",
98
97
  "test": "vitest run",
99
98
  "//canary:pi": "The offline pi canaries (tests/pi-canary/): the real pi SDK at the pinned version on pi-ai's faux provider, so no network and no cost. They also run inside `npm test`; this is the named entry point CI requires whenever the pi dependencies change (a bump is gated by it).",
100
99
  "canary:pi": "vitest run tests/pi-canary",
101
- "//canary:pi:range": "The same canaries on each pi version the plugin's peer range (packages/pi-plugin/package.json) should hold, refusing one outside it: no argument runs the installed pi; `-- --install 1.4.x` fetches another first (the registry: CI's pi-range matrix runs it in a fresh checkout). Needs the build.",
100
+ "//canary:pi:range": "The same canaries on each pi version the plugin's peer range (packages/pi-plugin/package.json) should hold, refusing one outside it: no argument runs the installed pi; `-- --install 1.4.x` fetches another first (the registry). Run it by hand before a version joins the peer range; CI runs only the installed pi's canaries. Needs the build.",
102
101
  "canary:pi:range": "node scripts/pi-canary-range.mjs",
103
102
  "//lint": "Deliberately NOT chained into `test` or `build`: scripts/dev-tui.sh blocks on the build before the TUI paints (ruling 5), and an ESLint pass there is seconds of blank terminal. CI runs it as its own step.",
104
103
  "lint": "node --max-old-space-size=4096 node_modules/eslint/bin/eslint.js .",
@@ -116,7 +115,7 @@
116
115
  },
117
116
  "dependencies": {
118
117
  "@earendil-works/pi-ai": "1.4.0",
119
- "@quickchatai/pi-quickreview": "1.2.0",
118
+ "@quickchatai/pi-quickreview": "1.3.2",
120
119
  "@earendil-works/pi-coding-agent": "1.4.0",
121
120
  "better-sqlite3": "13.0.3",
122
121
  "smol-toml": "1.9.1"
@@ -132,6 +131,9 @@
132
131
  "vitest": "5.0.5",
133
132
  "yaml": "2.9.1"
134
133
  },
134
+ "overrides": {
135
+ "undici": "8.12.2"
136
+ },
135
137
  "qcrPi": {
136
138
  "//": "Named qcrPi, never `pi`: pi reads a package.json `pi` key as a package manifest, and this root is not a pi package (the plugin is packages/pi-plugin). pi and pi-ai are exact dependencies above (types for the extensions, and the VM boot's plain `npm ci` installs them beside the global CLI the image carries). This block is the mirror a bare checkout without config.toml reads: package and version must equal the dependencies, the lockfile, config.toml [pi], ansible group_vars and every Dockerfile ARG (tests/fences.test.ts [T12], tests/docker-daemon.test.ts). Lens sessions run through our own lens tools (src/engine/lens/), not a pi subagent extension, so no extra extension pin exists.",
137
139
  "package": "@earendil-works/pi-coding-agent",
@@ -94,20 +94,21 @@ the own-account route: it is skipped there.
94
94
  `onboard_go_live` are marked "after setup": setup finishes without them. Do
95
95
  not run them here unless the person asks. Say plainly that the installation
96
96
  stays in dry-run (every intended GitHub write is recorded, nothing is
97
- posted), and that going live is their decision later: re-run `qcr onboard`
98
- and ask for it once a review on the board has shown them what it says.
97
+ posted), and that going live is their decision later, from the home
98
+ (Advanced → "Dry-run or live"), once a review has shown them what it says.
99
99
 
100
100
  ## Finishing
101
101
 
102
102
  When every setup step reports done (the "after setup" ones need not be), say
103
103
  that `finish_and_exit` will re-verify the whole checklist one last time and,
104
- only if all green, close this session and open the TUI on the board: there they
105
- select a pull request and press `R` to review it now. If it returns red checks, that is the answer: explain them, fix
104
+ only if all green, close this session: the home then lists the repository's
105
+ pull requests, and they select one and press `alt+r` to review it now. If it
106
+ returns red checks, that is the answer: explain them, fix
106
107
  what can be fixed through the step tools, and try again. Never present
107
108
  onboarding as finished while anything is red, and never tell the operator to
108
109
  quit around a red check.
109
110
 
110
- Before `finish_and_exit`, pass on what comes next: the first review is `R` on
111
- a pull request on the board; then set up the repo with `qcr setup` (or `I` on
112
- the TUI board). Until then the reviewer knows nothing about this codebase. It is a
111
+ Before `finish_and_exit`, pass on what comes next: the first review is `alt+r`
112
+ on a pull request in the home; then tune the reviewer to the repository with
113
+ the home's "Tune to this repo" (`t` on the panel). Until then the reviewer knows nothing about this codebase. It is a
113
114
  recommendation, once — never start it from here.